import _ from 'lodash';
import '../../../globals.js';
// Copyright 2018-2024, University of Colorado Boulder
/**
 * SoundGenerator is an abstract base class for Web-Audio-based sound-producing elements that work in conjunction with
 * the soundManager to produces sounds.
 *
 * @author John Blanco (PhET Interactive Simulations)
 */
import BooleanProperty from '../../../axon/js/BooleanProperty.js';
import DerivedProperty from '../../../axon/js/DerivedProperty.js';
import optionize from '../../../phet-core/js/optionize.js';
import phetAudioContext from '../phetAudioContext.js';
import soundConstants from '../soundConstants.js';
import tambo from '../tambo.js';
import isSettingPhetioStateProperty from '../../../tandem/js/isSettingPhetioStateProperty.js';
import EnabledComponent from '../../../axon/js/EnabledComponent.js';
import ResetAllButton from '../../../scenery-phet/js/buttons/ResetAllButton.js';
import soundManager from '../soundManager.js';
import SoundLevelEnum from '../SoundLevelEnum.js';
import { DisplayedProperty } from '../../../scenery/js/imports.js';
// constants
const DEFAULT_TIME_CONSTANT = soundConstants.DEFAULT_PARAM_CHANGE_TIME_CONSTANT;
class SoundGenerator extends EnabledComponent {
    constructor(providedOptions) {
        const options = optionize()({
            initialOutputLevel: 1,
            audioContext: phetAudioContext,
            connectImmediately: false,
            sonificationLevel: SoundLevelEnum.BASIC,
            additionalAudioNodes: [],
            associatedViewNode: null,
            enabledDuringReset: false,
            enabledDuringPhetioStateSetting: false
        }, providedOptions);
        super(options);
        this.audioContext = options.audioContext;
        this._outputLevel = options.initialOutputLevel;
        this.connectionList = [];
        this.mainGainNode = this.audioContext.createGain();
        this.mainGainNode.gain.setValueAtTime(this._outputLevel, this.audioContext.currentTime);
        // If an associated view node was specified, create a boolean Property that is true only when the node is displayed.
        const viewNodeDisplayedProperty = options.associatedViewNode ?
            new DisplayedProperty(options.associatedViewNode) :
            new BooleanProperty(true);
        // Define the property that will determine whether this sound generator should produce sound at all.
        this.fullyEnabledProperty = new DerivedProperty([
            soundManager.enabledProperty,
            this.enabledProperty,
            ResetAllButton.isResettingAllProperty,
            viewNodeDisplayedProperty,
            soundManager.extraSoundEnabledProperty,
            isSettingPhetioStateProperty
        ], (soundEnabled, enabled, isResettingAll, viewNodeDisplayed, extraSoundEnabled, isSettingPhetioState) => soundEnabled && enabled && viewNodeDisplayed &&
            (options.sonificationLevel === SoundLevelEnum.BASIC || extraSoundEnabled) &&
            (!isResettingAll || options.enabledDuringReset) &&
            (!isSettingPhetioState || options.enabledDuringPhetioStateSetting));
        // If the option specifies immediate connection, connect the main gain node to the audio context destination.
        if (options.connectImmediately) {
            this.mainGainNode.connect(this.audioContext.destination);
        }
        // Turn down the gain to zero when not fully enabled and up to the current output level when becoming fully enabled.
        this.fullyEnabledProperty.link(fullyEnabled => {
            const previousGainSetting = fullyEnabled ? 0 : this._outputLevel;
            const newGainSetting = fullyEnabled ? this._outputLevel : 0;
            const now = this.audioContext.currentTime;
            // For the linear ramp to work consistently on all browsers, the gain must be explicitly set to what it is
            // supposed to be before making any changes.  Otherwise, it may extrapolate from the most recent previous event.
            this.mainGainNode.gain.setValueAtTime(previousGainSetting, now);
            // Ramp the gain to the new level.
            this.mainGainNode.gain.linearRampToValueAtTime(newGainSetting, this.audioContext.currentTime + soundConstants.DEFAULT_LINEAR_GAIN_CHANGE_TIME);
        });
        this.soundSourceDestination = this.mainGainNode;
        // Insert any additional audio nodes into the signal chain by iterating backwards through the provided list.
        for (let i = options.additionalAudioNodes.length - 1; i >= 0; i--) {
            const audioNode = options.additionalAudioNodes[i];
            audioNode.connect(this.soundSourceDestination);
            this.soundSourceDestination = audioNode;
        }
        // Clean up memory references when this object is disposed.
        this.disposeEmitter.addListener(() => {
            this.fullyEnabledProperty.dispose();
        });
    }
    /**
     * Connect the sound generator to an audio parameter.
     */
    connect(audioParam) {
        this.mainGainNode.connect(audioParam);
        // Track this sound generator's connections.  This is necessary because Web Audio doesn't support checking which
        // nodes are connected to which, and we need this information when disconnecting.
        this.connectionList.push(audioParam);
    }
    /**
     * Disconnect the sound generator from an audio parameter.
     */
    disconnect(audioParam) {
        this.mainGainNode.disconnect(audioParam);
        this.connectionList = _.without(this.connectionList, audioParam);
    }
    /**
     * Test if this sound generator is connected to the provided audio param.
     */
    isConnectedTo(audioParam) {
        return this.connectionList.includes(audioParam);
    }
    /**
     * Sets the output level of the sound generator.
     *
     * @param outputLevel - generally between 0 and 1, but can be larger than 1 if necessary to amplify a small signal,
     *   and can be negative to invert the phase.
     * @param [timeConstant] - time constant for output level change, longer values mean slower transitions, in seconds.
     *   Note that timeConstant is NOT a fade time. It's an exponential approach to the target output level, and the argument to
     *   setTargetAtTime, documented at https://developer.mozilla.org/en-US/docs/Web/API/AudioParam/setTargetAtTime#timeconstant
     *   The document has this suggestion for choosing the value for timeConstant: "Depending on your use case, getting
     *   95% toward the target value may already be enough; in that case, you could set timeConstant to one third of the
     *   desired duration."
     */
    setOutputLevel(outputLevel, timeConstant = DEFAULT_TIME_CONSTANT) {
        // Ignore attempts to set the output level to the same value.
        if (outputLevel !== this._outputLevel) {
            const now = this.audioContext.currentTime;
            // The output level should take effect immediately if this sound generator is fully enabled.  Otherwise, the value
            // is saved and restored the next time the sound generator transitions to fully enabled.
            if (this.fullyEnabledProperty.value) {
                // Cancel any gain transitions that are currently in progress.
                this.mainGainNode.gain.cancelScheduledValues(now);
                // Set the output level on the gain node.  A different method is used for instant changes.
                if (timeConstant === 0) {
                    this.mainGainNode.gain.setValueAtTime(outputLevel, now);
                }
                else {
                    // The setTargetAtTime method doesn't seem to work if the audio context isn't running, and the event doesn't
                    // seem to be scheduled - it's just ignored.  So, if the audio context isn't running, use an alternative
                    // approach.  See https://github.com/phetsims/tambo/issues/74.
                    if (this.audioContext.state === 'running') {
                        this.mainGainNode.gain.setTargetAtTime(outputLevel, now, timeConstant);
                    }
                    else {
                        this.mainGainNode.gain.linearRampToValueAtTime(outputLevel, now + soundConstants.DEFAULT_LINEAR_GAIN_CHANGE_TIME);
                    }
                }
            }
            // Set local copy of output level.
            this._outputLevel = outputLevel;
        }
    }
    set outputLevel(outputLevel) {
        this.setOutputLevel(outputLevel);
    }
    get outputLevel() {
        return this.getOutputLevel();
    }
    /**
     * Get the current output level setting.  Note that if the sound generator is disabled, this could return a non-zero
     * value but the sound generator won't produce audible sound.
     */
    getOutputLevel() {
        return this._outputLevel;
    }
    get fullyEnabled() {
        return this.fullyEnabledProperty.value;
    }
}
tambo.register('SoundGenerator', SoundGenerator);
export default SoundGenerator;
//# sourceMappingURL=SoundGenerator.js.map