Skip to main content

oximedia_timecode/
embedded_tc.rs

1//! Embedded timecode (ATC - Ancillary Timecode) in SDI ancillary data packets.
2//!
3//! This module implements reading and writing of ATC (Ancillary Timecode)
4//! as defined in SMPTE ST 12-3 and SMPTE 309M for embedding timecode in
5//! HD-SDI ancillary data (HANC/VANC).
6//!
7//! # ATC Packet Format
8//!
9//! ATC packets are embedded in the Horizontal Ancillary (HANC) or Vertical
10//! Ancillary (VANC) data space of SDI streams. The packet consists of:
11//! - DID (Data Identifier): 0x60 for ATC
12//! - SDID (Secondary Data ID): 0x60 for LTC-ATC or 0x61 for VITC-ATC
13//! - DC (Data Count): number of data words
14//! - User data: encoded timecode
15//! - Checksum
16
17#![allow(dead_code)]
18#![allow(clippy::cast_possible_truncation)]
19
20use crate::{FrameRate, Timecode, TimecodeError};
21
22/// DID for ATC (Ancillary Timecode) per SMPTE ST 12-3.
23pub const ATC_DID: u16 = 0x60;
24
25/// SDID for LTC-based ATC.
26pub const ATC_SDID_LTC: u16 = 0x60;
27
28/// SDID for VITC-based ATC.
29pub const ATC_SDID_VITC: u16 = 0x61;
30
31/// ATC timecode type embedded in SDI ancillary data.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum AtcType {
34    /// LTC-based ancillary timecode.
35    Ltc,
36    /// VITC-based ancillary timecode.
37    Vitc,
38}
39
40/// Ancillary Timecode (ATC) packet for SDI embedding.
41///
42/// This struct holds a timecode value packaged for insertion into
43/// the ancillary data space of an SDI signal.
44#[derive(Debug, Clone)]
45pub struct AtcPacket {
46    /// Whether this is LTC-ATC or VITC-ATC.
47    pub atc_type: AtcType,
48    /// The timecode value.
49    pub timecode: Timecode,
50    /// User bits (32 bits of user-defined data).
51    pub user_bits: u32,
52    /// Whether continuity counter is valid.
53    pub continuity_count_valid: bool,
54    /// Continuity counter (0-127).
55    pub continuity_count: u8,
56    /// Whether this packet contains valid data.
57    pub valid: bool,
58}
59
60impl AtcPacket {
61    /// Create a new ATC packet with the given timecode.
62    #[must_use]
63    pub fn new(atc_type: AtcType, timecode: Timecode) -> Self {
64        Self {
65            atc_type,
66            timecode,
67            user_bits: 0,
68            continuity_count_valid: false,
69            continuity_count: 0,
70            valid: true,
71        }
72    }
73
74    /// Create an LTC-ATC packet.
75    #[must_use]
76    pub fn ltc(timecode: Timecode) -> Self {
77        Self::new(AtcType::Ltc, timecode)
78    }
79
80    /// Create a VITC-ATC packet.
81    #[must_use]
82    pub fn vitc(timecode: Timecode) -> Self {
83        Self::new(AtcType::Vitc, timecode)
84    }
85
86    /// Set user bits.
87    #[must_use]
88    pub fn with_user_bits(mut self, user_bits: u32) -> Self {
89        self.user_bits = user_bits;
90        self
91    }
92
93    /// Set continuity counter.
94    #[must_use]
95    pub fn with_continuity(mut self, count: u8) -> Self {
96        self.continuity_count = count & 0x7F;
97        self.continuity_count_valid = true;
98        self
99    }
100
101    /// Serialize the ATC packet to 10-bit SDI words (as u16).
102    ///
103    /// Returns the ancillary data packet words including ADF, DID, SDID, DC,
104    /// user data, and checksum per SMPTE ST 291.
105    #[must_use]
106    pub fn to_sdi_words(&self) -> Vec<u16> {
107        let mut words = Vec::with_capacity(16);
108
109        // Ancillary Data Flag (ADF): 0x000, 0x3FF, 0x3FF
110        words.push(0x000);
111        words.push(0x3FF);
112        words.push(0x3FF);
113
114        // DID (with 9-bit parity)
115        let did = add_parity_9bit(ATC_DID as u8);
116        words.push(did);
117
118        // SDID
119        let sdid = match self.atc_type {
120            AtcType::Ltc => add_parity_9bit(ATC_SDID_LTC as u8),
121            AtcType::Vitc => add_parity_9bit(ATC_SDID_VITC as u8),
122        };
123        words.push(sdid);
124
125        // DC (Data Count): 9 data words
126        let dc = add_parity_9bit(9);
127        words.push(dc);
128
129        // Encode timecode into 8 bytes (SMPTE 12M format)
130        let tc_bytes = encode_timecode_bytes(&self.timecode, self.user_bits);
131        for &byte in &tc_bytes {
132            words.push(add_parity_9bit(byte));
133        }
134
135        // Continuity count word
136        let cont = if self.continuity_count_valid {
137            0x80 | (self.continuity_count & 0x7F)
138        } else {
139            0x00
140        };
141        words.push(add_parity_9bit(cont));
142
143        // Checksum
144        let checksum = compute_checksum(&words[3..]);
145        words.push(checksum);
146
147        words
148    }
149
150    /// Parse an ATC packet from 10-bit SDI words.
151    ///
152    /// # Errors
153    ///
154    /// Returns error if the packet is malformed, checksum fails,
155    /// or the DID/SDID is not recognized as ATC.
156    pub fn from_sdi_words(words: &[u16]) -> Result<Self, TimecodeError> {
157        // Minimum size: ADF(3) + DID + SDID + DC + 9 data + checksum = 16
158        if words.len() < 16 {
159            return Err(TimecodeError::InvalidConfiguration);
160        }
161
162        // Validate ADF
163        if words[0] != 0x000 || words[1] != 0x3FF || words[2] != 0x3FF {
164            return Err(TimecodeError::InvalidConfiguration);
165        }
166
167        let did = (words[3] & 0xFF) as u8;
168        if did != ATC_DID as u8 {
169            return Err(TimecodeError::InvalidConfiguration);
170        }
171
172        let sdid = (words[4] & 0xFF) as u8;
173        let atc_type = match sdid {
174            s if s == ATC_SDID_LTC as u8 => AtcType::Ltc,
175            s if s == ATC_SDID_VITC as u8 => AtcType::Vitc,
176            _ => return Err(TimecodeError::InvalidConfiguration),
177        };
178
179        let dc = (words[5] & 0xFF) as usize;
180        if dc < 9 || words.len() < 6 + dc + 1 {
181            return Err(TimecodeError::InvalidConfiguration);
182        }
183
184        // Extract 8 timecode bytes + 1 continuity byte
185        let mut tc_bytes = [0u8; 8];
186        for (i, b) in tc_bytes.iter_mut().enumerate() {
187            *b = (words[6 + i] & 0xFF) as u8;
188        }
189        let cont_byte = (words[14] & 0xFF) as u8;
190
191        let (timecode, user_bits) = decode_timecode_bytes(&tc_bytes)?;
192
193        let continuity_count_valid = (cont_byte & 0x80) != 0;
194        let continuity_count = cont_byte & 0x7F;
195
196        Ok(Self {
197            atc_type,
198            timecode,
199            user_bits,
200            continuity_count_valid,
201            continuity_count,
202            valid: true,
203        })
204    }
205}
206
207/// Encode a timecode into 8 SMPTE 12M bytes plus user bits.
208fn encode_timecode_bytes(tc: &Timecode, user_bits: u32) -> [u8; 8] {
209    let drop_flag: u8 = if tc.frame_rate.drop_frame { 0x40 } else { 0x00 };
210    let frame_units = tc.frames % 10;
211    let frame_tens = tc.frames / 10;
212
213    let sec_units = tc.seconds % 10;
214    let sec_tens = tc.seconds / 10;
215
216    let min_units = tc.minutes % 10;
217    let min_tens = tc.minutes / 10;
218
219    let hour_units = tc.hours % 10;
220    let hour_tens = tc.hours / 10;
221
222    // User bits nibbles
223    let ub = [
224        ((user_bits >> 28) & 0xF) as u8,
225        ((user_bits >> 24) & 0xF) as u8,
226        ((user_bits >> 20) & 0xF) as u8,
227        ((user_bits >> 16) & 0xF) as u8,
228        ((user_bits >> 12) & 0xF) as u8,
229        ((user_bits >> 8) & 0xF) as u8,
230        ((user_bits >> 4) & 0xF) as u8,
231        (user_bits & 0xF) as u8,
232    ];
233
234    [
235        (frame_units & 0x0F) | (ub[0] << 4),
236        (frame_tens & 0x03) | drop_flag | (ub[1] << 4),
237        (sec_units & 0x0F) | (ub[2] << 4),
238        (sec_tens & 0x07) | (ub[3] << 4),
239        (min_units & 0x0F) | (ub[4] << 4),
240        (min_tens & 0x07) | (ub[5] << 4),
241        (hour_units & 0x0F) | (ub[6] << 4),
242        (hour_tens & 0x03) | (ub[7] << 4),
243    ]
244}
245
246/// Decode SMPTE 12M timecode bytes back to a Timecode and user bits.
247fn decode_timecode_bytes(bytes: &[u8; 8]) -> Result<(Timecode, u32), TimecodeError> {
248    let frame_units = bytes[0] & 0x0F;
249    let frame_tens = bytes[1] & 0x03;
250    let drop_frame = (bytes[1] & 0x40) != 0;
251    let sec_units = bytes[2] & 0x0F;
252    let sec_tens = bytes[3] & 0x07;
253    let min_units = bytes[4] & 0x0F;
254    let min_tens = bytes[5] & 0x07;
255    let hour_units = bytes[6] & 0x0F;
256    let hour_tens = bytes[7] & 0x03;
257
258    let hours = hour_tens * 10 + hour_units;
259    let minutes = min_tens * 10 + min_units;
260    let seconds = sec_tens * 10 + sec_units;
261    let frames = frame_tens * 10 + frame_units;
262
263    let frame_rate = if drop_frame {
264        FrameRate::Fps2997DF
265    } else {
266        FrameRate::Fps25
267    };
268
269    // User bits from upper nibbles
270    let ub: [u8; 8] = [
271        (bytes[0] >> 4) & 0x0F,
272        (bytes[1] >> 4) & 0x0F,
273        (bytes[2] >> 4) & 0x0F,
274        (bytes[3] >> 4) & 0x0F,
275        (bytes[4] >> 4) & 0x0F,
276        (bytes[5] >> 4) & 0x0F,
277        (bytes[6] >> 4) & 0x0F,
278        (bytes[7] >> 4) & 0x0F,
279    ];
280
281    let decoded_user_bits = ((ub[0] as u32) << 28)
282        | ((ub[1] as u32) << 24)
283        | ((ub[2] as u32) << 20)
284        | ((ub[3] as u32) << 16)
285        | ((ub[4] as u32) << 12)
286        | ((ub[5] as u32) << 8)
287        | ((ub[6] as u32) << 4)
288        | (ub[7] as u32);
289
290    let timecode = Timecode::new(hours, minutes, seconds, frames, frame_rate)?;
291
292    Ok((timecode, decoded_user_bits))
293}
294
295/// Add 9-bit odd parity to an 8-bit SDI word value.
296///
297/// SDI 10-bit words use bit 9 (NOT) and bit 8 (parity) for error detection.
298#[must_use]
299fn add_parity_9bit(byte: u8) -> u16 {
300    let value = byte as u16;
301    let ones = value.count_ones();
302    let parity_bit: u16 = if ones % 2 == 0 { 0x100 } else { 0x000 };
303    let not_bit: u16 = if parity_bit != 0 { 0x000 } else { 0x200 };
304    value | parity_bit | not_bit
305}
306
307/// Compute SMPTE 291 checksum for the given SDI words.
308#[must_use]
309fn compute_checksum(words: &[u16]) -> u16 {
310    let sum: u32 = words.iter().map(|&w| (w & 0x1FF) as u32).sum();
311    let checksum = (sum & 0x1FF) as u16;
312    // Bit 8 = NOT bit 8 of sum, bit 9 = NOT of bit 8
313    let b8 = (checksum >> 8) & 1;
314    checksum | ((1 - b8) << 9)
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320    use crate::FrameRate;
321
322    fn make_tc() -> Timecode {
323        Timecode::new(1, 30, 0, 12, FrameRate::Fps25).expect("valid timecode")
324    }
325
326    #[test]
327    fn test_atc_packet_creation() {
328        let tc = make_tc();
329        let pkt = AtcPacket::ltc(tc);
330        assert_eq!(pkt.atc_type, AtcType::Ltc);
331        assert!(pkt.valid);
332    }
333
334    #[test]
335    fn test_atc_to_sdi_words_length() {
336        let tc = make_tc();
337        let pkt = AtcPacket::ltc(tc);
338        let words = pkt.to_sdi_words();
339        // ADF(3) + DID + SDID + DC + 9 data + 1 cont + checksum = 16
340        assert_eq!(words.len(), 16);
341    }
342
343    #[test]
344    fn test_atc_adf_header() {
345        let tc = make_tc();
346        let pkt = AtcPacket::vitc(tc);
347        let words = pkt.to_sdi_words();
348        assert_eq!(words[0], 0x000);
349        assert_eq!(words[1], 0x3FF);
350        assert_eq!(words[2], 0x3FF);
351    }
352
353    #[test]
354    fn test_parity_9bit_even_input() {
355        // 0x00 has 0 ones — even — parity bit set, NOT bit clear
356        let w = add_parity_9bit(0x00);
357        assert_eq!(w & 0x100, 0x100); // parity bit set
358    }
359
360    #[test]
361    fn test_encode_decode_timecode_bytes() {
362        let tc = make_tc();
363        let bytes = encode_timecode_bytes(&tc, 0xDEAD_BEEF);
364        let (decoded_tc, decoded_ub) = decode_timecode_bytes(&bytes).expect("decode ok");
365        assert_eq!(decoded_tc.hours, tc.hours);
366        assert_eq!(decoded_tc.minutes, tc.minutes);
367        assert_eq!(decoded_tc.seconds, tc.seconds);
368        assert_eq!(decoded_tc.frames, tc.frames);
369        assert_eq!(decoded_ub, 0xDEAD_BEEF);
370    }
371
372    #[test]
373    fn test_atc_round_trip_ltc() {
374        let tc = make_tc();
375        let pkt = AtcPacket::ltc(tc).with_user_bits(0x1234_5678);
376        let words = pkt.to_sdi_words();
377        let decoded = AtcPacket::from_sdi_words(&words).expect("decode ok");
378        assert_eq!(decoded.atc_type, AtcType::Ltc);
379        assert_eq!(decoded.timecode.hours, 1);
380        assert_eq!(decoded.timecode.minutes, 30);
381        assert_eq!(decoded.timecode.seconds, 0);
382        assert_eq!(decoded.timecode.frames, 12);
383        assert_eq!(decoded.user_bits, 0x1234_5678);
384    }
385
386    #[test]
387    fn test_atc_round_trip_vitc() {
388        let tc = Timecode::new(23, 59, 59, 24, FrameRate::Fps25).expect("valid tc");
389        let pkt = AtcPacket::vitc(tc);
390        let words = pkt.to_sdi_words();
391        let decoded = AtcPacket::from_sdi_words(&words).expect("decode ok");
392        assert_eq!(decoded.atc_type, AtcType::Vitc);
393        assert_eq!(decoded.timecode.hours, 23);
394        assert_eq!(decoded.timecode.seconds, 59);
395    }
396
397    #[test]
398    fn test_atc_continuity_counter() {
399        let tc = make_tc();
400        let pkt = AtcPacket::ltc(tc).with_continuity(42);
401        assert!(pkt.continuity_count_valid);
402        assert_eq!(pkt.continuity_count, 42);
403        let words = pkt.to_sdi_words();
404        let decoded = AtcPacket::from_sdi_words(&words).expect("decode ok");
405        assert!(decoded.continuity_count_valid);
406        assert_eq!(decoded.continuity_count, 42);
407    }
408
409    #[test]
410    fn test_atc_from_sdi_words_too_short() {
411        let words = vec![0u16; 5];
412        assert!(AtcPacket::from_sdi_words(&words).is_err());
413    }
414
415    #[test]
416    fn test_atc_from_sdi_words_bad_adf() {
417        let mut words = vec![0u16; 16];
418        words[0] = 0x123; // invalid ADF
419        words[1] = 0x3FF;
420        words[2] = 0x3FF;
421        assert!(AtcPacket::from_sdi_words(&words).is_err());
422    }
423
424    #[test]
425    fn test_atc_zero_user_bits() {
426        let tc = make_tc();
427        let pkt = AtcPacket::ltc(tc).with_user_bits(0);
428        let words = pkt.to_sdi_words();
429        let decoded = AtcPacket::from_sdi_words(&words).expect("decode ok");
430        assert_eq!(decoded.user_bits, 0);
431    }
432
433    #[test]
434    fn test_atc_max_user_bits() {
435        let tc = make_tc();
436        let bytes = encode_timecode_bytes(&tc, 0x0FFF_FFFF);
437        let (_, decoded_ub) = decode_timecode_bytes(&bytes).expect("decode ok");
438        // Only lower 4 bits of each nibble are preserved (8 nibbles = 32 bits total,
439        // but upper nibble bits interleave with TC data). With 0x0FFF_FFFF the top
440        // nibble is 0x0 so everything fits.
441        assert_eq!(decoded_ub, 0x0FFF_FFFF);
442    }
443}