/* ============================================================ Wildlife Kingdom — js/audio.js Procedural WebAudio: gentle marimba soundtrack + synthesized SFX. No audio files, fully original. Global: WK.AudioSys ============================================================ */ (function(){ 'use strict'; const WK = window.WK; const A = { ctx:null, master:null, musicBus:null, sfxBus:null, enabled:true, musicOn:true, scene:'menu', timer:null, nextNote:0, step:0, }; function ensure(){ if(A.ctx){ if(A.ctx.state==='suspended') A.ctx.resume(); return true; } const AC = window.AudioContext || window.webkitAudioContext; if(!AC) return false; A.ctx = new AC(); A.master = A.ctx.createGain(); A.master.gain.value=0.9; A.master.connect(A.ctx.destination); A.musicBus = A.ctx.createGain(); A.musicBus.gain.value = 0.5; A.musicBus.connect(A.master); A.sfxBus = A.ctx.createGain(); A.sfxBus.gain.value = 0.85; A.sfxBus.connect(A.master); loadPrefs(); startScheduler(); return true; } function loadPrefs(){ try{ const p = JSON.parse(localStorage.getItem('wk.audio')||'{}'); if(p.enabled===false) A.enabled=false; if(p.musicOn===false) A.musicOn=false; }catch(e){} } function savePrefs(){ try{ localStorage.setItem('wk.audio', JSON.stringify({enabled:A.enabled,musicOn:A.musicOn})); }catch(e){} } /* ---------- tiny synth helpers ---------- */ function tone(opt){ // {f, f2, t='sine', dur, vol, bus, at, attack, release} const c=A.ctx; if(!c) return; const at = opt.at || c.currentTime; const o=c.createOscillator(), g=c.createGain(); o.type=opt.t||'sine'; o.frequency.setValueAtTime(opt.f,at); if(opt.f2) o.frequency.exponentialRampToValueAtTime(Math.max(20,opt.f2), at+(opt.dur||0.2)); const v=(opt.vol==null?0.2:opt.vol); g.gain.setValueAtTime(0.0001,at); g.gain.linearRampToValueAtTime(v, at+(opt.attack||0.008)); g.gain.exponentialRampToValueAtTime(0.0001, at+(opt.dur||0.2)); o.connect(g); g.connect(opt.bus||A.sfxBus); o.start(at); o.stop(at+(opt.dur||0.2)+0.05); } function noiseHit(opt){ // {dur,vol,f,q,bus,at} const c=A.ctx; if(!c) return; const at=opt.at||c.currentTime; const len=Math.max(1,Math.floor(c.sampleRate*(opt.dur||0.15))); const buf=c.createBuffer(1,len,c.sampleRate); const d=buf.getChannelData(0); for(let i=0;itone({f,t:'triangle',dur:0.18,vol:0.1,at:A.ctx&&A.ctx.currentTime+i*0.07})); }, roar(){ tone({f:150,f2:70,t:'sawtooth',dur:0.5,vol:0.14}); noiseHit({dur:0.4,vol:0.1,f:200,q:0.6}); }, chirp(){ tone({f:1400,f2:2100,t:'sine',dur:0.12,vol:0.1}); tone({f:1800,f2:2400,t:'sine',dur:0.1,vol:0.08,at:A.ctx&&A.ctx.currentTime+0.1}); }, splash(){ noiseHit({dur:0.3,vol:0.28,f:900,q:0.4}); }, trash(){ noiseHit({dur:0.12,vol:0.25,f:250,q:0.9}); }, unlock(){ [392,523,659,784].forEach((f,i)=>tone({f,t:'sine',dur:0.25,vol:0.11,at:A.ctx&&A.ctx.currentTime+i*0.09})); }, }; WK.AudioSys = { ensure, get enabled(){return A.enabled;}, get musicOn(){return A.musicOn;}, sfx(name){ if(!A.enabled) return; if(!ensure()) return; const fn=SFX[name]; if(fn) fn(); }, setScene(s){ A.scene=s; }, toggleSound(){ A.enabled=!A.enabled; savePrefs(); if(A.enabled) ensure(); WK.UI&&WK.UI.refreshSoundBtn&&WK.UI.refreshSoundBtn(); return A.enabled; }, toggleMusic(){ A.musicOn=!A.musicOn; savePrefs(); return A.musicOn; }, }; /* ---------- music scheduler ---------- Relaxed pentatonic marimba + warm pad, randomized gentle walk. Menu: slower & dreamier. Game: slightly brighter tempo. */ const PENTA=[0,2,4,7,9]; function nfreq(semi, base){ return base*Math.pow(2,semi/12); } function startScheduler(){ if(A.timer) return; A.timer=setInterval(()=>{ if(!A.ctx || !A.enabled || !A.musicOn) return; const ahead = 0.45; const menu = A.scene==='menu'; const stepDur = menu? 60/68/2 : 60/76/2; // eighth notes const root = menu? 220 : 246.94; // A3 / B3 while(A.nextNote < A.ctx.currentTime + ahead){ const t=Math.max(A.nextNote, A.ctx.currentTime+0.02); const bar=Math.floor(A.step/8), beat=A.step%8; // pad chord every bar if(beat===0){ const deg=PENTA[(bar*2)%5]; [0,7,12].forEach(iv=>tone({f:nfreq(deg+iv,root/2)*2,t:'sine',dur:stepDur*8,vol:0.035,bus:A.musicBus,at:t,attack:0.6})); tone({f:nfreq(PENTA[bar%5],root)/2,t:'triangle',dur:stepDur*3.6,vol:0.10,bus:A.musicBus,at:t,attack:0.01}); } // pluck melody: sparse random walk on pentatonic const density = menu?0.42:0.55; if(WK.hash2(A.step,bar,7) < density){ const idx = Math.floor(WK.hash2(A.step*13,bar,3)*PENTA.length); const oct = (WK.hash2(A.step,bar*3,11)<0.3)?12:0; tone({f:nfreq(PENTA[idx]+oct,root),t:'triangle',dur:stepDur*2.4,vol:0.085,bus:A.musicBus,at:t}); if(WK.hash2(A.step,bar,23)<0.25) tone({f:nfreq(PENTA[idx]+oct+12,root)*2,t:'sine',dur:stepDur*1.6,vol:0.04,bus:A.musicBus,at:t+stepDur*0.5}); } // soft shaker if(beat%2===1 && !menu) noiseHit({dur:0.03,vol:0.018,f:6000,q:1.2,bus:A.musicBus,at:t}); A.nextNote = t + stepDur; A.step++; } },120); } })();