大话Web-Audio-Api

文章是我在学习Web Audio 时翻译的 原文在此

Logic proX

在html5 <audio> 元素出现之前,一个网页想要发声,就得需要flash或者其他插件的辅助。而<audio>标签的出现,使网页在游戏(的音效)和网页交互(音频)方面有了极大的飞跃。

Web-Audio-API 是一个用与WebApp 统筹以及合成声音的高级Api。这套Api的目标是在音频处理方面能够达到当今很多游戏音频引擎,混音器,处理器,滤波器的水平。 以下就是相关介绍。

首先从 AudioContext 说起

AudioContext 是用来控制管理所有声音用的(网页上的声音吧)。
在实际用途中,我们可以用AudioContext的实例,创建一个音频源,或者很多音频源并且将他们连接到音频出口(一般是扬声器)。这种连接并不一定是直接连接了,这里可以在中间连接上一系列的AudioNodes(你可以当做 中间处理器吧 就跟美图里面的滤镜一样 最后还是要导出到相册),这些AudioNodes的作用就是对声音信号做相应的处理。

CAUTION! 后面简称Web-Audio-API => WAA

一个简单的AudioContext实例可以支持很多声音,还有非常复杂的声音图像,所以我们只需要简单的创建一个实例就可以展现WebAudioAPI 强大的本领。许多有趣的WAA函数比如创建一个AudioNodes 以及解码一个音频文件 都是属于AudioContext的。

如下 创建一个AudioContext的代码片段

var context;
window.addEventLisener('load',init,false)

function init(){
    try {
        window.AudioContext = window.AudioContext||window.webkitAudioContext;
        context = new AudioContext();
    }
    catch(e){

        alert('MLGB 你的浏览器连个Web-Audio-API 都不支持!!')
    }


}

可以看到在这里webkit 又要加一个前缀,不知道是因为他是先行者,还是要特立独行一把。

加载声音

Web-Audio-API 使用AudioBuffer 来加载短 或者中等长度的音频。最基本的加载方式就是使用XMLHttpRequest来获取声音文件。

Web-Audio-API 也支持很多种格式的音频,比如WAV, MP3, AAC, OGG 还有其他很多啦,具体看这里支持的音频格式

以下代码是展示如何用XMLHttpRequest来加载音频

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

function loadDogSound(url){
    var request = new XMLHttpRequest();
     request.oprn('GET',url,true);
     request.responseType= 'arraybuffer';
     

//下面就是对音频文件的异步解析
request.onload = function(){
    context.decodeAudioData(request.response,function(buffer){
    dogBarkingBuffer = buffer;
},onError);
}

request.send();

}

这里的音频文件是二进制文件(不是文本),所以我们要设置responseType为 ArrayBuffer。想了解更多关于ArrayBuffer的东西你可以点这里XHR2


一旦音频文件被加载之后,他就可以缓存下来继续用于后面的解码,或者可以使用AudioContext的decodeAudioData()将其马上解码。这个方法会拿到放在request.response里面的Arraybuffer文件,并且异步的执行解码操作。(这个过程不会干扰js代码的主进程)。
当decodeAudioData()执行完毕之后,它就会继续执行一个将被解码出来的音频PCM文件作为参数的回调函数。


播放音频

WAA简易视图

当加载完AudioBuffer文件之后,我们就可以准备去播放音频文件了。假设我们已经加载一个 dog barking文件并且已经被解码完毕了。那么我们就可以通过以下的代码来实现音频的播放了。

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

function playSound(buffer){
     var source = context.createBufferSource();
     source = context.createBuffersource();//创建一个音频源 相当于是装音频的容器
    source.buffer = buffer;//  告诉音频源 播放哪一段音频
    source.connect(context.destination);// 连接到输出源
    source.start(0);//开始播放
}

当然,playSound()可以任意事件调用,比如设置一个按键,单击之后执行。


start(time)函数则可以精确的控制音频的播放以及回放,在一些游戏以及对音频播放时间非常严格的应用里面必定有非常大的用处。(不过在一些老旧的系统里 你需要使用的函数是noteOn(time)不是start(time))

深入使用 Web Audio Api

这里还有其他加载音频的方式,比如 BufferLoader class

