container/ac3_sync.rs
1//! AC-3 / E-AC-3 bitstream sync-header parser.
2//!
3//! Pure-Rust, decoder-free. We only walk enough of the syncframe to populate
4//! the `dac3` / `dec3` MP4 sample-entry config fields. No coefficient parsing.
5//!
6//! Refs:
7//! - **AC-3**: ETSI TS 102 366 v1.4.1 (Annex E) — same wire format as ATSC
8//! A/52. Syncword = 0x0B77 (BE). bsid<=8 (no sub-stream extensions).
9//! - **E-AC-3**: ETSI TS 102 366 §E.1.2 / §E.1.3 — bsid==16; the same
10//! 0x0B77 syncword starts each independent / dependent substream frame.
11//!
12//! Squad-26 (AC-3 + E-AC-3 passthrough into MP4) — pure-Rust per task notes
13//! ("Do NOT introduce a Dolby decoder").
14
15/// Parsed AC-3 sync-header fields needed to populate the MP4 `dac3`
16/// AudioSpecificConfig box per ETSI TS 102 366 §F.4 (AC3SpecificBox).
17///
18/// All fields come straight off the BSI (Bit Stream Information) header:
19/// syncinfo (5 bytes) + bsi { bsid bsmod acmod cmixlev/surmixlev dsurmod
20/// lfeon ... }.
21///
22/// `bit_rate_code` and `fscod` come from the syncinfo block (frmsizecod
23/// upper 5 bits = bit_rate_code; fscod = 2 bits at top of syncinfo
24/// after the 4-byte sync prefix).
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct Ac3SyncInfo {
27 /// fscod (2 bits): sample rate code. 0=48k, 1=44.1k, 2=32k, 3=reserved.
28 pub fscod: u8,
29 /// bit_rate_code (5 bits, ETSI TS 102 366 Table F.6 / Table 4.6):
30 /// indexes the nominal bit-rate table 0..=18 → 32..=640 kbps.
31 pub bit_rate_code: u8,
32 /// bsid (5 bits): bit-stream identification. AC-3 = 8; bsid==16 marks
33 /// E-AC-3 (different parser path).
34 pub bsid: u8,
35 /// bsmod (3 bits): bit-stream mode (CM, music, dialogue, etc.).
36 pub bsmod: u8,
37 /// acmod (3 bits): audio coding mode / channel layout. See ETSI Table
38 /// F.4: 0 = 1+1 dual mono, 1 = 1/0 mono, 2 = 2/0 stereo, 3 = 3/0,
39 /// 4 = 2/1, 5 = 3/1, 6 = 2/2, 7 = 3/2 (5.1 if lfeon=1).
40 pub acmod: u8,
41 /// lfeon (1 bit): low-frequency-effects channel present.
42 pub lfeon: bool,
43}
44
45/// Parsed E-AC-3 sync-header fields needed to populate the MP4 `dec3`
46/// AudioSpecificConfig box per ETSI TS 102 366 §F.6 (EC3SpecificBox).
47///
48/// Independent-substream subset only — no dependent substream fields are
49/// extracted (`num_dep_sub` defaults to 0). Vanilla 5.1 E-AC-3 is the
50/// dominant case in the wild and fits this profile.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub struct Eac3SyncInfo {
53 /// strmtyp (2 bits). 0 = independent. We only support strmtyp=0 frames
54 /// (Squad-26 scope; dependent / independent-substream-w/-deps deferred).
55 pub strmtyp: u8,
56 /// substreamid (3 bits) — 0 for vanilla E-AC-3.
57 pub substreamid: u8,
58 /// frmsiz (11 bits): frame size in 16-bit words minus one (frame size
59 /// in bytes = (frmsiz + 1) * 2). Squad-26 uses this only to derive
60 /// data_rate for the dec3 box.
61 pub frmsiz: u16,
62 /// fscod (2 bits): 0=48k 1=44.1k 2=32k 3=use_fscod2 (reduced-rate).
63 pub fscod: u8,
64 /// fscod2 (2 bits): only valid when fscod==3. 0=24k 1=22.05k 2=16k.
65 pub fscod2: u8,
66 /// numblkscod (2 bits): 0..=3 → 1/2/3/6 audio blocks per frame.
67 pub numblkscod: u8,
68 /// acmod (3 bits): channel layout — same encoding as AC-3 (Table F.4).
69 pub acmod: u8,
70 /// lfeon (1 bit): LFE channel present.
71 pub lfeon: bool,
72 /// bsid (5 bits): always 16 for E-AC-3 (11..=16 reserved for AC-3
73 /// extensions; we only emit bsid==16 in dec3).
74 pub bsid: u8,
75 /// dialnorm (5 bits) — informational; dec3 doesn't carry it.
76 pub dialnorm: u8,
77 /// bsmod (3 bits) — only present when compre==1 / dialnorm valid; we
78 /// emit zero when absent (matches ffmpeg behaviour).
79 pub bsmod: u8,
80}
81
82/// Parsed bitstream sync info — discriminated union returned by the
83/// codec-agnostic `parse_sync_info` entry point.
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum SyncInfo {
86 Ac3(Ac3SyncInfo),
87 Eac3(Eac3SyncInfo),
88}
89
90#[derive(Debug, thiserror::Error, PartialEq, Eq)]
91pub enum SyncError {
92 #[error("AC-3/E-AC-3 sync: input shorter than minimum syncframe")]
93 Truncated,
94 #[error("AC-3/E-AC-3 sync: missing 0x0B77 syncword at offset 0")]
95 MissingSyncword,
96 #[error("AC-3/E-AC-3 sync: reserved fscod=3 outside E-AC-3 reduced-rate path")]
97 ReservedFscod,
98 #[error("AC-3/E-AC-3 sync: bsid {0} outside the AC-3 (≤10) / E-AC-3 (16) ranges supported")]
99 UnsupportedBsid(u8),
100}
101
102/// Discriminate AC-3 vs E-AC-3 from the bsid field and parse the relevant
103/// sync header. Both wire formats share the leading 0x0B77 sync word.
104///
105/// The bsid byte sits at byte offset 5 in either format:
106/// syncword 0x0B77 (2) | crc1 (2) | fscod+frmsizecod (1) | bsid... ←
107/// For AC-3 (bsid≤10) the upper 5 bits of byte 5 are bsid; the lower 3
108/// are bsmod. For E-AC-3 (bsid=16) the byte layout is different but bsid
109/// still occupies the top 5 bits of byte 5 (after a strmtyp+substreamid+
110/// frmsiz reordering at bytes 2-4). The shared "bsid is the top 5 bits
111/// of byte 5" property gives us the discriminator without parsing the
112/// rest first.
113pub fn parse_sync_info(bytes: &[u8]) -> Result<SyncInfo, SyncError> {
114 if bytes.len() < 6 {
115 return Err(SyncError::Truncated);
116 }
117 if bytes[0] != 0x0B || bytes[1] != 0x77 {
118 return Err(SyncError::MissingSyncword);
119 }
120 // bsid lives in the top 5 bits of byte 5 in BOTH AC-3 and E-AC-3
121 // wire layouts (E-AC-3 §E.1.3.1.1; AC-3 §F.5.4.2.4).
122 let bsid = bytes[5] >> 3;
123 if bsid <= 10 {
124 Ok(SyncInfo::Ac3(parse_ac3(bytes)?))
125 } else if bsid == 16 {
126 Ok(SyncInfo::Eac3(parse_eac3(bytes)?))
127 } else {
128 Err(SyncError::UnsupportedBsid(bsid))
129 }
130}
131
132/// Parse the AC-3 syncframe BSI prefix per ETSI TS 102 366 §F.5.4.2.
133/// Layout (bit positions, MSB-first within each byte):
134/// syncinfo (40 bits = 5 bytes):
135/// syncword 16 bits = 0x0B77
136/// crc1 16 bits (skipped)
137/// fscod 2 bits → byte 4, top 2
138/// frmsizecod 6 bits → byte 4, low 6
139/// bsi (variable, but the prefix is fixed):
140/// bsid 5 bits → byte 5, top 5
141/// bsmod 3 bits → byte 5, low 3
142/// acmod 3 bits → byte 6, top 3
143/// [cmixlev/surmixlev 2/2 bits when acmod warrants — skipped]
144/// [dsurmod 2 bits when acmod==2 — skipped]
145/// lfeon 1 bit → varies by acmod (see below)
146///
147/// The lfeon position depends on which optional cmix/surmix/dsurmod fields
148/// are present; we walk the bit cursor through them rather than guessing.
149fn parse_ac3(bytes: &[u8]) -> Result<Ac3SyncInfo, SyncError> {
150 if bytes.len() < 7 {
151 return Err(SyncError::Truncated);
152 }
153 let mut br = BitReader::new(bytes);
154 br.skip(16); // syncword
155 br.skip(16); // crc1
156 let fscod = br.read(2) as u8;
157 if fscod == 3 {
158 return Err(SyncError::ReservedFscod);
159 }
160 let frmsizecod = br.read(6) as u8;
161 let bit_rate_code = frmsizecod >> 1; // upper 5 bits index Table F.6
162 let bsid = br.read(5) as u8;
163 let bsmod = br.read(3) as u8;
164 let acmod = br.read(3) as u8;
165
166 // Skip the optional cmix/surmix/dsurmod fields that precede lfeon
167 // per §F.5.4.2.4 (the standard documents this as a chain of `if`s).
168 if (acmod & 0x01) != 0 && acmod != 0x01 {
169 br.skip(2); // cmixlev (2 bits) — present when 3 front channels and not mono
170 }
171 if (acmod & 0x04) != 0 {
172 br.skip(2); // surmixlev (2 bits) — present when surround channels
173 }
174 if acmod == 0x02 {
175 br.skip(2); // dsurmod (2 bits) — only for stereo
176 }
177 let lfeon = br.read(1) == 1;
178
179 Ok(Ac3SyncInfo {
180 fscod,
181 bit_rate_code,
182 bsid,
183 bsmod,
184 acmod,
185 lfeon,
186 })
187}
188
189/// Parse the E-AC-3 independent-substream syncframe per ETSI TS 102 366
190/// §E.1.3.1.1 (`syncinfo()` + `bsi()`).
191///
192/// Layout (bit positions, MSB-first):
193/// syncword 16 bits = 0x0B77
194/// strmtyp 2 bits
195/// substreamid 3 bits
196/// frmsiz 11 bits
197/// fscod 2 bits
198/// fscod2 / numblkscod 2 bits (which one depends on fscod)
199/// acmod 3 bits
200/// lfeon 1 bit
201/// bsid 5 bits (=16 for E-AC-3)
202/// dialnorm 5 bits
203/// compre 1 bit
204/// if compre: compr 8 bits
205/// ... (rest of bsi we don't need)
206fn parse_eac3(bytes: &[u8]) -> Result<Eac3SyncInfo, SyncError> {
207 if bytes.len() < 8 {
208 return Err(SyncError::Truncated);
209 }
210 let mut br = BitReader::new(bytes);
211 br.skip(16); // syncword
212 let strmtyp = br.read(2) as u8;
213 let substreamid = br.read(3) as u8;
214 let frmsiz = br.read(11) as u16;
215 let fscod = br.read(2) as u8;
216 let (fscod2, numblkscod) = if fscod == 3 {
217 // Reduced sample-rate mode; fscod2 occupies these 2 bits and the
218 // frame is implicitly 6 audio blocks (numblkscod==3 equivalent).
219 (br.read(2) as u8, 3u8)
220 } else {
221 (0u8, br.read(2) as u8)
222 };
223 let acmod = br.read(3) as u8;
224 let lfeon = br.read(1) == 1;
225 let bsid = br.read(5) as u8;
226 if bsid != 16 {
227 return Err(SyncError::UnsupportedBsid(bsid));
228 }
229 let dialnorm = br.read(5) as u8;
230 let compre = br.read(1) == 1;
231 let bsmod = 0u8;
232 if compre {
233 // compr (8 bits) — discarded; bsmod sits a few fields later in the
234 // bsi but isn't critical for dec3 (ffmpeg writes 0 unless an addbsi
235 // payload describes a film/music differentiator). Leave at 0.
236 br.skip(8);
237 // We'd continue parsing chanmap / mixmdat / infomdat / addbsi to
238 // recover bsmod from the addbsi block, but Squad-26's scope is the
239 // single-substream 5.1 case. ffmpeg / x265's MP4 muxer also writes
240 // bsmod=0 unless an explicit cli flag overrides — matches us.
241 let _ = bsmod;
242 }
243 Ok(Eac3SyncInfo {
244 strmtyp,
245 substreamid,
246 frmsiz,
247 fscod,
248 fscod2,
249 numblkscod,
250 acmod,
251 lfeon,
252 bsid,
253 dialnorm,
254 bsmod,
255 })
256}
257
258/// Channel count derived from acmod + lfeon per ETSI TS 102 366 Table F.4.
259/// 1+1 dual-mono (acmod==0) gets two distinct mono streams (count=2). All
260/// other modes follow the conventional layout: 1.0 / 2.0 / 3.0 / 2.1 /
261/// 3.1 / 2.2 / 3.2 plus an optional LFE.
262pub fn channel_count(acmod: u8, lfeon: bool) -> u16 {
263 let base = match acmod {
264 0 => 2, // 1+1 (dual mono)
265 1 => 1, // 1/0 (mono)
266 2 => 2, // 2/0 (stereo)
267 3 => 3, // 3/0 (L C R)
268 4 => 3, // 2/1 (L R S)
269 5 => 4, // 3/1 (L C R S)
270 6 => 4, // 2/2 (L R Ls Rs)
271 7 => 5, // 3/2 (L C R Ls Rs)
272 _ => 0,
273 };
274 base + if lfeon { 1 } else { 0 }
275}
276
277/// Nominal bit-rate (in kbps) for an AC-3 frame given `bit_rate_code`
278/// (frmsizecod >> 1) per ETSI TS 102 366 Table F.6 / ATSC A/52 Table 5.18.
279/// 0..=18 are valid; everything above is reserved (returns 0).
280pub fn ac3_bit_rate_kbps(bit_rate_code: u8) -> u32 {
281 match bit_rate_code {
282 0 => 32,
283 1 => 40,
284 2 => 48,
285 3 => 56,
286 4 => 64,
287 5 => 80,
288 6 => 96,
289 7 => 112,
290 8 => 128,
291 9 => 160,
292 10 => 192,
293 11 => 224,
294 12 => 256,
295 13 => 320,
296 14 => 384,
297 15 => 448,
298 16 => 512,
299 17 => 576,
300 18 => 640,
301 _ => 0,
302 }
303}
304
305/// Sample rate in Hz from the AC-3 fscod (Table F.5). Reserved (3) is
306/// invalid for AC-3 — caller already rejected it; for E-AC-3 with fscod==3
307/// the sample rate comes from `eac3_sample_rate_hz` instead.
308pub fn ac3_sample_rate_hz(fscod: u8) -> u32 {
309 match fscod {
310 0 => 48_000,
311 1 => 44_100,
312 2 => 32_000,
313 _ => 0,
314 }
315}
316
317/// Sample rate in Hz for an E-AC-3 frame. fscod==3 selects the reduced-
318/// rate table (24/22.05/16 kHz); otherwise the standard 48/44.1/32 kHz
319/// table applies.
320pub fn eac3_sample_rate_hz(fscod: u8, fscod2: u8) -> u32 {
321 if fscod < 3 {
322 return ac3_sample_rate_hz(fscod);
323 }
324 match fscod2 {
325 0 => 24_000,
326 1 => 22_050,
327 2 => 16_000,
328 _ => 0,
329 }
330}
331
332/// Number of audio samples per E-AC-3 syncframe = numblkscod-derived
333/// blocks × 256 samples/block. AC-3 syncframes are always 6 blocks ×
334/// 256 = 1536 samples.
335pub fn eac3_samples_per_frame(numblkscod: u8) -> u32 {
336 let blocks = match numblkscod & 0x03 {
337 0 => 1u32,
338 1 => 2u32,
339 2 => 3u32,
340 _ => 6u32,
341 };
342 blocks * 256
343}
344
345/// MSB-first bit reader scoped to a borrowed byte slice. Used only for
346/// the BSI prefix walk — no allocation, bounded read sizes (≤16 bits
347/// per `read` call). Caller is responsible for not over-reading past
348/// the input length; we return zero-padded bits for any bit past the
349/// end (matches the H.264 `more_rbsp_data` defensive style).
350struct BitReader<'a> {
351 data: &'a [u8],
352 bit_pos: usize,
353}
354
355impl<'a> BitReader<'a> {
356 fn new(data: &'a [u8]) -> Self {
357 Self { data, bit_pos: 0 }
358 }
359 fn skip(&mut self, n: usize) {
360 self.bit_pos += n;
361 }
362 fn read(&mut self, n: usize) -> u32 {
363 debug_assert!(n <= 24, "BitReader::read: cap is 24 bits per call");
364 let mut value: u32 = 0;
365 for _ in 0..n {
366 let byte_idx = self.bit_pos / 8;
367 let bit_idx = 7 - (self.bit_pos % 8);
368 let bit = if byte_idx < self.data.len() {
369 (self.data[byte_idx] >> bit_idx) & 0x01
370 } else {
371 0
372 };
373 value = (value << 1) | bit as u32;
374 self.bit_pos += 1;
375 }
376 value
377 }
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383
384 /// Build a synthetic AC-3 syncframe header: only the first ~7 bytes
385 /// matter for our parser. Field order (bit-by-bit) per §F.5.4.2:
386 /// syncword 0x0B77 | crc1=0 | fscod | frmsizecod | bsid bsmod acmod
387 /// [optional 2/4 bits] | lfeon | rest...
388 /// `frmsizecod` upper 5 bits encode bit_rate_code per Table F.6; lower
389 /// 1 bit is the 1/2-frame indicator we don't care about. So
390 /// `frmsizecod = bit_rate_code << 1`.
391 fn synth_ac3_header(
392 fscod: u8,
393 bit_rate_code: u8,
394 bsid: u8,
395 bsmod: u8,
396 acmod: u8,
397 lfeon: bool,
398 ) -> Vec<u8> {
399 let mut bw = BitWriter::new();
400 bw.put(16, 0x0B77); // syncword
401 bw.put(16, 0); // crc1 (don't care)
402 bw.put(2, fscod as u32);
403 bw.put(6, (bit_rate_code as u32) << 1);
404 bw.put(5, bsid as u32);
405 bw.put(3, bsmod as u32);
406 bw.put(3, acmod as u32);
407 if (acmod & 0x01) != 0 && acmod != 0x01 {
408 bw.put(2, 0); // cmixlev
409 }
410 if (acmod & 0x04) != 0 {
411 bw.put(2, 0); // surmixlev
412 }
413 if acmod == 0x02 {
414 bw.put(2, 0); // dsurmod
415 }
416 bw.put(1, if lfeon { 1 } else { 0 });
417 // Pad with zeros so the buffer has the minimum length the parser
418 // checks (7 bytes is enough but we go to 12 for safety).
419 while bw.bytes.len() < 12 {
420 bw.put(8, 0);
421 }
422 bw.flush()
423 }
424
425 /// Build a synthetic E-AC-3 independent syncframe header. Per §E.1.3.1.1.
426 fn synth_eac3_header(
427 strmtyp: u8,
428 substreamid: u8,
429 frmsiz: u16,
430 fscod: u8,
431 numblkscod: u8,
432 acmod: u8,
433 lfeon: bool,
434 ) -> Vec<u8> {
435 let mut bw = BitWriter::new();
436 bw.put(16, 0x0B77);
437 bw.put(2, strmtyp as u32);
438 bw.put(3, substreamid as u32);
439 bw.put(11, frmsiz as u32);
440 bw.put(2, fscod as u32);
441 bw.put(2, numblkscod as u32);
442 bw.put(3, acmod as u32);
443 bw.put(1, if lfeon { 1 } else { 0 });
444 bw.put(5, 16); // bsid = 16 for E-AC-3
445 bw.put(5, 0); // dialnorm
446 bw.put(1, 0); // compre = 0
447 while bw.bytes.len() < 16 {
448 bw.put(8, 0);
449 }
450 bw.flush()
451 }
452
453 struct BitWriter {
454 bytes: Vec<u8>,
455 bit_pos: usize,
456 }
457 impl BitWriter {
458 fn new() -> Self {
459 Self {
460 bytes: Vec::new(),
461 bit_pos: 0,
462 }
463 }
464 fn put(&mut self, n: usize, v: u32) {
465 // MSB-first
466 for i in (0..n).rev() {
467 let bit = ((v >> i) & 0x01) as u8;
468 if self.bit_pos % 8 == 0 {
469 self.bytes.push(0);
470 }
471 let byte_idx = self.bit_pos / 8;
472 let bit_idx = 7 - (self.bit_pos % 8);
473 self.bytes[byte_idx] |= bit << bit_idx;
474 self.bit_pos += 1;
475 }
476 }
477 fn flush(self) -> Vec<u8> {
478 self.bytes
479 }
480 }
481
482 #[test]
483 fn parse_ac3_5_1_384k_48k() {
484 // Canonical 5.1 384 kbps 48 kHz AC-3: fscod=0, bit_rate_code=14
485 // (Table F.6 row 14 = 384), bsid=8, bsmod=0, acmod=7 (3/2), lfeon=1.
486 let bytes = synth_ac3_header(0, 14, 8, 0, 7, true);
487 let info = parse_sync_info(&bytes).expect("must parse");
488 match info {
489 SyncInfo::Ac3(ac3) => {
490 assert_eq!(ac3.fscod, 0, "fscod=0 → 48 kHz");
491 assert_eq!(ac3.bit_rate_code, 14, "Table F.6 idx 14 = 384 kbps");
492 assert_eq!(ac3.bsid, 8, "AC-3 bsid = 8");
493 assert_eq!(ac3.bsmod, 0);
494 assert_eq!(ac3.acmod, 7, "acmod=7 → 3/2 (5.1 with LFE)");
495 assert!(ac3.lfeon);
496 assert_eq!(channel_count(ac3.acmod, ac3.lfeon), 6);
497 assert_eq!(ac3_bit_rate_kbps(ac3.bit_rate_code), 384);
498 assert_eq!(ac3_sample_rate_hz(ac3.fscod), 48_000);
499 }
500 _ => panic!("expected AC-3"),
501 }
502 }
503
504 #[test]
505 fn parse_ac3_stereo_192k() {
506 // 2.0 stereo 192 kbps 48 kHz. acmod=2, lfeon=0, bit_rate_code=10.
507 let bytes = synth_ac3_header(0, 10, 8, 0, 2, false);
508 let info = parse_sync_info(&bytes).expect("parse");
509 match info {
510 SyncInfo::Ac3(ac3) => {
511 assert_eq!(ac3.acmod, 2);
512 assert!(!ac3.lfeon);
513 assert_eq!(channel_count(ac3.acmod, ac3.lfeon), 2);
514 assert_eq!(ac3_bit_rate_kbps(ac3.bit_rate_code), 192);
515 }
516 _ => panic!("expected AC-3"),
517 }
518 }
519
520 #[test]
521 fn parse_ac3_mono_64k() {
522 // 1.0 mono 64 kbps. acmod=1, lfeon=0, bit_rate_code=4.
523 let bytes = synth_ac3_header(0, 4, 8, 0, 1, false);
524 match parse_sync_info(&bytes).expect("parse") {
525 SyncInfo::Ac3(ac3) => {
526 assert_eq!(ac3.acmod, 1);
527 assert_eq!(channel_count(ac3.acmod, ac3.lfeon), 1);
528 assert_eq!(ac3_bit_rate_kbps(ac3.bit_rate_code), 64);
529 }
530 _ => panic!("AC-3 expected"),
531 }
532 }
533
534 #[test]
535 fn parse_eac3_5_1_independent() {
536 // Vanilla 5.1 E-AC-3, indep substream 0, fscod=0 (48 kHz),
537 // numblkscod=3 (6 blocks → 1536 samples/frame), acmod=7, lfeon=1.
538 // frmsiz = 0x05F → frame_size_bytes = (0x05F + 1) * 2 = 192.
539 let bytes = synth_eac3_header(0, 0, 0x05F, 0, 3, 7, true);
540 match parse_sync_info(&bytes).expect("parse") {
541 SyncInfo::Eac3(e) => {
542 assert_eq!(e.strmtyp, 0);
543 assert_eq!(e.substreamid, 0);
544 assert_eq!(e.frmsiz, 0x05F);
545 assert_eq!(e.fscod, 0);
546 assert_eq!(e.numblkscod, 3);
547 assert_eq!(e.acmod, 7);
548 assert!(e.lfeon);
549 assert_eq!(e.bsid, 16);
550 assert_eq!(channel_count(e.acmod, e.lfeon), 6);
551 assert_eq!(eac3_samples_per_frame(e.numblkscod), 1536);
552 assert_eq!(eac3_sample_rate_hz(e.fscod, e.fscod2), 48_000);
553 }
554 _ => panic!("expected E-AC-3"),
555 }
556 }
557
558 #[test]
559 fn parse_rejects_bad_syncword() {
560 let mut bytes = synth_ac3_header(0, 14, 8, 0, 7, true);
561 bytes[0] = 0xAA;
562 assert_eq!(parse_sync_info(&bytes), Err(SyncError::MissingSyncword));
563 }
564
565 #[test]
566 fn parse_rejects_truncated() {
567 let bytes = vec![0x0B, 0x77];
568 assert_eq!(parse_sync_info(&bytes), Err(SyncError::Truncated));
569 }
570
571 #[test]
572 fn parse_rejects_unknown_bsid() {
573 // Build an AC-3-style header with bsid=12 (between 10 and 16 = reserved).
574 let bytes = synth_ac3_header(0, 14, 12, 0, 7, true);
575 assert_eq!(parse_sync_info(&bytes), Err(SyncError::UnsupportedBsid(12)));
576 }
577
578 #[test]
579 fn channel_count_table() {
580 // No-LFE
581 assert_eq!(channel_count(0, false), 2); // 1+1 dual mono
582 assert_eq!(channel_count(1, false), 1); // mono
583 assert_eq!(channel_count(2, false), 2); // stereo
584 assert_eq!(channel_count(3, false), 3); // 3/0
585 assert_eq!(channel_count(4, false), 3); // 2/1
586 assert_eq!(channel_count(5, false), 4); // 3/1
587 assert_eq!(channel_count(6, false), 4); // 2/2
588 assert_eq!(channel_count(7, false), 5); // 3/2
589 // LFE
590 assert_eq!(channel_count(7, true), 6); // 5.1
591 assert_eq!(channel_count(2, true), 3); // 2.1
592 }
593
594 #[test]
595 fn bit_rate_table_spans_zero_to_640() {
596 assert_eq!(ac3_bit_rate_kbps(0), 32);
597 assert_eq!(ac3_bit_rate_kbps(8), 128);
598 assert_eq!(ac3_bit_rate_kbps(14), 384);
599 assert_eq!(ac3_bit_rate_kbps(18), 640);
600 assert_eq!(ac3_bit_rate_kbps(19), 0); // reserved
601 }
602}