From f7c10a01852279b3c53d311edd47fedfe8e7a338 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sun, 1 Mar 2026 11:17:14 +0100 Subject: [PATCH] [fix](trx-backend-soapysdr): remove agc from wfm chain, use fixed output gain Replace both IQ AGC and audio AGC in the WFM path with a fixed output gain of 0.35. AGC pumping on broadcast audio degrades stereo separation and introduces audible artifacts. The IQ hard limiter already normalizes input magnitude, and the WFM decoder's internal deemphasis + matrix gain provides consistent output levels. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Stan Grams --- .../trx-backend-soapysdr/src/demod.rs | 1 + .../trx-backend-soapysdr/src/dsp.rs | 29 ++++++------------- 2 files changed, 10 insertions(+), 20 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 aea2033..052e7e7 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 @@ -348,6 +348,7 @@ impl SoftAgc { (x * gain).clamp(-1.0, 1.0) } + #[allow(dead_code)] pub(crate) fn process_pair(&mut self, left: f32, right: f32) -> (f32, f32) { let gain = self.update_gain(left.abs().max(right.abs())); ( 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 2602dbd..f3c8269 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 @@ -428,7 +428,9 @@ fn agc_for_mode(mode: &RigMode, audio_sample_rate: u32) -> SoftAgc { fn iq_agc_for_mode(mode: &RigMode, sample_rate: u32) -> Option { let sr = sample_rate.max(1) as f32; match mode { - RigMode::FM | RigMode::WFM | RigMode::PKT => Some(SoftAgc::new(sr, 0.5, 150.0, 0.8, 12.0)), + RigMode::FM | RigMode::PKT => Some(SoftAgc::new(sr, 0.5, 150.0, 0.8, 12.0)), + // WFM uses a hard IQ limiter instead of AGC to preserve phase accuracy. + RigMode::WFM => None, _ => None, } } @@ -864,27 +866,14 @@ impl ChannelDsp { } // --- 4. Demodulate + post-process ----------------------------------- - // 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. + // WFM: full composite decoder (handles its own DC blocks + deemphasis). + // No AGC — WFM uses IQ hard limiting + fixed output gain to preserve + // stereo separation and avoid AGC pumping on broadcast audio. + const WFM_OUTPUT_GAIN: f32 = 0.35; 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 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); - } + for sample in &mut out { + *sample = (*sample * WFM_OUTPUT_GAIN).clamp(-1.0, 1.0); } out } else {