[feat](trx-rs): expose WFM stereo denoise toggle
Add a server-side toggle for the multiband stereo denoiser so it can be enabled or disabled at runtime without restarting the server. Backend (trx-backend-soapysdr): - Add `denoise_enabled: bool` to `WfmStereoDecoder`; gate multiband blend behind it (falls back to uniform single-band blend when off) - Add `set_denoise_enabled()` method on `WfmStereoDecoder` - Propagate `wfm_denoise: bool` through `ChannelDsp`, `SdrPipeline`, and `SoapySdrRig`; add `set_wfm_denoise()` at each layer - Include `wfm_denoise` in `filter_state()` so it flows into snapshots Protocol / core (trx-core, trx-protocol, trx-server): - Add `SetWfmDenoise(bool)` to `RigCommand` and `ClientCommand` - Add default `set_wfm_denoise()` trait method to `RigCat` - Handle `SetWfmDenoise` in `rig_task.rs` and update `RigFilterState` - Add `wfm_denoise: bool` (default `true`) to `RigFilterState` Frontend (trx-frontend-http): - Add `POST /toggle_wfm_denoise` endpoint - Add "Denoise On/Off" button next to the stereo/mono audio picker - Sync button state from SSE filter snapshot (`update.filter.wfm_denoise`) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -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}")));
|
||||
|
||||
@@ -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<RdsData> {
|
||||
self.rds_decoder.snapshot()
|
||||
}
|
||||
|
||||
@@ -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<Vec<f32>>,
|
||||
) -> 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<RdsData> {
|
||||
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::<Vec<f32>>(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::<Vec<f32>>(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);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ pub struct SoapySdrRig {
|
||||
retune_cmd: Arc<std::sync::Mutex<Option<f64>>>,
|
||||
/// 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<Box<dyn std::future::Future<Output = DynResult<()>> + 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<RigFilterState> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user