Files
test1/game/AudioEngine.ts

290 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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)
}
}