cacophony 0.21.0 → 0.24.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 (149) hide show
  1. package/README.md +214 -71
  2. package/dist/autoplayUnlock.d.ts +28 -0
  3. package/dist/bus.d.ts +117 -0
  4. package/dist/cache.d.ts +23 -24
  5. package/dist/cacophony.d.ts +553 -11
  6. package/dist/container.d.ts +43 -16
  7. package/dist/context.d.ts +15 -0
  8. package/dist/effects.d.ts +544 -0
  9. package/dist/eventEmitter.d.ts +17 -1
  10. package/dist/events.d.ts +35 -31
  11. package/dist/filters.d.ts +2 -4
  12. package/dist/group.d.ts +8 -1
  13. package/dist/index.cjs +1 -1
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.ts +21 -6
  16. package/dist/index.mjs +3349 -1048
  17. package/dist/index.mjs.map +1 -1
  18. package/dist/mediaStream.d.ts +5 -6
  19. package/dist/meters/loudness-core.d.ts +179 -0
  20. package/dist/meters/loudness-core.test.d.ts +1 -0
  21. package/dist/meters/loudness-meter.d.ts +60 -0
  22. package/dist/meters/loudness-meter.test.d.ts +1 -0
  23. package/dist/meters/truepeak-core.d.ts +104 -0
  24. package/dist/meters/truepeak-core.test.d.ts +1 -0
  25. package/dist/microphone.d.ts +2 -4
  26. package/dist/pannerMixin.d.ts +34 -6
  27. package/dist/playback.d.ts +56 -9
  28. package/dist/sound.d.ts +119 -8
  29. package/dist/spatial/foa-encode.d.ts +37 -0
  30. package/dist/stream.d.ts +16 -2
  31. package/dist/synth.d.ts +26 -15
  32. package/docs/assets/navigation.js +1 -1
  33. package/docs/assets/search.js +1 -1
  34. package/docs/classes/AudioCache.html +17 -11
  35. package/docs/classes/BiquadEffect.html +8 -0
  36. package/docs/classes/Bus.html +79 -0
  37. package/docs/classes/Cacophony.html +414 -18
  38. package/docs/classes/DynamicsEffect.html +11 -0
  39. package/docs/classes/FdnReverbEffect.html +12 -0
  40. package/docs/classes/FoaDecoder.html +79 -0
  41. package/docs/classes/Group.html +18 -14
  42. package/docs/classes/KWeightingFilter.html +10 -0
  43. package/docs/classes/LoudnessMeter.html +39 -0
  44. package/docs/classes/MediaStreamPlayback.html +25 -27
  45. package/docs/classes/MediaStreamSound.html +31 -25
  46. package/docs/classes/MicrophonePlayback.html +3 -4
  47. package/docs/classes/ModulatedDelayEffect.html +12 -0
  48. package/docs/classes/PhaserEffect.html +11 -0
  49. package/docs/classes/Playback.html +81 -50
  50. package/docs/classes/ReverbEffect.html +15 -0
  51. package/docs/classes/ShareEffect.html +10 -0
  52. package/docs/classes/Sound.html +120 -28
  53. package/docs/classes/Synth.html +43 -25
  54. package/docs/classes/SynthGroup.html +2 -2
  55. package/docs/classes/TremoloEffect.html +11 -0
  56. package/docs/classes/TruePeakDetector.html +29 -0
  57. package/docs/classes/WaveshaperEffect.html +12 -0
  58. package/docs/functions/encodeMonoToFoaSN3D.html +31 -0
  59. package/docs/functions/integratedLoudness.html +12 -0
  60. package/docs/functions/integratedUngatedLoudness.html +7 -0
  61. package/docs/functions/loudnessRange.html +7 -0
  62. package/docs/functions/timeStretch.html +20 -0
  63. package/docs/functions/timeStretchChannels.html +4 -0
  64. package/docs/functions/truePeakDb.html +4 -0
  65. package/docs/hierarchy.html +1 -1
  66. package/docs/index.html +74 -6
  67. package/docs/interfaces/AudioBuffer.html +2 -2
  68. package/docs/interfaces/AudioBufferSourceNode.html +4 -4
  69. package/docs/interfaces/AudioEventCallbacks.html +11 -11
  70. package/docs/interfaces/AudioListener.html +2 -2
  71. package/docs/interfaces/AudioNode.html +3 -3
  72. package/docs/interfaces/AudioParam.html +2 -2
  73. package/docs/interfaces/AudioWorklet.html +2 -0
  74. package/docs/interfaces/AudioWorkletNode.html +9 -4
  75. package/docs/interfaces/BaseContext.html +4 -2
  76. package/docs/interfaces/BaseSound.html +2 -2
  77. package/docs/interfaces/BiquadCoefficients.html +8 -0
  78. package/docs/interfaces/BiquadFilterNode.html +4 -4
  79. package/docs/interfaces/CacheErrorEvent.html +2 -2
  80. package/docs/interfaces/CacheHitEvent.html +2 -2
  81. package/docs/interfaces/CacheMissEvent.html +2 -2
  82. package/docs/interfaces/CacophonyEffect.html +7 -0
  83. package/docs/interfaces/ChannelMergerNode.html +3 -3
  84. package/docs/interfaces/ChannelSplitterNode.html +3 -3
  85. package/docs/interfaces/DynamicsOptions.html +21 -0
  86. package/docs/interfaces/FadeStartEvent.html +2 -2
  87. package/docs/interfaces/FdnReverbOptions.html +19 -0
  88. package/docs/interfaces/FoaDecoderOptions.html +10 -0
  89. package/docs/interfaces/GainNode.html +3 -3
  90. package/docs/interfaces/GlobalPlaybackEvent.html +2 -2
  91. package/docs/interfaces/LoadingCompleteEvent.html +2 -2
  92. package/docs/interfaces/LoadingErrorEvent.html +2 -2
  93. package/docs/interfaces/LoadingProgressEvent.html +2 -2
  94. package/docs/interfaces/LoadingStartEvent.html +2 -2
  95. package/docs/interfaces/LoudnessChannelInput.html +4 -0
  96. package/docs/interfaces/LoudnessReading.html +10 -0
  97. package/docs/interfaces/MediaElementSourceNode.html +3 -3
  98. package/docs/interfaces/MediaStreamAudioSourceNode.html +3 -3
  99. package/docs/interfaces/MediaStreamSoundOptions.html +2 -2
  100. package/docs/interfaces/ModulatedDelayOptions.html +27 -0
  101. package/docs/interfaces/OfflineOptions.html +2 -2
  102. package/docs/interfaces/OscillatorNode.html +4 -4
  103. package/docs/interfaces/PannerNode.html +3 -3
  104. package/docs/interfaces/PhaserOptions.html +23 -0
  105. package/docs/interfaces/PlayOptions.html +2 -2
  106. package/docs/interfaces/PlaybackErrorEvent.html +2 -2
  107. package/docs/interfaces/ReverbOptions.html +31 -0
  108. package/docs/interfaces/RuntimeOptions.html +15 -2
  109. package/docs/interfaces/SoundCleanupHoldings.html +2 -2
  110. package/docs/interfaces/SoundErrorEvent.html +2 -2
  111. package/docs/interfaces/StereoPannerNode.html +3 -3
  112. package/docs/interfaces/TimeStretchOptions.html +16 -0
  113. package/docs/interfaces/TremoloOptions.html +21 -0
  114. package/docs/interfaces/WaveshaperOptions.html +18 -0
  115. package/docs/modules.html +51 -8
  116. package/docs/types/BaseAudioEvents.html +1 -0
  117. package/docs/types/BusConnectionTarget.html +4 -0
  118. package/docs/types/CacheEventCallback.html +2 -2
  119. package/docs/types/CacophonyEvents.html +6 -0
  120. package/docs/types/ErrorEventCallback.html +2 -2
  121. package/docs/types/FadeType.html +1 -1
  122. package/docs/types/HrtfPannerOptions.html +3 -0
  123. package/docs/types/LoadingEventCallback.html +2 -2
  124. package/docs/types/LoopCount.html +1 -1
  125. package/docs/types/LoudnessChannel.html +4 -0
  126. package/docs/types/Orientation.html +1 -1
  127. package/docs/types/PanCloneOverrides.html +1 -0
  128. package/docs/types/PanType.html +1 -1
  129. package/docs/types/PlaybackEvents.html +2 -0
  130. package/docs/types/Position.html +1 -1
  131. package/docs/types/SoundEvents.html +2 -0
  132. package/docs/types/SoundType.html +1 -0
  133. package/docs/types/SourceNode.html +1 -1
  134. package/docs/types/SynthEvents.html +2 -0
  135. package/docs/types/ThreeDOptions.html +14 -0
  136. package/docs/variables/CHANNEL_WEIGHTS.html +4 -0
  137. package/docs/variables/K_WEIGHTING_STAGE1_48K.html +4 -0
  138. package/docs/variables/K_WEIGHTING_STAGE2_48K.html +3 -0
  139. package/package.json +6 -2
  140. package/dist/basePlayback.d.ts +0 -81
  141. package/dist/oscillatorMixin.d.ts +0 -53
  142. package/dist/synthPlayback.d.ts +0 -69
  143. package/dist/volumeMixin.d.ts +0 -58
  144. package/docs/enums/SoundType.html +0 -5
  145. package/docs/interfaces/BaseAudioEvents.html +0 -11
  146. package/docs/interfaces/CacophonyEvents.html +0 -17
  147. package/docs/interfaces/PlaybackEvents.html +0 -13
  148. package/docs/interfaces/SoundEvents.html +0 -15
  149. package/docs/interfaces/SynthEvents.html +0 -15
