feat(core): vad-driven listening window state machine

This commit is contained in:
Bossiara13 2026-04-23 11:07:35 +03:00
parent 3f19366ad0
commit 37c8371f3f
6 changed files with 149 additions and 0 deletions

View file

@ -1,5 +1,8 @@
mod none;
mod energy;
pub mod listen_window;
#[cfg(feature = "webrtc-vad")]
pub mod webrtc;
use once_cell::sync::OnceCell;
use parking_lot::Mutex;

View file

@ -0,0 +1,79 @@
use crate::config;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WindowDecision {
KeepListening,
Close,
HardCap,
}
#[derive(Debug, Clone)]
pub struct ListenWindow {
frame_ms: u32,
elapsed_ms: u32,
speech_ms: u32,
silence_ms: u32,
end_silence_ms: u32,
min_speech_ms: u32,
min_listen_ms: u32,
max_listen_ms: u32,
}
impl ListenWindow {
pub fn new(frame_ms: u32) -> Self {
Self::with_params(
frame_ms,
config::VAD_COMMAND_END_SILENCE_MS,
config::VAD_COMMAND_MIN_SPEECH_MS,
config::VAD_COMMAND_MIN_LISTEN_MS,
config::VAD_COMMAND_MAX_LISTEN_MS,
)
}
pub fn with_params(
frame_ms: u32,
end_silence_ms: u32,
min_speech_ms: u32,
min_listen_ms: u32,
max_listen_ms: u32,
) -> Self {
Self {
frame_ms,
elapsed_ms: 0,
speech_ms: 0,
silence_ms: 0,
end_silence_ms,
min_speech_ms,
min_listen_ms,
max_listen_ms,
}
}
pub fn push(&mut self, is_speech: bool) -> WindowDecision {
self.elapsed_ms = self.elapsed_ms.saturating_add(self.frame_ms);
if is_speech {
self.speech_ms = self.speech_ms.saturating_add(self.frame_ms);
self.silence_ms = 0;
} else {
self.silence_ms = self.silence_ms.saturating_add(self.frame_ms);
}
if self.elapsed_ms >= self.max_listen_ms {
return WindowDecision::HardCap;
}
if self.elapsed_ms < self.min_listen_ms {
return WindowDecision::KeepListening;
}
if self.speech_ms >= self.min_speech_ms && self.silence_ms >= self.end_silence_ms {
return WindowDecision::Close;
}
WindowDecision::KeepListening
}
pub fn elapsed_ms(&self) -> u32 { self.elapsed_ms }
pub fn speech_ms(&self) -> u32 { self.speech_ms }
pub fn silence_ms(&self) -> u32 { self.silence_ms }
pub fn had_speech(&self) -> bool { self.speech_ms > 0 }
}

View file

@ -0,0 +1,49 @@
use webrtc_vad::{SampleRate, Vad, VadMode};
use crate::config;
pub const FRAME_SAMPLES: usize = 480;
pub const FRAME_MS: u32 = 30;
pub struct WebRtcVad {
inner: Vad,
buf: Vec<i16>,
}
impl WebRtcVad {
pub fn new() -> Self {
Self::with_aggressiveness(config::VAD_AGGRESSIVENESS)
}
pub fn with_aggressiveness(level: u8) -> Self {
let mode = match level {
0 => VadMode::Quality,
1 => VadMode::LowBitrate,
2 => VadMode::Aggressive,
_ => VadMode::VeryAggressive,
};
Self {
inner: Vad::new_with_rate_and_mode(SampleRate::Rate16kHz, mode),
buf: Vec::with_capacity(FRAME_SAMPLES * 2),
}
}
pub fn push_samples(&mut self, samples: &[i16]) -> Vec<bool> {
self.buf.extend_from_slice(samples);
let mut out = Vec::new();
while self.buf.len() >= FRAME_SAMPLES {
let frame: Vec<i16> = self.buf.drain(..FRAME_SAMPLES).collect();
let is_speech = self.inner.is_voice_segment(&frame).unwrap_or(false);
out.push(is_speech);
}
out
}
pub fn reset(&mut self) {
self.buf.clear();
}
}
impl Default for WebRtcVad {
fn default() -> Self { Self::new() }
}