以下就是使用BufferClass的例子。首先创建两个AudioBuffers;在他们加载完之后,开始播放他们。

window.onload = init;
var context;
var bufferLoader;

function init() {
    window.AudioContext = window.AudioContext||window.webkitAudioContext;
    context = new AudioContext();
    
bufferLoader = new BufferLoader(
    context,
[
   '../sounds/vdsbds/askhd/ashd.wav',
   '../hasjd/sadhasj/asdhasj.mp3'    // 仅做个示例 不要在意是乱打的
],
finishedLoading
);
bufferLoader.load();
}

function finishedLoading(bufferlist){
//创建两个音频源,并且同时播放
var source1 = context.createBufferSource();
var source2 = context.createBufferSource();

source1.buffer = bufferList[0]
source2.buffer = bufferList[1];

source1.connect(context.destination);
source2.connect(context.destination);


}

跟随时间舞蹈:按照节奏播放声音

开发者们也能够使用WAA产生有节奏的的声音。为了展示这项特性,我们可以创建一个简单的节奏音乐。以下的谱子可能是最常见的鼓点了。

一个简单的鼓点

在这个谱子里面一个hihat 每0.5拍响一次,一个kick和snare在每4拍响一次。假设我们已经加载了kick,snare,以及hihat的buffer,如下就是这首谱子怎么演奏的代码。

for(var bar = 0;bar <2;bar ++){
    var time = startTime + bar*8*eighthNoteTime;
// 在拍子在1,5的时候   kick响
playSound(kick,time)
playSound(kick,time+4*eigthNoteTime);
// 在 鼓点 3,7的时候 snare响
playSound(snare,time+2*eighthNoteTime);
playSound(snare,time+6*eighthNoteTime);

//  每一个8拍  hihat响
for(var i =0;i<8;++i){
playSound(hihat,time+i*eightthNoteTime);
}

}

在这个乐谱里面我们只做了一个简单的重复而不是无止境的循环。playsound是一个用来指定特定时间播放特定声音的函数,代码如下:

function playSound(buffer,time){
    var source = context.createBufferSource();
source.buffer = buffer;
source.connect(context.destination);
source.start(time);
}

改变一个声音的音量

对声音的操作之中,最普遍的应该就是改变一个声音的音量了。在WAA里面我们可以将音频元与一个GainNode相连,通过操作GainNode来达到这个目的。

GainNode

如下的代码可以完成以上的要求:

// 创建Gainnode
var gainNode = context.createGain();
//将音频源与GainNode连接
source.connect(gainNode);
//将GainNode与播放源连接
gainNode.connect(context.destination);

这些东西串联好了之后,你就可以操作GainNode来改变音量了

// 降音量
gainNode.gain.value = 0.5;

此处有一个完整的示例
点这里源代码

渐入渐出

现在,假比我们有一个略微或者比较复杂的剧本,里面会播放非常多的音频文件而且音频之间会有交叉的过渡。在一些类DJ的软件里面这种场景也时常出现,常常是两个唱片机,一个唱片机的音乐过渡到另一个唱片机的音乐。
下图将展示所描述的场景:

渐入渐出

为了建立这个场景,我们需要创建两个GainNodes,然后将他们连接到对应的source上面,用以下的代码就可以达到这个目的:

function createSource(buffer){
   var source = context.createBufferSource();
   // 创建一个gainNode
var gainNode = context.createGain();
source.buffer = buffer ;
// 开启循环模式
source.loop = true;
//将音频文件与gain连接
source.connect(gainNode);
// 将gainNode与输出源连接
gainNode.connect(context.destination);

return {
    source:source,
    gainNode:gainNode
};
}

线性过渡

通过改变音量的的大小可以做到一个简单的线性交叉过渡。

线性交叉过渡

为了解决这个问题,我们使用等功率曲线,其中相应的增益曲线是非线性的,并且以更高的幅度相交。这最小化音频区域之间的音量下降,导致在可能在级别上稍微不同的区域之间的更均匀的交叉淡入淡出。

等功率曲线

由于简书里面不能直接贴html标签
代码就放在这里吧 等功率曲线渐变

音乐切换之间的过渡

