<!DOCTYPE html>
<html lang=”zh-CN”>
<head>
<meta charset=”UTF-8″ />
<meta name=”viewport” content=”width=device-width, initial-scale=1.0″ />
<title>recent.design 点击音效 · 复现与提取</title>
<style>
:root { –bg:#0f1115; –panel:#171a21; –line:#262b36; –txt:#e6e9ef; –dim:#8b93a7; –acc:#5b8cff; }
* { box-sizing: border-box; }
body { margin:0; font-family:-apple-system,Segoe UI,Roboto,”PingFang SC”,”Microsoft YaHei”,sans-serif;
background:var(–bg); color:var(–txt); line-height:1.6; padding:32px 20px; }
.wrap { max-width:860px; margin:0 auto; }
h1 { font-size:22px; margin:0 0 4px; }
h2 { font-size:16px; margin:28px 0 10px; color:var(–acc); }
p, li { color:var(–txt); }
.dim { color:var(–dim); font-size:13px; }
.card { background:var(–panel); border:1px solid var(–line); border-radius:12px; padding:18px 20px; margin:14px 0; }
code { background:#0b0d12; padding:2px 6px; border-radius:5px; font-size:12.5px; color:#9fd0ff; }
table { width:100%; border-collapse:collapse; font-size:13px; margin-top:8px; }
th, td { text-align:left; padding:7px 9px; border-bottom:1px solid var(–line); }
th { color:var(–dim); font-weight:600; }
.btns { display:flex; flex-wrap:wrap; gap:10px; margin-top:8px; }
button { background:var(–acc); color:#fff; border:0; padding:11px 18px; border-radius:9px;
font-size:14px; cursor:pointer; }
button.ghost { background:transparent; border:1px solid var(–line); color:var(–txt); }
button:active { transform:translateY(1px); }
.row { display:flex; align-items:center; gap:12px; flex-wrap:wrap; }
input[type=range] { accent-color:var(–acc); }
pre { background:#0b0d12; border:1px solid var(–line); border-radius:10px; padding:14px;
overflow:auto; font-size:12px; color:#cfe3ff; }
.tag { display:inline-block; background:#1d2330; border:1px solid var(–line); color:var(–dim);
padding:2px 9px; border-radius:20px; font-size:12px; margin-right:6px; }
</style>
</head>
<body>
<div class=”wrap”>
<h1>recent.design 点击音效 — 分析与复现</h1>
<p class=”dim”>结论先行:该网站的点击音效 <b>不是音频文件</b>,而是由 Web Audio API 实时合成(AudioContext + OscillatorNode + BiquadFilter + GainNode + FM 调制)。下方可试听、调参、并直接导出为 .wav 文件。</p>
<div class=”card”>
<h2 style=”margin-top:0″>1. 它是怎么实现的</h2>
<p>源码位于打包文件 <code>dist-BmgBrDGN.js</code>(约 191KB,已混淆)。核心是一个自带的微型合成引擎:</p>
<ul>
<li><code>Zd()</code> 延迟创建并 <code>resume()</code> 一个共享 <code>AudioContext</code>。</li>
<li>声音预设存在一个对象 <code>Kd</code> 里,键名包括 <span class=”tag”>press</span><span class=”tag”>pop</span><span class=”tag”>select</span><span class=”tag”>toggleOn</span><span class=”tag”>open</span><span class=”tag”>close</span><span class=”tag”>success</span><span class=”tag”>error</span><span class=”tag”>copy</span> 等。</li>
<li>每个声音由若干 <code>layers</code> 叠加而成,每层 = 声源(noise / sine / triangle / square / sawtooth)→ 可选滤波器 → 包络增益。</li>
<li>默认“点击/按压”音效就是 <code>press</code> 预设。它<b>完全不加载任何 .mp3/.wav/.ogg</b>(站内没有任何 UI 音效的音频文件引用)。</li>
</ul>
<p class=”dim”>注意:引擎里确实有 <code>fetch + decodeAudioData</code> 的能力(<code>Tf()</code>),但那只服务于 <code>sample</code>/<code>stream</code> 类型的声源,<code>press</code> 并未使用。所以“提取音频文件”这条路走不通——正确做法是复现其合成逻辑。</p>
</div>
<div class=”card”>
<h2 style=”margin-top:0″>2. <code>press</code> 点击音效的精确合成参数</h2>
<p>共 3 层叠加,主增益 <code>qd.press = 1</code>,全局音量默认 1。</p>
<table>
<thead><tr><th>层</th><th>声源</th><th>音高 (Hz)</th><th>滤波器</th><th>包络 (attack/decay/sustain/release)</th><th>峰值增益</th></tr></thead>
<tbody>
<tr><td>1 瞬态</td><td>白噪声</td><td>—</td><td>bandpass 2100 / Q0.9</td><td>0 / 0.006 / 0 / 0.003</td><td>0.11</td></tr>
<tr><td>2 主体</td><td>triangle</td><td>520 → 430 (指数滑音)</td><td>无</td><td>0.001 / 0.038 / 0 / 0.014</td><td>0.62</td></tr>
<tr><td>3 亮音</td><td>sine + FM</td><td>1220 → 880,调制比0.5(610Hz) 深度35</td><td>无</td><td>0.001 / 0.018 / 0 / 0.007</td><td>0.18</td></tr>
</tbody>
</table>
<p class=”dim”>听感:一层被带通滤波的“咔”噪声瞬态 + 一个快速下滑的三角波主体 + 一点带 FM 的明亮正弦,整体约 30–50ms 收尾,干脆利落。</p>
</div>
<div class=”card”>
<h2 style=”margin-top:0″>3. 试听与导出</h2>
<div class=”row”>
<label class=”dim”>声音</label>
<select id=”sound” style=”background:#0b0d12;color:var(–txt);border:1px solid var(–line);border-radius:8px;padding:8px 10px;”>
<option value=”press”>press(点击/按压)</option>
<option value=”pop”>pop</option>
<option value=”select”>select</option>
<option value=”toggleOn”>toggleOn</option>
<option value=”open”>open</option>
<option value=”close”>close</option>
<option value=”success”>success</option>
<option value=”error”>error</option>
<option value=”copy”>copy</option>
</select>
<label class=”dim”>音量 <span id=”volv”>1.0</span></label>
<input id=”vol” type=”range” min=”0″ max=”1.5″ step=”0.05″ value=”1″ style=”width:160px”>
</div>
<div class=”btns”>
<button id=”play”>▶ 播放一次</button>
<button id=”rapid” class=”ghost”>连点 5 次</button>
<button id=”export” class=”ghost”>⤓ 导出为 WAV</button>
</div>
<p class=”dim”>“导出为 WAV”会用 OfflineAudioContext 离线渲染该音效,生成一段真实可用的 .wav 文件下载到本地——这就是你要的“提取”成果。</p>
</div>
<div class=”card”>
<h2 style=”margin-top:0″>4. 可复用的核心代码(已按原站引擎 1:1 还原)</h2>
<p class=”dim”>下面这段是下面页面所用引擎的精简版,可直接拷进你自己的项目。</p>
<pre id=”code”></pre>
</div>
</div>
<script id=”engine”>
/* ============================================================
还原 recent.design 的 Web Audio 合成引擎(基于 dist-BmgBrDGN.js)
– Vf: 渲染一个声音(layers 叠加)
– Lf: 包络增益 (attack/decay/sustain/release)
– Ef/Df: 振荡器 / 噪声声源
– Nf: BiquadFilter
============================================================ */
const EPS = 1e-4;
// 声音预设(原站 Kd 中的一部分,press 为默认点击音)
const SOUNDS = {
press: { layers: [
{ source:{type:’noise’,color:’white’}, filter:{type:’bandpass’,frequency:2100,resonance:0.9}, envelope:{attack:0,decay:0.006,sustain:0,release:0.003}, gain:0.11 },
{ source:{type:’triangle’,frequency:{start:520,end:430}}, envelope:{attack:0.001,decay:0.038,sustain:0,release:0.014}, gain:0.62 },
{ source:{type:’sine’,frequency:{start:1220,end:880}, fm:{ratio:0.5,depth:35}}, envelope:{attack:0.001,decay:0.018,sustain:0,release:0.007}, gain:0.18 },
]},
pop: { source:{type:’sine’,frequency:{start:300,end:2000}}, envelope:{attack:0.008,decay:0.12,sustain:0,release:0.04}, gain:0.32 },
select: { source:{type:’sine’,frequency:{start:860,end:1040}}, envelope:{attack:0.001,decay:0.05,sustain:0,release:0.02}, gain:1 },
toggleOn: { layers:[
{ source:{type:’sine’,frequency:520}, envelope:{attack:0.001,decay:0.065,sustain:0,release:0.02}, gain:1 },
{ source:{type:’sine’,frequency:760}, envelope:{attack:0.001,decay:0.07,sustain:0,release:0.02}, delay:0.035, gain:1 },
]},
open: { source:{type:’sine’,frequency:{start:420,end:760}}, envelope:{attack:0.002,decay:0.09,sustain:0,release:0.03}, gain:1 },
close: { source:{type:’sine’,frequency:{start:720,end:360}}, envelope:{attack:0.002,decay:0.08,sustain:0,release:0.025}, gain:1 },
success: { layers:[
{ source:{type:’sine’,frequency:523}, envelope:{attack:0.003,decay:0.3,sustain:0.06,release:0.1}, gain:0.16 },
{ source:{type:’sine’,frequency:659}, envelope:{attack:0.003,decay:0.28,sustain:0.05,release:0.1}, delay:0.07, gain:0.14 },
{ source:{type:’sine’,frequency:{start:784,end:880}}, envelope:{attack:0.003,decay:0.32,sustain:0.06,release:0.12}, delay:0.14, gain:0.15 },
]},
error: { layers:[
{ source:{type:’sawtooth’,frequency:{start:320,end:140}}, filter:{type:’lowpass’,frequency:1200}, envelope:{attack:0,decay:0.25,sustain:0,release:0.08}, gain:0.22 },
{ source:{type:’square’,frequency:{start:180,end:80}}, filter:{type:’lowpass’,frequency:800}, envelope:{attack:0,decay:0.2,sustain:0,release:0.06}, delay:0.03, gain:0.12 },
]},
copy: { layers:[
{ source:{type:’sine’,frequency:1200}, envelope:{attack:0,decay:0.015,sustain:0,release:0.006}, gain:0.16 },
{ source:{type:’sine’,frequency:1400}, envelope:{attack:0,decay:0.015,sustain:0,release:0.006}, delay:0.04, gain:0.14 },
]},
};
// 白/粉/棕噪声缓冲(对应原站 Cf/bf/xf/Sf)
function makeNoiseBuffer(ctx, color, dur){
const len = Math.max(1, Math.ceil(ctx.sampleRate * dur));
const buf = ctx.createBuffer(1, len, ctx.sampleRate);
const d = buf.getChannelData(0);
if(color === ‘pink’){
let b0=0,b1=0,b2=0,b3=0,b4=0,b5=0,b6=0;
for(let i=0;i<len;i++){ const w=Math.random()*2-1;
b0=.99886*b0+w*.0555179; b1=.99332*b1+w*.0750759; b2=.969*b2+w*.153852;
b3=.8665*b3+w*.3104856; b4=.55*b4+w*.5329522; b5=-.7616*b5-w*.016898;
d[i]=(b0+b1+b2+b3+b4+b5+b6+w*.5362)*.11; b6=w*.115926; }
} else if(color === ‘brown’){
let last=0; for(let i=0;i<len;i++){ const w=Math.random()*2-1; last=(last+.02*w)/1.02; d[i]=last*3.5; }
} else {
for(let i=0;i<len;i++) d[i]=Math.random()*2-1;
}
return buf;
}
// 包络增益节点(对应原站 Lf)
function makeEnvelope(ctx, env, peak, t0){
const g = ctx.createGain();
if(!env){ g.gain.setValueAtTime(peak, t0); g.gain.setTargetAtTime(EPS, t0, 0.15); return {node:g, duration:0.5}; }
const a=env.attack??0, o=env.decay, s=env.sustain??0, c=env.release??0;
const low=Math.max(s*peak, EPS), tau=o/3;
g.gain.setValueAtTime(EPS, t0);
if(a>0) g.gain.linearRampToValueAtTime(peak, t0+a); else g.gain.setValueAtTime(peak, t0);
if(s>0){ g.gain.setTargetAtTime(low, t0+a, tau); if(c>0) g.gain.setTargetAtTime(EPS, t0+a+o, c/3); }
else g.gain.setTargetAtTime(EPS, t0+a, tau);
return {node:g, duration:a+o+c};
}
// 声源节点(对应原站 Mf/Ef/Df)
function makeSource(ctx, src, t0, dur){
if(src.type === ‘noise’){
const b = makeNoiseBuffer(ctx, src.color||’white’, dur+0.1);
const n = ctx.createBufferSource(); n.buffer=b; n.start(t0); n.stop(t0+dur+0.1);
return {node:n};
}
const o = ctx.createOscillator();
if([‘sine’,’triangle’,’square’,’sawtooth’].includes(src.type)){
o.type = src.type;
if(typeof src.frequency === ‘number’){ o.frequency.setValueAtTime(src.frequency, t0); }
else { o.frequency.setValueAtTime(src.frequency.start, t0);
o.frequency.exponentialRampToValueAtTime(Math.max(src.frequency.end,1), t0+dur); }
if(src.detune) o.detune.value = src.detune;
o.start(t0); o.stop(t0+dur+0.1);
if(src.fm){
const base = (typeof src.frequency===’number’)?src.frequency:src.frequency.start;
const m = ctx.createOscillator(); m.type=’sine’; m.frequency.value=base*src.fm.ratio;
const mg = ctx.createGain(); mg.gain.value=src.fm.depth;
m.connect(mg); mg.connect(o.frequency); m.start(t0); m.stop(t0+dur+0.1);
}
return {node:o};
}
o.type=’sine’; o.frequency.value=440; o.start(t0); o.stop(t0+dur+0.1); return {node:o};
}
// 渲染一个声音到 ctx(对应原站 Vf)
function playSound(ctx, def, opts={}, dest){
dest = dest || ctx.destination;
const layers = def.layers || [def];
const volume = opts.volume ?? 1, velocity = opts.velocity ?? 1;
const base = opts.startTime ?? ctx.currentTime;
layers.forEach(layer=>{
const t0 = base + (layer.delay||0);
const peak = (layer.gain ?? 0.5) * volume * velocity;
const {node:env, duration} = makeEnvelope(ctx, layer.envelope, peak, t0);
const src = makeSource(ctx, layer.source, t0, duration);
let node = src.node;
if(layer.filter){
const flts = Array.isArray(layer.filter)?layer.filter:[layer.filter];
flts.forEach(f=>{
const b = ctx.createBiquadFilter();
b.type=f.type; b.frequency.setValueAtTime(f.frequency, t0); b.Q.value=f.resonance??1;
if(f.gain!==undefined) b.gain.value=f.gain;
node.connect(b); node=b;
});
}
node.connect(env); env.connect(dest);
});
}
/* ———- 交互 ———- */
</script>
<script>
let actx = null;
function getCtx(){ if(!actx) actx = new (window.AudioContext||window.webkitAudioContext)();
if(actx.state===’suspended’) actx.resume(); return actx; }
const $ = s => document.querySelector(s);
$(‘#play’).onclick = ()=>{ const c=getCtx(); playSound(c, SOUNDS[$(‘#sound’).value], {volume:+ $(‘#vol’).value}); };
$(‘#rapid’).onclick = ()=>{ const c=getCtx(); for(let i=0;i<5;i++) playSound(c, SOUNDS[$(‘#sound’).value], {volume:+ $(‘#vol’).value, startTime:c.currentTime+i*0.12}); };
$(‘#vol’).oninput = e => $(‘#volv’).textContent = (+e.target.value).toFixed(2);
// 离线渲染并导出 WAV(对应原站的“提取”目标)
$(‘#export’).onclick = async ()=>{
const sr = 44100, dur = 0.5;
const off = new OfflineAudioContext(1, Math.ceil(sr*dur), sr);
playSound(off, SOUNDS[$(‘#sound’).value], {volume:+ $(‘#vol’).value, startTime:0.02});
const buf = await off.startRendering();
const wav = audioBufferToWav(buf);
const url = URL.createObjectURL(new Blob([wav], {type:’audio/wav’}));
const a = document.createElement(‘a’); a.href=url; a.download=`recent-${$(‘#sound’).value}.wav`; a.click();
setTimeout(()=>URL.revokeObjectURL(url), 1000);
};
function audioBufferToWav(buffer){
const numCh=1, sr=buffer.sampleRate, samples=buffer.getChannelData(0);
const bytesPerSample=2, blockAlign=numCh*bytesPerSample, dataSize=samples.length*bytesPerSample;
const ab=new ArrayBuffer(44+dataSize), v=new DataView(ab);
const ws=(o,s)=>{ for(let i=0;i<s.length;i++) v.setUint8(o+i, s.charCodeAt(i)); };
ws(0,’RIFF’); v.setUint32(4,36+dataSize,true); ws(8,’WAVE’); ws(12,’fmt ‘);
v.setUint32(16,16,true); v.setUint16(20,1,true); v.setUint16(22,numCh,true);
v.setUint32(24,sr,true); v.setUint32(28,sr*blockAlign,true); v.setUint16(32,blockAlign,true);
v.setUint16(34,16,true); ws(36,’data’); v.setUint32(40,dataSize,true);
let off=44; for(let i=0;i<samples.length;i++,off+=2){ let s=Math.max(-1,Math.min(1,samples[i])); v.setInt16(off, s<0?s*0x8000:s*0x7FFF, true); }
return ab;
}
// 展示核心代码(直接读取上方引擎脚本)
$(‘#code’).textContent = document.getElementById(‘engine’).textContent.trim();
</script>
</body>
</html>























![表情[youhengheng]-设计盒子](https://www.shejihezi.com/wp-content/themes/zibll/img/smilies/youhengheng.gif)
![表情[deyi]-设计盒子](https://www.shejihezi.com/wp-content/themes/zibll/img/smilies/deyi.gif)


请登录后查看评论内容