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

packed 隔一个取一个,planar 直接拿平面。 分离与合并在数组层面都是「改变形状」,不涉及运算。

📝 本节和前面几节的关系

10.2.2 讲了两种摆法,这一节讲怎么从里面把声道拆出来。 10.3.3 会讲怎么用重采样器整批地转存法。

一、packed:隔一个取一个

交错存的时候,左右是轮流出现的。 取什么 写法 结果


左 a[0, 0::2] 1 3 5 7 9 11 右 a[0, 1::2] 2 4 6 8 10 12

0::2 的意思是「从第 0 个起,每隔一个取」,1::2 从第 1 个起隔一个取。 C 里对应的是一个步长为 2 的循环:for (i = 0; i < n; i += 2)。 Python 的切片写法把那个循环藏起来了,语义完全一样。

二、planar:直接拿平面

分平面存的时候,声道本来就是分开的。 取什么 写法 结果


左 b[0] 100 101 102 103 104 105 右 b[1] 200 201 202 203 204 205

一个切片都不用,直接取下标。 所以 planar 上「分离声道」是零成本的——它本来就已经分开了。 这也解释了为什么内部处理都用 planar: 声道相关的操作在 planar 上都是「拿一块」,在 packed 上都是「跨步挑」。

三、真实音轨上做一次

AAC 解出来是 planar 的浮点,左右直接两行拿到。 项 值


样本格式 fltp 布局 stereo shape (2, 1024) 取左 ra[0] 取右 ra[1]

值是 0 点几的小数,范围 −1.0 到 1.0,不是整数。这是浮点 PCM 的量级,别拿它当 int16 读。 另外注意:前几个样本往往全是 0.0(编码器开头有静音), 从 0 里看不出任何东西,要看数据得跳到有声的位置。 这是看音频数据的一条实用习惯。

四、合回去

分离的逆运算,也是改形状。 从哪到哪 怎么做


两个单声道→立体声 planar 堆成 (2, n) 两个单声道→立体声 packed 先堆再转置拉平 立体声→两个单声道 切片或取平面

这三行全都是 reshape 与转置,没有一个乘法。 所以「分离声道」这件事本身不花钱。 真正要算的是「混音」与「声道重映射」:把 5.1 压成立体声, 得按权重把几个声道加起来——那才是重采样器的活。

五、教材用 C 写的重点

C 版的重点不在算法,在指针。 C 里要处理的 坑在哪


平面指针数组 stereo 有两个指针,别当连续内存 每平面的字节数 packed 是总字节,planar 是每声道字节 越界与对齐 步长算错就写坏内存

这三条在 Python 里都被 numpy 挡住了,所以这里的代码看着简单。 但看懂 C 版能让你明白:为什么 planar 的「每平面大小」要单独算。 沙箱里没有编译器,这一节的 C 代码只能读、不能跑, 所以用 Python 演示同一套下标逻辑,把「算法」与「内存管理」分开。

六、动手 Demo:分离左右声道,两种存法两种写法

造 packed 与 planar 各一帧,用两种写法分离;再到真实音轨上取一次。

import numpy as np
import av

# packed:隔一个取一个
a = av.AudioFrame(format="s16", layout="stereo", samples=6)
a.sample_rate = 8000
arr = a.to_ndarray()
print(arr.shape)
print(arr[0, 0::2].tolist(), arr[0, 1::2].tolist())

# planar:直接拿平面
b = av.AudioFrame(format="s16p", layout="stereo", samples=6)
b.sample_rate = 8000
brr = b.to_ndarray()
print(brr.shape)
print(brr[0].tolist(), brr[1].tolist())

# 真实音轨:跳到有声的位置看
c = av.open("tiny.mp4")
st = c.streams.audio[0]
for pk in c.demux(st):
    if pk.size == 0:
        break
    for fr in st.codec_context.decode(pk):
        d = fr.to_ndarray()
        if np.abs(d).max() > 0.01:
            i = int(np.argmax(np.abs(d).max(axis=0)))
            print(fr.format.name, d.shape, i)
            print(d[0][i:i+3], d[1][i:i+3])
            raise SystemExit

实跑输出:

--------------------------------------------------------------------
分离左右声道,两种存法两种写法
--------------------------------------------------------------------

**① packed:隔一个取一个**

  取什么     写法           结果
  --------------------------------------------------
  左         a[0, 0::2]     [1, 3, 5, 7, 9, 11]
  右         a[0, 1::2]     [2, 4, 6, 8, 10, 12]

**`0::2` 的意思是「从第 0 个起,每隔一个取」**,
**`1::2` 从第 1 个起隔一个取。** 这就是 packed 的分离法。

**② planar:直接拿平面**

  取什么     写法     结果
  ------------------------------------------------------
  左         b[0]     [100, 101, 102, 103, 104, 105]
  右         b[1]     [200, 201, 202, 203, 204, 205]

**planar 里左右本来就是分开的,直接取下标就行**,一个切片都不用。
**所以「分离声道」这件事在 planar 上是零成本的。**

**③ 真实音轨上做一次**

  项                   值
  ---------------------------------------------------------
  样本格式             fltp
  布局                 stereo
  shape                (2, 1024)
  平面数               2
  最响处第几个样本     676
  左声道那四个         [-0.305, -0.305, -0.304, -0.301]
  右声道那四个         [-0.305, -0.305, -0.304, -0.301]

**解出来是 planar 的浮点帧**,左右两声道直接 `ra[0]` 与 `ra[1]` 拿到。
**注意值是 0 点几的小数**——32 位浮点的音频就是这个量级,**范围是 −1.0 到 1.0**,不是整数。

**两个声道前几个样本都是 0.0,所以上面取的是「最响那个位置」。**
**这是看音频数据时的一个实用习惯**:静音段所有声道都是 0,
**从 0 里看不出任何东西,得跳到有声的地方看。**

**④ 合回去**

  从哪到哪                     怎么做
  ------------------------------------------------
  两个单声道→立体声 planar     直接堆成 (2, n)
  两个单声道→立体声 packed     先堆再转置拉平
  立体声→两个单声道            切片或取平面

**分离与合并在数组层面都是「改变形状」,不涉及任何运算。**
**真正要算的是「混音」与「声道重映射」**,那是重采样器的工作。

packed 用 0::2 与 1::2,planar 用 [0] 与 [1],两种写法。 前几个样本常是 0.0,要看数据得先找到最响的位置。

30 秒实验

  1. 把 planar 两平面相减,看左比右大还是小(提示:看波形的差)。
  2. 把两平面相加再除以 2,得到的是什么(提示:一种混音)。
  3. 用 np.stack([l, r]) 把两个单声道合成立体声 planar,验证 shape。

常见误区

  1. 在 packed 上按 [0] 与 [1] 取声道。 那样取到的是两个样本,不是两个声道。
  2. 在 planar 上用 0::2。 planar 每平面本身已经是单个声道。
  3. 以为分离声道要重新编码。 它只是改形状,不涉及重采样。
自测三问(点开对答案) 1. packed 立体声怎么取左声道? —— 从第 0 个起隔一个取,Python 里是 `a[0, 0::2]`。 2. planar 立体声的 shape 是什么? —— (2, n),第一个下标是声道。 3. 为什么看真实音频要先找「最响的位置」? —— 开头常是静音,全是 0,看不出任何信息。

相关笔记