1#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
9pub enum WindowError {
10 #[error("win must be > 0")]
12 ZeroWindow,
13 #[error("hop must be > 0")]
15 ZeroHop,
16 #[error("hop ({hop}) must be <= win ({win})")]
18 HopExceedsWindow {
19 hop: usize,
21 win: usize,
23 },
24}
25
26#[derive(Debug, Clone)]
30pub struct WindowIter {
31 start: usize,
32 total: usize,
33 win: usize,
34 hop: usize,
35 include_partial: bool,
36}
37
38impl WindowIter {
39 #[allow(clippy::panic)] pub fn new(total: usize, win: usize, hop: usize) -> Self {
54 match Self::try_new(total, win, hop) {
55 Ok(iter) => iter,
56 Err(e) => panic!("WindowIter::new: {e}"),
57 }
58 }
59
60 pub fn try_new(total: usize, win: usize, hop: usize) -> Result<Self, WindowError> {
70 if win == 0 {
71 return Err(WindowError::ZeroWindow);
72 }
73 if hop == 0 {
74 return Err(WindowError::ZeroHop);
75 }
76 if hop > win {
77 return Err(WindowError::HopExceedsWindow { hop, win });
78 }
79 Ok(Self {
80 start: 0,
81 total,
82 win,
83 hop,
84 include_partial: false,
85 })
86 }
87
88 pub fn include_partial(mut self) -> Self {
93 self.include_partial = true;
94 self
95 }
96}
97
98impl Iterator for WindowIter {
99 type Item = (usize, usize);
100
101 fn next(&mut self) -> Option<Self::Item> {
102 if self.start >= self.total {
103 return None;
104 }
105 let end = if self.include_partial {
106 (self.start + self.win).min(self.total)
107 } else if self.start + self.win > self.total {
108 return None;
109 } else {
110 self.start + self.win
111 };
112 let item = (self.start, end);
113 self.start += self.hop;
114 Some(item)
115 }
116}
117
118#[derive(Debug, Clone)]
124pub struct WindowBuffer {
125 buf: Vec<f32>,
126 win: usize,
127 hop: usize,
128 next_start: usize,
129}
130
131impl WindowBuffer {
132 #[allow(clippy::panic)] pub fn new(win: usize, hop: usize) -> Self {
146 match Self::try_new(win, hop) {
147 Ok(buf) => buf,
148 Err(e) => panic!("WindowBuffer::new: {e}"),
149 }
150 }
151
152 pub fn try_new(win: usize, hop: usize) -> Result<Self, WindowError> {
161 if win == 0 {
162 return Err(WindowError::ZeroWindow);
163 }
164 if hop == 0 {
165 return Err(WindowError::ZeroHop);
166 }
167 if hop > win {
168 return Err(WindowError::HopExceedsWindow { hop, win });
169 }
170 Ok(Self {
171 buf: Vec::new(),
172 win,
173 hop,
174 next_start: 0,
175 })
176 }
177
178 pub fn extend(&mut self, samples: &[f32]) {
183 self.buf.extend_from_slice(samples);
184 }
185
186 pub fn try_pop(&mut self) -> Option<(usize, Vec<f32>)> {
195 if self.buf.len() < self.win {
196 return None;
197 }
198 let start = self.next_start;
199 let window = self.buf[..self.win].to_vec();
200 self.next_start += self.hop;
201 self.buf.drain(..self.hop);
202 Some((start, window))
203 }
204
205 pub fn flush(&mut self) -> Option<(usize, Vec<f32>)> {
212 if self.buf.is_empty() {
213 return None;
214 }
215 let start = self.next_start;
216 let mut padded = self.buf.clone();
217 if padded.len() < self.win {
218 padded.resize(self.win, 0.0f32);
219 }
220 self.buf.clear();
221 Some((start, padded))
222 }
223
224 pub fn is_empty(&self) -> bool {
229 self.buf.is_empty()
230 }
231
232 pub fn len(&self) -> usize {
237 self.buf.len()
238 }
239
240 pub fn clear(&mut self) {
245 self.buf.clear();
246 }
247
248 pub fn set_next_start(&mut self, start: usize) {
253 self.next_start = start;
254 }
255}
256
257#[allow(clippy::unwrap_used)]
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
263 fn window_iter_complete_only() {
264 let ranges: Vec<_> = WindowIter::new(10, 3, 2).collect();
265 assert_eq!(ranges, vec![(0, 3), (2, 5), (4, 7), (6, 9)]);
266 }
267
268 #[test]
269 fn window_iter_include_partial() {
270 let ranges: Vec<_> = WindowIter::new(10, 3, 2).include_partial().collect();
271 assert_eq!(ranges, vec![(0, 3), (2, 5), (4, 7), (6, 9), (8, 10)]);
272 }
273
274 #[test]
275 fn window_iter_empty() {
276 let ranges: Vec<_> = WindowIter::new(0, 3, 2).collect();
277 assert!(ranges.is_empty());
278 }
279
280 #[test]
281 fn window_iter_shorter_than_win() {
282 let ranges: Vec<_> = WindowIter::new(2, 3, 2).collect();
283 assert!(ranges.is_empty());
284 let ranges: Vec<_> = WindowIter::new(2, 3, 2).include_partial().collect();
285 assert_eq!(ranges, vec![(0, 2)]);
286 }
287
288 #[test]
289 fn window_buffer_pop_and_flush() {
290 let mut buf = WindowBuffer::new(4, 2);
291 buf.extend(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
292 let (s1, w1) = buf.try_pop().unwrap();
293 assert_eq!(s1, 0);
294 assert_eq!(w1, vec![1.0, 2.0, 3.0, 4.0]);
295 let (s2, w2) = buf.try_pop().unwrap();
296 assert_eq!(s2, 2);
297 assert_eq!(w2, vec![3.0, 4.0, 5.0, 6.0]);
298 assert!(buf.try_pop().is_none());
299
300 let (s3, w3) = buf.flush().unwrap();
301 assert_eq!(s3, 4);
302 assert_eq!(w3, vec![5.0, 6.0, 0.0, 0.0]);
303 }
304
305 #[test]
306 fn window_buffer_flush_empty() {
307 let mut buf = WindowBuffer::new(4, 2);
308 assert!(buf.flush().is_none());
309 }
310
311 #[test]
312 #[should_panic(expected = "WindowIter::new: win must be > 0")]
313 fn window_iter_rejects_zero_win() {
314 let _ = WindowIter::new(10, 0, 1);
315 }
316
317 #[test]
318 #[should_panic(expected = "WindowIter::new: hop must be > 0")]
319 fn window_iter_rejects_zero_hop() {
320 let _ = WindowIter::new(10, 2, 0);
321 }
322
323 #[test]
324 #[should_panic(expected = "WindowIter::new: hop (3) must be <= win (2)")]
325 fn window_iter_rejects_hop_greater_than_win() {
326 let _ = WindowIter::new(10, 2, 3);
327 }
328
329 #[test]
330 #[should_panic(expected = "WindowBuffer::new: win must be > 0")]
331 fn window_buffer_rejects_zero_win() {
332 let _ = WindowBuffer::new(0, 1);
333 }
334
335 #[test]
336 #[should_panic(expected = "WindowBuffer::new: hop must be > 0")]
337 fn window_buffer_rejects_zero_hop() {
338 let _ = WindowBuffer::new(4, 0);
339 }
340
341 #[test]
342 #[should_panic(expected = "WindowBuffer::new: hop (5) must be <= win (4)")]
343 fn window_buffer_rejects_hop_greater_than_win() {
344 let _ = WindowBuffer::new(4, 5);
345 }
346
347 #[test]
348 fn window_iter_try_new_reports_geometry_errors() {
349 assert_eq!(
350 WindowIter::try_new(10, 0, 1).unwrap_err(),
351 WindowError::ZeroWindow
352 );
353 assert_eq!(
354 WindowIter::try_new(10, 2, 0).unwrap_err(),
355 WindowError::ZeroHop
356 );
357 assert_eq!(
358 WindowIter::try_new(10, 2, 3).unwrap_err(),
359 WindowError::HopExceedsWindow { hop: 3, win: 2 }
360 );
361 assert!(WindowIter::try_new(10, 2, 1).is_ok());
362 }
363
364 #[test]
365 fn window_buffer_try_new_reports_geometry_errors() {
366 assert_eq!(
367 WindowBuffer::try_new(0, 1).unwrap_err(),
368 WindowError::ZeroWindow
369 );
370 assert_eq!(
371 WindowBuffer::try_new(4, 0).unwrap_err(),
372 WindowError::ZeroHop
373 );
374 assert_eq!(
375 WindowBuffer::try_new(4, 5).unwrap_err(),
376 WindowError::HopExceedsWindow { hop: 5, win: 4 }
377 );
378 assert!(WindowBuffer::try_new(4, 2).is_ok());
379 }
380
381 #[test]
382 fn window_buffer_len_empty_clear_and_set_next_start() {
383 let mut buf = WindowBuffer::new(4, 2);
384 assert!(buf.is_empty());
385 assert_eq!(buf.len(), 0);
386 buf.extend(&[1.0, 2.0]);
387 assert!(!buf.is_empty());
388 assert_eq!(buf.len(), 2);
389 buf.set_next_start(100);
390 buf.extend(&[3.0, 4.0]);
391 let (start, window) = buf.try_pop().unwrap();
392 assert_eq!(start, 100);
393 assert_eq!(window, vec![1.0, 2.0, 3.0, 4.0]);
394 buf.clear();
395 assert!(buf.is_empty());
396 assert!(buf.try_pop().is_none());
397 }
398
399 #[test]
400 fn window_buffer_flush_without_padding_when_buf_exceeds_win() {
401 let mut buf = WindowBuffer::new(4, 4);
402 buf.extend(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
403 let (start, window) = buf.flush().unwrap();
404 assert_eq!(start, 0);
405 assert_eq!(window, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
406 assert!(buf.is_empty());
407 }
408}
409
410#[allow(clippy::unwrap_used)]
411#[cfg(test)]
412mod prop_tests {
413 use super::*;
414 use proptest::prelude::*;
415
416 proptest! {
417 #![proptest_config(ProptestConfig {
418 cases: 1000,
419 ..ProptestConfig::default()
420 })]
421
422 #[test]
424 fn window_iter_start_less_than_end(
425 total in 0usize..10000,
426 (win, hop) in (1usize..5000)
427 .prop_flat_map(|w| (Just(w), 1usize..=w)),
428 include_partial in any::<bool>(),
429 ) {
430 let iter = if include_partial {
431 WindowIter::new(total, win, hop).include_partial()
432 } else {
433 WindowIter::new(total, win, hop)
434 };
435 for (start, end) in iter {
436 prop_assert!(start < end, "start {} must be < end {}", start, end);
437 }
438 }
439
440 #[test]
442 fn window_iter_end_le_total(
443 total in 0usize..10000,
444 (win, hop) in (1usize..5000)
445 .prop_flat_map(|w| (Just(w), 1usize..=w)),
446 include_partial in any::<bool>(),
447 ) {
448 let iter = if include_partial {
449 WindowIter::new(total, win, hop).include_partial()
450 } else {
451 WindowIter::new(total, win, hop)
452 };
453 for (_, end) in iter {
454 prop_assert!(end <= total, "end {} must be <= total {}", end, total);
455 }
456 }
457
458 #[test]
460 fn window_iter_no_partial(
461 total in 0usize..10000,
462 (win, hop) in (1usize..5000)
463 .prop_flat_map(|w| (Just(w), 1usize..=w)),
464 ) {
465 for (start, end) in WindowIter::new(total, win, hop) {
466 prop_assert_eq!(
467 end - start,
468 win,
469 "window [{}..{}] must be full size {} when include_partial=false",
470 start, end, win
471 );
472 }
473 }
474
475 #[test]
479 fn window_iter_partial_last(
480 total in 0usize..10000,
481 (win, hop) in (1usize..5000)
482 .prop_flat_map(|w| (Just(w), 1usize..=w)),
483 ) {
484 let ranges: Vec<_> = WindowIter::new(total, win, hop).include_partial().collect();
485 for (start, end) in &ranges {
486 prop_assert!(
487 *end == (*start + win).min(total),
488 "window [{}..{}] must equal ({} + {}).min({})",
489 start, end, start, win, total
490 );
491 }
492 }
493 }
494}