talw-timecode 0.1.0

SMPTE timecode arithmetic — parse, format, convert, drop-frame
Documentation
use crate::dropframe::frames_to_components;
use crate::framerate::FrameRate;

pub fn format_timecode(total_frames: i64, rate: FrameRate) -> [u8; 11] {
    let (h, m, s, f) = frames_to_components(total_frames, rate);
    let sep = if rate.is_drop_frame() { b';' } else { b':' };

    [
        b'0' + h / 10,
        b'0' + h % 10,
        b':',
        b'0' + m / 10,
        b'0' + m % 10,
        b':',
        b'0' + s / 10,
        b'0' + s % 10,
        sep,
        b'0' + f / 10,
        b'0' + f % 10,
    ]
}

pub fn format_timecode_str(total_frames: i64, rate: FrameRate, buf: &mut [u8; 11]) {
    *buf = format_timecode(total_frames, rate);
}

#[cfg(test)]
mod tests {
    use super::*;

    fn fmt(frames: i64, rate: FrameRate) -> &'static str {
        let bytes = format_timecode(frames, rate);
        let s: &str = core::str::from_utf8(&bytes).unwrap();
        // Leak for test convenience
        Box::leak(s.to_string().into_boxed_str())
    }

    #[test]
    fn zero() {
        assert_eq!(fmt(0, FrameRate::Fps24), "00:00:00:00");
    }

    #[test]
    fn one_hour() {
        assert_eq!(fmt(86400, FrameRate::Fps24), "01:00:00:00");
    }

    #[test]
    fn half_second_24fps() {
        assert_eq!(fmt(12, FrameRate::Fps24), "00:00:00:12");
    }

    #[test]
    fn drop_frame_uses_semicolon() {
        let bytes = format_timecode(1800, FrameRate::Fps29_97Df);
        assert_eq!(bytes[8], b';');
    }

    #[test]
    fn non_drop_uses_colon() {
        let bytes = format_timecode(0, FrameRate::Fps24);
        assert_eq!(bytes[8], b':');
    }
}