Skip to main content

arcly_stream/codec/
aac.rs

1//! AAC helpers: ADTS frame-header parsing.
2
3/// MPEG-4 sampling-frequency table indexed by `sampling_frequency_index`.
4pub const SAMPLE_RATES: [u32; 13] = [
5    96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350,
6];
7
8/// A decoded ADTS frame header (7-byte fixed + variable header).
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct AdtsHeader {
11    /// AAC audio object type (`profile + 1`; 2 = AAC-LC).
12    pub object_type: u8,
13    /// Sampling rate in Hz (resolved from the frequency index).
14    pub sample_rate: u32,
15    /// Channel configuration (1 = mono, 2 = stereo, …).
16    pub channels: u8,
17    /// Total frame length including the header, in bytes.
18    pub frame_length: usize,
19    /// Header length in bytes (7 without CRC, 9 with).
20    pub header_len: usize,
21}
22
23/// Parse the ADTS header at the start of `data`. Returns `None` if the syncword
24/// is absent or the buffer is too short.
25pub fn parse_adts(data: &[u8]) -> Option<AdtsHeader> {
26    if data.len() < 7 {
27        return None;
28    }
29    // Syncword: 12 bits of 1s (0xFFF).
30    if data[0] != 0xFF || (data[1] & 0xF0) != 0xF0 {
31        return None;
32    }
33    let protection_absent = data[1] & 0x01;
34    let profile = (data[2] >> 6) & 0x03;
35    let freq_index = ((data[2] >> 2) & 0x0F) as usize;
36    let channel_config = ((data[2] & 0x01) << 2) | ((data[3] >> 6) & 0x03);
37    let frame_length =
38        (((data[3] & 0x03) as usize) << 11) | ((data[4] as usize) << 3) | ((data[5] as usize) >> 5);
39
40    let sample_rate = *SAMPLE_RATES.get(freq_index)?;
41    let header_len = if protection_absent == 1 { 7 } else { 9 };
42    if frame_length < header_len {
43        return None;
44    }
45
46    Some(AdtsHeader {
47        object_type: profile + 1,
48        sample_rate,
49        channels: channel_config,
50        frame_length,
51        header_len,
52    })
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn parses_aac_lc_stereo_44100() {
61        // AAC-LC (profile 1), freq index 4 (44100), 2 channels, frame_length 13.
62        // byte2: profile(01) freq(0100) private(0) chan_hi(0) = 0b01_0100_0_0 = 0x50
63        // byte3: chan_lo(10) orig/home/etc(0000) len_hi(00) = 0b10_0000_00 = 0x80
64        // frame_length 13 = 0b0_0000_0000_1101 → len_hi(2)=00, byte4=0b0000_0001(=1)<<? ...
65        // 13 = (0<<11)|(1<<3)|(13>>5=0)? recompute: 13 = byte3[1:0]<<11 | byte4<<3 | byte5>>5
66        //   need 13 → byte4 = 1 (1<<3 = 8), byte5>>5 = 5 → byte5 = 0b101_00000 = 0xA0
67        let hdr = [0xFF, 0xF1, 0x50, 0x80, 0x01, 0xA0, 0x00];
68        let h = parse_adts(&hdr).expect("parse");
69        assert_eq!(h.object_type, 2); // AAC-LC
70        assert_eq!(h.sample_rate, 44100);
71        assert_eq!(h.channels, 2);
72        assert_eq!(h.frame_length, 13);
73        assert_eq!(h.header_len, 7); // protection_absent = 1
74    }
75
76    #[test]
77    fn rejects_without_syncword() {
78        assert!(parse_adts(&[0x00; 8]).is_none());
79        assert!(parse_adts(&[0xFF, 0x00]).is_none());
80    }
81}