use earshot::{DefaultPredictor, Detector};
use super::{VadBackend, VadStream};
const FRAME_SIZE: usize = 256;
const SAMPLE_RATE: u32 = 16_000;
#[derive(Debug, Clone, Copy, Default)]
pub struct EarshotVad;
impl EarshotVad {
pub fn new() -> Self {
Self
}
}
impl VadBackend for EarshotVad {
fn frame_size(&self) -> usize {
FRAME_SIZE
}
fn required_sample_rate(&self) -> Option<u32> {
Some(SAMPLE_RATE)
}
fn default_threshold(&self) -> f32 {
0.2
}
fn start(&self) -> Box<dyn VadStream> {
Box::new(EarshotStream {
detector: Detector::default(),
})
}
}
struct EarshotStream {
detector: Detector<DefaultPredictor>,
}
impl VadStream for EarshotStream {
fn speech_probability(&mut self, frame: &[f32]) -> f32 {
self.detector.predict_f32(frame)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vad::{segment_buffer, SegmenterConfig, VadError};
fn voiced(n: usize) -> Vec<f32> {
let mut state = 0x2545_F491_4F6C_DD1Du64;
(0..n)
.map(|i| {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
let noise = (state >> 40) as f32 / 8388608.0 - 1.0;
let envelope = 0.5 + 0.5 * (i as f32 * 0.0008).sin();
noise * 0.3 * envelope
})
.collect()
}
#[test]
fn digital_silence_yields_no_segments() {
let segments = segment_buffer(
&EarshotVad::new(),
&vec![0.0f32; 32_000],
16_000,
&SegmenterConfig::default(),
)
.unwrap();
assert!(
segments.is_empty(),
"silence must not produce an utterance, got {segments:?}"
);
}
#[test]
fn a_modulated_signal_scores_above_silence() {
let backend = EarshotVad::new();
let signal = voiced(FRAME_SIZE * 100);
let mut stream = backend.start();
let voiced_max = signal
.chunks(FRAME_SIZE)
.map(|f| stream.speech_probability(f))
.fold(0.0f32, f32::max);
let mut stream = backend.start();
let silent_max = vec![0.0f32; FRAME_SIZE * 100]
.chunks(FRAME_SIZE)
.map(|f| stream.speech_probability(f))
.fold(0.0f32, f32::max);
assert!(
voiced_max > silent_max,
"a modulated signal ({voiced_max}) should outscore silence ({silent_max})"
);
}
#[test]
fn a_detector_does_not_carry_state_between_passes() {
let backend = EarshotVad::new();
let signal = voiced(FRAME_SIZE * 40);
let score = |backend: &EarshotVad| {
let mut stream = backend.start();
signal
.chunks(FRAME_SIZE)
.map(|f| stream.speech_probability(f))
.sum::<f32>()
};
assert_eq!(
score(&backend),
score(&backend),
"two passes over the same audio must score identically"
);
}
#[test]
fn a_non_16k_recording_is_refused_rather_than_resampled() {
let err = segment_buffer(
&EarshotVad::new(),
&vec![0.0f32; 4_800],
48_000,
&SegmenterConfig::default(),
)
.unwrap_err();
assert!(
matches!(
err,
VadError::SampleRate {
required: 16_000,
actual: 48_000
}
),
"got {err:?}"
);
}
}