Add chiptune audio engine (Web Audio API)
This commit is contained in:
@@ -0,0 +1,197 @@
|
|||||||
|
/* ============================================================
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user