a7d43ece873434bd5186a4e18dfecaf263adc760
[dcpomatic.git] / src / wx / audio_dialog.cc
1 /*
2     Copyright (C) 2013-2018 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
6     DCP-o-matic is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     DCP-o-matic is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
18
19 */
20
21 #include "audio_dialog.h"
22 #include "audio_plot.h"
23 #include "wx_util.h"
24 #include "static_text.h"
25 #include "check_box.h"
26 #include "lib/audio_analysis.h"
27 #include "lib/film.h"
28 #include "lib/analyse_audio_job.h"
29 #include "lib/audio_content.h"
30 #include "lib/job_manager.h"
31 #include <libxml++/libxml++.h>
32 #include <boost/filesystem.hpp>
33 #include <boost/foreach.hpp>
34 #include <iostream>
35
36 using std::cout;
37 using std::list;
38 using std::vector;
39 using std::pair;
40 using boost::shared_ptr;
41 using boost::bind;
42 using boost::optional;
43 using boost::const_pointer_cast;
44 using boost::dynamic_pointer_cast;
45 #if BOOST_VERSION >= 106100
46 using namespace boost::placeholders;
47 #endif
48
49 /** @param parent Parent window.
50  *  @param film Film we are using.
51  *  @param content Content to analyse, or 0 to analyse all of the film's audio.
52  */
53 AudioDialog::AudioDialog (wxWindow* parent, shared_ptr<Film> film, shared_ptr<Content> content)
54         : wxDialog (
55                 parent,
56                 wxID_ANY,
57                 _("Audio"),
58                 wxDefaultPosition,
59                 wxSize (640, 512),
60 #ifdef DCPOMATIC_OSX
61                 /* I can't get wxFRAME_FLOAT_ON_PARENT to work on OS X, and although wxSTAY_ON_TOP keeps
62                    the window above all others (and not just our own) it's better than nothing for now.
63                 */
64                 wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER | wxFULL_REPAINT_ON_RESIZE | wxSTAY_ON_TOP
65 #else
66                 wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER | wxFULL_REPAINT_ON_RESIZE | wxFRAME_FLOAT_ON_PARENT
67 #endif
68                 )
69         , _film (film)
70         , _content (content)
71         , _channels (film->audio_channels ())
72         , _plot (0)
73 {
74         wxFont subheading_font (*wxNORMAL_FONT);
75         subheading_font.SetWeight (wxFONTWEIGHT_BOLD);
76
77         wxBoxSizer* overall_sizer = new wxBoxSizer (wxVERTICAL);
78         wxBoxSizer* lr_sizer = new wxBoxSizer (wxHORIZONTAL);
79
80         wxBoxSizer* left = new wxBoxSizer (wxVERTICAL);
81
82         _cursor = new StaticText (this, wxT("Cursor: none"));
83         left->Add (_cursor, 0, wxTOP, DCPOMATIC_SIZER_Y_GAP);
84         _plot = new AudioPlot (this);
85         left->Add (_plot, 1, wxTOP | wxEXPAND, 12);
86         _sample_peak = new StaticText (this, wxT (""));
87         left->Add (_sample_peak, 0, wxTOP, DCPOMATIC_SIZER_Y_GAP);
88         _true_peak = new StaticText (this, wxT (""));
89         left->Add (_true_peak, 0, wxTOP, DCPOMATIC_SIZER_Y_GAP);
90         _integrated_loudness = new StaticText (this, wxT (""));
91         left->Add (_integrated_loudness, 0, wxTOP, DCPOMATIC_SIZER_Y_GAP);
92         _loudness_range = new StaticText (this, wxT (""));
93         left->Add (_loudness_range, 0, wxTOP, DCPOMATIC_SIZER_Y_GAP);
94
95         lr_sizer->Add (left, 1, wxALL | wxEXPAND, 12);
96
97         wxBoxSizer* right = new wxBoxSizer (wxVERTICAL);
98
99         {
100                 wxStaticText* m = new StaticText (this, _("Channels"));
101                 m->SetFont (subheading_font);
102                 right->Add (m, 1, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM, 16);
103         }
104
105         for (int i = 0; i < MAX_DCP_AUDIO_CHANNELS; ++i) {
106                 _channel_checkbox[i] = new CheckBox (this, std_to_wx(audio_channel_name(i)));
107                 _channel_checkbox[i]->SetForegroundColour(wxColour(_plot->colour(i)));
108                 right->Add (_channel_checkbox[i], 0, wxEXPAND | wxALL, 3);
109                 _channel_checkbox[i]->Bind (wxEVT_CHECKBOX, boost::bind (&AudioDialog::channel_clicked, this, _1));
110         }
111
112         show_or_hide_channel_checkboxes ();
113
114         {
115                 wxStaticText* m = new StaticText (this, _("Type"));
116                 m->SetFont (subheading_font);
117                 right->Add (m, 1, wxALIGN_CENTER_VERTICAL | wxTOP, 16);
118         }
119
120         wxString const types[] = {
121                 _("Peak"),
122                 _("RMS")
123         };
124
125         for (int i = 0; i < AudioPoint::COUNT; ++i) {
126                 _type_checkbox[i] = new CheckBox (this, types[i]);
127                 right->Add (_type_checkbox[i], 0, wxEXPAND | wxALL, 3);
128                 _type_checkbox[i]->Bind (wxEVT_CHECKBOX, boost::bind (&AudioDialog::type_clicked, this, _1));
129         }
130
131         {
132                 wxStaticText* m = new StaticText (this, _("Smoothing"));
133                 m->SetFont (subheading_font);
134                 right->Add (m, 1, wxALIGN_CENTER_VERTICAL | wxTOP, 16);
135         }
136
137         _smoothing = new wxSlider (this, wxID_ANY, AudioPlot::max_smoothing / 2, 1, AudioPlot::max_smoothing);
138         _smoothing->Bind (wxEVT_SCROLL_THUMBTRACK, boost::bind (&AudioDialog::smoothing_changed, this));
139         right->Add (_smoothing, 0, wxEXPAND);
140
141         lr_sizer->Add (right, 0, wxALL, 12);
142
143         overall_sizer->Add (lr_sizer, 0, wxEXPAND);
144
145 #ifdef DCPOMATIC_LINUX
146         wxSizer* buttons = CreateSeparatedButtonSizer (wxCLOSE);
147         if (buttons) {
148                 overall_sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder());
149         }
150 #endif
151
152         SetSizer (overall_sizer);
153         overall_sizer->Layout ();
154         overall_sizer->SetSizeHints (this);
155
156         _film_connection = film->Change.connect (boost::bind(&AudioDialog::film_change, this, _1, _2));
157         _film_content_connection = film->ContentChange.connect (boost::bind (&AudioDialog::content_change, this, _1, _3));
158         DCPOMATIC_ASSERT (film->directory());
159         SetTitle(wxString::Format(_("DCP-o-matic audio - %s"), std_to_wx(film->directory().get().string())));
160
161         if (content) {
162                 _playlist.reset (new Playlist ());
163                 const_pointer_cast<Playlist>(_playlist)->add(film, content);
164         } else {
165                 _playlist = film->playlist ();
166         }
167
168         _plot->Cursor.connect (bind (&AudioDialog::set_cursor, this, _1, _2));
169 }
170
171
172 void
173 AudioDialog::show_or_hide_channel_checkboxes ()
174 {
175         for (int i = 0; i < _channels; ++i) {
176                 _channel_checkbox[i]->Show ();
177         }
178
179         for (int i = _channels; i < MAX_DCP_AUDIO_CHANNELS; ++i) {
180                 _channel_checkbox[i]->Hide ();
181         }
182 }
183
184
185 void
186 AudioDialog::try_to_load_analysis ()
187 {
188         if (!IsShown ()) {
189                 return;
190         }
191
192         shared_ptr<const Film> film = _film.lock ();
193         DCPOMATIC_ASSERT (film);
194
195         shared_ptr<Content> check = _content.lock();
196
197         boost::filesystem::path const path = film->audio_analysis_path (_playlist);
198         if (!boost::filesystem::exists (path)) {
199                 _plot->set_analysis (shared_ptr<AudioAnalysis> ());
200                 _analysis.reset ();
201
202                 BOOST_FOREACH (shared_ptr<Job> i, JobManager::instance()->get()) {
203                         if (dynamic_pointer_cast<AnalyseAudioJob>(i)) {
204                                 i->cancel ();
205                         }
206                 }
207
208                 JobManager::instance()->analyse_audio (
209                         film, _playlist, !static_cast<bool>(check), _analysis_finished_connection, bind (&AudioDialog::analysis_finished, this)
210                         );
211                 return;
212         }
213
214         try {
215                 _analysis.reset (new AudioAnalysis (path));
216         } catch (OldFormatError& e) {
217                 /* An old analysis file: recreate it */
218                 JobManager::instance()->analyse_audio (
219                         film, _playlist, !static_cast<bool>(check), _analysis_finished_connection, bind (&AudioDialog::analysis_finished, this)
220                         );
221                 return;
222         } catch (xmlpp::exception& e) {
223                 /* Probably a (very) old-style analysis file: recreate it */
224                 JobManager::instance()->analyse_audio (
225                         film, _playlist, !static_cast<bool>(check), _analysis_finished_connection, bind (&AudioDialog::analysis_finished, this)
226                         );
227                 return;
228         }
229
230         _plot->set_analysis (_analysis);
231         _plot->set_gain_correction (_analysis->gain_correction (_playlist));
232         setup_statistics ();
233         show_or_hide_channel_checkboxes ();
234
235         /* Set up some defaults if no check boxes are checked */
236
237         int i = 0;
238         while (i < _channels && (!_channel_checkbox[i] || !_channel_checkbox[i]->GetValue ())) {
239                 ++i;
240         }
241
242         if (i == _channels) {
243                 /* Nothing checked; check mapped ones */
244
245                 list<int> mapped;
246                 shared_ptr<Content> content = _content.lock ();
247
248                 if (content) {
249                         mapped = content->audio->mapping().mapped_output_channels ();
250                 } else {
251                         mapped = film->mapped_audio_channels ();
252                 }
253
254                 BOOST_FOREACH (int i, mapped) {
255                         if (_channel_checkbox[i]) {
256                                 _channel_checkbox[i]->SetValue (true);
257                                 _plot->set_channel_visible (i, true);
258                         }
259                 }
260         }
261
262         i = 0;
263         while (i < AudioPoint::COUNT && !_type_checkbox[i]->GetValue ()) {
264                 i++;
265         }
266
267         if (i == AudioPoint::COUNT) {
268                 for (int i = 0; i < AudioPoint::COUNT; ++i) {
269                         _type_checkbox[i]->SetValue (true);
270                         _plot->set_type_visible (i, true);
271                 }
272         }
273
274         Refresh ();
275 }
276
277 void
278 AudioDialog::analysis_finished ()
279 {
280         shared_ptr<const Film> film = _film.lock ();
281         if (!film) {
282                 /* This should not happen, but if it does we should just give up quietly */
283                 return;
284         }
285
286         if (!boost::filesystem::exists (film->audio_analysis_path (_playlist))) {
287                 /* We analysed and still nothing showed up, so maybe it was cancelled or it failed.
288                    Give up.
289                 */
290                 _plot->set_message (_("Could not analyse audio."));
291                 return;
292         }
293
294         try_to_load_analysis ();
295 }
296
297 void
298 AudioDialog::channel_clicked (wxCommandEvent& ev)
299 {
300         int c = 0;
301         while (c < _channels && ev.GetEventObject() != _channel_checkbox[c]) {
302                 ++c;
303         }
304
305         DCPOMATIC_ASSERT (c < _channels);
306
307         _plot->set_channel_visible (c, _channel_checkbox[c]->GetValue ());
308 }
309
310 void
311 AudioDialog::film_change (ChangeType type, int p)
312 {
313         if (type != CHANGE_TYPE_DONE) {
314                 return;
315         }
316
317         if (p == Film::AUDIO_CHANNELS) {
318                 shared_ptr<Film> film = _film.lock ();
319                 if (film) {
320                         _channels = film->audio_channels ();
321                         try_to_load_analysis ();
322                 }
323         }
324 }
325
326 void
327 AudioDialog::content_change (ChangeType type, int p)
328 {
329         if (type != CHANGE_TYPE_DONE) {
330                 return;
331         }
332
333         if (p == AudioContentProperty::STREAMS) {
334                 try_to_load_analysis ();
335         } else if (p == AudioContentProperty::GAIN) {
336                 if (_playlist->content().size() == 1 && _analysis) {
337                         /* We can use a short-cut to render the effect of this
338                            change, rather than recalculating everything.
339                         */
340                         _plot->set_gain_correction (_analysis->gain_correction (_playlist));
341                         setup_statistics ();
342                 } else {
343                         try_to_load_analysis ();
344                 }
345         }
346 }
347
348 void
349 AudioDialog::type_clicked (wxCommandEvent& ev)
350 {
351         int t = 0;
352         while (t < AudioPoint::COUNT && ev.GetEventObject() != _type_checkbox[t]) {
353                 ++t;
354         }
355
356         DCPOMATIC_ASSERT (t < AudioPoint::COUNT);
357
358         _plot->set_type_visible (t, _type_checkbox[t]->GetValue ());
359 }
360
361 void
362 AudioDialog::smoothing_changed ()
363 {
364         _plot->set_smoothing (_smoothing->GetValue ());
365 }
366
367 void
368 AudioDialog::setup_statistics ()
369 {
370         if (!_analysis) {
371                 return;
372         }
373
374         shared_ptr<Film> film = _film.lock ();
375         if (!film) {
376                 return;
377         }
378
379         pair<AudioAnalysis::PeakTime, int> const peak = _analysis->overall_sample_peak ();
380         float const peak_dB = 20 * log10 (peak.first.peak) + _analysis->gain_correction (_playlist);
381         _sample_peak->SetLabel (
382                 wxString::Format (
383                         _("Sample peak is %.2fdB at %s on %s"),
384                         peak_dB,
385                         time_to_timecode (peak.first.time, film->video_frame_rate ()).data (),
386                         std_to_wx (short_audio_channel_name (peak.second)).data ()
387                         )
388                 );
389
390         if (peak_dB > -3) {
391                 _sample_peak->SetForegroundColour (wxColour (255, 0, 0));
392         } else {
393                 _sample_peak->SetForegroundColour (wxColour (0, 0, 0));
394         }
395
396         if (_analysis->overall_true_peak()) {
397                 float const peak = _analysis->overall_true_peak().get();
398                 float const peak_dB = 20 * log10 (peak) + _analysis->gain_correction (_playlist);
399
400                 _true_peak->SetLabel (wxString::Format (_("True peak is %.2fdB"), peak_dB));
401
402                 if (peak_dB > -3) {
403                         _true_peak->SetForegroundColour (wxColour (255, 0, 0));
404                 } else {
405                         _true_peak->SetForegroundColour (wxColour (0, 0, 0));
406                 }
407         }
408
409         /* XXX: check whether it's ok to add dB gain to these quantities */
410
411         if (static_cast<bool>(_analysis->integrated_loudness ())) {
412                 _integrated_loudness->SetLabel (
413                         wxString::Format (
414                                 _("Integrated loudness %.2f LUFS"),
415                                 _analysis->integrated_loudness().get() + _analysis->gain_correction (_playlist)
416                                 )
417                         );
418         }
419
420         if (static_cast<bool>(_analysis->loudness_range ())) {
421                 _loudness_range->SetLabel (
422                         wxString::Format (
423                                 _("Loudness range %.2f LU"),
424                                 _analysis->loudness_range().get() + _analysis->gain_correction (_playlist)
425                                 )
426                         );
427         }
428 }
429
430 bool
431 AudioDialog::Show (bool show)
432 {
433         bool const r = wxDialog::Show (show);
434         try_to_load_analysis ();
435         return r;
436 }
437
438 void
439 AudioDialog::set_cursor (optional<DCPTime> time, optional<float> db)
440 {
441         if (!time || !db) {
442                 _cursor->SetLabel (_("Cursor: none"));
443                 return;
444         }
445
446         shared_ptr<Film> film = _film.lock();
447         DCPOMATIC_ASSERT (film);
448         _cursor->SetLabel (wxString::Format (_("Cursor: %.1fdB at %s"), *db, time->timecode(film->video_frame_rate())));
449 }