1fn sanitize(sample: f32) -> f32 {
16 if sample.is_finite() {
17 sample.clamp(-1.0, 1.0)
18 } else {
19 0.0
20 }
21}
22
23#[derive(Debug, Clone)]
28pub struct SampleWindow {
29 buf: Vec<f32>,
31 next: usize,
33 len: usize,
35}
36
37impl PartialEq for SampleWindow {
41 fn eq(&self, other: &Self) -> bool {
42 self.capacity() == other.capacity() && self.iter().eq(other.iter())
43 }
44}
45
46impl SampleWindow {
47 #[must_use]
53 pub fn new(capacity: usize) -> Self {
54 Self {
55 buf: vec![0.0; capacity],
56 next: 0,
57 len: 0,
58 }
59 }
60
61 pub fn push(&mut self, frame: &[f32]) {
66 let capacity = self.buf.len();
67 if capacity == 0 {
68 return;
69 }
70 let skip = frame.len().saturating_sub(capacity);
72 let mut written = 0;
73 for &sample in frame.iter().skip(skip) {
74 if let Some(slot) = self.buf.get_mut(self.next) {
75 *slot = sanitize(sample);
76 }
77 self.next += 1;
78 if self.next == capacity {
79 self.next = 0;
80 }
81 written += 1;
82 }
83 self.len = (self.len + written).min(capacity);
84 }
85
86 #[must_use]
88 pub const fn len(&self) -> usize {
89 self.len
90 }
91
92 #[must_use]
94 pub const fn is_empty(&self) -> bool {
95 self.len == 0
96 }
97
98 #[must_use]
100 pub fn capacity(&self) -> usize {
101 self.buf.len()
102 }
103
104 pub fn clear(&mut self) {
106 self.next = 0;
107 self.len = 0;
108 }
109
110 #[must_use]
112 pub fn samples(&self) -> Vec<f32> {
113 let mut out = Vec::with_capacity(self.len);
114 self.copy_into(&mut out);
115 out
116 }
117
118 pub fn copy_into(&self, out: &mut Vec<f32>) {
122 out.clear();
123 out.extend(self.iter());
124 }
125
126 #[must_use]
128 pub fn peak(&self) -> f32 {
129 self.iter().fold(0.0, |best, v| best.max(v.abs()))
130 }
131
132 #[must_use]
134 pub fn rms(&self) -> f32 {
135 if self.is_empty() {
136 return 0.0;
137 }
138 let sum: f64 = self.iter().map(|v| f64::from(v) * f64::from(v)).sum();
139 (sum / self.len as f64).sqrt() as f32
140 }
141
142 fn iter(&self) -> impl Iterator<Item = f32> + '_ {
144 let start = if self.len == self.buf.len() {
147 self.next
148 } else {
149 0
150 };
151 self.buf.iter().copied().cycle().skip(start).take(self.len)
152 }
153}
154
155#[cfg(test)]
156#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
157mod tests {
158 use super::*;
159 use crate::waveform::Waveform;
160 use presentar_core::{RecordingCanvas, Rect, Widget};
161 use proptest::prelude::*;
162
163 fn buffer_identity(w: &SampleWindow) -> (*const f32, usize) {
165 (w.buf.as_ptr(), w.buf.capacity())
166 }
167
168 fn model_sanitize(v: f32) -> f32 {
170 if v.is_nan() || v.is_infinite() {
171 0.0
172 } else {
173 v.clamp(-1.0, 1.0)
174 }
175 }
176
177 fn sample_strategy() -> impl Strategy<Value = f32> {
178 prop_oneof![
179 proptest::num::f32::ANY,
180 -3.0_f32..3.0,
181 Just(f32::NAN),
182 Just(f32::INFINITY),
183 Just(f32::NEG_INFINITY),
184 ]
185 }
186
187 proptest! {
188 #[test]
189 fn prop_window_equals_model(
190 capacity in 0_usize..64,
191 frames in proptest::collection::vec(
192 proptest::collection::vec(sample_strategy(), 0..200),
193 0..8,
194 ),
195 ) {
196 let mut window = SampleWindow::new(capacity);
197 let mut model: Vec<f32> = Vec::new();
198 for frame in &frames {
199 window.push(frame);
200 model.extend(frame.iter().copied().map(model_sanitize));
201 let keep = model.len().min(capacity);
202 let expected = model[model.len() - keep..].to_vec();
203 prop_assert_eq!(window.len(), expected.len());
204 prop_assert_eq!(window.is_empty(), expected.is_empty());
205 prop_assert_eq!(window.samples(), expected);
206 }
207 prop_assert_eq!(window.capacity(), capacity);
208 }
209 }
210
211 #[test]
212 fn new_is_empty_with_capacity() {
213 let w = SampleWindow::new(5);
214 assert_eq!(w.capacity(), 5);
215 assert_eq!(w.len(), 0);
216 assert!(w.is_empty());
217 assert!(w.samples().is_empty());
218 }
219
220 #[test]
221 fn frame_larger_than_capacity_keeps_tail() {
222 let mut w = SampleWindow::new(3);
223 w.push(&[0.1, 0.2, 0.3, 0.4, 0.5]);
224 assert_eq!(w.samples(), vec![0.3, 0.4, 0.5]);
225 assert_eq!(w.len(), 3);
226 w.push(&[-0.1, -0.2, -0.3, -0.4]);
228 assert_eq!(w.samples(), vec![-0.2, -0.3, -0.4]);
229 }
230
231 #[test]
232 fn empty_frame_changes_nothing() {
233 let mut w = SampleWindow::new(4);
234 w.push(&[]);
235 assert!(w.is_empty());
236 w.push(&[0.1, 0.2]);
237 let before = w.clone();
238 w.push(&[]);
239 assert_eq!(w, before);
240 assert_eq!(w.samples(), vec![0.1, 0.2]);
241 }
242
243 #[test]
244 fn capacity_zero_never_panics_and_stays_empty() {
245 let mut w = SampleWindow::new(0);
246 w.push(&[0.5, 0.6]);
247 w.push(&[]);
248 assert_eq!(w.capacity(), 0);
249 assert_eq!(w.len(), 0);
250 assert!(w.is_empty());
251 assert!(w.samples().is_empty());
252 assert_eq!(w.peak(), 0.0);
253 assert_eq!(w.rms(), 0.0);
254 let mut out = vec![9.0];
255 w.copy_into(&mut out);
256 assert!(out.is_empty());
257 w.clear();
258 assert!(w.is_empty());
259 }
260
261 #[test]
262 fn wrap_around_keeps_arrival_order() {
263 let mut w = SampleWindow::new(4);
264 w.push(&[0.1, 0.2, 0.3]);
265 assert_eq!(w.samples(), vec![0.1, 0.2, 0.3]);
266 w.push(&[0.4, 0.5, 0.6]);
267 assert_eq!(w.samples(), vec![0.3, 0.4, 0.5, 0.6]);
268 w.push(&[0.7]);
269 assert_eq!(w.samples(), vec![0.4, 0.5, 0.6, 0.7]);
270 let mut full = SampleWindow::new(3);
272 full.push(&[0.1, 0.2, 0.3]);
273 assert_eq!(full.samples(), vec![0.1, 0.2, 0.3]);
274 full.push(&[0.4]);
275 assert_eq!(full.samples(), vec![0.2, 0.3, 0.4]);
276 }
277
278 #[test]
279 fn non_finite_is_zero_and_finite_is_clamped() {
280 let mut w = SampleWindow::new(8);
281 w.push(&[
282 f32::NAN,
283 f32::INFINITY,
284 f32::NEG_INFINITY,
285 5.0,
286 -5.0,
287 f32::MAX,
288 0.25,
289 ]);
290 assert_eq!(w.samples(), vec![0.0, 0.0, 0.0, 1.0, -1.0, 1.0, 0.25]);
291 }
292
293 #[test]
294 fn copy_into_equals_samples_and_reuses_buffer() {
295 let mut w = SampleWindow::new(4);
296 w.push(&[0.1, 0.2, 0.3, 0.4, 0.5, 0.6]);
297 let mut out: Vec<f32> = Vec::with_capacity(64);
298 out.extend_from_slice(&[9.0; 10]);
299 let ptr = out.as_ptr();
300 w.copy_into(&mut out);
301 assert_eq!(out, w.samples());
302 assert_eq!(out, vec![0.3, 0.4, 0.5, 0.6]);
303 assert!(out.capacity() >= 64);
304 assert_eq!(out.as_ptr(), ptr);
305 let empty = SampleWindow::new(4);
307 empty.copy_into(&mut out);
308 assert!(out.is_empty());
309 assert!(out.capacity() >= 64);
310 }
311
312 #[test]
313 fn clear_empties_and_later_push_works() {
314 let mut w = SampleWindow::new(3);
315 w.push(&[0.1, 0.2, 0.3, 0.4]);
316 w.clear();
317 assert!(w.is_empty());
318 assert_eq!(w.len(), 0);
319 assert_eq!(w.capacity(), 3);
320 assert!(w.samples().is_empty());
321 assert_eq!(w.peak(), 0.0);
322 w.push(&[0.5, 0.6]);
323 assert_eq!(w.samples(), vec![0.5, 0.6]);
324 w.push(&[0.7, 0.8]);
325 assert_eq!(w.samples(), vec![0.6, 0.7, 0.8]);
326 }
327
328 #[test]
329 fn clear_of_a_part_filled_window_restarts_at_the_front() {
330 let mut w = SampleWindow::new(4);
333 w.push(&[0.1, 0.2, 0.3]);
334 w.clear();
335 w.push(&[0.5, 0.6]);
336 assert_eq!(w.samples(), vec![0.5, 0.6]);
337 w.push(&[0.7, 0.8, 0.9]);
338 assert_eq!(w.samples(), vec![0.6, 0.7, 0.8, 0.9]);
339 }
340
341 #[test]
342 fn peak_and_rms_hand_computed() {
343 let empty = SampleWindow::new(4);
344 assert_eq!(empty.peak(), 0.0);
345 assert_eq!(empty.rms(), 0.0);
346
347 let mut w = SampleWindow::new(4);
348 w.push(&[0.2, -0.7, 0.5]);
349 assert_eq!(w.peak(), 0.7);
350
351 let mut sq = SampleWindow::new(4);
352 sq.push(&[1.0, -1.0, 1.0, -1.0]);
353 assert_eq!(sq.peak(), 1.0);
354 assert_eq!(sq.rms(), 1.0);
355
356 let mut half = SampleWindow::new(4);
357 half.push(&[0.5; 4]);
358 assert!((half.rms() - 0.5).abs() < 1e-6);
359
360 let mut clamped = SampleWindow::new(4);
362 clamped.push(&[3.0, f32::NAN]);
363 assert_eq!(clamped.peak(), 1.0);
364 assert!((clamped.rms() - 0.5_f32.sqrt()).abs() < 1e-6);
365
366 let mut old = SampleWindow::new(2);
368 old.push(&[0.9, 0.1, 0.2]);
369 assert_eq!(old.peak(), 0.2);
370 assert!((old.rms() - (0.05_f32 / 2.0).sqrt()).abs() < 1e-6);
371 }
372
373 #[test]
374 fn push_never_reallocates() {
375 let mut w = SampleWindow::new(16);
376 let identity = buffer_identity(&w);
377 for i in 0..1000_u32 {
378 let base = (i % 7) as f32 / 10.0;
379 w.push(&[base, -base, f32::NAN]);
380 w.push(&[0.5; 40]);
381 }
382 w.clear();
383 w.push(&[0.1, 0.2]);
384 assert_eq!(buffer_identity(&w), identity);
385 assert_eq!(w.capacity(), 16);
386 }
387
388 #[test]
389 fn partial_eq_compares_content_not_rotation() {
390 let mut a = SampleWindow::new(3);
391 a.push(&[0.1, 0.2, 0.3]);
392 let mut b = SampleWindow::new(3);
393 b.push(&[0.9, 0.9, 0.1, 0.2, 0.3]); assert_eq!(a, b);
395 b.push(&[0.4]);
396 assert_ne!(a, b);
397 let mut c = SampleWindow::new(4);
398 c.push(&[0.1, 0.2, 0.3]);
399 assert_ne!(a, c); }
401
402 fn paint_commands(samples: Vec<f32>) -> Vec<presentar_core::draw::DrawCommand> {
403 let mut widget = Waveform::new().samples(samples);
404 widget.layout(Rect::new(10.0, 20.0, 100.0, 40.0));
405 let mut canvas = RecordingCanvas::new();
406 widget.paint(&mut canvas);
407 canvas.take_commands()
408 }
409
410 #[test]
411 fn waveform_paints_the_same_path_from_the_window() {
412 let mut w = SampleWindow::new(4);
413 w.push(&[0.1, 0.9, -0.4]);
414 w.push(&[0.3, -0.8, 0.2]);
415 let plain = [0.9_f32, -0.4, 0.3, -0.8, 0.2];
416 let expected = paint_commands(plain[plain.len() - 4..].to_vec());
417 let actual = paint_commands(w.samples());
418 assert_eq!(expected.len(), 1);
419 assert_eq!(actual, expected);
420
421 let none = SampleWindow::new(4);
423 assert_eq!(paint_commands(none.samples()), paint_commands(Vec::new()));
424 }
425
426 fn wild() -> impl Strategy<Value = f32> {
438 prop_oneof![
439 proptest::num::f32::ANY,
440 Just(f32::NAN),
441 Just(f32::INFINITY),
442 Just(f32::NEG_INFINITY),
443 Just(f32::MAX),
444 Just(f32::MIN),
445 Just(f32::MIN_POSITIVE),
446 Just(1.0e-40_f32),
447 Just(-0.0_f32),
448 -1000.0_f32..1000.0,
449 ]
450 }
451
452 fn unit_interval(v: f32) -> bool {
453 v.is_finite() && (-1.0..=1.0).contains(&v)
454 }
455
456 proptest! {
457 #[test]
458 fn class_window_every_entry_point_is_total_for_any_f32(
459 capacity in 0_usize..48,
460 frames in proptest::collection::vec(
461 proptest::collection::vec(wild(), 0..120),
462 0..6,
463 ),
464 ) {
465 let mut window = SampleWindow::new(capacity);
466 for frame in &frames {
467 window.push(frame);
468 let held = window.samples();
469 prop_assert_eq!(held.len(), window.len());
470 prop_assert!(window.len() <= window.capacity());
471 prop_assert!(held.iter().copied().all(unit_interval), "{held:?}");
472 let peak = window.peak();
473 let rms = window.rms();
474 prop_assert!(peak.is_finite() && (0.0..=1.0).contains(&peak), "{peak}");
475 prop_assert!(rms.is_finite() && (0.0..=1.0 + 1.0e-3).contains(&rms), "{rms}");
476 prop_assert!(rms <= peak + 1.0e-3);
477 let mut out = vec![f32::NAN; 3];
478 window.copy_into(&mut out);
479 prop_assert_eq!(out, held);
480 }
481 window.clear();
482 prop_assert!(window.is_empty());
483 prop_assert!(window.peak() == 0.0);
484 prop_assert!(window.rms() == 0.0); prop_assert_eq!(window.capacity(), capacity);
486 }
487
488 #[test]
489 fn class_window_samples_paint_finite_through_a_waveform(
490 capacity in 0_usize..48,
491 frame in proptest::collection::vec(wild(), 0..120),
492 x in wild(),
493 y in wild(),
494 w in wild(),
495 h in wild(),
496 ) {
497 let mut window = SampleWindow::new(capacity);
498 window.push(&frame);
499 let mut widget = Waveform::new().samples(window.samples());
500 widget.layout(Rect::new(x, y, w, h));
501 let mut canvas = RecordingCanvas::new();
502 widget.paint(&mut canvas);
503 for cmd in canvas.commands() {
504 let presentar_core::draw::DrawCommand::Path { points, style, .. } = cmd else {
505 return Err(TestCaseError::fail("not a path"));
506 };
507 prop_assert!(style.width.is_finite() && style.width >= 0.0);
508 prop_assert!(points.iter().all(|p| p.x.is_finite() && p.y.is_finite()));
509 }
510 }
511 }
512
513 #[test]
514 fn empty_rms_is_positive_zero() {
515 let w = SampleWindow::new(4);
516 assert!(w.rms() == 0.0 && w.rms().is_sign_positive());
517 }
518}