@adatechnology/game-audio 0.3.5 → 0.4.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/dist/index.js CHANGED
@@ -1,527 +1,54 @@
1
- // src/rng.ts
2
- var FNV_OFFSET = 2166136261;
3
- var FNV_PRIME = 16777619;
4
- var MIX = 2654435761;
5
- function createRng(seed) {
6
- let state = seed >>> 0;
7
- return () => {
8
- state = state + 1831565813 >>> 0;
9
- let mixed = Math.imul(state ^ state >>> 15, state | 1);
10
- mixed ^= mixed + Math.imul(mixed ^ mixed >>> 7, mixed | 61);
11
- return ((mixed ^ mixed >>> 14) >>> 0) / 4294967296;
12
- };
13
- }
14
- function streamSeed(seed, name) {
15
- const label = String(name);
16
- let hash = FNV_OFFSET;
17
- for (let index = 0; index < label.length; index++) hash = Math.imul(hash ^ label.charCodeAt(index), FNV_PRIME) >>> 0;
18
- return Math.imul(seed >>> 0 ^ hash, MIX) >>> 0;
19
- }
20
- function streamFor(seed, name) {
21
- return createRng(streamSeed(seed, name));
22
- }
23
- function drawBetween(random, min, max) {
24
- return min + random() * (max - min);
25
- }
26
-
27
- // src/recipe.ts
28
- var BUS_IDS = ["weapons", "voices", "world", "ambience", "weather", "ui"];
29
- var BUILD_ORDERS = ["layered", "burst", "scream", "groan", "boom", "static", "chord", "calls", "siren", "weapon"];
30
-
31
- // src/falloff.ts
32
- var DISTANCE_CURVES = {
33
- /** The game's `sfx`: `vol / (1 + d² × 0.004)`, cutoff `/ (1 + d × 0.01)`, silent below 0.01 (jogo L62091–62092, L62101). */
34
- game_sfx: { model: "inverse_square", k: 4e-3, cutoffK: 0.01, minGain: 0.01 },
35
- /** The game's `grito`: `1 / (1 + d² × 0.006)`, silent below 0.02 (jogo L62129–62130). */
36
- game_scream: { model: "inverse_square", k: 6e-3, minGain: 0.02 },
37
- /** The game's `voz`: `1 / (1 + d² × 0.01)`, silent below 0.02 (jogo L62146–62147). */
38
- game_voice: { model: "inverse_square", k: 0.01, minGain: 0.02 },
39
- /** The game's `boom`: `1.4 / (1 + d² × 0.002)`, never silent (jogo L68280). */
40
- game_boom: { model: "inverse_square", k: 2e-3, scale: 1.4 },
41
- /** The art scene's `outAt`: `vol / (1 + d² × 0.008)` (cena L1320). */
42
- art_scene: { model: "inverse_square", k: 8e-3 },
43
- /** The game's animal calls: `max(0, 1 − d / 70) × 0.25`, silent at or below 0.005 (jogo L66627–66628). */
44
- animals: { model: "linear", range: 70, scale: 0.25, minGain: 5e-3 }
45
- };
46
- function falloffGain(falloff, volume, distance) {
47
- if (!falloff || falloff.model === "none") return volume;
48
- if (falloff.model === "linear") {
49
- const level2 = Math.max(0, 1 - distance / falloff.range) * volume;
50
- return falloff.scale === void 0 ? level2 : level2 * falloff.scale;
51
- }
52
- const level = volume / (1 + distance * distance * falloff.k);
53
- return falloff.scale === void 0 ? level : falloff.scale * level;
54
- }
55
- function isSilent(falloff, gain) {
56
- if (!falloff || falloff.model === "none" || falloff.minGain === void 0) return false;
57
- return falloff.model === "linear" ? gain <= falloff.minGain : gain < falloff.minGain;
58
- }
59
- function cutoffDivisor(falloff, distance) {
60
- if (!falloff || falloff.model !== "inverse_square" || falloff.cutoffK === void 0) return 1;
61
- return 1 + distance * falloff.cutoffK;
62
- }
63
-
64
- // src/listener.ts
65
- var DEFAULT_LISTENER = { position: [0, 0, 0], yaw: 0 };
66
- var PAN_LIMIT = 0.85;
67
- var PAN_MIN_DISTANCE = 2;
68
- function listenerDistance(listener, position, mode = "xz") {
69
- const dx = position[0] - listener.position[0];
70
- const dz = position[2] - listener.position[2];
71
- if (mode === "xz") return Math.hypot(dx, dz);
72
- return Math.hypot(dx, position[1] - listener.position[1], dz);
73
- }
74
- function panFor(listener, position) {
75
- const rightX = Math.cos(listener.yaw);
76
- const rightZ = -Math.sin(listener.yaw);
77
- const dx = position[0] - listener.position[0];
78
- const dz = position[2] - listener.position[2];
79
- const distance = Math.hypot(dx, dz) || 1;
80
- const pan = (dx * rightX + dz * rightZ) / Math.max(distance, PAN_MIN_DISTANCE);
81
- return pan < -PAN_LIMIT ? -PAN_LIMIT : pan > PAN_LIMIT ? PAN_LIMIT : pan;
82
- }
83
-
84
- // src/data/voice-groups.json
85
- var voice_groups_default = {
86
- undead_voice: {
87
- maxConcurrent: 5,
88
- cooldown: 0.25,
89
- maxDistance: 40,
90
- sources: [
91
- "cena:L1342 `AU.groans > 4` refuses, so five play at once",
92
- "jogo:L66986 one groan every 0.25 s (`groanBudget`), only nearer than 40 m",
93
- "jogo:L70472 the budget counts down by the frame time"
94
- ]
95
- }
96
- };
97
-
98
- // src/voices.ts
99
- var VOICE_GROUPS = voice_groups_default;
100
- var BUS_PRIORITY = { ui: 5, weapons: 4, voices: 3, world: 2, weather: 1, ambience: 1 };
101
- var DEFAULT_MAX_VOICES = 32;
102
- function busPriority(bus) {
103
- return BUS_PRIORITY[bus];
104
- }
105
- function createVoiceManager(options = {}) {
106
- const groups = new Map(Object.entries(options.groups ?? VOICE_GROUPS));
107
- const maxVoices = options.maxVoices ?? DEFAULT_MAX_VOICES;
108
- const budgets = /* @__PURE__ */ new Map();
109
- let voices = [];
110
- let nextId = 1;
111
- let peak = 0;
112
- let dropped = 0;
113
- const refuse = (reason) => {
114
- dropped += 1;
115
- return { accepted: false, reason };
116
- };
117
- const groupCheck = (request, evicted) => {
118
- const group = request.group === void 0 ? void 0 : groups.get(request.group);
119
- if (!group) return void 0;
120
- if (group.maxDistance !== void 0 && !((request.distance ?? 0) < group.maxDistance)) return "distance";
121
- if (group.cooldown !== void 0 && (budgets.get(request.group) ?? 0) > 0) return "cooldown";
122
- const members = voices.filter((voice) => voice.group === request.group);
123
- if (group.maxConcurrent === void 0 || members.length < group.maxConcurrent) return void 0;
124
- if (!group.steal) return "group_full";
125
- evicted.push(members[0].id);
126
- return void 0;
127
- };
128
- return {
129
- request(request) {
130
- const evicted = [];
131
- const refusal = groupCheck(request, evicted);
132
- if (refusal) return refuse(refusal);
133
- const score = request.priority * request.gain;
134
- const remaining = voices.filter((voice) => !evicted.includes(voice.id));
135
- if (remaining.length >= maxVoices) {
136
- const lowest = remaining.reduce((low, voice) => voice.score < low.score ? voice : low);
137
- if (score <= lowest.score) return refuse("global_full");
138
- evicted.push(lowest.id);
139
- dropped += 1;
140
- }
141
- const group = request.group === void 0 ? void 0 : groups.get(request.group);
142
- if (group?.cooldown !== void 0) budgets.set(request.group, group.cooldown);
143
- const id = nextId++;
144
- voices = [...voices.filter((voice) => !evicted.includes(voice.id)), { id, ...request.group === void 0 ? {} : { group: request.group }, score, remaining: request.length }];
145
- peak = Math.max(peak, voices.length);
146
- return { accepted: true, id, evicted };
147
- },
148
- release(id) {
149
- voices = voices.filter((voice) => voice.id !== id);
150
- },
151
- setLength(id, seconds) {
152
- const voice = voices.find((entry) => entry.id === id);
153
- if (voice) voice.remaining = seconds;
154
- },
155
- advance(dt) {
156
- for (const [group, budget] of budgets) budgets.set(group, budget - dt);
157
- const ended = [];
158
- for (const voice of voices) {
159
- voice.remaining -= dt;
160
- if (voice.remaining <= 0) ended.push(voice.id);
161
- }
162
- if (ended.length > 0) voices = voices.filter((voice) => voice.remaining > 0);
163
- return ended;
164
- },
165
- setGroup(id, group) {
166
- if (group) groups.set(id, group);
167
- else groups.delete(id);
168
- },
169
- stats: () => ({ active: voices.length, peak, dropped, maxVoices })
170
- };
171
- }
172
-
173
- // src/plan.ts
174
- var DEFAULT_TAIL = 0.05;
175
- var DEFAULT_NOISE_SECONDS = 1.5;
176
- var OCCLUSION_GAIN = 0.5;
177
- var OCCLUSION_CUTOFF = 0.6;
178
- function isParamRef(range) {
179
- return typeof range === "object" && !Array.isArray(range);
180
- }
181
- function draw(range, scope) {
182
- if (typeof range === "number") return range;
183
- if (isParamRef(range)) {
184
- const value = scope.params[range.param];
185
- if (value === void 0) throw new Error(`game-audio: the recipe reads the param "${range.param}", which was not given`);
186
- return value * (range.scale ?? 1) + (range.add ?? 0);
187
- }
188
- const [min, max] = range;
189
- return min + scope.random() * (max - min);
190
- }
191
- function drawTime(time, scope) {
192
- if (time === "duration" || typeof time === "string" && time.endsWith("d")) return time;
193
- return draw(time, scope);
194
- }
195
- function timeOf(time, duration) {
196
- if (typeof time === "number") return time;
197
- if (time === "duration") return duration;
198
- return Number(time.slice(0, -1)) * duration;
199
- }
200
- function drawAutomation(automation, scope) {
201
- const pending = {};
202
- for (const key of Object.keys(automation)) {
203
- if (key === "set" && "set" in automation) pending.value = draw(automation.set, scope);
204
- else if (key === "from" && "from" in automation) pending.from = draw(automation.from, scope);
205
- else if (key === "to" && automation.to !== void 0) pending.to = automation.to === "from" ? pending.from ?? pending.value ?? 0 : draw(automation.to, scope);
206
- else if (key === "curve") pending.curve = automation.curve;
207
- else if (key === "over" && automation.over !== void 0) pending.over = drawTime(automation.over, scope);
208
- }
209
- return pending;
210
- }
211
- function finishAutomation(pending, duration) {
212
- if (pending.from !== void 0 && pending.to === void 0) return { from: pending.from };
213
- if (pending.from !== void 0) return { from: pending.from, to: pending.to, curve: pending.curve ?? "linear", at: timeOf(pending.over ?? "duration", duration) };
214
- if (pending.to === void 0 || pending.over === void 0) return { value: pending.value };
215
- return { value: pending.value, to: pending.to, curve: pending.curve ?? "linear", at: timeOf(pending.over, duration) };
216
- }
217
- var startFrequency = (automation) => automation.from ?? automation.value ?? 0;
218
- function drawFilter(spec, scope) {
219
- const pending = { spec, frequency: {} };
220
- for (const key of Object.keys(spec)) {
221
- if (key === "frequency") pending.frequency = drawAutomation(spec.frequency, scope);
222
- else if (key === "q" && spec.q !== void 0) pending.q = draw(spec.q, scope);
223
- else if (key === "gain" && spec.gain !== void 0) pending.gain = draw(spec.gain, scope);
224
- else if (key === "level" && spec.level !== void 0) pending.level = draw(spec.level, scope);
225
- }
226
- return pending;
227
- }
228
- function drawModulator(modulator, scope) {
229
- const pending = { modulator, rate: 0 };
230
- for (const key of Object.keys(modulator)) {
231
- if (key === "rate") pending.rate = draw(modulator.rate, scope);
232
- else if (key === "depth" && !(typeof modulator.depth === "object" && "scaleOf" in modulator.depth)) pending.depth = draw(modulator.depth, scope);
233
- }
234
- return pending;
235
- }
236
- function drawVoice(source, scope) {
237
- const pending = { frequency: {}, vibratoRate: 0, formants: [], breath: { frequency: 0, q: source.breath.q, gain: 0 } };
238
- for (const key of Object.keys(source)) {
239
- if (key === "frequency") pending.frequency = drawAutomation(source.frequency, scope);
240
- else if (key === "vibrato") pending.vibratoRate = draw(source.vibrato.rate, scope);
241
- else if (key === "formants") pending.formants = source.formants.map((formant) => ({ frequency: draw(formant.frequency, scope), q: formant.q }));
242
- else if (key === "breath") pending.breath = { frequency: draw(source.breath.frequency, scope), q: source.breath.q, gain: draw(source.breath.gain, scope) };
243
- }
244
- return pending;
245
- }
246
- function drawLayer(layer, scope) {
247
- const pending = { at: 0, gain: 1, filters: [], modulators: [] };
248
- for (const key of Object.keys(layer)) {
249
- if (key === "at" && layer.at !== void 0) pending.at = draw(layer.at, scope);
250
- else if (key === "duration" && layer.duration !== void 0) pending.duration = draw(layer.duration, scope);
251
- else if (key === "gain" && layer.gain !== void 0) pending.gain = draw(layer.gain, scope);
252
- else if (key === "source" && layer.source.kind === "osc") pending.automation = drawAutomation(layer.source.frequency, scope);
253
- else if (key === "source" && layer.source.kind === "formant_voice") pending.voice = drawVoice(layer.source, scope);
254
- else if (key === "filters" && layer.filters) pending.filters = layer.filters.bank.map((spec) => drawFilter(spec, scope));
255
- else if (key === "modulators" && layer.modulators) pending.modulators = layer.modulators.map((modulator) => drawModulator(modulator, scope));
256
- }
257
- return pending;
258
- }
259
- function drawOffset(source, duration, scope, noiseSeconds) {
260
- if (source.kind === "formant_voice") return scope.random();
261
- if (source.kind !== "noise" || source.offset === void 0) return 0;
262
- if (typeof source.offset === "number") return source.offset;
263
- if (source.offset === "fit") return scope.random() * Math.max(0, noiseSeconds - duration);
264
- return scope.random() * (source.offsetScale ?? 1);
265
- }
266
- function finishSource(layer, pending, duration, offset) {
267
- const source = layer.source;
268
- if (source.kind === "noise") return { kind: "noise", offset, loop: source.loop === true };
269
- if (source.kind === "osc") return { kind: "osc", wave: source.wave, frequency: finishAutomation(pending.automation ?? {}, duration) };
270
- const voice = pending.voice;
271
- return {
272
- kind: "formant_voice",
273
- wave: source.wave,
274
- frequency: finishAutomation(voice.frequency, duration),
275
- vibrato: { rate: voice.vibratoRate, depth: startFrequency(voice.frequency) * source.vibrato.depth },
276
- formants: voice.formants,
277
- breath: { ...voice.breath, offset },
278
- mix: source.mix
279
- };
280
- }
281
- function finishModulator(pending, sourceFrequency) {
282
- const { modulator } = pending;
283
- const depth = typeof modulator.depth === "object" && "scaleOf" in modulator.depth ? sourceFrequency * modulator.depth.factor : pending.depth ?? 0;
284
- return { target: modulator.target, ...modulator.wave ? { wave: modulator.wave } : {}, rate: pending.rate, depth, ...modulator.base === void 0 ? {} : { base: modulator.base } };
285
- }
286
- function finishEnvelope(envelope2, duration, level) {
287
- return envelope2.map(([time, value, curve, fixed2]) => ({ time: timeOf(time, duration), value: fixed2 === "fixed" ? value : value * level, curve }));
288
- }
289
- function planLayer(layer, scope, defaults, levelInEnvelope, soundLevel) {
290
- const pending = drawLayer(layer, scope);
291
- const duration = pending.duration ?? defaults.duration;
292
- if (duration === void 0) throw new Error("game-audio: a layer needs a duration (its own or the recipe\u2019s)");
293
- const offset = drawOffset(layer.source, duration, scope, defaults.noiseSeconds);
294
- const level = pending.gain * soundLevel;
295
- const sourceFrequency = layer.source.kind === "noise" ? 0 : startFrequency(pending.automation ?? pending.voice?.frequency ?? {});
296
- const planned = {
297
- at: pending.at,
298
- duration,
299
- tail: layer.tail ?? defaults.tail ?? DEFAULT_TAIL,
300
- level,
301
- ...layer.wet === void 0 ? {} : { wet: layer.wet },
302
- source: finishSource(layer, pending, duration, offset),
303
- ...layer.filters ? {
304
- filters: {
305
- mode: layer.filters.mode,
306
- bank: pending.filters.map(({ spec, frequency, q, gain, level: filterLevel }) => ({
307
- type: spec.type,
308
- frequency: finishAutomation(frequency, duration),
309
- ...q === void 0 ? {} : { q },
310
- ...gain === void 0 ? {} : { gain },
311
- ...filterLevel === void 0 ? {} : { level: filterLevel }
312
- }))
313
- }
314
- } : {},
315
- modulators: pending.modulators.map((modulator) => finishModulator(modulator, sourceFrequency)),
316
- envelope: finishEnvelope(layer.envelope, duration, levelInEnvelope ? level : 1)
317
- };
318
- if (!layer.repeat) return [planned];
319
- return Array.from({ length: layer.repeat.count }, (_, index) => ({ ...planned, at: planned.at + index * (layer.repeat?.spacing ?? 0) }));
320
- }
321
- function scaleAutomation(automation, divisor) {
322
- const scale = (value) => value === void 0 ? void 0 : value / divisor;
323
- const scaled = { ...automation };
324
- for (const key of ["value", "from", "to"]) if (automation[key] !== void 0) scaled[key] = scale(automation[key]);
325
- return scaled;
326
- }
327
- function muffle(layer, divisor) {
328
- if (divisor === 1 || !layer.filters) return layer;
329
- const bank = layer.filters.bank.map((filter) => filter.type === "lowpass" ? { ...filter, frequency: scaleAutomation(filter.frequency, divisor) } : filter);
330
- return { ...layer, filters: { ...layer.filters, bank } };
331
- }
332
- var layerEnd = (layer) => layer.at + layer.duration + layer.tail;
333
- function planSound(recipe, input = {}) {
334
- const listener = input.listener ?? DEFAULT_LISTENER;
335
- const distance = input.distance ?? (input.position ? listenerDistance(listener, input.position, input.distanceMode) : 0);
336
- const occlusion = Math.min(1, Math.max(0, input.occlusion ?? 0));
337
- const order = recipe.order ?? "layered";
338
- const falloffLevel = falloffGain(recipe.falloff, input.volume ?? 1, distance);
339
- const level = occlusion > 0 ? falloffLevel * (1 - OCCLUSION_GAIN * occlusion) : falloffLevel;
340
- const silent = isSilent(recipe.falloff, falloffLevel);
341
- const base = {
342
- id: recipe.id,
343
- bus: recipe.bus,
344
- order,
345
- ...recipe.group === void 0 ? {} : { group: recipe.group },
346
- priority: recipe.priority ?? busPriority(recipe.bus),
347
- level,
348
- distance,
349
- ...recipe.pan ? { pan: input.position ? panFor(listener, input.position) : 0 } : {}
350
- };
351
- if (silent) return recipe.keepSilent ? { ...base, silent: true, length: 0, layers: [] } : null;
352
- const scope = { random: input.random ?? createRng(1), params: { ...input.params } };
353
- for (const [name, range] of Object.entries(recipe.vars ?? {})) if (scope.params[name] === void 0) scope.params[name] = draw(range, scope);
354
- const defaults = {
355
- ...recipe.duration === void 0 ? {} : { duration: draw(recipe.duration, scope) },
356
- ...recipe.tail === void 0 ? {} : { tail: recipe.tail },
357
- noiseSeconds: input.noiseSeconds ?? DEFAULT_NOISE_SECONDS
358
- };
359
- const divisor = cutoffDivisor(recipe.falloff, distance) / (1 - OCCLUSION_CUTOFF * occlusion);
360
- const layers = recipe.layers.flatMap((layer) => planLayer(layer, scope, defaults, order !== "layered", level)).map((layer) => muffle(layer, divisor));
361
- return { ...base, silent: false, length: Math.max(0, ...layers.map(layerEnd)), layers };
362
- }
363
-
364
- // src/loops.ts
365
- var LOOP_START_ABOVE = 2e-3;
366
- var LOOP_SMOOTHING = { volume: 0.15, frequency: 0.08 };
367
- function createLoopState(recipe, open = false) {
368
- const startAbove = recipe.startAbove ?? LOOP_START_ABOVE;
369
- let created = open;
370
- let volume = recipe.volume ?? 0;
371
- return {
372
- set(state) {
373
- volume = state.volume ?? volume;
374
- const create = !created;
375
- if (create && !(volume >= startAbove)) return null;
376
- created = true;
377
- const frequency = recipe.source.kind === "osc" && state.frequency ? state.frequency : void 0;
378
- return { create, volume, ...frequency === void 0 ? {} : { frequency } };
379
- },
380
- get created() {
381
- return created;
382
- },
383
- reset() {
384
- created = false;
385
- }
386
- };
387
- }
388
-
389
- // src/data/mix-profiles.json
390
- var mix_profiles_default = [
391
- {
392
- id: "game",
393
- master: 0.5,
394
- noise: { seconds: 1.5, create: "lazy" },
395
- sources: ["jogo:L62077-62081 master 0.5 to the output", "jogo:L62082-62088 noise 1.5 s made by the first sound that needs it"]
396
- },
397
- {
398
- id: "art_scene",
399
- master: 0.85,
400
- compressor: { threshold: -12, ratio: 5 },
401
- reverb: { seconds: 2.2, decay: 3.4, wet: 0.4 },
402
- noise: { seconds: 2, create: "eager" },
403
- sources: [
404
- "cena:L1308 compressor -12 dB, ratio 5",
405
- "cena:L1309 master 0.85",
406
- "cena:L1310 convolver, impulse 2.2 s of (1 - i/len)^3.4",
407
- "cena:L1311 wet 0.4",
408
- "cena:L1312 noise 2 s made with the chain"
409
- ]
410
- }
411
- ];
412
-
413
- // src/mix.ts
414
- var MIX_PROFILES = Object.fromEntries(
415
- mix_profiles_default.map((profile) => [profile.id, profile])
416
- );
417
- var DEFAULT_MIX = "art_scene";
418
- function resolveMix(mix, profiles = MIX_PROFILES) {
419
- if (typeof mix !== "string") return mix;
420
- const profile = profiles[mix];
421
- if (!profile) throw new Error(`game-audio: unknown mix profile "${mix}" (known: ${Object.keys(profiles).join(", ")})`);
422
- return profile;
423
- }
424
- function busGain(setting) {
425
- if (!setting) return 1;
426
- return setting.muted ? 0 : setting.gain ?? 1;
427
- }
428
-
429
- // src/bank.ts
430
- function isPlainObject(value) {
431
- return typeof value === "object" && value !== null && !Array.isArray(value);
432
- }
433
- function mergeRecords(base, patch) {
434
- const merged = { ...base };
435
- for (const [key, value] of Object.entries(patch)) {
436
- if (value === void 0) continue;
437
- const current = merged[key];
438
- merged[key] = isPlainObject(current) && isPlainObject(value) ? mergeRecords(current, value) : value;
439
- }
440
- return merged;
441
- }
442
- function createSoundRegistry(banks = []) {
443
- const sounds = /* @__PURE__ */ new Map();
444
- const loops = /* @__PURE__ */ new Map();
445
- const owners = /* @__PURE__ */ new Map();
446
- const registry = {
447
- register(bank) {
448
- for (const sound of bank.sounds ?? []) {
449
- sounds.set(sound.id, sound);
450
- owners.set(sound.id, bank.id);
451
- }
452
- for (const loop of bank.loops ?? []) {
453
- loops.set(loop.id, loop);
454
- owners.set(loop.id, bank.id);
455
- }
456
- },
457
- patch(id, patch) {
458
- const sound = sounds.get(id);
459
- if (sound) sounds.set(id, mergeRecords(sound, patch));
460
- const loop = loops.get(id);
461
- if (loop) loops.set(id, mergeRecords(loop, patch));
462
- },
463
- remove(id) {
464
- sounds.delete(id);
465
- loops.delete(id);
466
- owners.delete(id);
467
- },
468
- sound: (id) => sounds.get(id),
469
- loop: (id) => loops.get(id),
470
- soundIds: () => [...sounds.keys()],
471
- loopIds: () => [...loops.keys()],
472
- bankOf: (id) => owners.get(id)
473
- };
474
- for (const bank of banks) registry.register(bank);
475
- return registry;
476
- }
477
-
478
- // src/data/game-sfx.json
479
- var game_sfx_default = {
480
- sources: ["jogo:L62089-62107"],
481
- note: "The game's sfx table P: [lowpass cutoff Hz, peak gain, seconds]. Played as a noise burst with the game's distance curve (falloff game_sfx) and a 0.05 s tail. Team names in `team`.",
482
- classes: [
483
- { id: "pistol", team: "pistola", bus: "weapons", cutoff: 1800, gain: 0.9, seconds: 0.18 },
484
- { id: "shotgun", team: "escopeta", bus: "weapons", cutoff: 900, gain: 1.2, seconds: 0.35 },
485
- { id: "rifle", team: "fuzil", bus: "weapons", cutoff: 1400, gain: 1.1, seconds: 0.28 },
486
- { id: "long_rifle", team: "rifle", bus: "weapons", cutoff: 1100, gain: 1.3, seconds: 0.45 },
487
- { id: "impact", team: "impacto", bus: "world", cutoff: 2500, gain: 0.25, seconds: 0.05 },
488
- { id: "flesh", team: "carne", bus: "world", cutoff: 600, gain: 0.35, seconds: 0.08 },
489
- { id: "step", team: "passo", bus: "world", cutoff: 400, gain: 0.08, seconds: 0.05 },
490
- { id: "glass", team: "vidro", bus: "world", cutoff: 5e3, gain: 0.5, seconds: 0.3 },
491
- { id: "wood", team: "madeira", bus: "world", cutoff: 700, gain: 0.5, seconds: 0.2 },
492
- { id: "reload", team: "recarga", bus: "weapons", cutoff: 3e3, gain: 0.15, seconds: 0.06 },
493
- { id: "empty", team: "vazio", bus: "weapons", cutoff: 4e3, gain: 0.12, seconds: 0.03 },
494
- { id: "explosion", team: "explosao", bus: "world", cutoff: 200, gain: 1.6, seconds: 1.4 }
495
- ],
496
- fallback: { id: "unknown", team: "(any other name)", bus: "world", cutoff: 1e3, gain: 0.5, seconds: 0.1 }
497
- };
498
-
499
- // src/recipes/noise-class.ts
500
- var GAME_SFX = game_sfx_default;
501
- var GAME_SFX_CLASSES = GAME_SFX.classes;
502
- var GAME_SFX_FALLBACK = GAME_SFX.fallback;
503
- function noiseClassRecipe(row, id = `sfx_${row.id}`) {
504
- return {
505
- id,
506
- bus: row.bus,
507
- order: "burst",
508
- duration: row.seconds,
509
- tail: 0.05,
510
- layers: [
511
- {
512
- source: { kind: "noise", offset: "random" },
513
- filters: { mode: "serial", bank: [{ type: "lowpass", frequency: { set: row.cutoff } }] },
514
- envelope: [
515
- [0, row.gain, "set"],
516
- ["1d", 1e-3, "exp", "fixed"]
517
- ]
518
- }
519
- ],
520
- falloff: DISTANCE_CURVES.game_sfx,
521
- sources: [...GAME_SFX.sources.map((source) => `${source} ${row.team}`)]
522
- };
523
- }
524
- var GAME_SFX_BANK = { id: "game_sfx", sounds: [...GAME_SFX_CLASSES, GAME_SFX_FALLBACK].map((row) => noiseClassRecipe(row)) };
1
+ import {
2
+ BENCH_BANK,
3
+ BUILD_ORDERS,
4
+ BUS_IDS,
5
+ BUS_PRIORITY,
6
+ DEFAULT_LISTENER,
7
+ DEFAULT_MAX_VOICES,
8
+ DEFAULT_MIX,
9
+ DISTANCE_CURVES,
10
+ GAME_SFX_BANK,
11
+ GAME_SFX_CLASSES,
12
+ GAME_SFX_FALLBACK,
13
+ LIBRARY_BANK,
14
+ LIBRARY_LOOPS,
15
+ LIBRARY_SOUNDS,
16
+ LOOP_SMOOTHING,
17
+ LOOP_START_ABOVE,
18
+ MIX_PROFILES,
19
+ PAN_LIMIT,
20
+ PAN_MIN_DISTANCE,
21
+ VOICE_GROUPS,
22
+ audioBenchTextOf,
23
+ bufferOf,
24
+ buildChain,
25
+ busGain,
26
+ busPriority,
27
+ createAudioEngine,
28
+ createAudioTestModule,
29
+ createLoopState,
30
+ createRng,
31
+ createSetPicker,
32
+ createSoundRegistry,
33
+ createVoiceManager,
34
+ cutoffDivisor,
35
+ drawBetween,
36
+ falloffGain,
37
+ fillImpulse,
38
+ fillNoise,
39
+ globalContext,
40
+ isSilent,
41
+ listenerDistance,
42
+ noiseClassRecipe,
43
+ panFor,
44
+ planSound,
45
+ realize,
46
+ resolveMix,
47
+ retunePlan,
48
+ streamFor,
49
+ streamSeed,
50
+ unlockContext
51
+ } from "./chunk-DZITVFMJ.js";
525
52
 
