#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(delayDataLeft, lookaheadSamples); delayBufferRight = new CircularBuffer(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; } }