3. 音频采集与预处理优化
音频采集是整个语音识别管线的第一环。这一环要是出了问题,后面再怎么优化也是白搭。我见过不少团队,花了大把精力去调模型、压网络,结果识别率上不去,最后发现是麦克风采集的音频本身就「脏」——采样率不对、声道混叠、回声没消除。嗯,今天我们就来把这几个坑填上。
3.1 getUserMedia 的核心配置项
浏览器里采集音频,绕不开 navigator.mediaDevices.getUserMedia。这个 API 的参数是一个 MediaStreamConstraints 对象,其中 audio 字段可以传一个配置对象。说白了,你给浏览器什么指令,它就给你什么流。
核心配置项一览:
- sampleRate —— 采样率,单位 Hz。常见值:16000、44100、48000。
- channelCount —— 声道数。语音识别通常用 1(单声道)。
- echoCancellation —— 回声消除开关。默认 true,但不同浏览器实现差异很大。
- noiseSuppression —— 降噪开关。建议开启,但注意它可能引入额外延迟。
- autoGainControl —— 自动增益控制。建议关闭,否则音量忽大忽小。
来看一段实际配置代码。我个人习惯把配置拆成两个版本:一个用于调试,一个用于生产。
// 生产环境推荐配置
const audioConstraints = {
sampleRate: 16000, // 语音识别标准采样率
channelCount: 1, // 单声道
echoCancellation: true, // 开启回声消除
noiseSuppression: true, // 开启降噪
autoGainControl: false // 关闭自动增益
};
// 调试环境配置(方便排查问题)
const debugConstraints = {
sampleRate: { ideal: 16000 },
channelCount: { ideal: 1 },
echoCancellation: { exact: true },
noiseSuppression: { exact: true },
autoGainControl: { exact: false }
};
async function initMicrophone() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: audioConstraints
});
return stream;
} catch (err) {
console.error('麦克风初始化失败:', err);
// 降级处理:去掉 exact 约束
}
}
注意: exact 关键字表示「必须满足」,如果浏览器不支持该值,会直接抛 OverconstrainedError。我建议生产环境用 ideal,让浏览器自动降级。
3.2 采样率:为什么 16000 Hz 是语音识别的黄金值?
你可能会问:手机录音不都是 44100 或 48000 吗?为什么语音识别偏要用 16000?
原因很简单。人类语音的主要能量集中在 300 Hz 到 3400 Hz 之间。根据奈奎斯特定理,采样率只要大于最高频率的两倍就能完整还原信号。16000 Hz 远高于 3400 Hz 的两倍,绰绰有余。
我在项目中遇到过一件事:某次集成第三方语音识别引擎,对方要求输入必须是 16000 Hz 的音频。我们前端采集的是 44100 Hz,想着后端转一下就行。结果后端重采样算法写得糙,高频混叠进了低频,识别率直接掉了 8 个百分点。后来我们改成前端直接采 16000 Hz,问题迎刃而解。
经验之谈: 如果后端或云端模型要求 16000 Hz,前端就采 16000 Hz。不要依赖后端重采样,前端用 AudioContext 的 resample() 或 OfflineAudioContext 做重采样,可控性更高。
3.3 声道数:单声道就够了
语音识别不需要立体声。多声道不仅浪费带宽,还会增加处理复杂度。你想想看,两个声道的数据量是单声道的两倍,但识别引擎通常只取第一个声道,或者把多声道混成单声道。何必呢?
我建议直接设置 channelCount: 1。如果浏览器不支持单声道(极少见),可以用 AudioContext 把多声道混音成单声道:
function convertToMono(audioBuffer) {
if (audioBuffer.numberOfChannels === 1) return audioBuffer;
const length = audioBuffer.length;
const monoData = new Float32Array(length);
// 取所有声道平均值
for (let i = 0; i < audioBuffer.numberOfChannels; i++) {
const channelData = audioBuffer.getChannelData(i);
for (let j = 0; j < length; j++) {
monoData[j] += channelData[j] / audioBuffer.numberOfChannels;
}
}
return monoData;
}
3.4 回声消除:开还是不开?
回声消除(AEC)是个双刃剑。开启后能消除扬声器反馈到麦克风的回声,但不同浏览器的 AEC 实现质量参差不齐。Chrome 的 AEC 做得不错,但某些移动端浏览器开启后反而会引入奇怪的噪声。
我曾经在一个视频会议场景里踩过坑:用户用外放说话,回声消除没开,结果识别引擎把回声也识别进去了,输出了一堆乱码。后来强制开启 echoCancellation: true,问题解决。但另一个项目里,用户反馈开启后声音变「闷」,我们排查发现是浏览器的 AEC 把低频语音也当回声滤掉了。
我的建议:
- 默认开启
echoCancellation: true。 - 如果用户反馈音质异常,提供开关让用户手动关闭。
- 在初始化时检测浏览器能力,降级方案备好。
3.5 音频缓冲区大小:延迟与稳定性的博弈
缓冲区大小直接影响延迟。缓冲区越小,延迟越低,但 CPU 压力越大,容易出现音频中断(glitch)。缓冲区越大,延迟越高,但更稳定。
在 AudioContext 中,缓冲区大小通过 createScriptProcessor 的 bufferSize 参数控制。常见值:256、512、1024、2048、4096。
| 缓冲区大小 | 延迟(@16000 Hz) | 适用场景 | 风险 |
|---|---|---|---|
| 256 | 16 ms | 实时交互(如语音助手) | 高 CPU,易 glitch |
| 512 | 32 ms | 轻度实时场景 | 中等 CPU |
| 1024 | 64 ms | 非实时录音 | 低 CPU,稳定 |
| 2048 | 128 ms | 离线转写 | 延迟明显 |
| 4096 | 256 ms | 不推荐用于语音识别 | 延迟过高 |
我个人习惯从 1024 开始调。如果 CPU 有余量,降到 512;如果出现 glitch,升到 2048。说白了,这是一个「试出来」的参数,没有绝对最优值。
const audioContext = new AudioContext({ sampleRate: 16000 });
const bufferSize = 1024; // 从 1024 开始
const processor = audioContext.createScriptProcessor(bufferSize, 1, 1);
processor.onaudioprocess = (event) => {
const inputData = event.inputBuffer.getChannelData(0);
// 将音频数据发送给识别引擎
sendToRecognizer(inputData);
};
注意: createScriptProcessor 已被标记为废弃,推荐使用 AudioWorklet。但 AudioWorklet 的兼容性还不够好,生产环境我仍然用 ScriptProcessor 加 polyfill 兜底。
3.6 整体配置流程
把上面这些知识点串起来,整个音频采集优化的流程大概是这样的:
这个流程看起来简单,但每一步都有坑。我建议你在开发时加上性能监控,实时观察延迟和音频中断情况。如果发现 glitch,优先考虑增大缓冲区,而不是去调采样率或声道数。
调试小技巧: 用 performance.now() 在 onaudioprocess 回调里打时间戳,计算相邻回调的时间差。如果差值大于缓冲区时长(如 1024/16000 = 64ms),说明有 glitch。
3.7 避坑指南
最后,分享几个我踩过的坑:
- 不要相信浏览器的默认值。 不同浏览器对
getUserMedia的默认配置差异很大。Safari 默认采样率是 48000,Chrome 是 44100,Firefox 是 16000。显式设置才是王道。 - 回声消除不是万能的。 我曾经在一个项目里发现,开启回声消除后,识别延迟反而增加了 30ms。排查发现是浏览器的 AEC 算法在内部做了额外的缓冲。如果延迟敏感,可以考虑关闭 AEC,用后端的回声消除方案。
- 移动端慎用高采样率。 在低端 Android 设备上,48000 Hz 的采集会导致 CPU 飙升,甚至掉帧。我建议移动端统一用 16000 Hz。
- 别忘了释放资源。 页面关闭或切换时,记得调用
stream.getTracks().forEach(track => track.stop()),否则麦克风一直亮着,用户会投诉。