use std::collections::VecDeque;
use std::path::{Path, PathBuf};
use crate::error::DecibriError;
use crate::microphone::{AudioChunk, DenoiseModel, HighpassFilter};
use crate::sample;
use crate::stage::{build_capture_stage, CaptureStage, Transforms};
#[cfg(feature = "vad")]
use crate::vad::{SileroVad, VadConfig};
#[cfg(feature = "vad")]
use decibri_resampler::{PolyphaseResampler, Resampler};
const FEED_FRAMES: usize = 1600;
const DELIVERY_FRAMES: usize = 1600;
#[cfg(feature = "vad")]
const VAD_QUEUE_BOUND_SECS: usize = 2;
#[cfg(feature = "vad")]
const DEFAULT_VAD_HOLDOFF_MS: u32 = 300;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct FileConfig {
pub sample_rate: u32,
pub dc_removal: bool,
pub denoise: Option<DenoiseModel>,
pub denoise_model_path: Option<PathBuf>,
pub ort_library_path: Option<PathBuf>,
pub highpass: Option<HighpassFilter>,
pub agc: Option<i8>,
pub limiter: Option<f32>,
#[cfg(feature = "vad")]
pub vad: Option<VadConfig>,
#[cfg(feature = "vad")]
pub vad_holdoff_ms: u32,
}
impl Default for FileConfig {
fn default() -> Self {
Self {
sample_rate: 16000,
dc_removal: false,
denoise: None,
denoise_model_path: None,
ort_library_path: None,
highpass: None,
agc: None,
limiter: None,
#[cfg(feature = "vad")]
vad: None,
#[cfg(feature = "vad")]
vad_holdoff_ms: DEFAULT_VAD_HOLDOFF_MS,
}
}
}
impl FileConfig {
pub fn validate(&self) -> Result<(), DecibriError> {
if !(1000..=384000).contains(&self.sample_rate) {
return Err(DecibriError::SampleRateOutOfRange);
}
if let Some(target) = self.agc {
if !(-40..=-3).contains(&target) {
return Err(DecibriError::AgcTargetOutOfRange);
}
}
if let Some(ceiling) = self.limiter {
if !(-3.0..=0.0).contains(&ceiling) {
return Err(DecibriError::LimiterCeilingOutOfRange);
}
}
#[cfg(feature = "vad")]
if let Some(vad) = &self.vad {
let mut detector = vad.clone();
detector.sample_rate = 16000;
detector.validate()?;
}
Ok(())
}
}
#[cfg(feature = "vad")]
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct VadWindow {
pub start: f64,
pub end: f64,
pub probability: f32,
pub is_speech: bool,
}
#[cfg(feature = "vad")]
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Segment {
pub start: f64,
pub end: f64,
}
#[cfg(feature = "vad")]
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct VadReport {
pub scores: Vec<VadWindow>,
pub segments: Vec<Segment>,
}
pub struct File {
source: Vec<f32>,
input_rate: u32,
input_channels: u16,
pos: usize,
stage: Option<CaptureStage>,
reblock: VecDeque<f32>,
flushed: bool,
finished: bool,
target_rate: u32,
scratch: Vec<f32>,
#[cfg(feature = "vad")]
vad: Option<VadConfig>,
#[cfg(feature = "vad")]
vad_rate: u32,
#[cfg(feature = "vad")]
vad_resampler: Option<PolyphaseResampler>,
#[cfg(feature = "vad")]
vad_queue: VecDeque<f32>,
#[cfg(feature = "vad")]
vad_queue_cap: usize,
#[cfg(feature = "vad")]
vad_holdoff_ms: u32,
}
impl File {
pub fn open(path: impl AsRef<Path>, config: FileConfig) -> Result<Self, DecibriError> {
let path = path.as_ref();
let bytes = std::fs::read(path).map_err(|source| DecibriError::FileReadFailed {
path: path.to_path_buf(),
source,
})?;
let wav = parse_wav(&bytes)?;
Self::from_source(wav.samples, wav.sample_rate, wav.channels, config)
}
pub fn new(path: impl AsRef<Path>, config: FileConfig) -> Result<Self, DecibriError> {
Self::open(path, config)
}
pub fn buffer(
samples: Vec<f32>,
input_rate: u32,
config: FileConfig,
) -> Result<Self, DecibriError> {
if !(1000..=384000).contains(&input_rate) {
return Err(DecibriError::SampleRateOutOfRange);
}
Self::from_source(samples, input_rate, 1, config)
}
fn from_source(
source: Vec<f32>,
input_rate: u32,
input_channels: u16,
config: FileConfig,
) -> Result<Self, DecibriError> {
config.validate()?;
let target_rate = config.sample_rate;
let denoise = config
.denoise
.zip(config.denoise_model_path.as_deref())
.map(|(model, path)| (model, path, config.ort_library_path.as_deref()));
let stage = build_capture_stage(
input_channels,
1,
input_rate,
target_rate,
Transforms {
dc_removal: config.dc_removal,
denoise,
highpass: config.highpass,
agc: config.agc,
limiter: config.limiter,
},
)?;
#[cfg(feature = "vad")]
let (vad_rate, vad_resampler) = if config.vad.is_some() {
match target_rate {
8000 | 16000 => (target_rate, None),
_ => (16000, Some(PolyphaseResampler::new(target_rate, 16000)?)),
}
} else {
(16000, None)
};
Ok(Self {
source,
input_rate,
input_channels,
pos: 0,
stage,
reblock: VecDeque::new(),
flushed: false,
finished: false,
target_rate,
scratch: Vec::new(),
#[cfg(feature = "vad")]
vad: config.vad,
#[cfg(feature = "vad")]
vad_rate,
#[cfg(feature = "vad")]
vad_resampler,
#[cfg(feature = "vad")]
vad_queue: VecDeque::new(),
#[cfg(feature = "vad")]
vad_queue_cap: vad_rate.max(target_rate) as usize * VAD_QUEUE_BOUND_SECS,
#[cfg(feature = "vad")]
vad_holdoff_ms: config.vad_holdoff_ms,
})
}
pub fn sample_rate(&self) -> u32 {
self.target_rate
}
pub fn input_rate(&self) -> u32 {
self.input_rate
}
fn feed_active(&self) -> bool {
#[cfg(feature = "vad")]
{
self.vad.is_some() || self.stage.as_ref().is_some_and(CaptureStage::has_transform)
}
#[cfg(not(feature = "vad"))]
{
false
}
}
#[cfg(feature = "vad")]
pub fn vad_rate(&self) -> Option<u32> {
self.vad.as_ref().map(|_| self.vad_rate)
}
#[cfg(feature = "vad")]
pub fn vad_input(&mut self) -> Option<Vec<f32>> {
if !self.feed_active() {
return None;
}
Some(self.vad_queue.drain(..).collect())
}
#[cfg(feature = "vad")]
pub fn analyze(mut self) -> Result<VadReport, DecibriError> {
let Some(vad) = self.vad.clone() else {
return Err(DecibriError::VadNotConfigured);
};
let mut detector_config = vad;
detector_config.sample_rate = self.vad_rate;
let threshold = detector_config.threshold;
let window = detector_config.validate()?;
let mut detector = SileroVad::new(detector_config)?;
let mut probabilities: Vec<f32> = Vec::new();
let mut pending: Vec<f32> = Vec::new();
while !self.flushed {
self.advance()?;
self.reblock.clear();
pending.extend(self.vad_queue.drain(..));
let mut offset = 0;
while pending.len() - offset >= window {
let result = detector.process(&pending[offset..offset + window])?;
probabilities.push(result.probability);
offset += window;
}
pending.drain(..offset);
}
let window_secs = window as f64 / f64::from(self.vad_rate);
let scores: Vec<VadWindow> = probabilities
.iter()
.enumerate()
.map(|(i, &probability)| VadWindow {
start: i as f64 * window_secs,
end: (i + 1) as f64 * window_secs,
probability,
is_speech: probability >= threshold,
})
.collect();
let segments = merge_segments(&scores, f64::from(self.vad_holdoff_ms) / 1000.0);
Ok(VadReport { scores, segments })
}
#[cfg(feature = "vad")]
pub fn analyse(self) -> Result<VadReport, DecibriError> {
self.analyze()
}
fn advance(&mut self) -> Result<(), DecibriError> {
while self.reblock.len() < DELIVERY_FRAMES && !self.flushed {
if self.pos < self.source.len() {
let feed = FEED_FRAMES * self.input_channels as usize;
let end = (self.pos + feed).min(self.source.len());
let range = self.pos..end;
self.pos = end;
self.ingest(range)?;
} else {
self.flush_chain()?;
self.flushed = true;
}
}
Ok(())
}
fn ingest(&mut self, range: std::ops::Range<usize>) -> Result<(), DecibriError> {
let want_feed = self.feed_active();
self.scratch.clear();
match &mut self.stage {
None => {
self.reblock.extend(&self.source[range.clone()]);
if want_feed {
self.scratch.extend_from_slice(&self.source[range]);
}
}
Some(stage) => {
let has_transform = stage.has_transform();
let out = stage.run(&self.source[range])?;
self.reblock.extend(out.iter().copied());
if want_feed {
if has_transform {
self.scratch.extend_from_slice(stage.tap());
} else {
self.scratch.extend_from_slice(out);
}
}
}
}
#[cfg(feature = "vad")]
if want_feed {
self.push_vad_feed();
}
Ok(())
}
fn flush_chain(&mut self) -> Result<(), DecibriError> {
let want_feed = self.feed_active();
self.scratch.clear();
if let Some(stage) = &mut self.stage {
let mut tail = Vec::new();
stage.flush(&mut tail)?;
self.reblock.extend(&tail);
if want_feed {
if stage.has_transform() {
self.scratch.extend_from_slice(stage.tap());
} else {
self.scratch.extend_from_slice(&tail);
}
}
}
#[cfg(feature = "vad")]
if want_feed {
self.push_vad_feed();
if let Some(resampler) = &mut self.vad_resampler {
let mut tail = Vec::new();
resampler.flush(&mut tail);
self.vad_queue.extend(tail);
self.cap_vad_queue();
}
}
Ok(())
}
#[cfg(feature = "vad")]
fn push_vad_feed(&mut self) {
if self.scratch.is_empty() {
return;
}
match &mut self.vad_resampler {
Some(resampler) => {
let mut out = Vec::new();
resampler.process(&self.scratch, &mut out);
self.vad_queue.extend(out);
}
None => self.vad_queue.extend(self.scratch.iter().copied()),
}
self.cap_vad_queue();
}
#[cfg(feature = "vad")]
fn cap_vad_queue(&mut self) {
if self.vad_queue.len() > self.vad_queue_cap {
let excess = self.vad_queue.len() - self.vad_queue_cap;
self.vad_queue.drain(..excess);
}
}
}
impl Iterator for File {
type Item = Result<AudioChunk, DecibriError>;
fn next(&mut self) -> Option<Self::Item> {
if self.finished {
return None;
}
if let Err(e) = self.advance() {
self.finished = true;
return Some(Err(e));
}
let take = self.reblock.len().min(DELIVERY_FRAMES);
if take == 0 {
self.finished = true;
return None;
}
let data: Vec<f32> = self.reblock.drain(..take).collect();
if self.flushed && self.reblock.is_empty() {
self.finished = true;
}
Some(Ok(AudioChunk {
data,
sample_rate: self.target_rate,
channels: 1,
}))
}
}
#[cfg(feature = "vad")]
fn merge_segments(scores: &[VadWindow], holdoff_secs: f64) -> Vec<Segment> {
let mut segments = Vec::new();
let mut current: Option<(f64, f64)> = None;
for w in scores.iter().filter(|w| w.is_speech) {
current = Some(match current {
None => (w.start, w.end),
Some((start, end)) => {
if w.start - end <= holdoff_secs {
(start, w.end)
} else {
segments.push(Segment { start, end });
(w.start, w.end)
}
}
});
}
if let Some((start, end)) = current {
segments.push(Segment { start, end });
}
segments
}
struct WavSource {
samples: Vec<f32>,
sample_rate: u32,
channels: u16,
}
fn parse_wav(bytes: &[u8]) -> Result<WavSource, DecibriError> {
fn u16_at(bytes: &[u8], at: usize) -> u16 {
u16::from_le_bytes([bytes[at], bytes[at + 1]])
}
fn u32_at(bytes: &[u8], at: usize) -> u32 {
u32::from_le_bytes([bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]])
}
let invalid = |reason: &'static str| DecibriError::WavInvalid { reason };
if bytes.len() < 12 || &bytes[0..4] != b"RIFF" || &bytes[8..12] != b"WAVE" {
return Err(invalid("not a RIFF/WAVE file"));
}
let mut fmt: Option<(u16, u16, u32, u16)> = None; let mut data: Option<(usize, usize)> = None; let mut offset = 12usize;
while offset + 8 <= bytes.len() {
let id = &bytes[offset..offset + 4];
let size = u32_at(bytes, offset + 4) as usize;
let body = offset + 8;
if body + size > bytes.len() {
return Err(invalid("chunk extends past end of file"));
}
if id == b"fmt " {
if size < 16 {
return Err(invalid("fmt chunk too short"));
}
fmt = Some((
u16_at(bytes, body),
u16_at(bytes, body + 2),
u32_at(bytes, body + 4),
u16_at(bytes, body + 14),
));
} else if id == b"data" {
data = Some((body, size));
break;
}
offset = body + size + (size & 1);
}
let (format, channels, sample_rate, bits) = fmt.ok_or(invalid("missing fmt chunk"))?;
let (data_offset, data_len) = data.ok_or(invalid("missing data chunk"))?;
if channels == 0 {
return Err(invalid("channel count is zero"));
}
if !(1000..=384000).contains(&sample_rate) {
return Err(DecibriError::SampleRateOutOfRange);
}
let payload = &bytes[data_offset..data_offset + data_len];
let samples = match (format, bits) {
(1, 16) => sample::i16_le_bytes_to_f32(payload),
(3, 32) => sample::f32_le_bytes_to_f32(payload),
_ => {
return Err(invalid(
"unsupported encoding; 16-bit PCM or 32-bit float required",
))
}
};
Ok(WavSource {
samples,
sample_rate,
channels,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn wav_bytes(format: u16, channels: u16, rate: u32, samples: &[f32]) -> Vec<u8> {
let payload = match format {
1 => sample::f32_to_i16_le_bytes(samples),
3 => sample::f32_to_f32_le_bytes(samples),
_ => unreachable!("test formats are 1 and 3"),
};
let bits: u16 = if format == 1 { 16 } else { 32 };
let block_align = channels * bits / 8;
let byte_rate = rate * u32::from(block_align);
let mut bytes = Vec::new();
bytes.extend_from_slice(b"RIFF");
bytes.extend_from_slice(&(36 + payload.len() as u32).to_le_bytes());
bytes.extend_from_slice(b"WAVE");
bytes.extend_from_slice(b"fmt ");
bytes.extend_from_slice(&16u32.to_le_bytes());
bytes.extend_from_slice(&format.to_le_bytes());
bytes.extend_from_slice(&channels.to_le_bytes());
bytes.extend_from_slice(&rate.to_le_bytes());
bytes.extend_from_slice(&byte_rate.to_le_bytes());
bytes.extend_from_slice(&block_align.to_le_bytes());
bytes.extend_from_slice(&bits.to_le_bytes());
bytes.extend_from_slice(b"data");
bytes.extend_from_slice(&(payload.len() as u32).to_le_bytes());
bytes.extend_from_slice(&payload);
bytes
}
fn sine(rate: u32, seconds: f64) -> Vec<f32> {
let count = (f64::from(rate) * seconds) as usize;
(0..count)
.map(|i| 0.5 * (2.0 * std::f64::consts::PI * 440.0 * i as f64 / f64::from(rate)).sin())
.map(|s| s as f32)
.collect()
}
fn collect(file: File) -> Vec<f32> {
let rate = file.sample_rate();
let mut out = Vec::new();
for chunk in file {
let chunk = chunk.expect("iteration should not error");
assert_eq!(chunk.sample_rate, rate);
assert_eq!(chunk.channels, 1);
out.extend(chunk.data);
}
out
}
#[test]
fn buffer_passthrough_is_byte_identical() {
let input = sine(16000, 0.5); let file = File::buffer(input.clone(), 16000, FileConfig::default()).unwrap();
let mut sizes = Vec::new();
let mut out = Vec::new();
for chunk in file {
let chunk = chunk.unwrap();
sizes.push(chunk.data.len());
out.extend(chunk.data);
}
assert_eq!(out, input);
assert_eq!(sizes, vec![1600, 1600, 1600, 1600, 1600]);
}
#[test]
fn buffer_resamples_and_flushes_tail() {
let input = sine(48000, 0.25);
let file = File::buffer(input.clone(), 48000, FileConfig::default()).unwrap();
let out = collect(file);
let mut resampler = PolyphaseResampler::new(48000, 16000).unwrap();
let mut expected = Vec::new();
for block in input.chunks(FEED_FRAMES) {
resampler.process(block, &mut expected);
}
resampler.flush(&mut expected);
assert_eq!(out, expected);
}
#[test]
fn conditioning_matches_direct_chain_drive() {
let input: Vec<f32> = sine(16000, 0.5).iter().map(|s| s + 0.25).collect();
let config = FileConfig {
dc_removal: true,
..Default::default()
};
let file = File::buffer(input.clone(), 16000, config).unwrap();
let out = collect(file);
let mut chain = build_capture_stage(
1,
1,
16000,
16000,
Transforms {
dc_removal: true,
denoise: None,
highpass: None,
agc: None,
limiter: None,
},
)
.unwrap()
.expect("dc removal builds a chain");
let mut expected = Vec::new();
for block in input.chunks(FEED_FRAMES) {
expected.extend_from_slice(chain.run(block).unwrap());
}
chain.flush(&mut expected).unwrap();
assert_eq!(out, expected);
}
#[test]
fn open_and_new_are_equivalent() {
let samples = sine(16000, 0.3);
let bytes = wav_bytes(1, 1, 16000, &samples);
let path = std::env::temp_dir().join("decibri-file-open-vs-new.wav");
std::fs::write(&path, &bytes).unwrap();
let opened = collect(File::open(&path, FileConfig::default()).unwrap());
let newed = collect(File::new(&path, FileConfig::default()).unwrap());
std::fs::remove_file(&path).ok();
assert_eq!(opened, newed);
assert!(!opened.is_empty());
}
#[test]
fn open_reads_header_and_downmixes() {
let mono = sine(16000, 0.2);
let stereo: Vec<f32> = mono.iter().flat_map(|&s| [s, s]).collect();
let bytes = wav_bytes(3, 2, 16000, &stereo);
let path = std::env::temp_dir().join("decibri-file-stereo-float.wav");
std::fs::write(&path, &bytes).unwrap();
let file = File::open(&path, FileConfig::default()).unwrap();
assert_eq!(file.input_rate(), 16000);
let out = collect(file);
std::fs::remove_file(&path).ok();
for (a, b) in out.iter().zip(mono.iter()) {
assert!((a - b).abs() < 1e-6);
}
assert_eq!(out.len(), mono.len());
}
#[test]
fn empty_buffer_ends_immediately() {
let file = File::buffer(Vec::new(), 16000, FileConfig::default()).unwrap();
assert_eq!(collect(file).len(), 0);
}
#[test]
fn open_missing_file_reports_read_failure() {
let err = File::open("no-such-file.wav", FileConfig::default())
.err()
.expect("a missing file should fail to open");
assert!(matches!(err, DecibriError::FileReadFailed { .. }));
}
#[test]
fn parse_rejects_invalid_wav() {
assert!(matches!(
parse_wav(b"not a wav file at all"),
Err(DecibriError::WavInvalid { .. })
));
let mut truncated = wav_bytes(1, 1, 16000, &sine(16000, 0.1));
truncated.truncate(60);
assert!(matches!(
parse_wav(&truncated),
Err(DecibriError::WavInvalid { .. })
));
let mut eight_bit = wav_bytes(1, 1, 16000, &sine(16000, 0.1));
eight_bit[34] = 8;
eight_bit[35] = 0;
assert!(matches!(
parse_wav(&eight_bit),
Err(DecibriError::WavInvalid { .. })
));
}
#[test]
fn config_validation_matches_live_ranges() {
let config = FileConfig {
sample_rate: 999,
..Default::default()
};
assert!(matches!(
File::buffer(vec![0.0], 16000, config),
Err(DecibriError::SampleRateOutOfRange)
));
let config = FileConfig {
agc: Some(-2),
..Default::default()
};
assert!(matches!(
File::buffer(vec![0.0], 16000, config),
Err(DecibriError::AgcTargetOutOfRange)
));
let config = FileConfig {
limiter: Some(0.5),
..Default::default()
};
assert!(matches!(
File::buffer(vec![0.0], 16000, config),
Err(DecibriError::LimiterCeilingOutOfRange)
));
assert!(matches!(
File::buffer(vec![0.0], 999, FileConfig::default()),
Err(DecibriError::SampleRateOutOfRange)
));
}
#[cfg(feature = "vad")]
fn window(i: usize, is_speech: bool) -> VadWindow {
let secs = 512.0 / 16000.0;
VadWindow {
start: i as f64 * secs,
end: (i + 1) as f64 * secs,
probability: if is_speech { 0.9 } else { 0.1 },
is_speech,
}
}
#[cfg(feature = "vad")]
#[test]
fn merge_joins_consecutive_speech() {
let scores = vec![
window(0, true),
window(1, true),
window(2, false),
window(3, false),
];
let segments = merge_segments(&scores, 0.3);
assert_eq!(segments.len(), 1);
assert_eq!(segments[0].start, 0.0);
assert_eq!(segments[0].end, scores[1].end);
}
#[cfg(feature = "vad")]
#[test]
fn merge_applies_file_time_holdoff() {
let mut inside: Vec<VadWindow> = Vec::new();
inside.push(window(0, true));
for i in 1..9 {
inside.push(window(i, false));
}
inside.push(window(9, true));
assert_eq!(merge_segments(&inside, 0.3).len(), 1);
let mut outside: Vec<VadWindow> = Vec::new();
outside.push(window(0, true));
for i in 1..12 {
outside.push(window(i, false));
}
outside.push(window(12, true));
let segments = merge_segments(&outside, 0.3);
assert_eq!(segments.len(), 2);
assert_eq!(segments[0].end, inside[0].end);
assert_eq!(segments[1].start, window(12, true).start);
}
#[cfg(feature = "vad")]
#[test]
fn merge_empty_and_silence_yield_no_segments() {
assert!(merge_segments(&[], 0.3).is_empty());
let silence = vec![window(0, false), window(1, false)];
assert!(merge_segments(&silence, 0.3).is_empty());
}
#[cfg(feature = "vad")]
fn silero_model_path() -> PathBuf {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
Path::new(manifest_dir)
.join("..")
.join("..")
.join("models")
.join("silero_vad.onnx")
}
#[cfg(feature = "vad")]
fn golden_wav_path() -> PathBuf {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
Path::new(manifest_dir)
.join("tests")
.join("assets")
.join("vad-golden-tts-speech-16k.wav")
}
#[cfg(feature = "vad")]
fn vad_file_config() -> FileConfig {
FileConfig {
vad: Some(VadConfig {
model_path: silero_model_path(),
..VadConfig::default()
}),
..Default::default()
}
}
#[cfg(feature = "vad")]
#[test]
fn analyze_without_vad_errors() {
let file = File::buffer(sine(16000, 0.1), 16000, FileConfig::default()).unwrap();
assert!(matches!(
file.analyze(),
Err(DecibriError::VadNotConfigured)
));
let file = File::buffer(sine(16000, 0.1), 16000, FileConfig::default()).unwrap();
assert!(matches!(
file.analyse(),
Err(DecibriError::VadNotConfigured)
));
}
#[cfg(feature = "vad")]
#[test]
fn analyze_matches_direct_window_scoring() {
let path = golden_wav_path();
let report = File::open(&path, vad_file_config())
.unwrap()
.analyze()
.unwrap();
let bytes = std::fs::read(&path).unwrap();
let samples = parse_wav(&bytes).unwrap().samples;
let mut detector = SileroVad::new(VadConfig {
model_path: silero_model_path(),
..VadConfig::default()
})
.unwrap();
let mut expected: Vec<f32> = Vec::new();
for window in samples.chunks_exact(512) {
expected.push(detector.process(window).unwrap().probability);
}
assert_eq!(report.scores.len(), expected.len());
for (w, e) in report.scores.iter().zip(expected.iter()) {
assert_eq!(w.probability, *e);
}
for (i, w) in report.scores.iter().enumerate() {
assert!((w.start - i as f64 * 512.0 / 16000.0).abs() < 1e-9);
assert!((w.end - w.start - 512.0 / 16000.0).abs() < 1e-9);
}
assert!(!report.segments.is_empty());
let duration = samples.len() as f64 / 16000.0;
for segment in &report.segments {
assert!(segment.start < segment.end);
assert!(segment.end <= duration + 1e-9);
}
}
#[cfg(feature = "vad")]
#[test]
fn analyse_equals_analyze() {
let path = golden_wav_path();
let a = File::open(&path, vad_file_config())
.unwrap()
.analyze()
.unwrap();
let b = File::open(&path, vad_file_config())
.unwrap()
.analyse()
.unwrap();
assert_eq!(a.scores, b.scores);
assert_eq!(a.segments, b.segments);
}
#[cfg(feature = "vad")]
#[test]
fn analyze_resamples_feed_for_non_detector_rate() {
let mut config = vad_file_config();
config.sample_rate = 22050;
let file = File::open(golden_wav_path(), config).unwrap();
assert_eq!(file.vad_rate(), Some(16000));
let report = file.analyze().unwrap();
assert!(!report.scores.is_empty());
let max = report
.scores
.iter()
.map(|w| w.probability)
.fold(0.0f32, f32::max);
assert!(
max >= 0.5,
"speech should still be found through the internal feed resample, max {max}"
);
assert!(!report.segments.is_empty());
}
#[cfg(feature = "vad")]
#[test]
fn vad_rate_follows_detector_native_targets() {
let mut config = vad_file_config();
config.sample_rate = 8000;
let file = File::buffer(sine(8000, 0.1), 8000, config).unwrap();
assert_eq!(file.vad_rate(), Some(8000));
let file = File::buffer(sine(16000, 0.1), 16000, FileConfig::default()).unwrap();
assert_eq!(file.vad_rate(), None);
}
#[cfg(feature = "vad")]
#[test]
fn vad_input_drains_feed_during_iteration() {
let input = sine(16000, 0.2);
let mut config = vad_file_config();
config.dc_removal = true;
let mut file = File::buffer(input.clone(), 16000, config).unwrap();
let mut fed: Vec<f32> = Vec::new();
loop {
let chunk = match file.next() {
Some(Ok(chunk)) => chunk,
Some(Err(e)) => panic!("iteration error: {e}"),
None => break,
};
let _ = chunk;
fed.extend(file.vad_input().expect("vad configured"));
}
assert_eq!(fed, input);
let mut plain = File::buffer(input, 16000, FileConfig::default()).unwrap();
assert!(plain.vad_input().is_none());
}
}