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_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_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_source=taotoken_aicg_blog_end&utm_content=console_key&utm_campaign=rewrite 。创建后不要写进前端代码,放到环境变量或本地.env,前端只读配置里的apiKeyEnv字段名。如果你需要模型对话来批量生成音名映射,可以用模型对话入口 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 先试跑一轮,确认输出格式再落到配置。
3. 可复制配置:settings.json 驱动音效触发链路
3.1 HTML 结构:保留多通道音频
原始 excerpt 用多个<video>标签,每个琴键组一个,避免尾音切断。这个思路保留,但改成由 JS 根据polyphony动态创建,不再手写一堆标签。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>js 钢琴按键音效验证</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 id="piano"></div> <div id="audioPool"></div> <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script> <script src="js/piano.js"></script> </body> </html>3.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_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 。如果你要长期做编码和 Agent 类任务,Coding Plan 入口是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite ,适合把这类校验脚本固化下来。
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_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。如果你需要创建新的 Key 来区分开发和生产环境,API Keys 管理入口是 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。Claude Code 相关的接入参考在 https://taotoken.net/claude-code?utm_source=taotoken_aicg_blog_end&utm_content=claude_code&utm_campaign=rewrite ,适合把音效包校验做成可复用的编码任务。
实测下来,把音效包路径和键盘映射从 JS 里抽到settings.json之后,换一套音效只需要改配置里的basePath,加八度只需要在keyboardMap里补几行。音频池轮询这个细节别省,它直接决定连按时的听感。最后提醒一句:音效包文件名统一用音名,别用1.mp3、2.mp3这种序号,否则配置和目录对不上时排查成本会翻倍。