Post-export Analysis
authorRobin Gareus <robin@gareus.org>
Wed, 10 Feb 2016 02:01:05 +0000 (03:01 +0100)
committerRobin Gareus <robin@gareus.org>
Wed, 10 Feb 2016 02:01:05 +0000 (03:01 +0100)
libs/ardour/ardour/export_analysis.h [new file with mode: 0644]
libs/ardour/ardour/export_graph_builder.h
libs/ardour/ardour/export_status.h
libs/ardour/export_graph_builder.cc
libs/ardour/export_handler.cc
libs/ardour/export_status.cc
libs/audiographer/audiographer/general/analyser.h [new file with mode: 0644]

diff --git a/libs/ardour/ardour/export_analysis.h b/libs/ardour/ardour/export_analysis.h
new file mode 100644 (file)
index 0000000..bab79bb
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2016 Robin Gareus <robin@gareus.org>
+ *
+ * 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 the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+#ifndef __ardour_export_analysis_h__
+#define __ardour_export_analysis_h__
+
+#include <map>
+#include <cstring>
+#include <boost/shared_ptr.hpp>
+
+#include "ardour/types.h"
+
+namespace ARDOUR {
+       struct ExportAnalysis {
+       public:
+               ExportAnalysis ()
+                       : loudness (0)
+                       , loudness_range (0)
+                       , have_loudness (false)
+               {
+                       memset (_peaks, 0, sizeof(_peaks));
+                       memset (_spectrum, 0, sizeof(_spectrum));
+               }
+
+               ExportAnalysis (const ExportAnalysis& other)
+                       : loudness (other.loudness)
+                       , loudness_range (other.loudness_range)
+                       , have_loudness (other.have_loudness)
+               {
+                       memcpy (_peaks, other._peaks, sizeof(_peaks));
+                       memcpy (_spectrum, other._spectrum, sizeof(_spectrum));
+               }
+
+               float loudness;
+               float loudness_range;
+               bool have_loudness;
+               PeakData _peaks[800];
+               float _spectrum[800][256];
+       };
+
+       typedef boost::shared_ptr<ExportAnalysis> ExportAnalysisPtr;
+       typedef std::map<std::string, ExportAnalysisPtr> AnalysisResults;
+
+} // namespace ARDOUR
+#endif
index 3e9fb58a1556e46090065ca3f6f7229a3711473a..d14a00997aae806738436f852393432178ac7e97 100644 (file)
@@ -22,6 +22,7 @@
 #define __ardour_export_graph_builder_h__
 
 #include "ardour/export_handler.h"
+#include "ardour/export_analysis.h"
 
 #include "audiographer/utils/identity_vertex.h"
 
