不跟手机比,跟 440Hz 比——零拍法验准的码道调音器

不跟手机比,跟 440Hz 比------零拍法验准的码道调音器

**一键开通华为云码道 CodeArts 代码智能体: **https://developer.huaweicloud.com/codeartsco.html?source=dmzntgwatomgit1&sourcead=dmzntgwatomgithd

作品介绍

一个纯前端调音器:对着麦克风吹、唱、弹一个音,它实时报出音名(A4)、偏离标准音的音分、还有一张 FFT 频谱。但它跟满大街调音 app 的区别在于------它敢把「自己测得准不准」摊在桌面上:拿 58 个键 × 4 种波形的合成音当已知真值去测,232 个用例最大误差 1.44 音分;再用 FFT 和 Goertzel 两条独立路子交叉验证,结果 232/232 全对上。46 项测试全绿,零第三方依赖。

一、为什么做「会验准的调音器」,而不是又一个测音 app

调音器这东西,手机上一搜一大把。但我越看越不踏实:它告诉我「你现在是 A4、偏了 3 音分」,可它凭什么说自己测得准?没有一个调音 app 会告诉你它的测频算法误差是多少、拿什么对标的。它就是个黑箱,你只能信。

这跟我上一篇「会算题的烟花」是同一个毛病------AI 生成的代码跑起来像模像样,但你不知道它对不对。所以我这次想做个能自证的调音器:不光测你的音,还能拿国际标准反过来验自己测得准不准。

说白了,别人是「跟手机里那个 app 比」,我这个是「跟 440Hz 这个物理事实比」。

二、先跑起来看

上面这张是它测一个标准 A4 时的样子:半圆弧表盘、指针基本居中、中间大字 A4、下面一个「准」字,底部 64 根 FFT 频谱条里 A4 那一根峰值柱转白,右下角读数 440.42 Hz。

没麦克风也能玩------内置一排校准音源(A4=440 等),点一下就合成一个纯正弦喂进同一套分析链。还有个「验证」抽屉,把测得音和标准音一起放,听它俩的拍频(嗡---嗡---嗡那个起伏),拍频越接近 0 说明越准。

三、提示词:把「可验证」钉死到函数级

我没跟码道说「做个调音器」。我一开始就把判据钉死:测频必须是纯函数、必须能用合成正弦(已知精确频率)当真值去验、必须能报音分误差。

复制代码
pitch.js: detectPitch(buffer, sampleRate) 纯函数,不读时钟不用随机。
scale.js: freqToNote(f) 用十二平均律 n=69+12·log2(f/440),返回 {name,midi,cents}。
groundtruth.js: 58 键平均律合成真值表,显式标注 A4=440 合成(非钢琴实测)。
测试:合成 440Hz 正弦喂 detectPitch,断言音分误差 < 5 cent。

「< 5 音分」这条是被逼出来的。第一版我写的是「误差 < 1%」,听着挺严,其实 1% ≈ 17 音分------人耳一听就知道跑调了,那还叫什么调音器。这是我拉专家评审时被技术专家当场点破的(第七节讲)。

四、架构:三锚护城河 + 单向分层

复制代码
src/
  pitch.js       McLeod NSDF/MPM + CIP 消倍频测基频(纯函数)
  scale.js       十二平均律 freqToNote / noteFreq
  groundtruth.js 58 键平均律合成真值表(A4=440)
  beat.js        零拍法:拍频 = |f − fref|,离线标定锚
  fft.js         radix-2 FFT(频谱显示 + 交叉验证)
  audio.js       麦克风 + 合成校准音源(无麦兜底)
  render.js      半圆弧表盘 + 音分刻度 + FFT 频谱条
  app.js         状态机 + 主循环 + 零拍验证抽屉

我管它叫「三锚」:① 合成音律真值表(已知频率,验检测器);② 零拍法(测得音和标准音的拍频,一个独立于算法的物理现象);③ FFT / Goertzel 交叉验证(换一条完全不同的数学路子,看是不是同一个频率)。三条路都指向同一个答案,才敢说「测得准」。

五、核心算法:怎么测、怎么不测错八度

音名映射是十二平均律,A4=440 为基准,偏多少音分一目了然:

