use alloc::vec::Vec;
use broadcast_common::Serialize;
use broadcast_common::bits::{BitReader, BitWriter};
use crate::error::{BitResultExt, Error, Result};
use crate::frame_rate::FrameRate;
use crate::plex::{plex_bits, read_plex, write_plex};
use crate::util::{expect_fully_consumed, read_reserved, write_reserved};
pub const MAX_KNOWN_ZONES: usize = 9;
const RESERVED_HEADER: u64 = 0x7FE;
const RESERVED_PAN_INFO: u64 = 0x1;
const RESERVED_SNAP: u64 = 0;
const RESERVED_PRE_DECOR: u64 = 0;
const RESERVED_FINAL: u64 = 0;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(u8)]
#[non_exhaustive]
pub enum ZoneId {
ScreenLeft = 0,
ScreenCenter = 1,
ScreenRight = 2,
WallLeft = 3,
WallRight = 4,
RearLeft = 5,
RearRight = 6,
OverheadLeft = 7,
OverheadRight = 8,
}
impl ZoneId {
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::ScreenLeft => "all screen speakers left of center",
Self::ScreenCenter => "screen center speakers",
Self::ScreenRight => "all screen speakers right of center",
Self::WallLeft => "all speakers on left wall",
Self::WallRight => "all speakers on right wall",
Self::RearLeft => "all speakers on left half of rear wall",
Self::RearRight => "all speakers on right half of rear wall",
Self::OverheadLeft => "all overhead speakers left of center",
Self::OverheadRight => "all overhead speakers right of center",
}
}
pub const ALL: [ZoneId; MAX_KNOWN_ZONES] = [
Self::ScreenLeft,
Self::ScreenCenter,
Self::ScreenRight,
Self::WallLeft,
Self::WallRight,
Self::RearLeft,
Self::RearRight,
Self::OverheadLeft,
Self::OverheadRight,
];
}
broadcast_common::impl_spec_display!(ZoneId);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum ZoneGain {
GainZero,
GainOne,
Reserved(u8),
}
impl ZoneGain {
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::GainZero => "gain 0.0",
Self::GainOne => "gain 1.0",
Self::Reserved(_) => "reserved",
}
}
fn from_bits(bits: u64) -> Self {
match bits {
0x0 => Self::GainZero,
0x1 => Self::GainOne,
other => Self::Reserved(other as u8),
}
}
fn to_bits(self) -> u64 {
match self {
Self::GainZero => 0x0,
Self::GainOne => 0x1,
Self::Reserved(v) => u64::from(v),
}
}
}
broadcast_common::impl_spec_display!(ZoneGain, Reserved);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum ObjectSpreadMode {
Lowrez,
OneD,
Reserved(u8),
}
impl ObjectSpreadMode {
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::Lowrez => "object spread lowrez",
Self::OneD => "object spread 1d",
Self::Reserved(_) => "reserved",
}
}
fn from_bits(bits: u64) -> Self {
match bits {
0x0 => Self::Lowrez,
0x2 => Self::OneD,
other => Self::Reserved(other as u8),
}
}
fn to_bits(self) -> u64 {
match self {
Self::Lowrez => 0x0,
Self::OneD => 0x2,
Self::Reserved(v) => u64::from(v),
}
}
fn spread_width(self) -> Option<u32> {
match self {
Self::Lowrez => Some(8),
Self::OneD => Some(12),
Self::Reserved(_) => None,
}
}
}
broadcast_common::impl_spec_display!(ObjectSpreadMode, Reserved);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum DecorCoefPrefix {
NoDecorrelation,
MaxDecorrelation,
CoefFollows,
Reserved,
}
impl DecorCoefPrefix {
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::NoDecorrelation => "no decorrelation",
Self::MaxDecorrelation => "maximum decorrelation",
Self::CoefFollows => "decorrelation coefficient follows",
Self::Reserved => "reserved",
}
}
fn from_bits(bits: u64) -> Self {
match bits {
0x0 => Self::NoDecorrelation,
0x1 => Self::MaxDecorrelation,
0x2 => Self::CoefFollows,
_ => Self::Reserved,
}
}
fn to_bits(self) -> u64 {
match self {
Self::NoDecorrelation => 0x0,
Self::MaxDecorrelation => 0x1,
Self::CoefFollows => 0x2,
Self::Reserved => 0x3,
}
}
fn reads_coef(self) -> bool {
matches!(self, Self::CoefFollows | Self::Reserved)
}
}
broadcast_common::impl_spec_display!(DecorCoefPrefix);
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PanInfo {
pub pos_x: u16,
pub pos_y: u16,
pub pos_z: u16,
pub snap: bool,
pub zone_gains: Option<[ZoneGain; MAX_KNOWN_ZONES]>,
pub spread_mode: ObjectSpreadMode,
pub spread: u16,
pub decor_coef_prefix: DecorCoefPrefix,
pub decor_coef: Option<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PanSubBlock {
pub pan: Option<PanInfo>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct AudioDescription<'a> {
pub flag_byte: u8,
pub text: Option<&'a [u8]>,
}
const AUDIO_DESCRIPTION_TEXT_FOLLOWS: u8 = 0x80;
impl<'a> AudioDescription<'a> {
#[must_use]
pub fn none() -> Self {
Self {
flag_byte: 0,
text: None,
}
}
pub fn with_text(text: &'a [u8]) -> Result<Self> {
if text.contains(&0) {
return Err(Error::InvalidValue {
field: "AudioDescription.text",
value: 0,
reason: "text must not contain an embedded NUL byte",
});
}
Ok(Self {
flag_byte: AUDIO_DESCRIPTION_TEXT_FOLLOWS,
text: Some(text),
})
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct ObjectDefinition1<'a> {
pub meta_id: u32,
pub audio_data_id: u32,
pub pan_sub_blocks: Vec<PanSubBlock>,
pub audio_description: AudioDescription<'a>,
}
impl<'a> ObjectDefinition1<'a> {
pub fn new(
meta_id: u32,
audio_data_id: u32,
pan_sub_blocks: Vec<PanSubBlock>,
audio_description: AudioDescription<'a>,
) -> Result<Self> {
if let Some(first) = pan_sub_blocks.first()
&& first.pan.is_none()
{
return Err(Error::InvalidValue {
field: "ObjectDefinition1.pan_sub_blocks[0]",
value: 0,
reason: "sub-block 0 always carries pan info (PanInfoExists is implied 1)",
});
}
Ok(Self {
meta_id,
audio_data_id,
pan_sub_blocks,
audio_description,
})
}
pub fn parse_with_frame_rate(bytes: &'a [u8], frame_rate: FrameRate) -> Result<Self> {
let num_pan_sub_blocks = frame_rate.num_pan_sub_blocks()?;
let mut r = BitReader::new(bytes);
let meta_id = read_plex(&mut r, 8, "ObjectDefinition1.MetaID")? as u32;
let audio_data_id = read_plex(&mut r, 8, "ObjectDefinition1.AudioDataID")? as u32;
read_reserved(
&mut r,
11,
RESERVED_HEADER,
"ObjectDefinition1.Reserved(header)",
)?;
let mut pan_sub_blocks = Vec::with_capacity(usize::from(num_pan_sub_blocks));
for sb in 0..num_pan_sub_blocks {
let pan_info_exists = if sb == 0 {
true
} else {
r.read_bool().ctx("ObjectDefinition1.PanInfoExists")?
};
let pan = if pan_info_exists {
Some(Self::parse_pan_info(&mut r)?)
} else {
None
};
pan_sub_blocks.push(PanSubBlock { pan });
}
r.align_to_byte();
let audio_description = Self::parse_audio_description(bytes, &mut r)?;
read_reserved(
&mut r,
8,
RESERVED_FINAL,
"ObjectDefinition1.Reserved(final)",
)?;
expect_fully_consumed(&r, "ObjectDefinition1")?;
Ok(Self {
meta_id,
audio_data_id,
pan_sub_blocks,
audio_description,
})
}
fn parse_pan_info(r: &mut BitReader<'_>) -> Result<PanInfo> {
read_reserved(
r,
5,
RESERVED_PAN_INFO,
"ObjectDefinition1.Reserved(pan-info)",
)?;
let pos_x = r.read_bits(16).ctx("ObjectDefinition1.ObjectPosX")? as u16;
let pos_y = r.read_bits(16).ctx("ObjectDefinition1.ObjectPosY")? as u16;
let pos_z = r.read_bits(16).ctx("ObjectDefinition1.ObjectPosZ")? as u16;
let snap = r.read_bool().ctx("ObjectDefinition1.ObjectSnap")?;
if snap {
read_reserved(r, 2, RESERVED_SNAP, "ObjectDefinition1.Reserved(snap)")?;
}
let zone_control = r.read_bool().ctx("ObjectDefinition1.ObjectZoneControl")?;
let zone_gains = if zone_control {
let mut gains = [ZoneGain::GainZero; MAX_KNOWN_ZONES];
for g in &mut gains {
*g = ZoneGain::from_bits(r.read_bits(2).ctx("ObjectDefinition1.ZoneGain")?);
}
Some(gains)
} else {
None
};
let spread_mode =
ObjectSpreadMode::from_bits(r.read_bits(2).ctx("ObjectDefinition1.ObjectSpreadMode")?);
let spread = match spread_mode.spread_width() {
Some(width) => r.read_bits(width).ctx("ObjectDefinition1.ObjectSpread")? as u16,
None => 0,
};
read_reserved(
r,
4,
RESERVED_PRE_DECOR,
"ObjectDefinition1.Reserved(pre-decor)",
)?;
let decor_coef_prefix = DecorCoefPrefix::from_bits(
r.read_bits(2)
.ctx("ObjectDefinition1.ObjectDecorCoefPrefix")?,
);
let decor_coef = if decor_coef_prefix.reads_coef() {
Some(r.read_bits(8).ctx("ObjectDefinition1.ObjectDecorCoef")? as u8)
} else {
None
};
Ok(PanInfo {
pos_x,
pos_y,
pos_z,
snap,
zone_gains,
spread_mode,
spread,
decor_coef_prefix,
decor_coef,
})
}
fn parse_audio_description(
bytes: &'a [u8],
r: &mut BitReader<'a>,
) -> Result<AudioDescription<'a>> {
debug_assert!(r.is_byte_aligned());
let flag_start = r.bits_read() / 8;
let flag_byte = bytes[flag_start];
r.skip_bits(8).ctx("ObjectDefinition1.AudioDescription")?;
let text =
if flag_byte & AUDIO_DESCRIPTION_TEXT_FOLLOWS != 0 {
let text_start = flag_start + 1;
let nul_offset = bytes[text_start..].iter().position(|&b| b == 0x00).ok_or(
Error::InvalidValue {
field: "ObjectDefinition1.AudioDescription",
value: 0,
reason: "NULL-terminated text ran past the end of the element body",
},
)?;
let text_end = text_start + nul_offset;
let consumed_bytes = (text_end + 1) - text_start; r.skip_bits(consumed_bytes * 8)
.ctx("ObjectDefinition1.AudioDescription(text)")?;
Some(&bytes[text_start..text_end])
} else {
None
};
Ok(AudioDescription { flag_byte, text })
}
fn pan_info_bits(pan: Option<&PanInfo>, is_first_sub_block: bool) -> u32 {
let mut bits = u32::from(!is_first_sub_block); if let Some(info) = pan {
bits += 5; bits += 16 * 3; bits += 1; if info.snap {
bits += 2; }
bits += 1; if info.zone_gains.is_some() {
bits += 2 * MAX_KNOWN_ZONES as u32; }
bits += 2; bits += info.spread_mode.spread_width().unwrap_or(0);
bits += 4; bits += 2; if info.decor_coef.is_some() {
bits += 8; }
}
bits
}
}
impl Serialize for ObjectDefinition1<'_> {
type Error = Error;
fn serialized_len(&self) -> usize {
let mut bits = plex_bits(u64::from(self.meta_id), 8)
+ plex_bits(u64::from(self.audio_data_id), 8)
+ 11;
for (i, sb) in self.pan_sub_blocks.iter().enumerate() {
bits += Self::pan_info_bits(sb.pan.as_ref(), i == 0);
}
let mut total_bytes = (bits as usize).div_ceil(8); total_bytes += 1; if let Some(text) = self.audio_description.text {
total_bytes += text.len() + 1; }
total_bytes += 1; total_bytes
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let need = self.serialized_len();
if buf.len() < need {
return Err(Error::BufferTooShort {
need,
have: buf.len(),
what: "ObjectDefinition1",
});
}
let mut w = BitWriter::new(&mut buf[..need]);
write_plex(
&mut w,
u64::from(self.meta_id),
8,
"ObjectDefinition1.MetaID",
)?;
write_plex(
&mut w,
u64::from(self.audio_data_id),
8,
"ObjectDefinition1.AudioDataID",
)?;
write_reserved(
&mut w,
11,
RESERVED_HEADER,
"ObjectDefinition1.Reserved(header)",
)?;
for (i, sb) in self.pan_sub_blocks.iter().enumerate() {
if i > 0 {
w.write_bool(sb.pan.is_some())
.ctx("ObjectDefinition1.PanInfoExists")?;
}
if let Some(info) = &sb.pan {
write_pan_info(&mut w, info)?;
}
}
w.align_to_byte().map_err(|source| Error::Bits {
what: "ObjectDefinition1.AlignBits",
source,
})?;
w.write_bits(u64::from(self.audio_description.flag_byte), 8)
.ctx("ObjectDefinition1.AudioDescription")?;
if let Some(text) = self.audio_description.text {
for &b in text {
w.write_bits(u64::from(b), 8)
.ctx("ObjectDefinition1.AudioDescription(text)")?;
}
w.write_bits(0, 8)
.ctx("ObjectDefinition1.AudioDescription(terminator)")?;
}
write_reserved(
&mut w,
8,
RESERVED_FINAL,
"ObjectDefinition1.Reserved(final)",
)?;
Ok(need)
}
}
fn write_pan_info(w: &mut BitWriter<'_>, info: &PanInfo) -> Result<()> {
write_reserved(
w,
5,
RESERVED_PAN_INFO,
"ObjectDefinition1.Reserved(pan-info)",
)?;
w.write_bits(u64::from(info.pos_x), 16)
.ctx("ObjectDefinition1.ObjectPosX")?;
w.write_bits(u64::from(info.pos_y), 16)
.ctx("ObjectDefinition1.ObjectPosY")?;
w.write_bits(u64::from(info.pos_z), 16)
.ctx("ObjectDefinition1.ObjectPosZ")?;
w.write_bool(info.snap)
.ctx("ObjectDefinition1.ObjectSnap")?;
if info.snap {
write_reserved(w, 2, RESERVED_SNAP, "ObjectDefinition1.Reserved(snap)")?;
}
w.write_bool(info.zone_gains.is_some())
.ctx("ObjectDefinition1.ObjectZoneControl")?;
if let Some(gains) = info.zone_gains {
for gain in gains {
w.write_bits(gain.to_bits(), 2)
.ctx("ObjectDefinition1.ZoneGain")?;
}
}
w.write_bits(info.spread_mode.to_bits(), 2)
.ctx("ObjectDefinition1.ObjectSpreadMode")?;
if let Some(width) = info.spread_mode.spread_width() {
w.write_bits(u64::from(info.spread), width)
.ctx("ObjectDefinition1.ObjectSpread")?;
}
write_reserved(
w,
4,
RESERVED_PRE_DECOR,
"ObjectDefinition1.Reserved(pre-decor)",
)?;
w.write_bits(info.decor_coef_prefix.to_bits(), 2)
.ctx("ObjectDefinition1.ObjectDecorCoefPrefix")?;
if let Some(coef) = info.decor_coef {
w.write_bits(u64::from(coef), 8)
.ctx("ObjectDefinition1.ObjectDecorCoef")?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn minimal_pan_info() -> PanInfo {
PanInfo {
pos_x: 0x8000,
pos_y: 0x8000,
pos_z: 0x8000,
snap: false,
zone_gains: None,
spread_mode: ObjectSpreadMode::Reserved(1),
spread: 0,
decor_coef_prefix: DecorCoefPrefix::NoDecorrelation,
decor_coef: None,
}
}
fn full_pan_info() -> PanInfo {
PanInfo {
pos_x: 0x1234,
pos_y: 0x5678,
pos_z: 0x9ABC,
snap: true,
zone_gains: Some([ZoneGain::GainOne; MAX_KNOWN_ZONES]),
spread_mode: ObjectSpreadMode::OneD,
spread: 0x0AB,
decor_coef_prefix: DecorCoefPrefix::CoefFollows,
decor_coef: Some(128),
}
}
fn sample(frame_rate: FrameRate) -> ObjectDefinition1<'static> {
let n = usize::from(frame_rate.num_pan_sub_blocks().unwrap());
let mut pan_sub_blocks = alloc::vec![PanSubBlock {
pan: Some(full_pan_info())
}];
for i in 1..n {
pan_sub_blocks.push(PanSubBlock {
pan: if i % 2 == 0 {
Some(minimal_pan_info())
} else {
None
},
});
}
ObjectDefinition1::new(
42,
7,
pan_sub_blocks,
AudioDescription::with_text(b"dialogue").unwrap(),
)
.unwrap()
}
#[test]
fn round_trips_at_every_frame_rate() {
for fr in [
FrameRate::Fps24,
FrameRate::Fps48,
FrameRate::Fps96,
FrameRate::Fps120,
] {
let obj = sample(fr);
let bytes = obj.to_bytes();
let parsed = ObjectDefinition1::parse_with_frame_rate(&bytes, fr).unwrap();
assert_eq!(parsed, obj, "frame rate {fr:?}");
assert_eq!(parsed.to_bytes(), bytes);
}
}
#[test]
fn no_description_text_round_trips() {
let obj = ObjectDefinition1::new(
1,
2,
alloc::vec![
PanSubBlock {
pan: Some(minimal_pan_info())
},
PanSubBlock { pan: None },
],
AudioDescription::none(),
)
.unwrap();
let bytes = obj.to_bytes();
let parsed = ObjectDefinition1::parse_with_frame_rate(&bytes, FrameRate::Fps120).unwrap();
assert_eq!(parsed, obj);
}
#[test]
fn leading_sub_block_without_pan_info_is_rejected() {
let err = ObjectDefinition1::new(
0,
0,
alloc::vec![PanSubBlock { pan: None }],
AudioDescription::none(),
)
.unwrap_err();
assert!(matches!(err, Error::InvalidValue { .. }));
}
#[test]
fn embedded_nul_in_text_is_rejected() {
assert!(AudioDescription::with_text(b"bad\0text").is_err());
}
#[test]
fn reserved_frame_rate_is_rejected() {
let obj = sample(FrameRate::Fps24);
let bytes = obj.to_bytes();
let err =
ObjectDefinition1::parse_with_frame_rate(&bytes, FrameRate::Reserved(0x9)).unwrap_err();
assert!(matches!(err, Error::InvalidValue { .. }));
}
#[test]
fn mutating_pos_x_changes_only_that_sub_blocks_bytes() {
let mut obj = sample(FrameRate::Fps120);
let original = obj.to_bytes();
obj.pan_sub_blocks[0].pan.as_mut().unwrap().pos_x ^= 0xFFFF;
let mutated = obj.to_bytes();
assert_ne!(original, mutated, "flipping pos_x must change wire bytes");
}
}