1
0

Add limiter

This commit is contained in:
Geraint 2022-06-14 14:18:10 +01:00
parent 3a8b805837
commit d5b0bea6b0
3 changed files with 247 additions and 29 deletions

45
delay.h
View File

@ -114,6 +114,7 @@ namespace signalsmith { namespace basics {
int maxDelaySamples = 0; int maxDelaySamples = 0;
signalsmith::delay::MultiBuffer<Sample> multiBuffer; signalsmith::delay::MultiBuffer<Sample> multiBuffer;
signalsmith::delay::Reader<Sample, signalsmith::delay::InterpolatorLagrange7> reader; signalsmith::delay::Reader<Sample, signalsmith::delay::InterpolatorLagrange7> reader;
signalsmith::delay::Reader<Sample, signalsmith::delay::InterpolatorNearest> readerNearest;
struct FilterAB { struct FilterAB {
signalsmith::filters::BiquadStatic<Sample> lowpassFrom, lowpassTo; signalsmith::filters::BiquadStatic<Sample> lowpassFrom, lowpassTo;
@ -152,7 +153,7 @@ namespace signalsmith { namespace basics {
template<class Storage> template<class Storage>
void state(Storage &storage) { void state(Storage &storage) {
storage.info("Basics: Delay", "A delay with feedback, filters and modulation"); storage.info("[Basics] Delay", "A delay with feedback, filters and modulation");
int version = storage.version(2); int version = storage.version(2);
if (version < 2) return; if (version < 2) return;
@ -203,12 +204,12 @@ namespace signalsmith { namespace basics {
Sample stepSamplesTo = 60*config.sampleRate/this->bpm.to()*stepFactors[beatSteps.to()]; Sample stepSamplesTo = 60*config.sampleRate/this->bpm.to()*stepFactors[beatSteps.to()];
auto samplesDelay = [&](double ms, double steps, double stepSamples) { auto samplesDelay = [&](double ms, double steps, double stepSamples) {
return maxDelaySamples, ms*0.001*config.sampleRate + steps*stepSamples; return int(ms*0.001*config.sampleRate + steps*stepSamples);
}; };
Sample initialDelayFrom = samplesDelay(initial.ms.from(), initial.steps.from(), stepSamplesFrom); int initialDelayFrom = samplesDelay(initial.ms.from(), initial.steps.from(), stepSamplesFrom);
Sample initialDelayTo = samplesDelay(initial.ms.to(), initial.steps.to(), stepSamplesTo); int initialDelayTo = samplesDelay(initial.ms.to(), initial.steps.to(), stepSamplesTo);
Sample feedbackDelayFrom = samplesDelay(feedback.ms.from(), feedback.steps.from(), stepSamplesFrom); int feedbackDelayFrom = samplesDelay(feedback.ms.from(), feedback.steps.from(), stepSamplesFrom);
Sample feedbackDelayTo = samplesDelay(feedback.ms.to(), feedback.steps.to(), stepSamplesTo); int feedbackDelayTo = samplesDelay(feedback.ms.to(), feedback.steps.to(), stepSamplesTo);
bool delayChanging = (initialDelayFrom != initialDelayTo) || (feedbackDelayFrom != feedbackDelayTo); bool delayChanging = (initialDelayFrom != initialDelayTo) || (feedbackDelayFrom != feedbackDelayTo);
bool filtersChanging = block.fading(initial.highpass, initial.lowpass, feedback.highpass, feedback.lowpass); bool filtersChanging = block.fading(initial.highpass, initial.lowpass, feedback.highpass, feedback.lowpass);
@ -229,22 +230,36 @@ namespace signalsmith { namespace basics {
auto smoothFeedbackGain = block.smooth(feedback.gain); auto smoothFeedbackGain = block.smooth(feedback.gain);
for (int i = 0; i < block.length; ++i) { for (int i = 0; i < block.length; ++i) {
for (int c = 0; c < channels; ++c) { for (int c = 0; c < channels; ++c) {
Sample value = io.input[c][i];
multiBuffer[c][0] = value;
Sample lfoDelay = channelLfos[c].next(); Sample lfoDelay = channelLfos[c].next();
Sample initialDelayToLfo = std::max<Sample>(1, std::min<Sample>(maxDelaySamples, initialDelayTo + lfoDelay)); Sample initialDelayToLfo = std::max<Sample>(1, std::min<Sample>(maxDelaySamples, initialDelayTo + lfoDelay));
Sample initialDelayFromLfo = std::max<Sample>(1, std::min<Sample>(maxDelaySamples, initialDelayFrom + lfoDelay)); Sample initialDelayFromLfo = std::max<Sample>(1, std::min<Sample>(maxDelaySamples, initialDelayFrom + lfoDelay));
Sample feedbackDelayToLfo = std::max<Sample>(1, std::min<Sample>(maxDelaySamples, feedbackDelayTo + lfoDelay*feedbackLfoMultiplier)); Sample feedbackDelayToLfo = std::max<Sample>(1, std::min<Sample>(maxDelaySamples, feedbackDelayTo + lfoDelay*feedbackLfoMultiplier));
Sample feedbackDelayFromLfo = std::max<Sample>(1, std::min<Sample>(maxDelaySamples, feedbackDelayFrom + lfoDelay*feedbackLfoMultiplier)); Sample feedbackDelayFromLfo = std::max<Sample>(1, std::min<Sample>(maxDelaySamples, feedbackDelayFrom + lfoDelay*feedbackLfoMultiplier));
Sample value = io.input[c][i]; Sample initialDelayed, feedbackDelayed;
multiBuffer[c][0] = value; if (lfoDepthSamples > 0) {
Sample initialDelayed = reader.read(multiBuffer[c], initialDelayToLfo); initialDelayed = reader.read(multiBuffer[c], initialDelayToLfo);
Sample feedbackDelayed = reader.read(multiBuffer[c], feedbackDelayToLfo); feedbackDelayed = reader.read(multiBuffer[c], feedbackDelayToLfo);
if (delayChanging) { if (delayChanging) {
Sample initialDelayedFrom = reader.read(multiBuffer[c], initialDelayFromLfo); Sample initialDelayedFrom = reader.read(multiBuffer[c], initialDelayFromLfo);
initialDelayed = block.fade(i, initialDelayedFrom, initialDelayed); initialDelayed = block.fade(i, initialDelayedFrom, initialDelayed);
Sample feedbackDelayedFrom = reader.read(multiBuffer[c], feedbackDelayFromLfo); Sample feedbackDelayedFrom = reader.read(multiBuffer[c], feedbackDelayFromLfo);
feedbackDelayed = block.fade(i, feedbackDelayedFrom, feedbackDelayed); feedbackDelayed = block.fade(i, feedbackDelayedFrom, feedbackDelayed);
}
} else { // use cheaper delay-reader
initialDelayed = readerNearest.read(multiBuffer[c], initialDelayToLfo);
feedbackDelayed = readerNearest.read(multiBuffer[c], feedbackDelayToLfo);
if (delayChanging) {
Sample initialDelayedFrom = readerNearest.read(multiBuffer[c], initialDelayFromLfo);
initialDelayed = block.fade(i, initialDelayedFrom, initialDelayed);
Sample feedbackDelayedFrom = readerNearest.read(multiBuffer[c], feedbackDelayFromLfo);
feedbackDelayed = block.fade(i, feedbackDelayedFrom, feedbackDelayed);
}
} }
if (filtersChanging) { if (filtersChanging) {
initialDelayed = initialFilters[c].process(initialDelayed, block.fade(i)); initialDelayed = initialFilters[c].process(initialDelayed, block.fade(i));
feedbackDelayed = feedbackFilters[c].process(feedbackDelayed, block.fade(i)); feedbackDelayed = feedbackFilters[c].process(feedbackDelayed, block.fade(i));

211
limiter.h Normal file
View File

@ -0,0 +1,211 @@
/* Copyright 2022 Signalsmith Audio Ltd. / Geraint Luff
Released under the Boost Software License (see LICENSE.txt) */
#ifndef SIGNALSMITH_BASICS_LIMITER_H
#define SIGNALSMITH_BASICS_LIMITER_H
#include "./dsp/delay.h"
#include "./dsp/envelopes.h"
SIGNALSMITH_DSP_VERSION_CHECK(1, 1, 0)
#include "./stfx-library.h"
#include <cmath>
namespace signalsmith { namespace basics {
template<typename Sample, class BaseEffect>
class LimiterSTFX : public BaseEffect {
using typename BaseEffect::ParamRange;
using typename BaseEffect::ParamSteps;
// Unit conversions (for display only)
static double db_gain(double db) {
return std::pow(10, db*0.05);
}
static double gain_db(double gain) {
return std::log10(std::max<double>(gain, 1e-10))*20;
}
static double pc_linear(double percent) {
return percent*0.01;
}
static double linear_pc(double linear) {
return linear*100;
}
int channels = 0;
double maxDelayMs = 0;
signalsmith::delay::MultiBuffer<Sample> multiBuffer;
public:
ParamRange inputGain{1};
ParamRange outputLimit{db_gain(-12)};
ParamRange attackMs{20}, holdMs{0}, releaseMs{0};
ParamSteps smoothingStages{1};
ParamRange linkChannels{0.5};
LimiterSTFX(double maxDelayMs=100) : maxDelayMs(maxDelayMs) {}
template<class Storage>
void state(Storage &storage) {
storage.info("[Basics] Limiter", "A simple lookahead limiter");
int version = storage.version(4);
if (version != 4) return;
storage.param("inputGain", inputGain)
.info("pre-gain", "amplifies the input before limiting")
.range(db_gain(-12), 1, db_gain(24))
.unit("dB", 1, db_gain, gain_db)
.unit("");
storage.param("outputLimit", outputLimit)
.info("limit", "maximum output amplitude")
.range(db_gain(-24), db_gain(-12), 1)
.unit("dB", 1, db_gain, gain_db)
// Extra resolution between -1dB and 0dB
.unit("dB", 2, db_gain, gain_db, db_gain(-1), 1)
.unit("");
storage.param("attackMs", attackMs)
.info("attack", "envelope smoothing time")
.range(1, 10, maxDelayMs/2)
.unit("ms", 0);
storage.param("holdMs", holdMs)
.info("hold", "hold constant after peaks")
.range(0, 10, maxDelayMs/2)
.unit("ms", 0);
storage.param("releaseMs", releaseMs)
.info("release", "extra release time (in addition to attack + hold)")
.range(0, 10, 250)
.unit("ms", 0);
storage.param("smoothingStages", smoothingStages)
.info("smoothing", "smoothing filter(s) used for attack-smoothing")
.names(1, "rect", "double");
storage.param("linkChannels", linkChannels)
.info("link", "link channel gains together")
.range(0, 0.5, 1)
.unit("%", 0, pc_linear, linear_pc);
}
// Gain envelopes are calculated per-channel
struct ChannelEnvelope {
signalsmith::envelopes::PeakHold<Sample> peakHold{0};
signalsmith::envelopes::BoxFilter<Sample> smoother1{0}, smoother2{0};
Sample released = 1;
Sample releaseSlew = 1;
void reset(Sample value=1) {
peakHold.reset(-value);
smoother1.reset(value);
smoother2.reset(value);
released = value;
}
void configure(int maxDelaySamples) {
peakHold.resize(maxDelaySamples);
smoother1.resize(maxDelaySamples);
smoother2.resize(maxDelaySamples/2 + 1);
}
Sample operator ()(Sample maxGain) {
// Moving minimum
Sample gain = -peakHold(-maxGain);
// Exponential release curve
released += (gain - released)*releaseSlew;
released = std::min(gain, released);
// Smoothing (attack)
return smoother1(smoother2(released));
}
};
std::vector<ChannelEnvelope> channelEnvelopes;
Sample sampleRate;
std::vector<Sample> channelGains;
template<class Config>
void configure(Config &config) {
channels = config.outputChannels = config.inputChannels;
config.auxInputs.resize(0);
config.auxOutputs.resize(0);
sampleRate = config.sampleRate;
int maxDelaySamples = std::ceil(maxDelayMs*0.001*sampleRate);
multiBuffer.resize(channels, maxDelaySamples + 1);
channelEnvelopes.resize(channels);
for (auto &e : channelEnvelopes) e.configure(maxDelaySamples);
channelGains.resize(channels);
}
void reset() {
multiBuffer.reset();
for (auto &e : channelEnvelopes) e.reset();
}
int latencySamples() const {
int attackSamples = std::ceil(attackMs*0.001*sampleRate);
return attackSamples;
}
template <class Io, class Config, class Block>
void process(Io &io, Config &, Block &block) {
Sample thresholdAmp = outputLimit;
auto smoothedPreGain = block.smooth(inputGain);
// If we change the attack, we want to fade between the two delay times
int delaySamplesFrom = std::ceil(attackMs.from()*0.001*sampleRate);
int delaySamplesTo = std::ceil(attackMs.to()*0.001*sampleRate);
int attackSamples = delaySamplesTo;
int holdSamples = std::ceil(holdMs*0.001*sampleRate);
Sample releaseSamples = releaseMs*0.001*sampleRate;
int stages = smoothingStages.to();
for (auto &envelope : channelEnvelopes) {
envelope.peakHold.set(attackSamples + holdSamples);
if (stages == 2) {
// Split into two (non-equal) box filters
int split = std::round(attackSamples*0.5822419);
envelope.smoother1.set(split + 1);
envelope.smoother2.set(attackSamples - split + 1);
} else {
envelope.smoother1.set(attackSamples + 1);
envelope.smoother2.set(1);
}
// Reasonable approximation for release curve: https://www.desmos.com/calculator/wbxakdgw1o
constexpr Sample ln2{0.69314718056};
envelope.releaseSlew = ln2/(releaseSamples + ln2);
}
for (int i = 0; i < block.length; ++i) {
Sample minChannelGain = 1;
for (int c = 0; c < channels; ++c) {
Sample value = io.input[c][i]*smoothedPreGain.at(i);
multiBuffer[c][i] = value;
// maximum gain (clips output to threshold)
Sample gain = thresholdAmp/std::max(thresholdAmp, std::abs(value));
channelGains[c] = gain;
minChannelGain = std::min(minChannelGain, gain);
}
for (int c = 0; c < channels; ++c) {
Sample gain = channelGains[c];
// blend between individual/minimum gain
gain += (minChannelGain - gain)*linkChannels;
// smooth envelope gain
auto &envelope = channelEnvelopes[c];
gain = envelope(gain);
Sample delayed = block.fade(i,
multiBuffer[c][i - delaySamplesFrom],
multiBuffer[c][i - delaySamplesTo]
);
io.output[c][i] = delayed*gain;
}
}
multiBuffer += block.length;
}
};
using Limiter = stfx::LibraryEffect<float, LimiterSTFX>;
}} // namespace
#endif // include guard

View File

@ -135,7 +135,7 @@ namespace stfx {
} }
/// Blocks can be processed in sub-blocks, which are split up by events. /// Blocks can be processed in sub-blocks, which are split up by events.
/// This method can return a sub-block (with `.split()` and `.forEach()` methods). /// This method may return a different sub-block type (which will also have `.split()` and `.forEach()` methods).
template<class EventList> template<class EventList>
const Block & split(EventList &&list, int count) const { const Block & split(EventList &&list, int count) const {
for (int i = 0; i < count; ++i) list[i](); for (int i = 0; i < count; ++i) list[i]();
@ -145,11 +145,11 @@ namespace stfx {
const Block & split(EventList &&list, Others &&...others) const { const Block & split(EventList &&list, Others &&...others) const {
return split(list).split(std::forward<Others>(others)...); return split(list).split(std::forward<Others>(others)...);
} }
/// Base-case for the templated recursion /// Base-case for templated recursion
const Block & split() const { const Block & split() const {
return *this; return *this;
} }
/// Execute the callback once per sub-block, with sample-index arguments: `callback(int start, int end)` /// Execute the callback once per sub-block, with sample-index arguments: `callback(int start, int end)`, calling events in between
template<class Callback> template<class Callback>
void forEach(Callback callback) const { void forEach(Callback callback) const {
callback(0, length); callback(0, length);
@ -233,18 +233,9 @@ namespace stfx {
protected: protected:
using ParamRange = LibraryParam<double>; using ParamRange = LibraryParam<double>;
using ParamSteps = LibraryParam<int>; using ParamSteps = LibraryParam<int>;
template<typename Enum, int size>
class ParamEnum : public ParamSteps {
static Enum castToEnum(int v) {return (Enum)v;};
public:
using Value = Enum;
ParamEnum(Enum value) : ParamSteps((int)value) {}
operator Enum() {
return (Enum)this->latest;
}
};
public: public:
ParamRange bpm{120};
double paramFadeMs() { double paramFadeMs() {
return 20; return 20;
} }
@ -311,6 +302,7 @@ namespace stfx {
public: public:
template<class ...Args> template<class ...Args>
LibraryEffect(Args &&...args) : EffectClass(std::forward<Args>(args)...) { LibraryEffect(Args &&...args) : EffectClass(std::forward<Args>(args)...) {
params.rangeParams.push_back(&this->bpm);
EffectClass::state(params); EffectClass::state(params);
} }