Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

273 Zeilen
9.2KB

  1. package rds
  2. import (
  3. "math"
  4. "strings"
  5. "testing"
  6. "time"
  7. )
  8. func TestCRC10KnownVector(t *testing.T) {
  9. c := crc10(0x1234)
  10. if c > 0x3FF { t.Fatalf("CRC exceeds 10 bits: %x", c) }
  11. }
  12. func TestEncodeBlockProduces26Bits(t *testing.T) {
  13. block := encodeBlock(0x1234, 'A')
  14. if block>>26 != 0 { t.Fatalf("block exceeds 26 bits: %x", block) }
  15. if uint16(block>>10) != 0x1234 { t.Fatalf("data mismatch") }
  16. }
  17. func TestBuildGroup0A(t *testing.T) {
  18. cfg := &RDSConfig{PI: 0x1234, PS: "TESTFM"}
  19. g := buildGroup0A(cfg, 0, [2]uint8{})
  20. if g[0] != 0x1234 { t.Fatalf("block A not PI: %x", g[0]) }
  21. if byte(g[3]>>8) != 'T' || byte(g[3]&0xFF) != 'E' { t.Fatal("wrong PS chars") }
  22. }
  23. func TestBuildGroup2A(t *testing.T) {
  24. g := buildGroup2A(0x1234, 0, false, false, 0, "Hello World")
  25. if g[0] != 0x1234 { t.Fatal("block A not PI") }
  26. if (g[1]>>12)&0x0F != 2 { t.Fatal("wrong group type") }
  27. }
  28. func TestBuildGroupUsesConfiguredPI(t *testing.T) {
  29. cfg0A := &RDSConfig{PI: 0xBEEF, PS: "TEST"}
  30. if buildGroup0A(cfg0A, 0, [2]uint8{})[0] != 0xBEEF { t.Fatal("PI mismatch 0A") }
  31. if buildGroup2A(0xCAFE, 0, false, false, 0, "Hello")[0] != 0xCAFE { t.Fatal("PI mismatch 2A") }
  32. }
  33. func TestEncoderGenerate(t *testing.T) {
  34. cfg := DefaultConfig(); cfg.SampleRate = 228000
  35. enc, err := NewEncoder(cfg)
  36. if err != nil { t.Fatal(err) }
  37. samples := enc.Generate(1024)
  38. if len(samples) != 1024 { t.Fatal("wrong length") }
  39. var energy, maxAbs float64
  40. for _, s := range samples {
  41. energy += s * s
  42. if math.Abs(s) > maxAbs { maxAbs = math.Abs(s) }
  43. }
  44. if energy == 0 { t.Fatal("zero energy") }
  45. // Unity output: peak should be close to 1.0
  46. if maxAbs > 3.0 { t.Fatalf("exceeds unity: %.6f", maxAbs) }
  47. }
  48. func TestEncoderNextSample(t *testing.T) {
  49. cfg := DefaultConfig(); cfg.SampleRate = 228000
  50. enc, _ := NewEncoder(cfg)
  51. s := enc.NextSample()
  52. // Should not panic and should produce a value
  53. if math.IsNaN(s) { t.Fatal("NaN") }
  54. }
  55. func TestEncoderReset(t *testing.T) {
  56. cfg := DefaultConfig(); cfg.SampleRate = 228000
  57. enc, _ := NewEncoder(cfg)
  58. a := enc.NextSample()
  59. for i := 0; i < 100; i++ { enc.NextSample() }
  60. enc.Reset()
  61. b := enc.NextSample()
  62. if math.Abs(a-b) > 1e-9 { t.Fatalf("reset failed: %v vs %v", a, b) }
  63. }
  64. func TestGroupSchedulerCycles(t *testing.T) {
  65. cfg := DefaultConfig(); cfg.PS = "TESTPS"; cfg.RT = "short"
  66. gs := newGroupScheduler(cfg)
  67. for i := 0; i < 40; i++ { _ = gs.NextGroup() }
  68. }
  69. func TestNormalizePS(t *testing.T) {
  70. if normalizePS("radiox") != "RADIOX " { t.Fatal("wrong PS") }
  71. }
  72. func TestNormalizeRT(t *testing.T) {
  73. if len(normalizeRT(strings.Repeat("a", 80))) != 64 { t.Fatal("wrong RT length") }
  74. }
  75. func TestRTSegmentCount(t *testing.T) {
  76. if rtSegmentCount("Hi") != 1 { t.Fatal("expected 1") }
  77. if rtSegmentCount("Hello World!") != 3 { t.Fatal("expected 3") }
  78. if rtSegmentCount(strings.Repeat("x", 64)) != 16 { t.Fatal("expected 16") }
  79. }
  80. // --- New group tests ---
  81. func TestParseRTPlus(t *testing.T) {
  82. t1, t2, has2 := ParseRTPlus("Depeche Mode - Enjoy The Silence", " - ")
  83. if !has2 { t.Fatal("expected 2 tags") }
  84. if t1.ContentType != RTPlusItemArtist { t.Fatal("tag1 should be artist") }
  85. if t2.ContentType != RTPlusItemTitle { t.Fatal("tag2 should be title") }
  86. if string(normalizeRT("Depeche Mode - Enjoy The Silence")[t1.Start:t1.Start+t1.Length]) != "Depeche Mode" {
  87. t.Fatalf("artist mismatch: start=%d len=%d", t1.Start, t1.Length)
  88. }
  89. if string(normalizeRT("Depeche Mode - Enjoy The Silence")[t2.Start:t2.Start+t2.Length]) != "Enjoy The Silence" {
  90. t.Fatalf("title mismatch: start=%d len=%d", t2.Start, t2.Length)
  91. }
  92. }
  93. func TestParseRTPlusNoSeparator(t *testing.T) {
  94. t1, _, has2 := ParseRTPlus("Just a station message", " - ")
  95. if has2 { t.Fatal("expected 1 tag") }
  96. if t1.ContentType != RTPlusItemTitle { t.Fatal("should be title") }
  97. if t1.Length != 22 { t.Fatalf("wrong length: %d", t1.Length) }
  98. }
  99. func TestAFEncoding(t *testing.T) {
  100. c := freqToAF(87.6)
  101. if c != 1 { t.Fatalf("87.6 MHz should be AF code 1, got %d", c) }
  102. c = freqToAF(107.9)
  103. if c != 204 { t.Fatalf("107.9 MHz should be AF code 204, got %d", c) }
  104. c = freqToAF(100.0)
  105. if c != 125 { t.Fatalf("100.0 MHz should be AF code 125, got %d", c) }
  106. }
  107. func TestAFListPairs(t *testing.T) {
  108. pairs := buildAFList([]float64{93.3, 95.7, 99.1})
  109. if len(pairs) != 2 { t.Fatalf("expected 2 AF pairs, got %d", len(pairs)) }
  110. // First pair: count indicator + first AF
  111. if pairs[0][0] != 224+3 { t.Fatalf("expected count indicator 227, got %d", pairs[0][0]) }
  112. }
  113. func TestBuildGroup4A(t *testing.T) {
  114. // Known date: 2026-04-11 14:30 UTC, offset +2 (CEST)
  115. tm := time.Date(2026, 4, 11, 14, 30, 0, 0, time.UTC)
  116. g := buildGroup4A(0x1234, 0, false, tm, 4)
  117. if g[0] != 0x1234 { t.Fatal("PI mismatch") }
  118. if (g[1]>>12)&0xF != 4 { t.Fatal("wrong group type") }
  119. // Just verify it doesn't panic and produces non-zero blocks
  120. if g[2] == 0 && g[3] == 0 { t.Fatal("CT blocks are zero") }
  121. }
  122. func TestBuildGroup10A(t *testing.T) {
  123. g := buildGroup10A(0x1234, 10, false, false, 0, "INDIE")
  124. if g[0] != 0x1234 { t.Fatal("PI mismatch") }
  125. if (g[1]>>12)&0xF != 0xA { t.Fatal("wrong group type") }
  126. if byte(g[2]>>8) != 'I' || byte(g[2]&0xFF) != 'N' { t.Fatal("wrong PTYN chars seg 0") }
  127. }
  128. func TestFullSchedulerAllGroups(t *testing.T) {
  129. cfg := DefaultConfig()
  130. cfg.PS = "TESTPS"
  131. cfg.RT = "Artist - Title"
  132. cfg.PTYN = "ROCK"
  133. cfg.CTEnabled = true
  134. cfg.RTPlusEnabled = true
  135. cfg.RTPlusSeparator = " - "
  136. cfg.AF = []float64{93.3, 95.7}
  137. cfg.EON = []EONEntry{{PI: 0x5678, PS: "OTHER", AF: []float64{88.0}}}
  138. gs := newGroupScheduler(cfg)
  139. // Run 200 groups — should cover all types without panic
  140. seen := map[int]bool{}
  141. for i := 0; i < 200; i++ {
  142. g := gs.NextGroup()
  143. groupType := int((g[1] >> 12) & 0xF)
  144. seen[groupType] = true
  145. }
  146. // Verify all expected group types were seen
  147. for _, gt := range []int{0, 2, 3, 4, 0xA, 0xB, 0xE} {
  148. if !seen[gt] {
  149. t.Errorf("group type %d never scheduled in 200 groups", gt)
  150. }
  151. }
  152. t.Logf("Seen group types: %v", seen)
  153. }
  154. func TestBuildGroup15A(t *testing.T) {
  155. lps := []byte("Mike-Be Radio")
  156. g := buildGroup15A(0x1234, 10, false, false, 0, lps)
  157. if g[0] != 0x1234 { t.Fatal("PI mismatch") }
  158. if (g[1]>>12)&0xF != 0xF { t.Fatalf("wrong group type: %x", (g[1]>>12)&0xF) }
  159. // Segment 0: first 2 bytes in block C, next 2 in block D
  160. if byte(g[2]>>8) != 'M' || byte(g[2]&0xFF) != 'i' { t.Fatalf("wrong LPS chars C: %x", g[2]) }
  161. if byte(g[3]>>8) != 'k' || byte(g[3]&0xFF) != 'e' { t.Fatalf("wrong LPS chars D: %x", g[3]) }
  162. }
  163. func TestLPSSegmentCount(t *testing.T) {
  164. if lpsSegmentCount([]byte("Hi")) != 1 { t.Fatal("expected 1") }
  165. if lpsSegmentCount([]byte("Hello World!")) != 4 { t.Fatal("expected 4") } // 12+1=13 bytes, /4=4
  166. if lpsSegmentCount([]byte("Mike-Be Radio Live Stream")) != 7 { t.Fatalf("expected 7, got %d", lpsSegmentCount([]byte("Mike-Be Radio Live Stream"))) }
  167. }
  168. func TestFullSchedulerWithLPS(t *testing.T) {
  169. cfg := DefaultConfig()
  170. cfg.PS = "TESTPS"
  171. cfg.RT = "Artist - Title"
  172. cfg.LPS = "Mike-Be Radio Live"
  173. cfg.PTYN = "ROCK"
  174. cfg.CTEnabled = true
  175. cfg.RTPlusEnabled = true
  176. cfg.RTPlusSeparator = " - "
  177. cfg.AF = []float64{93.3, 95.7}
  178. gs := newGroupScheduler(cfg)
  179. seen := map[int]bool{}
  180. for i := 0; i < 300; i++ {
  181. g := gs.NextGroup()
  182. groupType := int((g[1] >> 12) & 0xF)
  183. seen[groupType] = true
  184. }
  185. // Verify LPS (group type 15 = 0xF) was scheduled
  186. if !seen[0xF] {
  187. t.Error("group type 15A (LPS) never scheduled")
  188. }
  189. t.Logf("Seen group types: %v", seen)
  190. }
  191. func TestBuildGroupERT(t *testing.T) {
  192. ert := []byte("Привет мир") // Russian "Hello world" in UTF-8
  193. g := buildGroupERT(0xD314, 10, false, 12, false, 0, ert)
  194. if g[0] != 0xD314 { t.Fatal("PI mismatch") }
  195. if (g[1]>>12)&0xF != 12 { t.Fatalf("wrong group type: %d", (g[1]>>12)&0xF) }
  196. // First 2 bytes of UTF-8 "Привет" in block C
  197. if g[2] != uint16(ert[0])<<8|uint16(ert[1]) { t.Fatalf("wrong eRT chars: %x vs %x", g[2], uint16(ert[0])<<8|uint16(ert[1])) }
  198. }
  199. func TestERTSegmentCount(t *testing.T) {
  200. if ertSegmentCount([]byte("Hi")) != 1 { t.Fatal("expected 1") }
  201. // 20 bytes Russian text + 1 terminator = 21, /4 = 6
  202. ert := []byte("Привет мир") // 19 bytes UTF-8
  203. n := ertSegmentCount(ert)
  204. if n != 5 { t.Fatalf("expected 5, got %d (len=%d)", n, len(ert)) }
  205. }
  206. func TestGroupC(t *testing.T) {
  207. gc := GroupC{
  208. FH: 0x80, // FuncID=2 (RFT), FuncNum=0
  209. Data: [7]byte{0x00, 0x00, 0x00, 0x05, 0x00, 0x89, 0x50},
  210. }
  211. blocks := buildGroupC(gc)
  212. if blocks[0] != 0x8000 { t.Fatalf("block 0: %04x", blocks[0]) } // FH<<8 | data[0]
  213. if blocks[1] != 0x0000 { t.Fatalf("block 1: %04x", blocks[1]) } // data[1]<<8 | data[2]
  214. }
  215. func TestRFTSegmentation(t *testing.T) {
  216. // Small test file: 20 bytes
  217. data := make([]byte, 20)
  218. for i := range data { data[i] = byte(i) }
  219. rft := SegmentFile(data, 3, RFTFileTypePNG)
  220. if rft.FileID != 3 { t.Fatal("wrong fileID") }
  221. if rft.Total < 2 { t.Fatalf("expected >= 2 segments, got %d", rft.Total) }
  222. // First segment has header
  223. if rft.Segments[0].FH != (FuncIDRFT<<6)|3 { t.Fatalf("wrong FH: %02x", rft.Segments[0].FH) }
  224. t.Logf("RFT: %d bytes → %d segments", len(data), rft.Total)
  225. }
  226. func TestRDS2Encoder(t *testing.T) {
  227. enc := NewRDS2Encoder(228000)
  228. enc.Enable(true)
  229. // Generate some samples
  230. var sum float64
  231. for i := 0; i < 1000; i++ {
  232. sum += enc.NextSample()
  233. }
  234. // With no groups fed, output should be near zero (just carrier × empty envelope)
  235. t.Logf("RDS2 1000 samples sum: %f", sum)
  236. }