From 878b50cad792a58f844abe578cd09a33589350a8 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Wed, 4 Mar 2026 22:58:02 +0100 Subject: [PATCH] [fix](trx-server): gate near-silent audio before decode Apply an RMS gate to near-silent decoder input frames and zero them before APRS/CW/FT8/WSPR processing to avoid false decodes from unsquelched/no-signal noise. Co-authored-by: Codex Signed-off-by: Stan Grams --- src/trx-server/src/audio.rs | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/src/trx-server/src/audio.rs b/src/trx-server/src/audio.rs index c6c8685..0870eb0 100644 --- a/src/trx-server/src/audio.rs +++ b/src/trx-server/src/audio.rs @@ -41,6 +41,7 @@ const VDES_HISTORY_RETENTION: Duration = Duration::from_secs(24 * 60 * 60); const FT8_HISTORY_RETENTION: Duration = Duration::from_secs(24 * 60 * 60); const WSPR_HISTORY_RETENTION: Duration = Duration::from_secs(24 * 60 * 60); const FT8_SAMPLE_RATE: u32 = 12_000; +const DECODE_AUDIO_GATE_RMS: f32 = 2.5e-4; const AUDIO_STREAM_ERROR_LOG_INTERVAL: Duration = Duration::from_secs(60); const AUDIO_STREAM_RECOVERY_DELAY: Duration = Duration::from_secs(1); @@ -824,7 +825,7 @@ pub async fn run_aprs_decoder( } // Downmix to mono if stereo - let mono = if channels > 1 { + let mut mono = if channels > 1 { let num_frames = frame.len() / channels as usize; let mut mono = Vec::with_capacity(num_frames); for i in 0..num_frames { @@ -834,6 +835,7 @@ pub async fn run_aprs_decoder( } else { frame }; + apply_decode_audio_gate(&mut mono); was_active = true; for pkt in decoder.process_samples(&mono) { @@ -1125,7 +1127,7 @@ pub async fn run_cw_decoder( } // Downmix to mono if stereo - let mono = if channels > 1 { + let mut mono = if channels > 1 { let num_frames = frame.len() / channels as usize; let mut mono = Vec::with_capacity(num_frames); for i in 0..num_frames { @@ -1135,6 +1137,7 @@ pub async fn run_cw_decoder( } else { frame }; + apply_decode_audio_gate(&mut mono); was_active = true; for evt in decoder.process_samples(&mono) { @@ -1200,6 +1203,27 @@ fn downmix_mono(frame: Vec, channels: u16) -> Vec { mono } +fn frame_rms(samples: &[f32]) -> f32 { + if samples.is_empty() { + return 0.0; + } + let mut sum_sq = 0.0f32; + for &sample in samples { + sum_sq += sample * sample; + } + (sum_sq / samples.len() as f32).sqrt() +} + +fn apply_decode_audio_gate(samples: &mut [f32]) -> bool { + if frame_rms(samples) >= DECODE_AUDIO_GATE_RMS { + return false; + } + for sample in samples { + *sample = 0.0; + } + true +} + fn resample_to_12k(samples: &[f32], sample_rate: u32) -> Option> { if sample_rate == FT8_SAMPLE_RATE { return Some(samples.to_vec()); @@ -1291,7 +1315,8 @@ pub async fn run_ft8_decoder( ft8_buf.clear(); } - let mono = downmix_mono(frame, channels); + let mut mono = downmix_mono(frame, channels); + apply_decode_audio_gate(&mut mono); let Some(resampled) = resample_to_12k(&mono, sample_rate) else { warn!("FT8 decoder: unsupported sample rate {}", sample_rate); break; @@ -1452,7 +1477,8 @@ pub async fn run_wspr_decoder( last_slot = slot; } - let mono = downmix_mono(frame, channels); + let mut mono = downmix_mono(frame, channels); + apply_decode_audio_gate(&mut mono); let Some(resampled) = resample_to_12k(&mono, sample_rate) else { warn!("WSPR decoder: unsupported sample rate {}", sample_rate); break;