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