音视频修炼之基础理论(五):AAC格式与解析

AAC 格式与解析 ------ ADTS 头 / 元信息提取 / 声道分布 / 完整解码流程

第 2 篇音频基础讲了"为啥要 AAC",这一篇讲"AAC 长啥样、怎么拆开、怎么解码"。重点:ADTS 头 7 个字节里都装了啥、采样率/位深/声道这些元信息怎么从字节流里抠出来、解码后左右声道在内存里怎么排、完整解析流程。读完之后你能从 0 写一个 ADTS 拆帧器。


本文速览

章节 阅读重点
0. 引言:为啥要懂 AAC 字节细节 把握本节核心概念和使用场景
1. AAC 的四种装法 把握本节核心概念和使用场景
2. ADTS 头每个字节都装了啥 把握本节核心概念和使用场景
3. 怎么从 ADTS 头抠出元信息(C 代码示例) 对照代码和运行效果落地
4. 完整解码流程 重点看数据/调用如何流转
5. 解码后左右声道怎么排 把握本节核心概念和使用场景
6. 跟 MP4 里的 AAC 有啥区别(AudioSpecificConfig) 按场景做技术取舍
7. 实战代码:解析一个 .aac 文件 对照代码和运行效果落地
8. 常见坑 定位常见故障和规避方式
9. 一句话总结 收束要点,形成记忆锚点

0. 引言:为啥要懂 AAC 字节细节

很多人写音频程序时遇到过这种事:

序号 要点
1 拿到一个 .aac 文件,FFmpeg 能播,自己写代码调用 MediaCodec 报 MEDIA_ERROR_INVALID_INPUT
2 解码出来的 PCM 直接送 AudioTrack,结果声音是绿色噪音
3 跨进程传 AAC 数据,对端死活同步不上

这些坑共同的根源:没搞清 AAC 的字节格式 。AAC 不是单一格式------它有四种装法,每种存元信息的位置都不一样。一个 .aac 文件里看到的字节,跟 MP4 里 esds box 的字节,跟 RTMP 的 AAC 包里的字节,长得都不同。


1. AAC 的四种装法

四种容器选哪种,一眼看清:

容器 元信息位置 能从中间播放吗 实战用途
raw AAC 全无(必须靠外面带) MP4 内部 mdat
ADTS-AAC 每帧前 7-9 字节头 .aac 文件 / RTMP / HLS-TS
AAC in MP4 esds box 里一份 AudioSpecificConfig(2 字节) ✅(需先读 esds) MP4 容器、视频音轨
ADIF 文件开头一份 已淘汰

实战经验

序号 要点
1 自己存盘 → 用 ADTS-AAC(.aac 直接能播)
2 视频音轨 → 走 MP4(esds + raw 帧)
3 直播流(RTMP / HLS-TS)→ ADTS-AAC
4 看到陌生 .aac 文件第一件事hexdump -C file.aac | head -1 看头,FF F1 / FF F9 开头 = ADTS,否则是 ADIF 或 raw

2. ADTS 头每个字节都装了啥

一个标准 ADTS 帧 = 7 字节头 + AAC 压缩数据(带 CRC 是 9 字节头)。56 个 bit 的字段:

2.1 字节 1-2(关键:syncword)

字段 位数 含义
syncword 12 bit = 0xFFF ,固定 12 个 1,找帧起点的标志
ID 1 bit 0=MPEG-4,1=MPEG-2
layer 2 bit 固定 = 00
protection_absent 1 bit 1=无 CRC(头 7 字节),0=有 CRC(头 9 字节)

2.2 字节 3-4(关键:profile + 采样率索引 + 声道)

字段 位数 含义
profile 2 bit 0=Main,1=AAC LC ⭐ 最常见,2=SSR,3=LTP
sampling_frequency_index 4 bit 采样率索引(查表见下)
private_bit 1 bit 私有,= 0
channel_configuration 3 bit 声道配置(查表见下)

2.3 字节 5-7(关键:帧长度)

字段 位数 含义
original/copy 1 bit 忽略
home 1 bit 忽略
copyright_id_bit 1 bit 忽略
copyright_id_start 1 bit 忽略
aac_frame_length 13 bit 整帧长度 = 7 (header) + AAC payload,跳到下一帧靠这个
adts_buffer_fullness 11 bit CBR 时 = 0x7FF,VBR 时是缓冲状态
number_of_raw_data_blocks_in_frame 2 bit 0 = 1 个 block(最常见),每 block = 1024 samples

2.4 采样率索引表(必查)

