Skip to main content

alpha2_time/
frame_rate.rs

1use crate::{FrameRateError, MediaTime, TICKS_PER_SECOND};
2use core::fmt;
3use serde::{Deserialize, Serialize};
4
5/// A positive rational frame rate in reduced form, at most one frame per tick (120 000 fps).
6#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, Hash, Serialize)]
7#[serde(try_from = "Wire")]
8#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
9pub struct FrameRate {
10    num: u32,
11    den: u32,
12}
13#[derive(Deserialize)]
14#[serde(deny_unknown_fields)]
15#[cfg_attr(
16    feature = "schema",
17    derive(schemars::JsonSchema),
18    schemars(rename = "FrameRate")
19)]
20/// An exact rational frame rate such as `{ "num": 30000, "den": 1001 }`.
21///
22/// A frame lasts at least one tick, so `num` is at most 120 000 times `den`.
23struct Wire {
24    /// Frames per `den` seconds. Positive, and at most 120 000 times `den`.
25    #[cfg_attr(feature = "schema", schemars(range(min = 1)))]
26    num: u32,
27    /// Seconds per `num` frames. Positive.
28    #[cfg_attr(feature = "schema", schemars(range(min = 1)))]
29    den: u32,
30}
31impl TryFrom<Wire> for FrameRate {
32    type Error = FrameRateError;
33    fn try_from(wire: Wire) -> Result<Self, Self::Error> {
34        Self::new(wire.num, wire.den)
35    }
36}
37impl FrameRate {
38    /// Creates and reduces a positive rate.
39    ///
40    /// # Errors
41    /// Returns an error for zero components or sub-tick frame duration.
42    pub const fn new(num: u32, den: u32) -> Result<Self, FrameRateError> {
43        if num == 0 {
44            return Err(FrameRateError::ZeroNumerator);
45        }
46        if den == 0 {
47            return Err(FrameRateError::ZeroDenominator);
48        }
49        if (num as u64) > (TICKS_PER_SECOND as u64) * (den as u64) {
50            return Err(FrameRateError::ExceedsTimelineResolution);
51        }
52        let divisor = gcd(num, den);
53        Ok(Self {
54            num: num / divisor,
55            den: den / divisor,
56        })
57    }
58    /// Reduced numerator.
59    #[must_use]
60    pub const fn numerator(self) -> u32 {
61        self.num
62    }
63    /// Reduced denominator.
64    #[must_use]
65    pub const fn denominator(self) -> u32 {
66        self.den
67    }
68    /// Exact floored start of a signed frame index, or `None` outside the `i64` tick range.
69    #[must_use]
70    pub fn frame_start(self, frame: i64) -> Option<MediaTime> {
71        let value = i128::from(frame) * i128::from(TICKS_PER_SECOND) * i128::from(self.den);
72        i64::try_from(value.div_euclid(i128::from(self.num)))
73            .ok()
74            .map(MediaTime)
75    }
76    /// Frame whose half-open interval contains `time`, or `None` if the index overflows `i64`.
77    #[must_use]
78    pub fn frame_at_or_before(self, time: MediaTime) -> Option<i64> {
79        let n = (i128::from(time.0) + 1) * i128::from(self.num);
80        let d = i128::from(TICKS_PER_SECOND) * i128::from(self.den);
81        i64::try_from(-(-n).div_euclid(d) - 1).ok()
82    }
83}
84impl fmt::Display for FrameRate {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        write!(f, "{}/{} fps", self.num, self.den)
87    }
88}
89const fn gcd(mut a: u32, mut b: u32) -> u32 {
90    while b != 0 {
91        let r = a % b;
92        a = b;
93        b = r;
94    }
95    a
96}
97#[cfg(test)]
98mod tests {
99    use super::*;
100    #[test]
101    fn a_frame_lasts_at_least_one_tick() {
102        assert_eq!(
103            FrameRate::new(120_000, 1).map(FrameRate::numerator),
104            Ok(120_000)
105        );
106        assert_eq!(
107            FrameRate::new(240_000, 2).map(|rate| (rate.numerator(), rate.denominator())),
108            Ok((120_000, 1))
109        );
110        for (num, den) in [(120_001, 1), (240_001, 2), (u32::MAX, 35_791)] {
111            assert_eq!(
112                FrameRate::new(num, den),
113                Err(FrameRateError::ExceedsTimelineResolution),
114                "{num}/{den}"
115            );
116        }
117        assert_eq!(
118            FrameRate::new(u32::MAX, 35_792).map(FrameRate::numerator),
119            Ok(u32::MAX)
120        );
121        assert_eq!(FrameRate::new(0, 1), Err(FrameRateError::ZeroNumerator));
122        assert_eq!(FrameRate::new(1, 0), Err(FrameRateError::ZeroDenominator));
123    }
124
125    #[test]
126    fn a_rate_reduces() {
127        let rate = FrameRate::new(60, 2).unwrap();
128        assert_eq!((rate.numerator(), rate.denominator()), (30, 1));
129        assert_eq!(rate, FrameRate::new(30, 1).unwrap());
130    }
131
132    #[test]
133    fn the_wire_form_parses_only_what_new_accepts() {
134        let parse = |json: &str| serde_json::from_str::<FrameRate>(json);
135        assert_eq!(
136            parse(r#"{"num":60000,"den":2002}"#).unwrap(),
137            FrameRate::new(30_000, 1_001).unwrap()
138        );
139        assert_eq!(
140            parse(r#"{"num":120000,"den":1}"#).unwrap(),
141            FrameRate::new(120_000, 1).unwrap()
142        );
143        for (json, message) in [
144            (r#"{"num":120001,"den":1}"#, "exceeds timeline resolution"),
145            (r#"{"num":240001,"den":2}"#, "exceeds timeline resolution"),
146            (r#"{"num":0,"den":1}"#, "numerator must not be zero"),
147            (r#"{"num":30,"den":0}"#, "denominator must not be zero"),
148            (r#"{"num":4294967296,"den":1}"#, "expected u32"),
149            (r#"{"num":30,"den":4294967296}"#, "expected u32"),
150            (r#"{"num":-30,"den":1}"#, "expected u32"),
151            (r#"{"num":30.0,"den":1}"#, "expected u32"),
152            (r#"{"num":30,"den":1,"fps":30}"#, "unknown field `fps`"),
153            (r#"{"num":30}"#, "missing field `den`"),
154        ] {
155            let error = parse(json).expect_err(json).to_string();
156            assert!(error.contains(message), "{json}: {error}");
157        }
158    }
159
160    /// The first two rates have whole-tick frames; the rest start frames between ticks.
161    const RATES: [(u32, u32); 11] = [
162        (30, 1),
163        (30_000, 1_001),
164        (90, 1),
165        (72, 1),
166        (144, 1),
167        (165, 1),
168        (48_000, 1_001),
169        (7, 1),
170        (2_997, 100),
171        (119_999, 1),
172        (120_000, 1),
173    ];
174
175    #[test]
176    fn a_frame_starts_at_the_tick_its_exact_start_falls_in() {
177        let starts = |num, den| {
178            let rate = FrameRate::new(num, den).unwrap();
179            (-1..=3)
180                .map(|frame| rate.frame_start(frame).unwrap().ticks())
181                .collect::<Vec<_>>()
182        };
183        // 1333 1/3 ticks a frame.
184        assert_eq!(starts(90, 1), [-1334, 0, 1333, 2666, 4000]);
185        // 2502 1/2 ticks a frame.
186        assert_eq!(starts(48_000, 1_001), [-2503, 0, 2502, 5005, 7507]);
187        for (num, den) in RATES {
188            let rate = FrameRate::new(num, den).unwrap();
189            let exact =
190                |frame: i64| i128::from(frame) * i128::from(TICKS_PER_SECOND) * i128::from(den);
191            for frame in -1000..1000 {
192                // start <= exact start < start + 1, in units of 1/num ticks.
193                let start = i128::from(rate.frame_start(frame).unwrap().ticks());
194                assert!(
195                    start * i128::from(num) <= exact(frame)
196                        && exact(frame) < (start + 1) * i128::from(num),
197                    "{rate}: frame {frame} starts at {start}"
198                );
199            }
200        }
201    }
202
203    #[test]
204    fn every_tick_of_a_frame_names_that_frame() {
205        for (num, den) in RATES {
206            let rate = FrameRate::new(num, den).unwrap();
207            for frame in -1000..1000 {
208                let start = rate.frame_start(frame).unwrap();
209                let next = rate.frame_start(frame + 1).unwrap();
210                assert!(start < next, "{rate}: frame {frame} lasts no tick");
211                for tick in [start.0, start.0 + (next.0 - start.0) / 2, next.0 - 1] {
212                    assert_eq!(
213                        rate.frame_at_or_before(MediaTime(tick)),
214                        Some(frame),
215                        "{rate}: tick {tick} of frame {frame} [{start}, {next})"
216                    );
217                }
218            }
219        }
220    }
221
222    #[test]
223    fn a_frame_starts_only_where_its_start_is_a_tick() {
224        for (num, den) in RATES.into_iter().filter(|&rate| rate != (120_000, 1)) {
225            let rate = FrameRate::new(num, den).unwrap();
226            let last = rate.frame_at_or_before(MediaTime(i64::MAX)).unwrap();
227            let first = rate.frame_at_or_before(MediaTime(i64::MIN)).unwrap();
228            assert!(rate.frame_start(last).is_some(), "{rate}: frame {last}");
229            assert_eq!(rate.frame_start(last + 1), None, "{rate}: frame {last} + 1");
230            // The first frame starts before the timeline unless it starts on its first tick.
231            assert!(
232                rate.frame_start(first)
233                    .is_none_or(|start| start == MediaTime(i64::MIN)),
234                "{rate}: frame {first}"
235            );
236            assert!(
237                rate.frame_start(first + 1).is_some(),
238                "{rate}: frame {first} + 1"
239            );
240            assert_eq!(rate.frame_start(i64::MAX), None, "{rate}");
241            assert_eq!(rate.frame_start(i64::MIN), None, "{rate}");
242        }
243        let fastest = FrameRate::new(120_000, 1).unwrap();
244        assert_eq!(
245            fastest.frame_at_or_before(MediaTime(i64::MAX)),
246            Some(i64::MAX)
247        );
248        assert_eq!(
249            fastest.frame_at_or_before(MediaTime(i64::MIN)),
250            Some(i64::MIN)
251        );
252        assert_eq!(fastest.frame_start(i64::MAX), Some(MediaTime(i64::MAX)));
253        assert_eq!(fastest.frame_start(i64::MIN), Some(MediaTime(i64::MIN)));
254    }
255}