/** * CHAMPIONS OF LUNERARIA - Ambient Audio Engine * Pure Web Audio API Synthesizer (No external sound files, 100% offline & local) * Provides an atmospheric fantasy soundscape with gentle bass drone & crystalline pads. */ class LuneAudioEngine { constructor() { this.ctx = null; this.isPlaying = false; this.masterGain = null; this.nodes = []; this.intervalId = null; } init() { if (this.ctx) return; const AudioContextClass = window.AudioContext || window.webkitAudioContext; if (!AudioContextClass) return; this.ctx = new AudioContextClass(); this.masterGain = this.ctx.createGain(); this.masterGain.gain.setValueAtTime(0, this.ctx.currentTime); this.masterGain.connect(this.ctx.destination); } toggle() { this.init(); if (!this.ctx) return false; if (this.ctx.state === 'suspended') { this.ctx.resume(); } if (this.isPlaying) { this.stop(); return false; } else { this.start(); return true; } } start() { if (this.isPlaying) return; this.isPlaying = true; const now = this.ctx.currentTime; // Master fade in this.masterGain.gain.cancelScheduledValues(now); this.masterGain.gain.linearRampToValueAtTime(0.18, now + 3); // 1. Warm Sub Drone (A1: 55Hz) const droneOsc = this.ctx.createOscillator(); droneOsc.type = 'sawtooth'; droneOsc.frequency.setValueAtTime(55, now); const droneFilter = this.ctx.createBiquadFilter(); droneFilter.type = 'lowpass'; droneFilter.frequency.setValueAtTime(120, now); const droneGain = this.ctx.createGain(); droneGain.gain.setValueAtTime(0.6, now); droneOsc.connect(droneFilter); droneFilter.connect(droneGain); droneGain.connect(this.masterGain); droneOsc.start(); this.nodes.push(droneOsc); // 2. Harmonic Ethereal Pad (A2 / E3 chords) const padFrequencies = [110, 164.81, 220, 277.18]; padFrequencies.forEach((freq, i) => { const osc = this.ctx.createOscillator(); osc.type = 'sine'; osc.frequency.setValueAtTime(freq, now); // Subtle detune for shimmer osc.detune.setValueAtTime((i - 1.5) * 4, now); const pFilter = this.ctx.createBiquadFilter(); pFilter.type = 'bandpass'; pFilter.frequency.setValueAtTime(freq * 1.5, now); pFilter.Q.setValueAtTime(2.0, now); const pGain = this.ctx.createGain(); pGain.gain.setValueAtTime(0.25 / padFrequencies.length, now); osc.connect(pFilter); pFilter.connect(pGain); pGain.connect(this.masterGain); osc.start(); this.nodes.push(osc); }); // 3. Periodic Shimmer Bells (Crystalline Moon Chimes) this.startShimmerBells(); } startShimmerBells() { const bellNotes = [440, 554.37, 659.25, 830.61, 880, 1108.73]; this.intervalId = setInterval(() => { if (!this.isPlaying || !this.ctx) return; const note = bellNotes[Math.floor(Math.random() * bellNotes.length)]; this.playChime(note); }, 2800); } playChime(freq) { if (!this.ctx) return; const now = this.ctx.currentTime; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = 'triangle'; osc.frequency.setValueAtTime(freq, now); gain.gain.setValueAtTime(0.001, now); gain.gain.exponentialRampToValueAtTime(0.04, now + 0.08); gain.gain.exponentialRampToValueAtTime(0.0001, now + 2.5); osc.connect(gain); gain.connect(this.masterGain); osc.start(now); osc.stop(now + 2.6); } stop() { if (!this.isPlaying) return; this.isPlaying = false; const now = this.ctx.currentTime; // Fade out this.masterGain.gain.cancelScheduledValues(now); this.masterGain.gain.linearRampToValueAtTime(0.0001, now + 1.5); if (this.intervalId) { clearInterval(this.intervalId); this.intervalId = null; } setTimeout(() => { this.nodes.forEach(n => { try { n.stop(); n.disconnect(); } catch (e) {} }); this.nodes = []; }, 1600); } } window.luneAudio = new LuneAudioEngine();