cacophony 0.35.0 → 0.36.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.
Files changed (157) hide show
  1. package/CHANGELOG.md +10 -1
  2. package/README.md +35 -2
  3. package/dist/index.d.ts +1 -0
  4. package/dist/playback.cjs +1 -1
  5. package/dist/playback.cjs.map +1 -1
  6. package/dist/playback.d.ts +20 -0
  7. package/dist/playback.mjs +91 -11
  8. package/dist/playback.mjs.map +1 -1
  9. package/docs/assets/hierarchy.js +1 -1
  10. package/docs/assets/navigation.js +1 -1
  11. package/docs/assets/search.js +1 -1
  12. package/docs/classes/AudioCache.html +4 -4
  13. package/docs/classes/AudioSprite.html +7 -7
  14. package/docs/classes/BarberpoleEffect.html +6 -6
  15. package/docs/classes/BiquadEffect.html +3 -3
  16. package/docs/classes/Bus.html +21 -21
  17. package/docs/classes/Cacophony.html +92 -92
  18. package/docs/classes/DynamicsEffect.html +6 -6
  19. package/docs/classes/FdnReverbEffect.html +6 -6
  20. package/docs/classes/FoaDecoder.html +5 -5
  21. package/docs/classes/FoaDecoderEffect.html +3 -3
  22. package/docs/classes/FrequencyShifterEffect.html +6 -6
  23. package/docs/classes/Group.html +33 -33
  24. package/docs/classes/HarmonizerEffect.html +6 -6
  25. package/docs/classes/ImpulseResponseEffect.html +3 -3
  26. package/docs/classes/KWeightingFilter.html +4 -4
  27. package/docs/classes/LoudnessMeter.html +11 -11
  28. package/docs/classes/MediaStreamPlayback.html +70 -70
  29. package/docs/classes/MediaStreamSound.html +51 -51
  30. package/docs/classes/MicrophoneStream.html +52 -52
  31. package/docs/classes/ModulatedDelayEffect.html +6 -6
  32. package/docs/classes/PcmStreamPlayback.html +70 -70
  33. package/docs/classes/PcmStreamSound.html +61 -61
  34. package/docs/classes/PhaserEffect.html +6 -6
  35. package/docs/classes/Playback.html +112 -88
  36. package/docs/classes/ReverbEffect.html +6 -6
  37. package/docs/classes/Scheduler.html +3 -3
  38. package/docs/classes/ShareEffect.html +3 -3
  39. package/docs/classes/Sound.html +72 -72
  40. package/docs/classes/SpectralFreezeEffect.html +6 -6
  41. package/docs/classes/StereoWidenerEffect.html +3 -3
  42. package/docs/classes/Synth.html +63 -63
  43. package/docs/classes/SynthGroup.html +28 -28
  44. package/docs/classes/TremoloEffect.html +6 -6
  45. package/docs/classes/TruePeakDetector.html +8 -8
  46. package/docs/classes/WaveshaperEffect.html +6 -6
  47. package/docs/functions/encodeMonoToFoaSN3D.html +1 -1
  48. package/docs/functions/integratedLoudness.html +1 -1
  49. package/docs/functions/integratedUngatedLoudness.html +1 -1
  50. package/docs/functions/loudnessRange.html +1 -1
  51. package/docs/functions/timeStretch.html +1 -1
  52. package/docs/functions/timeStretchChannels.html +1 -1
  53. package/docs/functions/truePeakDb.html +1 -1
  54. package/docs/hierarchy.html +3 -3
  55. package/docs/index.html +26 -0
  56. package/docs/interfaces/AudioBuffer.html +8 -8
  57. package/docs/interfaces/AudioBufferSourceNode.html +21 -21
  58. package/docs/interfaces/AudioEventCallbacks.html +1 -1
  59. package/docs/interfaces/AudioListener.html +12 -12
  60. package/docs/interfaces/AudioNode.html +13 -13
  61. package/docs/interfaces/AudioParam.html +7 -7
  62. package/docs/interfaces/AudioWorklet.html +2 -2
  63. package/docs/interfaces/AudioWorkletNode.html +15 -15
  64. package/docs/interfaces/BarberpoleOptions.html +5 -5
  65. package/docs/interfaces/BaseContext.html +25 -25
  66. package/docs/interfaces/BaseSound.html +16 -16
  67. package/docs/interfaces/BiquadCoefficients.html +6 -6
  68. package/docs/interfaces/BiquadFilterNode.html +19 -19
  69. package/docs/interfaces/BuiltEffectGraph.html +6 -6
  70. package/docs/interfaces/CacheErrorEvent.html +5 -5
  71. package/docs/interfaces/CacheHitEvent.html +4 -4
  72. package/docs/interfaces/CacheMissEvent.html +4 -4
  73. package/docs/interfaces/CacophonyEffect.html +2 -2
  74. package/docs/interfaces/CacophonyLogger.html +4 -4
  75. package/docs/interfaces/ChannelMergerNode.html +13 -13
  76. package/docs/interfaces/ChannelSplitterNode.html +13 -13
  77. package/docs/interfaces/ConvolverNode.html +15 -15
  78. package/docs/interfaces/CreateSpriteOptions.html +3 -3
  79. package/docs/interfaces/DeviceChangeEvent.html +3 -3
  80. package/docs/interfaces/DynamicsOptions.html +7 -7
  81. package/docs/interfaces/FadeStartEvent.html +4 -4
  82. package/docs/interfaces/FdnReverbOptions.html +6 -6
  83. package/docs/interfaces/FoaDecoderOptions.html +2 -2
  84. package/docs/interfaces/FrequencyShifterOptions.html +3 -3
  85. package/docs/interfaces/GainNode.html +14 -14
  86. package/docs/interfaces/GlobalPlaybackEvent.html +3 -3
  87. package/docs/interfaces/HarmonizerOptions.html +6 -6
  88. package/docs/interfaces/ImpulseResponseOptions.html +5 -5
  89. package/docs/interfaces/LoadingCompleteEvent.html +5 -5
  90. package/docs/interfaces/LoadingErrorEvent.html +5 -5
  91. package/docs/interfaces/LoadingProgressEvent.html +6 -6
  92. package/docs/interfaces/LoadingStartEvent.html +3 -3
  93. package/docs/interfaces/LoudnessChannelInput.html +3 -3
  94. package/docs/interfaces/LoudnessReading.html +5 -5
  95. package/docs/interfaces/MediaElementSourceNode.html +14 -14
  96. package/docs/interfaces/MediaStreamAudioSourceNode.html +14 -14
  97. package/docs/interfaces/MediaStreamSoundOptions.html +4 -4
  98. package/docs/interfaces/MicrophoneStreamOptions.html +7 -7
  99. package/docs/interfaces/ModulatedDelayOptions.html +8 -8
  100. package/docs/interfaces/OfflineOptions.html +5 -5
  101. package/docs/interfaces/OscillatorNode.html +19 -19
  102. package/docs/interfaces/PannerNode.html +27 -27
  103. package/docs/interfaces/PcmStreamBufferEvent.html +2 -2
  104. package/docs/interfaces/PcmStreamSoundOptions.html +6 -6
  105. package/docs/interfaces/PhaserOptions.html +7 -7
  106. package/docs/interfaces/PlayOptions.html +13 -13
  107. package/docs/interfaces/PlaybackErrorEvent.html +5 -5
  108. package/docs/interfaces/PrepareSeekOptions.html +65 -0
  109. package/docs/interfaces/ReverbOptions.html +13 -13
  110. package/docs/interfaces/RuntimeOptions.html +8 -8
  111. package/docs/interfaces/ScheduledCallbackHandle.html +2 -2
  112. package/docs/interfaces/SendGainOptions.html +3 -3
  113. package/docs/interfaces/SoundCleanupHoldings.html +4 -4
  114. package/docs/interfaces/SoundErrorEvent.html +6 -6
  115. package/docs/interfaces/SpectralFreezeOptions.html +4 -4
  116. package/docs/interfaces/SpriteRegion.html +4 -4
  117. package/docs/interfaces/StereoPannerNode.html +14 -14
  118. package/docs/interfaces/StereoToBFormatOptions.html +2 -2
  119. package/docs/interfaces/StereoWidenerOptions.html +4 -4
  120. package/docs/interfaces/StreamCapabilities.html +5 -5
  121. package/docs/interfaces/TimeStretchOptions.html +5 -5
  122. package/docs/interfaces/TremoloOptions.html +5 -5
  123. package/docs/interfaces/WaveshaperOptions.html +5 -5
  124. package/docs/modules.html +2 -2
  125. package/docs/types/AudioSinkId.html +1 -1
  126. package/docs/types/BaseAudioEvents.html +11 -11
  127. package/docs/types/BuiltEffect.html +1 -1
  128. package/docs/types/BusConnectionTarget.html +1 -1
  129. package/docs/types/CacheEventCallback.html +1 -1
  130. package/docs/types/CacophonyEvents.html +20 -20
  131. package/docs/types/ErrorEventCallback.html +1 -1
  132. package/docs/types/FadeType.html +1 -1
  133. package/docs/types/HrtfPannerOptions.html +1 -1
  134. package/docs/types/ImpulseResponseSource.html +1 -1
  135. package/docs/types/LoadingEventCallback.html +1 -1
  136. package/docs/types/LoopCount.html +1 -1
  137. package/docs/types/LoudnessChannel.html +1 -1
  138. package/docs/types/Orientation.html +3 -3
  139. package/docs/types/PanCloneOverrides.html +6 -6
  140. package/docs/types/PanType.html +1 -1
  141. package/docs/types/PcmStreamEvents.html +6 -6
  142. package/docs/types/PcmStreamState.html +1 -1
  143. package/docs/types/PlaybackEvents.html +1 -1
  144. package/docs/types/Position.html +1 -1
  145. package/docs/types/SoundEvents.html +1 -1
  146. package/docs/types/SoundType.html +1 -1
  147. package/docs/types/SourceNode.html +1 -1
  148. package/docs/types/SpriteMap.html +1 -1
  149. package/docs/types/SynthEvents.html +1 -1
  150. package/docs/types/ThreeDOptions.html +1 -1
  151. package/docs/types/WebCodecsStreamSound.html +1 -1
  152. package/docs/variables/CHANNEL_WEIGHTS.html +1 -1
  153. package/docs/variables/K_WEIGHTING_STAGE1_48K.html +1 -1
  154. package/docs/variables/K_WEIGHTING_STAGE2_48K.html +1 -1
  155. package/docs/variables/consoleLogger.html +1 -1
  156. package/docs/variables/noopLogger.html +1 -1
  157. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -17,6 +17,14 @@ someone upgrading, not as a commit message. `npm version` stamps the release; se
