diff --git a/js/AmplitudeModulator.ts b/js/AmplitudeModulator.ts index fa082697..c1f8cb56 100644 --- a/js/AmplitudeModulator.ts +++ b/js/AmplitudeModulator.ts @@ -115,7 +115,7 @@ class AmplitudeModulator extends EnabledComponent { }; this.enabledProperty.link( enabledListener ); - const depthListener = ( depth: any ) => { + const depthListener = ( depth: number ) => { if ( lowFrequencyOscillator ) { lfoAttenuator.gain.setValueAtTime( depth / 2, phetAudioContext.currentTime ); this.modulatedGainNode.gain.setValueAtTime( 1 - depth / 2, phetAudioContext.currentTime ); diff --git a/js/ISoundPlayer.ts b/js/ISoundPlayer.ts index fa0da4f7..b71fa9ea 100644 --- a/js/ISoundPlayer.ts +++ b/js/ISoundPlayer.ts @@ -1,16 +1,15 @@ // Copyright 2022, University of Colorado Boulder /** - * SoundPlayer is a "definition" type, initially based off of PaintDef.js, that defines a type that is used in the tambo - * sound library but is not actually a base class. This is similar to the idea of an "interface" in Java. A - * SoundPlayer type is a sound generator that has just the most basic methods for playing a sound. + * ISoundPlayer defines a simple interface that can be used to support polymorphism when defining options and other + * API interfaces that include sound generation. * * @author John Blanco (PhET Interactive Simulations) */ -type SoundPlayer = { +interface ISoundPlayer { play: () => void; stop: () => void; -}; +} -export default SoundPlayer; +export default ISoundPlayer; diff --git a/js/PeakDetectorAudioNode.ts b/js/PeakDetectorAudioNode.ts index 899cf9e1..533d2581 100644 --- a/js/PeakDetectorAudioNode.ts +++ b/js/PeakDetectorAudioNode.ts @@ -22,7 +22,7 @@ * @author John Blanco (PhET Interactive Simulations) */ -import merge from '../../phet-core/js/merge.js'; +import optionize from '../../phet-core/js/optionize.js'; import phetAudioContext from './phetAudioContext.js'; import tambo from './tambo.js'; @@ -37,7 +37,7 @@ class PeakDetectorAudioNode extends AudioWorkletNode { constructor( providedOptions: PeakDetectorAudioNodeOptions ) { - const options = merge( { + const options = optionize( { logZeroValues: false }, providedOptions ); diff --git a/js/SoundPlayer.js b/js/SoundPlayer.js deleted file mode 100644 index df32a457..00000000 --- a/js/SoundPlayer.js +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2021, University of Colorado Boulder - -/** - * SoundPlayer is a "definition" type, based off of PaintDef.js, that defines a type that is used in the tambo sound - * library but is not actually a base class. This is similar to the idea of an "interface" in Java. A SoundPlayer type - * is a sound generator that has just the most basic methods for playing a sound. - * - * @author John Blanco (PhET Interactive Simulations) - */ - -import tambo from './tambo.js'; - -class SoundPlayer { - - /** - * See if an object has the necessary methods to be considered a SoundPlayer. - * @param objectInstance - * @returns {boolean} - * @public - */ - static isSoundPlayer( objectInstance ) { - return typeof objectInstance.play === 'function' && typeof objectInstance.stop === 'function'; - } -} - -// A static value that is a sound player but produces no sound. This is used as a constant in sound-related code to -// specify that no sound should be produced. -SoundPlayer.NO_SOUND = { - play() {}, - stop() {} -}; - -tambo.register( 'SoundPlayer', SoundPlayer ); -export default SoundPlayer; \ No newline at end of file diff --git a/js/demo/testing/view/CompositeSoundClipTestNode.ts b/js/demo/testing/view/CompositeSoundClipTestNode.ts index 549b8d3b..563cab17 100644 --- a/js/demo/testing/view/CompositeSoundClipTestNode.ts +++ b/js/demo/testing/view/CompositeSoundClipTestNode.ts @@ -12,10 +12,10 @@ import { VBox, VBoxOptions } from '../../../../../scenery/js/imports.js'; import TextPushButton from '../../../../../sun/js/buttons/TextPushButton.js'; import brightMarimba_mp3 from '../../../../sounds/brightMarimba_mp3.js'; import loonCall_mp3 from '../../../../sounds/demo-and-test/loonCall_mp3.js'; -import SoundPlayer from '../../../SoundPlayer.js'; import CompositeSoundClip from '../../../sound-generators/CompositeSoundClip.js'; import soundManager from '../../../soundManager.js'; import tambo from '../../../tambo.js'; +import nullSoundPlayer from '../../../shared-sound-players/nullSoundPlayer.js'; class CompositeSoundClipTestNode extends VBox { @@ -38,15 +38,14 @@ class CompositeSoundClipTestNode extends VBox { const playSoundClipChordButton = new TextPushButton( 'Play CompositeSoundClip', { baseColor: '#aad6cc', font: new PhetFont( 16 ), - soundPlayer: SoundPlayer.NO_SOUND, // turn off default sound generation - listener: () => { compositeSoundClip.play(); } + soundPlayer: compositeSoundClip } ); // add button to stop the sound const stopSoundClipChordButton = new TextPushButton( 'Stop CompositeSoundClip', { baseColor: '#DBB1CD', font: new PhetFont( 16 ), - soundPlayer: SoundPlayer.NO_SOUND, // turn off default sound generation + soundPlayer: nullSoundPlayer, // turn off default sound generation listener: () => { compositeSoundClip.stop(); } } ); diff --git a/js/demo/testing/view/SoundClipChordTestNode.ts b/js/demo/testing/view/SoundClipChordTestNode.ts index 85a25a98..7e08e7a4 100644 --- a/js/demo/testing/view/SoundClipChordTestNode.ts +++ b/js/demo/testing/view/SoundClipChordTestNode.ts @@ -10,7 +10,6 @@ import PhetFont from '../../../../../scenery-phet/js/PhetFont.js'; import { VBox, VBoxOptions } from '../../../../../scenery/js/imports.js'; import TextPushButton from '../../../../../sun/js/buttons/TextPushButton.js'; import brightMarimba_mp3 from '../../../../sounds/brightMarimba_mp3.js'; -import SoundPlayer from '../../../SoundPlayer.js'; import SoundClipChord from '../../../sound-generators/SoundClipChord.js'; import soundManager from '../../../soundManager.js'; import tambo from '../../../tambo.js'; @@ -35,16 +34,14 @@ class SoundClipChordTestNode extends VBox { const playChordButton = new TextPushButton( 'Play Chord', { baseColor: '#aad6cc', font: new PhetFont( 16 ), - soundPlayer: SoundPlayer.NO_SOUND, // turn off default sound generation - listener: () => { chordSoundClipChord.play(); } + soundPlayer: chordSoundClipChord } ); // add button to play an arpeggio const playArpeggioButton = new TextPushButton( 'Play Arpeggiated Chord', { baseColor: '#DBB1CD', font: new PhetFont( 16 ), - soundPlayer: SoundPlayer.NO_SOUND, // turn off default sound generation - listener: () => { arpeggioSoundClipChord.play(); } + soundPlayer: arpeggioSoundClipChord } ); super( optionize( { diff --git a/js/demo/testing/view/TestingScreenView.ts b/js/demo/testing/view/TestingScreenView.ts index 48ccfe3d..ec595514 100644 --- a/js/demo/testing/view/TestingScreenView.ts +++ b/js/demo/testing/view/TestingScreenView.ts @@ -28,7 +28,6 @@ import phetAudioContext from '../../../phetAudioContext.js'; import SoundClip from '../../../sound-generators/SoundClip.js'; import SoundLevelEnum from '../../../SoundLevelEnum.js'; import soundManager from '../../../soundManager.js'; -import SoundPlayer from '../../../SoundPlayer.js'; import tambo from '../../../tambo.js'; import AmplitudeModulatorDemoNode from './AmplitudeModulatorDemoNode.js'; import CompositeSoundClipTestNode from './CompositeSoundClipTestNode.js'; @@ -37,6 +36,7 @@ import RemoveAndDisposeSoundGeneratorsTestPanel from './RemoveAndDisposeSoundGen import SoundClipChordTestNode from './SoundClipChordTestNode.js'; import Bounds2 from '../../../../../dot/js/Bounds2.js'; import { TimerListener } from '../../../../../axon/js/Timer.js'; +import nullSoundPlayer from '../../../shared-sound-players/nullSoundPlayer.js'; // constants const CHECKBOX_SIZE = 16; @@ -149,16 +149,14 @@ class BasicAndEnhancedSoundTestNode extends VBox { const playBasicSoundButton = new TextPushButton( 'Play Basic-Level Sound', { baseColor: '#aad6cc', font: new PhetFont( 16 ), - soundPlayer: SoundPlayer.NO_SOUND, // turn off default sound generation - listener: () => { loonCallSoundClip.play(); } + soundPlayer: loonCallSoundClip } ); // add button to play enhanced-mode sound const playEnhancedSoundButton = new TextPushButton( 'Play Enhanced-Level Sound', { baseColor: '#DBB1CD', font: new PhetFont( 16 ), - soundPlayer: SoundPlayer.NO_SOUND, // turn off default sound generation - listener: () => { rhodesChordSoundClip.play(); } + soundPlayer: rhodesChordSoundClip } ); super( merge( { @@ -212,23 +210,21 @@ class AdditionalAudioNodesTestNode extends VBox { const playNormalSoundButton = new TextPushButton( 'Normal Sound Clip', { baseColor: '#CCFF00', font: buttonFont, - soundPlayer: SoundPlayer.NO_SOUND, // turn off default sound generation - listener: () => { shortSoundNormal.play(); } + soundPlayer: shortSoundNormal } ); // add button to play the sound with the reverb added in the signal path const playSoundWithInsertedAudioNodeButton = new TextPushButton( 'Same Clip with In-Line Reverb Node', { baseColor: '#CC99FF', font: buttonFont, - soundPlayer: SoundPlayer.NO_SOUND, // turn off default sound generation - listener: () => {shortSoundWithReverb.play();} + soundPlayer: shortSoundWithReverb } ); // add button to play both sounds at the same time const playBothSounds = new TextPushButton( 'Both Clips Simultaneously', { baseColor: '#FF9999', font: buttonFont, - soundPlayer: SoundPlayer.NO_SOUND, // turn off default sound generation + soundPlayer: nullSoundPlayer, // turn off default sound generation listener: () => { shortSoundNormal.play(); shortSoundWithReverb.play(); diff --git a/js/shared-sound-players/nullSoundPlayer.ts b/js/shared-sound-players/nullSoundPlayer.ts new file mode 100644 index 00000000..bba8abab --- /dev/null +++ b/js/shared-sound-players/nullSoundPlayer.ts @@ -0,0 +1,27 @@ +// Copyright 2020-2022, University of Colorado Boulder + +/** + * The nullSoundPlayer is a singleton that implements the ISoundPlayer interface but produces no sound. It is most + * often used to turn off sound generation in an interactive component that produces sound by default. + * + * @author John Blanco (PhET Interactive Simulations) + */ + +import tambo from '../tambo.js'; +import ISoundPlayer from '../ISoundPlayer.js'; + +class NullSoundPlayer implements ISoundPlayer { + + constructor() {} + + public play() {} + + public stop() {} +} + +// Create the singleton instance. +const nullSoundPlayer = new NullSoundPlayer(); + +tambo.register( 'nullSoundPlayer', nullSoundPlayer ); + +export default nullSoundPlayer; \ No newline at end of file diff --git a/js/sound-generators/SoundClip.ts b/js/sound-generators/SoundClip.ts index 002de192..d3aa2c93 100644 --- a/js/sound-generators/SoundClip.ts +++ b/js/sound-generators/SoundClip.ts @@ -112,9 +112,7 @@ class SoundClip extends SoundGenerator { // For sounds that are created statically during the module load phase, this listener will interpret the audio // data once the load of that data has completed. For all sounds constructed after the module load phase has // completed, this will process right away. - // TODO: (for and from @jbphet) - Review the 'any' typespec below with a developer who understands this better, see https://github.com/phetsims/tambo/issues/160 - // const setStartAndEndPoints = ( audioBuffer: AudioBuffer ) => { - const setStartAndEndPoints = ( audioBuffer: any ) => { + const setStartAndEndPoints = ( audioBuffer: AudioBuffer | null ) => { if ( audioBuffer ) { const loopBoundsInfo = SoundUtils.detectSoundBounds( audioBuffer ); this.soundStart = loopBoundsInfo.soundStart; diff --git a/js/sound-generators/SoundClipChord.ts b/js/sound-generators/SoundClipChord.ts index daf38cd6..84e6e6d1 100644 --- a/js/sound-generators/SoundClipChord.ts +++ b/js/sound-generators/SoundClipChord.ts @@ -14,6 +14,7 @@ import SoundClip, { SoundClipOptions } from '../../../tambo/js/sound-generators/ import SoundGenerator, { SoundGeneratorOptions } from '../../../tambo/js/sound-generators/SoundGenerator.js'; import tambo from '../tambo.js'; import WrappedAudioBuffer from '../WrappedAudioBuffer.js'; +import ISoundPlayer from '../ISoundPlayer.js'; type SelfOptions = { @@ -33,7 +34,7 @@ type SelfOptions = { export type SoundClipChordOptions = SelfOptions & SoundGeneratorOptions; -class SoundClipChord extends SoundGenerator { +class SoundClipChord extends SoundGenerator implements ISoundPlayer { // whether to play the chord as an arpeggio private readonly arpeggiate: boolean; @@ -92,6 +93,15 @@ class SoundClipChord extends SoundGenerator { } ); } + /** + * Stop the chord if it's playing. This is mostly here to complete the ISoundPlayer interface. + */ + public stop() { + this.playbackSoundClips.forEach( soundClip => { + soundClip.stop(); + } ); + } + /** * Release any memory references in order to avoid memory leaks. */ diff --git a/js/sound-generators/ValueChangeSoundGenerator.ts b/js/sound-generators/ValueChangeSoundGenerator.ts index 9bc585b9..5df9a7ca 100644 --- a/js/sound-generators/ValueChangeSoundGenerator.ts +++ b/js/sound-generators/ValueChangeSoundGenerator.ts @@ -17,7 +17,6 @@ import Range from '../../../dot/js/Range.js'; import optionize from '../../../phet-core/js/optionize.js'; import generalBoundaryBoopSoundPlayer from '../shared-sound-players/generalBoundaryBoopSoundPlayer.js'; import generalSoftClickSoundPlayer from '../shared-sound-players/generalSoftClickSoundPlayer.js'; -import SoundPlayer from '../SoundPlayer.js'; import tambo from '../tambo.js'; import ISoundPlayer from '../ISoundPlayer.js'; import SoundGenerator, { SoundGeneratorOptions } from './SoundGenerator.js'; @@ -27,6 +26,7 @@ import generalBoundaryBoop_mp3 from '../../sounds/generalBoundaryBoop_mp3.js'; import generalSoftClick_mp3 from '../../sounds/generalSoftClick_mp3.js'; import Utils from '../../../dot/js/Utils.js'; import SoundClip from './SoundClip.js'; +import nullSoundPlayer from '../shared-sound-players/nullSoundPlayer.js'; // constants const DEFAULT_NUMBER_OF_MIDDLE_THRESHOLDS = 5; // fairly arbitrary @@ -291,9 +291,9 @@ class ValueChangeSoundGenerator extends SoundGenerator { * Static instance that makes no sound. This is generally used as an option value to turn off sound generation. */ static NO_SOUND = new ValueChangeSoundGenerator( new Range( 0, 1 ), { - middleMovingUpSoundPlayer: SoundPlayer.NO_SOUND, - minSoundPlayer: SoundPlayer.NO_SOUND, - maxSoundPlayer: SoundPlayer.NO_SOUND + middleMovingUpSoundPlayer: nullSoundPlayer, + minSoundPlayer: nullSoundPlayer, + maxSoundPlayer: nullSoundPlayer } ) } diff --git a/js/soundManager.ts b/js/soundManager.ts index 65765934..c7ade24c 100644 --- a/js/soundManager.ts +++ b/js/soundManager.ts @@ -200,11 +200,11 @@ class SoundManager extends PhetioObject { this.dryGainNode.connect( this.masterGainNode ); // Create and hook up gain nodes for each of the defined categories. + assert && assert( this.convolver !== null && this.dryGainNode !== null, 'some audio nodes have not been initialized' ); options.categories.forEach( categoryName => { const gainNode = phetAudioContext.createGain(); - // TODO: Why are the following casts necessary? See https://github.com/phetsims/tambo/issues/160. - gainNode.connect( this.convolver as AudioNode ); - gainNode.connect( this.dryGainNode as AudioNode ); + gainNode.connect( this.convolver! ); + gainNode.connect( this.dryGainNode! ); this.gainNodesForCategories.set( categoryName, gainNode ); } ); @@ -351,6 +351,9 @@ class SoundManager extends PhetioObject { return; } + // state checking - make sure the needed nodes have been created + assert && assert( this.convolver !== null && this.dryGainNode !== null, 'some audio nodes have not been initialized' ); + // Verify that this is not a duplicate addition. const hasSoundGenerator = this.hasSoundGenerator( soundGenerator ); assert && assert( !hasSoundGenerator, 'can\'t add the same sound generator twice' ); @@ -379,15 +382,15 @@ class SoundManager extends PhetioObject { // Connect the sound generator to an output path. if ( options.categoryName === null ) { - soundGenerator.connect( this.convolver as AudioNode ); - soundGenerator.connect( this.dryGainNode as AudioNode ); + soundGenerator.connect( this.convolver! ); + soundGenerator.connect( this.dryGainNode! ); } else { assert && assert( this.gainNodesForCategories.has( options.categoryName! ), `category does not exist : ${options.categoryName}` ); - soundGenerator.connect( this.gainNodesForCategories.get( options.categoryName! ) as AudioNode ); + soundGenerator.connect( this.gainNodesForCategories.get( options.categoryName! )! ); } // Keep a record of the sound generator along with additional information about it. @@ -442,11 +445,11 @@ class SoundManager extends PhetioObject { assert && assert( soundGeneratorInfo, 'unable to remove sound generator - not found' ); // disconnect the sound generator from any audio nodes to which it may be connected - if ( soundGenerator.isConnectedTo( this.convolver as AudioNode ) ) { - soundGenerator.disconnect( this.convolver as AudioNode ); + if ( soundGenerator.isConnectedTo( this.convolver! ) ) { + soundGenerator.disconnect( this.convolver! ); } - if ( soundGenerator.isConnectedTo( this.dryGainNode as AudioNode ) ) { - soundGenerator.disconnect( this.dryGainNode as AudioNode ); + if ( soundGenerator.isConnectedTo( this.dryGainNode! ) ) { + soundGenerator.disconnect( this.dryGainNode! ); } this.gainNodesForCategories.forEach( gainNode => { if ( soundGenerator.isConnectedTo( gainNode ) ) {