game_actor_sound_sound_emitter.js

import Actor from "../actor.js";
import Vec2 from "../../vector/vec2.js";
import Util from "../../../util/misc/util.js";

/**
 * Sound emitter that can emit sounds in 3D space.
 * @class
 * @extends Actor
 */
class SoundEmitter extends Actor {
    /**
     * Create a new SoundEmitter.
     * @param {Vec2} pos The position of the SoundEmitter, defaults to (0, 0)
     * @param {string|null} sound The identifier of the sound to emit, defaults to null
     * @param {number} volume The volume of the sound, defaults to 1
     * @param {number} speed The playback speed of the sound, defaults to 1
     * @param {boolean} repeat Whether to repeat sound upon completion
     * @param {string|Sprite|null} sprite Sprite for visual rendering (optional)
     * @param {number} scale Scale of object (only relevant with sprites)
     * @param {boolean} temp Whether emitter should be deleted once playback has finished, defaults to true
     * @param {boolean} play Whether to play upon initialization, defaults to true
     * @param {Object.<string, ActorComponent>} components Components to add
     * @constructor
     */
    constructor({
        pos = new Vec2(),
        sound = null,
        volume = 1,
        speed = 1,
        repeat = false,
        sprite = null,
        scale = 1,
        temp = true,
        play = true,
        components = {}
    } = {}) {
        super({
            pos: pos,
            sprite: sprite,
            scale: scale,
            components: components
        });

        this._soundID = sound;
        this._resources = null;

        this._volume = volume;
        this._speed = speed;
        this._repeat = repeat;

        this._temp = temp;
        this._play = play;

        this.debugPrefix = "[Sound] ID: ";
    }

    init() {
        super.init();

        if (this._play)
            this.play();
    }

    delete() {
        this._release();

        super.delete();
    }


    //
    // Getters
    //

    /**
     * Get the identifier of the sound.
     * @returns {string|null} Global identifier of sound
     */
    get sound() {
        return this._soundID;
    }


    //
    // Updating
    //

    /**
     * Update the position of the sound emitter relative to the space's camera.
     * @param {number} elapsed Time since last update cycle
     */
    update(elapsed) {
        super.update(elapsed);

        if (this._resources === null || this.space === null)
            return;

        if (!this._repeat && this._resources.context.currentTime * this._speed >= this._resources.sound.duration) {
            this.delete();
            return;
        }

        const cam = this.space.camera;
        if (cam === null)
            return;

        this._resources.sound.playbackRate = this._speed * $$.flags.timeScale;

        const pos = cam.getOffset().sub(this.globalPos).addv($$.width / 2, $$.height / 2);
        const div = 32;

        this._resources.panner.positionX.setValueAtTime(-pos.x / div, this._resources.context.currentTime);
        this._resources.panner.positionY.setValueAtTime(-pos.y / div, this._resources.context.currentTime);

        let secs = Math.floor(this._resources.context.currentTime * this._speed);
        this.debugPrefix = `[Sound (${Util.timeFormat(secs)})] ID: `;
    }


    // Initialization

    /**
     * Setup 3D audio upon first play() call.
     * @private
     */
    _setup() {
        this._resources = $$.reg.sound.getInstance(this._soundID);

        this._resources.panner.positionX.setValueAtTime(0, this._resources.context.currentTime);
        this._resources.panner.positionY.setValueAtTime(0, this._resources.context.currentTime);
        this._resources.panner.positionZ.setValueAtTime(0, this._resources.context.currentTime);
    }

    _release() {
        if (this._resources !== null) {
            $$.reg.sound.releaseInstance(this._soundID, this._resources);
            this._resources = null;
        }
    }


    // Controls

    /**
     * Play the audio from the current time, or the start.
     * @param {boolean} fromStart Whether to play audio from beginning
     */
    play(fromStart = false) {
        if (this._resources === null)
            this._setup();

        this._resources.sound.currentTime = 0;
        this._resources.sound.volume = this._volume;

        this._resources.sound.play();
    }

    /**
     * Pause the audio at the current time.
     */
    pause() {
        if (this._resources !== null) {
            this._resources.sound.pause();
        }
    }

    /**
     * Stop the audio and reset back to the beginning.
     */
    stop() {
        if (this._resources !== null) {
            this._resources.sound.pause();
            this._resources.sound.currentTime = 0;

            this._release();
        }
    }
}

export default SoundEmitter;