cacophony 0.25.0 → 0.28.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 (128) hide show
  1. package/README.md +47 -16
  2. package/dist/bus.d.ts +180 -8
  3. package/dist/cacophony.d.ts +52 -155
  4. package/dist/effects.d.ts +167 -123
  5. package/dist/index.cjs +1 -1
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.ts +3 -3
  8. package/dist/index.mjs +1395 -1065
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/mediaStream.d.ts +25 -1
  11. package/dist/sound.d.ts +8 -2
  12. package/dist/synth.d.ts +3 -2
  13. package/dist/worklets.d.ts +58 -0
  14. package/docs/assets/navigation.js +1 -1
  15. package/docs/assets/search.js +1 -1
  16. package/docs/classes/AudioCache.html +7 -7
  17. package/docs/classes/BiquadEffect.html +2 -2
  18. package/docs/classes/Bus.html +138 -22
  19. package/docs/classes/Cacophony.html +86 -165
  20. package/docs/classes/DynamicsEffect.html +8 -5
  21. package/docs/classes/FdnReverbEffect.html +8 -5
  22. package/docs/classes/FoaDecoder.html +15 -16
  23. package/docs/classes/FoaDecoderEffect.html +8 -0
  24. package/docs/classes/Group.html +13 -13
  25. package/docs/classes/ImpulseResponseEffect.html +10 -0
  26. package/docs/classes/KWeightingFilter.html +3 -3
  27. package/docs/classes/LoudnessMeter.html +11 -11
  28. package/docs/classes/MediaStreamPlayback.html +24 -18
  29. package/docs/classes/MediaStreamSound.html +20 -19
  30. package/docs/classes/MicrophonePlayback.html +3 -3
  31. package/docs/classes/ModulatedDelayEffect.html +6 -5
  32. package/docs/classes/PhaserEffect.html +8 -5
  33. package/docs/classes/Playback.html +36 -36
  34. package/docs/classes/ReverbEffect.html +8 -5
  35. package/docs/classes/ShareEffect.html +2 -2
  36. package/docs/classes/Sound.html +36 -32
  37. package/docs/classes/Synth.html +22 -21
  38. package/docs/classes/SynthGroup.html +2 -2
  39. package/docs/classes/TremoloEffect.html +6 -5
  40. package/docs/classes/TruePeakDetector.html +7 -7
  41. package/docs/classes/WaveshaperEffect.html +6 -5
  42. package/docs/functions/encodeMonoToFoaSN3D.html +1 -1
  43. package/docs/functions/integratedLoudness.html +1 -1
  44. package/docs/functions/integratedUngatedLoudness.html +1 -1
  45. package/docs/functions/loudnessRange.html +1 -1
  46. package/docs/functions/timeStretch.html +1 -1
  47. package/docs/functions/timeStretchChannels.html +1 -1
  48. package/docs/functions/truePeakDb.html +1 -1
  49. package/docs/hierarchy.html +1 -1
  50. package/docs/index.html +17 -4
  51. package/docs/interfaces/AudioBuffer.html +2 -2
  52. package/docs/interfaces/AudioBufferSourceNode.html +4 -4
  53. package/docs/interfaces/AudioEventCallbacks.html +1 -1
  54. package/docs/interfaces/AudioListener.html +2 -2
  55. package/docs/interfaces/AudioNode.html +3 -3
  56. package/docs/interfaces/AudioParam.html +2 -2
  57. package/docs/interfaces/AudioWorklet.html +2 -2
  58. package/docs/interfaces/AudioWorkletNode.html +4 -4
  59. package/docs/interfaces/BaseContext.html +2 -2
  60. package/docs/interfaces/BaseSound.html +2 -2
  61. package/docs/interfaces/BiquadCoefficients.html +2 -2
  62. package/docs/interfaces/BiquadFilterNode.html +4 -4
  63. package/docs/interfaces/BuiltEffectGraph.html +10 -0
  64. package/docs/interfaces/CacheErrorEvent.html +2 -2
  65. package/docs/interfaces/CacheHitEvent.html +2 -2
  66. package/docs/interfaces/CacheMissEvent.html +2 -2
  67. package/docs/interfaces/CacophonyEffect.html +2 -2
  68. package/docs/interfaces/ChannelMergerNode.html +3 -3
  69. package/docs/interfaces/ChannelSplitterNode.html +3 -3
  70. package/docs/interfaces/ConvolverNode.html +26 -0
  71. package/docs/interfaces/DynamicsOptions.html +7 -7
  72. package/docs/interfaces/FadeStartEvent.html +2 -2
  73. package/docs/interfaces/FdnReverbOptions.html +6 -6
  74. package/docs/interfaces/FoaDecoderOptions.html +2 -2
  75. package/docs/interfaces/GainNode.html +3 -3
  76. package/docs/interfaces/GlobalPlaybackEvent.html +2 -2
  77. package/docs/interfaces/ImpulseResponseOptions.html +9 -0
  78. package/docs/interfaces/LoadingCompleteEvent.html +2 -2
  79. package/docs/interfaces/LoadingErrorEvent.html +2 -2
  80. package/docs/interfaces/LoadingProgressEvent.html +2 -2
  81. package/docs/interfaces/LoadingStartEvent.html +2 -2
  82. package/docs/interfaces/LoudnessChannelInput.html +2 -2
  83. package/docs/interfaces/LoudnessReading.html +5 -5
  84. package/docs/interfaces/MediaElementSourceNode.html +3 -3
  85. package/docs/interfaces/MediaStreamAudioSourceNode.html +3 -3
  86. package/docs/interfaces/MediaStreamSoundOptions.html +13 -2
  87. package/docs/interfaces/ModulatedDelayOptions.html +10 -8
  88. package/docs/interfaces/OfflineOptions.html +2 -2
  89. package/docs/interfaces/OscillatorNode.html +4 -4
  90. package/docs/interfaces/PannerNode.html +3 -3
  91. package/docs/interfaces/PhaserOptions.html +7 -7
  92. package/docs/interfaces/PlayOptions.html +2 -2
  93. package/docs/interfaces/PlaybackErrorEvent.html +2 -2
  94. package/docs/interfaces/ReverbOptions.html +13 -13
  95. package/docs/interfaces/RuntimeOptions.html +2 -2
  96. package/docs/interfaces/SoundCleanupHoldings.html +2 -2
  97. package/docs/interfaces/SoundErrorEvent.html +2 -2
  98. package/docs/interfaces/StereoPannerNode.html +3 -3
  99. package/docs/interfaces/TimeStretchOptions.html +5 -5
  100. package/docs/interfaces/TremoloOptions.html +7 -5
  101. package/docs/interfaces/WaveshaperOptions.html +8 -5
  102. package/docs/modules.html +7 -0
  103. package/docs/types/BaseAudioEvents.html +1 -1
  104. package/docs/types/BuiltEffect.html +1 -0
  105. package/docs/types/BusConnectionTarget.html +1 -1
  106. package/docs/types/CacheEventCallback.html +1 -1
  107. package/docs/types/CacophonyEvents.html +1 -1
  108. package/docs/types/ErrorEventCallback.html +1 -1
  109. package/docs/types/FadeType.html +1 -1
  110. package/docs/types/HrtfPannerOptions.html +1 -1
  111. package/docs/types/ImpulseResponseSource.html +1 -0
  112. package/docs/types/LoadingEventCallback.html +1 -1
  113. package/docs/types/LoopCount.html +1 -1
  114. package/docs/types/LoudnessChannel.html +1 -1
  115. package/docs/types/Orientation.html +1 -1
  116. package/docs/types/PanCloneOverrides.html +1 -1
  117. package/docs/types/PanType.html +1 -1
  118. package/docs/types/PlaybackEvents.html +1 -1
  119. package/docs/types/Position.html +1 -1
  120. package/docs/types/SoundEvents.html +1 -1
  121. package/docs/types/SoundType.html +1 -1
  122. package/docs/types/SourceNode.html +1 -1
  123. package/docs/types/SynthEvents.html +1 -1
  124. package/docs/types/ThreeDOptions.html +1 -1
  125. package/docs/variables/CHANNEL_WEIGHTS.html +1 -1
  126. package/docs/variables/K_WEIGHTING_STAGE1_48K.html +1 -1
  127. package/docs/variables/K_WEIGHTING_STAGE2_48K.html +1 -1
  128. package/package.json +2 -2