index 采样率(Hz) 备注
0 96000 ---
1 88200 ---
2 64000 ---
3 48000 视频音轨 ⭐
4 44100 音乐 ⭐
5 32000 ---
6 24000 ---
7 22050 ---
8 16000 通话
9 12000 ---
10 11025 ---
11 8000 窄带语音
12-15 保留 ---

2.5 声道配置表

value 声道数 布局
0 自定义 头里再带
1 1 Mono
2 2 L + R(最常见
3 3 C + L + R
4 4 C + L + R + 后中
5 5 C + L + R + LS + RS
6 6 5.1(FL+FR+C+LFE+BL+BR)
7 8 7.1

3. 怎么从 ADTS 头抠出元信息(C 代码示例)

实战代码------给一段 ADTS 字节流,抠出采样率、声道、帧长:

c 复制代码
typedef struct {
    int profile;          // AAC profile
    int sample_rate;      // 采样率
    int channels;         // 声道数
    int frame_length;     // 整帧长度
    int has_crc;          // 是否有 CRC
} AdtsHeader;

static const int aac_sample_rates[16] = {
    96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050,
    16000, 12000, 11025, 8000, 7350, 0, 0, 0
};

int parse_adts(const uint8_t* p, AdtsHeader* h) {
    // ① 检查 syncword 0xFFF
    if (p[0] != 0xFF || (p[1] & 0xF0) != 0xF0) {
        return -1;  // 不是 ADTS 帧起点
    }

    // ② protection_absent: byte 1 bit 0
    h->has_crc = !(p[1] & 0x01);

    // ③ profile: byte 2 bit 7-6 (注意 +1, 因为 AAC LC 在 ADTS 里是 1, ASC 里是 2)
    h->profile = ((p[2] >> 6) & 0x03) + 1;

    // ④ sampling_frequency_index: byte 2 bit 5-2
    int sr_index = (p[2] >> 2) & 0x0F;
    h->sample_rate = aac_sample_rates[sr_index];

    // ⑤ channel_configuration: byte 2 bit 0 + byte 3 bit 7-6
    h->channels = ((p[2] & 0x01) << 2) | ((p[3] >> 6) & 0x03);

    // ⑥ aac_frame_length: byte 3 bit 1-0 + byte 4 全 + byte 5 bit 7-5 (共 13 bit)
    h->frame_length = ((p[3] & 0x03) << 11)
                    | (p[4] << 3)
                    | ((p[5] >> 5) & 0x07);

    return 0;
}

几个易踩的坑

说明
profile 偏移 ADTS 里的 profile 跟 AudioSpecificConfig(ASC)里的 AOT 差 1。ADTS 的 1 = ASC 的 2 = AAC LC
位深 AAC 不存 AAC 是有损压缩,输入端是 16 bit PCM、输出端可能是 16 bit S16 或 Float32 ,但 ADTS 头里没有"位深"字段------位深只在解码器输出格式里有
frame_length 跨字节 13 bit 跨 3 个字节,移位别错

4. 完整解码流程

6 步串起来:

序号 要点 说明
1 找 syncword 0xFFF 12 个连续 1 bit,定位 ADTS 帧起点
2 解 ADTS 头 (7 字节)--- 抠出采样率索引、声道、profile、frame_length
3 切出 AAC payload = frame_length - 7(带 CRC 时 -9)
4 送解码器 (FAAD2 / FFmpeg avcodec / Android MediaCodec)--- 用采样率 + 声道初始化
5 拿 PCM 输出 一次解 1024 samples × 声道数
6 指针前移 p += frame_length,回到 ① 找下一帧

4.1 用 FFmpeg 解码(C 代码)

c 复制代码
// 1. 找 AAC 解码器
const AVCodec* codec = avcodec_find_decoder(AV_CODEC_ID_AAC);
AVCodecContext* ctx = avcodec_alloc_context3(codec);

// 2. 配置参数 (从 ADTS 头来的, 上一步抠出来的)
ctx->sample_rate = h.sample_rate;
ctx->ch_layout.nb_channels = h.channels;
ctx->profile = FF_PROFILE_AAC_LOW;  // AAC LC
avcodec_open2(ctx, codec, NULL);

// 3. 喂数据
AVPacket* pkt = av_packet_alloc();
pkt->data = (uint8_t*)adts_frame_data;  // 含 ADTS 头的整帧
pkt->size = h.frame_length;
avcodec_send_packet(ctx, pkt);

// 4. 拿 PCM
AVFrame* frame = av_frame_alloc();
while (avcodec_receive_frame(ctx, frame) >= 0) {
    // frame->data[0] = 左声道 PCM (Planar Float32)
    // frame->data[1] = 右声道 PCM
    // frame->nb_samples = 1024
    process_pcm(frame);
}

4.2 用 Android MediaCodec 解码(Java 代码)

java 复制代码
MediaCodec decoder = MediaCodec.createDecoderByType("audio/mp4a-latm");
MediaFormat format = MediaFormat.createAudioFormat(
    "audio/mp4a-latm", sampleRate, channels);

// AAC LC 用 csd-0 = AudioSpecificConfig 2 字节
// (从 ADTS 头转换: profile/sr_index/channel 拼出来)
byte[] csd = makeAudioSpecificConfig(profile, srIndex, channels);
format.setByteBuffer("csd-0", ByteBuffer.wrap(csd));

decoder.configure(format, null, null, 0);
decoder.start();

// 喂 raw AAC (注意: MediaCodec 默认要的是 raw, 不是 ADTS)
// 必须先把 ADTS 头剥掉再送
int idx = decoder.dequeueInputBuffer(-1);
ByteBuffer buf = decoder.getInputBuffer(idx);
buf.put(adts_frame, 7, frame_length - 7);  // 跳过 7 字节 ADTS 头
decoder.queueInputBuffer(idx, 0, frame_length - 7, pts, 0);

关键差异 :FFmpeg 接受带 ADTS 头 的整帧,MediaCodec 默认接受剥掉 ADTS 头的 raw AAC + csd 元信息。这俩别搞混。


5. 解码后左右声道怎么排

不同解码器的输出格式不一样,实战必须 query 一下

解码器 输出格式 排布 1 帧字节数(stereo)
MediaCodec S16 (16 bit int) Interleaved L0 R0 L1 R1... 1024 × 2 ch × 2 = 4096
AudioTrack 输入 S16 Interleaved 同上
FFmpeg AVFrame Float32 Planar data[0]=L*, data[1]=R* 每平面 1024 × 4 = 4096 (× 2 平面)
OpenSL ES S16 Interleaved 4096
AAudio output Float32 / S16 Interleaved(用 setSharingMode 视配置

互转套路

c 复制代码
// FFmpeg Planar Float → MediaCodec S16 Interleaved
for (int i = 0; i < 1024; i++) {
    int16_t L = (int16_t)(frame->data[0][i*4] * 32767);  // 左
    int16_t R = (int16_t)(frame->data[1][i*4] * 32767);  // 右
    interleaved[i*2]   = L;
    interleaved[i*2+1] = R;
}

少做这一步 → 左右声道错乱、或者满屏噪音。


6. 跟 MP4 里的 AAC 有啥区别(AudioSpecificConfig)

MP4 里的 AAC 不带 ADTS 头,而是把所有元信息浓缩到 2 字节 的 AudioSpecificConfig(ASC),存在 esds box 里:

text 复制代码
ASC 字节 (5 bit + 4 bit + 4 bit + 1 bit + 1 bit + 1 bit = 16 bit = 2 字节):

bit 0-4:   AOT (Audio Object Type)        // 5 bit, AAC LC = 2
bit 5-8:   sampling_frequency_index        // 4 bit, 跟 ADTS 同表
bit 9-12:  channel_configuration           // 4 bit
bit 13:    frame_length_flag               // 1 bit, 0=1024 sample, 1=960
bit 14:    depends_on_core_coder            // 1 bit, = 0
bit 15:    extension_flag                   // 1 bit, = 0

ADTS → ASC 转换公式(实战常用):

c 复制代码
uint8_t asc[2];
asc[0] = (aot << 3) | ((sr_index >> 1) & 0x07);   // AOT 5bit + sr 高 3bit
asc[1] = ((sr_index & 0x01) << 7) | (channels << 3);  // sr 低 1bit + ch 4bit
// AAC LC: aot = 2 (注意! ADTS 的 profile=1 对应 AOT=2)

知道这个,就能在"ADTS-AAC 文件"和"MP4 里的 AAC"之间互转。


7. 实战代码:解析一个 .aac 文件

完整可跑的最小拆帧器:

c 复制代码
#include <stdio.h>
#include <stdint.h>

static const int sr_table[16] = {
    96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050,
    16000, 12000, 11025, 8000, 7350, 0, 0, 0
};

int main(int argc, char** argv) {
    FILE* fp = fopen(argv[1], "rb");
    fseek(fp, 0, SEEK_END);
    long size = ftell(fp);
    fseek(fp, 0, SEEK_SET);

    uint8_t* buf = malloc(size);
    fread(buf, 1, size, fp);
    fclose(fp);

    long offset = 0;
    int frame_count = 0;

    while (offset < size - 7) {
        // 找 syncword 0xFFF
        if (buf[offset] != 0xFF || (buf[offset+1] & 0xF0) != 0xF0) {
            offset++;
            continue;
        }

        int profile = ((buf[offset+2] >> 6) & 0x03) + 1;
        int sr_idx  = (buf[offset+2] >> 2) & 0x0F;
        int ch      = ((buf[offset+2] & 0x01) << 2) | ((buf[offset+3] >> 6) & 0x03);
        int len     = ((buf[offset+3] & 0x03) << 11) | (buf[offset+4] << 3)
                    | ((buf[offset+5] >> 5) & 0x07);

        printf("Frame %d @ 0x%lx: profile=%d sr=%d ch=%d len=%d\n",
               frame_count, offset, profile, sr_table[sr_idx], ch, len);

        offset += len;
        frame_count++;
    }

    printf("Total frames: %d\n", frame_count);
    free(buf);
    return 0;
}

跑一下:

bash 复制代码
gcc parse_aac.c -o parse_aac
./parse_aac music.aac
# Frame 0 @ 0x0: profile=2 sr=44100 ch=2 len=415
# Frame 1 @ 0x19f: profile=2 sr=44100 ch=2 len=412
# ...
# Total frames: 8347

40 行 C 代码就把 ADTS-AAC 文件拆完了。


8. 常见坑

8.1 syncword 找错位置

0xFFF 这 12 bit 在数据里其实不算稀有------某些 AAC payload 里也可能出现这串 bit。

解决方案 :找到候选 syncword 后,用 frame_length 跳到下一帧再确认------如果下一个位置也是 0xFFF,才算真起点。常见叫"双校验"。

8.2 ADTS / raw AAC 混淆

直播流里 AAC 通常带 ADTS 头,但有些转推系统会剥掉头只传 raw + esds。

要点 说明
看到字节流以 0xFF F10xFF F9 开头 ADTS
看到字节流以其他字节开头 大概率 raw(要从外面拿 ASC)

8.3 profile 偏移坑

来源 AAC LC 编号
ADTS profile 字段 1
AudioSpecificConfig AOT 2
FFmpeg FF_PROFILE_AAC_LOW 1
MediaCodec MediaCodecInfo.CodecProfileLevel.AACObjectLC 2

互转代码记得 +1 / -1

8.4 1024 vs 960 samples

AAC 默认每帧 1024 samples。但有种"AAC LD"(Low Delay)profile 是 480 / 512,"AAC ELD"是 480 / 512。看到帧长不对立马查 profile。

8.5 LATM / LOAS

广播电视用的另一种 AAC 封装,不是 ADTS 也不是 MP4。看到 0x56 0xE0 开头基本就是 LATM。RTMP / HLS 里基本不会碰到,DVB-T 流里要小心。


9. 一句话总结

AAC = 裸压缩流 + 一份元信息 。元信息要么放在每帧头部(ADTS),要么放在容器里(MP4 esds),要么完全在外面(raw)。搞清楚拿到的字节是哪种装法,是写音频代码的第一步

记住三个数字:

说明
0xFFF ------ ADTS 帧 syncword(12 bit)
7 字节 ------ ADTS 头标准长度(带 CRC 是 9)
1024 samples ------ AAC LC 一帧定长(per channel)

剩下的所有解析都是从这三个数字推出来的。

相关推荐
法欧特斯卡雷特2 小时前
Kotlin 2.4.20 现已发布,新特性多不多?
android·开源·全栈
摇滚侠2 小时前
《SpringBoot 3:入门与应用实战》第 12 章 JDBC 与事务 使用 JdbcTemplate 阅读笔记 31
android·spring boot·笔记
米糕闯编程2 小时前
鱼香ros2(一)在ubuntu中安装ros2
linux·运维·ubuntu·机器学习
Huangjin007_2 小时前
【Linux 系统篇(十八)】进程(六) :环境变量
linux·运维·服务器
ShineWinsu2 小时前
对于MySQL:数据库的操作的解析
linux·数据库·c++·mysql·面试·笔试·库的操作
cnnews2 小时前
Ubuntu 编译 postmarketOS
linux·运维·arm开发·ubuntu·github·音视频
w01_02_032 小时前
linux, 桌面, 自启动。
linux·运维·服务器
云小逸2 小时前
C++ 第一阶段:对象、内存与生命周期
开发语言·c++
夜航海图2 小时前
S-57 数据解剖:把一个 ENC 文件拆给你看
c++