cacophony 0.31.0 → 0.32.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -7
- package/THIRD_PARTY_NOTICES.md +15 -0
- package/dist/basePlayback.cjs +1 -1
- package/dist/basePlayback.cjs.map +1 -1
- package/dist/basePlayback.d.ts +8 -5
- package/dist/basePlayback.mjs +7 -0
- package/dist/basePlayback.mjs.map +1 -1
- package/dist/cache.cjs +1 -1
- package/dist/cache.cjs.map +1 -1
- package/dist/cache.d.ts +20 -106
- package/dist/cache.mjs +345 -372
- package/dist/cache.mjs.map +1 -1
- package/dist/cacophony.cjs.map +1 -1
- package/dist/cacophony.d.ts +14 -1
- package/dist/cacophony.mjs.map +1 -1
- package/dist/container.cjs +1 -1
- package/dist/container.cjs.map +1 -1
- package/dist/container.mjs +13 -2
- package/dist/container.mjs.map +1 -1
- package/dist/events.d.ts +1 -0
- package/dist/group.cjs +1 -1
- package/dist/group.cjs.map +1 -1
- package/dist/group.mjs +1 -0
- package/dist/group.mjs.map +1 -1
- package/dist/mediaStream.cjs +1 -1
- package/dist/mediaStream.cjs.map +1 -1
- package/dist/mediaStream.d.ts +1 -0
- package/dist/mediaStream.mjs +17 -16
- package/dist/mediaStream.mjs.map +1 -1
- package/dist/pcmStream.cjs +1 -1
- package/dist/pcmStream.cjs.map +1 -1
- package/dist/pcmStream.d.ts +1 -0
- package/dist/pcmStream.mjs +13 -12
- package/dist/pcmStream.mjs.map +1 -1
- package/dist/playOptions.cjs +2 -0
- package/dist/playOptions.cjs.map +1 -0
- package/dist/playOptions.d.ts +9 -0
- package/dist/playOptions.mjs +56 -0
- package/dist/playOptions.mjs.map +1 -0
- package/dist/playback.cjs +1 -1
- package/dist/playback.cjs.map +1 -1
- package/dist/playback.d.ts +2 -2
- package/dist/playback.mjs +33 -21
- 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 +9 -6
- package/dist/sound.mjs +74 -59
- package/dist/sound.mjs.map +1 -1
- package/dist/synth.cjs +1 -1
- package/dist/synth.cjs.map +1 -1
- package/dist/synth.mjs +17 -13
- package/dist/synth.mjs.map +1 -1
- package/dist/synthPlayback.cjs +1 -1
- package/dist/synthPlayback.cjs.map +1 -1
- package/dist/synthPlayback.mjs +9 -8
- package/dist/synthPlayback.mjs.map +1 -1
- package/docs/assets/hierarchy.js +1 -1
- package/docs/assets/search.js +1 -1
- package/docs/classes/AudioCache.html +20 -51
- package/docs/classes/AudioSprite.html +7 -7
- package/docs/classes/BarberpoleEffect.html +7 -7
- package/docs/classes/BiquadEffect.html +3 -3
- package/docs/classes/Bus.html +21 -21
- package/docs/classes/Cacophony.html +86 -86
- package/docs/classes/DynamicsEffect.html +7 -7
- package/docs/classes/FdnReverbEffect.html +7 -7
- package/docs/classes/FoaDecoder.html +5 -5
- package/docs/classes/FoaDecoderEffect.html +3 -3
- package/docs/classes/FrequencyShifterEffect.html +7 -7
- package/docs/classes/Group.html +32 -32
- package/docs/classes/HarmonizerEffect.html +7 -7
- 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 +81 -68
- package/docs/classes/MediaStreamSound.html +48 -48
- package/docs/classes/MicrophoneStream.html +49 -49
- package/docs/classes/ModulatedDelayEffect.html +7 -7
- package/docs/classes/PcmStreamPlayback.html +81 -68
- package/docs/classes/PcmStreamSound.html +58 -58
- package/docs/classes/PhaserEffect.html +7 -7
- package/docs/classes/Playback.html +106 -93
- package/docs/classes/ReverbEffect.html +7 -7
- package/docs/classes/Scheduler.html +3 -3
- package/docs/classes/ShareEffect.html +3 -3
- package/docs/classes/Sound.html +108 -68
- package/docs/classes/SpectralFreezeEffect.html +7 -7
- package/docs/classes/StereoWidenerEffect.html +3 -3
- package/docs/classes/Synth.html +60 -60
- package/docs/classes/SynthGroup.html +28 -28
- package/docs/classes/TremoloEffect.html +7 -7
- package/docs/classes/TruePeakDetector.html +8 -8
- package/docs/classes/WaveshaperEffect.html +7 -7
- 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 +29 -29
- package/docs/index.html +50 -4
- package/docs/interfaces/AudioBuffer.html +8 -8
- package/docs/interfaces/AudioBufferSourceNode.html +22 -22
- package/docs/interfaces/AudioEventCallbacks.html +2 -2
- package/docs/interfaces/AudioListener.html +12 -12
- package/docs/interfaces/AudioNode.html +14 -14
- package/docs/interfaces/AudioParam.html +6 -6
- package/docs/interfaces/AudioWorklet.html +2 -2
- package/docs/interfaces/AudioWorkletNode.html +16 -16
- 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 +20 -20
- 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 +14 -14
- package/docs/interfaces/ChannelSplitterNode.html +14 -14
- package/docs/interfaces/ConvolverNode.html +16 -16
- package/docs/interfaces/CreateSpriteOptions.html +3 -3
- package/docs/interfaces/DynamicsOptions.html +7 -7
- package/docs/interfaces/FadeStartEvent.html +4 -4
- package/docs/interfaces/FdnReverbOptions.html +6 -6
- package/docs/interfaces/FoaDecoderOptions.html +2 -2
- package/docs/interfaces/FrequencyShifterOptions.html +3 -3
- package/docs/interfaces/GainNode.html +15 -15
- 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 +15 -15
- package/docs/interfaces/MediaStreamAudioSourceNode.html +15 -15
- package/docs/interfaces/MediaStreamSoundOptions.html +5 -5
- package/docs/interfaces/MicrophoneStreamOptions.html +8 -8
- package/docs/interfaces/ModulatedDelayOptions.html +8 -8
- package/docs/interfaces/OfflineOptions.html +5 -5
- package/docs/interfaces/OscillatorNode.html +20 -20
- package/docs/interfaces/PannerNode.html +28 -28
- 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 +62 -8
- package/docs/interfaces/PlaybackErrorEvent.html +5 -5
- package/docs/interfaces/ReverbOptions.html +13 -13
- package/docs/interfaces/RuntimeOptions.html +6 -6
- package/docs/interfaces/ScheduledCallbackHandle.html +2 -2
- package/docs/interfaces/SoundCleanupHoldings.html +4 -4
- package/docs/interfaces/SoundErrorEvent.html +6 -6
- package/docs/interfaces/SpectralFreezeOptions.html +4 -4
- package/docs/interfaces/SpriteRegion.html +4 -4
- package/docs/interfaces/StereoPannerNode.html +15 -15
- package/docs/interfaces/StereoToBFormatOptions.html +2 -2
- package/docs/interfaces/StereoWidenerOptions.html +4 -4
- package/docs/interfaces/StreamCapabilities.html +5 -5
- package/docs/interfaces/TimeStretchOptions.html +5 -5
- package/docs/interfaces/TremoloOptions.html +5 -5
- package/docs/interfaces/WaveshaperOptions.html +5 -5
- package/docs/types/BaseAudioEvents.html +11 -11
- package/docs/types/BuiltEffect.html +1 -1
- package/docs/types/BusConnectionTarget.html +1 -1
- package/docs/types/CacheEventCallback.html +1 -1
- package/docs/types/CacophonyEvents.html +17 -17
- package/docs/types/ErrorEventCallback.html +1 -1
- package/docs/types/FadeType.html +1 -1
- package/docs/types/HrtfPannerOptions.html +1 -1
- package/docs/types/ImpulseResponseSource.html +1 -1
- package/docs/types/LoadingEventCallback.html +1 -1
- package/docs/types/LoopCount.html +1 -1
- package/docs/types/LoudnessChannel.html +1 -1
- package/docs/types/Orientation.html +3 -3
- package/docs/types/PanCloneOverrides.html +5 -5
- package/docs/types/PanType.html +1 -1
- package/docs/types/PcmStreamEvents.html +6 -6
- package/docs/types/PcmStreamState.html +1 -1
- package/docs/types/PlaybackEvents.html +9 -2
- package/docs/types/Position.html +1 -1
- package/docs/types/SoundEvents.html +1 -1
- package/docs/types/SoundType.html +1 -1
- package/docs/types/SourceNode.html +1 -1
- package/docs/types/SpriteMap.html +1 -1
- package/docs/types/SynthEvents.html +1 -1
- package/docs/types/ThreeDOptions.html +1 -1
- package/docs/types/WebCodecsStreamSound.html +1 -1
- package/docs/variables/CHANNEL_WEIGHTS.html +1 -1
- package/docs/variables/K_WEIGHTING_STAGE1_48K.html +1 -1
- package/docs/variables/K_WEIGHTING_STAGE2_48K.html +1 -1
- package/docs/variables/consoleLogger.html +1 -1
- package/docs/variables/noopLogger.html +1 -1
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -127,7 +127,30 @@ Cacophony uses a two-tier architecture that separates audio assets from their pl
|
|
|
127
127
|
- **Sound**: Represents an audio asset (file or buffer). Acts as a container managing multiple playback instances.
|
|
128
128
|
- **Playback**: Represents a single playback instance of a Sound. Each call to `play()` creates a new Playback.
|
|
129
129
|
|
|
130
|
-
This design allows the same sound to be played multiple times simultaneously with different settings (volume, position, playback rate, etc.).
|
|
130
|
+
This design allows the same sound to be played multiple times simultaneously with different settings (volume, position, playback rate, etc.).
|
|
131
|
+
|
|
132
|
+
Pass invocation settings to `play(options)` to configure each voice before audio
|
|
133
|
+
starts. Omitted values inherit source defaults; `0` is valid for volume, stereo
|
|
134
|
+
pan, and loop count. Sound defaults and overlapping voices stay independent.
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
const [voice] = sound.play({ volume: 0.5, playbackRate: 1.2, loopCount: 0,
|
|
138
|
+
panType: 'stereo', stereoPan: 0 });
|
|
139
|
+
const step = group.playOrdered(true, { volume: 0.7, panType: 'HRTF',
|
|
140
|
+
position: [10, 0, 5], threeDOptions: { distanceModel: 'linear', rolloffFactor: 0.1 } });
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`stereoPan` requires the effective stereo mode; `position` and `threeDOptions`
|
|
144
|
+
require HRTF. Partial HRTF options preserve unspecified settings. Group all,
|
|
145
|
+
random, and ordered playback and sprite children accept these same options.
|
|
146
|
+
Invalid options throw before voice preparation, and group playback preflights
|
|
147
|
+
every member. Direct `Playback.play(options)` also supports these settings,
|
|
148
|
+
scheduling, and fades. Calling play on an already playing voice remains a no-op
|
|
149
|
+
after validation.
|
|
150
|
+
|
|
151
|
+
Synths and live streams support volume, spatial settings, and fade-in. They
|
|
152
|
+
reject playback rate, loop count, scheduled starts, end fades, and per-loop fade
|
|
153
|
+
settings because these sources do not support those controls.
|
|
131
154
|
|
|
132
155
|
```typescript
|
|
133
156
|
const sound = await cacophony.createSound('laser.mp3');
|
|
@@ -153,6 +176,20 @@ setTimeout(() => playback2.stop(), 2000);
|
|
|
153
176
|
sound.stop(); // Stops all three playbacks
|
|
154
177
|
```
|
|
155
178
|
|
|
179
|
+
Each playback exposes a read-only `state`: `"unplayed"`, `"playing"`, `"paused"`,
|
|
180
|
+
or `"stopped"`. When `sound.preplay()` or `sound.play()` prepares a new voice,
|
|
181
|
+
it removes stopped entries from `sound.playbacks`. Repeated play/stop calls
|
|
182
|
+
therefore retain at most the most recently stopped voice in that collection.
|
|
183
|
+
Unplayed, paused, and playing voices remain registered.
|
|
184
|
+
|
|
185
|
+
An individual `playback.stop()` preserves the ability to call `playback.play()`
|
|
186
|
+
again. A successfully restarted playback rejoins `sound.playbacks` and receives
|
|
187
|
+
Sound controls again, even if it was previously reaped. Reaping disconnects its
|
|
188
|
+
output and sends; replay restores the Sound's current routing and event forwarding.
|
|
189
|
+
The playback retains its individual volume, rate, and other settings while reaped.
|
|
190
|
+
Reaping does not call `cleanup()`; call `playback.cleanup()` when a retained playback
|
|
191
|
+
is no longer needed.
|
|
192
|
+
|
|
156
193
|
### Audio Sprites for Games
|
|
157
194
|
|
|
158
195
|
Pack many short effects into one decoded WAV atlas while keeping each named
|
|
@@ -1267,11 +1304,18 @@ synth.type = 'square'; // Triggers typeChange event
|
|
|
1267
1304
|
| `pause` | `void` | Fired when playback pauses. |
|
|
1268
1305
|
| `resume` | `void` | Fired when playback resumes after being paused. |
|
|
1269
1306
|
| `ended` | `void` | Fired when playback ends naturally. |
|
|
1270
|
-
| `seek` | `number` | Fired
|
|
1307
|
+
| `seek` | `number` | Fired once after a successful `Playback.seek(time)`, with the requested time in seconds. Rejected seeks do not emit. |
|
|
1271
1308
|
| `volumeChange` | `number` | Fired when playback volume changes. Receives new volume value. |
|
|
1272
1309
|
| `error` | `PlaybackErrorEvent` | Fired on playback errors. Contains `error`, `errorType`, `timestamp`, `recoverable`. |
|
|
1273
1310
|
|
|
1274
|
-
|
|
1311
|
+
`seek` is specific to `Playback` (buffer and media-element sources). Subscribe on
|
|
1312
|
+
each playback to observe seeks, including those requested through `Sound.seek()`.
|
|
1313
|
+
`Sound` does not relay a collective `seek`: its concurrent voices can have
|
|
1314
|
+
different positions, and a collective seek can fail after some voices have moved.
|
|
1315
|
+
For media elements, the event reports acceptance of the `currentTime` write;
|
|
1316
|
+
it does not wait for the browser's asynchronous `seeked` event or playback resumption.
|
|
1317
|
+
|
|
1318
|
+
#### Synth Events
|
|
1275
1319
|
|
|
1276
1320
|
| Event | Payload | Description |
|
|
1277
1321
|
|-------|---------|-------------|
|
|
@@ -1292,7 +1336,29 @@ Cacophony implements intelligent three-layer caching for optimal performance:
|
|
|
1292
1336
|
|
|
1293
1337
|
**Memory Cache (LRU)** → **Browser Cache API** → **Network**
|
|
1294
1338
|
|
|
1295
|
-
|
|
1339
|
+
HTTP reuse decisions are made by `http-cache-semantics` for both decoded memory entries and persistent responses, using the response headers available to the runtime. Fresh responses can be reused; stale responses are conditionally validated when ETag or Last-Modified is available. `no-cache` requires validation, and `no-store` prevents retention in either layer. A successful matching 304 can reuse the decoded buffer while updating its policy.
|
|
1340
|
+
|
|
1341
|
+
If a 304 cannot be matched to the stored representation, Cacophony retries with an unconditional Fetch reload. This includes a 304 that omits the ETag of an ETag-bearing stored response. Compared with the previous cache implementation, such servers can incur an additional request; return matching validators on 304 responses to allow reuse.
|
|
1342
|
+
|
|
1343
|
+
A matching 304 emits `onCacheHit` with `cacheType: "conditional"`. When it reuses an already decoded buffer, it does not emit `onLoadingComplete`; when it decodes retained bytes, it emits both events. This differs from the previous implementation's completion event on every 304. Await `getAudioBuffer()` to observe completion regardless of the cache path.
|
|
1344
|
+
|
|
1345
|
+
The 24-hour default TTL applies only when the response supplies no Cache-Control, Expires, Pragma, or validators. Explicit expiry never falls back to that TTL. Custom TTL values use milliseconds and are rounded down to whole seconds. Persistence uses a versioned response containing both bytes and policy; older cache entries are ignored and fetched again. On first persistent-cache access, Cacophony attempts to delete its legacy `audio-cache` namespace. Cleanup failure is reported through `onCacheError` and does not prevent loading or use of the current cache.
|
|
1346
|
+
|
|
1347
|
+
`AudioCache.setCacheExpirationTime()` affects newly created response policies. Existing decoded and persisted entries keep the lifetime assigned when their policy was created, including across cache instances. Clearing decoded memory does not rewrite persistent policies. Unlike the previous implementation, negative or non-finite TTL values throw `RangeError` and leave the configured TTL unchanged; zero is valid and disables fallback freshness.
|
|
1348
|
+
|
|
1349
|
+
Compatibility change: a response with only `Cache-Control: public` or `private` and no expiry or validators no longer receives the fallback TTL. These directives permit storage but do not specify how long the response is fresh. Such responses can be stored, but subsequent loads go through Fetch again. Cacophony deliberately does not assign heuristic freshness to responses with explicit cache directives. To enable fresh reuse, serve an appropriate `max-age` (for example, `private, max-age=60`) or `Expires`; increasing the fallback TTL does not override those responses.
|
|
1350
|
+
|
|
1351
|
+
Cross-origin audio uses both decoded memory and persistent caching. CORS exposes Cache-Control, Expires, and Last-Modified without extra server configuration. For cross-origin entries requiring validation, Cacophony asks Fetch to revalidate through the browser HTTP cache, avoiding application-added conditional headers and their CORS preflight requirements. Browser HTTP caching remains enabled. A browser-validated response is decoded again because Fetch delivers the resulting representation rather than its internal 304 exchange.
|
|
1352
|
+
|
|
1353
|
+
In browsers, the guarantee is HTTP policy based on browser-visible headers. Cross-origin caching remains enabled when CORS hides headers. A hidden Age can overestimate remaining freshness, and a hidden Vary cannot participate in application-cache matching. Browser validation applies when Fetch runs; a fresh application-cache hit does not consult the browser HTTP cache. This URL-only API explicitly controls Accept; a visible Vary on other, host-managed headers prevents application-cache retention. Data-URL decoding and caching are unchanged.
|
|
1354
|
+
|
|
1355
|
+
For static audio, use versioned or content-hashed URLs such as `/audio/chime.a1b2c3.ogg` with a long `Cache-Control: max-age`. Publish changed audio at a new URL. For mutable audio requiring validation on every load, use `Cache-Control: no-cache`. Expose relevant response headers when freshness or variants depend on them:
|
|
1356
|
+
|
|
1357
|
+
```http
|
|
1358
|
+
Access-Control-Expose-Headers: Age, Date, ETag, Vary
|
|
1359
|
+
```
|
|
1360
|
+
|
|
1361
|
+
This header supplements the server's normal CORS access configuration. Exposing ETag does not cause Cacophony to add conditional request headers to cross-origin requests; Fetch still handles validation.
|
|
1296
1362
|
|
|
1297
1363
|
```typescript
|
|
1298
1364
|
const cacophony = new Cacophony();
|
|
@@ -1300,7 +1366,7 @@ const cacophony = new Cacophony();
|
|
|
1300
1366
|
// First load - fetches from network, stores in cache
|
|
1301
1367
|
const sound1 = await cacophony.createSound('audio.mp3');
|
|
1302
1368
|
|
|
1303
|
-
// Second load -
|
|
1369
|
+
// Second load - from memory while the HTTP policy permits reuse
|
|
1304
1370
|
const sound2 = await cacophony.createSound('audio.mp3');
|
|
1305
1371
|
|
|
1306
1372
|
// Monitor cache performance via events
|
|
@@ -1317,12 +1383,12 @@ cacophony.on('cacheMiss', (event) => {
|
|
|
1317
1383
|
// Clear memory cache (browser cache persists)
|
|
1318
1384
|
cacophony.clearMemoryCache();
|
|
1319
1385
|
|
|
1320
|
-
// Optional: configure TTL for
|
|
1386
|
+
// Optional: configure TTL for responses without cache directives, expiry, or validators
|
|
1321
1387
|
import { AudioCache } from 'cacophony';
|
|
1322
1388
|
AudioCache.setCacheExpirationTime(60 * 60 * 1000); // 1 hour
|
|
1323
1389
|
```
|
|
1324
1390
|
|
|
1325
|
-
|
|
1391
|
+
Decoded buffers remain bounded to 64 MiB per audio context. Concurrent callers share fetching and decoding; cancelling one caller does not cancel the others. Storage errors are reported through cacheError while a successfully downloaded response can still be delivered.
|
|
1326
1392
|
|
|
1327
1393
|
## Cancellation with AbortSignal
|
|
1328
1394
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
## http-cache-semantics 4.1.1
|
|
4
|
+
|
|
5
|
+
https://github.com/kornelski/http-cache-semantics
|
|
6
|
+
|
|
7
|
+
Copyright 2016-2018 Kornel Lesiński
|
|
8
|
+
|
|
9
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
10
|
+
|
|
11
|
+
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
12
|
+
|
|
13
|
+
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/dist/basePlayback.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./effectChain.cjs"),t=require("./eventEmitter.cjs"),n=require("./filters.cjs"),r=require("./pannerMixin.cjs"),i=require("./volumeMixin.cjs");var a=class extends r.PannerMixin(i.VolumeMixin(n.FilterManager)){source;_effectChain;_effectContext;_sendGains=new Map;_state=`unplayed`;origin;eventEmitter=new t.TypedEventEmitter;constructor(e){super(),this.origin=e}setEffectChainEndpoints(t,n){if(this._effectContext=t.context??this.origin.context,this._effectChain){this._effectChain.setEndpoints(t,n);return}this._effectChain=new e.EffectChain(t,n,this.constructor.name)}addEffect(e){try{return this.materializeEffect(e)}catch(e){return Promise.reject(e)}}_addSourceEffect(e){return this.materializeEffect(e)}_addFilterEffect(e){return this.materializeEffect(e,e=>{this._filters.push(e)})}materializeEffect(e,t){if(!this._effectChain||!this._effectContext)throw Error(`Cannot add an effect before the playback effect chain is initialized`);let n=this._effectChain,r=n.reserve(),i;try{i=e.build(this._effectContext)}catch(e){throw n.cancel(r),e}if(i instanceof Promise||typeof i==`object`&&i&&`then`in i)return Promise.resolve(i).then(e=>{let i=n.resolve(r,e);return t?.(i),i},e=>{throw n.cancel(r),e});try{let e=n.resolve(r,i);return t?.(e),Promise.resolve(e)}catch(e){throw n.cancel(r),e}}rampEffectParam(e,t,n,r){if(!this._effectChain)throw Error(`Cannot automate an effect before the playback effect chain is initialized`);this._effectChain.rampParam(e,t,n,r)}addFilter(e){if(!this._effectChain)throw Error(`Cannot add a filter before the playback effect chain is initialized`);this._effectChain.add(e,this._filters.length),this._filters.push(e)}removeFilter(e){if(!this._effectChain)throw Error(`Cannot remove a filter before the playback effect chain is initialized`);this._effectChain.remove(e),this._filters=this._filters.filter(t=>t!==e)}setFilterOrder(e){if(!this._effectChain)throw Error(`Cannot reorder filters before the playback effect chain is initialized`);let t=this._effectChain.nodes.filter(e=>!this._filters.includes(e));this._effectChain.setOrder([...e,...t]),this._filters=[...e]}setFilterBypassed(e,t){if(!this._effectChain)throw Error(`Cannot bypass a filter before the playback effect chain is initialized`);this._effectChain.setBypassed(e,t)}isFilterBypassed(e){return this._effectChain?.isBypassed(e)??!1}rampFilterParam(e,t,n,r){if(!this._effectChain)throw Error(`Cannot automate a filter before the playback effect chain is initialized`);this._effectChain.rampParam(e,t,n,r)}get isPlaying(){return this._state===`playing`}get isPaused(){return this._state===`paused`}markPlaying(){this._state=`playing`}markPaused(){this._state=`paused`}markStopped(){this._state=`stopped`}emitPlayStarted(e){this.markPlaying(),this.emit(`play`,this),e&&this.emit(`resume`,void 0),this.origin.cacophony?.emit(`globalPlay`,{source:this.origin,timestamp:Date.now()})}emitPaused(){this.markPaused(),this.emit(`pause`,void 0),this.origin.cacophony?.emit(`globalPause`,{source:this.origin,timestamp:Date.now()})}emitStopped(){this.markStopped(),this.emit(`stop`,void 0),this.origin.cacophony?.emit(`globalStop`,{source:this.origin,timestamp:Date.now()})}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)}fadeTo(e,t,n=`linear`,r){return this.emit(`fadeStart`,{target:e,duration:t,type:n}),super.fadeTo(e,t,n,r).then(()=>{this.emit(`fadeEnd`,void 0)})}cancelFade(){let e=this._isFading;super.cancelFade(),e&&this.emit(`fadeCancel`,void 0)}cleanup(){for(let e of this._sendGains.values())try{e.disconnect()}catch{}this._sendGains.clear(),this._effectChain?.destroy(),this._effectChain=void 0,this._effectContext=void 0,this.eventEmitter.removeAllListeners(),super.cleanup()}};exports.BasePlayback=a;
|
|
1
|
+
const e=require("./effectChain.cjs"),t=require("./eventEmitter.cjs"),n=require("./filters.cjs"),r=require("./pannerMixin.cjs"),i=require("./volumeMixin.cjs");var a=class extends r.PannerMixin(i.VolumeMixin(n.FilterManager)){source;_effectChain;_effectContext;_sendGains=new Map;_state=`unplayed`;origin;eventEmitter=new t.TypedEventEmitter;constructor(e){super(),this.origin=e}setEffectChainEndpoints(t,n){if(this._effectContext=t.context??this.origin.context,this._effectChain){this._effectChain.setEndpoints(t,n);return}this._effectChain=new e.EffectChain(t,n,this.constructor.name)}addEffect(e){try{return this.materializeEffect(e)}catch(e){return Promise.reject(e)}}_addSourceEffect(e){return this.materializeEffect(e)}_addFilterEffect(e){return this.materializeEffect(e,e=>{this._filters.push(e)})}materializeEffect(e,t){if(!this._effectChain||!this._effectContext)throw Error(`Cannot add an effect before the playback effect chain is initialized`);let n=this._effectChain,r=n.reserve(),i;try{i=e.build(this._effectContext)}catch(e){throw n.cancel(r),e}if(i instanceof Promise||typeof i==`object`&&i&&`then`in i)return Promise.resolve(i).then(e=>{let i=n.resolve(r,e);return t?.(i),i},e=>{throw n.cancel(r),e});try{let e=n.resolve(r,i);return t?.(e),Promise.resolve(e)}catch(e){throw n.cancel(r),e}}rampEffectParam(e,t,n,r){if(!this._effectChain)throw Error(`Cannot automate an effect before the playback effect chain is initialized`);this._effectChain.rampParam(e,t,n,r)}addFilter(e){if(!this._effectChain)throw Error(`Cannot add a filter before the playback effect chain is initialized`);this._effectChain.add(e,this._filters.length),this._filters.push(e)}removeFilter(e){if(!this._effectChain)throw Error(`Cannot remove a filter before the playback effect chain is initialized`);this._effectChain.remove(e),this._filters=this._filters.filter(t=>t!==e)}setFilterOrder(e){if(!this._effectChain)throw Error(`Cannot reorder filters before the playback effect chain is initialized`);let t=this._effectChain.nodes.filter(e=>!this._filters.includes(e));this._effectChain.setOrder([...e,...t]),this._filters=[...e]}setFilterBypassed(e,t){if(!this._effectChain)throw Error(`Cannot bypass a filter before the playback effect chain is initialized`);this._effectChain.setBypassed(e,t)}isFilterBypassed(e){return this._effectChain?.isBypassed(e)??!1}rampFilterParam(e,t,n,r){if(!this._effectChain)throw Error(`Cannot automate a filter before the playback effect chain is initialized`);this._effectChain.rampParam(e,t,n,r)}setPanType(e,t){let n=this.panner;super.setPanType(e,t),!(this.panner===n||!this.panner||!this.source||!this.gainNode)&&(this.setEffectChainEndpoints(this.source,this.panner),this.panner.connect(this.gainNode))}get state(){return this._state}get isPlaying(){return this._state===`playing`}get isPaused(){return this._state===`paused`}markPlaying(){this._state=`playing`}markPaused(){this._state=`paused`}markStopped(){this._state=`stopped`}emitPlayStarted(e){this.markPlaying(),this.emit(`play`,this),e&&this.emit(`resume`,void 0),this.origin.cacophony?.emit(`globalPlay`,{source:this.origin,timestamp:Date.now()})}emitPaused(){this.markPaused(),this.emit(`pause`,void 0),this.origin.cacophony?.emit(`globalPause`,{source:this.origin,timestamp:Date.now()})}emitStopped(){this.markStopped(),this.emit(`stop`,void 0),this.origin.cacophony?.emit(`globalStop`,{source:this.origin,timestamp:Date.now()})}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)}fadeTo(e,t,n=`linear`,r){return this.emit(`fadeStart`,{target:e,duration:t,type:n}),super.fadeTo(e,t,n,r).then(()=>{this.emit(`fadeEnd`,void 0)})}cancelFade(){let e=this._isFading;super.cancelFade(),e&&this.emit(`fadeCancel`,void 0)}cleanup(){for(let e of this._sendGains.values())try{e.disconnect()}catch{}this._sendGains.clear(),this._effectChain?.destroy(),this._effectChain=void 0,this._effectContext=void 0,this.eventEmitter.removeAllListeners(),super.cleanup()}};exports.BasePlayback=a;
|
|
2
2
|
//# sourceMappingURL=basePlayback.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"basePlayback.cjs","names":[],"sources":["../src/basePlayback.ts"],"sourcesContent":["import type { Bus } from \"./bus\";\nimport type { FadeType, PlayOptions } from \"./cacophony\";\nimport type { PlaybackContainer } from \"./container\";\nimport type { AudioNode, BaseContext, BiquadFilterNode, GainNode } from \"./context\";\nimport { EffectChain } from \"./effectChain\";\nimport type { CacophonyEffect } from \"./effects\";\nimport { TypedEventEmitter } from \"./eventEmitter\";\nimport type { PlaybackEvents } from \"./events\";\nimport { FilterManager } from \"./filters\";\nimport { PannerMixin } from \"./pannerMixin\";\nimport { VolumeMixin } from \"./volumeMixin\";\n\nexport type PlaybackState = \"unplayed\" | \"playing\" | \"paused\" | \"stopped\";\n\nexport abstract class BasePlayback extends /* @__PURE__ */ PannerMixin(/* @__PURE__ */ VolumeMixin(FilterManager)) {\n public source?: AudioNode;\n protected _effectChain?: EffectChain;\n private _effectContext?: BaseContext;\n /**\n * Per-playback send-gain allocations owned by the shared routing state\n * machine. Cleanup disconnects every allocation deterministically.\n */\n _sendGains: Map<Bus, GainNode> = new Map();\n protected _state: PlaybackState = \"unplayed\";\n public origin: PlaybackContainer;\n public eventEmitter: TypedEventEmitter<PlaybackEvents> = new TypedEventEmitter<PlaybackEvents>();\n\n constructor(origin: PlaybackContainer) {\n super();\n this.origin = origin;\n }\n\n protected setEffectChainEndpoints(input: AudioNode, output: AudioNode): void {\n this._effectContext =\n (input.context as BaseContext | undefined) ??\n (this.origin as PlaybackContainer & { context?: BaseContext }).context;\n if (this._effectChain) {\n this._effectChain.setEndpoints(input, output);\n return;\n }\n this._effectChain = new EffectChain(input, output, this.constructor.name);\n }\n\n /** Build and splice an effect into this live playback's pre-panner chain. */\n addEffect(effect: CacophonyEffect): Promise<AudioNode> {\n try {\n return this.materializeEffect(effect);\n } catch (error) {\n return Promise.reject(error);\n }\n }\n\n /** @internal Source preplay path that preserves synchronous native-build failures. */\n _addSourceEffect(effect: CacophonyEffect): Promise<AudioNode> {\n return this.materializeEffect(effect);\n }\n\n /** @internal Materialize a source-level biquad recipe and retain filter API compatibility. */\n _addFilterEffect(effect: CacophonyEffect): Promise<AudioNode> {\n return this.materializeEffect(effect, (handle) => {\n this._filters.push(handle as BiquadFilterNode);\n });\n }\n\n private materializeEffect(effect: CacophonyEffect, onBuilt?: (handle: AudioNode) => void): Promise<AudioNode> {\n if (!this._effectChain || !this._effectContext) {\n throw new Error(\"Cannot add an effect before the playback effect chain is initialized\");\n }\n const chain = this._effectChain;\n const reservation = chain.reserve();\n let built;\n try {\n built = effect.build(this._effectContext);\n } catch (error) {\n chain.cancel(reservation);\n throw error;\n }\n if (built instanceof Promise || (typeof built === \"object\" && built !== null && \"then\" in built)) {\n return Promise.resolve(built).then(\n (resolved) => {\n const handle = chain.resolve(reservation, resolved);\n onBuilt?.(handle);\n return handle;\n },\n (error) => {\n chain.cancel(reservation);\n throw error;\n },\n );\n }\n try {\n const handle = chain.resolve(reservation, built);\n onBuilt?.(handle);\n return Promise.resolve(handle);\n } catch (error) {\n chain.cancel(reservation);\n throw error;\n }\n }\n\n rampEffectParam(\n handle: AudioNode,\n paramName: string,\n value: number,\n options?: { duration?: number; type?: FadeType },\n ): void {\n if (!this._effectChain) {\n throw new Error(\"Cannot automate an effect before the playback effect chain is initialized\");\n }\n this._effectChain.rampParam(handle, paramName, value, options);\n }\n\n addFilter(filter: BiquadFilterNode): void {\n if (!this._effectChain) {\n throw new Error(\"Cannot add a filter before the playback effect chain is initialized\");\n }\n this._effectChain.add(filter, this._filters.length);\n this._filters.push(filter);\n }\n\n removeFilter(filter: BiquadFilterNode): void {\n if (!this._effectChain) {\n throw new Error(\"Cannot remove a filter before the playback effect chain is initialized\");\n }\n this._effectChain.remove(filter);\n this._filters = this._filters.filter((candidate) => candidate !== filter);\n }\n\n setFilterOrder(filters: readonly BiquadFilterNode[]): void {\n if (!this._effectChain) {\n throw new Error(\"Cannot reorder filters before the playback effect chain is initialized\");\n }\n const nonFilters = this._effectChain.nodes.filter((node) => !this._filters.includes(node as BiquadFilterNode));\n this._effectChain.setOrder([...filters, ...nonFilters]);\n this._filters = [...filters];\n }\n\n setFilterBypassed(filter: BiquadFilterNode, bypassed: boolean): void {\n if (!this._effectChain) {\n throw new Error(\"Cannot bypass a filter before the playback effect chain is initialized\");\n }\n this._effectChain.setBypassed(filter, bypassed);\n }\n\n isFilterBypassed(filter: BiquadFilterNode): boolean {\n return this._effectChain?.isBypassed(filter) ?? false;\n }\n\n rampFilterParam(\n filter: BiquadFilterNode,\n paramName: string,\n value: number,\n options?: { duration?: number; type?: FadeType },\n ): void {\n if (!this._effectChain) {\n throw new Error(\"Cannot automate a filter before the playback effect chain is initialized\");\n }\n this._effectChain.rampParam(filter, paramName, value, options);\n }\n\n abstract play(options?: PlayOptions): [this];\n abstract pause(): void;\n abstract stop(): void;\n\n /**\n * Checks if the audio is currently playing.\n */\n\n get isPlaying(): boolean {\n return this._state === \"playing\";\n }\n\n get isPaused(): boolean {\n return this._state === \"paused\";\n }\n\n protected markPlaying(): void {\n this._state = \"playing\";\n }\n\n protected markPaused(): void {\n this._state = \"paused\";\n }\n\n protected markStopped(): void {\n this._state = \"stopped\";\n }\n\n protected emitPlayStarted(isResume: boolean): void {\n this.markPlaying();\n this.emit(\"play\", this);\n if (isResume) {\n this.emit(\"resume\", undefined);\n }\n this.origin.cacophony?.emit(\"globalPlay\", {\n source: this.origin,\n timestamp: Date.now(),\n });\n }\n\n protected emitPaused(): void {\n this.markPaused();\n this.emit(\"pause\", undefined);\n this.origin.cacophony?.emit(\"globalPause\", {\n source: this.origin,\n timestamp: Date.now(),\n });\n }\n\n protected emitStopped(): void {\n this.markStopped();\n this.emit(\"stop\", undefined);\n this.origin.cacophony?.emit(\"globalStop\", {\n source: this.origin,\n timestamp: Date.now(),\n });\n }\n\n /**\n * Register event listener.\n * @returns Cleanup function\n */\n on<K extends keyof PlaybackEvents>(event: K, listener: (data: PlaybackEvents[K]) => void): () => void {\n return this.eventEmitter.on(event, listener);\n }\n\n /**\n * Remove event listener.\n */\n off<K extends keyof PlaybackEvents>(event: K, listener: (data: PlaybackEvents[K]) => void): void {\n this.eventEmitter.off(event, listener);\n }\n\n public emit<K extends keyof PlaybackEvents>(event: K, data: PlaybackEvents[K]): void {\n this.eventEmitter.emit(event, data);\n }\n\n public async emitAsync<K extends keyof PlaybackEvents>(event: K, data: PlaybackEvents[K]): Promise<void> {\n return this.eventEmitter.emitAsync(event, data);\n }\n\n /**\n * Fades the volume to a target value, emitting fadeStart and fadeEnd events.\n */\n fadeTo(\n value: number,\n duration: number,\n type: FadeType = \"linear\",\n options?: { startTime?: number; startValue?: number },\n ): Promise<void> {\n this.emit(\"fadeStart\", { target: value, duration, type });\n return super.fadeTo(value, duration, type, options).then(() => {\n this.emit(\"fadeEnd\", undefined);\n });\n }\n\n /**\n * Cancels any in-progress fade, emitting fadeCancel if a fade was active.\n */\n cancelFade(): void {\n const wasFading = this._isFading;\n super.cancelFade();\n if (wasFading) {\n this.emit(\"fadeCancel\", undefined);\n }\n }\n\n cleanup(): void {\n for (const sendGain of this._sendGains.values()) {\n try {\n sendGain.disconnect();\n } catch {\n // The node may already have been disconnected externally.\n }\n }\n this._sendGains.clear();\n this._effectChain?.destroy();\n this._effectChain = undefined;\n this._effectContext = undefined;\n this.eventEmitter.removeAllListeners();\n super.cleanup();\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"basePlayback.cjs","names":[],"sources":["../src/basePlayback.ts"],"sourcesContent":["import type { Bus } from \"./bus\";\nimport type { FadeType, PanType, PlayOptions } from \"./cacophony\";\nimport type { PlaybackContainer } from \"./container\";\nimport type { AudioNode, BaseContext, BiquadFilterNode, GainNode } from \"./context\";\nimport { EffectChain } from \"./effectChain\";\nimport type { CacophonyEffect } from \"./effects\";\nimport { TypedEventEmitter } from \"./eventEmitter\";\nimport type { PlaybackEvents } from \"./events\";\nimport { FilterManager } from \"./filters\";\nimport { PannerMixin } from \"./pannerMixin\";\nimport { VolumeMixin } from \"./volumeMixin\";\n\nexport type PlaybackState = \"unplayed\" | \"playing\" | \"paused\" | \"stopped\";\n\nexport abstract class BasePlayback extends /* @__PURE__ */ PannerMixin(/* @__PURE__ */ VolumeMixin(FilterManager)) {\n public source?: AudioNode;\n protected _effectChain?: EffectChain;\n private _effectContext?: BaseContext;\n /**\n * Per-playback send-gain allocations owned by the shared routing state\n * machine. Cleanup disconnects every allocation deterministically.\n */\n _sendGains: Map<Bus, GainNode> = new Map();\n protected _state: PlaybackState = \"unplayed\";\n public origin: PlaybackContainer;\n public eventEmitter: TypedEventEmitter<PlaybackEvents> = new TypedEventEmitter<PlaybackEvents>();\n\n constructor(origin: PlaybackContainer) {\n super();\n this.origin = origin;\n }\n\n protected setEffectChainEndpoints(input: AudioNode, output: AudioNode): void {\n this._effectContext =\n (input.context as BaseContext | undefined) ??\n (this.origin as PlaybackContainer & { context?: BaseContext }).context;\n if (this._effectChain) {\n this._effectChain.setEndpoints(input, output);\n return;\n }\n this._effectChain = new EffectChain(input, output, this.constructor.name);\n }\n\n /** Build and splice an effect into this live playback's pre-panner chain. */\n addEffect(effect: CacophonyEffect): Promise<AudioNode> {\n try {\n return this.materializeEffect(effect);\n } catch (error) {\n return Promise.reject(error);\n }\n }\n\n /** @internal Source preplay path that preserves synchronous native-build failures. */\n _addSourceEffect(effect: CacophonyEffect): Promise<AudioNode> {\n return this.materializeEffect(effect);\n }\n\n /** @internal Materialize a source-level biquad recipe and retain filter API compatibility. */\n _addFilterEffect(effect: CacophonyEffect): Promise<AudioNode> {\n return this.materializeEffect(effect, (handle) => {\n this._filters.push(handle as BiquadFilterNode);\n });\n }\n\n private materializeEffect(effect: CacophonyEffect, onBuilt?: (handle: AudioNode) => void): Promise<AudioNode> {\n if (!this._effectChain || !this._effectContext) {\n throw new Error(\"Cannot add an effect before the playback effect chain is initialized\");\n }\n const chain = this._effectChain;\n const reservation = chain.reserve();\n let built;\n try {\n built = effect.build(this._effectContext);\n } catch (error) {\n chain.cancel(reservation);\n throw error;\n }\n if (built instanceof Promise || (typeof built === \"object\" && built !== null && \"then\" in built)) {\n return Promise.resolve(built).then(\n (resolved) => {\n const handle = chain.resolve(reservation, resolved);\n onBuilt?.(handle);\n return handle;\n },\n (error) => {\n chain.cancel(reservation);\n throw error;\n },\n );\n }\n try {\n const handle = chain.resolve(reservation, built);\n onBuilt?.(handle);\n return Promise.resolve(handle);\n } catch (error) {\n chain.cancel(reservation);\n throw error;\n }\n }\n\n rampEffectParam(\n handle: AudioNode,\n paramName: string,\n value: number,\n options?: { duration?: number; type?: FadeType },\n ): void {\n if (!this._effectChain) {\n throw new Error(\"Cannot automate an effect before the playback effect chain is initialized\");\n }\n this._effectChain.rampParam(handle, paramName, value, options);\n }\n\n addFilter(filter: BiquadFilterNode): void {\n if (!this._effectChain) {\n throw new Error(\"Cannot add a filter before the playback effect chain is initialized\");\n }\n this._effectChain.add(filter, this._filters.length);\n this._filters.push(filter);\n }\n\n removeFilter(filter: BiquadFilterNode): void {\n if (!this._effectChain) {\n throw new Error(\"Cannot remove a filter before the playback effect chain is initialized\");\n }\n this._effectChain.remove(filter);\n this._filters = this._filters.filter((candidate) => candidate !== filter);\n }\n\n setFilterOrder(filters: readonly BiquadFilterNode[]): void {\n if (!this._effectChain) {\n throw new Error(\"Cannot reorder filters before the playback effect chain is initialized\");\n }\n const nonFilters = this._effectChain.nodes.filter((node) => !this._filters.includes(node as BiquadFilterNode));\n this._effectChain.setOrder([...filters, ...nonFilters]);\n this._filters = [...filters];\n }\n\n setFilterBypassed(filter: BiquadFilterNode, bypassed: boolean): void {\n if (!this._effectChain) {\n throw new Error(\"Cannot bypass a filter before the playback effect chain is initialized\");\n }\n this._effectChain.setBypassed(filter, bypassed);\n }\n\n isFilterBypassed(filter: BiquadFilterNode): boolean {\n return this._effectChain?.isBypassed(filter) ?? false;\n }\n\n rampFilterParam(\n filter: BiquadFilterNode,\n paramName: string,\n value: number,\n options?: { duration?: number; type?: FadeType },\n ): void {\n if (!this._effectChain) {\n throw new Error(\"Cannot automate a filter before the playback effect chain is initialized\");\n }\n this._effectChain.rampParam(filter, paramName, value, options);\n }\n\n override setPanType(panType: PanType, audioContext: BaseContext): void {\n const previousPanner = this.panner;\n super.setPanType(panType, audioContext);\n if (this.panner === previousPanner || !this.panner || !this.source || !this.gainNode) return;\n this.setEffectChainEndpoints(this.source, this.panner);\n this.panner.connect(this.gainNode);\n }\n\n abstract play(options?: PlayOptions): [this];\n abstract pause(): void;\n abstract stop(): void;\n\n /**\n * Gets the current lifecycle state without changing playback.\n */\n\n get state(): PlaybackState {\n return this._state;\n }\n\n /** Checks if the audio is currently playing. */\n get isPlaying(): boolean {\n return this._state === \"playing\";\n }\n\n get isPaused(): boolean {\n return this._state === \"paused\";\n }\n\n protected markPlaying(): void {\n this._state = \"playing\";\n }\n\n protected markPaused(): void {\n this._state = \"paused\";\n }\n\n protected markStopped(): void {\n this._state = \"stopped\";\n }\n\n protected emitPlayStarted(isResume: boolean): void {\n this.markPlaying();\n this.emit(\"play\", this);\n if (isResume) {\n this.emit(\"resume\", undefined);\n }\n this.origin.cacophony?.emit(\"globalPlay\", {\n source: this.origin,\n timestamp: Date.now(),\n });\n }\n\n protected emitPaused(): void {\n this.markPaused();\n this.emit(\"pause\", undefined);\n this.origin.cacophony?.emit(\"globalPause\", {\n source: this.origin,\n timestamp: Date.now(),\n });\n }\n\n protected emitStopped(): void {\n this.markStopped();\n this.emit(\"stop\", undefined);\n this.origin.cacophony?.emit(\"globalStop\", {\n source: this.origin,\n timestamp: Date.now(),\n });\n }\n\n /**\n * Register event listener.\n * @returns Cleanup function\n */\n on<K extends keyof PlaybackEvents>(event: K, listener: (data: PlaybackEvents[K]) => void): () => void {\n return this.eventEmitter.on(event, listener);\n }\n\n /**\n * Remove event listener.\n */\n off<K extends keyof PlaybackEvents>(event: K, listener: (data: PlaybackEvents[K]) => void): void {\n this.eventEmitter.off(event, listener);\n }\n\n public emit<K extends keyof PlaybackEvents>(event: K, data: PlaybackEvents[K]): void {\n this.eventEmitter.emit(event, data);\n }\n\n public async emitAsync<K extends keyof PlaybackEvents>(event: K, data: PlaybackEvents[K]): Promise<void> {\n return this.eventEmitter.emitAsync(event, data);\n }\n\n /**\n * Fades the volume to a target value, emitting fadeStart and fadeEnd events.\n */\n fadeTo(\n value: number,\n duration: number,\n type: FadeType = \"linear\",\n options?: { startTime?: number; startValue?: number },\n ): Promise<void> {\n this.emit(\"fadeStart\", { target: value, duration, type });\n return super.fadeTo(value, duration, type, options).then(() => {\n this.emit(\"fadeEnd\", undefined);\n });\n }\n\n /**\n * Cancels any in-progress fade, emitting fadeCancel if a fade was active.\n */\n cancelFade(): void {\n const wasFading = this._isFading;\n super.cancelFade();\n if (wasFading) {\n this.emit(\"fadeCancel\", undefined);\n }\n }\n\n cleanup(): void {\n for (const sendGain of this._sendGains.values()) {\n try {\n sendGain.disconnect();\n } catch {\n // The node may already have been disconnected externally.\n }\n }\n this._sendGains.clear();\n this._effectChain?.destroy();\n this._effectChain = undefined;\n this._effectContext = undefined;\n this.eventEmitter.removeAllListeners();\n super.cleanup();\n }\n}\n"],"mappings":"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"}
|
package/dist/basePlayback.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Bus } from './bus';
|
|
2
|
-
import { FadeType, PlayOptions } from './cacophony';
|
|
2
|
+
import { FadeType, PanType, PlayOptions } from './cacophony';
|
|
3
3
|
import { PlaybackContainer } from './container';
|
|
4
4
|
import { AudioNode, BaseContext, BiquadFilterNode, GainNode } from './context';
|
|
5
5
|
import { EffectChain } from './effectChain';
|
|
@@ -10,9 +10,9 @@ import { FilterManager } from './filters';
|
|
|
10
10
|
export type PlaybackState = "unplayed" | "playing" | "paused" | "stopped";
|
|
11
11
|
declare const BasePlayback_base: (abstract new (...args: any[]) => {
|
|
12
12
|
panner?: import('./context').PannerNode | import('./context').StereoPannerNode | undefined;
|
|
13
|
-
_panType:
|
|
14
|
-
readonly panType:
|
|
15
|
-
setPanType(panType:
|
|
13
|
+
_panType: PanType;
|
|
14
|
+
readonly panType: PanType;
|
|
15
|
+
setPanType(panType: PanType, audioContext: BaseContext): void;
|
|
16
16
|
setPannerNode(pannerNode: import('./context').PannerNode): void;
|
|
17
17
|
get stereoPan(): number | null;
|
|
18
18
|
set stereoPan(value: number);
|
|
@@ -60,12 +60,15 @@ export declare abstract class BasePlayback extends /* @__PURE__ */ BasePlayback_
|
|
|
60
60
|
duration?: number;
|
|
61
61
|
type?: FadeType;
|
|
62
62
|
}): void;
|
|
63
|
+
setPanType(panType: PanType, audioContext: BaseContext): void;
|
|
63
64
|
abstract play(options?: PlayOptions): [this];
|
|
64
65
|
abstract pause(): void;
|
|
65
66
|
abstract stop(): void;
|
|
66
67
|
/**
|
|
67
|
-
*
|
|
68
|
+
* Gets the current lifecycle state without changing playback.
|
|
68
69
|
*/
|
|
70
|
+
get state(): PlaybackState;
|
|
71
|
+
/** Checks if the audio is currently playing. */
|
|
69
72
|
get isPlaying(): boolean;
|
|
70
73
|
get isPaused(): boolean;
|
|
71
74
|
protected markPlaying(): void;
|
package/dist/basePlayback.mjs
CHANGED
|
@@ -86,6 +86,13 @@ var a = class extends r(/* @__PURE__ */ i(n)) {
|
|
|
86
86
|
if (!this._effectChain) throw Error("Cannot automate a filter before the playback effect chain is initialized");
|
|
87
87
|
this._effectChain.rampParam(e, t, n, r);
|
|
88
88
|
}
|
|
89
|
+
setPanType(e, t) {
|
|
90
|
+
let n = this.panner;
|
|
91
|
+
super.setPanType(e, t), !(this.panner === n || !this.panner || !this.source || !this.gainNode) && (this.setEffectChainEndpoints(this.source, this.panner), this.panner.connect(this.gainNode));
|
|
92
|
+
}
|
|
93
|
+
get state() {
|
|
94
|
+
return this._state;
|
|
95
|
+
}
|
|
89
96
|
get isPlaying() {
|
|
90
97
|
return this._state === "playing";
|
|
91
98
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"basePlayback.mjs","names":[],"sources":["../src/basePlayback.ts"],"sourcesContent":["import type { Bus } from \"./bus\";\nimport type { FadeType, PlayOptions } from \"./cacophony\";\nimport type { PlaybackContainer } from \"./container\";\nimport type { AudioNode, BaseContext, BiquadFilterNode, GainNode } from \"./context\";\nimport { EffectChain } from \"./effectChain\";\nimport type { CacophonyEffect } from \"./effects\";\nimport { TypedEventEmitter } from \"./eventEmitter\";\nimport type { PlaybackEvents } from \"./events\";\nimport { FilterManager } from \"./filters\";\nimport { PannerMixin } from \"./pannerMixin\";\nimport { VolumeMixin } from \"./volumeMixin\";\n\nexport type PlaybackState = \"unplayed\" | \"playing\" | \"paused\" | \"stopped\";\n\nexport abstract class BasePlayback extends /* @__PURE__ */ PannerMixin(/* @__PURE__ */ VolumeMixin(FilterManager)) {\n public source?: AudioNode;\n protected _effectChain?: EffectChain;\n private _effectContext?: BaseContext;\n /**\n * Per-playback send-gain allocations owned by the shared routing state\n * machine. Cleanup disconnects every allocation deterministically.\n */\n _sendGains: Map<Bus, GainNode> = new Map();\n protected _state: PlaybackState = \"unplayed\";\n public origin: PlaybackContainer;\n public eventEmitter: TypedEventEmitter<PlaybackEvents> = new TypedEventEmitter<PlaybackEvents>();\n\n constructor(origin: PlaybackContainer) {\n super();\n this.origin = origin;\n }\n\n protected setEffectChainEndpoints(input: AudioNode, output: AudioNode): void {\n this._effectContext =\n (input.context as BaseContext | undefined) ??\n (this.origin as PlaybackContainer & { context?: BaseContext }).context;\n if (this._effectChain) {\n this._effectChain.setEndpoints(input, output);\n return;\n }\n this._effectChain = new EffectChain(input, output, this.constructor.name);\n }\n\n /** Build and splice an effect into this live playback's pre-panner chain. */\n addEffect(effect: CacophonyEffect): Promise<AudioNode> {\n try {\n return this.materializeEffect(effect);\n } catch (error) {\n return Promise.reject(error);\n }\n }\n\n /** @internal Source preplay path that preserves synchronous native-build failures. */\n _addSourceEffect(effect: CacophonyEffect): Promise<AudioNode> {\n return this.materializeEffect(effect);\n }\n\n /** @internal Materialize a source-level biquad recipe and retain filter API compatibility. */\n _addFilterEffect(effect: CacophonyEffect): Promise<AudioNode> {\n return this.materializeEffect(effect, (handle) => {\n this._filters.push(handle as BiquadFilterNode);\n });\n }\n\n private materializeEffect(effect: CacophonyEffect, onBuilt?: (handle: AudioNode) => void): Promise<AudioNode> {\n if (!this._effectChain || !this._effectContext) {\n throw new Error(\"Cannot add an effect before the playback effect chain is initialized\");\n }\n const chain = this._effectChain;\n const reservation = chain.reserve();\n let built;\n try {\n built = effect.build(this._effectContext);\n } catch (error) {\n chain.cancel(reservation);\n throw error;\n }\n if (built instanceof Promise || (typeof built === \"object\" && built !== null && \"then\" in built)) {\n return Promise.resolve(built).then(\n (resolved) => {\n const handle = chain.resolve(reservation, resolved);\n onBuilt?.(handle);\n return handle;\n },\n (error) => {\n chain.cancel(reservation);\n throw error;\n },\n );\n }\n try {\n const handle = chain.resolve(reservation, built);\n onBuilt?.(handle);\n return Promise.resolve(handle);\n } catch (error) {\n chain.cancel(reservation);\n throw error;\n }\n }\n\n rampEffectParam(\n handle: AudioNode,\n paramName: string,\n value: number,\n options?: { duration?: number; type?: FadeType },\n ): void {\n if (!this._effectChain) {\n throw new Error(\"Cannot automate an effect before the playback effect chain is initialized\");\n }\n this._effectChain.rampParam(handle, paramName, value, options);\n }\n\n addFilter(filter: BiquadFilterNode): void {\n if (!this._effectChain) {\n throw new Error(\"Cannot add a filter before the playback effect chain is initialized\");\n }\n this._effectChain.add(filter, this._filters.length);\n this._filters.push(filter);\n }\n\n removeFilter(filter: BiquadFilterNode): void {\n if (!this._effectChain) {\n throw new Error(\"Cannot remove a filter before the playback effect chain is initialized\");\n }\n this._effectChain.remove(filter);\n this._filters = this._filters.filter((candidate) => candidate !== filter);\n }\n\n setFilterOrder(filters: readonly BiquadFilterNode[]): void {\n if (!this._effectChain) {\n throw new Error(\"Cannot reorder filters before the playback effect chain is initialized\");\n }\n const nonFilters = this._effectChain.nodes.filter((node) => !this._filters.includes(node as BiquadFilterNode));\n this._effectChain.setOrder([...filters, ...nonFilters]);\n this._filters = [...filters];\n }\n\n setFilterBypassed(filter: BiquadFilterNode, bypassed: boolean): void {\n if (!this._effectChain) {\n throw new Error(\"Cannot bypass a filter before the playback effect chain is initialized\");\n }\n this._effectChain.setBypassed(filter, bypassed);\n }\n\n isFilterBypassed(filter: BiquadFilterNode): boolean {\n return this._effectChain?.isBypassed(filter) ?? false;\n }\n\n rampFilterParam(\n filter: BiquadFilterNode,\n paramName: string,\n value: number,\n options?: { duration?: number; type?: FadeType },\n ): void {\n if (!this._effectChain) {\n throw new Error(\"Cannot automate a filter before the playback effect chain is initialized\");\n }\n this._effectChain.rampParam(filter, paramName, value, options);\n }\n\n abstract play(options?: PlayOptions): [this];\n abstract pause(): void;\n abstract stop(): void;\n\n /**\n * Checks if the audio is currently playing.\n */\n\n get isPlaying(): boolean {\n return this._state === \"playing\";\n }\n\n get isPaused(): boolean {\n return this._state === \"paused\";\n }\n\n protected markPlaying(): void {\n this._state = \"playing\";\n }\n\n protected markPaused(): void {\n this._state = \"paused\";\n }\n\n protected markStopped(): void {\n this._state = \"stopped\";\n }\n\n protected emitPlayStarted(isResume: boolean): void {\n this.markPlaying();\n this.emit(\"play\", this);\n if (isResume) {\n this.emit(\"resume\", undefined);\n }\n this.origin.cacophony?.emit(\"globalPlay\", {\n source: this.origin,\n timestamp: Date.now(),\n });\n }\n\n protected emitPaused(): void {\n this.markPaused();\n this.emit(\"pause\", undefined);\n this.origin.cacophony?.emit(\"globalPause\", {\n source: this.origin,\n timestamp: Date.now(),\n });\n }\n\n protected emitStopped(): void {\n this.markStopped();\n this.emit(\"stop\", undefined);\n this.origin.cacophony?.emit(\"globalStop\", {\n source: this.origin,\n timestamp: Date.now(),\n });\n }\n\n /**\n * Register event listener.\n * @returns Cleanup function\n */\n on<K extends keyof PlaybackEvents>(event: K, listener: (data: PlaybackEvents[K]) => void): () => void {\n return this.eventEmitter.on(event, listener);\n }\n\n /**\n * Remove event listener.\n */\n off<K extends keyof PlaybackEvents>(event: K, listener: (data: PlaybackEvents[K]) => void): void {\n this.eventEmitter.off(event, listener);\n }\n\n public emit<K extends keyof PlaybackEvents>(event: K, data: PlaybackEvents[K]): void {\n this.eventEmitter.emit(event, data);\n }\n\n public async emitAsync<K extends keyof PlaybackEvents>(event: K, data: PlaybackEvents[K]): Promise<void> {\n return this.eventEmitter.emitAsync(event, data);\n }\n\n /**\n * Fades the volume to a target value, emitting fadeStart and fadeEnd events.\n */\n fadeTo(\n value: number,\n duration: number,\n type: FadeType = \"linear\",\n options?: { startTime?: number; startValue?: number },\n ): Promise<void> {\n this.emit(\"fadeStart\", { target: value, duration, type });\n return super.fadeTo(value, duration, type, options).then(() => {\n this.emit(\"fadeEnd\", undefined);\n });\n }\n\n /**\n * Cancels any in-progress fade, emitting fadeCancel if a fade was active.\n */\n cancelFade(): void {\n const wasFading = this._isFading;\n super.cancelFade();\n if (wasFading) {\n this.emit(\"fadeCancel\", undefined);\n }\n }\n\n cleanup(): void {\n for (const sendGain of this._sendGains.values()) {\n try {\n sendGain.disconnect();\n } catch {\n // The node may already have been disconnected externally.\n }\n }\n this._sendGains.clear();\n this._effectChain?.destroy();\n this._effectChain = undefined;\n this._effectContext = undefined;\n this.eventEmitter.removeAllListeners();\n super.cleanup();\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"basePlayback.mjs","names":[],"sources":["../src/basePlayback.ts"],"sourcesContent":["import type { Bus } from \"./bus\";\nimport type { FadeType, PanType, PlayOptions } from \"./cacophony\";\nimport type { PlaybackContainer } from \"./container\";\nimport type { AudioNode, BaseContext, BiquadFilterNode, GainNode } from \"./context\";\nimport { EffectChain } from \"./effectChain\";\nimport type { CacophonyEffect } from \"./effects\";\nimport { TypedEventEmitter } from \"./eventEmitter\";\nimport type { PlaybackEvents } from \"./events\";\nimport { FilterManager } from \"./filters\";\nimport { PannerMixin } from \"./pannerMixin\";\nimport { VolumeMixin } from \"./volumeMixin\";\n\nexport type PlaybackState = \"unplayed\" | \"playing\" | \"paused\" | \"stopped\";\n\nexport abstract class BasePlayback extends /* @__PURE__ */ PannerMixin(/* @__PURE__ */ VolumeMixin(FilterManager)) {\n public source?: AudioNode;\n protected _effectChain?: EffectChain;\n private _effectContext?: BaseContext;\n /**\n * Per-playback send-gain allocations owned by the shared routing state\n * machine. Cleanup disconnects every allocation deterministically.\n */\n _sendGains: Map<Bus, GainNode> = new Map();\n protected _state: PlaybackState = \"unplayed\";\n public origin: PlaybackContainer;\n public eventEmitter: TypedEventEmitter<PlaybackEvents> = new TypedEventEmitter<PlaybackEvents>();\n\n constructor(origin: PlaybackContainer) {\n super();\n this.origin = origin;\n }\n\n protected setEffectChainEndpoints(input: AudioNode, output: AudioNode): void {\n this._effectContext =\n (input.context as BaseContext | undefined) ??\n (this.origin as PlaybackContainer & { context?: BaseContext }).context;\n if (this._effectChain) {\n this._effectChain.setEndpoints(input, output);\n return;\n }\n this._effectChain = new EffectChain(input, output, this.constructor.name);\n }\n\n /** Build and splice an effect into this live playback's pre-panner chain. */\n addEffect(effect: CacophonyEffect): Promise<AudioNode> {\n try {\n return this.materializeEffect(effect);\n } catch (error) {\n return Promise.reject(error);\n }\n }\n\n /** @internal Source preplay path that preserves synchronous native-build failures. */\n _addSourceEffect(effect: CacophonyEffect): Promise<AudioNode> {\n return this.materializeEffect(effect);\n }\n\n /** @internal Materialize a source-level biquad recipe and retain filter API compatibility. */\n _addFilterEffect(effect: CacophonyEffect): Promise<AudioNode> {\n return this.materializeEffect(effect, (handle) => {\n this._filters.push(handle as BiquadFilterNode);\n });\n }\n\n private materializeEffect(effect: CacophonyEffect, onBuilt?: (handle: AudioNode) => void): Promise<AudioNode> {\n if (!this._effectChain || !this._effectContext) {\n throw new Error(\"Cannot add an effect before the playback effect chain is initialized\");\n }\n const chain = this._effectChain;\n const reservation = chain.reserve();\n let built;\n try {\n built = effect.build(this._effectContext);\n } catch (error) {\n chain.cancel(reservation);\n throw error;\n }\n if (built instanceof Promise || (typeof built === \"object\" && built !== null && \"then\" in built)) {\n return Promise.resolve(built).then(\n (resolved) => {\n const handle = chain.resolve(reservation, resolved);\n onBuilt?.(handle);\n return handle;\n },\n (error) => {\n chain.cancel(reservation);\n throw error;\n },\n );\n }\n try {\n const handle = chain.resolve(reservation, built);\n onBuilt?.(handle);\n return Promise.resolve(handle);\n } catch (error) {\n chain.cancel(reservation);\n throw error;\n }\n }\n\n rampEffectParam(\n handle: AudioNode,\n paramName: string,\n value: number,\n options?: { duration?: number; type?: FadeType },\n ): void {\n if (!this._effectChain) {\n throw new Error(\"Cannot automate an effect before the playback effect chain is initialized\");\n }\n this._effectChain.rampParam(handle, paramName, value, options);\n }\n\n addFilter(filter: BiquadFilterNode): void {\n if (!this._effectChain) {\n throw new Error(\"Cannot add a filter before the playback effect chain is initialized\");\n }\n this._effectChain.add(filter, this._filters.length);\n this._filters.push(filter);\n }\n\n removeFilter(filter: BiquadFilterNode): void {\n if (!this._effectChain) {\n throw new Error(\"Cannot remove a filter before the playback effect chain is initialized\");\n }\n this._effectChain.remove(filter);\n this._filters = this._filters.filter((candidate) => candidate !== filter);\n }\n\n setFilterOrder(filters: readonly BiquadFilterNode[]): void {\n if (!this._effectChain) {\n throw new Error(\"Cannot reorder filters before the playback effect chain is initialized\");\n }\n const nonFilters = this._effectChain.nodes.filter((node) => !this._filters.includes(node as BiquadFilterNode));\n this._effectChain.setOrder([...filters, ...nonFilters]);\n this._filters = [...filters];\n }\n\n setFilterBypassed(filter: BiquadFilterNode, bypassed: boolean): void {\n if (!this._effectChain) {\n throw new Error(\"Cannot bypass a filter before the playback effect chain is initialized\");\n }\n this._effectChain.setBypassed(filter, bypassed);\n }\n\n isFilterBypassed(filter: BiquadFilterNode): boolean {\n return this._effectChain?.isBypassed(filter) ?? false;\n }\n\n rampFilterParam(\n filter: BiquadFilterNode,\n paramName: string,\n value: number,\n options?: { duration?: number; type?: FadeType },\n ): void {\n if (!this._effectChain) {\n throw new Error(\"Cannot automate a filter before the playback effect chain is initialized\");\n }\n this._effectChain.rampParam(filter, paramName, value, options);\n }\n\n override setPanType(panType: PanType, audioContext: BaseContext): void {\n const previousPanner = this.panner;\n super.setPanType(panType, audioContext);\n if (this.panner === previousPanner || !this.panner || !this.source || !this.gainNode) return;\n this.setEffectChainEndpoints(this.source, this.panner);\n this.panner.connect(this.gainNode);\n }\n\n abstract play(options?: PlayOptions): [this];\n abstract pause(): void;\n abstract stop(): void;\n\n /**\n * Gets the current lifecycle state without changing playback.\n */\n\n get state(): PlaybackState {\n return this._state;\n }\n\n /** Checks if the audio is currently playing. */\n get isPlaying(): boolean {\n return this._state === \"playing\";\n }\n\n get isPaused(): boolean {\n return this._state === \"paused\";\n }\n\n protected markPlaying(): void {\n this._state = \"playing\";\n }\n\n protected markPaused(): void {\n this._state = \"paused\";\n }\n\n protected markStopped(): void {\n this._state = \"stopped\";\n }\n\n protected emitPlayStarted(isResume: boolean): void {\n this.markPlaying();\n this.emit(\"play\", this);\n if (isResume) {\n this.emit(\"resume\", undefined);\n }\n this.origin.cacophony?.emit(\"globalPlay\", {\n source: this.origin,\n timestamp: Date.now(),\n });\n }\n\n protected emitPaused(): void {\n this.markPaused();\n this.emit(\"pause\", undefined);\n this.origin.cacophony?.emit(\"globalPause\", {\n source: this.origin,\n timestamp: Date.now(),\n });\n }\n\n protected emitStopped(): void {\n this.markStopped();\n this.emit(\"stop\", undefined);\n this.origin.cacophony?.emit(\"globalStop\", {\n source: this.origin,\n timestamp: Date.now(),\n });\n }\n\n /**\n * Register event listener.\n * @returns Cleanup function\n */\n on<K extends keyof PlaybackEvents>(event: K, listener: (data: PlaybackEvents[K]) => void): () => void {\n return this.eventEmitter.on(event, listener);\n }\n\n /**\n * Remove event listener.\n */\n off<K extends keyof PlaybackEvents>(event: K, listener: (data: PlaybackEvents[K]) => void): void {\n this.eventEmitter.off(event, listener);\n }\n\n public emit<K extends keyof PlaybackEvents>(event: K, data: PlaybackEvents[K]): void {\n this.eventEmitter.emit(event, data);\n }\n\n public async emitAsync<K extends keyof PlaybackEvents>(event: K, data: PlaybackEvents[K]): Promise<void> {\n return this.eventEmitter.emitAsync(event, data);\n }\n\n /**\n * Fades the volume to a target value, emitting fadeStart and fadeEnd events.\n */\n fadeTo(\n value: number,\n duration: number,\n type: FadeType = \"linear\",\n options?: { startTime?: number; startValue?: number },\n ): Promise<void> {\n this.emit(\"fadeStart\", { target: value, duration, type });\n return super.fadeTo(value, duration, type, options).then(() => {\n this.emit(\"fadeEnd\", undefined);\n });\n }\n\n /**\n * Cancels any in-progress fade, emitting fadeCancel if a fade was active.\n */\n cancelFade(): void {\n const wasFading = this._isFading;\n super.cancelFade();\n if (wasFading) {\n this.emit(\"fadeCancel\", undefined);\n }\n }\n\n cleanup(): void {\n for (const sendGain of this._sendGains.values()) {\n try {\n sendGain.disconnect();\n } catch {\n // The node may already have been disconnected externally.\n }\n }\n this._sendGains.clear();\n this._effectChain?.destroy();\n this._effectChain = undefined;\n this._effectContext = undefined;\n this.eventEmitter.removeAllListeners();\n super.cleanup();\n }\n}\n"],"mappings":"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"}
|
package/dist/cache.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
function e(e){return e instanceof Error?e:Error(String(e))}function t(e){return`${e}:meta`}var n=class{maxBytes;cachedBytes=0;cache;estimateBytes;constructor(e,t){this.maxBytes=e,this.estimateBytes=t,this.cache=new Map}get(e){let t=this.cache.get(e);if(t!==void 0)return this.cache.delete(e),this.cache.set(e,t),t.value}set(e,t){let n=this.estimateBytes(t);if(this.cache.has(e)&&(this.cachedBytes-=this.cache.get(e)?.bytes??0,this.cache.delete(e)),!(n>this.maxBytes)){for(;this.cachedBytes+n>this.maxBytes;){let e=this.cache.keys().next();if(e.done)break;let t=e.value,n=this.cache.get(t);this.cachedBytes-=n?.bytes??0,this.cache.delete(t)}this.cache.set(e,{value:t,bytes:n}),this.cachedBytes+=n}}},r=64*1024*1024,i=4;function a(e){return e.length*e.numberOfChannels*i}function o(e){if(!e)return null;let t=e.match(/max-age\s*=\s*(?:"(\d+)"|(\d+))/i);if(!t)return null;let n=t[1]??t[2];if(n===void 0)return null;let r=Number.parseInt(n,10);return Number.isNaN(r)?null:r}function s(e){return e?/(?:^|,)\s*(no-cache|no-store|must-revalidate)\s*(?:,|$)/i.test(e):!1}function c(e){return e instanceof DOMException&&e.name===`AbortError`?`abort`:e instanceof Error?`network`:`unknown`}function l(e){let t=e.indexOf(`,`);if(t<0)return null;let n=e.slice(5,t),r=e.slice(t+1),i=/;base64$/i.test(n),a=(i?n.replace(/;base64$/i,``):n)||`text/plain`;try{if(i){let e=atob(r),t=new Uint8Array(e.length);for(let n=0;n<e.length;n++)t[n]=e.charCodeAt(n);return{mime:a,bytes:t}}let e=decodeURIComponent(r),t=new Uint8Array(e.length);for(let n=0;n<e.length;n++)t[n]=e.charCodeAt(n);return{mime:a,bytes:t}}catch{return null}}var u=class i{pendingRequests=new WeakMap;pendingCallbacks=new WeakMap;decodedBuffers=new WeakMap;static cacheExpirationTime=1440*60*1e3;static setCacheExpirationTime(e){i.cacheExpirationTime=e}static isCacheApiAvailable(){return typeof caches<`u`&&caches!=null}static async openCache(){try{return await caches.open(`audio-cache`)}catch(e){throw console.error(`Failed to open cache:`,e),e}}static callAllCallbacks(e,t,n,r){let i=e?.get(t);i&&i.forEach(e=>{let t=e[n];if(t)try{t(r)}catch(e){console.error(`Error in ${String(n)} callback:`,e)}})}async getOrCreatePendingRequest(e,t,n,r,i){if(r?.aborted)throw new DOMException(`Operation was aborted`,`AbortError`);let a=this.pendingRequests.get(e);a===void 0&&(a=new Map,this.pendingRequests.set(e,a));let o=this.pendingCallbacks.get(e);if(o===void 0&&(o=new Map,this.pendingCallbacks.set(e,o)),i){let e=o.get(t)||[];e.push(i),o.set(t,e)}let s=a.get(t);if(!s){let e=new AbortController;s={promise:(async()=>{try{let t=await n(e.signal);if(t===void 0)throw Error(`Failed to create audio buffer.`);return t}finally{a.get(t)===s&&(a.delete(t),o.delete(t))}})(),controller:e,callers:0},a.set(t,s)}return s.callers++,new Promise((e,n)=>{let c=!1,l=e=>{if(!c){if(c=!0,r?.removeEventListener(`abort`,u),s.callers--,e&&i){let e=o.get(t),n=e?.indexOf(i)??-1;n>=0&&e?.splice(n,1)}e&&s.callers===0&&a.get(t)===s&&(a.delete(t),o.delete(t),s.controller.abort())}},u=()=>{l(!0),n(new DOMException(`Operation was aborted`,`AbortError`))};r?.addEventListener(`abort`,u,{once:!0}),s.promise.then(t=>{c||(l(!1),e(t))},e=>{c||(l(!1),n(e))})})}static async updateMetadata(e,n,r){let i={url:n,timestamp:Date.now(),...r};await e.put(t(n),new Response(JSON.stringify(i),{headers:{"Content-Type":`application/json`}}))}static async getBufferFromCache(e,t){try{let n=await t.match(e);return n?.ok?await n.arrayBuffer():null}catch(e){return console.error(`Failed to get data from cache:`,e),null}}async fetchAndCacheBuffer(e,t,n,r,a,o){let s=r?.etag,c=r?.lastModified,l=new Headers;s&&l.append(`If-None-Match`,s),c&&l.append(`If-Modified-Since`,c),console.debug(`[AudioCache] Fetching ${t}`,{headers:Object.fromEntries(l.entries()),hasEtag:!!s,hasLastModified:!!c});let u=await fetch(t,{headers:l,signal:a});if(console.debug(`[AudioCache] Response ${t}`,{status:u.status,statusText:u.statusText,etag:u.headers?.get(`ETag`),lastModified:u.headers?.get(`Last-Modified`),cacheControl:u.headers?.get(`Cache-Control`)}),u.status===304){let r=await n.match(t);if(r){if(o?.onCacheHit){let e={url:t,cacheType:`conditional`,timestamp:Date.now()};o.onCacheHit(e)}let e=Date.now(),a=u.headers?.get(`Cache-Control`);return await i.updateMetadata(n,t,{timestamp:e,etag:s,lastModified:c,...a?{cacheControl:a}:{}}),await r.arrayBuffer()}else{console.warn(`Cache inconsistency detected for ${t}: 304 response but no cached body. Re-fetching.`);let r=await fetch(t,{signal:a});if(r.status===200){let o=r.clone(),s=r.headers.get(`ETag`),c=r.headers.get(`Last-Modified`),l=r.headers.get(`Cache-Control`);if(await i.writeBufferAndMetadata(n,t,o,{timestamp:Date.now(),etag:s||void 0,lastModified:c||void 0,cacheControl:l||void 0}),r.body){let{stream:n,total:o}=this.createProgressTrackingStream(e,r,t,a);return await i.collectStreamToArrayBuffer(n,o||void 0)}else return await r.arrayBuffer()}else throw Error(`Failed to fetch resource after cache inconsistency: ${r.status} ${r.statusText}`)}}if(u.status===200){let e=u.clone(),r=u.headers.get(`ETag`),a=u.headers.get(`Last-Modified`),o=u.headers.get(`Cache-Control`);await i.writeBufferAndMetadata(n,t,e,{timestamp:Date.now(),etag:r||void 0,lastModified:a||void 0,cacheControl:o||void 0})}if(a?.aborted)throw new DOMException(`Operation was aborted`,`AbortError`);if(u.body){let{stream:n,total:r}=this.createProgressTrackingStream(e,u,t,a);return await i.collectStreamToArrayBuffer(n,r||void 0)}else return await u.arrayBuffer()}static async writeBufferAndMetadata(e,n,r,a){try{await Promise.all([e.put(n,r),i.updateMetadata(e,n,a)])}catch(r){throw await Promise.allSettled([e.delete(n),e.delete(t(n))]),r}}createProgressTrackingStream(e,t,n,r){let a=t.headers.get(`content-length`),o=a?Number.parseInt(a,10):null,s=0;if(!t.body)throw Error(`Response body is null`);let c=t.body.getReader(),l=this.pendingCallbacks.get(e);return{stream:new ReadableStream({async start(e){try{for(;;){if(r?.aborted)throw new DOMException(`Operation was aborted`,`AbortError`);let{done:t,value:a}=await c.read();if(t){i.callAllCallbacks(l,n,`onLoadingProgress`,{url:n,loaded:s,total:o,progress:o?1:-1,timestamp:Date.now()}),e.close();return}if(a){s+=a.byteLength;let t=o?s/o:-1;i.callAllCallbacks(l,n,`onLoadingProgress`,{url:n,loaded:s,total:o,progress:t,timestamp:Date.now()}),e.enqueue(a)}}}catch(t){e.error(t)}finally{c.releaseLock()}},cancel(e){return c.cancel(e)}}),total:o}}static async collectStreamToArrayBuffer(e,t){let n=e.getReader();try{if(t!==void 0&&t>0){let e=new ArrayBuffer(t),r=new Uint8Array(e),i=0;for(;;){let{done:e,value:t}=await n.read();if(e)break;t&&(r.set(t,i),i+=t.byteLength)}return e}else{let e=new Uint8Array(8192),t=0;for(;;){let{done:r,value:i}=await n.read();if(r)break;if(i){if(t+i.byteLength>e.length){let n=Math.max(e.length*2,t+i.byteLength),r=new Uint8Array(n);r.set(e.subarray(0,t)),e=r}e.set(i,t),t+=i.byteLength}}return e.slice(0,t).buffer}}finally{n.releaseLock()}}async fetchAndDecodeWithoutCache(e,t,n){let r=await fetch(t,{signal:n});if(r.status!==200&&!r.ok)throw Error(`Failed to fetch resource: ${r.status} ${r.statusText}`);if(n?.aborted)throw new DOMException(`Operation was aborted`,`AbortError`);if(r.body){let{stream:a,total:o}=this.createProgressTrackingStream(e,r,t,n);return await i.collectStreamToArrayBuffer(a,o||void 0)}return await r.arrayBuffer()}async fetchDecodeAndCache(t,n,r,a,o,s=!1){try{let s=await a(),c;try{c=await i.decodeAudioData(t,s)}catch(t){throw o?.onLoadingError?.({url:n,error:e(t),errorType:`decode`,timestamp:Date.now()}),t}return o?.onLoadingComplete?.({url:n,duration:c.duration,size:s.byteLength,timestamp:Date.now()}),r.set(n,c),c}catch(t){throw o?.onLoadingError?.({url:n,error:e(t),errorType:c(t),timestamp:Date.now()}),s&&o?.onCacheError?.({url:n,error:e(t),operation:`get`,timestamp:Date.now()}),t}}static async decodeAudioData(e,t){try{return await e.decodeAudioData(t)}catch(e){throw console.error(`Failed to decode audio data:`,e),e}}static async getMetadataFromCache(e,n){try{let r=await n.match(t(e));return r?.ok?await r.json():null}catch(e){return console.error(`Failed to get metadata from cache:`,e),null}}async getAudioBuffer(e,t,c,u){let d=this.decodedBuffers.get(e);d===void 0&&(d=new n(r,a),this.decodedBuffers.set(e,d)),u?.onLoadingStart&&u.onLoadingStart({url:t,timestamp:Date.now()});let f=d.get(t);if(f!==void 0)return u?.onCacheHit&&u.onCacheHit({url:t,cacheType:`memory`,timestamp:Date.now()}),f;if(t.startsWith(`data:`)){let n=l(t);if(n===null){let e=Error(`Malformed data: URL: ${t.slice(0,32)}…`);throw u?.onLoadingError&&u.onLoadingError({url:t,error:e,errorType:`decode`,timestamp:Date.now()}),e}let r=await i.decodeAudioData(e,n.bytes.buffer);return d.set(t,r),r}if(!i.isCacheApiAvailable())return this.getOrCreatePendingRequest(e,t,async n=>(u?.onCacheMiss&&u.onCacheMiss({url:t,reason:`not-found`,timestamp:Date.now()}),await this.fetchDecodeAndCache(e,t,d,()=>this.fetchAndDecodeWithoutCache(e,t,n),u)),c,{onLoadingProgress:u?.onLoadingProgress});let p=await i.openCache(),m=await i.getMetadataFromCache(t,p),h=(()=>{if(!m||s(m.cacheControl))return!0;let e=o(m.cacheControl);if(e!==null){let t=(Date.now()-m.timestamp)/1e3;if(e>0&&t<e)return!1}return m.etag||m.lastModified?!0:Date.now()-m.timestamp>i.cacheExpirationTime})();return this.getOrCreatePendingRequest(e,t,async n=>{if(h)return u?.onCacheMiss&&u.onCacheMiss({url:t,reason:m?`expired`:`not-found`,timestamp:Date.now()}),await this.fetchDecodeAndCache(e,t,d,()=>this.fetchAndCacheBuffer(e,t,p,{etag:m?.etag,lastModified:m?.lastModified},n,{onCacheHit:u?.onCacheHit}),u,!0);{let r=await i.getBufferFromCache(t,p);if(r){u?.onCacheHit&&u.onCacheHit({url:t,cacheType:`browser`,timestamp:Date.now()});let n=await i.decodeAudioData(e,r);return d.set(t,n),n}else return u?.onCacheError&&u.onCacheError({url:t,error:Error(`Cache inconsistency: metadata exists but body is missing`),operation:`get`,timestamp:Date.now()}),await this.fetchDecodeAndCache(e,t,d,()=>this.fetchAndCacheBuffer(e,t,p,{etag:m?.etag,lastModified:m?.lastModified},n,{onCacheHit:u?.onCacheHit}),u)}},c,{onLoadingProgress:u?.onLoadingProgress})}clearMemoryCache(){this.decodedBuffers=new WeakMap,this.pendingRequests=new WeakMap,this.pendingCallbacks=new WeakMap}};exports.AudioCache=u;
|
|
1
|
+
const e=require("./_virtual/_rolldown/runtime.cjs");let t=require("http-cache-semantics");t=e.__toESM(t,1);var n=`audio-cache-v2`,r=`x-cacophony-cache-policy`,i=64*1024*1024,a={accept:`*/*`};function o(e){return e instanceof Error?e:Error(String(e))}function s(e){if(e?.aborted)throw new DOMException(`Operation was aborted`,`AbortError`)}var c=class{maxBytes;estimateBytes;cachedBytes=0;cache=new Map;constructor(e,t){this.maxBytes=e,this.estimateBytes=t}get(e){let t=this.cache.get(e);if(t)return this.cache.delete(e),this.cache.set(e,t),t.value}delete(e){this.cachedBytes-=this.cache.get(e)?.bytes??0,this.cache.delete(e)}set(e,t){this.delete(e);let n=this.estimateBytes(t);if(!(n>this.maxBytes)){for(;this.cachedBytes+n>this.maxBytes;){let e=this.cache.keys().next();if(e.done)break;this.delete(e.value)}this.cache.set(e,{value:t,bytes:n}),this.cachedBytes+=n}}};function l(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function u(e){return l(e)&&Object.values(e).every(e=>typeof e==`string`)}function d(e){let t={...e};return t[`cache-control`]&&=t[`cache-control`].replace(/("(?:\\.|[^"\\])*")|(^|,)(\s*[^=,\s]+)/g,(e,t,n,r)=>t??n+r.toLowerCase()),t}function f(e){let t=new Headers;for(let[n,r]of Object.entries(e))r!==void 0&&t.set(n,Array.isArray(r)?r.join(`, `):r);return t}var p=class e{static cacheExpirationTime=1440*60*1e3;static cleanedStorage=new WeakSet;decodedBuffers=new WeakMap;pendingRequests=new WeakMap;static setCacheExpirationTime(t){if(!Number.isFinite(t)||t<0)throw RangeError(`Cache expiration must be finite and non-negative`);e.cacheExpirationTime=t}static createEntry(n,r,i,a=!1){let o=d(r);[`cache-control`,`expires`,`pragma`,`etag`,`last-modified`].some(e=>e in r)||(o[`cache-control`]=`max-age=${Math.floor(e.cacheExpirationTime/1e3)}`);let s=Object.fromEntries(n.headers),c=new t.default({url:n.url,method:`GET`,headers:s},{status:i,headers:o},{shared:!1,cacheHeuristic:0,immutableMinTimeToLive:0});return{policy:c,status:i,headers:r,metadata:JSON.stringify({version:2,status:i,cors:a,url:n.url,time:c.toObject().t,requestHeaders:s,policyHeaders:o,headers:r}),cors:a}}static readEntry(e,n){let i=e.headers.get(r);if(i)try{let r=JSON.parse(i);if(!l(r)||r.version!==2||r.url!==n.url||typeof r.time!=`number`||!Number.isFinite(r.time)||r.time>Date.now()||!u(r.requestHeaders)||!u(r.policyHeaders)||!u(r.headers))return;let a=new t.default({url:n.url,method:`GET`,headers:r.requestHeaders},{status:e.status,headers:d(r.policyHeaders)},{shared:!1,cacheHeuristic:0,immutableMinTimeToLive:0}).toObject();return a.t=r.time,{policy:t.default.fromObject(a),status:e.status,headers:r.headers,metadata:i,cors:r.cors===!0}}catch{return}}static canRetain(e){let t=e.headers.vary?.split(`,`).map(e=>e.trim().toLowerCase()).filter(Boolean)??[];return e.policy.storable()&&t.every(e=>e===`accept`)}getMemory(e){let t=this.decodedBuffers.get(e);return t||(t=new c(i,({buffer:e})=>e.length*e.numberOfChannels*4),this.decodedBuffers.set(e,t)),t}join(e,t,n,r,i){s(n);let a=this.pendingRequests.get(e);a||(a=new Map,this.pendingRequests.set(e,a));let o={callbacks:r},c=a.get(t);if(!c){let e=new AbortController,n=new Set,r=e=>{for(let t of n)try{t.callbacks&&e(t.callbacks)}catch{}};c={controller:e,subscribers:n,promise:Promise.resolve().then(()=>i(e.signal,r))},a.set(t,c)}let l=c,u=a;return l.subscribers.add(o),new Promise((e,i)=>{let a=!1,s=()=>{a=!0,n?.removeEventListener(`abort`,c),l.subscribers.delete(o),l.subscribers.size===0&&u.get(t)===l&&u.delete(t)},c=()=>{if(a)return;s(),l.subscribers.size===0&&l.controller.abort();let e=new DOMException(`Operation was aborted`,`AbortError`);try{r?.onLoadingError?.({url:t,error:e,errorType:`abort`,timestamp:Date.now()})}catch{}i(e)};n?.addEventListener(`abort`,c,{once:!0}),l.promise.then(t=>{a||(s(),e(t))},e=>{a||(s(),i(e))})})}static cacheError(e,t,n,r){e(e=>e.onCacheError?.({url:t,error:o(n),operation:r,timestamp:Date.now()}))}static async openCache(t,r){if(!(typeof caches>`u`||caches===null)){if(!e.cleanedStorage.has(caches)){e.cleanedStorage.add(caches);try{await caches.delete(`audio-cache`)}catch(n){e.cacheError(t,r,n,`delete`)}}try{return await caches.open(n)}catch(n){e.cacheError(t,r,n,`get`);return}}}static async deleteResponse(t,n,r,i){if(t)try{await t.delete(n,{ignoreVary:!0})}catch(t){e.cacheError(r,i,t,`delete`)}}static async storeResponse(t,n,i,a,o,s){if(!t)return;let c=new Headers(a.headers);c.delete(`content-encoding`),c.set(`content-length`,String(i.byteLength)),c.set(r,a.metadata);try{await t.put(n,new Response(i,{status:a.status,headers:c}))}catch(r){await e.deleteResponse(t,n,o,s),e.cacheError(o,s,r,`set`)}}static async readBytes(e,t,n,r){if(!e.body)return e.arrayBuffer();let i=Number(e.headers.get(`content-length`)),a=Number.isFinite(i)&&i>0&&!e.headers.has(`content-encoding`)?i:null,o=e.body.getReader(),c=new Uint8Array(a??8192),l=0,u=()=>{o.cancel().catch(()=>{})};t.addEventListener(`abort`,u,{once:!0});let d=e=>{let t=Date.now();n(n=>n.onLoadingProgress?.({url:r,loaded:l,total:a,progress:a?e?1:Math.min(1,l/a):-1,timestamp:t}))};try{for(;;){s(t);let e=await o.read();if(s(t),e.done)break;if(l+e.value.byteLength>c.length){let t=new Uint8Array(Math.max(c.length*2,l+e.value.byteLength));t.set(c.subarray(0,l)),c=t}c.set(e.value,l),l+=e.value.byteLength,d(!1)}return d(!0),c.slice(0,l).buffer}finally{t.removeEventListener(`abort`,u),o.releaseLock()}}async loadHttp(t,n,i,c,l){let u=`network`;try{s(c);let o=new Request(n,{headers:a}),p={url:o.url,method:`GET`,headers:Object.fromEntries(o.headers)},m=i.get(n);if(m?.http&&e.canRetain(m.http)&&m.http.policy.satisfiesWithoutRevalidation(p))return l(e=>e.onCacheHit?.({url:n,cacheType:`memory`,timestamp:Date.now()})),m.buffer;let h=await e.openCache(l,n),g,_=m?.http;if(h)try{if(g=await h.match(o),g){let t=e.readEntry(g,o);!t||!e.canRetain(t)?(g=void 0,await e.deleteResponse(h,o,l,n)):_?.metadata!==t.metadata&&(_=t,m=void 0)}}catch(t){e.cacheError(l,n,t,`get`)}s(c);let v=async t=>{try{return await t.arrayBuffer()}catch(t){e.cacheError(l,n,t,`get`),i.delete(n),m=void 0,_=void 0,g=void 0,await e.deleteResponse(h,o,l,n);return}};if(_&&g&&_.policy.satisfiesWithoutRevalidation(p)){let r=await v(g);if(s(c),r){u=`decode`;let a;try{a=await t.decodeAudioData(r)}catch(t){throw await e.deleteResponse(h,o,l,n),t}return s(c),i.set(n,{buffer:a,http:_}),l(e=>e.onCacheHit?.({url:n,cacheType:`browser`,timestamp:Date.now()})),a}}l(e=>e.onCacheMiss?.({url:n,reason:_?`expired`:`not-found`,timestamp:Date.now()}));let y=_?.cors||typeof location<`u`&&new URL(o.url).origin!==location.origin,b=_&&!y?f(_.policy.revalidationHeaders(p)):o.headers,x=await fetch(n,{headers:b,signal:c,cache:_?`no-cache`:`default`});s(c);let S,C,w=!1;if(x.status===304&&_&&(g||m)){let t=_.policy.revalidatedPolicy(p,{status:304,headers:d(Object.fromEntries(x.headers))});if(t.matches&&!t.modified){let t={..._.headers};x.headers.forEach((e,n)=>{[`content-length`,`content-encoding`,r].includes(n)||(t[n]=e)}),t.date=x.headers.get(`date`)??new Date().toUTCString(),t.age=x.headers.get(`age`)??`0`,_=e.createEntry(o,t,_.status,_.cors),S=g?await v(g):void 0,C=m?.buffer,w=S!==void 0||C!==void 0}}if(x.status===304&&!w&&(s(c),x=await fetch(n,{headers:o.headers,signal:c,cache:`reload`}),s(c)),!w){if(!x.ok||[204,205,206].includes(x.status))throw Error(`Failed to fetch resource: ${x.status} ${x.statusText}`);let t=Object.fromEntries(x.headers);delete t[r],_=e.createEntry(o,t,x.status,x.type===`cors`),S=await e.readBytes(x,c,l,n)}if(s(c),!_)throw Error(`Missing response policy`);let T=e.canRetain(_);if(i.delete(n),T||await e.deleteResponse(h,o,l,n),C)T&&S&&await e.storeResponse(h,o,S,_,l,n);else{if(!S)throw Error(`Missing response body`);let r=S.byteLength,i=T&&h?S.slice(0):void 0;u=`decode`;try{C=await t.decodeAudioData(S)}catch(e){throw console.error(`Failed to decode audio data:`,e),e}s(c),i&&await e.storeResponse(h,o,i,_,l,n);let a=C;l(e=>e.onLoadingComplete?.({url:n,duration:a.duration,size:r,timestamp:Date.now()}))}return s(c),T&&i.set(n,{buffer:C,http:_}),w&&l(e=>e.onCacheHit?.({url:n,cacheType:`conditional`,timestamp:Date.now()})),C}catch(e){throw l(t=>t.onLoadingError?.({url:n,error:o(e),errorType:u,timestamp:Date.now()})),e}}async getAudioBuffer(e,t,n,r){s(n),r?.onLoadingStart?.({url:t,timestamp:Date.now()});let i=this.getMemory(e);if(t.startsWith(`data:`)){let a=i.get(t);if(a)return r?.onCacheHit?.({url:t,cacheType:`memory`,timestamp:Date.now()}),a.buffer;try{let r=t.indexOf(`,`);if(r<0)throw Error(`Malformed data URL`);let a=t.slice(5,r),o=t.slice(r+1),c=/;base64$/i.test(a)?atob(o):decodeURIComponent(o),l=Uint8Array.from(c,e=>e.charCodeAt(0)),u=await e.decodeAudioData(l.buffer);return s(n),i.set(t,{buffer:u}),u}catch(e){throw r?.onLoadingError?.({url:t,error:o(e),errorType:n?.aborted?`abort`:`decode`,timestamp:Date.now()}),e}}return this.join(e,t,n,r,(n,r)=>this.loadHttp(e,t,i,n,r))}clearMemoryCache(){this.decodedBuffers=new WeakMap,this.pendingRequests=new WeakMap}};exports.AudioCache=p;
|
|
2
2
|
//# sourceMappingURL=cache.cjs.map
|