@adatechnology/game-audio 0.7.0 → 0.9.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.
package/README.md CHANGED
@@ -103,5 +103,73 @@ engine.play('pistol_9mm', { position: [10, 0, -40], environment: 'indoor', delay
103
103
  - Each play: a take drawn among all but the last one (round-robin, never twice in a row), the rate ± `pitch` (default 4 %), the level by `falloff`, a lowpass by distance (`cutoffK`) and behind a wall (`occlusion`), the pan.
104
104
  - Sample voices of a bus go through a zero-delay limiter (ceiling −1 dBFS); `choke: { voices }` fades the oldest plays of a sound for rapid fire.
105
105
  - `onVoiceStart` reports every voice (recipe or sample, the files and takes, `requestedAt` / `startAt`): the event log the criteria measure.
106
- - Files: drop them in `assets/sfx-inbox/<categoria>/` with a line in its `CREDITOS.md` and run `pnpm --filter @adatechnology/game-audio sfx:convert` (ffmpeg): Ogg Vorbis mono 48 kHz, −1 dBFS peak, attack on the first ms, `manifest.json` and `CREDITOS.md`. Only CC0 and CC-BY go into the SDK.
106
+ - **The kit's default sounds ship with it**: `assets/sfx/<categoria>/<nome>.ogg` (1050 takes, CC0 and CC-BY only), with `assets/sfx/manifest.json`
107
+ (sound id → path, with source, author and licence) and `assets/sfx/CREDITOS.md` (who to credit for a CC-BY sound). A game serves that folder and
108
+ passes it as `baseUrl`; a take whose file is missing plays its fitted copy (or the recipe of its id). The originals the converter reads stay
109
+ outside the kit (ZombiAdA `assets/sfx-originais/`).
110
+ - **A game plays its own files for any id**: `pointSamples(engine, { pistol_9mm: ['armas/tiro_1.ogg', 'armas/tiro_2.ogg'] }, { baseUrl: './sfx/' })`
111
+ after `loadKitSamples` keeps the kit's numbers for the id (bus, level, falloff) and plays the game's takes; an id the kit lacks is added. A whole
112
+ folder laid out like the kit's, with its own `manifest.json`: `loadKitSamples(engine, { baseUrl: './game-sfx/', id: 'game_samples' })` after the
113
+ kit's. Nothing in the kit changes.
114
+ - On this machine, `scripts/sfx-dir.mjs` says where the files are: the kit's own folder, or `KIT_SFX_DIR` / `KIT_SFX_INBOX`, or `.sfx-local.json` at
115
+ the repo root (ignored by git) `{ "kit": "../zombiada/assets/sfx", "inbox": "../zombiada/assets/sfx-originais" }` to try a game's set in the
116
+ kit's pages. The test room links them into `dist/sfx/`. Git (CI and pre-commit, `tools/local-audio/local-audio.mjs guard-git`) takes only the
117
+ manifest's CC0 and CC-BY files under `assets/sfx/`, no other audio.
118
+ - New files: the original in `<inbox>/<categoria>/` with a line in its `CREDITOS.md`, then `pnpm --filter @adatechnology/game-audio sfx:convert`
119
+ (ffmpeg): Ogg Vorbis mono 48 kHz, −1 dBFS peak, attack on the first ms, written to the game's folder, with the manifest and credits updated here.
120
+ Only CC0 and CC-BY (the kit and the game are public).
107
121
  - Measuring: `measureCriteria({ sound, banks, loader })` renders C1–C9 through the real engine on an `OfflineAudioContext`; the test room's `?area=sound_check_area` (`createSoundCheckModule`) runs it on a button and records 3 s for C10. `CALIBRATION_SOUND` is a kit-made sample sound (no file) with every layer, for checking the path end to end.
122
+
123
+ ## Fitted copies of the recordings (`@adatechnology/game-audio/fitted`)
124
+
125
+ Where the files are not on the machine (CI, the published site, a clean clone) or have not loaded yet, a recorded id plays its **fitted copy**:
126
+ numbers measured from each take by `tools/sfx-sources/fit.mjs`, not audio. Each take becomes a recipe of white noise through fourteen bandpass bands
127
+ (35 Hz to 17 kHz, two biquads in series each), each band with its own gain curve down to -80 dB, plus the ringing partials as sines; the
128
+ takes of an id form a set, and a loop keeps its average band gains. Through the real engine, over the 136 one-shot sounds measured, they land a
129
+ median 2.3 dB from the recording (mean band error over time; the pistol 1.7, a punch 1.5) where the hand-made recipes were 26 dB away.
130
+
131
+ ```ts
132
+ import { registerFittedSounds } from '@adatechnology/game-audio/fitted'
133
+
134
+ registerFittedSounds(engine) // after the game's own banks: the fitted set replaces the older recipe of the same id, its recorded takes stay
135
+ ```
136
+
137
+ - `KIT_SAMPLE_ALIASES` are answered too (`shut_wood` plays the fitted `door_wood_shut`), and the bare id of tiered recordings plays the medium tier.
138
+ - `FITTED_KEEPS_RECIPE` (`fire_extinguish`, `gas_leak`) keep their recipes, which read params; `{ replace: false }` keeps every older recipe.
139
+ - Vehicles are not fitted: their loops follow the engine's RPM.
140
+ - After new recordings: `node tools/sfx-sources/fit.mjs` (ffmpeg, the files where `scripts/sfx-dir.mjs` finds them) rewrites `src/data/fitted-sfx.json`.
141
+
142
+ ## Local sound pack (your own files, by config)
143
+
144
+ A project can answer any SDK sound id with its own files, kept in its own folders and never in the SDK. A JSON config maps each id to its takes:
145
+
146
+ ```json
147
+ {
148
+ "version": 1,
149
+ "baseUrl": "audio/",
150
+ "sounds": {
151
+ "pistol_9mm": { "files": ["weapons/pistol_01.ogg", "weapons/pistol_02.ogg"], "license": "CC0" },
152
+ "door_wood_close": { "files": ["doors/close.ogg"], "license": "CC-BY", "score": 7 }
153
+ }
154
+ }
155
+ ```
156
+
157
+ ```ts
158
+ import { applyLocalSoundPack, parseLocalSoundPack } from '@adatechnology/game-audio'
159
+
160
+ const pack = applyLocalSoundPack(engine, parseLocalSoundPack(await (await fetch('audio/pack.json')).json()), { baseUrl: 'audio/' })
161
+ await pack.done // { file: ids now playing your files, fallback: ids whose files failed }
162
+ pack.unlicensed() // ids playing a file whose licence is not CC0 / CC-BY
163
+ pack.dispose() // every id gets its previous sound back
164
+ ```
165
+
166
+ - Urls are `baseUrl + file`: a folder served next to the page or any url (`https://cdn.example/sfx/`). `options.baseUrl` overrides the config's.
167
+ - Nothing runs in the frame a sound plays: each id's files are fetched and decoded in idle time (`requestIdleCallback`, one id per slot), once per
168
+ url and shared by every voice. Until they decode the id plays what it played before (the SDK recording, else the synthesized recipe); a file that
169
+ fails leaves the id on the SDK sound for good, decided once at load.
170
+ - `license` is `CC0`, `CC-BY`, `unknown` or `proprietary`. The sound panel (`createSoundCheckModule`, metric "File without licence") lists the ids
171
+ playing a file that is not CC0 / CC-BY: those may be tried locally but cannot ship.
172
+ - `parseLocalSoundPack` validates at the boundary and throws `LocalSoundPackError` with every problem at once.
173
+ - In this repo the test room reads `audio.local.json` at the root (git-ignored; see `examples/test-room/audio.local.example.json`, with `root` the
174
+ folder of the files): `tools/local-audio/local-audio.mjs link` symlinks it into `dist/local-audio/`, and `guard` / `guard-git` (build, site build,
175
+ pre-commit and CI) fail if a local file reaches a build or git.
@@ -1,7 +1,7 @@
1
1
  # Créditos dos sons
2
2
 
3
- Gerado por `scripts/sfx-convert.mjs` a partir de `assets/sfx-inbox/CREDITOS.md`. Pasta: `packages/game-audio/assets/sfx`.
4
- Só CC0 e CC-BY entram no SDK; quem usa um som CC-BY credita o autor abaixo.
3
+ Gerado por `scripts/sfx-convert.mjs` a partir do `CREDITOS.md` dos originais. Pasta: `packages/game-audio/assets/sfx` (os sons padrão do kit).
4
+ Só CC0 e CC-BY entram no SDK; quem usa um som CC-BY credita o autor abaixo. Um jogo pode apontar qualquer som para os seus arquivos (`pointSamples`).
5
5
 
6
6
  | arquivo | fonte | autor | licença |
7
7
  |---|---|---|---|
@@ -12088,6 +12088,54 @@
12088
12088
  }
12089
12089
  ]
