s16 换 s32 只是换写法,字节翻倍、信息一点没多。 planar 与 packed 的差别只在排法,转错不报错,只会让声音变成怪声。
声道、采样率都讲过了,这一节补上第三个参数:采样格式。 它决定「一个点用几个字节、用什么数表示」,也决定内存在怎么排。
一、四种格式,字节按位深翻倍
同一段声音,四种采样格式各存一份:
格式 全名 字节 每点字节
s16 16 位整数 356352 2 s32 32 位整数 712704 4 flt 32 位浮点 712704 4 dbl 64 位浮点 1425408 8
点数完全一样,字节按位深翻倍。这段素材本来就是从 16 位来的, 所以升到 32 位浮点只是把同样的值换了种写法,一个字节的新信息都没有。
那为什么还要有 32 位浮点?因为混音与滤镜要算加减乘除。 整数运算会削顶、会累积舍入误差,浮点中间结果用 32 位才好保住精度。 但这是「处理中」的格式,不是「交付」的格式。
二、planar 与 packed
同样一段立体声,两种排法:
格式 数组形状 排法
fltp 2×1024 一个声道一块 flt 1×2048 左右交错
字节总量一样,只是排法不同。名字末尾那个 p 就是 planar 的首字母。 fltp 是两个声道各占一片连续内存,flt 是左右左左地交错排。
转错这一层不会报错,只会让声音变成「先左后右」的怪声——因为解释方式错了。
所以看到 -ac 2 却又听到一半静音,先查 planar 与 packed 有没有搞反。
FFmpeg 内部几乎所有滤镜都用 planar 格式,因为按声道分片好做并行。
所以 fltp 出现得比 flt 频繁得多。
三、44100 转 48000 的误差
同一段 1 kHz 正弦波,走一趟 48000 再回来:
项 值
原始点数 44100 48000 时点数 47983 转回 44100 44085 最大绝对误差 0.01789
误差这一列是跳过开头两千点之后算的:前面那段是重采样的起始瞬态,不跳过会被它带偏。 这点要记住,否则同一段代码换个算法顺序,误差能差好几倍。
跳过之后误差很小——因为重采样器用的是带限插值,不是简单丢点补点。 但这一点点误差会累积:一次听不出,几十次之后高频就开始发毛。
所以「统一采样率」这件事,应该在流程开头做一次,不要在中间反复做。
四、动手 Demo:四种采样格式各存一份,再做一次来回重采样
把素材音频按 s16、s32、flt、dbl 各存一份裸 PCM,比字节与每点字节。 再看 fltp 与 flt 的数组形状,最后量一次 44100 与 48000 互转的误差。
import av, os
import numpy as np
FMT = {u"s16": u"pcm_s16le", u"s32": u"pcm_s32le",
u"flt": u"pcm_f32le", u"dbl": u"pcm_f64le"}
RAW = {u"s16": u"s16le", u"s32": u"s32le",
u"flt": u"f32le", u"dbl": u"f64le"}
def to_pcm(dst, fmt):
c1 = av.open(dst, u"w", format=RAW[fmt])
s1 = c1.add_stream(FMT[fmt], rate=44100)
s1.layout = u"stereo"
rs = av.AudioResampler(format=fmt, layout=u"stereo", rate=44100)
c0 = av.open(u"s5.mp4")
n = 0
for f in c0.decode(audio=0):
for rf in rs.resample(f):
n += rf.samples
for o in s1.encode(rf):
c1.mux(o)
for rf in rs.resample(None):
n += rf.samples
for o in s1.encode(rf):
c1.mux(o)
for o in s1.encode(None):
c1.mux(o)
c1.close()
c0.close()
return n, os.path.getsize(dst)
for fmt in (u"s16", u"s32", u"flt", u"dbl"):
n, b = to_pcm(u"fmt_%s.pcm" % fmt, fmt)
print(u"%-3s %d 点 %d 字节 每点 %d" % (fmt, n, b, b // (n * 2)))
a = np.vstack([np.zeros(1024), np.zeros(1024)]).astype(np.float32)
fp = av.AudioFrame.from_ndarray(np.ascontiguousarray(a),
format=u"fltp", layout=u"stereo")
fk = av.AudioFrame.from_ndarray(np.ascontiguousarray(a.T.reshape(1, -1)),
format=u"flt", layout=u"stereo")
print(u"fltp 数组 %dx%d" % fp.to_ndarray().shape)
print(u"flt 数组 %dx%d" % fk.to_ndarray().shape)
SR = 44100
t = np.arange(SR) / float(SR)
sig = (0.5 * np.sin(2 * np.pi * 1000 * t)).astype(np.float32)
fr = av.AudioFrame.from_ndarray(np.ascontiguousarray(sig.reshape(1, -1)),
format=u"fltp", layout=u"mono")
fr.sample_rate = SR
r1 = av.AudioResampler(format=u"fltp", layout=u"mono", rate=48000)
r2 = av.AudioResampler(format=u"fltp", layout=u"mono", rate=SR)
mid = r1.resample(fr)
back = r2.resample(mid[0]) + r2.resample(None)
out = np.concatenate([b.to_ndarray()[0] for b in back])
k = min(len(sig), len(out))
d = np.abs(sig[2000:k].astype(np.float64) - out[2000:k].astype(np.float64))
print(u"原始 %d 点 48000 后 %d 点 转回 %d 点"
% (len(sig), mid[0].samples, len(out)))
print(u"最大绝对误差 %.5f" % d.max())
实跑输出:
--------------------------------------------------------------------
5.5.3 采样格式转换及音频重采样
--------------------------------------------------------------------
**① 四种采样格式,同一段声音各存一份**
格式 全名 字节 每点字节
--------------------------------------------
s16 16 位整数 356352 2
s32 32 位整数 712704 4
flt 32 位浮点 712704 4
dbl 64 位浮点 1425408 8
**点数完全一样,字节按位深翻倍。这段素材本来就是从 16 位来的,**
**所以升到 32 位浮点只是把同样的值换了种写法,一个字节的新信息都没有。**
**② planar 与 packed 的区别**
格式 数组形状 排法
-----------------------------------
fltp 2×1024 一个声道一块
flt 1×2048 左右交错
**字节总量一样,只是排法不同。名字末尾那个 p 就是 planar 的首字母。**
**转错这一层不会报错,只会让声音变成「先左后右」的怪声——因为解释方式错了。**
**③ 44100 转 48000,来回一趟的误差**
项 值
-------------------------
原始点数 44100
48000 时点数 47983
转回 44100 44085
最大绝对误差 0.01789
**误差这一列是跳过开头两千点之后算的:前面那段是重采样的起始瞬态,不跳过会被它带偏。**
**跳过之后误差很小——因为重采样器用的是带限插值,不是简单丢点补点。**
**但这一点点误差会累积:一次听不出,几十次之后高频就开始发毛。**
**所以「统一采样率」这件事,应该在流程开头做一次,不要在中间反复做。**
裸流的格式名与编码名是两套:f32le 是格式名,pcm_f32le 是编码名。
av.open(..., format=u"fltle") 会报「没有这个封装」,因为格式名是 f32le。
AudioFrame.from_ndarray 对 planar 要二维(声道 × 点),
对 packed 要一维再 reshape 成 (1, 总点数)——形状错了会直接抛 ValueError。
误差要跳过开头再算,这是重采样测量的通用做法。 不跳过的话,起始瞬态会让误差看起来大好几倍。
30 秒实验
- 命令行对照:
ffmpeg -i 源.mp4 -f s32le 出.pcm,字节翻倍。 - 看排法:
ffmpeg -i 源.mp4 -f f32le与-f f32planar各来一份,字节一样。 - 量误差:
-ar 48000再-ar 44100,用astats看最大差。
常见误区
- 以为升到 32 位浮点能提升音质。源是 16 位,多出来的字节全是零。
- 混淆 planar 与 packed。不报错,声音会变成先左后右的怪声。
- 把
f32le写成fltle。格式名与编码名是两套,写混了会说「没有这个封装」。
自测三问(点开对答案)
1. s16 转 s32,字节为什么翻倍而信息量不变? —— 每个点从 2 字节变 4 字节,但源本来只有 16 位精度,高 16 位全是零。 2. fltp 与 flt 的差别是什么? —— fltp 是 planar,一个声道一片内存;flt 是 packed,左右交错。字节一样。 3. 为什么量重采样误差要跳过开头? —— 开头有滤波器起始瞬态,不跳过会把瞬态当误差,结果偏大好几倍。- ☐ 5.5 完,下一节 5.6.1 控制视频的帧率