基于 Web Audio API 实现一个 Tuner(调音器)

这个调音器是一年前做的,现在想起来写个文章分享实现的过程。

一个简单调音器的实现有两个关键点:实时音频录制及处理 + 音高识别算法。需要注意的是,为了让代码更容易理解,以下展示的代码没有做任何兼容、容错处理,只能保证在 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]}`)
  })
})

在线演示

可以看到,从浏览器获取录音数据还是很简单的,当然实际中你要处理 AudioContextgetUserMedia 的兼容性,以及必要的错误处理。

音高检测(Pitch detection)

音高检测算法已经很成熟了,不乏论文和资料,诸如 java、c/c++ 都有现成的库可用,而 js 在这方面显然是有缺失的。因为我的目的是快速实现一个调音器,所以我是基于一个现有的 c 音频库 aubioemscripten 编译成 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)
    }
  })
})

在线演示

这是我的吉他6弦拨出来的声音,可以看到,在末尾识别成了第二泛音,现实中确实会出现第二泛音比第一泛音强的情况,但我们认为这是错误的结果,是需要优化避免的

实现调音器界面

我的计划是做一个表盘式的调音器,首先,我需要把音高频率转成音名及对应的八度,比如 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,只要把音分差值转换成旋转角度:


$pointer.style.transform = 'rotate(' + (cents / 50 * 45) + 'deg)'

到这里,一个可用的调音器已经可以基本实现了,更多实现细节可以看项目代码。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,241评论 4 61
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 173,287评论 25 708
  • 如果是一个人是因愿来到这个世间,许多人都会嗤之以鼻。 但缘法的不可思议,愿的玄妙,又有几人看清? 怨可以产生愿,妄...
    狮子与熊阅读 240评论 0 1
  • 在当今的中国,高速发展的互联网,点燃了一代人的青春和激情,如果给这些人一个统一的名字那就是【创业】 每一代都有传奇...
    霄雲说自媒体阅读 1,036评论 0 1
  • 萌萌鼠儿阅读 188评论 0 0