@@ -32,6 +33,7 @@ namespace AudioGrapher {
        class SampleRateConverter;
        class PeakReader;
        class Normalizer;
+       class Analyser;
        template <typename T> class Chunker;
        template <typename T> class SampleFormatConverter;
        template <typename T> class Interleaver;
@@ -55,7 +57,9 @@ class LIBARDOUR_API ExportGraphBuilder
 
        typedef boost::shared_ptr<AudioGrapher::Sink<Sample> > FloatSinkPtr;
        typedef boost::shared_ptr<AudioGrapher::IdentityVertex<Sample> > IdentityVertexPtr;
+       typedef boost::shared_ptr<AudioGrapher::Analyser> AnalysisPtr;
        typedef std::map<ExportChannelPtr,  IdentityVertexPtr> ChannelMap;
+       typedef std::map<std::string, AnalysisPtr> AnalysisMap;
 
   public:
 
@@ -71,9 +75,14 @@ class LIBARDOUR_API ExportGraphBuilder
        void cleanup (bool remove_out_files = false);
        void set_current_timespan (boost::shared_ptr<ExportTimespan> span);
        void add_config (FileSpec const & config);
+       void get_analysis_results (AnalysisResults& results);
 
   private:
 
+       void add_analyser (const std::string& fn, AnalysisPtr ap) {
+               analysis_map.insert (std::make_pair (fn, ap));
+       }
+
        void add_split_config (FileSpec const & config);
 
        class Encoder {
@@ -125,6 +134,7 @@ class LIBARDOUR_API ExportGraphBuilder
                boost::ptr_list<Encoder> children;
                int                data_width;
 
+               AnalysisPtr     analyser;
                // Only one of these should be available at a time
                FloatConverterPtr float_converter;
                IntConverterPtr int_converter;
@@ -245,6 +255,8 @@ class LIBARDOUR_API ExportGraphBuilder
 
        std::list<Normalizer *> normalizers;
 
+       AnalysisMap analysis_map;
+
        Glib::ThreadPool thread_pool;
 };
 
index 95921629c7e327900600666492d9c87faaf9d82e..f250ae0dc6b062a04b69bf05e06ba646ba374acd 100644 (file)
@@ -24,6 +24,7 @@
 #include <stdint.h>
 
 #include "ardour/libardour_visibility.h"
+#include "ardour/export_analysis.h"
 #include "ardour/types.h"
 
 #include "pbd/signals.h"
@@ -79,6 +80,8 @@ class LIBARDOUR_API ExportStatus {
        volatile uint32_t       total_normalize_cycles;
        volatile uint32_t       current_normalize_cycle;
 
+       AnalysisResults         result_map;
+
   private:
        volatile bool          _aborted;
        volatile bool          _errors;
index c054d85242d1f619fc46779101fc841ca1223587..685db817d495673c1789aa749cf055bd0927c531 100644 (file)
@@ -28,6 +28,7 @@
 #include "audiographer/general/chunker.h"
 #include "audiographer/general/interleaver.h"
 #include "audiographer/general/normalizer.h"
+#include "audiographer/general/analyser.h"
 #include "audiographer/general/peak_reader.h"
 #include "audiographer/general/sample_format_converter.h"
 #include "audiographer/general/sr_converter.h"
@@ -110,6 +111,7 @@ ExportGraphBuilder::reset ()
        channel_configs.clear ();
        channels.clear ();
        normalizers.clear ();
+       analysis_map.clear();
 }
 
 void
@@ -173,6 +175,16 @@ ExportGraphBuilder::add_config (FileSpec const & config)
        }
 }
 
