[fix](trx-backend-soapysdr): use narrow carrier IIR for WFM signal strength

Replace peak |IQ|² measurement with a per-sample single-pole IIR lowpass
on the instantaneous power (~500 Hz cutoff).  FM has constant envelope so
the IIR converges to the true carrier power A², rejecting wideband noise
that previously inflated the peak reading and masked actual signal level.

Other modes keep the existing peak + EMA approach.

https://claude.ai/code/session_01X6tedMVpjX3DEqLFDBR7FK
Signed-off-by: Claude <noreply@anthropic.com>
This commit is contained in:
Claude
2026-03-27 16:28:24 +00:00
committed by Stan Grams
parent cf82c853cf
commit 0ad3440a2d
@@ -297,9 +297,23 @@ pub struct ChannelDsp {
squelch: VirtualSquelch, squelch: VirtualSquelch,
noise_blanker: NoiseBlanker, noise_blanker: NoiseBlanker,
last_signal_db: f32, 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,
} }
impl ChannelDsp { 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.
fn narrow_carrier_alpha(channel_sample_rate: u32) -> f32 {
const CARRIER_BW_HZ: f32 = 500.0;
if channel_sample_rate == 0 {
return 0.1;
}
(std::f32::consts::TAU * CARRIER_BW_HZ / channel_sample_rate as f32).min(1.0)
}
fn clamp_bandwidth_for_mode(mode: &RigMode, bandwidth_hz: u32) -> u32 { fn clamp_bandwidth_for_mode(mode: &RigMode, bandwidth_hz: u32) -> u32 {
match mode { match mode {
// SAM stereo requires ≥ 9 kHz to capture both sum (L+R) and difference // SAM stereo requires ≥ 9 kHz to capture both sum (L+R) and difference
@@ -408,6 +422,8 @@ impl ChannelDsp {
self.iq_agc = iq_agc_for_mode(&self.mode, channel_sample_rate); 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_agc = agc_for_mode(&self.mode, self.audio_sample_rate);
self.audio_dc = dc_for_mode(&self.mode); 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.frame_buf.clear(); self.frame_buf.clear();
self.frame_buf_offset = 0; self.frame_buf_offset = 0;
} }
@@ -523,6 +539,8 @@ impl ChannelDsp {
squelch: VirtualSquelch::new(squelch_cfg), squelch: VirtualSquelch::new(squelch_cfg),
noise_blanker: NoiseBlanker::new(nb_cfg.enabled, nb_cfg.threshold), noise_blanker: NoiseBlanker::new(nb_cfg.enabled, nb_cfg.threshold),
last_signal_db: -120.0, last_signal_db: -120.0,
carrier_pwr_iir: 0.0,
carrier_pwr_alpha: Self::narrow_carrier_alpha(channel_sample_rate),
} }
} }
@@ -734,17 +752,30 @@ impl ChannelDsp {
return; return;
} }
// Signal strength: peak IQ magnitude of filtered+decimated signal // Signal strength measurement (before AGC).
// BEFORE AGC. For FM (constant envelope) peak ≈ carrier power.
// EMA (α = 0.4) for fast response with light jitter reduction.
{ {
const SIGNAL_EMA_ALPHA: f32 = 0.4; if self.mode == RigMode::WFM {
let peak_power = decimated // WFM: narrow carrier measurement via per-sample IIR on |IQ|².
.iter() // FM has constant envelope so IIR converges to carrier power A²,
.map(|s| s.re * s.re + s.im * s.im) // rejecting wideband noise that inflates a peak reading.
.fold(0.0_f32, f32::max); let alpha = self.carrier_pwr_alpha;
let peak_db = 10.0 * peak_power.max(1e-12).log10(); for s in decimated.iter() {
self.last_signal_db += SIGNAL_EMA_ALPHA * (peak_db - self.last_signal_db); let pwr = s.re * s.re + s.im * s.im;
self.carrier_pwr_iir += alpha * (pwr - self.carrier_pwr_iir);
}
self.last_signal_db =
10.0 * self.carrier_pwr_iir.max(1e-12).log10();
} else {
// Other modes: peak IQ magnitude with EMA smoothing.
const SIGNAL_EMA_ALPHA: f32 = 0.4;
let peak_power = decimated
.iter()
.map(|s| s.re * s.re + s.im * s.im)
.fold(0.0_f32, f32::max);
let peak_db = 10.0 * peak_power.max(1e-12).log10();
self.last_signal_db +=
SIGNAL_EMA_ALPHA * (peak_db - self.last_signal_db);
}
} }
if let Some(iq_agc) = &mut self.iq_agc { if let Some(iq_agc) = &mut self.iq_agc {