|  | let playing = [];
let looping = {};
let loopGains = {};
let waiting = {};
let audioContext;
let gainControl;
let audioBaseUrl;
let storyName;
let audioDict = {};
function setVolume(vol) {
  gainControl.gain.value = vol;
}
// play some sound
function playSfx(name) {
  if (audioDict[name] == undefined) {
    if (waiting[name]) {
      waiting[name].push({
        type: "sfx",
        name: name
      });
      console.warn(name + " isn't ready yet");
      return;
    }
    console.error(name + " is not loaded yet, dingus");
    return;
  }
  let src = audioContext.createBufferSource();
  src.buffer = audioDict[name];
  src.connect(gainControl);
  playing.push(src);
  src.name = name;
  src.onended = (event) => src.done = true;
  src.start(0);
}
function playLoop(name, volume=1) {
  if (audioDict[name] === undefined) {
    if (waiting[name]) {
      waiting[name].push({
        type: "loop",
        name: name
      });
      console.warn(name + " isn't ready yet");
      return;
    }
    console.error(name + " is not loaded yet, dingus");
    return;
  }
  
  loopGains[name].gain.value = volume;
  // if already playing, just keep going
  if (looping[name] && !looping[name].done) {
    return;
  }
  let src = audioContext.createBufferSource();
  src.buffer = audioDict[name];
  src.connect(loopGains[name]);
  loopGains[name].connect(gainControl);
  looping[name] = src;
  src.name = name;
  src.onended = (event) => src.done = true;
  src.loop = true;
  src.start(0);
}
function stopSfx(name) {
  playing.map(item => {
    if (item.name == name)
      item.stop();
  } );
  cleanPlaying();
}
function stopAllSfx() {
  playing.map(item => item.stop());
  cleanPlaying();
}
function stopLoop(name) {
  if (looping[name]) {
    looping[name].stop();
    delete looping[name];
  }
}
function stopAllLoops() {
  Object.entries(looping).forEach(([key, val]) => {
    val.stop();
    delete looping[key];
  });
}
function stopAllSound() {
  stopAllSfx();
  stopAllLoops();
}
function cleanPlaying() {
  playing = playing.filter(item => !item.done);
}
// asynchronously load an audio file
function loadAudio(name, flush=false) {
  // are we already trying to get the audio?
  if (waiting[name]) {
    return;
  }
  // do we already have the audio?
  if (audioDict[name] && !flush) {
    return;
  }
  loopGains[name] = audioContext.createGain();
  waiting[name] = [];
  // is the audio already stored locally?
  if (!flush) {
    checkCache(
      "audio",
      name,
      (data) => parseAudioData(name, data),
      () => loadRemoteAudio(name)
    );
  } else {
    loadRemoteAudio(name);
  }
}
function cacheAndParse(name, data) {
  storeCache("audio", name, data.slice(0));
  parseAudioData(name, data);
}
function parseAudioData(name, data) {
  audioContext.decodeAudioData(data, function(buffer) {
    audioDict[name] = buffer;
    waiting[name].forEach(queued => {
      if (queued.type == "sfx") {
        playSfx(name);
      }
      if (queued.type == "loop") {
        playLoop(name);
      }
    });
    delete waiting[name];
  }, function(e){
    console.error("Error with decoding audio data" + e.err);
    delete waiting[name];
  });
}
function loadRemoteAudio(name) {
  let xhr = new XMLHttpRequest();
  xhr.open("GET", audioBaseUrl + name, true);
  xhr.responseType = "arraybuffer";
  xhr.onload = (res) => {
    if (xhr.status == 200)
      cacheAndParse(name, xhr.response);
    else {
      console.error("Couldn't load " + name);
      delete waiting[name];
    }
  }
  xhr.onerror = (xhr) => {
    console.error("Couldn't load " + name);
  }
  xhr.send();
}
// check if the content is cached
function checkCache(type, name, hit, miss) {
  const req = window.indexedDB.open("cache", 3);
  req.onsuccess = () => {
    const db = req.result;
    const tx = db.transaction([type], "readonly");
    const audio = tx.objectStore(type);
    const read = audio.get([storyName, name]);
    read.onsuccess = (event) => {
      const res = event.target.result;
      if (res) {
        console.log("cache hit on " + name);
        hit(res.content);
      } else {
        console.log("cache miss on " + name);
        miss();
      }
    }
    tx.oncomplete = () => {
      db.close();
    }
  }
}
function initAudio(story) {
  if (!audioContext)
    audioContext = new (window.AudioContext || window.webkitAudioContext)();
  if (!gainControl) {
    gainControl = audioContext.createGain();
    gainControl.gain.value = 1;
    gainControl.connect(audioContext.destination);
  }
  createCache();
  audioBaseUrl = "./media/" + story.id + "/audio/";
  storyName = story.id;
  story.sounds.forEach(sound => {
    loadAudio(sound);
  });
}
// caching stuff here
function storeCache(type, name, blob) {
  const req = window.indexedDB.open("cache", 3);
  req.onsuccess = () => {
    const db = req.result;
    const tx = db.transaction([type], "readwrite");
    const audio = tx.objectStore(type);
    const update = audio.put({
      story: storyName,
      name: name,
      content: blob
    });
    tx.oncomplete = () => {
      db.close();
    }
  }
}
// if the indexedDB table doesn't exist at all, make it
function createCache() {
  let idb = window.indexedDB;
  let req = idb.open("cache", 3);
  req.onupgradeneeded = event => {
    const db = event.target.result;
    if (event.oldVersion > 0 && event.oldVersion < 3) {
      db.deleteObjectStore("audio");
    }
    const audio = db.createObjectStore("audio", { keyPath: ["story", "name"] });
  }
  req.onerror = event => {
    alert("Couldn't open the database?");
  }
}
 |