Lee
Photo by James Kovin
Lee
Lee
Published at Aug 16, 2017 · 6 mins to read

深入浅出 Web Audio Api

Contents

什么是 Web Audio Api

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

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.

我们可以从上面这段文字中提取出几个关键词:

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

音频上下文(AudioContext)

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

const audioContext = new AudioContext()

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

<audio autoplay src="path/to/music.mp3"></audio>

或者:

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)

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

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

你可以试着以这种方式来理解这三个关键点:首先我们通过 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 还提供了一系列的方法和属性:

更多详细介绍可访问 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):

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 环绕音效听上去更逼真,比如:

这里只列举了常用的几个,如果想进一步了解 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 也足够做出一些有意思的音乐效果来了。

References

  1. Web Audio Api - MDN
  2. Getting Started with Web Audio API