package/README.md CHANGED
@@ -350,9 +350,9 @@ API documented below. The Bus class supersedes the older user-built
350
350
  A `Bus` is a named summing node with its own filter chain and per-edge
351
351
  gain on outgoing connections. Sounds and synths route to buses via
352
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.
353
+ adding a `CacophonyEffect` to their filter chain. The built-in
354
+ `cacophony.createReverb()` and `cacophony.createImpulseResponse()` effects
355
+ are ready to drop into a bus.
356
356
 
357
357
  ```typescript
358
358
  // 1. Create a named bus
@@ -393,29 +393,60 @@ the same node as `cacophony.globalGainNode`, so the existing
393
393
  transparently. Routing a sound to `cacophony.master` (or never calling
394
394
  `routeTo`) sends it through the master path.
395
395
 
396
- ### Bus-to-bus routing with per-edge gain
396
+ ### Bus-to-bus routing with per-edge gain
397
397
 
398
398
  ```typescript
399
399
  const groupBus = cacophony.createBus('group');
400
400
  const sendBus = cacophony.createBus('aux');
401
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).
402
+ groupBus.disconnect(sendBus); // tears down the sendGain too
403
+ ```
404
+
405
+ ### Native impulse responses
406
+
407
+ `createImpulseResponse()` builds a native `ConvolverNode` effect from an
408
+ `AudioBuffer` or URL. URL-backed IRs are fetched and decoded once per audio
409
+ context and URL; failed loads are evicted so a later call can retry. Convolver
410
+ normalization defaults to `false`, preserving measured IR gain.
411
+
412
+ ```typescript
413
+ // Wet-only send bus: returns a single ConvolverNode internally.
414
+ const chamber = cacophony.createBus('chamber');
415
+ await chamber.addFilter(cacophony.createImpulseResponse('/irs/chamber.wav'));
416
+ vocals.routeTo(chamber, 0.25);
417
+
418
+ // Inline dry/wet graph: exposes dry/wet AudioParams for bus automation.
419
+ const room = cacophony.createImpulseResponse('/irs/room.wav', {
420
+ dry: 0.7,
421
+ wet: 0.3,
422
+ normalize: false,
423
+ });
424
+ const handle = await bus.addFilter(room);
425
+ bus.rampFilterParam(handle, 'wet', 0.6, { duration: 500 });
426
+ ```
427
+
428
+ ### Adding custom effects to a bus
429
+
430
+ Bus filter chains accept Cacophony-built BiquadFilters and any
431
+ `CacophonyEffect`. Raw third-party AudioNodes are rejected unless you
432
+ wrap them explicitly with `cacophony.shareEffect(node)` — this surfaces
433
+ the shared-state intent (the same node will run on every bus that adds it).
434
+ An effect can build either a single `AudioNode` or a `BuiltEffectGraph` with
435
+ separate `input`/`output` endpoints for multi-node processors.
411
436
 
412
437
  ```typescript
