[fix](trx-backend-soapysdr): squelch against the meter, not the post-AGC level
The threshold was compared against the block level measured after the IQ AGC. Holding that level at a setpoint is the AGC's entire purpose, so for every mode that has one — FM, PKT, AIS, AM, SAM, which is to say the modes anyone squelches — the comparison was against a near-constant. With FM's 12 dB of gain a weak signal reads some 12 dB hotter than it is, and the value never had the decimation correction the meter applies on top of that: around 20 dB adrift at 48k/8k, more as decimation grows. The threshold arrives in the other scale entirely. The slider maps its percentage onto -120..-30 dB and Auto takes the spectrum noise floor plus 6, both of which are what the meter and the spectrum display show. So a gate set just above the noise sat open on it. It now reads last_signal_db, which is already computed each block before the AGC and corrected for decimation — the same number the meter shows. The post-AGC measurement had no other consumer. The test feeds one signal twice and takes the threshold from the channel's own meter: 6 dB above must gate it, 6 dB below must pass it. Nothing there depends on the absolute scale, only on the two agreeing. Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -837,12 +837,6 @@ impl ChannelDsp {
|
||||
}
|
||||
}
|
||||
|
||||
let signal_power = decimated
|
||||
.iter()
|
||||
.map(|s| s.re * s.re + s.im * s.im)
|
||||
.sum::<f32>()
|
||||
/ decimated.len() as f32;
|
||||
let signal_db = 10.0 * signal_power.max(1e-12).log10();
|
||||
const WFM_OUTPUT_GAIN: f32 = 0.50;
|
||||
let mut audio = if let Some(decoder) = self.wfm_decoder.as_mut() {
|
||||
let mut out = decoder.process_iq(decimated);
|
||||
@@ -884,7 +878,14 @@ impl ChannelDsp {
|
||||
raw
|
||||
}
|
||||
};
|
||||
if !self.squelch.update(&self.mode, signal_db) {
|
||||
// Against the meter reading, not the block level after the IQ AGC.
|
||||
// The threshold arrives in the scale the operator sets it from — the
|
||||
// S-meter and the spectrum — while the level measured here had been
|
||||
// through the AGC, whose whole job is to hold it at a setpoint. For
|
||||
// every mode that has one (FM, PKT, AIS, AM, SAM) the squelch was
|
||||
// therefore comparing against a near-constant, and for the rest it was
|
||||
// still off by the decimation correction the meter applies.
|
||||
if !self.squelch.update(&self.mode, self.last_signal_db) {
|
||||
audio.fill(0.0);
|
||||
}
|
||||
|
||||
@@ -933,6 +934,93 @@ mod tests {
|
||||
dsp.process_block(&block);
|
||||
}
|
||||
|
||||
/// Feeds one signal twice, with the squelch threshold set from the channel's
|
||||
/// own meter reading: 6 dB above it must gate the audio, 6 dB below it must
|
||||
/// pass. FM runs an IQ AGC, so a squelch measured after that stage compares
|
||||
/// against a level pinned near the AGC setpoint — some 20 dB adrift of the
|
||||
/// scale the operator reads the threshold off, and open on plain noise.
|
||||
#[test]
|
||||
fn squelch_follows_the_meter_the_threshold_is_set_from() {
|
||||
const AMPLITUDE: f32 = 0.0025;
|
||||
|
||||
let (pcm_tx, mut pcm_rx) = broadcast::channel::<Vec<f32>>(4096);
|
||||
let (iq_tx, _iq_rx) = broadcast::channel::<Vec<Complex<f32>>>(8);
|
||||
let mut dsp = ChannelDsp::new(
|
||||
0.0,
|
||||
&RigMode::FM,
|
||||
48_000,
|
||||
8_000,
|
||||
1,
|
||||
20,
|
||||
12_000,
|
||||
75,
|
||||
true,
|
||||
false,
|
||||
VirtualSquelchConfig::default(),
|
||||
NoiseBlankerConfig::default(),
|
||||
pcm_tx,
|
||||
iq_tx,
|
||||
);
|
||||
|
||||
// A 1 kHz tone on the carrier, so an open gate is audibly non-zero and
|
||||
// a closed one is unambiguously silent.
|
||||
let mut phase = 0.0_f32;
|
||||
let mut mod_phase = 0.0_f32;
|
||||
let mut feed = |dsp: &mut ChannelDsp, blocks: usize| {
|
||||
for _ in 0..blocks {
|
||||
let mut block = Vec::with_capacity(4096);
|
||||
for _ in 0..4096 {
|
||||
mod_phase += std::f32::consts::TAU * 1_000.0 / 48_000.0;
|
||||
phase += std::f32::consts::TAU * (3_000.0 * mod_phase.sin()) / 48_000.0;
|
||||
block.push(Complex::new(
|
||||
AMPLITUDE * phase.cos(),
|
||||
AMPLITUDE * phase.sin(),
|
||||
));
|
||||
}
|
||||
dsp.process_block(&block);
|
||||
}
|
||||
};
|
||||
let drain = |rx: &mut broadcast::Receiver<Vec<f32>>| {
|
||||
let mut audio = Vec::new();
|
||||
while let Ok(frame) = rx.try_recv() {
|
||||
audio.extend_from_slice(&frame);
|
||||
}
|
||||
audio
|
||||
};
|
||||
let peak = |audio: &[f32]| audio.iter().fold(0.0_f32, |acc, s| acc.max(s.abs()));
|
||||
|
||||
// Settle the meter on this signal, then read what the operator would.
|
||||
feed(&mut dsp, 24);
|
||||
let meter_db = dsp.signal_db();
|
||||
assert!(
|
||||
meter_db > -120.0,
|
||||
"the meter never moved off its floor ({meter_db} dB)"
|
||||
);
|
||||
|
||||
dsp.set_squelch(true, meter_db + 6.0);
|
||||
let _ = drain(&mut pcm_rx);
|
||||
feed(&mut dsp, 24);
|
||||
let gated = drain(&mut pcm_rx);
|
||||
assert!(!gated.is_empty(), "no audio frames were produced at all");
|
||||
// From the second half on: the first frame out still carries the audio
|
||||
// that was already buffered when the threshold changed.
|
||||
assert_eq!(
|
||||
peak(&gated[gated.len() / 2..]),
|
||||
0.0,
|
||||
"squelch set 6 dB above the meter ({meter_db} dB) still passed audio"
|
||||
);
|
||||
|
||||
dsp.set_squelch(true, meter_db - 6.0);
|
||||
let _ = drain(&mut pcm_rx);
|
||||
feed(&mut dsp, 24);
|
||||
let passed = drain(&mut pcm_rx);
|
||||
assert!(!passed.is_empty(), "no audio frames were produced at all");
|
||||
assert!(
|
||||
peak(&passed[passed.len() / 2..]) > 0.0,
|
||||
"squelch set 6 dB below the meter ({meter_db} dB) gated the audio"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_dsp_set_mode() {
|
||||
let (pcm_tx, _) = broadcast::channel::<Vec<f32>>(8);
|
||||
|
||||
Reference in New Issue
Block a user