吉他调音器 (2015)

Lobsters Hottest 工具

摘要

一个用于调吉他的Web应用,展示了现代网络技术如 Web Components、Service Worker 和 Polymer,以提高性能和提供离线支持。

<p><a href="https://guitar-tuner.appspot.com/" rel="ugc">https://guitar-tuner.appspot.com/</a></p> <p><a href="https://lobste.rs/s/twaqbs/guitar_tuner_2015">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/08/22 17:10

# 吉他调音器 来源:https://aerotwist.com/blog/guitar-tuner/ 参阅 吉他调音器 ## 概述与亮点 吉他调音器,尽管名字晦涩,实际上是一款帮助你调校吉他的网页应用。我相信你和我一样,对此发现感到震惊。以下是它的一些特性: - **Web 组件**。这是实践 Web 组件的绝佳时机。在这里,我考虑了三个组件:一个处理音频输入和分析;一个负责表盘显示;一个提供调音指示(调高、调低等)。 - **用于离线访问的 Service Worker**。当然,何乐而不为?它本质上是一个单页应用,这意味着添加 Service Worker 支持应该非常简单。况且离线支持可是赢家专属的运动。 - **Web Manifest**。以防万一有人想将应用添加到主屏幕,提供漂亮的图标、简称,并设置一些应用行为偏好似乎是不错的选择。Manifest 万岁! - **ES6 类、胖箭头函数和 Promises**。我最近尝试了这些特性,然后就上瘾了。除非你把我拖走,否则我不会回到 ES5,所以它们也被用在了这里。而且这次我还把它们与 Polymer / Web 组件部分结合使用了,这很有趣。 - **开源**。你可以获取代码 (https://github.com/GoogleChrome/guitar-tuner) 并四处查看!如果你不是吉他手,或者手边没有吉他,你总是可以观看下面的演示视频。不幸的是,视频里*确实*有我弹吉他的画面,对此我只能道歉,但希望至少能因尝试而获得加分。 ## 大大的注意事项 没有什么比在你构建的东西前面加上一堆“万一不能用”的免责声明更合适的了。所以请谨记…… - **我假设使用的是标准调音的 6 弦吉他**。至少我手边没有 12 弦吉他,通常它们也少见得多,在调音器方面,我可是个不折不扣的“多数派迎合者”。如果你想提交补丁 (https://github.com/GoogleChrome/guitar-tuner/),欢迎随时进行。它可能已经能工作了,只是我不确定。 - **调音通过设备麦克风进行**。它可能(嗯,是肯定)不如使用吉他琴颈振动提供频率信息的半音阶调音器准确。但在紧要关头可能会很方便。 - **不支持移动版 Safari,也不支持 Internet Explorer**。这是因为它们不支持 `getUserMedia`,我没什么办法绕过这个问题。如果浏览器无法聆听,它就无法帮助我调音。不过,Edge 将会支持 getUserMedia (http://caniuse.com/#search=getusermedia),这是个好消息! 好了,注意事项说完了,让我们来聊聊细节! ## Polymer 我最近写了一篇关于如何使用 Polymer 进行懒加载和渐进增强页面的文章 (https://aerotwist.com/blog/polymer-for-the-performance-obsessed/)。吉他调音器使用了完全相同的方法(因为我在构建应用时就想通了),这意味着我拥有了 Web 组件的精华,*但是*应用加载应该会非常快。事实上,在 WebPageTest 上,有线网络下的 Speed Index (https://sites.google.com/a/webpagetest.org/docs/using-webpagetest/metrics/speed-index) 大约是 **~450**,在 3G 网络下大约是 **~2800**,这让我非常满意。请注意,它*是*一个小型应用。整个应用(包括 Polymer,但不包括 12KB 的 Web 组件 polyfill)总共只有 40.1KB,如果加载缓慢,我会觉得相当沮丧。如果你想了解超级详细的内容,可以阅读另一篇文章 (https://aerotwist.com/blog/polymer-for-the-performance-obsessed/),但简单来说,我分别加载了三个 web 组件,每个组件加载完成时都会升级它管理的元素。为了防止 FOUC (http://en.wikipedia.org/wiki/Flash_of_unstyled_content),我在应用的 `index.html` 头部内联了一些样式,使其看起来像这样:占位符样式使应用看起来是这样的。它们被内联到页面中,并在元素升级时移除。> 当每个元素升级时,它会移除占位符样式。这意味着应用可以更快地达到“视觉上完整”的状态,而不是等待所有元素升级后再显示。当每个元素升级时,它会移除占位符样式。这意味着应用可以更快地达到“视觉上完整”的状态,而不是等待所有元素升级后再显示。所有元素都竞相加载 Polymer,并且由于 HTML Imports 的工作方式以及 Polymer 总是以相同的 URL 被请求,我们只请求它一次。一旦加载完成,所有三个组件都能使用它。 ## Web 组件 该应用包含三个 Web 组件: - **`<audio-analyzer>`**。负责通过 `getUserMedia` 请求麦克风权限,如果出现错误会弹出提示。它还使用了 Page Visibility API (https://developer.mozilla.org/en-US/docs/Web/Guide/User_experience/Using_the_Page_Visibility_API) 来切换麦克风访问权限,如果你隐藏应用,麦克风访问就会禁用,切换回应用时重新启用。它还会分析音频中的主要频率,并分发包含该频率值、八度音阶和最近音符的事件。 - **`<tuner-dial>`**。这是一个基于 Canvas 的元素,绘制一个表盘来指示当前的音调。它接收来自 `<audio-analyzer>` 的事件,并更新表盘、音符和八度音阶信息。 - **`<tuner-instructions>`**。这也接收来自 `<audio-analyzer>` 的事件。它利用这些事件来判断你最接近哪根弦,然后建议你目标频率以及应该调高还是调低。 我认为 Web 组件真正美妙之处在于它鼓励健康的代码解耦。当然,即使不创建组件也可以实现,但我发现时不时地被“推动”一下很有帮助!现在我当然可以把逻辑打包好,这样如果我需要更多的音频处理,就有一个现成的元素可以使用。 > 关于类似 `<audio-analyzer>` 的元素是否应该成为一个组件,我确实犹豫了一会儿(嗯...啊...)。一方面,它在 DOM 中并不能提供多少语义价值;另一方面,它可以分发事件,这非常方便。显然,你从我最终创建了 `<audio-analyzer>` 这个元素就能看出我倾向于哪一边,但我不会责怪任何人做出相反的选择。 ## ES6 类 + Polymer 显然,关于 Web 组件的理论是,你最终可以用 ES6 类来做到类似这样的事情: ```js class MyRadElement extends HTMLElement { // 哇,这个类会非常棒... 等等,不对。 // 应该是“amaze”。我跟上了潮流。 } ``` 但据我所知,这种特定语法仍在讨论中。我也在使用 Polymer,所以我想,也许...也许不是给 Polymer 一个对象,而是给它一个类的原型? ```js class MyRadElement { constructor () { Polymer(MyRadElement.prototype); } get is () { return 'my-rad-element'; } } ``` 你不能把类本身(或实例)传递给 Polymer,因为如果没有语法糖,类就是一个函数,而 Polymer 调用的 Web 组件 `registerElement` 函数期望第二个参数是一个对象,而不是函数。它还期望第一个参数是标签名,所以我用了一个 `is` 的 getter,因为它出现在原型上。我想我也可以用 `this.constructor.prototype.is = 'my-rad-element'`,但 getter 看起来更整洁。这种方法的另一个副作用是你无法在任何地方使用类的实例,所以任何你本想在 `constructor` 中做的事情,现在需要在 `created` 和 `attached` 回调中完成,这有点限制,但也不是什么大事。我想这就是使用类/函数而不是对象的本质。 > 这一切都不是严格必须的,甚至完全不是;给 Polymer 一个对象没有任何问题。但*我*喜欢 ES6 类(有争议,我知道),如果我在 ES6 的世界里,或者想用它,为什么不试试让它完美运行呢?对吗?赢家。 ## 错误的音频分析方法 有了组件,让我们来谈谈音频分析,因为我原以为这部分会相对容易。我错了。大错特错。本质上我是个小丑,仍然没学会好好评估工作量。但让我看看能否为下一个尝试做类似事情的倒霉蛋简化一下。 首先让我告诉你关于失败的事。不是真正的失败,而是爱迪生式的失败: > “我没有失败。我只是发现了 10000 种行不通的方法。” 第一次尝试:**快速傅里叶变换**,或 FFT。如果你不熟悉,它们的作用是给你当前音频的频率桶分解。Web Audio API 可以让你访问这些数据,例如在 `requestAnimationFrame` 中通过 `AnalyserNode`,并调用 `getFloatFrequencyData`。某些音频的 FFT。我假设我会寻找峰值,而峰值会告诉我我正在弹奏哪个音符。我原以为如果我对音频进行 FFT,我就能遍历它,找到最活跃的频率。然后根据频率确定它可能是哪根弦,然后相应地提供“调高”、“调低”或“音准”的信息。 然后性能问题出现了。还有谐波。主要是谐波。 ### 性能 > 最终这种方法导致帧率在 30 到 60 fps 之间剧烈波动,并且只能被它的朋友们称为“CPU 熔化机”。为了在频率上获得足够的分辨率,这种方法需要一个巨大的 FFT。使用 32K(最大可能值)的 FFT,数组中的每个桶代表的频率范围略低于 3Hz。在一台状态良好的 Nexus 5 上填充这么大的数组大约需要 11ms。如果你试图在 `requestAnimationFrame` 回调中完成这个操作,你将会度过一段糟糕的时光。更糟糕的是,你还需要在获取音频数据*后处理它*。为了达到 60fps,你最多只有大约 8-10ms 的 JavaScript 执行时间。浏览器还有自己的管理任务要做,所以你必须共享 CPU 时间。最终这种方法导致帧率在 30 到 60 fps 之间剧烈波动,并且只能被它的朋友们称为“CPU 熔化机”。 ### 谐波 然后是谐波。一个 B3 音符的频率是 493.883Hz,所以人们可能会合理地期待一个像上面那样的 FFT,但在 ~493Hz 处有一个峰值。实际上,当你拨动 B3 弦时,频率看起来是这样的:拨动 B3 弦时的 FFT。谐波会在意想不到的地方给你峰值,就是为了戏弄你。看到到处都是峰值了吗?每根弦都带来自己特有的频率组合,称为谐波。有一点是肯定的:这不是一个“纯净”的样本,你不能仅从最活跃的频率就推断出你弹的是哪根弦。 我有时理解力有点差,所以我尝试用一些老派的数字猜测和凑合来解决这个问题。在特定情况下它有点用,但确实不够稳健。 ## 更好的音频分析方法 然后 Chris Wilson (https://twitter.com/cwilso) 帮助了我。背景是,我到了我那套“猜测凑合”方法的尽头,开始谷歌搜索类似“拜托我是小丑怎么做好简单的音高检测?”的问题。正如你可能预料的,排名靠前的结果是维基百科文章 (https://en.wikipedia.org/wiki/Pitch_detection_algorithm),它们读起来就像用古埃及象形文字写的一样,毫无意义。它们显然由那些*已经*理解这些主题的人撰写,其唯一目标似乎是确保你不会理解。几年前我制作 3D 引擎时也有过同样的遭遇,就像那段生活时期一样,我全身心地渴望简单、像对待人类一样的解释。谢天谢地,这正是 Chris 在几个小时里提供的。 ### 自相关 第二次尝试:**自相关**。公平地说,自相关在我的“象形文字研究”中出现过,但它毫无意义。但 Chris 建议了它,我尝试了一下。回想起来,我猜名字就是一个线索:**auto-**(自)和 **correlation**(相关)。想法是,如果你有一个音频波形,你可以将它与自身在不同偏移量处进行比较。如果你找到一个匹配,那么你就找到了这个波形重复的位置,即使考虑了谐波(稍后详谈)。一旦你知道波形何时重复,理论上你就找到了它的频率。自相关就是尝试将波形与自身匹配的过程。你需要移动的量给出了它的周期性,从而给出了音高。你可以从 Web Audio API(当然可以,多么美妙的 API)通过 `getFloatTimeDomainData` 获取波形数据,这个函数几乎没有文档,而且听起来像是一个以流行词命名的函数。但它确实完成了我们需要的工作:**它用浮点波形数据填充一个数组,值的范围在 -1 到 1 之间**。`AnalyserNode` 上的 `fftSize` 属性决定了你获得的数据量。如果你将 `fftSize` 设置为 48,000(你不能这么做,因为最大限制是 32K 且必须是 2 的幂,但请继续听我说),并且采样率为 48kHz,你将获得一秒钟的波形音频数据。事实上,我将我的 `fftSize` 设置为 4,096,这给了我 4,096 / 48,000 ~= 85ms 的波形数据。因为我计划将波形与自身比较,我*一半*的数据,大约 42ms 的音频数据,可供我每次比较使用。我第一次尝试自相关时,比较了波形在所有偏移量(缓冲区的一半,或 2,048 个元素)上的情况,然后返回提供最接近匹配的偏移量: ```js let buffer = new Float32Array(4096) let halfBufferLength = Math.floor(buffer.length * 0.5); let difference = 0; let smallestDifference = Math.POSITIVE_INFINITY; let smallestDifferenceOffset = 0; // 填充波形数据。 analyserNode.getFloatTimeDomainData(buffer); // 从偏移量1开始。没必要在偏移量0处将波形与自身比较。 for (let o = 1; o < halfBufferLength; o++) { difference = 0; for (let i = 0; i < halfBufferLength; i++) { // 对于这次迭代,计算波形与 // 偏移版本之间的差异。 difference += Math.abs(buffer[i] - buffer[i + o]); } // 取平均值。 difference /= halfBufferLength; // 如果这是目前最小的差异,就记住它。 if (difference < smallestDifference) { smallestDifference = difference; smallestDifferenceOffset = o; } } // 现在我们知道了哪个偏移量产生了最小的差异, // 我们可以将它转换为频率。 return audioContext.sampleRate / smallestDifferenceOffset; ``` 理想情况下,人们会在这里进行一些曲线拟合,以确定波形*究竟*在哪里重复,但我搞砸了……

相似文章

Tweaklify

Product Hunt

Tweaklify 是一款用于快速 DOM 原型制作和实时样式编辑的工具。

Chromtuner

Product Hunt

Chromtuner 是一款适用于 macOS 的半音调谐器,精度为 ±1 音分。

渐进式 Web Components

Hacker News Top

Ariel Salminen 介绍了 Elena,一个 2.6kB 的开源库,用于构建渐进式 Web Components,这些组件无需 JavaScript 即可渲染 HTML 和 CSS,并逐步增强。文章解释了该方法背后的设计理念和分类法。

Show HN: YouTube吉他谱解析器

Hacker News Top

一个CLI工具,可下载YouTube吉他教学视频,使用Claude视觉模型检测并裁剪谱表区域,按小节编号去重帧,最后拼接成PDF。需要Node.js、yt-dlp、ffmpeg和Anthropic API密钥。