Skip to main content

oximedia_timecode/
lib.rs

1//! OxiMedia Timecode - LTC and VITC reading and writing
2//!
3//! This crate provides SMPTE 12M compliant timecode reading and writing for:
4//! - LTC (Linear Timecode) - audio-based timecode
5//! - VITC (Vertical Interval Timecode) - video line-based timecode
6//!
7//! # Features
8//! - All standard frame rates (23.976, 24, 25, 29.97, 30, 50, 59.94, 60)
9//! - Drop frame and non-drop frame support
10//! - User bits encoding/decoding
11//! - Real-time capable
12//! - No unsafe code
13#![allow(
14    clippy::cast_possible_truncation,
15    clippy::cast_precision_loss,
16    clippy::cast_sign_loss,
17    dead_code,
18    clippy::pedantic
19)]
20
21pub mod burn_in;
22pub mod continuity;
23pub mod drop_frame;
24pub mod duration;
25pub mod frame_offset;
26pub mod frame_rate;
27pub mod ltc;
28pub mod ltc_encoder;
29pub mod ltc_parser;
30pub mod midi_timecode;
31pub mod reader;
32pub mod sync;
33pub mod sync_map;
34pub mod tc_calculator;
35pub mod tc_compare;
36pub mod tc_convert;
37pub mod tc_drift;
38pub mod tc_interpolate;
39pub mod tc_math;
40pub mod tc_metadata;
41pub mod tc_range;
42pub mod tc_smpte_ranges;
43pub mod tc_validator;
44pub mod timecode_calculator;
45pub mod timecode_format;
46pub mod timecode_range;
47pub mod vitc;
48
49use std::fmt;
50
51/// SMPTE timecode frame rates
52#[derive(Debug, Clone, Copy, PartialEq)]
53pub enum FrameRate {
54    /// 23.976 fps (film transferred to NTSC)
55    Fps23976,
56    /// 24 fps (film)
57    Fps24,
58    /// 25 fps (PAL)
59    Fps25,
60    /// 29.97 fps (NTSC drop frame)
61    Fps2997DF,
62    /// 29.97 fps (NTSC non-drop frame)
63    Fps2997NDF,
64    /// 30 fps
65    Fps30,
66    /// 50 fps (PAL progressive)
67    Fps50,
68    /// 59.94 fps (NTSC progressive)
69    Fps5994,
70    /// 60 fps
71    Fps60,
72}
73
74impl FrameRate {
75    /// Get the nominal frame rate as a float
76    pub fn as_float(&self) -> f64 {
77        match self {
78            FrameRate::Fps23976 => 23.976,
79            FrameRate::Fps24 => 24.0,
80            FrameRate::Fps25 => 25.0,
81            FrameRate::Fps2997DF | FrameRate::Fps2997NDF => 29.97,
82            FrameRate::Fps30 => 30.0,
83            FrameRate::Fps50 => 50.0,
84            FrameRate::Fps5994 => 59.94,
85            FrameRate::Fps60 => 60.0,
86        }
87    }
88
89    /// Get the exact frame rate as a rational (numerator, denominator)
90    pub fn as_rational(&self) -> (u32, u32) {
91        match self {
92            FrameRate::Fps23976 => (24000, 1001),
93            FrameRate::Fps24 => (24, 1),
94            FrameRate::Fps25 => (25, 1),
95            FrameRate::Fps2997DF | FrameRate::Fps2997NDF => (30000, 1001),
96            FrameRate::Fps30 => (30, 1),
97            FrameRate::Fps50 => (50, 1),
98            FrameRate::Fps5994 => (60000, 1001),
99            FrameRate::Fps60 => (60, 1),
100        }
101    }
102
103    /// Check if this is a drop frame rate
104    pub fn is_drop_frame(&self) -> bool {
105        matches!(self, FrameRate::Fps2997DF)
106    }
107
108    /// Get the number of frames per second (rounded)
109    pub fn frames_per_second(&self) -> u32 {
110        match self {
111            FrameRate::Fps23976 => 24,
112            FrameRate::Fps24 => 24,
113            FrameRate::Fps25 => 25,
114            FrameRate::Fps2997DF | FrameRate::Fps2997NDF => 30,
115            FrameRate::Fps30 => 30,
116            FrameRate::Fps50 => 50,
117            FrameRate::Fps5994 => 60,
118            FrameRate::Fps60 => 60,
119        }
120    }
121}
122
123/// SMPTE timecode structure
124#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
125pub struct Timecode {
126    /// Hours (0-23)
127    pub hours: u8,
128    /// Minutes (0-59)
129    pub minutes: u8,
130    /// Seconds (0-59)
131    pub seconds: u8,
132    /// Frames (0 to frame_rate - 1)
133    pub frames: u8,
134    /// Frame rate
135    pub frame_rate: FrameRateInfo,
136    /// User bits (32 bits)
137    pub user_bits: u32,
138}
139
140/// Frame rate information for timecode
141#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
142pub struct FrameRateInfo {
143    /// Frames per second (rounded)
144    pub fps: u8,
145    /// Drop frame flag
146    pub drop_frame: bool,
147}
148
149impl Timecode {
150    /// Create a new timecode
151    pub fn new(
152        hours: u8,
153        minutes: u8,
154        seconds: u8,
155        frames: u8,
156        frame_rate: FrameRate,
157    ) -> Result<Self, TimecodeError> {
158        let fps = frame_rate.frames_per_second() as u8;
159
160        if hours > 23 {
161            return Err(TimecodeError::InvalidHours);
162        }
163        if minutes > 59 {
164            return Err(TimecodeError::InvalidMinutes);
165        }
166        if seconds > 59 {
167            return Err(TimecodeError::InvalidSeconds);
168        }
169        if frames >= fps {
170            return Err(TimecodeError::InvalidFrames);
171        }
172
173        // Validate drop frame rules
174        if frame_rate.is_drop_frame() {
175            // Frames 0 and 1 are dropped at the start of each minute, except minutes 0, 10, 20, 30, 40, 50
176            if seconds == 0 && frames < 2 && !minutes.is_multiple_of(10) {
177                return Err(TimecodeError::InvalidDropFrame);
178            }
179        }
180
181        Ok(Timecode {
182            hours,
183            minutes,
184            seconds,
185            frames,
186            frame_rate: FrameRateInfo {
187                fps,
188                drop_frame: frame_rate.is_drop_frame(),
189            },
190            user_bits: 0,
191        })
192    }
193
194    /// Create timecode with user bits
195    pub fn with_user_bits(mut self, user_bits: u32) -> Self {
196        self.user_bits = user_bits;
197        self
198    }
199
200    /// Convert to total frames since midnight
201    pub fn to_frames(&self) -> u64 {
202        let fps = self.frame_rate.fps as u64;
203        let mut total = self.hours as u64 * 3600 * fps;
204        total += self.minutes as u64 * 60 * fps;
205        total += self.seconds as u64 * fps;
206        total += self.frames as u64;
207
208        // Adjust for drop frame
209        if self.frame_rate.drop_frame {
210            // Drop 2 frames per minute except every 10th minute
211            let total_minutes = self.hours as u64 * 60 + self.minutes as u64;
212            let dropped_frames = 2 * (total_minutes - total_minutes / 10);
213            total -= dropped_frames;
214        }
215
216        total
217    }
218
219    /// Create from total frames since midnight
220    pub fn from_frames(frames: u64, frame_rate: FrameRate) -> Result<Self, TimecodeError> {
221        let fps = frame_rate.frames_per_second() as u64;
222        let mut remaining = frames;
223
224        // Adjust for drop frame
225        if frame_rate.is_drop_frame() {
226            // This is an approximation; exact calculation is complex
227            let frames_per_minute = fps * 60 - 2;
228            let frames_per_10_minutes = frames_per_minute * 9 + fps * 60;
229
230            let ten_minute_blocks = remaining / frames_per_10_minutes;
231            remaining += ten_minute_blocks * 18;
232
233            let remaining_in_block = remaining % frames_per_10_minutes;
234            if remaining_in_block >= fps * 60 {
235                let extra_minutes = (remaining_in_block - fps * 60) / frames_per_minute;
236                remaining += (extra_minutes + 1) * 2;
237            }
238        }
239
240        let hours = (remaining / (fps * 3600)) as u8;
241        remaining %= fps * 3600;
242        let minutes = (remaining / (fps * 60)) as u8;
243        remaining %= fps * 60;
244        let seconds = (remaining / fps) as u8;
245        let frame = (remaining % fps) as u8;
246
247        Self::new(hours, minutes, seconds, frame, frame_rate)
248    }
249
250    /// Increment by one frame
251    pub fn increment(&mut self) -> Result<(), TimecodeError> {
252        self.frames += 1;
253
254        if self.frames >= self.frame_rate.fps {
255            self.frames = 0;
256            self.seconds += 1;
257
258            if self.seconds >= 60 {
259                self.seconds = 0;
260                self.minutes += 1;
261
262                // Handle drop frame
263                if self.frame_rate.drop_frame && !self.minutes.is_multiple_of(10) {
264                    self.frames = 2; // Skip frames 0 and 1
265                }
266
267                if self.minutes >= 60 {
268                    self.minutes = 0;
269                    self.hours += 1;
270
271                    if self.hours >= 24 {
272                        self.hours = 0;
273                    }
274                }
275            }
276        }
277
278        Ok(())
279    }
280
281    /// Decrement by one frame
282    pub fn decrement(&mut self) -> Result<(), TimecodeError> {
283        if self.frames > 0 {
284            self.frames -= 1;
285
286            // Check if we're in a drop frame position
287            if self.frame_rate.drop_frame
288                && self.seconds == 0
289                && self.frames == 1
290                && !self.minutes.is_multiple_of(10)
291            {
292                self.frames = self.frame_rate.fps - 1;
293                if self.seconds > 0 {
294                    self.seconds -= 1;
295                } else {
296                    self.seconds = 59;
297                    if self.minutes > 0 {
298                        self.minutes -= 1;
299                    } else {
300                        self.minutes = 59;
301                        if self.hours > 0 {
302                            self.hours -= 1;
303                        } else {
304                            self.hours = 23;
305                        }
306                    }
307                }
308            }
309        } else if self.seconds > 0 {
310            self.seconds -= 1;
311            self.frames = self.frame_rate.fps - 1;
312        } else {
313            self.seconds = 59;
314            self.frames = self.frame_rate.fps - 1;
315
316            if self.minutes > 0 {
317                self.minutes -= 1;
318            } else {
319                self.minutes = 59;
320                if self.hours > 0 {
321                    self.hours -= 1;
322                } else {
323                    self.hours = 23;
324                }
325            }
326        }
327
328        Ok(())
329    }
330}
331
332impl fmt::Display for Timecode {
333    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334        let separator = if self.frame_rate.drop_frame { ';' } else { ':' };
335        write!(
336            f,
337            "{:02}:{:02}:{:02}{}{:02}",
338            self.hours, self.minutes, self.seconds, separator, self.frames
339        )
340    }
341}
342
343/// Timecode reader trait
344pub trait TimecodeReader {
345    /// Read the next timecode from the source
346    fn read_timecode(&mut self) -> Result<Option<Timecode>, TimecodeError>;
347
348    /// Get the current frame rate
349    fn frame_rate(&self) -> FrameRate;
350
351    /// Check if the reader is synchronized
352    fn is_synchronized(&self) -> bool;
353}
354
355/// Timecode writer trait
356pub trait TimecodeWriter {
357    /// Write a timecode to the output
358    fn write_timecode(&mut self, timecode: &Timecode) -> Result<(), TimecodeError>;
359
360    /// Get the current frame rate
361    fn frame_rate(&self) -> FrameRate;
362
363    /// Flush any buffered data
364    fn flush(&mut self) -> Result<(), TimecodeError>;
365}
366
367/// Timecode errors
368#[derive(Debug, Clone, PartialEq, Eq)]
369pub enum TimecodeError {
370    /// Invalid hours value
371    InvalidHours,
372    /// Invalid minutes value
373    InvalidMinutes,
374    /// Invalid seconds value
375    InvalidSeconds,
376    /// Invalid frames value
377    InvalidFrames,
378    /// Invalid drop frame timecode
379    InvalidDropFrame,
380    /// Sync word not found
381    SyncNotFound,
382    /// CRC error
383    CrcError,
384    /// Buffer too small
385    BufferTooSmall,
386    /// Invalid configuration
387    InvalidConfiguration,
388    /// IO error
389    IoError(String),
390    /// Not synchronized
391    NotSynchronized,
392}
393
394impl fmt::Display for TimecodeError {
395    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
396        match self {
397            TimecodeError::InvalidHours => write!(f, "Invalid hours value"),
398            TimecodeError::InvalidMinutes => write!(f, "Invalid minutes value"),
399            TimecodeError::InvalidSeconds => write!(f, "Invalid seconds value"),
400            TimecodeError::InvalidFrames => write!(f, "Invalid frames value"),
401            TimecodeError::InvalidDropFrame => write!(f, "Invalid drop frame timecode"),
402            TimecodeError::SyncNotFound => write!(f, "Sync word not found"),
403            TimecodeError::CrcError => write!(f, "CRC error"),
404            TimecodeError::BufferTooSmall => write!(f, "Buffer too small"),
405            TimecodeError::InvalidConfiguration => write!(f, "Invalid configuration"),
406            TimecodeError::IoError(e) => write!(f, "IO error: {}", e),
407            TimecodeError::NotSynchronized => write!(f, "Not synchronized"),
408        }
409    }
410}
411
412impl std::error::Error for TimecodeError {}
413
414#[cfg(test)]
415mod tests {
416    use super::*;
417
418    #[test]
419    fn test_timecode_creation() {
420        let tc = Timecode::new(1, 2, 3, 4, FrameRate::Fps25).unwrap();
421        assert_eq!(tc.hours, 1);
422        assert_eq!(tc.minutes, 2);
423        assert_eq!(tc.seconds, 3);
424        assert_eq!(tc.frames, 4);
425    }
426
427    #[test]
428    fn test_timecode_display() {
429        let tc = Timecode::new(1, 2, 3, 4, FrameRate::Fps25).unwrap();
430        assert_eq!(tc.to_string(), "01:02:03:04");
431
432        let tc_df = Timecode::new(1, 2, 3, 4, FrameRate::Fps2997DF).unwrap();
433        assert_eq!(tc_df.to_string(), "01:02:03;04");
434    }
435
436    #[test]
437    fn test_timecode_increment() {
438        let mut tc = Timecode::new(0, 0, 0, 24, FrameRate::Fps25).unwrap();
439        tc.increment().unwrap();
440        assert_eq!(tc.frames, 0);
441        assert_eq!(tc.seconds, 1);
442    }
443
444    #[test]
445    fn test_frame_rate() {
446        assert_eq!(FrameRate::Fps25.as_float(), 25.0);
447        assert_eq!(FrameRate::Fps2997DF.as_float(), 29.97);
448        assert!(FrameRate::Fps2997DF.is_drop_frame());
449        assert!(!FrameRate::Fps2997NDF.is_drop_frame());
450    }
451}