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 fd23fedc..af41daa2 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 @@ -837,12 +837,6 @@ impl ChannelDsp { } } - let signal_power = decimated - .iter() - .map(|s| s.re * s.re + s.im * s.im) - .sum::() - / 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::>(4096); + let (iq_tx, _iq_rx) = broadcast::channel::>>(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>| { + 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::>(8);