Skip to main content

container/
aac_asc.rs

1//! AudioSpecificConfig (ASC) parser for AAC family streams
2//! (Squad-25, HE-AAC + multichannel AAC passthrough).
3//!
4//! Reference: ISO/IEC 14496-3 §1.6.2 — `AudioSpecificConfig()` syntax,
5//! and §1.6.5 — backward-compatible explicit signaling for SBR / PS.
6//!
7//! ## Why this exists separately from `decode_asc_*`
8//!
9//! `demux.rs` already has tiny `decode_asc_sample_rate` /
10//! `decode_asc_channels` helpers that handle the **core** ASC layer only.
11//! That was sufficient for AAC-LC stereo passthrough (Squad-18) but loses
12//! the SBR / PS extension layer that distinguishes:
13//!   - AAC-LC (AOT=2)
14//!   - HE-AAC v1 (AOT=2 core wrapped by AOT=5 SBR extension)
15//!   - HE-AAC v2 (AOT=2 core wrapped by AOT=29 PS extension carrying AOT=5 SBR)
16//!
17//! and the **implicit** vs **explicit** signaling forms (ISO 14496-3 §1.6.5):
18//! - Implicit: the ASC contains only `audioObjectType=2` (LC). The actual
19//!   stream may still carry SBR/PS payloads in the AAC bitstream, but the
20//!   ASC does not advertise them. **Apple Core Audio (and AVFoundation)
21//!   silently downgrade implicit-signaled HE-AAC to mono 22.05 kHz core**
22//!   — playback is technically correct against the LC layer but loses the
23//!   stereo upmix and the high-frequency band, so listeners hear quiet,
24//!   muffled audio.
25//! - Explicit: the ASC starts with `audioObjectType=5` (SBR), then carries
26//!   the `extensionSamplingFrequencyIndex` followed by an inner
27//!   `audioObjectType=2` for the LC core. This is the form Apple players
28//!   require to honour the full HE-AAC output.
29//!
30//! ## What this module exports
31//!
32//! - `parse_aac_asc` — parse a 2..16-byte ASC, return a structured
33//!   `ParsedAsc { aot, sample_rate, channels, sbr_present, ps_present,
34//!    sbr_sample_rate, signaling }`. Pure; no allocations beyond the
35//!   returned struct.
36//! - `effective_output_channels` — apply the PS upmix rule (HE-AAC v2
37//!   PS: 1-channel core → 2-channel output) so the demuxer can surface
38//!   the post-decoder channel count rather than the pre-decoder one.
39//! - `upgrade_to_explicit_signaling` — rewrite an implicit-signaled
40//!   HE-AAC ASC (`AOT=2 + sfi + chan + ...`) into the explicit form
41//!   (`AOT=5 + sbr_sfi + AOT=2 + ...`) so Apple players honour the
42//!   SBR / PS extension. Returns `None` for ASCs that are already
43//!   explicit, ASCs that aren't HE-AAC eligible (e.g. AAC-LC > 24 kHz
44//!   has no SBR upgrade because SBR doubles the rate to >48 kHz,
45//!   which the AAC profile already covers natively at 32/44.1/48 kHz),
46//!   or ASCs we can't safely rewrite.
47//!
48//! ## What this module does NOT do
49//!
50//! - It does not parse PCE (Programme Config Element). The synthetic
51//!   `channelConfiguration=0` pathway is handled by callers that fall
52//!   back to a sane default (`AudioInfo.channels=2`) — explicit PCE
53//!   support is out of scope for HE-AAC + 5.1/7.1 passthrough.
54//! - It does not parse the GASpecificConfig body beyond the
55//!   `frameLengthFlag`/`dependsOnCoreCoder`/`extensionFlag` field
56//!   prefix needed to find the SBR-extension trailer. Most fields
57//!   downstream of GASpecificConfig don't affect the SBR/PS detection.
58
59/// Sampling frequency table per ISO/IEC 14496-3 Table 1.16.
60/// Index 0xF means "24-bit explicit rate follows inline".
61pub const SFI_FREQS: [u32; 13] = [
62    96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350,
63];
64
65/// Per ISO 14496-3 §1.6.5: how the SBR / PS extension layer was signaled
66/// inside the ASC, if at all. Drives Apple-compat handling.
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum AscSignaling {
69    /// Pure AAC-LC (or other non-HE AOT). No extension layer signaled.
70    NoExtension,
71    /// AAC-LC core with the SBR / PS layer signaled implicitly — that is,
72    /// the ASC starts with `AOT=2` and **does not** advertise SBR or PS,
73    /// but the audio bitstream itself contains SBR / PS payloads. Apple
74    /// players silently downgrade to mono 22.05 kHz. Must be upgraded to
75    /// `Explicit*` before muxing for Apple-compat or rejected.
76    ImplicitMaybe,
77    /// SBR / PS layer signaled explicitly via the `AOT=5` (SBR) or
78    /// `AOT=29` (PS) leading byte. Apple-compatible.
79    ExplicitSbr,
80    /// PS extension explicitly signaled (`AOT=29` leading byte). PS implies
81    /// SBR (PS rides on top of SBR per ISO 14496-3 §6).
82    ExplicitPs,
83}
84
85/// Parsed AudioSpecificConfig fields.
86#[derive(Debug, Clone)]
87pub struct ParsedAsc {
88    /// Core `audioObjectType` (the LC profile in HE-AAC bitstreams).
89    /// Value range after escape decoding: 2 (AAC-LC), 5 (SBR — only if
90    /// the explicit-signaling form puts SBR up front), 29 (PS — same
91    /// caveat), 42 (xHE-AAC USAC), etc.
92    pub aot: u8,
93    /// Sampling rate of the AAC core in Hz. For HE-AAC this is the
94    /// half-rate core; the SBR-extended output rate (typically 2×) is
95    /// in `sbr_sample_rate` when present.
96    pub sample_rate: u32,
97    /// Channel configuration as advertised in the ASC's `channelConfiguration`
98    /// field per ISO 14496-3 Table 1.19. Values 0..7 map directly to channel
99    /// counts (1=mono, 2=stereo, 3=3.0, 4=4.0, 5=5.0, 6=5.1, 7=7.1; value 0
100    /// means "consult the PCE", which we don't parse here — caller's choice
101    /// to default).
102    pub channels: u16,
103    /// True when the ASC explicitly signals an SBR layer (AOT=5 leading
104    /// byte) or has the implicit form's heuristic backbone (always false
105    /// for `Implicit*` because we can only confirm SBR by parsing the
106    /// AAC bitstream, which lives in samples, not the ASC).
107    pub sbr_present: bool,
108    /// True when the ASC explicitly signals a PS layer (AOT=29 leading
109    /// byte). PS implies SBR.
110    pub ps_present: bool,
111    /// Output sample rate of the SBR-doubled stream when `sbr_present`.
112    /// For explicit signaling this comes from `extensionSamplingFrequencyIndex`;
113    /// it equals `2 × sample_rate` for the canonical HE-AAC profile.
114    pub sbr_sample_rate: Option<u32>,
115    /// How the SBR / PS layer was signaled. See [`AscSignaling`] for the
116    /// Apple-compat consequences.
117    pub signaling: AscSignaling,
118}
119
120/// Parse an AudioSpecificConfig per ISO/IEC 14496-3 §1.6.2.1.
121///
122/// On success returns a [`ParsedAsc`] describing the AAC family layer
123/// stack. Returns `None` for malformed / truncated input or for ASCs
124/// whose AOT we don't model (the caller should reject in that case).
125///
126/// Recognized AOTs:
127/// - 2 (AAC-LC) — most common; HE-AAC uses this as the core layer.
128/// - 5 (SBR) — used as the leading AOT in HE-AAC v1 explicit signaling.
129/// - 29 (PS) — used as the leading AOT in HE-AAC v2 explicit signaling.
130///
131/// Other AOTs are surfaced as-is in `aot` with `signaling=NoExtension`
132/// so the caller can decide whether to accept (e.g. xHE-AAC=42) or
133/// reject.
134///
135/// ASC bit layout for the relevant AOTs:
136///
137/// ```text
138/// AOT (5 bits, escape via 31+6) | SFI (4 bits, 0xF→24-bit inline) |
139/// channelConfiguration (4 bits) |
140///   if (AOT == 5 || AOT == 29) {
141///     extensionSamplingFrequencyIndex (4 bits, 0xF→24-bit) |
142///     inner_AOT (5 bits, escape) |
143///     [GASpecificConfig follows for core inner AOT]
144///   } else {
145///     [GASpecificConfig follows]
146///   }
147/// ```
148///
149/// Per ISO 14496-3 §1.6.5 / §4.5.1.1, when explicit signaling is used:
150/// - The **outer** `samplingFrequencyIndex` = the **SBR-output** rate
151///   (typically 2× the AAC core rate). E.g. 48000 for an HE-AAC v1
152///   stream whose core operates at 24000.
153/// - `extensionSamplingFrequencyIndex` = the **SBR rate** = the same
154///   value as the outer SFI per spec recommendation.
155/// - The inner core AAC operates at **half** the SBR rate.
156pub fn parse_aac_asc(asc: &[u8]) -> Option<ParsedAsc> {
157    // ASC needs at least 2 bytes to carry AOT (5b) + SFI (4b) + channelConfig (4b).
158    if asc.len() < 2 {
159        return None;
160    }
161    let mut br = BitReader::new(asc);
162    let leading_aot = read_aot(&mut br)?;
163    let leading_sfi = br.bits(4)? as usize;
164    let leading_sample_rate = decode_sfi(leading_sfi, &mut br)?;
165    let leading_chan_cfg = br.bits(4)? as u16;
166
167    // Explicit-signaling form: the leading AOT is SBR (5) or PS (29). The
168    // *outer* SFI is the SBR-output (extended) rate per ISO 14496-3 §1.6.5.
169    // Next we read `extensionSamplingFrequencyIndex` (typically the same
170    // value, expressed redundantly per spec) and then the inner core AOT
171    // (typically AOT=2 for LC).
172    if leading_aot == 5 || leading_aot == 29 {
173        let ext_sfi = br.bits(4)? as usize;
174        let _sbr_rate_redundant = decode_sfi(ext_sfi, &mut br)?;
175        let core_aot = read_aot(&mut br)?;
176        // Outer SFI is the SBR/output rate; core operates at half that.
177        let sbr_output_rate = leading_sample_rate;
178        let core_rate = sbr_output_rate / 2;
179        return Some(ParsedAsc {
180            aot: core_aot,
181            sample_rate: core_rate,
182            channels: leading_chan_cfg,
183            sbr_present: true,
184            ps_present: leading_aot == 29,
185            sbr_sample_rate: Some(sbr_output_rate),
186            signaling: if leading_aot == 29 {
187                AscSignaling::ExplicitPs
188            } else {
189                AscSignaling::ExplicitSbr
190            },
191        });
192    }
193
194    // Plain / core AOT path. AAC-LC (2) is the common case; xHE-AAC (42),
195    // ER-AAC-LC (17) etc. surface here too. We don't try to chase the
196    // GASpecificConfig tail to find a back-door SBR signal — that's the
197    // implicit form, and we mark it `ImplicitMaybe` ONLY for the AOT=2 +
198    // ≤24 kHz core combination that's the canonical HE-AAC implicit shape.
199    let signaling = if leading_aot == 2 && leading_sample_rate <= 24_000 {
200        AscSignaling::ImplicitMaybe
201    } else {
202        AscSignaling::NoExtension
203    };
204
205    Some(ParsedAsc {
206        aot: leading_aot,
207        sample_rate: leading_sample_rate,
208        channels: leading_chan_cfg,
209        sbr_present: false,
210        ps_present: false,
211        sbr_sample_rate: None,
212        signaling,
213    })
214}
215
216/// Effective decoded-output channel count for an HE-AAC family stream.
217/// Per ISO/IEC 14496-3 §6 ("Parametric Stereo"), a 1-channel core wrapped
218/// in PS upmixes to 2-channel output. SBR alone does not change the
219/// channel count.
220///
221/// This is the value the demuxer surfaces in `AudioInfo.channels`, NOT
222/// the raw `channelConfiguration` from the ASC. Players honour the
223/// effective count for buffer allocation and downstream renderers.
224pub fn effective_output_channels(parsed: &ParsedAsc) -> u16 {
225    let raw = parsed.channels;
226    // PS upmix: mono LC + PS → stereo output.
227    if parsed.ps_present && raw == 1 {
228        return 2;
229    }
230    // channelConfiguration=0 is "consult PCE" — we default to 2 (matches
231    // the existing demux fallback) so callers never see 0.
232    if raw == 0 { 2 } else { raw }
233}
234
235/// Rewrite an implicit-signaled HE-AAC ASC into explicit form. Returns
236/// `None` when no rewrite is needed or possible:
237/// - The ASC is already explicit (`AOT=5` or `AOT=29` leading byte).
238/// - The ASC isn't HE-AAC-eligible (core sample rate > 24 kHz, since SBR
239///   would push it past the canonical 48 kHz output rate).
240/// - The ASC is malformed.
241///
242/// On success returns the explicit-form ASC bytes. Layout produced per
243/// ISO 14496-3 §1.6.2.1 / §1.6.5:
244///
245///   `outerAOT=5 (5b) | outerSFI=SBR_rate (4b) | channelConfiguration (4b) |
246///    extensionSamplingFrequencyIndex=SBR_rate (4b) | innerAOT=2 (5b) |
247///    GASpecificConfig tail bits...`
248///
249/// Where `SBR_rate = 2 × original_core_rate` (per the canonical HE-AAC
250/// rate doubling). If `2×core` matches a known SFI we use the index; else
251/// we emit the 24-bit explicit rate (`sfi=0xF` + `samplingFrequency u24`).
252///
253/// The original ASC's `channelConfiguration` and post-channelConfiguration
254/// `GASpecificConfig` tail bits are copied verbatim (after re-shifting to
255/// the new positions in the bit stream).
256pub fn upgrade_to_explicit_signaling(asc: &[u8]) -> Option<Vec<u8>> {
257    let parsed = parse_aac_asc(asc)?;
258    if parsed.signaling != AscSignaling::ImplicitMaybe {
259        return None;
260    }
261    if parsed.aot != 2 {
262        return None;
263    }
264    // SBR doubles the sample rate; for AAC-LC ≤24 kHz this lands at ≤48 kHz,
265    // which is the supported HE-AAC profile range.
266    if parsed.sample_rate > 24_000 {
267        return None;
268    }
269    let sbr_rate = parsed.sample_rate * 2;
270    let sbr_sfi = sfi_for_rate(sbr_rate);
271
272    // We need the original ASC bit layout as raw bits so we can copy the
273    // post-channelConfiguration GASpecificConfig tail verbatim. Build a bit
274    // reader, skip the 5+4(+24)+4 prefix, and read the rest as a tail.
275    let mut br = BitReader::new(asc);
276    br.bits(5)?; // AOT=2
277    let leading_sfi = br.bits(4)? as usize;
278    if leading_sfi == 0xF {
279        br.bits(24)?; // skip inline 24-bit core rate (we re-derive)
280    }
281    br.bits(4)?; // channelConfiguration (we already have it in `parsed.channels`)
282
283    // Drain remaining bits into a tail-bit-buffer (the GASpecificConfig).
284    let tail_bits: Vec<u8> = drain_remaining_bits(&mut br);
285
286    // Build the explicit-form bitstream.
287    let mut bw = BitWriter::new();
288    bw.bits(5, 5); // outer AOT=5 (SBR)
289    match sbr_sfi {
290        Some(idx) => bw.bits(idx, 4), // outer SFI = SBR rate
291        None => {
292            bw.bits(0xF, 4); // 0xF → 24-bit inline
293            bw.bits(sbr_rate, 24);
294        }
295    }
296    bw.bits(parsed.channels as u32, 4); // channelConfiguration
297    // extensionSamplingFrequencyIndex = same SBR rate (per spec recommendation).
298    match sbr_sfi {
299        Some(idx) => bw.bits(idx, 4),
300        None => {
301            bw.bits(0xF, 4);
302            bw.bits(sbr_rate, 24);
303        }
304    }
305    bw.bits(2, 5); // inner AOT=2 (LC core)
306    for bit in &tail_bits {
307        bw.bits(*bit as u32, 1);
308    }
309    Some(bw.into_bytes())
310}
311
312/// Reverse-lookup an SFI table index for the given Hz rate. Returns `None`
313/// when no canonical index matches (caller falls back to the 0xF inline form).
314fn sfi_for_rate(rate: u32) -> Option<u32> {
315    SFI_FREQS.iter().position(|r| *r == rate).map(|p| p as u32)
316}
317
318/// Read the (possibly extended) audioObjectType field per ISO 14496-3 §1.6.2.1:
319///   if audioObjectType == 31:
320///       audioObjectType = 32 + audioObjectTypeExt (6 bits)
321fn read_aot(br: &mut BitReader<'_>) -> Option<u8> {
322    let raw = br.bits(5)? as u8;
323    if raw == 31 {
324        let ext = br.bits(6)? as u8;
325        Some(32 + ext)
326    } else {
327        Some(raw)
328    }
329}
330
331/// Decode a samplingFrequencyIndex into the corresponding rate. Index 0xF
332/// triggers a 24-bit inline rate field per ISO 14496-3 Table 1.16.
333fn decode_sfi(sfi: usize, br: &mut BitReader<'_>) -> Option<u32> {
334    if sfi == 0xF {
335        let r = br.bits(24)? as u32;
336        if r == 0 { None } else { Some(r) }
337    } else {
338        SFI_FREQS.get(sfi).copied()
339    }
340}
341
342/// Read remaining bits from the bit-reader as a flat Vec<u8> of {0,1}.
343fn drain_remaining_bits(br: &mut BitReader<'_>) -> Vec<u8> {
344    let mut out = Vec::new();
345    while let Some(b) = br.bits(1) {
346        out.push(b as u8);
347    }
348    out
349}
350
351/// MSB-first bit reader over a byte slice. Mirrors the AscBitReader in
352/// `demux.rs` but stays in this module so we don't tangle the parser
353/// with the rest of the demux state.
354struct BitReader<'a> {
355    data: &'a [u8],
356    pos: usize,
357}
358
359impl<'a> BitReader<'a> {
360    fn new(data: &'a [u8]) -> Self {
361        Self { data, pos: 0 }
362    }
363
364    fn bits(&mut self, n: u32) -> Option<u64> {
365        let mut v: u64 = 0;
366        for _ in 0..n {
367            let byte = *self.data.get(self.pos / 8)?;
368            let bit = (byte >> (7 - (self.pos % 8))) & 1;
369            v = (v << 1) | bit as u64;
370            self.pos += 1;
371        }
372        Some(v)
373    }
374}
375
376/// MSB-first bit writer, byte-padded with zeros at the end.
377struct BitWriter {
378    buf: Vec<u8>,
379    bit_pos: u32,
380}
381
382impl BitWriter {
383    fn new() -> Self {
384        Self {
385            buf: Vec::new(),
386            bit_pos: 0,
387        }
388    }
389
390    fn bits(&mut self, value: u32, n: u32) {
391        for i in (0..n).rev() {
392            let bit = ((value >> i) & 1) as u8;
393            if self.bit_pos.is_multiple_of(8) {
394                self.buf.push(0);
395            }
396            let byte_idx = (self.bit_pos / 8) as usize;
397            let bit_offset = 7 - (self.bit_pos % 8) as u8;
398            self.buf[byte_idx] |= bit << bit_offset;
399            self.bit_pos += 1;
400        }
401    }
402
403    fn into_bytes(self) -> Vec<u8> {
404        self.buf
405    }
406}
407
408#[cfg(test)]
409mod tests {
410    use super::*;
411
412    /// AAC-LC stereo @ 48 kHz: AOT=2, SFI=3 (48000), chan=2.
413    /// 00010 0011 0010 000 = 0001 0001 1001 0000 = 0x11 0x90.
414    #[test]
415    fn parse_aac_lc_stereo_48k() {
416        let asc = vec![0x11, 0x90];
417        let p = parse_aac_asc(&asc).expect("parse should succeed");
418        assert_eq!(p.aot, 2, "AOT");
419        assert_eq!(p.sample_rate, 48_000, "sample_rate");
420        assert_eq!(p.channels, 2, "channels");
421        assert!(!p.sbr_present);
422        assert!(!p.ps_present);
423        assert_eq!(p.signaling, AscSignaling::NoExtension);
424        assert_eq!(effective_output_channels(&p), 2);
425    }
426
427    /// AAC-LC stereo @ 44.1 kHz: AOT=2, SFI=4, chan=2.
428    /// 00010 0100 0010 000 = 0001 0010 0001 0000 = 0x12 0x10.
429    #[test]
430    fn parse_aac_lc_stereo_44_1k() {
431        let asc = vec![0x12, 0x10];
432        let p = parse_aac_asc(&asc).expect("parse should succeed");
433        assert_eq!(p.aot, 2);
434        assert_eq!(p.sample_rate, 44_100);
435        assert_eq!(p.channels, 2);
436        assert_eq!(p.signaling, AscSignaling::NoExtension);
437    }
438
439    /// HE-AAC v1 5.1 explicit signaling at 48 kHz output (24 kHz LC core).
440    /// Per ISO 14496-3 §1.6.5 the *outer* SFI = SBR-output rate (48000),
441    /// `extensionSamplingFrequencyIndex` = same value, and the inner core
442    /// AAC operates at half (24000).
443    ///   AOT=5 (00101)
444    ///   outer SFI = 3 → 48000 (0011)
445    ///   channelConfiguration = 6 → 5.1 (0110)
446    ///   extensionSamplingFrequencyIndex = 3 → 48000 (0011)
447    ///   inner AOT = 2 → LC (00010)
448    /// Bits: 00101 0011 0110 0011 00010 = 22 bits.
449    /// 00101001 10110001 10001000 = 0x29 0xB1 0x88.
450    #[test]
451    fn parse_he_aac_v1_5_1_explicit() {
452        let asc = vec![0x29, 0xB1, 0x88];
453        let p = parse_aac_asc(&asc).expect("parse should succeed");
454        // The reported `aot` is the inner core (LC=2). The leading AOT=5
455        // disappears into `signaling=ExplicitSbr` + `sbr_present`.
456        assert_eq!(p.aot, 2, "core aot should be LC");
457        assert_eq!(p.sample_rate, 24_000, "LC core rate (half of SBR)");
458        assert_eq!(p.channels, 6, "5.1 channel config");
459        assert!(p.sbr_present, "SBR layer must be present");
460        assert!(!p.ps_present, "PS not present in v1");
461        assert_eq!(p.sbr_sample_rate, Some(48_000), "SBR output rate");
462        assert_eq!(p.signaling, AscSignaling::ExplicitSbr);
463        assert_eq!(effective_output_channels(&p), 6);
464    }
465
466    /// HE-AAC v2 mono PS explicit signaling at 44.1 kHz output (22.05 kHz LC core):
467    ///   AOT=29 (11101)
468    ///   outer SFI = 4 → 44100 (0100)  ← SBR-output rate
469    ///   channelConfiguration = 1 → mono (0001)
470    ///   extensionSamplingFrequencyIndex = 4 → 44100 (0100)
471    ///   inner AOT = 2 → LC (00010)
472    /// Bits: 11101 0100 0001 0100 00010 = 22 bits.
473    /// 11101010 00001010 00001000 = 0xEA 0x0A 0x08.
474    #[test]
475    fn parse_he_aac_v2_mono_ps_explicit() {
476        let asc = vec![0xEA, 0x0A, 0x08];
477        let p = parse_aac_asc(&asc).expect("parse should succeed");
478        assert_eq!(p.aot, 2, "core aot is LC");
479        assert_eq!(p.sample_rate, 22_050, "core rate (half of SBR)");
480        assert_eq!(p.channels, 1, "mono channel config (PS upmixes at decode)");
481        assert!(p.sbr_present, "PS implies SBR");
482        assert!(p.ps_present, "PS layer signaled");
483        assert_eq!(p.sbr_sample_rate, Some(44_100));
484        assert_eq!(p.signaling, AscSignaling::ExplicitPs);
485        // PS upmix: 1-channel core → 2-channel effective output.
486        assert_eq!(effective_output_channels(&p), 2);
487    }
488
489    /// HE-AAC implicit signaling: a plain AAC-LC ASC at low core rate
490    /// (≤24 kHz) is the canonical implicit-HE shape. parse_aac_asc must
491    /// flag `ImplicitMaybe` so the caller can decide to upgrade or reject.
492    ///   AOT=2, SFI=6 (24000), chan=1 → 00010 0110 0001 000
493    ///   = 0001 0011 0000 1000 = 0x13 0x08.
494    #[test]
495    fn parse_implicit_he_aac_flagged() {
496        let asc = vec![0x13, 0x08];
497        let p = parse_aac_asc(&asc).expect("parse");
498        assert_eq!(p.aot, 2);
499        assert_eq!(p.sample_rate, 24_000);
500        assert_eq!(
501            p.signaling,
502            AscSignaling::ImplicitMaybe,
503            "low-rate AAC-LC must be flagged as implicit-HE candidate"
504        );
505    }
506
507    /// upgrade_to_explicit_signaling rewrites a 24 kHz mono AAC-LC ASC into
508    /// the explicit HE-AAC v1 form: leading AOT=5, outer SFI=SBR rate (48 kHz),
509    /// then channelConfig + extSFI + inner AOT=2.
510    #[test]
511    fn upgrade_24k_mono_lc_to_explicit_he_aac_v1() {
512        let asc = vec![0x13, 0x08]; // AOT=2 SFI=6 (24000) chan=1
513        let upgraded =
514            upgrade_to_explicit_signaling(&asc).expect("upgrade should succeed for ≤24 kHz LC");
515        let reparsed = parse_aac_asc(&upgraded).expect("upgraded ASC parses");
516        assert_eq!(
517            reparsed.signaling,
518            AscSignaling::ExplicitSbr,
519            "upgraded ASC must be explicit-SBR"
520        );
521        // After upgrade: outer SFI = 48000 (SBR), inner core = 24000 (half).
522        assert_eq!(reparsed.sample_rate, 24_000, "core rate is half of SBR");
523        assert_eq!(
524            reparsed.sbr_sample_rate,
525            Some(48_000),
526            "SBR rate is 2× core"
527        );
528        assert_eq!(reparsed.channels, 1);
529    }
530
531    /// upgrade_to_explicit_signaling refuses to upgrade ASCs whose core
532    /// rate is too high to plausibly be HE-AAC (>24 kHz core would push
533    /// SBR output > 48 kHz, off-spec for the canonical HE profile).
534    #[test]
535    fn upgrade_rejects_high_rate_lc() {
536        let asc = vec![0x11, 0x90]; // AOT=2 SFI=3 (48000) chan=2
537        // 48 kHz core → 96 kHz SBR. Off-spec; refuse the upgrade.
538        // (Actually our gate is "core rate <= 24 kHz" so 48 kHz is rejected
539        // because signaling is NoExtension, not because of the rate gate;
540        // either way the function must return None.)
541        assert!(upgrade_to_explicit_signaling(&asc).is_none());
542    }
543
544    /// upgrade_to_explicit_signaling refuses to re-upgrade an already-explicit ASC.
545    #[test]
546    fn upgrade_rejects_already_explicit() {
547        let asc = vec![0x29, 0x89, 0x98]; // HE-AAC v1 5.1 explicit
548        assert!(upgrade_to_explicit_signaling(&asc).is_none());
549    }
550
551    /// AOT escape: AOT=31 → 6-bit extension. AOT=42 (xHE-AAC USAC) is the
552    /// canonical case. Bit pattern: 11111 (raw=31) | 001010 (ext=10) → 32+10=42.
553    /// Then SFI=3 (0011) chan=2 (0010), pad. 17 bits.
554    /// 11111 001010 0011 0010 0 = 1111 1001 0100 0110 0100 = 0xF9 0x46 0x40.
555    #[test]
556    fn parse_xhe_aac_aot_escape() {
557        let asc = vec![0xF9, 0x46, 0x40];
558        let p = parse_aac_asc(&asc).expect("parse");
559        assert_eq!(p.aot, 42, "xHE-AAC USAC AOT=42 via escape");
560        assert_eq!(p.sample_rate, 48_000);
561        assert_eq!(p.channels, 2);
562    }
563
564    /// 24-bit inline sample rate: SFI=0xF then 24-bit rate. Use 88200 just
565    /// to differ from the table values for variety.
566    /// AOT=2, SFI=0xF, rate=88200 (0x015888), chan=2.
567    /// Bits: 00010 1111 [88200 in 24 bits] 0010
568    /// 88200 = 0000 0000 0000 0001 0101 1000 1000 1000 → low 24 bits
569    /// = 0000 0001 0101 1000 1000 1000.
570    /// Concatenation: 00010 1111 000000010101100010001000 0010
571    /// = 0001 0111 1000 0000 1010 1100 0100 0100 0010 = 5 bytes
572    /// 17807ac442 first 4 bytes... let's just compute.
573    #[test]
574    fn parse_inline_24bit_sample_rate() {
575        // Construct via BitWriter to avoid hand-bit-fiddling errors.
576        let mut bw = BitWriter::new();
577        bw.bits(2, 5); // AOT=2
578        bw.bits(0xF, 4); // SFI=0xF
579        bw.bits(88_200, 24); // inline rate
580        bw.bits(2, 4); // chan=2
581        let asc = bw.into_bytes();
582        let p = parse_aac_asc(&asc).expect("parse");
583        assert_eq!(p.aot, 2);
584        assert_eq!(p.sample_rate, 88_200);
585        assert_eq!(p.channels, 2);
586    }
587
588    /// Bit writer round-trip smoke test.
589    #[test]
590    fn bit_writer_roundtrip() {
591        let mut bw = BitWriter::new();
592        bw.bits(0b10101, 5);
593        bw.bits(0b1100, 4);
594        let bytes = bw.into_bytes();
595        // 10101 1100 0 = 0xAE 0x00; total written = 9 bits → padded to 2 bytes.
596        // Wait: 10101 1100 = 9 bits. Layout: 1010 1110 0000 0000 = 0xAE 0x00.
597        assert_eq!(bytes, vec![0xAE, 0x00]);
598        let mut br = BitReader::new(&bytes);
599        assert_eq!(br.bits(5), Some(0b10101));
600        assert_eq!(br.bits(4), Some(0b1100));
601    }
602
603    /// 5.1 ASC with channelConfiguration=6 (the standard MPEG L/R/C/LFE/Ls/Rs ordering).
604    /// Plain AAC-LC core at 48 kHz: AOT=2 SFI=3 chan=6.
605    /// 00010 0011 0110 000 = 0001 0001 1011 0000 = 0x11 0xB0.
606    #[test]
607    fn parse_aac_lc_5_1_at_48k() {
608        let asc = vec![0x11, 0xB0];
609        let p = parse_aac_asc(&asc).expect("parse");
610        assert_eq!(p.aot, 2);
611        assert_eq!(p.sample_rate, 48_000);
612        assert_eq!(p.channels, 6, "5.1 channel config");
613        assert_eq!(
614            p.signaling,
615            AscSignaling::NoExtension,
616            "48 kHz LC has no implicit HE-AAC interpretation"
617        );
618        assert_eq!(effective_output_channels(&p), 6);
619    }
620
621    /// 7.1 ASC with channelConfiguration=7. Plain AAC-LC at 48 kHz.
622    /// 00010 0011 0111 000 = 0001 0001 1011 1000 = 0x11 0xB8.
623    #[test]
624    fn parse_aac_lc_7_1_at_48k() {
625        let asc = vec![0x11, 0xB8];
626        let p = parse_aac_asc(&asc).expect("parse");
627        assert_eq!(p.channels, 7, "7.1 channel config");
628        assert_eq!(effective_output_channels(&p), 7);
629    }
630
631    /// Empty / truncated input must return None.
632    #[test]
633    fn parse_rejects_truncated() {
634        assert!(parse_aac_asc(&[]).is_none());
635        assert!(parse_aac_asc(&[0x12]).is_none());
636    }
637}