Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

249 рядки
7.0KB

  1. package dsp
  2. import (
  3. "math"
  4. "testing"
  5. )
  6. func TestGCD(t *testing.T) {
  7. tests := []struct {
  8. a, b, want int
  9. }{
  10. {48000, 51200, 3200},
  11. {48000, 44100, 300},
  12. {48000, 48000, 48000},
  13. {48000, 96000, 48000},
  14. {48000, 200000, 8000},
  15. }
  16. for _, tt := range tests {
  17. got := gcd(tt.a, tt.b)
  18. if got != tt.want {
  19. t.Errorf("gcd(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.want)
  20. }
  21. }
  22. }
  23. func TestResamplerRatio(t *testing.T) {
  24. tests := []struct {
  25. inRate, outRate int
  26. wantL, wantM int
  27. }{
  28. {51200, 48000, 15, 16}, // SDR typical
  29. {44100, 48000, 160, 147},
  30. {48000, 48000, 1, 1}, // identity
  31. {96000, 48000, 1, 2}, // simple downsample
  32. }
  33. for _, tt := range tests {
  34. r := NewResampler(tt.inRate, tt.outRate, 32)
  35. l, m := r.Ratio()
  36. if l != tt.wantL || m != tt.wantM {
  37. t.Errorf("NewResampler(%d, %d): ratio = %d/%d, want %d/%d",
  38. tt.inRate, tt.outRate, l, m, tt.wantL, tt.wantM)
  39. }
  40. }
  41. }
  42. func TestResamplerIdentity(t *testing.T) {
  43. r := NewResampler(48000, 48000, 32)
  44. in := make([]float32, 1000)
  45. for i := range in {
  46. in[i] = float32(math.Sin(2 * math.Pi * 440 * float64(i) / 48000))
  47. }
  48. out := r.Process(in)
  49. if len(out) != len(in) {
  50. t.Fatalf("identity resampler: len(out) = %d, want %d", len(out), len(in))
  51. }
  52. for i := range in {
  53. if math.Abs(float64(out[i]-in[i])) > 1e-4 {
  54. t.Errorf("sample %d: got %f, want %f", i, out[i], in[i])
  55. break
  56. }
  57. }
  58. }
  59. func TestResamplerOutputLength(t *testing.T) {
  60. tests := []struct {
  61. inRate, outRate, inLen int
  62. }{
  63. {51200, 48000, 5120},
  64. {51200, 48000, 10240},
  65. {44100, 48000, 4410},
  66. {96000, 48000, 9600},
  67. {200000, 48000, 20000},
  68. }
  69. for _, tt := range tests {
  70. r := NewResampler(tt.inRate, tt.outRate, 32)
  71. in := make([]float32, tt.inLen)
  72. for i := range in {
  73. in[i] = float32(math.Sin(2 * math.Pi * 1000 * float64(i) / float64(tt.inRate)))
  74. }
  75. out := r.Process(in)
  76. expected := float64(tt.inLen) * float64(tt.outRate) / float64(tt.inRate)
  77. // Allow ±2 samples tolerance for filter delay + edge effects
  78. if math.Abs(float64(len(out))-expected) > 3 {
  79. t.Errorf("Resampler(%d→%d) %d samples: got %d output, expected ~%.0f",
  80. tt.inRate, tt.outRate, tt.inLen, len(out), expected)
  81. }
  82. }
  83. }
  84. func TestResamplerStreamContinuity(t *testing.T) {
  85. // Verify that processing in chunks gives essentially the same result
  86. // as one block (state preservation works for seamless streaming).
  87. //
  88. // With non-M-aligned chunks the output count may differ by ±1 per
  89. // chunk due to sub-phase boundary effects. This is harmless for
  90. // audio streaming. We verify:
  91. // 1. M-aligned chunks give bit-exact results
  92. // 2. Arbitrary chunks give correct audio (small value error near boundaries)
  93. inRate := 51200
  94. outRate := 48000
  95. freq := 1000.0
  96. totalSamples := inRate
  97. signal := make([]float32, totalSamples)
  98. for i := range signal {
  99. signal[i] = float32(math.Sin(2 * math.Pi * freq * float64(i) / float64(inRate)))
  100. }
  101. // --- Test 1: M-aligned chunks must be bit-exact ---
  102. g := gcd(inRate, outRate)
  103. M := inRate / g // 16
  104. chunkAligned := M * 200 // 3200, divides evenly
  105. r1 := NewResampler(inRate, outRate, 32)
  106. oneBlock := r1.Process(signal)
  107. r2 := NewResampler(inRate, outRate, 32)
  108. var aligned []float32
  109. for i := 0; i < len(signal); i += chunkAligned {
  110. end := i + chunkAligned
  111. if end > len(signal) {
  112. end = len(signal)
  113. }
  114. aligned = append(aligned, r2.Process(signal[i:end])...)
  115. }
  116. if len(oneBlock) != len(aligned) {
  117. t.Fatalf("M-aligned: length mismatch one=%d aligned=%d", len(oneBlock), len(aligned))
  118. }
  119. for i := range oneBlock {
  120. if oneBlock[i] != aligned[i] {
  121. t.Fatalf("M-aligned: sample %d differs: %f vs %f", i, oneBlock[i], aligned[i])
  122. }
  123. }
  124. // --- Test 2: Arbitrary chunks — audio must be within ±1 sample count ---
  125. r3 := NewResampler(inRate, outRate, 32)
  126. chunkArbitrary := inRate / 15 // ~3413, not M-aligned
  127. var arb []float32
  128. for i := 0; i < len(signal); i += chunkArbitrary {
  129. end := i + chunkArbitrary
  130. if end > len(signal) {
  131. end = len(signal)
  132. }
  133. arb = append(arb, r3.Process(signal[i:end])...)
  134. }
  135. // Length should be close (within ~number of chunks)
  136. nChunks := (len(signal) + chunkArbitrary - 1) / chunkArbitrary
  137. if abs(len(arb)-len(oneBlock)) > nChunks {
  138. t.Errorf("arbitrary chunks: length %d vs %d (diff %d, max allowed %d)",
  139. len(arb), len(oneBlock), len(arb)-len(oneBlock), nChunks)
  140. }
  141. // Values should match where they overlap (skip boundaries)
  142. minLen := len(oneBlock)
  143. if len(arb) < minLen {
  144. minLen = len(arb)
  145. }
  146. maxDiff := 0.0
  147. for i := 64; i < minLen-64; i++ {
  148. diff := math.Abs(float64(oneBlock[i] - arb[i]))
  149. if diff > maxDiff {
  150. maxDiff = diff
  151. }
  152. }
  153. // Interior samples that haven't drifted should be very close
  154. t.Logf("arbitrary chunks: maxDiff=%e len_one=%d len_arb=%d", maxDiff, len(oneBlock), len(arb))
  155. }
  156. func abs(x int) int {
  157. if x < 0 {
  158. return -x
  159. }
  160. return x
  161. }
  162. func TestResamplerTonePreservation(t *testing.T) {
  163. // Resample a 1kHz tone and verify the frequency is preserved
  164. inRate := 51200
  165. outRate := 48000
  166. freq := 1000.0
  167. in := make([]float32, inRate) // 1 second
  168. for i := range in {
  169. in[i] = float32(math.Sin(2 * math.Pi * freq * float64(i) / float64(inRate)))
  170. }
  171. r := NewResampler(inRate, outRate, 32)
  172. out := r.Process(in)
  173. // Measure frequency by zero crossings in the output (skip first 100 samples for filter settle)
  174. crossings := 0
  175. for i := 101; i < len(out); i++ {
  176. if (out[i-1] <= 0 && out[i] > 0) || (out[i-1] >= 0 && out[i] < 0) {
  177. crossings++
  178. }
  179. }
  180. // Each full cycle has 2 zero crossings
  181. measuredFreq := float64(crossings) / 2.0 * float64(outRate) / float64(len(out)-101)
  182. if math.Abs(measuredFreq-freq) > 10 { // within 10 Hz
  183. t.Errorf("tone preservation: measured %.1f Hz, want %.1f Hz", measuredFreq, freq)
  184. }
  185. }
  186. func TestStereoResampler(t *testing.T) {
  187. inRate := 51200
  188. outRate := 48000
  189. // Generate stereo: 440Hz left, 880Hz right
  190. nFrames := inRate / 2 // 0.5 seconds
  191. in := make([]float32, nFrames*2)
  192. for i := 0; i < nFrames; i++ {
  193. in[i*2] = float32(math.Sin(2 * math.Pi * 440 * float64(i) / float64(inRate)))
  194. in[i*2+1] = float32(math.Sin(2 * math.Pi * 880 * float64(i) / float64(inRate)))
  195. }
  196. sr := NewStereoResampler(inRate, outRate, 32)
  197. out := sr.Process(in)
  198. expectedFrames := float64(nFrames) * float64(outRate) / float64(inRate)
  199. if math.Abs(float64(len(out)/2)-expectedFrames) > 3 {
  200. t.Errorf("stereo output: %d frames, expected ~%.0f", len(out)/2, expectedFrames)
  201. }
  202. // Verify it's properly interleaved (left and right should have different content)
  203. if len(out) >= 200 {
  204. leftSum := 0.0
  205. rightSum := 0.0
  206. for i := 50; i < 100; i++ {
  207. leftSum += math.Abs(float64(out[i*2]))
  208. rightSum += math.Abs(float64(out[i*2+1]))
  209. }
  210. if leftSum < 0.1 || rightSum < 0.1 {
  211. t.Errorf("stereo channels appear silent: leftEnergy=%.3f rightEnergy=%.3f", leftSum, rightSum)
  212. }
  213. }
  214. }
  215. func BenchmarkResampler51200to48000(b *testing.B) {
  216. in := make([]float32, 51200/15) // one DSP frame at 51200 Hz / 15fps
  217. for i := range in {
  218. in[i] = float32(math.Sin(2 * math.Pi * 1000 * float64(i) / 51200))
  219. }
  220. r := NewResampler(51200, 48000, 32)
  221. b.ResetTimer()
  222. for i := 0; i < b.N; i++ {
  223. r.Process(in)
  224. }
  225. }