使用 Web Audio API 播放声音
你想在浏览器中精确控制音频播放——调度、效果、合成——但 <audio> 元素无法满足需求。Web Audio API 解决了这个问题,但其文档混杂了已弃用的模式和当前最佳实践。本文阐明了 Web Audio API 声音播放的现代方法,涵盖 AudioContext 基础、源节点和音频图模型,不涉及过时的内容。
核心要点
- 每个应用程序创建一个
AudioContext,并始终在用户交互后使用resume()处理挂起状态 - 对预加载的音频文件使用
AudioBufferSourceNode,对合成音调使用OscillatorNode——两者都是一次性节点 - 避免使用已弃用的
ScriptProcessorNode;使用AudioWorklet进行自定义音频处理 - 将通过
setSinkId()进行的输出设备路由视为实验性功能,浏览器支持不完整
理解 AudioContext 基础
每个 Web Audio 应用程序都从 AudioContext 开始。这个对象管理所有音频操作,并为调度提供时钟。可以将其视为音频图的运行时环境。
const audioContext = new AudioContext()
一个关键限制:浏览器会挂起新的上下文,直到发生用户手势。始终检查 audioContext.state 并从点击或按键处理程序调用 resume():
button.addEventListener('click', async () => {
if (audioContext.state === 'suspended') {
await audioContext.resume()
}
// 现在可以安全播放音频
})
在大多数应用程序中,单个 AudioContext 就足够了,也是推荐的做法。创建多个上下文会增加资源使用,并使时序和同步更难管理。(参见:https://developer.mozilla.org/en-US/docs/Web/API/AudioContext)
音频图模型
Web Audio 使用有向节点图。音频从源节点流经处理节点,最终到达目标节点。audioContext.destination 代表默认输出——通常是用户的扬声器。
节点通过 connect() 方法连接:
sourceNode.connect(gainNode)
gainNode.connect(audioContext.destination)
这种模块化设计允许你在链中的任何位置插入滤波器、增益控制或分析器。重新配置图时,使用 disconnect() 断开节点连接。
用于声音播放的源节点
两种源类型处理大多数播放场景。
AudioBufferSourceNode
对于预加载的音频文件,将数据解码为 AudioBuffer,然后通过 AudioBufferSourceNode 播放:
const response = await fetch('sound.mp3')
const arrayBuffer = await response.arrayBuffer()
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer)
const source = audioContext.createBufferSource()
source.buffer = audioBuffer
source.connect(audioContext.destination)
source.start()
每个 AudioBufferSourceNode 只播放一次。为每次播放创建一个新的源节点——它们是轻量级的。底层的 AudioBuffer 可以重复使用。
请注意,虽然现代浏览器中 decodeAudioData() 支持 Promise,但旧版 Safari 需要回调形式。这主要是遗留问题,但对于长尾兼容性仍然相关。
OscillatorNode
对于合成音调,使用 OscillatorNode:
const oscillator = audioContext.createOscillator()
oscillator.type = 'sine'
oscillator.frequency.setValueAtTime(440, audioContext.currentTime)
oscillator.connect(audioContext.destination)
oscillator.start()
oscillator.stop(audioContext.currentTime + 1)
振荡器也只播放一次。使用 audioContext.currentTime 调度 start() 和 stop() 时间,以实现采样精确的播放。
Discover how at OpenReplay.com.
AudioWorklet vs ScriptProcessor
对于自定义音频处理,避免使用 ScriptProcessorNode。它已被弃用,在主线程上运行,并在负载下导致音频卡顿。
AudioWorklet 是现代替代方案。它在专用的音频渲染线程中运行,具有确定性时序:
// processor.js
class MyProcessor extends AudioWorkletProcessor {
process(inputs, outputs, parameters) {
// 在此处理音频
return true
}
}
registerProcessor('my-processor', MyProcessor)
// main.js
await audioContext.audioWorklet.addModule('processor.js')
const workletNode = new AudioWorkletNode(audioContext, 'my-processor')
workletNode.connect(audioContext.destination)
AudioWorklet 在所有现代常青浏览器中都受支持。主要限制往往与环境相关(安全上下文、打包、跨域设置),而不是 API 支持不足。
Web Audio 输出路由限制
API 的输出路由能力有限且依赖于浏览器。默认情况下,音频路由到 audioContext.destination,它映射到系统的默认输出设备。
AudioContext.setSinkId() 允许选择特定的输出设备,但应将其视为实验性且高度受限的功能。它需要安全上下文(HTTPS)、用户权限和适当的权限策略标头。实际上,它在 Safari 或 iOS 上不可用,不应依赖它进行跨平台扬声器切换。
对于需要输出设备选择的应用程序,显式检测支持:
if (typeof audioContext.setSinkId === 'function') {
// 存在并不保证可用性;权限和策略仍可能阻止它
}
即使该方法存在,由于策略或平台限制,调用也可能失败。构建后备方案并向用户清楚地传达这些限制。
实践考虑
自动播放策略会阻止音频,直到用户交互。设计你的 UI,在初始化播放之前需要点击。
CORS 限制适用于跨域获取音频文件。确保适当的标头或在同一源上托管文件。
内存管理对于缓冲区密集型应用程序很重要。取消引用未使用的 AudioBuffer 对象,并断开你已完成使用的节点。
移动浏览器施加额外的限制——特别是 iOS Safari。在真实设备上测试,而不仅仅是模拟器。
结论
Web Audio API 通过基于图的模型提供强大的低级音频控制。从单个 AudioContext 开始,遵守自动播放策略,并使用现代模式:AudioBufferSourceNode 用于样本,OscillatorNode 用于合成,AudioWorklet 用于自定义处理。将输出设备路由视为高度受限且依赖于平台的功能。对所有内容进行特性检测,并针对浏览器实际施加的限制进行构建。
常见问题
浏览器会阻止 AudioContext 创建,直到发生用户交互。你的开发环境可能具有更宽松的设置。在尝试播放之前,始终在点击或按键处理程序内检查 audioContext.state 并调用 resume()。此自动播放策略在所有现代浏览器中普遍适用。
不可以。AudioBufferSourceNode 实例在设计上是一次性的。调用 start() 后,节点无法重新启动。为每次播放创建一个新的源节点,同时重复使用底层的 AudioBuffer。源节点是轻量级的,因此这种模式对性能的影响很小。
使用 AudioContext.setSinkId(),但将其视为高度受限的功能。它需要 HTTPS、用户权限和宽松的策略,并且在 Safari 或 iOS 上不受支持。始终进行特性检测,优雅地处理失败,并在设备选择不可用时通知用户。
ScriptProcessorNode 已被弃用,在主线程上运行,在繁重处理期间会导致音频卡顿。AudioWorklet 在专用的音频渲染线程中运行,具有确定性时序,使其适合实时音频操作。在新项目中始终使用 AudioWorklet 进行自定义处理。
Understand every bug
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.