跳到主要内容

Web Audio:浏览器里的音频处理与合成引擎(从入门到实战)

如果你只是“放一首背景音乐”,<audio> 往往已经够用了;但如果你要做下面这些事情,通常就要进入 Web Audio API 的世界:

  • 做音量控制、淡入淡出、滤波、压缩、回声等效果
  • 生成声音:蜂鸣器、提示音、合成器、游戏音效
  • 读取麦克风、视频流、<audio> 元素并继续处理
  • 绘制波形图、频谱图、音量表
  • 对声音做精确调度,而不是“差不多这个时刻播放”

一句话理解:Web Audio 是浏览器内置的“音频图形引擎”。 你把多个音频节点(AudioNode)连接成一张图,声音就会沿着这张图流动、处理,最后输出到扬声器或其它目标。


1. Web Audio 到底解决什么问题?

Web Audio API 的核心价值有三个:

  1. 统一音频处理模型:播放、分析、滤波、压缩、空间化都基于同一套节点图
  2. 更精确的时间控制:可以基于音频时钟做调度,而不是依赖不稳定的 JS 定时器
  3. 更强的可组合性:不同来源的音频都能接入同一条处理链

你可以把它理解为前端里的“音频版数据流”。

能力<audio>Web Audio
简单播放/暂停很方便也能做,但不一定更简单
音量/倍速/进度控制支持支持
多级音效处理很弱很强
波形/频谱分析基本不行原生支持
精确调度较弱很强
自定义 DSP不适合AudioWorklet 可做
什么时候优先用 Web Audio?

只要你的目标从“播放音频”升级到“处理音频、分析音频、合成音频、精确控制音频”,就应该优先考虑 Web Audio。


2. 先记住三个核心对象

2.1 AudioContext:整个音频世界的入口

AudioContext 可以理解为“音频引擎实例”。它负责:

  • 提供统一时钟:currentTime
  • 创建各种节点:createGain()createAnalyser()createOscillator()
  • 管理音频输出终点:destination
  • 控制上下文状态:suspend()resume()close()

最常见的创建方式:

const audioContext = new AudioContext();

你会经常遇到这几个属性:

  • audioContext.currentTime:当前音频时间,单位秒
  • audioContext.sampleRate:采样率,比如 44100 或 48000
  • audioContext.statesuspended / running / closed
  • audioContext.destination:最终输出节点,通常就是系统扬声器
自动播放限制一定要知道

现代浏览器通常不允许页面一加载就自动发声。最常见的做法是:在用户点击按钮后创建或 resume() AudioContext

2.2 AudioNode:音频图里的每一个节点

AudioNode 是声音处理链上的“积木块”。每个节点可能负责:

  • 产生声音
  • 改变声音
  • 分析声音
  • 输出声音

节点之间通过 connect() 串起来:

sourceNode.connect(gainNode);
gainNode.connect(audioContext.destination);

也可以断开:

sourceNode.disconnect();

2.3 AudioParam:可以被精确自动化的参数

很多节点的关键参数并不是普通属性,而是 AudioParam,比如:

  • gainNode.gain
  • oscillator.frequency
  • filter.frequency

它的价值在于:你可以提前安排参数在未来如何变化

常见方法:

  • setValueAtTime(value, time)
  • linearRampToValueAtTime(value, time)
  • exponentialRampToValueAtTime(value, time)
  • setTargetAtTime(target, startTime, timeConstant)
  • cancelScheduledValues(time)

这类调度是基于音频线程执行的,通常比 setTimeout() 更稳定。


3. 常见 AudioNode 全景图

理解 Web Audio,最有效的方法不是死记 API,而是先把节点分组。

3.1 声源节点:声音从哪里来?

节点作用典型场景
OscillatorNode生成基础波形蜂鸣器、合成器、测试音
AudioBufferSourceNode播放解码后的 PCM 数据音效、短音频、切片播放
MediaElementAudioSourceNode接入 <audio> / <video>播放器加音效
MediaStreamAudioSourceNode接入麦克风或实时流录音、语音分析、变声

3.2 处理节点:声音怎么变?

