Add hysteresis to VAD.

This commit is contained in:
CJ Pais 2025-07-11 15:40:47 -07:00
parent c23befd98a
commit dc90edadb1
2 changed files with 33 additions and 14 deletions

View file

@ -6,9 +6,11 @@ pub struct SmoothedVad {
inner_vad: Box<dyn VoiceActivityDetector>,
prefill_frames: usize,
hangover_frames: usize,
onset_frames: usize,
frame_buffer: VecDeque<Vec<f32>>,
hangover_counter: usize,
onset_counter: usize,
in_speech: bool,
temp_out: Vec<f32>,
@ -19,13 +21,16 @@ impl SmoothedVad {
inner_vad: Box<dyn VoiceActivityDetector>,
prefill_frames: usize,
hangover_frames: usize,
onset_frames: usize,
) -> Self {
Self {
inner_vad,
prefill_frames,
hangover_frames,
onset_frames,
frame_buffer: VecDeque::new(),
hangover_counter: 0,
onset_counter: 0,
in_speech: false,
temp_out: Vec::new(),
}
@ -42,19 +47,28 @@ impl VoiceActivityDetector for SmoothedVad {
// 2. Delegate to the wrapped boolean VAD
let is_voice = self.inner_vad.is_voice(frame)?;
// println!("Is Voice: {}", is_voice);
match (self.in_speech, is_voice) {
// Start of Speech
// Potential start of speech - need to accumulate onset frames
(false, true) => {
self.in_speech = true;
self.hangover_counter = self.hangover_frames;
self.onset_counter += 1;
if self.onset_counter >= self.onset_frames {
// We have enough consecutive voice frames to trigger speech
self.in_speech = true;
self.hangover_counter = self.hangover_frames;
self.onset_counter = 0; // Reset for next time
// Collect prefill + current frame
self.temp_out.clear();
for buf in &self.frame_buffer {
self.temp_out.extend(buf);
// Collect prefill + current frame
self.temp_out.clear();
for buf in &self.frame_buffer {
self.temp_out.extend(buf);
}
Ok(VadFrame::Speech(&self.temp_out))
} else {
// Not enough frames yet, still silence
Ok(VadFrame::Noise)
}
Ok(VadFrame::Speech(&self.temp_out))
}
// Ongoing Speech
@ -63,7 +77,7 @@ impl VoiceActivityDetector for SmoothedVad {
Ok(VadFrame::Speech(frame))
}
// End of Speech
// End of Speech or interruption during onset phase
(true, false) => {
if self.hangover_counter > 0 {
self.hangover_counter -= 1;
@ -74,14 +88,18 @@ impl VoiceActivityDetector for SmoothedVad {
}
}
// Silence
(false, false) => Ok(VadFrame::Noise),
// Silence or broken onset sequence
(false, false) => {
self.onset_counter = 0; // Reset onset counter on silence
Ok(VadFrame::Noise)
}
}
}
fn reset(&mut self) {
self.frame_buffer.clear();
self.hangover_counter = 0;
self.onset_counter = 0;
self.in_speech = false;
self.temp_out.clear();
}

View file

@ -24,9 +24,9 @@ pub enum MicrophoneMode {
/* ──────────────────────────────────────────────────────────────── */
fn create_audio_recorder(vad_path: &str) -> Result<AudioRecorder, anyhow::Error> {
let silero = SileroVad::new(vad_path, 0.5)
let silero = SileroVad::new(vad_path, 0.3)
.map_err(|e| anyhow::anyhow!("Failed to create SileroVad: {}", e))?;
let smoothed_vad = SmoothedVad::new(Box::new(silero), 15, 15);
let smoothed_vad = SmoothedVad::new(Box::new(silero), 15, 15, 2);
let recorder = AudioRecorder::new()
.map_err(|e| anyhow::anyhow!("Failed to create AudioRecorder: {}", e))?
.with_vad(Box::new(smoothed_vad));
@ -246,7 +246,8 @@ impl AudioRecordingManager {
// Pad if very short
let s_len = samples.len();
if s_len < WHISPER_SAMPLE_RATE && s_len > 1000 {
// println!("Got {} samples", { s_len });
if s_len < WHISPER_SAMPLE_RATE && s_len > 0 {
let mut padded = samples;
padded.resize(WHISPER_SAMPLE_RATE * 5 / 4, 0.0);
Some(padded)