use std::sync::Mutex;
use async_trait::async_trait;
use pipecrab_core::AudioFormat;
use crate::{SpeechScorer, VadError, VadEvent, VoiceActivityDetector};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DebounceConfig {
pub threshold: f32,
pub start_windows: u32,
pub stop_windows: u32,
}
impl Default for DebounceConfig {
fn default() -> Self {
Self { threshold: 0.5, start_windows: 1, stop_windows: 8 }
}
}
struct ObserveState {
in_speech: bool,
against: u32,
}
impl ObserveState {
fn observe(&mut self, is_speech: bool, config: &DebounceConfig) -> Option<VadEvent> {
if is_speech == self.in_speech {
self.against = 0;
return None;
}
self.against += 1;
let needed = if self.in_speech { config.stop_windows } else { config.start_windows };
if self.against < needed {
return None;
}
self.in_speech = is_speech;
self.against = 0;
Some(if is_speech { VadEvent::SpeechStarted } else { VadEvent::SpeechStopped })
}
fn reset(&mut self) {
self.in_speech = false;
self.against = 0;
}
}
struct DebouncedState {
remainder: Vec<f32>,
observe: ObserveState,
}
pub struct Debounced<S: SpeechScorer> {
scorer: S,
config: DebounceConfig,
state: Mutex<DebouncedState>,
}
impl<S: SpeechScorer> Debounced<S> {
pub fn new(scorer: S) -> Self {
Self::with_config(scorer, DebounceConfig::default())
}
pub fn with_config(scorer: S, config: DebounceConfig) -> Self {
Self {
scorer,
config,
state: Mutex::new(DebouncedState {
remainder: Vec::new(),
observe: ObserveState { in_speech: false, against: 0 },
}),
}
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<S: SpeechScorer> VoiceActivityDetector for Debounced<S> {
fn input_format(&self) -> AudioFormat {
self.scorer.input_format()
}
async fn process(&self, samples: &[f32]) -> Result<Vec<VadEvent>, VadError> {
let window_len = self.scorer.window_len();
let windows: Vec<Vec<f32>> = {
let mut st = self.state.lock().expect("Debounced state mutex poisoned");
st.remainder.extend_from_slice(samples);
let mut windows = Vec::new();
while st.remainder.len() >= window_len && window_len > 0 {
windows.push(st.remainder.drain(..window_len).collect());
}
windows
};
let mut events = Vec::new();
for window in windows {
let prob = self.scorer.score(&window).await?;
let is_speech = prob >= self.config.threshold;
let mut st = self.state.lock().expect("Debounced state mutex poisoned");
if let Some(event) = st.observe.observe(is_speech, &self.config) {
events.push(event);
}
}
Ok(events)
}
fn reset(&self) {
let mut st = self.state.lock().expect("Debounced state mutex poisoned");
st.remainder.clear();
st.observe.reset();
}
}