Basic (untested) ebur128 (#368).
authorCarl Hetherington <cth@carlh.net>
Wed, 18 Nov 2015 17:07:07 +0000 (17:07 +0000)
committerCarl Hetherington <cth@carlh.net>
Wed, 18 Nov 2015 20:52:20 +0000 (20:52 +0000)
15 files changed:
ChangeLog
cscript
src/lib/analyse_audio_job.cc
src/lib/analyse_audio_job.h
src/lib/audio_analysis.cc
src/lib/audio_analysis.h
src/lib/filter_graph.cc
src/lib/filter_graph.h
src/lib/video_filter_graph.cc
src/lib/video_filter_graph.h
src/lib/wscript
src/wx/audio_dialog.cc
src/wx/audio_dialog.h
src/wx/audio_panel.cc
test/audio_analysis_test.cc

index 47fc59b..0869080 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,7 @@
+2015-11-18  c.hetherington  <cth@carlh.net>
+
+       * Add LUFS / LRA analysis using FFmpeg's ebur128 filter (#368).
+
 2015-11-17  Carl Hetherington  <cth@carlh.net>
 
        * Bump libdcp for fix to encoded date header in MXF files when
diff --git a/cscript b/cscript
index b8f5f88..d8736cb 100644 (file)
--- a/cscript
+++ b/cscript
@@ -278,7 +278,7 @@ def dependencies(target):
     else:
         ffmpeg_options = {}
 
-    return (('ffmpeg-cdist', 'd0986a9', ffmpeg_options),
+    return (('ffmpeg-cdist', 'b9d9b12', ffmpeg_options),
             ('libdcp', '960fa45'),
             ('libsub', '8b82c40'))
 
index 525ac6c..5dba565 100644 (file)
 #include "film.h"
 #include "player.h"
 #include "playlist.h"
+#include "filter.h"
+#include "audio_filter_graph.h"
+extern "C" {
+#include <libavutil/channel_layout.h>
+}
 #include <boost/foreach.hpp>
 #include <iostream>
 
@@ -39,20 +44,35 @@ using boost::dynamic_pointer_cast;
 
 int const AnalyseAudioJob::_num_points = 1024;
 
+extern "C" {
+/* I added these functions to the FFmpeg that DCP-o-matic is built with, but there isn't an
+   existing header file to put the prototype in.  Cheat by putting it in here.
+*/
+double* av_ebur128_get_true_peaks         (void* context);
+double* av_ebur128_get_sample_peaks       (void* context);
+double  av_ebur128_get_integrated_loudness(void* context);
+double  av_ebur128_get_loudness_range     (void* context);
+}
+
 AnalyseAudioJob::AnalyseAudioJob (shared_ptr<const Film> film, shared_ptr<const Playlist> playlist)
        : Job (film)
        , _playlist (playlist)
        , _done (0)
        , _samples_per_point (1)
        , _current (0)
-       , _overall_peak (0)
-       , _overall_peak_frame (0)
+       , _sample_peak (0)
+       , _sample_peak_frame (0)
+       , _ebur128 (new AudioFilterGraph (film->audio_frame_rate(), av_get_default_channel_layout(film->audio_channels())))
 {
-
+       _filters.push_back (new Filter ("ebur128", "ebur128", "audio", "ebur128=peak=true"));
+       _ebur128->setup (_filters);
 }
 
 AnalyseAudioJob::~AnalyseAudioJob ()
 {
+       BOOST_FOREACH (Filter const * i, _filters) {
+               delete const_cast<Filter*> (i);
+       }
        delete[] _current;
 }
 
@@ -97,12 +117,23 @@ AnalyseAudioJob::run ()
                _done = 0;
                DCPTime const block = DCPTime::from_seconds (1.0 / 8);
                for (DCPTime t = start; t < length; t += block) {
-                       analyse (player->get_audio (t, block, false));
+                       shared_ptr<const AudioBuffers> audio = player->get_audio (t, block, false);
+                       _ebur128->process (audio);
+                       analyse (audio);
                        set_progress ((t.seconds() - start.seconds()) / (length.seconds() - start.seconds()));
                }
        }
 
-       _analysis->set_peak (_overall_peak, DCPTime::from_frames (_overall_peak_frame, _film->audio_frame_rate ()));
+       _analysis->set_sample_peak (_sample_peak, DCPTime::from_frames (_sample_peak_frame, _film->audio_frame_rate ()));
+
+       void* eb = _ebur128->get("Parsed_ebur128_0")->priv;
+       double true_peak = 0;
+       for (int i = 0; i < _film->audio_channels(); ++i) {
+               true_peak = max (true_peak, av_ebur128_get_true_peaks(eb)[i]);
+       }
+       _analysis->set_true_peak (true_peak);
+       _analysis->set_integrated_loudness (av_ebur128_get_integrated_loudness(eb));
+       _analysis->set_loudness_range (av_ebur128_get_loudness_range(eb));
 
        if (_playlist->content().size() == 1) {
                /* If there was only one piece of content in this analysis we may later need to know what its
@@ -138,9 +169,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 > _overall_peak) {
-                               _overall_peak = as;
-                               _overall_peak_frame = _done + i;
+                       if (as > _sample_peak) {
+                               _sample_peak = as;
+                               _sample_peak_frame = _done + i;
                        }
 
                        if (((_done + i) % _samples_per_point) == 0) {
index d9b90ec..65282b1 100644 (file)
@@ -29,6 +29,8 @@ class AudioBuffers;
 class AudioAnalysis;
 class Playlist;
 class AudioPoint;
+class AudioFilterGraph;
+class Filter;
 
 /** @class AnalyseAudioJob
  *  @brief A job to analyse the audio of a film and make a note of its
@@ -60,10 +62,13 @@ private:
        int64_t _samples_per_point;
        AudioPoint* _current;
 
-       float _overall_peak;
-       Frame _overall_peak_frame;
+       float _sample_peak;
+       Frame _sample_peak_frame;
 
        boost::shared_ptr<AudioAnalysis> _analysis;
 
+       boost::shared_ptr<AudioFilterGraph> _ebur128;
+       std::vector<Filter const *> _filters;
+
        static const int _num_points;
 };
index 10e0223..03f35d8 100644 (file)
@@ -62,8 +62,22 @@ AudioAnalysis::AudioAnalysis (boost::filesystem::path filename)
                _data.push_back (channel);
        }
 
-       _peak = f.number_child<float> ("Peak");
-       _peak_time = DCPTime (f.number_child<DCPTime::Type> ("PeakTime"));
+       _sample_peak = f.optional_number_child<float> ("Peak");
+       if (!_sample_peak) {
+               /* New key */
+               _sample_peak = f.optional_number_child<float> ("SamplePeak");
+       }
+
+       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"));
+       }
+
+       _true_peak = f.optional_number_child<float> ("TruePeak");
+       _integrated_loudness = f.optional_number_child<float> ("IntegratedLoudness");
+       _loudness_range = f.optional_number_child<float> ("LoudnessRange");
+
        _analysis_gain = f.optional_number_child<double> ("AnalysisGain");
 }
 
