/* ============================================================ DSP ARCADE — chiptune audio engine (Web Audio API) Procedural 8-bit SFX + looping background music. No files. ============================================================ */ (function () { 'use strict'; let actx = null; let master = null; let musicGain = null; let sfxGain = null; let muted = false; let musicTimer = null; let musicStep = 0; let currentTrack = null; function ensure() { if (actx) return actx; actx = new (window.AudioContext || window.webkitAudioContext)(); master = actx.createGain(); master.gain.value = 0.5; master.connect(actx.destination); musicGain = actx.createGain(); musicGain.gain.value = 0.35; musicGain.connect(master); sfxGain = actx.createGain(); sfxGain.gain.value = 0.6; sfxGain.connect(master); return actx; } function resume() { const a = ensure(); if (a.state === 'suspended') a.resume(); } // Unlock on first user gesture window.addEventListener('keydown', resume, { once: false }); window.addEventListener('pointerdown', resume, { once: false }); // ---------- SFX ---------- function blip(freq, dur, type = 'square', vol = 0.5, slide = 0) { const a = ensure(); const t = a.currentTime; const o = a.createOscillator(); const g = a.createGain(); o.type = type; o.frequency.setValueAtTime(freq, t); if (slide) o.frequency.exponentialRampToValueAtTime(Math.max(30, freq + slide), t + dur); g.gain.setValueAtTime(vol, t); g.gain.exponentialRampToValueAtTime(0.001, t + dur); o.connect(g); g.connect(sfxGain); o.start(t); o.stop(t + dur + 0.02); } function noise(dur, vol = 0.4, filterFreq = 1000) { const a = ensure(); const t = a.currentTime; const len = Math.floor(a.sampleRate * dur); const buf = a.createBuffer(1, len, a.sampleRate); const d = buf.getChannelData(0); for (let i = 0; i < len; i++) d[i] = Math.random() * 2 - 1; const src = a.createBufferSource(); src.buffer = buf; const f = a.createBiquadFilter(); f.type = 'lowpass'; f.frequency.value = filterFreq; const g = a.createGain(); g.gain.setValueAtTime(vol, t); g.gain.exponentialRampToValueAtTime(0.001, t + dur); src.connect(f); f.connect(g); g.connect(sfxGain); src.start(t); } const SFX = { shoot: () => { blip(880, 0.08, 'square', 0.35, -600); }, laser: () => { blip(1200, 0.1, 'sawtooth', 0.3, -900); }, explode: () => { noise(0.35, 0.5, 800); blip(120, 0.3, 'triangle', 0.5, -80); }, bigboom: () => { noise(0.6, 0.7, 500); blip(80, 0.5, 'triangle', 0.6, -50); }, powerup: () => { blip(523, 0.08, 'square', 0.4); setTimeout(() => blip(659, 0.08, 'square', 0.4), 80); setTimeout(() => blip(784, 0.12, 'square', 0.4), 160); }, hit: () => { blip(220, 0.06, 'square', 0.3, -100); }, brick: () => { blip(440, 0.05, 'square', 0.3, 200); }, wall: () => { blip(150, 0.05, 'triangle', 0.3); }, eat: () => { blip(660, 0.07, 'square', 0.35, 300); }, bonus: () => { blip(784, 0.07, 'square', 0.4); setTimeout(() => blip(1046, 0.1, 'square', 0.4), 70); }, levelup: () => { [523, 659, 784, 1046].forEach((f, i) => setTimeout(() => blip(f, 0.12, 'square', 0.4), i * 90)); }, gameover: () => { [440, 349, 293, 220, 174].forEach((f, i) => setTimeout(() => blip(f, 0.22, 'triangle', 0.5), i * 160)); }, start: () => { [262, 392, 523, 784].forEach((f, i) => setTimeout(() => blip(f, 0.1, 'square', 0.4), i * 70)); }, select: () => { blip(988, 0.06, 'square', 0.3); }, thrust: () => { noise(0.08, 0.12, 400); }, jump: () => { blip(300, 0.2, 'sawtooth', 0.3, 700); }, die: () => { blip(400, 0.4, 'sawtooth', 0.4, -350); }, }; function sfx(name) { if (muted) return; try { if (SFX[name]) SFX[name](); } catch (e) { /* audio not ready */ } } // ---------- Music ---------- const N = null; const TRACKS = { menu: { bpm: 110, bass: [65.4, N, 65.4, N, 98, N, 98, N, 73.4, N, 73.4, N, 87.3, N, 87.3, N], lead: [N, N, 261.6, N, 329.6, N, 392, N, 349.2, N, 349.2, N, 440, N, 392, N], }, asteroid: { bpm: 132, bass: [55, N, 55, 55, N, 55, 65.4, N, 55, N, 55, 55, N, 55, 49, N], lead: [N, 220, N, 261.6, N, 293.7, N, 329.6, N, 293.7, N, 261.6, N, 220, N, 196], }, breakout: { bpm: 140, bass: [82.4, N, 82.4, N, 98, N, 98, N, 110, N, 110, N, 98, N, 82.4, N], lead: [329.6, N, 392, N, 440, N, 523.3, N, 440, N, 392, N, 329.6, N, 293.7, N], }, snake: { bpm: 124, bass: [73.4, N, 73.4, N, 87.3, N, 87.3, N, 98, N, 98, N, 110, N, 110, N], lead: [N, N, 293.7, N, 349.2, N, 392, N, 440, N, 392, N, 349.2, N, 293.7, N], }, invader: { bpm: 128, bass: [61.7, N, 61.7, N, 73.4, N, 73.4, N, 82.4, N, 82.4, N, 98, N, 98, N], lead: [N, 246.9, N, 293.7, N, 329.6, N, 392, N, 329.6, N, 293.7, N, 246.9, N, 220], }, }; function playNote(freq, when, dur, type, dest, vol) { const a = ensure(); const o = a.createOscillator(); const g = a.createGain(); o.type = type; o.frequency.value = freq; g.gain.setValueAtTime(vol, when); g.gain.setValueAtTime(vol, when + dur * 0.7); g.gain.exponentialRampToValueAtTime(0.001, when + dur); o.connect(g); g.connect(dest); o.start(when); o.stop(when + dur + 0.02); } function scheduleMusic() { if (!currentTrack || muted) return; const track = TRACKS[currentTrack]; if (!track) return; const a = ensure(); const spb = 60 / track.bpm / 2; const now = a.currentTime + 0.05; for (let i = 0; i < track.bass.length; i++) { const step = musicStep + i; const t = now + i * spb; if (track.bass[i]) playNote(track.bass[i], t, spb * 0.9, 'triangle', musicGain, 0.5); if (track.lead[i]) playNote(track.lead[i], t, spb * 0.85, 'square', musicGain, 0.22); if (i % 2 === 1) { const len = Math.floor(a.sampleRate * 0.03); const buf = a.createBuffer(1, len, a.sampleRate); const d = buf.getChannelData(0); for (let j = 0; j < len; j++) d[j] = (Math.random() * 2 - 1) * 0.3; const src = a.createBufferSource(); src.buffer = buf; const f = a.createBiquadFilter(); f.type = 'highpass'; f.frequency.value = 6000; const g = a.createGain(); g.gain.value = 0.08; src.connect(f); f.connect(g); g.connect(musicGain); src.start(t); } } musicStep += track.bass.length; musicTimer = setTimeout(scheduleMusic, track.bpm / 60 * track.bass.length * 1000 * 0.92); } function playMusic(name) { stopMusic(); if (!TRACKS[name]) return; currentTrack = name; musicStep = 0; if (!muted) scheduleMusic(); } function stopMusic() { if (musicTimer) { clearTimeout(musicTimer); musicTimer = null; } currentTrack = null; } function setMuted(m) { muted = m; if (master) master.gain.value = m ? 0 : 0.5; if (m) stopMusic(); else if (currentTrack) scheduleMusic(); } window.DSP_AUDIO = { sfx, playMusic, stopMusic, setMuted, isMuted: () => muted, toggleMute: () => { setMuted(!muted); return muted; }, resume, }; })();