1use crate::audio::{AudioInput, WHISPER_SAMPLE_RATE};
10use crate::error::{Result, UserError};
11use std::sync::Arc;
12
13pub const DEFAULT_DICTATION_MAX_SECS: f64 = 60.0;
16
17#[derive(Debug, Clone)]
22pub struct PcmBuffer {
23 storage: Vec<f32>,
25 head: usize,
28 len: usize,
30 max_samples: usize,
31 rolling: bool,
34 clamped_count: u64,
36}
37
38impl PcmBuffer {
39 pub fn with_max_secs(max_secs: f64) -> Self {
41 let max_samples = (max_secs.max(0.0) * f64::from(WHISPER_SAMPLE_RATE)).round() as usize;
42 let max_samples = max_samples.max(1);
43 Self {
44 storage: vec![0.0; max_samples],
45 head: 0,
46 len: 0,
47 max_samples,
48 rolling: true,
49 clamped_count: 0,
50 }
51 }
52
53 pub fn dictation() -> Self {
55 Self::with_max_secs(DEFAULT_DICTATION_MAX_SECS)
56 }
57
58 pub fn bounded(max_secs: f64) -> Self {
60 let mut b = Self::with_max_secs(max_secs);
61 b.rolling = false;
62 b
63 }
64
65 pub fn clear(&mut self) {
66 self.head = 0;
67 self.len = 0;
68 }
70
71 pub fn len(&self) -> usize {
72 self.len
73 }
74
75 pub fn is_empty(&self) -> bool {
76 self.len == 0
77 }
78
79 pub fn max_samples(&self) -> usize {
80 self.max_samples
81 }
82
83 pub fn is_rolling(&self) -> bool {
84 self.rolling
85 }
86
87 pub fn duration_secs(&self) -> f64 {
88 self.len as f64 / f64::from(WHISPER_SAMPLE_RATE)
89 }
90
91 pub fn clamped_count(&self) -> u64 {
93 self.clamped_count
94 }
95
96 pub fn samples(&self) -> ContiguousSamples<'_> {
101 if self.len == 0 {
102 return ContiguousSamples::Borrowed(&[]);
103 }
104 let cap = self.max_samples;
105 let first = self.head % cap;
106 if first + self.len <= cap {
107 ContiguousSamples::Borrowed(&self.storage[first..first + self.len])
108 } else {
109 let mut out = Vec::with_capacity(self.len);
110 let n1 = cap - first;
111 out.extend_from_slice(&self.storage[first..]);
112 out.extend_from_slice(&self.storage[..self.len - n1]);
113 ContiguousSamples::Owned(out)
114 }
115 }
116
117 pub fn copy_samples_into(&self, dst: &mut Vec<f32>) {
119 dst.clear();
120 if self.len == 0 {
121 return;
122 }
123 dst.reserve(self.len);
124 let cap = self.max_samples;
125 let first = self.head % cap;
126 if first + self.len <= cap {
127 dst.extend_from_slice(&self.storage[first..first + self.len]);
128 } else {
129 let n1 = cap - first;
130 dst.extend_from_slice(&self.storage[first..]);
131 dst.extend_from_slice(&self.storage[..self.len - n1]);
132 }
133 }
134
135 pub fn push(&mut self, chunk: &[f32]) -> Result<()> {
141 if chunk.is_empty() {
142 return Ok(());
143 }
144 validate_finite(chunk)?;
145
146 if !self.rolling {
147 if self.len.saturating_add(chunk.len()) > self.max_samples {
148 return Err(UserError::AudioTooLong {
149 duration_secs: (self.len + chunk.len()) as f64 / f64::from(WHISPER_SAMPLE_RATE),
150 max_secs: self.max_samples as f64 / f64::from(WHISPER_SAMPLE_RATE),
151 }
152 .into());
153 }
154 for &s in chunk {
155 self.push_one(s);
156 }
157 return Ok(());
158 }
159
160 let src = if chunk.len() > self.max_samples {
162 &chunk[chunk.len() - self.max_samples..]
163 } else {
164 chunk
165 };
166 for &s in src {
167 self.push_one(s);
168 }
169 Ok(())
170 }
171
172 fn push_one(&mut self, sample: f32) {
173 let s = if sample.is_finite() {
174 if sample > 1.0 {
175 self.clamped_count = self.clamped_count.saturating_add(1);
176 1.0
177 } else if sample < -1.0 {
178 self.clamped_count = self.clamped_count.saturating_add(1);
179 -1.0
180 } else {
181 sample
182 }
183 } else {
184 0.0
186 };
187
188 let cap = self.max_samples;
189 if self.len < cap {
190 let idx = (self.head + self.len) % cap;
191 self.storage[idx] = s;
192 self.len += 1;
193 } else {
194 self.storage[self.head] = s;
196 self.head = (self.head + 1) % cap;
197 }
198 }
199
200 pub fn rms(&self) -> f32 {
202 if self.len == 0 {
203 return 0.0;
204 }
205 let mut sum = 0.0f64;
206 let cap = self.max_samples;
207 for i in 0..self.len {
208 let s = self.storage[(self.head + i) % cap] as f64;
209 sum += s * s;
210 }
211 (sum / self.len as f64).sqrt() as f32
212 }
213
214 pub fn to_audio_input(&self) -> Result<AudioInput> {
216 if self.len == 0 {
217 return Err(UserError::InvalidAudio {
218 reason: "PCM buffer is empty".into(),
219 }
220 .into());
221 }
222 let mut v = Vec::with_capacity(self.len);
223 self.copy_samples_into(&mut v);
224 let samples: Arc<[f32]> = v.into();
225 AudioInput::from_pcm(samples, WHISPER_SAMPLE_RATE)
226 }
227
228 pub fn take_audio_input(&mut self) -> Result<AudioInput> {
230 if self.len == 0 {
231 return Err(UserError::InvalidAudio {
232 reason: "PCM buffer is empty".into(),
233 }
234 .into());
235 }
236 let mut v = Vec::with_capacity(self.len);
237 self.copy_samples_into(&mut v);
238 self.clear();
239 let samples: Arc<[f32]> = v.into();
240 AudioInput::from_pcm(samples, WHISPER_SAMPLE_RATE)
241 }
242}
243
244#[derive(Debug)]
246pub enum ContiguousSamples<'a> {
247 Borrowed(&'a [f32]),
248 Owned(Vec<f32>),
249}
250
251impl ContiguousSamples<'_> {
252 pub fn as_slice(&self) -> &[f32] {
253 match self {
254 Self::Borrowed(s) => s,
255 Self::Owned(v) => v.as_slice(),
256 }
257 }
258}
259
260impl std::ops::Deref for ContiguousSamples<'_> {
261 type Target = [f32];
262 fn deref(&self) -> &Self::Target {
263 self.as_slice()
264 }
265}
266
267fn validate_finite(chunk: &[f32]) -> Result<()> {
268 for (i, s) in chunk.iter().enumerate() {
269 if !s.is_finite() {
270 return Err(UserError::InvalidAudio {
271 reason: format!("PCM contains non-finite sample at index {i} ({s})"),
272 }
273 .into());
274 }
275 }
276 Ok(())
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 #[test]
284 fn rolling_drops_oldest() {
285 let mut buf = PcmBuffer::with_max_secs(0.01); assert_eq!(buf.max_samples(), 160);
287 buf.push(&vec![0.1; 100]).unwrap();
288 buf.push(&vec![0.2; 100]).unwrap();
289 assert_eq!(buf.len(), 160);
290 let s = buf.samples();
291 assert_eq!(s[0], 0.1);
292 assert_eq!(s[59], 0.1);
293 assert_eq!(s[60], 0.2);
294 }
295
296 #[test]
297 fn oversized_chunk_keeps_tail_only() {
298 let mut buf = PcmBuffer::with_max_secs(0.01); let big: Vec<f32> = (0..500).map(|i| (i as f32) * 0.001).collect();
301 buf.push(&big).unwrap();
302 assert_eq!(buf.len(), 160);
303 let s = buf.samples();
304 assert!((s[0] - 0.340).abs() < 1e-6);
306 assert!((s[159] - 0.499).abs() < 1e-6);
307 assert_eq!(buf.storage.len(), 160);
308 }
309
310 #[test]
311 fn bounded_errors() {
312 let mut buf = PcmBuffer::bounded(0.01);
313 buf.push(&vec![0.1; 160]).unwrap();
314 let err = buf.push(&[0.1]);
315 assert!(err.is_err());
316 }
317
318 #[test]
319 fn rejects_nan() {
320 let mut buf = PcmBuffer::dictation();
321 let err = buf.push(&[0.0, f32::NAN, 0.1]);
322 assert!(err.is_err());
323 assert!(buf.is_empty());
324 }
325
326 #[test]
327 fn clamps_amplitude() {
328 let mut buf = PcmBuffer::dictation();
329 buf.push(&[2.0, -3.0, 0.5]).unwrap();
330 assert_eq!(buf.clamped_count(), 2);
331 let s = buf.samples();
332 assert_eq!(s[0], 1.0);
333 assert_eq!(s[1], -1.0);
334 assert_eq!(s[2], 0.5);
335 }
336
337 #[test]
338 fn wraparound_contiguous() {
339 let mut buf = PcmBuffer::with_max_secs(0.001); buf.push(&[0.1; 12]).unwrap();
341 buf.push(&[0.2; 12]).unwrap();
342 assert_eq!(buf.len(), 16);
343 let s = buf.samples();
344 assert_eq!(&s[..4], &[0.1; 4]);
346 assert_eq!(&s[4..], &[0.2; 12]);
347 }
348
349 #[test]
350 fn to_audio_input() {
351 let mut buf = PcmBuffer::dictation();
352 buf.push(&vec![0.0; 1600]).unwrap();
353 let audio = buf.to_audio_input().unwrap();
354 assert_eq!(audio.sample_rate, 16_000);
355 assert_eq!(audio.len(), 1600);
356 assert!(!buf.is_empty());
357 let taken = buf.take_audio_input().unwrap();
358 assert_eq!(taken.len(), 1600);
359 assert!(buf.is_empty());
360 }
361
362 #[test]
363 fn rms_f64() {
364 let mut buf = PcmBuffer::dictation();
365 buf.push(&[0.5; 100]).unwrap();
366 let r = buf.rms();
367 assert!((r - 0.5).abs() < 1e-5);
368 }
369}