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