利用HTML5 Web Audio API给网页JS交互增加声音

一、庞然的HTML5 Web Audio API

首先务必要弄清这一点,本文这里所说的HTML5 Web Audio API和HTML5 <audio>元素完全不是一个东西,其体量也完全不是一个等级的,HTML5 Web Audio API接口的丰富程度和体量可以和HTML canvas API相提并论,其能实现的功能也非常令人瞠目。

HTML5 Web Audio API可以让我们无中生有创造声音,而且是各种音调的声音,换句话说,我们通过JavaScript就会创建一个完整的音乐出来,类似卡农将有固定的音乐,或是通过一些随机算法创建随机的音乐都都可以由前端开发工程师来完成(例如这个使用Web Audio API创建多个经典游戏背景音乐的案例),如果再配合一点人工智能的东西,我们说不定可以造出一个人工智能音乐生成工具。

当然其功能绝不仅限于这一点,我们还可以:

  • 对简单或复杂的声音进行混合;
  • 精确控制声音的密度和节奏;
  • 内置淡入/淡出,颗粒噪点,音调控制等效果;
  • 灵活的处理在音频流的声道,使它们成为拆分和合并;
  • 处理从<audio>音频或<video>视频的媒体元素的音频源;
  • 使用MediaStream的getUserMedia()方法事实处理现场输入的音频,例如变声;
  • 立体音效,可以支持多种3D游戏和沉浸式环境;
  • 利用卷积引擎,创建各类线性音效,例如小/大房间、大教堂、音乐厅、洞穴、隧道、走廊、森林、圆形剧场等。尤其适用于创建高质量的房间效果。
  • 高效的实时的时域和频分析,配合CSS3或Canvas或webGL可以实现音乐可视化支持;
  • 以及其他多种音频波形算法控制,几乎就是把一个音频编辑类软件搬到web上。

但是对大多数的Web开发人员,我们并不需要和音频打交道这么深,因为毕竟不是做音乐网站的,此时,那些很高级的HTML5 Web Audio API功能就离我们实际工作的价值有些远,如果贸然花大量时间去深入学习其实是一件投入产出比很低的事情。当然,我这种担心实际上是多余的,“贸然花大量时间去深入学习?”搞反了吧,其实真实的现状是开发人员对此鸟都不鸟,看都不看。一来因为毕竟不火嘛,二来资料少看不懂嘛,三来自己项目很少有声音打交道,学了也白学。

考虑到这种现状,如果本文内容洋洋洒洒讲各种api,估计很多人瞟两眼就直观把页面关掉了,所以接下来我们从一个最简单最小的而且非常实用的案例来慢慢解开HTML5 Web Audio API的面纱。

二、HTML5 Web Audio API给网页交互增加声音

先来看实例效果,您可以狠狠地点击这里:鼠标hover按钮无中生有播放声音demo

当我们鼠标hover下图所示的按钮的时候,在非IE浏览器下,就会播放出类似电子琴琴键按下的声音,而且每次hover的时候,音调都会发生有规律的变化:

经过按钮播放声音截图

下面这句话是重点,demo页面中播放的声音是硬件自动生成的,不是调用现成的mp3音频播放出来的。

换种说法就是:无需任何音频文件,只需要几行JS代码,我们就能让网页发出各种各样的音调。

10年前,flash炫酷网页风兴起的时候,交互音效是非常普遍的,可以说是flash酷站必备元素。只是后来flash衰落了,而web对于多媒体资源的支持长久以来是比较弱的,于是Web网站全部都是安安静静的。在移动端,H5广告营销性质的网页都是音频用的比较多,但大多都是作为背景音乐使用的,交互时候其实用的并不多。我想,可能与大多数开发并不知道HTML5 Web Audio API可以直接弄出声音,知识的广度局限了技术的选型。

于是我就想到,HTML5 Web Audio API提及的人不多,使用并不火,并不一定是因为这个技术本身的局限或者使用场景不多,而是具有较大影响力的人,社区或企业并没有在这一块身先士卒进行推动。很多企业的官网、产品宣传页,广告营销性质的专题活动非常讲求炫酷,音视频层出不穷,很显然(参考flash时代),交互音效也其实是非常欢迎的,只是技术眼界的局限认为需要专门的mp3/wav音频调用成本高而不使用罢了。

HTML5 Web Audio API可以直接产生声音,并且可以随意控制音调,时长以及淡入淡出等效果,更强大更灵活。而这个功能点是HTML5 Web Audio API中最简单最基本也是最实用的,没有理由不给web开发带来交互音效热潮。

三、网页交互音效AudioContext,AudioParam等API介绍

下面就是声音出现的JS相关代码:

1.  window.AudioContext = window.AudioContext || window.webkitAudioContext;
2.  var audioCtx = new AudioContext();
3.  var oscillator = audioCtx.createOscillator();
4.  var gainNode = audioCtx.createGain();
5.  oscillator.connect(gainNode);
6.  gainNode.connect(audioCtx.destination);
7.  oscillator.type = 'sine';
8.  oscillator.frequency.value = 196.00;
9.  gainNode.gain.setValueAtTime(0, audioCtx.currentTime);
10. gainNode.gain.linearRampToValueAtTime(1, audioCtx.currentTime + 0.01);
11. oscillator.start(audioCtx.currentTime);
12. gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 1);
13. oscillator.stop(audioCtx.currentTime + 1);

总共13行代码,虽然行数不多,但其中出现的API数量以及其中的含义还是有点厚度的。

我们一行一行来看。

1. window.AudioContext = window.AudioContext || window.webkitAudioContext
没啥好说的,向前兼容一下老的webkit浏览器。

2. var audioCtx = new AudioContext()
AudioContext是HTML5 Web Audio最基础估计也是最常用的API了,其角色和地位可以和Canvas的canvas.getContext一拼。顾名思义,指定一个音频上下文,包含大量的属性和方法,而且这些方法名又多又长,根本就记不住,随便截个图感受一下

AudioContext方法名截图

所以,必须要有文档:MDN-AudioContext

但,不要怕,对于简单的音调播放,我们只需要关心下面几个属性和方法就好了,上面那么多高级的东西都是留给需要深入和音频打交道的开发人员用的。

3. var oscillator = audioCtx.createOscillator();
createOscillator()AudioContext的一个方法,创建一个OscillatorNodeOscillatorNode表示一个周期性波形,比如一个正弦波(就是上下弯弯的那个波),大家应该都知道声音本质其实就是震动,是和波形紧密关联的,因此波形不一样,最终的声音也就不一样,例如有的波形是“哔哔哔…”,有的波形出来的是“嘟呜呜…”。波还受频率影响,最终表现为不一样的音调高低。因此,OscillatorNode就有frequencytype下面2个即将登场的属性。

总之,这里的变量oscillator基本上来说创造了一个音调。

4. var gainNode = audioCtx.createGain()
createGain()也是AudioContext的一个方法,创建一个GainNode,它可以控制音频的总音量。也就是说createOscillator()控制音调,createGain()控制音量,互相配合,黄金搭档。

GainNode只有一个简单的只读属性,名为gain,完整书写GainNode.gain,本身没什么,但是GainNode.gain的返回值可是个大家伙,是个a-rate类型的AudioParam

AudioParam总共有2个类型,一个是a-rate还有一个是k-rate,前者AudioParam为音频信号每个样品帧的当前声音参数值;后者的AudioParam使用整个块(即128个样本帧)相同的初始音频参数值。

AudioParam包含诸多属性和方法,都是与音量控制相关的。

5. oscillator.connect(gainNode)
音调和音量关联。

6. gainNode.connect(audioCtx.destination)
这里的audioCtx.destination返回AudioDestinationNode对象,AudioDestinationNode接口表示音频图形在特定情况下的最终输出地址 – 通常为扬声器。话句话说就是和音频设备关联。

这个关联为audio context中所有节点的最终节点,看下面的工作流图:

audio context工作流

7. oscillator.type = ‘sine’
我们的声音波形指定为'sine',也就是正弦波,还支持其他3种波形,为'square'方波,'triangle'三角波以及'sawtooth'锯齿波。形状如下图(图来自css-tricks

4种默认的波形图

下面这个Codepen可以感受4中波形带来的哔哔啵啵的声音:

以上四种波形是内置的,我们还可以使用setPeriodicWave()方法自定义波形。

8. oscillator.frequency.value = 196.00
设置波形的频率,实际上,可以理解为设置声音的音调,也就是设置最后的声音是“多瑞米发嗦啦西”其中的一个。数值越小,声音越低沉,越大提琴;数值越大,声音越清脆,越小提琴。

demo页面中的音调完整范围如下:

[196.00, 220.00, 246.94, 261.63, 293.66, 329.63, 349.23, 392.00, 440.00, 493.88, 523.25, 587.33, 659.25, 698.46, 783.99, 880.00, 987.77, 1046.50]

每次鼠标hover我都会变换一个频率,这就是为什么每次经过按钮就会听到有规律的音调变化。

9. gainNode.gain.setValueAtTime(0, audioCtx.currentTime)
先来了解下audioCtx.currentTime,其值是以双精度浮点型数字返回硬件调用的秒数。只读,也就是说这个时间你是无法改变的,无论是暂停或者重置都不可以。当我们使用类似new AudioContext()创建AudioContext的时候,这个时间就可以从0开始走了,然后就像离弦的箭,一直不回头。

gainNode.gain这个前面介绍过了,返回的是AudioParam,所以这里实际上执行的是AudioParam.setValueAtTime()方法,此方法支持两个参数,一个是音量(范围0~1),一个时间,这里setValueAtTime(0, audioCtx.currentTime)表示当下就把音量设为0,也就是没声音,因为快速鼠标hover时候,之前声音可能还没播放结束,需要从0开始。

10. gainNode.gain.linearRampToValueAtTime(1, audioCtx.currentTime + 0.01)
linearRampToValueAtTime()方法表示音量在某时间线性变化到某值,这里linearRampToValueAtTime(1, audioCtx.currentTime + 0.01)实际上表示的是在0.01秒的时间内,声音音量线性从01

11. oscillator.start(audioCtx.currentTime)
开始出声啦……

12. gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 1)
exponentialRampToValueAtTime()方法表示音量在某时间指数变化到某值,这里的exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 1)实际上表示的是在1.00秒的时间内,音量由之前的1以指数曲线的速度降到极低的0.001音量。