+void
+ExportGraphBuilder::get_analysis_results (AnalysisResults& results) {
+       for (AnalysisMap::iterator i = analysis_map.begin(); i != analysis_map.end(); ++i) {
+               ExportAnalysisPtr p = i->second->result ();
+               if (p) {
+                       results.insert (std::make_pair (i->first, p));
+               }
+       }
+}
+
 void
 ExportGraphBuilder::add_split_config (FileSpec const & config)
 {
@@ -287,39 +299,39 @@ ExportGraphBuilder::Encoder::copy_files (std::string orig_path)
 
 /* SFC */
 
-ExportGraphBuilder::SFC::SFC (ExportGraphBuilder &, FileSpec const & new_config, framecnt_t max_frames)
+ExportGraphBuilder::SFC::SFC (ExportGraphBuilder &parent, FileSpec const & new_config, framecnt_t max_frames)
        : data_width(0)
 {
        config = new_config;
        data_width = sndfile_data_width (Encoder::get_real_format (config));
        unsigned channels = new_config.channel_config->get_n_chans();
+       analyser.reset (new Analyser (config.format->sample_rate(), channels, max_frames,
+                               (framecnt_t) ceil (parent.timespan->get_length () * config.format->sample_rate () / (double) parent.session.nominal_frame_rate ())));
+       parent.add_analyser (config.filename->get_path (config.format), analyser);
 
        if (data_width == 8 || data_width == 16) {
                short_converter = ShortConverterPtr (new SampleFormatConverter<short> (channels));
                short_converter->init (max_frames, config.format->dither_type(), data_width);
                add_child (config);
+               analyser->add_output (short_converter);
        } else if (data_width == 24 || data_width == 32) {
                int_converter = IntConverterPtr (new SampleFormatConverter<int> (channels));
                int_converter->init (max_frames, config.format->dither_type(), data_width);
                add_child (config);
+               analyser->add_output (int_converter);
        } else {
                int actual_data_width = 8 * sizeof(Sample);
                float_converter = FloatConverterPtr (new SampleFormatConverter<Sample> (channels));
                float_converter->init (max_frames, config.format->dither_type(), actual_data_width);
                add_child (config);
+               analyser->add_output (float_converter);
        }
 }
 
 ExportGraphBuilder::FloatSinkPtr
 ExportGraphBuilder::SFC::sink ()
 {
-       if (data_width == 8 || data_width == 16) {
-               return short_converter;
-       } else if (data_width == 24 || data_width == 32) {
-               return int_converter;
-       } else {
-               return float_converter;
-       }
+       return analyser;
 }
 
 void
index 70c807b7de5d35f43251b80290889b5e88b80be4..6b59afec89c8736285e47eb84b6ca9ea504b671f 100644 (file)
@@ -295,11 +295,12 @@ ExportHandler::command_output(std::string output, size_t size)
 void
 ExportHandler::finish_timespan ()
 {
+       graph_builder->get_analysis_results (export_status->result_map);
+
        while (config_map.begin() != timespan_bounds.second) {
 
                ExportFormatSpecPtr fmt = config_map.begin()->second.format;
                std::string filename = config_map.begin()->second.filename->get_path(fmt);
-
                if (fmt->with_cue()) {
                        export_cd_marker_file (current_timespan, fmt, filename, CDMarkerCUE);
                }
@@ -312,15 +313,17 @@ ExportHandler::finish_timespan ()
                        export_cd_marker_file (current_timespan, fmt, filename, MP4Chaps);
                }
 
+               /* close file first, otherwise TagLib enounters an ERROR_SHARING_VIOLATION
+                * The process cannot access the file because it is being used.
+                * ditto for post-export and upload.
+                */
+               graph_builder->reset ();
+
                if (fmt->tag()) {
-                       /* close file first, otherwise TagLib enounters an ERROR_SHARING_VIOLATION
-                        * The process cannot access the file because it is being used.
-                        *
-                        * TODO: check Umlauts and encoding in filename.
+                       /* TODO: check Umlauts and encoding in filename.
                         * TagLib eventually calls CreateFileA(),
                         */
                        export_status->active_job = ExportStatus::Tagging;
-                       graph_builder->reset ();
                        AudiofileTagger::tag_file(filename, *SessionMetadata::Metadata());
                }
 
index 4b48a8edd72d0ab8174658639caf10cf08550861..170073974b2eff7a6ae5822b20c0c6c5488a6956 100644 (file)
@@ -52,6 +52,7 @@ ExportStatus::init ()
 
        total_normalize_cycles = 0;
        current_normalize_cycle = 0;
+       result_map.clear();
 }
 
 void
diff --git a/libs/audiographer/audiographer/general/analyser.h b/libs/audiographer/audiographer/general/analyser.h
new file mode 100644 (file)
index 0000000..a9ce538
--- /dev/null
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2016 Robin Gareus <robin@gareus.org>
+ *
+ * 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 the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+#ifndef AUDIOGRAPHER_ANALYSER_H
+#define AUDIOGRAPHER_ANALYSER_H
+
+#include <fftw3.h>
+#include <vamp-hostsdk/PluginLoader.h>
+#include <vamp-sdk/Plugin.h>
+
+#include "audiographer/visibility.h"
+#include "audiographer/sink.h"
+#include "audiographer/routines.h"
+#include "audiographer/utils/listed_source.h"
+
+#include "pbd/fastlog.h"
+#include "ardour/export_analysis.h"
+
+namespace AudioGrapher
+{
+
+class /*LIBAUDIOGRAPHER_API*/ Analyser : public ListedSource<float>, public Sink<float>
+{
+  public:
+       Analyser (float sample_rate, unsigned int channels, framecnt_t bufsize, framecnt_t n_samples)
+       : _ebur128_plugin (0)
+       , _sample_rate (sample_rate)
+       , _channels (channels)
+       , _bufsize (bufsize / channels)
+       , _n_samples (n_samples)
+       , _pos (0)
+       {
+               assert (bufsize % channels == 0);
+               //printf("NEW ANALYSER %p r:%.1f c:%d f:%ld l%ld\n", this, sample_rate, channels, bufsize, n_samples);
+               if (channels > 0 && channels <= 2) {
+                       using namespace Vamp::HostExt;
+                       PluginLoader* loader (PluginLoader::getInstance());
+                       _ebur128_plugin = loader->loadPlugin ("libardourvampplugins:ebur128", sample_rate, PluginLoader::ADAPT_ALL_SAFE);
+                       assert (_ebur128_plugin);
+                       _ebur128_plugin->reset ();
+                       _ebur128_plugin->initialise (channels, _bufsize, _bufsize);
+               }
+               _bufs[0] = (float*) malloc (sizeof(float) * _bufsize);
+               _bufs[1] = (float*) malloc (sizeof(float) * _bufsize);
+               const size_t peaks = sizeof(_result._peaks) / sizeof (ARDOUR::PeakData::PeakDatum) / 2;
+               _spp = ceil ((_n_samples + 1.f) / (float) peaks);
+
+               _fft_data_size   = _bufsize / 2;
+               _fft_freq_per_bin = sample_rate / _fft_data_size / 2.f;
+
+               _fft_data_in  = (float *) fftwf_malloc (sizeof(float) * _bufsize);
+               _fft_data_out = (float *) fftwf_malloc (sizeof(float) * _bufsize);
+               _fft_power    = (float *) malloc (sizeof(float) * _fft_data_size);
+
+               for (uint32_t i = 0; i < _fft_data_size; ++i) {
+                       _fft_power[i] = 0;
+               }
+               for (uint32_t i = 0; i < _bufsize; ++i) {
+                       _fft_data_out[i] = 0;
+               }
+
+               _fft_plan = fftwf_plan_r2r_1d (_bufsize, _fft_data_in, _fft_data_out, FFTW_R2HC, FFTW_MEASURE);
+
+               _hann_window = (float *) malloc(sizeof(float) * _bufsize);
+               double sum = 0.0;
+
+               for (uint32_t i = 0; i < _bufsize; ++i) {
+                       _hann_window[i] = 0.5f - (0.5f * (float) cos (2.0f * M_PI * (float)i / (float)(_bufsize)));
+                       sum += _hann_window[i];
+               }
+               const double isum = 2.0 / sum;
+               for (uint32_t i = 0; i < _bufsize; ++i) {
+                       _hann_window[i] *= isum;
+               }
+       }
+
+       ~Analyser ()
+       {
+               delete _ebur128_plugin;
+               free (_bufs[0]);
+               free (_bufs[1]);
+               fftwf_destroy_plan (_fft_plan);
+               fftwf_free (_fft_data_in);
+               fftwf_free (_fft_data_out);
+               free (_fft_power);
+               free (_hann_window);
+       }
+
+       void process (ProcessContext<float> const & c)
+       {
+               framecnt_t n_samples = c.frames() / c.channels();
+               assert (c.frames() % c.channels() == 0);
+               assert (n_samples <= _bufsize);
+               //printf("PROC %p @%ld F: %ld, S: %ld C:%d\n", this, _pos, c.frames(), n_samples, c.channels());
+               float const * d = c.data ();
+               framecnt_t s;
+               for (s = 0; s < n_samples; ++s) {
+                       _fft_data_in[s] = 0;
+                       const framecnt_t pk = (_pos + s) / _spp;
+                       for (unsigned int c = 0; c < _channels; ++c) {
+                               const float v = *d;
+                               _bufs[c][s] = v;
+                               if (_result._peaks[pk].min > v) { _result._peaks[pk].min = *d; }
+                               if (_result._peaks[pk].max < v) { _result._peaks[pk].max = *d; }
+                               _fft_data_in[s] += v * _hann_window[s] / (float) _channels;
+                               ++d;
+                       }
+               }
+               for (; s < _bufsize; ++s) {
+                       for (unsigned int c = 0; c < _channels; ++c) {
+                               _bufs[c][s] = 0.f;
+                               _fft_data_in[s] = 0;
+                       }
+               }
+               if (_ebur128_plugin) {
+                       _ebur128_plugin->process (_bufs, Vamp::RealTime::fromSeconds ((double) _pos / _sample_rate));
+               }
+               fftwf_execute (_fft_plan);
+
+               _fft_power[0] = _fft_data_out[0] * _fft_data_out[0];
+
+#define FRe (_fft_data_out[i])
+#define FIm (_fft_data_out[_bufsize - i])
+               for (uint32_t i = 1; i < _fft_data_size - 1; ++i) {
+                       _fft_power[i] = (FRe * FRe) + (FIm * FIm);
+               }
+#undef FRe
+#undef FIm
+
+               // TODO handle case where  _pos / _spp != (_pos + _bufsize) / _spp
+               // TODO: get geometry from  ExportAnalysis
+               const framecnt_t x = _pos / _spp;
+               const float range = 80; // dB
+               const double ypb = 256.0 / _fft_data_size;
+
+               for (uint32_t i = 1; i < _fft_data_size - 1; ++i) {
+                       const float level = fft_power_at_bin (i, i);
+                       if (level < -range) continue;
+                       const float pk = level > 0.0 ? 1.0 : (range + level) / range;
+                       const uint32_t y = 256 - ceil (i * ypb); // log-y?
+                       assert (x >= 0 && x < 800);
+                       assert (y < 256);
+                       if (_result._spectrum[x][y] < pk) { _result._spectrum[x][y] = pk; }
+               }
+
+               _pos += n_samples;
+
+               /* pass audio audio through */
+               ListedSource<float>::output(c);
+       }
+
+       ARDOUR::ExportAnalysisPtr result () {
+               //printf("PROCESSED %ld / %ld samples\n", _pos, _n_samples);
+               if (_pos == 0) {
+                       return ARDOUR::ExportAnalysisPtr ();
+               }
+               if (_ebur128_plugin) {
+                       Vamp::Plugin::FeatureSet features = _ebur128_plugin->getRemainingFeatures ();
+                       if (!features.empty() && features.size() == 2) {
+                               _result.loudness = features[0][0].values[0];
+                               _result.loudness_range = features[1][0].values[0];
+                               _result.have_loudness = true;
+                       }
+               }
+               return ARDOUR::ExportAnalysisPtr (new ARDOUR::ExportAnalysis (_result));
+       }
+
+       using Sink<float>::process;
+
+  private:
+       ARDOUR::ExportAnalysis _result;
+       Vamp::Plugin* _ebur128_plugin;
+
+       float        _sample_rate;
+       unsigned int _channels;
+       framecnt_t   _bufsize;
+       framecnt_t   _n_samples;
+       framecnt_t   _pos;
+       framecnt_t   _spp;
+
+       float* _bufs[2];
+
+       float*     _hann_window;
+       uint32_t   _fft_data_size;
+       double     _fft_freq_per_bin;
+       float*     _fft_data_in;
+       float*     _fft_data_out;
+       float*     _fft_power;
+       fftwf_plan _fft_plan;
+
+       inline float fft_power_at_bin (const uint32_t b, const float norm) const {
+               const float a = _fft_power[b] * norm;
+               return a > 1e-12 ? 10.0 * fast_log10(a) : -INFINITY;
+       }
+
+};
+
+} // namespace
+
+#endif