[feat](trx-backend-soapysdr): add wfm denoise levels

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-01 19:22:02 +01:00
parent dc3c99b0ee
commit 616ff1b79e
12 changed files with 100 additions and 52 deletions
+3 -3
View File
@@ -480,12 +480,12 @@ 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 {
RigCommand::SetWfmDenoise(level) => {
if let Err(e) = ctx.rig.set_wfm_denoise(level).await {
return Err(RigError::communication(format!("set_wfm_denoise: {e}")));
}
if let Some(f) = ctx.state.filter.as_mut() {
f.wfm_denoise = enabled;
f.wfm_denoise = level;
}
let _ = ctx.state_tx.send(ctx.state.clone());
return snapshot_from(ctx.state);
@@ -3,7 +3,7 @@
// SPDX-License-Identifier: BSD-2-Clause
use num_complex::Complex;
use trx_core::rig::state::RdsData;
use trx_core::rig::state::{RdsData, WfmDenoiseLevel};
use trx_rds::RdsDecoder;
use super::{math::demod_fm_with_prev, DcBlocker};
@@ -387,7 +387,7 @@ impl DenoiseSubband {
#[derive(Debug, Clone)]
struct StereoDenoise {
bands: [DenoiseSubband; DENOISE_BANDS],
enabled: bool,
level: WfmDenoiseLevel,
}
impl StereoDenoise {
@@ -397,16 +397,12 @@ impl StereoDenoise {
});
Self {
bands,
enabled: true,
level: WfmDenoiseLevel::Auto,
}
}
#[inline]
fn process(&mut self, sum: f32, diff_i: f32, diff_q: f32) -> f32 {
if !self.enabled {
return diff_i;
}
let mut gain_sum = 0.0_f32;
let mut weight_sum = 0.0_f32;
for band in &mut self.bands {
@@ -420,7 +416,16 @@ impl StereoDenoise {
} else {
1.0
};
diff_i * broadband_gain
let effective_gain = match self.level {
WfmDenoiseLevel::Auto => {
let strength = (0.3 + (1.0 - broadband_gain) * 0.7).clamp(0.3, 1.0);
1.0 - (1.0 - broadband_gain) * strength
}
WfmDenoiseLevel::Low => 1.0 - (1.0 - broadband_gain) * 0.35,
WfmDenoiseLevel::Medium => 1.0 - (1.0 - broadband_gain) * 0.65,
WfmDenoiseLevel::High => broadband_gain,
};
diff_i * effective_gain.clamp(0.0, 1.0)
}
fn reset(&mut self) {
@@ -489,6 +494,7 @@ impl WfmStereoDecoder {
output_channels: usize,
stereo_enabled: bool,
deemphasis_us: u32,
denoise_level: WfmDenoiseLevel,
) -> 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;
@@ -538,7 +544,11 @@ impl WfmStereoDecoder {
diff_hist: [0.0; WFM_RESAMP_TAPS],
diff_q_hist: [0.0; WFM_RESAMP_TAPS],
hist_pos: 0,
denoise: StereoDenoise::new(audio_rate.max(1) as f32),
denoise: {
let mut denoise = StereoDenoise::new(audio_rate.max(1) as f32);
denoise.level = denoise_level;
denoise
},
prev_blend: 0.0,
output_phase_inc,
output_phase: 0.0,
@@ -768,8 +778,8 @@ impl WfmStereoDecoder {
self.output_phase = 0.0;
}
pub fn set_denoise_enabled(&mut self, enabled: bool) {
self.denoise.enabled = enabled;
pub fn set_denoise_level(&mut self, level: WfmDenoiseLevel) {
self.denoise.level = level;
}
pub fn stereo_detected(&self) -> bool {
@@ -815,7 +825,8 @@ mod tests {
iq.push(Complex::from_polar(1.0, phase));
}
let mut decoder = WfmStereoDecoder::new(composite_rate, audio_rate, 2, true, 50);
let mut decoder =
WfmStereoDecoder::new(composite_rate, audio_rate, 2, true, 50, WfmDenoiseLevel::Auto);
let output = decoder.process_iq(&iq);
let skip_samples = (0.2 * audio_rate as f32) as usize;
@@ -883,7 +894,14 @@ mod tests {
iq.push(Complex::from_polar(1.0, phase));
}
let mut decoder = WfmStereoDecoder::new(composite_rate, audio_rate, 2, true, 50);
let mut decoder = WfmStereoDecoder::new(
composite_rate,
audio_rate,
2,
true,
50,
WfmDenoiseLevel::Auto,
);
let output = decoder.process_iq(&iq);
let skip_samples = (0.3 * audio_rate as f32) as usize;
@@ -947,7 +965,8 @@ mod tests {
iq.push(Complex::from_polar(1.0, phase));
}
let mut decoder = WfmStereoDecoder::new(composite_rate, audio_rate, 2, true, 50);
let mut decoder =
WfmStereoDecoder::new(composite_rate, audio_rate, 2, true, 50, WfmDenoiseLevel::Auto);
let output = decoder.process_iq(&iq);
assert!(!decoder.stereo_detected());
@@ -1031,12 +1050,16 @@ mod tests {
}
#[test]
fn test_denoise_bypass_when_disabled() {
let mut denoise = StereoDenoise::new(48_000.0);
denoise.enabled = false;
fn test_denoise_low_preserves_more_than_high() {
let mut low = StereoDenoise::new(48_000.0);
low.level = WfmDenoiseLevel::Low;
let mut high = StereoDenoise::new(48_000.0);
high.level = WfmDenoiseLevel::High;
for &value in &[0.0_f32, 0.5, -0.3, 1.0, -1.0, 0.001] {
assert_eq!(denoise.process(0.1, value, 0.2), value);
let low_out = low.process(0.1, value, 0.2).abs();
let high_out = high.process(0.1, value, 0.2).abs();
assert!(low_out + 0.000_001 >= high_out);
}
}
@@ -1115,7 +1138,8 @@ mod tests {
iq.push(Complex::from_polar(1.0, phase));
}
let mut decoder = WfmStereoDecoder::new(composite_rate, audio_rate, 2, true, 50);
let mut decoder =
WfmStereoDecoder::new(composite_rate, audio_rate, 2, true, 50, WfmDenoiseLevel::Auto);
let output = decoder.process_iq(&iq);
let skip_samples = (0.2 * audio_rate as f32) as usize;
@@ -4,7 +4,7 @@
use num_complex::Complex;
use tokio::sync::broadcast;
use trx_core::rig::state::{RdsData, RigMode};
use trx_core::rig::state::{RdsData, RigMode, WfmDenoiseLevel};
use crate::demod::{DcBlocker, Demodulator, SoftAgc, WfmStereoDecoder};
@@ -59,7 +59,7 @@ pub struct ChannelDsp {
fir_taps: usize,
wfm_deemphasis_us: u32,
wfm_stereo: bool,
wfm_denoise: bool,
wfm_denoise: WfmDenoiseLevel,
pub decim_factor: usize,
output_channels: usize,
pub frame_buf: Vec<f32>,
@@ -153,6 +153,7 @@ impl ChannelDsp {
self.output_channels,
self.wfm_stereo,
self.wfm_deemphasis_us,
self.wfm_denoise,
));
}
} else {
@@ -214,7 +215,7 @@ impl ChannelDsp {
fir_taps: taps,
wfm_deemphasis_us,
wfm_stereo,
wfm_denoise: true,
wfm_denoise: WfmDenoiseLevel::Auto,
decim_factor,
output_channels,
frame_buf: Vec::with_capacity(frame_size + output_channels),
@@ -242,6 +243,7 @@ impl ChannelDsp {
output_channels,
wfm_stereo,
wfm_deemphasis_us,
WfmDenoiseLevel::Auto,
))
} else {
None
@@ -279,10 +281,10 @@ impl ChannelDsp {
}
}
pub fn set_wfm_denoise(&mut self, enabled: bool) {
self.wfm_denoise = enabled;
pub fn set_wfm_denoise(&mut self, level: WfmDenoiseLevel) {
self.wfm_denoise = level;
if let Some(decoder) = &mut self.wfm_decoder {
decoder.set_denoise_enabled(enabled);
decoder.set_denoise_level(level);
}
}
@@ -11,7 +11,7 @@ use std::sync::{Arc, Mutex};
use trx_core::radio::freq::{Band, Freq};
use trx_core::rig::response::RigError;
use trx_core::rig::state::{RigFilterState, SpectrumData};
use trx_core::rig::state::{RigFilterState, SpectrumData, WfmDenoiseLevel};
use trx_core::rig::{
AudioSource, Rig, RigAccessMethod, RigCapabilities, RigCat, RigInfo, RigStatusFuture,
};
@@ -42,7 +42,7 @@ pub struct SoapySdrRig {
/// Whether WFM stereo decode is enabled.
wfm_stereo: bool,
/// Whether WFM stereo denoise is enabled.
wfm_denoise: bool,
wfm_denoise: WfmDenoiseLevel,
/// Requested hardware gain setting in dB.
gain_db: f64,
/// Optional hard ceiling for the applied hardware gain in dB.
@@ -206,7 +206,7 @@ impl SoapySdrRig {
retune_cmd,
wfm_deemphasis_us,
wfm_stereo: true,
wfm_denoise: true,
wfm_denoise: WfmDenoiseLevel::Auto,
gain_db,
max_gain_db,
})
@@ -526,12 +526,12 @@ impl RigCat for SoapySdrRig {
fn set_wfm_denoise<'a>(
&'a mut self,
enabled: bool,
level: WfmDenoiseLevel,
) -> Pin<Box<dyn std::future::Future<Output = DynResult<()>> + Send + 'a>> {
Box::pin(async move {
self.wfm_denoise = enabled;
self.wfm_denoise = level;
if let Some(dsp_arc) = self.pipeline.channel_dsps.get(self.primary_channel_idx) {
dsp_arc.lock().unwrap().set_wfm_denoise(enabled);
dsp_arc.lock().unwrap().set_wfm_denoise(level);
}
Ok(())
})