1
0
basics/limiter.h
2025-07-06 12:53:41 +01:00

204 lines
6.3 KiB
C++

/* Copyright 2022 Signalsmith Audio Ltd. / Geraint Luff
Released under the Boost Software License (see LICENSE.txt) */
#pragma once
#include "dsp/delay.h"
#include "dsp/envelopes.h"
SIGNALSMITH_DSP_VERSION_CHECK(1, 1, 0)
#include "stfx/stfx-library.h"
#include <cmath>
namespace signalsmith { namespace basics {
template<class BaseEffect>
class LimiterSTFX;
using LimiterFloat = stfx::LibraryEffect<float, LimiterSTFX>;
using LimiterDouble = stfx::LibraryEffect<double, LimiterSTFX>;
template<class BaseEffect>
struct LimiterSTFX : public BaseEffect {
using typename BaseEffect::Sample;
using typename BaseEffect::ParamRange;
using typename BaseEffect::ParamStepped;
ParamRange inputGain{1};
ParamRange outputLimit{stfx::units::dbToGain(-3)};
ParamRange attackMs{20}, holdMs{0}, releaseMs{0};
ParamStepped smoothingStages{1};
ParamRange linkChannels{0.5};
LimiterSTFX(double maxDelayMs=100) : maxDelayMs(maxDelayMs) {}
template<class Storage>
void state(Storage &storage) {
storage.info("Limiter", "A simple lookahead limiter");
int version = storage.version(4);
if (version != 4) return;
using stfx::units::dbToGain;
stfx::units::rangeGain(storage.range("inputGain", inputGain)
.info("pre-gain", "amplifies the input before limiting")
.range(dbToGain(-12), 1, dbToGain(24)));
stfx::units::rangeGain(storage.range("outputLimit", outputLimit)
.info("limit", "maximum output amplitude")
.range(dbToGain(-24), dbToGain(-12), 1));
stfx::units::rangeMs(storage.range("attackMs", attackMs)
.info("attack", "envelope smoothing time")
.range(1, 10, maxDelayMs/2));
stfx::units::rangeMs(storage.range("holdMs", holdMs)
.info("hold", "hold constant after peaks")
.range(0, 10, maxDelayMs/2));
stfx::units::rangeMs(storage.range("releaseMs", releaseMs)
.info("release", "extra release time (in addition to attack + hold)")
.range(0, 10, 250));
storage.stepped("smoothingStages", smoothingStages)
.info("smoothing", "smoothing filter(s) used for attack-smoothing")
.label(1, "rect", "double");
stfx::units::rangePercent(storage.range("linkChannels", linkChannels)
.info("link", "link channel gains together")
.range(0, 0.5, 1));
}
// 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 configureSTFX(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();
sqrtWet = (smoothingStages > 1);
}
int latencySamples() const {
int attackSamples = std::ceil(attackMs*0.001*sampleRate);
return attackSamples;
}
template <class Io, class Config, class Block>
void processSTFX(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*smoothingStages;
int stages = smoothingStages;
sqrtWetSlew = 1/(Sample(attackSamples)*2 + 1);
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;
if (stages == 2) {
gain *= gain;
sqrtWet += (1 - sqrtWet)*sqrtWetSlew;
} else {
sqrtWet *= 1 - sqrtWetSlew;
}
// smooth envelope gain
auto &envelope = channelEnvelopes[c];
gain = envelope(gain);
if (sqrtWet > Sample(1e-30)) {
gain += (std::sqrt(gain) - gain)*sqrtWet;
gain = std::min(gain, channelGains[c]);
}
Sample delayed = block.fade(i,
multiBuffer[c][i - delaySamplesFrom],
multiBuffer[c][i - delaySamplesTo]
);
io.output[c][i] = delayed*gain;
}
}
multiBuffer += block.length;
}
private:
int channels = 0;
double maxDelayMs = 0;
Sample sqrtWet = 0, sqrtWetSlew = 1;
signalsmith::delay::MultiBuffer<Sample> multiBuffer;
};
}} // namespace