在 HTML5 <audio> 元素出现之前,需要使用 Flash 或其他插件才能打破网页的沉寂。虽然网页上的音频不再需要插件,但音频标记在实现复杂的游戏和互动式应用方面存在明显的限制。
Web Audio API 是一种高级 JavaScript API,用于在 Web 应用中处理和合成音频。此 API 的目标是纳入现代游戏音频引擎中的功能,以及现代桌面音频制作应用中的一些混音、处理和过滤任务。下面将简要介绍如何使用此强大的 API。
AudioContext 使用入门
AudioContext 用于管理和播放所有声音。如需使用 Web Audio API 生成声音,请创建一个或多个声源,并将它们连接到 AudioContext 实例提供的声音目的地。此连接不必是直接连接,可以经过任意数量的中间 AudioNodes,这些节点充当音频信号的处理模块。Web Audio 规范中更详细地介绍了这种路由。
单个 AudioContext 实例可以支持多个声音输入和复杂的音频图,因此对于我们创建的每个音频应用,只需要一个这样的实例。
以下代码段会创建一个 AudioContext:
var context;
window.addEventListener('load', init, false);
function init() {
try {
context = new AudioContext();
}
catch(e) {
alert('Web Audio API is not supported in this browser');
}
}
对于基于 WebKit 的旧版浏览器,请使用 webkit 前缀,例如 webkitAudioContext。
许多有趣的 Web Audio API 功能(例如创建 AudioNode 和解码音频文件数据)都是 AudioContext 的方法。
加载声音
Web Audio API 使用 AudioBuffer 来处理短到中等长度的声音。基本方法是使用 XMLHttpRequest 来获取声音文件。
该 API 支持加载多种格式的音频文件数据,例如 WAV、MP3、AAC、OGG 和其他格式。浏览器对不同音频格式的支持各不相同。
以下代码段演示了如何加载声音样本:
var dogBarkingBuffer = null;
var context = new AudioContext();
function loadDogSound(url) {
var request = new XMLHttpRequest();
request.open('GET', url, true);
request.responseType = 'arraybuffer';
// Decode asynchronously
request.onload = function() {
context.decodeAudioData(request.response, function(buffer) {
dogBarkingBuffer = buffer;
}, onError);
}
request.send();
}
音频文件数据是二进制数据(而非文本),因此我们将请求的 responseType 设置为 'arraybuffer'。如需详细了解 ArrayBuffers,请参阅这篇关于 XHR2 的文章。
收到(未解码的)音频文件数据后,可以将其保留以供日后解码,也可以使用 AudioContext decodeAudioData() 方法立即进行解码。此方法会获取存储在 request.response 中的音频文件数据的 ArrayBuffer,并以异步方式(不会阻塞主 JavaScript 执行线程)对其进行解码。
当 decodeAudioData() 完成时,它会调用一个回调函数,该函数以 AudioBuffer 的形式提供解码后的 PCM 音频数据。
播放声音
加载一个或多个 AudioBuffers 后,我们就可以播放声音了。假设我们刚刚加载了一个包含狗叫声的 AudioBuffer,并且加载已完成。然后,我们可以使用以下代码播放此缓冲区。
var context = new AudioContext();
function playSound(buffer) {
var source = context.createBufferSource(); // creates a sound source
source.buffer = buffer; // tell the source which sound to play
source.connect(context.destination); // connect the source to the context's destination (the speakers)
source.noteOn(0); // play the source now
}
每次有人按键或用鼠标点击某些内容时,都可以调用此 playSound() 函数。
noteOn(time) 函数可让游戏和其他时间关键型应用轻松安排精确的声音播放。不过,为了使此调度正常运行,请确保预加载声音缓冲区。
抽象化 Web Audio API
当然,最好创建一个更通用的加载系统,而不是硬编码为加载此特定声音。音频应用或游戏会使用许多短到中等长度的声音,有很多方法可以处理这些声音,下面介绍一种使用 BufferLoader(不属于 Web 标准)的方法。
以下示例展示了如何使用 BufferLoader 类。我们来创建两个 AudioBuffers;加载完成后,
同时播放这两个 。
window.onload = init;
var context;
var bufferLoader;
function init() {
context = new AudioContext();
bufferLoader = new BufferLoader(
context,
[
'../sounds/hyper-reality/br-jam-loop.wav',
'../sounds/hyper-reality/laughter.wav',
],
finishedLoading
);
bufferLoader.load();
}
function finishedLoading(bufferList) {
// Create two sources and play them both together.
var source1 = context.createBufferSource();
var source2 = context.createBufferSource();
source1.buffer = bufferList[0];
source2.buffer = bufferList[1];
source1.connect(context.destination);
source2.connect(context.destination);
source1.noteOn(0);
source2.noteOn(0);
}
处理时间:播放有节奏的声音
借助 Web Audio API,开发者可以精确安排播放时间。为了演示这一点,我们来设置一个简单的节奏轨道。最广为人知的架子鼓模式可能如下:
其中,踩镲每八分音符演奏一次,底鼓和军鼓每四分音符交替演奏一次,节拍为 4/4。
假设我们已加载 kick、snare 和 hihat 缓冲区,实现此目的的代码很简单:
for (var bar = 0; bar < 2; bar++) {
var time = startTime + bar * 8 * eighthNoteTime;
// Play the bass (kick) drum on beats 1, 5
playSound(kick, time);
playSound(kick, time + 4 * eighthNoteTime);
// Play the snare drum on beats 3, 7
playSound(snare, time + 2 * eighthNoteTime);
playSound(snare, time + 6 * eighthNoteTime);
// Play the hi-hat every eighth note.
for (var i = 0; i < 8; ++i) {
playSound(hihat, time + i * eighthNoteTime);
}
}
在此处,我们只重复一次,而不是乐谱中看到的无限循环。函数 playSound 是一种在指定时间播放缓冲区的函数,如下所示:
function playSound(buffer, time) {
var source = context.createBufferSource();
source.buffer = buffer;
source.connect(context.destination);
source.noteOn(time);
}
更改声音的音量
您可能需要对声音执行的最基本操作之一是更改其音量。使用 Web Audio API,我们可以通过 AudioGainNode 将来源路由到其目的地,以便控制音量:

