第七章:Web Audio API 核心 —— 浏览器里的音频处理引擎
前六章我们解决的都是"怎么播放音频"的问题。从这章开始,我们进入一个全新的维度——怎么处理音频信号本身。Web Audio API 是浏览器内置的一套完整音频处理框架,能做的事情远超你的想象:实时均衡器、混响效果、音频可视化、音频合成、3D 空间音效……这一切都不需要任何插件,纯浏览器原生实现。
一、Web Audio API 的设计哲学:音频图
Web Audio API 的核心设计思想来自模块化音频合成(Modular Synthesis),这是专业音频领域几十年前就成熟的概念。整个 API 围绕一个核心模型构建:音频图(Audio Graph)。
音频图是一张有向无环图,由节点(AudioNode)和连接(connect)构成。音频数据从源节点出发,经过若干处理节点的变换,最终流向目标节点(通常是扬声器输出):
[源节点] ──→ [处理节点A] ──→ [处理节点B] ──→ [目标节点]
↑
(可以分叉、合并、循环)
这个设计的精妙之处在于可组合性——每个节点只做一件事,通过任意组合就能构建出极其复杂的音频处理链。就像搭积木一样,你可以随时插入、移除、替换任意节点,而不影响整体结构。
二、AudioContext:一切的起点
AudioContext 是整个 Web Audio API 的运行时环境,所有节点都必须在同一个 AudioContext 中创建,才能相互连接。
// 创建 AudioContext
const audioCtx = new AudioContext();
// 检查状态
console.log(audioCtx.state); // "suspended" | "running" | "closed"
// 当前时间(高精度时钟,单位:秒)
console.log(audioCtx.currentTime);
// 采样率(通常是设备的原生采样率:44100 或 48000)
console.log(audioCtx.sampleRate);AudioContext 的三种状态
suspended 是 AudioContext 的初始状态。由于浏览器的自动播放策略,新创建的 AudioContext 默认处于暂停状态,必须在用户交互后才能恢复运行:
// 在用户点击时恢复 AudioContext
document.addEventListener('click', async () => {
if (audioCtx.state === 'suspended') {
await audioCtx.resume();
console.log('AudioContext 已恢复:', audioCtx.state); // "running"
}
}, { once: true });OfflineAudioContext:离线渲染
除了实时处理,Web Audio API 还提供了 OfflineAudioContext,用于非实时的离线渲染——以最快速度渲染音频,不受实时播放速率限制,适合音频导出、预处理等场景:
// 创建离线上下文:2 声道,44100Hz,渲染 10 秒音频
const offlineCtx = new OfflineAudioContext({
numberOfChannels: 2,
length: 44100 * 10, // 采样点数 = 采样率 × 时长
sampleRate: 44100,
});
// 在离线上下文中构建音频图...
const oscillator = offlineCtx.createOscillator();
oscillator.connect(offlineCtx.destination);
oscillator.start(0);
// 开始渲染(非实时,会尽快完成)
const renderedBuffer = await offlineCtx.startRendering();
console.log('离线渲染完成,时长:', renderedBuffer.duration);三、音频节点全家族
Web Audio API 定义了十余种节点,按功能分为三大类。
3.1 源节点(Source Nodes)
源节点是音频图的起点,只有输出,没有输入。
AudioBufferSourceNode — 播放 AudioBuffer
用于播放预先解码到内存中的音频数据,是游戏音效、短音频片段的首选方案:
// 第一步:通过 fetch 获取音频文件的 ArrayBuffer
const response = await fetch('sound.mp3');
const arrayBuffer = await response.arrayBuffer();
// 第二步:解码为 AudioBuffer(PCM 数据)
const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
// 第三步:创建 AudioBufferSourceNode 并播放
function playBuffer(buffer, when = 0, offset = 0) {
const source = audioCtx.createBufferSource();
source.buffer = buffer;
source.connect(audioCtx.destination);
source.start(when, offset); // when: 何时开始;offset: 从哪里开始
return source;
}
// 立即播放
playBuffer(audioBuffer);
// 精确调度:1 秒后播放,从第 5 秒开始
playBuffer(audioBuffer, audioCtx.currentTime + 1, 5);重要:
AudioBufferSourceNode是一次性的——播放完毕后不能复用,每次播放都需要新建一个实例。这是设计决策,不是 bug。
MediaElementAudioSourceNode — 连接 <audio> 元素
将 <audio> 或 <video> 元素的输出接入 Web Audio 处理链,是最常见的"桥接"方式:
const audio = document.getElementById('myAudio');
const source = audioCtx.createMediaElementSource(audio);
// 音频数据从 audio 元素流入 Web Audio 处理链
source.connect(audioCtx.destination);
// 注意:一旦创建 MediaElementAudioSourceNode,
// audio 元素的输出就完全由 Web Audio 接管
// 必须连接到 destination,否则听不到声音MediaStreamAudioSourceNode — 连接 MediaStream
将麦克风、屏幕录制等 MediaStream 接入 Web Audio,是实时音频处理的入口:
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const micSource = audioCtx.createMediaStreamSource(stream);
// 麦克风输入 → Web Audio 处理链
micSource.connect(audioCtx.destination); // 直接输出(会有回声,慎用)OscillatorNode — 振荡器(信号发生器)
生成周期性波形信号,是音频合成的基础构件:
const osc = audioCtx.createOscillator();
// 波形类型:sine(正弦)| square(方波)| sawtooth(锯齿)| triangle(三角)| custom
osc.type = 'sine';
// 频率(Hz):A4 音符 = 440Hz
osc.frequency.value = 440;
// 音分偏移(cents):100 cents = 1 个半音
osc.detune.value = 0;
osc.connect(audioCtx.destination);
osc.start(audioCtx.currentTime);
osc.stop(audioCtx.currentTime + 1); // 1 秒后停止ConstantSourceNode — 常量源
输出一个恒定值的信号,常用于参数自动化和信号路由:
const constantSource = audioCtx.createConstantSource();
constantSource.offset.value = 0.5;
constantSource.start();3.2 处理节点(Processing Nodes)
处理节点有输入和输出,对音频信号进行变换。
GainNode — 增益(音量控制)
最简单也最常用的节点,控制信号的幅度:
const gainNode = audioCtx.createGain();
gainNode.gain.value = 0.5; // 音量降低 50%
// 平滑地在 1 秒内将音量从 1 渐变到 0(淡出效果)
gainNode.gain.setValueAtTime(1, audioCtx.currentTime);
gainNode.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 1);BiquadFilterNode — 双二阶滤波器(均衡器核心)
实现各种频率滤波效果,是均衡器(EQ)的基础构件:
const filter = audioCtx.createBiquadFilter();
// 滤波器类型
filter.type = 'lowpass'; // 低通:保留低频,截止高频
// filter.type = 'highpass'; // 高通:保留高频,截止低频
// filter.type = 'bandpass'; // 带通:只保留特定频段
// filter.type = 'peaking'; // 峰值:提升/衰减特定频段(参数均衡器)
// filter.type = 'notch'; // 陷波:消除特定频率
// filter.type = 'lowshelf'; // 低架:提升/衰减低频段
// filter.type = 'highshelf';// 高架:提升/衰减高频段
filter.frequency.value = 1000; // 截止/中心频率(Hz)
filter.Q.value = 1; // 品质因数(Q 值越高,频率响应越陡峭)
filter.gain.value = 6; // 增益(dB),仅 peaking/shelf 类型有效DelayNode — 延迟
将信号延迟一段时间后输出,是混响、回声效果的基础:
const delay = audioCtx.createDelay(5.0); // 最大延迟 5 秒
delay.delayTime.value = 0.3; // 当前延迟 300msDynamicsCompressorNode — 动态压缩器
压缩音频的动态范围,让响亮的部分变小、安静的部分变大,使整体音量更均衡:
const compressor = audioCtx.createDynamicsCompressor();
compressor.threshold.value = -24; // 压缩触发阈值(dBFS)
compressor.knee.value = 30; // 软拐点宽度(dB)
compressor.ratio.value = 12; // 压缩比(12:1)
compressor.attack.value = 0.003; // 起音时间(秒)
compressor.release.value = 0.25; // 释音时间(秒)ConvolverNode — 卷积混响
通过与**脉冲响应(IR,Impulse Response)**进行卷积运算,模拟真实空间的混响效果。用真实录制的教堂、音乐厅等场所的 IR 文件,就能让声音"置身"于那个空间:
const convolver = audioCtx.createConvolver();
// 加载脉冲响应文件
const irResponse = await fetch('church-ir.wav');
const irBuffer = await irResponse.arrayBuffer();
convolver.buffer = await audioCtx.decodeAudioData(irBuffer);
// 干湿信号混合(dry/wet mix)
const dryGain = audioCtx.createGain();
const wetGain = audioCtx.createGain();
dryGain.gain.value = 0.5; // 原始信号 50%
wetGain.gain.value = 0.5; // 混响信号 50%
source.connect(dryGain);
source.connect(convolver);
convolver.connect(wetGain);
dryGain.connect(audioCtx.destination);
wetGain.connect(audioCtx.destination);WaveShaperNode — 波形整形(失真效果)
通过一条传递函数曲线对信号进行非线性变换,实现失真(Distortion)、饱和(Saturation)等效果:
const waveshaper = audioCtx.createWaveShaper();
// 生成软削波(soft clipping)失真曲线
function makeDistortionCurve(amount = 50) {
const samples = 256;
const curve = new Float32Array(samples);
const k = amount;
for (let i = 0; i < samples; i++) {
const x = (i * 2) / samples - 1; // -1 ~ 1
curve[i] = ((Math.PI + k) * x) / (Math.PI + k * Math.abs(x));
}
return curve;
}
waveshaper.curve = makeDistortionCurve(100);
waveshaper.oversample = '4x'; // 过采样,减少混叠失真StereoPannerNode — 立体声声像
控制声音在左右声道的位置:
const panner = audioCtx.createStereoPanner();
panner.pan.value = -1; // 完全左声道
panner.pan.value = 0; // 居中
panner.pan.value = 1; // 完全右声道PannerNode — 3D 空间音效
在三维空间中定位声源,实现 HRTF(头相关传递函数)空间音效:
const panner3D = audioCtx.createPanner();
panner3D.panningModel = 'HRTF'; // 使用 HRTF 模型,最真实
panner3D.distanceModel = 'inverse'; // 距离衰减模型
panner3D.refDistance = 1;
panner3D.maxDistance = 10000;
panner3D.rolloffFactor = 1;
// 设置声源位置(x, y, z)
panner3D.positionX.value = 5;
panner3D.positionY.value = 0;
panner3D.positionZ.value = -3;
// 设置听者位置
audioCtx.listener.positionX.value = 0;
audioCtx.listener.positionY.value = 0;
audioCtx.listener.positionZ.value = 0;3.3 分析与目标节点
AnalyserNode — 频谱分析
提取音频信号的频域和时域数据,是音频可视化的核心节点(第八章重点展开):
const analyser = audioCtx.createAnalyser();
analyser.fftSize = 2048; // FFT 大小,决定频率分辨率
analyser.smoothingTimeConstant = 0.8; // 平滑系数(0~1)
const bufferLength = analyser.frequencyBinCount; // fftSize / 2
const dataArray = new Uint8Array(bufferLength);
function getFrequencyData() {
analyser.getByteFrequencyData(dataArray); // 频域数据(0~255)
return dataArray;
}
function getTimeDomainData() {
analyser.getByteTimeDomainData(dataArray); // 时域波形数据
return dataArray;
}AudioDestinationNode — 最终输出
音频图的终点,对应设备的扬声器输出。通过 audioCtx.destination 访问,无需手动创建:
// 所有处理链的最终节点都要连接到这里
gainNode.connect(audioCtx.destination);MediaStreamAudioDestinationNode — 输出为 MediaStream
将 Web Audio 处理链的输出转换为 MediaStream,可以用于 WebRTC 传输或录音:
const streamDestination = audioCtx.createMediaStreamDestination();
gainNode.connect(streamDestination);
// 获取 MediaStream,用于录音或 WebRTC
const outputStream = streamDestination.stream;
const recorder = new MediaRecorder(outputStream);四、AudioParam:参数自动化系统
AudioParam 是 Web Audio API 中最精妙的设计之一。几乎所有节点的可变参数(频率、增益、延迟时间等)都是 AudioParam 类型,支持精确的时间调度和自动化曲线。
直接设置 vs 调度设置
const gain = audioCtx.createGain();
// 方式一:直接设置(立即生效)
gain.gain.value = 0.5;
// 方式二:调度设置(在指定时间生效)
gain.gain.setValueAtTime(0.5, audioCtx.currentTime + 1);自动化方法全览
const param = gain.gain;
const t = audioCtx.currentTime;
// 在指定时间设置为指定值(瞬变)
param.setValueAtTime(1.0, t);
// 线性渐变到目标值
param.linearRampToValueAtTime(0.0, t + 2); // 2 秒内线性淡出
// 指数渐变到目标值(更自然,符合人耳感知)
param.exponentialRampToValueAtTime(0.001, t + 2); // 指数淡出(不能到 0)
// 以指数方式趋近目标值(RC 电路充放电曲线)
param.setTargetAtTime(0.5, t, 0.5); // timeConstant=0.5s
// 用自定义曲线设置一段时间内的值
const curve = new Float32Array([0, 0.5, 1, 0.5, 0]); // 先升后降
param.setValueCurveAtTime(curve, t, 2); // 在 2 秒内按曲线变化
// 取消所有已调度的自动化
param.cancelScheduledValues(t);
// 取消并保持当前值
param.cancelAndHoldAtTime(t);用 AudioParam 连接节点(调制)
AudioParam 不仅可以手动设置,还可以接受另一个节点的输出作为输入,实现参数调制(Modulation)——这是合成器的核心技术:
// LFO(低频振荡器)调制音量,实现"颤音"效果
const lfo = audioCtx.createOscillator();
lfo.frequency.value = 5; // 5Hz 调制频率
const lfoGain = audioCtx.createGain();
lfoGain.gain.value = 0.3; // 调制深度
const mainGain = audioCtx.createGain();
mainGain.gain.value = 0.7;
lfo.connect(lfoGain);
lfoGain.connect(mainGain.gain); // LFO 输出连接到 gain 参数!
lfo.start();
// 效果:mainGain 的增益在 0.4~1.0 之间以 5Hz 频率振荡五、实战:构建一个完整的音频处理链
把本章所有知识整合起来,构建一个带有均衡器 + 压缩器 + 混响 + 音量控制的完整音频处理链:
class AudioProcessingChain {
constructor() {
this.ctx = new AudioContext();
this._buildGraph();
}
_buildGraph() {
const ctx = this.ctx;
// ── 源节点(由外部连接)──────────────────────────────
// 留空,通过 connect(chain.input) 接入
// ── 输入增益 ─────────────────────────────────────────
this.inputGain = ctx.createGain();
this.inputGain.gain.value = 1.0;
// ── 三段均衡器 ───────────────────────────────────────
this.eqLow = ctx.createBiquadFilter();
this.eqLow.type = 'lowshelf';
this.eqLow.frequency.value = 200;
this.eqLow.gain.value = 0; // dB,正值提升,负值衰减
this.eqMid = ctx.createBiquadFilter();
this.eqMid.type = 'peaking';
this.eqMid.frequency.value = 1000;
this.eqMid.Q.value = 1;
this.eqMid.gain.value = 0;
this.eqHigh = ctx.createBiquadFilter();
this.eqHigh.type = 'highshelf';
this.eqHigh.frequency.value = 8000;
this.eqHigh.gain.value = 0;
// ── 动态压缩器 ───────────────────────────────────────
this.compressor = ctx.createDynamicsCompressor();
this.compressor.threshold.value = -18;
this.compressor.knee.value = 6;
this.compressor.ratio.value = 4;
this.compressor.attack.value = 0.005;
this.compressor.release.value = 0.1;
// ── 卷积混响(干湿混合)─────────────────────────────
this.convolver = ctx.createConvolver();
this.dryGain = ctx.createGain();
this.wetGain = ctx.createGain();
this.dryGain.gain.value = 1.0;
this.wetGain.gain.value = 0.0; // 默认关闭混响
// ── 分析器(用于可视化)─────────────────────────────
this.analyser = ctx.createAnalyser();
this.analyser.fftSize = 2048;
this.analyser.smoothingTimeConstant = 0.8;
// ── 输出增益(主音量)───────────────────────────────
this.masterGain = ctx.createGain();
this.masterGain.gain.value = 1.0;
// ── 连接音频图 ───────────────────────────────────────
// 输入 → EQ 链
this.inputGain
.connect(this.eqLow)
.connect(this.eqMid)
.connect(this.eqHigh)
.connect(this.compressor);
// 压缩器 → 干湿分路
this.compressor.connect(this.dryGain);
this.compressor.connect(this.convolver);
this.convolver.connect(this.wetGain);
// 干湿合并 → 分析器 → 主音量 → 输出
this.dryGain.connect(this.analyser);
this.wetGain.connect(this.analyser);
this.analyser.connect(this.masterGain);
this.masterGain.connect(ctx.destination);
}
// ── 公开接口 ─────────────────────────────────────────
get input() { return this.inputGain; }
// 加载混响 IR 文件
async loadReverb(irUrl) {
const response = await fetch(irUrl);
const arrayBuffer = await response.arrayBuffer();
this.convolver.buffer = await this.ctx.decodeAudioData(arrayBuffer);
}
// 设置混响湿信号比例(0~1)
setReverbWet(value) {
const t = this.ctx.currentTime;
this.wetGain.gain.setTargetAtTime(value, t, 0.05);
this.dryGain.gain.setTargetAtTime(1 - value, t, 0.05);
}
// 设置 EQ(频段:'low'|'mid'|'high',增益:dB)
setEQ(band, gainDb) {
const node = { low: this.eqLow, mid: this.eqMid, high: this.eqHigh }[band];
if (node) node.gain.setTargetAtTime(gainDb, this.ctx.currentTime, 0.01);
}
// 设置主音量(带平滑)
setMasterVolume(value) {
this.masterGain.gain.setTargetAtTime(
Math.max(0, Math.min(2, value)),
this.ctx.currentTime,
0.02
);
}
// 淡入
fadeIn(duration = 1) {
const g = this.masterGain.gain;
g.setValueAtTime(0, this.ctx.currentTime);
g.linearRampToValueAtTime(1, this.ctx.currentTime + duration);
}
// 淡出
fadeOut(duration = 1) {
const g = this.masterGain.gain;
g.setValueAtTime(g.value, this.ctx.currentTime);
g.linearRampToValueAtTime(0, this.ctx.currentTime + duration);
}
// 获取频谱数据(用于可视化)
getFrequencyData() {
const data = new Uint8Array(this.analyser.frequencyBinCount);
this.analyser.getByteFrequencyData(data);
return data;
}
// 连接 audio 元素
connectAudioElement(audioEl) {
const source = this.ctx.createMediaElementSource(audioEl);
source.connect(this.input);
return source;
}
// 销毁
async destroy() {
await this.ctx.close();
}
}使用方式:
const audio = document.getElementById('myAudio');
const chain = new AudioProcessingChain();
// 连接音频元素
chain.connectAudioElement(audio);
// 加载混响 IR
await chain.loadReverb('/ir/concert-hall.wav');
// 在用户交互后恢复 AudioContext
document.getElementById('playBtn').addEventListener('click', async () => {
await chain.ctx.resume();
audio.play();
// 调整音效
chain.setEQ('low', 4); // 低频提升 4dB
chain.setEQ('high', 2); // 高频提升 2dB
chain.setReverbWet(0.3); // 30% 混响
chain.setMasterVolume(0.9);
chain.fadeIn(0.5); // 0.5 秒淡入
});六、AudioWorklet:在独立线程中处理音频
当内置节点无法满足需求时,AudioWorklet 允许你用 JavaScript 编写自定义的音频处理逻辑,运行在专用的音频线程中,不会被主线程的 UI 操作阻塞。
定义 Worklet Processor
// audio-processor.js(在独立线程中运行)
class GainProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [{
name: 'gain',
defaultValue: 1,
minValue: 0,
maxValue: 2,
automationRate: 'a-rate', // 'a-rate':每个采样点更新;'k-rate':每块更新
}];
}
process(inputs, outputs, parameters) {
const input = inputs[0];
const output = outputs[0];
const gainValues = parameters['gain'];
for (let channel = 0; channel < input.length; channel++) {
const inputChannel = input[channel];
const outputChannel = output[channel];
for (let i = 0; i < inputChannel.length; i++) {
// gainValues 可能是长度为 1(k-rate)或 128(a-rate)的数组
const gain = gainValues.length > 1 ? gainValues[i] : gainValues[0];
outputChannel[i] = inputChannel[i] * gain;
}
}
return true; // 返回 true 保持处理器存活
}
}
registerProcessor('gain-processor', GainProcessor);在主线程中使用 Worklet
// 主线程
await audioCtx.audioWorklet.addModule('audio-processor.js');
const workletNode = new AudioWorkletNode(audioCtx, 'gain-processor');
// 控制 Worklet 的参数(支持 AudioParam 自动化!)
workletNode.parameters.get('gain').setValueAtTime(0.5, audioCtx.currentTime);
workletNode.parameters.get('gain').linearRampToValueAtTime(1, audioCtx.currentTime + 2);
// 接入音频图
source.connect(workletNode);
workletNode.connect(audioCtx.destination);
// 主线程 → Worklet
workletNode.port.postMessage({ type: 'config', threshold: 0.8 });
// Worklet → 主线程
workletNode.port.onmessage = (event) => {
console.log('来自 Worklet 的消息:', event.data);
// 例如:{ type: 'peak', value: 0.95 }
};在 Worklet 处理器中接收消息:
// audio-processor.js
class MeterProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.peakValue = 0;
// 监听主线程消息
this.port.onmessage = (event) => {
if (event.data.type === 'reset') {
this.peakValue = 0;
}
};
}
process(inputs, outputs, parameters) {
const input = inputs[0][0]; // 第一个输入的第一个声道
if (!input) return true;
// 透传输出
outputs[0][0]?.set(input);
// 检测峰值
for (let i = 0; i < input.length; i++) {
const abs = Math.abs(input[i]);
if (abs > this.peakValue) {
this.peakValue = abs;
}
}
// 每 128 个采样点(一个处理块)向主线程报告一次峰值
this.port.postMessage({ type: 'peak', value: this.peakValue });
this.peakValue = 0; // 重置
return true;
}
}
registerProcessor('meter-processor', MeterProcessor);七、精确时间调度:Web Audio 的"超能力"
Web Audio API 最被低估的特性之一,是它的精确时间调度系统。AudioContext.currentTime 是一个高精度的硬件时钟,精度可以达到采样级别(约 0.02ms @ 44100Hz),远超 setTimeout / setInterval 的精度(通常误差 10~50ms)。
这使得 Web Audio 可以实现节拍器级别的精确调度,这是普通 JS 定时器完全无法做到的。
"提前调度"模式(Look-ahead Scheduling)
直接用 setTimeout 调度音频会有明显的抖动,正确的做法是用 setTimeout 粗粒度地"提前唤醒",然后用 AudioContext.currentTime 精确地调度音频事件:
class Metronome {
constructor(audioCtx) {
this.ctx = audioCtx;
this.tempo = 120; // BPM
this.lookahead = 25.0; // setTimeout 调度间隔(ms)
this.scheduleAheadTime = 0.1; // 提前调度时间窗口(秒)
this.nextNoteTime = 0;
this.isRunning = false;
this._timerID = null;
this._loadClickSound();
}
async _loadClickSound() {
// 合成一个短促的咔哒声
this.clickBuffer = this.ctx.createBuffer(1, 4410, 44100); // 0.1s
const data = this.clickBuffer.getChannelData(0);
for (let i = 0; i < data.length; i++) {
// 指数衰减的白噪声
data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / data.length, 10);
}
}
_scheduleNote(time) {
const source = this.ctx.createBufferSource();
source.buffer = this.clickBuffer;
const gainNode = this.ctx.createGain();
gainNode.gain.value = 1.0;
source.connect(gainNode);
gainNode.connect(this.ctx.destination);
// 精确调度到 time 时刻播放
source.start(time);
}
_scheduler() {
// 在调度窗口内,预先调度所有即将到来的音符
while (this.nextNoteTime < this.ctx.currentTime + this.scheduleAheadTime) {
this._scheduleNote(this.nextNoteTime);
// 计算下一个节拍的时间
const secondsPerBeat = 60.0 / this.tempo;
this.nextNoteTime += secondsPerBeat;
}
// 粗粒度的 setTimeout 只负责"唤醒"调度器
this._timerID = setTimeout(() => this._scheduler(), this.lookahead);
}
start() {
if (this.isRunning) return;
this.isRunning = true;
this.nextNoteTime = this.ctx.currentTime + 0.05;
this._scheduler();
}
stop() {
this.isRunning = false;
clearTimeout(this._timerID);
}
setTempo(bpm) {
this.tempo = bpm;
}
}
// 使用
const metronome = new Metronome(audioCtx);
document.getElementById('startBtn').addEventListener('click', async () => {
await audioCtx.resume();
metronome.start();
});
document.getElementById('stopBtn').addEventListener('click', () => {
metronome.stop();
});八、decodeAudioData:音频解码详解
decodeAudioData 是将编码音频(MP3、AAC 等)转换为原始 PCM 数据(AudioBuffer)的核心方法,是 AudioBufferSourceNode 的必经之路。
// 现代 Promise 写法
async function loadAudio(url) {
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
try {
const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
console.log('解码完成:');
console.log(' 时长:', audioBuffer.duration, '秒');
console.log(' 声道数:', audioBuffer.numberOfChannels);
console.log(' 采样率:', audioBuffer.sampleRate, 'Hz');
console.log(' 采样点数:', audioBuffer.length);
return audioBuffer;
} catch (err) {
console.error('解码失败:', err);
throw err;
}
}直接操作 AudioBuffer 的 PCM 数据
解码后的 AudioBuffer 可以直接读写每个采样点,这是实现音频波形显示、音频裁剪等功能的基础:
async function analyzeAudio(url) {
const buffer = await loadAudio(url);
// 获取左声道的 PCM 数据(Float32Array,值域 -1.0 ~ 1.0)
const channelData = buffer.getChannelData(0);
// 计算 RMS 音量(均方根)
let sum = 0;
for (let i = 0; i < channelData.length; i++) {
sum += channelData[i] * channelData[i];
}
const rms = Math.sqrt(sum / channelData.length);
const dB = 20 * Math.log10(rms); // 转换为 dBFS
console.log(`RMS 音量: dBFS`);
// 找出峰值
let peak = 0;
for (let i = 0; i < channelData.length; i++) {
if (Math.abs(channelData[i]) > peak) peak = Math.abs(channelData[i]);
}
console.log(`峰值: dBFS`);
return { rms, peak, buffer };
}修改 AudioBuffer 数据(音频处理)
// 反转音频(倒放效果)
function reverseAudio(buffer) {
const reversed = audioCtx.createBuffer(
buffer.numberOfChannels,
buffer.length,
buffer.sampleRate
);
for (let ch = 0; ch < buffer.numberOfChannels; ch++) {
const original = buffer.getChannelData(ch);
const target = reversed.getChannelData(ch);
for (let i = 0; i < original.length; i++) {
target[i] = original[original.length - 1 - i];
}
}
return reversed;
}
// 混合两个 AudioBuffer
function mixBuffers(bufferA, bufferB, gainA = 0.5, gainB = 0.5) {
const length = Math.max(bufferA.length, bufferB.length);
const mixed = audioCtx.createBuffer(1, length, audioCtx.sampleRate);
const output = mixed.getChannelData(0);
const dataA = bufferA.getChannelData(0);
const dataB = bufferB.getChannelData(0);
for (let i = 0; i < length; i++) {
const a = i < dataA.length ? dataA[i] * gainA : 0;
const b = i < dataB.length ? dataB[i] * gainB : 0;
output[i] = Math.max(-1, Math.min(1, a + b)); // 防止削波
}
return mixed;
}九、常见音频图模式速查
掌握几种常见的音频图拓扑,能解决 80% 的实际需求。
模式一:串联处理链
// 最基础的线性处理链
source
.connect(eqNode)
.connect(compressor)
.connect(gainNode)
.connect(audioCtx.destination);模式二:并联(干湿混合)
// 原始信号与效果信号并联混合
const dryGain = audioCtx.createGain();
const wetGain = audioCtx.createGain();
const merger = audioCtx.createGain(); // 用作混合点
source.connect(dryGain); // 干路
source.connect(effectNode); // 湿路
effectNode.connect(wetGain);
dryGain.connect(merger);
wetGain.connect(merger);
merger.connect(audioCtx.destination);模式三:多源混音
// 多个音源混合输出
const masterBus = audioCtx.createGain();
masterBus.connect(audioCtx.destination);
// 各音轨连接到主总线
musicSource.connect(musicGain).connect(masterBus);
sfxSource.connect(sfxGain).connect(masterBus);
voiceSource.connect(voiceGain).connect(masterBus);模式四:侧链压缩(Sidechain)
// 用鼓声的响度控制音乐的压缩量(Ducking 效果)
const compressor = audioCtx.createDynamicsCompressor();
// 主信号(音乐)经过压缩器
musicSource.connect(compressor);
compressor.connect(audioCtx.destination);
// 侧链信号(鼓声)连接到压缩器的 reduction 参数
// 注意:Web Audio API 原生不支持真正的 sidechain,
// 需要用 AudioWorklet 实现完整的侧链压缩
drumSource.connect(compressor); // 简化版:鼓声也流经压缩器模式五:LFO 参数调制
// 用低频振荡器调制滤波器频率(哇音效果)
const lfo = audioCtx.createOscillator();
const lfoGain = audioCtx.createGain();
const filter = audioCtx.createBiquadFilter();
lfo.frequency.value = 2; // 2Hz 调制频率
lfoGain.gain.value = 500; // 调制深度:±500Hz
filter.frequency.value = 1000; // 中心频率 1kHz
lfo.connect(lfoGain);
lfoGain.connect(filter.frequency); // LFO → 滤波器频率参数
source.connect(filter);
filter.connect(audioCtx.destination);
lfo.start();十、本章知识图谱
小结
Web Audio API 是浏览器音频能力的天花板。从 AudioContext 的运行时环境,到各类节点的组合搭配,再到 AudioParam 的精确自动化调度,每一层都经过精心设计,既有足够的灵活性应对复杂场景,又有足够的高层封装降低使用门槛。
AudioWorklet 的出现更是把 Web 音频推向了专业领域——在浏览器里实现一个功能完整的合成器、混音台或音频效果器,已经不再是天方夜谭。
下一章我们将深入音频可视化——AnalyserNode 只是起点,如何用 Canvas 和 WebGL 把音频数据转化成震撼的视觉效果,频谱图、波形图、粒子系统……让声音"看得见",是这个系列里最有趣的一章。