cacophony 0.31.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/README.md +64 -5
  2. package/THIRD_PARTY_NOTICES.md +15 -0
  3. package/dist/basePlayback.cjs +1 -1
  4. package/dist/basePlayback.cjs.map +1 -1
  5. package/dist/basePlayback.d.ts +8 -5
  6. package/dist/basePlayback.mjs +7 -0
  7. package/dist/basePlayback.mjs.map +1 -1
  8. package/dist/cache.cjs +1 -1
  9. package/dist/cache.cjs.map +1 -1
  10. package/dist/cache.d.ts +20 -106
  11. package/dist/cache.mjs +345 -372
  12. package/dist/cache.mjs.map +1 -1
  13. package/dist/cacophony.cjs.map +1 -1
  14. package/dist/cacophony.d.ts +14 -1
  15. package/dist/cacophony.mjs.map +1 -1
  16. package/dist/container.cjs +1 -1
  17. package/dist/container.cjs.map +1 -1
  18. package/dist/container.mjs +13 -2
  19. package/dist/container.mjs.map +1 -1
  20. package/dist/group.cjs +1 -1
  21. package/dist/group.cjs.map +1 -1
  22. package/dist/group.mjs +1 -0
  23. package/dist/group.mjs.map +1 -1
  24. package/dist/mediaStream.cjs +1 -1
  25. package/dist/mediaStream.cjs.map +1 -1
  26. package/dist/mediaStream.d.ts +1 -0
  27. package/dist/mediaStream.mjs +17 -16
  28. package/dist/mediaStream.mjs.map +1 -1
  29. package/dist/pcmStream.cjs +1 -1
  30. package/dist/pcmStream.cjs.map +1 -1
  31. package/dist/pcmStream.d.ts +1 -0
  32. package/dist/pcmStream.mjs +13 -12
  33. package/dist/pcmStream.mjs.map +1 -1
  34. package/dist/playOptions.cjs +2 -0
  35. package/dist/playOptions.cjs.map +1 -0
  36. package/dist/playOptions.d.ts +9 -0
  37. package/dist/playOptions.mjs +56 -0
  38. package/dist/playOptions.mjs.map +1 -0
  39. package/dist/playback.cjs +1 -1
  40. package/dist/playback.cjs.map +1 -1
  41. package/dist/playback.d.ts +2 -2
  42. package/dist/playback.mjs +32 -20
  43. package/dist/playback.mjs.map +1 -1
  44. package/dist/sound.cjs +1 -1
  45. package/dist/sound.cjs.map +1 -1
  46. package/dist/sound.d.ts +9 -6
  47. package/dist/sound.mjs +74 -59
  48. package/dist/sound.mjs.map +1 -1
  49. package/dist/synth.cjs +1 -1
  50. package/dist/synth.cjs.map +1 -1
  51. package/dist/synth.mjs +17 -13
  52. package/dist/synth.mjs.map +1 -1
  53. package/dist/synthPlayback.cjs +1 -1
  54. package/dist/synthPlayback.cjs.map +1 -1
  55. package/dist/synthPlayback.mjs +9 -8
  56. package/dist/synthPlayback.mjs.map +1 -1
  57. package/docs/assets/hierarchy.js +1 -1
  58. package/docs/assets/search.js +1 -1
  59. package/docs/classes/AudioCache.html +20 -51
  60. package/docs/classes/AudioSprite.html +7 -7
  61. package/docs/classes/BarberpoleEffect.html +7 -7
  62. package/docs/classes/BiquadEffect.html +3 -3
  63. package/docs/classes/Bus.html +21 -21
  64. package/docs/classes/Cacophony.html +86 -86
  65. package/docs/classes/DynamicsEffect.html +7 -7
  66. package/docs/classes/FdnReverbEffect.html +7 -7
  67. package/docs/classes/FoaDecoder.html +5 -5
  68. package/docs/classes/FoaDecoderEffect.html +3 -3
  69. package/docs/classes/FrequencyShifterEffect.html +7 -7
  70. package/docs/classes/Group.html +32 -32
  71. package/docs/classes/HarmonizerEffect.html +7 -7
  72. package/docs/classes/ImpulseResponseEffect.html +3 -3
  73. package/docs/classes/KWeightingFilter.html +4 -4
  74. package/docs/classes/LoudnessMeter.html +11 -11
  75. package/docs/classes/MediaStreamPlayback.html +81 -68
  76. package/docs/classes/MediaStreamSound.html +48 -48
  77. package/docs/classes/MicrophoneStream.html +49 -49
  78. package/docs/classes/ModulatedDelayEffect.html +7 -7
  79. package/docs/classes/PcmStreamPlayback.html +81 -68
  80. package/docs/classes/PcmStreamSound.html +58 -58
  81. package/docs/classes/PhaserEffect.html +7 -7
  82. package/docs/classes/Playback.html +106 -93
  83. package/docs/classes/ReverbEffect.html +7 -7
  84. package/docs/classes/Scheduler.html +3 -3
  85. package/docs/classes/ShareEffect.html +3 -3
  86. package/docs/classes/Sound.html +108 -68
  87. package/docs/classes/SpectralFreezeEffect.html +7 -7
  88. package/docs/classes/StereoWidenerEffect.html +3 -3
  89. package/docs/classes/Synth.html +60 -60
  90. package/docs/classes/SynthGroup.html +28 -28
  91. package/docs/classes/TremoloEffect.html +7 -7
  92. package/docs/classes/TruePeakDetector.html +8 -8
  93. package/docs/classes/WaveshaperEffect.html +7 -7
  94. package/docs/functions/encodeMonoToFoaSN3D.html +1 -1
  95. package/docs/functions/integratedLoudness.html +1 -1
  96. package/docs/functions/integratedUngatedLoudness.html +1 -1
  97. package/docs/functions/loudnessRange.html +1 -1
  98. package/docs/functions/timeStretch.html +1 -1
  99. package/docs/functions/timeStretchChannels.html +1 -1
  100. package/docs/functions/truePeakDb.html +1 -1
  101. package/docs/hierarchy.html +29 -29
  102. package/docs/index.html +43 -3
  103. package/docs/interfaces/AudioBuffer.html +8 -8
  104. package/docs/interfaces/AudioBufferSourceNode.html +22 -22
  105. package/docs/interfaces/AudioEventCallbacks.html +2 -2
  106. package/docs/interfaces/AudioListener.html +12 -12
  107. package/docs/interfaces/AudioNode.html +14 -14
  108. package/docs/interfaces/AudioParam.html +6 -6
  109. package/docs/interfaces/AudioWorklet.html +2 -2
  110. package/docs/interfaces/AudioWorkletNode.html +16 -16
  111. package/docs/interfaces/BarberpoleOptions.html +5 -5
  112. package/docs/interfaces/BaseContext.html +25 -25
  113. package/docs/interfaces/BaseSound.html +16 -16
  114. package/docs/interfaces/BiquadCoefficients.html +6 -6
  115. package/docs/interfaces/BiquadFilterNode.html +20 -20
  116. package/docs/interfaces/BuiltEffectGraph.html +6 -6
  117. package/docs/interfaces/CacheErrorEvent.html +5 -5
  118. package/docs/interfaces/CacheHitEvent.html +4 -4
  119. package/docs/interfaces/CacheMissEvent.html +4 -4
  120. package/docs/interfaces/CacophonyEffect.html +2 -2
  121. package/docs/interfaces/CacophonyLogger.html +4 -4
  122. package/docs/interfaces/ChannelMergerNode.html +14 -14
  123. package/docs/interfaces/ChannelSplitterNode.html +14 -14
  124. package/docs/interfaces/ConvolverNode.html +16 -16
  125. package/docs/interfaces/CreateSpriteOptions.html +3 -3
  126. package/docs/interfaces/DynamicsOptions.html +7 -7
  127. package/docs/interfaces/FadeStartEvent.html +4 -4
  128. package/docs/interfaces/FdnReverbOptions.html +6 -6
  129. package/docs/interfaces/FoaDecoderOptions.html +2 -2
  130. package/docs/interfaces/FrequencyShifterOptions.html +3 -3
  131. package/docs/interfaces/GainNode.html +15 -15
  132. package/docs/interfaces/GlobalPlaybackEvent.html +3 -3
  133. package/docs/interfaces/HarmonizerOptions.html +6 -6
  134. package/docs/interfaces/ImpulseResponseOptions.html +5 -5
  135. package/docs/interfaces/LoadingCompleteEvent.html +5 -5
  136. package/docs/interfaces/LoadingErrorEvent.html +5 -5
  137. package/docs/interfaces/LoadingProgressEvent.html +6 -6
  138. package/docs/interfaces/LoadingStartEvent.html +3 -3
  139. package/docs/interfaces/LoudnessChannelInput.html +3 -3
  140. package/docs/interfaces/LoudnessReading.html +5 -5
  141. package/docs/interfaces/MediaElementSourceNode.html +15 -15
  142. package/docs/interfaces/MediaStreamAudioSourceNode.html +15 -15
  143. package/docs/interfaces/MediaStreamSoundOptions.html +5 -5
  144. package/docs/interfaces/MicrophoneStreamOptions.html +8 -8
  145. package/docs/interfaces/ModulatedDelayOptions.html +8 -8
  146. package/docs/interfaces/OfflineOptions.html +5 -5
  147. package/docs/interfaces/OscillatorNode.html +20 -20
  148. package/docs/interfaces/PannerNode.html +28 -28
  149. package/docs/interfaces/PcmStreamBufferEvent.html +2 -2
  150. package/docs/interfaces/PcmStreamSoundOptions.html +6 -6
  151. package/docs/interfaces/PhaserOptions.html +7 -7
  152. package/docs/interfaces/PlayOptions.html +62 -8
  153. package/docs/interfaces/PlaybackErrorEvent.html +5 -5
  154. package/docs/interfaces/ReverbOptions.html +13 -13
  155. package/docs/interfaces/RuntimeOptions.html +6 -6
  156. package/docs/interfaces/ScheduledCallbackHandle.html +2 -2
  157. package/docs/interfaces/SoundCleanupHoldings.html +4 -4
  158. package/docs/interfaces/SoundErrorEvent.html +6 -6
  159. package/docs/interfaces/SpectralFreezeOptions.html +4 -4
  160. package/docs/interfaces/SpriteRegion.html +4 -4
  161. package/docs/interfaces/StereoPannerNode.html +15 -15
  162. package/docs/interfaces/StereoToBFormatOptions.html +2 -2
  163. package/docs/interfaces/StereoWidenerOptions.html +4 -4
  164. package/docs/interfaces/StreamCapabilities.html +5 -5
  165. package/docs/interfaces/TimeStretchOptions.html +5 -5
  166. package/docs/interfaces/TremoloOptions.html +5 -5
  167. package/docs/interfaces/WaveshaperOptions.html +5 -5
  168. package/docs/types/BaseAudioEvents.html +11 -11
  169. package/docs/types/BuiltEffect.html +1 -1
  170. package/docs/types/BusConnectionTarget.html +1 -1
  171. package/docs/types/CacheEventCallback.html +1 -1
  172. package/docs/types/CacophonyEvents.html +17 -17
  173. package/docs/types/ErrorEventCallback.html +1 -1
  174. package/docs/types/FadeType.html +1 -1
  175. package/docs/types/HrtfPannerOptions.html +1 -1
  176. package/docs/types/ImpulseResponseSource.html +1 -1
  177. package/docs/types/LoadingEventCallback.html +1 -1
  178. package/docs/types/LoopCount.html +1 -1
  179. package/docs/types/LoudnessChannel.html +1 -1
  180. package/docs/types/Orientation.html +3 -3
  181. package/docs/types/PanCloneOverrides.html +5 -5
  182. package/docs/types/PanType.html +1 -1
  183. package/docs/types/PcmStreamEvents.html +6 -6
  184. package/docs/types/PcmStreamState.html +1 -1
  185. package/docs/types/PlaybackEvents.html +1 -1
  186. package/docs/types/Position.html +1 -1
  187. package/docs/types/SoundEvents.html +1 -1
  188. package/docs/types/SoundType.html +1 -1
  189. package/docs/types/SourceNode.html +1 -1
  190. package/docs/types/SpriteMap.html +1 -1
  191. package/docs/types/SynthEvents.html +1 -1
  192. package/docs/types/ThreeDOptions.html +1 -1
  193. package/docs/types/WebCodecsStreamSound.html +1 -1
  194. package/docs/variables/CHANNEL_WEIGHTS.html +1 -1
  195. package/docs/variables/K_WEIGHTING_STAGE1_48K.html +1 -1
  196. package/docs/variables/K_WEIGHTING_STAGE2_48K.html +1 -1
  197. package/docs/variables/consoleLogger.html +1 -1
  198. package/docs/variables/noopLogger.html +1 -1
  199. 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
