Skip to main content

alpha2_time/
audio_grid.rs

1//! Where an audio stream's frames start when its container rounds their
2//! timestamps.
3
4use crate::media_time::rounding_quantum;
5
6/// The source samples an audio stream's frames start at, read from its first
7/// packets' timestamps ([`audio_frame_grid`]): every `period` samples from
8/// `anchor`, at the stream's rate.
9///
10/// A container that rounds timestamps (Matroska and `WebM` to the millisecond)
11/// stores a frame up to half its unit from where the frame starts, so the
12/// timestamp alone says where a frame starts only to that half unit, which
13/// is 24 samples at 48 kHz. A decode straight through places each frame
14/// where the one before it ended and is off by whatever the first timestamp
15/// was; a seek places the first frame after it by that frame's own timestamp
16/// and is off by another amount. A codec whose frames are all one length
17/// (AAC's 1024 samples, Opus's 20 ms) puts every frame on one grid, and the
18/// first timestamps together say where it lies far more closely than any
19/// one of them: placing a first frame on it gives the samples a decode
20/// straight through gives, however it is reached.
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub struct AudioFrameGrid {
23    /// A source sample, at the stream's rate, that a frame starts at.
24    pub anchor: i64,
25    /// The samples in every frame.
26    pub period: i64,
27    /// The farthest a frame's timestamp, as a sample, is from the start it
28    /// stands for: half the rounding unit and the half sample of rounding it
29    /// to a sample, rounded down to a whole sample after adding one.
30    pub start_tolerance: i64,
31    /// The farthest the end of a frame shorter than `period` is from the
32    /// grid: a decoder that drops a codec's delay from the first frame after
33    /// a start or a seek (Opus's pre-skip) moves its timestamp by the
34    /// samples dropped, rounded to the unit again, so twice the half unit
35    /// and a sample.
36    pub trimmed_tolerance: i64,
37}
38
39impl AudioFrameGrid {
40    /// Where a frame of `frames` samples whose timestamp puts it at source
41    /// sample `at` starts: on the grid when its timestamp is within the
42    /// grid's rounding of it, otherwise at `at`.
43    ///
44    /// A frame shorter than [`Self::period`] is first taken as the end of a
45    /// frame a decoder dropped the start of (Opus's pre-skip, after a start
46    /// or a seek): it ends on the grid. Otherwise a frame starts on it.
47    #[must_use]
48    pub fn place(&self, at: i64, frames: i64) -> i64 {
49        if (1..self.period).contains(&frames) {
50            let end = at.saturating_add(frames);
51            let shift = self.shift_onto(end);
52            if shift.abs() <= self.trimmed_tolerance {
53                return at.saturating_add(shift);
54            }
55        }
56        let shift = self.shift_onto(at);
57        if shift.abs() <= self.start_tolerance {
58            return at.saturating_add(shift);
59        }
60        at
61    }
62
63    /// What moves `position` onto the nearest grid sample.
64    fn shift_onto(&self, position: i64) -> i64 {
65        let offset =
66            (i128::from(position) - i128::from(self.anchor)).rem_euclid(i128::from(self.period));
67        let shift = if offset * 2 <= i128::from(self.period) {
68            -offset
69        } else {
70            i128::from(self.period) - offset
71        };
72        i64::try_from(shift).unwrap_or(0)
73    }
74}
75
76/// The frame grid an audio stream's first packet timestamps show, at
77/// `sample_rate`, in a stream whose timestamps are whole multiples of
78/// `num / den` seconds; `None` when they show none to place frames on.
79///
80/// `timestamps` are the stream's first packets' presentation timestamps in
81/// any order: its first [`crate::ROUNDING_SCAN_FRAMES`] packets', fewer when
82/// those before hold [`crate::ROUNDING_SCAN_BYTES`], as for
83/// [`rounding_quantum`]. Their rounding unit is that one's quantum, or one
84/// unit of the time base when they show none. The grid is one every
85/// timestamp is within half a unit of: of the whole-sample periods within a
86/// unit over their count of their span over it, the one that leaves the most
87/// room, the shortest of those that leave as much; and of the anchors it
88/// leaves, the one at the middle of where they can lie, rounded to a sample,
89/// which for a rounding to the nearest unit is where the frames are. There
90/// is none when:
91///
92/// - half the unit is under a sample (an MP4's time base is usually the
93///   sample rate, MPEG-TS's 1/90000): a timestamp is then where its frame
94///   starts;
95/// - fewer than three distinct timestamps are given, or no grid holds them
96///   all: the frames are not one length (Vorbis's long and short blocks, an
97///   Opus stream that changes frame size), or a packet is missing;
98/// - no period longer than twice the trimmed tolerance holds them, so a
99///   timestamp could stand for either of two frames;
100/// - `num`, `den` or `sample_rate` is not positive.
101#[must_use]
102pub fn audio_frame_grid(
103    timestamps: &[i64],
104    num: i64,
105    den: i64,
106    sample_rate: u32,
107) -> Option<AudioFrameGrid> {
108    if num <= 0 || den <= 0 || sample_rate == 0 {
109        return None;
110    }
111    let mut sorted = timestamps.to_vec();
112    sorted.sort_unstable();
113    sorted.dedup();
114    let (&first, &last) = (sorted.first()?, sorted.last()?);
115    let count = i128::try_from(sorted.len()).ok()?;
116    if count < 3 {
117        return None;
118    }
119    let den = i128::from(den);
120    // Samples of a time-base unit, times `den`.
121    let unit = i128::from(num) * i128::from(sample_rate);
122    // The rounding unit in samples, times `den`: twice the half unit.
123    let rounding = unit * i128::from(rounding_quantum(&sorted).max(1));
124    if rounding < 2 * den {
125        return None;
126    }
127    // Twice the trimmed tolerance must stay under the period for a timestamp to name one frame.
128    let trimmed_tolerance = (rounding + den).div_euclid(den);
129    // The span is off by up to a unit at either end, so the period is within a unit over the
130    // frames of the span over them. Of the lengths there, the grid is the one that holds every
131    // timestamp with the most room, the shortest of those with as much.
132    let span = (i128::from(last) - i128::from(first)) * unit;
133    let frames = count - 1;
134    let shortest = (span - rounding)
135        .div_euclid(den * frames)
136        .max(2 * trimmed_tolerance + 1);
137    let longest = (span + rounding).div_euclid(den * frames) + 1;
138    let mut best: Option<(i128, i128, i128)> = None;
139    for period in shortest..=longest {
140        if let Some((low, high)) = fit(&sorted, unit, rounding, den, period)
141            && best.is_none_or(|(_, best_low, best_high)| high - low > best_high - best_low)
142        {
143            best = Some((period, low, high));
144        }
145    }
146    let (period, low, high) = best?;
147    let anchor = (low + high + 2 * den).div_euclid(4 * den);
148    Some(AudioFrameGrid {
149        anchor: i64::try_from(anchor).ok()?,
150        period: i64::try_from(period).ok()?,
151        start_tolerance: i64::try_from((rounding + 2 * den).div_euclid(2 * den)).ok()?,
152        trimmed_tolerance: i64::try_from(trimmed_tolerance).ok()?,
153    })
154}
155
156/// Where the first frame of a grid of `period` samples that holds every one
157/// of `sorted` can start, in samples times `2 * den`: each timestamp, less
158/// the frames before it, within `rounding` (half a unit times `2 * den`) of
159/// it. `None` when no start holds them all.
160fn fit(
161    sorted: &[i64],
162    unit: i128,
163    rounding: i128,
164    den: i128,
165    period: i128,
166) -> Option<(i128, i128)> {
167    let (mut low, mut high) = (i128::MIN, i128::MAX);
168    for (index, &timestamp) in sorted.iter().enumerate() {
169        let index = i128::try_from(index).ok()?;
170        let center = 2 * i128::from(timestamp) * unit - 2 * index * period * den;
171        low = low.max(center - rounding);
172        high = high.min(center + rounding);
173    }
174    (low <= high).then_some((low, high))
175}
176
177#[cfg(test)]
178mod tests {
179    use super::{AudioFrameGrid, audio_frame_grid};
180
181    /// Timestamps of `count` frames of `period` samples at `rate` from sample
182    /// `start`, rounded to the nearest `1 / den` s, a half away from zero
183    /// (as `FFmpeg`'s `av_rescale_q` rounds).
184    fn rounded(start: i64, period: i64, rate: i64, den: i64, count: i64) -> Vec<i64> {
185        (0..count)
186            .map(|k| {
187                let scaled = 2 * (start + k * period) * den;
188                scaled.signum() * ((scaled.abs() + rate) / (2 * rate))
189            })
190            .collect()
191    }
192
193    #[test]
194    fn rounded_timestamps_show_where_their_frames_start() {
195        // AAC in Matroska: 1024-sample frames from -1024 (the priming frame), stored at -21, 0, 21,
196        // 43, 64 ms. A decode straight through would place every frame 16 samples late
197        // (from -1008).
198        let aac = rounded(-1_024, 1_024, 48_000, 1_000, 64);
199        assert_eq!(&aac[..5], [-21, 0, 21, 43, 64]);
200        let grid = audio_frame_grid(&aac, 1, 1_000, 48_000).expect("a grid");
201        assert_eq!(
202            grid,
203            AudioFrameGrid {
204                anchor: -1_024,
205                period: 1_024,
206                start_tolerance: 25,
207                trimmed_tolerance: 49,
208            }
209        );
210        // Opus in WebM, libopus's 312-sample pre-skip: 960-sample packets from -312 at -7, 14,
211        // 34 ms.
212        let opus = rounded(-312, 960, 48_000, 1_000, 64);
213        assert_eq!(&opus[..3], [-7, 14, 34]);
214        let grid = audio_frame_grid(&opus, 1, 1_000, 48_000).expect("a grid");
215        assert_eq!((grid.anchor, grid.period), (-312, 960));
216        // At 44.1 kHz a millisecond is 44.1 samples.
217        let aac = rounded(-1_024, 1_024, 44_100, 1_000, 64);
218        let grid = audio_frame_grid(&aac, 1, 1_000, 44_100).expect("a grid");
219        assert_eq!((grid.anchor, grid.period), (-1_024, 1_024));
220        assert_eq!((grid.start_tolerance, grid.trimmed_tolerance), (23, 45));
221    }
222
223    #[test]
224    fn a_first_frame_is_placed_on_the_grid() {
225        let grid = audio_frame_grid(&rounded(-312, 960, 48_000, 1_000, 64), 1, 1_000, 48_000)
226            .expect("a grid");
227        assert_eq!(grid.place(34 * 48, 960), 1_608);
228        // The first frame after a seek that the decoder dropped 312 samples of: its packet at
229        // 394 ms (18 888 samples, 393.5 ms) and the 6.5 ms it dropped, both rounded up, put it at
230        // 401 ms.
231        assert_eq!(grid.place(401 * 48, 648), 19_200);
232        // The stream's own first frame, the same 312 samples dropped: at 0, where it ends on 648.
233        assert_eq!(grid.place(0, 648), 0);
234        // A timestamp farther from the grid than its rounding is where the frame is.
235        assert_eq!(grid.place(1_608 + 26, 960), 1_634);
236        assert_eq!(grid.place(19_200 + 50, 648), 19_250);
237    }
238
239    #[test]
240    fn timestamps_that_place_frames_exactly_or_unevenly_show_no_grid() {
241        // A time base of the sample rate (MP4) and MPEG-TS's 1/90000 put a frame within half a
242        // sample.
243        let exact: Vec<i64> = (0..64).map(|k| k * 1_024).collect();
244        assert_eq!(audio_frame_grid(&exact, 1, 48_000, 48_000), None);
245        let ts: Vec<i64> = (0..64).map(|k| 126_000 + k * 1_920).collect();
246        assert_eq!(audio_frame_grid(&ts, 1, 90_000, 48_000), None);
247        // Vorbis in WebM: long and short blocks, 0, 3, 15, 36, 58 ms.
248        assert_eq!(
249            audio_frame_grid(&[0, 3, 15, 36, 58, 79, 100], 1, 1_000, 48_000),
250            None
251        );
252        let mut gap = rounded(0, 1_024, 48_000, 1_000, 64);
253        gap.remove(10);
254        assert_eq!(audio_frame_grid(&gap, 1, 1_000, 48_000), None);
255        // Two timestamps say nothing of a grid, and a unit near a frame's length could name two.
256        assert_eq!(audio_frame_grid(&[0, 21], 1, 1_000, 48_000), None);
257        assert_eq!(
258            audio_frame_grid(&rounded(0, 1_024, 48_000, 50, 64), 1, 50, 48_000),
259            None
260        );
261        assert_eq!(audio_frame_grid(&[0, 21, 43], 0, 1_000, 48_000), None);
262        assert_eq!(audio_frame_grid(&[0, 21, 43], 1, 1_000, 0), None);
263    }
264
265    #[test]
266    fn timestamps_that_keep_milliseconds_in_a_finer_time_base_show_their_grid() {
267        // Matroska's milliseconds copied into a 1/48000 time base: multiples of 48.
268        let copied: Vec<i64> = rounded(-1_024, 1_024, 48_000, 1_000, 64)
269            .into_iter()
270            .map(|ms| ms * 48)
271            .collect();
272        let grid = audio_frame_grid(&copied, 1, 48_000, 48_000).expect("a grid");
273        assert_eq!((grid.anchor, grid.period), (-1_024, 1_024));
274    }
275}