音频播放与控制
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 配置
| 属性 | 说明 | 推荐值 |
|---|---|---|
fftSize | FFT 窗口大小(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);
}下一节
继续学习:音频可视化