2、Web Audio API 基础:音频上下文、节点与数据流
做语音识别前端优化,绕不开 Web Audio API。说实话,我刚接触这个 API 时也被它那一堆「节点」搞晕过。但后来我发现,只要搞懂四个核心概念——音频上下文(AudioContext)、音频源节点、音频处理节点、音频目标节点——整个体系就清晰了。
这一章,我们就来拆解这四块内容。我会结合自己踩过的坑,帮你快速建立起对 Web Audio API 的直觉。
2.1 音频上下文(AudioContext)——一切开始的地方
AudioContext 是整个音频处理系统的「总控室」。它管理着音频硬件、音频节点图、以及音频数据的调度。说白了,没有它,你什么都干不了。
创建一个 AudioContext 很简单:
const audioCtx = new AudioContext();
但这里有个坑——浏览器安全策略。我记得第一次做语音识别项目时,页面加载后直接调用 audioCtx.resume(),结果控制台报错。后来才发现,Chrome 要求 AudioContext 必须在用户交互(比如点击按钮)之后才能激活。
AudioContext 有几个关键属性,我列个表方便你对照:
| 属性/方法 | 说明 | 我在项目中踩过的坑 |
|---|---|---|
sampleRate |
采样率,默认 48000 或 44100 | 语音识别通常需要 16000,记得用 audioCtx.createScriptProcessor 时做重采样 |
state |
当前状态:suspended / running / closed | 移动端经常自动挂起,要监听 statechange 事件 |
baseLatency |
音频处理延迟(秒) | 实时语音识别时这个值很关键,我一般控制在 0.01 以内 |
resume() |
恢复音频上下文 | 必须在用户手势回调中调用,否则无效 |
close() |
关闭上下文,释放资源 | 页面卸载时一定要调用,否则内存泄漏 |
2.2 音频源节点——声音从哪里来
音频源节点是音频数据的「入口」。它负责把原始音频数据(麦克风、音频文件、振荡器等)转换成 Web Audio API 能处理的格式。
常见的音频源节点有三种:
- MediaStreamAudioSourceNode:从麦克风或摄像头获取实时音频流
- AudioBufferSourceNode:播放内存中的音频数据(比如加载的音频文件)
- OscillatorNode:生成正弦波、方波等合成音
语音识别最常用的是第一种。获取麦克风音频流的代码长这样:
// 获取用户麦克风
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
// 创建音频上下文
const audioCtx = new AudioContext();
// 创建音频源节点
const source = audioCtx.createMediaStreamSource(stream);
// 现在 source 就可以连接到其他处理节点了
source.connect(audioCtx.destination);
嗯,这里要注意:getUserMedia 返回的 stream 是浏览器原生的媒体流,而 createMediaStreamSource 把它变成了 Web Audio API 的节点。我曾经犯过一个错误——直接拿 stream 去处理音频数据,结果发现根本拿不到 PCM 数据。后来才明白,必须通过 AudioContext 的节点图才能操作。
2.3 音频处理节点——核心加工厂
音频处理节点是 Web Audio API 最强大的部分。它就像一条流水线上的各种加工设备,可以对音频做滤波、增益、压缩、分析等操作。
对于语音识别来说,最常用的处理节点是:
| 节点类型 | 用途 | 我常用的场景 |
|---|---|---|
AnalyserNode |
实时分析音频频域/时域数据 | 检测说话人是否在发声(VAD) |
GainNode |
控制音量增益 | 麦克风音量归一化,防止爆音 |
BiquadFilterNode |
低通/高通/带通滤波 | 去除环境噪音(比如空调声) |
ScriptProcessorNode |
(已废弃)在 JS 线程处理音频 | 老项目中还在用,新项目建议用 AudioWorklet |
AudioWorkletNode |
在独立线程处理音频 | 实时语音识别的前端预处理 |
举个例子,如果你要做简单的降噪,可以串联一个低通滤波器:
// 创建低通滤波器
const filter = audioCtx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 3000; // 保留 3kHz 以下的人声
// 连接:麦克风 → 滤波器 → 输出
source.connect(filter);
filter.connect(audioCtx.destination);
我个人习惯在语音识别前加一个 AnalyserNode,用来做语音活动检测(VAD)。这样可以避免把静音片段也送去识别,大幅降低 API 调用次数和延迟。
2.4 音频目标节点——声音去哪里
音频目标节点是音频数据的「出口」。最常见的当然是 audioCtx.destination,它代表扬声器。但语音识别场景下,我们通常不需要把声音播放出来,而是要把音频数据「截获」下来,送给识别引擎。
这时候就需要用到 MediaStreamAudioDestinationNode 或者 ScriptProcessorNode/AudioWorkletNode 来获取原始 PCM 数据。
举个例子,用 AudioWorklet 获取麦克风数据:
// 在 AudioWorklet 处理器中
class RecorderProcessor extends AudioWorkletProcessor {
process(inputs, outputs, parameters) {
const input = inputs[0];
if (input.length > 0) {
const channelData = input[0]; // Float32Array
// 这里可以把 channelData 传给主线程
this.port.postMessage(channelData);
}
return true; // 保持处理器活跃
}
}
registerProcessor('recorder-processor', RecorderProcessor);
然后在主线程中:
// 添加 AudioWorklet 模块
await audioCtx.audioWorklet.addModule('recorder-processor.js');
// 创建 AudioWorkletNode
const recorderNode = new AudioWorkletNode(audioCtx, 'recorder-processor');
// 监听数据
recorderNode.port.onmessage = (event) => {
const audioData = event.data; // Float32Array
// 送给语音识别引擎
};
// 连接:麦克风 → 录音节点
source.connect(recorderNode);
我曾经在旧项目里用 ScriptProcessorNode 做这件事,但它的回调在主线程执行,容易造成卡顿。后来全部迁移到了 AudioWorklet,性能提升很明显。
2.5 节点图:把一切串起来
现在我们把四个核心概念串起来,画一张完整的节点图。这张图就是语音识别前端音频处理的核心骨架。
这张图展示了语音识别场景下最典型的节点连接方式。你想想看,麦克风采集的原始音频,经过滤波降噪、VAD 分析,最后送到识别引擎——整个过程都在浏览器内完成,不需要任何后端参与。
2.6 避坑指南:我踩过的几个坑
最后分享几个实战中容易踩的坑,希望能帮你少走弯路:
- 节点连接顺序不能乱:必须先 connect 源节点,再 connect 处理节点,最后 connect 目标节点。我曾经把顺序搞反,结果音频数据根本没流过去。
- 记得断开连接:页面卸载或停止录音时,调用
node.disconnect()和audioCtx.close()。否则浏览器会一直占用麦克风资源。 - 采样率匹配:语音识别引擎通常要求 16000Hz 采样率,但 AudioContext 默认可能是 48000Hz。我一般用
audioCtx.createScriptProcessor或 AudioWorklet 做重采样。 - 移动端兼容性:iOS Safari 对 AudioWorklet 支持不太好,老项目可能需要降级到 ScriptProcessorNode。
好了,这一章的内容就到这里。Web Audio API 的节点体系其实不复杂,核心就是「源 → 处理 → 目标」这条链。搞懂了它,后面的性能优化才有根基。