Web Audio API 使用入门

Boris Smus
Boris Smus

在 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 节奏每四分音符交替演奏。

假设我们已加载 kicksnarehihat 缓冲区,用于执行此操作的代码很简单:

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 方法,用于逐渐更改参数的值,例如 linearRampToValueAtTimeexponentialRampToValueAtTime

虽然您可以从内置的线性函数和指数函数中选择转换时间函数(如上所述),但您也可以通过使用 setValueCurveAtTime 函数的值数组来指定您自己的值曲线。

对声音应用简单的过滤效果

包含 BiquadFilterNode 的音频图表
包含 BiquadFilterNode 的音频图

借助 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 等于 440hz,A5 为 880hz)。如需了解详情,请参阅上方源代码链接中的 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 的协作音乐创作游戏。