可通过以下方式实现此连接设置:
// Create a gain node.
var gainNode = context.createGainNode();
// Connect the source to the gain node.
source.connect(gainNode);
// Connect the gain node to the destination.
gainNode.connect(context.destination);
设置好图表后,您可以通过操纵 gainNode.gain.value 以编程方式更改音量,如下所示:
// Reduce the volume.
gainNode.gain.value = 0.5;
在两种声音之间进行淡入淡出
现在,假设我们有一个稍微复杂一点的场景,即播放多种声音,但希望在这些声音之间进行交叉淡入淡出。这在 DJ 类应用中很常见,因为这类应用通常有两个唱盘,并且需要能够从一个音源平移到另一个音源。
这可以通过以下音频图实现:
若要进行此设置,我们只需创建两个 AudioGainNodes,然后使用如下函数将每个来源通过节点连接起来:
function createSource(buffer) {
var source = context.createBufferSource();
// Create a gain node.
var gainNode = context.createGainNode();
source.buffer = buffer;
// Turn on looping.
source.loop = true;
// Connect source to gain.
source.connect(gainNode);
// Connect gain to destination.
gainNode.connect(context.destination);
return {
source: source,
gainNode: gainNode
};
}
等功率交叉淡入淡出
当您在样本之间进行平移时,简单的线性交叉淡入淡出方法会出现音量下降的情况。
为了解决此问题,我们使用等功率曲线,其中相应的增益曲线是非线性的,并且在高振幅处相交。这样可以最大限度地减少音频区域之间的音量下降,从而在音量可能略有不同的区域之间实现更平滑的淡入淡出效果。
播放列表淡入淡出
另一个常见的交叉推子应用是音乐播放器应用。当歌曲发生变化时,我们希望淡出当前轨道,并淡入新轨道,以避免出现突兀的过渡。为此,请安排在未来进行淡入淡出。虽然我们可以使用 setTimeout 来安排此任务,但这种方式并不精确。借助 Web Audio API,我们可以使用 AudioParam 接口来安排参数(例如 AudioGainNode 的增益值)的未来值。
因此,给定一个播放列表,我们可以通过在当前播放的轨道上安排增益降低,并在下一个轨道上安排增益增加,从而在轨道之间进行过渡,这两个操作都在当前轨道播放结束前不久进行:
function playHelper(bufferNow, bufferLater) {
var playNow = createSource(bufferNow);
var source = playNow.source;
var gainNode = playNow.gainNode;
var duration = bufferNow.duration;
var currTime = context.currentTime;
// Fade the playNow track in.
gainNode.gain.linearRampToValueAtTime(0, currTime);
gainNode.gain.linearRampToValueAtTime(1, currTime + ctx.FADE_TIME);
// Play the playNow track.
source.noteOn(0);
// At the end of the track, fade it out.
gainNode.gain.linearRampToValueAtTime(1, currTime + duration-ctx.FADE_TIME);
gainNode.gain.linearRampToValueAtTime(0, currTime + duration);
// Schedule a recursive track change with the tracks swapped.
var recurse = arguments.callee;
ctx.timer = setTimeout(function() {
recurse(bufferLater, bufferNow);
}, (duration - ctx.FADE_TIME) - 1000);
}
Web Audio API 提供了一组便捷的 RampToValue 方法,用于逐步更改参数(例如 linearRampToValueAtTime 和 exponentialRampToValueAtTime)的值。
虽然您可以从内置的线性和指数过渡时间函数中选择过渡时间函数(如上所示),但也可以使用 setValueCurveAtTime 函数通过值数组指定自己的值曲线。
为声音应用简单的滤镜效果

