[{"data":1,"prerenderedAt":452},["ShallowReactive",2],{"blog-web-piano":3},{"id":4,"title":5,"body":6,"category":437,"date":438,"description":439,"draft":440,"extension":441,"meta":442,"navigation":443,"path":444,"seo":445,"stem":446,"tags":447,"__hash__":451},"blog\u002Fblog\u002Fweb-piano.md","网页钢琴与 Web Audio 合成",{"type":7,"value":8,"toc":427},"minimark",[9,13,36,40,45,57,262,266,277,285,304,308,319,323,388,395,398,410,413,416,423],[10,11,5],"h1",{"id":12},"网页钢琴与-web-audio-合成",[14,15,16,17,21,22,27,28,31,32,35],"p",{},"Web Audio 不只能分析已有音频——用 ",[18,19,20],"strong",{},"振荡器（Oscillator）"," 也能从零合成声音。下面 demo 实现了一个 3 个八度的网页钢琴：按住琴键持续发声，松手渐弱；空格键控制延音踏板；还可点击内置乐谱自动演奏。若你读过 ",[23,24,26],"a",{"href":25},"\u002Fblog\u002Faudio-visualizer","Web Audio 音乐可视化","，那篇讲的是 ",[18,29,30],{},"AnalyserNode 分析播放","，本篇则走 ",[18,33,34],{},"合成发声"," 路线。",[37,38,39],"h2",{"id":39},"核心链路",[41,42,44],"h3",{"id":43},"_1-音高音符名-频率","1. 音高：音符名 → 频率",[14,46,47,48,52,53,56],{},"每个琴键对应如 ",[49,50,51],"code",{},"C4","、",[49,54,55],{},"F#5"," 的音符名。通过 MIDI 公式换算频率（A4 = 440Hz）：",[58,59,64],"pre",{"className":60,"code":61,"language":62,"meta":63,"style":63},"language-javascript shiki shiki-themes github-light github-dark","function noteToFreq(note) {\n  const match = note.match(\u002F^([A-G]#?)(\\d)$\u002F)\n  const [, name, octaveStr] = match\n  const midi = (Number(octaveStr) + 1) * 12 + NOTE_INDEX[name]\n  return 440 * Math.pow(2, (midi - 69) \u002F 12)\n}\n","javascript","",[49,65,66,90,147,173,216,256],{"__ignoreMap":63},[67,68,71,75,79,83,87],"span",{"class":69,"line":70},"line",1,[67,72,74],{"class":73},"szBVR","function",[67,76,78],{"class":77},"sScJk"," noteToFreq",[67,80,82],{"class":81},"sVt8B","(",[67,84,86],{"class":85},"s4XuR","note",[67,88,89],{"class":81},") {\n",[67,91,93,96,100,103,106,109,111,115,118,121,124,127,130,133,136,139,142,144],{"class":69,"line":92},2,[67,94,95],{"class":73},"  const",[67,97,99],{"class":98},"sj4cs"," match",[67,101,102],{"class":73}," =",[67,104,105],{"class":81}," note.",[67,107,108],{"class":77},"match",[67,110,82],{"class":81},[67,112,114],{"class":113},"sZZnC","\u002F",[67,116,117],{"class":73},"^",[67,119,82],{"class":120},"sA_wV",[67,122,123],{"class":98},"[A-G]",[67,125,126],{"class":120},"#",[67,128,129],{"class":73},"?",[67,131,132],{"class":120},")(",[67,134,135],{"class":98},"\\d",[67,137,138],{"class":120},")",[67,140,141],{"class":73},"$",[67,143,114],{"class":113},[67,145,146],{"class":81},")\n",[67,148,150,152,155,158,161,164,167,170],{"class":69,"line":149},3,[67,151,95],{"class":73},[67,153,154],{"class":81}," [, ",[67,156,157],{"class":98},"name",[67,159,160],{"class":81},", ",[67,162,163],{"class":98},"octaveStr",[67,165,166],{"class":81},"] ",[67,168,169],{"class":73},"=",[67,171,172],{"class":81}," match\n",[67,174,176,178,181,183,186,189,192,195,198,201,204,207,210,213],{"class":69,"line":175},4,[67,177,95],{"class":73},[67,179,180],{"class":98}," midi",[67,182,102],{"class":73},[67,184,185],{"class":81}," (",[67,187,188],{"class":77},"Number",[67,190,191],{"class":81},"(octaveStr) ",[67,193,194],{"class":73},"+",[67,196,197],{"class":98}," 1",[67,199,200],{"class":81},") ",[67,202,203],{"class":73},"*",[67,205,206],{"class":98}," 12",[67,208,209],{"class":73}," +",[67,211,212],{"class":98}," NOTE_INDEX",[67,214,215],{"class":81},"[name]\n",[67,217,219,222,225,228,231,234,236,239,242,245,248,250,252,254],{"class":69,"line":218},5,[67,220,221],{"class":73},"  return",[67,223,224],{"class":98}," 440",[67,226,227],{"class":73}," *",[67,229,230],{"class":81}," Math.",[67,232,233],{"class":77},"pow",[67,235,82],{"class":81},[67,237,238],{"class":98},"2",[67,240,241],{"class":81},", (midi ",[67,243,244],{"class":73},"-",[67,246,247],{"class":98}," 69",[67,249,200],{"class":81},[67,251,114],{"class":73},[67,253,206],{"class":98},[67,255,146],{"class":81},[67,257,259],{"class":69,"line":258},6,[67,260,261],{"class":81},"}\n",[41,263,265],{"id":264},"_2-发声振荡器-adsr-包络","2. 发声：振荡器 + ADSR 包络",[14,267,268,269,272,273,276],{},"每次按键创建一个 ",[49,270,271],{},"OscillatorNode","（三角波）和 ",[49,274,275],{},"GainNode","：",[58,278,283],{"className":279,"code":281,"language":282},[280],"language-text","Oscillator → GainNode → destination\n","text",[49,284,281],{"__ignoreMap":63},[14,286,287,288,291,292,295,296,299,300,303],{},"按下时 ",[49,289,290],{},"gain"," 从极小值指数爬升到 ",[49,293,294],{},"PEAK_GAIN","（attack ≈ 20ms）；松手时指数衰减到 0 再 ",[49,297,298],{},"stop"," 振荡器。多音同奏用 ",[49,301,302],{},"Map\u003Cnote, voice>"," 管理，每个音符独立一条链路。",[41,305,307],{"id":306},"_3-延音踏板","3. 延音踏板",[14,309,310,311,314,315,318],{},"踩下踏板（空格或点击踏板按钮）时，松开的键不会立刻 ",[49,312,313],{},"releaseVoice","，声音会保持到抬踏板。抬踏板后，所有 ",[49,316,317],{},"keyHeld === false"," 的 voice 才进入释放包络。",[41,320,322],{"id":321},"_4-三路输入","4. 三路输入",[324,325,326,339],"table",{},[327,328,329],"thead",{},[330,331,332,336],"tr",{},[333,334,335],"th",{},"输入",[333,337,338],{},"处理方式",[340,341,342,365,376],"tbody",{},[330,343,344,348],{},[345,346,347],"td",{},"鼠标 \u002F 触摸",[345,349,350,353,354,357,358,361,362],{},[49,351,352],{},"pointerdown"," 捕获 → ",[49,355,356],{},"pressNote","；",[49,359,360],{},"pointerup"," → ",[49,363,364],{},"releaseNote",[330,366,367,370],{},[345,368,369],{},"物理键盘",[345,371,372,375],{},[49,373,374],{},"KEY_MAP"," 映射 QWERTY 键到音符",[330,377,378,381],{},[345,379,380],{},"内置乐谱",[345,382,383,384,387],{},"按 BPM 计算节拍时长，",[49,385,386],{},"playNoteTimed"," 定时发声",[14,389,390,391,394],{},"乐谱播放用 ",[49,392,393],{},"playToken"," 取消机制：切换曲目或点停止时递增 token，异步循环检测到 token 变化即退出。",[37,396,397],{"id":397},"在线体验",[14,399,400,401,404,405,409],{},"点击或键盘演奏（需用户手势后 ",[49,402,403],{},"AudioContext"," 才会 resume）。也可 ",[23,406,408],{"href":407},"\u002Fdemos\u002Fweb-piano","全屏预览","。",[411,412],"web-piano-demo",{},[37,414,415],{"id":415},"小结",[14,417,418,419,422],{},"网页钢琴的核心是 ",[18,420,421],{},"振荡器合成 + 包络塑形 + 多 voice 管理","。和音乐可视化的 AnalyserNode 不同，这里完全在「生成」声音。真实钢琴音色需要更复杂的采样或物理建模，但三角波 + 简单 ADSR 已足够演示 Web Audio 的合成路径。想继续深入，可以试试换波形、加滤波器，或接入 SoundFont 采样。",[424,425,426],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":63,"searchDepth":149,"depth":149,"links":428},[429,435,436],{"id":39,"depth":92,"text":39,"children":430},[431,432,433,434],{"id":43,"depth":149,"text":44},{"id":264,"depth":149,"text":265},{"id":306,"depth":149,"text":307},{"id":321,"depth":149,"text":322},{"id":397,"depth":92,"text":397},{"id":415,"depth":92,"text":415},"music","2026-06-20","用振荡器与 GainNode 包络合成钢琴音，支持键盘演奏、延音踏板与内置乐谱自动播放。",false,"md",{},true,"\u002Fblog\u002Fweb-piano",{"title":5,"description":439},"blog\u002Fweb-piano",[448,449,450],"web-audio","piano","synthesis","X7pBoWNSCuyPLsw20K-xCjEh7X0xvqwlB3lQ2I-Xk4o",1782309644801]