- 教程
- 文档
- 示例工程
【免费下载链接】website-archive
Archive of the Coding Train website (first version)
本文以 Coding Train 的 Coding Challenge #151 为核心,讲解如何在浏览器中仅用 JavaScript 实现一个实时尤克里里调音器:通过 p5.js 采集麦克风音频、ml5.js 加载预训练的 CREPE 音高检测模型识别当前频率,再与尤克里里标准调音(G-C-E-A)比对并可视化调音偏差。读完本文,你将掌握 ml5.jspitchDetection()的完整调用链路、p5.sound 麦克风输入的标准流程,以及如何用最少的代码把"机器学习音高估计"落地为一个可交互的调音工具。
一、挑战背景:用预训练模型替代传统调音算法
在 Coding Challenge #151 中,作者 Daniel Shiffman 提出的目标是:仅用浏览器技术创建一个尤克里里调音器。传统调音器通常依赖自相关等信号处理算法估算基频,而这个挑战选择了另一条路径——直接使用**预训练的机器学习模型 CREPE(A Convolutional REpresentation for Pitch Estimation)**来估计音高。CREPE 是一种基于卷积神经网络的音高估计模型,它把音频波形映射为音高概率分布,从而输出连续频率估计;该模型的作者是 Jesse Engel 等人,Hannah Davis 是向 Coding Train 推荐该模型的关键人物(以上信息均可在本仓库的挑战文档元数据中确认)。
整个项目只有三个文件,全部位于本仓库的 CC_151_Ukulele_Tuner/P5 目录 下:
| 文件 | 职责 |
|---|---|
| index.html | 引入 p5.js、p5.sound、ml5.js 与样式表 |
| sketch.js | 麦克风采集、模型加载、音高识别与可视化全部逻辑 |
| style.css | 基础页面样式(清空 margin、画布块级显示) |
实现路径非常简洁:麦克风 → CREPE 模型 → 频率 → 最近音符 → 偏差可视化。这也正是本挑战文档所描述的核心流程:用 JavaScript 结合 p5.js、ml5.js 和预训练模型 CREPE 打造一个尤克里里调音器。
二、依赖引入:三个 CDN 一行搞定
index.html 只做了三件事:引入 p5.js 主库、引入 p5.sound 音频插件、引入 ml5.js:
<script src="https://cdn.jsdelivr.net/npm/p5@1.4.1/lib/p5.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/p5@1.4.1/lib/addons/p5.sound.min.js"></script> <script src="https://unpkg.com/ml5@0.3.1/dist/ml5.min.js"></script>其中:
- p5.js:负责画布创建(
createCanvas(400, 400))与全部可视化绘制; - p5.sound:提供
p5.AudioIn麦克风输入与getAudioContext()音频上下文访问,是获取实时音频流的入口; - ml5.js:提供
ml5.pitchDetection()高层 API,封装了 CREPE 模型的加载与推理。
页面主体只加载一个脚本 sketch.js,所有逻辑都在其中。需要留意的是,这里使用的 ml5 是 0.3.x 版本,其pitchDetectionAPI 的调用签名与新版(如 ml5 1.x 的ml5.pitchDetection(model, audioContext, stream, callback))有所不同,本文以下内容均以仓库内实际代码为准。
三、模型地址与尤克里里音符表
sketch.js 的开头定义了两个关键常量:模型 URL 与音符频率表。
const model_url = 'https://cdn.jsdelivr.net/gh/ml5js/ml5-data-and-models/models/pitch-detection/crepe/'; let pitch; let mic; let freq = 0; let threshold = 1; let notes = [ { note: 'A', freq: 440 }, { note: 'E', freq: 329.6276 }, { note: 'C', freq: 261.6256 }, { note: 'G', freq: 391.9954 } ];这里有两个值得注意的细节:
- 模型托管方式:CREPE 模型被托管在 ml5 官方数据仓库中,并通过 jsDelivr 的 "GitHub repo 转 CDN" 方式直接引用。这正是挑战文档中"如何把任意 GitHub 仓库变成 CDN"这条参考链接的用武之地——无需自行搭建模型托管服务器,静态权重文件即可通过 CDN 加速加载。
- 音符表对应尤克里里标准调音(从下往上数第 1 弦到第 4 弦):A4(440 Hz)、E4(329.6276 Hz)、C4(261.6256 Hz)、G4(391.9954 Hz)。注意弦序在表中是倒序的,A 弦是离调音者最近的 1 弦,而 G 弦是最远的 4 弦。这组频率是十二平均律下的标准值:C4 为 261.6256 Hz(中央 C),E4 为 329.6276 Hz,G4 为 391.9954 Hz,A4 为 440 Hz。
threshold = 1是调音"是否已准"的判定阈值(单位为 Hz):当检测频率与目标音符频率的绝对偏差小于 1 Hz 时,界面判定为已调准。
四、麦克风接入与模型初始化流程
setup() 完成了三件事:创建 400×400 画布、获取音频上下文、启动麦克风:
function setup() { createCanvas(400, 400); audioContext = getAudioContext(); mic = new p5.AudioIn(); mic.start(listening); }getAudioContext()是 p5.sound 暴露的全局方法,用于获取(或创建)Web Audio API 的AudioContext实例,后续喂给 ml5 的pitchDetection使用;new p5.AudioIn()创建麦克风输入对象,mic.start(listening)在获得用户授权后调用回调listening。由于浏览器自动播放策略,这个"授权 → 回调"的异步模式是 Web Audio 麦克风采集的标准写法。
listening() 是模型初始化的真正入口:
function listening() { console.log('listening'); pitch = ml5.pitchDetection(model_url, audioContext, mic.stream, modelLoaded); }这里展示了 ml5 0.3.x 中pitchDetection的完整签名:模型 URL、音频上下文、麦克风流(mic.stream)、模型加载完成回调。mic.stream是p5.AudioIn暴露的底层MediaStream对象,ml5 会把它作为 CREPE 模型的实时输入。模型加载完成后,回调modelLoaded()被触发。
五、递归式音高获取循环
modelLoaded() 与 gotPitch() 构成一个持续采样循环:
function modelLoaded() { console.log('model loaded'); pitch.getPitch(gotPitch); } function gotPitch(error, frequency) { if (error) { console.error(error); } else { if (frequency) { freq = frequency; } pitch.getPitch(gotPitch); } }要点解析:
pitch.getPitch(callback)是异步的:CREPE 模型在后台对当前音频帧执行推理,完成后通过回调返回结果;- 回调签名为
(error, frequency),frequency是模型估计出的基频(单位 Hz); - 拿到频率后立即再次调用
pitch.getPitch(gotPitch),形成递归采样链,保证每一帧都持续更新全局变量freq,供draw()读取。
这种"回调里再发起下一次请求"的模式,是 ml5 音高检测 API 的典型用法,避免了传统setInterval轮询的不确定性。freq = 0的初始值会在模型尚未输出结果时保持,界面会显示 0.00 Hz。
六、实时可视化:频率显示、最近音符与调音指示器
draw() 承担全部界面绘制,分为三块:
1. 当前频率文本
text(freq.toFixed(2), width / 2, height - 150);在画布下方居中显示检测到的频率,保留两位小数。toFixed(2)让数值稳定在两位精度,方便调音时读数。
2. 最近音符匹配
let closestNote = -1; let recordDiff = Infinity; for (let i = 0; i < notes.length; i++) { let diff = freq - notes[i].freq; if (abs(diff) < abs(recordDiff)) { closestNote = notes[i]; recordDiff = diff; } } text(closestNote.note, width / 2, height - 50);遍历四根弦的目标频率,用线性搜索找出绝对偏差最小的音符,并记录带符号偏差diff = freq - target。diff的符号很有用:正值表示当前音偏高,负值表示偏低。这也是后续可视化指针位置的计算基础。
3. 偏差指示器
这是整个界面的核心交互元素,由三部分组成(sketch.js):
let alpha = map(abs(diff), 0, 100, 255, 0); rectMode(CENTER); fill(255, alpha); stroke(255); strokeWeight(1); if (abs(diff) < threshold) { fill(0, 255, 0); } rect(200, 100, 200, 50); // 中央刻度条 stroke(255); strokeWeight(4); line(200, 0, 200, 200); // 中间基准线 noStroke(); fill(255, 0, 0); if (abs(diff) < threshold) { fill(0, 255, 0); } rect(200 + diff / 2, 100, 10, 75); // 游标指针设计意图非常清晰:
- 中央横向刻度条:宽度 200 像素,透明度
alpha由abs(diff)经map(0→100, 255→0)映射——偏差越大越亮,偏差越小越透明,给用户一个"接近目标"的视觉反馈; - 中间基准线:代表目标频率位置(即偏差为 0 处);
- 红色游标指针:横向位置由
200 + diff / 2决定,即每 1 Hz 偏差移动 0.5 像素,偏差 +100 Hz 时移到最右侧、-100 Hz 时移到最左侧,直观显示"偏高/偏低"; - 调准判定:当
abs(diff) < threshold(即偏差小于 1 Hz)时,游标与刻度条从红/白变为绿色,表示该弦已调准。
代码中被注释掉的lerpColor/color(255,0,0)/color(0,255,0)部分是作者最初设想的"绿→红颜色渐变"方案,最终被简化为透明度映射 + 绿色命中态,读者可以作为扩展灵感保留。
七、整体运行流程与验证方式
完整的调用链可以概括为:
setup() 创建画布 + 启动麦克风 → listening() 用 mic.stream 初始化 ml5.pitchDetection → modelLoaded() 首次发起 getPitch → gotPitch() 更新 freq 并递归发起下一次 getPitch(持续循环) → draw() 每帧读取 freq,匹配最近音符并绘制调音指示器运行与验证方式:直接在浏览器打开 index.html(或部署到任意静态服务器 / p5.js Web Editor),首次访问时浏览器会请求麦克风权限,授权后即可对着麦克风弹奏尤克里里。没有乐器时,可以用挑战文档中提到的"在线音调发生器"(Online Tone Generator)播放对应频率的单音来验证:播放 440 Hz 时界面应显示 A 且游标停在正中变绿,播放 445 Hz 时游标右移且显示 A(偏高 5 Hz),播放 330 Hz 附近则切换到 E 弦。观察 sketch.js 中freq的变化,即可确认模型推理链路工作正常。
需要说明的限制:CREPE 模型对乐器泛音丰富的音色鲁棒性较好,但对环境噪声较敏感;threshold = 1(1 Hz)的判定精度已高于人耳分辨能力,实际调音时通常已足够。
八、社区贡献:同一模型的多种创意实现
挑战文档在contributions一节收录了大量社区二次开发,这些作品共享"p5.js + ml5.js + CREPE"的技术内核,但在交互与界面设计上各有侧重,可作为继续探索的参考:
- Luciano Tonet的 "Afinador":把调音器扩展为吉他/贝斯/任意乐器的通用调音 Web 应用;
- MineBill的 Ukulele/Guitar Tuner:覆盖尤克里里与吉他的双乐器调音;
- originalang的 "Ukulele Tuner with Pitch Trail":在调音基础上增加了音高轨迹可视化;
- David Snyder的同模型新设计版本:换皮不换芯,演示了同一模型在界面层可以做多大变化;
- Simon Tiger Houben的一系列变体:包括"同时调所有弦"、"点击切换下一根弦"、甚至恶搞的"随机数发生器"版本,展示了把同一核心逻辑重构为不同交互形态的思路;
- Lokesh Kumar的版本:改进了 UI 并改变了从模型获取频率的方式;
- Joseph Azar的 "A pitch detection Game":把音高检测做成了"录下旋律、用钢琴回放"的音乐游戏,把调音器技术延伸到作曲与游戏领域。
这些贡献印证了挑战文档末尾的提问——"你能做出一个界面更优雅、更具创意的版本吗?"——也说明了pitchDetectionAPI 的可复用性:一旦拿到稳定的实时频率流,上层交互就拥有无限的创作空间。
九、相关参考与延伸学习
- ml5.js:挑战使用的机器学习友好库,本文所用的
pitchDetection即其高层 API; - CREPE 模型:挑战所依赖的卷积音高估计模型,其名字 "Convolutional REpresentation for Pitch Estimation" 与本仓库中 model_url 指向的托管路径一一对应;
- Hannah Davis:向 Coding Train 推荐 CREPE 的艺术家/研究者,其跨学科创作是本文思路的重要来源;
- jsDelivr + GitHub 转 CDN 方案:本挑战用该方式直接托管模型权重,避免自建模型服务器;
- 在线音调发生器(Online Tone Generator):文档建议的验证工具,可生成指定频率单音来测试调音器的识别准确性。
如需更系统地理解本项目所依赖的机器学习工具链,可以在仓库的 _learning/ml5 系列文档中继续学习 ml5.js 的图像分类、特征提取、模型训练等知识,这些内容与本挑战共享同一套"预训练模型 + 浏览器推理"的架构思路。
- 教程
- 文档
- 示例工程
【免费下载链接】website-archive
Archive of the Coding Train website (first version)
相关推荐
ml5.js 音高检测(PitchDetection)指南:基于 CREPE 模型实时估计音频基频
ml5.js 音高检测(PitchDetection)指南:基于 CREPE 模型实时估计音频基频 本文围绕 ml5.js 的 ml5.pitchDetecti
人工智能机器学习深度学习计算机视觉NLPAll-in-RAG扩展实战:如何用Jina v5-omni多模态Omni Embedding让文本、图像、音频、视频统一向量(完整指南)
All in RAG扩展实战:如何用Jina v5 omni多模态Omni Embedding让文本、图像、音频、视频统一向量(完整指南) Datawhale
教程文档示例工程ml5.js 图像分类实战:用 MobileNet 预训练模型在 p5.js 中识别图片内容
ml5.js 图像分类实战:用 MobileNet 预训练模型在 p5.js 中识别图片内容 本文围绕 Coding Train 的 ml5.js 初学者指南系
教程文档示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考