Source: AudioManager.js

AudioManager.js

//-----------------------------------------------------------------------------
// AudioManager
//
// The static class that handles BGM, BGS, ME and SE.
/**
 * The static class that handles BGM, BGS, ME and SE.
 *
 * @namespace
 */
function AudioManager() {
    throw new Error("This is a static class");
}

AudioManager._bgmVolume = 100;
AudioManager._bgsVolume = 100;
AudioManager._meVolume = 100;
AudioManager._seVolume = 100;
AudioManager._currentBgm = null;
AudioManager._currentBgs = null;
AudioManager._bgmBuffer = null;
AudioManager._bgsBuffer = null;
AudioManager._meBuffer = null;
AudioManager._seBuffers = [];
AudioManager._staticBuffers = [];
AudioManager._replayFadeTime = 0.5;
AudioManager._path = "audio/";

/**
 * The volume of the BGM audio
 *
 * @type number
 * @static
 * @name AudioManager.bgmVolume
 */
Object.defineProperty(AudioManager, "bgmVolume", {
    get: function() {
        return this._bgmVolume;
    },
    set: function(value) {
        this._bgmVolume = value;
        this.updateBgmParameters(this._currentBgm);
    },
    configurable: true
});

/**
 * The volume of the BGS audio
 *
 * @type number
 * @static
 * @name AudioManager.bgsVolume
 */
Object.defineProperty(AudioManager, "bgsVolume", {
    get: function() {
        return this._bgsVolume;
    },
    set: function(value) {
        this._bgsVolume = value;
        this.updateBgsParameters(this._currentBgs);
    },
    configurable: true
});

/**
 * The volume of the ME audio
 *
 * @type number
 * @static
 * @name AudioManager.meVolume
 */
Object.defineProperty(AudioManager, "meVolume", {
    get: function() {
        return this._meVolume;
    },
    set: function(value) {
        this._meVolume = value;
        this.updateMeParameters(this._currentMe);
    },
    configurable: true
});

/**
 * The volume of the SE audio
 *
 * @type number
 * @static
 * @name AudioManager.seVolume
 */
Object.defineProperty(AudioManager, "seVolume", {
    get: function() {
        return this._seVolume;
    },
    set: function(value) {
        this._seVolume = value;
    },
    configurable: true
});

/**
 * Plays the BGM beginning at a position in the file
 *
 * @static
 * @param {{name: string, volume: number, pan: number, pitch: number}} bgm - the bgm object to play
 * @param {number} pos - position in the audio file to start at
 */
AudioManager.playBgm = function(bgm, pos) {
    if (this.isCurrentBgm(bgm)) {
        this.updateBgmParameters(bgm);
    } else {
        this.stopBgm();
        if (bgm.name) {
            this._bgmBuffer = this.createBuffer("bgm/", bgm.name);
            this.updateBgmParameters(bgm);
            if (!this._meBuffer) {
                this._bgmBuffer.play(true, pos || 0);
            }
        }
    }
    this.updateCurrentBgm(bgm, pos);
};

/**
 * Replays the given bgm from where it's last position was
 *
 * @static
 * @param {{name: string, volume: number, pan: number, pitch: number, pos: number}} bgm - the bgm object to replay
 */
AudioManager.replayBgm = function(bgm) {
    if (this.isCurrentBgm(bgm)) {
        this.updateBgmParameters(bgm);
    } else {
        this.playBgm(bgm, bgm.pos);
        if (this._bgmBuffer) {
            this._bgmBuffer.fadeIn(this._replayFadeTime);
        }
    }
};

/**
 * Checks if the given bgm is currently playing
 *
 * @static
 * @param {{name: string, volume: number, pan: number, pitch: number, pos: number}} bgm - compares the bgm name to currently playing bgm name
 */
AudioManager.isCurrentBgm = function(bgm) {
    return (
        this._currentBgm &&
        this._bgmBuffer &&
        this._currentBgm.name === bgm.name
    );
};

/**
 * Updates BGM parameters: volume, pitch, and pan
 *
 * @static
 * @param {{name: string, volume: number, pan: number, pitch: number, pos: number}} bgm - bgm object to update parameters from
 */
AudioManager.updateBgmParameters = function(bgm) {
    this.updateBufferParameters(this._bgmBuffer, this._bgmVolume, bgm);
};

