waves/public/assets/g/burgerandfrights/js/framework/audiosystem.class.js
2025-04-09 17:11:14 -05:00

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();