Wideband autonomous SDR analysis engine forked from sdr-visual-suite
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

295 lignes
7.8KB

  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. fc := 0.45 / float64(max(l, m)) // 0.45 instead of 0.5 for transition margin
  59. proto := windowedSinc(protoLen, fc, float64(l))
  60. // Decompose prototype into L polyphase arms
  61. actualTapsPerPh := (protoLen + l - 1) / l
  62. bank := make([][]float64, l)
  63. for p := 0; p < l; p++ {
  64. arm := make([]float64, actualTapsPerPh)
  65. for t := 0; t < actualTapsPerPh; t++ {
  66. idx := p + t*l
  67. if idx < protoLen {
  68. arm[t] = proto[idx]
  69. }
  70. }
  71. bank[p] = arm
  72. }
  73. return &Resampler{
  74. l: l,
  75. m: m,
  76. tapsPerPh: actualTapsPerPh,
  77. polyBank: bank,
  78. delay: make([]float64, actualTapsPerPh),
  79. outTime: 0,
  80. }
  81. }
  82. // Process resamples a mono float32 buffer and returns the resampled output.
  83. // State is preserved between calls for seamless streaming.
  84. //
  85. // The key insight: we conceptually interleave L-1 zeros between each input
  86. // sample (upsampled rate = L * Fs_in), then pick every M-th sample from
  87. // the filtered result (output rate = L/M * Fs_in).
  88. //
  89. // outTime tracks the sub-sample position of the next output within the
  90. // current input sample's L phases. When outTime wraps past L, we consume
  91. // the next input sample. This single counter gives exact, chunk-independent
  92. // output.
  93. func (r *Resampler) Process(in []float32) []float32 {
  94. if len(in) == 0 {
  95. return nil
  96. }
  97. if r.l == r.m {
  98. out := make([]float32, len(in))
  99. copy(out, in)
  100. return out
  101. }
  102. L := r.l
  103. M := r.m
  104. taps := r.tapsPerPh
  105. estOut := int(float64(len(in))*float64(L)/float64(M)) + 4
  106. out := make([]float32, 0, estOut)
  107. inPos := 0
  108. t := r.outTime
  109. for inPos < len(in) {
  110. // Consume input samples until outTime < L
  111. for t >= L {
  112. t -= L
  113. if inPos >= len(in) {
  114. r.outTime = t
  115. return out
  116. }
  117. copy(r.delay[1:], r.delay[:taps-1])
  118. r.delay[0] = float64(in[inPos])
  119. inPos++
  120. }
  121. // Produce output at phase = t
  122. arm := r.polyBank[t]
  123. var acc float64
  124. for k := 0; k < taps; k++ {
  125. acc += r.delay[k] * arm[k]
  126. }
  127. out = append(out, float32(acc))
  128. // Advance to next output position
  129. t += M
  130. }
  131. r.outTime = t
  132. return out
  133. }
  134. // Reset clears the delay line and phase state.
  135. func (r *Resampler) Reset() {
  136. for i := range r.delay {
  137. r.delay[i] = 0
  138. }
  139. r.outTime = 0
  140. }
  141. // OutputRate returns the effective output sample rate given an input rate.
  142. func (r *Resampler) OutputRate(inRate int) int {
  143. return inRate * r.l / r.m
  144. }
  145. // Ratio returns L and M.
  146. func (r *Resampler) Ratio() (int, int) {
  147. return r.l, r.m
  148. }
  149. // ---------------------------------------------------------------------------
  150. // StereoResampler — two synchronised mono resamplers
  151. // ---------------------------------------------------------------------------
  152. // StereoResampler wraps two Resampler instances sharing the same L/M ratio
  153. // for click-free stereo resampling with independent delay lines.
  154. type StereoResampler struct {
  155. left *Resampler
  156. right *Resampler
  157. }
  158. // NewStereoResampler creates a pair of synchronised resamplers.
  159. func NewStereoResampler(inRate, outRate, tapsPerPhase int) *StereoResampler {
  160. return &StereoResampler{
  161. left: NewResampler(inRate, outRate, tapsPerPhase),
  162. right: NewResampler(inRate, outRate, tapsPerPhase),
  163. }
  164. }
  165. // Process takes interleaved stereo [L0,R0,L1,R1,...] and returns
  166. // resampled interleaved stereo.
  167. func (sr *StereoResampler) Process(in []float32) []float32 {
  168. nFrames := len(in) / 2
  169. if nFrames == 0 {
  170. return nil
  171. }
  172. left := make([]float32, nFrames)
  173. right := make([]float32, nFrames)
  174. for i := 0; i < nFrames; i++ {
  175. left[i] = in[i*2]
  176. if i*2+1 < len(in) {
  177. right[i] = in[i*2+1]
  178. }
  179. }
  180. outL := sr.left.Process(left)
  181. outR := sr.right.Process(right)
  182. // Interleave — use shorter length if they differ by 1 sample
  183. n := len(outL)
  184. if len(outR) < n {
  185. n = len(outR)
  186. }
  187. out := make([]float32, n*2)
  188. for i := 0; i < n; i++ {
  189. out[i*2] = outL[i]
  190. out[i*2+1] = outR[i]
  191. }
  192. return out
  193. }
  194. // Reset clears both delay lines.
  195. func (sr *StereoResampler) Reset() {
  196. sr.left.Reset()
  197. sr.right.Reset()
  198. }
  199. // OutputRate returns the resampled output rate.
  200. func (sr *StereoResampler) OutputRate(inRate int) int {
  201. return sr.left.OutputRate(inRate)
  202. }
  203. // ---------------------------------------------------------------------------
  204. // Helpers
  205. // ---------------------------------------------------------------------------
  206. func gcd(a, b int) int {
  207. for b != 0 {
  208. a, b = b, a%b
  209. }
  210. if a < 0 {
  211. return -a
  212. }
  213. return a
  214. }
  215. func max(a, b int) int {
  216. if a > b {
  217. return a
  218. }
  219. return b
  220. }
  221. // windowedSinc generates a windowed-sinc prototype lowpass filter.
  222. // fc is the normalised cutoff (0..0.5 of the upsampled rate).
  223. // gain is the scaling factor (= L for polyphase interpolation).
  224. func windowedSinc(length int, fc float64, gain float64) []float64 {
  225. out := make([]float64, length)
  226. mid := float64(length-1) / 2.0
  227. for n := 0; n < length; n++ {
  228. x := float64(n) - mid
  229. // Sinc
  230. var s float64
  231. if math.Abs(x) < 1e-12 {
  232. s = 2 * math.Pi * fc
  233. } else {
  234. s = math.Sin(2*math.Pi*fc*x) / x
  235. }
  236. // Kaiser window (beta=6 gives ~-60dB sidelobe, good for audio)
  237. w := kaiserWindow(n, length, 6.0)
  238. out[n] = s * w * gain
  239. }
  240. return out
  241. }
  242. // kaiserWindow computes the Kaiser window value for sample n of N total.
  243. func kaiserWindow(n, N int, beta float64) float64 {
  244. mid := float64(N-1) / 2.0
  245. x := (float64(n) - mid) / mid
  246. return bessel0(beta*math.Sqrt(1-x*x)) / bessel0(beta)
  247. }
  248. // bessel0 is the zeroth-order modified Bessel function of the first kind.
  249. func bessel0(x float64) float64 {
  250. // Series expansion — converges rapidly for typical beta values
  251. sum := 1.0
  252. term := 1.0
  253. for k := 1; k < 30; k++ {
  254. term *= (x / (2 * float64(k))) * (x / (2 * float64(k)))
  255. sum += term
  256. if term < 1e-12*sum {
  257. break
  258. }
  259. }
  260. return sum
  261. }