use std::time::Duration;
mod energy;
mod segmenter;
pub use energy::EnergyVad;
pub use segmenter::{Segmenter, SegmenterConfig};
#[cfg(feature = "vad")]
mod earshot;
#[cfg(feature = "vad")]
pub use earshot::EarshotVad;
pub trait VadBackend: Send + Sync {
fn frame_size(&self) -> usize;
fn required_sample_rate(&self) -> Option<u32>;
fn start(&self) -> Box<dyn VadStream>;
fn default_threshold(&self) -> f32 {
0.5
}
}
impl VadBackend for Box<dyn VadBackend> {
fn frame_size(&self) -> usize {
(**self).frame_size()
}
fn required_sample_rate(&self) -> Option<u32> {
(**self).required_sample_rate()
}
fn start(&self) -> Box<dyn VadStream> {
(**self).start()
}
fn default_threshold(&self) -> f32 {
(**self).default_threshold()
}
}
pub trait VadStream: Send {
fn speech_probability(&mut self, frame: &[f32]) -> f32;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct SpeechSegment {
pub start: usize,
pub end: usize,
}
impl SpeechSegment {
pub fn len(&self) -> usize {
self.end.saturating_sub(self.start)
}
pub fn is_empty(&self) -> bool {
self.end <= self.start
}
pub fn duration(&self, sample_rate: u32) -> Duration {
if sample_rate == 0 {
return Duration::ZERO;
}
Duration::from_secs_f64(self.len() as f64 / sample_rate as f64)
}
pub fn offset(&self, sample_rate: u32) -> Duration {
if sample_rate == 0 {
return Duration::ZERO;
}
Duration::from_secs_f64(self.start as f64 / sample_rate as f64)
}
}
#[derive(Debug, Clone, thiserror::Error)]
#[non_exhaustive]
pub enum VadError {
#[error("backend requires {required} Hz audio, got {actual} Hz")]
SampleRate { required: u32, actual: u32 },
#[error("failed to load VAD backend: {0}")]
BackendLoad(String),
}
pub fn segment_buffer(
backend: &dyn VadBackend,
samples: &[f32],
sample_rate: u32,
config: &SegmenterConfig,
) -> Result<Vec<SpeechSegment>, VadError> {
let mut segmenter = Segmenter::new(backend, sample_rate, config.clone())?;
let frame_size = backend.frame_size();
let mut stream = backend.start();
let mut out = Vec::new();
for frame in samples.chunks(frame_size) {
let probability = if frame.len() == frame_size {
stream.speech_probability(frame)
} else {
let mut padded = frame.to_vec();
padded.resize(frame_size, 0.0);
stream.speech_probability(&padded)
};
if let Some(segment) = segmenter.push(probability) {
out.push(segment);
}
}
if let Some(segment) = segmenter.flush() {
out.push(segment);
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_boxed_backend_forwards_every_method() {
struct Odd;
impl VadBackend for Odd {
fn frame_size(&self) -> usize {
333
}
fn required_sample_rate(&self) -> Option<u32> {
Some(8_000)
}
fn default_threshold(&self) -> f32 {
0.17
}
fn start(&self) -> Box<dyn VadStream> {
unreachable!("not needed to compare the declarations")
}
}
let boxed: Box<dyn VadBackend> = Box::new(Odd);
assert_eq!(boxed.frame_size(), 333);
assert_eq!(boxed.required_sample_rate(), Some(8_000));
assert_eq!(
boxed.default_threshold(),
0.17,
"the boxed forwarder dropped default_threshold and fell back \
to the trait's 0.5"
);
}
fn synth(sample_rate: u32, spans: &[(f32, bool)]) -> Vec<f32> {
let mut out = Vec::new();
for (seconds, voiced) in spans {
let n = (seconds * sample_rate as f32) as usize;
for i in 0..n {
out.push(if *voiced {
(i as f32 * 0.3).sin() * 0.4
} else {
0.0
});
}
}
out
}
#[test]
fn silence_only_yields_no_segments() {
let samples = synth(16_000, &[(2.0, false)]);
let segments = segment_buffer(
&EnergyVad::new(),
&samples,
16_000,
&SegmenterConfig::default(),
)
.unwrap();
assert!(
segments.is_empty(),
"silence must not produce an utterance, got {segments:?}"
);
}
#[test]
fn one_utterance_between_silences() {
let samples = synth(16_000, &[(1.0, false), (1.5, true), (1.0, false)]);
let segments = segment_buffer(
&EnergyVad::new(),
&samples,
16_000,
&SegmenterConfig::default(),
)
.unwrap();
assert_eq!(segments.len(), 1, "got {segments:?}");
let seg = segments[0];
assert!(
seg.start <= 16_000 && seg.end >= 16_000 + 24_000,
"segment {seg:?} does not cover the speech at samples 16000..40000"
);
}
#[test]
fn short_pause_does_not_split_an_utterance() {
let samples = synth(
16_000,
&[
(1.0, false),
(1.0, true),
(0.2, false), (1.0, true),
(1.0, false),
],
);
let segments = segment_buffer(
&EnergyVad::new(),
&samples,
16_000,
&SegmenterConfig::default(),
)
.unwrap();
assert_eq!(
segments.len(),
1,
"a 200ms pause is shorter than the default min_silence and must \
not cut the utterance, got {segments:?}"
);
}
#[test]
fn long_pause_splits_into_two_utterances() {
let samples = synth(
16_000,
&[
(0.5, false),
(1.0, true),
(1.5, false), (1.0, true),
(0.5, false),
],
);
let segments = segment_buffer(
&EnergyVad::new(),
&samples,
16_000,
&SegmenterConfig::default(),
)
.unwrap();
assert_eq!(segments.len(), 2, "got {segments:?}");
assert!(
segments[0].end <= segments[1].start,
"segments must not overlap: {segments:?}"
);
}
#[test]
fn speech_running_to_the_end_is_still_emitted() {
let samples = synth(16_000, &[(0.5, false), (1.5, true)]);
let segments = segment_buffer(
&EnergyVad::new(),
&samples,
16_000,
&SegmenterConfig::default(),
)
.unwrap();
assert_eq!(
segments.len(),
1,
"a segment still open at end of audio must be flushed, got {segments:?}"
);
assert!(segments[0].end >= samples.len() - 16_000 / 100);
}
#[test]
fn segment_duration_and_offset() {
let seg = SpeechSegment {
start: 16_000,
end: 32_000,
};
assert_eq!(seg.duration(16_000), Duration::from_secs(1));
assert_eq!(seg.offset(16_000), Duration::from_secs(1));
assert_eq!(seg.duration(0), Duration::ZERO);
}
}