From 75829d20f238c528941b91c9a64fa2ee11f5a3bd Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Wed, 7 Apr 2021 21:03:22 +0200 Subject: [PATCH] Overhaul export loudness normalization * Fix exporting multiple formats with different normalization settings or demo-noise settings * Add true-peak limiter (based on x42-limiter dpl.lv2) * Optionally use a limiter for loudness normalization * Fall back to short-term loudness when normalizing material too short for integrating loudness. --- libs/ardour/ardour/export_format_manager.h | 1 + .../ardour/export_format_specification.h | 3 + libs/ardour/ardour/export_graph_builder.h | 21 +- libs/ardour/export_format_manager.cc | 7 + libs/ardour/export_format_specification.cc | 11 +- libs/ardour/export_graph_builder.cc | 145 +++--- .../audiographer/general/limiter.h | 40 ++ .../audiographer/general/loudness_reader.h | 6 +- .../audiographer/general/normalizer.h | 9 +- libs/audiographer/private/limiter/limiter.cc | 451 ++++++++++++++++++ libs/audiographer/private/limiter/limiter.h | 128 +++++ libs/audiographer/src/general/limiter.cc | 116 +++++ .../src/general/loudness_reader.cc | 66 ++- libs/audiographer/src/general/normalizer.cc | 23 +- libs/audiographer/wscript | 2 + 15 files changed, 903 insertions(+), 126 deletions(-) create mode 100644 libs/audiographer/audiographer/general/limiter.h create mode 100644 libs/audiographer/private/limiter/limiter.cc create mode 100644 libs/audiographer/private/limiter/limiter.h create mode 100644 libs/audiographer/src/general/limiter.cc diff --git a/libs/ardour/ardour/export_format_manager.h b/libs/ardour/ardour/export_format_manager.h index 1907358bbe..da07ba5f90 100644 --- a/libs/ardour/ardour/export_format_manager.h +++ b/libs/ardour/ardour/export_format_manager.h @@ -113,6 +113,7 @@ class LIBARDOUR_API ExportFormatManager : public PBD::ScopedConnectionList void select_silence_end (AnyTime const & time); void select_normalize (bool value); void select_normalize_loudness (bool value); + void select_tp_limiter (bool value); void select_normalize_dbfs (float value); void select_normalize_lufs (float value); void select_normalize_dbtp (float value); diff --git a/libs/ardour/ardour/export_format_specification.h b/libs/ardour/ardour/export_format_specification.h index 3478835f46..c5c0e6e5c7 100644 --- a/libs/ardour/ardour/export_format_specification.h +++ b/libs/ardour/ardour/export_format_specification.h @@ -95,6 +95,7 @@ class LIBARDOUR_API ExportFormatSpecification : public ExportFormatBase { void set_trim_end (bool value) { _trim_end = value; } void set_normalize (bool value) { _normalize = value; } void set_normalize_loudness (bool value) { _normalize_loudness = value; } + void set_use_tp_limiter (bool value) { _use_tp_limiter = value; } void set_normalize_dbfs (float value) { _normalize_dbfs = value; } void set_normalize_lufs (float value) { _normalize_lufs = value; } void set_normalize_dbtp (float value) { _normalize_dbtp = value; } @@ -169,6 +170,7 @@ class LIBARDOUR_API ExportFormatSpecification : public ExportFormatBase { bool trim_end () const { return _trim_end; } bool normalize () const { return _normalize; } bool normalize_loudness () const { return _normalize_loudness; } + bool use_tp_limiter () const { return _use_tp_limiter; } float normalize_dbfs () const { return _normalize_dbfs; } float normalize_lufs () const { return _normalize_lufs; } float normalize_dbtp () const { return _normalize_dbtp; } @@ -232,6 +234,7 @@ class LIBARDOUR_API ExportFormatSpecification : public ExportFormatBase { bool _normalize; bool _normalize_loudness; + bool _use_tp_limiter; float _normalize_dbfs; float _normalize_lufs; float _normalize_dbtp; diff --git a/libs/ardour/ardour/export_graph_builder.h b/libs/ardour/ardour/export_graph_builder.h index 142d8f1e5a..a91858ee31 100644 --- a/libs/ardour/ardour/export_graph_builder.h +++ b/libs/ardour/ardour/export_graph_builder.h @@ -35,6 +35,7 @@ namespace AudioGrapher { class PeakReader; class LoudnessReader; class Normalizer; + class Limiter; class Analyser; class DemoNoiseAdder; template class Chunker; @@ -127,26 +128,32 @@ class LIBARDOUR_API ExportGraphBuilder // sample format converter class SFC { - public: + public: // This constructor so that this can be constructed like a Normalizer SFC (ExportGraphBuilder &, FileSpec const & new_config, samplecnt_t max_samples); FloatSinkPtr sink (); void add_child (FileSpec const & new_config); void remove_children (bool remove_out_files); bool operator== (FileSpec const & other_config) const; - void set_peak (float); - private: + void set_peak_dbfs (float, bool force = false); + void set_peak_lufs (AudioGrapher::LoudnessReader const&); + + private: typedef boost::shared_ptr > ChunkerPtr; typedef boost::shared_ptr DemoNoisePtr; + typedef boost::shared_ptr NormalizerPtr; + typedef boost::shared_ptr LimiterPtr; typedef boost::shared_ptr > FloatConverterPtr; typedef boost::shared_ptr > IntConverterPtr; typedef boost::shared_ptr > ShortConverterPtr; FileSpec config; - boost::ptr_list children; int data_width; + boost::ptr_list children; + NormalizerPtr normalizer; + LimiterPtr limiter; DemoNoisePtr demo_noise_adder; ChunkerPtr chunker; AnalysisPtr analyser; @@ -158,7 +165,7 @@ class LIBARDOUR_API ExportGraphBuilder }; class Intermediate { - public: + public: Intermediate (ExportGraphBuilder & parent, FileSpec const & new_config, samplecnt_t max_samples); FloatSinkPtr sink (); void add_child (FileSpec const & new_config); @@ -170,10 +177,9 @@ class LIBARDOUR_API ExportGraphBuilder /// Returns true when finished bool process (); - private: + private: typedef boost::shared_ptr PeakReaderPtr; typedef boost::shared_ptr LoudnessReaderPtr; - typedef boost::shared_ptr NormalizerPtr; typedef boost::shared_ptr > TmpFilePtr; typedef boost::shared_ptr > ThreaderPtr; typedef boost::shared_ptr > BufferPtr; @@ -190,7 +196,6 @@ class LIBARDOUR_API ExportGraphBuilder BufferPtr buffer; PeakReaderPtr peak_reader; TmpFilePtr tmp_file; - NormalizerPtr normalizer; ThreaderPtr threader; LoudnessReaderPtr loudness_reader; diff --git a/libs/ardour/export_format_manager.cc b/libs/ardour/export_format_manager.cc index 0e6bc3494d..c1482f0dfb 100644 --- a/libs/ardour/export_format_manager.cc +++ b/libs/ardour/export_format_manager.cc @@ -366,6 +366,13 @@ ExportFormatManager::select_normalize_loudness (bool value) check_for_description_change (); } +void +ExportFormatManager::select_tp_limiter (bool value) +{ + current_selection->set_use_tp_limiter (value); + check_for_description_change (); +} + void ExportFormatManager::select_normalize_dbfs (float value) { diff --git a/libs/ardour/export_format_specification.cc b/libs/ardour/export_format_specification.cc index 31dfca78c0..87212f5d2b 100644 --- a/libs/ardour/export_format_specification.cc +++ b/libs/ardour/export_format_specification.cc @@ -150,6 +150,7 @@ ExportFormatSpecification::ExportFormatSpecification (Session & s) , _normalize (false) , _normalize_loudness (false) + , _use_tp_limiter (true) , _normalize_dbfs (GAIN_COEFF_UNITY) , _normalize_lufs (-23) , _normalize_dbtp (-1) @@ -189,6 +190,7 @@ ExportFormatSpecification::ExportFormatSpecification (Session & s, XMLNode const , _normalize (false) , _normalize_loudness (false) + , _use_tp_limiter (true) , _normalize_dbfs (GAIN_COEFF_UNITY) , _normalize_lufs (-23) , _normalize_dbtp (-1) @@ -251,6 +253,7 @@ ExportFormatSpecification::ExportFormatSpecification (ExportFormatSpecification set_trim_end (other.trim_end()); set_normalize (other.normalize()); set_normalize_loudness (other.normalize_loudness()); + set_use_tp_limiter (other.use_tp_limiter()); set_normalize_dbfs (other.normalize_dbfs()); set_normalize_lufs (other.normalize_lufs()); set_normalize_dbtp (other.normalize_dbtp()); @@ -319,6 +322,7 @@ ExportFormatSpecification::get_state () node = processing->add_child ("Normalize"); node->set_property ("enabled", normalize()); node->set_property ("loudness", normalize_loudness()); + node->set_property ("use-tp-limiter", use_tp_limiter()); node->set_property ("dbfs", normalize_dbfs()); node->set_property ("lufs", normalize_lufs()); node->set_property ("dbtp", normalize_dbtp()); @@ -458,9 +462,9 @@ ExportFormatSpecification::set_state (const XMLNode & root) if ((child = proc->child ("Normalize"))) { child->get_property ("enabled", _normalize); - // old formats before ~ 4.7-930ish - child->get_property ("target", _normalize_dbfs); + child->get_property ("target", _normalize_dbfs); // old formats before ~ 4.7-930ish child->get_property ("loudness", _normalize_loudness); + child->get_property ("use-tp-limiter", _use_tp_limiter); child->get_property ("dbfs", _normalize_dbfs); child->get_property ("lufs", _normalize_lufs); child->get_property ("dbtp", _normalize_dbtp); @@ -616,6 +620,9 @@ ExportFormatSpecification::description (bool include_name) if (_normalize) { if (_normalize_loudness) { components.push_back (_("normalize loudness")); + if (_use_tp_limiter) { + components.push_back (_("limit peak")); + } } else { components.push_back (_("normalize peak")); } diff --git a/libs/ardour/export_graph_builder.cc b/libs/ardour/export_graph_builder.cc index 1fef7749c0..0fbd997f9a 100644 --- a/libs/ardour/export_graph_builder.cc +++ b/libs/ardour/export_graph_builder.cc @@ -36,6 +36,7 @@ #include "audiographer/general/cmdpipe_writer.h" #include "audiographer/general/demo_noise.h" #include "audiographer/general/interleaver.h" +#include "audiographer/general/limiter.h" #include "audiographer/general/normalizer.h" #include "audiographer/general/analyser.h" #include "audiographer/general/peak_reader.h" @@ -438,7 +439,14 @@ ExportGraphBuilder::SFC::SFC (ExportGraphBuilder &parent, FileSpec const & new_c unsigned channels = new_config.channel_config->get_n_chans(); _analyse = config.format->analyse(); - boost::shared_ptr > intermediate; + float ntarget = (config.format->normalize_loudness () || !config.format->normalize()) ? 0.0 : config.format->normalize_dbfs(); + normalizer.reset (new AudioGrapher::Normalizer (ntarget, max_samples)); + limiter.reset (new AudioGrapher::Limiter (config.format->sample_rate(), channels, max_samples)); + + normalizer->add_output (limiter); + + boost::shared_ptr > intermediate = limiter; + if (_analyse) { samplecnt_t sample_rate = parent.session.nominal_sample_rate(); samplecnt_t sb = config.format->silence_beginning_at (parent.timespan->get_start(), sample_rate); @@ -448,10 +456,12 @@ ExportGraphBuilder::SFC::SFC (ExportGraphBuilder &parent, FileSpec const & new_c chunker.reset (new Chunker (max_samples)); analyser.reset (new Analyser (config.format->sample_rate(), channels, max_samples, (samplecnt_t) ceil (duration * config.format->sample_rate () / (double) sample_rate))); - chunker->add_output (analyser); config.filename->set_channel_config (config.channel_config); parent.add_analyser (config.filename->get_path (config.format), analyser); + + chunker->add_output (analyser); + intermediate->add_output (chunker); intermediate = analyser; } @@ -468,7 +478,8 @@ ExportGraphBuilder::SFC::SFC (ExportGraphBuilder &parent, FileSpec const & new_c sample_rate * config.format->demo_noise_interval () / 1000, sample_rate * config.format->demo_noise_duration () / 1000, config.format->demo_noise_level ()); - if (intermediate) { intermediate->add_output (demo_noise_adder); } + + intermediate->add_output (demo_noise_adder); intermediate = demo_noise_adder; } @@ -476,44 +487,55 @@ ExportGraphBuilder::SFC::SFC (ExportGraphBuilder &parent, FileSpec const & new_c short_converter = ShortConverterPtr (new SampleFormatConverter (channels)); short_converter->init (max_samples, config.format->dither_type(), data_width); add_child (config); - if (intermediate) { intermediate->add_output (short_converter); } - + intermediate->add_output (short_converter); } else if (data_width == 24 || data_width == 32) { int_converter = IntConverterPtr (new SampleFormatConverter (channels)); int_converter->init (max_samples, config.format->dither_type(), data_width); add_child (config); - if (intermediate) { intermediate->add_output (int_converter); } + intermediate->add_output (int_converter); } else { int actual_data_width = 8 * sizeof(Sample); float_converter = FloatConverterPtr (new SampleFormatConverter (channels)); float_converter->init (max_samples, config.format->dither_type(), actual_data_width); add_child (config); - if (intermediate) { intermediate->add_output (float_converter); } + intermediate->add_output (float_converter); } } void -ExportGraphBuilder::SFC::set_peak (float gain) +ExportGraphBuilder::SFC::set_peak_dbfs (float peak, bool force) { + if (!config.format->normalize () && !force) { + return; + } + float gain = normalizer->set_peak (peak); if (_analyse) { analyser->set_normalization_gain (gain); } } +void +ExportGraphBuilder::SFC::set_peak_lufs (AudioGrapher::LoudnessReader const& lr) +{ + if (!config.format->normalize_loudness ()) { + return; + } + float LUFSi, LUFSs; + if (!config.format->use_tp_limiter ()) { + float peak = lr.calc_peak (config.format->normalize_lufs (), config.format->normalize_dbtp ()); + set_peak_dbfs (peak, true); + } else if (lr.get_loudness (&LUFSi, &LUFSs) && (LUFSi > -180 || LUFSs > -180)) { + float lufs = LUFSi > -180 ? LUFSi : LUFSs; + float peak = powf (10.f, .05 * (lufs - config.format->normalize_lufs () - 0.05)); + limiter->set_threshold (config.format->normalize_dbtp ()); + set_peak_dbfs (peak, true); + } +} + ExportGraphBuilder::FloatSinkPtr ExportGraphBuilder::SFC::sink () { - if (chunker) { - return chunker; - } else if (demo_noise_adder) { - return demo_noise_adder; - } else 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 normalizer; } void @@ -553,9 +575,28 @@ ExportGraphBuilder::SFC::remove_children (bool remove_out_files) } bool -ExportGraphBuilder::SFC::operator== (FileSpec const & other_config) const +ExportGraphBuilder::SFC::operator== (FileSpec const& other_config) const { - return config.format->sample_format() == other_config.format->sample_format(); + ExportFormatSpecification const& a = *config.format; + ExportFormatSpecification const& b = *other_config.format; + + bool id = a.sample_format() == b.sample_format(); + + if (a.normalize_loudness () == b.normalize_loudness ()) { + id &= a.normalize_lufs () == b.normalize_lufs (); + } else { + return false; + } + if (a.normalize () == b.normalize ()) { + id &= a.normalize_dbfs () == b.normalize_dbfs (); + } else { + return false; + } + + id &= a.demo_noise_duration () == b.demo_noise_duration (); + id &= a.demo_noise_interval () == b.demo_noise_interval (); + + return id; } /* Intermediate (Normalizer, TmpFile) */ @@ -574,22 +615,12 @@ ExportGraphBuilder::Intermediate::Intermediate (ExportGraphBuilder & parent, Fil config = new_config; uint32_t const channels = config.channel_config->get_n_chans(); max_samples_out = 4086 - (4086 % channels); // TODO good chunk size - use_loudness = config.format->normalize_loudness (); - use_peak = config.format->normalize (); buffer.reset (new AllocatingProcessContext (max_samples_out, channels)); - if (use_peak) { - peak_reader.reset (new PeakReader ()); - } - if (use_loudness) { - loudness_reader.reset (new LoudnessReader (config.format->sample_rate(), channels, max_samples)); - } - - normalizer.reset (new AudioGrapher::Normalizer (use_loudness ? 0.0 : config.format->normalize_dbfs())); + peak_reader.reset (new PeakReader ()); + loudness_reader.reset (new LoudnessReader (config.format->sample_rate(), channels, max_samples)); threader.reset (new Threader (parent.thread_pool)); - normalizer->alloc_buffer (max_samples_out); - normalizer->add_output (threader); int format = ExportFormatBase::F_RAW | ExportFormatBase::SF_Float; @@ -606,27 +637,28 @@ ExportGraphBuilder::Intermediate::Intermediate (ExportGraphBuilder & parent, Fil add_child (new_config); - if (use_loudness) { - loudness_reader->add_output (tmp_file); - } else if (use_peak) { - peak_reader->add_output (tmp_file); - } + peak_reader->add_output (loudness_reader); + loudness_reader->add_output (tmp_file); } ExportGraphBuilder::FloatSinkPtr ExportGraphBuilder::Intermediate::sink () { - if (use_loudness) { - return loudness_reader; - } else if (use_peak) { + if (use_peak) { return peak_reader; + } else if (use_loudness) { + return loudness_reader; + } else { + return tmp_file; } - return tmp_file; } void ExportGraphBuilder::Intermediate::add_child (FileSpec const & new_config) { + use_peak |= new_config.format->normalize (); + use_loudness |= new_config.format->normalize_loudness (); + for (boost::ptr_list::iterator it = children.begin(); it != children.end(); ++it) { if (*it == new_config) { it->add_child (new_config); @@ -652,14 +684,7 @@ ExportGraphBuilder::Intermediate::remove_children (bool remove_out_files) bool ExportGraphBuilder::Intermediate::operator== (FileSpec const & other_config) const { - return config.format->normalize() == other_config.format->normalize() && - config.format->normalize_loudness () == other_config.format->normalize_loudness() && - ( - (!config.format->normalize_loudness () && config.format->normalize_dbfs() == other_config.format->normalize_dbfs()) - || - // FIXME: allow simultaneous export of two formats with different loundness normalization settings - (config.format->normalize_loudness () /* lufs/dbtp is a result option, not an instantaion option */) - ); + return true; } unsigned @@ -679,22 +704,18 @@ ExportGraphBuilder::Intermediate::process() void ExportGraphBuilder::Intermediate::prepare_post_processing() { - // called in sync rt-context - float gain; - if (use_loudness) { - gain = normalizer->set_peak (loudness_reader->get_peak (config.format->normalize_lufs (), config.format->normalize_dbtp ())); - } else if (use_peak) { - gain = normalizer->set_peak (peak_reader->get_peak()); - } else { - gain = normalizer->set_peak (0.0); - } if (use_loudness || use_peak) { - // push info to analyzers for (boost::ptr_list::iterator i = children.begin(); i != children.end(); ++i) { - (*i).set_peak (gain); + if (use_peak) { + (*i).set_peak_dbfs (peak_reader->get_peak()); + } + if (use_loudness) { + (*i).set_peak_lufs (*loudness_reader); + } } } - tmp_file->add_output (normalizer); + + tmp_file->add_output (threader); parent.intermediates.push_back (this); } diff --git a/libs/audiographer/audiographer/general/limiter.h b/libs/audiographer/audiographer/general/limiter.h new file mode 100644 index 0000000000..89dcfb189d --- /dev/null +++ b/libs/audiographer/audiographer/general/limiter.h @@ -0,0 +1,40 @@ +#ifndef AUDIOGRAPHER_LIMITER_H +#define AUDIOGRAPHER_LIMITER_H + +#include "audiographer/visibility.h" +#include "audiographer/sink.h" +#include "audiographer/utils/listed_source.h" + +#include "private/limiter/limiter.h" + +namespace AudioGrapher +{ + +class LIBAUDIOGRAPHER_API Limiter + : public ListedSource + , public Sink + , public Throwing<> +{ +public: + Limiter (float sample_rate, unsigned int channels, samplecnt_t); + ~Limiter (); + + void set_input_gain (float dB); + void set_threshold (float dB); + void set_release (float s); + + void process (ProcessContext const& ctx); + using Sink::process; + +private: + bool _enabled; + float* _buf; + samplecnt_t _size; + samplecnt_t _latency; + + AudioGrapherDSP::Limiter _limiter; +}; + +} // namespace + +#endif diff --git a/libs/audiographer/audiographer/general/loudness_reader.h b/libs/audiographer/audiographer/general/loudness_reader.h index 94643c2133..5e305cb132 100644 --- a/libs/audiographer/audiographer/general/loudness_reader.h +++ b/libs/audiographer/audiographer/general/loudness_reader.h @@ -39,10 +39,8 @@ class LIBAUDIOGRAPHER_API LoudnessReader : public ListedSource, public Si void reset (); - float get_normalize_gain (float target_lufs, float target_dbtp); - float get_peak (float target_lufs = -23.f, float target_dbtp = -1.f) { - return 1.f / get_normalize_gain (target_lufs, target_dbtp); - } + float calc_peak (float target_lufs = -23, float target_dbtp = -1) const; + bool get_loudness (float* integrated, float* short_term = NULL, float* momentary = NULL) const; virtual void process (ProcessContext const & c); diff --git a/libs/audiographer/audiographer/general/normalizer.h b/libs/audiographer/audiographer/general/normalizer.h index cd4e375db2..30aadde1f9 100644 --- a/libs/audiographer/audiographer/general/normalizer.h +++ b/libs/audiographer/audiographer/general/normalizer.h @@ -17,19 +17,12 @@ class LIBAUDIOGRAPHER_API Normalizer { public: /// Constructs a normalizer with a specific target in dB \n RT safe - Normalizer (float target_dB); + Normalizer (float target_dB, samplecnt_t); ~Normalizer(); /// Sets the peak found in the material to be normalized \see PeakReader \n RT safe float set_peak (float peak); - /** Allocates a buffer for using with const ProcessContexts - * This function does not need to be called if - * non-const ProcessContexts are given to \a process() . - * \n Not RT safe - */ - void alloc_buffer(samplecnt_t samples); - /// Process a const ProcessContext \see alloc_buffer() \n RT safe void process (ProcessContext const & c); diff --git a/libs/audiographer/private/limiter/limiter.cc b/libs/audiographer/private/limiter/limiter.cc new file mode 100644 index 0000000000..f940444ceb --- /dev/null +++ b/libs/audiographer/private/limiter/limiter.cc @@ -0,0 +1,451 @@ +/* + * Copyright (C) 2010-2018 Fons Adriaensen + * Copyright (C) 2021 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 3 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, see . + */ + +#include +#include +#include +#include + +#include "private/limiter/limiter.h" + +using namespace AudioGrapherDSP; + +void +Limiter::Histmin::init (int hlen) +{ + assert (hlen <= SIZE); + _hlen = hlen; + _hold = hlen; + _wind = 0; + _vmin = 1; + for (int i = 0; i < SIZE; i++) { + _hist[i] = _vmin; + } +} + +float +Limiter::Histmin::write (float v) +{ + int i = _wind; + _hist[i] = v; + + if (v <= _vmin) { + _vmin = v; + _hold = _hlen; + } else if (--_hold == 0) { + _vmin = v; + _hold = _hlen; + for (int j = 1 - _hlen; j < 0; j++) { + v = _hist[(i + j) & MASK]; + if (v < _vmin) { + _vmin = v; + _hold = _hlen + j; + } + } + } + _wind = ++i & MASK; + return _vmin; +} + +Limiter::Upsampler::Upsampler () + : _nchan (0) + , _z (0) +{ +} + +Limiter::Upsampler::~Upsampler () +{ + fini (); +} + +void +Limiter::Upsampler::fini () +{ + for (int i = 0; i < _nchan; ++i) { + delete _z[i]; + } + delete _z; + _nchan = 0; + _z = 0; +} + +void +Limiter::Upsampler::init (int nchan) +{ + fini (); + + _nchan = nchan; + _z = new float*[nchan]; + for (int i = 0; i < _nchan; ++i) { + _z[i] = new float[48]; + for (int j = 0; j < 48; ++j) { + _z[i][j] = 0.0f; + } + } +} + +float +Limiter::Upsampler::process_one (int chn, float const x) +{ + float* r = _z[chn]; + float u[4]; + r[47] = x; + /* 4x upsample for true-peak analysis, cosine windowed sinc + * + * This effectively introduces a latency of 23 samples, however + * the lookahead window is longer. Still, this may allow some + * true-peak transients to slip though. + * Note that digital peak limit is not affected by this. + */ + /* clang-format off */ + u[0] = r[47]; + u[1] = r[ 0] * -2.330790e-05f + r[ 1] * +1.321291e-04f + r[ 2] * -3.394408e-04f + r[ 3] * +6.562235e-04f + + r[ 4] * -1.094138e-03f + r[ 5] * +1.665807e-03f + r[ 6] * -2.385230e-03f + r[ 7] * +3.268371e-03f + + r[ 8] * -4.334012e-03f + r[ 9] * +5.604985e-03f + r[10] * -7.109989e-03f + r[11] * +8.886314e-03f + + r[12] * -1.098403e-02f + r[13] * +1.347264e-02f + r[14] * -1.645206e-02f + r[15] * +2.007155e-02f + + r[16] * -2.456432e-02f + r[17] * +3.031531e-02f + r[18] * -3.800644e-02f + r[19] * +4.896667e-02f + + r[20] * -6.616853e-02f + r[21] * +9.788141e-02f + r[22] * -1.788607e-01f + r[23] * +9.000753e-01f + + r[24] * +2.993829e-01f + r[25] * -1.269367e-01f + r[26] * +7.922398e-02f + r[27] * -5.647748e-02f + + r[28] * +4.295093e-02f + r[29] * -3.385706e-02f + r[30] * +2.724946e-02f + r[31] * -2.218943e-02f + + r[32] * +1.816976e-02f + r[33] * -1.489313e-02f + r[34] * +1.217411e-02f + r[35] * -9.891211e-03f + + r[36] * +7.961470e-03f + r[37] * -6.326144e-03f + r[38] * +4.942202e-03f + r[39] * -3.777065e-03f + + r[40] * +2.805240e-03f + r[41] * -2.006106e-03f + r[42] * +1.362416e-03f + r[43] * -8.592768e-04f + + r[44] * +4.834383e-04f + r[45] * -2.228007e-04f + r[46] * +6.607267e-05f + r[47] * -2.537056e-06f; + u[2] = r[ 0] * -1.450055e-05f + r[ 1] * +1.359163e-04f + r[ 2] * -3.928527e-04f + r[ 3] * +8.006445e-04f + + r[ 4] * -1.375510e-03f + r[ 5] * +2.134915e-03f + r[ 6] * -3.098103e-03f + r[ 7] * +4.286860e-03f + + r[ 8] * -5.726614e-03f + r[ 9] * +7.448018e-03f + r[10] * -9.489286e-03f + r[11] * +1.189966e-02f + + r[12] * -1.474471e-02f + r[13] * +1.811472e-02f + r[14] * -2.213828e-02f + r[15] * +2.700557e-02f + + r[16] * -3.301023e-02f + r[17] * +4.062971e-02f + r[18] * -5.069345e-02f + r[19] * +6.477499e-02f + + r[20] * -8.625619e-02f + r[21] * +1.239454e-01f + r[22] * -2.101678e-01f + r[23] * +6.359382e-01f + + r[24] * +6.359382e-01f + r[25] * -2.101678e-01f + r[26] * +1.239454e-01f + r[27] * -8.625619e-02f + + r[28] * +6.477499e-02f + r[29] * -5.069345e-02f + r[30] * +4.062971e-02f + r[31] * -3.301023e-02f + + r[32] * +2.700557e-02f + r[33] * -2.213828e-02f + r[34] * +1.811472e-02f + r[35] * -1.474471e-02f + + r[36] * +1.189966e-02f + r[37] * -9.489286e-03f + r[38] * +7.448018e-03f + r[39] * -5.726614e-03f + + r[40] * +4.286860e-03f + r[41] * -3.098103e-03f + r[42] * +2.134915e-03f + r[43] * -1.375510e-03f + + r[44] * +8.006445e-04f + r[45] * -3.928527e-04f + r[46] * +1.359163e-04f + r[47] * -1.450055e-05f; + u[3] = r[ 0] * -2.537056e-06f + r[ 1] * +6.607267e-05f + r[ 2] * -2.228007e-04f + r[ 3] * +4.834383e-04f + + r[ 4] * -8.592768e-04f + r[ 5] * +1.362416e-03f + r[ 6] * -2.006106e-03f + r[ 7] * +2.805240e-03f + + r[ 8] * -3.777065e-03f + r[ 9] * +4.942202e-03f + r[10] * -6.326144e-03f + r[11] * +7.961470e-03f + + r[12] * -9.891211e-03f + r[13] * +1.217411e-02f + r[14] * -1.489313e-02f + r[15] * +1.816976e-02f + + r[16] * -2.218943e-02f + r[17] * +2.724946e-02f + r[18] * -3.385706e-02f + r[19] * +4.295093e-02f + + r[20] * -5.647748e-02f + r[21] * +7.922398e-02f + r[22] * -1.269367e-01f + r[23] * +2.993829e-01f + + r[24] * +9.000753e-01f + r[25] * -1.788607e-01f + r[26] * +9.788141e-02f + r[27] * -6.616853e-02f + + r[28] * +4.896667e-02f + r[29] * -3.800644e-02f + r[30] * +3.031531e-02f + r[31] * -2.456432e-02f + + r[32] * +2.007155e-02f + r[33] * -1.645206e-02f + r[34] * +1.347264e-02f + r[35] * -1.098403e-02f + + r[36] * +8.886314e-03f + r[37] * -7.109989e-03f + r[38] * +5.604985e-03f + r[39] * -4.334012e-03f + + r[40] * +3.268371e-03f + r[41] * -2.385230e-03f + r[42] * +1.665807e-03f + r[43] * -1.094138e-03f + + r[44] * +6.562235e-04f + r[45] * -3.394408e-04f + r[46] * +1.321291e-04f + r[47] * -2.330790e-05f; + /* clang-format on */ + + for (int i = 0; i < 47; ++i) { + r[i] = r[i + 1]; + } + + float p1 = std::max (fabsf (u[0]), fabsf (u[1])); + float p2 = std::max (fabsf (u[2]), fabsf (u[3])); + return std::max (p1, p2); +} + +Limiter::Limiter (void) + : _fsamp (0) + , _nchan (0) + , _truepeak (false) + , _dly_buf (0) + , _zlf (0) + , _rstat (false) + , _peak (0) + , _gmax (1) + , _gmin (1) +{ +} + +Limiter::~Limiter (void) +{ + fini (); +} + +void +Limiter::set_inpgain (float v) +{ + _g1 = powf (10.f, 0.05f * v); +} + +void +Limiter::set_threshold (float v) +{ + _gt = powf (10.f, -0.05f * v); +} + +void +Limiter::set_release (float v) +{ + if (v > 1.f) { + v = 1.f; + } + if (v < 1e-3f) { + v = 1e-3f; + } + _w3 = 1.f / (v * _fsamp); +} + +void +Limiter::set_truepeak (bool v) +{ + if (_truepeak == v) { + return; + } + _upsampler.init (_nchan); + _truepeak = v; +} + +void +Limiter::init (float fsamp, int nchan) +{ + if (nchan == _nchan) { + return; + } + + fini (); + + if (nchan == 0) { + return; + } + + _fsamp = fsamp; + + if (fsamp > 130000) { + _div1 = 32; + } else if (fsamp > 65000) { + _div1 = 16; + } else { + _div1 = 8; + } + + _nchan = nchan; + _div2 = 8; + int k1 = (int)(ceilf (1.2e-3f * fsamp / _div1)); + int k2 = 12; + _delay = k1 * _div1; + + int dly_size; + for (dly_size = 64; dly_size < _delay + _div1; dly_size *= 2) ; + + + _dly_mask = dly_size - 1; + _dly_ridx = 0; + + _dly_buf = new float*[_nchan]; + _zlf = new float[_nchan]; + + for (int i = 0; i < _nchan; i++) { + _dly_buf[i] = new float[dly_size]; + memset (_dly_buf[i], 0, dly_size * sizeof (float)); + _zlf[i] = 0.f; + } + + _hist1.init (k1 + 1); + _hist2.init (k2); + + _c1 = _div1; + _c2 = _div2; + _m1 = 0.f; + _m2 = 0.f; + _wlf = 6.28f * 500.f / fsamp; + _w1 = 10.f / _delay; + _w2 = _w1 / _div2; + _w3 = 1.f / (0.01f * fsamp); + _z1 = 1.f; + _z2 = 1.f; + _z3 = 1.f; + _gt = 1.f; + _g0 = 1.f; + _g1 = 1.f; + _dg = 0.f; + + _peak = 0.f; + _gmax = 1.f; + _gmin = 1.f; +} + +void +Limiter::fini (void) +{ + for (int i = 0; i < _nchan; i++) { + delete[] _dly_buf[i]; + _dly_buf[i] = 0; + } + delete[] _dly_buf; + delete[] _zlf; + _zlf = 0; + _nchan = 0; +} + +/* + * _g1 : input-gain (target) + * _g0 : current gain (LPFed) + * _dg : gain-delta per sample, updated every (_div1 * _div2) samples + * + * _gt : threshold + * + * _m1 : digital-peak (reset per _div1 cycle) + * _m2 : low-pass filtered (_wlf) digital-peak (reset per _div2 cycle) + * + * _zlf[] helper to calc _m2 (per channel LPF'ed input) with input-gain applied + * + * _c1 : coarse chunk-size (sr dependent), count-down _div1 + * _c2 : 8x divider of _c1 cycle + * + * _h1 : target gain-reduction according to 1 / _m1 (per _div1 cycle) + * _h2 : target gain-reduction according to 1 / _m2 (per _div2 cycle) + * + * _z1 : LPFed (_w1) _h1 gain (digital peak) + * _z2 : LPFed (_w2) _h2 gain (_wlf filtered digital peak) + * + * _z3 : actual gain to apply (max of _z1, z2) + * falls (more gain-reduction) via _w1 (per sample); + * rises (less gain-reduction) via _w3 (per sample); + * + * _w1 : 10 / delay; + * _w2 : _w1 / _div2 + * _w3 : user-set release time + * + * _dly_ridx: offset in delay ringbuffer + * ri, wi; read/write indices + */ +void +Limiter::process (int nframes, float const* inp, float* out) +{ + int ri, wi; + float h1, h2, m1, m2, z1, z2, z3, pk, t0, t1; + + ri = _dly_ridx; + wi = (ri + _delay) & _dly_mask; + h1 = _hist1.vmin (); + h2 = _hist2.vmin (); + m1 = _m1; + m2 = _m2; + z1 = _z1; + z2 = _z2; + z3 = _z3; + + if (_rstat) { + _rstat = false; + pk = 0; + t0 = _gmax; + t1 = _gmin; + } else { + pk = _peak; + t0 = _gmin; + t1 = _gmax; + } + + int k = 0; + while (nframes) { + int n = (_c1 < nframes) ? _c1 : nframes; + float g = _g0; + for (int j = 0; j < _nchan; j++) { + float z = _zlf[j]; + float d = _dg; + g = _g0; + for (int i = 0; i < n; i++) { + float x = g * inp[j + (i + k) * _nchan]; + g += d; + _dly_buf[j][wi + i] = x; + z += _wlf * (x - z) + 1e-20f; + + if (_truepeak) { + x = _upsampler.process_one (j, x); + } else { + x = fabsf (x); + } + + if (x > m1) { + m1 = x; + } + x = fabsf (z); + if (x > m2) { + m2 = x; + } + } + _zlf[j] = z; + } + _g0 = g; + + _c1 -= n; + if (_c1 == 0) { + m1 *= _gt; + if (m1 > pk) { + pk = m1; + } + h1 = (m1 > 1.f) ? 1.f / m1 : 1.f; + h1 = _hist1.write (h1); + m1 = 0; + _c1 = _div1; + if (--_c2 == 0) { + m2 *= _gt; + h2 = (m2 > 1.f) ? 1.f / m2 : 1.f; + h2 = _hist2.write (h2); + m2 = 0; + _c2 = _div2; + _dg = _g1 - _g0; + if (fabsf (_dg) < 1e-9f) { + _g0 = _g1; + _dg = 0; + } else { + _dg /= _div1 * _div2; + } + } + } + + for (int i = 0; i < n; i++) { + z1 += _w1 * (h1 - z1); + z2 += _w2 * (h2 - z2); + float z = (z2 < z1) ? z2 : z1; + if (z < z3) { + z3 += _w1 * (z - z3); + } else { + z3 += _w3 * (z - z3); + } + if (z3 > t1) { + t1 = z3; + } + if (z3 < t0) { + t0 = z3; + } + for (int j = 0; j < _nchan; j++) { + out[j + (k + i) * _nchan] = z3 * _dly_buf[j][ri + i]; + } + } + + wi = (wi + n) & _dly_mask; + ri = (ri + n) & _dly_mask; + k += n; + nframes -= n; + } + + _m1 = m1; + _m2 = m2; + _z1 = z1; + _z2 = z2; + _z3 = z3; + + _dly_ridx = ri; + _peak = pk; + _gmin = t0; + _gmax = t1; +} diff --git a/libs/audiographer/private/limiter/limiter.h b/libs/audiographer/private/limiter/limiter.h new file mode 100644 index 0000000000..354372de19 --- /dev/null +++ b/libs/audiographer/private/limiter/limiter.h @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2010-2018 Fons Adriaensen + * Copyright (C) 2021 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 3 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, see . + */ + +#ifndef _PEAKLIM_H +#define _PEAKLIM_H + +#include + +namespace AudioGrapherDSP { + +class Limiter +{ +public: + Limiter (); + ~Limiter (); + + void init (float fsamp, int nchan); + void fini (); + + void set_inpgain (float); + void set_threshold (float); + void set_release (float); + void set_truepeak (bool); + + int + get_latency () const + { + return _delay; + } + + void + get_stats (float* peak, float* gmax, float* gmin) + { + *peak = _peak; + *gmax = _gmax; + *gmin = _gmin; + _rstat = true; + } + + void process (int nsamp, float const* inp, float* out); + +private: + class Histmin + { + public: + void init (int hlen); + float write (float v); + float vmin () { return _vmin; } + + private: + enum { + SIZE = 32, + MASK = SIZE - 1 + }; + + int _hlen; + int _hold; + int _wind; + float _vmin; + float _hist[SIZE]; + }; + + class Upsampler + { + public: + Upsampler (); + ~Upsampler (); + + void init (int nchan); + void fini (); + + int + get_latency () const + { + return 23; + } + + float process_one (int chn, float const x); + + private: + int _nchan; + float** _z; + }; + + float _fsamp; + int _nchan; + bool _truepeak; + + float** _dly_buf; + float* _zlf; + + int _delay; + int _dly_mask; + int _dly_ridx; + int _div1, _div2; + int _c1, _c2; + float _g0, _g1, _dg; + float _gt, _m1, _m2; + float _w1, _w2, _w3, _wlf; + float _z1, _z2, _z3; + + bool _rstat; + float _peak; + float _gmax; + float _gmin; + + Upsampler _upsampler; + Histmin _hist1; + Histmin _hist2; +}; + +} +#endif diff --git a/libs/audiographer/src/general/limiter.cc b/libs/audiographer/src/general/limiter.cc new file mode 100644 index 0000000000..1128f38284 --- /dev/null +++ b/libs/audiographer/src/general/limiter.cc @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2021 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. + */ + +#include "audiographer/general/limiter.h" + +namespace AudioGrapher +{ + +Limiter::Limiter (float sample_rate, unsigned int channels, samplecnt_t size) + : _enabled (false) + , _buf (0) + , _size (0) + { + + _limiter.init (sample_rate, channels); + _limiter.set_truepeak (true); + _limiter.set_inpgain (0); + _limiter.set_threshold (-1); + _limiter.set_release (0.01); + + _latency = _limiter.get_latency (); + _buf = new float[size]; + _size = size; +} + +Limiter::~Limiter () +{ + delete [] _buf; +} + +void +Limiter::set_input_gain (float dB) +{ + _enabled = _enabled || dB != 0; + _limiter.set_inpgain (dB); +} + +void +Limiter::set_threshold (float dB) +{ + _enabled = true; + _limiter.set_threshold (dB); +} + +void +Limiter::set_release (float s) +{ + _limiter.set_release (s); +} + +void Limiter::process (ProcessContext const& ctx) +{ + const samplecnt_t n_samples = ctx.samples_per_channel (); + const int n_channels = ctx.channels (); + + if (!_enabled) { + ProcessContext c_out (ctx); + ListedSource::output (c_out); + return; + } + + _limiter.process (n_samples, ctx.data (), _buf); + + if (_latency > 0) { + samplecnt_t ns = n_samples > _latency ? n_samples - _latency : 0; + if (ns > 0) { + ProcessContext ctx_out (ctx, &_buf[n_channels * _latency], n_channels * ns); + ctx_out.remove_flag (ProcessContext::EndOfInput); + this->output (ctx_out); + } + if (n_samples >= _latency) { + _latency = 0; + } else { + _latency -= n_samples; + } + } else { + ProcessContext ctx_out (ctx, _buf); + ctx_out.remove_flag (ProcessContext::EndOfInput); + this->output (ctx_out); + } + + if (ctx.has_flag(ProcessContext::EndOfInput)) { + samplecnt_t bs = _size / n_channels; + _latency = _limiter.get_latency (); + while (_latency > 0) { + memset (_buf, 0, _size * sizeof (float)); + samplecnt_t ns = _latency > bs ? bs : _latency; + _limiter.process (ns, _buf, _buf); + ProcessContext ctx_out (ctx, _buf, ns * n_channels); + if (_latency == ns) { + ctx_out.set_flag (ProcessContext::EndOfInput); + } else { + ctx_out.remove_flag (ProcessContext::EndOfInput); + } + this->output (ctx_out); + _latency -= ns; + } + } +} + +} // namespace diff --git a/libs/audiographer/src/general/loudness_reader.cc b/libs/audiographer/src/general/loudness_reader.cc index 6e9facb6ca..fa53f0bd96 100644 --- a/libs/audiographer/src/general/loudness_reader.cc +++ b/libs/audiographer/src/general/loudness_reader.cc @@ -143,52 +143,66 @@ LoudnessReader::process (ProcessContext const & ctx) ListedSource::output (ctx); } -float -LoudnessReader::get_normalize_gain (float target_lufs, float target_dbtp) +bool +LoudnessReader::get_loudness (float* integrated, float* short_term, float* momentary) const { - float dBTP = 0; - float LUFS = -200; - uint32_t have_lufs = 0; - uint32_t have_dbtp = 0; - if (_ebur_plugin) { Vamp::Plugin::FeatureSet features = _ebur_plugin->getRemainingFeatures (); if (!features.empty () && features.size () == 3) { - const float lufs = features[0][0].values[0]; - LUFS = std::max (LUFS, lufs); - ++have_lufs; + if (integrated) { + *integrated = features[0][0].values[0]; + } + if (short_term) { + *short_term = features[0][1].values[0]; + } + if (momentary) { + *momentary = features[0][2].values[0]; + } + return true; } } + return false; +} + +float +LoudnessReader::calc_peak (float target_lufs, float target_dbtp) const +{ + float LUFSi = 0; + float LUFSs = 0; + uint32_t have_dbtp = 0; + float tp_coeff = 0; + + bool have_lufs = get_loudness (&LUFSi, &LUFSs); for (unsigned int c = 0; c < _channels && c < _dbtp_plugins.size(); ++c) { Vamp::Plugin::FeatureSet features = _dbtp_plugins.at(c)->getRemainingFeatures (); if (!features.empty () && features.size () == 2) { - const float dbtp = features[0][0].values[0]; - dBTP = std::max (dBTP, dbtp); + const float tp = features[0][0].values[0]; + tp_coeff = std::max (tp_coeff, tp); ++have_dbtp; } } - float g = 100000.0; // +100dB + float g = 1.f; bool set = false; - if (have_lufs && LUFS > -180.0f && target_lufs <= 0.f) { - const float ge = pow (10.f, (target_lufs * 0.05f)) / pow (10.f, (LUFS * 0.05f)); - //printf ("LU: %f LUFS, %f\n", LUFS, ge); - g = std::min (g, ge); + + if (have_lufs && LUFSi > -180.0f && target_lufs <= 0.f) { + g = powf (10.f, .05f * (LUFSi - target_lufs)); + set = true; + } else if (have_lufs && LUFSs > -180.0f && target_lufs <= 0.f) { + g = powf (10.f, .05f * (LUFSs - target_lufs)); set = true; } - // TODO check that all channels were used.. ? (have_dbtp == _channels) - if (have_dbtp && dBTP > 0.f && target_dbtp <= 0.f) { - const float ge = pow (10.f, (target_dbtp * 0.05f)) / dBTP; - //printf ("TP:(%d chn) %fdBTP -> %f\n", have_dbtp, dBTP, ge); - g = std::min (g, ge); + if (have_dbtp && tp_coeff > 0.f && target_dbtp <= 0.f) { + const float ge = tp_coeff / powf (10.f, .05f * target_dbtp); + if (set) { + g = std::max (g, ge); + } else { + g = ge; + } set = true; } - if (!set) { - g = 1.f; - } - //printf ("LF %f / %f\n", g, 1.f / g); return g; } diff --git a/libs/audiographer/src/general/normalizer.cc b/libs/audiographer/src/general/normalizer.cc index c64f229d05..30073caa90 100644 --- a/libs/audiographer/src/general/normalizer.cc +++ b/libs/audiographer/src/general/normalizer.cc @@ -23,12 +23,14 @@ namespace AudioGrapher { -Normalizer::Normalizer (float target_dB) +Normalizer::Normalizer (float target_dB, samplecnt_t size) : enabled (false) , buffer (0) , buffer_size (0) { target = pow (10.0f, target_dB * 0.05f); + buffer = new float[size]; + buffer_size = size; } Normalizer::~Normalizer() @@ -49,18 +51,6 @@ float Normalizer::set_peak (float peak) return enabled ? gain : 1.0; } -/** Allocates a buffer for using with const ProcessContexts - * This function does not need to be called if - * non-const ProcessContexts are given to \a process() . - * \n Not RT safe - */ -void Normalizer::alloc_buffer(samplecnt_t samples) -{ - delete [] buffer; - buffer = new float[samples]; - buffer_size = samples; -} - /// Process a const ProcessContext \see alloc_buffer() \n RT safe void Normalizer::process (ProcessContext const & c) { @@ -71,10 +61,11 @@ void Normalizer::process (ProcessContext const & c) if (enabled) { memcpy (buffer, c.data(), c.samples() * sizeof(float)); Routines::apply_gain_to_buffer (buffer, c.samples(), gain); + ProcessContext c_out (c, buffer); + ListedSource::output (c_out); + } else { + ListedSource::output(c); } - - ProcessContext c_out (c, buffer); - ListedSource::output (c_out); } /// Process a non-const ProcsesContext in-place \n RT safe diff --git a/libs/audiographer/wscript b/libs/audiographer/wscript index b04245acea..4efd1bda98 100644 --- a/libs/audiographer/wscript +++ b/libs/audiographer/wscript @@ -60,6 +60,7 @@ def build(bld): audiographer_sources = [ 'private/gdither/gdither.cc', + 'private/limiter/limiter.cc', 'src/general/sample_format_converter.cc', 'src/routines.cc', 'src/debug_utils.cc', @@ -67,6 +68,7 @@ def build(bld): 'src/general/broadcast_info.cc', 'src/general/demo_noise.cc', 'src/general/loudness_reader.cc', + 'src/general/limiter.cc', 'src/general/normalizer.cc' ] if bld.is_defined('HAVE_SAMPLERATE'):