1use crate::media_time::rounding_quantum;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub struct AudioFrameGrid {
15 pub anchor: i64,
17 pub period: i64,
19 pub start_tolerance: i64,
22 pub trimmed_tolerance: i64,
25}
26
27impl AudioFrameGrid {
28 #[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#[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 let unit = i128::from(num) * i128::from(sample_rate);
91 let rounding = unit * i128::from(rounding_quantum(&sorted).max(1));
93 if rounding < 2 * den {
94 return None;
95 }
96 let trimmed_tolerance = (rounding + den).div_euclid(den);
98 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
123fn 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, ×tamp) 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 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 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 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 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 assert_eq!(grid.place(401 * 48, 648), 19_200);
190 assert_eq!(grid.place(0, 648), 0);
191 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 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 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 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}