From b7c1da138ba8543248f5a4d41518fbe8bdd0f63a Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sat, 28 Feb 2026 20:11:15 +0100 Subject: [PATCH] [fix](trx-backend-soapysdr): fix WFM stereo separation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 19 kHz pilot notch was applied only to the L+R sum path, introducing ~22° of phase shift at 15 kHz relative to the L-R diff path. This phase mismatch caused interchannel crosstalk (≈ −14 dB separation at 15 kHz). Fix: remove the notch from the sum processing chain so both sum and diff pass through identical 4th-order Butterworth LPFs, giving phase-coherent demodulation across the full audio band. The notch is relocated to the mono output branch where phase alignment with the diff channel is not required. Pilot rejection on the stereo L/R outputs is still adequate (~28 dB) from the combined LPF + deemphasis response at 19 kHz. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Stan Grams --- .../trx-backend-soapysdr/src/demod.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 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 d2c04da..54136aa 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 @@ -356,7 +356,8 @@ pub struct WfmStereoDecoder { /// 4th-order Butterworth cascade for L+R (two 2nd-order stages, Q = BW4_Q1/BW4_Q2). sum_lpf1: BiquadLowPass, sum_lpf2: BiquadLowPass, - /// Notch at 19 kHz to suppress pilot tone leakage in the L+R channel. + /// Notch at 19 kHz for the mono output path — keeps pilot tone out of mono + /// audio without introducing phase mismatch with the diff channel. sum_notch: BiquadNotch, /// 4th-order Butterworth cascade for L-R (matched to sum path for stereo phase accuracy). diff_lpf1: BiquadLowPass, @@ -461,8 +462,12 @@ impl WfmStereoDecoder { let rds_clean = self.rds_dc.process(rds_band); let _ = self.rds_decoder.process_sample(rds_clean, rds_quality); - // --- L+R (sum): 4th-order Butterworth + pilot notch --- - let sum = self.sum_notch.process(self.sum_lpf2.process(self.sum_lpf1.process(x))); + // --- L+R (sum): 4th-order Butterworth --- + // The pilot notch is NOT applied here so the sum and diff paths have + // identical phase responses, which is required for good stereo separation. + // The notch is applied only on the mono output path where phase matching + // with the diff channel is irrelevant. + let sum = self.sum_lpf2.process(self.sum_lpf1.process(x)); // --- L-R (diff): 38 kHz demod + 4th-order Butterworth (unblended) --- // Blend is applied per-band at audio rate in the emit step below. @@ -513,9 +518,12 @@ impl WfmStereoDecoder { output.push(left); output.push(right); } else { + // Mono path: apply the pilot notch here so the 19 kHz pilot tone + // does not leak into mono audio. Phase matching with diff is not + // a concern for mono, so the notch can sit anywhere in the chain. output.push( self.dc_m - .process(self.deemph_m.process(sum_i)) + .process(self.deemph_m.process(self.sum_notch.process(sum_i))) .clamp(-1.0, 1.0), ); }