11、音频处理基础:Web Audio API简介、AudioContext创建、音频源节点与目标节点。

各位同学,欢迎来到音频处理的第一课。

说实话,WebRTC 的视频部分大家可能更熟悉,但音频才是真正决定通话质量的关键。我见过太多项目,视频清晰度拉满,结果声音一塌糊涂,用户直接挂电话。所以,从今天开始,我们得把音频这块硬骨头啃下来。

11.1 为什么需要 Web Audio API?

你可能会问:「WebRTC 不是自带音频采集和播放吗?为什么还要单独学一个 API?」

嗯,这个问题问得好。WebRTC 的 getUserMedia 确实能拿到麦克风数据,但它只提供最原始的采集能力。如果你想做降噪、回声消除、音量调节、音频特效,甚至实时混音,WebRTC 本身是帮不上忙的。

这时候,Web Audio API 就登场了。它就像浏览器内置的音频工作站。你可以把音频流想象成水流,Web Audio API 就是一套管道系统。你可以接上各种处理器(节点),让水流经过过滤、增压、分流,最后再输出。

核心概念:Web Audio API 是一个基于节点的音频处理图(Audio Graph)。你创建节点,连接它们,音频数据就在节点之间流动。

我在项目中遇到过最典型的场景:用户用廉价耳机,背景噪音巨大。如果没有 Web Audio API,你只能把噪音原封不动地传给对方。但有了它,你可以插入一个降噪节点,瞬间提升通话体验。

11.2 创建 AudioContext:一切的起点

要使用 Web Audio API,第一步永远是创建一个 AudioContext。它是整个音频图的上下文环境,所有节点都必须在它里面创建。

// 创建一个 AudioContext
const audioCtx = new AudioContext();

console.log(audioCtx.state); // 输出: 'suspended' 或 'running'

这里有个坑,我一开始也踩过。浏览器为了用户体验,默认 AudioContext 是 suspended(暂停)状态。你必须通过用户交互(比如点击按钮)来唤醒它。

注意:不要在页面加载时直接调用 audioCtx.resume(),浏览器会拒绝。必须由用户手势触发,比如点击、触摸、键盘事件。

我个人习惯这样处理:

// 在用户点击按钮时恢复音频上下文
document.getElementById('startBtn').addEventListener('click', async () => {
  if (audioCtx.state === 'suspended') {
    await audioCtx.resume();
    console.log('AudioContext 已恢复');
  }
});

为什么浏览器要这么设计?说白了,就是防止网页自动播放音频,骚扰用户。你想想看,如果每个网站打开就自动放音乐,那得多烦人。

11.3 音频源节点:音频数据的入口

有了 AudioContext,接下来就要往里面灌数据了。音频源节点就是数据的入口。常见的源节点有三种:

节点类型 用途 典型场景
MediaStreamAudioSourceNode 从麦克风或摄像头获取实时音频流 WebRTC 通话、实时录音
AudioBufferSourceNode 播放内存中的音频数据(如文件加载后的 buffer) 播放音效、背景音乐
OscillatorNode 生成纯音波形(正弦波、方波等) 测试音频设备、生成提示音

在 WebRTC 场景中,我们最常用的是 MediaStreamAudioSourceNode。下面这段代码展示了如何把麦克风数据接入音频图:

// 获取麦克风流
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });

// 创建音频上下文
const audioCtx = new AudioContext();

// 创建源节点:把麦克风流接入音频图
const sourceNode = audioCtx.createMediaStreamSource(stream);

console.log('源节点已创建,音频流已接入');

注意,createMediaStreamSource 接收的是一个 MediaStream 对象,而不是单个轨道。如果你只想处理某个特定轨道,可以用 stream.getAudioTracks()[0] 来获取。

小技巧:如果你在调试时发现没有声音,先检查 sourceNode 是否成功连接到了后续节点。我曾经有一次忘记连接目标节点,结果折腾了半小时。

11.4 音频目标节点:数据的出口

音频流经过处理后,最终要去哪里?答案就是目标节点。最常用的目标节点是 AudioDestinationNode,它代表音频输出设备(扬声器或耳机)。

