package dsp import "math" // MPXLimiter is a look-ahead peak limiter for the composite MPX signal. // It prevents overmodulation by keeping the composite output within // the configured ceiling. The limiter applies gain reduction smoothly // to avoid introducing audible artifacts. type MPXLimiter struct { ceiling float64 // maximum absolute output level (e.g. 1.0) attackCoeff float64 // attack smoothing coefficient releaseCoeff float64 // release smoothing coefficient gainReduction float64 // current gain reduction in linear scale (0 = no reduction) } // NewMPXLimiter creates a limiter with the given ceiling, attack and release // times in milliseconds, and sample rate. func NewMPXLimiter(ceiling, attackMs, releaseMs, sampleRate float64) *MPXLimiter { if ceiling <= 0 { ceiling = 1.0 } if attackMs <= 0 { attackMs = 0.1 // fast attack for MPX } if releaseMs <= 0 { releaseMs = 50 // moderate release } attackSamples := attackMs * sampleRate / 1000 releaseSamples := releaseMs * sampleRate / 1000 return &MPXLimiter{ ceiling: ceiling, attackCoeff: 1.0 - math.Exp(-1.0/attackSamples), releaseCoeff: 1.0 - math.Exp(-1.0/releaseSamples), } } // Process applies limiting to a single composite sample. func (l *MPXLimiter) Process(in float64) float64 { absIn := math.Abs(in) targetReduction := 0.0 if absIn > l.ceiling { targetReduction = 1.0 - l.ceiling/absIn } if targetReduction > l.gainReduction { l.gainReduction += l.attackCoeff * (targetReduction - l.gainReduction) } else { l.gainReduction += l.releaseCoeff * (targetReduction - l.gainReduction) } gain := 1.0 - l.gainReduction return in * gain } // Reset clears the limiter state. func (l *MPXLimiter) Reset() { l.gainReduction = 0 } // HardClip provides a simple hard clipper as a safety net after the limiter. func HardClip(sample, ceiling float64) float64 { if sample > ceiling { return ceiling } if sample < -ceiling { return -ceiling } return sample }