转声道不改时间轴,只改「每个点存几份」:立体声转单声道字节正好一半。 这一步不可逆,两路合到一起就再也分不开。
前面几节动的都是「编码」与「容器」,这一节开始动采样参数。 声道、采样率、采样格式这三样,是音频转码里最常被要求改的东西。
一、字节正好一半,点数一个不少
同一段 2 秒素材,转成单声道:
声道布局 采样点 字节 每点字节
stereo 89088 356352 4 mono 89088 178176 2
点数一模一样,字节正好一半——转声道不改时间轴,只改「每个点存几份」。 立体声每点存两个数(左、右),单声道每点存一个。
这一步不可逆:两路声音被合到了一起,想再分开是办不到的。 所以「先转单声道省点空间,以后需要再转回来」这种想法是错的。
二、下混到底怎么合
两路合成一路,默认做法是取平均:
比较 结果
单声道与两路平均 最大差 1 左声道与右声道 最大差 11
第一行最大差只有 1,说明默认的下混就是「两路取平均」,误差只来自舍入。 这也回答了一个常见疑问:下混会不会让声音变小?不会,它是平均不是相加。
第二行说明这段素材的左右本来几乎一样,差 11 是 AAC 编解码带来的。 换成真正的立体声素材,第二行会是一个很大的数——那时候「取平均」就会把声场压塌。
要保住声场得用加权下混,或者干脆保留立体声。命令行里 -ac 1 走的就是平均。
三、反向:单声道升立体声
把单声道再转回立体声:
项 值
输入点数 89088 输出点数 89088 字节 356352(原 178176)
字节翻倍,点不变。多出来的那一半是「同一个内容抄了两份」,不是新的信息。 所以这一步唯一的作用是兼容那些只认立体声的播放器。
想从单声道「恢复」出立体声是不可能的,原始的两路差值在上一步就被平均掉了。 这也是为什么专业流程里,素材从一开始就要留立体声原件。
四、动手 Demo:立体声转单声道,再转回去
先把素材音频解成裸 PCM,分别按立体声与单声道存一份,比点数与字节。 再读回单声道,按「两路取平均」算一遍,看它与真下混的结果差多少。
import av, os
import numpy as np
def to_pcm(dst, layout, src=u"s5.mp4"):
c1 = av.open(dst, u"w", format=u"s16le")
s1 = c1.add_stream(u"pcm_s16le", rate=44100)
s1.layout = layout
if src.endswith(u".pcm"):
c0 = av.open(src, u"r", format=u"s16le",
options={u"ar": u"44100", u"ch_layout": u"mono"})
rs = None
else:
c0 = av.open(src)
rs = av.AudioResampler(format=u"s16", layout=layout, rate=44100)
n = 0
for f in c0.decode(audio=0):
frames = rs.resample(f) if rs is not None else [f]
for rf in frames:
n += rf.samples
for o in s1.encode(rf):
c1.mux(o)
if rs is not None:
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)
n1, b1 = to_pcm(u"st1.pcm", u"stereo")
n2, b2 = to_pcm(u"mo1.pcm", u"mono")
print(u"stereo %d 点 %d 字节" % (n1, b1))
print(u"mono %d 点 %d 字节" % (n2, b2))
st = np.frombuffer(open(u"st1.pcm", u"rb").read(), dtype=np.int16)
st = st.reshape(-1, 2)
mo = np.frombuffer(open(u"mo1.pcm", u"rb").read(), dtype=np.int16)
avg = (st[:, 0].astype(np.int32) + st[:, 1]) // 2
print(u"与两路平均的最大差 %d" % int(np.abs(mo.astype(np.int32) - avg).max()))
print(u"左右声道最大差 %d"
% int(np.abs(st[:, 0].astype(np.int32) - st[:, 1]).max()))
n3, b3 = to_pcm(u"st2.pcm", u"stereo", u"mo1.pcm")
print(u"升回立体声 %d 点 %d 字节" % (n3, b3))
实跑输出:
--------------------------------------------------------------------
5.5.1 单声道与立体声互转——省一半,还是丢一半
--------------------------------------------------------------------
**① 立体声转单声道**
声道布局 采样点 字节 每点字节
--------------------------------------------
stereo 89088 356352 4
mono 89088 178176 2
**点数一模一样,字节正好一半——转声道不改时间轴,只改「每个点存几份」。**
**这一步不可逆:两路声音被合到了一起,想再分开是办不到的。**
**② 立体声转单声道时,两路怎么合**
比较 结果
-------------------------------
单声道与两路平均 最大差 1
左声道与右声道 最大差 11
**第一行最大差只有 1,说明默认的下混就是「两路取平均」,误差只来自舍入。**
**第二行说明这段素材的左右本来几乎一样(差 11 是 AAC 编解码带来的),所以下混几乎不损失。**
**换成真正的立体声素材,第二行会是一个很大的数——那时候「取平均」就会把声场压塌。**
**③ 反向:单声道转立体声**
项 值
---------------------------------
输入点数 89088
输出点数 89088
字节 356352(原 178176)
**字节翻倍,点不变。多出来的那一半是「同一个内容抄了两份」,不是新的信息。**
**所以「把单声道升成立体声」这种需求,除了兼容播放器,没有别的意义。**
AudioResampler 名字带 resample,但它管的远不止采样率:
声道布局、采样格式也在它的活里,一次调用三样一起改。
读裸 PCM 的 ch_layout 一定写 mono 或 stereo 这样的名字,
写数字 1、2 不认,还不报错——这一坑在 5.3.1 里撞过一次。
最后一步的 st2.pcm 是「单声道升立体声」的结果,字节翻倍、点数不变。
拿它跟 st1.pcm 比会发现内容不同:差值已经在第一步被平均掉了。
30 秒实验
- 命令行对照:
ffmpeg -i 源.mp4 -ac 1 出.wav,字节正好减半。 - 看布局:
ffprobe -show_streams,找channel_layout那一行。 - 保留立体声但只留左声道:
-af pan=stereo|c0=c0|c1=c0,两路都填左。
常见误区
- 以为转单声道以后再转回来能恢复。平均掉的差值找不回来。
- 以为下混会让音量变小。默认是平均,不是相加,音量基本不变。
- 把
ch_layout写成数字。PyAV 不认数字,且静默按单声道读。
自测三问(点开对答案)
1. 立体声转单声道,字节为什么正好减半? —— 每点从两个数变成一个数,点数不变,所以字节正好一半。 2. 默认的下混算法是什么? —— 两路取平均。实测与手算平均的最大差只有 1,误差来自舍入。 3. 单声道升立体声有意义吗? —— 只有兼容意义。多出来的一半是同一份内容抄两遍,不是新信息。- ☐ (本节完,下一节 5.5.2 采样率转换)