@driftengine/audio 3.61.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 (88) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +11 -0
  4. package/dist/ambientLoop.d.ts +45 -0
  5. package/dist/ambientLoop.js +88 -0
  6. package/dist/audioHarness.d.ts +180 -0
  7. package/dist/audioHarness.js +244 -0
  8. package/dist/filters.d.ts +91 -0
  9. package/dist/filters.js +103 -0
  10. package/dist/formats.d.ts +18 -0
  11. package/dist/formats.js +19 -0
  12. package/dist/graph.d.ts +406 -0
  13. package/dist/graph.js +656 -0
  14. package/dist/index.d.ts +47 -0
  15. package/dist/index.js +39 -0
  16. package/dist/manifest.d.ts +28 -0
  17. package/dist/manifest.js +71 -0
  18. package/dist/mix/bus.d.ts +203 -0
  19. package/dist/mix/bus.js +293 -0
  20. package/dist/mix/console.d.ts +96 -0
  21. package/dist/mix/console.js +131 -0
  22. package/dist/mix/defaultLayout.d.ts +37 -0
  23. package/dist/mix/defaultLayout.js +63 -0
  24. package/dist/mix/inserts.d.ts +64 -0
  25. package/dist/mix/inserts.js +187 -0
  26. package/dist/mix/returns.d.ts +38 -0
  27. package/dist/mix/returns.js +86 -0
  28. package/dist/mix/snapshot.d.ts +30 -0
  29. package/dist/mix/snapshot.js +55 -0
  30. package/dist/positional.d.ts +37 -0
  31. package/dist/positional.js +47 -0
  32. package/dist/registry.d.ts +91 -0
  33. package/dist/registry.js +128 -0
  34. package/dist/rhythm/bands.d.ts +60 -0
  35. package/dist/rhythm/bands.js +12 -0
  36. package/dist/rhythm/beatGrid.d.ts +32 -0
  37. package/dist/rhythm/beatGrid.js +98 -0
  38. package/dist/rhythm/beatMap.d.ts +42 -0
  39. package/dist/rhythm/beatMap.js +405 -0
  40. package/dist/rhythm/kickCore.d.ts +79 -0
  41. package/dist/rhythm/kickCore.js +166 -0
  42. package/dist/rhythm/kickDetector.d.ts +65 -0
  43. package/dist/rhythm/kickDetector.js +202 -0
  44. package/dist/rhythm/renderedPulse.d.ts +15 -0
  45. package/dist/rhythm/renderedPulse.js +138 -0
  46. package/dist/session.d.ts +62 -0
  47. package/dist/session.js +83 -0
  48. package/dist/spatial/ambisonic.d.ts +135 -0
  49. package/dist/spatial/ambisonic.js +299 -0
  50. package/dist/spatial/listener.d.ts +109 -0
  51. package/dist/spatial/listener.js +186 -0
  52. package/dist/spatial/occlusion.d.ts +39 -0
  53. package/dist/spatial/occlusion.js +92 -0
  54. package/dist/spatial/source.d.ts +185 -0
  55. package/dist/spatial/source.js +366 -0
  56. package/dist/spatial/zones.d.ts +129 -0
  57. package/dist/spatial/zones.js +166 -0
  58. package/dist/synth.d.ts +92 -0
  59. package/dist/synth.js +282 -0
  60. package/package.json +54 -0
  61. package/src/ambientLoop.ts +101 -0
  62. package/src/audioHarness.ts +280 -0
  63. package/src/filters.ts +109 -0
  64. package/src/formats.ts +22 -0
  65. package/src/graph.ts +805 -0
  66. package/src/index.ts +84 -0
  67. package/src/manifest.ts +73 -0
  68. package/src/mix/bus.ts +356 -0
  69. package/src/mix/console.ts +181 -0
  70. package/src/mix/defaultLayout.ts +118 -0
  71. package/src/mix/inserts.ts +242 -0
  72. package/src/mix/returns.ts +114 -0
  73. package/src/mix/snapshot.ts +75 -0
  74. package/src/positional.ts +47 -0
  75. package/src/registry.ts +167 -0
  76. package/src/rhythm/bands.ts +45 -0
  77. package/src/rhythm/beatGrid.ts +106 -0
  78. package/src/rhythm/beatMap.ts +514 -0
  79. package/src/rhythm/kickCore.ts +197 -0
  80. package/src/rhythm/kickDetector.ts +233 -0
  81. package/src/rhythm/renderedPulse.ts +147 -0
  82. package/src/session.ts +93 -0
  83. package/src/spatial/ambisonic.ts +358 -0
  84. package/src/spatial/listener.ts +249 -0
  85. package/src/spatial/occlusion.ts +95 -0
  86. package/src/spatial/source.ts +452 -0
  87. package/src/spatial/zones.ts +213 -0
  88. package/src/synth.ts +351 -0
