qFelix's picture
Brad Did Something - Gradio/FastAPI Space on HF
78c0f6e
Raw
History Blame Contribute Delete
12.3 kB
// Cozy WebAudio sound — warm and quiet, not techy. Sine/triangle voices run
// through a soft lowpass + a little reverb; notes are pentatonic so nothing
// ever clashes; envelopes fade in/out so there are no clicks. Low master gain.
// No audio files. Mute with M.
let ctx = null, bus = null, master = null, muted = false, dead = false;
const MASTER = 0.32; // overall cosiness ceiling — everything sits under this
// looping background music (mp3 in static/audio/). Routed past the SFX
// lowpass/reverb so it stays full, but still under master + M mute.
const MUSIC_SRC = "/static/audio/bgm.mp3";
const MUSIC_VOL = 0.15; // sits gently under the SFX — tune to taste
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);
// warm everything: a gentle lowpass shaves the harsh top end
const lp = ctx.createBiquadFilter();
lp.type = "lowpass"; lp.frequency.value = 2600; lp.Q.value = 0.5;
lp.connect(master);
// a little room: short generated reverb, mixed in low
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); // dry
bus.connect(verb); // wet
}
if (ctx.state === "suspended") ctx.resume();
return ctx;
} catch {
dead = true; // no audio device — game keeps working, silently
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;
}
// pentatonic C-major — any mix of these sounds pleasant and warm
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,
};
// one soft voice with a click-free envelope
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);
// a faint octave/fifth layer warms plucks without making them louder
if (o.warm) tone(freq * o.warm, { ...o, warm: 0, gain: (o.gain || 0.1) * 0.35 });
}
// filtered noise — footsteps, paper, whooshes (cloth/wood, never beeps)
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);
}
// play a little melody/arpeggio
function seq(freqs, gap, o = {}) {
freqs.forEach((f, i) => tone(f, { ...o, t: (o.t || 0) + i * gap }));
}
// ---- ambient office bed: a near-silent warm drone, always running ----
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);
// a very slow filter drift so it breathes
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;
}
// ---- looping background music (mp3) ----
function musicStart() {
try {
if (!musicEl) {
musicEl = new Audio(MUSIC_SRC);
musicEl.loop = true; musicEl.preload = "auto";
musicEl.style.display = "none";
document.body.appendChild(musicEl); // attach for robustness + inspection
}
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); // past the SFX lowpass/reverb
musicGain.gain.exponentialRampToValueAtTime(MUSIC_VOL, a.currentTime + 2.5);
musicWired = true;
} catch { musicEl.volume = MUSIC_VOL; }
} else if (!a) {
musicEl.volume = MUSIC_VOL; // no WebAudio — plain element
} else if (musicGain) {
musicGain.gain.exponentialRampToValueAtTime(MUSIC_VOL, a.currentTime + 1.5);
}
const p = musicEl.play();
if (p && p.catch) p.catch(() => {}); // autoplay block — ignore
} 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 {}
}
// ---- rate limiters so footsteps / typing never stack into a buzz ----
let lastFoot = 0, footToggle = 0, lastType = 0;
export const sfx = {
// movement & ambient
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,
// dialogue & UI
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 }); // page reveal
seq([N.G5, N.C6, N.E6], 0.07, { type: "sine", gain: 0.04, dur: 0.26, warm: 0.5 }); },
// event arrivals
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; } },
// outcomes
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 })); },
// boardroom / presentation
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 }); },
// endings
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; // covers both routed + plain-element paths
return muted;
},
};