深刻浅出 Web Audio Api

题图:Egor Khomiakovcss

注:本文同时发布在 知乎专栏html

什么是 Web Audio Api

首先引用一下 MDN 上对 Web Audio Api 的一段描述:html5

The Web Audio API involves handling audio operations inside an audio context, and has been designed to allow modular routing. Basic audio operations are performed with audio nodes, which are linked together to form an audio routing graph.node

大体的意思就是 Web Audio API 须要在音频上下文中处理音频的操做,并具备模块化路由的特色。基本的音频操做是经过音频节点来执行的,这些音频节点被链接在一块儿造成音频路由图。webpack

咱们能够从上面这段文字中提取出几个关键词:git

  • 音频上下文
  • 音频节点
  • 模块化
  • 音频图

我将会以这些关键词为开始,慢慢介绍什么是 Web Audio Api,如何使用 Web Audio Api 来处理音频等等。github

音频上下文(AudioContext)

音频中的 AudioContext 能够类比于 canvas 中的 context,其中包含了一系列用来处理音频的 API,简而言之,就是能够用来控制音频的各类行为,好比播放、暂停、音量大小等等等等。建立音频的 context 比建立 canvascontext 简单多了(考虑代码的简洁性,下面代码都不考虑浏览器的兼容状况):web

const audioContext = new AudioContext();复制代码

在继续了解 AudioContext 以前,咱们先来回顾一下,平时咱们是如何播放音频的:ajax

<audio autoplay src="path/to/music.mp3"></audio>复制代码

或者:canvas

const audio = new Audio();
audio.autoplay = true;
audio.src = 'path/to/music.mp3';复制代码

没错,很是简单的几行代码就实现了音频的播放,可是这种方式播放的音频,只能控制播放、暂停等等一些简单的操做。可是若是咱们想要控制音频更「高级」的属性呢,好比声道的合并与分割、混响、音调、声相控制和音频振幅压缩等等,能够作到吗?答案固然是确定的,一切都基于 AudioContext。咱们以最简单的栗子来了解一下 AudioContext 的用法:

const URL = 'path/to/music.mp3';
const audioContext = new AudioContext();
const playAudio = function (buffer) {
    const source = audioContext.createBufferSource();
    source.buffer = buffer;
    source.connect(audioContext.destination);
    source.start();
};
const getBuffer = function (url) {
    const request = new XMLHttpRequest();
    return new Promise((resolve, reject) => {
        request.open('GET', url, true);
        request.responseType = 'arraybuffer';
        request.onload = () => {
            audioContext.decodeAudioData(request.response, buffer => buffer ? resolve(buffer) : reject('decoding error'));
        };
        request.onerror = error => reject(error);
        request.send();
    });
};
const buffer = await getBuffer(URL);
buffer && playAudio(buffer);复制代码

别方,这个栗子真的是最简单的栗子了(尽可能写得简短易懂了),其实仔细看下,代码无非就作了三件事:

  • 经过 ajax 把音频数据请求下来;
  • 经过 audioContext.decodeAudioData() 方法把音频数据转换成咱们所须要的 buffer 格式;
  • 经过 playAudio() 方法把音频播放出来。

你没猜错,达到效果和刚刚提到的播放音频的方式一毛同样。这里须要重点讲一下 playAudio 这个函数,我提取出了三个关键点:

  • source
  • connect
  • destination

你能够试着以这种方式来理解这三个关键点:首先咱们经过 audioContext.createBufferSource() 方法建立了一个「容器」 source 并装入接收进来的「水」 buffer;其次经过「管道」 connect 把它和「出口」 destination 链接起来;最终「出口」 destination 「流」出来的就是咱们所听到的音频了。不知道这么讲,你们有没有比较好理解。

AudioContext
AudioContext

或者也能够拿 webpack 的配置文件来类比:

module.exports = {
    // source.buffer
    entry: 'main.js',
    // destination
    output: {
        filename: 'app.js',
        path: '/path/to/dist',
    },
};复制代码

sourcedestination 分别至关于配置中的入口文件和输出文件,而 connect 至关于 webpack 内置的默认 loader,负责把源代码 buffer 生成到输出文件中。