12090
12090
  }
12091
+ },
12092
+ {
12093
+ "id": "mag_drop_concreto",
12094
+ "category": "armas",
12095
+ "layers": {
12096
+ "close": [
12097
+ {
12098
+ "file": "impactos/metal_fall_close_08.ogg",
12099
+ "sampleRate": 48000,
12100
+ "channels": 1,
12101
+ "peak": -1.05,
12102
+ "seconds": 0.418,
12103
+ "source": "https://opengameart.org/content/cc0-audio-uploader-rubberduck",
12104
+ "author": "rubberduck",
12105
+ "license": "CC0"
12106
+ },
12107
+ {
12108
+ "file": "impactos/metal_fall_close_09.ogg",
12109
+ "sampleRate": 48000,
12110
+ "channels": 1,
12111
+ "peak": -1.18,
12112
+ "seconds": 0.427,
12113
+ "source": "https://opengameart.org/content/cc0-audio-uploader-rubberduck",
12114
+ "author": "rubberduck",
12115
+ "license": "CC0"
12116
+ },
12117
+ {
12118
+ "file": "impactos/metal_fall_close_10.ogg",
12119
+ "sampleRate": 48000,
12120
+ "channels": 1,
12121
+ "peak": -1.1,
12122
+ "seconds": 0.403,
12123
+ "source": "https://opengameart.org/content/cc0-audio-uploader-rubberduck",
12124
+ "author": "rubberduck",
12125
+ "license": "CC0"
12126
+ },
12127
+ {
12128
+ "file": "itens-portas/item_drop_metal_05.ogg",
12129
+ "sampleRate": 48000,
12130
+ "channels": 1,
12131
+ "peak": -1.19,
12132
+ "seconds": 0.669,
12133
+ "source": "https://developers.meta.com/vr/downloads/package/oculus-audio-pack-1/",
12134
+ "author": "Oculus VR, LLC (\"Oculus Audio Pack 01\", via github.com/facebookincubator/Meta-open-sound)",
12135
+ "license": "CC-BY"
12136
+ }
12137
+ ]
12138
+ }
12091
12139
  }