节点作用典型参数
GainNode调整音量gain
BiquadFilterNode高频/低频/带通滤波typefrequencyQ
DelayNode延迟delayTime
DynamicsCompressorNode压缩动态范围阈值、压缩比
StereoPannerNode左右声像pan
PannerNode3D 空间音频位置、方向、距离模型
WaveShaperNode波形失真/非线性处理curve
ChannelSplitterNode / ChannelMergerNode声道拆分 / 合并声道处理

3.3 分析与输出节点:怎么“看见”声音、把声音送出去?

节点作用常见用途
AnalyserNode获取时域/频域数据波形图、频谱图、VU 表
AudioDestinationNode输出到设备最终播放
MediaStreamAudioDestinationNode输出成 MediaStreamWebRTC、录制、二次传输

4. 第一条音频链:一个最小可运行示例

先用最经典的链路理解整体思路:

OscillatorNode -> GainNode -> destination

这个示例会在按钮点击后播放一个短促的提示音,并顺带展示 包络(envelope) 的写法。

const button = document.querySelector('#play-tone');
const ctx = new AudioContext();

button.addEventListener('click', async () => {
if (ctx.state === 'suspended') {
await ctx.resume();
}

const now = ctx.currentTime;

const oscillator = ctx.createOscillator();
oscillator.type = 'sawtooth';
oscillator.frequency.setValueAtTime(440, now);

const gainNode = ctx.createGain();
gainNode.gain.setValueAtTime(0.0001, now);
gainNode.gain.linearRampToValueAtTime(0.2, now + 0.02);
gainNode.gain.exponentialRampToValueAtTime(0.0001, now + 0.4);

oscillator.connect(gainNode);
gainNode.connect(ctx.destination);

oscillator.start(now);
oscillator.stop(now + 0.42);
});

这个例子里最值得记住的是两点:

  1. 不要直接让音量从 0 跳到 1,容易出现爆音或点击声
  2. 使用 currentTime 调度,而不是 setTimeout() 控制声音变化
OscillatorNode 非常适合入门

它不需要加载任何音频文件,就能让你立刻理解“声源 -> 处理 -> 输出”的思路。


5. AudioContext:除了创建,它还负责“时间”

很多初学者把 AudioContext 当成“工厂对象”,其实它更重要的身份是:音频时间轴的管理者

5.1 为什么 Web Audio 特别强调 currentTime

因为 JS 主线程并不可靠:

  • 页面可能卡顿
  • 定时器可能被降频
  • 渲染、布局、脚本执行会互相竞争

而声音对时间非常敏感。你听到的节拍、鼓点、音效触发,如果误差太大,用户会明显感觉“不准”。

所以 Web Audio 的推荐思路是:

  • 用 JS 提前安排未来一小段时间内的音频事件
  • 让音频线程按时间轴精确执行

5.2 一个简单的“未来调度”例子

function scheduleBeep(ctx, startTime, frequency) {
const oscillator = ctx.createOscillator();
const gainNode = ctx.createGain();

oscillator.frequency.value = frequency;
gainNode.gain.setValueAtTime(0.0001, startTime);
gainNode.gain.linearRampToValueAtTime(0.15, startTime + 0.01);
gainNode.gain.exponentialRampToValueAtTime(0.0001, startTime + 0.2);

oscillator.connect(gainNode).connect(ctx.destination);
oscillator.start(startTime);
oscillator.stop(startTime + 0.22);
}

const base = ctx.currentTime + 0.1;
scheduleBeep(ctx, base, 440);
scheduleBeep(ctx, base + 0.5, 660);
scheduleBeep(ctx, base + 1.0, 880);

这类写法的关键不是“立刻播放”,而是“提前告诉音频引擎:未来这些时刻要发生什么”。


6. 音频文件怎么接入?

Web Audio 并不局限于程序生成声音,真实业务里更常见的是接入外部音源。

6.1 方式一:fetch + decodeAudioData()

适合:

  • 播放短音效
  • 把音频完整加载到内存再反复触发
  • 做切片播放、变速、倒放、精细控制
async function loadAudioBuffer(ctx, url) {
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
return await ctx.decodeAudioData(arrayBuffer);
}

