Skip to content

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 属性

属性类型说明
statestring上下文状态:runningsuspendedclosed
sampleRatenumber采样率(Hz)
currentTimenumber当前时间(秒)
destinationAudioDestinationNode音频输出目的地
baseLatencynumber基础延迟时间(秒)
outputLatencynumber输出延迟时间(秒)

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 在创建后不可修改
选项类型说明
sampleRatenumber采样率,默认为系统首选值
latencyHintstring延迟偏好
numberOfChannelsnumber输出声道数

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 或 48000

3. 错误处理

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;
  }
}

下一节

继续学习:音频播放与控制

参考链接