Add LEQ(m) when analysing audio (#1382). v2.15.52
authorCarl Hetherington <cth@carlh.net>
Sun, 19 Apr 2020 22:10:31 +0000 (00:10 +0200)
committerCarl Hetherington <cth@carlh.net>
Sun, 19 Apr 2020 22:43:03 +0000 (00:43 +0200)
12 files changed:
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/wscript
src/tools/wscript
src/wx/audio_dialog.cc
src/wx/audio_dialog.h
test/audio_analysis_test.cc
test/wscript
wscript

diff --git a/cscript b/cscript
index aeddb1cdd9d71ff49c339a12caf26b57b2ea1991..b10a7fc829e120c190b981dfb088cea156730213 100644 (file)
--- a/cscript
+++ b/cscript
@@ -368,6 +368,7 @@ def dependencies(target, options):
 
     deps.append(('libdcp', None, cpp_lib_options))
     deps.append(('libsub', None, cpp_lib_options))
+    deps.append(('leqm-nrt', 'carl'))
     deps.append(('rtaudio', 'carl'))
     # We get our OpenSSL libraries from the environment, but we
     # also need a patched openssl binary to make certificates.
index 1fc09b9055d0a9155960fc5d8470657a076e5428..ead36bca1e031ccc1c1eb3e25db8b826d40d270f 100644 (file)
@@ -30,6 +30,7 @@
 #include "audio_filter_graph.h"
 #include "config.h"
 extern "C" {
+#include <leqm_nrt.h>
 #include <libavutil/channel_layout.h>
 #ifdef DCPOMATIC_HAVE_EBUR128_PATCHED_FFMPEG
 #include <libavfilter/f_ebur128.h>
@@ -51,6 +52,13 @@ using namespace dcpomatic;
 
 int const AnalyseAudioJob::_num_points = 1024;
 
+static void add_if_required(vector<double>& v, size_t i, double db)
+{
+       if (v.size() > i) {
+               v[i] = pow(10, db / 20);
+       }
+}
+
 /** @param from_zero true to analyse audio from time 0 in the playlist, otherwise begin at Playlist::start */
 AnalyseAudioJob::AnalyseAudioJob (shared_ptr<const Film> film, shared_ptr<const Playlist> playlist, bool from_zero)
        : Job (film)
@@ -79,6 +87,31 @@ AnalyseAudioJob::AnalyseAudioJob (shared_ptr<const Film> film, shared_ptr<const
        if (!_from_zero) {
                _start = _playlist->start().get_value_or(DCPTime());
        }
+
+       /* XXX: is this right?  Especially for more than 5.1? */
+       vector<double> channel_corrections(film->audio_channels(), 1);
+       add_if_required (channel_corrections,  4,   -3); // Ls
+       add_if_required (channel_corrections,  5,   -3); // Rs
+       add_if_required (channel_corrections,  6, -144); // HI
+       add_if_required (channel_corrections,  7, -144); // VI
+       add_if_required (channel_corrections,  8,   -3); // Lc
+       add_if_required (channel_corrections,  9,   -3); // Rc
+       add_if_required (channel_corrections, 10,   -3); // Lc
+       add_if_required (channel_corrections, 11,   -3); // Rc
+       add_if_required (channel_corrections, 12, -144); // DBox
+       add_if_required (channel_corrections, 13, -144); // Sync
+       add_if_required (channel_corrections, 14, -144); // Sign Language
+       add_if_required (channel_corrections, 15, -144); // Unused
+
+       _leqm.reset(new leqm_nrt::Calculator(
+               film->audio_channels(),
+               film->audio_frame_rate(),
+               24,
+               channel_corrections,
+               850, // suggested by leqm_nrt CLI source
+               64,  // suggested by leqm_nrt CLI source
+               boost::thread::hardware_concurrency()
+               ));
 }
 
 AnalyseAudioJob::~AnalyseAudioJob ()
@@ -169,6 +202,7 @@ AnalyseAudioJob::run ()
 
        _analysis->set_samples_per_point (_samples_per_point);
        _analysis->set_sample_rate (_film->audio_frame_rate ());
+       _analysis->set_leqm (_leqm->leq_m());
        _analysis->write (_path);
 
        set_progress (1);
@@ -188,11 +222,15 @@ AnalyseAudioJob::analyse (shared_ptr<const AudioBuffers> b, DCPTime time)
 
        int const frames = b->frames ();
        int const channels = b->channels ();
+       vector<double> interleaved(frames * channels);
 
        for (int j = 0; j < channels; ++j) {
                float* data = b->data(j);
                for (int i = 0; i < frames; ++i) {
                        float s = data[i];
+
+                       interleaved[i * channels + j] = s;
+
                        float as = fabsf (s);
                        if (as < 10e-7) {
                                /* We may struggle to serialise and recover inf or -inf, so prevent such
@@ -215,6 +253,8 @@ AnalyseAudioJob::analyse (shared_ptr<const AudioBuffers> b, DCPTime time)
                }
        }
 
+       _leqm->add(interleaved);
+
        _done += frames;
 
        DCPTime const length = _playlist->length (_film);
index 5d6c091bcfbf3b835c68bdd771f3247c2bca6140..f7cc3e2567ffa39dd82205f3a48d664c119efd24 100644 (file)
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2012-2018 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2012-2020 Carl Hetherington <cth@carlh.net>
 
     This file is part of DCP-o-matic.
 
@@ -26,6 +26,8 @@
 #include "audio_point.h"
 #include "types.h"
 #include "dcpomatic_time.h"
+#include <leqm_nrt.h>
+#include <boost/scoped_ptr.hpp>
 
 class AudioBuffers;
 class AudioAnalysis;
@@ -76,5 +78,7 @@ private:
        boost::shared_ptr<AudioFilterGraph> _ebur128;
        std::vector<Filter const *> _filters;
 
+       boost::scoped_ptr<leqm_nrt::Calculator> _leqm;
+
        static const int _num_points;
 };
index 13917cb5f6d693f3987b0c8adb85526a6426747c..446fcccef1607323d7851dbffe98114cda5fb841 100644 (file)
@@ -93,6 +93,8 @@ AudioAnalysis::AudioAnalysis (boost::filesystem::path filename)
        _analysis_gain = f.optional_number_child<double> ("AnalysisGain");
        _samples_per_point = f.number_child<int64_t> ("SamplesPerPoint");
        _sample_rate = f.number_child<int64_t> ("SampleRate");
+
+       _leqm = f.optional_number_child<double>("Leqm");
 }
 
 void
@@ -162,6 +164,10 @@ AudioAnalysis::write (boost::filesystem::path filename)
        root->add_child("SamplesPerPoint")->add_child_text (raw_convert<string> (_samples_per_point));
        root->add_child("SampleRate")->add_child_text (raw_convert<string> (_sample_rate));
 
+       if (_leqm) {
+               root->add_child("Leqm")->add_child_text(raw_convert<string>(*_leqm));
+       }
+
        doc->write_to_file_formatted (filename.string ());
 }
 
@@ -212,3 +218,4 @@ AudioAnalysis::overall_true_peak () const
 
        return p;
 }
+
index 3684db96abdd97bfd48fa3693c3e2b46abcc5ecf..99a69edb4c26dfccccb77cd81c48e069301a436f 100644 (file)
@@ -116,6 +116,14 @@ public:
                _sample_rate = sr;
        }
 
+       void set_leqm (double leqm) {
+               _leqm = leqm;
+       }
+
+       boost::optional<double> leqm () const {
+               return _leqm;
+       }
+
        void write (boost::filesystem::path);
 
        float gain_correction (boost::shared_ptr<const Playlist> playlist);
@@ -126,6 +134,7 @@ private:
        std::vector<float> _true_peak;
        boost::optional<float> _integrated_loudness;
        boost::optional<float> _loudness_range;
+       boost::optional<double> _leqm;
        /** If this analysis was run on a single piece of
         *  content we store its gain in dB when the analysis
         *  happened.
index ca6786ef23440b617a8fd833bcb3030f40fa8f82..67bcf8d8b37a40cc59a6f74d3df65c468272cc81 100644 (file)
@@ -193,7 +193,7 @@ def build(bld):
                  AVCODEC AVUTIL AVFORMAT AVFILTER SWSCALE
                  BOOST_FILESYSTEM BOOST_THREAD BOOST_DATETIME BOOST_SIGNALS2 BOOST_REGEX
                  SAMPLERATE POSTPROC TIFF SSH DCP CXML GLIB LZMA XML++
-                 CURL ZIP FONTCONFIG PANGOMM CAIROMM XMLSEC SUB ICU NETTLE PNG
+                 CURL ZIP FONTCONFIG PANGOMM CAIROMM XMLSEC SUB ICU NETTLE PNG LEQM_NRT
                  """
 
     if bld.env.TARGET_OSX:
index c7c953a318b04eb3534679f8bbfef97e6ea871a7..7eeeecddfb80d48c909f290c7c14831cd79261e7 100644 (file)
@@ -30,7 +30,7 @@ def configure(conf):
 def build(bld):
     uselib =  'BOOST_THREAD BOOST_DATETIME DCP XMLSEC CXML XMLPP AVFORMAT AVFILTER AVCODEC '
     uselib += 'AVUTIL SWSCALE SWRESAMPLE POSTPROC CURL BOOST_FILESYSTEM SSH ZIP CAIROMM FONTCONFIG PANGOMM SUB '
-    uselib += 'SNDFILE SAMPLERATE BOOST_REGEX ICU NETTLE RTAUDIO PNG '
+    uselib += 'SNDFILE SAMPLERATE BOOST_REGEX ICU NETTLE RTAUDIO PNG LEQM_NRT '
 
     if bld.env.ENABLE_DISK:
         if bld.env.TARGET_LINUX:
index 2f1f1c8265f324f41cb1c2733dd0f02ad44267bc..efc506aff9a62bdec61946882308950c84fb3259 100644 (file)
@@ -89,6 +89,8 @@ AudioDialog::AudioDialog (wxWindow* parent, shared_ptr<Film> film, shared_ptr<Co
        left->Add (_integrated_loudness, 0, wxTOP, DCPOMATIC_SIZER_Y_GAP);
        _loudness_range = new StaticText (this, wxT (""));
        left->Add (_loudness_range, 0, wxTOP, DCPOMATIC_SIZER_Y_GAP);
+       _leqm = new StaticText (this, wxT(""));
+       left->Add (_leqm, 0, wxTOP, DCPOMATIC_SIZER_Y_GAP);
 
        lr_sizer->Add (left, 1, wxALL | wxEXPAND, 12);
 
@@ -414,6 +416,14 @@ AudioDialog::setup_statistics ()
                                )
                        );
        }
+
+       if (static_cast<bool>(_analysis->leqm())) {
+               _leqm->SetLabel(
+                       wxString::Format(
+                               _("LEQ(m) %.2fdB"), _analysis->leqm().get() + _analysis->gain_correction(_playlist)
+                               )
+                       );
+       }
 }
 
 bool
index 3a02fd87fdb2c0a8da0fa1d596bc6140c9475cf6..34c174cf46cdf711f5469a9889377dcad47c3986 100644 (file)
@@ -59,6 +59,7 @@ private:
        wxStaticText* _true_peak;
        wxStaticText* _integrated_loudness;
        wxStaticText* _loudness_range;
+       wxStaticText* _leqm;
        wxCheckBox* _channel_checkbox[MAX_DCP_AUDIO_CHANNELS];
        wxCheckBox* _type_checkbox[AudioPoint::COUNT];
        wxSlider* _smoothing;
index 562039d930c24f3ac88b5beea00922b7574ea751..2f51f7c4fd42f797db7847b6a0778279e47e1588 100644 (file)
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2012-2016 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2012-2020 Carl Hetherington <cth@carlh.net>
 
     This file is part of DCP-o-matic.
 
@@ -202,3 +202,23 @@ BOOST_AUTO_TEST_CASE (analyse_audio_test4)
        JobManager::instance()->analyse_audio (film, playlist, false, c, boost::bind (&finished));
        BOOST_CHECK (!wait_for_jobs ());
 }
+
+BOOST_AUTO_TEST_CASE (analyse_audio_leqm_test)
+{
+       shared_ptr<Film> film = new_test_film2 ("analyse_audio_leqm_test");
+       film->set_audio_channels (2);
+       shared_ptr<Content> content = content_factory(TestPaths::private_data / "betty_stereo_48k.wav").front();
+       film->examine_and_add_content (content);
+       BOOST_REQUIRE (!wait_for_jobs());
+
+       shared_ptr<Playlist> playlist (new Playlist);
+       playlist->add (film, content);
+       boost::signals2::connection c;
+       JobManager::instance()->analyse_audio (film, playlist, false, c, boost::bind (&finished));
+       BOOST_CHECK (!wait_for_jobs());
+
+       AudioAnalysis analysis(film->audio_analysis_path(playlist));
+
+       /* The CLI tool of leqm_nrt gives this value for betty_stereo_48k.wav */
+       BOOST_CHECK_CLOSE (analysis.leqm().get_value_or(0), 88.276, 0.001);
+}
index 4e5c57e0df366b5b0e9e29162da0fa57319ce5d2..d7441316ef88c6f1c78d7d9285f72b1d51375d6f 100644 (file)
@@ -37,6 +37,7 @@ def build(bld):
     obj.name   = 'unit-tests'
     obj.uselib =  'BOOST_TEST BOOST_THREAD BOOST_FILESYSTEM BOOST_DATETIME SNDFILE SAMPLERATE DCP FONTCONFIG CAIROMM PANGOMM XMLPP '
     obj.uselib += 'AVFORMAT AVFILTER AVCODEC AVUTIL SWSCALE SWRESAMPLE POSTPROC CXML SUB GLIB CURL SSH XMLSEC BOOST_REGEX ICU NETTLE MAGICK PNG '
+    obj.uselib += 'LEQM_NRT '
     if bld.env.TARGET_WINDOWS:
         obj.uselib += 'WINSOCK2 DBGHELP SHLWAPI MSWSOCK BOOST_LOCALE '
     if bld.env.TARGET_LINUX:
diff --git a/wscript b/wscript
index c386ed21d5469811f209d14e735ede61bb59cbbc..6451f6b2aca408fea69483649c8dc4409f9e249f 100644 (file)
--- a/wscript
+++ b/wscript
@@ -270,6 +270,9 @@ def configure(conf):
     # cairomm
     conf.check_cfg(package='cairomm-1.0', args='--cflags --libs', uselib_store='CAIROMM', mandatory=True)
 
+    # leqm_nrt
+    conf.check_cfg(package='leqm_nrt', args='--cflags --libs', uselib_store='LEQM_NRT', mandatory=True)
+
     test_cxxflags = ''
     if have_c11:
         test_cxxflags = '-std=c++11'