js 复制代码
export function freqToNote(f) {
  if (!f || f <= 0 || !Number.isFinite(f)) return { name: '-', midi: 0, cents: 0 };
  const n = 69 + 12 * Math.log2(f / 440);
  const midi = Math.round(n);
  const cents = (n - midi) * 100;
  const name = NOTE_NAMES[((midi % 12) + 12) % 12] + (Math.floor(midi / 12) - 1);
  return { name, midi, cents };
}

测频这里有个大坑:普通自相关(ACF)会把 220Hz 的音报成 440Hz ------因为一个周期的波形和隔一个周期的波形长得一样,ACF 第一个峰经常落在倍频上。对调音器来说这是致命错误(你弹 A3 它报 A4)。所以我让码道改用 McLeod 归一化自相关函数(NSDF/MPM)+ CIP 判据:在所有过阈值的峰里选清晰度最大的、再在并列里选最短的 lag,专门治这个八度错误。

js 复制代码
// NSDF: d(τ) = 2·r(τ) / m(τ),m 含 (N−τ) 无偏修正
// CIP:  过阈峰里取 clarity 最大;clarity ≥ 0.9·max 的峰里取 lag 最小(消八度)
export function detectPitch(buffer, sampleRate, options = {}) { /* ... */ }

零拍法是个很「物理」的锚:

js 复制代码
export function beatCalibrate(f, fref) {
  const beatHz = Math.abs(f - fref);           // 拍频,人耳能直接听出来
  const cents = 1200 * Math.log2(f / fref);    // 对应音分
  return { beatHz, cents };
}

两个几乎同频的音一起放,会听到「嗡---嗡---」的拍,拍频就是它们的差。这是模拟的、独立的、连算法都不用的验证------虽然严格说它还是拿合成真值当基准,不是凭空多一个第三方,这点我在文章里也不吹。

六、可验证:232 个用例,最大 1.44 音分

这是整个项目我最满意的地方。evidence 脚本拿 58 个键(C2 到某个高音)× 4 种波形(纯正弦、含 2/3 次谐波、锯齿、类单簧管混合)合成已知频率,喂进测频器,对答案:

复制代码
=== Evidence Report ===
Keys: 58 × 4 wave types = 232 cases
Max cents error: 1.443
|cents|<5 pass: true
FFT consistent: 232/232 (100.0%) pass=true
Goertzel consistent: 232/232 (100.0%) pass=true
Deterministic: true
SHA-256: 7d67796c48cc4468ac310ad95d81b824767aa01f7ecf33bcc03478a04228b0d4

232 个用例,最大音分误差 1.44,全部 <5;FFT 和 Goertzel 两条独立路子 232/232 都和主测频对上;同参数跑两遍逐帧一致(确定性);带一个 SHA-256 指纹,参数一改指纹就变。关键是它不只测纯正弦------谐波、锯齿、类单簧管这些真实乐器波形都测,否则就是拿最简单的情形糊弄测试。

七、真实的坑:三轮专家评审把我骂醒了

这项目最值钱的不是代码,是我给它拉了个 UI / 技术 / 产品三个 AI 专家组成的评审团,连审三轮。第一轮打分惨不忍睹:技术 62、UI 18、产品 83。

技术专家一上来就给了我一闷棍:「你的 ACF 会把 220Hz 报成 440Hz,我实测复现了。」 这就是我上面说的八度错误,第一版根本没测、真犯了。它还点破:clarity 是伪置信(噪声下测偏 5.8% 却显示高置信)、1% 容差=17 音分根本不算准、测试只喂纯正弦是躲开真实失效模式。产品专家说你和烟花「同源」(都是对拍真值),得区分------烟花是「自己对自己」,调音器要「跟世界对答案」。UI 专家说渲染层压根还没做,18 分。

第二轮我按这些改(MPM+CIP、<5音分、多波形真值、三锚、零拍、半圆弧表盘),分数爬到 85/86/88。第三轮又逼出更细的:零拍法低频要长窗不能追实时、真钢琴有不谐波和拉伸调律(所以对标合成真值而非实测钢琴)、稳态门(颤音/起音那几帧别当真)。最后 88/90/88,一致「可发布」。

说实话,没有这三轮互骂,我大概率会带着「ACF 报倍频」这个致命 bug 就发出去了。AI 写代码快,但「它到底对不对」这种问题,得有人(或另一个 AI)专门来挑刺。

