You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
0x40-web/src/js/SoundManager.js

632 lines
24 KiB

/* Copyright (c) 2015 William Toohey <will@mon.im>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
(function(window, document) {
"use strict";
function SoundManager(core) {
this.core = core;
this.playing = false;
this.playbackRate = 1;
this.song = null;
this.initPromise = null;
/* Lower level audio and timing info */
this.context = null; // Audio context, Web Audio API
this.oggSupport = false;
this.buildSource = null;
this.loopSource = null;
this.buildup = null;
this.loop = null;
this.startTime = 0; // File start time - 0 is loop start, not build start
this.buildLength = 0;
this.loopLength = 0; // For calculating beat lengths
// Volume
this.gainNode = null;
this.mute = false;
this.lastVol = 1;
// Visualiser
this.vReady = false;
this.vBars = 0;
this.vTotalBars = 0;
this.splitter = null;
this.analysers = [];
this.analyserArrays = [];
this.logArrays = [];
this.binCutoffs = [];
this.linBins = 0;
this.logBins = 0;
this.maxBinLin = 0;
}
SoundManager.prototype.init = function() {
if(!this.initPromise) {
this.initPromise = new Promise((resolve, reject) => {
// Check Web Audio API Support
try {
// More info at http://caniuse.com/#feat=audio-api
window.AudioContext = window.AudioContext || window.webkitAudioContext;
// These don't always exist
AudioContext.prototype.suspend = AudioContext.prototype.suspend || (() => {return Promise.resolve();});
AudioContext.prototype.resume = AudioContext.prototype.resume || (() => {return Promise.resolve();});
this.context = new window.AudioContext();
this.gainNode = this.context.createGain();
this.gainNode.connect(this.context.destination);
} catch(e) {
reject(Error("Web Audio API not supported in this browser."));
return;
}
resolve();
}).then(() => {
// check for .ogg support - if not, we'll have to load the ogg decoder
return new Promise((resolve, reject) => {
this.context.decodeAudioData(miniOgg, success => {
this.oggSupport = true;
resolve();
}, error => {
this.oggSupport = false;
resolve();
});
});
}).then(() => {
return new Promise((resolve, reject) => {
// See if our audio decoder is working
let audioWorker;
try {
audioWorker = this.createWorker();
} catch(e) {
console.log(e);
reject(Error("Audio Worker cannot be started - correct path set in defaults?"));
return;
}
let pingListener = event => {
audioWorker.terminate();
resolve();
};
audioWorker.addEventListener('message', pingListener, false);
audioWorker.addEventListener('error', () => {
reject(Error("Audio Worker cannot be started - correct path set in defaults?"));
}, false);
audioWorker.postMessage({ping:true, ogg:this.oggSupport});
});
}).then(() => {
return new Promise((resolve, reject) => {
// iOS and other some mobile browsers - unlock the context as
// it starts in a suspended state
if(this.context.state != "running") {
this.core.warning("We're about to load about 10MB of stuff. Tap to begin!");
let unlocker = () => {
// create empty buffer
let buffer = this.context.createBuffer(1, 1, 22050);
let source = this.context.createBufferSource();
source.buffer = buffer;
// connect to output (your speakers)
source.connect( this.context.destination);
// play the file
source.start(0);
window.removeEventListener('touchend', unlocker);
window.removeEventListener('click', unlocker);
this.core.clearMessage();
resolve();
};
window.addEventListener('touchend', unlocker, false);
window.addEventListener('click', unlocker, false);
} else {
resolve();
}
});
});
}
return this.initPromise;
};
SoundManager.prototype.playSong = function(song, playBuild, forcePlay) {
let p = Promise.resolve();
// Editor forces play on audio updates
if(this.song == song && !forcePlay) {
return p;
}
this.stop();
this.song = song;
if(!song || (!song.sound)) { // null song
return p;
}
// if there's a fadeout happening from AutoSong, kill it
this.gainNode.gain.cancelScheduledValues(0);
// Reset original volume
this.setVolume(this.lastVol);
if(this.mute) {
this.setMute(true);
}
p = p.then(() => {
return this.loadSong(song);
}).then(buffers => {
// To prevent race condition if you press "next" twice fast
if(song != this.song) {
return Promise.reject("Song changed between load and play - this message can be ignored");
}
this.buildup = buffers.buildup;
this.buildLength = this.buildup ? this.buildup.duration : 0;
this.loop = buffers.loop;
this.loopLength = this.loop.duration;
// This fixes sync issues on Firefox and slow machines.
return this.context.suspend();
}).then(() => {
if(playBuild) {
this.seek(-this.buildLength, true);
} else {
this.seek(0, true);
}
return this.context.resume();
}).then(() => {
this.playing = true;
});
return p;
};
SoundManager.prototype.stop = function(dontDeleteBuffers) {
if (this.playing) {
if(this.buildSource) {
this.buildSource.stop(0);
this.buildSource.disconnect();
this.buildSource = null;
if(!dontDeleteBuffers)
this.buildup = null;
}
// arg required for mobile webkit
this.loopSource.stop(0);
// TODO needed?
this.loopSource.disconnect();
this.loopSource = null;
if(!dontDeleteBuffers)
this.loop = null;
this.vReady = false;
this.playing = false;
this.startTime = 0;
}
};
SoundManager.prototype.setRate = function(rate) {
// Double speed is more than enough. Famous last words?
rate = Math.max(Math.min(rate, 2), 0.25);
let time = this.clampedTime();
this.playbackRate = rate;
this.seek(time);
};
SoundManager.prototype.seek = function(time, noPlayingUpdate) {
if(!this.song) {
return;
}
//console.log("Seeking to " + time);
// Clamp the blighter
time = Math.min(Math.max(time, -this.buildLength), this.loopLength);
this.stop(true);
if(!this.loop) {
return;
}
this.loopSource = this.context.createBufferSource();
this.loopSource.buffer = this.loop;
this.loopSource.playbackRate.value = this.playbackRate;
this.loopSource.loop = true;
this.loopSource.loopStart = 0;
this.loopSource.loopEnd = this.loopLength;
this.loopSource.connect(this.gainNode);
if(time < 0 && this.buildup) {
this.buildSource = this.context.createBufferSource();
this.buildSource.buffer = this.buildup;
this.buildSource.playbackRate.value = this.playbackRate;
this.buildSource.connect(this.gainNode);
this.buildSource.start(0, this.buildLength + time);
this.loopSource.start(this.context.currentTime - (time / this.playbackRate));
} else {
this.loopSource.start(0, time);
}
this.startTime = this.context.currentTime - (time / this.playbackRate);
if(!noPlayingUpdate) {
this.playing = true;
}
this.initVisualiser();
this.core.recalcBeatIndex();
};
// In seconds, relative to the loop start
SoundManager.prototype.currentTime = function() {
if(!this.playing) {
return 0;
}
return (this.context.currentTime - this.startTime) * this.playbackRate;
};
SoundManager.prototype.clampedTime = function() {
let time = this.currentTime();
if(time > 0) {
time %= this.loopLength;
}
return time;
};
SoundManager.prototype.loadSong = function(song) {
if(song._loadPromise) {
/* Caused when moving back/forwards rapidly.
The sound is still loading. We reject this promise, and the already
running decode will finish and resolve instead.
NOTE: If anything but playSong calls loadSong, this idea is broken. */
return Promise.reject("Song changed between load and play - this message can be ignored");
}
let buffers = {loop: null, buildup: null};
let promises = [this.loadBuffer(song, "sound").then(buffer => {
buffers.loop = buffer;
})];
if(song.buildup) {
promises.push(this.loadBuffer(song, "buildup").then(buffer => {
buffers.buildup = buffer;
}));
} else {
this.buildLength = 0;
}
song._loadPromise = Promise.all(promises)
.then(() => {
song._loadPromise = null;
return buffers;
});
return song._loadPromise;
};
SoundManager.prototype.loadBuffer = function(song, soundName) {
let buffer = song[soundName];
// Is this an ogg file?
let view = new Uint8Array(buffer);
// Signature for ogg file: OggS
if(this.oggSupport && view[0] == 0x4F && view[1] == 0x67 && view[2] == 0x67 && view[3] == 0x53) {
// As we don't control decodeAudioData, we cannot do fast transfers and must copy
let backup = buffer.slice(0);
return new Promise((resolve, reject) => {
this.context.decodeAudioData(buffer, result => {
resolve(result);
}, error => {
reject(Error("decodeAudioData failed to load track"));
});
}).then(result => {
// restore copied buffer
song[soundName] = backup;
return result;
});
} else { // Use our JS decoder
return new Promise((resolve, reject) => {
let audioWorker = this.createWorker();
audioWorker.addEventListener('error', () => {
reject(Error("Audio Worker failed to convert track"));
}, false);
audioWorker.addEventListener('message', e => {
let decoded = e.data;
audioWorker.terminate();
// restore transferred buffer
song[soundName] = decoded.arrayBuffer;
if(decoded.error) {
reject(new Error(decoded.error));
return;
}
// Convert to real audio buffer
let audio = this.audioBufFromRaw(decoded.rawAudio);
resolve(audio);
}, false);
// transfer the buffer to save time
audioWorker.postMessage({buffer: buffer, ogg: this.oggSupport}, [buffer]);
});
}
};
// Converts continuous PCM array to Web Audio API friendly format
SoundManager.prototype.audioBufFromRaw = function(raw) {
let buffer = raw.array;
let channels = raw.channels;
let samples = buffer.length/channels;
let audioBuf = this.context.createBuffer(channels, samples, raw.sampleRate);
for(let i = 0; i < channels; i++) {
// Offset is in bytes, length is in elements
let channel = new Float32Array(buffer.buffer , i * samples * 4, samples);
audioBuf.copyToChannel(channel, i, 0);
}
return audioBuf;
};
SoundManager.prototype.createWorker = function() {
return new Worker(this.core.settings.defaults.workersPath + 'audio-worker.js');
};
SoundManager.prototype.initVisualiser = function(bars) {
// When restarting the visualiser
if(!bars) {
bars = this.vTotalBars;
}
this.vReady = false;
this.vTotalBars = bars;
for(let i = 0; i < this.analysers.length; i++) {
this.analysers[i].disconnect();
}
if(this.splitter) {
this.splitter.disconnect();
this.splitter = null;
}
this.analysers = [];
this.analyserArrays = [];
this.logArrays = [];
this.binCutoffs = [];
this.linBins = 0;
this.logBins = 0;
this.maxBinLin = 0;
this.attachVisualiser();
};
SoundManager.prototype.attachVisualiser = function() {
if(!this.playing || this.vReady) {
return;
}
// Get our info from the loop
let channels = this.loopSource.channelCount;
// In case channel counts change, this is changed each time
this.splitter = this.context.createChannelSplitter(channels);
// Connect to the gainNode so we get buildup stuff too
this.loopSource.connect(this.splitter);
if(this.buildSource) {
this.buildSource.connect(this.splitter);
}
// Split display up into each channel
this.vBars = Math.floor(this.vTotalBars/channels);
for(let i = 0; i < channels; i++) {
let analyser = this.context.createAnalyser();
// big fft buffers are new-ish
try {
analyser.fftSize = 8192;
} catch(err) {
analyser.fftSize = 2048;
}
// Chosen because they look nice, no maths behind it
analyser.smoothingTimeConstant = 0.6;
analyser.minDecibels = -70;
analyser.maxDecibels = -25;
this.analyserArrays.push(new Uint8Array(analyser.frequencyBinCount));
analyser.getByteTimeDomainData(this.analyserArrays[i]);
this.splitter.connect(analyser, i);
this.analysers.push(analyser);
this.logArrays.push(new Uint8Array(this.vBars));
}
let binCount = this.analysers[0].frequencyBinCount;
let binWidth = this.loopSource.buffer.sampleRate / binCount;
// first 2kHz are linear
this.maxBinLin = Math.floor(2000/binWidth);
// Don't stretch the first 2kHz, it looks awful
this.linBins = Math.min(this.maxBinLin, Math.floor(this.vBars/2));
// Only go up to 22KHz
let maxBinLog = Math.floor(22000/binWidth);
let logBins = this.vBars - this.linBins;
let logLow = Math.log2(2000);
let logDiff = Math.log2(22000) - logLow;
for(let i = 0; i < logBins; i++) {
let cutoff = i * (logDiff/logBins) + logLow;
let freqCutoff = Math.pow(2, cutoff);
let binCutoff = Math.floor(freqCutoff / binWidth);
this.binCutoffs.push(binCutoff);
}
this.vReady = true;
};
SoundManager.prototype.sumArray = function(array, low, high) {
let total = 0;
for(let i = low; i <= high; i++) {
total += array[i];
}
return total/(high-low+1);
};
SoundManager.prototype.getVisualiserData = function() {
if(!this.vReady) {
return null;
}
for(let a = 0; a < this.analyserArrays.length; a++) {
let data = this.analyserArrays[a];
let result = this.logArrays[a];
this.analysers[a].getByteFrequencyData(data);
for(let i = 0; i < this.linBins; i++) {
let scaled = Math.round(i * this.maxBinLin / this.linBins);
result[i] = data[scaled];
}
result[this.linBins] = data[this.binCutoffs[0]];
for(let i = this.linBins+1; i < this.vBars; i++) {
let cutoff = i - this.linBins;
result[i] = this.sumArray(data, this.binCutoffs[cutoff-1],
this.binCutoffs[cutoff]);
}
}
return this.logArrays;
};
SoundManager.prototype.setMute = function(mute) {
if(!this.mute && mute) { // muting
this.lastVol = this.gainNode.gain.value;
}
if(mute) {
this.gainNode.gain.value = 0;
} else {
this.gainNode.gain.value = this.lastVol;
}
this.core.userInterface.updateVolume(this.gainNode.gain.value);
this.mute = mute;
return mute;
};
SoundManager.prototype.toggleMute = function() {
return this.setMute(!this.mute);
};
SoundManager.prototype.decreaseVolume = function() {
this.setMute(false);
let val = Math.max(this.gainNode.gain.value - 0.1, 0);
this.setVolume(val);
};
SoundManager.prototype.increaseVolume = function() {
this.setMute(false);
let val = Math.min(this.gainNode.gain.value + 0.1, 1);
this.setVolume(val);
};
SoundManager.prototype.setVolume = function(vol) {
this.gainNode.gain.value = vol;
this.lastVol = vol;
this.core.userInterface.updateVolume(vol);
};
SoundManager.prototype.fadeOut = function(callback) {
if(!this.mute) {
// Firefox hackery
this.gainNode.gain.setValueAtTime(this.lastVol, this.context.currentTime);
this.gainNode.gain.exponentialRampToValueAtTime(0.01, this.context.currentTime + 2);
}
setTimeout(callback, 2000);
};
let miniOggRaw =
"T2dnUwACAAAAAAAAAADFYgAAAAAAAMLKRdwBHgF2b3JiaXMAAAAAAUSsAAAA" +
"AAAAgLsAAAAAAAC4AU9nZ1MAAAAAAAAAAAAAxWIAAAEAAACcKCV2Dzv/////" +
"////////////MgN2b3JiaXMrAAAAWGlwaC5PcmcgbGliVm9yYmlzIEkgMjAx" +
"MjAyMDMgKE9tbmlwcmVzZW50KQAAAAABBXZvcmJpcx9CQ1YBAAABABhjVClG" +
"mVLSSokZc5QxRplikkqJpYQWQkidcxRTqTnXnGusubUghBAaU1ApBZlSjlJp" +
"GWOQKQWZUhBLSSV0EjonnWMQW0nB1phri0G2HIQNmlJMKcSUUopCCBlTjCnF" +
"lFJKQgcldA465hxTjkooQbicc6u1lpZji6l0kkrnJGRMQkgphZJKB6VTTkJI" +
"NZbWUikdc1JSakHoIIQQQrYghA2C0JBVAAABAMBAEBqyCgBQAAAQiqEYigKE" +
"hqwCADIAAASgKI7iKI4jOZJjSRYQGrIKAAACABAAAMBwFEmRFMmxJEvSLEvT" +
"RFFVfdU2VVX2dV3XdV3XdSA0ZBUAAAEAQEinmaUaIMIMZBgIDVkFACAAAABG" +
"KMIQA0JDVgEAAAEAAGIoOYgmtOZ8c46DZjloKsXmdHAi1eZJbirm5pxzzjkn" +
"m3PGOOecc4pyZjFoJrTmnHMSg2YpaCa05pxznsTmQWuqtOacc8Y5p4NxRhjn" +
"nHOatOZBajbW5pxzFrSmOWouxeaccyLl5kltLtXmnHPOOeecc84555xzqhen" +
"c3BOOOecc6L25lpuQhfnnHM+Gad7c0I455xzzjnnnHPOOeecc4LQkFUAABAA" +
"AEEYNoZxpyBIn6OBGEWIacikB92jwyRoDHIKqUejo5FS6iCUVMZJKZ0gNGQV" +
"AAAIAAAhhBRSSCGFFFJIIYUUUoghhhhiyCmnnIIKKqmkoooyyiyzzDLLLLPM" +
"Muuws8467DDEEEMMrbQSS0211VhjrbnnnGsO0lpprbXWSimllFJKKQgNWQUA" +
"gAAAEAgZZJBBRiGFFFKIIaaccsopqKACQkNWAQCAAAACAAAAPMlzREd0REd0" +
"REd0REd0RMdzPEeUREmUREm0TMvUTE8VVdWVXVvWZd32bWEXdt33dd/3dePX" +
"hWFZlmVZlmVZlmVZlmVZlmVZgtCQVQAACAAAgBBCCCGFFFJIIaUYY8wx56CT" +
"UEIgNGQVAAAIACAAAADAURzFcSRHciTJkixJkzRLszzN0zxN9ERRFE3TVEVX" +
"dEXdtEXZlE3XdE3ZdFVZtV1Ztm3Z1m1flm3f933f933f933f933f93UdCA1Z" +
"BQBIAADoSI6kSIqkSI7jOJIkAaEhqwAAGQAAAQAoiqM4juNIkiRJlqRJnuVZ" +
"omZqpmd6qqgCoSGrAABAAAABAAAAAAAomuIppuIpouI5oiNKomVaoqZqriib" +
"suu6ruu6ruu6ruu6ruu6ruu6ruu6ruu6ruu6ruu6ruu6ruu6LhAasgoAkAAA" +
"0JEcyZEcSZEUSZEcyQFCQ1YBADIAAAIAcAzHkBTJsSxL0zzN0zxN9ERP9ExP" +
"FV3RBUJDVgEAgAAAAgAAAAAAMCTDUixHczRJlFRLtVRNtVRLFVVPVVVVVVVV" +
"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVNU3TNE0gNGQlAAAEAMBijcHl" +
"ICElJeXeEMIQk54xJiG1XiEEkZLeMQYVg54yogxy3kLjEIMeCA1ZEQBEAQAA" +
"xiDHEHPIOUepkxI556h0lBrnHKWOUmcpxZhizSiV2FKsjXOOUketo5RiLC12" +
"lFKNqcYCAAACHAAAAiyEQkNWBABRAACEMUgppBRijDmnnEOMKeeYc4Yx5hxz" +
"jjnnoHRSKuecdE5KxBhzjjmnnHNSOieVc05KJ6EAAIAABwCAAAuh0JAVAUCc" +
"AIBBkjxP8jRRlDRPFEVTdF1RNF3X8jzV9ExTVT3RVFVTVW3ZVFVZljzPND3T" +
"VFXPNFXVVFVZNlVVlkVV1W3TdXXbdFXdlm3b911bFnZRVW3dVF3bN1XX9l3Z" +
"9n1Z1nVj8jxV9UzTdT3TdGXVdW1bdV1d90xTlk3XlWXTdW3blWVdd2XZ9zXT" +
"dF3TVWXZdF3ZdmVXt11Z9n3TdYXflWVfV2VZGHZd94Vb15XldF3dV2VXN1ZZ" +
"9n1b14Xh1nVhmTxPVT3TdF3PNF1XdV1fV13X1jXTlGXTdW3ZVF1ZdmXZ911X" +
"1nXPNGXZdF3bNl1Xll1Z9n1XlnXddF1fV2VZ+FVX9nVZ15Xh1m3hN13X91VZ" +
"9oVXlnXh1nVhuXVdGD5V9X1TdoXhdGXf14XfWW5dOJbRdX1hlW3hWGVZOX7h" +
"WJbd95VldF1fWG3ZGFZZFoZf+J3l9n3jeHVdGW7d58y67wzH76T7ytPVbWOZ" +
"fd1ZZl93juEYOr/w46mqr5uuKwynLAu/7evGs/u+soyu6/uqLAu/KtvCseu+" +
"8/y+sCyj7PrCasvCsNq2Mdy+biy/cBzLa+vKMeu+UbZ1fF94CsPzdHVdeWZd" +
"x/Z1dONHOH7KAACAAQcAgAATykChISsCgDgBAI8kiaJkWaIoWZYoiqbouqJo" +
"uq6kaaapaZ5pWppnmqZpqrIpmq4saZppWp5mmpqnmaZomq5rmqasiqYpy6Zq" +
"yrJpmrLsurJtu65s26JpyrJpmrJsmqYsu7Kr267s6rqkWaapeZ5pap5nmqZq" +
"yrJpmq6reZ5qep5oqp4oqqpqqqqtqqosW55nmproqaYniqpqqqatmqoqy6aq" +
"2rJpqrZsqqptu6rs+rJt67ppqrJtqqYtm6pq267s6rIs27ovaZppap5nmprn" +
"maZpmrJsmqorW56nmp4oqqrmiaZqqqosm6aqypbnmaoniqrqiZ5rmqoqy6Zq" +
"2qppmrZsqqotm6Yqy65t+77ryrJuqqpsm6pq66ZqyrJsy77vyqruiqYpy6aq" +
"2rJpqrIt27Lvy7Ks+6JpyrJpqrJtqqouy7JtG7Ns+7pomrJtqqYtm6oq27It" +
"+7os27rvyq5vq6qs67It+7ru+q5w67owvLJs+6qs+ror27pv6zLb9n1E05Rl" +
"UzVt21RVWXZl2fZl2/Z90TRtW1VVWzZN1bZlWfZ9WbZtYTRN2TZVVdZN1bRt" +
"WZZtYbZl4XZl2bdlW/Z115V1X9d949dl3ea6su3Lsq37qqv6tu77wnDrrvAK" +
"AAAYcAAACDChDBQashIAiAIAAIxhjDEIjVLOOQehUco55yBkzkEIIZXMOQgh" +
"lJI5B6GUlDLnIJSSUgihlJRaCyGUlFJrBQAAFDgAAATYoCmxOEChISsBgFQA" +
"AIPjWJbnmaJq2rJjSZ4niqqpqrbtSJbniaJpqqptW54niqapqq7r65rniaJp" +
"qqrr6rpomqapqq7ruroumqKpqqrrurKum6aqqq4ru7Ls66aqqqrryq4s+8Kq" +
"uq4ry7Jt68Kwqq7ryrJs27Zv3Lqu677v+8KRreu6LvzCMQxHAQDgCQ4AQAU2" +
"rI5wUjQWWGjISgAgAwCAMAYhgxBCBiGEkFJKIaWUEgAAMOAAABBgQhkoNGRF" +
"ABAnAAAYQymklFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSCmllFJKKaWU" +
"UkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSqmklFJKKaWU" +
"UkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWU" +
"UkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWU" +
"UkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWU" +
"UkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWU" +
"UkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimVUkoppZRS" +
"SimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRS" +
"SimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFIKAJCKcACQejCh" +
"DBQashIASAUAAIxRSinGnIMQMeYYY9BJKClizDnGHJSSUuUchBBSaS23yjkI" +
"IaTUUm2Zc1JaizHmGDPnpKQUW805h1JSi7HmmmvupLRWa64151paqzXXnHPN" +
"ubQWa64515xzyzHXnHPOOecYc84555xzzgUA4DQ4AIAe2LA6wknRWGChISsB" +
"gFQAAAIZpRhzzjnoEFKMOecchBAihRhzzjkIIVSMOeccdBBCqBhzzDkIIYSQ" +
"OecchBBCCCFzDjroIIQQQgcdhBBCCKGUzkEIIYQQSighhBBCCCGEEDoIIYQQ" +
"QgghhBBCCCGEUkoIIYQQQgmhlFAAAGCBAwBAgA2rI5wUjQUWGrISAAACAIAc" +
"lqBSzoRBjkGPDUHKUTMNQkw50ZliTmozFVOQORCddBIZakHZXjILAACAIAAg" +
"wAQQGCAo+EIIiDEAAEGIzBAJhVWwwKAMGhzmAcADRIREAJCYoEi7uIAuA1zQ" +
"xV0HQghCEIJYHEABCTg44YYn3vCEG5ygU1TqIAAAAAAADADgAQDgoAAiIpqr" +
"sLjAyNDY4OjwCAAAAAAAFgD4AAA4PoCIiOYqLC4wMjQ2ODo8AgAAAAAAAAAA" +
"gICAAAAAAABAAAAAgIBPZ2dTAAQBAAAAAAAAAMViAAACAAAA22A/JwIBAQAK";
// write the bytes of the string to an ArrayBuffer
let miniOggBin = atob(miniOggRaw);
let miniOgg = new ArrayBuffer(miniOggBin.length);
let view = new Uint8Array(miniOgg);
for (var i = 0; i < miniOggBin.length; i++) {
view[i] = miniOggBin.charCodeAt(i);
}
window.SoundManager = SoundManager;
})(window, document);