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