From 54038beb4437c027e584fc95110f6fd4dbf2207d Mon Sep 17 00:00:00 2001 From: Carl Hetherington Date: Thu, 25 Aug 2016 11:41:21 +0100 Subject: [PATCH] Add channel details to high-audio-level hints (#822). --- src/lib/analyse_audio_job.cc | 25 ++++++++---- src/lib/analyse_audio_job.h | 4 +- src/lib/audio_analysis.cc | 73 ++++++++++++++++++++++++++++-------- src/lib/audio_analysis.h | 32 ++++++++++------ src/lib/exceptions.h | 8 ++++ src/lib/film.cc | 24 +++--------- src/lib/hints.cc | 29 +++++++++++--- src/lib/util.cc | 34 ++++++++++++++++- src/lib/util.h | 3 +- src/wx/audio_dialog.cc | 45 ++++++++++++---------- src/wx/audio_panel.cc | 12 ++---- test/audio_analysis_test.cc | 18 +++++---- 12 files changed, 209 insertions(+), 98 deletions(-) diff --git a/src/lib/analyse_audio_job.cc b/src/lib/analyse_audio_job.cc index bfbf33a9d..9fce354df 100644 --- a/src/lib/analyse_audio_job.cc +++ b/src/lib/analyse_audio_job.cc @@ -41,6 +41,7 @@ extern "C" { #include "i18n.h" using std::string; +using std::vector; using std::max; using std::min; using std::cout; @@ -55,8 +56,8 @@ AnalyseAudioJob::AnalyseAudioJob (shared_ptr film, shared_ptraudio_channels()]) + , _sample_peak_frame (new Frame[film->audio_channels()]) #ifdef DCPOMATIC_HAVE_EBUR128_PATCHED_FFMPEG , _ebur128 (new AudioFilterGraph (film->audio_frame_rate(), film->audio_channels())) #endif @@ -73,6 +74,8 @@ AnalyseAudioJob::~AnalyseAudioJob () delete const_cast (i); } delete[] _current; + delete[] _sample_peak; + delete[] _sample_peak_frame; } string @@ -127,14 +130,20 @@ AnalyseAudioJob::run () } } - _analysis->set_sample_peak (_sample_peak, DCPTime::from_frames (_sample_peak_frame, _film->audio_frame_rate ())); + vector sample_peak; + for (int i = 0; i < _film->audio_channels(); ++i) { + sample_peak.push_back ( + AudioAnalysis::PeakTime (_sample_peak[i], DCPTime::from_frames (_sample_peak_frame[i], _film->audio_frame_rate ())) + ); + } + _analysis->set_sample_peak (sample_peak); #ifdef DCPOMATIC_HAVE_EBUR128_PATCHED_FFMPEG if (Config::instance()->analyse_ebur128 ()) { void* eb = _ebur128->get("Parsed_ebur128_0")->priv; - double true_peak = 0; + vector true_peak; for (int i = 0; i < _film->audio_channels(); ++i) { - true_peak = max (true_peak, av_ebur128_get_true_peaks(eb)[i]); + true_peak.push_back (av_ebur128_get_true_peaks(eb)[i]); } _analysis->set_true_peak (true_peak); _analysis->set_integrated_loudness (av_ebur128_get_integrated_loudness(eb)); @@ -176,9 +185,9 @@ AnalyseAudioJob::analyse (shared_ptr b) _current[j][AudioPoint::RMS] += pow (s, 2); _current[j][AudioPoint::PEAK] = max (_current[j][AudioPoint::PEAK], as); - if (as > _sample_peak) { - _sample_peak = as; - _sample_peak_frame = _done + i; + if (as > _sample_peak[j]) { + _sample_peak[j] = as; + _sample_peak_frame[j] = _done + i; } if (((_done + i) % _samples_per_point) == 0) { diff --git a/src/lib/analyse_audio_job.h b/src/lib/analyse_audio_job.h index ce86e62cf..ee20bedc4 100644 --- a/src/lib/analyse_audio_job.h +++ b/src/lib/analyse_audio_job.h @@ -63,8 +63,8 @@ private: int64_t _samples_per_point; AudioPoint* _current; - float _sample_peak; - Frame _sample_peak_frame; + float* _sample_peak; + Frame* _sample_peak_frame; boost::shared_ptr _analysis; diff --git a/src/lib/audio_analysis.cc b/src/lib/audio_analysis.cc index 1e5d08925..022c5935d 100644 --- a/src/lib/audio_analysis.cc +++ b/src/lib/audio_analysis.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2012-2015 Carl Hetherington + Copyright (C) 2012-2016 Carl Hetherington This file is part of DCP-o-matic. @@ -39,11 +39,16 @@ using std::string; using std::vector; using std::cout; using std::max; +using std::pair; +using std::make_pair; using std::list; using boost::shared_ptr; +using boost::optional; using boost::dynamic_pointer_cast; using dcp::raw_convert; +int const AudioAnalysis::_current_state_version = 2; + AudioAnalysis::AudioAnalysis (int channels) { _data.resize (channels); @@ -54,6 +59,11 @@ AudioAnalysis::AudioAnalysis (boost::filesystem::path filename) cxml::Document f ("AudioAnalysis"); f.read_file (filename); + if (f.optional_number_child("Version").get_value_or(1) < _current_state_version) { + /* Too old. Throw an exception so that this analysis is re-run. */ + throw OldFormatError ("Audio analysis file is too old"); + } + BOOST_FOREACH (cxml::NodePtr i, f.node_children ("Channel")) { vector channel; @@ -64,19 +74,18 @@ AudioAnalysis::AudioAnalysis (boost::filesystem::path filename) _data.push_back (channel); } - _sample_peak = f.optional_number_child ("Peak"); - if (!_sample_peak) { - /* New key */ - _sample_peak = f.optional_number_child ("SamplePeak"); + BOOST_FOREACH (cxml::ConstNodePtr i, f.node_children ("SamplePeak")) { + _sample_peak.push_back ( + PeakTime ( + dcp::raw_convert(i->content()), DCPTime(i->number_attribute("Time")) + ) + ); } - if (f.optional_number_child ("PeakTime")) { - _sample_peak_time = DCPTime (f.number_child ("PeakTime")); - } else if (f.optional_number_child ("SamplePeakTime")) { - _sample_peak_time = DCPTime (f.number_child ("SamplePeakTime")); + BOOST_FOREACH (cxml::ConstNodePtr i, f.node_children ("TruePeak")) { + _true_peak.push_back (dcp::raw_convert (i->content ())); } - _true_peak = f.optional_number_child ("TruePeak"); _integrated_loudness = f.optional_number_child ("IntegratedLoudness"); _loudness_range = f.optional_number_child ("LoudnessRange"); @@ -116,6 +125,8 @@ AudioAnalysis::write (boost::filesystem::path filename) shared_ptr doc (new xmlpp::Document); xmlpp::Element* root = doc->create_root_node ("AudioAnalysis"); + root->add_child("Version")->add_child_text (raw_convert (_current_state_version)); + BOOST_FOREACH (vector& i, _data) { xmlpp::Element* channel = root->add_child ("Channel"); BOOST_FOREACH (AudioPoint& j, i) { @@ -123,13 +134,14 @@ AudioAnalysis::write (boost::filesystem::path filename) } } - if (_sample_peak) { - root->add_child("SamplePeak")->add_child_text (raw_convert (_sample_peak.get ())); - root->add_child("SamplePeakTime")->add_child_text (raw_convert (_sample_peak_time.get().get ())); + for (size_t i = 0; i < _sample_peak.size(); ++i) { + xmlpp::Element* n = root->add_child("SamplePeak"); + n->add_child_text (raw_convert (_sample_peak[i].peak)); + n->set_attribute ("Time", raw_convert (_sample_peak[i].time.get())); } - if (_true_peak) { - root->add_child("TruePeak")->add_child_text (raw_convert (_true_peak.get ())); + BOOST_FOREACH (float i, _true_peak) { + root->add_child("TruePeak")->add_child_text (raw_convert (i)); } if (_integrated_loudness) { @@ -161,3 +173,34 @@ AudioAnalysis::gain_correction (shared_ptr playlist) return 0.0f; } + +/** @return Peak across all channels, and the channel number it is on */ +pair +AudioAnalysis::overall_sample_peak () const +{ + optional pt; + int c; + + for (size_t i = 0; i < _sample_peak.size(); ++i) { + if (!pt || _sample_peak[i].peak > pt->peak) { + pt = _sample_peak[i]; + c = i; + } + } + + return make_pair (pt.get(), c); +} + +optional +AudioAnalysis::overall_true_peak () const +{ + optional p; + + BOOST_FOREACH (float i, _true_peak) { + if (!p || i > *p) { + p = i; + } + } + + return p; +} diff --git a/src/lib/audio_analysis.h b/src/lib/audio_analysis.h index 9acd491ce..a8ef4fb2d 100644 --- a/src/lib/audio_analysis.h +++ b/src/lib/audio_analysis.h @@ -42,12 +42,21 @@ public: void add_point (int c, AudioPoint const & p); - void set_sample_peak (float peak, DCPTime time) { + struct PeakTime { + PeakTime (float p, DCPTime t) + : peak (p) + , time (t) + {} + + float peak; + DCPTime time; + }; + + void set_sample_peak (std::vector peak) { _sample_peak = peak; - _sample_peak_time = time; } - void set_true_peak (float peak) { + void set_true_peak (std::vector peak) { _true_peak = peak; } @@ -63,18 +72,18 @@ public: int points (int c) const; int channels () const; - boost::optional sample_peak () const { + std::vector sample_peak () const { return _sample_peak; } - boost::optional sample_peak_time () const { - return _sample_peak_time; - } + std::pair overall_sample_peak () const; - boost::optional true_peak () const { + std::vector true_peak () const { return _true_peak; } + boost::optional overall_true_peak () const; + boost::optional integrated_loudness () const { return _integrated_loudness; } @@ -97,9 +106,8 @@ public: private: std::vector > _data; - boost::optional _sample_peak; - boost::optional _sample_peak_time; - boost::optional _true_peak; + std::vector _sample_peak; + std::vector _true_peak; boost::optional _integrated_loudness; boost::optional _loudness_range; /** If this analysis was run on a single piece of @@ -107,6 +115,8 @@ private: * happened. */ boost::optional _analysis_gain; + + static int const _current_state_version; }; #endif diff --git a/src/lib/exceptions.h b/src/lib/exceptions.h index 75f4a8cd1..98727e0cc 100644 --- a/src/lib/exceptions.h +++ b/src/lib/exceptions.h @@ -241,4 +241,12 @@ public: {} }; +class OldFormatError : public std::runtime_error +{ +public: + OldFormatError (std::string s) + : std::runtime_error (s) + {} +}; + #endif diff --git a/src/lib/film.cc b/src/lib/film.cc index e9788d09e..13a03d929 100644 --- a/src/lib/film.cc +++ b/src/lib/film.cc @@ -1390,24 +1390,12 @@ Film::audio_output_names () const DCPOMATIC_ASSERT (MAX_DCP_AUDIO_CHANNELS == 16); vector n; - n.push_back (_("L")); - n.push_back (_("R")); - n.push_back (_("C")); - n.push_back (_("Lfe")); - n.push_back (_("Ls")); - n.push_back (_("Rs")); - n.push_back (_("HI")); - n.push_back (_("VI")); - n.push_back (_("Lc")); - n.push_back (_("Rc")); - n.push_back (_("BsL")); - n.push_back (_("BsR")); - n.push_back (_("DBP")); - n.push_back (_("DBS")); - n.push_back (""); - n.push_back (""); - - return vector (n.begin(), n.begin() + audio_channels ()); + + for (int i = 0; i < audio_channels(); ++i) { + n.push_back (short_audio_channel_name (i)); + } + + return n; } void diff --git a/src/lib/hints.cc b/src/lib/hints.cc index bf0f44096..d196b7ddb 100644 --- a/src/lib/hints.cc +++ b/src/lib/hints.cc @@ -28,6 +28,8 @@ #include "ratio.h" #include "audio_analysis.h" #include "compose.hpp" +#include "util.h" +#include #include #include @@ -128,15 +130,30 @@ get_hints (shared_ptr film) boost::filesystem::path path = film->audio_analysis_path (film->playlist ()); if (boost::filesystem::exists (path)) { shared_ptr an (new AudioAnalysis (path)); - if (an->sample_peak() || an->true_peak()) { - float const peak = max (an->sample_peak().get_value_or(0), an->true_peak().get_value_or(0)); + + string ch; + + vector sample_peak = an->sample_peak (); + vector true_peak = an->true_peak (); + + for (size_t i = 0; i < sample_peak.size(); ++i) { + float const peak = max (sample_peak[i].peak, true_peak.empty() ? 0 : true_peak[i]); float const peak_dB = 20 * log10 (peak) + an->gain_correction (film->playlist ()); - if (peak_dB > -3 && peak_dB < -0.5) { - hints.push_back (_("Your audio level is very high. You should reduce the gain of your audio content.")); - } else if (peak_dB > -0.5) { - hints.push_back (_("Your audio level is very close to clipping. You should reduce the gain of your audio content.")); + if (peak_dB > -3) { + ch += dcp::raw_convert (short_audio_channel_name (i)) + ", "; } } + + ch = ch.substr (0, ch.length() - 2); + + if (!ch.empty ()) { + hints.push_back ( + String::compose ( + _("Your audio level is very high (on %1). You should reduce the gain of your audio content."), + ch + ) + ); + } } return hints; diff --git a/src/lib/util.cc b/src/lib/util.cc index e497ecf3c..71b48a520 100644 --- a/src/lib/util.cc +++ b/src/lib/util.cc @@ -485,8 +485,7 @@ audio_channel_name (int c) DCPOMATIC_ASSERT (MAX_DCP_AUDIO_CHANNELS == 16); /// TRANSLATORS: these are the names of audio channels; Lfe (sub) is the low-frequency - /// enhancement channel (sub-woofer). HI is the hearing-impaired audio track and - /// VI is the visually-impaired audio track (audio describe). + /// enhancement channel (sub-woofer). string const channels[] = { _("Left"), _("Right"), @@ -509,6 +508,37 @@ audio_channel_name (int c) return channels[c]; } +string +short_audio_channel_name (int c) +{ + DCPOMATIC_ASSERT (MAX_DCP_AUDIO_CHANNELS == 16); + + /// TRANSLATORS: these are short names of audio channels; Lfe is the low-frequency + /// enhancement channel (sub-woofer). HI is the hearing-impaired audio track and + /// VI is the visually-impaired audio track (audio describe). + string const channels[] = { + _("L"), + _("R"), + _("C"), + _("Lfe"), + _("Ls"), + _("Rs"), + _("HI"), + _("VI"), + _("Lc"), + _("Rc"), + _("BsL"), + _("BsR"), + _("DBP"), + _("DBPS"), + _(""), + _("") + }; + + return channels[c]; +} + + bool valid_image_file (boost::filesystem::path f) { diff --git a/src/lib/util.h b/src/lib/util.h index e786bb9c1..f9b4d0e05 100644 --- a/src/lib/util.h +++ b/src/lib/util.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2012-2015 Carl Hetherington + Copyright (C) 2012-2016 Carl Hetherington This file is part of DCP-o-matic. @@ -64,6 +64,7 @@ extern void dcpomatic_setup_gettext_i18n (std::string); extern std::string digest_head_tail (std::vector, boost::uintmax_t size); extern void ensure_ui_thread (); extern std::string audio_channel_name (int); +extern std::string short_audio_channel_name (int); extern bool valid_image_file (boost::filesystem::path); extern bool valid_j2k_file (boost::filesystem::path); #ifdef DCPOMATIC_WINDOWS diff --git a/src/wx/audio_dialog.cc b/src/wx/audio_dialog.cc index b7f1f613b..b0867823e 100644 --- a/src/wx/audio_dialog.cc +++ b/src/wx/audio_dialog.cc @@ -33,6 +33,8 @@ using std::cout; using std::list; +using std::vector; +using std::pair; using boost::shared_ptr; using boost::bind; using boost::optional; @@ -166,8 +168,12 @@ AudioDialog::try_to_load_analysis () try { _analysis.reset (new AudioAnalysis (path)); + } catch (OldFormatError& e) { + /* An old analysis file: recreate it */ + JobManager::instance()->analyse_audio (film, _playlist, _analysis_finished_connection, bind (&AudioDialog::analysis_finished, this)); + return; } catch (xmlpp::exception& e) { - /* Probably an old-style analysis file: recreate it */ + /* Probably a (very) old-style analysis file: recreate it */ JobManager::instance()->analyse_audio (film, _playlist, _analysis_finished_connection, bind (&AudioDialog::analysis_finished, this)); return; } @@ -300,27 +306,26 @@ AudioDialog::setup_statistics () return; } - if (static_cast(_analysis->sample_peak ())) { - - float const peak_dB = 20 * log10 (_analysis->sample_peak().get()) + _analysis->gain_correction (_playlist); - - _sample_peak->SetLabel ( - wxString::Format ( - _("Sample peak is %.2fdB at %s"), - peak_dB, - time_to_timecode (_analysis->sample_peak_time().get(), film->video_frame_rate ()).data () - ) - ); - - if (peak_dB > -3) { - _sample_peak->SetForegroundColour (wxColour (255, 0, 0)); - } else { - _sample_peak->SetForegroundColour (wxColour (0, 0, 0)); - } + pair const peak = _analysis->overall_sample_peak (); + float const peak_dB = 20 * log10 (peak.first.peak) + _analysis->gain_correction (_playlist); + _sample_peak->SetLabel ( + wxString::Format ( + _("Sample peak is %.2fdB at %s on %s"), + peak_dB, + time_to_timecode (peak.first.time, film->video_frame_rate ()).data (), + std_to_wx (short_audio_channel_name (peak.second)).data () + ) + ); + + if (peak_dB > -3) { + _sample_peak->SetForegroundColour (wxColour (255, 0, 0)); + } else { + _sample_peak->SetForegroundColour (wxColour (0, 0, 0)); } - if (static_cast(_analysis->true_peak ())) { - float const peak_dB = 20 * log10 (_analysis->true_peak().get()) + _analysis->gain_correction (_playlist); + if (_analysis->overall_true_peak()) { + float const peak = _analysis->overall_true_peak().get(); + float const peak_dB = 20 * log10 (peak) + _analysis->gain_correction (_playlist); _true_peak->SetLabel (wxString::Format (_("True peak is %.2fdB"), peak_dB)); diff --git a/src/wx/audio_panel.cc b/src/wx/audio_panel.cc index cda38eaf6..4801fab00 100644 --- a/src/wx/audio_panel.cc +++ b/src/wx/audio_panel.cc @@ -320,15 +320,11 @@ AudioPanel::setup_peak () playlist->add (sel.front ()); try { shared_ptr analysis (new AudioAnalysis (_parent->film()->audio_analysis_path (playlist))); - if (analysis->sample_peak ()) { - float const peak_dB = 20 * log10 (analysis->sample_peak().get()) + analysis->gain_correction (playlist); - if (peak_dB > -3) { - alert = true; - } - _peak->SetLabel (wxString::Format (_("Peak: %.2fdB"), peak_dB)); - } else { - _peak->SetLabel (_("Peak: unknown")); + float const peak_dB = 20 * log10 (analysis->overall_sample_peak().first.peak) + analysis->gain_correction (playlist); + if (peak_dB > -3) { + alert = true; } + _peak->SetLabel (wxString::Format (_("Peak: %.2fdB"), peak_dB)); } catch (...) { _peak->SetLabel (_("Peak: unknown")); } diff --git a/test/audio_analysis_test.cc b/test/audio_analysis_test.cc index c960884a1..8328c7cd2 100644 --- a/test/audio_analysis_test.cc +++ b/test/audio_analysis_test.cc @@ -36,6 +36,7 @@ #include "lib/playlist.h" #include "test.h" +using std::vector; using boost::shared_ptr; static float @@ -61,9 +62,11 @@ BOOST_AUTO_TEST_CASE (audio_analysis_serialisation_test) } } - float const peak = random_float (); - DCPTime const peak_time = DCPTime (rand ()); - a.set_sample_peak (peak, peak_time); + vector peak; + for (int i = 0; i < channels; ++i) { + peak.push_back (AudioAnalysis::PeakTime (random_float(), DCPTime (rand()))); + } + a.set_sample_peak (peak); a.write ("build/test/audio_analysis_serialisation_test"); @@ -79,10 +82,11 @@ BOOST_AUTO_TEST_CASE (audio_analysis_serialisation_test) } } - BOOST_CHECK (b.sample_peak ()); - BOOST_CHECK_CLOSE (b.sample_peak().get(), peak, 1); - BOOST_CHECK (b.sample_peak_time ()); - BOOST_CHECK_EQUAL (b.sample_peak_time().get().get(), peak_time.get()); + BOOST_REQUIRE_EQUAL (b.sample_peak().size(), 3); + for (int i = 0; i < channels; ++i) { + BOOST_CHECK_CLOSE (b.sample_peak()[i].peak, peak[i].peak, 1); + BOOST_CHECK_EQUAL (b.sample_peak()[i].time.get(), peak[i].time.get()); + } } static void -- 2.30.2