base/asset/SoundAsset.js

/*
 * Copyright 2020 WICKLETS LLC
 *
 * This file is part of Wick Engine.
 *
 * Wick Engine is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Wick Engine is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Wick Engine.  If not, see <https://www.gnu.org/licenses/>.
 */

Wick.SoundAsset = class extends Wick.FileAsset {
    /**
     * Returns valid MIME types for a Sound Asset.
     * @returns {string[]} Array of strings representing MIME types in the form audio/Subtype.
     */
    static getValidMIMETypes() {
        let mp3Types = ['audio/mp3', 'audio/mpeg3', 'audio/x-mpeg-3', 'audio/mpeg', 'video/mpeg', 'video/x-mpeg']
        let oggTypes = ['audio/ogg', 'video/ogg', 'application/ogg']
        let wavTypes = ['audio/wave', 'audio/wav', 'audio/x-wav', 'audio/x-pn-wav']
        return mp3Types.concat(oggTypes).concat(wavTypes);
    }

    /**
     * Returns valid extensions for a sound asset.
     * @returns {string[]} Array of strings representing valid
     */
    static getValidExtensions() {
        return ['.mp3', '.ogg', '.wav'];
    }

    /**
     * Creates a new SoundAsset.
     * @param {object} args - Asset constructor args. see constructor for Wick.Asset
     */
    constructor(args) {
        super(args);

        this._waveform = null;
    }

    _serialize(args) {
        var data = super._serialize(args);
        return data;
    }

    _deserialize(data) {
        super._deserialize(data);
    }

    get classname() {
        return 'SoundAsset';
    }

    /**
     * Plays this asset's sound.
     * @param {number} seekMS - the amount of time in milliseconds into the sound the sound should start at.
     * @param {number} volume - the volume of the sound, from 0.0 - 1.0
     * @param {boolean} loop - if set to true, the sound will loop
     * @return {number} The id of the sound instance that was played.
     */
    play(options) {
        if (!options) options = {};
        if (options.seekMS === undefined) options.seekMS = 0;
        if (options.volume === undefined) options.volume = 1.0;
        if (options.loop === undefined) options.loop = false;

        // don't do anything if the project is muted...
        if (this.project.muted) {
            return;
        }

        var id = this._howl.play();

        this._howl.seek(options.seekMS / 1000, id);
        this._howl.volume(options.volume, id);
        this._howl.loop(options.loop, id);

        return id;
    }

    /**
     * Stops this asset's sound.
     * @param {number} id - (optional) the ID of the instance to stop. If ID is not given, every instance of this sound will stop.
     */
    stop(id) {
        // Howl instance was never created, sound has never played yet, so do nothing
        if (!this._howl) {
            return;
        }

        if (id === undefined) {
            this._howl.stop();
        } else {
            this._howl.stop(id);
        }
    }

    /**
     * The length of the sound in seconds
     * @type {number}
     */
    get duration() {
        return this._howl.duration();
    }

    /**
     * A list of frames that use this sound.
     * @returns {Wick.Frame[]}
     */
    getInstances() {
        var frames = [];
        this.project.getAllFrames().forEach(frame => {
            if (frame._soundAssetUUID === this.uuid) {
                frames.push(frame);
            }
        });
        return frames;
    }

    /**
     * Check if there are any objects in the project that use this asset.
     * @returns {boolean}
     */
    hasInstances() {
        return this.getInstances().length > 0;
    }

    /**
     * Remove the sound from any frames in the project that use this asset as their sound.
     */
    removeAllInstances() {
        this.getInstances().forEach(frame => {
            frame.removeSound();
        });
    }

    /**
     * Loads data about the sound into the asset.
     * @param {function} callback - function to call when the data is done being loaded.
     */
    load(callback) {
        this._generateWaveform(() => {
            this._waitForHowlLoad(() => {
                callback();
            });
        });
    }

    /**
     * Image of the waveform of this sound.
     * @type {Image}
     */
    get waveform() {
        return this._waveform;
    }

    get _howl() {
        // Lazily create howler instance
        if (!this._howlInstance) {
            // This fixes OGGs in firefox, as video/ogg is sometimes set as the MIMEType, which Howler doesn't like.
            var srcFixed = this.src;
            srcFixed = this.src.replace('video/ogg', 'audio/ogg');

            this._howlInstance = new Howl({
                src: [srcFixed]
            });
        }

        return this._howlInstance;
    }

    _waitForHowlLoad(callback) {
        if (this._howl.state() === 'loaded') {
            callback();
        } else {
            this._howl.on('load', () => {
                callback();
            });
        }
    }

    _generateWaveform(callback) {
        if (this._waveform) {
            callback();
            return;
        }

        var soundSrc = this.src;

        if (!soundSrc) console.log("error", this, soundSrc);

        var scwf = new SCWF();
        scwf.generate(soundSrc, {
            onComplete: (png, pixels) => {
                this._waveform = new Image();
                this._waveform.onload = () => {
                    callback();
                }
                this._waveform.src = png;
            }
        });
    }
}