@adatechnology/game-audio 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -23,31 +23,31 @@ import {
23
23
  GAME_SFX_BANK,
24
24
  GAME_SFX_CLASSES,
25
25
  GAME_SFX_FALLBACK,
26
- KIT_CATEGORY_BUS,
27
26
  KIT_SAMPLES_BANK,
28
- KIT_SAMPLE_ALIASES,
29
27
  LAYER_FLOOR,
30
28
  LIBRARY_BANK,
31
29
  LIBRARY_LOOPS,
32
30
  LIBRARY_SOUNDS,
31
+ LOCAL_PACK_LICENSES,
32
+ LOCAL_PACK_PREFERENCES,
33
33
  LOOP_SMOOTHING,
34
34
  LOOP_START_ABOVE,
35
+ LocalSoundPackError,
35
36
  MIX_PROFILES,
36
37
  OPEN_CUTOFF,
37
38
  PAN_LIMIT,
38
39
  PAN_MIN_DISTANCE,
39
40
  SAMPLE_ENVIRONMENTS,
40
- SDK_LICENSES,
41
41
  SFX_BLOWS,
42
42
  SFX_CATALOG,
43
43
  SFX_CATEGORIES,
44
44
  SFX_IMPACTS,
45
- SFX_LAYERS,
46
45
  SFX_MATERIALS,
47
- SFX_TIERS,
46
+ TRIM_FADE,
48
47
  USER_SAMPLES_BANK,
49
48
  USER_SOUND_PREFIX,
50
49
  VOICE_GROUPS,
50
+ aliasSample,
51
51
  audioBenchTextOf,
52
52
  balanceOf,
53
53
  bufferOf,
@@ -81,19 +81,19 @@ import {
81
81
  impactSoundId,
82
82
  inlineLoader,
83
83
  isCalibrationUrl,
84
+ isShippableLicense,
84
85
  isSilent,
85
- kitSampleBank,
86
- kitSampleDefaults,
87
86
  limiterCurve,
88
87
  listenerDistance,
89
- loadKitSamples,
90
88
  loadUserSamples,
89
+ localPackSamples,
91
90
  loopCrossfade,
91
+ loopRate,
92
92
  measureCriteria,
93
93
  noiseClassRecipe,
94
94
  onsetIndex,
95
95
  panFor,
96
- parseSfxName,
96
+ parseLocalSoundPack,
97
97
  peakIndex,
98
98
  peakOf,
99
99
  pickTake,
@@ -106,20 +106,109 @@ import {
106
106
  retunePlan,
107
107
  ringSeconds,
108
108
  rmsDb,
109
- sampleBankFromManifest,
110
109
  sampleCutoff,
111
110
  sampleLength,
112
111
  sampleUrls,
113
112
  sfxState,
114
113
  soundSourceOf,
115
- splitTier,
116
114
  streamFor,
117
115
  streamSeed,
118
116
  tierFor,
119
117
  unlockContext,
120
- userSampleBank,
118
+ userSampleBank
119
+ } from "./chunk-UUWKSG4J.js";
120
+ import {
121
+ KIT_CATEGORY_BUS,
122
+ KIT_SAMPLE_ALIASES,
123
+ SDK_LICENSES,
124
+ SFX_LAYERS,
125
+ SFX_TIERS,
126
+ kitSampleBank,
127
+ kitSampleDefaults,
128
+ loadKitSamples,
129
+ parseSfxName,
130
+ pointSamples,
131
+ sampleBankFromManifest,
132
+ splitTier,
121
133
  withAliases
122
- } from "./chunk-BXUGSOPY.js";
134
+ } from "./chunk-JV5XXCQE.js";
135
+
136
+ // src/local-pack-loader.ts
137
+ var LOCAL_PACK_BANK_ID = "local_sound_pack";
138
+ var STAGING_PREFIX = "local_pack_staging:";
139
+ var defaultIdleScheduler = (task) => {
140
+ const idle = globalThis;
141
+ if (idle.requestIdleCallback && idle.cancelIdleCallback) {
142
+ const handle2 = idle.requestIdleCallback(task);
143
+ const cancel = idle.cancelIdleCallback;
144
+ return () => cancel(handle2);
145
+ }
146
+ const handle = setTimeout(task, 0);
147
+ return () => clearTimeout(handle);
148
+ };
149
+ function applyLocalSoundPack(engine, pack, options = {}) {
150
+ const schedule = options.schedule ?? defaultIdleScheduler;
151
+ const samples = localPackSamples(pack, options);
152
+ const statuses = new Map(samples.map((sample) => [sample.id, "pending"]));
153
+ const priors = /* @__PURE__ */ new Map();
154
+ const report = { file: [], sdk: [], fallback: [] };
155
+ const queue = [...samples];
156
+ let cancel;
157
+ let disposed = false;
158
+ let finish = () => void 0;
159
+ const done = new Promise((resolve) => {
160
+ finish = resolve;
161
+ });
162
+ const swapIn = (sample) => {
163
+ const prior = engine.registry.sample(sample.id);
164
+ const bank = engine.registry.bankOf(sample.id);
165
+ priors.set(sample.id, { ...prior ? { sample: prior } : {}, ...bank ? { bank } : {} });
166
+ engine.register({ id: LOCAL_PACK_BANK_ID, samples: [{ ...sample, ...prior?.fallback ? { fallback: prior.fallback } : {} }] });
167
+ statuses.set(sample.id, "file");
168
+ report.file.push(sample.id);
169
+ };
170
+ const loadOne = async (sample) => {
171
+ if (pack.sounds[sample.id]?.prefer === "sdk" && engine.registry.sample(sample.id)) {
172
+ statuses.set(sample.id, "sdk");
173
+ report.sdk.push(sample.id);
174
+ return;
175
+ }
176
+ const staging = `${STAGING_PREFIX}${sample.id}`;
177
+ engine.register({ id: LOCAL_PACK_BANK_ID, samples: [{ ...sample, id: staging }] });
178
+ const loaded = await engine.loadSamples([staging]);
179
+ engine.registry.removeSample(staging);
180
+ if (disposed) return;
181
+ if (loaded.failed.length === 0) return swapIn(sample);
182
+ statuses.set(sample.id, "fallback");
183
+ report.fallback.push({ id: sample.id, error: loaded.failed.map((entry) => `${entry.url}: ${entry.error}`).join("; ") });
184
+ };
185
+ const next = () => {
186
+ const sample = queue.shift();
187
+ if (!sample || disposed) {
188
+ if (!disposed) finish(report);
189
+ return;
190
+ }
191
+ void loadOne(sample).finally(() => {
192
+ if (!disposed) cancel = schedule(next);
193
+ });
194
+ };
195
+ cancel = schedule(next);
196
+ return {
197
+ done,
198
+ status: (id) => statuses.get(id),
199
+ unlicensed: () => samples.filter((sample) => statuses.get(sample.id) === "file" && !isShippableLicense(sample.license)).map((sample) => sample.id),
200
+ dispose() {
201
+ disposed = true;
202
+ cancel?.();
203
+ for (const [id, prior] of priors) {
204
+ if (prior.sample) engine.register({ id: prior.bank ?? LOCAL_PACK_BANK_ID, samples: [prior.sample] });
205
+ else engine.registry.removeSample(id);
206
+ }
207
+ priors.clear();
208
+ finish(report);
209
+ }
210
+ };
211
+ }
123
212
 
124
213
  // src/recipes/scene.ts
125
214
  var ATTACK = 2e-3;
@@ -270,10 +359,140 @@ var CABIN_SOUNDS = [
270
359
  duration: 0.5,
271
360
  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"]] }],
272
361
  falloff: { model: "none" }
362
+ },
363
+ {
364
+ // the parking sensor: a short high beep, repeated by the cabin faster as the obstacle comes close
365
+ id: "parking_beep",
366
+ bus: "world",
367
+ duration: 0.09,
368
+ layers: [{ source: { kind: "osc", wave: "sine", frequency: { set: 2300 } }, envelope: [[0, 0.07, "set"], [0.08, 1e-4, "exp", "fixed"]] }],
369
+ falloff: { model: "none" }
370
+ },
371
+ {
372
+ // 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
373
+ id: "siren_wail",
374
+ bus: "world",
375
+ order: "siren",
376
+ duration: 3,
377
+ tail: 0.05,
378
+ layers: [
379
+ {
380
+ source: { kind: "osc", wave: "sawtooth", frequency: { set: 1050 } },
381
+ modulators: [{ target: "frequency", wave: "sine", rate: 0.33, depth: 420 }],
382
+ filters: { mode: "serial", bank: [{ type: "lowpass", frequency: { set: 2400 } }] },
383
+ envelope: [[0, 1e-4, "set", "fixed"], [0.08, 0.07, "linear"], [2.95, 0.07, "set"], [3, 1e-4, "exp", "fixed"]]
384
+ }
385
+ ],
386
+ falloff: { model: "none" }
387
+ },
388
+ {
389
+ // the yelp: the same tone swept fast, for a junction
390
+ id: "siren_yelp",
391
+ bus: "world",
392
+ order: "siren",
393
+ duration: 2,
394
+ tail: 0.05,
395
+ layers: [
396
+ {
397
+ source: { kind: "osc", wave: "sawtooth", frequency: { set: 1100 } },
398
+ modulators: [{ target: "frequency", wave: "triangle", rate: 4, depth: 380 }],
399
+ filters: { mode: "serial", bank: [{ type: "lowpass", frequency: { set: 2600 } }] },
400
+ envelope: [[0, 1e-4, "set", "fixed"], [0.05, 0.07, "linear"], [1.95, 0.07, "set"], [2, 1e-4, "exp", "fixed"]]
401
+ }
402
+ ],
403
+ falloff: { model: "none" }
404
+ },
405
+ {
406
+ // the wires under the column touching: a crackle of bright noise
407
+ id: "hotwire_spark",
408
+ bus: "world",
409
+ duration: 0.12,
410
+ layers: [
411
+ {
412
+ source: { kind: "noise", offset: "random" },
413
+ filters: { mode: "serial", bank: [{ type: "highpass", frequency: { set: 3200 } }] },
414
+ modulators: [{ target: "gain", wave: "square", rate: 60, depth: 0.1, base: 0.12 }],
415
+ envelope: [[0, 0.2, "set"], [0.12, 1e-4, "exp", "fixed"]]
416
+ }
417
+ ],
418
+ falloff: { model: "none" }
419
+ },
420
+ {
421
+ // the glovebox latch: a small plastic click and the lid dropping
422
+ id: "glovebox_latch",
423
+ bus: "world",
424
+ duration: 0.14,
425
+ layers: [
426
+ { 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"]] },
427
+ { 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"]] }
428
+ ],
429
+ falloff: { model: "none" }
273
430
  }
274
431
  ];
275
432
  var CABIN_BANK = { id: "vehicle_cabin", sounds: CABIN_SOUNDS, loops: [] };
276
433
 
434
+ // src/library/radio.ts
435
+ var hz = (midi) => 440 * 2 ** ((midi - 69) / 12);
436
+ var layer = (note, beat, wave, level) => {
437
+ const [at, midi, beats] = note;
438
+ const length = beats * beat;
439
+ return {
440
+ at: at * beat,
441
+ duration: length,
442
+ source: { kind: "osc", wave, frequency: { set: hz(midi) } },
443
+ 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"]]
444
+ };
445
+ };
446
+ function radioTrack(spec) {
447
+ const beat = 60 / spec.bpm;
448
+ const recipes = Array.from({ length: spec.bars }, (_, bar) => {
449
+ 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]]);
450
+ const layers = [
451
+ ...inBar(spec.bass).map((n) => layer(n, beat, "triangle", 0.16)),
452
+ ...inBar(spec.chords).map((n) => layer(n, beat, "sine", 0.05)),
453
+ ...inBar(spec.melody).map((n) => layer(n, beat, spec.wave, 0.07))
454
+ ];
455
+ return { id: `${spec.id}_${bar}`, bus: "world", duration: 4 * beat, layers, falloff: { model: "none" } };
456
+ });
457
+ return { song: { id: spec.id, barSeconds: 4 * beat, bars: recipes.map((r) => r.id) }, recipes };
458
+ }
459
+ var chord = (bar, root, minor = false) => [[bar * 4, root, 4], [bar * 4, root + (minor ? 3 : 4), 4], [bar * 4, root + 7, 4]];
460
+ var bassLine = (roots, pattern) => roots.flatMap((root, bar) => pattern.map((b) => [bar * 4 + b, root - 12, 0.9]));
461
+ var TRACKS = [
462
+ // a bright pop loop: I–V–vi–IV in C
463
+ radioTrack({
464
+ id: "radio_track_pop",
465
+ bpm: 112,
466
+ bars: 4,
467
+ wave: "square",
468
+ bass: bassLine([48, 55, 57, 53], [0, 1.5, 2, 3]),
469
+ chords: [...chord(0, 60), ...chord(1, 55), ...chord(2, 57, true), ...chord(3, 53)],
470
+ 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]]
471
+ }),
472
+ // a syncopated loop in A minor, the bass on the off-beats
473
+ radioTrack({
474
+ id: "radio_track_groove",
475
+ bpm: 100,
476
+ bars: 4,
477
+ wave: "triangle",
478
+ bass: bassLine([57, 62, 64, 57], [0, 0.75, 1.5, 2.5, 3.25]),
479
+ chords: [...chord(0, 57, true), ...chord(1, 62, true), ...chord(2, 64), ...chord(3, 57, true)],
480
+ 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]]
481
+ }),
482
+ // a slow blues-like loop in F with a swung melody
483
+ radioTrack({
484
+ id: "radio_track_slow",
485
+ bpm: 76,
486
+ bars: 4,
487
+ wave: "sine",
488
+ bass: bassLine([53, 58, 53, 60], [0, 2]),
489
+ chords: [...chord(0, 65), ...chord(1, 58), ...chord(2, 65), ...chord(3, 60)],
490
+ 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]]
491
+ })
492
+ ];
493
+ var RADIO_TRACKS = TRACKS.map((t) => t.song);
494
+ var RADIO_BANK = { id: "vehicle_radio", sounds: TRACKS.flatMap((t) => t.recipes), loops: [] };
495
+
277
496
  // src/recipes/foley.ts
