@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
package/src/synth.ts ADDED
@@ -0,0 +1,351 @@
1
+ /**
2
+ * Placeholder buffer synthesis: the stand-in a sound slot uses until a real
3
+ * file exists for it.
4
+ *
5
+ * Deliberately plain, and generic to any game. These exist so a build is
6
+ * audible and its timing can be felt while the real sounds are being made —
7
+ * they are not an attempt at the final thing, and treating them as one is the
8
+ * mistake the whole slot arrangement is designed to prevent.
9
+ */
10
+
11
+ /** A short noise burst shaped by an envelope — the workhorse placeholder. */
12
+ /**
13
+ * Makeup gain for the three-pole cascade below.
14
+ *
15
+ * **Why three poles and not one.** Every filtered-noise sound in this engine ran
16
+ * through a single pole, and a single pole is only 6 dB per octave — so at three or
17
+ * four octaves above its own cutoff it has taken off barely twenty decibels, and what
18
+ * survives is still broadband. Which is exactly what a listener hears: white noise
19
+ * everywhere, a waterfall that sounds like static. Lowering the cutoff did not fix it
20
+ * and could not — the leak is the slope, not the corner.
21
+ *
22
+ * Three poles in series is 18 dB per octave, which is a filter you can actually hear
23
+ * working. A landing becomes a thud, a slide becomes a scrape, and a waterfall becomes
24
+ * water rather than static with a swell on it — and every existing caller gets it
25
+ * without changing a single one of their numbers, because the corner frequency they
26
+ * each chose is unchanged. Only the slope past it moved.
27
+ *
28
+ * The cascade costs amplitude — each stage takes another bite out of the noise's own
29
+ * power — so this puts the level back. Measured to bring a mid-range colour term out
30
+ * at roughly what one pole used to.
31
+ */
32
+ const POLE_MAKEUP = 2.6;
33
+
34
+ export function noiseBuffer(
35
+ ctx: BaseAudioContext,
36
+ seconds: number,
37
+ decay: number,
38
+ colour: (t: number) => number,
39
+ ): AudioBuffer {
40
+ const rate = ctx.sampleRate;
41
+ const length = Math.max(1, Math.floor(rate * seconds));
42
+ const buffer = ctx.createBuffer(1, length, rate);
43
+ const data = buffer.getChannelData(0);
44
+ let p1 = 0;
45
+ let p2 = 0;
46
+ let p3 = 0;
47
+ for (let i = 0; i < length; i++) {
48
+ const t = i / length;
49
+ const white = Math.random() * 2 - 1;
50
+ const c = colour(t);
51
+ // Three poles, not one. See POLE_MAKEUP.
52
+ p1 += (white - p1) * c;
53
+ p2 += (p1 - p2) * c;
54
+ p3 += (p2 - p3) * c;
55
+ data[i] = p3 * POLE_MAKEUP * (1 - t) ** decay;
56
+ }
57
+ return buffer;
58
+ }
59
+
60
+ /** A pitched blip: a sine sweep, for anything that should read as a signal. */
61
+ export function toneBuffer(
62
+ ctx: BaseAudioContext,
63
+ seconds: number,
64
+ fromHz: number,
65
+ toHz: number,
66
+ decay: number,
67
+ ): AudioBuffer {
68
+ const rate = ctx.sampleRate;
69
+ const length = Math.max(1, Math.floor(rate * seconds));
70
+ const buffer = ctx.createBuffer(1, length, rate);
71
+ const data = buffer.getChannelData(0);
72
+ let phase = 0;
73
+ for (let i = 0; i < length; i++) {
74
+ const t = i / length;
75
+ const hz = fromHz + (toHz - fromHz) * t;
76
+ phase += (hz / rate) * Math.PI * 2;
77
+ data[i] = Math.sin(phase) * (1 - t) ** decay * 0.6;
78
+ }
79
+ return buffer;
80
+ }
81
+
82
+ /**
83
+ * A struck metal ring: inharmonic partials over a bright transient.
84
+ *
85
+ * The one thing that separates metal from every other synthesised hit is that its
86
+ * partials are **not** whole multiples of the fundamental. A harmonic stack reads as a
87
+ * bell at best and an organ at worst; detuning the partials by irrational-ish ratios
88
+ * is what makes the ear hear a struck bar. The ratios below are near a free bar's own
89
+ * modes rather than exact, which keeps it from ringing like a tuned instrument.
90
+ *
91
+ * Each partial decays at its own rate, faster the higher it is, because that is what
92
+ * real metal does and it is most of why a synthesised clang usually sounds like a
93
+ * synthesiser: hold the top partials as long as the bottom and you get a chime.
94
+ *
95
+ * @param seconds Total length. Metal rings on; too short and it is a click.
96
+ * @param baseHz The fundamental the partials are built off.
97
+ * @param decay How fast the whole thing dies away.
98
+ */
99
+ export function metalBuffer(
100
+ ctx: BaseAudioContext,
101
+ seconds: number,
102
+ baseHz: number,
103
+ decay: number,
104
+ ): AudioBuffer {
105
+ const rate = ctx.sampleRate;
106
+ const length = Math.max(1, Math.floor(rate * seconds));
107
+ const buffer = ctx.createBuffer(1, length, rate);
108
+ const data = buffer.getChannelData(0);
109
+ // Inharmonic on purpose. Whole multiples of these would be a bell.
110
+ const partials = [1, 2.76, 5.4, 8.93, 13.34, 18.64];
111
+ const phases = new Float64Array(partials.length);
112
+
113
+ for (let i = 0; i < length; i++) {
114
+ const t = i / length;
115
+ let sample = 0;
116
+ for (let p = 0; p < partials.length; p++) {
117
+ const ratio = partials[p] ?? 1;
118
+ phases[p] = (phases[p] ?? 0) + ((baseHz * ratio) / rate) * Math.PI * 2;
119
+ // Higher partials die first, which is the whole difference between struck
120
+ // metal and a chime.
121
+ const fade = (1 - t) ** (decay * (1 + p * 0.55));
122
+ sample += (Math.sin(phases[p] ?? 0) * fade) / (1 + p * 0.9);
123
+ }
124
+ // A short noise transient: the strike itself, before anything has begun to ring.
125
+ const strike = t < 0.012 ? (Math.random() * 2 - 1) * (1 - t / 0.012) * 0.7 : 0;
126
+ // Body over transient: the ring is the part that says *metal*, so it is weighted
127
+ // above the strike rather than under it.
128
+ data[i] = (sample * 0.52 + strike) * (1 - t) ** 0.6;
129
+ }
130
+ return buffer;
131
+ }
132
+
133
+ /** Silence, for a slot whose only honest placeholder is nothing at all. */
134
+ export function silentBuffer(ctx: BaseAudioContext): AudioBuffer {
135
+ return ctx.createBuffer(1, 1, ctx.sampleRate);
136
+ }
137
+
138
+ /**
139
+ * A continuous environmental bed, built to loop without a seam.
140
+ *
141
+ * Three things make a loop audible as a loop, and all three are handled here.
142
+ * The filters are warmed up before anything is recorded, so the buffer does not
143
+ * begin at silence and fade in. The join is crossfaded with equal-power
144
+ * weights, so the wrap has neither a click nor the ~3 dB hole a linear
145
+ * crossfade of noise leaves in the middle. And any slow swell is placed at a
146
+ * whole number of cycles per loop — the same reason the wind field uses integer
147
+ * harmonics — so it arrives back exactly where it started instead of turning
148
+ * the wrap into a lurch far more noticeable than the noise it rides on.
149
+ */
150
+ export interface AmbienceOptions {
151
+ /** Loop length. Longer costs memory but takes longer to recognise. */
152
+ seconds: number;
153
+ /** One-pole low-pass coefficient, 0–1. Lower is darker. */
154
+ colour: number;
155
+ /**
156
+ * Second, slower follower subtracted from the first — a high-pass. Removes
157
+ * the rumble that otherwise dominates any heavily filtered noise.
158
+ */
159
+ bodyCut?: number;
160
+ /** Slow level movement: how deep, 0–1, and how many whole cycles per loop. */
161
+ swellDepth?: number;
162
+ swellCycles?: number;
163
+ /** Sharp transients per second, and how fast each one decays. */
164
+ transientRate?: number;
165
+ transientDecay?: number;
166
+ /** Peak amplitude of the finished bed. */
167
+ gain?: number;
168
+ }
169
+
170
+ export function ambienceBuffer(ctx: BaseAudioContext, options: AmbienceOptions): AudioBuffer {
171
+ const rate = ctx.sampleRate;
172
+ const length = Math.max(1, Math.floor(rate * options.seconds));
173
+ // Fade over an eighth of the loop: long enough to hide the join in noise,
174
+ // short enough that most of the buffer is still unblended material.
175
+ const fade = Math.max(1, Math.min(Math.floor(length / 8), Math.floor(rate * 0.35)));
176
+
177
+ const colour = options.colour;
178
+ const bodyCut = options.bodyCut ?? 0;
179
+ const transientRate = options.transientRate ?? 0;
180
+ const transientDecay = options.transientDecay ?? 60;
181
+ const transientChance = transientRate / rate;
182
+
183
+ let p1 = 0;
184
+ let p2 = 0;
185
+ let low = 0;
186
+ let body = 0;
187
+ let transient = 0;
188
+
189
+ const step = (): number => {
190
+ const white = Math.random() * 2 - 1;
191
+ // Three poles, as in `noiseBuffer` and for the same reason.
192
+ p1 += (white - p1) * colour;
193
+ p2 += (p1 - p2) * colour;
194
+ low += (p2 - low) * colour;
195
+ body += (low - body) * bodyCut;
196
+ // A spark is a step that decays away, not a tone.
197
+ if (transientChance > 0 && Math.random() < transientChance) {
198
+ transient = (Math.random() * 2 - 1) * 0.9;
199
+ }
200
+ transient -= transient * (transientDecay / rate);
201
+ return (low - body) * POLE_MAKEUP + transient;
202
+ };
203
+
204
+ /*
205
+ * Settle the filters first. Both start at zero, and the slow one — the
206
+ * high-pass follower — takes a second or more of signal to reach its working
207
+ * point. Recording from cold puts an audible swell at the head of the buffer
208
+ * that the crossfade then smears across the join.
209
+ */
210
+ const slowest = Math.max(Math.min(bodyCut > 0 ? bodyCut : colour, colour), 1e-4);
211
+ const warmup = Math.min(Math.ceil(5 / slowest), rate * 3);
212
+ for (let i = 0; i < warmup; i++) step();
213
+
214
+ const raw = new Float32Array(length + fade);
215
+ for (let i = 0; i < raw.length; i++) raw[i] = step();
216
+
217
+ const buffer = ctx.createBuffer(1, length, rate);
218
+ const data = buffer.getChannelData(0);
219
+ const gain = options.gain ?? 1;
220
+ const swellDepth = options.swellDepth ?? 0;
221
+ const swellCycles = Math.max(0, Math.round(options.swellCycles ?? 0));
222
+
223
+ for (let i = 0; i < length; i++) {
224
+ let sample = raw[i] ?? 0;
225
+ if (i < fade) {
226
+ // Equal power, so noise keeps its level straight through the join.
227
+ const t = i / fade;
228
+ sample = sample * Math.sqrt(t) + (raw[length + i] ?? 0) * Math.sqrt(1 - t);
229
+ }
230
+ const swell =
231
+ swellCycles > 0
232
+ ? 1 -
233
+ swellDepth +
234
+ swellDepth * (0.5 + 0.5 * Math.sin((i / length) * swellCycles * Math.PI * 2))
235
+ : 1;
236
+ data[i] = sample * swell * gain;
237
+ }
238
+ return buffer;
239
+ }
240
+
241
+ /**
242
+ * Fire: a broadband hiss with sparks over it.
243
+ *
244
+ * The hiss alone reads as static; the sparks are what make it fire, and they
245
+ * have to be irregular — anything periodic turns a hearth into a machine.
246
+ */
247
+ export function fireLoopBuffer(ctx: BaseAudioContext, seconds = 4): AudioBuffer {
248
+ return ambienceBuffer(ctx, {
249
+ seconds,
250
+ colour: 0.22,
251
+ bodyCut: 0.004,
252
+ swellDepth: 0.25,
253
+ swellCycles: 3,
254
+ transientRate: 26,
255
+ transientDecay: 900,
256
+ gain: 0.5,
257
+ });
258
+ }
259
+
260
+ /**
261
+ * Water: a slow, dark wash that breathes.
262
+ *
263
+ * Nearly all the energy is low, and the swell is the part carrying the meaning
264
+ * — flat filtered noise at this darkness is indistinguishable from a fan.
265
+ */
266
+ export function waterLoopBuffer(ctx: BaseAudioContext, seconds = 6): AudioBuffer {
267
+ return ambienceBuffer(ctx, {
268
+ seconds,
269
+ colour: 0.05,
270
+ bodyCut: 0.0015,
271
+ swellDepth: 0.55,
272
+ swellCycles: 2,
273
+ gain: 0.85,
274
+ });
275
+ }
276
+
277
+ /** Wind: mid-heavy rush, swelling harder and faster than water. */
278
+ export function windLoopBuffer(ctx: BaseAudioContext, seconds = 5): AudioBuffer {
279
+ return ambienceBuffer(ctx, {
280
+ seconds,
281
+ colour: 0.1,
282
+ bodyCut: 0.02,
283
+ swellDepth: 0.7,
284
+ swellCycles: 3,
285
+ gain: 0.6,
286
+ });
287
+ }
288
+
289
+ /**
290
+ * A skate scrape: the sound of an edge losing its bite.
291
+ *
292
+ * Bright filtered noise with a slow chatter in it, so it reads as *grinding*
293
+ * rather than as hiss. The caller sweeps its playback rate with the drift's
294
+ * charge, which is what turns a texture into a meter you can hear — the pitch
295
+ * rising under you is the drift telling you how long you have held it.
296
+ *
297
+ * Loops without a seam: the chatter completes a whole number of cycles across
298
+ * the buffer, and the tail crossfades into the head.
299
+ */
300
+ export function driftScrapeBuffer(ctx: BaseAudioContext, seconds = 1.6): AudioBuffer {
301
+ const rate = ctx.sampleRate;
302
+ const length = Math.max(1, Math.floor(seconds * rate));
303
+ const buffer = ctx.createBuffer(1, length, rate);
304
+ const data = buffer.getChannelData(0);
305
+
306
+ /*
307
+ * Two poles of a resonant band, run over white noise.
308
+ *
309
+ * `f` sets where the band sits and is the whole difference between a scrape and a
310
+ * hiss. It was 0.34 — high enough that most of what survived the filter was still
311
+ * broadband top end, so a slide read as plain white noise. Down at 0.15 the band is
312
+ * in the low mids where a hard wheel on
313
+ * stone actually lives, and the tighter `q` narrows it further, so what comes
314
+ * through reads as a *material* being dragged rather than as noise being played.
315
+ */
316
+ let low = 0;
317
+ let band = 0;
318
+ const f = 0.15;
319
+ const q = 0.14;
320
+ /** Whole cycles across the buffer, so the chatter meets itself at the loop. */
321
+ const chatterCycles = 11;
322
+
323
+ let seed = 0x2f6e2b1;
324
+ const noise = (): number => {
325
+ seed = (Math.imul(seed, 1664525) + 1013904223) | 0;
326
+ return (seed / 0x7fffffff) % 1;
327
+ };
328
+
329
+ for (let i = 0; i < length; i++) {
330
+ const t = i / length;
331
+ const chatter = 0.62 + 0.38 * Math.abs(Math.sin(Math.PI * chatterCycles * t));
332
+ const input = noise();
333
+ low += f * band;
334
+ band += f * (input - low - q * band);
335
+ // Louder per unit of noise than before, because a narrower band passes far less
336
+ // through: the same 0.55 on this filter is a slide nobody can hear.
337
+ data[i] = band * chatter * 1.5;
338
+ }
339
+
340
+ // Equal-power crossfade of the tail into the head.
341
+ const fade = Math.min(Math.floor(rate * 0.04), Math.floor(length / 4));
342
+ for (let i = 0; i < fade; i++) {
343
+ const t = i / fade;
344
+ const a = Math.cos(t * Math.PI * 0.5);
345
+ const b = Math.sin(t * Math.PI * 0.5);
346
+ const head = data[i] ?? 0;
347
+ const tail = data[length - fade + i] ?? 0;
348
+ data[i] = head * b + tail * a;
349
+ }
350
+ return buffer;
351
+ }