🎬 第五章 FFmpeg命令行实现音视频转码
📄 一句话概括

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 秒实验

  1. 命令行对照:ffmpeg -i 源.mp4 -f s32le 出.pcm,字节翻倍。
  2. 看排法:ffmpeg -i 源.mp4 -f f32le 与 -f f32planar 各来一份,字节一样。
  3. 量误差:-ar 48000 再 -ar 44100,用 astats 看最大差。

常见误区

  1. 以为升到 32 位浮点能提升音质。源是 16 位,多出来的字节全是零。
  2. 混淆 planar 与 packed。不报错,声音会变成先左后右的怪声。
  3. 把 f32le 写成 fltle。格式名与编码名是两套,写混了会说「没有这个封装」。
自测三问(点开对答案) 1. s16 转 s32,字节为什么翻倍而信息量不变? —— 每个点从 2 字节变 4 字节,但源本来只有 16 位精度,高 16 位全是零。 2. fltp 与 flt 的差别是什么? —— fltp 是 planar,一个声道一片内存;flt 是 packed,左右交错。字节一样。 3. 为什么量重采样误差要跳过开头? —— 开头有滤波器起始瞬态,不跳过会把瞬态当误差,结果偏大好几倍。

相关笔记