17
17
 
18
18
  ## [Unreleased]
19
19
 
20
+ ## [0.36.0] - 2026-10-18
21
+
22
+ ### Added
23
+ - `Playback.prepareSeek(time, { signal, timeoutMs })` silently prepares an idle or paused voice at a finite offset. Media voices wait for metadata and completed seeking before resolving; cancellation and timeout release readiness listeners without starting audio.
24
+
25
+ ### Fixed
26
+ - Paused media voices resume from the media element's actual position, including after buffering stalls and native infinite loops.
27
+
20
28
  ## [0.35.0] - 2026-10-18
21
29
 
22
30
  ### ⚠ Breaking
@@ -1434,7 +1442,8 @@ _0.1.0–0.1.2 were published to npm but not tagged; this block covers everythin
1434
1442
  - Sounds loaded from URLs are cached with the browser Cache API (`CacheManager`), and concurrent loads of the same URL are de-duplicated.
1435
1443
  - `Playback.fadeIn()` and `Playback.fadeOut()` with `'linear'` or `'exponential'` fades.
1436
1444
 
1437
- [Unreleased]: https://github.com/ctoth/cacophony/compare/v0.35.0...HEAD
1445
+ [Unreleased]: https://github.com/ctoth/cacophony/compare/v0.36.0...HEAD
1446
+ [0.36.0]: https://github.com/ctoth/cacophony/compare/v0.35.0...v0.36.0
1438
1447
  [0.35.0]: https://github.com/ctoth/cacophony/compare/v0.34.0...v0.35.0
