Skip to main content

alpha2_time/
audio_grid.rs

1//! Where an audio stream's frames start when its container rounds their timestamps.
2
3use crate::media_time::rounding_quantum;
4
5/// Frame start samples of an audio stream, every `period` samples from `anchor`, inferred from
6/// its first packet timestamps ([`audio_frame_grid`]).
7///
8/// Containers that round timestamps (Matroska and `WebM` to the millisecond) place a frame up to
9/// half a unit (24 samples at 48 kHz) off its true start, and a seek and a straight decode are off
10/// by different amounts. For fixed-length frames (AAC's 1024 samples, Opus's 20 ms) the first
11/// timestamps together locate the grid far more precisely, so snapping to it makes a seek give
12/// the same samples as a straight decode.
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub struct AudioFrameGrid {
15    /// A source sample at which a frame starts.
16    pub anchor: i64,
17    /// Samples per frame.
18    pub period: i64,
19    /// Farthest a frame timestamp, in samples, can be from its true start: half the rounding
20    /// unit plus half a sample, rounded.
21    pub start_tolerance: i64,
22    /// Farthest the end of a trimmed frame (shorter than `period`, as after Opus pre-skip) can
23    /// be from the grid. The trim is rounded again, so this is a full unit plus a sample.
24    pub trimmed_tolerance: i64,
25}
26
27impl AudioFrameGrid {
28    /// Start of a `frames`-sample frame whose timestamp says `at`: snapped to the grid when
29    /// within tolerance, otherwise `at`.
30    ///
31    /// A frame shorter than [`Self::period`] is first tried as a trimmed frame ending on the grid.
32    #[must_use]
33    pub fn place(&self, at: i64, frames: i64) -> i64 {
34        if (1..self.period).contains(&frames) {
35            let end = at.saturating_add(frames);
36            let shift = self.shift_onto(end);
37            if shift.abs() <= self.trimmed_tolerance {
38                return at.saturating_add(shift);
39            }
40        }
41        let shift = self.shift_onto(at);
42        if shift.abs() <= self.start_tolerance {
43            return at.saturating_add(shift);
44        }
45        at
46    }
47
48    fn shift_onto(&self, position: i64) -> i64 {
49        let offset =
50            (i128::from(position) - i128::from(self.anchor)).rem_euclid(i128::from(self.period));
51        let shift = if offset * 2 <= i128::from(self.period) {
52            -offset
53        } else {
54            i128::from(self.period) - offset
55        };
56        i64::try_from(shift).unwrap_or(0)
57    }
58}
59
60/// The frame grid shown by an audio stream's first packet timestamps (any order, in `num / den`
61/// second units) at `sample_rate`, or `None`.
62///
63/// The rounding unit is [`rounding_quantum`], or one time-base unit when none shows. Every
64/// timestamp must lie within half a unit of the grid. Among whole-sample periods that fit, the one
65/// with the most slack wins (shortest on ties), and the anchor is the middle of its feasible range.
66///
67/// Returns `None` when half a unit is under a sample (timestamps are already exact), when fewer
68/// than three distinct timestamps are given or none fits (variable frame sizes, a missing packet),
69/// when no period exceeds twice the trimmed tolerance, or when an argument is not positive.
70#[must_use]
71pub fn audio_frame_grid(
72    timestamps: &[i64],
73    num: i64,
74    den: i64,
75    sample_rate: u32,
76) -> Option<AudioFrameGrid> {
77    if num <= 0 || den <= 0 || sample_rate == 0 {
78        return None;
79    }
80    let mut sorted = timestamps.to_vec();
81    sorted.sort_unstable();
82    sorted.dedup();
83    let (&first, &last) = (sorted.first()?, sorted.last()?);
84    let count = i128::try_from(sorted.len()).ok()?;
85    if count < 3 {
86        return None;
87    }
88    let den = i128::from(den);
89    // Samples per time-base unit, times `den`.
90    let unit = i128::from(num) * i128::from(sample_rate);
91    // Rounding unit in samples, times `den`.
92    let rounding = unit * i128::from(rounding_quantum(&sorted).max(1));
93    if rounding < 2 * den {
94        return None;
95    }
96    // Twice the trimmed tolerance must stay under the period for a timestamp to name one frame.
97    let trimmed_tolerance = (rounding + den).div_euclid(den);
98    // The span is off by up to a unit at either end, which bounds the candidate periods.
99    let span = (i128::from(last) - i128::from(first)) * unit;
100    let frames = count - 1;
101    let shortest = (span - rounding)
102        .div_euclid(den * frames)
103        .max(2 * trimmed_tolerance + 1);
104    let longest = (span + rounding).div_euclid(den * frames) + 1;
105    let mut best: Option<(i128, i128, i128)> = None;
106    for period in shortest..=longest {
107        if let Some((low, high)) = fit(&sorted, unit, rounding, den, period)
108            && best.is_none_or(|(_, best_low, best_high)| high - low > best_high - best_low)
109        {
110            best = Some((period, low, high));
111        }
112    }
113    let (period, low, high) = best?;
114    let anchor = (low + high + 2 * den).div_euclid(4 * den);
115    Some(AudioFrameGrid {
116        anchor: i64::try_from(anchor).ok()?,
117        period: i64::try_from(period).ok()?,
118        start_tolerance: i64::try_from((rounding + 2 * den).div_euclid(2 * den)).ok()?,
119        trimmed_tolerance: i64::try_from(trimmed_tolerance).ok()?,
120    })
121}
122
123/// Feasible first-frame starts (samples times `2 * den`) for a `period` grid within `rounding`
124/// of every timestamp, or `None`.
125fn fit(
126    sorted: &[i64],
127    unit: i128,
128    rounding: i128,
129    den: i128,
130    period: i128,
131) -> Option<(i128, i128)> {
132    let (mut low, mut high) = (i128::MIN, i128::MAX);
133    for (index, &timestamp) in sorted.iter().enumerate() {
134        let index = i128::try_from(index).ok()?;
135        let center = 2 * i128::from(timestamp) * unit - 2 * index * period * den;
136        low = low.max(center - rounding);
137        high = high.min(center + rounding);
138    }
139    (low <= high).then_some((low, high))
140}
141
142#[cfg(test)]
143mod tests {
144    use super::{AudioFrameGrid, audio_frame_grid};
145
146    /// Frame timestamps rounded to `1 / den` s, half away from zero like `FFmpeg`'s `av_rescale_q`.
147    fn rounded(start: i64, period: i64, rate: i64, den: i64, count: i64) -> Vec<i64> {
148        (0..count)
149            .map(|k| {
150                let scaled = 2 * (start + k * period) * den;
151                scaled.signum() * ((scaled.abs() + rate) / (2 * rate))
152            })
153            .collect()
154    }
155
156    #[test]
157    fn rounded_timestamps_show_where_their_frames_start() {
158        // AAC in Matroska with a priming frame at -1024; a straight decode would start at -1008.
159        let aac = rounded(-1_024, 1_024, 48_000, 1_000, 64);
160        assert_eq!(&aac[..5], [-21, 0, 21, 43, 64]);
161        let grid = audio_frame_grid(&aac, 1, 1_000, 48_000).expect("a grid");
162        assert_eq!(
163            grid,
164            AudioFrameGrid {
165                anchor: -1_024,
166                period: 1_024,
167                start_tolerance: 25,
168                trimmed_tolerance: 49,
169            }
170        );
171        // Opus in WebM with libopus's 312-sample pre-skip.
172        let opus = rounded(-312, 960, 48_000, 1_000, 64);
173        assert_eq!(&opus[..3], [-7, 14, 34]);
174        let grid = audio_frame_grid(&opus, 1, 1_000, 48_000).expect("a grid");
175        assert_eq!((grid.anchor, grid.period), (-312, 960));
176        // At 44.1 kHz a millisecond is 44.1 samples.
177        let aac = rounded(-1_024, 1_024, 44_100, 1_000, 64);
178        let grid = audio_frame_grid(&aac, 1, 1_000, 44_100).expect("a grid");
179        assert_eq!((grid.anchor, grid.period), (-1_024, 1_024));
180        assert_eq!((grid.start_tolerance, grid.trimmed_tolerance), (23, 45));
181    }
182
183    #[test]
184    fn a_first_frame_is_placed_on_the_grid() {
185        let grid = audio_frame_grid(&rounded(-312, 960, 48_000, 1_000, 64), 1, 1_000, 48_000)
186            .expect("a grid");
187        assert_eq!(grid.place(34 * 48, 960), 1_608);
188        // A post-seek frame trimmed by 312 samples: 393.5 ms + 6.5 ms, each rounded up, is 401 ms.
189        assert_eq!(grid.place(401 * 48, 648), 19_200);
190        assert_eq!(grid.place(0, 648), 0);
191        // Beyond tolerance, the timestamp is used as is.
192        assert_eq!(grid.place(1_608 + 26, 960), 1_634);
193        assert_eq!(grid.place(19_200 + 50, 648), 19_250);
194    }
195
196    #[test]
197    fn timestamps_that_place_frames_exactly_or_unevenly_show_no_grid() {
198        // Sample-rate (MP4) and 1/90000 (MPEG-TS) time bases are exact to half a sample.
199        let exact: Vec<i64> = (0..64).map(|k| k * 1_024).collect();
200        assert_eq!(audio_frame_grid(&exact, 1, 48_000, 48_000), None);
201        let ts: Vec<i64> = (0..64).map(|k| 126_000 + k * 1_920).collect();
202        assert_eq!(audio_frame_grid(&ts, 1, 90_000, 48_000), None);
203        // Vorbis in WebM: long and short blocks, 0, 3, 15, 36, 58 ms.
204        assert_eq!(
205            audio_frame_grid(&[0, 3, 15, 36, 58, 79, 100], 1, 1_000, 48_000),
206            None
207        );
208        let mut gap = rounded(0, 1_024, 48_000, 1_000, 64);
209        gap.remove(10);
210        assert_eq!(audio_frame_grid(&gap, 1, 1_000, 48_000), None);
211        assert_eq!(audio_frame_grid(&[0, 21], 1, 1_000, 48_000), None);
212        assert_eq!(
213            audio_frame_grid(&rounded(0, 1_024, 48_000, 50, 64), 1, 50, 48_000),
214            None
215        );
216        assert_eq!(audio_frame_grid(&[0, 21, 43], 0, 1_000, 48_000), None);
217        assert_eq!(audio_frame_grid(&[0, 21, 43], 1, 1_000, 0), None);
218    }
219
220    #[test]
221    fn timestamps_that_keep_milliseconds_in_a_finer_time_base_show_their_grid() {
222        // Matroska's milliseconds copied into a 1/48000 time base: multiples of 48.
223        let copied: Vec<i64> = rounded(-1_024, 1_024, 48_000, 1_000, 64)
224            .into_iter()
225            .map(|ms| ms * 48)
226            .collect();
227        let grid = audio_frame_grid(&copied, 1, 48_000, 48_000).expect("a grid");
228        assert_eq!((grid.anchor, grid.period), (-1_024, 1_024));
229    }
230}