@adatechnology/game-audio 0.3.6 → 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.
@@ -22,6 +22,10 @@ var GameAudio = (() => {
22
22
  var index_exports = {};
23
23
  __export(index_exports, {
24
24
  BENCH_BANK: () => BENCH_BANK,
25
+ BODY_FALLOFF: () => BODY_FALLOFF,
26
+ BODY_FOLEY_BANK: () => BODY_FOLEY_BANK,
27
+ BODY_FOLEY_SET_IDS: () => BODY_FOLEY_SET_IDS,
28
+ BODY_FOLEY_TAKES: () => BODY_FOLEY_TAKES,
25
29
  BUILD_ORDERS: () => BUILD_ORDERS,
26
30
  BUS_IDS: () => BUS_IDS,
27
31
  BUS_PRIORITY: () => BUS_PRIORITY,
@@ -29,6 +33,7 @@ var GameAudio = (() => {
29
33
  DEFAULT_MAX_VOICES: () => DEFAULT_MAX_VOICES,
30
34
  DEFAULT_MIX: () => DEFAULT_MIX,
31
35
  DISTANCE_CURVES: () => DISTANCE_CURVES,
36
+ FOOTSTEP_FALLOFF: () => FOOTSTEP_FALLOFF,
32
37
  GAME_SFX_BANK: () => GAME_SFX_BANK,
33
38
  GAME_SFX_CLASSES: () => GAME_SFX_CLASSES,
34
39
  GAME_SFX_FALLBACK: () => GAME_SFX_FALLBACK,
@@ -51,6 +56,7 @@ var GameAudio = (() => {
51
56
  createAudioTestModule: () => createAudioTestModule,
52
57
  createLoopState: () => createLoopState,
53
58
  createRng: () => createRng,
59
+ createSetPicker: () => createSetPicker,
54
60
  createSoundRegistry: () => createSoundRegistry,
55
61
  createVoiceManager: () => createVoiceManager,
56
62
  cutoffDivisor: () => cutoffDivisor,
@@ -66,6 +72,7 @@ var GameAudio = (() => {
66
72
  planSound: () => planSound,
67
73
  realize: () => realize,
68
74
  resolveMix: () => resolveMix,
75
+ retunePlan: () => retunePlan,
69
76
  sceneMany: () => sceneMany,
70
77
  sceneNoise: () => sceneNoise,
71
78
  sceneRecipe: () => sceneRecipe,
@@ -197,7 +204,7 @@ var GameAudio = (() => {
197
204
  if (!group) return void 0;
198
205
  if (group.maxDistance !== void 0 && !((request.distance ?? 0) < group.maxDistance)) return "distance";
199
206
  if (group.cooldown !== void 0 && (budgets.get(request.group) ?? 0) > 0) return "cooldown";
200
- const members = voices.filter((voice) => voice.group === request.group);
207
+ const members = voices.filter((voice2) => voice2.group === request.group);
201
208
  if (group.maxConcurrent === void 0 || members.length < group.maxConcurrent) return void 0;
202
209
  if (!group.steal) return "group_full";
203
210
  evicted.push(members[0].id);
@@ -209,9 +216,9 @@ var GameAudio = (() => {
209
216
  const refusal = groupCheck(request, evicted);
210
217
  if (refusal) return refuse(refusal);
211
218
  const score = request.priority * request.gain;
212
- const remaining = voices.filter((voice) => !evicted.includes(voice.id));
219
+ const remaining = voices.filter((voice2) => !evicted.includes(voice2.id));
213
220
  if (remaining.length >= maxVoices) {
214
- const lowest = remaining.reduce((low, voice) => voice.score < low.score ? voice : low);
221
+ const lowest = remaining.reduce((low, voice2) => voice2.score < low.score ? voice2 : low);
215
222
  if (score <= lowest.score) return refuse("global_full");
216
223
  evicted.push(lowest.id);
217
224
  dropped += 1;
@@ -219,25 +226,25 @@ var GameAudio = (() => {
219
226
  const group = request.group === void 0 ? void 0 : groups.get(request.group);
220
227
  if (group?.cooldown !== void 0) budgets.set(request.group, group.cooldown);
221
228
  const id = nextId++;
222
- voices = [...voices.filter((voice) => !evicted.includes(voice.id)), { id, ...request.group === void 0 ? {} : { group: request.group }, score, remaining: request.length }];
229
+ voices = [...voices.filter((voice2) => !evicted.includes(voice2.id)), { id, ...request.group === void 0 ? {} : { group: request.group }, score, remaining: request.length }];
223
230
  peak = Math.max(peak, voices.length);
224
231
  return { accepted: true, id, evicted };
225
232
  },
226
233
  release(id) {
227
- voices = voices.filter((voice) => voice.id !== id);
234
+ voices = voices.filter((voice2) => voice2.id !== id);
228
235
  },
229
236
  setLength(id, seconds) {
230
- const voice = voices.find((entry) => entry.id === id);
231
- if (voice) voice.remaining = seconds;
237
+ const voice2 = voices.find((entry) => entry.id === id);
238
+ if (voice2) voice2.remaining = seconds;
232
239
  },
233
240
  advance(dt) {
234
241
  for (const [group, budget] of budgets) budgets.set(group, budget - dt);
235
242
  const ended = [];
236
- for (const voice of voices) {
237
- voice.remaining -= dt;
238
- if (voice.remaining <= 0) ended.push(voice.id);
243
+ for (const voice2 of voices) {
244
+ voice2.remaining -= dt;
245
+ if (voice2.remaining <= 0) ended.push(voice2.id);
239
246
  }
240
- if (ended.length > 0) voices = voices.filter((voice) => voice.remaining > 0);
247
+ if (ended.length > 0) voices = voices.filter((voice2) => voice2.remaining > 0);
241
248
  return ended;
242
249
  },
243
250
  setGroup(id, group) {
@@ -345,14 +352,14 @@ var GameAudio = (() => {
345
352
  const source = layer.source;
346
353
  if (source.kind === "noise") return { kind: "noise", offset, loop: source.loop === true };
347
354
  if (source.kind === "osc") return { kind: "osc", wave: source.wave, frequency: finishAutomation(pending.automation ?? {}, duration) };
348
- const voice = pending.voice;
355
+ const voice2 = pending.voice;
349
356
  return {
350
357
  kind: "formant_voice",
351
358
  wave: source.wave,
352
- frequency: finishAutomation(voice.frequency, duration),
353
- vibrato: { rate: voice.vibratoRate, depth: startFrequency(voice.frequency) * source.vibrato.depth },
354
- formants: voice.formants,
355
- breath: { ...voice.breath, offset },
359
+ frequency: finishAutomation(voice2.frequency, duration),
360
+ vibrato: { rate: voice2.vibratoRate, depth: startFrequency(voice2.frequency) * source.vibrato.depth },
361
+ formants: voice2.formants,
362
+ breath: { ...voice2.breath, offset },
356
363
  mix: source.mix
357
364
  };
358
365
  }
@@ -408,51 +415,51 @@ var GameAudio = (() => {
408
415
  return { ...layer, filters: { ...layer.filters, bank } };
409
416
  }
410
417
  var layerEnd = (layer) => layer.at + layer.duration + layer.tail;
411
- function planSound(recipe, input = {}) {
418
+ function planSound(recipe2, input = {}) {
412
419
  const listener = input.listener ?? DEFAULT_LISTENER;
413
420
  const distance = input.distance ?? (input.position ? listenerDistance(listener, input.position, input.distanceMode) : 0);
414
421
  const occlusion = Math.min(1, Math.max(0, input.occlusion ?? 0));
415
- const order = recipe.order ?? "layered";
416
- const falloffLevel = falloffGain(recipe.falloff, input.volume ?? 1, distance);
422
+ const order = recipe2.order ?? "layered";
423
+ const falloffLevel = falloffGain(recipe2.falloff, input.volume ?? 1, distance);
417
424
  const level = occlusion > 0 ? falloffLevel * (1 - OCCLUSION_GAIN * occlusion) : falloffLevel;
418
- const silent = isSilent(recipe.falloff, falloffLevel);
425
+ const silent = isSilent(recipe2.falloff, falloffLevel);
419
426
  const base = {
420
- id: recipe.id,
421
- bus: recipe.bus,
427
+ id: recipe2.id,
428
+ bus: recipe2.bus,
422
429
  order,
423
- ...recipe.group === void 0 ? {} : { group: recipe.group },
424
- priority: recipe.priority ?? busPriority(recipe.bus),
430
+ ...recipe2.group === void 0 ? {} : { group: recipe2.group },
431
+ priority: recipe2.priority ?? busPriority(recipe2.bus),
425
432
  level,
426
433
  distance,
427
- ...recipe.pan ? { pan: input.position ? panFor(listener, input.position) : 0 } : {}
434
+ ...recipe2.pan ? { pan: input.position ? panFor(listener, input.position) : 0 } : {}
428
435
  };
429
- if (silent) return recipe.keepSilent ? { ...base, silent: true, length: 0, layers: [] } : null;
436
+ if (silent) return recipe2.keepSilent ? { ...base, silent: true, length: 0, layers: [] } : null;
430
437
  const scope = { random: input.random ?? createRng(1), params: { ...input.params } };
431
- for (const [name, range] of Object.entries(recipe.vars ?? {})) if (scope.params[name] === void 0) scope.params[name] = draw(range, scope);
438
+ for (const [name, range] of Object.entries(recipe2.vars ?? {})) if (scope.params[name] === void 0) scope.params[name] = draw(range, scope);
432
439
  const defaults = {
433
- ...recipe.duration === void 0 ? {} : { duration: draw(recipe.duration, scope) },
434
- ...recipe.tail === void 0 ? {} : { tail: recipe.tail },
440
+ ...recipe2.duration === void 0 ? {} : { duration: draw(recipe2.duration, scope) },
441
+ ...recipe2.tail === void 0 ? {} : { tail: recipe2.tail },
435
442
  noiseSeconds: input.noiseSeconds ?? DEFAULT_NOISE_SECONDS
436
443
  };
437
- const divisor = cutoffDivisor(recipe.falloff, distance) / (1 - OCCLUSION_CUTOFF * occlusion);
438
- const layers = recipe.layers.flatMap((layer) => planLayer(layer, scope, defaults, order !== "layered", level)).map((layer) => muffle(layer, divisor));
444
+ const divisor = cutoffDivisor(recipe2.falloff, distance) / (1 - OCCLUSION_CUTOFF * occlusion);
445
+ const layers = recipe2.layers.flatMap((layer) => planLayer(layer, scope, defaults, order !== "layered", level)).map((layer) => muffle(layer, divisor));
439
446
  return { ...base, silent: false, length: Math.max(0, ...layers.map(layerEnd)), layers };
440
447
  }
441
448
 
442
449
  // src/loops.ts
443
450
  var LOOP_START_ABOVE = 2e-3;
444
451
  var LOOP_SMOOTHING = { volume: 0.15, frequency: 0.08 };
445
- function createLoopState(recipe, open = false) {
446
- const startAbove = recipe.startAbove ?? LOOP_START_ABOVE;
452
+ function createLoopState(recipe2, open = false) {
453
+ const startAbove = recipe2.startAbove ?? LOOP_START_ABOVE;
447
454
  let created = open;
448
- let volume = recipe.volume ?? 0;
455
+ let volume = recipe2.volume ?? 0;
449
456
  return {
450
457
  set(state) {
451
458
  volume = state.volume ?? volume;
452
459
  const create = !created;
453
460
  if (create && !(volume >= startAbove)) return null;
454
461
  created = true;
455
- const frequency = recipe.source.kind === "osc" && state.frequency ? state.frequency : void 0;
462
+ const frequency = recipe2.source.kind === "osc" && state.frequency ? state.frequency : void 0;
456
463
  return { create, volume, ...frequency === void 0 ? {} : { frequency } };
457
464
  },
458
465
  get created() {
@@ -520,6 +527,7 @@ var GameAudio = (() => {
520
527
  function createSoundRegistry(banks = []) {
521
528
  const sounds = /* @__PURE__ */ new Map();
522
529
  const loops = /* @__PURE__ */ new Map();
530
+ const sets = /* @__PURE__ */ new Map();
523
531
  const owners = /* @__PURE__ */ new Map();
524
532
  const registry = {
525
533
  register(bank) {
@@ -531,6 +539,11 @@ var GameAudio = (() => {
531
539
  loops.set(loop.id, loop);
532
540
  owners.set(loop.id, bank.id);
533
541
  }
542
+ for (const set of bank.sets ?? []) {
543
+ if (set.variants.length === 0) throw new Error(`sound set ${set.id} has no variant`);
544
+ sets.set(set.id, set);
545
+ owners.set(set.id, bank.id);
546
+ }
534
547
  },
535
548
  patch(id, patch) {
536
549
  const sound = sounds.get(id);
@@ -541,11 +554,14 @@ var GameAudio = (() => {
541
554
  remove(id) {
542
555
  sounds.delete(id);
543
556
  loops.delete(id);
557
+ sets.delete(id);
544
558
  owners.delete(id);
545
559
  },
546
560
  sound: (id) => sounds.get(id),
547
561
  loop: (id) => loops.get(id),
562
+ set: (id) => sets.get(id),
548
563
  soundIds: () => [...sounds.keys()],
564
+ setIds: () => [...sets.keys()],
549
565
  loopIds: () => [...loops.keys()],
550
566
  bankOf: (id) => owners.get(id)
551
567
  };
@@ -780,6 +796,293 @@ var GameAudio = (() => {
780
796
  var LIBRARY_LOOPS = [engine_saw_default, rotor_blades_default, crowd_rumble_default];
781
797
  var LIBRARY_BANK = { id: "library", sounds: LIBRARY_SOUNDS, loops: LIBRARY_LOOPS };
782
798
 
799
+ // ../game-contracts/dist/index.js
800
+ var DEFAULT_COST_ALLOCATION_BYTES = 1024 * 1024;
801
+ var DEFERRED_QUEUE_ID = {
802
+ DESTRUCTION_EVENTS: "destruction.events",
803
+ BREACH_PAINT: "floorplan.breach-paint",
804
+ AUDIO_VOICES: "audio.voices",
805
+ VEHICLE_CREASE: "vehicles.crease"
806
+ };
807
+ var DEFERRED_PRIORITY = {
808
+ [DEFERRED_QUEUE_ID.DESTRUCTION_EVENTS]: { priority: 300, phase: "present" },
809
+ [DEFERRED_QUEUE_ID.BREACH_PAINT]: { priority: 250, phase: "present" },
810
+ [DEFERRED_QUEUE_ID.AUDIO_VOICES]: { priority: 200, phase: "idle", reserveMs: 1 },
811
+ [DEFERRED_QUEUE_ID.VEHICLE_CREASE]: { priority: 100, phase: "idle" }
812
+ };
813
+ var SURFACE_IDS = ["concrete", "asphalt", "wood", "tile", "dirt", "grass", "gravel", "sand", "metal", "water", "glass", "carpet"];
814
+ var GAIT_IDS = ["walk", "run"];
815
+ var LANDING_IDS = ["light", "heavy"];
816
+ var BODY_SOUND_IDS = [
817
+ "jump",
818
+ "cloth_rustle",
819
+ "body_fall",
820
+ "fall_damage",
821
+ "player_hurt",
822
+ "player_breath_run",
823
+ "player_death",
824
+ "bite",
825
+ "grab",
826
+ "punch_swing",
827
+ "punch_hit_body",
828
+ "punch_hit_face",
829
+ "punch_hit_object",
830
+ "kick_swing",
831
+ "kick_hit",
832
+ "kick_hit_object",
833
+ "shove_two_hands",
834
+ "knife_swing",
835
+ "knife_stab",
836
+ "knife_slash",
837
+ "knife_hit_object",
838
+ "blunt_swing",
839
+ "blunt_hit",
840
+ "blunt_hit_object",
841
+ "axe_swing",
842
+ "axe_chop",
843
+ "axe_hit_object"
844
+ ];
845
+ var footstepSoundId = (surface, gait) => `footstep_${surface}_${gait}`;
846
+ var FOOTSTEP_SOUND_IDS = SURFACE_IDS.flatMap((surface) => GAIT_IDS.map((gait) => footstepSoundId(surface, gait)));
847
+ var LAND_SOUND_IDS = SURFACE_IDS.flatMap((surface) => LANDING_IDS.map((landing2) => `land_${surface}_${landing2}`));
848
+ var BODY_FOLEY_IDS = [...FOOTSTEP_SOUND_IDS, ...LAND_SOUND_IDS, ...BODY_SOUND_IDS];
849
+ var SWING_LEAD_SECONDS = { punch: 0.07, kick: 0.09, shove: 0, blade: 0.05, blunt: 0.1, chop: 0.11 };
850
+
851
+ // src/recipes/body-foley.ts
852
+ var BODY_FOLEY_TAKES = 4;
853
+ var TAKES = [
854
+ { f: 1, t: 1, v: 1 },
855
+ { f: 1.08, t: 0.9, v: 0.92 },
856
+ { f: 0.93, t: 1.12, v: 1.05 },
857
+ { f: 1.03, t: 1.05, v: 0.88 }
858
+ ];
859
+ var FOOTSTEP_FALLOFF = { model: "inverse_square", k: 0.03, cutoffK: 0.03, minGain: 4e-3 };
860
+ var BODY_FALLOFF = { model: "inverse_square", k: 0.012, cutoffK: 0.015, minGain: 4e-3 };
861
+ var SURFACES = {
862
+ 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 } },
863
+ 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 } },
864
+ 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 }] },
865
+ 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 }] },
866
+ 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 } },
867
+ 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 } },
868
+ 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 } },
869
+ 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 } },
870
+ 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 }] },
871
+ 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 } },
872
+ 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 }] },
873
+ carpet: { heel: { f: 70, d: 0.05, vol: 0.32 }, roll: { type: "lowpass", f: 420, q: 0.7, d: 0.07, vol: 0.3 } }
874
+ };
875
+ var GAITS = {
876
+ walk: { vol: 1, bright: 1, toe: 0.075, short: 1 },
877
+ run: { vol: 1.35, bright: 1.25, toe: 0.035, short: 0.75 }
878
+ };
879
+ var grainLayers = (g, take, start, scale) => Array.from(
880
+ { length: g.count },
881
+ (_, 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 })
882
+ );
883
+ 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 }));
884
+ function footLayers(surface, take, gait, weight = 1) {
885
+ const s = SURFACES[surface];
886
+ const toe = gait.toe * take.t;
887
+ return [
888
+ 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 }),
889
+ sceneNoise({ type: "lowpass", f: 500 * gait.bright, d: 0.02 * take.t, vol: 0.18 * gait.vol * take.v * weight ** 0.5 }),
890
+ 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 }),
891
+ ...s.grains ? grainLayers(s.grains, take, toe * 0.5, gait.vol * weight ** 0.5) : [],
892
+ ...s.ring ? ringLayers(s.ring, take, gait.vol * weight ** 0.5) : [],
893
+ ...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 })] : []
894
+ ];
895
+ }
896
+ var quieter = (level, layers) => layers.map((layer) => {
897
+ const gain = layer.gain ?? 1;
898
+ if (typeof gain === "number") return { ...layer, gain: gain * level };
899
+ if (Array.isArray(gain)) return { ...layer, gain: [gain[0] * level, gain[1] * level] };
900
+ return layer;
901
+ });
902
+ var recipe = (id, layers, falloff, bus = "world") => ({ id, bus, pan: true, layers, falloff, sources: ["sdk"] });
903
+ var takeId = (id, index) => `${id}_${String(index + 1).padStart(2, "0")}`;
904
+ function takes(id, layersOf, falloff, rate = [0.96, 1.04], bus) {
905
+ const sounds = TAKES.map((take, index) => recipe(takeId(id, index), layersOf(take, index), falloff, bus));
906
+ return { sounds, set: { id, variants: sounds.map((sound) => sound.id), rate } };
907
+ }
908
+ var footstep = (surface, gait) => takes(footstepSoundId(surface, gait), (take) => footLayers(surface, take, GAITS[gait]), FOOTSTEP_FALLOFF);
909
+ var LANDINGS = { light: { weight: 1.4, cloth: 0.12 }, heavy: { weight: 2.4, cloth: 0.22 } };
910
+ var LANDING_LEVEL = 0.65;
911
+ var landing = (surface, landing2) => takes(
912
+ `land_${surface}_${landing2}`,
913
+ (take) => quieter(LANDING_LEVEL, [
914
+ // both feet, a hair apart, then the knees taking the weight (a low body thump) and the clothes settling
915
+ ...footLayers(surface, take, GAITS.run, LANDINGS[landing2].weight),
916
+ ...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 })),
917
+ 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 }),
918
+ sceneNoise({ type: "bandpass", f: [1800, 2600], q: 0.8, d: 0.18, vol: LANDINGS[landing2].cloth * take.v, delay: 0.04 })
919
+ ]),
920
+ FOOTSTEP_FALLOFF
921
+ );
922
+ var whoosh = (blow, from, to, d, vol, q = 1.6) => (take) => [
923
+ // 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
924
+ 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 }),
925
+ sceneNoise({ type: "lowpass", f: 600 * take.f, a: SWING_LEAD_SECONDS[blow], d: d * take.t * 0.8, vol: vol * 0.35 * take.v })
926
+ ];
927
+ var fleshHit = (o) => (take) => [
928
+ sceneNoise({ type: "bandpass", f: o.slapF * take.f, q: 1.2, d: 0.03 * take.t, vol: o.slap * take.v }),
929
+ sceneNoise({ type: "highpass", f: 3500 * take.f, d: 0.012, vol: o.slap * 0.4 * take.v }),
930
+ sceneTone({ f: o.thudF * take.f, f2: o.thudF * 0.5, d: o.thudD * take.t, vol: o.thud * take.v }),
931
+ sceneNoise({ type: "lowpass", f: 600 * take.f, d: o.thudD * 1.2 * take.t, vol: o.thud * 0.5 * take.v }),
932
+ ...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 })] : [],
933
+ ...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 })] : []
934
+ ];
935
+ var knock = (o) => (take) => [
936
+ sceneNoise({ type: "bandpass", f: o.clickF * take.f, q: 1.5, d: 0.018, vol: o.click * take.v }),
937
+ ...o.tones.map(([f, d, vol]) => sceneTone({ f: f * take.f, d: d * take.t, vol: vol * take.v }))
938
+ ];
939
+ var voice = (o) => (take) => [
940
+ 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 })
941
+ ];
942
+ var BODY = {
943
+ jump: (take) => [
944
+ sceneNoise({ type: "bandpass", f: 900 * take.f, f2: 1700 * take.f, q: 1, a: 0.03, d: 0.12, vol: 0.18 * take.v }),
945
+ sceneNoise({ type: "bandpass", f: 2200 * take.f, q: 0.8, d: 0.04, vol: 0.12 * take.v }),
946
+ sceneTone({ f: 85 * take.f, f2: 60, d: 0.04, vol: 0.2 * take.v })
947
+ ],
948
+ cloth_rustle: (take) => [
949
+ { ...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 }] }
950
+ ],
951
+ body_fall: (take) => [
952
+ sceneTone({ f: 58 * take.f, f2: 35, d: 0.28 * take.t, vol: 0.9 * take.v }),
953
+ sceneNoise({ type: "lowpass", f: 480 * take.f, d: 0.3 * take.t, vol: 0.55 * take.v }),
954
+ sceneNoise({ type: "bandpass", f: 1300 * take.f, q: 1, d: 0.05, vol: 0.3 * take.v }),
955
+ // the limbs a moment later, and the clothes
956
+ sceneTone({ f: 75 * take.f, f2: 45, d: 0.12, vol: 0.4 * take.v, delay: 0.13 * take.t }),
957
+ sceneNoise({ type: "bandpass", f: 2200, q: 0.8, d: 0.2, vol: 0.12 * take.v, delay: 0.05 })
958
+ ],
959
+ fall_damage: (take) => [
960
+ ...BODY.body_fall(take),
961
+ sceneNoise({ type: "highpass", f: 2800, d: 0.02, vol: 0.35 * take.v, delay: 0.01 }),
962
+ ...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 }))
963
+ ],
964
+ player_hurt: voice({ f0: 150, f1: 105, d: 0.26, fA: 620, fB: 1100, noise: 0.3, vol: 0.35 }),
965
+ player_breath_run: (take) => [
966
+ sceneNoise({ type: "bandpass", f: 1200 * take.f, q: 1, a: 0.08, d: 0.22 * take.t, vol: 0.25 * take.v }),
967
+ 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 })
968
+ ],
969
+ player_death: voice({ f0: 135, f1: 68, d: 1, fA: 560, fB: 980, noise: 0.35, vol: 0.4 }),
970
+ bite: (take) => [
971
+ sceneNoise({ type: "highpass", f: 2600, d: 0.015, vol: 0.4 * take.v }),
972
+ 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 }),
973
+ 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 }),
974
+ sceneTone({ f: 120 * take.f, f2: 70, d: 0.06, vol: 0.3 * take.v })
975
+ ],
976
+ grab: (take) => [
977
+ sceneNoise({ type: "bandpass", f: 1900 * take.f, q: 0.9, a: 4e-3, d: 0.13 * take.t, vol: 0.32 * take.v }),
978
+ sceneTone({ f: 95 * take.f, f2: 60, d: 0.07, vol: 0.3 * take.v }),
979
+ sceneNoise({ type: "lowpass", f: 700, d: 0.08, vol: 0.2 * take.v })
980
+ ],
981
+ punch_swing: whoosh("punch", 450, 1500, 0.16, 0.9),
982
+ punch_hit_body: fleshHit({ slap: 0.55, slapF: 1500, thud: 0.6, thudF: 115, thudD: 0.09 }),
983
+ punch_hit_face: fleshHit({ slap: 0.75, slapF: 2300, thud: 0.45, thudF: 150, thudD: 0.07, crack: 0.25 }),
984
+ punch_hit_object: knock({ clickF: 1800, click: 0.3, tones: [[210, 0.04, 0.25]] }),
985
+ kick_swing: whoosh("kick", 280, 950, 0.22, 1, 1.3),
986
+ kick_hit: fleshHit({ slap: 0.45, slapF: 1100, thud: 0.85, thudF: 82, thudD: 0.14 }),
987
+ kick_hit_object: knock({ clickF: 1e3, click: 0.3, tones: [[92, 0.08, 0.5]] }),
988
+ shove_two_hands: (take) => [
989
+ sceneNoise({ type: "bandpass", f: 950 * take.f, q: 0.9, a: 0.01, d: 0.13 * take.t, vol: 0.38 * take.v }),
990
+ sceneTone({ f: 100 * take.f, f2: 60, d: 0.1 * take.t, vol: 0.45 * take.v }),
991
+ sceneNoise({ type: "bandpass", f: 2300, q: 0.8, d: 0.16, vol: 0.12 * take.v, delay: 0.03 })
992
+ ],
993
+ knife_swing: whoosh("blade", 1900, 4600, 0.12, 1.1, 2.2),
994
+ knife_stab: fleshHit({ slap: 0.2, slapF: 1200, thud: 0.4, thudF: 160, thudD: 0.06, wet: 0.5 }),
995
+ knife_slash: (take) => [
996
+ sceneNoise({ type: "highpass", f: 3200 * take.f, f2: 1800, d: 0.13 * take.t, vol: 0.35 * take.v }),
997
+ 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 })
998
+ ],
999
+ knife_hit_object: knock({ clickF: 3200, click: 0.25, tones: [[2800, 0.08, 0.07], [4150, 0.06, 0.05]] }),
1000
+ blunt_swing: whoosh("blunt", 220, 760, 0.28, 1.1, 1.5),
1001
+ blunt_hit: fleshHit({ slap: 0.35, slapF: 900, thud: 1, thudF: 70, thudD: 0.18, crack: 0.3 }),
1002
+ blunt_hit_object: knock({ clickF: 1400, click: 0.35, tones: [[300, 0.12, 0.25], [760, 0.08, 0.12]] }),
1003
+ axe_swing: whoosh("chop", 190, 620, 0.3, 1.2, 1.4),
1004
+ axe_chop: fleshHit({ slap: 0.3, slapF: 1400, thud: 0.75, thudF: 140, thudD: 0.1, crack: 0.35, wet: 0.45 }),
1005
+ 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]] })
1006
+ };
1007
+ var BODY_BUS = { player_hurt: "voices", player_death: "voices", player_breath_run: "voices", fall_damage: "voices" };
1008
+ var built = [
1009
+ ...SURFACE_IDS.flatMap((surface) => GAIT_IDS.map((gait) => footstep(surface, gait))),
1010
+ ...SURFACE_IDS.flatMap((surface) => LANDING_IDS.map((l) => landing(surface, l))),
1011
+ ...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]))
1012
+ ];
1013
+ var BODY_FOLEY_BANK = {
1014
+ id: "body_foley",
1015
+ sounds: built.flatMap((entry) => entry.sounds),
1016
+ sets: built.map((entry) => entry.set)
1017
+ };
1018
+ var BODY_FOLEY_SET_IDS = [...FOOTSTEP_SOUND_IDS, ...LAND_SOUND_IDS, ...BODY_SOUND_IDS];
1019
+
1020
+ // src/variation.ts
1021
+ var tuneAutomation = (automation, rate) => {
1022
+ const tuned = { ...automation };
1023
+ if (automation.value !== void 0) tuned.value = automation.value * rate;
1024
+ if (automation.from !== void 0) tuned.from = automation.from * rate;
1025
+ if (automation.to !== void 0) tuned.to = automation.to * rate;
1026
+ if (automation.at !== void 0) tuned.at = automation.at / rate;
1027
+ return tuned;
1028
+ };
1029
+ var tuneFilter = (filter, rate) => ({ ...filter, frequency: tuneAutomation(filter.frequency, rate) });
1030
+ var tuneSource = (source, rate) => {
1031
+ switch (source.kind) {
1032
+ case "noise":
1033
+ return source;
1034
+ case "osc":
1035
+ return { ...source, frequency: tuneAutomation(source.frequency, rate) };
1036
+ case "formant_voice":
1037
+ return {
1038
+ ...source,
1039
+ frequency: tuneAutomation(source.frequency, rate),
1040
+ formants: source.formants.map((formant) => ({ ...formant, frequency: formant.frequency * rate })),
1041
+ breath: { ...source.breath, frequency: source.breath.frequency * rate }
1042
+ };
1043
+ }
1044
+ };
1045
+ var tuneModulator = (modulator, rate) => ({
1046
+ ...modulator,
1047
+ rate: modulator.rate * rate,
1048
+ ...modulator.target === "frequency" ? { depth: modulator.depth * rate } : {}
1049
+ });
1050
+ var tuneLayer = (layer, rate) => ({
1051
+ ...layer,
1052
+ at: layer.at / rate,
1053
+ duration: layer.duration / rate,
1054
+ tail: layer.tail / rate,
1055
+ source: tuneSource(layer.source, rate),
1056
+ ...layer.filters ? { filters: { ...layer.filters, bank: layer.filters.bank.map((filter) => tuneFilter(filter, rate)) } } : {},
1057
+ modulators: layer.modulators.map((modulator) => tuneModulator(modulator, rate)),
1058
+ envelope: layer.envelope.map((point) => ({ ...point, time: point.time / rate }))
1059
+ });
1060
+ function retunePlan(plan, rate) {
1061
+ if (rate === 1 || !(rate > 0)) return plan;
1062
+ return { ...plan, length: plan.length / rate, layers: plan.layers.map((layer) => tuneLayer(layer, rate)) };
1063
+ }
1064
+ function createSetPicker() {
1065
+ const last = /* @__PURE__ */ new Map();
1066
+ return {
1067
+ pick(set, random) {
1068
+ const count = set.variants.length;
1069
+ const previous = last.get(set.id);
1070
+ let index;
1071
+ if (count === 1) index = 0;
1072
+ else if (set.repeat || previous === void 0) index = Math.min(count - 1, Math.floor(random() * count));
1073
+ else {
1074
+ index = Math.min(count - 2, Math.floor(random() * (count - 1)));
1075
+ if (index >= previous) index += 1;
1076
+ }
1077
+ last.set(set.id, index);
1078
+ const [min, max] = set.rate ?? [1, 1];
1079
+ const rate = min === max ? min : min + random() * (max - min);
1080
+ return { id: set.variants[index], rate };
1081
+ },
1082
+ reset: () => last.clear()
1083
+ };
1084
+ }
1085
+
783
1086
  // src/webaudio/noise.ts