1439
1448
  [0.34.0]: https://github.com/ctoth/cacophony/compare/v0.33.0...v0.34.0
1440
1449
  [0.33.0]: https://github.com/ctoth/cacophony/compare/v0.32.1...v0.33.0
package/README.md CHANGED
@@ -1508,8 +1508,41 @@ synth.type = 'square'; // Triggers typeChange event
1508
1508
  each playback to observe seeks, including those requested through `Sound.seek()`.
1509
1509
  `Sound` does not relay a collective `seek`: its concurrent voices can have
1510
1510
  different positions, and a collective seek can fail after some voices have moved.
1511
- For media elements, the event reports acceptance of the `currentTime` write;
1512
- it does not wait for the browser's asynchronous `seeked` event or playback resumption.
1511
+ For media elements, the event reports acceptance of the `currentTime` write;
1512
+ it does not wait for the browser's asynchronous `seeked` event or playback resumption.
1513
+
1514
+ To seek silently before playback, prepare an individual voice:
1515
+
1516
+ ```typescript
1517
+ const [voice] = sound.preplay();
1518
+ await voice.prepareSeek(30, { signal: controller.signal, timeoutMs: 10_000 });
1519
+ // Still unplayed and silent. Start explicitly when ready.
1520
+ voice.play();
1521
+ ```
1522
+
1523
+ `prepareSeek()` accepts idle or paused buffer and media-element voices and retains
1524
+ their state. Buffers resolve immediately. Media preparation waits for finite
1525
+ duration metadata, a seekable range containing the requested offset, and completed
1526
+ seeking with current audio data. It never calls `play()` or resumes/unlocks the
1527
+ audio context. Offset `0` is valid; negative, non-finite, and beyond-duration
1528
+ offsets reject. Live or unknown-duration media rejects with `NotSupportedError`.
1529
+ A per-voice input gate keeps paused decoding silent, including browsers that emit
1530
+ decoded samples while seeking. It opens after explicit playback is accepted and
1531
+ closes on pause, stop, or cleanup, independently of volume and effect settings.
1532
+ Readiness is bounded by `timeoutMs` (positive finite milliseconds up to
1533
+ 2,147,483,647, default 10,000),
1534
+ rejecting with `TimeoutError` if the target never becomes seekable or ready.
1535
+
1536
+ An abort, a new preparation, or `play()`, `seek()`, `stop()`, or `cleanup()` cancels
1537
+ pending work with `AbortError` and removes its listeners and timeout. Already
1538
+ requested browser seeks are not rolled back; only successful preparation commits
1539
+ the voice's offset and emits `seek`. Await the promise before playing to use that
1540
+ offset. Cancellation cannot later start playback or overwrite a newer position.
1541
+ HTML media remains subject to browser buffering and autoplay policy and does not
1542
+ offer sample-accurate starts. This API does not apply to live microphone or push
1543
+ PCM streams.
1544
+ An offset equal to the duration represents end-of-media; subsequently playing an
1545
+ HTML element at its end can restart it from zero under the browser's media rules.
1513
1546
 
