1 /*
2  * Copyright (C) 2020 Robin Gareus <robin@gareus.org>
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 2 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 along
15  * with this program; if not, write to the Free Software Foundation, Inc.,
16  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17  */
18 
19 #include <gtkmm/alignment.h>
20 #include <gtkmm/label.h>
21 #include <gtkmm/stock.h>
22 
23 #include "pbd/unwind.h"
24 
25 #include "ardour/dB.h"
26 #include "ardour/plugin_insert.h"
27 #include "ardour/plugin_manager.h"
28 #include "ardour/processor.h"
29 #include "ardour/session.h"
30 
31 #include "ardour/export_channel_configuration.h"
32 #include "ardour/export_filename.h"
33 #include "ardour/export_format_base.h"
34 #include "ardour/export_format_specification.h"
35 #include "ardour/export_handler.h"
36 #include "ardour/export_status.h"
37 #include "ardour/export_timespan.h"
38 
39 #include "gtkmm2ext/menu_elems.h"
40 #include "gtkmm2ext/utils.h"
41 
42 #include "widgets/ardour_spacer.h"
43 #include "widgets/prompter.h"
44 #include "widgets/tooltips.h"
45 
46 #include "ardour_message.h"
47 #include "export_analysis_graphs.h"
48 #include "export_report.h"
49 #include "loudness_dialog.h"
50 #include "ui_config.h"
51 
52 #include "pbd/i18n.h"
53 
54 using namespace Gtk;
55 using namespace ARDOUR;
56 using namespace ArdourWidgets;
57 
58 bool LoudnessDialog::_first_time = true;
59 CLoudnessPreset LoudnessDialog::_last_preset;
60 
LoudnessDialog(Session * s,AudioRange const & ar,bool as)61 LoudnessDialog::LoudnessDialog (Session* s, AudioRange const& ar, bool as)
62 	: ArdourDialog (as ? _("Loudness Assistant") : _("Loudness Analyzer and Normalizer"))
63 	, _lp (false)
64 	, _session (s)
65 	, _range (ar)
66 	, _status (s->get_export_status ())
67 	, _autostart (as)
68 	, _conformity_frame (_("Conformity Analysis (with gain applied)"))
69 	, _save_preset (_("Save"))
70 	, _remove_preset (_("Remove"))
71 	, _dbfs_btn (_("Peak:"), ArdourButton::led_default_elements, true)
72 	, _dbtp_btn (_("True Peak:"), ArdourButton::led_default_elements, true)
73 	, _lufs_i_btn (_("Integrated Loudness:"), ArdourButton::led_default_elements, true)
74 	, _lufs_s_btn (_("Max. Short Loudness:"), ArdourButton::led_default_elements, true)
75 	, _lufs_m_btn (_("Max. Momentary Loudness:"), ArdourButton::led_default_elements, true)
76 	, _rt_analysis_button (_("Realtime"), ArdourButton::led_default_elements, true)
77 	, _start_analysis_button (_("Analyze"))
78 	, _show_report_button (_("Analysis Report"))
79 	, _custom_pos_button (_("Custom Amplifier Position"), ArdourButton::led_default_elements, true)
80 	, _dbfs_adjustment (0.00, -10.00, 0.00, 0.1, 0.2)
81 	, _dbtp_adjustment (-1.0, -10.00, 0.00, 0.1, 0.2)
82 	, _lufs_i_adjustment (-23.0, -90.00, 0.00, 0.5, 1.0)
83 	, _lufs_s_adjustment (-20.0, -90.00, 0.00, 0.5, 1.0)
84 	, _lufs_m_adjustment (-17.0, -90.00, 0.00, 0.5, 1.0)
85 	, _dbfs_spinbutton (_dbfs_adjustment, 0.1, 1)
86 	, _dbtp_spinbutton (_dbtp_adjustment, 0.1, 1)
87 	, _lufs_i_spinbutton (_lufs_i_adjustment, 0.1, 1)
88 	, _lufs_s_spinbutton (_lufs_s_adjustment, 0.1, 1)
89 	, _lufs_m_spinbutton (_lufs_m_adjustment, 0.1, 1)
90 	, _gain_out (0)
91 	, _gain_norm (0)
92 	, _ignore_preset (false)
93 	, _ignore_change (false)
94 {
95 	_preset = _lp[0];
96 	if (_first_time) {
97 		_first_time = false;
98 		_last_preset = _lp[0];
99 	}
100 
101 	_preset = _last_preset;
102 
103 	/* Dialog can be displayed from the mixer, override global transient_parent */
104 	unset_transient_for ();
105 
106 	/* query initial gain */
107 	_gain_out = accurate_coefficient_to_dB (_session->master_volume ()->get_value ());
108 
109 	/* setup styles */
110 	_start_analysis_button.set_name ("generic button");
111 	_rt_analysis_button.set_name ("generic button");
112 	_show_report_button.set_name ("generic button");
113 	_custom_pos_button.set_name ("generic button");
114 
115 	_custom_pos_button.set_active (!_session->master_out()->volume_applies_to_output ());
116 
117 	GtkRequisition req = _start_analysis_button.size_request ();
118 	_start_analysis_button.set_size_request (-1, req.height * 1.1);
119 	_rt_analysis_button.set_size_request (-1, req.height * 1.1);
120 
121 	_save_preset.set_name ("generic button");
122 	_remove_preset.set_name ("generic button");
123 
124 	_dbfs_btn.set_name ("generic button");
125 	_dbtp_btn.set_name ("generic button");
126 	_lufs_i_btn.set_name ("generic button");
127 	_lufs_s_btn.set_name ("generic button");
128 	_lufs_m_btn.set_name ("generic button");
129 
130 	_dbfs_btn.set_led_left (true);
131 	_dbtp_btn.set_led_left (true);
132 	_lufs_i_btn.set_led_left (true);
133 	_lufs_s_btn.set_led_left (true);
134 	_lufs_m_btn.set_led_left (true);
135 
136 	_preset_dropdown.set_can_focus (true);
137 	_start_analysis_button.set_can_focus (true);
138 	_rt_analysis_button.set_can_focus (true);
139 	_show_report_button.set_can_focus (true);
140 	_custom_pos_button.set_can_focus (true);
141 	_save_preset.set_can_focus (true);
142 	_remove_preset.set_can_focus (true);
143 	_dbfs_btn.set_can_focus (true);
144 	_dbtp_btn.set_can_focus (true);
145 	_lufs_i_btn.set_can_focus (true);
146 	_lufs_s_btn.set_can_focus (true);
147 	_lufs_m_btn.set_can_focus (true);
148 
149 	_dbfs_label.modify_font (UIConfiguration::instance ().get_NormalMonospaceFont ());
150 	_dbtp_label.modify_font (UIConfiguration::instance ().get_NormalMonospaceFont ());
151 	_lufs_i_label.modify_font (UIConfiguration::instance ().get_NormalMonospaceFont ());
152 	_lufs_s_label.modify_font (UIConfiguration::instance ().get_NormalMonospaceFont ());
153 	_lufs_m_label.modify_font (UIConfiguration::instance ().get_NormalMonospaceFont ());
154 
155 	_delta_dbfs_label.modify_font (UIConfiguration::instance ().get_NormalMonospaceFont ());
156 	_delta_dbtp_label.modify_font (UIConfiguration::instance ().get_NormalMonospaceFont ());
157 	_delta_lufs_i_label.modify_font (UIConfiguration::instance ().get_NormalMonospaceFont ());
158 	_delta_lufs_s_label.modify_font (UIConfiguration::instance ().get_NormalMonospaceFont ());
159 	_delta_lufs_m_label.modify_font (UIConfiguration::instance ().get_NormalMonospaceFont ());
160 
161 	_gain_out_label.modify_font (UIConfiguration::instance ().get_NormalMonospaceFont ());
162 	_gain_norm_label.modify_font (UIConfiguration::instance ().get_NormalMonospaceFont ());
163 	_gain_total_label.modify_font (UIConfiguration::instance ().get_NormalMonospaceFont ());
164 	_gain_exceeds_label.modify_font (UIConfiguration::instance ().get_NormalFont ());
165 
166 	/* result display layout */
167 #define ROW row, row + 1
168 
169 	int row = 0;
170 	Label* l;
171 	Table* t = manage (new Table (11, 8, false));
172 	t->set_spacings (4);
173 
174 	l = manage (new Label (_("Preset:"), ALIGN_LEFT));
175 	t->attach (*l, 0, 1, ROW, SHRINK | FILL);
176 	t->attach (_preset_dropdown, 1, 3, ROW);
177 
178 	t->attach (_save_preset, 3, 4, ROW, SHRINK | FILL);
179 	t->attach (_remove_preset, 4, 5, ROW, SHRINK | FILL);
180 
181 	/* horiz space */
182 	l = manage (new Label (" ", ALIGN_LEFT));
183 	t->attach (*l, 5, 6, ROW, SHRINK, SHRINK, 6, 0);
184 
185 	l = manage (new Label (" ", ALIGN_LEFT));
186 	t->attach (*l, 6, 7, ROW);
187 
188 	t->attach (_show_report_button, 7, 8, ROW, SHRINK | FILL);
189 
190 	++row;
191 	l = manage (new Label (_("<b>Target</b>"), ALIGN_CENTER));
192 	l->set_use_markup (true);
193 	t->attach (*l, 2, 3, ROW);
194 	l = manage (new Label (_("<b>Measured</b>"), ALIGN_CENTER));
195 	l->set_use_markup (true);
196 	t->attach (*l, 3, 4, ROW);
197 	l = manage (new Label (_("<b>Delta</b>"), ALIGN_CENTER));
198 	l->set_use_markup (true);
199 	t->attach (*l, 4, 5, ROW);
200 
201 	row = 2;
202 	t->attach (_dbfs_btn,   0, 2, ROW); ++row;
203 	t->attach (_dbtp_btn,   0, 2, ROW); ++row;
204 	t->attach (_lufs_i_btn, 0, 2, ROW); ++row;
205 	t->attach (_lufs_s_btn, 0, 2, ROW); ++row;
206 	t->attach (_lufs_m_btn, 0, 2, ROW); ++row;
207 
208 	++row; // spacer
209 
210 	l = manage (new Label (_("Gain to normalize:"), ALIGN_LEFT));
211 	t->attach (*l, 0, 2, ROW); ++row;
212 	l = manage (new Label (_("Previous output gain:"), ALIGN_LEFT));
213 	t->attach (*l, 0, 2, ROW); ++row;
214 
215 	l = manage (new Label (_("Total gain:"), ALIGN_LEFT));
216 	t->attach (*l, 0, 2, ROW);
217 
218 	row = 2;
219 	t->attach (_dbfs_spinbutton,    2, 3, ROW, EXPAND|FILL, EXPAND|FILL, 8, 0); ++row;
220 	t->attach (_dbtp_spinbutton,    2, 3, ROW, EXPAND|FILL, EXPAND|FILL, 8, 0); ++row;
221 	t->attach (_lufs_i_spinbutton,  2, 3, ROW, EXPAND|FILL, EXPAND|FILL, 8, 0); ++row;
222 	t->attach (_lufs_s_spinbutton,  2, 3, ROW, EXPAND|FILL, EXPAND|FILL, 8, 0); ++row;
223 	t->attach (_lufs_m_spinbutton,  2, 3, ROW, EXPAND|FILL, EXPAND|FILL, 8, 0); ++row;
224 
225 	row = 2;
226 	t->attach (_dbfs_label,         3, 4, ROW); ++row;
227 	t->attach (_dbtp_label,         3, 4, ROW); ++row;
228 	t->attach (_lufs_i_label,       3, 4, ROW); ++row;
229 	t->attach (_lufs_s_label,       3, 4, ROW); ++row;
230 	t->attach (_lufs_m_label,       3, 4, ROW); ++row;
231 
232 	row = 2;
233 	t->attach (_delta_dbfs_label,   4, 5, ROW); ++row;
234 	t->attach (_delta_dbtp_label,   4, 5, ROW); ++row;
235 	t->attach (_delta_lufs_i_label, 4, 5, ROW); ++row;
236 	t->attach (_delta_lufs_s_label, 4, 5, ROW); ++row;
237 	t->attach (_delta_lufs_m_label, 4, 5, ROW); ++row;
238 
239 	ArdourHSpacer* spc = manage (new ArdourHSpacer (1.0));
240 	t->attach (*spc,                3, 5, ROW); ++row;
241 	t->attach (_gain_norm_label,    4, 5, ROW); ++row;
242 	t->attach (_gain_out_label,     4, 5, ROW); ++row;
243 	t->attach (_gain_exceeds_label, 3, 4, ROW);
244 	t->attach (_gain_total_label,   4, 5, ROW);
245 	t->attach (_custom_pos_button,  7, 8, row - 1, row + 1, SHRINK | FILL, SHRINK);
246 	t->attach (_conformity_frame,   6, 8, 1,       row - 3, SHRINK | FILL, EXPAND | FILL, 0, 0);
247 
248 	set_tooltip (_custom_pos_button,
249 		_("<b>When enabled</b> an amplifier processor is used to apply the gain. "
250 		  "This allows for custom positioning of the gain-stage in the master-bus' signal flow, "
251 		  "potentially followed by a limiter to conform to both loudness and peak requirements. "
252 		  "Depending on limiter settings or DSP after the gain-stage, repeat loudness measurements may produce different results.\n"
253 		  "<b>When disabled</b>, the gain is applied directly to the output of the master-bus. This results in an efficient and reliable volume adjustment."
254 		 ));
255 
256 	_dbfs_label.set_alignment (ALIGN_RIGHT);
257 	_dbtp_label.set_alignment (ALIGN_RIGHT);
258 	_lufs_i_label.set_alignment (ALIGN_RIGHT);
259 	_lufs_s_label.set_alignment (ALIGN_RIGHT);
260 	_lufs_m_label.set_alignment (ALIGN_RIGHT);
261 
262 	_delta_dbfs_label.set_alignment (ALIGN_RIGHT);
263 	_delta_dbtp_label.set_alignment (ALIGN_RIGHT);
264 	_delta_lufs_i_label.set_alignment (ALIGN_RIGHT);
265 	_delta_lufs_s_label.set_alignment (ALIGN_RIGHT);
266 	_delta_lufs_m_label.set_alignment (ALIGN_RIGHT);
267 
268 	_gain_norm_label.set_alignment (ALIGN_RIGHT);
269 	_gain_out_label.set_alignment (ALIGN_RIGHT);
270 	_gain_total_label.set_alignment (ALIGN_RIGHT);
271 	_gain_exceeds_label.set_alignment (ALIGN_RIGHT);
272 
273 	HBox* hb = manage (new (HBox));
274 	hb->pack_start (_loudness_graph, true, false);
275 
276 	_result_box.pack_start (*hb, false, false, 0);
277 	_result_box.pack_start (*t, false, false, 6);
278 
279 	/* analysis progress layout */
280 	_progress_box.pack_start (_progress_bar, false, false, 6);
281 
282 	/* setup and info layout */
283 	t = manage (new Table (2, 3, false));
284 	t->set_spacings (4);
285 	l = manage (new Label ());
286 	l->set_markup (_("<b>Loudness Analysis</b>\n"));
287 	l->set_alignment (ALIGN_LEFT, ALIGN_TOP);
288 	t->attach (*l, 0, 1, 0, 1, EXPAND | FILL, FILL, 8, 2);
289 
290 	l = manage (new Label ());
291 	l->set_line_wrap ();
292 	l->set_alignment (ALIGN_LEFT, ALIGN_TOP);
293 	l->set_markup (_(
294 	    "This allows the user to analyze and conform the loudness of the signal at the master-bus "
295 	    "output of the complete session, as it would be exported. "
296 	    "When using this feature, remember to disable normalization in the session export profile."));
297 	t->attach (*l, 0, 1, 1, 2, EXPAND | FILL, FILL, 8, 2);
298 
299 	l = manage (new Label ());
300 	l->set_line_wrap ();
301 	l->set_alignment (ALIGN_LEFT, ALIGN_TOP);
302 	l->set_markup (_(
303 	    "By default, a faster-than-realtime export is used to assess the loudness of the "
304 	    "session. If any outboard gear is used, a <i>realtime</i> export is available, to "
305 	    "play at normal speed."));
306 	t->attach (*l, 0, 1, 2, 3, EXPAND | FILL, FILL, 8, 2);
307 
308 	Alignment* align = manage (new Alignment (0, 0, 1, 0));
309 	align->add (_start_analysis_button);
310 	t->attach (*align, 1, 2, 1, 2, FILL, FILL, 2, 0);
311 
312 	align = manage (new Alignment (0, 0, 1, 0));
313 	align->add (_rt_analysis_button);
314 	t->attach (*align, 1, 2, 2, 3, FILL, FILL, 2, 0);
315 
316 	_setup_box.pack_start (*t, false, false, 6);
317 
318 	/* global layout */
319 	get_vbox ()->pack_start (_setup_box);
320 	get_vbox ()->pack_start (_progress_box);
321 	get_vbox ()->pack_start (_result_box);
322 
323 	_progress_box.set_size_request (400, -1);
324 
325 	_ok_button     = add_button (Stock::APPLY, RESPONSE_APPLY);
326 	_cancel_button = add_button (Stock::CANCEL, RESPONSE_CANCEL);
327 
328 	/* fill in presets */
329 	for (size_t i = 0; i < _lp.n_presets (); ++i) {
330 		using namespace Gtkmm2ext;
331 		_preset_dropdown.AddMenuElem (MenuElemNoMnemonic (_lp[i].label, sigc::bind (sigc::mem_fun (*this, &LoudnessDialog::load_preset), i)));
332 	}
333 
334 	apply_preset ();
335 
336 	if (!_lp.find_preset (_preset)) {
337 		_preset.label = _("Custom");
338 	}
339 
340 	check_preset ();
341 
342 	_gain_out_label.set_text (string_compose (_("%1 dB"), std::setprecision (2), std::showpos, std::fixed, _gain_out));
343 
344 	/* setup graph */
345 	_loudness_graph.signal_size_request ().connect (sigc::mem_fun (*this, &LoudnessDialog::graph_size_request));
346 	_loudness_graph.signal_expose_event ().connect (sigc::mem_fun (*this, &LoudnessDialog::graph_expose_event));
347 
348 	/* connect signals */
349 	_cancel_button->signal_clicked ().connect (sigc::mem_fun (this, &LoudnessDialog::cancel_analysis));
350 	_dbfs_spinbutton.signal_value_changed ().connect (mem_fun (*this, &LoudnessDialog::update_settings));
351 	_dbtp_spinbutton.signal_value_changed ().connect (mem_fun (*this, &LoudnessDialog::update_settings));
352 	_lufs_i_spinbutton.signal_value_changed ().connect (mem_fun (*this, &LoudnessDialog::update_settings));
353 	_lufs_s_spinbutton.signal_value_changed ().connect (mem_fun (*this, &LoudnessDialog::update_settings));
354 	_lufs_m_spinbutton.signal_value_changed ().connect (mem_fun (*this, &LoudnessDialog::update_settings));
355 	_save_preset.signal_clicked.connect (mem_fun (*this, &LoudnessDialog::save_preset));
356 	_remove_preset.signal_clicked.connect (mem_fun (*this, &LoudnessDialog::remove_preset));
357 	_show_report_button.signal_clicked.connect (mem_fun (*this, &LoudnessDialog::display_report));
358 	_start_analysis_button.signal_clicked.connect (mem_fun (*this, &LoudnessDialog::start_analysis));
359 	_dbfs_btn.signal_clicked.connect (mem_fun (*this, &LoudnessDialog::update_settings));
360 	_dbtp_btn.signal_clicked.connect (mem_fun (*this, &LoudnessDialog::update_settings));
361 	_lufs_i_btn.signal_clicked.connect (mem_fun (*this, &LoudnessDialog::update_settings));
362 	_lufs_s_btn.signal_clicked.connect (mem_fun (*this, &LoudnessDialog::update_settings));
363 	_lufs_m_btn.signal_clicked.connect (mem_fun (*this, &LoudnessDialog::update_settings));
364 
365 	_ok_button->set_sensitive (false);
366 	_show_report_button.set_sensitive (false);
367 
368 	show_all_children ();
369 
370 	_result_box.hide ();
371 	_progress_box.hide ();
372 }
373 
374 void
cancel_analysis()375 LoudnessDialog::cancel_analysis ()
376 {
377 	if (_status->running ()) {
378 		_status->abort ();
379 	}
380 }
381 
382 void
start_analysis()383 LoudnessDialog::start_analysis ()
384 {
385 	if (0 == analyze ()) {
386 		display_results ();
387 	} else {
388 		_setup_box.show ();
389 	}
390 }
391 
392 bool
on_delete_event(GdkEventAny * ev)393 LoudnessDialog::on_delete_event (GdkEventAny* ev)
394 {
395 	cancel_analysis ();
396 	return ArdourDialog::on_delete_event (ev);
397 }
398 
399 int
run()400 LoudnessDialog::run ()
401 {
402 	if (_autostart) {
403 		show ();
404 		if (0 == analyze ()) {
405 			display_results ();
406 		} else {
407 			return RESPONSE_CANCEL;
408 		}
409 	}
410 
411 	int const r = ArdourDialog::run ();
412 	cancel_analysis ();
413 
414 	if (r == RESPONSE_APPLY) {
415 		_session->master_volume ()->set_value (dB_to_coefficient (gain_db ()), PBD::Controllable::NoGroup);
416 		_session->master_out()->set_volume_applies_to_output (!_custom_pos_button.get_active ());
417 
418 		_last_preset = _preset;
419 	}
420 
421 	return r;
422 }
423 
424 gint
progress_timeout()425 LoudnessDialog::progress_timeout ()
426 {
427 	float progress = ((float)_status->processed_samples_current_timespan) / _status->total_samples_current_timespan;
428 	_progress_bar.set_text ("Analyzing");
429 	_progress_bar.set_fraction (progress);
430 	return true;
431 }
432 
433 int
analyze()434 LoudnessDialog::analyze ()
435 {
436 	/* These are ensured in Editor::measure_master_loudness () */
437 	assert (_session->master_out ());
438 	assert (_session->master_volume ());
439 	assert (_session->master_out ()->output ());
440 	assert (_session->master_out ()->output ()->n_ports ().n_audio () == 2);
441 	assert (_range.start < _range.end);
442 
443 
444 	ExportTimespanPtr tsp = _session->get_export_handler ()->add_timespan ();
445 
446 	boost::shared_ptr<ExportChannelConfiguration> ccp = _session->get_export_handler ()->add_channel_config ();
447 	boost::shared_ptr<ARDOUR::ExportFilename>     fnp = _session->get_export_handler ()->add_filename ();
448 	boost::shared_ptr<ExportFormatSpecification>  fmp = _session->get_export_handler ()->add_format ();
449 
450 	/* setup format */
451 	fmp->set_tag (false);
452 	fmp->set_sample_format (ExportFormatBase::SF_Float);
453 	fmp->set_sample_rate (ExportFormatBase::SR_Session);
454 	fmp->set_format_id (ExportFormatBase::F_None);
455 	fmp->set_type (ExportFormatBase::T_None);
456 	fmp->set_extension ("wav");
457 	fmp->set_soundcloud_upload (false);
458 	fmp->set_analyse (true);
459 
460 	/* setup range */
461 	tsp->set_range (_range.start, _range.end);
462 	tsp->set_range_id ("selection");
463 	tsp->set_realtime (_rt_analysis_button.get_active ());
464 	tsp->set_name ("master");
465 
466 	/* setup channels, use master out */
467 	IO* master_out = _session->master_out ()->output ().get ();
468 	for (uint32_t n = 0; n < master_out->n_ports ().n_audio (); ++n) {
469 		PortExportChannel* channel = new PortExportChannel ();
470 		channel->add_port (master_out->audio (n));
471 		ExportChannelPtr chan_ptr (channel);
472 		ccp->register_channel (chan_ptr);
473 	}
474 
475 	/* do audio export */
476 	boost::shared_ptr<AudioGrapher::BroadcastInfo> b;
477 	_session->get_export_handler ()->reset ();
478 	_session->get_export_handler ()->add_export_config (tsp, ccp, fmp, fnp, b);
479 	_session->get_export_handler ()->do_export ();
480 
481 	/* show progress */
482 	_setup_box.hide ();
483 	_progress_box.show_all ();
484 
485 	/* shrink window height (setup box) */
486 	Gtk::Requisition wr;
487 	get_size (wr.width, wr.height);
488 	resize (wr.width, 60);
489 
490 	sigc::connection progress_connection = Glib::signal_timeout ().connect (sigc::mem_fun (*this, &LoudnessDialog::progress_timeout), 100);
491 
492 	while (_status->running ()) {
493 		if (gtk_events_pending ()) {
494 			gtk_main_iteration ();
495 		} else {
496 			Glib::usleep (10000);
497 		}
498 	}
499 	progress_connection.disconnect ();
500 	_progress_box.hide_all ();
501 
502 	/* done */
503 	_status->finish (TRS_UI);
504 
505 	if (!_status->aborted () && _status->result_map.size () != 1) {
506 		ArdourMessageDialog (_("Loudness measurement returned no results. Likely because the analyzed range is to short."), false, MESSAGE_ERROR).run ();
507 		return 1;
508 	}
509 
510 	return _status->aborted () ? 1 : 0;
511 }
512 
513 void
display_report()514 LoudnessDialog::display_report ()
515 {
516 	ExportReport er (_("Export Loudness Report"), _status->result_map);
517 	er.set_transient_for (*this);
518 	er.run ();
519 }
520 
521 void
save_preset()522 LoudnessDialog::save_preset ()
523 {
524 	assert (_preset.user);
525 	ArdourWidgets::Prompter name_prompter (*this, true, true);
526 	name_prompter.set_title (_("Save Loudness Preset"));
527 	name_prompter.set_prompt (_("Name:"));
528 	name_prompter.add_button (_("Save"), Gtk::RESPONSE_ACCEPT);
529 	name_prompter.set_response_sensitive (Gtk::RESPONSE_ACCEPT, false);
530 	name_prompter.set_initial_text (_preset.label, _preset.label != _("Custom"));
531 	name_prompter.show_all ();
532 
533 	bool        saved = false;
534 	bool        done  = false;
535 	std::string result;
536 
537 	while (!done) {
538 		switch (name_prompter.run ()) {
539 			case Gtk::RESPONSE_ACCEPT:
540 				name_prompter.get_result (result);
541 				name_prompter.hide ();
542 				if (result.length()) {
543 					if (result == _("Custom")) {
544 						/* ask again */
545 						continue;
546 					}
547 					_preset.label = result;
548 					_preset.report = false;
549 					if (_lp.push_back (_preset)) {
550 						done  = true;
551 						saved = true;
552 					} else {
553 						/* Invalid Name, ask again */
554 					}
555 				} else {
556 					/* nothing entered, just get out of here */
557 					done = true;
558 				}
559 				break;
560 			default:
561 				done = true;
562 				break;
563 		}
564 	}
565 	if (saved) {
566 		_preset_dropdown.clear_items ();
567 		for (size_t i = 0; i < _lp.n_presets (); ++i) {
568 			using namespace Gtkmm2ext;
569 			_preset_dropdown.AddMenuElem (MenuElemNoMnemonic (_lp[i].label, sigc::bind (sigc::mem_fun (*this, &LoudnessDialog::load_preset), i)));
570 		}
571 		PBD::Unwinder<bool> uw (_ignore_preset, true);
572 		_preset_dropdown.set_active (_preset.label);
573 		_save_preset.set_sensitive (false);
574 		_remove_preset.set_sensitive (_preset.user);
575 	}
576 }
577 
578 void
remove_preset()579 LoudnessDialog::remove_preset ()
580 {
581 	assert (_preset.user);
582 	if (_lp.erase (_preset)) {
583 		_preset_dropdown.clear_items ();
584 		for (size_t i = 0; i < _lp.n_presets (); ++i) {
585 			using namespace Gtkmm2ext;
586 			_preset_dropdown.AddMenuElem (MenuElemNoMnemonic (_lp[i].label, sigc::bind (sigc::mem_fun (*this, &LoudnessDialog::load_preset), i)));
587 		}
588 		_preset.label = _("Custom");
589 		update_settings ();
590 	}
591 }
592 
593 void
load_preset(size_t n)594 LoudnessDialog::load_preset (size_t n)
595 {
596 	if (_ignore_preset) {
597 		return;
598 	}
599 	_preset = _lp[n];
600 	_save_preset.set_sensitive (false);
601 	_remove_preset.set_sensitive (_preset.user);
602 	apply_preset ();
603 	calculate_gain ();
604 }
605 
606 void
apply_preset()607 LoudnessDialog::apply_preset ()
608 {
609 	PBD::Unwinder<bool> uw (_ignore_change, true);
610 	_preset_dropdown.set_text (_preset.label);
611 
612 	_dbfs_btn.set_active (_preset.enable[0]);
613 	_dbtp_btn.set_active (_preset.enable[1]);
614 	_lufs_i_btn.set_active (_preset.enable[2]);
615 	_lufs_s_btn.set_active (_preset.enable[3]);
616 	_lufs_m_btn.set_active (_preset.enable[4]);
617 	_dbfs_spinbutton.set_value (_preset.level[0]);
618 	_dbtp_spinbutton.set_value (_preset.level[1]);
619 	_lufs_i_spinbutton.set_value (_preset.level[2]);
620 	_lufs_s_spinbutton.set_value (_preset.level[3]);
621 	_lufs_m_spinbutton.set_value (_preset.level[4]);
622 	update_sensitivity ();
623 }
624 
625 void
update_sensitivity()626 LoudnessDialog::update_sensitivity ()
627 {
628 	_dbfs_spinbutton.set_sensitive (_dbfs_btn.get_active () && _dbfs_btn.sensitive ());
629 	_dbtp_spinbutton.set_sensitive (_dbtp_btn.get_active () && _dbtp_btn.sensitive ());
630 	_lufs_i_spinbutton.set_sensitive (_lufs_i_btn.get_active () && _dbtp_btn.sensitive ());
631 	_lufs_s_spinbutton.set_sensitive (_lufs_s_btn.get_active () && _lufs_s_btn.sensitive ());
632 	_lufs_m_spinbutton.set_sensitive (_lufs_m_btn.get_active () && _lufs_m_btn.sensitive ());
633 }
634 
635 void
check_preset()636 LoudnessDialog::check_preset ()
637 {
638 	if (_lp.find_preset (_preset)) {
639 		_save_preset.set_sensitive (false);
640 		_remove_preset.set_sensitive (_preset.user);
641 	} else {
642 		if (!_preset.user) {
643 			_preset.label = _("Custom");
644 		}
645 		_preset.user = true;
646 		_preset.report = false;
647 		_save_preset.set_sensitive (true);
648 		_remove_preset.set_sensitive (false);
649 	}
650 	_preset_dropdown.set_text (_preset.label);
651 }
652 
653 void
update_settings()654 LoudnessDialog::update_settings ()
655 {
656 	if (_ignore_change) {
657 		return;
658 	}
659 
660 	/* check if preset with current settings exists */
661 	_preset.level[0]  = _dbfs_spinbutton.get_value ();
662 	_preset.level[1]  = _dbtp_spinbutton.get_value ();
663 	_preset.level[2]  = _lufs_i_spinbutton.get_value ();
664 	_preset.level[3]  = _lufs_s_spinbutton.get_value ();
665 	_preset.level[4]  = _lufs_m_spinbutton.get_value ();
666 	_preset.enable[0] = _dbfs_btn.get_active ();
667 	_preset.enable[1] = _dbtp_btn.get_active ();
668 	_preset.enable[2] = _lufs_i_btn.get_active ();
669 	_preset.enable[3] = _lufs_s_btn.get_active ();
670 	_preset.enable[4] = _lufs_m_btn.get_active ();
671 
672 	check_preset ();
673 	update_sensitivity ();
674 	calculate_gain ();
675 }
676 
677 float
gain_db() const678 LoudnessDialog::gain_db () const
679 {
680 	return _gain_norm + _gain_out;
681 }
682 
683 void
display_results()684 LoudnessDialog::display_results ()
685 {
686 	AnalysisResults const& ar (_status->result_map);
687 	assert (ar.size () == 1);
688 	ExportAnalysisPtr p = ar.begin ()->second;
689 
690 	if (!p->have_loudness || !p->have_dbtp || !p->have_lufs_graph) {
691 		ArdourMessageDialog (
692 		    string_compose (_("True-peak and loudness measurement failed. %1-VAMP analysis plugin is missing on your system. Please contact your vendor."), PROGRAM_NAME),
693 		    false, MESSAGE_ERROR)
694 		    .run ();
695 	}
696 
697 	plot_graph (p);
698 
699 	_dbfs   = accurate_coefficient_to_dB (p->peak);
700 	_dbtp   = accurate_coefficient_to_dB (p->truepeak);
701 	_lufs_i = p->integrated_loudness    > -200 ? p->integrated_loudness    : -std::numeric_limits<float>::infinity ();
702 	_lufs_s = p->max_loudness_short     > -200 ? p->max_loudness_short     : -std::numeric_limits<float>::infinity ();
703 	_lufs_m = p->max_loudness_momentary > -200 ? p->max_loudness_momentary : -std::numeric_limits<float>::infinity ();
704 
705 	_dbfs_btn.set_sensitive (_dbfs > -300);
706 	_dbtp_btn.set_sensitive (_dbtp > -300);
707 	_lufs_i_btn.set_sensitive (p->integrated_loudness > -200);
708 	_lufs_s_btn.set_sensitive (p->max_loudness_short > -200);
709 	_lufs_m_btn.set_sensitive (p->max_loudness_momentary > -200);
710 
711 	_dbfs_label.set_text (string_compose (_("%1 dBFS"), std::setprecision (1), std::fixed, _dbfs));
712 	_dbtp_label.set_text (string_compose (_("%1 dBTP"), std::setprecision (1), std::fixed, _dbtp));
713 	_lufs_i_label.set_text (string_compose (_("%1 LUFS"), std::setprecision (1), std::fixed, _lufs_i));
714 	_lufs_s_label.set_text (string_compose (_("%1 LUFS"), std::setprecision (1), std::fixed, _lufs_s));
715 	_lufs_m_label.set_text (string_compose (_("%1 LUFS"), std::setprecision (1), std::fixed, _lufs_m));
716 
717 	update_sensitivity ();
718 	calculate_gain ();
719 
720 	_result_box.show_all ();
721 	_show_report_button.set_sensitive (true);
722 }
723 
724 void
calculate_gain()725 LoudnessDialog::calculate_gain ()
726 {
727 	float dbfs   = _dbfs_spinbutton.get_value ();
728 	float dbtp   = _dbtp_spinbutton.get_value ();
729 	float lufs_i = _lufs_i_spinbutton.get_value ();
730 	float lufs_s = _lufs_s_spinbutton.get_value ();
731 	float lufs_m = _lufs_m_spinbutton.get_value ();
732 
733 	float gain     = 0;
734 	bool  set      = false;
735 
736 #define MIN_IF_SET(A, B)                 \
737   {                                      \
738     if (set) {                           \
739       gain = std::min (gain, (A) - (B)); \
740     } else {                             \
741       gain = (A) - (B);                  \
742     }                                    \
743     set = true;                          \
744   }
745 
746 	if (_dbfs_btn.get_active () && _dbfs_btn.sensitive ()) {
747 		MIN_IF_SET (dbfs, _dbfs);
748 	}
749 	if (_dbtp_btn.get_active () && _dbtp_btn.sensitive ()) {
750 		MIN_IF_SET (dbtp, _dbtp);
751 	}
752 	if (_lufs_i_btn.get_active () && _lufs_i_btn.sensitive ()) {
753 		MIN_IF_SET (lufs_i, _lufs_i);
754 	}
755 	if (_lufs_s_btn.get_active () && _lufs_s_btn.sensitive ()) {
756 		MIN_IF_SET (lufs_s, _lufs_s);
757 	}
758 	if (_lufs_m_btn.get_active () && _lufs_m_btn.sensitive ()) {
759 		MIN_IF_SET (lufs_m, _lufs_m);
760 	}
761 
762 	_delta_dbfs_label.set_text (string_compose (_("%1 dB"), std::setprecision (2), std::showpos, std::fixed, dbfs - _dbfs));
763 	_delta_dbtp_label.set_text (string_compose (_("%1 dB"), std::setprecision (2), std::showpos, std::fixed, dbtp - _dbtp));
764 	_delta_lufs_i_label.set_text (string_compose (_("%1 LU"), std::setprecision (2), std::showpos, std::fixed, lufs_i - _lufs_i));
765 	_delta_lufs_s_label.set_text (string_compose (_("%1 LU"), std::setprecision (2), std::showpos, std::fixed, lufs_s - _lufs_s));
766 	_delta_lufs_m_label.set_text (string_compose (_("%1 LU"), std::setprecision (2), std::showpos, std::fixed, lufs_m - _lufs_m));
767 
768 	_delta_dbfs_label.set_sensitive (_dbfs_btn.get_active ());
769 	_delta_dbtp_label.set_sensitive (_dbtp_btn.get_active ());
770 	_delta_lufs_i_label.set_sensitive (_lufs_i_btn.get_active ());
771 	_delta_lufs_s_label.set_sensitive (_lufs_s_btn.get_active ());
772 	_delta_lufs_m_label.set_sensitive (_lufs_m_btn.get_active ());
773 
774 	_gain_norm    = gain;
775 	bool in_range = gain_db () >= -40 && gain_db () <= 40;
776 
777 	_gain_norm_label.set_text (string_compose (_("%1 dB"), std::setprecision (2), std::showpos, std::fixed, _gain_norm));
778 	if (!in_range) {
779 		_gain_exceeds_label.set_text (_("exceeds"));
780 		_gain_total_label.set_markup (_("<b>    \u00B140 dB</b>"));
781 	} else {
782 		_gain_exceeds_label.set_text (X_(""));
783 		_gain_total_label.set_markup (string_compose (_("<b>%1 dB</b>"), std::setw (7), std::setprecision (2), std::showpos, std::fixed, gain_db ()));
784 	}
785 
786 	test_conformity ();
787 	_ok_button->set_sensitive (in_range);
788 }
789 
790 void
test_conformity()791 LoudnessDialog::test_conformity ()
792 {
793 	if (_conformity_frame.get_child ()) {
794 		_conformity_frame.remove ();
795 	}
796 
797 	const float dbfs = rintf ((_dbfs + _gain_norm) * 10.f) / 10.f;
798 	const float dbtp = rintf ((_dbtp + _gain_norm) * 10.f) / 10.f;
799 	const float lufs_i = rintf ((_lufs_i + _gain_norm) * 10.f) / 10.f;
800 
801 	Table* t      = manage (new Table ());
802 	size_t n_pset = _lp.n_presets ();
803 	size_t n_rows = ceil (n_pset / 3.0);
804 
805 	size_t row = 0;
806 	size_t col = 0;
807 
808 	uint32_t c_good = UIConfigurationBase::instance ().color ("alert:green");  // OK / green
809 	uint32_t c_warn = UIConfigurationBase::instance ().color ("alert:yellow"); // Warning / yellow
810 	uint32_t c_fail = UIConfigurationBase::instance ().color ("alert:red");    // Fail / red
811 
812 	Gdk::Color color_good = ARDOUR_UI_UTILS::gdk_color_from_rgba (c_good);
813 	Gdk::Color color_warn = ARDOUR_UI_UTILS::gdk_color_from_rgba (c_warn);
814 	Gdk::Color color_fail = ARDOUR_UI_UTILS::gdk_color_from_rgba (c_fail);
815 
816 	for (size_t i = 1; i < n_pset; ++i) {
817 		CLoudnessPreset const& preset = _lp[i];
818 		Label* l = manage (new Label (preset.label + ":", ALIGN_LEFT));
819 		t->attach (*l, col, col + 1, row, row + 1, EXPAND | FILL, SHRINK, 2, 0);
820 
821 		if (lufs_i > preset.LUFS_range[0]
822 		    || (preset.enable[0] && dbfs > preset.level[0])
823 		    || (preset.enable[1] && dbtp > preset.level[1])
824 		   ) {
825 #ifdef PLATFORM_WINDOWS
826 			l = manage (new Label ("X", ALIGN_CENTER)); // cross mark
827 #else
828 			l = manage (new Label ("\u274C", ALIGN_CENTER)); // cross mark
829 #endif
830 			l->modify_font (UIConfiguration::instance ().get_BigFont ());
831 			l->modify_fg (Gtk::STATE_NORMAL, color_fail);
832 			Gtkmm2ext::set_size_request_to_display_given_text (*l, "\u274C\u2713", 0, 0);
833 			set_tooltip (*l, "The signal is too loud.");
834 		} else if (lufs_i < preset.LUFS_range[1]) {
835 			l = manage (new Label ("\u2713", ALIGN_CENTER)); // check mark
836 			l->modify_font (UIConfiguration::instance ().get_BigFont ());
837 			l->modify_fg (Gtk::STATE_NORMAL, color_warn);
838 			Gtkmm2ext::set_size_request_to_display_given_text (*l, "\u274C\u2713", 0, 0);
839 			set_tooltip (*l, "The signal is too quiet, but satisfies the max. loudness spec.");
840 		} else {
841 			l = manage (new Label ("\u2714", ALIGN_CENTER)); // heavy check mark
842 			l->modify_font (UIConfiguration::instance ().get_BigFont ());
843 			l->modify_fg (Gtk::STATE_NORMAL, color_good);
844 			set_tooltip (*l, "Signal loudness is within the spec.");
845 			Gtkmm2ext::set_size_request_to_display_given_text (*l, "\u274C\u2713", 0, 0);
846 		}
847 
848 		t->attach (*l, col + 1, col + 2, row, row + 1, SHRINK, SHRINK, 2, 0);
849 
850 		if (++row == n_rows) {
851 			ArdourVSpacer* spc = manage (new ArdourVSpacer (1.0));
852 			t->attach (*spc, col + 2, col + 3, 0, n_rows, FILL, EXPAND | FILL, 8, 0);
853 			row = 0;
854 			col += 3;
855 		}
856 	}
857 
858 	t->set_border_width (6);
859 	_conformity_frame.add (*t);
860 	_conformity_frame.show_all ();
861 }
862 
863 void
graph_size_request(Gtk::Requisition * req)864 LoudnessDialog::graph_size_request (Gtk::Requisition* req)
865 {
866 	if (_loudness_surf) {
867 		req->width  = _loudness_surf->get_width ();
868 		req->height = _loudness_surf->get_height ();
869 	} else {
870 		req->width  = 1;
871 		req->height = 1;
872 	}
873 }
874 
875 bool
graph_expose_event(GdkEventExpose * ev)876 LoudnessDialog::graph_expose_event (GdkEventExpose* ev)
877 {
878 	Cairo::RefPtr<Cairo::Context> cr = _loudness_graph.get_window ()->create_cairo_context ();
879 #if 0
880 	Gtk::Allocation a = _loudness_graph.get_allocation ();
881 	double const width = a.get_width ();
882 	double const height = a.get_height ();
883 #endif
884 
885 	cr->rectangle (ev->area.x, ev->area.y, ev->area.width, ev->area.height);
886 	cr->clip ();
887 
888 	if (_loudness_surf) {
889 		cr->set_source (_loudness_surf, 0, 0);
890 		cr->set_operator (Cairo::OPERATOR_OVER);
891 		cr->paint ();
892 	}
893 
894 	return true;
895 }
896 
897 void
plot_graph(ExportAnalysisPtr p)898 LoudnessDialog::plot_graph (ExportAnalysisPtr p)
899 {
900 	_loudness_surf = ArdourGraphs::plot_loudness (get_pango_context (), p, -1, 0, _session->nominal_sample_rate ());
901 	_loudness_graph.queue_resize ();
902 }
903