cacophony 0.36.0 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -1
- package/README.md +39 -4
- package/dist/playback.cjs +1 -1
- package/dist/playback.cjs.map +1 -1
- package/dist/playback.d.ts +25 -0
- package/dist/playback.mjs +92 -17
- package/dist/playback.mjs.map +1 -1
- package/dist/sound.cjs +1 -1
- package/dist/sound.cjs.map +1 -1
- package/dist/sound.d.ts +2 -0
- package/dist/sound.mjs +6 -1
- package/dist/sound.mjs.map +1 -1
- package/docs/assets/hierarchy.js +1 -1
- package/docs/assets/search.js +1 -1
- package/docs/classes/AudioCache.html +4 -4
- package/docs/classes/AudioSprite.html +7 -7
- package/docs/classes/BarberpoleEffect.html +6 -6
- package/docs/classes/BiquadEffect.html +3 -3
- package/docs/classes/Bus.html +21 -21
- package/docs/classes/Cacophony.html +92 -92
- package/docs/classes/DynamicsEffect.html +6 -6
- package/docs/classes/FdnReverbEffect.html +6 -6
- package/docs/classes/FoaDecoder.html +5 -5
- package/docs/classes/FoaDecoderEffect.html +3 -3
- package/docs/classes/FrequencyShifterEffect.html +6 -6
- package/docs/classes/Group.html +33 -33
- package/docs/classes/HarmonizerEffect.html +6 -6
- package/docs/classes/ImpulseResponseEffect.html +3 -3
- package/docs/classes/KWeightingFilter.html +4 -4
- package/docs/classes/LoudnessMeter.html +11 -11
- package/docs/classes/MediaStreamPlayback.html +70 -70
- package/docs/classes/MediaStreamSound.html +51 -51
- package/docs/classes/MicrophoneStream.html +52 -52
- package/docs/classes/ModulatedDelayEffect.html +6 -6
- package/docs/classes/PcmStreamPlayback.html +70 -70
- package/docs/classes/PcmStreamSound.html +61 -61
- package/docs/classes/PhaserEffect.html +6 -6
- package/docs/classes/Playback.html +160 -89
- package/docs/classes/ReverbEffect.html +6 -6
- package/docs/classes/Scheduler.html +3 -3
- package/docs/classes/ShareEffect.html +3 -3
- package/docs/classes/Sound.html +85 -73
- package/docs/classes/SpectralFreezeEffect.html +6 -6
- package/docs/classes/StereoWidenerEffect.html +3 -3
- package/docs/classes/Synth.html +63 -63
- package/docs/classes/SynthGroup.html +28 -28
- package/docs/classes/TremoloEffect.html +6 -6
- package/docs/classes/TruePeakDetector.html +8 -8
- package/docs/classes/WaveshaperEffect.html +6 -6
- package/docs/functions/encodeMonoToFoaSN3D.html +1 -1
- package/docs/functions/integratedLoudness.html +1 -1
- package/docs/functions/integratedUngatedLoudness.html +1 -1
- package/docs/functions/loudnessRange.html +1 -1
- package/docs/functions/timeStretch.html +1 -1
- package/docs/functions/timeStretchChannels.html +1 -1
- package/docs/functions/truePeakDb.html +1 -1
- package/docs/hierarchy.html +2 -2
- package/docs/index.html +22 -0
- package/docs/interfaces/AudioBuffer.html +8 -8
- package/docs/interfaces/AudioBufferSourceNode.html +21 -21
- package/docs/interfaces/AudioEventCallbacks.html +1 -1
- package/docs/interfaces/AudioListener.html +12 -12
- package/docs/interfaces/AudioNode.html +13 -13
- package/docs/interfaces/AudioParam.html +7 -7
- package/docs/interfaces/AudioWorklet.html +2 -2
- package/docs/interfaces/AudioWorkletNode.html +15 -15
- package/docs/interfaces/BarberpoleOptions.html +5 -5
- package/docs/interfaces/BaseContext.html +25 -25
- package/docs/interfaces/BaseSound.html +16 -16
- package/docs/interfaces/BiquadCoefficients.html +6 -6
- package/docs/interfaces/BiquadFilterNode.html +19 -19
- package/docs/interfaces/BuiltEffectGraph.html +6 -6
- package/docs/interfaces/CacheErrorEvent.html +5 -5
- package/docs/interfaces/CacheHitEvent.html +4 -4
- package/docs/interfaces/CacheMissEvent.html +4 -4
- package/docs/interfaces/CacophonyEffect.html +2 -2
- package/docs/interfaces/CacophonyLogger.html +4 -4
- package/docs/interfaces/ChannelMergerNode.html +13 -13
- package/docs/interfaces/ChannelSplitterNode.html +13 -13
- package/docs/interfaces/ConvolverNode.html +15 -15
- package/docs/interfaces/CreateSpriteOptions.html +3 -3
- package/docs/interfaces/DeviceChangeEvent.html +3 -3
- package/docs/interfaces/DynamicsOptions.html +7 -7
- package/docs/interfaces/FadeStartEvent.html +4 -4
- package/docs/interfaces/FdnReverbOptions.html +6 -6
- package/docs/interfaces/FoaDecoderOptions.html +2 -2
- package/docs/interfaces/FrequencyShifterOptions.html +3 -3
- package/docs/interfaces/GainNode.html +14 -14
- package/docs/interfaces/GlobalPlaybackEvent.html +3 -3
- package/docs/interfaces/HarmonizerOptions.html +6 -6
- package/docs/interfaces/ImpulseResponseOptions.html +5 -5
- package/docs/interfaces/LoadingCompleteEvent.html +5 -5
- package/docs/interfaces/LoadingErrorEvent.html +5 -5
- package/docs/interfaces/LoadingProgressEvent.html +6 -6
- package/docs/interfaces/LoadingStartEvent.html +3 -3
- package/docs/interfaces/LoudnessChannelInput.html +3 -3
- package/docs/interfaces/LoudnessReading.html +5 -5
- package/docs/interfaces/MediaElementSourceNode.html +14 -14
- package/docs/interfaces/MediaStreamAudioSourceNode.html +14 -14
- package/docs/interfaces/MediaStreamSoundOptions.html +4 -4
- package/docs/interfaces/MicrophoneStreamOptions.html +7 -7
- package/docs/interfaces/ModulatedDelayOptions.html +8 -8
- package/docs/interfaces/OfflineOptions.html +5 -5
- package/docs/interfaces/OscillatorNode.html +19 -19
- package/docs/interfaces/PannerNode.html +27 -27
- package/docs/interfaces/PcmStreamBufferEvent.html +2 -2
- package/docs/interfaces/PcmStreamSoundOptions.html +6 -6
- package/docs/interfaces/PhaserOptions.html +7 -7
- package/docs/interfaces/PlayOptions.html +13 -13
- package/docs/interfaces/PlaybackErrorEvent.html +5 -5
- package/docs/interfaces/PrepareSeekOptions.html +3 -3
- package/docs/interfaces/ReverbOptions.html +13 -13
- package/docs/interfaces/RuntimeOptions.html +8 -8
- package/docs/interfaces/ScheduledCallbackHandle.html +2 -2
- package/docs/interfaces/SendGainOptions.html +3 -3
- package/docs/interfaces/SoundCleanupHoldings.html +4 -4
- package/docs/interfaces/SoundErrorEvent.html +6 -6
- package/docs/interfaces/SpectralFreezeOptions.html +4 -4
- package/docs/interfaces/SpriteRegion.html +4 -4
- package/docs/interfaces/StereoPannerNode.html +14 -14
- package/docs/interfaces/StereoToBFormatOptions.html +2 -2
- package/docs/interfaces/StereoWidenerOptions.html +4 -4
- package/docs/interfaces/StreamCapabilities.html +5 -5
- package/docs/interfaces/TimeStretchOptions.html +5 -5
- package/docs/interfaces/TremoloOptions.html +5 -5
- package/docs/interfaces/WaveshaperOptions.html +5 -5
- package/docs/types/AudioSinkId.html +1 -1
- package/docs/types/BaseAudioEvents.html +11 -11
- package/docs/types/BuiltEffect.html +1 -1
- package/docs/types/BusConnectionTarget.html +1 -1
- package/docs/types/CacheEventCallback.html +1 -1
- package/docs/types/CacophonyEvents.html +20 -20
- package/docs/types/ErrorEventCallback.html +1 -1
- package/docs/types/FadeType.html +1 -1
- package/docs/types/HrtfPannerOptions.html +1 -1
- package/docs/types/ImpulseResponseSource.html +1 -1
- package/docs/types/LoadingEventCallback.html +1 -1
- package/docs/types/LoopCount.html +1 -1
- package/docs/types/LoudnessChannel.html +1 -1
- package/docs/types/Orientation.html +3 -3
- package/docs/types/PanCloneOverrides.html +6 -6
- package/docs/types/PanType.html +1 -1
- package/docs/types/PcmStreamEvents.html +6 -6
- package/docs/types/PcmStreamState.html +1 -1
- package/docs/types/PlaybackEvents.html +1 -1
- package/docs/types/Position.html +1 -1
- package/docs/types/SoundEvents.html +1 -1
- package/docs/types/SoundType.html +1 -1
- package/docs/types/SourceNode.html +1 -1
- package/docs/types/SpriteMap.html +1 -1
- package/docs/types/SynthEvents.html +1 -1
- package/docs/types/ThreeDOptions.html +1 -1
- package/docs/types/WebCodecsStreamSound.html +1 -1
- package/docs/variables/CHANNEL_WEIGHTS.html +1 -1
- package/docs/variables/K_WEIGHTING_STAGE1_48K.html +1 -1
- package/docs/variables/K_WEIGHTING_STAGE2_48K.html +1 -1
- package/docs/variables/consoleLogger.html +1 -1
- package/docs/variables/noopLogger.html +1 -1
- package/package.json +1 -1
package/dist/playback.mjs
CHANGED
|
@@ -13,6 +13,8 @@ var s = class s extends a {
|
|
|
13
13
|
_offset = 0;
|
|
14
14
|
_startTime = 0;
|
|
15
15
|
_playbackRate = 1;
|
|
16
|
+
_clipRegion;
|
|
17
|
+
_scheduledStop;
|
|
16
18
|
_mediaPlayPromise;
|
|
17
19
|
_cancelPrepareSeek;
|
|
18
20
|
_mediaPreparationGate;
|
|
@@ -26,7 +28,9 @@ var s = class s extends a {
|
|
|
26
28
|
}
|
|
27
29
|
setupSourceNode(e) {
|
|
28
30
|
if ("mediaElement" in e && e.mediaElement) e.mediaElement.onended = this.loopEnded;
|
|
29
|
-
else if ("onended" in e) e.onended =
|
|
31
|
+
else if ("onended" in e) e.onended = () => {
|
|
32
|
+
this.source === e && this.loopEnded();
|
|
33
|
+
};
|
|
30
34
|
else throw Error("Unsupported source type");
|
|
31
35
|
}
|
|
32
36
|
get occlusion() {
|
|
@@ -53,6 +57,37 @@ var s = class s extends a {
|
|
|
53
57
|
get playbackRate() {
|
|
54
58
|
return this._playbackRate;
|
|
55
59
|
}
|
|
60
|
+
get supportsClipLoop() {
|
|
61
|
+
return !!this.source && "buffer" in this.source && !!this.buffer;
|
|
62
|
+
}
|
|
63
|
+
setLoopRegion(e, t) {
|
|
64
|
+
if (this.assertNotCleanedUp(), !this.supportsClipLoop) throw new DOMException("Clip bounds require a decoded buffer", "NotSupportedError");
|
|
65
|
+
if (this._state === "playing") throw Error("Pause playback before configuring clip bounds");
|
|
66
|
+
if (!this.buffer || !Number.isFinite(e) || !Number.isFinite(t) || e < 0 || t > this.duration || (t - e) * this.buffer.sampleRate + 2 ** -52 * Math.max(1, Math.abs(e * this.buffer.sampleRate), Math.abs(t * this.buffer.sampleRate)) * 4 < 1 || Math.round(t * this.buffer.sampleRate) <= Math.round(e * this.buffer.sampleRate)) throw RangeError("Clip bounds must span at least one sample within the Sound duration");
|
|
67
|
+
if (this._offset >= t) throw RangeError("The current offset must precede the clip end");
|
|
68
|
+
if (this.loopCount !== 0 && this.loopCount !== "infinite") throw Error("Clip bounds support only zero or infinite loop count");
|
|
69
|
+
if (this._fadeInConfig?.perLoop) throw Error("Per-loop fades are not supported with clip bounds");
|
|
70
|
+
this._clipRegion = {
|
|
71
|
+
start: e,
|
|
72
|
+
end: t
|
|
73
|
+
}, this.configureBufferLoopSource();
|
|
74
|
+
}
|
|
75
|
+
scheduleStop(e) {
|
|
76
|
+
if (this.assertNotCleanedUp(), !this.supportsClipLoop || !this.source || !("stop" in this.source)) throw new DOMException("Scheduled ends require a decoded buffer", "NotSupportedError");
|
|
77
|
+
if (!Number.isFinite(e) || e < 0) throw RangeError("End time must be finite and non-negative");
|
|
78
|
+
if (this._state !== "playing") throw Error("Start the buffer before scheduling its end");
|
|
79
|
+
if (this.loopCount !== 0 && this.loopCount !== "infinite") throw Error("Scheduled ends do not support legacy numeric repeats");
|
|
80
|
+
if (this._fadeOutConfig) throw Error("An absolute end cannot use an ended-time fade-out");
|
|
81
|
+
let t = Math.max(this.context.currentTime, e);
|
|
82
|
+
this.source.stop(t), this._scheduledStop = {
|
|
83
|
+
source: this.source,
|
|
84
|
+
at: t
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
_validateLoopCount(e) {
|
|
88
|
+
if (this._clipRegion && e !== 0 && e !== "infinite") throw Error("Clip bounds support only zero or infinite loop count");
|
|
89
|
+
if (this._scheduledStop && e !== 0 && e !== "infinite") throw Error("Scheduled ends do not support legacy numeric repeats");
|
|
90
|
+
}
|
|
56
91
|
get volume() {
|
|
57
92
|
return super.volume;
|
|
58
93
|
}
|
|
@@ -69,6 +104,16 @@ var s = class s extends a {
|
|
|
69
104
|
}
|
|
70
105
|
loopEnded = () => {
|
|
71
106
|
if (!(!this.source || this._state !== "playing")) {
|
|
107
|
+
if (this._scheduledStop?.source === this.source) {
|
|
108
|
+
let e = this.source;
|
|
109
|
+
this._scheduledStop = void 0, "onended" in e && (e.onended = null);
|
|
110
|
+
try {
|
|
111
|
+
this.emit("ended", void 0);
|
|
112
|
+
} finally {
|
|
113
|
+
this.source === e && this._state === "playing" && this.stop(), this.source === e && this.state === "stopped" && this.removeFromOrigin();
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
72
117
|
if ("mediaElement" in this.source && this._mediaPlayPromise) {
|
|
73
118
|
this._mediaPlayPromise.then(() => this._runLoopEnded());
|
|
74
119
|
return;
|
|
@@ -77,14 +122,24 @@ var s = class s extends a {
|
|
|
77
122
|
}
|
|
78
123
|
};
|
|
79
124
|
_runLoopEnded() {
|
|
80
|
-
!this.source || this._state !== "playing"
|
|
81
|
-
|
|
82
|
-
|
|
125
|
+
if (!this.source || this._state !== "playing") return;
|
|
126
|
+
let e = this.source;
|
|
127
|
+
if (this.currentLoop++, this.loopCount !== "infinite" && this.currentLoop > this.loopCount) {
|
|
128
|
+
if (this.emit("ended", void 0), this.source !== e || this._state !== "playing") return;
|
|
129
|
+
this._fadeOutConfig ? this.fadeOut(this._fadeOutConfig.duration, this._fadeOutConfig.type).then(() => {
|
|
130
|
+
this._state !== "stopped" && this.source === e && (this.stop(), this.source === e && this.state === "stopped" && this.removeFromOrigin());
|
|
131
|
+
}) : (this.stop(), this.source === e && this.state === "stopped" && this.removeFromOrigin());
|
|
132
|
+
} else {
|
|
133
|
+
if (this._loopEndCallback?.(), this.source !== e || this._state !== "playing") return;
|
|
134
|
+
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));
|
|
135
|
+
}
|
|
83
136
|
}
|
|
84
137
|
play(e) {
|
|
85
138
|
if (!this.source) throw Error("Cannot play a sound that has been cleaned up");
|
|
86
|
-
if (n(e, this.panType, "mediaElement" in this.source ? "media" : "buffer"), this.
|
|
87
|
-
|
|
139
|
+
if (n(e, this.panType, "mediaElement" in this.source ? "media" : "buffer"), e?.loopCount !== void 0 && this._validateLoopCount(e.loopCount), this._clipRegion && e?.fadeInPerLoop) throw Error("Per-loop fades are not supported with clip bounds");
|
|
140
|
+
if (this._scheduledStop && e?.fadeOut !== void 0) throw Error("An absolute end cannot use an ended-time fade-out");
|
|
141
|
+
if (this._state === "playing") return [this];
|
|
142
|
+
this._cancelPrepareSeek?.(), this._scheduledStop = void 0, t(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, {
|
|
88
143
|
perLoop: e.fadeInPerLoop,
|
|
89
144
|
startTime: e.at
|
|
90
145
|
}), e?.fadeOut !== void 0 && this.configureFadeOut(e.fadeOut, e.fadeType);
|
|
@@ -134,7 +189,7 @@ var s = class s extends a {
|
|
|
134
189
|
}
|
|
135
190
|
pause() {
|
|
136
191
|
if (!(!this.source || this._state !== "playing")) {
|
|
137
|
-
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;
|
|
192
|
+
if (this._mediaPlayPromise = void 0, this._scheduledStop = void 0, "mediaElement" in this.source && this.source.mediaElement) this._mediaPreparationGate?.gain.setValueAtTime(0, this.context.currentTime), this.source.mediaElement.pause(), this._offset = this.source.mediaElement.currentTime;
|
|
138
193
|
else {
|
|
139
194
|
let e = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);
|
|
140
195
|
this._offset = this.regionRelativeOffset(this._offset + e), "stop" in this.source && this.source.stop();
|
|
@@ -144,11 +199,12 @@ var s = class s extends a {
|
|
|
144
199
|
}
|
|
145
200
|
stop() {
|
|
146
201
|
if (!this.source) throw Error("Cannot stop a sound that has been cleaned up");
|
|
147
|
-
this._cancelPrepareSeek?.(), this._mediaPreparationGate?.gain.setValueAtTime(0, this.context.currentTime);
|
|
202
|
+
this._cancelPrepareSeek?.(), this._scheduledStop = void 0, this._mediaPreparationGate?.gain.setValueAtTime(0, this.context.currentTime);
|
|
148
203
|
let e = this._mediaPlayPromise !== void 0;
|
|
149
204
|
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());
|
|
150
205
|
}
|
|
151
206
|
fadeIn(e, t, n) {
|
|
207
|
+
if (n?.perLoop && this._clipRegion) throw Error("Per-loop fades are not supported with clip bounds");
|
|
152
208
|
return n?.perLoop && (this._fadeInConfig = {
|
|
153
209
|
duration: e,
|
|
154
210
|
type: t ?? "linear",
|
|
@@ -157,6 +213,7 @@ var s = class s extends a {
|
|
|
157
213
|
}), super.fadeIn(e, t, { startTime: n?.startTime });
|
|
158
214
|
}
|
|
159
215
|
configureFadeOut(e, t) {
|
|
216
|
+
if (this._scheduledStop) throw Error("An absolute end cannot use an ended-time fade-out");
|
|
160
217
|
this._fadeOutConfig = {
|
|
161
218
|
duration: e,
|
|
162
219
|
type: t ?? "linear"
|
|
@@ -176,6 +233,7 @@ var s = class s extends a {
|
|
|
176
233
|
this._cancelPrepareSeek?.();
|
|
177
234
|
let r = this.source;
|
|
178
235
|
if (!("mediaElement" in r)) {
|
|
236
|
+
if (this._clipRegion && e >= this._clipRegion.end) throw RangeError("Seek time must precede the clip end");
|
|
179
237
|
if (!Number.isFinite(this.duration) || e > this.duration) throw RangeError("Seek time exceeds duration");
|
|
180
238
|
this._offset = e, this.emit("seek", e);
|
|
181
239
|
return;
|
|
@@ -242,12 +300,14 @@ var s = class s extends a {
|
|
|
242
300
|
seek(e) {
|
|
243
301
|
if (!this.source || !this.gainNode || !this.panner) throw Error("Cannot seek a sound that has been cleaned up");
|
|
244
302
|
if (!Number.isFinite(e) || e < 0 || this.origin.region !== void 0 && e > this.duration) throw Error("Invalid time value for seek");
|
|
303
|
+
if (this._clipRegion && e >= this._clipRegion.end) throw RangeError("Seek time must precede the clip end");
|
|
245
304
|
if (this._cancelPrepareSeek?.(), this.origin.soundType === "streaming" && "mediaElement" in this.source && (this.source.mediaElement.seekable?.length ?? 0) === 0) throw this.source.mediaElement.duration === Infinity ? Error("Live media-element streams do not expose a seekable range") : Error("Media-element stream does not currently expose a seekable range");
|
|
246
305
|
let t = this._state === "playing";
|
|
247
306
|
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);
|
|
248
307
|
}
|
|
249
308
|
get currentTime() {
|
|
250
309
|
if (this._state === "playing") {
|
|
310
|
+
if (this.source && "mediaElement" in this.source) return this.source.mediaElement.currentTime;
|
|
251
311
|
let e = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate), t = this._offset + e;
|
|
252
312
|
return this.regionRelativeOffset(t);
|
|
253
313
|
} else return this._offset;
|
|
@@ -259,7 +319,7 @@ var s = class s extends a {
|
|
|
259
319
|
recreateSource() {
|
|
260
320
|
if (!this.buffer || !this.panner || !this.context || !this.gainNode) throw Error("Cannot recreate source of a sound that has been cleaned up");
|
|
261
321
|
try {
|
|
262
|
-
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.
|
|
322
|
+
this.source && (!("mediaElement" in this.source) && "onended" in this.source && (this.source.onended = null), this.source.disconnect()), this.source = this.context.createBufferSource(), this._scheduledStop = void 0, this.source.buffer = this.buffer, this.setEffectChainEndpoints(this.source, this.panner), this.setupSourceNode(this.source), this.playbackRate = this._playbackRate, this.configureBufferLoopSource();
|
|
263
323
|
} catch (e) {
|
|
264
324
|
throw this.emitAsync("error", {
|
|
265
325
|
error: e,
|
|
@@ -274,7 +334,7 @@ var s = class s extends a {
|
|
|
274
334
|
"loop" in this.source && (this.source.loop = e), "mediaElement" in this.source && this.source.mediaElement && (this.source.mediaElement.loop = e);
|
|
275
335
|
}
|
|
276
336
|
cleanup() {
|
|
277
|
-
if (this._cancelPrepareSeek?.(), this._mediaPreparationGate?.gain.setValueAtTime(0, this.context.currentTime), this._mediaPlayPromise = void 0, this.source) {
|
|
337
|
+
if (this._scheduledStop = void 0, this._cancelPrepareSeek?.(), this._mediaPreparationGate?.gain.setValueAtTime(0, this.context.currentTime), this._mediaPlayPromise = void 0, this.source) {
|
|
278
338
|
if ("mediaElement" in this.source && this.source.mediaElement) {
|
|
279
339
|
let e = this.source.mediaElement;
|
|
280
340
|
e.pause();
|
|
@@ -298,10 +358,15 @@ var s = class s extends a {
|
|
|
298
358
|
loop(e) {
|
|
299
359
|
if (!this.source) throw Error("Cannot loop a sound that has been cleaned up");
|
|
300
360
|
if (e !== void 0) {
|
|
301
|
-
|
|
302
|
-
|
|
361
|
+
this._validateLoopCount(e);
|
|
362
|
+
let t = this.loopCount, n = e === "infinite" ? "infinite" : Math.max(0, e), r = t === "infinite" != (n === "infinite");
|
|
363
|
+
if (r && (this.origin.region || this._clipRegion) && this._state === "playing" && "loop" in this.source && "stop" in this.source) {
|
|
303
364
|
let e = Math.max(this._startTime, this.context.currentTime), r = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);
|
|
304
|
-
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;
|
|
365
|
+
return this._offset = this.regionRelativeOffset(this.regionRelativeOffset(this._offset + r, t), n), "onended" in this.source && (this.source.onended = null), this.source.stop(), this._scheduledStop = void 0, this.loopCount = n, this.currentLoop = 0, this._startTime = e, this.recreateSource(), this.startBufferSource(e), this.loopCount;
|
|
366
|
+
}
|
|
367
|
+
if (r && this._state === "playing" && "buffer" in this.source) {
|
|
368
|
+
let e = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);
|
|
369
|
+
this._offset = this.regionRelativeOffset(this._offset + e, t), this._startTime = Math.max(this._startTime, this.context.currentTime);
|
|
305
370
|
}
|
|
306
371
|
this.loopCount = n, this.currentLoop = 0;
|
|
307
372
|
}
|
|
@@ -313,15 +378,24 @@ var s = class s extends a {
|
|
|
313
378
|
return this.loopCount;
|
|
314
379
|
}
|
|
315
380
|
configureBufferLoopSource() {
|
|
316
|
-
!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));
|
|
381
|
+
!this.source || !("loop" in this.source) || (this.source.loop = this.loopCount === "infinite", this.source.buffer && (this.source.loopEnd = this._clipRegion ? (this.origin.region?.start ?? 0) + this._clipRegion.end : this.origin.region ? this.origin.region.start + this.origin.region.duration : this.source.buffer.duration, this.source.loopStart = (this.origin.region?.start ?? 0) + (this._clipRegion?.start ?? 0)));
|
|
317
382
|
}
|
|
318
383
|
regionRelativeOffset(e, t = this.loopCount) {
|
|
384
|
+
if (this._clipRegion) {
|
|
385
|
+
let { start: n, end: r } = this._clipRegion;
|
|
386
|
+
return t === "infinite" && e >= r ? n + (e - n) % (r - n) : Math.min(r, e);
|
|
387
|
+
}
|
|
319
388
|
let n = this.origin.region;
|
|
320
|
-
return n ? t === "infinite" ? e % n.duration : Math.min(n.duration, e) : e;
|
|
389
|
+
return n ? t === "infinite" ? e % n.duration : Math.min(n.duration, e) : t === "infinite" && this.buffer && this.buffer.duration > 0 ? e % this.buffer.duration : e;
|
|
321
390
|
}
|
|
322
391
|
startBufferSource(e) {
|
|
323
392
|
if (!this.source || !("start" in this.source)) return !1;
|
|
324
393
|
let t = this.origin.region;
|
|
394
|
+
if (this._clipRegion) {
|
|
395
|
+
if (this._offset >= this._clipRegion.end) return !1;
|
|
396
|
+
let n = (t?.start ?? 0) + this._offset;
|
|
397
|
+
return this.loopCount === "infinite" ? this.source.start(e, n) : this.source.start(e, n, this._clipRegion.end - this._offset), !0;
|
|
398
|
+
}
|
|
325
399
|
if (!t) return this.source.start(e, this._offset), !0;
|
|
326
400
|
if (this._offset === t.duration) return !1;
|
|
327
401
|
let n = t.start + this._offset;
|
|
@@ -363,7 +437,8 @@ var s = class s extends a {
|
|
|
363
437
|
}
|
|
364
438
|
clone(e = {}) {
|
|
365
439
|
if (!this.source || !this.gainNode || !this.context) throw Error("Cannot clone a sound that has been cleaned up");
|
|
366
|
-
|
|
440
|
+
if (this._scheduledStop) throw Error("Cannot clone a playback with an armed absolute end");
|
|
441
|
+
this._validateLoopCount(e.loopCount ?? this.loopCount), i(e.spatialSmoothingTau ?? this.spatialSmoothingTau);
|
|
367
442
|
let t = e.panType ?? this.panType;
|
|
368
443
|
r(e, t);
|
|
369
444
|
let n = this.context.createGain();
|
|
@@ -376,7 +451,7 @@ var s = class s extends a {
|
|
|
376
451
|
a = this.context.createMediaElementSource(e);
|
|
377
452
|
} else throw Error("Unsupported source type");
|
|
378
453
|
let o = e.loopCount === void 0 ? this.loopCount : e.loopCount, c = new s(this.origin, a, n);
|
|
379
|
-
return c.loopCount = o, c.currentLoop = this.currentLoop, c.setPanType(t, this.context), c.spatialSmoothingTau = e.spatialSmoothingTau ?? this.spatialSmoothingTau, t === "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) => {
|
|
454
|
+
return c.loopCount = o, c._clipRegion = this._clipRegion && { ...this._clipRegion }, c.currentLoop = this.currentLoop, c.setPanType(t, this.context), c.spatialSmoothingTau = e.spatialSmoothingTau ?? this.spatialSmoothingTau, t === "HRTF" ? (this.panType === "HRTF" && (c.threeDOptions = this.threeDOptions), e.threeDOptions !== void 0 && (c.threeDOptions = e.threeDOptions), e.position !== void 0 && (c.position = e.position)) : c.stereoPan = e.stereoPan ?? this.stereoPan ?? 0, c.volume = this.volume, c.playbackRate = this._playbackRate, c._offset = this._clipRegion && this._state === "playing" ? this.currentTime : this._offset, c.setOcclusion(this.occlusion, 0), this._fadeInConfig && (c._fadeInConfig = { ...this._fadeInConfig }), this._fadeOutConfig && (c._fadeOutConfig = { ...this._fadeOutConfig }), this._state === "paused" ? c.markPaused() : this._state === "stopped" && c.markStopped(), this._filters.forEach((e) => {
|
|
380
455
|
let t = this.context.createBiquadFilter();
|
|
381
456
|
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);
|
|
382
457
|
}), this._pitchFactor !== 1 && c.setPitchShift(this._pitchFactor).catch(() => {}), this._state === "playing" && c.play(), this.origin.playbacks.push(c), c;
|
package/dist/playback.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"playback.mjs","names":[],"sources":["../src/playback.ts"],"sourcesContent":["/**\n * The Playback class encapsulates the functionality for playing audio in a web application.\n * It integrates with the repo's audio-context abstractions so playback works across native and mocked contexts.\n * This class allows for the manipulation of audio playback through various features such as:\n * - Playing and stopping audio\n * - Looping audio a specific number of times or infinitely\n * - Adjusting volume and playback rate\n * - Applying stereo or 3D (HRTF) panning\n * - Adding and removing filters to modify the audio output\n * - Handling audio looping with custom logic\n * - Fading audio in and out linearly or exponentially\n * - Seeking to specific points in the audio\n * - Checking if the audio is currently playing\n * - Cleaning up resources when the audio is no longer needed\n *\n * The class is designed to be flexible and can be used with different types of audio sources,\n * including buffer sources and media elements. It also provides detailed control over the audio's\n * spatial characteristics when using 3D audio.\n */\n\nimport { BasePlayback } from \"./basePlayback\";\nimport type { BaseSound, FadeType, LoopCount, PanType, PlayOptions } from \"./cacophony\";\nimport type {\n AudioBuffer,\n AudioNode,\n AudioParam,\n AudioWorkletNode,\n BaseContext,\n BiquadFilterNode,\n GainNode,\n SourceNode,\n} from \"./context\";\nimport { Occlusion } from \"./occlusion\";\nimport type { PanCloneOverrides } from \"./pannerMixin\";\nimport { applyPlayOptions, validatePlayOptions, validateSpatialOptions } from \"./playOptions\";\nimport type { Sound } from \"./sound\";\nimport { validateSpatialSmoothingTau } from \"./spatialAutomation\";\nimport { WORKLETS } from \"./worklets\";\n\ntype PlaybackCloneOverrides = PanCloneOverrides & {\n loopCount: LoopCount;\n panType: PanType;\n};\n\nexport interface PrepareSeekOptions {\n /** Cancel preparation without starting playback. */\n signal?: AbortSignal;\n /** Maximum readiness wait in milliseconds, default 10,000. Range: (0, 2^31 - 1]. */\n timeoutMs?: number;\n}\n\nexport class Playback extends BasePlayback implements BaseSound {\n private context: BaseContext;\n public declare source?: SourceNode;\n loopCount: LoopCount = 0;\n currentLoop: number = 0;\n private buffer?: AudioBuffer;\n private _offset: number = 0;\n private _startTime: number = 0;\n private _playbackRate: number = 1;\n /**\n * Promise that tracks an in-flight media-element `.play()` settlement.\n * Set when the media element's `play()` returns a pending promise; cleared\n * once that promise settles. Used by `loopEnded` to avoid racing source-state\n * mutations (`seek`/`play`) against the deferred state transition driven by\n * this promise.\n */\n private _mediaPlayPromise?: Promise<void>;\n private _cancelPrepareSeek?: () => void;\n /** Isolates paused media decoding (notably WebKit seek output) from the graph. */\n private _mediaPreparationGate?: GainNode;\n _fadeInConfig?: { duration: number; type: FadeType; perLoop: boolean; targetVolume: number };\n _fadeOutConfig?: { duration: number; type: FadeType };\n _loopEndCallback?: () => void;\n /**\n * Desired pitch-shift factor (1 = no shift, 2 = +1 octave, 0.5 = -1 octave).\n * Stored even before the worklet node exists so a value set on a cleaned-up /\n * not-yet-built playback is honoured once {@link setPitchShift} builds the node.\n */\n private _pitchFactor: number = 1;\n private _occlusion?: Occlusion;\n /**\n * Creates an instance of the Playback class.\n * @throws {Error} Throws an error if an invalid pan type is provided.\n */\n\n constructor(\n public origin: Sound,\n source: SourceNode,\n gainNode: GainNode,\n ) {\n super(origin);\n this.context = origin.context;\n this.loopCount = origin.loopCount;\n this.setPanType(origin.panType, origin.context);\n this.source = source;\n if (\"buffer\" in source && source.buffer) {\n this.buffer = source.buffer;\n }\n this.setupSourceNode(source);\n this.setGainNode(gainNode);\n this.setEffectChainEndpoints(this.source, this.panner!);\n this.panner!.connect(this.gainNode!);\n }\n\n private setupSourceNode(source: SourceNode) {\n if (\"mediaElement\" in source && source.mediaElement) {\n source.mediaElement.onended = this.loopEnded;\n } else if (\"onended\" in source) {\n source.onended = this.loopEnded;\n } else {\n throw new Error(\"Unsupported source type\");\n }\n }\n\n /** The last requested normalized occlusion amount, initially 0 (clear direct path). */\n get occlusion(): number {\n return this._occlusion?.amount ?? 0;\n }\n\n /**\n * Renders obstruction for this voice independently of volume, fades, and positioning.\n * The caller owns geometry, materials, and policy; Cacophony only renders the amount.\n * Finite amounts clamp to [0, 1]; non-finite amounts throw RangeError.\n * A dedicated pre-panner low-pass opens to Nyquist at 0 and closes logarithmically\n * to 800 Hz at 1, with attenuation from 0 to -18 dB. Resetting to 0 is transparent.\n * Nodes are allocated on the first nonzero amount and reused, including after reset.\n * @param amount - 0 for a clear path, 1 for maximum obstruction.\n * @param duration - Linear transition duration in milliseconds, default 50; 0 applies immediately.\n * @throws {RangeError} For a non-finite amount or a negative/non-finite duration.\n * @throws {Error} If this playback has been cleaned up.\n */\n setOcclusion(amount: number, duration = 50): void {\n this.assertNotCleanedUp();\n if (!Number.isFinite(amount)) throw new RangeError(\"Occlusion amount must be finite\");\n if (!Number.isFinite(duration) || duration < 0) {\n throw new RangeError(\"Occlusion duration must be finite and non-negative\");\n }\n const normalized = Math.max(0, Math.min(1, amount));\n if (!this._occlusion) {\n if (normalized === 0) return;\n this._occlusion = new Occlusion(this.context, this.panner!);\n this.setEffectChainEndpoints(this.source!, this.panner!);\n }\n this._occlusion.setAmount(normalized, duration);\n }\n\n protected override setEffectChainEndpoints(input: AudioNode, output: AudioNode): void {\n this._occlusion?.setOutput(output);\n super.setEffectChainEndpoints(this._mediaPreparationGate ?? input, this._occlusion?.input ?? output);\n }\n\n /**\n * Gets the duration of the audio in seconds.\n * @returns {number} The duration of the audio or NaN if the duration is unknown.\n * @throws {Error} Throws an error if the sound has been cleaned up.\n */\n\n get duration() {\n if (this.origin.region) {\n return this.origin.region.duration;\n }\n if (!this.source) {\n throw new Error(\"Cannot get duration of a sound that has been cleaned up\");\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n return this.source.mediaElement.duration;\n }\n if (!this.buffer) {\n return NaN;\n }\n return this.buffer.duration || NaN;\n }\n\n /**\n * Gets the current playback rate of the audio.\n */\n\n get playbackRate() {\n return this._playbackRate;\n }\n\n get volume(): number {\n return super.volume;\n }\n\n set volume(volume: number) {\n super.volume = volume;\n this.emit(\"volumeChange\", volume);\n }\n\n /**\n * Sets the playback rate of the audio.\n * @throws {Error} Throws an error if the sound has been cleaned up or if the source type is unsupported.\n */\n\n set playbackRate(rate: number) {\n if (rate <= 0) {\n throw new Error(\"Playback rate must be greater than 0\");\n }\n if (this._state === \"playing\") {\n const elapsed = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);\n this._offset = this.regionRelativeOffset(this._offset + elapsed);\n if (this.context.currentTime >= this._startTime) {\n this._startTime = this.context.currentTime;\n }\n }\n this._playbackRate = rate;\n if (!this.source) {\n return;\n }\n if (\"playbackRate\" in this.source) {\n this.source.playbackRate.value = rate;\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.playbackRate = rate;\n }\n }\n\n /**\n * Handles the loop event when the audio ends.\n * This method is bound to the 'onended' event of the audio source.\n * It manages looping logic and restarts playback if necessary.\n */\n loopEnded = (): void => {\n if (!this.source || this._state !== \"playing\") {\n return;\n }\n\n // For media-element sources, a previous play() may still have a pending\n // settlement promise -- the deferred state transition races with the\n // seek+play we are about to perform. Defer the loop work until the\n // pending play resolves so source mutations don't interleave.\n if (\"mediaElement\" in this.source && this._mediaPlayPromise) {\n void this._mediaPlayPromise.then(() => this._runLoopEnded());\n return;\n }\n\n this._runLoopEnded();\n };\n\n private _runLoopEnded(): void {\n if (!this.source || this._state !== \"playing\") {\n return;\n }\n\n this.currentLoop++;\n\n if (this.loopCount !== \"infinite\" && this.currentLoop > this.loopCount) {\n // Final iteration -- emit ended, then either fade out or stop immediately\n this.emit(\"ended\", undefined);\n if (this._fadeOutConfig) {\n // Guard the chained stop: an external stop()/cleanup() between the\n // fade resolving and this .then would otherwise run stop()/source\n // access against torn-down state. cancelFade() in stop()/cleanup()\n // resolves the fade promise synchronously, which is what makes this\n // .then fire at all after a teardown.\n void this.fadeOut(this._fadeOutConfig.duration, this._fadeOutConfig.type).then(() => {\n if (this._state !== \"stopped\" && this.source) {\n this.stop();\n this.removeFromOrigin();\n }\n });\n } else {\n this.stop();\n this.removeFromOrigin();\n }\n } else {\n this._loopEndCallback?.();\n this.seek(0); // Resets offset and handles play/pause state internally.\n // seek() calls pause() then play() when wasPlaying is true,\n // so play() handles both media-element and buffer sources correctly.\n if (this._state !== \"playing\" && this.source && !(\"mediaElement\" in this.source)) {\n // For AudioBufferSourceNode only: if seek() didn't restart playback\n // (e.g., state wasn't Playing before seek), start it now.\n this.play();\n }\n\n // Re-trigger fade-in if perLoop config is set\n if (this._fadeInConfig?.perLoop && this.gainNode) {\n this.gainNode.gain.setValueAtTime(0.0001, this.gainNode.context.currentTime);\n this.fadeTo(this._fadeInConfig.targetVolume, this._fadeInConfig.duration, this._fadeInConfig.type);\n }\n }\n }\n\n /**\n * Starts playing the audio.\n * @returns {[this]} Returns the instance of the Playback class for chaining.\n * @throws {Error} Throws an error if the sound has been cleaned up.\n */\n\n play(options?: PlayOptions): [this] {\n if (!this.source) {\n throw new Error(\"Cannot play a sound that has been cleaned up\");\n }\n\n validatePlayOptions(options, this.panType, \"mediaElement\" in this.source ? \"media\" : \"buffer\");\n\n if (this._state === \"playing\") {\n return [this];\n }\n\n this._cancelPrepareSeek?.();\n\n applyPlayOptions(this, this.context, options);\n if (options?.playbackRate !== undefined) this.playbackRate = options.playbackRate;\n if (options?.loopCount !== undefined) this.loop(options.loopCount);\n if (options?.fadeIn !== undefined) {\n void this.fadeIn(options.fadeIn, options.fadeType, { perLoop: options.fadeInPerLoop, startTime: options.at });\n }\n if (options?.fadeOut !== undefined) this.configureFadeOut(options.fadeOut, options.fadeType);\n\n const isResume = this._state === \"paused\";\n if (this._state === \"stopped\") this.currentLoop = 0;\n const originGeneration = this.origin._playbackGeneration;\n\n try {\n let mediaPlayPromise: Promise<void> | undefined;\n\n if (this._state === \"paused\") {\n // If we're resuming from a paused state\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n if (this.source.mediaElement.currentTime !== this._offset) {\n this.source.mediaElement.currentTime = this._offset;\n }\n mediaPlayPromise = this.source.mediaElement.play();\n } else {\n // For non-mediaElement sources, we need to recreate and start the source\n this.recreateSource();\n if (\"start\" in this.source) {\n if (!this.startBufferSource(options?.at ?? 0)) {\n return [this];\n }\n }\n }\n } else {\n // If we're starting from the beginning or a stopped state\n\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n if (this.source.mediaElement.currentTime !== this._offset) {\n this.source.mediaElement.currentTime = this._offset;\n }\n mediaPlayPromise = this.source.mediaElement.play();\n } else if (\"start\" in this.source) {\n this.recreateSource();\n if (!this.startBufferSource(options?.at ?? 0)) {\n return [this];\n }\n }\n }\n\n if (mediaPlayPromise) {\n // For media-element sources, defer state and events until browser accepts playback\n // Stash the pending promise so loopEnded (and any future caller that\n // needs to sequence after the deferred state transition) can await it\n // rather than racing source-state mutations against it.\n const tracked = mediaPlayPromise\n .then(\n () => {\n if (this._mediaPlayPromise !== tracked || !this.source) return;\n if (this.origin._playbackGeneration !== originGeneration) {\n if (\"mediaElement\" in this.source) this.source.mediaElement.pause();\n return;\n }\n this._startTime = this.context.currentTime;\n this.emitPlayStarted(isResume);\n },\n (error: Error) => {\n if (this._mediaPlayPromise !== tracked || this.origin._playbackGeneration !== originGeneration) return;\n void this.emitAsync(\"error\", {\n error,\n errorType: \"source\",\n timestamp: Date.now(),\n recoverable: true,\n });\n },\n )\n .finally(() => {\n if (this._mediaPlayPromise === tracked) {\n this._mediaPlayPromise = undefined;\n }\n });\n this._mediaPlayPromise = tracked;\n } else {\n // For buffer sources, state transition is immediate (start() is synchronous)\n this._startTime = Math.max(options?.at ?? this.context.currentTime, this.context.currentTime);\n this.emitPlayStarted(isResume);\n }\n\n return [this];\n } catch (error) {\n this.emitAsync(\"error\", {\n error: error as Error,\n errorType: \"source\",\n timestamp: Date.now(),\n recoverable: true,\n });\n throw error;\n }\n }\n\n protected override emitPlayStarted(isResume: boolean): void {\n this._mediaPreparationGate?.gain.setValueAtTime(1, this.context.currentTime);\n // A stopped voice may have been reaped by Sound.preplay() or natural end.\n // Restore ownership only after its source has successfully started.\n if (this._state === \"stopped\" && !this.origin.playbacks.includes(this)) {\n this.origin._readmitPlayback(this);\n }\n super.emitPlayStarted(isResume);\n }\n\n pause(): void {\n if (!this.source || this._state !== \"playing\") {\n return;\n }\n\n // A play listener can pause synchronously before the settled play promise's\n // finally runs. That bookkeeping must not block preparing this paused voice.\n this._mediaPlayPromise = undefined;\n\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this._mediaPreparationGate?.gain.setValueAtTime(0, this.context.currentTime);\n this.source.mediaElement.pause();\n // The context keeps advancing through network stalls and native loops.\n // Resume from the element's real position, never an elapsed-time guess.\n this._offset = this.source.mediaElement.currentTime;\n } else {\n const elapsed = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);\n this._offset = this.regionRelativeOffset(this._offset + elapsed);\n if (\"stop\" in this.source) {\n // Buffer sources cannot be restarted; play() creates a new one.\n this.source.stop();\n }\n }\n\n this.emitPaused();\n }\n\n stop(): void {\n if (!this.source) {\n throw new Error(\"Cannot stop a sound that has been cleaned up\");\n }\n this._cancelPrepareSeek?.();\n this._mediaPreparationGate?.gain.setValueAtTime(0, this.context.currentTime);\n const pendingMediaPlay = this._mediaPlayPromise !== undefined;\n this._mediaPlayPromise = undefined;\n if (!pendingMediaPlay && (this._state === \"stopped\" || this._state === \"unplayed\")) {\n return;\n }\n\n this.cancelFade();\n\n if (\"stop\" in this.source && this._state === \"playing\") {\n this.source.stop();\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.pause();\n this.source.mediaElement.currentTime = 0;\n }\n\n this._offset = 0;\n this._startTime = 0;\n this.emitStopped();\n }\n\n /**\n * Fades in from silence to the current volume.\n * Optionally stores config to re-trigger the fade on each loop iteration.\n * @param {number} duration - The fade duration in milliseconds.\n * @param {FadeType} type - The fade curve type. Defaults to \"linear\".\n * @param {object} options - Optional. Set perLoop: true to re-trigger fadeIn on each loop.\n * @returns {Promise<void>} Resolves when the fade completes.\n */\n fadeIn(duration: number, type?: FadeType, options?: { perLoop?: boolean; startTime?: number }): Promise<void> {\n if (options?.perLoop) {\n this._fadeInConfig = {\n duration,\n type: type ?? \"linear\",\n perLoop: true,\n targetVolume: this.gainNode!.gain.value,\n };\n }\n return super.fadeIn(duration, type, { startTime: options?.startTime });\n }\n\n /**\n * Configures a fade-out to be applied when the playback ends naturally (last loop iteration).\n * @param {number} duration - The fade-out duration in milliseconds.\n * @param {FadeType} type - The fade curve type. Defaults to \"linear\".\n */\n configureFadeOut(duration: number, type?: FadeType): void {\n this._fadeOutConfig = { duration, type: type ?? \"linear\" };\n }\n\n /**\n * Fades out then stops the playback.\n * @param {number} duration - The fade-out duration in milliseconds.\n * @param {FadeType} type - The fade curve type. Defaults to \"linear\".\n * @returns {Promise<void>} Resolves when the fade completes and playback is stopped.\n */\n stopWithFade(duration: number, type?: FadeType): Promise<void> {\n // Guard the chained stop: if an external stop()/cleanup() runs between\n // the fade resolving and this .then, the chained stop() would otherwise\n // throw against torn-down state (\"Cannot stop a sound that has been\n // cleaned up\"). cancelFade() inside stop()/cleanup() resolves the fade\n // promise synchronously, so this .then will still fire -- the guard\n // makes it a no-op when the playback is already stopped or cleaned up.\n return this.fadeOut(duration, type).then(() => {\n if (this._state !== \"stopped\" && this.source) {\n this.stop();\n }\n });\n }\n\n /**\n * Silently prepares an idle or paused voice at a finite offset in seconds.\n * Buffers resolve immediately. Media elements wait for metadata, a seekable\n * range containing the target, and completed seeking with current audio data.\n * No play(), context resume, or autoplay primer is performed. State is retained.\n * A later preparation, play, seek, stop, or cleanup rejects pending work with\n * AbortError. Cancellation does not roll back a browser seek already requested.\n * Live/unknown-duration media rejects with NotSupportedError; readiness timeout\n * rejects with TimeoutError. Browser media seeking is not sample accurate.\n */\n async prepareSeek(time: number, options: PrepareSeekOptions = {}): Promise<void> {\n this.assertNotCleanedUp();\n if (!Number.isFinite(time) || time < 0) throw new RangeError(\"Seek time must be finite and non-negative\");\n const timeoutMs = options.timeoutMs ?? 10_000;\n if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2_147_483_647) {\n throw new RangeError(\"Seek timeout must be positive and at most 2147483647 milliseconds\");\n }\n if (options.signal?.aborted) throw new DOMException(\"Seek preparation aborted\", \"AbortError\");\n if (this._state === \"playing\" || this._mediaPlayPromise) {\n throw new Error(\"Pause playback before preparing a seek\");\n }\n this._cancelPrepareSeek?.();\n const source = this.source!;\n if (!(\"mediaElement\" in source)) {\n if (!Number.isFinite(this.duration) || time > this.duration) throw new RangeError(\"Seek time exceeds duration\");\n this._offset = time;\n this.emit(\"seek\", time);\n return;\n }\n const media = source.mediaElement;\n if (!media.paused) throw new Error(\"Pause playback before preparing a seek\");\n if (!this._mediaPreparationGate) {\n const gate = this.context.createGain();\n gate.gain.value = 0;\n this._mediaPreparationGate = gate;\n this.setEffectChainEndpoints(source, this.panner!);\n source.connect(gate);\n }\n this._mediaPreparationGate.gain.setValueAtTime(0, this.context.currentTime);\n await new Promise<void>((resolve, reject) => {\n let finished = false;\n let requested = false;\n let awaitingSeeked = false;\n const events = [\n \"loadedmetadata\",\n \"durationchange\",\n \"progress\",\n \"loadeddata\",\n \"canplay\",\n \"seeked\",\n \"error\",\n \"emptied\",\n \"play\",\n ];\n const settle = (error?: unknown) => {\n if (finished) return;\n finished = true;\n clearTimeout(timer);\n for (const event of events) media.removeEventListener(event, inspect);\n options.signal?.removeEventListener(\"abort\", cancel);\n if (this._cancelPrepareSeek === cancel) this._cancelPrepareSeek = undefined;\n if (error !== undefined) reject(error);\n else {\n this._offset = time;\n resolve();\n }\n };\n const cancel = () => settle(new DOMException(\"Seek preparation aborted\", \"AbortError\"));\n const inspect = (event?: Event) => {\n if (finished) return;\n try {\n if (media.error || event?.type === \"error\") {\n settle(new Error(media.error?.message || \"Media failed during seek preparation\"));\n return;\n }\n // preplay() can queue emptied while assigning the initial resource.\n // Once a seek is requested, a resource reset invalidates that request.\n if ((event?.type === \"emptied\" && requested) || event?.type === \"play\") {\n cancel();\n return;\n }\n if (media.readyState < 1) return;\n if (!Number.isFinite(media.duration)) {\n settle(new DOMException(\"Seek preparation requires finite-duration media\", \"NotSupportedError\"));\n return;\n }\n if (time > media.duration) {\n settle(new RangeError(\"Seek time exceeds duration\"));\n return;\n }\n if (!requested) {\n let inRange = false;\n for (let i = 0; i < media.seekable.length; i++) {\n if (time >= media.seekable.start(i) && time <= media.seekable.end(i)) inRange = true;\n }\n if (!inRange) return;\n requested = true;\n awaitingSeeked = media.seeking || media.currentTime !== time;\n if (media.currentTime !== time) media.currentTime = time;\n } else if (event?.type === \"seeked\") awaitingSeeked = false;\n if (\n !awaitingSeeked &&\n !media.seeking &&\n media.readyState >= 2 &&\n Math.abs(media.currentTime - time) < 0.001\n ) {\n settle();\n }\n } catch (error) {\n settle(error);\n }\n };\n const timer = setTimeout(() => settle(new DOMException(\"Seek preparation timed out\", \"TimeoutError\")), timeoutMs);\n this._cancelPrepareSeek = cancel;\n for (const event of events) media.addEventListener(event, inspect);\n options.signal?.addEventListener(\"abort\", cancel, { once: true });\n inspect();\n });\n // Notify outside the readiness listener's error boundary: observer errors\n // reject this async call instead of being swallowed after it has settled.\n this.emit(\"seek\", time);\n }\n\n seek(time: number): void {\n if (!this.source || !this.gainNode || !this.panner) {\n throw new Error(\"Cannot seek a sound that has been cleaned up\");\n }\n if (!Number.isFinite(time) || time < 0 || (this.origin.region !== undefined && time > this.duration)) {\n throw new Error(\"Invalid time value for seek\");\n }\n\n this._cancelPrepareSeek?.();\n\n if (\n this.origin.soundType === \"streaming\" &&\n \"mediaElement\" in this.source &&\n (this.source.mediaElement.seekable?.length ?? 0) === 0\n ) {\n if (this.source.mediaElement.duration === Number.POSITIVE_INFINITY) {\n throw new Error(\"Live media-element streams do not expose a seekable range\");\n }\n throw new Error(\"Media-element stream does not currently expose a seekable range\");\n }\n\n const wasPlaying = this._state === \"playing\";\n if (wasPlaying) {\n this.pause();\n }\n\n this._offset = time;\n\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.currentTime = time;\n }\n // For non-media elements, play() will handle recreating the source if needed.\n\n if (wasPlaying) {\n this.play();\n }\n this.emit(\"seek\", time);\n }\n\n get currentTime(): number {\n if (this._state === \"playing\") {\n const elapsed = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);\n const time = this._offset + elapsed;\n return this.regionRelativeOffset(time);\n } else {\n return this._offset;\n }\n }\n\n private removeFromOrigin(): void {\n const idx = this.origin.playbacks.indexOf(this);\n if (idx !== -1) {\n this.origin.playbacks.splice(idx, 1);\n }\n }\n\n private recreateSource() {\n if (!this.buffer || !this.panner || !this.context || !this.gainNode) {\n throw new Error(\"Cannot recreate source of a sound that has been cleaned up\");\n }\n try {\n if (this.source) {\n // It's crucial to nullify onended of the old source if it's an AudioBufferSourceNode (or similar non-restartable source),\n // as its onended event could otherwise interfere with the new source created for seek/resume.\n // MediaElementAudioSourceNode is handled differently as its underlying element can be paused/played.\n if (!(\"mediaElement\" in this.source) && \"onended\" in this.source) {\n this.source.onended = null;\n }\n this.source.disconnect();\n }\n this.source = this.context.createBufferSource();\n this.source.buffer = this.buffer;\n this.setEffectChainEndpoints(this.source, this.panner);\n this.source.onended = this.loopEnded;\n this.playbackRate = this._playbackRate;\n this.configureBufferLoopSource();\n } catch (error) {\n this.emitAsync(\"error\", {\n error: error as Error,\n errorType: \"source\",\n timestamp: Date.now(),\n recoverable: false,\n });\n throw error;\n }\n }\n\n /**\n * Sets whether the audio source should loop.\n * @throws {Error} Throws an error if the sound has been cleaned up.\n */\n set sourceLoop(loop: boolean) {\n if (!this.source) {\n throw new Error(\"Cannot set loop on a sound that has been cleaned up\");\n }\n if (\"loop\" in this.source) {\n this.source.loop = loop;\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.loop = loop;\n }\n }\n\n /**\n * Cleans up resources used by the Playback instance.\n * This method should be called when the audio is no longer needed to free up resources.\n */\n\n cleanup(): void {\n this._cancelPrepareSeek?.();\n this._mediaPreparationGate?.gain.setValueAtTime(0, this.context.currentTime);\n this._mediaPlayPromise = undefined;\n if (!this.source) {\n return; // Already cleaned up\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n const mediaElement = this.source.mediaElement;\n mediaElement.pause();\n try {\n mediaElement.currentTime = 0;\n } catch {\n // Live media without a seekable range can reject currentTime writes.\n }\n mediaElement.loop = false;\n mediaElement.onended = null;\n mediaElement.src = \"\";\n mediaElement.load();\n } else if (\"onended\" in this.source) {\n this.source.onended = null;\n // Disconnecting alone leaves a started source running, including native loops.\n // Scheduled starts also have playing state; unplayed/paused/stopped sources do not need stop().\n if (\"stop\" in this.source && this._state === \"playing\") {\n this.source.stop();\n }\n }\n this._offset = 0;\n this._startTime = 0;\n this.markStopped();\n this.currentLoop = 0;\n this.source.disconnect();\n this.source = undefined;\n super.cleanup();\n this._mediaPreparationGate?.disconnect();\n this._mediaPreparationGate = undefined;\n this._occlusion?.destroy();\n this._occlusion = undefined;\n }\n\n private assertNotCleanedUp(): void {\n if (!this.source || !this.gainNode || !this.panner) {\n throw new Error(\"Cannot perform operation on a sound that has been cleaned up\");\n }\n }\n\n addFilter(filter: BiquadFilterNode): void {\n this.assertNotCleanedUp();\n super.addFilter(filter);\n }\n\n removeFilter(filter: BiquadFilterNode): void {\n this.assertNotCleanedUp();\n super.removeFilter(filter);\n }\n\n /**\n * Sets or gets the loop count for the audio.\n * @param {LoopCount} loopCount - The number of times the audio should loop. 'infinite' for endless looping.\n * @returns {LoopCount} The loop count if no parameter is provided.\n * @throws {Error} Throws an error if the sound has been cleaned up or if the source type is unsupported.\n */\n\n loop(loopCount?: LoopCount): LoopCount {\n if (!this.source) {\n throw new Error(\"Cannot loop a sound that has been cleaned up\");\n }\n if (loopCount !== undefined) {\n const previousLoopCount = this.loopCount;\n const nextLoopCount = loopCount === \"infinite\" ? \"infinite\" : Math.max(0, loopCount);\n const changesInfiniteMode = (previousLoopCount === \"infinite\") !== (nextLoopCount === \"infinite\");\n if (\n changesInfiniteMode &&\n this.origin.region &&\n this._state === \"playing\" &&\n \"loop\" in this.source &&\n \"stop\" in this.source\n ) {\n const restartAt = Math.max(this._startTime, this.context.currentTime);\n const elapsed = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);\n this._offset = this.regionRelativeOffset(\n this.regionRelativeOffset(this._offset + elapsed, previousLoopCount),\n nextLoopCount,\n );\n if (\"onended\" in this.source) {\n this.source.onended = null;\n }\n this.source.stop();\n this.loopCount = nextLoopCount;\n this.currentLoop = 0;\n this._startTime = restartAt;\n this.recreateSource();\n this.startBufferSource(restartAt);\n return this.loopCount;\n }\n this.loopCount = nextLoopCount;\n this.currentLoop = 0;\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n const mediaElement = this.source.mediaElement;\n mediaElement.loop = this.loopCount === \"infinite\";\n } else if (\"loop\" in this.source) {\n this.configureBufferLoopSource();\n } else {\n throw new Error(\"Unsupported source type\");\n }\n return this.loopCount;\n }\n\n /** Copy loop flags and atlas bounds to the current buffer source without changing loop progress. */\n private configureBufferLoopSource(): void {\n if (!this.source || !(\"loop\" in this.source)) return;\n this.source.loop = this.loopCount === \"infinite\";\n if (this.source.buffer) {\n this.source.loopEnd = this.origin.region\n ? this.origin.region.start + this.origin.region.duration\n : this.source.buffer.duration;\n this.source.loopStart = this.origin.region?.start ?? 0;\n }\n }\n\n /** Clamp finite regions and wrap infinite regions while keeping offsets region-relative. */\n private regionRelativeOffset(offset: number, loopCount: LoopCount = this.loopCount): number {\n const region = this.origin.region;\n if (!region) {\n return offset;\n }\n return loopCount === \"infinite\" ? offset % region.duration : Math.min(region.duration, offset);\n }\n\n /** Start a buffer source, translating the region-relative offset once. */\n private startBufferSource(when: number): boolean {\n if (!this.source || !(\"start\" in this.source)) {\n return false;\n }\n const region = this.origin.region;\n if (!region) {\n this.source.start(when, this._offset);\n return true;\n }\n if (this._offset === region.duration) {\n return false;\n }\n const absoluteOffset = region.start + this._offset;\n if (this.loopCount === \"infinite\") {\n this.source.start(when, absoluteOffset);\n } else {\n this.source.start(when, absoluteOffset, region.duration - this._offset);\n }\n return true;\n }\n\n /**\n * Sets the pitch-shift factor for this playback, resurrecting the dormant\n * phase-vocoder worklet (Jean Laroche & Mark Dolson, \"New Phase-Vocoder\n * Techniques for Pitch-Shifting, Harmonizing and Other Exotic Effects\",\n * 1999 IEEE WASPAA — peak-based pitch shift with Identity Phase-Locking).\n *\n * On first use (factor !== 1) the phase-vocoder AudioWorkletNode is built and\n * added as an ordinary entry at the tail of the pre-panner effect chain:\n * source → [filters] → phase vocoder → panner → gainNode. The factor is\n * forwarded to the node's `pitchFactor` AudioParam (1 = no shift, 2 = +1\n * octave, 0.5 = -1 octave).\n *\n * @param factor Pitch multiplier (> 0).\n * @throws {Error} if the playback has been cleaned up or factor <= 0.\n */\n async setPitchShift(factor: number): Promise<void> {\n this.assertNotCleanedUp();\n if (!Number.isFinite(factor) || factor <= 0) {\n throw new Error(\"Pitch-shift factor must be greater than 0\");\n }\n this._pitchFactor = factor;\n const effectChain = this._effectChain;\n if (!effectChain) {\n throw new Error(\"Cannot pitch-shift a playback before its effect chain is initialized\");\n }\n let pitchShiftNode = effectChain.nodes.find((node) => {\n const parameters = (node as AudioWorkletNode).parameters;\n return parameters && typeof parameters.get === \"function\" && parameters.get(\"pitchFactor\") !== undefined;\n }) as AudioWorkletNode | undefined;\n\n // factor === 1 is the documented \"no shift\" contract and MUST be a genuine\n // passthrough. The peak/region phase-vocoder pipeline does NOT guarantee\n // identity for peakless / edge-bin / broadband content (it zero-fills and\n // only repopulates detected peak regions), so we cannot leave the node in\n // the chain at factor 1. Remove its ordinary chain entry so the signal\n // bypasses the worklet entirely (source → [filters] → panner). A later\n // non-unity factor rebuilds a fresh node.\n if (factor === 1) {\n if (pitchShiftNode) {\n effectChain.remove(pitchShiftNode);\n }\n return;\n }\n\n if (!pitchShiftNode) {\n const cacophony = this.origin.cacophony;\n if (!cacophony) {\n throw new Error(\"Cannot pitch-shift a playback whose Sound has no Cacophony instance\");\n }\n pitchShiftNode = await cacophony.buildWorkletEffect(WORKLETS.phaseVocoder, {}, this.context);\n effectChain.add(pitchShiftNode);\n }\n\n const pitchParam = pitchShiftNode.parameters?.get(\"pitchFactor\");\n if (pitchParam) {\n pitchParam.value = factor;\n }\n }\n\n /**\n * The current pitch-shift factor (1 = no shift). See {@link setPitchShift}.\n */\n get pitchShift(): number {\n return this._pitchFactor;\n }\n\n /**\n * Gets the output node of this playback's audio graph.\n * This is the final node in the internal chain before connection to destination.\n * Use this to manually wire the playback into custom audio graphs.\n *\n * @returns {GainNode} The gain node that serves as the output of this playback.\n * @throws {Error} Throws an error if the playback has been cleaned up.\n *\n * @example\n * // Manual routing through custom effects\n * const playback = sound.play()[0];\n * playback.disconnect(); // Disconnect from default destination\n * playback.connect(reverbNode).connect(context.destination);\n */\n get outputNode(): GainNode {\n if (!this.gainNode) {\n throw new Error(\"Cannot access output node of a playback that has been cleaned up\");\n }\n return this.gainNode;\n }\n\n /**\n * Connects this playback's output to an AudioNode or AudioParam.\n * Follows the Web Audio API connection pattern.\n *\n * @returns {AudioNode} The destination node (for chaining).\n * @throws {Error} Throws an error if the playback has been cleaned up.\n *\n * @example\n * // Chain multiple effects\n * playback.connect(delay).connect(reverb).connect(context.destination);\n */\n connect(destination: AudioNode | AudioParam): AudioNode {\n return this.outputNode.connect(destination as any);\n }\n\n /**\n * Disconnects this playback's output from a specific destination or from all destinations.\n *\n * @param {AudioNode | AudioParam} [destination] - Optional specific destination to disconnect from.\n * If omitted, disconnects from all destinations.\n * @throws {Error} Throws an error if the playback has been cleaned up.\n *\n * @example\n * // Disconnect from all\n * playback.disconnect();\n *\n * @example\n * // Disconnect from specific node\n * playback.disconnect(reverbNode);\n */\n disconnect(destination?: AudioNode | AudioParam): void {\n if (destination) {\n this.outputNode.disconnect(destination as any);\n } else {\n this.outputNode.disconnect();\n }\n }\n\n /**\n * Creates a clone of the current Playback instance with optional overrides for certain properties.\n * This method allows for the creation of a new Playback instance that shares the same audio context\n * and source node but can have different settings such as loop count or pan type.\n * The clone is connected to the origin Sound's primary route and registered with the Sound.\n * @throws {Error} Throws an error if the sound has been cleaned up.\n */\n\n clone(overrides: Partial<PlaybackCloneOverrides> = {}): Playback {\n if (!this.source || !this.gainNode || !this.context) {\n throw new Error(\"Cannot clone a sound that has been cleaned up\");\n }\n validateSpatialSmoothingTau(overrides.spatialSmoothingTau ?? this.spatialSmoothingTau);\n const panType = overrides.panType ?? this.panType;\n validateSpatialOptions(overrides, panType);\n // we'll need to create a new gain node\n const gainNode = this.context.createGain();\n gainNode.connect(this.origin._resolveRouteTargetNode());\n // clone the source node\n let source: SourceNode;\n if (\"buffer\" in this.source && this.source.buffer) {\n source = this.context.createBufferSource();\n source.buffer = this.source.buffer;\n } else if (\"mediaElement\" in this.source && this.source.mediaElement) {\n if (!this.context.createMediaElementSource) {\n throw new Error(\"Media element sources are not supported on this audio context.\");\n }\n // The Web Audio spec forbids creating a second MediaElementAudioSourceNode\n // from the same HTMLMediaElement -- the second call throws\n // InvalidStateError. Clone the underlying element first so the new\n // playback owns an independent element that hasn't been bound yet.\n const originalElement = this.source.mediaElement;\n const clonedElement = originalElement.cloneNode(true) as HTMLMediaElement;\n source = this.context.createMediaElementSource(clonedElement);\n } else {\n throw new Error(\"Unsupported source type\");\n }\n const loopCount = overrides.loopCount !== undefined ? overrides.loopCount : this.loopCount;\n const clone = new Playback(this.origin, source, gainNode);\n\n // Copy all relevant properties\n clone.loopCount = loopCount;\n clone.currentLoop = this.currentLoop;\n clone.setPanType(panType, this.context);\n clone.spatialSmoothingTau = overrides.spatialSmoothingTau ?? this.spatialSmoothingTau;\n if (panType === \"HRTF\") {\n if (this.panType === \"HRTF\") clone.threeDOptions = this.threeDOptions;\n if (overrides.threeDOptions !== undefined) clone.threeDOptions = overrides.threeDOptions;\n if (overrides.position !== undefined) clone.position = overrides.position;\n } else {\n clone.stereoPan = overrides.stereoPan ?? this.stereoPan ?? 0;\n }\n clone.volume = this.volume;\n clone.playbackRate = this._playbackRate;\n clone._offset = this._offset;\n clone.setOcclusion(this.occlusion, 0);\n if (this._fadeInConfig) {\n clone._fadeInConfig = { ...this._fadeInConfig };\n }\n if (this._fadeOutConfig) {\n clone._fadeOutConfig = { ...this._fadeOutConfig };\n }\n if (this._state === \"paused\") {\n clone.markPaused();\n } else if (this._state === \"stopped\") {\n clone.markStopped();\n }\n\n // Deep clone filters\n this._filters.forEach((filter) => {\n const clonedFilter = this.context.createBiquadFilter();\n clonedFilter.type = filter.type;\n clonedFilter.frequency.value = filter.frequency.value;\n clonedFilter.Q.value = filter.Q.value;\n clonedFilter.gain.value = filter.gain.value;\n clone.addFilter(clonedFilter as unknown as BiquadFilterNode);\n });\n\n if (this._pitchFactor !== 1) {\n void clone.setPitchShift(this._pitchFactor).catch(() => {\n /* worklet unavailable on this context — clone proceeds unshifted */\n });\n }\n\n // If the original is playing, start the clone\n if (this._state === \"playing\") {\n clone.play();\n }\n\n this.origin.playbacks.push(clone);\n return clone;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"playback.mjs","names":[],"sources":["../src/playback.ts"],"sourcesContent":["/**\n * The Playback class encapsulates the functionality for playing audio in a web application.\n * It integrates with the repo's audio-context abstractions so playback works across native and mocked contexts.\n * This class allows for the manipulation of audio playback through various features such as:\n * - Playing and stopping audio\n * - Looping audio a specific number of times or infinitely\n * - Adjusting volume and playback rate\n * - Applying stereo or 3D (HRTF) panning\n * - Adding and removing filters to modify the audio output\n * - Handling audio looping with custom logic\n * - Fading audio in and out linearly or exponentially\n * - Seeking to specific points in the audio\n * - Checking if the audio is currently playing\n * - Cleaning up resources when the audio is no longer needed\n *\n * The class is designed to be flexible and can be used with different types of audio sources,\n * including buffer sources and media elements. It also provides detailed control over the audio's\n * spatial characteristics when using 3D audio.\n */\n\nimport { BasePlayback } from \"./basePlayback\";\nimport type { BaseSound, FadeType, LoopCount, PanType, PlayOptions } from \"./cacophony\";\nimport type {\n AudioBuffer,\n AudioNode,\n AudioParam,\n AudioWorkletNode,\n BaseContext,\n BiquadFilterNode,\n GainNode,\n SourceNode,\n} from \"./context\";\nimport { Occlusion } from \"./occlusion\";\nimport type { PanCloneOverrides } from \"./pannerMixin\";\nimport { applyPlayOptions, validatePlayOptions, validateSpatialOptions } from \"./playOptions\";\nimport type { Sound } from \"./sound\";\nimport { validateSpatialSmoothingTau } from \"./spatialAutomation\";\nimport { WORKLETS } from \"./worklets\";\n\ntype PlaybackCloneOverrides = PanCloneOverrides & {\n loopCount: LoopCount;\n panType: PanType;\n};\n\nexport interface PrepareSeekOptions {\n /** Cancel preparation without starting playback. */\n signal?: AbortSignal;\n /** Maximum readiness wait in milliseconds, default 10,000. Range: (0, 2^31 - 1]. */\n timeoutMs?: number;\n}\n\nexport class Playback extends BasePlayback implements BaseSound {\n private context: BaseContext;\n public declare source?: SourceNode;\n loopCount: LoopCount = 0;\n currentLoop: number = 0;\n private buffer?: AudioBuffer;\n private _offset: number = 0;\n private _startTime: number = 0;\n private _playbackRate: number = 1;\n private _clipRegion?: Readonly<{ start: number; end: number }>;\n private _scheduledStop?: { source: SourceNode; at: number };\n /**\n * Promise that tracks an in-flight media-element `.play()` settlement.\n * Set when the media element's `play()` returns a pending promise; cleared\n * once that promise settles. Used by `loopEnded` to avoid racing source-state\n * mutations (`seek`/`play`) against the deferred state transition driven by\n * this promise.\n */\n private _mediaPlayPromise?: Promise<void>;\n private _cancelPrepareSeek?: () => void;\n /** Isolates paused media decoding (notably WebKit seek output) from the graph. */\n private _mediaPreparationGate?: GainNode;\n _fadeInConfig?: { duration: number; type: FadeType; perLoop: boolean; targetVolume: number };\n _fadeOutConfig?: { duration: number; type: FadeType };\n _loopEndCallback?: () => void;\n /**\n * Desired pitch-shift factor (1 = no shift, 2 = +1 octave, 0.5 = -1 octave).\n * Stored even before the worklet node exists so a value set on a cleaned-up /\n * not-yet-built playback is honoured once {@link setPitchShift} builds the node.\n */\n private _pitchFactor: number = 1;\n private _occlusion?: Occlusion;\n /**\n * Creates an instance of the Playback class.\n * @throws {Error} Throws an error if an invalid pan type is provided.\n */\n\n constructor(\n public origin: Sound,\n source: SourceNode,\n gainNode: GainNode,\n ) {\n super(origin);\n this.context = origin.context;\n this.loopCount = origin.loopCount;\n this.setPanType(origin.panType, origin.context);\n this.source = source;\n if (\"buffer\" in source && source.buffer) {\n this.buffer = source.buffer;\n }\n this.setupSourceNode(source);\n this.setGainNode(gainNode);\n this.setEffectChainEndpoints(this.source, this.panner!);\n this.panner!.connect(this.gainNode!);\n }\n\n private setupSourceNode(source: SourceNode) {\n if (\"mediaElement\" in source && source.mediaElement) {\n source.mediaElement.onended = this.loopEnded;\n } else if (\"onended\" in source) {\n source.onended = () => {\n if (this.source === source) this.loopEnded();\n };\n } else {\n throw new Error(\"Unsupported source type\");\n }\n }\n\n /** The last requested normalized occlusion amount, initially 0 (clear direct path). */\n get occlusion(): number {\n return this._occlusion?.amount ?? 0;\n }\n\n /**\n * Renders obstruction for this voice independently of volume, fades, and positioning.\n * The caller owns geometry, materials, and policy; Cacophony only renders the amount.\n * Finite amounts clamp to [0, 1]; non-finite amounts throw RangeError.\n * A dedicated pre-panner low-pass opens to Nyquist at 0 and closes logarithmically\n * to 800 Hz at 1, with attenuation from 0 to -18 dB. Resetting to 0 is transparent.\n * Nodes are allocated on the first nonzero amount and reused, including after reset.\n * @param amount - 0 for a clear path, 1 for maximum obstruction.\n * @param duration - Linear transition duration in milliseconds, default 50; 0 applies immediately.\n * @throws {RangeError} For a non-finite amount or a negative/non-finite duration.\n * @throws {Error} If this playback has been cleaned up.\n */\n setOcclusion(amount: number, duration = 50): void {\n this.assertNotCleanedUp();\n if (!Number.isFinite(amount)) throw new RangeError(\"Occlusion amount must be finite\");\n if (!Number.isFinite(duration) || duration < 0) {\n throw new RangeError(\"Occlusion duration must be finite and non-negative\");\n }\n const normalized = Math.max(0, Math.min(1, amount));\n if (!this._occlusion) {\n if (normalized === 0) return;\n this._occlusion = new Occlusion(this.context, this.panner!);\n this.setEffectChainEndpoints(this.source!, this.panner!);\n }\n this._occlusion.setAmount(normalized, duration);\n }\n\n protected override setEffectChainEndpoints(input: AudioNode, output: AudioNode): void {\n this._occlusion?.setOutput(output);\n super.setEffectChainEndpoints(this._mediaPreparationGate ?? input, this._occlusion?.input ?? output);\n }\n\n /**\n * Gets the duration of the audio in seconds.\n * @returns {number} The duration of the audio or NaN if the duration is unknown.\n * @throws {Error} Throws an error if the sound has been cleaned up.\n */\n\n get duration() {\n if (this.origin.region) {\n return this.origin.region.duration;\n }\n if (!this.source) {\n throw new Error(\"Cannot get duration of a sound that has been cleaned up\");\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n return this.source.mediaElement.duration;\n }\n if (!this.buffer) {\n return NaN;\n }\n return this.buffer.duration || NaN;\n }\n\n /**\n * Gets the current playback rate of the audio.\n */\n\n get playbackRate() {\n return this._playbackRate;\n }\n\n /** Whether this live voice can enforce native buffer clip boundaries. HTML media cannot. */\n get supportsClipLoop(): boolean {\n return !!this.source && \"buffer\" in this.source && !!this.buffer;\n }\n\n /**\n * Configure a hard clip end and native loop points, in Sound-relative seconds.\n * Configure while idle or paused. loop(0) ends at R; loop(\"infinite\") wraps R to L.\n * Numeric repeats and per-loop fades are unsupported with explicit bounds.\n */\n setLoopRegion(startSeconds: number, endSeconds: number): void {\n this.assertNotCleanedUp();\n if (!this.supportsClipLoop) throw new DOMException(\"Clip bounds require a decoded buffer\", \"NotSupportedError\");\n if (this._state === \"playing\") throw new Error(\"Pause playback before configuring clip bounds\");\n if (\n !this.buffer ||\n !Number.isFinite(startSeconds) ||\n !Number.isFinite(endSeconds) ||\n startSeconds < 0 ||\n endSeconds > this.duration ||\n (endSeconds - startSeconds) * this.buffer.sampleRate +\n Number.EPSILON *\n Math.max(1, Math.abs(startSeconds * this.buffer.sampleRate), Math.abs(endSeconds * this.buffer.sampleRate)) *\n 4 <\n 1 ||\n Math.round(endSeconds * this.buffer.sampleRate) <= Math.round(startSeconds * this.buffer.sampleRate)\n ) {\n throw new RangeError(\"Clip bounds must span at least one sample within the Sound duration\");\n }\n if (this._offset >= endSeconds) throw new RangeError(\"The current offset must precede the clip end\");\n if (this.loopCount !== 0 && this.loopCount !== \"infinite\")\n throw new Error(\"Clip bounds support only zero or infinite loop count\");\n if (this._fadeInConfig?.perLoop) throw new Error(\"Per-loop fades are not supported with clip bounds\");\n this._clipRegion = { start: startSeconds, end: endSeconds };\n this.configureBufferLoopSource();\n }\n\n /**\n * End this started buffer activation at an absolute AudioContext time.\n * A deadline before a future onset prevents all onset audio. Past deadlines stop now.\n * Rate changes do not move this render-clock deadline. A later call replaces it;\n * pause, seek, source recreation, stop and cleanup revoke it. Downstream effect tails remain.\n * Legacy numeric repeats and ended-time fade-outs cannot share this per-source deadline.\n */\n scheduleStop(atAudioSeconds: number): void {\n this.assertNotCleanedUp();\n if (!this.supportsClipLoop || !this.source || !(\"stop\" in this.source))\n throw new DOMException(\"Scheduled ends require a decoded buffer\", \"NotSupportedError\");\n if (!Number.isFinite(atAudioSeconds) || atAudioSeconds < 0)\n throw new RangeError(\"End time must be finite and non-negative\");\n if (this._state !== \"playing\") throw new Error(\"Start the buffer before scheduling its end\");\n if (this.loopCount !== 0 && this.loopCount !== \"infinite\")\n throw new Error(\"Scheduled ends do not support legacy numeric repeats\");\n if (this._fadeOutConfig) throw new Error(\"An absolute end cannot use an ended-time fade-out\");\n const at = Math.max(this.context.currentTime, atAudioSeconds);\n this.source.stop(at);\n this._scheduledStop = { source: this.source, at };\n }\n\n /** @internal Preflight Sound broadcasts and play options before any mutation. */\n _validateLoopCount(loopCount: LoopCount): void {\n if (this._clipRegion && loopCount !== 0 && loopCount !== \"infinite\")\n throw new Error(\"Clip bounds support only zero or infinite loop count\");\n if (this._scheduledStop && loopCount !== 0 && loopCount !== \"infinite\")\n throw new Error(\"Scheduled ends do not support legacy numeric repeats\");\n }\n\n get volume(): number {\n return super.volume;\n }\n\n set volume(volume: number) {\n super.volume = volume;\n this.emit(\"volumeChange\", volume);\n }\n\n /**\n * Sets the playback rate of the audio.\n * @throws {Error} Throws an error if the sound has been cleaned up or if the source type is unsupported.\n */\n\n set playbackRate(rate: number) {\n if (rate <= 0) {\n throw new Error(\"Playback rate must be greater than 0\");\n }\n if (this._state === \"playing\") {\n const elapsed = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);\n this._offset = this.regionRelativeOffset(this._offset + elapsed);\n if (this.context.currentTime >= this._startTime) {\n this._startTime = this.context.currentTime;\n }\n }\n this._playbackRate = rate;\n if (!this.source) {\n return;\n }\n if (\"playbackRate\" in this.source) {\n this.source.playbackRate.value = rate;\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.playbackRate = rate;\n }\n }\n\n /**\n * Handles the loop event when the audio ends.\n * This method is bound to the 'onended' event of the audio source.\n * It manages looping logic and restarts playback if necessary.\n */\n loopEnded = (): void => {\n if (!this.source || this._state !== \"playing\") {\n return;\n }\n if (this._scheduledStop?.source === this.source) {\n const source = this.source;\n this._scheduledStop = undefined;\n if (\"onended\" in source) source.onended = null;\n try {\n this.emit(\"ended\", undefined);\n } finally {\n if (this.source === source && this._state === \"playing\") this.stop();\n if (this.source === source && this.state === \"stopped\") this.removeFromOrigin();\n }\n return;\n }\n\n // For media-element sources, a previous play() may still have a pending\n // settlement promise -- the deferred state transition races with the\n // seek+play we are about to perform. Defer the loop work until the\n // pending play resolves so source mutations don't interleave.\n if (\"mediaElement\" in this.source && this._mediaPlayPromise) {\n void this._mediaPlayPromise.then(() => this._runLoopEnded());\n return;\n }\n\n this._runLoopEnded();\n };\n\n private _runLoopEnded(): void {\n if (!this.source || this._state !== \"playing\") {\n return;\n }\n\n const source = this.source;\n this.currentLoop++;\n\n if (this.loopCount !== \"infinite\" && this.currentLoop > this.loopCount) {\n // Final iteration -- emit ended, then either fade out or stop immediately\n this.emit(\"ended\", undefined);\n if (this.source !== source || this._state !== \"playing\") return;\n if (this._fadeOutConfig) {\n // Guard the chained stop: an external stop()/cleanup() between the\n // fade resolving and this .then would otherwise run stop()/source\n // access against torn-down state. cancelFade() in stop()/cleanup()\n // resolves the fade promise synchronously, which is what makes this\n // .then fire at all after a teardown.\n void this.fadeOut(this._fadeOutConfig.duration, this._fadeOutConfig.type).then(() => {\n if (this._state !== \"stopped\" && this.source === source) {\n this.stop();\n if (this.source === source && this.state === \"stopped\") this.removeFromOrigin();\n }\n });\n } else {\n this.stop();\n if (this.source === source && this.state === \"stopped\") this.removeFromOrigin();\n }\n } else {\n this._loopEndCallback?.();\n if (this.source !== source || this._state !== \"playing\") return;\n this.seek(0); // Resets offset and handles play/pause state internally.\n // seek() calls pause() then play() when wasPlaying is true,\n // so play() handles both media-element and buffer sources correctly.\n if (this._state !== \"playing\" && this.source && !(\"mediaElement\" in this.source)) {\n // For AudioBufferSourceNode only: if seek() didn't restart playback\n // (e.g., state wasn't Playing before seek), start it now.\n this.play();\n }\n\n // Re-trigger fade-in if perLoop config is set\n if (this._fadeInConfig?.perLoop && this.gainNode) {\n this.gainNode.gain.setValueAtTime(0.0001, this.gainNode.context.currentTime);\n this.fadeTo(this._fadeInConfig.targetVolume, this._fadeInConfig.duration, this._fadeInConfig.type);\n }\n }\n }\n\n /**\n * Starts playing the audio.\n * @returns {[this]} Returns the instance of the Playback class for chaining.\n * @throws {Error} Throws an error if the sound has been cleaned up.\n */\n\n play(options?: PlayOptions): [this] {\n if (!this.source) {\n throw new Error(\"Cannot play a sound that has been cleaned up\");\n }\n\n validatePlayOptions(options, this.panType, \"mediaElement\" in this.source ? \"media\" : \"buffer\");\n if (options?.loopCount !== undefined) this._validateLoopCount(options.loopCount);\n if (this._clipRegion && options?.fadeInPerLoop)\n throw new Error(\"Per-loop fades are not supported with clip bounds\");\n if (this._scheduledStop && options?.fadeOut !== undefined)\n throw new Error(\"An absolute end cannot use an ended-time fade-out\");\n\n if (this._state === \"playing\") {\n return [this];\n }\n\n this._cancelPrepareSeek?.();\n this._scheduledStop = undefined;\n\n applyPlayOptions(this, this.context, options);\n if (options?.playbackRate !== undefined) this.playbackRate = options.playbackRate;\n if (options?.loopCount !== undefined) this.loop(options.loopCount);\n if (options?.fadeIn !== undefined) {\n void this.fadeIn(options.fadeIn, options.fadeType, { perLoop: options.fadeInPerLoop, startTime: options.at });\n }\n if (options?.fadeOut !== undefined) this.configureFadeOut(options.fadeOut, options.fadeType);\n\n const isResume = this._state === \"paused\";\n if (this._state === \"stopped\") this.currentLoop = 0;\n const originGeneration = this.origin._playbackGeneration;\n\n try {\n let mediaPlayPromise: Promise<void> | undefined;\n\n if (this._state === \"paused\") {\n // If we're resuming from a paused state\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n if (this.source.mediaElement.currentTime !== this._offset) {\n this.source.mediaElement.currentTime = this._offset;\n }\n mediaPlayPromise = this.source.mediaElement.play();\n } else {\n // For non-mediaElement sources, we need to recreate and start the source\n this.recreateSource();\n if (\"start\" in this.source) {\n if (!this.startBufferSource(options?.at ?? 0)) {\n return [this];\n }\n }\n }\n } else {\n // If we're starting from the beginning or a stopped state\n\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n if (this.source.mediaElement.currentTime !== this._offset) {\n this.source.mediaElement.currentTime = this._offset;\n }\n mediaPlayPromise = this.source.mediaElement.play();\n } else if (\"start\" in this.source) {\n this.recreateSource();\n if (!this.startBufferSource(options?.at ?? 0)) {\n return [this];\n }\n }\n }\n\n if (mediaPlayPromise) {\n // For media-element sources, defer state and events until browser accepts playback\n // Stash the pending promise so loopEnded (and any future caller that\n // needs to sequence after the deferred state transition) can await it\n // rather than racing source-state mutations against it.\n const tracked = mediaPlayPromise\n .then(\n () => {\n if (this._mediaPlayPromise !== tracked || !this.source) return;\n if (this.origin._playbackGeneration !== originGeneration) {\n if (\"mediaElement\" in this.source) this.source.mediaElement.pause();\n return;\n }\n this._startTime = this.context.currentTime;\n this.emitPlayStarted(isResume);\n },\n (error: Error) => {\n if (this._mediaPlayPromise !== tracked || this.origin._playbackGeneration !== originGeneration) return;\n void this.emitAsync(\"error\", {\n error,\n errorType: \"source\",\n timestamp: Date.now(),\n recoverable: true,\n });\n },\n )\n .finally(() => {\n if (this._mediaPlayPromise === tracked) {\n this._mediaPlayPromise = undefined;\n }\n });\n this._mediaPlayPromise = tracked;\n } else {\n // For buffer sources, state transition is immediate (start() is synchronous)\n this._startTime = Math.max(options?.at ?? this.context.currentTime, this.context.currentTime);\n this.emitPlayStarted(isResume);\n }\n\n return [this];\n } catch (error) {\n this.emitAsync(\"error\", {\n error: error as Error,\n errorType: \"source\",\n timestamp: Date.now(),\n recoverable: true,\n });\n throw error;\n }\n }\n\n protected override emitPlayStarted(isResume: boolean): void {\n this._mediaPreparationGate?.gain.setValueAtTime(1, this.context.currentTime);\n // A stopped voice may have been reaped by Sound.preplay() or natural end.\n // Restore ownership only after its source has successfully started.\n if (this._state === \"stopped\" && !this.origin.playbacks.includes(this)) {\n this.origin._readmitPlayback(this);\n }\n super.emitPlayStarted(isResume);\n }\n\n pause(): void {\n if (!this.source || this._state !== \"playing\") {\n return;\n }\n\n // A play listener can pause synchronously before the settled play promise's\n // finally runs. That bookkeeping must not block preparing this paused voice.\n this._mediaPlayPromise = undefined;\n this._scheduledStop = undefined;\n\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this._mediaPreparationGate?.gain.setValueAtTime(0, this.context.currentTime);\n this.source.mediaElement.pause();\n // The context keeps advancing through network stalls and native loops.\n // Resume from the element's real position, never an elapsed-time guess.\n this._offset = this.source.mediaElement.currentTime;\n } else {\n const elapsed = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);\n this._offset = this.regionRelativeOffset(this._offset + elapsed);\n if (\"stop\" in this.source) {\n // Buffer sources cannot be restarted; play() creates a new one.\n this.source.stop();\n }\n }\n\n this.emitPaused();\n }\n\n stop(): void {\n if (!this.source) {\n throw new Error(\"Cannot stop a sound that has been cleaned up\");\n }\n this._cancelPrepareSeek?.();\n this._scheduledStop = undefined;\n this._mediaPreparationGate?.gain.setValueAtTime(0, this.context.currentTime);\n const pendingMediaPlay = this._mediaPlayPromise !== undefined;\n this._mediaPlayPromise = undefined;\n if (!pendingMediaPlay && (this._state === \"stopped\" || this._state === \"unplayed\")) {\n return;\n }\n\n this.cancelFade();\n\n if (\"stop\" in this.source && this._state === \"playing\") {\n this.source.stop();\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.pause();\n this.source.mediaElement.currentTime = 0;\n }\n\n this._offset = 0;\n this._startTime = 0;\n this.emitStopped();\n }\n\n /**\n * Fades in from silence to the current volume.\n * Optionally stores config to re-trigger the fade on each loop iteration.\n * @param {number} duration - The fade duration in milliseconds.\n * @param {FadeType} type - The fade curve type. Defaults to \"linear\".\n * @param {object} options - Optional. Set perLoop: true to re-trigger fadeIn on each loop.\n * @returns {Promise<void>} Resolves when the fade completes.\n */\n fadeIn(duration: number, type?: FadeType, options?: { perLoop?: boolean; startTime?: number }): Promise<void> {\n if (options?.perLoop && this._clipRegion) throw new Error(\"Per-loop fades are not supported with clip bounds\");\n if (options?.perLoop) {\n this._fadeInConfig = {\n duration,\n type: type ?? \"linear\",\n perLoop: true,\n targetVolume: this.gainNode!.gain.value,\n };\n }\n return super.fadeIn(duration, type, { startTime: options?.startTime });\n }\n\n /**\n * Configures a fade-out to be applied when the playback ends naturally (last loop iteration).\n * @param {number} duration - The fade-out duration in milliseconds.\n * @param {FadeType} type - The fade curve type. Defaults to \"linear\".\n */\n configureFadeOut(duration: number, type?: FadeType): void {\n if (this._scheduledStop) throw new Error(\"An absolute end cannot use an ended-time fade-out\");\n this._fadeOutConfig = { duration, type: type ?? \"linear\" };\n }\n\n /**\n * Fades out then stops the playback.\n * @param {number} duration - The fade-out duration in milliseconds.\n * @param {FadeType} type - The fade curve type. Defaults to \"linear\".\n * @returns {Promise<void>} Resolves when the fade completes and playback is stopped.\n */\n stopWithFade(duration: number, type?: FadeType): Promise<void> {\n // Guard the chained stop: if an external stop()/cleanup() runs between\n // the fade resolving and this .then, the chained stop() would otherwise\n // throw against torn-down state (\"Cannot stop a sound that has been\n // cleaned up\"). cancelFade() inside stop()/cleanup() resolves the fade\n // promise synchronously, so this .then will still fire -- the guard\n // makes it a no-op when the playback is already stopped or cleaned up.\n return this.fadeOut(duration, type).then(() => {\n if (this._state !== \"stopped\" && this.source) {\n this.stop();\n }\n });\n }\n\n /**\n * Silently prepares an idle or paused voice at a finite offset in seconds.\n * Buffers resolve immediately. Media elements wait for metadata, a seekable\n * range containing the target, and completed seeking with current audio data.\n * No play(), context resume, or autoplay primer is performed. State is retained.\n * A later preparation, play, seek, stop, or cleanup rejects pending work with\n * AbortError. Cancellation does not roll back a browser seek already requested.\n * Live/unknown-duration media rejects with NotSupportedError; readiness timeout\n * rejects with TimeoutError. Browser media seeking is not sample accurate.\n */\n async prepareSeek(time: number, options: PrepareSeekOptions = {}): Promise<void> {\n this.assertNotCleanedUp();\n if (!Number.isFinite(time) || time < 0) throw new RangeError(\"Seek time must be finite and non-negative\");\n const timeoutMs = options.timeoutMs ?? 10_000;\n if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2_147_483_647) {\n throw new RangeError(\"Seek timeout must be positive and at most 2147483647 milliseconds\");\n }\n if (options.signal?.aborted) throw new DOMException(\"Seek preparation aborted\", \"AbortError\");\n if (this._state === \"playing\" || this._mediaPlayPromise) {\n throw new Error(\"Pause playback before preparing a seek\");\n }\n this._cancelPrepareSeek?.();\n const source = this.source!;\n if (!(\"mediaElement\" in source)) {\n if (this._clipRegion && time >= this._clipRegion.end) throw new RangeError(\"Seek time must precede the clip end\");\n if (!Number.isFinite(this.duration) || time > this.duration) throw new RangeError(\"Seek time exceeds duration\");\n this._offset = time;\n this.emit(\"seek\", time);\n return;\n }\n const media = source.mediaElement;\n if (!media.paused) throw new Error(\"Pause playback before preparing a seek\");\n if (!this._mediaPreparationGate) {\n const gate = this.context.createGain();\n gate.gain.value = 0;\n this._mediaPreparationGate = gate;\n this.setEffectChainEndpoints(source, this.panner!);\n source.connect(gate);\n }\n this._mediaPreparationGate.gain.setValueAtTime(0, this.context.currentTime);\n await new Promise<void>((resolve, reject) => {\n let finished = false;\n let requested = false;\n let awaitingSeeked = false;\n const events = [\n \"loadedmetadata\",\n \"durationchange\",\n \"progress\",\n \"loadeddata\",\n \"canplay\",\n \"seeked\",\n \"error\",\n \"emptied\",\n \"play\",\n ];\n const settle = (error?: unknown) => {\n if (finished) return;\n finished = true;\n clearTimeout(timer);\n for (const event of events) media.removeEventListener(event, inspect);\n options.signal?.removeEventListener(\"abort\", cancel);\n if (this._cancelPrepareSeek === cancel) this._cancelPrepareSeek = undefined;\n if (error !== undefined) reject(error);\n else {\n this._offset = time;\n resolve();\n }\n };\n const cancel = () => settle(new DOMException(\"Seek preparation aborted\", \"AbortError\"));\n const inspect = (event?: Event) => {\n if (finished) return;\n try {\n if (media.error || event?.type === \"error\") {\n settle(new Error(media.error?.message || \"Media failed during seek preparation\"));\n return;\n }\n // preplay() can queue emptied while assigning the initial resource.\n // Once a seek is requested, a resource reset invalidates that request.\n if ((event?.type === \"emptied\" && requested) || event?.type === \"play\") {\n cancel();\n return;\n }\n if (media.readyState < 1) return;\n if (!Number.isFinite(media.duration)) {\n settle(new DOMException(\"Seek preparation requires finite-duration media\", \"NotSupportedError\"));\n return;\n }\n if (time > media.duration) {\n settle(new RangeError(\"Seek time exceeds duration\"));\n return;\n }\n if (!requested) {\n let inRange = false;\n for (let i = 0; i < media.seekable.length; i++) {\n if (time >= media.seekable.start(i) && time <= media.seekable.end(i)) inRange = true;\n }\n if (!inRange) return;\n requested = true;\n awaitingSeeked = media.seeking || media.currentTime !== time;\n if (media.currentTime !== time) media.currentTime = time;\n } else if (event?.type === \"seeked\") awaitingSeeked = false;\n if (\n !awaitingSeeked &&\n !media.seeking &&\n media.readyState >= 2 &&\n Math.abs(media.currentTime - time) < 0.001\n ) {\n settle();\n }\n } catch (error) {\n settle(error);\n }\n };\n const timer = setTimeout(() => settle(new DOMException(\"Seek preparation timed out\", \"TimeoutError\")), timeoutMs);\n this._cancelPrepareSeek = cancel;\n for (const event of events) media.addEventListener(event, inspect);\n options.signal?.addEventListener(\"abort\", cancel, { once: true });\n inspect();\n });\n // Notify outside the readiness listener's error boundary: observer errors\n // reject this async call instead of being swallowed after it has settled.\n this.emit(\"seek\", time);\n }\n\n seek(time: number): void {\n if (!this.source || !this.gainNode || !this.panner) {\n throw new Error(\"Cannot seek a sound that has been cleaned up\");\n }\n if (!Number.isFinite(time) || time < 0 || (this.origin.region !== undefined && time > this.duration)) {\n throw new Error(\"Invalid time value for seek\");\n }\n if (this._clipRegion && time >= this._clipRegion.end) throw new RangeError(\"Seek time must precede the clip end\");\n\n this._cancelPrepareSeek?.();\n\n if (\n this.origin.soundType === \"streaming\" &&\n \"mediaElement\" in this.source &&\n (this.source.mediaElement.seekable?.length ?? 0) === 0\n ) {\n if (this.source.mediaElement.duration === Number.POSITIVE_INFINITY) {\n throw new Error(\"Live media-element streams do not expose a seekable range\");\n }\n throw new Error(\"Media-element stream does not currently expose a seekable range\");\n }\n\n const wasPlaying = this._state === \"playing\";\n if (wasPlaying) {\n this.pause();\n }\n\n this._offset = time;\n\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.currentTime = time;\n }\n // For non-media elements, play() will handle recreating the source if needed.\n\n if (wasPlaying) {\n this.play();\n }\n this.emit(\"seek\", time);\n }\n\n /**\n * Current source position in seconds. Playing HTML media reports its observed\n * cursor, including buffering stalls and native loop wraps. Buffer playback\n * follows the render clock; idle and paused voices retain their committed offset.\n */\n get currentTime(): number {\n if (this._state === \"playing\") {\n if (this.source && \"mediaElement\" in this.source) return this.source.mediaElement.currentTime;\n const elapsed = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);\n const time = this._offset + elapsed;\n return this.regionRelativeOffset(time);\n } else {\n return this._offset;\n }\n }\n\n private removeFromOrigin(): void {\n const idx = this.origin.playbacks.indexOf(this);\n if (idx !== -1) {\n this.origin.playbacks.splice(idx, 1);\n }\n }\n\n private recreateSource() {\n if (!this.buffer || !this.panner || !this.context || !this.gainNode) {\n throw new Error(\"Cannot recreate source of a sound that has been cleaned up\");\n }\n try {\n if (this.source) {\n // It's crucial to nullify onended of the old source if it's an AudioBufferSourceNode (or similar non-restartable source),\n // as its onended event could otherwise interfere with the new source created for seek/resume.\n // MediaElementAudioSourceNode is handled differently as its underlying element can be paused/played.\n if (!(\"mediaElement\" in this.source) && \"onended\" in this.source) {\n this.source.onended = null;\n }\n this.source.disconnect();\n }\n this.source = this.context.createBufferSource();\n this._scheduledStop = undefined;\n this.source.buffer = this.buffer;\n this.setEffectChainEndpoints(this.source, this.panner);\n this.setupSourceNode(this.source);\n this.playbackRate = this._playbackRate;\n this.configureBufferLoopSource();\n } catch (error) {\n this.emitAsync(\"error\", {\n error: error as Error,\n errorType: \"source\",\n timestamp: Date.now(),\n recoverable: false,\n });\n throw error;\n }\n }\n\n /**\n * Sets whether the audio source should loop.\n * @throws {Error} Throws an error if the sound has been cleaned up.\n */\n set sourceLoop(loop: boolean) {\n if (!this.source) {\n throw new Error(\"Cannot set loop on a sound that has been cleaned up\");\n }\n if (\"loop\" in this.source) {\n this.source.loop = loop;\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n this.source.mediaElement.loop = loop;\n }\n }\n\n /**\n * Cleans up resources used by the Playback instance.\n * This method should be called when the audio is no longer needed to free up resources.\n */\n\n cleanup(): void {\n this._scheduledStop = undefined;\n this._cancelPrepareSeek?.();\n this._mediaPreparationGate?.gain.setValueAtTime(0, this.context.currentTime);\n this._mediaPlayPromise = undefined;\n if (!this.source) {\n return; // Already cleaned up\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n const mediaElement = this.source.mediaElement;\n mediaElement.pause();\n try {\n mediaElement.currentTime = 0;\n } catch {\n // Live media without a seekable range can reject currentTime writes.\n }\n mediaElement.loop = false;\n mediaElement.onended = null;\n mediaElement.src = \"\";\n mediaElement.load();\n } else if (\"onended\" in this.source) {\n this.source.onended = null;\n // Disconnecting alone leaves a started source running, including native loops.\n // Scheduled starts also have playing state; unplayed/paused/stopped sources do not need stop().\n if (\"stop\" in this.source && this._state === \"playing\") {\n this.source.stop();\n }\n }\n this._offset = 0;\n this._startTime = 0;\n this.markStopped();\n this.currentLoop = 0;\n this.source.disconnect();\n this.source = undefined;\n super.cleanup();\n this._mediaPreparationGate?.disconnect();\n this._mediaPreparationGate = undefined;\n this._occlusion?.destroy();\n this._occlusion = undefined;\n }\n\n private assertNotCleanedUp(): void {\n if (!this.source || !this.gainNode || !this.panner) {\n throw new Error(\"Cannot perform operation on a sound that has been cleaned up\");\n }\n }\n\n addFilter(filter: BiquadFilterNode): void {\n this.assertNotCleanedUp();\n super.addFilter(filter);\n }\n\n removeFilter(filter: BiquadFilterNode): void {\n this.assertNotCleanedUp();\n super.removeFilter(filter);\n }\n\n /**\n * Sets or gets the loop count for the audio.\n * @param {LoopCount} loopCount - The number of times the audio should loop. 'infinite' for endless looping.\n * @returns {LoopCount} The loop count if no parameter is provided.\n * @throws {Error} Throws an error if the sound has been cleaned up or if the source type is unsupported.\n */\n\n loop(loopCount?: LoopCount): LoopCount {\n if (!this.source) {\n throw new Error(\"Cannot loop a sound that has been cleaned up\");\n }\n if (loopCount !== undefined) {\n this._validateLoopCount(loopCount);\n const previousLoopCount = this.loopCount;\n const nextLoopCount = loopCount === \"infinite\" ? \"infinite\" : Math.max(0, loopCount);\n const changesInfiniteMode = (previousLoopCount === \"infinite\") !== (nextLoopCount === \"infinite\");\n if (\n changesInfiniteMode &&\n (this.origin.region || this._clipRegion) &&\n this._state === \"playing\" &&\n \"loop\" in this.source &&\n \"stop\" in this.source\n ) {\n const restartAt = Math.max(this._startTime, this.context.currentTime);\n const elapsed = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);\n this._offset = this.regionRelativeOffset(\n this.regionRelativeOffset(this._offset + elapsed, previousLoopCount),\n nextLoopCount,\n );\n if (\"onended\" in this.source) {\n this.source.onended = null;\n }\n this.source.stop();\n this._scheduledStop = undefined;\n this.loopCount = nextLoopCount;\n this.currentLoop = 0;\n this._startTime = restartAt;\n this.recreateSource();\n this.startBufferSource(restartAt);\n return this.loopCount;\n }\n if (changesInfiniteMode && this._state === \"playing\" && \"buffer\" in this.source) {\n // Whole-buffer mode changes keep their native source and its deadline.\n // Commit the cursor under the old mode before changing how it folds.\n const elapsed = Math.max(0, (this.context.currentTime - this._startTime) * this._playbackRate);\n this._offset = this.regionRelativeOffset(this._offset + elapsed, previousLoopCount);\n this._startTime = Math.max(this._startTime, this.context.currentTime);\n }\n this.loopCount = nextLoopCount;\n this.currentLoop = 0;\n }\n if (\"mediaElement\" in this.source && this.source.mediaElement) {\n const mediaElement = this.source.mediaElement;\n mediaElement.loop = this.loopCount === \"infinite\";\n } else if (\"loop\" in this.source) {\n this.configureBufferLoopSource();\n } else {\n throw new Error(\"Unsupported source type\");\n }\n return this.loopCount;\n }\n\n /** Copy loop flags and atlas bounds to the current buffer source without changing loop progress. */\n private configureBufferLoopSource(): void {\n if (!this.source || !(\"loop\" in this.source)) return;\n this.source.loop = this.loopCount === \"infinite\";\n if (this.source.buffer) {\n this.source.loopEnd = this._clipRegion\n ? (this.origin.region?.start ?? 0) + this._clipRegion.end\n : this.origin.region\n ? this.origin.region.start + this.origin.region.duration\n : this.source.buffer.duration;\n this.source.loopStart = (this.origin.region?.start ?? 0) + (this._clipRegion?.start ?? 0);\n }\n }\n\n /** Clamp finite regions and wrap infinite regions while keeping offsets region-relative. */\n private regionRelativeOffset(offset: number, loopCount: LoopCount = this.loopCount): number {\n if (this._clipRegion) {\n const { start, end } = this._clipRegion;\n return loopCount === \"infinite\" && offset >= end\n ? start + ((offset - start) % (end - start))\n : Math.min(end, offset);\n }\n const region = this.origin.region;\n if (!region) {\n if (loopCount === \"infinite\" && this.buffer && this.buffer.duration > 0) return offset % this.buffer.duration;\n return offset;\n }\n return loopCount === \"infinite\" ? offset % region.duration : Math.min(region.duration, offset);\n }\n\n /** Start a buffer source, translating the region-relative offset once. */\n private startBufferSource(when: number): boolean {\n if (!this.source || !(\"start\" in this.source)) {\n return false;\n }\n const region = this.origin.region;\n if (this._clipRegion) {\n if (this._offset >= this._clipRegion.end) return false;\n const absoluteOffset = (region?.start ?? 0) + this._offset;\n if (this.loopCount === \"infinite\") this.source.start(when, absoluteOffset);\n else this.source.start(when, absoluteOffset, this._clipRegion.end - this._offset);\n return true;\n }\n if (!region) {\n this.source.start(when, this._offset);\n return true;\n }\n if (this._offset === region.duration) {\n return false;\n }\n const absoluteOffset = region.start + this._offset;\n if (this.loopCount === \"infinite\") {\n this.source.start(when, absoluteOffset);\n } else {\n this.source.start(when, absoluteOffset, region.duration - this._offset);\n }\n return true;\n }\n\n /**\n * Sets the pitch-shift factor for this playback, resurrecting the dormant\n * phase-vocoder worklet (Jean Laroche & Mark Dolson, \"New Phase-Vocoder\n * Techniques for Pitch-Shifting, Harmonizing and Other Exotic Effects\",\n * 1999 IEEE WASPAA — peak-based pitch shift with Identity Phase-Locking).\n *\n * On first use (factor !== 1) the phase-vocoder AudioWorkletNode is built and\n * added as an ordinary entry at the tail of the pre-panner effect chain:\n * source → [filters] → phase vocoder → panner → gainNode. The factor is\n * forwarded to the node's `pitchFactor` AudioParam (1 = no shift, 2 = +1\n * octave, 0.5 = -1 octave).\n *\n * @param factor Pitch multiplier (> 0).\n * @throws {Error} if the playback has been cleaned up or factor <= 0.\n */\n async setPitchShift(factor: number): Promise<void> {\n this.assertNotCleanedUp();\n if (!Number.isFinite(factor) || factor <= 0) {\n throw new Error(\"Pitch-shift factor must be greater than 0\");\n }\n this._pitchFactor = factor;\n const effectChain = this._effectChain;\n if (!effectChain) {\n throw new Error(\"Cannot pitch-shift a playback before its effect chain is initialized\");\n }\n let pitchShiftNode = effectChain.nodes.find((node) => {\n const parameters = (node as AudioWorkletNode).parameters;\n return parameters && typeof parameters.get === \"function\" && parameters.get(\"pitchFactor\") !== undefined;\n }) as AudioWorkletNode | undefined;\n\n // factor === 1 is the documented \"no shift\" contract and MUST be a genuine\n // passthrough. The peak/region phase-vocoder pipeline does NOT guarantee\n // identity for peakless / edge-bin / broadband content (it zero-fills and\n // only repopulates detected peak regions), so we cannot leave the node in\n // the chain at factor 1. Remove its ordinary chain entry so the signal\n // bypasses the worklet entirely (source → [filters] → panner). A later\n // non-unity factor rebuilds a fresh node.\n if (factor === 1) {\n if (pitchShiftNode) {\n effectChain.remove(pitchShiftNode);\n }\n return;\n }\n\n if (!pitchShiftNode) {\n const cacophony = this.origin.cacophony;\n if (!cacophony) {\n throw new Error(\"Cannot pitch-shift a playback whose Sound has no Cacophony instance\");\n }\n pitchShiftNode = await cacophony.buildWorkletEffect(WORKLETS.phaseVocoder, {}, this.context);\n effectChain.add(pitchShiftNode);\n }\n\n const pitchParam = pitchShiftNode.parameters?.get(\"pitchFactor\");\n if (pitchParam) {\n pitchParam.value = factor;\n }\n }\n\n /**\n * The current pitch-shift factor (1 = no shift). See {@link setPitchShift}.\n */\n get pitchShift(): number {\n return this._pitchFactor;\n }\n\n /**\n * Gets the output node of this playback's audio graph.\n * This is the final node in the internal chain before connection to destination.\n * Use this to manually wire the playback into custom audio graphs.\n *\n * @returns {GainNode} The gain node that serves as the output of this playback.\n * @throws {Error} Throws an error if the playback has been cleaned up.\n *\n * @example\n * // Manual routing through custom effects\n * const playback = sound.play()[0];\n * playback.disconnect(); // Disconnect from default destination\n * playback.connect(reverbNode).connect(context.destination);\n */\n get outputNode(): GainNode {\n if (!this.gainNode) {\n throw new Error(\"Cannot access output node of a playback that has been cleaned up\");\n }\n return this.gainNode;\n }\n\n /**\n * Connects this playback's output to an AudioNode or AudioParam.\n * Follows the Web Audio API connection pattern.\n *\n * @returns {AudioNode} The destination node (for chaining).\n * @throws {Error} Throws an error if the playback has been cleaned up.\n *\n * @example\n * // Chain multiple effects\n * playback.connect(delay).connect(reverb).connect(context.destination);\n */\n connect(destination: AudioNode | AudioParam): AudioNode {\n return this.outputNode.connect(destination as any);\n }\n\n /**\n * Disconnects this playback's output from a specific destination or from all destinations.\n *\n * @param {AudioNode | AudioParam} [destination] - Optional specific destination to disconnect from.\n * If omitted, disconnects from all destinations.\n * @throws {Error} Throws an error if the playback has been cleaned up.\n *\n * @example\n * // Disconnect from all\n * playback.disconnect();\n *\n * @example\n * // Disconnect from specific node\n * playback.disconnect(reverbNode);\n */\n disconnect(destination?: AudioNode | AudioParam): void {\n if (destination) {\n this.outputNode.disconnect(destination as any);\n } else {\n this.outputNode.disconnect();\n }\n }\n\n /**\n * Creates a clone of the current Playback instance with optional overrides for certain properties.\n * This method allows for the creation of a new Playback instance that shares the same audio context\n * and source node but can have different settings such as loop count or pan type.\n * The clone is connected to the origin Sound's primary route and registered with the Sound.\n * @throws {Error} Throws an error if the sound has been cleaned up.\n */\n\n clone(overrides: Partial<PlaybackCloneOverrides> = {}): Playback {\n if (!this.source || !this.gainNode || !this.context) {\n throw new Error(\"Cannot clone a sound that has been cleaned up\");\n }\n if (this._scheduledStop) throw new Error(\"Cannot clone a playback with an armed absolute end\");\n this._validateLoopCount(overrides.loopCount ?? this.loopCount);\n validateSpatialSmoothingTau(overrides.spatialSmoothingTau ?? this.spatialSmoothingTau);\n const panType = overrides.panType ?? this.panType;\n validateSpatialOptions(overrides, panType);\n // we'll need to create a new gain node\n const gainNode = this.context.createGain();\n gainNode.connect(this.origin._resolveRouteTargetNode());\n // clone the source node\n let source: SourceNode;\n if (\"buffer\" in this.source && this.source.buffer) {\n source = this.context.createBufferSource();\n source.buffer = this.source.buffer;\n } else if (\"mediaElement\" in this.source && this.source.mediaElement) {\n if (!this.context.createMediaElementSource) {\n throw new Error(\"Media element sources are not supported on this audio context.\");\n }\n // The Web Audio spec forbids creating a second MediaElementAudioSourceNode\n // from the same HTMLMediaElement -- the second call throws\n // InvalidStateError. Clone the underlying element first so the new\n // playback owns an independent element that hasn't been bound yet.\n const originalElement = this.source.mediaElement;\n const clonedElement = originalElement.cloneNode(true) as HTMLMediaElement;\n source = this.context.createMediaElementSource(clonedElement);\n } else {\n throw new Error(\"Unsupported source type\");\n }\n const loopCount = overrides.loopCount !== undefined ? overrides.loopCount : this.loopCount;\n const clone = new Playback(this.origin, source, gainNode);\n\n // Copy all relevant properties\n clone.loopCount = loopCount;\n clone._clipRegion = this._clipRegion && { ...this._clipRegion };\n clone.currentLoop = this.currentLoop;\n clone.setPanType(panType, this.context);\n clone.spatialSmoothingTau = overrides.spatialSmoothingTau ?? this.spatialSmoothingTau;\n if (panType === \"HRTF\") {\n if (this.panType === \"HRTF\") clone.threeDOptions = this.threeDOptions;\n if (overrides.threeDOptions !== undefined) clone.threeDOptions = overrides.threeDOptions;\n if (overrides.position !== undefined) clone.position = overrides.position;\n } else {\n clone.stereoPan = overrides.stereoPan ?? this.stereoPan ?? 0;\n }\n clone.volume = this.volume;\n clone.playbackRate = this._playbackRate;\n clone._offset = this._clipRegion && this._state === \"playing\" ? this.currentTime : this._offset;\n clone.setOcclusion(this.occlusion, 0);\n if (this._fadeInConfig) {\n clone._fadeInConfig = { ...this._fadeInConfig };\n }\n if (this._fadeOutConfig) {\n clone._fadeOutConfig = { ...this._fadeOutConfig };\n }\n if (this._state === \"paused\") {\n clone.markPaused();\n } else if (this._state === \"stopped\") {\n clone.markStopped();\n }\n\n // Deep clone filters\n this._filters.forEach((filter) => {\n const clonedFilter = this.context.createBiquadFilter();\n clonedFilter.type = filter.type;\n clonedFilter.frequency.value = filter.frequency.value;\n clonedFilter.Q.value = filter.Q.value;\n clonedFilter.gain.value = filter.gain.value;\n clone.addFilter(clonedFilter as unknown as BiquadFilterNode);\n });\n\n if (this._pitchFactor !== 1) {\n void clone.setPitchShift(this._pitchFactor).catch(() => {\n /* worklet unavailable on this context — clone proceeds unshifted */\n });\n }\n\n // If the original is playing, start the clone\n if (this._state === \"playing\") {\n clone.play();\n }\n\n this.origin.playbacks.push(clone);\n return clone;\n }\n}\n"],"mappings":"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"}
|
package/dist/sound.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./eventEmitter.cjs"),t=require("./playOptions.cjs"),n=require("./spatialAutomation.cjs"),r=require("./routableSource.cjs"),i=require("./playback.cjs");var a=class a extends r.RoutableSource{url;globalGainNode;soundType;panType;_cacophony;_preparedMediaElementCleanup;region;spriteName;buffer;streamCapabilities;context;loopCount=0;_playbackRate=1;_pitchFactor=1;_playbackGeneration=0;eventEmitter=new e.TypedEventEmitter;_holdings={sources:[],gainNodes:[],mediaElements:[]};_unregisterToken={};_preparedMediaElement;_playbackUnsubscribes=new WeakMap;constructor(e,t,n,r,i=`buffer`,a=`HRTF`,o,s,c,l,u){super(),this.url=e,this.globalGainNode=r,this.soundType=i,this.panType=a,this._cacophony=o,this._preparedMediaElementCleanup=c,this.region=l,this.spriteName=u,this.buffer=t,this.context=n,this._preparedMediaElement=s,this._cacophony?.registerSoundForCleanup(this,this._holdings,this._unregisterToken)}get cacophony(){return this._cacophony}get volume(){return super.volume}on(e,t){return this.eventEmitter.on(e,t)}off(e,t){this.eventEmitter.off(e,t)}emit(e,t){this.eventEmitter.emit(e,t)}async emitAsync(e,t){return this.eventEmitter.emitAsync(e,t)}reportLoadError(e,t){this.emitAsync(`soundError`,{url:this.url,error:e,errorType:`load`,timestamp:Date.now(),recoverable:t})}clone(e={}){n.validateSpatialSmoothingTau(e.spatialSmoothingTau??this.spatialSmoothingTau);let r=e.panType??this.panType;t.validateSpatialOptions(e,r);let i=e.stereoPan===void 0?this.stereoPan:e.stereoPan,o=e.loopCount===void 0?this.loopCount:e.loopCount,s=e.playbackRate??this.playbackRate,c=e.volume===void 0?this.volume:e.volume,l=e.position===void 0?this.position:e.position,u=e.filters??this._filters,d=new a(this.url,this.buffer,this.context,this.globalGainNode,this.soundType,r,this.cacophony,void 0,void 0,this.region,this.spriteName);return d.loop(o),d.playbackRate=s,d.volume=c,d.spatialSmoothingTau=e.spatialSmoothingTau??this.spatialSmoothingTau,r===`HRTF`?(e.threeDOptions===void 0?this.panType===`HRTF`&&(d.threeDOptions=this.threeDOptions):d.threeDOptions=e.threeDOptions,d.position=l):d.stereoPan=i,d.addFilters(u),d}preplay(){this.playbacks.some(e=>e.state===`stopped`)&&(this.playbacks=this.playbacks.filter(e=>e.state===`stopped`?(this._unsubscribeFromPlayback(e),this._detachPlaybackOutput(e),!1):!0));let e=this._holdings.sources.length,t=this._holdings.gainNodes.length,n=this._holdings.mediaElements.length,r=this.playbacks.length,a,o,s;try{let e;if(this.buffer)e=this.context.createBufferSource(),e.buffer=this.buffer;else{if(!this.context.createMediaElementSource)throw Error(`Media element sources are not supported on this audio context (e.g. OfflineAudioContext). Use buffer-based sounds instead.`);let t=this._preparedMediaElement??new Audio;t.crossOrigin=`anonymous`,t.src=this.url,t.preload=`auto`,e=this.context.createMediaElementSource(t),t===this._preparedMediaElement&&(this._preparedMediaElement=void 0)}a=e;let t=this.context.createGain();o=t;let n=this._resolveRouteTargetNode();t.connect(n);let r=new i.Playback(this,e,t);return s=r,this._preparePlayback(r),this._holdings.sources.push(e),this._holdings.gainNodes.push(t),`mediaElement`in e&&e.mediaElement&&this._holdings.mediaElements.push(e.mediaElement),r.setGainNode(t),r.volume=this.volume,r.playbackRate=this.playbackRate,r.loop(this.loopCount),this._pitchFactor!==1&&r.setPitchShift(this._pitchFactor).catch(()=>{}),this.panType===`HRTF`?(r.threeDOptions=this.threeDOptions,r.position=this.position):this.panType===`stereo`&&(r.stereoPan=this.stereoPan),this._subscribeToPlayback(r),this.playbacks.push(r),[r]}catch(i){if(this._holdings.sources.length=e,this._holdings.gainNodes.length=t,this._holdings.mediaElements.length=n,this.playbacks.length=r,s&&(this._unsubscribeFromPlayback(s),s.cleanup()),o)try{o.disconnect()}catch{}if(a)try{a.disconnect()}catch{}let c={url:this.url,error:i,errorType:`playback`,timestamp:Date.now(),recoverable:!0};throw this.emitAsync(`soundError`,c),i}}_detachPlaybackOutput(e){if(e.source){e.outputNode.disconnect();for(let t of e._sendGains.values())t.disconnect();e._sendGains.clear()}}_readmitPlayback(e){this.playbacks.includes(e)||(this._detachPlaybackOutput(e),e.outputNode.connect(this._resolveRouteTargetNode()),this._wireRouteSends(e),this._subscribeToPlayback(e),this.playbacks.push(e))}_subscribeToPlayback(e){if(this._playbackUnsubscribes.has(e))return;let t=e.on(`ended`,()=>{this.emit(`ended`,void 0),this._unsubscribeFromPlayback(e)});e._loopEndCallback=()=>{this.emit(`loopEnd`,void 0)};let n=e.on(`error`,e=>{this.emitAsync(`soundError`,{url:this.url,error:e.error,errorType:`playback`,timestamp:e.timestamp,recoverable:e.recoverable})});this._playbackUnsubscribes.set(e,[t,n,()=>{e._loopEndCallback&&=void 0}])}_unsubscribeFromPlayback(e){let t=this._playbackUnsubscribes.get(e);if(t){this._playbackUnsubscribes.delete(e);for(let e of t)e()}}validatePlayOptions(e){t.validatePlayOptions(e,this.panType,this.buffer?`buffer`:`media`)}play(e){this.validatePlayOptions(e);let t=this._holdings.sources.length,n=this._holdings.gainNodes.length,r=this._holdings.mediaElements.length,i=this.preplay();for(let a of i){let i=e=>{o(),this.emit(`play`,e)},o=()=>{a.off(`play`,i),a.off(`error`,o)};a.on(`play`,i),a.on(`error`,o);try{a.play(e)}catch(e){o();let i=this.playbacks.indexOf(a);throw i!==-1&&this.playbacks.splice(i,1),this._unsubscribeFromPlayback(a),a.cleanup(),this._holdings.sources.length=t,this._holdings.gainNodes.length=n,this._holdings.mediaElements.length=r,e}}return i}stop(){this._playbackGeneration++,this._holdings.sources.length=0,this._holdings.gainNodes.length=0,this._holdings.mediaElements.length=0,super.stop(),this.emit(`stop`,void 0)}pause(){super.pause(),this.emit(`pause`,void 0)}resume(){this.playbacks.filter(e=>e.isPaused).forEach(e=>e.play()),this.emit(`resume`,void 0)}seek(e){this.playbacks.forEach(t=>t.seek(e))}get duration(){return this.region?this.region.duration:this.playbacks.length>0?this.playbacks[0].duration:this.buffer?.duration||NaN}timeStretch(e,t){if(!this.buffer)throw Error(`Sound.timeStretch requires a buffer-based sound (a loaded AudioBuffer).`);if(!this._cacophony)throw Error(`Sound.timeStretch requires the owning Cacophony instance.`);let n=this.buffer;if(this.region){if(typeof this.context.createBuffer!=`function`)throw Error(`Sound.timeStretch requires an audio context that supports createBuffer().`);let e=Math.round(this.region.start*this.buffer.sampleRate),t=Math.round((this.region.start+this.region.duration)*this.buffer.sampleRate),r=Math.max(1,t-e);n=this.context.createBuffer(this.buffer.numberOfChannels,r,this.buffer.sampleRate);for(let r=0;r<this.buffer.numberOfChannels;r++)n.getChannelData(r).set(this.buffer.getChannelData(r).subarray(e,t))}let r=this._cacophony.timeStretchBuffer(n,e,t);return new a(this.url,r,this.context,this.globalGainNode,`buffer`,this.panType,this._cacophony)}loop(e){
|
|
1
|
+
const e=require("./eventEmitter.cjs"),t=require("./playOptions.cjs"),n=require("./spatialAutomation.cjs"),r=require("./routableSource.cjs"),i=require("./playback.cjs");var a=class a extends r.RoutableSource{url;globalGainNode;soundType;panType;_cacophony;_preparedMediaElementCleanup;region;spriteName;buffer;streamCapabilities;context;loopCount=0;_playbackRate=1;_pitchFactor=1;_playbackGeneration=0;eventEmitter=new e.TypedEventEmitter;_holdings={sources:[],gainNodes:[],mediaElements:[]};_unregisterToken={};_preparedMediaElement;_playbackUnsubscribes=new WeakMap;constructor(e,t,n,r,i=`buffer`,a=`HRTF`,o,s,c,l,u){super(),this.url=e,this.globalGainNode=r,this.soundType=i,this.panType=a,this._cacophony=o,this._preparedMediaElementCleanup=c,this.region=l,this.spriteName=u,this.buffer=t,this.context=n,this._preparedMediaElement=s,this._cacophony?.registerSoundForCleanup(this,this._holdings,this._unregisterToken)}get cacophony(){return this._cacophony}get supportsClipLoop(){return this.buffer!==void 0}get volume(){return super.volume}on(e,t){return this.eventEmitter.on(e,t)}off(e,t){this.eventEmitter.off(e,t)}emit(e,t){this.eventEmitter.emit(e,t)}async emitAsync(e,t){return this.eventEmitter.emitAsync(e,t)}reportLoadError(e,t){this.emitAsync(`soundError`,{url:this.url,error:e,errorType:`load`,timestamp:Date.now(),recoverable:t})}clone(e={}){n.validateSpatialSmoothingTau(e.spatialSmoothingTau??this.spatialSmoothingTau);let r=e.panType??this.panType;t.validateSpatialOptions(e,r);let i=e.stereoPan===void 0?this.stereoPan:e.stereoPan,o=e.loopCount===void 0?this.loopCount:e.loopCount,s=e.playbackRate??this.playbackRate,c=e.volume===void 0?this.volume:e.volume,l=e.position===void 0?this.position:e.position,u=e.filters??this._filters,d=new a(this.url,this.buffer,this.context,this.globalGainNode,this.soundType,r,this.cacophony,void 0,void 0,this.region,this.spriteName);return d.loop(o),d.playbackRate=s,d.volume=c,d.spatialSmoothingTau=e.spatialSmoothingTau??this.spatialSmoothingTau,r===`HRTF`?(e.threeDOptions===void 0?this.panType===`HRTF`&&(d.threeDOptions=this.threeDOptions):d.threeDOptions=e.threeDOptions,d.position=l):d.stereoPan=i,d.addFilters(u),d}preplay(){this.playbacks.some(e=>e.state===`stopped`)&&(this.playbacks=this.playbacks.filter(e=>e.state===`stopped`?(this._unsubscribeFromPlayback(e),this._detachPlaybackOutput(e),!1):!0));let e=this._holdings.sources.length,t=this._holdings.gainNodes.length,n=this._holdings.mediaElements.length,r=this.playbacks.length,a,o,s;try{let e;if(this.buffer)e=this.context.createBufferSource(),e.buffer=this.buffer;else{if(!this.context.createMediaElementSource)throw Error(`Media element sources are not supported on this audio context (e.g. OfflineAudioContext). Use buffer-based sounds instead.`);let t=this._preparedMediaElement??new Audio;t.crossOrigin=`anonymous`,t.src=this.url,t.preload=`auto`,e=this.context.createMediaElementSource(t),t===this._preparedMediaElement&&(this._preparedMediaElement=void 0)}a=e;let t=this.context.createGain();o=t;let n=this._resolveRouteTargetNode();t.connect(n);let r=new i.Playback(this,e,t);return s=r,this._preparePlayback(r),this._holdings.sources.push(e),this._holdings.gainNodes.push(t),`mediaElement`in e&&e.mediaElement&&this._holdings.mediaElements.push(e.mediaElement),r.setGainNode(t),r.volume=this.volume,r.playbackRate=this.playbackRate,r.loop(this.loopCount),this._pitchFactor!==1&&r.setPitchShift(this._pitchFactor).catch(()=>{}),this.panType===`HRTF`?(r.threeDOptions=this.threeDOptions,r.position=this.position):this.panType===`stereo`&&(r.stereoPan=this.stereoPan),this._subscribeToPlayback(r),this.playbacks.push(r),[r]}catch(i){if(this._holdings.sources.length=e,this._holdings.gainNodes.length=t,this._holdings.mediaElements.length=n,this.playbacks.length=r,s&&(this._unsubscribeFromPlayback(s),s.cleanup()),o)try{o.disconnect()}catch{}if(a)try{a.disconnect()}catch{}let c={url:this.url,error:i,errorType:`playback`,timestamp:Date.now(),recoverable:!0};throw this.emitAsync(`soundError`,c),i}}_detachPlaybackOutput(e){if(e.source){e.outputNode.disconnect();for(let t of e._sendGains.values())t.disconnect();e._sendGains.clear()}}_readmitPlayback(e){this.playbacks.includes(e)||(this._detachPlaybackOutput(e),e.outputNode.connect(this._resolveRouteTargetNode()),this._wireRouteSends(e),this._subscribeToPlayback(e),this.playbacks.push(e))}_subscribeToPlayback(e){if(this._playbackUnsubscribes.has(e))return;let t=e.on(`ended`,()=>{this.emit(`ended`,void 0),this._unsubscribeFromPlayback(e)});e._loopEndCallback=()=>{this.emit(`loopEnd`,void 0)};let n=e.on(`error`,e=>{this.emitAsync(`soundError`,{url:this.url,error:e.error,errorType:`playback`,timestamp:e.timestamp,recoverable:e.recoverable})});this._playbackUnsubscribes.set(e,[t,n,()=>{e._loopEndCallback&&=void 0}])}_unsubscribeFromPlayback(e){let t=this._playbackUnsubscribes.get(e);if(t){this._playbackUnsubscribes.delete(e);for(let e of t)e()}}validatePlayOptions(e){t.validatePlayOptions(e,this.panType,this.buffer?`buffer`:`media`)}play(e){this.validatePlayOptions(e);let t=this._holdings.sources.length,n=this._holdings.gainNodes.length,r=this._holdings.mediaElements.length,i=this.preplay();for(let a of i){let i=e=>{o(),this.emit(`play`,e)},o=()=>{a.off(`play`,i),a.off(`error`,o)};a.on(`play`,i),a.on(`error`,o);try{a.play(e)}catch(e){o();let i=this.playbacks.indexOf(a);throw i!==-1&&this.playbacks.splice(i,1),this._unsubscribeFromPlayback(a),a.cleanup(),this._holdings.sources.length=t,this._holdings.gainNodes.length=n,this._holdings.mediaElements.length=r,e}}return i}stop(){this._playbackGeneration++,this._holdings.sources.length=0,this._holdings.gainNodes.length=0,this._holdings.mediaElements.length=0,super.stop(),this.emit(`stop`,void 0)}pause(){super.pause(),this.emit(`pause`,void 0)}resume(){this.playbacks.filter(e=>e.isPaused).forEach(e=>e.play()),this.emit(`resume`,void 0)}seek(e){this.playbacks.forEach(t=>t.seek(e))}get duration(){return this.region?this.region.duration:this.playbacks.length>0?this.playbacks[0].duration:this.buffer?.duration||NaN}timeStretch(e,t){if(!this.buffer)throw Error(`Sound.timeStretch requires a buffer-based sound (a loaded AudioBuffer).`);if(!this._cacophony)throw Error(`Sound.timeStretch requires the owning Cacophony instance.`);let n=this.buffer;if(this.region){if(typeof this.context.createBuffer!=`function`)throw Error(`Sound.timeStretch requires an audio context that supports createBuffer().`);let e=Math.round(this.region.start*this.buffer.sampleRate),t=Math.round((this.region.start+this.region.duration)*this.buffer.sampleRate),r=Math.max(1,t-e);n=this.context.createBuffer(this.buffer.numberOfChannels,r,this.buffer.sampleRate);for(let r=0;r<this.buffer.numberOfChannels;r++)n.getChannelData(r).set(this.buffer.getChannelData(r).subarray(e,t))}let r=this._cacophony.timeStretchBuffer(n,e,t);return new a(this.url,r,this.context,this.globalGainNode,`buffer`,this.panType,this._cacophony)}loop(e){if(e===void 0)return this.loopCount;for(let t of this.playbacks)t._validateLoopCount(e);return this.loopCount=e,this.playbacks.forEach(t=>t.loop(e)),this.loopCount}get playbackRate(){return this._playbackRate}set playbackRate(e){this._playbackRate=e,this.playbacks.forEach(t=>t.playbackRate=e),this.emit(`rateChange`,e)}set volume(e){super.volume=e,this.emit(`volumeChange`,e)}get pitchShift(){return this._pitchFactor}async setPitchShift(e){if(!Number.isFinite(e)||e<=0)throw Error(`Pitch-shift factor must be greater than 0`);this._pitchFactor=e,await Promise.all(this.playbacks.map(t=>t.setPitchShift(e)))}cleanup(){this._playbackGeneration++,this._cacophony?.unregisterSoundCleanup(this._unregisterToken),this._preparedMediaElementCleanup?.(),this._preparedMediaElementCleanup=void 0,this._preparedMediaElement&&=(this._preparedMediaElement.pause(),this._preparedMediaElement.src=``,this._preparedMediaElement.load(),void 0),this._holdings.sources.length=0,this._holdings.gainNodes.length=0,this._holdings.mediaElements.length=0,this.playbacks.forEach(e=>{this._unsubscribeFromPlayback(e),e.cleanup()}),this.playbacks=[],this.eventEmitter.removeAllListeners(),super.cleanup()}};exports.Sound=a;
|
|
2
2
|
//# sourceMappingURL=sound.cjs.map
|