Bump ffmpeg; gain-correct new LUFS/LRA.
[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 <libxml++/libxml++.h>
29 #include <boost/filesystem.hpp>
30 #include <iostream>
31
32 using std::cout;
33 using boost::shared_ptr;
34 using boost::bind;
35 using boost::optional;
36 using boost::const_pointer_cast;
37 using boost::dynamic_pointer_cast;
38
39 /** @param content Content to analyse, or 0 to analyse all of the film's audio */
40 AudioDialog::AudioDialog (wxWindow* parent, shared_ptr<Film> film, shared_ptr<AudioContent> content)
41         : wxDialog (parent, wxID_ANY, _("Audio"), wxDefaultPosition, wxSize (640, 512), wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER | wxFULL_REPAINT_ON_RESIZE)
42         , _film (film)
43         , _channels (film->audio_channels ())
44         , _plot (0)
45 {
46         wxFont subheading_font (*wxNORMAL_FONT);
47         subheading_font.SetWeight (wxFONTWEIGHT_BOLD);
48
49         wxBoxSizer* overall_sizer = new wxBoxSizer (wxVERTICAL);
50         wxBoxSizer* lr_sizer = new wxBoxSizer (wxHORIZONTAL);
51
52         wxBoxSizer* left = new wxBoxSizer (wxVERTICAL);
53
54         _plot = new AudioPlot (this);
55         left->Add (_plot, 1, wxTOP | wxEXPAND, 12);
56         _sample_peak = new wxStaticText (this, wxID_ANY, wxT (""));
57         left->Add (_sample_peak, 0, wxTOP, DCPOMATIC_SIZER_Y_GAP);
58         _true_peak = new wxStaticText (this, wxID_ANY, wxT (""));
59         left->Add (_true_peak, 0, wxTOP, DCPOMATIC_SIZER_Y_GAP);
60         _integrated_loudness = new wxStaticText (this, wxID_ANY, wxT (""));
61         left->Add (_integrated_loudness, 0, wxTOP, DCPOMATIC_SIZER_Y_GAP);
62         _loudness_range = new wxStaticText (this, wxID_ANY, wxT (""));
63         left->Add (_loudness_range, 0, wxTOP, DCPOMATIC_SIZER_Y_GAP);
64
65         lr_sizer->Add (left, 1, wxALL, 12);
66
67         wxBoxSizer* right = new wxBoxSizer (wxVERTICAL);
68
69         {
70                 wxStaticText* m = new wxStaticText (this, wxID_ANY, _("Channels"));
71                 m->SetFont (subheading_font);
72                 right->Add (m, 1, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM, 16);
73         }
74
75         for (int i = 0; i < _channels; ++i) {
76                 _channel_checkbox[i] = new wxCheckBox (this, wxID_ANY, std_to_wx (audio_channel_name (i)));
77                 right->Add (_channel_checkbox[i], 0, wxEXPAND | wxALL, 3);
78                 _channel_checkbox[i]->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AudioDialog::channel_clicked, this, _1));
79         }
80
81         {
82                 wxStaticText* m = new wxStaticText (this, wxID_ANY, _("Type"));
83                 m->SetFont (subheading_font);
84                 right->Add (m, 1, wxALIGN_CENTER_VERTICAL | wxTOP, 16);
85         }
86
87         wxString const types[] = {
88                 _("Peak"),
89                 _("RMS")
90         };
91
92         for (int i = 0; i < AudioPoint::COUNT; ++i) {
93                 _type_checkbox[i] = new wxCheckBox (this, wxID_ANY, types[i]);
94                 right->Add (_type_checkbox[i], 0, wxEXPAND | wxALL, 3);
95                 _type_checkbox[i]->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AudioDialog::type_clicked, this, _1));
96         }
97
98         {
99                 wxStaticText* m = new wxStaticText (this, wxID_ANY, _("Smoothing"));
100                 m->SetFont (subheading_font);
101                 right->Add (m, 1, wxALIGN_CENTER_VERTICAL | wxTOP, 16);
102         }
103
104         _smoothing = new wxSlider (this, wxID_ANY, AudioPlot::max_smoothing / 2, 1, AudioPlot::max_smoothing);
105         _smoothing->Bind (wxEVT_SCROLL_THUMBTRACK, boost::bind (&AudioDialog::smoothing_changed, this));
106         right->Add (_smoothing, 0, wxEXPAND);
107
108         lr_sizer->Add (right, 0, wxALL, 12);
109
110         overall_sizer->Add (lr_sizer);
111
112 #ifdef DCPOMATIC_LINUX
113         wxSizer* buttons = CreateSeparatedButtonSizer (wxCLOSE);
114         if (buttons) {
115                 overall_sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder());
116         }
117 #endif
118
119         SetSizer (overall_sizer);
120         overall_sizer->Layout ();
121         overall_sizer->SetSizeHints (this);
122
123         _film_connection = film->ContentChanged.connect (boost::bind (&AudioDialog::content_changed, this, _2));
124         SetTitle (_("DCP-o-matic audio"));
125
126         if (content) {
127                 _playlist.reset (new Playlist ());
128                 const_pointer_cast<Playlist> (_playlist)->add (content);
129         } else {
130                 _playlist = film->playlist ();
131         }
132 }
133
134 void
135 AudioDialog::try_to_load_analysis ()
136 {
137         if (!IsShown ()) {
138                 return;
139         }
140
141         shared_ptr<const Film> film = _film.lock ();
142         DCPOMATIC_ASSERT (film);
143
144         boost::filesystem::path const path = film->audio_analysis_path (_playlist);
145         if (!boost::filesystem::exists (path)) {
146                 _plot->set_analysis (shared_ptr<AudioAnalysis> ());
147                 _analysis.reset ();
148                 JobManager::instance()->analyse_audio (film, _playlist, _analysis_finished_connection, bind (&AudioDialog::analysis_finished, this));
149                 return;
150         }
151
152         try {
153                 _analysis.reset (new AudioAnalysis (path));
154         } catch (xmlpp::exception& e) {
155                 /* Probably an old-style analysis file: recreate it */
156                 JobManager::instance()->analyse_audio (film, _playlist, _analysis_finished_connection, bind (&AudioDialog::analysis_finished, this));
157                 return;
158         }
159
160         _plot->set_analysis (_analysis);
161         _plot->set_gain_correction (_analysis->gain_correction (_playlist));
162         setup_statistics ();
163
164         /* Set up some defaults if no check boxes are checked */
165
166         int i = 0;
167         while (i < _channels && (!_channel_checkbox[i] || !_channel_checkbox[i]->GetValue ())) {
168                 ++i;
169         }
170
171         if (i == _channels && _channel_checkbox[0]) {
172                 _channel_checkbox[0]->SetValue (true);
173                 _plot->set_channel_visible (0, true);
174         }
175
176         i = 0;
177         while (i < AudioPoint::COUNT && !_type_checkbox[i]->GetValue ()) {
178                 i++;
179         }
180
181         if (i == AudioPoint::COUNT) {
182                 for (int i = 0; i < AudioPoint::COUNT; ++i) {
183                         _type_checkbox[i]->SetValue (true);
184                         _plot->set_type_visible (i, true);
185                 }
186         }
187
188         Refresh ();
189 }
190
191 void
192 AudioDialog::analysis_finished ()
193 {
194         shared_ptr<const Film> film = _film.lock ();
195         DCPOMATIC_ASSERT (film);
196
197         if (!boost::filesystem::exists (film->audio_analysis_path (_playlist))) {
198                 /* We analysed and still nothing showed up, so maybe it was cancelled or it failed.
199                    Give up.
200                 */
201                 _plot->set_message (_("Could not analyse audio."));
202                 return;
203         }
204
205         try_to_load_analysis ();
206 }
207
208 void
209 AudioDialog::channel_clicked (wxCommandEvent& ev)
210 {
211         int c = 0;
212         while (c < _channels && ev.GetEventObject() != _channel_checkbox[c]) {
213                 ++c;
214         }
215
216         DCPOMATIC_ASSERT (c < _channels);
217
218         _plot->set_channel_visible (c, _channel_checkbox[c]->GetValue ());
219 }
220
221 void
222 AudioDialog::content_changed (int p)
223 {
224         if (p == AudioContentProperty::AUDIO_STREAMS) {
225                 try_to_load_analysis ();
226         } else if (p == AudioContentProperty::AUDIO_GAIN) {
227                 if (_playlist->content().size() == 1 && _analysis) {
228                         /* We can use a short-cut to render the effect of this
229                            change, rather than recalculating everything.
230                         */
231                         _plot->set_gain_correction (_analysis->gain_correction (_playlist));
232                         setup_statistics ();
233                 } else {
234                         try_to_load_analysis ();
235                 }
236         }
237 }
238
239 void
240 AudioDialog::type_clicked (wxCommandEvent& ev)
241 {
242         int t = 0;
243         while (t < AudioPoint::COUNT && ev.GetEventObject() != _type_checkbox[t]) {
244                 ++t;
245         }
246
247         DCPOMATIC_ASSERT (t < AudioPoint::COUNT);
248
249         _plot->set_type_visible (t, _type_checkbox[t]->GetValue ());
250 }
251
252 void
253 AudioDialog::smoothing_changed ()
254 {
255         _plot->set_smoothing (_smoothing->GetValue ());
256 }
257
258 void
259 AudioDialog::setup_statistics ()
260 {
261         if (!_analysis) {
262                 return;
263         }
264
265         shared_ptr<Film> film = _film.lock ();
266         if (!film) {
267                 return;
268         }
269
270         if (static_cast<bool>(_analysis->sample_peak ())) {
271
272                 float const peak_dB = 20 * log10 (_analysis->sample_peak().get()) + _analysis->gain_correction (_playlist);
273
274                 _sample_peak->SetLabel (
275                         wxString::Format (
276                                 _("Sample peak is %.2fdB at %s"),
277                                 peak_dB,
278                                 time_to_timecode (_analysis->sample_peak_time().get(), film->video_frame_rate ()).data ()
279                                 )
280                         );
281
282                 if (peak_dB > -3) {
283                         _sample_peak->SetForegroundColour (wxColour (255, 0, 0));
284                 } else {
285                         _sample_peak->SetForegroundColour (wxColour (0, 0, 0));
286                 }
287         }
288
289         if (static_cast<bool>(_analysis->true_peak ())) {
290                 float const peak_dB = 20 * log10 (_analysis->true_peak().get()) + _analysis->gain_correction (_playlist);
291
292                 _true_peak->SetLabel (wxString::Format (_("True peak is %.2fdB"), peak_dB));
293
294                 if (peak_dB > -3) {
295                         _true_peak->SetForegroundColour (wxColour (255, 0, 0));
296                 } else {
297                         _true_peak->SetForegroundColour (wxColour (0, 0, 0));
298                 }
299         }
300
301         /* XXX: check whether it's ok to add dB gain to these quantities */
302
303         if (static_cast<bool>(_analysis->integrated_loudness ())) {
304                 _integrated_loudness->SetLabel (
305                         wxString::Format (
306                                 _("Integrated loudness %.2f LUFS"),
307                                 _analysis->integrated_loudness().get() + _analysis->gain_correction (_playlist)
308                                 )
309                         );
310         }
311
312         if (static_cast<bool>(_analysis->loudness_range ())) {
313                 _loudness_range->SetLabel (
314                         wxString::Format (
315                                 _("Loudness range %.2f LU"),
316                                 _analysis->loudness_range().get() + _analysis->gain_correction (_playlist)
317                                 )
318                         );
319         }
320 }
321
322 bool
323 AudioDialog::Show (bool show)
324 {
325         bool const r = wxDialog::Show (show);
326         try_to_load_analysis ();
327         return r;
328 }