Add limiter
This commit is contained in:
parent
3a8b805837
commit
d5b0bea6b0
35
delay.h
35
delay.h
@ -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
211
limiter.h
Normal 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
|
||||||
@ -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>;
|
||||||
|
public:
|
||||||
|
ParamRange bpm{120};
|
||||||
|
|
||||||
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:
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user