// 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. // // Two rules hold the whole file up. // // **Nothing is built until it's asked for.** A browser will not let a page make a // noise before the user has touched it, and a page that builds an AudioContext on // load gets one in the "suspended" state and a warning in the console for its // trouble. So the context is made on the first sound anybody actually asks for — // which, in a casino, is a click on a chip. // // **Muted means silent, not quiet.** When the room is muted nothing is // constructed, nothing is scheduled and no context is opened. Mute is the // default-off switch for the entire file, checked before anything else happens. // // Exposed as window.PeteSFX. Nothing in here knows what blackjack is. (function () { "use strict"; var KEY = "pete.sfx.off"; var ctx = null; var master = null; var noiseBuf = null; var muted = false; var listeners = []; try { muted = localStorage.getItem(KEY) === "1"; } catch (e) {} // ---- the instrument -------------------------------------------------------- function boot() { if (ctx) return ctx; var AC = window.AudioContext || window.webkitAudioContext; if (!AC) return null; try { ctx = new AC(); } catch (e) { return null; } master = ctx.createGain(); master.gain.value = 0.45; // the room is furniture, not a nightclub master.connect(ctx.destination); return ctx; } // A second of white noise, made once and played at different speeds and through // different filters for the rest of the session. Card flicks, riffles, the // transient on a chip: all of them are this buffer wearing a different coat. function noise() { if (noiseBuf) return noiseBuf; var n = ctx.sampleRate; noiseBuf = ctx.createBuffer(1, n, n); var d = noiseBuf.getChannelData(0); for (var i = 0; i < n; i++) d[i] = Math.random() * 2 - 1; return noiseBuf; } // env is the shape of every sound in here: up fast, down slow, and never to // zero — an exponential ramp to actual zero is undefined, and a browser that // hits one either throws or clicks. function env(g, t0, attack, decay, peak) { g.gain.setValueAtTime(0.0001, t0); g.gain.exponentialRampToValueAtTime(Math.max(0.0001, peak), t0 + attack); g.gain.exponentialRampToValueAtTime(0.0001, t0 + attack + decay); } // tone is one note. function tone(t0, freq, o) { o = o || {}; var osc = ctx.createOscillator(); var g = ctx.createGain(); osc.type = o.type || "sine"; osc.frequency.setValueAtTime(freq, t0); if (o.to) osc.frequency.exponentialRampToValueAtTime(o.to, t0 + (o.glide || o.decay || 0.1)); env(g, t0, o.attack || 0.004, o.decay || 0.12, o.gain == null ? 0.3 : o.gain); osc.connect(g).connect(master); osc.start(t0); osc.stop(t0 + (o.attack || 0.004) + (o.decay || 0.12) + 0.02); } // hiss is a burst of the noise buffer through a filter, which is what a card, // a riffle and the knock on a chip all are. function hiss(t0, o) { o = o || {}; var src = ctx.createBufferSource(); src.buffer = noise(); src.playbackRate.value = o.rate || 1; var f = ctx.createBiquadFilter(); f.type = o.filter || "bandpass"; f.frequency.setValueAtTime(o.freq || 2000, t0); if (o.sweepTo) f.frequency.exponentialRampToValueAtTime(o.sweepTo, t0 + (o.decay || 0.1)); f.Q.value = o.q == null ? 1.1 : o.q; var g = ctx.createGain(); env(g, t0, o.attack || 0.003, o.decay || 0.09, o.gain == null ? 0.3 : o.gain); src.connect(f).connect(g).connect(master); src.start(t0); src.stop(t0 + (o.attack || 0.003) + (o.decay || 0.09) + 0.02); } // ---- the sounds ------------------------------------------------------------ // // Each one takes the time it starts at, and a `v` — a small per-call variation, // usually the index of the card or chip in a run. Four cards dealt with the // identical sample four times is a machine gun; four cards each a semitone off // is a hand dealing. var SOUNDS = { // A card landing: mostly air, with a slap on the front of it. card: function (t, v) { hiss(t, { freq: 1800 + v * 140, q: 0.9, decay: 0.075, rate: 1 + v * 0.05, gain: 0.3 }); hiss(t, { filter: "highpass", freq: 4200, decay: 0.02, gain: 0.16 }); }, // Dealing is the same card, lower and softer: it's being placed, not thrown. deal: function (t, v) { hiss(t, { freq: 1250 + v * 110, q: 1.1, decay: 0.085, rate: 0.9, gain: 0.24 }); }, // A card turning over — shorter, brighter, and it comes in two halves, which // is what a flip actually sounds like. flip: function (t) { hiss(t, { freq: 2600, q: 1.4, decay: 0.035, gain: 0.22 }); hiss(t + 0.045, { freq: 1500, q: 1.0, decay: 0.06, gain: 0.24 }); }, // The riffle. One long sweep of noise with the filter climbing through it, // and a rattle laid over the top so it isn't just a shhh. shuffle: function (t) { hiss(t, { freq: 700, sweepTo: 3400, q: 0.7, attack: 0.02, decay: 0.34, gain: 0.2 }); for (var i = 0; i < 9; i++) { hiss(t + 0.03 + i * 0.032, { freq: 2400 + i * 130, q: 2.2, decay: 0.02, gain: 0.09 }); } }, // A chip landing on a chip. Two detuned partials well above the staff, with a // knock underneath — the knock is most of what makes it read as *clay* rather // than as a bell. chip: function (t, v) { var f = 1750 + v * 55; tone(t, f, { type: "sine", decay: 0.07, gain: 0.16 }); tone(t, f * 1.34, { type: "sine", decay: 0.05, gain: 0.1 }); hiss(t, { filter: "lowpass", freq: 900, decay: 0.03, gain: 0.3, q: 0.5 }); }, // Chips sliding away across felt. sweep: function (t) { hiss(t, { freq: 1600, sweepTo: 500, q: 0.6, attack: 0.015, decay: 0.26, gain: 0.16 }); }, // Four notes up. This is the only sound in the room allowed to be pleased. win: function (t) { [523.25, 659.25, 783.99, 1046.5].forEach(function (f, i) { tone(t + i * 0.085, f, { type: "triangle", decay: 0.26, gain: 0.24 }); }); }, // Two notes down, and the second one is flat. Nobody needs telling twice. lose: function (t) { tone(t, 311.13, { type: "triangle", decay: 0.24, gain: 0.22 }); tone(t + 0.15, 233.08, { type: "triangle", decay: 0.42, gain: 0.22 }); }, // Nothing happened and nothing was lost. push: function (t) { tone(t, 392, { type: "sine", decay: 0.2, gain: 0.16 }); }, // A button. Short enough that you feel it rather than hear it. blip: function (t) { tone(t, 880, { type: "sine", decay: 0.055, gain: 0.12 }); }, // UNO, called. A bright stab plus a zip upwards: it is a shout, and it is the // one thing on that table you want to feel good about pressing. uno: function (t) { [659.25, 830.61, 987.77].forEach(function (f) { tone(t, f, { type: "triangle", decay: 0.3, gain: 0.2 }); }); tone(t, 500, { type: "sawtooth", to: 1600, glide: 0.16, decay: 0.18, gain: 0.09 }); }, // Got them. A rising snap with a thump on the end — the sound of a finger // being pointed. catch: function (t) { tone(t, 300, { type: "square", to: 900, glide: 0.11, decay: 0.12, gain: 0.13 }); hiss(t + 0.1, { filter: "lowpass", freq: 1100, decay: 0.1, gain: 0.34, q: 0.7 }); tone(t + 0.1, 174.61, { type: "triangle", decay: 0.22, gain: 0.2 }); }, // Something bad landed on you: a stack, a +4, a catch you walked into. bad: function (t) { tone(t, 155.56, { type: "square", to: 98, glide: 0.18, decay: 0.22, gain: 0.14 }); hiss(t, { filter: "lowpass", freq: 700, decay: 0.14, gain: 0.3, q: 0.6 }); }, // A clock you can hear. Used sparingly, and never in a run of more than a few. tick: function (t) { hiss(t, { freq: 3200, q: 3, decay: 0.02, gain: 0.14 }); }, }; // ---- the door -------------------------------------------------------------- // A browser will not make a noise until the user has touched the page, and a // context built before that arrives suspended. This wakes it on the first real // gesture — after which play() can schedule freely. function wake() { if (ctx && ctx.state === "suspended") ctx.resume().catch(function () {}); } ["pointerdown", "keydown", "touchstart"].forEach(function (ev) { window.addEventListener(ev, wake, { passive: true }); }); // play makes a sound, or — if the room is muted — makes nothing at all, opens no // context and builds no graph. // // `v` is the variation: pass the index of the card or chip in a run and the // sound moves a little each time, which is the difference between a hand and a // machine. `delay` schedules it ahead in seconds, so a caller that knows a card // lands in 400ms can say so rather than sleeping. function play(name, opts) { if (muted) return; var s = SOUNDS[name]; if (!s) return; if (!boot()) return; wake(); if (ctx.state !== "running") return; // not yet touched: no sound, and no error try { s(ctx.currentTime + ((opts && opts.delay) || 0), ((opts && opts.v) || 0)); } catch (e) { /* a sound is never worth throwing over */ } } function setMuted(on) { muted = !!on; try { if (muted) localStorage.setItem(KEY, "1"); else localStorage.removeItem(KEY); } catch (e) {} if (window.PetePrefs) window.PetePrefs.push(); listeners.forEach(function (fn) { fn(muted); }); if (!muted) play("blip"); // so you know what you just turned back on } window.PeteSFX = { play: play, muted: function () { return muted; }, toggle: function () { setMuted(!muted); return muted; }, set: setMuted, // onChange lets the mute button re-label itself without owning the state. onChange: function (fn) { listeners.push(fn); fn(muted); }, }; })();