认识 Web Audio API

HTML5 在做游戏方面越来越完善,虽然在性能上还是不尽如人意但做一些简单的游戏仍然是能满足大部分需求的。画面和音效作为游戏的重要元素两者缺一不可,HTML5的canvas作为画面的渲染等都已经很成熟了,在音效方面也有Web Audio API 作为支持。

这一篇文章主要简单介绍一下Web Audio API的应用。

初识HTML5音频

<audio>应该是最为简单而且为人熟知的标签,一般用于在文档中表示音频内容,可以很简单的指定一个音频源。通常使用场景都是作为背景音乐、歌曲等播放。

虽然都是能达到播放音频的效果,但在作为游戏音效中有一个致命的问题就是频繁在停止和播放过程中切换会导致卡顿甚至播放没有声音等现象。对于一些打击音效来说使用<audio>会导致画面和音效不同步时有发生。

AudioContext 是作为音频播放的基础。不同于 <Audio>,其功能完全由 JavaScript 控制,在事件上的控制相比<Audio>更完善因为它实现了EventTarget的接口,在音频源上可以利用AudioBuffer进行多音频编辑或者捕捉处理等,甚至能对音频进行混响、双二阶滤波器、平移、压缩处理。

使用 Web Audio API

Web Audio API最大的优点在于通过音频节点的输入输出相连,通常开始播放都是从一个或多个源。事实上,声音文件只是自身声音声频的记录,这些节点的输出可以链接到其他节点的输入,将这些声音流经过混合或修改后形成其他的流,常见的修改就是将样本乘以一个值,使其声音更加响亮或安静。

开始之前,先了解一下工作模式

  1. 创建音频上下文
  2. 在上下文中,创建音频源 —— 例如: <audio>OscillatorNode
  3. 创建效果节点 —— 例如:混响滤波器平移压缩
  4. 为音频选择一个终端,例如你的系统扬声器。
  5. 连接源到效果器,以及效果器到终端。

web-audio-api-flowchart

创建 AudioContext

AudioContext 是用于管理和播放所有声音。为了使Web Audio API有声音,创建一个或者多个音源并把他们连接到由 AudioContext 提供的终端,而且该连接不需要直接连接的,可以经过任何数量的 AudioNodes 充当音频信号的处理器。

一个 AudioContext 的实例可以支持多个音频输入和复杂的音频图形,所以很多时候我们只需创建一个实例即可。而且 AudioContext 的方法使 Web Audio API 变得十分有趣。

1
2
3
4
5
6
7
// 初始化创建音频上下文
// 由于部分浏览器需要webkit前缀,所以这里做了个兼容处理。
try{
var context = new (window.AudioContext || window.webkitAudioContext)();
}catch(e){
alert('Web Audio API is not supported in this browser');
}

加载音频

Web Audio API 使用 AudioBuffer 作为短、中长度的音频源。而且都是通过 XMLHttpRequest 进行异步加载这些音频文件的。

Web Audio API 支持多种格式的加载,例如:WAV、MP3、ACC、OGG等。可以通过 MDN 提供的表格查看详情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var dogBarkingBuffer = null;

var context = new (window.AudioContext || window.webkitAudioContext)();

function loadDogSound(url) {
// 异步获取音频文件
var request = new XMLHttpRequest();
request.open('GET', url, true);
request.responseType = 'arraybuffer';

// 加载成功后进行解码操作
request.onload = function() {
context.decodeAudioData(request.response, function(buffer) {
// 获得 AudioBuffer
dogBarkingBuffer = buffer;
}, onError);
}

// 发起请求
request.send();
}

由于音频文件的数据是二进制(非文本),所以要设置请求头的responseTypearraybuffer

当异步加载完文件后需要利用 AudioContext.decodeAudioData()进行解码,如果是原始数据(raw data)则使用 AudioContext.createBuffer(),一旦放入 AudioBuffer 就可以通过传递放入到 AudioBufferSourceNode 进行播放。

decodeAudioData完成后,它会执行回调并且传递一个参数是一个已经解码了的AudioBuffer音频数据。

播放音频

一旦一个或者多个 AudioBuffer 被加载完成,然后我们就可以准备播放这些音频了。接下来我们就能通过以下的代码来播放刚才加载的音频。

1
2
3
4
5
6
7
8
9
function playSound(buffer) {
var source = context.createBufferSource(); // 利用上下文创建一个源缓冲器
source.buffer = buffer; // 设置源的数据
source.connect(context.destination); // 连接源到终端(默认是系统扬声器/耳机)
source.start(0); // 立即播放

// 需要注意的是,在一些旧的系统中是调用 noteOn(time)
// 详情查看:https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Porting_webkitAudioContext_code_to_standards_based_AudioContext
}

在任何时间当用户按下键盘或者鼠标, playSound(buffer) 方法都是可以调用的。

