@adatechnology/game-audio 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,7 @@ var GameAudio = (() => {
4
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
7
8
  var __export = (target, all) => {
8
9
  for (var name in all)
9
10
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -17,6 +18,7 @@ var GameAudio = (() => {
17
18
  return to;
18
19
  };
19
20
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
21
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
20
22
 
21
23
  // src/index.ts
22
24
  var index_exports = {};
@@ -64,13 +66,19 @@ var GameAudio = (() => {
64
66
  LIBRARY_BANK: () => LIBRARY_BANK,
65
67
  LIBRARY_LOOPS: () => LIBRARY_LOOPS,
66
68
  LIBRARY_SOUNDS: () => LIBRARY_SOUNDS,
69
+ LOCAL_PACK_BANK_ID: () => LOCAL_PACK_BANK_ID,
70
+ LOCAL_PACK_LICENSES: () => LOCAL_PACK_LICENSES,
71
+ LOCAL_PACK_PREFERENCES: () => LOCAL_PACK_PREFERENCES,
67
72
  LOOP_SMOOTHING: () => LOOP_SMOOTHING,
68
73
  LOOP_START_ABOVE: () => LOOP_START_ABOVE,
74
+ LocalSoundPackError: () => LocalSoundPackError,
69
75
  MIX_PROFILES: () => MIX_PROFILES,
70
76
  OPEN_CUTOFF: () => OPEN_CUTOFF,
71
77
  PAN_LIMIT: () => PAN_LIMIT,
72
78
  PAN_MIN_DISTANCE: () => PAN_MIN_DISTANCE,
73
79
  PICKUP_CLASSES: () => PICKUP_CLASSES,
80
+ RADIO_BANK: () => RADIO_BANK,
81
+ RADIO_TRACKS: () => RADIO_TRACKS,
74
82
  SAMPLE_ENVIRONMENTS: () => SAMPLE_ENVIRONMENTS,
75
83
  SDK_LICENSES: () => SDK_LICENSES,
76
84
  SFX_BLOWS: () => SFX_BLOWS,
@@ -80,9 +88,12 @@ var GameAudio = (() => {
80
88
  SFX_LAYERS: () => SFX_LAYERS,
81
89
  SFX_MATERIALS: () => SFX_MATERIALS,
82
90
  SFX_TIERS: () => SFX_TIERS,
91
+ TRIM_FADE: () => TRIM_FADE,
83
92
  USER_SAMPLES_BANK: () => USER_SAMPLES_BANK,
84
93
  USER_SOUND_PREFIX: () => USER_SOUND_PREFIX,
85
94
  VOICE_GROUPS: () => VOICE_GROUPS,
95
+ aliasSample: () => aliasSample,
96
+ applyLocalSoundPack: () => applyLocalSoundPack,
86
97
  audioBenchTextOf: () => audioBenchTextOf,
87
98
  balanceOf: () => balanceOf,
88
99
  bufferOf: () => bufferOf,
@@ -104,6 +115,7 @@ var GameAudio = (() => {
104
115
  crossGains: () => crossGains,
105
116
  cutoffDivisor: () => cutoffDivisor,
106
117
  dbOf: () => dbOf,
118
+ defaultIdleScheduler: () => defaultIdleScheduler,
107
119
  drawBetween: () => drawBetween,
108
120
  encodeWav: () => encodeWav,
109
121
  endDropDb: () => endDropDb,
@@ -117,6 +129,7 @@ var GameAudio = (() => {
117
129
  impactSoundId: () => impactSoundId,
118
130
  inlineLoader: () => inlineLoader,
119
131
  isCalibrationUrl: () => isCalibrationUrl,
132
+ isShippableLicense: () => isShippableLicense,
120
133
  isSilent: () => isSilent,
121
134
  kitSampleBank: () => kitSampleBank,
122
135
  kitSampleDefaults: () => kitSampleDefaults,
@@ -124,17 +137,22 @@ var GameAudio = (() => {
124
137
  listenerDistance: () => listenerDistance,
125
138
  loadKitSamples: () => loadKitSamples,
126
139
  loadUserSamples: () => loadUserSamples,
140
+ localPackSamples: () => localPackSamples,
127
141
  loopCrossfade: () => loopCrossfade,
142
+ loopRate: () => loopRate,
128
143
  measureCriteria: () => measureCriteria,
129
144
  noiseClassRecipe: () => noiseClassRecipe,
130
145
  onsetIndex: () => onsetIndex,
131
146
  panFor: () => panFor,
147
+ parseLocalSoundPack: () => parseLocalSoundPack,
132
148
  parseSfxName: () => parseSfxName,
133
149
  peakIndex: () => peakIndex,
134
150
  peakOf: () => peakOf,
135
151
  pickTake: () => pickTake,
136
152
  planSample: () => planSample,
137
153
  planSound: () => planSound,
154
+ pointSamples: () => pointSamples,
155
+ radioTrack: () => radioTrack,
138
156
  realize: () => realize,
139
157
  realizeSample: () => realizeSample,
140
158
  recordOutput: () => recordOutput,
@@ -269,6 +287,20 @@ var GameAudio = (() => {
269
287
  sources: [
270
288
  "sdk: the crackles of every fire share four voices"
271
289
  ]
290
+ },
291
+ debris_knock: {
292
+ maxConcurrent: 3,
293
+ cooldown: 0.04,
294
+ sources: [
295
+ "sdk: forty parts coming loose in one blast are a few knocks, not forty (room log: thunk x34 in under a second when a molotov broke)"
296
+ ]
297
+ },
298
+ sizzle: {
299
+ maxConcurrent: 1,
300
+ cooldown: 0.5,
301
+ sources: [
302
+ "sdk: every melting part of a burning bin is one sizzle at a time (room log: hiss x34 in 1.6 s on a plastic bin)"
303
+ ]
272
304
  }
273
305
  };
274
306
 
@@ -420,16 +452,16 @@ var GameAudio = (() => {
420
452
  }
421
453
  return pending;
422
454
  }
423
- function drawLayer(layer, scope) {
455
+ function drawLayer(layer2, scope) {
424
456
  const pending = { at: 0, gain: 1, filters: [], modulators: [] };
425
- for (const key of Object.keys(layer)) {
426
- if (key === "at" && layer.at !== void 0) pending.at = draw(layer.at, scope);
427
- else if (key === "duration" && layer.duration !== void 0) pending.duration = draw(layer.duration, scope);
428
- else if (key === "gain" && layer.gain !== void 0) pending.gain = draw(layer.gain, scope);
429
- else if (key === "source" && layer.source.kind === "osc") pending.automation = drawAutomation(layer.source.frequency, scope);
430
- else if (key === "source" && layer.source.kind === "formant_voice") pending.voice = drawVoice(layer.source, scope);
431
- else if (key === "filters" && layer.filters) pending.filters = layer.filters.bank.map((spec) => drawFilter(spec, scope));
432
- else if (key === "modulators" && layer.modulators) pending.modulators = layer.modulators.map((modulator) => drawModulator(modulator, scope));
457
+ for (const key of Object.keys(layer2)) {
458
+ if (key === "at" && layer2.at !== void 0) pending.at = draw(layer2.at, scope);
459
+ else if (key === "duration" && layer2.duration !== void 0) pending.duration = draw(layer2.duration, scope);
460
+ else if (key === "gain" && layer2.gain !== void 0) pending.gain = draw(layer2.gain, scope);
461
+ else if (key === "source" && layer2.source.kind === "osc") pending.automation = drawAutomation(layer2.source.frequency, scope);
462
+ else if (key === "source" && layer2.source.kind === "formant_voice") pending.voice = drawVoice(layer2.source, scope);
463
+ else if (key === "filters" && layer2.filters) pending.filters = layer2.filters.bank.map((spec) => drawFilter(spec, scope));
464
+ else if (key === "modulators" && layer2.modulators) pending.modulators = layer2.modulators.map((modulator) => drawModulator(modulator, scope));
433
465
  }
434
466
  return pending;
435
467
  }
@@ -440,8 +472,8 @@ var GameAudio = (() => {
440
472
  if (source.offset === "fit") return scope.random() * Math.max(0, noiseSeconds - duration);
441
473
  return scope.random() * (source.offsetScale ?? 1);
442
474
  }
443
- function finishSource(layer, pending, duration, offset) {
444
- const source = layer.source;
475
+ function finishSource(layer2, pending, duration, offset) {
476
+ const source = layer2.source;
445
477
  if (source.kind === "noise") return { kind: "noise", offset, loop: source.loop === true };
446
478
  if (source.kind === "osc") return { kind: "osc", wave: source.wave, frequency: finishAutomation(pending.automation ?? {}, duration) };
447
479
  const voice2 = pending.voice;
@@ -463,23 +495,23 @@ var GameAudio = (() => {
463
495
  function finishEnvelope(envelope2, duration, level) {
464
496
  return envelope2.map(([time, value, curve, fixed2]) => ({ time: timeOf(time, duration), value: fixed2 === "fixed" ? value : value * level, curve }));
465
497
  }
466
- function planLayer(layer, scope, defaults, levelInEnvelope, soundLevel) {
467
- const pending = drawLayer(layer, scope);
498
+ function planLayer(layer2, scope, defaults, levelInEnvelope, soundLevel) {
499
+ const pending = drawLayer(layer2, scope);
468
500
  const duration = pending.duration ?? defaults.duration;
469
501
  if (duration === void 0) throw new Error("game-audio: a layer needs a duration (its own or the recipe\u2019s)");
470
- const offset = drawOffset(layer.source, duration, scope, defaults.noiseSeconds);
502
+ const offset = drawOffset(layer2.source, duration, scope, defaults.noiseSeconds);
471
503
  const level = pending.gain * soundLevel;
472
- const sourceFrequency = layer.source.kind === "noise" ? 0 : startFrequency(pending.automation ?? pending.voice?.frequency ?? {});
504
+ const sourceFrequency = layer2.source.kind === "noise" ? 0 : startFrequency(pending.automation ?? pending.voice?.frequency ?? {});
473
505
  const planned = {
474
506
  at: pending.at,
475
507
  duration,
476
- tail: layer.tail ?? defaults.tail ?? DEFAULT_TAIL,
508
+ tail: layer2.tail ?? defaults.tail ?? DEFAULT_TAIL,
477
509
  level,
478
- ...layer.wet === void 0 ? {} : { wet: layer.wet },
479
- source: finishSource(layer, pending, duration, offset),
480
- ...layer.filters ? {
510
+ ...layer2.wet === void 0 ? {} : { wet: layer2.wet },
511
+ source: finishSource(layer2, pending, duration, offset),
512
+ ...layer2.filters ? {
481
513
  filters: {
482
- mode: layer.filters.mode,
514
+ mode: layer2.filters.mode,
483
515
  bank: pending.filters.map(({ spec, frequency, q, gain, level: filterLevel }) => ({
484
516
  type: spec.type,
485
517
  frequency: finishAutomation(frequency, duration),
@@ -490,10 +522,10 @@ var GameAudio = (() => {
490
522
  }
491
523
  } : {},
492
524
  modulators: pending.modulators.map((modulator) => finishModulator(modulator, sourceFrequency)),
493
- envelope: finishEnvelope(layer.envelope, duration, levelInEnvelope ? level : 1)
525
+ envelope: finishEnvelope(layer2.envelope, duration, levelInEnvelope ? level : 1)
494
526
  };
495
- if (!layer.repeat) return [planned];
496
- return Array.from({ length: layer.repeat.count }, (_, index) => ({ ...planned, at: planned.at + index * (layer.repeat?.spacing ?? 0) }));
527
+ if (!layer2.repeat) return [planned];
528
+ return Array.from({ length: layer2.repeat.count }, (_, index) => ({ ...planned, at: planned.at + index * (layer2.repeat?.spacing ?? 0) }));
497
529
  }
498
530
  function scaleAutomation(automation, divisor) {
499
531
  const scale = (value) => value === void 0 ? void 0 : value / divisor;
@@ -501,12 +533,12 @@ var GameAudio = (() => {
501
533
  for (const key of ["value", "from", "to"]) if (automation[key] !== void 0) scaled[key] = scale(automation[key]);
502
534
  return scaled;
503
535
  }
504
- function muffle(layer, divisor) {
505
- if (divisor === 1 || !layer.filters) return layer;
506
- const bank = layer.filters.bank.map((filter) => filter.type === "lowpass" ? { ...filter, frequency: scaleAutomation(filter.frequency, divisor) } : filter);
507
- return { ...layer, filters: { ...layer.filters, bank } };
536
+ function muffle(layer2, divisor) {
537
+ if (divisor === 1 || !layer2.filters) return layer2;
538
+ const bank = layer2.filters.bank.map((filter) => filter.type === "lowpass" ? { ...filter, frequency: scaleAutomation(filter.frequency, divisor) } : filter);
539
+ return { ...layer2, filters: { ...layer2.filters, bank } };
508
540
  }
509
- var layerEnd = (layer) => layer.at + layer.duration + layer.tail;
541
+ var layerEnd = (layer2) => layer2.at + layer2.duration + layer2.tail;
510
542
  function planSound(recipe2, input = {}) {
511
543
  const listener = input.listener ?? DEFAULT_LISTENER;
512
544
  const distance = input.distance ?? (input.position ? listenerDistance(listener, input.position, input.distanceMode) : 0);
@@ -534,7 +566,7 @@ var GameAudio = (() => {
534
566
  noiseSeconds: input.noiseSeconds ?? DEFAULT_NOISE_SECONDS
535
567
  };
536
568
  const divisor = cutoffDivisor(recipe2.falloff, distance) / (1 - OCCLUSION_CUTOFF * occlusion);
537
- const layers = recipe2.layers.flatMap((layer) => planLayer(layer, scope, defaults, order !== "layered", level)).map((layer) => muffle(layer, divisor));
569
+ const layers = recipe2.layers.flatMap((layer2) => planLayer(layer2, scope, defaults, order !== "layered", level)).map((layer2) => muffle(layer2, divisor));
538
570
  return { ...base, silent: false, length: Math.max(0, ...layers.map(layerEnd)), layers };
539
571
  }
540
572
 
@@ -600,7 +632,16 @@ var GameAudio = (() => {
600
632
  const index = pickTake(refs.length, input.memory?.get(key), input.random);
601
633
  input.memory?.set(key, index);
602
634
  const ref = refs[index];
603
- layers.push({ role, url: ref.url, take: index, gain: gain * (ref.gain ?? 1) * level, rate, at });
635
+ layers.push({
636
+ role,
637
+ url: ref.url,
638
+ take: index,
639
+ gain: gain * (ref.gain ?? 1) * level,
640
+ rate,
641
+ at,
642
+ ...ref.offset === void 0 ? {} : { offset: ref.offset },
643
+ ...ref.duration === void 0 ? {} : { duration: ref.duration }
644
+ });
604
645
  };
605
646
  take("close", close, gains.close, 0);
606
647
  take("far", far, gains.far, 0);
@@ -747,12 +788,48 @@ var GameAudio = (() => {
747
788
  }
748
789
  return merged;
749
790
  }
791
+ var trimmed = (refs, alias) => refs.filter((ref) => alias.only === void 0 || ref.url.includes(alias.only)).map((ref) => ({
792
+ ...ref,
793
+ ...alias.offset === void 0 ? {} : { offset: alias.offset },
794
+ ...alias.duration === void 0 ? {} : { duration: alias.duration }
795
+ }));
796
+ function aliasSample(alias, file) {
797
+ const close = trimmed(file.close, alias);
798
+ if (close.length === 0) return void 0;
799
+ const { tiers: _tiers, fallback: _fallback, choke: _choke, ...rest } = file;
800
+ return {
801
+ ...rest,
802
+ id: alias.id,
803
+ close,
804
+ ...file.far ? { far: trimmed(file.far, alias) } : {},
805
+ ...alias.loop === void 0 ? {} : { loop: alias.loop },
806
+ ...alias.baseFrequency === void 0 ? {} : { baseFrequency: alias.baseFrequency },
807
+ ...alias.gain === void 0 ? {} : { gain: alias.gain },
808
+ ...alias.pitch === void 0 ? {} : { pitch: alias.pitch },
809
+ ...alias.falloff ? { falloff: alias.falloff } : {},
810
+ fallback: alias.id,
811
+ sources: [...file.sources, `alias: ${alias.id} \u2192 ${alias.file}`]
812
+ };
813
+ }
750
814
  function createSoundRegistry(banks = []) {
751
815
  const sounds = /* @__PURE__ */ new Map();
752
816
  const loops = /* @__PURE__ */ new Map();
753
817
  const sets = /* @__PURE__ */ new Map();
754
818
  const samples = /* @__PURE__ */ new Map();
755
819
  const owners = /* @__PURE__ */ new Map();
820
+ const aliases = /* @__PURE__ */ new Map();
821
+ const derived = /* @__PURE__ */ new Map();
822
+ const resolveAlias = (id) => {
823
+ const alias = aliases.get(id);
824
+ if (!alias) return void 0;
825
+ const cached = derived.get(id);
826
+ if (cached) return cached;
827
+ const file = samples.get(alias.file);
828
+ if (!file) return void 0;
829
+ const sample = aliasSample(alias, file);
830
+ if (sample) derived.set(id, sample);
831
+ return sample;
832
+ };
756
833
  const registry = {
757
834
  register(bank) {
758
835
  for (const sound of bank.sounds ?? []) {
@@ -772,6 +849,11 @@ var GameAudio = (() => {
772
849
  samples.set(sample.id, sample);
773
850
  owners.set(sample.id, bank.id);
774
851
  }
852
+ for (const alias of bank.sampleAliases ?? []) {
853
+ aliases.set(alias.id, alias);
854
+ if (!owners.has(alias.id)) owners.set(alias.id, bank.id);
855
+ }
856
+ derived.clear();
775
857
  },
776
858
  patch(id, patch) {
777
859
  const sound = sounds.get(id);
@@ -780,22 +862,31 @@ var GameAudio = (() => {
780
862
  if (loop) loops.set(id, mergeRecords(loop, patch));
781
863
  const sample = samples.get(id);
782
864
  if (sample) samples.set(id, mergeRecords(sample, patch));
865
+ derived.clear();
783
866
  },
784
867
  remove(id) {
785
868
  sounds.delete(id);
786
869
  loops.delete(id);
787
870
  sets.delete(id);
788
871
  samples.delete(id);
872
+ aliases.delete(id);
789
873
  owners.delete(id);
874
+ derived.clear();
875
+ },
876
+ removeSample(id) {
877
+ samples.delete(id);
878
+ aliases.delete(id);
879
+ if (!sounds.has(id) && !loops.has(id) && !sets.has(id)) owners.delete(id);
880
+ derived.clear();
790
881
  },
791
882
  sound: (id) => sounds.get(id),
792
883
  loop: (id) => loops.get(id),
793
884
  set: (id) => sets.get(id),
794
- sample: (id) => samples.get(id),
885
+ sample: (id) => samples.get(id) ?? resolveAlias(id),
795
886
  soundIds: () => [...sounds.keys()],
796
887
  setIds: () => [...sets.keys()],
797
888
  loopIds: () => [...loops.keys()],
798
- sampleIds: () => [...samples.keys()],
889
+ sampleIds: () => [...samples.keys(), ...[...aliases.keys()].filter((id) => !samples.has(id) && resolveAlias(id) !== void 0)],
799
890
  bankOf: (id) => owners.get(id)
800
891
  };
801
892
  for (const bank of banks) registry.register(bank);
@@ -1223,6 +1314,8 @@ var GameAudio = (() => {
1223
1314
  var DEFAULT_LOOP_CROSSFADE = 0.25;
1224
1315
  var LOOKAHEAD = 1.5;
1225
1316
  var VOLUME_SMOOTHING = 0.05;
1317
+ var RATE_SMOOTHING = 0.04;
1318
+ var RATE_RANGE = [0.25, 4];
1226
1319
  function loopCrossfade(sound) {
1227
1320
  if (!sound.loop) return void 0;
1228
1321
  return sound.loop === true ? DEFAULT_LOOP_CROSSFADE : Math.max(0, sound.loop.crossfade ?? DEFAULT_LOOP_CROSSFADE);
@@ -1234,6 +1327,7 @@ var GameAudio = (() => {
1234
1327
  param.setValueAtTime(value(0), at);
1235
1328
  for (let step = 1; step <= CROSSFADE_STEPS; step++) param.linearRampToValueAtTime(value(step / CROSSFADE_STEPS), at + seconds * step / CROSSFADE_STEPS);
1236
1329
  };
1330
+ var loopRate = (sound, frequency) => sound.baseFrequency && sound.baseFrequency > 0 && frequency > 0 ? Math.max(RATE_RANGE[0], Math.min(RATE_RANGE[1], frequency / sound.baseFrequency)) : 1;
1237
1331
  function createSampleLoopHandle(sound, host, volume = 0) {
1238
1332
  const crossfade = loopCrossfade(sound) ?? DEFAULT_LOOP_CROSSFADE;
1239
1333
  let level = volume;
@@ -1243,13 +1337,17 @@ var GameAudio = (() => {
1243
1337
  let segments = [];
1244
1338
  let last;
1245
1339
  let timer;
1340
+ let panner;
1341
+ let rate = 1;
1342
+ let position;
1246
1343
  const entry = {
1247
1344
  get created() {
1248
1345
  return master !== void 0;
1249
1346
  },
1250
1347
  rewire() {
1251
- master?.disconnect();
1252
- master?.connect(host.output(sound.bus));
1348
+ const out = panner ?? master;
1349
+ out?.disconnect();
1350
+ out?.connect(host.output(sound.bus));
1253
1351
  },
1254
1352
  stop() {
1255
1353
  stopped = true;
@@ -1266,23 +1364,29 @@ var GameAudio = (() => {
1266
1364
  segments = [];
1267
1365
  master?.disconnect();
1268
1366
  master = void 0;
1367
+ panner?.disconnect();
1368
+ panner = void 0;
1269
1369
  fallback?.stop();
1270
1370
  fallback = void 0;
1271
1371
  host.unregister(entry);
1272
1372
  }
1273
1373
  };
1274
- const nextBuffer = () => {
1374
+ const nextTake = () => {
1275
1375
  const takes2 = sound.close;
1276
1376
  const take = pickTake(takes2.length, last, host.random);
1277
1377
  last = take;
1278
1378
  const ref = takes2[take];
1279
- return ref ? host.buffer(ref.url) : void 0;
1379
+ const buffer = ref ? host.buffer(ref.url) : void 0;
1380
+ return ref && buffer ? { buffer, ref } : void 0;
1280
1381
  };
1281
1382
  const schedule = (context, output, at, first) => {
1282
- const buffer = nextBuffer();
1283
- if (!buffer) return void 0;
1383
+ const next = nextTake();
1384
+ if (!next) return void 0;
1385
+ const { buffer, ref } = next;
1386
+ const offset = Math.max(0, Math.min(ref.offset ?? 0, buffer.duration));
1284
1387
  const source = context.createBufferSource();
1285
1388
  source.buffer = buffer;
1389
+ source.playbackRate.value = rate;
1286
1390
  const gain = context.createGain();
1287
1391
  source.connect(gain);
1288
1392
  gain.connect(output);
@@ -1290,15 +1394,15 @@ var GameAudio = (() => {
1290
1394
  if (buffer.duration < 2 * crossfade || sound.close.length === 1) {
1291
1395
  source.loop = true;
1292
1396
  if (!first) ramp2(gain.gain, "in", at, Math.max(1e-3, fade));
1293
- source.start(at);
1397
+ source.start(at, offset);
1294
1398
  segments.push({ source, gain, end: Number.POSITIVE_INFINITY });
1295
1399
  return void 0;
1296
1400
  }
1297
1401
  if (first) gain.gain.setValueAtTime(1, at);
1298
1402
  else ramp2(gain.gain, "in", at, fade);
1299
- const end3 = at + buffer.duration;
1403
+ const end3 = at + (buffer.duration - offset) / rate;
1300
1404
  ramp2(gain.gain, "out", end3 - fade, fade);
1301
- source.start(at);
1405
+ source.start(at, offset);
1302
1406
  source.stop(end3);
1303
1407
  segments.push({ source, gain, end: end3 });
1304
1408
  source.onended = () => {
@@ -1319,14 +1423,26 @@ var GameAudio = (() => {
1319
1423
  pump(context, output, next, false);
1320
1424
  }, wait);
1321
1425
  };
1322
- const fileLevel = () => level * (sound.gain ?? 1);
1426
+ const spatial = () => {
1427
+ if (!position || !host.listener) return 1;
1428
+ return falloffGain(sound.falloff ?? DEFAULT_SAMPLE_FALLOFF, 1, listenerDistance(host.listener(), position, host.distance));
1429
+ };
1430
+ const fileLevel = () => level * (sound.gain ?? 1) * spatial();
1431
+ const place = (context) => {
1432
+ if (panner && position && host.listener) panner.pan.setTargetAtTime(panFor(host.listener(), position), context.currentTime, VOLUME_SMOOTHING);
1433
+ };
1323
1434
  const startFiles = () => {
1324
1435
  if (stopped || master || !host.isReady(sound)) return master !== void 0;
1325
1436
  const context = host.ready();
1326
1437
  if (!context) return false;
1327
1438
  master = context.createGain();
1328
1439
  master.gain.setValueAtTime(fileLevel(), context.currentTime);
1329
- master.connect(host.output(sound.bus));
1440
+ if (sound.pan !== false && position && typeof context.createStereoPanner === "function") {
1441
+ panner = context.createStereoPanner();
1442
+ if (host.listener) panner.pan.value = panFor(host.listener(), position);
1443
+ master.connect(panner);
1444
+ panner.connect(host.output(sound.bus));
1445
+ } else master.connect(host.output(sound.bus));
1330
1446
  host.register(entry);
1331
1447
  pump(context, master, context.currentTime, true);
1332
1448
  if (fallback) {
@@ -1352,9 +1468,15 @@ var GameAudio = (() => {
1352
1468
  set(next) {
1353
1469
  if (stopped) return;
1354
1470
  if (next.volume !== void 0) level = host.muted(sound.bus) ? 0 : Math.max(0, next.volume);
1471
+ if (next.position) position = next.position;
1472
+ if (next.frequency !== void 0 && sound.baseFrequency) rate = loopRate(sound, next.frequency);
1355
1473
  if (level > 0) ensure();
1356
1474
  const context = host.ready();
1357
- if (master && context) master.gain.setTargetAtTime(fileLevel(), context.currentTime, VOLUME_SMOOTHING);
1475
+ if (master && context) {
1476
+ master.gain.setTargetAtTime(fileLevel(), context.currentTime, VOLUME_SMOOTHING);
1477
+ place(context);
1478
+ if (next.frequency !== void 0 && sound.baseFrequency) for (const segment of segments) segment.source.playbackRate.setTargetAtTime(rate, context.currentTime, RATE_SMOOTHING);
1479
+ }
1358
1480
  fallback?.set(next.volume === void 0 ? next : { ...next, volume: level });
1359
1481
  },
1360
1482
  stop: entry.stop
@@ -1516,9 +1638,9 @@ var GameAudio = (() => {
1516
1638
  }
1517
1639
 
1518
1640
  // src/webaudio/realize-game.ts
1519
- var end = (layer, start) => start + layer.duration + layer.tail;
1520
- var firstFilter = (layer) => layer.filters?.bank[0];
1521
- var offsetOf = (layer) => layer.source.kind === "noise" ? layer.source.offset : 0;
1641
+ var end = (layer2, start) => start + layer2.duration + layer2.tail;
1642
+ var firstFilter = (layer2) => layer2.filters?.bank[0];
1643
+ var offsetOf = (layer2) => layer2.source.kind === "noise" ? layer2.source.offset : 0;
1522
1644
  function setFilter(node, filter, time) {
1523
1645
  if (!filter) return;
1524
1646
  node.type = filter.type;
@@ -1527,28 +1649,28 @@ var GameAudio = (() => {
1527
1649
  }
1528
1650
  function realizeBurst(plan, target) {
1529
1651
  const { context } = target;
1530
- const layer = plan.layers[0];
1531
- const time = target.time + layer.at;
1652
+ const layer2 = plan.layers[0];
1653
+ const time = target.time + layer2.at;
1532
1654
  const gain = context.createGain();
1533
1655
  gain.connect(target.output);
1534
- const source = noiseSource(target, layer.source.kind === "noise" && layer.source.loop);
1656
+ const source = noiseSource(target, layer2.source.kind === "noise" && layer2.source.loop);
1535
1657
  const filter = context.createBiquadFilter();
1536
1658
  source.connect(filter);
1537
1659
  filter.connect(gain);
1538
- setFilter(filter, firstFilter(layer), time);
1539
- applyEnvelope(gain.gain, layer.envelope, time);
1540
- source.start(time, offsetOf(layer));
1541
- source.stop(end(layer, time));
1660
+ setFilter(filter, firstFilter(layer2), time);
1661
+ applyEnvelope(gain.gain, layer2.envelope, time);
1662
+ source.start(time, offsetOf(layer2));
1663
+ source.stop(end(layer2, time));
1542
1664
  return voiceOf([source]);
1543
1665
  }
1544
1666
  function realizeScream(plan, target) {
1545
1667
  const { context } = target;
1546
- const layer = plan.layers[0];
1547
- const time = target.time + layer.at;
1548
- const source = noiseSource(target, layer.source.kind === "noise" && layer.source.loop);
1668
+ const layer2 = plan.layers[0];
1669
+ const time = target.time + layer2.at;
1670
+ const source = noiseSource(target, layer2.source.kind === "noise" && layer2.source.loop);
1549
1671
  const gain = context.createGain();
1550
1672
  gain.connect(target.output);
1551
- for (const band of layer.filters?.bank ?? []) {
1673
+ for (const band of layer2.filters?.bank ?? []) {
1552
1674
  const filter = context.createBiquadFilter();
1553
1675
  filter.type = band.type;
1554
1676
  if (band.q !== void 0) filter.Q.value = band.q;
@@ -1561,7 +1683,7 @@ var GameAudio = (() => {
1561
1683
  level.connect(gain);
1562
1684
  }
1563
1685
  const sources = [source];
1564
- const lfos = layer.modulators.map((modulator) => {
1686
+ const lfos = layer2.modulators.map((modulator) => {
1565
1687
  const lfo = context.createOscillator();
1566
1688
  const depth = context.createGain();
1567
1689
  if (modulator.wave) lfo.type = modulator.wave;
@@ -1571,75 +1693,75 @@ var GameAudio = (() => {
1571
1693
  depth.connect(gain.gain);
1572
1694
  return lfo;
1573
1695
  });
1574
- applyEnvelope(gain.gain, layer.envelope, time);
1575
- source.start(time, offsetOf(layer));
1576
- source.stop(end(layer, time));
1696
+ applyEnvelope(gain.gain, layer2.envelope, time);
1697
+ source.start(time, offsetOf(layer2));
1698
+ source.stop(end(layer2, time));
1577
1699
  for (const lfo of lfos) {
1578
1700
  lfo.start(time);
1579
- lfo.stop(end(layer, time));
1701
+ lfo.stop(end(layer2, time));
1580
1702
  sources.push(lfo);
1581
1703
  }
1582
1704
  return voiceOf(sources);
1583
1705
  }
1584
1706
  function realizeGroan(plan, target) {
1585
1707
  const { context } = target;
1586
- const layer = plan.layers[0];
1587
- const time = target.time + layer.at;
1708
+ const layer2 = plan.layers[0];
1709
+ const time = target.time + layer2.at;
1588
1710
  const oscillator = context.createOscillator();
1589
1711
  const gain = context.createGain();
1590
1712
  const filter = context.createBiquadFilter();
1591
- const band = firstFilter(layer);
1592
- if (layer.source.kind === "osc") oscillator.type = layer.source.wave;
1713
+ const band = firstFilter(layer2);
1714
+ if (layer2.source.kind === "osc") oscillator.type = layer2.source.wave;
1593
1715
  if (band) {
1594
1716
  filter.type = band.type;
1595
1717
  if (band.q !== void 0) filter.Q.value = band.q;
1596
1718
  }
1597
- if (layer.source.kind === "osc") {
1598
- assign(oscillator.frequency, layer.source.frequency, time);
1599
- rampTo(oscillator.frequency, layer.source.frequency, time);
1719
+ if (layer2.source.kind === "osc") {
1720
+ assign(oscillator.frequency, layer2.source.frequency, time);
1721
+ rampTo(oscillator.frequency, layer2.source.frequency, time);
1600
1722
  }
1601
1723
  if (band) assign(filter.frequency, band.frequency, time);
1602
- applyEnvelope(gain.gain, layer.envelope, time);
1724
+ applyEnvelope(gain.gain, layer2.envelope, time);
1603
1725
  oscillator.connect(filter);
1604
1726
  filter.connect(gain);
1605
1727
  gain.connect(target.output);
1606
1728
  oscillator.start(time);
1607
- oscillator.stop(end(layer, time));
1729
+ oscillator.stop(end(layer2, time));
1608
1730
  return voiceOf([oscillator]);
1609
1731
  }
1610
1732
  function realizeBoom(plan, target) {
1611
1733
  const { context } = target;
1612
- const layer = plan.layers[0];
1613
- const time = target.time + layer.at;
1734
+ const layer2 = plan.layers[0];
1735
+ const time = target.time + layer2.at;
1614
1736
  const oscillator = context.createOscillator();
1615
1737
  const gain = context.createGain();
1616
- if (layer.source.kind === "osc") {
1617
- oscillator.type = layer.source.wave;
1618
- assign(oscillator.frequency, layer.source.frequency, time);
1619
- rampTo(oscillator.frequency, layer.source.frequency, time);
1738
+ if (layer2.source.kind === "osc") {
1739
+ oscillator.type = layer2.source.wave;
1740
+ assign(oscillator.frequency, layer2.source.frequency, time);
1741
+ rampTo(oscillator.frequency, layer2.source.frequency, time);
1620
1742
  }
1621
- applyEnvelope(gain.gain, layer.envelope, time);
1743
+ applyEnvelope(gain.gain, layer2.envelope, time);
1622
1744
  oscillator.connect(gain);
1623
1745
  gain.connect(target.output);
1624
1746
  oscillator.start(time);
1625
- oscillator.stop(end(layer, time));
1747
+ oscillator.stop(end(layer2, time));
1626
1748
  return voiceOf([oscillator]);
1627
1749
  }
1628
1750
  function realizeStatic(plan, target) {
1629
1751
  const { context } = target;
1630
- const layer = plan.layers[0];
1631
- const time = target.time + layer.at;
1752
+ const layer2 = plan.layers[0];
1753
+ const time = target.time + layer2.at;
1632
1754
  const gain = context.createGain();
1633
1755
  const source = context.createBufferSource();
1634
1756
  const filter = context.createBiquadFilter();
1635
1757
  source.buffer = target.noise();
1636
- setFilter(filter, firstFilter(layer), time);
1637
- applyEnvelope(gain.gain, layer.envelope, time);
1758
+ setFilter(filter, firstFilter(layer2), time);
1759
+ applyEnvelope(gain.gain, layer2.envelope, time);
1638
1760
  source.connect(filter);
1639
1761
  filter.connect(gain);
1640
1762
  gain.connect(target.output);
1641
- source.start(time, offsetOf(layer));
1642
- source.stop(end(layer, time));
1763
+ source.start(time, offsetOf(layer2));
1764
+ source.stop(end(layer2, time));
1643
1765
  return voiceOf([source]);
1644
1766
  }
1645
1767
  function realizeChord(plan, target) {
@@ -1651,14 +1773,14 @@ var GameAudio = (() => {
1651
1773
  const source = layers[index].source;
1652
1774
  if (source.kind === "osc") oscillators[index].type = source.wave;
1653
1775
  }
1654
- layers.forEach((layer, index) => {
1655
- if (layer.source.kind === "osc") assign(oscillators[index].frequency, layer.source.frequency, target.time + layer.at);
1776
+ layers.forEach((layer2, index) => {
1777
+ if (layer2.source.kind === "osc") assign(oscillators[index].frequency, layer2.source.frequency, target.time + layer2.at);
1656
1778
  });
1657
1779
  applyEnvelope(gain.gain, layers[0].envelope, target.time + layers[0].at);
1658
1780
  for (const oscillator of oscillators) oscillator.connect(gain);
1659
1781
  gain.connect(target.output);
1660
- layers.forEach((layer, index) => oscillators[index].start(target.time + layer.at));
1661
- layers.forEach((layer, index) => oscillators[index].stop(end(layer, target.time + layer.at)));
1782
+ layers.forEach((layer2, index) => oscillators[index].start(target.time + layer2.at));
1783
+ layers.forEach((layer2, index) => oscillators[index].stop(end(layer2, target.time + layer2.at)));
1662
1784
  return voiceOf(oscillators);
1663
1785
  }
1664
1786
  function realizeCalls(plan, target) {
@@ -1670,34 +1792,34 @@ var GameAudio = (() => {
1670
1792
  if (band) filter.type = band.type;
1671
1793
  filter.connect(gain);
1672
1794
  gain.connect(target.output);
1673
- const oscillators = plan.layers.map((layer) => {
1674
- const time = target.time + layer.at;
1795
+ const oscillators = plan.layers.map((layer2) => {
1796
+ const time = target.time + layer2.at;
1675
1797
  const oscillator = context.createOscillator();
1676
- if (layer.source.kind === "osc") {
1677
- oscillator.type = layer.source.wave;
1678
- assign(oscillator.frequency, layer.source.frequency, time);
1679
- rampTo(oscillator.frequency, layer.source.frequency, time);
1798
+ if (layer2.source.kind === "osc") {
1799
+ oscillator.type = layer2.source.wave;
1800
+ assign(oscillator.frequency, layer2.source.frequency, time);
1801
+ rampTo(oscillator.frequency, layer2.source.frequency, time);
1680
1802
  }
1681
1803
  if (band) {
1682
1804
  assign(filter.frequency, band.frequency, time);
1683
1805
  if (band.q !== void 0) filter.Q.value = band.q;
1684
1806
  }
1685
1807
  const envelope2 = context.createGain();
1686
- applyEnvelope(envelope2.gain, layer.envelope, time);
1808
+ applyEnvelope(envelope2.gain, layer2.envelope, time);
1687
1809
  oscillator.connect(envelope2);
1688
1810
  envelope2.connect(filter);
1689
1811
  oscillator.start(time);
1690
- oscillator.stop(end(layer, time));
1812
+ oscillator.stop(end(layer2, time));
1691
1813
  return oscillator;
1692
1814
  });
1693
1815
  return voiceOf(oscillators);
1694
1816
  }
1695
1817
 
1696
1818
  // src/webaudio/realize-layered.ts
1697
- var end2 = (layer, start) => start + layer.duration + layer.tail;
1698
- function outputStage2(plan, layer, target) {
1819
+ var end2 = (layer2, start) => start + layer2.duration + layer2.tail;
1820
+ function outputStage2(plan, layer2, target) {
1699
1821
  const { context } = target;
1700
- const level = gainNode(context, layer.level);
1822
+ const level = gainNode(context, layer2.level);
1701
1823
  let node = level;
1702
1824
  if (plan.pan !== void 0 && typeof context.createStereoPanner === "function") {
1703
1825
  const panner = context.createStereoPanner();
@@ -1706,54 +1828,54 @@ var GameAudio = (() => {
1706
1828
  node = panner;
1707
1829
  }
1708
1830
  node.connect(target.output);
1709
- if (layer.wet && target.wet) {
1710
- const send = gainNode(context, layer.wet);
1831
+ if (layer2.wet && target.wet) {
1832
+ const send = gainNode(context, layer2.wet);
1711
1833
  node.connect(send);
1712
1834
  send.connect(target.wet);
1713
1835
  }
1714
1836
  return level;
1715
1837
  }
1716
- function realizeNoiseLayer(plan, layer, target) {
1838
+ function realizeNoiseLayer(plan, layer2, target) {
1717
1839
  const { context } = target;
1718
- const time = target.time + layer.at;
1719
- const source = noiseSource(target, layer.source.kind === "noise" && layer.source.loop);
1720
- const filters = (layer.filters?.bank ?? []).map((filter) => {
1840
+ const time = target.time + layer2.at;
1841
+ const source = noiseSource(target, layer2.source.kind === "noise" && layer2.source.loop);
1842
+ const filters = (layer2.filters?.bank ?? []).map((filter) => {
1721
1843
  const node = filterNode(context, filter, time);
1722
1844
  rampTo(node.frequency, filter.frequency, time);
1723
1845
  return node;
1724
1846
  });
1725
1847
  const envelope2 = context.createGain();
1726
- applyEnvelope(envelope2.gain, layer.envelope, time);
1848
+ applyEnvelope(envelope2.gain, layer2.envelope, time);
1727
1849
  let tail = source;
1728
1850
  for (const filter of filters) tail = tail.connect(filter);
1729
1851
  tail.connect(envelope2);
1730
- envelope2.connect(outputStage2(plan, layer, target));
1731
- source.start(time, layer.source.kind === "noise" ? layer.source.offset : 0);
1732
- source.stop(end2(layer, time));
1852
+ envelope2.connect(outputStage2(plan, layer2, target));
1853
+ source.start(time, layer2.source.kind === "noise" ? layer2.source.offset : 0);
1854
+ source.stop(end2(layer2, time));
1733
1855
  return [source];
1734
1856
  }
1735
- function realizeToneLayer(plan, layer, target) {
1857
+ function realizeToneLayer(plan, layer2, target) {
1736
1858
  const { context } = target;
1737
- const time = target.time + layer.at;
1859
+ const time = target.time + layer2.at;
1738
1860
  const oscillator = context.createOscillator();
1739
- if (layer.source.kind === "osc") {
1740
- oscillator.type = layer.source.wave;
1741
- assign(oscillator.frequency, layer.source.frequency, time);
1742
- rampTo(oscillator.frequency, layer.source.frequency, time);
1861
+ if (layer2.source.kind === "osc") {
1862
+ oscillator.type = layer2.source.wave;
1863
+ assign(oscillator.frequency, layer2.source.frequency, time);
1864
+ rampTo(oscillator.frequency, layer2.source.frequency, time);
1743
1865
  }
1744
1866
  const envelope2 = context.createGain();
1745
- applyEnvelope(envelope2.gain, layer.envelope, time);
1867
+ applyEnvelope(envelope2.gain, layer2.envelope, time);
1746
1868
  oscillator.connect(envelope2);
1747
- envelope2.connect(outputStage2(plan, layer, target));
1869
+ envelope2.connect(outputStage2(plan, layer2, target));
1748
1870
  oscillator.start(time);
1749
- oscillator.stop(end2(layer, time));
1871
+ oscillator.stop(end2(layer2, time));
1750
1872
  return [oscillator];
1751
1873
  }
1752
- function realizeVoiceLayer(plan, layer, target) {
1753
- const source = layer.source;
1874
+ function realizeVoiceLayer(plan, layer2, target) {
1875
+ const source = layer2.source;
1754
1876
  if (source.kind !== "formant_voice") return [];
1755
1877
  const { context } = target;
1756
- const time = target.time + layer.at;
1878
+ const time = target.time + layer2.at;
1757
1879
  const oscillator = context.createOscillator();
1758
1880
  oscillator.type = source.wave;
1759
1881
  assign(oscillator.frequency, source.frequency, time);
@@ -1774,10 +1896,10 @@ var GameAudio = (() => {
1774
1896
  for (const formant of formants) formant.connect(mix);
1775
1897
  breathLevel.connect(mix);
1776
1898
  const envelope2 = context.createGain();
1777
- applyEnvelope(envelope2.gain, layer.envelope, time);
1899
+ applyEnvelope(envelope2.gain, layer2.envelope, time);
1778
1900
  mix.connect(envelope2);
1779
- envelope2.connect(outputStage2(plan, layer, target));
1780
- const stop = end2(layer, time);
1901
+ envelope2.connect(outputStage2(plan, layer2, target));
1902
+ const stop = end2(layer2, time);
1781
1903
  oscillator.start(time);
1782
1904
  vibrato.start(time);
1783
1905
  breath.start(time, source.breath.offset);
@@ -1787,24 +1909,24 @@ var GameAudio = (() => {
1787
1909
  return [oscillator, vibrato, breath];
1788
1910
  }
1789
1911
  function realizeLayered(plan, target) {
1790
- const sources = plan.layers.flatMap((layer) => {
1791
- if (layer.source.kind === "noise") return realizeNoiseLayer(plan, layer, target);
1792
- if (layer.source.kind === "osc") return realizeToneLayer(plan, layer, target);
1793
- return realizeVoiceLayer(plan, layer, target);
1912
+ const sources = plan.layers.flatMap((layer2) => {
1913
+ if (layer2.source.kind === "noise") return realizeNoiseLayer(plan, layer2, target);
1914
+ if (layer2.source.kind === "osc") return realizeToneLayer(plan, layer2, target);
1915
+ return realizeVoiceLayer(plan, layer2, target);
1794
1916
  });
1795
1917
  return voiceOf(sources);
1796
1918
  }
1797
1919
  function realizeSiren(plan, target) {
1798
1920
  const { context } = target;
1799
- const layer = plan.layers[0];
1800
- const time = target.time + layer.at;
1921
+ const layer2 = plan.layers[0];
1922
+ const time = target.time + layer2.at;
1801
1923
  const oscillator = context.createOscillator();
1802
- if (layer.source.kind === "osc") {
1803
- oscillator.type = layer.source.wave;
1804
- assign(oscillator.frequency, layer.source.frequency, time);
1805
- rampTo(oscillator.frequency, layer.source.frequency, time);
1924
+ if (layer2.source.kind === "osc") {
1925
+ oscillator.type = layer2.source.wave;
1926
+ assign(oscillator.frequency, layer2.source.frequency, time);
1927
+ rampTo(oscillator.frequency, layer2.source.frequency, time);
1806
1928
  }
1807
- const depths = layer.modulators.map((modulator) => {
1929
+ const depths = layer2.modulators.map((modulator) => {
1808
1930
  const lfo = context.createOscillator();
1809
1931
  if (modulator.wave) lfo.type = modulator.wave;
1810
1932
  lfo.frequency.value = modulator.rate;
@@ -1815,19 +1937,19 @@ var GameAudio = (() => {
1815
1937
  });
1816
1938
  const lfos = depths.map(({ lfo }) => lfo);
1817
1939
  const envelope2 = context.createGain();
1818
- applyEnvelope(envelope2.gain, layer.envelope, time);
1940
+ applyEnvelope(envelope2.gain, layer2.envelope, time);
1819
1941
  for (const { depth, target: modulated } of depths) if (modulated === "gain") depth.connect(envelope2.gain);
1820
- const filters = (layer.filters?.bank ?? []).map((filter) => filterNode(context, filter, time));
1942
+ const filters = (layer2.filters?.bank ?? []).map((filter) => filterNode(context, filter, time));
1821
1943
  let tail = oscillator;
1822
1944
  for (const filter of filters) tail = tail.connect(filter);
1823
1945
  tail.connect(envelope2);
1824
1946
  envelope2.connect(target.output);
1825
- if (layer.wet && target.wet) {
1826
- const send = gainNode(context, layer.wet);
1947
+ if (layer2.wet && target.wet) {
1948
+ const send = gainNode(context, layer2.wet);
1827
1949
  envelope2.connect(send);
1828
1950
  send.connect(target.wet);
1829
1951
  }
1830
- const stop = end2(layer, time);
1952
+ const stop = end2(layer2, time);
1831
1953
  oscillator.start(time);
1832
1954
  for (const lfo of lfos) lfo.start(time);
1833
1955
  oscillator.stop(stop);
@@ -1836,38 +1958,38 @@ var GameAudio = (() => {
1836
1958
  }
1837
1959
 
1838
1960
  // src/webaudio/realize-weapon.ts
1839
- function noiseLayer(layer, target, time) {
1961
+ function noiseLayer(layer2, target, time) {
1840
1962
  const { context } = target;
1841
- const source = noiseSource(target, layer.source.kind === "noise" && layer.source.loop);
1842
- const filters = (layer.filters?.bank ?? []).map((filter) => filterNode(context, filter, time));
1963
+ const source = noiseSource(target, layer2.source.kind === "noise" && layer2.source.loop);
1964
+ const filters = (layer2.filters?.bank ?? []).map((filter) => filterNode(context, filter, time));
1843
1965
  const envelope2 = context.createGain();
1844
- applyEnvelope(envelope2.gain, layer.envelope, time);
1966
+ applyEnvelope(envelope2.gain, layer2.envelope, time);
1845
1967
  let tail = source;
1846
1968
  for (const filter of filters) tail = tail.connect(filter);
1847
1969
  tail.connect(envelope2).connect(target.output);
1848
- source.start(time, layer.source.kind === "noise" ? layer.source.offset : 0);
1849
- source.stop(time + layer.duration + layer.tail);
1970
+ source.start(time, layer2.source.kind === "noise" ? layer2.source.offset : 0);
1971
+ source.stop(time + layer2.duration + layer2.tail);
1850
1972
  return source;
1851
1973
  }
1852
- function toneLayer(layer, target, time) {
1974
+ function toneLayer(layer2, target, time) {
1853
1975
  const { context } = target;
1854
1976
  const oscillator = context.createOscillator();
1855
- if (layer.source.kind === "osc") {
1856
- oscillator.type = layer.source.wave;
1857
- assign(oscillator.frequency, layer.source.frequency, time);
1858
- rampTo(oscillator.frequency, layer.source.frequency, time);
1977
+ if (layer2.source.kind === "osc") {
1978
+ oscillator.type = layer2.source.wave;
1979
+ assign(oscillator.frequency, layer2.source.frequency, time);
1980
+ rampTo(oscillator.frequency, layer2.source.frequency, time);
1859
1981
  }
1860
1982
  const envelope2 = context.createGain();
1861
- applyEnvelope(envelope2.gain, layer.envelope, time);
1983
+ applyEnvelope(envelope2.gain, layer2.envelope, time);
1862
1984
  oscillator.connect(envelope2).connect(target.output);
1863
1985
  oscillator.start(time);
1864
- oscillator.stop(time + layer.duration + layer.tail);
1986
+ oscillator.stop(time + layer2.duration + layer2.tail);
1865
1987
  return oscillator;
1866
1988
  }
1867
1989
  function realizeWeapon(plan, target) {
1868
- const sources = plan.layers.map((layer) => {
1869
- const time = target.time + layer.at;
1870
- return layer.source.kind === "noise" ? noiseLayer(layer, target, time) : toneLayer(layer, target, time);
1990
+ const sources = plan.layers.map((layer2) => {
1991
+ const time = target.time + layer2.at;
1992
+ return layer2.source.kind === "noise" ? noiseLayer(layer2, target, time) : toneLayer(layer2, target, time);
1871
1993
  });
1872
1994
  return voiceOf(sources);
1873
1995
  }
@@ -1919,19 +2041,19 @@ var GameAudio = (() => {
1919
2041
  rate: modulator.rate * rate,
1920
2042
  ...modulator.target === "frequency" ? { depth: modulator.depth * rate } : {}
1921
2043
  });
1922
- var tuneLayer = (layer, rate) => ({
1923
- ...layer,
1924
- at: layer.at / rate,
1925
- duration: layer.duration / rate,
1926
- tail: layer.tail / rate,
1927
- source: tuneSource(layer.source, rate),
1928
- ...layer.filters ? { filters: { ...layer.filters, bank: layer.filters.bank.map((filter) => tuneFilter(filter, rate)) } } : {},
1929
- modulators: layer.modulators.map((modulator) => tuneModulator(modulator, rate)),
1930
- envelope: layer.envelope.map((point) => ({ ...point, time: point.time / rate }))
2044
+ var tuneLayer = (layer2, rate) => ({
2045
+ ...layer2,
2046
+ at: layer2.at / rate,
2047
+ duration: layer2.duration / rate,
2048
+ tail: layer2.tail / rate,
2049
+ source: tuneSource(layer2.source, rate),
2050
+ ...layer2.filters ? { filters: { ...layer2.filters, bank: layer2.filters.bank.map((filter) => tuneFilter(filter, rate)) } } : {},
2051
+ modulators: layer2.modulators.map((modulator) => tuneModulator(modulator, rate)),
2052
+ envelope: layer2.envelope.map((point) => ({ ...point, time: point.time / rate }))
1931
2053
  });
1932
2054
  function retunePlan(plan, rate) {
1933
2055
  if (rate === 1 || !(rate > 0)) return plan;
1934
- return { ...plan, length: plan.length / rate, layers: plan.layers.map((layer) => tuneLayer(layer, rate)) };
2056
+ return { ...plan, length: plan.length / rate, layers: plan.layers.map((layer2) => tuneLayer(layer2, rate)) };
1935
2057
  }
1936
2058
  function createSetPicker() {
1937
2059
  const last = /* @__PURE__ */ new Map();
@@ -2034,6 +2156,7 @@ var GameAudio = (() => {
2034
2156
  }
2035
2157
 
2036
2158
  // src/webaudio/realize-sample.ts
2159
+ var TRIM_FADE = 0.04;
2037
2160
  function realizeSample(plan, target) {
2038
2161
  const context = target.context;
2039
2162
  let head = target.output;
@@ -2055,16 +2178,26 @@ var GameAudio = (() => {
2055
2178
  voice2.connect(head);
2056
2179
  head = voice2;
2057
2180
  const sources = [];
2058
- for (const layer of plan.layers) {
2059
- const buffer = target.buffer(layer.url);
2181
+ for (const layer2 of plan.layers) {
2182
+ const buffer = target.buffer(layer2.url);
2060
2183
  if (!buffer) continue;
2061
2184
  const source = context.createBufferSource();
2062
2185
  source.buffer = buffer;
2063
- source.playbackRate.value = layer.rate;
2064
- const gain = gainNode(context, layer.gain);
2186
+ source.playbackRate.value = layer2.rate;
2187
+ const gain = gainNode(context, layer2.gain);
2065
2188
  source.connect(gain);
2066
2189
  gain.connect(head);
2067
- source.start(target.time + layer.at);
2190
+ const at = target.time + layer2.at;
2191
+ const offset = Math.max(0, Math.min(layer2.offset ?? 0, buffer.duration));
2192
+ if (layer2.duration === void 0) source.start(at, offset);
2193
+ else {
2194
+ const duration = Math.max(0, Math.min(layer2.duration, buffer.duration - offset));
2195
+ const end3 = at + duration / layer2.rate;
2196
+ const fade = Math.min(TRIM_FADE, (end3 - at) / 4);
2197
+ gain.gain.setValueAtTime(layer2.gain, end3 - fade);
2198
+ gain.gain.linearRampToValueAtTime(0, end3);
2199
+ source.start(at, offset, duration);
2200
+ }
2068
2201
  sources.push(source);
2069
2202
  }
2070
2203
  const realized = voiceOf(sources);
@@ -2084,7 +2217,11 @@ var GameAudio = (() => {
2084
2217
  }
2085
2218
  function sampleLength(plan, seconds) {
2086
2219
  let length = 0;
2087
- for (const layer of plan.layers) length = Math.max(length, layer.at + (seconds(layer.url) ?? 0) / layer.rate);
2220
+ for (const layer2 of plan.layers) {
2221
+ const file = seconds(layer2.url) ?? 0;
2222
+ const played = layer2.duration === void 0 ? file - (layer2.offset ?? 0) : Math.min(layer2.duration, file - (layer2.offset ?? 0));
2223
+ length = Math.max(length, layer2.at + Math.max(0, played) / layer2.rate);
2224
+ }
2088
2225
  return length;
2089
2226
  }
2090
2227
 
@@ -2100,8 +2237,8 @@ var GameAudio = (() => {
2100
2237
  }
2101
2238
  return curve;
2102
2239
  }
2103
- var usesNoise = (recipe2) => recipe2.layers.some((layer) => layer.source.kind !== "osc");
2104
- var planUsesNoise = (plan) => plan.layers.some((layer) => layer.source.kind !== "osc");
2240
+ var usesNoise = (recipe2) => recipe2.layers.some((layer2) => layer2.source.kind !== "osc");
2241
+ var planUsesNoise = (plan) => plan.layers.some((layer2) => layer2.source.kind !== "osc");
2105
2242
  function createAudioEngine(options = {}) {
2106
2243
  const seed = options.seed ?? 1;
2107
2244
  const registry = createSoundRegistry(options.banks);
@@ -2293,6 +2430,7 @@ var GameAudio = (() => {
2293
2430
  const set = registry.set(id);
2294
2431
  return set ? chooser.pick(set, random) : { id, rate: 1 };
2295
2432
  };
2433
+ const wanted = /* @__PURE__ */ new Set();
2296
2434
  const loadSounds = (sounds) => {
2297
2435
  const audio = ready();
2298
2436
  if (!audio) return Promise.resolve({ loaded: [], failed: sounds.flatMap(sampleUrls).map((url2) => ({ url: url2, error: "no audio context" })) });
@@ -2340,7 +2478,7 @@ var GameAudio = (() => {
2340
2478
  level: plan.level,
2341
2479
  distance: plan.distance,
2342
2480
  ...plan.pan === void 0 ? {} : { pan: plan.pan },
2343
- layers: plan.layers.map((layer) => ({ role: layer.role, url: layer.url, take: layer.take, rate: layer.rate })),
2481
+ layers: plan.layers.map((layer2) => ({ role: layer2.role, url: layer2.url, take: layer2.take, rate: layer2.rate })),
2344
2482
  ...plan.tier ? { tier: plan.tier } : {}
2345
2483
  });
2346
2484
  voices.setLength(voiceId, sampleLength(plan, (url2) => sampleStore.seconds(url2)) + (playOptions.delay ?? 0));
@@ -2417,13 +2555,20 @@ var GameAudio = (() => {
2417
2555
  };
2418
2556
  return {
2419
2557
  registry,
2420
- register: (bank) => registry.register(bank),
2558
+ register: (bank) => {
2559
+ registry.register(bank);
2560
+ if (wanted.size === 0) return;
2561
+ const now = [...wanted].map((id) => registry.sample(id)).filter((sample) => sample !== void 0);
2562
+ for (const sample of now) wanted.delete(sample.id);
2563
+ if (now.length > 0) void loadSounds(now).catch(() => void 0);
2564
+ },
2421
2565
  plan: (id, playOptions = {}) => planWith(id, playOptions, createRng(playOptions.seed ?? seed)),
2422
2566
  planSample(id, playOptions = {}) {
2423
2567
  const sample = registry.sample(id);
2424
2568
  return sample ? planSample(sample, sampleInput(playOptions, createRng(playOptions.seed ?? seed), false)) : null;
2425
2569
  },
2426
2570
  loadSamples(ids) {
2571
+ for (const id of ids ?? []) if (!registry.sample(id)) wanted.add(id);
2427
2572
  const chosen = (ids ?? registry.sampleIds()).map((id) => registry.sample(id)).filter((sample) => sample !== void 0);
2428
2573
  return loadSounds(chosen);
2429
2574
  },
@@ -2473,7 +2618,9 @@ var GameAudio = (() => {
2473
2618
  random: engineRandom,
2474
2619
  register: loopHost.register,
2475
2620
  unregister: loopHost.unregister,
2476
- muted: (bus) => busSettings.get(bus)?.muted === true
2621
+ muted: (bus) => busSettings.get(bus)?.muted === true,
2622
+ listener: () => listener,
2623
+ ...options.distance ? { distance: options.distance } : {}
2477
2624
  },
2478
2625
  loopOptions.volume
2479
2626
  );
@@ -2787,12 +2934,12 @@ var GameAudio = (() => {
2787
2934
  brightness.push(onset < 0 ? 0 : centroidHz(result.channels, result.sampleRate, onset));
2788
2935
  }
2789
2936
  const far = planSample(sound, { random: createRng(1), distance: CRITERIA_TARGETS.distances.at(-1) });
2790
- const switched = (far?.farMix ?? 0) === 1 && (far?.layers.some((layer) => layer.role === "far") ?? false);
2937
+ const switched = (far?.farMix ?? 0) === 1 && (far?.layers.some((layer2) => layer2.role === "far") ?? false);
2791
2938
  const pass = decreasing(levels) && decreasing(brightness) && switched;
2792
2939
  return {
2793
2940
  id: "C3",
2794
2941
  pass,
2795
- value: `${levels.map((level) => db(level)).join(" \u203A ")}; ${brightness.map((hz) => `${Math.round(hz)} Hz`).join(" \u203A ")}${switched ? "; longe = far" : "; sem camada far"}`,
2942
+ value: `${levels.map((level) => db(level)).join(" \u203A ")}; ${brightness.map((hz2) => `${Math.round(hz2)} Hz`).join(" \u203A ")}${switched ? "; longe = far" : "; sem camada far"}`,
2796
2943
  target: "cai e escurece com a dist\xE2ncia; troca para a vers\xE3o de longe depois de ~50 m",
2797
2944
  data: { distances: CRITERIA_TARGETS.distances, levels, brightness, switched }
2798
2945
  };
@@ -2827,11 +2974,11 @@ var GameAudio = (() => {
2827
2974
  for (let index = 0; index < CRITERIA_TARGETS.plays; index++) if (engine.play(options.sound, { distance: 1, delay: 0.1 + index * 0.3 })) played++;
2828
2975
  return played;
2829
2976
  });
2830
- const close = result.events.map((event) => event.layers?.find((layer) => layer.role === "close"));
2831
- const takes2 = close.map((layer) => layer?.url ?? "");
2977
+ const close = result.events.map((event) => event.layers?.find((layer2) => layer2.role === "close"));
2978
+ const takes2 = close.map((layer2) => layer2?.url ?? "");
2832
2979
  const repeats = takes2.filter((take, index) => index > 0 && take === takes2[index - 1]).length;
2833
2980
  const distinct = new Set(takes2).size;
2834
- const rates = close.map((layer) => layer?.rate ?? 1);
2981
+ const rates = close.map((layer2) => layer2?.rate ?? 1);
2835
2982
  const spread = (Math.max(...rates) - Math.min(...rates)) / 2;
2836
2983
  const pitch = sound.pitch ?? 0.04;
2837
2984
  const pass = distinct >= CRITERIA_TARGETS.minTakes && repeats === 0 && pitch >= CRITERIA_TARGETS.pitch[0] && pitch <= CRITERIA_TARGETS.pitch[1] && spread > 0.01;
@@ -2869,7 +3016,7 @@ var GameAudio = (() => {
2869
3016
  const result = await render(options, 1.2, (engine) => engine.play(options.sound, { distance: 1, seed: 1, intensity, delay: EVENT_AT }) ? 1 : 0);
2870
3017
  const onset = onsetIndex(result.channels, 0, 1e-5);
2871
3018
  const event = result.events[0];
2872
- return { tier: event?.tier ?? "", rms: onset < 0 ? -Infinity : rmsDb(result.channels, onset, onset + result.sampleRate * 0.3), files: event?.layers?.find((layer) => layer.role === "close")?.url ?? "" };
3019
+ return { tier: event?.tier ?? "", rms: onset < 0 ? -Infinity : rmsDb(result.channels, onset, onset + result.sampleRate * 0.3), files: event?.layers?.find((layer2) => layer2.role === "close")?.url ?? "" };
2873
3020
  };
2874
3021
  const light = await at(0.1);
2875
3022
  const heavy = await at(0.9);
@@ -3212,9 +3359,9 @@ var GameAudio = (() => {
3212
3359
  function parseSfxName(name) {
3213
3360
  const match = /^(.*?)(?:_(close|far|tail_in|tail_out))?(?:_(\d+))?$/.exec(name);
3214
3361
  const id = match?.[1] || name;
3215
- const layer = match?.[2] ?? "close";
3362
+ const layer2 = match?.[2] ?? "close";
3216
3363
  const take = match?.[3] ? Number(match[3]) : 1;
3217
- return { id, layer, take };
3364
+ return { id, layer: layer2, take };
3218
3365
  }
3219
3366
  function splitTier(id) {
3220
3367
  const match = /^(.*)_(light|medium|heavy)$/.exec(id);
@@ -3290,7 +3437,16 @@ var GameAudio = (() => {
3290
3437
  // a drawer stays on its recipe: the recorded runs (1 to 1.9 s) outlast a drawer's pull
3291
3438
  // an item taken in the hand (game-destruction-three grab_* keys)
3292
3439
  grab_hard: "item_pickup_generic",
3293
- grab_soft: "item_pickup_leather"
3440
+ grab_soft: "item_pickup_leather",
3441
+ // the hand foley of the test room (game-audio FOLEY_BANK) whose id is not a file id: a wooden leaf shut and slammed, an item picked up by material.
3442
+ // The metal door stays on its recipe (two of its takes run 10 s), and so do the fridge, the microwave and paper (no file for them).
3443
+ door_wood_close: "door_wood_shut",
3444
+ door_wood_slam: "door_slam_wood",
3445
+ item_pickup_metal: "item_pickup_generic",
3446
+ item_pickup_plastic: "item_pickup_generic",
3447
+ item_pickup_glass: "item_pickup_generic",
3448
+ item_pickup_wood: "item_pickup_generic",
3449
+ item_pickup_fabric: "item_pickup_leather"
3294
3450
  };
3295
3451
  function kitSampleDefaults(sound) {
3296
3452
  const bus = VOICE_IDS.test(sound.id) ? "voices" : KIT_CATEGORY_BUS[sound.category];
@@ -3340,6 +3496,20 @@ var GameAudio = (() => {
3340
3496
  if (options.preload && engine.loadSamples) void engine.loadSamples(options.preload).catch(() => void 0);
3341
3497
  return bank;
3342
3498
  }
3499
+ function pointSamples(engine, files, options = {}) {
3500
+ const baseUrl = options.baseUrl ?? "";
3501
+ const samples = Object.entries(files).map(([id, urls]) => {
3502
+ const close = (typeof urls === "string" ? [urls] : [...urls]).map((url2) => ({ url: `${baseUrl}${url2}` }));
3503
+ if (close.length === 0) throw new Error(`pointSamples: ${id} has no file`);
3504
+ const kit = engine.registry.sample(id);
3505
+ if (!kit) return { id, bus: options.bus ?? "world", close, sources: ["game: pointSamples"] };
3506
+ const { far: _far, tiers: _tiers, tail: _tail, ...numbers } = kit;
3507
+ return { ...numbers, close };
3508
+ });
3509
+ const bank = { id: options.id ?? "game_samples", samples };
3510
+ engine.register(bank);
3511
+ return bank;
3512
+ }
3343
3513
 
3344
3514
  // src/user-samples.ts
3345
3515
  var USER_SAMPLES_BANK = "user_samples";
@@ -3394,6 +3564,145 @@ var GameAudio = (() => {
3394
3564
  return result;
3395
3565
  }
3396
3566
 
3567
+ // src/local-pack.ts
3568
+ var LOCAL_PACK_LICENSES = ["CC0", "CC-BY", "unknown", "proprietary"];
3569
+ var LOCAL_PACK_PREFERENCES = ["pack", "sdk"];
3570
+ var LocalSoundPackError = class extends Error {
3571
+ constructor(problems) {
3572
+ super(`invalid local sound pack: ${problems.join("; ")}`);
3573
+ __publicField(this, "problems");
3574
+ this.name = "LocalSoundPackError";
3575
+ this.problems = problems;
3576
+ }
3577
+ };
3578
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3579
+ var isLicense = (value) => LOCAL_PACK_LICENSES.includes(value);
3580
+ var isBus = (value) => BUS_IDS.includes(value);
3581
+ function entryProblems(id, entry) {
3582
+ if (!isRecord(entry)) return [`${id}: not an object`];
3583
+ const problems = [];
3584
+ const files = entry.files;
3585
+ if (!Array.isArray(files) || files.length === 0 || !files.every((file) => typeof file === "string" && file.length > 0)) problems.push(`${id}: files must be a non-empty list of paths`);
3586
+ if (!isLicense(entry.license)) problems.push(`${id}: license must be one of ${LOCAL_PACK_LICENSES.join(", ")}`);
3587
+ if (entry.prefer !== void 0 && !LOCAL_PACK_PREFERENCES.includes(entry.prefer)) problems.push(`${id}: prefer must be pack or sdk`);
3588
+ if (entry.bus !== void 0 && !isBus(entry.bus)) problems.push(`${id}: unknown bus ${String(entry.bus)}`);
3589
+ if (entry.gain !== void 0 && (typeof entry.gain !== "number" || entry.gain < 0)) problems.push(`${id}: gain must be a number \u2265 0`);
3590
+ if (entry.score !== void 0 && (typeof entry.score !== "number" || entry.score < 0 || entry.score > 10)) problems.push(`${id}: score must be 0\u201310`);
3591
+ return problems;
3592
+ }
3593
+ function parseLocalSoundPack(value) {
3594
+ if (!isRecord(value)) throw new LocalSoundPackError(["the pack is not an object"]);
3595
+ const problems = [];
3596
+ if (value.version !== 1) problems.push("version must be 1");
3597
+ if (value.baseUrl !== void 0 && typeof value.baseUrl !== "string") problems.push("baseUrl must be a string");
3598
+ if (!isRecord(value.sounds)) problems.push("sounds must be an object of id \u2192 entry");
3599
+ else for (const [id, entry] of Object.entries(value.sounds)) problems.push(...entryProblems(id, entry));
3600
+ if (problems.length > 0) throw new LocalSoundPackError(problems);
3601
+ return value;
3602
+ }
3603
+ var isShippableLicense = (license) => SDK_LICENSES.includes(license);
3604
+ var CATALOG = new Map([...SFX_CATALOG, ...SFX_IMPACTS].map((entry) => [entry.id, entry]));
3605
+ var busOf = (id, entry) => {
3606
+ if (entry.bus) return entry.bus;
3607
+ const category = CATALOG.get(id)?.category ?? "";
3608
+ return kitSampleDefaults({ id, category })?.bus ?? "world";
3609
+ };
3610
+ function localPackSamples(pack, options = {}) {
3611
+ const baseUrl = options.baseUrl ?? pack.baseUrl ?? "";
3612
+ return Object.entries(pack.sounds).map(([id, entry]) => {
3613
+ const current = CATALOG.get(id)?.current;
3614
+ const isLoop = /_loop(_|$)/.test(id);
3615
+ return {
3616
+ id,
3617
+ bus: busOf(id, entry),
3618
+ close: entry.files.map((file) => ({ url: `${baseUrl}${file}` })),
3619
+ pitch: isLoop ? 0 : 0.04,
3620
+ ...isLoop ? { loop: true } : {},
3621
+ ...entry.gain === void 0 ? {} : { gain: entry.gain },
3622
+ ...current ? { fallback: current } : {},
3623
+ license: entry.license,
3624
+ sources: entry.files.map((file) => `local: ${file} ${entry.license}`)
3625
+ };
3626
+ });
3627
+ }
3628
+
3629
+ // src/local-pack-loader.ts
3630
+ var LOCAL_PACK_BANK_ID = "local_sound_pack";
3631
+ var STAGING_PREFIX = "local_pack_staging:";
3632
+ var defaultIdleScheduler = (task) => {
3633
+ const idle = globalThis;
3634
+ if (idle.requestIdleCallback && idle.cancelIdleCallback) {
3635
+ const handle2 = idle.requestIdleCallback(task);
3636
+ const cancel = idle.cancelIdleCallback;
3637
+ return () => cancel(handle2);
3638
+ }
3639
+ const handle = setTimeout(task, 0);
3640
+ return () => clearTimeout(handle);
3641
+ };
3642
+ function applyLocalSoundPack(engine, pack, options = {}) {
3643
+ const schedule = options.schedule ?? defaultIdleScheduler;
3644
+ const samples = localPackSamples(pack, options);
3645
+ const statuses = new Map(samples.map((sample) => [sample.id, "pending"]));
3646
+ const priors = /* @__PURE__ */ new Map();
3647
+ const report = { file: [], sdk: [], fallback: [] };
3648
+ const queue = [...samples];
3649
+ let cancel;
3650
+ let disposed = false;
3651
+ let finish = () => void 0;
3652
+ const done = new Promise((resolve) => {
3653
+ finish = resolve;
3654
+ });
3655
+ const swapIn = (sample) => {
3656
+ const prior = engine.registry.sample(sample.id);
3657
+ const bank = engine.registry.bankOf(sample.id);
3658
+ priors.set(sample.id, { ...prior ? { sample: prior } : {}, ...bank ? { bank } : {} });
3659
+ engine.register({ id: LOCAL_PACK_BANK_ID, samples: [{ ...sample, ...prior?.fallback ? { fallback: prior.fallback } : {} }] });
3660
+ statuses.set(sample.id, "file");
3661
+ report.file.push(sample.id);
3662
+ };
3663
+ const loadOne = async (sample) => {
3664
+ if (pack.sounds[sample.id]?.prefer === "sdk" && engine.registry.sample(sample.id)) {
3665
+ statuses.set(sample.id, "sdk");
3666
+ report.sdk.push(sample.id);
3667
+ return;
3668
+ }
3669
+ const staging = `${STAGING_PREFIX}${sample.id}`;
3670
+ engine.register({ id: LOCAL_PACK_BANK_ID, samples: [{ ...sample, id: staging }] });
3671
+ const loaded = await engine.loadSamples([staging]);
3672
+ engine.registry.removeSample(staging);
3673
+ if (disposed) return;
3674
+ if (loaded.failed.length === 0) return swapIn(sample);
3675
+ statuses.set(sample.id, "fallback");
3676
+ report.fallback.push({ id: sample.id, error: loaded.failed.map((entry) => `${entry.url}: ${entry.error}`).join("; ") });
3677
+ };
3678
+ const next = () => {
3679
+ const sample = queue.shift();
3680
+ if (!sample || disposed) {
3681
+ if (!disposed) finish(report);
3682
+ return;
3683
+ }
3684
+ void loadOne(sample).finally(() => {
3685
+ if (!disposed) cancel = schedule(next);
3686
+ });
3687
+ };
3688
+ cancel = schedule(next);
3689
+ return {
3690
+ done,
3691
+ status: (id) => statuses.get(id),
3692
+ unlicensed: () => samples.filter((sample) => statuses.get(sample.id) === "file" && !isShippableLicense(sample.license)).map((sample) => sample.id),
3693
+ dispose() {
3694
+ disposed = true;
3695
+ cancel?.();
3696
+ for (const [id, prior] of priors) {
3697
+ if (prior.sample) engine.register({ id: prior.bank ?? LOCAL_PACK_BANK_ID, samples: [prior.sample] });
3698
+ else engine.registry.removeSample(id);
3699
+ }
3700
+ priors.clear();
3701
+ finish(report);
3702
+ }
3703
+ };
3704
+ }
3705
+
3397
3706
  // src/data/game-sfx.json
3398
3707
  var game_sfx_default = {
3399
3708
  sources: ["jogo:L62089-62107"],
@@ -3693,10 +4002,140 @@ var GameAudio = (() => {
3693
4002
  duration: 0.5,
3694
4003
  layers: [{ source: { kind: "osc", wave: "sine", frequency: { set: 880 } }, repeat: { count: 3, spacing: 0.7 }, envelope: [[0, 0.06, "set"], [0.5, 1e-4, "exp", "fixed"]] }],
3695
4004
  falloff: { model: "none" }
4005
+ },
4006
+ {
4007
+ // the parking sensor: a short high beep, repeated by the cabin faster as the obstacle comes close
4008
+ id: "parking_beep",
4009
+ bus: "world",
4010
+ duration: 0.09,
4011
+ layers: [{ source: { kind: "osc", wave: "sine", frequency: { set: 2300 } }, envelope: [[0, 0.07, "set"], [0.08, 1e-4, "exp", "fixed"]] }],
4012
+ falloff: { model: "none" }
4013
+ },
4014
+ {
4015
+ // an emergency siren's wail: a tone rising and falling slowly (one sweep about every 3 s), played again by the cabin as it ends
4016
+ id: "siren_wail",
4017
+ bus: "world",
4018
+ order: "siren",
4019
+ duration: 3,
4020
+ tail: 0.05,
4021
+ layers: [
4022
+ {
4023
+ source: { kind: "osc", wave: "sawtooth", frequency: { set: 1050 } },
4024
+ modulators: [{ target: "frequency", wave: "sine", rate: 0.33, depth: 420 }],
4025
+ filters: { mode: "serial", bank: [{ type: "lowpass", frequency: { set: 2400 } }] },
4026
+ envelope: [[0, 1e-4, "set", "fixed"], [0.08, 0.07, "linear"], [2.95, 0.07, "set"], [3, 1e-4, "exp", "fixed"]]
4027
+ }
4028
+ ],
4029
+ falloff: { model: "none" }
4030
+ },
4031
+ {
4032
+ // the yelp: the same tone swept fast, for a junction
4033
+ id: "siren_yelp",
4034
+ bus: "world",
4035
+ order: "siren",
4036
+ duration: 2,
4037
+ tail: 0.05,
4038
+ layers: [
4039
+ {
4040
+ source: { kind: "osc", wave: "sawtooth", frequency: { set: 1100 } },
4041
+ modulators: [{ target: "frequency", wave: "triangle", rate: 4, depth: 380 }],
4042
+ filters: { mode: "serial", bank: [{ type: "lowpass", frequency: { set: 2600 } }] },
4043
+ envelope: [[0, 1e-4, "set", "fixed"], [0.05, 0.07, "linear"], [1.95, 0.07, "set"], [2, 1e-4, "exp", "fixed"]]
4044
+ }
4045
+ ],
4046
+ falloff: { model: "none" }
4047
+ },
4048
+ {
4049
+ // the wires under the column touching: a crackle of bright noise
4050
+ id: "hotwire_spark",
4051
+ bus: "world",
4052
+ duration: 0.12,
4053
+ layers: [
4054
+ {
4055
+ source: { kind: "noise", offset: "random" },
4056
+ filters: { mode: "serial", bank: [{ type: "highpass", frequency: { set: 3200 } }] },
4057
+ modulators: [{ target: "gain", wave: "square", rate: 60, depth: 0.1, base: 0.12 }],
4058
+ envelope: [[0, 0.2, "set"], [0.12, 1e-4, "exp", "fixed"]]
4059
+ }
4060
+ ],
4061
+ falloff: { model: "none" }
4062
+ },
4063
+ {
4064
+ // the glovebox latch: a small plastic click and the lid dropping
4065
+ id: "glovebox_latch",
4066
+ bus: "world",
4067
+ duration: 0.14,
4068
+ layers: [
4069
+ { source: { kind: "noise", offset: "random" }, filters: { mode: "serial", bank: [{ type: "bandpass", frequency: { set: 1800 }, q: 2 }] }, envelope: [[0, 0.16, "set"], [0.03, 1e-4, "exp", "fixed"]] },
4070
+ { source: { kind: "osc", wave: "triangle", frequency: { set: 240 } }, envelope: [[0, 1e-4, "set", "fixed"], [0.06, 0.08, "linear"], [0.14, 1e-4, "exp", "fixed"]] }
4071
+ ],
4072
+ falloff: { model: "none" }
3696
4073
  }
3697
4074
  ];
3698
4075
  var CABIN_BANK = { id: "vehicle_cabin", sounds: CABIN_SOUNDS, loops: [] };
3699
4076
 
4077
+ // src/library/radio.ts
4078
+ var hz = (midi) => 440 * 2 ** ((midi - 69) / 12);
4079
+ var layer = (note, beat, wave, level) => {
4080
+ const [at, midi, beats] = note;
4081
+ const length = beats * beat;
4082
+ return {
4083
+ at: at * beat,
4084
+ duration: length,
4085
+ source: { kind: "osc", wave, frequency: { set: hz(midi) } },
4086
+ envelope: [[0, 1e-4, "set"], [0.01, level, "linear"], [Math.max(0.02, length * 0.9), level * 0.35, "linear"], [length, 1e-4, "exp", "fixed"]]
4087
+ };
4088
+ };
4089
+ function radioTrack(spec) {
4090
+ const beat = 60 / spec.bpm;
4091
+ const recipes = Array.from({ length: spec.bars }, (_, bar) => {
4092
+ const inBar = (notes) => notes.filter((n) => n[0] >= bar * 4 && n[0] < bar * 4 + 4).map((n) => [n[0] - bar * 4, n[1], n[2]]);
4093
+ const layers = [
4094
+ ...inBar(spec.bass).map((n) => layer(n, beat, "triangle", 0.16)),
4095
+ ...inBar(spec.chords).map((n) => layer(n, beat, "sine", 0.05)),
4096
+ ...inBar(spec.melody).map((n) => layer(n, beat, spec.wave, 0.07))
4097
+ ];
4098
+ return { id: `${spec.id}_${bar}`, bus: "world", duration: 4 * beat, layers, falloff: { model: "none" } };
4099
+ });
4100
+ return { song: { id: spec.id, barSeconds: 4 * beat, bars: recipes.map((r) => r.id) }, recipes };
4101
+ }
4102
+ var chord = (bar, root, minor = false) => [[bar * 4, root, 4], [bar * 4, root + (minor ? 3 : 4), 4], [bar * 4, root + 7, 4]];
4103
+ var bassLine = (roots, pattern) => roots.flatMap((root, bar) => pattern.map((b) => [bar * 4 + b, root - 12, 0.9]));
4104
+ var TRACKS = [
4105
+ // a bright pop loop: I–V–vi–IV in C
4106
+ radioTrack({
4107
+ id: "radio_track_pop",
4108
+ bpm: 112,
4109
+ bars: 4,
4110
+ wave: "square",
4111
+ bass: bassLine([48, 55, 57, 53], [0, 1.5, 2, 3]),
4112
+ chords: [...chord(0, 60), ...chord(1, 55), ...chord(2, 57, true), ...chord(3, 53)],
4113
+ melody: [[0, 76, 1], [1, 74, 0.5], [1.5, 72, 1.5], [4, 74, 1], [5, 76, 1], [6, 79, 2], [8, 76, 1], [9, 74, 1], [10, 72, 2], [12, 72, 1], [13, 74, 1], [14, 72, 2]]
4114
+ }),
4115
+ // a syncopated loop in A minor, the bass on the off-beats
4116
+ radioTrack({
4117
+ id: "radio_track_groove",
4118
+ bpm: 100,
4119
+ bars: 4,
4120
+ wave: "triangle",
4121
+ bass: bassLine([57, 62, 64, 57], [0, 0.75, 1.5, 2.5, 3.25]),
4122
+ chords: [...chord(0, 57, true), ...chord(1, 62, true), ...chord(2, 64), ...chord(3, 57, true)],
4123
+ melody: [[0.5, 72, 0.5], [1.25, 76, 0.75], [2.5, 74, 1], [4.5, 77, 0.5], [5.25, 74, 0.75], [6.5, 72, 1.5], [8.5, 71, 0.5], [9.25, 74, 0.75], [10.5, 76, 1.5], [12.5, 72, 0.5], [13.25, 69, 2]]
4124
+ }),
4125
+ // a slow blues-like loop in F with a swung melody
4126
+ radioTrack({
4127
+ id: "radio_track_slow",
4128
+ bpm: 76,
4129
+ bars: 4,
4130
+ wave: "sine",
4131
+ bass: bassLine([53, 58, 53, 60], [0, 2]),
4132
+ chords: [...chord(0, 65), ...chord(1, 58), ...chord(2, 65), ...chord(3, 60)],
4133
+ melody: [[0, 72, 1.5], [1.67, 75, 0.33], [2, 77, 2], [4, 74, 1], [5, 72, 1], [6, 70, 2], [8, 72, 1.5], [9.67, 69, 0.33], [10, 65, 2], [12, 67, 1], [13, 70, 1], [14, 72, 2]]
4134
+ })
4135
+ ];
4136
+ var RADIO_TRACKS = TRACKS.map((t) => t.song);
4137
+ var RADIO_BANK = { id: "vehicle_radio", sounds: TRACKS.flatMap((t) => t.recipes), loops: [] };
4138
+
3700
4139
  // src/recipes/foley.ts
3701
4140
  var FOLEY_FALLOFF = {
3702
4141
  model: "inverse_square",
@@ -4338,11 +4777,11 @@ var GameAudio = (() => {
4338
4777
  ...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 })] : []
4339
4778
  ];
4340
4779
  }
4341
- var quieter = (level, layers) => layers.map((layer) => {
4342
- const gain = layer.gain ?? 1;
4343
- if (typeof gain === "number") return { ...layer, gain: gain * level };
4344
- if (Array.isArray(gain)) return { ...layer, gain: [gain[0] * level, gain[1] * level] };
4345
- return layer;
4780
+ var quieter = (level, layers) => layers.map((layer2) => {
4781
+ const gain = layer2.gain ?? 1;
4782
+ if (typeof gain === "number") return { ...layer2, gain: gain * level };
4783
+ if (Array.isArray(gain)) return { ...layer2, gain: [gain[0] * level, gain[1] * level] };
4784
+ return layer2;
4346
4785
  });
4347
4786
  var recipe = (id, layers, falloff, bus = "world") => ({ id, bus, pan: true, layers, falloff, sources: ["sdk"] });
4348
4787
  var takeId = (id, index) => `${id}_${String(index + 1).padStart(2, "0")}`;
@@ -4358,7 +4797,7 @@ var GameAudio = (() => {
4358
4797
  (take) => quieter(LANDING_LEVEL, [
4359
4798
  // both feet, a hair apart, then the knees taking the weight (a low body thump) and the clothes settling
4360
4799
  ...footLayers(surface, take, GAITS.run, LANDINGS[landing2].weight),
4361
- ...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 })),
4800
+ ...footLayers(surface, { ...take, v: take.v * 0.6 }, GAITS.run, LANDINGS[landing2].weight * 0.6).map((layer2) => ({ ...layer2, at: (typeof layer2.at === "number" ? layer2.at : 0) + 0.018 * take.t })),
4362
4801
  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 }),
4363
4802
  sceneNoise({ type: "bandpass", f: [1800, 2600], q: 0.8, d: 0.18, vol: LANDINGS[landing2].cloth * take.v, delay: 0.04 })
4364
4803
  ]),
@@ -4404,7 +4843,7 @@ var GameAudio = (() => {
4404
4843
  fall_damage: (take) => [
4405
4844
  ...BODY.body_fall(take),
4406
4845
  sceneNoise({ type: "highpass", f: 2800, d: 0.02, vol: 0.35 * take.v, delay: 0.01 }),
4407
- ...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 }))
4846
+ ...voice({ f0: 170, f1: 110, d: 0.32, fA: 650, fB: 1150, noise: 0.25, vol: 0.32 })(take).map((layer2) => ({ ...layer2, at: 0.08 }))
4408
4847
  ],
4409
4848
  player_hurt: voice({ f0: 150, f1: 105, d: 0.26, fA: 620, fB: 1100, noise: 0.3, vol: 0.35 }),
4410
4849
  player_breath_run: (take) => [
@@ -4506,6 +4945,9 @@ var GameAudio = (() => {
4506
4945
  },
4507
4946
  register(bank) {
4508
4947
  engine.register(bank);
4948
+ },
4949
+ preload(sounds) {
4950
+ void engine.loadSamples(sounds).catch(() => void 0);
4509
4951
  }
4510
4952
  };
4511
4953
  }
@@ -4766,11 +5208,11 @@ var GameAudio = (() => {
4766
5208
  const planned = plan();
4767
5209
  if (!planned) return `${text.sheet.title}: ${engine.registry.sound(state.sound) ? text.sheet.silent : text.sheet.none}`;
4768
5210
  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}`;
4769
- const layers2 = planned.layers.map((layer, index) => {
4770
- 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;
4771
- const filters = (layer.filters?.bank ?? []).map((filter) => `${filter.type}@${(filter.frequency.value ?? filter.frequency.from ?? 0).toFixed(1)}`).join(" ");
4772
- const envelope2 = layer.envelope.map((point) => `${point.time.toFixed(3)}:${point.value.toPrecision(3)}`).join(" ");
4773
- return ` ${text.sheet.layer} ${index + 1}: ${source} at ${layer.at.toFixed(3)} for ${layer.duration.toFixed(3)} ${filters} [${envelope2}]`;
5211
+ const layers2 = planned.layers.map((layer2, index) => {
5212
+ const source = layer2.source.kind === "noise" ? `noise@${layer2.source.offset.toFixed(4)}` : layer2.source.kind === "osc" ? `${layer2.source.wave}@${layer2.source.frequency.value ?? layer2.source.frequency.from}` : layer2.source.kind;
5213
+ const filters = (layer2.filters?.bank ?? []).map((filter) => `${filter.type}@${(filter.frequency.value ?? filter.frequency.from ?? 0).toFixed(1)}`).join(" ");
5214
+ const envelope2 = layer2.envelope.map((point) => `${point.time.toFixed(3)}:${point.value.toPrecision(3)}`).join(" ");
5215
+ return ` ${text.sheet.layer} ${index + 1}: ${source} at ${layer2.at.toFixed(3)} for ${layer2.duration.toFixed(3)} ${filters} [${envelope2}]`;
4774
5216
  });
4775
5217
  return [head, ...layers2].join("\n");
4776
5218
  };