语音识别前端系统化实战 - 第2节
音频数字化原理:从模拟到数字的桥梁
做语音识别,第一步就是理解声音怎么变成计算机能处理的数据。说白了,声音是连续的波,计算机只能处理离散的数字。这个转换过程,就是音频数字化。
我记得刚接触这个领域时,总觉得不就是录音嘛,有什么难的?直到有一次,我采集的语音数据在识别时总是出现奇怪的噪声,排查了半天才发现是采样率设置出了问题。从那以后,我再也不敢小看这三个核心参数了。
采样率:每秒拍多少张“照片”
采样率,就是每秒钟对声音信号采样的次数。单位是 Hz(赫兹)。
根据奈奎斯特采样定理:采样率必须大于信号最高频率的两倍,才能完整还原信号。
核心公式: 采样率 ≥ 2 × 信号最高频率
人耳能听到的频率范围是 20Hz ~ 20kHz。所以 CD 音质的采样率是 44.1kHz,刚好覆盖人耳范围。
语音识别场景下,我们关心的是人声。人声的主要能量集中在 300Hz ~ 3.4kHz。所以电话语音用 8kHz 就够了。但为了更好的识别效果,我一般建议用 16kHz。
| 采样率 | 适用场景 | 我的建议 |
|---|---|---|
| 8kHz | 电话语音、低带宽通信 | 不推荐用于语音识别 |
| 16kHz | 语音识别、语音助手 | 首选,平衡质量与性能 |
| 44.1kHz | 音乐、高保真录音 | 语音识别没必要,浪费带宽 |
| 48kHz | 视频配音、专业音频 | 除非有视频同步需求 |
我的经验: 做前端语音识别,16kHz 是黄金采样率。既能保证识别精度,又不会让音频数据太大。我曾经试过用 44.1kHz 采集,结果网络传输时延迟明显增加,识别效果并没有提升。
位深度:每个样本的“精度”
位深度决定了每个采样点能记录多少种音量等级。16 位就是 2^16 = 65536 个等级,8 位只有 256 个等级。
你想想看,如果位深度太低,音量变化就会变得“阶梯状”,听起来就是失真。语音识别对位深度没那么敏感,但太低会影响信噪比。
| 位深度 | 动态范围 | 推荐场景 |
|---|---|---|
| 8 位 | 48dB | 极低带宽,不推荐 |
| 16 位 | 96dB | 语音识别标准配置 |
| 24 位 | 144dB | 专业录音,语音识别过剩 |
| 32 位浮点 | 理论无限 | 后期处理,前端不常用 |
避坑指南: 我曾经在项目里用了 8 位深度采集,结果背景噪声和语音信号混在一起,识别率直接掉了 20%。位深度太低,量化噪声会淹没弱音信号。语音识别至少用 16 位。
声道:单声道还是立体声?
声道数就是同时记录的音频通道数量。单声道(Mono)1 个通道,立体声(Stereo)2 个通道。
语音识别用单声道就够了。为什么?因为语音识别只关心声音的内容,不关心声场定位。立体声的数据量是单声道的两倍,处理起来更慢,对识别结果没有帮助。
我的习惯: 所有语音识别项目,一律用单声道 + 16kHz + 16 位。这个组合经过了无数次验证,稳定可靠。
浏览器获取麦克风权限:getUserMedia API
前端做语音识别,第一步就是拿到麦克风的数据。浏览器提供了 navigator.mediaDevices.getUserMedia 这个 API。
这个 API 其实很简单,但坑不少。我刚开始用的时候,经常遇到权限被拒绝、浏览器不兼容、设备选择错误等问题。
基础用法
async function getMicrophone() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
sampleRate: 16000,
channelCount: 1,
echoCancellation: true,
noiseSuppression: true
}
});
return stream;
} catch (err) {
console.error('麦克风获取失败:', err);
throw err;
}
}
这里有几个关键点:
- sampleRate: 16000 — 强制使用 16kHz 采样率
- channelCount: 1 — 单声道
- echoCancellation: true — 开启回声消除,避免扬声器声音被麦克风再次采集
- noiseSuppression: true — 开启降噪,减少环境噪声
注意: 浏览器不一定会完全遵守你设置的 sampleRate。有些浏览器会忽略这个参数,使用默认采样率。所以拿到音频流后,最好检查一下实际的采样率。
权限处理
用户第一次访问页面时,浏览器会弹出权限请求框。如果用户拒绝,后续调用会直接抛错。
我建议在页面加载时就检测权限状态,而不是等到用户点击“开始录音”时才去请求。这样可以提前引导用户开启权限。
async function checkMicrophonePermission() {
const result = await navigator.permissions.query({ name: 'microphone' });
if (result.state === 'denied') {
// 引导用户去浏览器设置中开启权限
showPermissionGuide();
}
result.onchange = () => {
if (result.state === 'granted') {
// 权限已开启,可以初始化录音
initRecording();
}
};
}
我的经验: 在移动端 Safari 上,getUserMedia 必须在用户手势(如点击按钮)的上下文中调用,否则会失败。PC 端 Chrome 也有类似限制。所以一定要把录音按钮放在用户能点击的位置。
音频流处理基础
拿到 MediaStream 之后,怎么把它变成我们能处理的音频数据?这里要用到 AudioContext 和 ScriptProcessorNode(或者更现代的 AudioWorklet)。
AudioContext 与音频管道
AudioContext 是 Web Audio API 的核心。它创建了一个音频处理图,你可以把麦克风输入、音频文件、合成声音等作为“源”,经过各种“节点”处理,最终输出到扬声器或进行分析。
从 MediaStream 到 PCM 数据
下面是一个完整的音频采集示例,把麦克风数据转换成 Float32Array:
class AudioCapture {
constructor() {
this.audioContext = null;
this.stream = null;
this.processor = null;
this.onAudioData = null;
}
async start() {
// 1. 获取麦克风流
this.stream = await navigator.mediaDevices.getUserMedia({
audio: {
sampleRate: 16000,
channelCount: 1,
echoCancellation: true,
noiseSuppression: true
}
});
// 2. 创建 AudioContext
this.audioContext = new AudioContext({
sampleRate: 16000
});
// 3. 创建源节点
const source = this.audioContext.createMediaStreamSource(this.stream);
// 4. 创建处理节点
const bufferSize = 4096;
this.processor = this.audioContext.createScriptProcessor(bufferSize, 1, 1);
// 5. 处理音频数据
this.processor.onaudioprocess = (event) => {
const inputData = event.inputBuffer.getChannelData(0);
// inputData 是 Float32Array,范围 -1 到 1
if (this.onAudioData) {
this.onAudioData(inputData);
}
};
// 6. 连接节点
source.connect(this.processor);
this.processor.connect(this.audioContext.destination);
}
stop() {
if (this.processor) {
this.processor.disconnect();
}
if (this.stream) {
this.stream.getTracks().forEach(track => track.stop());
}
if (this.audioContext) {
this.audioContext.close();
}
}
}
核心要点: onaudioprocess 回调中拿到的 inputData 就是我们要的音频数据。它是一个 Float32Array,每个元素代表一个采样点的振幅,范围在 -1 到 1 之间。
bufferSize 的选择
bufferSize 决定了每次回调返回的采样点数量。常见值有 256、512、1024、2048、4096。
| bufferSize | 延迟 | CPU 占用 | 推荐场景 |
|---|---|---|---|
| 256 | 低 | 高 | 实时性要求极高 |
| 1024 | 中 | 中 | 语音识别,平衡之选 |
| 4096 | 高 | 低 | 非实时处理 |
我的建议: 语音识别场景,bufferSize 用 1024 或 2048。太小了 CPU 扛不住,太大了延迟明显。我曾经试过 256,结果移动端直接卡死。
关于 AudioWorklet
ScriptProcessorNode 虽然好用,但它是在主线程运行的,可能会阻塞 UI。Chrome 已经推荐使用 AudioWorklet,它在独立线程运行,性能更好。
不过 AudioWorklet 需要单独加载一个 JS 文件,配置稍微复杂一些。对于初学者,先用 ScriptProcessorNode 上手没问题。等项目稳定了,再考虑迁移到 AudioWorklet。
兼容性提醒: AudioWorklet 在 Safari 上支持不完整。如果你的项目需要兼容 iOS,建议先用 ScriptProcessorNode 做 fallback。
小结
这一章我们聊了音频数字化的三个核心参数:采样率、位深度、声道。也讲了怎么用 getUserMedia 获取麦克风权限,以及如何用 AudioContext 把音频流转换成可处理的 PCM 数据。
这些是语音识别前端开发的基石。后面的章节,我们会基于这些数据做语音活动检测、特征提取,最终送到识别引擎里去。