cacophony 0.32.0 → 0.32.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -2
- package/dist/events.d.ts +1 -0
- package/dist/playback.cjs +1 -1
- package/dist/playback.cjs.map +1 -1
- package/dist/playback.mjs +1 -1
- package/dist/playback.mjs.map +1 -1
- package/docs/classes/AudioCache.html +4 -4
- package/docs/classes/AudioSprite.html +7 -7
- package/docs/classes/BarberpoleEffect.html +6 -6
- package/docs/classes/BiquadEffect.html +3 -3
- package/docs/classes/Bus.html +21 -21
- package/docs/classes/Cacophony.html +86 -86
- package/docs/classes/DynamicsEffect.html +6 -6
- package/docs/classes/FdnReverbEffect.html +6 -6
- package/docs/classes/FoaDecoder.html +5 -5
- package/docs/classes/FoaDecoderEffect.html +3 -3
- package/docs/classes/FrequencyShifterEffect.html +6 -6
- package/docs/classes/Group.html +32 -32
- package/docs/classes/HarmonizerEffect.html +6 -6
- package/docs/classes/ImpulseResponseEffect.html +3 -3
- package/docs/classes/KWeightingFilter.html +4 -4
- package/docs/classes/LoudnessMeter.html +11 -11
- package/docs/classes/MediaStreamPlayback.html +67 -67
- package/docs/classes/MediaStreamSound.html +47 -47
- package/docs/classes/MicrophoneStream.html +48 -48
- package/docs/classes/ModulatedDelayEffect.html +6 -6
- package/docs/classes/PcmStreamPlayback.html +67 -67
- package/docs/classes/PcmStreamSound.html +57 -57
- package/docs/classes/PhaserEffect.html +6 -6
- package/docs/classes/Playback.html +81 -81
- package/docs/classes/ReverbEffect.html +6 -6
- package/docs/classes/Scheduler.html +3 -3
- package/docs/classes/ShareEffect.html +3 -3
- package/docs/classes/Sound.html +68 -68
- package/docs/classes/SpectralFreezeEffect.html +6 -6
- package/docs/classes/StereoWidenerEffect.html +3 -3
- package/docs/classes/Synth.html +59 -59
- package/docs/classes/SynthGroup.html +28 -28
- package/docs/classes/TremoloEffect.html +6 -6
- package/docs/classes/TruePeakDetector.html +8 -8
- package/docs/classes/WaveshaperEffect.html +6 -6
- package/docs/functions/encodeMonoToFoaSN3D.html +1 -1
- package/docs/functions/integratedLoudness.html +1 -1
- package/docs/functions/integratedUngatedLoudness.html +1 -1
- package/docs/functions/loudnessRange.html +1 -1
- package/docs/functions/timeStretch.html +1 -1
- package/docs/functions/timeStretchChannels.html +1 -1
- package/docs/functions/truePeakDb.html +1 -1
- package/docs/index.html +7 -1
- package/docs/interfaces/AudioBuffer.html +8 -8
- package/docs/interfaces/AudioBufferSourceNode.html +21 -21
- package/docs/interfaces/AudioEventCallbacks.html +1 -1
- package/docs/interfaces/AudioListener.html +12 -12
- package/docs/interfaces/AudioNode.html +13 -13
- package/docs/interfaces/AudioParam.html +6 -6
- package/docs/interfaces/AudioWorklet.html +2 -2
- package/docs/interfaces/AudioWorkletNode.html +15 -15
- package/docs/interfaces/BarberpoleOptions.html +5 -5
- package/docs/interfaces/BaseContext.html +25 -25
- package/docs/interfaces/BaseSound.html +16 -16
- package/docs/interfaces/BiquadCoefficients.html +6 -6
- package/docs/interfaces/BiquadFilterNode.html +19 -19
- package/docs/interfaces/BuiltEffectGraph.html +6 -6
- package/docs/interfaces/CacheErrorEvent.html +5 -5
- package/docs/interfaces/CacheHitEvent.html +4 -4
- package/docs/interfaces/CacheMissEvent.html +4 -4
- package/docs/interfaces/CacophonyEffect.html +2 -2
- package/docs/interfaces/CacophonyLogger.html +4 -4
- package/docs/interfaces/ChannelMergerNode.html +13 -13
- package/docs/interfaces/ChannelSplitterNode.html +13 -13
- package/docs/interfaces/ConvolverNode.html +15 -15
- package/docs/interfaces/CreateSpriteOptions.html +3 -3
- package/docs/interfaces/DynamicsOptions.html +7 -7
- package/docs/interfaces/FadeStartEvent.html +4 -4
- package/docs/interfaces/FdnReverbOptions.html +6 -6
- package/docs/interfaces/FoaDecoderOptions.html +2 -2
- package/docs/interfaces/FrequencyShifterOptions.html +3 -3
- package/docs/interfaces/GainNode.html +14 -14
- package/docs/interfaces/GlobalPlaybackEvent.html +3 -3
- package/docs/interfaces/HarmonizerOptions.html +6 -6
- package/docs/interfaces/ImpulseResponseOptions.html +5 -5
- package/docs/interfaces/LoadingCompleteEvent.html +5 -5
- package/docs/interfaces/LoadingErrorEvent.html +5 -5
- package/docs/interfaces/LoadingProgressEvent.html +6 -6
- package/docs/interfaces/LoadingStartEvent.html +3 -3
- package/docs/interfaces/LoudnessChannelInput.html +3 -3
- package/docs/interfaces/LoudnessReading.html +5 -5
- package/docs/interfaces/MediaElementSourceNode.html +14 -14
- package/docs/interfaces/MediaStreamAudioSourceNode.html +14 -14
- package/docs/interfaces/MediaStreamSoundOptions.html +4 -4
- package/docs/interfaces/MicrophoneStreamOptions.html +7 -7
- package/docs/interfaces/ModulatedDelayOptions.html +8 -8
- package/docs/interfaces/OfflineOptions.html +5 -5
- package/docs/interfaces/OscillatorNode.html +19 -19
- package/docs/interfaces/PannerNode.html +27 -27
- package/docs/interfaces/PcmStreamBufferEvent.html +2 -2
- package/docs/interfaces/PcmStreamSoundOptions.html +6 -6
- package/docs/interfaces/PhaserOptions.html +7 -7
- package/docs/interfaces/PlayOptions.html +13 -13
- package/docs/interfaces/PlaybackErrorEvent.html +5 -5
- package/docs/interfaces/ReverbOptions.html +13 -13
- package/docs/interfaces/RuntimeOptions.html +6 -6
- package/docs/interfaces/ScheduledCallbackHandle.html +2 -2
- package/docs/interfaces/SoundCleanupHoldings.html +4 -4
- package/docs/interfaces/SoundErrorEvent.html +6 -6
- package/docs/interfaces/SpectralFreezeOptions.html +4 -4
- package/docs/interfaces/SpriteRegion.html +4 -4
- package/docs/interfaces/StereoPannerNode.html +14 -14
- package/docs/interfaces/StereoToBFormatOptions.html +2 -2
- package/docs/interfaces/StereoWidenerOptions.html +4 -4
- package/docs/interfaces/StreamCapabilities.html +5 -5
- package/docs/interfaces/TimeStretchOptions.html +5 -5
- package/docs/interfaces/TremoloOptions.html +5 -5
- package/docs/interfaces/WaveshaperOptions.html +5 -5
- package/docs/types/BaseAudioEvents.html +11 -11
- package/docs/types/BuiltEffect.html +1 -1
- package/docs/types/BusConnectionTarget.html +1 -1
- package/docs/types/CacheEventCallback.html +1 -1
- package/docs/types/CacophonyEvents.html +17 -17
- package/docs/types/ErrorEventCallback.html +1 -1
- package/docs/types/FadeType.html +1 -1
- package/docs/types/HrtfPannerOptions.html +1 -1
- package/docs/types/ImpulseResponseSource.html +1 -1
- package/docs/types/LoadingEventCallback.html +1 -1
- package/docs/types/LoopCount.html +1 -1
- package/docs/types/LoudnessChannel.html +1 -1
- package/docs/types/Orientation.html +3 -3
- package/docs/types/PanCloneOverrides.html +5 -5
- package/docs/types/PanType.html +1 -1
- package/docs/types/PcmStreamEvents.html +6 -6
- package/docs/types/PcmStreamState.html +1 -1
- package/docs/types/PlaybackEvents.html +9 -2
- package/docs/types/Position.html +1 -1
- package/docs/types/SoundEvents.html +1 -1
- package/docs/types/SoundType.html +1 -1
- package/docs/types/SourceNode.html +1 -1
- package/docs/types/SpriteMap.html +1 -1
- package/docs/types/SynthEvents.html +1 -1
- package/docs/types/ThreeDOptions.html +1 -1
- package/docs/types/WebCodecsStreamSound.html +1 -1
- package/docs/variables/CHANNEL_WEIGHTS.html +1 -1
- package/docs/variables/K_WEIGHTING_STAGE1_48K.html +1 -1
- package/docs/variables/K_WEIGHTING_STAGE2_48K.html +1 -1
- package/docs/variables/consoleLogger.html +1 -1
- package/docs/variables/noopLogger.html +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1304,11 +1304,18 @@ synth.type = 'square'; // Triggers typeChange event
|
|
|
1304
1304
|
| `pause` | `void` | Fired when playback pauses. |
|
|
1305
1305
|
| `resume` | `void` | Fired when playback resumes after being paused. |
|
|
1306
1306
|
| `ended` | `void` | Fired when playback ends naturally. |
|
|
1307
|
-
| `seek` | `number` | Fired
|
|
1307
|
+
| `seek` | `number` | Fired once after a successful `Playback.seek(time)`, with the requested time in seconds. Rejected seeks do not emit. |
|
|
1308
1308
|
| `volumeChange` | `number` | Fired when playback volume changes. Receives new volume value. |
|
|
1309
1309
|
| `error` | `PlaybackErrorEvent` | Fired on playback errors. Contains `error`, `errorType`, `timestamp`, `recoverable`. |
|
|
1310
1310
|
|
|
1311
|
-
|
|
1311
|
+
`seek` is specific to `Playback` (buffer and media-element sources). Subscribe on
|
|
1312
|
+
each playback to observe seeks, including those requested through `Sound.seek()`.
|
|
1313
|
+
`Sound` does not relay a collective `seek`: its concurrent voices can have
|
|
1314
|
+
different positions, and a collective seek can fail after some voices have moved.
|
|
1315
|
+
For media elements, the event reports acceptance of the `currentTime` write;
|
|
1316
|
+
it does not wait for the browser's asynchronous `seeked` event or playback resumption.
|
|
1317
|
+
|
|
1318
|
+
#### Synth Events
|
|
1312
1319
|
|
|
1313
1320
|
| Event | Payload | Description |
|
|
1314
1321
|
|-------|---------|-------------|
|
package/dist/events.d.ts
CHANGED
package/dist/playback.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./worklets.cjs"),t=require("./basePlayback.cjs"),n=require("./playOptions.cjs");var r=class r extends t.BasePlayback{origin;context;loopCount=0;currentLoop=0;buffer;_offset=0;_startTime=0;_playbackRate=1;_mediaPlayPromise;_fadeInConfig;_fadeOutConfig;_loopEndCallback;_pitchFactor=1;constructor(e,t,n){super(e),this.origin=e,this.context=e.context,this.loopCount=e.loopCount,this.setPanType(e.panType,e.context),this.source=t,`buffer`in t&&t.buffer&&(this.buffer=t.buffer),this.setupSourceNode(t),this.setGainNode(n),this.setEffectChainEndpoints(this.source,this.panner),this.panner.connect(this.gainNode)}setupSourceNode(e){if(`mediaElement`in e&&e.mediaElement)e.mediaElement.onended=this.loopEnded;else if(`onended`in e)e.onended=this.loopEnded;else throw Error(`Unsupported source type`)}get duration(){if(this.origin.region)return this.origin.region.duration;if(!this.source)throw Error(`Cannot get duration of a sound that has been cleaned up`);return`mediaElement`in this.source&&this.source.mediaElement?this.source.mediaElement.duration:this.buffer&&this.buffer.duration||NaN}get playbackRate(){return this._playbackRate}get volume(){return super.volume}set volume(e){super.volume=e,this.emit(`volumeChange`,e)}set playbackRate(e){if(e<=0)throw Error(`Playback rate must be greater than 0`);if(this._state===`playing`){let e=Math.max(0,(this.context.currentTime-this._startTime)*this._playbackRate);this._offset=this.regionRelativeOffset(this._offset+e),this.context.currentTime>=this._startTime&&(this._startTime=this.context.currentTime)}this._playbackRate=e,this.source&&(`playbackRate`in this.source&&(this.source.playbackRate.value=e),`mediaElement`in this.source&&this.source.mediaElement&&(this.source.mediaElement.playbackRate=e))}loopEnded=()=>{if(!(!this.source||this._state!==`playing`)){if(`mediaElement`in this.source&&this._mediaPlayPromise){this._mediaPlayPromise.then(()=>this._runLoopEnded());return}this._runLoopEnded()}};_runLoopEnded(){!this.source||this._state!==`playing`||(this.currentLoop++,this.loopCount!==`infinite`&&this.currentLoop>this.loopCount?(this.emit(`ended`,void 0),this._fadeOutConfig?this.fadeOut(this._fadeOutConfig.duration,this._fadeOutConfig.type).then(()=>{this._state!==`stopped`&&this.source&&(this.stop(),this.removeFromOrigin())}):(this.stop(),this.removeFromOrigin())):(this._loopEndCallback?.(),this.seek(0),this._state!==`playing`&&this.source&&!(`mediaElement`in this.source)&&this.play(),this._fadeInConfig?.perLoop&&this.gainNode&&(this.gainNode.gain.setValueAtTime(1e-4,this.gainNode.context.currentTime),this.fadeTo(this._fadeInConfig.targetVolume,this._fadeInConfig.duration,this._fadeInConfig.type))))}play(e){if(!this.source)throw Error(`Cannot play a sound that has been cleaned up`);if(n.validatePlayOptions(e,this.panType,`mediaElement`in this.source?`media`:`buffer`),this._state===`playing`)return[this];n.applyPlayOptions(this,this.context,e),e?.playbackRate!==void 0&&(this.playbackRate=e.playbackRate),e?.loopCount!==void 0&&this.loop(e.loopCount),e?.fadeIn!==void 0&&this.fadeIn(e.fadeIn,e.fadeType,{perLoop:e.fadeInPerLoop,startTime:e.at}),e?.fadeOut!==void 0&&this.configureFadeOut(e.fadeOut,e.fadeType);let t=this._state===`paused`;this._state===`stopped`&&(this.currentLoop=0);let r=this.origin._playbackGeneration;try{let n;if(this._state===`paused`){if(`mediaElement`in this.source&&this.source.mediaElement)n=this.source.mediaElement.play();else if(this.recreateSource(),`start`in this.source&&!this.startBufferSource(e?.at??0))return[this]}else if(`mediaElement`in this.source&&this.source.mediaElement)this.source.mediaElement.currentTime=this._offset,n=this.source.mediaElement.play();else if(`start`in this.source&&(this.recreateSource(),!this.startBufferSource(e?.at??0)))return[this];if(n){let e=n.then(()=>{if(!(this._mediaPlayPromise!==e||!this.source)){if(this.origin._playbackGeneration!==r){`mediaElement`in this.source&&this.source.mediaElement.pause();return}this._startTime=this.context.currentTime,this.emitPlayStarted(t)}},t=>{this._mediaPlayPromise!==e||this.origin._playbackGeneration!==r||this.emitAsync(`error`,{error:t,errorType:`source`,timestamp:Date.now(),recoverable:!0})}).finally(()=>{this._mediaPlayPromise===e&&(this._mediaPlayPromise=void 0)});this._mediaPlayPromise=e}else this._startTime=Math.max(e?.at??this.context.currentTime,this.context.currentTime),this.emitPlayStarted(t);return[this]}catch(e){throw this.emitAsync(`error`,{error:e,errorType:`source`,timestamp:Date.now(),recoverable:!0}),e}}emitPlayStarted(e){this._state===`stopped`&&!this.origin.playbacks.includes(this)&&this.origin._readmitPlayback(this),super.emitPlayStarted(e)}pause(){if(!this.source||this._state!==`playing`)return;let e=Math.max(0,(this.context.currentTime-this._startTime)*this._playbackRate);this._offset=this.regionRelativeOffset(this._offset+e),`mediaElement`in this.source&&this.source.mediaElement?this.source.mediaElement.pause():`stop`in this.source&&this.source.stop(),this.emitPaused()}stop(){if(!this.source)throw Error(`Cannot stop a sound that has been cleaned up`);let e=this._mediaPlayPromise!==void 0;this._mediaPlayPromise=void 0,!(!e&&(this._state===`stopped`||this._state===`unplayed`))&&(this.cancelFade(),`stop`in this.source&&this._state===`playing`&&this.source.stop(),`mediaElement`in this.source&&this.source.mediaElement&&(this.source.mediaElement.pause(),this.source.mediaElement.currentTime=0),this._offset=0,this._startTime=0,this.emitStopped())}fadeIn(e,t,n){return n?.perLoop&&(this._fadeInConfig={duration:e,type:t??`linear`,perLoop:!0,targetVolume:this.gainNode.gain.value}),super.fadeIn(e,t,{startTime:n?.startTime})}configureFadeOut(e,t){this._fadeOutConfig={duration:e,type:t??`linear`}}stopWithFade(e,t){return this.fadeOut(e,t).then(()=>{this._state!==`stopped`&&this.source&&this.stop()})}seek(e){if(!this.source||!this.gainNode||!this.panner)throw Error(`Cannot seek a sound that has been cleaned up`);if(!Number.isFinite(e)||e<0||this.origin.region!==void 0&&e>this.duration)throw Error(`Invalid time value for seek`);if(this.origin.soundType===`streaming`&&`mediaElement`in this.source&&(this.source.mediaElement.seekable?.length??0)===0)throw this.source.mediaElement.duration===1/0?Error(`Live media-element streams do not expose a seekable range`):Error(`Media-element stream does not currently expose a seekable range`);let t=this._state===`playing`;t&&this.pause(),this._offset=e,`mediaElement`in this.source&&this.source.mediaElement&&(this.source.mediaElement.currentTime=e),t&&this.play()}get currentTime(){if(this._state===`playing`){let e=Math.max(0,(this.context.currentTime-this._startTime)*this._playbackRate),t=this._offset+e;return this.regionRelativeOffset(t)}else return this._offset}removeFromOrigin(){let e=this.origin.playbacks.indexOf(this);e!==-1&&this.origin.playbacks.splice(e,1)}recreateSource(){if(!this.buffer||!this.panner||!this.context||!this.gainNode)throw Error(`Cannot recreate source of a sound that has been cleaned up`);try{this.source&&(!(`mediaElement`in this.source)&&`onended`in this.source&&(this.source.onended=null),this.source.disconnect()),this.source=this.context.createBufferSource(),this.source.buffer=this.buffer,this.setEffectChainEndpoints(this.source,this.panner),this.source.onended=this.loopEnded,this.playbackRate=this._playbackRate,this.configureBufferLoopSource()}catch(e){throw this.emitAsync(`error`,{error:e,errorType:`source`,timestamp:Date.now(),recoverable:!1}),e}}set sourceLoop(e){if(!this.source)throw Error(`Cannot set loop on a sound that has been cleaned up`);`loop`in this.source&&(this.source.loop=e),`mediaElement`in this.source&&this.source.mediaElement&&(this.source.mediaElement.loop=e)}cleanup(){if(this._mediaPlayPromise=void 0,this.source){if(`mediaElement`in this.source&&this.source.mediaElement){let e=this.source.mediaElement;e.pause();try{e.currentTime=0}catch{}e.loop=!1,e.onended=null,e.src=``,e.load()}else `onended`in this.source&&(this.source.onended=null);this._offset=0,this._startTime=0,this.markStopped(),this.currentLoop=0,this.source.disconnect(),this.source=void 0,super.cleanup()}}assertNotCleanedUp(){if(!this.source||!this.gainNode||!this.panner)throw Error(`Cannot perform operation on a sound that has been cleaned up`)}addFilter(e){this.assertNotCleanedUp(),super.addFilter(e)}removeFilter(e){this.assertNotCleanedUp(),super.removeFilter(e)}loop(e){if(!this.source)throw Error(`Cannot loop a sound that has been cleaned up`);if(e!==void 0){let t=this.loopCount,n=e===`infinite`?`infinite`:Math.max(0,e);if(t===`infinite`!=(n===`infinite`)&&this.origin.region&&this._state===`playing`&&`loop`in this.source&&`stop`in this.source){let e=Math.max(this._startTime,this.context.currentTime),r=Math.max(0,(this.context.currentTime-this._startTime)*this._playbackRate);return this._offset=this.regionRelativeOffset(this.regionRelativeOffset(this._offset+r,t),n),`onended`in this.source&&(this.source.onended=null),this.source.stop(),this.loopCount=n,this.currentLoop=0,this._startTime=e,this.recreateSource(),this.startBufferSource(e),this.loopCount}this.loopCount=n,this.currentLoop=0}if(`mediaElement`in this.source&&this.source.mediaElement){let e=this.source.mediaElement;e.loop=this.loopCount===`infinite`}else if(`loop`in this.source)this.configureBufferLoopSource();else throw Error(`Unsupported source type`);return this.loopCount}configureBufferLoopSource(){!this.source||!(`loop`in this.source)||(this.source.loop=this.loopCount===`infinite`,this.source.buffer&&(this.source.loopEnd=this.origin.region?this.origin.region.start+this.origin.region.duration:this.source.buffer.duration,this.source.loopStart=this.origin.region?.start??0))}regionRelativeOffset(e,t=this.loopCount){let n=this.origin.region;return n?t===`infinite`?e%n.duration:Math.min(n.duration,e):e}startBufferSource(e){if(!this.source||!(`start`in this.source))return!1;let t=this.origin.region;if(!t)return this.source.start(e,this._offset),!0;if(this._offset===t.duration)return!1;let n=t.start+this._offset;return this.loopCount===`infinite`?this.source.start(e,n):this.source.start(e,n,t.duration-this._offset),!0}async setPitchShift(t){if(this.assertNotCleanedUp(),!Number.isFinite(t)||t<=0)throw Error(`Pitch-shift factor must be greater than 0`);this._pitchFactor=t;let n=this._effectChain;if(!n)throw Error(`Cannot pitch-shift a playback before its effect chain is initialized`);let r=n.nodes.find(e=>{let t=e.parameters;return t&&typeof t.get==`function`&&t.get(`pitchFactor`)!==void 0});if(t===1){r&&n.remove(r);return}if(!r){let t=this.origin.cacophony;if(!t)throw Error(`Cannot pitch-shift a playback whose Sound has no Cacophony instance`);r=await t.buildWorkletEffect(e.WORKLETS.phaseVocoder,{},this.context),n.add(r)}let i=r.parameters?.get(`pitchFactor`);i&&(i.value=t)}get pitchShift(){return this._pitchFactor}get outputNode(){if(!this.gainNode)throw Error(`Cannot access output node of a playback that has been cleaned up`);return this.gainNode}connect(e){return this.outputNode.connect(e)}disconnect(e){e?this.outputNode.disconnect(e):this.outputNode.disconnect()}clone(e={}){if(!this.source||!this.gainNode||!this.context)throw Error(`Cannot clone a sound that has been cleaned up`);let t=e.panType??this.panType,n=this.context.createGain();n.connect(this.origin._resolveRouteTargetNode());let i;if(`buffer`in this.source&&this.source.buffer)i=this.context.createBufferSource(),i.buffer=this.source.buffer;else if(`mediaElement`in this.source&&this.source.mediaElement){if(!this.context.createMediaElementSource)throw Error(`Media element sources are not supported on this audio context.`);let e=this.source.mediaElement.cloneNode(!0);i=this.context.createMediaElementSource(e)}else throw Error(`Unsupported source type`);let a=e.loopCount===void 0?this.loopCount:e.loopCount,o=new r(this.origin,i,n);return o.loopCount=a,o.currentLoop=this.currentLoop,o.setPanType(t,this.context),o.volume=this.volume,o.playbackRate=this._playbackRate,o._offset=this._offset,this._fadeInConfig&&(o._fadeInConfig={...this._fadeInConfig}),this._fadeOutConfig&&(o._fadeOutConfig={...this._fadeOutConfig}),this._state===`paused`?o.markPaused():this._state===`stopped`&&o.markStopped(),this._filters.forEach(e=>{let t=this.context.createBiquadFilter();t.type=e.type,t.frequency.value=e.frequency.value,t.Q.value=e.Q.value,t.gain.value=e.gain.value,o.addFilter(t)}),this._pitchFactor!==1&&o.setPitchShift(this._pitchFactor).catch(()=>{}),this._state===`playing`&&o.play(),this.origin.playbacks.push(o),o}};exports.Playback=r;
|
|
1
|
+
const e=require("./worklets.cjs"),t=require("./basePlayback.cjs"),n=require("./playOptions.cjs");var r=class r extends t.BasePlayback{origin;context;loopCount=0;currentLoop=0;buffer;_offset=0;_startTime=0;_playbackRate=1;_mediaPlayPromise;_fadeInConfig;_fadeOutConfig;_loopEndCallback;_pitchFactor=1;constructor(e,t,n){super(e),this.origin=e,this.context=e.context,this.loopCount=e.loopCount,this.setPanType(e.panType,e.context),this.source=t,`buffer`in t&&t.buffer&&(this.buffer=t.buffer),this.setupSourceNode(t),this.setGainNode(n),this.setEffectChainEndpoints(this.source,this.panner),this.panner.connect(this.gainNode)}setupSourceNode(e){if(`mediaElement`in e&&e.mediaElement)e.mediaElement.onended=this.loopEnded;else if(`onended`in e)e.onended=this.loopEnded;else throw Error(`Unsupported source type`)}get duration(){if(this.origin.region)return this.origin.region.duration;if(!this.source)throw Error(`Cannot get duration of a sound that has been cleaned up`);return`mediaElement`in this.source&&this.source.mediaElement?this.source.mediaElement.duration:this.buffer&&this.buffer.duration||NaN}get playbackRate(){return this._playbackRate}get volume(){return super.volume}set volume(e){super.volume=e,this.emit(`volumeChange`,e)}set playbackRate(e){if(e<=0)throw Error(`Playback rate must be greater than 0`);if(this._state===`playing`){let e=Math.max(0,(this.context.currentTime-this._startTime)*this._playbackRate);this._offset=this.regionRelativeOffset(this._offset+e),this.context.currentTime>=this._startTime&&(this._startTime=this.context.currentTime)}this._playbackRate=e,this.source&&(`playbackRate`in this.source&&(this.source.playbackRate.value=e),`mediaElement`in this.source&&this.source.mediaElement&&(this.source.mediaElement.playbackRate=e))}loopEnded=()=>{if(!(!this.source||this._state!==`playing`)){if(`mediaElement`in this.source&&this._mediaPlayPromise){this._mediaPlayPromise.then(()=>this._runLoopEnded());return}this._runLoopEnded()}};_runLoopEnded(){!this.source||this._state!==`playing`||(this.currentLoop++,this.loopCount!==`infinite`&&this.currentLoop>this.loopCount?(this.emit(`ended`,void 0),this._fadeOutConfig?this.fadeOut(this._fadeOutConfig.duration,this._fadeOutConfig.type).then(()=>{this._state!==`stopped`&&this.source&&(this.stop(),this.removeFromOrigin())}):(this.stop(),this.removeFromOrigin())):(this._loopEndCallback?.(),this.seek(0),this._state!==`playing`&&this.source&&!(`mediaElement`in this.source)&&this.play(),this._fadeInConfig?.perLoop&&this.gainNode&&(this.gainNode.gain.setValueAtTime(1e-4,this.gainNode.context.currentTime),this.fadeTo(this._fadeInConfig.targetVolume,this._fadeInConfig.duration,this._fadeInConfig.type))))}play(e){if(!this.source)throw Error(`Cannot play a sound that has been cleaned up`);if(n.validatePlayOptions(e,this.panType,`mediaElement`in this.source?`media`:`buffer`),this._state===`playing`)return[this];n.applyPlayOptions(this,this.context,e),e?.playbackRate!==void 0&&(this.playbackRate=e.playbackRate),e?.loopCount!==void 0&&this.loop(e.loopCount),e?.fadeIn!==void 0&&this.fadeIn(e.fadeIn,e.fadeType,{perLoop:e.fadeInPerLoop,startTime:e.at}),e?.fadeOut!==void 0&&this.configureFadeOut(e.fadeOut,e.fadeType);let t=this._state===`paused`;this._state===`stopped`&&(this.currentLoop=0);let r=this.origin._playbackGeneration;try{let n;if(this._state===`paused`){if(`mediaElement`in this.source&&this.source.mediaElement)n=this.source.mediaElement.play();else if(this.recreateSource(),`start`in this.source&&!this.startBufferSource(e?.at??0))return[this]}else if(`mediaElement`in this.source&&this.source.mediaElement)this.source.mediaElement.currentTime=this._offset,n=this.source.mediaElement.play();else if(`start`in this.source&&(this.recreateSource(),!this.startBufferSource(e?.at??0)))return[this];if(n){let e=n.then(()=>{if(!(this._mediaPlayPromise!==e||!this.source)){if(this.origin._playbackGeneration!==r){`mediaElement`in this.source&&this.source.mediaElement.pause();return}this._startTime=this.context.currentTime,this.emitPlayStarted(t)}},t=>{this._mediaPlayPromise!==e||this.origin._playbackGeneration!==r||this.emitAsync(`error`,{error:t,errorType:`source`,timestamp:Date.now(),recoverable:!0})}).finally(()=>{this._mediaPlayPromise===e&&(this._mediaPlayPromise=void 0)});this._mediaPlayPromise=e}else this._startTime=Math.max(e?.at??this.context.currentTime,this.context.currentTime),this.emitPlayStarted(t);return[this]}catch(e){throw this.emitAsync(`error`,{error:e,errorType:`source`,timestamp:Date.now(),recoverable:!0}),e}}emitPlayStarted(e){this._state===`stopped`&&!this.origin.playbacks.includes(this)&&this.origin._readmitPlayback(this),super.emitPlayStarted(e)}pause(){if(!this.source||this._state!==`playing`)return;let e=Math.max(0,(this.context.currentTime-this._startTime)*this._playbackRate);this._offset=this.regionRelativeOffset(this._offset+e),`mediaElement`in this.source&&this.source.mediaElement?this.source.mediaElement.pause():`stop`in this.source&&this.source.stop(),this.emitPaused()}stop(){if(!this.source)throw Error(`Cannot stop a sound that has been cleaned up`);let e=this._mediaPlayPromise!==void 0;this._mediaPlayPromise=void 0,!(!e&&(this._state===`stopped`||this._state===`unplayed`))&&(this.cancelFade(),`stop`in this.source&&this._state===`playing`&&this.source.stop(),`mediaElement`in this.source&&this.source.mediaElement&&(this.source.mediaElement.pause(),this.source.mediaElement.currentTime=0),this._offset=0,this._startTime=0,this.emitStopped())}fadeIn(e,t,n){return n?.perLoop&&(this._fadeInConfig={duration:e,type:t??`linear`,perLoop:!0,targetVolume:this.gainNode.gain.value}),super.fadeIn(e,t,{startTime:n?.startTime})}configureFadeOut(e,t){this._fadeOutConfig={duration:e,type:t??`linear`}}stopWithFade(e,t){return this.fadeOut(e,t).then(()=>{this._state!==`stopped`&&this.source&&this.stop()})}seek(e){if(!this.source||!this.gainNode||!this.panner)throw Error(`Cannot seek a sound that has been cleaned up`);if(!Number.isFinite(e)||e<0||this.origin.region!==void 0&&e>this.duration)throw Error(`Invalid time value for seek`);if(this.origin.soundType===`streaming`&&`mediaElement`in this.source&&(this.source.mediaElement.seekable?.length??0)===0)throw this.source.mediaElement.duration===1/0?Error(`Live media-element streams do not expose a seekable range`):Error(`Media-element stream does not currently expose a seekable range`);let t=this._state===`playing`;t&&this.pause(),this._offset=e,`mediaElement`in this.source&&this.source.mediaElement&&(this.source.mediaElement.currentTime=e),t&&this.play(),this.emit(`seek`,e)}get currentTime(){if(this._state===`playing`){let e=Math.max(0,(this.context.currentTime-this._startTime)*this._playbackRate),t=this._offset+e;return this.regionRelativeOffset(t)}else return this._offset}removeFromOrigin(){let e=this.origin.playbacks.indexOf(this);e!==-1&&this.origin.playbacks.splice(e,1)}recreateSource(){if(!this.buffer||!this.panner||!this.context||!this.gainNode)throw Error(`Cannot recreate source of a sound that has been cleaned up`);try{this.source&&(!(`mediaElement`in this.source)&&`onended`in this.source&&(this.source.onended=null),this.source.disconnect()),this.source=this.context.createBufferSource(),this.source.buffer=this.buffer,this.setEffectChainEndpoints(this.source,this.panner),this.source.onended=this.loopEnded,this.playbackRate=this._playbackRate,this.configureBufferLoopSource()}catch(e){throw this.emitAsync(`error`,{error:e,errorType:`source`,timestamp:Date.now(),recoverable:!1}),e}}set sourceLoop(e){if(!this.source)throw Error(`Cannot set loop on a sound that has been cleaned up`);`loop`in this.source&&(this.source.loop=e),`mediaElement`in this.source&&this.source.mediaElement&&(this.source.mediaElement.loop=e)}cleanup(){if(this._mediaPlayPromise=void 0,this.source){if(`mediaElement`in this.source&&this.source.mediaElement){let e=this.source.mediaElement;e.pause();try{e.currentTime=0}catch{}e.loop=!1,e.onended=null,e.src=``,e.load()}else `onended`in this.source&&(this.source.onended=null);this._offset=0,this._startTime=0,this.markStopped(),this.currentLoop=0,this.source.disconnect(),this.source=void 0,super.cleanup()}}assertNotCleanedUp(){if(!this.source||!this.gainNode||!this.panner)throw Error(`Cannot perform operation on a sound that has been cleaned up`)}addFilter(e){this.assertNotCleanedUp(),super.addFilter(e)}removeFilter(e){this.assertNotCleanedUp(),super.removeFilter(e)}loop(e){if(!this.source)throw Error(`Cannot loop a sound that has been cleaned up`);if(e!==void 0){let t=this.loopCount,n=e===`infinite`?`infinite`:Math.max(0,e);if(t===`infinite`!=(n===`infinite`)&&this.origin.region&&this._state===`playing`&&`loop`in this.source&&`stop`in this.source){let e=Math.max(this._startTime,this.context.currentTime),r=Math.max(0,(this.context.currentTime-this._startTime)*this._playbackRate);return this._offset=this.regionRelativeOffset(this.regionRelativeOffset(this._offset+r,t),n),`onended`in this.source&&(this.source.onended=null),this.source.stop(),this.loopCount=n,this.currentLoop=0,this._startTime=e,this.recreateSource(),this.startBufferSource(e),this.loopCount}this.loopCount=n,this.currentLoop=0}if(`mediaElement`in this.source&&this.source.mediaElement){let e=this.source.mediaElement;e.loop=this.loopCount===`infinite`}else if(`loop`in this.source)this.configureBufferLoopSource();else throw Error(`Unsupported source type`);return this.loopCount}configureBufferLoopSource(){!this.source||!(`loop`in this.source)||(this.source.loop=this.loopCount===`infinite`,this.source.buffer&&(this.source.loopEnd=this.origin.region?this.origin.region.start+this.origin.region.duration:this.source.buffer.duration,this.source.loopStart=this.origin.region?.start??0))}regionRelativeOffset(e,t=this.loopCount){let n=this.origin.region;return n?t===`infinite`?e%n.duration:Math.min(n.duration,e):e}startBufferSource(e){if(!this.source||!(`start`in this.source))return!1;let t=this.origin.region;if(!t)return this.source.start(e,this._offset),!0;if(this._offset===t.duration)return!1;let n=t.start+this._offset;return this.loopCount===`infinite`?this.source.start(e,n):this.source.start(e,n,t.duration-this._offset),!0}async setPitchShift(t){if(this.assertNotCleanedUp(),!Number.isFinite(t)||t<=0)throw Error(`Pitch-shift factor must be greater than 0`);this._pitchFactor=t;let n=this._effectChain;if(!n)throw Error(`Cannot pitch-shift a playback before its effect chain is initialized`);let r=n.nodes.find(e=>{let t=e.parameters;return t&&typeof t.get==`function`&&t.get(`pitchFactor`)!==void 0});if(t===1){r&&n.remove(r);return}if(!r){let t=this.origin.cacophony;if(!t)throw Error(`Cannot pitch-shift a playback whose Sound has no Cacophony instance`);r=await t.buildWorkletEffect(e.WORKLETS.phaseVocoder,{},this.context),n.add(r)}let i=r.parameters?.get(`pitchFactor`);i&&(i.value=t)}get pitchShift(){return this._pitchFactor}get outputNode(){if(!this.gainNode)throw Error(`Cannot access output node of a playback that has been cleaned up`);return this.gainNode}connect(e){return this.outputNode.connect(e)}disconnect(e){e?this.outputNode.disconnect(e):this.outputNode.disconnect()}clone(e={}){if(!this.source||!this.gainNode||!this.context)throw Error(`Cannot clone a sound that has been cleaned up`);let t=e.panType??this.panType,n=this.context.createGain();n.connect(this.origin._resolveRouteTargetNode());let i;if(`buffer`in this.source&&this.source.buffer)i=this.context.createBufferSource(),i.buffer=this.source.buffer;else if(`mediaElement`in this.source&&this.source.mediaElement){if(!this.context.createMediaElementSource)throw Error(`Media element sources are not supported on this audio context.`);let e=this.source.mediaElement.cloneNode(!0);i=this.context.createMediaElementSource(e)}else throw Error(`Unsupported source type`);let a=e.loopCount===void 0?this.loopCount:e.loopCount,o=new r(this.origin,i,n);return o.loopCount=a,o.currentLoop=this.currentLoop,o.setPanType(t,this.context),o.volume=this.volume,o.playbackRate=this._playbackRate,o._offset=this._offset,this._fadeInConfig&&(o._fadeInConfig={...this._fadeInConfig}),this._fadeOutConfig&&(o._fadeOutConfig={...this._fadeOutConfig}),this._state===`paused`?o.markPaused():this._state===`stopped`&&o.markStopped(),this._filters.forEach(e=>{let t=this.context.createBiquadFilter();t.type=e.type,t.frequency.value=e.frequency.value,t.Q.value=e.Q.value,t.gain.value=e.gain.value,o.addFilter(t)}),this._pitchFactor!==1&&o.setPitchShift(this._pitchFactor).catch(()=>{}),this._state===`playing`&&o.play(),this.origin.playbacks.push(o),o}};exports.Playback=r;
|
|
2
2
|
//# sourceMappingURL=playback.cjs.map
|
package/dist/playback.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"playback.cjs","names":[],"sources":["../src/playback.ts"],"sourcesContent":["/**\n * The Playback class encapsulates the functionality for playing audio in a web application.\n * It integrates with the repo's audio-context abstractions so playback works across native and mocked contexts.\n * This class allows for the manipulation of audio playback through various features such as:\n * - Playing and stopping audio\n * - Looping audio a specific number of times or infinitely\n * - Adjusting volume and playback rate\n * - Applying stereo or 3D (HRTF) panning\n * - Adding and removing filters to modify the audio output\n * - Handling audio looping with custom logic\n * - Fading audio in and out linearly or exponentially\n * - Seeking to specific points in the audio\n * - Checking if the audio is currently playing\n * - Cleaning up resources when the audio is no longer needed\n *\n * The class is designed to be flexible and can be used with different types of audio sources,\n * including buffer sources and media elements. It also provides detailed control over the audio's\n * spatial characteristics when using 3D audio.\n */\n\nimport { BasePlayback } from \"./basePlayback\";\nimport type { BaseSound, FadeType, LoopCount, PanType, PlayOptions } from \"./cacophony\";\nimport type {\n AudioBuffer,\n AudioNode,\n AudioParam,\n AudioWorkletNode,\n BaseContext,\n BiquadFilterNode,\n GainNode,\n SourceNode,\n} from \"./context\";\nimport { applyPlayOptions, validatePlayOptions } from \"./playOptions\";\nimport type { Sound } from \"./sound\";\nimport { WORKLETS } from \"./worklets\";\n\ntype PlaybackCloneOverrides = {\n loopCount: LoopCount;\n panType: PanType;\n};\n\nexport class Playback extends BasePlayback implements BaseSound {\n private context: BaseContext;\n public declare source?: SourceNode;\n loopCount: LoopCount = 0;\n currentLoop: number = 0;\n private buffer?: AudioBuffer;\n private _offset: number = 0;\n private _startTime: number = 0;\n private _playbackRate: number = 1;\n /**\n * Promise that tracks an in-flight media-element `.play()` settlement.\n * Set when the media element's `play()` returns a pending promise; cleared\n * once that promise settles. Used by `loopEnded` to avoid racing source-state\n * mutations (`seek`/`play`) against the deferred state transition driven by\n * this promise.\n */\n private _mediaPlayPromise?: Promise<void>;\n _fadeInConfig?: { duration: number; type: FadeType; perLoop: boolean; targetVolume: number };\n _fadeOutConfig?: { duration: number; type: FadeType };\n _loopEndCallback?: () => void;\n /**\n * Desired pitch-shift factor (1 = no shift, 2 = +1 octave, 0.5 = -1 octave).\n * Stored even before the worklet node exists so a value set on a cleaned-up /\n * not-yet-built playback is honoured once {@link setPitchShift} builds the node.\n */\n private _pitchFactor: number = 1;\n /**\n * Creates an instance of the Playback class.\n * @throws {Error} Throws an error if an invalid pan type is provided.\n */\n\n constructor(\n public origin: Sound,\n source: SourceNode,\n gainNode: GainNode,\n ) {\n super(origin);\n this.context = origin.context;\n this.loopCount = origin.loopCount;\n this.setPanType(origin.panType, origin.context);\n this.source = source;\n if (\"buffer\" in source && source.buffer) {\n this.buffer = source.buffer;\n }\n this.setupSourceNode(source);\n this.setGainNode(gainNode);\n this.setEffectChainEndpoints(this.source, this.panner!);\n this.panner!.connect(this.gainNode!);\n }\n\n private setupSourceNode(source: SourceNode) {\n if (\"mediaElement\" in source && source.mediaElement) {\n source.mediaElement.onended = this.loopEnded;\n } else if (\"onended\" in source) {\n source.onended = this.loopEnded;\n } else {\n throw new Error(\"Unsupported source type\");\n }\n }\n\n /**\n * Gets the duration of the audio in seconds.\n * @returns {number} The duration of the audio or NaN if the duration is unknown.\n * @throws {Error} Throws an error if the sound has been cleaned up.\n */\n\n get duration() {\n if (this.origin.region) {\n return this.origin.region.duration;\n }\n if (!this.source) {\n throw new Error(\"Cannot get duration of a sound that has been cleaned up\");\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n return this.source.mediaElement.duration;\n }\n if (!this.buffer) {\n return NaN;\n }\n return this.buffer.duration || NaN;\n }\n\n /**\n * Gets the current playback rate of the audio.\n */\n\n get playbackRate() {\n return this._playbackRate;\n }\n\n get volume(): number {\n return super.volume;\n }\n\n set volume(volume: number) {\n super.volume = volume;\n this.emit(\"volumeChange\", volume);\n }\n\n /**\n * Sets the playback rate of the audio.\n * @throws {Error} Throws an error if the sound has been cleaned up or if the source type is unsupported.\n */\n\n set playbackRate(rate: number) {\n if (rate <= 0) {\n throw new Error(\"Playback rate must be greater than 0\");\n }\n if (this._state === \"playing\") {\n const elapsed = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);\n this._offset = this.regionRelativeOffset(this._offset + elapsed);\n if (this.context.currentTime >= this._startTime) {\n this._startTime = this.context.currentTime;\n }\n }\n this._playbackRate = rate;\n if (!this.source) {\n return;\n }\n if (\"playbackRate\" in this.source) {\n this.source.playbackRate.value = rate;\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.playbackRate = rate;\n }\n }\n\n /**\n * Handles the loop event when the audio ends.\n * This method is bound to the 'onended' event of the audio source.\n * It manages looping logic and restarts playback if necessary.\n */\n loopEnded = (): void => {\n if (!this.source || this._state !== \"playing\") {\n return;\n }\n\n // For media-element sources, a previous play() may still have a pending\n // settlement promise -- the deferred state transition races with the\n // seek+play we are about to perform. Defer the loop work until the\n // pending play resolves so source mutations don't interleave.\n if (\"mediaElement\" in this.source && this._mediaPlayPromise) {\n void this._mediaPlayPromise.then(() => this._runLoopEnded());\n return;\n }\n\n this._runLoopEnded();\n };\n\n private _runLoopEnded(): void {\n if (!this.source || this._state !== \"playing\") {\n return;\n }\n\n this.currentLoop++;\n\n if (this.loopCount !== \"infinite\" && this.currentLoop > this.loopCount) {\n // Final iteration -- emit ended, then either fade out or stop immediately\n this.emit(\"ended\", undefined);\n if (this._fadeOutConfig) {\n // Guard the chained stop: an external stop()/cleanup() between the\n // fade resolving and this .then would otherwise run stop()/source\n // access against torn-down state. cancelFade() in stop()/cleanup()\n // resolves the fade promise synchronously, which is what makes this\n // .then fire at all after a teardown.\n void this.fadeOut(this._fadeOutConfig.duration, this._fadeOutConfig.type).then(() => {\n if (this._state !== \"stopped\" && this.source) {\n this.stop();\n this.removeFromOrigin();\n }\n });\n } else {\n this.stop();\n this.removeFromOrigin();\n }\n } else {\n this._loopEndCallback?.();\n this.seek(0); // Resets offset and handles play/pause state internally.\n // seek() calls pause() then play() when wasPlaying is true,\n // so play() handles both media-element and buffer sources correctly.\n if (this._state !== \"playing\" && this.source && !(\"mediaElement\" in this.source)) {\n // For AudioBufferSourceNode only: if seek() didn't restart playback\n // (e.g., state wasn't Playing before seek), start it now.\n this.play();\n }\n\n // Re-trigger fade-in if perLoop config is set\n if (this._fadeInConfig?.perLoop && this.gainNode) {\n this.gainNode.gain.setValueAtTime(0.0001, this.gainNode.context.currentTime);\n this.fadeTo(this._fadeInConfig.targetVolume, this._fadeInConfig.duration, this._fadeInConfig.type);\n }\n }\n }\n\n /**\n * Starts playing the audio.\n * @returns {[this]} Returns the instance of the Playback class for chaining.\n * @throws {Error} Throws an error if the sound has been cleaned up.\n */\n\n play(options?: PlayOptions): [this] {\n if (!this.source) {\n throw new Error(\"Cannot play a sound that has been cleaned up\");\n }\n\n validatePlayOptions(options, this.panType, \"mediaElement\" in this.source ? \"media\" : \"buffer\");\n\n if (this._state === \"playing\") {\n return [this];\n }\n\n applyPlayOptions(this, this.context, options);\n if (options?.playbackRate !== undefined) this.playbackRate = options.playbackRate;\n if (options?.loopCount !== undefined) this.loop(options.loopCount);\n if (options?.fadeIn !== undefined) {\n void this.fadeIn(options.fadeIn, options.fadeType, { perLoop: options.fadeInPerLoop, startTime: options.at });\n }\n if (options?.fadeOut !== undefined) this.configureFadeOut(options.fadeOut, options.fadeType);\n\n const isResume = this._state === \"paused\";\n if (this._state === \"stopped\") this.currentLoop = 0;\n const originGeneration = this.origin._playbackGeneration;\n\n try {\n let mediaPlayPromise: Promise<void> | undefined;\n\n if (this._state === \"paused\") {\n // If we're resuming from a paused state\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n mediaPlayPromise = this.source.mediaElement.play();\n } else {\n // For non-mediaElement sources, we need to recreate and start the source\n this.recreateSource();\n if (\"start\" in this.source) {\n if (!this.startBufferSource(options?.at ?? 0)) {\n return [this];\n }\n }\n }\n } else {\n // If we're starting from the beginning or a stopped state\n\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.currentTime = this._offset;\n mediaPlayPromise = this.source.mediaElement.play();\n } else if (\"start\" in this.source) {\n this.recreateSource();\n if (!this.startBufferSource(options?.at ?? 0)) {\n return [this];\n }\n }\n }\n\n if (mediaPlayPromise) {\n // For media-element sources, defer state and events until browser accepts playback\n // Stash the pending promise so loopEnded (and any future caller that\n // needs to sequence after the deferred state transition) can await it\n // rather than racing source-state mutations against it.\n const tracked = mediaPlayPromise\n .then(\n () => {\n if (this._mediaPlayPromise !== tracked || !this.source) return;\n if (this.origin._playbackGeneration !== originGeneration) {\n if (\"mediaElement\" in this.source) this.source.mediaElement.pause();\n return;\n }\n this._startTime = this.context.currentTime;\n this.emitPlayStarted(isResume);\n },\n (error: Error) => {\n if (this._mediaPlayPromise !== tracked || this.origin._playbackGeneration !== originGeneration) return;\n void this.emitAsync(\"error\", {\n error,\n errorType: \"source\",\n timestamp: Date.now(),\n recoverable: true,\n });\n },\n )\n .finally(() => {\n if (this._mediaPlayPromise === tracked) {\n this._mediaPlayPromise = undefined;\n }\n });\n this._mediaPlayPromise = tracked;\n } else {\n // For buffer sources, state transition is immediate (start() is synchronous)\n this._startTime = Math.max(options?.at ?? this.context.currentTime, this.context.currentTime);\n this.emitPlayStarted(isResume);\n }\n\n return [this];\n } catch (error) {\n this.emitAsync(\"error\", {\n error: error as Error,\n errorType: \"source\",\n timestamp: Date.now(),\n recoverable: true,\n });\n throw error;\n }\n }\n\n protected override emitPlayStarted(isResume: boolean): void {\n // A stopped voice may have been reaped by Sound.preplay() or natural end.\n // Restore ownership only after its source has successfully started.\n if (this._state === \"stopped\" && !this.origin.playbacks.includes(this)) {\n this.origin._readmitPlayback(this);\n }\n super.emitPlayStarted(isResume);\n }\n\n pause(): void {\n if (!this.source || this._state !== \"playing\") {\n return;\n }\n\n const elapsed = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);\n this._offset = this.regionRelativeOffset(this._offset + elapsed);\n\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.pause();\n } else if (\"stop\" in this.source) {\n // For AudioBufferSourceNode and OscillatorNode, stop the source.\n // It cannot be restarted; a new one will be created on play().\n this.source.stop();\n }\n\n this.emitPaused();\n }\n\n stop(): void {\n if (!this.source) {\n throw new Error(\"Cannot stop a sound that has been cleaned up\");\n }\n const pendingMediaPlay = this._mediaPlayPromise !== undefined;\n this._mediaPlayPromise = undefined;\n if (!pendingMediaPlay && (this._state === \"stopped\" || this._state === \"unplayed\")) {\n return;\n }\n\n this.cancelFade();\n\n if (\"stop\" in this.source && this._state === \"playing\") {\n this.source.stop();\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.pause();\n this.source.mediaElement.currentTime = 0;\n }\n\n this._offset = 0;\n this._startTime = 0;\n this.emitStopped();\n }\n\n /**\n * Fades in from silence to the current volume.\n * Optionally stores config to re-trigger the fade on each loop iteration.\n * @param {number} duration - The fade duration in milliseconds.\n * @param {FadeType} type - The fade curve type. Defaults to \"linear\".\n * @param {object} options - Optional. Set perLoop: true to re-trigger fadeIn on each loop.\n * @returns {Promise<void>} Resolves when the fade completes.\n */\n fadeIn(duration: number, type?: FadeType, options?: { perLoop?: boolean; startTime?: number }): Promise<void> {\n if (options?.perLoop) {\n this._fadeInConfig = {\n duration,\n type: type ?? \"linear\",\n perLoop: true,\n targetVolume: this.gainNode!.gain.value,\n };\n }\n return super.fadeIn(duration, type, { startTime: options?.startTime });\n }\n\n /**\n * Configures a fade-out to be applied when the playback ends naturally (last loop iteration).\n * @param {number} duration - The fade-out duration in milliseconds.\n * @param {FadeType} type - The fade curve type. Defaults to \"linear\".\n */\n configureFadeOut(duration: number, type?: FadeType): void {\n this._fadeOutConfig = { duration, type: type ?? \"linear\" };\n }\n\n /**\n * Fades out then stops the playback.\n * @param {number} duration - The fade-out duration in milliseconds.\n * @param {FadeType} type - The fade curve type. Defaults to \"linear\".\n * @returns {Promise<void>} Resolves when the fade completes and playback is stopped.\n */\n stopWithFade(duration: number, type?: FadeType): Promise<void> {\n // Guard the chained stop: if an external stop()/cleanup() runs between\n // the fade resolving and this .then, the chained stop() would otherwise\n // throw against torn-down state (\"Cannot stop a sound that has been\n // cleaned up\"). cancelFade() inside stop()/cleanup() resolves the fade\n // promise synchronously, so this .then will still fire -- the guard\n // makes it a no-op when the playback is already stopped or cleaned up.\n return this.fadeOut(duration, type).then(() => {\n if (this._state !== \"stopped\" && this.source) {\n this.stop();\n }\n });\n }\n\n seek(time: number): void {\n if (!this.source || !this.gainNode || !this.panner) {\n throw new Error(\"Cannot seek a sound that has been cleaned up\");\n }\n if (!Number.isFinite(time) || time < 0 || (this.origin.region !== undefined && time > this.duration)) {\n throw new Error(\"Invalid time value for seek\");\n }\n\n if (\n this.origin.soundType === \"streaming\" &&\n \"mediaElement\" in this.source &&\n (this.source.mediaElement.seekable?.length ?? 0) === 0\n ) {\n if (this.source.mediaElement.duration === Number.POSITIVE_INFINITY) {\n throw new Error(\"Live media-element streams do not expose a seekable range\");\n }\n throw new Error(\"Media-element stream does not currently expose a seekable range\");\n }\n\n const wasPlaying = this._state === \"playing\";\n if (wasPlaying) {\n this.pause();\n }\n\n this._offset = time;\n\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.currentTime = time;\n }\n // For non-media elements, play() will handle recreating the source if needed.\n\n if (wasPlaying) {\n this.play();\n }\n }\n\n get currentTime(): number {\n if (this._state === \"playing\") {\n const elapsed = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);\n const time = this._offset + elapsed;\n return this.regionRelativeOffset(time);\n } else {\n return this._offset;\n }\n }\n\n private removeFromOrigin(): void {\n const idx = this.origin.playbacks.indexOf(this);\n if (idx !== -1) {\n this.origin.playbacks.splice(idx, 1);\n }\n }\n\n private recreateSource() {\n if (!this.buffer || !this.panner || !this.context || !this.gainNode) {\n throw new Error(\"Cannot recreate source of a sound that has been cleaned up\");\n }\n try {\n if (this.source) {\n // It's crucial to nullify onended of the old source if it's an AudioBufferSourceNode (or similar non-restartable source),\n // as its onended event could otherwise interfere with the new source created for seek/resume.\n // MediaElementAudioSourceNode is handled differently as its underlying element can be paused/played.\n if (!(\"mediaElement\" in this.source) && \"onended\" in this.source) {\n this.source.onended = null;\n }\n this.source.disconnect();\n }\n this.source = this.context.createBufferSource();\n this.source.buffer = this.buffer;\n this.setEffectChainEndpoints(this.source, this.panner);\n this.source.onended = this.loopEnded;\n this.playbackRate = this._playbackRate;\n this.configureBufferLoopSource();\n } catch (error) {\n this.emitAsync(\"error\", {\n error: error as Error,\n errorType: \"source\",\n timestamp: Date.now(),\n recoverable: false,\n });\n throw error;\n }\n }\n\n /**\n * Sets whether the audio source should loop.\n * @throws {Error} Throws an error if the sound has been cleaned up.\n */\n set sourceLoop(loop: boolean) {\n if (!this.source) {\n throw new Error(\"Cannot set loop on a sound that has been cleaned up\");\n }\n if (\"loop\" in this.source) {\n this.source.loop = loop;\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.loop = loop;\n }\n }\n\n /**\n * Cleans up resources used by the Playback instance.\n * This method should be called when the audio is no longer needed to free up resources.\n */\n\n cleanup(): void {\n this._mediaPlayPromise = undefined;\n if (!this.source) {\n return; // Already cleaned up\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n const mediaElement = this.source.mediaElement;\n mediaElement.pause();\n try {\n mediaElement.currentTime = 0;\n } catch {\n // Live media without a seekable range can reject currentTime writes.\n }\n mediaElement.loop = false;\n mediaElement.onended = null;\n mediaElement.src = \"\";\n mediaElement.load();\n } else if (\"onended\" in this.source) {\n this.source.onended = null;\n }\n this._offset = 0;\n this._startTime = 0;\n this.markStopped();\n this.currentLoop = 0;\n this.source.disconnect();\n this.source = undefined;\n super.cleanup();\n }\n\n private assertNotCleanedUp(): void {\n if (!this.source || !this.gainNode || !this.panner) {\n throw new Error(\"Cannot perform operation on a sound that has been cleaned up\");\n }\n }\n\n addFilter(filter: BiquadFilterNode): void {\n this.assertNotCleanedUp();\n super.addFilter(filter);\n }\n\n removeFilter(filter: BiquadFilterNode): void {\n this.assertNotCleanedUp();\n super.removeFilter(filter);\n }\n\n /**\n * Sets or gets the loop count for the audio.\n * @param {LoopCount} loopCount - The number of times the audio should loop. 'infinite' for endless looping.\n * @returns {LoopCount} The loop count if no parameter is provided.\n * @throws {Error} Throws an error if the sound has been cleaned up or if the source type is unsupported.\n */\n\n loop(loopCount?: LoopCount): LoopCount {\n if (!this.source) {\n throw new Error(\"Cannot loop a sound that has been cleaned up\");\n }\n if (loopCount !== undefined) {\n const previousLoopCount = this.loopCount;\n const nextLoopCount = loopCount === \"infinite\" ? \"infinite\" : Math.max(0, loopCount);\n const changesInfiniteMode = (previousLoopCount === \"infinite\") !== (nextLoopCount === \"infinite\");\n if (\n changesInfiniteMode &&\n this.origin.region &&\n this._state === \"playing\" &&\n \"loop\" in this.source &&\n \"stop\" in this.source\n ) {\n const restartAt = Math.max(this._startTime, this.context.currentTime);\n const elapsed = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);\n this._offset = this.regionRelativeOffset(\n this.regionRelativeOffset(this._offset + elapsed, previousLoopCount),\n nextLoopCount,\n );\n if (\"onended\" in this.source) {\n this.source.onended = null;\n }\n this.source.stop();\n this.loopCount = nextLoopCount;\n this.currentLoop = 0;\n this._startTime = restartAt;\n this.recreateSource();\n this.startBufferSource(restartAt);\n return this.loopCount;\n }\n this.loopCount = nextLoopCount;\n this.currentLoop = 0;\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n const mediaElement = this.source.mediaElement;\n mediaElement.loop = this.loopCount === \"infinite\";\n } else if (\"loop\" in this.source) {\n this.configureBufferLoopSource();\n } else {\n throw new Error(\"Unsupported source type\");\n }\n return this.loopCount;\n }\n\n /** Copy loop flags and atlas bounds to the current buffer source without changing loop progress. */\n private configureBufferLoopSource(): void {\n if (!this.source || !(\"loop\" in this.source)) return;\n this.source.loop = this.loopCount === \"infinite\";\n if (this.source.buffer) {\n this.source.loopEnd = this.origin.region\n ? this.origin.region.start + this.origin.region.duration\n : this.source.buffer.duration;\n this.source.loopStart = this.origin.region?.start ?? 0;\n }\n }\n\n /** Clamp finite regions and wrap infinite regions while keeping offsets region-relative. */\n private regionRelativeOffset(offset: number, loopCount: LoopCount = this.loopCount): number {\n const region = this.origin.region;\n if (!region) {\n return offset;\n }\n return loopCount === \"infinite\" ? offset % region.duration : Math.min(region.duration, offset);\n }\n\n /** Start a buffer source, translating the region-relative offset once. */\n private startBufferSource(when: number): boolean {\n if (!this.source || !(\"start\" in this.source)) {\n return false;\n }\n const region = this.origin.region;\n if (!region) {\n this.source.start(when, this._offset);\n return true;\n }\n if (this._offset === region.duration) {\n return false;\n }\n const absoluteOffset = region.start + this._offset;\n if (this.loopCount === \"infinite\") {\n this.source.start(when, absoluteOffset);\n } else {\n this.source.start(when, absoluteOffset, region.duration - this._offset);\n }\n return true;\n }\n\n /**\n * Sets the pitch-shift factor for this playback, resurrecting the dormant\n * phase-vocoder worklet (Jean Laroche & Mark Dolson, \"New Phase-Vocoder\n * Techniques for Pitch-Shifting, Harmonizing and Other Exotic Effects\",\n * 1999 IEEE WASPAA — peak-based pitch shift with Identity Phase-Locking).\n *\n * On first use (factor !== 1) the phase-vocoder AudioWorkletNode is built and\n * added as an ordinary entry at the tail of the pre-panner effect chain:\n * source → [filters] → phase vocoder → panner → gainNode. The factor is\n * forwarded to the node's `pitchFactor` AudioParam (1 = no shift, 2 = +1\n * octave, 0.5 = -1 octave).\n *\n * @param factor Pitch multiplier (> 0).\n * @throws {Error} if the playback has been cleaned up or factor <= 0.\n */\n async setPitchShift(factor: number): Promise<void> {\n this.assertNotCleanedUp();\n if (!Number.isFinite(factor) || factor <= 0) {\n throw new Error(\"Pitch-shift factor must be greater than 0\");\n }\n this._pitchFactor = factor;\n const effectChain = this._effectChain;\n if (!effectChain) {\n throw new Error(\"Cannot pitch-shift a playback before its effect chain is initialized\");\n }\n let pitchShiftNode = effectChain.nodes.find((node) => {\n const parameters = (node as AudioWorkletNode).parameters;\n return parameters && typeof parameters.get === \"function\" && parameters.get(\"pitchFactor\") !== undefined;\n }) as AudioWorkletNode | undefined;\n\n // factor === 1 is the documented \"no shift\" contract and MUST be a genuine\n // passthrough. The peak/region phase-vocoder pipeline does NOT guarantee\n // identity for peakless / edge-bin / broadband content (it zero-fills and\n // only repopulates detected peak regions), so we cannot leave the node in\n // the chain at factor 1. Remove its ordinary chain entry so the signal\n // bypasses the worklet entirely (source → [filters] → panner). A later\n // non-unity factor rebuilds a fresh node.\n if (factor === 1) {\n if (pitchShiftNode) {\n effectChain.remove(pitchShiftNode);\n }\n return;\n }\n\n if (!pitchShiftNode) {\n const cacophony = this.origin.cacophony;\n if (!cacophony) {\n throw new Error(\"Cannot pitch-shift a playback whose Sound has no Cacophony instance\");\n }\n pitchShiftNode = await cacophony.buildWorkletEffect(WORKLETS.phaseVocoder, {}, this.context);\n effectChain.add(pitchShiftNode);\n }\n\n const pitchParam = pitchShiftNode.parameters?.get(\"pitchFactor\");\n if (pitchParam) {\n pitchParam.value = factor;\n }\n }\n\n /**\n * The current pitch-shift factor (1 = no shift). See {@link setPitchShift}.\n */\n get pitchShift(): number {\n return this._pitchFactor;\n }\n\n /**\n * Gets the output node of this playback's audio graph.\n * This is the final node in the internal chain before connection to destination.\n * Use this to manually wire the playback into custom audio graphs.\n *\n * @returns {GainNode} The gain node that serves as the output of this playback.\n * @throws {Error} Throws an error if the playback has been cleaned up.\n *\n * @example\n * // Manual routing through custom effects\n * const playback = sound.play()[0];\n * playback.disconnect(); // Disconnect from default destination\n * playback.connect(reverbNode).connect(context.destination);\n */\n get outputNode(): GainNode {\n if (!this.gainNode) {\n throw new Error(\"Cannot access output node of a playback that has been cleaned up\");\n }\n return this.gainNode;\n }\n\n /**\n * Connects this playback's output to an AudioNode or AudioParam.\n * Follows the Web Audio API connection pattern.\n *\n * @returns {AudioNode} The destination node (for chaining).\n * @throws {Error} Throws an error if the playback has been cleaned up.\n *\n * @example\n * // Chain multiple effects\n * playback.connect(delay).connect(reverb).connect(context.destination);\n */\n connect(destination: AudioNode | AudioParam): AudioNode {\n return this.outputNode.connect(destination as any);\n }\n\n /**\n * Disconnects this playback's output from a specific destination or from all destinations.\n *\n * @param {AudioNode | AudioParam} [destination] - Optional specific destination to disconnect from.\n * If omitted, disconnects from all destinations.\n * @throws {Error} Throws an error if the playback has been cleaned up.\n *\n * @example\n * // Disconnect from all\n * playback.disconnect();\n *\n * @example\n * // Disconnect from specific node\n * playback.disconnect(reverbNode);\n */\n disconnect(destination?: AudioNode | AudioParam): void {\n if (destination) {\n this.outputNode.disconnect(destination as any);\n } else {\n this.outputNode.disconnect();\n }\n }\n\n /**\n * Creates a clone of the current Playback instance with optional overrides for certain properties.\n * This method allows for the creation of a new Playback instance that shares the same audio context\n * and source node but can have different settings such as loop count or pan type.\n * The clone is connected to the origin Sound's primary route and registered with the Sound.\n * @throws {Error} Throws an error if the sound has been cleaned up.\n */\n\n clone(overrides: Partial<PlaybackCloneOverrides> = {}): Playback {\n if (!this.source || !this.gainNode || !this.context) {\n throw new Error(\"Cannot clone a sound that has been cleaned up\");\n }\n const panType = overrides.panType ?? this.panType;\n // we'll need to create a new gain node\n const gainNode = this.context.createGain();\n gainNode.connect(this.origin._resolveRouteTargetNode());\n // clone the source node\n let source: SourceNode;\n if (\"buffer\" in this.source && this.source.buffer) {\n source = this.context.createBufferSource();\n source.buffer = this.source.buffer;\n } else if (\"mediaElement\" in this.source && this.source.mediaElement) {\n if (!this.context.createMediaElementSource) {\n throw new Error(\"Media element sources are not supported on this audio context.\");\n }\n // The Web Audio spec forbids creating a second MediaElementAudioSourceNode\n // from the same HTMLMediaElement -- the second call throws\n // InvalidStateError. Clone the underlying element first so the new\n // playback owns an independent element that hasn't been bound yet.\n const originalElement = this.source.mediaElement;\n const clonedElement = originalElement.cloneNode(true) as HTMLMediaElement;\n source = this.context.createMediaElementSource(clonedElement);\n } else {\n throw new Error(\"Unsupported source type\");\n }\n const loopCount = overrides.loopCount !== undefined ? overrides.loopCount : this.loopCount;\n const clone = new Playback(this.origin, source, gainNode);\n\n // Copy all relevant properties\n clone.loopCount = loopCount;\n clone.currentLoop = this.currentLoop;\n clone.setPanType(panType, this.context);\n clone.volume = this.volume;\n clone.playbackRate = this._playbackRate;\n clone._offset = this._offset;\n if (this._fadeInConfig) {\n clone._fadeInConfig = { ...this._fadeInConfig };\n }\n if (this._fadeOutConfig) {\n clone._fadeOutConfig = { ...this._fadeOutConfig };\n }\n if (this._state === \"paused\") {\n clone.markPaused();\n } else if (this._state === \"stopped\") {\n clone.markStopped();\n }\n\n // Deep clone filters\n this._filters.forEach((filter) => {\n const clonedFilter = this.context.createBiquadFilter();\n clonedFilter.type = filter.type;\n clonedFilter.frequency.value = filter.frequency.value;\n clonedFilter.Q.value = filter.Q.value;\n clonedFilter.gain.value = filter.gain.value;\n clone.addFilter(clonedFilter as unknown as BiquadFilterNode);\n });\n\n if (this._pitchFactor !== 1) {\n void clone.setPitchShift(this._pitchFactor).catch(() => {\n /* worklet unavailable on this context — clone proceeds unshifted */\n });\n }\n\n // If the original is playing, start the clone\n if (this._state === \"playing\") {\n clone.play();\n }\n\n this.origin.playbacks.push(clone);\n return clone;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"playback.cjs","names":[],"sources":["../src/playback.ts"],"sourcesContent":["/**\n * The Playback class encapsulates the functionality for playing audio in a web application.\n * It integrates with the repo's audio-context abstractions so playback works across native and mocked contexts.\n * This class allows for the manipulation of audio playback through various features such as:\n * - Playing and stopping audio\n * - Looping audio a specific number of times or infinitely\n * - Adjusting volume and playback rate\n * - Applying stereo or 3D (HRTF) panning\n * - Adding and removing filters to modify the audio output\n * - Handling audio looping with custom logic\n * - Fading audio in and out linearly or exponentially\n * - Seeking to specific points in the audio\n * - Checking if the audio is currently playing\n * - Cleaning up resources when the audio is no longer needed\n *\n * The class is designed to be flexible and can be used with different types of audio sources,\n * including buffer sources and media elements. It also provides detailed control over the audio's\n * spatial characteristics when using 3D audio.\n */\n\nimport { BasePlayback } from \"./basePlayback\";\nimport type { BaseSound, FadeType, LoopCount, PanType, PlayOptions } from \"./cacophony\";\nimport type {\n AudioBuffer,\n AudioNode,\n AudioParam,\n AudioWorkletNode,\n BaseContext,\n BiquadFilterNode,\n GainNode,\n SourceNode,\n} from \"./context\";\nimport { applyPlayOptions, validatePlayOptions } from \"./playOptions\";\nimport type { Sound } from \"./sound\";\nimport { WORKLETS } from \"./worklets\";\n\ntype PlaybackCloneOverrides = {\n loopCount: LoopCount;\n panType: PanType;\n};\n\nexport class Playback extends BasePlayback implements BaseSound {\n private context: BaseContext;\n public declare source?: SourceNode;\n loopCount: LoopCount = 0;\n currentLoop: number = 0;\n private buffer?: AudioBuffer;\n private _offset: number = 0;\n private _startTime: number = 0;\n private _playbackRate: number = 1;\n /**\n * Promise that tracks an in-flight media-element `.play()` settlement.\n * Set when the media element's `play()` returns a pending promise; cleared\n * once that promise settles. Used by `loopEnded` to avoid racing source-state\n * mutations (`seek`/`play`) against the deferred state transition driven by\n * this promise.\n */\n private _mediaPlayPromise?: Promise<void>;\n _fadeInConfig?: { duration: number; type: FadeType; perLoop: boolean; targetVolume: number };\n _fadeOutConfig?: { duration: number; type: FadeType };\n _loopEndCallback?: () => void;\n /**\n * Desired pitch-shift factor (1 = no shift, 2 = +1 octave, 0.5 = -1 octave).\n * Stored even before the worklet node exists so a value set on a cleaned-up /\n * not-yet-built playback is honoured once {@link setPitchShift} builds the node.\n */\n private _pitchFactor: number = 1;\n /**\n * Creates an instance of the Playback class.\n * @throws {Error} Throws an error if an invalid pan type is provided.\n */\n\n constructor(\n public origin: Sound,\n source: SourceNode,\n gainNode: GainNode,\n ) {\n super(origin);\n this.context = origin.context;\n this.loopCount = origin.loopCount;\n this.setPanType(origin.panType, origin.context);\n this.source = source;\n if (\"buffer\" in source && source.buffer) {\n this.buffer = source.buffer;\n }\n this.setupSourceNode(source);\n this.setGainNode(gainNode);\n this.setEffectChainEndpoints(this.source, this.panner!);\n this.panner!.connect(this.gainNode!);\n }\n\n private setupSourceNode(source: SourceNode) {\n if (\"mediaElement\" in source && source.mediaElement) {\n source.mediaElement.onended = this.loopEnded;\n } else if (\"onended\" in source) {\n source.onended = this.loopEnded;\n } else {\n throw new Error(\"Unsupported source type\");\n }\n }\n\n /**\n * Gets the duration of the audio in seconds.\n * @returns {number} The duration of the audio or NaN if the duration is unknown.\n * @throws {Error} Throws an error if the sound has been cleaned up.\n */\n\n get duration() {\n if (this.origin.region) {\n return this.origin.region.duration;\n }\n if (!this.source) {\n throw new Error(\"Cannot get duration of a sound that has been cleaned up\");\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n return this.source.mediaElement.duration;\n }\n if (!this.buffer) {\n return NaN;\n }\n return this.buffer.duration || NaN;\n }\n\n /**\n * Gets the current playback rate of the audio.\n */\n\n get playbackRate() {\n return this._playbackRate;\n }\n\n get volume(): number {\n return super.volume;\n }\n\n set volume(volume: number) {\n super.volume = volume;\n this.emit(\"volumeChange\", volume);\n }\n\n /**\n * Sets the playback rate of the audio.\n * @throws {Error} Throws an error if the sound has been cleaned up or if the source type is unsupported.\n */\n\n set playbackRate(rate: number) {\n if (rate <= 0) {\n throw new Error(\"Playback rate must be greater than 0\");\n }\n if (this._state === \"playing\") {\n const elapsed = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);\n this._offset = this.regionRelativeOffset(this._offset + elapsed);\n if (this.context.currentTime >= this._startTime) {\n this._startTime = this.context.currentTime;\n }\n }\n this._playbackRate = rate;\n if (!this.source) {\n return;\n }\n if (\"playbackRate\" in this.source) {\n this.source.playbackRate.value = rate;\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.playbackRate = rate;\n }\n }\n\n /**\n * Handles the loop event when the audio ends.\n * This method is bound to the 'onended' event of the audio source.\n * It manages looping logic and restarts playback if necessary.\n */\n loopEnded = (): void => {\n if (!this.source || this._state !== \"playing\") {\n return;\n }\n\n // For media-element sources, a previous play() may still have a pending\n // settlement promise -- the deferred state transition races with the\n // seek+play we are about to perform. Defer the loop work until the\n // pending play resolves so source mutations don't interleave.\n if (\"mediaElement\" in this.source && this._mediaPlayPromise) {\n void this._mediaPlayPromise.then(() => this._runLoopEnded());\n return;\n }\n\n this._runLoopEnded();\n };\n\n private _runLoopEnded(): void {\n if (!this.source || this._state !== \"playing\") {\n return;\n }\n\n this.currentLoop++;\n\n if (this.loopCount !== \"infinite\" && this.currentLoop > this.loopCount) {\n // Final iteration -- emit ended, then either fade out or stop immediately\n this.emit(\"ended\", undefined);\n if (this._fadeOutConfig) {\n // Guard the chained stop: an external stop()/cleanup() between the\n // fade resolving and this .then would otherwise run stop()/source\n // access against torn-down state. cancelFade() in stop()/cleanup()\n // resolves the fade promise synchronously, which is what makes this\n // .then fire at all after a teardown.\n void this.fadeOut(this._fadeOutConfig.duration, this._fadeOutConfig.type).then(() => {\n if (this._state !== \"stopped\" && this.source) {\n this.stop();\n this.removeFromOrigin();\n }\n });\n } else {\n this.stop();\n this.removeFromOrigin();\n }\n } else {\n this._loopEndCallback?.();\n this.seek(0); // Resets offset and handles play/pause state internally.\n // seek() calls pause() then play() when wasPlaying is true,\n // so play() handles both media-element and buffer sources correctly.\n if (this._state !== \"playing\" && this.source && !(\"mediaElement\" in this.source)) {\n // For AudioBufferSourceNode only: if seek() didn't restart playback\n // (e.g., state wasn't Playing before seek), start it now.\n this.play();\n }\n\n // Re-trigger fade-in if perLoop config is set\n if (this._fadeInConfig?.perLoop && this.gainNode) {\n this.gainNode.gain.setValueAtTime(0.0001, this.gainNode.context.currentTime);\n this.fadeTo(this._fadeInConfig.targetVolume, this._fadeInConfig.duration, this._fadeInConfig.type);\n }\n }\n }\n\n /**\n * Starts playing the audio.\n * @returns {[this]} Returns the instance of the Playback class for chaining.\n * @throws {Error} Throws an error if the sound has been cleaned up.\n */\n\n play(options?: PlayOptions): [this] {\n if (!this.source) {\n throw new Error(\"Cannot play a sound that has been cleaned up\");\n }\n\n validatePlayOptions(options, this.panType, \"mediaElement\" in this.source ? \"media\" : \"buffer\");\n\n if (this._state === \"playing\") {\n return [this];\n }\n\n applyPlayOptions(this, this.context, options);\n if (options?.playbackRate !== undefined) this.playbackRate = options.playbackRate;\n if (options?.loopCount !== undefined) this.loop(options.loopCount);\n if (options?.fadeIn !== undefined) {\n void this.fadeIn(options.fadeIn, options.fadeType, { perLoop: options.fadeInPerLoop, startTime: options.at });\n }\n if (options?.fadeOut !== undefined) this.configureFadeOut(options.fadeOut, options.fadeType);\n\n const isResume = this._state === \"paused\";\n if (this._state === \"stopped\") this.currentLoop = 0;\n const originGeneration = this.origin._playbackGeneration;\n\n try {\n let mediaPlayPromise: Promise<void> | undefined;\n\n if (this._state === \"paused\") {\n // If we're resuming from a paused state\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n mediaPlayPromise = this.source.mediaElement.play();\n } else {\n // For non-mediaElement sources, we need to recreate and start the source\n this.recreateSource();\n if (\"start\" in this.source) {\n if (!this.startBufferSource(options?.at ?? 0)) {\n return [this];\n }\n }\n }\n } else {\n // If we're starting from the beginning or a stopped state\n\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.currentTime = this._offset;\n mediaPlayPromise = this.source.mediaElement.play();\n } else if (\"start\" in this.source) {\n this.recreateSource();\n if (!this.startBufferSource(options?.at ?? 0)) {\n return [this];\n }\n }\n }\n\n if (mediaPlayPromise) {\n // For media-element sources, defer state and events until browser accepts playback\n // Stash the pending promise so loopEnded (and any future caller that\n // needs to sequence after the deferred state transition) can await it\n // rather than racing source-state mutations against it.\n const tracked = mediaPlayPromise\n .then(\n () => {\n if (this._mediaPlayPromise !== tracked || !this.source) return;\n if (this.origin._playbackGeneration !== originGeneration) {\n if (\"mediaElement\" in this.source) this.source.mediaElement.pause();\n return;\n }\n this._startTime = this.context.currentTime;\n this.emitPlayStarted(isResume);\n },\n (error: Error) => {\n if (this._mediaPlayPromise !== tracked || this.origin._playbackGeneration !== originGeneration) return;\n void this.emitAsync(\"error\", {\n error,\n errorType: \"source\",\n timestamp: Date.now(),\n recoverable: true,\n });\n },\n )\n .finally(() => {\n if (this._mediaPlayPromise === tracked) {\n this._mediaPlayPromise = undefined;\n }\n });\n this._mediaPlayPromise = tracked;\n } else {\n // For buffer sources, state transition is immediate (start() is synchronous)\n this._startTime = Math.max(options?.at ?? this.context.currentTime, this.context.currentTime);\n this.emitPlayStarted(isResume);\n }\n\n return [this];\n } catch (error) {\n this.emitAsync(\"error\", {\n error: error as Error,\n errorType: \"source\",\n timestamp: Date.now(),\n recoverable: true,\n });\n throw error;\n }\n }\n\n protected override emitPlayStarted(isResume: boolean): void {\n // A stopped voice may have been reaped by Sound.preplay() or natural end.\n // Restore ownership only after its source has successfully started.\n if (this._state === \"stopped\" && !this.origin.playbacks.includes(this)) {\n this.origin._readmitPlayback(this);\n }\n super.emitPlayStarted(isResume);\n }\n\n pause(): void {\n if (!this.source || this._state !== \"playing\") {\n return;\n }\n\n const elapsed = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);\n this._offset = this.regionRelativeOffset(this._offset + elapsed);\n\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.pause();\n } else if (\"stop\" in this.source) {\n // For AudioBufferSourceNode and OscillatorNode, stop the source.\n // It cannot be restarted; a new one will be created on play().\n this.source.stop();\n }\n\n this.emitPaused();\n }\n\n stop(): void {\n if (!this.source) {\n throw new Error(\"Cannot stop a sound that has been cleaned up\");\n }\n const pendingMediaPlay = this._mediaPlayPromise !== undefined;\n this._mediaPlayPromise = undefined;\n if (!pendingMediaPlay && (this._state === \"stopped\" || this._state === \"unplayed\")) {\n return;\n }\n\n this.cancelFade();\n\n if (\"stop\" in this.source && this._state === \"playing\") {\n this.source.stop();\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.pause();\n this.source.mediaElement.currentTime = 0;\n }\n\n this._offset = 0;\n this._startTime = 0;\n this.emitStopped();\n }\n\n /**\n * Fades in from silence to the current volume.\n * Optionally stores config to re-trigger the fade on each loop iteration.\n * @param {number} duration - The fade duration in milliseconds.\n * @param {FadeType} type - The fade curve type. Defaults to \"linear\".\n * @param {object} options - Optional. Set perLoop: true to re-trigger fadeIn on each loop.\n * @returns {Promise<void>} Resolves when the fade completes.\n */\n fadeIn(duration: number, type?: FadeType, options?: { perLoop?: boolean; startTime?: number }): Promise<void> {\n if (options?.perLoop) {\n this._fadeInConfig = {\n duration,\n type: type ?? \"linear\",\n perLoop: true,\n targetVolume: this.gainNode!.gain.value,\n };\n }\n return super.fadeIn(duration, type, { startTime: options?.startTime });\n }\n\n /**\n * Configures a fade-out to be applied when the playback ends naturally (last loop iteration).\n * @param {number} duration - The fade-out duration in milliseconds.\n * @param {FadeType} type - The fade curve type. Defaults to \"linear\".\n */\n configureFadeOut(duration: number, type?: FadeType): void {\n this._fadeOutConfig = { duration, type: type ?? \"linear\" };\n }\n\n /**\n * Fades out then stops the playback.\n * @param {number} duration - The fade-out duration in milliseconds.\n * @param {FadeType} type - The fade curve type. Defaults to \"linear\".\n * @returns {Promise<void>} Resolves when the fade completes and playback is stopped.\n */\n stopWithFade(duration: number, type?: FadeType): Promise<void> {\n // Guard the chained stop: if an external stop()/cleanup() runs between\n // the fade resolving and this .then, the chained stop() would otherwise\n // throw against torn-down state (\"Cannot stop a sound that has been\n // cleaned up\"). cancelFade() inside stop()/cleanup() resolves the fade\n // promise synchronously, so this .then will still fire -- the guard\n // makes it a no-op when the playback is already stopped or cleaned up.\n return this.fadeOut(duration, type).then(() => {\n if (this._state !== \"stopped\" && this.source) {\n this.stop();\n }\n });\n }\n\n seek(time: number): void {\n if (!this.source || !this.gainNode || !this.panner) {\n throw new Error(\"Cannot seek a sound that has been cleaned up\");\n }\n if (!Number.isFinite(time) || time < 0 || (this.origin.region !== undefined && time > this.duration)) {\n throw new Error(\"Invalid time value for seek\");\n }\n\n if (\n this.origin.soundType === \"streaming\" &&\n \"mediaElement\" in this.source &&\n (this.source.mediaElement.seekable?.length ?? 0) === 0\n ) {\n if (this.source.mediaElement.duration === Number.POSITIVE_INFINITY) {\n throw new Error(\"Live media-element streams do not expose a seekable range\");\n }\n throw new Error(\"Media-element stream does not currently expose a seekable range\");\n }\n\n const wasPlaying = this._state === \"playing\";\n if (wasPlaying) {\n this.pause();\n }\n\n this._offset = time;\n\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.currentTime = time;\n }\n // For non-media elements, play() will handle recreating the source if needed.\n\n if (wasPlaying) {\n this.play();\n }\n this.emit(\"seek\", time);\n }\n\n get currentTime(): number {\n if (this._state === \"playing\") {\n const elapsed = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);\n const time = this._offset + elapsed;\n return this.regionRelativeOffset(time);\n } else {\n return this._offset;\n }\n }\n\n private removeFromOrigin(): void {\n const idx = this.origin.playbacks.indexOf(this);\n if (idx !== -1) {\n this.origin.playbacks.splice(idx, 1);\n }\n }\n\n private recreateSource() {\n if (!this.buffer || !this.panner || !this.context || !this.gainNode) {\n throw new Error(\"Cannot recreate source of a sound that has been cleaned up\");\n }\n try {\n if (this.source) {\n // It's crucial to nullify onended of the old source if it's an AudioBufferSourceNode (or similar non-restartable source),\n // as its onended event could otherwise interfere with the new source created for seek/resume.\n // MediaElementAudioSourceNode is handled differently as its underlying element can be paused/played.\n if (!(\"mediaElement\" in this.source) && \"onended\" in this.source) {\n this.source.onended = null;\n }\n this.source.disconnect();\n }\n this.source = this.context.createBufferSource();\n this.source.buffer = this.buffer;\n this.setEffectChainEndpoints(this.source, this.panner);\n this.source.onended = this.loopEnded;\n this.playbackRate = this._playbackRate;\n this.configureBufferLoopSource();\n } catch (error) {\n this.emitAsync(\"error\", {\n error: error as Error,\n errorType: \"source\",\n timestamp: Date.now(),\n recoverable: false,\n });\n throw error;\n }\n }\n\n /**\n * Sets whether the audio source should loop.\n * @throws {Error} Throws an error if the sound has been cleaned up.\n */\n set sourceLoop(loop: boolean) {\n if (!this.source) {\n throw new Error(\"Cannot set loop on a sound that has been cleaned up\");\n }\n if (\"loop\" in this.source) {\n this.source.loop = loop;\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.loop = loop;\n }\n }\n\n /**\n * Cleans up resources used by the Playback instance.\n * This method should be called when the audio is no longer needed to free up resources.\n */\n\n cleanup(): void {\n this._mediaPlayPromise = undefined;\n if (!this.source) {\n return; // Already cleaned up\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n const mediaElement = this.source.mediaElement;\n mediaElement.pause();\n try {\n mediaElement.currentTime = 0;\n } catch {\n // Live media without a seekable range can reject currentTime writes.\n }\n mediaElement.loop = false;\n mediaElement.onended = null;\n mediaElement.src = \"\";\n mediaElement.load();\n } else if (\"onended\" in this.source) {\n this.source.onended = null;\n }\n this._offset = 0;\n this._startTime = 0;\n this.markStopped();\n this.currentLoop = 0;\n this.source.disconnect();\n this.source = undefined;\n super.cleanup();\n }\n\n private assertNotCleanedUp(): void {\n if (!this.source || !this.gainNode || !this.panner) {\n throw new Error(\"Cannot perform operation on a sound that has been cleaned up\");\n }\n }\n\n addFilter(filter: BiquadFilterNode): void {\n this.assertNotCleanedUp();\n super.addFilter(filter);\n }\n\n removeFilter(filter: BiquadFilterNode): void {\n this.assertNotCleanedUp();\n super.removeFilter(filter);\n }\n\n /**\n * Sets or gets the loop count for the audio.\n * @param {LoopCount} loopCount - The number of times the audio should loop. 'infinite' for endless looping.\n * @returns {LoopCount} The loop count if no parameter is provided.\n * @throws {Error} Throws an error if the sound has been cleaned up or if the source type is unsupported.\n */\n\n loop(loopCount?: LoopCount): LoopCount {\n if (!this.source) {\n throw new Error(\"Cannot loop a sound that has been cleaned up\");\n }\n if (loopCount !== undefined) {\n const previousLoopCount = this.loopCount;\n const nextLoopCount = loopCount === \"infinite\" ? \"infinite\" : Math.max(0, loopCount);\n const changesInfiniteMode = (previousLoopCount === \"infinite\") !== (nextLoopCount === \"infinite\");\n if (\n changesInfiniteMode &&\n this.origin.region &&\n this._state === \"playing\" &&\n \"loop\" in this.source &&\n \"stop\" in this.source\n ) {\n const restartAt = Math.max(this._startTime, this.context.currentTime);\n const elapsed = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);\n this._offset = this.regionRelativeOffset(\n this.regionRelativeOffset(this._offset + elapsed, previousLoopCount),\n nextLoopCount,\n );\n if (\"onended\" in this.source) {\n this.source.onended = null;\n }\n this.source.stop();\n this.loopCount = nextLoopCount;\n this.currentLoop = 0;\n this._startTime = restartAt;\n this.recreateSource();\n this.startBufferSource(restartAt);\n return this.loopCount;\n }\n this.loopCount = nextLoopCount;\n this.currentLoop = 0;\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n const mediaElement = this.source.mediaElement;\n mediaElement.loop = this.loopCount === \"infinite\";\n } else if (\"loop\" in this.source) {\n this.configureBufferLoopSource();\n } else {\n throw new Error(\"Unsupported source type\");\n }\n return this.loopCount;\n }\n\n /** Copy loop flags and atlas bounds to the current buffer source without changing loop progress. */\n private configureBufferLoopSource(): void {\n if (!this.source || !(\"loop\" in this.source)) return;\n this.source.loop = this.loopCount === \"infinite\";\n if (this.source.buffer) {\n this.source.loopEnd = this.origin.region\n ? this.origin.region.start + this.origin.region.duration\n : this.source.buffer.duration;\n this.source.loopStart = this.origin.region?.start ?? 0;\n }\n }\n\n /** Clamp finite regions and wrap infinite regions while keeping offsets region-relative. */\n private regionRelativeOffset(offset: number, loopCount: LoopCount = this.loopCount): number {\n const region = this.origin.region;\n if (!region) {\n return offset;\n }\n return loopCount === \"infinite\" ? offset % region.duration : Math.min(region.duration, offset);\n }\n\n /** Start a buffer source, translating the region-relative offset once. */\n private startBufferSource(when: number): boolean {\n if (!this.source || !(\"start\" in this.source)) {\n return false;\n }\n const region = this.origin.region;\n if (!region) {\n this.source.start(when, this._offset);\n return true;\n }\n if (this._offset === region.duration) {\n return false;\n }\n const absoluteOffset = region.start + this._offset;\n if (this.loopCount === \"infinite\") {\n this.source.start(when, absoluteOffset);\n } else {\n this.source.start(when, absoluteOffset, region.duration - this._offset);\n }\n return true;\n }\n\n /**\n * Sets the pitch-shift factor for this playback, resurrecting the dormant\n * phase-vocoder worklet (Jean Laroche & Mark Dolson, \"New Phase-Vocoder\n * Techniques for Pitch-Shifting, Harmonizing and Other Exotic Effects\",\n * 1999 IEEE WASPAA — peak-based pitch shift with Identity Phase-Locking).\n *\n * On first use (factor !== 1) the phase-vocoder AudioWorkletNode is built and\n * added as an ordinary entry at the tail of the pre-panner effect chain:\n * source → [filters] → phase vocoder → panner → gainNode. The factor is\n * forwarded to the node's `pitchFactor` AudioParam (1 = no shift, 2 = +1\n * octave, 0.5 = -1 octave).\n *\n * @param factor Pitch multiplier (> 0).\n * @throws {Error} if the playback has been cleaned up or factor <= 0.\n */\n async setPitchShift(factor: number): Promise<void> {\n this.assertNotCleanedUp();\n if (!Number.isFinite(factor) || factor <= 0) {\n throw new Error(\"Pitch-shift factor must be greater than 0\");\n }\n this._pitchFactor = factor;\n const effectChain = this._effectChain;\n if (!effectChain) {\n throw new Error(\"Cannot pitch-shift a playback before its effect chain is initialized\");\n }\n let pitchShiftNode = effectChain.nodes.find((node) => {\n const parameters = (node as AudioWorkletNode).parameters;\n return parameters && typeof parameters.get === \"function\" && parameters.get(\"pitchFactor\") !== undefined;\n }) as AudioWorkletNode | undefined;\n\n // factor === 1 is the documented \"no shift\" contract and MUST be a genuine\n // passthrough. The peak/region phase-vocoder pipeline does NOT guarantee\n // identity for peakless / edge-bin / broadband content (it zero-fills and\n // only repopulates detected peak regions), so we cannot leave the node in\n // the chain at factor 1. Remove its ordinary chain entry so the signal\n // bypasses the worklet entirely (source → [filters] → panner). A later\n // non-unity factor rebuilds a fresh node.\n if (factor === 1) {\n if (pitchShiftNode) {\n effectChain.remove(pitchShiftNode);\n }\n return;\n }\n\n if (!pitchShiftNode) {\n const cacophony = this.origin.cacophony;\n if (!cacophony) {\n throw new Error(\"Cannot pitch-shift a playback whose Sound has no Cacophony instance\");\n }\n pitchShiftNode = await cacophony.buildWorkletEffect(WORKLETS.phaseVocoder, {}, this.context);\n effectChain.add(pitchShiftNode);\n }\n\n const pitchParam = pitchShiftNode.parameters?.get(\"pitchFactor\");\n if (pitchParam) {\n pitchParam.value = factor;\n }\n }\n\n /**\n * The current pitch-shift factor (1 = no shift). See {@link setPitchShift}.\n */\n get pitchShift(): number {\n return this._pitchFactor;\n }\n\n /**\n * Gets the output node of this playback's audio graph.\n * This is the final node in the internal chain before connection to destination.\n * Use this to manually wire the playback into custom audio graphs.\n *\n * @returns {GainNode} The gain node that serves as the output of this playback.\n * @throws {Error} Throws an error if the playback has been cleaned up.\n *\n * @example\n * // Manual routing through custom effects\n * const playback = sound.play()[0];\n * playback.disconnect(); // Disconnect from default destination\n * playback.connect(reverbNode).connect(context.destination);\n */\n get outputNode(): GainNode {\n if (!this.gainNode) {\n throw new Error(\"Cannot access output node of a playback that has been cleaned up\");\n }\n return this.gainNode;\n }\n\n /**\n * Connects this playback's output to an AudioNode or AudioParam.\n * Follows the Web Audio API connection pattern.\n *\n * @returns {AudioNode} The destination node (for chaining).\n * @throws {Error} Throws an error if the playback has been cleaned up.\n *\n * @example\n * // Chain multiple effects\n * playback.connect(delay).connect(reverb).connect(context.destination);\n */\n connect(destination: AudioNode | AudioParam): AudioNode {\n return this.outputNode.connect(destination as any);\n }\n\n /**\n * Disconnects this playback's output from a specific destination or from all destinations.\n *\n * @param {AudioNode | AudioParam} [destination] - Optional specific destination to disconnect from.\n * If omitted, disconnects from all destinations.\n * @throws {Error} Throws an error if the playback has been cleaned up.\n *\n * @example\n * // Disconnect from all\n * playback.disconnect();\n *\n * @example\n * // Disconnect from specific node\n * playback.disconnect(reverbNode);\n */\n disconnect(destination?: AudioNode | AudioParam): void {\n if (destination) {\n this.outputNode.disconnect(destination as any);\n } else {\n this.outputNode.disconnect();\n }\n }\n\n /**\n * Creates a clone of the current Playback instance with optional overrides for certain properties.\n * This method allows for the creation of a new Playback instance that shares the same audio context\n * and source node but can have different settings such as loop count or pan type.\n * The clone is connected to the origin Sound's primary route and registered with the Sound.\n * @throws {Error} Throws an error if the sound has been cleaned up.\n */\n\n clone(overrides: Partial<PlaybackCloneOverrides> = {}): Playback {\n if (!this.source || !this.gainNode || !this.context) {\n throw new Error(\"Cannot clone a sound that has been cleaned up\");\n }\n const panType = overrides.panType ?? this.panType;\n // we'll need to create a new gain node\n const gainNode = this.context.createGain();\n gainNode.connect(this.origin._resolveRouteTargetNode());\n // clone the source node\n let source: SourceNode;\n if (\"buffer\" in this.source && this.source.buffer) {\n source = this.context.createBufferSource();\n source.buffer = this.source.buffer;\n } else if (\"mediaElement\" in this.source && this.source.mediaElement) {\n if (!this.context.createMediaElementSource) {\n throw new Error(\"Media element sources are not supported on this audio context.\");\n }\n // The Web Audio spec forbids creating a second MediaElementAudioSourceNode\n // from the same HTMLMediaElement -- the second call throws\n // InvalidStateError. Clone the underlying element first so the new\n // playback owns an independent element that hasn't been bound yet.\n const originalElement = this.source.mediaElement;\n const clonedElement = originalElement.cloneNode(true) as HTMLMediaElement;\n source = this.context.createMediaElementSource(clonedElement);\n } else {\n throw new Error(\"Unsupported source type\");\n }\n const loopCount = overrides.loopCount !== undefined ? overrides.loopCount : this.loopCount;\n const clone = new Playback(this.origin, source, gainNode);\n\n // Copy all relevant properties\n clone.loopCount = loopCount;\n clone.currentLoop = this.currentLoop;\n clone.setPanType(panType, this.context);\n clone.volume = this.volume;\n clone.playbackRate = this._playbackRate;\n clone._offset = this._offset;\n if (this._fadeInConfig) {\n clone._fadeInConfig = { ...this._fadeInConfig };\n }\n if (this._fadeOutConfig) {\n clone._fadeOutConfig = { ...this._fadeOutConfig };\n }\n if (this._state === \"paused\") {\n clone.markPaused();\n } else if (this._state === \"stopped\") {\n clone.markStopped();\n }\n\n // Deep clone filters\n this._filters.forEach((filter) => {\n const clonedFilter = this.context.createBiquadFilter();\n clonedFilter.type = filter.type;\n clonedFilter.frequency.value = filter.frequency.value;\n clonedFilter.Q.value = filter.Q.value;\n clonedFilter.gain.value = filter.gain.value;\n clone.addFilter(clonedFilter as unknown as BiquadFilterNode);\n });\n\n if (this._pitchFactor !== 1) {\n void clone.setPitchShift(this._pitchFactor).catch(() => {\n /* worklet unavailable on this context — clone proceeds unshifted */\n });\n }\n\n // If the original is playing, start the clone\n if (this._state === \"playing\") {\n clone.play();\n }\n\n this.origin.playbacks.push(clone);\n return clone;\n }\n}\n"],"mappings":"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"}
|
package/dist/playback.mjs
CHANGED
|
@@ -145,7 +145,7 @@ var i = class i extends t {
|
|
|
145
145
|
if (!Number.isFinite(e) || e < 0 || this.origin.region !== void 0 && e > this.duration) throw Error("Invalid time value for seek");
|
|
146
146
|
if (this.origin.soundType === "streaming" && "mediaElement" in this.source && (this.source.mediaElement.seekable?.length ?? 0) === 0) throw this.source.mediaElement.duration === Infinity ? Error("Live media-element streams do not expose a seekable range") : Error("Media-element stream does not currently expose a seekable range");
|
|
147
147
|
let t = this._state === "playing";
|
|
148
|
-
t && this.pause(), this._offset = e, "mediaElement" in this.source && this.source.mediaElement && (this.source.mediaElement.currentTime = e), t && this.play();
|
|
148
|
+
t && this.pause(), this._offset = e, "mediaElement" in this.source && this.source.mediaElement && (this.source.mediaElement.currentTime = e), t && this.play(), this.emit("seek", e);
|
|
149
149
|
}
|
|
150
150
|
get currentTime() {
|
|
151
151
|
if (this._state === "playing") {
|
package/dist/playback.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"playback.mjs","names":[],"sources":["../src/playback.ts"],"sourcesContent":["/**\n * The Playback class encapsulates the functionality for playing audio in a web application.\n * It integrates with the repo's audio-context abstractions so playback works across native and mocked contexts.\n * This class allows for the manipulation of audio playback through various features such as:\n * - Playing and stopping audio\n * - Looping audio a specific number of times or infinitely\n * - Adjusting volume and playback rate\n * - Applying stereo or 3D (HRTF) panning\n * - Adding and removing filters to modify the audio output\n * - Handling audio looping with custom logic\n * - Fading audio in and out linearly or exponentially\n * - Seeking to specific points in the audio\n * - Checking if the audio is currently playing\n * - Cleaning up resources when the audio is no longer needed\n *\n * The class is designed to be flexible and can be used with different types of audio sources,\n * including buffer sources and media elements. It also provides detailed control over the audio's\n * spatial characteristics when using 3D audio.\n */\n\nimport { BasePlayback } from \"./basePlayback\";\nimport type { BaseSound, FadeType, LoopCount, PanType, PlayOptions } from \"./cacophony\";\nimport type {\n AudioBuffer,\n AudioNode,\n AudioParam,\n AudioWorkletNode,\n BaseContext,\n BiquadFilterNode,\n GainNode,\n SourceNode,\n} from \"./context\";\nimport { applyPlayOptions, validatePlayOptions } from \"./playOptions\";\nimport type { Sound } from \"./sound\";\nimport { WORKLETS } from \"./worklets\";\n\ntype PlaybackCloneOverrides = {\n loopCount: LoopCount;\n panType: PanType;\n};\n\nexport class Playback extends BasePlayback implements BaseSound {\n private context: BaseContext;\n public declare source?: SourceNode;\n loopCount: LoopCount = 0;\n currentLoop: number = 0;\n private buffer?: AudioBuffer;\n private _offset: number = 0;\n private _startTime: number = 0;\n private _playbackRate: number = 1;\n /**\n * Promise that tracks an in-flight media-element `.play()` settlement.\n * Set when the media element's `play()` returns a pending promise; cleared\n * once that promise settles. Used by `loopEnded` to avoid racing source-state\n * mutations (`seek`/`play`) against the deferred state transition driven by\n * this promise.\n */\n private _mediaPlayPromise?: Promise<void>;\n _fadeInConfig?: { duration: number; type: FadeType; perLoop: boolean; targetVolume: number };\n _fadeOutConfig?: { duration: number; type: FadeType };\n _loopEndCallback?: () => void;\n /**\n * Desired pitch-shift factor (1 = no shift, 2 = +1 octave, 0.5 = -1 octave).\n * Stored even before the worklet node exists so a value set on a cleaned-up /\n * not-yet-built playback is honoured once {@link setPitchShift} builds the node.\n */\n private _pitchFactor: number = 1;\n /**\n * Creates an instance of the Playback class.\n * @throws {Error} Throws an error if an invalid pan type is provided.\n */\n\n constructor(\n public origin: Sound,\n source: SourceNode,\n gainNode: GainNode,\n ) {\n super(origin);\n this.context = origin.context;\n this.loopCount = origin.loopCount;\n this.setPanType(origin.panType, origin.context);\n this.source = source;\n if (\"buffer\" in source && source.buffer) {\n this.buffer = source.buffer;\n }\n this.setupSourceNode(source);\n this.setGainNode(gainNode);\n this.setEffectChainEndpoints(this.source, this.panner!);\n this.panner!.connect(this.gainNode!);\n }\n\n private setupSourceNode(source: SourceNode) {\n if (\"mediaElement\" in source && source.mediaElement) {\n source.mediaElement.onended = this.loopEnded;\n } else if (\"onended\" in source) {\n source.onended = this.loopEnded;\n } else {\n throw new Error(\"Unsupported source type\");\n }\n }\n\n /**\n * Gets the duration of the audio in seconds.\n * @returns {number} The duration of the audio or NaN if the duration is unknown.\n * @throws {Error} Throws an error if the sound has been cleaned up.\n */\n\n get duration() {\n if (this.origin.region) {\n return this.origin.region.duration;\n }\n if (!this.source) {\n throw new Error(\"Cannot get duration of a sound that has been cleaned up\");\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n return this.source.mediaElement.duration;\n }\n if (!this.buffer) {\n return NaN;\n }\n return this.buffer.duration || NaN;\n }\n\n /**\n * Gets the current playback rate of the audio.\n */\n\n get playbackRate() {\n return this._playbackRate;\n }\n\n get volume(): number {\n return super.volume;\n }\n\n set volume(volume: number) {\n super.volume = volume;\n this.emit(\"volumeChange\", volume);\n }\n\n /**\n * Sets the playback rate of the audio.\n * @throws {Error} Throws an error if the sound has been cleaned up or if the source type is unsupported.\n */\n\n set playbackRate(rate: number) {\n if (rate <= 0) {\n throw new Error(\"Playback rate must be greater than 0\");\n }\n if (this._state === \"playing\") {\n const elapsed = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);\n this._offset = this.regionRelativeOffset(this._offset + elapsed);\n if (this.context.currentTime >= this._startTime) {\n this._startTime = this.context.currentTime;\n }\n }\n this._playbackRate = rate;\n if (!this.source) {\n return;\n }\n if (\"playbackRate\" in this.source) {\n this.source.playbackRate.value = rate;\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.playbackRate = rate;\n }\n }\n\n /**\n * Handles the loop event when the audio ends.\n * This method is bound to the 'onended' event of the audio source.\n * It manages looping logic and restarts playback if necessary.\n */\n loopEnded = (): void => {\n if (!this.source || this._state !== \"playing\") {\n return;\n }\n\n // For media-element sources, a previous play() may still have a pending\n // settlement promise -- the deferred state transition races with the\n // seek+play we are about to perform. Defer the loop work until the\n // pending play resolves so source mutations don't interleave.\n if (\"mediaElement\" in this.source && this._mediaPlayPromise) {\n void this._mediaPlayPromise.then(() => this._runLoopEnded());\n return;\n }\n\n this._runLoopEnded();\n };\n\n private _runLoopEnded(): void {\n if (!this.source || this._state !== \"playing\") {\n return;\n }\n\n this.currentLoop++;\n\n if (this.loopCount !== \"infinite\" && this.currentLoop > this.loopCount) {\n // Final iteration -- emit ended, then either fade out or stop immediately\n this.emit(\"ended\", undefined);\n if (this._fadeOutConfig) {\n // Guard the chained stop: an external stop()/cleanup() between the\n // fade resolving and this .then would otherwise run stop()/source\n // access against torn-down state. cancelFade() in stop()/cleanup()\n // resolves the fade promise synchronously, which is what makes this\n // .then fire at all after a teardown.\n void this.fadeOut(this._fadeOutConfig.duration, this._fadeOutConfig.type).then(() => {\n if (this._state !== \"stopped\" && this.source) {\n this.stop();\n this.removeFromOrigin();\n }\n });\n } else {\n this.stop();\n this.removeFromOrigin();\n }\n } else {\n this._loopEndCallback?.();\n this.seek(0); // Resets offset and handles play/pause state internally.\n // seek() calls pause() then play() when wasPlaying is true,\n // so play() handles both media-element and buffer sources correctly.\n if (this._state !== \"playing\" && this.source && !(\"mediaElement\" in this.source)) {\n // For AudioBufferSourceNode only: if seek() didn't restart playback\n // (e.g., state wasn't Playing before seek), start it now.\n this.play();\n }\n\n // Re-trigger fade-in if perLoop config is set\n if (this._fadeInConfig?.perLoop && this.gainNode) {\n this.gainNode.gain.setValueAtTime(0.0001, this.gainNode.context.currentTime);\n this.fadeTo(this._fadeInConfig.targetVolume, this._fadeInConfig.duration, this._fadeInConfig.type);\n }\n }\n }\n\n /**\n * Starts playing the audio.\n * @returns {[this]} Returns the instance of the Playback class for chaining.\n * @throws {Error} Throws an error if the sound has been cleaned up.\n */\n\n play(options?: PlayOptions): [this] {\n if (!this.source) {\n throw new Error(\"Cannot play a sound that has been cleaned up\");\n }\n\n validatePlayOptions(options, this.panType, \"mediaElement\" in this.source ? \"media\" : \"buffer\");\n\n if (this._state === \"playing\") {\n return [this];\n }\n\n applyPlayOptions(this, this.context, options);\n if (options?.playbackRate !== undefined) this.playbackRate = options.playbackRate;\n if (options?.loopCount !== undefined) this.loop(options.loopCount);\n if (options?.fadeIn !== undefined) {\n void this.fadeIn(options.fadeIn, options.fadeType, { perLoop: options.fadeInPerLoop, startTime: options.at });\n }\n if (options?.fadeOut !== undefined) this.configureFadeOut(options.fadeOut, options.fadeType);\n\n const isResume = this._state === \"paused\";\n if (this._state === \"stopped\") this.currentLoop = 0;\n const originGeneration = this.origin._playbackGeneration;\n\n try {\n let mediaPlayPromise: Promise<void> | undefined;\n\n if (this._state === \"paused\") {\n // If we're resuming from a paused state\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n mediaPlayPromise = this.source.mediaElement.play();\n } else {\n // For non-mediaElement sources, we need to recreate and start the source\n this.recreateSource();\n if (\"start\" in this.source) {\n if (!this.startBufferSource(options?.at ?? 0)) {\n return [this];\n }\n }\n }\n } else {\n // If we're starting from the beginning or a stopped state\n\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.currentTime = this._offset;\n mediaPlayPromise = this.source.mediaElement.play();\n } else if (\"start\" in this.source) {\n this.recreateSource();\n if (!this.startBufferSource(options?.at ?? 0)) {\n return [this];\n }\n }\n }\n\n if (mediaPlayPromise) {\n // For media-element sources, defer state and events until browser accepts playback\n // Stash the pending promise so loopEnded (and any future caller that\n // needs to sequence after the deferred state transition) can await it\n // rather than racing source-state mutations against it.\n const tracked = mediaPlayPromise\n .then(\n () => {\n if (this._mediaPlayPromise !== tracked || !this.source) return;\n if (this.origin._playbackGeneration !== originGeneration) {\n if (\"mediaElement\" in this.source) this.source.mediaElement.pause();\n return;\n }\n this._startTime = this.context.currentTime;\n this.emitPlayStarted(isResume);\n },\n (error: Error) => {\n if (this._mediaPlayPromise !== tracked || this.origin._playbackGeneration !== originGeneration) return;\n void this.emitAsync(\"error\", {\n error,\n errorType: \"source\",\n timestamp: Date.now(),\n recoverable: true,\n });\n },\n )\n .finally(() => {\n if (this._mediaPlayPromise === tracked) {\n this._mediaPlayPromise = undefined;\n }\n });\n this._mediaPlayPromise = tracked;\n } else {\n // For buffer sources, state transition is immediate (start() is synchronous)\n this._startTime = Math.max(options?.at ?? this.context.currentTime, this.context.currentTime);\n this.emitPlayStarted(isResume);\n }\n\n return [this];\n } catch (error) {\n this.emitAsync(\"error\", {\n error: error as Error,\n errorType: \"source\",\n timestamp: Date.now(),\n recoverable: true,\n });\n throw error;\n }\n }\n\n protected override emitPlayStarted(isResume: boolean): void {\n // A stopped voice may have been reaped by Sound.preplay() or natural end.\n // Restore ownership only after its source has successfully started.\n if (this._state === \"stopped\" && !this.origin.playbacks.includes(this)) {\n this.origin._readmitPlayback(this);\n }\n super.emitPlayStarted(isResume);\n }\n\n pause(): void {\n if (!this.source || this._state !== \"playing\") {\n return;\n }\n\n const elapsed = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);\n this._offset = this.regionRelativeOffset(this._offset + elapsed);\n\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.pause();\n } else if (\"stop\" in this.source) {\n // For AudioBufferSourceNode and OscillatorNode, stop the source.\n // It cannot be restarted; a new one will be created on play().\n this.source.stop();\n }\n\n this.emitPaused();\n }\n\n stop(): void {\n if (!this.source) {\n throw new Error(\"Cannot stop a sound that has been cleaned up\");\n }\n const pendingMediaPlay = this._mediaPlayPromise !== undefined;\n this._mediaPlayPromise = undefined;\n if (!pendingMediaPlay && (this._state === \"stopped\" || this._state === \"unplayed\")) {\n return;\n }\n\n this.cancelFade();\n\n if (\"stop\" in this.source && this._state === \"playing\") {\n this.source.stop();\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.pause();\n this.source.mediaElement.currentTime = 0;\n }\n\n this._offset = 0;\n this._startTime = 0;\n this.emitStopped();\n }\n\n /**\n * Fades in from silence to the current volume.\n * Optionally stores config to re-trigger the fade on each loop iteration.\n * @param {number} duration - The fade duration in milliseconds.\n * @param {FadeType} type - The fade curve type. Defaults to \"linear\".\n * @param {object} options - Optional. Set perLoop: true to re-trigger fadeIn on each loop.\n * @returns {Promise<void>} Resolves when the fade completes.\n */\n fadeIn(duration: number, type?: FadeType, options?: { perLoop?: boolean; startTime?: number }): Promise<void> {\n if (options?.perLoop) {\n this._fadeInConfig = {\n duration,\n type: type ?? \"linear\",\n perLoop: true,\n targetVolume: this.gainNode!.gain.value,\n };\n }\n return super.fadeIn(duration, type, { startTime: options?.startTime });\n }\n\n /**\n * Configures a fade-out to be applied when the playback ends naturally (last loop iteration).\n * @param {number} duration - The fade-out duration in milliseconds.\n * @param {FadeType} type - The fade curve type. Defaults to \"linear\".\n */\n configureFadeOut(duration: number, type?: FadeType): void {\n this._fadeOutConfig = { duration, type: type ?? \"linear\" };\n }\n\n /**\n * Fades out then stops the playback.\n * @param {number} duration - The fade-out duration in milliseconds.\n * @param {FadeType} type - The fade curve type. Defaults to \"linear\".\n * @returns {Promise<void>} Resolves when the fade completes and playback is stopped.\n */\n stopWithFade(duration: number, type?: FadeType): Promise<void> {\n // Guard the chained stop: if an external stop()/cleanup() runs between\n // the fade resolving and this .then, the chained stop() would otherwise\n // throw against torn-down state (\"Cannot stop a sound that has been\n // cleaned up\"). cancelFade() inside stop()/cleanup() resolves the fade\n // promise synchronously, so this .then will still fire -- the guard\n // makes it a no-op when the playback is already stopped or cleaned up.\n return this.fadeOut(duration, type).then(() => {\n if (this._state !== \"stopped\" && this.source) {\n this.stop();\n }\n });\n }\n\n seek(time: number): void {\n if (!this.source || !this.gainNode || !this.panner) {\n throw new Error(\"Cannot seek a sound that has been cleaned up\");\n }\n if (!Number.isFinite(time) || time < 0 || (this.origin.region !== undefined && time > this.duration)) {\n throw new Error(\"Invalid time value for seek\");\n }\n\n if (\n this.origin.soundType === \"streaming\" &&\n \"mediaElement\" in this.source &&\n (this.source.mediaElement.seekable?.length ?? 0) === 0\n ) {\n if (this.source.mediaElement.duration === Number.POSITIVE_INFINITY) {\n throw new Error(\"Live media-element streams do not expose a seekable range\");\n }\n throw new Error(\"Media-element stream does not currently expose a seekable range\");\n }\n\n const wasPlaying = this._state === \"playing\";\n if (wasPlaying) {\n this.pause();\n }\n\n this._offset = time;\n\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.currentTime = time;\n }\n // For non-media elements, play() will handle recreating the source if needed.\n\n if (wasPlaying) {\n this.play();\n }\n }\n\n get currentTime(): number {\n if (this._state === \"playing\") {\n const elapsed = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);\n const time = this._offset + elapsed;\n return this.regionRelativeOffset(time);\n } else {\n return this._offset;\n }\n }\n\n private removeFromOrigin(): void {\n const idx = this.origin.playbacks.indexOf(this);\n if (idx !== -1) {\n this.origin.playbacks.splice(idx, 1);\n }\n }\n\n private recreateSource() {\n if (!this.buffer || !this.panner || !this.context || !this.gainNode) {\n throw new Error(\"Cannot recreate source of a sound that has been cleaned up\");\n }\n try {\n if (this.source) {\n // It's crucial to nullify onended of the old source if it's an AudioBufferSourceNode (or similar non-restartable source),\n // as its onended event could otherwise interfere with the new source created for seek/resume.\n // MediaElementAudioSourceNode is handled differently as its underlying element can be paused/played.\n if (!(\"mediaElement\" in this.source) && \"onended\" in this.source) {\n this.source.onended = null;\n }\n this.source.disconnect();\n }\n this.source = this.context.createBufferSource();\n this.source.buffer = this.buffer;\n this.setEffectChainEndpoints(this.source, this.panner);\n this.source.onended = this.loopEnded;\n this.playbackRate = this._playbackRate;\n this.configureBufferLoopSource();\n } catch (error) {\n this.emitAsync(\"error\", {\n error: error as Error,\n errorType: \"source\",\n timestamp: Date.now(),\n recoverable: false,\n });\n throw error;\n }\n }\n\n /**\n * Sets whether the audio source should loop.\n * @throws {Error} Throws an error if the sound has been cleaned up.\n */\n set sourceLoop(loop: boolean) {\n if (!this.source) {\n throw new Error(\"Cannot set loop on a sound that has been cleaned up\");\n }\n if (\"loop\" in this.source) {\n this.source.loop = loop;\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.loop = loop;\n }\n }\n\n /**\n * Cleans up resources used by the Playback instance.\n * This method should be called when the audio is no longer needed to free up resources.\n */\n\n cleanup(): void {\n this._mediaPlayPromise = undefined;\n if (!this.source) {\n return; // Already cleaned up\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n const mediaElement = this.source.mediaElement;\n mediaElement.pause();\n try {\n mediaElement.currentTime = 0;\n } catch {\n // Live media without a seekable range can reject currentTime writes.\n }\n mediaElement.loop = false;\n mediaElement.onended = null;\n mediaElement.src = \"\";\n mediaElement.load();\n } else if (\"onended\" in this.source) {\n this.source.onended = null;\n }\n this._offset = 0;\n this._startTime = 0;\n this.markStopped();\n this.currentLoop = 0;\n this.source.disconnect();\n this.source = undefined;\n super.cleanup();\n }\n\n private assertNotCleanedUp(): void {\n if (!this.source || !this.gainNode || !this.panner) {\n throw new Error(\"Cannot perform operation on a sound that has been cleaned up\");\n }\n }\n\n addFilter(filter: BiquadFilterNode): void {\n this.assertNotCleanedUp();\n super.addFilter(filter);\n }\n\n removeFilter(filter: BiquadFilterNode): void {\n this.assertNotCleanedUp();\n super.removeFilter(filter);\n }\n\n /**\n * Sets or gets the loop count for the audio.\n * @param {LoopCount} loopCount - The number of times the audio should loop. 'infinite' for endless looping.\n * @returns {LoopCount} The loop count if no parameter is provided.\n * @throws {Error} Throws an error if the sound has been cleaned up or if the source type is unsupported.\n */\n\n loop(loopCount?: LoopCount): LoopCount {\n if (!this.source) {\n throw new Error(\"Cannot loop a sound that has been cleaned up\");\n }\n if (loopCount !== undefined) {\n const previousLoopCount = this.loopCount;\n const nextLoopCount = loopCount === \"infinite\" ? \"infinite\" : Math.max(0, loopCount);\n const changesInfiniteMode = (previousLoopCount === \"infinite\") !== (nextLoopCount === \"infinite\");\n if (\n changesInfiniteMode &&\n this.origin.region &&\n this._state === \"playing\" &&\n \"loop\" in this.source &&\n \"stop\" in this.source\n ) {\n const restartAt = Math.max(this._startTime, this.context.currentTime);\n const elapsed = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);\n this._offset = this.regionRelativeOffset(\n this.regionRelativeOffset(this._offset + elapsed, previousLoopCount),\n nextLoopCount,\n );\n if (\"onended\" in this.source) {\n this.source.onended = null;\n }\n this.source.stop();\n this.loopCount = nextLoopCount;\n this.currentLoop = 0;\n this._startTime = restartAt;\n this.recreateSource();\n this.startBufferSource(restartAt);\n return this.loopCount;\n }\n this.loopCount = nextLoopCount;\n this.currentLoop = 0;\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n const mediaElement = this.source.mediaElement;\n mediaElement.loop = this.loopCount === \"infinite\";\n } else if (\"loop\" in this.source) {\n this.configureBufferLoopSource();\n } else {\n throw new Error(\"Unsupported source type\");\n }\n return this.loopCount;\n }\n\n /** Copy loop flags and atlas bounds to the current buffer source without changing loop progress. */\n private configureBufferLoopSource(): void {\n if (!this.source || !(\"loop\" in this.source)) return;\n this.source.loop = this.loopCount === \"infinite\";\n if (this.source.buffer) {\n this.source.loopEnd = this.origin.region\n ? this.origin.region.start + this.origin.region.duration\n : this.source.buffer.duration;\n this.source.loopStart = this.origin.region?.start ?? 0;\n }\n }\n\n /** Clamp finite regions and wrap infinite regions while keeping offsets region-relative. */\n private regionRelativeOffset(offset: number, loopCount: LoopCount = this.loopCount): number {\n const region = this.origin.region;\n if (!region) {\n return offset;\n }\n return loopCount === \"infinite\" ? offset % region.duration : Math.min(region.duration, offset);\n }\n\n /** Start a buffer source, translating the region-relative offset once. */\n private startBufferSource(when: number): boolean {\n if (!this.source || !(\"start\" in this.source)) {\n return false;\n }\n const region = this.origin.region;\n if (!region) {\n this.source.start(when, this._offset);\n return true;\n }\n if (this._offset === region.duration) {\n return false;\n }\n const absoluteOffset = region.start + this._offset;\n if (this.loopCount === \"infinite\") {\n this.source.start(when, absoluteOffset);\n } else {\n this.source.start(when, absoluteOffset, region.duration - this._offset);\n }\n return true;\n }\n\n /**\n * Sets the pitch-shift factor for this playback, resurrecting the dormant\n * phase-vocoder worklet (Jean Laroche & Mark Dolson, \"New Phase-Vocoder\n * Techniques for Pitch-Shifting, Harmonizing and Other Exotic Effects\",\n * 1999 IEEE WASPAA — peak-based pitch shift with Identity Phase-Locking).\n *\n * On first use (factor !== 1) the phase-vocoder AudioWorkletNode is built and\n * added as an ordinary entry at the tail of the pre-panner effect chain:\n * source → [filters] → phase vocoder → panner → gainNode. The factor is\n * forwarded to the node's `pitchFactor` AudioParam (1 = no shift, 2 = +1\n * octave, 0.5 = -1 octave).\n *\n * @param factor Pitch multiplier (> 0).\n * @throws {Error} if the playback has been cleaned up or factor <= 0.\n */\n async setPitchShift(factor: number): Promise<void> {\n this.assertNotCleanedUp();\n if (!Number.isFinite(factor) || factor <= 0) {\n throw new Error(\"Pitch-shift factor must be greater than 0\");\n }\n this._pitchFactor = factor;\n const effectChain = this._effectChain;\n if (!effectChain) {\n throw new Error(\"Cannot pitch-shift a playback before its effect chain is initialized\");\n }\n let pitchShiftNode = effectChain.nodes.find((node) => {\n const parameters = (node as AudioWorkletNode).parameters;\n return parameters && typeof parameters.get === \"function\" && parameters.get(\"pitchFactor\") !== undefined;\n }) as AudioWorkletNode | undefined;\n\n // factor === 1 is the documented \"no shift\" contract and MUST be a genuine\n // passthrough. The peak/region phase-vocoder pipeline does NOT guarantee\n // identity for peakless / edge-bin / broadband content (it zero-fills and\n // only repopulates detected peak regions), so we cannot leave the node in\n // the chain at factor 1. Remove its ordinary chain entry so the signal\n // bypasses the worklet entirely (source → [filters] → panner). A later\n // non-unity factor rebuilds a fresh node.\n if (factor === 1) {\n if (pitchShiftNode) {\n effectChain.remove(pitchShiftNode);\n }\n return;\n }\n\n if (!pitchShiftNode) {\n const cacophony = this.origin.cacophony;\n if (!cacophony) {\n throw new Error(\"Cannot pitch-shift a playback whose Sound has no Cacophony instance\");\n }\n pitchShiftNode = await cacophony.buildWorkletEffect(WORKLETS.phaseVocoder, {}, this.context);\n effectChain.add(pitchShiftNode);\n }\n\n const pitchParam = pitchShiftNode.parameters?.get(\"pitchFactor\");\n if (pitchParam) {\n pitchParam.value = factor;\n }\n }\n\n /**\n * The current pitch-shift factor (1 = no shift). See {@link setPitchShift}.\n */\n get pitchShift(): number {\n return this._pitchFactor;\n }\n\n /**\n * Gets the output node of this playback's audio graph.\n * This is the final node in the internal chain before connection to destination.\n * Use this to manually wire the playback into custom audio graphs.\n *\n * @returns {GainNode} The gain node that serves as the output of this playback.\n * @throws {Error} Throws an error if the playback has been cleaned up.\n *\n * @example\n * // Manual routing through custom effects\n * const playback = sound.play()[0];\n * playback.disconnect(); // Disconnect from default destination\n * playback.connect(reverbNode).connect(context.destination);\n */\n get outputNode(): GainNode {\n if (!this.gainNode) {\n throw new Error(\"Cannot access output node of a playback that has been cleaned up\");\n }\n return this.gainNode;\n }\n\n /**\n * Connects this playback's output to an AudioNode or AudioParam.\n * Follows the Web Audio API connection pattern.\n *\n * @returns {AudioNode} The destination node (for chaining).\n * @throws {Error} Throws an error if the playback has been cleaned up.\n *\n * @example\n * // Chain multiple effects\n * playback.connect(delay).connect(reverb).connect(context.destination);\n */\n connect(destination: AudioNode | AudioParam): AudioNode {\n return this.outputNode.connect(destination as any);\n }\n\n /**\n * Disconnects this playback's output from a specific destination or from all destinations.\n *\n * @param {AudioNode | AudioParam} [destination] - Optional specific destination to disconnect from.\n * If omitted, disconnects from all destinations.\n * @throws {Error} Throws an error if the playback has been cleaned up.\n *\n * @example\n * // Disconnect from all\n * playback.disconnect();\n *\n * @example\n * // Disconnect from specific node\n * playback.disconnect(reverbNode);\n */\n disconnect(destination?: AudioNode | AudioParam): void {\n if (destination) {\n this.outputNode.disconnect(destination as any);\n } else {\n this.outputNode.disconnect();\n }\n }\n\n /**\n * Creates a clone of the current Playback instance with optional overrides for certain properties.\n * This method allows for the creation of a new Playback instance that shares the same audio context\n * and source node but can have different settings such as loop count or pan type.\n * The clone is connected to the origin Sound's primary route and registered with the Sound.\n * @throws {Error} Throws an error if the sound has been cleaned up.\n */\n\n clone(overrides: Partial<PlaybackCloneOverrides> = {}): Playback {\n if (!this.source || !this.gainNode || !this.context) {\n throw new Error(\"Cannot clone a sound that has been cleaned up\");\n }\n const panType = overrides.panType ?? this.panType;\n // we'll need to create a new gain node\n const gainNode = this.context.createGain();\n gainNode.connect(this.origin._resolveRouteTargetNode());\n // clone the source node\n let source: SourceNode;\n if (\"buffer\" in this.source && this.source.buffer) {\n source = this.context.createBufferSource();\n source.buffer = this.source.buffer;\n } else if (\"mediaElement\" in this.source && this.source.mediaElement) {\n if (!this.context.createMediaElementSource) {\n throw new Error(\"Media element sources are not supported on this audio context.\");\n }\n // The Web Audio spec forbids creating a second MediaElementAudioSourceNode\n // from the same HTMLMediaElement -- the second call throws\n // InvalidStateError. Clone the underlying element first so the new\n // playback owns an independent element that hasn't been bound yet.\n const originalElement = this.source.mediaElement;\n const clonedElement = originalElement.cloneNode(true) as HTMLMediaElement;\n source = this.context.createMediaElementSource(clonedElement);\n } else {\n throw new Error(\"Unsupported source type\");\n }\n const loopCount = overrides.loopCount !== undefined ? overrides.loopCount : this.loopCount;\n const clone = new Playback(this.origin, source, gainNode);\n\n // Copy all relevant properties\n clone.loopCount = loopCount;\n clone.currentLoop = this.currentLoop;\n clone.setPanType(panType, this.context);\n clone.volume = this.volume;\n clone.playbackRate = this._playbackRate;\n clone._offset = this._offset;\n if (this._fadeInConfig) {\n clone._fadeInConfig = { ...this._fadeInConfig };\n }\n if (this._fadeOutConfig) {\n clone._fadeOutConfig = { ...this._fadeOutConfig };\n }\n if (this._state === \"paused\") {\n clone.markPaused();\n } else if (this._state === \"stopped\") {\n clone.markStopped();\n }\n\n // Deep clone filters\n this._filters.forEach((filter) => {\n const clonedFilter = this.context.createBiquadFilter();\n clonedFilter.type = filter.type;\n clonedFilter.frequency.value = filter.frequency.value;\n clonedFilter.Q.value = filter.Q.value;\n clonedFilter.gain.value = filter.gain.value;\n clone.addFilter(clonedFilter as unknown as BiquadFilterNode);\n });\n\n if (this._pitchFactor !== 1) {\n void clone.setPitchShift(this._pitchFactor).catch(() => {\n /* worklet unavailable on this context — clone proceeds unshifted */\n });\n }\n\n // If the original is playing, start the clone\n if (this._state === \"playing\") {\n clone.play();\n }\n\n this.origin.playbacks.push(clone);\n return clone;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"playback.mjs","names":[],"sources":["../src/playback.ts"],"sourcesContent":["/**\n * The Playback class encapsulates the functionality for playing audio in a web application.\n * It integrates with the repo's audio-context abstractions so playback works across native and mocked contexts.\n * This class allows for the manipulation of audio playback through various features such as:\n * - Playing and stopping audio\n * - Looping audio a specific number of times or infinitely\n * - Adjusting volume and playback rate\n * - Applying stereo or 3D (HRTF) panning\n * - Adding and removing filters to modify the audio output\n * - Handling audio looping with custom logic\n * - Fading audio in and out linearly or exponentially\n * - Seeking to specific points in the audio\n * - Checking if the audio is currently playing\n * - Cleaning up resources when the audio is no longer needed\n *\n * The class is designed to be flexible and can be used with different types of audio sources,\n * including buffer sources and media elements. It also provides detailed control over the audio's\n * spatial characteristics when using 3D audio.\n */\n\nimport { BasePlayback } from \"./basePlayback\";\nimport type { BaseSound, FadeType, LoopCount, PanType, PlayOptions } from \"./cacophony\";\nimport type {\n AudioBuffer,\n AudioNode,\n AudioParam,\n AudioWorkletNode,\n BaseContext,\n BiquadFilterNode,\n GainNode,\n SourceNode,\n} from \"./context\";\nimport { applyPlayOptions, validatePlayOptions } from \"./playOptions\";\nimport type { Sound } from \"./sound\";\nimport { WORKLETS } from \"./worklets\";\n\ntype PlaybackCloneOverrides = {\n loopCount: LoopCount;\n panType: PanType;\n};\n\nexport class Playback extends BasePlayback implements BaseSound {\n private context: BaseContext;\n public declare source?: SourceNode;\n loopCount: LoopCount = 0;\n currentLoop: number = 0;\n private buffer?: AudioBuffer;\n private _offset: number = 0;\n private _startTime: number = 0;\n private _playbackRate: number = 1;\n /**\n * Promise that tracks an in-flight media-element `.play()` settlement.\n * Set when the media element's `play()` returns a pending promise; cleared\n * once that promise settles. Used by `loopEnded` to avoid racing source-state\n * mutations (`seek`/`play`) against the deferred state transition driven by\n * this promise.\n */\n private _mediaPlayPromise?: Promise<void>;\n _fadeInConfig?: { duration: number; type: FadeType; perLoop: boolean; targetVolume: number };\n _fadeOutConfig?: { duration: number; type: FadeType };\n _loopEndCallback?: () => void;\n /**\n * Desired pitch-shift factor (1 = no shift, 2 = +1 octave, 0.5 = -1 octave).\n * Stored even before the worklet node exists so a value set on a cleaned-up /\n * not-yet-built playback is honoured once {@link setPitchShift} builds the node.\n */\n private _pitchFactor: number = 1;\n /**\n * Creates an instance of the Playback class.\n * @throws {Error} Throws an error if an invalid pan type is provided.\n */\n\n constructor(\n public origin: Sound,\n source: SourceNode,\n gainNode: GainNode,\n ) {\n super(origin);\n this.context = origin.context;\n this.loopCount = origin.loopCount;\n this.setPanType(origin.panType, origin.context);\n this.source = source;\n if (\"buffer\" in source && source.buffer) {\n this.buffer = source.buffer;\n }\n this.setupSourceNode(source);\n this.setGainNode(gainNode);\n this.setEffectChainEndpoints(this.source, this.panner!);\n this.panner!.connect(this.gainNode!);\n }\n\n private setupSourceNode(source: SourceNode) {\n if (\"mediaElement\" in source && source.mediaElement) {\n source.mediaElement.onended = this.loopEnded;\n } else if (\"onended\" in source) {\n source.onended = this.loopEnded;\n } else {\n throw new Error(\"Unsupported source type\");\n }\n }\n\n /**\n * Gets the duration of the audio in seconds.\n * @returns {number} The duration of the audio or NaN if the duration is unknown.\n * @throws {Error} Throws an error if the sound has been cleaned up.\n */\n\n get duration() {\n if (this.origin.region) {\n return this.origin.region.duration;\n }\n if (!this.source) {\n throw new Error(\"Cannot get duration of a sound that has been cleaned up\");\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n return this.source.mediaElement.duration;\n }\n if (!this.buffer) {\n return NaN;\n }\n return this.buffer.duration || NaN;\n }\n\n /**\n * Gets the current playback rate of the audio.\n */\n\n get playbackRate() {\n return this._playbackRate;\n }\n\n get volume(): number {\n return super.volume;\n }\n\n set volume(volume: number) {\n super.volume = volume;\n this.emit(\"volumeChange\", volume);\n }\n\n /**\n * Sets the playback rate of the audio.\n * @throws {Error} Throws an error if the sound has been cleaned up or if the source type is unsupported.\n */\n\n set playbackRate(rate: number) {\n if (rate <= 0) {\n throw new Error(\"Playback rate must be greater than 0\");\n }\n if (this._state === \"playing\") {\n const elapsed = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);\n this._offset = this.regionRelativeOffset(this._offset + elapsed);\n if (this.context.currentTime >= this._startTime) {\n this._startTime = this.context.currentTime;\n }\n }\n this._playbackRate = rate;\n if (!this.source) {\n return;\n }\n if (\"playbackRate\" in this.source) {\n this.source.playbackRate.value = rate;\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.playbackRate = rate;\n }\n }\n\n /**\n * Handles the loop event when the audio ends.\n * This method is bound to the 'onended' event of the audio source.\n * It manages looping logic and restarts playback if necessary.\n */\n loopEnded = (): void => {\n if (!this.source || this._state !== \"playing\") {\n return;\n }\n\n // For media-element sources, a previous play() may still have a pending\n // settlement promise -- the deferred state transition races with the\n // seek+play we are about to perform. Defer the loop work until the\n // pending play resolves so source mutations don't interleave.\n if (\"mediaElement\" in this.source && this._mediaPlayPromise) {\n void this._mediaPlayPromise.then(() => this._runLoopEnded());\n return;\n }\n\n this._runLoopEnded();\n };\n\n private _runLoopEnded(): void {\n if (!this.source || this._state !== \"playing\") {\n return;\n }\n\n this.currentLoop++;\n\n if (this.loopCount !== \"infinite\" && this.currentLoop > this.loopCount) {\n // Final iteration -- emit ended, then either fade out or stop immediately\n this.emit(\"ended\", undefined);\n if (this._fadeOutConfig) {\n // Guard the chained stop: an external stop()/cleanup() between the\n // fade resolving and this .then would otherwise run stop()/source\n // access against torn-down state. cancelFade() in stop()/cleanup()\n // resolves the fade promise synchronously, which is what makes this\n // .then fire at all after a teardown.\n void this.fadeOut(this._fadeOutConfig.duration, this._fadeOutConfig.type).then(() => {\n if (this._state !== \"stopped\" && this.source) {\n this.stop();\n this.removeFromOrigin();\n }\n });\n } else {\n this.stop();\n this.removeFromOrigin();\n }\n } else {\n this._loopEndCallback?.();\n this.seek(0); // Resets offset and handles play/pause state internally.\n // seek() calls pause() then play() when wasPlaying is true,\n // so play() handles both media-element and buffer sources correctly.\n if (this._state !== \"playing\" && this.source && !(\"mediaElement\" in this.source)) {\n // For AudioBufferSourceNode only: if seek() didn't restart playback\n // (e.g., state wasn't Playing before seek), start it now.\n this.play();\n }\n\n // Re-trigger fade-in if perLoop config is set\n if (this._fadeInConfig?.perLoop && this.gainNode) {\n this.gainNode.gain.setValueAtTime(0.0001, this.gainNode.context.currentTime);\n this.fadeTo(this._fadeInConfig.targetVolume, this._fadeInConfig.duration, this._fadeInConfig.type);\n }\n }\n }\n\n /**\n * Starts playing the audio.\n * @returns {[this]} Returns the instance of the Playback class for chaining.\n * @throws {Error} Throws an error if the sound has been cleaned up.\n */\n\n play(options?: PlayOptions): [this] {\n if (!this.source) {\n throw new Error(\"Cannot play a sound that has been cleaned up\");\n }\n\n validatePlayOptions(options, this.panType, \"mediaElement\" in this.source ? \"media\" : \"buffer\");\n\n if (this._state === \"playing\") {\n return [this];\n }\n\n applyPlayOptions(this, this.context, options);\n if (options?.playbackRate !== undefined) this.playbackRate = options.playbackRate;\n if (options?.loopCount !== undefined) this.loop(options.loopCount);\n if (options?.fadeIn !== undefined) {\n void this.fadeIn(options.fadeIn, options.fadeType, { perLoop: options.fadeInPerLoop, startTime: options.at });\n }\n if (options?.fadeOut !== undefined) this.configureFadeOut(options.fadeOut, options.fadeType);\n\n const isResume = this._state === \"paused\";\n if (this._state === \"stopped\") this.currentLoop = 0;\n const originGeneration = this.origin._playbackGeneration;\n\n try {\n let mediaPlayPromise: Promise<void> | undefined;\n\n if (this._state === \"paused\") {\n // If we're resuming from a paused state\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n mediaPlayPromise = this.source.mediaElement.play();\n } else {\n // For non-mediaElement sources, we need to recreate and start the source\n this.recreateSource();\n if (\"start\" in this.source) {\n if (!this.startBufferSource(options?.at ?? 0)) {\n return [this];\n }\n }\n }\n } else {\n // If we're starting from the beginning or a stopped state\n\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.currentTime = this._offset;\n mediaPlayPromise = this.source.mediaElement.play();\n } else if (\"start\" in this.source) {\n this.recreateSource();\n if (!this.startBufferSource(options?.at ?? 0)) {\n return [this];\n }\n }\n }\n\n if (mediaPlayPromise) {\n // For media-element sources, defer state and events until browser accepts playback\n // Stash the pending promise so loopEnded (and any future caller that\n // needs to sequence after the deferred state transition) can await it\n // rather than racing source-state mutations against it.\n const tracked = mediaPlayPromise\n .then(\n () => {\n if (this._mediaPlayPromise !== tracked || !this.source) return;\n if (this.origin._playbackGeneration !== originGeneration) {\n if (\"mediaElement\" in this.source) this.source.mediaElement.pause();\n return;\n }\n this._startTime = this.context.currentTime;\n this.emitPlayStarted(isResume);\n },\n (error: Error) => {\n if (this._mediaPlayPromise !== tracked || this.origin._playbackGeneration !== originGeneration) return;\n void this.emitAsync(\"error\", {\n error,\n errorType: \"source\",\n timestamp: Date.now(),\n recoverable: true,\n });\n },\n )\n .finally(() => {\n if (this._mediaPlayPromise === tracked) {\n this._mediaPlayPromise = undefined;\n }\n });\n this._mediaPlayPromise = tracked;\n } else {\n // For buffer sources, state transition is immediate (start() is synchronous)\n this._startTime = Math.max(options?.at ?? this.context.currentTime, this.context.currentTime);\n this.emitPlayStarted(isResume);\n }\n\n return [this];\n } catch (error) {\n this.emitAsync(\"error\", {\n error: error as Error,\n errorType: \"source\",\n timestamp: Date.now(),\n recoverable: true,\n });\n throw error;\n }\n }\n\n protected override emitPlayStarted(isResume: boolean): void {\n // A stopped voice may have been reaped by Sound.preplay() or natural end.\n // Restore ownership only after its source has successfully started.\n if (this._state === \"stopped\" && !this.origin.playbacks.includes(this)) {\n this.origin._readmitPlayback(this);\n }\n super.emitPlayStarted(isResume);\n }\n\n pause(): void {\n if (!this.source || this._state !== \"playing\") {\n return;\n }\n\n const elapsed = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);\n this._offset = this.regionRelativeOffset(this._offset + elapsed);\n\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.pause();\n } else if (\"stop\" in this.source) {\n // For AudioBufferSourceNode and OscillatorNode, stop the source.\n // It cannot be restarted; a new one will be created on play().\n this.source.stop();\n }\n\n this.emitPaused();\n }\n\n stop(): void {\n if (!this.source) {\n throw new Error(\"Cannot stop a sound that has been cleaned up\");\n }\n const pendingMediaPlay = this._mediaPlayPromise !== undefined;\n this._mediaPlayPromise = undefined;\n if (!pendingMediaPlay && (this._state === \"stopped\" || this._state === \"unplayed\")) {\n return;\n }\n\n this.cancelFade();\n\n if (\"stop\" in this.source && this._state === \"playing\") {\n this.source.stop();\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.pause();\n this.source.mediaElement.currentTime = 0;\n }\n\n this._offset = 0;\n this._startTime = 0;\n this.emitStopped();\n }\n\n /**\n * Fades in from silence to the current volume.\n * Optionally stores config to re-trigger the fade on each loop iteration.\n * @param {number} duration - The fade duration in milliseconds.\n * @param {FadeType} type - The fade curve type. Defaults to \"linear\".\n * @param {object} options - Optional. Set perLoop: true to re-trigger fadeIn on each loop.\n * @returns {Promise<void>} Resolves when the fade completes.\n */\n fadeIn(duration: number, type?: FadeType, options?: { perLoop?: boolean; startTime?: number }): Promise<void> {\n if (options?.perLoop) {\n this._fadeInConfig = {\n duration,\n type: type ?? \"linear\",\n perLoop: true,\n targetVolume: this.gainNode!.gain.value,\n };\n }\n return super.fadeIn(duration, type, { startTime: options?.startTime });\n }\n\n /**\n * Configures a fade-out to be applied when the playback ends naturally (last loop iteration).\n * @param {number} duration - The fade-out duration in milliseconds.\n * @param {FadeType} type - The fade curve type. Defaults to \"linear\".\n */\n configureFadeOut(duration: number, type?: FadeType): void {\n this._fadeOutConfig = { duration, type: type ?? \"linear\" };\n }\n\n /**\n * Fades out then stops the playback.\n * @param {number} duration - The fade-out duration in milliseconds.\n * @param {FadeType} type - The fade curve type. Defaults to \"linear\".\n * @returns {Promise<void>} Resolves when the fade completes and playback is stopped.\n */\n stopWithFade(duration: number, type?: FadeType): Promise<void> {\n // Guard the chained stop: if an external stop()/cleanup() runs between\n // the fade resolving and this .then, the chained stop() would otherwise\n // throw against torn-down state (\"Cannot stop a sound that has been\n // cleaned up\"). cancelFade() inside stop()/cleanup() resolves the fade\n // promise synchronously, so this .then will still fire -- the guard\n // makes it a no-op when the playback is already stopped or cleaned up.\n return this.fadeOut(duration, type).then(() => {\n if (this._state !== \"stopped\" && this.source) {\n this.stop();\n }\n });\n }\n\n seek(time: number): void {\n if (!this.source || !this.gainNode || !this.panner) {\n throw new Error(\"Cannot seek a sound that has been cleaned up\");\n }\n if (!Number.isFinite(time) || time < 0 || (this.origin.region !== undefined && time > this.duration)) {\n throw new Error(\"Invalid time value for seek\");\n }\n\n if (\n this.origin.soundType === \"streaming\" &&\n \"mediaElement\" in this.source &&\n (this.source.mediaElement.seekable?.length ?? 0) === 0\n ) {\n if (this.source.mediaElement.duration === Number.POSITIVE_INFINITY) {\n throw new Error(\"Live media-element streams do not expose a seekable range\");\n }\n throw new Error(\"Media-element stream does not currently expose a seekable range\");\n }\n\n const wasPlaying = this._state === \"playing\";\n if (wasPlaying) {\n this.pause();\n }\n\n this._offset = time;\n\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.currentTime = time;\n }\n // For non-media elements, play() will handle recreating the source if needed.\n\n if (wasPlaying) {\n this.play();\n }\n this.emit(\"seek\", time);\n }\n\n get currentTime(): number {\n if (this._state === \"playing\") {\n const elapsed = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);\n const time = this._offset + elapsed;\n return this.regionRelativeOffset(time);\n } else {\n return this._offset;\n }\n }\n\n private removeFromOrigin(): void {\n const idx = this.origin.playbacks.indexOf(this);\n if (idx !== -1) {\n this.origin.playbacks.splice(idx, 1);\n }\n }\n\n private recreateSource() {\n if (!this.buffer || !this.panner || !this.context || !this.gainNode) {\n throw new Error(\"Cannot recreate source of a sound that has been cleaned up\");\n }\n try {\n if (this.source) {\n // It's crucial to nullify onended of the old source if it's an AudioBufferSourceNode (or similar non-restartable source),\n // as its onended event could otherwise interfere with the new source created for seek/resume.\n // MediaElementAudioSourceNode is handled differently as its underlying element can be paused/played.\n if (!(\"mediaElement\" in this.source) && \"onended\" in this.source) {\n this.source.onended = null;\n }\n this.source.disconnect();\n }\n this.source = this.context.createBufferSource();\n this.source.buffer = this.buffer;\n this.setEffectChainEndpoints(this.source, this.panner);\n this.source.onended = this.loopEnded;\n this.playbackRate = this._playbackRate;\n this.configureBufferLoopSource();\n } catch (error) {\n this.emitAsync(\"error\", {\n error: error as Error,\n errorType: \"source\",\n timestamp: Date.now(),\n recoverable: false,\n });\n throw error;\n }\n }\n\n /**\n * Sets whether the audio source should loop.\n * @throws {Error} Throws an error if the sound has been cleaned up.\n */\n set sourceLoop(loop: boolean) {\n if (!this.source) {\n throw new Error(\"Cannot set loop on a sound that has been cleaned up\");\n }\n if (\"loop\" in this.source) {\n this.source.loop = loop;\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.loop = loop;\n }\n }\n\n /**\n * Cleans up resources used by the Playback instance.\n * This method should be called when the audio is no longer needed to free up resources.\n */\n\n cleanup(): void {\n this._mediaPlayPromise = undefined;\n if (!this.source) {\n return; // Already cleaned up\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n const mediaElement = this.source.mediaElement;\n mediaElement.pause();\n try {\n mediaElement.currentTime = 0;\n } catch {\n // Live media without a seekable range can reject currentTime writes.\n }\n mediaElement.loop = false;\n mediaElement.onended = null;\n mediaElement.src = \"\";\n mediaElement.load();\n } else if (\"onended\" in this.source) {\n this.source.onended = null;\n }\n this._offset = 0;\n this._startTime = 0;\n this.markStopped();\n this.currentLoop = 0;\n this.source.disconnect();\n this.source = undefined;\n super.cleanup();\n }\n\n private assertNotCleanedUp(): void {\n if (!this.source || !this.gainNode || !this.panner) {\n throw new Error(\"Cannot perform operation on a sound that has been cleaned up\");\n }\n }\n\n addFilter(filter: BiquadFilterNode): void {\n this.assertNotCleanedUp();\n super.addFilter(filter);\n }\n\n removeFilter(filter: BiquadFilterNode): void {\n this.assertNotCleanedUp();\n super.removeFilter(filter);\n }\n\n /**\n * Sets or gets the loop count for the audio.\n * @param {LoopCount} loopCount - The number of times the audio should loop. 'infinite' for endless looping.\n * @returns {LoopCount} The loop count if no parameter is provided.\n * @throws {Error} Throws an error if the sound has been cleaned up or if the source type is unsupported.\n */\n\n loop(loopCount?: LoopCount): LoopCount {\n if (!this.source) {\n throw new Error(\"Cannot loop a sound that has been cleaned up\");\n }\n if (loopCount !== undefined) {\n const previousLoopCount = this.loopCount;\n const nextLoopCount = loopCount === \"infinite\" ? \"infinite\" : Math.max(0, loopCount);\n const changesInfiniteMode = (previousLoopCount === \"infinite\") !== (nextLoopCount === \"infinite\");\n if (\n changesInfiniteMode &&\n this.origin.region &&\n this._state === \"playing\" &&\n \"loop\" in this.source &&\n \"stop\" in this.source\n ) {\n const restartAt = Math.max(this._startTime, this.context.currentTime);\n const elapsed = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);\n this._offset = this.regionRelativeOffset(\n this.regionRelativeOffset(this._offset + elapsed, previousLoopCount),\n nextLoopCount,\n );\n if (\"onended\" in this.source) {\n this.source.onended = null;\n }\n this.source.stop();\n this.loopCount = nextLoopCount;\n this.currentLoop = 0;\n this._startTime = restartAt;\n this.recreateSource();\n this.startBufferSource(restartAt);\n return this.loopCount;\n }\n this.loopCount = nextLoopCount;\n this.currentLoop = 0;\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n const mediaElement = this.source.mediaElement;\n mediaElement.loop = this.loopCount === \"infinite\";\n } else if (\"loop\" in this.source) {\n this.configureBufferLoopSource();\n } else {\n throw new Error(\"Unsupported source type\");\n }\n return this.loopCount;\n }\n\n /** Copy loop flags and atlas bounds to the current buffer source without changing loop progress. */\n private configureBufferLoopSource(): void {\n if (!this.source || !(\"loop\" in this.source)) return;\n this.source.loop = this.loopCount === \"infinite\";\n if (this.source.buffer) {\n this.source.loopEnd = this.origin.region\n ? this.origin.region.start + this.origin.region.duration\n : this.source.buffer.duration;\n this.source.loopStart = this.origin.region?.start ?? 0;\n }\n }\n\n /** Clamp finite regions and wrap infinite regions while keeping offsets region-relative. */\n private regionRelativeOffset(offset: number, loopCount: LoopCount = this.loopCount): number {\n const region = this.origin.region;\n if (!region) {\n return offset;\n }\n return loopCount === \"infinite\" ? offset % region.duration : Math.min(region.duration, offset);\n }\n\n /** Start a buffer source, translating the region-relative offset once. */\n private startBufferSource(when: number): boolean {\n if (!this.source || !(\"start\" in this.source)) {\n return false;\n }\n const region = this.origin.region;\n if (!region) {\n this.source.start(when, this._offset);\n return true;\n }\n if (this._offset === region.duration) {\n return false;\n }\n const absoluteOffset = region.start + this._offset;\n if (this.loopCount === \"infinite\") {\n this.source.start(when, absoluteOffset);\n } else {\n this.source.start(when, absoluteOffset, region.duration - this._offset);\n }\n return true;\n }\n\n /**\n * Sets the pitch-shift factor for this playback, resurrecting the dormant\n * phase-vocoder worklet (Jean Laroche & Mark Dolson, \"New Phase-Vocoder\n * Techniques for Pitch-Shifting, Harmonizing and Other Exotic Effects\",\n * 1999 IEEE WASPAA — peak-based pitch shift with Identity Phase-Locking).\n *\n * On first use (factor !== 1) the phase-vocoder AudioWorkletNode is built and\n * added as an ordinary entry at the tail of the pre-panner effect chain:\n * source → [filters] → phase vocoder → panner → gainNode. The factor is\n * forwarded to the node's `pitchFactor` AudioParam (1 = no shift, 2 = +1\n * octave, 0.5 = -1 octave).\n *\n * @param factor Pitch multiplier (> 0).\n * @throws {Error} if the playback has been cleaned up or factor <= 0.\n */\n async setPitchShift(factor: number): Promise<void> {\n this.assertNotCleanedUp();\n if (!Number.isFinite(factor) || factor <= 0) {\n throw new Error(\"Pitch-shift factor must be greater than 0\");\n }\n this._pitchFactor = factor;\n const effectChain = this._effectChain;\n if (!effectChain) {\n throw new Error(\"Cannot pitch-shift a playback before its effect chain is initialized\");\n }\n let pitchShiftNode = effectChain.nodes.find((node) => {\n const parameters = (node as AudioWorkletNode).parameters;\n return parameters && typeof parameters.get === \"function\" && parameters.get(\"pitchFactor\") !== undefined;\n }) as AudioWorkletNode | undefined;\n\n // factor === 1 is the documented \"no shift\" contract and MUST be a genuine\n // passthrough. The peak/region phase-vocoder pipeline does NOT guarantee\n // identity for peakless / edge-bin / broadband content (it zero-fills and\n // only repopulates detected peak regions), so we cannot leave the node in\n // the chain at factor 1. Remove its ordinary chain entry so the signal\n // bypasses the worklet entirely (source → [filters] → panner). A later\n // non-unity factor rebuilds a fresh node.\n if (factor === 1) {\n if (pitchShiftNode) {\n effectChain.remove(pitchShiftNode);\n }\n return;\n }\n\n if (!pitchShiftNode) {\n const cacophony = this.origin.cacophony;\n if (!cacophony) {\n throw new Error(\"Cannot pitch-shift a playback whose Sound has no Cacophony instance\");\n }\n pitchShiftNode = await cacophony.buildWorkletEffect(WORKLETS.phaseVocoder, {}, this.context);\n effectChain.add(pitchShiftNode);\n }\n\n const pitchParam = pitchShiftNode.parameters?.get(\"pitchFactor\");\n if (pitchParam) {\n pitchParam.value = factor;\n }\n }\n\n /**\n * The current pitch-shift factor (1 = no shift). See {@link setPitchShift}.\n */\n get pitchShift(): number {\n return this._pitchFactor;\n }\n\n /**\n * Gets the output node of this playback's audio graph.\n * This is the final node in the internal chain before connection to destination.\n * Use this to manually wire the playback into custom audio graphs.\n *\n * @returns {GainNode} The gain node that serves as the output of this playback.\n * @throws {Error} Throws an error if the playback has been cleaned up.\n *\n * @example\n * // Manual routing through custom effects\n * const playback = sound.play()[0];\n * playback.disconnect(); // Disconnect from default destination\n * playback.connect(reverbNode).connect(context.destination);\n */\n get outputNode(): GainNode {\n if (!this.gainNode) {\n throw new Error(\"Cannot access output node of a playback that has been cleaned up\");\n }\n return this.gainNode;\n }\n\n /**\n * Connects this playback's output to an AudioNode or AudioParam.\n * Follows the Web Audio API connection pattern.\n *\n * @returns {AudioNode} The destination node (for chaining).\n * @throws {Error} Throws an error if the playback has been cleaned up.\n *\n * @example\n * // Chain multiple effects\n * playback.connect(delay).connect(reverb).connect(context.destination);\n */\n connect(destination: AudioNode | AudioParam): AudioNode {\n return this.outputNode.connect(destination as any);\n }\n\n /**\n * Disconnects this playback's output from a specific destination or from all destinations.\n *\n * @param {AudioNode | AudioParam} [destination] - Optional specific destination to disconnect from.\n * If omitted, disconnects from all destinations.\n * @throws {Error} Throws an error if the playback has been cleaned up.\n *\n * @example\n * // Disconnect from all\n * playback.disconnect();\n *\n * @example\n * // Disconnect from specific node\n * playback.disconnect(reverbNode);\n */\n disconnect(destination?: AudioNode | AudioParam): void {\n if (destination) {\n this.outputNode.disconnect(destination as any);\n } else {\n this.outputNode.disconnect();\n }\n }\n\n /**\n * Creates a clone of the current Playback instance with optional overrides for certain properties.\n * This method allows for the creation of a new Playback instance that shares the same audio context\n * and source node but can have different settings such as loop count or pan type.\n * The clone is connected to the origin Sound's primary route and registered with the Sound.\n * @throws {Error} Throws an error if the sound has been cleaned up.\n */\n\n clone(overrides: Partial<PlaybackCloneOverrides> = {}): Playback {\n if (!this.source || !this.gainNode || !this.context) {\n throw new Error(\"Cannot clone a sound that has been cleaned up\");\n }\n const panType = overrides.panType ?? this.panType;\n // we'll need to create a new gain node\n const gainNode = this.context.createGain();\n gainNode.connect(this.origin._resolveRouteTargetNode());\n // clone the source node\n let source: SourceNode;\n if (\"buffer\" in this.source && this.source.buffer) {\n source = this.context.createBufferSource();\n source.buffer = this.source.buffer;\n } else if (\"mediaElement\" in this.source && this.source.mediaElement) {\n if (!this.context.createMediaElementSource) {\n throw new Error(\"Media element sources are not supported on this audio context.\");\n }\n // The Web Audio spec forbids creating a second MediaElementAudioSourceNode\n // from the same HTMLMediaElement -- the second call throws\n // InvalidStateError. Clone the underlying element first so the new\n // playback owns an independent element that hasn't been bound yet.\n const originalElement = this.source.mediaElement;\n const clonedElement = originalElement.cloneNode(true) as HTMLMediaElement;\n source = this.context.createMediaElementSource(clonedElement);\n } else {\n throw new Error(\"Unsupported source type\");\n }\n const loopCount = overrides.loopCount !== undefined ? overrides.loopCount : this.loopCount;\n const clone = new Playback(this.origin, source, gainNode);\n\n // Copy all relevant properties\n clone.loopCount = loopCount;\n clone.currentLoop = this.currentLoop;\n clone.setPanType(panType, this.context);\n clone.volume = this.volume;\n clone.playbackRate = this._playbackRate;\n clone._offset = this._offset;\n if (this._fadeInConfig) {\n clone._fadeInConfig = { ...this._fadeInConfig };\n }\n if (this._fadeOutConfig) {\n clone._fadeOutConfig = { ...this._fadeOutConfig };\n }\n if (this._state === \"paused\") {\n clone.markPaused();\n } else if (this._state === \"stopped\") {\n clone.markStopped();\n }\n\n // Deep clone filters\n this._filters.forEach((filter) => {\n const clonedFilter = this.context.createBiquadFilter();\n clonedFilter.type = filter.type;\n clonedFilter.frequency.value = filter.frequency.value;\n clonedFilter.Q.value = filter.Q.value;\n clonedFilter.gain.value = filter.gain.value;\n clone.addFilter(clonedFilter as unknown as BiquadFilterNode);\n });\n\n if (this._pitchFactor !== 1) {\n void clone.setPitchShift(this._pitchFactor).catch(() => {\n /* worklet unavailable on this context — clone proceeds unshifted */\n });\n }\n\n // If the original is playing, start the clone\n if (this._state === \"playing\") {\n clone.play();\n }\n\n this.origin.playbacks.push(clone);\n return clone;\n }\n}\n"],"mappings":";;;;AAyCA,IAAa,IAAb,MAAa,UAAiB,EAAkC;CAgCrD;CA/BT;CAEA,YAAuB;CACvB,cAAsB;CACtB;CACA,UAA0B;CAC1B,aAA6B;CAC7B,gBAAgC;CAQhC;CACA;CACA;CACA;CAMA,eAA+B;CAM/B,YACE,GACA,GACA,GACA;EAYA,AAXA,MAAM,CAAM,GAJL,KAAA,SAAA,GAKP,KAAK,UAAU,EAAO,SACtB,KAAK,YAAY,EAAO,WACxB,KAAK,WAAW,EAAO,SAAS,EAAO,OAAO,GAC9C,KAAK,SAAS,GACV,YAAY,KAAU,EAAO,WAC/B,KAAK,SAAS,EAAO,SAEvB,KAAK,gBAAgB,CAAM,GAC3B,KAAK,YAAY,CAAQ,GACzB,KAAK,wBAAwB,KAAK,QAAQ,KAAK,MAAO,GACtD,KAAK,OAAQ,QAAQ,KAAK,QAAS;CACrC;CAEA,gBAAwB,GAAoB;EAC1C,IAAI,kBAAkB,KAAU,EAAO,cACrC,EAAO,aAAa,UAAU,KAAK;OAC9B,IAAI,aAAa,GACtB,EAAO,UAAU,KAAK;OAEtB,MAAU,MAAM,yBAAyB;CAE7C;CAQA,IAAI,WAAW;EACb,IAAI,KAAK,OAAO,QACd,OAAO,KAAK,OAAO,OAAO;EAE5B,IAAI,CAAC,KAAK,QACR,MAAU,MAAM,yDAAyD;EAQ3E,OANI,kBAAkB,KAAK,UAAU,KAAK,OAAO,eACxC,KAAK,OAAO,aAAa,WAE7B,KAAK,UAGH,KAAK,OAAO,YAFV;CAGX;CAMA,IAAI,eAAe;EACjB,OAAO,KAAK;CACd;CAEA,IAAI,SAAiB;EACnB,OAAO,MAAM;CACf;CAEA,IAAI,OAAO,GAAgB;EAEzB,AADA,MAAM,SAAS,GACf,KAAK,KAAK,gBAAgB,CAAM;CAClC;CAOA,IAAI,aAAa,GAAc;EAC7B,IAAI,KAAQ,GACV,MAAU,MAAM,sCAAsC;EAExD,IAAI,KAAK,WAAW,WAAW;GAC7B,IAAM,IAAU,KAAK,IAAI,IAAI,KAAK,QAAQ,cAAc,KAAK,cAAc,KAAK,aAAa;GAE7F,AADA,KAAK,UAAU,KAAK,qBAAqB,KAAK,UAAU,CAAO,GAC3D,KAAK,QAAQ,eAAe,KAAK,eACnC,KAAK,aAAa,KAAK,QAAQ;EAEnC;EACA,KAAK,gBAAgB,GAChB,KAAK,WAGN,kBAAkB,KAAK,WACzB,KAAK,OAAO,aAAa,QAAQ,IAE/B,kBAAkB,KAAK,UAAU,KAAK,OAAO,iBAC/C,KAAK,OAAO,aAAa,eAAe;CAE5C;CAOA,kBAAwB;EAClB,OAAC,KAAK,UAAU,KAAK,WAAW,YAQpC;OAAI,kBAAkB,KAAK,UAAU,KAAK,mBAAmB;IAC3D,KAAU,kBAAkB,WAAW,KAAK,cAAc,CAAC;IAC3D;GACF;GAEA,KAAK,cAAc;EAFnB;CAGF;CAEA,gBAA8B;EACxB,CAAC,KAAK,UAAU,KAAK,WAAW,cAIpC,KAAK,eAED,KAAK,cAAc,cAAc,KAAK,cAAc,KAAK,aAE3D,KAAK,KAAK,SAAS,KAAA,CAAS,GACxB,KAAK,iBAMP,KAAU,QAAQ,KAAK,eAAe,UAAU,KAAK,eAAe,IAAI,EAAE,WAAW;GACnF,AAAI,KAAK,WAAW,aAAa,KAAK,WACpC,KAAK,KAAK,GACV,KAAK,iBAAiB;EAE1B,CAAC,KAED,KAAK,KAAK,GACV,KAAK,iBAAiB,OAGxB,KAAK,mBAAmB,GACxB,KAAK,KAAK,CAAC,GAGP,KAAK,WAAW,aAAa,KAAK,UAAU,EAAE,kBAAkB,KAAK,WAGvE,KAAK,KAAK,GAIR,KAAK,eAAe,WAAW,KAAK,aACtC,KAAK,SAAS,KAAK,eAAe,MAAQ,KAAK,SAAS,QAAQ,WAAW,GAC3E,KAAK,OAAO,KAAK,cAAc,cAAc,KAAK,cAAc,UAAU,KAAK,cAAc,IAAI;CAGvG;CAQA,KAAK,GAA+B;EAClC,IAAI,CAAC,KAAK,QACR,MAAU,MAAM,8CAA8C;EAKhE,IAFA,EAAoB,GAAS,KAAK,SAAS,kBAAkB,KAAK,SAAS,UAAU,QAAQ,GAEzF,KAAK,WAAW,WAClB,OAAO,CAAC,IAAI;EASd,AANA,EAAiB,MAAM,KAAK,SAAS,CAAO,GACxC,GAAS,iBAAiB,KAAA,MAAW,KAAK,eAAe,EAAQ,eACjE,GAAS,cAAc,KAAA,KAAW,KAAK,KAAK,EAAQ,SAAS,GAC7D,GAAS,WAAW,KAAA,KACtB,KAAU,OAAO,EAAQ,QAAQ,EAAQ,UAAU;GAAE,SAAS,EAAQ;GAAe,WAAW,EAAQ;EAAG,CAAC,GAE1G,GAAS,YAAY,KAAA,KAAW,KAAK,iBAAiB,EAAQ,SAAS,EAAQ,QAAQ;EAE3F,IAAM,IAAW,KAAK,WAAW;EACjC,AAAI,KAAK,WAAW,cAAW,KAAK,cAAc;EAClD,IAAM,IAAmB,KAAK,OAAO;EAErC,IAAI;GACF,IAAI;GAEJ,IAAI,KAAK,WAAW,UAElB;QAAI,kBAAkB,KAAK,UAAU,KAAK,OAAO,cAC/C,IAAmB,KAAK,OAAO,aAAa,KAAK;SAIjD,IADA,KAAK,eAAe,GAChB,WAAW,KAAK,UACd,CAAC,KAAK,kBAAkB,GAAS,MAAM,CAAC,GAC1C,OAAO,CAAC,IAAI;GAGlB,OAIA,IAAI,kBAAkB,KAAK,UAAU,KAAK,OAAO,cAE/C,AADA,KAAK,OAAO,aAAa,cAAc,KAAK,SAC5C,IAAmB,KAAK,OAAO,aAAa,KAAK;QAC5C,IAAI,WAAW,KAAK,WACzB,KAAK,eAAe,GAChB,CAAC,KAAK,kBAAkB,GAAS,MAAM,CAAC,IAC1C,OAAO,CAAC,IAAI;GAKlB,IAAI,GAAkB;IAKpB,IAAM,IAAU,EACb,WACO;KACA,WAAK,sBAAsB,KAAW,CAAC,KAAK,SAChD;UAAI,KAAK,OAAO,wBAAwB,GAAkB;OACxD,AAAI,kBAAkB,KAAK,UAAQ,KAAK,OAAO,aAAa,MAAM;OAClE;MACF;MAEA,AADA,KAAK,aAAa,KAAK,QAAQ,aAC/B,KAAK,gBAAgB,CAAQ;KAF7B;IAGF,IACC,MAAiB;KACZ,KAAK,sBAAsB,KAAW,KAAK,OAAO,wBAAwB,KAC9E,KAAU,UAAU,SAAS;MAC3B;MACA,WAAW;MACX,WAAW,KAAK,IAAI;MACpB,aAAa;KACf,CAAC;IACH,CACF,EACC,cAAc;KACb,AAAI,KAAK,sBAAsB,MAC7B,KAAK,oBAAoB,KAAA;IAE7B,CAAC;IACH,KAAK,oBAAoB;GAC3B,OAGE,AADA,KAAK,aAAa,KAAK,IAAI,GAAS,MAAM,KAAK,QAAQ,aAAa,KAAK,QAAQ,WAAW,GAC5F,KAAK,gBAAgB,CAAQ;GAG/B,OAAO,CAAC,IAAI;EACd,SAAS,GAAO;GAOd,MANA,KAAK,UAAU,SAAS;IACf;IACP,WAAW;IACX,WAAW,KAAK,IAAI;IACpB,aAAa;GACf,CAAC,GACK;EACR;CACF;CAEA,gBAAmC,GAAyB;EAM1D,AAHI,KAAK,WAAW,aAAa,CAAC,KAAK,OAAO,UAAU,SAAS,IAAI,KACnE,KAAK,OAAO,iBAAiB,IAAI,GAEnC,MAAM,gBAAgB,CAAQ;CAChC;CAEA,QAAc;EACZ,IAAI,CAAC,KAAK,UAAU,KAAK,WAAW,WAClC;EAGF,IAAM,IAAU,KAAK,IAAI,IAAI,KAAK,QAAQ,cAAc,KAAK,cAAc,KAAK,aAAa;EAW7F,AAVA,KAAK,UAAU,KAAK,qBAAqB,KAAK,UAAU,CAAO,GAE3D,kBAAkB,KAAK,UAAU,KAAK,OAAO,eAC/C,KAAK,OAAO,aAAa,MAAM,IACtB,UAAU,KAAK,UAGxB,KAAK,OAAO,KAAK,GAGnB,KAAK,WAAW;CAClB;CAEA,OAAa;EACX,IAAI,CAAC,KAAK,QACR,MAAU,MAAM,8CAA8C;EAEhE,IAAM,IAAmB,KAAK,sBAAsB,KAAA;EACpD,KAAK,oBAAoB,KAAA,GACrB,GAAC,MAAqB,KAAK,WAAW,aAAa,KAAK,WAAW,iBAIvE,KAAK,WAAW,GAEZ,UAAU,KAAK,UAAU,KAAK,WAAW,aAC3C,KAAK,OAAO,KAAK,GAEf,kBAAkB,KAAK,UAAU,KAAK,OAAO,iBAC/C,KAAK,OAAO,aAAa,MAAM,GAC/B,KAAK,OAAO,aAAa,cAAc,IAGzC,KAAK,UAAU,GACf,KAAK,aAAa,GAClB,KAAK,YAAY;CACnB;CAUA,OAAO,GAAkB,GAAiB,GAAoE;EAS5G,OARI,GAAS,YACX,KAAK,gBAAgB;GACnB;GACA,MAAM,KAAQ;GACd,SAAS;GACT,cAAc,KAAK,SAAU,KAAK;EACpC,IAEK,MAAM,OAAO,GAAU,GAAM,EAAE,WAAW,GAAS,UAAU,CAAC;CACvE;CAOA,iBAAiB,GAAkB,GAAuB;EACxD,KAAK,iBAAiB;GAAE;GAAU,MAAM,KAAQ;EAAS;CAC3D;CAQA,aAAa,GAAkB,GAAgC;EAO7D,OAAO,KAAK,QAAQ,GAAU,CAAI,EAAE,WAAW;GAC7C,AAAI,KAAK,WAAW,aAAa,KAAK,UACpC,KAAK,KAAK;EAEd,CAAC;CACH;CAEA,KAAK,GAAoB;EACvB,IAAI,CAAC,KAAK,UAAU,CAAC,KAAK,YAAY,CAAC,KAAK,QAC1C,MAAU,MAAM,8CAA8C;EAEhE,IAAI,CAAC,OAAO,SAAS,CAAI,KAAK,IAAO,KAAM,KAAK,OAAO,WAAW,KAAA,KAAa,IAAO,KAAK,UACzF,MAAU,MAAM,6BAA6B;EAG/C,IACE,KAAK,OAAO,cAAc,eAC1B,kBAAkB,KAAK,WACtB,KAAK,OAAO,aAAa,UAAU,UAAU,OAAO,GAKrD,MAHI,KAAK,OAAO,aAAa,aAAa,WAC9B,MAAM,2DAA2D,IAEnE,MAAM,iEAAiE;EAGnF,IAAM,IAAa,KAAK,WAAW;EAenC,AAdI,KACF,KAAK,MAAM,GAGb,KAAK,UAAU,GAEX,kBAAkB,KAAK,UAAU,KAAK,OAAO,iBAC/C,KAAK,OAAO,aAAa,cAAc,IAIrC,KACF,KAAK,KAAK,GAEZ,KAAK,KAAK,QAAQ,CAAI;CACxB;CAEA,IAAI,cAAsB;EACxB,IAAI,KAAK,WAAW,WAAW;GAC7B,IAAM,IAAU,KAAK,IAAI,IAAI,KAAK,QAAQ,cAAc,KAAK,cAAc,KAAK,aAAa,GACvF,IAAO,KAAK,UAAU;GAC5B,OAAO,KAAK,qBAAqB,CAAI;EACvC,OACE,OAAO,KAAK;CAEhB;CAEA,mBAAiC;EAC/B,IAAM,IAAM,KAAK,OAAO,UAAU,QAAQ,IAAI;EAC9C,AAAI,MAAQ,MACV,KAAK,OAAO,UAAU,OAAO,GAAK,CAAC;CAEvC;CAEA,iBAAyB;EACvB,IAAI,CAAC,KAAK,UAAU,CAAC,KAAK,UAAU,CAAC,KAAK,WAAW,CAAC,KAAK,UACzD,MAAU,MAAM,4DAA4D;EAE9E,IAAI;GAeF,AAdI,KAAK,WAIH,EAAE,kBAAkB,KAAK,WAAW,aAAa,KAAK,WACxD,KAAK,OAAO,UAAU,OAExB,KAAK,OAAO,WAAW,IAEzB,KAAK,SAAS,KAAK,QAAQ,mBAAmB,GAC9C,KAAK,OAAO,SAAS,KAAK,QAC1B,KAAK,wBAAwB,KAAK,QAAQ,KAAK,MAAM,GACrD,KAAK,OAAO,UAAU,KAAK,WAC3B,KAAK,eAAe,KAAK,eACzB,KAAK,0BAA0B;EACjC,SAAS,GAAO;GAOd,MANA,KAAK,UAAU,SAAS;IACf;IACP,WAAW;IACX,WAAW,KAAK,IAAI;IACpB,aAAa;GACf,CAAC,GACK;EACR;CACF;CAMA,IAAI,WAAW,GAAe;EAC5B,IAAI,CAAC,KAAK,QACR,MAAU,MAAM,qDAAqD;EAKvE,AAHI,UAAU,KAAK,WACjB,KAAK,OAAO,OAAO,IAEjB,kBAAkB,KAAK,UAAU,KAAK,OAAO,iBAC/C,KAAK,OAAO,aAAa,OAAO;CAEpC;CAOA,UAAgB;EACd,SAAK,oBAAoB,KAAA,GACpB,KAAK,QAGV;OAAI,kBAAkB,KAAK,UAAU,KAAK,OAAO,cAAc;IAC7D,IAAM,IAAe,KAAK,OAAO;IACjC,EAAa,MAAM;IACnB,IAAI;KACF,EAAa,cAAc;IAC7B,QAAQ,CAER;IAIA,AAHA,EAAa,OAAO,IACpB,EAAa,UAAU,MACvB,EAAa,MAAM,IACnB,EAAa,KAAK;GACpB,OAAO,AAAI,aAAa,KAAK,WAC3B,KAAK,OAAO,UAAU;GAQxB,AANA,KAAK,UAAU,GACf,KAAK,aAAa,GAClB,KAAK,YAAY,GACjB,KAAK,cAAc,GACnB,KAAK,OAAO,WAAW,GACvB,KAAK,SAAS,KAAA,GACd,MAAM,QAAQ;EARU;CAS1B;CAEA,qBAAmC;EACjC,IAAI,CAAC,KAAK,UAAU,CAAC,KAAK,YAAY,CAAC,KAAK,QAC1C,MAAU,MAAM,8DAA8D;CAElF;CAEA,UAAU,GAAgC;EAExC,AADA,KAAK,mBAAmB,GACxB,MAAM,UAAU,CAAM;CACxB;CAEA,aAAa,GAAgC;EAE3C,AADA,KAAK,mBAAmB,GACxB,MAAM,aAAa,CAAM;CAC3B;CASA,KAAK,GAAkC;EACrC,IAAI,CAAC,KAAK,QACR,MAAU,MAAM,8CAA8C;EAEhE,IAAI,MAAc,KAAA,GAAW;GAC3B,IAAM,IAAoB,KAAK,WACzB,IAAgB,MAAc,aAAa,aAAa,KAAK,IAAI,GAAG,CAAS;GAEnF,IAD6B,MAAsB,eAAiB,MAAkB,eAGpF,KAAK,OAAO,UACZ,KAAK,WAAW,aAChB,UAAU,KAAK,UACf,UAAU,KAAK,QACf;IACA,IAAM,IAAY,KAAK,IAAI,KAAK,YAAY,KAAK,QAAQ,WAAW,GAC9D,IAAU,KAAK,IAAI,IAAI,KAAK,QAAQ,cAAc,KAAK,cAAc,KAAK,aAAa;IAc7F,OAbA,KAAK,UAAU,KAAK,qBAClB,KAAK,qBAAqB,KAAK,UAAU,GAAS,CAAiB,GACnE,CACF,GACI,aAAa,KAAK,WACpB,KAAK,OAAO,UAAU,OAExB,KAAK,OAAO,KAAK,GACjB,KAAK,YAAY,GACjB,KAAK,cAAc,GACnB,KAAK,aAAa,GAClB,KAAK,eAAe,GACpB,KAAK,kBAAkB,CAAS,GACzB,KAAK;GACd;GAEA,AADA,KAAK,YAAY,GACjB,KAAK,cAAc;EACrB;EACA,IAAI,kBAAkB,KAAK,UAAU,KAAK,OAAO,cAAc;GAC7D,IAAM,IAAe,KAAK,OAAO;GACjC,EAAa,OAAO,KAAK,cAAc;EACzC,OAAO,IAAI,UAAU,KAAK,QACxB,KAAK,0BAA0B;OAE/B,MAAU,MAAM,yBAAyB;EAE3C,OAAO,KAAK;CACd;CAGA,4BAA0C;EACpC,CAAC,KAAK,UAAU,EAAE,UAAU,KAAK,YACrC,KAAK,OAAO,OAAO,KAAK,cAAc,YAClC,KAAK,OAAO,WACd,KAAK,OAAO,UAAU,KAAK,OAAO,SAC9B,KAAK,OAAO,OAAO,QAAQ,KAAK,OAAO,OAAO,WAC9C,KAAK,OAAO,OAAO,UACvB,KAAK,OAAO,YAAY,KAAK,OAAO,QAAQ,SAAS;CAEzD;CAGA,qBAA6B,GAAgB,IAAuB,KAAK,WAAmB;EAC1F,IAAM,IAAS,KAAK,OAAO;EAI3B,OAHK,IAGE,MAAc,aAAa,IAAS,EAAO,WAAW,KAAK,IAAI,EAAO,UAAU,CAAM,IAFpF;CAGX;CAGA,kBAA0B,GAAuB;EAC/C,IAAI,CAAC,KAAK,UAAU,EAAE,WAAW,KAAK,SACpC,OAAO;EAET,IAAM,IAAS,KAAK,OAAO;EAC3B,IAAI,CAAC,GAEH,OADA,KAAK,OAAO,MAAM,GAAM,KAAK,OAAO,GAC7B;EAET,IAAI,KAAK,YAAY,EAAO,UAC1B,OAAO;EAET,IAAM,IAAiB,EAAO,QAAQ,KAAK;EAM3C,OALI,KAAK,cAAc,aACrB,KAAK,OAAO,MAAM,GAAM,CAAc,IAEtC,KAAK,OAAO,MAAM,GAAM,GAAgB,EAAO,WAAW,KAAK,OAAO,GAEjE;CACT;CAiBA,MAAM,cAAc,GAA+B;EAEjD,IADA,KAAK,mBAAmB,GACpB,CAAC,OAAO,SAAS,CAAM,KAAK,KAAU,GACxC,MAAU,MAAM,2CAA2C;EAE7D,KAAK,eAAe;EACpB,IAAM,IAAc,KAAK;EACzB,IAAI,CAAC,GACH,MAAU,MAAM,sEAAsE;EAExF,IAAI,IAAiB,EAAY,MAAM,MAAM,MAAS;GACpD,IAAM,IAAc,EAA0B;GAC9C,OAAO,KAAc,OAAO,EAAW,OAAQ,cAAc,EAAW,IAAI,aAAa,MAAM,KAAA;EACjG,CAAC;EASD,IAAI,MAAW,GAAG;GAChB,AAAI,KACF,EAAY,OAAO,CAAc;GAEnC;EACF;EAEA,IAAI,CAAC,GAAgB;GACnB,IAAM,IAAY,KAAK,OAAO;GAC9B,IAAI,CAAC,GACH,MAAU,MAAM,qEAAqE;GAGvF,AADA,IAAiB,MAAM,EAAU,mBAAmB,EAAS,cAAc,CAAC,GAAG,KAAK,OAAO,GAC3F,EAAY,IAAI,CAAc;EAChC;EAEA,IAAM,IAAa,EAAe,YAAY,IAAI,aAAa;EAC/D,AAAI,MACF,EAAW,QAAQ;CAEvB;CAKA,IAAI,aAAqB;EACvB,OAAO,KAAK;CACd;CAgBA,IAAI,aAAuB;EACzB,IAAI,CAAC,KAAK,UACR,MAAU,MAAM,kEAAkE;EAEpF,OAAO,KAAK;CACd;CAaA,QAAQ,GAAgD;EACtD,OAAO,KAAK,WAAW,QAAQ,CAAkB;CACnD;CAiBA,WAAW,GAA4C;EACrD,AAAI,IACF,KAAK,WAAW,WAAW,CAAkB,IAE7C,KAAK,WAAW,WAAW;CAE/B;CAUA,MAAM,IAA6C,CAAC,GAAa;EAC/D,IAAI,CAAC,KAAK,UAAU,CAAC,KAAK,YAAY,CAAC,KAAK,SAC1C,MAAU,MAAM,+CAA+C;EAEjE,IAAM,IAAU,EAAU,WAAW,KAAK,SAEpC,IAAW,KAAK,QAAQ,WAAW;EACzC,EAAS,QAAQ,KAAK,OAAO,wBAAwB,CAAC;EAEtD,IAAI;EACJ,IAAI,YAAY,KAAK,UAAU,KAAK,OAAO,QAEzC,AADA,IAAS,KAAK,QAAQ,mBAAmB,GACzC,EAAO,SAAS,KAAK,OAAO;OACvB,IAAI,kBAAkB,KAAK,UAAU,KAAK,OAAO,cAAc;GACpE,IAAI,CAAC,KAAK,QAAQ,0BAChB,MAAU,MAAM,gEAAgE;GAOlF,IAAM,IADkB,KAAK,OAAO,aACE,UAAU,EAAI;GACpD,IAAS,KAAK,QAAQ,yBAAyB,CAAa;EAC9D,OACE,MAAU,MAAM,yBAAyB;EAE3C,IAAM,IAAY,EAAU,cAAc,KAAA,IAAkC,KAAK,YAA3B,EAAU,WAC1D,IAAQ,IAAI,EAAS,KAAK,QAAQ,GAAQ,CAAQ;EA2CxD,OAxCA,EAAM,YAAY,GAClB,EAAM,cAAc,KAAK,aACzB,EAAM,WAAW,GAAS,KAAK,OAAO,GACtC,EAAM,SAAS,KAAK,QACpB,EAAM,eAAe,KAAK,eAC1B,EAAM,UAAU,KAAK,SACjB,KAAK,kBACP,EAAM,gBAAgB,EAAE,GAAG,KAAK,cAAc,IAE5C,KAAK,mBACP,EAAM,iBAAiB,EAAE,GAAG,KAAK,eAAe,IAE9C,KAAK,WAAW,WAClB,EAAM,WAAW,IACR,KAAK,WAAW,aACzB,EAAM,YAAY,GAIpB,KAAK,SAAS,SAAS,MAAW;GAChC,IAAM,IAAe,KAAK,QAAQ,mBAAmB;GAKrD,AAJA,EAAa,OAAO,EAAO,MAC3B,EAAa,UAAU,QAAQ,EAAO,UAAU,OAChD,EAAa,EAAE,QAAQ,EAAO,EAAE,OAChC,EAAa,KAAK,QAAQ,EAAO,KAAK,OACtC,EAAM,UAAU,CAA2C;EAC7D,CAAC,GAEG,KAAK,iBAAiB,KACxB,EAAW,cAAc,KAAK,YAAY,EAAE,YAAY,CAExD,CAAC,GAIC,KAAK,WAAW,aAClB,EAAM,KAAK,GAGb,KAAK,OAAO,UAAU,KAAK,CAAK,GACzB;CACT;AACF"}
|
|
@@ -20,7 +20,7 @@ writes. Legacy v1 entries are not read. Memory is bounded per decoding context.<
|
|
|
20
20
|
<ul class="tsd-hierarchy">
|
|
21
21
|
<li><span class="tsd-signature-type">ICache</span></li></ul></section><aside class="tsd-sources">
|
|
22
22
|
<ul>
|
|
23
|
-
<li>Defined in <a href="https://github.com/ctoth/cacophony/blob/
|
|
23
|
+
<li>Defined in <a href="https://github.com/ctoth/cacophony/blob/0986e1b3c5ad05ed2dade5582ffad96844f341b5/src/cache.ts#L132">src/cache.ts:132</a></li></ul></aside>
|
|
24
24
|
<section class="tsd-panel-group tsd-index-group">
|
|
25
25
|
<section class="tsd-panel tsd-index-panel">
|
|
26
26
|
<details class="tsd-index-content tsd-accordion" open><summary class="tsd-accordion-summary tsd-index-summary"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-chevronDown"></use></svg>
|
|
@@ -58,7 +58,7 @@ writes. Legacy v1 entries are not read. Memory is bounded per decoding context.<
|
|
|
58
58
|
<h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">void</span></h4><aside class="tsd-sources">
|
|
59
59
|
<p>Implementation of ICache.clearMemoryCache</p>
|
|
60
60
|
<ul>
|
|
61
|
-
<li>Defined in <a href="https://github.com/ctoth/cacophony/blob/
|
|
61
|
+
<li>Defined in <a href="https://github.com/ctoth/cacophony/blob/0986e1b3c5ad05ed2dade5582ffad96844f341b5/src/cache.ts#L621">src/cache.ts:621</a></li></ul></aside></div></li></ul></section>
|
|
62
62
|
<section class="tsd-panel tsd-member">
|
|
63
63
|
<h3 class="tsd-anchor-link" id="getaudiobuffer"><span>get<wbr/>Audio<wbr/>Buffer</span><a href="#getaudiobuffer" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="../assets/icons.svg#icon-anchor"></use></svg></a></h3>
|
|
64
64
|
<ul class="tsd-signatures">
|
|
@@ -75,7 +75,7 @@ writes. Legacy v1 entries are not read. Memory is bounded per decoding context.<
|
|
|
75
75
|
<h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">Promise</span><span class="tsd-signature-symbol"><</span><span class="tsd-signature-type">AudioBuffer</span><span class="tsd-signature-symbol">></span></h4><aside class="tsd-sources">
|
|
76
76
|
<p>Implementation of ICache.getAudioBuffer</p>
|
|
77
77
|
<ul>
|
|
78
|
-
<li>Defined in <a href="https://github.com/ctoth/cacophony/blob/
|
|
78
|
+
<li>Defined in <a href="https://github.com/ctoth/cacophony/blob/0986e1b3c5ad05ed2dade5582ffad96844f341b5/src/cache.ts#L580">src/cache.ts:580</a></li></ul></aside></div></li></ul></section>
|
|
79
79
|
<section class="tsd-panel tsd-member">
|
|
80
80
|
<h3 class="tsd-anchor-link" id="setcacheexpirationtime"><code class="tsd-tag">Static</code><span>set<wbr/>Cache<wbr/>Expiration<wbr/>Time</span><a href="#setcacheexpirationtime" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="../assets/icons.svg#icon-anchor"></use></svg></a></h3>
|
|
81
81
|
<ul class="tsd-signatures">
|
|
@@ -95,7 +95,7 @@ Existing memory and persistent policies retain their original lifetime.</p>
|
|
|
95
95
|
<h4 class="tsd-anchor-link" id="throws">Throws<a href="#throws" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="../assets/icons.svg#icon-anchor"></use></svg></a></h4><p>If time is negative or non-finite.</p>
|
|
96
96
|
</div></div><aside class="tsd-sources">
|
|
97
97
|
<ul>
|
|
98
|
-
<li>Defined in <a href="https://github.com/ctoth/cacophony/blob/
|
|
98
|
+
<li>Defined in <a href="https://github.com/ctoth/cacophony/blob/0986e1b3c5ad05ed2dade5582ffad96844f341b5/src/cache.ts#L143">src/cache.ts:143</a></li></ul></aside></div></li></ul></section></section></details></div>
|
|
99
99
|
<div class="col-sidebar">
|
|
100
100
|
<div class="page-menu">
|
|
101
101
|
<div class="tsd-navigation settings">
|