use broadcast_common::{Parse, Serialize};
use crate::error::{Error, Result};
pub const FRAME_LEN: usize = 10;
pub const MAX_HOURS: u8 = 23;
pub const MAX_MINUTES_SECONDS: u8 = 59;
pub const MAX_FRAMES: u8 = 29;
pub const MAX_BINARY_GROUP: u8 = 0x0F;
pub const SYNC_WORD: [u8; 2] = [0xFC, 0xBF];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum FrameRate {
Fps30,
Fps25,
Fps24,
}
impl FrameRate {
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::Fps30 => "30-frame",
Self::Fps25 => "25-frame",
Self::Fps24 => "24-frame",
}
}
}
broadcast_common::impl_spec_display!(FrameRate);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum BinaryGroupUsage {
UnspecifiedUnspecified,
UnspecifiedEightBitCodes,
ClockTimeUnspecified,
Reserved,
UnspecifiedDateTimeZone,
UnspecifiedPageLine,
ClockTimeDateTimeZone,
ClockTimePageLine,
}
impl BinaryGroupUsage {
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::UnspecifiedUnspecified => "unspecified time address, unspecified binary group",
Self::UnspecifiedEightBitCodes => "unspecified time address, 8-bit codes",
Self::ClockTimeUnspecified => "clock time, unspecified binary group",
Self::Reserved => "reserved time address, reserved binary group",
Self::UnspecifiedDateTimeZone => "unspecified time address, date and time zone",
Self::UnspecifiedPageLine => "unspecified time address, page/line",
Self::ClockTimeDateTimeZone => "clock time, date and time zone",
Self::ClockTimePageLine => "clock time, page/line",
}
}
#[must_use]
pub fn from_flags(bgf2: bool, bgf1: bool, bgf0: bool) -> Self {
match (bgf2, bgf1, bgf0) {
(false, false, false) => Self::UnspecifiedUnspecified,
(false, false, true) => Self::UnspecifiedEightBitCodes,
(false, true, false) => Self::ClockTimeUnspecified,
(false, true, true) => Self::Reserved,
(true, false, false) => Self::UnspecifiedDateTimeZone,
(true, false, true) => Self::UnspecifiedPageLine,
(true, true, false) => Self::ClockTimeDateTimeZone,
(true, true, true) => Self::ClockTimePageLine,
}
}
}
broadcast_common::impl_spec_display!(BinaryGroupUsage);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BinaryGroupFlags {
pub bgf2: bool,
pub bgf1: bool,
pub bgf0: bool,
}
impl BinaryGroupFlags {
#[must_use]
pub fn usage(&self) -> BinaryGroupUsage {
BinaryGroupUsage::from_flags(self.bgf2, self.bgf1, self.bgf0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct LtcFrame {
pub hours: u8,
pub minutes: u8,
pub seconds: u8,
pub frames: u8,
pub drop_frame_flag: bool,
pub color_frame_flag: bool,
pub flag_bit_27: bool,
pub flag_bit_43: bool,
pub flag_bit_58: bool,
pub flag_bit_59: bool,
pub user_bits: [u8; 8],
}
impl LtcFrame {
#[must_use]
pub fn polarity_correction(&self, rate: FrameRate) -> bool {
match rate {
FrameRate::Fps25 => self.flag_bit_59,
FrameRate::Fps30 | FrameRate::Fps24 => self.flag_bit_27,
}
}
#[must_use]
pub fn binary_group_flags(&self, rate: FrameRate) -> BinaryGroupFlags {
let (bgf0, bgf2) = match rate {
FrameRate::Fps25 => (self.flag_bit_27, self.flag_bit_43),
FrameRate::Fps30 | FrameRate::Fps24 => (self.flag_bit_43, self.flag_bit_59),
};
BinaryGroupFlags {
bgf2,
bgf1: self.flag_bit_58,
bgf0,
}
}
fn validate(&self) -> Result<()> {
if self.hours > MAX_HOURS {
return Err(Error::InvalidValue {
field: "hours",
value: self.hours,
reason: "exceeds the 24-hour-clock maximum (23)",
});
}
if self.minutes > MAX_MINUTES_SECONDS {
return Err(Error::InvalidValue {
field: "minutes",
value: self.minutes,
reason: "exceeds the maximum (59)",
});
}
if self.seconds > MAX_MINUTES_SECONDS {
return Err(Error::InvalidValue {
field: "seconds",
value: self.seconds,
reason: "exceeds the maximum (59)",
});
}
if self.frames > MAX_FRAMES {
return Err(Error::InvalidValue {
field: "frames",
value: self.frames,
reason: "exceeds the widest supported frame-rate maximum (29)",
});
}
for (index, &value) in self.user_bits.iter().enumerate() {
if value > MAX_BINARY_GROUP {
return Err(Error::InvalidBinaryGroup { index, value });
}
}
Ok(())
}
}
impl<'a> Parse<'a> for LtcFrame {
type Error = Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
if bytes.len() < FRAME_LEN {
return Err(Error::BufferTooShort {
need: FRAME_LEN,
have: bytes.len(),
what: "LTC codeword",
});
}
if [bytes[8], bytes[9]] != SYNC_WORD {
return Err(Error::SyncWordMismatch {
expected: SYNC_WORD,
found: [bytes[8], bytes[9]],
});
}
let frame_units = bytes[0] & 0x0F;
let user_bits_1 = bytes[0] >> 4;
let frame_tens = bytes[1] & 0x03;
let drop_frame_flag = bytes[1] & 0x04 != 0;
let color_frame_flag = bytes[1] & 0x08 != 0;
let user_bits_2 = bytes[1] >> 4;
let seconds_units = bytes[2] & 0x0F;
let user_bits_3 = bytes[2] >> 4;
let seconds_tens = bytes[3] & 0x07;
let flag_bit_27 = bytes[3] & 0x08 != 0;
let user_bits_4 = bytes[3] >> 4;
let minutes_units = bytes[4] & 0x0F;
let user_bits_5 = bytes[4] >> 4;
let minutes_tens = bytes[5] & 0x07;
let flag_bit_43 = bytes[5] & 0x08 != 0;
let user_bits_6 = bytes[5] >> 4;
let hours_units = bytes[6] & 0x0F;
let user_bits_7 = bytes[6] >> 4;
let hours_tens = bytes[7] & 0x03;
let flag_bit_58 = bytes[7] & 0x04 != 0;
let flag_bit_59 = bytes[7] & 0x08 != 0;
let user_bits_8 = bytes[7] >> 4;
let frame = Self {
hours: hours_tens * 10 + hours_units,
minutes: minutes_tens * 10 + minutes_units,
seconds: seconds_tens * 10 + seconds_units,
frames: frame_tens * 10 + frame_units,
drop_frame_flag,
color_frame_flag,
flag_bit_27,
flag_bit_43,
flag_bit_58,
flag_bit_59,
user_bits: [
user_bits_1,
user_bits_2,
user_bits_3,
user_bits_4,
user_bits_5,
user_bits_6,
user_bits_7,
user_bits_8,
],
};
frame.validate()?;
Ok(frame)
}
}
impl Serialize for LtcFrame {
type Error = Error;
fn serialized_len(&self) -> usize {
FRAME_LEN
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
if buf.len() < FRAME_LEN {
return Err(Error::BufferTooShort {
need: FRAME_LEN,
have: buf.len(),
what: "LTC codeword serialize output",
});
}
self.validate()?;
let frame_units = self.frames % 10;
let frame_tens = self.frames / 10;
let seconds_units = self.seconds % 10;
let seconds_tens = self.seconds / 10;
let minutes_units = self.minutes % 10;
let minutes_tens = self.minutes / 10;
let hours_units = self.hours % 10;
let hours_tens = self.hours / 10;
buf[0] = frame_units | (self.user_bits[0] << 4);
buf[1] = frame_tens
| (u8::from(self.drop_frame_flag) << 2)
| (u8::from(self.color_frame_flag) << 3)
| (self.user_bits[1] << 4);
buf[2] = seconds_units | (self.user_bits[2] << 4);
buf[3] = seconds_tens | (u8::from(self.flag_bit_27) << 3) | (self.user_bits[3] << 4);
buf[4] = minutes_units | (self.user_bits[4] << 4);
buf[5] = minutes_tens | (u8::from(self.flag_bit_43) << 3) | (self.user_bits[5] << 4);
buf[6] = hours_units | (self.user_bits[6] << 4);
buf[7] = hours_tens
| (u8::from(self.flag_bit_58) << 2)
| (u8::from(self.flag_bit_59) << 3)
| (self.user_bits[7] << 4);
buf[8] = SYNC_WORD[0];
buf[9] = SYNC_WORD[1];
Ok(FRAME_LEN)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_frame() -> LtcFrame {
LtcFrame {
hours: 1,
minutes: 23,
seconds: 45,
frames: 13,
drop_frame_flag: false,
color_frame_flag: true,
flag_bit_27: true,
flag_bit_43: true,
flag_bit_58: false,
flag_bit_59: true,
user_bits: [1, 2, 3, 4, 5, 6, 7, 8],
}
}
#[test]
fn round_trip() {
let f = sample_frame();
let mut out = [0u8; FRAME_LEN];
f.serialize_into(&mut out).unwrap();
assert_eq!(LtcFrame::parse(&out).unwrap(), f);
}
#[test]
fn serialize_then_parse_matches_spec_vector() {
let f = sample_frame();
let mut out = [0u8; FRAME_LEN];
f.serialize_into(&mut out).unwrap();
assert_eq!(
out,
[0x13, 0x29, 0x35, 0x4C, 0x53, 0x6A, 0x71, 0x88, 0xFC, 0xBF]
);
}
#[test]
fn rejects_sync_word_mismatch() {
let mut bytes = [0x13, 0x29, 0x35, 0x4C, 0x53, 0x6A, 0x71, 0x88, 0xFC, 0xBF];
bytes[9] = 0x00;
assert!(matches!(
LtcFrame::parse(&bytes),
Err(Error::SyncWordMismatch { .. })
));
}
#[test]
fn rejects_buffer_too_short() {
let bytes = [0u8; 9];
assert!(matches!(
LtcFrame::parse(&bytes),
Err(Error::BufferTooShort {
need: 10,
have: 9,
..
})
));
}
#[test]
fn rejects_hours_over_23() {
let mut f = sample_frame();
f.hours = 24;
let mut out = [0u8; FRAME_LEN];
assert!(matches!(
f.serialize_into(&mut out),
Err(Error::InvalidValue { field: "hours", .. })
));
}
#[test]
fn rejects_frames_over_29() {
let mut f = sample_frame();
f.frames = 30;
let mut out = [0u8; FRAME_LEN];
assert!(matches!(
f.serialize_into(&mut out),
Err(Error::InvalidValue {
field: "frames",
..
})
));
}
#[test]
fn rejects_user_bit_over_15() {
let mut f = sample_frame();
f.user_bits[3] = 0x10;
let mut out = [0u8; FRAME_LEN];
assert!(matches!(
f.serialize_into(&mut out),
Err(Error::InvalidBinaryGroup {
index: 3,
value: 0x10
})
));
}
#[test]
fn polarity_correction_and_bgf_swap_between_25_and_other_rates() {
let f = sample_frame(); assert!(f.polarity_correction(FrameRate::Fps30));
assert!(f.polarity_correction(FrameRate::Fps24));
assert!(f.polarity_correction(FrameRate::Fps25));
let bg30 = f.binary_group_flags(FrameRate::Fps30);
assert_eq!(
bg30,
BinaryGroupFlags {
bgf2: true,
bgf1: false,
bgf0: true
}
);
assert_eq!(bg30.usage(), BinaryGroupUsage::UnspecifiedPageLine);
let mut g = f;
g.flag_bit_27 = true; g.flag_bit_43 = false; g.flag_bit_58 = true; g.flag_bit_59 = false;
assert!(g.polarity_correction(FrameRate::Fps30));
assert!(g.polarity_correction(FrameRate::Fps24));
assert!(!g.polarity_correction(FrameRate::Fps25));
let bg30 = g.binary_group_flags(FrameRate::Fps30);
assert_eq!(
bg30,
BinaryGroupFlags {
bgf2: false,
bgf1: true,
bgf0: false
}
);
let bg25 = g.binary_group_flags(FrameRate::Fps25);
assert_eq!(
bg25,
BinaryGroupFlags {
bgf2: false,
bgf1: true,
bgf0: true
}
);
assert_ne!(
bg30, bg25,
"25-frame's BGF0 must differ from 30/24-frame's here"
);
}
#[test]
fn binary_group_usage_covers_all_eight_combinations() {
for bgf2 in [false, true] {
for bgf1 in [false, true] {
for bgf0 in [false, true] {
let _ = BinaryGroupUsage::from_flags(bgf2, bgf1, bgf0);
}
}
}
}
#[test]
fn field_mutation_changes_bytes() {
let a = sample_frame();
let mut b = a;
b.seconds = a.seconds.wrapping_add(1);
let mut oa = [0u8; FRAME_LEN];
let mut ob = [0u8; FRAME_LEN];
a.serialize_into(&mut oa).unwrap();
b.serialize_into(&mut ob).unwrap();
assert_ne!(oa, ob);
}
}