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
221pub fn ndf_to_df(tc: &Timecode) -> Result<Timecode, TimecodeError> {
238 if tc.frame_rate.drop_frame {
239 return Err(TimecodeError::InvalidConfiguration); }
241
242 let df_rate = match tc.frame_rate.fps {
243 30 => FrameRate::Fps2997DF,
244 60 => FrameRate::Fps5994DF,
245 24 => FrameRate::Fps23976DF,
246 48 => FrameRate::Fps47952DF,
247 _ => return Err(TimecodeError::InvalidConfiguration),
248 };
249
250 Timecode::from_frames(tc.to_frames(), df_rate)
255}
256
257pub fn df_to_ndf(tc: &Timecode) -> Result<Timecode, TimecodeError> {
268 if !tc.frame_rate.drop_frame {
269 return Err(TimecodeError::InvalidConfiguration); }
271
272 let ndf_rate = match tc.frame_rate.fps {
273 30 => FrameRate::Fps2997NDF,
274 60 => FrameRate::Fps5994,
275 24 => FrameRate::Fps23976,
276 48 => FrameRate::Fps47952,
277 _ => return Err(TimecodeError::InvalidConfiguration),
278 };
279
280 Timecode::from_frames(tc.to_frames(), ndf_rate)
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn test_seconds_to_timecode_25fps() {
289 let tc = seconds_to_timecode(3661.0, FrameRate::Fps25)
290 .expect("seconds to timecode should succeed");
291 assert_eq!(tc.hours, 1);
292 assert_eq!(tc.minutes, 1);
293 assert_eq!(tc.seconds, 1);
294 assert_eq!(tc.frames, 0);
295 }
296
297 #[test]
298 fn test_timecode_to_seconds() {
299 let tc = Timecode::new(1, 0, 0, 0, FrameRate::Fps25).expect("valid timecode");
300 let secs = timecode_to_seconds(&tc);
301 assert!((secs - 3600.0).abs() < 0.01);
302 }
303
304 #[test]
305 fn test_parse_smpte_ndf() {
306 let tc = parse_smpte_string("01:02:03:04", FrameRate::Fps25).expect("should succeed");
307 assert_eq!(tc.hours, 1);
308 assert_eq!(tc.minutes, 2);
309 assert_eq!(tc.seconds, 3);
310 assert_eq!(tc.frames, 4);
311 }
312
313 #[test]
314 fn test_parse_smpte_invalid() {
315 assert!(parse_smpte_string("bad", FrameRate::Fps25).is_err());
316 }
317
318 #[test]
319 fn test_frames_to_smpte_string() {
320 let s =
321 frames_to_smpte_string(25, FrameRate::Fps25).expect("frames to SMPTE should succeed");
322 assert_eq!(s, "00:00:01:00");
323 }
324
325 #[test]
326 fn test_millis_roundtrip() {
327 let tc = Timecode::new(0, 1, 30, 0, FrameRate::Fps25).expect("valid timecode");
328 let ms = timecode_to_millis(&tc);
329 let tc2 =
330 millis_to_timecode(ms, FrameRate::Fps25).expect("millis to timecode should succeed");
331 assert_eq!(tc.hours, tc2.hours);
332 assert_eq!(tc.minutes, tc2.minutes);
333 assert_eq!(tc.seconds, tc2.seconds);
334 }
335
336 #[test]
337 fn test_convert_preserve_time_same_rate() {
338 let tc = Timecode::new(1, 0, 0, 0, FrameRate::Fps25).expect("valid timecode");
339 let result = convert_frame_rate(&tc, FrameRate::Fps25, ConvertStrategy::PreserveTime)
340 .expect("conversion should succeed");
341 assert!(result.rounding_error_secs.abs() < 0.001);
342 }
343
344 #[test]
345 fn test_convert_preserve_time_25_to_30() {
346 let tc = Timecode::new(0, 0, 1, 0, FrameRate::Fps25).expect("valid timecode");
347 let result = convert_frame_rate(&tc, FrameRate::Fps30, ConvertStrategy::PreserveTime)
348 .expect("conversion should succeed");
349 assert_eq!(result.timecode.seconds, 1);
350 assert_eq!(result.timecode.frames, 0);
351 }
352
353 #[test]
354 fn test_convert_preserve_display() {
355 let tc = Timecode::new(1, 2, 3, 10, FrameRate::Fps30).expect("valid timecode");
356 let result = convert_frame_rate(&tc, FrameRate::Fps25, ConvertStrategy::PreserveDisplay)
357 .expect("conversion should succeed");
358 assert_eq!(result.timecode.hours, 1);
359 assert_eq!(result.timecode.minutes, 2);
360 assert_eq!(result.timecode.seconds, 3);
361 assert_eq!(result.timecode.frames, 10);
362 }
363
364 #[test]
365 fn test_convert_preserve_frame() {
366 let tc = Timecode::new(0, 0, 0, 10, FrameRate::Fps25).expect("valid timecode");
367 let result = convert_frame_rate(&tc, FrameRate::Fps30, ConvertStrategy::PreserveFrame)
368 .expect("conversion should succeed");
369 assert_eq!(result.timecode.frames, 10);
370 }
371
372 #[test]
373 fn test_audio_samples() {
374 let tc = Timecode::new(0, 0, 1, 0, FrameRate::Fps25).expect("valid timecode");
375 let samples = timecode_to_audio_samples(&tc, 48000);
376 assert_eq!(samples, 48000);
377 }
378
379 #[test]
380 fn test_negative_seconds_error() {
381 assert!(seconds_to_timecode(-1.0, FrameRate::Fps25).is_err());
382 }
383
384 #[test]
385 fn test_ndf_to_df_29_97() {
386 let ndf = Timecode::new(0, 1, 0, 0, FrameRate::Fps2997NDF).expect("valid NDF");
388 let df = ndf_to_df(&ndf).expect("ndf_to_df should succeed");
389 assert!(df.frame_rate.drop_frame);
390 assert_eq!(df.frame_rate.fps, 30);
391 assert_eq!(ndf.to_frames(), df.to_frames());
393 }
394
395 #[test]
396 fn test_df_to_ndf_29_97() {
397 let df = Timecode::new(0, 1, 0, 2, FrameRate::Fps2997DF).expect("valid DF");
398 let ndf = df_to_ndf(&df).expect("df_to_ndf should succeed");
399 assert!(!ndf.frame_rate.drop_frame);
400 assert_eq!(ndf.frame_rate.fps, 30);
401 assert_eq!(df.to_frames(), ndf.to_frames());
402 }
403
404 #[test]
405 fn test_ndf_to_df_already_df_is_error() {
406 let df = Timecode::new(0, 1, 0, 2, FrameRate::Fps2997DF).expect("valid DF");
407 assert!(ndf_to_df(&df).is_err());
408 }
409
410 #[test]
411 fn test_df_to_ndf_already_ndf_is_error() {
412 let ndf = Timecode::new(0, 1, 0, 0, FrameRate::Fps2997NDF).expect("valid NDF");
413 assert!(df_to_ndf(&ndf).is_err());
414 }
415
416 #[test]
417 fn test_ndf_to_df_unsupported_rate_is_error() {
418 let tc = Timecode::new(0, 0, 1, 0, FrameRate::Fps25).expect("valid");
419 assert!(ndf_to_df(&tc).is_err());
420 }
421
422 #[test]
423 fn test_df_ndf_roundtrip_preserves_frame_count() {
424 let ndf = Timecode::new(1, 23, 45, 12, FrameRate::Fps2997NDF).expect("valid NDF");
426 let df = ndf_to_df(&ndf).expect("ndf→df");
427 let back = df_to_ndf(&df).expect("df→ndf");
428 assert_eq!(ndf.to_frames(), back.to_frames());
429 }
430}