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 29a9a7b..3900002 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,12 @@ function render(update) {
if (wfmDeemphasisEl && typeof update.filter.wfm_deemphasis_us === "number") {
wfmDeemphasisEl.value = String(update.filter.wfm_deemphasis_us);
}
+ if (wfmDenoiseBtn && typeof update.filter.wfm_denoise === "boolean") {
+ const on = update.filter.wfm_denoise;
+ wfmDenoiseBtn.textContent = on ? "On" : "Off";
+ wfmDenoiseBtn.style.borderColor = on ? "" : "var(--accent-warn, #f0a500)";
+ wfmDenoiseBtn.style.color = on ? "" : "var(--accent-warn, #f0a500)";
+ }
}
if (update.status && update.status.freq && typeof update.status.freq.hz === "number") {
applyLocalTunedFrequency(update.status.freq.hz, true);
@@ -2546,6 +2552,7 @@ const audioRow = document.getElementById("audio-row");
const wfmControlsCol = document.getElementById("wfm-controls-col");
const wfmDeemphasisEl = document.getElementById("wfm-deemphasis");
const wfmAudioModeEl = document.getElementById("wfm-audio-mode");
+const wfmDenoiseBtn = document.getElementById("wfm-denoise-btn");
// Hide audio row if audio is not configured on the server
fetch("/audio", { method: "GET" }).then((r) => {
@@ -2587,6 +2594,11 @@ if (wfmDeemphasisEl) {
postPath(`/set_wfm_deemphasis?us=${encodeURIComponent(wfmDeemphasisEl.value)}`).catch(() => {});
});
}
+if (wfmDenoiseBtn) {
+ wfmDenoiseBtn.addEventListener("click", () => {
+ postPath("/toggle_wfm_denoise").catch(() => {});
+ });
+}
function updateWfmControls() {
if (!wfmControlsCol) return;
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html
index 8365a74..192c30b 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html
@@ -165,6 +165,9 @@
+
WFM
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 81ae91e..1cedd09 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,21 @@ pub async fn set_wfm_deemphasis(
send_command(&rig_tx, RigCommand::SetWfmDeemphasis(query.us)).await
}
+#[post("/toggle_wfm_denoise")]
+pub async fn toggle_wfm_denoise(
+ state: web::Data>,
+ rig_tx: web::Data>,
+) -> Result {
+ let enabled = state
+ .get_ref()
+ .borrow()
+ .filter
+ .as_ref()
+ .map(|f| f.wfm_denoise)
+ .unwrap_or(true);
+ send_command(&rig_tx, RigCommand::SetWfmDenoise(!enabled)).await
+}
+
#[post("/toggle_aprs_decode")]
pub async fn toggle_aprs_decode(
state: web::Data>,
@@ -698,6 +713,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
.service(set_bandwidth)
.service(set_fir_taps)
.service(set_wfm_deemphasis)
+ .service(toggle_wfm_denoise)
.service(toggle_aprs_decode)
.service(toggle_cw_decode)
.service(set_cw_auto)
diff --git a/src/trx-core/src/rig/command.rs b/src/trx-core/src/rig/command.rs
index f9dde32..fad3b7f 100644
--- a/src/trx-core/src/rig/command.rs
+++ b/src/trx-core/src/rig/command.rs
@@ -34,5 +34,6 @@ pub enum RigCommand {
SetBandwidth(u32),
SetFirTaps(u32),
SetWfmDeemphasis(u32),
+ SetWfmDenoise(bool),
GetSpectrum,
}
diff --git a/src/trx-core/src/rig/controller/handlers.rs b/src/trx-core/src/rig/controller/handlers.rs
index 55b1eaa..69d519c 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::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 1d89dc2..efb3686 100644
--- a/src/trx-core/src/rig/mod.rs
+++ b/src/trx-core/src/rig/mod.rs
@@ -165,6 +165,16 @@ pub trait RigCat: Rig + Send {
)))
}
+ fn set_wfm_denoise<'a>(
+ &'a mut self,
+ _enabled: bool,
+ ) -> Pin> + Send + 'a>> {
+ Box::pin(std::future::ready(Err(
+ Box::new(response::RigError::not_supported("set_wfm_denoise"))
+ 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 1082011..7dfbfbd 100644
--- a/src/trx-core/src/rig/state.rs
+++ b/src/trx-core/src/rig/state.rs
@@ -271,12 +271,18 @@ pub struct RigFilterState {
pub cw_center_hz: u32,
#[serde(default = "default_wfm_deemphasis_us")]
pub wfm_deemphasis_us: u32,
+ #[serde(default = "default_wfm_denoise")]
+ pub wfm_denoise: bool,
}
fn default_wfm_deemphasis_us() -> u32 {
75
}
+fn default_wfm_denoise() -> 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 cb17699..9fd7daa 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_denoise: true,
}),
..minimal_snapshot()
})
@@ -334,6 +335,7 @@ mod tests {
fir_taps: 128,
cw_center_hz: 700,
wfm_deemphasis_us: 50,
+ wfm_denoise: true,
}),
..minimal_snapshot()
};
diff --git a/src/trx-protocol/src/mapping.rs b/src/trx-protocol/src/mapping.rs
index a5df583..357879c 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::SetWfmDenoise { enabled } => RigCommand::SetWfmDenoise(enabled),
ClientCommand::GetSpectrum => RigCommand::GetSpectrum,
}
}
@@ -95,6 +96,7 @@ pub fn rig_command_to_client(cmd: RigCommand) -> ClientCommand {
RigCommand::SetWfmDeemphasis(deemphasis_us) => {
ClientCommand::SetWfmDeemphasis { deemphasis_us }
}
+ 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 7a4678c..eb6784e 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 },
+ SetWfmDenoise { enabled: bool },
GetSpectrum,
}
diff --git a/src/trx-server/src/rig_task.rs b/src/trx-server/src/rig_task.rs
index bd4bd6e..271cd60 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::SetWfmDenoise(enabled) => {
+ if let Err(e) = ctx.rig.set_wfm_denoise(enabled).await {
+ return Err(RigError::communication(format!("set_wfm_denoise: {e}")));
+ }
+ if let Some(f) = ctx.state.filter.as_mut() {
+ f.wfm_denoise = enabled;
+ }
+ let _ = ctx.state_tx.send(ctx.state.clone());
+ return snapshot_from(ctx.state);
+ }
RigCommand::SetCenterFreq(freq) => {
if let Err(e) = ctx.rig.set_center_freq(freq).await {
return Err(RigError::communication(format!("set_center_freq: {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 45cf2ab..d2c04da 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
@@ -370,6 +370,8 @@ pub struct WfmStereoDecoder {
deemph_r: Deemphasis,
/// Multiband stereo blending applied at audio rate to the L-R diff channel.
diff_denoise: MultibandStereoBlend,
+ /// Whether multiband stereo denoising is active.
+ denoise_enabled: bool,
/// Previous filtered sum/diff composite samples used for linear interpolation.
prev_sum: f32,
/// Unblended L-R diff at the previous composite sample, for interpolation.
@@ -391,6 +393,7 @@ impl WfmStereoDecoder {
audio_rate: u32,
output_channels: usize,
deemphasis_us: u32,
+ denoise_enabled: bool,
) -> Self {
let composite_rate_f = composite_rate.max(1) as f32;
let output_phase_inc = audio_rate.max(1) as f64 / composite_rate.max(1) as f64;
@@ -419,6 +422,7 @@ impl WfmStereoDecoder {
deemph_l: Deemphasis::new(audio_rate.max(1) as f32, deemphasis_us),
deemph_r: Deemphasis::new(audio_rate.max(1) as f32, deemphasis_us),
diff_denoise: MultibandStereoBlend::new(audio_rate.max(1) as f32),
+ denoise_enabled,
prev_sum: 0.0,
prev_diff: 0.0,
prev_blend: 0.0,
@@ -491,11 +495,15 @@ impl WfmStereoDecoder {
// --- Deemphasis + DC block + output ---
if self.output_channels >= 2 {
- // Apply multiband stereo denoising at audio rate.
- // Higher frequency bands of the diff are attenuated more aggressively
- // when the pilot is weak, reducing stereo noise without collapsing
- // the low-frequency stereo image.
- let diff_denoised = self.diff_denoise.process(diff_i, blend_i);
+ // Apply multiband or single-band stereo blend at audio rate.
+ let diff_denoised = if self.denoise_enabled {
+ // Multiband: attenuates high-frequency diff more aggressively
+ // when the pilot is weak, preserving the low-frequency stereo image.
+ self.diff_denoise.process(diff_i, blend_i)
+ } else {
+ // Single-band: uniform blend across all frequencies.
+ diff_i * blend_i
+ };
let left = self.dc_l
.process(self.deemph_l.process((sum_i + diff_denoised) * 0.5))
.clamp(-1.0, 1.0);
@@ -516,6 +524,10 @@ impl WfmStereoDecoder {
output
}
+ pub fn set_denoise_enabled(&mut self, enabled: bool) {
+ self.denoise_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 f71f116..0a05fb9 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 multiband stereo denoising is enabled for WFM.
+ wfm_denoise: bool,
/// Decimation factor: `sdr_sample_rate / audio_sample_rate`.
pub decim_factor: usize,
/// Number of PCM channels emitted in each frame.
@@ -409,6 +411,7 @@ impl ChannelDsp {
self.audio_sample_rate,
self.output_channels,
self.wfm_deemphasis_us,
+ self.wfm_denoise,
));
}
} else {
@@ -429,6 +432,7 @@ impl ChannelDsp {
frame_duration_ms: u16,
audio_bandwidth_hz: u32,
wfm_deemphasis_us: u32,
+ wfm_denoise: bool,
fir_taps: usize,
pcm_tx: broadcast::Sender>,
) -> Self {
@@ -473,6 +477,7 @@ impl ChannelDsp {
audio_bandwidth_hz,
fir_taps: taps,
wfm_deemphasis_us,
+ wfm_denoise,
decim_factor,
output_channels,
frame_buf: Vec::with_capacity(frame_size + output_channels),
@@ -493,6 +498,7 @@ impl ChannelDsp {
audio_sample_rate,
output_channels,
wfm_deemphasis_us,
+ wfm_denoise,
))
} else {
None
@@ -538,6 +544,13 @@ impl ChannelDsp {
self.rebuild_filters(true);
}
+ pub fn set_wfm_denoise(&mut self, enabled: bool) {
+ self.wfm_denoise = enabled;
+ if let Some(decoder) = &mut self.wfm_decoder {
+ decoder.set_denoise_enabled(enabled);
+ }
+ }
+
pub fn rds_data(&self) -> Option {
self.wfm_decoder.as_ref().and_then(WfmStereoDecoder::rds_data)
}
@@ -676,6 +689,7 @@ impl SdrPipeline {
output_channels: usize,
frame_duration_ms: u16,
wfm_deemphasis_us: u32,
+ wfm_denoise: bool,
channels: &[(f64, RigMode, u32, usize)],
) -> Self {
const IQ_BROADCAST_CAPACITY: usize = 64;
@@ -697,6 +711,7 @@ impl SdrPipeline {
frame_duration_ms,
audio_bandwidth_hz,
wfm_deemphasis_us,
+ wfm_denoise,
fir_taps,
pcm_tx.clone(),
);
@@ -945,7 +960,7 @@ mod tests {
fn channel_dsp_processes_silence() {
let (pcm_tx, _pcm_rx) = broadcast::channel::>(8);
let mut dsp =
- ChannelDsp::new(0.0, &RigMode::USB, 48_000, 8_000, 1, 20, 3000, 75, 31, pcm_tx);
+ ChannelDsp::new(0.0, &RigMode::USB, 48_000, 8_000, 1, 20, 3000, 75, true, 31, pcm_tx);
let block = vec![Complex::new(0.0_f32, 0.0_f32); 4096];
dsp.process_block(&block);
}
@@ -954,7 +969,7 @@ mod tests {
fn channel_dsp_set_mode() {
let (pcm_tx, _) = broadcast::channel::>(8);
let mut dsp =
- ChannelDsp::new(0.0, &RigMode::USB, 48_000, 8_000, 1, 20, 3000, 75, 31, pcm_tx);
+ ChannelDsp::new(0.0, &RigMode::USB, 48_000, 8_000, 1, 20, 3000, 75, true, 31, pcm_tx);
assert_eq!(dsp.demodulator, Demodulator::Usb);
dsp.set_mode(&RigMode::FM);
assert_eq!(dsp.demodulator, Demodulator::Fm);
@@ -969,6 +984,7 @@ mod tests {
1,
20,
75,
+ true,
&[(200_000.0, RigMode::USB, 3000, 64)],
);
assert_eq!(pipeline.pcm_senders.len(), 1);
@@ -977,7 +993,7 @@ mod tests {
#[test]
fn pipeline_empty_channels() {
- let pipeline = SdrPipeline::start(Box::new(MockIqSource), 1_920_000, 48_000, 1, 20, 75, &[]);
+ let pipeline = SdrPipeline::start(Box::new(MockIqSource), 1_920_000, 48_000, 1, 20, 75, true, &[]);
assert_eq!(pipeline.pcm_senders.len(), 0);
assert_eq!(pipeline.channel_dsps.len(), 0);
}
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 65ae832..edc849f 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 multiband WFM stereo denoising is enabled.
+ wfm_denoise: bool,
}
impl SoapySdrRig {
@@ -115,6 +117,7 @@ impl SoapySdrRig {
audio_channels,
frame_duration_ms,
wfm_deemphasis_us,
+ true, // wfm_denoise: enabled by default
channels,
);
@@ -182,6 +185,7 @@ impl SoapySdrRig {
center_hz: hardware_center_hz,
retune_cmd,
wfm_deemphasis_us,
+ wfm_denoise: true,
})
}
@@ -460,12 +464,26 @@ impl RigCat for SoapySdrRig {
})
}
+ fn set_wfm_denoise<'a>(
+ &'a mut self,
+ enabled: bool,
+ ) -> Pin> + Send + 'a>> {
+ Box::pin(async move {
+ self.wfm_denoise = enabled;
+ if let Some(dsp_arc) = self.pipeline.channel_dsps.get(self.primary_channel_idx) {
+ dsp_arc.lock().unwrap().set_wfm_denoise(enabled);
+ }
+ Ok(())
+ })
+ }
+
fn filter_state(&self) -> Option {
Some(RigFilterState {
bandwidth_hz: self.bandwidth_hz,
fir_taps: self.fir_taps,
cw_center_hz: 700,
wfm_deemphasis_us: self.wfm_deemphasis_us,
+ wfm_denoise: self.wfm_denoise,
})
}