413
438
  const eq = cacophony.createBiquadFilter({ type: 'highshelf', frequency: 4000, gain: 3 });
414
439
  await bus.addFilter(eq);
415
440
 
416
- const sharedWorklet = new AudioWorkletNode(cacophony.context, 'my-fx');
417
- await bus.addFilter(cacophony.shareEffect(sharedWorklet));
418
- ```
441
+ const sharedWorklet = new AudioWorkletNode(cacophony.context, 'my-fx');
442
+ await bus.addFilter(cacophony.shareEffect(sharedWorklet));
443
+ ```
444
+
445
+ FOA binaural decoding is available in both forms. Use
446
+ `createFoaDecoder()` when you want explicit custom wiring through
447
+ `decoder.input` and `decoder.output`; use `createFoaDecoderEffect()` only on a
448
+ dedicated 4-channel ACN/SN3D FOA bus, typically as the first and only filter
449
+ that converts that bus to stereo binaural output.
419
450
 
420
451
  ### Cleaning up
421
452
 
package/dist/bus.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { FadeType } from './cacophony';
1
2
  import { AudioNode, BaseContext, BiquadFilterNode, GainNode } from './context';
2
3
  import { CacophonyEffect } from './effects';
3
4
  /**
@@ -6,6 +7,20 @@ import { CacophonyEffect } from './effects';
6
7
  * directly to it — escape hatch for advanced wiring).
7
8
  */
8
9
  export type BusConnectionTarget = Bus | AudioNode;
10
+ /**
11
+ * Structural contract a routed source (e.g. a {@link Sound}) implements so a
12
+ * Bus can move it off itself before teardown. Declared here — rather than
13
+ * importing `Sound` — to avoid an import cycle (`sound.ts` already imports
14
+ * `Bus`). A Bus only ever needs this one method on its inbound sources.
15
+ */
16
+ export interface BusRoutedSource {
17
+ /**
18
+ * Called by {@link Bus.drainTo} to move this source off the draining bus
19
+ * onto `target`. The source reroutes its primary route and/or any send that
20
+ * targeted `bus`.
21
+ */
22
+ _onBusDrained(bus: Bus, target: Bus): void;
23
+ }
9
24
  /**
10
25
  * A named summing node with a filter chain and per-edge send gain. See
11
26
  * module-level docstring for topology.
@@ -24,10 +39,26 @@ export declare class Bus {
24
39
  */
25
40
  readonly output: GainNode;
26
41
  private readonly _context;
27
- private readonly _filterNodes;
42
+ private readonly _filterEntries;
43
+ /**
44
+ * Filter nodes currently bypassed (skipped in the audible chain). A bypassed
45
+ * node stays in {@link _filterEntries} — so {@link filters} order, identity, and
46
+ * its live AudioParams are preserved — but {@link _desiredFilterChainEdges}
47
+ * builds the series chain over the NON-bypassed filters only, wiring the
48
+ * signal around it. Membership is by node identity.
49
+ */
50
+ private readonly _bypassedFilters;
28
51
  private readonly _filterChainEdges;
29
52
  private readonly _sendGains;
30
53
  private readonly _directConnections;
54
+ /**
55
+ * Inbound sources currently routed to this bus (primary route and/or sends).
56
+ * A Web Audio node cannot enumerate its own inputs, so sources register
57
+ * themselves here (via {@link _registerRoutedSource}) when they route to
58
+ * this bus and unregister on reroute/cleanup. {@link drainTo} walks this set
59
+ * to move live sounds off the bus before it is torn down.
60
+ */
61
+ private readonly _routedSources;
31
62
  /**
32
63
  * Hook invoked by the owning Cacophony instance to remove this bus from
33
64
  * the named-bus registry on destroy. Anonymous buses leave this undefined.
@@ -61,10 +92,15 @@ export declare class Bus {
61
92
  * `cacophony.shareEffect(node)` (or a proper CacophonyEffect class) to
62
93
  * make the shared-state intent explicit.
63
94
  *
95
+ * @returns the built AudioNode that was added to the chain. For a biquad this
96
+ * is the argument itself; for a {@link CacophonyEffect} it is the node
97
+ * produced by `build`. The returned handle can be passed to
98
+ * {@link rampFilterParam} to automate the node's parameters. Existing
99
+ * callers that ignore the result keep working unchanged.
64
100
  * @throws if the bus has been destroyed, or if the argument is a raw
65
101
  * AudioNode that is not a Cacophony-built biquad.
66
102
  */
67
- addFilter(arg: BiquadFilterNode | CacophonyEffect | AudioNode): Promise<void>;
103
+ addFilter(arg: BiquadFilterNode | CacophonyEffect | AudioNode): Promise<AudioNode>;
68
104
  /**
69
105
  * Remove a filter node from the bus's chain. The node must have been added
70
106
  * via {@link addFilter}; the same object identity is used to match.
@@ -72,6 +108,93 @@ export declare class Bus {
72
108
  * @throws if the bus has been destroyed or if the node was never added.
73
109
  */
74
110
  removeFilter(node: AudioNode): void;
