Skip to main content

oximedia_timecode/
midi_timecode.rs

1//! MIDI Timecode (MTC) implementation.
2//!
3//! MIDI Timecode is a standard for synchronizing MIDI devices to a timecode
4//! reference. It transmits timecode as either Full Frame SysEx messages or
5//! as a sequence of 8 quarter-frame messages.
6
7/// MTC frame rate codes and values.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9#[allow(dead_code)]
10pub enum MtcFrameRate {
11    /// 24 fps (film)
12    Fps24,
13    /// 25 fps (PAL)
14    Fps25,
15    /// 29.97 fps drop frame (NTSC)
16    Fps2997,
17    /// 30 fps (non-drop frame)
18    Fps30,
19}
20
21impl MtcFrameRate {
22    /// Get the MTC frame rate code (0-3) as encoded in the SysEx message.
23    ///
24    /// - 0 = 24 fps
25    /// - 1 = 25 fps
26    /// - 2 = 29.97 fps (drop frame)
27    /// - 3 = 30 fps
28    #[must_use]
29    pub fn code(&self) -> u8 {
30        match self {
31            MtcFrameRate::Fps24 => 0,
32            MtcFrameRate::Fps25 => 1,
33            MtcFrameRate::Fps2997 => 2,
34            MtcFrameRate::Fps30 => 3,
35        }
36    }
37
38    /// Get the frame rate as a floating point value.
39    #[must_use]
40    pub fn frames_per_sec(&self) -> f32 {
41        match self {
42            MtcFrameRate::Fps24 => 24.0,
43            MtcFrameRate::Fps25 => 25.0,
44            MtcFrameRate::Fps2997 => 29.97,
45            MtcFrameRate::Fps30 => 30.0,
46        }
47    }
48
49    /// Get the integer frame count per second (ceiling).
50    #[must_use]
51    pub fn frames_per_sec_int(&self) -> u8 {
52        match self {
53            MtcFrameRate::Fps24 => 24,
54            MtcFrameRate::Fps25 => 25,
55            MtcFrameRate::Fps2997 => 30, // counted as 30 in MTC
56            MtcFrameRate::Fps30 => 30,
57        }
58    }
59
60    /// Create from the 2-bit MTC rate code.
61    #[must_use]
62    pub fn from_code(code: u8) -> Option<Self> {
63        match code & 0x03 {
64            0 => Some(MtcFrameRate::Fps24),
65            1 => Some(MtcFrameRate::Fps25),
66            2 => Some(MtcFrameRate::Fps2997),
67            3 => Some(MtcFrameRate::Fps30),
68            _ => None,
69        }
70    }
71}
72
73/// MIDI Timecode value.
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75#[allow(dead_code)]
76pub struct MtcTimecode {
77    /// Hours (0-23)
78    pub hours: u8,
79    /// Minutes (0-59)
80    pub minutes: u8,
81    /// Seconds (0-59)
82    pub seconds: u8,
83    /// Frames (0 to frames_per_sec - 1)
84    pub frames: u8,
85    /// Frame rate
86    pub frame_rate: MtcFrameRate,
87}
88
89impl MtcTimecode {
90    /// Create a new MTC timecode value.
91    #[must_use]
92    pub fn new(hours: u8, minutes: u8, seconds: u8, frames: u8, frame_rate: MtcFrameRate) -> Self {
93        Self {
94            hours: hours.min(23),
95            minutes: minutes.min(59),
96            seconds: seconds.min(59),
97            frames: frames.min(frame_rate.frames_per_sec_int() - 1),
98            frame_rate,
99        }
100    }
101
102    /// Convert to total frame count since 00:00:00:00.
103    #[must_use]
104    pub fn to_frame_count(&self) -> u64 {
105        let fps = u64::from(self.frame_rate.frames_per_sec_int());
106        let hours = u64::from(self.hours);
107        let minutes = u64::from(self.minutes);
108        let seconds = u64::from(self.seconds);
109        let frames = u64::from(self.frames);
110
111        hours * 3600 * fps + minutes * 60 * fps + seconds * fps + frames
112    }
113
114    /// Create an MTC timecode from a frame count.
115    #[must_use]
116    pub fn from_frame_count(frame_count: u64, rate: MtcFrameRate) -> Self {
117        let fps = u64::from(rate.frames_per_sec_int());
118        let total_seconds = frame_count / fps;
119        let frames = (frame_count % fps) as u8;
120        let seconds = (total_seconds % 60) as u8;
121        let total_minutes = total_seconds / 60;
122        let minutes = (total_minutes % 60) as u8;
123        let hours = ((total_minutes / 60) % 24) as u8;
124
125        Self::new(hours, minutes, seconds, frames, rate)
126    }
127}
128
129/// MTC Full Frame message encoder/decoder.
130///
131/// Full Frame SysEx format: `[0xF0, 0x7F, 0x7F, 0x01, 0x01, hh, mm, ss, ff, 0xF7]`
132/// where `hh` encodes both the hours (5 bits) and rate code (2 bits).
133#[allow(dead_code)]
134pub struct MtcFullFrame;
135
136impl MtcFullFrame {
137    /// Encode an MTC timecode as a Full Frame SysEx message.
138    ///
139    /// The `hh` byte encodes: `0rrhhhhh` where `rr` is the rate code and
140    /// `hhhhh` is the hours value.
141    #[must_use]
142    pub fn encode(tc: &MtcTimecode) -> Vec<u8> {
143        let rate_code = tc.frame_rate.code();
144        let hh = (rate_code << 5) | (tc.hours & 0x1F);
145
146        vec![
147            0xF0, // SysEx start
148            0x7F, // Real-time universal SysEx
149            0x7F, // All devices
150            0x01, // MTC
151            0x01, // Full Frame
152            hh, tc.minutes, tc.seconds, tc.frames, 0xF7, // SysEx end
153        ]
154    }
155
156    /// Decode a Full Frame SysEx message into an MTC timecode.
157    ///
158    /// Returns `None` if the data is not a valid MTC Full Frame message.
159    #[must_use]
160    pub fn decode(data: &[u8]) -> Option<MtcTimecode> {
161        if data.len() < 10 {
162            return None;
163        }
164        if data[0] != 0xF0
165            || data[1] != 0x7F
166            || data[2] != 0x7F
167            || data[3] != 0x01
168            || data[4] != 0x01
169            || data[9] != 0xF7
170        {
171            return None;
172        }
173
174        let hh = data[5];
175        let rate_code = (hh >> 5) & 0x03;
176        let hours = hh & 0x1F;
177        let minutes = data[6];
178        let seconds = data[7];
179        let frames = data[8];
180
181        let frame_rate = MtcFrameRate::from_code(rate_code)?;
182
183        Some(MtcTimecode::new(
184            hours, minutes, seconds, frames, frame_rate,
185        ))
186    }
187}
188
189/// MTC quarter-frame message utilities.
190///
191/// MTC is transmitted as 8 quarter-frame messages per timecode frame.
192/// Each message carries 4 bits of timecode data.
193#[allow(dead_code)]
194pub struct MtcQuarterFrame;
195
196impl MtcQuarterFrame {
197    /// Encode one of the 8 quarter-frame pieces.
198    ///
199    /// Each quarter-frame message is a 2-byte sequence: `[0xF1, data]`
200    /// where `data` is `ppppdddd` (piece number in high nibble, data in low).
201    ///
202    /// The 8 pieces (0-7) carry:
203    /// - 0: frames low nibble
204    /// - 1: frames high nibble (2 bits)
205    /// - 2: seconds low nibble
206    /// - 3: seconds high nibble (3 bits)
207    /// - 4: minutes low nibble
208    /// - 5: minutes high nibble (3 bits)
209    /// - 6: hours low nibble
210    /// - 7: hours high nibble + rate code (3 bits)
211    ///
212    /// Returns the data byte (the second byte of the 0xF1 message pair).
213    #[must_use]
214    pub fn encode_quarter(tc: &MtcTimecode, piece: u8) -> u8 {
215        let piece = piece & 0x07;
216        let data: u8 = match piece {
217            0 => tc.frames & 0x0F,
218            1 => (tc.frames >> 4) & 0x01,
219            2 => tc.seconds & 0x0F,
220            3 => (tc.seconds >> 4) & 0x03,
221            4 => tc.minutes & 0x0F,
222            5 => (tc.minutes >> 4) & 0x03,
223            6 => tc.hours & 0x0F,
224            7 => ((tc.frame_rate.code() & 0x03) << 1) | ((tc.hours >> 4) & 0x01),
225            _ => 0,
226        };
227        (piece << 4) | (data & 0x0F)
228    }
229}
230
231/// MTC receiver that assembles quarter-frame messages into timecodes.
232#[derive(Debug)]
233#[allow(dead_code)]
234pub struct MtcReceiver {
235    /// Accumulated quarter-frame data (8 nibbles)
236    nibbles: [u8; 8],
237    /// Number of quarter frames received in current sequence
238    count: usize,
239    /// Whether we have received a complete set of 8 quarter frames
240    complete: bool,
241}
242
243impl MtcReceiver {
244    /// Create a new MTC receiver.
245    #[must_use]
246    pub fn new() -> Self {
247        Self {
248            nibbles: [0u8; 8],
249            count: 0,
250            complete: false,
251        }
252    }
253
254    /// Process a single MTC quarter-frame data byte.
255    ///
256    /// Returns `Some(MtcTimecode)` when all 8 quarter frames have been received
257    /// and assembled into a complete timecode.
258    pub fn process_message(&mut self, msg: u8) -> Option<MtcTimecode> {
259        let piece = (msg >> 4) & 0x07;
260        let data = msg & 0x0F;
261
262        self.nibbles[piece as usize] = data;
263        self.count += 1;
264
265        // We need all 8 pieces to reconstruct a timecode
266        if self.count >= 8 {
267            self.complete = true;
268            self.count = 0;
269            return self.assemble();
270        }
271
272        None
273    }
274
275    /// Assemble accumulated nibbles into a timecode.
276    fn assemble(&self) -> Option<MtcTimecode> {
277        let frames = self.nibbles[0] | (self.nibbles[1] << 4);
278        let seconds = self.nibbles[2] | (self.nibbles[3] << 4);
279        let minutes = self.nibbles[4] | (self.nibbles[5] << 4);
280        let hours = self.nibbles[6] | ((self.nibbles[7] & 0x01) << 4);
281        let rate_code = (self.nibbles[7] >> 1) & 0x03;
282
283        let frame_rate = MtcFrameRate::from_code(rate_code)?;
284        Some(MtcTimecode::new(
285            hours, minutes, seconds, frames, frame_rate,
286        ))
287    }
288
289    /// Reset the receiver state.
290    pub fn reset(&mut self) {
291        self.nibbles = [0u8; 8];
292        self.count = 0;
293        self.complete = false;
294    }
295
296    /// Check if a complete timecode has been received.
297    #[must_use]
298    pub fn is_complete(&self) -> bool {
299        self.complete
300    }
301}
302
303impl Default for MtcReceiver {
304    fn default() -> Self {
305        Self::new()
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312
313    #[test]
314    fn test_mtc_frame_rate_code() {
315        assert_eq!(MtcFrameRate::Fps24.code(), 0);
316        assert_eq!(MtcFrameRate::Fps25.code(), 1);
317        assert_eq!(MtcFrameRate::Fps2997.code(), 2);
318        assert_eq!(MtcFrameRate::Fps30.code(), 3);
319    }
320
321    #[test]
322    fn test_mtc_frame_rate_from_code() {
323        assert_eq!(MtcFrameRate::from_code(0), Some(MtcFrameRate::Fps24));
324        assert_eq!(MtcFrameRate::from_code(1), Some(MtcFrameRate::Fps25));
325        assert_eq!(MtcFrameRate::from_code(2), Some(MtcFrameRate::Fps2997));
326        assert_eq!(MtcFrameRate::from_code(3), Some(MtcFrameRate::Fps30));
327    }
328
329    #[test]
330    fn test_mtc_frame_rate_fps() {
331        assert!((MtcFrameRate::Fps24.frames_per_sec() - 24.0).abs() < f32::EPSILON);
332        assert!((MtcFrameRate::Fps25.frames_per_sec() - 25.0).abs() < f32::EPSILON);
333        assert!((MtcFrameRate::Fps2997.frames_per_sec() - 29.97).abs() < 0.001);
334        assert!((MtcFrameRate::Fps30.frames_per_sec() - 30.0).abs() < f32::EPSILON);
335    }
336
337    #[test]
338    fn test_mtc_timecode_new() {
339        let tc = MtcTimecode::new(1, 2, 3, 4, MtcFrameRate::Fps25);
340        assert_eq!(tc.hours, 1);
341        assert_eq!(tc.minutes, 2);
342        assert_eq!(tc.seconds, 3);
343        assert_eq!(tc.frames, 4);
344    }
345
346    #[test]
347    fn test_mtc_timecode_to_frame_count() {
348        let tc = MtcTimecode::new(0, 0, 1, 0, MtcFrameRate::Fps25);
349        assert_eq!(tc.to_frame_count(), 25);
350
351        let tc2 = MtcTimecode::new(1, 0, 0, 0, MtcFrameRate::Fps30);
352        assert_eq!(tc2.to_frame_count(), 3600 * 30);
353    }
354
355    #[test]
356    fn test_mtc_timecode_from_frame_count() {
357        let tc = MtcTimecode::from_frame_count(3600 * 25, MtcFrameRate::Fps25);
358        assert_eq!(tc.hours, 1);
359        assert_eq!(tc.minutes, 0);
360        assert_eq!(tc.seconds, 0);
361        assert_eq!(tc.frames, 0);
362    }
363
364    #[test]
365    fn test_mtc_timecode_roundtrip() {
366        let original = MtcTimecode::new(1, 30, 45, 12, MtcFrameRate::Fps25);
367        let frames = original.to_frame_count();
368        let recovered = MtcTimecode::from_frame_count(frames, MtcFrameRate::Fps25);
369        assert_eq!(original, recovered);
370    }
371
372    #[test]
373    fn test_mtc_full_frame_encode() {
374        let tc = MtcTimecode::new(1, 2, 3, 4, MtcFrameRate::Fps25);
375        let data = MtcFullFrame::encode(&tc);
376        assert_eq!(data.len(), 10);
377        assert_eq!(data[0], 0xF0);
378        assert_eq!(data[1], 0x7F);
379        assert_eq!(data[2], 0x7F);
380        assert_eq!(data[3], 0x01);
381        assert_eq!(data[4], 0x01);
382        // hh: rate_code=1 (25fps), hours=1 => (1<<5) | 1 = 33
383        assert_eq!(data[5], (1 << 5) | 1);
384        assert_eq!(data[6], 2);
385        assert_eq!(data[7], 3);
386        assert_eq!(data[8], 4);
387        assert_eq!(data[9], 0xF7);
388    }
389
390    #[test]
391    fn test_mtc_full_frame_decode() {
392        let tc = MtcTimecode::new(2, 10, 30, 15, MtcFrameRate::Fps30);
393        let encoded = MtcFullFrame::encode(&tc);
394        let decoded = MtcFullFrame::decode(&encoded).expect("should succeed");
395        assert_eq!(decoded.hours, 2);
396        assert_eq!(decoded.minutes, 10);
397        assert_eq!(decoded.seconds, 30);
398        assert_eq!(decoded.frames, 15);
399        assert_eq!(decoded.frame_rate, MtcFrameRate::Fps30);
400    }
401
402    #[test]
403    fn test_mtc_full_frame_decode_invalid() {
404        assert!(MtcFullFrame::decode(&[]).is_none());
405        assert!(MtcFullFrame::decode(&[0xF0, 0x00, 0x7F, 0x01, 0x01, 0, 0, 0, 0, 0xF7]).is_none());
406    }
407
408    #[test]
409    fn test_mtc_receiver_assemble() {
410        let tc = MtcTimecode::new(1, 2, 3, 4, MtcFrameRate::Fps25);
411        let mut receiver = MtcReceiver::new();
412
413        // Send all 8 quarter frames
414        let mut result = None;
415        for piece in 0..8u8 {
416            let byte = MtcQuarterFrame::encode_quarter(&tc, piece);
417            result = receiver.process_message(byte);
418        }
419
420        let decoded = result.expect("result should be ok");
421        assert_eq!(decoded.hours, tc.hours);
422        assert_eq!(decoded.minutes, tc.minutes);
423        assert_eq!(decoded.seconds, tc.seconds);
424        assert_eq!(decoded.frames, tc.frames);
425        assert_eq!(decoded.frame_rate, tc.frame_rate);
426    }
427
428    #[test]
429    fn test_mtc_receiver_reset() {
430        let mut receiver = MtcReceiver::new();
431        receiver.process_message(0x00);
432        receiver.reset();
433        assert!(!receiver.is_complete());
434    }
435
436    #[test]
437    fn test_mtc_receiver_default() {
438        let receiver = MtcReceiver::default();
439        assert!(!receiver.is_complete());
440    }
441}