package/README.md CHANGED
@@ -13,7 +13,7 @@ Cacophony is a powerful and intuitive audio library designed for modern web appl
13
13
  - **Synthesizer Integration**: Create and manipulate synthesized sounds with customizable oscillator options
14
14
  - **Efficient Group Management**: Organize and control multiple sounds or synthesizers as groups for streamlined audio management
15
15
  - **Live Microphone Input**: Capture and process real-time audio input from the user's microphone
16
- - **Network-Backed Playback**: Play audio directly from URLs using media-element-backed sounds
16
+ - **Network-Backed Playback**: Play audio directly from URLs using media-element-backed sounds
17
17
  - **Flexible Caching**: Implement efficient audio caching strategies for improved performance
18
18
 
19
19
  ## Installation
@@ -55,6 +55,43 @@ async function audioDemo() {
55
55
  audioDemo();
56
56
  ```
57
57
 
58
+ ## Mobile Autoplay Handling
59
+
60
+ Modern browsers — especially iOS Safari and Chrome on Android — refuse to
61
+ produce sound from an `AudioContext` that was constructed before any user
62
+ interaction. The context is created in `suspended` state and must be both
63
+ resumed AND have a node started from inside a real user-gesture call stack
64
+ before audio can play. Calling `context.resume()` alone is not enough on iOS.
65
+
66
+ Cacophony handles this transparently by default. When you construct a
67
+ `Cacophony` instance and the context is suspended, it installs one-time
68
+ `touchend` / `click` / `keydown` listeners on `document.body`. The first
69
+ user gesture resumes the context, plays a 1-sample silent primer buffer (the
70
+ iOS unlock primer), removes the listeners, and emits the `unlock` event.
71
+
72
+ ```typescript
73
+ const cacophony = new Cacophony();
74
+
75
+ if (cacophony.locked) {
76
+ // Mobile: waiting for the first user interaction.
77
+ // Show a "tap to enable audio" affordance if you want one.
78
+ }
79
+
80
+ cacophony.on('unlock', () => {
81
+ // Audio is now usable.
82
+ });
83
+ ```
84
+
85
+ If you prefer to manage unlock yourself, opt out:
86
+
87
+ ```typescript
88
+ const cacophony = new Cacophony(undefined, undefined, { autoUnlock: false });
89
+ // You are responsible for calling cacophony.resume() inside a user gesture.
90
+ ```
91
+
92
+ The auto-unlock has no effect on offline contexts or in non-browser
93
+ environments (`typeof document === 'undefined'`).
94
+
58
95
  ## Core Concepts
59
96
 
60
97
  ### Sound vs Playback Architecture
@@ -92,25 +129,59 @@ sound.stop(); // Stops all three playbacks
92
129
 
93
130
  ## Sound Types
94
131
 
95
- Cacophony supports three sound types:
132
+ Cacophony supports three sound types:
96
133
 
97
- | Type | Memory | Latency | Seeking | Multiple Instances | Best For |
98
- |------|--------|---------|---------|-------------------|----------|
99
- | **Buffer** (default) | High | None | Full | Yes | Sound effects, UI sounds, short music clips |
100
- | **HTML** | Medium | Low | Full | Yes | Background music, large audio files, podcasts |
101
- | **Streaming** | Medium | Low | Full | Yes | Network-backed playback created via `createStream()` |
134
+ | Type | Memory | Latency | Seeking | Multiple Instances | Best For |
135
+ |------|--------|---------|---------|-------------------|----------|
136
+ | **Buffer** (default) | High | None | Full | Yes | Sound effects, UI sounds, short music clips |
137
+ | **HTML** | Medium | Low | Full | Yes | Background music, large audio files, podcasts |
138
+ | **Streaming** | Medium | Low | Full | Yes | Network-backed playback created via `createStream()` |
102
139
 
103
140
  ```typescript
104
141
  // Buffer - entire file loaded into memory
105
- const sfx = await cacophony.createSound('explosion.mp3', SoundType.Buffer);
142
+ const sfx = await cacophony.createSound('explosion.mp3', 'buffer');
106
143
 
107
144
  // HTML - streams from network, good for large files
108
- const music = await cacophony.createSound('bgm.mp3', SoundType.HTML);
145
+ const music = await cacophony.createSound('bgm.mp3', 'html');
109
146
 
110
- // Streaming - convenience helper for network-backed playback
111
- const radio = await cacophony.createStream('https://example.com/stream.m3u8');
147
+ // Streaming - convenience helper for network-backed playback
148
+ const radio = await cacophony.createStream('https://example.com/stream.m3u8');
112
149
  ```
113
150
 
151
+ ### Format Fallback (Howler-style)
152
+
153
+ Pass an array of URLs and Cacophony picks the first one the browser can play.
154
+ It queries `HTMLAudioElement.canPlayType` per extension and fetches only the
155
+ chosen source (cache and loading events fire only for that URL). If the
156
+ selected source's `canPlayType` was `'maybe'` and decoding actually fails,
157
+ the next playable candidate is tried.
158
+
159
+ ```typescript
160
+ // First playable source wins. Cacophony fetches only that one.
161
+ const boom = await cacophony.createSound([
162
+ 'sfx/boom.webm', // small/modern, preferred when supported
163
+ 'sfx/boom.mp3', // universal fallback
164
+ 'sfx/boom.wav', // last-resort uncompressed
165
+ ]);
166
+ boom.play();
167
+ ```
168
+
169
+ Recognised extensions: `.webm`, `.mp3`, `.ogg`, `.wav`, `.flac`, `.m4a`,
170
+ `.aac`, `.opus`. If no candidate is reported playable, or every playable
171
+ candidate fails to decode, the promise rejects with an error naming the
172
+ URLs that were tried and the reason each failed (codec unsupported or
173
+ decode failure).
174
+
175
+ Only decode failures advance to the next candidate. Fetch/network/cache
176
+ failures of the selected source propagate immediately so the caller sees
177
+ the real cause instead of a silent format swap. Decode failures are
178
+ detected by the Web Audio spec marker -- `DOMException` with name
179
+ `EncodingError`.
180
+
181
+ v1 limitation: format fallback is only available for the default `'buffer'`
182
+ sound type. Passing an array together with `'html'` or `'streaming'`
183
+ rejects with a "not yet supported" error.
184
+
114
185
  ## Playback Control
115
186
 
116
187
  ### Seeking
@@ -269,26 +340,98 @@ playback.connect(distortion)
269
340
 
270
341
  ### Parallel Effects (Send/Return)
271
342
 
272
- Create parallel effect sends like a mixing console:
343
+ For mixing-console-style routing named buses, shared effects, and
344
+ per-edge send gain — use the first-class [Buses and Sends](#buses-and-sends)
345
+ API documented below. The Bus class supersedes the older user-built
346
+ `playback.connect()` send/return pattern.
347
+
348
+ ## Buses and Sends
349
+
350
+ A `Bus` is a named summing node with its own filter chain and per-edge
351
+ gain on outgoing connections. Sounds and synths route to buses via
352
+ `routeTo`; buses can carry rich effects (not just BiquadFilter) by
353
+ adding a `CacophonyEffect` to their filter chain. The built-in
354
+ `cacophony.createReverb()` returns a DattorroReverb effect ready to drop
355
+ into a bus.
273
356
 
274
357
  ```typescript
275
- const sound = await cacophony.createSound('vocals.mp3');
276
- const [playback] = sound.play();
358
+ // 1. Create a named bus
359
+ const reverbBus = cacophony.createBus('reverb');
277
360
 
278
- // Create send effects
279
- const reverb = cacophony.context.createConvolver();
280
- const reverbReturn = cacophony.context.createGain();
281
- reverbReturn.gain.value = 0.3; // 30% wet
361
+ // 2. Add a DattorroReverb effect to the bus
362
+ const reverb = cacophony.createReverb({ wet: 0.6, dry: 0.4, decay: 0.7 });
363
+ await reverbBus.addFilter(reverb);
282
364
 
283
- // Dry signal to destination
284
- playback.connect(cacophony.context.destination);
365
+ // 3. Lower the bus's level a bit before it hits master
366
+ reverbBus.gain = 0.5;
285
367
 
286
- // Parallel wet signal: playback reverb reverbReturn → destination
287
- playback.connect(reverb)
288
- .connect(reverbReturn)
289
- .connect(cacophony.context.destination);
368
+ // 4. Route one sound's primary output to the bus
369
+ const vocals = await cacophony.createSound('vocals.mp3');
370
+ vocals.routeTo(reverbBus);
371
+ vocals.play();
372
+
373
+ // 5. Send another sound to the bus at 30% (primary route still goes to master)
374
+ const drums = await cacophony.createSound('drums.mp3');
375
+ drums.routeTo(reverbBus, 0.3);
376
+ drums.play();
290
377
  ```
291
378
 
379
+ ### Looking up buses by name
380
+
381
+ ```typescript
382
+ const fx = cacophony.createBus('fx');
383
+ cacophony.getBus('fx'); // → same Bus instance
384
+ cacophony.listBuses(); // → ['master', 'fx']
385
+ sound.routeTo('fx'); // string lookup via the registry
386
+ ```
387
+
388
+ ### The master bus
389
+
390
+ `cacophony.master` is the built-in master bus. Its `input` is literally
391
+ the same node as `cacophony.globalGainNode`, so the existing
392
+ `cacophony.volume` and `cacophony.mute` APIs continue to work
393
+ transparently. Routing a sound to `cacophony.master` (or never calling
394
+ `routeTo`) sends it through the master path.
395
+
396
+ ### Bus-to-bus routing with per-edge gain
397
+
398
+ ```typescript
399
+ const groupBus = cacophony.createBus('group');
400
+ const sendBus = cacophony.createBus('aux');
401
+ groupBus.connect(sendBus, 0.2); // 20% send: groupBus.output → sendGain(0.2) → sendBus.input
402
+ groupBus.disconnect(sendBus); // tears down the sendGain too
403
+ ```
404
+
405
+ ### Adding custom effects to a bus
406
+
407
+ Bus filter chains accept Cacophony-built BiquadFilters and any
408
+ `CacophonyEffect`. Raw third-party AudioNodes are rejected unless you
409
+ wrap them explicitly with `cacophony.shareEffect(node)` — this surfaces
410
+ the shared-state intent (the same node will run on every bus that adds it).
411
+
412
+ ```typescript
413
+ const eq = cacophony.createBiquadFilter({ type: 'highshelf', frequency: 4000, gain: 3 });
414
+ await bus.addFilter(eq);
415
+
416
+ const sharedWorklet = new AudioWorkletNode(cacophony.context, 'my-fx');
417
+ await bus.addFilter(cacophony.shareEffect(sharedWorklet));
418
+ ```
419
+
420
+ ### Cleaning up
421
+
422
+ ```typescript
423
+ reverbBus.destroy(); // disconnects everything, deregisters the name
424
+ ```
425
+
426
+ Sounds still routed to a destroyed bus fall back to master on their
427
+ next playback with a `console.warn`.
428
+
429
+ ### Legacy: user-built send/return
430
+
431
+ Before buses existed, send/return was a manual `playback.connect()`
432
+ chain (see the [Custom Audio Routing](#custom-audio-routing) section
433
+ for the low-level pattern). New code should use a Bus and `routeTo`.
434
+
292
435
  ### Dynamic Routing
293
436
 
294
437
  Change routing in real-time:
@@ -441,28 +584,28 @@ const footsteps = await cacophony.createGroupFromUrls([
441
584
 
442
585
  footsteps.playRandom(); // Picks one at random
443
586
 
444
- // Advance through sounds in sequence, one call at a time
445
- const dialog = await cacophony.createGroupFromUrls(['line1.mp3', 'line2.mp3', 'line3.mp3']);
446
- dialog.playOrdered(true); // Plays line1, then advances internal order
447
- dialog.playOrdered(true); // Plays line2
448
- dialog.playOrdered(true); // Plays line3
449
- dialog.playOrdered(true); // Plays line1 again because looping is enabled
450
-
451
- // SynthGroup for synthesizers
452
- const synthGroup = new SynthGroup();
453
- const synth1 = cacophony.createOscillator({ frequency: 440, type: 'sine' });
454
- const synth2 = cacophony.createOscillator({ frequency: 660, type: 'square' });
455
- synthGroup.addSynth(synth1);
456
- synthGroup.addSynth(synth2);
457
- synthGroup.play();
458
- synthGroup.volume = 0.5;
459
- synthGroup.type = 'triangle';
460
- synthGroup.pause();
461
- synthGroup.resume();
462
-
463
- // Remove a synth from the group
464
- synthGroup.removeSynth(synth1);
465
- ```
587
+ // Advance through sounds in sequence, one call at a time
588
+ const dialog = await cacophony.createGroupFromUrls(['line1.mp3', 'line2.mp3', 'line3.mp3']);
589
+ dialog.playOrdered(true); // Plays line1, then advances internal order
590
+ dialog.playOrdered(true); // Plays line2
591
+ dialog.playOrdered(true); // Plays line3
592
+ dialog.playOrdered(true); // Plays line1 again because looping is enabled
593
+
594
+ // SynthGroup for synthesizers
595
+ const synthGroup = new SynthGroup();
596
+ const synth1 = cacophony.createOscillator({ frequency: 440, type: 'sine' });
597
+ const synth2 = cacophony.createOscillator({ frequency: 660, type: 'square' });
598
+ synthGroup.addSynth(synth1);
599
+ synthGroup.addSynth(synth2);
600
+ synthGroup.play();
601
+ synthGroup.volume = 0.5;
602
+ synthGroup.type = 'triangle';
603
+ synthGroup.pause();
604
+ synthGroup.resume();
605
+
606
+ // Remove a synth from the group
607
+ synthGroup.removeSynth(synth1);
608
+ ```
466
609
 
467
610
  ## Synthesizer Functionality
468
611
 
@@ -489,22 +632,22 @@ bass.addFilter(cacophony.createBiquadFilter({ type: 'lowpass', frequency: 1000 }
489
632
  bass.play();
490
633
  lead.play();
491
634
 
492
- // Modulate frequency over time
493
- let time = 0;
494
- setInterval(() => {
495
- const frequency = 440 + Math.sin(time) * 100;
496
- sineOsc.frequency = frequency;
497
- time += 0.1;
498
- }, 50);
499
-
500
- // Pause and resume the active synth playback without losing its settings
501
- sineOsc.pause();
502
- sineOsc.resume();
503
- ```
504
-
505
- `synth.pause()` keeps the existing synth playback object so `synth.resume()` can restart it with the current frequency, detune, type, volume, pan, and filter settings. `synth.play()` still creates a fresh playback instance, just like `Sound.play()`.
506
-
507
- ## 3D Audio Positioning
635
+ // Modulate frequency over time
636
+ let time = 0;
637
+ setInterval(() => {
638
+ const frequency = 440 + Math.sin(time) * 100;
639
+ sineOsc.frequency = frequency;
640
+ time += 0.1;
641
+ }, 50);
642
+
643
+ // Pause and resume the active synth playback without losing its settings
644
+ sineOsc.pause();
645
+ sineOsc.resume();
646
+ ```
647
+
648
+ `synth.pause()` keeps the existing synth playback object so `synth.resume()` can restart it with the current frequency, detune, type, volume, pan, and filter settings. `synth.play()` still creates a fresh playback instance, just like `Sound.play()`.
649
+
650
+ ## 3D Audio Positioning
508
651
 
509
652
  Create immersive soundscapes with precise spatial audio control using HRTF (Head-Related Transfer Function) or stereo panning.
510
653
 
@@ -512,9 +655,9 @@ Create immersive soundscapes with precise spatial audio control using HRTF (Head
512
655
  const cacophony = new Cacophony();
513
656
 
514
657
  // Create sounds with HRTF panning
515
- const ambience = await cacophony.createSound('forest_ambience.mp3', SoundType.Buffer, 'HRTF');
516
- const birdSound = await cacophony.createSound('bird_chirp.mp3', SoundType.Buffer, 'HRTF');
517
- const footsteps = await cacophony.createSound('footsteps.mp3', SoundType.Buffer, 'HRTF');
658
+ const ambience = await cacophony.createSound('forest_ambience.mp3', 'buffer', 'HRTF');
659
+ const birdSound = await cacophony.createSound('bird_chirp.mp3', 'buffer', 'HRTF');
660
+ const footsteps = await cacophony.createSound('footsteps.mp3', 'buffer', 'HRTF');
518
661
 
519
662
  // Position sounds in 3D space
520
663
  // Coordinate system: X (left- to right+), Y (down- to up+), Z (front+ to back-)
@@ -550,7 +693,7 @@ setInterval(() => {
550
693
  }, 50);
551
694
 
552
695
  // Stereo panning (simple left-right)
553
- const stereoSound = await cacophony.createSound('audio.mp3', SoundType.Buffer, 'stereo');
696
+ const stereoSound = await cacophony.createSound('audio.mp3', 'buffer', 'stereo');
554
697
  stereoSound.stereoPan = 0.5; // -1 (left) to 1 (right)
555
698
  stereoSound.play();
556
699
  ```
@@ -838,7 +981,7 @@ const controller = new AbortController();
838
981
 
839
982
  const soundPromise = cacophony.createSound(
840
983
  'large-file.mp3',
841
- SoundType.Buffer,
984
+ 'buffer',
842
985
  'HRTF',
843
986
  controller.signal
844
987
  );
@@ -858,7 +1001,7 @@ try {
858
1001
  // Works with groups too
859
1002
  const group = await cacophony.createGroupFromUrls(
860
1003
  ['a.mp3', 'b.mp3', 'c.mp3'],
861
- SoundType.Buffer,
1004
+ 'buffer',
862
1005
  'HRTF',
863
1006
  controller.signal
864
1007
  );
@@ -869,9 +1012,9 @@ const group = await cacophony.createGroupFromUrls(
869
1012
  Call `cleanup()` when done with sounds to free resources:
870
1013
 
871
1014
  ```typescript
872
- const sound = await cacophony.createSound('temp.mp3');
873
- sound.play();
874
- sound.cleanup(); // Tears down playbacks, including pausing and resetting active HTML/streaming media
1015
+ const sound = await cacophony.createSound('temp.mp3');
1016
+ sound.play();
1017
+ sound.cleanup(); // Tears down playbacks, including pausing and resetting active HTML/streaming media
875
1018
 
876
1019
  // Clear memory cache
877
1020
  cacophony.clearMemoryCache();
@@ -0,0 +1,28 @@
1
+ import { BaseContext } from './context';
2
+ /**
3
+ * Options for `installAutoplayUnlock`.
4
+ */
5
+ export interface AutoplayUnlockOptions {
6
+ /**
7
+ * The audio context that should be resumed and primed.
8
+ */
9
+ context: BaseContext;
10
+ /**
11
+ * Called after the context has been resumed and the primer buffer has been
12
+ * started. Used by `Cacophony` to emit the public `unlock` event.
13
+ * Errors thrown by the callback are caught and logged so a faulty listener
14
+ * cannot break the unlock flow.
15
+ */
16
+ onUnlock: () => void;
17
+ }
18
+ /**
19
+ * Install one-time unlock listeners. Returns a cleanup function that removes
20
+ * the listeners if they have not already fired (e.g. when a `Cacophony`
21
+ * instance is torn down before the user interacts).
22
+ *
23
+ * If the environment is non-browser (`document` is undefined) or the context
24
+ * is not in `suspended` state, this is a no-op and returns a no-op cleanup.
25
+ *
26
+ * @internal
27
+ */
28
+ export declare function installAutoplayUnlock(opts: AutoplayUnlockOptions): () => void;
package/dist/bus.d.ts ADDED
@@ -0,0 +1,117 @@
1
+ import { AudioNode, BaseContext, BiquadFilterNode, GainNode } from './context';
2
+ import { CacophonyEffect } from './effects';
3
+ /**
4
+ * Connection target for {@link Bus.connect} / {@link Bus.disconnect}. Either
5
+ * another Bus (we connect into its `input`) or a raw AudioNode (we connect
6
+ * directly to it — escape hatch for advanced wiring).
7
+ */
8
+ export type BusConnectionTarget = Bus | AudioNode;
9
+ /**
10
+ * A named summing node with a filter chain and per-edge send gain. See
11
+ * module-level docstring for topology.
12
+ */
13
+ export declare class Bus {
14
+ /** Stable name for registry lookup, or null for anonymous buses. */
15
+ readonly name: string | null;
16
+ /**
17
+ * Entry node — connect upstream sources here (sound playbacks, other bus
18
+ * outputs).
19
+ */
20
+ readonly input: GainNode;
21
+ /**
22
+ * Exit node — connected to downstream targets (other bus inputs, master,
23
+ * raw nodes) via {@link connect}.
24
+ */
25
+ readonly output: GainNode;
26
+ private readonly _context;
27
+ private readonly _filterNodes;
28
+ private readonly _filterChainEdges;
29
+ private readonly _sendGains;
30
+ private readonly _directConnections;
31
+ /**
32
+ * Hook invoked by the owning Cacophony instance to remove this bus from
33
+ * the named-bus registry on destroy. Anonymous buses leave this undefined.
34
+ */
35
+ private readonly _onDestroy?;
36
+ private _destroyed;
37
+ /**
38
+ * @param context Web Audio context the bus's nodes live on.
39
+ * @param name Name to register under, or null for an anonymous bus.
40
+ * @param input Optional pre-existing GainNode to use as the input. Used by
41
+ * the `master` bus to alias `cacophony.globalGainNode`. If omitted, a
42
+ * fresh GainNode is allocated.
43
+ * @param onDestroy Optional registry-cleanup hook fired by destroy().
44
+ */
45
+ constructor(context: BaseContext, name?: string | null, input?: GainNode, onDestroy?: () => void);
46
+ /** True after {@link destroy} has been called. */
47
+ get destroyed(): boolean;
48
+ /** Output node gain — controls the overall level the bus sends downstream. */
49
+ get gain(): number;
50
+ set gain(v: number);
51
+ /** Live filter chain (read-only view). */
52
+ get filters(): readonly AudioNode[];
53
+ /**
54
+ * Add a filter node to the bus's chain. Accepts:
55
+ *
56
+ * - A Cacophony-built BiquadFilterNode (created via
57
+ * `cacophony.createBiquadFilter`) → added directly to the chain.
58
+ * - A {@link CacophonyEffect} → `build(context)` is awaited; the resulting
59
+ * node is added to the chain.
60
+ * - A raw third-party AudioNode → REJECTED. Wrap it with
61
+ * `cacophony.shareEffect(node)` (or a proper CacophonyEffect class) to
62
+ * make the shared-state intent explicit.
63
+ *
64
+ * @throws if the bus has been destroyed, or if the argument is a raw
65
+ * AudioNode that is not a Cacophony-built biquad.
66
+ */
67
+ addFilter(arg: BiquadFilterNode | CacophonyEffect | AudioNode): Promise<void>;
68
+ /**
69
+ * Remove a filter node from the bus's chain. The node must have been added
70
+ * via {@link addFilter}; the same object identity is used to match.
71
+ *
72
+ * @throws if the bus has been destroyed or if the node was never added.
73
+ */
74
+ removeFilter(node: AudioNode): void;
75
+ /**
76
+ * Connect this bus's output to another bus or to a raw AudioNode.
77
+ *
78
+ * If `gain` is omitted or equal to 1, connect directly (output →
79
+ * targetInput). If `gain` is provided, allocate an internal GainNode for
80
+ * per-edge attenuation: output → sendGain → targetInput. The sendGain is
81
+ * tracked so {@link disconnect} can tear it down cleanly.
82
+ *
83
+ * Re-connecting a target that is already wired is a no-op for the direct
84
+ * case; for a gained connection, the existing sendGain's `gain.value` is
85
+ * updated in place (no new edge is allocated).
86
+ *
87
+ * @throws if the bus has been destroyed.
88
+ */
89
+ connect(target: BusConnectionTarget, gain?: number): void;
90
+ /**
91
+ * Disconnect this bus's output from a target previously connected with
92
+ * {@link connect}. Tears down the allocated sendGain (if any). No-op if
93
+ * the target was never connected.
94
+ *
95
+ * @throws if the bus has been destroyed.
96
+ */
97
+ disconnect(target: BusConnectionTarget): void;
98
+ /**
99
+ * Tear down the bus — disconnects input, output, every send-gain, every
100
+ * filter, then deregisters from the owner Cacophony's named-bus map.
101
+ * Subsequent `addFilter`/`removeFilter`/`connect`/`disconnect` calls throw.
102
+ *
103
+ * Sounds routed to a destroyed bus fall back to master on their next
104
+ * playback (the routeTo machinery checks `destroyed` at preplay).
105
+ */
106
+ destroy(): void;
107
+ /**
108
+ * Rebuild the chain `input → [filter1 → ... → filterN] → output`. Called
109
+ * after any add/remove of a filter. Disconnects only the internal chain
110
+ * edges this bus created, then reapplies the chain. The output node's edges
111
+ * to downstream targets are not touched.
112
+ */
113
+ private _refreshFilters;
114
+ private _connectFilterChainEdge;
115
+ private _disconnectFilterChainEdges;
116
+ private _throwIfDestroyed;
117
+ }
package/dist/cache.d.ts CHANGED
@@ -1,14 +1,13 @@
1
1
  import { BaseContext } from './context';
2
+ import { AudioEventCallbacks } from './events';
3
+ /**
4
+ * Subset of {@link AudioEventCallbacks} relevant to {@link AudioCache} public API.
5
+ * Lets callers opt in to any combination of loading/cache events without
6
+ * having to import the full union.
7
+ */
8
+ export type CacheCallbacks = Pick<AudioEventCallbacks, "onLoadingStart" | "onLoadingProgress" | "onLoadingComplete" | "onLoadingError" | "onCacheHit" | "onCacheMiss" | "onCacheError">;
2
9
  export interface ICache {
3
- getAudioBuffer(context: BaseContext, url: string, signal?: AbortSignal, callbacks?: {
4
- onLoadingStart?: (event: any) => void;
5
- onLoadingProgress?: (event: any) => void;
6
- onLoadingComplete?: (event: any) => void;
7
- onLoadingError?: (event: any) => void;
8
- onCacheHit?: (event: any) => void;
9
- onCacheMiss?: (event: any) => void;
10
- onCacheError?: (event: any) => void;
11
- }): Promise<AudioBuffer>;
10
+ getAudioBuffer(context: BaseContext, url: string, signal?: AbortSignal, callbacks?: CacheCallbacks): Promise<AudioBuffer>;
12
11
  clearMemoryCache(): void;
13
12
  }
14
13
  /**
@@ -41,10 +40,9 @@ export declare class AudioCache implements ICache {
41
40
  static setCacheExpirationTime(time: number): void;
42
41
  private static openCache;
43
42
  /**
44
- * Calls all registered callbacks for a specific event type on a URL
45
- * @param url - The URL to get callbacks for
46
- * @param callbackName - Name of the callback method to invoke
47
- * @param eventData - Data to pass to the callbacks
43
+ * Calls all registered callbacks for a specific event type on a URL.
44
+ * Generic over the callback name so the payload type is checked against
45
+ * the canonical {@link CacheCallbacks} shape rather than `any`.
48
46
  */
49
47
  private static callAllCallbacks;
50
48
  private static getOrCreatePendingRequest;
@@ -52,10 +50,19 @@ export declare class AudioCache implements ICache {
52
50
  private static getBufferFromCache;
53
51
  private static fetchAndCacheBuffer;
54
52
  /**
55
- * Creates a ReadableStream wrapper that tracks download progress
56
- * Uses the callback aggregation system to emit progress to all registered listeners
53
+ * Atomically write a response body and its metadata into the cache.
54
+ * On failure, deletes both partial entries (best-effort via `allSettled`)
55
+ * and rethrows the original error.
56
+ */
57
+ private static writeBufferAndMetadata;
58
+ /**
59
+ * Creates a ReadableStream wrapper that tracks download progress.
60
+ * Uses the callback aggregation system to emit progress to all registered listeners.
61
+ * Honours `signal` between reads and releases the underlying reader on
62
+ * any exit path (done, error, abort).
57
63
  * @param response - The fetch Response object with ReadableStream body
58
64
  * @param url - URL being downloaded (for progress event data and callback lookup)
65
+ * @param signal - Optional AbortSignal observed at chunk boundaries
59
66
  * @returns Object containing the progress-tracking stream and total size
60
67
  */
61
68
  private static createProgressTrackingStream;
@@ -85,14 +92,6 @@ export declare class AudioCache implements ICache {
85
92
  * @returns Promise that resolves to decoded AudioBuffer
86
93
  * @throws Error if audio cannot be fetched or decoded
87
94
  */
88
- getAudioBuffer(context: BaseContext, url: string, signal?: AbortSignal, callbacks?: {
89
- onLoadingStart?: (event: any) => void;
90
- onLoadingProgress?: (event: any) => void;
91
- onLoadingComplete?: (event: any) => void;
92
- onLoadingError?: (event: any) => void;
93
- onCacheHit?: (event: any) => void;
94
- onCacheMiss?: (event: any) => void;
95
- onCacheError?: (event: any) => void;
96
- }): Promise<AudioBuffer>;
95
+ getAudioBuffer(context: BaseContext, url: string, signal?: AbortSignal, callbacks?: CacheCallbacks): Promise<AudioBuffer>;
97
96
  clearMemoryCache(): void;
98
97
  }