[fix](trx-backend-soapysdr,trx-frontend-http): make wfm mono mode real

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-02-28 20:25:04 +01:00
parent b7c1da138b
commit 244f91d97b
13 changed files with 105 additions and 6 deletions
@@ -1252,6 +1252,13 @@ function render(update) {
if (wfmDeemphasisEl && typeof update.filter.wfm_deemphasis_us === "number") { if (wfmDeemphasisEl && typeof update.filter.wfm_deemphasis_us === "number") {
wfmDeemphasisEl.value = String(update.filter.wfm_deemphasis_us); 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") { if (wfmDenoiseBtn && typeof update.filter.wfm_denoise === "boolean") {
const on = update.filter.wfm_denoise; const on = update.filter.wfm_denoise;
wfmDenoiseBtn.textContent = on ? "On" : "Off"; wfmDenoiseBtn.textContent = on ? "On" : "Off";
@@ -2587,6 +2594,8 @@ if (wfmAudioModeEl) {
wfmAudioModeEl.value = loadSetting("wfmAudioMode", "stereo"); wfmAudioModeEl.value = loadSetting("wfmAudioMode", "stereo");
wfmAudioModeEl.addEventListener("change", () => { wfmAudioModeEl.addEventListener("change", () => {
saveSetting("wfmAudioMode", wfmAudioModeEl.value); saveSetting("wfmAudioMode", wfmAudioModeEl.value);
const enabled = wfmAudioModeEl.value !== "mono";
postPath(`/set_wfm_stereo?enabled=${enabled ? "true" : "false"}`).catch(() => {});
}); });
} }
if (wfmDeemphasisEl) { if (wfmDeemphasisEl) {
@@ -486,6 +486,19 @@ pub async fn set_wfm_deemphasis(
send_command(&rig_tx, RigCommand::SetWfmDeemphasis(query.us)).await 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<WfmStereoQuery>,
rig_tx: web::Data<mpsc::Sender<RigRequest>>,
) -> Result<HttpResponse, Error> {
send_command(&rig_tx, RigCommand::SetWfmStereo(query.enabled)).await
}
#[post("/toggle_wfm_denoise")] #[post("/toggle_wfm_denoise")]
pub async fn toggle_wfm_denoise( pub async fn toggle_wfm_denoise(
state: web::Data<watch::Receiver<RigState>>, state: web::Data<watch::Receiver<RigState>>,
@@ -713,6 +726,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
.service(set_bandwidth) .service(set_bandwidth)
.service(set_fir_taps) .service(set_fir_taps)
.service(set_wfm_deemphasis) .service(set_wfm_deemphasis)
.service(set_wfm_stereo)
.service(toggle_wfm_denoise) .service(toggle_wfm_denoise)
.service(toggle_aprs_decode) .service(toggle_aprs_decode)
.service(toggle_cw_decode) .service(toggle_cw_decode)
+1
View File
@@ -34,6 +34,7 @@ pub enum RigCommand {
SetBandwidth(u32), SetBandwidth(u32),
SetFirTaps(u32), SetFirTaps(u32),
SetWfmDeemphasis(u32), SetWfmDeemphasis(u32),
SetWfmStereo(bool),
SetWfmDenoise(bool), SetWfmDenoise(bool),
GetSpectrum, GetSpectrum,
} }
@@ -517,6 +517,7 @@ pub fn command_from_rig_command(cmd: RigCommand) -> Box<dyn RigCommandHandler> {
| RigCommand::SetBandwidth(_) | RigCommand::SetBandwidth(_)
| RigCommand::SetFirTaps(_) | RigCommand::SetFirTaps(_)
| RigCommand::SetWfmDeemphasis(_) | RigCommand::SetWfmDeemphasis(_)
| RigCommand::SetWfmStereo(_)
| RigCommand::SetWfmDenoise(_) | RigCommand::SetWfmDenoise(_)
| RigCommand::GetSpectrum => Box::new(GetSnapshotCommand), | RigCommand::GetSpectrum => Box::new(GetSnapshotCommand),
} }
+10
View File
@@ -175,6 +175,16 @@ pub trait RigCat: Rig + Send {
))) )))
} }
fn set_wfm_stereo<'a>(
&'a mut self,
_enabled: bool,
) -> Pin<Box<dyn Future<Output = DynResult<()>> + Send + 'a>> {
Box::pin(std::future::ready(Err(
Box::new(response::RigError::not_supported("set_wfm_stereo"))
as Box<dyn std::error::Error + Send + Sync>,
)))
}
/// Return the current filter state if this backend supports filter controls. /// Return the current filter state if this backend supports filter controls.
fn filter_state(&self) -> Option<state::RigFilterState> { fn filter_state(&self) -> Option<state::RigFilterState> {
None None
+6
View File
@@ -271,6 +271,8 @@ pub struct RigFilterState {
pub cw_center_hz: u32, pub cw_center_hz: u32,
#[serde(default = "default_wfm_deemphasis_us")] #[serde(default = "default_wfm_deemphasis_us")]
pub wfm_deemphasis_us: u32, pub wfm_deemphasis_us: u32,
#[serde(default = "default_wfm_stereo")]
pub wfm_stereo: bool,
#[serde(default = "default_wfm_denoise")] #[serde(default = "default_wfm_denoise")]
pub wfm_denoise: bool, pub wfm_denoise: bool,
} }
@@ -283,6 +285,10 @@ fn default_wfm_denoise() -> bool {
true true
} }
fn default_wfm_stereo() -> bool {
true
}
/// Spectrum data from SDR backends (FFT magnitude over the full capture bandwidth). /// Spectrum data from SDR backends (FFT magnitude over the full capture bandwidth).
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpectrumData { pub struct SpectrumData {
+2
View File
@@ -298,6 +298,7 @@ mod tests {
fir_taps: 64, fir_taps: 64,
cw_center_hz: 700, cw_center_hz: 700,
wfm_deemphasis_us: 75, wfm_deemphasis_us: 75,
wfm_stereo: true,
wfm_denoise: true, wfm_denoise: true,
}), }),
..minimal_snapshot() ..minimal_snapshot()
@@ -335,6 +336,7 @@ mod tests {
fir_taps: 128, fir_taps: 128,
cw_center_hz: 700, cw_center_hz: 700,
wfm_deemphasis_us: 50, wfm_deemphasis_us: 50,
wfm_stereo: true,
wfm_denoise: true, wfm_denoise: true,
}), }),
..minimal_snapshot() ..minimal_snapshot()
+2
View File
@@ -51,6 +51,7 @@ pub fn client_command_to_rig(cmd: ClientCommand) -> RigCommand {
ClientCommand::SetWfmDeemphasis { deemphasis_us } => { ClientCommand::SetWfmDeemphasis { deemphasis_us } => {
RigCommand::SetWfmDeemphasis(deemphasis_us) RigCommand::SetWfmDeemphasis(deemphasis_us)
} }
ClientCommand::SetWfmStereo { enabled } => RigCommand::SetWfmStereo(enabled),
ClientCommand::SetWfmDenoise { enabled } => RigCommand::SetWfmDenoise(enabled), ClientCommand::SetWfmDenoise { enabled } => RigCommand::SetWfmDenoise(enabled),
ClientCommand::GetSpectrum => RigCommand::GetSpectrum, ClientCommand::GetSpectrum => RigCommand::GetSpectrum,
} }
@@ -96,6 +97,7 @@ pub fn rig_command_to_client(cmd: RigCommand) -> ClientCommand {
RigCommand::SetWfmDeemphasis(deemphasis_us) => { RigCommand::SetWfmDeemphasis(deemphasis_us) => {
ClientCommand::SetWfmDeemphasis { deemphasis_us } ClientCommand::SetWfmDeemphasis { deemphasis_us }
} }
RigCommand::SetWfmStereo(enabled) => ClientCommand::SetWfmStereo { enabled },
RigCommand::SetWfmDenoise(enabled) => ClientCommand::SetWfmDenoise { enabled }, RigCommand::SetWfmDenoise(enabled) => ClientCommand::SetWfmDenoise { enabled },
RigCommand::GetSpectrum => ClientCommand::GetSpectrum, RigCommand::GetSpectrum => ClientCommand::GetSpectrum,
} }
+1
View File
@@ -39,6 +39,7 @@ pub enum ClientCommand {
SetBandwidth { bandwidth_hz: u32 }, SetBandwidth { bandwidth_hz: u32 },
SetFirTaps { taps: u32 }, SetFirTaps { taps: u32 },
SetWfmDeemphasis { deemphasis_us: u32 }, SetWfmDeemphasis { deemphasis_us: u32 },
SetWfmStereo { enabled: bool },
SetWfmDenoise { enabled: bool }, SetWfmDenoise { enabled: bool },
GetSpectrum, GetSpectrum,
} }
+10
View File
@@ -462,6 +462,16 @@ async fn process_command(
let _ = ctx.state_tx.send(ctx.state.clone()); let _ = ctx.state_tx.send(ctx.state.clone());
return snapshot_from(ctx.state); 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) => { RigCommand::SetWfmDenoise(enabled) => {
if let Err(e) = ctx.rig.set_wfm_denoise(enabled).await { if let Err(e) = ctx.rig.set_wfm_denoise(enabled).await {
return Err(RigError::communication(format!("set_wfm_denoise: {e}"))); return Err(RigError::communication(format!("set_wfm_denoise: {e}")));
@@ -345,6 +345,7 @@ impl Deemphasis {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct WfmStereoDecoder { pub struct WfmStereoDecoder {
output_channels: usize, output_channels: usize,
stereo_enabled: bool,
rds_decoder: RdsDecoder, rds_decoder: RdsDecoder,
rds_bpf: BiquadBandPass, rds_bpf: BiquadBandPass,
rds_dc: DcBlocker, rds_dc: DcBlocker,
@@ -393,6 +394,7 @@ impl WfmStereoDecoder {
composite_rate: u32, composite_rate: u32,
audio_rate: u32, audio_rate: u32,
output_channels: usize, output_channels: usize,
stereo_enabled: bool,
deemphasis_us: u32, deemphasis_us: u32,
denoise_enabled: bool, denoise_enabled: bool,
) -> Self { ) -> Self {
@@ -401,6 +403,7 @@ impl WfmStereoDecoder {
let deemphasis_us = deemphasis_us as f32; let deemphasis_us = deemphasis_us as f32;
Self { Self {
output_channels: output_channels.max(1), output_channels: output_channels.max(1),
stereo_enabled,
rds_decoder: RdsDecoder::new(composite_rate), rds_decoder: RdsDecoder::new(composite_rate),
rds_bpf: BiquadBandPass::new(composite_rate_f, RDS_SUBCARRIER_HZ, RDS_BPF_Q), rds_bpf: BiquadBandPass::new(composite_rate_f, RDS_SUBCARRIER_HZ, RDS_BPF_Q),
rds_dc: DcBlocker::new(0.995), rds_dc: DcBlocker::new(0.995),
@@ -499,7 +502,7 @@ impl WfmStereoDecoder {
let blend_i = prev_blend + frac * (stereo_blend - prev_blend); let blend_i = prev_blend + frac * (stereo_blend - prev_blend);
// --- Deemphasis + DC block + output --- // --- 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. // Apply multiband or single-band stereo blend at audio rate.
let diff_denoised = if self.denoise_enabled { let diff_denoised = if self.denoise_enabled {
// Multiband: attenuates high-frequency diff more aggressively // 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 // 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 // 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. // a concern for mono, so the notch can sit anywhere in the chain.
output.push( let mono = self
self.dc_m .dc_m
.process(self.deemph_m.process(self.sum_notch.process(sum_i))) .process(self.deemph_m.process(self.sum_notch.process(sum_i)))
.clamp(-1.0, 1.0), .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; self.denoise_enabled = enabled;
} }
pub fn set_stereo_enabled(&mut self, enabled: bool) {
self.stereo_enabled = enabled;
}
pub fn rds_data(&self) -> Option<RdsData> { pub fn rds_data(&self) -> Option<RdsData> {
self.rds_decoder.snapshot() self.rds_decoder.snapshot()
} }
@@ -309,6 +309,8 @@ pub struct ChannelDsp {
fir_taps: usize, fir_taps: usize,
/// WFM deemphasis time constant in microseconds. /// WFM deemphasis time constant in microseconds.
wfm_deemphasis_us: u32, wfm_deemphasis_us: u32,
/// Whether WFM stereo decoding is enabled.
wfm_stereo: bool,
/// Whether multiband stereo denoising is enabled for WFM. /// Whether multiband stereo denoising is enabled for WFM.
wfm_denoise: bool, wfm_denoise: bool,
/// Decimation factor: `sdr_sample_rate / audio_sample_rate`. /// Decimation factor: `sdr_sample_rate / audio_sample_rate`.
@@ -410,6 +412,7 @@ impl ChannelDsp {
channel_sample_rate, channel_sample_rate,
self.audio_sample_rate, self.audio_sample_rate,
self.output_channels, self.output_channels,
self.wfm_stereo,
self.wfm_deemphasis_us, self.wfm_deemphasis_us,
self.wfm_denoise, self.wfm_denoise,
)); ));
@@ -432,6 +435,7 @@ impl ChannelDsp {
frame_duration_ms: u16, frame_duration_ms: u16,
audio_bandwidth_hz: u32, audio_bandwidth_hz: u32,
wfm_deemphasis_us: u32, wfm_deemphasis_us: u32,
wfm_stereo: bool,
wfm_denoise: bool, wfm_denoise: bool,
fir_taps: usize, fir_taps: usize,
pcm_tx: broadcast::Sender<Vec<f32>>, pcm_tx: broadcast::Sender<Vec<f32>>,
@@ -477,6 +481,7 @@ impl ChannelDsp {
audio_bandwidth_hz, audio_bandwidth_hz,
fir_taps: taps, fir_taps: taps,
wfm_deemphasis_us, wfm_deemphasis_us,
wfm_stereo,
wfm_denoise, wfm_denoise,
decim_factor, decim_factor,
output_channels, output_channels,
@@ -497,6 +502,7 @@ impl ChannelDsp {
channel_sample_rate, channel_sample_rate,
audio_sample_rate, audio_sample_rate,
output_channels, output_channels,
wfm_stereo,
wfm_deemphasis_us, wfm_deemphasis_us,
wfm_denoise, wfm_denoise,
)) ))
@@ -544,6 +550,13 @@ impl ChannelDsp {
self.rebuild_filters(true); 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) { pub fn set_wfm_denoise(&mut self, enabled: bool) {
self.wfm_denoise = enabled; self.wfm_denoise = enabled;
if let Some(decoder) = &mut self.wfm_decoder { if let Some(decoder) = &mut self.wfm_decoder {
@@ -689,6 +702,7 @@ impl SdrPipeline {
output_channels: usize, output_channels: usize,
frame_duration_ms: u16, frame_duration_ms: u16,
wfm_deemphasis_us: u32, wfm_deemphasis_us: u32,
wfm_stereo: bool,
wfm_denoise: bool, wfm_denoise: bool,
channels: &[(f64, RigMode, u32, usize)], channels: &[(f64, RigMode, u32, usize)],
) -> Self { ) -> Self {
@@ -711,6 +725,7 @@ impl SdrPipeline {
frame_duration_ms, frame_duration_ms,
audio_bandwidth_hz, audio_bandwidth_hz,
wfm_deemphasis_us, wfm_deemphasis_us,
wfm_stereo,
wfm_denoise, wfm_denoise,
fir_taps, fir_taps,
pcm_tx.clone(), pcm_tx.clone(),
@@ -39,6 +39,8 @@ pub struct SoapySdrRig {
retune_cmd: Arc<std::sync::Mutex<Option<f64>>>, retune_cmd: Arc<std::sync::Mutex<Option<f64>>>,
/// Current WFM deemphasis setting in microseconds. /// Current WFM deemphasis setting in microseconds.
wfm_deemphasis_us: u32, wfm_deemphasis_us: u32,
/// Whether WFM stereo decode is enabled.
wfm_stereo: bool,
/// Whether multiband WFM stereo denoising is enabled. /// Whether multiband WFM stereo denoising is enabled.
wfm_denoise: bool, wfm_denoise: bool,
} }
@@ -117,6 +119,7 @@ impl SoapySdrRig {
audio_channels, audio_channels,
frame_duration_ms, frame_duration_ms,
wfm_deemphasis_us, wfm_deemphasis_us,
true, // wfm_stereo: enabled by default
true, // wfm_denoise: enabled by default true, // wfm_denoise: enabled by default
channels, channels,
); );
@@ -185,6 +188,7 @@ impl SoapySdrRig {
center_hz: hardware_center_hz, center_hz: hardware_center_hz,
retune_cmd, retune_cmd,
wfm_deemphasis_us, wfm_deemphasis_us,
wfm_stereo: true,
wfm_denoise: true, wfm_denoise: true,
}) })
} }
@@ -464,6 +468,19 @@ impl RigCat for SoapySdrRig {
}) })
} }
fn set_wfm_stereo<'a>(
&'a mut self,
enabled: bool,
) -> Pin<Box<dyn std::future::Future<Output = DynResult<()>> + 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>( fn set_wfm_denoise<'a>(
&'a mut self, &'a mut self,
enabled: bool, enabled: bool,
@@ -483,6 +500,7 @@ impl RigCat for SoapySdrRig {
fir_taps: self.fir_taps, fir_taps: self.fir_taps,
cw_center_hz: 700, cw_center_hz: 700,
wfm_deemphasis_us: self.wfm_deemphasis_us, wfm_deemphasis_us: self.wfm_deemphasis_us,
wfm_stereo: self.wfm_stereo,
wfm_denoise: self.wfm_denoise, wfm_denoise: self.wfm_denoise,
}) })
} }