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 a59cc3a..d76dae6 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 @@ -1428,8 +1428,10 @@ function render(update) { saveSetting("wfmAudioMode", nextMode); } } - if (wfmDenoiseEl && typeof update.filter.wfm_denoise === "boolean") { - const nextDenoise = update.filter.wfm_denoise ? "on" : "off"; + if (wfmDenoiseEl && (typeof update.filter.wfm_denoise === "string" || typeof update.filter.wfm_denoise === "boolean")) { + const nextDenoise = typeof update.filter.wfm_denoise === "string" + ? normalizeWfmDenoiseLevel(update.filter.wfm_denoise) + : (update.filter.wfm_denoise ? "auto" : "low"); if (wfmDenoiseEl.value !== nextDenoise) { wfmDenoiseEl.value = nextDenoise; saveSetting("wfmDenoise", nextDenoise); @@ -2990,6 +2992,13 @@ function levelFromChannels(channels, frameCount) { return Math.min(100, rms * 220); } +function normalizeWfmDenoiseLevel(value) { + const next = String(value ?? "").toLowerCase(); + if (next === "auto" || next === "low" || next === "medium" || next === "high") return next; + if (next === "off") return "low"; + return "auto"; +} + if (wfmAudioModeEl) { wfmAudioModeEl.value = loadSetting("wfmAudioMode", "stereo"); wfmAudioModeEl.addEventListener("change", () => { @@ -2999,11 +3008,12 @@ if (wfmAudioModeEl) { }); } if (wfmDenoiseEl) { - wfmDenoiseEl.value = loadSetting("wfmDenoise", "on"); + wfmDenoiseEl.value = normalizeWfmDenoiseLevel(loadSetting("wfmDenoise", "auto")); wfmDenoiseEl.addEventListener("change", () => { - saveSetting("wfmDenoise", wfmDenoiseEl.value); - const enabled = wfmDenoiseEl.value !== "off"; - postPath(`/set_wfm_denoise?enabled=${enabled ? "true" : "false"}`).catch(() => {}); + const level = normalizeWfmDenoiseLevel(wfmDenoiseEl.value); + wfmDenoiseEl.value = level; + saveSetting("wfmDenoise", level); + postPath(`/set_wfm_denoise?level=${encodeURIComponent(level)}`).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 b5d4a92..df30bed 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 @@ -14,6 +14,7 @@ use tokio::time::{self, Duration}; use tokio_stream::wrappers::{IntervalStream, WatchStream}; use trx_core::radio::freq::Freq; +use trx_core::rig::state::WfmDenoiseLevel; use trx_core::rig::{RigAccessMethod, RigCapabilities, RigInfo}; use trx_core::{RigCommand, RigRequest, RigSnapshot, RigState}; use trx_frontend::{FrontendRuntimeContext, RemoteRigEntry}; @@ -551,7 +552,7 @@ pub async fn set_wfm_stereo( #[derive(serde::Deserialize)] pub struct WfmDenoiseQuery { - pub enabled: bool, + pub level: WfmDenoiseLevel, } #[post("/set_wfm_denoise")] @@ -559,7 +560,7 @@ pub async fn set_wfm_denoise( query: web::Query, rig_tx: web::Data>, ) -> Result { - send_command(&rig_tx, RigCommand::SetWfmDenoise(query.enabled)).await + send_command(&rig_tx, RigCommand::SetWfmDenoise(query.level)).await } #[post("/toggle_aprs_decode")] diff --git a/src/trx-core/src/lib.rs b/src/trx-core/src/lib.rs index 32acd2b..2fe326b 100644 --- a/src/trx-core/src/lib.rs +++ b/src/trx-core/src/lib.rs @@ -13,5 +13,5 @@ pub type DynResult = Result>; pub use rig::command::RigCommand; pub use rig::request::RigRequest; pub use rig::response::{RigError, RigResult}; -pub use rig::state::{RdsData, RigFilterState, RigMode, RigSnapshot, RigState}; +pub use rig::state::{RdsData, RigFilterState, RigMode, RigSnapshot, RigState, WfmDenoiseLevel}; pub use rig::AudioSource; diff --git a/src/trx-core/src/rig/command.rs b/src/trx-core/src/rig/command.rs index f7e2941..1c874e2 100644 --- a/src/trx-core/src/rig/command.rs +++ b/src/trx-core/src/rig/command.rs @@ -3,6 +3,7 @@ // SPDX-License-Identifier: BSD-2-Clause use crate::radio::freq::Freq; +use crate::rig::state::WfmDenoiseLevel; use crate::RigMode; /// Internal command handled by the rig task. @@ -36,6 +37,6 @@ pub enum RigCommand { SetSdrGain(f64), SetWfmDeemphasis(u32), SetWfmStereo(bool), - SetWfmDenoise(bool), + SetWfmDenoise(WfmDenoiseLevel), GetSpectrum, } diff --git a/src/trx-core/src/rig/mod.rs b/src/trx-core/src/rig/mod.rs index 72825d8..c97416e 100644 --- a/src/trx-core/src/rig/mod.rs +++ b/src/trx-core/src/rig/mod.rs @@ -187,7 +187,7 @@ pub trait RigCat: Rig + Send { fn set_wfm_denoise<'a>( &'a mut self, - _enabled: bool, + _level: state::WfmDenoiseLevel, ) -> Pin> + Send + 'a>> { Box::pin(std::future::ready(Err( Box::new(response::RigError::not_supported("set_wfm_denoise")) diff --git a/src/trx-core/src/rig/state.rs b/src/trx-core/src/rig/state.rs index b11a7ba..edfdf65 100644 --- a/src/trx-core/src/rig/state.rs +++ b/src/trx-core/src/rig/state.rs @@ -277,8 +277,17 @@ pub struct RigFilterState { pub wfm_stereo: bool, #[serde(default)] pub wfm_stereo_detected: bool, - #[serde(default = "default_wfm_denoise")] - pub wfm_denoise: bool, + #[serde(default = "default_wfm_denoise_level")] + pub wfm_denoise: WfmDenoiseLevel, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum WfmDenoiseLevel { + Auto, + Low, + Medium, + High, } fn default_wfm_deemphasis_us() -> u32 { @@ -289,8 +298,8 @@ fn default_wfm_stereo() -> bool { true } -fn default_wfm_denoise() -> bool { - true +fn default_wfm_denoise_level() -> WfmDenoiseLevel { + WfmDenoiseLevel::Auto } /// Spectrum data from SDR backends (FFT magnitude over the full capture bandwidth). diff --git a/src/trx-protocol/src/mapping.rs b/src/trx-protocol/src/mapping.rs index e22ef50..68b24e8 100644 --- a/src/trx-protocol/src/mapping.rs +++ b/src/trx-protocol/src/mapping.rs @@ -53,7 +53,7 @@ pub fn client_command_to_rig(cmd: ClientCommand) -> RigCommand { RigCommand::SetWfmDeemphasis(deemphasis_us) } ClientCommand::SetWfmStereo { enabled } => RigCommand::SetWfmStereo(enabled), - ClientCommand::SetWfmDenoise { enabled } => RigCommand::SetWfmDenoise(enabled), + ClientCommand::SetWfmDenoise { level } => RigCommand::SetWfmDenoise(level), ClientCommand::GetSpectrum => RigCommand::GetSpectrum, } } @@ -100,7 +100,7 @@ pub fn rig_command_to_client(cmd: RigCommand) -> ClientCommand { ClientCommand::SetWfmDeemphasis { deemphasis_us } } RigCommand::SetWfmStereo(enabled) => ClientCommand::SetWfmStereo { enabled }, - RigCommand::SetWfmDenoise(enabled) => ClientCommand::SetWfmDenoise { enabled }, + RigCommand::SetWfmDenoise(level) => ClientCommand::SetWfmDenoise { level }, RigCommand::GetSpectrum => ClientCommand::GetSpectrum, } } diff --git a/src/trx-protocol/src/types.rs b/src/trx-protocol/src/types.rs index 7d828d4..09e61a2 100644 --- a/src/trx-protocol/src/types.rs +++ b/src/trx-protocol/src/types.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize}; use trx_core::rig::state::RigSnapshot; +use trx_core::WfmDenoiseLevel; /// Command received from network clients (JSON). #[derive(Debug, Serialize, Deserialize)] @@ -41,7 +42,7 @@ pub enum ClientCommand { SetSdrGain { gain_db: f64 }, SetWfmDeemphasis { deemphasis_us: u32 }, SetWfmStereo { enabled: bool }, - SetWfmDenoise { enabled: bool }, + SetWfmDenoise { level: WfmDenoiseLevel }, GetSpectrum, } diff --git a/src/trx-server/src/rig_task.rs b/src/trx-server/src/rig_task.rs index 32d0c3a..1abde55 100644 --- a/src/trx-server/src/rig_task.rs +++ b/src/trx-server/src/rig_task.rs @@ -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); diff --git a/src/trx-server/trx-backend/trx-backend-soapysdr/src/demod/wfm.rs b/src/trx-server/trx-backend/trx-backend-soapysdr/src/demod/wfm.rs index 122e88a..043587e 100644 --- a/src/trx-server/trx-backend/trx-backend-soapysdr/src/demod/wfm.rs +++ b/src/trx-server/trx-backend/trx-backend-soapysdr/src/demod/wfm.rs @@ -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; diff --git a/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp/channel.rs b/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp/channel.rs index 5540c84..51c2a9f 100644 --- a/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp/channel.rs +++ b/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp/channel.rs @@ -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, @@ -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); } } 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 cead42e..4d2e513 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 @@ -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> + 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(()) })