1514
1547
  #### Synth Events
1515
1548
 
package/dist/index.d.ts CHANGED
@@ -24,6 +24,7 @@ export { MicrophonePlayback, MicrophoneStream } from './microphone';
24
24
  export type { HrtfPannerOptions, PanCloneOverrides, ThreeDOptions } from './pannerMixin';
25
25
  export type { PcmStreamBufferEvent, PcmStreamEvents, PcmStreamSoundOptions, PcmStreamState, } from './pcmStream';
26
26
  export { PcmStreamPlayback, PcmStreamSound } from './pcmStream';
27
+ export type { PrepareSeekOptions } from './playback';
27
28
  export { Playback } from './playback';
28
29
  export type { TimeStretchOptions } from './processors/timestretch-core';
29
30
  export { timeStretch, timeStretchChannels } from './processors/timestretch-core';
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;_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(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];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)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,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._state===`stopped`&&!this.origin.playbacks.includes(this)&&this.origin._readmitPlayback(this),super.emitPlayStarted(e)}pause(){if(!this.source||this._state!==`playing`)return;let e=Math.max(0,(this.context.currentTime-this._startTime)*this._playbackRate);this._offset=this.regionRelativeOffset(this._offset+e),`mediaElement`in this.source&&this.source.mediaElement?this.source.mediaElement.pause():`stop`in this.source&&this.source.stop(),this.emitPaused()}stop(){if(!this.source)throw Error(`Cannot stop a sound that has been cleaned up`);let e=this._mediaPlayPromise!==void 0;this._mediaPlayPromise=void 0,!(!e&&(this._state===`stopped`||this._state===`unplayed`))&&(this.cancelFade(),`stop`in this.source&&this._state===`playing`&&this.source.stop(),`mediaElement`in this.source&&this.source.mediaElement&&(this.source.mediaElement.pause(),this.source.mediaElement.currentTime=0),this._offset=0,this._startTime=0,this.emitStopped())}fadeIn(e,t,n){return n?.perLoop&&(this._fadeInConfig={duration:e,type:t??`linear`,perLoop:!0,targetVolume:this.gainNode.gain.value}),super.fadeIn(e,t,{startTime:n?.startTime})}configureFadeOut(e,t){this._fadeOutConfig={duration:e,type:t??`linear`}}stopWithFade(e,t){return this.fadeOut(e,t).then(()=>{this._state!==`stopped`&&this.source&&this.stop()})}seek(e){if(!this.source||!this.gainNode||!this.panner)throw Error(`Cannot seek a sound that has been cleaned up`);if(!Number.isFinite(e)||e<0||this.origin.region!==void 0&&e>this.duration)throw Error(`Invalid time value for seek`);if(this.origin.soundType===`streaming`&&`mediaElement`in this.source&&(this.source.mediaElement.seekable?.length??0)===0)throw this.source.mediaElement.duration===1/0?Error(`Live media-element streams do not expose a seekable range`):Error(`Media-element stream does not currently expose a seekable range`);let t=this._state===`playing`;t&&this.pause(),this._offset=e,`mediaElement`in this.source&&this.source.mediaElement&&(this.source.mediaElement.currentTime=e),t&&this.play(),this.emit(`seek`,e)}get currentTime(){if(this._state===`playing`){let e=Math.max(0,(this.context.currentTime-this._startTime)*this._playbackRate),t=this._offset+e;return this.regionRelativeOffset(t)}else return this._offset}removeFromOrigin(){let e=this.origin.playbacks.indexOf(this);e!==-1&&this.origin.playbacks.splice(e,1)}recreateSource(){if(!this.buffer||!this.panner||!this.context||!this.gainNode)throw Error(`Cannot recreate source of a sound that has been cleaned up`);try{this.source&&(!(`mediaElement`in this.source)&&`onended`in this.source&&(this.source.onended=null),this.source.disconnect()),this.source=this.context.createBufferSource(),this.source.buffer=this.buffer,this.setEffectChainEndpoints(this.source,this.panner),this.source.onended=this.loopEnded,this.playbackRate=this._playbackRate,this.configureBufferLoopSource()}catch(e){throw this.emitAsync(`error`,{error:e,errorType:`source`,timestamp:Date.now(),recoverable:!1}),e}}set sourceLoop(e){if(!this.source)throw Error(`Cannot set loop on a sound that has been cleaned up`);`loop`in this.source&&(this.source.loop=e),`mediaElement`in this.source&&this.source.mediaElement&&(this.source.mediaElement.loop=e)}cleanup(){if(this._mediaPlayPromise=void 0,this.source){if(`mediaElement`in this.source&&this.source.mediaElement){let e=this.source.mediaElement;e.pause();try{e.currentTime=0}catch{}e.loop=!1,e.onended=null,e.src=``,e.load()}else `onended`in this.source&&(this.source.onended=null,`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._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;_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;
2
2
  //# sourceMappingURL=playback.cjs.map
@@ -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 class Playback extends BasePlayback implements BaseSound {\n private context: BaseContext;\n public declare source?: SourceNode;\n loopCount: LoopCount = 0;\n currentLoop: number = 0;\n private buffer?: AudioBuffer;\n private _offset: number = 0;\n private _startTime: number = 0;\n private _playbackRate: number = 1;\n /**\n * Promise that tracks an in-flight media-element `.play()` settlement.\n * Set when the media element's `play()` returns a pending promise; cleared\n * once that promise settles. Used by `loopEnded` to avoid racing source-state\n * mutations (`seek`/`play`) against the deferred state transition driven by\n * this promise.\n */\n private _mediaPlayPromise?: Promise<void>;\n _fadeInConfig?: { duration: number; type: FadeType; perLoop: boolean; targetVolume: number };\n _fadeOutConfig?: { duration: number; type: FadeType };\n _loopEndCallback?: () => void;\n /**\n * Desired pitch-shift factor (1 = no shift, 2 = +1 octave, 0.5 = -1 octave).\n * Stored even before the worklet node exists so a value set on a cleaned-up /\n * not-yet-built playback is honoured once {@link setPitchShift} builds the node.\n */\n private _pitchFactor: number = 1;\n 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(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 applyPlayOptions(this, this.context, options);\n if (options?.playbackRate !== undefined) this.playbackRate = options.playbackRate;\n if (options?.loopCount !== undefined) this.loop(options.loopCount);\n if (options?.fadeIn !== undefined) {\n void this.fadeIn(options.fadeIn, options.fadeType, { perLoop: options.fadeInPerLoop, startTime: options.at });\n }\n if (options?.fadeOut !== undefined) this.configureFadeOut(options.fadeOut, options.fadeType);\n\n const isResume = this._state === \"paused\";\n if (this._state === \"stopped\") this.currentLoop = 0;\n const originGeneration = this.origin._playbackGeneration;\n\n try {\n let mediaPlayPromise: Promise<void> | undefined;\n\n if (this._state === \"paused\") {\n // If we're resuming from a paused state\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n mediaPlayPromise = this.source.mediaElement.play();\n } else {\n // For non-mediaElement sources, we need to recreate and start the source\n this.recreateSource();\n if (\"start\" in this.source) {\n if (!this.startBufferSource(options?.at ?? 0)) {\n return [this];\n }\n }\n }\n } else {\n // If we're starting from the beginning or a stopped state\n\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.currentTime = this._offset;\n mediaPlayPromise = this.source.mediaElement.play();\n } else if (\"start\" in this.source) {\n this.recreateSource();\n if (!this.startBufferSource(options?.at ?? 0)) {\n return [this];\n }\n }\n }\n\n if (mediaPlayPromise) {\n // For media-element sources, defer state and events until browser accepts playback\n // Stash the pending promise so loopEnded (and any future caller that\n // needs to sequence after the deferred state transition) can await it\n // rather than racing source-state mutations against it.\n const tracked = mediaPlayPromise\n .then(\n () => {\n if (this._mediaPlayPromise !== tracked || !this.source) return;\n if (this.origin._playbackGeneration !== originGeneration) {\n if (\"mediaElement\" in this.source) this.source.mediaElement.pause();\n return;\n }\n this._startTime = this.context.currentTime;\n this.emitPlayStarted(isResume);\n },\n (error: Error) => {\n if (this._mediaPlayPromise !== tracked || this.origin._playbackGeneration !== originGeneration) return;\n void this.emitAsync(\"error\", {\n error,\n errorType: \"source\",\n timestamp: Date.now(),\n recoverable: true,\n });\n },\n )\n .finally(() => {\n if (this._mediaPlayPromise === tracked) {\n this._mediaPlayPromise = undefined;\n }\n });\n this._mediaPlayPromise = tracked;\n } else {\n // For buffer sources, state transition is immediate (start() is synchronous)\n this._startTime = Math.max(options?.at ?? this.context.currentTime, this.context.currentTime);\n this.emitPlayStarted(isResume);\n }\n\n return [this];\n } catch (error) {\n this.emitAsync(\"error\", {\n error: error as Error,\n errorType: \"source\",\n timestamp: Date.now(),\n recoverable: true,\n });\n throw error;\n }\n }\n\n protected override emitPlayStarted(isResume: boolean): void {\n // A stopped voice may have been reaped by Sound.preplay() or natural end.\n // Restore ownership only after its source has successfully started.\n if (this._state === \"stopped\" && !this.origin.playbacks.includes(this)) {\n this.origin._readmitPlayback(this);\n }\n super.emitPlayStarted(isResume);\n }\n\n pause(): void {\n if (!this.source || this._state !== \"playing\") {\n return;\n }\n\n const elapsed = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);\n this._offset = this.regionRelativeOffset(this._offset + elapsed);\n\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.pause();\n } else if (\"stop\" in this.source) {\n // For AudioBufferSourceNode and OscillatorNode, stop the source.\n // It cannot be restarted; a new one will be created on play().\n this.source.stop();\n }\n\n this.emitPaused();\n }\n\n stop(): void {\n if (!this.source) {\n throw new Error(\"Cannot stop a sound that has been cleaned up\");\n }\n const pendingMediaPlay = this._mediaPlayPromise !== undefined;\n this._mediaPlayPromise = undefined;\n if (!pendingMediaPlay && (this._state === \"stopped\" || this._state === \"unplayed\")) {\n return;\n }\n\n this.cancelFade();\n\n if (\"stop\" in this.source && this._state === \"playing\") {\n this.source.stop();\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.pause();\n this.source.mediaElement.currentTime = 0;\n }\n\n this._offset = 0;\n this._startTime = 0;\n this.emitStopped();\n }\n\n /**\n * Fades in from silence to the current volume.\n * Optionally stores config to re-trigger the fade on each loop iteration.\n * @param {number} duration - The fade duration in milliseconds.\n * @param {FadeType} type - The fade curve type. Defaults to \"linear\".\n * @param {object} options - Optional. Set perLoop: true to re-trigger fadeIn on each loop.\n * @returns {Promise<void>} Resolves when the fade completes.\n */\n fadeIn(duration: number, type?: FadeType, options?: { perLoop?: boolean; startTime?: number }): Promise<void> {\n if (options?.perLoop) {\n this._fadeInConfig = {\n duration,\n type: type ?? \"linear\",\n perLoop: true,\n targetVolume: this.gainNode!.gain.value,\n };\n }\n return super.fadeIn(duration, type, { startTime: options?.startTime });\n }\n\n /**\n * Configures a fade-out to be applied when the playback ends naturally (last loop iteration).\n * @param {number} duration - The fade-out duration in milliseconds.\n * @param {FadeType} type - The fade curve type. Defaults to \"linear\".\n */\n configureFadeOut(duration: number, type?: FadeType): void {\n this._fadeOutConfig = { duration, type: type ?? \"linear\" };\n }\n\n /**\n * Fades out then stops the playback.\n * @param {number} duration - The fade-out duration in milliseconds.\n * @param {FadeType} type - The fade curve type. Defaults to \"linear\".\n * @returns {Promise<void>} Resolves when the fade completes and playback is stopped.\n */\n stopWithFade(duration: number, type?: FadeType): Promise<void> {\n // Guard the chained stop: if an external stop()/cleanup() runs between\n // the fade resolving and this .then, the chained stop() would otherwise\n // throw against torn-down state (\"Cannot stop a sound that has been\n // cleaned up\"). cancelFade() inside stop()/cleanup() resolves the fade\n // promise synchronously, so this .then will still fire -- the guard\n // makes it a no-op when the playback is already stopped or cleaned up.\n return this.fadeOut(duration, type).then(() => {\n if (this._state !== \"stopped\" && this.source) {\n this.stop();\n }\n });\n }\n\n seek(time: number): void {\n if (!this.source || !this.gainNode || !this.panner) {\n throw new Error(\"Cannot seek a sound that has been cleaned up\");\n }\n if (!Number.isFinite(time) || time < 0 || (this.origin.region !== undefined && time > this.duration)) {\n throw new Error(\"Invalid time value for seek\");\n }\n\n if (\n this.origin.soundType === \"streaming\" &&\n \"mediaElement\" in this.source &&\n (this.source.mediaElement.seekable?.length ?? 0) === 0\n ) {\n if (this.source.mediaElement.duration === Number.POSITIVE_INFINITY) {\n throw new Error(\"Live media-element streams do not expose a seekable range\");\n }\n throw new Error(\"Media-element stream does not currently expose a seekable range\");\n }\n\n const wasPlaying = this._state === \"playing\";\n if (wasPlaying) {\n this.pause();\n }\n\n this._offset = time;\n\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.currentTime = time;\n }\n // For non-media elements, play() will handle recreating the source if needed.\n\n if (wasPlaying) {\n this.play();\n }\n this.emit(\"seek\", time);\n }\n\n get currentTime(): number {\n if (this._state === \"playing\") {\n const elapsed = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);\n const time = this._offset + elapsed;\n return this.regionRelativeOffset(time);\n } else {\n return this._offset;\n }\n }\n\n private removeFromOrigin(): void {\n const idx = this.origin.playbacks.indexOf(this);\n if (idx !== -1) {\n this.origin.playbacks.splice(idx, 1);\n }\n }\n\n private recreateSource() {\n if (!this.buffer || !this.panner || !this.context || !this.gainNode) {\n throw new Error(\"Cannot recreate source of a sound that has been cleaned up\");\n }\n try {\n if (this.source) {\n // It's crucial to nullify onended of the old source if it's an AudioBufferSourceNode (or similar non-restartable source),\n // as its onended event could otherwise interfere with the new source created for seek/resume.\n // MediaElementAudioSourceNode is handled differently as its underlying element can be paused/played.\n if (!(\"mediaElement\" in this.source) && \"onended\" in this.source) {\n this.source.onended = null;\n }\n this.source.disconnect();\n }\n this.source = this.context.createBufferSource();\n this.source.buffer = this.buffer;\n this.setEffectChainEndpoints(this.source, this.panner);\n this.source.onended = this.loopEnded;\n this.playbackRate = this._playbackRate;\n this.configureBufferLoopSource();\n } catch (error) {\n this.emitAsync(\"error\", {\n error: error as Error,\n errorType: \"source\",\n timestamp: Date.now(),\n recoverable: false,\n });\n throw error;\n }\n }\n\n /**\n * Sets whether the audio source should loop.\n * @throws {Error} Throws an error if the sound has been cleaned up.\n */\n set sourceLoop(loop: boolean) {\n if (!this.source) {\n throw new Error(\"Cannot set loop on a sound that has been cleaned up\");\n }\n if (\"loop\" in this.source) {\n this.source.loop = loop;\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.loop = loop;\n }\n }\n\n /**\n * Cleans up resources used by the Playback instance.\n * This method should be called when the audio is no longer needed to free up resources.\n */\n\n cleanup(): void {\n this._mediaPlayPromise = undefined;\n if (!this.source) {\n return; // Already cleaned up\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n const mediaElement = this.source.mediaElement;\n mediaElement.pause();\n try {\n mediaElement.currentTime = 0;\n } catch {\n // Live media without a seekable range can reject currentTime writes.\n }\n mediaElement.loop = false;\n mediaElement.onended = null;\n mediaElement.src = \"\";\n mediaElement.load();\n } else if (\"onended\" in this.source) {\n this.source.onended = null;\n // 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._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 /**\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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"}
@@ -7,6 +7,12 @@ type PlaybackCloneOverrides = PanCloneOverrides & {
7
7
  loopCount: LoopCount;
8
8
  panType: PanType;
9
9
  };
10
+ export interface PrepareSeekOptions {
11
+ /** Cancel preparation without starting playback. */
12
+ signal?: AbortSignal;
13
+ /** Maximum readiness wait in milliseconds, default 10,000. Range: (0, 2^31 - 1]. */
14
+ timeoutMs?: number;
15
+ }
10
16
  export declare class Playback extends BasePlayback implements BaseSound {
11
17
  origin: Sound;
12
18
  private context;
@@ -25,6 +31,9 @@ export declare class Playback extends BasePlayback implements BaseSound {
25
31
  * this promise.
26
32
  */
27
33
  private _mediaPlayPromise?;
34
+ private _cancelPrepareSeek?;
35
+ /** Isolates paused media decoding (notably WebKit seek output) from the graph. */
36
+ private _mediaPreparationGate?;
28
37
  _fadeInConfig?: {
29
38
  duration: number;
30
39
  type: FadeType;
@@ -123,6 +132,17 @@ export declare class Playback extends BasePlayback implements BaseSound {
123
132
  * @returns {Promise<void>} Resolves when the fade completes and playback is stopped.
124
133
  */
125
134
  stopWithFade(duration: number, type?: FadeType): Promise<void>;
135
+ /**
136
+ * Silently prepares an idle or paused voice at a finite offset in seconds.
137
+ * Buffers resolve immediately. Media elements wait for metadata, a seekable
138
+ * range containing the target, and completed seeking with current audio data.
139
+ * No play(), context resume, or autoplay primer is performed. State is retained.
140
+ * A later preparation, play, seek, stop, or cleanup rejects pending work with
141
+ * AbortError. Cancellation does not roll back a browser seek already requested.
142
+ * Live/unknown-duration media rejects with NotSupportedError; readiness timeout
143
+ * rejects with TimeoutError. Browser media seeking is not sample accurate.
144
+ */
145
+ prepareSeek(time: number, options?: PrepareSeekOptions): Promise<void>;
126
146
  seek(time: number): void;
127
147
  get currentTime(): number;
128
148
  private removeFromOrigin;