1#![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 compare;
23pub mod continuity;
24pub mod drop_frame;
25pub mod duration;
26pub mod embedded_tc;
27pub mod frame_offset;
28pub mod frame_rate;
29pub mod jam_sync;
30pub mod ltc;
31pub mod ltc_encoder;
32pub mod ltc_parser;
33pub mod ltc_simd;
34pub mod midi_timecode;
35pub mod reader;
36pub mod simd_manchester;
37pub mod subframe;
38pub mod sync;
39pub mod sync_map;
40pub mod tc_calculator;
41pub mod tc_compare;
42pub mod tc_convert;
43pub mod tc_drift;
44pub mod tc_interpolate;
45pub mod tc_list;
46pub mod tc_math;
47pub mod tc_metadata;
48pub mod tc_offset_table;
49pub mod tc_range;
50pub mod tc_sequence;
51pub mod tc_smpte_ranges;
52pub mod tc_subtitle_sync;
53pub mod tc_validator;
54pub mod timecode_calculator;
55pub mod timecode_display;
56pub mod timecode_event;
57pub mod timecode_format;
58pub mod timecode_generator;
59pub mod timecode_log;
60pub mod timecode_overlay;
61pub mod timecode_range;
62pub mod vitc;
63
64use std::fmt;
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
68pub enum FrameRate {
69 Fps23976,
71 Fps23976DF,
73 Fps24,
75 Fps25,
77 Fps2997DF,
79 Fps2997NDF,
81 Fps30,
83 Fps47952,
85 Fps47952DF,
87 Fps50,
89 Fps5994,
91 Fps5994DF,
93 Fps60,
95 Fps120,
97}
98
99impl FrameRate {
100 pub fn as_float(&self) -> f64 {
102 match self {
103 FrameRate::Fps23976 | FrameRate::Fps23976DF => 24000.0 / 1001.0,
104 FrameRate::Fps24 => 24.0,
105 FrameRate::Fps25 => 25.0,
106 FrameRate::Fps2997DF | FrameRate::Fps2997NDF => 30000.0 / 1001.0,
107 FrameRate::Fps30 => 30.0,
108 FrameRate::Fps47952 | FrameRate::Fps47952DF => 48000.0 / 1001.0,
109 FrameRate::Fps50 => 50.0,
110 FrameRate::Fps5994 | FrameRate::Fps5994DF => 60000.0 / 1001.0,
111 FrameRate::Fps60 => 60.0,
112 FrameRate::Fps120 => 120.0,
113 }
114 }
115
116 pub fn as_rational(&self) -> (u32, u32) {
118 match self {
119 FrameRate::Fps23976 | FrameRate::Fps23976DF => (24000, 1001),
120 FrameRate::Fps24 => (24, 1),
121 FrameRate::Fps25 => (25, 1),
122 FrameRate::Fps2997DF | FrameRate::Fps2997NDF => (30000, 1001),
123 FrameRate::Fps30 => (30, 1),
124 FrameRate::Fps47952 | FrameRate::Fps47952DF => (48000, 1001),
125 FrameRate::Fps50 => (50, 1),
126 FrameRate::Fps5994 | FrameRate::Fps5994DF => (60000, 1001),
127 FrameRate::Fps60 => (60, 1),
128 FrameRate::Fps120 => (120, 1),
129 }
130 }
131
132 pub fn is_drop_frame(&self) -> bool {
134 matches!(
135 self,
136 FrameRate::Fps2997DF
137 | FrameRate::Fps23976DF
138 | FrameRate::Fps5994DF
139 | FrameRate::Fps47952DF
140 )
141 }
142
143 pub fn drop_frames_per_minute(&self) -> u64 {
150 match self {
151 FrameRate::Fps23976DF => 2,
152 FrameRate::Fps2997DF => 2,
153 FrameRate::Fps47952DF => 4,
154 FrameRate::Fps5994DF => 4,
155 _ => 0,
156 }
157 }
158
159 pub fn frames_per_second(&self) -> u32 {
161 match self {
162 FrameRate::Fps23976 | FrameRate::Fps23976DF => 24,
163 FrameRate::Fps24 => 24,
164 FrameRate::Fps25 => 25,
165 FrameRate::Fps2997DF | FrameRate::Fps2997NDF => 30,
166 FrameRate::Fps30 => 30,
167 FrameRate::Fps47952 | FrameRate::Fps47952DF => 48,
168 FrameRate::Fps50 => 50,
169 FrameRate::Fps5994 | FrameRate::Fps5994DF => 60,
170 FrameRate::Fps60 => 60,
171 FrameRate::Fps120 => 120,
172 }
173 }
174}
175
176#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
178pub struct FrameRateInfo {
179 pub fps: u8,
181 pub drop_frame: bool,
183}
184
185impl PartialEq for FrameRateInfo {
186 fn eq(&self, other: &Self) -> bool {
187 self.fps == other.fps && self.drop_frame == other.drop_frame
188 }
189}
190
191impl Eq for FrameRateInfo {}
192
193pub fn frame_rate_from_info(info: &FrameRateInfo) -> FrameRate {
199 match (info.fps, info.drop_frame) {
200 (24, true) => FrameRate::Fps23976DF,
201 (24, false) => FrameRate::Fps23976, (25, _) => FrameRate::Fps25,
203 (30, true) => FrameRate::Fps2997DF,
204 (30, false) => FrameRate::Fps2997NDF,
205 (48, true) => FrameRate::Fps47952DF,
206 (48, false) => FrameRate::Fps47952,
207 (50, _) => FrameRate::Fps50,
208 (60, true) => FrameRate::Fps5994DF,
209 (60, false) => FrameRate::Fps5994,
210 (120, _) => FrameRate::Fps120,
211 _ => FrameRate::Fps25, }
213}
214
215#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
222pub struct Timecode {
223 pub hours: u8,
225 pub minutes: u8,
227 pub seconds: u8,
229 pub frames: u8,
231 pub frame_rate: FrameRateInfo,
233 pub user_bits: u32,
235 #[serde(skip)]
237 frame_count_cache: u64,
238}
239
240impl PartialEq for Timecode {
241 fn eq(&self, other: &Self) -> bool {
242 self.hours == other.hours
243 && self.minutes == other.minutes
244 && self.seconds == other.seconds
245 && self.frames == other.frames
246 && self.frame_rate == other.frame_rate
247 && self.user_bits == other.user_bits
248 }
249}
250
251impl Eq for Timecode {}
252
253impl PartialOrd for Timecode {
254 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
255 Some(self.cmp(other))
256 }
257}
258
259impl Ord for Timecode {
260 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
261 self.to_frames().cmp(&other.to_frames())
262 }
263}
264
265impl Timecode {
266 fn compute_frames_from_fields(
272 hours: u8,
273 minutes: u8,
274 seconds: u8,
275 frames: u8,
276 fps: u64,
277 drop_per_min: u64,
278 ) -> u64 {
279 let mut total = hours as u64 * 3600 * fps;
280 total += minutes as u64 * 60 * fps;
281 total += seconds as u64 * fps;
282 total += frames as u64;
283
284 if drop_per_min > 0 {
285 let total_minutes = hours as u64 * 60 + minutes as u64;
286 let dropped_frames = drop_per_min * (total_minutes - total_minutes / 10);
287 total -= dropped_frames;
288 }
289
290 total
291 }
292
293 pub fn new(
295 hours: u8,
296 minutes: u8,
297 seconds: u8,
298 frames: u8,
299 frame_rate: FrameRate,
300 ) -> Result<Self, TimecodeError> {
301 let fps = frame_rate.frames_per_second() as u8;
302
303 if hours > 23 {
304 return Err(TimecodeError::InvalidHours);
305 }
306 if minutes > 59 {
307 return Err(TimecodeError::InvalidMinutes);
308 }
309 if seconds > 59 {
310 return Err(TimecodeError::InvalidSeconds);
311 }
312 if frames >= fps {
313 return Err(TimecodeError::InvalidFrames);
314 }
315
316 if frame_rate.is_drop_frame() {
318 let drop_count = frame_rate.drop_frames_per_minute() as u8;
319 if seconds == 0 && frames < drop_count && !minutes.is_multiple_of(10) {
322 return Err(TimecodeError::InvalidDropFrame);
323 }
324 }
325
326 let drop_frame = frame_rate.is_drop_frame();
327 let drop_per_min = frame_rate.drop_frames_per_minute();
328 let frame_count_cache = Self::compute_frames_from_fields(
329 hours,
330 minutes,
331 seconds,
332 frames,
333 fps as u64,
334 drop_per_min,
335 );
336
337 Ok(Timecode {
338 hours,
339 minutes,
340 seconds,
341 frames,
342 frame_rate: FrameRateInfo { fps, drop_frame },
343 user_bits: 0,
344 frame_count_cache,
345 })
346 }
347
348 pub fn from_string(s: &str, frame_rate: FrameRate) -> Result<Self, TimecodeError> {
361 let s = s.trim();
362 if s.len() < 11 {
364 return Err(TimecodeError::InvalidConfiguration);
365 }
366
367 let parts: Vec<&str> = s.split([':', ';']).collect();
369 if parts.len() != 4 {
370 return Err(TimecodeError::InvalidConfiguration);
371 }
372
373 let hours: u8 = parts[0].parse().map_err(|_| TimecodeError::InvalidHours)?;
374 let minutes: u8 = parts[1]
375 .parse()
376 .map_err(|_| TimecodeError::InvalidMinutes)?;
377 let seconds: u8 = parts[2]
378 .parse()
379 .map_err(|_| TimecodeError::InvalidSeconds)?;
380 let frames: u8 = parts[3].parse().map_err(|_| TimecodeError::InvalidFrames)?;
381
382 Self::new(hours, minutes, seconds, frames, frame_rate)
383 }
384
385 fn infer_drop_per_min(fps: u8, drop_frame: bool) -> u64 {
391 if !drop_frame {
392 0
393 } else if fps >= 48 {
394 4
395 } else {
396 2
397 }
398 }
399
400 pub fn from_raw_fields(
406 hours: u8,
407 minutes: u8,
408 seconds: u8,
409 frames: u8,
410 fps: u8,
411 drop_frame: bool,
412 user_bits: u32,
413 ) -> Self {
414 let drop_per_min = Self::infer_drop_per_min(fps, drop_frame);
415 let frame_count_cache = Self::compute_frames_from_fields(
416 hours,
417 minutes,
418 seconds,
419 frames,
420 fps as u64,
421 drop_per_min,
422 );
423 Self {
424 hours,
425 minutes,
426 seconds,
427 frames,
428 frame_rate: FrameRateInfo { fps, drop_frame },
429 user_bits,
430 frame_count_cache,
431 }
432 }
433
434 pub fn with_user_bits(mut self, user_bits: u32) -> Self {
436 self.user_bits = user_bits;
437 self
438 }
439
440 #[inline]
444 pub fn to_frames(&self) -> u64 {
445 self.frame_count_cache
446 }
447
448 #[allow(clippy::cast_precision_loss)]
453 pub fn to_seconds_f64(&self) -> f64 {
454 let rate = frame_rate_from_info(&self.frame_rate);
455 let (num, den) = rate.as_rational();
456 self.frame_count_cache as f64 * den as f64 / num as f64
458 }
459
460 pub fn from_frames(frames: u64, frame_rate: FrameRate) -> Result<Self, TimecodeError> {
479 let fps = frame_rate.frames_per_second() as u64;
480
481 if !frame_rate.is_drop_frame() {
482 let mut remaining = frames;
484 let hours = (remaining / (fps * 3600)) as u8;
485 remaining %= fps * 3600;
486 let minutes = (remaining / (fps * 60)) as u8;
487 remaining %= fps * 60;
488 let seconds = (remaining / fps) as u8;
489 let frame = (remaining % fps) as u8;
490 return Self::new(hours, minutes, seconds, frame, frame_rate);
491 }
492
493 let drop_per_min = frame_rate.drop_frames_per_minute();
495 let non_drop_min_frames: u64 = fps * 60;
497 let drop_min_frames: u64 = non_drop_min_frames - drop_per_min;
499 let frames_per_10min: u64 = non_drop_min_frames + drop_min_frames * 9;
502
503 let t = frames;
504 let d_10 = t / frames_per_10min;
506 let d_10_remainder = t % frames_per_10min;
507
508 let (minutes_in_block, frames_in_minute) = if d_10_remainder < non_drop_min_frames {
512 (0u64, d_10_remainder)
513 } else {
514 let r = d_10_remainder - non_drop_min_frames;
515 let min_in_blk = 1 + r / drop_min_frames;
516 let frm_in_min = r % drop_min_frames;
517 (min_in_blk, frm_in_min)
518 };
519
520 let total_minutes = d_10 * 10 + minutes_in_block;
521 let hours = (total_minutes / 60) as u8;
522 let minutes = (total_minutes % 60) as u8;
523
524 let displayed_position = if minutes_in_block > 0 {
530 frames_in_minute + drop_per_min
531 } else {
532 frames_in_minute
533 };
534 let seconds = (displayed_position / fps) as u8;
535 let frame = (displayed_position % fps) as u8;
536
537 Self::new(hours, minutes, seconds, frame, frame_rate)
538 }
539
540 pub fn increment(&mut self) -> Result<(), TimecodeError> {
542 self.frames += 1;
543
544 if self.frames >= self.frame_rate.fps {
545 self.frames = 0;
546 self.seconds += 1;
547
548 if self.seconds >= 60 {
549 self.seconds = 0;
550 self.minutes += 1;
551
552 if self.frame_rate.drop_frame && !self.minutes.is_multiple_of(10) {
554 let drop_count =
555 Self::infer_drop_per_min(self.frame_rate.fps, self.frame_rate.drop_frame)
556 as u8;
557 self.frames = drop_count;
558 }
559
560 if self.minutes >= 60 {
561 self.minutes = 0;
562 self.hours += 1;
563
564 if self.hours >= 24 {
565 self.hours = 0;
566 }
567 }
568 }
569 }
570
571 let drop_per_min =
573 Self::infer_drop_per_min(self.frame_rate.fps, self.frame_rate.drop_frame);
574 self.frame_count_cache = Self::compute_frames_from_fields(
575 self.hours,
576 self.minutes,
577 self.seconds,
578 self.frames,
579 self.frame_rate.fps as u64,
580 drop_per_min,
581 );
582
583 Ok(())
584 }
585
586 pub fn decrement(&mut self) -> Result<(), TimecodeError> {
588 if self.frames > 0 {
589 self.frames -= 1;
590
591 let drop_count =
593 Self::infer_drop_per_min(self.frame_rate.fps, self.frame_rate.drop_frame) as u8;
594 if self.frame_rate.drop_frame
595 && self.seconds == 0
596 && self.frames < drop_count
597 && !self.minutes.is_multiple_of(10)
598 {
599 self.frames = self.frame_rate.fps - 1;
600 if self.seconds > 0 {
601 self.seconds -= 1;
602 } else {
603 self.seconds = 59;
604 if self.minutes > 0 {
605 self.minutes -= 1;
606 } else {
607 self.minutes = 59;
608 if self.hours > 0 {
609 self.hours -= 1;
610 } else {
611 self.hours = 23;
612 }
613 }
614 }
615 }
616 } else if self.seconds > 0 {
617 self.seconds -= 1;
618 self.frames = self.frame_rate.fps - 1;
619 } else {
620 self.seconds = 59;
621 self.frames = self.frame_rate.fps - 1;
622
623 if self.minutes > 0 {
624 self.minutes -= 1;
625 } else {
626 self.minutes = 59;
627 if self.hours > 0 {
628 self.hours -= 1;
629 } else {
630 self.hours = 23;
631 }
632 }
633 }
634
635 let drop_per_min =
637 Self::infer_drop_per_min(self.frame_rate.fps, self.frame_rate.drop_frame);
638 self.frame_count_cache = Self::compute_frames_from_fields(
639 self.hours,
640 self.minutes,
641 self.seconds,
642 self.frames,
643 self.frame_rate.fps as u64,
644 drop_per_min,
645 );
646
647 Ok(())
648 }
649}
650
651impl std::ops::Add for Timecode {
656 type Output = Result<Timecode, TimecodeError>;
657
658 fn add(self, rhs: Timecode) -> Self::Output {
663 let rate = frame_rate_from_info(&self.frame_rate);
664 let fps = self.frame_rate.fps as u64;
665 let frames_per_day = fps * 86_400;
666
667 let sum = if frames_per_day > 0 {
668 (self.frame_count_cache + rhs.frame_count_cache) % frames_per_day
669 } else {
670 self.frame_count_cache + rhs.frame_count_cache
671 };
672
673 Timecode::from_frames(sum, rate)
674 }
675}
676
677impl std::ops::Sub for Timecode {
678 type Output = Result<Timecode, TimecodeError>;
679
680 fn sub(self, rhs: Timecode) -> Self::Output {
685 let rate = frame_rate_from_info(&self.frame_rate);
686 let fps = self.frame_rate.fps as u64;
687 let frames_per_day = fps * 86_400;
688
689 let result = if frames_per_day > 0 {
690 if self.frame_count_cache >= rhs.frame_count_cache {
691 self.frame_count_cache - rhs.frame_count_cache
692 } else {
693 frames_per_day - (rhs.frame_count_cache - self.frame_count_cache) % frames_per_day
695 }
696 } else {
697 self.frame_count_cache.saturating_sub(rhs.frame_count_cache)
698 };
699
700 Timecode::from_frames(result, rate)
701 }
702}
703
704impl std::ops::Add<u32> for Timecode {
705 type Output = Result<Timecode, TimecodeError>;
706
707 fn add(self, rhs: u32) -> Self::Output {
711 let rate = frame_rate_from_info(&self.frame_rate);
712 let fps = self.frame_rate.fps as u64;
713 let frames_per_day = fps * 86_400;
714
715 let sum = if frames_per_day > 0 {
716 (self.frame_count_cache + rhs as u64) % frames_per_day
717 } else {
718 self.frame_count_cache + rhs as u64
719 };
720
721 Timecode::from_frames(sum, rate)
722 }
723}
724
725impl fmt::Display for Timecode {
730 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
731 let separator = if self.frame_rate.drop_frame { ';' } else { ':' };
732 write!(
733 f,
734 "{:02}:{:02}:{:02}{}{:02}",
735 self.hours, self.minutes, self.seconds, separator, self.frames
736 )
737 }
738}
739
740pub trait TimecodeReader {
746 fn read_timecode(&mut self) -> Result<Option<Timecode>, TimecodeError>;
748
749 fn frame_rate(&self) -> FrameRate;
751
752 fn is_synchronized(&self) -> bool;
754}
755
756pub trait TimecodeWriter {
758 fn write_timecode(&mut self, timecode: &Timecode) -> Result<(), TimecodeError>;
760
761 fn frame_rate(&self) -> FrameRate;
763
764 fn flush(&mut self) -> Result<(), TimecodeError>;
766}
767
768#[derive(Debug, Clone, PartialEq, Eq)]
774pub enum TimecodeError {
775 InvalidHours,
777 InvalidMinutes,
779 InvalidSeconds,
781 InvalidFrames,
783 InvalidDropFrame,
785 SyncNotFound,
787 CrcError,
789 BufferTooSmall,
791 InvalidConfiguration,
793 IoError(String),
795 NotSynchronized,
797}
798
799impl fmt::Display for TimecodeError {
800 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
801 match self {
802 TimecodeError::InvalidHours => write!(f, "Invalid hours value"),
803 TimecodeError::InvalidMinutes => write!(f, "Invalid minutes value"),
804 TimecodeError::InvalidSeconds => write!(f, "Invalid seconds value"),
805 TimecodeError::InvalidFrames => write!(f, "Invalid frames value"),
806 TimecodeError::InvalidDropFrame => write!(f, "Invalid drop frame timecode"),
807 TimecodeError::SyncNotFound => write!(f, "Sync word not found"),
808 TimecodeError::CrcError => write!(f, "CRC error"),
809 TimecodeError::BufferTooSmall => write!(f, "Buffer too small"),
810 TimecodeError::InvalidConfiguration => write!(f, "Invalid configuration"),
811 TimecodeError::IoError(e) => write!(f, "IO error: {}", e),
812 TimecodeError::NotSynchronized => write!(f, "Not synchronized"),
813 }
814 }
815}
816
817impl std::error::Error for TimecodeError {}
818
819#[cfg(test)]
824mod tests {
825 use super::*;
826
827 #[test]
828 fn test_timecode_creation() {
829 let tc = Timecode::new(1, 2, 3, 4, FrameRate::Fps25).expect("valid timecode");
830 assert_eq!(tc.hours, 1);
831 assert_eq!(tc.minutes, 2);
832 assert_eq!(tc.seconds, 3);
833 assert_eq!(tc.frames, 4);
834 }
835
836 #[test]
837 fn test_timecode_display() {
838 let tc = Timecode::new(1, 2, 3, 4, FrameRate::Fps25).expect("valid timecode");
839 assert_eq!(tc.to_string(), "01:02:03:04");
840
841 let tc_df = Timecode::new(1, 2, 3, 4, FrameRate::Fps2997DF).expect("valid timecode");
842 assert_eq!(tc_df.to_string(), "01:02:03;04");
843 }
844
845 #[test]
846 fn test_timecode_increment() {
847 let mut tc = Timecode::new(0, 0, 0, 24, FrameRate::Fps25).expect("valid timecode");
848 tc.increment().expect("increment should succeed");
849 assert_eq!(tc.frames, 0);
850 assert_eq!(tc.seconds, 1);
851 }
852
853 #[test]
854 fn test_frame_rate() {
855 assert_eq!(FrameRate::Fps25.as_float(), 25.0);
856 assert!((FrameRate::Fps2997DF.as_float() - 29.97002997).abs() < 1e-6);
857 assert!(FrameRate::Fps2997DF.is_drop_frame());
858 assert!(!FrameRate::Fps2997NDF.is_drop_frame());
859 }
860
861 #[test]
862 fn test_framerate_47952_and_120() {
863 assert_eq!(FrameRate::Fps47952.frames_per_second(), 48);
864 assert_eq!(FrameRate::Fps47952DF.frames_per_second(), 48);
865 assert_eq!(FrameRate::Fps120.frames_per_second(), 120);
866 assert!(!FrameRate::Fps47952.is_drop_frame());
867 assert!(FrameRate::Fps47952DF.is_drop_frame());
868 assert!(!FrameRate::Fps120.is_drop_frame());
869 assert_eq!(FrameRate::Fps47952.as_rational(), (48000, 1001));
870 assert_eq!(FrameRate::Fps120.as_rational(), (120, 1));
871 }
872
873 #[test]
874 fn test_from_string_ndf() {
875 let tc = Timecode::from_string("01:02:03:04", FrameRate::Fps25).expect("should parse");
876 assert_eq!(tc.hours, 1);
877 assert_eq!(tc.minutes, 2);
878 assert_eq!(tc.seconds, 3);
879 assert_eq!(tc.frames, 4);
880 }
881
882 #[test]
883 fn test_from_string_df() {
884 let tc = Timecode::from_string("01:02:03;04", FrameRate::Fps2997DF).expect("should parse");
886 assert_eq!(tc.frames, 4);
887 assert!(tc.frame_rate.drop_frame);
888 }
889
890 #[test]
891 fn test_from_string_invalid_too_short() {
892 assert!(Timecode::from_string("1:2:3:4", FrameRate::Fps25).is_err());
893 }
894
895 #[test]
896 fn test_from_string_invalid_parts() {
897 assert!(Timecode::from_string("01:02:03", FrameRate::Fps25).is_err());
898 }
899
900 #[test]
901 fn test_to_seconds_f64_one_hour_25fps() {
902 let tc = Timecode::new(1, 0, 0, 0, FrameRate::Fps25).expect("valid");
903 let secs = tc.to_seconds_f64();
904 assert!((secs - 3600.0).abs() < 1e-6);
905 }
906
907 #[test]
908 fn test_to_seconds_f64_pull_down() {
909 let tc = Timecode::new(0, 0, 0, 1, FrameRate::Fps2997NDF).expect("valid");
911 let expected = 1001.0 / 30000.0;
912 assert!((tc.to_seconds_f64() - expected).abs() < 1e-12);
913 }
914
915 #[test]
916 fn test_ord_timecodes() {
917 let tc1 = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
918 let tc2 = Timecode::new(0, 0, 0, 1, FrameRate::Fps25).expect("valid");
919 let tc3 = Timecode::new(1, 0, 0, 0, FrameRate::Fps25).expect("valid");
920 assert!(tc1 < tc2);
921 assert!(tc2 < tc3);
922 assert!(tc1 < tc3);
923 assert_eq!(tc1, tc1);
924 }
925
926 #[test]
927 fn test_add_timecodes() {
928 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");
931 assert_eq!(result.seconds, 3);
932 assert_eq!(result.frames, 0);
933 }
934
935 #[test]
936 fn test_sub_timecodes() {
937 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");
940 assert_eq!(result.seconds, 2);
941 assert_eq!(result.frames, 0);
942 }
943
944 #[test]
945 fn test_add_u32_frames() {
946 let tc = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
948 let result = (tc + 25_u32).expect("add u32 should succeed");
949 assert_eq!(result.seconds, 1);
950 assert_eq!(result.frames, 0);
951
952 let tc_near_end = Timecode::new(23, 59, 59, 24, FrameRate::Fps25).expect("valid");
954 let wrapped = (tc_near_end + 1_u32).expect("wrap should succeed");
955 assert_eq!(wrapped.hours, 0);
956 assert_eq!(wrapped.minutes, 0);
957 assert_eq!(wrapped.seconds, 0);
958 assert_eq!(wrapped.frames, 0);
959 }
960
961 #[test]
962 fn test_frame_count_cache_matches_recomputed() {
963 let tc = Timecode::new(1, 23, 45, 12, FrameRate::Fps25).expect("valid");
964 let expected: u64 = 1 * 3600 * 25 + 23 * 60 * 25 + 45 * 25 + 12;
965 assert_eq!(tc.to_frames(), expected);
966 }
967
968 #[test]
969 fn test_frame_count_cache_after_increment() {
970 let mut tc = Timecode::new(0, 0, 0, 24, FrameRate::Fps25).expect("valid");
971 let before = tc.to_frames();
972 tc.increment().expect("ok");
973 assert_eq!(tc.to_frames(), before + 1);
974 }
975
976 #[test]
977 fn test_frame_rate_from_info() {
978 let info = FrameRateInfo {
979 fps: 25,
980 drop_frame: false,
981 };
982 assert_eq!(frame_rate_from_info(&info), FrameRate::Fps25);
983
984 let info_df = FrameRateInfo {
985 fps: 30,
986 drop_frame: true,
987 };
988 assert_eq!(frame_rate_from_info(&info_df), FrameRate::Fps2997DF);
989
990 let info_120 = FrameRateInfo {
991 fps: 120,
992 drop_frame: false,
993 };
994 assert_eq!(frame_rate_from_info(&info_120), FrameRate::Fps120);
995 }
996
997 #[test]
1000 fn test_fps47952_to_frames_round_trip() {
1001 let tc = Timecode::new(0, 1, 30, 20, FrameRate::Fps47952).expect("valid 47.952 timecode");
1003 let frame_count = tc.to_frames();
1004 let tc2 = Timecode::from_frames(frame_count, FrameRate::Fps47952)
1005 .expect("from_frames must succeed at 47.952");
1006 assert_eq!(tc, tc2, "47.952 NDF round-trip failed");
1007 }
1008
1009 #[test]
1010 fn test_fps47952df_to_frames_round_trip() {
1011 let tc = Timecode::new(0, 1, 0, 4, FrameRate::Fps47952DF).expect("valid 47.952DF tc");
1014 let n = tc.to_frames();
1015 let tc2 = Timecode::from_frames(n, FrameRate::Fps47952DF)
1016 .expect("from_frames must succeed for 47.952 DF");
1017 assert_eq!(tc, tc2, "47.952 DF round-trip failed");
1018 }
1019
1020 #[test]
1021 fn test_fps120_to_frames_round_trip() {
1022 let tc = Timecode::new(0, 0, 5, 60, FrameRate::Fps120).expect("valid 120fps timecode");
1023 let n = tc.to_frames();
1024 let tc2 = Timecode::from_frames(n, FrameRate::Fps120)
1025 .expect("from_frames must succeed at 120fps");
1026 assert_eq!(tc, tc2, "120fps round-trip failed");
1027 }
1028
1029 #[test]
1030 fn test_fps120_non_drop_only() {
1031 assert!(!FrameRate::Fps120.is_drop_frame());
1032 }
1033
1034 #[test]
1035 fn test_fps47952_rational() {
1036 assert_eq!(FrameRate::Fps47952.as_rational(), (48000, 1001));
1037 assert_eq!(FrameRate::Fps47952DF.as_rational(), (48000, 1001));
1038 }
1039
1040 #[test]
1041 fn test_fps120_rational() {
1042 assert_eq!(FrameRate::Fps120.as_rational(), (120, 1));
1043 }
1044
1045 #[test]
1046 fn test_fps47952_drop_frames_per_minute() {
1047 assert_eq!(FrameRate::Fps47952DF.drop_frames_per_minute(), 4);
1048 }
1049
1050 #[test]
1053 fn test_to_frames_is_idempotent() {
1054 let tc = Timecode::new(12, 34, 56, 7, FrameRate::Fps25).expect("valid");
1055 let first = tc.to_frames();
1056 let second = tc.to_frames();
1057 assert_eq!(
1058 first, second,
1059 "to_frames() must return the same value each call"
1060 );
1061 }
1062
1063 #[test]
1064 fn test_to_frames_consistent_after_clone() {
1065 let tc = Timecode::new(1, 2, 3, 4, FrameRate::Fps2997NDF).expect("valid");
1066 let cloned = tc;
1067 assert_eq!(tc.to_frames(), cloned.to_frames());
1068 }
1069
1070 #[test]
1073 fn test_drop_frame_boundary_minute_1_frame_0_invalid() {
1074 assert!(Timecode::new(0, 1, 0, 0, FrameRate::Fps2997DF).is_err());
1076 }
1077
1078 #[test]
1079 fn test_drop_frame_boundary_minute_1_frame_1_invalid() {
1080 assert!(Timecode::new(0, 1, 0, 1, FrameRate::Fps2997DF).is_err());
1081 }
1082
1083 #[test]
1084 fn test_drop_frame_boundary_minute_1_frame_2_valid() {
1085 assert!(Timecode::new(0, 1, 0, 2, FrameRate::Fps2997DF).is_ok());
1086 }
1087
1088 #[test]
1089 fn test_drop_frame_boundary_minute_10_frame_0_valid() {
1090 assert!(Timecode::new(0, 10, 0, 0, FrameRate::Fps2997DF).is_ok());
1092 }
1093
1094 #[test]
1095 fn test_drop_frame_known_vector_00_01_00_02() {
1096 let tc =
1099 Timecode::from_frames(1800, FrameRate::Fps2997DF).expect("from_frames must succeed");
1100 assert_eq!(tc.hours, 0);
1101 assert_eq!(tc.minutes, 1);
1102 assert_eq!(tc.seconds, 0);
1103 assert_eq!(tc.frames, 2);
1104 }
1105
1106 #[test]
1109 fn test_increment_midnight_rollover() {
1110 let mut tc = Timecode::new(23, 59, 59, 24, FrameRate::Fps25).expect("valid");
1112 tc.increment().expect("increment must succeed");
1113 assert_eq!(tc.hours, 0);
1114 assert_eq!(tc.minutes, 0);
1115 assert_eq!(tc.seconds, 0);
1116 assert_eq!(tc.frames, 0);
1117 }
1118
1119 #[test]
1120 fn test_increment_minute_boundary_drop_frame() {
1121 let mut tc = Timecode::new(0, 0, 59, 29, FrameRate::Fps2997DF).expect("valid");
1123 tc.increment().expect("increment must succeed");
1124 assert_eq!(tc.minutes, 1);
1125 assert_eq!(tc.seconds, 0);
1126 assert_eq!(
1127 tc.frames, 2,
1128 "drop-frame skip: first frame in minute 1 must be 02"
1129 );
1130 }
1131
1132 #[test]
1133 fn test_increment_minute_10_boundary_no_skip() {
1134 let mut tc = Timecode::new(0, 9, 59, 29, FrameRate::Fps2997DF).expect("valid");
1136 tc.increment().expect("increment must succeed");
1137 assert_eq!(tc.minutes, 10);
1138 assert_eq!(tc.seconds, 0);
1139 assert_eq!(tc.frames, 0, "minute 10 is a keep-minute: frame 0 is valid");
1140 }
1141
1142 #[test]
1143 fn test_decrement_midnight_rollover() {
1144 let mut tc = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
1146 tc.decrement().expect("decrement must succeed");
1147 assert_eq!(tc.hours, 23);
1148 assert_eq!(tc.minutes, 59);
1149 assert_eq!(tc.seconds, 59);
1150 assert_eq!(tc.frames, 24);
1151 }
1152
1153 #[test]
1156 fn test_one_minute_dropframe_29_97_exact_frame_count() {
1157 let tc = Timecode::new(0, 2, 0, 2, FrameRate::Fps2997DF).expect("valid 2-minute tc");
1163 assert_eq!(
1164 tc.to_frames(),
1165 3598,
1166 "00;02;00;02 in 29.97DF must be real frame 3598"
1167 );
1168 }
1169
1170 #[test]
1171 fn test_one_minute_roundtrip_29_97df() {
1172 for n in 0u64..1800 {
1174 let tc = Timecode::from_frames(n, FrameRate::Fps2997DF)
1175 .unwrap_or_else(|_| panic!("from_frames({n}) must succeed"));
1176 assert_eq!(
1177 tc.to_frames(),
1178 n,
1179 "1-min round-trip failed at frame {n}: got {tc}"
1180 );
1181 }
1182 }
1183
1184 #[test]
1195 #[ignore = "exhaustive: iterates 24h at 29.97DF (~2.6M frames, ~10s)"]
1196 fn test_exhaustive_dropframe_29_97() {
1197 const FRAMES_PER_HOUR_29_97_DF: u64 = 107892;
1198 const TOTAL_FRAMES_24H: u64 = 24 * FRAMES_PER_HOUR_29_97_DF; for n in 0..TOTAL_FRAMES_24H {
1202 let tc = Timecode::from_frames(n, FrameRate::Fps2997DF)
1203 .unwrap_or_else(|_| panic!("from_frames({n}) must succeed"));
1204 let back = tc.to_frames();
1205 assert_eq!(
1206 back, n,
1207 "exhaustive 29.97DF round-trip failed at frame {n}: got {back}"
1208 );
1209 }
1210
1211 let total = TOTAL_FRAMES_24H;
1213 assert_eq!(
1214 total, 2_589_408,
1215 "SMPTE spec: 24h at 29.97DF = 2,589,408 frames"
1216 );
1217 }
1218
1219 #[test]
1237 fn test_ltc_noisy_round_trip_snr20db() {
1238 use crate::ltc::encoder::LtcEncoder;
1239
1240 let sample_rate = 48000u32;
1241 let frame_rate = FrameRate::Fps25;
1242 let target_tc = Timecode::new(1, 2, 3, 4, frame_rate).expect("valid timecode");
1243
1244 let clean: Vec<f32> = LtcEncoder::new(sample_rate, frame_rate, 1.0)
1246 .encode_frame(&target_tc)
1247 .expect("encode must succeed");
1248
1249 let expected_samples = (sample_rate as usize) / 25;
1251 assert_eq!(
1252 clean.len(),
1253 expected_samples,
1254 "LTC frame must contain exactly {expected_samples} samples at 25fps/48kHz"
1255 );
1256
1257 let sum_sq: f32 = clean.iter().map(|&s| s * s).sum();
1259 let rms = (sum_sq / clean.len() as f32).sqrt();
1260 let noise_amplitude = rms / 10.0;
1262
1263 let mut lcg: u64 = 0xDEAD_BEEF_CAFE_0101;
1264 let noisy: Vec<f32> = clean
1265 .iter()
1266 .map(|&s| {
1267 lcg = lcg
1268 .wrapping_mul(6364136223846793005)
1269 .wrapping_add(1442695040888963407);
1270 let n = ((lcg >> 33) as f32 / (1u32 << 31) as f32) - 1.0;
1271 s + n * noise_amplitude
1272 })
1273 .collect();
1274
1275 let noisy_peak: f32 = noisy.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
1279 let signal_peak: f32 = clean.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
1280 assert!(
1281 noisy_peak > signal_peak - noise_amplitude,
1282 "at 20 dB SNR the signal peak must remain detectable (noisy_peak={noisy_peak:.3}, \
1283 signal_peak={signal_peak:.3}, noise_amplitude={noise_amplitude:.3})"
1284 );
1285
1286 let batch = LtcEncoder::encode_batch_interleaved(&[target_tc, target_tc], sample_rate);
1289 assert_eq!(
1290 batch.len(),
1291 2 * expected_samples,
1292 "two-frame interleaved batch must be 2× single-frame length"
1293 );
1294 }
1295}