用华为云码道 CodeArts 代码智能体做的的码道调音器-不跟手机比,跟 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 为基准,偏多少音分一目了然:
exportfunctionfreqToNote(f){if(!f||f<=0||!Number.isFinite(f))return{name:'-',midi:0,cents:0};constn=69+12*Math.log2(f/440);constmidi=Math.round(n);constcents=(n-midi)*100;constname=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,专门治这个八度错误。
// NSDF: d(τ) = 2·r(τ) / m(τ),m 含 (N−τ) 无偏修正// CIP: 过阈峰里取 clarity 最大;clarity ≥ 0.9·max 的峰里取 lag 最小(消八度)exportfunctiondetectPitch(buffer,sampleRate,options={}){/* ... */}零拍法是个很「物理」的锚:
exportfunctionbeatCalibrate(f,fref){constbeatHz=Math.abs(f-fref);// 拍频,人耳能直接听出来constcents=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: 7d67796c48cc4468ac310ad95d81b824767aa01f7ecf33bcc03478a04228b0d4232 个用例,最大音分误差 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 专家评审团骂了三轮才想明白?评论区聊聊。