moq-mux 0.10.13

Media muxers and demuxers for MoQ
Documentation
//! ADTS framing for AAC.
//!
//! TS carries AAC as ADTS: every raw AAC frame is prefixed by a 7-byte header
//! (9 with CRC). The moq-mux AAC codec layer works in raw AAC plus an
//! AudioSpecificConfig, so import strips ADTS headers (and synthesizes a
//! config from the first one) while export re-frames raw AAC into ADTS.

use anyhow::Context;

/// ISO 14496-3 sampling-frequency-index table.
const SAMPLE_RATES: [u32; 13] = [
	96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350,
];

/// Bytes needed to attempt a header parse: the fixed header without a CRC.
pub(super) const MIN_HEADER_LEN: usize = 7;

/// Fields decoded from an ADTS fixed+variable header.
#[derive(Clone, Copy, Debug)]
pub(super) struct Header {
	/// audioObjectType (ADTS `profile` + 1). AAC-LC is 2.
	pub object_type: u8,
	pub sample_rate: u32,
	/// channelConfiguration; 0 means a program config element in the raw data block describes the channels.
	pub channel_config: u8,
	/// Total access-unit length, header included.
	pub frame_len: usize,
	/// Header length: 7 without CRC, 9 with.
	pub header_len: usize,
}

impl Header {
	/// Parse the ADTS header at the start of `data`.
	pub fn parse(data: &[u8]) -> anyhow::Result<Self> {
		anyhow::ensure!(data.len() >= MIN_HEADER_LEN, "ADTS header truncated");
		anyhow::ensure!(data[0] == 0xFF && (data[1] & 0xF0) == 0xF0, "missing ADTS syncword");

		let protection_absent = data[1] & 0x01;
		let profile = (data[2] >> 6) & 0x03;
		let freq_index = (data[2] >> 2) & 0x0F;
		let channel_config = ((data[2] & 0x01) << 2) | ((data[3] >> 6) & 0x03);
		let frame_len =
			((data[3] as usize & 0x03) << 11) | ((data[4] as usize) << 3) | ((data[5] as usize >> 5) & 0x07);

		let sample_rate = SAMPLE_RATES
			.get(freq_index as usize)
			.copied()
			.context("reserved ADTS sample rate index")?;
		let header_len = if protection_absent == 1 { 7 } else { 9 };
		anyhow::ensure!(frame_len >= header_len, "ADTS frame length smaller than its header");

		Ok(Self {
			object_type: profile + 1,
			sample_rate,
			channel_config,
			frame_len,
			header_len,
		})
	}
}

/// Build a 7-byte ADTS header (no CRC) for a raw AAC frame of `raw_len` bytes.
pub(super) fn write_header(
	object_type: u8,
	sample_rate: u32,
	channel_config: u8,
	raw_len: usize,
) -> anyhow::Result<[u8; 7]> {
	// ADTS `profile` is the 2-bit audioObjectType - 1, so only Main, LC, SSR, and LTP fit.
	anyhow::ensure!(
		(1..=4).contains(&object_type),
		"audioObjectType {object_type} not representable in ADTS"
	);
	let profile = object_type - 1;
	let freq_index = freq_index_from_rate(sample_rate)?;
	// ADTS has 3 bits for it; the higher configurations only fit an AudioSpecificConfig.
	anyhow::ensure!(
		channel_config < 8,
		"channelConfiguration {channel_config} not representable in ADTS"
	);

	let frame_len = raw_len + 7;
	anyhow::ensure!(frame_len < (1 << 13), "AAC frame too large for ADTS framing");

	let mut h = [0u8; 7];
	h[0] = 0xFF;
	// syncword high nibble + MPEG-4 + layer 0 + protection_absent (no CRC).
	h[1] = 0xF1;
	h[2] = (profile << 6) | ((freq_index & 0x0F) << 2) | ((channel_config >> 2) & 0x01);
	h[3] = ((channel_config & 0x03) << 6) | ((frame_len >> 11) as u8 & 0x03);
	h[4] = ((frame_len >> 3) & 0xFF) as u8;
	// low 3 bits of frame_len, then buffer fullness = 0x7FF (variable rate).
	h[5] = (((frame_len & 0x07) << 5) as u8) | 0x1F;
	h[6] = 0xFC;
	Ok(h)
}

fn freq_index_from_rate(sample_rate: u32) -> anyhow::Result<u8> {
	// ADTS has no explicit-rate escape (unlike AudioSpecificConfig), so a rate
	// outside the table can't be represented. Fail rather than mislabel it.
	SAMPLE_RATES
		.iter()
		.position(|&r| r == sample_rate)
		.map(|i| i as u8)
		.with_context(|| format!("sample rate {sample_rate} not representable in ADTS"))
}

/// Map a channel count to the ADTS `channel_config` (ISO 14496-3 Table 1.19) naming it, refusing a
/// count that none does rather than guess a layout.
pub(super) fn channel_config_from_count(channel_count: u32) -> anyhow::Result<u8> {
	match channel_count {
		1..=6 => Ok(channel_count as u8),
		8 => Ok(7),
		_ => anyhow::bail!("{channel_count} channels have no ADTS channelConfiguration"),
	}
}

#[cfg(test)]
mod tests {
	use super::*;

	#[test]
	fn header_roundtrip() {
		// AAC-LC (object_type 2), 48 kHz, stereo, 100-byte raw frame.
		let header = write_header(2, 48_000, 2, 100).unwrap();
		let parsed = Header::parse(&header).unwrap();

		assert_eq!(parsed.object_type, 2);
		assert_eq!(parsed.sample_rate, 48_000);
		assert_eq!(parsed.channel_config, 2);
		assert_eq!(parsed.header_len, 7);
		assert_eq!(parsed.frame_len, 107, "frame_len includes the 7-byte header");
	}

	#[test]
	fn parse_rejects_bad_syncword() {
		let mut header = write_header(2, 44_100, 2, 10).unwrap();
		header[0] = 0x00;
		assert!(Header::parse(&header).is_err());
	}

	#[test]
	fn write_refuses_object_types_outside_the_profile_field() {
		for object_type in [0, 5, 29] {
			assert!(write_header(object_type, 48_000, 2, 10).is_err());
		}
		for object_type in 1..=4 {
			let header = write_header(object_type, 48_000, 2, 10).unwrap();
			assert_eq!(Header::parse(&header).unwrap().object_type, object_type);
		}
	}

	#[test]
	fn channel_counts_without_a_configuration_are_refused() {
		assert_eq!(channel_config_from_count(8).unwrap(), 7);
		for count in [0, 7, 9, 24] {
			assert!(channel_config_from_count(count).is_err());
		}
	}

	#[test]
	fn frame_len_for_5_1() {
		let header = write_header(2, 44_100, 6, 512).unwrap();
		let parsed = Header::parse(&header).unwrap();
		assert_eq!(parsed.channel_config, 6);
		assert_eq!(parsed.sample_rate, 44_100);
		assert_eq!(parsed.frame_len, 519);
	}
}