[fix](trx-backend-soapysdr): measure WFM signal strength in IQ domain, not power domain
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 <noreply@anthropic.com>
This commit is contained in:
@@ -297,15 +297,18 @@ 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).
|
/// Single-pole IIR states for narrow IQ lowpass (WFM carrier measurement).
|
||||||
carrier_pwr_iir: f32,
|
/// Filtering the IQ signal (not the power) rejects out-of-band noise so the
|
||||||
/// IIR coefficient for the narrow carrier filter, precomputed from sample rate.
|
/// meter reads carrier level, not total wideband noise power.
|
||||||
carrier_pwr_alpha: f32,
|
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 {
|
impl ChannelDsp {
|
||||||
/// Compute the single-pole IIR alpha for narrow carrier power measurement.
|
/// Compute the single-pole IIR alpha for narrow IQ carrier measurement.
|
||||||
/// Uses ~500 Hz cutoff so the meter reads carrier envelope, not wideband noise.
|
/// Uses ~500 Hz cutoff so the meter reads carrier level, not wideband noise.
|
||||||
fn narrow_carrier_alpha(channel_sample_rate: u32) -> f32 {
|
fn narrow_carrier_alpha(channel_sample_rate: u32) -> f32 {
|
||||||
const CARRIER_BW_HZ: f32 = 500.0;
|
const CARRIER_BW_HZ: f32 = 500.0;
|
||||||
if channel_sample_rate == 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.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_iq_alpha = Self::narrow_carrier_alpha(channel_sample_rate);
|
||||||
self.carrier_pwr_iir = 0.0;
|
self.carrier_iq_i = 0.0;
|
||||||
|
self.carrier_iq_q = 0.0;
|
||||||
self.frame_buf.clear();
|
self.frame_buf.clear();
|
||||||
self.frame_buf_offset = 0;
|
self.frame_buf_offset = 0;
|
||||||
}
|
}
|
||||||
@@ -539,8 +543,9 @@ 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_iq_i: 0.0,
|
||||||
carrier_pwr_alpha: Self::narrow_carrier_alpha(channel_sample_rate),
|
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).
|
// Signal strength measurement (before AGC).
|
||||||
{
|
{
|
||||||
if self.mode == RigMode::WFM {
|
if self.mode == RigMode::WFM {
|
||||||
// WFM: narrow carrier measurement via per-sample IIR on |IQ|².
|
// WFM: narrow-band carrier measurement via IQ-domain lowpass.
|
||||||
// FM has constant envelope so IIR converges to carrier power A²,
|
// A single-pole IIR on each of I and Q (≈500 Hz cutoff) rejects
|
||||||
// rejecting wideband noise that inflates a peak reading.
|
// wideband noise *before* computing power, so the meter reads
|
||||||
let alpha = self.carrier_pwr_alpha;
|
// carrier level rather than total noise across the 180 kHz channel.
|
||||||
|
let alpha = self.carrier_iq_alpha;
|
||||||
for s in decimated.iter() {
|
for s in decimated.iter() {
|
||||||
let pwr = s.re * s.re + s.im * s.im;
|
self.carrier_iq_i += alpha * (s.re - self.carrier_iq_i);
|
||||||
self.carrier_pwr_iir += alpha * (pwr - self.carrier_pwr_iir);
|
self.carrier_iq_q += alpha * (s.im - self.carrier_iq_q);
|
||||||
}
|
}
|
||||||
self.last_signal_db =
|
let carrier_pwr =
|
||||||
10.0 * self.carrier_pwr_iir.max(1e-12).log10();
|
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 {
|
} else {
|
||||||
// Other modes: peak IQ magnitude with EMA smoothing.
|
// Other modes: peak IQ magnitude with EMA smoothing.
|
||||||
const SIGNAL_EMA_ALPHA: f32 = 0.4;
|
const SIGNAL_EMA_ALPHA: f32 = 0.4;
|
||||||
|
|||||||
Reference in New Issue
Block a user