Add channel details to high-audio-level hints (#822).
authorCarl Hetherington <cth@carlh.net>
Thu, 25 Aug 2016 10:41:21 +0000 (11:41 +0100)
committerCarl Hetherington <cth@carlh.net>
Thu, 25 Aug 2016 15:16:37 +0000 (16:16 +0100)
12 files changed:
src/lib/analyse_audio_job.cc
src/lib/analyse_audio_job.h
src/lib/audio_analysis.cc
src/lib/audio_analysis.h
src/lib/exceptions.h
src/lib/film.cc
src/lib/hints.cc
src/lib/util.cc
src/lib/util.h
src/wx/audio_dialog.cc
src/wx/audio_panel.cc
test/audio_analysis_test.cc

index bfbf33a..9fce354 100644 (file)
@@ -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<const Film> film, shared_ptr<const
        , _done (0)
        , _samples_per_point (1)
        , _current (0)
-       , _sample_peak (0)
-       , _sample_peak_frame (0)
+       , _sample_peak (new float[film->audio_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<Filter*> (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<AudioAnalysis::PeakTime> 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<float> 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<const AudioBuffers> 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) {
index ce86e62..ee20bed 100644 (file)
@@ -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<AudioAnalysis> _analysis;
 
index 1e5d089..022c593 100644 (file)
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2012-2015 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2012-2016 Carl Hetherington <cth@carlh.net>
 
     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<int>("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<AudioPoint> channel;
 
@@ -64,19 +74,18 @@ AudioAnalysis::AudioAnalysis (boost::filesystem::path filename)
                _data.push_back (channel);
        }
 
-       _sample_peak = f.optional_number_child<float> ("Peak");
-       if (!_sample_peak) {
-               /* New key */
-               _sample_peak = f.optional_number_child<float> ("SamplePeak");
+       BOOST_FOREACH (cxml::ConstNodePtr i, f.node_children ("SamplePeak")) {
+               _sample_peak.push_back (
+                       PeakTime (
+                               dcp::raw_convert<float>(i->content()), DCPTime(i->number_attribute<Frame>("Time"))
+                               )
+                       );
        }
 
-       if (f.optional_number_child<DCPTime::Type> ("PeakTime")) {
-               _sample_peak_time = DCPTime (f.number_child<DCPTime::Type> ("PeakTime"));
-       } else if (f.optional_number_child<DCPTime::Type> ("SamplePeakTime")) {
-               _sample_peak_time = DCPTime (f.number_child<DCPTime::Type> ("SamplePeakTime"));
+       BOOST_FOREACH (cxml::ConstNodePtr i, f.node_children ("TruePeak")) {
+               _true_peak.push_back (dcp::raw_convert<float> (i->content ()));
        }
 
-       _true_peak = f.optional_number_child<float> ("TruePeak");
        _integrated_loudness = f.optional_number_child<float> ("IntegratedLoudness");
        _loudness_range = f.optional_number_child<float> ("LoudnessRange");
 
@@ -116,6 +125,8 @@ AudioAnalysis::write (boost::filesystem::path filename)
        shared_ptr<xmlpp::Document> doc (new xmlpp::Document);
        xmlpp::Element* root = doc->create_root_node ("AudioAnalysis");
 
+       root->add_child("Version")->add_child_text (raw_convert<string> (_current_state_version));
+
        BOOST_FOREACH (vector<AudioPoint>& 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<string> (_sample_peak.get ()));
-               root->add_child("SamplePeakTime")->add_child_text (raw_convert<string> (_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<string> (_sample_peak[i].peak));
+               n->set_attribute ("Time", raw_convert<string> (_sample_peak[i].time.get()));
        }
 
-       if (_true_peak) {
-               root->add_child("TruePeak")->add_child_text (raw_convert<string> (_true_peak.get ()));
+       BOOST_FOREACH (float i, _true_peak) {
+               root->add_child("TruePeak")->add_child_text (raw_convert<string> (i));
        }
 
        if (_integrated_loudness) {
@@ -161,3 +173,34 @@ AudioAnalysis::gain_correction (shared_ptr<const Playlist> playlist)
 
        return 0.0f;
 }
+
+/** @return Peak across all channels, and the channel number it is on */
+pair<AudioAnalysis::PeakTime, int>
+AudioAnalysis::overall_sample_peak () const
+{
+       optional<PeakTime> 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<float>
+AudioAnalysis::overall_true_peak () const
+{
+       optional<float> p;
+
+       BOOST_FOREACH (float i, _true_peak) {
+               if (!p || i > *p) {
+                       p = i;
+               }
+       }
+
+       return p;
+}
index 9acd491..a8ef4fb 100644 (file)
@@ -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<PeakTime> peak) {
                _sample_peak = peak;
-               _sample_peak_time = time;
        }
 
-       void set_true_peak (float peak) {
+       void set_true_peak (std::vector<float> peak) {
                _true_peak = peak;
        }
 
@@ -63,18 +72,18 @@ public:
        int points (int c) const;
        int channels () const;
 
-       boost::optional<float> sample_peak () const {
+       std::vector<PeakTime> sample_peak () const {
                return _sample_peak;
        }
 
-       boost::optional<DCPTime> sample_peak_time () const {
-               return _sample_peak_time;
-       }
+       std::pair<PeakTime, int> overall_sample_peak () const;
 
-       boost::optional<float> true_peak () const {
+       std::vector<float> true_peak () const {
                return _true_peak;
        }
 
+       boost::optional<float> overall_true_peak () const;
+
        boost::optional<float> integrated_loudness () const {
                return _integrated_loudness;
        }
@@ -97,9 +106,8 @@ public:
 
 private:
        std::vector<std::vector<AudioPoint> > _data;
-       boost::optional<float> _sample_peak;
-       boost::optional<DCPTime> _sample_peak_time;
-       boost::optional<float> _true_peak;
+       std::vector<PeakTime> _sample_peak;
+       std::vector<float> _true_peak;
        boost::optional<float> _integrated_loudness;
        boost::optional<float> _loudness_range;
        /** If this analysis was run on a single piece of
@@ -107,6 +115,8 @@ private:
         *  happened.
         */
        boost::optional<double> _analysis_gain;
+
+       static int const _current_state_version;
 };
 
 #endif
index 75f4a8c..98727e0 100644 (file)
@@ -241,4 +241,12 @@ public:
        {}
 };
 
+class OldFormatError : public std::runtime_error
+{
+public:
+       OldFormatError (std::string s)
+               : std::runtime_error (s)
+       {}
+};
+
 #endif
index e9788d0..13a03d9 100644 (file)
@@ -1390,24 +1390,12 @@ Film::audio_output_names () const
        DCPOMATIC_ASSERT (MAX_DCP_AUDIO_CHANNELS == 16);
 
        vector<string> 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<string> (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
index bf0f440..d196b7d 100644 (file)
@@ -28,6 +28,8 @@
 #include "ratio.h"
 #include "audio_analysis.h"
 #include "compose.hpp"
+#include "util.h"
+#include <dcp/raw_convert.h>
 #include <boost/foreach.hpp>
 #include <boost/algorithm/string.hpp>
 
@@ -128,15 +130,30 @@ get_hints (shared_ptr<const Film> film)
        boost::filesystem::path path = film->audio_analysis_path (film->playlist ());
        if (boost::filesystem::exists (path)) {
                shared_ptr<AudioAnalysis> 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<AudioAnalysis::PeakTime> sample_peak = an->sample_peak ();
+               vector<float> 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<string> (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;
index e497ecf..71b48a5 100644 (file)
@@ -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)
 {
index e786bb9..f9b4d0e 100644 (file)
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2012-2015 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2012-2016 Carl Hetherington <cth@carlh.net>
 
     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::filesystem::path>, 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
index b7f1f61..b086782 100644 (file)
@@ -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<bool>(_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<AudioAnalysis::PeakTime, int> 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<bool>(_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));
 
index cda38ea..4801fab 100644 (file)
@@ -320,15 +320,11 @@ AudioPanel::setup_peak ()
                playlist->add (sel.front ());
                try {
                        shared_ptr<AudioAnalysis> 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"));
                }
index c960884..8328c7c 100644 (file)
@@ -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<AudioAnalysis::PeakTime> 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