use crate::bitreader::write_bits_checked;
use crate::error::{Error, Result};
use alloc::vec::Vec;
use broadcast_common::bits::{BitReader, BitWriter};
use broadcast_common::error::BufferTooShort;
use broadcast_common::len::{fit_u16, need_out};
use broadcast_common::{Parse, Serialize};
pub const DCA3_FOURCC: [u8; 4] = *b"dca3";
pub const AV3A_FOURCC: [u8; 4] = *b"av3a";
pub const SAMPLING_FREQUENCY_INDEX_EXPLICIT: u8 = 0xF;
fn short(what: &'static str) -> Error {
Error::BufferTooShort(BufferTooShort {
need: 0,
have: 0,
what,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum Avs3AudioContent {
Channel {
channel_number_index: u8,
reserved: u8,
},
Object {
number_objects: u8,
reserved: u8,
},
ChannelAndObject {
channel_number_index: u8,
reserved_channel: u8,
number_objects: u8,
reserved_object: u8,
},
Hoa {
hoa_order: u8,
},
Reserved(u8),
}
impl Avs3AudioContent {
#[must_use]
pub const fn content_type(&self) -> u8 {
match self {
Self::Channel { .. } => 0,
Self::Object { .. } => 1,
Self::ChannelAndObject { .. } => 2,
Self::Hoa { .. } => 3,
Self::Reserved(v) => *v,
}
}
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::Channel { .. } => "channel signal",
Self::Object { .. } => "object signal",
Self::ChannelAndObject { .. } => "channel and object signal",
Self::Hoa { .. } => "HOA signal",
Self::Reserved(_) => "reserved",
}
}
}
broadcast_common::impl_spec_display!(Avs3AudioContent, Reserved);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct Avs3AudioGaConfig {
pub sampling_frequency_index: u8,
pub nn_type: u8,
pub reserved: u8,
pub content: Avs3AudioContent,
pub total_bitrate: u16,
pub resolution: u8,
pub reserved_tail: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct Avs3AudioLlConfig {
pub sampling_frequency_index: u8,
pub sampling_frequency: Option<u32>,
pub anc_data_index: bool,
pub coding_profile: u8,
pub channel_number: u8,
pub resolution: u8,
pub addition_info: Vec<u8>,
pub reserved: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct Avs3AudioGhConfig {
pub sampling_frequency_index: u8,
pub anc_data_index: bool,
pub coding_profile: u8,
pub bitstream_type: bool,
pub channel_number_index: u8,
pub bitrate_index: u8,
pub raw_frame_length: u16,
pub resolution: u8,
pub addition_info: Vec<u8>,
pub reserved: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum Ca3Config {
GeneralBitrate(Avs3AudioGaConfig),
Lossless(Avs3AudioLlConfig),
GeneralHigh(Avs3AudioGhConfig),
Other {
audio_codec_id: u8,
body: Vec<u8>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct Ca3SpecificBox {
pub config: Ca3Config,
}
impl Ca3Config {
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::GeneralBitrate(_) => "general full-rate (audio_codec_id 2)",
Self::Lossless(_) => "lossless (audio_codec_id 1)",
Self::GeneralHigh(_) => "general high-rate (audio_codec_id 0)",
Self::Other { .. } => "reserved audio_codec_id",
}
}
#[must_use]
pub const fn audio_codec_id(&self) -> u8 {
match self {
Self::GeneralBitrate(_) => 2,
Self::Lossless(_) => 1,
Self::GeneralHigh(_) => 0,
Self::Other { audio_codec_id, .. } => *audio_codec_id,
}
}
}
broadcast_common::impl_spec_display!(Ca3Config);
fn parse_ga(r: &mut BitReader<'_>) -> Result<Avs3AudioGaConfig> {
let w = "dca3 GA config";
let rd = |r: &mut BitReader<'_>, n: u32| r.read_bits(n).map_err(|_| short(w));
let sampling_frequency_index = rd(r, 4)? as u8;
let nn_type = rd(r, 3)? as u8;
let reserved = rd(r, 1)? as u8;
let content_type = rd(r, 4)? as u8;
let content = match content_type {
0 => Avs3AudioContent::Channel {
channel_number_index: rd(r, 7)? as u8,
reserved: rd(r, 1)? as u8,
},
1 => Avs3AudioContent::Object {
number_objects: rd(r, 7)? as u8,
reserved: rd(r, 1)? as u8,
},
2 => Avs3AudioContent::ChannelAndObject {
channel_number_index: rd(r, 7)? as u8,
reserved_channel: rd(r, 1)? as u8,
number_objects: rd(r, 7)? as u8,
reserved_object: rd(r, 1)? as u8,
},
3 => Avs3AudioContent::Hoa {
hoa_order: rd(r, 4)? as u8,
},
other => Avs3AudioContent::Reserved(other),
};
let total_bitrate = rd(r, 16)? as u16;
let resolution = rd(r, 2)? as u8;
let tail_bits = if content_type == 3 { 2 } else { 6 };
let reserved_tail = rd(r, tail_bits)? as u8;
Ok(Avs3AudioGaConfig {
sampling_frequency_index,
nn_type,
reserved,
content,
total_bitrate,
resolution,
reserved_tail,
})
}
fn parse_gh(r: &mut BitReader<'_>) -> Result<Avs3AudioGhConfig> {
let w = "dca3 GH config";
let rd = |r: &mut BitReader<'_>, n: u32| r.read_bits(n).map_err(|_| short(w));
let sampling_frequency_index = rd(r, 4)? as u8;
let anc_data_index = rd(r, 1)? != 0;
let coding_profile = rd(r, 3)? as u8;
let bitstream_type = rd(r, 1)? != 0;
let channel_number_index = rd(r, 7)? as u8;
let bitrate_index = rd(r, 4)? as u8;
let raw_frame_length = rd(r, 16)? as u16;
let resolution = rd(r, 2)? as u8;
let n = rd(r, 16)? as usize;
let mut addition_info = Vec::with_capacity(n.min(r.bits_remaining() / 8));
for _ in 0..n {
addition_info.push(rd(r, 8)? as u8);
}
let reserved = rd(r, 6)? as u8;
Ok(Avs3AudioGhConfig {
sampling_frequency_index,
anc_data_index,
coding_profile,
bitstream_type,
channel_number_index,
bitrate_index,
raw_frame_length,
resolution,
addition_info,
reserved,
})
}
fn parse_ll(r: &mut BitReader<'_>) -> Result<Avs3AudioLlConfig> {
let w = "dca3 LL config";
let rd = |r: &mut BitReader<'_>, n: u32| r.read_bits(n).map_err(|_| short(w));
let sampling_frequency_index = rd(r, 4)? as u8;
let sampling_frequency = if sampling_frequency_index == SAMPLING_FREQUENCY_INDEX_EXPLICIT {
Some(rd(r, 24)? as u32)
} else {
None
};
let anc_data_index = rd(r, 1)? != 0;
let coding_profile = rd(r, 3)? as u8;
let channel_number = rd(r, 8)? as u8;
let resolution = rd(r, 2)? as u8;
let n = rd(r, 16)? as usize;
let mut addition_info = Vec::with_capacity(n.min(r.bits_remaining() / 8));
for _ in 0..n {
addition_info.push(rd(r, 8)? as u8);
}
let reserved = rd(r, 2)? as u8;
Ok(Avs3AudioLlConfig {
sampling_frequency_index,
sampling_frequency,
anc_data_index,
coding_profile,
channel_number,
resolution,
addition_info,
reserved,
})
}
impl<'a> Parse<'a> for Ca3SpecificBox {
type Error = Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
let first = *bytes.first().ok_or(Error::BufferTooShort(BufferTooShort {
need: 1,
have: 0,
what: "dca3 body",
}))?;
let id = first >> 4;
let mut r = BitReader::new(bytes);
r.skip_bits(4).map_err(|_| short("dca3 body"))?;
let config = match id {
2 => Ca3Config::GeneralBitrate(parse_ga(&mut r)?),
1 => Ca3Config::Lossless(parse_ll(&mut r)?),
0 => Ca3Config::GeneralHigh(parse_gh(&mut r)?),
other => {
return Ok(Self {
config: Ca3Config::Other {
audio_codec_id: other,
body: bytes.to_vec(),
},
});
}
};
if r.bits_remaining() >= 8 {
return Err(Error::InvalidInput("dca3 body has trailing bytes"));
}
Ok(Self { config })
}
}
impl Ca3SpecificBox {
fn bits(&self) -> usize {
match &self.config {
Ca3Config::GeneralBitrate(g) => {
let content = match g.content {
Avs3AudioContent::Channel { .. } | Avs3AudioContent::Object { .. } => 8,
Avs3AudioContent::ChannelAndObject { .. } => 16,
Avs3AudioContent::Hoa { .. } => 4,
Avs3AudioContent::Reserved(_) => 0,
};
let tail = if g.content.content_type() == 3 { 2 } else { 6 };
4 + 4 + 3 + 1 + 4 + content + 16 + 2 + tail
}
Ca3Config::Lossless(l) => {
let sf = if l.sampling_frequency.is_some() {
24
} else {
0
};
4 + 4 + sf + 1 + 3 + 8 + 2 + 16 + 8 * l.addition_info.len() + 2
}
Ca3Config::GeneralHigh(g) => {
4 + 4 + 1 + 3 + 1 + 7 + 4 + 16 + 2 + 16 + 8 * g.addition_info.len() + 6
}
Ca3Config::Other { body, .. } => 8 * body.len(),
}
}
}
impl Serialize for Ca3SpecificBox {
type Error = Error;
fn serialized_len(&self) -> usize {
self.bits().div_ceil(8)
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let need = self.serialized_len();
need_out(buf, need)?;
if let Ca3Config::Other {
body,
audio_codec_id,
} = &self.config
{
if body.first().map(|b| b >> 4) != Some(*audio_codec_id)
|| matches!(*audio_codec_id, 0..=2)
{
return Err(Error::InvalidInput(
"dca3 Other body must start with a non-1/2 audio_codec_id",
));
}
buf[..need].copy_from_slice(body);
return Ok(need);
}
buf[..need].fill(0);
let mut w = BitWriter::new(&mut buf[..need]);
write_bits_checked(
&mut w,
u64::from(self.config.audio_codec_id()),
4,
"audio_codec_id",
)?;
match &self.config {
Ca3Config::GeneralBitrate(g) => {
write_bits_checked(
&mut w,
u64::from(g.sampling_frequency_index),
4,
"sampling_frequency_index",
)?;
write_bits_checked(&mut w, u64::from(g.nn_type), 3, "nn_type")?;
write_bits_checked(&mut w, u64::from(g.reserved), 1, "reserved")?;
write_bits_checked(
&mut w,
u64::from(g.content.content_type()),
4,
"content_type",
)?;
match g.content {
Avs3AudioContent::Channel {
channel_number_index,
reserved,
} => {
write_bits_checked(
&mut w,
u64::from(channel_number_index),
7,
"channel_number_index",
)?;
write_bits_checked(&mut w, u64::from(reserved), 1, "reserved")?;
}
Avs3AudioContent::Object {
number_objects,
reserved,
} => {
write_bits_checked(&mut w, u64::from(number_objects), 7, "number_objects")?;
write_bits_checked(&mut w, u64::from(reserved), 1, "reserved")?;
}
Avs3AudioContent::ChannelAndObject {
channel_number_index,
reserved_channel,
number_objects,
reserved_object,
} => {
write_bits_checked(
&mut w,
u64::from(channel_number_index),
7,
"channel_number_index",
)?;
write_bits_checked(&mut w, u64::from(reserved_channel), 1, "reserved")?;
write_bits_checked(&mut w, u64::from(number_objects), 7, "number_objects")?;
write_bits_checked(&mut w, u64::from(reserved_object), 1, "reserved")?;
}
Avs3AudioContent::Hoa { hoa_order } => {
write_bits_checked(&mut w, u64::from(hoa_order), 4, "hoa_order")?;
}
Avs3AudioContent::Reserved(_) => {}
}
write_bits_checked(&mut w, u64::from(g.total_bitrate), 16, "total_bitrate")?;
write_bits_checked(&mut w, u64::from(g.resolution), 2, "resolution")?;
let tail_bits = if g.content.content_type() == 3 { 2 } else { 6 };
write_bits_checked(&mut w, u64::from(g.reserved_tail), tail_bits, "reserved")?;
}
Ca3Config::Lossless(l) => {
let explicit = l.sampling_frequency_index == SAMPLING_FREQUENCY_INDEX_EXPLICIT;
write_bits_checked(
&mut w,
u64::from(l.sampling_frequency_index),
4,
"sampling_frequency_index",
)?;
match (explicit, l.sampling_frequency) {
(true, Some(f)) => {
write_bits_checked(&mut w, u64::from(f), 24, "sampling_frequency")?
}
(false, None) => {}
_ => {
return Err(Error::InvalidInput(
"sampling_frequency present iff sampling_frequency_index == 0xF",
));
}
}
write_bits_checked(&mut w, u64::from(l.anc_data_index), 1, "anc_data_index")?;
write_bits_checked(&mut w, u64::from(l.coding_profile), 3, "coding_profile")?;
write_bits_checked(&mut w, u64::from(l.channel_number), 8, "channel_number")?;
write_bits_checked(&mut w, u64::from(l.resolution), 2, "resolution")?;
let n = fit_u16(l.addition_info.len(), "addition_info_length")?;
write_bits_checked(&mut w, u64::from(n), 16, "addition_info_length")?;
for b in &l.addition_info {
write_bits_checked(&mut w, u64::from(*b), 8, "addition_info")?;
}
write_bits_checked(&mut w, u64::from(l.reserved), 2, "reserved")?;
}
Ca3Config::GeneralHigh(g) => {
write_bits_checked(
&mut w,
u64::from(g.sampling_frequency_index),
4,
"sampling_frequency_index",
)?;
write_bits_checked(&mut w, u64::from(g.anc_data_index), 1, "anc_data_index")?;
write_bits_checked(&mut w, u64::from(g.coding_profile), 3, "coding_profile")?;
write_bits_checked(&mut w, u64::from(g.bitstream_type), 1, "bitstream_type")?;
write_bits_checked(
&mut w,
u64::from(g.channel_number_index),
7,
"channel_number_index",
)?;
write_bits_checked(&mut w, u64::from(g.bitrate_index), 4, "bitrate_index")?;
write_bits_checked(
&mut w,
u64::from(g.raw_frame_length),
16,
"raw_frame_length",
)?;
write_bits_checked(&mut w, u64::from(g.resolution), 2, "resolution")?;
let n = fit_u16(g.addition_info.len(), "addition_info_length")?;
write_bits_checked(&mut w, u64::from(n), 16, "addition_info_length")?;
for b in &g.addition_info {
write_bits_checked(&mut w, u64::from(*b), 8, "addition_info")?;
}
write_bits_checked(&mut w, u64::from(g.reserved), 6, "reserved")?;
}
Ca3Config::Other { .. } => {}
}
Ok(need)
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
fn round_trip(bytes: &[u8]) -> Ca3SpecificBox {
let b = Ca3SpecificBox::parse(bytes).unwrap();
let mut out = vec![0u8; b.serialized_len()];
let n = b.serialize_into(&mut out).unwrap();
assert_eq!(&out[..n], bytes, "byte-exact round trip");
b
}
#[test]
fn dca3_general_channel_signal() {
let bytes = [0x22, 0x20, 0x04, 0x01, 0x40, 0x80];
let b = round_trip(&bytes);
let Ca3Config::GeneralBitrate(g) = b.config else {
panic!()
};
assert_eq!((g.sampling_frequency_index, g.nn_type), (2, 1));
assert_eq!(
g.content,
Avs3AudioContent::Channel {
channel_number_index: 2,
reserved: 0
}
);
assert_eq!(
(g.total_bitrate, g.resolution, g.reserved_tail),
(320, 2, 0)
);
}
#[test]
fn dca3_general_mixed_and_hoa() {
let mixed = [0x21, 0x02, 0x06, 0x08, 0x01, 0x00, 0x40];
let b = round_trip(&mixed);
let Ca3Config::GeneralBitrate(g) = b.config else {
panic!()
};
assert_eq!(
g.content,
Avs3AudioContent::ChannelAndObject {
channel_number_index: 3,
reserved_channel: 0,
number_objects: 4,
reserved_object: 0
}
);
let hoa = [0x22, 0x03, 0x30, 0x20, 0x08];
let b = round_trip(&hoa);
let Ca3Config::GeneralBitrate(g) = b.config else {
panic!()
};
assert_eq!(g.content, Avs3AudioContent::Hoa { hoa_order: 3 });
assert_eq!((g.total_bitrate, g.resolution), (0x0200, 2));
}
#[test]
fn dca3_lossless_explicit_frequency() {
let ll = Ca3SpecificBox {
config: Ca3Config::Lossless(Avs3AudioLlConfig {
sampling_frequency_index: 0xF,
sampling_frequency: Some(96_000),
anc_data_index: false,
coding_profile: 0,
channel_number: 2,
resolution: 2,
addition_info: vec![0xDE, 0xAD],
reserved: 0,
}),
};
let mut out = vec![0u8; ll.serialized_len()];
let n = ll.serialize_into(&mut out).unwrap();
assert_eq!(
&out[..n],
&[0x1F, 0x01, 0x77, 0x00, 0x00, 0x28, 0x00, 0x0B, 0x7A, 0xB4][..]
);
let back = Ca3SpecificBox::parse(&out[..n]).unwrap();
assert_eq!(back, ll);
}
#[test]
fn dca3_general_high_bitrate() {
let g = Avs3AudioGhConfig {
sampling_frequency_index: 2,
anc_data_index: false,
coding_profile: 0,
bitstream_type: true,
channel_number_index: 2,
bitrate_index: 3,
raw_frame_length: 0x0400,
resolution: 2,
addition_info: vec![],
reserved: 0,
};
let b = Ca3SpecificBox {
config: Ca3Config::GeneralHigh(g),
};
let mut out = vec![0u8; b.serialized_len()];
let n = b.serialize_into(&mut out).unwrap();
assert_eq!(n, 8);
assert_eq!(
&out[..n],
&[0x02, 0x08, 0x23, 0x04, 0x00, 0x80, 0x00, 0x00][..]
);
assert_eq!(round_trip(&out[..n]), b);
}
#[test]
fn dca3_other_codec_id_is_preserved_and_malformed_rejected() {
let b = round_trip(&[0x70, 0xAB, 0xCD]);
assert_eq!(b.config.audio_codec_id(), 7);
assert!(matches!(
Ca3SpecificBox::parse(&[]),
Err(crate::error::Error::BufferTooShort(..))
));
assert!(
matches!(
Ca3SpecificBox::parse(&[0x22, 0x20]),
Err(crate::error::Error::BufferTooShort(..))
),
"truncated GA config"
);
assert!(
matches!(
Ca3SpecificBox::parse(&[0x22, 0x20, 0x04, 0x01, 0x40, 0x80, 0xFF]),
Err(crate::error::Error::InvalidInput(..))
),
"trailing"
);
let bad = Ca3SpecificBox {
config: Ca3Config::Other {
audio_codec_id: 2,
body: vec![0x20],
},
};
assert!(matches!(
bad.serialize_into(&mut [0u8; 4]),
Err(crate::error::Error::InvalidInput(..))
));
}
#[test]
fn dca3_serialize_rejects_overwide_and_inconsistent() {
let mut g = match round_trip(&[0x22, 0x20, 0x04, 0x01, 0x40, 0x80]).config {
Ca3Config::GeneralBitrate(g) => g,
_ => unreachable!(),
};
g.nn_type = 8;
let b = Ca3SpecificBox {
config: Ca3Config::GeneralBitrate(g),
};
assert!(matches!(
b.serialize_into(&mut [0u8; 8]),
Err(crate::error::Error::FieldOverflow(..))
));
let b = Ca3SpecificBox {
config: Ca3Config::Lossless(Avs3AudioLlConfig {
sampling_frequency_index: 0xF,
sampling_frequency: None,
anc_data_index: false,
coding_profile: 0,
channel_number: 2,
resolution: 1,
addition_info: vec![],
reserved: 0,
}),
};
assert!(matches!(
b.serialize_into(&mut [0u8; 16]),
Err(crate::error::Error::InvalidInput(..))
));
}
}