From f8d7472510be0547677cca6f4ccf9b123e5b8076 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sun, 8 Mar 2026 08:44:29 +0100 Subject: [PATCH] [fix](trx-vdes): fix VDES warm-up period Signed-off-by: Stan Grams --- src/decoders/trx-vdes/src/lib.rs | 33 +++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/decoders/trx-vdes/src/lib.rs b/src/decoders/trx-vdes/src/lib.rs index e4ffb0c..18a7412 100644 --- a/src/decoders/trx-vdes/src/lib.rs +++ b/src/decoders/trx-vdes/src/lib.rs @@ -40,6 +40,14 @@ const BURST_TRIGGER_FLOOR: f32 = 1.0e-10; const BURST_SUSTAIN_NOISE_MULT: f32 = 1.15; const BURST_SUSTAIN_FLOOR: f32 = 1.0e-11; +/// Warmup period: number of samples to observe before burst detection starts. +/// This allows the noise-floor EMA (α = 0.005, τ ≈ 200 samples) to converge +/// to the actual SDR noise level. Without warmup the initial floor of 1e-12 +/// is far below any real ADC noise, so the very first sample always triggers a +/// burst whose sustain threshold is also near 1e-12 — meaning quiet_run never +/// reaches quiet_limit and the burst never terminates. +const NOISE_FLOOR_WARMUP_SECS: f32 = 0.05; // 50 ms ≈ 10 EMA time-constants + #[derive(Debug, Clone)] pub struct VdesDecoder { sample_rate: f32, @@ -47,16 +55,20 @@ pub struct VdesDecoder { in_burst: bool, quiet_run: u32, burst_samples: Vec>, + /// Remaining warmup samples. Burst detection is suppressed while > 0. + warmup_remaining: usize, } impl VdesDecoder { pub fn new(sample_rate: u32) -> Self { + let sr = sample_rate.max(1) as f32; Self { - sample_rate: sample_rate.max(1) as f32, + sample_rate: sr, noise_floor: 1.0e-12, in_burst: false, quiet_run: 0, burst_samples: Vec::new(), + warmup_remaining: (sr * NOISE_FLOOR_WARMUP_SECS) as usize, } } @@ -65,6 +77,7 @@ impl VdesDecoder { self.in_burst = false; self.quiet_run = 0; self.burst_samples.clear(); + self.warmup_remaining = (self.sample_rate * NOISE_FLOOR_WARMUP_SECS) as usize; } pub fn process_samples(&mut self, samples: &[Complex], channel: &str) -> Vec { @@ -73,6 +86,24 @@ impl VdesDecoder { ((self.sample_rate * (MIN_BURST_MS / 1000.0)).round() as usize).max(16); let quiet_limit = ((self.sample_rate * (BURST_END_MS / 1000.0)).round() as u32).max(4); + // Warmup phase: feed samples into the noise-floor EMA only. + if self.warmup_remaining > 0 { + let n = self.warmup_remaining.min(samples.len()); + for &sample in &samples[..n] { + let power = sample.norm_sqr(); + self.noise_floor = 0.995 * self.noise_floor + 0.005 * power; + } + self.warmup_remaining -= n; + if n == samples.len() { + return out; + } + // Process any remaining samples after warmup completes. + return out + .into_iter() + .chain(self.process_samples(&samples[n..], channel)) + .collect(); + } + for &sample in samples { let power = sample.norm_sqr(); if !self.in_burst {