Add limiter
This commit is contained in:
parent
3a8b805837
commit
d5b0bea6b0
45
delay.h
45
delay.h
@ -114,6 +114,7 @@ namespace signalsmith { namespace basics {
|
||||
int maxDelaySamples = 0;
|
||||
signalsmith::delay::MultiBuffer<Sample> multiBuffer;
|
||||
signalsmith::delay::Reader<Sample, signalsmith::delay::InterpolatorLagrange7> reader;
|
||||
signalsmith::delay::Reader<Sample, signalsmith::delay::InterpolatorNearest> readerNearest;
|
||||
|
||||
struct FilterAB {
|
||||
signalsmith::filters::BiquadStatic<Sample> lowpassFrom, lowpassTo;
|
||||
@ -152,7 +153,7 @@ namespace signalsmith { namespace basics {
|
||||
|
||||
template<class 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);
|
||||
if (version < 2) return;
|
||||
|
||||
@ -203,12 +204,12 @@ namespace signalsmith { namespace basics {
|
||||
Sample stepSamplesTo = 60*config.sampleRate/this->bpm.to()*stepFactors[beatSteps.to()];
|
||||
|
||||
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);
|
||||
Sample initialDelayTo = samplesDelay(initial.ms.to(), initial.steps.to(), stepSamplesTo);
|
||||
Sample feedbackDelayFrom = samplesDelay(feedback.ms.from(), feedback.steps.from(), stepSamplesFrom);
|
||||
Sample feedbackDelayTo = samplesDelay(feedback.ms.to(), feedback.steps.to(), stepSamplesTo);
|
||||
int initialDelayFrom = samplesDelay(initial.ms.from(), initial.steps.from(), stepSamplesFrom);
|
||||
int initialDelayTo = samplesDelay(initial.ms.to(), initial.steps.to(), stepSamplesTo);
|
||||
int feedbackDelayFrom = samplesDelay(feedback.ms.from(), feedback.steps.from(), stepSamplesFrom);
|
||||
int feedbackDelayTo = samplesDelay(feedback.ms.to(), feedback.steps.to(), stepSamplesTo);
|
||||
bool delayChanging = (initialDelayFrom != initialDelayTo) || (feedbackDelayFrom != feedbackDelayTo);
|
||||
|
||||
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);
|
||||
for (int i = 0; i < block.length; ++i) {
|
||||
for (int c = 0; c < channels; ++c) {
|
||||
Sample value = io.input[c][i];
|
||||
multiBuffer[c][0] = value;
|
||||
|
||||
Sample lfoDelay = channelLfos[c].next();
|
||||
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 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 value = io.input[c][i];
|
||||
multiBuffer[c][0] = value;
|
||||
Sample initialDelayed = reader.read(multiBuffer[c], initialDelayToLfo);
|
||||
Sample feedbackDelayed = reader.read(multiBuffer[c], feedbackDelayToLfo);
|
||||
if (delayChanging) {
|
||||
Sample initialDelayedFrom = reader.read(multiBuffer[c], initialDelayFromLfo);
|
||||
initialDelayed = block.fade(i, initialDelayedFrom, initialDelayed);
|
||||
Sample feedbackDelayedFrom = reader.read(multiBuffer[c], feedbackDelayFromLfo);
|
||||
feedbackDelayed = block.fade(i, feedbackDelayedFrom, feedbackDelayed);
|
||||
Sample initialDelayed, feedbackDelayed;
|
||||
if (lfoDepthSamples > 0) {
|
||||
initialDelayed = reader.read(multiBuffer[c], initialDelayToLfo);
|
||||
feedbackDelayed = reader.read(multiBuffer[c], feedbackDelayToLfo);
|
||||
if (delayChanging) {
|
||||
Sample initialDelayedFrom = reader.read(multiBuffer[c], initialDelayFromLfo);
|
||||
initialDelayed = block.fade(i, initialDelayedFrom, initialDelayed);
|
||||
Sample feedbackDelayedFrom = reader.read(multiBuffer[c], feedbackDelayFromLfo);
|
||||
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) {
|
||||
initialDelayed = initialFilters[c].process(initialDelayed, 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.
|
||||
/// 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>
|
||||
const Block & split(EventList &&list, int count) const {
|
||||
for (int i = 0; i < count; ++i) list[i]();
|
||||
@ -145,11 +145,11 @@ namespace stfx {
|
||||
const Block & split(EventList &&list, Others &&...others) const {
|
||||
return split(list).split(std::forward<Others>(others)...);
|
||||
}
|
||||
/// Base-case for the templated recursion
|
||||
/// Base-case for templated recursion
|
||||
const Block & split() const {
|
||||
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>
|
||||
void forEach(Callback callback) const {
|
||||
callback(0, length);
|
||||
@ -233,18 +233,9 @@ namespace stfx {
|
||||
protected:
|
||||
using ParamRange = LibraryParam<double>;
|
||||
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:
|
||||
ParamRange bpm{120};
|
||||
|
||||
double paramFadeMs() {
|
||||
return 20;
|
||||
}
|
||||
@ -311,6 +302,7 @@ namespace stfx {
|
||||
public:
|
||||
template<class ...Args>
|
||||
LibraryEffect(Args &&...args) : EffectClass(std::forward<Args>(args)...) {
|
||||
params.rangeParams.push_back(&this->bpm);
|
||||
EffectClass::state(params);
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user