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 3900002..615a535 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 @@ -1252,6 +1252,13 @@ function render(update) { if (wfmDeemphasisEl && typeof update.filter.wfm_deemphasis_us === "number") { wfmDeemphasisEl.value = String(update.filter.wfm_deemphasis_us); } + if (wfmAudioModeEl && typeof update.filter.wfm_stereo === "boolean") { + const nextMode = update.filter.wfm_stereo ? "stereo" : "mono"; + if (wfmAudioModeEl.value !== nextMode) { + wfmAudioModeEl.value = nextMode; + saveSetting("wfmAudioMode", nextMode); + } + } if (wfmDenoiseBtn && typeof update.filter.wfm_denoise === "boolean") { const on = update.filter.wfm_denoise; wfmDenoiseBtn.textContent = on ? "On" : "Off"; @@ -2587,6 +2594,8 @@ if (wfmAudioModeEl) { wfmAudioModeEl.value = loadSetting("wfmAudioMode", "stereo"); wfmAudioModeEl.addEventListener("change", () => { saveSetting("wfmAudioMode", wfmAudioModeEl.value); + const enabled = wfmAudioModeEl.value !== "mono"; + postPath(`/set_wfm_stereo?enabled=${enabled ? "true" : "false"}`).catch(() => {}); }); } if (wfmDeemphasisEl) { diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs index 1cedd09..33580af 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs @@ -486,6 +486,19 @@ pub async fn set_wfm_deemphasis( send_command(&rig_tx, RigCommand::SetWfmDeemphasis(query.us)).await } +#[derive(serde::Deserialize)] +pub struct WfmStereoQuery { + pub enabled: bool, +} + +#[post("/set_wfm_stereo")] +pub async fn set_wfm_stereo( + query: web::Query, + rig_tx: web::Data>, +) -> Result { + send_command(&rig_tx, RigCommand::SetWfmStereo(query.enabled)).await +} + #[post("/toggle_wfm_denoise")] pub async fn toggle_wfm_denoise( state: web::Data>, @@ -713,6 +726,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) { .service(set_bandwidth) .service(set_fir_taps) .service(set_wfm_deemphasis) + .service(set_wfm_stereo) .service(toggle_wfm_denoise) .service(toggle_aprs_decode) .service(toggle_cw_decode) diff --git a/src/trx-core/src/rig/command.rs b/src/trx-core/src/rig/command.rs index fad3b7f..fff460d 100644 --- a/src/trx-core/src/rig/command.rs +++ b/src/trx-core/src/rig/command.rs @@ -34,6 +34,7 @@ pub enum RigCommand { SetBandwidth(u32), SetFirTaps(u32), SetWfmDeemphasis(u32), + SetWfmStereo(bool), SetWfmDenoise(bool), GetSpectrum, } diff --git a/src/trx-core/src/rig/controller/handlers.rs b/src/trx-core/src/rig/controller/handlers.rs index 69d519c..cf3adf8 100644 --- a/src/trx-core/src/rig/controller/handlers.rs +++ b/src/trx-core/src/rig/controller/handlers.rs @@ -517,6 +517,7 @@ pub fn command_from_rig_command(cmd: RigCommand) -> Box { | RigCommand::SetBandwidth(_) | RigCommand::SetFirTaps(_) | RigCommand::SetWfmDeemphasis(_) + | RigCommand::SetWfmStereo(_) | RigCommand::SetWfmDenoise(_) | RigCommand::GetSpectrum => Box::new(GetSnapshotCommand), } diff --git a/src/trx-core/src/rig/mod.rs b/src/trx-core/src/rig/mod.rs index efb3686..37c4bce 100644 --- a/src/trx-core/src/rig/mod.rs +++ b/src/trx-core/src/rig/mod.rs @@ -175,6 +175,16 @@ pub trait RigCat: Rig + Send { ))) } + fn set_wfm_stereo<'a>( + &'a mut self, + _enabled: bool, + ) -> Pin> + Send + 'a>> { + Box::pin(std::future::ready(Err( + Box::new(response::RigError::not_supported("set_wfm_stereo")) + as Box, + ))) + } + /// Return the current filter state if this backend supports filter controls. fn filter_state(&self) -> Option { None diff --git a/src/trx-core/src/rig/state.rs b/src/trx-core/src/rig/state.rs index 7dfbfbd..d2d4e6e 100644 --- a/src/trx-core/src/rig/state.rs +++ b/src/trx-core/src/rig/state.rs @@ -271,6 +271,8 @@ pub struct RigFilterState { pub cw_center_hz: u32, #[serde(default = "default_wfm_deemphasis_us")] pub wfm_deemphasis_us: u32, + #[serde(default = "default_wfm_stereo")] + pub wfm_stereo: bool, #[serde(default = "default_wfm_denoise")] pub wfm_denoise: bool, } @@ -283,6 +285,10 @@ fn default_wfm_denoise() -> bool { true } +fn default_wfm_stereo() -> bool { + true +} + /// Spectrum data from SDR backends (FFT magnitude over the full capture bandwidth). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SpectrumData { diff --git a/src/trx-protocol/src/codec.rs b/src/trx-protocol/src/codec.rs index 9fd7daa..4f79866 100644 --- a/src/trx-protocol/src/codec.rs +++ b/src/trx-protocol/src/codec.rs @@ -298,6 +298,7 @@ mod tests { fir_taps: 64, cw_center_hz: 700, wfm_deemphasis_us: 75, + wfm_stereo: true, wfm_denoise: true, }), ..minimal_snapshot() @@ -335,6 +336,7 @@ mod tests { fir_taps: 128, cw_center_hz: 700, wfm_deemphasis_us: 50, + wfm_stereo: true, wfm_denoise: true, }), ..minimal_snapshot() diff --git a/src/trx-protocol/src/mapping.rs b/src/trx-protocol/src/mapping.rs index 357879c..2420c28 100644 --- a/src/trx-protocol/src/mapping.rs +++ b/src/trx-protocol/src/mapping.rs @@ -51,6 +51,7 @@ pub fn client_command_to_rig(cmd: ClientCommand) -> RigCommand { ClientCommand::SetWfmDeemphasis { deemphasis_us } => { RigCommand::SetWfmDeemphasis(deemphasis_us) } + ClientCommand::SetWfmStereo { enabled } => RigCommand::SetWfmStereo(enabled), ClientCommand::SetWfmDenoise { enabled } => RigCommand::SetWfmDenoise(enabled), ClientCommand::GetSpectrum => RigCommand::GetSpectrum, } @@ -96,6 +97,7 @@ pub fn rig_command_to_client(cmd: RigCommand) -> ClientCommand { RigCommand::SetWfmDeemphasis(deemphasis_us) => { ClientCommand::SetWfmDeemphasis { deemphasis_us } } + RigCommand::SetWfmStereo(enabled) => ClientCommand::SetWfmStereo { enabled }, RigCommand::SetWfmDenoise(enabled) => ClientCommand::SetWfmDenoise { enabled }, RigCommand::GetSpectrum => ClientCommand::GetSpectrum, } diff --git a/src/trx-protocol/src/types.rs b/src/trx-protocol/src/types.rs index eb6784e..968ba3f 100644 --- a/src/trx-protocol/src/types.rs +++ b/src/trx-protocol/src/types.rs @@ -39,6 +39,7 @@ pub enum ClientCommand { SetBandwidth { bandwidth_hz: u32 }, SetFirTaps { taps: u32 }, SetWfmDeemphasis { deemphasis_us: u32 }, + SetWfmStereo { enabled: bool }, SetWfmDenoise { enabled: bool }, GetSpectrum, } diff --git a/src/trx-server/src/rig_task.rs b/src/trx-server/src/rig_task.rs index 271cd60..2bfa366 100644 --- a/src/trx-server/src/rig_task.rs +++ b/src/trx-server/src/rig_task.rs @@ -462,6 +462,16 @@ async fn process_command( let _ = ctx.state_tx.send(ctx.state.clone()); return snapshot_from(ctx.state); } + RigCommand::SetWfmStereo(enabled) => { + if let Err(e) = ctx.rig.set_wfm_stereo(enabled).await { + return Err(RigError::communication(format!("set_wfm_stereo: {e}"))); + } + if let Some(f) = ctx.state.filter.as_mut() { + f.wfm_stereo = enabled; + } + let _ = ctx.state_tx.send(ctx.state.clone()); + return snapshot_from(ctx.state); + } RigCommand::SetWfmDenoise(enabled) => { if let Err(e) = ctx.rig.set_wfm_denoise(enabled).await { return Err(RigError::communication(format!("set_wfm_denoise: {e}"))); 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 54136aa..645e898 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 @@ -345,6 +345,7 @@ impl Deemphasis { #[derive(Debug, Clone)] pub struct WfmStereoDecoder { output_channels: usize, + stereo_enabled: bool, rds_decoder: RdsDecoder, rds_bpf: BiquadBandPass, rds_dc: DcBlocker, @@ -393,6 +394,7 @@ impl WfmStereoDecoder { composite_rate: u32, audio_rate: u32, output_channels: usize, + stereo_enabled: bool, deemphasis_us: u32, denoise_enabled: bool, ) -> Self { @@ -401,6 +403,7 @@ impl WfmStereoDecoder { let deemphasis_us = deemphasis_us as f32; Self { output_channels: output_channels.max(1), + stereo_enabled, rds_decoder: RdsDecoder::new(composite_rate), rds_bpf: BiquadBandPass::new(composite_rate_f, RDS_SUBCARRIER_HZ, RDS_BPF_Q), rds_dc: DcBlocker::new(0.995), @@ -499,7 +502,7 @@ impl WfmStereoDecoder { let blend_i = prev_blend + frac * (stereo_blend - prev_blend); // --- Deemphasis + DC block + output --- - if self.output_channels >= 2 { + if self.output_channels >= 2 && self.stereo_enabled { // Apply multiband or single-band stereo blend at audio rate. let diff_denoised = if self.denoise_enabled { // Multiband: attenuates high-frequency diff more aggressively @@ -521,11 +524,14 @@ impl WfmStereoDecoder { // Mono path: apply the pilot notch here so the 19 kHz pilot tone // does not leak into mono audio. Phase matching with diff is not // a concern for mono, so the notch can sit anywhere in the chain. - output.push( - self.dc_m - .process(self.deemph_m.process(self.sum_notch.process(sum_i))) - .clamp(-1.0, 1.0), - ); + let mono = self + .dc_m + .process(self.deemph_m.process(self.sum_notch.process(sum_i))) + .clamp(-1.0, 1.0); + output.push(mono); + if self.output_channels >= 2 { + output.push(mono); + } } } @@ -536,6 +542,10 @@ impl WfmStereoDecoder { self.denoise_enabled = enabled; } + pub fn set_stereo_enabled(&mut self, enabled: bool) { + self.stereo_enabled = enabled; + } + pub fn rds_data(&self) -> Option { self.rds_decoder.snapshot() } 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 0a05fb9..a649d60 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 @@ -309,6 +309,8 @@ pub struct ChannelDsp { fir_taps: usize, /// WFM deemphasis time constant in microseconds. wfm_deemphasis_us: u32, + /// Whether WFM stereo decoding is enabled. + wfm_stereo: bool, /// Whether multiband stereo denoising is enabled for WFM. wfm_denoise: bool, /// Decimation factor: `sdr_sample_rate / audio_sample_rate`. @@ -410,6 +412,7 @@ impl ChannelDsp { channel_sample_rate, self.audio_sample_rate, self.output_channels, + self.wfm_stereo, self.wfm_deemphasis_us, self.wfm_denoise, )); @@ -432,6 +435,7 @@ impl ChannelDsp { frame_duration_ms: u16, audio_bandwidth_hz: u32, wfm_deemphasis_us: u32, + wfm_stereo: bool, wfm_denoise: bool, fir_taps: usize, pcm_tx: broadcast::Sender>, @@ -477,6 +481,7 @@ impl ChannelDsp { audio_bandwidth_hz, fir_taps: taps, wfm_deemphasis_us, + wfm_stereo, wfm_denoise, decim_factor, output_channels, @@ -497,6 +502,7 @@ impl ChannelDsp { channel_sample_rate, audio_sample_rate, output_channels, + wfm_stereo, wfm_deemphasis_us, wfm_denoise, )) @@ -544,6 +550,13 @@ impl ChannelDsp { self.rebuild_filters(true); } + pub fn set_wfm_stereo(&mut self, enabled: bool) { + self.wfm_stereo = enabled; + if let Some(decoder) = &mut self.wfm_decoder { + decoder.set_stereo_enabled(enabled); + } + } + pub fn set_wfm_denoise(&mut self, enabled: bool) { self.wfm_denoise = enabled; if let Some(decoder) = &mut self.wfm_decoder { @@ -689,6 +702,7 @@ impl SdrPipeline { output_channels: usize, frame_duration_ms: u16, wfm_deemphasis_us: u32, + wfm_stereo: bool, wfm_denoise: bool, channels: &[(f64, RigMode, u32, usize)], ) -> Self { @@ -711,6 +725,7 @@ impl SdrPipeline { frame_duration_ms, audio_bandwidth_hz, wfm_deemphasis_us, + wfm_stereo, wfm_denoise, fir_taps, pcm_tx.clone(), 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 edc849f..37b239c 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 @@ -39,6 +39,8 @@ pub struct SoapySdrRig { retune_cmd: Arc>>, /// Current WFM deemphasis setting in microseconds. wfm_deemphasis_us: u32, + /// Whether WFM stereo decode is enabled. + wfm_stereo: bool, /// Whether multiband WFM stereo denoising is enabled. wfm_denoise: bool, } @@ -117,6 +119,7 @@ impl SoapySdrRig { audio_channels, frame_duration_ms, wfm_deemphasis_us, + true, // wfm_stereo: enabled by default true, // wfm_denoise: enabled by default channels, ); @@ -185,6 +188,7 @@ impl SoapySdrRig { center_hz: hardware_center_hz, retune_cmd, wfm_deemphasis_us, + wfm_stereo: true, wfm_denoise: true, }) } @@ -464,6 +468,19 @@ impl RigCat for SoapySdrRig { }) } + fn set_wfm_stereo<'a>( + &'a mut self, + enabled: bool, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { + self.wfm_stereo = enabled; + if let Some(dsp_arc) = self.pipeline.channel_dsps.get(self.primary_channel_idx) { + dsp_arc.lock().unwrap().set_wfm_stereo(enabled); + } + Ok(()) + }) + } + fn set_wfm_denoise<'a>( &'a mut self, enabled: bool, @@ -483,6 +500,7 @@ impl RigCat for SoapySdrRig { fir_taps: self.fir_taps, cw_center_hz: 700, wfm_deemphasis_us: self.wfm_deemphasis_us, + wfm_stereo: self.wfm_stereo, wfm_denoise: self.wfm_denoise, }) }