Web Audio API 使用入门

Boris Smus
Boris Smus

在 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 将源路由到其目标,以便控制音量:

一个简单的流程图,描绘了音频从源到 GainNode 再到目的地的流动过程。

此连接设置可通过以下方式实现:

// 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 函数通过值数组指定自己的值曲线。

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

简单流程图,描绘了音频从源流向 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 Audio 应用了!

如果您正在寻找灵感,许多开发者已经使用 Web Audio API 创作了优秀作品。我最喜欢的一些功能包括:

  • AudioJedit,一款使用 SoundCloud 永久链接的浏览器内声音拼接工具。
  • ToneCraft,一款通过堆叠 3D 方块来创作声音的音序器。
  • Plink,一款使用 Web Audio 和 Web Socket 的协作式音乐制作游戏。