278
497
  var FOLEY_FALLOFF = {
279
498
  model: "inverse_square",
@@ -925,11 +1144,11 @@ function footLayers(surface, take, gait, weight = 1) {
925
1144
  ...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 })] : []
926
1145
  ];
927
1146
  }
928
- var quieter = (level, layers) => layers.map((layer) => {
929
- const gain = layer.gain ?? 1;
930
- if (typeof gain === "number") return { ...layer, gain: gain * level };
931
- if (Array.isArray(gain)) return { ...layer, gain: [gain[0] * level, gain[1] * level] };
932
- return layer;
1147
+ var quieter = (level, layers) => layers.map((layer2) => {
1148
+ const gain = layer2.gain ?? 1;
1149
+ if (typeof gain === "number") return { ...layer2, gain: gain * level };
1150
+ if (Array.isArray(gain)) return { ...layer2, gain: [gain[0] * level, gain[1] * level] };
1151
+ return layer2;
933
1152
  });
934
1153
  var recipe = (id, layers, falloff, bus = "world") => ({ id, bus, pan: true, layers, falloff, sources: ["sdk"] });
935
1154
  var takeId = (id, index) => `${id}_${String(index + 1).padStart(2, "0")}`;
@@ -945,7 +1164,7 @@ var landing = (surface, landing2) => takes(
945
1164
  (take) => quieter(LANDING_LEVEL, [
946
1165
  // both feet, a hair apart, then the knees taking the weight (a low body thump) and the clothes settling
947
1166
  ...footLayers(surface, take, GAITS.run, LANDINGS[landing2].weight),
948
- ...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 })),
1167
+ ...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 })),
949
1168
  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 }),
