cacophony 0.36.0 → 0.37.0
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/CHANGELOG.md +11 -1
- package/README.md +39 -4
- package/dist/playback.cjs +1 -1
- package/dist/playback.cjs.map +1 -1
- package/dist/playback.d.ts +25 -0
- package/dist/playback.mjs +92 -17
- package/dist/playback.mjs.map +1 -1
- package/dist/sound.cjs +1 -1
- package/dist/sound.cjs.map +1 -1
- package/dist/sound.d.ts +2 -0
- package/dist/sound.mjs +6 -1
- package/dist/sound.mjs.map +1 -1
- package/docs/assets/hierarchy.js +1 -1
- package/docs/assets/search.js +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 +92 -92
- 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 +33 -33
- 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 +70 -70
- package/docs/classes/MediaStreamSound.html +51 -51
- package/docs/classes/MicrophoneStream.html +52 -52
- package/docs/classes/ModulatedDelayEffect.html +6 -6
- package/docs/classes/PcmStreamPlayback.html +70 -70
- package/docs/classes/PcmStreamSound.html +61 -61
- package/docs/classes/PhaserEffect.html +6 -6
- package/docs/classes/Playback.html +160 -89
- 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 +85 -73
- package/docs/classes/SpectralFreezeEffect.html +6 -6
- package/docs/classes/StereoWidenerEffect.html +3 -3
- package/docs/classes/Synth.html +63 -63
- 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/hierarchy.html +2 -2
- package/docs/index.html +22 -0
- 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 +7 -7
- 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/DeviceChangeEvent.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/PrepareSeekOptions.html +3 -3
- package/docs/interfaces/ReverbOptions.html +13 -13
- package/docs/interfaces/RuntimeOptions.html +8 -8
- package/docs/interfaces/ScheduledCallbackHandle.html +2 -2
- package/docs/interfaces/SendGainOptions.html +3 -3
- 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/AudioSinkId.html +1 -1
- 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 +20 -20
- 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 +6 -6
- 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 +1 -1
- 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/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,15 @@ someone upgrading, not as a commit message. `npm version` stamps the release; se
|
|
|
17
17
|
|
|
18
18
|
## [Unreleased]
|
|
19
19
|
|
|
20
|
+
## [0.37.0] - 2026-10-18
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- Buffer `Playback.setLoopRegion(L, R)` and `scheduleStop(atAudioSeconds)` keep clip loops and finite repeated playback on the native audio clock, without polling past R or restarting every pass. `Sound.supportsClipLoop` and `Playback.supportsClipLoop` identify decoded-buffer support; HTML media rejects these exact-boundary operations. Explicit clips accept zero or infinite loop mode, preserve bounds through pause/resume, and clone from their folded live position; an armed absolute end cannot be cloned.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- Playing HTML media now reports its actual source position through `Playback.currentTime`, including network buffering stalls and native loop wraps, so synchronization can detect real drift.
|
|
27
|
+
- Native whole-buffer infinite playback now reports a wrapped cursor consistently through rate changes and pause/resume.
|
|
28
|
+
|
|
20
29
|
## [0.36.0] - 2026-10-18
|
|
21
30
|
|
|
22
31
|
### Added
|
|
@@ -1442,7 +1451,8 @@ _0.1.0–0.1.2 were published to npm but not tagged; this block covers everythin
|
|
|
1442
1451
|
- Sounds loaded from URLs are cached with the browser Cache API (`CacheManager`), and concurrent loads of the same URL are de-duplicated.
|
|
1443
1452
|
- `Playback.fadeIn()` and `Playback.fadeOut()` with `'linear'` or `'exponential'` fades.
|
|
1444
1453
|
|
|
1445
|
-
[Unreleased]: https://github.com/ctoth/cacophony/compare/v0.
|
|
1454
|
+
[Unreleased]: https://github.com/ctoth/cacophony/compare/v0.37.0...HEAD
|
|
1455
|
+
[0.37.0]: https://github.com/ctoth/cacophony/compare/v0.36.0...v0.37.0
|
|
1446
1456
|
[0.36.0]: https://github.com/ctoth/cacophony/compare/v0.35.0...v0.36.0
|
|
1447
1457
|
[0.35.0]: https://github.com/ctoth/cacophony/compare/v0.34.0...v0.35.0
|
|
1448
1458
|
[0.34.0]: https://github.com/ctoth/cacophony/compare/v0.33.0...v0.34.0
|
package/README.md
CHANGED
|
@@ -478,10 +478,45 @@ sound.loop(3);
|
|
|
478
478
|
sound.play();
|
|
479
479
|
|
|
480
480
|
// No looping (default)
|
|
481
|
-
sound.loop(0);
|
|
482
|
-
```
|
|
483
|
-
|
|
484
|
-
|
|
481
|
+
sound.loop(0);
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
Decoded buffer voices can enforce clip boundaries on the audio render clock:
|
|
485
|
+
|
|
486
|
+
```typescript
|
|
487
|
+
const sound = await cacophony.createSound('loop.wav', 'buffer');
|
|
488
|
+
if (!sound.supportsClipLoop) throw new Error('A decoded buffer is required');
|
|
489
|
+
const [voice] = sound.preplay();
|
|
490
|
+
voice.setLoopRegion(1, 3); // Sound-relative seconds: L=1, R=3
|
|
491
|
+
voice.loop('infinite'); // Native, continuous R-to-L wrapping
|
|
492
|
+
await voice.prepareSeek(1);
|
|
493
|
+
const at = cacophony.context.currentTime + 0.25;
|
|
494
|
+
voice.play({ at });
|
|
495
|
+
voice.scheduleStop(at + 6); // Three passes at rate1; ends at the final R
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
Configure bounds while idle or paused, before seeking. They must span at least
|
|
499
|
+
one decoded sample within the Sound duration. Seeking to or beyond R rejects;
|
|
500
|
+
an offset before L plays the prefix once. With explicit bounds, `loop(0)` plays
|
|
501
|
+
once and ends at R; `'infinite'` wraps natively. Numeric repeat counts and
|
|
502
|
+
per-loop fades reject. For finite repeated clips use native infinite looping
|
|
503
|
+
and one `scheduleStop()` deadline, as above. Sprite bounds remain Sound-relative.
|
|
504
|
+
|
|
505
|
+
`scheduleStop()` accepts an absolute AudioContext time after `play()`, including
|
|
506
|
+
a future scheduled start. Stopping before that onset prevents all onset audio;
|
|
507
|
+
a past deadline stops now. Changing playback rate leaves the absolute deadline
|
|
508
|
+
unchanged. Calling it again replaces the deadline on the same source. Pause,
|
|
509
|
+
seek, source recreation, stop and cleanup revoke it; arm it again after resume.
|
|
510
|
+
Legacy numeric repeats and ended-time fade-outs cannot share a scheduled end.
|
|
511
|
+
It cuts the source; downstream effects retain their ordinary tails.
|
|
512
|
+
|
|
513
|
+
HTML media does not support these exact bounds or scheduled ends and throws
|
|
514
|
+
`NotSupportedError`. `supportsClipLoop` checks the actual Sound/Playback buffer
|
|
515
|
+
representation; it does not fetch or decode a URL. A configured clip clone
|
|
516
|
+
copies its bounds and folded live position, while cloning an armed absolute end
|
|
517
|
+
rejects before allocation. Replace the voice to remove its explicit bounds.
|
|
518
|
+
|
|
519
|
+
## Volume Control
|
|
485
520
|
|
|
486
521
|
Cacophony provides a hierarchical volume control system:
|
|
487
522
|
|
package/dist/playback.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./worklets.cjs"),t=require("./playOptions.cjs"),n=require("./spatialAutomation.cjs"),r=require("./basePlayback.cjs"),i=require("./occlusion.cjs");var a=class a extends r.BasePlayback{origin;context;loopCount=0;currentLoop=0;buffer;_offset=0;_startTime=0;_playbackRate=1;_mediaPlayPromise;_cancelPrepareSeek;_mediaPreparationGate;_fadeInConfig;_fadeOutConfig;_loopEndCallback;_pitchFactor=1;_occlusion;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 occlusion(){return this._occlusion?.amount??0}setOcclusion(e,t=50){if(this.assertNotCleanedUp(),!Number.isFinite(e))throw RangeError(`Occlusion amount must be finite`);if(!Number.isFinite(t)||t<0)throw RangeError(`Occlusion duration must be finite and non-negative`);let n=Math.max(0,Math.min(1,e));if(!this._occlusion){if(n===0)return;this._occlusion=new i.Occlusion(this.context,this.panner),this.setEffectChainEndpoints(this.source,this.panner)}this._occlusion.setAmount(n,t)}setEffectChainEndpoints(e,t){this._occlusion?.setOutput(t),super.setEffectChainEndpoints(this._mediaPreparationGate??e,this._occlusion?.input??t)}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(t.validatePlayOptions(e,this.panType,`mediaElement`in this.source?`media`:`buffer`),this._state===`playing`)return[this];this._cancelPrepareSeek?.(),t.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 n=this._state===`paused`;this._state===`stopped`&&(this.currentLoop=0);let r=this.origin._playbackGeneration;try{let t;if(this._state===`paused`){if(`mediaElement`in this.source&&this.source.mediaElement)this.source.mediaElement.currentTime!==this._offset&&(this.source.mediaElement.currentTime=this._offset),t=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&&(this.source.mediaElement.currentTime=this._offset),t=this.source.mediaElement.play();else if(`start`in this.source&&(this.recreateSource(),!this.startBufferSource(e?.at??0)))return[this];if(t){let e=t.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(n)}},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(n);return[this]}catch(e){throw this.emitAsync(`error`,{error:e,errorType:`source`,timestamp:Date.now(),recoverable:!0}),e}}emitPlayStarted(e){this._mediaPreparationGate?.gain.setValueAtTime(1,this.context.currentTime),this._state===`stopped`&&!this.origin.playbacks.includes(this)&&this.origin._readmitPlayback(this),super.emitPlayStarted(e)}pause(){if(!(!this.source||this._state!==`playing`)){if(this._mediaPlayPromise=void 0,`mediaElement`in this.source&&this.source.mediaElement)this._mediaPreparationGate?.gain.setValueAtTime(0,this.context.currentTime),this.source.mediaElement.pause(),this._offset=this.source.mediaElement.currentTime;else{let e=Math.max(0,(this.context.currentTime-this._startTime)*this._playbackRate);this._offset=this.regionRelativeOffset(this._offset+e),`stop`in this.source&&this.source.stop()}this.emitPaused()}}stop(){if(!this.source)throw Error(`Cannot stop a sound that has been cleaned up`);this._cancelPrepareSeek?.(),this._mediaPreparationGate?.gain.setValueAtTime(0,this.context.currentTime);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()})}async prepareSeek(e,t={}){if(this.assertNotCleanedUp(),!Number.isFinite(e)||e<0)throw RangeError(`Seek time must be finite and non-negative`);let n=t.timeoutMs??1e4;if(!Number.isFinite(n)||n<=0||n>2147483647)throw RangeError(`Seek timeout must be positive and at most 2147483647 milliseconds`);if(t.signal?.aborted)throw new DOMException(`Seek preparation aborted`,`AbortError`);if(this._state===`playing`||this._mediaPlayPromise)throw Error(`Pause playback before preparing a seek`);this._cancelPrepareSeek?.();let r=this.source;if(!(`mediaElement`in r)){if(!Number.isFinite(this.duration)||e>this.duration)throw RangeError(`Seek time exceeds duration`);this._offset=e,this.emit(`seek`,e);return}let i=r.mediaElement;if(!i.paused)throw Error(`Pause playback before preparing a seek`);if(!this._mediaPreparationGate){let e=this.context.createGain();e.gain.value=0,this._mediaPreparationGate=e,this.setEffectChainEndpoints(r,this.panner),r.connect(e)}this._mediaPreparationGate.gain.setValueAtTime(0,this.context.currentTime),await new Promise((r,a)=>{let o=!1,s=!1,c=!1,l=[`loadedmetadata`,`durationchange`,`progress`,`loadeddata`,`canplay`,`seeked`,`error`,`emptied`,`play`],u=n=>{if(!o){o=!0,clearTimeout(p);for(let e of l)i.removeEventListener(e,f);t.signal?.removeEventListener(`abort`,d),this._cancelPrepareSeek===d&&(this._cancelPrepareSeek=void 0),n===void 0?(this._offset=e,r()):a(n)}},d=()=>u(new DOMException(`Seek preparation aborted`,`AbortError`)),f=t=>{if(!o)try{if(i.error||t?.type===`error`){u(Error(i.error?.message||`Media failed during seek preparation`));return}if(t?.type===`emptied`&&s||t?.type===`play`){d();return}if(i.readyState<1)return;if(!Number.isFinite(i.duration)){u(new DOMException(`Seek preparation requires finite-duration media`,`NotSupportedError`));return}if(e>i.duration){u(RangeError(`Seek time exceeds duration`));return}if(s)t?.type===`seeked`&&(c=!1);else{let t=!1;for(let n=0;n<i.seekable.length;n++)e>=i.seekable.start(n)&&e<=i.seekable.end(n)&&(t=!0);if(!t)return;s=!0,c=i.seeking||i.currentTime!==e,i.currentTime!==e&&(i.currentTime=e)}!c&&!i.seeking&&i.readyState>=2&&Math.abs(i.currentTime-e)<.001&&u()}catch(e){u(e)}},p=setTimeout(()=>u(new DOMException(`Seek preparation timed out`,`TimeoutError`)),n);this._cancelPrepareSeek=d;for(let e of l)i.addEventListener(e,f);t.signal?.addEventListener(`abort`,d,{once:!0}),f()}),this.emit(`seek`,e)}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._cancelPrepareSeek?.(),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._cancelPrepareSeek?.(),this._mediaPreparationGate?.gain.setValueAtTime(0,this.context.currentTime),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,`stop`in this.source&&this._state===`playing`&&this.source.stop());this._offset=0,this._startTime=0,this.markStopped(),this.currentLoop=0,this.source.disconnect(),this.source=void 0,super.cleanup(),this._mediaPreparationGate?.disconnect(),this._mediaPreparationGate=void 0,this._occlusion?.destroy(),this._occlusion=void 0}}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`);n.validateSpatialSmoothingTau(e.spatialSmoothingTau??this.spatialSmoothingTau);let r=e.panType??this.panType;t.validateSpatialOptions(e,r);let i=this.context.createGain();i.connect(this.origin._resolveRouteTargetNode());let o;if(`buffer`in this.source&&this.source.buffer)o=this.context.createBufferSource(),o.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);o=this.context.createMediaElementSource(e)}else throw Error(`Unsupported source type`);let s=e.loopCount===void 0?this.loopCount:e.loopCount,c=new a(this.origin,o,i);return c.loopCount=s,c.currentLoop=this.currentLoop,c.setPanType(r,this.context),c.spatialSmoothingTau=e.spatialSmoothingTau??this.spatialSmoothingTau,r===`HRTF`?(this.panType===`HRTF`&&(c.threeDOptions=this.threeDOptions),e.threeDOptions!==void 0&&(c.threeDOptions=e.threeDOptions),e.position!==void 0&&(c.position=e.position)):c.stereoPan=e.stereoPan??this.stereoPan??0,c.volume=this.volume,c.playbackRate=this._playbackRate,c._offset=this._offset,c.setOcclusion(this.occlusion,0),this._fadeInConfig&&(c._fadeInConfig={...this._fadeInConfig}),this._fadeOutConfig&&(c._fadeOutConfig={...this._fadeOutConfig}),this._state===`paused`?c.markPaused():this._state===`stopped`&&c.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,c.addFilter(t)}),this._pitchFactor!==1&&c.setPitchShift(this._pitchFactor).catch(()=>{}),this._state===`playing`&&c.play(),this.origin.playbacks.push(c),c}};exports.Playback=a;
|
|
1
|
+
const e=require("./worklets.cjs"),t=require("./playOptions.cjs"),n=require("./spatialAutomation.cjs"),r=require("./basePlayback.cjs"),i=require("./occlusion.cjs");var a=class a extends r.BasePlayback{origin;context;loopCount=0;currentLoop=0;buffer;_offset=0;_startTime=0;_playbackRate=1;_clipRegion;_scheduledStop;_mediaPlayPromise;_cancelPrepareSeek;_mediaPreparationGate;_fadeInConfig;_fadeOutConfig;_loopEndCallback;_pitchFactor=1;_occlusion;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.source===e&&this.loopEnded()};else throw Error(`Unsupported source type`)}get occlusion(){return this._occlusion?.amount??0}setOcclusion(e,t=50){if(this.assertNotCleanedUp(),!Number.isFinite(e))throw RangeError(`Occlusion amount must be finite`);if(!Number.isFinite(t)||t<0)throw RangeError(`Occlusion duration must be finite and non-negative`);let n=Math.max(0,Math.min(1,e));if(!this._occlusion){if(n===0)return;this._occlusion=new i.Occlusion(this.context,this.panner),this.setEffectChainEndpoints(this.source,this.panner)}this._occlusion.setAmount(n,t)}setEffectChainEndpoints(e,t){this._occlusion?.setOutput(t),super.setEffectChainEndpoints(this._mediaPreparationGate??e,this._occlusion?.input??t)}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 supportsClipLoop(){return!!this.source&&`buffer`in this.source&&!!this.buffer}setLoopRegion(e,t){if(this.assertNotCleanedUp(),!this.supportsClipLoop)throw new DOMException(`Clip bounds require a decoded buffer`,`NotSupportedError`);if(this._state===`playing`)throw Error(`Pause playback before configuring clip bounds`);if(!this.buffer||!Number.isFinite(e)||!Number.isFinite(t)||e<0||t>this.duration||(t-e)*this.buffer.sampleRate+2**-52*Math.max(1,Math.abs(e*this.buffer.sampleRate),Math.abs(t*this.buffer.sampleRate))*4<1||Math.round(t*this.buffer.sampleRate)<=Math.round(e*this.buffer.sampleRate))throw RangeError(`Clip bounds must span at least one sample within the Sound duration`);if(this._offset>=t)throw RangeError(`The current offset must precede the clip end`);if(this.loopCount!==0&&this.loopCount!==`infinite`)throw Error(`Clip bounds support only zero or infinite loop count`);if(this._fadeInConfig?.perLoop)throw Error(`Per-loop fades are not supported with clip bounds`);this._clipRegion={start:e,end:t},this.configureBufferLoopSource()}scheduleStop(e){if(this.assertNotCleanedUp(),!this.supportsClipLoop||!this.source||!(`stop`in this.source))throw new DOMException(`Scheduled ends require a decoded buffer`,`NotSupportedError`);if(!Number.isFinite(e)||e<0)throw RangeError(`End time must be finite and non-negative`);if(this._state!==`playing`)throw Error(`Start the buffer before scheduling its end`);if(this.loopCount!==0&&this.loopCount!==`infinite`)throw Error(`Scheduled ends do not support legacy numeric repeats`);if(this._fadeOutConfig)throw Error(`An absolute end cannot use an ended-time fade-out`);let t=Math.max(this.context.currentTime,e);this.source.stop(t),this._scheduledStop={source:this.source,at:t}}_validateLoopCount(e){if(this._clipRegion&&e!==0&&e!==`infinite`)throw Error(`Clip bounds support only zero or infinite loop count`);if(this._scheduledStop&&e!==0&&e!==`infinite`)throw Error(`Scheduled ends do not support legacy numeric repeats`)}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(this._scheduledStop?.source===this.source){let e=this.source;this._scheduledStop=void 0,`onended`in e&&(e.onended=null);try{this.emit(`ended`,void 0)}finally{this.source===e&&this._state===`playing`&&this.stop(),this.source===e&&this.state===`stopped`&&this.removeFromOrigin()}return}if(`mediaElement`in this.source&&this._mediaPlayPromise){this._mediaPlayPromise.then(()=>this._runLoopEnded());return}this._runLoopEnded()}};_runLoopEnded(){if(!this.source||this._state!==`playing`)return;let e=this.source;if(this.currentLoop++,this.loopCount!==`infinite`&&this.currentLoop>this.loopCount){if(this.emit(`ended`,void 0),this.source!==e||this._state!==`playing`)return;this._fadeOutConfig?this.fadeOut(this._fadeOutConfig.duration,this._fadeOutConfig.type).then(()=>{this._state!==`stopped`&&this.source===e&&(this.stop(),this.source===e&&this.state===`stopped`&&this.removeFromOrigin())}):(this.stop(),this.source===e&&this.state===`stopped`&&this.removeFromOrigin())}else{if(this._loopEndCallback?.(),this.source!==e||this._state!==`playing`)return;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(t.validatePlayOptions(e,this.panType,`mediaElement`in this.source?`media`:`buffer`),e?.loopCount!==void 0&&this._validateLoopCount(e.loopCount),this._clipRegion&&e?.fadeInPerLoop)throw Error(`Per-loop fades are not supported with clip bounds`);if(this._scheduledStop&&e?.fadeOut!==void 0)throw Error(`An absolute end cannot use an ended-time fade-out`);if(this._state===`playing`)return[this];this._cancelPrepareSeek?.(),this._scheduledStop=void 0,t.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 n=this._state===`paused`;this._state===`stopped`&&(this.currentLoop=0);let r=this.origin._playbackGeneration;try{let t;if(this._state===`paused`){if(`mediaElement`in this.source&&this.source.mediaElement)this.source.mediaElement.currentTime!==this._offset&&(this.source.mediaElement.currentTime=this._offset),t=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&&(this.source.mediaElement.currentTime=this._offset),t=this.source.mediaElement.play();else if(`start`in this.source&&(this.recreateSource(),!this.startBufferSource(e?.at??0)))return[this];if(t){let e=t.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(n)}},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(n);return[this]}catch(e){throw this.emitAsync(`error`,{error:e,errorType:`source`,timestamp:Date.now(),recoverable:!0}),e}}emitPlayStarted(e){this._mediaPreparationGate?.gain.setValueAtTime(1,this.context.currentTime),this._state===`stopped`&&!this.origin.playbacks.includes(this)&&this.origin._readmitPlayback(this),super.emitPlayStarted(e)}pause(){if(!(!this.source||this._state!==`playing`)){if(this._mediaPlayPromise=void 0,this._scheduledStop=void 0,`mediaElement`in this.source&&this.source.mediaElement)this._mediaPreparationGate?.gain.setValueAtTime(0,this.context.currentTime),this.source.mediaElement.pause(),this._offset=this.source.mediaElement.currentTime;else{let e=Math.max(0,(this.context.currentTime-this._startTime)*this._playbackRate);this._offset=this.regionRelativeOffset(this._offset+e),`stop`in this.source&&this.source.stop()}this.emitPaused()}}stop(){if(!this.source)throw Error(`Cannot stop a sound that has been cleaned up`);this._cancelPrepareSeek?.(),this._scheduledStop=void 0,this._mediaPreparationGate?.gain.setValueAtTime(0,this.context.currentTime);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){if(n?.perLoop&&this._clipRegion)throw Error(`Per-loop fades are not supported with clip bounds`);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){if(this._scheduledStop)throw Error(`An absolute end cannot use an ended-time fade-out`);this._fadeOutConfig={duration:e,type:t??`linear`}}stopWithFade(e,t){return this.fadeOut(e,t).then(()=>{this._state!==`stopped`&&this.source&&this.stop()})}async prepareSeek(e,t={}){if(this.assertNotCleanedUp(),!Number.isFinite(e)||e<0)throw RangeError(`Seek time must be finite and non-negative`);let n=t.timeoutMs??1e4;if(!Number.isFinite(n)||n<=0||n>2147483647)throw RangeError(`Seek timeout must be positive and at most 2147483647 milliseconds`);if(t.signal?.aborted)throw new DOMException(`Seek preparation aborted`,`AbortError`);if(this._state===`playing`||this._mediaPlayPromise)throw Error(`Pause playback before preparing a seek`);this._cancelPrepareSeek?.();let r=this.source;if(!(`mediaElement`in r)){if(this._clipRegion&&e>=this._clipRegion.end)throw RangeError(`Seek time must precede the clip end`);if(!Number.isFinite(this.duration)||e>this.duration)throw RangeError(`Seek time exceeds duration`);this._offset=e,this.emit(`seek`,e);return}let i=r.mediaElement;if(!i.paused)throw Error(`Pause playback before preparing a seek`);if(!this._mediaPreparationGate){let e=this.context.createGain();e.gain.value=0,this._mediaPreparationGate=e,this.setEffectChainEndpoints(r,this.panner),r.connect(e)}this._mediaPreparationGate.gain.setValueAtTime(0,this.context.currentTime),await new Promise((r,a)=>{let o=!1,s=!1,c=!1,l=[`loadedmetadata`,`durationchange`,`progress`,`loadeddata`,`canplay`,`seeked`,`error`,`emptied`,`play`],u=n=>{if(!o){o=!0,clearTimeout(p);for(let e of l)i.removeEventListener(e,f);t.signal?.removeEventListener(`abort`,d),this._cancelPrepareSeek===d&&(this._cancelPrepareSeek=void 0),n===void 0?(this._offset=e,r()):a(n)}},d=()=>u(new DOMException(`Seek preparation aborted`,`AbortError`)),f=t=>{if(!o)try{if(i.error||t?.type===`error`){u(Error(i.error?.message||`Media failed during seek preparation`));return}if(t?.type===`emptied`&&s||t?.type===`play`){d();return}if(i.readyState<1)return;if(!Number.isFinite(i.duration)){u(new DOMException(`Seek preparation requires finite-duration media`,`NotSupportedError`));return}if(e>i.duration){u(RangeError(`Seek time exceeds duration`));return}if(s)t?.type===`seeked`&&(c=!1);else{let t=!1;for(let n=0;n<i.seekable.length;n++)e>=i.seekable.start(n)&&e<=i.seekable.end(n)&&(t=!0);if(!t)return;s=!0,c=i.seeking||i.currentTime!==e,i.currentTime!==e&&(i.currentTime=e)}!c&&!i.seeking&&i.readyState>=2&&Math.abs(i.currentTime-e)<.001&&u()}catch(e){u(e)}},p=setTimeout(()=>u(new DOMException(`Seek preparation timed out`,`TimeoutError`)),n);this._cancelPrepareSeek=d;for(let e of l)i.addEventListener(e,f);t.signal?.addEventListener(`abort`,d,{once:!0}),f()}),this.emit(`seek`,e)}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._clipRegion&&e>=this._clipRegion.end)throw RangeError(`Seek time must precede the clip end`);if(this._cancelPrepareSeek?.(),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`){if(this.source&&`mediaElement`in this.source)return this.source.mediaElement.currentTime;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._scheduledStop=void 0,this.source.buffer=this.buffer,this.setEffectChainEndpoints(this.source,this.panner),this.setupSourceNode(this.source),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._scheduledStop=void 0,this._cancelPrepareSeek?.(),this._mediaPreparationGate?.gain.setValueAtTime(0,this.context.currentTime),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,`stop`in this.source&&this._state===`playing`&&this.source.stop());this._offset=0,this._startTime=0,this.markStopped(),this.currentLoop=0,this.source.disconnect(),this.source=void 0,super.cleanup(),this._mediaPreparationGate?.disconnect(),this._mediaPreparationGate=void 0,this._occlusion?.destroy(),this._occlusion=void 0}}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){this._validateLoopCount(e);let t=this.loopCount,n=e===`infinite`?`infinite`:Math.max(0,e),r=t===`infinite`!=(n===`infinite`);if(r&&(this.origin.region||this._clipRegion)&&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._scheduledStop=void 0,this.loopCount=n,this.currentLoop=0,this._startTime=e,this.recreateSource(),this.startBufferSource(e),this.loopCount}if(r&&this._state===`playing`&&`buffer`in this.source){let e=Math.max(0,(this.context.currentTime-this._startTime)*this._playbackRate);this._offset=this.regionRelativeOffset(this._offset+e,t),this._startTime=Math.max(this._startTime,this.context.currentTime)}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._clipRegion?(this.origin.region?.start??0)+this._clipRegion.end:this.origin.region?this.origin.region.start+this.origin.region.duration:this.source.buffer.duration,this.source.loopStart=(this.origin.region?.start??0)+(this._clipRegion?.start??0)))}regionRelativeOffset(e,t=this.loopCount){if(this._clipRegion){let{start:n,end:r}=this._clipRegion;return t===`infinite`&&e>=r?n+(e-n)%(r-n):Math.min(r,e)}let n=this.origin.region;return n?t===`infinite`?e%n.duration:Math.min(n.duration,e):t===`infinite`&&this.buffer&&this.buffer.duration>0?e%this.buffer.duration:e}startBufferSource(e){if(!this.source||!(`start`in this.source))return!1;let t=this.origin.region;if(this._clipRegion){if(this._offset>=this._clipRegion.end)return!1;let n=(t?.start??0)+this._offset;return this.loopCount===`infinite`?this.source.start(e,n):this.source.start(e,n,this._clipRegion.end-this._offset),!0}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`);if(this._scheduledStop)throw Error(`Cannot clone a playback with an armed absolute end`);this._validateLoopCount(e.loopCount??this.loopCount),n.validateSpatialSmoothingTau(e.spatialSmoothingTau??this.spatialSmoothingTau);let r=e.panType??this.panType;t.validateSpatialOptions(e,r);let i=this.context.createGain();i.connect(this.origin._resolveRouteTargetNode());let o;if(`buffer`in this.source&&this.source.buffer)o=this.context.createBufferSource(),o.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);o=this.context.createMediaElementSource(e)}else throw Error(`Unsupported source type`);let s=e.loopCount===void 0?this.loopCount:e.loopCount,c=new a(this.origin,o,i);return c.loopCount=s,c._clipRegion=this._clipRegion&&{...this._clipRegion},c.currentLoop=this.currentLoop,c.setPanType(r,this.context),c.spatialSmoothingTau=e.spatialSmoothingTau??this.spatialSmoothingTau,r===`HRTF`?(this.panType===`HRTF`&&(c.threeDOptions=this.threeDOptions),e.threeDOptions!==void 0&&(c.threeDOptions=e.threeDOptions),e.position!==void 0&&(c.position=e.position)):c.stereoPan=e.stereoPan??this.stereoPan??0,c.volume=this.volume,c.playbackRate=this._playbackRate,c._offset=this._clipRegion&&this._state===`playing`?this.currentTime:this._offset,c.setOcclusion(this.occlusion,0),this._fadeInConfig&&(c._fadeInConfig={...this._fadeInConfig}),this._fadeOutConfig&&(c._fadeOutConfig={...this._fadeOutConfig}),this._state===`paused`?c.markPaused():this._state===`stopped`&&c.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,c.addFilter(t)}),this._pitchFactor!==1&&c.setPitchShift(this._pitchFactor).catch(()=>{}),this._state===`playing`&&c.play(),this.origin.playbacks.push(c),c}};exports.Playback=a;
|
|
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 { Occlusion } from \"./occlusion\";\nimport type { PanCloneOverrides } from \"./pannerMixin\";\nimport { applyPlayOptions, validatePlayOptions, validateSpatialOptions } from \"./playOptions\";\nimport type { Sound } from \"./sound\";\nimport { validateSpatialSmoothingTau } from \"./spatialAutomation\";\nimport { WORKLETS } from \"./worklets\";\n\ntype PlaybackCloneOverrides = PanCloneOverrides & {\n loopCount: LoopCount;\n panType: PanType;\n};\n\nexport interface PrepareSeekOptions {\n /** Cancel preparation without starting playback. */\n signal?: AbortSignal;\n /** Maximum readiness wait in milliseconds, default 10,000. Range: (0, 2^31 - 1]. */\n timeoutMs?: number;\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 private _cancelPrepareSeek?: () => void;\n /** Isolates paused media decoding (notably WebKit seek output) from the graph. */\n private _mediaPreparationGate?: GainNode;\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 private _occlusion?: Occlusion;\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 /** The last requested normalized occlusion amount, initially 0 (clear direct path). */\n get occlusion(): number {\n return this._occlusion?.amount ?? 0;\n }\n\n /**\n * Renders obstruction for this voice independently of volume, fades, and positioning.\n * The caller owns geometry, materials, and policy; Cacophony only renders the amount.\n * Finite amounts clamp to [0, 1]; non-finite amounts throw RangeError.\n * A dedicated pre-panner low-pass opens to Nyquist at 0 and closes logarithmically\n * to 800 Hz at 1, with attenuation from 0 to -18 dB. Resetting to 0 is transparent.\n * Nodes are allocated on the first nonzero amount and reused, including after reset.\n * @param amount - 0 for a clear path, 1 for maximum obstruction.\n * @param duration - Linear transition duration in milliseconds, default 50; 0 applies immediately.\n * @throws {RangeError} For a non-finite amount or a negative/non-finite duration.\n * @throws {Error} If this playback has been cleaned up.\n */\n setOcclusion(amount: number, duration = 50): void {\n this.assertNotCleanedUp();\n if (!Number.isFinite(amount)) throw new RangeError(\"Occlusion amount must be finite\");\n if (!Number.isFinite(duration) || duration < 0) {\n throw new RangeError(\"Occlusion duration must be finite and non-negative\");\n }\n const normalized = Math.max(0, Math.min(1, amount));\n if (!this._occlusion) {\n if (normalized === 0) return;\n this._occlusion = new Occlusion(this.context, this.panner!);\n this.setEffectChainEndpoints(this.source!, this.panner!);\n }\n this._occlusion.setAmount(normalized, duration);\n }\n\n protected override setEffectChainEndpoints(input: AudioNode, output: AudioNode): void {\n this._occlusion?.setOutput(output);\n super.setEffectChainEndpoints(this._mediaPreparationGate ?? input, this._occlusion?.input ?? output);\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 this._cancelPrepareSeek?.();\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 if (this.source.mediaElement.currentTime !== this._offset) {\n this.source.mediaElement.currentTime = this._offset;\n }\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 if (this.source.mediaElement.currentTime !== this._offset) {\n this.source.mediaElement.currentTime = this._offset;\n }\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 this._mediaPreparationGate?.gain.setValueAtTime(1, this.context.currentTime);\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 // A play listener can pause synchronously before the settled play promise's\n // finally runs. That bookkeeping must not block preparing this paused voice.\n this._mediaPlayPromise = undefined;\n\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this._mediaPreparationGate?.gain.setValueAtTime(0, this.context.currentTime);\n this.source.mediaElement.pause();\n // The context keeps advancing through network stalls and native loops.\n // Resume from the element's real position, never an elapsed-time guess.\n this._offset = this.source.mediaElement.currentTime;\n } else {\n const elapsed = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);\n this._offset = this.regionRelativeOffset(this._offset + elapsed);\n if (\"stop\" in this.source) {\n // Buffer sources cannot be restarted; play() creates a new one.\n this.source.stop();\n }\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 this._cancelPrepareSeek?.();\n this._mediaPreparationGate?.gain.setValueAtTime(0, this.context.currentTime);\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 /**\n * Silently prepares an idle or paused voice at a finite offset in seconds.\n * Buffers resolve immediately. Media elements wait for metadata, a seekable\n * range containing the target, and completed seeking with current audio data.\n * No play(), context resume, or autoplay primer is performed. State is retained.\n * A later preparation, play, seek, stop, or cleanup rejects pending work with\n * AbortError. Cancellation does not roll back a browser seek already requested.\n * Live/unknown-duration media rejects with NotSupportedError; readiness timeout\n * rejects with TimeoutError. Browser media seeking is not sample accurate.\n */\n async prepareSeek(time: number, options: PrepareSeekOptions = {}): Promise<void> {\n this.assertNotCleanedUp();\n if (!Number.isFinite(time) || time < 0) throw new RangeError(\"Seek time must be finite and non-negative\");\n const timeoutMs = options.timeoutMs ?? 10_000;\n if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2_147_483_647) {\n throw new RangeError(\"Seek timeout must be positive and at most 2147483647 milliseconds\");\n }\n if (options.signal?.aborted) throw new DOMException(\"Seek preparation aborted\", \"AbortError\");\n if (this._state === \"playing\" || this._mediaPlayPromise) {\n throw new Error(\"Pause playback before preparing a seek\");\n }\n this._cancelPrepareSeek?.();\n const source = this.source!;\n if (!(\"mediaElement\" in source)) {\n if (!Number.isFinite(this.duration) || time > this.duration) throw new RangeError(\"Seek time exceeds duration\");\n this._offset = time;\n this.emit(\"seek\", time);\n return;\n }\n const media = source.mediaElement;\n if (!media.paused) throw new Error(\"Pause playback before preparing a seek\");\n if (!this._mediaPreparationGate) {\n const gate = this.context.createGain();\n gate.gain.value = 0;\n this._mediaPreparationGate = gate;\n this.setEffectChainEndpoints(source, this.panner!);\n source.connect(gate);\n }\n this._mediaPreparationGate.gain.setValueAtTime(0, this.context.currentTime);\n await new Promise<void>((resolve, reject) => {\n let finished = false;\n let requested = false;\n let awaitingSeeked = false;\n const events = [\n \"loadedmetadata\",\n \"durationchange\",\n \"progress\",\n \"loadeddata\",\n \"canplay\",\n \"seeked\",\n \"error\",\n \"emptied\",\n \"play\",\n ];\n const settle = (error?: unknown) => {\n if (finished) return;\n finished = true;\n clearTimeout(timer);\n for (const event of events) media.removeEventListener(event, inspect);\n options.signal?.removeEventListener(\"abort\", cancel);\n if (this._cancelPrepareSeek === cancel) this._cancelPrepareSeek = undefined;\n if (error !== undefined) reject(error);\n else {\n this._offset = time;\n resolve();\n }\n };\n const cancel = () => settle(new DOMException(\"Seek preparation aborted\", \"AbortError\"));\n const inspect = (event?: Event) => {\n if (finished) return;\n try {\n if (media.error || event?.type === \"error\") {\n settle(new Error(media.error?.message || \"Media failed during seek preparation\"));\n return;\n }\n // preplay() can queue emptied while assigning the initial resource.\n // Once a seek is requested, a resource reset invalidates that request.\n if ((event?.type === \"emptied\" && requested) || event?.type === \"play\") {\n cancel();\n return;\n }\n if (media.readyState < 1) return;\n if (!Number.isFinite(media.duration)) {\n settle(new DOMException(\"Seek preparation requires finite-duration media\", \"NotSupportedError\"));\n return;\n }\n if (time > media.duration) {\n settle(new RangeError(\"Seek time exceeds duration\"));\n return;\n }\n if (!requested) {\n let inRange = false;\n for (let i = 0; i < media.seekable.length; i++) {\n if (time >= media.seekable.start(i) && time <= media.seekable.end(i)) inRange = true;\n }\n if (!inRange) return;\n requested = true;\n awaitingSeeked = media.seeking || media.currentTime !== time;\n if (media.currentTime !== time) media.currentTime = time;\n } else if (event?.type === \"seeked\") awaitingSeeked = false;\n if (\n !awaitingSeeked &&\n !media.seeking &&\n media.readyState >= 2 &&\n Math.abs(media.currentTime - time) < 0.001\n ) {\n settle();\n }\n } catch (error) {\n settle(error);\n }\n };\n const timer = setTimeout(() => settle(new DOMException(\"Seek preparation timed out\", \"TimeoutError\")), timeoutMs);\n this._cancelPrepareSeek = cancel;\n for (const event of events) media.addEventListener(event, inspect);\n options.signal?.addEventListener(\"abort\", cancel, { once: true });\n inspect();\n });\n // Notify outside the readiness listener's error boundary: observer errors\n // reject this async call instead of being swallowed after it has settled.\n this.emit(\"seek\", time);\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 this._cancelPrepareSeek?.();\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._cancelPrepareSeek?.();\n this._mediaPreparationGate?.gain.setValueAtTime(0, this.context.currentTime);\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 // Disconnecting alone leaves a started source running, including native loops.\n // Scheduled starts also have playing state; unplayed/paused/stopped sources do not need stop().\n if (\"stop\" in this.source && this._state === \"playing\") {\n this.source.stop();\n }\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 this._mediaPreparationGate?.disconnect();\n this._mediaPreparationGate = undefined;\n this._occlusion?.destroy();\n this._occlusion = undefined;\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 validateSpatialSmoothingTau(overrides.spatialSmoothingTau ?? this.spatialSmoothingTau);\n const panType = overrides.panType ?? this.panType;\n validateSpatialOptions(overrides, 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.spatialSmoothingTau = overrides.spatialSmoothingTau ?? this.spatialSmoothingTau;\n if (panType === \"HRTF\") {\n if (this.panType === \"HRTF\") clone.threeDOptions = this.threeDOptions;\n if (overrides.threeDOptions !== undefined) clone.threeDOptions = overrides.threeDOptions;\n if (overrides.position !== undefined) clone.position = overrides.position;\n } else {\n clone.stereoPan = overrides.stereoPan ?? this.stereoPan ?? 0;\n }\n clone.volume = this.volume;\n clone.playbackRate = this._playbackRate;\n clone._offset = this._offset;\n clone.setOcclusion(this.occlusion, 0);\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 { Occlusion } from \"./occlusion\";\nimport type { PanCloneOverrides } from \"./pannerMixin\";\nimport { applyPlayOptions, validatePlayOptions, validateSpatialOptions } from \"./playOptions\";\nimport type { Sound } from \"./sound\";\nimport { validateSpatialSmoothingTau } from \"./spatialAutomation\";\nimport { WORKLETS } from \"./worklets\";\n\ntype PlaybackCloneOverrides = PanCloneOverrides & {\n loopCount: LoopCount;\n panType: PanType;\n};\n\nexport interface PrepareSeekOptions {\n /** Cancel preparation without starting playback. */\n signal?: AbortSignal;\n /** Maximum readiness wait in milliseconds, default 10,000. Range: (0, 2^31 - 1]. */\n timeoutMs?: number;\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 private _clipRegion?: Readonly<{ start: number; end: number }>;\n private _scheduledStop?: { source: SourceNode; at: number };\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 private _cancelPrepareSeek?: () => void;\n /** Isolates paused media decoding (notably WebKit seek output) from the graph. */\n private _mediaPreparationGate?: GainNode;\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 private _occlusion?: Occlusion;\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 = () => {\n if (this.source === source) this.loopEnded();\n };\n } else {\n throw new Error(\"Unsupported source type\");\n }\n }\n\n /** The last requested normalized occlusion amount, initially 0 (clear direct path). */\n get occlusion(): number {\n return this._occlusion?.amount ?? 0;\n }\n\n /**\n * Renders obstruction for this voice independently of volume, fades, and positioning.\n * The caller owns geometry, materials, and policy; Cacophony only renders the amount.\n * Finite amounts clamp to [0, 1]; non-finite amounts throw RangeError.\n * A dedicated pre-panner low-pass opens to Nyquist at 0 and closes logarithmically\n * to 800 Hz at 1, with attenuation from 0 to -18 dB. Resetting to 0 is transparent.\n * Nodes are allocated on the first nonzero amount and reused, including after reset.\n * @param amount - 0 for a clear path, 1 for maximum obstruction.\n * @param duration - Linear transition duration in milliseconds, default 50; 0 applies immediately.\n * @throws {RangeError} For a non-finite amount or a negative/non-finite duration.\n * @throws {Error} If this playback has been cleaned up.\n */\n setOcclusion(amount: number, duration = 50): void {\n this.assertNotCleanedUp();\n if (!Number.isFinite(amount)) throw new RangeError(\"Occlusion amount must be finite\");\n if (!Number.isFinite(duration) || duration < 0) {\n throw new RangeError(\"Occlusion duration must be finite and non-negative\");\n }\n const normalized = Math.max(0, Math.min(1, amount));\n if (!this._occlusion) {\n if (normalized === 0) return;\n this._occlusion = new Occlusion(this.context, this.panner!);\n this.setEffectChainEndpoints(this.source!, this.panner!);\n }\n this._occlusion.setAmount(normalized, duration);\n }\n\n protected override setEffectChainEndpoints(input: AudioNode, output: AudioNode): void {\n this._occlusion?.setOutput(output);\n super.setEffectChainEndpoints(this._mediaPreparationGate ?? input, this._occlusion?.input ?? output);\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 /** Whether this live voice can enforce native buffer clip boundaries. HTML media cannot. */\n get supportsClipLoop(): boolean {\n return !!this.source && \"buffer\" in this.source && !!this.buffer;\n }\n\n /**\n * Configure a hard clip end and native loop points, in Sound-relative seconds.\n * Configure while idle or paused. loop(0) ends at R; loop(\"infinite\") wraps R to L.\n * Numeric repeats and per-loop fades are unsupported with explicit bounds.\n */\n setLoopRegion(startSeconds: number, endSeconds: number): void {\n this.assertNotCleanedUp();\n if (!this.supportsClipLoop) throw new DOMException(\"Clip bounds require a decoded buffer\", \"NotSupportedError\");\n if (this._state === \"playing\") throw new Error(\"Pause playback before configuring clip bounds\");\n if (\n !this.buffer ||\n !Number.isFinite(startSeconds) ||\n !Number.isFinite(endSeconds) ||\n startSeconds < 0 ||\n endSeconds > this.duration ||\n (endSeconds - startSeconds) * this.buffer.sampleRate +\n Number.EPSILON *\n Math.max(1, Math.abs(startSeconds * this.buffer.sampleRate), Math.abs(endSeconds * this.buffer.sampleRate)) *\n 4 <\n 1 ||\n Math.round(endSeconds * this.buffer.sampleRate) <= Math.round(startSeconds * this.buffer.sampleRate)\n ) {\n throw new RangeError(\"Clip bounds must span at least one sample within the Sound duration\");\n }\n if (this._offset >= endSeconds) throw new RangeError(\"The current offset must precede the clip end\");\n if (this.loopCount !== 0 && this.loopCount !== \"infinite\")\n throw new Error(\"Clip bounds support only zero or infinite loop count\");\n if (this._fadeInConfig?.perLoop) throw new Error(\"Per-loop fades are not supported with clip bounds\");\n this._clipRegion = { start: startSeconds, end: endSeconds };\n this.configureBufferLoopSource();\n }\n\n /**\n * End this started buffer activation at an absolute AudioContext time.\n * A deadline before a future onset prevents all onset audio. Past deadlines stop now.\n * Rate changes do not move this render-clock deadline. A later call replaces it;\n * pause, seek, source recreation, stop and cleanup revoke it. Downstream effect tails remain.\n * Legacy numeric repeats and ended-time fade-outs cannot share this per-source deadline.\n */\n scheduleStop(atAudioSeconds: number): void {\n this.assertNotCleanedUp();\n if (!this.supportsClipLoop || !this.source || !(\"stop\" in this.source))\n throw new DOMException(\"Scheduled ends require a decoded buffer\", \"NotSupportedError\");\n if (!Number.isFinite(atAudioSeconds) || atAudioSeconds < 0)\n throw new RangeError(\"End time must be finite and non-negative\");\n if (this._state !== \"playing\") throw new Error(\"Start the buffer before scheduling its end\");\n if (this.loopCount !== 0 && this.loopCount !== \"infinite\")\n throw new Error(\"Scheduled ends do not support legacy numeric repeats\");\n if (this._fadeOutConfig) throw new Error(\"An absolute end cannot use an ended-time fade-out\");\n const at = Math.max(this.context.currentTime, atAudioSeconds);\n this.source.stop(at);\n this._scheduledStop = { source: this.source, at };\n }\n\n /** @internal Preflight Sound broadcasts and play options before any mutation. */\n _validateLoopCount(loopCount: LoopCount): void {\n if (this._clipRegion && loopCount !== 0 && loopCount !== \"infinite\")\n throw new Error(\"Clip bounds support only zero or infinite loop count\");\n if (this._scheduledStop && loopCount !== 0 && loopCount !== \"infinite\")\n throw new Error(\"Scheduled ends do not support legacy numeric repeats\");\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 if (this._scheduledStop?.source === this.source) {\n const source = this.source;\n this._scheduledStop = undefined;\n if (\"onended\" in source) source.onended = null;\n try {\n this.emit(\"ended\", undefined);\n } finally {\n if (this.source === source && this._state === \"playing\") this.stop();\n if (this.source === source && this.state === \"stopped\") this.removeFromOrigin();\n }\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 const source = this.source;\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.source !== source || this._state !== \"playing\") return;\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 === source) {\n this.stop();\n if (this.source === source && this.state === \"stopped\") this.removeFromOrigin();\n }\n });\n } else {\n this.stop();\n if (this.source === source && this.state === \"stopped\") this.removeFromOrigin();\n }\n } else {\n this._loopEndCallback?.();\n if (this.source !== source || this._state !== \"playing\") return;\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 if (options?.loopCount !== undefined) this._validateLoopCount(options.loopCount);\n if (this._clipRegion && options?.fadeInPerLoop)\n throw new Error(\"Per-loop fades are not supported with clip bounds\");\n if (this._scheduledStop && options?.fadeOut !== undefined)\n throw new Error(\"An absolute end cannot use an ended-time fade-out\");\n\n if (this._state === \"playing\") {\n return [this];\n }\n\n this._cancelPrepareSeek?.();\n this._scheduledStop = undefined;\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 if (this.source.mediaElement.currentTime !== this._offset) {\n this.source.mediaElement.currentTime = this._offset;\n }\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 if (this.source.mediaElement.currentTime !== this._offset) {\n this.source.mediaElement.currentTime = this._offset;\n }\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 this._mediaPreparationGate?.gain.setValueAtTime(1, this.context.currentTime);\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 // A play listener can pause synchronously before the settled play promise's\n // finally runs. That bookkeeping must not block preparing this paused voice.\n this._mediaPlayPromise = undefined;\n this._scheduledStop = undefined;\n\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this._mediaPreparationGate?.gain.setValueAtTime(0, this.context.currentTime);\n this.source.mediaElement.pause();\n // The context keeps advancing through network stalls and native loops.\n // Resume from the element's real position, never an elapsed-time guess.\n this._offset = this.source.mediaElement.currentTime;\n } else {\n const elapsed = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);\n this._offset = this.regionRelativeOffset(this._offset + elapsed);\n if (\"stop\" in this.source) {\n // Buffer sources cannot be restarted; play() creates a new one.\n this.source.stop();\n }\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 this._cancelPrepareSeek?.();\n this._scheduledStop = undefined;\n this._mediaPreparationGate?.gain.setValueAtTime(0, this.context.currentTime);\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 && this._clipRegion) throw new Error(\"Per-loop fades are not supported with clip bounds\");\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 if (this._scheduledStop) throw new Error(\"An absolute end cannot use an ended-time fade-out\");\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 /**\n * Silently prepares an idle or paused voice at a finite offset in seconds.\n * Buffers resolve immediately. Media elements wait for metadata, a seekable\n * range containing the target, and completed seeking with current audio data.\n * No play(), context resume, or autoplay primer is performed. State is retained.\n * A later preparation, play, seek, stop, or cleanup rejects pending work with\n * AbortError. Cancellation does not roll back a browser seek already requested.\n * Live/unknown-duration media rejects with NotSupportedError; readiness timeout\n * rejects with TimeoutError. Browser media seeking is not sample accurate.\n */\n async prepareSeek(time: number, options: PrepareSeekOptions = {}): Promise<void> {\n this.assertNotCleanedUp();\n if (!Number.isFinite(time) || time < 0) throw new RangeError(\"Seek time must be finite and non-negative\");\n const timeoutMs = options.timeoutMs ?? 10_000;\n if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2_147_483_647) {\n throw new RangeError(\"Seek timeout must be positive and at most 2147483647 milliseconds\");\n }\n if (options.signal?.aborted) throw new DOMException(\"Seek preparation aborted\", \"AbortError\");\n if (this._state === \"playing\" || this._mediaPlayPromise) {\n throw new Error(\"Pause playback before preparing a seek\");\n }\n this._cancelPrepareSeek?.();\n const source = this.source!;\n if (!(\"mediaElement\" in source)) {\n if (this._clipRegion && time >= this._clipRegion.end) throw new RangeError(\"Seek time must precede the clip end\");\n if (!Number.isFinite(this.duration) || time > this.duration) throw new RangeError(\"Seek time exceeds duration\");\n this._offset = time;\n this.emit(\"seek\", time);\n return;\n }\n const media = source.mediaElement;\n if (!media.paused) throw new Error(\"Pause playback before preparing a seek\");\n if (!this._mediaPreparationGate) {\n const gate = this.context.createGain();\n gate.gain.value = 0;\n this._mediaPreparationGate = gate;\n this.setEffectChainEndpoints(source, this.panner!);\n source.connect(gate);\n }\n this._mediaPreparationGate.gain.setValueAtTime(0, this.context.currentTime);\n await new Promise<void>((resolve, reject) => {\n let finished = false;\n let requested = false;\n let awaitingSeeked = false;\n const events = [\n \"loadedmetadata\",\n \"durationchange\",\n \"progress\",\n \"loadeddata\",\n \"canplay\",\n \"seeked\",\n \"error\",\n \"emptied\",\n \"play\",\n ];\n const settle = (error?: unknown) => {\n if (finished) return;\n finished = true;\n clearTimeout(timer);\n for (const event of events) media.removeEventListener(event, inspect);\n options.signal?.removeEventListener(\"abort\", cancel);\n if (this._cancelPrepareSeek === cancel) this._cancelPrepareSeek = undefined;\n if (error !== undefined) reject(error);\n else {\n this._offset = time;\n resolve();\n }\n };\n const cancel = () => settle(new DOMException(\"Seek preparation aborted\", \"AbortError\"));\n const inspect = (event?: Event) => {\n if (finished) return;\n try {\n if (media.error || event?.type === \"error\") {\n settle(new Error(media.error?.message || \"Media failed during seek preparation\"));\n return;\n }\n // preplay() can queue emptied while assigning the initial resource.\n // Once a seek is requested, a resource reset invalidates that request.\n if ((event?.type === \"emptied\" && requested) || event?.type === \"play\") {\n cancel();\n return;\n }\n if (media.readyState < 1) return;\n if (!Number.isFinite(media.duration)) {\n settle(new DOMException(\"Seek preparation requires finite-duration media\", \"NotSupportedError\"));\n return;\n }\n if (time > media.duration) {\n settle(new RangeError(\"Seek time exceeds duration\"));\n return;\n }\n if (!requested) {\n let inRange = false;\n for (let i = 0; i < media.seekable.length; i++) {\n if (time >= media.seekable.start(i) && time <= media.seekable.end(i)) inRange = true;\n }\n if (!inRange) return;\n requested = true;\n awaitingSeeked = media.seeking || media.currentTime !== time;\n if (media.currentTime !== time) media.currentTime = time;\n } else if (event?.type === \"seeked\") awaitingSeeked = false;\n if (\n !awaitingSeeked &&\n !media.seeking &&\n media.readyState >= 2 &&\n Math.abs(media.currentTime - time) < 0.001\n ) {\n settle();\n }\n } catch (error) {\n settle(error);\n }\n };\n const timer = setTimeout(() => settle(new DOMException(\"Seek preparation timed out\", \"TimeoutError\")), timeoutMs);\n this._cancelPrepareSeek = cancel;\n for (const event of events) media.addEventListener(event, inspect);\n options.signal?.addEventListener(\"abort\", cancel, { once: true });\n inspect();\n });\n // Notify outside the readiness listener's error boundary: observer errors\n // reject this async call instead of being swallowed after it has settled.\n this.emit(\"seek\", time);\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 if (this._clipRegion && time >= this._clipRegion.end) throw new RangeError(\"Seek time must precede the clip end\");\n\n this._cancelPrepareSeek?.();\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 /**\n * Current source position in seconds. Playing HTML media reports its observed\n * cursor, including buffering stalls and native loop wraps. Buffer playback\n * follows the render clock; idle and paused voices retain their committed offset.\n */\n get currentTime(): number {\n if (this._state === \"playing\") {\n if (this.source && \"mediaElement\" in this.source) return this.source.mediaElement.currentTime;\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._scheduledStop = undefined;\n this.source.buffer = this.buffer;\n this.setEffectChainEndpoints(this.source, this.panner);\n this.setupSourceNode(this.source);\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._scheduledStop = undefined;\n this._cancelPrepareSeek?.();\n this._mediaPreparationGate?.gain.setValueAtTime(0, this.context.currentTime);\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 // Disconnecting alone leaves a started source running, including native loops.\n // Scheduled starts also have playing state; unplayed/paused/stopped sources do not need stop().\n if (\"stop\" in this.source && this._state === \"playing\") {\n this.source.stop();\n }\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 this._mediaPreparationGate?.disconnect();\n this._mediaPreparationGate = undefined;\n this._occlusion?.destroy();\n this._occlusion = undefined;\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 this._validateLoopCount(loopCount);\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 || this._clipRegion) &&\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._scheduledStop = undefined;\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 if (changesInfiniteMode && this._state === \"playing\" && \"buffer\" in this.source) {\n // Whole-buffer mode changes keep their native source and its deadline.\n // Commit the cursor under the old mode before changing how it folds.\n const elapsed = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);\n this._offset = this.regionRelativeOffset(this._offset + elapsed, previousLoopCount);\n this._startTime = Math.max(this._startTime, this.context.currentTime);\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._clipRegion\n ? (this.origin.region?.start ?? 0) + this._clipRegion.end\n : 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) + (this._clipRegion?.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 if (this._clipRegion) {\n const { start, end } = this._clipRegion;\n return loopCount === \"infinite\" && offset >= end\n ? start + ((offset - start) % (end - start))\n : Math.min(end, offset);\n }\n const region = this.origin.region;\n if (!region) {\n if (loopCount === \"infinite\" && this.buffer && this.buffer.duration > 0) return offset % this.buffer.duration;\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 (this._clipRegion) {\n if (this._offset >= this._clipRegion.end) return false;\n const absoluteOffset = (region?.start ?? 0) + this._offset;\n if (this.loopCount === \"infinite\") this.source.start(when, absoluteOffset);\n else this.source.start(when, absoluteOffset, this._clipRegion.end - this._offset);\n return true;\n }\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 if (this._scheduledStop) throw new Error(\"Cannot clone a playback with an armed absolute end\");\n this._validateLoopCount(overrides.loopCount ?? this.loopCount);\n validateSpatialSmoothingTau(overrides.spatialSmoothingTau ?? this.spatialSmoothingTau);\n const panType = overrides.panType ?? this.panType;\n validateSpatialOptions(overrides, 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._clipRegion = this._clipRegion && { ...this._clipRegion };\n clone.currentLoop = this.currentLoop;\n clone.setPanType(panType, this.context);\n clone.spatialSmoothingTau = overrides.spatialSmoothingTau ?? this.spatialSmoothingTau;\n if (panType === \"HRTF\") {\n if (this.panType === \"HRTF\") clone.threeDOptions = this.threeDOptions;\n if (overrides.threeDOptions !== undefined) clone.threeDOptions = overrides.threeDOptions;\n if (overrides.position !== undefined) clone.position = overrides.position;\n } else {\n clone.stereoPan = overrides.stereoPan ?? this.stereoPan ?? 0;\n }\n clone.volume = this.volume;\n clone.playbackRate = this._playbackRate;\n clone._offset = this._clipRegion && this._state === \"playing\" ? this.currentTime : this._offset;\n clone.setOcclusion(this.occlusion, 0);\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.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export declare class Playback extends BasePlayback implements BaseSound {
|
|
|
23
23
|
private _offset;
|
|
24
24
|
private _startTime;
|
|
25
25
|
private _playbackRate;
|
|
26
|
+
private _clipRegion?;
|
|
27
|
+
private _scheduledStop?;
|
|
26
28
|
/**
|
|
27
29
|
* Promise that tracks an in-flight media-element `.play()` settlement.
|
|
28
30
|
* Set when the media element's `play()` returns a pending promise; cleared
|
|
@@ -84,6 +86,24 @@ export declare class Playback extends BasePlayback implements BaseSound {
|
|
|
84
86
|
* Gets the current playback rate of the audio.
|
|
85
87
|
*/
|
|
86
88
|
get playbackRate(): number;
|
|
89
|
+
/** Whether this live voice can enforce native buffer clip boundaries. HTML media cannot. */
|
|
90
|
+
get supportsClipLoop(): boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Configure a hard clip end and native loop points, in Sound-relative seconds.
|
|
93
|
+
* Configure while idle or paused. loop(0) ends at R; loop("infinite") wraps R to L.
|
|
94
|
+
* Numeric repeats and per-loop fades are unsupported with explicit bounds.
|
|
95
|
+
*/
|
|
96
|
+
setLoopRegion(startSeconds: number, endSeconds: number): void;
|
|
97
|
+
/**
|
|
98
|
+
* End this started buffer activation at an absolute AudioContext time.
|
|
99
|
+
* A deadline before a future onset prevents all onset audio. Past deadlines stop now.
|
|
100
|
+
* Rate changes do not move this render-clock deadline. A later call replaces it;
|
|
101
|
+
* pause, seek, source recreation, stop and cleanup revoke it. Downstream effect tails remain.
|
|
102
|
+
* Legacy numeric repeats and ended-time fade-outs cannot share this per-source deadline.
|
|
103
|
+
*/
|
|
104
|
+
scheduleStop(atAudioSeconds: number): void;
|
|
105
|
+
/** @internal Preflight Sound broadcasts and play options before any mutation. */
|
|
106
|
+
_validateLoopCount(loopCount: LoopCount): void;
|
|
87
107
|
get volume(): number;
|
|
88
108
|
set volume(volume: number);
|
|
89
109
|
/**
|
|
@@ -144,6 +164,11 @@ export declare class Playback extends BasePlayback implements BaseSound {
|
|
|
144
164
|
*/
|
|
145
165
|
prepareSeek(time: number, options?: PrepareSeekOptions): Promise<void>;
|
|
146
166
|
seek(time: number): void;
|
|
167
|
+
/**
|
|
168
|
+
* Current source position in seconds. Playing HTML media reports its observed
|
|
169
|
+
* cursor, including buffering stalls and native loop wraps. Buffer playback
|
|
170
|
+
* follows the render clock; idle and paused voices retain their committed offset.
|
|
171
|
+
*/
|
|
147
172
|
get currentTime(): number;
|
|
148
173
|
private removeFromOrigin;
|
|
149
174
|
private recreateSource;
|