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, ×tamp) 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}