还有一个比较常见的过渡就是在音乐应用里面,音乐之间的切换一般也是渐入渐出的,这样做是为了避免音乐切换造成的突兀感。为此,我们可以使用定时器来定时调节音量来做到渐入渐出,但是这么做是不够精确的。在Web Audio Api里面,我们可以使用AudioParam 接口来深度的调节GainNode里的音量。

好了,设置一个播放列表,我们可以通过提前设置当前播放的音频减小音量,下一个要播放的音频增加音量来完成一个过渡,并且要让下一个音频略微的在前一个音频播放完之前开始播放。

function playHelper(bufferNow,bufferLater){
    var playNow = createSource(bufferNow);
    var source = playNow.Source;
    var gainNode = playNow.gainNode;
    var duration = bufferNow.duration;
    var currTime = context.currentTime;
// 开始播放 playNow
 source.start(0);
// 在音频的最后 将其 渐出
gainNode.gain.linearRampToValueAtTime(1,currTime+duration-ctx.FADE_TIME);
gainNode.gain.linearRampToValueAtTime(0,currTime+duration);
// 在音频完成之后做一个反转
var recurse = arguments.callee;
ctx.timer = setTimeout(function(){
    recurse(bufferLater,bufferNow);
    },(duration-ctx.FADE_TIME)*1000);
}

WebAudioApi 提供了很多 函数曲线式的逐步改变值得方法,比如上面的linearRampToValueAtTime 还有其他的比如exponentialRampToValueAtTime(指数式的改变)。

除了上面两种函数式之外,我们还可以自定义函数式来对值做出一些改变,使用setValueCurveAtTime函数就可以达到这个目的。

一下就是一个示例:
code


给音频添加 一个简单的滤波器

滤波器

WebAudioApi 让你的音频就像水管里面的水一样从一个AudioNode 导向另外一个,从而构成一个比较复杂,且完整的音频处理过程。
这里要说的,是对音频做一些效果上面的处理,我们用到的是BiquadFilterNode,将其放置在你的音频源和输出源之间,添加一些设置就可以做出很多效果出来。这种音频节点可以做出各种低阶模拟器的效果,其可以用于构件图形均衡器甚至更加复杂的效果,这与你选择强调哪些频谱抑制哪些频谱有关。

以下是支持的滤波器

  • 低通滤波器
  • 高通滤波器
  • 带通滤波器
  • 低架滤波器
  • 高架滤波器
  • 峰值滤波器
  • 陷波滤波器
  • 全通滤波器
桌面级音频应用示例

所有这些滤波器都含有特定的参数去限定增益。低通滤波器保持较低的频率范围,但是丢弃了高频。断裂点由频率值,以及Q因子是无单位的来确定,这也决定了图像的形状。增益仅影响某些滤波器,如低架滤波器和峰值滤波器,而不是此低通滤波器。

以下是一个简单的滤波器示例:

var filter = context.createBiquadFilter();
// 创建音频图像
source.connect(filter);
filter.connect(context.destinaton);
//  设置filter的参数
filter.type = 'lowpass';// 低通 滤波器 详情可以见 BiquadFilterNode的文档
filter.frequency.value = 440;// 设置截止位置为 440HZ
// 回放音频
source.start(0);

完整的示例代码
CODE
一般来说,频率控制需要调整到工作时的对数刻度,因为人类听觉本身工作在相同的原理(即,A4是440hz,和A5是880hz)。
最后,请注意,示例代码允许您连接和断开连接过滤器,动态更改AudioContext图形。我们可以通过调用node.disconnect(outputNumber)从图中断开AudioNodes。例如,要将图表从通过过滤器重新路由到直接连接,我们可以执行以下操作:

// 断开 音频源于 滤波器之间的连接
source.disconnect(0);
filter.disconnect(0);
// 音频源 直接与输出源连接
source.connect(context.destination);

后记

以上,所讲的东西已经覆盖了大部分WAA 的基本Api,包括了加载,播放音频文件,使用增益节点和滤波器绘制出了音频图像,设置音频的节奏,改变音频的参数,来达到一些常见的音效。 学会了这些,我们也可以做一些比较cool的音频应用了。

(可惜。。。Web Audio Api 普及太少,不像canvas一样为广大前端开发工程师所知,所以作品也比较少,这里我也没有什么音频WebApp 可以推荐的)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容