八、测试:46 项全绿

测频、音名映射、倍频回归、真值对标、零拍、FFT、坏输入不抛,46 项,每一轮我都 clone 到本地独立 node --test 复核。

九、提效数据

环节 纯手写估摸 码道实际
测频 DSP + 音律 + 46 测试 两三天 约 3 小时(五轮)
FFT + Goertzel + 零拍 + evidence 一天 一轮
半圆弧表盘 + 频谱 UI 一天 一轮

我逐字敲的代码不到一成,但真正的功夫在验收和那三轮评审------倍频 bug、容差口径、自证循环,全是评审揪出来我才改的。

十、还没做好的地方(不装)

  • 零拍法目前是离线标定锚,低频音分精度要秒级窗,没做到实时;

  • 真钢琴的不谐波和拉伸调律没建模,对标的是平均律合成真值,不是「上台给真钢琴调音」;

  • 颤音、起音瞬态的稳态门是粗判,复杂真实演奏下置信度还会抖。

十一、五维自检

架构:三锚 + 单向分层,测频/音律/真值/验证解耦;代码:46 测试 + 232 例最大 1.44 音分 + 确定性 + 指纹;安全:麦克风音频只在本地 AnalyserNode 分析、绝不上传、零联网零依赖;UI:克制深色 + 半圆弧表盘 + FFT 频谱 + 零拍抽屉;内容:真实截图 + 真实代码 + 真跑出来的误差 + 三轮评审的诚实复盘。

十二、本地怎么跑

复制代码
git clone https://atomgit.com/azhiqiu/tuner-pro.git
cd tuner-pro
node --test              # 46 项全绿
node tools/evidence.cjs  # 232 例测频对标真值 + 交叉验证 + 指纹
npm run serve            # 打开 http://localhost:8082

打开后建议亲手试三下:点「校准音源」放个 A4 看指针居中标「准」;开「验证」抽屉听零拍;对着麦克风唱个音看它报音名和音分。

写在最后

上一篇烟花我学会的是「让程序自己算答案再对答案」;这一篇我多学了一招------让另一群 AI 来挑你的刺。三轮 UI/技术/产品互评,把一个带着致命倍频 bug 的半成品,逼成了一个敢把误差摊在桌上的调音器。AI 负责快和像模像样,「对不对」这件事,永远得有个较真的人(或另一个 AI)盯着。

数据与致谢:音律基准 A4=440Hz、十二平均律;开发工具 华为云码道 CodeArts 代码智能体(AtomGit)。仓库 MIT 开源,地址就是上面 clone 命令那个。

你做工具类项目时,有没有想过「怎么证明它自己是对的」?还是跟我一样,被 AI 专家评审团骂了三轮才想明白?评论区聊聊。

相关推荐
LucianaiB6 天前
华为云码道 AI 编程实战:我用 CodeArts 智能体打造了一款 HarmonyOS 游戏化专注应用「智办 ZhiBan」
人工智能·ai·华为云·harmonyos·codearts·材料
故乡dee云6 天前
2026年华为云国际账号开通避坑:IP自动跳转中国站,选错地区只能重新注册
服务器·tcp/ip·华为云
渣哥搞AI6 天前
中秋猜灯谜:基于华为云码道零依赖纯 Canvas 手写一个灯会小游戏
华为云
闲云自留地13 天前
华为云计算云服务
华为云
云老大-阿里云国际站代理商15 天前
华为云国际站服务代理商:NAT网关怎么配?多台ECS共用公网IP要注意什么
服务器·华为云·云计算
闲云野鹤在人间16 天前
docker 入门 | 第5章 网络详解
网络·docker·架构·华为云·php
SatanII16 天前
容器运行时Containerd完整学习笔记|原理、安装、ctr/nerdctl/crictl实操全梳理
运维·docker·华为云·containerd
月落汀兰19 天前
从需求到上线:华为云搭建高可用 Web 站点,ECS/RDS/ELB/AS 组件协同实践
华为云·云计算
月落汀兰19 天前
云上故障怎么排查?华为云 IAM 权限、CES 监控、LTS 日志、CTS 审计完整指南
华为云·云计算