Web Audio API 使用入门

Boris Smus
Boris Smus

在 HTML5 <audio> 元素问世之前,需要使用 Flash 或其他插件来打破网络的沉寂。虽然网页中的音频不再需要插件,但音频代码会给实现复杂的游戏和交互式应用带来很大的限制。

Web Audio API 是一种用于在 Web 应用中处理和合成音频的高级别 JavaScript API。此 API 的目标是加入现代游戏音频引擎中的功能,以及现代桌面音频制作应用中的一些混音、处理和过滤任务。下面将简要介绍如何使用以下强大的 API。

AudioContext 使用入门

AudioContext 用于管理和播放所有声音。如需使用 Web Audio API 生成声音,请创建一个或多个声音来源,并将其连接到 AudioContext 实例提供的声音目的地。该连接不需要是直接连接,并且可以经过任意数量的中间 AudioNodes 作为音频信号的处理模块。网络音频规范中更详细地介绍了此路由

单个 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 可让开发者精确安排播放时间。为了演示这一点,让我们设置一个简单的节奏轨道。最广为人知的架子鼓模式大概如下:

简单的摇滚鼓模式
简单的摇滚鼓模式

采用这种模式,每八个音演奏一次踩踏板,踢踏板和小鼓每节以四四拍交替演奏

假设我们已加载 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 音频和 Web 套接字的协作音乐制作游戏。