1use std::error::Error;
8use std::ffi::c_void;
9use std::fmt;
10use std::ptr::NonNull;
11
12pub const SAMPLE_RATE: usize = 16_000;
14
15pub const DEFAULT_CONTEXT_SAMPLES: usize = 64;
17
18pub const DEFAULT_CHUNK_SAMPLES: usize = 512;
20
21pub const DEFAULT_INPUT_SAMPLES: usize = DEFAULT_CONTEXT_SAMPLES + DEFAULT_CHUNK_SAMPLES;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct SpeechTimestamp {
29 pub start: usize,
31 pub end: usize,
33}
34
35impl SpeechTimestamp {
36 pub fn to_seconds(self, sampling_rate: usize) -> SpeechTimestampSeconds {
42 assert!(sampling_rate > 0, "sampling_rate must be greater than zero");
43
44 SpeechTimestampSeconds {
45 start: self.start as f32 / sampling_rate as f32,
46 end: self.end as f32 / sampling_rate as f32,
47 }
48 }
49}
50
51#[derive(Debug, Clone, Copy, PartialEq)]
53pub struct SpeechTimestampSeconds {
54 pub start: f32,
56 pub end: f32,
58}
59
60#[derive(Debug, Clone, Copy)]
62pub struct TimestampConfig {
63 pub sampling_rate: usize,
65 pub threshold: f32,
67 pub min_speech_duration_ms: usize,
69 pub max_speech_duration_s: f32,
71 pub min_silence_duration_ms: usize,
73 pub speech_pad_ms: usize,
75 pub neg_threshold: Option<f32>,
77 pub window_size_samples: usize,
79 pub min_silence_at_max_speech_ms: usize,
81 pub use_max_possible_silence_at_max_speech: bool,
83}
84
85impl Default for TimestampConfig {
86 fn default() -> Self {
87 Self {
88 sampling_rate: SAMPLE_RATE,
89 threshold: 0.5,
90 min_speech_duration_ms: 250,
91 max_speech_duration_s: f32::INFINITY,
92 min_silence_duration_ms: 100,
93 speech_pad_ms: 30,
94 neg_threshold: None,
95 window_size_samples: DEFAULT_CHUNK_SAMPLES,
96 min_silence_at_max_speech_ms: 98,
97 use_max_possible_silence_at_max_speech: true,
98 }
99 }
100}
101
102#[derive(Debug, Clone, PartialEq, Eq)]
104pub enum SileroVadError {
105 CreateFailed,
107 InvalidInputLength {
109 expected: usize,
111 actual: usize,
113 },
114 InvalidContextLength {
116 context_samples: usize,
118 input_samples: usize,
120 },
121 NativeStatus(SileroVadStatus),
123 UnsupportedContextChunkSampleRate {
125 sampling_rate: usize,
127 },
128}
129
130impl fmt::Display for SileroVadError {
131 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
132 match self {
133 Self::CreateFailed => write!(formatter, "failed to create the native Silero VAD model"),
134 Self::InvalidInputLength { expected, actual } => {
135 write!(formatter, "expected {expected} audio samples, got {actual}")
136 }
137 Self::InvalidContextLength {
138 context_samples,
139 input_samples,
140 } => write!(
141 formatter,
142 "context length {context_samples} is invalid for model input length {input_samples}"
143 ),
144 Self::NativeStatus(status) => write!(formatter, "native Silero VAD error: {status:?}"),
145 Self::UnsupportedContextChunkSampleRate { sampling_rate } => write!(
146 formatter,
147 "context-bearing chunk inference expects {SAMPLE_RATE} Hz input, got {sampling_rate} Hz"
148 ),
149 }
150 }
151}
152
153impl Error for SileroVadError {}
154
155#[repr(i32)]
157#[derive(Debug, Copy, Clone, PartialEq, Eq)]
158pub enum SileroVadStatus {
159 Ok = 0,
161 InvalidArgument = 1,
163 AllocationFailed = 2,
165 InvalidShape = 3,
167 Unknown = -1,
169}
170
171impl SileroVadStatus {
172 fn from_raw(value: i32) -> Self {
173 match value {
174 0 => Self::Ok,
175 1 => Self::InvalidArgument,
176 2 => Self::AllocationFailed,
177 3 => Self::InvalidShape,
178 _ => Self::Unknown,
179 }
180 }
181
182 fn into_result(value: i32) -> Result<(), SileroVadError> {
183 match Self::from_raw(value) {
184 Self::Ok => Ok(()),
185 status => Err(SileroVadError::NativeStatus(status)),
186 }
187 }
188}
189
190pub struct SileroVad {
196 model: NonNull<c_void>,
197 input_samples: usize,
198 sampling_rate: usize,
199 source_window_samples: usize,
200 context: Vec<f32>,
201 pending_samples: Vec<f32>,
202}
203
204impl SileroVad {
205 pub fn new() -> Result<Self, SileroVadError> {
209 Self::with_sample_rate(SAMPLE_RATE)
210 }
211
212 pub fn with_sample_rate(sampling_rate: usize) -> Result<Self, SileroVadError> {
217 Self::with_input_samples_and_sample_rate(DEFAULT_INPUT_SAMPLES, sampling_rate)
218 }
219
220 pub fn with_input_samples(input_samples: usize) -> Result<Self, SileroVadError> {
225 Self::with_input_samples_and_sample_rate(input_samples, SAMPLE_RATE)
226 }
227
228 pub fn with_input_samples_and_sample_rate(
230 input_samples: usize,
231 sampling_rate: usize,
232 ) -> Result<Self, SileroVadError> {
233 let weights = unsafe { ffi::silero_vad_get_embedded_weights() };
234 let model = unsafe {
235 ffi::silero_vad_model_create_with_sample_rate(weights, input_samples, sampling_rate)
236 };
237 let model = NonNull::new(model).ok_or(SileroVadError::CreateFailed)?;
238 let source_window_samples =
239 unsafe { ffi::silero_vad_model_get_source_window_samples(model.as_ptr()) };
240
241 Ok(Self {
242 model,
243 input_samples,
244 sampling_rate,
245 source_window_samples,
246 context: Vec::new(),
247 pending_samples: Vec::new(),
248 })
249 }
250
251 pub fn input_samples(&self) -> usize {
253 self.input_samples
254 }
255
256 pub fn sampling_rate(&self) -> usize {
258 self.sampling_rate
259 }
260
261 pub fn source_window_samples(&self) -> usize {
265 self.source_window_samples
266 }
267
268 pub fn reset(&mut self) {
270 unsafe {
271 ffi::silero_vad_model_reset(self.model.as_ptr());
272 }
273 self.context.clear();
274 self.pending_samples.clear();
275 }
276
277 pub fn forward_chunk_with_context(&mut self, chunk: &[f32]) -> Result<f32, SileroVadError> {
283 if self.sampling_rate != SAMPLE_RATE {
284 return Err(SileroVadError::UnsupportedContextChunkSampleRate {
285 sampling_rate: self.sampling_rate,
286 });
287 }
288
289 if chunk.len() != self.input_samples {
290 return Err(SileroVadError::InvalidInputLength {
291 expected: self.input_samples,
292 actual: chunk.len(),
293 });
294 }
295
296 let mut speech_probability = 0.0_f32;
297 let status = unsafe {
298 ffi::silero_vad_model_forward(
299 self.model.as_ptr(),
300 chunk.as_ptr(),
301 &mut speech_probability,
302 )
303 };
304 SileroVadStatus::into_result(status)?;
305 Ok(speech_probability)
306 }
307
308 pub fn forward_chunk(&mut self, chunk: &[f32]) -> Result<f32, SileroVadError> {
314 if chunk.len() != self.source_window_samples {
315 return Err(SileroVadError::InvalidInputLength {
316 expected: self.source_window_samples,
317 actual: chunk.len(),
318 });
319 }
320
321 let mut speech_probability = 0.0_f32;
322 let status = unsafe {
323 ffi::silero_vad_model_forward_source_chunk(
324 self.model.as_ptr(),
325 chunk.as_ptr(),
326 chunk.len(),
327 &mut speech_probability,
328 )
329 };
330 SileroVadStatus::into_result(status)?;
331 Ok(speech_probability)
332 }
333
334 pub fn push(&mut self, samples: &[f32]) -> Result<Vec<f32>, SileroVadError> {
341 if samples.is_empty() {
342 return Ok(Vec::new());
343 }
344
345 let mut merged = Vec::with_capacity(self.pending_samples.len() + samples.len());
346 merged.extend_from_slice(&self.pending_samples);
347 merged.extend_from_slice(samples);
348
349 let mut probabilities = Vec::new();
350 let mut offset = 0;
351
352 while offset + self.source_window_samples <= merged.len() {
353 probabilities
354 .push(self.forward_chunk(&merged[offset..offset + self.source_window_samples])?);
355 offset += self.source_window_samples;
356 }
357
358 self.pending_samples.clear();
359 self.pending_samples.extend_from_slice(&merged[offset..]);
360 Ok(probabilities)
361 }
362
363 pub fn forward_chunk_with_rolling_context(
365 &mut self,
366 chunk: &[f32],
367 context_samples: usize,
368 ) -> Result<f32, SileroVadError> {
369 if self.sampling_rate != SAMPLE_RATE {
370 return Err(SileroVadError::UnsupportedContextChunkSampleRate {
371 sampling_rate: self.sampling_rate,
372 });
373 }
374
375 if context_samples > self.input_samples {
376 return Err(SileroVadError::InvalidContextLength {
377 context_samples,
378 input_samples: self.input_samples,
379 });
380 }
381
382 let expected = self.input_samples - context_samples;
383 if chunk.len() != expected {
384 return Err(SileroVadError::InvalidInputLength {
385 expected,
386 actual: chunk.len(),
387 });
388 }
389
390 if self.context.len() != context_samples {
391 self.context = vec![0.0; context_samples];
392 }
393
394 let mut native_input = Vec::with_capacity(self.input_samples);
395 native_input.extend_from_slice(&self.context);
396 native_input.extend_from_slice(chunk);
397
398 self.context.clear();
399 self.context
400 .extend_from_slice(&native_input[native_input.len() - context_samples..]);
401
402 self.forward_chunk_with_context(&native_input)
403 }
404
405 pub fn forward_audio(&mut self, audio: &[f32]) -> Result<Vec<f32>, SileroVadError> {
410 let capacity = audio_probability_count_for_sample_rate(audio.len(), self.sampling_rate);
411 let mut probabilities = vec![0.0_f32; capacity];
412 let mut written = 0_usize;
413
414 let status = unsafe {
415 ffi::silero_vad_model_forward_audio(
416 self.model.as_ptr(),
417 audio.as_ptr(),
418 audio.len(),
419 probabilities.as_mut_ptr(),
420 probabilities.len(),
421 &mut written,
422 )
423 };
424 SileroVadStatus::into_result(status)?;
425 probabilities.truncate(written);
426 self.context.clear();
427 self.pending_samples.clear();
428 Ok(probabilities)
429 }
430}
431
432impl Drop for SileroVad {
433 fn drop(&mut self) {
434 unsafe {
435 ffi::silero_vad_model_destroy(self.model.as_ptr());
436 }
437 }
438}
439
440pub fn audio_probability_count(audio_samples: usize) -> usize {
442 unsafe { ffi::silero_vad_model_audio_prob_count(audio_samples) }
443}
444
445pub fn audio_probability_count_for_sample_rate(
447 audio_samples: usize,
448 sampling_rate: usize,
449) -> usize {
450 unsafe { ffi::silero_vad_model_audio_prob_count_for_sample_rate(audio_samples, sampling_rate) }
451}
452
453pub fn source_window_samples_for_sample_rate(sampling_rate: usize) -> usize {
455 unsafe { ffi::silero_vad_model_source_window_samples(sampling_rate) }
456}
457
458pub fn get_timestamps_from_probs(
465 speech_probs: &[f32],
466 audio_length_samples: usize,
467) -> Vec<SpeechTimestamp> {
468 get_timestamps_from_probs_with_config(
469 speech_probs,
470 audio_length_samples,
471 TimestampConfig::default(),
472 )
473}
474
475pub fn get_timestamps_from_probs_seconds(
480 speech_probs: &[f32],
481 audio_length_samples: usize,
482) -> Vec<SpeechTimestampSeconds> {
483 get_timestamps_from_probs(speech_probs, audio_length_samples)
484 .into_iter()
485 .map(|timestamp| timestamp.to_seconds(SAMPLE_RATE))
486 .collect()
487}
488
489pub fn get_timestamps_from_probs_with_config(
491 speech_probs: &[f32],
492 audio_length_samples: usize,
493 config: TimestampConfig,
494) -> Vec<SpeechTimestamp> {
495 if speech_probs.is_empty() || config.window_size_samples == 0 || config.sampling_rate == 0 {
496 return Vec::new();
497 }
498
499 let threshold = config.threshold;
500 let neg_threshold = config
501 .neg_threshold
502 .unwrap_or_else(|| (threshold - 0.15).max(0.01));
503 let sampling_rate = config.sampling_rate as f64;
504 let min_speech_samples = sampling_rate * config.min_speech_duration_ms as f64 / 1000.0;
505 let speech_pad_samples = sampling_rate * config.speech_pad_ms as f64 / 1000.0;
506 let max_speech_samples = sampling_rate * config.max_speech_duration_s as f64
507 - config.window_size_samples as f64
508 - 2.0 * speech_pad_samples;
509 let min_silence_samples = sampling_rate * config.min_silence_duration_ms as f64 / 1000.0;
510 let min_silence_samples_at_max_speech =
511 sampling_rate * config.min_silence_at_max_speech_ms as f64 / 1000.0;
512
513 let mut triggered = false;
514 let mut speeches = Vec::new();
515 let mut current_speech: Option<SpeechTimestamp> = None;
516 let mut temp_end = 0_usize;
517 let mut prev_end = 0_usize;
518 let mut next_start = 0_usize;
519 let mut possible_ends: Vec<(usize, usize)> = Vec::new();
520
521 for (index, speech_prob) in speech_probs.iter().copied().enumerate() {
522 let cur_sample = config.window_size_samples * index;
523
524 if speech_prob >= threshold && temp_end != 0 {
525 let silence_duration = cur_sample.saturating_sub(temp_end);
526 if silence_duration as f64 > min_silence_samples_at_max_speech {
527 possible_ends.push((temp_end, silence_duration));
528 }
529 temp_end = 0;
530 if next_start < prev_end {
531 next_start = cur_sample;
532 }
533 }
534
535 if speech_prob >= threshold && !triggered {
536 triggered = true;
537 current_speech = Some(SpeechTimestamp {
538 start: cur_sample,
539 end: 0,
540 });
541 continue;
542 }
543
544 if triggered
545 && current_speech.is_some_and(|speech| {
546 cur_sample.saturating_sub(speech.start) as f64 > max_speech_samples
547 })
548 {
549 if config.use_max_possible_silence_at_max_speech && !possible_ends.is_empty() {
550 let (best_end, best_duration) = possible_ends
551 .iter()
552 .copied()
553 .max_by_key(|(_, duration)| *duration)
554 .expect("possible_ends is not empty");
555 prev_end = best_end;
556
557 if let Some(mut speech) = current_speech.take() {
558 speech.end = prev_end;
559 speeches.push(speech);
560 }
561
562 next_start = prev_end + best_duration;
563 if next_start < prev_end + cur_sample {
564 current_speech = Some(SpeechTimestamp {
565 start: next_start,
566 end: 0,
567 });
568 } else {
569 triggered = false;
570 }
571
572 prev_end = 0;
573 next_start = 0;
574 temp_end = 0;
575 possible_ends.clear();
576 } else if prev_end != 0 {
577 if let Some(mut speech) = current_speech.take() {
578 speech.end = prev_end;
579 speeches.push(speech);
580 }
581
582 if next_start < prev_end {
583 triggered = false;
584 } else {
585 current_speech = Some(SpeechTimestamp {
586 start: next_start,
587 end: 0,
588 });
589 }
590
591 prev_end = 0;
592 next_start = 0;
593 temp_end = 0;
594 possible_ends.clear();
595 } else {
596 if let Some(mut speech) = current_speech.take() {
597 speech.end = cur_sample;
598 speeches.push(speech);
599 }
600
601 prev_end = 0;
602 next_start = 0;
603 temp_end = 0;
604 triggered = false;
605 possible_ends.clear();
606 continue;
607 }
608 }
609
610 if speech_prob < neg_threshold && triggered {
611 if temp_end == 0 {
612 temp_end = cur_sample;
613 }
614 let silence_duration_now = cur_sample.saturating_sub(temp_end);
615
616 if !config.use_max_possible_silence_at_max_speech
617 && silence_duration_now as f64 > min_silence_samples_at_max_speech
618 {
619 prev_end = temp_end;
620 }
621
622 if (silence_duration_now as f64) < min_silence_samples {
623 continue;
624 }
625
626 if let Some(mut speech) = current_speech.take() {
627 speech.end = temp_end;
628 if speech.end.saturating_sub(speech.start) as f64 > min_speech_samples {
629 speeches.push(speech);
630 }
631 }
632
633 prev_end = 0;
634 next_start = 0;
635 temp_end = 0;
636 triggered = false;
637 possible_ends.clear();
638 }
639 }
640
641 if let Some(mut speech) = current_speech {
642 if audio_length_samples.saturating_sub(speech.start) as f64 > min_speech_samples {
643 speech.end = audio_length_samples;
644 speeches.push(speech);
645 }
646 }
647
648 apply_speech_padding(&mut speeches, audio_length_samples, speech_pad_samples);
649 speeches
650}
651
652pub fn get_timestamps_from_probs_seconds_with_config(
657 speech_probs: &[f32],
658 audio_length_samples: usize,
659 config: TimestampConfig,
660) -> Vec<SpeechTimestampSeconds> {
661 get_timestamps_from_probs_with_config(speech_probs, audio_length_samples, config)
662 .into_iter()
663 .map(|timestamp| timestamp.to_seconds(config.sampling_rate))
664 .collect()
665}
666
667fn apply_speech_padding(
668 speeches: &mut [SpeechTimestamp],
669 audio_length_samples: usize,
670 speech_pad_samples: f64,
671) {
672 let speech_pad_samples = speech_pad_samples as usize;
673
674 for index in 0..speeches.len() {
675 if index == 0 {
676 speeches[index].start = speeches[index].start.saturating_sub(speech_pad_samples);
677 }
678
679 if index != speeches.len() - 1 {
680 let silence_duration = speeches[index + 1]
681 .start
682 .saturating_sub(speeches[index].end);
683
684 if silence_duration < 2 * speech_pad_samples {
685 let half_silence = silence_duration / 2;
686 speeches[index].end += half_silence;
687 speeches[index + 1].start = speeches[index + 1].start.saturating_sub(half_silence);
688 } else {
689 speeches[index].end =
690 audio_length_samples.min(speeches[index].end + speech_pad_samples);
691 speeches[index + 1].start =
692 speeches[index + 1].start.saturating_sub(speech_pad_samples);
693 }
694 } else {
695 speeches[index].end =
696 audio_length_samples.min(speeches[index].end + speech_pad_samples);
697 }
698 }
699}
700
701mod ffi {
702 use std::ffi::c_void;
703
704 unsafe extern "C" {
705 pub fn silero_vad_get_embedded_weights() -> *const c_void;
706
707 pub fn silero_vad_model_create_with_sample_rate(
708 weights: *const c_void,
709 input_samples: usize,
710 sampling_rate: usize,
711 ) -> *mut c_void;
712
713 pub fn silero_vad_model_reset(model: *mut c_void);
714
715 pub fn silero_vad_model_destroy(model: *mut c_void);
716
717 pub fn silero_vad_model_forward(
718 model: *mut c_void,
719 input: *const f32,
720 speech_probability: *mut f32,
721 ) -> i32;
722
723 pub fn silero_vad_model_forward_source_chunk(
724 model: *mut c_void,
725 input: *const f32,
726 input_samples: usize,
727 speech_probability: *mut f32,
728 ) -> i32;
729
730 pub fn silero_vad_model_audio_prob_count(audio_samples: usize) -> usize;
731
732 pub fn silero_vad_model_audio_prob_count_for_sample_rate(
733 audio_samples: usize,
734 sampling_rate: usize,
735 ) -> usize;
736
737 pub fn silero_vad_model_source_window_samples(sampling_rate: usize) -> usize;
738
739 pub fn silero_vad_model_get_source_window_samples(model: *const c_void) -> usize;
740
741 pub fn silero_vad_model_forward_audio(
742 model: *mut c_void,
743 audio: *const f32,
744 audio_samples: usize,
745 speech_probabilities: *mut f32,
746 speech_probabilities_capacity: usize,
747 speech_probabilities_written: *mut usize,
748 ) -> i32;
749 }
750}
751
752#[cfg(test)]
753mod tests {
754 use super::*;
755
756 #[test]
757 fn reports_probability_count_for_empty_audio() {
758 assert_eq!(audio_probability_count(0), 0);
759 }
760
761 #[test]
762 fn reports_source_window_samples_for_higher_sample_rate() {
763 assert_eq!(source_window_samples_for_sample_rate(16_000), 512);
764 assert_eq!(source_window_samples_for_sample_rate(48_000), 1536);
765 }
766
767 #[test]
768 fn converts_sample_timestamps_to_seconds() {
769 let timestamp = SpeechTimestamp {
770 start: 16_000,
771 end: 24_000,
772 };
773
774 assert_eq!(
775 timestamp.to_seconds(16_000),
776 SpeechTimestampSeconds {
777 start: 1.0,
778 end: 1.5,
779 }
780 );
781 }
782
783 #[test]
784 fn returns_timestamps_in_seconds_with_custom_sample_rate() {
785 let probabilities = [0.0, 0.9, 0.9, 0.0, 0.0, 0.0];
786 let timestamps = get_timestamps_from_probs_seconds_with_config(
787 &probabilities,
788 6_000,
789 TimestampConfig {
790 sampling_rate: 48_000,
791 threshold: 0.5,
792 min_speech_duration_ms: 0,
793 min_silence_duration_ms: 0,
794 speech_pad_ms: 0,
795 window_size_samples: 1_000,
796 ..Default::default()
797 },
798 );
799
800 assert_eq!(
801 timestamps,
802 vec![SpeechTimestampSeconds {
803 start: 1_000.0 / 48_000.0,
804 end: 3_000.0 / 48_000.0,
805 }]
806 );
807 }
808
809 #[test]
810 fn forwards_full_audio_with_resampling() {
811 let mut vad = SileroVad::with_sample_rate(48_000).unwrap();
812 let probabilities = vad.forward_audio(&vec![0.0; 48_000]).unwrap();
813
814 assert_eq!(vad.sampling_rate(), 48_000);
815 assert_eq!(vad.source_window_samples(), 1536);
816 assert_eq!(
817 probabilities.len(),
818 audio_probability_count_for_sample_rate(48_000, 48_000)
819 );
820 assert!(probabilities
821 .iter()
822 .all(|probability| probability.is_finite()));
823 }
824
825 #[test]
826 fn forwards_streaming_chunks_with_resampling() {
827 let mut vad = SileroVad::with_sample_rate(48_000).unwrap();
828 let probability = vad
829 .forward_chunk(&vec![0.0; vad.source_window_samples()])
830 .unwrap();
831
832 assert!(probability.is_finite());
833 }
834
835 #[test]
836 fn push_buffers_arbitrary_chunk_sizes() {
837 let mut vad = SileroVad::new().unwrap();
838
839 let first = vad
840 .push(&vec![0.0; DEFAULT_CHUNK_SAMPLES.div_ceil(2)])
841 .unwrap();
842 let second = vad.push(&vec![0.0; 2 * DEFAULT_CHUNK_SAMPLES]).unwrap();
843
844 assert!(first.is_empty());
845 assert_eq!(second.len(), 2);
846 assert!(second.iter().all(|probability| probability.is_finite()));
847 }
848
849 #[test]
850 fn push_uses_source_window_size_for_resampled_streams() {
851 let mut vad = SileroVad::with_sample_rate(48_000).unwrap();
852 let chunk_len = vad.source_window_samples();
853
854 let first = vad.push(&vec![0.0; chunk_len - 1]).unwrap();
855 let second = vad.push(&[0.0]).unwrap();
856
857 assert!(first.is_empty());
858 assert_eq!(second.len(), 1);
859 assert!(second[0].is_finite());
860 }
861
862 #[test]
863 fn can_run_silence_through_static_c_model() {
864 let mut vad = SileroVad::new().unwrap();
865 let probabilities = vad.forward_audio(&vec![0.0; SAMPLE_RATE]).unwrap();
866
867 assert!(!probabilities.is_empty());
868 assert!(probabilities
869 .iter()
870 .all(|probability| probability.is_finite()));
871 }
872}