Skip to main content

presentar_widgets/
sample_window.rs

1//! `SampleWindow`: a fixed-capacity window of the newest audio samples.
2//!
3//! This is the step before the [`Waveform`](crate::waveform::Waveform) widget
4//! in a live meter: capture code pushes frames as they arrive, and each paint
5//! reads the window, oldest sample first. The window is a ring buffer over a
6//! `Vec<f32>` allocated once in [`SampleWindow::new`], so [`SampleWindow::push`]
7//! never allocates. Pure Rust: nothing here touches time, threads or files, so
8//! it builds for `wasm32`.
9//!
10//! Contract: `contracts/present-sample-window-v1.yaml`.
11
12/// Treat NaN and infinities as silence; clamp everything else to [-1, 1].
13///
14/// The same rule as `sanitize` in `waveform.rs`, which is private there.
15fn 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/// The newest `capacity` audio samples, oldest first.
24///
25/// Stored samples are already sanitized: non-finite values are 0.0 and finite
26/// values lie in [-1, 1]. Capacity 0 is allowed and holds nothing.
27#[derive(Debug, Clone)]
28pub struct SampleWindow {
29    /// Ring storage; `buf.len()` is the capacity and never changes.
30    buf: Vec<f32>,
31    /// Index the next sample is written to, in `0..capacity` (0 when capacity is 0).
32    next: usize,
33    /// Number of valid samples, `<= capacity`. While `len < capacity`, `next == len`.
34    len: usize,
35}
36
37/// Two windows are equal when they have the same capacity and hold the same
38/// samples in the same order; the rotation of the ring storage is irrelevant.
39/// Stored samples are never NaN, so this is a true equivalence.
40impl 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    /// Create an empty window holding at most `capacity` samples.
48    ///
49    /// Capacity 0 is allowed: such a window ignores every push and stays
50    /// empty. The only allocation this type ever makes is here (and in
51    /// [`Self::samples`] / [`Self::copy_into`] for the caller's copy).
52    #[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    /// Append a frame, keeping the newest samples and dropping the oldest.
62    ///
63    /// A frame longer than the capacity keeps its tail. Non-finite samples are
64    /// stored as 0.0 and finite ones are clamped to [-1, 1]. Never allocates.
65    pub fn push(&mut self, frame: &[f32]) {
66        let capacity = self.buf.len();
67        if capacity == 0 {
68            return;
69        }
70        // Only the last `capacity` samples of the frame can survive.
71        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    /// Number of samples currently held.
87    #[must_use]
88    pub const fn len(&self) -> usize {
89        self.len
90    }
91
92    /// Whether the window holds no samples.
93    #[must_use]
94    pub const fn is_empty(&self) -> bool {
95        self.len == 0
96    }
97
98    /// Maximum number of samples the window holds.
99    #[must_use]
100    pub fn capacity(&self) -> usize {
101        self.buf.len()
102    }
103
104    /// Drop every sample; the capacity (and the storage) is unchanged.
105    pub fn clear(&mut self) {
106        self.next = 0;
107        self.len = 0;
108    }
109
110    /// The window, oldest sample first. Allocates; see [`Self::copy_into`].
111    #[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    /// Clear `out` and fill it with the window, oldest sample first.
119    ///
120    /// Reuses `out`'s allocation when it is large enough.
121    pub fn copy_into(&self, out: &mut Vec<f32>) {
122        out.clear();
123        out.extend(self.iter());
124    }
125
126    /// Largest magnitude in the window; 0.0 when empty.
127    #[must_use]
128    pub fn peak(&self) -> f32 {
129        self.iter().fold(0.0, |best, v| best.max(v.abs()))
130    }
131
132    /// Root mean square of the window; 0.0 when empty.
133    #[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    /// The window, oldest first, without allocating.
143    fn iter(&self) -> impl Iterator<Item = f32> + '_ {
144        // Not full: samples sit at 0..len and `next == len`. Full: the oldest
145        // sample is at `next`. (Capacity 0 is "full" with `next == 0`.)
146        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    /// Address and capacity of the ring storage, to prove push does not reallocate.
164    fn buffer_identity(w: &SampleWindow) -> (*const f32, usize) {
165        (w.buf.as_ptr(), w.buf.capacity())
166    }
167
168    /// Independent model of the sanitizing rule.
169    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        // and again over a non-empty window
227        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        // exactly full, no wrap yet, then a push that lands on index 0
271        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        // an empty window empties the buffer but keeps its capacity
306        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        // The write position is 3 here, not 0: a clear that forgot it would
331        // put the next frame at the wrong end of the storage.
332        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        // 3.0 clamps to 1.0, NaN counts as 0 -> sqrt(1/2)
361        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        // only the window counts: 0.9 has been dropped
367        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]); // rotated storage, same window
394        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); // same content, different capacity
400    }
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        // an empty window paints the same centre line as an empty slice
422        let none = SampleWindow::new(4);
423        assert_eq!(paint_commands(none.samples()), paint_commands(Vec::new()));
424    }
425
426    // ===== Class property: every public entry point, any f32 (PR #6897, round 3) =====
427    //
428    // Entry point                       covered by
429    // new / capacity / len / is_empty   class_window_every_entry_point_is_total_for_any_f32
430    // push                              class_window_every_entry_point_is_total_for_any_f32
431    // samples / copy_into               class_window_every_entry_point_is_total_for_any_f32
432    // peak / rms                        class_window_every_entry_point_is_total_for_any_f32
433    // clear                             class_window_every_entry_point_is_total_for_any_f32
434    // (feeds Waveform::paint)           class_window_samples_paint_finite_through_a_waveform
435
436    /// Any f32, with the awkward ones over-represented.
437    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); // -0.0 is allowed: an empty f32 sum is -0.0
485            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}