Skip to content

音频播放与控制

Web Audio API 通过各种节点(Node)实现音频的播放、控制和处理。常见的源节点包括 OscillatorNode(生成音频波形)、AudioBufferSourceNode(播放音频缓冲区)和 MediaElementAudioSourceNode(包装 <audio> 元素)。处理节点如 GainNode 控制音量,BiquadFilterNode 控制滤波。本节将详细介绍音频播放与节点连接的核心知识。

前置知识

阅读本节前,建议先了解:AudioContext 基础

基础概念

Web Audio API 使用"节点图"(Node Graph)模型来处理音频。音频数据从源节点出发,通过连接(connect())传递到一个或多个处理节点,最终到达目的地节点(destination,代表扬声器)。

常用节点类型

节点类型说明典型用途
OscillatorNode波形生成器合成音效、提示音
AudioBufferSourceNode缓冲区播放器播放音频文件
GainNode音量控制调节音量、淡入淡出
BiquadFilterNode滤波器低通/高通/带通滤波
AnalyserNode频谱分析器可视化数据获取
MediaElementAudioSourceNode媒体元素包装控制 <audio>
DelayNode延迟回声效果

OscillatorNode - 波形生成

基本用法

javascript
const audioCtx = new AudioContext();

// 创建振荡器
const oscillator = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();

// 配置振荡器
oscillator.type = 'sine';           // 波形类型
oscillator.frequency.value = 440;    // 频率(Hz)

// 连接:振荡器 → 增益 → 输出
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);

// 设置音量并淡出
gainNode.gain.setValueAtTime(0.3, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 1);

// 播放
oscillator.start();
oscillator.stop(audioCtx.currentTime + 1); // 1秒后停止

波形类型

类型说明音色
sine正弦波纯净、柔和
square方波8 位游戏风格
sawtooth锯齿波明亮、粗糙
triangle三角波温暖、柔和
custom自定义波形PeriodicWave

频率控制

javascript
const oscillator = audioCtx.createOscillator();
oscillator.type = 'sine';

// 静态设置
oscillator.frequency.value = 440; // A4

// 使用 setValueAtTime 和 linearRampToValueAtTime 做频率滑动
oscillator.frequency.setValueAtTime(440, audioCtx.currentTime);
oscillator.frequency.linearRampToValueAtTime(880, audioCtx.currentTime + 1);

// 使用 exponentialRampToValueAtTime
oscillator.frequency.setValueAtTime(220, audioCtx.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(1760, audioCtx.currentTime + 2);

GainNode - 音量控制

基本用法

javascript
const gainNode = audioCtx.createGain();

// 设置音量(0.0 ~ 1.0,但可以超过 1.0)
gainNode.gain.value = 0.5; // 50% 音量

// 立即设置到指定值
gainNode.gain.setValueAtTime(0.8, audioCtx.currentTime);

// 线性渐变到目标值
gainNode.gain.linearRampToValueAtTime(0.1, audioCtx.currentTime + 2);

// 指数渐变(不能到 0)
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 1);

// 设置特定时间点的值
gainNode.gain.setValueAtTime(0.0, audioCtx.currentTime);
gainNode.gain.linearRampToValueAtTime(1.0, audioCtx.currentTime + 0.1); // 淡入
gainNode.gain.setValueAtTime(1.0, audioCtx.currentTime + 2);
gainNode.gain.linearRampToValueAtTime(0.0, audioCtx.currentTime + 3); // 淡出

AudioParam 的方法

方法说明
setValueAtTime(value, time)在指定时间设置值
linearRampToValueAtTime(value, time)线性渐变到目标值
exponentialRampToValueAtTime(value, time)指数渐变到目标值
setTargetAtTime(target, startTime, timeConstant)指数衰减到目标值
setValueCurveAtTime(values, startTime, duration)沿自定义曲线变化

AnalyserNode - 频谱分析

基本用法

javascript
const analyser = audioCtx.createAnalyser();

// 配置分析器
analyser.fftSize = 2048;          // FFT 大小(决定频率分辨率)
analyser.smoothingTimeConstant = 0.8; // 时间平滑系数(0~1)

// 连接
source.connect(analyser);
analyser.connect(audioCtx.destination);

// 获取频率数据
const bufferLength = analyser.frequencyBinCount; // fftSize / 2
const dataArray = new Uint8Array(bufferLength);
analyser.getByteFrequencyData(dataArray); // 0~255 的频率数据

