// 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); },
  };
})();