111
+ /**
112
+ * Reorder the existing filter chain. `nodes` must be a PERMUTATION of the
113
+ * current filters — the same set of node objects (matched by identity), the
114
+ * same length, with no duplicates — just in a new order. Because
115
+ * {@link _refreshFilters} is incremental, only the edges that actually move
116
+ * are reconnected; unchanged edges are left untouched.
117
+ *
118
+ * @throws if the bus has been destroyed, or if `nodes` is not a permutation
119
+ * of the current filters.
120
+ */
121
+ setFilterOrder(nodes: readonly AudioNode[]): void;
122
+ /**
123
+ * Bypass (or un-bypass) a filter without removing it from the chain. A
124
+ * bypassed filter stays in {@link filters} — its order, identity, and live
125
+ * AudioParams are preserved (so an automation target survives a bypass) — but
126
+ * it is skipped in the audible series chain: the signal is wired around it.
127
+ * Un-bypassing wires it back in at its original position.
128
+ *
129
+ * The reconnect goes through the incremental {@link _refreshFilters}, so only
130
+ * the seam around `node` is touched — the rest of the chain is left connected.
131
+ *
132
+ * @param node A filter node currently on this bus (from {@link addFilter} or
133
+ * {@link filters}).
134
+ * @param bypassed `true` to skip the node, `false` to wire it back in. A no-op
135
+ * if the node is already in the requested state.
136
+ * @throws if the bus has been destroyed, or if `node` was never added to this
137
+ * bus.
138
+ */
139
+ setFilterBypassed(node: AudioNode, bypassed: boolean): void;
140
+ /**
141
+ * Whether `node` is currently bypassed (skipped in the audible chain). Returns
142
+ * `false` for nodes that were never added to this bus.
143
+ */
144
+ isFilterBypassed(node: AudioNode): boolean;
145
+ /**
146
+ * Ramp an effect node's parameter to a target value over time. This is the
147
+ * uniform automation handle for filter-chain effects: pass a node obtained
148
+ * from {@link addFilter} (or the {@link filters} getter) and the name of the
149
+ * parameter to drive.
150
+ *
151
+ * Parameter resolution:
152
+ * - If `node` exposes a worklet-style `parameters` AudioParamMap, the param
153
+ * is resolved via `parameters.get(paramName)` (e.g. a worklet effect's
154
+ * named params).
155
+ * - Otherwise, if `node[paramName]` is itself an AudioParam (native nodes
156
+ * such as a biquad expose `.frequency` / `.Q` / `.gain` directly), that is
157
+ * used.
158
+ *
159
+ * Ramp shape (mirrors the codebase fade convention): the target time base is
160
+ * `node.context.currentTime`. With no `duration` (or `duration <= 0`) the
161
+ * value is set immediately via `setValueAtTime(value, now)`. Otherwise the
162
+ * start is pinned with `setValueAtTime(param.value, now)` and the value ramps
163
+ * to `now + duration / 1000` (milliseconds) using `linearRampToValueAtTime`
164
+ * (default) or `exponentialRampToValueAtTime` when `type` is `"exponential"`
165
+ * (an exponential target of 0 is floored to 0.0001, matching `fadeTo`).
166
+ *
167
+ * Automation degrades gracefully: if `node` is not on this bus, or the
168
+ * parameter cannot be resolved to an AudioParam, a warning is logged and the
169
+ * call is a no-op. The only condition that throws is a destroyed bus.
170
+ *
171
+ * @param node A filter node currently on this bus (from {@link addFilter}).
172
+ * @param paramName The name of the parameter to automate.
173
+ * @param value The target value.
174
+ * @param options.duration Ramp duration in milliseconds. Absent/`<= 0` sets
175
+ * the value immediately.
176
+ * @param options.type Ramp curve, `"linear"` (default) or `"exponential"`.
177
+ * @throws if the bus has been destroyed.
178
+ */
179
+ rampFilterParam(node: AudioNode, paramName: string, value: number, options?: {
180
+ duration?: number;
181
+ type?: FadeType;
182
+ }): void;
183
+ /**
184
+ * Resolve the named {@link AudioParam} on a node. Tries the worklet
185
+ * `parameters` map first, then a directly-exposed native param
186
+ * (`node[paramName]`). Returns `undefined` if neither yields an AudioParam.
187
+ *
188
+ * Detection is structural (duck-typed), never `instanceof` — the mocked test
189
+ * context may not provide the `AudioParam` global.
190
+ */
191
+ private _resolveAudioParam;
192
+ /**
193
+ * Structural AudioParam check: a value is treated as an AudioParam if it
194
+ * exposes the ramp scheduling methods. Avoids `instanceof AudioParam` so it
195
+ * works under the standardized-audio-context mock (which may lack the global).
196
+ */
197
+ private _isAudioParam;
75
198
  /**
76
199
  * Connect this bus's output to another bus or to a raw AudioNode.
77
200
  *
@@ -95,22 +218,71 @@ export declare class Bus {
95
218
  * @throws if the bus has been destroyed.
96
219
  */
97
220
  disconnect(target: BusConnectionTarget): void;
