[fix](trx-vdes): fix VDES warm-up period

Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-08 08:44:29 +01:00
parent b4fb42dba4
commit f8d7472510
+32 -1
View File
@@ -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<Complex<f32>>,
/// 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<f32>], channel: &str) -> Vec<VdesMessage> {
@@ -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 {