// 合成音效:鼓点·钟鸣·纸韵(Web Audio,零素材依赖) let ctx: AudioContext | null = null let enabled = true export function setSoundEnabled(on: boolean): void { enabled = on } export function isSoundEnabled(): boolean { return enabled } function ac(): AudioContext | null { if (!enabled) return null try { if (!ctx) { const AC = window.AudioContext ?? (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext ctx = new AC() } if (ctx.state === 'suspended') void ctx.resume() return ctx } catch { return null } } function tone(freq: number, dur: number, gain: number, type: OscillatorType = 'sine', when = 0, slideTo?: number): void { const a = ac() if (!a) return const t0 = a.currentTime + when const o = a.createOscillator() const g = a.createGain() o.type = type o.frequency.setValueAtTime(freq, t0) if (slideTo) o.frequency.exponentialRampToValueAtTime(slideTo, t0 + dur) g.gain.setValueAtTime(0, t0) g.gain.linearRampToValueAtTime(gain, t0 + 0.012) g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur) o.connect(g) g.connect(a.destination) o.start(t0) o.stop(t0 + dur + 0.05) } function noise(dur: number, gain: number, when = 0, freq = 3200): void { const a = ac() if (!a) return const t0 = a.currentTime + when const len = Math.floor(a.sampleRate * dur) const buf = a.createBuffer(1, len, a.sampleRate) const data = buf.getChannelData(0) for (let i = 0; i < len; i++) data[i] = (Math.random() * 2 - 1) * (1 - i / len) const src = a.createBufferSource() src.buffer = buf const f = a.createBiquadFilter() f.type = 'highpass' f.frequency.value = freq const g = a.createGain() g.gain.setValueAtTime(gain, t0) g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur) src.connect(f) f.connect(g) g.connect(a.destination) src.start(t0) } /** 月度推进:极轻的硖墨声 */ export function sTick(): void { noise(0.05, 0.015, 0, 5000) } /** 开卷 / 纸笺 */ export function sPaper(): void { noise(0.12, 0.03, 0, 2400) tone(620, 0.1, 0.015, 'sine', 0.02) } /** 好消息:铜钟一声 */ export function sGood(): void { tone(660, 0.5, 0.06, 'sine') tone(990, 0.42, 0.03, 'sine', 0.05) tone(1320, 0.3, 0.018, 'sine', 0.09) } /** 坏消息:折磬低响 */ export function sBad(): void { tone(196, 0.4, 0.05, 'triangle', 0, 130) tone(131, 0.5, 0.04, 'triangle', 0.04, 82) } /** 战报:鼓点急雨 */ export function sWar(): void { tone(120, 0.16, 0.07, 'sine', 0, 60) tone(120, 0.16, 0.06, 'sine', 0.16, 60) tone(160, 0.3, 0.06, 'sine', 0.32, 80) noise(0.28, 0.02, 0.36, 1500) } /** 钟鸣:大事件(碑立/飞升) */ export function sBell(): void { tone(392, 1.4, 0.06, 'sine') tone(494, 1.2, 0.035, 'sine', 0.08) tone(587, 1.0, 0.028, 'sine', 0.16) } /** 突破:磬缶清越 */ export function sGong(): void { tone(440, 0.9, 0.05, 'sine') tone(660, 0.7, 0.03, 'sine', 0.06) } /** 按钮:纸面轻叩 */ export function sClick(): void { noise(0.045, 0.03, 0, 4200) }