重采样器不只管采样率,它也是「换存法」的官方工具。 采样率一个没改、样本一个没少,只把摆法换了。
10.2.2 讲了两种摆法的差别,10.3.2 讲了转采样率,这一节把两件事合起来。 这是第十章最后一节。
一、只换存法,别的都不动
s16 packed 转 s16p planar,参数只改一个字母。 项 进 出
样本格式 s16 s16p 存法 packed planar shape (1, 32) (2, 16) 平面数 1 2 字节 64 64
采样率没改、样本数没少、字节数没变,只把 32 个数字换了位置。所以这一步几乎不花时间。
名字上的差别只有一个 p,但数组形状从一维变成二维——代码里读法要跟着改,否则会读错声道。
二、真实音轨:planar 浮点 → packed 整数
这是最常见的实际需求。 项 进 出
格式 fltp s16 存法 planar packed shape (2, 1024) (1, 2048) 值域 −1.0 到 1.0 −32768 到 32767
解码器给 fltp,落盘交付要 s16,这一步就是靠重采样器做的。
它顺手把精度也换了(浮点转整数,带舍入与限幅)。
所以「转存法」这一步往往顺手就把精度也转了,
两件事在同一次调用里完成,不需要中间再折腾一次。
三、为什么内部偏爱 planar
三条理由,都是性能层面的。 理由 说明
编码器按声道算 声道分开存,不用来回拆 滤波按声道做 内存连续,预取有效 混音矩阵好写 每个声道一个指针
这是「内部用 planar、对外给 packed」的分工。解码器给 planar、滤镜要 planar、 编码器内部按 planar 算;而落盘成 WAV 或裸 PCM、送硬件,多是 packed。 所以真实链路里几乎一定有一次转换,只是它通常藏在你调的某个函数肚子里,你看不见。
四、不转存法会怎样
不转不会崩,但会读错。 场合 后果
按 packed 读 planar 读到的是两个声道拼一起的错位数据 按 planar 读 packed 第二个平面越界或为空 送编码器时格式不对 可能报错,也可能悄悄降质
第一行的表现是「声音变闷或有回声」,因为左右声道错开了几个样本。
这种错很难联想到「存法搞错了」,所以排查时容易走远。
最省事的排查动作:在链的每一段打印 format.name 与 shape,
两头一对比,就知道是哪一步换的。
五、第十章的账
四句话结掉这一章。 记住的 一句话
时间怎么算 样本数除以采样率,没有第二个公式 两种摆法 packed 按时间、planar 按声道 重采样要冲空包 不冲就丢几毫秒,还不报错 它不只转采样率 格式、布局、采样率三样一起转
四句里有两句是「不报错」的事:冲空包、格式被改。 音频的坑大多属于这一类——听不出、看不见、也不报。 第十一章换到画面那头的「换尺寸与换颜色」,要处理的还是格式与刻度,只是从一维变成二维。
六、动手 Demo:顺带把 packed 转成 planar
先只改存法不改采样率,再把真实音轨从 planar 浮点转成 packed 整数。
import av
# 只换存法:s16 packed -> s16p planar,采样率不动
f = av.AudioFrame(format="s16", layout="stereo", samples=16)
f.sample_rate = 8000
r = av.AudioResampler(format="s16p", layout="stereo", rate=8000)
o = r.resample(f)[0]
print(f.format.name, f.to_ndarray().shape, len(f.planes))
print(o.format.name, o.to_ndarray().shape, len(o.planes))
print(f.to_ndarray().nbytes, o.to_ndarray().nbytes)
# 真实音轨:fltp planar -> s16 packed
c = av.open("tiny.mp4")
st = c.streams.audio[0]
for pk in c.demux(st):
if pk.size == 0:
break
fs = st.codec_context.decode(pk)
if not fs:
continue
src = fs[0]
r2 = av.AudioResampler(format="s16", layout="stereo", rate=44100)
got = list(r2.resample(src)) + list(r2.resample(None))
g = got[0]
print(src.format.name, src.to_ndarray().shape)
print(g.format.name, g.to_ndarray().shape)
break
c.close()
实跑输出:
--------------------------------------------------------------------
顺带把 packed 转成 planar
--------------------------------------------------------------------
**① 重采样器不只管采样率**
输入:s16(packed)立体声,shape (1, 32)
项 进 出
------------------------------------
样本格式 s16 s16p
存法 packed planar
shape (1, 32) (2, 16)
平面数 1 2
字节 64 64
**采样率一个没改、样本数一个没少,只把摆法换了。**
**所以「重采样器」这个名字其实窄了**,它是音频格式转换器。
**② 真实音轨:planar 浮点 → packed 整数**
项 进 出
---------------------------------------------
格式 fltp s16
存法 planar packed
shape (2, 1024) (1, 2048)
值域 −1.0 到 1.0 −32768 到 32767
**解码器给的是 fltp,编码器往往要 s16**,中间这一步就是靠重采样器做的。
**它顺手把精度也换了**(浮点转整数,带舍入与限幅)。
**③ 为什么内部偏爱 planar,交货却给 packed**
理由 说明
----------------------------------------------------
AAC/MP3 内部按声道算 声道分开存,不用来回拆
滤波按声道做 一块内存连续,缓存友好
混音矩阵好写 每个声道一个指针
**这是「内部用 planar、对外给 packed」的分工。**解码器给 planar、滤镜要 planar、
编码器内部也按 planar 算;**而落盘成交付文件(WAV 与裸 PCM)与送硬件,多是 packed。**
**所以真实链路里几乎一定有一次「planar→packed」或反过来**:
**它不在你写的代码里,而在某个封装函数的肚子里。**
**④ 这一章的账**
记住的 一句话
-----------------------------------------------------------
时间怎么算 样本数除以采样率,没有第二个公式
两种摆法 packed 按时间、planar 按声道
重采样要冲空包 不冲就丢几毫秒,还不报错
它其实不只转采样率 格式、布局、采样率三样一起转
**四句话结完账。** 第十一章换到画面那头的「换尺寸与换颜色」,
**处理的还是格式与刻度,只是从一维变成二维。**
采样率没动、字节数一样(64 对 64),只是 shape 从 (1,32) 变 (2,16)。 真实音轨是 fltp → s16,它顺手把精度也换了。
30 秒实验
- 把目标改成
format="s16p"(同精度换存法),看真实音轨那一步的输出 shape。 - 把目标
layout改成mono,看字节数变成几分之一。 - 对比转前转后的时长,确认没变(提示:
samples / rate)。
常见误区
- 以为换存法会损失音质。 字节原样搬,只是位置变了。
- 按 packed 的形状读 planar。 读到的是错位数据,听感是回声。
- 手工 reshape 转存法却忘了改精度。 该转的时候还是让库转。
自测三问(点开对答案)
1. 只改存法时,哪三样东西不变? —— 采样率、样本数、字节数。 2. 真实音轨解码后是什么存法? —— planar(fltp),而落盘交付多用 packed s16。 3. 按 packed 读 planar 会听到什么? —— 声音变闷或有回声,因为两个声道错位了。- ☐ (本节完,第十章已完结,下一章 11.1 色彩空间转换的原理简介)