🎬 第十章 SWResample音频重采样理论及案例实战
📄 一句话概括

把一条 44.1k 的真实音轨转到 48k,落成裸 PCM,再用字节数核对。 样本数变了,时长必须一模一样——这是唯一验收标准。

📝 本节和前面几节的关系

10.3.1 讲了六步,这一节拿真实素材走一遍并核对账目。 10.3.3 是它的一个变种:只转存法,不改采样率。

一、一进一出,逐帧转

解码出来的帧一帧帧喂进去,收多少算多少。 项 进 出


采样率 44100 48000 格式 fltp s16 总样本 90112 98082 时长 2.0434 秒 2.0434 秒

时长到小数点后四位都对得上。 样本数从 90112 变 98082,比例 1.088445, 而 48000 ÷ 44100 是 1.088435——差在小数点后第五位,就是取整造成的。 这一条差值不是 bug:样本必须是整数个, 任何重采样都得在某个地方取整。 只要误差小于一个样本,就算做对了。

二、落成裸 PCM,用字节数核对

账目干净是裸 PCM 最大的好处。 项 值


样本 98082 声道×字节 2 × 2 文件字节 392328 手算结果 98082 × 4 = 392328

一个不差。 这种「手算与实测对得上」的核对,在容器里做不到——容器有头、有索引、有填充, 字节数对不上任何简单公式。 所以调试音频的标准动作是:先转裸 PCM 核对账,再回容器里跑。 账对上了,剩下的问题就只可能在容器那一层。

三、再转回去,看会不会越转越少

48000 转回 44100,看样本数回不回得来。 路线 样本数 时长


原始 44100 90112 2.0434 秒 转 48000 再转回 90113 2.0434 秒

多了一个样本,时长不变。 说明来回转不会「越转越少」, 但会差一两个样本——每次取整都可能偏一点。 音质上是有损失的:每次降采样都要过一遍低通, 高频被切掉的那部分回不来了。 所以「能一次转到位就别转两次」。

四、这条链的三个关键点

三条都不报错,三条都得自己记住。 关键点 写错会怎样


目标参数构造时给全 缺一项就转不到位 每帧都送,不只送第一帧 后面全丢 末尾 resample(None) 丢尾巴几毫秒

第一条最容易被简化掉:有人只给 rate=48000,格式与声道走默认, 结果出来是 s16/mono——采样率对了,声道少了一半,而且不报错。 所以六个参数要当「必须写全」来对待,哪怕其中三个看起来「应该不变」。

五、真实项目里的位置

重采样一般卡在三个位置。 位置 为什么在那里


解码之后 统一成内部格式再处理 编码之前 编码器只吃特定格式 送设备之前 声卡只认特定采样率

最常见的是「解码之后统一」:不管素材是什么采样率,进来先转成内部统一的 48k 浮点, 后面所有处理都不必再管格式。 这叫「归一化」,代价是多一次转换,收益是后面几百行代码都不用判断格式。 工程上这笔账通常划算。

六、动手 Demo:把 44.1k 的真实音轨重采样到 48k

逐帧转、落成裸 PCM、核对字节数,最后再转回去看会不会变短。

import av

c = av.open("tiny.mp4")
st = c.streams.audio[0]
r = av.AudioResampler(format="s16", layout="stereo", rate=48000)

sin = sout = 0
chunks = []
for pk in c.demux(st):
    if pk.size == 0:
        break
    for fr in st.codec_context.decode(pk):
        sin += fr.samples
        for x in r.resample(fr):
            sout += x.samples
            chunks.append(x.to_ndarray())
for x in r.resample(None):
    sout += x.samples
    chunks.append(x.to_ndarray())
c.close()

data = b"".join(ch.tobytes() for ch in chunks)
open("resampled_48k_s16.pcm", "wb").write(data)
print(sin, sout)
print(sin / 44100.0, sout / 48000.0)
print(len(data), sout * 2 * 2)

实跑输出:

--------------------------------------------------------------------
把 44.1k 的真实音轨重采样到 48k
--------------------------------------------------------------------

**① 一进一出,逐帧转**

  项           进            出
  ----------------------------------------
  采样率       44100         48000
  格式         fltp          s16
  总样本       90112         98082
  时长         2.0434 秒     2.0434 秒
  输出帧数     —             89

**两边的时长一模一样,到小数点后四位都对得上。**
**样本数从 90112 变成 98082,比例是 1.088445,而 48000÷44100 是 1.088435。**

**② 落成裸 PCM,核对字节数**

  项             值
  -----------------------------------------
  文件           resampled_48k_s16.pcm
  字节           392328
  该等于         98082 × 2 × 2 = 392328
  对不对得上     对

**98082 个样本 × 2 声道 × 2 字节 = 392328 字节,一个不差。**
**裸 PCM 的好处就在这里:账目干净,全都能手算核对。**

**③ 再转回去,看会不会越转越少**

  路线                      样本数     时长
  --------------------------------------------------
  原始 44100                90112      2.0434 秒
  转 48000 再转回 44100     90113      2.0434 秒

**转一圈回来,样本数差 1 个,时长差不到 1 毫秒。**
**这说明来回转不会「越转越短」**——但每转一次都过一遍滤波器,
**音质是会有损失的,所以能一次转到位就别转两次。**

**④ 这段代码的三个关键点**

  关键点                     写错会怎样
  -----------------------------------------------
  目标参数在构造时给全       缺一项就转不到位
  每帧都送,不只送第一帧     后面全丢
  末尾 resample(None)        丢尾巴几毫秒

**三条都不报错,三条都得自己记住。** 这就是音频处理比视频更容易出错的地方——视频错了能看见,
音频错了只是「感觉有点不对」。

两边时长完全一致(2.0434 秒),样本数从 90112 变 98082。 落盘 392328 字节 = 98082 × 2 × 2,手算与实测一个不差。

30 秒实验

  1. 把目标改成 16000,看输出样本数与时长(提示:时长仍 2.04 秒)。
  2. 把目标改成 format="flt",看落盘字节数变成几倍。
  3. 不冲空包跑一遍,看样本数少多少。

常见误区

  1. 只看样本数不看时长。 两条都要对。
  2. 只给 rate 不给 format 与 layout。 声道会悄悄变成默认值。
  3. 来回转多次求「无损」。 每次降采样都有损失。
自测三问(点开对答案) 1. 90112 个 44.1k 样本转到 48k 该有多少? —— 约 98082 个,比例就是 48000 ÷ 44100。 2. 为什么样本数不是精确的 98083.8? —— 样本必须是整数个,必然要在某处取整。 3. 落盘裸 PCM 有什么用? —— 字节数能手算核对,是验收音频处理最快的办法。

相关笔记