#[derive(Debug, Clone, Copy)]
pub struct VadResult {
pub speaking: bool,
pub silence_ms: u64,
pub energy: f32,
pub noise_floor: f32,
}
pub trait VadEngine: Send {
fn process(&mut self, samples: &[f32], rms: f32) -> VadResult;
fn name(&self) -> &'static str;
fn is_healthy(&self) -> bool {
true
}
fn reset(&mut self) {}
}
pub struct Vad {
noise_floor: f32,
multiplier: f32,
is_speaking: bool,
hangover_chunks: u32,
hangover_remaining: u32,
silence_ms: u64,
chunk_ms: u64,
adapt_rate: f32,
}
impl Vad {
pub fn new() -> Self {
Self {
noise_floor: 0.001,
multiplier: 4.0,
is_speaking: false,
hangover_chunks: 5, hangover_remaining: 0,
silence_ms: 0,
chunk_ms: 100,
adapt_rate: 0.02,
}
}
pub fn process(&mut self, rms: f32) -> VadResult {
let threshold = self.noise_floor * self.multiplier;
if rms > threshold {
self.is_speaking = true;
self.hangover_remaining = self.hangover_chunks;
self.silence_ms = 0;
} else if self.hangover_remaining > 0 {
self.hangover_remaining -= 1;
self.silence_ms = 0;
} else {
self.is_speaking = false;
self.silence_ms += self.chunk_ms;
if rms > self.noise_floor {
self.noise_floor += (rms - self.noise_floor) * self.adapt_rate;
} else {
self.noise_floor += (rms - self.noise_floor) * (self.adapt_rate * 3.0);
}
self.noise_floor = self.noise_floor.clamp(0.0001, 0.02);
}
VadResult {
speaking: self.is_speaking,
silence_ms: self.silence_ms,
energy: rms,
noise_floor: self.noise_floor,
}
}
pub fn reset(&mut self) {
self.noise_floor = 0.001;
self.is_speaking = false;
self.hangover_remaining = 0;
self.silence_ms = 0;
}
}
impl Default for Vad {
fn default() -> Self {
Self::new()
}
}
impl VadEngine for Vad {
fn process(&mut self, _samples: &[f32], rms: f32) -> VadResult {
Vad::process(self, rms)
}
fn name(&self) -> &'static str {
"energy"
}
fn reset(&mut self) {
Vad::reset(self);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn silence_stays_silent() {
let mut vad = Vad::new();
for _ in 0..20 {
let r = vad.process(0.0005);
assert!(!r.speaking);
}
assert!(vad.process(0.0005).silence_ms > 0);
}
#[test]
fn speech_detected() {
let mut vad = Vad::new();
for _ in 0..10 {
vad.process(0.0005);
}
let r = vad.process(0.05);
assert!(r.speaking);
assert_eq!(r.silence_ms, 0);
}
#[test]
fn hangover_prevents_flapping() {
let mut vad = Vad::new();
for _ in 0..10 {
vad.process(0.0005);
}
vad.process(0.05);
assert!(vad.is_speaking);
let r = vad.process(0.0005);
assert!(r.speaking);
for _ in 0..6 {
vad.process(0.0005);
}
assert!(!vad.process(0.0005).speaking);
}
struct MinimalEngine;
impl VadEngine for MinimalEngine {
fn process(&mut self, _samples: &[f32], rms: f32) -> VadResult {
VadResult {
speaking: false,
silence_ms: 0,
energy: rms,
noise_floor: 0.0,
}
}
fn name(&self) -> &'static str {
"minimal"
}
}
#[test]
fn trait_defaults_are_healthy_and_no_op_reset() {
let mut engine = MinimalEngine;
assert!(engine.is_healthy(), "default is_healthy must be true");
engine.reset(); assert!(
engine.is_healthy(),
"default reset must not change is_healthy"
);
}
#[test]
fn energy_engine_reset_via_trait_clears_state() {
let mut vad = Vad::new();
let initial = vad.noise_floor;
for _ in 0..200 {
vad.process(0.003);
}
assert!(vad.noise_floor > initial);
<Vad as VadEngine>::reset(&mut vad);
assert!(
(vad.noise_floor - initial).abs() < f32::EPSILON,
"trait reset must restore noise_floor to initial"
);
assert!(<Vad as VadEngine>::is_healthy(&vad));
}
#[test]
fn energy_engine_trait_matches_inherent_process() {
let rms_sequence = [0.0005_f32, 0.0005, 0.05, 0.05, 0.0005];
let mut inherent = Vad::new();
let mut via_trait = Vad::new();
for &rms in &rms_sequence {
let a = inherent.process(rms);
let b = <Vad as VadEngine>::process(&mut via_trait, &[], rms);
assert_eq!(a.speaking, b.speaking);
assert_eq!(a.silence_ms, b.silence_ms);
assert_eq!(a.energy, b.energy);
}
assert_eq!(<Vad as VadEngine>::name(&via_trait), "energy");
}
#[test]
fn noise_floor_adapts() {
let mut vad = Vad::new();
let initial = vad.noise_floor;
for _ in 0..100 {
vad.process(0.003);
}
assert!(vad.noise_floor > initial);
}
}