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 整体配置流程

把上面这些知识点串起来,整个音频采集优化的流程大概是这样的:

音频采集优化流程图 音频采集优化流程 getUserMedia 配置检查(采样率/声道/AEC) AudioContext 初始化 缓冲区大小选择(1024→512) 音频流输出 关键决策点 采样率:16000 Hz 声道:1(单声道) 性能监控 延迟 < 100ms 无 glitch

这个流程看起来简单,但每一步都有坑。我建议你在开发时加上性能监控,实时观察延迟和音频中断情况。如果发现 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()),否则麦克风一直亮着,用户会投诉。