/** * Synthesizer for Drone Rotor Hum and Tactical HUD Audio FX * Uses Web Audio API without external audio file dependencies */ class DroneAudioSystem { constructor() { this.ctx = null; this.enabled = false; this.leaderGain = null; this.followerGain = null; this.leaderOsc1 = null; this.leaderOsc2 = null; this.followerOsc1 = null; this.followerOsc2 = null; } init() { if (this.ctx) return; try { const AudioContext = window.AudioContext || window.webkitAudioContext; this.ctx = new AudioContext(); // Master compressor to prevent clipping const compressor = this.ctx.createDynamicsCompressor(); compressor.threshold.setValueAtTime(-18, this.ctx.currentTime); compressor.knee.setValueAtTime(30, this.ctx.currentTime); compressor.ratio.setValueAtTime(12, this.ctx.currentTime); compressor.attack.setValueAtTime(0.003, this.ctx.currentTime); compressor.release.setValueAtTime(0.25, this.ctx.currentTime); compressor.connect(this.ctx.destination); this.masterGain = this.ctx.createGain(); this.masterGain.gain.setValueAtTime(0.2, this.ctx.currentTime); this.masterGain.connect(compressor); // Leader Drone sound synthesizer (Dual harmonic oscillator + lowpass filter) this.leaderFilter = this.ctx.createBiquadFilter(); this.leaderFilter.type = "lowpass"; this.leaderFilter.frequency.setValueAtTime(650, this.ctx.currentTime); this.leaderGain = this.ctx.createGain(); this.leaderGain.gain.setValueAtTime(0.0, this.ctx.currentTime); this.leaderGain.connect(this.leaderFilter); this.leaderFilter.connect(this.masterGain); this.leaderOsc1 = this.ctx.createOscillator(); this.leaderOsc1.type = "sawtooth"; this.leaderOsc1.frequency.setValueAtTime(95, this.ctx.currentTime); this.leaderOsc2 = this.ctx.createOscillator(); this.leaderOsc2.type = "triangle"; this.leaderOsc2.frequency.setValueAtTime(190, this.ctx.currentTime); this.leaderOsc1.connect(this.leaderGain); this.leaderOsc2.connect(this.leaderGain); this.leaderOsc1.start(); this.leaderOsc2.start(); // Follower Drone sound synthesizer this.followerFilter = this.ctx.createBiquadFilter(); this.followerFilter.type = "lowpass"; this.followerFilter.frequency.setValueAtTime(600, this.ctx.currentTime); this.followerGain = this.ctx.createGain(); this.followerGain.gain.setValueAtTime(0.0, this.ctx.currentTime); this.followerGain.connect(this.followerFilter); this.followerFilter.connect(this.masterGain); this.followerOsc1 = this.ctx.createOscillator(); this.followerOsc1.type = "sawtooth"; this.followerOsc1.frequency.setValueAtTime(98, this.ctx.currentTime); this.followerOsc2 = this.ctx.createOscillator(); this.followerOsc2.type = "triangle"; this.followerOsc2.frequency.setValueAtTime(196, this.ctx.currentTime); this.followerOsc1.connect(this.followerGain); this.followerOsc2.connect(this.followerGain); this.followerOsc1.start(); this.followerOsc2.start(); this.enabled = true; } catch (e) { console.warn("Web Audio not supported or blocked", e); } } toggle() { if (!this.ctx) { this.init(); return true; } if (this.ctx.state === 'suspended') { this.ctx.resume(); this.enabled = true; return true; } this.enabled = !this.enabled; if (!this.enabled && this.leaderGain && this.followerGain) { this.leaderGain.gain.setValueAtTime(0.0, this.ctx.currentTime); this.followerGain.gain.setValueAtTime(0.0, this.ctx.currentTime); } return this.enabled; } updateMotors(leaderRpm, followerRpm, speedLeader, speedFollower) { if (!this.enabled || !this.ctx || this.ctx.state !== 'running') return; const now = this.ctx.currentTime; // Pitch proportional to RPM and velocity const baseFreq1 = 85 + (speedLeader * 2.5) + ((leaderRpm - 4500) * 0.015); const baseFreq2 = 88 + (speedFollower * 2.5) + ((followerRpm - 4500) * 0.015); this.leaderOsc1.frequency.setTargetAtTime(Math.max(60, Math.min(220, baseFreq1)), now, 0.08); this.leaderOsc2.frequency.setTargetAtTime(Math.max(120, Math.min(440, baseFreq1 * 2)), now, 0.08); this.followerOsc1.frequency.setTargetAtTime(Math.max(60, Math.min(220, baseFreq2)), now, 0.08); this.followerOsc2.frequency.setTargetAtTime(Math.max(120, Math.min(440, baseFreq2 * 2)), now, 0.08); this.leaderGain.gain.setTargetAtTime(0.08, now, 0.1); this.followerGain.gain.setTargetAtTime(0.08, now, 0.1); } playBeep(freq = 880, duration = 0.08) { if (!this.enabled || !this.ctx) return; try { const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = "sine"; osc.frequency.setValueAtTime(freq, this.ctx.currentTime); gain.gain.setValueAtTime(0.08, this.ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + duration); osc.connect(gain); gain.connect(this.masterGain); osc.start(); osc.stop(this.ctx.currentTime + duration); } catch (e) {} } } window.droneAudio = new DroneAudioSystem();