950
1169
  sceneNoise({ type: "bandpass", f: [1800, 2600], q: 0.8, d: 0.18, vol: LANDINGS[landing2].cloth * take.v, delay: 0.04 })
951
1170
  ]),
@@ -991,7 +1210,7 @@ var BODY = {
991
1210
  fall_damage: (take) => [
992
1211
  ...BODY.body_fall(take),
993
1212
  sceneNoise({ type: "highpass", f: 2800, d: 0.02, vol: 0.35 * take.v, delay: 0.01 }),
994
- ...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 }))
1213
+ ...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 }))
995
1214
  ],
996
1215
  player_hurt: voice({ f0: 150, f1: 105, d: 0.26, fA: 620, fB: 1100, noise: 0.3, vol: 0.35 }),
997
1216
  player_breath_run: (take) => [
@@ -1093,6 +1312,9 @@ function createAudioSink(engine) {
1093
1312
  },
1094
1313
  register(bank) {
1095
1314
  engine.register(bank);
1315
+ },
1316
+ preload(sounds) {
1317
+ void engine.loadSamples(sounds).catch(() => void 0);
1096
1318
  }
1097
1319
  };
1098
1320
  }
@@ -1140,13 +1362,19 @@ export {
1140
1362
  LIBRARY_BANK,
1141
1363
  LIBRARY_LOOPS,
1142
1364
  LIBRARY_SOUNDS,
1365
+ LOCAL_PACK_BANK_ID,
1366
+ LOCAL_PACK_LICENSES,
1367
+ LOCAL_PACK_PREFERENCES,
1143
1368
  LOOP_SMOOTHING,
1144
1369
  LOOP_START_ABOVE,
1370
+ LocalSoundPackError,
1145
1371
  MIX_PROFILES,
1146
1372
  OPEN_CUTOFF,
1147
1373
  PAN_LIMIT,
1148
1374
  PAN_MIN_DISTANCE,
1149
1375
  PICKUP_CLASSES,
1376
+ RADIO_BANK,
1377
+ RADIO_TRACKS,
1150
1378
  SAMPLE_ENVIRONMENTS,
1151
1379
  SDK_LICENSES,
1152
1380
  SFX_BLOWS,
@@ -1156,9 +1384,12 @@ export {
1156
1384
  SFX_LAYERS,
1157
1385
  SFX_MATERIALS,
1158
1386
  SFX_TIERS,
1387
+ TRIM_FADE,
1159
1388
  USER_SAMPLES_BANK,
1160
1389
  USER_SOUND_PREFIX,
1161
1390
  VOICE_GROUPS,
1391
+ aliasSample,
1392
+ applyLocalSoundPack,
1162
1393
  audioBenchTextOf,
1163
1394
  balanceOf,
1164
1395
  bufferOf,
@@ -1180,6 +1411,7 @@ export {
1180
1411
  crossGains,
1181
1412
  cutoffDivisor,
1182
1413
  dbOf,
1414
+ defaultIdleScheduler,
1183
1415
  drawBetween,
1184
1416
  encodeWav,
1185
1417
  endDropDb,
@@ -1193,6 +1425,7 @@ export {
1193
1425
  impactSoundId,
1194
1426
  inlineLoader,
1195
1427
  isCalibrationUrl,
1428
+ isShippableLicense,
1196
1429
  isSilent,
1197
1430
  kitSampleBank,
1198
1431
  kitSampleDefaults,
@@ -1200,17 +1433,22 @@ export {
1200
1433
  listenerDistance,
1201
1434
  loadKitSamples,
1202
1435
  loadUserSamples,
1436
+ localPackSamples,
1203
1437
  loopCrossfade,
1438
+ loopRate,
1204
1439
  measureCriteria,
1205
1440
  noiseClassRecipe,
1206
1441
  onsetIndex,
1207
1442
  panFor,
1443
+ parseLocalSoundPack,
1208
1444
  parseSfxName,
1209
1445
  peakIndex,
1210
1446
  peakOf,
1211
1447
  pickTake,
1212
1448
  planSample,
1213
1449
  planSound,
1450
+ pointSamples,
1451
+ radioTrack,
1214
1452
  realize,
1215
1453
  realizeSample,
1216
1454
  recordOutput,