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?"); } }