每个 AudioContext 都有一个默认的目标节点:audioCtx.destination。你只需要把源节点或处理节点连接到它,就能听到声音。

// 把源节点直接连接到扬声器
sourceNode.connect(audioCtx.destination);

console.log('音频流已连接到扬声器');

就这么简单?是的,但实际项目中你通常不会直接连接。你会在中间插入各种处理节点,比如增益节点、滤波器节点。但无论如何,最终都要连到 destination

除了扬声器,你还可以把音频输出到其他目标,比如:

  • MediaStreamAudioDestinationNode:把处理后的音频导出为新的 MediaStream,用于 WebRTC 发送或录制。
  • OfflineAudioContext:离线渲染音频,不实时播放,适合音频文件处理。

在 WebRTC 中,我们经常需要把处理后的音频流再发送给远端。这时候就会用到 MediaStreamAudioDestinationNode

// 创建一个音频目标节点,用于生成新的 MediaStream
const destNode = audioCtx.createMediaStreamDestination();

// 把处理后的音频连接到这个目标节点
someProcessingNode.connect(destNode);

// 获取新的流,用于 WebRTC 发送
const processedStream = destNode.stream;
// 然后添加到 RTCPeerConnection 中

11.5 完整的音频图:从源到目标

现在,我们把所有东西串起来,画一张完整的音频图。这张图展示了音频数据从采集到输出的完整路径。

Web Audio API 音频处理流程图 音频源节点 麦克风 / 文件 / 振荡器 处理节点(可选) 增益 / 滤波器 / 降噪 动态压缩 / 混音 目标节点 扬声器 / 新 MediaStream 音频数据从左向右流动 处理节点可以串联多个,也可以跳过 AudioContext 作用域

这张图很直观。音频数据从左侧的源节点进入,经过中间的处理节点(可以串联多个),最终到达右侧的目标节点。所有节点都必须在同一个 AudioContext 内创建和连接。

11.6 节点连接与断开:动态调整音频图

音频图不是静态的。你可以在运行时动态连接或断开节点。比如,用户点击「静音」按钮时,你可以断开源节点和目标节点的连接。

// 断开连接
sourceNode.disconnect();

// 重新连接
sourceNode.connect(audioCtx.destination);

这里有个细节:disconnect() 如果不带参数,会断开该节点的所有连接。如果你只想断开某个特定目标,可以传入参数:

// 只断开与某个节点的连接
sourceNode.disconnect(audioCtx.destination);

我曾经在项目中犯过一个错误:在断开连接后忘记重新连接,导致用户一直听不到声音。调试了半天才发现是连接状态没管理好。所以,我建议你用一个变量来跟踪当前连接状态:

let isConnected = true;

function toggleAudio() {
  if (isConnected) {
    sourceNode.disconnect();
    isConnected = false;
  } else {
    sourceNode.connect(audioCtx.destination);
    isConnected = true;
  }
}

11.7 总结与避坑指南

好了,今天的内容就到这里。我们来快速回顾一下:

  • AudioContext 是音频图的容器,必须由用户手势唤醒。
  • 源节点 是音频数据的入口,WebRTC 中常用 createMediaStreamSource
  • 目标节点 是音频数据的出口,默认是扬声器,也可以导出为新的 MediaStream。
  • 节点之间通过 connectdisconnect 动态连接。

避坑指南:

  • 我曾经在移动端 Safari 上遇到过 AudioContext 无法恢复的问题。解决方案是:在用户点击事件中调用 audioCtx.resume(),并且确保事件是用户直接触发的,不是通过 setTimeoutPromise 间接触发的。
  • 另一个常见坑:创建多个 AudioContext 实例。每个 AudioContext 都会占用独立的内存和线程资源。除非有特殊需求,否则整个应用只创建一个 AudioContext 就够了。
  • 记得在页面卸载时关闭 AudioContext:audioCtx.close(),释放资源。

音频处理是个很有意思的领域。你掌握了这些基础,后面学降噪、回声消除、音量自动增益就会轻松很多。嗯,今天就先到这里,大家动手试试吧。

公众号:蓝海资料掘金营,微信 deep3321