From b0964e71dc84d36a6e43e38ce15174a2600ebdb3 Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 7 Apr 2026 22:07:19 +0200 Subject: [PATCH] rds: support explicit text clearing and symbol bootstrap --- internal/rds/encoder.go | 69 +++++++++++++++++++++++++++++++---------- 1 file changed, 53 insertions(+), 16 deletions(-) diff --git a/internal/rds/encoder.go b/internal/rds/encoder.go index fbe7b93..266e572 100644 --- a/internal/rds/encoder.go +++ b/internal/rds/encoder.go @@ -94,8 +94,17 @@ type Encoder struct { // Live-updatable text — written by control API, read at group boundaries. // Zero-contention: atomic swap, checked once per RDS group (~88ms at 228kHz). - livePS atomic.Value // string - liveRT atomic.Value // string + // pendingText.set distinguishes "no pending update" from "update to empty string" + // so that PS/RT can be explicitly cleared via UpdateText. + livePS atomic.Value // pendingText + liveRT atomic.Value // pendingText +} + +// pendingText carries a pending text update for PS or RT. +// set=false means no update is pending; set=true means apply val (even if empty). +type pendingText struct { + val string + set bool } func NewEncoder(cfg RDSConfig) (*Encoder, error) { @@ -163,16 +172,29 @@ func (e *Encoder) Reset() { // UpdateText hot-swaps PS and/or RT. Thread-safe — called from HTTP handlers, // applied at the next RDS group boundary by the DSP goroutine. -// Pass empty string to leave a field unchanged. +// +// Pass empty string to leave a field unchanged. To explicitly clear a field +// (set PS to 8 spaces, or RT to empty), use ClearPS/ClearRT instead. func (e *Encoder) UpdateText(ps, rt string) { if ps != "" { - e.livePS.Store(normalizePS(ps)) + e.livePS.Store(pendingText{val: normalizePS(ps), set: true}) } if rt != "" { - e.liveRT.Store(normalizeRT(rt)) + e.liveRT.Store(pendingText{val: normalizeRT(rt), set: true}) } } +// ClearPS resets the Program Service name to 8 spaces at the next group boundary. +func (e *Encoder) ClearPS() { + e.livePS.Store(pendingText{val: normalizePS(""), set: true}) +} + +// ClearRT resets RadioText to an empty string at the next group boundary. +// Per RDS spec, an empty RT causes receivers to clear their display. +func (e *Encoder) ClearRT() { + e.liveRT.Store(pendingText{val: "", set: true}) +} + // NextSample returns the next RDS subcarrier sample at the configured rate. // Uses the internal free-running 57 kHz carrier. Prefer NextSampleWithCarrier // for phase-locked operation in a stereo MPX chain. @@ -192,15 +214,15 @@ func (e *Encoder) NextSampleWithCarrier(carrier float64) float64 { // Apply live text updates at group boundaries (~88ms at 228kHz). // Atomics are consumed (cleared) after reading to prevent // re-applying the same text every group and toggling A/B flag. - if ps, ok := e.livePS.Load().(string); ok && ps != "" { - e.scheduler.cfg.PS = ps - e.livePS.Store("") // consumed + if pt, ok := e.livePS.Load().(pendingText); ok && pt.set { + e.scheduler.cfg.PS = pt.val + e.livePS.Store(pendingText{}) // consumed } - if rt, ok := e.liveRT.Load().(string); ok && rt != "" { - e.scheduler.cfg.RT = rt + if pt, ok := e.liveRT.Load().(pendingText); ok && pt.set { + e.scheduler.cfg.RT = pt.val e.scheduler.rtIdx = 0 // restart RT transmission for new text e.scheduler.rtABFlag = !e.scheduler.rtABFlag // toggle A/B per RDS spec - e.liveRT.Store("") // consumed + e.liveRT.Store(pendingText{}) // consumed } e.getRDSGroup() e.bitPos = 0 @@ -240,12 +262,27 @@ func (e *Encoder) Generate(n int) []float64 { out := make([]float64, n); for i := range out { out[i] = e.NextSample() }; return out } func (e *Encoder) Symbol() float64 { - if e.bitPos >= bitsPerGroup { return -1 } - sym := 1.0; if e.bitBuffer[e.bitPos] == 0 { sym = -1.0 } + // Populate the bit buffer on first call (bitPos starts at bitsPerGroup + // after NewEncoder/Reset, so the guard below would return -1 immediately + // without this bootstrap step). + if e.bitPos >= bitsPerGroup { + e.getRDSGroup() + e.bitPos = 0 + } + sym := 1.0 + if e.bitBuffer[e.bitPos] == 0 { + sym = -1.0 + } e.sampleCount++ - if e.sampleCount >= e.spb { e.sampleCount = 0; e.bitPos++ - if e.bitPos >= bitsPerGroup { e.getRDSGroup(); e.bitPos = 0 } - }; return sym + if e.sampleCount >= e.spb { + e.sampleCount = 0 + e.bitPos++ + if e.bitPos >= bitsPerGroup { + e.getRDSGroup() + e.bitPos = 0 + } + } + return sym } func (e *Encoder) getRDSGroup() {