Skip to main content

oximedia_timecode/vitc/
smpte309m.rs

1//! SMPTE 309M HD-VITC ANC packet encoding and decoding.
2//!
3//! SMPTE 309M defines HD-SDI timecode carried in ANC (ancillary data) packets.
4//! The standard specifies DID=0x60, SDID=0x60, and 16 10-bit user data words.
5//!
6//! # Packet Structure
7//! Each word is 10 bits:
8//! - Bits[7:0]: data payload (BCD timecode or binary group byte)
9//! - Bits[9:8]: odd parity — exactly one of these two bits is set so that the
10//!   total number of 1-bits across all 10 positions is odd.
11//!
12//! ## Word layout (words 0–3: timecode; words 4–15: binary groups)
13//! | Word | Content                                        |
14//! |------|------------------------------------------------|
15//! | 0    | frames_units[3:0] | frames_tens[7:4]; bit 8 = drop-frame flag |
16//! | 1    | seconds_units[3:0] | seconds_tens[7:4]                        |
17//! | 2    | minutes_units[3:0] | minutes_tens[7:4]; bit 8 = color-frame (0)|
18//! | 3    | hours_units[3:0] | hours_tens[7:4]                            |
19//! | 4–7  | binary group bytes 0–3 (from `binary_groups`)                  |
20//! | 8–15 | binary group bytes 4–11 (zero-padded)                           |
21
22use crate::{FrameRate, Timecode};
23
24/// SMPTE 309M ancillary timecode packet for HD-SDI.
25///
26/// `DID=0x60`, `SDID=0x60`, 16 10-bit user data words.
27/// Bits[7:0] of each word carry data; bits[9:8] carry odd parity.
28#[derive(Debug, Clone, PartialEq)]
29pub struct Smpte309mPacket {
30    /// DID byte — always 0x60 for SMPTE 309M timecode.
31    pub did: u8,
32    /// SDID byte — always 0x60 for SMPTE 309M timecode.
33    pub sdid: u8,
34    /// 16 10-bit words (bits[7:0] = data; bits[9:8] = parity).
35    pub payload: [u16; 16],
36}
37
38/// Compute odd parity for a byte per SMPTE 291M ANC encoding.
39///
40/// SMPTE 291M uses 10-bit words where bits[7:0] carry data and bits[9:8]
41/// carry parity such that the total number of 1-bits across all 10 positions
42/// is odd.  The parity encoding follows the convention:
43/// - bit 9 (`b9`, "P"): 1 iff `data` has an **even** number of 1-bits
44///   (so that `bits[8:0]` evaluated with b9 in position 8 has odd parity).
45/// - bit 8 (`b8`, "P̄"): always 0 (not used; b9 alone carries the correction).
46///
47/// This ensures `popcount(bits[9:0]) % 2 == 1` (odd) for every byte value,
48/// which the receiver validates via [`parity_ok`].
49fn with_parity(data: u8) -> u16 {
50    // Count 1-bits in the 8-bit data payload.
51    let ones = data.count_ones();
52    // If already odd → no parity bit needed → bits[9:8] = 0b00.
53    // If even        → set bit 9 to make total (data_ones + 1) odd → bits[9:8] = 0b10.
54    if ones % 2 == 0 {
55        // Even data → set bit 9 to flip parity to odd.
56        (1u16 << 9) | u16::from(data)
57    } else {
58        // Odd data → total already odd → no parity bit needed.
59        u16::from(data)
60    }
61}
62
63/// Verify that the parity of a 10-bit word is correct (odd parity over bits[9:0]).
64fn parity_ok(word: u16) -> bool {
65    (word & 0x3ff).count_ones() % 2 == 1
66}
67
68/// Encode a [`Timecode`] into a SMPTE 309M ANC packet.
69///
70/// `binary_groups` — 4 bytes of user binary group data (packed into words 4–7;
71/// words 8–15 are zero-padded binary group bytes).  Pass `[0u8; 4]` if unused.
72///
73/// The returned packet has `DID=0x60`, `SDID=0x60`, and 16 parity-protected
74/// 10-bit words encoding the timecode and binary group data.
75pub fn encode_anc_timecode(tc: &Timecode, binary_groups: [u8; 4]) -> Smpte309mPacket {
76    let frames_units = tc.frames % 10;
77    let frames_tens = tc.frames / 10;
78    let seconds_units = tc.seconds % 10;
79    let seconds_tens = tc.seconds / 10;
80    let minutes_units = tc.minutes % 10;
81    let minutes_tens = tc.minutes / 10;
82    let hours_units = tc.hours % 10;
83    let hours_tens = tc.hours / 10;
84
85    // Word 0: frames BCD, drop-frame flag in bit 8 of the data byte.
86    // Per SMPTE 309M the drop-frame flag is carried in bit 7 of word-0 data
87    // (the MSB of the data byte), but the specification also places it as
88    // part of the parity calculation. We encode it as bit 7 of the 8-bit
89    // data byte so that the parity helper sees it correctly.
90    let word0_data: u8 = frames_units | (frames_tens << 4);
91    // Drop-frame flag: embed in bit 7 of the data byte (after BCD digits
92    // occupy bits[6:0] for frames ≤ 29).  For frames up to 29: tens ≤ 2
93    // (bits 6:4), units ≤ 9 (bits 3:0), so bit 7 is free.
94    let drop_flag: u8 = if tc.frame_rate.drop_frame { 0x80 } else { 0x00 };
95    let word0_data = word0_data | drop_flag;
96
97    let word1_data: u8 = seconds_units | (seconds_tens << 4);
98    let word2_data: u8 = minutes_units | (minutes_tens << 4);
99    // color-frame flag would occupy bit 7 of word 2; always 0 here.
100    let word3_data: u8 = hours_units | (hours_tens << 4);
101
102    // Build all 16 words with parity.
103    let mut payload = [0u16; 16];
104    payload[0] = with_parity(word0_data);
105    payload[1] = with_parity(word1_data);
106    payload[2] = with_parity(word2_data);
107    payload[3] = with_parity(word3_data);
108
109    // Words 4–7: caller-supplied binary group bytes 0–3.
110    for (i, &bg) in binary_groups.iter().enumerate() {
111        payload[4 + i] = with_parity(bg);
112    }
113    // Words 8–15: zero-padded binary group bytes 4–11.
114    for i in 8..16usize {
115        payload[i] = with_parity(0x00);
116    }
117
118    Smpte309mPacket {
119        did: 0x60,
120        sdid: 0x60,
121        payload,
122    }
123}
124
125/// Decode a SMPTE 309M ANC packet into a [`Timecode`] and binary group bytes.
126///
127/// Returns `None` if:
128/// - `DID` or `SDID` do not equal `0x60`, or
129/// - any of the 16 payload words fails its odd-parity check.
130pub fn decode_anc_timecode(pkt: &Smpte309mPacket) -> Option<(Timecode, [u8; 4])> {
131    if pkt.did != 0x60 || pkt.sdid != 0x60 {
132        return None;
133    }
134
135    // Verify parity on all 16 words.
136    for &word in &pkt.payload {
137        if !parity_ok(word) {
138            return None;
139        }
140    }
141
142    // Extract data bytes (bits[7:0]).
143    let w0 = (pkt.payload[0] & 0xff) as u8;
144    let w1 = (pkt.payload[1] & 0xff) as u8;
145    let w2 = (pkt.payload[2] & 0xff) as u8;
146    let w3 = (pkt.payload[3] & 0xff) as u8;
147
148    let drop_frame = (w0 & 0x80) != 0;
149
150    let frames_units = w0 & 0x0f;
151    let frames_tens = (w0 & 0x70) >> 4; // bits[6:4]
152    let frames = frames_tens * 10 + frames_units;
153
154    let seconds_units = w1 & 0x0f;
155    let seconds_tens = (w1 & 0x70) >> 4;
156    let seconds = seconds_tens * 10 + seconds_units;
157
158    let minutes_units = w2 & 0x0f;
159    let minutes_tens = (w2 & 0x70) >> 4;
160    let minutes = minutes_tens * 10 + minutes_units;
161
162    let hours_units = w3 & 0x0f;
163    let hours_tens = (w3 & 0x70) >> 4;
164    let hours = hours_tens * 10 + hours_units;
165
166    // Binary groups: words 4–7.
167    let mut binary_groups = [0u8; 4];
168    for (i, slot) in binary_groups.iter_mut().enumerate() {
169        *slot = (pkt.payload[4 + i] & 0xff) as u8;
170    }
171
172    // Reconstruct FrameRate: use drop-frame flag + 30fps as the default;
173    // HD-SDI SMPTE 309M carries 29.97DF or 30NDF most commonly.
174    let frame_rate = if drop_frame {
175        FrameRate::Fps2997DF
176    } else {
177        FrameRate::Fps30
178    };
179
180    let tc = Timecode::from_raw_fields(hours, minutes, seconds, frames, 30, drop_frame, 0);
181
182    // Validate the reconstructed timecode fields before returning.
183    // Re-encode to check: use the public constructor for validation.
184    let _ = Timecode::new(hours, minutes, seconds, frames, frame_rate).ok()?;
185
186    Some((tc, binary_groups))
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192    use crate::FrameRate;
193
194    fn make_tc(h: u8, m: u8, s: u8, f: u8, df: bool) -> Timecode {
195        let rate = if df {
196            FrameRate::Fps2997DF
197        } else {
198            FrameRate::Fps30
199        };
200        Timecode::new(h, m, s, f, rate).expect("valid timecode")
201    }
202
203    #[test]
204    fn test_smpte309m_round_trip_zero() {
205        let tc = make_tc(0, 0, 0, 0, false);
206        let pkt = encode_anc_timecode(&tc, [0u8; 4]);
207        let (decoded, _bg) = decode_anc_timecode(&pkt).expect("decode must succeed");
208        assert_eq!(decoded.hours, tc.hours);
209        assert_eq!(decoded.minutes, tc.minutes);
210        assert_eq!(decoded.seconds, tc.seconds);
211        assert_eq!(decoded.frames, tc.frames);
212    }
213
214    #[test]
215    fn test_smpte309m_round_trip_max() {
216        // 23:59:59:29 is valid for 30fps NDF.
217        let tc = make_tc(23, 59, 59, 29, false);
218        let pkt = encode_anc_timecode(&tc, [0u8; 4]);
219        let (decoded, _bg) = decode_anc_timecode(&pkt).expect("decode must succeed");
220        assert_eq!(decoded.hours, 23);
221        assert_eq!(decoded.minutes, 59);
222        assert_eq!(decoded.seconds, 59);
223        assert_eq!(decoded.frames, 29);
224    }
225
226    #[test]
227    fn test_smpte309m_parity_correct() {
228        let tc = make_tc(1, 23, 45, 12, false);
229        let pkt = encode_anc_timecode(&tc, [0xAB, 0xCD, 0xEF, 0x01]);
230        // Every word in the payload must have odd parity across bits[9:0].
231        for (i, &word) in pkt.payload.iter().enumerate() {
232            assert!(
233                parity_ok(word),
234                "word {i} failed parity check: 0x{word:03x}"
235            );
236        }
237    }
238
239    #[test]
240    fn test_smpte309m_binary_groups_passthrough() {
241        let tc = make_tc(0, 0, 0, 0, false);
242        let bg_in = [0x12u8, 0x34, 0x56, 0x78];
243        let pkt = encode_anc_timecode(&tc, bg_in);
244        let (_decoded, bg_out) = decode_anc_timecode(&pkt).expect("decode must succeed");
245        assert_eq!(bg_out, bg_in);
246    }
247
248    #[test]
249    fn test_smpte309m_drop_frame_flag() {
250        // 29.97 DF timecode: 00:10:00:02 is a valid DF position.
251        let tc = make_tc(0, 10, 0, 2, true);
252        let pkt = encode_anc_timecode(&tc, [0u8; 4]);
253        // Word 0 data byte should have bit 7 set for drop-frame.
254        let w0_data = (pkt.payload[0] & 0xff) as u8;
255        assert!(
256            (w0_data & 0x80) != 0,
257            "drop-frame flag not set in word 0: 0x{w0_data:02x}"
258        );
259        // And decode should produce a drop-frame timecode.
260        let (decoded, _) = decode_anc_timecode(&pkt).expect("decode must succeed");
261        assert!(decoded.frame_rate.drop_frame);
262    }
263
264    #[test]
265    fn test_smpte309m_did_sdid_mismatch_returns_none() {
266        let tc = make_tc(0, 0, 0, 0, false);
267        let mut pkt = encode_anc_timecode(&tc, [0u8; 4]);
268        pkt.did = 0x61; // wrong DID
269        assert!(decode_anc_timecode(&pkt).is_none());
270    }
271
272    #[test]
273    fn test_smpte309m_parity_error_returns_none() {
274        let tc = make_tc(0, 0, 0, 0, false);
275        let mut pkt = encode_anc_timecode(&tc, [0u8; 4]);
276        // Flip a parity bit in word 0 to corrupt parity.
277        pkt.payload[0] ^= 0x100;
278        assert!(decode_anc_timecode(&pkt).is_none());
279    }
280}