Spaces:
Running
Running
Download audio.js from CyberMax-tools/stoopshake: direct link, hf CLI and curl.
- Browser
- Download file 9.92 kB
-
https://huggingface.co/spaces/CyberMax-tools/stoopshake/resolve/main/audio.js
- Command line
-
hf download hf://spaces/CyberMax-tools/stoopshake/audio.js
-
curl -L -o audio.js https://huggingface.co/spaces/CyberMax-tools/stoopshake/resolve/main/audio.js
9.92 kB
| // Stoopshake sound: everything synthesised in Web Audio (no samples, original): dice clacks on concrete / wall / each other, | |
| // the shake in the hand, UI taps, result stings, and "Stoop Night", an original 88 BPM boom-bap loop (kick, snare, swung hats, | |
| // a warm electric-piano chord loop and a bass line, with soft vinyl crackle). Music and effects toggle separately. | |
| // (c) 2026 CyberMax. All rights reserved. | |
| let ac = null, master = null, sfxBus = null, musBus = null, noiseBuf = null; | |
| const st = { sfx: true, music: true }; | |
| try { const s = JSON.parse(localStorage.getItem('ssk-audio') || 'null'); if (s) Object.assign(st, s); } catch { /* private mode */ } | |
| const save = () => { try { localStorage.setItem('ssk-audio', JSON.stringify(st)); } catch { /* */ } }; | |
| export function ensure() { | |
| if (ac) { if (ac.state === 'suspended') ac.resume().catch(() => {}); return ac; } | |
| const C = window.AudioContext || window.webkitAudioContext; if (!C) return null; | |
| ac = new C(); master = ac.createGain(); master.gain.value = 0.9; | |
| const comp = ac.createDynamicsCompressor(); comp.threshold.value = -14; comp.ratio.value = 3; master.connect(comp).connect(ac.destination); | |
| sfxBus = ac.createGain(); sfxBus.gain.value = st.sfx ? 1 : 0; sfxBus.connect(master); | |
| musBus = ac.createGain(); musBus.gain.value = st.music ? 0.42 : 0; musBus.connect(master); | |
| noiseBuf = ac.createBuffer(1, ac.sampleRate * 2, ac.sampleRate); const d = noiseBuf.getChannelData(0); for (let i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1; | |
| return ac; | |
| } | |
| export const settings = () => ({ ...st }); | |
| export function setSfx(on) { st.sfx = !!on; save(); if (sfxBus) sfxBus.gain.value = on ? 1 : 0; } | |
| export function setMusic(on) { st.music = !!on; save(); if (musBus) musBus.gain.setTargetAtTime(on ? 0.42 : 0, ac.currentTime, 0.2); if (on) startMusic(); } | |
| /** Expose the mix as a MediaStream for clip recording (optional). */ | |
| export function stream() { if (!ensure()) return null; if (!master._dest) { master._dest = ac.createMediaStreamDestination(); master.connect(master._dest); } return master._dest.stream; } | |
| function noise(t, dur, { f = 2000, q = 1, type = 'bandpass', g = 0.5, bus = sfxBus, attack = 0.001 } = {}) { | |
| const s = ac.createBufferSource(); s.buffer = noiseBuf; s.playbackRate.value = 0.8 + Math.random() * 0.4; | |
| const fl = ac.createBiquadFilter(); fl.type = type; fl.frequency.value = f; fl.Q.value = q; | |
| const e = ac.createGain(); e.gain.setValueAtTime(0, t); e.gain.linearRampToValueAtTime(g, t + attack); e.gain.exponentialRampToValueAtTime(0.0008, t + dur); | |
| s.connect(fl).connect(e).connect(bus); s.start(t, Math.random() * 1.5); s.stop(t + dur + 0.05); | |
| } | |
| function tone(t, f, dur, { type = 'sine', g = 0.3, bus = sfxBus, f2 = null, attack = 0.004 } = {}) { | |
| const o = ac.createOscillator(); o.type = type; o.frequency.setValueAtTime(f, t); if (f2) o.frequency.exponentialRampToValueAtTime(f2, t + dur); | |
| const e = ac.createGain(); e.gain.setValueAtTime(0, t); e.gain.linearRampToValueAtTime(g, t + attack); e.gain.exponentialRampToValueAtTime(0.0008, t + dur); | |
| o.connect(e).connect(bus); o.start(t); o.stop(t + dur + 0.05); | |
| } | |
| // ---------- dice | |
| let lastHit = 0; | |
| /** One impact: kind 'floor' | 'wall' | 'die', v = impact speed. Rate-limited so piles don't buzz. */ | |
| export function hit(kind, v) { | |
| if (!ensure() || !st.sfx) return; const t = ac.currentTime; if (t - lastHit < 0.018) return; lastHit = t; | |
| const k = Math.min(1, v / 14); | |
| if (kind === 'die') { noise(t, 0.05, { f: 3800 + Math.random() * 900, q: 7, g: 0.55 * k }); tone(t, 2600 + Math.random() * 500, 0.035, { type: 'triangle', g: 0.12 * k }); } | |
| else if (kind === 'wall') { noise(t, 0.09, { f: 1700, q: 2.5, g: 0.7 * k }); tone(t, 220, 0.08, { g: 0.2 * k, f2: 120 }); } | |
| else { noise(t, 0.07, { f: 2300 + Math.random() * 600, q: 3.2, g: 0.6 * k }); tone(t, 1300 + Math.random() * 300, 0.03, { type: 'triangle', g: 0.08 * k }); } | |
| } | |
| let rattleT = 0; | |
| /** Dice shaken in the hand (call every frame while held; k = shake strength 0..1). */ | |
| export function rattle(k = 1) { | |
| if (!ensure() || !st.sfx) return; const t = ac.currentTime; if (t - rattleT < 0.055 + Math.random() * 0.05) return; rattleT = t; | |
| for (let i = 0; i < 2; i++) noise(t + i * 0.012, 0.03, { f: 3200 + Math.random() * 1500, q: 6, g: 0.18 * k }); | |
| } | |
| export function whoosh(power = 0.6) { if (!ensure() || !st.sfx) return; const t = ac.currentTime; noise(t, 0.28, { f: 900 + power * 900, q: 0.7, g: 0.25, attack: 0.06 }); } | |
| export function tap() { if (!ensure() || !st.sfx) return; const t = ac.currentTime; tone(t, 880, 0.06, { type: 'square', g: 0.05 }); tone(t + 0.03, 1320, 0.05, { type: 'square', g: 0.04 }); } | |
| export function chips(n = 4) { if (!ensure() || !st.sfx) return; const t = ac.currentTime; for (let i = 0; i < n; i++) { noise(t + i * 0.045, 0.04, { f: 5200, q: 9, g: 0.22 }); tone(t + i * 0.045, 3100 + i * 60, 0.03, { type: 'triangle', g: 0.05 }); } } | |
| /** Result stings: '456' horn stab + crowd-ish burst, 'trips' rising chord, 'point' two notes, '123' low drop, 'none' shrug. */ | |
| export function sting(kind) { | |
| if (!ensure() || !st.sfx) return; const t = ac.currentTime; | |
| if (kind === '456') { [0, 4, 7, 12].forEach((s, i) => tone(t + i * 0.06, 349.2 * 2 ** (s / 12), 0.5, { type: 'sawtooth', g: 0.07 })); noise(t, 1.1, { f: 1500, q: 0.4, g: 0.18, attack: 0.15 }); tone(t, 55, 0.6, { g: 0.5, f2: 40 }); } | |
| else if (kind === 'trips') { [0, 3, 7, 10, 14].forEach((s, i) => tone(t + i * 0.07, 293.7 * 2 ** (s / 12), 0.55, { type: 'triangle', g: 0.09 })); } | |
| else if (kind === 'point') { tone(t, 523.3, 0.16, { type: 'triangle', g: 0.12 }); tone(t + 0.12, 659.3, 0.22, { type: 'triangle', g: 0.12 }); } | |
| else if (kind === '123') { tone(t, 220, 0.7, { type: 'sawtooth', g: 0.09, f2: 70 }); tone(t, 110, 0.7, { g: 0.3, f2: 45 }); } | |
| else if (kind === 'win') { [0, 7, 12].forEach((s, i) => tone(t + i * 0.08, 440 * 2 ** (s / 12), 0.3, { type: 'square', g: 0.05 })); } | |
| else if (kind === 'lose') { tone(t, 330, 0.25, { type: 'square', g: 0.05, f2: 220 }); } | |
| else { tone(t, 392, 0.1, { type: 'triangle', g: 0.07 }); tone(t + 0.1, 370, 0.14, { type: 'triangle', g: 0.07 }); } | |
| } | |
| // ---------- "Stoop Night": original boom-bap loop (scheduled ahead with the audio clock) | |
| const BPM = 88, SPB = 60 / BPM, STEP = SPB / 4; | |
| // two-bar pattern, 32 sixteenths. K = kick, S = snare, H = hat | |
| const KICK = [1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0]; | |
| const SNARE = [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1]; | |
| // chords (MIDI notes): Fm9 -> Db maj7 -> Bbm9 -> C7(b9) one per half bar... over 2 bars: one per bar half | |
| const CHORDS = [[53, 56, 60, 63, 67], [49, 53, 56, 60, 65], [46, 49, 53, 56, 60], [48, 52, 55, 58, 61]]; | |
| const BASS = [41, 0, 0, 41, 0, 0, 44, 0, 37, 0, 0, 37, 0, 0, 39, 0, 34, 0, 0, 34, 0, 0, 37, 0, 36, 0, 0, 36, 0, 0, 40, 43]; | |
| const mf = (m) => 440 * 2 ** ((m - 69) / 12); | |
| let playing = false, nextT = 0, stepI = 0, timer = 0, crackle = null; | |
| function kick(t) { const o = ac.createOscillator(), e = ac.createGain(); o.frequency.setValueAtTime(130, t); o.frequency.exponentialRampToValueAtTime(42, t + 0.14); e.gain.setValueAtTime(0.95, t); e.gain.exponentialRampToValueAtTime(0.001, t + 0.42); o.connect(e).connect(musBus); o.start(t); o.stop(t + 0.45); noise(t, 0.02, { f: 3000, q: 1, g: 0.15, bus: musBus }); } | |
| function snare(t) { noise(t, 0.2, { f: 1900, q: 0.9, g: 0.55, bus: musBus }); tone(t, 190, 0.12, { type: 'triangle', g: 0.28, bus: musBus, f2: 150 }); } | |
| function hat(t, open) { noise(t, open ? 0.16 : 0.035, { f: 8200, q: 1.3, type: 'highpass', g: open ? 0.13 : 0.1, bus: musBus }); } | |
| function keys(t, notes, dur) { // warm EP: sine + soft FM bell, slow tremolo | |
| for (const n of notes) { const f = mf(n); const o = ac.createOscillator(), m = ac.createOscillator(), mg = ac.createGain(), e = ac.createGain(); | |
| o.type = 'sine'; o.frequency.value = f; m.frequency.value = f * 2; mg.gain.value = f * 0.9; m.connect(mg).connect(o.frequency); | |
| e.gain.setValueAtTime(0, t); e.gain.linearRampToValueAtTime(0.045, t + 0.02); e.gain.exponentialRampToValueAtTime(0.012, t + dur * 0.7); e.gain.exponentialRampToValueAtTime(0.0005, t + dur); | |
| mg.gain.setValueAtTime(f * 0.9, t); mg.gain.exponentialRampToValueAtTime(f * 0.05, t + 0.4); | |
| o.connect(e).connect(musBus); o.start(t); m.start(t); o.stop(t + dur + 0.05); m.stop(t + dur + 0.05); } | |
| } | |
| function bass(t, n) { if (!n) return; const o = ac.createOscillator(), fl = ac.createBiquadFilter(), e = ac.createGain(); o.type = 'sawtooth'; o.frequency.value = mf(n - 12); fl.type = 'lowpass'; fl.frequency.value = 260; | |
| e.gain.setValueAtTime(0, t); e.gain.linearRampToValueAtTime(0.26, t + 0.01); e.gain.exponentialRampToValueAtTime(0.001, t + STEP * 2.6); o.connect(fl).connect(e).connect(musBus); o.start(t); o.stop(t + STEP * 3); } | |
| function schedule() { | |
| while (nextT < ac.currentTime + 0.25) { | |
| const i = stepI % 32, swing = i % 2 ? STEP * 0.18 : 0, t = nextT + swing; | |
| if (KICK[i]) kick(t); if (SNARE[i]) snare(t); hat(t, i % 8 === 6); | |
| if (i % 8 === 0) keys(t, CHORDS[(i / 8) | 0], STEP * 7.5); | |
| bass(t, BASS[i]); | |
| nextT += STEP; stepI++; | |
| } | |
| } | |
| export function startMusic() { | |
| if (!ensure() || !st.music || playing) return; playing = true; nextT = ac.currentTime + 0.1; stepI = 0; | |
| timer = setInterval(schedule, 60); schedule(); | |
| if (!crackle) { crackle = ac.createBufferSource(); crackle.buffer = noiseBuf; crackle.loop = true; const f = ac.createBiquadFilter(); f.type = 'highpass'; f.frequency.value = 6000; const g = ac.createGain(); g.gain.value = 0.012; crackle.connect(f).connect(g).connect(musBus); crackle.start(); } | |
| } | |
| export function stopMusic() { playing = false; clearInterval(timer); } | |
| document.addEventListener('visibilitychange', () => { if (!ac) return; if (document.hidden) ac.suspend().catch(() => {}); else ac.resume().catch(() => {}); }); | |