12092
12140
  ]
12093
12141
  }
@@ -0,0 +1,391 @@
1
+ /**
2
+ * 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
3
+ * 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.
4
+ */
5
+ declare const BUS_IDS: readonly ["weapons", "voices", "world", "ambience", "weather", "ui"];
6
+ type BusId = (typeof BUS_IDS)[number];
7
+ /** A named number: from `PlayOptions.params` or the recipe's `vars`, times `scale`, plus `add`. */
8
+ type ParamRef = {
9
+ readonly param: string;
10
+ readonly scale?: number;
11
+ readonly add?: number;
12
+ };
13
+ /** A number, a `[min, max]` drawn uniformly (`min + random × (max − min)`), or a named number. */
14
+ type Range = number | readonly [min: number, max: number] | ParamRef;
15
+ /** Seconds from the layer start: a range, the layer `'duration'`, or `'0.6d'` (0.6 × the layer duration). */
16
+ type Time = Range | 'duration' | `${number}d`;
17
+ type Curve = 'linear' | 'exp';
18
+ /**
19
+ * How an AudioParam moves:
20
+ * - `{ set }` assigns `.value`;
21
+ * - `{ set, to, curve, over }` assigns `.value`, then ramps to `to` at `over`;
22
+ * - `{ 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).
23
+ */
24
+ type Automation = {
25
+ readonly set: Range;
26
+ readonly to?: Range | 'from';
27
+ readonly curve?: Curve;
28
+ readonly over?: Time;
29
+ } | {
30
+ readonly from: Range;
31
+ readonly to?: Range | 'from';
32
+ readonly curve?: Curve;
33
+ readonly over?: Time;
34
+ };
35
+ /**
36
+ * 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
37
+ * layer level (volume, distance and layer gain) except when the fourth item is `'fixed'` (the floor a ramp falls to, such as 0.001).
38
+ */
39
+ type EnvelopePoint = readonly [time: number | `${number}d`, value: number, curve: 'set' | Curve] | readonly [time: number | `${number}d`, value: number, curve: 'set' | Curve, fixed: 'fixed'];
40
+ type Envelope = readonly EnvelopePoint[];
41
+ type FilterSpec = {
42
+ readonly type: BiquadFilterType;
43
+ readonly frequency: Automation;
44
+ readonly q?: Range;
45
+ /** The BiquadFilter gain (shelf and peaking types). */
46
+ readonly gain?: Range;
47
+ /** A gain node after the filter, for a parallel bank where each band has its own level (the game's scream formants). */
48
+ readonly level?: Range;
49
+ };
50
+ type FilterBank = {
51
+ readonly mode: 'serial' | 'parallel';
52
+ readonly bank: readonly FilterSpec[];
53
+ };
54
+ /** 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). */
55
+ type Modulator = {
56
+ readonly target: 'gain' | 'frequency';
57
+ /** Left out, the oscillator keeps its default (sine) and no type is set. */
58
+ readonly wave?: OscillatorType;
59
+ readonly rate: Range;
60
+ readonly depth: Range | {
61
+ readonly scaleOf: 'frequency';
62
+ readonly factor: number;
63
+ };
64
+ readonly base?: number;
65
+ };
66
+ /**
67
+ * Noise from the engine's seeded buffer. `offset` is where playback starts in it: `'random'` (random × `offsetScale`, default 1), `'fit'`
68
+ * (random × (buffer length − duration)) or seconds. `loop` sets the source to loop (the art scene sets it on every noise source).
69
+ */
70
+ type NoiseSource = {
71
+ readonly kind: 'noise';
72
+ readonly offset?: 'random' | 'fit' | number;
73
+ readonly offsetScale?: number;
74
+ readonly loop?: boolean;
75
+ };
76
+ type OscSource = {
77
+ readonly kind: 'osc';
78
+ readonly wave: OscillatorType;
79
+ readonly frequency: Automation;
80
+ };
81
+ /** The art scene's `voice`: an oscillator with vibrato through formant bands, plus a band of breath noise, summed and boosted. */
82
+ type FormantVoiceSource = {
83
+ readonly kind: 'formant_voice';
84
+ readonly wave: OscillatorType;
85
+ readonly frequency: Automation;
86
+ /** Vibrato rate, and depth as a factor of the start frequency. */
87
+ readonly vibrato: {
88
+ readonly rate: Range;
89
+ readonly depth: number;
90
+ };
91
+ readonly formants: readonly {
92
+ readonly frequency: Range;
93
+ readonly q: number;
94
+ }[];
95
+ readonly breath: {
96
+ readonly frequency: Range;
97
+ readonly q: number;
98
+ readonly gain: Range;
99
+ };
100
+ readonly mix: number;
101
+ };
102
+ type LayerSource = NoiseSource | OscSource | FormantVoiceSource;
103
+ type Layer = {
104
+ /** Start, seconds after the sound starts. */
105
+ readonly at?: Range;
106
+ /** Overrides the recipe duration for this layer. */
107
+ readonly duration?: Range;
108
+ /** Seconds the source keeps running after the duration (the `+ 0.05` of the team routines). Default: the recipe's, else 0.05. */
109
+ readonly tail?: number;
110
+ /** The layer played `count` times, `spacing` seconds apart (the game's animal calls). */
111
+ readonly repeat?: {
112
+ readonly count: number;
113
+ readonly spacing: number;
114
+ };
115
+ /** Layer level before distance. Default 1. */
116
+ readonly gain?: Range;
117
+ /** Send to the reverb, 0..1 or more (the art scene uses up to 1.2). */
118
+ readonly wet?: number;
119
+ readonly source: LayerSource;
120
+ readonly filters?: FilterBank;
121
+ readonly modulators?: readonly Modulator[];
122
+ readonly envelope: Envelope;
123
+ };
124
+ /**
125
+ * Distance falloff of a sound's level:
126
+ * - `inverse_square`: `volume / (1 + d² × k)`; `cutoffK` also divides every lowpass cutoff by `1 + d × cutoffK` (the game's muffling);
127
+ * - `linear`: `max(0, 1 − d / range)`, silent at or below `minGain`;
128
+ * - `none`: no distance.
129
+ * `scale` multiplies the level (the game's boom is 1.4 at the source), and below `minGain` the sound is not played.
130
+ */
131
+ type Falloff = {
132
+ readonly model: 'inverse_square';
133
+ readonly k: number;
134
+ readonly minGain?: number;
135
+ readonly cutoffK?: number;
136
+ readonly scale?: number;
137
+ } | {
138
+ readonly model: 'linear';
139
+ readonly range: number;
140
+ readonly minGain?: number;
141
+ readonly scale?: number;
142
+ } | {
143
+ readonly model: 'none';
144
+ };
145
+ /**
146
+ * 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
147
+ * matches the trace of the team code node for node:
148
+ * - `layered` (default): the art scene's `sNoise`, `sTone` and `voice`; every layer has its own output stage (distance gain, pan, reverb send);
149
+ * - `burst`: the game's `sfx` (jogo L62089);
150
+ * - `scream`: the game's `grito` (L62128);
151
+ * - `groan`: the game's `voz` (L62143);
152
+ * - `boom`: the game's `boom` (L68278);
153
+ * - `static`: the game's `chiado` (L69998);
154
+ * - `chord`: the game's `buzina` (L70272);
155
+ * - `calls`: the game's `bichoSom` (L66625);
156
+ * - `siren`: the art scene's `siren` (L1369);
157
+ * - `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.
158
+ * Every order but `layered` puts the level into the envelope and connects straight to the bus.
159
+ */
160
+ declare const BUILD_ORDERS: readonly ["layered", "burst", "scream", "groan", "boom", "static", "chord", "calls", "siren", "weapon"];
161
+ type BuildOrder = (typeof BUILD_ORDERS)[number];
162
+ type SoundRecipe = {
163
+ readonly id: string;
164
+ readonly bus: BusId;
165
+ readonly order?: BuildOrder;
166
+ /** Named numbers drawn first, in the order written; layers read them with `{ param }`. */
167
+ readonly vars?: Readonly<Record<string, Range>>;
168
+ readonly duration?: Range;
169
+ readonly tail?: number;
170
+ readonly layers: readonly Layer[];
171
+ readonly falloff?: Falloff;
172
+ /** Stereo pan from the listener (the art scene's `panFor`). Only the `layered` order has an output stage to pan. */
173
+ readonly pan?: boolean;
174
+ /** Voice group for the limiter (`undead_voice`...). */
175
+ readonly group?: string;
176
+ /** Limiter priority; default from the bus (weapons > voices > world > ambience). */
177
+ readonly priority?: number;
178
+ /**
179
+ * 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
180
+ * a plan with `silent: true` so the realizer repeats those nodes. Off by default.
181
+ */
182
+ readonly keepSilent?: boolean;
183
+ /** Where the numbers come from: `"jogo:L62128"`, `"cena:L1342"`, or `"sdk"` for values the kit chose. */
184
+ readonly sources: readonly string[];
185
+ };
186
+ /**
187
+ * The order the loop realizer creates the nodes in:
188
+ * - `game_loop`: the game's `loopSet` (L62109): gain and filter first, nodes created the first time the volume passes `startAbove`, gain from 0;
189
+ * - `chain_loop`: the art scene's loops in `initAudio` (L1314–1316): source, filters, gain at its starting volume, created when the loop opens.
190
+ */
191
+ type LoopOrder = 'game_loop' | 'chain_loop';
192
+ type LoopRecipe = {
193
+ readonly id: string;
194
+ readonly bus: BusId;
195
+ readonly order?: LoopOrder;
196
+ /** An oscillator loop may leave the frequency out: it starts at the default and `set({ frequency })` glides it (the game's motor). */
197
+ readonly source: (NoiseSource & {
198
+ readonly loop: true;
199
+ }) | {
200
+ readonly kind: 'osc';
201
+ readonly wave: OscillatorType;
202
+ readonly frequency?: Automation;
203
+ readonly loop: true;
204
+ };
205
+ readonly filters?: FilterBank;
206
+ readonly modulators?: readonly Modulator[];
207
+ /** `setTargetAtTime` time constants (jogo L62125–62126: 0.15 for the volume, 0.08 for the frequency). */
208
+ readonly smoothing?: {
209
+ readonly volume: number;
210
+ readonly frequency: number;
211
+ };
212
+ /** Nodes are created the first time the volume reaches this (jogo L62112: 0.002). */
213
+ readonly startAbove?: number;
214
+ /** Volume the gain starts at when the loop opens (`chain_loop`). */
215
+ readonly volume?: number;
216
+ /**
217
+ * Distance falloff of the loop, applied each time `set` gives a `position` (an engine, a fire, a skid heard from where it is). Left out, the loop is
218
+ * heard at the listener, as the team loops are. `cutoffK` also lowers the first filter's cutoff with the distance, as it does for one-shots.
219
+ */
220
+ readonly falloff?: Falloff;
221
+ /** Stereo pan from the listener, from the `position` of each `set` (a panner after the gain). Off by default. */
222
+ readonly pan?: boolean;
223
+ readonly sources: readonly string[];
224
+ };
225
+ /**
226
+ * One id that plays one of several recipes (the takes of a footstep, of a punch): `play(id)` draws a variant, never the one it played last time (unless
227
+ * `repeat`), and plays it at a rate drawn from `rate` (1.04 is 4% higher and 4% shorter; see `retunePlan`), so ten footsteps never sound like one.
228
+ */
229
+ type SoundSet = {
230
+ readonly id: string;
231
+ /** Recipe ids, at least one. */
232
+ readonly variants: readonly string[];
233
+ /** `[min, max]` of the playback rate (pitch and speed together). Default `[1, 1]`. */
234
+ readonly rate?: readonly [min: number, max: number];
235
+ /** Allow the same variant twice in a row (default false). */
236
+ readonly repeat?: boolean;
237
+ };
238
+ type SoundBank = {
239
+ readonly id: string;
240
+ readonly sounds?: readonly SoundRecipe[];
241
+ readonly loops?: readonly LoopRecipe[];
242
+ readonly sets?: readonly SoundSet[];
243
+ /** Sounds played from recorded files (see `sample.ts`). A sample and a recipe may share an id: the sample plays once its files are loaded. */
244
+ readonly samples?: readonly SampleSound[];
245
+ /**
246
+ * Ids of this bank that play the takes of a sample sound registered by another bank (a manifest's file id), resolved when the id is looked up, so
247
+ * the order the banks register in does not matter. A sample of the alias's own id, when one is registered, wins.
248
+ */
249
+ readonly sampleAliases?: readonly SampleAlias[];
250
+ };
251
+ /**
252
+ * One id answered by the takes of another sample sound (`file`): `engine_car_low` by `engine_loop_car_idle`. The other numbers replace the file's;
253
+ * `offset` and `duration` trim every take (one recording holding the start, the run and the stop gives each its own id). The recipe or loop of `id`
254
+ * stays as the fallback while the files load or where they are missing.
255
+ */
256
+ type SampleAlias = {
257
+ readonly id: string;
258
+ readonly file: string;
259
+ /**
260
+ * Keep only the takes whose url contains this (`_01.`): a trim measured on one recording must not land on another take of the same file id,
261
+ * which may be longer and hold something else at that time.
262
+ */
263
+ readonly only?: string;
264
+ readonly offset?: number;
265
+ readonly duration?: number;
266
+ readonly loop?: SampleSound['loop'];
267
+ readonly baseFrequency?: number;
268
+ readonly gain?: number;
269
+ readonly pitch?: number;
270
+ readonly falloff?: Falloff;
271
+ };
272
+ /** One take: a file and its own trim (linear gain, default 1). */
273
+ type SampleRef = {
274
+ readonly url: string;
275
+ readonly gain?: number;
276
+ /** Seconds into the file the take starts at (default 0). A looping take starts there the first time round. */
277
+ readonly offset?: number;
278
+ /** Seconds of the file the take plays (default to its end); a trimmed one-shot fades out over its last 40 ms, so the cut does not click. */
279
+ readonly duration?: number;
280
+ };
281
+ /** Where a sound happens: a closed room rings short and dense, the open field long and thin. */
282
+ type SampleEnvironment = 'indoor' | 'outdoor';
283
+ /** Where the near take hands over to the far one: all near up to `from` metres, all far from `to`, an equal-power crossfade between. */
284
+ type SampleCrossover = {
285
+ readonly from: number;
286
+ readonly to: number;
287
+ };
288
+ type SampleTail = {
289
+ readonly indoor?: readonly SampleRef[];
290
+ readonly outdoor?: readonly SampleRef[];
291
+ /** Level of the tail against the dry take (default 0.6). */
292
+ readonly gain?: number;
293
+ /** Seconds after the start (default 0: the tail file carries its own pre-delay). */
294
+ readonly at?: number;
295
+ };
296
+ /**
297
+ * A set of takes for a strength of the action (criterion C7: a light blow, a heavy one, a car at 10 or 60 km/h): from `from` (0..1) up, these takes
298
+ * replace `close` and `far`. The tier played is the last one whose `from` is at or below `PlayOptions.intensity`.
299
+ */
300
+ type SampleTier = {
301
+ readonly name: string;
302
+ readonly from: number;
303
+ readonly close: readonly SampleRef[];
304
+ readonly far?: readonly SampleRef[];
305
+ };
306
+ type SampleSound = {
307
+ readonly id: string;
308
+ readonly bus: BusId;
309
+ /** The takes heard near; at least one (the criteria ask four for a sound heard all the time). */
310
+ readonly close: readonly SampleRef[];
311
+ /** The takes heard far; without them the near take plays at every distance (muffled by the lowpass). */
312
+ readonly far?: readonly SampleRef[];
313
+ /** Takes by strength (light, medium, heavy), in rising `from`; without `intensity` the near takes above play. */
314
+ readonly tiers?: readonly SampleTier[];
315
+ /** Default `{ from: 30, to: 80 }`. */
316
+ readonly crossover?: SampleCrossover;
317
+ readonly tail?: SampleTail;
318
+ /** ± fraction of the playback rate drawn per play (default 0.04: ±4 %, inside the 3–5 % of the criteria). 0 plays every take at its pitch. */
319
+ readonly pitch?: number;
320
+ /** Level of the sound (default 1). */
321
+ readonly gain?: number;
322
+ /** Default `{ model: 'inverse_square', k: 0.002, minGain: 0.002 }`: a recorded take already sounds near or far, so the curve is gentler than a recipe's. */
323
+ readonly falloff?: Falloff;
324
+ /** The lowpass cutoff is `20000 / (1 + d × cutoffK)` Hz (default 0.01: 10 kHz at 100 m). 0 turns the distance lowpass off. */
325
+ readonly cutoffK?: number;
326
+ /** Stereo pan from the listener (default true). */
327
+ readonly pan?: boolean;
328
+ readonly group?: string;
329
+ readonly priority?: number;
330
+ /**
331
+ * At most `voices` plays of this sound ring at once: a new one fades the oldest out in `fade` seconds (default 0.03) at its own start. Rapid fire
332
+ * then stays shot by shot instead of a wall of overlapping takes (criterion C9). Without it every play rings to its end.
333
+ */
334
+ readonly choke?: {
335
+ readonly voices: number;
336
+ readonly fade?: number;
337
+ };
338
+ /**
339
+ * A continuous sound (fire, rain, an engine idling) that `engine.loop(id)` plays: the near takes chained without a seam, each crossed into the next
340
+ * in `crossfade` seconds (default 0.25, equal power), never the same take twice in a row. While the files load, the recipe loop of `fallback`
341
+ * (or of the same id) plays. `play(id)` still plays one take as a one-shot.
342
+ */
343
+ readonly loop?: boolean | {
344
+ readonly crossfade?: number;
345
+ };
346
+ /**
347
+ * The frequency the recording of a loop sounds at (an engine's firing frequency at the RPM it was recorded at): `set({ frequency })` then plays
348
+ * the takes at `frequency / baseFrequency`, gliding, as an oscillator loop follows it. Without it the loop keeps the pitch of its files.
349
+ */
350
+ readonly baseFrequency?: number;
351
+ /** A recipe id the engine plays while the files are not loaded yet (or failed): the synthesized sound stays as the fallback. */
352
+ readonly fallback?: string;
353
+ /** Where each file came from: `"freesound:123456 autor CC0"`, or `"sdk"` for files the kit made. */
354
+ readonly sources: readonly string[];
355
+ /** The licence of the files when one is declared (a local pack's entry); the sound panel lists those that are not CC0 / CC-BY. */
356
+ readonly license?: string;
357
+ };
358
+
359
+ type DeepPartial<TValue> = TValue extends readonly unknown[] ? TValue : TValue extends object ? {
360
+ readonly [Key in keyof TValue]?: DeepPartial<TValue[Key]>;
361
+ } : TValue;
362
+ /** Every sound and loop the engine knows, by id. Banks register in order; a later bank replaces an earlier recipe with the same id. */
363
+ type SoundRegistry = {
364
+ register(bank: SoundBank): void;
365
+ /** Merged into the recipe by id (objects merge, arrays such as `layers` are replaced); an unknown id is ignored. */
366
+ patch(id: string, patch: DeepPartial<SoundRecipe> | DeepPartial<LoopRecipe> | DeepPartial<SampleSound>): void;
367
+ remove(id: string): void;
368
+ /** Drops only the sample sound of an id; a recipe or loop of the same id stays. */
369
+ removeSample(id: string): void;
370
+ sound(id: string): SoundRecipe | undefined;
371
+ loop(id: string): LoopRecipe | undefined;
372
+ /** The set an id names (`SoundBank.sets`), if any; a recipe with the same id wins in `play`. */
373
+ set(id: string): SoundSet | undefined;
374
+ /** The sample sound of an id: its own, or the takes of the sound a bank's `sampleAliases` points it at (once that one is registered). */
375
+ sample(id: string): SampleSound | undefined;
376
+ soundIds(): string[];
377
+ setIds(): string[];
378
+ loopIds(): string[];
379
+ /** Every id `sample` answers, aliases whose file is registered included. */
380
+ sampleIds(): string[];
381
+ /** Which bank each id came from. */
382
+ bankOf(id: string): string | undefined;
383
+ };
384
+ /**
385
+ * The sample an alias names: the file's takes (those `only` keeps) and numbers, with the alias's id and its own numbers; the recipe of the alias id is
386
+ * the fallback. `undefined` when `only` keeps no take.
387
+ */
388
+ declare function aliasSample(alias: SampleAlias, file: SampleSound): SampleSound | undefined;
389
+ declare function createSoundRegistry(banks?: readonly SoundBank[]): SoundRegistry;
390
+
391
+ export { type Automation as A, type BusId as B, type Curve as C, type DeepPartial as D, type Envelope as E, type Falloff as F, type LoopRecipe as L, type Modulator as M, type NoiseSource as N, type OscSource as O, type ParamRef as P, type Range as R, type SoundRegistry as S, type Time as T, type SoundBank as a, type SoundRecipe as b, type SampleSound as c, type Layer as d, type SoundSet as e, BUILD_ORDERS as f, BUS_IDS as g, type BuildOrder as h, type EnvelopePoint as i, type FilterBank as j, type FilterSpec as k, type FormantVoiceSource as l, type LayerSource as m, type LoopOrder as n, type SampleAlias as o, type SampleCrossover as p, type SampleEnvironment as q, type SampleRef as r, type SampleTail as s, type SampleTier as t, aliasSample as u, createSoundRegistry as v };