const buffer = await loadAudioBuffer(ctx, '/audio/click.mp3');

function playBuffer(buffer, when = ctx.currentTime) {
const source = ctx.createBufferSource();
source.buffer = buffer;
source.connect(ctx.destination);
source.start(when);
}
AudioBufferSourceNode 是一次性的

一个 AudioBufferSourceNode 调用 start() 之后就不能重复使用。想再次播放,必须重新创建一个 source 节点。 但底层的 AudioBuffer 可以复用。

6.2 方式二:把 <audio> / <video> 元素接进来

适合:

  • 你已经有一个普通播放器
  • 希望在播放器基础上叠加 EQ、音量、频谱分析等能力
const audio = document.querySelector('audio');
const ctx = new AudioContext();

const mediaSource = ctx.createMediaElementSource(audio);
const gainNode = ctx.createGain();
const analyser = ctx.createAnalyser();

mediaSource.connect(gainNode);
gainNode.connect(analyser);
analyser.connect(ctx.destination);

这种做法的好处是:播放控制仍然由 <audio> 负责,但声音路径改由 Web Audio 接管。

6.3 方式三:接入麦克风或实时流

适合:

  • 录音
  • 音量检测
  • 语音可视化
  • 语音前处理
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
},
});

const micSource = ctx.createMediaStreamSource(stream);
const analyser = ctx.createAnalyser();

micSource.connect(analyser);
analyser.connect(ctx.destination);

如果你只是做分析,不想把麦克风声音再放出来,通常可以不把它接到 destination,避免回授或啸叫。


7. AudioParam:音量、频率、滤波为什么能“平滑变化”?

因为很多参数背后都是 AudioParam,它支持时间轴自动化。

7.1 最常见的自动化:淡入淡出

const gainNode = ctx.createGain();
const now = ctx.currentTime;

gainNode.gain.setValueAtTime(0, now);
gainNode.gain.linearRampToValueAtTime(1, now + 1);
gainNode.gain.linearRampToValueAtTime(0, now + 3);

7.2 为什么不推荐直接频繁写 gain.value = xxx

因为这种写法:

  • 容易受主线程调度影响
  • 高频修改可能不够平滑
  • 在快速变化的声音里更容易产生毛刺

更稳妥的做法是:

  • 能预知变化时,用自动化 API 调度
  • 需要高频自定义 DSP 时,用 AudioWorklet

7.3 linearRampexponentialRamp 怎么选?

  • 线性变化:适合“匀速变化”的视觉或数值直觉
  • 指数变化:更贴近人耳对音量、频率变化的感知

实际项目里,音量包络 很常用指数变化,因为听感通常更自然。


8. 分析声音:AnalyserNode 是做可视化的核心

如果你做过音乐播放器里的频谱柱状图、录音波形、音量跳动动画,核心通常就是 AnalyserNode

const analyser = ctx.createAnalyser();
analyser.fftSize = 2048;

source.connect(analyser);
analyser.connect(ctx.destination);

const frequencyData = new Uint8Array(analyser.frequencyBinCount);
const timeDomainData = new Uint8Array(analyser.fftSize);

function draw() {
requestAnimationFrame(draw);

analyser.getByteFrequencyData(frequencyData);
analyser.getByteTimeDomainData(timeDomainData);

// 在这里把数据画到 canvas
}

draw();

常用属性和方法:

  • fftSize:FFT 窗口大小,越大频率分辨率越高,但开销也更大
  • frequencyBinCount:频域数组长度,等于 fftSize / 2
  • getByteFrequencyData():获取频谱数据,范围 0~255
  • getByteTimeDomainData():获取波形数据,范围 0~255
  • smoothingTimeConstant:平滑程度,柱状图跳动是否更顺滑
AnalyserNode 不会“改变声音”

它更像一个监听探针:读取数据用于可视化或检测,但并不负责实际音效处理。


9. 实战里最常见的处理节点

9.1 GainNode:最常用,没有之一

用途:

  • 主音量
  • 静音/取消静音
  • 淡入淡出
  • 包络控制
const gainNode = ctx.createGain();
gainNode.gain.value = 0.5;

