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

手写重采样只要十几行:按比例取点,或者按比例插值。 但手写版省掉了抗混叠与跨帧状态,这才是库的价值。

📝 本节和前面几节的关系

10.1.3 讲了重采样的账,这一节自己动手写一遍。 教材用 C 写,这里用 Python 演示同一套算法——算法本身与语言无关。

一、最近邻:最简单的做法

按比例算下标,取最近的那个点。 原始 6 个样本 升两倍(12 个)


0 100 200 300 400 500 0 0 100 100 200 200 …

升采样就是「每个点重复一次」,降采样就是「隔一个丢一个」。 两行代码,快到可以实时。 代价是台阶:波形变成锯齿状,听起来发毛。 但它在「只要样本数对、不管音质」的场合完全够用,比如某些硬件的固定采样率要求。

二、线性插值:在两点之间补

取相邻两点,按小数部分加权。 做法 结果


原始 0 100 200 300 400 500 线性插值升两倍 0 50 100 150 200 250 …

值不再是重复的,而是平滑过渡。 代码也就十几行,这就是教材里 C 版重采样的核心。 C 里的写法与这里一模一样:一个浮点下标、两层整形、一次加权。 换成 Python 只是把循环换成了数组运算,算式没变。

三、手写版缺的三样

库比手写强的地方,全在这三条里。 缺什么 后果


抗混叠低通 降采样会有金属味杂音 跨帧状态 每帧接缝处会「咔」一下 足够的插值阶数 高频细节丢失,听感发闷

第二条最容易被忽略:手写版每帧单独算,接缝处因为缺少前文,会跳一下。 25 帧一秒,就是每秒二十五次咔哒。 库把滤波器状态存在上下文里,所以跨帧连续。 这就是「为什么要有个 SwrContext 对象」的原因——它存的就是这份状态。

四、教材用 C 写的理由

不是为了让你在生产里手写,是为了看懂每一层。 自己写一遍 得到什么


懂下标怎么算 看懂别人的实现 懂延迟从哪来 不再奇怪为什么要冲包 懂为什么需要上下文 不再把 swr 当黑盒

这三条都是「理解」层面的收益,不是「代码」层面的。 所以教材那几十行 C 值得跟着敲一遍,然后忘掉它、用库。 这个模式在整套书里反复出现:先手写一遍看懂原理,再用库。手写是为了懂,用库是为了对。

五、沙箱能验到什么

C 版跑不了(本机没有编译器),但算法能验。 能验 验不了


按下标取点对不对 C 的内存与指针写法 插值公式对不对 定点运算的精度 输出样本数与理论一致 处理速度

算法与语言无关,这是「用 Python 演 C 算法」的正当理由。 而内存布局、定点优化、SIMD 这些只有真编译才验得到,所以清单里标清楚,不假装跑过。

六、动手 Demo:自己动手写一次重采样,再看库为什么强

用 6 个样本做例子:最近邻升两倍、线性插值升两倍,最后对比库。

import numpy as np
import av

src = np.array([0, 100, 200, 300, 400, 500], dtype=np.int16)

# 最近邻:按比例算下标
up = src[np.floor(np.arange(12) * len(src) / 12.0).astype(int)]
print(up.tolist())

# 线性插值:取相邻两点加权
x = np.arange(12) * len(src) / 12.0
i0 = np.floor(x).astype(int)
i1 = np.minimum(i0 + 1, len(src) - 1)
frac = x - i0
lin = (src[i0] * (1 - frac) + src[i1] * frac).astype(np.int16)
print(lin.tolist())

# 库的做法:冲空包之后样本数正好对上
r = av.AudioResampler(format="s16", layout="mono", rate=16000)
f = av.AudioFrame(format="s16", layout="mono", samples=800)
f.sample_rate = 8000
a = sum(x.samples for x in r.resample(f))
b = sum(x.samples for x in r.resample(None))
print(a, b, a + b)

实跑输出:

--------------------------------------------------------------------
自己动手写一次重采样,再看库为什么强
--------------------------------------------------------------------

**① 最近邻:照着比例取点**

原始 6 个样本:[0, 100, 200, 300, 400, 500]

  做法       输出
  -----------------------------------------------------------------------
  升两倍     [0, 0, 100, 100, 200, 200, 300, 300, 400, 400, 500, 500]
  降一半     [0, 200, 400]

**最近邻就是「重复」与「丢弃」**,两行代码搞定,快得不得了。
**代价是台阶**——升采样出来的值是重复的,听着发毛。

**② 线性插值:在两点之间补**

  做法               输出
  --------------------------------------------------------------------------------
  线性插值升两倍     [0, 50, 100, 150, 200, 250, 300, 350, 400, 450, 500, 500]

**值不再是重复的,而是平滑过渡。** 代码也就十行,**这就是 C 语言版重采样的核心。**

**③ 库强在哪:不是插值,是抗混叠**

  做法                     升采样     降采样       抗混叠
  -------------------------------------------------------------
  最近邻                   重复       丢弃         完全没有
  线性插值                 平滑       抽点         没有
  swr(libswresample)     滤波       低通后抽     **有**

**降采样前必须先低通**:不滤直接抽,高频会「折」到低频去,变成一种听起来像金属声的杂音,
**这个现象叫混叠。**
**手写插值省不掉这一步,而库替你做了。**

**④ 库还替你做了两件事**

  库替你做的             表现
  -------------------------------------------------------
  滤波器状态跨帧保存     多帧连起来不会在接缝处爆音
  冲空包把存货吐出来     合计 1600 个样本,等于 800×2
  按帧边界对齐           输出帧长由它决定,不用你凑

**手写版最容易被忽略的是第一条**:每帧单独插值,接缝处会跳一下,听感是「每秒二十五次的咔哒」。
**这就是为什么真的做音频不能手写。**

最近邻的值是重复的,线性插值的值是过渡的,一眼就能看出差别。 库的样本数合计 1600,等于 800×2,但必须冲空包才凑齐。

30 秒实验

  1. 把插值公式里的 frac 全当 0(退化成最近邻),看结果与第一组是不是一样。
  2. 把 i1 改成 i0(不取下一个点),看输出变成什么。
  3. 用 8000→8000 转一次,看库是不是原样返回(提示:也会少几个样本)。

常见误区

  1. 以为手写插值就够了。 缺抗混叠,降采样会有杂音。
  2. 每帧单独算。 接缝处会咔哒,因为滤波器状态没跨帧保存。
  3. 把教材的 C 代码当生产代码。 它是教学代码,用库才对。
自测三问(点开对答案) 1. 最近邻重采样的本质是什么? —— 按比例取点,升采样是重复、降采样是丢弃。 2. 手写版最容易缺哪一样? —— 跨帧的滤波器状态,缺了会在接缝处爆音。 3. 既然有库,为什么还要手写一遍? —— 为了看懂下标、延迟、上下文各是什么,之后才用得对。

相关笔记