Skip to main content

talw_timecode/
dropframe.rs

1use crate::framerate::FrameRate;
2
3pub fn frames_to_components(total_frames: i64, rate: FrameRate) -> (u8, u8, u8, u8) {
4    let nom = rate.nominal();
5
6    if !rate.is_drop_frame() {
7        return non_drop_decompose(total_frames, nom);
8    }
9
10    let drop = rate.drop_count();
11    let frames_per_min = nom * 60 - drop;
12    let frames_per_10min = frames_per_min * 10 + drop;
13
14    let abs_frames = if total_frames < 0 {
15        let day_frames = frames_per_10min * 6 * 24;
16        ((total_frames % day_frames as i64) + day_frames as i64) as u64
17    } else {
18        total_frames as u64
19    };
20
21    let first_minute_frames = (nom * 60) as u64;
22
23    let ten_min_blocks = abs_frames / frames_per_10min as u64;
24    let remainder = abs_frames % frames_per_10min as u64;
25
26    let (minutes_in_block, frames_in_block) = if remainder < first_minute_frames {
27        (0u64, remainder)
28    } else {
29        let adjusted = remainder - first_minute_frames;
30        let mins = 1 + adjusted / frames_per_min as u64;
31        let fr = adjusted % frames_per_min as u64 + drop as u64;
32        (mins, fr)
33    };
34
35    let total_minutes = ten_min_blocks * 10 + minutes_in_block;
36    let hours = (total_minutes / 60) % 24;
37    let minutes = total_minutes % 60;
38    let seconds = frames_in_block / nom as u64;
39    let frames = frames_in_block % nom as u64;
40
41    (hours as u8, minutes as u8, seconds as u8, frames as u8)
42}
43
44pub fn components_to_frames(h: u8, m: u8, s: u8, f: u8, rate: FrameRate) -> i64 {
45    let nom = rate.nominal();
46
47    let nominal_frames =
48        h as i64 * 3600 * nom as i64
49        + m as i64 * 60 * nom as i64
50        + s as i64 * nom as i64
51        + f as i64;
52
53    if !rate.is_drop_frame() {
54        return nominal_frames;
55    }
56
57    let drop = rate.drop_count() as i64;
58    let total_minutes = h as i64 * 60 + m as i64;
59    let drop_adjustment = drop * (total_minutes - total_minutes / 10);
60
61    nominal_frames - drop_adjustment
62}
63
64fn non_drop_decompose(total_frames: i64, nom: u32) -> (u8, u8, u8, u8) {
65    let fps = nom as u64;
66    let day_frames = fps * 86400;
67
68    let abs_frames = if total_frames < 0 {
69        ((total_frames % day_frames as i64) + day_frames as i64) as u64
70    } else {
71        total_frames as u64
72    };
73
74    let frames = abs_frames % fps;
75    let total_secs = abs_frames / fps;
76    let seconds = total_secs % 60;
77    let total_mins = total_secs / 60;
78    let minutes = total_mins % 60;
79    let hours = (total_mins / 60) % 24;
80
81    (hours as u8, minutes as u8, seconds as u8, frames as u8)
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn non_drop_zero() {
90        assert_eq!(frames_to_components(0, FrameRate::Fps24), (0, 0, 0, 0));
91    }
92
93    #[test]
94    fn non_drop_one_second() {
95        assert_eq!(frames_to_components(24, FrameRate::Fps24), (0, 0, 1, 0));
96    }
97
98    #[test]
99    fn non_drop_one_hour() {
100        assert_eq!(
101            frames_to_components(86400, FrameRate::Fps24),
102            (1, 0, 0, 0)
103        );
104    }
105
106    #[test]
107    fn non_drop_roundtrip() {
108        for frames in [0, 1, 12, 24, 1439, 86400, 172800, 2073599] {
109            let (h, m, s, f) = frames_to_components(frames, FrameRate::Fps24);
110            assert_eq!(
111                components_to_frames(h, m, s, f, FrameRate::Fps24),
112                frames
113            );
114        }
115    }
116
117    #[test]
118    fn drop_frame_minute_boundary_29_97() {
119        // Frame 1799 = 00:00:59:29 (last frame before minute 1)
120        assert_eq!(
121            frames_to_components(1799, FrameRate::Fps29_97Df),
122            (0, 0, 59, 29)
123        );
124        // Frame 1800 = 00:01:00;02 (frames 0,1 are dropped)
125        assert_eq!(
126            frames_to_components(1800, FrameRate::Fps29_97Df),
127            (0, 1, 0, 2)
128        );
129    }
130
131    #[test]
132    fn drop_frame_10th_minute_29_97() {
133        // At 10-minute boundary, no frames are dropped
134        // 10 minutes = 10*60*30 - 9*2 = 17982 frames
135        let ten_min_frames = 17982i64;
136        assert_eq!(
137            frames_to_components(ten_min_frames, FrameRate::Fps29_97Df),
138            (0, 10, 0, 0)
139        );
140    }
141
142    #[test]
143    fn drop_frame_roundtrip_29_97() {
144        for frames in [0, 1, 2, 1798, 1799, 1800, 1801, 1802, 17982, 17983, 107892] {
145            let (h, m, s, f) = frames_to_components(frames, FrameRate::Fps29_97Df);
146            let back = components_to_frames(h, m, s, f, FrameRate::Fps29_97Df);
147            assert_eq!(back, frames, "roundtrip failed at frame {frames}: ({h}:{m}:{s};{f})");
148        }
149    }
150
151    #[test]
152    fn drop_frame_59_94_minute_boundary() {
153        // 59.94 DF drops 4 frames per minute (except every 10th)
154        // Frame 3599 = 00:00:59:59
155        assert_eq!(
156            frames_to_components(3599, FrameRate::Fps59_94Df),
157            (0, 0, 59, 59)
158        );
159        // Frame 3600 = 00:01:00;04
160        assert_eq!(
161            frames_to_components(3600, FrameRate::Fps59_94Df),
162            (0, 1, 0, 4)
163        );
164    }
165
166    #[test]
167    fn drop_frame_59_94_roundtrip() {
168        for frames in [0, 4, 3599, 3600, 3604, 35964, 35968] {
169            let (h, m, s, f) = frames_to_components(frames, FrameRate::Fps59_94Df);
170            let back = components_to_frames(h, m, s, f, FrameRate::Fps59_94Df);
171            assert_eq!(back, frames, "59.94DF roundtrip failed at frame {frames}");
172        }
173    }
174
175    #[test]
176    fn negative_wraps_to_24h() {
177        let (h, _, _, _) = frames_to_components(-24, FrameRate::Fps24);
178        assert_eq!(h, 23);
179    }
180}