3. 降噪问题定义
音频降噪不是凭空变魔术。你得先拿到原始音频数据,处理完再送出去。这一章,我们就来打通这条从麦克风到扬声器的路。
说实话,我刚接触 Web Audio API 时,觉得这东西挺绕的。一堆节点连来连去,像搭积木。但用顺手了就会发现,这套设计其实很优雅——每个节点只管一件事,组合起来就能做很复杂的事情。
3.1 用 getUserMedia 采集麦克风音频
浏览器里拿麦克风权限,靠的是 navigator.mediaDevices.getUserMedia。这个 API 会弹个权限框,用户点了允许,你才能拿到音频流。
核心要点:getUserMedia 返回的是一个 Promise,resolve 后给你一个 MediaStream 对象。这个流里可能包含音频轨和视频轨,我们只需要音频。
async function getMicrophoneStream() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: false,
noiseSuppression: false,
autoGainControl: false
}
});
return stream;
} catch (err) {
console.error('麦克风权限被拒绝:', err);
throw err;
}
}
注意:我把 echoCancellation、noiseSuppression、autoGainControl 都关了。为什么?因为这些是浏览器自带的处理,会干扰我们自己的降噪算法。你想想看,如果浏览器先降了一次噪,你的算法再降一次,效果反而会变差。我曾经在一个项目里没关这些,调试了整整两天才发现是浏览器在捣鬼。
3.2 把 MediaStream 接入 Web Audio 图
拿到 MediaStream 之后,得把它接到 Web Audio 的图里。这里需要一个 MediaStreamSourceNode。
const audioContext = new AudioContext();
const sourceNode = audioContext.createMediaStreamSource(stream);
嗯,就这么简单。现在 sourceNode 就成了音频图的起点。你可以把它连到任意处理节点上。
3.3 播放音频文件:AudioBufferSourceNode
除了实时麦克风,我们还得能播放音频文件。比如测试降噪效果时,放一段带噪语音,处理完再听对比。
加载音频文件,我习惯用 fetch 加 arrayBuffer,然后 decode 成 AudioBuffer:
async function loadAudioFile(url) {
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
return audioBuffer;
}
function playAudioBuffer(buffer) {
const source = audioContext.createBufferSource();
source.buffer = buffer;
source.connect(audioContext.destination);
source.start(0);
return source;
}
小技巧:AudioBufferSourceNode 是一次性的。播完就不能再 start 了。如果你想反复播同一段音频,每次都得新建一个 source 节点。我一般会封装一个播放器类,内部维护 source 的创建和销毁。
3.4 实时处理链路:从麦克风到扬声器
现在我们把采集和播放串起来。实时处理链路长这样:
麦克风 → MediaStreamSourceNode → [处理节点] → AudioDestinationNode → 扬声器
最简单的链路,就是直接把麦克风接到扬声器——相当于一个直通。但我们要做降噪,所以中间得插一个处理节点。
Web Audio API 提供了几种处理节点:
| 节点类型 | 用途 | 适用场景 |
|---|---|---|
| GainNode | 调节音量 | 音量控制、静音 |
| BiquadFilterNode | 频率滤波 | 简单降噪、EQ |
| ScriptProcessorNode | 自定义处理(已废弃) | 旧项目兼容 |
| AudioWorkletNode | 自定义处理(推荐) | 实时降噪算法 |
对于降噪算法,我们主要用 AudioWorkletNode。它跑在独立线程里,不会阻塞主线程,延迟也低。
我的建议:别用 ScriptProcessorNode 了。它虽然写起来简单,但跑在主线程上,音频处理一重就会卡。AudioWorklet 才是正道。我第一次用 ScriptProcessorNode 做实时降噪时,音频一卡一卡的,换成 AudioWorklet 后立马流畅了。
3.5 完整的实时处理链路代码
下面是一个完整的例子,把麦克风采集的音频经过一个自定义处理节点,再送到扬声器:
async function startRealTimeProcessing() {
// 1. 创建 AudioContext
const audioContext = new AudioContext();
// 2. 获取麦克风流
const stream = await getMicrophoneStream();
// 3. 创建源节点
const sourceNode = audioContext.createMediaStreamSource(stream);
// 4. 创建处理节点(这里用 GainNode 做演示,实际换成你的降噪节点)
const gainNode = audioContext.createGain();
gainNode.gain.value = 1.0;
// 5. 连接:源 → 处理 → 输出
sourceNode.connect(gainNode);
gainNode.connect(audioContext.destination);
console.log('实时处理链路已启动');
}
这段代码跑起来后,你对着麦克风说话,扬声器会实时播放出来。当然,现在还没做降噪,只是直通。后面几章我们会把真正的降噪算法塞进这个链路里。
3.6 链路结构总览
我把整个音频采集与播放的链路画成了 SVG 图,方便你理解各个节点的关系:
3.7 避坑指南
做音频采集和播放时,有几个坑我踩过,分享给你:
- AudioContext 必须在用户交互后创建——浏览器为了防滥用,不允许页面一加载就自动创建 AudioContext。你得在点击按钮之类的用户操作后再 new。我习惯在点击「开始录音」时创建。
- 采样率要一致——getUserMedia 返回的音频采样率通常是 48000Hz 或 44100Hz。如果你的降噪算法要求 16000Hz,得做重采样。AudioContext 的 sampleRate 属性可以帮你确认当前采样率。
- 别忘了断开连接——停止录音时,要调用
sourceNode.disconnect()和stream.getTracks().forEach(track => track.stop())。否则麦克风一直亮着,用户会慌。 - 延迟问题——实时处理链路会有延迟。AudioWorklet 的延迟通常在 10ms 以内,人耳基本感觉不到。但如果你的处理逻辑太重,延迟会飙升。我曾经把一个复杂的 FFT 算法塞进 AudioWorklet,延迟直接飙到 100ms,说话都有回声了。
经验之谈:调试音频链路时,我习惯先搭一个直通链路(麦克风直接到扬声器),确认采集和播放都没问题,再逐步加入处理节点。这样出了问题,你能快速定位是采集端、处理端还是播放端的问题。
3.8 小结
这一章我们打通了音频采集与播放的完整链路。你学会了:
- 用 getUserMedia 采集麦克风音频
- 用 AudioBufferSourceNode 播放音频文件
- 搭建实时处理链路:源节点 → 处理节点 → 输出节点
- 理解了 AudioWorklet 为什么是实时降噪的首选
链路搭好了,下一件事就是把降噪算法塞进去。嗯,那是后面几章的事。先把基础打牢,后面才能跑得快。