cacophony 0.25.0 → 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/dist/bus.d.ts +178 -7
  2. package/dist/effects.d.ts +46 -6
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.mjs +1029 -764
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/sound.d.ts +8 -2
  8. package/dist/synth.d.ts +3 -2
  9. package/docs/assets/search.js +1 -1
  10. package/docs/classes/AudioCache.html +7 -7
  11. package/docs/classes/BiquadEffect.html +2 -2
  12. package/docs/classes/Bus.html +136 -21
  13. package/docs/classes/Cacophony.html +64 -64
  14. package/docs/classes/DynamicsEffect.html +2 -2
  15. package/docs/classes/FdnReverbEffect.html +2 -2
  16. package/docs/classes/FoaDecoder.html +6 -6
  17. package/docs/classes/Group.html +13 -13
  18. package/docs/classes/KWeightingFilter.html +3 -3
  19. package/docs/classes/LoudnessMeter.html +11 -11
  20. package/docs/classes/MediaStreamPlayback.html +18 -18
  21. package/docs/classes/MediaStreamSound.html +19 -19
  22. package/docs/classes/MicrophonePlayback.html +3 -3
  23. package/docs/classes/ModulatedDelayEffect.html +2 -2
  24. package/docs/classes/PhaserEffect.html +2 -2
  25. package/docs/classes/Playback.html +36 -36
  26. package/docs/classes/ReverbEffect.html +2 -2
  27. package/docs/classes/ShareEffect.html +2 -2
  28. package/docs/classes/Sound.html +36 -32
  29. package/docs/classes/Synth.html +22 -21
  30. package/docs/classes/SynthGroup.html +2 -2
  31. package/docs/classes/TremoloEffect.html +2 -2
  32. package/docs/classes/TruePeakDetector.html +7 -7
  33. package/docs/classes/WaveshaperEffect.html +2 -2
  34. package/docs/functions/encodeMonoToFoaSN3D.html +1 -1
  35. package/docs/functions/integratedLoudness.html +1 -1
  36. package/docs/functions/integratedUngatedLoudness.html +1 -1
  37. package/docs/functions/loudnessRange.html +1 -1
  38. package/docs/functions/timeStretch.html +1 -1
  39. package/docs/functions/timeStretchChannels.html +1 -1
  40. package/docs/functions/truePeakDb.html +1 -1
  41. package/docs/interfaces/AudioBuffer.html +2 -2
  42. package/docs/interfaces/AudioBufferSourceNode.html +4 -4
  43. package/docs/interfaces/AudioEventCallbacks.html +1 -1
  44. package/docs/interfaces/AudioListener.html +2 -2
  45. package/docs/interfaces/AudioNode.html +3 -3
  46. package/docs/interfaces/AudioParam.html +2 -2
  47. package/docs/interfaces/AudioWorklet.html +2 -2
  48. package/docs/interfaces/AudioWorkletNode.html +4 -4
  49. package/docs/interfaces/BaseContext.html +2 -2
  50. package/docs/interfaces/BaseSound.html +2 -2
  51. package/docs/interfaces/BiquadCoefficients.html +2 -2
  52. package/docs/interfaces/BiquadFilterNode.html +4 -4
  53. package/docs/interfaces/CacheErrorEvent.html +2 -2
  54. package/docs/interfaces/CacheHitEvent.html +2 -2
  55. package/docs/interfaces/CacheMissEvent.html +2 -2
  56. package/docs/interfaces/CacophonyEffect.html +2 -2
  57. package/docs/interfaces/ChannelMergerNode.html +3 -3
  58. package/docs/interfaces/ChannelSplitterNode.html +3 -3
  59. package/docs/interfaces/DynamicsOptions.html +7 -7
  60. package/docs/interfaces/FadeStartEvent.html +2 -2
  61. package/docs/interfaces/FdnReverbOptions.html +6 -6
  62. package/docs/interfaces/FoaDecoderOptions.html +2 -2
  63. package/docs/interfaces/GainNode.html +3 -3
  64. package/docs/interfaces/GlobalPlaybackEvent.html +2 -2
  65. package/docs/interfaces/LoadingCompleteEvent.html +2 -2
  66. package/docs/interfaces/LoadingErrorEvent.html +2 -2
  67. package/docs/interfaces/LoadingProgressEvent.html +2 -2
  68. package/docs/interfaces/LoadingStartEvent.html +2 -2
  69. package/docs/interfaces/LoudnessChannelInput.html +2 -2
  70. package/docs/interfaces/LoudnessReading.html +5 -5
  71. package/docs/interfaces/MediaElementSourceNode.html +3 -3
  72. package/docs/interfaces/MediaStreamAudioSourceNode.html +3 -3
  73. package/docs/interfaces/MediaStreamSoundOptions.html +2 -2
  74. package/docs/interfaces/ModulatedDelayOptions.html +10 -8
  75. package/docs/interfaces/OfflineOptions.html +2 -2
  76. package/docs/interfaces/OscillatorNode.html +4 -4
  77. package/docs/interfaces/PannerNode.html +3 -3
  78. package/docs/interfaces/PhaserOptions.html +7 -7
  79. package/docs/interfaces/PlayOptions.html +2 -2
  80. package/docs/interfaces/PlaybackErrorEvent.html +2 -2
  81. package/docs/interfaces/ReverbOptions.html +13 -13
  82. package/docs/interfaces/RuntimeOptions.html +2 -2
  83. package/docs/interfaces/SoundCleanupHoldings.html +2 -2
  84. package/docs/interfaces/SoundErrorEvent.html +2 -2
  85. package/docs/interfaces/StereoPannerNode.html +3 -3
  86. package/docs/interfaces/TimeStretchOptions.html +5 -5
  87. package/docs/interfaces/TremoloOptions.html +7 -5
  88. package/docs/interfaces/WaveshaperOptions.html +8 -5
  89. package/docs/types/BaseAudioEvents.html +1 -1
  90. package/docs/types/BusConnectionTarget.html +1 -1
  91. package/docs/types/CacheEventCallback.html +1 -1
  92. package/docs/types/CacophonyEvents.html +1 -1
  93. package/docs/types/ErrorEventCallback.html +1 -1
  94. package/docs/types/FadeType.html +1 -1
  95. package/docs/types/HrtfPannerOptions.html +1 -1
  96. package/docs/types/LoadingEventCallback.html +1 -1
  97. package/docs/types/LoopCount.html +1 -1
  98. package/docs/types/LoudnessChannel.html +1 -1
  99. package/docs/types/Orientation.html +1 -1
  100. package/docs/types/PanCloneOverrides.html +1 -1
  101. package/docs/types/PanType.html +1 -1
  102. package/docs/types/PlaybackEvents.html +1 -1
  103. package/docs/types/Position.html +1 -1
  104. package/docs/types/SoundEvents.html +1 -1
  105. package/docs/types/SoundType.html +1 -1
  106. package/docs/types/SourceNode.html +1 -1
  107. package/docs/types/SynthEvents.html +1 -1
  108. package/docs/types/ThreeDOptions.html +1 -1
  109. package/docs/variables/CHANNEL_WEIGHTS.html +1 -1
  110. package/docs/variables/K_WEIGHTING_STAGE1_48K.html +1 -1
  111. package/docs/variables/K_WEIGHTING_STAGE2_48K.html +1 -1
  112. package/package.json +1 -1
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.
@@ -25,9 +40,25 @@ export declare class Bus {
25
40
  readonly output: GainNode;
26
41
  private readonly _context;
27
42
  private readonly _filterNodes;
43
+ /**
44
+ * Filter nodes currently bypassed (skipped in the audible chain). A bypassed
45
+ * node stays in {@link _filterNodes} — 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,70 @@ 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
+ * `_filterNodes`, 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 _filterNodes} (and thus in
282
+ * {@link filters}) but receive no inbound/outbound chain edge.
283
+ */
284
+ private _desiredFilterChainEdges;
114
285
  private _connectFilterChainEdge;
115
286
  private _disconnectFilterChainEdges;
116
287
  private _throwIfDestroyed;
package/dist/effects.d.ts CHANGED
@@ -258,6 +258,33 @@ export declare class FdnReverbEffect implements CacophonyEffect {
258
258
  constructor(host: FdnReverbHost, options?: FdnReverbOptions);
259
259
  build(context: BaseContext): Promise<AudioWorkletNode>;
260
260
  }
261
+ /**
262
+ * String aliases for the integer mode indices that {@link WaveshaperOptions},
263
+ * {@link TremoloOptions} and {@link ModulatedDelayOptions} flow straight through
264
+ * as `parameterData`. An AudioParam can only carry a number, so these maps are
265
+ * applied inside each effect's `build()` to translate a string mode into its
266
+ * integer index BEFORE the options become `parameterData`. The index assignments
267
+ * mirror the worklet shells' `*_BY_INDEX` arrays exactly
268
+ * (`processors/waveshaper.ts`, `processors/tremolo.ts`,
269
+ * `processors/modulated-delay.ts`). Declared locally so this module does not
270
+ * import from the worklet/processor modules (keeping that boundary intact).
271
+ */
272
+ declare const WAVESHAPER_SHAPE_TO_INDEX: {
273
+ readonly hardclip: 0;
274
+ readonly tanh: 1;
275
+ };
276
+ type WaveshaperShapeAlias = keyof typeof WAVESHAPER_SHAPE_TO_INDEX;
277
+ declare const TREMOLO_SHAPE_TO_INDEX: {
278
+ readonly sine: 0;
279
+ readonly triangle: 1;
280
+ readonly square: 2;
281
+ };
282
+ type TremoloShapeAlias = keyof typeof TREMOLO_SHAPE_TO_INDEX;
283
+ declare const MODULATED_DELAY_INTERPOLATION_TO_INDEX: {
284
+ readonly cubic: 0;
285
+ readonly linear: 1;
286
+ };
287
+ type ModulatedDelayInterpolationAlias = keyof typeof MODULATED_DELAY_INTERPOLATION_TO_INDEX;
261
288
  /**
262
289
  * Construction-time configuration for a {@link WaveshaperEffect}, mirroring the
263
290
  * `waveshaper` AudioWorkletProcessor's AudioParam set (see
@@ -273,8 +300,13 @@ export declare class FdnReverbEffect implements CacophonyEffect {
273
300
  export interface WaveshaperOptions {
274
301
  /** Pre-gain (drive) into the nonlinearity. Default 1. Range 0..100; >1 = harder saturation. */
275
302
  drive?: number;
276
- /** Nonlinearity index: 0 = hard clip (polynomial F0, eq.25), 1 = tanh soft clip (F0 = log cosh, eq.20). Default 0. */
277
- shape?: number;
303
+ /**
304
+ * Nonlinearity: 0 = hard clip (polynomial F0, eq.25), 1 = tanh soft clip
305
+ * (F0 = log cosh, eq.20). Default 0. Accepts either the integer index or the
306
+ * matching string alias (`"hardclip"` = 0, `"tanh"` = 1), translated to the
307
+ * index in `build()`.
308
+ */
309
+ shape?: number | WaveshaperShapeAlias;
278
310
  /** Wet/dry mix (0..1); 0 = dry bypass, 1 = fully shaped. Default 1. */
279
311
  mix?: number;
280
312
  /** Post-nonlinearity output gain (linear). Default 1. Range 0..4. */
@@ -323,8 +355,12 @@ export interface ModulatedDelayOptions {
323
355
  blend?: number;
324
356
  /** Wet (modulated tap) gain (feedforward). Default 0.7071. Range 0..1. */
325
357
  feedforward?: number;
326
- /** Interpolation index: 0 = cubic (4-tap Lagrange N=3), 1 = linear (2-tap N=1). Default 0. */
327
- interpolation?: number;
358
+ /**
359
+ * Interpolation: 0 = cubic (4-tap Lagrange N=3), 1 = linear (2-tap N=1).
360
+ * Default 0. Accepts either the integer index or the matching string alias
361
+ * (`"cubic"` = 0, `"linear"` = 1), translated to the index in `build()`.
362
+ */
363
+ interpolation?: number | ModulatedDelayInterpolationAlias;
328
364
  }
329
365
  /**
330
366
  * CacophonyEffect that builds a `modulated-delay` AudioWorkletNode — Dattorro's
@@ -402,8 +438,12 @@ export interface TremoloOptions {
402
438
  rate?: number;
403
439
  /** Modulation depth (0 = bypass, 1 = full 0..1 swing). Default 0.5. Range 0..1. */
404
440
  depth?: number;
405
- /** LFO shape index: 0 = sine, 1 = triangle, 2 = square. Default 0. */
406
- shape?: number;
441
+ /**
442
+ * LFO shape: 0 = sine, 1 = triangle, 2 = square. Default 0. Accepts either
443
+ * the integer index or the matching string alias (`"sine"` = 0,
444
+ * `"triangle"` = 1, `"square"` = 2), translated to the index in `build()`.
445
+ */
446
+ shape?: number | TremoloShapeAlias;
407
447
  /** Per-channel LFO phase offset in degrees (0 = mono, 90 = quadrature, 180 = auto-pan). Default 0. Range 0..180. */
408
448
  stereoPhase?: number;
409
449
  }