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:
parent
8f5c3fcddb
commit
75829d20f2
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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"));
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
40
libs/audiographer/audiographer/general/limiter.h
Normal file
40
libs/audiographer/audiographer/general/limiter.h
Normal 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
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
451
libs/audiographer/private/limiter/limiter.cc
Normal file
451
libs/audiographer/private/limiter/limiter.cc
Normal 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;
|
||||
}
|
128
libs/audiographer/private/limiter/limiter.h
Normal file
128
libs/audiographer/private/limiter/limiter.h
Normal 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
|
116
libs/audiographer/src/general/limiter.cc
Normal file
116
libs/audiographer/src/general/limiter.cc
Normal 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
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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'):
|
||||
|
Loading…
Reference in New Issue
Block a user