arcadia / orbit-survivors

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 pad
20 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..1
89 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