Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
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.

377 lines
8.5KB

  1. package audio
  2. import (
  3. "bytes"
  4. "encoding/binary"
  5. "io"
  6. "math"
  7. "sync"
  8. "sync/atomic"
  9. "testing"
  10. )
  11. func TestStreamSource_WriteRead(t *testing.T) {
  12. s := NewStreamSource(1024, 44100)
  13. if s.size != 1024 {
  14. t.Fatalf("expected size 1024, got %d", s.size)
  15. }
  16. // Write and read a frame
  17. f := NewFrame(0.5, -0.3)
  18. if !s.WriteFrame(f) {
  19. t.Fatal("write failed")
  20. }
  21. if s.Available() != 1 {
  22. t.Fatalf("expected 1 available, got %d", s.Available())
  23. }
  24. out := s.ReadFrame()
  25. if out.L != 0.5 || out.R != -0.3 {
  26. t.Fatalf("read mismatch: got L=%.2f R=%.2f", out.L, out.R)
  27. }
  28. if s.Available() != 0 {
  29. t.Fatalf("expected 0 available, got %d", s.Available())
  30. }
  31. }
  32. func TestStreamSource_Underrun(t *testing.T) {
  33. s := NewStreamSource(16, 44100)
  34. // Read from empty buffer — should return silence
  35. f := s.ReadFrame()
  36. if f.L != 0 || f.R != 0 {
  37. t.Fatal("expected silence on underrun")
  38. }
  39. if s.Underruns.Load() != 1 {
  40. t.Fatalf("expected 1 underrun, got %d", s.Underruns.Load())
  41. }
  42. }
  43. func TestStreamSource_Overflow(t *testing.T) {
  44. s := NewStreamSource(4, 44100) // size rounds up to 4
  45. // Fill completely
  46. for i := 0; i < 4; i++ {
  47. if !s.WriteFrame(NewFrame(Sample(float64(i)/10), 0)) {
  48. t.Fatalf("write %d failed", i)
  49. }
  50. }
  51. // Next write should overflow
  52. if s.WriteFrame(NewFrame(1, 1)) {
  53. t.Fatal("expected overflow")
  54. }
  55. if s.Overflows.Load() != 1 {
  56. t.Fatalf("expected 1 overflow, got %d", s.Overflows.Load())
  57. }
  58. }
  59. func TestStreamSource_PowerOf2Rounding(t *testing.T) {
  60. tests := []struct{ in, expect int }{
  61. {1, 1}, {2, 2}, {3, 4}, {5, 8}, {100, 128}, {1024, 1024}, {1025, 2048},
  62. }
  63. for _, tt := range tests {
  64. s := NewStreamSource(tt.in, 44100)
  65. if s.size != tt.expect {
  66. t.Fatalf("NewStreamSource(%d): size=%d, expected %d", tt.in, s.size, tt.expect)
  67. }
  68. }
  69. }
  70. func TestStreamSource_FIFO(t *testing.T) {
  71. s := NewStreamSource(64, 44100)
  72. n := 50
  73. for i := 0; i < n; i++ {
  74. s.WriteFrame(NewFrame(Sample(float64(i)), 0))
  75. }
  76. for i := 0; i < n; i++ {
  77. f := s.ReadFrame()
  78. if int(f.L) != i {
  79. t.Fatalf("FIFO order broken at %d: got %d", i, int(f.L))
  80. }
  81. }
  82. }
  83. func TestStreamSource_Wraparound(t *testing.T) {
  84. s := NewStreamSource(8, 44100) // size = 8
  85. // Write and read more than buffer size to test wraparound
  86. for round := 0; round < 10; round++ {
  87. for i := 0; i < 8; i++ {
  88. val := float64(round*8 + i)
  89. if !s.WriteFrame(NewFrame(Sample(val), 0)) {
  90. t.Fatalf("write failed round=%d i=%d", round, i)
  91. }
  92. }
  93. for i := 0; i < 8; i++ {
  94. expected := float64(round*8 + i)
  95. f := s.ReadFrame()
  96. if float64(f.L) != expected {
  97. t.Fatalf("round=%d i=%d: got %f expected %f", round, i, float64(f.L), expected)
  98. }
  99. }
  100. }
  101. stats := s.Stats()
  102. if stats.Underruns != 0 || stats.Overflows != 0 {
  103. t.Fatalf("unexpected errors: underruns=%d overflows=%d", stats.Underruns, stats.Overflows)
  104. }
  105. }
  106. func TestStreamSource_WritePCM(t *testing.T) {
  107. s := NewStreamSource(256, 44100)
  108. // Create 10 stereo frames of S16LE PCM
  109. var buf bytes.Buffer
  110. for i := 0; i < 10; i++ {
  111. l := int16(i * 1000)
  112. r := int16(-i * 1000)
  113. binary.Write(&buf, binary.LittleEndian, l)
  114. binary.Write(&buf, binary.LittleEndian, r)
  115. }
  116. written := s.WritePCM(buf.Bytes())
  117. if written != 10 {
  118. t.Fatalf("expected 10 frames, wrote %d", written)
  119. }
  120. // Verify first frame
  121. f := s.ReadFrame()
  122. if f.L != 0 || f.R != 0 {
  123. t.Fatalf("frame 0: L=%.4f R=%.4f, expected 0", f.L, f.R)
  124. }
  125. // Verify frame 5
  126. for i := 1; i < 5; i++ {
  127. s.ReadFrame()
  128. }
  129. f = s.ReadFrame()
  130. expectedL := 5000.0 / 32768.0
  131. if math.Abs(float64(f.L)-expectedL) > 0.001 {
  132. t.Fatalf("frame 5 L=%.4f, expected %.4f", f.L, expectedL)
  133. }
  134. }
  135. func TestStreamSource_ConcurrentSPSC(t *testing.T) {
  136. s := NewStreamSource(4096, 44100)
  137. frames := 50000
  138. var producerDone atomic.Bool
  139. var wg sync.WaitGroup
  140. wg.Add(2)
  141. // Producer
  142. go func() {
  143. defer wg.Done()
  144. for i := 0; i < frames; i++ {
  145. for !s.WriteFrame(NewFrame(Sample(float64(i+1)), 0)) {
  146. // Buffer full — yield
  147. }
  148. }
  149. producerDone.Store(true)
  150. }()
  151. // Consumer
  152. var lastVal float64
  153. var orderOK = true
  154. var readCount int
  155. go func() {
  156. defer wg.Done()
  157. for {
  158. if s.Available() == 0 {
  159. if producerDone.Load() {
  160. break
  161. }
  162. continue
  163. }
  164. f := s.ReadFrame()
  165. readCount++
  166. v := float64(f.L)
  167. if v > 0 && v < lastVal {
  168. orderOK = false
  169. }
  170. if v > 0 {
  171. lastVal = v
  172. }
  173. }
  174. }()
  175. wg.Wait()
  176. if !orderOK {
  177. t.Fatal("FIFO order broken in concurrent SPSC")
  178. }
  179. if readCount < frames/2 {
  180. t.Fatalf("read too few frames: %d (expected ~%d)", readCount, frames)
  181. }
  182. }
  183. // --- StreamResampler tests ---
  184. func TestStreamResampler_1to1(t *testing.T) {
  185. s := NewStreamSource(256, 44100)
  186. r := NewStreamResampler(s, 44100) // 1:1
  187. for i := 0; i < 100; i++ {
  188. s.WriteFrame(NewFrame(Sample(float64(i)/100), 0))
  189. }
  190. // At 1:1 ratio, output should track input with a small startup delay.
  191. // Skip first few samples (resampler priming), then verify monotonic increase.
  192. prev := -1.0
  193. for i := 0; i < 90; i++ {
  194. f := r.NextFrame()
  195. v := float64(f.L)
  196. if i > 5 && v < prev-0.001 {
  197. t.Fatalf("sample %d: non-monotonic %.4f < %.4f", i, v, prev)
  198. }
  199. if v > 0 {
  200. prev = v
  201. }
  202. }
  203. // Final value should be close to 0.9 (we wrote 0..0.99)
  204. if prev < 0.5 {
  205. t.Fatalf("final value %.4f too low (expected > 0.5)", prev)
  206. }
  207. }
  208. func TestStreamResampler_Upsample(t *testing.T) {
  209. // 44100 → 228000 (ratio ≈ 0.1934, ~5.17× upsampling)
  210. s := NewStreamSource(4096, 44100)
  211. r := NewStreamResampler(s, 228000)
  212. // Write 1000 frames of a 1kHz sine at 44100 Hz
  213. for i := 0; i < 1000; i++ {
  214. v := math.Sin(2 * math.Pi * 1000 * float64(i) / 44100)
  215. s.WriteFrame(NewFrame(Sample(v), Sample(v)))
  216. }
  217. // Read upsampled output — should be ~5170 samples for 1000 input
  218. // (minus a few for resampler priming)
  219. out := make([]float64, 0, 5200)
  220. for i := 0; i < 5000; i++ {
  221. f := r.NextFrame()
  222. out = append(out, float64(f.L))
  223. }
  224. // Verify the output is a smooth sine, not clicks or zeros
  225. // Check that max amplitude is close to 1.0
  226. maxAmp := 0.0
  227. for _, v := range out[100:] { // skip initial ramp
  228. if math.Abs(v) > maxAmp {
  229. maxAmp = math.Abs(v)
  230. }
  231. }
  232. if maxAmp < 0.8 {
  233. t.Fatalf("max amplitude %.4f too low (expected ~1.0)", maxAmp)
  234. }
  235. // Check smoothness: no sudden jumps > 0.1 between adjacent samples
  236. maxJump := 0.0
  237. for i := 101; i < len(out); i++ {
  238. d := math.Abs(out[i] - out[i-1])
  239. if d > maxJump {
  240. maxJump = d
  241. }
  242. }
  243. // At 228kHz with 1kHz tone: max step ≈ sin(2π*1000/228000) ≈ 0.0276
  244. if maxJump > 0.05 {
  245. t.Fatalf("max inter-sample jump %.4f (expected < 0.05 for smooth sine)", maxJump)
  246. }
  247. }
  248. func TestStreamResampler_Downsample(t *testing.T) {
  249. // 96000 → 44100 (ratio ≈ 2.177, downsampling)
  250. s := NewStreamSource(8192, 96000)
  251. r := NewStreamResampler(s, 44100)
  252. // Write 4000 frames at 96kHz
  253. for i := 0; i < 4000; i++ {
  254. v := math.Sin(2 * math.Pi * 440 * float64(i) / 96000)
  255. s.WriteFrame(NewFrame(Sample(v), 0))
  256. }
  257. // Should get ~1837 output frames (4000 × 44100/96000)
  258. count := 0
  259. for i := 0; i < 1800; i++ {
  260. f := r.NextFrame()
  261. _ = f
  262. count++
  263. }
  264. if count != 1800 {
  265. t.Fatalf("expected 1800 reads, got %d", count)
  266. }
  267. }
  268. func TestStreamResampler_NilSource(t *testing.T) {
  269. r := NewStreamResampler(nil, 228000)
  270. f := r.NextFrame()
  271. if f.L != 0 || f.R != 0 {
  272. t.Fatal("expected silence from nil source")
  273. }
  274. }
  275. // --- IngestReader test ---
  276. func TestIngestReader(t *testing.T) {
  277. s := NewStreamSource(4096, 44100)
  278. // Create PCM data: 100 stereo frames
  279. var buf bytes.Buffer
  280. for i := 0; i < 100; i++ {
  281. l := int16(i * 100)
  282. r := int16(-i * 100)
  283. binary.Write(&buf, binary.LittleEndian, l)
  284. binary.Write(&buf, binary.LittleEndian, r)
  285. }
  286. // IngestReader should read all data and return nil (EOF)
  287. err := IngestReader(bytes.NewReader(buf.Bytes()), s)
  288. if err != nil {
  289. t.Fatalf("IngestReader: %v", err)
  290. }
  291. if s.Available() != 100 {
  292. t.Fatalf("expected 100 frames, got %d", s.Available())
  293. }
  294. // Verify first and last
  295. f := s.ReadFrame()
  296. if f.L != 0 {
  297. t.Fatalf("frame 0 L=%.4f, expected 0", f.L)
  298. }
  299. for i := 1; i < 99; i++ {
  300. s.ReadFrame()
  301. }
  302. f = s.ReadFrame()
  303. expectedL := 9900.0 / 32768.0
  304. if math.Abs(float64(f.L)-expectedL) > 0.01 {
  305. t.Fatalf("frame 99 L=%.4f, expected ~%.4f", f.L, expectedL)
  306. }
  307. }
  308. func TestIngestReader_Error(t *testing.T) {
  309. s := NewStreamSource(256, 44100)
  310. errReader := &errAfterN{n: 10}
  311. err := IngestReader(errReader, s)
  312. if err == nil {
  313. t.Fatal("expected error")
  314. }
  315. }
  316. type errAfterN struct {
  317. n, count int
  318. }
  319. func (r *errAfterN) Read(p []byte) (int, error) {
  320. if r.count >= r.n {
  321. return 0, io.ErrUnexpectedEOF
  322. }
  323. r.count++
  324. // Return 4 bytes (one stereo frame)
  325. if len(p) >= 4 {
  326. p[0], p[1], p[2], p[3] = 0, 0, 0, 0
  327. return 4, nil
  328. }
  329. return 0, nil
  330. }