start(time) 可以很方便地为游戏或者其他关键时间进行精确的声音播放。但首先要确保的是 AudioBuffer 已经预加载。(由于部分旧系统采用的是旧的API,所以播放需要使用noteOn(time)更多的API变更信息

值得注意的是,在iOS上,用户进行第一次交互之前所有声音都是被设置为静音的。所以需要用户在屏幕上触摸一下才能正常工作。

播放时间处理

Web Audio API 允许开发人员可以精确地处理播放时间。下面我们设置一个简单的节奏音乐。

web_audio_api_drum

上面是一份4/4拍的谱子,其中每八分音符就播放一次踩钹,贝司和小鼓轮流播放。

假如我们已经缓冲了贝司、小鼓、踩钹的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for (var bar = 0; bar < 2; bar++) {
var time = startTime + bar * 8 * eighthNoteTime;
// 在1、5拍时播放贝司
playSound(kick, time);
playSound(kick, time + 4 * eighthNoteTime);

// 在3、7拍shi时播放小鼓
playSound(snare, time + 2 * eighthNoteTime);
playSound(snare, time + 6 * eighthNoteTime);

// 每八分拍播放一次踩钹
for (var i = 0; i < 8; ++i) {
playSound(hihat, time + i * eighthNoteTime);
}
}

在上面的例子里,我们在playSound方法中是指定了时间进行播放的。

改变音量

在最基本的操作之一就是控制音量的大小,在Web Audio API中,我们可以利用 GainNode作为处理器,然后再输出到终端。

web-audio-api-GainNode.png

1
2
3
4
5
6
// 创建一个增益处理器
var gainNode = context.createGain();
// 将源连接到增益处理器
source.connect(gainNode);
// 将增益处理连接到终端
gainNode.connect(context.destination);

然后可以通过修改 gainNode.gain.value 的值来处理声音的大小。

1
gainNode.gain.value = 0.5; // 0 静音 1 最大

音乐淡出淡入

音乐的淡出淡入现在基本上是一个播放器基本的功能而且在游戏中场景的切换也会伴随着背景音乐的改变,在 Web Audio API 的帮助下我们也能很轻易的实现歌曲的淡入淡出功能。

我们还是使用GainNode去做到这一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
CrossfadePlaylistSample.play = function() {
/**
* 创建源和增益器
**/
function createSource(buffer) {
var source = context.createBufferSource();
var gainNode = context.createGain ? context.createGain() : context.createGainNode();
source.buffer = buffer;
source.connect(gainNode);
gainNode.connect(context.destination);
return {
source: source,
gainNode: gainNode
};
}
var self = this;
var bufferNow = BUFFERS[this.playIndex];
var playNow = createSource(bufferNow);
var source = this.source = playNow.source;
var gainNode = this.gainNode = playNow.gainNode;
var duration = bufferNow.duration; //持续时间
var currTime = context.currentTime;

//开始的时候渐强
gainNode.gain.linearRampToValueAtTime(0, currTime);
gainNode.gain.linearRampToValueAtTime(1, currTime + this.FADE_TIME);
//结束的时候减弱
gainNode.gain.linearRampToValueAtTime(1, currTime + duration - this.FADE_TIME);
gainNode.gain.linearRampToValueAtTime(0, currTime + duration);

//开始播放
source.start ? source.start(0) : source.noteOn(0);

//当歌曲播放完自动切换到下一首。
this.timer = setTimeout(function() {
// console.log(duration - self.FADE_TIME)
self.playIndex === 0 ? self.playIndex = 1 : self.playIndex = 0;
self.play();
}, ((currTime + duration - this.FADE_TIME) - currTime) * 1000);
};

更多的处理器

Web Audio API 提供了众多的处理器,在本文简单的为大家介绍一下:

AnalyserNode 分析器,可以通过该处理器绘出波形图之类的视图效果。

BiquadFilterNode 滤波器,可以通过该处理器对音乐进行降噪、去人声等处理。

ChannelMergerNodeChannelSplitterNode 信道合并\分离处理器,对多个信道进行合并或分离,一般用于对某个信道的音源进行增益处理。

ConvolverNode 混响,主播们都需要。

DynamicsCompressorNode 削峰处理,一般对音源最响亮的部分进行压缩处理。多用于音乐或游戏多个音频同时播放,以帮助防止在多个声音一起播放和多路复用时可能发生的削波和失真。

DelayNode 延迟输出。

OscillatorNode 一个恒定的音调,在游戏中一般用来做一些持续性的响声如:愤怒小鸟中里面小鸟飞出去时候的“啊”!

PannerNode 空间处理器,可以使音频在多声道中从左到右,从右到左或者在其中一边播放。CS 的脚本声要用到。

不一一介绍的 MediaElementAudioSourceNodeMediaStreamAudioDestinationNode、[]