这个需求来自于小宇宙Studio的试听页面:试听页面本身除了需要支持播放之外,还有着倍速播放以及展示频率图的需求。好在WebAudio API本身内置的功能足以覆盖这些需求。
音频播放部分
浏览器内置播放音频有两种方式,一是使用html5标准中的<audio>
标签内置的播放功能,二则是通过WebAudio API来播放。
WebAudio提供了一系列处理音频的能力。在音频处理方面,只要是在某个AudioContext中的Node就拥有着从上一个Node中拿到采样并且处理传递给下一个Node的能力;WebAudio API也有着多种AudioSourceNode,可以通过Stream、Buffer或者MediaElement创建出对应的AudioSourceNode。
在目前的场景下,需要播放的音频是通过URL下载的,因此我们可以通过<audio>
标签来完成下载以及解码音频的工作,之后可以通过MediaElementAudioSourceNode
来获取解码之后的数据。大致代码如下:
const audioElement: HTMLAudioElement = /* 获取到audio标签 */
const audioContext = new AudioContext()
const sourceNode = MediaElementSourceNode(audioElement)
sourceNode.connect(audioContext.destination)
这样以来,只要触发audio标签的播放,音频的数据不会直接流向正常的播放流程,而且发送到了我们的AudioContext中,这里我们直接将source node和AudioContext的destination相连,音频数据最终就输出到了声卡等其他地方变成声音播放了出来;使用audio标签作为音频的来源还有一个好处就是我们可以使用audio标签自带的诸如playback rate之类的控制,这样就不需要自己重复实现一次了。
获取频率数据
WebAudio API内置了AnalyserNode
,能够对输入的sample数据进行一些分析,例如能进行快速傅立叶变换从而获得输入sample的实时频率以及时域信息。只要指定了fftSize
然后调用对应API时传入一个Uint8Array
就可以获取这些数据了。简单的实现如下:
cconst audioElement: HTMLAudioElement = /* 获取到audio标签 */
const audioContext = new AudioContext()
const sourceNode = audioContext.createMediaElementSource(audioElement)
const analyserNode = audioContext.createAnalyser()
analyserNode.fftSize = 1024
// sourceNode -> analyserNode -> audiocContext.destination
sourceNode.connect(analyserNode)
analyserNode.connect(audioContext.destination)
const LENGTH = analyserNode.frequencyBinCount
const dataArray = new Uint8Array(LENGTH)
// 每调用一次就会将当前的实时频率数据放入dataArray中
analyserNode.getByteFrequencyData(dataArray)
绘制频率图
有了频率数据后,就可以开始着手绘制频率图了。音频在播放时,每一个瞬间获取到的频率/时域数据都是不一样的,因此我们可以在requestAnimationFrame
函数的回调中通过getByteFrequencyData
获取一次数据,并且基于获取到的Uint8Array来进行绘制;在音频领域一般频率信息是使用直方图进行绘制的,恰好小宇宙Studio的波形样式一定程度上也能看做直方图。
如同剪辑页面波形图在绘制上做的优化,这里也可以采用先绘制一堆矩形的path然后一次填充的方式来绘图,这样每一次绘制的步骤比较少;同时由于frequencyBinCount
(其值为fftSize
的一半)可能会比较大,而这里的频率图对精度没有要求,我们可以将数据分组求平均值然后使用平均值作为矩形的高度。
下面是绘制的代码:
const ctx = canvas.getContext('2d')
// BAR_WIDTH为矩形条的宽度,BAR_GAP为两个矩形条之间的间隔,均为定值
const barCount = Math.floor(CANVAS_WIDTH / (BAR_WIDTH + BAR_GAP))
const draw = () => {
analyserNode.getByteFrequencyData(dataArray)
const chunks = chunk(Array.from(dataArray), dataArray.length / barCount)
ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT)
ctx.fillStyle = BAR_COLOR
ctx.beginPath()
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i]
const x = i * (BAR_WIDTH + BAR_GAP)
const avgFrequency = sum(chunk) / chunk.length
const barHeight = calcBarHeight(avgFrequency, CANVAS_HEIGHT)
const y = CANVAS_HEIGHT - barHeight
ctx.rect(x, y / 2, BAR_WIDTH, barHeight)
}
ctx.closePath()
ctx.fill()
requestAnimationFrame(draw)
}
requestAnimationFrame(draw)
最后实际会发现效果不错,浏览器每一帧都会使用当前的实时绘制出图像,频率数据会随着音频实时变化,因此图像也会随着音频实时变化,自然又流畅。
附上这一套代码的架构图以及实际效果图:
如果你想亲自看看上面提到的频率图,可以访问这个页面 (密码是QUHY
,点击右上角e可以切换到波形模式)。美中不足的是,由于部分WebKit内核WebAudio播放音频时会有杂音和卡顿,所以在这些浏览器(就不点名批评是谁了)上为了收听体验我们选择fallback到了通过audio播放,因此是没有显示波形图的。
最后, AnalyserNode
提供的能力除了上面提到的getByteFrequencyData
之外还有其他很多可以使用的API,读到这里的大家可以试试使用 getByteTimeDomainData
获取时域信息,从而绘制类似示波器的图案。