|
- package dsp
-
- import "math"
-
- // ---------------------------------------------------------------------------
- // Rational Polyphase Resampler
- // ---------------------------------------------------------------------------
- //
- // Converts sample rate by a rational factor L/M (upsample by L, then
- // downsample by M) using a polyphase FIR implementation. The polyphase
- // decomposition avoids computing intermediate upsampled samples that
- // would be discarded, making it efficient even for large L/M.
- //
- // The resampler is stateful: it preserves its internal delay line and
- // phase index between calls to Process(), enabling click-free streaming
- // across frame boundaries.
- //
- // Usage:
- //
- // r := dsp.NewResampler(51200, 48000, 64) // 64 taps per phase
- // for each frame {
- // out := r.Process(audio) // or r.ProcessStereo(interleaved)
- // }
- //
- // ---------------------------------------------------------------------------
-
- // Resampler performs rational polyphase sample rate conversion.
- type Resampler struct {
- l int // upsample factor
- m int // downsample factor
- tapsPerPh int // taps per polyphase arm
- polyBank [][]float64 // polyBank[phase][tap]
- delay []float64 // delay line, length = tapsPerPh
- // outTime is the position (in upsampled-rate units) of the next output
- // sample, relative to the next input sample to be consumed. It is
- // always in [0, L). Between calls it persists so that the fractional
- // position is perfectly continuous.
- outTime int
- }
-
- // NewResampler creates a polyphase resampler converting from inRate to
- // outRate. tapsPerPhase controls the filter quality (16 = basic, 32 =
- // good, 64 = high quality). The total prototype filter length is
- // L * tapsPerPhase.
- func NewResampler(inRate, outRate, tapsPerPhase int) *Resampler {
- if inRate <= 0 || outRate <= 0 {
- inRate, outRate = 1, 1
- }
- if tapsPerPhase < 4 {
- tapsPerPhase = 4
- }
-
- g := gcd(inRate, outRate)
- l := outRate / g // upsample factor
- m := inRate / g // downsample factor
-
- // Prototype lowpass: cutoff at min(1/L, 1/M) * Nyquist of the
- // upsampled rate, with some margin for the transition band.
- protoLen := l * tapsPerPhase
- if protoLen%2 == 0 {
- protoLen++ // ensure odd length for symmetric filter
- }
-
- // Normalized cutoff: passband edge relative to upsampled rate
- fc := 0.45 / float64(max(l, m)) // 0.45 instead of 0.5 for transition margin
- proto := windowedSinc(protoLen, fc, float64(l))
-
- // Decompose prototype into L polyphase arms
- actualTapsPerPh := (protoLen + l - 1) / l
- bank := make([][]float64, l)
- for p := 0; p < l; p++ {
- arm := make([]float64, actualTapsPerPh)
- for t := 0; t < actualTapsPerPh; t++ {
- idx := p + t*l
- if idx < protoLen {
- arm[t] = proto[idx]
- }
- }
- bank[p] = arm
- }
-
- return &Resampler{
- l: l,
- m: m,
- tapsPerPh: actualTapsPerPh,
- polyBank: bank,
- delay: make([]float64, actualTapsPerPh),
- outTime: 0,
- }
- }
-
- // Process resamples a mono float32 buffer and returns the resampled output.
- // State is preserved between calls for seamless streaming.
- //
- // The key insight: we conceptually interleave L-1 zeros between each input
- // sample (upsampled rate = L * Fs_in), then pick every M-th sample from
- // the filtered result (output rate = L/M * Fs_in).
- //
- // outTime tracks the sub-sample position of the next output within the
- // current input sample's L phases. When outTime wraps past L, we consume
- // the next input sample. This single counter gives exact, chunk-independent
- // output.
- func (r *Resampler) Process(in []float32) []float32 {
- if len(in) == 0 {
- return nil
- }
- if r.l == r.m {
- out := make([]float32, len(in))
- copy(out, in)
- return out
- }
-
- L := r.l
- M := r.m
- taps := r.tapsPerPh
- estOut := int(float64(len(in))*float64(L)/float64(M)) + 4
- out := make([]float32, 0, estOut)
-
- inPos := 0
- t := r.outTime
-
- for inPos < len(in) {
- // Consume input samples until outTime < L
- for t >= L {
- t -= L
- if inPos >= len(in) {
- r.outTime = t
- return out
- }
- copy(r.delay[1:], r.delay[:taps-1])
- r.delay[0] = float64(in[inPos])
- inPos++
- }
-
- // Produce output at phase = t
- arm := r.polyBank[t]
- var acc float64
- for k := 0; k < taps; k++ {
- acc += r.delay[k] * arm[k]
- }
- out = append(out, float32(acc))
-
- // Advance to next output position
- t += M
- }
-
- r.outTime = t
- return out
- }
-
- // Reset clears the delay line and phase state.
- func (r *Resampler) Reset() {
- for i := range r.delay {
- r.delay[i] = 0
- }
- r.outTime = 0
- }
-
- // OutputRate returns the effective output sample rate given an input rate.
- func (r *Resampler) OutputRate(inRate int) int {
- return inRate * r.l / r.m
- }
-
- // Ratio returns L and M.
- func (r *Resampler) Ratio() (int, int) {
- return r.l, r.m
- }
-
- // ---------------------------------------------------------------------------
- // StereoResampler — two synchronised mono resamplers
- // ---------------------------------------------------------------------------
-
- // StereoResampler wraps two Resampler instances sharing the same L/M ratio
- // for click-free stereo resampling with independent delay lines.
- type StereoResampler struct {
- left *Resampler
- right *Resampler
- }
-
- // NewStereoResampler creates a pair of synchronised resamplers.
- func NewStereoResampler(inRate, outRate, tapsPerPhase int) *StereoResampler {
- return &StereoResampler{
- left: NewResampler(inRate, outRate, tapsPerPhase),
- right: NewResampler(inRate, outRate, tapsPerPhase),
- }
- }
-
- // Process takes interleaved stereo [L0,R0,L1,R1,...] and returns
- // resampled interleaved stereo.
- func (sr *StereoResampler) Process(in []float32) []float32 {
- nFrames := len(in) / 2
- if nFrames == 0 {
- return nil
- }
- left := make([]float32, nFrames)
- right := make([]float32, nFrames)
- for i := 0; i < nFrames; i++ {
- left[i] = in[i*2]
- if i*2+1 < len(in) {
- right[i] = in[i*2+1]
- }
- }
-
- outL := sr.left.Process(left)
- outR := sr.right.Process(right)
-
- // Interleave — use shorter length if they differ by 1 sample
- n := len(outL)
- if len(outR) < n {
- n = len(outR)
- }
- out := make([]float32, n*2)
- for i := 0; i < n; i++ {
- out[i*2] = outL[i]
- out[i*2+1] = outR[i]
- }
- return out
- }
-
- // Reset clears both delay lines.
- func (sr *StereoResampler) Reset() {
- sr.left.Reset()
- sr.right.Reset()
- }
-
- // OutputRate returns the resampled output rate.
- func (sr *StereoResampler) OutputRate(inRate int) int {
- return sr.left.OutputRate(inRate)
- }
-
- // ---------------------------------------------------------------------------
- // Helpers
- // ---------------------------------------------------------------------------
-
- func gcd(a, b int) int {
- for b != 0 {
- a, b = b, a%b
- }
- if a < 0 {
- return -a
- }
- return a
- }
-
- func max(a, b int) int {
- if a > b {
- return a
- }
- return b
- }
-
- // windowedSinc generates a windowed-sinc prototype lowpass filter.
- // fc is the normalised cutoff (0..0.5 of the upsampled rate).
- // gain is the scaling factor (= L for polyphase interpolation).
- func windowedSinc(length int, fc float64, gain float64) []float64 {
- out := make([]float64, length)
- mid := float64(length-1) / 2.0
- for n := 0; n < length; n++ {
- x := float64(n) - mid
- // Sinc
- var s float64
- if math.Abs(x) < 1e-12 {
- s = 2 * math.Pi * fc
- } else {
- s = math.Sin(2*math.Pi*fc*x) / x
- }
- // Kaiser window (beta=6 gives ~-60dB sidelobe, good for audio)
- w := kaiserWindow(n, length, 6.0)
- out[n] = s * w * gain
- }
- return out
- }
-
- // kaiserWindow computes the Kaiser window value for sample n of N total.
- func kaiserWindow(n, N int, beta float64) float64 {
- mid := float64(N-1) / 2.0
- x := (float64(n) - mid) / mid
- return bessel0(beta*math.Sqrt(1-x*x)) / bessel0(beta)
- }
-
- // bessel0 is the zeroth-order modified Bessel function of the first kind.
- func bessel0(x float64) float64 {
- // Series expansion — converges rapidly for typical beta values
- sum := 1.0
- term := 1.0
- for k := 1; k < 30; k++ {
- term *= (x / (2 * float64(k))) * (x / (2 * float64(k)))
- sum += term
- if term < 1e-12*sum {
- break
- }
- }
- return sum
- }
|