Skip to main content

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}