1#![allow(dead_code)]
2use crate::{FrameRate, Timecode, TimecodeError};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ConvertStrategy {
12 PreserveTime,
14 PreserveFrame,
16 PreserveDisplay,
18}
19
20#[derive(Debug, Clone)]
22pub struct ConvertResult {
23 pub timecode: Timecode,
25 pub rounding_error_secs: f64,
27 pub exact: bool,
29}
30
31#[allow(clippy::cast_precision_loss)]
37pub fn convert_frame_rate(
38 tc: &Timecode,
39 target_rate: FrameRate,
40 strategy: ConvertStrategy,
41) -> Result<ConvertResult, TimecodeError> {
42 match strategy {
43 ConvertStrategy::PreserveTime => convert_preserve_time(tc, target_rate),
44 ConvertStrategy::PreserveFrame => convert_preserve_frame(tc, target_rate),
45 ConvertStrategy::PreserveDisplay => convert_preserve_display(tc, target_rate),
46 }
47}
48
49#[allow(clippy::cast_precision_loss)]
51fn convert_preserve_time(
52 tc: &Timecode,
53 target_rate: FrameRate,
54) -> Result<ConvertResult, TimecodeError> {
55 let source_fps = if tc.frame_rate.drop_frame {
56 29.97
57 } else {
58 tc.frame_rate.fps as f64
59 };
60 let src_frames = tc.to_frames();
61 let time_secs = src_frames as f64 / source_fps;
62
63 let target_fps = target_rate.as_float();
64 let target_frames = (time_secs * target_fps).round() as u64;
65
66 let result_tc = Timecode::from_frames(target_frames, target_rate)?;
67 let result_time = target_frames as f64 / target_fps;
68 let error = result_time - time_secs;
69
70 Ok(ConvertResult {
71 timecode: result_tc,
72 rounding_error_secs: error,
73 exact: error.abs() < 1e-9,
74 })
75}
76
77#[allow(clippy::cast_precision_loss)]
79fn convert_preserve_frame(
80 tc: &Timecode,
81 target_rate: FrameRate,
82) -> Result<ConvertResult, TimecodeError> {
83 let src_frames = tc.to_frames();
84 let result_tc = Timecode::from_frames(src_frames, target_rate)?;
85 let source_fps = if tc.frame_rate.drop_frame {
86 29.97
87 } else {
88 tc.frame_rate.fps as f64
89 };
90 let target_fps = target_rate.as_float();
91 let error = src_frames as f64 * (1.0 / target_fps - 1.0 / source_fps);
92
93 Ok(ConvertResult {
94 timecode: result_tc,
95 rounding_error_secs: error,
96 exact: (source_fps - target_fps).abs() < 1e-9,
97 })
98}
99
100#[allow(clippy::cast_precision_loss)]
102fn convert_preserve_display(
103 tc: &Timecode,
104 target_rate: FrameRate,
105) -> Result<ConvertResult, TimecodeError> {
106 let target_fps = target_rate.frames_per_second() as u8;
107 let frames = if tc.frames >= target_fps {
108 target_fps - 1
109 } else {
110 tc.frames
111 };
112 let result_tc = Timecode::new(tc.hours, tc.minutes, tc.seconds, frames, target_rate)?;
113 let source_fps = if tc.frame_rate.drop_frame {
114 29.97
115 } else {
116 tc.frame_rate.fps as f64
117 };
118 let tfps = target_rate.as_float();
119 let src_time = tc.to_frames() as f64 / source_fps;
120 let dst_time = result_tc.to_frames() as f64 / tfps;
121
122 Ok(ConvertResult {
123 timecode: result_tc,
124 rounding_error_secs: dst_time - src_time,
125 exact: false,
126 })
127}
128
129#[allow(clippy::cast_precision_loss)]
135pub fn seconds_to_timecode(secs: f64, rate: FrameRate) -> Result<Timecode, TimecodeError> {
136 if secs < 0.0 {
137 return Err(TimecodeError::InvalidConfiguration);
138 }
139 let fps = rate.as_float();
140 let total_frames = (secs * fps).round() as u64;
141 Timecode::from_frames(total_frames, rate)
142}
143
144#[allow(clippy::cast_precision_loss)]
146pub fn timecode_to_seconds(tc: &Timecode) -> f64 {
147 let fps = if tc.frame_rate.drop_frame {
148 29.97
149 } else {
150 tc.frame_rate.fps as f64
151 };
152 tc.to_frames() as f64 / fps
153}
154
155pub fn parse_smpte_string(s: &str, rate: FrameRate) -> Result<Timecode, TimecodeError> {
165 let s = s.trim();
166 if s.len() < 11 {
167 return Err(TimecodeError::InvalidConfiguration);
168 }
169 let parts: Vec<&str> = s.split([':', ';']).collect();
170 if parts.len() != 4 {
171 return Err(TimecodeError::InvalidConfiguration);
172 }
173 let hours: u8 = parts[0].parse().map_err(|_| TimecodeError::InvalidHours)?;
174 let minutes: u8 = parts[1]
175 .parse()
176 .map_err(|_| TimecodeError::InvalidMinutes)?;
177 let seconds: u8 = parts[2]
178 .parse()
179 .map_err(|_| TimecodeError::InvalidSeconds)?;
180 let frames: u8 = parts[3].parse().map_err(|_| TimecodeError::InvalidFrames)?;
181
182 Timecode::new(hours, minutes, seconds, frames, rate)
183}
184
185pub fn frames_to_smpte_string(frames: u64, rate: FrameRate) -> Result<String, TimecodeError> {
191 let tc = Timecode::from_frames(frames, rate)?;
192 Ok(tc.to_string())
193}
194
195#[allow(clippy::cast_precision_loss)]
197pub fn timecode_to_millis(tc: &Timecode) -> u64 {
198 let secs = timecode_to_seconds(tc);
199 (secs * 1000.0).round() as u64
200}
201
202pub fn millis_to_timecode(ms: u64, rate: FrameRate) -> Result<Timecode, TimecodeError> {
208 #[allow(clippy::cast_precision_loss)]
209 let secs = ms as f64 / 1000.0;
210 seconds_to_timecode(secs, rate)
211}
212
213#[allow(clippy::cast_precision_loss)]
216pub fn timecode_to_audio_samples(tc: &Timecode, sample_rate: u32) -> u64 {
217 let secs = timecode_to_seconds(tc);
218 (secs * sample_rate as f64).round() as u64
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn test_seconds_to_timecode_25fps() {
227 let tc = seconds_to_timecode(3661.0, FrameRate::Fps25).unwrap();
228 assert_eq!(tc.hours, 1);
229 assert_eq!(tc.minutes, 1);
230 assert_eq!(tc.seconds, 1);
231 assert_eq!(tc.frames, 0);
232 }
233
234 #[test]
235 fn test_timecode_to_seconds() {
236 let tc = Timecode::new(1, 0, 0, 0, FrameRate::Fps25).unwrap();
237 let secs = timecode_to_seconds(&tc);
238 assert!((secs - 3600.0).abs() < 0.01);
239 }
240
241 #[test]
242 fn test_parse_smpte_ndf() {
243 let tc = parse_smpte_string("01:02:03:04", FrameRate::Fps25).unwrap();
244 assert_eq!(tc.hours, 1);
245 assert_eq!(tc.minutes, 2);
246 assert_eq!(tc.seconds, 3);
247 assert_eq!(tc.frames, 4);
248 }
249
250 #[test]
251 fn test_parse_smpte_invalid() {
252 assert!(parse_smpte_string("bad", FrameRate::Fps25).is_err());
253 }
254
255 #[test]
256 fn test_frames_to_smpte_string() {
257 let s = frames_to_smpte_string(25, FrameRate::Fps25).unwrap();
258 assert_eq!(s, "00:00:01:00");
259 }
260
261 #[test]
262 fn test_millis_roundtrip() {
263 let tc = Timecode::new(0, 1, 30, 0, FrameRate::Fps25).unwrap();
264 let ms = timecode_to_millis(&tc);
265 let tc2 = millis_to_timecode(ms, FrameRate::Fps25).unwrap();
266 assert_eq!(tc.hours, tc2.hours);
267 assert_eq!(tc.minutes, tc2.minutes);
268 assert_eq!(tc.seconds, tc2.seconds);
269 }
270
271 #[test]
272 fn test_convert_preserve_time_same_rate() {
273 let tc = Timecode::new(1, 0, 0, 0, FrameRate::Fps25).unwrap();
274 let result =
275 convert_frame_rate(&tc, FrameRate::Fps25, ConvertStrategy::PreserveTime).unwrap();
276 assert!(result.rounding_error_secs.abs() < 0.001);
277 }
278
279 #[test]
280 fn test_convert_preserve_time_25_to_30() {
281 let tc = Timecode::new(0, 0, 1, 0, FrameRate::Fps25).unwrap();
282 let result =
283 convert_frame_rate(&tc, FrameRate::Fps30, ConvertStrategy::PreserveTime).unwrap();
284 assert_eq!(result.timecode.seconds, 1);
285 assert_eq!(result.timecode.frames, 0);
286 }
287
288 #[test]
289 fn test_convert_preserve_display() {
290 let tc = Timecode::new(1, 2, 3, 10, FrameRate::Fps30).unwrap();
291 let result =
292 convert_frame_rate(&tc, FrameRate::Fps25, ConvertStrategy::PreserveDisplay).unwrap();
293 assert_eq!(result.timecode.hours, 1);
294 assert_eq!(result.timecode.minutes, 2);
295 assert_eq!(result.timecode.seconds, 3);
296 assert_eq!(result.timecode.frames, 10);
297 }
298
299 #[test]
300 fn test_convert_preserve_frame() {
301 let tc = Timecode::new(0, 0, 0, 10, FrameRate::Fps25).unwrap();
302 let result =
303 convert_frame_rate(&tc, FrameRate::Fps30, ConvertStrategy::PreserveFrame).unwrap();
304 assert_eq!(result.timecode.frames, 10);
305 }
306
307 #[test]
308 fn test_audio_samples() {
309 let tc = Timecode::new(0, 0, 1, 0, FrameRate::Fps25).unwrap();
310 let samples = timecode_to_audio_samples(&tc, 48000);
311 assert_eq!(samples, 48000);
312 }
313
314 #[test]
315 fn test_negative_seconds_error() {
316 assert!(seconds_to_timecode(-1.0, FrameRate::Fps25).is_err());
317 }
318}