1 /*
2 * Copyright (c) 2016-2021 Meltytech, LLC
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 #include "audioloudnessscopewidget.h"
19 #include <Logger.h>
20 #include <QVBoxLayout>
21 #include <QQmlEngine>
22 #include <QDir>
23 #include <QQuickWidget>
24 #include <QQuickItem>
25 #include <QPushButton>
26 #include <QToolButton>
27 #include <QMenu>
28 #include <QLabel>
29 #include <QTimer>
30 #include <MltProfile.h>
31 #include <math.h>
32 #include "qmltypes/qmlutilities.h"
33 #include "mltcontroller.h"
34 #include "settings.h"
35
onedec(double in)36 static double onedec( double in )
37 {
38 return round( in * 10.0 ) / 10.0;
39 }
40
AudioLoudnessScopeWidget()41 AudioLoudnessScopeWidget::AudioLoudnessScopeWidget()
42 : ScopeWidget("AudioLoudnessMeter")
43 , m_loudnessFilter(0)
44 , m_peak(-100)
45 , m_true_peak(-100)
46 , m_newData(false)
47 , m_orientation((Qt::Orientation)-1)
48 , m_qview(new QQuickWidget(QmlUtilities::sharedEngine(), this))
49 , m_timeLabel(new QLabel(this))
50 {
51 LOG_DEBUG() << "begin";
52 m_loudnessFilter = new Mlt::Filter(MLT.profile(), "loudness_meter");
53 m_loudnessFilter->set("calc_program", Settings.loudnessScopeShowMeter("integrated"));
54 m_loudnessFilter->set("calc_shortterm", Settings.loudnessScopeShowMeter("shortterm"));
55 m_loudnessFilter->set("calc_momentary", Settings.loudnessScopeShowMeter("momentary"));
56 m_loudnessFilter->set("calc_range", Settings.loudnessScopeShowMeter("range"));
57 m_loudnessFilter->set("calc_peak", Settings.loudnessScopeShowMeter("peak"));
58 m_loudnessFilter->set("calc_true_peak", Settings.loudnessScopeShowMeter("truepeak"));
59
60 setAutoFillBackground(true);
61
62 // Use a timer to update the meters for two reasons:
63 // 1) The spec requires 10Hz updates
64 // 2) Minimize QML GUI updates
65 m_timer = new QTimer(this);
66 connect(m_timer, SIGNAL(timeout()), this, SLOT(updateMeters()));
67 m_timer->start(100);
68
69 m_qview->setFocusPolicy(Qt::StrongFocus);
70 QmlUtilities::setCommonProperties(m_qview->rootContext());
71
72 QVBoxLayout* vlayout = new QVBoxLayout(this);
73 vlayout->setContentsMargins(4, 4, 4, 4);
74 vlayout->addWidget(m_qview);
75
76 QHBoxLayout* hlayout = new QHBoxLayout();
77 vlayout->addLayout(hlayout);
78
79 // Create config menu
80 QMenu* configMenu = new QMenu(this);
81 QAction* action;
82 action = configMenu->addAction(tr("Momentary Loudness"), this, SLOT(onMomentaryToggled(bool)));
83 action->setCheckable(true);
84 action->setChecked(Settings.loudnessScopeShowMeter("momentary"));
85 action = configMenu->addAction(tr("Short Term Loudness"), this, SLOT(onShorttermToggled(bool)));
86 action->setCheckable(true);
87 action->setChecked(Settings.loudnessScopeShowMeter("shortterm"));
88 action = configMenu->addAction(tr("Integrated Loudness"), this, SLOT(onIntegratedToggled(bool)));
89 action->setCheckable(true);
90 action->setChecked(Settings.loudnessScopeShowMeter("integrated"));
91 action = configMenu->addAction(tr("Loudness Range"), this, SLOT(onRangeToggled(bool)));
92 action->setCheckable(true);
93 action->setChecked(Settings.loudnessScopeShowMeter("range"));
94 action = configMenu->addAction(tr("Peak"), this, SLOT(onPeakToggled(bool)));
95 action->setCheckable(true);
96 action->setChecked(Settings.loudnessScopeShowMeter("peak"));
97 action = configMenu->addAction(tr("True Peak"), this, SLOT(onTruePeakToggled(bool)));
98 action->setCheckable(true);
99 action->setChecked(Settings.loudnessScopeShowMeter("truepeak"));
100
101 // Add config button
102 QToolButton* configButton = new QToolButton(this);
103 configButton->setToolTip(tr("Configure Graphs"));
104 configButton->setIcon(QIcon::fromTheme("show-menu", QIcon(":/icons/oxygen/32x32/actions/show-menu.png")));
105 configButton->setPopupMode(QToolButton::InstantPopup);
106 configButton->setMenu(configMenu);
107 hlayout->addWidget(configButton);
108
109 // Add reset button
110 QPushButton* resetButton = new QPushButton(tr("Reset"), this);
111 resetButton->setToolTip(tr("Reset the measurement."));
112 resetButton->setCheckable(false);
113 resetButton->setMaximumWidth(100);
114 hlayout->addWidget(resetButton);
115 connect(resetButton, SIGNAL(clicked()), this, SLOT(onResetButtonClicked()));
116
117 // Add time label
118 m_timeLabel->setToolTip(tr("Time Since Reset"));
119 m_timeLabel->setText("00:00:00:00");
120 m_timeLabel->setFixedSize(this->fontMetrics().width("HH:MM:SS:MM"), this->fontMetrics().height());
121 hlayout->addWidget(m_timeLabel);
122
123 hlayout->addStretch();
124
125 connect(m_qview->quickWindow(), SIGNAL(sceneGraphInitialized()), SLOT(resetQview()));
126
127 LOG_DEBUG() << "end";
128 }
129
~AudioLoudnessScopeWidget()130 AudioLoudnessScopeWidget::~AudioLoudnessScopeWidget()
131 {
132 m_timer->stop();
133 delete m_loudnessFilter;
134 }
135
refreshScope(const QSize &,bool)136 void AudioLoudnessScopeWidget::refreshScope(const QSize& /*size*/, bool /*full*/)
137 {
138 SharedFrame sFrame;
139 while (m_queue.count() > 0) {
140 sFrame = m_queue.pop();
141 if (sFrame.is_valid() && sFrame.get_audio_samples() > 0) {
142 mlt_audio_format format = mlt_audio_f32le;
143 int channels = sFrame.get_audio_channels();
144 int frequency = sFrame.get_audio_frequency();
145 int samples = sFrame.get_audio_samples();
146 if (channels && frequency && samples) {
147 Mlt::Frame mFrame = sFrame.clone(true, false, false);
148 m_loudnessFilter->process(mFrame);
149 mFrame.get_audio(format, frequency, channels, samples);
150 if( m_peak < m_loudnessFilter->get_double("peak") ) {
151 m_peak = m_loudnessFilter->get_double("peak");
152 }
153 if( m_true_peak < m_loudnessFilter->get_double("true_peak") ) {
154 m_true_peak = m_loudnessFilter->get_double("true_peak");
155 }
156 m_newData = true;
157 }
158 }
159 }
160
161 // Update the time with every frame.
162 m_timeLabel->setText( m_loudnessFilter->get_time( "frames_processed" ) );
163 }
164
getTitle()165 QString AudioLoudnessScopeWidget::getTitle()
166 {
167 return tr("Audio Loudness");
168 }
169
setOrientation(Qt::Orientation orientation)170 void AudioLoudnessScopeWidget::setOrientation(Qt::Orientation orientation)
171 {
172 setOrientation(orientation, false);
173 }
174
setOrientation(Qt::Orientation orientation,bool force)175 void AudioLoudnessScopeWidget::setOrientation(Qt::Orientation orientation, bool force)
176 {
177 if (force || orientation != m_orientation) {
178 if (orientation == Qt::Vertical) {
179 // Calculate the minimum width
180 int x = 0;
181 const int meterWidth = 54;
182 if (Settings.loudnessScopeShowMeter("momentary")) x += meterWidth;
183 if (Settings.loudnessScopeShowMeter("shortterm")) x += meterWidth;
184 if (Settings.loudnessScopeShowMeter("integrated")) x += meterWidth;
185 if (Settings.loudnessScopeShowMeter("range")) x += meterWidth;
186 if (Settings.loudnessScopeShowMeter("peak")) x += meterWidth;
187 if (Settings.loudnessScopeShowMeter("truepeak")) x += meterWidth;
188 x = std::max(x, 200);
189 setMinimumSize(x, 250);
190 setMaximumSize(x, 500);
191 } else {
192 // Calculate the minimum height
193 int y = 32;
194 const int meterHeight = 47;
195 if (Settings.loudnessScopeShowMeter("momentary")) y += meterHeight;
196 if (Settings.loudnessScopeShowMeter("shortterm")) y += meterHeight;
197 if (Settings.loudnessScopeShowMeter("integrated")) y += meterHeight;
198 if (Settings.loudnessScopeShowMeter("range")) y += meterHeight;
199 if (Settings.loudnessScopeShowMeter("peak")) y += meterHeight;
200 if (Settings.loudnessScopeShowMeter("truepeak")) y += meterHeight;
201 y = std::max(y, 80);
202 setMinimumSize(250, y);
203 setMaximumSize(500, y);
204 }
205 updateGeometry();
206 m_orientation = orientation;
207 if (m_qview->status() != QQuickWidget::Null) {
208 m_qview->rootObject()->setProperty("orientation", m_orientation);
209 }
210 }
211 }
212
onResetButtonClicked()213 void AudioLoudnessScopeWidget::onResetButtonClicked()
214 {
215 m_loudnessFilter->set("reset", 1);
216 m_timeLabel->setText( "00:00:00:00" );
217 setOrientation(m_orientation, true);
218 resetQview();
219 }
220
onIntegratedToggled(bool checked)221 void AudioLoudnessScopeWidget::onIntegratedToggled(bool checked)
222 {
223 m_loudnessFilter->set("calc_program", checked);
224 Settings.setLoudnessScopeShowMeter("integrated", checked);
225 setOrientation(m_orientation, true);
226 resetQview();
227 }
228
onShorttermToggled(bool checked)229 void AudioLoudnessScopeWidget::onShorttermToggled(bool checked)
230 {
231 m_loudnessFilter->set("calc_shortterm", checked);
232 Settings.setLoudnessScopeShowMeter("shortterm", checked);
233 setOrientation(m_orientation, true);
234 resetQview();
235 }
236
onMomentaryToggled(bool checked)237 void AudioLoudnessScopeWidget::onMomentaryToggled(bool checked)
238 {
239 m_loudnessFilter->set("calc_momentary", checked);
240 Settings.setLoudnessScopeShowMeter("momentary", checked);
241 setOrientation(m_orientation, true);
242 resetQview();
243 }
244
onRangeToggled(bool checked)245 void AudioLoudnessScopeWidget::onRangeToggled(bool checked)
246 {
247 m_loudnessFilter->set("calc_range", checked);
248 Settings.setLoudnessScopeShowMeter("range", checked);
249 setOrientation(m_orientation, true);
250 resetQview();
251 }
252
onPeakToggled(bool checked)253 void AudioLoudnessScopeWidget::onPeakToggled(bool checked)
254 {
255 m_loudnessFilter->set("calc_peak", checked);
256 Settings.setLoudnessScopeShowMeter("peak", checked);
257 setOrientation(m_orientation, true);
258 resetQview();
259 }
260
onTruePeakToggled(bool checked)261 void AudioLoudnessScopeWidget::onTruePeakToggled(bool checked)
262 {
263 m_loudnessFilter->set("calc_true_peak", checked);
264 Settings.setLoudnessScopeShowMeter("truepeak", checked);
265 setOrientation(m_orientation, true);
266 resetQview();
267 }
268
updateMeters(void)269 void AudioLoudnessScopeWidget::updateMeters(void)
270 {
271 if (!m_newData) return;
272 if (m_loudnessFilter->get_int("calc_program") )
273 m_qview->rootObject()->setProperty("integrated", onedec(m_loudnessFilter->get_double("program")));
274 if (m_loudnessFilter->get_int("calc_shortterm") )
275 m_qview->rootObject()->setProperty("shortterm", onedec(m_loudnessFilter->get_double("shortterm")));
276 if (m_loudnessFilter->get_int("calc_momentary") )
277 m_qview->rootObject()->setProperty("momentary", onedec(m_loudnessFilter->get_double("momentary")));
278 if (m_loudnessFilter->get_int("calc_range") )
279 m_qview->rootObject()->setProperty("range", onedec(m_loudnessFilter->get_double("range")));
280 if (m_loudnessFilter->get_int("calc_peak") )
281 m_qview->rootObject()->setProperty("peak", onedec(m_peak));
282 if (m_loudnessFilter->get_int("calc_true_peak") )
283 m_qview->rootObject()->setProperty("truePeak", onedec(m_true_peak));
284 m_peak = -100;
285 m_true_peak = -100;
286 m_newData = false;
287 }
288
event(QEvent * event)289 bool AudioLoudnessScopeWidget::event(QEvent *event)
290 {
291 bool result = ScopeWidget::event(event);
292 if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange) {
293 resetQview();
294 }
295 return result;
296 }
297
resetQview()298 void AudioLoudnessScopeWidget::resetQview()
299 {
300 LOG_DEBUG() << "begin";
301 if (m_qview->status() != QQuickWidget::Null) {
302 m_qview->setSource(QUrl(""));
303 }
304
305 QDir viewPath = QmlUtilities::qmlDir();
306 viewPath.cd("scopes");
307 viewPath.cd("audioloudness");
308 m_qview->engine()->addImportPath(viewPath.path());
309
310 QDir modulePath = QmlUtilities::qmlDir();
311 modulePath.cd("modules");
312 m_qview->engine()->addImportPath(modulePath.path());
313
314 m_qview->setResizeMode(QQuickWidget::SizeRootObjectToView);
315 m_qview->quickWindow()->setColor(palette().window().color());
316 QUrl source = QUrl::fromLocalFile(viewPath.absoluteFilePath("audioloudnessscope.qml"));
317 m_qview->setSource(source);
318
319 m_qview->rootObject()->setProperty("enableIntegrated", Settings.loudnessScopeShowMeter("integrated"));
320 m_qview->rootObject()->setProperty("enableShortterm", Settings.loudnessScopeShowMeter("shortterm"));
321 m_qview->rootObject()->setProperty("enableMomentary", Settings.loudnessScopeShowMeter("momentary"));
322 m_qview->rootObject()->setProperty("enableRange", Settings.loudnessScopeShowMeter("range"));
323 m_qview->rootObject()->setProperty("enablePeak", Settings.loudnessScopeShowMeter("peak"));
324 m_qview->rootObject()->setProperty("enableTruePeak", Settings.loudnessScopeShowMeter("truepeak"));
325 m_qview->rootObject()->setProperty("orientation", m_orientation);
326 }
327