🎬 第五章 FFmpeg命令行实现音视频转码
📄 一句话概括

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 秒实验

  1. 命令行对照:ffmpeg -f s16le -ar 44100 -ac 2 -i 出.pcm -c:a aac -b:a 128k 出.m4a。
  2. 换封装:把输出名改成 出.aac,ffmpeg 会自动选 ADTS。
  3. 比包数:ffprobe -show_packets 数一下音频包,应当与本节一致。

常见误区

  1. 以为码率越高包越大。包数固定,变大的是每包内部的数据量。
  2. 读裸 PCM 时漏掉声道数。默认按单声道解释,声音会变调。
  3. 以为 .aac 与 .m4a 只是后缀不同。封装完全不同,互不认账。
自测三问(点开对答案) 1. AAC 一包多少个采样点?与码率有关吗? —— 1024 点,与码率无关。码率改的是每一包内部花多少字节。 2. 为什么实跑的包数比「点数除 1024」多一? —— 编码器收尾时会 flush 出一个空尾包,所以总是多一包。 3. m4a 与 ADTS 的差别在哪? —— m4a 把所有包的索引放在末尾的 moov 里;ADTS 每包前面加 7 字节头自描述。

相关笔记