|
|
@@ -1,136 +1,288 @@ |
|
|
package rds |
|
|
package rds |
|
|
|
|
|
|
|
|
import ( |
|
|
import ( |
|
|
"math" |
|
|
|
|
|
|
|
|
"math" |
|
|
) |
|
|
) |
|
|
|
|
|
|
|
|
const ( |
|
|
const ( |
|
|
defaultBitRate = 1187.5 |
|
|
|
|
|
defaultSubcarrier = 57000 |
|
|
|
|
|
defaultAmplitude = 0.02 |
|
|
|
|
|
|
|
|
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 |
|
|
) |
|
|
) |
|
|
|
|
|
|
|
|
// Encoder emits a simple BPSK-like RDS subcarrier stream for offline MPX builds. |
|
|
|
|
|
|
|
|
// ----------------------------------------------------------------------- |
|
|
|
|
|
// 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 { |
|
|
type Encoder struct { |
|
|
config RDSConfig |
|
|
config RDSConfig |
|
|
sampleRate float64 |
|
|
sampleRate float64 |
|
|
bits []float64 |
|
|
|
|
|
bitRate float64 |
|
|
|
|
|
subFreq float64 |
|
|
|
|
|
amplitude float64 |
|
|
amplitude float64 |
|
|
|
|
|
|
|
|
|
|
|
scheduler *GroupScheduler |
|
|
|
|
|
diff diffEncoder |
|
|
|
|
|
|
|
|
|
|
|
bitBuf []uint8 |
|
|
|
|
|
bitPos int |
|
|
bitPhase float64 |
|
|
bitPhase float64 |
|
|
bitIndex int |
|
|
|
|
|
subPhase float64 |
|
|
subPhase float64 |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// NewEncoder builds a new encoder for the provided configuration and sample rate. |
|
|
// NewEncoder builds a new encoder for the provided configuration and sample rate. |
|
|
func NewEncoder(cfg RDSConfig) (*Encoder, error) { |
|
|
func NewEncoder(cfg RDSConfig) (*Encoder, error) { |
|
|
if cfg.SampleRate <= 0 { |
|
|
if cfg.SampleRate <= 0 { |
|
|
cfg.SampleRate = 48000 |
|
|
|
|
|
} |
|
|
|
|
|
cfg.PS = normalizePS(cfg.PS) |
|
|
|
|
|
cfg.RT = normalizeRT(cfg.RT) |
|
|
|
|
|
|
|
|
|
|
|
bits := buildBits(cfg) |
|
|
|
|
|
if len(bits) == 0 { |
|
|
|
|
|
bits = []float64{1} |
|
|
|
|
|
|
|
|
cfg.SampleRate = 228000 |
|
|
} |
|
|
} |
|
|
|
|
|
cfg.PS = normalizePS(cfg.PS) |
|
|
|
|
|
cfg.RT = normalizeRT(cfg.RT) |
|
|
|
|
|
|
|
|
return &Encoder{ |
|
|
|
|
|
|
|
|
enc := &Encoder{ |
|
|
config: cfg, |
|
|
config: cfg, |
|
|
sampleRate: cfg.SampleRate, |
|
|
sampleRate: cfg.SampleRate, |
|
|
bits: bits, |
|
|
|
|
|
bitRate: defaultBitRate, |
|
|
|
|
|
subFreq: defaultSubcarrier, |
|
|
|
|
|
amplitude: defaultAmplitude, |
|
|
amplitude: defaultAmplitude, |
|
|
}, nil |
|
|
|
|
|
|
|
|
scheduler: newGroupScheduler(cfg), |
|
|
|
|
|
} |
|
|
|
|
|
enc.loadNextGroup() |
|
|
|
|
|
return enc, nil |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// Reset restarts the encoder phases so Generate outputs from the beginning of the bit stream again. |
|
|
|
|
|
|
|
|
// Reset restarts the encoder. |
|
|
func (e *Encoder) Reset() { |
|
|
func (e *Encoder) Reset() { |
|
|
e.bitPhase = 0 |
|
|
e.bitPhase = 0 |
|
|
e.bitIndex = 0 |
|
|
|
|
|
e.subPhase = 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. |
|
|
// Generate produces the requested number of RDS samples. |
|
|
func (e *Encoder) Generate(samples int) []float64 { |
|
|
func (e *Encoder) Generate(samples int) []float64 { |
|
|
out := make([]float64, samples) |
|
|
out := make([]float64, samples) |
|
|
if len(e.bits) == 0 || samples == 0 { |
|
|
|
|
|
return out |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
for i := 0; i < samples; i++ { |
|
|
|
|
|
|
|
|
for i := range out { |
|
|
out[i] = e.nextSample() |
|
|
out[i] = e.nextSample() |
|
|
} |
|
|
} |
|
|
return out |
|
|
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 { |
|
|
func (e *Encoder) nextSample() float64 { |
|
|
symbol := e.bits[e.bitIndex] |
|
|
|
|
|
|
|
|
symbol := e.currentSymbol() |
|
|
value := e.amplitude * symbol * math.Sin(2*math.Pi*e.subPhase) |
|
|
value := e.amplitude * symbol * math.Sin(2*math.Pi*e.subPhase) |
|
|
e.subPhase += e.subFreq / e.sampleRate |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
e.subPhase += defaultSubcarrierHz / e.sampleRate |
|
|
if e.subPhase >= 1 { |
|
|
if e.subPhase >= 1 { |
|
|
e.subPhase -= math.Floor(e.subPhase) |
|
|
e.subPhase -= math.Floor(e.subPhase) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
e.bitPhase += e.bitRate / e.sampleRate |
|
|
|
|
|
|
|
|
e.bitPhase += defaultBitRateHz / e.sampleRate |
|
|
if e.bitPhase >= 1 { |
|
|
if e.bitPhase >= 1 { |
|
|
steps := int(e.bitPhase) |
|
|
steps := int(e.bitPhase) |
|
|
e.bitIndex = (e.bitIndex + steps) % len(e.bits) |
|
|
|
|
|
e.bitPhase -= float64(steps) |
|
|
e.bitPhase -= float64(steps) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return value |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func buildBits(cfg RDSConfig) []float64 { |
|
|
|
|
|
var bits []float64 |
|
|
|
|
|
bits = append(bits, wordToBits(cfg.PI)...) |
|
|
|
|
|
status := uint8(cfg.PTY&0x1F) | boolToBit(cfg.TP)<<7 | boolToBit(cfg.TA)<<6 |
|
|
|
|
|
bits = append(bits, byteToBits(status)...) |
|
|
|
|
|
bits = append(bits, stringToBits(cfg.PS)...) |
|
|
|
|
|
bits = append(bits, stringToBits(cfg.RT)...) |
|
|
|
|
|
return bits |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func wordToBits(word uint16) []float64 { |
|
|
|
|
|
bits := make([]float64, 0, 16) |
|
|
|
|
|
for i := 15; i >= 0; i-- { |
|
|
|
|
|
bits = append(bits, bitToSymbol(uint8((word>>i)&1))) |
|
|
|
|
|
} |
|
|
|
|
|
return bits |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func byteToBits(b uint8) []float64 { |
|
|
|
|
|
bits := make([]float64, 0, 8) |
|
|
|
|
|
for i := 7; i >= 0; i-- { |
|
|
|
|
|
bits = append(bits, bitToSymbol(uint8((b>>i)&1))) |
|
|
|
|
|
} |
|
|
|
|
|
return bits |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func stringToBits(text string) []float64 { |
|
|
|
|
|
bits := make([]float64, 0, len(text)*8) |
|
|
|
|
|
for i := 0; i < len(text); i++ { |
|
|
|
|
|
for bit := 7; bit >= 0; bit-- { |
|
|
|
|
|
bits = append(bits, bitToSymbol(uint8((text[i]>>bit)&1))) |
|
|
|
|
|
|
|
|
e.bitPos += steps |
|
|
|
|
|
if e.bitPos >= len(e.bitBuf) { |
|
|
|
|
|
e.loadNextGroup() |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
return bits |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func bitToSymbol(bit uint8) float64 { |
|
|
|
|
|
if bit == 0 { |
|
|
|
|
|
return -1 |
|
|
|
|
|
} |
|
|
|
|
|
return 1 |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func boolToBit(value bool) uint8 { |
|
|
|
|
|
if value { |
|
|
|
|
|
return 1 |
|
|
|
|
|
} |
|
|
|
|
|
return 0 |
|
|
|
|
|
|
|
|
return value |
|
|
} |
|
|
} |