AudioContext 基础
Web Audio API 提供了一个强大的音频处理系统,可以在浏览器中进行音频的播放、生成、处理和分析。AudioContext 是整个音频系统的入口,所有音频节点都创建在其下。本节将介绍 AudioContext 的创建、状态管理和浏览器自动播放策略。
前置知识
阅读本节前,建议先了解:SVG Sprite
基础概念
AudioContext 是 Web Audio API 的核心对象,它代表一个音频处理图(Audio Graph)。音频从源节点(如 OscillatorNode、AudioBufferSourceNode)出发,经过处理节点(如 GainNode、BiquadFilterNode),最终到达目的地(通常是扬声器,即 AudioContext.destination)。
音频处理图模型
源节点 处理节点 目的地
(Oscillator) → (GainNode) → (BiquadFilter) → (destination)
或 或
(AudioBuffer) → (AnalyserNode) → (destination)
或
(MediaElement) → (ConvolverNode) → (destination)创建 AudioContext
基本创建方式
javascript
// 标准(推荐)
const audioCtx = new AudioContext();
// 兼容旧浏览器(Safari)
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();AudioContext 属性
| 属性 | 类型 | 说明 |
|---|---|---|
state | string | 上下文状态:running、suspended、closed |
sampleRate | number | 采样率(Hz) |
currentTime | number | 当前时间(秒) |
destination | AudioDestinationNode | 音频输出目的地 |
baseLatency | number | 基础延迟时间(秒) |
outputLatency | number | 输出延迟时间(秒) |
AudioContext 状态
javascript
const audioCtx = new AudioContext();
console.log(audioCtx.state); // 可能是 "suspended"(自动播放策略)
console.log(audioCtx.sampleRate); // 通常是 44100 或 48000
console.log(audioCtx.currentTime); // 0.0(刚创建时)状态管理
state 属性
| 状态 | 说明 |
|---|---|
suspended | 暂停状态,不处理音频 |
running | 运行状态,正在处理音频 |
closed | 已关闭,无法恢复 |
resume() 和 suspend()
javascript
const audioCtx = new AudioContext();
// 恢复运行状态(通常需要用户交互触发)
audioCtx.resume().then(() => {
console.log('AudioContext 已恢复运行');
});
// 暂停
audioCtx.suspend().then(() => {
console.log('AudioContext 已暂停');
});
// 关闭(不可逆)
audioCtx.close().then(() => {
console.log('AudioContext 已关闭');
});状态变化监听
javascript
audioCtx.addEventListener('statechange', () => {
console.log(`AudioContext 状态变为: ${audioCtx.state}`);
});浏览器自动播放策略
现代浏览器要求 AudioContext 必须在用户交互后才能进入 running 状态。这是为了防止网页在未经用户同意的情况下自动播放声音。
自动播放策略的背景
javascript
// 在用户交互之前创建 AudioContext
const audioCtx = new AudioContext();
console.log(audioCtx.state); // "suspended"(大多数浏览器)
// 此时尝试播放音频会静默
const oscillator = audioCtx.createOscillator();
oscillator.connect(audioCtx.destination);
oscillator.start(); // 静默!因为上下文处于 suspended 状态解决方案:用户交互后 resume
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>AudioContext 自动播放</title>
<style>
body { font-family: sans-serif; padding: 20px; }
button { padding: 10px 20px; font-size: 16px; cursor: pointer; margin: 5px; }
#status { margin: 10px 0; padding: 10px; background: #f5f5f5; border-radius: 4px; }
</style>
</head>
<body>
<h1>Web Audio API - 自动播放策略</h1>
<div id="status">AudioContext 状态: 未创建</div>
<button id="btnResume">恢复 AudioContext 并播放</button>
<button id="btnSuspend">暂停 AudioContext</button>
<button id="btnBeep">播放提示音</button>
<script>
let audioCtx = null;
const statusEl = document.getElementById('status');
function updateStatus() {
if (audioCtx) {
statusEl.textContent = `AudioContext 状态: ${audioCtx.state} | 采样率: ${audioCtx.sampleRate}Hz`;
}
}
// 方案 1:点击按钮时创建并恢复
document.getElementById('btnResume').addEventListener('click', async () => {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
console.log('AudioContext 已创建');
}
if (audioCtx.state === 'suspended') {
await audioCtx.resume();
console.log('AudioContext 已恢复');
}
updateStatus();
});
// 暂停
document.getElementById('btnSuspend').addEventListener('click', async () => {
if (audioCtx && audioCtx.state === 'running') {
await audioCtx.suspend();
console.log('AudioContext 已暂停');
}
updateStatus();
});
// 播放提示音
document.getElementById('btnBeep').addEventListener('click', async () => {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
if (audioCtx.state === 'suspended') {
await audioCtx.resume();
}
// 创建一个短促的提示音
const oscillator = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
oscillator.type = 'sine';
oscillator.frequency.value = 440; // A4 音高
gainNode.gain.setValueAtTime(0.3, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.5);
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
oscillator.start(audioCtx.currentTime);
oscillator.stop(audioCtx.currentTime + 0.5);
updateStatus();
});
</script>
</body>
</html>方案 2:首次交互时自动初始化
javascript
let audioCtx = null;
function initAudio() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
}
// 在多个可能的交互入口注册
document.addEventListener('click', initAudio, { once: true });
document.addEventListener('touchstart', initAudio, { once: true });
document.addEventListener('keydown', initAudio, { once: true });方案 3:AudioContext 构造函数中设置
javascript
// 部分浏览器支持在构造时直接启动
const audioCtx = new AudioContext({ latencyHint: 'interactive' });
// latencyHint 选项
// 'balanced' - 平衡(默认)
// 'interactive' - 低延迟,适合交互
// 'playback' - 高延迟,适合播放详细说明
AudioContext 配置选项
javascript
const audioCtx = new AudioContext({
sampleRate: 44100, // 采样率
latencyHint: 'interactive', // 延迟提示
numberOfChannels: 2, // 声道数
});
// 注意:sampleRate 和 numberOfChannels 在创建后不可修改| 选项 | 类型 | 说明 |
|---|---|---|
sampleRate | number | 采样率,默认为系统首选值 |
latencyHint | string | 延迟偏好 |
numberOfChannels | number | 输出声道数 |
AudioContext 的生命周期
javascript
// 1. 创建
const audioCtx = new AudioContext();
// 2. 使用(创建节点、连接、播放)
const osc = audioCtx.createOscillator();
osc.connect(audioCtx.destination);
osc.start();
// 3. 暂停(节省资源)
await audioCtx.suspend();
// 4. 恢复
await audioCtx.resume();
// 5. 关闭(释放所有资源)
await audioCtx.close();currentTime 的作用
audioCtx.currentTime 是音频系统的内部时钟,所有音频调度的基准时间:
javascript
const now = audioCtx.currentTime;
// 从现在起 1 秒后开始播放
oscillator.start(now + 1);
// 从现在起 2 秒后停止
oscillator.stop(now + 2);
// 0.1 秒后改变音量
gainNode.gain.setValueAtTime(0.5, now + 0.1);注意事项
1. 每个 Tab 页面限制
大多数浏览器对每个 Tab 页面的 AudioContext 数量有限制(通常为 6 个)。创建过多会收到错误:
javascript
// 错误:创建过多的 AudioContext
for (let i = 0; i < 10; i++) {
new AudioContext(); // 超过限制会报错
}
// 正确:复用同一个 AudioContext
const audioCtx = new AudioContext();2. iOS Safari 的特殊限制
iOS Safari 对自动播放的限制更严格,且 AudioContext 必须在用户点击事件的回调中创建:
javascript
// iOS Safari 兼容写法
document.addEventListener('click', async () => {
// 必须在用户交互回调中创建
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
await audioCtx.resume(); // 可能仍然需要 resume
}, { once: true });3. 关闭后不可恢复
javascript
const audioCtx = new AudioContext();
await audioCtx.close();
// audioCtx.state === "closed"
// 不可恢复!必须创建新的 AudioContext
await audioCtx.resume(); // 报错!最佳实践
1. 封装 AudioContext 管理器
javascript
/**
* AudioContext 单例管理器
*/
class AudioManager {
static #instance = null;
#ctx = null;
static getInstance() {
if (!AudioManager.#instance) {
AudioManager.#instance = new AudioManager();
}
return AudioManager.#instance;
}
getContext() {
return this.#ctx;
}
async init() {
if (!this.#ctx) {
this.#ctx = new (window.AudioContext || window.webkitAudioContext)();
}
if (this.#ctx.state === 'suspended') {
await this.#ctx.resume();
}
return this.#ctx;
}
async suspend() {
if (this.#ctx && this.#ctx.state === 'running') {
await this.#ctx.suspend();
}
}
async close() {
if (this.#ctx) {
await this.#ctx.close();
this.#ctx = null;
}
}
}
// 使用
const manager = AudioManager.getInstance();
document.addEventListener('click', () => manager.init(), { once: true });2. 设备采样率检测
javascript
/**
* 获取最佳采样率
*/
function getBestSampleRate() {
// 检测设备支持的高采样率
const testCtx = new (window.AudioContext || window.webkitAudioContext)();
const rate = testCtx.sampleRate;
testCtx.close();
return rate;
}
console.log(`设备采样率: ${getBestSampleRate()}Hz`);
// 通常为 44100 或 480003. 错误处理
javascript
async function safeInitAudio() {
try {
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
if (audioCtx.state === 'suspended') {
await audioCtx.resume();
}
return audioCtx;
} catch (error) {
if (error.name === 'NotAllowedError') {
console.warn('用户拒绝了音频权限');
} else {
console.error('AudioContext 创建失败:', error);
}
return null;
}
}下一节
继续学习:音频播放与控制