Skip to main content

use_frame_rate/
lib.rs

1#![forbid(unsafe_code)]
2//! Primitive frame-rate helpers.
3//!
4//! These helpers keep frame duration and frame-count calculations explicit.
5//!
6//! # Examples
7//!
8//! ```rust
9//! use use_frame_rate::{FrameRate, frame_count, is_standard_frame_rate};
10//!
11//! let rate = FrameRate::new(24.0).unwrap();
12//!
13//! assert!((rate.frame_duration_seconds() - (1.0 / 24.0)).abs() < 1.0e-12);
14//! assert_eq!(frame_count(2.5, 24.0).unwrap(), 60);
15//! assert!(is_standard_frame_rate(23.976));
16//! ```
17
18#[derive(Debug, Clone, Copy, PartialEq)]
19pub struct FrameRate {
20    fps: f64,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum FrameRateError {
25    InvalidFps,
26    InvalidDuration,
27}
28
29fn validate_fps(fps: f64) -> Result<f64, FrameRateError> {
30    if !fps.is_finite() || fps <= 0.0 {
31        Err(FrameRateError::InvalidFps)
32    } else {
33        Ok(fps)
34    }
35}
36
37fn validate_duration(duration_seconds: f64) -> Result<f64, FrameRateError> {
38    if !duration_seconds.is_finite() || duration_seconds < 0.0 {
39        Err(FrameRateError::InvalidDuration)
40    } else {
41        Ok(duration_seconds)
42    }
43}
44
45impl FrameRate {
46    pub fn new(fps: f64) -> Result<Self, FrameRateError> {
47        Ok(Self {
48            fps: validate_fps(fps)?,
49        })
50    }
51
52    #[must_use]
53    pub fn fps(&self) -> f64 {
54        self.fps
55    }
56
57    #[must_use]
58    pub fn frame_duration_seconds(&self) -> f64 {
59        1.0 / self.fps
60    }
61
62    #[must_use]
63    pub fn frame_duration_millis(&self) -> f64 {
64        self.frame_duration_seconds() * 1_000.0
65    }
66}
67
68pub fn frame_duration_seconds(fps: f64) -> Result<f64, FrameRateError> {
69    Ok(1.0 / validate_fps(fps)?)
70}
71
72pub fn frame_count(duration_seconds: f64, fps: f64) -> Result<u64, FrameRateError> {
73    let duration_seconds = validate_duration(duration_seconds)?;
74    let fps = validate_fps(fps)?;
75    Ok((duration_seconds * fps).round() as u64)
76}
77
78pub fn duration_from_frames(frame_count: u64, fps: f64) -> Result<f64, FrameRateError> {
79    Ok(frame_count as f64 / validate_fps(fps)?)
80}
81
82#[must_use]
83pub fn is_standard_frame_rate(fps: f64) -> bool {
84    const STANDARD_FRAME_RATES: [f64; 9] =
85        [23.976, 24.0, 25.0, 29.97, 30.0, 50.0, 59.94, 60.0, 120.0];
86
87    fps.is_finite()
88        && STANDARD_FRAME_RATES
89            .into_iter()
90            .any(|standard| (fps - standard).abs() <= 0.01)
91}
92
93#[cfg(test)]
94mod tests {
95    use super::{
96        FrameRate, FrameRateError, duration_from_frames, frame_count, frame_duration_seconds,
97        is_standard_frame_rate,
98    };
99
100    #[test]
101    fn computes_frame_durations_and_counts() {
102        let rate = FrameRate::new(24.0).unwrap();
103
104        assert_eq!(rate.fps(), 24.0);
105        assert!((rate.frame_duration_seconds() - (1.0 / 24.0)).abs() < 1.0e-12);
106        assert!((rate.frame_duration_millis() - 41.666_666_666_666_664).abs() < 1.0e-12);
107        assert_eq!(frame_count(2.5, 24.0).unwrap(), 60);
108        assert!((duration_from_frames(60, 24.0).unwrap() - 2.5).abs() < 1.0e-12);
109        assert!((frame_duration_seconds(25.0).unwrap() - 0.04).abs() < 1.0e-12);
110    }
111
112    #[test]
113    fn detects_standard_frame_rates() {
114        assert!(is_standard_frame_rate(23.976));
115        assert!(is_standard_frame_rate(23.98));
116        assert!(is_standard_frame_rate(60.0));
117        assert!(!is_standard_frame_rate(27.0));
118    }
119
120    #[test]
121    fn rejects_invalid_frame_rate_inputs() {
122        assert_eq!(FrameRate::new(0.0), Err(FrameRateError::InvalidFps));
123        assert_eq!(
124            frame_count(-1.0, 24.0),
125            Err(FrameRateError::InvalidDuration)
126        );
127        assert_eq!(
128            duration_from_frames(10, f64::NAN),
129            Err(FrameRateError::InvalidFps)
130        );
131    }
132}