/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}
Web Audio API 振荡器与滤波包络实战:在浏览器中合成 808 经典底鼓

在现代电子音乐、嘻哈(Trap)与工业摇滚中,Roland TR-808 鼓机的底鼓(808 Kick)是奠定整首乐曲低频灵魂(Sub-bass Punch)的绝对基石。
很多前端开发者在做 Web 音频交互或网页游戏打击乐时,往往习惯直接通过 new Audio('kick.mp3').play() 播放一段预录好的音频切片。这种方式存在三大致命缺陷:
利用浏览器原生内置的 Web Audio API 节点图(Audio Graph),通过 正弦波振荡器(OscillatorNode)、指数音高瞬态包络(Pitch Envelope) 与 增益衰减器(GainNode),我们能够用不到 50 行纯 TypeScript 代码,在浏览器内存中实时物理合成出震撼、紧实、毫秒级零延迟的经典 808 模拟底鼓。
808 底鼓的声学物理合成模型
一个充满力量感与胸腔共鸣的 808 底鼓,在物理声学上由两部分构成:
【808 底鼓合成 AudioNode 拓扑图】
[正弦波振荡器 (OscillatorNode: sine)]
– frequency 参数由指数音高包络控制 (160Hz ──> 50Hz)
│
▼
[动态增益包络器 (GainNode: volume envelope)]
– gain 参数由指数音量包络控制 (1.0 ──> 0.001)
│
▼
[低通滤波器 (BiquadFilterNode: lowpass 250Hz)]
– 滤除高频杂音,强化浑厚 Sub-bass
│
▼
[音频输出上下文 (AudioContext.destination)]
核心实现:生产级 Web 808 纯代码合成引擎
export interface Kick808Params {
baseFrequency?: number; // 基频 (默认 50 Hz)
punchFrequency?: number; // 起音最高冲击频率 (默认 160 Hz)
punchTime?: number; // 俯冲起音时间 (默认 0.035 秒)
decayTime?: number; // 整体衰减尾音长度 (默认 0.6 秒)
gain?: number; // 主音量 (0.0 ~ 1.0)
}
export class WebKick808Synthesizer {
private ctx: AudioContext;
private masterGain: GainNode;
constructor() {
// 兼容各浏览器前缀
const AudioCtx = window.AudioContext || (window as any).webkitAudioContext;
this.ctx = new AudioCtx();
this.masterGain = this.ctx.createGain();
this.masterGain.connect(this.ctx.destination);
}
public trigger(params: Kick808Params = {}) {
const {
baseFrequency = 50,
punchFrequency = 160,
punchTime = 0.035,
decayTime = 0.6,
gain = 0.9,
} = params;
// 解决浏览器自动播放安全策略 (用户首个交互后恢复上下文)
if (this.ctx.state === 'suspended') {
this.ctx.resume();
}
const now = this.ctx.currentTime;
// 1. 创建正弦波振荡器 (808 的核心声源)
const osc = this.ctx.createOscillator();
osc.type = 'sine';
// 2. 创建独立的音量包络 Gain 节点
const gainNode = this.ctx.createGain();
// 3. 创建低通滤波器,切掉多余高频,让低频更结实
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(300, now);
// ==========================================
// 核心秘诀:音高包络(Pitch Envelope)
// ==========================================
osc.frequency.setValueAtTime(punchFrequency, now);
// 在 punchTime 时间内,音高以指数曲线迅速俯冲至基频 baseFrequency
osc.frequency.exponentialRampToValueAtTime(baseFrequency, now + punchTime);
// ==========================================
// 核心秘诀:音量包络(Amplitude Envelope)
// ==========================================
gainNode.gain.setValueAtTime(gain, now);
// 在 decayTime 内,振幅沿指数曲线平滑衰减至接近 0 (防爆音)
gainNode.gain.exponentialRampToValueAtTime(0.001, now + decayTime);
// 4. 连接节点图
osc.connect(gainNode);
gainNode.connect(filter);
filter.connect(this.masterGain);
// 5. 调度发声与自动销毁释放内存
osc.start(now);
osc.stop(now + decayTime + 0.05);
// 发声结束后断开连接,避免内存泄漏
setTimeout(() => {
osc.disconnect();
gainNode.disconnect();
filter.disconnect();
}, (decayTime + 0.1) * 1000);
}
}
前端 React 动态打击垫组件
import React, { useRef, useState } from 'react';
import { WebKick808Synthesizer } from './synth808';
export const DrumPad808: React.FC = () => {
const synthRef = useRef<WebKick808Synthesizer | null>(null);
const [decay, setDecay] = useState<number>(0.6);
const [pitch, setPitch] = useState<number>(50);
const getSynth = () => {
if (!synthRef.current) {
synthRef.current = new WebKick808Synthesizer();
}
return synthRef.current;
};
const handleHit = () => {
getSynth().trigger({
baseFrequency: pitch,
decayTime: decay,
punchFrequency: pitch * 3.2,
});
};
return (
<div className="p-8 bg-slate-950 text-white rounded-3xl border border-slate-800 shadow-2xl flex flex-col items-center max-w-sm">
<h2 className="text-xl font-black text-cyan-400 tracking-wider">808 SUB-BASS KICK</h2>
<p className="text-xs text-slate-400 mt-1">Web Audio API 纯代码实时物理合成</p>
{/* 实体动态打击垫按钮 */}
<button
onClick={handleHit}
className="mt-8 w-40 h-40 rounded-2xl bg-gradient-to-br from-cyan-500 to-blue-600 font-black text-2xl shadow-[0_0_40px_rgba(6,182,212,0.4)] active:scale-95 active:shadow-[0_0_15px_rgba(6,182,212,0.6)] transition-all flex items-center justify-center border-2 border-cyan-300/30"
>
💥 PUNCH
</button>
{/* 参数调音面板 */}
<div className="mt-8 w-full space-y-4 text-xs font-mono">
<div>
<div className="flex justify-between text-slate-300">
<span>基调音高 (Pitch):</span>
<span className="text-cyan-400 font-bold">{pitch} Hz</span>
</div>
<input
type="range"
min="35"
max="80"
value={pitch}
onChange={(e) => setPitch(Number(e.target.value))}
className="w-full mt-1 accent-cyan-400"
/>
</div>
<div>
<div className="flex justify-between text-slate-300">
<span>尾音衰减 (Decay):</span>
<span className="text-cyan-400 font-bold">{decay}s</span>
</div>
<input
type="range"
min="0.2"
max="1.5"
step="0.05"
value={decay}
onChange={(e) => setDecay(Number(e.target.value))}
className="w-full mt-1 accent-cyan-400"
/>
</div>
</div>
</div>
);
};



