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.

156 line
4.7KB

  1. package audio
  2. import (
  3. "encoding/binary"
  4. "os"
  5. "path/filepath"
  6. "testing"
  7. )
  8. func TestPCM16ToSample(t *testing.T) {
  9. if pcm16ToSample(32767) <= 0 {
  10. t.Fatal("expected positive sample")
  11. }
  12. if pcm16ToSample(-32768) < -1.0 {
  13. t.Fatal("expected clamped lower bound")
  14. }
  15. }
  16. func TestLoadWAVSource(t *testing.T) {
  17. dir := t.TempDir()
  18. path := filepath.Join(dir, "test.wav")
  19. wav := buildMinimalWAV(48000, 1, []int16{0, 32767, -32768, 0})
  20. if err := os.WriteFile(path, wav, 0o644); err != nil {
  21. t.Fatalf("write wav: %v", err)
  22. }
  23. src, err := LoadWAVSource(path)
  24. if err != nil {
  25. t.Fatalf("LoadWAVSource failed: %v", err)
  26. }
  27. if src.SampleRate != 48000 {
  28. t.Fatalf("unexpected sample rate: %d", src.SampleRate)
  29. }
  30. if src.Channels != 1 {
  31. t.Fatalf("unexpected channels: %d", src.Channels)
  32. }
  33. _ = src.NextFrame()
  34. }
  35. func TestLoadWAVWithExtraChunks(t *testing.T) {
  36. dir := t.TempDir()
  37. path := filepath.Join(dir, "extra.wav")
  38. // Build a WAV with a LIST chunk between fmt and data
  39. wav := buildWAVWithExtraChunks(44100, 1, []int16{100, -100})
  40. if err := os.WriteFile(path, wav, 0o644); err != nil {
  41. t.Fatalf("write wav: %v", err)
  42. }
  43. src, err := LoadWAVSource(path)
  44. if err != nil {
  45. t.Fatalf("LoadWAVSource with extra chunks failed: %v", err)
  46. }
  47. if src.SampleRate != 44100 {
  48. t.Fatalf("unexpected sample rate: %d", src.SampleRate)
  49. }
  50. if len(src.frames) != 2 {
  51. t.Fatalf("expected 2 frames, got %d", len(src.frames))
  52. }
  53. }
  54. func TestRejectInvalidWAV(t *testing.T) {
  55. dir := t.TempDir()
  56. path := filepath.Join(dir, "bad.wav")
  57. if err := os.WriteFile(path, []byte("nope"), 0o644); err != nil {
  58. t.Fatalf("write wav: %v", err)
  59. }
  60. if _, err := LoadWAVSource(path); err == nil {
  61. t.Fatal("expected wav load error")
  62. }
  63. }
  64. func TestStereoWAV(t *testing.T) {
  65. dir := t.TempDir()
  66. path := filepath.Join(dir, "stereo.wav")
  67. wav := buildMinimalWAV(48000, 2, []int16{1000, -1000, 2000, -2000})
  68. if err := os.WriteFile(path, wav, 0o644); err != nil {
  69. t.Fatalf("write wav: %v", err)
  70. }
  71. src, err := LoadWAVSource(path)
  72. if err != nil {
  73. t.Fatalf("LoadWAVSource stereo failed: %v", err)
  74. }
  75. if src.Channels != 2 {
  76. t.Fatalf("expected 2 channels, got %d", src.Channels)
  77. }
  78. if len(src.frames) != 2 {
  79. t.Fatalf("expected 2 frames, got %d", len(src.frames))
  80. }
  81. }
  82. // -- helpers --
  83. func buildMinimalWAV(sampleRate int, channels int, samples []int16) []byte {
  84. dataSize := len(samples) * 2
  85. fileSize := 36 + dataSize
  86. buf := make([]byte, 0, 44+dataSize)
  87. buf = append(buf, []byte("RIFF")...)
  88. buf = binary.LittleEndian.AppendUint32(buf, uint32(fileSize))
  89. buf = append(buf, []byte("WAVE")...)
  90. // fmt chunk
  91. buf = append(buf, []byte("fmt ")...)
  92. buf = binary.LittleEndian.AppendUint32(buf, 16)
  93. buf = binary.LittleEndian.AppendUint16(buf, 1) // PCM
  94. buf = binary.LittleEndian.AppendUint16(buf, uint16(channels))
  95. buf = binary.LittleEndian.AppendUint32(buf, uint32(sampleRate))
  96. buf = binary.LittleEndian.AppendUint32(buf, uint32(sampleRate*channels*2)) // byte rate
  97. buf = binary.LittleEndian.AppendUint16(buf, uint16(channels*2)) // block align
  98. buf = binary.LittleEndian.AppendUint16(buf, 16) // bits per sample
  99. // data chunk
  100. buf = append(buf, []byte("data")...)
  101. buf = binary.LittleEndian.AppendUint32(buf, uint32(dataSize))
  102. for _, s := range samples {
  103. buf = binary.LittleEndian.AppendUint16(buf, uint16(s))
  104. }
  105. return buf
  106. }
  107. func buildWAVWithExtraChunks(sampleRate int, channels int, samples []int16) []byte {
  108. dataSize := len(samples) * 2
  109. // Add a fake LIST chunk of 12 bytes between fmt and data
  110. listChunkData := []byte("INFOtest") // 8 bytes
  111. listChunkSize := uint32(len(listChunkData))
  112. totalExtraChunk := 8 + len(listChunkData) // "LIST" + size + data
  113. fileSize := 36 + totalExtraChunk + dataSize
  114. buf := make([]byte, 0, 44+totalExtraChunk+dataSize)
  115. buf = append(buf, []byte("RIFF")...)
  116. buf = binary.LittleEndian.AppendUint32(buf, uint32(fileSize))
  117. buf = append(buf, []byte("WAVE")...)
  118. // fmt chunk
  119. buf = append(buf, []byte("fmt ")...)
  120. buf = binary.LittleEndian.AppendUint32(buf, 16)
  121. buf = binary.LittleEndian.AppendUint16(buf, 1)
  122. buf = binary.LittleEndian.AppendUint16(buf, uint16(channels))
  123. buf = binary.LittleEndian.AppendUint32(buf, uint32(sampleRate))
  124. buf = binary.LittleEndian.AppendUint32(buf, uint32(sampleRate*channels*2))
  125. buf = binary.LittleEndian.AppendUint16(buf, uint16(channels*2))
  126. buf = binary.LittleEndian.AppendUint16(buf, 16)
  127. // LIST chunk (extra, should be skipped)
  128. buf = append(buf, []byte("LIST")...)
  129. buf = binary.LittleEndian.AppendUint32(buf, listChunkSize)
  130. buf = append(buf, listChunkData...)
  131. // data chunk
  132. buf = append(buf, []byte("data")...)
  133. buf = binary.LittleEndian.AppendUint32(buf, uint32(dataSize))
  134. for _, s := range samples {
  135. buf = binary.LittleEndian.AppendUint16(buf, uint16(s))
  136. }
  137. return buf
  138. }