@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.
@@ -1,3 +1,11 @@
1
+ import {
2
+ SDK_LICENSES,
3
+ __publicField,
4
+ kitSampleDefaults,
5
+ sampleBankFromManifest,
6
+ splitTier
7
+ } from "./chunk-JV5XXCQE.js";
8
+
1
9
  // src/falloff.ts
2
10
  var DISTANCE_CURVES = {
3
11
  /** The game's `sfx`: `vol / (1 + d² × 0.004)`, cutoff `/ (1 + d × 0.01)`, silent below 0.01 (jogo L62091–62092, L62101). */
@@ -289,6 +297,20 @@ var voice_groups_default = {
289
297
  sources: [
290
298
  "sdk: the crackles of every fire share four voices"
291
299
  ]
300
+ },
301
+ debris_knock: {
302
+ maxConcurrent: 3,
303
+ cooldown: 0.04,
304
+ sources: [
305
+ "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)"
306
+ ]
307
+ },
308
+ sizzle: {
309
+ maxConcurrent: 1,
310
+ cooldown: 0.5,
311
+ sources: [
312
+ "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)"
313
+ ]
292
314
  }
293
315
  };
294
316
 
@@ -380,12 +402,48 @@ function mergeRecords(base, patch) {
380
402
  }
381
403
  return merged;
382
404
  }
