把声音变成数字 — 采样率·帧·dBFS,以及混叠进来的声音
一句话总结
麦克风给出的,是以固定间隔测得的空气压力的数字序列(PCM)。每秒测了多少次是采样率,一个数字的大小是位数。语音流水线中所有部件,都是把这个数字序列切成20 ms 左右的帧来相互传递的。改变采样率时如果不先滤波,就会出现原本没有的声音;强度必须用 dBFS 来衡量,才能用一个阈值把静音和噪声区分开。
为什么需要它
第一次做语音助手时,人们会先去挑模型。但在现场最先爆发的事故,并不出在模型上,而是出在声音的形态上。浏览器以 48 kHz 录音,电话网以 8 kHz 传输,本课程的识别模型接收 16 kHz,合成模型输出 22.05 kHz。这四个数字在一次通话中全都会出现。只要有一处把采样率写错,声音就会快一倍,识别率变成 0(在模块 3 中会实际测量——把 8 kHz 谎称为 16 kHz,WER 就是 100%),字节序弄错的话,就只剩下杂音。不会报错。因为数字依然是数字。
工作原理
采样率与奈奎斯特。每秒测 fs 次,就只能表示不超过 fs/2 Hz 的成分(采样定理)。以 16 kHz 测量,就是到 8 kHz 为止。人类语音中对听懂最重要的成分大多在这个频率以下,所以语音识别模型把 16 kHz 作为标准。有线电话(ITU-T G.711)以 8 kHz 测量,只能传送 300–3,400 Hz——这就是电话里的声音听起来发闷的原因。
折叠(混叠,aliasing)。从 48 kHz 降到 16 kHz 时,如果只是每三个取一个,原本在 8 kHz 以上的成分不会消失,而是向下折叠进来。12 kHz 的成分会出现在 16 − 12 = 4 kHz 处。这是原本没有的哔声。所以在抽取之前,要先加上一个在低于新奈奎斯特频率(8 kHz)的位置截断的低通滤波器。为了让人观察这一点,实验的原始文件里混入了 12 kHz 的导频音(-26 dBFS)。直接抽取,4 kHz 处会出现 -29 dBFS;用 127 抽头窗口 sinc 滤波器滤波之后再抽取,则降到 -65 dBFS(在这个镜像中的实测值)。反过来,把 8 kHz 升到 16 kHz,并不会产生 4 kHz 以上的声音——原本没有的信息是不会凭空出现的。
位深与字节序。16 位 PCM 是 -32768 到 32767 的整数。计算时用除以 32768 之后的 [-1, 1) 实数,文件和网络中则以整数书写。字节通常是小端优先(little-endian),所以格式名称是 s16le。把两个声道混成单声道时,要取平均。如果只是相加,两个声道都是 0.8 时就会变成 1.6 而被截断(削波,clipping)。
帧与强度。声音以 20 ms(16 kHz 下是 320 个采样点,640 字节)为单位切开处理。人类语音的性质在几十 ms 内几乎保持不变,实时传输(WebRTC 的 Opus 等)也常用 20 ms。帧的强度,是把 RMS(平方平均的平方根)写成 dBFS——把最大值(1.0)定为 0 dB 的对数刻度:20·log10(RMS)。-40 dBFS 是最大值的 1%,-60 dBFS 是 0.1%。因为乘法变成了加法,所以可以像“说话在 -20 附近,房间噪声在 -50 附近”这样,用一个阈值把它们分开。
SNR。信噪比是功率之比:10·log10(P_信号 / P_噪声)。如果写成振幅比,则是 20·log10。把两者混用,本想加入 10 dB,结果却加入了 5 dB 或 20 dB——实验中的反例就是这个。
在现场相遇的样子
本课程与实时通信课程的分界就在这里。那门课程负责把浏览器麦克风的声音通过 WebSocket 送到服务器,而本课程则从“16 kHz · 单声道 · s16le · 20 ms 帧到达”这个约定开始。约定的四项中只要有一项错位,接收方就会在没有任何错误的情况下听到离谱的声音。所以,即使不在每条消息里携带格式,最好也在打开连接时互相交换一次来确认。
LabHub 的语言会话练习功能用的是相反的方式——用浏览器 MediaRecorder 把整句话完整录下来再发送(backend/app/lang_talk.py,static/lang/talk.js)。把一次发送的录音上限定为“15 秒 × 16 kHz × 16 位再留出余量的 2 MB”,也是出自这个计算。整段发送时实现很简单,但人说完话之后才开始识别。分成帧发送的原因,就是为了消除这段等待时间(模块 3、8)。
帧的大小也存在权衡。帧越小,收集一个片段所需的时间越少(20 ms 的帧就是 20 ms),但消息数量会增加,报头和系统调用所占的比重就变大。反过来,如果加大到 100 ms,消息会减少为五分之一,但所有环节接收声音都会至少晚 100 ms。本课程的流式 ASR 是以 100 ms 的片段接收的——相当于把五个 WebSocket 帧攒在一起一次输入。
下一项实验要做什么
读取 48 kHz 立体声原始文件的头部,混成单声道,不滤波直接降到 16 kHz 并测量 4 kHz 的折叠,再用低通滤波器正确地降采样。计算 20 ms 帧的 dBFS 来找出静音区间,把固定了种子的噪声以 SNR 10 dB 混入,然后把声音切成 WebSocket 会传送的 640 字节帧并保存。