@@ -107,9 +121,21 @@ AudioAnalysis::write (boost::filesystem::path filename)
                }
        }
 
-       if (_peak) {
-               root->add_child("Peak")->add_child_text (raw_convert<string> (_peak.get ()));
-               root->add_child("PeakTime")->add_child_text (raw_convert<string> (_peak_time.get().get ()));
+       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 ()));
+       }
+
+       if (_true_peak) {
+               root->add_child("TruePeak")->add_child_text (raw_convert<string> (_true_peak.get ()));
+       }
+
+       if (_integrated_loudness) {
+               root->add_child("IntegratedLoudness")->add_child_text (raw_convert<string> (_integrated_loudness.get ()));
+       }
+
+       if (_loudness_range) {
+               root->add_child("LoudnessRange")->add_child_text (raw_convert<string> (_loudness_range.get ()));
        }
 
        if (_analysis_gain) {
index 0d06e59..9594534 100644 (file)
@@ -40,21 +40,46 @@ public:
        AudioAnalysis (boost::filesystem::path);
 
        void add_point (int c, AudioPoint const & p);
-       void set_peak (float peak, DCPTime time) {
-               _peak = peak;
-               _peak_time = time;
+
+       void set_sample_peak (float peak, DCPTime time) {
+               _sample_peak = peak;
+               _sample_peak_time = time;
+       }
+
+       void set_true_peak (float peak) {
+               _true_peak = peak;
+       }
+
+       void set_integrated_loudness (float l) {
+               _integrated_loudness = l;
+       }
+
+       void set_loudness_range (float r) {
+               _loudness_range = r;
        }
 
        AudioPoint get_point (int c, int p) const;
        int points (int c) const;
        int channels () const;
 
-       boost::optional<float> peak () const {
-               return _peak;
+       boost::optional<float> sample_peak () const {
+               return _sample_peak;
+       }
+
+       boost::optional<DCPTime> sample_peak_time () const {
+               return _sample_peak_time;
+       }
+
+       boost::optional<float> true_peak () const {
+               return _true_peak;
+       }
+
+       boost::optional<float> integrated_loudness () const {
+               return _integrated_loudness;
        }
 
-       boost::optional<DCPTime> peak_time () const {
-               return _peak_time;
+       boost::optional<float> loudness_range () const {
+               return _loudness_range;
        }
 
        boost::optional<double> analysis_gain () const {
@@ -71,8 +96,11 @@ public:
 
 private:
        std::vector<std::vector<AudioPoint> > _data;
-       boost::optional<float> _peak;
-       boost::optional<DCPTime> _peak_time;
+       boost::optional<float> _sample_peak;
+       boost::optional<DCPTime> _sample_peak_time;
+       boost::optional<float> _true_peak;
+       boost::optional<float> _integrated_loudness;
+       boost::optional<float> _loudness_range;
        /** If this analysis was run on a single piece of
         *  content we store its gain in dB when the analysis
         *  happened.
index 3efb697..249fa79 100644 (file)
@@ -54,7 +54,8 @@ using dcp::Size;
  *  @param p Pixel format of the images to process.
  */
 FilterGraph::FilterGraph ()
-       : _copy (false)
+       : _graph (0)
+       , _copy (false)
        , _buffer_src_context (0)
        , _buffer_sink_context (0)
        , _frame (0)
@@ -73,28 +74,28 @@ FilterGraph::setup (vector<Filter const *> filters)
 
        _frame = av_frame_alloc ();
 
-       AVFilterGraph* graph = avfilter_graph_alloc();
-       if (graph == 0) {
+       _graph = avfilter_graph_alloc();
+       if (!_graph) {
                throw DecodeError (N_("could not create filter graph."));
        }
 
-       AVFilter* buffer_src = avfilter_get_by_name(N_("buffer"));
-       if (buffer_src == 0) {
+       AVFilter* buffer_src = avfilter_get_by_name (src_name().c_str());
+       if (!buffer_src) {
                throw DecodeError (N_("could not find buffer src filter"));
        }
 
-       AVFilter* buffer_sink = avfilter_get_by_name(N_("buffersink"));
-       if (buffer_sink == 0) {
+       AVFilter* buffer_sink = avfilter_get_by_name (sink_name().c_str());
+       if (!buffer_sink) {
                throw DecodeError (N_("Could not create buffer sink filter"));
        }
 
-       if (avfilter_graph_create_filter (&_buffer_src_context, buffer_src, "in", src_parameters().c_str(), 0, graph) < 0) {
+       if (avfilter_graph_create_filter (&_buffer_src_context, buffer_src, N_("in"), src_parameters().c_str(), 0, _graph) < 0) {
                throw DecodeError (N_("could not create buffer source"));
        }
 
-       AVBufferSinkParams* sink_params = sink_parameters ();
+       void* sink_params = sink_parameters ();
 
-       if (avfilter_graph_create_filter (&_buffer_sink_context, buffer_sink, N_("out"), 0, sink_params, graph) < 0) {
+       if (avfilter_graph_create_filter (&_buffer_sink_context, buffer_sink, N_("out"), 0, sink_params, _graph) < 0) {
                throw DecodeError (N_("could not create buffer sink."));
        }
 
@@ -112,11 +113,11 @@ FilterGraph::setup (vector<Filter const *> filters)
        inputs->pad_idx = 0;
        inputs->next = 0;
 
-       if (avfilter_graph_parse (graph, filters_string.c_str(), inputs, outputs, 0) < 0) {
+       if (avfilter_graph_parse (_graph, filters_string.c_str(), inputs, outputs, 0) < 0) {
                throw DecodeError (N_("could not set up filter graph."));
        }
 
-       if (avfilter_graph_config (graph, 0) < 0) {
+       if (avfilter_graph_config (_graph, 0) < 0) {
                throw DecodeError (N_("could not configure filter graph."));
        }
 }
@@ -126,4 +127,14 @@ FilterGraph::~FilterGraph ()
        if (_frame) {
                av_frame_free (&_frame);
        }
+
+       if (_graph) {
+               avfilter_graph_free (&_graph);
+       }
+}
+
+AVFilterContext *
+FilterGraph::get (string name)
+{
+       return avfilter_graph_get_filter (_graph, name.c_str ());
 }
index 0733406..00b89d4 100644 (file)
@@ -44,11 +44,15 @@ public:
        virtual ~FilterGraph ();
 
        void setup (std::vector<Filter const *>);
+       AVFilterContext* get (std::string name);
 
 protected:
        virtual std::string src_parameters () const = 0;
-       virtual AVBufferSinkParams* sink_parameters () const = 0;
+       virtual std::string src_name () const = 0;
+       virtual void* sink_parameters () const = 0;
+       virtual std::string sink_name () const = 0;
 
+       AVFilterGraph* _graph;
        /** true if this graph has no filters in, so it just copies stuff straight through */
        bool _copy;
        AVFilterContext* _buffer_src_context;
index 306a17a..8aef4fb 100644 (file)
@@ -93,13 +93,27 @@ VideoFilterGraph::src_parameters () const
        return a.str ();
 }
 
-AVBufferSinkParams *
+void *
 VideoFilterGraph::sink_parameters () const
 {
        AVBufferSinkParams* sink_params = av_buffersink_params_alloc ();
        AVPixelFormat* pixel_fmts = new AVPixelFormat[2];
+       pixel_fmts = new AVPixelFormat[2];
        pixel_fmts[0] = _pixel_format;
        pixel_fmts[1] = AV_PIX_FMT_NONE;
        sink_params->pixel_fmts = pixel_fmts;
        return sink_params;
 }
+
+string
+VideoFilterGraph::src_name () const
+{
+       return "buffer";
+}
+
+string
+VideoFilterGraph::sink_name () const
+{
+       return "buffersink";
+}
+
index 08de557..8607e02 100644 (file)
@@ -29,7 +29,9 @@ public:
 
 protected:
        std::string src_parameters () const;
-       AVBufferSinkParams* sink_parameters () const;
+       std::string src_name () const;
+       void* sink_parameters () const;
+       std::string sink_name () const;
 
 private:
        dcp::Size _size; ///< size of the images that this chain can process
index 3151903..a76ca9b 100644 (file)
@@ -28,6 +28,7 @@ sources = """
           audio_decoder_stream.cc
           audio_delay.cc
           audio_filter.cc
+          audio_filter_graph.cc
           audio_mapping.cc
           audio_point.cc
           audio_processor.cc
index 8e67215..e9dee3d 100644 (file)
@@ -52,9 +52,15 @@ AudioDialog::AudioDialog (wxWindow* parent, shared_ptr<Film> film, shared_ptr<Au
        wxBoxSizer* left = new wxBoxSizer (wxVERTICAL);
 
        _plot = new AudioPlot (this);
-       left->Add (_plot, 1, wxALL | wxEXPAND, 12);
-       _peak_time = new wxStaticText (this, wxID_ANY, wxT (""));
-       left->Add (_peak_time, 0, wxALL, 12);
+       left->Add (_plot, 1, wxTOP | wxEXPAND, 12);
+       _sample_peak = new wxStaticText (this, wxID_ANY, wxT (""));
+       left->Add (_sample_peak, 0, wxTOP, DCPOMATIC_SIZER_Y_GAP);
+       _true_peak = new wxStaticText (this, wxID_ANY, wxT (""));
+       left->Add (_true_peak, 0, wxTOP, DCPOMATIC_SIZER_Y_GAP);
+       _integrated_loudness = new wxStaticText (this, wxID_ANY, wxT (""));
+       left->Add (_integrated_loudness, 0, wxTOP, DCPOMATIC_SIZER_Y_GAP);
+       _loudness_range = new wxStaticText (this, wxID_ANY, wxT (""));
+       left->Add (_loudness_range, 0, wxTOP, DCPOMATIC_SIZER_Y_GAP);
 
        lr_sizer->Add (left, 1, wxALL, 12);
 
@@ -153,7 +159,7 @@ AudioDialog::try_to_load_analysis ()
 
        _plot->set_analysis (_analysis);
        _plot->set_gain_correction (_analysis->gain_correction (_playlist));
-       setup_peak_time ();
+       setup_statistics ();
 
        /* Set up some defaults if no check boxes are checked */
 
@@ -223,7 +229,7 @@ AudioDialog::content_changed (int p)
                           change, rather than recalculating everything.
                        */
                        _plot->set_gain_correction (_analysis->gain_correction (_playlist));
-                       setup_peak_time ();
+                       setup_statistics ();
                } else {
                        try_to_load_analysis ();
                }
@@ -250,9 +256,9 @@ AudioDialog::smoothing_changed ()
 }
 
 void
-AudioDialog::setup_peak_time ()
+AudioDialog::setup_statistics ()
 {
-       if (!_analysis || !_analysis->peak ()) {
+       if (!_analysis) {
                return;
        }
 
@@ -261,20 +267,43 @@ AudioDialog::setup_peak_time ()
                return;
        }
 
-       float const peak_dB = 20 * log10 (_analysis->peak().get()) + _analysis->gain_correction (_playlist);
+       if (static_cast<bool>(_analysis->sample_peak ())) {
 
-       _peak_time->SetLabel (
-               wxString::Format (
-                       _("Peak is %.2fdB at %s"),
-                       peak_dB,
-                       time_to_timecode (_analysis->peak_time().get(), film->video_frame_rate ()).data ()
-                       )
-               );
+               float const peak_dB = 20 * log10 (_analysis->sample_peak().get()) + _analysis->gain_correction (_playlist);
 
-       if (peak_dB > -3) {
-               _peak_time->SetForegroundColour (wxColour (255, 0, 0));
-       } else {
-               _peak_time->SetForegroundColour (wxColour (0, 0, 0));
+               _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));
+               }
+       }
+
+       if (static_cast<bool>(_analysis->true_peak ())) {
+               float const peak_dB = 20 * log10 (_analysis->true_peak().get()) + _analysis->gain_correction (_playlist);
+
+               _true_peak->SetLabel (wxString::Format (_("True peak is %.2fdB"), peak_dB));
+
+               if (peak_dB > -3) {
+                       _true_peak->SetForegroundColour (wxColour (255, 0, 0));
+               } else {
+                       _true_peak->SetForegroundColour (wxColour (0, 0, 0));
+               }
+       }
+
+       if (static_cast<bool>(_analysis->integrated_loudness ())) {
+               _integrated_loudness->SetLabel (wxString::Format (_("Integrated loudness %.2f LUFS"), _analysis->integrated_loudness().get()));
+       }
+
+       if (static_cast<bool>(_analysis->loudness_range ())) {
+               _loudness_range->SetLabel (wxString::Format (_("Loudness range %.2f LRA"), _analysis->loudness_range().get()));
        }
 }
 
index 588f711..0f791dc 100644 (file)
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2013 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2013-2015 Carl Hetherington <cth@carlh.net>
 
     This program is free software; you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published by
@@ -41,14 +41,17 @@ private:
        void smoothing_changed ();
        void try_to_load_analysis ();
        void analysis_finished ();
-       void setup_peak_time ();
+       void setup_statistics ();
 
        boost::shared_ptr<AudioAnalysis> _analysis;
        boost::weak_ptr<Film> _film;
        int _channels;
        boost::shared_ptr<const Playlist> _playlist;
        AudioPlot* _plot;
-       wxStaticText* _peak_time;
+       wxStaticText* _sample_peak;
+       wxStaticText* _true_peak;
+       wxStaticText* _integrated_loudness;
+       wxStaticText* _loudness_range;
        wxCheckBox* _channel_checkbox[MAX_DCP_AUDIO_CHANNELS];
        wxCheckBox* _type_checkbox[AudioPoint::COUNT];
        wxSlider* _smoothing;
index b190410..ff62acc 100644 (file)
@@ -303,8 +303,8 @@ AudioPanel::setup_peak ()
                playlist->add (sel.front ());
                try {
                        shared_ptr<AudioAnalysis> analysis (new AudioAnalysis (_parent->film()->audio_analysis_path (playlist)));
-                       if (analysis->peak ()) {
-                               float const peak_dB = 20 * log10 (analysis->peak().get()) + analysis->gain_correction (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;
                                }
index 7cbe283..c2c0673 100644 (file)
@@ -59,7 +59,7 @@ BOOST_AUTO_TEST_CASE (audio_analysis_serialisation_test)
 
        float const peak = random_float ();
        DCPTime const peak_time = DCPTime (rand ());
-       a.set_peak (peak, peak_time);
+       a.set_sample_peak (peak, peak_time);
 
        a.write ("build/test/audio_analysis_serialisation_test");
 
@@ -75,10 +75,10 @@ BOOST_AUTO_TEST_CASE (audio_analysis_serialisation_test)
                }
        }
 
-       BOOST_CHECK (b.peak ());
-       BOOST_CHECK_CLOSE (b.peak().get(), peak, 1);
-       BOOST_CHECK (b.peak_time ());
-       BOOST_CHECK_EQUAL (b.peak_time().get(), peak_time);
+       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(), peak_time);
 }
 
 static void