use std::sync::Mutex;
use async_trait::async_trait;
use pipecrab_core::{AudioChunk, DataFrame, Decision, Direction, Processor, SystemFrame};
use pipecrab_runtime::{Outbound, Stage, StageError};
use crate::{VadError, VoiceActivityDetector};
pub struct VadStage<V: VoiceActivityDetector> {
detector: V,
config: VadConfig,
state: Mutex<VadState>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VadConfig {
pub start_windows: u32,
pub stop_windows: u32,
}
impl Default for VadConfig {
fn default() -> Self {
Self { start_windows: 1, stop_windows: 8 }
}
}
struct VadState {
in_speech: bool,
against: u32,
}
enum Edge {
Started,
Stopped,
}
impl VadState {
fn observe(&mut self, is_speech: bool, config: &VadConfig) -> Option<Edge> {
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 { Edge::Started } else { Edge::Stopped })
}
}
impl<V: VoiceActivityDetector> VadStage<V> {
pub fn new(detector: V) -> Self {
Self::with_config(detector, VadConfig::default())
}
pub fn with_config(detector: V, config: VadConfig) -> Self {
Self { detector, config, state: Mutex::new(VadState { in_speech: false, against: 0 }) }
}
}
pub struct Detect(AudioChunk);
impl<V: VoiceActivityDetector> Processor for VadStage<V> {
type Effect = Detect;
fn decide_data(&mut self, frame: &DataFrame) -> Decision<Detect> {
match frame {
DataFrame::Audio(chunk) => Decision::forward().emit(Detect(chunk.clone())),
_ => Decision::forward(),
}
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<V: VoiceActivityDetector> Stage for VadStage<V> {
async fn perform(&self, Detect(chunk): Detect, out: &Outbound) -> Result<(), StageError> {
let verdict = self.detector.detect(&chunk.samples, chunk.format).await?;
let edge = {
let mut state = self.state.lock().expect("VAD state mutex poisoned");
state.observe(verdict.is_speech, &self.config)
};
let frame = match edge {
Some(Edge::Started) => SystemFrame::SpeechStarted,
Some(Edge::Stopped) => SystemFrame::SpeechStopped,
None => return Ok(()),
};
let _ = out.send_system(Direction::Down, frame).await;
Ok(())
}
}
impl From<VadError> for StageError {
fn from(e: VadError) -> Self {
StageError::new(e.to_string())
}
}