diff --git a/game/AudioEngine.ts b/game/AudioEngine.ts index 5b057a6..208d0e9 100644 --- a/game/AudioEngine.ts +++ b/game/AudioEngine.ts @@ -230,8 +230,9 @@ export class AudioEngine { // ─── 背景音乐 ──────────────────────────────────────────────────────────────── /** - * 启动背景音乐(办公室 ambient:低频嗡鸣 + 轻微节奏脉冲) - * 完全程序化合成,循环播放 + * 启动背景音乐 + * 风格:轻快 chiptune / 像素塔防,BPM=128,C大调,4层结构 + * 完全程序化合成,无音频文件依赖 */ startBGM(): void { if (!this.ac || this.bgmRunning) return @@ -249,41 +250,170 @@ export class AudioEngine { private _scheduleBGM(): void { if (!this.ac || !this.bgmRunning) return - const t = this.ac.currentTime + const ac = this.ac + const t = 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) + const BPM = 128 + const beat = 60 / BPM // 0.469s / 拍 + const bar = beat * 4 // 1.875s / 小节 + const BARS = 8 // 循环 8 小节 + const loopLen = bar * BARS // ≈ 15s - // 低频键盘噪声节拍(每 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) + // ── 辅助:创建带 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) } - // 16s 后循环 + // ── 辅助:创建打击音(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._scheduleBGM() - }, 15500) + if (this.bgmRunning) { + this.bgmNodes = [] + this._scheduleBGM() + } + }, (loopLen - 0.3) * 1000) } }