From 419afb743ad2aaf1ea301356ac09f9a26ee15567 Mon Sep 17 00:00:00 2001 From: Carl Hetherington Date: Wed, 18 Nov 2015 17:07:07 +0000 Subject: [PATCH] Basic (untested) ebur128 (#368). --- ChangeLog | 4 +++ cscript | 2 +- src/lib/analyse_audio_job.cc | 47 +++++++++++++++++++----- src/lib/analyse_audio_job.h | 9 +++-- src/lib/audio_analysis.cc | 36 ++++++++++++++++--- src/lib/audio_analysis.h | 46 +++++++++++++++++++----- src/lib/filter_graph.cc | 35 +++++++++++------- src/lib/filter_graph.h | 6 +++- src/lib/video_filter_graph.cc | 16 ++++++++- src/lib/video_filter_graph.h | 4 ++- src/lib/wscript | 1 + src/wx/audio_dialog.cc | 67 +++++++++++++++++++++++++---------- src/wx/audio_dialog.h | 9 +++-- src/wx/audio_panel.cc | 4 +-- test/audio_analysis_test.cc | 10 +++--- 15 files changed, 227 insertions(+), 69 deletions(-) diff --git a/ChangeLog b/ChangeLog index 47fc59b4f..086908002 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ +2015-11-18 c.hetherington + + * Add LUFS / LRA analysis using FFmpeg's ebur128 filter (#368). + 2015-11-17 Carl Hetherington * Bump libdcp for fix to encoded date header in MXF files when diff --git a/cscript b/cscript index b8f5f8813..d8736cb5e 100644 --- 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')) diff --git a/src/lib/analyse_audio_job.cc b/src/lib/analyse_audio_job.cc index 525ac6c91..5dba56598 100644 --- a/src/lib/analyse_audio_job.cc +++ b/src/lib/analyse_audio_job.cc @@ -25,6 +25,11 @@ #include "film.h" #include "player.h" #include "playlist.h" +#include "filter.h" +#include "audio_filter_graph.h" +extern "C" { +#include +} #include #include @@ -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 film, shared_ptr 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 (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 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 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) { diff --git a/src/lib/analyse_audio_job.h b/src/lib/analyse_audio_job.h index d9b90ec66..65282b192 100644 --- a/src/lib/analyse_audio_job.h +++ b/src/lib/analyse_audio_job.h @@ -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 _analysis; + boost::shared_ptr _ebur128; + std::vector _filters; + static const int _num_points; }; diff --git a/src/lib/audio_analysis.cc b/src/lib/audio_analysis.cc index 10e022322..03f35d84e 100644 --- a/src/lib/audio_analysis.cc +++ b/src/lib/audio_analysis.cc @@ -62,8 +62,22 @@ AudioAnalysis::AudioAnalysis (boost::filesystem::path filename) _data.push_back (channel); } - _peak = f.number_child ("Peak"); - _peak_time = DCPTime (f.number_child ("PeakTime")); + _sample_peak = f.optional_number_child ("Peak"); + if (!_sample_peak) { + /* New key */ + _sample_peak = f.optional_number_child ("SamplePeak"); + } + + 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")); + } + + _true_peak = f.optional_number_child ("TruePeak"); + _integrated_loudness = f.optional_number_child ("IntegratedLoudness"); + _loudness_range = f.optional_number_child ("LoudnessRange"); + _analysis_gain = f.optional_number_child ("AnalysisGain"); } @@ -107,9 +121,21 @@ AudioAnalysis::write (boost::filesystem::path filename) } } - if (_peak) { - root->add_child("Peak")->add_child_text (raw_convert (_peak.get ())); - root->add_child("PeakTime")->add_child_text (raw_convert (_peak_time.get().get ())); + 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 ())); + } + + if (_true_peak) { + root->add_child("TruePeak")->add_child_text (raw_convert (_true_peak.get ())); + } + + if (_integrated_loudness) { + root->add_child("IntegratedLoudness")->add_child_text (raw_convert (_integrated_loudness.get ())); + } + + if (_loudness_range) { + root->add_child("LoudnessRange")->add_child_text (raw_convert (_loudness_range.get ())); } if (_analysis_gain) { diff --git a/src/lib/audio_analysis.h b/src/lib/audio_analysis.h index 0d06e5973..959453496 100644 --- a/src/lib/audio_analysis.h +++ b/src/lib/audio_analysis.h @@ -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 peak () const { - return _peak; + boost::optional sample_peak () const { + return _sample_peak; + } + + boost::optional sample_peak_time () const { + return _sample_peak_time; + } + + boost::optional true_peak () const { + return _true_peak; + } + + boost::optional integrated_loudness () const { + return _integrated_loudness; } - boost::optional peak_time () const { - return _peak_time; + boost::optional loudness_range () const { + return _loudness_range; } boost::optional analysis_gain () const { @@ -71,8 +96,11 @@ public: private: std::vector > _data; - boost::optional _peak; - boost::optional _peak_time; + boost::optional _sample_peak; + boost::optional _sample_peak_time; + boost::optional _true_peak; + boost::optional _integrated_loudness; + boost::optional _loudness_range; /** If this analysis was run on a single piece of * content we store its gain in dB when the analysis * happened. diff --git a/src/lib/filter_graph.cc b/src/lib/filter_graph.cc index 3efb6970c..249fa7966 100644 --- a/src/lib/filter_graph.cc +++ b/src/lib/filter_graph.cc @@ -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 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 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 ()); } diff --git a/src/lib/filter_graph.h b/src/lib/filter_graph.h index 073340637..00b89d494 100644 --- a/src/lib/filter_graph.h +++ b/src/lib/filter_graph.h @@ -44,11 +44,15 @@ public: virtual ~FilterGraph (); void setup (std::vector); + 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; diff --git a/src/lib/video_filter_graph.cc b/src/lib/video_filter_graph.cc index 306a17a5d..8aef4fb00 100644 --- a/src/lib/video_filter_graph.cc +++ b/src/lib/video_filter_graph.cc @@ -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"; +} + diff --git a/src/lib/video_filter_graph.h b/src/lib/video_filter_graph.h index 08de557ca..8607e02fc 100644 --- a/src/lib/video_filter_graph.h +++ b/src/lib/video_filter_graph.h @@ -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 diff --git a/src/lib/wscript b/src/lib/wscript index 315190307..a76ca9b74 100644 --- a/src/lib/wscript +++ b/src/lib/wscript @@ -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 diff --git a/src/wx/audio_dialog.cc b/src/wx/audio_dialog.cc index 8e672150e..e9dee3dac 100644 --- a/src/wx/audio_dialog.cc +++ b/src/wx/audio_dialog.cc @@ -52,9 +52,15 @@ AudioDialog::AudioDialog (wxWindow* parent, shared_ptr film, shared_ptrAdd (_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(_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(_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(_analysis->integrated_loudness ())) { + _integrated_loudness->SetLabel (wxString::Format (_("Integrated loudness %.2f LUFS"), _analysis->integrated_loudness().get())); + } + + if (static_cast(_analysis->loudness_range ())) { + _loudness_range->SetLabel (wxString::Format (_("Loudness range %.2f LRA"), _analysis->loudness_range().get())); } } diff --git a/src/wx/audio_dialog.h b/src/wx/audio_dialog.h index 588f71159..0f791dc16 100644 --- a/src/wx/audio_dialog.h +++ b/src/wx/audio_dialog.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2013 Carl Hetherington + Copyright (C) 2013-2015 Carl Hetherington 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 _analysis; boost::weak_ptr _film; int _channels; boost::shared_ptr _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; diff --git a/src/wx/audio_panel.cc b/src/wx/audio_panel.cc index b1904107b..ff62acc67 100644 --- a/src/wx/audio_panel.cc +++ b/src/wx/audio_panel.cc @@ -303,8 +303,8 @@ AudioPanel::setup_peak () playlist->add (sel.front ()); try { shared_ptr 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; } diff --git a/test/audio_analysis_test.cc b/test/audio_analysis_test.cc index 7cbe283fd..c2c06734a 100644 --- a/test/audio_analysis_test.cc +++ b/test/audio_analysis_test.cc @@ -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 -- 2.30.2