137 lines
4.6 KiB
C++
137 lines
4.6 KiB
C++
#include "fast_limiter.hpp"
|
|
#include "basicmaths.h"
|
|
|
|
FastLimiter::FastLimiter(float sampleRate, float lookaheadMs)
|
|
: LimiterBase(sampleRate),
|
|
lookaheadMs(lookaheadMs),
|
|
envelope(0.0f),
|
|
peakHold(0.0f),
|
|
peakHoldCounter(0),
|
|
currentGain(1.0f)
|
|
{
|
|
lookaheadSamples = (uint8_t)(lookaheadMs * 0.001f * sampleRate);
|
|
if (lookaheadSamples < 2) lookaheadSamples = 2; // Minimum 2 samples
|
|
|
|
// Allocate delay line buffers
|
|
float* delayDataLeft = new float[lookaheadSamples];
|
|
float* delayDataRight = new float[lookaheadSamples];
|
|
|
|
// Initialize buffers with zeros
|
|
for (uint8_t i = 0; i < lookaheadSamples; i++) {
|
|
delayDataLeft[i] = 0.0f;
|
|
delayDataRight[i] = 0.0f;
|
|
}
|
|
|
|
// Set up circular buffers
|
|
delayBufferLeft = new CircularBuffer<float, uint8_t>(delayDataLeft, lookaheadSamples);
|
|
delayBufferRight = new CircularBuffer<float, uint8_t>(delayDataRight, lookaheadSamples);
|
|
|
|
// 1ms peak hold by default
|
|
setPeakHold(1.0f);
|
|
}
|
|
|
|
FastLimiter::~FastLimiter()
|
|
{
|
|
if (delayBufferLeft) {
|
|
delete[] delayBufferLeft->getData(); // Delete the underlying data first
|
|
delete delayBufferLeft;
|
|
delayBufferLeft = nullptr;
|
|
}
|
|
if (delayBufferRight) {
|
|
delete[] delayBufferRight->getData(); // Delete the underlying data first
|
|
delete delayBufferRight;
|
|
delayBufferRight = nullptr;
|
|
}
|
|
}
|
|
|
|
void FastLimiter::setPeakHold(float peakHoldMs)
|
|
{
|
|
peakHoldSamples = (uint8_t)(peakHoldMs * 0.001f * sampleRate);
|
|
if (peakHoldSamples > 255) peakHoldSamples = 255; // Cap at uint8_t max
|
|
}
|
|
|
|
float FastLimiter::analyzeLevel(float left, float right)
|
|
{
|
|
// Get peak level from both channels
|
|
float leftAbs = (left < 0) ? -left : left;
|
|
float rightAbs = (right < 0) ? -right : right;
|
|
float peakLevel = (leftAbs > rightAbs) ? leftAbs : rightAbs;
|
|
|
|
// Peak hold logic - maintain peaks for a short duration
|
|
// This helps catch transients that might be missed
|
|
if (peakLevel > peakHold) {
|
|
peakHold = peakLevel;
|
|
peakHoldCounter = peakHoldSamples;
|
|
} else if (peakHoldCounter > 0) {
|
|
peakHoldCounter--;
|
|
peakLevel = peakHold; // Use held peak
|
|
} else {
|
|
peakHold = peakLevel;
|
|
}
|
|
|
|
return peakLevel;
|
|
}
|
|
|
|
void FastLimiter::process(float input[2], float thresholdDb, float ceilingDb)
|
|
{
|
|
// Step 1: Write input to delay buffers
|
|
delayBufferLeft->write(input[0]);
|
|
delayBufferRight->write(input[1]);
|
|
|
|
// Step 2: Analyze the current input level (lookahead analysis)
|
|
float currentLevel = analyzeLevel(input[0], input[1]);
|
|
|
|
// Step 3: Envelope following for level detection
|
|
if (currentLevel > envelope) {
|
|
// Fast attack for rising levels
|
|
envelope = currentLevel + attackCoeff * (envelope - currentLevel);
|
|
} else {
|
|
// Slower release for falling levels
|
|
envelope = currentLevel + releaseCoeff * (envelope - currentLevel);
|
|
}
|
|
|
|
// Step 4: Convert dB values to linear
|
|
float thresholdLinear = powf(10.0f, thresholdDb / 20.0f);
|
|
float ceilingLinear = powf(10.0f, ceilingDb / 20.0f);
|
|
|
|
// Step 5: Calculate required gain reduction
|
|
float targetGain = 1.0f;
|
|
if (envelope > thresholdLinear) {
|
|
// Calculate how much we need to reduce gain
|
|
float overAmount = envelope / thresholdLinear;
|
|
targetGain = 1.0f / overAmount;
|
|
|
|
// Ensure we don't exceed ceiling
|
|
float potentialOutput = envelope * targetGain;
|
|
if (potentialOutput > ceilingLinear) {
|
|
targetGain = ceilingLinear / envelope;
|
|
}
|
|
}
|
|
|
|
// Step 6: Smooth gain changes (this is critical for clean limiting)
|
|
if (targetGain < currentGain) {
|
|
// Fast attack when reducing gain (limiting kicks in)
|
|
currentGain = targetGain + attackCoeff * (currentGain - targetGain);
|
|
} else {
|
|
// Slower release when increasing gain (limiter backing off)
|
|
currentGain = targetGain + releaseCoeff * (currentGain - targetGain);
|
|
}
|
|
|
|
// Step 7: Calculate makeup gain
|
|
makeupGain = ceilingLinear / thresholdLinear;
|
|
|
|
// Step 8: Read delayed samples and apply processing
|
|
if (!delayBufferLeft->isEmpty() && !delayBufferRight->isEmpty()) {
|
|
float delayedLeft = delayBufferLeft->read();
|
|
float delayedRight = delayBufferRight->read();
|
|
|
|
// Apply gain reduction and makeup gain to delayed signal
|
|
float finalGain = currentGain * makeupGain;
|
|
input[0] = delayedLeft * finalGain;
|
|
input[1] = delayedRight * finalGain;
|
|
} else {
|
|
// Buffers not full yet, output silence to avoid pops
|
|
input[0] = 0.0f;
|
|
input[1] = 0.0f;
|
|
}
|
|
} |