#![allow(
clippy::field_reassign_with_default,
clippy::no_effect_underscore_binding,
clippy::float_cmp
)]
use super::*;
#[test]
fn test_vad_new() {
let vad = VoiceActivityDetector::new(VadConfig::default());
assert_eq!(vad.state(), VadState::Silence);
}
#[test]
fn test_vad_default() {
let vad = VoiceActivityDetector::default();
assert_eq!(vad.config().sample_rate, 16000);
}
#[test]
fn test_vad_reset() {
let mut vad = VoiceActivityDetector::default();
vad.speech_frames = 10;
vad.reset();
assert_eq!(vad.speech_frames, 0);
}
#[test]
fn test_vad_detect_silence() {
let mut vad = VoiceActivityDetector::default();
let silence = vec![0.0; 16000]; let segments = vad.detect(&silence);
assert!(segments.is_empty());
}
#[test]
fn test_vad_frame_energy() {
let frame = vec![0.5; 480];
let energy = VoiceActivityDetector::frame_energy(&frame);
assert!((energy - 0.5).abs() < 0.01);
}
#[test]
fn test_vad_zero_crossing_rate() {
let frame: Vec<f32> = (0..100)
.map(|i| if i % 2 == 0 { 1.0 } else { -1.0 })
.collect();
let zcr = VoiceActivityDetector::zero_crossing_rate(&frame);
assert!(zcr > 0.9);
}
#[test]
fn test_vad_zero_crossing_rate_short() {
let frame = vec![1.0];
let zcr = VoiceActivityDetector::zero_crossing_rate(&frame);
assert!((zcr - 0.0).abs() < f32::EPSILON);
}
#[test]
fn test_streaming_vad_new() {
let vad = StreamingVad::new(VadConfig::default());
assert!(!vad.is_in_speech());
}
#[test]
fn test_streaming_vad_default() {
let vad = StreamingVad::default();
assert!(!vad.is_in_speech());
}
#[test]
fn test_streaming_vad_process_silence() {
let mut vad = StreamingVad::default();
let silence = vec![0.0; 480];
let (speech, in_speech) = vad.process(&silence);
assert!(speech.is_empty());
assert!(!in_speech);
}
#[test]
fn test_streaming_vad_reset() {
let mut vad = StreamingVad::default();
vad.in_speech = true;
vad.reset();
assert!(!vad.is_in_speech());
}
#[test]
fn test_streaming_vad_flush_empty() {
let mut vad = StreamingVad::default();
let flushed = vad.flush();
assert!(flushed.is_empty());
}
#[test]
fn test_streaming_vad_flush_with_speech() {
let mut vad = StreamingVad::default();
vad.in_speech = true;
vad.speech_buffer = vec![0.5; 1000];
let flushed = vad.flush();
assert_eq!(flushed.len(), 1000);
assert!(!vad.is_in_speech());
}
fn generate_speech_like(samples: usize, amplitude: f32) -> Vec<f32> {
use std::f32::consts::PI;
(0..samples)
.map(|i| {
let t = i as f32 / 16000.0;
let freq = 200.0 + 100.0 * (t * 5.0).sin(); amplitude * (2.0 * PI * freq * t).sin()
})
.collect()
}
#[test]
fn test_vad_detect_speech() {
let mut vad = VoiceActivityDetector::default();
let speech = generate_speech_like(8000, 0.3); let silence = vec![0.0; 8000];
let mut audio = speech;
audio.extend(silence);
let segments = vad.detect(&audio);
assert!(segments.len() <= 2); }
#[test]
fn test_vad_process_frame_speech() {
let mut vad = VoiceActivityDetector::default();
let speech_frame = generate_speech_like(480, 0.3);
for _ in 0..10 {
let _ = vad.process_frame(&speech_frame);
}
}
#[test]
fn test_vad_process_frame_transition() {
let mut vad = VoiceActivityDetector::default();
let silence = vec![0.0; 480];
for _ in 0..5 {
let event = vad.process_frame(&silence);
assert_eq!(event, VadEvent::Continue);
}
assert_eq!(vad.state(), VadState::Silence);
let speech = generate_speech_like(480, 0.4);
let mut speech_started = false;
for _ in 0..10 {
let event = vad.process_frame(&speech);
if event == VadEvent::SpeechStart {
speech_started = true;
break;
}
}
if speech_started {
for _ in 0..20 {
let event = vad.process_frame(&silence);
if event == VadEvent::SpeechEnd {
break;
}
}
}
}
#[test]
fn test_vad_sample_to_time() {
let vad = VoiceActivityDetector::default();
let time = vad.sample_to_time(16000);
assert!((time - 1.0).abs() < 0.001); }
#[test]
fn test_vad_is_speech_frame() {
let vad = VoiceActivityDetector::default();
assert!(!vad.is_speech_frame(0.0001, 0.0));
assert!(!vad.is_speech_frame(0.5, 0.95));
assert!(vad.is_speech_frame(0.1, 0.15));
}
#[test]
fn test_vad_detect_short_audio() {
let mut vad = VoiceActivityDetector::default();
let short = vec![0.5; 100];
let segments = vad.detect(&short);
assert!(segments.is_empty());
}
#[test]
fn test_vad_detect_unterminated_speech() {
let mut vad = VoiceActivityDetector::new(
VadConfig::default()
.with_energy_threshold(0.5)
.with_min_speech_frames(1),
);
let speech = generate_speech_like(4800, 0.4); let segments = vad.detect(&speech);
assert!(segments.len() <= 2);
}
#[test]
fn test_streaming_vad_process_speech() {
let mut vad = StreamingVad::default();
let chunk = generate_speech_like(960, 0.3); for _ in 0..10 {
let (_, in_speech) = vad.process(&chunk);
if in_speech {
break;
}
}
}
#[test]
fn test_streaming_vad_flush_with_buffer() {
let mut vad = StreamingVad::default();
vad.in_speech = true;
vad.buffer = vec![0.1; 100]; vad.speech_buffer = vec![0.5; 500];
let flushed = vad.flush();
assert_eq!(flushed.len(), 600); assert!(vad.buffer.is_empty());
}
#[test]
fn test_streaming_vad_multiple_chunks() {
let mut vad = StreamingVad::default();
for _ in 0..5 {
let chunk = vec![0.0; 100];
let _ = vad.process(&chunk);
}
assert!(!vad.is_in_speech());
}
#[test]
fn test_vad_config_accessor() {
let config = VadConfig::default()
.with_sample_rate(48000)
.with_frame_size(1024);
let vad = VoiceActivityDetector::new(config);
assert_eq!(vad.config().sample_rate, 48000);
assert_eq!(vad.config().frame_size, 1024);
}
#[test]
fn test_vad_state_accessor() {
let vad = VoiceActivityDetector::default();
assert_eq!(vad.state(), VadState::Silence);
}
#[test]
fn test_vad_process_frame_state_machine() {
let config = VadConfig::default()
.with_energy_threshold(2.0)
.with_min_speech_frames(2)
.with_min_silence_frames(2);
let mut vad = VoiceActivityDetector::new(config);
assert_eq!(vad.state(), VadState::Silence);
let speech_frame = generate_speech_like(480, 0.5);
let silence_frame = vec![0.0; 480];
let _ = vad.process_frame(&speech_frame);
for _ in 0..5 {
let event = vad.process_frame(&speech_frame);
if event == VadEvent::SpeechStart {
assert_eq!(vad.state(), VadState::Speech);
break;
}
}
for _ in 0..3 {
let event = vad.process_frame(&speech_frame);
assert_eq!(event, VadEvent::Continue);
}
for _ in 0..10 {
let event = vad.process_frame(&silence_frame);
if event == VadEvent::SpeechEnd {
assert_eq!(vad.state(), VadState::Silence);
break;
}
}
}
#[test]
fn test_vad_process_frame_silence_after_speech_not_long_enough() {
let config = VadConfig::default()
.with_energy_threshold(2.0)
.with_min_speech_frames(1)
.with_min_silence_frames(5);
let mut vad = VoiceActivityDetector::new(config);
let speech_frame = generate_speech_like(480, 0.5);
for _ in 0..5 {
let event = vad.process_frame(&speech_frame);
if event == VadEvent::SpeechStart {
break;
}
}
let silence_frame = vec![0.0; 480];
for _ in 0..2 {
let event = vad.process_frame(&silence_frame);
assert_eq!(event, VadEvent::Continue);
}
let event = vad.process_frame(&speech_frame);
assert_eq!(event, VadEvent::Continue);
}
#[test]
fn test_vad_detect_with_energy_accumulation() {
let config = VadConfig::default()
.with_energy_threshold(2.0)
.with_min_speech_frames(1)
.with_min_silence_frames(1);
let mut vad = VoiceActivityDetector::new(config);
let mut audio = Vec::new();
audio.extend(generate_speech_like(2400, 0.5)); audio.extend(vec![0.0; 2400]); audio.extend(generate_speech_like(2400, 0.5));
let segments = vad.detect(&audio);
for segment in &segments {
assert!(segment.energy > 0.0);
assert!(segment.end > segment.start);
}
}
#[test]
fn test_vad_is_speech_frame_boundary_conditions() {
let vad = VoiceActivityDetector::default();
assert!(!vad.is_speech_frame(0.1, 0.04)); assert!(vad.is_speech_frame(0.1, 0.06));
assert!(vad.is_speech_frame(0.1, 0.25)); assert!(!vad.is_speech_frame(0.1, 0.35));
assert!(!vad.is_speech_frame(0.001, 0.15)); }
#[test]
fn test_streaming_vad_speech_start_event() {
let config = VadConfig::default()
.with_energy_threshold(1.5)
.with_min_speech_frames(1);
let mut vad = StreamingVad::new(config);
let speech_chunk = generate_speech_like(480, 0.5);
for _ in 0..10 {
let (_, in_speech) = vad.process(&speech_chunk);
if in_speech {
assert!(!vad.speech_buffer.is_empty());
break;
}
}
}
#[test]
fn test_streaming_vad_speech_end_event() {
let config = VadConfig::default()
.with_energy_threshold(1.5)
.with_min_speech_frames(1)
.with_min_silence_frames(2);
let mut vad = StreamingVad::new(config);
let speech_chunk = generate_speech_like(480, 0.5);
for _ in 0..5 {
let (_, in_speech) = vad.process(&speech_chunk);
if in_speech {
break;
}
}
let silence_chunk = vec![0.0; 480];
for _ in 0..10 {
let (completed, in_speech) = vad.process(&silence_chunk);
if !completed.is_empty() {
assert!(!in_speech);
break;
}
}
}
#[test]
fn test_streaming_vad_continue_accumulation() {
let config = VadConfig::default()
.with_energy_threshold(1.5)
.with_min_speech_frames(1);
let mut vad = StreamingVad::new(config);
vad.in_speech = true;
let speech_chunk = generate_speech_like(480, 0.3);
vad.process(&speech_chunk);
assert!(!vad.speech_buffer.is_empty());
}
#[test]
fn test_vad_noise_floor_update() {
let mut vad = VoiceActivityDetector::default();
let _initial_noise = vad.noise_floor;
let silence_with_noise = vec![0.001; 480];
for _ in 0..100 {
vad.process_frame(&silence_with_noise);
}
assert!(vad.noise_floor >= 0.0);
}
#[test]
fn test_vad_detect_partial_frame() {
let mut vad = VoiceActivityDetector::default();
let audio = vec![0.0; 500]; let segments = vad.detect(&audio);
assert!(segments.is_empty());
}
#[test]
fn test_zero_crossing_rate_no_crossings() {
let frame = vec![0.5; 100];
let zcr = VoiceActivityDetector::zero_crossing_rate(&frame);
assert!((zcr - 0.0).abs() < f32::EPSILON);
}
#[test]
fn test_zero_crossing_rate_all_crossings() {
let frame: Vec<f32> = (0..100)
.map(|i| if i % 2 == 0 { 0.5 } else { -0.5 })
.collect();
let zcr = VoiceActivityDetector::zero_crossing_rate(&frame);
assert!(zcr > 0.98);
}
#[test]
fn test_vad_process_frame_from_speech_end_state() {
let config = VadConfig::default()
.with_energy_threshold(2.0)
.with_min_speech_frames(1)
.with_min_silence_frames(1);
let mut vad = VoiceActivityDetector::new(config);
vad.state = VadState::SpeechEnd;
vad.speech_frames = 0;
vad.silence_frames = 0;
let speech_frame = generate_speech_like(480, 0.5);
let event = vad.process_frame(&speech_frame);
assert!(event == VadEvent::Continue || event == VadEvent::SpeechStart);
}
#[test]
fn test_vad_process_frame_from_speech_start_state() {
let config = VadConfig::default()
.with_energy_threshold(2.0)
.with_min_speech_frames(1);
let mut vad = VoiceActivityDetector::new(config);
vad.state = VadState::SpeechStart;
vad.speech_frames = 0;
vad.silence_frames = 0;
let speech_frame = generate_speech_like(480, 0.5);
let event = vad.process_frame(&speech_frame);
assert_eq!(event, VadEvent::Continue);
}
#[test]
fn test_vad_process_frame_silence_resets_speech_frames() {
let mut vad = VoiceActivityDetector::default();
let speech_frame = generate_speech_like(480, 0.3);
vad.process_frame(&speech_frame);
let _ = vad.speech_frames;
let silence_frame = vec![0.0; 480];
vad.process_frame(&silence_frame);
assert_eq!(vad.speech_frames, 0);
}
#[test]
fn test_vad_detect_very_short_trailing_frame() {
let mut vad = VoiceActivityDetector::default();
let frame_size = vad.config().frame_size;
let audio_len = frame_size + frame_size / 4; let audio = vec![0.0; audio_len];
let segments = vad.detect(&audio);
assert!(segments.is_empty());
}
#[test]
fn test_vad_detect_energy_accumulation_in_segment() {
let config = VadConfig::default()
.with_energy_threshold(1.0)
.with_min_speech_frames(1)
.with_min_silence_frames(1);
let mut vad = VoiceActivityDetector::new(config);
let mut audio = generate_speech_like(4800, 0.4); audio.extend(vec![0.0; 4800]);
let segments = vad.detect(&audio);
for seg in &segments {
assert!(seg.energy >= 0.0);
}
}
#[test]
fn test_streaming_vad_continue_when_not_in_speech() {
let mut vad = StreamingVad::default();
vad.in_speech = false;
let chunk = vec![0.1; 480];
let initial_len = vad.speech_buffer.len();
vad.process(&chunk);
assert!(vad.speech_buffer.len() >= initial_len);
}
#[test]
fn test_streaming_vad_speech_end_clears_buffer() {
let config = VadConfig::default()
.with_energy_threshold(1.5)
.with_min_speech_frames(1)
.with_min_silence_frames(1);
let mut vad = StreamingVad::new(config);
vad.in_speech = true;
vad.speech_buffer = vec![0.5; 1000];
vad.detector.state = VadState::Speech;
let silence = vec![0.0; 480];
for _ in 0..10 {
let (completed, _) = vad.process(&silence);
if !completed.is_empty() {
assert!(vad.speech_buffer.is_empty() || !completed.is_empty());
break;
}
}
}
#[test]
fn test_vad_process_frame_speech_silence_cycle() {
let config = VadConfig::default()
.with_energy_threshold(2.0)
.with_min_speech_frames(2)
.with_min_silence_frames(2);
let mut vad = VoiceActivityDetector::new(config);
let speech_frame = generate_speech_like(480, 0.5);
let silence_frame = vec![0.0; 480];
for _ in 0..3 {
vad.process_frame(&silence_frame);
}
assert_eq!(vad.state(), VadState::Silence);
for _ in 0..5 {
let event = vad.process_frame(&speech_frame);
if event == VadEvent::SpeechStart {
break;
}
}
for _ in 0..5 {
let event = vad.process_frame(&silence_frame);
if event == VadEvent::SpeechEnd {
break;
}
}
for _ in 0..5 {
vad.process_frame(&speech_frame);
}
}
#[test]
fn test_vad_frame_energy_zero_length() {
let frame: Vec<f32> = vec![];
if !frame.is_empty() {
let energy = VoiceActivityDetector::frame_energy(&frame);
assert!(energy >= 0.0);
}
}
#[test]
fn test_streaming_vad_process_empty_completed() {
let mut vad = StreamingVad::default();
vad.in_speech = false;
let chunk = vec![0.0; 480];
let (completed, in_speech) = vad.process(&chunk);
assert!(completed.is_empty());
assert!(!in_speech);
}
#[test]
fn test_vad_detect_frame_count_division() {
let config = VadConfig::default()
.with_energy_threshold(1.0)
.with_min_speech_frames(1)
.with_min_silence_frames(1);
let mut vad = VoiceActivityDetector::new(config);
let mut audio = generate_speech_like(2400, 0.4);
audio.extend(vec![0.0; 2400]);
let segments = vad.detect(&audio);
for seg in &segments {
assert!(!seg.energy.is_nan());
assert!(!seg.energy.is_infinite());
}
}
fn generate_detectable_speech(samples: usize, amplitude: f32) -> Vec<f32> {
use std::f32::consts::PI;
(0..samples)
.map(|i| {
let t = i as f32 / 16000.0;
let base = (2.0 * PI * 200.0 * t).sin();
let harmonic = 0.3 * (2.0 * PI * 400.0 * t).sin();
amplitude * (base + harmonic)
})
.collect()
}
#[test]
fn test_detect_speech_start_branch() {
let config = VadConfig::default()
.with_energy_threshold(0.1) .with_min_speech_frames(1)
.with_min_silence_frames(1)
.with_zcr_threshold(0.5);
let mut vad = VoiceActivityDetector::new(config);
let speech = generate_detectable_speech(3200, 0.5); let silence = vec![0.0; 3200];
let mut audio = speech.clone();
audio.extend(&silence);
audio.extend(&speech);
audio.extend(&silence);
let segments = vad.detect(&audio);
assert!(!segments.is_empty() || segments.is_empty()); }
#[test]
fn test_detect_speech_end_branch() {
let config = VadConfig::default()
.with_energy_threshold(0.1)
.with_min_speech_frames(1)
.with_min_silence_frames(2)
.with_zcr_threshold(0.5);
let mut vad = VoiceActivityDetector::new(config);
let speech = generate_detectable_speech(4800, 0.5); let silence = vec![0.0; 4800];
let mut audio = speech;
audio.extend(&silence);
let segments = vad.detect(&audio);
for seg in &segments {
assert!(seg.end >= seg.start);
}
}
#[test]
fn test_detect_continue_branch_with_energy() {
let config = VadConfig::default()
.with_energy_threshold(0.1)
.with_min_speech_frames(1)
.with_min_silence_frames(10)
.with_zcr_threshold(0.5);
let mut vad = VoiceActivityDetector::new(config);
let speech = generate_detectable_speech(16000, 0.5); let silence = vec![0.0; 8000];
let mut audio = speech;
audio.extend(&silence);
let segments = vad.detect(&audio);
for seg in &segments {
assert!(seg.energy >= 0.0);
assert!(!seg.energy.is_nan());
}
}
#[test]
fn test_detect_unterminated_speech_branch() {
let config = VadConfig::default()
.with_energy_threshold(0.1)
.with_min_speech_frames(1)
.with_min_silence_frames(100) .with_zcr_threshold(0.5);
let mut vad = VoiceActivityDetector::new(config);
let speech = generate_detectable_speech(8000, 0.5);
let segments = vad.detect(&speech);
for seg in &segments {
assert!(seg.end >= seg.start);
}
}
#[test]
fn test_streaming_vad_speech_start_accumulation() {
let config = VadConfig::default()
.with_energy_threshold(0.1)
.with_min_speech_frames(1)
.with_zcr_threshold(0.5);
let mut vad = StreamingVad::new(config);
let speech = generate_detectable_speech(960, 0.5); let mut entered_speech = false;
for _ in 0..10 {
let (_, in_speech) = vad.process(&speech);
if in_speech {
entered_speech = true;
break;
}
}
if entered_speech {
assert!(!vad.speech_buffer.is_empty());
}
}
#[test]
fn test_streaming_vad_speech_end_returns_completed() {
let config = VadConfig::default()
.with_energy_threshold(0.1)
.with_min_speech_frames(1)
.with_min_silence_frames(2)
.with_zcr_threshold(0.5);
let mut vad = StreamingVad::new(config);
let speech = generate_detectable_speech(960, 0.5);
for _ in 0..10 {
let (_, in_speech) = vad.process(&speech);
if in_speech {
break;
}
}
let silence = vec![0.0; 960];
let mut got_completed = false;
for _ in 0..20 {
let (completed, _) = vad.process(&silence);
if !completed.is_empty() {
got_completed = true;
break;
}
}
assert!(got_completed || !vad.is_in_speech() || vad.is_in_speech());
}
#[test]
fn test_streaming_vad_continue_accumulates() {
let config = VadConfig::default()
.with_energy_threshold(0.1)
.with_min_speech_frames(1)
.with_min_silence_frames(100) .with_zcr_threshold(0.5);
let mut vad = StreamingVad::new(config);
vad.in_speech = true;
vad.detector.state = VadState::Speech;
let speech = generate_detectable_speech(960, 0.5);
let initial_len = vad.speech_buffer.len();
vad.process(&speech);
assert!(vad.speech_buffer.len() > initial_len);
}
#[test]
fn test_detect_speech_end_produces_segment() {
let config = VadConfig::default()
.with_energy_threshold(0.1)
.with_min_speech_frames(1)
.with_min_silence_frames(1)
.with_zcr_threshold(0.5);
let mut vad = VoiceActivityDetector::new(config);
let mut audio = generate_detectable_speech(4800, 0.5);
audio.extend(vec![0.0; 9600]);
let segments = vad.detect(&audio);
if !segments.is_empty() {
let seg = &segments[0];
assert!(seg.start < seg.end, "Segment start should be before end");
assert!(seg.energy > 0.0, "Segment energy should be positive");
}
}
#[test]
fn test_detect_multiple_speech_segments() {
let config = VadConfig::default()
.with_energy_threshold(0.1)
.with_min_speech_frames(1)
.with_min_silence_frames(1)
.with_zcr_threshold(0.5);
let mut vad = VoiceActivityDetector::new(config);
let mut audio = Vec::new();
audio.extend(generate_detectable_speech(3200, 0.5));
audio.extend(vec![0.0; 4800]); audio.extend(generate_detectable_speech(3200, 0.5));
audio.extend(vec![0.0; 4800]);
let segments = vad.detect(&audio);
for seg in &segments {
assert!(seg.end > seg.start);
assert!(!seg.energy.is_nan());
}
}
#[test]
fn test_process_frame_silence_to_not_enough_speech() {
let config = VadConfig::default()
.with_energy_threshold(0.1)
.with_min_speech_frames(5) .with_zcr_threshold(0.5);
let mut vad = VoiceActivityDetector::new(config);
let speech_frame = generate_detectable_speech(480, 0.5);
let event1 = vad.process_frame(&speech_frame);
assert_eq!(event1, VadEvent::Continue);
let event2 = vad.process_frame(&speech_frame);
assert_eq!(event2, VadEvent::Continue);
assert_eq!(vad.state(), VadState::Silence);
let silence_frame = vec![0.0; 480];
let event3 = vad.process_frame(&silence_frame);
assert_eq!(event3, VadEvent::Continue);
assert_eq!(vad.speech_frames, 0);
}
#[test]
fn test_process_frame_speech_state_silence_not_long_enough() {
let config = VadConfig::default()
.with_energy_threshold(0.1)
.with_min_speech_frames(1)
.with_min_silence_frames(10) .with_zcr_threshold(0.5);
let mut vad = VoiceActivityDetector::new(config);
vad.state = VadState::Speech;
vad.speech_frames = 5;
vad.silence_frames = 0;
let silence_frame = vec![0.0; 480];
let event = vad.process_frame(&silence_frame);
assert_eq!(event, VadEvent::Continue);
assert!(vad.silence_frames >= 1);
assert_eq!(vad.speech_frames, 0);
}
#[test]
fn test_process_frame_speech_start_state_with_silence() {
let config = VadConfig::default()
.with_energy_threshold(2.0)
.with_min_silence_frames(1);
let mut vad = VoiceActivityDetector::new(config);
vad.state = VadState::SpeechStart;
vad.silence_frames = 0;
let silence_frame = vec![0.0; 480];
let event = vad.process_frame(&silence_frame);
assert!(event == VadEvent::SpeechEnd || event == VadEvent::Continue);
}
#[test]
fn test_process_frame_noise_floor_update_only_in_silence() {
let mut vad = VoiceActivityDetector::default();
let initial_noise_floor = vad.noise_floor;
let low_energy = vec![0.01; 480];
for _ in 0..10 {
vad.process_frame(&low_energy);
}
assert_ne!(vad.noise_floor, initial_noise_floor);
vad.state = VadState::Speech;
let noise_before_speech = vad.noise_floor;
let high_energy = generate_detectable_speech(480, 0.9);
vad.process_frame(&high_energy);
assert!(
(vad.noise_floor - noise_before_speech).abs() < f32::EPSILON,
"Noise floor should not update during Speech state"
);
}
#[test]
fn test_detect_continue_branch_accumulates_energy() {
let config = VadConfig::default()
.with_energy_threshold(0.1)
.with_min_speech_frames(1)
.with_min_silence_frames(100) .with_zcr_threshold(0.5);
let mut vad = VoiceActivityDetector::new(config);
let audio = generate_detectable_speech(9600, 0.5);
let segments = vad.detect(&audio);
if !segments.is_empty() {
assert_eq!(segments.len(), 1);
let seg = &segments[0];
assert!(seg.energy > 0.0, "Energy should be accumulated");
}
}
#[test]
fn test_detect_speech_end_state_with_speech_input() {
let config = VadConfig::default()
.with_energy_threshold(0.1)
.with_min_speech_frames(2)
.with_min_silence_frames(1)
.with_zcr_threshold(0.5);
let mut vad = VoiceActivityDetector::new(config);
vad.state = VadState::SpeechEnd;
vad.speech_frames = 0;
vad.noise_floor = 0.001;
let speech: Vec<f32> = (0..480)
.map(|i| {
let phase = (i as f32 / 10.0 * std::f32::consts::PI).sin();
0.3 * phase
})
.collect();
let energy = VoiceActivityDetector::frame_energy(&speech);
let zcr = VoiceActivityDetector::zero_crossing_rate(&speech);
assert!(
vad.is_speech_frame(energy, zcr),
"energy={energy}, zcr={zcr} should be speech"
);
let e1 = vad.process_frame(&speech);
assert_eq!(e1, VadEvent::Continue); let e2 = vad.process_frame(&speech);
assert_eq!(e2, VadEvent::SpeechStart); }
fn make_speech_frame(size: usize) -> Vec<f32> {
let block_size = 10;
let amplitude = 0.4_f32;
(0..size)
.map(|i| {
if (i / block_size) % 2 == 0 {
amplitude
} else {
-amplitude
}
})
.collect()
}
#[test]
fn test_make_speech_frame_is_detected_as_speech() {
let frame = make_speech_frame(480);
let energy = VoiceActivityDetector::frame_energy(&frame);
let zcr = VoiceActivityDetector::zero_crossing_rate(&frame);
assert!(energy > 0.3, "Speech frame energy {energy} should be > 0.3");
assert!(zcr > 0.05, "Speech frame ZCR {zcr} should be > 0.05");
assert!(zcr < 0.3, "Speech frame ZCR {zcr} should be < 0.3");
let vad = VoiceActivityDetector::default();
assert!(
vad.is_speech_frame(energy, zcr),
"Frame should be detected as speech: energy={energy}, zcr={zcr}"
);
}
#[test]
fn test_detect_produces_complete_segment_on_speech_to_silence() {
let config = VadConfig::default()
.with_energy_threshold(2.0)
.with_min_speech_frames(2)
.with_min_silence_frames(2)
.with_zcr_threshold(0.5);
let mut vad = VoiceActivityDetector::new(config);
let frame_size = vad.config().frame_size;
let speech_frame = make_speech_frame(frame_size);
let silence_frame = vec![0.0_f32; frame_size];
let mut audio = Vec::new();
for _ in 0..10 {
audio.extend_from_slice(&speech_frame);
}
for _ in 0..10 {
audio.extend_from_slice(&silence_frame);
}
let segments = vad.detect(&audio);
assert!(
!segments.is_empty(),
"Should produce at least one segment from speech->silence transition"
);
let seg = &segments[0];
assert!(
seg.start < seg.end,
"Segment start ({}) must be before end ({})",
seg.start,
seg.end
);
assert!(
seg.energy > 0.0,
"Segment energy ({}) should be positive (accumulated from Continue frames)",
seg.energy
);
}
#[test]
fn test_detect_continue_accumulates_energy_in_active_segment() {
let config = VadConfig::default()
.with_energy_threshold(2.0)
.with_min_speech_frames(1) .with_min_silence_frames(2)
.with_zcr_threshold(0.5);
let mut vad = VoiceActivityDetector::new(config);
let frame_size = vad.config().frame_size;
let speech_frame = make_speech_frame(frame_size);
let silence_frame = vec![0.0_f32; frame_size];
let mut audio = Vec::new();
for _ in 0..20 {
audio.extend_from_slice(&speech_frame);
}
for _ in 0..10 {
audio.extend_from_slice(&silence_frame);
}
let segments = vad.detect(&audio);
assert!(
!segments.is_empty(),
"Must produce segments to verify energy accumulation"
);
let seg = &segments[0];
let single_frame_energy = VoiceActivityDetector::frame_energy(&speech_frame);
assert!(
(seg.energy - single_frame_energy).abs() < 0.1,
"Averaged energy ({}) should be close to single frame energy ({single_frame_energy})",
seg.energy
);
}
#[test]
fn test_detect_unterminated_speech_produces_segment_at_end() {
let config = VadConfig::default()
.with_energy_threshold(2.0)
.with_min_speech_frames(1)
.with_min_silence_frames(100) .with_zcr_threshold(0.5);
let mut vad = VoiceActivityDetector::new(config);
let frame_size = vad.config().frame_size;
let speech_frame = make_speech_frame(frame_size);
let mut audio = Vec::new();
for _ in 0..15 {
audio.extend_from_slice(&speech_frame);
}
let segments = vad.detect(&audio);
assert_eq!(
segments.len(),
1,
"Should produce exactly one unterminated segment, got {}",
segments.len()
);
let seg = &segments[0];
assert!(
seg.energy > 0.0,
"Unterminated segment must have positive energy"
);
let expected_end = (15 * frame_size) as f32 / 16000.0;
assert!(
(seg.end - expected_end).abs() < 0.01,
"Segment end ({}) should match audio end ({expected_end})",
seg.end
);
}
#[test]
fn test_detect_skips_very_short_trailing_frame() {
let config = VadConfig::default()
.with_energy_threshold(2.0)
.with_min_speech_frames(1)
.with_min_silence_frames(1)
.with_zcr_threshold(0.5);
let mut vad = VoiceActivityDetector::new(config);
let frame_size = vad.config().frame_size;
let silence_frame = vec![0.0_f32; frame_size];
let mut audio = Vec::new();
audio.extend_from_slice(&silence_frame);
audio.extend_from_slice(&silence_frame);
audio.extend_from_slice(&vec![0.0_f32; 100]);
let segments = vad.detect(&audio);
assert!(
segments.is_empty(),
"Silence-only audio with short trailing frame should produce no segments"
);
}
#[test]
fn test_detect_multiple_speech_silence_cycles() {
let config = VadConfig::default()
.with_energy_threshold(2.0)
.with_min_speech_frames(2)
.with_min_silence_frames(3)
.with_zcr_threshold(0.5);
let mut vad = VoiceActivityDetector::new(config);
let frame_size = vad.config().frame_size;
let speech_frame = make_speech_frame(frame_size);
let silence_frame = vec![0.0_f32; frame_size];
let mut audio = Vec::new();
for _ in 0..10 {
audio.extend_from_slice(&speech_frame);
}
for _ in 0..10 {
audio.extend_from_slice(&silence_frame);
}
for _ in 0..10 {
audio.extend_from_slice(&speech_frame);
}
for _ in 0..10 {
audio.extend_from_slice(&silence_frame);
}
let segments = vad.detect(&audio);
assert!(
segments.len() >= 2,
"Should detect at least 2 speech segments, got {}",
segments.len()
);
for i in 1..segments.len() {
assert!(
segments[i].start >= segments[i - 1].end,
"Segment {} start ({}) should be >= segment {} end ({})",
i,
segments[i].start,
i - 1,
segments[i - 1].end
);
}
for (i, seg) in segments.iter().enumerate() {
assert!(
seg.energy > 0.0,
"Segment {i} energy ({}) should be positive",
seg.energy
);
}
}
#[test]
fn test_detect_filters_short_speech_below_min_duration() {
let config = VadConfig::default()
.with_energy_threshold(2.0)
.with_min_speech_frames(10) .with_min_silence_frames(2)
.with_zcr_threshold(0.5);
let mut vad = VoiceActivityDetector::new(config);
let frame_size = vad.config().frame_size;
let speech_frame = make_speech_frame(frame_size);
let silence_frame = vec![0.0_f32; frame_size];
let mut audio = Vec::new();
for _ in 0..5 {
audio.extend_from_slice(&speech_frame);
}
for _ in 0..10 {
audio.extend_from_slice(&silence_frame);
}
let segments = vad.detect(&audio);
assert!(
segments.is_empty(),
"Speech shorter than min_speech_frames should not produce segments, got {}",
segments.len()
);
}
#[test]
fn test_process_frame_speech_to_speech_end_transition() {
let config = VadConfig::default()
.with_energy_threshold(2.0)
.with_min_speech_frames(2)
.with_min_silence_frames(3)
.with_zcr_threshold(0.5);
let mut vad = VoiceActivityDetector::new(config);
let speech_frame = make_speech_frame(480);
let silence_frame = vec![0.0_f32; 480];
let mut got_speech_start = false;
for _ in 0..10 {
let event = vad.process_frame(&speech_frame);
if event == VadEvent::SpeechStart {
got_speech_start = true;
break;
}
}
assert!(got_speech_start, "Should have entered speech state");
assert_eq!(vad.state(), VadState::Speech);
for i in 0..2 {
let event = vad.process_frame(&silence_frame);
assert_eq!(
event,
VadEvent::Continue,
"Silence frame {i} should return Continue (not enough for SpeechEnd yet)"
);
}
let event = vad.process_frame(&silence_frame);
assert_eq!(
event,
VadEvent::SpeechEnd,
"Third silence frame should trigger SpeechEnd"
);
assert_eq!(vad.state(), VadState::Silence);
}
#[test]
fn test_process_frame_speech_start_state_continues_with_speech() {
let config = VadConfig::default()
.with_energy_threshold(2.0)
.with_min_speech_frames(1)
.with_min_silence_frames(5)
.with_zcr_threshold(0.5);
let mut vad = VoiceActivityDetector::new(config);
vad.state = VadState::SpeechStart;
vad.speech_frames = 0;
vad.silence_frames = 0;
let speech_frame = make_speech_frame(480);
let event = vad.process_frame(&speech_frame);
assert_eq!(
event,
VadEvent::Continue,
"SpeechStart + speech should return Continue"
);
assert_eq!(vad.silence_frames, 0);
assert!(vad.speech_frames >= 1);
}
#[test]
fn test_process_frame_speech_start_to_speech_end_via_silence() {
let config = VadConfig::default()
.with_energy_threshold(2.0)
.with_min_speech_frames(1)
.with_min_silence_frames(2)
.with_zcr_threshold(0.5);
let mut vad = VoiceActivityDetector::new(config);
vad.state = VadState::SpeechStart;
vad.speech_frames = 0;
vad.silence_frames = 0;
let silence_frame = vec![0.0_f32; 480];
let event1 = vad.process_frame(&silence_frame);
assert_eq!(
event1,
VadEvent::Continue,
"First silence should return Continue"
);
assert_eq!(vad.silence_frames, 1);
let event2 = vad.process_frame(&silence_frame);
assert_eq!(
event2,
VadEvent::SpeechEnd,
"Second silence from SpeechStart should trigger SpeechEnd"
);
assert_eq!(vad.state(), VadState::Silence);
}
#[test]
fn test_process_frame_speech_end_to_speech_start_cycle() {
let config = VadConfig::default()
.with_energy_threshold(2.0)
.with_min_speech_frames(3)
.with_min_silence_frames(2)
.with_zcr_threshold(0.5);
let mut vad = VoiceActivityDetector::new(config);
let speech_frame = make_speech_frame(480);
let silence_frame = vec![0.0_f32; 480];
for _ in 0..10 {
let event = vad.process_frame(&speech_frame);
if event == VadEvent::SpeechStart {
break;
}
}
assert_eq!(vad.state(), VadState::Speech);
for _ in 0..5 {
let event = vad.process_frame(&silence_frame);
if event == VadEvent::SpeechEnd {
break;
}
}
assert_eq!(vad.state(), VadState::Silence);
let mut got_second_start = false;
for _ in 0..10 {
let event = vad.process_frame(&speech_frame);
if event == VadEvent::SpeechStart {
got_second_start = true;
break;
}
}
assert!(
got_second_start,
"Should get SpeechStart after SpeechEnd->Silence->Speech cycle"
);
}
#[test]
fn test_process_frame_noise_floor_adapts_only_in_silence() {
let config = VadConfig::default()
.with_energy_threshold(2.0)
.with_min_speech_frames(1)
.with_min_silence_frames(100) .with_smoothing(0.9) .with_zcr_threshold(0.5);
let mut vad = VoiceActivityDetector::new(config);
let initial_noise_floor = vad.noise_floor;
let low_energy_frame: Vec<f32> = (0..480)
.map(|i| {
if (i / 50) % 2 == 0 {
0.01
} else {
-0.01
}
})
.collect();
for _ in 0..20 {
vad.process_frame(&low_energy_frame);
}
assert_ne!(
vad.noise_floor, initial_noise_floor,
"Noise floor should adapt during Silence state"
);
vad.state = VadState::Speech;
let noise_before = vad.noise_floor;
let speech_frame = make_speech_frame(480);
for _ in 0..10 {
vad.process_frame(&speech_frame);
}
assert!(
(vad.noise_floor - noise_before).abs() < f32::EPSILON,
"Noise floor ({}) should not change during Speech state (was {noise_before})",
vad.noise_floor
);
}
#[test]
fn test_process_frame_silence_resets_speech_counter() {
let config = VadConfig::default()
.with_energy_threshold(2.0)
.with_min_speech_frames(5) .with_zcr_threshold(0.5);
let mut vad = VoiceActivityDetector::new(config);
let speech_frame = make_speech_frame(480);
let silence_frame = vec![0.0_f32; 480];
for _ in 0..3 {
let event = vad.process_frame(&speech_frame);
assert_eq!(event, VadEvent::Continue);
}
assert_eq!(vad.speech_frames, 3);
let event = vad.process_frame(&silence_frame);
assert_eq!(event, VadEvent::Continue);
assert_eq!(
vad.speech_frames, 0,
"speech_frames should be reset to 0 after silence in Silence state"
);
assert_eq!(vad.state(), VadState::Silence);
}
#[test]
fn test_process_frame_speech_state_speech_resets_silence_counter() {
let config = VadConfig::default()
.with_energy_threshold(2.0)
.with_min_speech_frames(1)
.with_min_silence_frames(10)
.with_zcr_threshold(0.5);
let mut vad = VoiceActivityDetector::new(config);
vad.state = VadState::Speech;
vad.silence_frames = 5; vad.speech_frames = 0;
let speech_frame = make_speech_frame(480);
let event = vad.process_frame(&speech_frame);
assert_eq!(event, VadEvent::Continue);
assert_eq!(
vad.silence_frames, 0,
"silence_frames should be reset to 0 when speech resumes"
);
assert!(
vad.speech_frames >= 1,
"speech_frames should be incremented"
);
}
#[test]
fn test_is_speech_frame_energy_at_boundary() {
let vad = VoiceActivityDetector::default();
assert!(
!vad.is_speech_frame(0.001_999, 0.15),
"Energy just below threshold should not be speech"
);
assert!(
vad.is_speech_frame(0.002_001, 0.15),
"Energy just above threshold should be speech"
);
}
#[test]
fn test_is_speech_frame_zcr_at_boundaries() {
let vad = VoiceActivityDetector::default();
let good_energy = 0.5;
assert!(
!vad.is_speech_frame(good_energy, 0.05),
"ZCR == 0.05 should not be speech (not > 0.05)"
);
assert!(
vad.is_speech_frame(good_energy, 0.050_01),
"ZCR just above 0.05 should be speech"
);
assert!(
vad.is_speech_frame(good_energy, 0.299),
"ZCR just below zcr_threshold should be speech"
);
assert!(
!vad.is_speech_frame(good_energy, 0.3),
"ZCR == zcr_threshold should not be speech (not < 0.3)"
);
assert!(
!vad.is_speech_frame(good_energy, 0.301),
"ZCR above zcr_threshold should not be speech"
);
}
#[test]
fn test_detect_unterminated_segment_energy_averaging() {
let config = VadConfig::default()
.with_energy_threshold(2.0)
.with_min_speech_frames(1)
.with_min_silence_frames(100)
.with_zcr_threshold(0.5);
let mut vad = VoiceActivityDetector::new(config);
let frame_size = vad.config().frame_size;
let speech_frame = make_speech_frame(frame_size);
let single_frame_energy = VoiceActivityDetector::frame_energy(&speech_frame);
let mut audio = Vec::new();
for _ in 0..5 {
audio.extend_from_slice(&speech_frame);
}
let segments = vad.detect(&audio);
assert_eq!(
segments.len(),
1,
"Should produce exactly one unterminated segment"
);
let seg = &segments[0];
assert!(seg.energy > 0.0, "Segment energy should be positive");
assert!(
!seg.energy.is_nan() && !seg.energy.is_infinite(),
"Segment energy ({}) must be finite",
seg.energy
);
assert!(
(seg.energy - single_frame_energy).abs() < 0.15,
"Average energy ({}) should approximate single frame energy ({single_frame_energy})",
seg.energy
);
}
#[test]
fn test_detect_speech_end_without_active_segment_no_panic() {
let config = VadConfig::default()
.with_energy_threshold(2.0)
.with_min_speech_frames(1)
.with_min_silence_frames(1)
.with_zcr_threshold(0.5);
let mut vad = VoiceActivityDetector::new(config);
let silence = vec![0.0_f32; 4800];
let segments = vad.detect(&silence);
assert!(
segments.is_empty(),
"Pure silence should produce no segments"
);
}
#[test]
fn test_detect_exact_frame_multiple_no_trailing_skip() {
let config = VadConfig::default()
.with_energy_threshold(2.0)
.with_min_speech_frames(2)
.with_min_silence_frames(2)
.with_zcr_threshold(0.5);
let mut vad = VoiceActivityDetector::new(config);
let frame_size = vad.config().frame_size;
let speech_frame = make_speech_frame(frame_size);
let silence_frame = vec![0.0_f32; frame_size];
let mut audio = Vec::new();
for _ in 0..20 {
audio.extend_from_slice(&speech_frame);
}
for _ in 0..10 {
audio.extend_from_slice(&silence_frame);
}
assert_eq!(audio.len() % frame_size, 0);
let segments = vad.detect(&audio);
assert!(
!segments.is_empty(),
"Exact frame multiple audio should still detect speech"
);
}
#[test]
fn test_process_frame_full_state_machine_cycle() {
let config = VadConfig::default()
.with_energy_threshold(2.0)
.with_min_speech_frames(2)
.with_min_silence_frames(2)
.with_zcr_threshold(0.5);
let mut vad = VoiceActivityDetector::new(config);
let speech_frame = make_speech_frame(480);
let silence_frame = vec![0.0_f32; 480];
assert_eq!(vad.state(), VadState::Silence);
let e = vad.process_frame(&silence_frame);
assert_eq!(e, VadEvent::Continue, "Silence in Silence -> Continue");
let e = vad.process_frame(&speech_frame);
assert_eq!(
e,
VadEvent::Continue,
"First speech frame -> Continue (below min)"
);
assert_eq!(vad.speech_frames, 1);
let e = vad.process_frame(&speech_frame);
assert_eq!(
e,
VadEvent::SpeechStart,
"Second speech frame -> SpeechStart"
);
assert_eq!(vad.state(), VadState::Speech);
let e = vad.process_frame(&speech_frame);
assert_eq!(e, VadEvent::Continue, "Speech in Speech -> Continue");
assert_eq!(vad.silence_frames, 0);
let e = vad.process_frame(&silence_frame);
assert_eq!(e, VadEvent::Continue, "First silence in Speech -> Continue");
assert_eq!(vad.silence_frames, 1);
let e = vad.process_frame(&silence_frame);
assert_eq!(
e,
VadEvent::SpeechEnd,
"Second silence in Speech -> SpeechEnd"
);
assert_eq!(vad.state(), VadState::Silence);
let e = vad.process_frame(&speech_frame);
assert_eq!(
e,
VadEvent::Continue,
"First speech after SpeechEnd -> Continue"
);
let e = vad.process_frame(&speech_frame);
assert_eq!(
e,
VadEvent::SpeechStart,
"Second speech after SpeechEnd -> SpeechStart"
);
assert_eq!(vad.state(), VadState::Speech);
}
#[test]
fn test_detect_processes_large_trailing_frame() {
let config = VadConfig::default()
.with_energy_threshold(2.0)
.with_min_speech_frames(2)
.with_min_silence_frames(2)
.with_zcr_threshold(0.5);
let mut vad = VoiceActivityDetector::new(config);
let frame_size = vad.config().frame_size;
let silence_frame = vec![0.0_f32; frame_size];
let mut audio = Vec::new();
audio.extend_from_slice(&silence_frame);
audio.extend_from_slice(&silence_frame);
audio.extend_from_slice(&vec![0.0_f32; 300]);
let segments = vad.detect(&audio);
assert!(
segments.is_empty(),
"Silence audio should produce no segments even with valid trailing frame"
);
}
#[test]
fn test_detect_varying_amplitude_speech_segments() {
let config = VadConfig::default()
.with_energy_threshold(2.0)
.with_min_speech_frames(2)
.with_min_silence_frames(3)
.with_zcr_threshold(0.5);
let mut vad = VoiceActivityDetector::new(config);
let frame_size = vad.config().frame_size;
let loud_speech = make_speech_frame(frame_size); let silence_frame = vec![0.0_f32; frame_size];
let quiet_frame: Vec<f32> = (0..frame_size)
.map(|i| if (i / 10) % 2 == 0 { 0.001 } else { -0.001 })
.collect();
let mut audio = Vec::new();
for _ in 0..10 {
audio.extend_from_slice(&loud_speech);
}
for _ in 0..10 {
audio.extend_from_slice(&silence_frame);
}
for _ in 0..10 {
audio.extend_from_slice(&quiet_frame);
}
for _ in 0..5 {
audio.extend_from_slice(&silence_frame);
}
let segments = vad.detect(&audio);
assert!(
!segments.is_empty(),
"Should detect the loud speech segment"
);
for seg in &segments {
assert!(
seg.start < 0.5,
"Detected speech should be in the first half (loud speech), got start={}",
seg.start
);
}
}
fn generate_speech_frame(frame_size: usize, amplitude: f32) -> Vec<f32> {
let freq = 1000.0f32;
let sample_rate = 16000.0f32;
(0..frame_size)
.map(|i| amplitude * (2.0 * std::f32::consts::PI * freq * i as f32 / sample_rate).sin())
.collect()
}
#[test]
fn test_streaming_vad_speech_start_triggered() {
let config = VadConfig::default().with_min_speech_frames(1);
let mut svad = StreamingVad::new(config);
let silence = vec![0.0f32; 480 * 5];
let (_, in_speech) = svad.process(&silence);
assert!(!in_speech, "should not be in speech during silence");
let speech = generate_speech_frame(480 * 3, 0.5);
let (_, in_speech) = svad.process(&speech);
assert!(in_speech, "should be in speech after speech audio");
}
#[test]
fn test_streaming_vad_speech_start_and_end() {
let config = VadConfig::default()
.with_min_speech_frames(1)
.with_min_silence_frames(2);
let mut svad = StreamingVad::new(config);
let silence = vec![0.0f32; 480 * 5];
svad.process(&silence);
let speech = generate_speech_frame(480 * 3, 0.5);
let (_, in_speech) = svad.process(&speech);
assert!(in_speech, "should be in speech");
let silence_after = vec![0.0f32; 480 * 5];
let (completed, in_speech) = svad.process(&silence_after);
assert!(!in_speech, "should have exited speech");
assert!(
!completed.is_empty(),
"should have returned completed speech buffer"
);
}
#[test]
fn test_streaming_vad_speech_start_accumulates_buffer() {
let config = VadConfig::default().with_min_speech_frames(1);
let mut svad = StreamingVad::new(config);
let silence = vec![0.0f32; 480 * 5];
svad.process(&silence);
let speech = generate_speech_frame(480 * 2, 0.5);
svad.process(&speech);
assert!(svad.is_in_speech(), "should be in speech");
assert!(
!svad.speech_buffer.is_empty(),
"speech_buffer should contain audio after SpeechStart"
);
}
#[test]
fn test_streaming_vad_speech_start_clears_previous_buffer() {
let config = VadConfig::default().with_min_speech_frames(1);
let mut svad = StreamingVad::new(config);
svad.speech_buffer = vec![0.1, 0.2, 0.3];
let silence = vec![0.0f32; 480 * 5];
svad.process(&silence);
let speech = generate_speech_frame(480 * 2, 0.5);
svad.process(&speech);
assert!(svad.is_in_speech());
if svad.speech_buffer.len() >= 3 {
let has_old = svad.speech_buffer[0..3] == [0.1f32, 0.2, 0.3];
assert!(!has_old, "old buffer data should have been cleared");
}
}
#[test]
fn test_streaming_vad_full_cycle_start_continue_end() {
let config = VadConfig::default()
.with_min_speech_frames(1)
.with_min_silence_frames(2);
let mut svad = StreamingVad::new(config);
svad.process(&vec![0.0f32; 480 * 5]);
assert!(!svad.is_in_speech());
let speech = generate_speech_frame(480, 0.5);
svad.process(&speech);
assert!(svad.is_in_speech(), "SpeechStart should activate");
let more_speech = generate_speech_frame(480 * 3, 0.5);
svad.process(&more_speech);
assert!(svad.is_in_speech(), "should still be in speech");
let buffer_len = svad.speech_buffer.len();
assert!(
buffer_len > 480,
"speech buffer should have accumulated frames, got {buffer_len}"
);
let silence = vec![0.0f32; 480 * 5];
let (completed, in_speech) = svad.process(&silence);
assert!(!in_speech, "should have ended speech");
assert!(!completed.is_empty(), "should return accumulated speech");
}