上周末脑洞大开的想到能否完全用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
, AudioSource
和AnalyzerNode
。
前两者的作用很显然不用多说,AnalyzerNode
提供实时的FFT数据。
首先当然是创建一个context
,绑定一个analyzer
:
1 | context = new AudioContext() |
其中fftSize
指定把频域空间平均分成多少份.
接着是创建source
,可以是麦克风的输入:
1 | navigator.getUserMedia audio: true, ((stream) -> |
也可以是载入一个音频文件:
1 | source = context.createBufferSource() |
然后需要创建一个ScriptProcessorNode
,和analyzer
链接,这样在每帧数据可用时会执行前者的onaudioprocess
,音频处理算法一般都放在这里:
1 | node = context.createScriptProcessor 2048, 1, 1 |
读取FFT数据:
1 | node.onaudioprocess = -> |
读出来的数组里包含了从低频到高频的强度,可以直接用于绘制频谱图,求和平均就是音量,也可以作为频域处理算法的输入。
数组里元素$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上性能很惨淡。