From c1642fead82c58e7ca546b375aaf95459a803d96 Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Wed, 10 Feb 2016 03:01:05 +0100 Subject: Post-export Analysis --- libs/ardour/ardour/export_analysis.h | 60 ++++++ libs/ardour/ardour/export_graph_builder.h | 12 ++ libs/ardour/ardour/export_status.h | 3 + libs/ardour/export_graph_builder.cc | 28 ++- libs/ardour/export_handler.cc | 15 +- libs/ardour/export_status.cc | 1 + libs/audiographer/audiographer/general/analyser.h | 215 ++++++++++++++++++++++ 7 files changed, 320 insertions(+), 14 deletions(-) create mode 100644 libs/ardour/ardour/export_analysis.h create mode 100644 libs/audiographer/audiographer/general/analyser.h diff --git a/libs/ardour/ardour/export_analysis.h b/libs/ardour/ardour/export_analysis.h new file mode 100644 index 0000000000..bab79bba4e --- /dev/null +++ b/libs/ardour/ardour/export_analysis.h @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2016 Robin Gareus + * + * 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 +#include +#include + +#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 ExportAnalysisPtr; + typedef std::map AnalysisResults; + +} // namespace ARDOUR +#endif diff --git a/libs/ardour/ardour/export_graph_builder.h b/libs/ardour/ardour/export_graph_builder.h index 3e9fb58a15..d14a00997a 100644 --- a/libs/ardour/ardour/export_graph_builder.h +++ b/libs/ardour/ardour/export_graph_builder.h @@ -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 class Chunker; template class SampleFormatConverter; template class Interleaver; @@ -55,7 +57,9 @@ class LIBARDOUR_API ExportGraphBuilder typedef boost::shared_ptr > FloatSinkPtr; typedef boost::shared_ptr > IdentityVertexPtr; + typedef boost::shared_ptr AnalysisPtr; typedef std::map ChannelMap; + typedef std::map AnalysisMap; public: @@ -71,9 +75,14 @@ class LIBARDOUR_API ExportGraphBuilder void cleanup (bool remove_out_files = false); void set_current_timespan (boost::shared_ptr 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 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 normalizers; + AnalysisMap analysis_map; + Glib::ThreadPool thread_pool; }; diff --git a/libs/ardour/ardour/export_status.h b/libs/ardour/ardour/export_status.h index 95921629c7..f250ae0dc6 100644 --- a/libs/ardour/ardour/export_status.h +++ b/libs/ardour/ardour/export_status.h @@ -24,6 +24,7 @@ #include #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; diff --git a/libs/ardour/export_graph_builder.cc b/libs/ardour/export_graph_builder.cc index c054d85242..685db817d4 100644 --- a/libs/ardour/export_graph_builder.cc +++ b/libs/ardour/export_graph_builder.cc @@ -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 (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 (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 (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 diff --git a/libs/ardour/export_handler.cc b/libs/ardour/export_handler.cc index 70c807b7de..6b59afec89 100644 --- a/libs/ardour/export_handler.cc +++ b/libs/ardour/export_handler.cc @@ -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()); } diff --git a/libs/ardour/export_status.cc b/libs/ardour/export_status.cc index 4b48a8edd7..170073974b 100644 --- a/libs/ardour/export_status.cc +++ b/libs/ardour/export_status.cc @@ -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 index 0000000000..a9ce538fba --- /dev/null +++ b/libs/audiographer/audiographer/general/analyser.h @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2016 Robin Gareus + * + * 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 +#include +#include + +#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, public Sink +{ + 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 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::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::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 -- cgit v1.2.3