Orbit Survivors
audio.js7.8 KB
1// Orbit Survivors - WebAudio synthesised SFX + ambient pulse (no assets).2window.SFX = (() => {3 let ac = null, master, sb, mb, nbuf, muted = false, lastT = {}, nextBeat = 0, beat = 0;4 function init() {5 if (ac) return true;6 try {7 const AC = window.AudioContext || window.webkitAudioContext;8 if (!AC) return false;9 ac = new AC();10 const comp = ac.createDynamicsCompressor();11 comp.threshold.value = -16; comp.ratio.value = 6; comp.attack.value = 0.003; comp.release.value = 0.2;12 master = ac.createGain(); master.gain.value = muted ? 0 : 0.85;13 master.connect(comp); comp.connect(ac.destination);14 sb = ac.createGain(); sb.gain.value = 0.75; sb.connect(master);15 mb = ac.createGain(); mb.gain.value = 0.55; mb.connect(master);16 nbuf = ac.createBuffer(1, ac.sampleRate, ac.sampleRate);17 const d = nbuf.getChannelData(0);18 for (let i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1;19 // soft drone pad20 const lp = ac.createBiquadFilter(); lp.type = "lowpass"; lp.frequency.value = 240;21 const dg = ac.createGain(); dg.gain.value = 0.05; lp.connect(dg); dg.connect(mb);22 [[55, "sawtooth"], [55.35, "sawtooth"], [110.2, "sine"]].forEach((p) => {23 const o = ac.createOscillator(); o.type = p[1]; o.frequency.value = p[0]; o.connect(lp); o.start();24 });25 return true;26 } catch (e) { ac = null; return false; }27 }28 function resume() { if (ac && ac.state !== "running") { try { ac.resume(); } catch (e) {} } }29 function suspend() { if (ac && ac.state === "running") { try { ac.suspend(); } catch (e) {} } }30 function ok(name, gap) {31 if (!ac || muted || ac.state !== "running") return false;32 const t = ac.currentTime;33 if (t - (lastT[name] || -9) < gap) return false;34 lastT[name] = t; return true;35 }36 function tone(type, f0, f1, dur, vol, t0, bus) {37 const t = t0 || ac.currentTime;38 const o = ac.createOscillator(), g = ac.createGain();39 o.type = type; o.frequency.setValueAtTime(f0, t);40 if (f1 !== f0) o.frequency.exponentialRampToValueAtTime(Math.max(20, f1), t + dur);41 g.gain.setValueAtTime(0.0001, t);42 g.gain.exponentialRampToValueAtTime(vol, t + 0.006);43 g.gain.exponentialRampToValueAtTime(0.0001, t + dur);44 o.connect(g); g.connect(bus || sb); o.start(t); o.stop(t + dur + 0.03);45 }46 function noise(dur, vol, f0, f1, type, t0, bus) {47 const t = t0 || ac.currentTime;48 const s = ac.createBufferSource(); s.buffer = nbuf; s.loop = true;49 const f = ac.createBiquadFilter(); f.type = type || "bandpass";50 f.frequency.setValueAtTime(f0, t);51 if (f1 !== f0) f.frequency.exponentialRampToValueAtTime(Math.max(30, f1), t + dur);52 f.Q.value = 1.2;53 const g = ac.createGain();54 g.gain.setValueAtTime(vol, t); g.gain.exponentialRampToValueAtTime(0.0001, t + dur);55 s.connect(f); f.connect(g); g.connect(sb); s.start(t, Math.random() * 0.5); s.stop(t + dur + 0.03);56 }57 const F = {58 hit(a) { if (ok("hit", 0.04)) tone("square", 260 + (a || 0) * 30, 120, 0.05, 0.05); },59 crit() { if (ok("crit", 0.05)) { tone("square", 520, 180, 0.09, 0.08); noise(0.05, 0.1, 3000, 1500, "highpass"); } },60 kill(a) { if (ok("kill", 0.03)) { tone("triangle", 420 * (1 + (a || 0) * 0.05), 90, 0.13, 0.13); noise(0.09, 0.09, 1800, 400); } },61 bigkill() { if (ok("bigkill", 0.1)) { tone("sawtooth", 200, 40, 0.35, 0.2); noise(0.3, 0.2, 1200, 120); } },62 gem(combo) { if (ok("gem", 0.03)) tone("sine", 620 + Math.min(combo, 14) * 46, 900 + Math.min(combo, 14) * 60, 0.09, 0.09); },63 heart() { tone("sine", 500, 1000, 0.25, 0.14); tone("sine", 750, 1500, 0.25, 0.1, ac && ac.currentTime + 0.08); },64 hurt() { if (ok("hurt", 0.12)) { tone("sawtooth", 170, 45, 0.3, 0.25); noise(0.2, 0.2, 900, 150); } },65 levelup() { if (!ac || muted) return; [523, 659, 784, 1047].forEach((f, i) => tone("triangle", f, f, 0.22, 0.14, ac.currentTime + i * 0.07)); },66 pick() { if (!ac || muted) return; tone("sine", 660, 1320, 0.18, 0.15); tone("triangle", 990, 1980, 0.22, 0.09, ac.currentTime + 0.05); },67 evo() { if (!ac || muted) return; [392, 523, 659, 784, 1047, 1319].forEach((f, i) => tone("sawtooth", f, f * 1.01, 0.3, 0.09, ac.currentTime + i * 0.06)); noise(0.6, 0.12, 400, 4000, "bandpass"); },68 shoot() { if (ok("shoot", 0.05)) tone("square", 900, 320, 0.07, 0.04); },69 zap() { if (ok("zap", 0.06)) { noise(0.12, 0.12, 4200, 600); tone("sawtooth", 1400, 200, 0.1, 0.05); } },70 dash() { if (ok("dash", 0.1)) { noise(0.25, 0.16, 400, 5000, "bandpass"); tone("sine", 300, 900, 0.18, 0.08); } },71 shieldUp() { if (ok("shup", 0.2)) tone("sine", 400, 1200, 0.3, 0.1); },72 shieldPop() { if (ok("shpop", 0.1)) { tone("square", 900, 90, 0.3, 0.2); noise(0.25, 0.2, 3000, 300); } },73 reflect() { if (ok("refl", 0.05)) tone("triangle", 1400, 2200, 0.08, 0.08); },74 boom() { if (ok("boom", 0.05)) { noise(0.2, 0.15, 900, 120); tone("sine", 120, 40, 0.2, 0.15); } },75 warn() { if (!ac || muted) return; for (let i = 0; i < 4; i++) { tone("sawtooth", 440, 440, 0.2, 0.12, ac.currentTime + i * 0.42); tone("sawtooth", 330, 330, 0.2, 0.12, ac.currentTime + i * 0.42 + 0.21); } },76 bossHit() { if (ok("bhit", 0.06)) tone("square", 140, 80, 0.06, 0.07); },77 bossDie() { if (!ac || muted) return; noise(1.2, 0.3, 2000, 80); tone("sawtooth", 300, 30, 1.0, 0.25); [523, 659, 784, 1047, 1319].forEach((f, i) => tone("triangle", f, f, 0.4, 0.13, ac.currentTime + 0.5 + i * 0.09)); },78 telegraph() { if (ok("tele", 0.2)) tone("sine", 200, 600, 0.3, 0.08); },79 bossShot() { if (ok("bshot", 0.08)) tone("square", 300, 150, 0.1, 0.05); },80 over() { if (!ac || muted) return; [400, 330, 262, 196].forEach((f, i) => tone("sawtooth", f, f * 0.9, 0.45, 0.14, ac.currentTime + i * 0.2)); },81 click() { if (!ac || muted) return; tone("sine", 800, 1100, 0.07, 0.1); },82 buy() { if (!ac || muted) return; [660, 880, 1320].forEach((f, i) => tone("triangle", f, f, 0.15, 0.12, ac.currentTime + i * 0.06)); },83 deny() { if (!ac || muted) return; tone("square", 150, 110, 0.15, 0.08); },84 wave() { if (!ac || muted) return; tone("sawtooth", 110, 220, 0.5, 0.12); noise(0.4, 0.08, 200, 2000); },85 revive() { if (!ac || muted) return; [262, 392, 523, 784].forEach((f, i) => tone("sine", f, f, 0.4, 0.15, ac.currentTime + i * 0.08)); },86 };87 function play(n, a) { if (!ac || muted) return; try { if (F[n]) F[n](a); } catch (e) {} }88 // ambient pulse: quarter-note kick + sub bass + sparse pings. intensity 0..189 const NOTES = [55, 55, 65.4, 49, 55, 73.4, 65.4, 49];90 const PENT = [220, 261.6, 293.7, 349.2, 392, 440, 523.3];91 function tick(active, intensity) {92 if (!ac || muted || !active || ac.state !== "running") return;93 const t = ac.currentTime;94 if (nextBeat < t - 0.4) nextBeat = t + 0.05;95 const period = 60 / (86 + intensity * 44);96 while (nextBeat < t + 0.14) {97 const s = beat & 15, w = nextBeat;98 tone("sine", 120, 42, 0.22, 0.42 + intensity * 0.1, w, mb);99 if ((s & 1) === 0) tone("sawtooth", NOTES[(s >> 1) & 7], NOTES[(s >> 1) & 7], period * 1.7, 0.10, w, mb);100 if ((s & 3) === 3 || (intensity > 0.5 && (s & 1))) tone("sine", PENT[(beat * 5 + (beat >> 2)) % 7] * (s & 4 ? 2 : 1), PENT[(beat * 5 + (beat >> 2)) % 7] * (s & 4 ? 2 : 1), 0.5, 0.035 + intensity * 0.03, w + period * 0.5, mb);101 if (intensity > 0.3 && (s & 1)) { const st = w + period * 0.5; const src = ac.createBufferSource(); src.buffer = nbuf; const f = ac.createBiquadFilter(); f.type = "highpass"; f.frequency.value = 7000; const g = ac.createGain(); g.gain.setValueAtTime(0.06, st); g.gain.exponentialRampToValueAtTime(0.0001, st + 0.05); src.connect(f); f.connect(g); g.connect(mb); src.start(st, Math.random() * 0.5); src.stop(st + 0.07); }102 nextBeat += period; beat++;103 }104 }105 function setMuted(m) { muted = m; if (master) master.gain.value = m ? 0 : 0.85; }106 return { init, resume, suspend, play, tick, setMuted, get muted() { return muted; }, get ready() { return !!ac; } };107})();108