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 31c09dd..ada9e63 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 @@ -11,14 +11,13 @@ const RDS_BPF_Q: f32 = 10.0; /// Pilot tone frequency (Hz). const PILOT_HZ: f32 = 19_000.0; /// Audio bandwidth for WFM (Hz). -/// 15.8 kHz leaves more guard band below the 19 kHz pilot and reduces -/// top-end artifacts on strong signals while still preserving the useful -/// broadcast audio range. -const AUDIO_BW_HZ: f32 = 15_800.0; +/// 17 kHz preserves more top-end while still leaving guard band below the +/// 19 kHz pilot. +const AUDIO_BW_HZ: f32 = 17_000.0; /// Stereo L-R subchannel bandwidth for WFM (Hz). /// Keep this a bit lower than the mono path because the recovered difference /// signal is noisier and more prone to high-frequency artifacts. -const STEREO_DIFF_BW_HZ: f32 = 14_500.0; +const STEREO_DIFF_BW_HZ: f32 = 15_800.0; /// Q values for a proper 4th-order Butterworth cascade (two 2nd-order stages). /// Stage 1: Q = 1 / (2 cos(π/8)) const BW4_Q1: f32 = 0.5412; @@ -100,7 +99,6 @@ fn polyphase_resample( .sum() } -#[inline] fn smoothstep01(x: f32) -> f32 { let x = x.clamp(0.0, 1.0); x * x * (3.0 - 2.0 * x) @@ -222,24 +220,6 @@ impl SoftAgc { (x * gain).clamp(-1.0, 1.0) } - pub(crate) fn process_with_level(&mut self, x: f32, level: f32) -> f32 { - let gain = self.update_gain(level.abs()); - (x * gain).clamp(-1.0, 1.0) - } - - pub(crate) fn process_pair_with_level( - &mut self, - left: f32, - right: f32, - level: f32, - ) -> (f32, f32) { - let gain = self.update_gain(level.abs()); - ( - (left * gain).clamp(-1.0, 1.0), - (right * gain).clamp(-1.0, 1.0), - ) - } - pub(crate) fn process_complex(&mut self, x: Complex) -> Complex { let gain = self.update_gain((x.re * x.re + x.im * x.im).sqrt()); let mut y = x * gain; 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 edc7280..35fe1d6 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 @@ -267,15 +267,12 @@ impl BlockFirFilter { /// 500 ms / 5 s only reacts to slow carrier-amplitude fading, not audio. /// /// CW uses a fast attack/release to follow individual dots and dashes. -/// WFM uses a linked downward-only peak limiter (max gain = 1.0) so louder -/// stations are tamed without boosting quieter ones. /// All other modes use 5 ms / 500 ms, suitable for SSB voice and FM. fn agc_for_mode(mode: &RigMode, audio_sample_rate: u32) -> SoftAgc { let sr = audio_sample_rate.max(1) as f32; match mode { RigMode::CW | RigMode::CWR => SoftAgc::new(sr, 1.0, 50.0, 0.5, 30.0), RigMode::AM => SoftAgc::new(sr, 500.0, 5_000.0, 0.5, 30.0), - RigMode::WFM => SoftAgc::new(sr, 0.2, 80.0, 0.92, 0.0), _ => SoftAgc::new(sr, 5.0, 500.0, 0.5, 30.0), } } @@ -305,10 +302,6 @@ fn dc_for_mode(mode: &RigMode) -> Option { } } -fn wfm_limiter_sidechain() -> Option<(DcBlocker, DcBlocker)> { - Some((DcBlocker::new(0.985), DcBlocker::new(0.985))) -} - // --------------------------------------------------------------------------- // Channel DSP context // --------------------------------------------------------------------------- @@ -366,9 +359,6 @@ pub struct ChannelDsp { iq_agc: Option, /// Soft AGC applied to all demodulated audio for consistent cross-mode levels. audio_agc: SoftAgc, - /// Optional high-passed detector path for the WFM limiter so bass does not - /// dominate gain reduction and smear treble. - limiter_sidechain: Option<(DcBlocker, DcBlocker)>, /// DC blocker for modes whose demodulator output can carry a DC offset /// (USB/LSB/AM/FM/DIG). None for CW and WFM. audio_dc: Option, @@ -450,11 +440,6 @@ impl ChannelDsp { } self.iq_agc = iq_agc_for_mode(&self.mode, channel_sample_rate); self.audio_agc = agc_for_mode(&self.mode, self.audio_sample_rate); - self.limiter_sidechain = if self.mode == RigMode::WFM { - wfm_limiter_sidechain() - } else { - None - }; self.audio_dc = dc_for_mode(&self.mode); self.frame_buf.clear(); } @@ -542,11 +527,6 @@ impl ChannelDsp { }, iq_agc: iq_agc_for_mode(mode, channel_sample_rate), audio_agc: agc_for_mode(mode, audio_sample_rate), - limiter_sidechain: if *mode == RigMode::WFM { - wfm_limiter_sidechain() - } else { - None - }, audio_dc: dc_for_mode(mode), } } @@ -701,44 +681,10 @@ impl ChannelDsp { // --- 4. Demodulate + post-process ----------------------------------- // WFM: full composite decoder (handles its own DC blocks + deemphasis). // All other modes: stateless demodulator → DC blocker (where enabled) → AGC. - // WFM uses linked audio AGC after stereo decode; all other modes use - // the normal post-demod AGC path. + // WFM bypasses post-audio AGC so the deemphasized stereo path is + // heard directly; all other modes use the normal post-demod AGC path. let audio = if let Some(decoder) = self.wfm_decoder.as_mut() { - let mut out = decoder.process_iq(&decimated); - if !self.wfm_stereo && self.output_channels >= 2 { - for pair in out.chunks_exact_mut(2) { - let detect = if let Some((left_sc, _)) = &mut self.limiter_sidechain { - left_sc.process(pair[0]).abs() - } else { - pair[0].abs() - }; - let mono = self.audio_agc.process_with_level(pair[0], detect); - pair[0] = mono; - pair[1] = mono; - } - } else if self.wfm_stereo && self.output_channels >= 2 { - for pair in out.chunks_exact_mut(2) { - let detect = if let Some((left_sc, right_sc)) = &mut self.limiter_sidechain { - left_sc.process(pair[0]).abs().max(right_sc.process(pair[1]).abs()) - } else { - pair[0].abs().max(pair[1].abs()) - }; - let (left, right) = - self.audio_agc.process_pair_with_level(pair[0], pair[1], detect); - pair[0] = left; - pair[1] = right; - } - } else { - for s in &mut out { - let detect = if let Some((left_sc, _)) = &mut self.limiter_sidechain { - left_sc.process(*s).abs() - } else { - s.abs() - }; - *s = self.audio_agc.process_with_level(*s, detect); - } - } - out + decoder.process_iq(&decimated) } else { let mut raw = self.demodulator.demodulate(&decimated); for s in &mut raw {