221
+ /**
222
+ * Register an inbound source that routes to this bus (primary and/or send).
223
+ * Called by the source when it begins routing here. Idempotent (Set). Safe
224
+ * to call without a destroyed guard — registration during normal routing
225
+ * must never throw — but a destroyed bus has nothing to drain, so this
226
+ * early-returns once destroyed.
227
+ *
228
+ * @internal
229
+ */
230
+ _registerRoutedSource(source: BusRoutedSource): void;
231
+ /**
232
+ * Unregister an inbound source (it rerouted away or was cleaned up). No-op
233
+ * if the source was never registered.
234
+ *
235
+ * @internal
236
+ */
237
+ _unregisterRoutedSource(source: BusRoutedSource): void;
238
+ /**
239
+ * Move every source currently routed to this bus onto `target`, so live
240
+ * sounds keep feeding a live bus instead of the dead `input` after this bus
241
+ * is torn down. Each registered source's {@link BusRoutedSource._onBusDrained}
242
+ * reroutes its primary route and/or the send that targeted this bus.
243
+ *
244
+ * @throws if this bus has been destroyed, or if `target` is this bus.
245
+ */
246
+ drainTo(target: Bus): void;
98
247
  /**
99
248
  * Tear down the bus — disconnects input, output, every send-gain, every
100
249
  * filter, then deregisters from the owner Cacophony's named-bus map.
101
250
  * Subsequent `addFilter`/`removeFilter`/`connect`/`disconnect` calls throw.
102
251
  *
103
- * Sounds routed to a destroyed bus fall back to master on their next
252
+ * If `options.drainTo` is provided, every source routed to this bus is first
253
+ * rerouted onto that bus (via {@link drainTo}) so live sounds keep playing
254
+ * through a live bus. With no options the default teardown is unchanged:
255
+ * sounds routed to the destroyed bus fall back to master on their next
104
256
  * playback (the routeTo machinery checks `destroyed` at preplay).
105
257
  */
106
- destroy(): void;
258
+ destroy(options?: {
259
+ drainTo?: Bus;
260
+ }): void;
107
261
  /**
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.
262
+ * Reconcile the live chain to `input → [filter1 → ... → filterN] → output`.
263
+ * Called after any add/remove/reorder of a filter. This is an INCREMENTAL
264
+ * diff, not a full rebuild: it disconnects only edges that are no longer part
265
+ * of the desired chain and connects only edges that are newly required,
266
+ * leaving edges present in both the old and new chain connected and
267
+ * untouched (no audible click on the unchanged portion of the chain).
268
+ *
269
+ * Edges are matched by OBJECT IDENTITY on both endpoints. Only this bus's own
270
+ * internal `input → ... → output` edges are touched — never a broad
271
+ * `node.disconnect()`, never the outbound send/direct edges.
112
272
  */
113
273
  private _refreshFilters;
274
+ /**
275
+ * Compute the desired ordered chain edge list from the current
276
+ * `_filterEntries`, skipping any node in {@link _bypassedFilters}: the series
277
+ * chain is built over the NON-bypassed filters only. With no active (non-
278
+ * bypassed) filters — whether the bus has no filters at all or every filter is
279
+ * bypassed — the desired list is `[[input, output]]` (the direct edge);
280
+ * otherwise `[[input, a1], [a1, a2], ..., [aN, output]]` over the active
281
+ * filters `a1..aN`. Bypassed nodes stay in {@link _filterEntries} (and thus in
282
+ * {@link filters}) but receive no inbound/outbound chain edge.
283
+ */
284
+ private _desiredFilterChainEdges;
285
+ private _normalizeBuiltEffect;
114
286
  private _connectFilterChainEdge;
115
287
  private _disconnectFilterChainEdges;
116
288
  private _throwIfDestroyed;
@@ -1,7 +1,7 @@
1
1
  import { Bus } from './bus';
2
2
  import { ICache } from './cache';
3
3
  import { AudioBuffer, AudioListener, AudioNode, AudioWorkletNode, BaseContext, BiquadFilterNode, ChannelMergerNode, ChannelSplitterNode, GainNode, PannerNode } from './context';
4
- import { CacophonyEffect, DynamicsEffect, DynamicsOptions, FdnReverbEffect, FdnReverbOptions, FoaDecoder, FoaDecoderOptions, ModulatedDelayEffect, ModulatedDelayOptions, PhaserEffect, PhaserOptions, ReverbEffect, ReverbOptions, TremoloEffect, TremoloOptions, WaveshaperEffect, WaveshaperOptions } from './effects';
4
+ import { CacophonyEffect, DynamicsEffect, DynamicsOptions, FdnReverbEffect, FdnReverbOptions, FoaDecoder, FoaDecoderEffect, FoaDecoderOptions, ImpulseResponseEffect, ImpulseResponseOptions, ImpulseResponseSource, ModulatedDelayEffect, ModulatedDelayOptions, PhaserEffect, PhaserOptions, ReverbEffect, ReverbOptions, TremoloEffect, TremoloOptions, WaveshaperEffect, WaveshaperOptions } from './effects';
5
5
  import { CacophonyEvents } from './events';
6
6
  import { Group } from './group';
7
7
  import { MediaStreamSound, MediaStreamSoundOptions } from './mediaStream';
@@ -11,6 +11,7 @@ import { ThreeDOptions } from './pannerMixin';
11
11
  import { TimeStretchOptions } from './processors/timestretch-core';
12
12
  import { Sound } from './sound';
13
13
  import { Synth } from './synth';
14
+ import { WorkletModule } from './worklets';
14
15
  export type SoundType = "html" | "streaming" | "buffer" | "oscillator";
15
16
  /**
16
17
  * Represents a 3D position in space.
@@ -148,6 +149,12 @@ export declare class Cacophony {
148
149
  * `loadFoaHrir` calls share a single fetch/decode.
149
150
  */
150
151
  private foaHrirCache;
152
+ /**
153
+ * Per-context, per-URL decoded impulse-response cache. Stores in-flight
154
+ * promises so concurrent effect builds for the same context/URL share the
155
+ * same fetch/decode. Rejected promises are evicted so the caller can retry.
156
+ */
157
+ private impulseResponseCache;
151
158
  private finalizationRegistry;
152
159
  private eventEmitter;
153
160
  private cache;