/**
 * Updates the currently playing BGM's volume, pitch, pan, pos, and name
 *
 * @static
 * @param {{name: string, volume: number, pan: number, pitch: number}} bgm - bgm object to set as the current bgm
 * @param {number} pos - position in the audio file to begin playing from
 */
AudioManager.updateCurrentBgm = function(bgm, pos) {
    this._currentBgm = {
        name: bgm.name,
        volume: bgm.volume,
        pitch: bgm.pitch,
        pan: bgm.pan,
        pos: pos
    };
};

/**
 * Stops playing the current BGM
 * @static
 */
AudioManager.stopBgm = function() {
    if (this._bgmBuffer) {
        this._bgmBuffer.destroy();
        this._bgmBuffer = null;
        this._currentBgm = null;
    }
};

/**
 * Fades out the current BGM
 *
 * @static
 * @param {number} duration - How long the fade out effect lasts (in seconds)
 */
AudioManager.fadeOutBgm = function(duration) {
    if (this._bgmBuffer && this._currentBgm) {
        this._bgmBuffer.fadeOut(duration);
        this._currentBgm = null;
    }
};

/**
 * Fades in the current BGM
 *
 * @static
 * @param {number} duration - How long the fade in effect lasts (in seconds)
 */
AudioManager.fadeInBgm = function(duration) {
    if (this._bgmBuffer && this._currentBgm) {
        this._bgmBuffer.fadeIn(duration);
    }
};

/**
 * Plays the BGS beginning at a position in the file
 *
 * @static
 * @param {{name: string, volume: number, pan: number, pitch: number}} bgs - the bgs object to play
 * @param {number} pos - position in the audio file to start at
 */
AudioManager.playBgs = function(bgs, pos) {
    if (this.isCurrentBgs(bgs)) {
        this.updateBgsParameters(bgs);
    } else {
        this.stopBgs();
        if (bgs.name) {
            this._bgsBuffer = this.createBuffer("bgs/", bgs.name);
            this.updateBgsParameters(bgs);
            this._bgsBuffer.play(true, pos || 0);
        }
    }
    this.updateCurrentBgs(bgs, pos);
};

/**
 * Replays the given bgs from where it's last position was
 *
 * @static
 * @param {{name: string, volume: number, pan: number, pitch: number, pos: number}} bgs - the bgs object to replay
 */
AudioManager.replayBgs = function(bgs) {
    if (this.isCurrentBgs(bgs)) {
        this.updateBgsParameters(bgs);
    } else {
        this.playBgs(bgs, bgs.pos);
        if (this._bgsBuffer) {
            this._bgsBuffer.fadeIn(this._replayFadeTime);
        }
    }
};

/**
 * Checks if the given bgs is currently playing
 *
 * @static
 * @param {{name: string, volume: number, pan: number, pitch: number, pos: number}} bgs - compares the bgs name to currently playing bgs name
 */
AudioManager.isCurrentBgs = function(bgs) {
    return (
        this._currentBgs &&
        this._bgsBuffer &&
        this._currentBgs.name === bgs.name
    );
};

/**
 * Updates BGS parameters: volume, pitch, and pan
 *
 * @static
 * @param {{name: string, volume: number, pan: number, pitch: number, pos: number}} bgs - bgs object to update parameters from
 */
AudioManager.updateBgsParameters = function(bgs) {
    this.updateBufferParameters(this._bgsBuffer, this._bgsVolume, bgs);
};

/**
 * Updates the currently playing BGS's volume, pitch, pan, pos, and name
 *
 * @static
 * @param {{name: string, volume: number, pan: number, pitch: number}} bgs - bgs object to set as the current bgs
 * @param {number} pos - position in the audio file to begin playing from
 */
AudioManager.updateCurrentBgs = function(bgs, pos) {
    this._currentBgs = {
        name: bgs.name,
        volume: bgs.volume,
        pitch: bgs.pitch,
        pan: bgs.pan,
        pos: pos
    };
};

/**
 * Stops playing the current BGS
 * @static
 */
AudioManager.stopBgs = function() {
    if (this._bgsBuffer) {
        this._bgsBuffer.destroy();
        this._bgsBuffer = null;
        this._currentBgs = null;
    }
};

/**
 * Fades out the current BGS
 *
 * @static
 * @param {number} duration - How long the fade out effect lasts (in seconds)
 */
AudioManager.fadeOutBgs = function(duration) {
    if (this._bgsBuffer && this._currentBgs) {
        this._bgsBuffer.fadeOut(duration);
        this._currentBgs = null;
    }
};

