package dsp // StatefulFIRReal is a real-valued FIR filter that preserves its delay line // between calls to Process(). This eliminates click/pop artifacts at frame // boundaries in streaming audio pipelines. type StatefulFIRReal struct { taps []float64 delay []float64 pos int // write position in circular delay buffer } // NewStatefulFIRReal creates a stateful FIR filter with the given taps. func NewStatefulFIRReal(taps []float64) *StatefulFIRReal { t := make([]float64, len(taps)) copy(t, taps) return &StatefulFIRReal{ taps: t, delay: make([]float64, len(taps)), } } // Process filters the input through the FIR with persistent state. // Allocates a new output slice. For zero-alloc hot paths, use ProcessInto. func (f *StatefulFIRReal) Process(x []float32) []float32 { out := make([]float32, len(x)) f.ProcessInto(x, out) return out } // ProcessInto filters into a pre-allocated output buffer. func (f *StatefulFIRReal) ProcessInto(x []float32, out []float32) []float32 { if len(x) == 0 || len(f.taps) == 0 { return out[:0] } n := len(f.taps) for i := 0; i < len(x); i++ { copy(f.delay[1:], f.delay[:n-1]) f.delay[0] = float64(x[i]) var acc float64 for k := 0; k < n; k++ { acc += f.delay[k] * f.taps[k] } out[i] = float32(acc) } return out[:len(x)] } // Reset clears the delay line. func (f *StatefulFIRReal) Reset() { for i := range f.delay { f.delay[i] = 0 } } // StatefulFIRComplex is a complex-valued FIR filter with persistent state. type StatefulFIRComplex struct { taps []float64 delayR []float64 delayI []float64 } // NewStatefulFIRComplex creates a stateful complex FIR filter. func NewStatefulFIRComplex(taps []float64) *StatefulFIRComplex { t := make([]float64, len(taps)) copy(t, taps) return &StatefulFIRComplex{ taps: t, delayR: make([]float64, len(taps)), delayI: make([]float64, len(taps)), } } // Process filters complex IQ through the FIR with persistent state. // Allocates a new output slice. For zero-alloc hot paths, use ProcessInto. func (f *StatefulFIRComplex) Process(iq []complex64) []complex64 { out := make([]complex64, len(iq)) f.ProcessInto(iq, out) return out } // ProcessInto filters complex IQ into a pre-allocated output buffer. // out must be at least len(iq) long. Returns the used portion of out. func (f *StatefulFIRComplex) ProcessInto(iq []complex64, out []complex64) []complex64 { if len(iq) == 0 || len(f.taps) == 0 { return out[:0] } n := len(f.taps) for i := 0; i < len(iq); i++ { copy(f.delayR[1:], f.delayR[:n-1]) copy(f.delayI[1:], f.delayI[:n-1]) f.delayR[0] = float64(real(iq[i])) f.delayI[0] = float64(imag(iq[i])) var accR, accI float64 for k := 0; k < n; k++ { w := f.taps[k] accR += f.delayR[k] * w accI += f.delayI[k] * w } out[i] = complex(float32(accR), float32(accI)) } return out[:len(iq)] } // Reset clears the delay line. func (f *StatefulFIRComplex) Reset() { for i := range f.delayR { f.delayR[i] = 0 f.delayI[i] = 0 } }