diff --git a/internal/web/static/audio/casino/LICENSE.txt b/internal/web/static/audio/casino/LICENSE.txt new file mode 100644 index 0000000..b99597a --- /dev/null +++ b/internal/web/static/audio/casino/LICENSE.txt @@ -0,0 +1,21 @@ + + + Casino Audio + + by Kenney Vleugels (Kenney.nl) + + ------------------------------ + + License (Creative Commons Zero, CC0) + http://creativecommons.org/publicdomain/zero/1.0/ + + You may use these assets in personal and commercial projects. + Credit (Kenney or www.kenney.nl) would be nice but is not mandatory. + + ------------------------------ + + Donate: http://support.kenney.nl + Request: http://request.kenney.nl + + Follow on Twitter for updates: + @KenneyNL \ No newline at end of file diff --git a/internal/web/static/audio/casino/cardFan1.ogg b/internal/web/static/audio/casino/cardFan1.ogg new file mode 100644 index 0000000..2b22dc3 Binary files /dev/null and b/internal/web/static/audio/casino/cardFan1.ogg differ diff --git a/internal/web/static/audio/casino/cardFan2.ogg b/internal/web/static/audio/casino/cardFan2.ogg new file mode 100644 index 0000000..ff0cde2 Binary files /dev/null and b/internal/web/static/audio/casino/cardFan2.ogg differ diff --git a/internal/web/static/audio/casino/cardPlace1.ogg b/internal/web/static/audio/casino/cardPlace1.ogg new file mode 100644 index 0000000..42bbfa8 Binary files /dev/null and b/internal/web/static/audio/casino/cardPlace1.ogg differ diff --git a/internal/web/static/audio/casino/cardPlace2.ogg b/internal/web/static/audio/casino/cardPlace2.ogg new file mode 100644 index 0000000..8424bec Binary files /dev/null and b/internal/web/static/audio/casino/cardPlace2.ogg differ diff --git a/internal/web/static/audio/casino/cardPlace3.ogg b/internal/web/static/audio/casino/cardPlace3.ogg new file mode 100644 index 0000000..69198d9 Binary files /dev/null and b/internal/web/static/audio/casino/cardPlace3.ogg differ diff --git a/internal/web/static/audio/casino/cardPlace4.ogg b/internal/web/static/audio/casino/cardPlace4.ogg new file mode 100644 index 0000000..7ced521 Binary files /dev/null and b/internal/web/static/audio/casino/cardPlace4.ogg differ diff --git a/internal/web/static/audio/casino/cardShuffle.ogg b/internal/web/static/audio/casino/cardShuffle.ogg new file mode 100644 index 0000000..8c73494 Binary files /dev/null and b/internal/web/static/audio/casino/cardShuffle.ogg differ diff --git a/internal/web/static/audio/casino/cardSlide1.ogg b/internal/web/static/audio/casino/cardSlide1.ogg new file mode 100644 index 0000000..414b570 Binary files /dev/null and b/internal/web/static/audio/casino/cardSlide1.ogg differ diff --git a/internal/web/static/audio/casino/cardSlide2.ogg b/internal/web/static/audio/casino/cardSlide2.ogg new file mode 100644 index 0000000..bd6800a Binary files /dev/null and b/internal/web/static/audio/casino/cardSlide2.ogg differ diff --git a/internal/web/static/audio/casino/cardSlide3.ogg b/internal/web/static/audio/casino/cardSlide3.ogg new file mode 100644 index 0000000..20623e4 Binary files /dev/null and b/internal/web/static/audio/casino/cardSlide3.ogg differ diff --git a/internal/web/static/audio/casino/cardSlide4.ogg b/internal/web/static/audio/casino/cardSlide4.ogg new file mode 100644 index 0000000..1ad7eba Binary files /dev/null and b/internal/web/static/audio/casino/cardSlide4.ogg differ diff --git a/internal/web/static/audio/casino/cardSlide5.ogg b/internal/web/static/audio/casino/cardSlide5.ogg new file mode 100644 index 0000000..f2173e8 Binary files /dev/null and b/internal/web/static/audio/casino/cardSlide5.ogg differ diff --git a/internal/web/static/audio/casino/cardSlide6.ogg b/internal/web/static/audio/casino/cardSlide6.ogg new file mode 100644 index 0000000..b009003 Binary files /dev/null and b/internal/web/static/audio/casino/cardSlide6.ogg differ diff --git a/internal/web/static/audio/casino/chipLay1.ogg b/internal/web/static/audio/casino/chipLay1.ogg new file mode 100644 index 0000000..50f077a Binary files /dev/null and b/internal/web/static/audio/casino/chipLay1.ogg differ diff --git a/internal/web/static/audio/casino/chipLay2.ogg b/internal/web/static/audio/casino/chipLay2.ogg new file mode 100644 index 0000000..2a78757 Binary files /dev/null and b/internal/web/static/audio/casino/chipLay2.ogg differ diff --git a/internal/web/static/audio/casino/chipLay3.ogg b/internal/web/static/audio/casino/chipLay3.ogg new file mode 100644 index 0000000..aadccef Binary files /dev/null and b/internal/web/static/audio/casino/chipLay3.ogg differ diff --git a/internal/web/static/audio/casino/chipsHandle2.ogg b/internal/web/static/audio/casino/chipsHandle2.ogg new file mode 100644 index 0000000..ac59f80 Binary files /dev/null and b/internal/web/static/audio/casino/chipsHandle2.ogg differ diff --git a/internal/web/static/audio/casino/chipsHandle4.ogg b/internal/web/static/audio/casino/chipsHandle4.ogg new file mode 100644 index 0000000..5d12030 Binary files /dev/null and b/internal/web/static/audio/casino/chipsHandle4.ogg differ diff --git a/internal/web/static/audio/casino/chipsHandle6.ogg b/internal/web/static/audio/casino/chipsHandle6.ogg new file mode 100644 index 0000000..dc717bd Binary files /dev/null and b/internal/web/static/audio/casino/chipsHandle6.ogg differ diff --git a/internal/web/static/js/casino-sfx.js b/internal/web/static/js/casino-sfx.js index 6a1ed09..340b9b0 100644 --- a/internal/web/static/js/casino-sfx.js +++ b/internal/web/static/js/casino-sfx.js @@ -1,12 +1,18 @@ // The noise the room makes. // -// There are no audio files here and there is nothing to download. Every sound in -// this casino is *made* — an oscillator, a burst of filtered noise, an envelope — -// the same bargain the weather engine takes with its clouds. A card is a short -// slap of noise through a bandpass; a chip is two detuned sines with a click on -// the front; a win is four notes going up. It costs about six kilobytes and no -// round trips, and it means a sound can be pitched, stretched and detuned per -// call instead of being the same wav 300 times. +// The room speaks in two voices. The cards and chips are *recorded* — real clay +// and real card stock, CC0 foley from Kenney's casino pack, sitting as small ogg +// files under /static/audio/casino. Synthesis never quite caught the grain of a +// chip landing on a chip, so for those we stopped trying. Everything melodic — +// the wins, the losses, the little ticks — is still *made*: an oscillator, a +// burst of filtered noise, an envelope, the same bargain the weather engine takes +// with its clouds. A win is four notes going up; a chip is a recording of a chip. +// +// A name is a recording if it appears in SAMPLES and synthesised if it appears in +// SOUNDS. The recordings guard against the one weakness of a sample — the same wav +// 300 times is a machine gun — by keeping several takes per sound and rotating +// through them, and by nudging every play a few percent off pitch. The synthesised +// half was always immune to that; it varies itself. // // Two rules hold the whole file up. // @@ -21,6 +27,10 @@ // default-off switch for the entire file, checked before anything else happens. // // Exposed as window.PeteSFX. Nothing in here knows what blackjack is. +// +// If a recording hasn't finished decoding on the frame it's first needed, that one +// call falls through to the synthesised version — so the table is never silent +// while the ogg loads, and by the second card everything is real. (function () { "use strict"; @@ -108,6 +118,82 @@ src.stop(t0 + (o.attack || 0.003) + (o.decay || 0.09) + 0.02); } + // ---- the recordings -------------------------------------------------------- + // + // The foley. Each name maps to a handful of takes; `v` (the index of the card or + // chip in a run) picks which one and how far off pitch it lands, so a dealt hand + // is four different cards rather than one card four times. `gain` is per-sound + // and multiplies the master, exactly like the synthesised sounds' peak does. + + var SAMPLE_BASE = "/static/audio/casino/"; + var SAMPLES = { + // A card thrown down onto the table. + card: { files: ["cardPlace1", "cardPlace2", "cardPlace3", "cardPlace4"], gain: 0.6, vary: 0.04 }, + // A card slid into place — softer, longer than a throw. + deal: { files: ["cardSlide1", "cardSlide2", "cardSlide3", "cardSlide4", "cardSlide5", "cardSlide6"], gain: 0.5, vary: 0.05 }, + // A card flicked over. + flip: { files: ["cardFan1", "cardFan2"], gain: 0.5, vary: 0.04 }, + // The riffle. + shuffle: { files: ["cardShuffle"], gain: 0.6 }, + // A clay chip set down on a stack. + chip: { files: ["chipLay1", "chipLay2", "chipLay3"], gain: 0.6, vary: 0.05 }, + // Chips gathered and slid away. + sweep: { files: ["chipsHandle2", "chipsHandle4", "chipsHandle6"], gain: 0.5, vary: 0.03 }, + }; + + // Decoded buffers by filename. A value of null means "claimed, in flight or + // decoding"; false means "we tried and it failed, never bother again"; an + // AudioBuffer means ready. + var buffers = {}; + var warmed = false; + + // decode fetches a file and turns it into a buffer, exactly once per filename. + // decodeAudioData exists in a modern promise flavour and an old callback one; + // this handles both so the foley works on older Safari too. + function decode(file) { + if (file in buffers) return; // ready, in flight, or known-bad + buffers[file] = null; + fetch(SAMPLE_BASE + file + ".ogg") + .then(function (r) { return r.ok ? r.arrayBuffer() : Promise.reject(); }) + .then(function (ab) { + return new Promise(function (res, rej) { + var p = ctx.decodeAudioData(ab, res, rej); + if (p && p.then) p.then(res, rej); + }); + }) + .then(function (buf) { buffers[file] = buf; }) + .catch(function () { buffers[file] = false; }); + } + + // warm pulls every take down once the context is awake, so that after the first + // click the whole set is decoded and ready and nothing has to fall back. + function warm() { + if (warmed || !ctx) return; + warmed = true; + for (var name in SAMPLES) SAMPLES[name].files.forEach(decode); + } + + // sample plays one recorded take. It returns false — rather than making a noise — + // when the name isn't a recording or its buffer isn't decoded yet, which is the + // caller's signal to reach for the synthesised version instead. + function sample(name, t0, v) { + var cfg = SAMPLES[name]; + if (!cfg) return false; + var i = Math.abs(Math.round(v)); + var buf = buffers[cfg.files[i % cfg.files.length]]; + if (!buf) return false; // not decoded yet, or failed: let synth cover it + var src = ctx.createBufferSource(); + src.buffer = buf; + // A few percent off pitch, spread across a small range keyed to v, so a run of + // the same sound never reads as a loop. + if (cfg.vary) src.playbackRate.value = 1 + (((i % 5) - 2) * cfg.vary); + var g = ctx.createGain(); + g.gain.value = cfg.gain == null ? 0.6 : cfg.gain; + src.connect(g).connect(master); + src.start(t0); + return true; + } + // ---- the sounds ------------------------------------------------------------ // // Each one takes the time it starts at, and a `v` — a small per-call variation, @@ -217,6 +303,7 @@ // gesture — after which play() can schedule freely. function wake() { if (ctx && ctx.state === "suspended") ctx.resume().catch(function () {}); + warm(); } ["pointerdown", "keydown", "touchstart"].forEach(function (ev) { window.addEventListener(ev, wake, { passive: true }); @@ -231,13 +318,18 @@ // lands in 400ms can say so rather than sleeping. function play(name, opts) { if (muted) return; - var s = SOUNDS[name]; - if (!s) return; + if (!SOUNDS[name] && !SAMPLES[name]) return; // known to neither voice if (!boot()) return; wake(); if (ctx.state !== "running") return; // not yet touched: no sound, and no error + var t0 = ctx.currentTime + ((opts && opts.delay) || 0); + var v = (opts && opts.v) || 0; try { - s(ctx.currentTime + ((opts && opts.delay) || 0), ((opts && opts.v) || 0)); + // A recording if we have one decoded; the synthesised take otherwise — both + // for names that are only ever synthesised, and for the first call of a + // recorded name while its ogg is still loading. + if (sample(name, t0, v)) return; + if (SOUNDS[name]) SOUNDS[name](t0, v); } catch (e) { /* a sound is never worth throwing over */ }