@@ -0,0 +1,406 @@
1
+ import { AmbientLoop } from './ambientLoop.ts';
2
+ import { KickDetector } from './rhythm/kickDetector.ts';
3
+ import type { FetchLike } from './registry.ts';
4
+ import { SoundRegistry } from './registry.ts';
5
+ import { MixConsole } from './mix/console.ts';
6
+ import { type DefaultLayout } from './mix/defaultLayout.ts';
7
+ /**
8
+ * The audio graph: layered stems into a master filter, with parallel sends.
9
+ *
10
+ * stems[] → stemGain[] → musicGain ─┬→ lift(highpass→duck) → dry ─┬→ bus → lowpass → dest
11
+ * └→ slam(shelf→drive→clip) ──┘
12
+ * one-shots → level ─────→ effectsGain ───────────────────┘ │
13
+ * │
14
+ * lift ─────────────────────────┬──┼→ convolver ────→ dest
15
+ * ├──┼→ longConvolver → dest
16
+ * └──┼→ feedbackDelay → dest
17
+ *
18
+ * Music and effects have their own gain stage because players expect to turn them
19
+ * down independently — muting the score while keeping the game audible is the single
20
+ * most-used audio setting there is.
21
+ *
22
+ * **The sends are fed from the music alone.** They hung off the shared bus first, which
23
+ * put reverb and delay on every sound the game made when only the score should carry
24
+ * them. A footstep with a
25
+ * six-second tail on it is not atmosphere, it is a bug, and the effects that carry
26
+ * the world's own sound need to stay dry and immediate to be legible. The master
27
+ * filter still applies to everything, which is deliberate: going under water muffles
28
+ * the world, not only the score.
29
+ *
30
+ * Game code expresses musical intent — "louder, faster, brighter" — and never
31
+ * builds nodes. That boundary is what stops mixing decisions from ending up
32
+ * spread across gameplay code where nobody can find them.
33
+ *
34
+ * Everything here degrades to silence rather than to a crash. A browser that
35
+ * blocks audio, an unsupported node type, a context that never resumes: all of
36
+ * them leave a playable game, because sound is not what the game is for.
37
+ */
38
+ export interface AudioGraphOptions {
39
+ /** How many simultaneous music layers to allocate. */
40
+ stemCount: number;
41
+ /**
42
+ * Build on this context instead of creating a live one.
43
+ *
44
+ * For rendering a mix rather than hearing it: hand in an `OfflineAudioContext` and
45
+ * every node below is built on it, so `startRendering` produces the same mix the
46
+ * speakers would have made. Additive — omitting it is exactly the previous
47
+ * behaviour.
48
+ */
49
+ readonly context?: BaseAudioContext;
50
+ /**
51
+ * Where the music and effects stages start. Unity for both when omitted.
52
+ *
53
+ * The reason this exists rather than a `setMusicVolume` call after construction: a
54
+ * graph built to *reproduce* a mix has to be at that mix's levels from its own zero,
55
+ * and every parameter move here is a `setTargetAtTime` — which approaches its target
56
+ * over `RAMP` and so would open a rendered clip with a third of a second of glide
57
+ * down from unity to whatever the player actually chose. A level that does not change
58
+ * for the whole render is not a move; it is where the parameter starts.
59
+ *
60
+ * It also cannot be scheduled on the wrong clock, which the other shape can: offline
61
+ * there is no "now", so a level set imperatively lands wherever the render happens to
62
+ * have got to. See `at`.
63
+ */
64
+ readonly levels?: MixLevels;
65
+ /**
66
+ * Fetch every registered sound goes through, instead of the global `fetch`.
67
+ *
68
+ * A game may need to gate its own asset requests — a signed URL, a token header —
69
+ * without the engine knowing why. Additive — omitting it is exactly the previous
70
+ * behaviour, and `SoundRegistry` already degrades any fetch failure to its `synth`
71
+ * fallback, so a caller's custom fetch can fail as loudly or as quietly as it likes
72
+ * without a new error path opening up here.
73
+ */
74
+ readonly fetchImpl?: FetchLike;
75
+ /**
76
+ * Told why this browser gave no audio at all, when `create` returns null.
77
+ *
78
+ * Null is deliberately coarse — it means "there is nothing to wake", and a caller
79
+ * reporting it learns only that somebody, somewhere, heard nothing. A game with
80
+ * telemetry needs the other half: a missing constructor and a context that threw
81
+ * are different bugs with different fixes, and the field that said neither was
82
+ * `soundtrack_init_null`. Never called when a graph is returned; a suspended
83
+ * context is not unavailable.
84
+ */
85
+ readonly onUnavailable?: (reason: string) => void;
86
+ /**
87
+ * Where randomness comes from, for the noise the reverb impulses are made of.
88
+ *
89
+ * Defaults to `Math.random`, which is what a game wants: a hall built from a fresh sequence every
90
+ * session is a hall, and one built from a fixed one is a hall with a repeating texture in its
91
+ * tail. Supplied only where a render has to be reproducible sample for sample — a check script
92
+ * comparing two mixes cannot do that while every convolver is different.
93
+ *
94
+ * Additive: omitting it is exactly the previous behaviour.
95
+ */
96
+ readonly random?: () => number;
97
+ }
98
+ /**
99
+ * The two levels a player is given control of: the score, and everything else.
100
+ *
101
+ * Named as a pair because they travel as one — a second graph reproducing this mix
102
+ * needs both or neither, and the failure of carrying one is silent.
103
+ */
104
+ export interface MixLevels {
105
+ readonly music: number;
106
+ readonly effects: number;
107
+ }
108
+ export declare class AudioGraph {
109
+ readonly context: BaseAudioContext;
110
+ readonly registry: SoundRegistry;
111
+ /**
112
+ * The mix, as a tree of buses rather than as nodes held here.
113
+ *
114
+ * Everything below that used to be a field — the music and effects stages, the master filter, the
115
+ * lift, the slam, the three sends and their returns — is a bus or an insert now, and
116
+ * `defaultLayout` is where the shape they make is written down. What is left in this class is the
117
+ * transport: what is playing, from where, at what rate.
118
+ */
119
+ private readonly mix;
120
+ private readonly layoutNodes;
121
+ /**
122
+ * The recording tap, created once.
123
+ *
124
+ * Cached because it used to be built per call and never taken down — the mix accumulated one
125
+ * `MediaStreamAudioDestinationNode` per export, each still pulling audio for the rest of the
126
+ * session. Ten exports while testing is ten of them, on the thread least able to absorb it and
127
+ * the one whose overrun is heard as a click.
128
+ */
129
+ private tap;
130
+ /**
131
+ * The player's own two levels, held here because a second graph built to render this mix has to
132
+ * be built at them.
133
+ *
134
+ * Mirrored rather than read back off the buses for the reason the buses themselves mirror: a
135
+ * level is *ramped*, and mid-ramp a parameter is somewhere between where it was and where it is
136
+ * going. A fade asking "back to the player's level" or a render asking "at what level" would both
137
+ * get whatever instant they happened to ask on.
138
+ *
139
+ * Which is also why `fadeMusic` does not write here. A fade is part of an edit, not a setting; it
140
+ * has to return to the setting when it is over.
141
+ */
142
+ private readonly mixLevels;
143
+ /**
144
+ * A gain per live source, so a stem can be faded out without touching the stem's own level —
145
+ * which the *replacement* source is already connected to.
146
+ */
147
+ private readonly sourceLevels;
148
+ /** Where scheduled work lands: an explicit instant, or null for "now". See `at`. */
149
+ private atSec;
150
+ private readonly stemGains;
151
+ private readonly stemBuffers;
152
+ private readonly stemSources;
153
+ private started;
154
+ /**
155
+ * Transport position, integrated rather than derived from elapsed wall time.
156
+ *
157
+ * The playback rate moves with the character, so three seconds of context time at
158
+ * rate 1.1 is 3.3 seconds of tape — and a caller that puts the track's beat zero
159
+ * on a start line needs to know where the tape actually is.
160
+ */
161
+ private positionSec;
162
+ private positionAt;
163
+ private rate;
164
+ private paused;
165
+ private constructor();
166
+ /** The mix this graph plays into, for a caller that wants a bus of its own. */
167
+ get console(): MixConsole;
168
+ /**
169
+ * Build a graph, or return null only if the browser has no audio to give.
170
+ *
171
+ * **Null means "this browser will not do audio at all", never "not yet".** The
172
+ * distinction is the whole of a bug that silenced audio on mobile devices, both
173
+ * Android and iOS: this used to `await context.resume()`
174
+ * inside the try, so a browser that *rejects* that call — which is what a
175
+ * rejection means when autoplay is blocked — threw a perfectly good graph into
176
+ * the `catch` and reported no audio. The caller latches its load so it happens
177
+ * once, so that null was permanent: silence for the session, with a `wake()`
178
+ * that had nothing left to wake.
179
+ *
180
+ * A suspended context is not a failure. Its clock does not advance, so nothing
181
+ * scheduled on it is missed, and `wake()` exists to start it on the first
182
+ * gesture. The resume is still *attempted* here, because when this is called
183
+ * from a gesture — or on a site the browser already trusts — it starts
184
+ * immediately and there is no reason to wait for a tap that already happened.
185
+ * It is just no longer awaited, and no longer fatal.
186
+ *
187
+ * Desktop cannot show you this. Chrome grants autoplay to a site its user keeps
188
+ * visiting, so on the machine this game is built on the context comes up
189
+ * already running. It only breaks on a device that has not earned that trust,
190
+ * which is every phone arriving from a share link.
191
+ */
192
+ static create(options: AudioGraphOptions): Promise<AudioGraph | null>;
193
+ /**
194
+ * Whether the context is actually producing sound.
195
+ *
196
+ * A context built without a user gesture is `suspended`: nodes run, sources are
197
+ * scheduled, and nothing is heard. A caller that has something to say about that — an
198
+ * intro film with a score, say — needs to be able to ask.
199
+ */
200
+ get audible(): boolean;
201
+ /**
202
+ * Ask the browser to start the context, if it will.
203
+ *
204
+ * Safe to call from anywhere and safe to call repeatedly: outside a gesture the promise
205
+ * simply rejects, which is not an error state — it is the policy working. Anything
206
+ * already scheduled begins when it succeeds, because a suspended context's clock does
207
+ * not advance, so nothing is missed in the meantime.
208
+ */
209
+ wake(): void;
210
+ loadStem(index: number, buffer: AudioBuffer): void;
211
+ /** Whether the stems are running. */
212
+ get playing(): boolean;
213
+ /**
214
+ * Start every stem at one scheduled instant.
215
+ *
216
+ * Layers must be sample-locked: started independently they drift apart by
217
+ * however long each `start()` call happened to take, and a bassline a few
218
+ * milliseconds off its drums is heard as flamming rather than as one track.
219
+ */
220
+ start(): void;
221
+ /** Whether the transport is stopped mid-track, as opposed to not yet started. */
222
+ get held(): boolean;
223
+ /**
224
+ * Stop the stems where they are.
225
+ *
226
+ * The tape stops — this is a *pause*, not a duck, and the distinction is what
227
+ * several rounds of feedback kept correcting toward: the pause itself was right, it
228
+ * only ever needed a longer tail of effects to keep the music in the background.
229
+ * So the source stops
230
+ * and the sends carry what was already in flight. Reverb and delay live downstream
231
+ * of the stems, so cutting the source is exactly what leaves a decaying tail
232
+ * behind, and `setLongReverbSend` is how far that tail reaches.
233
+ *
234
+ * Idempotent: the caller is a per-frame mix that knows a *state*, not an event.
235
+ */
236
+ hold(): void;
237
+ /**
238
+ * Start the stems again from where `hold` left them.
239
+ *
240
+ * From where it left them, rather than from where the tape *would* have been: a
241
+ * pause that catches up is a jump cut, and on a long glide it is audible as the
242
+ * track skipping. The cost is that airborne time puts the score behind the route's
243
+ * bar grid — a real trade, taken deliberately, because the hold is felt on every
244
+ * jump and the grid is felt once at the start line.
245
+ */
246
+ release(): void;
247
+ /**
248
+ * Stop the stems and start them again from the top.
249
+ *
250
+ * A `BufferSource` cannot be rewound — the spec makes it one-shot — so starting
251
+ * over means discarding the sources and creating new ones. That is cheap: a
252
+ * source node is a handle onto a buffer that is already decoded and already
253
+ * resident, and nothing about the graph downstream of it is rebuilt.
254
+ *
255
+ * Exists because a caller needs the track's beat zero to coincide with something
256
+ * in its own world. Left running instead, a loop's downbeats land somewhere
257
+ * different on every attempt.
258
+ *
259
+ * **Returns how long until beat zero is actually heard**, in seconds, because
260
+ * `launch` schedules a little ahead of now and a caller lining a picture up
261
+ * against the music needs that number rather than an assumption. Zero when
262
+ * nothing started.
263
+ */
264
+ restart(): number;
265
+ /**
266
+ * Seconds until the scheduled start of whatever is playing, or 0 if it is
267
+ * already sounding.
268
+ *
269
+ * Reads the instant `launch` scheduled, which is the only authority on when the
270
+ * stems begin: everything else about the transport is a consequence of it.
271
+ */
272
+ get startsInSec(): number;
273
+ /**
274
+ * Create one source per stem, all at the same scheduled instant and the same
275
+ * offset into the buffer.
276
+ *
277
+ * Layers must be sample-locked: started independently they drift apart by however
278
+ * long each `start()` call happened to take, and a bassline a few milliseconds off
279
+ * its drums is heard as flamming rather than as one track.
280
+ */
281
+ private launch;
282
+ /**
283
+ * Stop every stem, quietly. See `FADE_OUT` for why the fade is not optional.
284
+ *
285
+ * The sources are dropped from `stemSources` immediately but stay connected until
286
+ * their fade has run: disconnecting a node mid-fade is the same discontinuity this
287
+ * exists to remove.
288
+ */
289
+ private stopSources;
290
+ /**
291
+ * Let go of a faded-out source once it can no longer be heard.
292
+ *
293
+ * A timer rather than `onended`, because an offline render has no wall clock to fire
294
+ * one on and the nodes it leaves behind are discarded with the context anyway. Live,
295
+ * a handful of nodes for a fifth of a second is cheaper than a listener per source.
296
+ */
297
+ private release_;
298
+ /**
299
+ * Schedule everything that follows at `seconds` on this context's timeline, or at
300
+ * "now" when null.
301
+ *
302
+ * An offline render sets it once per frame and gets a mix whose every move lands
303
+ * where the picture is, exactly, with no clock involved. Live callers never touch it.
304
+ */
305
+ at(seconds: number | null): void;
306
+ /**
307
+ * The instant scheduled work lands on.
308
+ *
309
+ * One reader, so "now" exists in one place — and so an offline render can move it.
310
+ */
311
+ private scheduleAt;
312
+ /**
313
+ * How far ahead the stems are launched, seconds.
314
+ *
315
+ * Live it is a lead, so the layers start sample-locked however long the calls take.
316
+ * Offline there is nothing to be late for: work scheduled at an exact instant is
317
+ * already sample-locked, and a lead would only push beat zero off the clip's zero.
318
+ */
319
+ private startLead;
320
+ /** The context as a live one, or null when this graph is rendering offline. */
321
+ private live;
322
+ /** Carry the transport position up to now at the rate it has been running at. */
323
+ private advance;
324
+ setStemGain(index: number, gain: number): void;
325
+ /**
326
+ * The layout this graph plays into: its buses, its inserts and its returns.
327
+ *
328
+ * **This is where the mix went in 3.0.0.** Every setter this class used to carry — the two
329
+ * volumes, the lift, the slam, the master filter, the three sends and the delay — is a method on
330
+ * a bus or an insert now, and `PORTING.md` maps them one for one. They were removed rather than
331
+ * left forwarding, because a shim that works forever is a second answer to every question the
332
+ * console already answers, and the two would drift the first time one of them grew a clamp.
333
+ */
334
+ get layout(): DefaultLayout;
335
+ /**
336
+ * The two levels a player chose, read from the buses that hold them.
337
+ *
338
+ * Derived rather than mirrored, which it was until 3.0.0. A mirror is a second place the answer
339
+ * is decided, and the reason the old one existed — that a level is ramped, so mid-ramp the
340
+ * *parameter* is between two values — is answered by the bus itself keeping its own fader
341
+ * setting. A duck does not move it, which is the distinction `fadeMusic` needed a paragraph for.
342
+ */
343
+ get levels(): MixLevels;
344
+ setPlaybackRate(rate: number): void;
345
+ /**
346
+ * Start a looping environmental bed, silent until the caller gives it a
347
+ * level. Routed through the effects stage, so the effects slider governs the
348
+ * world's own sound and the music slider governs only the score.
349
+ *
350
+ * Returns null for a missing buffer, so a caller can create loops
351
+ * unconditionally and let an unresolved slot simply be silent.
352
+ */
353
+ createLoop(buffer: AudioBuffer | undefined): AmbientLoop | null;
354
+ /**
355
+ * A live kick detector listening to the music.
356
+ *
357
+ * Tapped off the music stage rather than the master bus, so it hears the
358
+ * track and not the game's own sound effects — a splash landing on the beat
359
+ * would otherwise read as a kick and flash the world.
360
+ *
361
+ * The taps are pure observers: nothing is connected onward from them, so
362
+ * inserting a detector cannot change what anyone hears.
363
+ */
364
+ createKickDetector(): KickDetector | null;
365
+ /**
366
+ * A stream of everything the player is hearing, for a clip recording.
367
+ *
368
+ * Tapped off the mix rather than replacing the destination, so recording cannot
369
+ * silence the game — a clip that captures perfectly while the player hears
370
+ * nothing is a bug they would report as "the export broke the sound".
371
+ *
372
+ * **Off `out`, which is the whole mix, and not off `master`, which is the dry
373
+ * path.** The send returns rejoin downstream of the master filter, so a tap on
374
+ * `master` hears the track and the speed filter and nothing wet at all. See
375
+ * `out`.
376
+ *
377
+ * **The alignment of this against the video is not ours to fix, and that was
378
+ * measured rather than assumed.** A probe that flashed one frame white while
379
+ * scheduling a click at the same instant, decoded back out of the file, found the
380
+ * audio leading the picture by a mean of 51 ms in one run and 85 ms in the next, with
381
+ * `outputLatency` reporting 0.048 then 0.024. Feeding the tap through a delay does
382
+ * move it — a forced 200 ms landed at +132 ms, near one for one — but there is no
383
+ * constant to use: correcting by an unstable reading made a run worse, from -51 to
384
+ * -85. `MediaRecorder` aligns its tracks by when data reached it, and nothing here can
385
+ * see that. The offline path exists because it never asks this question.
386
+ *
387
+ * The same tap every time. Building one per recording left the last one
388
+ * connected and running.
389
+ */
390
+ captureStream(): MediaStream | null;
391
+ /**
392
+ * Fire a one-shot. Routed so it sits under the same master filter and sends.
393
+ *
394
+ * `pan` places it across the stereo field (-1 to 1); pass the result of
395
+ * `stereoPan`. Omitted, the sound is centred, which is right for anything
396
+ * that happens *to* the player rather than somewhere near them.
397
+ */
398
+ play(buffer: AudioBuffer | undefined, gain?: number, pan?: number): void;
399
+ dispose(): void;
400
+ /**
401
+ * Every parameter move is ramped. Assigning `.value` directly steps the
402
+ * signal, and a step in a gain or a filter cutoff is an audible click — which
403
+ * at sixty updates a second becomes a buzz rather than a mix.
404
+ */
405
+ private ramp;
406
+ }