use crate::Error;
const PROFILE_LC: u8 = 2;
const OBJECT_TYPE_SBR: u8 = 5;
const OBJECT_TYPE_PS: u8 = 29;
const SYNC_EXTENSION: u32 = 0x2B7;
const MAX_SAMPLE_RATE: u32 = 0xFF_FFFF;
fn validate(config: &moq_mux::codec::aac::Config) -> Result<(), Error> {
if config.profile != PROFILE_LC {
return Err(Error::Unsupported(format!(
"only AAC-LC is supported (got mp4a.40.{})",
config.profile
)));
}
if !matches!(config.channel_count, 1 | 2) {
return Err(Error::Unsupported(format!(
"aac decoding is limited to mono and stereo (got {} channels)",
config.channel_count
)));
}
if config.sample_rate > MAX_SAMPLE_RATE {
return Err(Error::Unsupported(format!(
"aac sample rate must fit 24 bits (got {})",
config.sample_rate
)));
}
Ok(())
}
pub(crate) fn description(catalog: &hang::catalog::AudioConfig, profile: u8) -> Result<bytes::Bytes, Error> {
let description = match &catalog.description {
Some(description) => {
if description.len() < 2 {
return Err(Error::Unsupported(format!(
"aac description must be at least 2 bytes (got {})",
description.len()
)));
}
description.clone()
}
None => {
if catalog.sample_rate == 0 || catalog.channel_count == 0 {
return Err(Error::Unsupported(
"aac catalog without a description must declare a sample rate and channel count".into(),
));
}
let config = moq_mux::codec::aac::Config {
profile,
sample_rate: catalog.sample_rate,
channel_count: catalog.channel_count,
};
validate(&config)?;
config.encode()
}
};
let parsed = moq_mux::codec::aac::Config::parse(&mut description.as_ref()).map_err(moq_mux::Error::from)?;
validate(&parsed)?;
if declares_sbr(&description) {
return Err(Error::Unsupported(
"only AAC-LC is supported (the config declares SBR after the core)".into(),
));
}
Ok(description)
}
fn declares_sbr(description: &[u8]) -> bool {
fn scan(bits: &mut Bits) -> Option<bool> {
if object_type(bits)? != PROFILE_LC {
return Some(false);
}
if bits.read(4)? == 15 {
bits.read(24)?;
}
if bits.read(4)? == 0 {
return Some(false);
}
bits.read(1)?;
if bits.read(1)? == 1 {
bits.read(14)?;
}
if bits.read(1)? == 1 {
bits.read(1)?;
}
if bits.left() < 16 || bits.read(11)? != SYNC_EXTENSION {
return Some(false);
}
if !matches!(object_type(bits)?, OBJECT_TYPE_SBR | OBJECT_TYPE_PS) {
return Some(false);
}
Some(bits.read(1)? == 1)
}
scan(&mut Bits::new(description)).unwrap_or(false)
}
fn object_type(bits: &mut Bits) -> Option<u8> {
match bits.read(5)? {
31 => Some(32 + bits.read(6)? as u8),
value => Some(value as u8),
}
}
struct Bits<'a> {
data: &'a [u8],
pos: usize,
}
impl<'a> Bits<'a> {
fn new(data: &'a [u8]) -> Self {
Self { data, pos: 0 }
}
fn left(&self) -> usize {
self.data.len() * 8 - self.pos
}
fn read(&mut self, count: usize) -> Option<u32> {
if count > 32 || self.left() < count {
return None;
}
let mut value = 0;
for _ in 0..count {
let bit = (self.data[self.pos / 8] >> (7 - self.pos % 8)) & 1;
value = (value << 1) | u32::from(bit);
self.pos += 1;
}
Some(value)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn catalog(profile: u8, sample_rate: u32, channels: u32) -> hang::catalog::AudioConfig {
hang::catalog::AudioConfig::new(hang::catalog::AAC { profile }, sample_rate, channels)
}
#[test]
fn description_is_kept_verbatim() {
let mut config = catalog(2, 44_100, 1);
config.description = Some(bytes::Bytes::from_static(&[0x17, 0x80, 0x56, 0x22, 0x08]));
let description = description(&config, 2).unwrap();
assert_eq!(description.as_ref(), &[0x17, 0x80, 0x56, 0x22, 0x08]);
}
#[test]
fn description_is_synthesized_from_catalog_fields() {
let description = description(&catalog(2, 44_100, 1), 2).unwrap();
assert_eq!(description.as_ref(), &[0x12, 0x08]);
}
#[test]
fn rejects_he_aac() {
let err = description(&catalog(5, 44_100, 2), 5).unwrap_err();
assert!(matches!(err, Error::Unsupported(msg) if msg.contains("mp4a.40.5")));
}
#[test]
fn rejects_an_extended_profile_before_synthesis_masks_it() {
let err = description(&catalog(34, 44_100, 2), 34).unwrap_err();
assert!(matches!(err, Error::Unsupported(msg) if msg.contains("mp4a.40.34")));
}
#[test]
fn rejects_more_than_stereo() {
assert!(matches!(
description(&catalog(2, 48_000, 12), 2),
Err(Error::Unsupported(_))
));
assert!(matches!(
description(&catalog(2, 48_000, 6), 2),
Err(Error::Unsupported(_))
));
}
#[test]
fn rejects_a_sample_rate_the_config_cannot_hold() {
let err = description(&catalog(2, 0x0100_AC44, 2), 2).unwrap_err();
assert!(matches!(err, Error::Unsupported(msg) if msg.contains("24 bits")));
}
#[test]
fn rejects_backward_compatible_he_aac() {
let mut config = catalog(2, 44_100, 2);
config.description = Some(bytes::Bytes::from_static(&[0x12, 0x10, 0x56, 0xE5, 0x98]));
let err = description(&config, 2).unwrap_err();
assert!(matches!(err, Error::Unsupported(msg) if msg.contains("SBR")));
}
#[test]
fn accepts_a_sync_extension_that_declares_no_sbr() {
let mut config = catalog(2, 44_100, 2);
config.description = Some(bytes::Bytes::from_static(&[0x12, 0x10, 0x56, 0xE5, 0x18]));
assert!(description(&config, 2).is_ok());
}
#[test]
fn rejects_description_disagreeing_with_the_codec_string() {
let mut config = catalog(2, 44_100, 2);
config.description = Some(bytes::Bytes::from_static(&[0x2A, 0x10]));
assert!(matches!(description(&config, 2), Err(Error::Unsupported(_))));
}
#[test]
fn rejects_truncated_description() {
let mut config = catalog(2, 44_100, 1);
config.description = Some(bytes::Bytes::from_static(&[0x12]));
assert!(matches!(description(&config, 2), Err(Error::Unsupported(_))));
}
#[test]
fn rejects_missing_shape() {
assert!(matches!(description(&catalog(2, 0, 1), 2), Err(Error::Unsupported(_))));
assert!(matches!(
description(&catalog(2, 44_100, 0), 2),
Err(Error::Unsupported(_))
));
}
}