784
1087
  function fillNoise(random, length) {
785
1088
  const samples = new Float32Array(length);
@@ -908,11 +1211,11 @@ var GameAudio = (() => {
908
1211
  // src/webaudio/realize-loop.ts
909
1212
  var fixed = (range) => typeof range === "number" ? range : 0;
910
1213
  var fixedFrequency = (automation) => fixed("set" in automation ? automation.set : automation.from);
911
- function sourceOf(recipe, target) {
912
- if (recipe.source.kind === "noise") return { source: noiseSource(target, true) };
1214
+ function sourceOf(recipe2, target) {
1215
+ if (recipe2.source.kind === "noise") return { source: noiseSource(target, true) };
913
1216
  const oscillator = target.context.createOscillator();
914
- oscillator.type = recipe.source.wave;
915
- if (recipe.source.frequency) oscillator.frequency.value = fixedFrequency(recipe.source.frequency);
1217
+ oscillator.type = recipe2.source.wave;
1218
+ if (recipe2.source.frequency) oscillator.frequency.value = fixedFrequency(recipe2.source.frequency);
916
1219
  return { source: oscillator, frequency: oscillator.frequency };
917
1220
  }
918
1221
  function stopAll(sources) {
@@ -925,23 +1228,23 @@ var GameAudio = (() => {
925
1228
  }
926
1229
  };
927
1230
  }
928
- function realizeGameLoop(recipe, target) {
1231
+ function realizeGameLoop(recipe2, target) {
929
1232
  const { context } = target;
930
1233
  const gain = context.createGain();
931
- const filters = (recipe.filters?.bank ?? []).map(() => context.createBiquadFilter());
1234
+ const filters = (recipe2.filters?.bank ?? []).map(() => context.createBiquadFilter());
932
1235
  gain.gain.value = 0;
933
1236
  const last = filters.reduce((previous, filter) => previous ? previous.connect(filter) : filter, void 0);
934
1237
  last?.connect(gain);
935
1238
  gain.connect(target.output);
936
- const { source, frequency } = sourceOf(recipe, target);
937
- recipe.filters?.bank.forEach((spec, index) => {
1239
+ const { source, frequency } = sourceOf(recipe2, target);
1240
+ recipe2.filters?.bank.forEach((spec, index) => {
938
1241
  const filter = filters[index];
939
1242
  filter.type = spec.type;
940
1243
  filter.frequency.value = fixedFrequency(spec.frequency);
941
1244
  if (spec.q !== void 0) filter.Q.value = fixed(spec.q);
942
1245
  });
943
1246
  const sources = [source];
944
- for (const modulator of recipe.modulators ?? []) {
1247
+ for (const modulator of recipe2.modulators ?? []) {
945
1248
  const lfo = context.createOscillator();
946
1249
  const depth = context.createGain();
947
1250
  const stage = context.createGain();
@@ -961,14 +1264,14 @@ var GameAudio = (() => {
961
1264
  source.start();
962
1265
  return { gain, source, ...frequency ? { frequency } : {}, stop: stopAll(sources) };
963
1266
  }
964
- function realizeChainLoop(recipe, target, volume) {
1267
+ function realizeChainLoop(recipe2, target, volume) {
965
1268
  const { context } = target;
966
- const { source, frequency } = sourceOf(recipe, target);
967
- const filters = (recipe.filters?.bank ?? []).map(
1269
+ const { source, frequency } = sourceOf(recipe2, target);
1270
+ const filters = (recipe2.filters?.bank ?? []).map(
968
1271
  (spec) => filterNode(context, { type: spec.type, frequency: { value: fixedFrequency(spec.frequency) }, ...spec.q === void 0 ? {} : { q: fixed(spec.q) } }, target.time)
969
1272
  );
970
1273
  const gain = gainNode(context, volume);
971
- const lfos = (recipe.modulators ?? []).map((modulator) => {
1274
+ const lfos = (recipe2.modulators ?? []).map((modulator) => {
972
1275
  const lfo = context.createOscillator();
973
1276
  if (modulator.wave) lfo.type = modulator.wave;
974
1277
  lfo.frequency.value = fixed(modulator.rate);
@@ -990,9 +1293,9 @@ var GameAudio = (() => {
990
1293
  var INERT = { set() {
991
1294
  }, stop() {
992
1295
  } };
993
- function createLoopHandle(recipe, host, volume) {
994
- if (!recipe) return INERT;
995
- const loopRecipe = recipe;
1296
+ function createLoopHandle(recipe2, host, volume) {
1297
+ if (!recipe2) return INERT;
1298
+ const loopRecipe = recipe2;
996
1299
  const isChain = loopRecipe.order === "chain_loop";
997
1300
  const opening = volume ?? loopRecipe.volume ?? 0;
998
1301
  const smoothing = loopRecipe.smoothing ?? LOOP_SMOOTHING;
@@ -1083,23 +1386,28 @@ var GameAudio = (() => {
1083
1386
  const maxLatencyMs = options.maxLatencyMs ?? 250;
1084
1387
  let jobs = [];
1085
1388
  let scheduled = false;
1389
+ let attached = false;
1390
+ const runNext = () => {
1391
+ const job = jobs.shift();
1392
+ if (nowMs() - job.queuedAt > maxLatencyMs) job.drop();
1393
+ else job.run();
1394
+ };
1086
1395
  const drain = () => {
1087
1396
  scheduled = false;
1397
+ if (attached) return;
1088
1398
  const began = nowMs();
1089
1399
  while (jobs.length > 0) {
1090
- const job = jobs.shift();
1091
- if (nowMs() - job.queuedAt > maxLatencyMs) job.drop();
1092
- else job.run();
1400
+ runNext();
1093
1401
  if (nowMs() - began >= budgetMs) break;
1094
1402
  }
1095
1403
  if (jobs.length > 0) schedule();
1096
1404
  };
1097
1405
  const schedule = () => {
1098
- if (scheduled) return;
1406
+ if (scheduled || attached) return;
1099
1407
  scheduled = true;
1100
1408
  scheduleTask(drain);
1101
1409
  };
1102
- return {
1410
+ const queue = {
1103
1411
  push(job) {
1104
1412
  jobs.push({ ...job, queuedAt: nowMs() });
1105
1413
  schedule();
@@ -1110,8 +1418,33 @@ var GameAudio = (() => {
1110
1418
  },
1111
1419
  get size() {
1112
1420
  return jobs.length;
1421
+ },
1422
+ deferredQueue: () => deferredQueue
1423
+ };
1424
+ const deferredQueue = {
1425
+ id: DEFERRED_QUEUE_ID.AUDIO_VOICES,
1426
+ pending: () => jobs.length,
1427
+ run(deadline, clock) {
1428
+ let ran = 0;
1429
+ while (jobs.length > 0) {
1430
+ runNext();
1431
+ ran++;
1432
+ if (clock() >= deadline) break;
1433
+ }
1434
+ return ran;
1435
+ },
1436
+ flush() {
1437
+ while (jobs.length > 0) runNext();
1438
+ },
1439
+ attach() {
1440
+ attached = true;
1441
+ },
1442
+ detach() {
1443
+ attached = false;
1444
+ if (jobs.length > 0) schedule();
1113
1445
  }
1114
1446
  };
1447
+ return queue;
1115
1448
  }
1116
1449
 
1117
1450
  // src/webaudio/realize-game.ts
@@ -1489,7 +1822,7 @@ var GameAudio = (() => {
1489
1822
  }
1490
1823
 
1491
1824
  // src/webaudio/engine.ts
1492
- var usesNoise = (recipe) => recipe.layers.some((layer) => layer.source.kind !== "osc");
1825
+ var usesNoise = (recipe2) => recipe2.layers.some((layer) => layer.source.kind !== "osc");
1493
1826
  var planUsesNoise = (plan) => plan.layers.some((layer) => layer.source.kind !== "osc");
1494
1827
  function createAudioEngine(options = {}) {
1495
1828
  const seed = options.seed ?? 1;
@@ -1552,7 +1885,7 @@ var GameAudio = (() => {
1552
1885
  lastTime = context.currentTime;
1553
1886
  if (elapsed > 0) for (const id of voices.advance(elapsed)) active.delete(id);
1554
1887
  };
1555
- const inputFor = (recipe, playOptions, random) => ({
1888
+ const inputFor = (recipe2, playOptions, random) => ({
1556
1889
  random,
1557
1890
  listener,
1558
1891
  noiseSeconds: mix.noise.seconds,
@@ -1561,7 +1894,7 @@ var GameAudio = (() => {
1561
1894
  ...playOptions.distance === void 0 ? {} : { distance: playOptions.distance },
1562
1895
  ...playOptions.position ? { position: playOptions.position } : {},
1563
1896
  ...playOptions.params ? { params: playOptions.params } : {},
1564
- ...options.occlusion && playOptions.position && recipe.falloff?.model !== "none" ? { occlusion: options.occlusion(playOptions.position) } : {}
1897
+ ...options.occlusion && playOptions.position && recipe2.falloff?.model !== "none" ? { occlusion: options.occlusion(playOptions.position) } : {}
1565
1898
  });
1566
1899
  const startQueue = options.deferredStart ? createStartQueue(options.deferredStart) : null;
1567
1900
  const build = (plan, store, delay) => {
@@ -1575,20 +1908,20 @@ var GameAudio = (() => {
1575
1908
  });
1576
1909
  };
1577
1910
  const deferVoice = (plan, store, voiceId, delay) => {
1578
- let built = null;
1911
+ let built2 = null;
1579
1912
  let cancelled = false;
1580
1913
  const pending = {
1581
1914
  get sources() {
1582
- return built?.sources ?? [];
1915
+ return built2?.sources ?? [];
1583
1916
  },
1584
1917
  stop() {
1585
1918
  cancelled = true;
1586
- built?.stop();
1919
+ built2?.stop();
1587
1920
  }
1588
1921
  };
1589
1922
  startQueue.push({
1590
1923
  run() {
1591
- if (!cancelled && context) built = build(plan, store, delay);
1924
+ if (!cancelled && context) built2 = build(plan, store, delay);
1592
1925
  },
1593
1926
  drop() {
1594
1927
  if (voiceId === void 0) return;
@@ -1613,8 +1946,8 @@ var GameAudio = (() => {
1613
1946
  }
1614
1947
  };
1615
1948
  };
1616
- const admit = (recipe, gain, distance) => {
1617
- const decision = voices.request({ ...recipe.group ? { group: recipe.group } : {}, priority: recipe.priority ?? busPriority(recipe.bus), gain, distance, length: 0 });
1949
+ const admit = (recipe2, gain, distance) => {
1950
+ const decision = voices.request({ ...recipe2.group ? { group: recipe2.group } : {}, priority: recipe2.priority ?? busPriority(recipe2.bus), gain, distance, length: 0 });
1618
1951
  if (!decision.accepted) return null;
1619
1952
  for (const id of decision.evicted) {
1620
1953
  active.get(id)?.stop();
@@ -1622,22 +1955,31 @@ var GameAudio = (() => {
1622
1955
  }
1623
1956
  return decision.id;
1624
1957
  };
1625
- const playWith = (id, playOptions, random, store) => {
1626
- const recipe = registry.sound(id);
1627
- if (!recipe || busSettings.get(recipe.bus)?.muted) return null;
1958
+ const picker = createSetPicker();
1959
+ const resolve = (id, random, chooser = picker) => {
1960
+ if (registry.sound(id)) return { id, rate: 1 };
1961
+ const set = registry.set(id);
1962
+ return set ? chooser.pick(set, random) : { id, rate: 1 };
1963
+ };
1964
+ const playWith = (asked, playOptions, streamRandom, store) => {
1965
+ const seeded = playOptions.seed === void 0 ? null : createRng(playOptions.seed);
1966
+ const { id, rate } = resolve(asked, seeded ?? streamRandom);
1967
+ const random = seeded ?? streamRandom;
1968
+ const recipe2 = registry.sound(id);
1969
+ if (!recipe2 || busSettings.get(recipe2.bus)?.muted) return null;
1628
1970
  const distance = playOptions.distance ?? (playOptions.position ? listenerDistance(listener, playOptions.position, options.distance) : 0);
1629
- const level = falloffGain(recipe.falloff, playOptions.volume ?? 1, distance);
1630
- const silent = isSilent(recipe.falloff, level);
1631
- if (silent && !recipe.keepSilent) return null;
1971
+ const level = falloffGain(recipe2.falloff, playOptions.volume ?? 1, distance);
1972
+ const silent = isSilent(recipe2.falloff, level);
1973
+ if (silent && !recipe2.keepSilent) return null;
1632
1974
  const audio = ready();
1633
1975
  if (!audio) return null;
1634
1976
  unlockContext(audio);
1635
1977
  advance();
1636
- const voiceId = silent ? void 0 : admit(recipe, level, distance);
1978
+ const voiceId = silent ? void 0 : admit(recipe2, level, distance);
1637
1979
  if (voiceId === null) return null;
1638
- if (!silent && usesNoise(recipe)) prepareNoise(store, audio, mix.noise.seconds);
1639
- const plan = planSound(recipe, inputFor(recipe, playOptions, playOptions.seed === void 0 ? random : createRng(playOptions.seed)));
1640
- return plan ? start(plan, store, voiceId, playOptions.delay) : null;
1980
+ if (!silent && usesNoise(recipe2)) prepareNoise(store, audio, mix.noise.seconds);
1981
+ const plan = planSound(recipe2, inputFor(recipe2, playOptions, random));
1982
+ return plan ? start(retunePlan(plan, rate), store, voiceId, playOptions.delay) : null;
1641
1983
  };
1642
1984
  const randomFor = (playOptions) => {
1643
1985
  if (playOptions.channel === void 0) return engineRandom;
@@ -1647,9 +1989,11 @@ var GameAudio = (() => {
1647
1989
  named.set(playOptions.channel, stream);
1648
1990
  return stream;
1649
1991
  };
1650
- const planWith = (id, playOptions, random) => {
1651
- const recipe = registry.sound(id);
1652
- return recipe ? planSound(recipe, inputFor(recipe, playOptions, random)) : null;
1992
+ const planWith = (asked, playOptions, random) => {
1993
+ const { id, rate } = resolve(asked, random, createSetPicker());
1994
+ const recipe2 = registry.sound(id);
1995
+ const plan = recipe2 ? planSound(recipe2, inputFor(recipe2, playOptions, random)) : null;
1996
+ return plan ? retunePlan(plan, rate) : null;
1653
1997
  };
1654
1998
  const loopHost = {
1655
1999
  ready,
@@ -1668,8 +2012,8 @@ var GameAudio = (() => {
1668
2012
  const audio = ready();
1669
2013
  if (!audio) return null;
1670
2014
  advance();
1671
- const recipe = { id: plan.id, bus: plan.bus, layers: [], sources: [], priority: plan.priority, ...plan.group ? { group: plan.group } : {} };
1672
- const voiceId = plan.silent ? void 0 : admit(recipe, plan.level, plan.distance);
2015
+ const recipe2 = { id: plan.id, bus: plan.bus, layers: [], sources: [], priority: plan.priority, ...plan.group ? { group: plan.group } : {} };
2016
+ const voiceId = plan.silent ? void 0 : admit(recipe2, plan.level, plan.distance);
1673
2017
  if (voiceId === null) return null;
1674
2018
  if (planUsesNoise(plan)) prepareNoise(shared, audio, mix.noise.seconds);
1675
2019
  return start(plan, shared, voiceId, playOptions.delay);
@@ -1731,6 +2075,7 @@ var GameAudio = (() => {
1731
2075
  for (const sample of meter.data) sum += sample * sample;
1732
2076
  return Math.sqrt(sum / meter.data.length);
1733
2077
  },
2078
+ deferredQueue: () => startQueue?.deferredQueue() ?? null,
1734
2079
  stats() {
1735
2080
  advance();
1736
2081
  const voiceStats = voices.stats();
@@ -1768,7 +2113,8 @@ var GameAudio = (() => {
1768
2113
  ...request.position ? { position: request.position } : {},
1769
2114
  ...request.volume === void 0 ? {} : { volume: request.volume },
1770
2115
  ...request.params ? { params: request.params } : {},
1771
- ...request.seed === void 0 ? {} : { seed: request.seed }
2116
+ ...request.seed === void 0 ? {} : { seed: request.seed },
2117
+ ...request.delay === void 0 ? {} : { delay: request.delay }
1772
2118
  });
1773
2119
  if (!handle) return null;
1774
2120
  playing.set(handle.id, handle);