Files
momentry_core/src/core/time.rs

384 lines
12 KiB
Rust

//! Frame-based time representation for video processing.
//!
//! This module provides a `FrameTime` struct that stores time as frame count
//! with a given FPS (frames per second). This avoids floating-point precision
//! issues when converting between seconds and frames.
//!
//! # Examples
//!
//! ```
//! use momentry_core::time::FrameTime;
//!
//! // Create a FrameTime from frames
//! let time = FrameTime::from_frames(1234, 30.0);
//! assert_eq!(time.seconds(), 41.13333333333333);
//! assert_eq!(time.format_sec_frame(), "41.04");
//!
//! // Create from seconds (useful for migration)
//! let time = FrameTime::from_seconds(41.133333, 30.0);
//! assert_eq!(time.frames(), 1234);
//! ```
use serde::{Deserialize, Serialize};
use std::fmt;
/// Frame-based time representation.
///
/// Stores time as an integer frame count with a floating-point FPS.
/// All calculations are performed using integer frame counts to avoid
/// floating-point precision issues.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct FrameTime {
/// Frame count (0-based)
frames: i64,
/// Frames per second (can be fractional, e.g., 29.97, 23.976)
fps: f64,
}
impl FrameTime {
/// Creates a new `FrameTime` from frame count and FPS.
///
/// If `fps <= 0.0` or `fps.is_nan()`, defaults to 30.0 FPS.
pub fn from_frames(frames: i64, fps: f64) -> Self {
let fps = if fps <= 0.0 || !fps.is_finite() {
30.0
} else {
fps
};
Self { frames, fps }
}
/// Creates a new `FrameTime` from seconds and FPS.
///
/// This is useful for migrating from existing time representations.
/// The frame count is calculated as `(seconds * fps).round() as i64`
/// to minimize precision loss.
///
/// If `fps <= 0.0` or `fps.is_nan()`, defaults to 30.0 FPS.
pub fn from_seconds(seconds: f64, fps: f64) -> Self {
let fps = if fps <= 0.0 || !fps.is_finite() {
30.0
} else {
fps
};
let frames = (seconds * fps).round() as i64;
Self { frames, fps }
}
/// Returns the frame count.
pub fn frames(&self) -> i64 {
self.frames
}
/// Returns the FPS (frames per second).
pub fn fps(&self) -> f64 {
self.fps
}
/// Returns the time in seconds as a floating-point value.
///
/// Note: This may have precision limitations for fractional FPS values.
/// For display purposes, use `format_sec_frame()` or `format_hms()` instead.
pub fn seconds(&self) -> f64 {
self.frames as f64 / self.fps
}
/// Formats the time as "seconds.frame" with fixed two-digit frame number.
///
/// The frame number is displayed as a zero-padded two-digit number
/// representing the frame within the current second.
///
/// # Examples
///
/// - `123.04` = 123 seconds, frame 4 (at 30 FPS, frame 4 = 0.133 seconds)
/// - `5.29` = 5 seconds, frame 29 (at 30 FPS, last frame of that second)
pub fn format_sec_frame(&self) -> String {
let total_seconds = self.frames as f64 / self.fps;
let seconds = total_seconds.floor() as i64;
// For fractional FPS, use ceil of fps for modulo operation
let fps_ceil = self.fps.ceil() as i64;
// Ensure fps_ceil > 0
let frames_in_second = if fps_ceil == 0 {
0
} else {
self.frames % fps_ceil
};
// Handle negative frames
let frames_in_second = if frames_in_second < 0 {
// This shouldn't happen in practice
0
} else {
frames_in_second
};
format!("{}.{:02}", seconds, frames_in_second)
}
/// Formats the time as "HH:MM:SS" (hours, minutes, seconds).
///
/// This displays whole seconds only, without frame information.
/// Useful for human-readable time displays.
pub fn format_hms(&self) -> String {
let total_seconds = self.seconds();
let hours = (total_seconds / 3600.0) as i64;
let minutes = ((total_seconds % 3600.0) / 60.0) as i64;
let seconds = (total_seconds % 60.0) as i64;
format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
}
/// Formats the time as "HH:MM:SS.FF" (hours, minutes, seconds, frames).
///
/// Displays full time with frame information. Frames are shown as
/// zero-padded two-digit numbers.
pub fn format_hms_frame(&self) -> String {
let total_seconds = self.seconds();
let hours = (total_seconds / 3600.0) as i64;
let minutes = ((total_seconds % 3600.0) / 60.0) as i64;
let seconds = (total_seconds % 60.0) as i64;
// For fractional FPS, use ceil of fps for modulo operation
let fps_ceil = self.fps.ceil() as i64;
let frames_in_second = if fps_ceil == 0 {
0
} else {
self.frames % fps_ceil
};
let frames_in_second = if frames_in_second < 0 {
0
} else {
frames_in_second
};
format!(
"{:02}:{:02}:{:02}.{:02}",
hours, minutes, seconds, frames_in_second
)
}
/// Adds frames to this time, returning a new `FrameTime`.
///
/// # Panics
///
/// Panics if the FPS doesn't match.
pub fn add_frames(&self, frames: i64) -> Self {
Self {
frames: self.frames + frames,
fps: self.fps,
}
}
/// Subtracts frames from this time, returning a new `FrameTime`.
///
/// # Panics
///
/// Panics if the FPS doesn't match or if the result would be negative.
pub fn sub_frames(&self, frames: i64) -> Self {
assert!(
self.frames >= frames,
"Cannot subtract more frames than available"
);
Self {
frames: self.frames - frames,
fps: self.fps,
}
}
/// Adds seconds to this time, returning a new `FrameTime`.
///
/// # Panics
///
/// Panics if the FPS doesn't match.
pub fn add_seconds(&self, seconds: f64) -> Self {
let frames_to_add = (seconds * self.fps).round() as i64;
self.add_frames(frames_to_add)
}
/// Subtracts seconds from this time, returning a new `FrameTime`.
///
/// # Panics
///
/// Panics if the FPS doesn't match or if the result would be negative.
pub fn sub_seconds(&self, seconds: f64) -> Self {
let frames_to_sub = (seconds * self.fps).round() as i64;
self.sub_frames(frames_to_sub)
}
/// Returns the duration between two `FrameTime` instances.
///
/// # Panics
///
/// Panics if the FPS values don't match.
pub fn duration(&self, other: &FrameTime) -> FrameDuration {
assert!(
(self.fps - other.fps).abs() < f64::EPSILON * 2.0,
"FPS mismatch: {} != {}",
self.fps,
other.fps
);
let frame_diff = (self.frames - other.frames).abs();
FrameDuration::from_frames(frame_diff, self.fps)
}
}
impl fmt::Display for FrameTime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.format_sec_frame())
}
}
/// Duration between two frame times.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FrameDuration {
frames: i64,
fps: f64,
}
impl FrameDuration {
/// Creates a duration from frame count and FPS.
/// If `fps <= 0.0` or `fps.is_nan()`, defaults to 30.0 FPS.
pub fn from_frames(frames: i64, fps: f64) -> Self {
let fps = if fps <= 0.0 || !fps.is_finite() {
30.0
} else {
fps
};
Self { frames, fps }
}
/// Creates a duration from seconds and FPS.
/// If `fps <= 0.0` or `fps.is_nan()`, defaults to 30.0 FPS.
pub fn from_seconds(seconds: f64, fps: f64) -> Self {
let fps = if fps <= 0.0 || !fps.is_finite() {
30.0
} else {
fps
};
let frames = (seconds * fps).round() as i64;
Self { frames, fps }
}
/// Returns the duration in frames.
pub fn frames(&self) -> i64 {
self.frames
}
/// Returns the duration in seconds.
pub fn seconds(&self) -> f64 {
self.frames as f64 / self.fps
}
/// Formats the duration as "seconds.frame" (same as `FrameTime`).
pub fn format_sec_frame(&self) -> String {
let temp_time = FrameTime::from_frames(self.frames, self.fps);
temp_time.format_sec_frame()
}
/// Formats the duration as "HH:MM:SS".
pub fn format_hms(&self) -> String {
let temp_time = FrameTime::from_frames(self.frames, self.fps);
temp_time.format_hms()
}
}
impl fmt::Display for FrameDuration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.format_sec_frame())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_frames() {
let time = FrameTime::from_frames(150, 30.0);
assert_eq!(time.frames(), 150);
assert_eq!(time.fps(), 30.0);
assert_eq!(time.seconds(), 5.0);
}
#[test]
fn test_from_seconds() {
let time = FrameTime::from_seconds(5.0, 30.0);
assert_eq!(time.frames(), 150);
assert_eq!(time.seconds(), 5.0);
}
#[test]
fn test_format_sec_frame() {
let time = FrameTime::from_frames(123, 30.0);
assert_eq!(time.format_sec_frame(), "4.03");
let time = FrameTime::from_frames(29, 30.0);
assert_eq!(time.format_sec_frame(), "0.29");
let time = FrameTime::from_frames(60, 30.0);
assert_eq!(time.format_sec_frame(), "2.00");
}
#[test]
fn test_format_sec_frame_fractional_fps() {
// 29.97 fps (NTSC)
let time = FrameTime::from_frames(30, 29.97);
// 30 frames at 29.97 fps = 1.001 seconds = 1 second, frame 0
assert_eq!(time.format_sec_frame(), "1.00");
let time = FrameTime::from_frames(60, 29.97);
// 60 frames at 29.97 fps = 2.002 seconds = 2 seconds, frame 0
assert_eq!(time.format_sec_frame(), "2.00");
}
#[test]
fn test_format_hms() {
let time = FrameTime::from_frames(3661, 30.0); // 122.033 seconds = 2 minutes 2 seconds
assert_eq!(time.format_hms(), "00:02:02");
let time = FrameTime::from_frames(4500, 30.0); // 150 seconds = 2 minutes 30 seconds
assert_eq!(time.format_hms(), "00:02:30");
}
#[test]
fn test_format_hms_frame() {
let time = FrameTime::from_frames(123, 30.0); // 4 seconds, 3 frames
assert_eq!(time.format_hms_frame(), "00:00:04.03");
}
#[test]
fn test_add_sub_frames() {
let time = FrameTime::from_frames(100, 30.0);
let new_time = time.add_frames(50);
assert_eq!(new_time.frames(), 150);
let new_time = time.sub_frames(30);
assert_eq!(new_time.frames(), 70);
}
#[test]
fn test_add_sub_seconds() {
let time = FrameTime::from_frames(100, 30.0);
let new_time = time.add_seconds(2.0);
assert_eq!(new_time.frames(), 160); // 100 + 60
let new_time = time.sub_seconds(1.0);
assert_eq!(new_time.frames(), 70); // 100 - 30
}
#[test]
fn test_duration() {
let time1 = FrameTime::from_frames(200, 30.0);
let time2 = FrameTime::from_frames(150, 30.0);
let duration = time1.duration(&time2);
assert_eq!(duration.frames(), 50);
assert_eq!(duration.seconds(), 50.0 / 30.0);
}
#[test]
fn test_frame_duration() {
let duration = FrameDuration::from_frames(90, 30.0);
assert_eq!(duration.seconds(), 3.0);
assert_eq!(duration.format_sec_frame(), "3.00");
assert_eq!(duration.format_hms(), "00:00:03");
}
}