13
0

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.
This commit is contained in:
Robin Gareus 2021-04-07 21:03:22 +02:00
parent 8f5c3fcddb
commit 75829d20f2
Signed by: rgareus
GPG Key ID: A090BCE02CF57F04
15 changed files with 903 additions and 126 deletions

View File

@ -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);

View File

@ -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;

View File

@ -35,6 +35,7 @@ namespace AudioGrapher {
class PeakReader;
class LoudnessReader;
class Normalizer;
class Limiter;
class Analyser;
class DemoNoiseAdder;
template <typename T> 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<AudioGrapher::Chunker<float> > ChunkerPtr;
typedef boost::shared_ptr<AudioGrapher::DemoNoiseAdder> DemoNoisePtr;
typedef boost::shared_ptr<AudioGrapher::Normalizer> NormalizerPtr;
typedef boost::shared_ptr<AudioGrapher::Limiter> LimiterPtr;
typedef boost::shared_ptr<AudioGrapher::SampleFormatConverter<Sample> > FloatConverterPtr;
typedef boost::shared_ptr<AudioGrapher::SampleFormatConverter<int> > IntConverterPtr;
typedef boost::shared_ptr<AudioGrapher::SampleFormatConverter<short> > ShortConverterPtr;
FileSpec config;
boost::ptr_list<Encoder> children;
int data_width;
boost::ptr_list<Encoder> 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<AudioGrapher::PeakReader> PeakReaderPtr;
typedef boost::shared_ptr<AudioGrapher::LoudnessReader> LoudnessReaderPtr;
typedef boost::shared_ptr<AudioGrapher::Normalizer> NormalizerPtr;
typedef boost::shared_ptr<AudioGrapher::TmpFile<Sample> > TmpFilePtr;
typedef boost::shared_ptr<AudioGrapher::Threader<Sample> > ThreaderPtr;
typedef boost::shared_ptr<AudioGrapher::AllocatingProcessContext<Sample> > BufferPtr;
@ -190,7 +196,6 @@ class LIBARDOUR_API ExportGraphBuilder
BufferPtr buffer;
PeakReaderPtr peak_reader;
TmpFilePtr tmp_file;
NormalizerPtr normalizer;
ThreaderPtr threader;
LoudnessReaderPtr loudness_reader;

View File

@ -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)
{

View File

@ -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"));
}

View File

@ -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<AudioGrapher::ListedSource<float> > 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<AudioGrapher::ListedSource<float> > 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<Sample> (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<short> (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<int> (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<Sample> (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<Sample> (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<Sample> (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<SFC>::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<SFC>::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);
}

View File

@ -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<float>
, public Sink<float>
, 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<float> const& ctx);
using Sink<float>::process;
private:
bool _enabled;
float* _buf;
samplecnt_t _size;
samplecnt_t _latency;
AudioGrapherDSP::Limiter _limiter;
};
} // namespace
#endif

View File

@ -39,10 +39,8 @@ class LIBAUDIOGRAPHER_API LoudnessReader : public ListedSource<float>, 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<float> const & c);

View File

@ -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<float> const & c);

View File

@ -0,0 +1,451 @@
/*
* Copyright (C) 2010-2018 Fons Adriaensen <fons@linuxaudio.org>
* Copyright (C) 2021 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 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 <http://www.gnu.org/licenses/>.
*/
#include <algorithm>
#include <assert.h>
#include <math.h>
#include <string.h>
#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;
}

View File

@ -0,0 +1,128 @@
/*
* Copyright (C) 2010-2018 Fons Adriaensen <fons@linuxaudio.org>
* Copyright (C) 2021 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 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 <http://www.gnu.org/licenses/>.
*/
#ifndef _PEAKLIM_H
#define _PEAKLIM_H
#include <stdint.h>
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

View File

@ -0,0 +1,116 @@
/*
* Copyright (C) 2021 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.
*/
#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<float> const& ctx)
{
const samplecnt_t n_samples = ctx.samples_per_channel ();
const int n_channels = ctx.channels ();
if (!_enabled) {
ProcessContext<float> c_out (ctx);
ListedSource<float>::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<float> ctx_out (ctx, &_buf[n_channels * _latency], n_channels * ns);
ctx_out.remove_flag (ProcessContext<float>::EndOfInput);
this->output (ctx_out);
}
if (n_samples >= _latency) {
_latency = 0;
} else {
_latency -= n_samples;
}
} else {
ProcessContext<float> ctx_out (ctx, _buf);
ctx_out.remove_flag (ProcessContext<float>::EndOfInput);
this->output (ctx_out);
}
if (ctx.has_flag(ProcessContext<float>::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<float> ctx_out (ctx, _buf, ns * n_channels);
if (_latency == ns) {
ctx_out.set_flag (ProcessContext<float>::EndOfInput);
} else {
ctx_out.remove_flag (ProcessContext<float>::EndOfInput);
}
this->output (ctx_out);
_latency -= ns;
}
}
}
} // namespace

View File

@ -143,52 +143,66 @@ LoudnessReader::process (ProcessContext<float> const & ctx)
ListedSource<float>::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;
}

View File

@ -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<float> const & c)
{
@ -71,10 +61,11 @@ void Normalizer::process (ProcessContext<float> const & c)
if (enabled) {
memcpy (buffer, c.data(), c.samples() * sizeof(float));
Routines::apply_gain_to_buffer (buffer, c.samples(), gain);
ProcessContext<float> c_out (c, buffer);
ListedSource<float>::output (c_out);
} else {
ListedSource<float>::output(c);
}
ProcessContext<float> c_out (c, buffer);
ListedSource<float>::output (c_out);
}
/// Process a non-const ProcsesContext in-place \n RT safe

View File

@ -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'):