type AudioContextCtor = typeof AudioContext; export class AudioEngine { private context?: AudioContext; private master?: GainNode; private drone?: OscillatorNode; private shimmer?: OscillatorNode; private active = false; activate(): void { if (this.active) { void this.context?.resume(); return; } const ctor = window.AudioContext ?? (window as Window & { webkitAudioContext?: AudioContextCtor }).webkitAudioContext; if (!ctor) { return; } this.context = new ctor(); this.master = this.context.createGain(); this.master.gain.value = 0.07; this.master.connect(this.context.destination); this.drone = this.context.createOscillator(); this.drone.type = 'sawtooth'; this.drone.frequency.value = 82; const droneGain = this.context.createGain(); droneGain.gain.value = 0.18; this.drone.connect(droneGain); droneGain.connect(this.master); this.drone.start(); this.shimmer = this.context.createOscillator(); this.shimmer.type = 'triangle'; this.shimmer.frequency.value = 164; const shimmerGain = this.context.createGain(); shimmerGain.gain.value = 0.08; this.shimmer.connect(shimmerGain); shimmerGain.connect(this.master); this.shimmer.start(); this.active = true; } setMood(fidelity: number, cycle: number, layerIndex: number): void { if (!this.context || !this.master || !this.drone || !this.shimmer) { return; } const now = this.context.currentTime; const base = 82 + cycle * 8 + layerIndex * 3; this.drone.frequency.linearRampToValueAtTime(base + fidelity * 18, now + 0.18); this.shimmer.frequency.linearRampToValueAtTime(base * 2 + (1 - fidelity) * 70, now + 0.22); this.master.gain.linearRampToValueAtTime(0.045 + fidelity * 0.04, now + 0.25); } pulseJump(boost = false): void { if (!this.context || !this.master) { return; } const osc = this.context.createOscillator(); const gain = this.context.createGain(); osc.type = boost ? 'square' : 'sine'; osc.frequency.value = boost ? 420 : 260; gain.gain.value = 0.0001; osc.connect(gain); gain.connect(this.master); const now = this.context.currentTime; gain.gain.exponentialRampToValueAtTime(0.06, now + 0.01); gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.24); osc.frequency.exponentialRampToValueAtTime(boost ? 170 : 120, now + 0.24); osc.start(now); osc.stop(now + 0.26); } pulseSnap(): void { if (!this.context || !this.master) { return; } const osc = this.context.createOscillator(); const gain = this.context.createGain(); osc.type = 'triangle'; osc.frequency.value = 680; gain.gain.value = 0.0001; osc.connect(gain); gain.connect(this.master); const now = this.context.currentTime; gain.gain.exponentialRampToValueAtTime(0.08, now + 0.01); gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.45); osc.frequency.exponentialRampToValueAtTime(240, now + 0.45); osc.start(now); osc.stop(now + 0.5); } }