fellwing / audio.js
CyberMax-tools's picture
Deploy fellwing
8f3ff28 verified
Raw History Blame Contribute Delete
5.61 kB
// Fellwing sound, synthesised with Web Audio (no sound files): piston or turbofan engine, wind that rises with
// airspeed, the stall horn, tyre rumble on the runway and a gate chime. Starts on the first tap or key.
// (c) 2026 CyberMax. All rights reserved.
export class Sound {
constructor() { this.ctx = null; this.on = true; this.kind = 'prop'; }
start() {
if (this.ctx) { if (this.ctx.state === 'suspended') this.ctx.resume(); return; }
const AC = window.AudioContext || window.webkitAudioContext; if (!AC) return;
const c = this.ctx = new AC();
this.master = c.createGain(); this.master.gain.value = this.on ? 0.55 : 0; this.master.connect(c.destination);
const noiseBuf = c.createBuffer(1, c.sampleRate * 2, c.sampleRate); const d = noiseBuf.getChannelData(0);
let b = 0; for (let i = 0; i < d.length; i++) { const w = Math.random() * 2 - 1; b = 0.97 * b + 0.03 * w; d[i] = b * 6 + w * 0.15; }
const noise = () => { const s = c.createBufferSource(); s.buffer = noiseBuf; s.loop = true; s.start(); return s; };
// engine: two detuned saws through a low-pass + a filtered noise bed
this.eLp = c.createBiquadFilter(); this.eLp.type = 'lowpass'; this.eLp.frequency.value = 600; this.eLp.Q.value = 2;
this.eG = c.createGain(); this.eG.gain.value = 0; this.eLp.connect(this.eG); this.eG.connect(this.master);
this.o1 = c.createOscillator(); this.o1.type = 'sawtooth'; this.o2 = c.createOscillator(); this.o2.type = 'square';
this.o1.connect(this.eLp); const g2 = c.createGain(); g2.gain.value = 0.35; this.o2.connect(g2); g2.connect(this.eLp); this.o1.start(); this.o2.start();
this.eN = noise(); this.eNf = c.createBiquadFilter(); this.eNf.type = 'bandpass'; this.eNf.frequency.value = 900; this.eNf.Q.value = 0.7;
this.eNg = c.createGain(); this.eNg.gain.value = 0; this.eN.connect(this.eNf); this.eNf.connect(this.eNg); this.eNg.connect(this.master);
// jet whine
this.jw = c.createOscillator(); this.jw.type = 'sine'; this.jwG = c.createGain(); this.jwG.gain.value = 0; this.jw.connect(this.jwG); this.jwG.connect(this.master); this.jw.start();
// wind
this.wN = noise(); this.wF = c.createBiquadFilter(); this.wF.type = 'lowpass'; this.wF.frequency.value = 400; this.wG = c.createGain(); this.wG.gain.value = 0;
this.wN.connect(this.wF); this.wF.connect(this.wG); this.wG.connect(this.master);
// stall horn
this.hO = c.createOscillator(); this.hO.type = 'square'; this.hO.frequency.value = 440; this.hG = c.createGain(); this.hG.gain.value = 0;
const hf = c.createBiquadFilter(); hf.type = 'lowpass'; hf.frequency.value = 1400; this.hO.connect(hf); hf.connect(this.hG); this.hG.connect(this.master); this.hO.start();
// rumble
this.rN = noise(); this.rF = c.createBiquadFilter(); this.rF.type = 'lowpass'; this.rF.frequency.value = 120; this.rG = c.createGain(); this.rG.gain.value = 0;
this.rN.connect(this.rF); this.rF.connect(this.rG); this.rG.connect(this.master);
}
setOn(on) { this.on = on; if (this.master) this.master.gain.setTargetAtTime(on ? 0.55 : 0, this.ctx.currentTime, 0.05); }
update(P, paused) {
if (!this.ctx) return;
const t = this.ctx.currentTime, k = 0.08, thr = paused || P.crashed ? 0 : P.surf.thr, v = paused ? 0 : P.tas;
const jet = P.A.engine === 'jet';
const rpm = jet ? 0.25 + 0.75 * thr : 0.3 + 0.7 * thr + Math.min(0.15, v / 400);
if (jet) {
this.eG.gain.setTargetAtTime(0, t, k);
this.eNf.frequency.setTargetAtTime(500 + rpm * 2400, t, k); this.eNg.gain.setTargetAtTime(paused || P.crashed ? 0 : 0.05 + rpm * 0.22, t, k);
this.jw.frequency.setTargetAtTime(2200 + rpm * 4200, t, k); this.jwG.gain.setTargetAtTime(paused || P.crashed ? 0 : 0.012 + rpm * 0.03, t, k);
} else {
const f = 18 + rpm * 62;
this.o1.frequency.setTargetAtTime(f, t, k); this.o2.frequency.setTargetAtTime(f * 2.01, t, k);
this.eLp.frequency.setTargetAtTime(260 + rpm * 1100, t, k);
this.eG.gain.setTargetAtTime(paused || P.crashed ? 0 : 0.10 + thr * 0.16, t, k);
this.eNf.frequency.setTargetAtTime(700 + rpm * 900, t, k); this.eNg.gain.setTargetAtTime(paused || P.crashed ? 0 : 0.02 + thr * 0.06, t, k);
this.jwG.gain.setTargetAtTime(0, t, k);
}
this.wF.frequency.setTargetAtTime(250 + v * 9, t, k); this.wG.gain.setTargetAtTime(Math.min(0.32, (v / 120) ** 2 * 0.2), t, k);
this.hG.gain.setTargetAtTime(!paused && !P.crashed && P.buffet > 0.4 && !P.onGround ? 0.06 : 0, t, 0.02);
this.rG.gain.setTargetAtTime(!paused && P.onGround ? Math.min(0.5, v / 30) * 0.5 : 0, t, k);
}
chime(hi = false) {
if (!this.ctx) return;
const c = this.ctx, t = c.currentTime;
for (const [f, d] of hi ? [[660, 0], [880, 0.09], [1320, 0.18]] : [[880, 0], [1175, 0.08]]) {
const o = c.createOscillator(), g = c.createGain(); o.type = 'triangle'; o.frequency.value = f; g.gain.setValueAtTime(0, t + d);
g.gain.linearRampToValueAtTime(0.18, t + d + 0.01); g.gain.exponentialRampToValueAtTime(0.001, t + d + 0.5); o.connect(g); g.connect(this.master); o.start(t + d); o.stop(t + d + 0.6);
}
}
crash() {
if (!this.ctx) return;
const c = this.ctx, t = c.currentTime, s = c.createBufferSource(); s.buffer = this.eN.buffer;
const f = c.createBiquadFilter(); f.type = 'lowpass'; f.frequency.setValueAtTime(1800, t); f.frequency.exponentialRampToValueAtTime(80, t + 1.4);
const g = c.createGain(); g.gain.setValueAtTime(0.9, t); g.gain.exponentialRampToValueAtTime(0.001, t + 1.6);
s.connect(f); f.connect(g); g.connect(this.master); s.start(); s.stop(t + 1.7);
}
}