| @@ -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() { | |||