| |
| |
| |
| |
|
|
| let ctx = null, bus = null, master = null, muted = false, dead = false; |
| const MASTER = 0.32; |
|
|
| |
| |
| const MUSIC_SRC = "/static/audio/bgm.mp3"; |
| const MUSIC_VOL = 0.15; |
| let musicEl = null, musicGain = null, musicWired = false; |
|
|
| function ac() { |
| if (dead) return null; |
| try { |
| if (!ctx) { |
| ctx = new (window.AudioContext || window.webkitAudioContext)(); |
| master = ctx.createGain(); |
| master.gain.value = MASTER; |
| master.connect(ctx.destination); |
| |
| const lp = ctx.createBiquadFilter(); |
| lp.type = "lowpass"; lp.frequency.value = 2600; lp.Q.value = 0.5; |
| lp.connect(master); |
| |
| const wet = ctx.createGain(); wet.gain.value = 0.16; |
| const verb = ctx.createConvolver(); verb.buffer = impulse(0.9, 2.6); |
| verb.connect(wet); wet.connect(master); |
| bus = ctx.createGain(); bus.gain.value = 1; |
| bus.connect(lp); |
| bus.connect(verb); |
| } |
| if (ctx.state === "suspended") ctx.resume(); |
| return ctx; |
| } catch { |
| dead = true; |
| return null; |
| } |
| } |
|
|
| function impulse(seconds, decay) { |
| const a = ctx, len = Math.floor(a.sampleRate * seconds); |
| const buf = a.createBuffer(2, len, a.sampleRate); |
| for (let ch = 0; ch < 2; ch++) { |
| const d = buf.getChannelData(ch); |
| for (let i = 0; i < len; i++) |
| d[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / len, decay); |
| } |
| return buf; |
| } |
|
|
| |
| const N = { |
| C3: 130.81, E3: 164.81, G3: 196.0, |
| C4: 261.63, D4: 293.66, E4: 329.63, G4: 392.0, A4: 440.0, |
| C5: 523.25, D5: 587.33, E5: 659.25, G5: 783.99, A5: 880.0, |
| C6: 1046.5, D6: 1174.7, E6: 1318.5, G6: 1568.0, |
| }; |
|
|
| |
| function tone(freq, o = {}) { |
| if (muted) return; |
| const a = ac(), t0 = a.currentTime + (o.t || 0); |
| const dur = o.dur || 0.2, atk = o.attack || 0.012, rel = o.rel || dur * 0.85; |
| const osc = a.createOscillator(); |
| osc.type = o.type || "sine"; |
| osc.frequency.setValueAtTime(freq, t0); |
| if (o.glideTo) osc.frequency.exponentialRampToValueAtTime(o.glideTo, t0 + dur); |
| if (o.detune) osc.detune.value = o.detune; |
| const g = a.createGain(); |
| g.gain.setValueAtTime(0.0001, t0); |
| g.gain.exponentialRampToValueAtTime(o.gain || 0.1, t0 + atk); |
| g.gain.exponentialRampToValueAtTime(0.0001, t0 + atk + rel); |
| osc.connect(g).connect(bus); |
| osc.start(t0); osc.stop(t0 + atk + rel + 0.05); |
| |
| if (o.warm) tone(freq * o.warm, { ...o, warm: 0, gain: (o.gain || 0.1) * 0.35 }); |
| } |
|
|
| |
| function noise(o = {}) { |
| if (muted) return; |
| const a = ac(), t0 = a.currentTime + (o.t || 0), dur = o.dur || 0.08; |
| const buf = a.createBuffer(1, Math.floor(a.sampleRate * dur), a.sampleRate); |
| const d = buf.getChannelData(0); |
| for (let i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1; |
| const src = a.createBufferSource(); src.buffer = buf; |
| const f = a.createBiquadFilter(); |
| f.type = o.filter || "lowpass"; |
| f.frequency.setValueAtTime(o.cutoff || 700, t0); |
| f.Q.value = o.q || 0.7; |
| if (o.sweep) f.frequency.linearRampToValueAtTime(o.sweep, t0 + dur); |
| const g = a.createGain(); |
| g.gain.setValueAtTime(0.0001, t0); |
| g.gain.exponentialRampToValueAtTime(o.gain || 0.04, t0 + (o.attack || 0.005)); |
| g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur); |
| src.connect(f).connect(g).connect(bus); |
| src.start(t0); src.stop(t0 + dur + 0.02); |
| } |
|
|
| |
| function seq(freqs, gap, o = {}) { |
| freqs.forEach((f, i) => tone(f, { ...o, t: (o.t || 0) + i * gap })); |
| } |
|
|
| |
| let bed = null; |
| function ambientStart() { |
| if (bed) return; |
| const a = ac(); |
| const g = a.createGain(); g.gain.value = 0.0001; g.connect(bus); |
| g.gain.exponentialRampToValueAtTime(0.013, a.currentTime + 3); |
| const f = a.createBiquadFilter(); |
| f.type = "lowpass"; f.frequency.value = 380; f.Q.value = 0.4; f.connect(g); |
| const o1 = a.createOscillator(); o1.type = "sine"; o1.frequency.value = N.C3; |
| const o2 = a.createOscillator(); o2.type = "sine"; o2.frequency.value = N.C3 + 0.6; |
| const o3 = a.createOscillator(); o3.type = "sine"; o3.frequency.value = N.G3; |
| const g3 = a.createGain(); g3.gain.value = 0.4; |
| o1.connect(f); o2.connect(f); o3.connect(g3).connect(f); |
| |
| const lfo = a.createOscillator(); lfo.frequency.value = 0.06; |
| const lg = a.createGain(); lg.gain.value = 60; |
| lfo.connect(lg).connect(f.frequency); |
| [o1, o2, o3, lfo].forEach((x) => x.start()); |
| bed = { g, nodes: [o1, o2, o3, lfo] }; |
| } |
| function ambientStop() { |
| if (!bed) return; |
| const a = ac(), { g, nodes } = bed; |
| g.gain.exponentialRampToValueAtTime(0.0001, a.currentTime + 1.2); |
| nodes.forEach((x) => { try { x.stop(a.currentTime + 1.4); } catch {} }); |
| bed = null; |
| } |
|
|
| |
| function musicStart() { |
| try { |
| if (!musicEl) { |
| musicEl = new Audio(MUSIC_SRC); |
| musicEl.loop = true; musicEl.preload = "auto"; |
| musicEl.style.display = "none"; |
| document.body.appendChild(musicEl); |
| } |
| musicEl.muted = muted; |
| const a = ac(); |
| if (a && !musicWired) { |
| try { |
| const src = a.createMediaElementSource(musicEl); |
| musicGain = a.createGain(); musicGain.gain.value = 0.0001; |
| src.connect(musicGain).connect(master); |
| musicGain.gain.exponentialRampToValueAtTime(MUSIC_VOL, a.currentTime + 2.5); |
| musicWired = true; |
| } catch { musicEl.volume = MUSIC_VOL; } |
| } else if (!a) { |
| musicEl.volume = MUSIC_VOL; |
| } else if (musicGain) { |
| musicGain.gain.exponentialRampToValueAtTime(MUSIC_VOL, a.currentTime + 1.5); |
| } |
| const p = musicEl.play(); |
| if (p && p.catch) p.catch(() => {}); |
| } catch {} |
| } |
| function musicStop() { |
| if (!musicEl) return; |
| try { |
| if (musicGain && ctx) { |
| musicGain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + 1.2); |
| setTimeout(() => { try { musicEl.pause(); } catch {} }, 1300); |
| } else { musicEl.pause(); } |
| } catch {} |
| } |
|
|
| |
| let lastFoot = 0, footToggle = 0, lastType = 0; |
|
|
| export const sfx = { |
| |
| footstep() { |
| const now = performance.now(); |
| if (now - lastFoot < 250) return; |
| lastFoot = now; footToggle ^= 1; |
| noise({ dur: 0.06, cutoff: footToggle ? 540 : 600, q: 1.1, gain: 0.022 }); |
| tone(footToggle ? N.C3 : N.E3, { type: "sine", dur: 0.05, gain: 0.012 }); |
| }, |
| bump() { noise({ dur: 0.11, cutoff: 300, gain: 0.03 }); |
| tone(90, { type: "sine", dur: 0.12, gain: 0.035, glideTo: 70 }); }, |
| ambientStart, ambientStop, musicStart, musicStop, |
|
|
| |
| open() { seq([N.E5, N.A5], 0.06, { type: "sine", gain: 0.06, dur: 0.22 }); }, |
| close() { seq([N.A5, N.E5], 0.06, { type: "sine", gain: 0.045, dur: 0.2 }); }, |
| type() { |
| const now = performance.now(); |
| if (now - lastType < 55) return; |
| lastType = now; |
| tone(350 + Math.random() * 36, { type: "triangle", dur: 0.035, |
| gain: 0.016, attack: 0.002 }); |
| }, |
| click() { tone(N.A5, { type: "triangle", dur: 0.1, gain: 0.05, warm: 0.5 }); }, |
| send() { seq([N.G5, N.C6], 0.05, { type: "sine", gain: 0.06, dur: 0.16 }); }, |
| prompt() { tone(N.D6, { type: "sine", dur: 0.12, gain: 0.028 }); }, |
| comic() { noise({ dur: 0.34, cutoff: 420, sweep: 1800, gain: 0.04 }); |
| seq([N.G5, N.C6, N.E6], 0.07, { type: "sine", gain: 0.04, dur: 0.26, warm: 0.5 }); }, |
|
|
| |
| bubble() { tone(N.E5, { type: "sine", dur: 0.2, gain: 0.06, glideTo: N.A5 }); }, |
| amberBubble() { tone(N.D5, { type: "sine", dur: 0.22, gain: 0.05, glideTo: N.G4 }); }, |
| heart() { seq([N.E5, N.A5, N.C6], 0.07, { type: "sine", gain: 0.045, dur: 0.24 }); |
| tone(N.E6, { t: 0.2, dur: 0.32, gain: 0.022 }); }, |
| newspaper() { noise({ dur: 0.4, cutoff: 380, sweep: 1700, gain: 0.04 }); |
| noise({ t: 0.32, dur: 0.05, cutoff: 2400, gain: 0.03 }); |
| noise({ t: 0.4, dur: 0.05, cutoff: 2000, gain: 0.025 }); }, |
| envelope() { noise({ dur: 0.3, cutoff: 1100, sweep: 520, gain: 0.035, |
| filter: "bandpass", q: 0.8 }); }, |
| mail() { seq([N.G5, N.C6], 0.13, { type: "sine", gain: 0.05, dur: 0.32, warm: 0.5 }); }, |
| phoneRingStart() { |
| if (sfx._ring) return; |
| const ring = () => { seq([N.E5, N.G5], 0.12, { type: "sine", gain: 0.04, dur: 0.22 }); |
| seq([N.E5, N.G5], 0.12, { type: "sine", gain: 0.04, dur: 0.22, t: 0.3 }); }; |
| ring(); sfx._ring = setInterval(ring, 1400); |
| }, |
| phoneRingStop() { if (sfx._ring) { clearInterval(sfx._ring); sfx._ring = null; } }, |
|
|
| |
| moneyUp() { seq([N.C5, N.E5, N.G5, N.C6], 0.06, |
| { type: "triangle", gain: 0.055, dur: 0.24, warm: 0.5 }); }, |
| moneyDown() { seq([N.A5, N.G5, N.E5, N.C5], 0.07, |
| { type: "sine", gain: 0.05, dur: 0.26 }); }, |
| disaster() { noise({ dur: 0.28, cutoff: 220, gain: 0.05 }); |
| tone(72, { type: "sine", dur: 0.4, gain: 0.05, glideTo: 54 }); }, |
| titleSwap() { seq([N.D6, N.G6], 0.04, { type: "sine", gain: 0.02, dur: 0.12 }); }, |
| trail() { noise({ dur: 0.04, cutoff: 3200, gain: 0.013, filter: "highpass" }); }, |
| gift() { seq([N.G5, N.A5, N.C6], 0.05, { type: "triangle", gain: 0.045, dur: 0.2 }); }, |
| coffee() { [N.C5, N.E5, N.G5].forEach((f) => |
| tone(f, { type: "sine", gain: 0.038, dur: 0.5 })); }, |
|
|
| |
| gold() { seq([N.G5, N.D6, N.G6], 0.08, { type: "sine", gain: 0.045, dur: 0.5 }); }, |
| wipe() { noise({ dur: 0.35, cutoff: 550, sweep: 1900, gain: 0.035 }); }, |
| boardIn() { noise({ dur: 0.08, cutoff: 380, gain: 0.03 }); |
| noise({ t: 0.16, dur: 0.08, cutoff: 420, gain: 0.03 }); }, |
| wrongSlide() { tone(N.G5, { type: "triangle", dur: 0.42, gain: 0.06, glideTo: N.D5 }); |
| tone(N.E5, { t: 0.4, type: "triangle", dur: 0.32, gain: 0.05, glideTo: N.C5 }); }, |
| score() { seq([N.C5, N.E5, N.G5], 0.08, { type: "triangle", gain: 0.05, dur: 0.2 }); |
| tone(N.C6, { t: 0.28, dur: 0.5, gain: 0.05, warm: 0.5 }); }, |
| stamp() { noise({ dur: 0.18, cutoff: 280, gain: 0.05 }); |
| tone(110, { type: "sine", dur: 0.26, gain: 0.05, glideTo: 88 }); }, |
|
|
| |
| win() { seq([N.C5, N.E5, N.G5, N.C6, N.E6, N.G6], 0.09, |
| { type: "triangle", gain: 0.055, dur: 0.42, warm: 0.5 }); |
| [N.C5, N.E5, N.G5].forEach((f) => tone(f, { t: 0.6, dur: 0.9, gain: 0.04 })); }, |
| lose(tier) { |
| const sets = { |
| hit_target: [N.C5, N.E5, N.G5], above_600k: [N.A4, N.G4, N.E4], |
| "300k_to_600k": [N.G4, N.E4, N.C4], below_300k: [N.E4, N.D4, N.C4, N.C3], |
| }; |
| seq(sets[tier] || sets["300k_to_600k"], 0.22, |
| { type: "sine", gain: 0.05, dur: 0.4 }); }, |
| confetti() { for (let i = 0; i < 12; i++) { |
| const notes = [N.C6, N.D6, N.E6, N.G6, N.A5]; |
| tone(notes[(Math.random() * notes.length) | 0], |
| { t: Math.random() * 1.1, type: "sine", dur: 0.18, gain: 0.03 }); } }, |
| review() { [N.C4, N.G4, N.C5].forEach((f, i) => |
| tone(f, { type: "sine", dur: 1.1, gain: 0.035, t: i * 0.08 })); }, |
| play() { seq([N.G5, N.C6], 0.07, { type: "sine", gain: 0.05, dur: 0.25 }); }, |
|
|
| toggleMute() { |
| muted = !muted; |
| if (master) master.gain.exponentialRampToValueAtTime( |
| muted ? 0.0001 : MASTER, (ctx ? ctx.currentTime : 0) + 0.15); |
| if (musicEl) musicEl.muted = muted; |
| return muted; |
| }, |
| }; |
|
|