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