'use strict'; /* ============================================================ NEON SURVIVORS — audio.js : WebAudio synth SFX + generative music No audio files needed; everything is synthesized. ============================================================ */ const Snd = { ctx: null, master: null, musicG: null, sfxG: null, // music sequencer state _step: 0, _nextTime: 0, _timer: null, intensity: 0.2, // 0..1 — drives music layers _xpCombo: 0, _xpComboT: 0, init() { if (this.ctx) { if (this.ctx.state === 'suspended') this.ctx.resume(); return; } const AC = window.AudioContext || window.webkitAudioContext; if (!AC) return; this.ctx = new AC(); this.master = this.ctx.createGain(); this.musicG = this.ctx.createGain(); this.sfxG = this.ctx.createGain(); this.master.connect(this.ctx.destination); this.musicG.connect(this.master); this.sfxG.connect(this.master); this.applyVolumes(); this._startMusic(); }, applyVolumes() { if (!this.ctx) return; const s = Store.s(); this.master.gain.value = s.volMaster; this.musicG.gain.value = s.volMusic * 0.9; this.sfxG.gain.value = s.volSfx; }, midi(n) { return 440 * Math.pow(2, (n - 69) / 12); }, /** Generic oscillator blip. */ tone(o) { if (!this.ctx) return; const t0 = (o.delay || 0) + this.ctx.currentTime; const osc = this.ctx.createOscillator(); const g = this.ctx.createGain(); osc.type = o.type || 'square'; osc.frequency.setValueAtTime(o.f || 440, t0); if (o.f2) osc.frequency.exponentialRampToValueAtTime(Math.max(1, o.f2), t0 + (o.dur || .1)); const vol = o.vol !== undefined ? o.vol : .2; const dur = o.dur || .1; g.gain.setValueAtTime(0.0001, t0); g.gain.linearRampToValueAtTime(vol, t0 + (o.attack || .004)); g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur); osc.connect(g); g.connect(o.dest || this.sfxG); osc.start(t0); osc.stop(t0 + dur + .05); }, /** Filtered noise burst. */ noise(o) { if (!this.ctx) return; const dur = o.dur || .15; const t0 = (o.delay || 0) + this.ctx.currentTime; const len = Math.max(1, Math.floor(this.ctx.sampleRate * dur)); const buf = this.ctx.createBuffer(1, len, this.ctx.sampleRate); const d = buf.getChannelData(0); for (let i = 0; i < len; i++) d[i] = Math.random() * 2 - 1; const src = this.ctx.createBufferSource(); src.buffer = buf; const flt = this.ctx.createBiquadFilter(); flt.type = o.hp ? 'highpass' : 'lowpass'; flt.frequency.setValueAtTime(o.f || 1200, t0); if (o.f2) flt.frequency.exponentialRampToValueAtTime(Math.max(20, o.f2), t0 + dur); const g = this.ctx.createGain(); g.gain.setValueAtTime(o.vol !== undefined ? o.vol : .2, t0); g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur); src.connect(flt); flt.connect(g); g.connect(this.sfxG); src.start(t0); src.stop(t0 + dur + .02); }, /** Play named SFX (throttled per name). */ play(name, force) { if (!this.ctx) return; if (!force && !throttled('sfx_' + name, 45)) return; switch (name) { case 'shoot': this.tone({ f: rand(640, 760), f2: 260, type: 'triangle', dur: .06, vol: .05 }); break; case 'hit': this.noise({ dur: .05, vol: .09, f: 1900 }); break; case 'crit': this.tone({ f: 1568, type: 'sine', dur: .06, vol: .12 }); this.tone({ f: 2093, type: 'sine', dur: .09, vol: .1, delay: .045 }); break; case 'kill': this.noise({ dur: .08, vol: .12, f: 520, f2: 160 }); this.tone({ f: 300, f2: 90, type: 'sine', dur: .08, vol: .11 }); break; case 'hurt': this.tone({ f: 170, f2: 70, type: 'sawtooth', dur: .22, vol: .3 }); this.noise({ dur: .12, vol: .18, f: 700 }); break; case 'xp': { const now = performance.now(); if (now - this._xpComboT > 900) this._xpCombo = 0; this._xpComboT = now; this._xpCombo = (this._xpCombo + 1) % 8; this.tone({ f: 660 * Math.pow(2, this._xpCombo / 12), type: 'sine', dur: .07, vol: .09 }); break; } case 'coin': this.tone({ f: 1318, type: 'square', dur: .05, vol: .08 }); this.tone({ f: 1760, type: 'square', dur: .09, vol: .08, delay: .055 }); break; case 'heal': [523, 659, 784].forEach((f, i) => this.tone({ f, type: 'sine', dur: .16, vol: .12, delay: i * .07 })); break; case 'levelup':[392, 494, 587, 784].forEach((f, i) => this.tone({ f, type: 'triangle', dur: .22, vol: .18, delay: i * .09 })); break; case 'chest': [659, 830, 988, 1318].forEach((f, i) => this.tone({ f, type: 'sine', dur: .2, vol: .14, delay: i * .08 })); this.noise({ dur: .4, vol: .06, f: 6000, hp: true }); break; case 'evolve': this.tone({ f: 300, f2: 1400, type: 'sawtooth', dur: .5, vol: .16 }); [523, 659, 784, 1046].forEach((f, i) => this.tone({ f, type: 'triangle', dur: .3, vol: .14, delay: .25 + i * .07 })); break; case 'boom': this.noise({ dur: .38, vol: .4, f: 900, f2: 90 }); this.tone({ f: 130, f2: 36, type: 'sine', dur: .32, vol: .35 }); break; case 'zap': this.tone({ f: 1500, f2: 90, type: 'sawtooth', dur: .13, vol: .16 }); break; case 'freeze': this.noise({ dur: .14, vol: .12, f: 4200, hp: true }); this.tone({ f: 1800, f2: 2400, type: 'sine', dur: .1, vol: .06 }); break; case 'roar': this.tone({ f: 58, type: 'sawtooth', dur: .7, vol: .34 }); this.tone({ f: 61, type: 'sawtooth', dur: .7, vol: .3 }); this.noise({ dur: .5, vol: .15, f: 300 }); break; case 'click': this.tone({ f: 900, type: 'square', dur: .03, vol: .1 }); break; case 'death': this.tone({ f: 220, f2: 40, type: 'sawtooth', dur: .9, vol: .35 }); break; case 'win': [523, 659, 784, 1046, 1318].forEach((f, i) => this.tone({ f, type: 'triangle', dur: .35, vol: .18, delay: i * .12 })); break; } }, /* ---------------- generative dark-synthwave loop ---------------- */ _startMusic() { if (this._timer) clearInterval(this._timer); this._step = 0; this._nextTime = this.ctx.currentTime + .1; this._timer = setInterval(() => this._schedule(), 30); }, _schedule() { if (!this.ctx || document.hidden) return; const fl = this.stageFlavor || {}; const spb = 60 / (118 * (fl.tempo || 1)) / 4; // 16th-note @118bpm × stage tempo while (this._nextTime < this.ctx.currentTime + .14) { this._playStep(this._step, this._nextTime); this._step = (this._step + 1) % 64; this._nextTime += spb; } }, /** Per-stage musical flavor: semitone shift + tempo multiplier. */ setFlavor(f) { this.stageFlavor = f || null; }, _mnote(freq, when, dur, vol, type, dest) { const osc = this.ctx.createOscillator(); const g = this.ctx.createGain(); osc.type = type; osc.frequency.value = freq; g.gain.setValueAtTime(0.0001, when); g.gain.linearRampToValueAtTime(vol, when + .01); g.gain.exponentialRampToValueAtTime(0.0001, when + dur); osc.connect(g); g.connect(dest); osc.start(when); osc.stop(when + dur + .03); }, _playStep(step, when) { const I = this.intensity; const fl = this.stageFlavor || {}; const chords = [45, 41, 48, 43].map(c => c + (fl.shift || 0)); // Am F C G, stage-shifted const root = chords[(step >> 4) & 3]; // Bass on syncopated steps if (step % 8 === 0 || step % 8 === 3 || step % 8 === 6) { this._mnote(this.midi(root - 12), when, .17, .16, 'square', this.musicG); } // Kick if (I > .25 && step % 8 === 0) this._mnote(110, when, .12, .22 * I, 'sine', this.musicG); // Hats if (I > .15 && step % 2 === 1) { // cheap hat via short high sine tick this._mnote(8000, when, .02, .04 * Math.min(1, I * 1.6), 'square', this.musicG); } // Lead pentatonic random walk const pent = [0, 3, 5, 7, 10]; const leadDensity = .18 + I * .5; if (Math.random() < leadDensity && step % 2 === 0) { const oct = chance(.25) ? 24 : 12; const note = root + oct + pick(pent); this._mnote(this.midi(note), when, .14, .07 + I * .05, 'triangle', this.musicG); } // Pad chord swell each bar if (step % 16 === 0) { [0, 3, 7].forEach(iv => this._mnote(this.midi(root + iv), when, 1.6, .028, 'sawtooth', this.musicG)); } // Boss tension drone if (I > .75 && step % 16 === 8) this._mnote(this.midi(root - 24), when, .3, .12, 'sawtooth', this.musicG); }, setIntensity(v) { this.intensity = clamp(v, 0, 1); } };