// 获取时域数据(波形)
const timeData = new Uint8Array(bufferLength);
analyser.getByteTimeDomainData(timeData); // 0~255 的时域数据

AnalyserNode 配置

属性说明推荐值
fftSizeFFT 窗口大小(2 的幂)2048
frequencyBinCount频率数据长度(只读)fftSize / 2
smoothingTimeConstant平滑系数0.8
minDecibels最小分贝值-100
maxDecibels最大分贝值-30

实战示例

示例 1:简单音乐播放器

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Web Audio 播放器</title>
  <style>
    body { font-family: sans-serif; padding: 20px; }
    .controls { display: flex; gap: 10px; align-items: center; margin: 15px 0; }
    button { padding: 8px 16px; cursor: pointer; }
    input[type="range"] { width: 200px; }
    label { min-width: 60px; }
  </style>
</head>
<body>
  <h1>Web Audio API 播放器</h1>

  <div class="controls">
    <button id="btnPlay">播放</button>
    <button id="btnStop">停止</button>
  </div>

  <div class="controls">
    <label>频率:</label>
    <input type="range" id="freqSlider" min="100" max="2000" value="440">
    <span id="freqValue">440 Hz</span>
  </div>

  <div class="controls">
    <label>音量:</label>
    <input type="range" id="volumeSlider" min="0" max="100" value="50">
    <span id="volumeValue">50%</span>
  </div>

  <div class="controls">
    <label>波形:</label>
    <select id="waveType">
      <option value="sine">正弦波 (sine)</option>
      <option value="square">方波 (square)</option>
      <option value="sawtooth">锯齿波 (sawtooth)</option>
      <option value="triangle">三角波 (triangle)</option>
    </select>
  </div>

  <script>
    let audioCtx = null;
    let oscillator = null;
    let gainNode = null;
    let isPlaying = false;

    const freqSlider = document.getElementById('freqSlider');
    const volumeSlider = document.getElementById('volumeSlider');
    const waveTypeSelect = document.getElementById('waveType');
    const freqValue = document.getElementById('freqValue');
    const volumeValue = document.getElementById('volumeValue');

    // 频率滑块
    freqSlider.addEventListener('input', () => {
      const freq = parseFloat(freqSlider.value);
      freqValue.textContent = `${freq} Hz`;
      if (oscillator) {
        oscillator.frequency.setValueAtTime(freq, audioCtx.currentTime);
      }
    });

    // 音量滑块
    volumeSlider.addEventListener('input', () => {
      const volume = parseInt(volumeSlider.value);
      volumeValue.textContent = `${volume}%`;
      if (gainNode) {
        gainNode.gain.setValueAtTime(volume / 100, audioCtx.currentTime);
      }
    });

    // 波形选择
    waveTypeSelect.addEventListener('change', () => {
      if (oscillator) {
        oscillator.type = waveTypeSelect.value;
      }
    });

    // 播放
    document.getElementById('btnPlay').addEventListener('click', async () => {
      if (!audioCtx) {
        audioCtx = new (window.AudioContext || window.webkitAudioContext)();
      }
      if (audioCtx.state === 'suspended') {
        await audioCtx.resume();
      }

      if (isPlaying) return;

      // 创建节点
      oscillator = audioCtx.createOscillator();
      gainNode = audioCtx.createGain();

      oscillator.type = waveTypeSelect.value;
      oscillator.frequency.value = parseFloat(freqSlider.value);
      gainNode.gain.value = parseInt(volumeSlider.value) / 100;

      // 连接
      oscillator.connect(gainNode);
      gainNode.connect(audioCtx.destination);

      oscillator.start();
      isPlaying = true;
      document.getElementById('btnPlay').textContent = '播放中...';
    });

    // 停止
    document.getElementById('btnStop').addEventListener('click', () => {
      if (oscillator && isPlaying) {
        gainNode.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.1);
        oscillator.stop(audioCtx.currentTime + 0.15);
        oscillator = null;
        gainNode = null;
        isPlaying = false;
        document.getElementById('btnPlay').textContent = '播放';
      }
    });
  </script>
</body>
</html>

示例 2:播放音频文件

javascript
/**
 * 使用 AudioBufferSourceNode 播放音频文件
 */