9.2 BiquadFilterNode:均衡器和滤波器基础

const filter = ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 1200;
filter.Q.value = 1;

常见 type

  • lowpass:低通
  • highpass:高通
  • bandpass:带通
  • lowshelf / highshelf:搁架滤波
  • peaking:峰值滤波

9.3 StereoPannerNode:左右声道移动

const panner = ctx.createStereoPanner();
panner.pan.value = -0.6; // 偏左

9.4 DynamicsCompressorNode:压缩动态范围

适合:

  • 避免声音忽大忽小
  • 降低突然峰值带来的刺耳感
  • 让整体听感更稳定

它在播放器、语音处理、混音场景里都很常见。

9.5 DelayNode:延迟与回声的基础材料

const delay = ctx.createDelay();
delay.delayTime.value = 0.25;

如果再结合反馈回路和 GainNode,就能做出简单回声效果。但涉及反馈时要格外注意音量控制,避免无限放大。


10. AudioWorklet:需要自定义音频算法时的标准方案

如果内置节点不够用,你就要考虑 AudioWorklet

它适合:

  • 自定义 DSP 算法
  • 低延迟处理
  • 更稳定的实时音频逻辑
  • 配合 WASM 做复杂运算

最基本的使用流程:

  1. await audioContext.audioWorklet.addModule('/processor.js')
  2. new AudioWorkletNode(audioContext, 'processor-name')
  3. 把它接入你的音频图
await ctx.audioWorklet.addModule('/processors/volume-meter.js');

const workletNode = new AudioWorkletNode(ctx, 'volume-meter');
source.connect(workletNode).connect(ctx.destination);
不要再优先考虑 ScriptProcessorNode

ScriptProcessorNode 已经属于过时方案。需要自定义处理时,应优先使用 AudioWorklet


11. OfflineAudioContext:不实时播放,也能渲染声音

OfflineAudioContext 的思路是:不走真实扬声器,而是在离线环境里把结果提前算出来。

适合:

  • 导出处理后的音频
  • 预计算波形/特征
  • 批量渲染效果
  • 测试音频图的输出结果
const offlineCtx = new OfflineAudioContext(2, 48000 * 3, 48000);

const oscillator = offlineCtx.createOscillator();
const gainNode = offlineCtx.createGain();

oscillator.connect(gainNode).connect(offlineCtx.destination);
oscillator.start(0);
oscillator.stop(2);

const renderedBuffer = await offlineCtx.startRendering();

它返回的是 AudioBuffer,后续你可以继续保存、分析或再次播放。


12. 几条高频实战链路

12.1 播放器 + 音量 + 频谱

HTMLAudioElement -> MediaElementSource -> Gain -> Analyser -> destination

适合音乐播放器、播客播放器、视频站点。

12.2 麦克风 + 分析 + WebRTC/录制

MediaStreamSource -> 处理节点 -> MediaStreamDestination

适合语音房、录音、实时音频转发。

12.3 游戏音效 / 提示音

AudioBufferSource 或 Oscillator -> Gain/Filter -> destination

适合短音效高频触发,强调低延迟和可重复调度。

12.4 自定义算法处理

Source -> AudioWorkletNode -> destination

适合降噪、变声、检测、实时音频特征提取。


13. 常见坑与性能建议

13.1 尽量复用同一个 AudioContext

不要每次点击按钮都 new AudioContext()。多数应用里,一个页面维护一个长期存在的 context 会更合理。

13.2 记住 AudioBufferSourceNode 只能用一次

想重复播放同一个音效:复用 AudioBuffer,重新创建 source 节点。

13.3 不需要的节点要及时断开

比如:

  • source.stop() 后不再使用
  • disconnect() 无用链路
  • 关闭麦克风时记得 track.stop()

13.4 自动播放限制不是 bug

如果你发现“代码没报错但就是没声音”,先检查:

  • 是否已经用户交互
  • audioContext.state 是否还是 suspended
  • 是否调用过 resume()

13.5 跨域媒体要注意 CORS

如果你要把跨域音频/视频元素接入 Web Audio 做分析或处理,通常需要正确的跨域响应头和元素配置。

