|
- package rds
-
- import (
- "math"
- )
-
- const (
- defaultSubcarrierHz = 57000.0
- defaultBitRateHz = 1187.5
- defaultAmplitude = 0.05
-
- // Each RDS group has 4 blocks of 26 bits each (16 data + 10 check).
- bitsPerBlock = 26
- bitsPerGroup = 4 * bitsPerBlock // 104
- )
-
- // -----------------------------------------------------------------------
- // CRC / offset words per IEC 62106 / EN 50067
- // -----------------------------------------------------------------------
-
- // Generator polynomial: x^10 + x^8 + x^7 + x^5 + x^4 + x^3 + 1 = 0x1B9
- const crcPoly = 0x1B9
-
- // Offset words for blocks A, B, C, C', D.
- var offsetWords = map[byte]uint16{
- 'A': 0x0FC,
- 'B': 0x198,
- 'C': 0x168,
- 'c': 0x350, // C' for type B groups
- 'D': 0x1B4,
- }
-
- func crc10(data uint16) uint16 {
- var reg uint32 = uint32(data) << 10
- for i := 15; i >= 0; i-- {
- if reg&(1<<(uint(i)+10)) != 0 {
- reg ^= uint32(crcPoly) << uint(i)
- }
- }
- return uint16(reg & 0x3FF)
- }
-
- func encodeBlock(data uint16, offset byte) uint32 {
- check := crc10(data) ^ offsetWords[offset]
- return (uint32(data) << 10) | uint32(check)
- }
-
- // -----------------------------------------------------------------------
- // Group building
- // -----------------------------------------------------------------------
-
- // buildGroup0A creates a type 0A group (basic tuning and PS name).
- // segIdx selects which 2-char segment of the 8-char PS name (0..3).
- func buildGroup0A(pi uint16, pty uint8, tp, ta bool, segIdx int, ps string) [4]uint16 {
- ps = normalizePS(ps)
-
- blockA := pi
-
- // Block B: group type 0A (0000 0), TP, PTY, TA, MS=1, DI=0, segment address
- var blockB uint16
- if tp {
- blockB |= 1 << 10
- }
- blockB |= uint16(pty&0x1F) << 5
- if ta {
- blockB |= 1 << 4
- }
- blockB |= 1 << 3 // MS = music
- blockB |= uint16(segIdx & 0x03)
-
- // Block C: AF (not implemented) – send PI as filler (common practice)
- blockC := pi
-
- // Block D: 2 PS characters
- ci := segIdx * 2
- ch0 := uint16(ps[ci])
- ch1 := uint16(ps[ci+1])
- blockD := (ch0 << 8) | ch1
-
- return [4]uint16{blockA, blockB, blockC, blockD}
- }
-
- // buildGroup2A creates a type 2A group (RadioText).
- // segIdx selects which 4-char segment (0..15) of the 64-char RT.
- func buildGroup2A(pi uint16, pty uint8, tp bool, abFlag bool, segIdx int, rt string) [4]uint16 {
- rt = normalizeRT(rt)
-
- blockA := pi
-
- var blockB uint16
- blockB = 2 << 12 // group type 2
- if tp {
- blockB |= 1 << 10
- }
- blockB |= uint16(pty&0x1F) << 5
- if abFlag {
- blockB |= 1 << 4
- }
- blockB |= uint16(segIdx & 0x0F)
-
- ci := segIdx * 4
- ch0, ch1, ch2, ch3 := padRT(rt, ci)
- blockC := (uint16(ch0) << 8) | uint16(ch1)
- blockD := (uint16(ch2) << 8) | uint16(ch3)
-
- return [4]uint16{blockA, blockB, blockC, blockD}
- }
-
- func padRT(rt string, offset int) (byte, byte, byte, byte) {
- get := func(i int) byte {
- if i < len(rt) {
- return rt[i]
- }
- return ' '
- }
- return get(offset), get(offset + 1), get(offset + 2), get(offset + 3)
- }
-
- // -----------------------------------------------------------------------
- // Group scheduler
- // -----------------------------------------------------------------------
-
- // GroupScheduler cycles through 0A and 2A groups.
- type GroupScheduler struct {
- cfg RDSConfig
- psIdx int
- rtIdx int
- rtABFlag bool
- phase int
- }
-
- func newGroupScheduler(cfg RDSConfig) *GroupScheduler {
- return &GroupScheduler{cfg: cfg}
- }
-
- // NextGroup returns the next RDS group as 4 raw 16-bit words.
- func (gs *GroupScheduler) NextGroup() [4]uint16 {
- // Pattern: 4x 0A (full PS cycle), then N x 2A (full RT cycle), repeat.
- if gs.phase < 4 {
- g := buildGroup0A(gs.cfg.PI, gs.cfg.PTY, gs.cfg.TP, gs.cfg.TA, gs.psIdx, gs.cfg.PS)
- gs.psIdx = (gs.psIdx + 1) % 4
- gs.phase++
- return g
- }
-
- g := buildGroup2A(gs.cfg.PI, gs.cfg.PTY, gs.cfg.TP, gs.rtABFlag, gs.rtIdx, gs.cfg.RT)
- gs.rtIdx++
- rtSegs := rtSegmentCount(gs.cfg.RT)
- if gs.rtIdx >= rtSegs {
- gs.rtIdx = 0
- gs.rtABFlag = !gs.rtABFlag
- }
- gs.phase++
- if gs.phase >= 4+rtSegs {
- gs.phase = 0
- }
- return g
- }
-
- func rtSegmentCount(rt string) int {
- rt = normalizeRT(rt)
- n := (len(rt) + 3) / 4
- if n == 0 {
- n = 1
- }
- if n > 16 {
- n = 16
- }
- return n
- }
-
- // -----------------------------------------------------------------------
- // Differential encoder
- // -----------------------------------------------------------------------
-
- type diffEncoder struct {
- prev uint8
- }
-
- func (d *diffEncoder) encode(bit uint8) uint8 {
- out := d.prev ^ bit
- d.prev = out
- return out
- }
-
- // -----------------------------------------------------------------------
- // Encoder
- // -----------------------------------------------------------------------
-
- // Encoder produces a standards-grade RDS BPSK subcarrier at 57 kHz.
- type Encoder struct {
- config RDSConfig
- sampleRate float64
- amplitude float64
-
- scheduler *GroupScheduler
- diff diffEncoder
-
- bitBuf []uint8
- bitPos int
- bitPhase float64
- subPhase float64
- }
-
- // NewEncoder builds a new encoder for the provided configuration and sample rate.
- func NewEncoder(cfg RDSConfig) (*Encoder, error) {
- if cfg.SampleRate <= 0 {
- cfg.SampleRate = 228000
- }
- cfg.PS = normalizePS(cfg.PS)
- cfg.RT = normalizeRT(cfg.RT)
-
- enc := &Encoder{
- config: cfg,
- sampleRate: cfg.SampleRate,
- amplitude: defaultAmplitude,
- scheduler: newGroupScheduler(cfg),
- }
- enc.loadNextGroup()
- return enc, nil
- }
-
- // Reset restarts the encoder.
- func (e *Encoder) Reset() {
- e.bitPhase = 0
- e.subPhase = 0
- e.diff = diffEncoder{}
- e.scheduler = newGroupScheduler(e.config)
- e.bitBuf = nil
- e.bitPos = 0
- e.loadNextGroup()
- }
-
- // Generate produces the requested number of RDS samples.
- func (e *Encoder) Generate(samples int) []float64 {
- out := make([]float64, samples)
- for i := range out {
- out[i] = e.nextSample()
- }
- return out
- }
-
- func (e *Encoder) loadNextGroup() {
- group := e.scheduler.NextGroup()
- e.bitBuf = make([]uint8, 0, bitsPerGroup)
- offsets := [4]byte{'A', 'B', 'C', 'D'}
- for blk := 0; blk < 4; blk++ {
- encoded := encodeBlock(group[blk], offsets[blk])
- for bit := bitsPerBlock - 1; bit >= 0; bit-- {
- raw := uint8((encoded >> uint(bit)) & 1)
- diffBit := e.diff.encode(raw)
- e.bitBuf = append(e.bitBuf, diffBit)
- }
- }
- e.bitPos = 0
- }
-
- func (e *Encoder) currentSymbol() float64 {
- if len(e.bitBuf) == 0 {
- return 1
- }
- if e.bitBuf[e.bitPos] == 0 {
- return -1
- }
- return 1
- }
-
- func (e *Encoder) nextSample() float64 {
- symbol := e.currentSymbol()
- value := e.amplitude * symbol * math.Sin(2*math.Pi*e.subPhase)
-
- e.subPhase += defaultSubcarrierHz / e.sampleRate
- if e.subPhase >= 1 {
- e.subPhase -= math.Floor(e.subPhase)
- }
-
- e.bitPhase += defaultBitRateHz / e.sampleRate
- if e.bitPhase >= 1 {
- steps := int(e.bitPhase)
- e.bitPhase -= float64(steps)
- e.bitPos += steps
- if e.bitPos >= len(e.bitBuf) {
- e.loadNextGroup()
- }
- }
-
- return value
- }
|