Skip to main content

av_drift_detector/
timing.rs

1//! Exact frame-rate arithmetic and time parsing.
2
3use std::fmt;
4use std::str::FromStr;
5
6use serde::{Serialize, Serializer};
7
8use crate::{Error, Result};
9
10/// A frame rate stored as an exact reduced fraction, e.g. `30000/1001` for 29.97 fps.
11#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
12pub struct FrameRate {
13    num: u32,
14    den: u32,
15}
16
17/// NTSC-style rates (`n * 1000 / 1001`) recognised when given as decimals like `29.97`.
18const NTSC_BASES: [u32; 6] = [24, 30, 48, 60, 120, 240];
19
20impl FrameRate {
21    pub fn new(num: u32, den: u32) -> Result<Self> {
22        if num == 0 || den == 0 {
23            return Err(Error::InvalidFrameRate(format!("{num}/{den}")));
24        }
25        let g = gcd(num, den);
26        let rate = FrameRate {
27            num: num / g,
28            den: den / g,
29        };
30        if !(1.0..=480.0).contains(&rate.as_f64()) {
31            return Err(Error::InvalidFrameRate(format!(
32                "{num}/{den} (must be between 1 and 480 fps)"
33            )));
34        }
35        Ok(rate)
36    }
37
38    pub fn num(self) -> u32 {
39        self.num
40    }
41
42    pub fn den(self) -> u32 {
43        self.den
44    }
45
46    pub fn as_f64(self) -> f64 {
47        f64::from(self.num) / f64::from(self.den)
48    }
49
50    /// Duration of one frame in seconds.
51    pub fn frame_duration(self) -> f64 {
52        f64::from(self.den) / f64::from(self.num)
53    }
54
55    /// Presentation time of `frame` in seconds.
56    pub fn frame_time(self, frame: i64) -> f64 {
57        frame as f64 * self.frame_duration()
58    }
59
60    /// The integer rate used for timecode counting (30 for 29.97).
61    pub fn nominal(self) -> u32 {
62        ((self.num + self.den / 2) / self.den).max(1)
63    }
64
65    /// Whether SMPTE drop-frame timecode applies (29.97 and 59.94).
66    pub fn is_drop_frame(self) -> bool {
67        self.den == 1001 && (self.num == 30000 || self.num == 60000)
68    }
69
70    /// Audio samples per frame at `sample_rate` (fractional for NTSC rates).
71    pub fn samples_per_frame(self, sample_rate: u32) -> f64 {
72        f64::from(sample_rate) * self.frame_duration()
73    }
74
75    /// Index of the first audio sample whose time is at or after the start of `frame`.
76    pub fn frame_start_sample(self, frame: i64, sample_rate: u32) -> i64 {
77        let n = i128::from(frame) * i128::from(sample_rate) * i128::from(self.den);
78        let d = i128::from(self.num);
79        // Ceiling division that is correct for negative frames too.
80        (-(-n).div_euclid(d)) as i64
81    }
82
83    /// Number of whole frames needed to cover `seconds` (at least one).
84    pub fn frames_in(self, seconds: f64) -> u64 {
85        ((seconds * self.as_f64()).round() as u64).max(1)
86    }
87
88    /// Short human label, e.g. `29.97` or `30`.
89    pub fn label(self) -> String {
90        if self.den == 1 {
91            return self.num.to_string();
92        }
93        let s = format!("{:.3}", self.as_f64());
94        s.trim_end_matches('0').trim_end_matches('.').to_string()
95    }
96}
97
98impl fmt::Display for FrameRate {
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        if self.den == 1 {
101            write!(f, "{}", self.num)
102        } else {
103            write!(f, "{}/{}", self.num, self.den)
104        }
105    }
106}
107
108impl FromStr for FrameRate {
109    type Err = Error;
110
111    fn from_str(s: &str) -> Result<Self> {
112        let s = s.trim();
113        let bad = || Error::InvalidFrameRate(s.to_string());
114        if let Some((n, d)) = s.split_once('/') {
115            let n = n.trim().parse().map_err(|_| bad())?;
116            let d = d.trim().parse().map_err(|_| bad())?;
117            return FrameRate::new(n, d);
118        }
119        let v: f64 = s.parse().map_err(|_| bad())?;
120        if !v.is_finite() || v <= 0.0 {
121            return Err(bad());
122        }
123        for base in NTSC_BASES {
124            let ntsc = f64::from(base) * 1000.0 / 1001.0;
125            if (v - ntsc).abs() < 0.006 {
126                return FrameRate::new(base * 1000, 1001);
127            }
128        }
129        if (v - v.round()).abs() < 1e-9 {
130            return FrameRate::new(v.round() as u32, 1);
131        }
132        FrameRate::new((v * 1000.0).round() as u32, 1000)
133    }
134}
135
136impl Serialize for FrameRate {
137    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
138        serializer.collect_str(self)
139    }
140}
141
142#[cfg(feature = "ffmpeg")]
143impl From<FrameRate> for ffmpeg_next::Rational {
144    fn from(rate: FrameRate) -> Self {
145        ffmpeg_next::Rational::new(rate.num as i32, rate.den as i32)
146    }
147}
148
149fn gcd(mut a: u32, mut b: u32) -> u32 {
150    while b != 0 {
151        (a, b) = (b, a % b);
152    }
153    a
154}
155
156/// Parses a signed time into seconds.
157///
158/// Accepts plain seconds (`2.5`), suffixed values (`250ms`, `1.5s`) and clock
159/// notation (`01:02.5`, `00:01:02.500`), each optionally prefixed with `+`/`-`.
160pub fn parse_time(input: &str) -> Result<f64> {
161    let s = input.trim();
162    let bad = || Error::InvalidTime(input.to_string());
163    let (sign, body) = match s.as_bytes().first() {
164        Some(b'-') => (-1.0, &s[1..]),
165        Some(b'+') => (1.0, &s[1..]),
166        _ => (1.0, s),
167    };
168    let body = body.trim();
169    let number = |t: &str| -> Result<f64> {
170        let v: f64 = t.trim().parse().map_err(|_| bad())?;
171        if v.is_finite() && v >= 0.0 {
172            Ok(v)
173        } else {
174            Err(bad())
175        }
176    };
177    let seconds = if let Some(ms) = body.strip_suffix("ms") {
178        number(ms)? / 1000.0
179    } else if let Some(sec) = body.strip_suffix('s') {
180        number(sec)?
181    } else if body.contains(':') {
182        let parts: Vec<&str> = body.split(':').collect();
183        if parts.len() > 3 {
184            return Err(bad());
185        }
186        let mut total = 0.0;
187        for (i, part) in parts.iter().enumerate() {
188            let v = number(part)?;
189            if i + 1 < parts.len() && v.fract() != 0.0 {
190                return Err(bad());
191            }
192            total = total * 60.0 + v;
193        }
194        total
195    } else {
196        number(body)?
197    };
198    Ok(sign * seconds)
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    fn fr(s: &str) -> FrameRate {
206        s.parse().unwrap()
207    }
208
209    #[test]
210    fn parses_frame_rates() {
211        assert_eq!(fr("30"), FrameRate::new(30, 1).unwrap());
212        assert_eq!(fr("29.97"), FrameRate::new(30000, 1001).unwrap());
213        assert_eq!(fr("23.976"), FrameRate::new(24000, 1001).unwrap());
214        assert_eq!(fr("23.98"), FrameRate::new(24000, 1001).unwrap());
215        assert_eq!(fr("59.94"), FrameRate::new(60000, 1001).unwrap());
216        assert_eq!(fr("119.88"), FrameRate::new(120000, 1001).unwrap());
217        assert_eq!(fr("60000/1001"), FrameRate::new(60000, 1001).unwrap());
218        assert_eq!(fr("50/2"), FrameRate::new(25, 1).unwrap());
219        assert_eq!(fr("12.5"), FrameRate::new(25, 2).unwrap());
220        assert!("0".parse::<FrameRate>().is_err());
221        assert!("abc".parse::<FrameRate>().is_err());
222        assert!("1000".parse::<FrameRate>().is_err());
223    }
224
225    #[test]
226    fn labels_and_display() {
227        assert_eq!(fr("29.97").to_string(), "30000/1001");
228        assert_eq!(fr("29.97").label(), "29.97");
229        assert_eq!(fr("23.976").label(), "23.976");
230        assert_eq!(fr("25").label(), "25");
231        assert_eq!(fr("29.97").nominal(), 30);
232        assert!(fr("59.94").is_drop_frame());
233        assert!(!fr("23.976").is_drop_frame());
234    }
235
236    #[test]
237    fn ntsc_sample_schedule_is_exact() {
238        let rate = fr("29.97");
239        // 30000 frames at 29.97 fps last exactly 1001 seconds.
240        assert_eq!(rate.frame_start_sample(30000, 48000), 1001 * 48000);
241        let lens: Vec<i64> = (0..10)
242            .map(|n| rate.frame_start_sample(n + 1, 48000) - rate.frame_start_sample(n, 48000))
243            .collect();
244        assert!(lens.iter().all(|&l| l == 1601 || l == 1602), "{lens:?}");
245        assert_eq!(fr("30").frame_start_sample(-1, 48000), -1600);
246    }
247
248    #[test]
249    fn parses_times() {
250        assert_eq!(parse_time("2.5").unwrap(), 2.5);
251        assert_eq!(parse_time("250ms").unwrap(), 0.25);
252        assert_eq!(parse_time("+78.5ms").unwrap(), 0.0785);
253        assert_eq!(parse_time("-40ms").unwrap(), -0.04);
254        assert_eq!(parse_time("1.5s").unwrap(), 1.5);
255        assert_eq!(parse_time("00:00:05").unwrap(), 5.0);
256        assert_eq!(parse_time("01:02.5").unwrap(), 62.5);
257        assert_eq!(parse_time("1:00:00").unwrap(), 3600.0);
258        assert!(parse_time("1.5:00").is_err());
259        assert!(parse_time("ms").is_err());
260        assert!(parse_time("nan").is_err());
261    }
262}