+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
else:
ffmpeg_options = {}
- return (('ffmpeg-cdist', 'd0986a9', ffmpeg_options),
+ return (('ffmpeg-cdist', 'b9d9b12', ffmpeg_options),
('libdcp', '960fa45'),
('libsub', '8b82c40'))
#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>
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;
}
_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
_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) {
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
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;
};
_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");
}
}
}
- 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) {
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 {
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.
* @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)
_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."));
}
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."));
}
}
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 ());
}
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;
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";
+}
+
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
audio_decoder_stream.cc
audio_delay.cc
audio_filter.cc
+ audio_filter_graph.cc
audio_mapping.cc
audio_point.cc
audio_processor.cc
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);
_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 */
change, rather than recalculating everything.
*/
_plot->set_gain_correction (_analysis->gain_correction (_playlist));
- setup_peak_time ();
+ setup_statistics ();
} else {
try_to_load_analysis ();
}
}
void
-AudioDialog::setup_peak_time ()
+AudioDialog::setup_statistics ()
{
- if (!_analysis || !_analysis->peak ()) {
+ if (!_analysis) {
return;
}
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()));
}
}
/*
- 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
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;
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;
}
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");
}
}
- 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