这个调音器是一年前做的,现在想起来写个文章分享实现的过程。
一个简单调音器的实现有两个关键点:实时音频录制及处理 + 音高识别算法。需要注意的是,为了让代码更容易理解,以下展示的代码没有做任何兼容、容错处理,只能保证在 chrome 正常运行。完整的项目请看:在线演示,Github。
通过 Web Audio 获取实时录音数据
数据来源是我们首要考虑的,上一个时代其实就已经有可以运行在浏览器的调音器,只不过是用 flash 实现的。现在,我们很高兴地看到 Web Audio API 已经成为了标准,在浏览器里获取实时录音数据是可行的,兼容性尚且可以,所有最新主流浏览器基本都支持。相关链接:
接下来,我们来实践一下。
AudioContext = window.AudioContext || window.webkitAudioContext
const audioContext = new AudioContext()
const analyser = audioContext.createAnalyser()
const scriptProcessor = audioContext.createScriptProcessor(8192, 1, 1)
navigator.mediaDevices.getUserMedia({audio: true}).then(streamSource => {
// connect 的顺序一定要 streamSource => analyser => scriptProcessor
audioContext.createMediaStreamSource(streamSource).connect(analyser)
analyser.connect(scriptProcessor)
scriptProcessor.connect(audioContext.destination)
scriptProcessor.addEventListener('audioprocess', event => {
const data = event.inputBuffer.getChannelData(0)
// 为了避免卡住浏览器,只打印一些简单的数据
console.log(`${data.length}, ${data[0]}`)
})
})
可以看到,从浏览器获取录音数据还是很简单的,当然实际中你要处理 AudioContext
和 getUserMedia
的兼容性,以及必要的错误处理。
音高检测(Pitch detection)
音高检测算法已经很成熟了,不乏论文和资料,诸如 java、c/c++ 都有现成的库可用,而 js 在这方面显然是有缺失的。因为我的目的是快速实现一个调音器,所以我是基于一个现有的 c 音频库 aubio 用 emscripten 编译成 js,以便在浏览器里运行。
我们来试试效果:
AudioContext = window.AudioContext || window.webkitAudioContext
const bufferSize = 8192
const audioContext = new AudioContext()
const analyser = audioContext.createAnalyser()
const scriptProcessor = audioContext.createScriptProcessor(bufferSize, 1, 1)
const pitchDetector = new (Module().AubioPitch)(
'default', bufferSize, 1, audioContext.sampleRate)
navigator.mediaDevices.getUserMedia({audio: true}).then(streamSource => {
// connect 的顺序一定要 streamSource => analyser => scriptProcessor
audioContext.createMediaStreamSource(streamSource).connect(analyser)
analyser.connect(scriptProcessor)
scriptProcessor.connect(audioContext.destination)
scriptProcessor.addEventListener('audioprocess', event => {
const frequency = self.pitchDetector.do(event.inputBuffer.getChannelData(0))
if (frequency) {
console.log(frequency)
}
})
})
实现调音器界面
我的计划是做一个表盘式的调音器,首先,我需要把音高频率转成音名及对应的八度,比如 82.41 Hz 对应 E2。音符 - 维基百科 里有详细的描述及公式。
这里我们就用标准的 MIDI 转换公式,基于十二平均律,设定标准音高为 440 Hz:
其次,为了描述音的偏移距离,还需要引入音分的概念,其公式:
写成代码就是:
const MIDDLE_A = 440
const SEMITONE = 69
/**
* get musical note from frequency
*
* @param {float} frequency
* @returns {int}
*/
function getNote(frequency) {
var note = 12 * (Math.log(frequency / MIDDLE_A) / Math.log(2))
return Math.round(note) + SEMITONE
}
/**
* get the musical note's standard frequency
*
* @param note
* @returns {number}
*/
function getStandardFrequency(note) {
return MIDDLE_A * Math.pow(2, (note - SEMINETON) / 12)
}
/**
* get cents difference between given frequency and musical note's standard frequency
*
* @param {float} frequency
* @param {int} note
* @returns {int}
*/
function getCents(frequency, note) {
return Math.floor(1200 * Math.log(frequency / getStandardFrequency(note)) / Math.log(2))
}
表盘的实现
我们可以用 css 的 transform: rotate,只要把音分差值转换成旋转角度:
到这里,一个可用的调音器已经可以基本实现了,更多实现细节可以看项目代码。