use crate::{FrameRate, Timecode};
#[derive(Debug, Clone, PartialEq)]
pub struct Smpte309mPacket {
pub did: u8,
pub sdid: u8,
pub payload: [u16; 16],
}
fn with_parity(data: u8) -> u16 {
let ones = data.count_ones();
if ones % 2 == 0 {
(1u16 << 9) | u16::from(data)
} else {
u16::from(data)
}
}
fn parity_ok(word: u16) -> bool {
(word & 0x3ff).count_ones() % 2 == 1
}
pub fn encode_anc_timecode(tc: &Timecode, binary_groups: [u8; 4]) -> Smpte309mPacket {
let frames_units = tc.frames % 10;
let frames_tens = tc.frames / 10;
let seconds_units = tc.seconds % 10;
let seconds_tens = tc.seconds / 10;
let minutes_units = tc.minutes % 10;
let minutes_tens = tc.minutes / 10;
let hours_units = tc.hours % 10;
let hours_tens = tc.hours / 10;
let word0_data: u8 = frames_units | (frames_tens << 4);
let drop_flag: u8 = if tc.frame_rate.drop_frame { 0x80 } else { 0x00 };
let word0_data = word0_data | drop_flag;
let word1_data: u8 = seconds_units | (seconds_tens << 4);
let word2_data: u8 = minutes_units | (minutes_tens << 4);
let word3_data: u8 = hours_units | (hours_tens << 4);
let mut payload = [0u16; 16];
payload[0] = with_parity(word0_data);
payload[1] = with_parity(word1_data);
payload[2] = with_parity(word2_data);
payload[3] = with_parity(word3_data);
for (i, &bg) in binary_groups.iter().enumerate() {
payload[4 + i] = with_parity(bg);
}
for i in 8..16usize {
payload[i] = with_parity(0x00);
}
Smpte309mPacket {
did: 0x60,
sdid: 0x60,
payload,
}
}
pub fn decode_anc_timecode(pkt: &Smpte309mPacket) -> Option<(Timecode, [u8; 4])> {
if pkt.did != 0x60 || pkt.sdid != 0x60 {
return None;
}
for &word in &pkt.payload {
if !parity_ok(word) {
return None;
}
}
let w0 = (pkt.payload[0] & 0xff) as u8;
let w1 = (pkt.payload[1] & 0xff) as u8;
let w2 = (pkt.payload[2] & 0xff) as u8;
let w3 = (pkt.payload[3] & 0xff) as u8;
let drop_frame = (w0 & 0x80) != 0;
let frames_units = w0 & 0x0f;
let frames_tens = (w0 & 0x70) >> 4; let frames = frames_tens * 10 + frames_units;
let seconds_units = w1 & 0x0f;
let seconds_tens = (w1 & 0x70) >> 4;
let seconds = seconds_tens * 10 + seconds_units;
let minutes_units = w2 & 0x0f;
let minutes_tens = (w2 & 0x70) >> 4;
let minutes = minutes_tens * 10 + minutes_units;
let hours_units = w3 & 0x0f;
let hours_tens = (w3 & 0x70) >> 4;
let hours = hours_tens * 10 + hours_units;
let mut binary_groups = [0u8; 4];
for (i, slot) in binary_groups.iter_mut().enumerate() {
*slot = (pkt.payload[4 + i] & 0xff) as u8;
}
let frame_rate = if drop_frame {
FrameRate::Fps2997DF
} else {
FrameRate::Fps30
};
let tc = Timecode::from_raw_fields(hours, minutes, seconds, frames, 30, drop_frame, 0);
let _ = Timecode::new(hours, minutes, seconds, frames, frame_rate).ok()?;
Some((tc, binary_groups))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::FrameRate;
fn make_tc(h: u8, m: u8, s: u8, f: u8, df: bool) -> Timecode {
let rate = if df {
FrameRate::Fps2997DF
} else {
FrameRate::Fps30
};
Timecode::new(h, m, s, f, rate).expect("valid timecode")
}
#[test]
fn test_smpte309m_round_trip_zero() {
let tc = make_tc(0, 0, 0, 0, false);
let pkt = encode_anc_timecode(&tc, [0u8; 4]);
let (decoded, _bg) = decode_anc_timecode(&pkt).expect("decode must succeed");
assert_eq!(decoded.hours, tc.hours);
assert_eq!(decoded.minutes, tc.minutes);
assert_eq!(decoded.seconds, tc.seconds);
assert_eq!(decoded.frames, tc.frames);
}
#[test]
fn test_smpte309m_round_trip_max() {
let tc = make_tc(23, 59, 59, 29, false);
let pkt = encode_anc_timecode(&tc, [0u8; 4]);
let (decoded, _bg) = decode_anc_timecode(&pkt).expect("decode must succeed");
assert_eq!(decoded.hours, 23);
assert_eq!(decoded.minutes, 59);
assert_eq!(decoded.seconds, 59);
assert_eq!(decoded.frames, 29);
}
#[test]
fn test_smpte309m_parity_correct() {
let tc = make_tc(1, 23, 45, 12, false);
let pkt = encode_anc_timecode(&tc, [0xAB, 0xCD, 0xEF, 0x01]);
for (i, &word) in pkt.payload.iter().enumerate() {
assert!(
parity_ok(word),
"word {i} failed parity check: 0x{word:03x}"
);
}
}
#[test]
fn test_smpte309m_binary_groups_passthrough() {
let tc = make_tc(0, 0, 0, 0, false);
let bg_in = [0x12u8, 0x34, 0x56, 0x78];
let pkt = encode_anc_timecode(&tc, bg_in);
let (_decoded, bg_out) = decode_anc_timecode(&pkt).expect("decode must succeed");
assert_eq!(bg_out, bg_in);
}
#[test]
fn test_smpte309m_drop_frame_flag() {
let tc = make_tc(0, 10, 0, 2, true);
let pkt = encode_anc_timecode(&tc, [0u8; 4]);
let w0_data = (pkt.payload[0] & 0xff) as u8;
assert!(
(w0_data & 0x80) != 0,
"drop-frame flag not set in word 0: 0x{w0_data:02x}"
);
let (decoded, _) = decode_anc_timecode(&pkt).expect("decode must succeed");
assert!(decoded.frame_rate.drop_frame);
}
#[test]
fn test_smpte309m_did_sdid_mismatch_returns_none() {
let tc = make_tc(0, 0, 0, 0, false);
let mut pkt = encode_anc_timecode(&tc, [0u8; 4]);
pkt.did = 0x61; assert!(decode_anc_timecode(&pkt).is_none());
}
#[test]
fn test_smpte309m_parity_error_returns_none() {
let tc = make_tc(0, 0, 0, 0, false);
let mut pkt = encode_anc_timecode(&tc, [0u8; 4]);
pkt.payload[0] ^= 0x100;
assert!(decode_anc_timecode(&pkt).is_none());
}
}