@@ -1292,7 +1329,29 @@ Cacophony implements intelligent three-layer caching for optimal performance:
1292
1329
 
1293
1330
  **Memory Cache (LRU)** → **Browser Cache API** → **Network**
1294
1331
 
1295
- The cache system is fully automatic and HTTP-compliant, respecting standard cache headers (ETag, Last-Modified). When cache validation tokens are available, Cacophony makes lightweight conditional requests (304 responses have no body). When tokens are unavailable, it falls back to TTL-based caching (24 hours default).
1332
+ 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.
1333
+
1334
+ 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.
1335
+
1336
+ 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.
1337
+
1338
+ 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.
1339
+
1340
+ `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.
1341
+
1342
+ 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.
1343
+
1344
+ 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.
1345
+
1346
+ 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.
1347
+
1348
+ 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:
1349
+
1350
+ ```http
1351
+ Access-Control-Expose-Headers: Age, Date, ETag, Vary
1352
+ ```
1353
+
1354
+ 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
1355
 
1297
1356
  ```typescript
1298
1357
  const cacophony = new Cacophony();
@@ -1300,7 +1359,7 @@ const cacophony = new Cacophony();
1300
1359
  // First load - fetches from network, stores in cache
1301
1360
  const sound1 = await cacophony.createSound('audio.mp3');
1302
1361
 
1303
- // Second load - instant from memory cache
1362
+ // Second load - from memory while the HTTP policy permits reuse
1304
1363
  const sound2 = await cacophony.createSound('audio.mp3');
1305
1364
 
1306
1365
  // Monitor cache performance via events
