/** * DOOM 3D - Retro Sound Synthesizer & Music Sequencer (Web Audio API) * Generates authentic 90s DOOM-inspired sound effects and the iconic E1M1 metal riff. */ class SoundSystem { constructor() { this.ctx = null; this.masterGain = null; this.sfxGain = null; this.musicGain = null; this.initialized = false; this.sfxVolume = 0.8; this.musicVolume = 0.5; this.masterVolume = 1.0; this.musicPlaying = false; this.musicStep = 0; this.musicTimer = null; this.musicBpm = 135; } init() { if (this.initialized) return; try { const AudioContext = window.AudioContext || window.webkitAudioContext; this.ctx = new AudioContext(); this.masterGain = this.ctx.createGain(); this.masterGain.gain.value = this.masterVolume; this.masterGain.connect(this.ctx.destination); this.sfxGain = this.ctx.createGain(); this.sfxGain.gain.value = this.sfxVolume; this.sfxGain.connect(this.masterGain); this.musicGain = this.ctx.createGain(); this.musicGain.gain.value = this.musicVolume; this.musicGain.connect(this.masterGain); this.initialized = true; this.startMusic(); } catch (e) { console.warn('Web Audio API not supported or blocked:', e); } } resume() { if (this.ctx && this.ctx.state === 'suspended') { this.ctx.resume(); } if (!this.initialized) { this.init(); } } setMasterVolume(val) { this.masterVolume = Math.max(0, Math.min(1, val)); if (this.masterGain) this.masterGain.gain.setValueAtTime(this.masterVolume, this.ctx.currentTime); } setSfxVolume(val) { this.sfxVolume = Math.max(0, Math.min(1, val)); if (this.sfxGain) this.sfxGain.gain.setValueAtTime(this.sfxVolume, this.ctx.currentTime); } setMusicVolume(val) { this.musicVolume = Math.max(0, Math.min(1, val)); if (this.musicGain) this.musicGain.gain.setValueAtTime(this.musicVolume, this.ctx.currentTime); } // Helper: Create White Noise buffer createNoiseBuffer(duration = 1) { if (!this.ctx) return null; const bufferSize = this.ctx.sampleRate * duration; const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate); const data = buffer.getChannelData(0); for (let i = 0; i < bufferSize; i++) { data[i] = Math.random() * 2 - 1; } return buffer; } // Distortion curve for heavy metal tone makeDistortionCurve(amount = 50) { const k = typeof amount === 'number' ? amount : 50; const n_samples = 44100; const curve = new Float32Array(n_samples); const deg = Math.PI / 180; for (let i = 0; i < n_samples; ++i) { const x = (i * 2) / n_samples - 1; curve[i] = ((3 + k) * x * 20 * deg) / (Math.PI + k * Math.abs(x)); } return curve; } // ---------------------------------------------------- // WEAPON SOUND EFFECTS // ---------------------------------------------------- playFist() { if (!this.ctx) return; const t = this.ctx.currentTime; // Swoosh const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = 'triangle'; osc.frequency.setValueAtTime(120, t); osc.frequency.exponentialRampToValueAtTime(40, t + 0.15); gain.gain.setValueAtTime(0.6, t); gain.gain.exponentialRampToValueAtTime(0.01, t + 0.15); osc.connect(gain); gain.connect(this.sfxGain); osc.start(t); osc.stop(t + 0.16); // Impact punch const noise = this.ctx.createBufferSource(); noise.buffer = this.createNoiseBuffer(0.12); const filter = this.ctx.createBiquadFilter(); filter.type = 'lowpass'; filter.frequency.setValueAtTime(800, t); filter.frequency.exponentialRampToValueAtTime(100, t + 0.12); const nGain = this.ctx.createGain(); nGain.gain.setValueAtTime(0.8, t); nGain.gain.exponentialRampToValueAtTime(0.01, t + 0.12); noise.connect(filter); filter.connect(nGain); nGain.connect(this.sfxGain); noise.start(t); } playPistol() { if (!this.ctx) return; const t = this.ctx.currentTime; // Punch osc const osc = this.ctx.createOscillator(); const oGain = this.ctx.createGain(); osc.type = 'sawtooth'; osc.frequency.setValueAtTime(320, t); osc.frequency.exponentialRampToValueAtTime(45, t + 0.18); oGain.gain.setValueAtTime(0.7, t); oGain.gain.exponentialRampToValueAtTime(0.01, t + 0.18); osc.connect(oGain); oGain.connect(this.sfxGain); osc.start(t); osc.stop(t + 0.19); // Noise crack const noise = this.ctx.createBufferSource(); noise.buffer = this.createNoiseBuffer(0.2); const filter = this.ctx.createBiquadFilter(); filter.type = 'bandpass'; filter.frequency.setValueAtTime(1800, t); filter.frequency.exponentialRampToValueAtTime(300, t + 0.2); const nGain = this.ctx.createGain(); nGain.gain.setValueAtTime(0.9, t); nGain.gain.exponentialRampToValueAtTime(0.01, t + 0.2); noise.connect(filter); filter.connect(nGain); nGain.connect(this.sfxGain); noise.start(t); } playShotgun() { if (!this.ctx) return; const t = this.ctx.currentTime; // Low boom const osc = this.ctx.createOscillator(); const oGain = this.ctx.createGain(); osc.type = 'triangle'; osc.frequency.setValueAtTime(200, t); osc.frequency.exponentialRampToValueAtTime(30, t + 0.35); oGain.gain.setValueAtTime(1.0, t); oGain.gain.exponentialRampToValueAtTime(0.01, t + 0.35); osc.connect(oGain); oGain.connect(this.sfxGain); osc.start(t); osc.stop(t + 0.36); // Heavy blast noise const noise = this.ctx.createBufferSource(); noise.buffer = this.createNoiseBuffer(0.35); const filter = this.ctx.createBiquadFilter(); filter.type = 'lowpass'; filter.frequency.setValueAtTime(2800, t); filter.frequency.exponentialRampToValueAtTime(400, t + 0.35); const nGain = this.ctx.createGain(); nGain.gain.setValueAtTime(1.2, t); nGain.gain.exponentialRampToValueAtTime(0.01, t + 0.35); noise.connect(filter); filter.connect(nGain); nGain.connect(this.sfxGain); noise.start(t); // Pump action slide (clack-clack) after 0.35s setTimeout(() => { if (!this.ctx) return; const t2 = this.ctx.currentTime; const p1 = this.ctx.createOscillator(); const pGain = this.ctx.createGain(); p1.type = 'square'; p1.frequency.setValueAtTime(400, t2); p1.frequency.setValueAtTime(650, t2 + 0.06); pGain.gain.setValueAtTime(0.3, t2); pGain.gain.exponentialRampToValueAtTime(0.01, t2 + 0.15); p1.connect(pGain); pGain.connect(this.sfxGain); p1.start(t2); p1.stop(t2 + 0.16); }, 320); } playChaingun() { if (!this.ctx) return; const t = this.ctx.currentTime; const osc = this.ctx.createOscillator(); const oGain = this.ctx.createGain(); osc.type = 'sawtooth'; osc.frequency.setValueAtTime(260 + Math.random() * 40, t); osc.frequency.exponentialRampToValueAtTime(50, t + 0.12); oGain.gain.setValueAtTime(0.65, t); oGain.gain.exponentialRampToValueAtTime(0.01, t + 0.12); osc.connect(oGain); oGain.connect(this.sfxGain); osc.start(t); osc.stop(t + 0.13); const noise = this.ctx.createBufferSource(); noise.buffer = this.createNoiseBuffer(0.12); const filter = this.ctx.createBiquadFilter(); filter.type = 'bandpass'; filter.frequency.setValueAtTime(2200, t); const nGain = this.ctx.createGain(); nGain.gain.setValueAtTime(0.7, t); nGain.gain.exponentialRampToValueAtTime(0.01, t + 0.12); noise.connect(filter); filter.connect(nGain); nGain.connect(this.sfxGain); noise.start(t); } playRocketLaunch() { if (!this.ctx) return; const t = this.ctx.currentTime; // Swoosh tone const osc = this.ctx.createOscillator(); const oGain = this.ctx.createGain(); osc.type = 'sawtooth'; osc.frequency.setValueAtTime(100, t); osc.frequency.exponentialRampToValueAtTime(450, t + 0.25); osc.frequency.exponentialRampToValueAtTime(120, t + 0.5); oGain.gain.setValueAtTime(0.7, t); oGain.gain.exponentialRampToValueAtTime(0.01, t + 0.5); osc.connect(oGain); oGain.connect(this.sfxGain); osc.start(t); osc.stop(t + 0.51); // Rocket booster hiss const noise = this.ctx.createBufferSource(); noise.buffer = this.createNoiseBuffer(0.4); const filter = this.ctx.createBiquadFilter(); filter.type = 'bandpass'; filter.frequency.setValueAtTime(800, t); filter.frequency.linearRampToValueAtTime(1600, t + 0.4); const nGain = this.ctx.createGain(); nGain.gain.setValueAtTime(0.8, t); nGain.gain.exponentialRampToValueAtTime(0.01, t + 0.4); noise.connect(filter); filter.connect(nGain); nGain.connect(this.sfxGain); noise.start(t); } playExplosion(volumeMultiplier = 1.0) { if (!this.ctx) return; const t = this.ctx.currentTime; // Sub bass boom const osc = this.ctx.createOscillator(); const oGain = this.ctx.createGain(); osc.type = 'triangle'; osc.frequency.setValueAtTime(160, t); osc.frequency.exponentialRampToValueAtTime(25, t + 0.7); oGain.gain.setValueAtTime(1.2 * volumeMultiplier, t); oGain.gain.exponentialRampToValueAtTime(0.01, t + 0.7); osc.connect(oGain); oGain.connect(this.sfxGain); osc.start(t); osc.stop(t + 0.71); // Crumbling explosion noise const noise = this.ctx.createBufferSource(); noise.buffer = this.createNoiseBuffer(0.8); const filter = this.ctx.createBiquadFilter(); filter.type = 'lowpass'; filter.frequency.setValueAtTime(1400, t); filter.frequency.exponentialRampToValueAtTime(120, t + 0.8); const nGain = this.ctx.createGain(); nGain.gain.setValueAtTime(1.4 * volumeMultiplier, t); nGain.gain.exponentialRampToValueAtTime(0.01, t + 0.8); noise.connect(filter); filter.connect(nGain); nGain.connect(this.sfxGain); noise.start(t); } playPlasma() { if (!this.ctx) return; const t = this.ctx.currentTime; const osc = this.ctx.createOscillator(); const oGain = this.ctx.createGain(); osc.type = 'sawtooth'; osc.frequency.setValueAtTime(950, t); osc.frequency.exponentialRampToValueAtTime(180, t + 0.16); oGain.gain.setValueAtTime(0.7, t); oGain.gain.exponentialRampToValueAtTime(0.01, t + 0.16); osc.connect(oGain); oGain.connect(this.sfxGain); osc.start(t); osc.stop(t + 0.17); const osc2 = this.ctx.createOscillator(); const oGain2 = this.ctx.createGain(); osc2.type = 'sine'; osc2.frequency.setValueAtTime(1400, t); osc2.frequency.exponentialRampToValueAtTime(300, t + 0.14); oGain2.gain.setValueAtTime(0.5, t); oGain2.gain.exponentialRampToValueAtTime(0.01, t + 0.14); osc2.connect(oGain2); oGain2.connect(this.sfxGain); osc2.start(t); osc2.stop(t + 0.15); } playBFGCharge() { if (!this.ctx) return; const t = this.ctx.currentTime; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = 'sawtooth'; osc.frequency.setValueAtTime(100, t); osc.frequency.exponentialRampToValueAtTime(1200, t + 0.8); gain.gain.setValueAtTime(0.1, t); gain.gain.linearRampToValueAtTime(0.9, t + 0.8); osc.connect(gain); gain.connect(this.sfxGain); osc.start(t); osc.stop(t + 0.82); } playBFGFire() { if (!this.ctx) return; const t = this.ctx.currentTime; // Big apocalyptic energy discharge const osc = this.ctx.createOscillator(); const oGain = this.ctx.createGain(); osc.type = 'sawtooth'; osc.frequency.setValueAtTime(800, t); osc.frequency.exponentialRampToValueAtTime(60, t + 0.9); oGain.gain.setValueAtTime(1.3, t); oGain.gain.exponentialRampToValueAtTime(0.01, t + 0.9); osc.connect(oGain); oGain.connect(this.sfxGain); osc.start(t); osc.stop(t + 0.92); const noise = this.ctx.createBufferSource(); noise.buffer = this.createNoiseBuffer(1.0); const filter = this.ctx.createBiquadFilter(); filter.type = 'bandpass'; filter.frequency.setValueAtTime(3000, t); filter.frequency.exponentialRampToValueAtTime(200, t + 1.0); const nGain = this.ctx.createGain(); nGain.gain.setValueAtTime(1.5, t); nGain.gain.exponentialRampToValueAtTime(0.01, t + 1.0); noise.connect(filter); filter.connect(nGain); nGain.connect(this.sfxGain); noise.start(t); } // ---------------------------------------------------- // MONSTER & WORLD SOUND EFFECTS // ---------------------------------------------------- playEnemyAlert(type = 'zombie') { if (!this.ctx) return; const t = this.ctx.currentTime; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); if (type === 'pinky' || type === 'caco') { osc.type = 'sawtooth'; osc.frequency.setValueAtTime(140, t); osc.frequency.linearRampToValueAtTime(220, t + 0.15); osc.frequency.exponentialRampToValueAtTime(60, t + 0.4); gain.gain.setValueAtTime(0.7, t); gain.gain.exponentialRampToValueAtTime(0.01, t + 0.4); osc.connect(gain); gain.connect(this.sfxGain); osc.start(t); osc.stop(t + 0.42); } else { // Zombie / Imp grunt osc.type = 'sawtooth'; osc.frequency.setValueAtTime(220, t); osc.frequency.exponentialRampToValueAtTime(90, t + 0.28); gain.gain.setValueAtTime(0.6, t); gain.gain.exponentialRampToValueAtTime(0.01, t + 0.28); osc.connect(gain); gain.connect(this.sfxGain); osc.start(t); osc.stop(t + 0.3); } } playEnemyPain() { if (!this.ctx) return; const t = this.ctx.currentTime; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = 'sawtooth'; osc.frequency.setValueAtTime(180, t); osc.frequency.exponentialRampToValueAtTime(70, t + 0.18); gain.gain.setValueAtTime(0.6, t); gain.gain.exponentialRampToValueAtTime(0.01, t + 0.18); osc.connect(gain); gain.connect(this.sfxGain); osc.start(t); osc.stop(t + 0.19); } playEnemyDeath() { if (!this.ctx) return; const t = this.ctx.currentTime; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = 'sawtooth'; osc.frequency.setValueAtTime(130, t); osc.frequency.exponentialRampToValueAtTime(30, t + 0.45); gain.gain.setValueAtTime(0.8, t); gain.gain.exponentialRampToValueAtTime(0.01, t + 0.45); osc.connect(gain); gain.connect(this.sfxGain); osc.start(t); osc.stop(t + 0.46); // Gurgle const noise = this.ctx.createBufferSource(); noise.buffer = this.createNoiseBuffer(0.35); const filter = this.ctx.createBiquadFilter(); filter.type = 'bandpass'; filter.frequency.setValueAtTime(600, t); const nGain = this.ctx.createGain(); nGain.gain.setValueAtTime(0.7, t); nGain.gain.exponentialRampToValueAtTime(0.01, t + 0.35); noise.connect(filter); filter.connect(nGain); nGain.connect(this.sfxGain); noise.start(t); } playFireball() { if (!this.ctx) return; const t = this.ctx.currentTime; const noise = this.ctx.createBufferSource(); noise.buffer = this.createNoiseBuffer(0.3); const filter = this.ctx.createBiquadFilter(); filter.type = 'bandpass'; filter.frequency.setValueAtTime(1200, t); filter.frequency.exponentialRampToValueAtTime(300, t + 0.3); const gain = this.ctx.createGain(); gain.gain.setValueAtTime(0.8, t); gain.gain.exponentialRampToValueAtTime(0.01, t + 0.3); noise.connect(filter); filter.connect(gain); gain.connect(this.sfxGain); noise.start(t); } playItemPickup(type = 'ammo') { if (!this.ctx) return; const t = this.ctx.currentTime; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = 'sine'; if (type === 'health' || type === 'armor') { osc.frequency.setValueAtTime(440, t); osc.frequency.setValueAtTime(554.37, t + 0.06); osc.frequency.setValueAtTime(659.25, t + 0.12); gain.gain.setValueAtTime(0.5, t); gain.gain.exponentialRampToValueAtTime(0.01, t + 0.25); osc.connect(gain); gain.connect(this.sfxGain); osc.start(t); osc.stop(t + 0.26); } else if (type === 'weapon' || type === 'key') { // Fanfare chime osc.frequency.setValueAtTime(330, t); osc.frequency.setValueAtTime(440, t + 0.08); osc.frequency.setValueAtTime(660, t + 0.16); osc.frequency.setValueAtTime(880, t + 0.24); gain.gain.setValueAtTime(0.7, t); gain.gain.exponentialRampToValueAtTime(0.01, t + 0.45); osc.connect(gain); gain.connect(this.sfxGain); osc.start(t); osc.stop(t + 0.46); } else { // Ammo / click osc.frequency.setValueAtTime(600, t); osc.frequency.setValueAtTime(900, t + 0.05); gain.gain.setValueAtTime(0.4, t); gain.gain.exponentialRampToValueAtTime(0.01, t + 0.12); osc.connect(gain); gain.connect(this.sfxGain); osc.start(t); osc.stop(t + 0.13); } } playDoor(open = true) { if (!this.ctx) return; const t = this.ctx.currentTime; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = 'sawtooth'; const startFreq = open ? 90 : 150; const endFreq = open ? 160 : 70; osc.frequency.setValueAtTime(startFreq, t); osc.frequency.linearRampToValueAtTime(endFreq, t + 0.6); gain.gain.setValueAtTime(0.4, t); gain.gain.exponentialRampToValueAtTime(0.01, t + 0.6); const filter = this.ctx.createBiquadFilter(); filter.type = 'lowpass'; filter.frequency.setValueAtTime(450, t); osc.connect(filter); filter.connect(gain); gain.connect(this.sfxGain); osc.start(t); osc.stop(t + 0.62); } playPlayerPain() { if (!this.ctx) return; const t = this.ctx.currentTime; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = 'triangle'; osc.frequency.setValueAtTime(200, t); osc.frequency.exponentialRampToValueAtTime(80, t + 0.22); gain.gain.setValueAtTime(0.9, t); gain.gain.exponentialRampToValueAtTime(0.01, t + 0.22); osc.connect(gain); gain.connect(this.sfxGain); osc.start(t); osc.stop(t + 0.23); } playSlimeHurt() { if (!this.ctx) return; const t = this.ctx.currentTime; const noise = this.ctx.createBufferSource(); noise.buffer = this.createNoiseBuffer(0.2); const filter = this.ctx.createBiquadFilter(); filter.type = 'bandpass'; filter.frequency.setValueAtTime(3200, t); filter.Q.setValueAtTime(3, t); const gain = this.ctx.createGain(); gain.gain.setValueAtTime(0.5, t); gain.gain.exponentialRampToValueAtTime(0.01, t + 0.2); noise.connect(filter); filter.connect(gain); gain.connect(this.sfxGain); noise.start(t); } // ---------------------------------------------------- // E1M1 RETRO SYNTHESIZER SOUNDTRACK // ---------------------------------------------------- startMusic() { if (this.musicPlaying) return; this.musicPlaying = true; this.musicStep = 0; // E1M1 Notes in Hz (E Minor riff) const E1 = 41.20; const E2 = 82.41; const G2 = 98.00; const A2 = 110.00; const Bb2 = 116.54; const B2 = 123.47; const D3 = 146.83; const E3 = 164.81; // 32-step bass riff pattern for classic E1M1 // E E E3 E E D3 E E | C3 E E Bb2 E E B2 C3 const riffNotes = [ E2, E2, E3, E2, E2, D3, E2, E2, E3, E2, E2, D3, E2, E2, Bb2, B2, E2, E2, E3, E2, E2, D3, E2, E2, E3, E2, E2, G2, E2, A2, Bb2, B2 ]; const stepDuration = (60 / this.musicBpm) / 4; // 16th notes const scheduleStep = () => { if (!this.musicPlaying || !this.ctx) return; const now = this.ctx.currentTime; const note = riffNotes[this.musicStep % riffNotes.length]; const isHeavyBeat = (this.musicStep % 4 === 0); const isSnareBeat = (this.musicStep % 8 === 4); // Synth Distortion Bass/Guitar const osc = this.ctx.createOscillator(); const dist = this.ctx.createWaveShaper(); const filter = this.ctx.createBiquadFilter(); const gain = this.ctx.createGain(); osc.type = 'sawtooth'; osc.frequency.setValueAtTime(note, now); dist.curve = this.makeDistortionCurve(30); dist.oversample = '4x'; filter.type = 'lowpass'; filter.frequency.setValueAtTime(1400, now); filter.frequency.exponentialRampToValueAtTime(350, now + stepDuration * 0.9); gain.gain.setValueAtTime(0.35, now); gain.gain.exponentialRampToValueAtTime(0.01, now + stepDuration * 0.95); osc.connect(dist); dist.connect(filter); filter.connect(gain); gain.connect(this.musicGain); osc.start(now); osc.stop(now + stepDuration); // Kick Drum on quarter notes if (isHeavyBeat) { const kick = this.ctx.createOscillator(); const kGain = this.ctx.createGain(); kick.type = 'sine'; kick.frequency.setValueAtTime(140, now); kick.frequency.exponentialRampToValueAtTime(35, now + 0.12); kGain.gain.setValueAtTime(0.7, now); kGain.gain.exponentialRampToValueAtTime(0.01, now + 0.12); kick.connect(kGain); kGain.connect(this.musicGain); kick.start(now); kick.stop(now + 0.13); } // Snare Drum on 2 and 4 if (isSnareBeat) { const snare = this.ctx.createBufferSource(); snare.buffer = this.createNoiseBuffer(0.15); const sFilter = this.ctx.createBiquadFilter(); sFilter.type = 'highpass'; sFilter.frequency.setValueAtTime(1000, now); const sGain = this.ctx.createGain(); sGain.gain.setValueAtTime(0.45, now); sGain.gain.exponentialRampToValueAtTime(0.01, now + 0.15); snare.connect(sFilter); sFilter.connect(sGain); sGain.connect(this.musicGain); snare.start(now); } // Closed Hi-hat every 8th note if (this.musicStep % 2 === 0) { const hat = this.ctx.createBufferSource(); hat.buffer = this.createNoiseBuffer(0.04); const hFilter = this.ctx.createBiquadFilter(); hFilter.type = 'highpass'; hFilter.frequency.setValueAtTime(7000, now); const hGain = this.ctx.createGain(); hGain.gain.setValueAtTime(0.15, now); hGain.gain.exponentialRampToValueAtTime(0.01, now + 0.04); hat.connect(hFilter); hFilter.connect(hGain); hGain.connect(this.musicGain); hat.start(now); } this.musicStep++; this.musicTimer = setTimeout(scheduleStep, stepDuration * 1000); }; scheduleStep(); } stopMusic() { this.musicPlaying = false; if (this.musicTimer) { clearTimeout(this.musicTimer); this.musicTimer = null; } } toggleMusic() { if (this.musicPlaying) { this.stopMusic(); return false; } else { this.startMusic(); return true; } } } window.SoundSystem = SoundSystem;