Files
test1/game/AudioEngine.ts

420 lines
15 KiB
TypeScript
Raw Permalink 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)
}
// ─── 背景音乐 ────────────────────────────────────────────────────────────────
/**
* 启动背景音乐
* 风格:轻快 chiptune / 像素塔防BPM=128C大调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<string, number> = {
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,3snare 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)
}
}