|
- package dsp
-
- import "math"
-
- // StereoLimiter applies identical gain reduction to L and R channels,
- // driven by the peak of max(|L|, |R|). This preserves the stereo image
- // while preventing either channel from exceeding the ceiling.
- //
- // Attack is INSTANTANEOUS — gain is reduced in the same sample that
- // exceeds the ceiling. This avoids overshoot entirely, which is critical
- // because overshoot causes composite clipping that destroys pilot/RDS.
- // Unlike hard clipping, gain scaling preserves the waveform shape and
- // does not create harmonics.
- //
- // Release is smooth (exponential decay) to avoid audible pumping.
- type StereoLimiter struct {
- ceiling float64
- releaseCoeff float64
- gainReduction float64
- }
-
- // NewStereoLimiter creates a stereo-linked limiter with instant attack.
- // releaseMs controls how quickly gain recovers after a peak (typ. 50-200ms).
- func NewStereoLimiter(ceiling, attackMs, releaseMs, sampleRate float64) *StereoLimiter {
- if ceiling <= 0 {
- ceiling = 1.0
- }
- if releaseMs <= 0 {
- releaseMs = 100
- }
- releaseSamples := releaseMs * sampleRate / 1000
-
- return &StereoLimiter{
- ceiling: ceiling,
- releaseCoeff: 1.0 - math.Exp(-1.0/releaseSamples),
- }
- }
-
- // Process applies stereo-linked limiting. Both channels receive the
- // same gain factor, determined by the louder of the two.
- //
- // If the peak exceeds ceiling, gain is INSTANTLY reduced (zero overshoot).
- // When the signal drops below ceiling, gain recovers smoothly via release.
- func (l *StereoLimiter) Process(left, right float64) (float64, float64) {
- peak := math.Max(math.Abs(left), math.Abs(right))
-
- // Target: how much gain reduction do we need right now?
- targetReduction := 0.0
- if peak > l.ceiling {
- targetReduction = 1.0 - l.ceiling/peak
- }
-
- // Instant attack: if we need MORE reduction, apply it NOW.
- // Smooth release: if we need LESS reduction, decay slowly.
- if targetReduction > l.gainReduction {
- l.gainReduction = targetReduction // instant
- } else {
- l.gainReduction += l.releaseCoeff * (targetReduction - l.gainReduction) // smooth
- }
-
- gain := 1.0 - l.gainReduction
- return left * gain, right * gain
- }
-
- // Reset clears the limiter state.
- func (l *StereoLimiter) Reset() {
- l.gainReduction = 0
- }
|