Web Audio,给声音加点料

2018-03-12 放松一下

Web Audio, 这个名字乍看之下会让很多人误以为是 HTML5 中的 <audio> 元素。但其实二者并不是一回事。那么,什么是 Web Audio 呢?它是 web 中处理和分析音频内容的一套高级应用接口。由 W3C Audio Working Group 定制,目前还处在草案规范的阶段。

Web Audio vs <audio>

在 H5 <audio> 出现之前,网页中要想播放音频资源就需要借助 flash 或者其他插件,带来的体验比较差。H5 引入 <audio> 之后,就可以直接在网页当中嵌入音频,使用起来比较简单,可以使用浏览器默认的播放控件,可以在 js 中监听相应的事件,操作 Audio 对象的属性和方法,实现自定义的播放控件等等。总体上,H5 的 audio 元素满足了 web 上音频播放的基本需求,比如简单的音乐播放器。

但是对于一些场景中更复杂的音频处理需求,这个时候就需要 Web Audio 出场了,这也是 Web Audio 设计的目的,提供给开发者对音频数据进行进一步加工和分析的能力,就像对音频文件进行 PS。

web 音频从 Flash 时代到大家熟悉的的 H5 <audio>, 再到目前还处在草案规范的 Web Audio。Falsh 已经退出历史舞台了,但是 Web Audio 并不是用来取代 H5 <audio> 的,而是对它的功能上的补充。它们的关系可以类比 <img> 元素与 Canvas 的关系。

对于简单的需求场景,<audio> 就足够能 hold 住了,对于更复杂的需求才需要用到 Web Audio, 而且可以将二者结合起来使用。

:point_down:下面,看一下更复杂的需求是指哪些?或者说,Web Audio 究竟有哪些神奇的魔法?

Web Audio 可以做什么?

W3C Web Audio API 草案规范 中列出了 Web Audio 支持的一些特性:

比如可以对简单或复杂的声音进行混合;添加淡入/淡出,音调控制等效果; 灵活处理音频流的声道,分离和合并声道;处理来源于 Audio 和 Video 媒体元素的音频源;实现立体音效从而支持 3D 游戏和沉浸式环境;利用高效的实时、时域和频率分析,配合 CSS3 或 Canvas 或者 WebGL 实现音乐可视化效果等等。

总体来看,通过 Web Audio API 可以为音频添加不同的音效, 就像 DJ 可以灵活地控制音乐播放的节奏和效果一样。

:point_down:下面,整理一下 Web Audio 最基础的一些概念和用法。具体参见: MDN Web Audio API

Web Audio 工作流程

在了解 Web Audio 具体的 API 之前,需要知道它的基本工作流程。

Web Audio 有一个 AudioContext(音频上下文),类似于 Canvas 当中的 context 对象,所有的操作都需要基于这个 AudioContext 进行。Web Audio 具有模块化路由的特点,什么是模块化路由呢?就是 Web Audio 里面有许多的音频节点(AudioNode), 这些音频节点通过输入和输出相互连接,构成链状或者网状的音频路由结构。 一般来说,音频路由会从一个或多个音频源开始,由音频源提供声音采样数据,作为最初的输入,然后再经过中间一系列效果节点的处理,最后连接到目的地进行输出。

inputs → effects → destination

Web Audio 的基本流程可以归纳为以下几个主要步骤:

  • 创建音频上下文
  • 在音频上下文里创建源节点
  • 根据需要,创建效果节点
  • 为音频选择一个目地,通常是系统扬声器
  • 将源节点、效果节点和目的地节点连接起来,实现音频内容的效果输出

Web Audio 常用 API

AudioContext

音频上下文,是 Web Audio 中处理 web音频的核心对象。构建音频节点图的第一步就是需要先创建 AudioContext 对象。

// 最简单的创建方法:
var audioCtx = new AudioContext();
// 推荐的兼容性写法:
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();