526
53
  // src/recipes/scene.ts
527
54
  var ATTACK = 2e-3;
@@ -600,1085 +127,184 @@ function sceneRecipe(id, layers, line, options = {}) {
600
127
  };
601
128
  }
602
129
 
603
- // src/library/crowd_rumble.json
604
- var crowd_rumble_default = {
605
- id: "crowd_rumble",
606
- bus: "ambience",
607
- order: "game_loop",
608
- source: { kind: "noise", loop: true },
609
- filters: { mode: "serial", bank: [{ type: "lowpass", frequency: { set: 160 } }] },
610
- smoothing: { volume: 0.15, frequency: 0.08 },
611
- startAbove: 2e-3,
612
- sources: ["jogo:L62116 horda: noise through a 160 Hz lowpass", "jogo:L70424 level min(0.16, near \xD7 0.0016)", "temporary home until the crowd package (Q12)"]
130
+ // src/recipes/body-foley.ts
131
+ import {
132
+ BODY_SOUND_IDS,
133
+ FOOTSTEP_SOUND_IDS,
134
+ GAIT_IDS,
135
+ LAND_SOUND_IDS,
136
+ LANDING_IDS,
137
+ SURFACE_IDS,
138
+ SWING_LEAD_SECONDS,
139
+ footstepSoundId
140
+ } from "@adatechnology/game-contracts";
141
+ var BODY_FOLEY_TAKES = 4;
142
+ var TAKES = [
143
+ { f: 1, t: 1, v: 1 },
144
+ { f: 1.08, t: 0.9, v: 0.92 },
145
+ { f: 0.93, t: 1.12, v: 1.05 },
146
+ { f: 1.03, t: 1.05, v: 0.88 }
147
+ ];
148
+ var FOOTSTEP_FALLOFF = { model: "inverse_square", k: 0.03, cutoffK: 0.03, minGain: 4e-3 };
149
+ var BODY_FALLOFF = { model: "inverse_square", k: 0.012, cutoffK: 0.015, minGain: 4e-3 };
150
+ var SURFACES = {
151
+ concrete: { heel: { f: 95, d: 0.05, vol: 0.55 }, roll: { type: "bandpass", f: 2300, q: 1.1, d: 0.05, vol: 0.38 }, grains: { count: 2, f: 4200, q: 2, spread: 0.04, vol: 0.08 } },
152
+ asphalt: { heel: { f: 90, d: 0.05, vol: 0.5 }, roll: { type: "bandpass", f: 1700, q: 0.9, d: 0.06, vol: 0.36 }, grains: { count: 4, f: 3600, q: 2.5, spread: 0.06, vol: 0.1 } },
153
+ wood: { heel: { f: 130, d: 0.08, vol: 0.55 }, roll: { type: "lowpass", f: 1300, q: 0.8, d: 0.06, vol: 0.3 }, ring: [{ f: 210, d: 0.1, vol: 0.18 }, { f: 470, d: 0.06, vol: 0.06 }] },
154
+ tile: { heel: { f: 110, d: 0.04, vol: 0.45 }, roll: { type: "bandpass", f: 3600, q: 2, d: 0.03, vol: 0.42 }, ring: [{ f: 1900, d: 0.03, vol: 0.05 }] },
155
+ dirt: { heel: { f: 75, d: 0.06, vol: 0.45 }, roll: { type: "lowpass", f: 750, q: 0.7, d: 0.09, vol: 0.4 }, grains: { count: 3, f: 1800, q: 1.5, spread: 0.07, vol: 0.06 } },
156
+ grass: { heel: { f: 70, d: 0.05, vol: 0.32 }, roll: { type: "highpass", f: 2600, q: 0.7, d: 0.13, vol: 0.28 }, grains: { count: 4, f: 5200, q: 1.2, spread: 0.1, vol: 0.07 } },
157
+ gravel: { heel: { f: 80, d: 0.05, vol: 0.4 }, roll: { type: "lowpass", f: 950, q: 0.8, d: 0.1, vol: 0.3 }, grains: { count: 8, f: 2600, q: 3, spread: 0.13, vol: 0.2 } },
158
+ sand: { heel: { f: 65, d: 0.06, vol: 0.3 }, roll: { type: "bandpass", f: 1500, q: 0.6, d: 0.16, vol: 0.3 }, grains: { count: 5, f: 4800, q: 1.5, spread: 0.12, vol: 0.05 } },
159
+ metal: { heel: { f: 120, d: 0.05, vol: 0.5 }, roll: { type: "bandpass", f: 2800, q: 1.5, d: 0.03, vol: 0.25 }, ring: [{ f: 420, d: 0.25, vol: 0.16 }, { f: 1130, d: 0.18, vol: 0.09 }, { f: 2470, d: 0.1, vol: 0.04 }] },
160
+ water: { heel: { f: 70, d: 0.04, vol: 0.25 }, roll: { type: "bandpass", f: 3e3, q: 1.2, d: 0.1, vol: 0.18 }, splash: { f: 1100, f2: 380, d: 0.22, vol: 0.55 }, grains: { count: 4, f: 2400, q: 6, spread: 0.18, vol: 0.12 } },
161
+ glass: { heel: { f: 90, d: 0.04, vol: 0.4 }, roll: { type: "highpass", f: 4500, q: 0.8, d: 0.06, vol: 0.2 }, grains: { count: 9, f: 6200, q: 5, spread: 0.12, vol: 0.22 }, ring: [{ f: 3900, d: 0.04, vol: 0.04 }, { f: 5300, d: 0.03, vol: 0.03 }] },
162
+ carpet: { heel: { f: 70, d: 0.05, vol: 0.32 }, roll: { type: "lowpass", f: 420, q: 0.7, d: 0.07, vol: 0.3 } }
613
163
  };
614
-
615
- // src/library/engine_saw.json
616
- var engine_saw_default = {
617
- id: "engine_saw",
618
- bus: "world",
619
- order: "game_loop",
620
- source: { kind: "osc", wave: "sawtooth", loop: true },
621
- filters: { mode: "serial", bank: [{ type: "lowpass", frequency: { set: 400 } }] },
622
- smoothing: { volume: 0.15, frequency: 0.08 },
623
- startAbove: 2e-3,
624
- sources: [
625
- "jogo:L62115 motor: sawtooth through a 400 Hz lowpass",
626
- "jogo:L70421 level 0.035 + speed \xD7 0.003, frequency 38 + speed \xD7 7 (the host drives `set`)",
627
- "temporary home until the vehicles package (Q12)"
628
- ]
164
+ var GAITS = {
165
+ walk: { vol: 1, bright: 1, toe: 0.075, short: 1 },
166
+ run: { vol: 1.35, bright: 1.25, toe: 0.035, short: 0.75 }
629
167
  };
630
-
631
- // src/library/horn.json
632
- var horn_default = {
633
- id: "horn",
634
- bus: "world",
635
- order: "chord",
636
- duration: 0.6,
637
- tail: 0.02,
638
- layers: [
639
- { source: { kind: "osc", wave: "square", frequency: { set: 415 } }, envelope: [[0, 0.08, "set"], [0.6, 1e-4, "set", "fixed"]] },
640
- { source: { kind: "osc", wave: "square", frequency: { set: 523 } }, envelope: [[0, 0.08, "set"], [0.6, 1e-4, "set", "fixed"]] }
168
+ var grainLayers = (g, take, start, scale) => Array.from(
169
+ { length: g.count },
170
+ (_, i) => sceneNoise({ type: "bandpass", f: [g.f * take.f * 0.85, g.f * take.f * 1.15], q: g.q, d: [8e-3, 0.02], vol: g.vol * scale * take.v, delay: start + (i + 0.5) / g.count * g.spread * take.t })
171
+ );
172
+ var ringLayers = (rings, take, scale, at = 0) => rings.map((r) => sceneTone({ f: r.f * take.f, d: r.d * take.t, vol: r.vol * scale * take.v, delay: at }));
173
+ function footLayers(surface, take, gait, weight = 1) {
174
+ const s = SURFACES[surface];
175
+ const toe = gait.toe * take.t;
176
+ return [
177
+ sceneTone({ f: s.heel.f * take.f * weight ** -0.3, f2: s.heel.f * take.f * 0.6, d: s.heel.d * gait.short * take.t * weight ** 0.5, vol: s.heel.vol * gait.vol * take.v * weight ** 0.5 }),
178
+ sceneNoise({ type: "lowpass", f: 500 * gait.bright, d: 0.02 * take.t, vol: 0.18 * gait.vol * take.v * weight ** 0.5 }),
179
+ sceneNoise({ type: s.roll.type, f: [s.roll.f * take.f * gait.bright * 0.92, s.roll.f * take.f * gait.bright * 1.08], q: s.roll.q, d: s.roll.d * gait.short * take.t * weight ** 0.3, vol: s.roll.vol * gait.vol * take.v, delay: toe }),
180
+ ...s.grains ? grainLayers(s.grains, take, toe * 0.5, gait.vol * weight ** 0.5) : [],
181
+ ...s.ring ? ringLayers(s.ring, take, gait.vol * weight ** 0.5) : [],
182
+ ...s.splash ? [sceneNoise({ type: "bandpass", f: s.splash.f * take.f, f2: s.splash.f2 * take.f, q: 1.4, d: s.splash.d * take.t * weight ** 0.4, vol: s.splash.vol * gait.vol * take.v * weight ** 0.5 })] : []
183
+ ];
184
+ }
185
+ var quieter = (level, layers) => layers.map((layer) => {
186
+ const gain = layer.gain ?? 1;
187
+ if (typeof gain === "number") return { ...layer, gain: gain * level };
188
+ if (Array.isArray(gain)) return { ...layer, gain: [gain[0] * level, gain[1] * level] };
189
+ return layer;
190
+ });
191
+ var recipe = (id, layers, falloff, bus = "world") => ({ id, bus, pan: true, layers, falloff, sources: ["sdk"] });
192
+ var takeId = (id, index) => `${id}_${String(index + 1).padStart(2, "0")}`;
193
+ function takes(id, layersOf, falloff, rate = [0.96, 1.04], bus) {
194
+ const sounds = TAKES.map((take, index) => recipe(takeId(id, index), layersOf(take, index), falloff, bus));
195
+ return { sounds, set: { id, variants: sounds.map((sound) => sound.id), rate } };
196
+ }
197
+ var footstep = (surface, gait) => takes(footstepSoundId(surface, gait), (take) => footLayers(surface, take, GAITS[gait]), FOOTSTEP_FALLOFF);
198
+ var LANDINGS = { light: { weight: 1.4, cloth: 0.12 }, heavy: { weight: 2.4, cloth: 0.22 } };
199
+ var LANDING_LEVEL = 0.65;
200
+ var landing = (surface, landing2) => takes(
201
+ `land_${surface}_${landing2}`,
202
+ (take) => quieter(LANDING_LEVEL, [
203
+ // both feet, a hair apart, then the knees taking the weight (a low body thump) and the clothes settling
204
+ ...footLayers(surface, take, GAITS.run, LANDINGS[landing2].weight),
205
+ ...footLayers(surface, { ...take, v: take.v * 0.6 }, GAITS.run, LANDINGS[landing2].weight * 0.6).map((layer) => ({ ...layer, at: (typeof layer.at === "number" ? layer.at : 0) + 0.018 * take.t })),
206
+ sceneTone({ f: 60 * take.f, f2: 38, d: 0.14 * LANDINGS[landing2].weight * take.t, vol: 0.22 * LANDINGS[landing2].weight ** 0.7 * take.v, delay: 0.02 }),
207
+ sceneNoise({ type: "bandpass", f: [1800, 2600], q: 0.8, d: 0.18, vol: LANDINGS[landing2].cloth * take.v, delay: 0.04 })
208
+ ]),
209
+ FOOTSTEP_FALLOFF
210
+ );
211
+ var whoosh = (blow, from, to, d, vol, q = 1.6) => (take) => [
212
+ // the air: a band sweeping up as the limb speeds up; it peaks `SWING_LEAD_SECONDS` in, the moment before the contact, then dies away
213
+ sceneNoise({ type: "bandpass", f: from * take.f, f2: to * take.f, q, a: SWING_LEAD_SECONDS[blow], d: d * take.t, vol: vol * take.v }),
214
+ sceneNoise({ type: "lowpass", f: 600 * take.f, a: SWING_LEAD_SECONDS[blow], d: d * take.t * 0.8, vol: vol * 0.35 * take.v })
215
+ ];
216
+ var fleshHit = (o) => (take) => [
217
+ sceneNoise({ type: "bandpass", f: o.slapF * take.f, q: 1.2, d: 0.03 * take.t, vol: o.slap * take.v }),
218
+ sceneNoise({ type: "highpass", f: 3500 * take.f, d: 0.012, vol: o.slap * 0.4 * take.v }),
219
+ sceneTone({ f: o.thudF * take.f, f2: o.thudF * 0.5, d: o.thudD * take.t, vol: o.thud * take.v }),
220
+ sceneNoise({ type: "lowpass", f: 600 * take.f, d: o.thudD * 1.2 * take.t, vol: o.thud * 0.5 * take.v }),
221
+ ...o.crack ? [sceneNoise({ type: "highpass", f: 2500, d: 0.015, vol: o.crack * take.v, delay: 6e-3 }), sceneTone({ wave: "square", f: 900 * take.f, f2: 300, d: 0.02, vol: o.crack * 0.15 * take.v, delay: 6e-3 })] : [],
222
+ ...o.wet ? [sceneNoise({ type: "bandpass", f: 1400 * take.f, f2: 500, q: 2.5, d: 0.12 * take.t, vol: o.wet * take.v, delay: 0.01 })] : []
223
+ ];
224
+ var knock = (o) => (take) => [
225
+ sceneNoise({ type: "bandpass", f: o.clickF * take.f, q: 1.5, d: 0.018, vol: o.click * take.v }),
226
+ ...o.tones.map(([f, d, vol]) => sceneTone({ f: f * take.f, d: d * take.t, vol: vol * take.v }))
227
+ ];
228
+ var voice = (o) => (take) => [
229
+ sceneVoice({ f0: o.f0 * take.f, f1: o.f1 * take.f, d: o.d * take.t, fA: o.fA * take.f, fB: o.fB * take.f, noise: o.noise, vol: o.vol * take.v })
230
+ ];
231
+ var BODY = {
232
+ jump: (take) => [
233
+ sceneNoise({ type: "bandpass", f: 900 * take.f, f2: 1700 * take.f, q: 1, a: 0.03, d: 0.12, vol: 0.18 * take.v }),
234
+ sceneNoise({ type: "bandpass", f: 2200 * take.f, q: 0.8, d: 0.04, vol: 0.12 * take.v }),
235
+ sceneTone({ f: 85 * take.f, f2: 60, d: 0.04, vol: 0.2 * take.v })
641
236
  ],
642
- falloff: { model: "none" },
643
- sources: ["jogo:L70272-70276 buzina: squares at 415 and 523 Hz, 0.08 for 0.6 s", "temporary home until the vehicles package (Q12)"]
644
- };
645
-
646
- // src/library/radio_static.json
647
- var radio_static_default = {
648
- id: "radio_static",
649
- bus: "world",
650
- order: "static",
651
- duration: 0.6,
652
- layers: [
653
- {
654
- source: { kind: "noise", offset: "random" },
655
- filters: { mode: "serial", bank: [{ type: "bandpass", frequency: { set: 1800 }, q: 0.8 }] },
656
- envelope: [[0, 0.06, "set"], ["1d", 1e-3, "exp", "fixed"]]
657
- }
237
+ cloth_rustle: (take) => [
238
+ { ...sceneNoise({ type: "bandpass", f: 2400 * take.f, q: 0.8, a: 0.06, d: 0.22 * take.t, vol: 0.12 * take.v }), modulators: [{ target: "gain", wave: "triangle", rate: [9, 14], depth: 0.5 }] }
658
239
  ],
659
- falloff: { model: "none" },
660
- sources: ["jogo:L69998-70004 chiado: noise through a 1800 Hz bandpass (Q 0.8), 0.06 falling over 0.6 s", "temporary home until the props or radio package (Q12)"]
661
- };
662
-
663
- // src/library/rotor_blades.json
664
- var rotor_blades_default = {
665
- id: "rotor_blades",
666
- bus: "world",
667
- order: "game_loop",
668
- source: { kind: "noise", loop: true },
669
- filters: { mode: "serial", bank: [{ type: "lowpass", frequency: { set: 300 } }] },
670
- modulators: [{ target: "gain", rate: 11, depth: 0.5, base: 0.5 }],
671
- smoothing: { volume: 0.15, frequency: 0.08 },
672
- startAbove: 2e-3,
673
- sources: [
674
- "jogo:L62116-62120 rotor: noise through a 300 Hz lowpass, blade beat as an 11 Hz LFO of 0.5 on a 0.5 gain stage",
675
- "jogo:L70426 level min(0.5, 30 / (distance + 10))",
676
- "temporary home until the vehicles package (Q12)"
677
- ]
678
- };
679
-
680
- // src/library/siren.json
681
- var siren_default = {
682
- id: "siren",
683
- bus: "world",
684
- order: "siren",
685
- duration: 9,
686
- tail: 0.2,
687
- layers: [
688
- {
689
- wet: 1.5,
690
- source: { kind: "osc", wave: "sine", frequency: { set: 850 } },
691
- modulators: [{ target: "frequency", rate: 0.18, depth: 280 }],
692
- filters: { mode: "serial", bank: [{ type: "lowpass", frequency: { set: 1400 } }] },
693
- envelope: [[0, 1e-4, "set", "fixed"], [2, 0.035, "exp"], [6, 0.035, "set"], [9, 1e-4, "exp", "fixed"]]
694
- }
240
+ body_fall: (take) => [
241
+ sceneTone({ f: 58 * take.f, f2: 35, d: 0.28 * take.t, vol: 0.9 * take.v }),
242
+ sceneNoise({ type: "lowpass", f: 480 * take.f, d: 0.3 * take.t, vol: 0.55 * take.v }),
243
+ sceneNoise({ type: "bandpass", f: 1300 * take.f, q: 1, d: 0.05, vol: 0.3 * take.v }),
244
+ // the limbs a moment later, and the clothes
245
+ sceneTone({ f: 75 * take.f, f2: 45, d: 0.12, vol: 0.4 * take.v, delay: 0.13 * take.t }),
246
+ sceneNoise({ type: "bandpass", f: 2200, q: 0.8, d: 0.2, vol: 0.12 * take.v, delay: 0.05 })
247
+ ],
248
+ fall_damage: (take) => [
249
+ ...BODY.body_fall(take),
250
+ sceneNoise({ type: "highpass", f: 2800, d: 0.02, vol: 0.35 * take.v, delay: 0.01 }),
251
+ ...voice({ f0: 170, f1: 110, d: 0.32, fA: 650, fB: 1150, noise: 0.25, vol: 0.32 })(take).map((layer) => ({ ...layer, at: 0.08 }))
252
+ ],
253
+ player_hurt: voice({ f0: 150, f1: 105, d: 0.26, fA: 620, fB: 1100, noise: 0.3, vol: 0.35 }),
254
+ player_breath_run: (take) => [
255
+ sceneNoise({ type: "bandpass", f: 1200 * take.f, q: 1, a: 0.08, d: 0.22 * take.t, vol: 0.25 * take.v }),
256
+ sceneNoise({ type: "bandpass", f: 900 * take.f, q: 1, a: 0.06, d: 0.26 * take.t, vol: 0.3 * take.v, delay: 0.3 * take.t })
257
+ ],
258
+ player_death: voice({ f0: 135, f1: 68, d: 1, fA: 560, fB: 980, noise: 0.35, vol: 0.4 }),
259
+ bite: (take) => [
260
+ sceneNoise({ type: "highpass", f: 2600, d: 0.015, vol: 0.4 * take.v }),
261
+ sceneNoise({ type: "bandpass", f: 1600 * take.f, f2: 450, q: 2.2, d: 0.16 * take.t, vol: 0.55 * take.v, delay: 0.01 }),
262
+ sceneNoise({ type: "bandpass", f: 3200 * take.f, f2: 1800, q: 1.2, d: 0.22 * take.t, vol: 0.25 * take.v, delay: 0.09 }),
263
+ sceneTone({ f: 120 * take.f, f2: 70, d: 0.06, vol: 0.3 * take.v })
264
+ ],
265
+ grab: (take) => [
266
+ sceneNoise({ type: "bandpass", f: 1900 * take.f, q: 0.9, a: 4e-3, d: 0.13 * take.t, vol: 0.32 * take.v }),
267
+ sceneTone({ f: 95 * take.f, f2: 60, d: 0.07, vol: 0.3 * take.v }),
268
+ sceneNoise({ type: "lowpass", f: 700, d: 0.08, vol: 0.2 * take.v })
695
269
  ],
696
- falloff: { model: "none" },
697
- sources: ["cena:L1369 siren: 850 Hz sine swept \xB1280 Hz at 0.18 Hz, 1400 Hz lowpass, 0.035 for 9 s, reverb send 1.5", "temporary home until the vehicles package (Q12)"]
270
+ punch_swing: whoosh("punch", 450, 1500, 0.16, 0.9),
271
+ punch_hit_body: fleshHit({ slap: 0.55, slapF: 1500, thud: 0.6, thudF: 115, thudD: 0.09 }),
272
+ punch_hit_face: fleshHit({ slap: 0.75, slapF: 2300, thud: 0.45, thudF: 150, thudD: 0.07, crack: 0.25 }),
273
+ punch_hit_object: knock({ clickF: 1800, click: 0.3, tones: [[210, 0.04, 0.25]] }),
274
+ kick_swing: whoosh("kick", 280, 950, 0.22, 1, 1.3),
275
+ kick_hit: fleshHit({ slap: 0.45, slapF: 1100, thud: 0.85, thudF: 82, thudD: 0.14 }),
276
+ kick_hit_object: knock({ clickF: 1e3, click: 0.3, tones: [[92, 0.08, 0.5]] }),
277
+ shove_two_hands: (take) => [
278
+ sceneNoise({ type: "bandpass", f: 950 * take.f, q: 0.9, a: 0.01, d: 0.13 * take.t, vol: 0.38 * take.v }),
279
+ sceneTone({ f: 100 * take.f, f2: 60, d: 0.1 * take.t, vol: 0.45 * take.v }),
280
+ sceneNoise({ type: "bandpass", f: 2300, q: 0.8, d: 0.16, vol: 0.12 * take.v, delay: 0.03 })
281
+ ],
282
+ knife_swing: whoosh("blade", 1900, 4600, 0.12, 1.1, 2.2),
283
+ knife_stab: fleshHit({ slap: 0.2, slapF: 1200, thud: 0.4, thudF: 160, thudD: 0.06, wet: 0.5 }),
284
+ knife_slash: (take) => [
285
+ sceneNoise({ type: "highpass", f: 3200 * take.f, f2: 1800, d: 0.13 * take.t, vol: 0.35 * take.v }),
286
+ sceneNoise({ type: "bandpass", f: 1300 * take.f, f2: 600, q: 2.5, d: 0.12 * take.t, vol: 0.35 * take.v, delay: 0.02 })
287
+ ],
288
+ knife_hit_object: knock({ clickF: 3200, click: 0.25, tones: [[2800, 0.08, 0.07], [4150, 0.06, 0.05]] }),
289
+ blunt_swing: whoosh("blunt", 220, 760, 0.28, 1.1, 1.5),
290
+ blunt_hit: fleshHit({ slap: 0.35, slapF: 900, thud: 1, thudF: 70, thudD: 0.18, crack: 0.3 }),
291
+ blunt_hit_object: knock({ clickF: 1400, click: 0.35, tones: [[300, 0.12, 0.25], [760, 0.08, 0.12]] }),
292
+ axe_swing: whoosh("chop", 190, 620, 0.3, 1.2, 1.4),
293
+ axe_chop: fleshHit({ slap: 0.3, slapF: 1400, thud: 0.75, thudF: 140, thudD: 0.1, crack: 0.35, wet: 0.45 }),
294
+ axe_hit_object: knock({ clickF: 2200, click: 0.4, tones: [[1500, 0.15, 0.1], [2300, 0.1, 0.06], [140, 0.1, 0.35]] })
698
295
  };
699
-
700
- // src/library/index.ts
701
- var LIBRARY_SOUNDS = [horn_default, radio_static_default, siren_default];
702
- var LIBRARY_LOOPS = [engine_saw_default, rotor_blades_default, crowd_rumble_default];
703
- var LIBRARY_BANK = { id: "library", sounds: LIBRARY_SOUNDS, loops: LIBRARY_LOOPS };
704
-
705
- // src/webaudio/noise.ts
706
- function fillNoise(random, length) {
707
- const samples = new Float32Array(length);
708
- for (let index = 0; index < length; index++) samples[index] = random() * 2 - 1;
709
- return samples;
710
- }
711
- function fillImpulse(random, options) {
712
- const size = Math.floor(options.length);
713
- return Array.from({ length: options.channels }, () => {
714
- const samples = new Float32Array(size);
715
- for (let index = 0; index < options.length; index++) {
716
- const value = (random() * 2 - 1) * Math.exp(options.decay * Math.log(1 - index / options.length));
717
- if (index < size) samples[index] = value;
718
- }
719
- return samples;
720
- });
721
- }
722
- function bufferOf(context, channels, requestedLength) {
723
- const buffer = context.createBuffer(channels.length, requestedLength, context.sampleRate);
724
- channels.forEach((samples, channel) => buffer.getChannelData(channel).set(samples));
725
- return buffer;
726
- }
727
-
728
- // src/webaudio/chain.ts
729
- function buildChain(context, profile, random) {
730
- let compressor;
731
- if (profile.compressor) {
732
- compressor = context.createDynamicsCompressor();
733
- compressor.threshold.value = profile.compressor.threshold;
734
- compressor.ratio.value = profile.compressor.ratio;
735
- if (profile.compressor.knee !== void 0) compressor.knee.value = profile.compressor.knee;
736
- if (profile.compressor.attack !== void 0) compressor.attack.value = profile.compressor.attack;
737
- if (profile.compressor.release !== void 0) compressor.release.value = profile.compressor.release;
738
- compressor.connect(context.destination);
739
- }
740
- const master = context.createGain();
741
- master.gain.value = profile.master;
742
- master.connect(compressor ?? context.destination);
743
- if (!profile.reverb) {
744
- return {
745
- master,
746
- disconnect: () => {
747
- master.disconnect();
748
- compressor?.disconnect();
749
- }
750
- };
751
- }
752
- const convolver = context.createConvolver();
753
- const length = context.sampleRate * profile.reverb.seconds;
754
- convolver.buffer = bufferOf(context, fillImpulse(random, { channels: 2, length, decay: profile.reverb.decay }), length);
755
- const wet = context.createGain();
756
- wet.gain.value = profile.reverb.wet;
757
- wet.connect(convolver);
758
- convolver.connect(master);
759
- return {
760
- master,
761
- wet,
762
- disconnect: () => {
763
- wet.disconnect();
764
- convolver.disconnect();
765
- master.disconnect();
766
- compressor?.disconnect();
767
- }
768
- };
769
- }
770
-
771
- // src/webaudio/context.ts
772
- function globalContext() {
773
- const scope = globalThis;
774
- const Constructor = scope.AudioContext ?? scope.webkitAudioContext;
775
- return Constructor ? new Constructor() : null;
776
- }
777
- function unlockContext(context) {
778
- if (context.state === "suspended") void context.resume();
779
- }
780
-
781
- // src/webaudio/nodes.ts
782
- function assign(param, automation, time) {
783
- if (automation.value !== void 0) param.value = automation.value;
784
- if (automation.from !== void 0) param.setValueAtTime(automation.from, time);
785
- }
786
- function ramp(param, curve, value, at) {
787
- if (curve === "set") param.setValueAtTime(value, at);
788
- else if (curve === "exp") param.exponentialRampToValueAtTime(value, at);
789
- else param.linearRampToValueAtTime(value, at);
790
- }
791
- function rampTo(param, automation, time) {
792
- if (automation.to !== void 0 && automation.at !== void 0) ramp(param, automation.curve ?? "linear", automation.to, time + automation.at);
793
- }
794
- function applyEnvelope(param, points, time) {
795
- for (const point of points) ramp(param, point.curve, point.value, time + point.time);
796
- }
797
- function noiseSource(target, loop) {
798
- const source = target.context.createBufferSource();
799
- source.buffer = target.noise();
800
- if (loop) source.loop = true;
801
- return source;
802
- }
803
- function filterNode(context, filter, time) {
804
- const node = context.createBiquadFilter();
805
- node.type = filter.type;
806
- assign(node.frequency, filter.frequency, time);
807
- if (filter.q !== void 0) node.Q.value = filter.q;
808
- if (filter.gain !== void 0) node.gain.value = filter.gain;
809
- return node;
810
- }
811
- function gainNode(context, value) {
812
- const node = context.createGain();
813
- node.gain.value = value;
814
- return node;
815
- }
816
- function voiceOf(sources) {
817
- return {
818
- sources,
819
- stop() {
820
- for (const source of sources) {
821
- try {
822
- source.stop();
823
- } catch {
824
- }
825
- }
826
- }
827
- };
828
- }
829
-
830
- // src/webaudio/realize-loop.ts
831
- var fixed = (range) => typeof range === "number" ? range : 0;
832
- var fixedFrequency = (automation) => fixed("set" in automation ? automation.set : automation.from);
833
- function sourceOf(recipe, target) {
834
- if (recipe.source.kind === "noise") return { source: noiseSource(target, true) };
835
- const oscillator = target.context.createOscillator();
836
- oscillator.type = recipe.source.wave;
837
- if (recipe.source.frequency) oscillator.frequency.value = fixedFrequency(recipe.source.frequency);
838
- return { source: oscillator, frequency: oscillator.frequency };
839
- }
840
- function stopAll(sources) {
841
- return () => {
842
- for (const source of sources) {
843
- try {
844
- source.stop();
845
- } catch {
846
- }
847
- }
848
- };
849
- }
850
- function realizeGameLoop(recipe, target) {
851
- const { context } = target;
852
- const gain = context.createGain();
853
- const filters = (recipe.filters?.bank ?? []).map(() => context.createBiquadFilter());
854
- gain.gain.value = 0;
855
- const last = filters.reduce((previous, filter) => previous ? previous.connect(filter) : filter, void 0);
856
- last?.connect(gain);
857
- gain.connect(target.output);
858
- const { source, frequency } = sourceOf(recipe, target);
859
- recipe.filters?.bank.forEach((spec, index) => {
860
- const filter = filters[index];
861
- filter.type = spec.type;
862
- filter.frequency.value = fixedFrequency(spec.frequency);
863
- if (spec.q !== void 0) filter.Q.value = fixed(spec.q);
864
- });
865
- const sources = [source];
866
- for (const modulator of recipe.modulators ?? []) {
867
- const lfo = context.createOscillator();
868
- const depth = context.createGain();
869
- const stage = context.createGain();
870
- if (modulator.wave) lfo.type = modulator.wave;
871
- lfo.frequency.value = fixed(modulator.rate);
872
- depth.gain.value = fixed(modulator.depth);
873
- stage.gain.value = modulator.base ?? 0;
874
- lfo.connect(depth);
875
- depth.connect(stage.gain);
876
- last?.disconnect();
877
- last?.connect(stage);
878
- stage.connect(gain);
879
- lfo.start();
880
- sources.push(lfo);
881
- }
882
- source.connect(filters[0] ?? gain);
883
- source.start();
884
- return { gain, source, ...frequency ? { frequency } : {}, stop: stopAll(sources) };
885
- }
886
- function realizeChainLoop(recipe, target, volume) {
887
- const { context } = target;
888
- const { source, frequency } = sourceOf(recipe, target);
889
- const filters = (recipe.filters?.bank ?? []).map(
890
- (spec) => filterNode(context, { type: spec.type, frequency: { value: fixedFrequency(spec.frequency) }, ...spec.q === void 0 ? {} : { q: fixed(spec.q) } }, target.time)
891
- );
892
- const gain = gainNode(context, volume);
893
- const lfos = (recipe.modulators ?? []).map((modulator) => {
894
- const lfo = context.createOscillator();
895
- if (modulator.wave) lfo.type = modulator.wave;
896
- lfo.frequency.value = fixed(modulator.rate);
897
- const depth = gainNode(context, fixed(modulator.depth));
898
- lfo.connect(depth);
899
- depth.connect(gain.gain);
900
- return lfo;
901
- });
902
- let tail = source;
903
- for (const filter of filters) tail = tail.connect(filter);
904
- tail.connect(gain);
905
- gain.connect(target.output);
906
- source.start();
907
- for (const lfo of lfos) lfo.start();
908
- return { gain, source, ...frequency ? { frequency } : {}, stop: stopAll([source, ...lfos]) };
909
- }
910
-
911
- // src/webaudio/engine-loop.ts
912
- var INERT = { set() {
913
- }, stop() {
914
- } };
915
- function createLoopHandle(recipe, host, volume) {
916
- if (!recipe) return INERT;
917
- const loopRecipe = recipe;
918
- const isChain = loopRecipe.order === "chain_loop";
919
- const opening = volume ?? loopRecipe.volume ?? 0;
920
- const smoothing = loopRecipe.smoothing ?? LOOP_SMOOTHING;
921
- let state = createLoopState(loopRecipe);
922
- let nodes;
923
- const entry = {
924
- get created() {
925
- return nodes !== void 0;
926
- },
927
- rewire() {
928
- nodes?.gain.disconnect();
929
- nodes?.gain.connect(host.output(loopRecipe.bus));
930
- },
931
- stop() {
932
- nodes?.stop();
933
- nodes?.gain.disconnect();
934
- nodes = void 0;
935
- state = createLoopState(loopRecipe);
936
- host.unregister(entry);
937
- }
938
- };
939
- const create = (context, startVolume) => {
940
- if (loopRecipe.source.kind === "noise") host.prepareNoise();
941
- const target = { context, output: host.output(loopRecipe.bus), noise: host.noise, time: context.currentTime };
942
- const made = isChain ? realizeChainLoop(loopRecipe, target, startVolume) : realizeGameLoop(loopRecipe, target);
943
- host.register(entry);
944
- return made;
945
- };
946
- const open = () => {
947
- const context = host.ready();
948
- if (!context) return;
949
- nodes = create(context, opening);
950
- state = createLoopState({ ...loopRecipe, volume: opening }, true);
951
- };
952
- if (isChain) open();
953
- return {
954
- set(next) {
955
- if (isChain && !nodes) open();
956
- const context = host.ready();
957
- if (!context) return;
958
- const action = state.set(next);
959
- if (!action) return;
960
- nodes ?? (nodes = create(context, 0));
961
- nodes.gain.gain.setTargetAtTime(action.volume, context.currentTime, smoothing.volume);
962
- if (action.frequency !== void 0 && nodes.frequency) nodes.frequency.setTargetAtTime(action.frequency, context.currentTime, smoothing.frequency);
963
- },
964
- stop: entry.stop
965
- };
966
- }
967
-
968
- // src/webaudio/engine-noise.ts
969
- function createNoiseStore(random) {
970
- const store = {
971
- random,
972
- reset() {
973
- store.samples = void 0;
974
- store.buffer = void 0;
975
- }
976
- };
977
- return store;
978
- }
979
- function prepareNoise(store, context, seconds) {
980
- store.samples ?? (store.samples = fillNoise(store.random, Math.floor(context.sampleRate * seconds)));
981
- return store.samples;
982
- }
983
- function noiseBuffer(store, context, seconds) {
984
- const samples = prepareNoise(store, context, seconds);
985
- store.buffer ?? (store.buffer = bufferOf(context, [samples], context.sampleRate * seconds));
986
- return store.buffer;
987
- }
988
-
989
- // src/webaudio/engine-queue.ts
990
- var nowMs = () => typeof performance === "undefined" ? Date.now() : performance.now();
991
- function scheduleTask(task) {
992
- if (typeof MessageChannel === "undefined") {
993
- setTimeout(task, 0);
994
- return;
995
- }
996
- const channel = new MessageChannel();
997
- channel.port1.onmessage = () => {
998
- channel.port1.close();
999
- task();
1000
- };
1001
- channel.port2.postMessage(0);
1002
- }
1003
- function createStartQueue(options = {}) {
1004
- const budgetMs = options.budgetMs ?? 2;
1005
- const maxLatencyMs = options.maxLatencyMs ?? 250;
1006
- let jobs = [];
1007
- let scheduled = false;
1008
- const drain = () => {
1009
- scheduled = false;
1010
- const began = nowMs();
1011
- while (jobs.length > 0) {
1012
- const job = jobs.shift();
1013
- if (nowMs() - job.queuedAt > maxLatencyMs) job.drop();
1014
- else job.run();
1015
- if (nowMs() - began >= budgetMs) break;
1016
- }
1017
- if (jobs.length > 0) schedule();
1018
- };
1019
- const schedule = () => {
1020
- if (scheduled) return;
1021
- scheduled = true;
1022
- scheduleTask(drain);
1023
- };
1024
- return {
1025
- push(job) {
1026
- jobs.push({ ...job, queuedAt: nowMs() });
1027
- schedule();
1028
- },
1029
- clear() {
1030
- for (const job of jobs) job.drop();
1031
- jobs = [];
1032
- },
1033
- get size() {
1034
- return jobs.length;
1035
- }
1036
- };
1037
- }
1038
-
1039
- // src/webaudio/realize-game.ts
1040
- var end = (layer, start) => start + layer.duration + layer.tail;
1041
- var firstFilter = (layer) => layer.filters?.bank[0];
1042
- var offsetOf = (layer) => layer.source.kind === "noise" ? layer.source.offset : 0;
1043
- function setFilter(node, filter, time) {
1044
- if (!filter) return;
1045
- node.type = filter.type;
1046
- assign(node.frequency, filter.frequency, time);
1047
- if (filter.q !== void 0) node.Q.value = filter.q;
1048
- }
1049
- function realizeBurst(plan, target) {
1050
- const { context } = target;
1051
- const layer = plan.layers[0];
1052
- const time = target.time + layer.at;
1053
- const gain = context.createGain();
1054
- gain.connect(target.output);
1055
- const source = noiseSource(target, layer.source.kind === "noise" && layer.source.loop);
1056
- const filter = context.createBiquadFilter();
1057
- source.connect(filter);
1058
- filter.connect(gain);
1059
- setFilter(filter, firstFilter(layer), time);
1060
- applyEnvelope(gain.gain, layer.envelope, time);
1061
- source.start(time, offsetOf(layer));
1062
- source.stop(end(layer, time));
1063
- return voiceOf([source]);
1064
- }
1065
- function realizeScream(plan, target) {
1066
- const { context } = target;
1067
- const layer = plan.layers[0];
1068
- const time = target.time + layer.at;
1069
- const source = noiseSource(target, layer.source.kind === "noise" && layer.source.loop);
1070
- const gain = context.createGain();
1071
- gain.connect(target.output);
1072
- for (const band of layer.filters?.bank ?? []) {
1073
- const filter = context.createBiquadFilter();
1074
- filter.type = band.type;
1075
- if (band.q !== void 0) filter.Q.value = band.q;
1076
- assign(filter.frequency, band.frequency, time);
1077
- rampTo(filter.frequency, band.frequency, time);
1078
- const level = context.createGain();
1079
- level.gain.value = band.level ?? 1;
1080
- source.connect(filter);
1081
- filter.connect(level);
1082
- level.connect(gain);
1083
- }
1084
- const sources = [source];
1085
- const lfos = layer.modulators.map((modulator) => {
1086
- const lfo = context.createOscillator();
1087
- const depth = context.createGain();
1088
- if (modulator.wave) lfo.type = modulator.wave;
1089
- lfo.frequency.value = modulator.rate;
1090
- depth.gain.value = modulator.depth;
1091
- lfo.connect(depth);
1092
- depth.connect(gain.gain);
1093
- return lfo;
1094
- });
1095
- applyEnvelope(gain.gain, layer.envelope, time);
1096
- source.start(time, offsetOf(layer));
1097
- source.stop(end(layer, time));
1098
- for (const lfo of lfos) {
1099
- lfo.start(time);
1100
- lfo.stop(end(layer, time));
1101
- sources.push(lfo);
1102
- }
1103
- return voiceOf(sources);
1104
- }
1105
- function realizeGroan(plan, target) {
1106
- const { context } = target;
1107
- const layer = plan.layers[0];
1108
- const time = target.time + layer.at;
1109
- const oscillator = context.createOscillator();
1110
- const gain = context.createGain();
1111
- const filter = context.createBiquadFilter();
1112
- const band = firstFilter(layer);
1113
- if (layer.source.kind === "osc") oscillator.type = layer.source.wave;
1114
- if (band) {
1115
- filter.type = band.type;
1116
- if (band.q !== void 0) filter.Q.value = band.q;
1117
- }
1118
- if (layer.source.kind === "osc") {
1119
- assign(oscillator.frequency, layer.source.frequency, time);
1120
- rampTo(oscillator.frequency, layer.source.frequency, time);
1121
- }
1122
- if (band) assign(filter.frequency, band.frequency, time);
1123
- applyEnvelope(gain.gain, layer.envelope, time);
1124
- oscillator.connect(filter);
1125
- filter.connect(gain);
1126
- gain.connect(target.output);
1127
- oscillator.start(time);
1128
- oscillator.stop(end(layer, time));
1129
- return voiceOf([oscillator]);
1130
- }
1131
- function realizeBoom(plan, target) {
1132
- const { context } = target;
1133
- const layer = plan.layers[0];
1134
- const time = target.time + layer.at;
1135
- const oscillator = context.createOscillator();
1136
- const gain = context.createGain();
1137
- if (layer.source.kind === "osc") {
1138
- oscillator.type = layer.source.wave;
1139
- assign(oscillator.frequency, layer.source.frequency, time);
1140
- rampTo(oscillator.frequency, layer.source.frequency, time);
1141
- }
1142
- applyEnvelope(gain.gain, layer.envelope, time);
1143
- oscillator.connect(gain);
1144
- gain.connect(target.output);
1145
- oscillator.start(time);
1146
- oscillator.stop(end(layer, time));
1147
- return voiceOf([oscillator]);
1148
- }
1149
- function realizeStatic(plan, target) {
1150
- const { context } = target;
1151
- const layer = plan.layers[0];
1152
- const time = target.time + layer.at;
1153
- const gain = context.createGain();
1154
- const source = context.createBufferSource();
1155
- const filter = context.createBiquadFilter();
1156
- source.buffer = target.noise();
1157
- setFilter(filter, firstFilter(layer), time);
1158
- applyEnvelope(gain.gain, layer.envelope, time);
1159
- source.connect(filter);
1160
- filter.connect(gain);
1161
- gain.connect(target.output);
1162
- source.start(time, offsetOf(layer));
1163
- source.stop(end(layer, time));
1164
- return voiceOf([source]);
1165
- }
1166
- function realizeChord(plan, target) {
1167
- const { context } = target;
1168
- const layers = plan.layers;
1169
- const oscillators = layers.map(() => context.createOscillator());
1170
- const gain = context.createGain();
1171
- for (let index = layers.length - 1; index >= 0; index--) {
1172
- const source = layers[index].source;
1173
- if (source.kind === "osc") oscillators[index].type = source.wave;
1174
- }
1175
- layers.forEach((layer, index) => {
1176
- if (layer.source.kind === "osc") assign(oscillators[index].frequency, layer.source.frequency, target.time + layer.at);
1177
- });
1178
- applyEnvelope(gain.gain, layers[0].envelope, target.time + layers[0].at);
1179
- for (const oscillator of oscillators) oscillator.connect(gain);
1180
- gain.connect(target.output);
1181
- layers.forEach((layer, index) => oscillators[index].start(target.time + layer.at));
1182
- layers.forEach((layer, index) => oscillators[index].stop(end(layer, target.time + layer.at)));
1183
- return voiceOf(oscillators);
1184
- }
1185
- function realizeCalls(plan, target) {
1186
- const { context } = target;
1187
- const gain = context.createGain();
1188
- const filter = context.createBiquadFilter();
1189
- if (plan.silent) return voiceOf([]);
1190
- const band = firstFilter(plan.layers[0]);
1191
- if (band) filter.type = band.type;
1192
- filter.connect(gain);
1193
- gain.connect(target.output);
1194
- const oscillators = plan.layers.map((layer) => {
1195
- const time = target.time + layer.at;
1196
- const oscillator = context.createOscillator();
1197
- if (layer.source.kind === "osc") {
1198
- oscillator.type = layer.source.wave;
1199
- assign(oscillator.frequency, layer.source.frequency, time);
1200
- rampTo(oscillator.frequency, layer.source.frequency, time);
1201
- }
1202
- if (band) {
1203
- assign(filter.frequency, band.frequency, time);
1204
- if (band.q !== void 0) filter.Q.value = band.q;
1205
- }
1206
- const envelope2 = context.createGain();
1207
- applyEnvelope(envelope2.gain, layer.envelope, time);
1208
- oscillator.connect(envelope2);
1209
- envelope2.connect(filter);
1210
- oscillator.start(time);
1211
- oscillator.stop(end(layer, time));
1212
- return oscillator;
1213
- });
1214
- return voiceOf(oscillators);
1215
- }
1216
-
1217
- // src/webaudio/realize-layered.ts
1218
- var end2 = (layer, start) => start + layer.duration + layer.tail;
1219
- function outputStage(plan, layer, target) {
1220
- const { context } = target;
1221
- const level = gainNode(context, layer.level);
1222
- let node = level;
1223
- if (plan.pan !== void 0 && typeof context.createStereoPanner === "function") {
1224
- const panner = context.createStereoPanner();
1225
- panner.pan.value = plan.pan;
1226
- level.connect(panner);
1227
- node = panner;
1228
- }
1229
- node.connect(target.output);
1230
- if (layer.wet && target.wet) {
1231
- const send = gainNode(context, layer.wet);
1232
- node.connect(send);
1233
- send.connect(target.wet);
1234
- }
1235
- return level;
1236
- }
1237
- function realizeNoiseLayer(plan, layer, target) {
1238
- const { context } = target;
1239
- const time = target.time + layer.at;
1240
- const source = noiseSource(target, layer.source.kind === "noise" && layer.source.loop);
1241
- const filters = (layer.filters?.bank ?? []).map((filter) => {
1242
- const node = filterNode(context, filter, time);
1243
- rampTo(node.frequency, filter.frequency, time);
1244
- return node;
1245
- });
1246
- const envelope2 = context.createGain();
1247
- applyEnvelope(envelope2.gain, layer.envelope, time);
1248
- let tail = source;
1249
- for (const filter of filters) tail = tail.connect(filter);
1250
- tail.connect(envelope2);
1251
- envelope2.connect(outputStage(plan, layer, target));
1252
- source.start(time, layer.source.kind === "noise" ? layer.source.offset : 0);
1253
- source.stop(end2(layer, time));
1254
- return [source];
1255
- }
1256
- function realizeToneLayer(plan, layer, target) {
1257
- const { context } = target;
1258
- const time = target.time + layer.at;
1259
- const oscillator = context.createOscillator();
1260
- if (layer.source.kind === "osc") {
1261
- oscillator.type = layer.source.wave;
1262
- assign(oscillator.frequency, layer.source.frequency, time);
1263
- rampTo(oscillator.frequency, layer.source.frequency, time);
1264
- }
1265
- const envelope2 = context.createGain();
1266
- applyEnvelope(envelope2.gain, layer.envelope, time);
1267
- oscillator.connect(envelope2);
1268
- envelope2.connect(outputStage(plan, layer, target));
1269
- oscillator.start(time);
1270
- oscillator.stop(end2(layer, time));
1271
- return [oscillator];
1272
- }
1273
- function realizeVoiceLayer(plan, layer, target) {
1274
- const source = layer.source;
1275
- if (source.kind !== "formant_voice") return [];
1276
- const { context } = target;
1277
- const time = target.time + layer.at;
1278
- const oscillator = context.createOscillator();
1279
- oscillator.type = source.wave;
1280
- assign(oscillator.frequency, source.frequency, time);
1281
- rampTo(oscillator.frequency, source.frequency, time);
1282
- const vibrato = context.createOscillator();
1283
- vibrato.frequency.value = source.vibrato.rate;
1284
- const vibratoDepth = gainNode(context, source.vibrato.depth);
1285
- vibrato.connect(vibratoDepth);
1286
- vibratoDepth.connect(oscillator.frequency);
1287
- const formants = source.formants.map((formant) => filterNode(context, { type: "bandpass", frequency: { value: formant.frequency }, q: formant.q }, time));
1288
- const breath = noiseSource(target, true);
1289
- const breathBand = filterNode(context, { type: "bandpass", frequency: { value: source.breath.frequency }, q: source.breath.q }, time);
1290
- const breathLevel = gainNode(context, source.breath.gain);
1291
- const mix = gainNode(context, source.mix);
1292
- for (const formant of formants) oscillator.connect(formant);
1293
- breath.connect(breathBand);
1294
- breathBand.connect(breathLevel);
1295
- for (const formant of formants) formant.connect(mix);
1296
- breathLevel.connect(mix);
1297
- const envelope2 = context.createGain();
1298
- applyEnvelope(envelope2.gain, layer.envelope, time);
1299
- mix.connect(envelope2);
1300
- envelope2.connect(outputStage(plan, layer, target));
1301
- const stop = end2(layer, time);
1302
- oscillator.start(time);
1303
- vibrato.start(time);
1304
- breath.start(time, source.breath.offset);
1305
- oscillator.stop(stop);
1306
- vibrato.stop(stop);
1307
- breath.stop(stop);
1308
- return [oscillator, vibrato, breath];
1309
- }
1310
- function realizeLayered(plan, target) {
1311
- const sources = plan.layers.flatMap((layer) => {
1312
- if (layer.source.kind === "noise") return realizeNoiseLayer(plan, layer, target);
1313
- if (layer.source.kind === "osc") return realizeToneLayer(plan, layer, target);
1314
- return realizeVoiceLayer(plan, layer, target);
1315
- });
1316
- return voiceOf(sources);
1317
- }
1318
- function realizeSiren(plan, target) {
1319
- const { context } = target;
1320
- const layer = plan.layers[0];
1321
- const time = target.time + layer.at;
1322
- const oscillator = context.createOscillator();
1323
- if (layer.source.kind === "osc") {
1324
- oscillator.type = layer.source.wave;
1325
- assign(oscillator.frequency, layer.source.frequency, time);
1326
- rampTo(oscillator.frequency, layer.source.frequency, time);
1327
- }
1328
- const depths = layer.modulators.map((modulator) => {
1329
- const lfo = context.createOscillator();
1330
- if (modulator.wave) lfo.type = modulator.wave;
1331
- lfo.frequency.value = modulator.rate;
1332
- const depth = gainNode(context, modulator.depth);
1333
- lfo.connect(depth);
1334
- if (modulator.target === "frequency") depth.connect(oscillator.frequency);
1335
- return { lfo, depth, target: modulator.target };
1336
- });
1337
- const lfos = depths.map(({ lfo }) => lfo);
1338
- const envelope2 = context.createGain();
1339
- applyEnvelope(envelope2.gain, layer.envelope, time);
1340
- for (const { depth, target: modulated } of depths) if (modulated === "gain") depth.connect(envelope2.gain);
1341
- const filters = (layer.filters?.bank ?? []).map((filter) => filterNode(context, filter, time));
1342
- let tail = oscillator;
1343
- for (const filter of filters) tail = tail.connect(filter);
1344
- tail.connect(envelope2);
1345
- envelope2.connect(target.output);
1346
- if (layer.wet && target.wet) {
1347
- const send = gainNode(context, layer.wet);
1348
- envelope2.connect(send);
1349
- send.connect(target.wet);
1350
- }
1351
- const stop = end2(layer, time);
1352
- oscillator.start(time);
1353
- for (const lfo of lfos) lfo.start(time);
1354
- oscillator.stop(stop);
1355
- for (const lfo of lfos) lfo.stop(stop);
1356
- return voiceOf([oscillator, ...lfos]);
1357
- }
1358
-
1359
- // src/webaudio/realize-weapon.ts
1360
- function noiseLayer(layer, target, time) {
1361
- const { context } = target;
1362
- const source = noiseSource(target, layer.source.kind === "noise" && layer.source.loop);
1363
- const filters = (layer.filters?.bank ?? []).map((filter) => filterNode(context, filter, time));
1364
- const envelope2 = context.createGain();
1365
- applyEnvelope(envelope2.gain, layer.envelope, time);
1366
- let tail = source;
1367
- for (const filter of filters) tail = tail.connect(filter);
1368
- tail.connect(envelope2).connect(target.output);
1369
- source.start(time, layer.source.kind === "noise" ? layer.source.offset : 0);
1370
- source.stop(time + layer.duration + layer.tail);
1371
- return source;
1372
- }
1373
- function toneLayer(layer, target, time) {
1374
- const { context } = target;
1375
- const oscillator = context.createOscillator();
1376
- if (layer.source.kind === "osc") {
1377
- oscillator.type = layer.source.wave;
1378
- assign(oscillator.frequency, layer.source.frequency, time);
1379
- rampTo(oscillator.frequency, layer.source.frequency, time);
1380
- }
1381
- const envelope2 = context.createGain();
1382
- applyEnvelope(envelope2.gain, layer.envelope, time);
1383
- oscillator.connect(envelope2).connect(target.output);
1384
- oscillator.start(time);
1385
- oscillator.stop(time + layer.duration + layer.tail);
1386
- return oscillator;
1387
- }
1388
- function realizeWeapon(plan, target) {
1389
- const sources = plan.layers.map((layer) => {
1390
- const time = target.time + layer.at;
1391
- return layer.source.kind === "noise" ? noiseLayer(layer, target, time) : toneLayer(layer, target, time);
1392
- });
1393
- return voiceOf(sources);
1394
- }
1395
-
1396
- // src/webaudio/realize.ts
1397
- var BUILDERS = {
1398
- layered: realizeLayered,
1399
- burst: realizeBurst,
1400
- scream: realizeScream,
1401
- groan: realizeGroan,
1402
- boom: realizeBoom,
1403
- static: realizeStatic,
1404
- chord: realizeChord,
1405
- calls: realizeCalls,
1406
- siren: realizeSiren,
1407
- weapon: realizeWeapon
296
+ var BODY_BUS = { player_hurt: "voices", player_death: "voices", player_breath_run: "voices", fall_damage: "voices" };
297
+ var built = [
298
+ ...SURFACE_IDS.flatMap((surface) => GAIT_IDS.map((gait) => footstep(surface, gait))),
299
+ ...SURFACE_IDS.flatMap((surface) => LANDING_IDS.map((l) => landing(surface, l))),
300
+ ...BODY_SOUND_IDS.map((id) => takes(id, BODY[id], BODY_FALLOFF, id.endsWith("_swing") ? [0.94, 1.06] : [0.96, 1.04], BODY_BUS[id]))
301
+ ];
302
+ var BODY_FOLEY_BANK = {
303
+ id: "body_foley",
304
+ sounds: built.flatMap((entry) => entry.sounds),
305
+ sets: built.map((entry) => entry.set)
1408
306
  };
1409
- function realize(plan, target) {
1410
- return BUILDERS[plan.order](plan, target);
1411
- }
1412
-
1413
- // src/webaudio/engine.ts
1414
- var usesNoise = (recipe) => recipe.layers.some((layer) => layer.source.kind !== "osc");
1415
- var planUsesNoise = (plan) => plan.layers.some((layer) => layer.source.kind !== "osc");
1416
- function createAudioEngine(options = {}) {
1417
- const seed = options.seed ?? 1;
1418
- const registry = createSoundRegistry(options.banks);
1419
- const voices = createVoiceManager({ ...options.groups ? { groups: options.groups } : {}, ...options.maxVoices === void 0 ? {} : { maxVoices: options.maxVoices } });
1420
- const engineRandom = createRng(seed);
1421
- const shared = createNoiseStore(engineRandom);
1422
- const busSettings = /* @__PURE__ */ new Map();
1423
- const busNodes = /* @__PURE__ */ new Map();
1424
- const meters = /* @__PURE__ */ new Map();
1425
- const active = /* @__PURE__ */ new Map();
1426
- const loops = /* @__PURE__ */ new Set();
1427
- const named = /* @__PURE__ */ new Map();
1428
- let mix = resolveMix(options.mix ?? DEFAULT_MIX, options.profiles);
1429
- let context = null;
1430
- let chain = null;
1431
- let listener = DEFAULT_LISTENER;
1432
- let lastTime = 0;
1433
- const outputFor = (bus) => busNodes.get(bus) ?? chain?.master;
1434
- const watched = /* @__PURE__ */ new Set();
1435
- const meterOf = (bus) => {
1436
- const existing = meters.get(bus);
1437
- if (existing) return existing;
1438
- const node = busNodes.get(bus);
1439
- if (!context || !node || typeof context.createAnalyser !== "function") return void 0;
1440
- const analyser = context.createAnalyser();
1441
- analyser.fftSize = 1024;
1442
- node.connect(analyser);
1443
- const made = { analyser, data: new Float32Array(analyser.fftSize) };
1444
- meters.set(bus, made);
1445
- return made;
1446
- };
1447
- const ensureBus = (bus) => {
1448
- if (!context || !chain || busNodes.has(bus)) return;
1449
- const node = context.createGain();
1450
- node.connect(chain.master);
1451
- busNodes.set(bus, node);
1452
- if (watched.has(bus)) meterOf(bus);
1453
- };
1454
- const ready = () => {
1455
- if (context) return context;
1456
- const created = (options.createContext ?? globalContext)();
1457
- if (!created) return null;
1458
- context = created;
1459
- chain = buildChain(created, mix, engineRandom);
1460
- if (mix.noise.create === "eager") noiseBuffer(shared, created, mix.noise.seconds);
1461
- for (const [bus, setting] of Object.entries(mix.buses ?? {})) {
1462
- busSettings.set(bus, { ...setting, ...busSettings.get(bus) });
1463
- }
1464
- for (const [bus, setting] of busSettings) {
1465
- ensureBus(bus);
1466
- busNodes.get(bus).gain.value = busGain(setting);
1467
- }
1468
- lastTime = created.currentTime;
1469
- return created;
1470
- };
1471
- const advance = () => {
1472
- if (!context) return;
1473
- const elapsed = context.currentTime - lastTime;
1474
- lastTime = context.currentTime;
1475
- if (elapsed > 0) for (const id of voices.advance(elapsed)) active.delete(id);
1476
- };
1477
- const inputFor = (recipe, playOptions, random) => ({
1478
- random,
1479
- listener,
1480
- noiseSeconds: mix.noise.seconds,
1481
- ...options.distance ? { distanceMode: options.distance } : {},
1482
- ...playOptions.volume === void 0 ? {} : { volume: playOptions.volume },
1483
- ...playOptions.distance === void 0 ? {} : { distance: playOptions.distance },
1484
- ...playOptions.position ? { position: playOptions.position } : {},
1485
- ...playOptions.params ? { params: playOptions.params } : {},
1486
- ...options.occlusion && playOptions.position && recipe.falloff?.model !== "none" ? { occlusion: options.occlusion(playOptions.position) } : {}
1487
- });
1488
- const startQueue = options.deferredStart ? createStartQueue(options.deferredStart) : null;
1489
- const build = (plan, store, delay) => {
1490
- const audio = context;
1491
- return realize(plan, {
1492
- context: audio,
1493
- output: outputFor(plan.bus),
1494
- ...chain?.wet ? { wet: chain.wet } : {},
1495
- noise: () => noiseBuffer(store, audio, mix.noise.seconds),
1496
- time: audio.currentTime + delay
1497
- });
1498
- };
1499
- const deferVoice = (plan, store, voiceId, delay) => {
1500
- let built = null;
1501
- let cancelled = false;
1502
- const pending = {
1503
- get sources() {
1504
- return built?.sources ?? [];
1505
- },
1506
- stop() {
1507
- cancelled = true;
1508
- built?.stop();
1509
- }
1510
- };
1511
- startQueue.push({
1512
- run() {
1513
- if (!cancelled && context) built = build(plan, store, delay);
1514
- },
1515
- drop() {
1516
- if (voiceId === void 0) return;
1517
- voices.release(voiceId);
1518
- active.delete(voiceId);
1519
- }
1520
- });
1521
- return pending;
1522
- };
1523
- const start = (plan, store, voiceId, delay = 0) => {
1524
- const realized = startQueue ? deferVoice(plan, store, voiceId, delay) : build(plan, store, delay);
1525
- if (voiceId === void 0) return null;
1526
- voices.setLength(voiceId, plan.length + delay);
1527
- active.set(voiceId, realized);
1528
- return {
1529
- id: voiceId,
1530
- plan,
1531
- stop() {
1532
- realized.stop();
1533
- voices.release(voiceId);
1534
- active.delete(voiceId);
1535
- }
1536
- };
1537
- };
1538
- const admit = (recipe, gain, distance) => {
1539
- const decision = voices.request({ ...recipe.group ? { group: recipe.group } : {}, priority: recipe.priority ?? busPriority(recipe.bus), gain, distance, length: 0 });
1540
- if (!decision.accepted) return null;
1541
- for (const id of decision.evicted) {
1542
- active.get(id)?.stop();
1543
- active.delete(id);
1544
- }
1545
- return decision.id;
1546
- };
1547
- const playWith = (id, playOptions, random, store) => {
1548
- const recipe = registry.sound(id);
1549
- if (!recipe || busSettings.get(recipe.bus)?.muted) return null;
1550
- const distance = playOptions.distance ?? (playOptions.position ? listenerDistance(listener, playOptions.position, options.distance) : 0);
1551
- const level = falloffGain(recipe.falloff, playOptions.volume ?? 1, distance);
1552
- const silent = isSilent(recipe.falloff, level);
1553
- if (silent && !recipe.keepSilent) return null;
1554
- const audio = ready();
1555
- if (!audio) return null;
1556
- unlockContext(audio);
1557
- advance();
1558
- const voiceId = silent ? void 0 : admit(recipe, level, distance);
1559
- if (voiceId === null) return null;
1560
- if (!silent && usesNoise(recipe)) prepareNoise(store, audio, mix.noise.seconds);
1561
- const plan = planSound(recipe, inputFor(recipe, playOptions, playOptions.seed === void 0 ? random : createRng(playOptions.seed)));
1562
- return plan ? start(plan, store, voiceId, playOptions.delay) : null;
1563
- };
1564
- const randomFor = (playOptions) => {
1565
- if (playOptions.channel === void 0) return engineRandom;
1566
- const existing = named.get(playOptions.channel);
1567
- if (existing) return existing;
1568
- const stream = streamFor(seed, playOptions.channel);
1569
- named.set(playOptions.channel, stream);
1570
- return stream;
1571
- };
1572
- const planWith = (id, playOptions, random) => {
1573
- const recipe = registry.sound(id);
1574
- return recipe ? planSound(recipe, inputFor(recipe, playOptions, random)) : null;
1575
- };
1576
- const loopHost = {
1577
- ready,
1578
- output: outputFor,
1579
- prepareNoise: () => context && prepareNoise(shared, context, mix.noise.seconds),
1580
- noise: () => noiseBuffer(shared, context, mix.noise.seconds),
1581
- register: (loop) => void loops.add(loop),
1582
- unregister: (loop) => void loops.delete(loop)
1583
- };
1584
- return {
1585
- registry,
1586
- register: (bank) => registry.register(bank),
1587
- plan: (id, playOptions = {}) => planWith(id, playOptions, createRng(playOptions.seed ?? seed)),
1588
- play: (id, playOptions = {}) => playWith(id, playOptions, randomFor(playOptions), shared),
1589
- playPlan(plan, playOptions = {}) {
1590
- const audio = ready();
1591
- if (!audio) return null;
1592
- advance();
1593
- const recipe = { id: plan.id, bus: plan.bus, layers: [], sources: [], priority: plan.priority, ...plan.group ? { group: plan.group } : {} };
1594
- const voiceId = plan.silent ? void 0 : admit(recipe, plan.level, plan.distance);
1595
- if (voiceId === null) return null;
1596
- if (planUsesNoise(plan)) prepareNoise(shared, audio, mix.noise.seconds);
1597
- return start(plan, shared, voiceId, playOptions.delay);
1598
- },
1599
- loop: (id, loopOptions = {}) => createLoopHandle(registry.loop(id), loopHost, loopOptions.volume),
1600
- channel(channelOptions) {
1601
- const random = channelOptions.random ?? (channelOptions.seed === void 0 ? engineRandom : createRng(channelOptions.seed));
1602
- const store = channelOptions.ownNoise ? createNoiseStore(random) : shared;
1603
- return {
1604
- random,
1605
- play: (id, playOptions = {}) => playWith(id, playOptions, random, store),
1606
- plan: (id, playOptions = {}) => planWith(id, playOptions, createRng(playOptions.seed ?? seed))
1607
- };
1608
- },
1609
- setListener(next) {
1610
- listener = next;
1611
- },
1612
- setBus(id, setting) {
1613
- busSettings.set(id, { ...busSettings.get(id), ...setting });
1614
- if (!context) return;
1615
- ensureBus(id);
1616
- busNodes.get(id).gain.value = busGain(busSettings.get(id));
1617
- },
1618
- setMix(next) {
1619
- const previous = mix;
1620
- mix = resolveMix(next, options.profiles);
1621
- if (!context || !chain) return;
1622
- const old = chain;
1623
- chain = buildChain(context, mix, engineRandom);
1624
- if (mix.noise.seconds !== previous.noise.seconds) shared.reset();
1625
- for (const node of busNodes.values()) {
1626
- node.disconnect();
1627
- node.connect(chain.master);
1628
- }
1629
- for (const loop of loops) loop.rewire();
1630
- old.disconnect();
1631
- },
1632
- setMaster(gain) {
1633
- mix = { ...mix, master: gain };
1634
- if (chain) chain.master.gain.value = gain;
1635
- },
1636
- unlock() {
1637
- const audio = ready();
1638
- if (audio) unlockContext(audio);
1639
- },
1640
- warmUp() {
1641
- const audio = ready();
1642
- if (!audio) return false;
1643
- noiseBuffer(shared, audio, mix.noise.seconds);
1644
- return true;
1645
- },
1646
- level(bus) {
1647
- watched.add(bus);
1648
- ensureBus(bus);
1649
- const meter = meterOf(bus);
1650
- if (!meter) return 0;
1651
- meter.analyser.getFloatTimeDomainData(meter.data);
1652
- let sum = 0;
1653
- for (const sample of meter.data) sum += sample * sample;
1654
- return Math.sqrt(sum / meter.data.length);
1655
- },
1656
- stats() {
1657
- advance();
1658
- const voiceStats = voices.stats();
1659
- return {
1660
- context: context ? context.state : "none",
1661
- voices: voiceStats.active,
1662
- peak: voiceStats.peak,
1663
- dropped: voiceStats.dropped,
1664
- maxVoices: voiceStats.maxVoices,
1665
- loops: [...loops].filter((loop) => loop.created).length
1666
- };
1667
- },
1668
- dispose() {
1669
- startQueue?.clear();
1670
- for (const id of active.keys()) voices.release(id);
1671
- for (const loop of [...loops]) loop.stop();
1672
- active.clear();
1673
- void context?.close();
1674
- context = null;
1675
- chain = null;
1676
- busNodes.clear();
1677
- meters.clear();
1678
- shared.reset();
1679
- }
1680
- };
1681
- }
307
+ var BODY_FOLEY_SET_IDS = [...FOOTSTEP_SOUND_IDS, ...LAND_SOUND_IDS, ...BODY_SOUND_IDS];
1682
308
 
1683
309
  // src/webaudio/sink.ts
1684
310
  function createAudioSink(engine) {
@@ -1690,7 +316,8 @@ function createAudioSink(engine) {
1690
316
  ...request.position ? { position: request.position } : {},
1691
317
  ...request.volume === void 0 ? {} : { volume: request.volume },
1692
318
  ...request.params ? { params: request.params } : {},
1693
- ...request.seed === void 0 ? {} : { seed: request.seed }
319
+ ...request.seed === void 0 ? {} : { seed: request.seed },
320
+ ...request.delay === void 0 ? {} : { delay: request.delay }
1694
321
  });
1695
322
  if (!handle) return null;
1696
323
  playing.set(handle.id, handle);
@@ -1712,468 +339,12 @@ function createAudioSink(engine) {
1712
339
  }
1713
340
  };
1714
341
  }
1715
-
1716
- // src/bench/locale.ts
1717
- var EN = {
1718
- group: "Sound",
1719
- name: "Sound area",
1720
- probe: "Sound probe",
1721
- controls: {
1722
- sound: "Sound",
1723
- distance: "Distance",
1724
- bearing: "Bearing",
1725
- volume: "Volume",
1726
- mix: "Mixing chain",
1727
- seed: "Seed",
1728
- play: "Play",
1729
- voice_storm: "Voice storm",
1730
- loop: "Loop",
1731
- loop_level: "Loop level",
1732
- loop_pitch: "Loop pitch",
1733
- source: "Source"
1734
- },
1735
- hints: {
1736
- distance: "Metres from the listener: the level falls and the sound is muffled with it.",
1737
- bearing: "Degrees from straight ahead, positive to the right: shows the panning.",
1738
- mix: "The game chain (master only) or the art scene chain (compressor and reverb).",
1739
- seed: "The same seed gives the same sound.",
1740
- voice_storm: "Twenty of the chosen sound at once: the voice limiter drops what its group and the global cap refuse.",
1741
- loop_pitch: "Frequency of an oscillator loop, in hertz (0 leaves it where it is).",
1742
- source: "Where the sound comes from: the distance and bearing set here, or the probe standing in the area."
1743
- },
1744
- options: { none: "None", bench: "Bench settings", probe: "The probe" },
1745
- layers: { reverb: "Reverb", compressor: "Compressor", panning: "Panning", muffling: "Muffling by distance" },
1746
- metrics: {
1747
- voices: "Voices playing",
1748
- peak: "Most voices at once",
1749
- dropped: "Voices dropped",
1750
- loops: "Loops running",
1751
- context: "Audio context",
1752
- plan_length: "Chosen sound: length",
1753
- plan_level: "Chosen sound: level",
1754
- plan_pan: "Chosen sound: pan",
1755
- level_weapons: "Level: weapons",
1756
- level_voices: "Level: voices",
1757
- level_world: "Level: world",
1758
- level_ambience: "Level: ambience",
1759
- level_weather: "Level: weather",
1760
- level_ui: "Level: interface"
1761
- },
1762
- scenarios: {
1763
- panning: "A sound to the right is heard on the right, one to the left on the left, one ahead in the middle",
1764
- distance: "The farther a sound, the quieter, until it is not played",
1765
- determinism: "The same seed gives the same sound plan, another seed another one",
1766
- limiter: "The voice limiter holds a storm of voices to its group rule and its global cap"
1767
- },
1768
- mixes: { game: "Game", art_scene: "Art scene" },
1769
- sheet: { title: "Sound plan", level: "level", distance: "distance", pan: "pan", length: "seconds", silent: "too far to be heard", layer: "layer", none: "no sound chosen" }
1770
- };
1771
- var PT_BR = {
1772
- group: "Som",
1773
- name: "\xC1rea de som",
1774
- probe: "Sonda de som",
1775
- controls: {
1776
- sound: "Som",
1777
- distance: "Dist\xE2ncia",
1778
- bearing: "Dire\xE7\xE3o",
1779
- volume: "Volume",
1780
- mix: "Cadeia de mixagem",
1781
- seed: "Semente",
1782
- play: "Tocar",
1783
- voice_storm: "Tempestade de vozes",
1784
- loop: "La\xE7o",
1785
- loop_level: "N\xEDvel do la\xE7o",
1786
- loop_pitch: "Altura do la\xE7o",
1787
- source: "Origem"
1788
- },
1789
- hints: {
1790
- distance: "Metros at\xE9 o ouvinte: o n\xEDvel cai e o som abafa junto.",
1791
- bearing: "Graus a partir da frente, positivo para a direita: mostra o pan.",
1792
- mix: "A cadeia do jogo (s\xF3 o master) ou a da Cena de Arte (compressor e reverb).",
1793
- seed: "A mesma semente d\xE1 o mesmo som.",
1794
- voice_storm: "Vinte do som escolhido de uma vez: o limite de vozes derruba o que a regra do grupo e o teto global recusam.",
1795
- loop_pitch: "Frequ\xEAncia de um la\xE7o de oscilador, em hertz (0 deixa onde est\xE1).",
1796
- source: "De onde vem o som: a dist\xE2ncia e a dire\xE7\xE3o daqui, ou a sonda que est\xE1 na \xE1rea."
1797
- },
1798
- options: { none: "Nenhum", bench: "Ajustes da bancada", probe: "A sonda" },
1799
- layers: { reverb: "Reverb", compressor: "Compressor", panning: "Pan", muffling: "Abafamento pela dist\xE2ncia" },
1800
- metrics: {
1801
- voices: "Vozes tocando",
1802
- peak: "Mais vozes de uma vez",
1803
- dropped: "Vozes derrubadas",
1804
- loops: "La\xE7os ligados",
1805
- context: "Contexto de \xE1udio",
1806
- plan_length: "Som escolhido: dura\xE7\xE3o",
1807
- plan_level: "Som escolhido: n\xEDvel",
1808
- plan_pan: "Som escolhido: pan",
1809
- level_weapons: "N\xEDvel: armas",
1810
- level_voices: "N\xEDvel: vozes",
1811
- level_world: "N\xEDvel: mundo",
1812
- level_ambience: "N\xEDvel: ambiente",
1813
- level_weather: "N\xEDvel: clima",
1814
- level_ui: "N\xEDvel: interface"
1815
- },
1816
- scenarios: {
1817
- panning: "Um som \xE0 direita sai \xE0 direita, \xE0 esquerda sai \xE0 esquerda, \xE0 frente sai no meio",
1818
- distance: "Quanto mais longe, mais baixo, at\xE9 n\xE3o tocar",
1819
- determinism: "A mesma semente d\xE1 o mesmo plano de som, outra semente d\xE1 outro",
1820
- limiter: "O limite de vozes segura uma tempestade de vozes na regra do grupo e no teto global"
1821
- },
1822
- mixes: { game: "Jogo", art_scene: "Cena de Arte" },
1823
- sheet: { title: "Plano do som", level: "n\xEDvel", distance: "dist\xE2ncia", pan: "pan", length: "segundos", silent: "longe demais para ouvir", layer: "camada", none: "nenhum som escolhido" }
1824
- };
1825
- var audioBenchTextOf = (locale) => locale === "en" ? EN : PT_BR;
1826
-
1827
- // src/bench/module.ts
1828
- var BENCH_BANK = {
1829
- id: "audio_bench",
1830
- sounds: [
1831
- {
1832
- id: "bench_ping",
1833
- bus: "ui",
1834
- pan: true,
1835
- duration: 0.25,
1836
- layers: [
1837
- {
1838
- gain: 0.5,
1839
- wet: 0.3,
1840
- source: { kind: "osc", wave: "sine", frequency: { from: 880 } },
1841
- envelope: [
1842
- [0, 1e-4, "set", "fixed"],
1843
- [2e-3, 1, "exp"],
1844
- ["1d", 1e-4, "exp", "fixed"]
1845
- ]
1846
- }
1847
- ],
1848
- falloff: DISTANCE_CURVES.art_scene,
1849
- sources: ["sdk: bench probe, a 880 Hz ping through the art scene output stage"]
1850
- },
1851
- {
1852
- id: "bench_hiss",
1853
- bus: "ui",
1854
- pan: true,
1855
- duration: [0.3, 0.9],
1856
- layers: [
1857
- {
1858
- gain: 0.4,
1859
- source: { kind: "noise", offset: "random", offsetScale: 1.5, loop: true },
1860
- filters: { mode: "serial", bank: [{ type: "highpass", frequency: { set: 3e3 }, q: 0.7 }] },
1861
- envelope: [
1862
- [0, 1e-4, "set", "fixed"],
1863
- [2e-3, 1, "exp"],
1864
- ["1d", 1e-4, "exp", "fixed"]
1865
- ]
1866
- }
1867
- ],
1868
- falloff: DISTANCE_CURVES.art_scene,
1869
- sources: ["sdk: bench probe, a highpass hiss of random length"]
1870
- }
1871
- ]
1872
- };
1873
- var DISTANCES = [0, 10, 40, 60, 120, 160];
1874
- var STORM = 20;
1875
- var PROBE_ID = "audio_probe";
1876
- var PROBE_AT = [0, 0, -10];
1877
- var NONE = "none";
1878
- var DEFAULT_SOUND = "bench_ping";
1879
- var variantOf = (profile, reverb, compressor) => {
1880
- const { reverb: wet, compressor: chain, ...rest } = profile;
1881
- return { ...rest, id: `${profile.id}${reverb ? "" : "+dry"}${compressor ? "" : "+open"}`, ...wet && reverb ? { reverb: wet } : {}, ...chain && compressor ? { compressor: chain } : {} };
1882
- };
1883
- function createAudioTestModule(options = {}) {
1884
- const locale = options.locale ?? "pt-BR";
1885
- const text = audioBenchTextOf(locale);
1886
- const banks = [BENCH_BANK, ...options.banks ?? [GAME_SFX_BANK, LIBRARY_BANK]];
1887
- let engine = options.engine ?? createAudioEngine({ seed: options.seed ?? 1, banks });
1888
- for (const bank of banks) engine.register(bank);
1889
- let host = null;
1890
- let loop;
1891
- const state = {
1892
- sound: options.sound ?? DEFAULT_SOUND,
1893
- distance: 10,
1894
- bearing: 0,
1895
- volume: 1,
1896
- mix: "art_scene",
1897
- seed: options.seed ?? 1,
1898
- source: "bench",
1899
- loop: NONE,
1900
- loopLevel: 0.1,
1901
- loopPitch: 0,
1902
- reverb: true,
1903
- compressor: true,
1904
- panning: true,
1905
- muffling: true
1906
- };
1907
- const nameOf = (id) => {
1908
- const translated = options.translate?.(`sound.${id}`, locale);
1909
- if (translated) return translated;
1910
- const bank = engine.registry.bankOf(id);
1911
- return bank ? `${bank} / ${id}` : id;
1912
- };
1913
- const soundOptions = () => engine.registry.soundIds().map((id) => ({ id, name: nameOf(id) }));
1914
- const loopOptions = () => [{ id: NONE, name: text.options.none }, ...engine.registry.loopIds().map((id) => ({ id, name: nameOf(id) }))];
1915
- const listener = () => {
1916
- const camera = host?.camera;
1917
- const three = host?.THREE;
1918
- if (typeof camera?.getWorldDirection !== "function" || typeof three?.Vector3 !== "function" || !camera.position) return { position: [0, 0, 0], yaw: 0 };
1919
- const direction = camera.getWorldDirection(new three.Vector3());
1920
- return { position: [camera.position.x, camera.position.y, camera.position.z], yaw: Math.atan2(-direction.x, -direction.z) };
1921
- };
1922
- const probePosition = () => {
1923
- const scene = host?.scene;
1924
- const three = host?.THREE;
1925
- if (typeof scene?.traverse !== "function" || typeof three?.Vector3 !== "function") return void 0;
1926
- let found;
1927
- scene.traverse((object) => {
1928
- if (found || object.userData?.[PROBE_ID] !== true) return;
1929
- const at = object.getWorldPosition(new three.Vector3());
1930
- found = [at.x, at.y, at.z];
1931
- });
1932
- return found;
1933
- };
1934
- const sourceAt = (ears) => {
1935
- const probe = state.source === "probe" ? probePosition() : void 0;
1936
- if (probe) return probe;
1937
- const bearing = state.bearing * Math.PI / 180;
1938
- const forward = [-Math.sin(ears.yaw), -Math.cos(ears.yaw)];
1939
- const right = [Math.cos(ears.yaw), -Math.sin(ears.yaw)];
1940
- return [ears.position[0] + state.distance * (forward[0] * Math.cos(bearing) + right[0] * Math.sin(bearing)), ears.position[1], ears.position[2] + state.distance * (forward[1] * Math.cos(bearing) + right[1] * Math.sin(bearing))];
1941
- };
1942
- const playOptions = () => {
1943
- const ears = listener();
1944
- engine.setListener(ears);
1945
- const at = sourceAt(ears);
1946
- const distance = Math.hypot(at[0] - ears.position[0], at[2] - ears.position[2]);
1947
- return { ...state.panning ? { position: at } : { distance }, volume: state.volume, seed: state.seed };
1948
- };
1949
- const withMuffling = (id, run) => {
1950
- const falloff = engine.registry.sound(id)?.falloff;
1951
- if (state.muffling || !falloff || falloff.model !== "inverse_square" || falloff.cutoffK === void 0) return run();
1952
- engine.registry.patch(id, { falloff: { cutoffK: 0 } });
1953
- try {
1954
- return run();
1955
- } finally {
1956
- engine.registry.patch(id, { falloff: { cutoffK: falloff.cutoffK } });
1957
- }
1958
- };
1959
- const play = () => {
1960
- engine.unlock();
1961
- const chosen = state.sound;
1962
- return withMuffling(chosen, () => engine.play(chosen, playOptions()));
1963
- };
1964
- const plan = () => {
1965
- const chosen = state.sound;
1966
- return withMuffling(chosen, () => engine.plan(chosen, playOptions()));
1967
- };
1968
- const sheet = () => {
1969
- const planned = plan();
1970
- if (!planned) return `${text.sheet.title}: ${engine.registry.sound(state.sound) ? text.sheet.silent : text.sheet.none}`;
1971
- const head = `${text.sheet.title}: ${planned.id} (${planned.bus}) ${text.sheet.level} ${planned.level.toFixed(4)} ${text.sheet.distance} ${planned.distance.toFixed(1)} m${planned.pan === void 0 ? "" : ` ${text.sheet.pan} ${planned.pan.toFixed(3)}`} ${planned.length.toFixed(3)} ${text.sheet.length}`;
1972
- const layers2 = planned.layers.map((layer, index) => {
1973
- const source = layer.source.kind === "noise" ? `noise@${layer.source.offset.toFixed(4)}` : layer.source.kind === "osc" ? `${layer.source.wave}@${layer.source.frequency.value ?? layer.source.frequency.from}` : layer.source.kind;
1974
- const filters = (layer.filters?.bank ?? []).map((filter) => `${filter.type}@${(filter.frequency.value ?? filter.frequency.from ?? 0).toFixed(1)}`).join(" ");
1975
- const envelope2 = layer.envelope.map((point) => `${point.time.toFixed(3)}:${point.value.toPrecision(3)}`).join(" ");
1976
- return ` ${text.sheet.layer} ${index + 1}: ${source} at ${layer.at.toFixed(3)} for ${layer.duration.toFixed(3)} ${filters} [${envelope2}]`;
1977
- });
1978
- return [head, ...layers2].join("\n");
1979
- };
1980
- const applyMix = () => {
1981
- const profile = engine && MIX_PROFILES[state.mix];
1982
- if (profile) engine.setMix(variantOf(profile, state.reverb, state.compressor));
1983
- };
1984
- const setLoop = () => {
1985
- loop?.stop();
1986
- loop = void 0;
1987
- if (state.loop === NONE) return;
1988
- engine.unlock();
1989
- loop = engine.loop(state.loop);
1990
- loop.set({ volume: state.loopLevel, ...state.loopPitch > 0 ? { frequency: state.loopPitch } : {} });
1991
- };
1992
- const storm = () => {
1993
- engine.unlock();
1994
- const base = playOptions();
1995
- const chosen = state.sound;
1996
- for (let index = 0; index < STORM; index++) {
1997
- const spread = base.position ? { position: [base.position[0] + (index - STORM / 2), base.position[1], base.position[2]] } : {};
1998
- withMuffling(chosen, () => engine.play(chosen, { ...base, ...spread, seed: state.seed + index }));
1999
- }
2000
- };
2001
- const onChange = (change) => {
2002
- if (change.kind === "layer") {
2003
- if (change.id === "reverb" || change.id === "compressor") {
2004
- state[change.id] = change.on;
2005
- applyMix();
2006
- } else if (change.id === "panning" || change.id === "muffling") state[change.id] = change.on;
2007
- return;
2008
- }
2009
- if (change.kind === "action") {
2010
- if (change.id === "play") play();
2011
- else if (change.id === "voice_storm") storm();
2012
- return;
2013
- }
2014
- const { id, value } = change;
2015
- if (id === "sound" && typeof value === "string") state.sound = value;
2016
- else if (id === "distance" && typeof value === "string") state.distance = Number(value);
2017
- else if (id === "bearing" && typeof value === "number") state.bearing = value;
2018
- else if (id === "volume" && typeof value === "number") state.volume = value;
2019
- else if (id === "seed" && typeof value === "number") state.seed = value;
2020
- else if (id === "source" && typeof value === "string") state.source = value;
2021
- else if (id === "mix" && typeof value === "string") {
2022
- state.mix = value;
2023
- applyMix();
2024
- } else if (id === "loop" && typeof value === "string") {
2025
- state.loop = value;
2026
- setLoop();
2027
- } else if (id === "loop_level" && typeof value === "number") {
2028
- state.loopLevel = value;
2029
- loop?.set({ volume: value });
2030
- } else if (id === "loop_pitch" && typeof value === "number") {
2031
- state.loopPitch = value;
2032
- if (value > 0) loop?.set({ frequency: value });
2033
- }
2034
- };
2035
- const items = () => {
2036
- const probe = options.probe;
2037
- if (!probe) return [];
2038
- return [
2039
- {
2040
- id: PROBE_ID,
2041
- name: text.probe,
2042
- movable: false,
2043
- build: (THREE) => {
2044
- const root = probe(THREE);
2045
- root.userData = root.userData ?? {};
2046
- root.userData[PROBE_ID] = true;
2047
- return root;
2048
- }
2049
- }
2050
- ];
2051
- };
2052
- const controls = () => [
2053
- { kind: "choice", id: "sound", name: text.controls.sound, options: soundOptions(), default: state.sound },
2054
- { kind: "choice", id: "source", name: text.controls.source, hint: text.hints.source, options: [{ id: "bench", name: text.options.bench }, { id: "probe", name: text.options.probe }], default: state.source },
2055
- { kind: "choice", id: "distance", name: text.controls.distance, hint: text.hints.distance, options: DISTANCES.map((meters) => ({ id: String(meters), name: `${meters} m` })), default: String(state.distance) },
2056
- { kind: "number", id: "bearing", name: text.controls.bearing, hint: text.hints.bearing, unit: "\xB0", min: -180, max: 180, step: 15, default: state.bearing },
2057
- { kind: "number", id: "volume", name: text.controls.volume, min: 0, max: 1, step: 0.05, default: state.volume },
2058
- { kind: "choice", id: "mix", name: text.controls.mix, hint: text.hints.mix, options: Object.keys(MIX_PROFILES).map((id) => ({ id, name: text.mixes[id] ?? id })), default: state.mix },
2059
- { kind: "number", id: "seed", name: text.controls.seed, hint: text.hints.seed, min: 1, max: 9999, step: 1, default: state.seed },
2060
- { kind: "action", id: "play", name: text.controls.play },
2061
- { kind: "action", id: "voice_storm", name: text.controls.voice_storm, hint: text.hints.voice_storm },
2062
- { kind: "choice", id: "loop", name: text.controls.loop, options: loopOptions(), default: state.loop },
2063
- { kind: "number", id: "loop_level", name: text.controls.loop_level, min: 0, max: 0.6, step: 0.01, default: state.loopLevel },
2064
- { kind: "number", id: "loop_pitch", name: text.controls.loop_pitch, hint: text.hints.loop_pitch, unit: "Hz", min: 0, max: 400, step: 5, default: state.loopPitch }
2065
- ];
2066
- const layers = () => ["reverb", "compressor", "panning", "muffling"].map((id) => ({ id, name: text.layers[id] }));
2067
- const metrics = () => [
2068
- { id: "voices", name: text.metrics.voices, digits: 0, read: () => engine.stats().voices },
2069
- { id: "peak", name: text.metrics.peak, digits: 0, max: engine.stats().maxVoices > 0 && Number.isFinite(engine.stats().maxVoices) ? engine.stats().maxVoices : void 0, read: () => engine.stats().peak },
2070
- { id: "dropped", name: text.metrics.dropped, digits: 0, read: () => engine.stats().dropped },
2071
- { id: "loops", name: text.metrics.loops, digits: 0, read: () => engine.stats().loops },
2072
- { id: "context", name: text.metrics.context, read: () => engine.stats().context },
2073
- { id: "plan_length", name: text.metrics.plan_length, unit: "s", digits: 2, read: () => plan()?.length ?? 0 },
2074
- { id: "plan_level", name: text.metrics.plan_level, digits: 3, read: () => plan()?.level ?? 0 },
2075
- { id: "plan_pan", name: text.metrics.plan_pan, digits: 2, read: () => plan()?.pan ?? 0 },
2076
- ...BUS_IDS.map((bus) => ({ id: `level_${bus}`, name: text.metrics[`level_${bus}`], digits: 3, read: () => engine.level(bus) }))
2077
- ];
2078
- const scenarios = () => [
2079
- {
2080
- id: "panning",
2081
- name: text.scenarios.panning,
2082
- run: ({ expect }) => {
2083
- engine.setListener({ position: [0, 0, 0], yaw: 0 });
2084
- const pan = (at) => engine.plan("bench_ping", { position: at })?.pan ?? Number.NaN;
2085
- expect(pan([10, 0, 0]) > 0.3, "a sound to the right pans right", { pan: pan([10, 0, 0]) });
2086
- expect(pan([-10, 0, 0]) < -0.3, "a sound to the left pans left", { pan: pan([-10, 0, 0]) });
2087
- expect(Math.abs(pan([0, 0, -10])) < 0.05, "a sound ahead stays in the middle", { pan: pan([0, 0, -10]) });
2088
- engine.setListener({ position: [0, 0, 0], yaw: Math.PI / 2 });
2089
- expect(pan([0, 0, -10]) > 0.3, "turning the listener left puts what was ahead on the right", { pan: pan([0, 0, -10]) });
2090
- engine.setListener(listener());
2091
- }
2092
- },
2093
- {
2094
- id: "distance",
2095
- name: text.scenarios.distance,
2096
- run: ({ expect }) => {
2097
- const levels = [0, 10, 40, 100].map((meters) => engine.plan("bench_ping", { distance: meters })?.level ?? Number.NaN);
2098
- expect(levels.every((level, index) => index === 0 || level < levels[index - 1]), "the level falls with the distance", { levels });
2099
- const far = engine.registry.sound("sfx_pistol") ? engine.plan("sfx_pistol", { distance: 400 }) : null;
2100
- expect(far === null, "a game noise class is not played too far to be heard");
2101
- }
2102
- },
2103
- {
2104
- id: "determinism",
2105
- name: text.scenarios.determinism,
2106
- run: ({ expect }) => {
2107
- const a = engine.plan("bench_hiss", { seed: 5 });
2108
- const b = engine.plan("bench_hiss", { seed: 5 });
2109
- const c = engine.plan("bench_hiss", { seed: 6 });
2110
- expect(JSON.stringify(a) === JSON.stringify(b), "the same seed gives the same plan");
2111
- expect(JSON.stringify(a) !== JSON.stringify(c), "another seed gives another plan");
2112
- }
2113
- },
2114
- {
2115
- id: "limiter",
2116
- name: text.scenarios.limiter,
2117
- run: ({ expect }) => {
2118
- const voices = createVoiceManager({ maxVoices: 8 });
2119
- const request = (group) => voices.request({ ...group ? { group } : {}, priority: 3, gain: 0.5, distance: 5, length: 1 });
2120
- const grouped = Array.from({ length: STORM }, () => request("undead_voice")).filter((decision) => decision.accepted).length;
2121
- expect(grouped === 1, "the group cooldown lets one voice of a storm through", { accepted: grouped });
2122
- const plain = Array.from({ length: STORM }, () => request()).filter((decision) => decision.accepted).length;
2123
- expect(plain <= 8, "the global cap holds the rest", { accepted: plain });
2124
- }
2125
- }
2126
- ];
2127
- const area = () => ({
2128
- placements: options.probe ? [{ item: PROBE_ID, position: PROBE_AT, asleep: true }] : [],
2129
- view: { eye: [0, 1.7, 3], target: [0, 1.4, -10] }
2130
- });
2131
- const module = {
2132
- id: "audio_area",
2133
- name: text.name,
2134
- ground: { surface: "concrete_slab" },
2135
- group: text.group,
2136
- get engine() {
2137
- return engine;
2138
- },
2139
- items: () => items(),
2140
- area: () => area(),
2141
- controls: () => controls(),
2142
- layers: () => layers(),
2143
- metrics: () => metrics(),
2144
- scenarios: () => scenarios(),
2145
- sheet,
2146
- play,
2147
- attach(lent) {
2148
- host = lent;
2149
- const shared = lent.audio?.engine;
2150
- if (shared && shared !== engine) {
2151
- for (const bank of banks) shared.register(bank);
2152
- engine = shared;
2153
- }
2154
- for (const bus of BUS_IDS) {
2155
- engine.setBus(bus, {});
2156
- engine.level(bus);
2157
- }
2158
- lent.bench?.patch("sound", { options: soundOptions() });
2159
- lent.bench?.patch("loop", { options: loopOptions() });
2160
- const off = lent.bench?.onChange(onChange);
2161
- const offFrame = lent.onFrame(() => {
2162
- if (!shared) engine.setListener(listener());
2163
- });
2164
- return () => {
2165
- off?.();
2166
- offFrame();
2167
- loop?.stop();
2168
- loop = void 0;
2169
- host = null;
2170
- };
2171
- }
2172
- };
2173
- return module;
2174
- }
2175
342
  export {
2176
343
  BENCH_BANK,
344
+ BODY_FALLOFF,
345
+ BODY_FOLEY_BANK,
346
+ BODY_FOLEY_SET_IDS,
347
+ BODY_FOLEY_TAKES,
2177
348
  BUILD_ORDERS,
2178
349
  BUS_IDS,
2179
350
  BUS_PRIORITY,
@@ -2181,6 +352,7 @@ export {
2181
352
  DEFAULT_MAX_VOICES,
2182
353
  DEFAULT_MIX,
2183
354
  DISTANCE_CURVES,
355
+ FOOTSTEP_FALLOFF,
2184
356
  GAME_SFX_BANK,
2185
357
  GAME_SFX_CLASSES,
2186
358
  GAME_SFX_FALLBACK,
@@ -2203,6 +375,7 @@ export {
2203
375
  createAudioTestModule,
2204
376
  createLoopState,
2205
377
  createRng,
378
+ createSetPicker,
2206
379
  createSoundRegistry,
2207
380
  createVoiceManager,
2208
381
  cutoffDivisor,
@@ -2218,6 +391,7 @@ export {
2218
391
  planSound,
2219
392
  realize,
2220
393
  resolveMix,
394
+ retunePlan,
2221
395
  sceneMany,
2222
396
  sceneNoise,
2223
397
  sceneRecipe,