在 HTML5 <audio>
元素出现之前,需要使用 Flash 或其他插件才能打破网络的沉默。虽然 Web 上的音频不再需要插件,但音频标记在实现复杂的游戏和互动应用方面存在明显限制。
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()
完成后,它会调用一个回调函数,该函数会将解码后的 PCM 音频数据作为 AudioBuffer
提供。
播放提示音
加载一个或多个 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。这种类型的音频节点可以执行各种低阶滤波,这些滤波可用于构建平衡器,甚至更复杂的效果,主要与选择要强调和要抑制的声音频谱部分有关。
支持的过滤条件类型包括:
- 低通滤波器
- 高通滤波器
- 带通滤波器
- 低搁架过滤器
- 高搁架过滤器
- 峰值过滤器
- 陷波滤波器
- “全部通过”过滤条件
所有滤波器都包含用于指定一定量的增益、应用滤波器的频率和质量因子的参数。低通滤波器会保留较低的频率范围,但会舍弃较高的频率。断点由频率值决定,Q 因子无单位,并决定图表的形状。增益仅会影响某些滤波器(例如低通滤波器和峰值滤波器),而不会影响此低通滤波器。
我们来设置一个简单的低通滤波器,以便仅从音频采样中提取基音:
// 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 赫兹,A5 是 880 赫兹)。如需了解详情,请参阅上面的源代码链接中的 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 Sockets 的协作音乐创作游戏。