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 字节流,抠出采样率、声道、帧长:
typedefstruct{intprofile;// AAC profileintsample_rate;// 采样率intchannels;// 声道数intframe_length;// 整帧长度inthas_crc;// 是否有 CRC}AdtsHeader;staticconstintaac_sample_rates[16]={96000,88200,64000,48000,44100,32000,24000,22050,16000,12000,11025,8000,7350,0,0,0};intparse_adts(constuint8_t*p,AdtsHeader*h){// ① 检查 syncword 0xFFFif(p[0]!=0xFF||(p[1]&0xF0)!=0xF0){return-1;// 不是 ADTS 帧起点}// ② protection_absent: byte 1 bit 0h->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-2intsr_index=(p[2]>>2)&0x0F;h->sample_rate=aac_sample_rates[sr_index];// ⑤ channel_configuration: byte 2 bit 0 + byte 3 bit 7-6h->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);return0;}几个易踩的坑:
| 项 | 说明 |
|---|---|
| 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 代码)
// 1. 找 AAC 解码器constAVCodec*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 LCavcodec_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. 拿 PCMAVFrame*frame=av_frame_alloc();while(avcodec_receive_frame(ctx,frame)>=0){// frame->data[0] = 左声道 PCM (Planar Float32)// frame->data[1] = 右声道 PCM// frame->nb_samples = 1024process_pcm(frame);}4.2 用 Android MediaCodec 解码(Java 代码)
MediaCodecdecoder=MediaCodec.createDecoderByType("audio/mp4a-latm");MediaFormatformat=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 头剥掉再送intidx=decoder.dequeueInputBuffer(-1);ByteBufferbuf=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) | InterleavedL0 R0 L1 R1... | 1024 × 2 ch × 2 =4096 |
| AudioTrack 输入 | S16 | Interleaved | 同上 |
| FFmpeg AVFrame | Float32 | Planardata[0]=L*, data[1]=R* | 每平面 1024 × 4 = 4096 (× 2 平面) |
| OpenSL ES | S16 | Interleaved | 4096 |
| AAudio output | Float32 / S16 | Interleaved(用setSharingMode) | 视配置 |
互转套路:
// FFmpeg Planar Float → MediaCodec S16 Interleavedfor(inti=0;i<1024;i++){int16_tL=(int16_t)(frame->data[0][i*4]*32767);// 左int16_tR=(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),存在esdsbox 里:
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, = 0ADTS → ASC 转换公式(实战常用):
uint8_tasc[2];asc[0]=(aot<<3)|((sr_index>>1)&0x07);// AOT 5bit + sr 高 3bitasc[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 文件
完整可跑的最小拆帧器:
#include<stdio.h>#include<stdint.h>staticconstintsr_table[16]={96000,88200,64000,48000,44100,32000,24000,22050,16000,12000,11025,8000,7350,0,0,0};intmain(intargc,char**argv){FILE*fp=fopen(argv[1],"rb");fseek(fp,0,SEEK_END);longsize=ftell(fp);fseek(fp,0,SEEK_SET);uint8_t*buf=malloc(size);fread(buf,1,size,fp);fclose(fp);longoffset=0;intframe_count=0;while(offset<size-7){// 找 syncword 0xFFFif(buf[offset]!=0xFF||(buf[offset+1]&0xF0)!=0xF0){offset++;continue;}intprofile=((buf[offset+2]>>6)&0x03)+1;intsr_idx=(buf[offset+2]>>2)&0x0F;intch=((buf[offset+2]&0x01)<<2)|((buf[offset+3]>>6)&0x03);intlen=((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);return0;}跑一下:
gcc parse_aac.c-oparse_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: 834740 行 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 |
FFmpegFF_PROFILE_AAC_LOW | 1 |
MediaCodecMediaCodecInfo.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) |
剩下的所有解析都是从这三个数字推出来的。