package rds import ( "math" "strings" "testing" "time" ) func TestCRC10KnownVector(t *testing.T) { c := crc10(0x1234) if c > 0x3FF { t.Fatalf("CRC exceeds 10 bits: %x", c) } } func TestEncodeBlockProduces26Bits(t *testing.T) { block := encodeBlock(0x1234, 'A') if block>>26 != 0 { t.Fatalf("block exceeds 26 bits: %x", block) } if uint16(block>>10) != 0x1234 { t.Fatalf("data mismatch") } } func TestBuildGroup0A(t *testing.T) { cfg := &RDSConfig{PI: 0x1234, PS: "TESTFM"} g := buildGroup0A(cfg, 0, [2]uint8{}) if g[0] != 0x1234 { t.Fatalf("block A not PI: %x", g[0]) } if byte(g[3]>>8) != 'T' || byte(g[3]&0xFF) != 'E' { t.Fatal("wrong PS chars") } } func TestBuildGroup2A(t *testing.T) { g := buildGroup2A(0x1234, 0, false, false, 0, "Hello World") if g[0] != 0x1234 { t.Fatal("block A not PI") } if (g[1]>>12)&0x0F != 2 { t.Fatal("wrong group type") } } func TestBuildGroupUsesConfiguredPI(t *testing.T) { cfg0A := &RDSConfig{PI: 0xBEEF, PS: "TEST"} if buildGroup0A(cfg0A, 0, [2]uint8{})[0] != 0xBEEF { t.Fatal("PI mismatch 0A") } if buildGroup2A(0xCAFE, 0, false, false, 0, "Hello")[0] != 0xCAFE { t.Fatal("PI mismatch 2A") } } func TestEncoderGenerate(t *testing.T) { cfg := DefaultConfig(); cfg.SampleRate = 228000 enc, err := NewEncoder(cfg) if err != nil { t.Fatal(err) } samples := enc.Generate(1024) if len(samples) != 1024 { t.Fatal("wrong length") } var energy, maxAbs float64 for _, s := range samples { energy += s * s if math.Abs(s) > maxAbs { maxAbs = math.Abs(s) } } if energy == 0 { t.Fatal("zero energy") } // Unity output: peak should be close to 1.0 if maxAbs > 3.0 { t.Fatalf("exceeds unity: %.6f", maxAbs) } } func TestEncoderNextSample(t *testing.T) { cfg := DefaultConfig(); cfg.SampleRate = 228000 enc, _ := NewEncoder(cfg) s := enc.NextSample() // Should not panic and should produce a value if math.IsNaN(s) { t.Fatal("NaN") } } func TestEncoderReset(t *testing.T) { cfg := DefaultConfig(); cfg.SampleRate = 228000 enc, _ := NewEncoder(cfg) a := enc.NextSample() for i := 0; i < 100; i++ { enc.NextSample() } enc.Reset() b := enc.NextSample() if math.Abs(a-b) > 1e-9 { t.Fatalf("reset failed: %v vs %v", a, b) } } func TestGroupSchedulerCycles(t *testing.T) { cfg := DefaultConfig(); cfg.PS = "TESTPS"; cfg.RT = "short" gs := newGroupScheduler(cfg) for i := 0; i < 40; i++ { _ = gs.NextGroup() } } func TestNormalizePS(t *testing.T) { if normalizePS("radiox") != "RADIOX " { t.Fatal("wrong PS") } } func TestNormalizeRT(t *testing.T) { if len(normalizeRT(strings.Repeat("a", 80))) != 64 { t.Fatal("wrong RT length") } } func TestRTSegmentCount(t *testing.T) { if rtSegmentCount("Hi") != 1 { t.Fatal("expected 1") } if rtSegmentCount("Hello World!") != 3 { t.Fatal("expected 3") } if rtSegmentCount(strings.Repeat("x", 64)) != 16 { t.Fatal("expected 16") } } // --- New group tests --- func TestParseRTPlus(t *testing.T) { t1, t2, has2 := ParseRTPlus("Depeche Mode - Enjoy The Silence", " - ") if !has2 { t.Fatal("expected 2 tags") } if t1.ContentType != RTPlusItemArtist { t.Fatal("tag1 should be artist") } if t2.ContentType != RTPlusItemTitle { t.Fatal("tag2 should be title") } if string(normalizeRT("Depeche Mode - Enjoy The Silence")[t1.Start:t1.Start+t1.Length]) != "Depeche Mode" { t.Fatalf("artist mismatch: start=%d len=%d", t1.Start, t1.Length) } if string(normalizeRT("Depeche Mode - Enjoy The Silence")[t2.Start:t2.Start+t2.Length]) != "Enjoy The Silence" { t.Fatalf("title mismatch: start=%d len=%d", t2.Start, t2.Length) } } func TestParseRTPlusNoSeparator(t *testing.T) { t1, _, has2 := ParseRTPlus("Just a station message", " - ") if has2 { t.Fatal("expected 1 tag") } if t1.ContentType != RTPlusItemTitle { t.Fatal("should be title") } if t1.Length != 22 { t.Fatalf("wrong length: %d", t1.Length) } } func TestAFEncoding(t *testing.T) { c := freqToAF(87.6) if c != 1 { t.Fatalf("87.6 MHz should be AF code 1, got %d", c) } c = freqToAF(107.9) if c != 204 { t.Fatalf("107.9 MHz should be AF code 204, got %d", c) } c = freqToAF(100.0) if c != 125 { t.Fatalf("100.0 MHz should be AF code 125, got %d", c) } } func TestAFListPairs(t *testing.T) { pairs := buildAFList([]float64{93.3, 95.7, 99.1}) if len(pairs) != 2 { t.Fatalf("expected 2 AF pairs, got %d", len(pairs)) } // First pair: count indicator + first AF if pairs[0][0] != 224+3 { t.Fatalf("expected count indicator 227, got %d", pairs[0][0]) } } func TestBuildGroup4A(t *testing.T) { // Known date: 2026-04-11 14:30 UTC, offset +2 (CEST) tm := time.Date(2026, 4, 11, 14, 30, 0, 0, time.UTC) g := buildGroup4A(0x1234, 0, false, tm, 4) if g[0] != 0x1234 { t.Fatal("PI mismatch") } if (g[1]>>12)&0xF != 4 { t.Fatal("wrong group type") } // Just verify it doesn't panic and produces non-zero blocks if g[2] == 0 && g[3] == 0 { t.Fatal("CT blocks are zero") } } func TestBuildGroup10A(t *testing.T) { g := buildGroup10A(0x1234, 10, false, false, 0, "INDIE") if g[0] != 0x1234 { t.Fatal("PI mismatch") } if (g[1]>>12)&0xF != 0xA { t.Fatal("wrong group type") } if byte(g[2]>>8) != 'I' || byte(g[2]&0xFF) != 'N' { t.Fatal("wrong PTYN chars seg 0") } } func TestFullSchedulerAllGroups(t *testing.T) { cfg := DefaultConfig() cfg.PS = "TESTPS" cfg.RT = "Artist - Title" cfg.PTYN = "ROCK" cfg.CTEnabled = true cfg.RTPlusEnabled = true cfg.RTPlusSeparator = " - " cfg.AF = []float64{93.3, 95.7} cfg.EON = []EONEntry{{PI: 0x5678, PS: "OTHER", AF: []float64{88.0}}} gs := newGroupScheduler(cfg) // Run 200 groups — should cover all types without panic seen := map[int]bool{} for i := 0; i < 200; i++ { g := gs.NextGroup() groupType := int((g[1] >> 12) & 0xF) seen[groupType] = true } // Verify all expected group types were seen for _, gt := range []int{0, 2, 3, 4, 0xA, 0xB, 0xE} { if !seen[gt] { t.Errorf("group type %d never scheduled in 200 groups", gt) } } t.Logf("Seen group types: %v", seen) } func TestBuildGroup15A(t *testing.T) { lps := []byte("Mike-Be Radio") g := buildGroup15A(0x1234, 10, false, false, 0, lps) if g[0] != 0x1234 { t.Fatal("PI mismatch") } if (g[1]>>12)&0xF != 0xF { t.Fatalf("wrong group type: %x", (g[1]>>12)&0xF) } // Segment 0: first 2 bytes in block C, next 2 in block D if byte(g[2]>>8) != 'M' || byte(g[2]&0xFF) != 'i' { t.Fatalf("wrong LPS chars C: %x", g[2]) } if byte(g[3]>>8) != 'k' || byte(g[3]&0xFF) != 'e' { t.Fatalf("wrong LPS chars D: %x", g[3]) } } func TestLPSSegmentCount(t *testing.T) { if lpsSegmentCount([]byte("Hi")) != 1 { t.Fatal("expected 1") } if lpsSegmentCount([]byte("Hello World!")) != 4 { t.Fatal("expected 4") } // 12+1=13 bytes, /4=4 if lpsSegmentCount([]byte("Mike-Be Radio Live Stream")) != 7 { t.Fatalf("expected 7, got %d", lpsSegmentCount([]byte("Mike-Be Radio Live Stream"))) } } func TestFullSchedulerWithLPS(t *testing.T) { cfg := DefaultConfig() cfg.PS = "TESTPS" cfg.RT = "Artist - Title" cfg.LPS = "Mike-Be Radio Live" cfg.PTYN = "ROCK" cfg.CTEnabled = true cfg.RTPlusEnabled = true cfg.RTPlusSeparator = " - " cfg.AF = []float64{93.3, 95.7} gs := newGroupScheduler(cfg) seen := map[int]bool{} for i := 0; i < 300; i++ { g := gs.NextGroup() groupType := int((g[1] >> 12) & 0xF) seen[groupType] = true } // Verify LPS (group type 15 = 0xF) was scheduled if !seen[0xF] { t.Error("group type 15A (LPS) never scheduled") } t.Logf("Seen group types: %v", seen) } func TestBuildGroupERT(t *testing.T) { ert := []byte("Привет мир") // Russian "Hello world" in UTF-8 g := buildGroupERT(0xD314, 10, false, 12, false, 0, ert) if g[0] != 0xD314 { t.Fatal("PI mismatch") } if (g[1]>>12)&0xF != 12 { t.Fatalf("wrong group type: %d", (g[1]>>12)&0xF) } // First 2 bytes of UTF-8 "Привет" in block C 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])) } } func TestERTSegmentCount(t *testing.T) { if ertSegmentCount([]byte("Hi")) != 1 { t.Fatal("expected 1") } // 20 bytes Russian text + 1 terminator = 21, /4 = 6 ert := []byte("Привет мир") // 19 bytes UTF-8 n := ertSegmentCount(ert) if n != 5 { t.Fatalf("expected 5, got %d (len=%d)", n, len(ert)) } } func TestGroupC(t *testing.T) { gc := GroupC{ FH: 0x80, // FuncID=2 (RFT), FuncNum=0 Data: [7]byte{0x00, 0x00, 0x00, 0x05, 0x00, 0x89, 0x50}, } blocks := buildGroupC(gc) if blocks[0] != 0x8000 { t.Fatalf("block 0: %04x", blocks[0]) } // FH<<8 | data[0] if blocks[1] != 0x0000 { t.Fatalf("block 1: %04x", blocks[1]) } // data[1]<<8 | data[2] } func TestRFTSegmentation(t *testing.T) { // Small test file: 20 bytes data := make([]byte, 20) for i := range data { data[i] = byte(i) } rft := SegmentFile(data, 3, RFTFileTypePNG) if rft.FileID != 3 { t.Fatal("wrong fileID") } if rft.Total < 2 { t.Fatalf("expected >= 2 segments, got %d", rft.Total) } // First segment has header if rft.Segments[0].FH != (FuncIDRFT<<6)|3 { t.Fatalf("wrong FH: %02x", rft.Segments[0].FH) } t.Logf("RFT: %d bytes → %d segments", len(data), rft.Total) } func TestRDS2Encoder(t *testing.T) { enc := NewRDS2Encoder(228000) enc.Enable(true) // Generate some samples var sum float64 for i := 0; i < 1000; i++ { sum += enc.NextSample() } // With no groups fed, output should be near zero (just carrier × empty envelope) t.Logf("RDS2 1000 samples sum: %f", sum) }