games: real recorded foley for cards and chips, synth for the rest
The card and chip sounds never convinced as oscillators, so they're now CC0 recordings (Kenney's casino pack) under /static/audio/casino, embedded in the binary. Six names (card, deal, flip, shuffle, chip, sweep) play several rotating takes with a little pitch jitter; everything melodic stays synthesised. PeteSFX's API is unchanged and synthesis is the fallback while an ogg is still decoding, so no caller changed and the table is never silent.
This commit is contained in:
21
internal/web/static/audio/casino/LICENSE.txt
Normal file
21
internal/web/static/audio/casino/LICENSE.txt
Normal file
@@ -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
|
||||
BIN
internal/web/static/audio/casino/cardFan1.ogg
Normal file
BIN
internal/web/static/audio/casino/cardFan1.ogg
Normal file
Binary file not shown.
BIN
internal/web/static/audio/casino/cardFan2.ogg
Normal file
BIN
internal/web/static/audio/casino/cardFan2.ogg
Normal file
Binary file not shown.
BIN
internal/web/static/audio/casino/cardPlace1.ogg
Normal file
BIN
internal/web/static/audio/casino/cardPlace1.ogg
Normal file
Binary file not shown.
BIN
internal/web/static/audio/casino/cardPlace2.ogg
Normal file
BIN
internal/web/static/audio/casino/cardPlace2.ogg
Normal file
Binary file not shown.
BIN
internal/web/static/audio/casino/cardPlace3.ogg
Normal file
BIN
internal/web/static/audio/casino/cardPlace3.ogg
Normal file
Binary file not shown.
BIN
internal/web/static/audio/casino/cardPlace4.ogg
Normal file
BIN
internal/web/static/audio/casino/cardPlace4.ogg
Normal file
Binary file not shown.
BIN
internal/web/static/audio/casino/cardShuffle.ogg
Normal file
BIN
internal/web/static/audio/casino/cardShuffle.ogg
Normal file
Binary file not shown.
BIN
internal/web/static/audio/casino/cardSlide1.ogg
Normal file
BIN
internal/web/static/audio/casino/cardSlide1.ogg
Normal file
Binary file not shown.
BIN
internal/web/static/audio/casino/cardSlide2.ogg
Normal file
BIN
internal/web/static/audio/casino/cardSlide2.ogg
Normal file
Binary file not shown.
BIN
internal/web/static/audio/casino/cardSlide3.ogg
Normal file
BIN
internal/web/static/audio/casino/cardSlide3.ogg
Normal file
Binary file not shown.
BIN
internal/web/static/audio/casino/cardSlide4.ogg
Normal file
BIN
internal/web/static/audio/casino/cardSlide4.ogg
Normal file
Binary file not shown.
BIN
internal/web/static/audio/casino/cardSlide5.ogg
Normal file
BIN
internal/web/static/audio/casino/cardSlide5.ogg
Normal file
Binary file not shown.
BIN
internal/web/static/audio/casino/cardSlide6.ogg
Normal file
BIN
internal/web/static/audio/casino/cardSlide6.ogg
Normal file
Binary file not shown.
BIN
internal/web/static/audio/casino/chipLay1.ogg
Normal file
BIN
internal/web/static/audio/casino/chipLay1.ogg
Normal file
Binary file not shown.
BIN
internal/web/static/audio/casino/chipLay2.ogg
Normal file
BIN
internal/web/static/audio/casino/chipLay2.ogg
Normal file
Binary file not shown.
BIN
internal/web/static/audio/casino/chipLay3.ogg
Normal file
BIN
internal/web/static/audio/casino/chipLay3.ogg
Normal file
Binary file not shown.
BIN
internal/web/static/audio/casino/chipsHandle2.ogg
Normal file
BIN
internal/web/static/audio/casino/chipsHandle2.ogg
Normal file
Binary file not shown.
BIN
internal/web/static/audio/casino/chipsHandle4.ogg
Normal file
BIN
internal/web/static/audio/casino/chipsHandle4.ogg
Normal file
Binary file not shown.
BIN
internal/web/static/audio/casino/chipsHandle6.ogg
Normal file
BIN
internal/web/static/audio/casino/chipsHandle6.ogg
Normal file
Binary file not shown.
@@ -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 */
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user