采音频与采视频是同一段代码,只换前缀 audio=。
但设备给什么格式由它自己定,所以采完第一件事是重采样。
12.2 采的是画面,这一节采声音。 第十章的重采样在这里第一次派上真实用场,因为设备不会迁就你。
一、音频设备怎么写
三个平台的写法,与视频那一张表几乎一样。 平台 格式名 设备名
Windows dshow audio=麦克风名 Linux alsa default 或 hw:0 macOS avfoundation :0
audio= 与 video= 是同一套前缀规则,只是接的设备类型不同。
所以「采音频」与「采摄像头」在代码上是同一段,只换前缀。
音频那半边还多一个设备格式:Linux 上视频走 v4l2、音频走 alsa,两条格式名不一样;
Windows 上两者都在 dshow 里,靠前缀区分。这套不对称是历史原因,只能记。
macOS 的 :0 不要看漏那个冒号:那一串是「视频序号:音频序号」,0:0 是全都要,
只写 :0 就是「不要视频、只要第 0 个音频」。
二、本机没有话筒
按标准写法试一次。 项 值
设备名 audio=麦克风 结果 OSError
结果与摄像头那一条一模一样,都是 OSError。 因为 dshow 这一层先要拿到设备句柄,拿不到就报 I/O 错,它不区分「没插」与「被占用」。 这条「不区分」是排查里最费时间的地方:开不了先关掉所有可能占用麦克风的程序(会议、 语音输入、浏览器标签),再试一次。很多「没设备」的报障其实是占用。
三、采到之后马上重采样
设备给的格式是它自己定的:常见是 48000 立体声 s16,但也可能是 44100、浮点、甚至 8 声道。 而你的处理链要的是统一格式。 项 进 出
格式 fltp s16 布局 stereo mono 采样率 44100 48000 样本数 480 523
480 变成 523 是算得出来的:480 × 48000 ÷ 44100 = 522.4,进一位就是 523。
比例是 160 比 147,不是整数倍。
不是整数倍就是麻烦的来源:整数倍可以整数拷贝,非整数倍每个输出样本都要在输入里插值,
这就是重采样要留状态、不能一帧一帧独立算的原因。
还有一个坑:调 resample(frame) 拿到的样本会比理论少几个,
因为重采样器内部要攒一点尾巴;
必须再调一次 resample(None) 把尾巴冲出来,总数才对得上 523。
沙箱没有麦克风,但这一段能真跑:拿合成的帧走一遍重采样,
证明「采到之后要做的处理」这条路是通的。
四、音频采集特有的三个坑
视频那头的坑是尺寸与格式,音频这头是时间。 坑 表现
设备被占用 打不开,报 I/O 错,与没设备一样 漂移与抖动 每帧样本数不固定,不能按时长切 首次打开慢 要等设备初始化,别放实时线程
第二条最要命:音频设备的时序由硬件时钟决定,不是标准帧长。
所以「按 1024 个样本一帧」这种假设在采集侧不成立。
这条与第十章的结论正好接上:文件里的 AAC 一帧固定 1024 个样本,
那是编码器定的;采集侧没有编码器,就没有固定帧长。
后果是时间要自己算:靠 sample_rate 与累计样本数推 pts,不能靠「第几帧 × 每帧时长」。
这也是直播里音画不同步最常见的根子。
五、采完往后接什么
采集只是入口,后面三步才是这条链的目的。 下一步 为什么
重采样成统一格式 后面所有处理只认一种格式 按时间打 pts 设备不给你标准时间基 编码或推流 那才是链的出口
真实直播链是「采→转→编→推」四步。 这一章只做到第一步,后面几步在第十三章。 四步里最容易被忽略的是第二步:很多人把设备给的格式直接丢给编码器,结果换个麦克风就崩。 加一层重采样,等于把「设备千奇百怪」挡在外面。 这也是一条通用的架构习惯:入口处做归一化,后面的代码就只需要认一种格式,测试也好写。
六、动手 Demo:话筒采集,以及采到之后马上要做的事
先试 audio=麦克风 看报错,再用合成帧走一遍 44100 立体声浮点到 48000 单声道 s16。
import numpy as np
import av
# 1) 标准写法:向 dshow 要麦克风
try:
av.open("audio=麦克风", format="dshow").close()
print("开成功")
except Exception as e:
print(type(e).__name__) # OSError:路在、没设备
# 2) 采到之后必做:归一到统一格式
r = av.AudioResampler(format="s16", layout="mono", rate=48000)
a = np.zeros((2, 480), dtype=np.float32) # 立体声、480 个样本
f = av.AudioFrame.from_ndarray(a, format="fltp", layout="stereo")
f.sample_rate = 44100
out = list(r.resample(f)) + list(r.resample(None)) # 别忘冲空包
print(out[0].format.name, out[0].layout.name, out[0].sample_rate)
print(sum(x.samples for x in out)) # 523
实跑输出:
--------------------------------------------------------------------
话筒采集,以及采到之后马上要做的事
--------------------------------------------------------------------
**① 音频设备的写法**
平台 格式名 设备名
------------------------------------------------
Windows dshow audio=麦克风名
Linux alsa default 或 hw:0
macOS avfoundation :0
**`audio=` 与 `video=` 是同一套前缀规则**,只是接的设备类型不同。
**所以「采音频」与「采摄像头」在代码上是同一段,只换前缀。**
**② 本机没有话筒设备**
项 值
---------------------------
设备名 audio=麦克风
结果 OSError
**注意这个结果与摄像头那一条一样**,都是 OSError。
**因为 dshow 这一层先要拿到设备句柄,拿不到就报 I/O 错。**
**③ 采到之后马上要重采样**
**设备给的格式是它自己定的**,常见是 48000 立体声 s16,但也可能是 44100 或浮点。
**而你的处理链要的是统一的格式。**
项 进 出
-------------------------------
格式 fltp s16
布局 stereo mono
采样率 44100 48000
样本数 480 523
**没有麦克风,但这一段能真跑**:拿合成的帧走一遍重采样,
**证明「采到之后要做的处理」这条路是通的。**
**④ 音频采集特有的三个坑**
坑 表现
------------------------------------------------------
设备被占用 打不开,报 I/O 错,与「没设备」一样
漂移与抖动 每帧样本数不固定,不能按时长切
首次打开慢 要等设备初始化,别放在实时线程里
**第二条最要命**:音频设备的时序由硬件时钟决定,**不是标准帧长。**
**所以「按 1024 个样本一帧」这种假设在采集侧不成立。**
**⑤ 采完往后接什么**
下一步 为什么
-------------------------------------------------
重采样成统一格式 后面所有处理只认一种格式
按时间打 pts 设备不给你标准时间基
编码或推流 那才是链的出口
**采集只是入口。** 真实直播链是「采→转→编→推」,**这一章只做到第一步**,后面几步在第十三章。
audio= 与 video= 同一套前缀,只是设备类型不同。
480 个 44100 的样本换成 48000 是 523 个(522.4 进一位),比例非整数。
30 秒实验
- 把
rate=48000改成rate=8000,算一下 480 个样本应该出多少个(提示:87.07)。 - 去掉
+ list(r.resample(None)),看总数少几个(提示:少的是攒在内部的尾巴)。 - 把
layout="mono"改成"stereo",看输出样本数变不变(提示:不变,变的是通道)。
常见误区
- 把设备给的格式直接喂编码器。 换个麦克风就崩,中间要加一层归一的。
- 按固定帧长切音频。 采集侧的帧长由硬件时钟决定,不固定。
- 以为重采样一次调用就出全。 要再冲一次空包,尾巴才出来。
自测三问(点开对答案)
1. 音频设备与视频设备的写法差在哪? —— 只差 `audio=` 与 `video=` 这个前缀,其余同一段。 2. 44100 的 480 个样本换成 48000 是多少个? —— 523 个(480×48000÷44100=522.4,进一位)。 3. 为什么采完要先重采样? —— 设备给什么格式由它定,归一之后后面只认一种格式。- ☐ (本节完,下一节 12.5 FFmpeg 读取网络视频流并解码为 YUV)