[fix](trx-backend-soapysdr): stabilize WFM signal strength and speed up SDR polling
Smooth envelope power (I²+Q²) instead of filtering I/Q components separately — eliminates ~6 dB modulation-dependent fluctuation caused by FM carrier rotation in the IQ plane. Reset signal strength on frequency change. Reduce SDR poll interval from 500ms to 100ms. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -1049,6 +1049,10 @@ async fn main() -> DynResult<()> {
|
|||||||
);
|
);
|
||||||
if let Some(prebuilt) = sdr_prebuilt_rig {
|
if let Some(prebuilt) = sdr_prebuilt_rig {
|
||||||
task_config.prebuilt_rig = Some(prebuilt);
|
task_config.prebuilt_rig = Some(prebuilt);
|
||||||
|
// SDR signal strength is a pre-computed field read — no serial
|
||||||
|
// round-trip — so we can poll much faster than the CAT default.
|
||||||
|
task_config.polling =
|
||||||
|
AdaptivePolling::new(Duration::from_millis(100), Duration::from_millis(100));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spawn rig task.
|
// Spawn rig task.
|
||||||
|
|||||||
@@ -297,18 +297,15 @@ pub struct ChannelDsp {
|
|||||||
squelch: VirtualSquelch,
|
squelch: VirtualSquelch,
|
||||||
noise_blanker: NoiseBlanker,
|
noise_blanker: NoiseBlanker,
|
||||||
last_signal_db: f32,
|
last_signal_db: f32,
|
||||||
/// Single-pole IIR states for narrow IQ lowpass (WFM carrier measurement).
|
/// Single-pole IIR state for smoothed envelope power (WFM signal strength).
|
||||||
/// 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_i: f32,
|
||||||
carrier_iq_q: f32,
|
/// IIR coefficient for the envelope power smoother, precomputed from sample rate.
|
||||||
/// IIR coefficient for the narrow IQ carrier filter, precomputed from sample rate.
|
|
||||||
carrier_iq_alpha: f32,
|
carrier_iq_alpha: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChannelDsp {
|
impl ChannelDsp {
|
||||||
/// Compute the single-pole IIR alpha for narrow IQ carrier measurement.
|
/// Compute the single-pole IIR alpha for envelope power smoothing.
|
||||||
/// Uses ~500 Hz cutoff so the meter reads carrier level, not wideband noise.
|
/// Uses ~500 Hz cutoff for a responsive but stable S-meter reading.
|
||||||
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 {
|
||||||
@@ -333,6 +330,10 @@ impl ChannelDsp {
|
|||||||
} else {
|
} else {
|
||||||
2.0 * std::f64::consts::PI * channel_if_hz / self.sdr_sample_rate as f64
|
2.0 * std::f64::consts::PI * channel_if_hz / self.sdr_sample_rate as f64
|
||||||
};
|
};
|
||||||
|
// Reset signal strength so the meter doesn't show a stale reading
|
||||||
|
// from the previous frequency while the IIR catches up.
|
||||||
|
self.carrier_iq_i = 0.0;
|
||||||
|
self.last_signal_db = -120.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pipeline_rates(
|
fn pipeline_rates(
|
||||||
@@ -427,7 +428,6 @@ impl ChannelDsp {
|
|||||||
self.audio_dc = dc_for_mode(&self.mode);
|
self.audio_dc = dc_for_mode(&self.mode);
|
||||||
self.carrier_iq_alpha = Self::narrow_carrier_alpha(channel_sample_rate);
|
self.carrier_iq_alpha = Self::narrow_carrier_alpha(channel_sample_rate);
|
||||||
self.carrier_iq_i = 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;
|
||||||
}
|
}
|
||||||
@@ -544,7 +544,6 @@ impl ChannelDsp {
|
|||||||
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_iq_i: 0.0,
|
carrier_iq_i: 0.0,
|
||||||
carrier_iq_q: 0.0,
|
|
||||||
carrier_iq_alpha: Self::narrow_carrier_alpha(channel_sample_rate),
|
carrier_iq_alpha: Self::narrow_carrier_alpha(channel_sample_rate),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -760,18 +759,17 @@ impl ChannelDsp {
|
|||||||
// Signal strength measurement (before AGC).
|
// Signal strength measurement (before AGC).
|
||||||
{
|
{
|
||||||
if self.mode == RigMode::WFM {
|
if self.mode == RigMode::WFM {
|
||||||
// WFM: narrow-band carrier measurement via IQ-domain lowpass.
|
// WFM: smooth envelope power directly.
|
||||||
// A single-pole IIR on each of I and Q (≈500 Hz cutoff) rejects
|
// FM is constant-envelope, so I²+Q² is inherently stable
|
||||||
// wideband noise *before* computing power, so the meter reads
|
// regardless of modulation content. Averaging power (not I/Q
|
||||||
// carrier level rather than total noise across the 180 kHz channel.
|
// components) avoids the ~6 dB dip that occurs when modulation
|
||||||
|
// rotates the carrier away from DC in the IQ plane.
|
||||||
let alpha = self.carrier_iq_alpha;
|
let alpha = self.carrier_iq_alpha;
|
||||||
for s in decimated.iter() {
|
for s in decimated.iter() {
|
||||||
self.carrier_iq_i += alpha * (s.re - self.carrier_iq_i);
|
let pwr = s.re * s.re + s.im * s.im;
|
||||||
self.carrier_iq_q += alpha * (s.im - self.carrier_iq_q);
|
self.carrier_iq_i += alpha * (pwr - self.carrier_iq_i);
|
||||||
}
|
}
|
||||||
let carrier_pwr =
|
self.last_signal_db = 10.0 * self.carrier_iq_i.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;
|
||||||
@@ -780,8 +778,7 @@ impl ChannelDsp {
|
|||||||
.map(|s| s.re * s.re + s.im * s.im)
|
.map(|s| s.re * s.re + s.im * s.im)
|
||||||
.fold(0.0_f32, f32::max);
|
.fold(0.0_f32, f32::max);
|
||||||
let peak_db = 10.0 * peak_power.max(1e-12).log10();
|
let peak_db = 10.0 * peak_power.max(1e-12).log10();
|
||||||
self.last_signal_db +=
|
self.last_signal_db += SIGNAL_EMA_ALPHA * (peak_db - self.last_signal_db);
|
||||||
SIGNAL_EMA_ALPHA * (peak_db - self.last_signal_db);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user