/** * 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) } // ─── 背景音乐 ──────────────────────────────────────────────────────────────── /** * 启动背景音乐 * 风格:轻快 chiptune / 像素塔防,BPM=128,C大调,4层结构 * 完全程序化合成,无音频文件依赖 */ 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 ac = this.ac const t = ac.currentTime const BPM = 128 const beat = 60 / BPM // 0.469s / 拍 const bar = beat * 4 // 1.875s / 小节 const BARS = 8 // 循环 8 小节 const loopLen = bar * BARS // ≈ 15s // ── 辅助:创建带 Gain 的振荡器,自动连接到 masterGain ────────────────── const addOsc = ( type: OscillatorType, freq: number, start: number, end: number, vol: number, freqEnd?: number ): void => { const g = ac.createGain() g.gain.setValueAtTime(vol, start) g.gain.exponentialRampToValueAtTime(0.0001, end) g.connect(this.masterGain) const o = ac.createOscillator() o.type = type o.frequency.setValueAtTime(freq, start) if (freqEnd !== undefined) { o.frequency.linearRampToValueAtTime(freqEnd, end) } o.connect(g) o.start(start) o.stop(end) this.bgmNodes.push(o) } // ── 辅助:创建打击音(noise burst) ──────────────────────────────────── const addDrum = (start: number, dur: number, vol: number, hp = 0): void => { const bufLen = Math.ceil(ac.sampleRate * dur) const buf = ac.createBuffer(1, bufLen, ac.sampleRate) const d = buf.getChannelData(0) for (let i = 0; i < bufLen; i++) d[i] = Math.random() * 2 - 1 const src = ac.createBufferSource() src.buffer = buf const g = ac.createGain() g.gain.setValueAtTime(vol, start) g.gain.exponentialRampToValueAtTime(0.0001, start + dur) if (hp > 0) { const f = ac.createBiquadFilter() f.type = 'highpass' f.frequency.value = hp src.connect(f); f.connect(g) } else { src.connect(g) } g.connect(this.masterGain) src.start(start) src.stop(start + dur) } // ── 音符频率表(C4 = 261.63 Hz) ────────────────────────────────────── // C4 D4 E4 F4 G4 A4 B4 C5 D5 E5 G5 const N: Record = { C4: 261.63, D4: 293.66, E4: 329.63, F4: 349.23, G4: 392.00, A4: 440.00, B4: 493.88, C5: 523.25, D5: 587.33, E5: 659.25, G5: 783.99, // 低八度 C3: 130.81, E3: 164.81, F3: 174.61, G3: 196.00, A3: 220.00, } // ═══════════════════════════════════════════════════════════════ // 层 1:底鼓 + 军鼓(kick on 1,3;snare on 2,4) // ═══════════════════════════════════════════════════════════════ for (let b = 0; b < BARS * 4; b++) { const st = t + b * beat const pos = b % 4 // 每小节内位置 0-3 if (pos === 0 || pos === 2) { // Kick:低频 sine 扫频(100→30Hz)+ noise burst addOsc('sine', 100, st, st + 0.18, 0.45, 30) addDrum(st, 0.12, 0.12, 0) } if (pos === 1 || pos === 3) { // Snare:中频 noise + 轻微 sine addDrum(st, 0.1, 0.18, 800) addOsc('sine', 220, st, st + 0.07, 0.08) } // Hi-hat 每半拍(8 分音符) addDrum(st, 0.05, 0.07, 6000) addDrum(st + beat / 2, 0.04, 0.05, 6000) } // ═══════════════════════════════════════════════════════════════ // 层 2:和弦低音线 (square, 低八度) // 进行:C - F - G - C - Am - F - G - C // ═══════════════════════════════════════════════════════════════ const bassChord = [ N.C3, N.F3, N.G3, N.C3, N.A3, N.F3, N.G3, N.C3, ] for (let b = 0; b < BARS; b++) { const st = t + b * bar const freq = bassChord[b] // 每小节 4 拍低音 arpeggio(根音每拍重复) for (let i = 0; i < 4; i++) { addOsc('square', freq, st + i * beat, st + i * beat + beat * 0.8, 0.1) } // 五度音(和声感) addOsc('triangle', freq * 1.5, st, st + bar * 0.9, 0.035) } // ═══════════════════════════════════════════════════════════════ // 层 3:主旋律(triangle,亮而柔和,8小节 melody) // 轻快的 C 大调旋律,8小节/32拍 // ═══════════════════════════════════════════════════════════════ // 每项:[频率, 持续拍数] const melody: [number, number][] = [ // 小节 1-2:上行跳跃 [N.E4, 0.5], [N.G4, 0.5], [N.C5, 1], [N.B4, 0.5], [N.A4, 0.5], [N.G4, 1], [N.A4, 0.5], [N.G4, 0.5], // 小节 3-4:中段装饰 [N.E4, 0.5], [N.F4, 0.5], [N.G4, 1], [N.E4, 0.5], [N.D4, 0.5], [N.C4, 1], [N.D4, 0.5], [N.E4, 0.5], // 小节 5-6:高潮段 [N.G4, 0.5], [N.A4, 0.5], [N.C5, 1], [N.D5, 0.5], [N.C5, 0.5], [N.B4, 1], [N.A4, 0.5], [N.G4, 0.5], // 小节 7-8:收尾回落 [N.E4, 0.5], [N.D4, 0.5], [N.C4, 0.5], [N.E4, 0.5], [N.G4, 0.5], [N.F4, 0.5], [N.E4, 0.5], [N.D4, 0.5], [N.C4, 1.5], [0, 0.5], ] let melodyT = t for (const [freq, dur] of melody) { const durSec = dur * beat if (freq > 0) { addOsc('triangle', freq, melodyT, melodyT + durSec * 0.85, 0.18) // 加一层八度泛音(更亮更饱满) addOsc('sine', freq * 2, melodyT, melodyT + durSec * 0.6, 0.06) } melodyT += durSec } // ═══════════════════════════════════════════════════════════════ // 层 4:装饰音(高八度 sine 短促音符,活跃感) // 每2小节出现一次 arpegio 装饰 // ═══════════════════════════════════════════════════════════════ const decoPattern = [ // 小节2结尾装饰 { st: t + bar * 1.75, notes: [N.C5, N.E5, N.G5] }, // 小节4结尾装饰 { st: t + bar * 3.75, notes: [N.G4, N.C5, N.E5] }, // 小节6结尾装饰 { st: t + bar * 5.75, notes: [N.C5, N.D5, N.E5] }, ] for (const { st, notes } of decoPattern) { notes.forEach((freq, i) => { addOsc('sine', freq, st + i * 0.08, st + i * 0.08 + 0.12, 0.1) }) } // 循环调度 setTimeout(() => { if (this.bgmRunning) { this.bgmNodes = [] this._scheduleBGM() } }, (loopLen - 0.3) * 1000) } }