Restore short-cutting of analysis gain updates.
[dcpomatic.git] / src / wx / audio_dialog.cc
1 /*
2     Copyright (C) 2013-2015 Carl Hetherington <cth@carlh.net>
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
15     along with this program; if not, write to the Free Software
16     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17
18 */
19
20 #include "audio_dialog.h"
21 #include "audio_plot.h"
22 #include "wx_util.h"
23 #include "lib/audio_analysis.h"
24 #include "lib/film.h"
25 #include "lib/analyse_audio_job.h"
26 #include "lib/audio_content.h"
27 #include "lib/job_manager.h"
28 #include <boost/filesystem.hpp>
29
30 using std::cout;
31 using boost::shared_ptr;
32 using boost::bind;
33 using boost::optional;
34 using boost::const_pointer_cast;
35 using boost::dynamic_pointer_cast;
36
37 /** @param content Content to analyse, or 0 to analyse all of the film's audio */
38 AudioDialog::AudioDialog (wxWindow* parent, shared_ptr<Film> film, shared_ptr<AudioContent> content)
39         : wxDialog (parent, wxID_ANY, _("Audio"), wxDefaultPosition, wxSize (640, 512), wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER | wxFULL_REPAINT_ON_RESIZE)
40         , _film (film)
41         , _plot (0)
42 {
43         wxFont subheading_font (*wxNORMAL_FONT);
44         subheading_font.SetWeight (wxFONTWEIGHT_BOLD);
45
46         wxBoxSizer* overall_sizer = new wxBoxSizer (wxVERTICAL);
47         wxBoxSizer* lr_sizer = new wxBoxSizer (wxHORIZONTAL);
48
49         wxBoxSizer* left = new wxBoxSizer (wxVERTICAL);
50
51         _plot = new AudioPlot (this);
52         left->Add (_plot, 1, wxALL | wxEXPAND, 12);
53         _peak_time = new wxStaticText (this, wxID_ANY, wxT (""));
54         left->Add (_peak_time, 0, wxALL, 12);
55
56         lr_sizer->Add (left, 1, wxALL, 12);
57
58         wxBoxSizer* right = new wxBoxSizer (wxVERTICAL);
59
60         {
61                 wxStaticText* m = new wxStaticText (this, wxID_ANY, _("Channels"));
62                 m->SetFont (subheading_font);
63                 right->Add (m, 1, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM, 16);
64         }
65
66         for (int i = 0; i < MAX_DCP_AUDIO_CHANNELS; ++i) {
67                 _channel_checkbox[i] = new wxCheckBox (this, wxID_ANY, std_to_wx (audio_channel_name (i)));
68                 right->Add (_channel_checkbox[i], 0, wxEXPAND | wxALL, 3);
69                 _channel_checkbox[i]->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AudioDialog::channel_clicked, this, _1));
70         }
71
72         {
73                 wxStaticText* m = new wxStaticText (this, wxID_ANY, _("Type"));
74                 m->SetFont (subheading_font);
75                 right->Add (m, 1, wxALIGN_CENTER_VERTICAL | wxTOP, 16);
76         }
77
78         wxString const types[] = {
79                 _("Peak"),
80                 _("RMS")
81         };
82
83         for (int i = 0; i < AudioPoint::COUNT; ++i) {
84                 _type_checkbox[i] = new wxCheckBox (this, wxID_ANY, types[i]);
85                 right->Add (_type_checkbox[i], 0, wxEXPAND | wxALL, 3);
86                 _type_checkbox[i]->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AudioDialog::type_clicked, this, _1));
87         }
88
89         {
90                 wxStaticText* m = new wxStaticText (this, wxID_ANY, _("Smoothing"));
91                 m->SetFont (subheading_font);
92                 right->Add (m, 1, wxALIGN_CENTER_VERTICAL | wxTOP, 16);
93         }
94
95         _smoothing = new wxSlider (this, wxID_ANY, AudioPlot::max_smoothing / 2, 1, AudioPlot::max_smoothing);
96         _smoothing->Bind (wxEVT_SCROLL_THUMBTRACK, boost::bind (&AudioDialog::smoothing_changed, this));
97         right->Add (_smoothing, 0, wxEXPAND);
98
99         lr_sizer->Add (right, 0, wxALL, 12);
100
101         overall_sizer->Add (lr_sizer);
102
103 #ifdef DCPOMATIC_LINUX
104         wxSizer* buttons = CreateSeparatedButtonSizer (wxCLOSE);
105         if (buttons) {
106                 overall_sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder());
107         }
108 #endif
109
110         SetSizer (overall_sizer);
111         overall_sizer->Layout ();
112         overall_sizer->SetSizeHints (this);
113
114         _film_connection = film->ContentChanged.connect (boost::bind (&AudioDialog::content_changed, this, _2));
115         SetTitle (_("DCP-o-matic audio"));
116
117         if (content) {
118                 _playlist.reset (new Playlist ());
119                 const_pointer_cast<Playlist> (_playlist)->add (content);
120         } else {
121                 _playlist = film->playlist ();
122         }
123 }
124
125 void
126 AudioDialog::try_to_load_analysis ()
127 {
128         if (!IsShown ()) {
129                 return;
130         }
131
132         shared_ptr<const Film> film = _film.lock ();
133         DCPOMATIC_ASSERT (film);
134
135         boost::filesystem::path path = film->audio_analysis_path (_playlist);
136
137         if (!boost::filesystem::exists (path)) {
138                 _plot->set_analysis (shared_ptr<AudioAnalysis> ());
139                 _analysis.reset ();
140                 shared_ptr<AnalyseAudioJob> job (new AnalyseAudioJob (film, _playlist));
141                 _analysis_finished_connection = job->Finished.connect (bind (&AudioDialog::analysis_finished, this));
142                 JobManager::instance()->add (job);
143                 return;
144         }
145
146         try {
147                 _analysis.reset (new AudioAnalysis (path));
148         } catch (xmlpp::exception& e) {
149                 /* Probably an old-style analysis file: recreate it */
150                 shared_ptr<AnalyseAudioJob> job (new AnalyseAudioJob (film, _playlist));
151                 _analysis_finished_connection = job->Finished.connect (bind (&AudioDialog::analysis_finished, this));
152                 JobManager::instance()->add (job);
153                 return;
154         }
155
156         _plot->set_analysis (_analysis);
157         _plot->set_gain_correction (gain_correction ());
158         setup_peak_time ();
159
160         /* Set up some defaults if no check boxes are checked */
161
162         int i = 0;
163         while (i < MAX_DCP_AUDIO_CHANNELS && (!_channel_checkbox[i] || !_channel_checkbox[i]->GetValue ())) {
164                 ++i;
165         }
166
167         if (i == MAX_DCP_AUDIO_CHANNELS && _channel_checkbox[0]) {
168                 _channel_checkbox[0]->SetValue (true);
169                 _plot->set_channel_visible (0, true);
170         }
171
172         i = 0;
173         while (i < AudioPoint::COUNT && !_type_checkbox[i]->GetValue ()) {
174                 i++;
175         }
176
177         if (i == AudioPoint::COUNT) {
178                 for (int i = 0; i < AudioPoint::COUNT; ++i) {
179                         _type_checkbox[i]->SetValue (true);
180                         _plot->set_type_visible (i, true);
181                 }
182         }
183
184         Refresh ();
185 }
186
187 void
188 AudioDialog::analysis_finished ()
189 {
190         shared_ptr<const Film> film = _film.lock ();
191         DCPOMATIC_ASSERT (film);
192
193         if (!boost::filesystem::exists (film->audio_analysis_path (_playlist))) {
194                 /* We analysed and still nothing showed up, so maybe it was cancelled or it failed.
195                    Give up.
196                 */
197                 _plot->set_message (_("Could not analyse audio."));
198                 return;
199         }
200
201         try_to_load_analysis ();
202 }
203
204 void
205 AudioDialog::channel_clicked (wxCommandEvent& ev)
206 {
207         int c = 0;
208         while (c < MAX_DCP_AUDIO_CHANNELS && ev.GetEventObject() != _channel_checkbox[c]) {
209                 ++c;
210         }
211
212         DCPOMATIC_ASSERT (c < MAX_DCP_AUDIO_CHANNELS);
213
214         _plot->set_channel_visible (c, _channel_checkbox[c]->GetValue ());
215 }
216
217 void
218 AudioDialog::content_changed (int p)
219 {
220         if (p == AudioContentProperty::AUDIO_STREAMS) {
221                 try_to_load_analysis ();
222         } else if (p == AudioContentProperty::AUDIO_GAIN) {
223                 if (_playlist->content().size() == 1) {
224                         /* We can use a short-cut to render the effect of this
225                            change, rather than recalculating everything.
226                         */
227                         _plot->set_gain_correction (gain_correction ());
228                         setup_peak_time ();
229                 } else {
230                         try_to_load_analysis ();
231                 }
232         }
233 }
234
235 void
236 AudioDialog::type_clicked (wxCommandEvent& ev)
237 {
238         int t = 0;
239         while (t < AudioPoint::COUNT && ev.GetEventObject() != _type_checkbox[t]) {
240                 ++t;
241         }
242
243         DCPOMATIC_ASSERT (t < AudioPoint::COUNT);
244
245         _plot->set_type_visible (t, _type_checkbox[t]->GetValue ());
246 }
247
248 void
249 AudioDialog::smoothing_changed ()
250 {
251         _plot->set_smoothing (_smoothing->GetValue ());
252 }
253
254 void
255 AudioDialog::setup_peak_time ()
256 {
257         if (!_analysis || !_analysis->peak ()) {
258                 return;
259         }
260
261         shared_ptr<Film> film = _film.lock ();
262         if (!film) {
263                 return;
264         }
265
266         float const peak_dB = 20 * log10 (_analysis->peak().get()) + gain_correction ();
267
268         _peak_time->SetLabel (
269                 wxString::Format (
270                         _("Peak is %.2fdB at %s"),
271                         peak_dB,
272                         time_to_timecode (_analysis->peak_time().get(), film->video_frame_rate ()).data ()
273                         )
274                 );
275
276         if (peak_dB > -3) {
277                 _peak_time->SetForegroundColour (wxColour (255, 0, 0));
278         } else {
279                 _peak_time->SetForegroundColour (wxColour (0, 0, 0));
280         }
281 }
282
283 bool
284 AudioDialog::Show (bool show)
285 {
286         bool const r = wxDialog::Show (show);
287         try_to_load_analysis ();
288         return r;
289 }
290
291 /** @return gain correction in dB required to be added to raw gain values to render
292  *  the dialog correctly.
293  */
294 float
295 AudioDialog::gain_correction ()
296 {
297         if (_playlist->content().size() == 1 && _analysis->analysis_gain ()) {
298                 /* In this case we know that the analysis was of a single piece of content and
299                    we know that content's gain when the analysis was run.  Hence we can work out
300                    what correction is now needed to make it look `right'.
301                 */
302                 shared_ptr<const AudioContent> ac = dynamic_pointer_cast<const AudioContent> (_playlist->content().front ());
303                 DCPOMATIC_ASSERT (ac);
304                 return ac->audio_gain() - _analysis->analysis_gain().get ();
305         }
306
307         return 0.0f;
308 }