Skip to main content

alpha2_audio/
waveform.rs

1//! Multi-resolution waveform summaries for timeline display.
2//!
3//! A timeline draws a clip's audio as a column per pixel: the extremes of the
4//! samples under it and their RMS. [`WaveformSummary`] computes those columns
5//! at a base bucket size and at coarser zoom levels, each four times the
6//! last, from planar samples streamed in blocks of any size.
7//!
8//! Level 0 is computed from the samples. Every coarser bucket is the fold of
9//! four finer ones: its minimum and maximum are theirs exactly, and its RMS
10//! comes from their sums of squares, accumulated in `f64` (a squared `f32` is
11//! exact there). Results do not depend on how the input is split into blocks,
12//! and they are bit-identical natively and as wasm. Extremes compare in
13//! `f32::total_cmp` order, -0 below +0: `f32::min` and `max` may return
14//! either zero when they compare equal, and aarch64 and `x86_64` differ. A
15//! NaN or infinite sample counts as silence, as it plays in a mix.
16
17/// The extremes and RMS of one run of samples of one channel.
18#[derive(Copy, Clone, Debug, Default, PartialEq)]
19pub struct WaveformBucket {
20    /// Smallest sample.
21    pub min: f32,
22    /// Largest sample.
23    pub max: f32,
24    /// Root mean square of the samples.
25    pub rms: f32,
26}
27
28/// One zoom level of a [`Waveform`].
29#[derive(Clone, Debug, PartialEq)]
30pub struct WaveformLevel {
31    /// Samples per bucket at this level. The last bucket may cover fewer.
32    pub bucket_samples: usize,
33    /// Buckets per channel, in time order.
34    pub channels: Vec<Vec<WaveformBucket>>,
35}
36
37/// A finished waveform summary: its levels, finest first.
38#[derive(Clone, Debug, PartialEq)]
39pub struct Waveform {
40    /// Samples summarized per channel.
41    pub frames: u64,
42    /// Levels from the base bucket size up, each four times coarser.
43    pub levels: Vec<WaveformLevel>,
44}
45
46/// Why a waveform summary cannot be configured or fed.
47#[derive(Copy, Clone, Debug, Eq, PartialEq)]
48#[non_exhaustive]
49pub enum WaveformError {
50    /// No channels, a zero bucket, or no levels.
51    InvalidShape,
52    /// A block's channel count or plane lengths do not match.
53    MismatchedBlock,
54}
55
56impl std::fmt::Display for WaveformError {
57    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        formatter.write_str(match self {
59            Self::InvalidShape => "a waveform needs channels, a positive bucket, and a level",
60            Self::MismatchedBlock => "the block's channels do not match the waveform's",
61        })
62    }
63}
64
65impl std::error::Error for WaveformError {}
66
67/// Buckets folded into one bucket of the next level.
68const FOLD: usize = 4;
69/// Most zoom levels: a 1-sample base reaches 4^15 samples, over six hours at
70/// 48 kHz, which is coarser than any timeline shows.
71pub const MAX_WAVEFORM_LEVELS: usize = 16;
72
73/// A bucket still being filled: exact extremes and the sum of squares.
74#[derive(Copy, Clone, Debug)]
75struct Accumulator {
76    min: f32,
77    max: f32,
78    sum_squares: f64,
79    samples: u64,
80    parts: usize,
81}
82
83impl Accumulator {
84    const EMPTY: Self = Self {
85        min: f32::INFINITY,
86        max: f32::NEG_INFINITY,
87        sum_squares: 0.0,
88        samples: 0,
89        parts: 0,
90    };
91
92    fn is_empty(&self) -> bool {
93        self.samples == 0
94    }
95
96    /// Adds one sample. A NaN or infinite one counts as silence, as a mix
97    /// plays it and the loudness meter measures it.
98    fn add_sample(&mut self, sample: f32) {
99        let sample = if sample.is_finite() { sample } else { 0.0 };
100        self.min = lower(self.min, sample);
101        self.max = higher(self.max, sample);
102        let square = f64::from(sample);
103        self.sum_squares += square * square;
104        self.samples += 1;
105    }
106
107    fn add_part(&mut self, part: &Self) {
108        self.min = lower(self.min, part.min);
109        self.max = higher(self.max, part.max);
110        self.sum_squares += part.sum_squares;
111        self.samples += part.samples;
112        self.parts += 1;
113    }
114
115    #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
116    fn bucket(&self) -> WaveformBucket {
117        WaveformBucket {
118            min: self.min,
119            max: self.max,
120            rms: (self.sum_squares / self.samples as f64).sqrt() as f32,
121        }
122    }
123}
124
125/// The smaller of `current` and `sample`, -0 below +0.
126fn lower(current: f32, sample: f32) -> f32 {
127    if sample.total_cmp(&current).is_lt() {
128        sample
129    } else {
130        current
131    }
132}
133
134/// The larger of `current` and `sample`, +0 above -0.
135fn higher(current: f32, sample: f32) -> f32 {
136    if sample.total_cmp(&current).is_gt() {
137        sample
138    } else {
139        current
140    }
141}
142
143/// Builds a [`Waveform`] from planar samples streamed in blocks.
144#[derive(Clone, Debug)]
145pub struct WaveformSummary {
146    base_bucket: usize,
147    levels: Vec<WaveformLevel>,
148    /// The open bucket of each level, per channel: `open[level][channel]`.
149    open: Vec<Vec<Accumulator>>,
150    frames: u64,
151}
152
153impl WaveformSummary {
154    /// A summary of `channels` channels whose finest buckets hold
155    /// `base_bucket` samples, with `levels` zoom levels in all.
156    ///
157    /// # Errors
158    /// Returns [`WaveformError::InvalidShape`] for zero channels, a zero
159    /// bucket, or a level count outside `1..=MAX_WAVEFORM_LEVELS`.
160    pub fn new(channels: usize, base_bucket: usize, levels: usize) -> Result<Self, WaveformError> {
161        if channels == 0 || base_bucket == 0 || !(1..=MAX_WAVEFORM_LEVELS).contains(&levels) {
162            return Err(WaveformError::InvalidShape);
163        }
164        let mut bucket_samples = base_bucket;
165        let mut summary_levels = Vec::with_capacity(levels);
166        for _ in 0..levels {
167            summary_levels.push(WaveformLevel {
168                bucket_samples,
169                channels: vec![Vec::new(); channels],
170            });
171            bucket_samples = bucket_samples.saturating_mul(FOLD);
172        }
173        Ok(Self {
174            base_bucket,
175            levels: summary_levels,
176            open: vec![vec![Accumulator::EMPTY; channels]; levels],
177            frames: 0,
178        })
179    }
180
181    /// Adds one block of planar samples, one slice per channel, all of one
182    /// length.
183    ///
184    /// # Errors
185    /// Returns [`WaveformError::MismatchedBlock`] for a block with another
186    /// channel count or ragged planes.
187    pub fn push(&mut self, planes: &[&[f32]]) -> Result<(), WaveformError> {
188        let channels = self.levels[0].channels.len();
189        let Some(frames) = planes.first().map(|plane| plane.len()) else {
190            return Err(WaveformError::MismatchedBlock);
191        };
192        if planes.len() != channels || planes.iter().any(|plane| plane.len() != frames) {
193            return Err(WaveformError::MismatchedBlock);
194        }
195        for index in 0..frames {
196            for (channel, plane) in planes.iter().enumerate() {
197                self.open[0][channel].add_sample(plane[index]);
198            }
199            if self.open[0][0].samples == self.base_bucket as u64 {
200                self.close(0);
201            }
202        }
203        self.frames += frames as u64;
204        Ok(())
205    }
206
207    /// Closes the open bucket of `level` on every channel and folds it into
208    /// the next level, closing that too once it has all its parts.
209    fn close(&mut self, level: usize) {
210        let channels = self.open[level].len();
211        for channel in 0..channels {
212            let finished = self.open[level][channel];
213            self.levels[level].channels[channel].push(finished.bucket());
214            if let Some(next) = self.open.get_mut(level + 1) {
215                next[channel].add_part(&finished);
216            }
217            self.open[level][channel] = Accumulator::EMPTY;
218        }
219        if level + 1 < self.open.len() && self.open[level + 1][0].parts == FOLD {
220            self.close(level + 1);
221        }
222    }
223
224    /// Closes the partial buckets and returns the summary.
225    #[must_use]
226    pub fn finish(mut self) -> Waveform {
227        for level in 0..self.open.len() {
228            if !self.open[level][0].is_empty() {
229                self.close(level);
230            }
231        }
232        Waveform {
233            frames: self.frames,
234            levels: self.levels,
235        }
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::{WaveformBucket, WaveformError, WaveformSummary};
242
243    /// The extremes of each level-0 bucket of one channel, as bits.
244    fn extreme_bits(samples: &[f32], bucket: usize) -> Vec<(u32, u32)> {
245        let mut summary = WaveformSummary::new(1, bucket, 1).expect("shape");
246        summary.push(&[samples]).expect("block");
247        summary.finish().levels[0].channels[0]
248            .iter()
249            .map(|bucket| (bucket.min.to_bits(), bucket.max.to_bits()))
250            .collect()
251    }
252
253    #[test]
254    fn a_signed_zero_extreme_is_the_same_on_every_target() {
255        // f32::min and max may return either zero when they compare equal, and aarch64 and x86_64
256        // pick differently: -0 is the smaller and +0 the larger, in whatever order they arrive, so
257        // the bits are one answer everywhere.
258        let (positive, negative) = (0.0_f32.to_bits(), (-0.0_f32).to_bits());
259        let samples = [
260            0.0, -0.0, 0.0, -0.0, // +0 first
261            -0.0, 0.0, -0.0, 0.0, // -0 first
262            -0.25, 0.0, -0.0, -0.0, // a negative extreme, a zero maximum
263            0.25, -0.0, 0.0, 0.0, // a zero minimum, a positive extreme
264            -0.0, -0.0, -0.0, -0.0, // one zero alone
265        ];
266        assert_eq!(
267            extreme_bits(&samples, 4),
268            [
269                (negative, positive),
270                (negative, positive),
271                ((-0.25_f32).to_bits(), positive),
272                (negative, 0.25_f32.to_bits()),
273                (negative, negative),
274            ]
275        );
276        let mut summary = WaveformSummary::new(1, 2, 2).expect("shape");
277        summary.push(&[&samples[..8]]).expect("block");
278        let coarse = summary.finish().levels[1].channels[0][0];
279        assert_eq!(
280            (coarse.min.to_bits(), coarse.max.to_bits()),
281            (negative, positive)
282        );
283    }
284
285    fn sine(frames: usize, period: usize, amplitude: f32) -> Vec<f32> {
286        (0..frames)
287            .map(|index| {
288                #[allow(clippy::cast_precision_loss)]
289                let phase = index as f32 / period as f32 * core::f32::consts::TAU;
290                amplitude * libm::sinf(phase)
291            })
292            .collect()
293    }
294
295    #[test]
296    fn a_sine_has_its_peak_and_its_rms() {
297        // 64 samples per period, whole periods per bucket.
298        let signal = sine(4_096, 64, 0.5);
299        let mut summary = WaveformSummary::new(1, 256, 1).expect("shape");
300        summary.push(&[&signal]).expect("block");
301        let waveform = summary.finish();
302        assert_eq!(waveform.frames, 4_096);
303        let buckets = &waveform.levels[0].channels[0];
304        assert_eq!(buckets.len(), 16);
305        for bucket in buckets {
306            assert!((bucket.max - 0.5).abs() < 1e-6, "{bucket:?}");
307            assert!((bucket.min + 0.5).abs() < 1e-6, "{bucket:?}");
308            assert!(
309                (bucket.rms - 0.5 / 2.0_f32.sqrt()).abs() < 1e-5,
310                "{bucket:?}"
311            );
312        }
313    }
314
315    #[test]
316    fn coarser_levels_are_the_fold_of_finer_ones_and_match_direct_summaries() {
317        let left = sine(10_000, 37, 0.8);
318        let right = sine(10_000, 91, 0.3)
319            .into_iter()
320            .enumerate()
321            .map(|(index, sample)| if index % 7 == 0 { -sample } else { sample })
322            .collect::<Vec<_>>();
323        let mut folded = WaveformSummary::new(2, 16, 3).expect("shape");
324        folded.push(&[&left, &right]).expect("block");
325        let folded = folded.finish();
326        for (level, bucket_samples) in [(1, 64), (2, 256)] {
327            let mut direct = WaveformSummary::new(2, bucket_samples, 1).expect("shape");
328            direct.push(&[&left, &right]).expect("block");
329            let direct = direct.finish();
330            assert_eq!(folded.levels[level].bucket_samples, bucket_samples);
331            for channel in 0..2 {
332                let ours = &folded.levels[level].channels[channel];
333                let theirs = &direct.levels[0].channels[channel];
334                assert_eq!(ours.len(), theirs.len(), "level {level}");
335                for (a, b) in ours.iter().zip(theirs) {
336                    assert_eq!(a.min.to_bits(), b.min.to_bits());
337                    assert_eq!(a.max.to_bits(), b.max.to_bits());
338                    // The sums of squares add in another order; in f64 that moves the f32 RMS by at
339                    // most one unit in the last place.
340                    assert!((a.rms - b.rms).abs() <= f32::EPSILON * b.rms, "{a:?} {b:?}");
341                }
342            }
343        }
344        // 10 000 samples in buckets of 16: 625 full buckets; 157 and 40 above.
345        assert_eq!(folded.levels[0].channels[0].len(), 625);
346        assert_eq!(folded.levels[1].channels[0].len(), 157);
347        assert_eq!(folded.levels[2].channels[0].len(), 40);
348    }
349
350    #[test]
351    fn block_boundaries_do_not_change_the_result() {
352        let signal = sine(5_000, 23, 0.9);
353        let whole = {
354            let mut summary = WaveformSummary::new(1, 30, 4).expect("shape");
355            summary.push(&[&signal]).expect("block");
356            summary.finish()
357        };
358        for block in [1, 7, 29, 128, 4_999] {
359            let mut summary = WaveformSummary::new(1, 30, 4).expect("shape");
360            for chunk in signal.chunks(block) {
361                summary.push(&[chunk]).expect("block");
362            }
363            assert_eq!(summary.finish(), whole, "block {block}");
364        }
365    }
366
367    #[test]
368    fn a_partial_last_bucket_covers_only_its_samples() {
369        let mut summary = WaveformSummary::new(1, 4, 2).expect("shape");
370        summary
371            .push(&[&[0.5, -0.5, 0.5, -0.5, 1.0, 1.0]])
372            .expect("block");
373        let waveform = summary.finish();
374        assert_eq!(
375            waveform.levels[0].channels[0],
376            vec![
377                WaveformBucket {
378                    min: -0.5,
379                    max: 0.5,
380                    rms: 0.5
381                },
382                WaveformBucket {
383                    min: 1.0,
384                    max: 1.0,
385                    rms: 1.0
386                },
387            ]
388        );
389        let top = waveform.levels[1].channels[0][0];
390        assert_eq!((top.min, top.max), (-0.5, 1.0));
391        // sqrt((4 * 0.25 + 2 * 1) / 6) = sqrt(0.5)
392        assert!((top.rms - 0.5_f32.sqrt()).abs() < 1e-7);
393    }
394
395    #[test]
396    fn a_non_finite_sample_counts_as_silence_at_every_level() {
397        // A mix and the loudness meter play a non-finite sample as silence, so the waveform does
398        // too rather than blanking the RMS of every bucket above it.
399        let samples = [
400            0.1,
401            f32::NAN,
402            0.2,
403            0.3,
404            f32::NAN,
405            f32::NAN,
406            f32::NAN,
407            f32::NAN,
408            0.1,
409            f32::INFINITY,
410            f32::NEG_INFINITY,
411            0.1,
412        ];
413        let mut summary = WaveformSummary::new(1, 4, 2).expect("shape");
414        summary.push(&[&samples]).expect("block");
415        let waveform = summary.finish();
416        let silenced = samples.map(|sample| if sample.is_finite() { sample } else { 0.0 });
417        let mut reference = WaveformSummary::new(1, 4, 2).expect("shape");
418        reference.push(&[&silenced]).expect("block");
419        assert_eq!(waveform, reference.finish());
420        for level in &waveform.levels {
421            for bucket in &level.channels[0] {
422                assert!(
423                    bucket.min.is_finite() && bucket.max.is_finite() && bucket.rms.is_finite(),
424                    "{bucket:?}"
425                );
426            }
427        }
428        // A bucket of nothing but NaN is a silent bucket, not the empty sentinel.
429        assert_eq!(
430            waveform.levels[0].channels[0][1],
431            WaveformBucket {
432                min: 0.0,
433                max: 0.0,
434                rms: 0.0
435            }
436        );
437    }
438
439    #[test]
440    fn shapes_and_blocks_are_checked() {
441        assert_eq!(
442            WaveformSummary::new(0, 1, 1).map(|_| ()),
443            Err(WaveformError::InvalidShape)
444        );
445        assert_eq!(
446            WaveformSummary::new(1, 0, 1).map(|_| ()),
447            Err(WaveformError::InvalidShape)
448        );
449        assert_eq!(
450            WaveformSummary::new(1, 1, 17).map(|_| ()),
451            Err(WaveformError::InvalidShape)
452        );
453        let mut summary = WaveformSummary::new(2, 8, 1).expect("shape");
454        assert_eq!(
455            summary.push(&[&[0.0; 3], &[0.0; 2]]),
456            Err(WaveformError::MismatchedBlock)
457        );
458        assert_eq!(
459            summary.push(&[&[0.0; 3]]),
460            Err(WaveformError::MismatchedBlock)
461        );
462    }
463}