/* ===================================================================== Audio: chiptune effects and generative music that changes with depth. Everything is synthesized with WebAudio, no samples. ===================================================================== */ const AU = { ctx: null, master: null, music: null, sfx: null, amb: null, noise: null, pulse12: null, pulse25: null, last: {}, started: false, zone: 0, nextNote: 0, step: 0, bar: 0, chordIdx: 0, timer: 0, melody: null, melPos: 0, lastDeg: 0, hum: null, humGain: null, crackleT: 0, wind: null, windGain: null, }; function auInit() { if (AU.ctx) { if (AU.ctx.state === 'suspended') AU.ctx.resume(); return; } const AC = window.AudioContext || window.webkitAudioContext; if (!AC) return; const ctx = new AC(); AU.ctx = ctx; const comp = ctx.createDynamicsCompressor(); comp.threshold.value = -16; comp.ratio.value = 4; comp.connect(ctx.destination); AU.master = ctx.createGain(); AU.master.connect(comp); AU.music = ctx.createGain(); AU.music.connect(AU.master); AU.sfx = ctx.createGain(); AU.sfx.connect(AU.master); AU.amb = ctx.createGain(); AU.amb.connect(AU.sfx); // white noise buffer const len = ctx.sampleRate * 2, buf = ctx.createBuffer(1, len, ctx.sampleRate), d = buf.getChannelData(0); for (let i = 0; i < len; i++) d[i] = Math.random() * 2 - 1; AU.noise = buf; const pulse = duty => { const n = 64, re = new Float32Array(n), im = new Float32Array(n); for (let k = 1; k < n; k++) { re[k] = 0; im[k] = (2 / (k * Math.PI)) * Math.sin(k * Math.PI * duty); } return ctx.createPeriodicWave(re, im); }; AU.pulse12 = pulse(0.125); AU.pulse25 = pulse(0.25); // machine hum AU.hum = ctx.createOscillator(); AU.hum.setPeriodicWave(AU.pulse25); AU.hum.frequency.value = 55; const lp = ctx.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.value = 320; AU.humGain = ctx.createGain(); AU.humGain.gain.value = 0; AU.hum.connect(lp); lp.connect(AU.humGain); AU.humGain.connect(AU.amb); AU.hum.start(); // wind / surface air const ws = ctx.createBufferSource(); ws.buffer = buf; ws.loop = true; const wf = ctx.createBiquadFilter(); wf.type = 'bandpass'; wf.frequency.value = 500; wf.Q.value = 0.7; AU.windGain = ctx.createGain(); AU.windGain.gain.value = 0; ws.connect(wf); wf.connect(AU.windGain); AU.windGain.connect(AU.amb); ws.start(); AU.windFilter = wf; auVolumes(); AU.nextNote = ctx.currentTime + 0.3; AU.timer = setInterval(musicScheduler, 60); AU.started = true; } function auVolumes() { if (!AU.ctx) return; const m = SET.muted ? 0 : SET.volMaster; AU.master.gain.setTargetAtTime(m, AU.ctx.currentTime, 0.05); AU.music.gain.setTargetAtTime(SET.volMusic * 0.55, AU.ctx.currentTime, 0.1); AU.sfx.gain.setTargetAtTime(SET.volSfx, AU.ctx.currentTime, 0.05); } function rateOk(key, ms) { const now = performance.now(); if (AU.last[key] && now - AU.last[key] < ms) return false; AU.last[key] = now; return true; } function tone(freq, dur, opts = {}) { const ctx = AU.ctx; if (!ctx) return; const t = (opts.at || ctx.currentTime) + 0.005; const o = ctx.createOscillator(); if (opts.wave === 'p12') o.setPeriodicWave(AU.pulse12); else if (opts.wave === 'p25') o.setPeriodicWave(AU.pulse25); else o.type = opts.wave || 'square'; o.frequency.setValueAtTime(freq, t); if (opts.to) o.frequency.exponentialRampToValueAtTime(opts.to, t + dur); if (opts.vib) { const l = ctx.createOscillator(), lg = ctx.createGain(); l.frequency.value = 5.5; lg.gain.value = freq * 0.012; l.connect(lg); lg.connect(o.frequency); l.start(t); l.stop(t + dur + 0.05); } const g = ctx.createGain(); const v = opts.vol || 0.1, a = opts.attack || 0.005, rel = opts.release || 0.04; g.gain.setValueAtTime(0.0001, t); g.gain.exponentialRampToValueAtTime(v, t + a); g.gain.setValueAtTime(v, t + Math.max(a, dur - rel)); g.gain.exponentialRampToValueAtTime(0.0001, t + dur); o.connect(g); g.connect(opts.bus || AU.bus || AU.sfx); o.start(t); o.stop(t + dur + 0.02); } function noiseBurst(dur, opts = {}) { const ctx = AU.ctx; if (!ctx) return; const t = (opts.at || ctx.currentTime) + 0.005; const s = ctx.createBufferSource(); s.buffer = AU.noise; s.playbackRate.value = opts.rate || 1; const f = ctx.createBiquadFilter(); f.type = opts.type || 'bandpass'; f.frequency.value = opts.freq || 1200; f.Q.value = opts.q || 1; if (opts.sweep) f.frequency.exponentialRampToValueAtTime(opts.sweep, t + dur); const g = ctx.createGain(); const v = opts.vol || 0.08; g.gain.setValueAtTime(v, t); g.gain.exponentialRampToValueAtTime(0.0001, t + dur); s.connect(f); f.connect(g); g.connect(opts.bus || AU.bus || AU.sfx); s.start(t, Math.random() * 1.5); s.stop(t + dur + 0.02); } const SFX = { dig(h) { if (!rateOk('dig', 75)) return; noiseBurst(0.06, { freq: 260 + h * 260, q: 1.6, vol: 0.09 }); if (h >= 3) tone(120 + h * 40, 0.04, { wave: 'p12', vol: 0.03 }); }, // a resonance pulse: a low thrum, higher and tighter for a narrow pulse pulse(w) { if (!rateOk('pulse', 70)) return; const f = [150, 110, 82][w | 0] || 110; tone(f, 0.16, { wave: 'sine', to: f * 0.72, vol: 0.09, release: 0.12 }); tone(f * 3.02, 0.07, { wave: 'triangle', vol: 0.012, release: 0.06 }); }, place() { if (!rateOk('place', 70)) return; tone(520, 0.045, { wave: 'p25', to: 660, vol: 0.05 }); }, build() { if (!rateOk('build', 90)) return; tone(440, 0.05, { wave: 'p25', to: 520, vol: 0.035 }); }, decon() { if (!rateOk('decon', 80)) return; tone(520, 0.06, { wave: 'p25', to: 300, vol: 0.05 }); }, spark() { if (!rateOk('spark', 90)) return; noiseBurst(0.05, { type: 'highpass', freq: 3000, vol: 0.06 }); }, ignite() { if (!rateOk('ignite', 400)) return; noiseBurst(0.5, { type: 'lowpass', freq: 400, sweep: 2400, vol: 0.12 }); }, sizzle() { if (!rateOk('sizzle', 220)) return; noiseBurst(0.22, { type: 'highpass', freq: 4500, vol: 0.05 }); }, ui() { if (!rateOk('ui', 40)) return; tone(1100, 0.025, { wave: 'p25', vol: 0.035 }); }, error() { if (!rateOk('err', 300)) return; tone(110, 0.14, { wave: 'p12', vol: 0.07 }); tone(98, 0.14, { wave: 'p12', vol: 0.05, at: AU.ctx && AU.ctx.currentTime + 0.07 }); }, gate() { noiseBurst(0.12, { type: 'lowpass', freq: 600, vol: 0.12 }); tone(90, 0.12, { wave: 'triangle', to: 60, vol: 0.12 }); }, cast() { if (!AU.ctx) return; const t = AU.ctx.currentTime; [523, 659, 784, 1047].forEach((f, k) => tone(f, 0.12, { wave: 'p25', vol: 0.06, at: t + k * 0.08 })); }, upgrade() { if (!AU.ctx) return; const t = AU.ctx.currentTime; [392, 523, 659, 784, 659, 784, 1047].forEach((f, k) => tone(f, k === 6 ? 0.5 : 0.11, { wave: k % 2 ? 'p25' : 'p12', vol: 0.07, at: t + k * 0.09, vib: k === 6 })); [196, 262, 330].forEach((f, k) => tone(f, 0.35, { wave: 'triangle', vol: 0.08, at: t + 0.27 + k * 0.18 })); }, milestone() { if (!AU.ctx) return; const t = AU.ctx.currentTime; [659, 784, 988, 1319].forEach((f, k) => tone(f, k === 3 ? 0.35 : 0.1, { wave: 'p25', vol: 0.06, at: t + k * 0.07, vib: k === 3 })); }, suck() { if (!rateOk('suck', 110)) return; noiseBurst(0.1, { type: 'bandpass', freq: 900, sweep: 2600, q: 2.2, vol: 0.045 }); }, blow() { if (!rateOk('blow', 110)) return; noiseBurst(0.12, { type: 'bandpass', freq: 2200, sweep: 700, q: 1.6, vol: 0.05 }); }, sale() { if (!AU.ctx) return; const t = AU.ctx.currentTime; [784, 988, 1175, 1568].forEach((f, k) => tone(f, 0.09, { wave: 'p25', vol: 0.05, at: t + k * 0.06 })); }, discover() { if (!rateOk('disc', 500)) return; const t = AU.ctx && AU.ctx.currentTime; tone(880, 0.06, { wave: 'p25', vol: 0.04, at: t }); tone(1175, 0.1, { wave: 'p25', vol: 0.04, at: t + 0.06 }); }, }; // World sounds are heard around the bot, whatever the zoom: full within 24 grains, silent at 96 const SFX_NEAR = 24, SFX_FAR = 96; function sfxPlace(x, y) { if (typeof bot === 'undefined' || !bot.w) return { gain: 1, pan: 0 }; const dx = x - (bot.x + bot.w / 2), dy = y - (bot.y + bot.h / 2), d = Math.hypot(dx, dy); return { gain: Math.max(0, Math.min(1, (SFX_FAR - d) / (SFX_FAR - SFX_NEAR))), pan: Math.max(-1, Math.min(1, dx / SFX_FAR)) * 0.6 }; } function playSfx(name, arg, x, y) { if (!AU.ctx || !SFX[name]) return; const place = typeof x === 'number' && typeof y === 'number' ? sfxPlace(x, y) : null; if (place && place.gain < 0.02) return; const prev = AU.bus; let gain, pan; try { if (place) { gain = AU.ctx.createGain(); pan = AU.ctx.createStereoPanner(); gain.gain.value = place.gain; pan.pan.value = place.pan; gain.connect(pan); pan.connect(AU.sfx); AU.bus = gain; } SFX[name](arg); } catch (e) { /* ignore */ } finally { AU.bus = prev; if (gain || pan) setTimeout(() => { if (gain) gain.disconnect(); if (pan) pan.disconnect(); }, 1500); } } // Ambient layer driven by what is on screen. function auAmbient(info) { if (!AU.ctx) return; const t = AU.ctx.currentTime; AU.humGain.gain.setTargetAtTime(Math.min(0.05, info.power * 0.004), t, 0.4); AU.hum.frequency.setTargetAtTime(48 + Math.min(30, info.power * 0.6), t, 0.6); // wind on the surface; rain turns it into a hiss const rain = info.rain || 0; AU.windGain.gain.setTargetAtTime(info.surface ? 0.018 + 0.01 * Math.sin(t * 0.3) + rain * 0.05 : 0.004 + rain * 0.008, t, 1.2); AU.windFilter.frequency.setTargetAtTime(info.surface ? 420 + 160 * Math.sin(t * 0.21) + rain * 1900 : 260 + rain * 400, t, 1.5); // fire crackle if (info.fire > 0 && performance.now() > AU.crackleT) { const n = Math.min(4, 1 + (info.fire / 60) | 0); for (let k = 0; k < n; k++) noiseBurst(0.02 + Math.random() * 0.03, { type: 'bandpass', freq: 900 + Math.random() * 2200, q: 2, vol: 0.02 + Math.min(0.05, info.fire * 0.0004), at: t + Math.random() * 0.15, bus: AU.amb }); AU.crackleT = performance.now() + 120 + Math.random() * Math.max(60, 700 - info.fire * 4); } } // ---------- Generative music ---------- const ZONES = [ { name: 'surface', root: 60, scale: [0, 2, 4, 5, 7, 9, 11], prog: [0, 5, 3, 4], tempo: 92, density: 0.62, hat: 0.5, lead: 0.55, oct: 0 }, { name: 'shallows', root: 57, scale: [0, 2, 3, 5, 7, 9, 10], prog: [0, 3, 6, 0], tempo: 84, density: 0.5, hat: 0.3, lead: 0.45, oct: 0 }, { name: 'caverns', root: 62, scale: [0, 2, 3, 5, 7, 8, 10], prog: [0, 5, 6, 4], tempo: 76, density: 0.4, hat: 0.18, lead: 0.35, oct: -12 }, { name: 'deep', root: 52, scale: [0, 1, 3, 5, 7, 8, 10], prog: [0, 1, 0, 6], tempo: 66, density: 0.3, hat: 0.08, lead: 0.25, oct: -12 }, { name: 'magma', root: 49, scale: [0, 1, 4, 5, 7, 8, 10], prog: [0, 1, 0, 1], tempo: 60, density: 0.24, hat: 0.04, lead: 0.2, oct: -12 }, ]; function mtof(m) { return 440 * Math.pow(2, (m - 69) / 12); } function zoneForDepth() { const cx = Math.max(0, Math.min(W - 1, Math.floor(bot.x))); const y = bot.y; if (y < surfY[cx] + 12) return 0; const u = depthU(y); if (u < 0.3) return 1; if (y < graniteY[cx]) return 2; if (u < 0.86) return 3; return 4; } function scaleNote(z, deg, octave) { const n = z.scale.length; const o = Math.floor(deg / n), d = ((deg % n) + n) % n; return z.root + z.scale[d] + 12 * (o + octave) + z.oct; } function musicScheduler() { const ctx = AU.ctx; if (!ctx || !AU.started || SET.volMusic <= 0 || (window.GAME && !GAME.running)) { if (ctx) AU.nextNote = Math.max(AU.nextNote, ctx.currentTime + 0.1); return; } const z = ZONES[AU.zone]; const spb = 60 / z.tempo / 4; // seconds per 16th while (AU.nextNote < ctx.currentTime + 0.25) { const s = AU.step % 16, t = AU.nextNote; if (s === 0) { // new bar: maybe change zone, advance chord const nz = W ? zoneForDepth() : 0; if (nz !== AU.zone) { AU.zone = nz; } AU.chordIdx = (AU.chordIdx + 1) % 4; if (Math.random() < ZONES[AU.zone].lead * 0.5 && !AU.melody) { const len = 4 + ((Math.random() * 5) | 0), mel = []; let d = AU.lastDeg; for (let k = 0; k < len; k++) { d += [-2, -1, -1, 1, 1, 2, 0][(Math.random() * 7) | 0]; d = Math.max(-2, Math.min(9, d)); mel.push({ d, at: k * (Math.random() < 0.6 ? 2 : 4) }); } AU.melody = mel; AU.melPos = AU.step; AU.lastDeg = d; } } const zz = ZONES[AU.zone]; const chordRoot = zz.prog[AU.chordIdx]; // bass if (s === 0 || (s === 8 && Math.random() < 0.6)) { const deg = s === 8 && Math.random() < 0.5 ? chordRoot + 4 : chordRoot; tone(mtof(scaleNote(zz, deg, -1)), spb * (s === 0 ? 7 : 5), { wave: 'p12', vol: 0.07, bus: AU.music, release: 0.15, at: t }); } // arpeggio if (s % 2 === 0 && Math.random() < zz.density) { const pat = [0, 2, 4, 2, 0, 4, 2, 4][(s >> 1) & 7] + (Math.random() < 0.15 ? 7 : 0); tone(mtof(scaleNote(zz, chordRoot + pat, 1)), spb * 1.6, { wave: 'triangle', vol: 0.05, bus: AU.music, release: 0.06, at: t }); } // melody phrase if (AU.melody) { const rel = AU.step - AU.melPos; for (const n of AU.melody) if (n.at === rel) tone(mtof(scaleNote(zz, n.d, 1)), spb * 2.6, { wave: 'p25', vol: 0.035, bus: AU.music, vib: true, attack: 0.02, release: 0.1, at: t }); if (rel > 40) AU.melody = null; } // hats if ((s % 4 === 2) && Math.random() < zz.hat) noiseBurst(0.03, { type: 'highpass', freq: 6000, vol: 0.025, at: t, bus: AU.music }); if (s === 0 && AU.zone <= 1 && Math.random() < 0.7) tone(110, 0.12, { wave: 'triangle', to: 50, vol: 0.06, at: t, bus: AU.music }); if (AU.zone >= 3 && s === 0 && AU.chordIdx % 2 === 0) tone(mtof(scaleNote(zz, chordRoot, -2)), spb * 30, { wave: 'p12', vol: 0.03, at: t, bus: AU.music, attack: 0.6, release: 1.2 }); AU.nextNote += spb; AU.step++; } }