34923fb022cc5050598ba7b03efe8ccffe467d69
[dcpomatic.git] / src / wx / audio_panel.cc
1 /*
2     Copyright (C) 2012-2021 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
22 #include "audio_dialog.h"
23 #include "audio_mapping_view.h"
24 #include "audio_panel.h"
25 #include "check_box.h"
26 #include "content_panel.h"
27 #include "dcpomatic_button.h"
28 #include "gain_calculator_dialog.h"
29 #include "static_text.h"
30 #include "wx_util.h"
31 #include "lib/audio_content.h"
32 #include "lib/cinema_sound_processor.h"
33 #include "lib/config.h"
34 #include "lib/dcp_content.h"
35 #include "lib/ffmpeg_audio_stream.h"
36 #include "lib/ffmpeg_content.h"
37 #include "lib/film.h"
38 #include "lib/job_manager.h"
39 #include "lib/maths_util.h"
40 #include <dcp/warnings.h>
41 LIBDCP_DISABLE_WARNINGS
42 #include <wx/spinctrl.h>
43 LIBDCP_ENABLE_WARNINGS
44
45
46 using std::dynamic_pointer_cast;
47 using std::list;
48 using std::make_shared;
49 using std::pair;
50 using std::set;
51 using std::shared_ptr;
52 using std::string;
53 using std::vector;
54 using boost::optional;
55 #if BOOST_VERSION >= 106100
56 using namespace boost::placeholders;
57 #endif
58 using namespace dcpomatic;
59
60
61 std::map<boost::filesystem::path, float> AudioPanel::_peak_cache;
62
63
64 AudioPanel::AudioPanel (ContentPanel* p)
65         : ContentSubPanel (p, _("Audio"))
66 {
67
68 }
69
70
71 void
72 AudioPanel::create ()
73 {
74         _show = new Button (this, _("Show graph of audio levels..."));
75         _peak = new StaticText (this, wxT (""));
76
77         _gain_label = create_label (this, _("Gain"), true);
78         _gain = new ContentSpinCtrlDouble<AudioContent> (
79                 this,
80                 new wxSpinCtrlDouble (this),
81                 AudioContentProperty::GAIN,
82                 &Content::audio,
83                 boost::mem_fn (&AudioContent::gain),
84                 boost::mem_fn (&AudioContent::set_gain)
85                 );
86
87         _gain_db_label = create_label (this, _("dB"), false);
88         _gain_calculate_button = new Button (this, _("Calculate..."));
89
90         _delay_label = create_label (this, _("Delay"), true);
91         _delay = new ContentSpinCtrl<AudioContent> (
92                 this,
93                 new wxSpinCtrl (this),
94                 AudioContentProperty::DELAY,
95                 &Content::audio,
96                 boost::mem_fn (&AudioContent::delay),
97                 boost::mem_fn (&AudioContent::set_delay)
98                 );
99
100         /// TRANSLATORS: this is an abbreviation for milliseconds, the unit of time
101         _delay_ms_label = create_label (this, _("ms"), false);
102
103         _fade_in_label = create_label (this, _("Fade in"), true);
104         _fade_in = new Timecode<ContentTime> (this);
105
106         _fade_out_label = create_label (this, _("Fade out"), true);
107         _fade_out = new Timecode<ContentTime> (this);
108
109         _use_same_fades_as_video = new CheckBox(this, _("Use same fades as video"));
110
111         _mapping = new AudioMappingView (this, _("Content"), _("content"), _("DCP"), _("DCP"));
112         _sizer->Add (_mapping, 1, wxEXPAND | wxALL, 6);
113
114         _description = new StaticText (this, wxT(" \n"), wxDefaultPosition, wxDefaultSize);
115         _sizer->Add (_description, 0, wxALL, 12);
116         auto font = _description->GetFont();
117         font.SetStyle(wxFONTSTYLE_ITALIC);
118         font.SetPointSize(font.GetPointSize() - 1);
119         _description->SetFont (font);
120
121         _gain->wrapped()->SetRange (-60, 60);
122         _gain->wrapped()->SetDigits (1);
123         _gain->wrapped()->SetIncrement (0.5);
124         _delay->wrapped()->SetRange (-1000, 1000);
125
126         content_selection_changed ();
127         film_changed(FilmProperty::AUDIO_CHANNELS);
128         film_changed(FilmProperty::VIDEO_FRAME_RATE);
129         film_changed(FilmProperty::REEL_TYPE);
130
131         _show->Bind                  (wxEVT_BUTTON,   boost::bind (&AudioPanel::show_clicked, this));
132         _gain_calculate_button->Bind (wxEVT_BUTTON,   boost::bind (&AudioPanel::gain_calculate_button_clicked, this));
133
134         _fade_in->Changed.connect (boost::bind(&AudioPanel::fade_in_changed, this));
135         _fade_out->Changed.connect (boost::bind(&AudioPanel::fade_out_changed, this));
136         _use_same_fades_as_video->bind(&AudioPanel::use_same_fades_as_video_changed, this);
137
138         _mapping_connection = _mapping->Changed.connect (boost::bind (&AudioPanel::mapping_changed, this, _1));
139         _active_jobs_connection = JobManager::instance()->ActiveJobsChanged.connect (boost::bind (&AudioPanel::active_jobs_changed, this, _1, _2));
140
141         add_to_grid ();
142
143         layout();
144 }
145
146
147 void
148 AudioPanel::add_to_grid ()
149 {
150         int r = 0;
151
152         _grid->Add (_show, wxGBPosition (r, 0), wxGBSpan (1, 2));
153         _grid->Add (_peak, wxGBPosition (r, 2), wxGBSpan (1, 2), wxALIGN_CENTER_VERTICAL);
154         ++r;
155
156         add_label_to_sizer (_grid, _gain_label, true, wxGBPosition(r, 0));
157         {
158                 auto s = new wxBoxSizer (wxHORIZONTAL);
159                 s->Add (_gain->wrapped(), 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, DCPOMATIC_SIZER_GAP);
160                 s->Add (_gain_db_label, 0, wxALIGN_CENTER_VERTICAL);
161                 _grid->Add (s, wxGBPosition(r, 1));
162         }
163
164         _grid->Add (_gain_calculate_button, wxGBPosition(r, 2), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
165         ++r;
166
167         add_label_to_sizer (_grid, _delay_label, true, wxGBPosition(r, 0));
168         auto s = new wxBoxSizer (wxHORIZONTAL);
169         s->Add (_delay->wrapped(), 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, DCPOMATIC_SIZER_GAP);
170         s->Add (_delay_ms_label, 0, wxALIGN_CENTER_VERTICAL);
171         _grid->Add (s, wxGBPosition(r, 1));
172         ++r;
173
174         add_label_to_sizer (_grid, _fade_in_label, true, wxGBPosition(r, 0));
175         _grid->Add (_fade_in, wxGBPosition(r, 1), wxGBSpan(1, 3));
176         ++r;
177
178         add_label_to_sizer (_grid, _fade_out_label, true, wxGBPosition(r, 0));
179         _grid->Add (_fade_out, wxGBPosition(r, 1), wxGBSpan(1, 3));
180         ++r;
181
182         _grid->Add (_use_same_fades_as_video, wxGBPosition(r, 0), wxGBSpan(1, 4));
183         ++r;
184 }
185
186
187 void
188 AudioPanel::film_changed (FilmProperty property)
189 {
190         if (!_parent->film()) {
191                 return;
192         }
193
194         switch (property) {
195         case FilmProperty::AUDIO_CHANNELS:
196         case FilmProperty::AUDIO_PROCESSOR:
197                 _mapping->set_output_channels (_parent->film()->audio_output_names ());
198                 setup_peak ();
199                 setup_sensitivity();
200                 break;
201         case FilmProperty::VIDEO_FRAME_RATE:
202                 setup_description ();
203                 break;
204         case FilmProperty::REEL_TYPE:
205         case FilmProperty::INTEROP:
206                 setup_sensitivity ();
207                 break;
208         default:
209                 break;
210         }
211 }
212
213
214 void
215 AudioPanel::film_content_changed (int property)
216 {
217         auto ac = _parent->selected_audio ();
218         if (property == AudioContentProperty::STREAMS) {
219                 if (ac.size() == 1) {
220                         _mapping->set (ac.front()->audio->mapping());
221                         _mapping->set_input_channels (ac.front()->audio->channel_names ());
222
223                         vector<AudioMappingView::Group> groups;
224                         int c = 0;
225                         for (auto i: ac.front()->audio->streams()) {
226                                 auto f = dynamic_pointer_cast<const FFmpegAudioStream> (i);
227                                 string name = "";
228                                 if (f) {
229                                         name = f->name;
230                                         if (f->codec_name) {
231                                                 name += " (" + f->codec_name.get() + ")";
232                                         }
233                                 }
234                                 groups.push_back (AudioMappingView::Group (c, c + i->channels() - 1, name));
235                                 c += i->channels ();
236                         }
237                         _mapping->set_input_groups (groups);
238
239                 } else {
240                         _mapping->set (AudioMapping ());
241                 }
242                 setup_description ();
243                 setup_peak ();
244                 layout ();
245         } else if (property == AudioContentProperty::GAIN) {
246                 /* This is a bit aggressive but probably not so bad */
247                 _peak_cache.clear();
248                 setup_peak ();
249         } else if (property == ContentProperty::VIDEO_FRAME_RATE) {
250                 setup_description ();
251         } else if (property == AudioContentProperty::FADE_IN) {
252                 set<Frame> check;
253                 for (auto i: ac) {
254                         check.insert (i->audio->fade_in().get());
255                 }
256
257                 if (check.size() == 1) {
258                         _fade_in->set (
259                                 ac.front()->audio->fade_in(),
260                                 ac.front()->active_video_frame_rate(_parent->film())
261                                 );
262                 } else {
263                         _fade_in->clear ();
264                 }
265         } else if (property == AudioContentProperty::FADE_OUT) {
266                 set<Frame> check;
267                 for (auto i: ac) {
268                         check.insert (i->audio->fade_out().get());
269                 }
270
271                 if (check.size() == 1) {
272                         _fade_out->set (
273                                 ac.front()->audio->fade_out(),
274                                 ac.front()->active_video_frame_rate(_parent->film())
275                                 );
276                 } else {
277                         _fade_out->clear ();
278                 }
279         } else if (property == AudioContentProperty::USE_SAME_FADES_AS_VIDEO) {
280                 setup_sensitivity ();
281         }
282 }
283
284
285 void
286 AudioPanel::gain_calculate_button_clicked ()
287 {
288         GainCalculatorDialog dialog(this);
289         auto const r = dialog.ShowModal();
290         auto change = dialog.db_change();
291
292         if (r == wxID_CANCEL || !change) {
293                 return;
294         }
295
296         auto old_peak_dB = peak ();
297         auto old_value = _gain->wrapped()->GetValue();
298         _gain->wrapped()->SetValue(old_value + *change);
299
300         /* This appears to be necessary, as the change is not signalled,
301            I think.
302         */
303         _gain->view_changed ();
304
305         auto peak_dB = peak ();
306         if (old_peak_dB && *old_peak_dB < -0.5 && peak_dB && *peak_dB > -0.5) {
307                 error_dialog (this, _("It is not possible to adjust the content's gain for this fader change as it would cause the DCP's audio to clip.  The gain has not been changed."));
308                 _gain->wrapped()->SetValue (old_value);
309                 _gain->view_changed ();
310         }
311 }
312
313
314 void
315 AudioPanel::setup_description ()
316 {
317         auto ac = _parent->selected_audio ();
318         if (ac.size () != 1) {
319                 checked_set (_description, wxT (""));
320                 return;
321         }
322
323         checked_set (_description, ac.front()->audio->processing_description(_parent->film()));
324 }
325
326
327 void
328 AudioPanel::mapping_changed (AudioMapping m)
329 {
330         auto c = _parent->selected_audio ();
331         if (c.size() == 1) {
332                 c.front()->audio->set_mapping (m);
333         }
334 }
335
336
337 void
338 AudioPanel::content_selection_changed ()
339 {
340         auto sel = _parent->selected_audio ();
341
342         _gain->set_content (sel);
343         _delay->set_content (sel);
344
345         film_content_changed (AudioContentProperty::STREAMS);
346         film_content_changed (AudioContentProperty::GAIN);
347         film_content_changed (AudioContentProperty::FADE_IN);
348         film_content_changed (AudioContentProperty::FADE_OUT);
349         film_content_changed (AudioContentProperty::USE_SAME_FADES_AS_VIDEO);
350         film_content_changed (DCPContentProperty::REFERENCE_AUDIO);
351
352         setup_sensitivity ();
353 }
354
355
356 void
357 AudioPanel::setup_sensitivity ()
358 {
359         auto sel = _parent->selected_audio ();
360
361         shared_ptr<DCPContent> dcp;
362         if (sel.size() == 1) {
363                 dcp = dynamic_pointer_cast<DCPContent> (sel.front ());
364         }
365
366         auto const ref = dcp && dcp->reference_audio();
367         auto const single = sel.size() == 1;
368         auto const all_have_video = std::all_of(sel.begin(), sel.end(), [](shared_ptr<const Content> c) { return static_cast<bool>(c->video); });
369
370         _gain->wrapped()->Enable (!ref);
371         _gain_calculate_button->Enable (!ref && single);
372         _show->Enable (single);
373         _peak->Enable (!ref && single);
374         _delay->wrapped()->Enable (!ref);
375         _mapping->Enable (!ref && single);
376         _description->Enable (!ref && single);
377         _fade_in->Enable (!_use_same_fades_as_video->GetValue());
378         _fade_out->Enable (!_use_same_fades_as_video->GetValue());
379         _use_same_fades_as_video->Enable (!ref && all_have_video);
380 }
381
382
383 void
384 AudioPanel::show_clicked ()
385 {
386         _audio_dialog.reset();
387
388         auto ac = _parent->selected_audio ();
389         if (ac.size() != 1) {
390                 return;
391         }
392
393         _audio_dialog.reset(this, _parent->film(), _parent->film_viewer(), ac.front());
394         _audio_dialog->Show ();
395 }
396
397
398 /** @return If there is one selected piece of audio content, return its peak value in dB (if known) */
399 optional<float>
400 AudioPanel::peak () const
401 {
402         optional<float> peak_dB;
403
404         auto sel = _parent->selected_audio ();
405         if (sel.size() == 1) {
406                 auto playlist = make_shared<Playlist>();
407                 playlist->add (_parent->film(), sel.front());
408                 try {
409                         /* Loading the audio analysis file is slow, and this ::peak() is called a few times when
410                          * the content selection is changed, so cache it.
411                          */
412                         auto const path = _parent->film()->audio_analysis_path(playlist);
413                         auto cached = _peak_cache.find(path);
414                         if (cached != _peak_cache.end()) {
415                                 peak_dB = cached->second;
416                         } else {
417                                 auto analysis = make_shared<AudioAnalysis>(path);
418                                 peak_dB = linear_to_db(analysis->overall_sample_peak().first.peak) + analysis->gain_correction(playlist);
419                                 _peak_cache[path] = *peak_dB;
420                         }
421                 } catch (...) {
422
423                 }
424         }
425
426         return peak_dB;
427 }
428
429
430 void
431 AudioPanel::setup_peak ()
432 {
433         auto sel = _parent->selected_audio ();
434
435         auto peak_dB = peak ();
436         if (sel.size() != 1) {
437                 _peak->SetLabel (wxT(""));
438         } else {
439                 peak_dB = peak ();
440                 if (peak_dB) {
441                         _peak->SetLabel (wxString::Format(_("Peak: %.2fdB"), *peak_dB));
442                 } else {
443                         _peak->SetLabel (_("Peak: unknown"));
444                 }
445         }
446
447         static auto normal = _peak->GetForegroundColour ();
448
449         if (peak_dB && *peak_dB > -0.5) {
450                 _peak->SetForegroundColour (wxColour (255, 0, 0));
451         } else if (peak_dB && *peak_dB > -3) {
452                 _peak->SetForegroundColour (wxColour (186, 120, 0));
453         } else {
454                 _peak->SetForegroundColour (normal);
455         }
456 }
457
458
459 void
460 AudioPanel::active_jobs_changed (optional<string> old_active, optional<string> new_active)
461 {
462         if (old_active && *old_active == "analyse_audio") {
463                 setup_peak ();
464                 _mapping->Enable (true);
465         } else if (new_active && *new_active == "analyse_audio") {
466                 _mapping->Enable (false);
467         }
468 }
469
470
471 void
472 AudioPanel::set_film (shared_ptr<Film>)
473 {
474         /* We are changing film, so destroy any audio dialog for the old one */
475         _audio_dialog.reset();
476 }
477
478
479 void
480 AudioPanel::fade_in_changed ()
481 {
482         auto const hmsf = _fade_in->get();
483         for (auto i: _parent->selected_audio()) {
484                 auto const vfr = i->active_video_frame_rate(_parent->film());
485                 i->audio->set_fade_in (dcpomatic::ContentTime(hmsf, vfr));
486         }
487 }
488
489
490 void
491 AudioPanel::fade_out_changed ()
492 {
493         auto const hmsf = _fade_out->get();
494         for (auto i: _parent->selected_audio()) {
495                 auto const vfr = i->active_video_frame_rate (_parent->film());
496                 i->audio->set_fade_out (dcpomatic::ContentTime(hmsf, vfr));
497         }
498 }
499
500
501 void
502 AudioPanel::use_same_fades_as_video_changed ()
503 {
504         for (auto content: _parent->selected_audio()) {
505                 content->audio->set_use_same_fades_as_video(_use_same_fades_as_video->GetValue());
506         }
507 }
508