13.6 高频自定义处理不要硬塞主线程

复杂实时算法优先考虑:

  • AudioWorklet
  • 必要时配合 WebAssembly

否则很容易出现爆音、延迟升高、可视化和音频互相抢资源。

13.7 分析可视化和音频调度是两条节奏

requestAnimationFrame() 适合画图;currentTime 适合调度声音。不要混用心智模型。


14. 学习 Web Audio 时最容易混淆的几个概念

14.1 AudioBufferAudioBufferSourceNode 有什么区别?

  • AudioBuffer:音频数据本身
  • AudioBufferSourceNode:负责“播放这段数据”的节点

可以把前者理解为“磁带内容”,后者理解为“播放机”。

14.2 MediaElementAudioSourceNodeAudioBufferSourceNode 怎么选?

  • 已经用 <audio> / <video> 管理播放:选 MediaElementAudioSourceNode
  • 想要更细粒度调度、切片、低延迟音效:选 AudioBufferSourceNode

14.3 GainNodegain.valuegain.setValueAtTime() 有什么区别?

  • gain.value = x:立即改值,简单直接
  • setValueAtTime():把变化安排到音频时间轴,更适合可预测、精确的变化

14.4 AnalyserNode 是不是“音效器”?

不是。它主要是“观察”音频数据,而不是“改变”音频数据。


面试高频问答

Q1:Web Audio 和 <audio> 有什么本质区别?

<audio> 更像一个现成播放器,适合播放、暂停、进度、倍速这类高层控制;Web Audio 是底层音频处理引擎,强调音频图、节点连接、精确调度、音效处理、分析和合成。简单播放用 <audio> 更省事,复杂处理用 Web Audio 更强。

Q2:AudioContext 的作用是什么?

:它是整个音频系统的入口,负责创建节点、提供统一时钟 currentTime、维护输出终点 destination,并管理音频上下文状态(suspendedrunningclosed)。

Q3:为什么 Web Audio 不推荐只靠 setTimeout() 来控制播放时机?

:因为 JS 定时器运行在主线程,可能受页面卡顿、任务拥塞、后台降频影响,时间误差较大;Web Audio 提供基于音频时钟的调度机制,能更稳定地按指定时刻执行播放和参数变化。

Q4:AudioBufferSourceNode 为什么说是一次性的?

:因为一个 source 节点调用 start() 后生命周期就确定了,不能再次 start()。如果想重复播放同一个音频,需要复用 AudioBuffer,重新创建新的 AudioBufferSourceNode

Q5:AudioParam 的价值是什么?

:它让音量、频率、滤波等参数可以沿时间轴自动变化,支持线性、指数等多种插值方式,适合做淡入淡出、包络、扫频、自动化控制,而且精度通常比主线程定时器更高。

Q6:AnalyserNode 能做什么?

:它能读取时域和频域数据,用来做波形图、频谱图、音量检测、节奏可视化等,但本身不负责改变声音。

Q7:AudioWorklet 为什么重要?

:当内置节点不够用时,AudioWorklet 提供了更适合实时音频处理的自定义能力,性能和稳定性通常优于老旧的 ScriptProcessorNode,也是现代 Web Audio 做复杂 DSP 的标准做法。

Q8:开发 Web Audio 时最常见的无声问题有哪些?

:常见原因包括:浏览器自动播放限制导致 AudioContext 仍是 suspended、节点没有正确 connect()destination、音量参数被设为 0、音频资源跨域受限、麦克风权限未授予等。排查时建议先从 state、链路连接和用户手势三个方向入手。


总结

如果你要把 Web Audio 真正学会,建议按这个顺序掌握:

  1. 先懂图模型AudioContextAudioNodeAudioParam
  2. 再懂时间轴currentTimestart()、参数自动化
  3. 然后做三类项目:播放器增强、音频可视化、短音效调度
  4. 最后进阶AudioWorkletOfflineAudioContext、空间音频、WASM

一旦你习惯“节点图 + 音频时间轴”这套思维,Web Audio 就不会再像一堆零散 API,而会变成一个非常统一、非常强大的音频编程模型。