1
0
basics/limiter.h
2022-12-05 21:06:19 +00:00

212 lines
6.5 KiB
C++

/* 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(-3)};
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 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;
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