#![allow(
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_sign_loss,
dead_code,
clippy::pedantic
)]
pub mod burn_in;
pub mod compare;
pub mod continuity;
pub mod drop_frame;
pub mod duration;
pub mod embedded_tc;
pub mod frame_offset;
pub mod frame_rate;
pub mod jam_sync;
pub mod ltc;
pub mod ltc_encoder;
pub mod ltc_parser;
pub mod ltc_simd;
pub mod midi_timecode;
pub mod reader;
pub mod subframe;
pub mod sync;
pub mod sync_map;
pub mod tc_calculator;
pub mod tc_compare;
pub mod tc_convert;
pub mod tc_drift;
pub mod tc_interpolate;
pub mod tc_list;
pub mod tc_math;
pub mod tc_metadata;
pub mod tc_offset_table;
pub mod tc_range;
pub mod tc_sequence;
pub mod tc_smpte_ranges;
pub mod tc_subtitle_sync;
pub mod tc_validator;
pub mod timecode_calculator;
pub mod timecode_display;
pub mod timecode_event;
pub mod timecode_format;
pub mod timecode_generator;
pub mod timecode_log;
pub mod timecode_overlay;
pub mod timecode_range;
pub mod vitc;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum FrameRate {
Fps23976,
Fps23976DF,
Fps24,
Fps25,
Fps2997DF,
Fps2997NDF,
Fps30,
Fps47952,
Fps47952DF,
Fps50,
Fps5994,
Fps5994DF,
Fps60,
Fps120,
}
impl FrameRate {
pub fn as_float(&self) -> f64 {
match self {
FrameRate::Fps23976 | FrameRate::Fps23976DF => 24000.0 / 1001.0,
FrameRate::Fps24 => 24.0,
FrameRate::Fps25 => 25.0,
FrameRate::Fps2997DF | FrameRate::Fps2997NDF => 30000.0 / 1001.0,
FrameRate::Fps30 => 30.0,
FrameRate::Fps47952 | FrameRate::Fps47952DF => 48000.0 / 1001.0,
FrameRate::Fps50 => 50.0,
FrameRate::Fps5994 | FrameRate::Fps5994DF => 60000.0 / 1001.0,
FrameRate::Fps60 => 60.0,
FrameRate::Fps120 => 120.0,
}
}
pub fn as_rational(&self) -> (u32, u32) {
match self {
FrameRate::Fps23976 | FrameRate::Fps23976DF => (24000, 1001),
FrameRate::Fps24 => (24, 1),
FrameRate::Fps25 => (25, 1),
FrameRate::Fps2997DF | FrameRate::Fps2997NDF => (30000, 1001),
FrameRate::Fps30 => (30, 1),
FrameRate::Fps47952 | FrameRate::Fps47952DF => (48000, 1001),
FrameRate::Fps50 => (50, 1),
FrameRate::Fps5994 | FrameRate::Fps5994DF => (60000, 1001),
FrameRate::Fps60 => (60, 1),
FrameRate::Fps120 => (120, 1),
}
}
pub fn is_drop_frame(&self) -> bool {
matches!(
self,
FrameRate::Fps2997DF
| FrameRate::Fps23976DF
| FrameRate::Fps5994DF
| FrameRate::Fps47952DF
)
}
pub fn drop_frames_per_minute(&self) -> u64 {
match self {
FrameRate::Fps23976DF => 2,
FrameRate::Fps2997DF => 2,
FrameRate::Fps47952DF => 4,
FrameRate::Fps5994DF => 4,
_ => 0,
}
}
pub fn frames_per_second(&self) -> u32 {
match self {
FrameRate::Fps23976 | FrameRate::Fps23976DF => 24,
FrameRate::Fps24 => 24,
FrameRate::Fps25 => 25,
FrameRate::Fps2997DF | FrameRate::Fps2997NDF => 30,
FrameRate::Fps30 => 30,
FrameRate::Fps47952 | FrameRate::Fps47952DF => 48,
FrameRate::Fps50 => 50,
FrameRate::Fps5994 | FrameRate::Fps5994DF => 60,
FrameRate::Fps60 => 60,
FrameRate::Fps120 => 120,
}
}
}
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub struct FrameRateInfo {
pub fps: u8,
pub drop_frame: bool,
}
impl PartialEq for FrameRateInfo {
fn eq(&self, other: &Self) -> bool {
self.fps == other.fps && self.drop_frame == other.drop_frame
}
}
impl Eq for FrameRateInfo {}
pub fn frame_rate_from_info(info: &FrameRateInfo) -> FrameRate {
match (info.fps, info.drop_frame) {
(24, true) => FrameRate::Fps23976DF,
(24, false) => FrameRate::Fps23976, (25, _) => FrameRate::Fps25,
(30, true) => FrameRate::Fps2997DF,
(30, false) => FrameRate::Fps2997NDF,
(48, true) => FrameRate::Fps47952DF,
(48, false) => FrameRate::Fps47952,
(50, _) => FrameRate::Fps50,
(60, true) => FrameRate::Fps5994DF,
(60, false) => FrameRate::Fps5994,
(120, _) => FrameRate::Fps120,
_ => FrameRate::Fps25, }
}
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub struct Timecode {
pub hours: u8,
pub minutes: u8,
pub seconds: u8,
pub frames: u8,
pub frame_rate: FrameRateInfo,
pub user_bits: u32,
#[serde(skip)]
frame_count_cache: u64,
}
impl PartialEq for Timecode {
fn eq(&self, other: &Self) -> bool {
self.hours == other.hours
&& self.minutes == other.minutes
&& self.seconds == other.seconds
&& self.frames == other.frames
&& self.frame_rate == other.frame_rate
&& self.user_bits == other.user_bits
}
}
impl Eq for Timecode {}
impl PartialOrd for Timecode {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Timecode {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.to_frames().cmp(&other.to_frames())
}
}
impl Timecode {
fn compute_frames_from_fields(
hours: u8,
minutes: u8,
seconds: u8,
frames: u8,
fps: u64,
drop_frame: bool,
) -> u64 {
let mut total = hours as u64 * 3600 * fps;
total += minutes as u64 * 60 * fps;
total += seconds as u64 * fps;
total += frames as u64;
if drop_frame {
let drop_per_min = if fps >= 60 { 4u64 } else { 2u64 };
let total_minutes = hours as u64 * 60 + minutes as u64;
let dropped_frames = drop_per_min * (total_minutes - total_minutes / 10);
total -= dropped_frames;
}
total
}
pub fn new(
hours: u8,
minutes: u8,
seconds: u8,
frames: u8,
frame_rate: FrameRate,
) -> Result<Self, TimecodeError> {
let fps = frame_rate.frames_per_second() as u8;
if hours > 23 {
return Err(TimecodeError::InvalidHours);
}
if minutes > 59 {
return Err(TimecodeError::InvalidMinutes);
}
if seconds > 59 {
return Err(TimecodeError::InvalidSeconds);
}
if frames >= fps {
return Err(TimecodeError::InvalidFrames);
}
if frame_rate.is_drop_frame() {
let drop_count = frame_rate.drop_frames_per_minute() as u8;
if seconds == 0 && frames < drop_count && !minutes.is_multiple_of(10) {
return Err(TimecodeError::InvalidDropFrame);
}
}
let drop_frame = frame_rate.is_drop_frame();
let frame_count_cache = Self::compute_frames_from_fields(
hours, minutes, seconds, frames, fps as u64, drop_frame,
);
Ok(Timecode {
hours,
minutes,
seconds,
frames,
frame_rate: FrameRateInfo { fps, drop_frame },
user_bits: 0,
frame_count_cache,
})
}
pub fn from_string(s: &str, frame_rate: FrameRate) -> Result<Self, TimecodeError> {
let s = s.trim();
if s.len() < 11 {
return Err(TimecodeError::InvalidConfiguration);
}
let parts: Vec<&str> = s.split([':', ';']).collect();
if parts.len() != 4 {
return Err(TimecodeError::InvalidConfiguration);
}
let hours: u8 = parts[0].parse().map_err(|_| TimecodeError::InvalidHours)?;
let minutes: u8 = parts[1]
.parse()
.map_err(|_| TimecodeError::InvalidMinutes)?;
let seconds: u8 = parts[2]
.parse()
.map_err(|_| TimecodeError::InvalidSeconds)?;
let frames: u8 = parts[3].parse().map_err(|_| TimecodeError::InvalidFrames)?;
Self::new(hours, minutes, seconds, frames, frame_rate)
}
pub fn from_raw_fields(
hours: u8,
minutes: u8,
seconds: u8,
frames: u8,
fps: u8,
drop_frame: bool,
user_bits: u32,
) -> Self {
let frame_count_cache = Self::compute_frames_from_fields(
hours, minutes, seconds, frames, fps as u64, drop_frame,
);
Self {
hours,
minutes,
seconds,
frames,
frame_rate: FrameRateInfo { fps, drop_frame },
user_bits,
frame_count_cache,
}
}
pub fn with_user_bits(mut self, user_bits: u32) -> Self {
self.user_bits = user_bits;
self
}
#[inline]
pub fn to_frames(&self) -> u64 {
self.frame_count_cache
}
#[allow(clippy::cast_precision_loss)]
pub fn to_seconds_f64(&self) -> f64 {
let rate = frame_rate_from_info(&self.frame_rate);
let (num, den) = rate.as_rational();
self.frame_count_cache as f64 * den as f64 / num as f64
}
pub fn from_frames(frames: u64, frame_rate: FrameRate) -> Result<Self, TimecodeError> {
let fps = frame_rate.frames_per_second() as u64;
let mut remaining = frames;
if frame_rate.is_drop_frame() {
let drop_per_min = frame_rate.drop_frames_per_minute();
let frames_per_minute = fps * 60 - drop_per_min;
let frames_per_10_minutes = frames_per_minute * 9 + fps * 60;
let ten_minute_blocks = remaining / frames_per_10_minutes;
remaining += ten_minute_blocks * (drop_per_min * 9);
let remaining_in_block = remaining % frames_per_10_minutes;
if remaining_in_block >= fps * 60 {
let extra_minutes = (remaining_in_block - fps * 60) / frames_per_minute;
remaining += (extra_minutes + 1) * drop_per_min;
}
}
let hours = (remaining / (fps * 3600)) as u8;
remaining %= fps * 3600;
let minutes = (remaining / (fps * 60)) as u8;
remaining %= fps * 60;
let seconds = (remaining / fps) as u8;
let frame = (remaining % fps) as u8;
Self::new(hours, minutes, seconds, frame, frame_rate)
}
pub fn increment(&mut self) -> Result<(), TimecodeError> {
self.frames += 1;
if self.frames >= self.frame_rate.fps {
self.frames = 0;
self.seconds += 1;
if self.seconds >= 60 {
self.seconds = 0;
self.minutes += 1;
if self.frame_rate.drop_frame && !self.minutes.is_multiple_of(10) {
let drop_count = if self.frame_rate.fps >= 60 { 4u8 } else { 2u8 };
self.frames = drop_count;
}
if self.minutes >= 60 {
self.minutes = 0;
self.hours += 1;
if self.hours >= 24 {
self.hours = 0;
}
}
}
}
self.frame_count_cache = Self::compute_frames_from_fields(
self.hours,
self.minutes,
self.seconds,
self.frames,
self.frame_rate.fps as u64,
self.frame_rate.drop_frame,
);
Ok(())
}
pub fn decrement(&mut self) -> Result<(), TimecodeError> {
if self.frames > 0 {
self.frames -= 1;
let drop_count = if self.frame_rate.fps >= 60 { 4u8 } else { 2u8 };
if self.frame_rate.drop_frame
&& self.seconds == 0
&& self.frames < drop_count
&& !self.minutes.is_multiple_of(10)
{
self.frames = self.frame_rate.fps - 1;
if self.seconds > 0 {
self.seconds -= 1;
} else {
self.seconds = 59;
if self.minutes > 0 {
self.minutes -= 1;
} else {
self.minutes = 59;
if self.hours > 0 {
self.hours -= 1;
} else {
self.hours = 23;
}
}
}
}
} else if self.seconds > 0 {
self.seconds -= 1;
self.frames = self.frame_rate.fps - 1;
} else {
self.seconds = 59;
self.frames = self.frame_rate.fps - 1;
if self.minutes > 0 {
self.minutes -= 1;
} else {
self.minutes = 59;
if self.hours > 0 {
self.hours -= 1;
} else {
self.hours = 23;
}
}
}
self.frame_count_cache = Self::compute_frames_from_fields(
self.hours,
self.minutes,
self.seconds,
self.frames,
self.frame_rate.fps as u64,
self.frame_rate.drop_frame,
);
Ok(())
}
}
impl std::ops::Add for Timecode {
type Output = Result<Timecode, TimecodeError>;
fn add(self, rhs: Timecode) -> Self::Output {
let rate = frame_rate_from_info(&self.frame_rate);
let fps = self.frame_rate.fps as u64;
let frames_per_day = fps * 86_400;
let sum = if frames_per_day > 0 {
(self.frame_count_cache + rhs.frame_count_cache) % frames_per_day
} else {
self.frame_count_cache + rhs.frame_count_cache
};
Timecode::from_frames(sum, rate)
}
}
impl std::ops::Sub for Timecode {
type Output = Result<Timecode, TimecodeError>;
fn sub(self, rhs: Timecode) -> Self::Output {
let rate = frame_rate_from_info(&self.frame_rate);
let fps = self.frame_rate.fps as u64;
let frames_per_day = fps * 86_400;
let result = if frames_per_day > 0 {
if self.frame_count_cache >= rhs.frame_count_cache {
self.frame_count_cache - rhs.frame_count_cache
} else {
frames_per_day - (rhs.frame_count_cache - self.frame_count_cache) % frames_per_day
}
} else {
self.frame_count_cache.saturating_sub(rhs.frame_count_cache)
};
Timecode::from_frames(result, rate)
}
}
impl std::ops::Add<u32> for Timecode {
type Output = Result<Timecode, TimecodeError>;
fn add(self, rhs: u32) -> Self::Output {
let rate = frame_rate_from_info(&self.frame_rate);
let fps = self.frame_rate.fps as u64;
let frames_per_day = fps * 86_400;
let sum = if frames_per_day > 0 {
(self.frame_count_cache + rhs as u64) % frames_per_day
} else {
self.frame_count_cache + rhs as u64
};
Timecode::from_frames(sum, rate)
}
}
impl fmt::Display for Timecode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let separator = if self.frame_rate.drop_frame { ';' } else { ':' };
write!(
f,
"{:02}:{:02}:{:02}{}{:02}",
self.hours, self.minutes, self.seconds, separator, self.frames
)
}
}
pub trait TimecodeReader {
fn read_timecode(&mut self) -> Result<Option<Timecode>, TimecodeError>;
fn frame_rate(&self) -> FrameRate;
fn is_synchronized(&self) -> bool;
}
pub trait TimecodeWriter {
fn write_timecode(&mut self, timecode: &Timecode) -> Result<(), TimecodeError>;
fn frame_rate(&self) -> FrameRate;
fn flush(&mut self) -> Result<(), TimecodeError>;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TimecodeError {
InvalidHours,
InvalidMinutes,
InvalidSeconds,
InvalidFrames,
InvalidDropFrame,
SyncNotFound,
CrcError,
BufferTooSmall,
InvalidConfiguration,
IoError(String),
NotSynchronized,
}
impl fmt::Display for TimecodeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TimecodeError::InvalidHours => write!(f, "Invalid hours value"),
TimecodeError::InvalidMinutes => write!(f, "Invalid minutes value"),
TimecodeError::InvalidSeconds => write!(f, "Invalid seconds value"),
TimecodeError::InvalidFrames => write!(f, "Invalid frames value"),
TimecodeError::InvalidDropFrame => write!(f, "Invalid drop frame timecode"),
TimecodeError::SyncNotFound => write!(f, "Sync word not found"),
TimecodeError::CrcError => write!(f, "CRC error"),
TimecodeError::BufferTooSmall => write!(f, "Buffer too small"),
TimecodeError::InvalidConfiguration => write!(f, "Invalid configuration"),
TimecodeError::IoError(e) => write!(f, "IO error: {}", e),
TimecodeError::NotSynchronized => write!(f, "Not synchronized"),
}
}
}
impl std::error::Error for TimecodeError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_timecode_creation() {
let tc = Timecode::new(1, 2, 3, 4, FrameRate::Fps25).expect("valid timecode");
assert_eq!(tc.hours, 1);
assert_eq!(tc.minutes, 2);
assert_eq!(tc.seconds, 3);
assert_eq!(tc.frames, 4);
}
#[test]
fn test_timecode_display() {
let tc = Timecode::new(1, 2, 3, 4, FrameRate::Fps25).expect("valid timecode");
assert_eq!(tc.to_string(), "01:02:03:04");
let tc_df = Timecode::new(1, 2, 3, 4, FrameRate::Fps2997DF).expect("valid timecode");
assert_eq!(tc_df.to_string(), "01:02:03;04");
}
#[test]
fn test_timecode_increment() {
let mut tc = Timecode::new(0, 0, 0, 24, FrameRate::Fps25).expect("valid timecode");
tc.increment().expect("increment should succeed");
assert_eq!(tc.frames, 0);
assert_eq!(tc.seconds, 1);
}
#[test]
fn test_frame_rate() {
assert_eq!(FrameRate::Fps25.as_float(), 25.0);
assert!((FrameRate::Fps2997DF.as_float() - 29.97002997).abs() < 1e-6);
assert!(FrameRate::Fps2997DF.is_drop_frame());
assert!(!FrameRate::Fps2997NDF.is_drop_frame());
}
#[test]
fn test_framerate_47952_and_120() {
assert_eq!(FrameRate::Fps47952.frames_per_second(), 48);
assert_eq!(FrameRate::Fps47952DF.frames_per_second(), 48);
assert_eq!(FrameRate::Fps120.frames_per_second(), 120);
assert!(!FrameRate::Fps47952.is_drop_frame());
assert!(FrameRate::Fps47952DF.is_drop_frame());
assert!(!FrameRate::Fps120.is_drop_frame());
assert_eq!(FrameRate::Fps47952.as_rational(), (48000, 1001));
assert_eq!(FrameRate::Fps120.as_rational(), (120, 1));
}
#[test]
fn test_from_string_ndf() {
let tc = Timecode::from_string("01:02:03:04", FrameRate::Fps25).expect("should parse");
assert_eq!(tc.hours, 1);
assert_eq!(tc.minutes, 2);
assert_eq!(tc.seconds, 3);
assert_eq!(tc.frames, 4);
}
#[test]
fn test_from_string_df() {
let tc = Timecode::from_string("01:02:03;04", FrameRate::Fps2997DF).expect("should parse");
assert_eq!(tc.frames, 4);
assert!(tc.frame_rate.drop_frame);
}
#[test]
fn test_from_string_invalid_too_short() {
assert!(Timecode::from_string("1:2:3:4", FrameRate::Fps25).is_err());
}
#[test]
fn test_from_string_invalid_parts() {
assert!(Timecode::from_string("01:02:03", FrameRate::Fps25).is_err());
}
#[test]
fn test_to_seconds_f64_one_hour_25fps() {
let tc = Timecode::new(1, 0, 0, 0, FrameRate::Fps25).expect("valid");
let secs = tc.to_seconds_f64();
assert!((secs - 3600.0).abs() < 1e-6);
}
#[test]
fn test_to_seconds_f64_pull_down() {
let tc = Timecode::new(0, 0, 0, 1, FrameRate::Fps2997NDF).expect("valid");
let expected = 1001.0 / 30000.0;
assert!((tc.to_seconds_f64() - expected).abs() < 1e-12);
}
#[test]
fn test_ord_timecodes() {
let tc1 = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
let tc2 = Timecode::new(0, 0, 0, 1, FrameRate::Fps25).expect("valid");
let tc3 = Timecode::new(1, 0, 0, 0, FrameRate::Fps25).expect("valid");
assert!(tc1 < tc2);
assert!(tc2 < tc3);
assert!(tc1 < tc3);
assert_eq!(tc1, tc1);
}
#[test]
fn test_add_timecodes() {
let tc1 = Timecode::new(0, 0, 1, 0, FrameRate::Fps25).expect("valid"); let tc2 = Timecode::new(0, 0, 2, 0, FrameRate::Fps25).expect("valid"); let result = (tc1 + tc2).expect("add should succeed");
assert_eq!(result.seconds, 3);
assert_eq!(result.frames, 0);
}
#[test]
fn test_sub_timecodes() {
let tc1 = Timecode::new(0, 0, 3, 0, FrameRate::Fps25).expect("valid"); let tc2 = Timecode::new(0, 0, 1, 0, FrameRate::Fps25).expect("valid"); let result = (tc1 - tc2).expect("sub should succeed");
assert_eq!(result.seconds, 2);
assert_eq!(result.frames, 0);
}
#[test]
fn test_add_u32_frames() {
let tc = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
let result = (tc + 25_u32).expect("add u32 should succeed");
assert_eq!(result.seconds, 1);
assert_eq!(result.frames, 0);
let tc_near_end = Timecode::new(23, 59, 59, 24, FrameRate::Fps25).expect("valid");
let wrapped = (tc_near_end + 1_u32).expect("wrap should succeed");
assert_eq!(wrapped.hours, 0);
assert_eq!(wrapped.minutes, 0);
assert_eq!(wrapped.seconds, 0);
assert_eq!(wrapped.frames, 0);
}
#[test]
fn test_frame_count_cache_matches_recomputed() {
let tc = Timecode::new(1, 23, 45, 12, FrameRate::Fps25).expect("valid");
let expected: u64 = 1 * 3600 * 25 + 23 * 60 * 25 + 45 * 25 + 12;
assert_eq!(tc.to_frames(), expected);
}
#[test]
fn test_frame_count_cache_after_increment() {
let mut tc = Timecode::new(0, 0, 0, 24, FrameRate::Fps25).expect("valid");
let before = tc.to_frames();
tc.increment().expect("ok");
assert_eq!(tc.to_frames(), before + 1);
}
#[test]
fn test_frame_rate_from_info() {
let info = FrameRateInfo {
fps: 25,
drop_frame: false,
};
assert_eq!(frame_rate_from_info(&info), FrameRate::Fps25);
let info_df = FrameRateInfo {
fps: 30,
drop_frame: true,
};
assert_eq!(frame_rate_from_info(&info_df), FrameRate::Fps2997DF);
let info_120 = FrameRateInfo {
fps: 120,
drop_frame: false,
};
assert_eq!(frame_rate_from_info(&info_120), FrameRate::Fps120);
}
}