From d5b0bea6b00366094064e2e63bbaa7c7eee83c3d Mon Sep 17 00:00:00 2001 From: Geraint Date: Tue, 14 Jun 2022 14:18:10 +0100 Subject: [PATCH] Add limiter --- delay.h | 45 +++++++---- limiter.h | 211 +++++++++++++++++++++++++++++++++++++++++++++++++ stfx-library.h | 20 ++--- 3 files changed, 247 insertions(+), 29 deletions(-) create mode 100644 limiter.h diff --git a/delay.h b/delay.h index 0eeba6a..2bf30c0 100644 --- a/delay.h +++ b/delay.h @@ -114,6 +114,7 @@ namespace signalsmith { namespace basics { int maxDelaySamples = 0; signalsmith::delay::MultiBuffer multiBuffer; signalsmith::delay::Reader reader; + signalsmith::delay::Reader readerNearest; struct FilterAB { signalsmith::filters::BiquadStatic lowpassFrom, lowpassTo; @@ -152,7 +153,7 @@ namespace signalsmith { namespace basics { template 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(1, std::min(maxDelaySamples, initialDelayTo + lfoDelay)); Sample initialDelayFromLfo = std::max(1, std::min(maxDelaySamples, initialDelayFrom + lfoDelay)); Sample feedbackDelayToLfo = std::max(1, std::min(maxDelaySamples, feedbackDelayTo + lfoDelay*feedbackLfoMultiplier)); Sample feedbackDelayFromLfo = std::max(1, std::min(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)); diff --git a/limiter.h b/limiter.h new file mode 100644 index 0000000..031acc0 --- /dev/null +++ b/limiter.h @@ -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 + +namespace signalsmith { namespace basics { + + template + 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(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 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 + 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 peakHold{0}; + signalsmith::envelopes::BoxFilter 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 channelEnvelopes; + + Sample sampleRate; + std::vector channelGains; + template + 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 + 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; + +}} // namespace + +#endif // include guard diff --git a/stfx-library.h b/stfx-library.h index 348d480..9137e63 100644 --- a/stfx-library.h +++ b/stfx-library.h @@ -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 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)...); } - /// 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 void forEach(Callback callback) const { callback(0, length); @@ -233,18 +233,9 @@ namespace stfx { protected: using ParamRange = LibraryParam; using ParamSteps = LibraryParam; - - template - 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 LibraryEffect(Args &&...args) : EffectClass(std::forward(args)...) { + params.rangeParams.push_back(&this->bpm); EffectClass::state(params); }