aede_core/playback/
waveform.rs

1//! Progressive complete-track peak envelope, bounded independently of track length.
2use super::stream::PcmTrack;
3use std::convert::Infallible;
4use std::path::Path;
5use std::time::{Duration, Instant};
6/// Original-sample envelope, before playback normalization, volume or effects.
7pub struct Waveform {
8    /// Complete source duration derived from decoded frame count.
9    pub duration_ms: u64,
10    /// Equal frame buckets; the final bucket can be shorter.
11    pub peaks: Vec<f32>,
12    /// Complete source frames in each bucket except the last.
13    pub bucket_frames: u64,
14    /// Original source sampling rate.
15    pub sample_rate: u32,
16}
17struct Envelope {
18    peaks: Vec<f32>,
19    width: u64,
20    used: u64,
21    peak: f32,
22    frames: u64,
23}
24impl Envelope {
25    fn new() -> Self {
26        Self {
27            peaks: vec![],
28            width: 256,
29            used: 0,
30            peak: 0.,
31            frames: 0,
32        }
33    }
34    fn push(&mut self, peak: f32) {
35        self.frames += 1;
36        self.used += 1;
37        self.peak = self.peak.max(peak.abs().min(1.));
38        if self.used == self.width {
39            self.peaks.push(self.peak);
40            self.used = 0;
41            self.peak = 0.;
42            if self.peaks.len() == 2048 {
43                self.peaks = self
44                    .peaks
45                    .as_chunks::<2>()
46                    .0
47                    .iter()
48                    .map(|v| v[0].max(v[1]))
49                    .collect();
50                self.width *= 2;
51            }
52        }
53    }
54    fn finish(mut self, rate: u32) -> Waveform {
55        if self.used > 0 {
56            self.peaks.push(self.peak);
57        }
58        Waveform {
59            duration_ms: self.frames.saturating_mul(1000) / u64::from(rate),
60            peaks: self.peaks,
61            bucket_frames: self.width,
62            sample_rate: rate,
63        }
64    }
65}
66/// Decode an explicitly selected local source with bounded memory and a wall-clock budget.
67/// The caller must validate source identity before and after this read.
68pub fn decode(path: &Path, budget: Duration) -> Result<Waveform, String> {
69    let mut track = PcmTrack::open(path).map_err(|error| error.to_string())?;
70    let format = track.format();
71    let start = Instant::now();
72    let mut envelope = Envelope::new();
73    while let Some(block) = track
74        .read_block::<Infallible>(|_| Ok(()))
75        .map_err(|error| error.to_string())?
76    {
77        if start.elapsed() > budget || envelope.frames > u64::from(format.sample_rate()) * 86400 {
78            return Err("waveform decode budget exceeded".into());
79        }
80        for frame in block.samples.chunks_exact(usize::from(format.channels())) {
81            envelope.push(frame.iter().copied().map(f32::abs).fold(0., f32::max));
82        }
83    }
84    if envelope.frames == 0 {
85        return Err("empty audio source".into());
86    }
87    Ok(envelope.finish(format.sample_rate()))
88}
89#[cfg(test)]
90#[path = "waveform_tests.rs"]
91mod tests;