class GameAudio { private ctx: AudioContext | null = null; private ambientOscillators: { osc: OscillatorNode; gain: GainNode }[] = []; private isAmbientPlaying = false; private initCtx() { if (!this.ctx) { const AudioCtx = window.AudioContext || (window as any).webkitAudioContext; if (AudioCtx) { this.ctx = new AudioCtx(); } } if (this.ctx && this.ctx.state === 'suspended') { this.ctx.resume(); } return this.ctx; } playPop() { const ctx = this.initCtx(); if (!ctx) return; const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); const now = ctx.currentTime; osc.type = 'sine'; // Quick pitch decay from 800Hz to 150Hz osc.frequency.setValueAtTime(800, now); osc.frequency.exponentialRampToValueAtTime(150, now + 0.08); // Fast volume envelope gain.gain.setValueAtTime(0.3, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.08); osc.start(now); osc.stop(now + 0.08); } playExplosion() { const ctx = this.initCtx(); if (!ctx) return; const now = ctx.currentTime; // Create a noise buffer (white noise) const bufferSize = ctx.sampleRate * 0.3; // 0.3 seconds const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate); const data = buffer.getChannelData(0); for (let i = 0; i < bufferSize; i++) { data[i] = Math.random() * 2 - 1; } const noise = ctx.createBufferSource(); noise.buffer = buffer; // Filter to make it sound like a rumble/thud explosion const filter = ctx.createBiquadFilter(); filter.type = 'lowpass'; filter.frequency.setValueAtTime(1000, now); filter.frequency.exponentialRampToValueAtTime(10, now + 0.3); const gain = ctx.createGain(); gain.gain.setValueAtTime(0.5, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.3); noise.connect(filter); filter.connect(gain); gain.connect(ctx.destination); noise.start(now); noise.stop(now + 0.3); // Add a low sine sub-bass drop for punch const sub = ctx.createOscillator(); const subGain = ctx.createGain(); sub.type = 'sine'; sub.frequency.setValueAtTime(120, now); sub.frequency.linearRampToValueAtTime(40, now + 0.2); subGain.gain.setValueAtTime(0.6, now); subGain.gain.exponentialRampToValueAtTime(0.001, now + 0.25); sub.connect(subGain); subGain.connect(ctx.destination); sub.start(now); sub.stop(now + 0.25); } playLaser() { const ctx = this.initCtx(); if (!ctx) return; const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); const now = ctx.currentTime; osc.type = 'sawtooth'; // Sweep frequency up rapidly osc.frequency.setValueAtTime(200, now); osc.frequency.exponentialRampToValueAtTime(1500, now + 0.12); // Fast volume envelope gain.gain.setValueAtTime(0.15, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.12); osc.start(now); osc.stop(now + 0.12); } playChime() { const ctx = this.initCtx(); if (!ctx) return; const now = ctx.currentTime; const notes = [523.25, 659.25, 783.99, 1046.50]; // C5, E5, G5, C6 (Arpeggio) notes.forEach((freq, index) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); const delay = index * 0.06; osc.connect(gain); gain.connect(ctx.destination); osc.type = 'triangle'; osc.frequency.setValueAtTime(freq, now + delay); gain.gain.setValueAtTime(0, now + delay); gain.gain.linearRampToValueAtTime(0.25, now + delay + 0.02); gain.gain.exponentialRampToValueAtTime(0.001, now + delay + 0.5); osc.start(now + delay); osc.stop(now + delay + 0.6); }); } startAmbient() { const ctx = this.initCtx(); if (!ctx || this.isAmbientPlaying) return; this.isAmbientPlaying = true; const now = ctx.currentTime; // A beautiful major pentatonic warm chord: C3, G3, C4, E4 const freqs = [130.81, 196.00, 261.63, 329.63]; this.ambientOscillators = freqs.map((freq) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'sine'; osc.frequency.value = freq; // Soft volume level gain.gain.setValueAtTime(0, now); gain.gain.linearRampToValueAtTime(0.025, now + 2.0); // Slow fade-in osc.connect(gain); gain.connect(ctx.destination); osc.start(now); return { osc, gain }; }); } stopAmbient() { if (!this.isAmbientPlaying) return; this.isAmbientPlaying = false; if (!this.ctx) return; const now = this.ctx.currentTime; this.ambientOscillators.forEach(({ osc, gain }) => { try { gain.gain.setValueAtTime(gain.gain.value, now); gain.gain.linearRampToValueAtTime(0, now + 1.5); // Slow fade-out setTimeout(() => { try { osc.stop(); } catch {} }, 1600); } catch {} }); this.ambientOscillators = []; } toggleAmbient() { if (this.isAmbientPlaying) { this.stopAmbient(); return false; } else { this.startAmbient(); return true; } } isPlayingAmbient() { return this.isAmbientPlaying; } } export const gameAudio = new GameAudio();