diff --git a/src/decoders/trx-rds/src/lib.rs b/src/decoders/trx-rds/src/lib.rs index 5806239..cc87910 100644 --- a/src/decoders/trx-rds/src/lib.rs +++ b/src/decoders/trx-rds/src/lib.rs @@ -294,6 +294,7 @@ impl Candidate { #[derive(Debug, Clone)] pub struct RdsDecoder { + sample_rate_hz: u32, carrier_phase: f32, carrier_inc: f32, i_lp: OnePoleLowPass, @@ -314,6 +315,7 @@ impl RdsDecoder { )); } Self { + sample_rate_hz: sample_rate.max(1), carrier_phase: 0.0, carrier_inc: TAU * RDS_SUBCARRIER_HZ / sample_rate_f, i_lp: OnePoleLowPass::new(sample_rate_f, 3_000.0), @@ -343,6 +345,10 @@ impl RdsDecoder { self.best_state.as_ref() } + pub fn reset(&mut self) { + *self = Self::new(self.sample_rate_hz); + } + pub fn snapshot(&self) -> Option { self.best_state.clone() } diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index 884c906..09aba86 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -671,6 +671,7 @@ function resizeHeaderSignalCanvas() { _wfResetOffscreen(); trimOverviewWaterfallRows(); } + positionRdsPsOverlay(); drawHeaderSignalGraph(); } @@ -751,6 +752,7 @@ function drawHeaderSignalGraph() { drawOverviewSignalHistory(ctx, w, h, pal); } ctx.restore(); + positionRdsPsOverlay(); } function _wfDrawRows(oct, rows, startRowIdx, endRowIdx, iW, iH, pal) { @@ -927,8 +929,32 @@ function refreshFreqDisplay() { refreshWavelengthDisplay(lastFreqHz); } +function positionRdsPsOverlay() { + if (!rdsPsOverlay || !lastSpectrumData || lastFreqHz == null || !overviewCanvas) return; + const width = overviewCanvas.clientWidth || overviewCanvas.width || 0; + if (width <= 0) { + return; + } + const range = spectrumVisibleRange(lastSpectrumData); + if (!Number.isFinite(range.visLoHz) || !Number.isFinite(range.visSpanHz) || range.visSpanHz <= 0) { + return; + } + const rel = (lastFreqHz - range.visLoHz) / range.visSpanHz; + const clamped = Math.max(0.06, Math.min(0.94, rel)); + rdsPsOverlay.style.left = `${clamped * width}px`; +} + +function resetRdsDisplay() { + rdsFrameCount = 0; + updateRdsPsOverlay(null); +} + function applyLocalTunedFrequency(hz) { if (!Number.isFinite(hz)) return; + const freqChanged = lastFreqHz !== hz; + if (freqChanged) { + resetRdsDisplay(); + } lastFreqHz = hz; refreshWavelengthDisplay(lastFreqHz); if (!freqDirty) { @@ -941,6 +967,7 @@ function applyLocalTunedFrequency(hz) { if (lastSpectrumData) { scheduleSpectrumDraw(); } + positionRdsPsOverlay(); } function refreshCenterFreqDisplay() { @@ -3150,6 +3177,7 @@ function updateRdsPsOverlay(rds) { const ps = rds?.program_service?.trim(); if (ps) { rdsPsOverlay.textContent = ps; + positionRdsPsOverlay(); rdsPsOverlay.style.display = ""; } else { rdsPsOverlay.style.display = "none"; diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css index 7d941a0..339db30 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css @@ -451,18 +451,29 @@ small { color: var(--text-muted); } #rds-ps-overlay { display: none; position: absolute; - top: calc(var(--header-waterfall-overlap) + clamp(4.2rem, 11vh, 6.25rem) / 2); + top: 0.35rem; left: 50%; - transform: translate(-50%, -50%); + transform: translate(-50%, 0); z-index: 2; pointer-events: none; font-family: 'DSEG14 Classic', monospace; - font-size: 2rem; - letter-spacing: 0.05em; + font-size: clamp(1rem, 2.2vw, 1.45rem); + letter-spacing: 0.08em; color: var(--text-heading); - opacity: 0.88; - text-shadow: 0 1px 8px color-mix(in srgb, var(--bg) 60%, transparent); + padding: 0.22rem 0.6rem 0.16rem; + border: 1px solid color-mix(in srgb, var(--border-light) 72%, transparent); + border-radius: 999px; + background: color-mix(in srgb, var(--card-bg) 52%, transparent); + backdrop-filter: blur(14px) saturate(135%); + -webkit-backdrop-filter: blur(14px) saturate(135%); + box-shadow: + 0 8px 18px color-mix(in srgb, #000000 16%, transparent), + inset 0 1px 0 color-mix(in srgb, #ffffff 10%, transparent); + text-shadow: 0 1px 10px color-mix(in srgb, var(--bg) 68%, transparent); white-space: nowrap; + max-width: min(88vw, 22rem); + overflow: hidden; + text-overflow: ellipsis; } .overview-toolbar { display: flex; diff --git a/src/trx-server/trx-backend/trx-backend-soapysdr/src/demod.rs b/src/trx-server/trx-backend/trx-backend-soapysdr/src/demod.rs index 8f1a51c..a7c5c6e 100644 --- a/src/trx-server/trx-backend/trx-backend-soapysdr/src/demod.rs +++ b/src/trx-server/trx-backend/trx-backend-soapysdr/src/demod.rs @@ -149,6 +149,10 @@ impl WfmStereoDecoder { pub fn rds_data(&self) -> Option { self.rds_decoder.snapshot() } + + pub fn reset_rds(&mut self) { + self.rds_decoder.reset(); + } } /// Selects the demodulation algorithm for a channel. diff --git a/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp.rs b/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp.rs index 4a40b63..75affe8 100644 --- a/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp.rs +++ b/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp.rs @@ -502,6 +502,12 @@ impl ChannelDsp { self.wfm_decoder.as_ref().and_then(WfmStereoDecoder::rds_data) } + pub fn reset_rds(&mut self) { + if let Some(decoder) = &mut self.wfm_decoder { + decoder.reset_rds(); + } + } + /// Process a block of raw IQ samples through the full DSP chain. /// /// 1. **Batch mixer**: compute the full LO signal for the block at once, diff --git a/src/trx-server/trx-backend/trx-backend-soapysdr/src/lib.rs b/src/trx-server/trx-backend/trx-backend-soapysdr/src/lib.rs index abedbde..a656134 100644 --- a/src/trx-server/trx-backend/trx-backend-soapysdr/src/lib.rs +++ b/src/trx-server/trx-backend/trx-backend-soapysdr/src/lib.rs @@ -252,6 +252,7 @@ impl RigCat for SoapySdrRig { ) -> Pin> + Send + 'a>> { Box::pin(async move { tracing::debug!("SoapySdrRig: set_freq -> {} Hz", freq.hz); + let freq_changed = self.freq.hz != freq.hz; self.freq = freq; let half_span_hz = i128::from(self.pipeline.sdr_sample_rate) / 2; let current_center_hz = i128::from(self.center_hz); @@ -271,7 +272,11 @@ impl RigCat for SoapySdrRig { if let Some(dsp_arc) = self.pipeline.channel_dsps.get(self.primary_channel_idx) { let channel_if_hz = (self.freq.hz as i64 - self.center_hz) as f64; - dsp_arc.lock().unwrap().set_channel_if_hz(channel_if_hz); + let mut dsp = dsp_arc.lock().unwrap(); + dsp.set_channel_if_hz(channel_if_hz); + if freq_changed { + dsp.reset_rds(); + } } Ok(()) })