|
|
|
@@ -0,0 +1,249 @@ |
|
|
|
export abstract class Source { |
|
|
|
public abstract kind: string; |
|
|
|
protected sounds: Array<AudioBuffer> = []; |
|
|
|
public gain: GainNode; |
|
|
|
public output: StereoPannerNode; |
|
|
|
|
|
|
|
constructor(public name: string) { |
|
|
|
this.gain = context.createGain(); |
|
|
|
this.output = context.createStereoPanner(); |
|
|
|
this.gain.connect(this.output); |
|
|
|
} |
|
|
|
|
|
|
|
public loadSound(name: string): void { |
|
|
|
loadAudio(name, this); |
|
|
|
} |
|
|
|
|
|
|
|
public addLoadedSound(sound: AudioBuffer): void { |
|
|
|
this.sounds.push(sound); |
|
|
|
} |
|
|
|
|
|
|
|
public abstract start(): void; |
|
|
|
|
|
|
|
public abstract tick(dt: number): void; |
|
|
|
} |
|
|
|
|
|
|
|
export class IntervalSource extends Source { |
|
|
|
kind = "Interval"; |
|
|
|
private remaining: number; |
|
|
|
private started = false; |
|
|
|
constructor(name: string, public interval: number, public randomness = 0) { |
|
|
|
super(name); |
|
|
|
|
|
|
|
this.remaining = this.interval + (Math.random() - 0.5) * 2 * this.randomness |
|
|
|
} |
|
|
|
|
|
|
|
public start(): void { |
|
|
|
this.started = true; |
|
|
|
} |
|
|
|
|
|
|
|
public tick(dt: number): void { |
|
|
|
if (this.started) { |
|
|
|
this.remaining -= dt; |
|
|
|
if (this.remaining <= 0) { |
|
|
|
const index = Math.floor(Math.random() * this.sounds.length); |
|
|
|
|
|
|
|
const node = context.createBufferSource(); |
|
|
|
|
|
|
|
node.buffer = this.sounds[index]; |
|
|
|
node.connect(this.gain); |
|
|
|
|
|
|
|
node.start(); |
|
|
|
this.output.pan.value = Math.random() * 0.4 - 0.2; |
|
|
|
this.remaining = this.interval + (Math.random() - 0.5) * 2 * this.randomness |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
export class LoopingSource extends Source { |
|
|
|
kind = "Looping"; |
|
|
|
private source!: AudioBufferSourceNode; |
|
|
|
private started = false; |
|
|
|
private running = false; |
|
|
|
constructor(name: string) { |
|
|
|
super(name); |
|
|
|
} |
|
|
|
|
|
|
|
public start(): void { |
|
|
|
this.started = true; |
|
|
|
} |
|
|
|
|
|
|
|
private pickRandom(): void { |
|
|
|
const index = Math.floor(Math.random() * this.sounds.length); |
|
|
|
this.source = context.createBufferSource(); |
|
|
|
this.source.buffer = this.sounds[index]; |
|
|
|
this.source.connect(this.gain); |
|
|
|
this.source.onended = () => { this.pickRandom(); this.source.start(); }; |
|
|
|
} |
|
|
|
public tick(dt: number) { |
|
|
|
if (this.started && this.sounds.length > 0 && !this.running) { |
|
|
|
this.pickRandom(); |
|
|
|
this.source.start(); |
|
|
|
this.running = true; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
let context: AudioContext; |
|
|
|
|
|
|
|
const audioBaseUrl = "/audio/"; |
|
|
|
|
|
|
|
const waiting: Map<string, Array<Source>> = new Map(); |
|
|
|
const audioDict: Map<string, AudioBuffer> = new Map(); |
|
|
|
|
|
|
|
// asynchronously load an audio file |
|
|
|
|
|
|
|
function loadAudio(name: string, source: Source, flush = false) { |
|
|
|
// are we already trying to get the audio? |
|
|
|
|
|
|
|
if (!waiting.has(name)) { |
|
|
|
waiting.set(name, []); |
|
|
|
} |
|
|
|
|
|
|
|
const list: Array<Source> | undefined = waiting.get(name); |
|
|
|
|
|
|
|
if (list !== undefined) list.push(source); |
|
|
|
|
|
|
|
// do we already have the audio? |
|
|
|
|
|
|
|
if (audioDict.has(name) && !flush) { |
|
|
|
const buf: AudioBuffer | undefined = audioDict.get(name); |
|
|
|
|
|
|
|
if (buf !== undefined) source.addLoadedSound(buf); |
|
|
|
} |
|
|
|
|
|
|
|
// is the audio already stored locally? |
|
|
|
|
|
|
|
if (!flush) { |
|
|
|
checkCache(name); |
|
|
|
} else { |
|
|
|
loadRemoteAudio(name); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function cacheAndParse(name: string, data: ArrayBuffer) { |
|
|
|
storeCache(name, data.slice(0)); |
|
|
|
parseAudioData(name, data); |
|
|
|
} |
|
|
|
|
|
|
|
function parseAudioData(name: string, data: ArrayBuffer) { |
|
|
|
context.decodeAudioData( |
|
|
|
data, |
|
|
|
function (buffer) { |
|
|
|
audioDict.set(name, buffer); |
|
|
|
|
|
|
|
const waitingSources: Array<Source> | undefined = waiting.get(name); |
|
|
|
|
|
|
|
if (waitingSources !== undefined) { |
|
|
|
waitingSources.forEach((source) => { |
|
|
|
source.addLoadedSound(buffer); |
|
|
|
}); |
|
|
|
} |
|
|
|
}, |
|
|
|
function (e) { |
|
|
|
console.error("Error with decoding audio data for " + name + ": " + e); |
|
|
|
} |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
function loadRemoteAudio(name: string) { |
|
|
|
const xhr = new XMLHttpRequest(); |
|
|
|
|
|
|
|
xhr.open("GET", audioBaseUrl + name, true); |
|
|
|
xhr.responseType = "arraybuffer"; |
|
|
|
xhr.onload = () => { |
|
|
|
if (xhr.status == 200) cacheAndParse(name, xhr.response); |
|
|
|
else { |
|
|
|
console.error("Couldn't load " + name); |
|
|
|
} |
|
|
|
}; |
|
|
|
xhr.onerror = () => { |
|
|
|
console.error("Couldn't load " + name); |
|
|
|
}; |
|
|
|
|
|
|
|
xhr.send(); |
|
|
|
} |
|
|
|
|
|
|
|
// check if the content is cached |
|
|
|
function checkCache(name: string) { |
|
|
|
const req = window.indexedDB.open("cache", 1); |
|
|
|
req.onsuccess = () => { |
|
|
|
const db = req.result; |
|
|
|
const tx = db.transaction("audio", "readonly"); |
|
|
|
|
|
|
|
const audio = tx.objectStore("audio"); |
|
|
|
|
|
|
|
const read: IDBRequest = audio.get([name]); |
|
|
|
|
|
|
|
read.onsuccess = () => { |
|
|
|
const res = read.result; |
|
|
|
if (res === undefined) { |
|
|
|
loadRemoteAudio(name); |
|
|
|
} else { |
|
|
|
parseAudioData(name, res.content); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
tx.oncomplete = () => { |
|
|
|
db.close(); |
|
|
|
}; |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
// caching stuff here |
|
|
|
|
|
|
|
function storeCache(name: string, blob: ArrayBuffer) { |
|
|
|
const req = window.indexedDB.open("cache", 1); |
|
|
|
req.onsuccess = () => { |
|
|
|
const db = req.result; |
|
|
|
const tx = db.transaction("audio", "readwrite"); |
|
|
|
|
|
|
|
const audio = tx.objectStore("audio"); |
|
|
|
|
|
|
|
audio.put({ |
|
|
|
name: name, |
|
|
|
content: blob, |
|
|
|
}); |
|
|
|
|
|
|
|
tx.oncomplete = () => { |
|
|
|
db.close(); |
|
|
|
}; |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
export function setup(): AudioContext { |
|
|
|
context = new AudioContext(); |
|
|
|
createCache(); |
|
|
|
return context; |
|
|
|
} |
|
|
|
|
|
|
|
export function clearCache(): void { |
|
|
|
const idb = window.indexedDB; |
|
|
|
|
|
|
|
idb.deleteDatabase("cache"); |
|
|
|
|
|
|
|
createCache(); |
|
|
|
} |
|
|
|
|
|
|
|
// if the indexedDB table doesn't exist at all, make it |
|
|
|
function createCache(): void { |
|
|
|
const idb = window.indexedDB; |
|
|
|
|
|
|
|
const req = idb.open("cache", 1); |
|
|
|
|
|
|
|
req.onupgradeneeded = (event) => { |
|
|
|
const db = req.result; |
|
|
|
|
|
|
|
if (event.oldVersion > 0 && event.oldVersion < 3) { |
|
|
|
db.deleteObjectStore("audio"); |
|
|
|
} |
|
|
|
|
|
|
|
db.createObjectStore("audio", { keyPath: ["name"] }); |
|
|
|
}; |
|
|
|
|
|
|
|
req.onerror = () => { |
|
|
|
alert("Couldn't open the database?"); |
|
|
|
}; |
|
|
|
} |