forked from sent/waves
231 lines
7.5 KiB
JavaScript
231 lines
7.5 KiB
JavaScript
/**
|
|
* @file audiosystem.class.js
|
|
* @version 1.0.0
|
|
*/
|
|
|
|
import config from '../resources/config.js';
|
|
import resources from '../resources/resources.js';
|
|
|
|
export default class AudioSystem {
|
|
static _init() {
|
|
AudioSystem._ac = null;
|
|
AudioSystem._acResuming = null;
|
|
|
|
AudioSystem._gain = null;
|
|
AudioSystem._gainConnected = null;
|
|
|
|
AudioSystem._destinationNode = null;
|
|
|
|
AudioSystem._volume = config.audio.volume;
|
|
|
|
AudioSystem._sounds = new Map();
|
|
AudioSystem._soundInstances = [];
|
|
|
|
window.addEventListener('load', AudioSystem._handleLoad);
|
|
}
|
|
|
|
static _handleLoad() {
|
|
const userGesture = () => {
|
|
const ac = AudioSystem._ac;
|
|
if (ac === null) {
|
|
return;
|
|
}
|
|
|
|
if (ac.state !== 'running' && !AudioSystem._acResuming) {
|
|
AudioSystem._acResuming = true;
|
|
|
|
ac.resume().finally(() => {
|
|
AudioSystem._acResuming = false;
|
|
});
|
|
}
|
|
};
|
|
|
|
document.addEventListener('click', userGesture);
|
|
document.addEventListener('touchend', userGesture);
|
|
document.addEventListener('keydown', userGesture);
|
|
}
|
|
|
|
static _getAudioContext() {
|
|
const ac = AudioSystem._ac;
|
|
if (ac === null) {
|
|
throw new Error('Call .createAudioContext first');
|
|
}
|
|
return ac;
|
|
}
|
|
|
|
static createAudioContext(audioContext = null, destinationNode = null) {
|
|
const ac = audioContext === null ? new AudioContext() : audioContext;
|
|
AudioSystem._ac = ac;
|
|
AudioSystem._acResuming = false;
|
|
|
|
AudioSystem._destinationNode = destinationNode === null ? ac.destination : destinationNode;
|
|
|
|
AudioSystem._gain = ac.createGain();
|
|
AudioSystem._gainConnected = false;
|
|
|
|
AudioSystem.setGlobalVolume(AudioSystem._volume);
|
|
}
|
|
|
|
static setGlobalVolume(volume) {
|
|
AudioSystem._volume = volume;
|
|
|
|
AudioSystem._getAudioContext();
|
|
|
|
AudioSystem._gain.gain.value = volume;
|
|
|
|
if (volume <= 0 && AudioSystem._gainConnected) {
|
|
AudioSystem._gain.disconnect();
|
|
AudioSystem._gainConnected = false;
|
|
} else if (volume > 0 && !AudioSystem._gainConnected) {
|
|
AudioSystem._gain.connect(AudioSystem._destinationNode);
|
|
AudioSystem._gainConnected = true;
|
|
}
|
|
}
|
|
|
|
static asyncLoadSounds() {
|
|
const ac = AudioSystem._getAudioContext();
|
|
|
|
return Promise.all(Object.keys(resources.sounds).map(name => {
|
|
const info = resources.sounds[name];
|
|
const url = `./sounds/${info.path}${config.debug.preventCaching ? '?time=' + new Date().getTime() : ''}`;
|
|
|
|
return fetch(url, { cache: 'no-cache' })
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(response.statusText);
|
|
}
|
|
|
|
return response.arrayBuffer();
|
|
})
|
|
.then(buffer => ac.decodeAudioData(buffer))
|
|
.then(buffer => {
|
|
AudioSystem._sounds.set(name, {
|
|
buffer,
|
|
maxSources: info.maxSources,
|
|
baseVolume: info.baseVolume,
|
|
instances: [],
|
|
});
|
|
})
|
|
.catch(error => {
|
|
throw new Error(`Error loading sound file "${url}", ${error}`);
|
|
});
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Create, play and return a new sound instance.
|
|
* @param {string} soundName - The sound name.
|
|
* @param {number} [volume] - The sound volume (gain). Scaled by the sound base volume. Defaults to 1.
|
|
* @param {bool} [loop] - Whether the sound should loop. Defaults to false.
|
|
* @param {object} [tag] - A tag identifying the new sound instance. Disabled by default.
|
|
* @param {number} [offset] - The sound start offset in seconds. Defaults to 0.
|
|
* @param {number} [fadeInSeconds] - Seconds to fade in the sound.
|
|
* @param {bool} [stopOldestIfMax] - Whether to stop the oldest sound instance if max instances are playing, else ignore play.
|
|
* @returns {object} - The sound instance.
|
|
*/
|
|
static play(soundName, volume = 1, loop = false, tag = null, offset = 0, fadeInSeconds = 0, stopOldestIfMax = true) {
|
|
const ac = AudioSystem._getAudioContext();
|
|
|
|
const sound = AudioSystem._sounds.get(soundName);
|
|
if (sound === undefined) {
|
|
throw new Error(`Sound "${soundName}" does not exist`);
|
|
}
|
|
|
|
if (sound.maxSources <= 0) {
|
|
return null;
|
|
}
|
|
|
|
if (sound.instances.length >= sound.maxSources) {
|
|
if (stopOldestIfMax) {
|
|
sound.instances[0].source.stop();
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const gain = ac.createGain();
|
|
if (fadeInSeconds <= 0) {
|
|
gain.gain.value = sound.baseVolume * volume;
|
|
} else {
|
|
gain.gain.setValueAtTime(1e-4, ac.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(sound.baseVolume * volume, ac.currentTime + fadeInSeconds);
|
|
}
|
|
gain.connect(AudioSystem._gain);
|
|
|
|
const source = ac.createBufferSource();
|
|
source.buffer = sound.buffer;
|
|
if (loop) {
|
|
source.loop = true;
|
|
if (offset > 0) {
|
|
source.loopStart = offset;
|
|
source.loopEnd = sound.buffer.duration;
|
|
}
|
|
}
|
|
source.connect(gain);
|
|
|
|
const instance = { source, gain, tag, stopping: false };
|
|
|
|
sound.instances.push(instance);
|
|
AudioSystem._soundInstances.push(instance);
|
|
|
|
source.onended = () => {
|
|
gain.disconnect();
|
|
|
|
const si = sound.instances.indexOf(instance);
|
|
if (si > -1) {
|
|
sound.instances.splice(si, 1);
|
|
}
|
|
|
|
const i = AudioSystem._soundInstances.indexOf(instance);
|
|
if (i > -1) {
|
|
AudioSystem._soundInstances.splice(i, 1);
|
|
}
|
|
};
|
|
|
|
source.start(ac.currentTime, offset);
|
|
|
|
return instance;
|
|
}
|
|
|
|
/**
|
|
* Stop all sound instances or sound instances identified by a tag.
|
|
* @param {object} [tag] - A tag identifying the sound instances to stop if not null.
|
|
* @param {number} [fadeOutSeconds] - The number of seconds over which to fade out the sound instances.
|
|
*/
|
|
static stop(tag = null, fadeOutSeconds = 0) {
|
|
const ac = AudioSystem._getAudioContext();
|
|
|
|
for (let i = AudioSystem._soundInstances.length - 1; i > -1; i--) {
|
|
const instance = AudioSystem._soundInstances[i];
|
|
|
|
if (instance.stopping || tag !== null && instance.tag !== tag) {
|
|
continue;
|
|
}
|
|
|
|
instance.stopping = true;
|
|
|
|
if (fadeOutSeconds <= 0) {
|
|
instance.source.stop();
|
|
} else {
|
|
instance.gain.gain.setValueAtTime(instance.gain.gain.value, ac.currentTime);
|
|
instance.gain.gain.exponentialRampToValueAtTime(1e-4, ac.currentTime + fadeOutSeconds);
|
|
instance.source.stop(ac.currentTime + fadeOutSeconds);
|
|
}
|
|
}
|
|
}
|
|
|
|
static getSoundNames() {
|
|
return Array.from(AudioSystem._sounds.keys());
|
|
}
|
|
|
|
static getSoundBuffer(soundName) {
|
|
const sound = AudioSystem._sounds.get(soundName);
|
|
if (sound === undefined) {
|
|
throw new Error(`Sound "${soundName}" does not exist`);
|
|
}
|
|
return sound.buffer;
|
|
}
|
|
}
|
|
|
|
AudioSystem._init();
|