mirror of https://github.com/kurisufriend/0x40-web
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.
632 lines
24 KiB
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); |