From 3ca38367020f10904c34597d00473ab0333c688a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 27 Mar 2026 17:07:54 +0000 Subject: [PATCH] [fix](trx-backend-soapysdr): measure WFM signal strength in IQ domain, not power domain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous carrier power IIR filtered |IQ|² (power), which only smoothed temporal fluctuations but still integrated noise across the full 180 kHz WFM channel bandwidth. This caused background noise to read ~-78 dBFS instead of the expected ~-110 dBFS (~32 dB too high ≈ 10·log₁₀(180kHz/500Hz)). Move the single-pole IIR lowpass to the IQ domain (filter I and Q separately at ~500 Hz cutoff), then compute power from the filtered output. This rejects out-of-band noise before the power measurement, so the meter reads true carrier level rather than total wideband noise. https://claude.ai/code/session_01W4WPMB2Lg3hgaY6opsk25f Signed-off-by: Claude --- .../trx-backend-soapysdr/src/dsp/channel.rs | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) 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 b53a69d..9c8b515 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 @@ -297,15 +297,18 @@ pub struct ChannelDsp { squelch: VirtualSquelch, noise_blanker: NoiseBlanker, last_signal_db: f32, - /// Per-sample IIR state for narrow carrier power measurement (WFM). - carrier_pwr_iir: f32, - /// IIR coefficient for the narrow carrier filter, precomputed from sample rate. - carrier_pwr_alpha: f32, + /// Single-pole IIR states for narrow IQ lowpass (WFM carrier measurement). + /// Filtering the IQ signal (not the power) rejects out-of-band noise so the + /// meter reads carrier level, not total wideband noise power. + carrier_iq_i: f32, + carrier_iq_q: f32, + /// IIR coefficient for the narrow IQ carrier filter, precomputed from sample rate. + carrier_iq_alpha: f32, } impl ChannelDsp { - /// Compute the single-pole IIR alpha for narrow carrier power measurement. - /// Uses ~500 Hz cutoff so the meter reads carrier envelope, not wideband noise. + /// Compute the single-pole IIR alpha for narrow IQ carrier measurement. + /// Uses ~500 Hz cutoff so the meter reads carrier level, not wideband noise. fn narrow_carrier_alpha(channel_sample_rate: u32) -> f32 { const CARRIER_BW_HZ: f32 = 500.0; if channel_sample_rate == 0 { @@ -422,8 +425,9 @@ 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.audio_dc = dc_for_mode(&self.mode); - self.carrier_pwr_alpha = Self::narrow_carrier_alpha(channel_sample_rate); - self.carrier_pwr_iir = 0.0; + self.carrier_iq_alpha = Self::narrow_carrier_alpha(channel_sample_rate); + self.carrier_iq_i = 0.0; + self.carrier_iq_q = 0.0; self.frame_buf.clear(); self.frame_buf_offset = 0; } @@ -539,8 +543,9 @@ impl ChannelDsp { squelch: VirtualSquelch::new(squelch_cfg), noise_blanker: NoiseBlanker::new(nb_cfg.enabled, nb_cfg.threshold), last_signal_db: -120.0, - carrier_pwr_iir: 0.0, - carrier_pwr_alpha: Self::narrow_carrier_alpha(channel_sample_rate), + carrier_iq_i: 0.0, + carrier_iq_q: 0.0, + carrier_iq_alpha: Self::narrow_carrier_alpha(channel_sample_rate), } } @@ -755,16 +760,18 @@ impl ChannelDsp { // Signal strength measurement (before AGC). { if self.mode == RigMode::WFM { - // WFM: narrow carrier measurement via per-sample IIR on |IQ|². - // FM has constant envelope so IIR converges to carrier power A², - // rejecting wideband noise that inflates a peak reading. - let alpha = self.carrier_pwr_alpha; + // WFM: narrow-band carrier measurement via IQ-domain lowpass. + // A single-pole IIR on each of I and Q (≈500 Hz cutoff) rejects + // wideband noise *before* computing power, so the meter reads + // carrier level rather than total noise across the 180 kHz channel. + let alpha = self.carrier_iq_alpha; for s in decimated.iter() { - let pwr = s.re * s.re + s.im * s.im; - self.carrier_pwr_iir += alpha * (pwr - self.carrier_pwr_iir); + self.carrier_iq_i += alpha * (s.re - self.carrier_iq_i); + self.carrier_iq_q += alpha * (s.im - self.carrier_iq_q); } - self.last_signal_db = - 10.0 * self.carrier_pwr_iir.max(1e-12).log10(); + let carrier_pwr = + self.carrier_iq_i * self.carrier_iq_i + self.carrier_iq_q * self.carrier_iq_q; + self.last_signal_db = 10.0 * carrier_pwr.max(1e-12).log10(); } else { // Other modes: peak IQ magnitude with EMA smoothing. const SIGNAL_EMA_ALPHA: f32 = 0.4;