use crate::AudioInfo;
use crate::ac3_sync::{Ac3SyncInfo, Eac3SyncInfo};
use super::super::Av1Mp4Muxer;
use super::super::audio_track::{
build_audio_stsd, build_dac3, build_dec3, build_ac3_sample_entry,
build_ec3_sample_entry, dac3_body_from_sync, dec3_body_from_sync,
};
fn ac3_sync_5_1_384k_48k() -> Ac3SyncInfo {
Ac3SyncInfo {
fscod: 0,
bit_rate_code: 14,
bsid: 8,
bsmod: 0,
acmod: 7,
lfeon: true,
}
}
fn ac3_info_5_1_384k() -> AudioInfo {
let body = dac3_body_from_sync(&ac3_sync_5_1_384k_48k());
AudioInfo::ac3(48_000, 6, body.to_vec())
}
fn eac3_sync_5_1_48k() -> Eac3SyncInfo {
Eac3SyncInfo {
strmtyp: 0,
substreamid: 0,
frmsiz: 191,
fscod: 0,
fscod2: 0,
numblkscod: 3,
acmod: 7,
lfeon: true,
bsid: 16,
dialnorm: 0,
bsmod: 0,
}
}
fn eac3_info_5_1_384k() -> AudioInfo {
let body = dec3_body_from_sync(&eac3_sync_5_1_48k(), 192);
AudioInfo::eac3(48_000, 6, body.to_vec())
}
#[test]
fn dac3_box_3_byte_payload_layout() {
let info = ac3_info_5_1_384k();
let dac3 = build_dac3(&info);
assert_eq!(dac3.len(), 11, "dac3 = 8-byte header + 3-byte body");
let size = u32::from_be_bytes([dac3[0], dac3[1], dac3[2], dac3[3]]) as usize;
assert_eq!(size, dac3.len(), "size field equals box length");
assert_eq!(&dac3[4..8], b"dac3", "box type 'dac3'");
let raw = ((dac3[8] as u32) << 16) | ((dac3[9] as u32) << 8) | dac3[10] as u32;
assert_eq!((raw >> 22) & 0x03, 0, "fscod = 0 (48 kHz)");
assert_eq!((raw >> 17) & 0x1F, 8, "bsid = 8 (AC-3)");
assert_eq!((raw >> 14) & 0x07, 0, "bsmod = 0");
assert_eq!((raw >> 11) & 0x07, 7, "acmod = 7 (3/2 = 5.1 with LFE)");
assert_eq!((raw >> 10) & 0x01, 1, "lfeon = 1");
assert_eq!((raw >> 5) & 0x1F, 14, "bit_rate_code = 14 (= 384 kbps)");
assert_eq!(raw & 0x1F, 0, "reserved 5 bits = 0");
}
#[test]
fn ac3_sample_entry_size_and_fourcc() {
let info = ac3_info_5_1_384k();
let entry = build_ac3_sample_entry(&info);
let size = u32::from_be_bytes([entry[0], entry[1], entry[2], entry[3]]) as usize;
assert_eq!(size, entry.len(), "size field equals box length");
assert_eq!(&entry[4..8], b"ac-3", "4cc MUST be 'ac-3' (with hyphen)");
assert_ne!(
&entry[4..8],
b"ac3\0",
"4cc 'ac3' (3-char) is non-conformant"
);
assert_eq!(
entry.len(),
47,
"ac-3 sample entry = 36 (preamble) + 11 (dac3)"
);
let dac3_pos = entry
.windows(4)
.position(|w| w == b"dac3")
.expect("dac3 child missing");
assert!(
dac3_pos > 28,
"dac3 must come after AudioSampleEntry preamble"
);
let sr_q16 = u32::from_be_bytes([entry[32], entry[33], entry[34], entry[35]]);
assert_eq!(sr_q16, 48_000u32 << 16, "samplerate = 48000 << 16 (Q16)");
}
#[test]
fn dec3_box_5_byte_payload_layout() {
let info = eac3_info_5_1_384k();
let dec3 = build_dec3(&info);
assert_eq!(dec3.len(), 13, "dec3 = 8-byte header + 5-byte body");
let size = u32::from_be_bytes([dec3[0], dec3[1], dec3[2], dec3[3]]) as usize;
assert_eq!(size, dec3.len(), "size field equals box length");
assert_eq!(&dec3[4..8], b"dec3", "box type 'dec3'");
let header = ((dec3[8] as u16) << 8) | dec3[9] as u16;
let data_rate = (header >> 3) & 0x1FFF;
assert_eq!(data_rate, 192, "data_rate = 192 (= 384 kbps / 2)");
let num_ind_sub_minus_1 = header & 0x07;
assert_eq!(num_ind_sub_minus_1, 0, "single substream → field = 0");
let sub = ((dec3[10] as u32) << 16) | ((dec3[11] as u32) << 8) | dec3[12] as u32;
assert_eq!((sub >> 22) & 0x03, 0, "fscod = 0 (48 kHz)");
assert_eq!((sub >> 17) & 0x1F, 16, "bsid = 16 (E-AC-3 marker)");
assert_eq!((sub >> 12) & 0x07, 0, "bsmod = 0");
assert_eq!((sub >> 9) & 0x07, 7, "acmod = 7 (3/2 = 5.1 with LFE)");
assert_eq!((sub >> 8) & 0x01, 1, "lfeon = 1");
assert_eq!((sub >> 1) & 0x0F, 0, "num_dep_sub = 0 (single substream)");
}
#[test]
fn ec3_sample_entry_size_and_fourcc() {
let info = eac3_info_5_1_384k();
let entry = build_ec3_sample_entry(&info);
let size = u32::from_be_bytes([entry[0], entry[1], entry[2], entry[3]]) as usize;
assert_eq!(size, entry.len(), "size field equals box length");
assert_eq!(&entry[4..8], b"ec-3", "4cc MUST be 'ec-3' (with hyphen)");
assert_eq!(
entry.len(),
49,
"ec-3 sample entry = 36 (preamble) + 13 (dec3)"
);
let dec3_pos = entry
.windows(4)
.position(|w| w == b"dec3")
.expect("dec3 child missing");
assert!(
dec3_pos > 28,
"dec3 must come after AudioSampleEntry preamble"
);
}
#[test]
fn stsd_dispatcher_routes_ac3_eac3() {
let stsd_ac3 = build_audio_stsd(&ac3_info_5_1_384k());
assert!(
stsd_ac3.windows(4).any(|w| w == b"ac-3"),
"AC-3 stsd has 'ac-3'"
);
assert!(
stsd_ac3.windows(4).any(|w| w == b"dac3"),
"AC-3 stsd has 'dac3'"
);
assert!(
!stsd_ac3.windows(4).any(|w| w == b"mp4a"),
"AC-3 stsd MUST NOT have mp4a"
);
assert!(
!stsd_ac3.windows(4).any(|w| w == b"Opus"),
"AC-3 stsd MUST NOT have Opus"
);
assert!(
!stsd_ac3.windows(4).any(|w| w == b"esds"),
"AC-3 stsd MUST NOT have esds"
);
let stsd_eac3 = build_audio_stsd(&eac3_info_5_1_384k());
assert!(
stsd_eac3.windows(4).any(|w| w == b"ec-3"),
"E-AC-3 stsd has 'ec-3'"
);
assert!(
stsd_eac3.windows(4).any(|w| w == b"dec3"),
"E-AC-3 stsd has 'dec3'"
);
assert!(
!stsd_eac3.windows(4).any(|w| w == b"mp4a"),
"E-AC-3 stsd MUST NOT have mp4a"
);
assert!(
!stsd_eac3.windows(4).any(|w| w == b"esds"),
"E-AC-3 stsd MUST NOT have esds"
);
assert!(
!stsd_eac3.windows(4).any(|w| w == b"dac3"),
"E-AC-3 stsd MUST NOT have dac3"
);
}
#[test]
fn with_audio_accepts_ac3_5_1_and_rejects_bad_shape() {
let mut muxer = Av1Mp4Muxer::new(320, 240, 30.0).unwrap();
muxer
.with_audio(ac3_info_5_1_384k())
.expect("5.1 AC-3 must be accepted");
let mut muxer2 = Av1Mp4Muxer::new(320, 240, 30.0).unwrap();
let mut bad = ac3_info_5_1_384k();
bad.codec_private = vec![0u8; 2];
let err = muxer2
.with_audio(bad)
.err()
.expect("must reject 2-byte dac3");
assert!(format!("{err:#}").contains("3 bytes"));
let mut muxer3 = Av1Mp4Muxer::new(320, 240, 30.0).unwrap();
let bad_sr = AudioInfo {
sample_rate: 22_050,
timescale: 22_050,
..ac3_info_5_1_384k()
};
let err = muxer3
.with_audio(bad_sr)
.err()
.expect("must reject 22050 for AC-3");
assert!(format!("{err:#}").contains("32000"));
}
#[test]
fn with_audio_accepts_eac3_5_1_and_rejects_short_dec3() {
let mut muxer = Av1Mp4Muxer::new(320, 240, 30.0).unwrap();
muxer
.with_audio(eac3_info_5_1_384k())
.expect("5.1 E-AC-3 must be accepted");
let mut muxer2 = Av1Mp4Muxer::new(320, 240, 30.0).unwrap();
let mut bad = eac3_info_5_1_384k();
bad.codec_private = vec![0u8; 4];
let err = muxer2
.with_audio(bad)
.err()
.expect("must reject short dec3");
assert!(format!("{err:#}").contains("≥5"));
}
#[test]
fn with_audio_rejects_ac3_more_than_6_channels() {
let mut muxer = Av1Mp4Muxer::new(320, 240, 30.0).unwrap();
let bad = AudioInfo {
channels: 8,
..ac3_info_5_1_384k()
};
let err = muxer.with_audio(bad).err().expect("must reject 8 channels");
assert!(format!("{err:#}").contains("1..=6"));
}
#[test]
fn ac3_sync_to_dac3_to_sample_entry_roundtrip() {
let sync = ac3_sync_5_1_384k_48k();
let body = dac3_body_from_sync(&sync);
let info = AudioInfo::ac3(48_000, 6, body.to_vec());
let entry = build_ac3_sample_entry(&info);
let dac3_pos = entry.windows(4).position(|w| w == b"dac3").unwrap();
let dac3_body_start = dac3_pos + 4;
let raw = ((entry[dac3_body_start] as u32) << 16)
| ((entry[dac3_body_start + 1] as u32) << 8)
| entry[dac3_body_start + 2] as u32;
assert_eq!((raw >> 22) & 0x03, sync.fscod as u32);
assert_eq!((raw >> 17) & 0x1F, sync.bsid as u32);
assert_eq!((raw >> 11) & 0x07, sync.acmod as u32);
assert_eq!((raw >> 10) & 0x01, sync.lfeon as u32);
assert_eq!((raw >> 5) & 0x1F, sync.bit_rate_code as u32);
}