Wideband autonomous SDR analysis engine forked from sdr-visual-suite
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

298 line
8.0KB

  1. package dsp
  2. import "math"
  3. // ---------------------------------------------------------------------------
  4. // Rational Polyphase Resampler
  5. // ---------------------------------------------------------------------------
  6. //
  7. // Converts sample rate by a rational factor L/M (upsample by L, then
  8. // downsample by M) using a polyphase FIR implementation. The polyphase
  9. // decomposition avoids computing intermediate upsampled samples that
  10. // would be discarded, making it efficient even for large L/M.
  11. //
  12. // The resampler is stateful: it preserves its internal delay line and
  13. // phase index between calls to Process(), enabling click-free streaming
  14. // across frame boundaries.
  15. //
  16. // Usage:
  17. //
  18. // r := dsp.NewResampler(51200, 48000, 64) // 64 taps per phase
  19. // for each frame {
  20. // out := r.Process(audio) // or r.ProcessStereo(interleaved)
  21. // }
  22. //
  23. // ---------------------------------------------------------------------------
  24. // Resampler performs rational polyphase sample rate conversion.
  25. type Resampler struct {
  26. l int // upsample factor
  27. m int // downsample factor
  28. tapsPerPh int // taps per polyphase arm
  29. polyBank [][]float64 // polyBank[phase][tap]
  30. delay []float64 // delay line, length = tapsPerPh
  31. // outTime is the position (in upsampled-rate units) of the next output
  32. // sample, relative to the next input sample to be consumed. It is
  33. // always in [0, L). Between calls it persists so that the fractional
  34. // position is perfectly continuous.
  35. outTime int
  36. }
  37. // NewResampler creates a polyphase resampler converting from inRate to
  38. // outRate. tapsPerPhase controls the filter quality (16 = basic, 32 =
  39. // good, 64 = high quality). The total prototype filter length is
  40. // L * tapsPerPhase.
  41. func NewResampler(inRate, outRate, tapsPerPhase int) *Resampler {
  42. if inRate <= 0 || outRate <= 0 {
  43. inRate, outRate = 1, 1
  44. }
  45. if tapsPerPhase < 4 {
  46. tapsPerPhase = 4
  47. }
  48. g := gcd(inRate, outRate)
  49. l := outRate / g // upsample factor
  50. m := inRate / g // downsample factor
  51. // Prototype lowpass: cutoff at min(1/L, 1/M) * Nyquist of the
  52. // upsampled rate, with some margin for the transition band.
  53. protoLen := l * tapsPerPhase
  54. if protoLen%2 == 0 {
  55. protoLen++ // ensure odd length for symmetric filter
  56. }
  57. // Normalized cutoff: passband edge relative to upsampled rate.
  58. // 0.90 passes up to ~95% of output Nyquist (≈22.8kHz at 48kHz out),
  59. // providing full 15kHz FM stereo bandwidth. The Kaiser window (β=6)
  60. // gives ≈-60dB sidelobe suppression for clean anti-alias rejection.
  61. fc := 0.90 / float64(max(l, m))
  62. proto := windowedSinc(protoLen, fc, float64(l))
  63. // Decompose prototype into L polyphase arms
  64. actualTapsPerPh := (protoLen + l - 1) / l
  65. bank := make([][]float64, l)
  66. for p := 0; p < l; p++ {
  67. arm := make([]float64, actualTapsPerPh)
  68. for t := 0; t < actualTapsPerPh; t++ {
  69. idx := p + t*l
  70. if idx < protoLen {
  71. arm[t] = proto[idx]
  72. }
  73. }
  74. bank[p] = arm
  75. }
  76. return &Resampler{
  77. l: l,
  78. m: m,
  79. tapsPerPh: actualTapsPerPh,
  80. polyBank: bank,
  81. delay: make([]float64, actualTapsPerPh),
  82. outTime: 0,
  83. }
  84. }
  85. // Process resamples a mono float32 buffer and returns the resampled output.
  86. // State is preserved between calls for seamless streaming.
  87. //
  88. // The key insight: we conceptually interleave L-1 zeros between each input
  89. // sample (upsampled rate = L * Fs_in), then pick every M-th sample from
  90. // the filtered result (output rate = L/M * Fs_in).
  91. //
  92. // outTime tracks the sub-sample position of the next output within the
  93. // current input sample's L phases. When outTime wraps past L, we consume
  94. // the next input sample. This single counter gives exact, chunk-independent
  95. // output.
  96. func (r *Resampler) Process(in []float32) []float32 {
  97. if len(in) == 0 {
  98. return nil
  99. }
  100. if r.l == r.m {
  101. out := make([]float32, len(in))
  102. copy(out, in)
  103. return out
  104. }
  105. L := r.l
  106. M := r.m
  107. taps := r.tapsPerPh
  108. estOut := int(float64(len(in))*float64(L)/float64(M)) + 4
  109. out := make([]float32, 0, estOut)
  110. inPos := 0
  111. t := r.outTime
  112. for inPos < len(in) {
  113. // Consume input samples until outTime < L
  114. for t >= L {
  115. t -= L
  116. if inPos >= len(in) {
  117. r.outTime = t
  118. return out
  119. }
  120. copy(r.delay[1:], r.delay[:taps-1])
  121. r.delay[0] = float64(in[inPos])
  122. inPos++
  123. }
  124. // Produce output at phase = t
  125. arm := r.polyBank[t]
  126. var acc float64
  127. for k := 0; k < taps; k++ {
  128. acc += r.delay[k] * arm[k]
  129. }
  130. out = append(out, float32(acc))
  131. // Advance to next output position
  132. t += M
  133. }
  134. r.outTime = t
  135. return out
  136. }
  137. // Reset clears the delay line and phase state.
  138. func (r *Resampler) Reset() {
  139. for i := range r.delay {
  140. r.delay[i] = 0
  141. }
  142. r.outTime = 0
  143. }
  144. // OutputRate returns the effective output sample rate given an input rate.
  145. func (r *Resampler) OutputRate(inRate int) int {
  146. return inRate * r.l / r.m
  147. }
  148. // Ratio returns L and M.
  149. func (r *Resampler) Ratio() (int, int) {
  150. return r.l, r.m
  151. }
  152. // ---------------------------------------------------------------------------
  153. // StereoResampler — two synchronised mono resamplers
  154. // ---------------------------------------------------------------------------
  155. // StereoResampler wraps two Resampler instances sharing the same L/M ratio
  156. // for click-free stereo resampling with independent delay lines.
  157. type StereoResampler struct {
  158. left *Resampler
  159. right *Resampler
  160. }
  161. // NewStereoResampler creates a pair of synchronised resamplers.
  162. func NewStereoResampler(inRate, outRate, tapsPerPhase int) *StereoResampler {
  163. return &StereoResampler{
  164. left: NewResampler(inRate, outRate, tapsPerPhase),
  165. right: NewResampler(inRate, outRate, tapsPerPhase),
  166. }
  167. }
  168. // Process takes interleaved stereo [L0,R0,L1,R1,...] and returns
  169. // resampled interleaved stereo.
  170. func (sr *StereoResampler) Process(in []float32) []float32 {
  171. nFrames := len(in) / 2
  172. if nFrames == 0 {
  173. return nil
  174. }
  175. left := make([]float32, nFrames)
  176. right := make([]float32, nFrames)
  177. for i := 0; i < nFrames; i++ {
  178. left[i] = in[i*2]
  179. if i*2+1 < len(in) {
  180. right[i] = in[i*2+1]
  181. }
  182. }
  183. outL := sr.left.Process(left)
  184. outR := sr.right.Process(right)
  185. // Interleave — use shorter length if they differ by 1 sample
  186. n := len(outL)
  187. if len(outR) < n {
  188. n = len(outR)
  189. }
  190. out := make([]float32, n*2)
  191. for i := 0; i < n; i++ {
  192. out[i*2] = outL[i]
  193. out[i*2+1] = outR[i]
  194. }
  195. return out
  196. }
  197. // Reset clears both delay lines.
  198. func (sr *StereoResampler) Reset() {
  199. sr.left.Reset()
  200. sr.right.Reset()
  201. }
  202. // OutputRate returns the resampled output rate.
  203. func (sr *StereoResampler) OutputRate(inRate int) int {
  204. return sr.left.OutputRate(inRate)
  205. }
  206. // ---------------------------------------------------------------------------
  207. // Helpers
  208. // ---------------------------------------------------------------------------
  209. func gcd(a, b int) int {
  210. for b != 0 {
  211. a, b = b, a%b
  212. }
  213. if a < 0 {
  214. return -a
  215. }
  216. return a
  217. }
  218. func max(a, b int) int {
  219. if a > b {
  220. return a
  221. }
  222. return b
  223. }
  224. // windowedSinc generates a windowed-sinc prototype lowpass filter.
  225. // fc is the normalised cutoff (0..0.5 of the upsampled rate).
  226. // gain is the scaling factor (= L for polyphase interpolation).
  227. func windowedSinc(length int, fc float64, gain float64) []float64 {
  228. out := make([]float64, length)
  229. mid := float64(length-1) / 2.0
  230. for n := 0; n < length; n++ {
  231. x := float64(n) - mid
  232. // Sinc
  233. var s float64
  234. if math.Abs(x) < 1e-12 {
  235. s = 2 * math.Pi * fc
  236. } else {
  237. s = math.Sin(2*math.Pi*fc*x) / x
  238. }
  239. // Kaiser window (beta=6 gives ~-60dB sidelobe, good for audio)
  240. w := kaiserWindow(n, length, 6.0)
  241. out[n] = s * w * gain
  242. }
  243. return out
  244. }
  245. // kaiserWindow computes the Kaiser window value for sample n of N total.
  246. func kaiserWindow(n, N int, beta float64) float64 {
  247. mid := float64(N-1) / 2.0
  248. x := (float64(n) - mid) / mid
  249. return bessel0(beta*math.Sqrt(1-x*x)) / bessel0(beta)
  250. }
  251. // bessel0 is the zeroth-order modified Bessel function of the first kind.
  252. func bessel0(x float64) float64 {
  253. // Series expansion — converges rapidly for typical beta values
  254. sum := 1.0
  255. term := 1.0
  256. for k := 1; k < 30; k++ {
  257. term *= (x / (2 * float64(k))) * (x / (2 * float64(k)))
  258. sum += term
  259. if term < 1e-12*sum {
  260. break
  261. }
  262. }
  263. return sum
  264. }