/* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { Howl, Howler, HowlOptions } from 'howler' export enum SOUND_TYPE { SFX, MUSIC, AMBIENT, } export type AudioEngineSound = { name: string sound: Howl type: SOUND_TYPE } export type PlaySoundOptions = { fadeIn?: boolean fadeTime?: number } export type StopSoundOptions = { fadeOut?: boolean fadeTime?: number } /** * Audio Engine. Note that this should only be called from within the AudioHookStateController, * so that hookstate and the audio engine remain in sync. */ export class AudioEngine { // used just for preloading; sounds here aren't defined as part of the map and shouldn't be played // the key is the _filename_ protected preloadedSounds: Record<string, Howl> protected sounds: Record<string, AudioEngineSound> // volumes are a number from 0 to 1 // TODO: make this synced up with the hookstate version. May require us to duplicate or move it entirely. protected volume: Record<SOUND_TYPE, number> // currently playing sounds protected currentSoundBuffer: AudioEngineSound[] protected listeners: AudioEngineListenerInterface[] constructor() { this.preloadedSounds = {} this.sounds = {} this.currentSoundBuffer = [] this.volume = { [SOUND_TYPE.SFX]: 1, [SOUND_TYPE.MUSIC]: 1, [SOUND_TYPE.AMBIENT]: 1, } this.listeners = [] } addListener(listener: AudioEngineListenerInterface) { this.listeners.push(listener) } removeListener(listener: AudioEngineListenerInterface) { const index = this.listeners.indexOf(listener) if (index > -1) { this.listeners.splice(index, 1) } } public exists(name: string) { return this.sounds[name] !== undefined; } public getType(name: string) { if (!this.sounds[name]) { console.warn(`Trying to get type of sound ${name} that doesn't exist`) return } return SOUND_TYPE.SFX } public getVolume(type: SOUND_TYPE) { return this.volume[type] } public setVolume(type: SOUND_TYPE, volume: number) { this.volume[type] = volume const sounds = this.getPlayingSoundsOfType(type) for (let sound of sounds) { sound.sound.volume(volume) } for (let listener of this.listeners) { if (listener.setVolume) { listener.setVolume(type, volume) } } } /** * Helper function to stop the previous music and play a new song. * @param name * @param opts */ public changeMusic( name: string, playOpts: PlaySoundOptions = {}, stopOpts: StopSoundOptions = {} ) { if (!this.sounds[name]) { console.warn(`Trying to play music ${name} that doesn't exist`) return } const sound = this.sounds[name] if (sound.type !== SOUND_TYPE.MUSIC) { console.warn('Trying to change music into a sound clip that is not music') } this.stopAllSoundsOfType(SOUND_TYPE.MUSIC, stopOpts) this.playSound(name, playOpts) } public playSound(name: string, opts: PlaySoundOptions = {}) { if (!this.sounds[name]) { console.warn(`Trying to play sound ${name} that doesn't exist`) return } const sound = this.sounds[name].sound const type = this.sounds[name].type if (sound.loop() && sound.playing()) { sound.stop() } if (opts.fadeIn) { const fadeTime = opts.fadeTime ?? 1000 sound.fade(0, this.volume[type], fadeTime) } else { sound.volume(this.volume[type]) } sound.seek(0) sound.play() this.addSoundToBuffer(this.sounds[name]) for (let listener of this.listeners) { if (listener.playSound) { listener.playSound(this.sounds[name]) } } } public stopSound(name: string, opts: StopSoundOptions = {}) { if (!this.sounds[name]) { console.warn(`Trying to stop sound ${name} that doesn't exist`) return } const sound = this.sounds[name].sound if (opts.fadeOut) { const fadeTime = opts.fadeTime ?? 1000 sound.fade(sound.volume(), 0, fadeTime) } else { sound.stop() } this.removeSoundFromBuffer(this.sounds[name]) for (let listener of this.listeners) { if (listener.stopSound) { listener.stopSound(this.sounds[name]) } } } public stopAllSounds(opts: StopSoundOptions = {}) { const sounds = this.getPlayingSounds() for (let sound of sounds) { this.stopSound(sound.name, opts) } } public stopAllSoundsOfType(type: SOUND_TYPE, opts: StopSoundOptions = {}) { const sounds = this.getPlayingSoundsOfType(type) for (let sound of sounds) { this.stopSound(sound.name, opts) } } public preloadSound(source: string[], options: HowlOptions = {}): Howl { const howlOptions: any = options const sound = new Howl({ ...howlOptions, src: source, }) this.preloadedSounds[source[0]] = sound return sound } public addSound(source: string[], name: string, type: SOUND_TYPE) { const howlOptions: any = {} if (type === SOUND_TYPE.MUSIC || type === SOUND_TYPE.AMBIENT) { howlOptions.loop = true } let sound = this.preloadedSounds[source[0]] if (!sound) { sound = this.preloadSound(source) } if (type === SOUND_TYPE.MUSIC || type === SOUND_TYPE.AMBIENT) { sound.loop(true) } else { sound.loop(false) } const soundObj: AudioEngineSound = { name, sound, type, } this.sounds[name] = soundObj sound.on('fade', () => { if (sound.volume() === 0) { if (!this.isSoundPlayingInBuffer(this.sounds[name])) sound.stop() } }) if (type === SOUND_TYPE.SFX) { sound.on('end', () => { this.removeSoundFromBuffer(this.sounds[name]) for (let listener of this.listeners) { if (listener.stopSound) { listener.stopSound(this.sounds[name]) } } }) } return soundObj } public isPlaying(name: string): boolean { if (!this.sounds[name]) { return false } return this.sounds[name].sound.playing() } public getPlayingSounds(): AudioEngineSound[] { return this.currentSoundBuffer } public getPlayingSoundsOfType(type: SOUND_TYPE): AudioEngineSound[] { const sounds: AudioEngineSound[] = [] for (let sound of this.currentSoundBuffer) { if (sound.type === type) { sounds.push(sound) } } return sounds } protected isSoundPlayingInBuffer(sound: AudioEngineSound) { const index = this.currentSoundBuffer.indexOf(sound) if (index > -1) { return true } return false } protected addSoundToBuffer(sound: AudioEngineSound) { const index = this.currentSoundBuffer.indexOf(sound) if (index > -1) { return } this.currentSoundBuffer.push(sound) } protected removeSoundFromBuffer(sound: AudioEngineSound) { const index = this.currentSoundBuffer.indexOf(sound) if (index > -1) { this.currentSoundBuffer.splice(index, 1) } } } export interface AudioEngineListenerInterface { setVolume?: (type: SOUND_TYPE, volume: number) => void playSound?: (sound: AudioEngineSound) => void stopSound?: (sound: AudioEngineSound) => void }