1. 从一次「按键没声音」的调试说起做纯前端 js 钢琴项目时最容易卡住的不是画键盘而是按键音效包接不进去。我见过太多人把 HTML 键盘画得漂漂亮亮鼠标点击也有反应但一按键盘就是静音控制台还干干净净什么都不报。这个场景的核心检索词就是js 钢琴、按键音效包、settings.json 配置、按键音效验证。它适合谁适合已经能写出琴键布局、想让每个琴键真正发声的前端开发者也适合想把音效触发链路一次跑通、不想反复改代码的人。传统做法是每个琴键配一个video或audio标签点击时改src。这个思路本身没错问题出在音效包路径散落在 JS 里换一套音效就要全局搜索替换键盘映射也硬编码在switch里加一个八度就得复制一堆case。更麻烦的是当你想把「音效包从哪来、用哪个模型辅助生成映射、Key 怎么统一管理」这几件事串起来时代码里没有一处集中配置。这篇要做的是把音效包接入这件事从「散落代码」变成「一份 settings.json 驱动」。同时用 TaoToken 统一 Key 的方式把音效包元数据、键盘映射、模型辅助校验这几块收口到一个配置里。目标很明确一次配置浏览器里每个琴键按下都能听到对应音效键盘和鼠标两条触发链路都通。2. TaoToken 前置统一 Key 与音效包元数据2.1 为什么音效项目也需要统一 Key你可能会问一个纯前端钢琴项目音效包就是本地 mp3跟 Key 有什么关系。实际开发里音效包往往不是一次性凑齐的有的来自公开音效库有的需要模型帮你把「C4、D4、E4」这种音名批量转成文件名映射有的需要校验音效包目录里到底缺了哪个音。这些环节如果各自去申请、各自去配 Key项目里就会散落多个密钥换环境时非常痛苦。TaoToken 在这里的角色是统一入口一个 Key 覆盖模型对话、编码辅助、密钥管理。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 。注意 API 地址不带 UTM 参数配置时直接写这个。2.2 音效包目录结构与 settings.json 骨架先定目录。音效包放在项目根目录的min/下文件名就是音名比如C4.mp3、D4.mp3。这样 JS 里拼接路径时只需要min/ 音名 .mp3跟原始 excerpt 里的思路一致但路径来源改成配置读取。piano-js/ ├── index.html ├── settings.json ├── min/ │ ├── C4.mp3 │ ├── D4.mp3 │ ├── E4.mp3 │ └── ... └── js/ └── piano.jssettings.json骨架如下把音效包路径、键盘映射、TaoToken 接入信息集中管理{ audio: { basePath: min/, extension: .mp3, polyphony: 7 }, keyboardMap: { q: C4, w: D4, e: E4, r: F4, t: G4, y: A4, u: B4, i: C5, d: D4, f: E4, g: F4, h: G4, j: A4, k: B4, l: C5 }, taotoken: { apiBase: https://taotoken.net/api, model: claude-sonnet, apiKeyEnv: TAOTOKEN_API_KEY } }这里polyphony对应原始 excerpt 里「每个 ui 配一个 video」的思路7 个音频通道避免尾音被切断。keyboardMap把原来switch里的硬编码搬出来加八度只改配置。taotoken段用于后续模型辅助校验音效包完整性。2.3 获取并配置 Key进入控制台创建 API Key地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsole_keyutm_campaignrewrite 。创建后不要写进前端代码放到环境变量或本地.env前端只读配置里的apiKeyEnv字段名。如果你需要模型对话来批量生成音名映射可以用模型对话入口 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 先试跑一轮确认输出格式再落到配置。3. 可复制配置settings.json 驱动音效触发链路3.1 HTML 结构保留多通道音频原始 excerpt 用多个video标签每个琴键组一个避免尾音切断。这个思路保留但改成由 JS 根据polyphony动态创建不再手写一堆标签。!DOCTYPE html html langzh-CN head meta charsetUTF-8 titlejs 钢琴按键音效验证/title style div, ul, li { margin: 0; padding: 0; } ul, li { list-style: none; cursor: pointer; } #piano { width: 1000px; overflow: hidden; margin: auto; } #piano ul { width: 14.28%; overflow: hidden; float: left; } #piano ul li { width: 100%; height: 40px; text-align: center; line-height: 40px; border: 1px solid skyblue; } #piano ul li.active { background: #d0f0ff; } /style /head body div idpiano/div div idaudioPool/div script srchttps://code.jquery.com/jquery-3.7.1.min.js/script script srcjs/piano.js/script /body /html3.2 JS读取 settings.json 并绑定事件核心逻辑分三步加载配置、渲染键盘、绑定鼠标与键盘事件。音频池按polyphony创建每个通道独立src这样连按多个键不会互相切断。// js/piano.js $(async function () { const settings await fetch(settings.json).then(r r.json()); const { basePath, extension, polyphony } settings.audio; const keyboardMap settings.keyboardMap; // 1. 创建音频池 const audioPool []; for (let i 0; i polyphony; i) { const audio document.createElement(audio); audio.preload auto; document.getElementById(audioPool).appendChild(audio); audioPool.push(audio); } // 2. 渲染键盘按音名分组每组一个 ul const notes [C, D, E, F, G, A, B]; const piano document.getElementById(piano); notes.forEach(note { const ul document.createElement(ul); for (let octave 4; octave 5; octave) { const li document.createElement(li); li.textContent note octave; li.dataset.note note octave; ul.appendChild(li); } piano.appendChild(ul); }); // 3. 播放函数轮询音频池避免尾音切断 let poolIndex 0; function playNote(note) { const audio audioPool[poolIndex]; poolIndex (poolIndex 1) % audioPool.length; audio.src basePath note extension; audio.currentTime 0; audio.play().catch(err console.warn(播放失败, note, err)); } // 4. 鼠标点击 $(#piano).on(click, li, function () { const note $(this).data(note); playNote(note); $(this).addClass(active); setTimeout(() $(this).removeClass(active), 150); }); // 5. 键盘事件从配置读取映射 $(window).on(keydown, function (e) { const key e.key.toLowerCase(); const note keyboardMap[key]; if (!note) return; playNote(note); const $li $(#piano li[data-note${note}]); $li.addClass(active); setTimeout(() $li.removeClass(active), 150); }); });3.3 用模型辅助校验音效包完整性音效包最容易出的问题是「配置里有 C5但 min 目录里没有 C5.mp3」。可以用 TaoToken 的模型对话能力把目录清单和配置映射丢进去让它列出缺失项。模型对话入口是 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 。如果你要长期做编码和 Agent 类任务Coding Plan 入口是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 适合把这类校验脚本固化下来。4. 验证请求按键到音效播放的完整链路4.1 本地起服务并打开页面fetch(settings.json)在file://协议下会被浏览器拦截必须用本地服务。任选一种# 方式一Python 自带 python3 -m http.server 8080 # 方式二Node 环境 npx serve -l 8080浏览器打开http://localhost:8080按 F12 打开控制台。4.2 验证鼠标点击链路点击任意琴键比如C4。预期结果控制台无报错Network 面板出现min/C4.mp3请求状态 200页面能听到对应音效。如果请求 404说明音效包文件名或路径不对检查settings.json里的basePath和extension。4.3 验证键盘触发链路按下键盘q预期触发C4音效同时对应琴键高亮 150ms。按下w触发D4以此类推。如果按键无反应先在控制台执行console.log(keyboardMap)确认配置已加载再检查keydown是否被其他脚本阻止。4.4 验证多通道不切断尾音快速连按q、w、e听是否有尾音被切断。因为音频池有 7 个通道轮询正常情况下每个音都能完整播放。如果仍然切断把polyphony调大比如改成 14。5. 本篇常见错排查5.1 报错Failed to fetch settings.json原因用了file://直接打开 HTML。解决必须通过http://localhost访问。这是纯前端项目最常踩的坑跟音效包本身无关。5.2 音效 404但文件名看起来没错检查三点basePath末尾有没有斜杠extension有没有点音名大小写是否一致。Linux 服务器区分大小写c4.mp3和C4.mp3是两个文件。用ls min/ | head确认实际文件名。5.3 键盘按下没声音鼠标点击正常说明音频池和播放函数没问题问题在keyboardMap。检查e.key.toLowerCase()拿到的值是否在映射里。注意keypress已废弃本篇用keydown。如果用了输入法keydown可能拿到Process需要判断e.isComposing。5.4 连按同一个键第二次没声音因为audio.currentTime 0后立即play()部分浏览器需要重新加载。解决在playNote里先audio.load()再play()或者确保音频池轮询生效不要复用同一个通道。5.5 TaoToken 请求 401检查 API Key 是否放对环境变量请求头是否为Authorization: Bearer key。API 地址用 https://taotoken.net/api 不要带 UTM 参数。如果要在前端直接调注意不要把 Key 暴露在客户端代码里建议通过本地代理转发。6. 接入文档与后续动作音效包接入跑通后下一步通常是把校验脚本固化、把键盘映射扩展到更多八度、把音效包替换成更高质量的音源。这些动作都涉及 Key 的统一管理和接入细节建议直接看接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。如果你需要创建新的 Key 来区分开发和生产环境API Keys 管理入口是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。Claude Code 相关的接入参考在 https://taotoken.net/claude-code?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_codeutm_campaignrewrite 适合把音效包校验做成可复用的编码任务。实测下来把音效包路径和键盘映射从 JS 里抽到settings.json之后换一套音效只需要改配置里的basePath加八度只需要在keyboardMap里补几行。音频池轮询这个细节别省它直接决定连按时的听感。最后提醒一句音效包文件名统一用音名别用1.mp3、2.mp3这种序号否则配置和目录对不上时排查成本会翻倍。