基于 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)'

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

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