把一条 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 秒实验
- 把目标改成 16000,看输出样本数与时长(提示:时长仍 2.04 秒)。
- 把目标改成
format="flt",看落盘字节数变成几倍。 - 不冲空包跑一遍,看样本数少多少。
常见误区
- 只看样本数不看时长。 两条都要对。
- 只给 rate 不给 format 与 layout。 声道会悄悄变成默认值。
- 来回转多次求「无损」。 每次降采样都有损失。
自测三问(点开对答案)
1. 90112 个 44.1k 样本转到 48k 该有多少? —— 约 98082 个,比例就是 48000 ÷ 44100。 2. 为什么样本数不是精确的 98083.8? —— 样本必须是整数个,必然要在某处取整。 3. 落盘裸 PCM 有什么用? —— 字节数能手算核对,是验收音频处理最快的办法。- ☐ (本节完,下一节 10.3.3 Packed模式转Planar模式)