async function playAudioFile(url) {
  const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
  if (audioCtx.state === 'suspended') await audioCtx.resume();

  try {
    // 1. 加载音频文件
    const response = await fetch(url);
    const arrayBuffer = await response.arrayBuffer();

    // 2. 解码音频数据
    const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);

    // 3. 创建源节点
    const source = audioCtx.createBufferSource();
    source.buffer = audioBuffer;

    // 4. 连接到输出
    source.connect(audioCtx.destination);

    // 5. 播放
    source.start();

    // 播放结束后
    source.onended = () => {
      console.log('播放结束');
      audioCtx.close();
    };
  } catch (error) {
    console.error('音频播放失败:', error);
  }
}

// 使用
// playAudioFile('audio/music.mp3');

示例 3:多音符播放(简单乐器)

javascript
/**
 * 简单的音符播放器
 */
class SimpleSynth {
  constructor() {
    this.audioCtx = null;
  }

  async init() {
    if (!this.audioCtx) {
      this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    }
    if (this.audioCtx.state === 'suspended') {
      await this.audioCtx.resume();
    }
  }

  /**
   * 播放一个音符
   * @param {number} frequency - 频率(Hz)
   * @param {number} duration - 持续时间(秒)
   * @param {number} startTime - 开始时间(秒)
   */
  playNote(frequency, duration = 0.5, startTime = null) {
    const now = startTime || this.audioCtx.currentTime;

    const oscillator = this.audioCtx.createOscillator();
    const gainNode = this.audioCtx.createGain();

    oscillator.type = 'triangle';
    oscillator.frequency.setValueAtTime(frequency, now);

    // ADSR 包络(简化版)
    gainNode.gain.setValueAtTime(0, now);
    gainNode.gain.linearRampToValueAtTime(0.5, now + 0.05);  // Attack
    gainNode.gain.linearRampToValueAtTime(0.3, now + 0.15);  // Decay
    gainNode.gain.setValueAtTime(0.3, now + duration - 0.1);  // Sustain
    gainNode.gain.linearRampToValueAtTime(0, now + duration);  // Release

    oscillator.connect(gainNode);
    gainNode.connect(this.audioCtx.destination);

    oscillator.start(now);
    oscillator.stop(now + duration + 0.01);
  }
}

// 使用
const synth = new SimpleSynth();
document.addEventListener('click', async () => {
  await synth.init();

  // 播放 C 大调音阶
  const C4 = 261.63, D4 = 293.66, E4 = 329.63, F4 = 349.23;
  const G4 = 392.00, A4 = 440.00, B4 = 493.88, C5 = 523.25;
  const notes = [C4, D4, E4, F4, G4, A4, B4, C5];

  notes.forEach((freq, i) => {
    synth.playNote(freq, 0.4, synth.audioCtx.currentTime + i * 0.5);
  });
}, { once: true });

注意事项

1. OscillatorNode 只能启动一次

javascript
// 错误:尝试重复使用同一个振荡器
const osc = audioCtx.createOscillator();
osc.start();
osc.stop();
osc.start(); // 报错!OscillatorNode 只能 start 一次

// 正确:需要重新创建
const newOsc = audioCtx.createOscillator();
newOsc.start();

2. exponentialRampToValueAtTime 不能到 0

javascript
// 错误
gain.gain.exponentialRampToValueAtTime(0, time); // 报错!

// 正确:使用接近 0 的值
gain.gain.exponentialRampToValueAtTime(0.001, time);

3. AudioBufferSourceNode 也只能用一次

javascript
// 每次播放都需要创建新的 AudioBufferSourceNode
function playBuffer(audioBuffer) {
  const source = audioCtx.createBufferSource();
  source.buffer = audioBuffer;
  source.connect(audioCtx.destination);
  source.start();
  // source 用完后需要重新创建才能再次播放
}

最佳实践

1. 节点连接链路封装

javascript
/**
 * 创建简单的音频链路:源 → 增益 → 输出
 */
function createAudioChain(audioCtx) {
  const gainNode = audioCtx.createGain();
  gainNode.connect(audioCtx.destination);
  return { gainNode, destination: audioCtx.destination };
}

2. 资源清理

javascript
// 播放结束时清理节点
function playSoundWithCleanup(audioCtx, duration) {
  const osc = audioCtx.createOscillator();
  const gain = audioCtx.createGain();

  osc.connect(gain);
  gain.connect(audioCtx.destination);

  osc.start();

  // 淡出并停止
  gain.gain.setValueAtTime(0.5, audioCtx.currentTime + duration - 0.1);
  gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + duration);
  osc.stop(audioCtx.currentTime + duration);
}

下一节

继续学习:音频可视化

参考链接