@@ -253,162 +260,24 @@ export declare class Cacophony {
253
260
  off<K extends keyof CacophonyEvents>(event: K, listener: (data: CacophonyEvents[K]) => void): void;
254
261
  emit<K extends keyof CacophonyEvents>(event: K, data: CacophonyEvents[K]): void;
255
262
  emitAsync<K extends keyof CacophonyEvents>(event: K, data: CacophonyEvents[K]): Promise<void>;
256
- loadWorklets(signal?: AbortSignal): Promise<void>;
257
- loadStereoToBFormatWorklet(signal?: AbortSignal): Promise<void>;
258
- /**
259
- * Idempotently registers the `phase-vocoder` AudioWorkletProcessor (peak-based
260
- * pitch-shifter with Identity Phase-Locking, Laroche & Dolson 1999 WASPAA) on
261
- * this context. Safe to call repeatedly — subsequent calls short-circuit via
262
- * the per-context {@link loadedAudioWorklets} set. Cross-context: pass
263
- * `context` so a node built against a bus on a different context loads there.
264
- */
265
- loadPhaseVocoder(signal?: AbortSignal, context?: BaseContext): Promise<void>;
266
- /**
267
- * Constructs a `phase-vocoder` AudioWorkletNode. Caller is expected to have
268
- * loaded the module already (via {@link loadPhaseVocoder} or {@link loadWorklets}).
269
- * Uses the same construct/fallback path as {@link createWorkletNode}. The
270
- * returned node carries a single `pitchFactor` AudioParam (1 = no shift).
271
- */
272
- createPhaseVocoderNode(options?: AudioWorkletNodeOptions, context?: BaseContext): Promise<AudioWorkletNode>;
273
- /**
274
- * Idempotently registers the DattorroReverb AudioWorkletProcessor on this
275
- * context. Safe to call repeatedly — subsequent calls short-circuit via
276
- * the {@link loadedAudioWorklets} set used by
277
- * {@link loadAudioWorkletModule}.
278
- *
279
- * @param signal Optional abort signal forwarded to the module load.
280
- * @param context Optional BaseContext to load the worklet on. Defaults to
281
- * the host Cacophony instance's `context`. Supplied so a
282
- * {@link ReverbEffect} added to a bus whose context is NOT this host's
283
- * own (cross-context use) can load the worklet on the right context.
284
- */
285
- loadDattorroReverb(signal?: AbortSignal, context?: BaseContext): Promise<void>;
286
- /**
287
- * Constructs a DattorroReverb AudioWorkletNode. Caller is expected to have
288
- * loaded the module already (via {@link loadDattorroReverb} or by reaching
289
- * here through {@link ReverbEffect.build}). Uses the same construct/fallback
290
- * path as {@link createWorkletNode}.
291
- *
292
- * @param options AudioWorkletNode construction options.
293
- * @param context Optional BaseContext to construct on. Defaults to the
294
- * host Cacophony instance's `context`. See {@link loadDattorroReverb}
295
- * for the cross-context rationale.
296
- */
297
- createDattorroReverbNode(options: AudioWorkletNodeOptions, context?: BaseContext): Promise<AudioWorkletNode>;
298
- /**
299
- * Idempotently registers the `dynamics` AudioWorkletProcessor (feed-forward
300
- * compressor/limiter/expander/gate, Giannoulis 2012) on this context. Safe to
301
- * call repeatedly — subsequent calls short-circuit via the per-context
302
- * {@link loadedAudioWorklets} set. Cross-context: pass `context` so a
303
- * {@link DynamicsEffect} added to a bus on a different context loads there.
304
- */
305
- loadDynamics(signal?: AbortSignal, context?: BaseContext): Promise<void>;
306
- /**
307
- * Constructs a `dynamics` AudioWorkletNode. Caller is expected to have loaded
308
- * the module already (via {@link loadDynamics} or by reaching here through
309
- * {@link DynamicsEffect.build}). Uses the same construct/fallback path as
310
- * {@link createWorkletNode}.
311
- */
312
- createDynamicsNode(options: AudioWorkletNodeOptions, context?: BaseContext): Promise<AudioWorkletNode>;
313
- /**
314
- * Idempotently registers the `fdn-reverb` AudioWorkletProcessor (Feedback
315
- * Delay Network reverb — lossless paraunitary Hadamard feedback per Schlecht
316
- * & Habets 2019, per-line absorption T60 per Jot 1991, velvet-noise diffusion
317
- * per Fagerström 2020) on this context. Safe to call repeatedly — subsequent
318
- * calls short-circuit via the per-context {@link loadedAudioWorklets} set.
319
- * Cross-context: pass `context` so an {@link FdnReverbEffect} added to a bus
320
- * on a different context loads there.
321
- */
322
- loadFdnReverb(signal?: AbortSignal, context?: BaseContext): Promise<void>;
323
- /**
324
- * Constructs an `fdn-reverb` AudioWorkletNode. Caller is expected to have
325
- * loaded the module already (via {@link loadFdnReverb} or by reaching here
326
- * through {@link FdnReverbEffect.build}). Uses the same construct/fallback
327
- * path as {@link createWorkletNode}.
328
- */
329
- createFdnReverbNode(options: AudioWorkletNodeOptions, context?: BaseContext): Promise<AudioWorkletNode>;
330
- /**
331
- * Idempotently registers the `waveshaper` AudioWorkletProcessor (antialiased
332
- * distortion/waveshaper via first-order Antiderivative Antialiasing, Parker,
333
- * Zavalishin & Le Bivic 2016, DAFx-16) on this context. Safe to call
334
- * repeatedly — subsequent calls short-circuit via the per-context
335
- * {@link loadedAudioWorklets} set. Cross-context: pass `context` so a
336
- * {@link WaveshaperEffect} added to a bus on a different context loads there.
337
- */
338
- loadWaveshaper(signal?: AbortSignal, context?: BaseContext): Promise<void>;
339
- /**
340
- * Constructs a `waveshaper` AudioWorkletNode. Caller is expected to have
341
- * loaded the module already (via {@link loadWaveshaper} or by reaching here
342
- * through {@link WaveshaperEffect.build}). Uses the same construct/fallback
343
- * path as {@link createWorkletNode}.
344
- */
345
- createWaveshaperNode(options: AudioWorkletNodeOptions, context?: BaseContext): Promise<AudioWorkletNode>;
346
263
  /**
347
- * Idempotently registers the `modulated-delay` AudioWorkletProcessor
348
- * Dattorro's unified modulated-delay circuit (JAES 1997, Fig. 36) backing
349
- * delay/chorus/flanger/vibrato/doubling, with Lagrange FIR fractional-delay
350
- * interpolation (Laakso 1996) on this context. Safe to call repeatedly
351
- * subsequent calls short-circuit via the per-context {@link loadedAudioWorklets}
352
- * set. Cross-context: pass `context` so a {@link ModulatedDelayEffect} added to
353
- * a bus on a different context loads there.
264
+ * Eagerly registers every bundled AudioWorklet module on this context. This is
265
+ * OPTIONAL effects load their own worklet lazily in `build`, and the
266
+ * pitch-shift path loads the phase-vocoder on first use. Call this up front to
267
+ * pay the registration cost ahead of time. Idempotent per context (each module
268
+ * short-circuits via the per-context {@link loadedAudioWorklets} set).
354
269
  */
355
- loadModulatedDelay(signal?: AbortSignal, context?: BaseContext): Promise<void>;
356
- /**
357
- * Constructs a `modulated-delay` AudioWorkletNode. Caller is expected to have
358
- * loaded the module already (via {@link loadModulatedDelay} or by reaching here
359
- * through {@link ModulatedDelayEffect.build}). Uses the same construct/fallback
360
- * path as {@link createWorkletNode}.
361
- */
362
- createModulatedDelayNode(options: AudioWorkletNodeOptions, context?: BaseContext): Promise<AudioWorkletNode>;
363
- /**
364
- * Idempotently registers the `phaser` AudioWorkletProcessor — a classic
365
- * MXR/Univibe-style allpass-cascade phase shifter (Smith STAN-M-21; PASP §8.9:
366
- * a cascade of first-order allpass sections at a common LFO-swept break
367
- * frequency, summed additively with the dry signal to sweep notches) on this
368
- * context. Safe to call repeatedly — subsequent calls short-circuit via the
369
- * per-context {@link loadedAudioWorklets} set. Cross-context: pass `context` so
370
- * a {@link PhaserEffect} added to a bus on a different context loads there.
371
- */
372
- loadPhaser(signal?: AbortSignal, context?: BaseContext): Promise<void>;
373
- /**
374
- * Constructs a `phaser` AudioWorkletNode. Caller is expected to have loaded the
375
- * module already (via {@link loadPhaser} or by reaching here through
376
- * {@link PhaserEffect.build}). Uses the same construct/fallback path as
377
- * {@link createWorkletNode}.
378
- */
379
- createPhaserNode(options: AudioWorkletNodeOptions, context?: BaseContext): Promise<AudioWorkletNode>;
380
- /**
381
- * Idempotently registers the `tremolo` AudioWorkletProcessor — LFO-driven
382
- * amplitude modulation (a VCA swung by a low-frequency oscillator; standard AM
383
- * theory + Dattorro 1997 p.776 quadrature stereo LFO + Mitcheltree et al.
384
- * DAFx23 LFO framing) on this context. Safe to call repeatedly — subsequent
385
- * calls short-circuit via the per-context {@link loadedAudioWorklets} set.
386
- * Cross-context: pass `context` so a {@link TremoloEffect} added to a bus on a
387
- * different context loads there.
388
- */
389
- loadTremolo(signal?: AbortSignal, context?: BaseContext): Promise<void>;
390
- /**
391
- * Constructs a `tremolo` AudioWorkletNode. Caller is expected to have loaded the
392
- * module already (via {@link loadTremolo} or by reaching here through
393
- * {@link TremoloEffect.build}). Uses the same construct/fallback path as
394
- * {@link createWorkletNode}.
395
- */
396
- createTremoloNode(options: AudioWorkletNodeOptions, context?: BaseContext): Promise<AudioWorkletNode>;
397
- /**
398
- * Idempotently registers the `loudness-meter` AudioWorkletProcessor (ITU-R
399
- * BS.1770-5 momentary / short-term / integrated loudness + true-peak) on this
400
- * context. Safe to call repeatedly — subsequent calls short-circuit via the
401
- * per-context {@link loadedAudioWorklets} set. Cross-context: pass `context`
402
- * so a meter tapping a bus on a different context loads there.
403
- */
404
- loadLoudnessMeter(signal?: AbortSignal, context?: BaseContext): Promise<void>;
270
+ loadWorklets(signal?: AbortSignal): Promise<void>;
271
+ loadStereoToBFormatWorklet(signal?: AbortSignal): Promise<void>;
405
272
  /**
406
- * Constructs a `loudness-meter` AudioWorkletNode. Caller is expected to have
407
- * loaded the module already (via {@link loadLoudnessMeter} or {@link createLoudnessMeter}).
408
- * The node is a PASS-THROUGH metering tap (1 input, 1 output) that posts
409
- * momentary/short-term/integrated loudness and true-peak back over its port.
273
+ * The single worklet-effect construction seam ({@link WorkletEffectHost}).
274
+ * Idempotently registers `worklet`'s module on `context` or this host's own
275
+ * context when omitted, honoring the cross-context contract that effects'
276
+ * `build(context)` promises then constructs the AudioWorkletNode with
277
+ * `parameterData`. Every worklet-backed {@link CacophonyEffect} routes through
278
+ * here, as does the phase-vocoder pitch-shift path ({@link Playback.setPitchShift}).
410
279
  */
411
- createLoudnessMeterNode(options?: AudioWorkletNodeOptions, context?: BaseContext): Promise<AudioWorkletNode>;
280
+ buildWorkletEffect(worklet: WorkletModule, parameterData: Record<string, number>, context?: BaseContext): Promise<AudioWorkletNode>;
412
281
  /**
413
282
  * Fetches and decodes the bundled order-1 SH-HRIR
414
283
  * (`sh_hrir_order_1.wav`, from Omnitone, Apache-2.0 — see
@@ -426,6 +295,18 @@ export declare class Cacophony {
426
295
  * a different context decodes the HRIR on the right context.
427
296
  */
428
297
  loadFoaHrir(context?: BaseContext): Promise<AudioBuffer>;
298
+ /**
299
+ * Fetches and decodes an impulse response URL on the requested audio context,
300
+ * memoized per context and URL. Buffers decoded by one context are not reused
301
+ * on another context, matching Web Audio's context-bound decode behavior.
302
+ *
303
+ * @param url Impulse-response audio URL.
304
+ * @param context Optional decode context. Defaults to this Cacophony instance.
305
+ * @param signal Optional abort signal for the fetch. Decode itself is not
306
+ * abortable in Web Audio, but an already-aborted signal is honored before
307
+ * decode starts.
308
+ */
309
+ loadImpulseResponseBuffer(url: string, context?: BaseContext, signal?: AbortSignal): Promise<AudioBuffer>;
429
310
  createWorkletNode(name: string, url: string, signal?: AbortSignal, options?: AudioWorkletNodeOptions, context?: BaseContext): Promise<AudioWorkletNode>;
430
311
  createStereoToBFormatNode(signal?: AbortSignal): Promise<AudioWorkletNode>;
431
312
  /**
@@ -439,6 +320,7 @@ export declare class Cacophony {
439
320
  private markWorkletLoadedOn;
440
321
  private loadAudioWorkletModule;
441
322
  private createAbortError;
323
+ private waitForImpulseResponseLoad;
442
324
  private createMediaSound;
443
325
  clearMemoryCache(): void;
444
326
  createOscillator(options: OscillatorOptions, panType?: PanType): Synth;
@@ -529,6 +411,14 @@ export declare class Cacophony {
529
411
  * chain via `bus.addFilter(effect)`.
530
412
  */
531
413
  createReverb(options?: ReverbOptions): ReverbEffect;
414
+ /**
415
+ * Creates a native ConvolverNode impulse-response effect. Pass an AudioBuffer
416
+ * for an already-decoded IR or a URL to fetch/decode through the per-context
417
+ * IR cache. The default is wet-only (`dry: 0`, `wet: 1`) and returns a single
418
+ * ConvolverNode when added to a bus; setting `dry` or non-unity `wet` builds
419
+ * an owned dry/wet endpoint graph exposing `dry` and `wet` automation params.
420
+ */
421
+ createImpulseResponse(source: ImpulseResponseSource, options?: ImpulseResponseOptions): ImpulseResponseEffect;
532
422
  /**
533
423
  * Creates a Feedback Delay Network (FDN) {@link CacophonyEffect} — an
534
424
  * algorithmic reverb with a lossless degree-0 paraunitary Hadamard feedback
@@ -668,8 +558,8 @@ export declare class Cacophony {
668
558
  * Omnitone's WY/ZX 2-stereo-ConvolverNode packing and the bundled order-1
669
559
  * SH-HRIR.
670
560
  *
671
- * It is 4-channel-in / 2-channel-out, so it is NOT a `CacophonyEffect` and is
672
- * NOT added via `bus.addFilter`. Wire it EXPLICITLY using its two endpoints:
561
+ * It is 4-channel-in / 2-channel-out; this method returns the explicit
562
+ * endpoint object for custom graph wiring:
673
563
  * feed FOA into `decoder.input` (4-ch) and route `decoder.output` (2-ch
674
564
  * stereo) downstream:
675
565
  * ```ts
@@ -682,6 +572,13 @@ export declare class Cacophony {
682
572
  * `createStereoToBFormatNode` (the perceptual, approximate stereo-upmix path).
683
573
  */
684
574
  createFoaDecoder(options?: FoaDecoderOptions, context?: BaseContext): Promise<FoaDecoder>;
575
+ /**
576
+ * Creates a bus-filter wrapper around {@link FoaDecoder}. Use this on a
577
+ * dedicated 4-channel ACN/SN3D FOA bus, typically as the first and only
578
+ * filter that converts that bus to stereo binaural output. For custom manual
579
+ * wiring, use {@link createFoaDecoder} instead.
580
+ */
581
+ createFoaDecoderEffect(options?: FoaDecoderOptions): FoaDecoderEffect;
685
582
  /**
686
583
  * Creates an ITU-R BS.1770-5 {@link LoudnessMeter} that TAPS the output of a
687
584
  * target node without altering the audible path. The meter reports momentary