aria_kit/voice_activity/
endpointer.rs1use crate::audio::{SilenceDuration, canonical_samples};
2
3use super::{SPEECH_THRESHOLD, VoiceActivityDetector, VoiceActivityError};
4
5const SPEECH_END_THRESHOLD: f32 = 0.35;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum Boundary {
14 Start(u64),
16 End(u64),
19}
20
21pub struct Endpointer<V> {
53 vad: V,
54 trailing_silence_samples: u64,
55 frame: Vec<i16>,
56 scored_samples: u64,
57 speaking: bool,
58 silence_since: Option<u64>,
59}
60
61impl<V: VoiceActivityDetector> Endpointer<V> {
62 pub fn new(mut vad: V, trailing_silence: SilenceDuration) -> Self {
64 const { assert!(V::FRAME_SAMPLES > 0) };
66 vad.reset();
67 Self {
68 vad,
69 trailing_silence_samples: canonical_samples(trailing_silence.get()),
70 frame: Vec::with_capacity(V::FRAME_SAMPLES),
71 scored_samples: 0,
72 speaking: false,
73 silence_since: None,
74 }
75 }
76
77 pub fn push(&mut self, mut samples: &[i16]) -> Result<Vec<Boundary>, VoiceActivityError> {
79 let mut boundaries = Vec::new();
80 while !samples.is_empty() {
81 let copied = (V::FRAME_SAMPLES - self.frame.len()).min(samples.len());
82 self.frame.extend_from_slice(&samples[..copied]);
83 samples = &samples[copied..];
84 if self.frame.len() == V::FRAME_SAMPLES {
85 boundaries.extend(self.score_frame()?);
86 self.frame.clear();
87 }
88 }
89 Ok(boundaries)
90 }
91
92 fn score_frame(&mut self) -> Result<Option<Boundary>, VoiceActivityError> {
93 let probability = self.vad.probability(&self.frame)?;
94 let frame_start = self.scored_samples;
97 self.scored_samples += V::FRAME_SAMPLES as u64;
98
99 if !self.speaking {
100 self.speaking = probability >= SPEECH_THRESHOLD;
101 return Ok(self.speaking.then_some(Boundary::Start(frame_start)));
102 }
103 if probability >= SPEECH_THRESHOLD {
104 self.silence_since = None;
105 } else if probability <= SPEECH_END_THRESHOLD && self.silence_since.is_none() {
106 self.silence_since = Some(frame_start);
107 }
108 let silence = self
109 .silence_since
110 .map_or(0, |since| self.scored_samples - since);
111 if silence < self.trailing_silence_samples {
112 return Ok(None);
113 }
114 self.speaking = false;
115 self.silence_since = None;
116 Ok(Some(Boundary::End(self.scored_samples)))
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use std::time::Duration;
123
124 use super::*;
125
126 struct Scripted {
127 scores: Vec<f32>,
128 calls: usize,
129 resets: usize,
130 }
131
132 impl VoiceActivityDetector for Scripted {
133 const FRAME_SAMPLES: usize = 160;
134
135 fn reset(&mut self) {
136 self.resets += 1;
137 }
138
139 fn probability(&mut self, _samples: &[i16]) -> Result<f32, VoiceActivityError> {
140 let score = self.scores[self.calls];
141 self.calls += 1;
142 Ok(score)
143 }
144 }
145
146 fn scripted(scores: &[f32]) -> Scripted {
147 Scripted {
148 scores: scores.to_vec(),
149 calls: 0,
150 resets: 0,
151 }
152 }
153
154 const FRAME: u64 = Scripted::FRAME_SAMPLES as u64;
155
156 fn ten_frames_of_silence() -> SilenceDuration {
157 SilenceDuration::new(Duration::from_millis(100)).expect("the duration is in range")
158 }
159
160 fn boundaries(scores: &[f32], batch: usize) -> Vec<Boundary> {
161 let mut endpointer = Endpointer::new(scripted(scores), ten_frames_of_silence());
162 let samples = vec![0; scores.len() * Scripted::FRAME_SAMPLES];
163 let mut found = Vec::new();
164 for piece in samples.chunks(batch) {
165 found.extend(endpointer.push(piece).expect("the scripted VAD scores"));
166 }
167 found
168 }
169
170 #[test]
171 fn consecutive_utterances_each_get_a_start_and_an_end() {
172 let mut scores = vec![0.0, 0.9, 0.9];
173 scores.extend([0.0; 10]);
174 scores.extend([0.9, 0.0]);
175 let ten_frames = 10 * FRAME;
176
177 for batch in [1, 100, 160, 1_024] {
178 assert_eq!(
179 boundaries(&scores, batch),
180 [
181 Boundary::Start(FRAME),
182 Boundary::End(3 * FRAME + ten_frames),
183 Boundary::Start(13 * FRAME),
184 ],
185 "batches of {batch} samples"
186 );
187 }
188 }
189
190 #[test]
191 fn a_score_between_the_thresholds_does_not_end_the_utterance() {
192 let mut scores = vec![0.9];
193 scores.extend([0.4; 20]);
194
195 assert_eq!(boundaries(&scores, 160), [Boundary::Start(0)]);
196 }
197
198 #[test]
199 fn a_new_endpointer_resets_its_detector() {
200 let mut vad = scripted(&[]);
201
202 Endpointer::new(&mut vad, ten_frames_of_silence());
203
204 assert_eq!(vad.resets, 1);
205 }
206}