Web Audio:浏览器里的音频处理与合成引擎(从入门到实战)
如果你只是“放一首背景音乐”,<audio> 往往已经够用了;但如果你要做下面这些事情,通常就要进入 Web Audio API 的世界:
- 做音量控制、淡入淡出、滤波、压缩、回声等效果
- 生成声音:蜂鸣器、提示音、合成器、游戏音效
- 读取麦克风、视频流、
<audio>元素并继续处理 - 绘制波形图、频谱图、音量表
- 对声音做精确调度,而不是“差不多这个时刻播放”
一句话理解:Web Audio 是浏览器内置的“音频图形引擎”。 你把多个音频节点(AudioNode)连接成一张图,声音就会沿着这张图流动、处理,最后输出到扬声器或其它目标。
1. Web Audio 到底解决什么问题?
Web Audio API 的核心价值有三个:
- 统一音频处理模型:播放、分析、滤波、压缩、空间化都基于同一套节点图
- 更精确的时间控制:可以基于音频时钟做调度,而不是依赖不稳定的 JS 定时器
- 更强的可组合性:不同来源的音频都能接入同一条处理链
你可以把它理解为前端里的“音频版数据流”。
| 能力 | <audio> | Web Audio |
|---|---|---|
| 简单播放/暂停 | 很方便 | 也能做,但不一定更简单 |
| 音量/倍速/进度控制 | 支持 | 支持 |
| 多级音效处理 | 很弱 | 很强 |
| 波形/频谱分析 | 基本不行 | 原生支持 |
| 精确调度 | 较弱 | 很强 |
| 自定义 DSP | 不适合 | AudioWorklet 可做 |
只要你的目标从“播放音频”升级到“处理音频、分析音频、合成音频、精确控制音频”,就应该优先考虑 Web Audio。
2. 先记住三个核心对象
2.1 AudioContext:整个音频世界的入口
AudioContext 可以理解为“音频引擎实例”。它负责:
- 提供统一时钟:
currentTime - 创建各种节点:
createGain()、createAnalyser()、createOscillator()等 - 管理音频输出终点:
destination - 控制上下文状态:
suspend()、resume()、close()
最常见的创建方式:
const audioContext = new AudioContext();
你会经常遇到这几个属性:
audioContext.currentTime:当前音频时间,单位秒audioContext.sampleRate:采样率,比如 44100 或 48000audioContext.state:suspended/running/closedaudioContext.destination:最终输出节点,通常就是系统扬声器
现代浏览器通常不允许页面一加载就自动发声。最常见的做法是:在用户点击按钮后创建或 resume() AudioContext。
2.2 AudioNode:音频图里的每一个节点
AudioNode 是声音处理链上的“积木块”。每个节点可能负责:
- 产生声音
- 改变声音
- 分析声音
- 输出声音
节点之间通过 connect() 串起来:
sourceNode.connect(gainNode);
gainNode.connect(audioContext.destination);
也可以断开:
sourceNode.disconnect();
2.3 AudioParam:可以被精确自动化的参数
很多节点的关键参数并不是普通属性,而是 AudioParam,比如:
gainNode.gainoscillator.frequencyfilter.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 | 高频/低频/带通滤波 | type、frequency、Q |
DelayNode | 延迟 | delayTime |
DynamicsCompressorNode | 压缩动态范围 | 阈值、压缩比 |
StereoPannerNode | 左右声像 | pan |
PannerNode | 3D 空间音频 | 位置、方向、距离模型 |
WaveShaperNode | 波形失真/非线性处理 | curve |
ChannelSplitterNode / ChannelMergerNode | 声道拆分 / 合并 | 声道处理 |
3.3 分析与输出节点:怎么“看见”声音、把声音送出去?
| 节点 | 作用 | 常见用途 |
|---|---|---|
AnalyserNode | 获取时域/频域数据 | 波形图、频谱图、VU 表 |
AudioDestinationNode | 输出到设备 | 最终播放 |
MediaStreamAudioDestinationNode | 输出成 MediaStream | WebRTC、录制、二次传输 |
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);
});
这个例子里最值得记住的是两点:
- 不要直接让音量从 0 跳到 1,容易出现爆音或点击声
- 使用
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 linearRamp 和 exponentialRamp 怎么选?
- 线性变化:适合“匀速变化”的视觉或数值直觉
- 指数变化:更贴近人耳对音量、频率变化的感知
实际项目里,音量包络 很常用指数变化,因为听感通常更自然。
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 / 2getByteFrequencyData():获取频谱数据,范围 0~255getByteTimeDomainData():获取波形数据,范围 0~255smoothingTimeConstant:平滑程度,柱状图跳动是否更顺滑
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 做复杂运算
最基本的使用流程:
await audioContext.audioWorklet.addModule('/processor.js')new AudioWorkletNode(audioContext, 'processor-name')- 把它接入你的音频图
await ctx.audioWorklet.addModule('/processors/volume-meter.js');
const workletNode = new AudioWorkletNode(ctx, 'volume-meter');
source.connect(workletNode).connect(ctx.destination);
ScriptProcessorNodeScriptProcessorNode 已经属于过时方案。需要自定义处理时,应优先使用 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 AudioBuffer 和 AudioBufferSourceNode 有什么区别?
AudioBuffer:音频数据本身AudioBufferSourceNode:负责“播放这段数据”的节点
可以把前者理解为“磁带内容”,后者理解为“播放机”。
14.2 MediaElementAudioSourceNode 和 AudioBufferSourceNode 怎么选?
- 已经用
<audio>/<video>管理播放:选MediaElementAudioSourceNode - 想要更细粒度调度、切片、低延迟音效:选
AudioBufferSourceNode
14.3 GainNode 的 gain.value 和 gain.setValueAtTime() 有什么区别?
gain.value = x:立即改值,简单直接setValueAtTime():把变化安排到音频时间轴,更适合可预测、精确的变化
14.4 AnalyserNode 是不是“音效器”?
不是。它主要是“观察”音频数据,而不是“改变”音频数据。
面试高频问答
Q1:Web Audio 和 <audio> 有什么本质区别?
答:<audio> 更像一个现成播放器,适合播放、暂停、进度、倍速这类高层控制;Web Audio 是底层音频处理引擎,强调音频图、节点连接、精确调度、音效处理、分析和合成。简单播放用 <audio> 更省事,复杂处理用 Web Audio 更强。
Q2:AudioContext 的作用是什么?
答:它是整个音频系统的入口,负责创建节点、提供统一时钟 currentTime、维护输出终点 destination,并管理音频上下文状态(suspended、running、closed)。
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 真正学会,建议按这个顺序掌握:
- 先懂图模型:
AudioContext、AudioNode、AudioParam - 再懂时间轴:
currentTime、start()、参数自动化 - 然后做三类项目:播放器增强、音频可视化、短音效调度
- 最后进阶:
AudioWorklet、OfflineAudioContext、空间音频、WASM
一旦你习惯“节点图 + 音频时间轴”这套思维,Web Audio 就不会再像一堆零散 API,而会变成一个非常统一、非常强大的音频编程模型。