1use crate::{Error, ErrorKind, Result};
2use std::cmp::Ordering;
3
4#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
6pub struct Rational {
7 numerator: i64,
8 denominator: i64,
9}
10
11impl Rational {
12 pub fn new(numerator: i64, denominator: i64) -> Result<Self> {
13 Self::from_wide(i128::from(numerator), i128::from(denominator))
14 }
15
16 pub const fn numerator(self) -> i64 {
17 self.numerator
18 }
19
20 pub const fn denominator(self) -> i64 {
21 self.denominator
22 }
23
24 pub const fn is_positive(self) -> bool {
25 self.numerator > 0
26 }
27
28 pub fn checked_add(self, other: Self) -> Result<Self> {
29 let numerator = i128::from(self.numerator) * i128::from(other.denominator)
30 + i128::from(other.numerator) * i128::from(self.denominator);
31 let denominator = i128::from(self.denominator) * i128::from(other.denominator);
32 Self::from_wide(numerator, denominator)
33 }
34
35 pub fn checked_sub(self, other: Self) -> Result<Self> {
36 let numerator = i128::from(self.numerator) * i128::from(other.denominator)
37 - i128::from(other.numerator) * i128::from(self.denominator);
38 let denominator = i128::from(self.denominator) * i128::from(other.denominator);
39 Self::from_wide(numerator, denominator)
40 }
41
42 pub fn checked_mul(self, other: Self) -> Result<Self> {
43 Self::from_wide(
44 i128::from(self.numerator) * i128::from(other.numerator),
45 i128::from(self.denominator) * i128::from(other.denominator),
46 )
47 }
48
49 pub fn checked_div(self, other: Self) -> Result<Self> {
50 if other.numerator == 0 {
51 return Err(invalid_timeline("cannot divide by zero"));
52 }
53 Self::from_wide(
54 i128::from(self.numerator) * i128::from(other.denominator),
55 i128::from(self.denominator) * i128::from(other.numerator),
56 )
57 }
58
59 fn from_wide(mut numerator: i128, mut denominator: i128) -> Result<Self> {
60 if denominator == 0 {
61 return Err(invalid_timeline("a rational denominator cannot be zero"));
62 }
63 if numerator == 0 {
64 return Ok(Self {
65 numerator: 0,
66 denominator: 1,
67 });
68 }
69 if denominator < 0 {
70 numerator = numerator
71 .checked_neg()
72 .ok_or_else(|| timeline_overflow("normalizing a rational"))?;
73 denominator = denominator
74 .checked_neg()
75 .ok_or_else(|| timeline_overflow("normalizing a rational"))?;
76 }
77
78 let divisor = gcd(numerator.unsigned_abs(), denominator.unsigned_abs());
79 let numerator = numerator / divisor as i128;
80 let denominator = denominator / divisor as i128;
81 Ok(Self {
82 numerator: i64::try_from(numerator)
83 .map_err(|_| timeline_overflow("storing a rational numerator"))?,
84 denominator: i64::try_from(denominator)
85 .map_err(|_| timeline_overflow("storing a rational denominator"))?,
86 })
87 }
88}
89
90impl Ord for Rational {
91 fn cmp(&self, other: &Self) -> Ordering {
92 (i128::from(self.numerator) * i128::from(other.denominator))
93 .cmp(&(i128::from(other.numerator) * i128::from(self.denominator)))
94 }
95}
96
97impl PartialOrd for Rational {
98 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
99 Some(self.cmp(other))
100 }
101}
102
103#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
105pub struct FrameIndex(pub u64);
106
107#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
109pub struct FrameRate(Rational);
110
111impl FrameRate {
112 pub fn new(numerator: u32, denominator: u32) -> Result<Self> {
113 let value = Rational::new(i64::from(numerator), i64::from(denominator))?;
114 if !value.is_positive() {
115 return Err(invalid_timeline("a frame rate must be positive"));
116 }
117 Ok(Self(value))
118 }
119
120 pub const fn as_rational(self) -> Rational {
121 self.0
122 }
123}
124
125#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
127pub struct SampleRange {
128 pub start: u64,
129 pub end: u64,
130}
131
132impl SampleRange {
133 pub fn new(start: u64, end: u64) -> Result<Self> {
134 if end < start {
135 return Err(invalid_timeline(
136 "a sample range cannot end before it starts",
137 ));
138 }
139 Ok(Self { start, end })
140 }
141
142 pub const fn len(self) -> u64 {
143 self.end - self.start
144 }
145
146 pub const fn is_empty(self) -> bool {
147 self.start == self.end
148 }
149}
150
151#[derive(Clone, Copy, Debug, Eq, PartialEq)]
153pub struct Timeline {
154 frame_rate: FrameRate,
155 audio_sample_rate: u32,
156}
157
158impl Timeline {
159 pub fn new(frame_rate: FrameRate, audio_sample_rate: u32) -> Result<Self> {
160 if audio_sample_rate == 0 {
161 return Err(invalid_timeline("an audio sample rate must be positive"));
162 }
163 Ok(Self {
164 frame_rate,
165 audio_sample_rate,
166 })
167 }
168
169 pub const fn frame_rate(self) -> FrameRate {
170 self.frame_rate
171 }
172
173 pub const fn audio_sample_rate(self) -> u32 {
174 self.audio_sample_rate
175 }
176
177 pub fn audio_interval_for_frame(self, frame: FrameIndex) -> Result<SampleRange> {
183 let next_frame = frame
184 .0
185 .checked_add(1)
186 .ok_or_else(|| timeline_overflow("advancing a frame index"))?;
187 let rate = self.frame_rate.as_rational();
188 let numerator = u128::try_from(rate.numerator())
189 .map_err(|_| invalid_timeline("a frame rate must be positive"))?;
190 let denominator = u128::try_from(rate.denominator())
191 .map_err(|_| invalid_timeline("a frame-rate denominator must be positive"))?;
192 let samples = u128::from(self.audio_sample_rate);
193
194 let boundary = |index: u64| -> Result<u64> {
195 let value = u128::from(index)
196 .checked_mul(samples)
197 .and_then(|value| value.checked_mul(denominator))
198 .ok_or_else(|| timeline_overflow("mapping a frame to audio samples"))?
199 / numerator;
200 u64::try_from(value).map_err(|_| timeline_overflow("storing an audio sample boundary"))
201 };
202
203 SampleRange::new(boundary(frame.0)?, boundary(next_frame)?)
204 }
205
206 pub fn frame_for_audio_sample(self, sample: u64) -> Result<FrameIndex> {
212 let rate = self.frame_rate.as_rational();
213 let numerator = u128::try_from(rate.numerator())
214 .map_err(|_| invalid_timeline("a frame rate must be positive"))?;
215 let denominator = u128::try_from(rate.denominator())
216 .map_err(|_| invalid_timeline("a frame-rate denominator must be positive"))?;
217 let divisor = u128::from(self.audio_sample_rate)
218 .checked_mul(denominator)
219 .ok_or_else(|| timeline_overflow("mapping an audio sample to a frame"))?;
220 let frame = u128::from(sample)
221 .checked_add(1)
222 .and_then(|value| value.checked_mul(numerator))
223 .and_then(|value| value.checked_sub(1))
224 .ok_or_else(|| timeline_overflow("mapping an audio sample to a frame"))?
225 / divisor;
226 Ok(FrameIndex(
227 u64::try_from(frame).map_err(|_| timeline_overflow("storing a frame index"))?,
228 ))
229 }
230}
231
232fn gcd(mut left: u128, mut right: u128) -> u128 {
233 while right != 0 {
234 let remainder = left % right;
235 left = right;
236 right = remainder;
237 }
238 left
239}
240
241fn invalid_timeline(message: &str) -> Error {
242 Error::new(ErrorKind::InvalidInput, message)
243}
244
245fn timeline_overflow(operation: &str) -> Error {
246 Error::new(
247 ErrorKind::ResourceLimit,
248 format!("timeline overflow while {operation}"),
249 )
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn rational_normalizes_sign_and_common_factors() {
258 assert_eq!(Rational::new(6, -8), Rational::new(-3, 4));
259 assert_eq!(Rational::new(0, -9), Rational::new(0, 1));
260 }
261
262 #[test]
263 fn rational_rejects_zero_denominator() {
264 let error = Rational::new(1, 0).unwrap_err();
265 assert_eq!(error.kind(), ErrorKind::InvalidInput);
266 }
267
268 #[test]
269 fn rational_arithmetic_is_checked_and_exact() {
270 let half = Rational::new(1, 2).unwrap();
271 let third = Rational::new(1, 3).unwrap();
272 assert_eq!(
273 half.checked_add(third).unwrap(),
274 Rational::new(5, 6).unwrap()
275 );
276 assert_eq!(
277 half.checked_sub(third).unwrap(),
278 Rational::new(1, 6).unwrap()
279 );
280 assert_eq!(
281 half.checked_mul(third).unwrap(),
282 Rational::new(1, 6).unwrap()
283 );
284 assert_eq!(
285 half.checked_div(third).unwrap(),
286 Rational::new(3, 2).unwrap()
287 );
288 }
289
290 #[test]
291 fn integral_frame_rate_maps_to_integral_sample_ranges() {
292 let timeline = Timeline::new(FrameRate::new(30, 1).unwrap(), 44_100).unwrap();
293 assert_eq!(
294 timeline.audio_interval_for_frame(FrameIndex(5)).unwrap(),
295 SampleRange {
296 start: 7_350,
297 end: 8_820
298 }
299 );
300 }
301
302 #[test]
303 fn fractional_frame_rate_has_contiguous_sample_ranges() {
304 let timeline = Timeline::new(FrameRate::new(30_000, 1_001).unwrap(), 48_000).unwrap();
305 let first = timeline.audio_interval_for_frame(FrameIndex(0)).unwrap();
306 let second = timeline.audio_interval_for_frame(FrameIndex(1)).unwrap();
307 let third = timeline.audio_interval_for_frame(FrameIndex(2)).unwrap();
308 assert_eq!(
309 first,
310 SampleRange {
311 start: 0,
312 end: 1_601
313 }
314 );
315 assert_eq!(first.end, second.start);
316 assert_eq!(second.end, third.start);
317 assert_eq!(third.end, 4_804);
318 }
319
320 #[test]
321 fn maximum_frame_index_reports_overflow() {
322 let timeline = Timeline::new(FrameRate::new(24, 1).unwrap(), 48_000).unwrap();
323 let error = timeline
324 .audio_interval_for_frame(FrameIndex(u64::MAX))
325 .unwrap_err();
326 assert_eq!(error.kind(), ErrorKind::ResourceLimit);
327 }
328
329 #[test]
330 fn audio_clock_mapping_selects_the_containing_frame() {
331 let timeline = Timeline::new(FrameRate::new(30_000, 1_001).unwrap(), 48_000).unwrap();
332 assert_eq!(timeline.frame_for_audio_sample(0).unwrap(), FrameIndex(0));
333 assert_eq!(
334 timeline.frame_for_audio_sample(1_600).unwrap(),
335 FrameIndex(0)
336 );
337 assert_eq!(
338 timeline.frame_for_audio_sample(1_601).unwrap(),
339 FrameIndex(1)
340 );
341 }
342}