借助 Web Audio API,您可以将声音从一个音频节点传输到另一个音频节点,从而创建可能复杂的处理器链,以向声波添加复杂的效果。
一种方法是在声音源和声音目的地之间放置 BiquadFilterNode。此类音频节点可以执行各种低阶滤波,可用于构建图形均衡器,甚至更复杂的效果,主要用于选择要强调声音频谱的哪些部分以及要减弱哪些部分。
支持的过滤条件类型包括:
- 低通滤波器
- 高通滤波器
- 带通滤波器
- 低频搁架滤波器
- 高架滤波器
- 峰值滤波器
- 陷波滤波器
- “全部通过”过滤条件
所有滤波器都包含用于指定一定程度的增益、应用滤波器的频率和品质因子的参数。低通滤波器会保留较低的频率范围,但会舍弃高频。截止点由频次值确定,而品质因数没有单位,用于确定图表的形状。增益仅影响某些滤波器(例如低架滤波器和峰值滤波器),而不影响此低通滤波器。
我们来设置一个简单的低通滤波器,以仅从声音样本中提取低音:
// Create the filter
var filter = context.createBiquadFilter();
// Create the audio graph.
source.connect(filter);
filter.connect(context.destination);
// Create and specify parameters for the low-pass filter.
filter.type = 0; // Low-pass filter. See BiquadFilterNode docs
filter.frequency.value = 440; // Set cutoff to 440 HZ
// Playback the sound.
source.noteOn(0);
一般来说,需要调整频次控制变量,使其以对数比例运行,因为人耳本身就是以这种方式工作的(即 A4 为 440 Hz,A5 为 880 Hz)。如需了解详情,请参阅上方源代码链接中的 FilterSample.changeFrequency 函数。
最后,请注意,示例代码可让您连接和断开滤波器,从而动态更改 AudioContext 图。我们可以通过调用 node.disconnect(outputNumber) 将 AudioNode 从图中断开连接。例如,若要将图的路由从通过过滤器更改为直接连接,我们可以执行以下操作:
// Disconnect the source and filter.
source.disconnect(0);
filter.disconnect(0);
// Connect the source directly.
source.connect(context.destination);
进一步聆听
我们已介绍该 API 的基础知识,包括加载和播放音频样本。我们构建了包含增益节点和过滤器的音频图,并安排了声音和音频参数调整,以实现一些常见的音效。至此,您已准备就绪,可以开始构建一些出色的 Web 音频应用了!
如果您想寻找灵感,许多开发者已经使用 Web Audio API 创作了优秀作品。以下是我最喜欢的一些:
- AudioJedit,一款使用 SoundCloud 永久链接的浏览器内声音剪辑工具。
- ToneCraft:一种声音序列器,通过堆叠 3D 块来创建声音。
- Plink:一款使用 Web Audio 和 Web Socket 的协作式音乐创作游戏。