重点理解这三个关键点的关系

注意:Audio 和 Web Audio 是不同的,它们之间的关系大概像这样:

Web audio API and Audio
Web audio API and Audio

Audio:

  • 简单的音频播放器;
  • 「单线程」的音频;

Web Audio:

  • 音频合成;
  • 能够作音频的各类处理;
  • 游戏或可交互应用中的环绕音效;
  • 可视化音频等等等等。

音频节点(AudioNode)

到这里,你们应该大体知道了如何经过 AudioContext 去控制音频的播放。可是会发现写了这么一大堆作的事情和前面提到的一行代码的所作的事情没什么区别(<audio autoplay src="path/to/music.mp3"></audio>),那么 AudioContext 具体是如何去处理咱们前面所提到的那些「高级」的功能呢?就是咱们接下来正要了解的 音频节点

那么什么是音频节点呢?能够把它理解为是经过「管道」 connect 链接在「容器」source 和「出口」 destination 之间一系列的音频「处理器」。AudioContext 提供了许多「处理器」用来处理音频,好比音量「处理器」 GainNode、延时「处理器」 DelayNode 或声道合并「处理器」 ChannelMergerNode 等等。

前面所提到的「管道」 connect 也是由音频节点 AudioNode 提供的,因此你猜的没错,「容器」 source 也是一种音频节点。

const source = audioContext.createBufferSource();
console.log(source instanceof AudioNode); // true复制代码

AudioNode 还提供了一系列的方法和属性:

  • .context (read only): audioContext 的引用
  • .channelCount: 声道数
  • .connect(): 链接另一个音频节点
  • .start(): 开始播放
  • .stop(): 中止播放

更多详细介绍可访问 MDN 文档

GainNode

GainNode
GainNode

前面有提到音频处理是经过一个个「处理器」来处理的,那么在实际应用中怎么把咱们想要的「处理器」装上去呢?

Don't BB, show me the code:

const source = audioContext.createBufferSource();
const gainNode = audioContext.createGain();
const buffer = await getBuffer(URL);

source.buffer = buffer;
source.connect(gainNode);
gainNode.connect(source.destination);

const updateVolume = volume => gainNode.gain.value = volume;复制代码

能够发现和上面提到的 playAudio 方法很像,区别只是 source 不直接 connect 到 source.destination,而是先 connect 到 gainNode,而后再经过 gainNode connect 到 source.destination。这样其实就把「音量处理器」装载上去了,此时咱们经过更新 gainNode.gain.value 的值(0 - 1 之间)就能够控制音量的大小了。

Full Demo

BiquadFilterNode(waiting for perfection)

BiquadFilterNode
BiquadFilterNode

不知道怎么翻译这个「处理器」,暂且叫作低阶滤波器吧,简单来讲它就是一个经过过滤音频的数字信号进而达到控制 音调 的音频节点。把它装上:

const filterNode = audioContext.createBiquadFilter();
// ...
source.connect(filterNode);
filterNode.connect(source.destination);

const updateFrequency = frequency => filterNode.frequency.value = frequency;复制代码

这样一来咱们就能够经过 updateFrequency() 方法来控制音频的音调(频率)了。固然,除了 frequency 咱们还能够调整的属性还有(MDN Docs):

  • .Q: quality factor;
  • .type: lowpass, highpass, bandpass, lowshelf, highshelf, peaking, notch, allpass;
  • .detune: detuning of the frequency in cents.

Full Demo

PannerNode

咱们能够调用 PannerNode.setPosition() 方法来作出很是有意思的 3D 环绕音效:

<input type="range" name="rangeX" value="0" max="10" min="-10">复制代码
const rangeX = document.querySelector('input[name="rangeX"]');
const source = audioContext.createBufferSource();
const pannerNode = audioContext.createPanner();

source.connect(pannerNode);
pannerNode.connect(source.destination);

rangeX.addEventListener('input', () => pannerNode.setPosition(rangeX.value, 0, 0));复制代码

仍是老方法「装上」 PannerNode 「处理器」,而后经过监听 range 控件的 input 事件,经过 .setPosition() 方法更新 声源相对于听音者的位置,这里我只简单的更新了声源相对于听音者的 X 方向上的距离,当值为负值时,声音在左边,反之则在右边。

