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 F1 或 0xFF 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) |
剩下的所有解析都是从这三个数字推出来的。