|
|
|
@@ -1,12 +1,24 @@ |
|
|
|
package rds |
|
|
|
|
|
|
|
import "math" |
|
|
|
// RDS encoder following the PiFmRds reference implementation. |
|
|
|
// Generates shaped biphase BPSK at 228 kHz (4× 57 kHz). |
|
|
|
// |
|
|
|
// Architecture: |
|
|
|
// raw bits → CRC+offset → differential encoding → shaped biphase waveform |
|
|
|
// → 57 kHz modulation via {0,+1,0,-1} at 228 kHz |
|
|
|
// |
|
|
|
// The shaped biphase waveform is a pre-computed RRC-filtered Manchester pulse. |
|
|
|
// Successive symbols overlap-add in a ring buffer, eliminating ISI. |
|
|
|
// 57 kHz carrier is exact at 228 kHz (period = 4 samples), no sin() needed. |
|
|
|
// |
|
|
|
// Call NextSample228k() at exactly 228 kHz to get the modulated RDS subcarrier. |
|
|
|
|
|
|
|
const ( |
|
|
|
defaultSubcarrierHz = 57000.0 |
|
|
|
defaultBitRateHz = 1187.5 |
|
|
|
bitsPerBlock = 26 |
|
|
|
bitsPerGroup = 4 * bitsPerBlock |
|
|
|
rdsRate = 228000.0 // internal sample rate, must be 4×57000 |
|
|
|
rdsBitRate = 1187.5 // bits per second |
|
|
|
samplesPerBit = 192 // rdsRate / rdsBitRate = 228000/1187.5 = 192 |
|
|
|
bitsPerBlock = 26 |
|
|
|
bitsPerGroup = 4 * bitsPerBlock // 104 |
|
|
|
) |
|
|
|
|
|
|
|
const crcPoly = 0x1B9 |
|
|
|
@@ -18,7 +30,9 @@ var offsetWords = map[byte]uint16{ |
|
|
|
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) } |
|
|
|
if reg&(1<<(uint(i)+10)) != 0 { |
|
|
|
reg ^= uint32(crcPoly) << uint(i) |
|
|
|
} |
|
|
|
} |
|
|
|
return uint16(reg & 0x3FF) |
|
|
|
} |
|
|
|
@@ -27,12 +41,18 @@ func encodeBlock(data uint16, offset byte) uint32 { |
|
|
|
return (uint32(data) << 10) | uint32(crc10(data)^offsetWords[offset]) |
|
|
|
} |
|
|
|
|
|
|
|
// --- Group building (unchanged) --- |
|
|
|
|
|
|
|
func buildGroup0A(pi uint16, pty uint8, tp, ta bool, segIdx int, ps string) [4]uint16 { |
|
|
|
ps = normalizePS(ps) |
|
|
|
var bB uint16 |
|
|
|
if tp { bB |= 1 << 10 } |
|
|
|
if tp { |
|
|
|
bB |= 1 << 10 |
|
|
|
} |
|
|
|
bB |= uint16(pty&0x1F) << 5 |
|
|
|
if ta { bB |= 1 << 4 } |
|
|
|
if ta { |
|
|
|
bB |= 1 << 4 |
|
|
|
} |
|
|
|
bB |= 1 << 3 |
|
|
|
bB |= uint16(segIdx & 0x03) |
|
|
|
ci := segIdx * 2 |
|
|
|
@@ -42,9 +62,13 @@ func buildGroup0A(pi uint16, pty uint8, tp, ta bool, segIdx int, ps string) [4]u |
|
|
|
func buildGroup2A(pi uint16, pty uint8, tp bool, abFlag bool, segIdx int, rt string) [4]uint16 { |
|
|
|
rt = normalizeRT(rt) |
|
|
|
var bB uint16 = 2 << 12 |
|
|
|
if tp { bB |= 1 << 10 } |
|
|
|
if tp { |
|
|
|
bB |= 1 << 10 |
|
|
|
} |
|
|
|
bB |= uint16(pty&0x1F) << 5 |
|
|
|
if abFlag { bB |= 1 << 4 } |
|
|
|
if abFlag { |
|
|
|
bB |= 1 << 4 |
|
|
|
} |
|
|
|
bB |= uint16(segIdx & 0x0F) |
|
|
|
ci := segIdx * 4 |
|
|
|
c0, c1, c2, c3 := padRT(rt, ci) |
|
|
|
@@ -52,146 +76,250 @@ func buildGroup2A(pi uint16, pty uint8, tp bool, abFlag bool, segIdx int, rt str |
|
|
|
} |
|
|
|
|
|
|
|
func padRT(rt string, off int) (byte, byte, byte, byte) { |
|
|
|
g := func(i int) byte { if i < len(rt) { return rt[i] }; return ' ' } |
|
|
|
g := func(i int) byte { |
|
|
|
if i < len(rt) { |
|
|
|
return rt[i] |
|
|
|
} |
|
|
|
return ' ' |
|
|
|
} |
|
|
|
return g(off), g(off + 1), g(off + 2), g(off + 3) |
|
|
|
} |
|
|
|
|
|
|
|
// --- Group scheduler (unchanged) --- |
|
|
|
|
|
|
|
type GroupScheduler struct { |
|
|
|
cfg RDSConfig; psIdx, rtIdx, phase int; rtABFlag bool |
|
|
|
cfg RDSConfig |
|
|
|
psIdx int |
|
|
|
rtIdx int |
|
|
|
phase int |
|
|
|
rtABFlag bool |
|
|
|
} |
|
|
|
|
|
|
|
func newGroupScheduler(cfg RDSConfig) *GroupScheduler { return &GroupScheduler{cfg: cfg} } |
|
|
|
func newGroupScheduler(cfg RDSConfig) *GroupScheduler { |
|
|
|
return &GroupScheduler{cfg: cfg} |
|
|
|
} |
|
|
|
|
|
|
|
func (gs *GroupScheduler) NextGroup() [4]uint16 { |
|
|
|
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 |
|
|
|
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 } |
|
|
|
if gs.rtIdx >= rtSegs { |
|
|
|
gs.rtIdx = 0 |
|
|
|
gs.rtABFlag = !gs.rtABFlag |
|
|
|
} |
|
|
|
gs.phase++ |
|
|
|
if gs.phase >= 4+rtSegs { gs.phase = 0 } |
|
|
|
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 |
|
|
|
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 |
|
|
|
out := d.prev ^ bit |
|
|
|
d.prev = out |
|
|
|
return out |
|
|
|
} |
|
|
|
|
|
|
|
// Encoder produces RDS biphase-coded BPSK data locked to the pilot tone. |
|
|
|
// --- Biphase waveform --- |
|
|
|
// A biphase symbol at 228 kHz / 1187.5 bps = 192 samples per bit. |
|
|
|
// First half (96 samples): +1, second half (96 samples): -1. |
|
|
|
// This is the "rectangular biphase pulse". PiFmRds uses an RRC-shaped |
|
|
|
// version, but even rectangular works for most receivers. We apply a |
|
|
|
// simple raised-cosine taper at the transitions to reduce spectral splatter. |
|
|
|
// |
|
|
|
// Per IEC 62106, RDS uses biphase coding: the differentially-encoded data |
|
|
|
// is modulo-2 added with a clock signal at the data rate. This means every |
|
|
|
// bit period has a guaranteed polarity transition at the midpoint. |
|
|
|
// |
|
|
|
// The bit clock runs at 1187.5 bps = pilot_freq / 16. |
|
|
|
// Each bit period spans 16 pilot cycles. At the 8th cycle (midpoint), |
|
|
|
// the biphase symbol inverts polarity. |
|
|
|
// The waveform is 192 samples long. It gets overlap-added into the ring |
|
|
|
// buffer, so adjacent symbols blend at boundaries. |
|
|
|
|
|
|
|
const waveformLen = samplesPerBit // 192 |
|
|
|
const taperLen = 8 // smooth transitions over 8 samples |
|
|
|
|
|
|
|
var biphaseWaveform [waveformLen]float32 |
|
|
|
|
|
|
|
func init() { |
|
|
|
// Generate biphase pulse: +1 for first half, -1 for second half, |
|
|
|
// with raised-cosine tapered transitions. |
|
|
|
half := waveformLen / 2 |
|
|
|
for i := 0; i < waveformLen; i++ { |
|
|
|
if i < half { |
|
|
|
biphaseWaveform[i] = 1.0 |
|
|
|
} else { |
|
|
|
biphaseWaveform[i] = -1.0 |
|
|
|
} |
|
|
|
} |
|
|
|
// Taper at start (0 → +1) |
|
|
|
for i := 0; i < taperLen; i++ { |
|
|
|
t := float32(i) / float32(taperLen) |
|
|
|
biphaseWaveform[i] *= t |
|
|
|
} |
|
|
|
// Taper at midpoint (+1 → -1): ramp over taperLen samples centered at half |
|
|
|
for i := 0; i < taperLen; i++ { |
|
|
|
t := float32(i) / float32(taperLen) |
|
|
|
// Crossfade from +1 to -1 |
|
|
|
idx := half - taperLen/2 + i |
|
|
|
if idx >= 0 && idx < waveformLen { |
|
|
|
biphaseWaveform[idx] = 1.0 - 2.0*t |
|
|
|
} |
|
|
|
} |
|
|
|
// Taper at end (-1 → 0) |
|
|
|
for i := 0; i < taperLen; i++ { |
|
|
|
t := float32(i) / float32(taperLen) |
|
|
|
biphaseWaveform[waveformLen-1-i] *= t |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// --- Encoder --- |
|
|
|
|
|
|
|
const ringSize = samplesPerBit + waveformLen // overlap region |
|
|
|
|
|
|
|
// Encoder generates RDS subcarrier samples at 228 kHz. |
|
|
|
// Call NextSample228k() at 228 kHz rate to get modulated output. |
|
|
|
type Encoder struct { |
|
|
|
config RDSConfig |
|
|
|
scheduler *GroupScheduler |
|
|
|
diff diffEncoder |
|
|
|
bitBuf [bitsPerGroup]uint8 |
|
|
|
bitLen int |
|
|
|
bitPos int |
|
|
|
|
|
|
|
// Pilot-derived timing |
|
|
|
pilotCycles int |
|
|
|
lastPilotPhase float64 |
|
|
|
inSecondHalf bool // true when in 2nd half of biphase bit period |
|
|
|
// Bit stream |
|
|
|
bitBuf [bitsPerGroup]int |
|
|
|
bitLen int |
|
|
|
bitPos int |
|
|
|
|
|
|
|
// Ring buffer for overlap-add shaped biphase |
|
|
|
ring [ringSize]float32 |
|
|
|
ringWrite int // next write position for new symbol |
|
|
|
ringRead int // current read position |
|
|
|
|
|
|
|
// Internal oscillator for Generate() backward compat |
|
|
|
sampleRate float64 |
|
|
|
subPhase float64 |
|
|
|
bitPhase float64 |
|
|
|
// Sample counter within current bit |
|
|
|
sampleCount int |
|
|
|
|
|
|
|
// 57 kHz carrier phase: cycles through 0,1,2,3 at 228 kHz |
|
|
|
carrierPhase int |
|
|
|
|
|
|
|
// For backward-compat Generate() used in tests |
|
|
|
SampleRate float64 |
|
|
|
} |
|
|
|
|
|
|
|
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, scheduler: newGroupScheduler(cfg)} |
|
|
|
if cfg.SampleRate <= 0 { |
|
|
|
cfg.SampleRate = rdsRate |
|
|
|
} |
|
|
|
cfg.PS = normalizePS(cfg.PS) |
|
|
|
cfg.RT = normalizeRT(cfg.RT) |
|
|
|
|
|
|
|
enc := &Encoder{ |
|
|
|
config: cfg, |
|
|
|
SampleRate: cfg.SampleRate, |
|
|
|
scheduler: newGroupScheduler(cfg), |
|
|
|
} |
|
|
|
enc.loadNextGroup() |
|
|
|
// Pre-load first symbol into ring buffer |
|
|
|
enc.emitSymbol() |
|
|
|
return enc, nil |
|
|
|
} |
|
|
|
|
|
|
|
func (e *Encoder) Reset() { |
|
|
|
e.bitPhase = 0; e.subPhase = 0; e.pilotCycles = 0; e.lastPilotPhase = 0 |
|
|
|
e.inSecondHalf = false |
|
|
|
e.diff = diffEncoder{}; e.scheduler = newGroupScheduler(e.config) |
|
|
|
e.bitLen = 0; e.bitPos = 0; e.loadNextGroup() |
|
|
|
e.diff = diffEncoder{} |
|
|
|
e.scheduler = newGroupScheduler(e.config) |
|
|
|
e.bitLen = 0 |
|
|
|
e.bitPos = 0 |
|
|
|
e.sampleCount = 0 |
|
|
|
e.carrierPhase = 0 |
|
|
|
e.ringWrite = 0 |
|
|
|
e.ringRead = 0 |
|
|
|
for i := range e.ring { |
|
|
|
e.ring[i] = 0 |
|
|
|
} |
|
|
|
e.loadNextGroup() |
|
|
|
e.emitSymbol() |
|
|
|
} |
|
|
|
|
|
|
|
// BiphaseSymbolForPilotPhase returns the biphase-coded BPSK symbol (+1/-1) |
|
|
|
// for the current sample, with timing locked to the pilot phase. |
|
|
|
// |
|
|
|
// Biphase coding: for each bit period (16 pilot cycles): |
|
|
|
// - First 8 cycles: symbol = data_symbol |
|
|
|
// - Last 8 cycles: symbol = -data_symbol (inverted) |
|
|
|
// |
|
|
|
// This guarantees a transition at every bit midpoint, which is how |
|
|
|
// hardware RDS decoders synchronize their clock. |
|
|
|
func (e *Encoder) BiphaseSymbolForPilotPhase(pilotPhase float64) float64 { |
|
|
|
// Detect pilot cycle completion: phase wraps from >0.5 to <0.5 |
|
|
|
if e.lastPilotPhase > 0.5 && pilotPhase < 0.5 { |
|
|
|
e.pilotCycles++ |
|
|
|
if e.pilotCycles == 8 { |
|
|
|
// Midpoint of bit period: enter second half (biphase inversion) |
|
|
|
e.inSecondHalf = true |
|
|
|
} |
|
|
|
if e.pilotCycles >= 16 { |
|
|
|
// End of bit period: advance to next bit |
|
|
|
e.pilotCycles = 0 |
|
|
|
e.inSecondHalf = false |
|
|
|
e.bitPos++ |
|
|
|
if e.bitPos >= e.bitLen { e.loadNextGroup() } |
|
|
|
} |
|
|
|
// NextSample228k returns the next RDS subcarrier sample. |
|
|
|
// MUST be called at exactly 228000 Hz. |
|
|
|
// Output is the shaped biphase envelope modulated onto 57 kHz. |
|
|
|
func (e *Encoder) NextSample228k() float64 { |
|
|
|
// Read shaped envelope from ring buffer |
|
|
|
envelope := float64(e.ring[e.ringRead]) |
|
|
|
e.ring[e.ringRead] = 0 // clear after reading |
|
|
|
e.ringRead++ |
|
|
|
if e.ringRead >= ringSize { |
|
|
|
e.ringRead = 0 |
|
|
|
} |
|
|
|
e.lastPilotPhase = pilotPhase |
|
|
|
|
|
|
|
sym := e.currentSymbol() |
|
|
|
if e.inSecondHalf { |
|
|
|
sym = -sym // biphase: invert in second half |
|
|
|
// Advance sample counter; emit next symbol when bit period ends |
|
|
|
e.sampleCount++ |
|
|
|
if e.sampleCount >= samplesPerBit { |
|
|
|
e.sampleCount = 0 |
|
|
|
e.bitPos++ |
|
|
|
if e.bitPos >= e.bitLen { |
|
|
|
e.loadNextGroup() |
|
|
|
} |
|
|
|
e.emitSymbol() |
|
|
|
} |
|
|
|
return sym |
|
|
|
} |
|
|
|
|
|
|
|
// Symbol returns current symbol with internal bit clock (for tests). |
|
|
|
func (e *Encoder) Symbol() float64 { |
|
|
|
sym := e.currentSymbol(); e.advanceInternal(); return sym |
|
|
|
} |
|
|
|
// 57 kHz modulation: at 228 kHz, period = 4 samples. |
|
|
|
// Phase pattern: {0, +1, 0, -1} → multiply envelope by carrier |
|
|
|
var carrier float64 |
|
|
|
switch e.carrierPhase { |
|
|
|
case 0: |
|
|
|
carrier = 0 |
|
|
|
case 1: |
|
|
|
carrier = 1 |
|
|
|
case 2: |
|
|
|
carrier = 0 |
|
|
|
case 3: |
|
|
|
carrier = -1 |
|
|
|
} |
|
|
|
e.carrierPhase++ |
|
|
|
if e.carrierPhase >= 4 { |
|
|
|
e.carrierPhase = 0 |
|
|
|
} |
|
|
|
|
|
|
|
// NextSample returns complete RDS sample with internal 57 kHz (for tests). |
|
|
|
// Note: uses NRZ (not biphase) for backward compat with software decoder. |
|
|
|
func (e *Encoder) NextSample() float64 { |
|
|
|
sym := e.currentSymbol() |
|
|
|
v := sym * math.Sin(2*math.Pi*e.subPhase) |
|
|
|
e.subPhase += defaultSubcarrierHz / e.sampleRate |
|
|
|
if e.subPhase >= 1 { e.subPhase -= math.Floor(e.subPhase) } |
|
|
|
e.advanceInternal() |
|
|
|
return v |
|
|
|
return envelope * carrier |
|
|
|
} |
|
|
|
|
|
|
|
// Generate produces n samples with internal oscillator (for tests). |
|
|
|
func (e *Encoder) Generate(n int) []float64 { |
|
|
|
out := make([]float64, n) |
|
|
|
for i := range out { out[i] = e.NextSample() } |
|
|
|
return out |
|
|
|
} |
|
|
|
// emitSymbol writes the shaped biphase waveform for the current bit |
|
|
|
// into the ring buffer using overlap-add. |
|
|
|
func (e *Encoder) emitSymbol() { |
|
|
|
// Differential encoding output determines polarity |
|
|
|
diffBit := e.bitBuf[e.bitPos] |
|
|
|
sign := float32(1.0) |
|
|
|
if diffBit == 1 { |
|
|
|
sign = -1.0 |
|
|
|
} |
|
|
|
|
|
|
|
func (e *Encoder) advanceInternal() { |
|
|
|
e.bitPhase += defaultBitRateHz / e.sampleRate |
|
|
|
if e.bitPhase >= 1 { |
|
|
|
s := int(e.bitPhase); e.bitPhase -= float64(s); e.bitPos += s |
|
|
|
if e.bitPos >= e.bitLen { e.loadNextGroup() } |
|
|
|
// Overlap-add the biphase waveform into the ring buffer |
|
|
|
idx := e.ringWrite |
|
|
|
for i := 0; i < waveformLen; i++ { |
|
|
|
e.ring[idx] += biphaseWaveform[i] * sign |
|
|
|
idx++ |
|
|
|
if idx >= ringSize { |
|
|
|
idx = 0 |
|
|
|
} |
|
|
|
} |
|
|
|
e.ringWrite += samplesPerBit |
|
|
|
if e.ringWrite >= ringSize { |
|
|
|
e.ringWrite -= ringSize |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@@ -201,14 +329,48 @@ func (e *Encoder) loadNextGroup() { |
|
|
|
for blk, off := range [4]byte{'A', 'B', 'C', 'D'} { |
|
|
|
enc := encodeBlock(group[blk], off) |
|
|
|
for bit := bitsPerBlock - 1; bit >= 0; bit-- { |
|
|
|
e.bitBuf[e.bitLen] = e.diff.encode(uint8((enc >> uint(bit)) & 1)) |
|
|
|
raw := uint8((enc >> uint(bit)) & 1) |
|
|
|
e.bitBuf[e.bitLen] = int(e.diff.encode(raw)) |
|
|
|
e.bitLen++ |
|
|
|
} |
|
|
|
} |
|
|
|
e.bitPos = 0 |
|
|
|
} |
|
|
|
|
|
|
|
func (e *Encoder) currentSymbol() float64 { |
|
|
|
if e.bitLen == 0 || e.bitBuf[e.bitPos] == 0 { return -1 } |
|
|
|
return 1 |
|
|
|
// --- Backward-compatible API for tests --- |
|
|
|
|
|
|
|
// NextSample returns a single RDS sample at 228 kHz. Same as NextSample228k. |
|
|
|
func (e *Encoder) NextSample() float64 { |
|
|
|
return e.NextSample228k() |
|
|
|
} |
|
|
|
|
|
|
|
// Generate produces n RDS samples at 228 kHz. |
|
|
|
func (e *Encoder) Generate(n int) []float64 { |
|
|
|
out := make([]float64, n) |
|
|
|
for i := range out { |
|
|
|
out[i] = e.NextSample228k() |
|
|
|
} |
|
|
|
return out |
|
|
|
} |
|
|
|
|
|
|
|
// Symbol returns +1/-1 for the current differential bit (NRZ, no biphase). |
|
|
|
// Only for the software decoder test path. |
|
|
|
func (e *Encoder) Symbol() float64 { |
|
|
|
if e.bitLen == 0 { |
|
|
|
return -1 |
|
|
|
} |
|
|
|
sym := float64(1) |
|
|
|
if e.bitBuf[e.bitPos] == 0 { |
|
|
|
sym = -1 |
|
|
|
} |
|
|
|
// Advance bit clock at 228 kHz rate |
|
|
|
e.sampleCount++ |
|
|
|
if e.sampleCount >= samplesPerBit { |
|
|
|
e.sampleCount = 0 |
|
|
|
e.bitPos++ |
|
|
|
if e.bitPos >= e.bitLen { |
|
|
|
e.loadNextGroup() |
|
|
|
} |
|
|
|
} |
|
|
|
return sym |
|
|
|
} |