我们把10, 12行代码联系起来,可能就是这样一个曲线(自己手绘的,凑合看吧):
声音曲线图

这样处理,声音消失就有“念念不忘,必有回响”的效果,就很有琴键按下去声音的感觉。

13. oscillator.stop(audioCtx.currentTime + 1)
1秒后,声音关闭。

最终,我们把源码和注释重新整合下就有:

// 创建音频上下文  
var audioCtx = new AudioContext();
// 创建音调控制对象  
var oscillator = audioCtx.createOscillator();
// 创建音量控制对象  
var gainNode = audioCtx.createGain();
// 音调音量关联  
oscillator.connect(gainNode);
// 音量和设备关联  
gainNode.connect(audioCtx.destination);
// 音调类型指定为正弦波  
oscillator.type = 'sine';
// 设置音调频率  
oscillator.frequency.value = 196.00;
// 先把当前音量设为0  
gainNode.gain.setValueAtTime(0, audioCtx.currentTime);
// 0.01秒时间内音量从刚刚的0变成1,线性变化 
gainNode.gain.linearRampToValueAtTime(1, audioCtx.currentTime + 0.01);
// 声音走起 
oscillator.start(audioCtx.currentTime);
// 1秒时间内音量从刚刚的1变成0.001,指数变化 
gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 1);
// 1秒后停止声音 
oscillator.stop(audioCtx.currentTime + 1);

这下就好理解多了!

四、结束语

本文展示的交互音效仅仅是HTML5 Web Audio API众多特性中的冰山一角,但已经足够让广告性质类的网页熠熠生辉,甚至传统web站点在一些特殊场合也是可以尝试使用的。

最后,介绍几个和web audio相关的JS库,Pizzicato.jswebaudiox.jshowler.jsWADTone.js,适合复杂音频处理场景。

HTML5 Web Audio API Chrome,Safari以及Firefox浏览器都是支持的,兼容性还是不错的。

参考文章:

感谢阅读!

(本篇完)

分享到:

标签: , , , , , , ,

赞助商推荐(我也要赞助)

想学到点真东西? ×
如果你有1~3年前端开发经验,不妨 ×


发表评论(目前20 条评论)

  1. 夜晚风景说道:

    看到你的文章都是堪称经典!就是想问问有没有网页输入音频的办法!如果有的话希望能写一下给我们引路的文章!

  2. 小刚说道:

    web这厉害了

  3. 小杰说道:

    请问可以录音吗?

  4. jcomey说道:

    gainNode.gain.exponentialRampToValueAtTime(0.000000000000000000000000000000000000001, audioCtx.currentTime + 1);试试~什么效果 。哈哈哈哈哈

  5. 刀刀说道:

    哈哈哈。。可以在页面里写个钢琴自己玩了

  6. HTML5功能还是很强大的!

  7. 当年明月说道:

    学习了

  8. wingmeng说道:

    Web上还能这么玩?继美学、数学后,Web又涉足音乐领域,感觉Web要一统天下了,哈哈

  9. 盖大楼说道:

    下一期能不能写个关于 html嵌套规则的 文章

  10. 卷子说道:

    第一次看到,长见识了,但是这个api能兼容到ie几?

  11. nicholasurey说道:

    FF中并没有钢琴声啊。。。

  12. 刘东奇说道:

    是不是可以做一个html版的节奏大师?

  13. ###win7killer说道:

    https://win7killer.github.io/can_demo/demo/voice.html
    借着大神的代码撸了一个钢琴键的demo,移动端还没处理。。。

  14. 墨丶水瓶说道:

    这个点太前卫了,目前还没遇到这样站点。
    PC端的站点运用的话,没有太明显的效果,多数用户不会连接声音输出设备。
    移动端可能会好点吧 。

  15. 不二很纯洁说道:

    之前只是做过可视化的,对创建音乐的没怎么弄明白,大神就是大神啊,清晰明了

  16. zhzh5724说道:

    画个声音出来,
    啥时候能画视频前端就一统天下了

  17. 曾小乱说道:

    这个好酷,我要拿来改进一下我的网站