// Copyright 2019-2024, University of Colorado Boulder
/**
 * ContinuousPropertySoundClip is a sound generator that alters the playback rate of a sound clip based on the
 * value of a continuous numerical Property.  It is specifically designed to work with sound clips and does not support
 * other types of sound production, such as oscillators.  It is implemented such that the sound fades in when changes
 * occur in the Property's value and fades out when the value doesn't change for some (configurable) amount of time.
 * This was generalized from GRAVITY_FORCE_LAB_BASICS/ForceSoundGenerator, see
 * https://github.com/phetsims/tambo/issues/76.
 *
 * @author John Blanco (PhET Interactive Simulations)
 * @author Sam Reid (PhET Interactive Simulations)
 */
import '../../../globals.js';
import stepTimer from '../../../axon/js/stepTimer.js';
import Range from '../../../dot/js/Range.js';
import optionize from '../../../phet-core/js/optionize.js';
import isSettingPhetioStateProperty from '../../../tandem/js/isSettingPhetioStateProperty.js';
import soundConstants from '../soundConstants.js';
import SoundClip from './SoundClip.js';
class ContinuousPropertySoundClip extends SoundClip {
    /**
     * @param property
     * @param range - the range of values that the provided property can take on
     * @param sound - returned by the import directive, should be optimized for good continuous looping, which
     *   may require it to be a .wav file, since .mp3 files generally have a bit of silence at the beginning.
     * @param [providedOptions]
     */
    constructor(property, range, sound, providedOptions) {
        ;
        const options = optionize()({
            initialOutputLevel: 0.7,
            loop: true,
            trimSilence: true,
            fadeStartDelay: 0.2,
            fadeTime: 0.15,
            delayBeforeStop: 0.1,
            playbackRateRange: new Range(0.5, 2), // 2 octaves, one below and one above the provided sound's inherent pitch
            normalizationMappingExponent: 1, // linear mapping by default
            stopOnDisabled: false,
            stepEmitter: stepTimer
        }, providedOptions);
        super(sound, options);
        this.fadeTime = options.fadeTime;
        this.delayBeforeStop = options.delayBeforeStop;
        this.nonFadedOutputLevel = options.initialOutputLevel === undefined ? 1 : options.initialOutputLevel;
        this.remainingFadeTime = 0;
        // start with the output level at zero so that the initial sound generation has a bit of fade in
        this.setOutputLevel(0, 0);
        // function for starting the sound or adjusting the volume
        const listener = (value) => {
            // Update the sound generation when the value changes, but only if we enabled. This prevents the play() from
            // occurring at all.
            if (this.fullyEnabled) {
                // Calculate the playback rate.  This is done by first normalizing the value over the provided range, then
                // mapping that value using an exponential function that can be used to create a non-linear mapping to emphasize
                // certain portions of the range.
                const normalizedValue = range.getNormalizedValue(value);
                const mappedNormalizedValueNew = Math.pow(normalizedValue, options.normalizationMappingExponent);
                const playbackRate = options.playbackRateRange.expandNormalizedValue(mappedNormalizedValueNew);
                // Update the parameters of the sound clip based on the new value.
                this.setPlaybackRate(playbackRate);
                this.setOutputLevel(this.nonFadedOutputLevel);
                if (!this.isPlaying && !isSettingPhetioStateProperty.value) {
                    this.play();
                }
                // reset the fade countdown
                this.remainingFadeTime = options.fadeStartDelay + options.fadeTime + this.delayBeforeStop;
            }
        };
        property.lazyLink(listener);
        this.disposeEmitter.addListener(() => property.unlink(listener));
        if (options.stopOnDisabled) {
            this.fullyEnabledProperty.lazyLink(enabled => {
                !enabled && this.stop(soundConstants.DEFAULT_LINEAR_GAIN_CHANGE_TIME);
            });
        }
        // Hook up the time-driven behavior.
        if (options.stepEmitter) {
            const stepEmitterListener = (dt) => this.step(dt);
            options.stepEmitter.addListener(stepEmitterListener);
            // Remove step emitter listener on disposal.
            this.disposeEmitter.addListener(() => options.stepEmitter?.removeListener(stepEmitterListener));
        }
    }
    /**
     * Step this sound generator, used for fading out the sound in the absence of change.
     * @param dt - change in time (i.e. delta time) in seconds
     */
    step(dt) {
        if (this.remainingFadeTime > 0) {
            this.remainingFadeTime = Math.max(this.remainingFadeTime - dt, 0);
            if ((this.remainingFadeTime < this.fadeTime + this.delayBeforeStop) && this.outputLevel > 0) {
                // the sound is fading out, adjust the output level
                const outputLevel = Math.max((this.remainingFadeTime - this.delayBeforeStop) / this.fadeTime, 0);
                this.setOutputLevel(outputLevel * this.nonFadedOutputLevel);
            }
            // fade out complete, stop playback
            if (this.remainingFadeTime === 0 && this.isPlaying) {
                this.stop(0);
            }
        }
    }
    /**
     * stop any in-progress sound generation
     */
    reset() {
        this.stop(0);
        this.remainingFadeTime = 0;
    }
}
export default ContinuousPropertySoundClip;
//# sourceMappingURL=ContinuousPropertySoundClip.js.map