Skip to main content

audiobox/analyze/
measurements.rs

1//! Offline, non-mutating measurements for numerical render review.
2use crate::{Audio, Error, Limits, Result};
3use std::ops::Range;
4
5/// Maximum requests in one [`Audio::measure_boundaries`] call.
6pub const MAX_MEASUREMENT_BOUNDARIES: usize = 4096;
7
8/// Frame-exact leading/trailing silence under a caller's linear amplitude threshold.
9#[derive(Clone, Debug, Eq, PartialEq)]
10pub struct ThresholdSilence {
11    pub frames: usize,
12    /// Frames before the first frame where any channel exceeds the threshold.
13    pub leading_frames: usize,
14    /// Frames after the last frame where any channel exceeds the threshold.
15    pub trailing_frames: usize,
16    /// First through last active frame, half-open. None for empty/wholly quiet PCM.
17    /// For wholly quiet PCM both counts equal frames; they overlap, not add.
18    pub active_range: Option<Range<usize>>,
19}
20
21/// Unweighted stereo statistics. Power units are squared input-sample amplitude.
22/// Empty input has no power result; silence has Some(0). No width/quality verdict.
23#[derive(Clone, Copy, Debug, PartialEq)]
24pub struct StereoMeasurements {
25    pub frames: usize,
26    /// Mean-centered Pearson correlation in [-1, 1]. None for fewer than two
27    /// frames or either constant channel, including silence. DC is removed only
28    /// for this statistic; all four power measurements retain DC.
29    pub correlation: Option<f64>,
30    pub left_mean_square: Option<f64>,
31    pub right_mean_square: Option<f64>,
32    /// Mean square of M=(L+R)/2; no sqrt(2) normalization or loudness weighting.
33    pub mid_mean_square: Option<f64>,
34    /// Mean square of S=(L-R)/2, with the same scaling as mid.
35    pub side_mean_square: Option<f64>,
36}
37
38/// An explicitly requested adjacent-sample join; no automatic transient search.
39#[derive(Clone, Copy, Debug, Eq, PartialEq)]
40pub enum MeasurementBoundary {
41    /// Between frame-1 and frame. Requires 0 < frame < Audio::frames().
42    Frame(usize),
43    /// Last frame to frame zero. Fewer than two frames gives no measurement.
44    LoopSeam,
45}
46/// Signed step in input-sample amplitude units, preserving headroom outside +/-1.
47#[derive(Clone, Copy, Debug, PartialEq)]
48pub struct ChannelStep {
49    pub before: f64,
50    pub after: f64,
51    /// after - before; this number alone cannot establish an audible defect.
52    pub difference: f64,
53}
54#[derive(Clone, Debug, PartialEq)]
55pub struct BoundarySamples {
56    pub before_frame: usize,
57    pub after_frame: usize,
58    /// One entry per input channel, in original order.
59    pub channels: Vec<ChannelStep>,
60    pub max_abs_step: f64,
61}
62#[derive(Clone, Debug, PartialEq)]
63pub struct BoundaryMeasurement {
64    pub boundary: MeasurementBoundary,
65    /// None only for an empty/one-frame LoopSeam, without synthetic zero padding.
66    pub samples: Option<BoundarySamples>,
67}
68
69// Compensated f64 accumulation retains small terms when positive/negative samples
70// cancel. Finite f32 samples, differences and their squared sums fit in f64 for
71// any addressable slice; no platform logarithms/trigonometry are needed.
72#[derive(Default)]
73struct Sum {
74    sum: f64,
75    correction: f64,
76}
77impl Sum {
78    fn add(&mut self, value: f64) {
79        let next = self.sum + value;
80        self.correction += if self.sum.abs() >= value.abs() {
81            (self.sum - next) + value
82        } else {
83            (value - next) + self.sum
84        };
85        self.sum = next;
86    }
87    fn value(&self) -> f64 {
88        self.sum + self.correction
89    }
90}
91fn finite(sample: f32) -> Result<f64> {
92    if sample.is_finite() {
93        Ok(sample as f64)
94    } else {
95        Err(Error::invalid("Measurement samples must be finite"))
96    }
97}
98fn reserve<T>(count: usize) -> Result<Vec<T>> {
99    let mut values = Vec::new();
100    values
101        .try_reserve_exact(count)
102        .map_err(|_| Error::limit("Cannot allocate measurement output"))?;
103    Ok(values)
104}
105/// Arithmetic DC mean of one channel, in signed input-sample amplitude units.
106/// Empty input returns None; silence returns Some(0). All samples must be finite.
107/// Compensated f64 summation, O(samples) time and O(1) storage; no allocation.
108/// This is numerical reporting, not an extension of the rendered-PCM bit contract.
109pub fn dc_mean(samples: &[f32]) -> Result<Option<f64>> {
110    let mut sum = Sum::default();
111    for &sample in samples {
112        sum.add(finite(sample)?);
113    }
114    Ok((!samples.is_empty()).then(|| sum.value() / samples.len() as f64))
115}
116/// Statistics of equal-length stereo channels; no allocation or PCM modification.
117/// Requires finite samples, including constant/silent inputs. A two-pass,
118/// compensated f64 scan avoids raw-moment variance cancellation under large DC.
119/// Power is the same unweighted mean-square concept as [`Audio::mean_square`];
120/// accumulation order/compensation can change last bits. Pearson uses platform
121/// sqrt and is report-only; compare results numerically, not as portable bytes.
122pub fn stereo_measurements(left: &[f32], right: &[f32]) -> Result<StereoMeasurements> {
123    if left.len() != right.len() {
124        return Err(Error::invalid("Stereo channels must have equal lengths"));
125    }
126    let mut out = StereoMeasurements {
127        frames: left.len(),
128        correlation: None,
129        left_mean_square: None,
130        right_mean_square: None,
131        mid_mean_square: None,
132        side_mean_square: None,
133    };
134    let l_mean = dc_mean(left)?;
135    let r_mean = dc_mean(right)?;
136    let (Some(l_mean), Some(r_mean)) = (l_mean, r_mean) else {
137        return Ok(out);
138    };
139    let [mut lp, mut rp, mut mp, mut sp, mut lv, mut rv, mut cov] =
140        std::array::from_fn::<_, 7, _>(|_| Sum::default());
141    let (mut l_varies, mut r_varies) = (false, false);
142    for (&l, &r) in left.iter().zip(right) {
143        l_varies |= l != left[0];
144        r_varies |= r != right[0];
145        let (l, r) = (l as f64, r as f64);
146        let (m, s) = ((l + r) * 0.5, (l - r) * 0.5);
147        let (ld, rd) = (l - l_mean, r - r_mean);
148        lp.add(l * l);
149        rp.add(r * r);
150        mp.add(m * m);
151        sp.add(s * s);
152        lv.add(ld * ld);
153        rv.add(rd * rd);
154        cov.add(ld * rd);
155    }
156    let n = left.len() as f64;
157    out.left_mean_square = Some(lp.value() / n);
158    out.right_mean_square = Some(rp.value() / n);
159    out.mid_mean_square = Some(mp.value() / n);
160    out.side_mean_square = Some(sp.value() / n);
161    if left.len() >= 2 && l_varies && r_varies {
162        let denominator = lv.value().sqrt() * rv.value().sqrt();
163        if denominator > 0.0 {
164            out.correlation = Some((cov.value() / denominator).clamp(-1.0, 1.0));
165        }
166    }
167    Ok(out)
168}
169
170impl Audio {
171    /// Per-channel [`dc_mean`], with None for each empty channel. Allocates at
172    /// most one Vec of channel-count options, not a PCM copy. Default Limits
173    /// apply, including to input decoded with custom larger limits.
174    pub fn dc_means(&self) -> Result<Vec<Option<f64>>> {
175        Limits::default().check(self.frames(), self.channels(), self.rate)?;
176        let mut values = reserve(self.channels())?;
177        for channel in &self.data {
178            values.push(dc_mean(channel)?);
179        }
180        Ok(values)
181    }
182    /// Frame-exact threshold silence: a frame is quiet iff every channel's
183    /// absolute sample is <= threshold (linear input units, finite and >=0).
184    /// Checks all samples, including inside the active range. Empty input has
185    /// zero counts; wholly quiet input has two overlapping full-length counts.
186    /// This is independent of detect_silence's window-RMS/min-duration policy.
187    /// Default Limits apply. O(samples) time, O(1) storage, no allocation.
188    pub fn threshold_silence(&self, threshold: f64) -> Result<ThresholdSilence> {
189        if !threshold.is_finite() || threshold < 0.0 {
190            return Err(Error::invalid(
191                "Silence threshold must be finite and nonnegative",
192            ));
193        }
194        Limits::default().check(self.frames(), self.channels(), self.rate)?;
195        let (mut first, mut end) = (self.frames(), 0);
196        for channel in &self.data {
197            for (i, &sample) in channel.iter().enumerate() {
198                if finite(sample)?.abs() > threshold {
199                    first = first.min(i);
200                    end = end.max(i + 1);
201                }
202            }
203        }
204        let active_range = (end > 0).then_some(first..end);
205        Ok(ThresholdSilence {
206            frames: self.frames(),
207            leading_frames: first,
208            trailing_frames: if end > 0 {
209                self.frames() - end
210            } else {
211                self.frames()
212            },
213            active_range,
214        })
215    }
216    /// [`stereo_measurements`] of this Audio, requiring exactly two channels.
217    /// No downmixing, channel selection or allocation. Default Limits apply.
218    pub fn stereo_measurements(&self) -> Result<StereoMeasurements> {
219        if self.channels() != 2 {
220            return Err(Error::invalid("Stereo measurements require two channels"));
221        }
222        Limits::default().check(self.frames(), self.channels(), self.rate)?;
223        stereo_measurements(&self.data[0], &self.data[1])
224    }
225    /// Measure explicit joins, in request order (including duplicates), without
226    /// changing samples/metadata or labeling steps as audible clicks. All ranges
227    /// and finite samples are checked before allocating output. Frame boundaries
228    /// must have two existing neighbors; LoopSeam yields None below two frames.
229    ///
230    /// Default Limits cap input shape, including 64 channels. At most
231    /// MAX_MEASUREMENT_BOUNDARIES requests (4,096), each with one bounded channel
232    /// Vec; allocation failure/oversize returns LimitExceeded. Empty requests
233    /// still validate PCM and return an empty Vec. Time O(samples+requests*channels),
234    /// storage O(requests*channels), with no per-frame PCM copy/window scan.
235    pub fn measure_boundaries(
236        &self,
237        boundaries: &[MeasurementBoundary],
238    ) -> Result<Vec<BoundaryMeasurement>> {
239        if boundaries.len() > MAX_MEASUREMENT_BOUNDARIES {
240            return Err(Error::limit("Too many measurement boundaries"));
241        }
242        Limits::default().check(self.frames(), self.channels(), self.rate)?;
243        for boundary in boundaries {
244            if let MeasurementBoundary::Frame(frame) = *boundary {
245                if frame == 0 || frame >= self.frames() {
246                    return Err(Error::invalid("Boundary must lie between existing frames"));
247                }
248            }
249        }
250        for &sample in self.data.iter().flatten() {
251            finite(sample)?;
252        }
253        let mut out = reserve(boundaries.len())?;
254        for &boundary in boundaries {
255            let frames = match boundary {
256                MeasurementBoundary::Frame(frame) => Some((frame - 1, frame)),
257                MeasurementBoundary::LoopSeam if self.frames() >= 2 => Some((self.frames() - 1, 0)),
258                MeasurementBoundary::LoopSeam => None,
259            };
260            let samples = if let Some((before_frame, after_frame)) = frames {
261                let mut channels = reserve(self.channels())?;
262                let mut max_abs_step = 0.0f64;
263                for channel in &self.data {
264                    let before = channel[before_frame] as f64;
265                    let after = channel[after_frame] as f64;
266                    let difference = after - before;
267                    max_abs_step = max_abs_step.max(difference.abs());
268                    channels.push(ChannelStep {
269                        before,
270                        after,
271                        difference,
272                    });
273                }
274                Some(BoundarySamples {
275                    before_frame,
276                    after_frame,
277                    channels,
278                    max_abs_step,
279                })
280            } else {
281                None
282            };
283            out.push(BoundaryMeasurement { boundary, samples });
284        }
285        Ok(out)
286    }
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292    #[test]
293    fn defensive_audio_scans_reject_nonfinite_samples_even_without_boundaries() {
294        for value in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
295            let a = Audio::new(vec![vec![0.0; 3]; 2], 48000)
296                .unwrap()
297                .with_data(vec![vec![0.0, 0.0, 0.0], vec![0.0, value, 0.0]]);
298            assert_eq!(
299                a.dc_means().unwrap_err().code,
300                crate::ErrorCode::InvalidArgument
301            );
302            assert_eq!(
303                a.threshold_silence(0.0).unwrap_err().code,
304                crate::ErrorCode::InvalidArgument
305            );
306            assert_eq!(
307                a.stereo_measurements().unwrap_err().code,
308                crate::ErrorCode::InvalidArgument
309            );
310            assert_eq!(
311                a.measure_boundaries(&[]).unwrap_err().code,
312                crate::ErrorCode::InvalidArgument
313            );
314            assert_eq!(
315                a.measure_boundaries(&[MeasurementBoundary::LoopSeam])
316                    .unwrap_err()
317                    .code,
318                crate::ErrorCode::InvalidArgument
319            );
320        }
321    }
322}