|
- package dsp
-
- import "math"
-
- // BiquadLPF is a second-order Butterworth lowpass filter (biquad, direct form II transposed).
- // Used after the audio limiter to remove intermodulation products and harmonics
- // that could fall into the 19kHz pilot, 38kHz stereo sub, or 57kHz RDS bands.
- //
- // At 228kHz with fc=15kHz:
- // 15kHz: -3 dB (corner)
- // 19kHz: -5 dB
- // 38kHz: -18 dB
- // 57kHz: -27 dB ← protects RDS band
- type BiquadLPF struct {
- b0, b1, b2 float64
- a1, a2 float64
- z1, z2 float64 // state (direct form II transposed)
- }
-
- // NewBiquadLPF creates a 2nd-order Butterworth lowpass at the given cutoff.
- func NewBiquadLPF(cutoffHz, sampleRate float64) *BiquadLPF {
- if cutoffHz <= 0 || sampleRate <= 0 || cutoffHz >= sampleRate/2 {
- // Passthrough: return unity filter
- return &BiquadLPF{b0: 1}
- }
-
- omega := 2 * math.Pi * cutoffHz / sampleRate
- cosW := math.Cos(omega)
- sinW := math.Sin(omega)
- alpha := sinW / (2 * math.Sqrt2) // Q = 1/√2 for Butterworth
-
- a0 := 1 + alpha
- return &BiquadLPF{
- b0: (1 - cosW) / 2 / a0,
- b1: (1 - cosW) / a0,
- b2: (1 - cosW) / 2 / a0,
- a1: (-2 * cosW) / a0,
- a2: (1 - alpha) / a0,
- }
- }
-
- // Process filters a single sample.
- func (f *BiquadLPF) Process(in float64) float64 {
- out := f.b0*in + f.z1
- f.z1 = f.b1*in - f.a1*out + f.z2
- f.z2 = f.b2*in - f.a2*out
- return out
- }
-
- // Reset clears the filter state.
- func (f *BiquadLPF) Reset() {
- f.z1 = 0
- f.z2 = 0
- }
|