SOURCE / PINNED RELEASE
Made of little things.
Poly Dogfight
- Release
- b8511ee374e3…
- Author-recorded commit
- 5579434b9d8b…
- License
- LICENSE
- Author’s source reference
- nostr://npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr/wss%3A%2F%2Fgit.napplet.soy%2F/n-44f63e5422b
Archive hash verified: bcadd2430d8ea2bc…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
export function createNoise(ctx: BaseAudioContext): AudioBuffer {
const buffer = ctx.createBuffer(1, ctx.sampleRate, ctx.sampleRate);
const samples = buffer.getChannelData(0);
for (let i = 0; i < samples.length; i++) samples[i] = Math.random() * 2 - 1;
return buffer;
}
/**
* Shared gun channel: a fast compressor so overlapping rounds punch instead of
* clipping, plus a short dark slapback that turns a held trigger into a rattle.
*/
export function createGunBus(ctx: BaseAudioContext, destination: AudioNode): AudioNode {
const input = ctx.createGain();
input.gain.value = 0.9;
const comp = ctx.createDynamicsCompressor();
comp.threshold.value = -20;
comp.knee.value = 6;
comp.ratio.value = 5;
comp.attack.value = 0.002;
comp.release.value = 0.08;
input.connect(comp).connect(destination);
const echo = ctx.createDelay(0.2);
echo.delayTime.value = 0.07;
const tone = ctx.createBiquadFilter();
tone.type = 'lowpass';
tone.frequency.value = 1600;
const feedback = ctx.createGain();
feedback.gain.value = 0.22;
const wet = ctx.createGain();
wet.gain.value = 0.3;
input.connect(echo).connect(tone).connect(feedback).connect(echo);
tone.connect(wet).connect(comp);
return input;
}
/**
* A single machine-gun round, three layers: a bright crack (the muzzle snap),
* a mid blast whose filter slams shut (the bang) and a falling sine thump
* (the weight). Small random variation keeps a burst from sounding looped.
*/
export function playShot(ctx: BaseAudioContext, bus: AudioNode, noise: AudioBuffer, when: number, pan = 0): void {
const vary = 0.92 + Math.random() * 0.16;
const out = ctx.createStereoPanner();
out.pan.value = Math.max(-1, Math.min(1, pan));
out.connect(bus);
const crack = ctx.createBufferSource();
crack.buffer = noise;
crack.playbackRate.value = vary;
const crackFilter = ctx.createBiquadFilter();
crackFilter.type = 'highpass';
crackFilter.frequency.value = 2400;
const crackGain = ctx.createGain();
crackGain.gain.setValueAtTime(0.55, when);
crackGain.gain.exponentialRampToValueAtTime(0.001, when + 0.03);
crack.connect(crackFilter).connect(crackGain).connect(out);
crack.start(when, Math.random() * 0.8, 0.04);
const blast = ctx.createBufferSource();
blast.buffer = noise;
blast.playbackRate.value = vary;
const blastFilter = ctx.createBiquadFilter();
blastFilter.type = 'lowpass';
blastFilter.Q.value = 1.5;
blastFilter.frequency.setValueAtTime(2200 * vary, when);
blastFilter.frequency.exponentialRampToValueAtTime(260, when + 0.09);
const blastGain = ctx.createGain();
blastGain.gain.setValueAtTime(0.0001, when);
blastGain.gain.linearRampToValueAtTime(0.9, when + 0.002);
blastGain.gain.exponentialRampToValueAtTime(0.001, when + 0.12);
blast.connect(blastFilter).connect(blastGain).connect(out);
blast.start(when, Math.random() * 0.8, 0.13);
const thump = ctx.createOscillator();
thump.type = 'sine';
thump.frequency.setValueAtTime(170 * vary, when);
thump.frequency.exponentialRampToValueAtTime(48, when + 0.07);
const thumpGain = ctx.createGain();
thumpGain.gain.setValueAtTime(0.0001, when);
thumpGain.gain.linearRampToValueAtTime(0.8, when + 0.003);
thumpGain.gain.exponentialRampToValueAtTime(0.001, when + 0.1);
thump.connect(thumpGain).connect(out);
thump.start(when);
thump.stop(when + 0.11);
}
/** Tiny synthesized engine, wind, gun and stall-horn sounds. Starts only after a user gesture. */
export function createAudio() {
let ctx: AudioContext | null = null;
let master: GainNode | null = null;
let engineOsc: OscillatorNode | null = null;
let engineOsc2: OscillatorNode | null = null;
let engineFilter: BiquadFilterNode | null = null;
let windGain: GainNode | null = null;
let hornGain: GainNode | null = null;
let muted = false;
let gunNoise: AudioBuffer | null = null;
let gunBus: AudioNode | null = null;
const recentShots: number[] = [];
function start() {
if (ctx) {
void ctx.resume();
return;
}
try {
ctx = new AudioContext();
} catch {
return;
}
master = ctx.createGain();
master.gain.value = muted ? 0 : 0.5;
master.connect(ctx.destination);
engineFilter = ctx.createBiquadFilter();
engineFilter.type = 'lowpass';
engineFilter.frequency.value = 600;
const engineGain = ctx.createGain();
engineGain.gain.value = 0.08;
engineOsc = ctx.createOscillator();
engineOsc.type = 'sawtooth';
engineOsc2 = ctx.createOscillator();
engineOsc2.type = 'square';
engineOsc.connect(engineFilter);
engineOsc2.connect(engineFilter);
engineFilter.connect(engineGain).connect(master);
engineOsc.start();
engineOsc2.start();
// Wind: looping filtered noise.
const buffer = ctx.createBuffer(1, ctx.sampleRate * 2, ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < data.length; i++) data[i] = Math.random() * 2 - 1;
const noise = ctx.createBufferSource();
noise.buffer = buffer;
noise.loop = true;
const windFilter = ctx.createBiquadFilter();
windFilter.type = 'bandpass';
windFilter.frequency.value = 900;
windFilter.Q.value = 0.6;
windGain = ctx.createGain();
windGain.gain.value = 0;
noise.connect(windFilter).connect(windGain).connect(master);
noise.start();
const horn = ctx.createOscillator();
horn.type = 'triangle';
horn.frequency.value = 880;
hornGain = ctx.createGain();
hornGain.gain.value = 0;
horn.connect(hornGain).connect(master);
horn.start();
gunNoise = createNoise(ctx);
gunBus = createGunBus(ctx, master);
}
return {
start,
get muted() {
return muted;
},
setMuted(value: boolean) {
muted = value;
if (ctx && master) master.gain.setTargetAtTime(muted ? 0 : 0.5, ctx.currentTime, 0.05);
},
suspend() {
void ctx?.suspend();
},
update(params: { throttle: number; airspeed: number; boosting: boolean; stall: number; alive: boolean; time: number }) {
if (!ctx || !engineOsc || !engineOsc2 || !engineFilter || !windGain || !hornGain) return;
const t = ctx.currentTime;
const rpm = params.alive ? 0.25 + params.throttle * 0.75 + (params.boosting ? 0.35 : 0) : 0;
engineOsc.frequency.setTargetAtTime(38 + rpm * 70, t, 0.1);
engineOsc2.frequency.setTargetAtTime(19 + rpm * 35.5, t, 0.1);
engineFilter.frequency.setTargetAtTime(params.alive ? 300 + rpm * 900 : 10, t, 0.1);
windGain.gain.setTargetAtTime(params.alive ? Math.min(0.18, (params.airspeed / 100) ** 2 * 0.15) : 0, t, 0.2);
const beep = params.stall > 0.2 && params.alive && Math.floor(params.time * 5) % 2 === 0 ? 0.07 : 0;
hornGain.gain.setTargetAtTime(beep, t, 0.01);
},
/**
* One gun round. `delay` places it on the audio clock (the sim step it was
* fired in) so a held trigger keeps an even rhythm; `pan` is -1..1.
*/
shot(delay = 0, pan = 0) {
if (!ctx || !gunBus || !gunNoise) return;
const when = ctx.currentTime + delay;
// Several pilots firing together: cap overlapping voices so it can't turn to mush.
while (recentShots.length && recentShots[0]! < when - 0.03) recentShots.shift();
if (recentShots.length >= 3) return;
recentShots.push(when);
playShot(ctx, gunBus, gunNoise, when, pan);
},
/** A rocket leaving the rail: a hiss sweeping up through a band-pass, over a low thump. */
rocketLaunch(delay = 0, pan = 0) {
if (!ctx || !master || !gunNoise) return;
const t = ctx.currentTime + delay;
const out = ctx.createStereoPanner();
out.pan.value = Math.max(-1, Math.min(1, pan));
out.connect(master);
const hiss = ctx.createBufferSource();
hiss.buffer = gunNoise;
const band = ctx.createBiquadFilter();
band.type = 'bandpass';
band.Q.value = 1.2;
band.frequency.setValueAtTime(500, t);
band.frequency.exponentialRampToValueAtTime(3200, t + 0.5);
const hissGain = ctx.createGain();
hissGain.gain.setValueAtTime(0.0001, t);
hissGain.gain.linearRampToValueAtTime(0.5, t + 0.04);
hissGain.gain.exponentialRampToValueAtTime(0.001, t + 0.9);
hiss.connect(band).connect(hissGain).connect(out);
hiss.start(t, Math.random() * 0.05, 0.95);
const thump = ctx.createOscillator();
thump.type = 'sine';
thump.frequency.setValueAtTime(120, t);
thump.frequency.exponentialRampToValueAtTime(40, t + 0.18);
const thumpGain = ctx.createGain();
thumpGain.gain.setValueAtTime(0.0001, t);
thumpGain.gain.linearRampToValueAtTime(0.7, t + 0.005);
thumpGain.gain.exponentialRampToValueAtTime(0.001, t + 0.2);
thump.connect(thumpGain).connect(out);
thump.start(t);
thump.stop(t + 0.22);
},
/** A rocket going off: a short dark boom, heavier when it strikes a plane. */
rocketBoom(delay = 0, heavy = false) {
if (!ctx || !master || !gunNoise) return;
const t = ctx.currentTime + delay;
const src = ctx.createBufferSource();
src.buffer = gunNoise;
const filter = ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(heavy ? 2400 : 1500, t);
filter.frequency.exponentialRampToValueAtTime(90, t + 0.6);
const gain = ctx.createGain();
gain.gain.setValueAtTime(heavy ? 0.85 : 0.45, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.65);
src.connect(filter).connect(gain).connect(master);
src.start(t, Math.random() * 0.2, 0.7);
const thump = ctx.createOscillator();
thump.type = 'sine';
thump.frequency.setValueAtTime(90, t);
thump.frequency.exponentialRampToValueAtTime(32, t + 0.35);
const thumpGain = ctx.createGain();
thumpGain.gain.setValueAtTime(heavy ? 0.8 : 0.4, t);
thumpGain.gain.exponentialRampToValueAtTime(0.001, t + 0.4);
thump.connect(thumpGain).connect(master);
thump.start(t);
thump.stop(t + 0.42);
},
/** A round striking a plane: a bright metallic tick, lower when a part breaks. */
hit(destroyed: boolean) {
if (!ctx || !master) return;
const t = ctx.currentTime;
const osc = ctx.createOscillator();
osc.type = 'square';
osc.frequency.setValueAtTime(destroyed ? 220 : 1400 + Math.random() * 500, t);
osc.frequency.exponentialRampToValueAtTime(destroyed ? 60 : 500, t + (destroyed ? 0.5 : 0.06));
const gain = ctx.createGain();
gain.gain.setValueAtTime(destroyed ? 0.25 : 0.08, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + (destroyed ? 0.5 : 0.07));
osc.connect(gain).connect(master);
osc.start(t);
osc.stop(t + 0.6);
},
crash() {
if (!ctx || !master) return;
const t = ctx.currentTime;
const buffer = ctx.createBuffer(1, ctx.sampleRate, ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < data.length; i++) data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / data.length, 2.5);
const src = ctx.createBufferSource();
src.buffer = buffer;
const filter = ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(1800, t);
filter.frequency.exponentialRampToValueAtTime(120, t + 1);
const gain = ctx.createGain();
gain.gain.value = 0.9;
src.connect(filter).connect(gain).connect(master);
src.start();
},
};
}