/**
 * Fades in the current BGS
 *
 * @static
 * @param {number} duration - How long the fade in effect lasts (in seconds)
 */
AudioManager.fadeInBgs = function(duration) {
    if (this._bgsBuffer && this._currentBgs) {
        this._bgsBuffer.fadeIn(duration);
    }
};

/**
 * Plays the given ME.
 *
 * @static
 * @param {{name: string, volume: number, pan: number, pitch: number}} me - the me object to play
 */
AudioManager.playMe = function(me) {
    this.stopMe();
    if (me.name) {
        if (this._bgmBuffer && this._currentBgm) {
            this._currentBgm.pos = this._bgmBuffer.seek();
            this._bgmBuffer.stop();
        }
        this._meBuffer = this.createBuffer("me/", me.name);
        this.updateMeParameters(me);
        this._meBuffer.play(false);
        this._meBuffer.addStopListener(this.stopMe.bind(this));
    }
};

/**
 * Updates ME parameters: volume, pitch, and pan
 *
 * @static
 * @param {{name: string, volume: number, pan: number, pitch: number}} me - me object to update parameters from
 */
AudioManager.updateMeParameters = function(me) {
    this.updateBufferParameters(this._meBuffer, this._meVolume, me);
};

/**
 * Fades out the current ME
 *
 * @static
 * @param {number} duration - How long the fade out effect lasts (in seconds)
 */
AudioManager.fadeOutMe = function(duration) {
    if (this._meBuffer) {
        this._meBuffer.fadeOut(duration);
    }
};

/**
 * Stops playing the current ME
 * @static
 */
AudioManager.stopMe = function() {
    if (this._meBuffer) {
        this._meBuffer.destroy();
        this._meBuffer = null;
        if (
            this._bgmBuffer &&
            this._currentBgm &&
            !this._bgmBuffer.isPlaying()
        ) {
            this._bgmBuffer.play(true, this._currentBgm.pos);
            this._bgmBuffer.fadeIn(this._replayFadeTime);
        }
    }
};

/**
 * Plays the given SE.
 *
 * @static
 * @param {{name: string, volume: number, pan: number, pitch: number}} se - the se object to play
 */
AudioManager.playSe = function(se) {
    if (se.name) {
        // [Note] Do not play the same sound in the same frame.
        const latestBuffers = this._seBuffers.filter(
            buffer => buffer.frameCount === Graphics.frameCount
        );
        if (latestBuffers.find(buffer => buffer.name === se.name)) {
            return;
        }
        const buffer = this.createBuffer("se/", se.name);
        this.updateSeParameters(buffer, se);
        buffer.play(false);
        this._seBuffers.push(buffer);
        this.cleanupSe();
    }
};

/**
 * Updates SE parameters: volume, pitch, and pan
 *
 * @static
 * @param {WebAudio} buffer - the se buffer to update
 * @param {{name: string, volume: number, pan: number, pitch: number}} se - se object to update parameters from
 */
AudioManager.updateSeParameters = function(buffer, se) {
    this.updateBufferParameters(buffer, this._seVolume, se);
};

/**
 * Removes any SE buffers that are not currently playing
 * @static
 */
AudioManager.cleanupSe = function() {
    for (const buffer of this._seBuffers) {
        if (!buffer.isPlaying()) {
            buffer.destroy();
        }
    }
    this._seBuffers = this._seBuffers.filter(buffer => buffer.isPlaying());
};

/**
 * Stops all SEs that are currently playing
 * @static
 */
AudioManager.stopSe = function() {
    for (const buffer of this._seBuffers) {
        buffer.destroy();
    }
    this._seBuffers = [];
};

/**
 * Plays the given static SE. Static SEs do not stop when stopSe is called.
 *
 * @static
 * @param {{name: string, volume: number, pan: number, pitch: number}} se - the se object to play
 */
AudioManager.playStaticSe = function(se) {
    if (se.name) {
        this.loadStaticSe(se);
        for (const buffer of this._staticBuffers) {
            if (buffer.name === se.name) {
                buffer.stop();
                this.updateSeParameters(buffer, se);
                buffer.play(false);
                break;
            }
        }
    }
};

/**
 * Creates a buffer and pushes it to the static se buffer stack
 *
 * @static
 * @param {{name: string, volume: number, pan: number, pitch: number}} se - the se object to load
 */
AudioManager.loadStaticSe = function(se) {
    if (se.name && !this.isStaticSe(se)) {
        const buffer = this.createBuffer("se/", se.name);
        this._staticBuffers.push(buffer);
    }
};

