mod config;
mod models;
mod silence;
#[cfg(test)]
mod tests;
pub use config::{SilenceConfig, VadConfig};
pub use models::{SpeechSegment, VadEvent, VadState};
pub use silence::{SilenceDetector, SilenceSegment};
#[derive(Debug, Clone)]
pub struct VoiceActivityDetector {
config: VadConfig,
noise_floor: f32,
pub(crate) state: VadState,
pub(crate) speech_frames: usize,
pub(crate) silence_frames: usize,
current_sample: usize,
}
impl VoiceActivityDetector {
#[must_use]
pub fn new(config: VadConfig) -> Self {
Self {
config,
noise_floor: 0.001, state: VadState::Silence,
speech_frames: 0,
silence_frames: 0,
current_sample: 0,
}
}
#[must_use]
pub const fn config(&self) -> &VadConfig {
&self.config
}
#[must_use]
pub const fn state(&self) -> VadState {
self.state
}
pub fn reset(&mut self) {
self.noise_floor = 0.001;
self.state = VadState::Silence;
self.speech_frames = 0;
self.silence_frames = 0;
self.current_sample = 0;
}
#[must_use]
pub fn detect(&mut self, audio: &[f32]) -> Vec<SpeechSegment> {
self.reset();
let mut segments = Vec::new();
let mut current_segment: Option<(f32, f32)> = None; let mut frame_count = 0;
for frame in audio.chunks(self.config.frame_size) {
if frame.len() < self.config.frame_size / 2 {
break; }
let event = self.process_frame(frame);
let time = self.sample_to_time(self.current_sample);
match event {
VadEvent::SpeechStart => {
current_segment = Some((time, Self::frame_energy(frame)));
frame_count = 1;
}
VadEvent::SpeechEnd => {
if let Some((start, energy_sum)) = current_segment.take() {
segments.push(SpeechSegment {
start,
end: time,
energy: energy_sum / frame_count.max(1) as f32,
});
}
}
VadEvent::Continue => {
if let Some((_, ref mut energy_sum)) = current_segment {
*energy_sum += Self::frame_energy(frame);
frame_count += 1;
}
}
}
self.current_sample += frame.len();
}
if let Some((start, energy_sum)) = current_segment {
let time = self.sample_to_time(self.current_sample);
segments.push(SpeechSegment {
start,
end: time,
energy: energy_sum / frame_count.max(1) as f32,
});
}
segments
}
pub fn process_frame(&mut self, frame: &[f32]) -> VadEvent {
let energy = Self::frame_energy(frame);
let zcr = Self::zero_crossing_rate(frame);
if self.state == VadState::Silence {
self.noise_floor = self
.config
.smoothing
.mul_add(self.noise_floor, (1.0 - self.config.smoothing) * energy);
}
let is_speech = self.is_speech_frame(energy, zcr);
match self.state {
VadState::Silence | VadState::SpeechEnd => {
if is_speech {
self.speech_frames += 1;
self.silence_frames = 0;
if self.speech_frames >= self.config.min_speech_frames {
self.state = VadState::Speech;
VadEvent::SpeechStart
} else {
VadEvent::Continue
}
} else {
self.speech_frames = 0;
self.state = VadState::Silence;
VadEvent::Continue
}
}
VadState::Speech | VadState::SpeechStart => {
if is_speech {
self.silence_frames = 0;
self.speech_frames += 1;
VadEvent::Continue
} else {
self.silence_frames += 1;
self.speech_frames = 0;
if self.silence_frames >= self.config.min_silence_frames {
self.state = VadState::Silence;
VadEvent::SpeechEnd
} else {
VadEvent::Continue
}
}
}
}
}
pub(crate) fn is_speech_frame(&self, energy: f32, zcr: f32) -> bool {
let energy_above = energy > self.noise_floor * self.config.energy_threshold;
let zcr_in_range = zcr > 0.05 && zcr < self.config.zcr_threshold;
energy_above && zcr_in_range
}
pub(crate) fn frame_energy(frame: &[f32]) -> f32 {
let sum: f32 = frame.iter().map(|&x| x * x).sum();
(sum / frame.len() as f32).sqrt()
}
pub(crate) fn zero_crossing_rate(frame: &[f32]) -> f32 {
if frame.len() < 2 {
return 0.0;
}
let crossings: f32 = frame
.windows(2)
.filter(|w| (w[0] >= 0.0) != (w[1] >= 0.0))
.count() as f32;
crossings / (frame.len() - 1) as f32
}
pub(crate) fn sample_to_time(&self, sample: usize) -> f32 {
sample as f32 / self.config.sample_rate as f32
}
}
impl Default for VoiceActivityDetector {
fn default() -> Self {
Self::new(VadConfig::default())
}
}
#[derive(Debug, Clone)]
pub struct StreamingVad {
pub(crate) detector: VoiceActivityDetector,
pub(crate) buffer: Vec<f32>,
pub(crate) speech_buffer: Vec<f32>,
pub(crate) in_speech: bool,
}
impl StreamingVad {
#[must_use]
pub fn new(config: VadConfig) -> Self {
Self {
detector: VoiceActivityDetector::new(config),
buffer: Vec::new(),
speech_buffer: Vec::new(),
in_speech: false,
}
}
pub fn process(&mut self, audio: &[f32]) -> (Vec<f32>, bool) {
self.buffer.extend_from_slice(audio);
let frame_size = self.detector.config.frame_size;
let mut completed_speech: Option<Vec<f32>> = None;
while self.buffer.len() >= frame_size {
let frame: Vec<f32> = self.buffer.drain(..frame_size).collect();
let event = self.detector.process_frame(&frame);
match event {
VadEvent::SpeechStart => {
self.in_speech = true;
self.speech_buffer.clear();
self.speech_buffer.extend_from_slice(&frame);
}
VadEvent::Continue => {
if self.in_speech {
self.speech_buffer.extend_from_slice(&frame);
}
}
VadEvent::SpeechEnd => {
self.in_speech = false;
if !self.speech_buffer.is_empty() {
completed_speech = Some(std::mem::take(&mut self.speech_buffer));
}
}
}
}
(completed_speech.unwrap_or_default(), self.in_speech)
}
#[must_use]
pub fn flush(&mut self) -> Vec<f32> {
if !self.buffer.is_empty() && self.in_speech {
self.speech_buffer.extend_from_slice(&self.buffer);
}
self.buffer.clear();
self.in_speech = false;
std::mem::take(&mut self.speech_buffer)
}
pub fn reset(&mut self) {
self.detector.reset();
self.buffer.clear();
self.speech_buffer.clear();
self.in_speech = false;
}
#[must_use]
pub const fn is_in_speech(&self) -> bool {
self.in_speech
}
}
impl Default for StreamingVad {
fn default() -> Self {
Self::new(VadConfig::default())
}
}