#![allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum StandardRate {
Fps23_976,
Fps24,
Fps25,
Fps29_97,
Fps30,
Fps48,
Fps50,
Fps59_94,
Fps60,
}
impl StandardRate {
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn as_f64(self) -> f64 {
match self {
Self::Fps23_976 => 24000.0 / 1001.0,
Self::Fps24 => 24.0,
Self::Fps25 => 25.0,
Self::Fps29_97 => 30000.0 / 1001.0,
Self::Fps30 => 30.0,
Self::Fps48 => 48.0,
Self::Fps50 => 50.0,
Self::Fps59_94 => 60000.0 / 1001.0,
Self::Fps60 => 60.0,
}
}
#[must_use]
pub const fn is_drop_frame(self) -> bool {
matches!(self, Self::Fps29_97 | Self::Fps59_94)
}
#[must_use]
pub fn frame_duration_secs(self) -> f64 {
1.0 / self.as_f64()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SimpleTimecode {
pub hours: u32,
pub minutes: u32,
pub seconds: u32,
pub frames: u32,
}
impl SimpleTimecode {
#[must_use]
pub const fn new(hours: u32, minutes: u32, seconds: u32, frames: u32) -> Self {
Self {
hours,
minutes,
seconds,
frames,
}
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
pub fn to_frame_count(self, rate: StandardRate) -> u64 {
let fps = rate.as_f64().round() as u64;
let total_secs =
u64::from(self.hours) * 3600 + u64::from(self.minutes) * 60 + u64::from(self.seconds);
let raw_frames = total_secs * fps + u64::from(self.frames);
if rate.is_drop_frame() {
let total_minutes = u64::from(self.hours) * 60 + u64::from(self.minutes);
let drop_count = 2 * (total_minutes - total_minutes / 10);
raw_frames.saturating_sub(drop_count)
} else {
raw_frames
}
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
#[allow(clippy::cast_possible_truncation)]
pub fn from_frame_count(mut frame_count: u64, rate: StandardRate) -> Self {
let fps = rate.as_f64().round() as u64;
if rate.is_drop_frame() {
let frames_per_10min = fps * 60 * 10 - 2 * 9;
let d = frame_count / frames_per_10min;
let m = frame_count % frames_per_10min;
let extra = if m > 2 { (m - 2) / (fps * 60 - 2) } else { 0 };
frame_count += 2 * (9 * d + extra);
}
let total_secs = frame_count / fps;
let frames = (frame_count % fps) as u32;
let seconds = (total_secs % 60) as u32;
let total_mins = total_secs / 60;
let minutes = (total_mins % 60) as u32;
let hours = (total_mins / 60) as u32;
Self {
hours,
minutes,
seconds,
frames,
}
}
}
impl std::fmt::Display for SimpleTimecode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{:02}:{:02}:{:02}:{:02}",
self.hours, self.minutes, self.seconds, self.frames
)
}
}
#[derive(Debug, Clone, Copy)]
pub struct RateConversion {
pub source: StandardRate,
pub target: StandardRate,
}
impl RateConversion {
#[must_use]
pub const fn new(source: StandardRate, target: StandardRate) -> Self {
Self { source, target }
}
#[must_use]
pub fn speed_factor(&self) -> f64 {
self.target.as_f64() / self.source.as_f64()
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
pub fn convert_frame(&self, source_frame: u64) -> u64 {
let source_time_sec = source_frame as f64 / self.source.as_f64();
let target_frame = source_time_sec * self.target.as_f64();
target_frame.round() as u64
}
#[must_use]
pub fn convert_duration_ms(&self, source_ms: u64) -> u64 {
source_ms
}
#[must_use]
pub fn convert_timecode(&self, tc: SimpleTimecode) -> SimpleTimecode {
let source_frames = tc.to_frame_count(self.source);
let target_frames = self.convert_frame(source_frames);
SimpleTimecode::from_frame_count(target_frames, self.target)
}
#[must_use]
pub fn is_pulldown(&self) -> bool {
self.target.as_f64() > self.source.as_f64()
}
#[must_use]
pub fn is_pullup(&self) -> bool {
self.target.as_f64() < self.source.as_f64()
}
#[must_use]
pub fn is_identity(&self) -> bool {
(self.source.as_f64() - self.target.as_f64()).abs() < 0.001
}
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn frames_to_seconds(frame_count: u64, rate: StandardRate) -> f64 {
frame_count as f64 / rate.as_f64()
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
pub fn seconds_to_frames(seconds: f64, rate: StandardRate) -> u64 {
(seconds * rate.as_f64()).round() as u64
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
pub fn frame_diff_count(duration_secs: f64, source: StandardRate, target: StandardRate) -> i64 {
let source_frames = (duration_secs * source.as_f64()).round() as i64;
let target_frames = (duration_secs * target.as_f64()).round() as i64;
target_frames - source_frames
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_standard_rate_values() {
assert!((StandardRate::Fps24.as_f64() - 24.0).abs() < 0.001);
assert!((StandardRate::Fps25.as_f64() - 25.0).abs() < 0.001);
assert!((StandardRate::Fps23_976.as_f64() - 23.976).abs() < 0.001);
assert!((StandardRate::Fps29_97.as_f64() - 29.97).abs() < 0.01);
}
#[test]
fn test_drop_frame_flag() {
assert!(StandardRate::Fps29_97.is_drop_frame());
assert!(StandardRate::Fps59_94.is_drop_frame());
assert!(!StandardRate::Fps24.is_drop_frame());
assert!(!StandardRate::Fps25.is_drop_frame());
}
#[test]
fn test_timecode_display() {
let tc = SimpleTimecode::new(1, 2, 3, 4);
assert_eq!(format!("{tc}"), "01:02:03:04");
}
#[test]
fn test_timecode_to_frames_non_drop() {
let tc = SimpleTimecode::new(0, 1, 0, 0);
let frames = tc.to_frame_count(StandardRate::Fps24);
assert_eq!(frames, 24 * 60);
}
#[test]
fn test_timecode_roundtrip() {
let original = SimpleTimecode::new(1, 30, 45, 12);
let frames = original.to_frame_count(StandardRate::Fps24);
let recovered = SimpleTimecode::from_frame_count(frames, StandardRate::Fps24);
assert_eq!(original, recovered);
}
#[test]
fn test_conversion_identity() {
let conv = RateConversion::new(StandardRate::Fps25, StandardRate::Fps25);
assert!(conv.is_identity());
assert_eq!(conv.convert_frame(100), 100);
}
#[test]
fn test_conversion_pulldown() {
let conv = RateConversion::new(StandardRate::Fps24, StandardRate::Fps25);
assert!(conv.is_pulldown());
assert!(!conv.is_pullup());
}
#[test]
fn test_conversion_pullup() {
let conv = RateConversion::new(StandardRate::Fps25, StandardRate::Fps24);
assert!(conv.is_pullup());
assert!(!conv.is_pulldown());
}
#[test]
fn test_convert_frame_24_to_25() {
let conv = RateConversion::new(StandardRate::Fps24, StandardRate::Fps25);
let result = conv.convert_frame(24);
assert_eq!(result, 25);
}
#[test]
fn test_frames_to_seconds() {
let secs = frames_to_seconds(48, StandardRate::Fps24);
assert!((secs - 2.0).abs() < 0.001);
}
#[test]
fn test_seconds_to_frames() {
let frames = seconds_to_frames(2.0, StandardRate::Fps24);
assert_eq!(frames, 48);
}
#[test]
fn test_frame_diff_count() {
let diff = frame_diff_count(10.0, StandardRate::Fps24, StandardRate::Fps25);
assert_eq!(diff, 10);
}
#[test]
fn test_speed_factor() {
let conv = RateConversion::new(StandardRate::Fps24, StandardRate::Fps48);
assert!((conv.speed_factor() - 2.0).abs() < 0.001);
}
#[test]
fn test_frame_duration() {
let dur = StandardRate::Fps25.frame_duration_secs();
assert!((dur - 0.04).abs() < 0.001);
}
}