diff --git a/temp_cortex/fast_limiter.cpp b/temp_cortex/fast_limiter.cpp new file mode 100644 index 0000000..2e41dc7 --- /dev/null +++ b/temp_cortex/fast_limiter.cpp @@ -0,0 +1,137 @@ +#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; + } +} \ No newline at end of file diff --git a/temp_cortex/fast_limiter.hpp b/temp_cortex/fast_limiter.hpp new file mode 100644 index 0000000..9e430b9 --- /dev/null +++ b/temp_cortex/fast_limiter.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include "limiter_base.hpp" +#include "CircularBuffer.h" + +class FastLimiter : public LimiterBase +{ + protected: + // Lookahead buffer for clean limiting + CircularBuffer* delayBufferLeft; + CircularBuffer* delayBufferRight; + + // Lookahead parameters + float lookaheadMs; + uint8_t lookaheadSamples; + + // Envelope following + float envelope; + float peakHold; + uint8_t peakHoldCounter; + uint8_t peakHoldSamples; + + // Internal gain smoothing + float currentGain; + + public: + FastLimiter(float sampleRate, float lookaheadMs = 5.0f); + ~FastLimiter(); + + void setPeakHold(float peakHoldMs); + void process(float input[2], float thresholdDb = -3.0f, float ceilingDb = -0.1f) override; + + private: + float analyzeLevel(float left, float right); +}; \ No newline at end of file diff --git a/temp_cortex/limiter_base.cpp b/temp_cortex/limiter_base.cpp new file mode 100644 index 0000000..816368b --- /dev/null +++ b/temp_cortex/limiter_base.cpp @@ -0,0 +1,15 @@ +#include "limiter_base.hpp" +#include "basicmaths.h" + +LimiterBase::LimiterBase(float sampleRate) : sampleRate(sampleRate), gainReduction(1.0f), makeupGain(1.0f) +{ + setTiming(0.01f, 12.0f); +} + +void LimiterBase::setTiming(float attackMs, float releaseMs) +{ + // Convert milliseconds to coefficient for exponential decay + // Formula: coeff = exp(-1 / (time_in_samples)) + attackCoeff = expf(-1.0f / (attackMs * 0.001f * sampleRate + 1e-6f)); + releaseCoeff = expf(-1.0f / (releaseMs * 0.001f * sampleRate + 1e-6f)); +} diff --git a/temp_cortex/limiter_base.hpp b/temp_cortex/limiter_base.hpp new file mode 100644 index 0000000..8f1931c --- /dev/null +++ b/temp_cortex/limiter_base.hpp @@ -0,0 +1,17 @@ +#pragma once + +class LimiterBase +{ + protected: + float sampleRate; + float gainReduction; + float attackCoeff; + float releaseCoeff; + float makeupGain; + + public: + LimiterBase(float sampleRate); + virtual ~LimiterBase() = default; + virtual void setTiming(float attackMs, float releaseMs); + virtual void process(float input[2], float thresholdDb = -3.0f, float ceilingDb = -0.1f) = 0; +}; \ No newline at end of file