SOURCE / PINNED RELEASE
Made of little things.
Squad Table
- Release
- d36201f8ed4f…
- Author-recorded commit
- 7b9bb681d6ba…
- License
- LICENSE
- Author’s source reference
- nostr://npub1sj2vketepqw9v6fmge5fdzuj7atj9sv6qv2jss05n2z6rl64lcfqumdsnt/wss%3A%2F%2Fgit.napplet.soy%2F/n-c043b0c0f1d
Archive hash verified: 8c0604b31f00cc5a…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
// Small Web Audio synth for battle sounds: rifle cracks, MG bursts and
// explosions from filtered noise. No audio files. Muted until enabled and only
// starts after a user gesture has allowed audio.
let ctx: AudioContext | null = null;
let noise: AudioBuffer | null = null;
let enabled = true;
export function setSoundEnabled(on: boolean): void {
enabled = on;
}
export function soundEnabled(): boolean {
return enabled;
}
function audio(): AudioContext | null {
if (!enabled) return null;
try {
ctx ??= new AudioContext();
if (ctx.state === 'suspended') void ctx.resume();
if (!noise) {
noise = ctx.createBuffer(1, ctx.sampleRate, ctx.sampleRate);
const data = noise.getChannelData(0);
for (let i = 0; i < data.length; i += 1) data[i] = Math.random() * 2 - 1;
}
return ctx;
} catch {
return null;
}
}
/** One gunshot: a band-passed noise crack with a fast decay. */
function crack(a: AudioContext, at: number, pitch: number, volume: number, length = 0.12): void {
const src = a.createBufferSource();
src.buffer = noise;
const band = a.createBiquadFilter();
band.type = 'bandpass';
band.frequency.value = pitch;
band.Q.value = 0.8;
const gain = a.createGain();
gain.gain.setValueAtTime(volume, at);
gain.gain.exponentialRampToValueAtTime(0.001, at + length);
src.connect(band).connect(gain).connect(a.destination);
src.start(at, Math.random() * 0.5, length + 0.05);
}
/** Rifle volley or MG burst; more firepower means more shots. */
export function gunfire(shots: number, mg: boolean): void {
const a = audio();
if (!a) return;
const t = a.currentTime + 0.02;
const n = Math.max(1, Math.min(14, shots));
for (let i = 0; i < n; i += 1) {
const at = mg ? t + i * 0.07 : t + i * 0.11 + Math.random() * 0.06;
crack(a, at, mg ? 1400 + Math.random() * 300 : 1900 + Math.random() * 600, mg ? 0.35 : 0.3);
}
}
/** A deep thump with a long rumble. */
export function explosion(delay = 0): void {
const a = audio();
if (!a) return;
const at = a.currentTime + delay;
const src = a.createBufferSource();
src.buffer = noise;
const low = a.createBiquadFilter();
low.type = 'lowpass';
low.frequency.setValueAtTime(900, at);
low.frequency.exponentialRampToValueAtTime(80, at + 0.9);
const gain = a.createGain();
gain.gain.setValueAtTime(0.9, at);
gain.gain.exponentialRampToValueAtTime(0.001, at + 1.1);
src.connect(low).connect(gain).connect(a.destination);
src.start(at, 0, 1.2);
}
/** Short rising chime for a successful rally. */
export function chime(): void {
const a = audio();
if (!a) return;
const at = a.currentTime;
for (const [i, f] of [523, 659, 784].entries()) {
const osc = a.createOscillator();
osc.type = 'triangle';
osc.frequency.value = f;
const gain = a.createGain();
gain.gain.setValueAtTime(0.0001, at + i * 0.08);
gain.gain.exponentialRampToValueAtTime(0.12, at + i * 0.08 + 0.02);
gain.gain.exponentialRampToValueAtTime(0.0001, at + i * 0.08 + 0.4);
osc.connect(gain).connect(a.destination);
osc.start(at + i * 0.08);
osc.stop(at + i * 0.08 + 0.45);
}
}
