Feature image

HTML5音频可视化试验

上周末脑洞大开的想到能否完全用HTML5的Web Audio API实现一个吉他调音器,折腾过后这个主要的目标失败了,音频可视化的部分倒是相对完整,实现了音量、频谱图以及音高的可视化。

音高检测算法

调音器的原理其实很简单,本质上就是音高检测算法(pitch detection algorithm),实现的方式有很多[1],时域方法和频域方法都有。

乐音最基本的特征就是由一系列谐波组成,包含一个基本频率$f$,以及一系列基本频率的整数倍的子波$2f, 3f, 4f, …, nf$,基础频率$f$的值就是乐音的音调。

所以一种简单的频域音高检测算法就是HPS(harmonic product spectrum):

$$Y(\omega) = \prod_{r = 1}^R|X(\omega r)|$$
$$\hat{Y} = \max_{\omega_i}{Y(\omega_i)}$$

其中$X$是音频在频域空间的向量表示(通常通过FFT得到),HPS就是求一个频率的$R$个整数数倍位置的信号强度的乘积,形成一个新的频谱图$Y$。

谐波的基础频率就会在$Y$中形成波峰:

这个方法的缺点很明显,高频信号由于已经没有多少谐波被采样,因此在高频并不可靠。不过对于音乐而言,这并不是问题,钢琴的最高音C8不过才4186.01Hz,而音乐文件的采样率普遍是kHz级别。

HTML5 Audio API

用Audio API做音频可视化至少会创建三个对象:AudioContext, AudioSourceAnalyzerNode
前两者的作用很显然不用多说,AnalyzerNode提供实时的FFT数据。

首先当然是创建一个context,绑定一个analyzer:

1
2
3
4
context = new AudioContext()
analyzer = context.createAnalyser()
analyzer.smoothingTimeConstant = 0.3
analyzer.fftSize = 2048

其中fftSize指定把频域空间平均分成多少份.

接着是创建source,可以是麦克风的输入:

1
2
3
4
5
6
7
8
navigator.getUserMedia audio: true, ((stream) ->
source = context.createMediaStreamSource stream
mic_stream = stream
source.connect analyzer
analyzer.connect context.destination
console.log "Microphone open. Sample rate: #{context.sampleRate} Hz"
), (err) ->
console.error "Fail to access microphone: #{err}"

也可以是载入一个音频文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
source = context.createBufferSource()

source.connect analyzer
analyzer.connect context.destination

xhr = new XMLHttpRequest()
xhr.onload = =>
context.decodeAudioData xhr.response, ((b) ->
console.log "Audio loaded: #{url}, Sample rate: #{context.sampleRate}Hz"
source.buffer = b
source.loop = true
source.start 0.0
), (err) ->
console.error "Fail to load audio: #{url}"
xhr.open "GET", url, true
xhr.responseType = 'arraybuffer'
xhr.send()

然后需要创建一个ScriptProcessorNode,和analyzer链接,这样在每帧数据可用时会执行前者的onaudioprocess,音频处理算法一般都放在这里:

1
2
3
4
5
node = context.createScriptProcessor 2048, 1, 1
node.onaudioprocess = ->
# TODO: process audio
node.connect context.destination
analyzer.connect node

读取FFT数据:

1
2
3
4
5
node.onaudioprocess = ->
n = analyzer.frequencyBinCount
arr = new Uint8Array(n)
analyzer.getByteFrequencyData arr
# TODO: Draw

读出来的数组里包含了从低频到高频的强度,可以直接用于绘制频谱图,求和平均就是音量,也可以作为频域处理算法的输入。

数组里元素$i$对应的频率为$f_i = \frac{Sample Rate}{FFT Size} i$。

在调音器这个应用中,最大的问题就是精度,Audio API里最大取值只能是2048,在48kHz采样率时,频率分辨率只能到23.43Hz。
这个精度是无法接受的,如$E4 = 329.628, F4 = 349.228$之间只相差了19.6Hz,完全无法区分。

可视化

在这个项目里尝试了下processing.js,虽然API很清晰,但是绘图功能不足,没有原生的gradient,blur支持,只能人肉实现,于是性能一不小心就惨淡了。

源码

建议使用Chrome打开,FireFox上性能很惨淡。