Skip to main content

oxideav_dvd/
ac3.rs

1//! DVD-Video AC-3 (Dolby Digital) sync-frame header decoder.
2//!
3//! On DVD-Video, AC-3 audio is carried inside MPEG-PS
4//! `private_stream_1` (`stream_id = 0xBD`) PES packets under the
5//! `0x80..=0x87` substream allocation that the VOB demuxer already
6//! routes (see [`crate::vob::DvdSubstream::Ac3`]). The demuxer hands
7//! the elementary stream to a downstream audio decoder as raw bytes;
8//! the very first bytes of that stream are an AC-3 *sync frame*, whose
9//! `syncinfo()` + the leading fixed-position fields of `bsi()` pin the
10//! sample rate, the frame size, the nominal bit rate, and the channel
11//! layout. This module decodes those header fields so a player can
12//! size buffers, label the track, and seek to a frame boundary without
13//! pulling in a full AC-3 audio decoder.
14//!
15//! ## Scope
16//!
17//! The decode covers the `syncinfo()` (sync word, `crc1`, `fscod`,
18//! `frmsizecod`) and the deterministically-positioned prefix of
19//! `bsi()` — `bsid`, `bsmod`, `acmod`, and the four conditional
20//! mix-level / surround-mode fields whose presence is a pure function
21//! of `acmod` (`cmixlev`, `surmixlev`, `dsurmod`), plus `lfeon`. After
22//! `lfeon` the `bsi()` layout becomes variable-length (conditional
23//! fields gated by their own flag bits), which a header-only reader
24//! cannot traverse without a full bit-budget walk; those fields are
25//! out of scope and the raw frame bytes stay available to the audio
26//! decoder.
27//!
28//! The decode is read-only and allocation-free.
29//!
30//! ## Clean-room references
31//!
32//! - `docs/container/dvd/application/stnsoft-ac3hdr.html` — the
33//!   `syncinfo()` field layout, the `fscod` sampling-rate table, the
34//!   `frmsizecod` frame-size / nominal-bit-rate table (16-bit words
35//!   per sync frame at each of the three sample rates), and the
36//!   `bsi()` field order with the `acmod` audio-coding-mode table,
37//!   the `cmixlev` / `surmixlev` / `dsurmod` conditional-presence
38//!   rules, and the `bsmod` bitstream-mode table.
39//! - `docs/container/dvd/application/mpucoder-dvdmpeg.html` — the
40//!   `0x80..=0x87` substream allocation that locates the AC-3
41//!   elementary stream inside the `private_stream_1` PES payload.
42//!
43//! Field layouts derive from the `stnsoft-ac3hdr.html` reference cited
44//! above.
45
46use crate::error::{Error, Result};
47
48/// AC-3 sync word — the big-endian `0x0B77` at the start of every
49/// sync frame.
50pub const AC3_SYNC_WORD: u16 = 0x0B77;
51
52/// Sampling rate carried in the 2-bit `fscod` field.
53///
54/// `00 = 48 kHz`, `01 = 44.1 kHz`, `10 = 32 kHz`, `11 = reserved`.
55/// The `Reserved` variant marks a malformed / future code without
56/// losing the fact that the field was read.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum Ac3SampleRate {
59    /// 48 kHz (`fscod = 00`).
60    Hz48000,
61    /// 44.1 kHz (`fscod = 01`).
62    Hz44100,
63    /// 32 kHz (`fscod = 10`).
64    Hz32000,
65    /// Reserved (`fscod = 11`).
66    Reserved,
67}
68
69impl Ac3SampleRate {
70    fn from_code(code: u8) -> Self {
71        match code & 0b11 {
72            0 => Self::Hz48000,
73            1 => Self::Hz44100,
74            2 => Self::Hz32000,
75            _ => Self::Reserved,
76        }
77    }
78
79    /// Sample rate in Hz for the three defined codes; `None` for
80    /// [`Self::Reserved`].
81    pub fn hz(self) -> Option<u32> {
82        match self {
83            Self::Hz48000 => Some(48_000),
84            Self::Hz44100 => Some(44_100),
85            Self::Hz32000 => Some(32_000),
86            Self::Reserved => None,
87        }
88    }
89}
90
91/// Audio coding mode carried in the 3-bit `acmod` field.
92///
93/// The enum names the speaker layout in the spec's `front/surround`
94/// notation; [`Self::channel_count`] returns `nfchans` (the count of
95/// full-bandwidth channels, *excluding* the optional LFE channel).
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum Ac3AudioCodingMode {
98    /// `000` — 1+1 (two independent mono channels, Ch1 + Ch2).
99    DualMono,
100    /// `001` — 1/0 (centre only).
101    Mono,
102    /// `010` — 2/0 (left + right stereo).
103    Stereo,
104    /// `011` — 3/0 (left, centre, right).
105    ThreeZero,
106    /// `100` — 2/1 (left, right, mono surround).
107    TwoOne,
108    /// `101` — 3/1 (left, centre, right, mono surround).
109    ThreeOne,
110    /// `110` — 2/2 (left, right, left-surround, right-surround).
111    TwoTwo,
112    /// `111` — 3/2 (left, centre, right, left-surround,
113    /// right-surround).
114    ThreeTwo,
115}
116
117impl Ac3AudioCodingMode {
118    fn from_code(code: u8) -> Self {
119        match code & 0b111 {
120            0 => Self::DualMono,
121            1 => Self::Mono,
122            2 => Self::Stereo,
123            3 => Self::ThreeZero,
124            4 => Self::TwoOne,
125            5 => Self::ThreeOne,
126            6 => Self::TwoTwo,
127            _ => Self::ThreeTwo,
128        }
129    }
130
131    /// The raw 3-bit `acmod` code.
132    pub fn code(self) -> u8 {
133        match self {
134            Self::DualMono => 0,
135            Self::Mono => 1,
136            Self::Stereo => 2,
137            Self::ThreeZero => 3,
138            Self::TwoOne => 4,
139            Self::ThreeOne => 5,
140            Self::TwoTwo => 6,
141            Self::ThreeTwo => 7,
142        }
143    }
144
145    /// `nfchans` — the number of full-bandwidth channels, excluding
146    /// any LFE. (Add 1 to this for the total channel count when
147    /// [`Ac3Header::lfe_on`] is `true`.)
148    pub fn channel_count(self) -> u8 {
149        match self {
150            Self::DualMono => 2,
151            Self::Mono => 1,
152            Self::Stereo => 2,
153            Self::ThreeZero => 3,
154            Self::TwoOne => 3,
155            Self::ThreeOne => 4,
156            Self::TwoTwo => 4,
157            Self::ThreeTwo => 5,
158        }
159    }
160
161    /// `true` for the three modes (`3/0`, `3/1`, `3/2`) that carry a
162    /// centre channel and therefore a `cmixlev` field — per the
163    /// `(acmod & 0x1) && (acmod != 0x1)` rule in the BSI layout.
164    pub fn has_center_mix_level(self) -> bool {
165        let code = self.code();
166        (code & 0x1) != 0 && code != 0x1
167    }
168
169    /// `true` for the four modes (`2/1`, `3/1`, `2/2`, `3/2`) that
170    /// carry a surround channel and therefore a `surmixlev` field —
171    /// per the `acmod & 0x4` rule in the BSI layout.
172    pub fn has_surround_mix_level(self) -> bool {
173        (self.code() & 0x4) != 0
174    }
175
176    /// `true` only for the `2/0` stereo mode, which carries the
177    /// `dsurmod` Dolby Surround flag — per the `acmod == 0x2` rule.
178    pub fn has_dolby_surround_mode(self) -> bool {
179        self.code() == 0x2
180    }
181}
182
183/// Bitstream mode (`bsmod`) — the type of audio service the frame
184/// carries. The `VoiceOverOrKaraoke` variant covers the `111` code whose
185/// meaning further depends on `acmod` (voice-over for `acmod = 001`,
186/// otherwise a main / karaoke service); the raw code is preserved for
187/// a caller that wants to disambiguate against `acmod`.
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189pub enum Ac3BitstreamMode {
190    /// `000` — main audio service: complete main (CM).
191    CompleteMain,
192    /// `001` — main audio service: music and effects (ME).
193    MusicAndEffects,
194    /// `010` — associated service: visually impaired (VI).
195    VisuallyImpaired,
196    /// `011` — associated service: hearing impaired (HI).
197    HearingImpaired,
198    /// `100` — associated service: dialogue (D).
199    Dialogue,
200    /// `101` — associated service: commentary (C).
201    Commentary,
202    /// `110` — associated service: emergency (E).
203    Emergency,
204    /// `111` — voice-over (when `acmod == 001`) or a main /
205    /// karaoke service (otherwise). Disambiguate via `acmod`.
206    VoiceOverOrKaraoke,
207}
208
209impl Ac3BitstreamMode {
210    fn from_code(code: u8) -> Self {
211        match code & 0b111 {
212            0 => Self::CompleteMain,
213            1 => Self::MusicAndEffects,
214            2 => Self::VisuallyImpaired,
215            3 => Self::HearingImpaired,
216            4 => Self::Dialogue,
217            5 => Self::Commentary,
218            6 => Self::Emergency,
219            _ => Self::VoiceOverOrKaraoke,
220        }
221    }
222}
223
224/// One row of the `frmsizecod` table: the nominal bit rate and the
225/// frame size, in 16-bit words, at each of the three sample rates.
226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
227struct FrmSizeRow {
228    /// Nominal bit rate in kbps.
229    bitrate_kbps: u16,
230    /// 16-bit words per sync frame at 32 kHz.
231    words_32k: u16,
232    /// 16-bit words per sync frame at 44.1 kHz.
233    words_44k: u16,
234    /// 16-bit words per sync frame at 48 kHz.
235    words_48k: u16,
236}
237
238/// The `frmsizecod` table from `stnsoft-ac3hdr.html`. The 6-bit code
239/// indexes this 38-entry table directly (`0b000000..=0b100101`);
240/// codes `0b100110..=0b111111` are reserved and absent from the
241/// table.
242const FRM_SIZE_TABLE: [FrmSizeRow; 38] = [
243    FrmSizeRow {
244        bitrate_kbps: 32,
245        words_32k: 96,
246        words_44k: 69,
247        words_48k: 64,
248    },
249    FrmSizeRow {
250        bitrate_kbps: 32,
251        words_32k: 96,
252        words_44k: 70,
253        words_48k: 64,
254    },
255    FrmSizeRow {
256        bitrate_kbps: 40,
257        words_32k: 120,
258        words_44k: 87,
259        words_48k: 80,
260    },
261    FrmSizeRow {
262        bitrate_kbps: 40,
263        words_32k: 120,
264        words_44k: 88,
265        words_48k: 80,
266    },
267    FrmSizeRow {
268        bitrate_kbps: 48,
269        words_32k: 144,
270        words_44k: 104,
271        words_48k: 96,
272    },
273    FrmSizeRow {
274        bitrate_kbps: 48,
275        words_32k: 144,
276        words_44k: 105,
277        words_48k: 96,
278    },
279    FrmSizeRow {
280        bitrate_kbps: 56,
281        words_32k: 168,
282        words_44k: 121,
283        words_48k: 112,
284    },
285    FrmSizeRow {
286        bitrate_kbps: 56,
287        words_32k: 168,
288        words_44k: 122,
289        words_48k: 112,
290    },
291    FrmSizeRow {
292        bitrate_kbps: 64,
293        words_32k: 192,
294        words_44k: 139,
295        words_48k: 128,
296    },
297    FrmSizeRow {
298        bitrate_kbps: 64,
299        words_32k: 192,
300        words_44k: 140,
301        words_48k: 128,
302    },
303    FrmSizeRow {
304        bitrate_kbps: 80,
305        words_32k: 240,
306        words_44k: 174,
307        words_48k: 160,
308    },
309    FrmSizeRow {
310        bitrate_kbps: 80,
311        words_32k: 240,
312        words_44k: 175,
313        words_48k: 160,
314    },
315    FrmSizeRow {
316        bitrate_kbps: 96,
317        words_32k: 288,
318        words_44k: 208,
319        words_48k: 192,
320    },
321    FrmSizeRow {
322        bitrate_kbps: 96,
323        words_32k: 288,
324        words_44k: 209,
325        words_48k: 192,
326    },
327    FrmSizeRow {
328        bitrate_kbps: 112,
329        words_32k: 336,
330        words_44k: 243,
331        words_48k: 224,
332    },
333    FrmSizeRow {
334        bitrate_kbps: 112,
335        words_32k: 336,
336        words_44k: 244,
337        words_48k: 224,
338    },
339    FrmSizeRow {
340        bitrate_kbps: 128,
341        words_32k: 384,
342        words_44k: 278,
343        words_48k: 256,
344    },
345    FrmSizeRow {
346        bitrate_kbps: 128,
347        words_32k: 384,
348        words_44k: 279,
349        words_48k: 256,
350    },
351    FrmSizeRow {
352        bitrate_kbps: 160,
353        words_32k: 480,
354        words_44k: 348,
355        words_48k: 320,
356    },
357    FrmSizeRow {
358        bitrate_kbps: 160,
359        words_32k: 480,
360        words_44k: 349,
361        words_48k: 320,
362    },
363    FrmSizeRow {
364        bitrate_kbps: 192,
365        words_32k: 576,
366        words_44k: 417,
367        words_48k: 384,
368    },
369    FrmSizeRow {
370        bitrate_kbps: 192,
371        words_32k: 576,
372        words_44k: 418,
373        words_48k: 384,
374    },
375    FrmSizeRow {
376        bitrate_kbps: 224,
377        words_32k: 672,
378        words_44k: 487,
379        words_48k: 448,
380    },
381    FrmSizeRow {
382        bitrate_kbps: 224,
383        words_32k: 672,
384        words_44k: 488,
385        words_48k: 448,
386    },
387    FrmSizeRow {
388        bitrate_kbps: 256,
389        words_32k: 768,
390        words_44k: 557,
391        words_48k: 512,
392    },
393    FrmSizeRow {
394        bitrate_kbps: 256,
395        words_32k: 768,
396        words_44k: 558,
397        words_48k: 512,
398    },
399    FrmSizeRow {
400        bitrate_kbps: 320,
401        words_32k: 960,
402        words_44k: 696,
403        words_48k: 640,
404    },
405    FrmSizeRow {
406        bitrate_kbps: 320,
407        words_32k: 960,
408        words_44k: 697,
409        words_48k: 640,
410    },
411    FrmSizeRow {
412        bitrate_kbps: 384,
413        words_32k: 1152,
414        words_44k: 835,
415        words_48k: 768,
416    },
417    FrmSizeRow {
418        bitrate_kbps: 384,
419        words_32k: 1152,
420        words_44k: 836,
421        words_48k: 768,
422    },
423    FrmSizeRow {
424        bitrate_kbps: 448,
425        words_32k: 1344,
426        words_44k: 975,
427        words_48k: 896,
428    },
429    FrmSizeRow {
430        bitrate_kbps: 448,
431        words_32k: 1344,
432        words_44k: 976,
433        words_48k: 896,
434    },
435    FrmSizeRow {
436        bitrate_kbps: 512,
437        words_32k: 1536,
438        words_44k: 1114,
439        words_48k: 1024,
440    },
441    FrmSizeRow {
442        bitrate_kbps: 512,
443        words_32k: 1536,
444        words_44k: 1115,
445        words_48k: 1024,
446    },
447    FrmSizeRow {
448        bitrate_kbps: 576,
449        words_32k: 1728,
450        words_44k: 1253,
451        words_48k: 1152,
452    },
453    FrmSizeRow {
454        bitrate_kbps: 576,
455        words_32k: 1728,
456        words_44k: 1254,
457        words_48k: 1152,
458    },
459    FrmSizeRow {
460        bitrate_kbps: 640,
461        words_32k: 1920,
462        words_44k: 1393,
463        words_48k: 1280,
464    },
465    FrmSizeRow {
466        bitrate_kbps: 640,
467        words_32k: 1920,
468        words_44k: 1394,
469        words_48k: 1280,
470    },
471];
472
473/// Decoded AC-3 sync-frame header (`syncinfo()` + the fixed-position
474/// prefix of `bsi()`).
475///
476/// Field layout from `stnsoft-ac3hdr.html`:
477///
478/// ```text
479/// syncinfo()
480///   syncword   16  0x0B77
481///   crc1       16  CRC of the first 5/8 of the frame
482///   fscod       2  sampling-rate code
483///   frmsizecod  6  frame-size code
484/// bsi()  (fixed-position prefix)
485///   bsid        5  bitstream identification (8 in this version)
486///   bsmod       3  bitstream mode
487///   acmod       3  audio coding mode
488///   [cmixlev    2] if acmod has a centre channel
489///   [surmixlev  2] if acmod has a surround channel
490///   [dsurmod    2] if acmod == 2/0
491///   lfeon       1  LFE channel present
492/// ```
493#[derive(Debug, Clone, Copy, PartialEq, Eq)]
494pub struct Ac3Header {
495    /// 16-bit CRC over the first 5/8 of the sync frame (carried, not
496    /// verified — the decoder is header-only).
497    pub crc1: u16,
498    /// Decoded sampling rate (`fscod`).
499    pub sample_rate: Ac3SampleRate,
500    /// Raw 6-bit `frmsizecod` frame-size code.
501    pub frame_size_code: u8,
502    /// 5-bit `bsid` bitstream identification (8 for the standard
503    /// A/52 stream, 6 for the alternate A/52a layout).
504    pub bsid: u8,
505    /// Decoded bitstream mode (`bsmod`).
506    pub bitstream_mode: Ac3BitstreamMode,
507    /// Decoded audio coding mode (`acmod`).
508    pub audio_coding_mode: Ac3AudioCodingMode,
509    /// 2-bit `cmixlev` centre-mix level, present only when
510    /// [`Ac3AudioCodingMode::has_center_mix_level`].
511    pub center_mix_level: Option<u8>,
512    /// 2-bit `surmixlev` surround-mix level, present only when
513    /// [`Ac3AudioCodingMode::has_surround_mix_level`].
514    pub surround_mix_level: Option<u8>,
515    /// 2-bit `dsurmod` Dolby Surround mode, present only for the
516    /// `2/0` mode (`acmod == 2`).
517    pub dolby_surround_mode: Option<u8>,
518    /// `lfeon` — `true` when a low-frequency-effects channel is
519    /// present.
520    pub lfe_on: bool,
521}
522
523impl Ac3Header {
524    /// Decode an AC-3 sync-frame header from the start of `frame`
525    /// (the first byte of an AC-3 elementary stream routed from
526    /// [`crate::vob::DvdSubstream::Ac3`]).
527    ///
528    /// Returns [`Error::InvalidUdf`] when the buffer is too short to
529    /// reach `lfeon` or when the leading two bytes are not the
530    /// `0x0B77` sync word.
531    pub fn parse(frame: &[u8]) -> Result<Self> {
532        // syncinfo() is 5 bytes; bsi() up to lfeon adds at most 5
533        // more bits past byte 5's bit boundary, so 7 bytes always
534        // covers the deterministic prefix regardless of acmod.
535        if frame.len() < 7 {
536            return Err(Error::InvalidUdf("AC-3 sync frame truncated (< 7 bytes)"));
537        }
538        let syncword = u16::from_be_bytes([frame[0], frame[1]]);
539        if syncword != AC3_SYNC_WORD {
540            return Err(Error::InvalidUdf(
541                "AC-3 sync frame: sync word is not 0x0B77",
542            ));
543        }
544        let crc1 = u16::from_be_bytes([frame[2], frame[3]]);
545
546        let byte4 = frame[4];
547        let sample_rate = Ac3SampleRate::from_code(byte4 >> 6);
548        let frame_size_code = byte4 & 0b0011_1111;
549
550        // bsi() begins at byte 5, MSB-first. A small bit cursor walks
551        // the deterministic prefix (bsid .. lfeon).
552        let mut bits = BitReader::new(&frame[5..]);
553        let bsid = bits.read(5);
554        let bitstream_mode = Ac3BitstreamMode::from_code(bits.read(3));
555        let audio_coding_mode = Ac3AudioCodingMode::from_code(bits.read(3));
556
557        let center_mix_level = if audio_coding_mode.has_center_mix_level() {
558            Some(bits.read(2))
559        } else {
560            None
561        };
562        let surround_mix_level = if audio_coding_mode.has_surround_mix_level() {
563            Some(bits.read(2))
564        } else {
565            None
566        };
567        let dolby_surround_mode = if audio_coding_mode.has_dolby_surround_mode() {
568            Some(bits.read(2))
569        } else {
570            None
571        };
572        let lfe_on = bits.read(1) != 0;
573
574        Ok(Self {
575            crc1,
576            sample_rate,
577            frame_size_code,
578            bsid,
579            bitstream_mode,
580            audio_coding_mode,
581            center_mix_level,
582            surround_mix_level,
583            dolby_surround_mode,
584            lfe_on,
585        })
586    }
587
588    /// Sample rate in Hz for the three defined `fscod` codes; `None`
589    /// when `fscod == 11` (reserved).
590    pub fn sample_rate_hz(self) -> Option<u32> {
591        self.sample_rate.hz()
592    }
593
594    /// Nominal bit rate in kbps from the `frmsizecod` table; `None`
595    /// for the reserved codes (`>= 0b100110`).
596    pub fn nominal_bitrate_kbps(self) -> Option<u16> {
597        FRM_SIZE_TABLE
598            .get(self.frame_size_code as usize)
599            .map(|r| r.bitrate_kbps)
600    }
601
602    /// Sync-frame size in 16-bit words from the `frmsizecod` table,
603    /// selected by the decoded sample rate. `None` for a reserved
604    /// `frmsizecod` or a reserved `fscod`.
605    pub fn frame_size_words(self) -> Option<u16> {
606        let row = FRM_SIZE_TABLE.get(self.frame_size_code as usize)?;
607        match self.sample_rate {
608            Ac3SampleRate::Hz32000 => Some(row.words_32k),
609            Ac3SampleRate::Hz44100 => Some(row.words_44k),
610            Ac3SampleRate::Hz48000 => Some(row.words_48k),
611            Ac3SampleRate::Reserved => None,
612        }
613    }
614
615    /// Sync-frame size in bytes (`frame_size_words × 2`); `None` when
616    /// [`Self::frame_size_words`] is `None`.
617    pub fn frame_size_bytes(self) -> Option<u32> {
618        self.frame_size_words().map(|w| w as u32 * 2)
619    }
620
621    /// Total channel count including the LFE channel when present
622    /// (`nfchans + lfeon`).
623    pub fn total_channel_count(self) -> u8 {
624        self.audio_coding_mode.channel_count() + u8::from(self.lfe_on)
625    }
626}
627
628/// A minimal MSB-first bit reader over a byte slice. Reads past the
629/// end of the buffer yield zero bits — the [`Ac3Header::parse`] caller
630/// has already guaranteed at least 7 bytes, which covers every
631/// deterministic-prefix path, so the saturating behaviour never
632/// fabricates a meaningful field here.
633struct BitReader<'a> {
634    data: &'a [u8],
635    bit_pos: usize,
636}
637
638impl<'a> BitReader<'a> {
639    fn new(data: &'a [u8]) -> Self {
640        Self { data, bit_pos: 0 }
641    }
642
643    /// Read `n` (≤ 8) bits MSB-first and return them right-aligned.
644    fn read(&mut self, n: usize) -> u8 {
645        let mut out = 0u8;
646        for _ in 0..n {
647            let byte_idx = self.bit_pos >> 3;
648            let bit_idx = 7 - (self.bit_pos & 7);
649            let bit = self
650                .data
651                .get(byte_idx)
652                .map(|b| (b >> bit_idx) & 1)
653                .unwrap_or(0);
654            out = (out << 1) | bit;
655            self.bit_pos += 1;
656        }
657        out
658    }
659}
660
661#[cfg(test)]
662mod tests {
663    use super::*;
664
665    /// Build a sync frame whose `syncinfo()` declares 48 kHz, the
666    /// given `frmsizecod`, and a `bsi()` prefix of `bsid=8`,
667    /// `bsmod=0`, the given `acmod`, no conditional mix fields beyond
668    /// what `acmod` forces, and `lfeon` from `lfe`.
669    fn build_frame(
670        fscod: u8,
671        frmsizecod: u8,
672        bsid: u8,
673        bsmod: u8,
674        acmod: u8,
675        lfe: bool,
676    ) -> Vec<u8> {
677        let mut f = vec![0x0B, 0x77, 0x12, 0x34];
678        f.push((fscod << 6) | (frmsizecod & 0b0011_1111));
679
680        // Assemble bsi() prefix MSB-first into a bit buffer.
681        let mut bitbuf: Vec<bool> = Vec::new();
682        let push = |bb: &mut Vec<bool>, val: u8, n: usize| {
683            for i in (0..n).rev() {
684                bb.push((val >> i) & 1 != 0);
685            }
686        };
687        push(&mut bitbuf, bsid, 5);
688        push(&mut bitbuf, bsmod, 3);
689        push(&mut bitbuf, acmod, 3);
690        let acm = Ac3AudioCodingMode::from_code(acmod);
691        if acm.has_center_mix_level() {
692            push(&mut bitbuf, 0b01, 2);
693        }
694        if acm.has_surround_mix_level() {
695            push(&mut bitbuf, 0b10, 2);
696        }
697        if acm.has_dolby_surround_mode() {
698            push(&mut bitbuf, 0b10, 2);
699        }
700        bitbuf.push(lfe);
701        // Pack the bit buffer into bytes (MSB-first), padding the tail.
702        while bitbuf.len() % 8 != 0 {
703            bitbuf.push(false);
704        }
705        for chunk in bitbuf.chunks(8) {
706            let mut b = 0u8;
707            for (i, &bit) in chunk.iter().enumerate() {
708                if bit {
709                    b |= 1 << (7 - i);
710                }
711            }
712            f.push(b);
713        }
714        // Ensure at least 7 bytes total.
715        while f.len() < 7 {
716            f.push(0);
717        }
718        f
719    }
720
721    #[test]
722    fn parse_stereo_48k() {
723        // 48 kHz, frmsizecod=0 (32 kbps), bsid=8, bsmod=0, acmod=2 (2/0), no LFE.
724        let f = build_frame(0, 0, 8, 0, 2, false);
725        let h = Ac3Header::parse(&f).unwrap();
726        assert_eq!(h.crc1, 0x1234);
727        assert_eq!(h.sample_rate, Ac3SampleRate::Hz48000);
728        assert_eq!(h.sample_rate_hz(), Some(48_000));
729        assert_eq!(h.frame_size_code, 0);
730        assert_eq!(h.bsid, 8);
731        assert_eq!(h.bitstream_mode, Ac3BitstreamMode::CompleteMain);
732        assert_eq!(h.audio_coding_mode, Ac3AudioCodingMode::Stereo);
733        // 2/0 has dsurmod but no cmixlev / surmixlev.
734        assert_eq!(h.center_mix_level, None);
735        assert_eq!(h.surround_mix_level, None);
736        assert_eq!(h.dolby_surround_mode, Some(0b10));
737        assert!(!h.lfe_on);
738        assert_eq!(h.total_channel_count(), 2);
739        assert_eq!(h.nominal_bitrate_kbps(), Some(32));
740        assert_eq!(h.frame_size_words(), Some(64));
741        assert_eq!(h.frame_size_bytes(), Some(128));
742    }
743
744    #[test]
745    fn parse_five_one_48k() {
746        // acmod=7 (3/2) + LFE → 5.1. frmsizecod=24 → 256 kbps.
747        let f = build_frame(0, 24, 8, 0, 7, true);
748        let h = Ac3Header::parse(&f).unwrap();
749        assert_eq!(h.audio_coding_mode, Ac3AudioCodingMode::ThreeTwo);
750        // 3/2 carries both cmixlev and surmixlev, no dsurmod.
751        assert_eq!(h.center_mix_level, Some(0b01));
752        assert_eq!(h.surround_mix_level, Some(0b10));
753        assert_eq!(h.dolby_surround_mode, None);
754        assert!(h.lfe_on);
755        assert_eq!(h.audio_coding_mode.channel_count(), 5);
756        assert_eq!(h.total_channel_count(), 6);
757        assert_eq!(h.nominal_bitrate_kbps(), Some(256));
758        assert_eq!(h.frame_size_words(), Some(512));
759    }
760
761    #[test]
762    fn frmsizecod_table_sample_rate_columns() {
763        // frmsizecod=26 (320 kbps) at each sample rate.
764        let at = |fscod: u8| Ac3Header::parse(&build_frame(fscod, 26, 8, 0, 2, false)).unwrap();
765        let h48 = at(0);
766        assert_eq!(h48.sample_rate_hz(), Some(48_000));
767        assert_eq!(h48.frame_size_words(), Some(640));
768        let h44 = at(1);
769        assert_eq!(h44.sample_rate_hz(), Some(44_100));
770        assert_eq!(h44.frame_size_words(), Some(696));
771        let h32 = at(2);
772        assert_eq!(h32.sample_rate_hz(), Some(32_000));
773        assert_eq!(h32.frame_size_words(), Some(960));
774    }
775
776    #[test]
777    fn reserved_fscod_yields_none() {
778        let h = Ac3Header::parse(&build_frame(3, 0, 8, 0, 2, false)).unwrap();
779        assert_eq!(h.sample_rate, Ac3SampleRate::Reserved);
780        assert_eq!(h.sample_rate_hz(), None);
781        // frame_size_words needs a defined sample rate.
782        assert_eq!(h.frame_size_words(), None);
783        // but the nominal bitrate is sample-rate-independent.
784        assert_eq!(h.nominal_bitrate_kbps(), Some(32));
785    }
786
787    #[test]
788    fn reserved_frmsizecod_yields_none() {
789        // frmsizecod=0b100110 (38) is the first reserved code.
790        let h = Ac3Header::parse(&build_frame(0, 38, 8, 0, 2, false)).unwrap();
791        assert_eq!(h.frame_size_code, 38);
792        assert_eq!(h.nominal_bitrate_kbps(), None);
793        assert_eq!(h.frame_size_words(), None);
794        assert_eq!(h.frame_size_bytes(), None);
795    }
796
797    #[test]
798    fn all_acmod_channel_counts() {
799        let expected = [2u8, 1, 2, 3, 3, 4, 4, 5];
800        for (code, &n) in expected.iter().enumerate() {
801            assert_eq!(Ac3AudioCodingMode::from_code(code as u8).channel_count(), n);
802        }
803    }
804
805    #[test]
806    fn conditional_field_presence_by_acmod() {
807        // Centre present for 3/0(3), 3/1(5), 3/2(7).
808        for code in [3u8, 5, 7] {
809            assert!(Ac3AudioCodingMode::from_code(code).has_center_mix_level());
810        }
811        for code in [0u8, 1, 2, 4, 6] {
812            assert!(!Ac3AudioCodingMode::from_code(code).has_center_mix_level());
813        }
814        // Surround present for 2/1(4), 3/1(5), 2/2(6), 3/2(7).
815        for code in [4u8, 5, 6, 7] {
816            assert!(Ac3AudioCodingMode::from_code(code).has_surround_mix_level());
817        }
818        for code in [0u8, 1, 2, 3] {
819            assert!(!Ac3AudioCodingMode::from_code(code).has_surround_mix_level());
820        }
821        // dsurmod only for 2/0(2).
822        assert!(Ac3AudioCodingMode::from_code(2).has_dolby_surround_mode());
823        for code in [0u8, 1, 3, 4, 5, 6, 7] {
824            assert!(!Ac3AudioCodingMode::from_code(code).has_dolby_surround_mode());
825        }
826    }
827
828    #[test]
829    fn bitstream_mode_table() {
830        let modes = [
831            Ac3BitstreamMode::CompleteMain,
832            Ac3BitstreamMode::MusicAndEffects,
833            Ac3BitstreamMode::VisuallyImpaired,
834            Ac3BitstreamMode::HearingImpaired,
835            Ac3BitstreamMode::Dialogue,
836            Ac3BitstreamMode::Commentary,
837            Ac3BitstreamMode::Emergency,
838            Ac3BitstreamMode::VoiceOverOrKaraoke,
839        ];
840        for (code, &m) in modes.iter().enumerate() {
841            let h = Ac3Header::parse(&build_frame(0, 0, 8, code as u8, 2, false)).unwrap();
842            assert_eq!(h.bitstream_mode, m);
843        }
844    }
845
846    #[test]
847    fn rejects_bad_syncword() {
848        let mut f = build_frame(0, 0, 8, 0, 2, false);
849        f[0] = 0x00;
850        let err = Ac3Header::parse(&f).unwrap_err();
851        assert!(matches!(err, Error::InvalidUdf(_)));
852    }
853
854    #[test]
855    fn rejects_short_buffer() {
856        let err = Ac3Header::parse(&[0x0B, 0x77, 0, 0, 0, 0]).unwrap_err();
857        assert!(matches!(err, Error::InvalidUdf(_)));
858    }
859
860    #[test]
861    fn frmsizecod_table_is_complete() {
862        // 38 defined codes, indices 0..=37.
863        assert_eq!(FRM_SIZE_TABLE.len(), 38);
864        // First and last bit rates per the table.
865        assert_eq!(FRM_SIZE_TABLE[0].bitrate_kbps, 32);
866        assert_eq!(FRM_SIZE_TABLE[37].bitrate_kbps, 640);
867    }
868}