Back

使用 Web Audio API 播放声音

使用 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() 时间,以实现采样精确的播放。

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.

OpenReplay