-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathsoundManager.js
644 lines (541 loc) · 25.3 KB
/
soundManager.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
// Copyright 2018-2021, University of Colorado Boulder
/**
* A singleton object that registers sound generators, connects them to the audio output, and provides a number of
* related services, such as:
* - master enable/disable
* - master gain control
* - enable/disable of sounds based on visibility of an associated Scenery node
* - enable/disable of sounds based on their assigned sonification level (e.g. "basic" or "enhanced")
* - gain control for sounds based on their assigned category, e.g. UI versus sim-specific sounds
* - a shared reverb unit to add some spatialization and make all sounds seem to originate with the same space
*
* The singleton object must be initialized before sound generators can be added.
*/
import BooleanProperty from '../../axon/js/BooleanProperty.js';
import Property from '../../axon/js/Property.js';
import Utils from '../../dot/js/Utils.js';
import merge from '../../phet-core/js/merge.js';
import Display from '../../scenery/js/display/Display.js';
import DisplayedProperty from '../../scenery/js/util/DisplayedProperty.js';
import PhetioObject from '../../tandem/js/PhetioObject.js';
import Tandem from '../../tandem/js/Tandem.js';
import reverbImpulseResponseSound from '../sounds/empty_apartment_bedroom_06_resampled_mp3.js';
import audioContextStateChangeMonitor from './audioContextStateChangeMonitor.js';
import phetAudioContext from './phetAudioContext.js';
import soundConstants from './soundConstants.js';
import SoundLevelEnum from './SoundLevelEnum.js';
import tambo from './tambo.js';
// constants
const DEFAULT_REVERB_LEVEL = 0.02;
const LINEAR_GAIN_CHANGE_TIME = soundConstants.DEFAULT_LINEAR_GAIN_CHANGE_TIME; // in seconds
const GAIN_LOGGING_ENABLED = false;
class SoundManager extends PhetioObject {
/**
* @param {Tandem} tandem
*/
constructor( tandem ) {
super( {
tandem: tandem,
phetioState: false,
phetioDocumentation: 'Controls the simulation\'s sound. For sims that do not support sound, this element and ' +
'its children can be ignored.'
} );
// @public (read-only) {BooleanProperty} - global enabled state for sound generation
this.enabledProperty = new BooleanProperty(
( phet.chipper.queryParameters.sound === 'enabled' && phet.chipper.queryParameters.supportsSound ), {
tandem: tandem.createTandem( 'enabledProperty' ),
phetioFeatured: true,
phetioDocumentation: 'Determines whether sound is enabled.'
} );
// @public (read-only) {BooleanProperty} - enabled state for enhanced sounds
this.enhancedSoundEnabledProperty = new BooleanProperty( phet.chipper.queryParameters.enhancedSoundInitiallyEnabled, {
tandem: tandem.createTandem( 'enhancedSoundEnabledProperty' ),
phetioFeatured: true,
phetioDocumentation: 'Determines whether enhanced sound is enabled. Enhanced sound is additional sounds that ' +
'can serve to improve the learning experience for individuals with visual disabilities. ' +
'Note that not all simulations that support sound also support enhanced sound. Also note ' +
'that the value is irrelevant when enabledProperty is false.'
} );
// @private {Array.<{ soundGenerator:SoundGenerator, sonificationLevel:string }>} - array where the sound
// generators are stored along with information about how to manage them
this.soundGeneratorInfoArray = [];
// @private {number} - output level for the master gain node when sonification is enabled, valid range is 0 to 1
this._masterOutputLevel = 1;
// @private {number} - reverb level, needed because some browsers don't support reading of gain values, see
// methods for more info
this._reverbLevel = DEFAULT_REVERB_LEVEL;
// @private {Object} - a map of category name to GainNode instances that control gains for that category name,
// will be filled in during init, see the usage of options.categories in the initialize function for more
// information.
this.gainNodesForCategories = {};
// @private {boolean} - flag that tracks whether the sonification manager has been initialized
this.initialized = false;
// @private {Object[]} - sound generators and options that were added before initialization and will be added once
// initialization is complete
this.soundGeneratorsAwaitingAdd = [];
}
/**
* Initialize the sonification manager. This function must be invoked before any sound generators can be added.
* @param {Property.<boolean>} simConstructionCompleteProperty
* @param {Property.<boolean>} audioEnabledProperty
* @param {Property.<boolean>} simVisibleProperty
* @param {Property.<boolean>} simActiveProperty
* @param {Property.<boolean>} simSettingPhetioStateProperty
* @param {Object} [options]
* @public
*/
initialize( simConstructionCompleteProperty,
audioEnabledProperty,
simVisibleProperty,
simActiveProperty,
simSettingPhetioStateProperty,
options ) {
assert && assert( !this.initialized, 'can\'t initialize the sound manager more than once' );
options = merge( {
// Categories that can be used to group sound generators together and control their volume as a group - the
// names can be anything that will work as a key for a JavaScript object, but initially we've chosen to use
// names with conventions similar to what is commonly seen for CSS classes.
categories: [ 'sim-specific', 'user-interface' ]
}, options );
// options validation
assert && assert( typeof Array.isArray( options.categories ), 'unexpected type for options.categories' );
assert && assert(
_.every( options.categories, categoryName => typeof categoryName === 'string' ),
'unexpected type of element in options.categories'
);
const now = phetAudioContext.currentTime;
// The final stage is a dynamics compressor that is used essentially as a limiter to prevent clipping.
const dynamicsCompressor = phetAudioContext.createDynamicsCompressor();
dynamicsCompressor.threshold.setValueAtTime( -6, now );
dynamicsCompressor.knee.setValueAtTime( 5, now );
dynamicsCompressor.ratio.setValueAtTime( 12, now );
dynamicsCompressor.attack.setValueAtTime( 0, now );
dynamicsCompressor.release.setValueAtTime( 0.25, now );
dynamicsCompressor.connect( phetAudioContext.destination );
// Create the master gain node for all sounds managed by this sonification manager.
this.masterGainNode = phetAudioContext.createGain();
this.masterGainNode.connect( dynamicsCompressor );
// convolver node, which will be used to create the reverb effect
this.convolver = phetAudioContext.createConvolver();
const setConvolverBuffer = audioBuffer => {
if ( audioBuffer ) {
this.convolver.buffer = audioBuffer;
reverbImpulseResponseSound.audioBufferProperty.unlink( setConvolverBuffer );
}
};
reverbImpulseResponseSound.audioBufferProperty.link( setConvolverBuffer );
// gain node that will control the reverb level
this.reverbGainNode = phetAudioContext.createGain();
this.reverbGainNode.connect( this.masterGainNode );
this.reverbGainNode.gain.setValueAtTime( this._reverbLevel, phetAudioContext.currentTime );
this.convolver.connect( this.reverbGainNode );
// dry (non-reverbed) portion of the output
this.dryGainNode = phetAudioContext.createGain();
this.dryGainNode.gain.setValueAtTime( 1 - this._reverbLevel, phetAudioContext.currentTime );
this.dryGainNode.gain.linearRampToValueAtTime(
1 - this._reverbLevel,
phetAudioContext.currentTime + LINEAR_GAIN_CHANGE_TIME
);
this.dryGainNode.connect( this.masterGainNode );
// Create and hook up gain nodes for each of the defined categories.
options.categories.forEach( categoryName => {
const gainNode = phetAudioContext.createGain();
gainNode.connect( this.convolver );
gainNode.connect( this.dryGainNode );
this.gainNodesForCategories[ categoryName ] = gainNode;
} );
// Hook up a listener that turns down the gain if sonification is disabled or if the sim isn't visible or isn't
// active.
Property.multilink(
[
this.enabledProperty,
audioEnabledProperty,
simConstructionCompleteProperty,
simVisibleProperty,
simActiveProperty,
simSettingPhetioStateProperty
],
( enabled, audioEnabled, simInitComplete, simVisible, simActive, simSettingPhetioState ) => {
const fullyEnabled = enabled && audioEnabled && simInitComplete && simVisible && simActive && !simSettingPhetioState;
const gain = fullyEnabled ? this._masterOutputLevel : 0;
// Set the gain, but somewhat gradually in order to avoid rapid transients, which can sound like clicks.
this.masterGainNode.gain.linearRampToValueAtTime(
gain,
phetAudioContext.currentTime + LINEAR_GAIN_CHANGE_TIME
);
}
);
// Handle the audio context state, both when changes occur and when it is initially muted. As of this writing
// (Feb 2019), there are some differences in how the audio context state behaves on different platforms, so the
// code monitors different events and states to keep the audio context running. As the behavior of the audio
// context becomes more consistent across browsers, it may be possible to simplify this.
if ( !phetAudioContext.isStubbed ) {
// function to remove the listeners, used to avoid code duplication
const removeUserInteractionListeners = () => {
window.removeEventListener( 'touchstart', resumeAudioContext, false );
if ( Display.userGestureEmitter.hasListener( resumeAudioContext ) ) {
Display.userGestureEmitter.removeListener( resumeAudioContext );
}
};
// listener that resumes the audio context
const resumeAudioContext = () => {
if ( phetAudioContext.state !== 'running' ) {
phet.log && phet.log( `audio context not running, attempting to resume, state = ${phetAudioContext.state}` );
// tell the audio context to resume
phetAudioContext.resume()
.then( () => {
phet.log && phet.log( `resume appears to have succeeded, phetAudioContext.state = ${phetAudioContext.state}` );
removeUserInteractionListeners();
} )
.catch( err => {
const errorMessage = `error when trying to resume audio context, err = ${err}`;
console.error( errorMessage );
assert && alert( errorMessage );
} );
}
else {
// audio context is already running, no need to listen anymore
removeUserInteractionListeners();
}
};
// listen for a touchstart - this only works to resume the audio context on iOS devices (as of this writing)
window.addEventListener( 'touchstart', resumeAudioContext, false );
// listen for other user gesture events
Display.userGestureEmitter.addListener( resumeAudioContext );
// During testing, several use cases were found where the audio context state changes to something other than
// the "running" state while the sim is in use (generally either "suspended" or "interrupted", depending on the
// browser). The following code is intended to handle this situation by trying to resume it right away. GitHub
// issues with details about why this is necessary are:
// - https://github.com/phetsims/tambo/issues/58
// - https://github.com/phetsims/tambo/issues/59
// - https://github.com/phetsims/fractions-common/issues/82
// - https://github.com/phetsims/friction/issues/173
// - https://github.com/phetsims/resistance-in-a-wire/issues/190
// - https://github.com/phetsims/tambo/issues/90
let previousAudioContextState = phetAudioContext.state;
audioContextStateChangeMonitor.addStateChangeListener( phetAudioContext, state => {
phet.log && phet.log(
`audio context state changed, old state = ${
previousAudioContextState
}, new state = ${
state
}, audio context time = ${
phetAudioContext.currentTime}`
);
if ( state !== 'running' ) {
// add a listener that will resume the audio context on the next touchstart
window.addEventListener( 'touchstart', resumeAudioContext, false );
// listen for other user gesture events too
Display.userGestureEmitter.addListener( resumeAudioContext );
}
previousAudioContextState = state;
} );
}
this.initialized = true;
// Add any sound generators that were waiting for initialization to complete (must be done after init complete).
this.soundGeneratorsAwaitingAdd.forEach( soundGeneratorAwaitingAdd => {
this.addSoundGenerator( soundGeneratorAwaitingAdd.soundGenerator, soundGeneratorAwaitingAdd.options );
} );
this.soundGeneratorsAwaitingAdd.length = 0;
}
/**
* Returns true if the soundGenerator has been added to the soundManager.
* @param {SoundGenerator} soundGenerator
* @returns {boolean}
* @public
*/
hasSoundGenerator( soundGenerator ) {
return _.some(
this.soundGeneratorInfoArray,
soundGeneratorInfo => soundGeneratorInfo.soundGenerator === soundGenerator
);
}
/**
* Add a sound generator. This connects the sound generator to the audio path, puts it on the list of sound
* generators, and creates and returns a unique ID.
* @param {SoundGenerator} soundGenerator
* @param {Object} [options]
* @public
*/
addSoundGenerator( soundGenerator, options ) {
// Check if initialization has been done and, if not, queue the sound generator and its options for addition
// once initialization is complete. Note that when sound is not supported, initialization will never occur.
if ( !this.initialized ) {
this.soundGeneratorsAwaitingAdd.push( { soundGenerator: soundGenerator, options: options } );
return;
}
// Verify that this is not a duplicate addition.
const hasSoundGenerator = this.hasSoundGenerator( soundGenerator );
assert && assert( !hasSoundGenerator, 'can\'t add the same sound generator twice' );
// default options
options = merge( {
// {string} - The 'sonification level' is used to determine whether a given sound should be enabled given the
// setting of the sonification level parameter for the sim. Valid values are 'BASIC' or 'ENHANCED'.
sonificationLevel: SoundLevelEnum.BASIC,
// {Node|null} - a Scenery node that, if provided, must be visible in the display for the sound generator to be
// enabled. This is generally used only for sounds that can play for long durations, such as a looping sound
// clip, that should be stopped when the associated visual representation is hidden.
associatedViewNode: null,
// {string} - category name for this sound, which can be used to group sounds together an control them as a group
categoryName: null
}, options );
// option validation
assert && assert(
_.includes( _.values( SoundLevelEnum ), options.sonificationLevel ),
`invalid value for sonification level: ${options.sonificationLevel}`
);
// Connect the sound generator to an output path.
if ( options.categoryName === null ) {
soundGenerator.connect( this.convolver );
soundGenerator.connect( this.dryGainNode );
}
else {
soundGenerator.connect( this.gainNodesForCategories[ options.categoryName ] );
}
// Keep a record of the sound generator along with additional information about it.
const soundGeneratorInfo = {
soundGenerator: soundGenerator,
sonificationLevel: options.sonificationLevel
};
this.soundGeneratorInfoArray.push( soundGeneratorInfo );
// Add the global enable Property to the list of Properties that enable this sound generator.
soundGenerator.addEnableControlProperty( this.enabledProperty );
// If this sound generator is only enabled in enhanced mode, add the enhanced mode Property as an enable control.
if ( options.sonificationLevel === SoundLevelEnum.ENHANCED ) {
soundGenerator.addEnableControlProperty( this.enhancedSoundEnabledProperty );
}
// If a view node was specified, create and pass in a boolean Property that is true only when the node is displayed.
if ( options.associatedViewNode ) {
soundGenerator.addEnableControlProperty(
new DisplayedProperty( options.associatedViewNode )
);
}
}
/**
* Remove the specified sound generator.
* @param {SoundGenerator} soundGenerator
* @public
*/
removeSoundGenerator( soundGenerator ) {
// Check if the sound manager is initialized and, if not, issue a warning and ignore the request. This is not an
// assertion because the sound manager may not be initialized in cases where the sound is not enabled for the
// simulation, but this method can still end up being invoked.
if ( !this.initialized ) {
console.warn( 'an attempt was made to remove a sound generator from an uninitialized sound manager, ignoring' );
return;
}
// find the info object for this sound generator
let soundGeneratorInfo = null;
for ( let i = 0; i < this.soundGeneratorInfoArray.length; i++ ) {
if ( this.soundGeneratorInfoArray[ i ].soundGenerator === soundGenerator ) {
// found it
soundGeneratorInfo = this.soundGeneratorInfoArray[ i ];
break;
}
}
// make sure it is actually present on the list
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 ) ) {
soundGenerator.disconnect( this.convolver );
}
if ( soundGenerator.isConnectedTo( this.dryGainNode ) ) {
soundGenerator.disconnect( this.dryGainNode );
}
_.values( this.gainNodesForCategories ).forEach( gainNode => {
if ( soundGenerator.isConnectedTo( gainNode ) ) {
soundGenerator.disconnect( gainNode );
}
} );
// remove the sound generator from the list
this.soundGeneratorInfoArray = _.without( this.soundGeneratorInfoArray, soundGeneratorInfo );
}
/**
* Set the master output level for sonification.
* @param {number} level - valid values from 0 (min) through 1 (max)
* @public
*/
setMasterOutputLevel( level ) {
// Check if initialization has been done. This is not an assertion because the sound manager may not be
// initialized if sound is not enabled for the sim.
if ( !this.initialized ) {
console.warn( 'an attempt was made to set the master output level on an uninitialized sound manager, ignoring' );
return;
}
// range check
assert && assert( level >= 0 && level <= 1, `output level value out of range: ${level}` );
this._masterOutputLevel = level;
if ( this.enabledProperty.value ) {
this.masterGainNode.gain.linearRampToValueAtTime(
level,
phetAudioContext.currentTime + LINEAR_GAIN_CHANGE_TIME
);
}
}
set masterOutputLevel( outputLevel ) {
this.setMasterOutputLevel( outputLevel );
}
/**
* Get the current output level setting.
* @returns {number}
* @public
*/
getMasterOutputLevel() {
return this._masterOutputLevel;
}
get masterOutputLevel() {
return this.getMasterOutputLevel();
}
/**
* Set the output level for the specified category of sound generator.
* @param {String} categoryName - name of category to which this invocation applies
* @param {number} outputLevel - valid values from 0 through 1
* @public
*/
setOutputLevelForCategory( categoryName, outputLevel ) {
// Check if initialization has been done. This is not an assertion because the sound manager may not be
// initialized if sound is not enabled for the sim.
if ( !this.initialized ) {
console.warn( 'an attempt was made to set the output level for a sound category on an uninitialized sound manager, ignoring' );
return;
}
assert && assert( this.initialized, 'output levels for categories cannot be added until initialization has been done' );
// range check
assert && assert( outputLevel >= 0 && outputLevel <= 1, `output level value out of range: ${outputLevel}` );
// verify that the specified category exists
assert && assert( this.gainNodesForCategories[ categoryName ], `no category with name = ${categoryName}` );
this.gainNodesForCategories[ categoryName ].gain.setValueAtTime( outputLevel, phetAudioContext.currentTime );
}
/**
* Get the output level for the specified sound generator category.
* @param {String} categoryName - name of category to which this invocation applies
* @returns {number}
* @public
*/
getOutputLevelForCategory( categoryName ) {
// Check if initialization has been done. This is not an assertion because the sound manager may not be
// initialized if sound is not enabled for the sim.
if ( !this.initialized ) {
console.warn( 'an attempt was made to get the output level for a sound category on an uninitialized sound manager, returning 0' );
return 0;
}
// Get the GainNode for the specified category.
const gainNode = this.gainNodesForCategories[ categoryName ];
assert && assert( gainNode, `no category with name = ${categoryName}` );
return gainNode.gain.value;
}
/**
* Set the amount of reverb.
* @param {number} newReverbLevel - value from 0 to 1, 0 = totally dry, 1 = wet
* @public
*/
setReverbLevel( newReverbLevel ) {
// Check if initialization has been done. This is not an assertion because the sound manager may not be
// initialized if sound is not enabled for the sim.
if ( !this.initialized ) {
console.warn( 'an attempt was made to set the reverb level on an uninitialized sound manager, ignoring' );
return;
}
if ( newReverbLevel !== this._reverbLevel ) {
assert && assert( newReverbLevel >= 0 && newReverbLevel <= 1, `reverb value out of range: ${newReverbLevel}` );
const now = phetAudioContext.currentTime;
this.reverbGainNode.gain.linearRampToValueAtTime( newReverbLevel, now + LINEAR_GAIN_CHANGE_TIME );
this.dryGainNode.gain.linearRampToValueAtTime( 1 - newReverbLevel, now + LINEAR_GAIN_CHANGE_TIME );
this._reverbLevel = newReverbLevel;
}
}
set reverbLevel( reverbLevel ) {
this.setReverbLevel( reverbLevel );
}
/**
* @returns {number}
* @public
*/
getReverbLevel() {
return this._reverbLevel;
}
get reverbLevel() {
return this.getReverbLevel();
}
/**
* ES5 setter for enabled state
* @param {boolean} enabled
*/
set enabled( enabled ) {
this.enabledProperty.value = enabled;
}
/**
* ES5 getter for enabled state
* @returns {boolean}
*/
get enabled() {
return this.enabledProperty.value;
}
/**
* ES5 setter for sonification level
* @param {string} sonificationLevel
*/
set sonificationLevel( sonificationLevel ) {
assert && assert(
_.includes( _.values( SoundLevelEnum ), sonificationLevel ),
`invalid sonification level: ${sonificationLevel}`
);
this.enhancedSoundEnabledProperty.value = sonificationLevel === SoundLevelEnum.ENHANCED;
}
/**
* ES5 getter for sonification level
* @returns {string}
*/
get sonificationLevel() {
return this.enhancedSoundEnabledProperty.value ? SoundLevelEnum.ENHANCED : SoundLevelEnum.BASIC;
}
/**
* Log the value of the gain parameter at every animation frame for the specified duration. This is useful for
* debugging, because these parameters change over time when set using methods like "setTargetAtTime", and the
* details of how they change seems to be different on the different browsers.
*
* It may be possible to remove this method someday once the behavior is more consistent across browsers. See
* https://github.com/phetsims/resistance-in-a-wire/issues/205 for some history on this.
*
* @param {GainNode} gainNode
* @param {number} duration - duration for logging, in seconds
* @public
*/
logGain( gainNode, duration ) {
duration = duration || 1;
const startTime = Date.now();
// closure that will be invoked multiple times to log the changing values
function logGain() {
const now = Date.now();
const timeInMilliseconds = now - startTime;
console.log( `Time (ms): ${Utils.toFixed( timeInMilliseconds, 2 )}, Gain Value: ${gainNode.gain.value}` );
if ( now - startTime < ( duration * 1000 ) ) {
window.requestAnimationFrame( logGain );
}
}
if ( GAIN_LOGGING_ENABLED ) {
// kick off the logging
console.log( '------- start of gain logging -----' );
logGain();
}
}
/**
* Log the value of the master gain as it changes, used primarily for debug.
* @param {number} duration - in seconds
* @public
*/
logMasterGain( duration ) {
this.logGain( this.masterGainNode, duration );
}
/**
* Log the value of the reverb gain as it changes, used primarily for debug.
* @param {number} duration - in seconds
* @public
*/
logReverbGain( duration ) {
this.logGain( this.reverbGainNode, duration );
}
}
const soundManager = new SoundManager( Tandem.GENERAL_VIEW.createTandem( 'soundManager' ) );
tambo.register( 'soundManager', soundManager );
export default soundManager;