(详见: MDN AudioContext

AudioNode

音频节点,每个节点都是一个音频处理模块。最常用的两个方法:

// 用于连接节点,前一个节点的输出会作为下一个节点的输入
AudioNode.connect() 

// 用于断开节点之间的连接
AudioNode.disconnect()

(详见: MDN AudioNode

按照功能划分,音频节点 AudioNode 大致包含三种类型:一类是作为音频源的节点,一类是作为最终输出的目的地节点,还有就是中间处理环节会用到的各种效果节点。

:point_down:下面,分别介绍三种类型中最常用的音频节点。

Inputs 音频源节点 :sparkles:

音频源节点的特点是只有一个输出而没有输入。因为他们本身就是最初的输入。

OscillatorNode

振荡器节点,用于产生一个恒定的音调。通过 AudioContext.createOscillator() 方法创建的。两个主要属性:

  • OscillatorNode.frequency,代表振动的频率(单位为赫兹hertz),默认值是 440 Hz;
  • OscillatorNode.type ,决定 OscillatorNode 播放的声音的周期波形, 基础值有 sine(默认值) / square / sawtooth / triangle/ custom。

两个主要方法:start() / stop(),用于指定开始 / 结束播放音调的确切时间。参数可选,默认是0,也就是立即开始或停止播放。

使用的时候一般会用到 AudioContext 的一个常用属性。 AudioContext.currentTime 只读,返回 double 类型。以秒为单位,从0开始,表示一个只增不减的硬件时间戳,可以用来调度音频播放、实现可视化时间轴等等。

(示例代码可戳 → OscillatorNode Example

(详见: MDN OscillatorNode )

AudioBufferSourceNode

AudioBufferSourceNode 表示由内存音频数据组成的音频源,通过 AudioContext.createBufferSource() 创建。它的音频数据是存储在 AudioBuffer (代表内存中的一段音频数据)中。

AudioBufferSourceNode.buffer = soundBuffer;

可以通过 AudioContext.createBuffer() 方法从原始数据创建 AudioBuffer,更常用的是通过 XMLHttpRequest 或者 FileReader 来获取原始的 ArrayBuffer 数据,然后通过 AudioContext.decodeAudioData() 来进行异步解码获取音频文件数据之后再将其放入创建好的 AudioBufferSourceNode 中使用。

// 旧版的回调函数语法
audioCtx.decodeAudioData(audioData, function(decodedData) {
  // use the decoded data here
});
// 新版的promise-based语法:
audioCtx.decodeAudioData(audioData).then(function(decodedData) {
  // use the decoded data here
});

(示例代码可戳→ AudioBufferSourceNode Example )

(详见: MDN AudioBufferSourceNode )

MediaElementAudioSourceNode

这个接口表示由 HTML5 <audio> 或 <video> 元素生成的音频源。使用 AudioContext.createMediaElementSource() 方法创建。

这个节点本身没有 start 和 stop 方法。它相应的播放操作还是使用 H5 <audio> 默认的播放控件或使用 H5 媒体元素提供的 API, 比如调用 audio.play()等。

(示例代码可戳 → MediaElementAudioSourceNode Example )

(详见: MDN MediaElementAudioSourceNode

Destination 目的地节点 :sparkles:

AudioDestinationNode

定义了最后音频要输出到哪里,通常不需要复杂的设置,默认输出到系统的扬声器。跟音频源节点相反的是,有一个输入,没有输出,因为它本身就是输出。通过 AudioContext.destination 属性来获取。

...
// connect source to destination
oscillator.connect(audioCtx.destination);

(详见: MDN AudioDestinationNode )

Effects 效果节点 :sparkles:

GainNode

最常用的音频处理模块。用于控制音量。一个 GainNode 总是只有一个输入和一个输出,在输出前使用给定增益应用到输入。增益是一个无单位量,会对所有输入声道的音频进行相应的增加。使用 AudioContext.createGain() 来实例化一个GainNode对象。

var audioCtx = new AudioContext();
var gainNode = audioCtx.createGain();
gainNode.gain.value = 0.5; // 值为 0 时即静音

(示例代码可戳 → GainNode Example )

(详见: MDN GainNode )

AnalyserNode

Web Audio 一个有趣的特性就是能够实现音频数据可视化。如果想从音频里提取时间、频率或者其它数据,就需要用到这个音频模块。创建方法是 AudioContext.createAnalyser() 。两个主要属性:

  • AnalyserNode.fftSize。一个无符号长整形(unsigned long)的值, 用于确定频域的 FFT (快速傅里叶变换) 的大小。fftSize 属性的值必须是从 32 到 32768范围内的 2 的非零幂; 其默认值为2048。如果设置的值不是 2 的幂, 或者在指定范围之外, 则抛出异常。
var audioCtx = new AudioContext();
var analyser = audioCtx.createAnalyser();
analyser.fftSize = 2048;
  • AnalyserNode.frequencyBinCount 。只读属性。值为fftSize的一半. 这通常等于将要用于可视化的数据值的数量。
var bufferLength = analyser.frequencyBinCount;

主要的几个方法:

  • AnalyserNode.getFloatFrequencyData()。将当前频域数据拷贝进Float32Array数组
  • AnalyserNode.getByteFrequencyData()。将当前频域数据拷贝进Uint8Array数组。
  • AnalyserNode.getFloatTimeDomainData()。将当前波形,或者时域数据拷贝进Float32Array数组。
  • AnalyserNode.getByteTimeDomainData()。将当前波形,或者时域数据拷贝进 Uint8Array数组。

以上方法,前两个用于获取频率数据,后两个用于获取波形数据。它们都是用来将数据复制到指定的数组当中,数组类型是 Uint8 或者 是 Float32。需要在调用这些方法之前创建相应的数组。数组的长度就是 analyser.frequencyBinCount。

...
var bufferLength = analyser.frequencyBinCount;
var dataArray = new Uint8Array(bufferLength);

然后就可以调用上面的其中一种方法,并且将这个创建好的数组作为参数,来获取需要的数据。

analyser.getByteTimeDomainData(dataArray);

(示例代码可戳 → AnalyserNode Example )

(详见: MDN AnalyserNode )

PannerNode

这个音频处理模块可以为音频源添加空间平移效果,用来制作 3D 音效。通过 AudioContext.createPanner() 来实例化一个PannerNode对象。两个主要方法:

  • PannerNode.setPosition(x,y,z)。用来设置声源的位置,默认值是(0,0,0)。
  • PannerNode.setOrientation(x,y,z)。用来设置声源的朝向,默认值是(1,0,0)。

它们的参数x,y和z是无单位的,用于表示三维空间中的位置。

(示例代码可戳 → PannerNode Example )

(详见: MDN PannerNode )

Demo

:point_down:下面是两个 Web Audio 的小 Demo,主要是对上面一些基础概念和用法的综合运用, 还有一些相关知识点的补充。

Demo1: OscillatorNode 实现钢琴键盘效果

代码戳 → Mini-Piano Demo

前面设置音量的时候用到了 GainNode.gain, 这是一个 AudioParam(详见: MDN AudioParam )。AudioParam 接口代表音频相关的参数,可以直接在一段时间内设定值,或者预定在将来更改,这些更改可以是线性的,指数的,也可以是自定义的曲线。

  • AudioParam.setValueAtTime()。在一个确切的时间,即时更改 AudioParam 的值。
  • AudioParam.linearRampToValueAtTime()。调整 AudioParam 的值,使其逐渐按线性变化。
  • AudioParam.exponentialRampToValueAtTime() 调整 AudioParam 的值,使其逐渐按指数变化。

Demo1 中用到了上面的几种方法来实现钢琴琴键切换时候的淡入淡出效果。

Demo2: AnalyserNode + Canvas 实现音乐可视化效果

代码戳 → Music-Visual Demo

Demo2 通过 AnalyserNode.getByteFrequencyData() 获取音频的频率数据,结合 Canvas 来实现音乐可视化效果。这里面其实 Web Audio 的部分比较简单,更多的是 Canvas 的一些绘图操作。

慕课网上一个不错的 Web Audio 音乐可视化教学视频: 慕课网 HTML5音乐可视化