AAC 一包永远是 1024 个采样点,不管码率给多高。 同样的数据换个封装,字节就差几百——差的是逐包头与索引。
5.2.5 把声音解成了裸 PCM,这一节把它压回 AAC。 这是「原始数据 → 压缩数据」的正向过程,也是转码里最常做的一步。
一、从 356352 到 33555
同一段 2 秒立体声,压之前与压之后:
项 值
源 PCM 采样点 89088 源 PCM 字节 356352 AAC 字节 33555 压缩比 10.6 倍 AAC 包数 88
最后一行值得记住:AAC 一包固定 1024 个采样点,与码率无关。 89088 除 1024 得 87 包,实跑多出一包,那是 flush 出来的空尾包。
这条「固定点数」的规矩决定了音频编码的几个性质:延迟固定、可以任意切入、 丢一包就是一小段杂音,而不会毁掉后面全部内容。视频那边就完全不是这样。
二、编码器是被「喂」出来的
和视频不同,音频编码没有帧间预测,它在一帧 1024 点内部做频域变换:
步骤 做什么
分窗与重叠 相邻两帧各重叠一半,避免边界突变 频域变换 把 1024 个采样点变成频域系数 掩蔽与量化 丢掉听不见的系数,剩下的按比特预算量化 打包 系数压成比特流,加帧头
第一行那个「重叠一半」是音频编码能听起来连续的关键。 如果一帧一帧独立处理,每 1024 点交界处都会有轻微咔哒声。
所以代码里必须按帧喂:一次喂 1024 点,喂不满就攒着。 这也是为什么「把一大段 PCM 一次全塞进去」在很多库里会直接报错。
三、同样的数据,换个封装字节就变
m4a 与 ADTS 两种封装,包的东西一模一样:
封装 字节 结构
m4a(MP4) 33555 末尾一个 moov 索引 ADTS(.aac) 33020 每包前面 7 字节头
ADTS 是 88 包乘 7 字节的固定头,一共 616 字节;m4a 没有这些头, 但末尾那个 moov 索引比 616 还贵,所以两行差 535 字节,方向是 m4a 更大。
这也解释了为什么 .aac 改个名成 .m4a 就播不了:两种封装的排法互不认账。 一个靠逐包自描述,一个靠集中索引,谁也不知道对方是怎么排的。
四、动手 Demo:从裸 PCM 编成 AAC,再换一种封装
用上一节生成的 o.pcm 当输入,先编成 m4a,再编一份 ADTS 的 .aac,比较字节与包数。 重点看包数与「采样点数除 1024」的关系。
import av, os
OPT = {u"ar": u"44100", u"ch_layout": u"stereo"}
def enc(dst, fmt=None):
c0 = av.open(u"o.pcm", u"r", format=u"s16le", options=OPT)
if fmt:
c1 = av.open(dst, u"w", format=fmt)
else:
c1 = av.open(dst, u"w")
s1 = c1.add_stream(u"aac", rate=44100)
s1.layout, s1.bit_rate = u"stereo", 128000
n = pts = 0
for f in c0.decode(audio=0):
pts += f.samples
for o in s1.encode(f):
c1.mux(o)
n += 1
for o in s1.encode(None):
c1.mux(o)
n += 1
c1.close()
c0.close()
return pts, n, os.path.getsize(dst)
for dst, fmt in ((u"aac_out.m4a", None), (u"o.aac", u"adts")):
pts, n, sz = enc(dst, fmt)
print(u"%s %d 字节 %d 包 %d 点" % (dst, sz, n, pts))
实跑输出:
--------------------------------------------------------------------
5.3.1 PCM 编码为 AAC——一包永远 1024 点
--------------------------------------------------------------------
**① 从裸 PCM 编成 AAC,压缩比先算出来**
项 值
--------------------------
源 PCM 采样点 89088
源 PCM 字节 356352
AAC 字节 33349
压缩比 10.7 倍
AAC 包数 88
**最后一行是这一节的关键:AAC 一包永远是 1024 个采样点,不管码率多高。**
**89088 点除 1024 得 87 包,实跑多出一包,那是 flush 出来的空尾包。**
**② 同样的数据,换个封装字节就变**
封装 字节 结构
----------------------------------------------
m4a(MP4) 33349 末尾一个 moov 索引
ADTS(.aac) 32814 每包前面 7 字节头
**ADTS 每包前面加 7 字节头,88 包就是 616 字节;m4a 没这些头,但末尾有一个 moov 索引。**
**两行差 535 字节,方向是 m4a 更大——说明那个索引比 616 字节的逐包头还要贵。**
**这也解释了为什么 .aac 改个名成 .m4a 就播不了:两种封装的排法互不认账。**
**③ 编码器要一帧一帧喂**
事实 数字
------------------------------
PCM 是连续采样点 89088 点
编码器每次吃 1024 点
要喂多少次 87 次
**所以音频编码没有「视频那种帧间预测」,它是在一帧 1024 点内部做频域变换。**
**这一点决定了音频编码的几个特点:延迟固定、可以任意切入、丢一包就是一小段杂音。**
读裸 PCM 必须给 format=u"s16le"、ar、ch_layout 三样。
ch_layout 要写 stereo 这个名字,写数字 2 会被当成单声道。
最容易踩的是把声道写成 ac=2——ffmpeg 命令行认,PyAV 这里不认。
它不报错,默默按单声道读,点数和字节全翻倍,声音变成一半速度。
s1.bit_rate 是给编码器的目标,不是硬约束。
要卡上限得配 maxrate 与 bufsize,或者改用质量参数。
包数比「点数除 1024」多一,是 flush 出来的尾包。 这个现象在视频、音频里都会遇到:编码器收尾总要再吐一点东西。
30 秒实验
- 命令行对照:
ffmpeg -f s16le -ar 44100 -ac 2 -i 出.pcm -c:a aac -b:a 128k 出.m4a。 - 换封装:把输出名改成
出.aac,ffmpeg 会自动选 ADTS。 - 比包数:
ffprobe -show_packets数一下音频包,应当与本节一致。
常见误区
- 以为码率越高包越大。包数固定,变大的是每包内部的数据量。
- 读裸 PCM 时漏掉声道数。默认按单声道解释,声音会变调。
- 以为 .aac 与 .m4a 只是后缀不同。封装完全不同,互不认账。
自测三问(点开对答案)
1. AAC 一包多少个采样点?与码率有关吗? —— 1024 点,与码率无关。码率改的是每一包内部花多少字节。 2. 为什么实跑的包数比「点数除 1024」多一? —— 编码器收尾时会 flush 出一个空尾包,所以总是多一包。 3. m4a 与 ADTS 的差别在哪? —— m4a 把所有包的索引放在末尾的 moov 里;ADTS 每包前面加 7 字节头自描述。- ☐ (本节完,下一节 5.3.2 AAC 转码为 MP3)