From b0d8ce8e299cc9c9151009111798e8760e7147b2 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sun, 1 Mar 2026 00:35:19 +0100 Subject: [PATCH] [fix](trx-backend-soapysdr): restore wfm agc and cutoffs Signed-off-by: Stan Grams --- .../trx-backend-soapysdr/src/demod.rs | 17 +++++++++--- .../trx-backend-soapysdr/src/dsp.rs | 27 +++++++++++++++---- 2 files changed, 35 insertions(+), 9 deletions(-) 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 fea69e9..e736cae 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,13 +11,14 @@ const RDS_BPF_Q: f32 = 10.0; /// Pilot tone frequency (Hz). const PILOT_HZ: f32 = 19_000.0; /// Audio bandwidth for WFM (Hz). -/// 17 kHz preserves more top-end while still leaving guard band below the -/// 19 kHz pilot. -const AUDIO_BW_HZ: f32 = 17_000.0; +/// 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; /// 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 = 15_800.0; +const STEREO_DIFF_BW_HZ: f32 = 14_500.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; @@ -220,6 +221,14 @@ impl SoftAgc { (x * gain).clamp(-1.0, 1.0) } + pub(crate) fn process_pair(&mut self, left: f32, right: f32) -> (f32, f32) { + let gain = self.update_gain(left.abs().max(right.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 6c8c0c9..7007c4d 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 @@ -690,12 +690,29 @@ 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 bypasses post-audio AGC so the deemphasized stereo path is - // heard directly; all other modes use the normal post-demod AGC path. + // WFM: full composite decoder (handles its own DC blocks + deemphasis), + // then apply post-audio AGC on the decoded PCM. Other modes use the + // normal stateless demodulator → DC blocker (where enabled) → AGC path. let audio = if let Some(decoder) = self.wfm_decoder.as_mut() { - decoder.process_iq(&decimated) + let mut out = decoder.process_iq(&decimated); + if !self.wfm_stereo && self.output_channels >= 2 { + for pair in out.chunks_exact_mut(2) { + let mono = self.audio_agc.process(pair[0]); + pair[0] = mono; + pair[1] = mono; + } + } else if self.wfm_stereo && self.output_channels >= 2 { + for pair in out.chunks_exact_mut(2) { + let (left, right) = self.audio_agc.process_pair(pair[0], pair[1]); + pair[0] = left; + pair[1] = right; + } + } else { + for sample in &mut out { + *sample = self.audio_agc.process(*sample); + } + } + out } else { let mut raw = self.demodulator.demodulate(&decimated); for s in &mut raw {