temp-cortex limiter dump

This commit is contained in:
2026-08-01 15:09:30 -04:00
parent ae9019465e
commit 3ad30942a7
4 changed files with 204 additions and 0 deletions
+137
View File
@@ -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<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;
}
}
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include "limiter_base.hpp"
#include "CircularBuffer.h"
class FastLimiter : public LimiterBase
{
protected:
// Lookahead buffer for clean limiting
CircularBuffer<float, uint8_t>* delayBufferLeft;
CircularBuffer<float, uint8_t>* 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);
};
+15
View File
@@ -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));
}
+17
View File
@@ -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;
};