/** * AudioEngine — 纯 Web Audio API 程序化合成,零音频文件依赖 * * 使用单例模式,所有音效通过 AudioEngine.getInstance().play('soundName') 调用 * 背景音乐通过 startBGM() / stopBGM() 控制 */ export class AudioEngine { private static _instance: AudioEngine | null = null private ctx: AudioContext | null = null private bgmNodes: AudioNode[] = [] private bgmRunning = false private muted = false private masterGain!: GainNode private constructor() {} static getInstance(): AudioEngine { if (!AudioEngine._instance) AudioEngine._instance = new AudioEngine() return AudioEngine._instance } /** 首次用户交互后调用,初始化 AudioContext */ init(): void { if (this.ctx) return try { this.ctx = new AudioContext() this.masterGain = this.ctx.createGain() this.masterGain.gain.value = 0.55 this.masterGain.connect(this.ctx.destination) } catch { // 浏览器不支持 Web Audio,静默失败 } } setMuted(v: boolean): void { this.muted = v if (this.masterGain) this.masterGain.gain.value = v ? 0 : 0.55 } isMuted(): boolean { return this.muted } private get ac(): AudioContext | null { return this.ctx } // ─── 工具方法 ──────────────────────────────────────────────────────────────── private osc( type: OscillatorType, freq: number, startTime: number, endTime: number, gainStart = 0.3, gainEnd = 0, destination: AudioNode | null = null ): void { if (!this.ac) return const g = this.ac.createGain() g.gain.setValueAtTime(gainStart, startTime) g.gain.exponentialRampToValueAtTime(Math.max(gainEnd, 0.001), endTime) g.connect(destination ?? this.masterGain) const o = this.ac.createOscillator() o.type = type o.frequency.setValueAtTime(freq, startTime) o.connect(g) o.start(startTime) o.stop(endTime) } private noise( startTime: number, duration: number, gainVal = 0.15, hipass = 0, destination: AudioNode | null = null ): void { if (!this.ac) return const bufLen = this.ac.sampleRate * duration const buf = this.ac.createBuffer(1, bufLen, this.ac.sampleRate) const data = buf.getChannelData(0) for (let i = 0; i < bufLen; i++) data[i] = Math.random() * 2 - 1 const src = this.ac.createBufferSource() src.buffer = buf const g = this.ac.createGain() g.gain.setValueAtTime(gainVal, startTime) g.gain.exponentialRampToValueAtTime(0.001, startTime + duration) if (hipass > 0) { const f = this.ac.createBiquadFilter() f.type = 'highpass' f.frequency.value = hipass src.connect(f) f.connect(g) } else { src.connect(g) } g.connect(destination ?? this.masterGain) src.start(startTime) src.stop(startTime + duration) } // ─── 音效库 ───────────────────────────────────────────────────────────────── /** 钉钉消息通知音:建塔 / 波次开始 */ playDingTalk(): void { if (!this.ac) return const t = this.ac.currentTime // 叮 — 高频正弦 this.osc('sine', 1047, t, t + 0.18, 0.4, 0.001) // 咚 — 低频跟随 this.osc('sine', 523, t + 0.1, t + 0.35, 0.25, 0.001) } /** 钉钉消息发送音(更轻):波次开始 */ playNotify(): void { if (!this.ac) return const t = this.ac.currentTime this.osc('sine', 880, t, t + 0.12, 0.3, 0.001) this.osc('sine', 1320, t + 0.07, t + 0.2, 0.2, 0.001) } /** 拳击音:实习生近战 */ playPunch(): void { if (!this.ac) return const t = this.ac.currentTime this.noise(t, 0.06, 0.3, 200) this.osc('sine', 120, t, t + 0.06, 0.35, 0.001) } /** 键盘敲击音:资深开发射击 */ playKeyboard(): void { if (!this.ac) return const t = this.ac.currentTime this.noise(t, 0.04, 0.18, 1200) this.osc('square', 440, t, t + 0.03, 0.12, 0.001) } /** PPT 大师 AOE — 空洞回声 */ playPPTBlast(): void { if (!this.ac) return const t = this.ac.currentTime this.osc('sawtooth', 180, t, t + 0.4, 0.2, 0.001) this.osc('sine', 90, t, t + 0.5, 0.15, 0.001) this.noise(t, 0.3, 0.08, 100) } /** 产品经理 — 需求变更铃 */ playPMAttack(): void { if (!this.ac) return const t = this.ac.currentTime this.osc('triangle', 660, t, t + 0.15, 0.25, 0.001) this.osc('triangle', 440, t + 0.08, t + 0.25, 0.2, 0.001) } /** 运营专员 — 数据上涨 */ playOpsAttack(): void { if (!this.ac) return const t = this.ac.currentTime this.osc('sine', 523, t, t + 0.08, 0.2, 0.001) this.osc('sine', 659, t + 0.05, t + 0.15, 0.2, 0.001) this.osc('sine', 784, t + 0.1, t + 0.22, 0.2, 0.001) } /** 外包程序员 — 闷声丢出 */ playOutsourceAttack(): void { if (!this.ac) return const t = this.ac.currentTime this.noise(t, 0.08, 0.2, 300) this.osc('sawtooth', 200, t, t + 0.08, 0.15, 0.001) } /** 怪物死亡 — 碎纸机 */ playEnemyDeath(): void { if (!this.ac) return const t = this.ac.currentTime this.noise(t, 0.12, 0.22, 800) this.noise(t + 0.05, 0.08, 0.15, 2000) } /** Boss 登场 — 警报 + 低沉冲击 */ playBossAppear(): void { if (!this.ac) return const t = this.ac.currentTime // 低沉轰鸣 this.osc('sawtooth', 55, t, t + 1.2, 0.5, 0.001) this.osc('sawtooth', 60, t + 0.1, t + 1.0, 0.3, 0.001) // 警报扫频 for (let i = 0; i < 4; i++) { const st = t + i * 0.25 this.osc('square', 440, st, st + 0.1, 0.25, 0.001) this.osc('square', 660, st + 0.12, st + 0.22, 0.2, 0.001) } } /** Boss 技能触发 — 电话挂断 */ playBossSkill(): void { if (!this.ac) return const t = this.ac.currentTime this.osc('square', 1480, t, t + 0.06, 0.3, 0.001) this.osc('square', 1109, t + 0.07, t + 0.13, 0.3, 0.001) this.osc('square', 740, t + 0.14, t + 0.22, 0.3, 0.001) this.noise(t, 0.25, 0.1, 1000) } /** 塔被摧毁 — 电话挂断 */ playTowerDestroyed(): void { if (!this.ac) return const t = this.ac.currentTime this.osc('square', 440, t, t + 0.1, 0.35, 0.001) this.osc('square', 350, t + 0.1, t + 0.25, 0.35, 0.001) this.osc('square', 220, t + 0.2, t + 0.4, 0.35, 0.001) this.noise(t, 0.4, 0.12, 500) } /** 新波次开始 — 钉钉来电铃声 */ playWaveStart(): void { if (!this.ac) return const t = this.ac.currentTime const pattern = [0, 0.18, 0.36, 0.64, 0.82, 1.0] const freqs = [784, 784, 784, 659, 784, 880] pattern.forEach((delay, i) => { this.osc('sine', freqs[i], t + delay, t + delay + 0.14, 0.3, 0.001) }) } /** KPI 告警(KPI < 30%)— 紧张低音 */ playKPIWarning(): void { if (!this.ac) return const t = this.ac.currentTime this.osc('sawtooth', 110, t, t + 0.3, 0.4, 0.001) this.osc('sawtooth', 146, t + 0.1, t + 0.3, 0.3, 0.001) } // ─── 背景音乐 ──────────────────────────────────────────────────────────────── /** * 启动背景音乐(办公室 ambient:低频嗡鸣 + 轻微节奏脉冲) * 完全程序化合成,循环播放 */ startBGM(): void { if (!this.ac || this.bgmRunning) return this.bgmRunning = true this._scheduleBGM() } stopBGM(): void { this.bgmRunning = false this.bgmNodes.forEach(n => { try { (n as OscillatorNode).stop?.() } catch { /* ignore */ } }) this.bgmNodes = [] } private _scheduleBGM(): void { if (!this.ac || !this.bgmRunning) return const t = this.ac.currentTime // 底层办公室嗡鸣(电脑风扇感) const droneGain = this.ac.createGain() droneGain.gain.value = 0.04 droneGain.connect(this.masterGain) const drone = this.ac.createOscillator() drone.type = 'sawtooth' drone.frequency.setValueAtTime(55, t) drone.frequency.linearRampToValueAtTime(58, t + 8) drone.frequency.linearRampToValueAtTime(55, t + 16) drone.connect(droneGain) drone.start(t) drone.stop(t + 16) this.bgmNodes.push(drone) // 低频键盘噪声节拍(每 0.8s 一拍) for (let i = 0; i < 20; i++) { const beat = t + i * 0.8 const ng = this.ac.createGain() ng.gain.setValueAtTime(0.03, beat) ng.gain.exponentialRampToValueAtTime(0.001, beat + 0.15) ng.connect(this.masterGain) const ns = this.ac.createOscillator() ns.type = 'square' ns.frequency.value = i % 4 === 0 ? 130 : i % 2 === 0 ? 110 : 87 ns.connect(ng) ns.start(beat) ns.stop(beat + 0.12) this.bgmNodes.push(ns) } // 16s 后循环 setTimeout(() => { if (this.bgmRunning) this._scheduleBGM() }, 15500) } }