你能够这么去理解 PannerNode,它把你(听音者)置身于一个四面八方都很是空旷安静的空间中,其中还有一个音响(声源),而 .setPosition() 方法就是用来控制 音响 在空间中 相对于你(听音者) 的位置的,因此上面这段代码能够控制声源在你左右俩耳边来回晃动(带上耳机)。

Full Demo

固然,对于 PannerNode 来讲,还有许多属性可使得 3D 环绕音效听上去更逼真,好比:

  • .distanceModel: 控制音量变化的方式,有 3 种可能的值:linear, inverseexponential
  • .maxDistance: 表示 声源听音者 之间的最大距离,超出这个距离后,听音者将再也不能听到声音;
  • .rolloffFactor: 表示当 声源 远离 听音者 的时候,音量以多快的速率减少;

这里只列举了经常使用的几个,若是想进一步了解 PannerNode 能作什么的话,能够查阅 MDN 上的 文档

多个音频源

前面有提到过,在 AudioContext 中能够同时使用多个「处理器」去处理一个音频源,那么多个音频源 source 能够同时输出吗?答案固然也是确定的,在 AudioContext 中能够有多个音频处理通道,它们之间互不影响:

cross fading
cross fading

const sourceOne = audioContext.createBufferSource();
const sourceTwo = audioContext.createBufferSource();
const gainNodeOne = audioContext.createGain();
const gainNodeTwo = audioContext.createGain();

sourceOne.connect(gainNodeOne);
sourceTwo.connect(gainNodeTwo);
gainNodeOne.connect(audioContext.destination);
gainNodeTwo.connect(audioContext.destination);复制代码

Full Demo

模块化(Modular)

Modular
Modular

经过前面 音频节点 的介绍,相信大家已经感觉到了 Web Audio 的模块化设计了,它提供了一种很是方便的方式来为音频装上(connect)不一样的「处理器」 AudioNode。不只一个音频源可使用多个「处理器」,而多个音频源也能够合并为一个「输出」 destination

得益于 Web Audio 的模块化设计,除了上面提到的模块(AudioNode),它还提供了很是多的可配置的、高阶的、开箱即用的模块。因此经过使用这些模块,咱们彻底能够建立出功能丰富的音频处理应用。

若是你对 AudioContextAudioNode 之间的关系尚未一个比较清晰的概念的话,就和前面一开始所说的那样,把它们和 webpack 和 loader 作类比,AudioContext 和 webpack 至关于一个「环境」,模块(AudioNodeloader)能够很方便在「环境」中处理数据源(AudioContext 中的 buffer 或 webpack 中的 js, css, image 等静态资源),对好比下:

module.exports = {
    entry: {
        // 多音频源合并为一个输出
        app: ['main.js'], // source.buffer
        vender: ['vender'], // source.buffer
    },
    output: { // source.destination
        filename: 'app.js',
        path: '/path/to/dist',
    },
    // AudioNode
    module: {
        rules: [{
            // source.buffer
            test: /\.(scss|css)$/,
            // AudioNode: GainNode, BiquadFilterNode, PannerNode ...
            use: ['style-loader', 'css-loader', 'sass-loader'],
        }],
    },
};复制代码

再次发现,Web Audio Api 和 webpack 的设计理念如此的类似。

音频图(Audio Graph)

Audio Graph
Audio Graph

An audio graph is a set of interconnected audio nodes.

如今咱们知道了,音频的处理都是经过 音频节点 来处理的,而多个音频节点 connect 到一块儿就造成了 音频导向图(Audio Routing Graph),简而言之就是多个相互链接在一块儿的音频节点。

总结

本文展现的仅仅只是 Web Audio 众多 API 中的冰山一角,若是想更深刻了解 Web Audio 的话,建议能够去查阅相关文档。尽管如此,利用上面介绍的一些 API 也足够作出一些有意思的音乐效果来了。

参考资料

  1. Web Audio Api - MDN
  2. Getting Started with Web Audio API
相关文章
相关标签/搜索