@@ -1317,12 +1376,12 @@ cacophony.on('cacheMiss', (event) => {
1317
1376
  // Clear memory cache (browser cache persists)
1318
1377
  cacophony.clearMemoryCache();
1319
1378
 
1320
- // Optional: configure TTL for when no validation tokens exist
1379
+ // Optional: configure TTL for responses without cache directives, expiry, or validators
1321
1380
  import { AudioCache } from 'cacophony';
1322
1381
  AudioCache.setCacheExpirationTime(60 * 60 * 1000); // 1 hour
1323
1382
  ```
1324
1383
 
1325
- The caching system requires no configuration in most cases. It automatically optimizes for performance while respecting HTTP standards.
1384
+ 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
1385
 
1327
1386
  ## Cancellation with AbortSignal
1328
1387
 
@@ -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.
@@ -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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"}
@@ -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: import('./cacophony').PanType;
14
- readonly panType: import('./cacophony').PanType;
15
- setPanType(panType: import('./cacophony').PanType, audioContext: BaseContext): void;
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
- * Checks if the audio is currently playing.
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;
@@ -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
@@ -1 +1 @@
1
- {"version":3,"file":"cache.cjs","names":[],"sources":["../src/cache.ts"],"sourcesContent":["import type { BaseContext } from \"./context\";\nimport type { AudioEventCallbacks, CacheHitEvent, LoadingProgressEvent } from \"./events\";\n\n/**\n * Subset of {@link AudioEventCallbacks} relevant to {@link AudioCache} public API.\n * Lets callers opt in to any combination of loading/cache events without\n * having to import the full union.\n */\nexport type CacheCallbacks = Pick<\n AudioEventCallbacks,\n | \"onLoadingStart\"\n | \"onLoadingProgress\"\n | \"onLoadingComplete\"\n | \"onLoadingError\"\n | \"onCacheHit\"\n | \"onCacheMiss\"\n | \"onCacheError\"\n>;\n\n/**\n * Coerce an unknown caught value into an `Error`. Use at the boundary\n * of `catch (error: unknown)` blocks where downstream typing requires a real\n * `Error` (e.g. `LoadingErrorEvent.error`, `CacheErrorEvent.error`).\n */\nfunction toError(value: unknown): Error {\n return value instanceof Error ? value : new Error(String(value));\n}\n\n/**\n * Build the metadata cache key for a given URL. Centralised so the\n * \":meta\" suffix never drifts across writers/readers/cleanup paths.\n */\nfunction metaKey(url: string): string {\n return `${url}:meta`;\n}\n\nclass ByteBoundedLRUCache<K, V> {\n private maxBytes: number;\n private cachedBytes = 0;\n private cache: Map<K, { value: V; bytes: number }>;\n private estimateBytes: (value: V) => number;\n\n constructor(maxBytes: number, estimateBytes: (value: V) => number) {\n this.maxBytes = maxBytes;\n this.estimateBytes = estimateBytes;\n this.cache = new Map();\n }\n\n get(key: K): V | undefined {\n const entry = this.cache.get(key);\n if (entry === undefined) return undefined;\n this.cache.delete(key);\n this.cache.set(key, entry);\n return entry.value;\n }\n\n set(key: K, value: V): void {\n const bytes = this.estimateBytes(value);\n\n if (this.cache.has(key)) {\n this.cachedBytes -= this.cache.get(key)?.bytes ?? 0;\n this.cache.delete(key);\n }\n\n if (bytes > this.maxBytes) {\n return;\n }\n\n while (this.cachedBytes + bytes > this.maxBytes) {\n const first = this.cache.keys().next();\n if (first.done) {\n break;\n }\n const firstKey = first.value;\n const oldest = this.cache.get(firstKey);\n this.cachedBytes -= oldest?.bytes ?? 0;\n this.cache.delete(firstKey);\n }\n\n this.cache.set(key, { value, bytes });\n this.cachedBytes += bytes;\n }\n}\n\ninterface CacheMetadata {\n url: string;\n etag?: string;\n lastModified?: string;\n cacheControl?: string;\n timestamp: number;\n}\n\n/**\n * Validator tokens carried in cache metadata, used to build conditional\n * requests (`If-None-Match`, `If-Modified-Since`).\n */\ntype CacheValidators = Pick<CacheMetadata, \"etag\" | \"lastModified\">;\n\nconst DEFAULT_DECODED_BUFFER_CACHE_BYTES = 64 * 1024 * 1024;\nconst BYTES_PER_FLOAT32_SAMPLE = 4;\n\nfunction estimateAudioBufferBytes(audioBuffer: AudioBuffer): number {\n return audioBuffer.length * audioBuffer.numberOfChannels * BYTES_PER_FLOAT32_SAMPLE;\n}\n\n/**\n * Parse the max-age value from a Cache-Control header.\n *\n * Accepts unquoted (`max-age=3600`) and properly-quoted (`max-age=\"3600\"`)\n * forms only; rejects malformed half-quoted variants like `max-age=\"3600`\n * or `max-age=3600\"`.\n *\n * @returns max-age in seconds, or null if not found / malformed\n */\nfunction parseMaxAge(cacheControlHeader: string | undefined): number | null {\n if (!cacheControlHeader) {\n return null;\n }\n // Either both quotes or neither; (?:...) groups the alternatives.\n const match = cacheControlHeader.match(/max-age\\s*=\\s*(?:\"(\\d+)\"|(\\d+))/i);\n if (!match) return null;\n const captured = match[1] ?? match[2];\n if (captured === undefined) return null;\n const parsed = Number.parseInt(captured, 10);\n return Number.isNaN(parsed) ? null : parsed;\n}\n\n/**\n * Check if Cache-Control header contains directives that require revalidation\n * @param cacheControlHeader - The Cache-Control header value\n * @returns true if revalidation is required regardless of age\n */\nfunction requiresRevalidation(cacheControlHeader: string | undefined): boolean {\n if (!cacheControlHeader) {\n return false;\n }\n return /(?:^|,)\\s*(no-cache|no-store|must-revalidate)\\s*(?:,|$)/i.test(cacheControlHeader);\n}\n\nfunction getNetworkErrorType(error: unknown): \"network\" | \"abort\" | \"unknown\" {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n return \"abort\";\n }\n if (error instanceof Error) {\n return \"network\";\n }\n return \"unknown\";\n}\n\n/**\n * Parse a `data:` URL into its mime type and decoded bytes.\n * Returns `null` if the URL is malformed (no comma, undecodable base64, etc.)\n * so callers can fire a typed `onLoadingError` instead of crashing.\n */\nfunction parseDataUrl(url: string): { mime: string; bytes: Uint8Array } | null {\n const commaIndex = url.indexOf(\",\");\n if (commaIndex < 0) return null;\n const header = url.slice(5, commaIndex); // strip \"data:\"\n const payload = url.slice(commaIndex + 1);\n const isBase64 = /;base64$/i.test(header);\n const mime = (isBase64 ? header.replace(/;base64$/i, \"\") : header) || \"text/plain\";\n try {\n if (isBase64) {\n const binary = atob(payload);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return { mime, bytes };\n }\n const decoded = decodeURIComponent(payload);\n const bytes = new Uint8Array(decoded.length);\n for (let i = 0; i < decoded.length; i++) {\n bytes[i] = decoded.charCodeAt(i);\n }\n return { mime, bytes };\n } catch {\n return null;\n }\n}\n\nexport interface ICache {\n getAudioBuffer(\n context: BaseContext,\n url: string,\n signal?: AbortSignal,\n callbacks?: CacheCallbacks,\n ): Promise<AudioBuffer>;\n clearMemoryCache(): void;\n}\n\ntype PendingProgressCallbacks = Pick<CacheCallbacks, \"onLoadingProgress\">;\n\ninterface PendingAudioRequest {\n promise: Promise<AudioBuffer>;\n controller: AbortController;\n callers: number;\n}\n\n/**\n * AudioCache provides efficient caching of audio resources using HTTP caching standards.\n *\n * Features:\n * - Three-layer caching: Memory (LRU) → Browser Cache API → Network\n * - HTTP conditional requests with ETag and Last-Modified support\n * - Robust error handling with cache inconsistency recovery\n *\n * Caching Strategy:\n * - Always makes conditional requests when validation tokens (ETag/Last-Modified) are available\n * - Uses TTL as fallback only when no validation tokens exist\n * - Conditional requests are lightweight (304 responses have no body)\n *\n * @example\n * ```typescript\n * const cache = new AudioCache();\n * const audioBuffer = await cache.getAudioBuffer(audioContext, 'audio.mp3');\n *\n * // Optional: Configure TTL for when no validation tokens exist\n * AudioCache.setCacheExpirationTime(60 * 60 * 1000); // 1 hour\n * ```\n */\nexport class AudioCache implements ICache {\n private pendingRequests = new WeakMap<BaseContext, Map<string, PendingAudioRequest>>();\n private pendingCallbacks = new WeakMap<BaseContext, Map<string, PendingProgressCallbacks[]>>();\n private decodedBuffers = new WeakMap<BaseContext, ByteBoundedLRUCache<string, AudioBuffer>>();\n private static cacheExpirationTime: number = 24 * 60 * 60 * 1000; // 24 hours in milliseconds\n\n public static setCacheExpirationTime(time: number): void {\n AudioCache.cacheExpirationTime = time;\n }\n\n /**\n * Whether the browser Cache API (`caches`) global is available in the\n * current runtime. Uses `typeof` so it never throws when the global is\n * entirely absent (e.g. Node). When this returns `false`, the URL path\n * degrades to fetch + decode (still using the in-memory LRU) instead of\n * touching the persistent Cache API.\n */\n private static isCacheApiAvailable(): boolean {\n return typeof caches !== \"undefined\" && caches != null;\n }\n\n private static async openCache(): Promise<Cache> {\n try {\n return await caches.open(\"audio-cache\");\n } catch (error) {\n console.error(\"Failed to open cache:\", error);\n throw error;\n }\n }\n\n /**\n * Calls all registered callbacks for a specific event type on a URL.\n * Generic over the callback name so the payload type is checked against\n * the canonical {@link CacheCallbacks} shape rather than `any`.\n */\n private static callAllCallbacks<K extends keyof CacheCallbacks>(\n pendingCallbacks: Map<string, PendingProgressCallbacks[]> | undefined,\n url: string,\n callbackName: K,\n eventData: Parameters<NonNullable<CacheCallbacks[K]>>[0],\n ): void {\n const callbacks = pendingCallbacks?.get(url);\n if (callbacks) {\n callbacks.forEach((callbackSet) => {\n const callback = callbackSet[callbackName as \"onLoadingProgress\"];\n if (callback) {\n try {\n // Cast eventData here: the surrounding generic guarantees the\n // payload type matches the chosen callback name.\n (callback as (e: typeof eventData) => void)(eventData);\n } catch (error) {\n console.error(`Error in ${String(callbackName)} callback:`, error);\n }\n }\n });\n }\n }\n\n private async getOrCreatePendingRequest(\n context: BaseContext,\n url: string,\n createRequest: (signal: AbortSignal) => Promise<AudioBuffer | undefined>,\n signal?: AbortSignal,\n callbacks?: PendingProgressCallbacks,\n ): Promise<AudioBuffer> {\n if (signal?.aborted) {\n throw new DOMException(\"Operation was aborted\", \"AbortError\");\n }\n\n let pendingRequests = this.pendingRequests.get(context);\n if (pendingRequests === undefined) {\n pendingRequests = new Map();\n this.pendingRequests.set(context, pendingRequests);\n }\n let pendingCallbacks = this.pendingCallbacks.get(context);\n if (pendingCallbacks === undefined) {\n pendingCallbacks = new Map();\n this.pendingCallbacks.set(context, pendingCallbacks);\n }\n\n // Add callbacks to aggregation if provided\n if (callbacks) {\n const existingCallbacks = pendingCallbacks.get(url) || [];\n existingCallbacks.push(callbacks);\n pendingCallbacks.set(url, existingCallbacks);\n }\n\n let pendingRequest = pendingRequests.get(url);\n if (!pendingRequest) {\n const controller = new AbortController();\n const requestPromise = (async () => {\n try {\n const result = await createRequest(controller.signal);\n if (result === undefined) {\n throw new Error(\"Failed to create audio buffer.\");\n }\n return result;\n } finally {\n if (pendingRequests.get(url) === pendingRequest) {\n pendingRequests.delete(url);\n pendingCallbacks.delete(url);\n }\n }\n })();\n\n pendingRequest = { promise: requestPromise, controller, callers: 0 };\n pendingRequests.set(url, pendingRequest);\n }\n\n pendingRequest.callers++;\n return new Promise((resolve, reject) => {\n let settled = false;\n const release = (aborted: boolean) => {\n if (settled) return;\n settled = true;\n signal?.removeEventListener(\"abort\", handleAbort);\n pendingRequest.callers--;\n\n if (aborted && callbacks) {\n const registeredCallbacks = pendingCallbacks.get(url);\n const callbackIndex = registeredCallbacks?.indexOf(callbacks) ?? -1;\n if (callbackIndex >= 0) {\n registeredCallbacks?.splice(callbackIndex, 1);\n }\n }\n\n if (aborted && pendingRequest.callers === 0 && pendingRequests.get(url) === pendingRequest) {\n pendingRequests.delete(url);\n pendingCallbacks.delete(url);\n pendingRequest.controller.abort();\n }\n };\n const handleAbort = () => {\n release(true);\n reject(new DOMException(\"Operation was aborted\", \"AbortError\"));\n };\n\n signal?.addEventListener(\"abort\", handleAbort, { once: true });\n pendingRequest.promise.then(\n (buffer) => {\n if (settled) return;\n release(false);\n resolve(buffer);\n },\n (error: unknown) => {\n if (settled) return;\n release(false);\n reject(error);\n },\n );\n });\n }\n\n private static async updateMetadata(cache: Cache, url: string, data: Partial<CacheMetadata>): Promise<void> {\n const metadata: CacheMetadata = {\n url,\n timestamp: Date.now(),\n ...data,\n };\n\n await cache.put(\n metaKey(url),\n new Response(JSON.stringify(metadata), {\n headers: { \"Content-Type\": \"application/json\" },\n }),\n );\n }\n\n private static async getBufferFromCache(url: string, cache: Cache): Promise<ArrayBuffer | null> {\n try {\n const response = await cache.match(url);\n if (response?.ok) {\n return await response.arrayBuffer();\n }\n return null;\n } catch (error) {\n console.error(\"Failed to get data from cache:\", error);\n return null;\n }\n }\n\n private async fetchAndCacheBuffer(\n context: BaseContext,\n url: string,\n cache: Cache,\n validators?: CacheValidators,\n signal?: AbortSignal,\n callbacks?: Pick<CacheCallbacks, \"onCacheHit\">,\n ): Promise<ArrayBuffer> {\n const etag = validators?.etag;\n const lastModified = validators?.lastModified;\n const headers = new Headers();\n if (etag) headers.append(\"If-None-Match\", etag);\n if (lastModified) headers.append(\"If-Modified-Since\", lastModified);\n\n console.debug(`[AudioCache] Fetching ${url}`, {\n headers: Object.fromEntries(headers.entries()),\n hasEtag: !!etag,\n hasLastModified: !!lastModified,\n });\n\n const fetchResponse = await fetch(url, { headers, signal });\n\n console.debug(`[AudioCache] Response ${url}`, {\n status: fetchResponse.status,\n statusText: fetchResponse.statusText,\n etag: fetchResponse.headers?.get(\"ETag\"),\n lastModified: fetchResponse.headers?.get(\"Last-Modified\"),\n cacheControl: fetchResponse.headers?.get(\"Cache-Control\"),\n });\n\n if (fetchResponse.status === 304) {\n const cachedResponse = await cache.match(url);\n if (cachedResponse) {\n // Emit cache hit for 304 responses\n if (callbacks?.onCacheHit) {\n const event: CacheHitEvent = {\n url,\n cacheType: \"conditional\",\n timestamp: Date.now(),\n };\n callbacks.onCacheHit(event);\n }\n\n // Update metadata timestamp on revalidation\n const timestamp = Date.now();\n const newCacheControl = fetchResponse.headers?.get(\"Cache-Control\");\n await AudioCache.updateMetadata(cache, url, {\n timestamp,\n etag,\n lastModified,\n // Only update cacheControl if present in response, otherwise preserve existing\n ...(newCacheControl ? { cacheControl: newCacheControl } : {}),\n });\n return await cachedResponse.arrayBuffer();\n } else {\n // Cache inconsistency: 304 response but no cached body\n // This can happen if cache was partially corrupted or cleared\n // Fall back to re-fetching without validation headers\n console.warn(`Cache inconsistency detected for ${url}: 304 response but no cached body. Re-fetching.`);\n\n // Re-fetch without validation headers to get fresh content\n const freshResponse = await fetch(url, { signal });\n if (freshResponse.status === 200) {\n const responseClone = freshResponse.clone();\n const newEtag = freshResponse.headers.get(\"ETag\");\n const newLastModified = freshResponse.headers.get(\"Last-Modified\");\n const newCacheControl = freshResponse.headers.get(\"Cache-Control\");\n\n await AudioCache.writeBufferAndMetadata(cache, url, responseClone, {\n timestamp: Date.now(),\n etag: newEtag || undefined,\n lastModified: newLastModified || undefined,\n cacheControl: newCacheControl || undefined,\n });\n\n // Use progress tracking for cache recovery scenario if body exists\n if (freshResponse.body) {\n const { stream, total } = this.createProgressTrackingStream(context, freshResponse, url, signal);\n return await AudioCache.collectStreamToArrayBuffer(stream, total || undefined);\n } else {\n // Fallback for mock responses without body (testing scenario)\n return await freshResponse.arrayBuffer();\n }\n } else {\n throw new Error(\n `Failed to fetch resource after cache inconsistency: ${freshResponse.status} ${freshResponse.statusText}`,\n );\n }\n }\n }\n\n if (fetchResponse.status === 200) {\n const responseClone = fetchResponse.clone();\n const newEtag = fetchResponse.headers.get(\"ETag\");\n const newLastModified = fetchResponse.headers.get(\"Last-Modified\");\n const newCacheControl = fetchResponse.headers.get(\"Cache-Control\");\n\n await AudioCache.writeBufferAndMetadata(cache, url, responseClone, {\n timestamp: Date.now(),\n etag: newEtag || undefined,\n lastModified: newLastModified || undefined,\n cacheControl: newCacheControl || undefined,\n });\n }\n\n if (signal?.aborted) {\n throw new DOMException(\"Operation was aborted\", \"AbortError\");\n }\n\n // Use progress tracking for the main response if body exists\n if (fetchResponse.body) {\n const { stream, total } = this.createProgressTrackingStream(context, fetchResponse, url, signal);\n return await AudioCache.collectStreamToArrayBuffer(stream, total || undefined);\n } else {\n // Fallback for mock responses without body (testing scenario)\n return await fetchResponse.arrayBuffer();\n }\n }\n\n /**\n * Atomically write a response body and its metadata into the cache.\n * On failure, deletes both partial entries (best-effort via `allSettled`)\n * and rethrows the original error.\n */\n private static async writeBufferAndMetadata(\n cache: Cache,\n url: string,\n response: Response,\n metadata: Partial<CacheMetadata>,\n ): Promise<void> {\n try {\n await Promise.all([cache.put(url, response), AudioCache.updateMetadata(cache, url, metadata)]);\n } catch (error) {\n // Clean up partial cache entries on error; both deletes run regardless.\n await Promise.allSettled([cache.delete(url), cache.delete(metaKey(url))]);\n throw error;\n }\n }\n\n /**\n * Creates a ReadableStream wrapper that tracks download progress.\n * Uses the callback aggregation system to emit progress to all registered listeners.\n * Honours `signal` between reads and releases the underlying reader on\n * any exit path (done, error, abort).\n * @param response - The fetch Response object with ReadableStream body\n * @param url - URL being downloaded (for progress event data and callback lookup)\n * @param signal - Optional AbortSignal observed at chunk boundaries\n * @returns Object containing the progress-tracking stream and total size\n */\n private createProgressTrackingStream(\n context: BaseContext,\n response: Response,\n url: string,\n signal?: AbortSignal,\n ): { stream: ReadableStream<Uint8Array>; total: number | null } {\n // Extract Content-Length from response headers\n const contentLengthHeader = response.headers.get(\"content-length\");\n const total = contentLengthHeader ? Number.parseInt(contentLengthHeader, 10) : null;\n\n let loaded = 0;\n\n if (!response.body) {\n // Fallback for responses without body - shouldn't happen for audio files\n throw new Error(\"Response body is null\");\n }\n\n const reader = response.body.getReader();\n const pendingCallbacks = this.pendingCallbacks.get(context);\n\n const stream = new ReadableStream<Uint8Array>({\n async start(controller) {\n try {\n while (true) {\n if (signal?.aborted) {\n throw new DOMException(\"Operation was aborted\", \"AbortError\");\n }\n const { done, value } = await reader.read();\n if (done) {\n // Emit final progress event at 100%\n AudioCache.callAllCallbacks(pendingCallbacks, url, \"onLoadingProgress\", {\n url,\n loaded,\n total,\n progress: total ? 1 : -1, // 100% if total known, -1 if unknown\n timestamp: Date.now(),\n } satisfies LoadingProgressEvent);\n controller.close();\n return;\n }\n\n if (value) {\n loaded += value.byteLength;\n\n // Emit progress event\n const progress = total ? loaded / total : -1;\n AudioCache.callAllCallbacks(pendingCallbacks, url, \"onLoadingProgress\", {\n url,\n loaded,\n total,\n progress,\n timestamp: Date.now(),\n } satisfies LoadingProgressEvent);\n\n controller.enqueue(value);\n }\n }\n } catch (error) {\n controller.error(error);\n } finally {\n reader.releaseLock();\n }\n },\n\n cancel(reason) {\n // Clean up reader when stream is cancelled\n return reader.cancel(reason);\n },\n });\n\n return { stream, total };\n }\n\n /**\n * Collects all chunks from a ReadableStream into a single ArrayBuffer\n * @param stream - The ReadableStream to collect from\n * @returns Promise that resolves to the complete ArrayBuffer\n */\n private static async collectStreamToArrayBuffer(\n stream: ReadableStream<Uint8Array>,\n knownLength?: number,\n ): Promise<ArrayBuffer> {\n const reader = stream.getReader();\n\n try {\n if (knownLength !== undefined && knownLength > 0) {\n // Pre-allocation path: we know the exact size\n const result = new ArrayBuffer(knownLength);\n const uint8View = new Uint8Array(result);\n let offset = 0;\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n if (value) {\n uint8View.set(value, offset);\n offset += value.byteLength;\n }\n }\n\n return result;\n } else {\n // Exponential growth path: unknown size\n let buffer = new Uint8Array(8192); // Start with 8KB\n let totalLength = 0;\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n if (value) {\n // Grow buffer if needed\n if (totalLength + value.byteLength > buffer.length) {\n const newSize = Math.max(buffer.length * 2, totalLength + value.byteLength);\n const newBuffer = new Uint8Array(newSize);\n newBuffer.set(buffer.subarray(0, totalLength));\n buffer = newBuffer;\n }\n\n buffer.set(value, totalLength);\n totalLength += value.byteLength;\n }\n }\n\n // Return exact-sized ArrayBuffer\n return buffer.slice(0, totalLength).buffer;\n }\n } finally {\n reader.releaseLock();\n }\n }\n\n /**\n * Fetch + decode an audio URL WITHOUT touching the persistent Cache API.\n * Used when the Cache API global (`caches`) is unavailable (e.g. Node).\n *\n * Mirrors the network branch of {@link fetchAndCacheBuffer}/{@link getAudioBuffer}\n * but never reads from or writes to `caches`. Still:\n * - uses the in-memory decoded-buffer LRU (write happens in {@link getAudioBuffer}),\n * - emits progress callbacks via {@link createProgressTrackingStream},\n * - is deduped through {@link getOrCreatePendingRequest} by the caller.\n */\n private async fetchAndDecodeWithoutCache(\n context: BaseContext,\n url: string,\n signal?: AbortSignal,\n ): Promise<ArrayBuffer> {\n const fetchResponse = await fetch(url, { signal });\n\n if (fetchResponse.status !== 200 && !fetchResponse.ok) {\n throw new Error(`Failed to fetch resource: ${fetchResponse.status} ${fetchResponse.statusText}`);\n }\n\n if (signal?.aborted) {\n throw new DOMException(\"Operation was aborted\", \"AbortError\");\n }\n\n // Use progress tracking for the response if body exists\n if (fetchResponse.body) {\n const { stream, total } = this.createProgressTrackingStream(context, fetchResponse, url, signal);\n return await AudioCache.collectStreamToArrayBuffer(stream, total || undefined);\n }\n // Fallback for mock responses without body (testing scenario)\n return await fetchResponse.arrayBuffer();\n }\n\n /**\n * Run the shared network-result pipeline: fetch bytes, decode them, emit\n * loading callbacks, and retain the decoded buffer in the in-memory LRU.\n * The caller supplies the fetch strategy so persistent-cache and degraded\n * runtimes share this plumbing without changing their network behavior.\n */\n private async fetchDecodeAndCache(\n context: BaseContext,\n url: string,\n decodedBuffers: ByteBoundedLRUCache<string, AudioBuffer>,\n fetchBuffer: () => Promise<ArrayBuffer>,\n callbacks?: Pick<CacheCallbacks, \"onLoadingComplete\" | \"onLoadingError\" | \"onCacheError\">,\n reportCacheError = false,\n ): Promise<AudioBuffer> {\n try {\n const arrayBuffer = await fetchBuffer();\n let audioBuffer: AudioBuffer;\n try {\n audioBuffer = await AudioCache.decodeAudioData(context, arrayBuffer);\n } catch (error) {\n callbacks?.onLoadingError?.({\n url,\n error: toError(error),\n errorType: \"decode\",\n timestamp: Date.now(),\n });\n throw error;\n }\n\n callbacks?.onLoadingComplete?.({\n url,\n duration: audioBuffer.duration,\n size: arrayBuffer.byteLength,\n timestamp: Date.now(),\n });\n decodedBuffers.set(url, audioBuffer);\n return audioBuffer;\n } catch (error) {\n callbacks?.onLoadingError?.({\n url,\n error: toError(error),\n errorType: getNetworkErrorType(error),\n timestamp: Date.now(),\n });\n if (reportCacheError) {\n callbacks?.onCacheError?.({\n url,\n error: toError(error),\n operation: \"get\",\n timestamp: Date.now(),\n });\n }\n throw error;\n }\n }\n\n private static async decodeAudioData(context: BaseContext, arrayBuffer: ArrayBuffer): Promise<AudioBuffer> {\n try {\n return await context.decodeAudioData(arrayBuffer);\n } catch (error) {\n console.error(\"Failed to decode audio data:\", error);\n throw error;\n }\n }\n\n private static async getMetadataFromCache(url: string, cache: Cache): Promise<CacheMetadata | null> {\n try {\n const metaResponse = await cache.match(metaKey(url));\n if (metaResponse?.ok) {\n return await metaResponse.json();\n }\n return null;\n } catch (error) {\n console.error(\"Failed to get metadata from cache:\", error);\n return null;\n }\n }\n\n /**\n * Get an AudioBuffer for the specified URL, using intelligent caching strategies.\n *\n * Caching Flow:\n * 1. Check memory cache (LRU) for decoded AudioBuffer\n * 2. Check persistent cache for raw ArrayBuffer and metadata\n * 3. Make conditional HTTP request if validation tokens available\n * 4. Decode audio data and cache at all levels\n *\n * The cache prioritizes HTTP conditional requests (ETag/Last-Modified) over TTL\n * to ensure content freshness while maintaining performance through 304 responses.\n *\n * @param context - AudioContext for decoding audio data\n * @param url - URL of the audio resource to fetch\n * @param signal - Optional AbortSignal to cancel the operation\n * @returns Promise that resolves to decoded AudioBuffer\n * @throws Error if audio cannot be fetched or decoded\n */\n public async getAudioBuffer(\n context: BaseContext,\n url: string,\n signal?: AbortSignal,\n callbacks?: CacheCallbacks,\n ): Promise<AudioBuffer> {\n let decodedBuffers = this.decodedBuffers.get(context);\n if (decodedBuffers === undefined) {\n decodedBuffers = new ByteBoundedLRUCache<string, AudioBuffer>(\n DEFAULT_DECODED_BUFFER_CACHE_BYTES,\n estimateAudioBufferBytes,\n );\n this.decodedBuffers.set(context, decodedBuffers);\n }\n\n // Call loading start callback\n if (callbacks?.onLoadingStart) {\n callbacks.onLoadingStart({\n url,\n timestamp: Date.now(),\n });\n }\n\n // Check if the decoded buffer is already available in memory cache.\n // Single get + narrow avoids the has/get/`!` race.\n const memoryHit = decodedBuffers.get(url);\n if (memoryHit !== undefined) {\n if (callbacks?.onCacheHit) {\n callbacks.onCacheHit({\n url,\n cacheType: \"memory\",\n timestamp: Date.now(),\n });\n }\n return memoryHit;\n }\n\n // handle data: urls\n if (url.startsWith(\"data:\")) {\n const parsed = parseDataUrl(url);\n if (parsed === null) {\n const error = new Error(`Malformed data: URL: ${url.slice(0, 32)}…`);\n if (callbacks?.onLoadingError) {\n callbacks.onLoadingError({\n url,\n error,\n errorType: \"decode\",\n timestamp: Date.now(),\n });\n }\n throw error;\n }\n const audioBuffer = await AudioCache.decodeAudioData(context, parsed.bytes.buffer as ArrayBuffer);\n decodedBuffers.set(url, audioBuffer);\n return audioBuffer;\n }\n\n // When the browser Cache API is unavailable (e.g. Node), transparently\n // degrade to a fetch-only path: no persistent cache read/write, no error\n // log — still using the in-memory LRU, pending-request dedup, and progress\n // callbacks. The available-cache path below is byte-for-byte unchanged.\n if (!AudioCache.isCacheApiAvailable()) {\n return this.getOrCreatePendingRequest(\n context,\n url,\n async (requestSignal) => {\n // No persistent cache, so this is always a miss.\n if (callbacks?.onCacheMiss) {\n callbacks.onCacheMiss({\n url,\n reason: \"not-found\",\n timestamp: Date.now(),\n });\n }\n\n return await this.fetchDecodeAndCache(\n context,\n url,\n decodedBuffers,\n () => this.fetchAndDecodeWithoutCache(context, url, requestSignal),\n callbacks,\n );\n },\n signal,\n { onLoadingProgress: callbacks?.onLoadingProgress },\n );\n }\n\n const cache = await AudioCache.openCache();\n\n const metadata = await AudioCache.getMetadataFromCache(url, cache);\n\n // Determine if we should make a network request\n // This logic implements HTTP caching best practices:\n // 1. Check Cache-Control freshness first (RFC-compliant behavior)\n // 2. If stale, use validation headers for conditional requests\n // 3. Fall back to TTL when no validation tokens exist\n const shouldFetch = (() => {\n if (!metadata) {\n return true; // Must fetch if nothing is cached\n }\n\n // Check for directives that require revalidation\n if (requiresRevalidation(metadata.cacheControl)) {\n return true; // Must revalidate due to no-cache, no-store, or must-revalidate\n }\n\n // Check Cache-Control freshness\n const maxAge = parseMaxAge(metadata.cacheControl);\n if (maxAge !== null) {\n const age = (Date.now() - metadata.timestamp) / 1000;\n if (maxAge > 0 && age < maxAge) {\n return false; // Fresh content, serve from cache\n }\n // If max-age=0 or content is stale, proceed to validation\n }\n\n // Content is stale (or max-age=0), check if we can revalidate\n if (metadata.etag || metadata.lastModified) {\n return true; // Stale but can be validated with conditional request\n }\n\n // No validation headers available, fall back to TTL\n return Date.now() - metadata.timestamp > AudioCache.cacheExpirationTime;\n })();\n\n return this.getOrCreatePendingRequest(\n context,\n url,\n async (requestSignal) => {\n if (shouldFetch) {\n // Cache miss - need to fetch from network\n if (callbacks?.onCacheMiss) {\n callbacks.onCacheMiss({\n url,\n reason: metadata ? \"expired\" : \"not-found\",\n timestamp: Date.now(),\n });\n }\n\n return await this.fetchDecodeAndCache(\n context,\n url,\n decodedBuffers,\n () =>\n this.fetchAndCacheBuffer(\n context,\n url,\n cache,\n { etag: metadata?.etag, lastModified: metadata?.lastModified },\n requestSignal,\n { onCacheHit: callbacks?.onCacheHit },\n ),\n callbacks,\n true,\n );\n } else {\n // Content should be fresh in cache\n const cachedBuffer = await AudioCache.getBufferFromCache(url, cache);\n if (cachedBuffer) {\n // Cache hit from browser cache\n if (callbacks?.onCacheHit) {\n callbacks.onCacheHit({\n url,\n cacheType: \"browser\",\n timestamp: Date.now(),\n });\n }\n\n const audioBuffer = await AudioCache.decodeAudioData(context, cachedBuffer);\n decodedBuffers.set(url, audioBuffer);\n return audioBuffer;\n } else {\n // Cache inconsistency - metadata exists but body is missing\n if (callbacks?.onCacheError) {\n callbacks.onCacheError({\n url,\n error: new Error(\"Cache inconsistency: metadata exists but body is missing\"),\n operation: \"get\",\n timestamp: Date.now(),\n });\n }\n\n // Fallback to network if body missing but metadata is fresh\n return await this.fetchDecodeAndCache(\n context,\n url,\n decodedBuffers,\n () =>\n this.fetchAndCacheBuffer(\n context,\n url,\n cache,\n { etag: metadata?.etag, lastModified: metadata?.lastModified },\n requestSignal,\n { onCacheHit: callbacks?.onCacheHit },\n ),\n callbacks,\n );\n }\n }\n },\n signal,\n { onLoadingProgress: callbacks?.onLoadingProgress },\n );\n }\n\n public clearMemoryCache(): void {\n this.decodedBuffers = new WeakMap();\n this.pendingRequests = new WeakMap();\n this.pendingCallbacks = new WeakMap();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cache.cjs","names":[],"sources":["../src/cache.ts"],"sourcesContent":["import CachePolicy from \"http-cache-semantics\";\nimport type { BaseContext } from \"./context\";\nimport type { AudioEventCallbacks } from \"./events\";\n\nexport type CacheCallbacks = Pick<\n AudioEventCallbacks,\n | \"onLoadingStart\"\n | \"onLoadingProgress\"\n | \"onLoadingComplete\"\n | \"onLoadingError\"\n | \"onCacheHit\"\n | \"onCacheMiss\"\n | \"onCacheError\"\n>;\n\nexport interface ICache {\n getAudioBuffer(\n context: BaseContext,\n url: string,\n signal?: AbortSignal,\n callbacks?: CacheCallbacks,\n ): Promise<AudioBuffer>;\n clearMemoryCache(): void;\n}\n\nconst CACHE_NAME = \"audio-cache-v2\";\nconst POLICY_HEADER = \"x-cacophony-cache-policy\";\nconst MEMORY_BYTES = 64 * 1024 * 1024;\nconst REQUEST_HEADERS = { accept: \"*/*\" };\n\nfunction toError(error: unknown): Error {\n return error instanceof Error ? error : new Error(String(error));\n}\n\nfunction checkAbort(signal?: AbortSignal): void {\n if (signal?.aborted) throw new DOMException(\"Operation was aborted\", \"AbortError\");\n}\n\nclass ByteBoundedLRUCache<K, V> {\n private cachedBytes = 0;\n private readonly cache = new Map<K, { value: V; bytes: number }>();\n constructor(\n private readonly maxBytes: number,\n private readonly estimateBytes: (value: V) => number,\n ) {}\n\n get(key: K): V | undefined {\n const entry = this.cache.get(key);\n if (!entry) return undefined;\n this.cache.delete(key);\n this.cache.set(key, entry);\n return entry.value;\n }\n\n delete(key: K): void {\n this.cachedBytes -= this.cache.get(key)?.bytes ?? 0;\n this.cache.delete(key);\n }\n\n set(key: K, value: V): void {\n this.delete(key);\n const bytes = this.estimateBytes(value);\n if (bytes > this.maxBytes) return;\n while (this.cachedBytes + bytes > this.maxBytes) {\n const first = this.cache.keys().next();\n if (first.done) break;\n this.delete(first.value);\n }\n this.cache.set(key, { value, bytes });\n this.cachedBytes += bytes;\n }\n}\n\ninterface HttpEntry {\n policy: CachePolicy;\n status: number;\n cors: boolean;\n /** Origin headers before the explicitly configured fallback TTL is applied. */\n headers: Record<string, string>;\n /** Identical envelope attached to stored bytes and their decoded buffer. */\n metadata: string;\n}\n\ninterface MemoryEntry {\n buffer: AudioBuffer;\n http?: HttpEntry;\n}\ninterface Subscriber {\n callbacks?: CacheCallbacks;\n}\ninterface PendingRequest {\n controller: AbortController;\n subscribers: Set<Subscriber>;\n promise: Promise<AudioBuffer>;\n}\ntype Notify = (deliver: (callbacks: CacheCallbacks) => void) => void;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isHeaders(value: unknown): value is Record<string, string> {\n return isRecord(value) && Object.values(value).every((item) => typeof item === \"string\");\n}\n\nfunction normalizePolicyHeaders(headers: Record<string, string>): Record<string, string> {\n const normalized = { ...headers };\n if (normalized[\"cache-control\"]) {\n // Upstream 4.1.1 treats directive names as case-sensitive. Skip quoted\n // values (including embedded commas) so only directive names change case.\n normalized[\"cache-control\"] = normalized[\"cache-control\"].replace(\n /(\"(?:\\\\.|[^\"\\\\])*\")|(^|,)(\\s*[^=,\\s]+)/g,\n (_match, quoted: string | undefined, separator: string, name: string) => quoted ?? separator + name.toLowerCase(),\n );\n }\n return normalized;\n}\n\nfunction policyHeaders(headers: CachePolicy.Headers): Headers {\n const result = new Headers();\n for (const [key, value] of Object.entries(headers)) {\n if (value !== undefined) result.set(key, Array.isArray(value) ? value.join(\", \") : value);\n }\n return result;\n}\n\n/**\n * HTTP policy governs decoded memory entries and persistent response bytes.\n * Bytes and versioned policy share ONE Cache API response, preventing split\n * writes. Legacy v1 entries are not read. Memory is bounded per decoding context.\n */\nexport class AudioCache implements ICache {\n private static cacheExpirationTime = 24 * 60 * 60 * 1000;\n private static readonly cleanedStorage = new WeakSet<CacheStorage>();\n private decodedBuffers = new WeakMap<BaseContext, ByteBoundedLRUCache<string, MemoryEntry>>();\n private pendingRequests = new WeakMap<BaseContext, Map<string, PendingRequest>>();\n\n /**\n * Fallback for new responses with no cache directives, expiry, or validators.\n * Existing memory and persistent policies retain their original lifetime.\n * @throws {RangeError} If time is negative or non-finite.\n */\n static setCacheExpirationTime(time: number): void {\n if (!Number.isFinite(time) || time < 0) throw new RangeError(\"Cache expiration must be finite and non-negative\");\n AudioCache.cacheExpirationTime = time;\n }\n\n private static createEntry(\n request: Request,\n headers: Record<string, string>,\n status: number,\n cors = false,\n ): HttpEntry {\n const effectiveHeaders = normalizePolicyHeaders(headers);\n // Application default only: explicit directives, Expires and validators win.\n // All parsing and reuse decisions still belong to upstream CachePolicy.\n if (![\"cache-control\", \"expires\", \"pragma\", \"etag\", \"last-modified\"].some((key) => key in headers)) {\n effectiveHeaders[\"cache-control\"] = `max-age=${Math.floor(AudioCache.cacheExpirationTime / 1000)}`;\n }\n const requestHeaders = Object.fromEntries(request.headers);\n const policy = new CachePolicy(\n { url: request.url, method: \"GET\", headers: requestHeaders },\n { status, headers: effectiveHeaders },\n { shared: false, cacheHeuristic: 0, immutableMinTimeToLive: 0 },\n );\n const metadata = JSON.stringify({\n version: 2,\n status,\n cors,\n url: request.url,\n time: policy.toObject().t,\n requestHeaders,\n policyHeaders: effectiveHeaders,\n headers,\n });\n return { policy, status, headers, metadata, cors };\n }\n\n private static readEntry(response: Response, request: Request): HttpEntry | undefined {\n const metadata = response.headers.get(POLICY_HEADER);\n if (!metadata) return undefined;\n try {\n const value: unknown = JSON.parse(metadata);\n if (\n !isRecord(value) ||\n value.version !== 2 ||\n value.url !== request.url ||\n typeof value.time !== \"number\" ||\n !Number.isFinite(value.time) ||\n value.time > Date.now() ||\n !isHeaders(value.requestHeaders) ||\n !isHeaders(value.policyHeaders) ||\n !isHeaders(value.headers)\n )\n return undefined;\n // Reparse directives upstream, rather than trusting internal parsed fields\n // from persistent JSON. Restore time with the public serialization API.\n const serialized = new CachePolicy(\n { url: request.url, method: \"GET\", headers: value.requestHeaders },\n { status: response.status, headers: normalizePolicyHeaders(value.policyHeaders) },\n { shared: false, cacheHeuristic: 0, immutableMinTimeToLive: 0 },\n ).toObject();\n serialized.t = value.time;\n return {\n policy: CachePolicy.fromObject(serialized),\n status: response.status,\n headers: value.headers,\n metadata,\n cors: value.cors === true,\n };\n } catch {\n return undefined;\n }\n }\n\n private static canRetain(entry: HttpEntry): boolean {\n // Only Accept is explicitly controlled by this URL-only API. Cookie,\n // Accept-Language, etc. may be added or changed invisibly by the host.\n const vary =\n entry.headers.vary\n ?.split(\",\")\n .map((name) => name.trim().toLowerCase())\n .filter(Boolean) ?? [];\n return entry.policy.storable() && vary.every((name) => name === \"accept\");\n }\n\n private getMemory(context: BaseContext): ByteBoundedLRUCache<string, MemoryEntry> {\n let memory = this.decodedBuffers.get(context);\n if (!memory) {\n memory = new ByteBoundedLRUCache(MEMORY_BYTES, ({ buffer }) => buffer.length * buffer.numberOfChannels * 4);\n this.decodedBuffers.set(context, memory);\n }\n return memory;\n }\n\n private join(\n context: BaseContext,\n url: string,\n signal: AbortSignal | undefined,\n callbacks: CacheCallbacks | undefined,\n load: (signal: AbortSignal, notify: Notify) => Promise<AudioBuffer>,\n ): Promise<AudioBuffer> {\n checkAbort(signal);\n let requests = this.pendingRequests.get(context);\n if (!requests) {\n requests = new Map();\n this.pendingRequests.set(context, requests);\n }\n const subscriber = { callbacks };\n let pending = requests.get(url);\n if (!pending) {\n const controller = new AbortController();\n const subscribers = new Set<Subscriber>();\n const notify: Notify = (deliver) => {\n for (const current of subscribers) {\n try {\n if (current.callbacks) deliver(current.callbacks);\n } catch {\n // An observer must not interrupt the load or other subscribers.\n }\n }\n };\n const promise = Promise.resolve().then(() => load(controller.signal, notify));\n pending = { controller, subscribers, promise };\n requests.set(url, pending);\n }\n const active = pending;\n const requestMap = requests;\n active.subscribers.add(subscriber);\n return new Promise((resolve, reject) => {\n let settled = false;\n const release = () => {\n settled = true;\n signal?.removeEventListener(\"abort\", abort);\n active.subscribers.delete(subscriber);\n if (active.subscribers.size === 0 && requestMap.get(url) === active) requestMap.delete(url);\n };\n const abort = () => {\n if (settled) return;\n release();\n if (active.subscribers.size === 0) active.controller.abort();\n const error = new DOMException(\"Operation was aborted\", \"AbortError\");\n try {\n callbacks?.onLoadingError?.({ url, error, errorType: \"abort\", timestamp: Date.now() });\n } catch {\n // An observer must not prevent cancellation from settling.\n }\n reject(error);\n };\n signal?.addEventListener(\"abort\", abort, { once: true });\n active.promise.then(\n (buffer) => {\n if (!settled) {\n release();\n resolve(buffer);\n }\n },\n (error: unknown) => {\n if (!settled) {\n release();\n reject(error);\n }\n },\n );\n });\n }\n\n private static cacheError(notify: Notify, url: string, error: unknown, operation: \"get\" | \"set\" | \"delete\"): void {\n notify((callbacks) => callbacks.onCacheError?.({ url, error: toError(error), operation, timestamp: Date.now() }));\n }\n\n private static async openCache(notify: Notify, url: string): Promise<Cache | undefined> {\n if (typeof caches === \"undefined\" || caches === null) return undefined;\n if (!AudioCache.cleanedStorage.has(caches)) {\n AudioCache.cleanedStorage.add(caches);\n try {\n await caches.delete(\"audio-cache\");\n } catch (error) {\n AudioCache.cacheError(notify, url, error, \"delete\");\n }\n }\n try {\n return await caches.open(CACHE_NAME);\n } catch (error) {\n AudioCache.cacheError(notify, url, error, \"get\");\n return undefined;\n }\n }\n\n private static async deleteResponse(\n cache: Cache | undefined,\n request: Request,\n notify: Notify,\n url: string,\n ): Promise<void> {\n if (!cache) return;\n try {\n await cache.delete(request, { ignoreVary: true });\n } catch (error) {\n AudioCache.cacheError(notify, url, error, \"delete\");\n }\n }\n\n private static async storeResponse(\n cache: Cache | undefined,\n request: Request,\n bytes: ArrayBuffer,\n entry: HttpEntry,\n notify: Notify,\n url: string,\n ): Promise<void> {\n if (!cache) return;\n const headers = new Headers(entry.headers);\n // Fetch already decoded content coding; stored bytes are uncompressed.\n headers.delete(\"content-encoding\");\n headers.set(\"content-length\", String(bytes.byteLength));\n headers.set(POLICY_HEADER, entry.metadata);\n try {\n await cache.put(request, new Response(bytes, { status: entry.status, headers }));\n } catch (error) {\n await AudioCache.deleteResponse(cache, request, notify, url);\n AudioCache.cacheError(notify, url, error, \"set\");\n }\n }\n\n private static async readBytes(\n response: Response,\n signal: AbortSignal,\n notify: Notify,\n url: string,\n ): Promise<ArrayBuffer> {\n if (!response.body) return response.arrayBuffer();\n const contentLength = Number(response.headers.get(\"content-length\"));\n const total =\n Number.isFinite(contentLength) && contentLength > 0 && !response.headers.has(\"content-encoding\")\n ? contentLength\n : null;\n const reader = response.body.getReader();\n let bytes = new Uint8Array(total ?? 8192);\n let loaded = 0;\n const abort = () => {\n void reader.cancel().catch(() => {});\n };\n signal.addEventListener(\"abort\", abort, { once: true });\n const progress = (done: boolean) => {\n const timestamp = Date.now();\n notify((callbacks) =>\n callbacks.onLoadingProgress?.({\n url,\n loaded,\n total,\n progress: total ? (done ? 1 : Math.min(1, loaded / total)) : -1,\n timestamp,\n }),\n );\n };\n try {\n while (true) {\n checkAbort(signal);\n const next = await reader.read();\n checkAbort(signal);\n if (next.done) break;\n if (loaded + next.value.byteLength > bytes.length) {\n const grown = new Uint8Array(Math.max(bytes.length * 2, loaded + next.value.byteLength));\n grown.set(bytes.subarray(0, loaded));\n bytes = grown;\n }\n bytes.set(next.value, loaded);\n loaded += next.value.byteLength;\n progress(false);\n }\n progress(true);\n return bytes.slice(0, loaded).buffer;\n } finally {\n signal.removeEventListener(\"abort\", abort);\n reader.releaseLock();\n }\n }\n\n private async loadHttp(\n context: BaseContext,\n url: string,\n memory: ByteBoundedLRUCache<string, MemoryEntry>,\n signal: AbortSignal,\n notify: Notify,\n ): Promise<AudioBuffer> {\n let errorType: \"network\" | \"decode\" = \"network\";\n try {\n checkAbort(signal);\n const request = new Request(url, { headers: REQUEST_HEADERS });\n const policyRequest = { url: request.url, method: \"GET\", headers: Object.fromEntries(request.headers) };\n let cached = memory.get(url);\n if (\n cached?.http &&\n AudioCache.canRetain(cached.http) &&\n cached.http.policy.satisfiesWithoutRevalidation(policyRequest)\n ) {\n notify((callbacks) => callbacks.onCacheHit?.({ url, cacheType: \"memory\", timestamp: Date.now() }));\n return cached.buffer;\n }\n const persistent = await AudioCache.openCache(notify, url);\n let stored: Response | undefined;\n let entry = cached?.http;\n if (persistent) {\n try {\n stored = await persistent.match(request);\n if (stored) {\n const storedEntry = AudioCache.readEntry(stored, request);\n if (!storedEntry || !AudioCache.canRetain(storedEntry)) {\n stored = undefined;\n await AudioCache.deleteResponse(persistent, request, notify, url);\n } else if (entry?.metadata !== storedEntry.metadata) {\n // Never combine a different persisted representation with an older\n // decoded buffer (another context/cache instance may have written).\n entry = storedEntry;\n cached = undefined;\n }\n }\n } catch (error) {\n AudioCache.cacheError(notify, url, error, \"get\");\n }\n }\n checkAbort(signal);\n const readStoredBytes = async (response: Response): Promise<ArrayBuffer | undefined> => {\n try {\n return await response.arrayBuffer();\n } catch (error) {\n AudioCache.cacheError(notify, url, error, \"get\");\n memory.delete(url);\n cached = undefined;\n entry = undefined;\n stored = undefined;\n await AudioCache.deleteResponse(persistent, request, notify, url);\n return undefined;\n }\n };\n if (entry && stored && entry.policy.satisfiesWithoutRevalidation(policyRequest)) {\n const bytes = await readStoredBytes(stored);\n checkAbort(signal);\n if (bytes) {\n errorType = \"decode\";\n let buffer: AudioBuffer;\n try {\n buffer = await context.decodeAudioData(bytes);\n } catch (error) {\n await AudioCache.deleteResponse(persistent, request, notify, url);\n throw error;\n }\n checkAbort(signal);\n memory.set(url, { buffer, http: entry });\n notify((callbacks) => callbacks.onCacheHit?.({ url, cacheType: \"browser\", timestamp: Date.now() }));\n return buffer;\n }\n }\n notify((callbacks) =>\n callbacks.onCacheMiss?.({ url, reason: entry ? \"expired\" : \"not-found\", timestamp: Date.now() }),\n );\n const cors = entry?.cors || (typeof location !== \"undefined\" && new URL(request.url).origin !== location.origin);\n // Browser-generated conditional headers do not require CORS preflight.\n // Let Fetch validate cross-origin entries, including hidden validators.\n const headers = entry && !cors ? policyHeaders(entry.policy.revalidationHeaders(policyRequest)) : request.headers;\n let response = await fetch(url, { headers, signal, cache: entry ? \"no-cache\" : \"default\" });\n checkAbort(signal);\n let bytes: ArrayBuffer | undefined;\n let buffer: AudioBuffer | undefined;\n let conditional = false;\n if (response.status === 304 && entry && (stored || cached)) {\n const update = entry.policy.revalidatedPolicy(policyRequest, {\n status: 304,\n headers: normalizePolicyHeaders(Object.fromEntries(response.headers)),\n });\n if (update.matches && !update.modified) {\n const merged = { ...entry.headers };\n // Include NEW origin header names: upstream 4.1.1's merge only carries\n // names that were already in the old response. No directive parsing.\n response.headers.forEach((value, key) => {\n if (![\"content-length\", \"content-encoding\", POLICY_HEADER].includes(key)) merged[key] = value;\n });\n merged.date = response.headers.get(\"date\") ?? new Date().toUTCString();\n merged.age = response.headers.get(\"age\") ?? \"0\";\n entry = AudioCache.createEntry(request, merged, entry.status, entry.cors);\n bytes = stored ? await readStoredBytes(stored) : undefined;\n buffer = cached?.buffer;\n conditional = bytes !== undefined || buffer !== undefined;\n }\n }\n if (response.status === 304 && !conditional) {\n checkAbort(signal);\n response = await fetch(url, { headers: request.headers, signal, cache: \"reload\" });\n checkAbort(signal);\n }\n if (!conditional) {\n // Partial and bodyless successes cannot provide a complete audio file.\n if (!response.ok || [204, 205, 206].includes(response.status))\n throw new Error(`Failed to fetch resource: ${response.status} ${response.statusText}`);\n const originHeaders = Object.fromEntries(response.headers);\n delete originHeaders[POLICY_HEADER];\n entry = AudioCache.createEntry(request, originHeaders, response.status, response.type === \"cors\");\n bytes = await AudioCache.readBytes(response, signal, notify, url);\n }\n checkAbort(signal);\n if (!entry) throw new Error(\"Missing response policy\");\n const retain = AudioCache.canRetain(entry);\n memory.delete(url);\n if (!retain) await AudioCache.deleteResponse(persistent, request, notify, url);\n if (!buffer) {\n if (!bytes) throw new Error(\"Missing response body\");\n const byteLength = bytes.byteLength;\n // Native decoding can detach its input buffer.\n const persistentBytes = retain && persistent ? bytes.slice(0) : undefined;\n errorType = \"decode\";\n try {\n buffer = await context.decodeAudioData(bytes);\n } catch (error) {\n console.error(\"Failed to decode audio data:\", error);\n throw error;\n }\n checkAbort(signal);\n if (persistentBytes) await AudioCache.storeResponse(persistent, request, persistentBytes, entry, notify, url);\n const decoded = buffer;\n notify((callbacks) =>\n callbacks.onLoadingComplete?.({ url, duration: decoded.duration, size: byteLength, timestamp: Date.now() }),\n );\n } else if (retain && bytes) {\n await AudioCache.storeResponse(persistent, request, bytes, entry, notify, url);\n }\n checkAbort(signal);\n if (retain) memory.set(url, { buffer, http: entry });\n if (conditional)\n notify((callbacks) => callbacks.onCacheHit?.({ url, cacheType: \"conditional\", timestamp: Date.now() }));\n return buffer;\n } catch (error) {\n notify((callbacks) =>\n callbacks.onLoadingError?.({ url, error: toError(error), errorType, timestamp: Date.now() }),\n );\n throw error;\n }\n }\n\n async getAudioBuffer(\n context: BaseContext,\n url: string,\n signal?: AbortSignal,\n callbacks?: CacheCallbacks,\n ): Promise<AudioBuffer> {\n checkAbort(signal);\n callbacks?.onLoadingStart?.({ url, timestamp: Date.now() });\n const memory = this.getMemory(context);\n if (url.startsWith(\"data:\")) {\n const hit = memory.get(url);\n if (hit) {\n callbacks?.onCacheHit?.({ url, cacheType: \"memory\", timestamp: Date.now() });\n return hit.buffer;\n }\n try {\n const comma = url.indexOf(\",\");\n if (comma < 0) throw new Error(\"Malformed data URL\");\n const header = url.slice(5, comma);\n const payload = url.slice(comma + 1);\n const decoded = /;base64$/i.test(header) ? atob(payload) : decodeURIComponent(payload);\n const bytes = Uint8Array.from(decoded, (character) => character.charCodeAt(0));\n const buffer = await context.decodeAudioData(bytes.buffer);\n checkAbort(signal);\n memory.set(url, { buffer });\n return buffer;\n } catch (error) {\n callbacks?.onLoadingError?.({\n url,\n error: toError(error),\n errorType: signal?.aborted ? \"abort\" : \"decode\",\n timestamp: Date.now(),\n });\n throw error;\n }\n }\n return this.join(context, url, signal, callbacks, (runSignal, notify) =>\n this.loadHttp(context, url, memory, runSignal, notify),\n );\n }\n\n clearMemoryCache(): void {\n this.decodedBuffers = new WeakMap();\n this.pendingRequests = new WeakMap();\n }\n}\n"],"mappings":"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"}