405
+ var trimmed = (refs, alias) => refs.filter((ref) => alias.only === void 0 || ref.url.includes(alias.only)).map((ref) => ({
406
+ ...ref,
407
+ ...alias.offset === void 0 ? {} : { offset: alias.offset },
408
+ ...alias.duration === void 0 ? {} : { duration: alias.duration }
409
+ }));
410
+ function aliasSample(alias, file) {
411
+ const close = trimmed(file.close, alias);
412
+ if (close.length === 0) return void 0;
413
+ const { tiers: _tiers, fallback: _fallback, choke: _choke, ...rest } = file;
414
+ return {
415
+ ...rest,
416
+ id: alias.id,
417
+ close,
418
+ ...file.far ? { far: trimmed(file.far, alias) } : {},
419
+ ...alias.loop === void 0 ? {} : { loop: alias.loop },
420
+ ...alias.baseFrequency === void 0 ? {} : { baseFrequency: alias.baseFrequency },
421
+ ...alias.gain === void 0 ? {} : { gain: alias.gain },
422
+ ...alias.pitch === void 0 ? {} : { pitch: alias.pitch },
423
+ ...alias.falloff ? { falloff: alias.falloff } : {},
424
+ fallback: alias.id,
425
+ sources: [...file.sources, `alias: ${alias.id} \u2192 ${alias.file}`]
426
+ };
427
+ }
383
428
  function createSoundRegistry(banks = []) {
384
429
  const sounds = /* @__PURE__ */ new Map();
385
430
  const loops = /* @__PURE__ */ new Map();
386
431
  const sets = /* @__PURE__ */ new Map();
387
432
  const samples = /* @__PURE__ */ new Map();
388
433
  const owners = /* @__PURE__ */ new Map();
434
+ const aliases = /* @__PURE__ */ new Map();
435
+ const derived = /* @__PURE__ */ new Map();
436
+ const resolveAlias = (id) => {
437
+ const alias = aliases.get(id);
438
+ if (!alias) return void 0;
439
+ const cached = derived.get(id);
440
+ if (cached) return cached;
441
+ const file = samples.get(alias.file);
442
+ if (!file) return void 0;
443
+ const sample = aliasSample(alias, file);
444
+ if (sample) derived.set(id, sample);
445
+ return sample;
446
+ };
389
447
  const registry = {
390
448
  register(bank) {
391
449
  for (const sound of bank.sounds ?? []) {
@@ -405,6 +463,11 @@ function createSoundRegistry(banks = []) {
405
463
  samples.set(sample.id, sample);
406
464
  owners.set(sample.id, bank.id);
407
465
  }
466
+ for (const alias of bank.sampleAliases ?? []) {
467
+ aliases.set(alias.id, alias);
468
+ if (!owners.has(alias.id)) owners.set(alias.id, bank.id);
469
+ }
470
+ derived.clear();
408
471
  },
409
472
  patch(id, patch) {
410
473
  const sound = sounds.get(id);
@@ -413,22 +476,31 @@ function createSoundRegistry(banks = []) {
413
476
  if (loop) loops.set(id, mergeRecords(loop, patch));
414
477
  const sample = samples.get(id);
415
478
  if (sample) samples.set(id, mergeRecords(sample, patch));
479
+ derived.clear();
416
480
  },
417
481
  remove(id) {
418
482
  sounds.delete(id);
419
483
  loops.delete(id);
420
484
  sets.delete(id);
421
485
  samples.delete(id);
486
+ aliases.delete(id);
422
487
  owners.delete(id);
488
+ derived.clear();
489
+ },
490
+ removeSample(id) {
491
+ samples.delete(id);
492
+ aliases.delete(id);
493
+ if (!sounds.has(id) && !loops.has(id) && !sets.has(id)) owners.delete(id);
494
+ derived.clear();
423
495
  },
424
496
  sound: (id) => sounds.get(id),
425
497
  loop: (id) => loops.get(id),
426
498
  set: (id) => sets.get(id),
427
- sample: (id) => samples.get(id),
499
+ sample: (id) => samples.get(id) ?? resolveAlias(id),
428
500
  soundIds: () => [...sounds.keys()],
429
501
  setIds: () => [...sets.keys()],
430
502
  loopIds: () => [...loops.keys()],
431
- sampleIds: () => [...samples.keys()],
503
+ sampleIds: () => [...samples.keys(), ...[...aliases.keys()].filter((id) => !samples.has(id) && resolveAlias(id) !== void 0)],
432
504
  bankOf: (id) => owners.get(id)
433
505
  };
434
506
  for (const bank of banks) registry.register(bank);
@@ -1071,7 +1143,16 @@ function planSample(sound, input) {
1071
1143
  const index = pickTake(refs.length, input.memory?.get(key), input.random);
1072
1144
  input.memory?.set(key, index);
1073
1145
  const ref = refs[index];
1074
- layers.push({ role, url: ref.url, take: index, gain: gain * (ref.gain ?? 1) * level, rate, at });
1146
+ layers.push({
1147
+ role,
1148
+ url: ref.url,
1149
+ take: index,
1150
+ gain: gain * (ref.gain ?? 1) * level,
1151
+ rate,
1152
+ at,
1153
+ ...ref.offset === void 0 ? {} : { offset: ref.offset },
1154
+ ...ref.duration === void 0 ? {} : { duration: ref.duration }
1155
+ });
1075
1156
  };
1076
1157
  take("close", close, gains.close, 0);
1077
1158
  take("far", far, gains.far, 0);
@@ -1100,6 +1181,8 @@ function planSample(sound, input) {
1100
1181
  var DEFAULT_LOOP_CROSSFADE = 0.25;
1101
1182
  var LOOKAHEAD = 1.5;
1102
1183
  var VOLUME_SMOOTHING = 0.05;
1184
+ var RATE_SMOOTHING = 0.04;
1185
+ var RATE_RANGE = [0.25, 4];
1103
1186
  function loopCrossfade(sound) {
1104
1187
  if (!sound.loop) return void 0;
1105
1188
  return sound.loop === true ? DEFAULT_LOOP_CROSSFADE : Math.max(0, sound.loop.crossfade ?? DEFAULT_LOOP_CROSSFADE);
@@ -1111,6 +1194,7 @@ var ramp2 = (param, direction, at, seconds) => {
1111
1194
  param.setValueAtTime(value(0), at);
1112
1195
  for (let step = 1; step <= CROSSFADE_STEPS; step++) param.linearRampToValueAtTime(value(step / CROSSFADE_STEPS), at + seconds * step / CROSSFADE_STEPS);
1113
1196
  };
1197
+ 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;
1114
1198
  function createSampleLoopHandle(sound, host, volume = 0) {
1115
1199
  const crossfade = loopCrossfade(sound) ?? DEFAULT_LOOP_CROSSFADE;
1116
1200
  let level = volume;
@@ -1120,13 +1204,17 @@ function createSampleLoopHandle(sound, host, volume = 0) {
1120
1204
  let segments = [];
1121
1205
  let last;
1122
1206
  let timer;
1207
+ let panner;
1208
+ let rate = 1;
1209
+ let position;
1123
1210
  const entry = {
1124
1211
  get created() {
1125
1212
  return master !== void 0;
1126
1213
  },
1127
1214
  rewire() {
1128
- master?.disconnect();
1129
- master?.connect(host.output(sound.bus));
1215
+ const out = panner ?? master;
1216
+ out?.disconnect();
1217
+ out?.connect(host.output(sound.bus));
1130
1218
  },
1131
1219
  stop() {
1132
1220
  stopped = true;
@@ -1143,23 +1231,29 @@ function createSampleLoopHandle(sound, host, volume = 0) {
1143
1231
  segments = [];
1144
1232
  master?.disconnect();
1145
1233
  master = void 0;
1234
+ panner?.disconnect();
1235
+ panner = void 0;
1146
1236
  fallback?.stop();
1147
1237
  fallback = void 0;
1148
1238
  host.unregister(entry);
1149
1239
  }
1150
1240
  };
1151
- const nextBuffer = () => {
1241
+ const nextTake = () => {
1152
1242
  const takes = sound.close;
1153
1243
  const take = pickTake(takes.length, last, host.random);
1154
1244
  last = take;
1155
1245
  const ref = takes[take];
1156
- return ref ? host.buffer(ref.url) : void 0;
1246
+ const buffer = ref ? host.buffer(ref.url) : void 0;
1247
+ return ref && buffer ? { buffer, ref } : void 0;
1157
1248
  };
1158
1249
  const schedule = (context, output, at, first) => {
1159
- const buffer = nextBuffer();
1160
- if (!buffer) return void 0;
1250
+ const next = nextTake();
1251
+ if (!next) return void 0;
1252
+ const { buffer, ref } = next;
1253
+ const offset = Math.max(0, Math.min(ref.offset ?? 0, buffer.duration));
1161
1254
  const source = context.createBufferSource();
1162
1255
  source.buffer = buffer;
1256
+ source.playbackRate.value = rate;
1163
1257
  const gain = context.createGain();
1164
1258
  source.connect(gain);
1165
1259
  gain.connect(output);
@@ -1167,15 +1261,15 @@ function createSampleLoopHandle(sound, host, volume = 0) {
1167
1261
  if (buffer.duration < 2 * crossfade || sound.close.length === 1) {
1168
1262
  source.loop = true;
1169
1263
  if (!first) ramp2(gain.gain, "in", at, Math.max(1e-3, fade));
1170
- source.start(at);
1264
+ source.start(at, offset);
1171
1265
  segments.push({ source, gain, end: Number.POSITIVE_INFINITY });
1172
1266
  return void 0;
1173
1267
  }
1174
1268
  if (first) gain.gain.setValueAtTime(1, at);
1175
1269
  else ramp2(gain.gain, "in", at, fade);
1176
- const end3 = at + buffer.duration;
1270
+ const end3 = at + (buffer.duration - offset) / rate;
1177
1271
  ramp2(gain.gain, "out", end3 - fade, fade);
1178
- source.start(at);
1272
+ source.start(at, offset);
1179
1273
  source.stop(end3);
1180
1274
  segments.push({ source, gain, end: end3 });
1181
1275
  source.onended = () => {
@@ -1196,14 +1290,26 @@ function createSampleLoopHandle(sound, host, volume = 0) {
1196
1290
  pump(context, output, next, false);
1197
1291
  }, wait);
1198
1292
  };
1199
- const fileLevel = () => level * (sound.gain ?? 1);
1293
+ const spatial = () => {
1294
+ if (!position || !host.listener) return 1;
1295
+ return falloffGain(sound.falloff ?? DEFAULT_SAMPLE_FALLOFF, 1, listenerDistance(host.listener(), position, host.distance));
1296
+ };
1297
+ const fileLevel = () => level * (sound.gain ?? 1) * spatial();
1298
+ const place = (context) => {
1299
+ if (panner && position && host.listener) panner.pan.setTargetAtTime(panFor(host.listener(), position), context.currentTime, VOLUME_SMOOTHING);
1300
+ };
1200
1301
  const startFiles = () => {
1201
1302
  if (stopped || master || !host.isReady(sound)) return master !== void 0;
1202
1303
  const context = host.ready();
1203
1304
  if (!context) return false;
1204
1305
  master = context.createGain();
1205
1306
  master.gain.setValueAtTime(fileLevel(), context.currentTime);
1206
- master.connect(host.output(sound.bus));
1307
+ if (sound.pan !== false && position && typeof context.createStereoPanner === "function") {
1308
+ panner = context.createStereoPanner();
1309
+ if (host.listener) panner.pan.value = panFor(host.listener(), position);
1310
+ master.connect(panner);
1311
+ panner.connect(host.output(sound.bus));
1312
+ } else master.connect(host.output(sound.bus));
1207
1313
  host.register(entry);
1208
1314
  pump(context, master, context.currentTime, true);
1209
1315
  if (fallback) {
@@ -1229,9 +1335,15 @@ function createSampleLoopHandle(sound, host, volume = 0) {
1229
1335
  set(next) {
1230
1336
  if (stopped) return;
1231
1337
  if (next.volume !== void 0) level = host.muted(sound.bus) ? 0 : Math.max(0, next.volume);
1338
+ if (next.position) position = next.position;
1339
+ if (next.frequency !== void 0 && sound.baseFrequency) rate = loopRate(sound, next.frequency);
1232
1340
  if (level > 0) ensure();
1233
1341
  const context = host.ready();
1234
- if (master && context) master.gain.setTargetAtTime(fileLevel(), context.currentTime, VOLUME_SMOOTHING);
1342
+ if (master && context) {
1343
+ master.gain.setTargetAtTime(fileLevel(), context.currentTime, VOLUME_SMOOTHING);
1344
+ place(context);
1345
+ if (next.frequency !== void 0 && sound.baseFrequency) for (const segment of segments) segment.source.playbackRate.setTargetAtTime(rate, context.currentTime, RATE_SMOOTHING);
1346
+ }
1235
1347
  fallback?.set(next.volume === void 0 ? next : { ...next, volume: level });
1236
1348
  },
1237
1349
  stop: entry.stop
@@ -1859,6 +1971,7 @@ function createSampleStore(loader = fetchLoader()) {
1859
1971
  }
1860
1972
 
1861
1973
  // src/webaudio/realize-sample.ts
1974
+ var TRIM_FADE = 0.04;
1862
1975
  function realizeSample(plan, target) {
1863
1976
  const context = target.context;
1864
1977
  let head = target.output;
@@ -1889,7 +2002,17 @@ function realizeSample(plan, target) {
1889
2002
  const gain = gainNode(context, layer.gain);
1890
2003
  source.connect(gain);
1891
2004
  gain.connect(head);
1892
- source.start(target.time + layer.at);
2005
+ const at = target.time + layer.at;
2006
+ const offset = Math.max(0, Math.min(layer.offset ?? 0, buffer.duration));
2007
+ if (layer.duration === void 0) source.start(at, offset);
2008
+ else {
2009
+ const duration = Math.max(0, Math.min(layer.duration, buffer.duration - offset));
2010
+ const end3 = at + duration / layer.rate;
2011
+ const fade = Math.min(TRIM_FADE, (end3 - at) / 4);
2012
+ gain.gain.setValueAtTime(layer.gain, end3 - fade);
2013
+ gain.gain.linearRampToValueAtTime(0, end3);
2014
+ source.start(at, offset, duration);
2015
+ }
1893
2016
  sources.push(source);
1894
2017
  }
1895
2018
  const realized = voiceOf(sources);
@@ -1909,7 +2032,11 @@ function realizeSample(plan, target) {
1909
2032
  }
1910
2033
  function sampleLength(plan, seconds) {
1911
2034
  let length = 0;
1912
- for (const layer of plan.layers) length = Math.max(length, layer.at + (seconds(layer.url) ?? 0) / layer.rate);
2035
+ for (const layer of plan.layers) {
2036
+ const file = seconds(layer.url) ?? 0;
2037
+ const played = layer.duration === void 0 ? file - (layer.offset ?? 0) : Math.min(layer.duration, file - (layer.offset ?? 0));
2038
+ length = Math.max(length, layer.at + Math.max(0, played) / layer.rate);
2039
+ }
1913
2040
  return length;
1914
2041
  }
1915
2042
 
@@ -2118,6 +2245,7 @@ function createAudioEngine(options = {}) {
2118
2245
  const set = registry.set(id);
2119
2246
  return set ? chooser.pick(set, random) : { id, rate: 1 };
2120
2247
  };
2248
+ const wanted = /* @__PURE__ */ new Set();
2121
2249
  const loadSounds = (sounds) => {
2122
2250
  const audio = ready();
2123
2251
  if (!audio) return Promise.resolve({ loaded: [], failed: sounds.flatMap(sampleUrls).map((url2) => ({ url: url2, error: "no audio context" })) });
@@ -2242,13 +2370,20 @@ function createAudioEngine(options = {}) {
2242
2370
  };
2243
2371
  return {
2244
2372
  registry,
2245
- register: (bank) => registry.register(bank),
2373
+ register: (bank) => {
2374
+ registry.register(bank);
2375
+ if (wanted.size === 0) return;
2376
+ const now = [...wanted].map((id) => registry.sample(id)).filter((sample) => sample !== void 0);
2377
+ for (const sample of now) wanted.delete(sample.id);
2378
+ if (now.length > 0) void loadSounds(now).catch(() => void 0);
2379
+ },
2246
2380
  plan: (id, playOptions = {}) => planWith(id, playOptions, createRng(playOptions.seed ?? seed)),
2247
2381
  planSample(id, playOptions = {}) {
2248
2382
  const sample = registry.sample(id);
2249
2383
  return sample ? planSample(sample, sampleInput(playOptions, createRng(playOptions.seed ?? seed), false)) : null;
2250
2384
  },
2251
2385
  loadSamples(ids) {
2386
+ for (const id of ids ?? []) if (!registry.sample(id)) wanted.add(id);
2252
2387
  const chosen = (ids ?? registry.sampleIds()).map((id) => registry.sample(id)).filter((sample) => sample !== void 0);
2253
2388
  return loadSounds(chosen);
2254
2389
  },
@@ -2298,7 +2433,9 @@ function createAudioEngine(options = {}) {
2298
2433
  random: engineRandom,
2299
2434
  register: loopHost.register,
2300
2435
  unregister: loopHost.unregister,
2301
- muted: (bus) => busSettings.get(bus)?.muted === true
2436
+ muted: (bus) => busSettings.get(bus)?.muted === true,
2437
+ listener: () => listener,
2438
+ ...options.distance ? { distance: options.distance } : {}
2302
2439
  },
2303
2440
  loopOptions.volume
2304
2441
  );
@@ -3599,141 +3736,67 @@ async function recordOutput(engine, seconds, act) {
3599
3736
  return new Blob(chunks, { type: recorder.mimeType || "audio/webm" });
3600
3737
  }
3601
3738
 
3602
- // src/manifest.ts
3603
- var SDK_LICENSES = ["CC0", "CC-BY"];
3604
- var SFX_LAYERS = ["close", "far", "tail_in", "tail_out"];
3605
- var SFX_TIERS = { light: 0, medium: 0.34, heavy: 0.67 };
3606
- function parseSfxName(name) {
3607
- const match = /^(.*?)(?:_(close|far|tail_in|tail_out))?(?:_(\d+))?$/.exec(name);
3608
- const id = match?.[1] || name;
3609
- const layer = match?.[2] ?? "close";
3610
- const take = match?.[3] ? Number(match[3]) : 1;
3611
- return { id, layer, take };
3612
- }
3613
- function splitTier(id) {
3614
- const match = /^(.*)_(light|medium|heavy)$/.exec(id);
3615
- return match ? { base: match[1], tier: match[2] } : { base: id };
3616
- }
3617
- var refsOf = (entries2, baseUrl) => (entries2 ?? []).map((entry) => ({ url: `${baseUrl}${entry.file}` }));
3618
- function sampleBankFromManifest(manifest, options) {
3619
- const baseUrl = options.baseUrl ?? "";
3620
- const samples = [];
3621
- const tiersOf = /* @__PURE__ */ new Map();
3622
- for (const sound of manifest.sounds) {
3623
- const { base, tier } = splitTier(sound.id);
3624
- if (tier) tiersOf.set(base, [...tiersOf.get(base) ?? [], sound]);
3625
- }
3626
- const bases = /* @__PURE__ */ new Map();
3627
- for (const sound of manifest.sounds) {
3628
- if (!splitTier(sound.id).tier) bases.set(sound.id, sound);
3629
- }
3630
- for (const [base, tiers] of tiersOf) {
3631
- if (!bases.has(base)) bases.set(base, { id: base, category: tiers[0]?.category ?? "", layers: { close: tiers[0]?.layers.close ?? [] } });
3632
- }
3633
- for (const sound of bases.values()) {
3634
- const numbers = options.sounds?.[sound.id] ?? options.defaults?.(sound);
3635
- const close = refsOf(sound.layers.close, baseUrl);
3636
- if (!numbers || close.length === 0) continue;
3637
- const tiers = (tiersOf.get(sound.id) ?? []).map((tier) => {
3638
- const name = splitTier(tier.id).tier;
3639
- const tierFar = refsOf(tier.layers.far, baseUrl);
3640
- return { name, from: SFX_TIERS[name] ?? 0, close: refsOf(tier.layers.close, baseUrl), ...tierFar.length > 0 ? { far: tierFar } : {} };
3641
- }).sort((a, b) => a.from - b.from);
3642
- const far = refsOf(sound.layers.far, baseUrl);
3643
- const indoor = refsOf(sound.layers.tail_in, baseUrl);
3644
- const outdoor = refsOf(sound.layers.tail_out, baseUrl);
3645
- const files = [sound, ...tiersOf.get(sound.id) ?? []].flatMap((entry) => Object.values(entry.layers).flatMap((entries2) => entries2 ?? []));
3646
- samples.push({
3647
- id: sound.id,
3648
- bus: numbers.bus,
3649
- close,
3650
- ...far.length > 0 ? { far } : {},
3651
- ...tiers.length > 0 ? { tiers } : {},
3652
- ...numbers.crossover ? { crossover: numbers.crossover } : {},
3653
- ...indoor.length > 0 || outdoor.length > 0 ? { tail: { ...indoor.length > 0 ? { indoor } : {}, ...outdoor.length > 0 ? { outdoor } : {}, ...numbers.tailGain === void 0 ? {} : { gain: numbers.tailGain } } } : {},
3654
- ...numbers.pitch === void 0 ? {} : { pitch: numbers.pitch },
3655
- ...numbers.gain === void 0 ? {} : { gain: numbers.gain },
3656
- ...numbers.falloff ? { falloff: numbers.falloff } : {},
3657
- ...numbers.cutoffK === void 0 ? {} : { cutoffK: numbers.cutoffK },
3658
- ...numbers.group ? { group: numbers.group } : {},
3659
- ...numbers.priority === void 0 ? {} : { priority: numbers.priority },
3660
- ...numbers.fallback ? { fallback: numbers.fallback } : {},
3661
- sources: files.map((file) => `${file.file}: ${file.source} ${file.author} ${file.license}`)
3662
- });
3739
+ // src/local-pack.ts
3740
+ var LOCAL_PACK_LICENSES = ["CC0", "CC-BY", "unknown", "proprietary"];
3741
+ var LOCAL_PACK_PREFERENCES = ["pack", "sdk"];
3742
+ var LocalSoundPackError = class extends Error {
3743
+ constructor(problems) {
3744
+ super(`invalid local sound pack: ${problems.join("; ")}`);
3745
+ __publicField(this, "problems");
3746
+ this.name = "LocalSoundPackError";
3747
+ this.problems = problems;
3663
3748
  }
3664
- return { id: options.id, samples };
3665
- }
3666
-
3667
- // src/kit-samples.ts
3668
- var KIT_CATEGORY_BUS = {
3669
- armas: "weapons",
3670
- impactos: "world",
3671
- "explosoes-fogo": "world",
3672
- personagem: "world",
3673
- veiculos: "world",
3674
- "itens-portas": "world",
3675
- ambiente: "ambience"
3676
3749
  };
3677
- var VOICE_IDS = /^(player_(hurt|death|effort|breath)|scream|zombie_)/;
3678
- var KIT_SAMPLE_ALIASES = {
3679
- // a house door: the latch giving, the hinge, the leaf meeting the frame, a locked one shaken; a drawer on its runners
3680
- latch: "metal_latch",
3681
- creak_wood: "door_creak",
3682
- shut_wood: "door_wood_shut",
3683
- rattle: "door_locked_rattle",
3684
- // a drawer stays on its recipe: the recorded runs (1 to 1.9 s) outlast a drawer's pull
3685
- // an item taken in the hand (game-destruction-three grab_* keys)
3686
- grab_hard: "item_pickup_generic",
3687
- grab_soft: "item_pickup_leather"
3750
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3751
+ var isLicense = (value) => LOCAL_PACK_LICENSES.includes(value);
3752
+ var isBus = (value) => BUS_IDS.includes(value);
3753
+ function entryProblems(id, entry) {
3754
+ if (!isRecord(entry)) return [`${id}: not an object`];
3755
+ const problems = [];
3756
+ const files = entry.files;
3757
+ 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`);
3758
+ if (!isLicense(entry.license)) problems.push(`${id}: license must be one of ${LOCAL_PACK_LICENSES.join(", ")}`);
3759
+ if (entry.prefer !== void 0 && !LOCAL_PACK_PREFERENCES.includes(entry.prefer)) problems.push(`${id}: prefer must be pack or sdk`);
3760
+ if (entry.bus !== void 0 && !isBus(entry.bus)) problems.push(`${id}: unknown bus ${String(entry.bus)}`);
3761
+ if (entry.gain !== void 0 && (typeof entry.gain !== "number" || entry.gain < 0)) problems.push(`${id}: gain must be a number \u2265 0`);
3762
+ if (entry.score !== void 0 && (typeof entry.score !== "number" || entry.score < 0 || entry.score > 10)) problems.push(`${id}: score must be 0\u201310`);
3763
+ return problems;
3764
+ }
3765
+ function parseLocalSoundPack(value) {
3766
+ if (!isRecord(value)) throw new LocalSoundPackError(["the pack is not an object"]);
3767
+ const problems = [];
3768
+ if (value.version !== 1) problems.push("version must be 1");
3769
+ if (value.baseUrl !== void 0 && typeof value.baseUrl !== "string") problems.push("baseUrl must be a string");
3770
+ if (!isRecord(value.sounds)) problems.push("sounds must be an object of id \u2192 entry");
3771
+ else for (const [id, entry] of Object.entries(value.sounds)) problems.push(...entryProblems(id, entry));
3772
+ if (problems.length > 0) throw new LocalSoundPackError(problems);
3773
+ return value;
3774
+ }
3775
+ var isShippableLicense = (license) => SDK_LICENSES.includes(license);
3776
+ var CATALOG = new Map([...SFX_CATALOG, ...SFX_IMPACTS].map((entry) => [entry.id, entry]));
3777
+ var busOf = (id, entry) => {
3778
+ if (entry.bus) return entry.bus;
3779
+ const category = CATALOG.get(id)?.category ?? "";
3780
+ return kitSampleDefaults({ id, category })?.bus ?? "world";
3688
3781
  };
3689
- function kitSampleDefaults(sound) {
3690
- const bus = VOICE_IDS.test(sound.id) ? "voices" : KIT_CATEGORY_BUS[sound.category];
3691
- if (!bus) return void 0;
3692
- return { bus, pitch: sound.category === "ambiente" || /_loop/.test(sound.id) ? 0 : 0.04 };
3693
- }
3694
- function withAliases(manifest, aliases) {
3695
- const byId = new Map(manifest.sounds.map((sound) => [sound.id, sound]));
3696
- const extra = [];
3697
- for (const [alias, target] of Object.entries(aliases)) {
3698
- const sound = byId.get(target);
3699
- if (sound && !byId.has(alias)) extra.push({ ...sound, id: alias });
3700
- }
3701
- return { ...manifest, sounds: [...manifest.sounds, ...extra] };
3702
- }
3703
- function kitSampleBank(manifest, options = {}) {
3704
- return sampleBankFromManifest(withAliases(manifest, options.aliases ?? KIT_SAMPLE_ALIASES), {
3705
- id: options.id ?? "kit_samples",
3706
- baseUrl: options.baseUrl ?? "",
3707
- defaults: kitSampleDefaults
3782
+ function localPackSamples(pack, options = {}) {
3783
+ const baseUrl = options.baseUrl ?? pack.baseUrl ?? "";
3784
+ return Object.entries(pack.sounds).map(([id, entry]) => {
3785
+ const current = CATALOG.get(id)?.current;
3786
+ const isLoop = /_loop(_|$)/.test(id);
3787
+ return {
3788
+ id,
3789
+ bus: busOf(id, entry),
3790
+ close: entry.files.map((file) => ({ url: `${baseUrl}${file}` })),
3791
+ pitch: isLoop ? 0 : 0.04,
3792
+ ...isLoop ? { loop: true } : {},
3793
+ ...entry.gain === void 0 ? {} : { gain: entry.gain },
3794
+ ...current ? { fallback: current } : {},
3795
+ license: entry.license,
3796
+ sources: entry.files.map((file) => `local: ${file} ${entry.license}`)
3797
+ };
3708
3798
  });
3709
3799
  }
3710
- var isFileUrl = (url2) => {
3711
- const page = globalThis.location?.href;
3712
- try {
3713
- return new URL(url2, page).protocol === "file:";
3714
- } catch {
3715
- return false;
3716
- }
3717
- };
3718
- var fetchJson = async (url2) => {
3719
- if (isFileUrl(url2)) throw new Error(`${url2}: file:// is not fetched`);
3720
- const response = await fetch(url2);
3721
- if (!response.ok) throw new Error(`${url2}: ${response.status}`);
3722
- return await response.json();
3723
- };
3724
- async function loadKitSamples(engine, options = {}) {
3725
- const baseUrl = options.baseUrl ?? "";
3726
- let manifest;
3727
- try {
3728
- manifest = await (options.fetchManifest ?? fetchJson)(`${baseUrl}manifest.json`);
3729
- } catch {
3730
- return null;
3731
- }
3732
- const bank = kitSampleBank(manifest, options);
3733
- engine.register(bank);
3734
- if (options.preload && engine.loadSamples) void engine.loadSamples(options.preload).catch(() => void 0);
3735
- return bank;
3736
- }
3737
3800
 
3738
3801
  // src/user-samples.ts
3739
3802
  var USER_SAMPLES_BANK = "user_samples";
@@ -3863,9 +3926,9 @@ var PT = {
3863
3926
  strengths: { light: "leve", medium: "m\xE9dio", heavy: "forte" },
3864
3927
  states: { file: "A", synth: "S", missing: "F" },
3865
3928
  criteria: { C1: "C1 Sincronia", C2: "C2 Certo p/ a\xE7\xE3o", C3: "C3 Dist\xE2ncia", C4: "C4 Posi\xE7\xE3o", C5: "C5 Varia\xE7\xE3o", C6: "C6 Volume", C7: "C7 Intensidade", C8: "C8 Cauda", C9: "C9 Rajada", C10: "C10 Escuta" },
3866
- metrics: { source: "1 \xB7 Fonte do que tocou", chosen: "1 \xB7 Escolhido", matrix: "2 \xB7 Matriz", last: "\xDAltimo tocado", ab: "4 \xB7 A \xD7 B", blind: "4 \xB7 X", verdicts: "4 \xB7 Prefer\xEAncias", state: "5 \xB7 Medi\xE7\xE3o", count: "1 \xB7 Cat\xE1logo" },
3929
+ metrics: { source: "1 \xB7 Fonte do que tocou", chosen: "1 \xB7 Escolhido", matrix: "2 \xB7 Matriz", last: "\xDAltimo tocado", ab: "4 \xB7 A \xD7 B", blind: "4 \xB7 X", verdicts: "4 \xB7 Prefer\xEAncias", state: "5 \xB7 Medi\xE7\xE3o", count: "1 \xB7 Cat\xE1logo", unlicensed: "1 \xB7 Arquivo sem licen\xE7a" },
3867
3930
  run: { idle: "parada", measuring: "medindo\u2026", done: "pronta", failed: "falhou", recording: "gravando\u2026", recorded: "grava\xE7\xE3o baixada" },
3868
- words: { none: "\u2014", hidden: "? (revele)", prefers: "prefere", approved: "aprovado pelo dono" },
3931
+ words: { none: "\u2014", hidden: "? (revele)", prefers: "prefere", approved: "aprovado pelo dono", local_file: "arquivo do pacote local", sdk_file: "grava\xE7\xE3o do SDK", synth: "s\xEDntese do SDK" },
3869
3932
  sources: { sdk: "SDK", user: "seu som", synth: "sintetizado" },
3870
3933
  probe: "Fonte do som",
3871
3934
  wall: "Parede (oclus\xE3o)",
@@ -3936,9 +3999,9 @@ var EN2 = {
3936
3999
  environments: { indoor: "indoor", outdoor: "outdoor" },
3937
4000
  strengths: { light: "light", medium: "medium", heavy: "heavy" },
3938
4001
  criteria: { C1: "C1 Sync", C2: "C2 Right sound", C3: "C3 Distance", C4: "C4 Position", C5: "C5 Variation", C6: "C6 Level", C7: "C7 Strength", C8: "C8 Tail", C9: "C9 Burst", C10: "C10 Listening" },
3939
- metrics: { source: "1 \xB7 Source of what played", chosen: "1 \xB7 Chosen", matrix: "2 \xB7 Matrix", last: "Last played", ab: "4 \xB7 A \xD7 B", blind: "4 \xB7 X", verdicts: "4 \xB7 Preferences", state: "5 \xB7 Measurement", count: "1 \xB7 Catalogue" },
4002
+ metrics: { source: "1 \xB7 Source of what played", chosen: "1 \xB7 Chosen", matrix: "2 \xB7 Matrix", last: "Last played", ab: "4 \xB7 A \xD7 B", blind: "4 \xB7 X", verdicts: "4 \xB7 Preferences", state: "5 \xB7 Measurement", count: "1 \xB7 Catalogue", unlicensed: "1 \xB7 File without licence" },
3940
4003
  run: { idle: "idle", measuring: "measuring\u2026", done: "done", failed: "failed", recording: "recording\u2026", recorded: "recording downloaded" },
3941
- words: { none: "\u2014", hidden: "? (reveal)", prefers: "prefers", approved: "approved by the owner" },
4004
+ words: { none: "\u2014", hidden: "? (reveal)", prefers: "prefers", approved: "approved by the owner", local_file: "local pack file", sdk_file: "SDK recording", synth: "SDK synthesis" },
3942
4005
  sources: { sdk: "SDK", user: "your sound", synth: "synthesized" },
3943
4006
  probe: "Sound source",
3944
4007
  wall: "Wall (occlusion)",
@@ -4146,6 +4209,18 @@ function createSoundCheckModule(options = {}) {
4146
4209
  const count = (state) => states.filter((each) => each === state).length;
4147
4210
  return `${text.states.file} ${count("file")} \xB7 ${text.states.synth} ${count("synth")} \xB7 ${text.states.missing} ${count("missing")}`;
4148
4211
  };
4212
+ const sourceOf2 = (id) => {
4213
+ const sample = registry().sample(id);
4214
+ if (!sample) return text.words.synth;
4215
+ return sample.sources.some((source) => source.startsWith("local:")) ? text.words.local_file : text.words.sdk_file;
4216
+ };
4217
+ const unlicensed = () => {
4218
+ const ids = registry().sampleIds().filter((id) => {
4219
+ const license = registry().sample(id)?.license;
4220
+ return license !== void 0 && !isShippableLicense(license);
4221
+ });
4222
+ return ids.length === 0 ? text.words.none : `${ids.length}: ${ids.join(", ")}`;
4223
+ };
4149
4224
  const strikeId = () => impactSoundId(chosen.material, chosen.blow);
4150
4225
  const lastLine = () => {
4151
4226
  const last = log.at(-1);
@@ -4162,7 +4237,8 @@ function createSoundCheckModule(options = {}) {
4162
4237
  const metrics = () => [
4163
4238
  { id: "source", name: text.metrics.source, read: sourceLine },
4164
4239
  { id: "count", name: text.metrics.count, read: counts },
4165
- { id: "chosen", name: text.metrics.chosen, read: () => `${label(chosen.sound)}${registry().sample(chosen.sound) ? ` (${engine.sampleStatus(chosen.sound)})` : ""}` },
4240
+ { id: "unlicensed", name: text.metrics.unlicensed, read: unlicensed },
4241
+ { id: "chosen", name: text.metrics.chosen, read: () => `${label(chosen.sound)}${registry().sample(chosen.sound) ? ` (${engine.sampleStatus(chosen.sound)})` : ""} \xB7 ${sourceOf2(chosen.sound)}` },
4166
4242
  { id: "matrix", name: text.metrics.matrix, read: () => `${strikeId()} \xB7 ${text.states[stateOf(strikeId())]}${pairOf(strikeId()).a ? ` \xB7 A=${pairOf(strikeId()).a}` : ""}` },
4167
4243
  { id: "ab", name: text.metrics.ab, read: () => `A ${pairOf(chosen.sound).a ?? text.words.none} \xB7 B ${pairOf(chosen.sound).b ?? text.words.none}` },
4168
4244
  { id: "blind_state", name: text.metrics.blind, read: () => !blind ? text.words.none : blind.revealed ? `X = ${blind.side}` : text.words.hidden },
@@ -4405,6 +4481,7 @@ export {
4405
4481
  CALIBRATION_LOOP_SOUND,
4406
4482
  isCalibrationUrl,
4407
4483
  calibrationLoader,
4484
+ aliasSample,
4408
4485
  createSoundRegistry,
4409
4486
  MIX_PROFILES,
4410
4487
  DEFAULT_MIX,
@@ -4423,12 +4500,14 @@ export {
4423
4500
  loopCrossfade,
4424
4501
  CROSSFADE_STEPS,
4425
4502
  equalPowerIn,
4503
+ loopRate,
4426
4504
  realize,
4427
4505
  retunePlan,
4428
4506
  createSetPicker,
4429
4507
  fetchLoader,
4430
4508
  inlineLoader,
4431
4509
  createSampleStore,
4510
+ TRIM_FADE,
4432
4511
  realizeSample,
4433
4512
  sampleLength,
4434
4513
  limiterCurve2 as limiterCurve,
@@ -4454,24 +4533,18 @@ export {
4454
4533
  impactSoundId,
4455
4534
  SFX_IMPACTS,
4456
4535
  sfxState,
4457
- SDK_LICENSES,
4458
- SFX_LAYERS,
4459
- SFX_TIERS,
4460
- parseSfxName,
4461
- splitTier,
4462
- sampleBankFromManifest,
4463
- KIT_CATEGORY_BUS,
4464
- KIT_SAMPLE_ALIASES,
4465
- kitSampleDefaults,
4466
- withAliases,
4467
- kitSampleBank,
4468
- loadKitSamples,
4469
4536
  USER_SAMPLES_BANK,
4470
4537
  KIT_SAMPLES_BANK,
4471
4538
  USER_SOUND_PREFIX,
4472
4539
  soundSourceOf,
4473
4540
  userSampleBank,
4474
4541
  loadUserSamples,
4542
+ LOCAL_PACK_LICENSES,
4543
+ LOCAL_PACK_PREFERENCES,
4544
+ LocalSoundPackError,
4545
+ parseLocalSoundPack,
4546
+ isShippableLicense,
4547
+ localPackSamples,
4475
4548
  GAME_SFX_CLASSES,
4476
4549
  GAME_SFX_FALLBACK,
4477
4550
  noiseClassRecipe,
@@ -4487,4 +4560,4 @@ export {
4487
4560
  CALIBRATION_CATEGORY,
4488
4561
  createSoundCheckModule
4489
4562
  };
4490
- //# sourceMappingURL=chunk-BXUGSOPY.js.map
4563
+ //# sourceMappingURL=chunk-UUWKSG4J.js.map