/**
 * Checks if the given SE is a static SE by name
 *
 * @static
 * @param {{name: string, volume: number, pan: number, pitch: number}} se - the se object to compare
 */
AudioManager.isStaticSe = function(se) {
    for (const buffer of this._staticBuffers) {
        if (buffer.name === se.name) {
            return true;
        }
    }
    return false;
};

/**
 * Stops ME, SE, BGM, and BGS
 * @static
 */
AudioManager.stopAll = function() {
    this.stopMe();
    this.stopBgm();
    this.stopBgs();
    this.stopSe();
};

/**
 * Saves the currently playing BGM
 *
 * @static
 * @return {{name: string, volume: number, pan: number, pitch: number, pos: number}} The currently playing BGM, or an empty audio object is no current BGM exists
 */
AudioManager.saveBgm = function() {
    if (this._currentBgm) {
        const bgm = this._currentBgm;
        return {
            name: bgm.name,
            volume: bgm.volume,
            pitch: bgm.pitch,
            pan: bgm.pan,
            pos: this._bgmBuffer ? this._bgmBuffer.seek() : 0
        };
    } else {
        return this.makeEmptyAudioObject();
    }
};

/**
 * Saves the currently playing BGS
 *
 * @static
 * @return {{name: string, volume: number, pan: number, pitch: number, pos: number}} The currently playing BGS, or an empty audio object is no current BGS exists
 */
AudioManager.saveBgs = function() {
    if (this._currentBgs) {
        const bgs = this._currentBgs;
        return {
            name: bgs.name,
            volume: bgs.volume,
            pitch: bgs.pitch,
            pan: bgs.pan,
            pos: this._bgsBuffer ? this._bgsBuffer.seek() : 0
        };
    } else {
        return this.makeEmptyAudioObject();
    }
};

/**
 * Creates an empty audio object
 *
 * @static
 * @return {{name: string, volume: number, pitch: number}} An empty audio object with an empty string name, 0 volume and 0 pitch
 */
AudioManager.makeEmptyAudioObject = function() {
    return { name: "", volume: 0, pitch: 0 };
};

/**
 * Creates a WebAudio buffer from the file found at the folder and filename location given
 *
 * @static
 * @param {string} folder - The folder to find the audio file
 * @param {string} name - The name of the audio file (not including file extension)
 * @return {WebAudio} A WebAudio buffer object
 */
AudioManager.createBuffer = function(folder, name) {
    const ext = this.audioFileExt();
    const url = this._path + folder + Utils.encodeURI(name) + ext;
    const buffer = new WebAudio(url);
    buffer.name = name;
    buffer.frameCount = Graphics.frameCount;
    return buffer;
};

/**
 * Updates a buffer's parameters for volume, pitch, and pan from a given audio object
 *
 * @static
 * @param {WebAudio} buffer - The WebAudio buffer to update
 * @param {number} configVolume - The volume setting from the player's audio settings
 * @param {{volume: number, pitch: number, pan: number} audio - The audio object to update volume, pitch, and pan from
 */
AudioManager.updateBufferParameters = function(buffer, configVolume, audio) {
    if (buffer && audio) {
        buffer.volume = (configVolume * (audio.volume || 0)) / 10000;
        buffer.pitch = (audio.pitch || 0) / 100;
        buffer.pan = (audio.pan || 0) / 100;
    }
};

/**
 * Returns the file extension to use for audio files
 *
 * @static
 * @return {string} The file extension to use for audio files
 */
AudioManager.audioFileExt = function() {
    return ".ogg";
};

/**
 * Checks if any buffers could not load their audio file
 *
 * @static
 */
AudioManager.checkErrors = function() {
    const buffers = [this._bgmBuffer, this._bgsBuffer, this._meBuffer];
    buffers.push(...this._seBuffers);
    buffers.push(...this._staticBuffers);
    for (const buffer of buffers) {
        if (buffer && buffer.isError()) {
            this.throwLoadError(buffer);
        }
    }
};

/**
 * Throws an error screen with retry button if audio file could not be loaded.
 *
 * @static
 * @param {WebAudio} webAudio - The buffer that could not be loaded.
 * @throws Will throw a retry screen error
 */
AudioManager.throwLoadError = function(webAudio) {
    const retry = webAudio.retry.bind(webAudio);
    throw ["LoadError", webAudio.url, retry];
};