@adatechnology/game-audio 0.0.0-stage → 0.1.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.
@@ -0,0 +1,783 @@
1
+ import { Vec3, AudioSink, TestableModule } from '@adatechnology/game-contracts';
2
+
3
+ /** Deterministic mulberry32 generator; floats in [0, 1). The same generator the golden tool seeds `Math.random` with, so a team routine and its recipe draw the same numbers. */
4
+ declare function createRng(seed: number): () => number;
5
+ /** The seed of the stream called `name` (or numbered `name`): one seed gives each channel its own sequence, so one subject's sounds never shift another's. */
6
+ declare function streamSeed(seed: number, name: string | number): number;
7
+ declare function streamFor(seed: number, name: string | number): () => number;
8
+ /** `min + random × (max − min)`: the form every team routine uses (`95 + Math.random() * 40`). */
9
+ declare function drawBetween(random: () => number, min: number, max: number): number;
10
+
11
+ /**
12
+ * Sound recipes are data. A recipe says which nodes a sound has and which numbers drive them; ranges are drawn when the sound is planned, in
13
+ * the order the fields are written (see `planSound`), so a recipe ported from a team routine draws its random numbers in the routine's order.
14
+ */
15
+ declare const BUS_IDS: readonly ["weapons", "voices", "world", "ambience", "weather", "ui"];
16
+ type BusId = (typeof BUS_IDS)[number];
17
+ /** A named number: from `PlayOptions.params` or the recipe's `vars`, times `scale`, plus `add`. */
18
+ type ParamRef = {
19
+ readonly param: string;
20
+ readonly scale?: number;
21
+ readonly add?: number;
22
+ };
23
+ /** A number, a `[min, max]` drawn uniformly (`min + random × (max − min)`), or a named number. */
24
+ type Range = number | readonly [min: number, max: number] | ParamRef;
25
+ /** Seconds from the layer start: a range, the layer `'duration'`, or `'0.6d'` (0.6 × the layer duration). */
26
+ type Time = Range | 'duration' | `${number}d`;
27
+ type Curve = 'linear' | 'exp';
28
+ /**
29
+ * How an AudioParam moves:
30
+ * - `{ set }` assigns `.value`;
31
+ * - `{ set, to, curve, over }` assigns `.value`, then ramps to `to` at `over`;
32
+ * - `{ from }` calls `setValueAtTime(from)` at the start; with `to`, `curve` and `over` it then ramps to `to` (`'from'` is the value drawn for `from`: the art scene ramps a tone back to its own start when it gives no end).
33
+ */
34
+ type Automation = {
35
+ readonly set: Range;
36
+ readonly to?: Range | 'from';
37
+ readonly curve?: Curve;
38
+ readonly over?: Time;
39
+ } | {
40
+ readonly from: Range;
41
+ readonly to?: Range | 'from';
42
+ readonly curve?: Curve;
43
+ readonly over?: Time;
44
+ };
45
+ /**
46
+ * One gain point: time (seconds, or `'0.6d'`), value and how it is reached (`set` = `setValueAtTime`, or a ramp). The value is multiplied by the
47
+ * layer level (volume, distance and layer gain) except when the fourth item is `'fixed'` (the floor a ramp falls to, such as 0.001).
48
+ */
49
+ type EnvelopePoint = readonly [time: number | `${number}d`, value: number, curve: 'set' | Curve] | readonly [time: number | `${number}d`, value: number, curve: 'set' | Curve, fixed: 'fixed'];
50
+ type Envelope = readonly EnvelopePoint[];
51
+ type FilterSpec = {
52
+ readonly type: BiquadFilterType;
53
+ readonly frequency: Automation;
54
+ readonly q?: Range;
55
+ /** The BiquadFilter gain (shelf and peaking types). */
56
+ readonly gain?: Range;
57
+ /** A gain node after the filter, for a parallel bank where each band has its own level (the game's scream formants). */
58
+ readonly level?: Range;
59
+ };
60
+ type FilterBank = {
61
+ readonly mode: 'serial' | 'parallel';
62
+ readonly bank: readonly FilterSpec[];
63
+ };
64
+ /** An LFO. `depth` is absolute, or a factor of the source start frequency. With `base`, gain modulation goes through its own gain stage (the game rotor). */
65
+ type Modulator = {
66
+ readonly target: 'gain' | 'frequency';
67
+ /** Left out, the oscillator keeps its default (sine) and no type is set. */
68
+ readonly wave?: OscillatorType;
69
+ readonly rate: Range;
70
+ readonly depth: Range | {
71
+ readonly scaleOf: 'frequency';
72
+ readonly factor: number;
73
+ };
74
+ readonly base?: number;
75
+ };
76
+ /**
77
+ * Noise from the engine's seeded buffer. `offset` is where playback starts in it: `'random'` (random × `offsetScale`, default 1), `'fit'`
78
+ * (random × (buffer length − duration)) or seconds. `loop` sets the source to loop (the art scene sets it on every noise source).
79
+ */
80
+ type NoiseSource = {
81
+ readonly kind: 'noise';
82
+ readonly offset?: 'random' | 'fit' | number;
83
+ readonly offsetScale?: number;
84
+ readonly loop?: boolean;
85
+ };
86
+ type OscSource = {
87
+ readonly kind: 'osc';
88
+ readonly wave: OscillatorType;
89
+ readonly frequency: Automation;
90
+ };
91
+ /** The art scene's `voice`: an oscillator with vibrato through formant bands, plus a band of breath noise, summed and boosted. */
92
+ type FormantVoiceSource = {
93
+ readonly kind: 'formant_voice';
94
+ readonly wave: OscillatorType;
95
+ readonly frequency: Automation;
96
+ /** Vibrato rate, and depth as a factor of the start frequency. */
97
+ readonly vibrato: {
98
+ readonly rate: Range;
99
+ readonly depth: number;
100
+ };
101
+ readonly formants: readonly {
102
+ readonly frequency: Range;
103
+ readonly q: number;
104
+ }[];
105
+ readonly breath: {
106
+ readonly frequency: Range;
107
+ readonly q: number;
108
+ readonly gain: Range;
109
+ };
110
+ readonly mix: number;
111
+ };
112
+ type LayerSource = NoiseSource | OscSource | FormantVoiceSource;
113
+ type Layer = {
114
+ /** Start, seconds after the sound starts. */
115
+ readonly at?: Range;
116
+ /** Overrides the recipe duration for this layer. */
117
+ readonly duration?: Range;
118
+ /** Seconds the source keeps running after the duration (the `+ 0.05` of the team routines). Default: the recipe's, else 0.05. */
119
+ readonly tail?: number;
120
+ /** The layer played `count` times, `spacing` seconds apart (the game's animal calls). */
121
+ readonly repeat?: {
122
+ readonly count: number;
123
+ readonly spacing: number;
124
+ };
125
+ /** Layer level before distance. Default 1. */
126
+ readonly gain?: Range;
127
+ /** Send to the reverb, 0..1 or more (the art scene uses up to 1.2). */
128
+ readonly wet?: number;
129
+ readonly source: LayerSource;
130
+ readonly filters?: FilterBank;
131
+ readonly modulators?: readonly Modulator[];
132
+ readonly envelope: Envelope;
133
+ };
134
+ /**
135
+ * Distance falloff of a sound's level:
136
+ * - `inverse_square`: `volume / (1 + d² × k)`; `cutoffK` also divides every lowpass cutoff by `1 + d × cutoffK` (the game's muffling);
137
+ * - `linear`: `max(0, 1 − d / range)`, silent at or below `minGain`;
138
+ * - `none`: no distance.
139
+ * `scale` multiplies the level (the game's boom is 1.4 at the source), and below `minGain` the sound is not played.
140
+ */
141
+ type Falloff = {
142
+ readonly model: 'inverse_square';
143
+ readonly k: number;
144
+ readonly minGain?: number;
145
+ readonly cutoffK?: number;
146
+ readonly scale?: number;
147
+ } | {
148
+ readonly model: 'linear';
149
+ readonly range: number;
150
+ readonly minGain?: number;
151
+ readonly scale?: number;
152
+ } | {
153
+ readonly model: 'none';
154
+ };
155
+ /**
156
+ * The order the realizer creates and wires the nodes in. Each one is the order of the team routine it reproduces, so the trace of the SDK
157
+ * matches the trace of the team code node for node:
158
+ * - `layered` (default): the art scene's `sNoise`, `sTone` and `voice`; every layer has its own output stage (distance gain, pan, reverb send);
159
+ * - `burst`: the game's `sfx` (jogo L62089);
160
+ * - `scream`: the game's `grito` (L62128);
161
+ * - `groan`: the game's `voz` (L62143);
162
+ * - `boom`: the game's `boom` (L68278);
163
+ * - `static`: the game's `chiado` (L69998);
164
+ * - `chord`: the game's `buzina` (L70272);
165
+ * - `calls`: the game's `bichoSom` (L66625);
166
+ * - `siren`: the art scene's `siren` (L1369);
167
+ * - `weapon`: the weapon audio of the kit (`weapons-three`): per layer a filtered noise burst or an oscillator, then its own gain, chained to the bus.
168
+ * Every order but `layered` puts the level into the envelope and connects straight to the bus.
169
+ */
170
+ declare const BUILD_ORDERS: readonly ["layered", "burst", "scream", "groan", "boom", "static", "chord", "calls", "siren", "weapon"];
171
+ type BuildOrder = (typeof BUILD_ORDERS)[number];
172
+ type SoundRecipe = {
173
+ readonly id: string;
174
+ readonly bus: BusId;
175
+ readonly order?: BuildOrder;
176
+ /** Named numbers drawn first, in the order written; layers read them with `{ param }`. */
177
+ readonly vars?: Readonly<Record<string, Range>>;
178
+ readonly duration?: Range;
179
+ readonly tail?: number;
180
+ readonly layers: readonly Layer[];
181
+ readonly falloff?: Falloff;
182
+ /** Stereo pan from the listener (the art scene's `panFor`). Only the `layered` order has an output stage to pan. */
183
+ readonly pan?: boolean;
184
+ /** Voice group for the limiter (`undead_voice`...). */
185
+ readonly group?: string;
186
+ /** Limiter priority; default from the bus (weapons > voices > world > ambience). */
187
+ readonly priority?: number;
188
+ /**
189
+ * The team routine creates nodes before it checks the level (the game's `bichoSom` creates its gain and filter first): a silent sound is kept as
190
+ * a plan with `silent: true` so the realizer repeats those nodes. Off by default.
191
+ */
192
+ readonly keepSilent?: boolean;
193
+ /** Where the numbers come from: `"jogo:L62128"`, `"cena:L1342"`, or `"sdk"` for values the kit chose. */
194
+ readonly sources: readonly string[];
195
+ };
196
+ /**
197
+ * The order the loop realizer creates the nodes in:
198
+ * - `game_loop`: the game's `loopSet` (L62109): gain and filter first, nodes created the first time the volume passes `startAbove`, gain from 0;
199
+ * - `chain_loop`: the art scene's loops in `initAudio` (L1314–1316): source, filters, gain at its starting volume, created when the loop opens.
200
+ */
201
+ type LoopOrder = 'game_loop' | 'chain_loop';
202
+ type LoopRecipe = {
203
+ readonly id: string;
204
+ readonly bus: BusId;
205
+ readonly order?: LoopOrder;
206
+ /** An oscillator loop may leave the frequency out: it starts at the default and `set({ frequency })` glides it (the game's motor). */
207
+ readonly source: (NoiseSource & {
208
+ readonly loop: true;
209
+ }) | {
210
+ readonly kind: 'osc';
211
+ readonly wave: OscillatorType;
212
+ readonly frequency?: Automation;
213
+ readonly loop: true;
214
+ };
215
+ readonly filters?: FilterBank;
216
+ readonly modulators?: readonly Modulator[];
217
+ /** `setTargetAtTime` time constants (jogo L62125–62126: 0.15 for the volume, 0.08 for the frequency). */
218
+ readonly smoothing?: {
219
+ readonly volume: number;
220
+ readonly frequency: number;
221
+ };
222
+ /** Nodes are created the first time the volume reaches this (jogo L62112: 0.002). */
223
+ readonly startAbove?: number;
224
+ /** Volume the gain starts at when the loop opens (`chain_loop`). */
225
+ readonly volume?: number;
226
+ readonly sources: readonly string[];
227
+ };
228
+ type SoundBank = {
229
+ readonly id: string;
230
+ readonly sounds?: readonly SoundRecipe[];
231
+ readonly loops?: readonly LoopRecipe[];
232
+ };
233
+
234
+ /** Where the ears are. `yaw` turns around y like a three.js camera: the right ear points at `(cos yaw, 0, −sin yaw)`. */
235
+ type Listener = {
236
+ readonly position: Vec3;
237
+ readonly yaw: number;
238
+ };
239
+ type DistanceMode = 'xz' | '3d';
240
+ declare const DEFAULT_LISTENER: Listener;
241
+ /** The art scene clamps the pan to ±0.85 and never divides by less than 2 m (cena L1319). */
242
+ declare const PAN_LIMIT = 0.85;
243
+ declare const PAN_MIN_DISTANCE = 2;
244
+ /** Distance from the listener: on the ground (`xz`, the teams' choice) or in 3D. */
245
+ declare function listenerDistance(listener: Listener, position: Vec3, mode?: DistanceMode): number;
246
+ /** The art scene's `panFor`: the side the sound is on, −0.85 (left) to 0.85 (right). No HRTF, for cost. */
247
+ declare function panFor(listener: Listener, position: Vec3): number;
248
+
249
+ /** How an AudioParam moves, with every number resolved and every time in seconds from the layer start. */
250
+ type PlannedAutomation = {
251
+ /** Assigned to `.value`. */
252
+ readonly value?: number;
253
+ /** `setValueAtTime(from)` at the layer start. */
254
+ readonly from?: number;
255
+ /** Ramp target, reached `at` seconds after the layer start. */
256
+ readonly to?: number;
257
+ readonly curve?: Curve;
258
+ readonly at?: number;
259
+ };
260
+ type PlannedFilter = {
261
+ readonly type: BiquadFilterType;
262
+ readonly frequency: PlannedAutomation;
263
+ readonly q?: number;
264
+ readonly gain?: number;
265
+ readonly level?: number;
266
+ };
267
+ type PlannedModulator = {
268
+ readonly target: 'gain' | 'frequency';
269
+ readonly wave?: OscillatorType;
270
+ readonly rate: number;
271
+ readonly depth: number;
272
+ readonly base?: number;
273
+ };
274
+ type PlannedSource = {
275
+ readonly kind: 'noise';
276
+ readonly offset: number;
277
+ readonly loop: boolean;
278
+ } | {
279
+ readonly kind: 'osc';
280
+ readonly wave: OscillatorType;
281
+ readonly frequency: PlannedAutomation;
282
+ } | {
283
+ readonly kind: 'formant_voice';
284
+ readonly wave: OscillatorType;
285
+ readonly frequency: PlannedAutomation;
286
+ readonly vibrato: {
287
+ readonly rate: number;
288
+ readonly depth: number;
289
+ };
290
+ readonly formants: readonly {
291
+ readonly frequency: number;
292
+ readonly q: number;
293
+ }[];
294
+ readonly breath: {
295
+ readonly frequency: number;
296
+ readonly q: number;
297
+ readonly gain: number;
298
+ readonly offset: number;
299
+ };
300
+ readonly mix: number;
301
+ };
302
+ type PlannedEnvelopePoint = {
303
+ readonly time: number;
304
+ readonly value: number;
305
+ readonly curve: 'set' | Curve;
306
+ };
307
+ type PlannedLayer = {
308
+ readonly at: number;
309
+ readonly duration: number;
310
+ readonly tail: number;
311
+ /** Layer gain × the sound's level: in the envelope for every order but `layered`, in the output stage for `layered`. */
312
+ readonly level: number;
313
+ readonly wet?: number;
314
+ readonly source: PlannedSource;
315
+ readonly filters?: {
316
+ readonly mode: 'serial' | 'parallel';
317
+ readonly bank: readonly PlannedFilter[];
318
+ };
319
+ readonly modulators: readonly PlannedModulator[];
320
+ readonly envelope: readonly PlannedEnvelopePoint[];
321
+ };
322
+ /** A sound with every number decided: what the realizer builds, what goldens compare and what the bench sheet prints. */
323
+ type SoundPlan = {
324
+ readonly id: string;
325
+ readonly bus: BusId;
326
+ readonly order: BuildOrder;
327
+ readonly group?: string;
328
+ readonly priority: number;
329
+ /** The level from volume and distance, before layer gains. */
330
+ readonly level: number;
331
+ readonly distance: number;
332
+ readonly pan?: number;
333
+ /** Too quiet to hear, kept only for a recipe with `keepSilent`. */
334
+ readonly silent: boolean;
335
+ /** Seconds until the last source stops. */
336
+ readonly length: number;
337
+ readonly layers: readonly PlannedLayer[];
338
+ };
339
+ type PlanInput = {
340
+ /** Where the numbers are drawn from. Default: a fresh stream with seed 1. */
341
+ readonly random?: () => number;
342
+ readonly volume?: number;
343
+ /** Distance in meters; overrides `position`. */
344
+ readonly distance?: number;
345
+ readonly position?: Vec3;
346
+ readonly listener?: Listener;
347
+ readonly distanceMode?: DistanceMode;
348
+ readonly params?: Readonly<Record<string, number>>;
349
+ /** 0..1 from the host's occlusion callback: gain × (1 − 0.5 × o), lowpass cutoffs × (1 − 0.6 × o). */
350
+ readonly occlusion?: number;
351
+ /** Length of the noise buffer, for `offset: 'fit'`. Default 1.5. */
352
+ readonly noiseSeconds?: number;
353
+ };
354
+ /**
355
+ * Turns a recipe into a plan: pure, Node-safe, the same plan for the same `random`. Numbers are drawn in this order: `vars` (in the order written),
356
+ * `duration`, then each layer in the order its fields are written, the noise start point last in its layer. A sound below its falloff's `minGain`
357
+ * gives `null` (or a silent plan with `keepSilent`) before any number is drawn.
358
+ */
359
+ declare function planSound(recipe: SoundRecipe, input?: PlanInput): SoundPlan | null;
360
+
361
+ /** The distance curves of the team routines, by name. Each recipe carries its own; these are the values to copy. */
362
+ declare const DISTANCE_CURVES: {
363
+ /** The game's `sfx`: `vol / (1 + d² × 0.004)`, cutoff `/ (1 + d × 0.01)`, silent below 0.01 (jogo L62091–62092, L62101). */
364
+ readonly game_sfx: {
365
+ readonly model: "inverse_square";
366
+ readonly k: 0.004;
367
+ readonly cutoffK: 0.01;
368
+ readonly minGain: 0.01;
369
+ };
370
+ /** The game's `grito`: `1 / (1 + d² × 0.006)`, silent below 0.02 (jogo L62129–62130). */
371
+ readonly game_scream: {
372
+ readonly model: "inverse_square";
373
+ readonly k: 0.006;
374
+ readonly minGain: 0.02;
375
+ };
376
+ /** The game's `voz`: `1 / (1 + d² × 0.01)`, silent below 0.02 (jogo L62146–62147). */
377
+ readonly game_voice: {
378
+ readonly model: "inverse_square";
379
+ readonly k: 0.01;
380
+ readonly minGain: 0.02;
381
+ };
382
+ /** The game's `boom`: `1.4 / (1 + d² × 0.002)`, never silent (jogo L68280). */
383
+ readonly game_boom: {
384
+ readonly model: "inverse_square";
385
+ readonly k: 0.002;
386
+ readonly scale: 1.4;
387
+ };
388
+ /** The art scene's `outAt`: `vol / (1 + d² × 0.008)` (cena L1320). */
389
+ readonly art_scene: {
390
+ readonly model: "inverse_square";
391
+ readonly k: 0.008;
392
+ };
393
+ /** The game's animal calls: `max(0, 1 − d / 70) × 0.25`, silent at or below 0.005 (jogo L66627–66628). */
394
+ readonly animals: {
395
+ readonly model: "linear";
396
+ readonly range: 70;
397
+ readonly scale: 0.25;
398
+ readonly minGain: 0.005;
399
+ };
400
+ };
401
+ type DistanceCurveName = keyof typeof DISTANCE_CURVES;
402
+ /** The level at `distance` for a sound at `volume`, before the recipe's envelope. */
403
+ declare function falloffGain(falloff: Falloff | undefined, volume: number, distance: number): number;
404
+ /** Whether a level is too quiet to play: below `minGain` (the game's `att < 0.01`), or at or below it for `linear` (the animal calls' `v <= 0.005`). */
405
+ declare function isSilent(falloff: Falloff | undefined, gain: number): boolean;
406
+ /** What every lowpass cutoff is divided by at `distance` (1 when the curve has no muffling). */
407
+ declare function cutoffDivisor(falloff: Falloff | undefined, distance: number): number;
408
+
409
+ /**
410
+ * Rules for a group of voices (`undead_voice`...). A request passes when every rule passes:
411
+ * - `maxConcurrent`: at most this many of the group play at once (`steal` replaces the group's oldest instead of refusing);
412
+ * - `cooldown`: seconds after an accepted voice before the next one (the game's `groanBudget`, jogo L66986 and L70472);
413
+ * - `maxDistance`: only voices nearer than this (the game's `d < 40`).
414
+ */
415
+ type VoiceGroup = {
416
+ readonly maxConcurrent?: number;
417
+ readonly cooldown?: number;
418
+ readonly maxDistance?: number;
419
+ readonly steal?: boolean;
420
+ readonly sources?: readonly string[];
421
+ };
422
+ /** `undead_voice`: the art scene's five at once with the game's 0.25 s cooldown and 40 m. */
423
+ declare const VOICE_GROUPS: Readonly<Record<string, VoiceGroup>>;
424
+ /** Q7: weapons > voices > world > ambience; the interface above all, weather with the ambience. */
425
+ declare const BUS_PRIORITY: Readonly<Record<BusId, number>>;
426
+ declare const DEFAULT_MAX_VOICES = 32;
427
+ declare function busPriority(bus: BusId): number;
428
+ type VoiceRequest = {
429
+ readonly group?: string;
430
+ readonly priority: number;
431
+ /** Planned level; the global cap drops the lowest `priority × gain`. */
432
+ readonly gain: number;
433
+ readonly distance?: number;
434
+ /** Seconds the voice lasts. */
435
+ readonly length: number;
436
+ };
437
+ type VoiceRefusal = 'cooldown' | 'distance' | 'group_full' | 'global_full';
438
+ type VoiceDecision = {
439
+ readonly accepted: true;
440
+ readonly id: number;
441
+ readonly evicted: readonly number[];
442
+ } | {
443
+ readonly accepted: false;
444
+ readonly reason: VoiceRefusal;
445
+ };
446
+ type VoiceStats = {
447
+ readonly active: number;
448
+ readonly peak: number;
449
+ readonly dropped: number;
450
+ readonly maxVoices: number;
451
+ };
452
+ type VoiceManager = {
453
+ request(request: VoiceRequest): VoiceDecision;
454
+ /** Ends a voice early (stopped by the host). */
455
+ release(id: number): void;
456
+ /** Sets how long an accepted voice lasts (the engine admits a voice before its random length is drawn). */
457
+ setLength(id: number, seconds: number): void;
458
+ /** Moves time on: cooldowns count down and finished voices leave. Returns the ids that ended. */
459
+ advance(dt: number): number[];
460
+ setGroup(id: string, group: VoiceGroup | undefined): void;
461
+ stats(): VoiceStats;
462
+ };
463
+ type VoiceManagerOptions = {
464
+ /** Default `VOICE_GROUPS`. */
465
+ readonly groups?: Readonly<Record<string, VoiceGroup>>;
466
+ readonly maxVoices?: number;
467
+ };
468
+ /** The voice limiter: per-group rules, a global cap and priority dropping. Pure and time-driven (`advance`), so goldens script it frame by frame. */
469
+ declare function createVoiceManager(options?: VoiceManagerOptions): VoiceManager;
470
+
471
+ /** jogo L62112: a loop's nodes exist only once its volume reaches 0.002. */
472
+ declare const LOOP_START_ABOVE = 0.002;
473
+ /** jogo L62125–62126: `setTargetAtTime` time constants. */
474
+ declare const LOOP_SMOOTHING: {
475
+ readonly volume: 0.15;
476
+ readonly frequency: 0.08;
477
+ };
478
+ /** What the realizer does for one `set`: create the nodes first (once), then glide the volume and, for an oscillator, the frequency. */
479
+ type LoopAction = {
480
+ readonly create: boolean;
481
+ readonly volume: number;
482
+ readonly frequency?: number;
483
+ };
484
+ type LoopState = {
485
+ /** `null` while the loop has no nodes and the volume is still below the start threshold. */
486
+ set(state: {
487
+ readonly volume?: number;
488
+ readonly frequency?: number;
489
+ }): LoopAction | null;
490
+ /** Whether the nodes exist. */
491
+ readonly created: boolean;
492
+ /** The nodes were torn down (`stop`): the next audible `set` creates them again. */
493
+ reset(): void;
494
+ };
495
+ /**
496
+ * The game's `loopSet` as state: nodes on the first volume at or above `startAbove`, then every `set` glides. The volume set last is kept, so a
497
+ * `set` of only the frequency keeps the level. `chain_loop` recipes open with their nodes already made (`open` is true).
498
+ */
499
+ declare function createLoopState(recipe: LoopRecipe, open?: boolean): LoopState;
500
+
501
+ type BusSetting = {
502
+ readonly gain?: number;
503
+ readonly muted?: boolean;
504
+ };
505
+ /**
506
+ * A mixing chain. `master` → (compressor) → output; with `reverb`, a wet bus → convolver (an impulse from the engine's seed) → master. `noise` is
507
+ * the shared noise buffer: its length, and whether it is made with the chain (`eager`, the art scene) or by the first sound that needs it (`lazy`,
508
+ * the game). Buses sit under the master and have no node until one is changed.
509
+ */
510
+ type MixProfile = {
511
+ readonly id: string;
512
+ readonly master: number;
513
+ readonly compressor?: {
514
+ readonly threshold: number;
515
+ readonly ratio: number;
516
+ readonly knee?: number;
517
+ readonly attack?: number;
518
+ readonly release?: number;
519
+ };
520
+ readonly reverb?: {
521
+ readonly seconds: number;
522
+ readonly decay: number;
523
+ readonly wet: number;
524
+ };
525
+ readonly noise: {
526
+ readonly seconds: number;
527
+ readonly create: 'lazy' | 'eager';
528
+ };
529
+ readonly buses?: Readonly<Partial<Record<BusId, BusSetting>>>;
530
+ readonly sources: readonly string[];
531
+ };
532
+ /** The two team chains: `game` (master 0.5, jogo L62080) and `art_scene` (compressor and reverb, cena L1305–1313). */
533
+ declare const MIX_PROFILES: Readonly<Record<string, MixProfile>>;
534
+ /** Q1: the kit plays through the art scene chain (compressor and reverb); `mix: 'game'` reverts. */
535
+ declare const DEFAULT_MIX = "art_scene";
536
+ /** A profile by id from `profiles`, or the profile itself. */
537
+ declare function resolveMix(mix: string | MixProfile, profiles?: Readonly<Record<string, MixProfile>>): MixProfile;
538
+ /** The gain a bus plays at: 0 when muted, its gain (default 1) otherwise. */
539
+ declare function busGain(setting: BusSetting | undefined): number;
540
+
541
+ type DeepPartial<TValue> = TValue extends readonly unknown[] ? TValue : TValue extends object ? {
542
+ readonly [Key in keyof TValue]?: DeepPartial<TValue[Key]>;
543
+ } : TValue;
544
+ /** Every sound and loop the engine knows, by id. Banks register in order; a later bank replaces an earlier recipe with the same id. */
545
+ type SoundRegistry = {
546
+ register(bank: SoundBank): void;
547
+ /** Merged into the recipe by id (objects merge, arrays such as `layers` are replaced); an unknown id is ignored. */
548
+ patch(id: string, patch: DeepPartial<SoundRecipe> | DeepPartial<LoopRecipe>): void;
549
+ remove(id: string): void;
550
+ sound(id: string): SoundRecipe | undefined;
551
+ loop(id: string): LoopRecipe | undefined;
552
+ soundIds(): string[];
553
+ loopIds(): string[];
554
+ /** Which bank each id came from. */
555
+ bankOf(id: string): string | undefined;
556
+ };
557
+ declare function createSoundRegistry(banks?: readonly SoundBank[]): SoundRegistry;
558
+
559
+ /** One row of the game's `sfx` table (jogo L62096–62100): lowpass cutoff, peak gain and seconds of a noise burst. */
560
+ type NoiseClass = {
561
+ readonly id: string;
562
+ /** The name in the team code (`pistola`, `vidro`...). */
563
+ readonly team: string;
564
+ readonly bus: BusId;
565
+ readonly cutoff: number;
566
+ readonly gain: number;
567
+ readonly seconds: number;
568
+ };
569
+ /** The 12 game classes, in the table's order. */
570
+ declare const GAME_SFX_CLASSES: readonly NoiseClass[];
571
+ /** What the game plays for a name it does not know: `[1000, 0.5, 0.1]`. */
572
+ declare const GAME_SFX_FALLBACK: NoiseClass;
573
+ /**
574
+ * A class as a recipe: noise from a random point, the class lowpass (muffled by distance), the peak set at once and an exponential fall to 0.001 over
575
+ * the class seconds, stopped 0.05 s later; the game's distance curve, silent below 0.01. Built in the game's `sfx` order (`burst`).
576
+ */
577
+ declare function noiseClassRecipe(row: NoiseClass, id?: string): SoundRecipe;
578
+ /** The game's classes as a bank (`sfx_pistol`... and `sfx_unknown`). Subject banks pick their own classes from `GAME_SFX_CLASSES`. */
579
+ declare const GAME_SFX_BANK: SoundBank;
580
+
581
+ declare const LIBRARY_SOUNDS: readonly SoundRecipe[];
582
+ declare const LIBRARY_LOOPS: readonly LoopRecipe[];
583
+ declare const LIBRARY_BANK: SoundBank;
584
+
585
+ /** A continuous sound: `set` glides the volume and, for an oscillator, the frequency; `stop` tears the nodes down. */
586
+ type AudioLoopHandle = {
587
+ set(state: {
588
+ readonly volume?: number;
589
+ readonly frequency?: number;
590
+ }): void;
591
+ stop(): void;
592
+ };
593
+
594
+ type PlayOptions = {
595
+ readonly position?: Vec3;
596
+ /** Meters; overrides `position` for the distance (not for the pan). */
597
+ readonly distance?: number;
598
+ readonly volume?: number;
599
+ readonly params?: Readonly<Record<string, number>>;
600
+ /** This sound's own stream: same seed, same sound. */
601
+ readonly seed?: number;
602
+ /** Seconds from now. */
603
+ readonly delay?: number;
604
+ /** A named stream of the engine (`streamFor(seed, name)`), so one subject never shifts another's sounds. */
605
+ readonly channel?: string;
606
+ };
607
+ type VoiceHandle = {
608
+ readonly id: number;
609
+ readonly plan: SoundPlan;
610
+ stop(): void;
611
+ };
612
+ /** A subject's own random stream; `ownNoise` also gives it its own noise buffer, filled from that stream (the order `createWeaponAudio` uses). */
613
+ type AudioChannel = {
614
+ readonly random: () => number;
615
+ play(id: string, options?: PlayOptions): VoiceHandle | null;
616
+ plan(id: string, options?: PlayOptions): SoundPlan | null;
617
+ };
618
+ type AudioStats = {
619
+ readonly context: 'none' | AudioContextState;
620
+ readonly voices: number;
621
+ readonly peak: number;
622
+ readonly dropped: number;
623
+ readonly maxVoices: number;
624
+ readonly loops: number;
625
+ };
626
+ type AudioEngineOptions = {
627
+ /** Seeds the engine stream: the noise, the reverb impulse and every sound played without its own seed or channel. Default 1. */
628
+ readonly seed?: number;
629
+ /** Default `art_scene` (Q1). */
630
+ readonly mix?: string | MixProfile;
631
+ readonly profiles?: Readonly<Record<string, MixProfile>>;
632
+ readonly banks?: readonly SoundBank[];
633
+ readonly maxVoices?: number;
634
+ readonly groups?: Readonly<Record<string, VoiceGroup>>;
635
+ readonly distance?: DistanceMode;
636
+ /** 0 (clear) to 1 (behind a wall) for a sound at a point; without it there is no occlusion (Q8). */
637
+ readonly occlusion?: (point: Vec3) => number;
638
+ readonly createContext?: () => AudioContext | null;
639
+ };
640
+ type AudioEngine = {
641
+ readonly registry: SoundRegistry;
642
+ register(bank: SoundBank): void;
643
+ /** Pure: the plan a sound would play with, from `options.seed` or the engine seed; draws nothing from the live streams. */
644
+ plan(id: string, options?: PlayOptions): SoundPlan | null;
645
+ play(id: string, options?: PlayOptions): VoiceHandle | null;
646
+ playPlan(plan: SoundPlan, options?: {
647
+ readonly delay?: number;
648
+ }): VoiceHandle | null;
649
+ loop(id: string, options?: {
650
+ readonly volume?: number;
651
+ }): AudioLoopHandle;
652
+ channel(options: {
653
+ readonly seed?: number;
654
+ readonly random?: () => number;
655
+ readonly ownNoise?: boolean;
656
+ }): AudioChannel;
657
+ setListener(listener: Listener): void;
658
+ setBus(id: BusId, setting: BusSetting): void;
659
+ setMix(mix: string | MixProfile): void;
660
+ /** The master volume of the current chain and of any later one (0 to 1). */
661
+ setMaster(gain: number): void;
662
+ /** Creates the context and the mixing chain if needed and resumes it; call from a click or a key. */
663
+ unlock(): void;
664
+ stats(): AudioStats;
665
+ /** RMS (0 to 1) of what a bus carries now, read from an analyser the first time you ask; sounds started before that do not reach it. 0 without a context. */
666
+ level(bus: BusId): number;
667
+ dispose(): void;
668
+ };
669
+ /**
670
+ * The sound engine: plans recipes and builds them on a lazily created AudioContext through one of the team mixing chains. Without an AudioContext
671
+ * (Node, workers) every call except `plan` does nothing.
672
+ */
673
+ declare function createAudioEngine(options?: AudioEngineOptions): AudioEngine;
674
+
675
+ /** Where a plan is built: the context, the bus it plays into, the reverb send (if the mix has one), the shared noise and the start time. */
676
+ type RealizeTarget = {
677
+ readonly context: AudioContext;
678
+ readonly output: AudioNode;
679
+ readonly wet?: AudioNode;
680
+ /** The noise buffer; made on the first call when the mix makes it on demand. */
681
+ readonly noise: () => AudioBuffer;
682
+ readonly time: number;
683
+ };
684
+ /** What `realize` built: every scheduled source (so the voice can be cut short) and `stop`. */
685
+ type RealizedVoice = {
686
+ readonly sources: readonly AudioScheduledSourceNode[];
687
+ stop(): void;
688
+ };
689
+
690
+ /** Builds a plan's nodes on `target`, in the node order of the team routine its `order` names. */
691
+ declare function realize(plan: SoundPlan, target: RealizeTarget): RealizedVoice;
692
+
693
+ type MixChain = {
694
+ readonly master: GainNode;
695
+ readonly wet?: GainNode;
696
+ disconnect(): void;
697
+ };
698
+ /**
699
+ * The mixing chain in the team order (cena L1308–1311; jogo L62080): compressor to the output, master into it (or straight to the output), then the
700
+ * convolver with an impulse drawn from `random` and the wet bus into it, back into the master.
701
+ */
702
+ declare function buildChain(context: AudioContext, profile: MixProfile, random: () => number): MixChain;
703
+
704
+ /** `length` samples of white noise, `random × 2 − 1`, drawn in order (jogo L62086, cena L1312). */
705
+ declare function fillNoise(random: () => number, length: number): Float32Array;
706
+ /**
707
+ * A reverb impulse: per channel, noise times `(1 − i / length)^decay` (cena L1310). `length` is `sampleRate × seconds` as the art scene computes it,
708
+ * which is not always whole (48000 × 2.2 = 105600.00000000001): the loop runs while `i < length`, so one more number is drawn than the buffer holds,
709
+ * exactly like the team code. The power is `exp(decay × log(base))`, which every engine computes the same.
710
+ */
711
+ declare function fillImpulse(random: () => number, options: {
712
+ readonly channels: number;
713
+ readonly length: number;
714
+ readonly decay: number;
715
+ }): Float32Array[];
716
+ /** An AudioBuffer holding the samples (one channel per array). */
717
+ declare function bufferOf(context: AudioContext, channels: readonly Float32Array[], requestedLength: number): AudioBuffer;
718
+
719
+ /** A new context from the page (`AudioContext` or the old `webkitAudioContext`), or `null` in Node and workers. */
720
+ declare function globalContext(): AudioContext | null;
721
+ /** Browsers start a context suspended until a click or key; call from one. */
722
+ declare function unlockContext(context: AudioContext): void;
723
+
724
+ /** An `AudioSink` (what a host lends its modules) that carries the engine behind it, so a module that wants more than `play` can use the engine. */
725
+ type EngineSink = AudioSink & {
726
+ readonly engine: AudioEngine;
727
+ };
728
+ /** Lends an engine as the host's `audio`: sounds by key, loops, the listener, and banks registered into the same engine. */
729
+ declare function createAudioSink(engine: AudioEngine): EngineSink;
730
+
731
+ /**
732
+ * Two probe sounds of the bench itself (a ping and a hiss), so panning, distance and the seed can be heard on something that has no subject. They are
733
+ * made by the kit, not delivered by any team (`sdk` in the sources).
734
+ */
735
+ declare const BENCH_BANK: SoundBank;
736
+ type AudioTestModuleOptions = {
737
+ /** The engine to test (default: the one the host lends in `host.audio`, else a new one of the module's own). */
738
+ engine?: AudioEngine;
739
+ /** Banks registered in the engine besides the bench's probes (default the game's noise classes and the library). */
740
+ banks?: readonly SoundBank[];
741
+ locale?: string;
742
+ /** Text of a sound or loop id (`sound.glass`) in the game's words; without it the bench shows the bank and the id. */
743
+ translate?: (key: string, locale: string) => string | undefined;
744
+ seed?: number;
745
+ /** First sound chosen (default `bench_ping`). */
746
+ sound?: string;
747
+ };
748
+ /** What the scenarios use of the context the test room passes. */
749
+ interface AudioScenarioLike {
750
+ expect(condition: boolean, label: string, data?: Record<string, unknown>): void;
751
+ }
752
+ type AudioTestModule = TestableModule<AudioScenarioLike> & {
753
+ /** The engine in use: the host's when it lent one, the module's own otherwise. */
754
+ readonly engine: AudioEngine;
755
+ /** The resolved plan of the chosen sound as text (the S3 view: every number decided). */
756
+ sheet(): string;
757
+ /** Plays the chosen sound as the bench is set. */
758
+ play(): VoiceHandle | null;
759
+ };
760
+ /**
761
+ * The sound area of a test room, written against the generic contracts only: it plays any sound of an engine at a distance and bearing from the listener
762
+ * (or from a probe standing in the area), runs loops, throws a storm of voices at the limiter, switches the mixing chain and its parts, reads the
763
+ * level of each bus and prints the resolved plan. The engine is the host's (`host.audio`) when lent, else the module's own.
764
+ */
765
+ declare function createAudioTestModule(options?: AudioTestModuleOptions): AudioTestModule;
766
+
767
+ type AudioBenchText = {
768
+ readonly group: string;
769
+ readonly name: string;
770
+ readonly probe: string;
771
+ readonly controls: Readonly<Record<'sound' | 'distance' | 'bearing' | 'volume' | 'mix' | 'seed' | 'play' | 'voice_storm' | 'loop' | 'loop_level' | 'loop_pitch' | 'source', string>>;
772
+ readonly hints: Readonly<Record<'distance' | 'bearing' | 'mix' | 'seed' | 'voice_storm' | 'loop_pitch' | 'source', string>>;
773
+ readonly options: Readonly<Record<'none' | 'bench' | 'probe', string>>;
774
+ readonly layers: Readonly<Record<'reverb' | 'compressor' | 'panning' | 'muffling', string>>;
775
+ readonly metrics: Readonly<Record<'voices' | 'peak' | 'dropped' | 'loops' | 'context' | 'plan_length' | 'plan_level' | 'plan_pan' | 'level_weapons' | 'level_voices' | 'level_world' | 'level_ambience' | 'level_weather' | 'level_ui', string>>;
776
+ readonly scenarios: Readonly<Record<'panning' | 'distance' | 'determinism' | 'limiter', string>>;
777
+ readonly mixes: Readonly<Record<string, string>>;
778
+ /** Text of a sound or loop id when the game has one; the bench falls back to the id. */
779
+ readonly sheet: Readonly<Record<'title' | 'level' | 'distance' | 'pan' | 'length' | 'silent' | 'layer' | 'none', string>>;
780
+ };
781
+ declare const audioBenchTextOf: (locale: string) => AudioBenchText;
782
+
783
+ export { type AudioBenchText, type AudioChannel, type AudioEngine, type AudioEngineOptions, type AudioLoopHandle, type AudioScenarioLike, type AudioStats, type AudioTestModule, type AudioTestModuleOptions, type Automation, BENCH_BANK, BUILD_ORDERS, BUS_IDS, BUS_PRIORITY, type BuildOrder, type BusId, type BusSetting, type Curve, DEFAULT_LISTENER, DEFAULT_MAX_VOICES, DEFAULT_MIX, DISTANCE_CURVES, type DeepPartial, type DistanceCurveName, type DistanceMode, type EngineSink, type Envelope, type EnvelopePoint, type Falloff, type FilterBank, type FilterSpec, type FormantVoiceSource, GAME_SFX_BANK, GAME_SFX_CLASSES, GAME_SFX_FALLBACK, LIBRARY_BANK, LIBRARY_LOOPS, LIBRARY_SOUNDS, LOOP_SMOOTHING, LOOP_START_ABOVE, type Layer, type LayerSource, type Listener, type LoopAction, type LoopOrder, type LoopRecipe, type LoopState, MIX_PROFILES, type MixChain, type MixProfile, type Modulator, type NoiseClass, type NoiseSource, type OscSource, PAN_LIMIT, PAN_MIN_DISTANCE, type ParamRef, type PlanInput, type PlannedAutomation, type PlannedEnvelopePoint, type PlannedFilter, type PlannedLayer, type PlannedModulator, type PlannedSource, type PlayOptions, type Range, type RealizeTarget, type RealizedVoice, type SoundBank, type SoundPlan, type SoundRecipe, type SoundRegistry, type Time, VOICE_GROUPS, type VoiceDecision, type VoiceGroup, type VoiceHandle, type VoiceManager, type VoiceManagerOptions, type VoiceRefusal, type VoiceRequest, type VoiceStats, audioBenchTextOf, bufferOf, buildChain, busGain, busPriority, createAudioEngine, createAudioSink, createAudioTestModule, createLoopState, createRng, createSoundRegistry, createVoiceManager, cutoffDivisor, drawBetween, falloffGain, fillImpulse, fillNoise, globalContext, isSilent, listenerDistance, noiseClassRecipe, panFor, planSound, realize, resolveMix, streamFor, streamSeed, unlockContext };