use crate::audio::{SAMPLE_RATE, SpeechSegment};
const CHUNK_SAMPLES: usize = 512;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VadKind {
Energy,
SileroLite,
}
#[derive(Debug, Clone)]
pub struct VadConfig {
pub kind: VadKind,
pub threshold: f32,
pub min_speech_samples: usize,
pub pad_samples: usize,
pub min_silence_samples: usize,
}
impl Default for VadConfig {
fn default() -> Self {
Self {
kind: VadKind::SileroLite,
threshold: 0.5,
min_speech_samples: SAMPLE_RATE / 5,
pad_samples: SAMPLE_RATE / 10,
min_silence_samples: SAMPLE_RATE / 5,
}
}
}
pub fn segments_by_vad(cfg: &VadConfig, pcm: &[f32]) -> Vec<SpeechSegment> {
if pcm.is_empty() {
return Vec::new();
}
let probs = match cfg.kind {
VadKind::Energy => energy_probs(pcm),
VadKind::SileroLite => silero_lite_probs(pcm),
};
probs_to_segments(cfg, pcm.len(), &probs)
}
fn energy_probs(pcm: &[f32]) -> Vec<f32> {
let mut out = Vec::new();
for chunk in pcm.chunks(CHUNK_SAMPLES) {
let rms = (chunk.iter().map(|x| x * x).sum::<f32>() / chunk.len() as f32).sqrt();
out.push((rms * 10.0).min(1.0));
}
out
}
fn silero_lite_probs(pcm: &[f32]) -> Vec<f32> {
let raw = energy_probs(pcm);
let mut smooth = raw.clone();
let alpha = 0.15f32;
for i in 1..smooth.len() {
smooth[i] = alpha * raw[i] + (1.0 - alpha) * smooth[i - 1];
}
for i in (0..smooth.len().saturating_sub(1)).rev() {
smooth[i] = alpha * smooth[i] + (1.0 - alpha) * smooth[i + 1];
}
smooth
}
fn probs_to_segments(cfg: &VadConfig, n_samples: usize, probs: &[f32]) -> Vec<SpeechSegment> {
let mut segments = Vec::new();
let mut in_speech = false;
let mut start_chunk = 0usize;
for (ci, &p) in probs.iter().enumerate() {
let speech = p >= cfg.threshold;
if speech && !in_speech {
in_speech = true;
start_chunk = ci;
} else if !speech && in_speech {
push_segment(
cfg,
&mut segments,
start_chunk * CHUNK_SAMPLES,
ci * CHUNK_SAMPLES,
n_samples,
);
in_speech = false;
}
}
if in_speech {
push_segment(
cfg,
&mut segments,
start_chunk * CHUNK_SAMPLES,
n_samples,
n_samples,
);
}
merge_close(cfg, segments)
}
fn push_segment(
cfg: &VadConfig,
out: &mut Vec<SpeechSegment>,
start: usize,
end: usize,
n_samples: usize,
) {
let s = start.saturating_sub(cfg.pad_samples);
let e = (end + cfg.pad_samples).min(n_samples);
if e.saturating_sub(s) < cfg.min_speech_samples {
return;
}
out.push(SpeechSegment { start: s, end: e });
}
fn merge_close(cfg: &VadConfig, segs: Vec<SpeechSegment>) -> Vec<SpeechSegment> {
if segs.len() < 2 {
return segs;
}
let mut merged = vec![segs[0]];
for seg in segs.into_iter().skip(1) {
let last = merged.last_mut().unwrap();
if seg.start.saturating_sub(last.end) <= cfg.min_silence_samples {
last.end = seg.end;
} else {
merged.push(seg);
}
}
merged
}