use std::collections::VecDeque;
use crate::{
event::{FrameResult, SpeechSegment},
options::VadOptions,
};
const FRAME_SHIFT_SAMPLES: u32 = 160;
#[derive(Debug, Clone, Copy, PartialEq)]
enum VadState {
Silence,
PossibleSpeech,
Speech,
PossibleSilence,
}
#[derive(Debug)]
pub(crate) struct Postprocessor {
options: VadOptions,
smooth_window: VecDeque<f32>,
smooth_window_sum: f64,
state: VadState,
speech_cnt: u32,
silence_cnt: u32,
hit_max_speech: bool,
last_speech_start_frame: Option<u64>,
last_speech_end_frame: Option<u64>,
frame_cnt_1based: u64,
}
impl Postprocessor {
pub(crate) fn new(options: VadOptions) -> Self {
let smooth_window_size = options.smooth_window_size_frames().max(1) as usize;
Self {
options,
smooth_window: VecDeque::with_capacity(smooth_window_size),
smooth_window_sum: 0.0,
state: VadState::Silence,
speech_cnt: 0,
silence_cnt: 0,
hit_max_speech: false,
last_speech_start_frame: None,
last_speech_end_frame: None,
frame_cnt_1based: 0,
}
}
pub(crate) fn set_options(&mut self, options: VadOptions) {
let old_window = self.options.smooth_window_size_frames();
let new_window = options.smooth_window_size_frames();
self.options = options;
if new_window != old_window {
self.smooth_window.clear();
self.smooth_window_sum = 0.0;
}
}
pub(crate) const fn options_const(&self) -> &VadOptions {
&self.options
}
pub(crate) const fn frame_count(&self) -> u64 {
self.frame_cnt_1based
}
pub(crate) fn reset(&mut self) {
self.smooth_window.clear();
self.smooth_window_sum = 0.0;
self.state = VadState::Silence;
self.speech_cnt = 0;
self.silence_cnt = 0;
self.hit_max_speech = false;
self.last_speech_start_frame = None;
self.last_speech_end_frame = None;
self.frame_cnt_1based = 0;
}
pub(crate) fn is_active(&self) -> bool {
matches!(self.state, VadState::Speech | VadState::PossibleSilence)
}
fn smooth(&mut self, raw: f32) -> f32 {
let size = self.options.smooth_window_size_frames().max(1) as usize;
if size <= 1 {
return raw;
}
self.smooth_window.push_back(raw);
self.smooth_window_sum += raw as f64;
while self.smooth_window.len() > size {
let dropped = self.smooth_window.pop_front().unwrap_or(0.0);
self.smooth_window_sum -= dropped as f64;
}
(self.smooth_window_sum / self.smooth_window.len() as f64) as f32
}
fn padded_speech_start(&self) -> u64 {
let pad = self.options.pad_start_frames() as u64;
let raw = self
.frame_cnt_1based
.saturating_sub(self.speech_cnt as u64)
.saturating_add(1)
.saturating_sub(pad);
let lower = self.last_speech_end_frame.map(|e| e + 1).unwrap_or(0);
let one_based = raw.max(1).max(lower + 1);
one_based.saturating_sub(1)
}
}
impl Postprocessor {
pub(crate) fn push_probability(&mut self, raw_prob: f32) -> (FrameResult, Option<SpeechSegment>) {
self.frame_cnt_1based += 1;
let smoothed = self.smooth(raw_prob);
let is_speech = smoothed >= self.options.speech_threshold();
let mut is_speech_start = false;
let mut is_speech_end = false;
let mut start_frame: Option<u64> = None;
let mut end_frame: Option<u64> = None;
if self.hit_max_speech {
is_speech_start = true;
let new_start = self.frame_cnt_1based.saturating_sub(1); self.last_speech_start_frame = Some(new_start);
start_frame = Some(new_start);
self.hit_max_speech = false;
}
let max_speech = self.options.max_speech_frames();
match self.state {
VadState::Silence => {
if is_speech {
self.state = VadState::PossibleSpeech;
self.speech_cnt += 1;
} else {
self.silence_cnt += 1;
self.speech_cnt = 0;
}
}
VadState::PossibleSpeech => {
if is_speech {
self.speech_cnt += 1;
if self.speech_cnt >= self.options.min_speech_frames() {
self.state = VadState::Speech;
is_speech_start = true;
let new_start = self.padded_speech_start();
self.last_speech_start_frame = Some(new_start);
start_frame = Some(new_start);
self.silence_cnt = 0;
}
} else {
self.state = VadState::Silence;
self.silence_cnt = 1;
self.speech_cnt = 0;
}
}
VadState::Speech => {
self.speech_cnt += 1;
if is_speech {
self.silence_cnt = 0;
if let Some(max) = max_speech
&& self.speech_cnt >= max
{
self.hit_max_speech = true;
self.speech_cnt = 0;
is_speech_end = true;
let close = self.frame_cnt_1based.saturating_sub(1);
end_frame = Some(close);
start_frame = self.last_speech_start_frame;
self.last_speech_end_frame = Some(close);
self.last_speech_start_frame = None;
}
} else {
self.state = VadState::PossibleSilence;
self.silence_cnt += 1;
}
}
VadState::PossibleSilence => {
self.speech_cnt += 1;
if is_speech {
self.state = VadState::Speech;
self.silence_cnt = 0;
if let Some(max) = max_speech
&& self.speech_cnt >= max
{
self.hit_max_speech = true;
self.speech_cnt = 0;
is_speech_end = true;
let close = self.frame_cnt_1based.saturating_sub(1);
end_frame = Some(close);
start_frame = self.last_speech_start_frame;
self.last_speech_end_frame = Some(close);
self.last_speech_start_frame = None;
}
} else {
self.silence_cnt += 1;
if self.silence_cnt >= self.options.min_silence_frames() {
self.state = VadState::Silence;
is_speech_end = true;
let close = self.frame_cnt_1based.saturating_sub(1);
end_frame = Some(close);
start_frame = self.last_speech_start_frame;
self.last_speech_end_frame = Some(close);
self.last_speech_start_frame = None;
self.speech_cnt = 0;
}
}
}
}
let frame_index_0based = self.frame_cnt_1based.saturating_sub(1);
let frame_result = FrameResult::new(
frame_index_0based,
raw_prob,
smoothed,
is_speech,
is_speech_start,
is_speech_end,
start_frame,
end_frame,
);
let segment = if is_speech_end {
match (start_frame, end_frame) {
(Some(start), Some(end)) => Some(SpeechSegment::new(
start * (FRAME_SHIFT_SAMPLES as u64),
end * (FRAME_SHIFT_SAMPLES as u64),
)),
_ => None,
}
} else {
None
};
(frame_result, segment)
}
pub(crate) fn finish_active(&mut self) -> Option<SpeechSegment> {
if !self.is_active() {
return None;
}
let start = self.last_speech_start_frame.take()?;
let end = self.frame_cnt_1based; self.state = VadState::Silence;
self.last_speech_end_frame = Some(end.saturating_sub(1));
self.speech_cnt = 0;
self.silence_cnt = 0;
Some(SpeechSegment::new(
start * (FRAME_SHIFT_SAMPLES as u64),
end * (FRAME_SHIFT_SAMPLES as u64),
))
}
}
#[cfg(test)]
mod tests {
use core::time::Duration;
use super::*;
fn opts() -> VadOptions {
VadOptions::new()
.with_smooth_window_size(1)
.with_speech_threshold(0.5)
.with_min_speech_duration(Duration::from_millis(30)) .with_min_silence_duration(Duration::from_millis(30)) .with_pad_start(Duration::from_millis(10)) .clear_max_speech_duration()
}
fn drive(post: &mut Postprocessor, probs: &[f32]) -> Vec<SpeechSegment> {
let mut out = Vec::new();
for &p in probs {
let (_, seg) = post.push_probability(p);
if let Some(s) = seg {
out.push(s);
}
}
out
}
#[test]
fn silence_alone_yields_no_segments() {
let mut p = Postprocessor::new(opts());
let segs = drive(&mut p, &[0.0; 50]);
assert!(segs.is_empty());
assert!(!p.is_active());
}
#[test]
fn min_speech_must_be_reached_before_speech_state() {
let mut p = Postprocessor::new(opts());
drive(&mut p, &[0.9, 0.9, 0.0, 0.0, 0.0, 0.0]);
assert!(!p.is_active());
}
#[test]
fn three_speech_frames_then_silence_closes_one_segment() {
let mut p = Postprocessor::new(opts());
let mut probs = vec![0.9; 3]; probs.extend(vec![0.9; 5]); probs.extend(vec![0.0; 4]); let segs = drive(&mut p, &probs);
assert_eq!(segs.len(), 1);
assert!(segs[0].sample_count() > 0);
}
#[test]
fn finish_flushes_open_segment() {
let mut p = Postprocessor::new(opts());
let probs = vec![0.9; 10]; drive(&mut p, &probs);
assert!(p.is_active());
let segment = p.finish_active().expect("trailing segment");
assert!(segment.sample_count() > 0);
assert!(!p.is_active());
}
#[test]
fn max_speech_force_split_produces_two_segments_back_to_back() {
let mut p = Postprocessor::new(
opts().with_max_speech_duration(Duration::from_millis(50)), );
let mut probs = vec![0.9; 12];
probs.extend(vec![0.0; 5]);
let segs = drive(&mut p, &probs);
assert!(
segs.len() >= 2,
"expected at least 2 segments; got {}",
segs.len()
);
}
#[test]
fn smoothing_window_dampens_isolated_high_probs() {
let opts = opts().with_smooth_window_size(5).with_speech_threshold(0.5);
let mut p = Postprocessor::new(opts);
drive(&mut p, &[0.0, 0.0, 0.0, 0.0, 0.9]);
assert!(!p.is_active());
}
}