@adatechnology/game-weapons-three 0.5.0 → 0.6.1

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.
@@ -12937,10 +12937,10 @@ var GameWeaponsThree = (() => {
12937
12937
  __publicField(this, "lastRoundAt", Number.NEGATIVE_INFINITY);
12938
12938
  }
12939
12939
  /** When the barrels of a rotary gun are at speed: `upSeconds` after the pull began, and at once while they still spin from the last round. */
12940
- spunUpAt(spin, start, end) {
12940
+ spunUpAt(spin, start, end3) {
12941
12941
  if (!spin || start - this.lastRoundAt <= spin.holdSeconds + EPS3) return 0;
12942
12942
  if (this.spinSince === void 0 || start - this.spinFrameEnd > SPIN_PULL_GAP) this.spinSince = start;
12943
- this.spinFrameEnd = end;
12943
+ this.spinFrameEnd = end3;
12944
12944
  return this.spinSince + spin.upSeconds;
12945
12945
  }
12946
12946
  oncePerPull(held) {
@@ -12949,7 +12949,7 @@ var GameWeaponsThree = (() => {
12949
12949
  this.latched = true;
12950
12950
  return true;
12951
12951
  }
12952
- fireFirearm(a, origin, dir, start, end, held, source) {
12952
+ fireFirearm(a, origin, dir, start, end3, held, source) {
12953
12953
  if (this._reloading) {
12954
12954
  if (this.loadStep !== null && this._ammo > 0) this.stopReload(start);
12955
12955
  else return result("reloading");
@@ -12959,10 +12959,10 @@ var GameWeaponsThree = (() => {
12959
12959
  this.emit({ type: "empty", at: start });
12960
12960
  return result("empty");
12961
12961
  }
12962
- const t0 = Math.max(start, this.readyAt, this.spunUpAt(a.spin, start, end));
12963
- if (t0 > end + EPS3) return result("cooldown");
12962
+ const t0 = Math.max(start, this.readyAt, this.spunUpAt(a.spin, start, end3));
12963
+ if (t0 > end3 + EPS3) return result("cooldown");
12964
12964
  const res = result("ok");
12965
- const times = planRounds(a, this.mode, this._ammo, t0, end);
12965
+ const times = planRounds(a, this.mode, this._ammo, t0, end3);
12966
12966
  let lastAt = t0;
12967
12967
  for (const t of times) {
12968
12968
  const fault = rollFault({ def: this.def, caliber: this.caliber, condition: this._condition, ammoQuality: this._ammoQuality, heat: this._heat, rules: this.faultRules(), random: this.faultRng });
@@ -13078,7 +13078,7 @@ var GameWeaponsThree = (() => {
13078
13078
  this.pend = { src: "trigger", amt: d };
13079
13079
  }
13080
13080
  const start = this.clock - overlap;
13081
- const end = start + d;
13081
+ const end3 = start + d;
13082
13082
  this.settle(start);
13083
13083
  const dir = normalize(direction);
13084
13084
  let res;
@@ -13089,10 +13089,10 @@ var GameWeaponsThree = (() => {
13089
13089
  else if (launcher) res = launcher.trigger(origin, dir, d, held, source);
13090
13090
  else switch (a.kind) {
13091
13091
  case "firearm":
13092
- res = this.fireFirearm(a, origin, dir, start, end, held, source);
13092
+ res = this.fireFirearm(a, origin, dir, start, end3, held, source);
13093
13093
  break;
13094
13094
  case "melee":
13095
- res = this.fireMelee(a, origin, dir, start, end);
13095
+ res = this.fireMelee(a, origin, dir, start, end3);
13096
13096
  break;
13097
13097
  case "flame":
13098
13098
  case "heat":
@@ -13102,7 +13102,7 @@ var GameWeaponsThree = (() => {
13102
13102
  res = this.fireExtinguisher(a, origin, dir, d);
13103
13103
  break;
13104
13104
  case "throwable":
13105
- res = this.fireThrowable(a, origin, dir, held, start, end);
13105
+ res = this.fireThrowable(a, origin, dir, held, start, end3);
13106
13106
  break;
13107
13107
  case "launcher":
13108
13108
  res = this.fireLauncher(a, origin, dir, start, held);
@@ -13114,7 +13114,7 @@ var GameWeaponsThree = (() => {
13114
13114
  res = this.oncePerPull(held) ? result("ok", { uses: [{ kind: "hand", reach: a.reach, origin, direction: dir }] }) : result("cooldown");
13115
13115
  break;
13116
13116
  }
13117
- this.advance(Math.max(0, end - this.clock));
13117
+ this.advance(Math.max(0, end3 - this.clock));
13118
13118
  this.settle(this.clock);
13119
13119
  return res;
13120
13120
  }
@@ -13146,9 +13146,9 @@ var GameWeaponsThree = (() => {
13146
13146
  this.emit({ type: "shot", shots: 0, rounds: 1, mode: this.mode, at: t });
13147
13147
  return result("ok", { uses: launcherUses(a, this.explosive, origin, dir), recoil: [a.recoil[0], a.recoil[1]], noise: a.noise ?? 0, rounds: 1, roundTimes: [t - start] });
13148
13148
  }
13149
- fireMelee(a, origin, dir, start, end) {
13149
+ fireMelee(a, origin, dir, start, end3) {
13150
13150
  const t0 = Math.max(start, this.readyAt);
13151
- if (t0 > end + EPS3) return result("cooldown");
13151
+ if (t0 > end3 + EPS3) return result("cooldown");
13152
13152
  this.readyAt = t0 + 60 / a.rpm;
13153
13153
  const use = { kind: "melee", damage: a.damage, energy: a.energy, radius: a.radius, reach: a.reach, origin, direction: dir };
13154
13154
  return result("ok", { uses: [use], noise: a.noise });
@@ -13182,8 +13182,8 @@ var GameWeaponsThree = (() => {
13182
13182
  return result("ok", { uses: [throwableUse({ action: a, explosive: this.explosive, origin, direction: dir, fuse, rules: this.rules.explosives, ...extra })], noise: a.noise });
13183
13183
  }
13184
13184
  /** A timer throwable is cooked from the pull; impact, remote and wire ones (and a long fuse that sets down) act at once. */
13185
- fireThrowable(a, origin, dir, held, start, end) {
13186
- if (this.cook) return this.cookStep(a, origin, dir, end);
13185
+ fireThrowable(a, origin, dir, held, start, end3) {
13186
+ if (this.cook) return this.cookStep(a, origin, dir, end3);
13187
13187
  if (!this.oncePerPull(held)) return result("cooldown");
13188
13188
  if (this._ammo <= 0) return result("empty");
13189
13189
  const fuse = this.sampleThrowFuse(a);
@@ -13193,7 +13193,7 @@ var GameWeaponsThree = (() => {
13193
13193
  }
13194
13194
  this.cook = { start, fuse, origin, direction: dir };
13195
13195
  this.emit({ type: "cook", phase: "start", fuse, at: start });
13196
- return this.cookStep(a, origin, dir, end);
13196
+ return this.cookStep(a, origin, dir, end3);
13197
13197
  }
13198
13198
  /** The trigger stays down on a cooked throwable: nothing happens until the fuse runs out in the hand. */
13199
13199
  cookStep(a, origin, dir, now) {
@@ -13621,14 +13621,1434 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
13621
13621
  };
13622
13622
  var accessoryTextOf = (locale) => ACCESSORY_TEXT[locale] ?? ACCESSORY_TEXT.en;
13623
13623
 
13624
+ // ../game-audio/dist/index.js
13625
+ var FNV_OFFSET = 2166136261;
13626
+ var FNV_PRIME = 16777619;
13627
+ var MIX = 2654435761;
13628
+ function createRng2(seed) {
13629
+ let state = seed >>> 0;
13630
+ return () => {
13631
+ state = state + 1831565813 >>> 0;
13632
+ let mixed = Math.imul(state ^ state >>> 15, state | 1);
13633
+ mixed ^= mixed + Math.imul(mixed ^ mixed >>> 7, mixed | 61);
13634
+ return ((mixed ^ mixed >>> 14) >>> 0) / 4294967296;
13635
+ };
13636
+ }
13637
+ function streamSeed(seed, name) {
13638
+ const label = String(name);
13639
+ let hash = FNV_OFFSET;
13640
+ for (let index = 0; index < label.length; index++) hash = Math.imul(hash ^ label.charCodeAt(index), FNV_PRIME) >>> 0;
13641
+ return Math.imul(seed >>> 0 ^ hash, MIX) >>> 0;
13642
+ }
13643
+ function streamFor(seed, name) {
13644
+ return createRng2(streamSeed(seed, name));
13645
+ }
13646
+ var DISTANCE_CURVES = {
13647
+ /** The game's `sfx`: `vol / (1 + d² × 0.004)`, cutoff `/ (1 + d × 0.01)`, silent below 0.01 (jogo L62091–62092, L62101). */
13648
+ game_sfx: { model: "inverse_square", k: 4e-3, cutoffK: 0.01, minGain: 0.01 },
13649
+ /** The game's `grito`: `1 / (1 + d² × 0.006)`, silent below 0.02 (jogo L62129–62130). */
13650
+ game_scream: { model: "inverse_square", k: 6e-3, minGain: 0.02 },
13651
+ /** The game's `voz`: `1 / (1 + d² × 0.01)`, silent below 0.02 (jogo L62146–62147). */
13652
+ game_voice: { model: "inverse_square", k: 0.01, minGain: 0.02 },
13653
+ /** The game's `boom`: `1.4 / (1 + d² × 0.002)`, never silent (jogo L68280). */
13654
+ game_boom: { model: "inverse_square", k: 2e-3, scale: 1.4 },
13655
+ /** The art scene's `outAt`: `vol / (1 + d² × 0.008)` (cena L1320). */
13656
+ art_scene: { model: "inverse_square", k: 8e-3 },
13657
+ /** The game's animal calls: `max(0, 1 − d / 70) × 0.25`, silent at or below 0.005 (jogo L66627–66628). */
13658
+ animals: { model: "linear", range: 70, scale: 0.25, minGain: 5e-3 }
13659
+ };
13660
+ function falloffGain(falloff, volume, distance2) {
13661
+ if (!falloff || falloff.model === "none") return volume;
13662
+ if (falloff.model === "linear") {
13663
+ const level2 = Math.max(0, 1 - distance2 / falloff.range) * volume;
13664
+ return falloff.scale === void 0 ? level2 : level2 * falloff.scale;
13665
+ }
13666
+ const level = volume / (1 + distance2 * distance2 * falloff.k);
13667
+ return falloff.scale === void 0 ? level : falloff.scale * level;
13668
+ }
13669
+ function isSilent(falloff, gain) {
13670
+ if (!falloff || falloff.model === "none" || falloff.minGain === void 0) return false;
13671
+ return falloff.model === "linear" ? gain <= falloff.minGain : gain < falloff.minGain;
13672
+ }
13673
+ function cutoffDivisor(falloff, distance2) {
13674
+ if (!falloff || falloff.model !== "inverse_square" || falloff.cutoffK === void 0) return 1;
13675
+ return 1 + distance2 * falloff.cutoffK;
13676
+ }
13677
+ var DEFAULT_LISTENER = { position: [0, 0, 0], yaw: 0 };
13678
+ var PAN_LIMIT = 0.85;
13679
+ var PAN_MIN_DISTANCE = 2;
13680
+ function listenerDistance(listener, position, mode = "xz") {
13681
+ const dx = position[0] - listener.position[0];
13682
+ const dz = position[2] - listener.position[2];
13683
+ if (mode === "xz") return Math.hypot(dx, dz);
13684
+ return Math.hypot(dx, position[1] - listener.position[1], dz);
13685
+ }
13686
+ function panFor(listener, position) {
13687
+ const rightX = Math.cos(listener.yaw);
13688
+ const rightZ = -Math.sin(listener.yaw);
13689
+ const dx = position[0] - listener.position[0];
13690
+ const dz = position[2] - listener.position[2];
13691
+ const distance2 = Math.hypot(dx, dz) || 1;
13692
+ const pan = (dx * rightX + dz * rightZ) / Math.max(distance2, PAN_MIN_DISTANCE);
13693
+ return pan < -PAN_LIMIT ? -PAN_LIMIT : pan > PAN_LIMIT ? PAN_LIMIT : pan;
13694
+ }
13695
+ var voice_groups_default = {
13696
+ undead_voice: {
13697
+ maxConcurrent: 5,
13698
+ cooldown: 0.25,
13699
+ maxDistance: 40,
13700
+ sources: [
13701
+ "cena:L1342 `AU.groans > 4` refuses, so five play at once",
13702
+ "jogo:L66986 one groan every 0.25 s (`groanBudget`), only nearer than 40 m",
13703
+ "jogo:L70472 the budget counts down by the frame time"
13704
+ ]
13705
+ }
13706
+ };
13707
+ var VOICE_GROUPS = voice_groups_default;
13708
+ var BUS_PRIORITY = { ui: 5, weapons: 4, voices: 3, world: 2, weather: 1, ambience: 1 };
13709
+ var DEFAULT_MAX_VOICES = 32;
13710
+ function busPriority(bus) {
13711
+ return BUS_PRIORITY[bus];
13712
+ }
13713
+ function createVoiceManager(options = {}) {
13714
+ const groups = new Map(Object.entries(options.groups ?? VOICE_GROUPS));
13715
+ const maxVoices = options.maxVoices ?? DEFAULT_MAX_VOICES;
13716
+ const budgets = /* @__PURE__ */ new Map();
13717
+ let voices = [];
13718
+ let nextId = 1;
13719
+ let peak = 0;
13720
+ let dropped = 0;
13721
+ const refuse = (reason) => {
13722
+ dropped += 1;
13723
+ return { accepted: false, reason };
13724
+ };
13725
+ const groupCheck = (request, evicted) => {
13726
+ const group = request.group === void 0 ? void 0 : groups.get(request.group);
13727
+ if (!group) return void 0;
13728
+ if (group.maxDistance !== void 0 && !((request.distance ?? 0) < group.maxDistance)) return "distance";
13729
+ if (group.cooldown !== void 0 && (budgets.get(request.group) ?? 0) > 0) return "cooldown";
13730
+ const members = voices.filter((voice) => voice.group === request.group);
13731
+ if (group.maxConcurrent === void 0 || members.length < group.maxConcurrent) return void 0;
13732
+ if (!group.steal) return "group_full";
13733
+ evicted.push(members[0].id);
13734
+ return void 0;
13735
+ };
13736
+ return {
13737
+ request(request) {
13738
+ const evicted = [];
13739
+ const refusal = groupCheck(request, evicted);
13740
+ if (refusal) return refuse(refusal);
13741
+ const score = request.priority * request.gain;
13742
+ const remaining = voices.filter((voice) => !evicted.includes(voice.id));
13743
+ if (remaining.length >= maxVoices) {
13744
+ const lowest = remaining.reduce((low, voice) => voice.score < low.score ? voice : low);
13745
+ if (score <= lowest.score) return refuse("global_full");
13746
+ evicted.push(lowest.id);
13747
+ dropped += 1;
13748
+ }
13749
+ const group = request.group === void 0 ? void 0 : groups.get(request.group);
13750
+ if (group?.cooldown !== void 0) budgets.set(request.group, group.cooldown);
13751
+ const id = nextId++;
13752
+ voices = [...voices.filter((voice) => !evicted.includes(voice.id)), { id, ...request.group === void 0 ? {} : { group: request.group }, score, remaining: request.length }];
13753
+ peak = Math.max(peak, voices.length);
13754
+ return { accepted: true, id, evicted };
13755
+ },
13756
+ release(id) {
13757
+ voices = voices.filter((voice) => voice.id !== id);
13758
+ },
13759
+ setLength(id, seconds) {
13760
+ const voice = voices.find((entry) => entry.id === id);
13761
+ if (voice) voice.remaining = seconds;
13762
+ },
13763
+ advance(dt) {
13764
+ for (const [group, budget] of budgets) budgets.set(group, budget - dt);
13765
+ const ended = [];
13766
+ for (const voice of voices) {
13767
+ voice.remaining -= dt;
13768
+ if (voice.remaining <= 0) ended.push(voice.id);
13769
+ }
13770
+ if (ended.length > 0) voices = voices.filter((voice) => voice.remaining > 0);
13771
+ return ended;
13772
+ },
13773
+ setGroup(id, group) {
13774
+ if (group) groups.set(id, group);
13775
+ else groups.delete(id);
13776
+ },
13777
+ stats: () => ({ active: voices.length, peak, dropped, maxVoices })
13778
+ };
13779
+ }
13780
+ var DEFAULT_TAIL = 0.05;
13781
+ var DEFAULT_NOISE_SECONDS = 1.5;
13782
+ var OCCLUSION_GAIN = 0.5;
13783
+ var OCCLUSION_CUTOFF = 0.6;
13784
+ function isParamRef(range) {
13785
+ return typeof range === "object" && !Array.isArray(range);
13786
+ }
13787
+ function draw(range, scope2) {
13788
+ if (typeof range === "number") return range;
13789
+ if (isParamRef(range)) {
13790
+ const value = scope2.params[range.param];
13791
+ if (value === void 0) throw new Error(`game-audio: the recipe reads the param "${range.param}", which was not given`);
13792
+ return value * (range.scale ?? 1) + (range.add ?? 0);
13793
+ }
13794
+ const [min, max] = range;
13795
+ return min + scope2.random() * (max - min);
13796
+ }
13797
+ function drawTime(time, scope2) {
13798
+ if (time === "duration" || typeof time === "string" && time.endsWith("d")) return time;
13799
+ return draw(time, scope2);
13800
+ }
13801
+ function timeOf(time, duration) {
13802
+ if (typeof time === "number") return time;
13803
+ if (time === "duration") return duration;
13804
+ return Number(time.slice(0, -1)) * duration;
13805
+ }
13806
+ function drawAutomation(automation, scope2) {
13807
+ const pending = {};
13808
+ for (const key of Object.keys(automation)) {
13809
+ if (key === "set" && "set" in automation) pending.value = draw(automation.set, scope2);
13810
+ else if (key === "from" && "from" in automation) pending.from = draw(automation.from, scope2);
13811
+ else if (key === "to" && automation.to !== void 0) pending.to = automation.to === "from" ? pending.from ?? pending.value ?? 0 : draw(automation.to, scope2);
13812
+ else if (key === "curve") pending.curve = automation.curve;
13813
+ else if (key === "over" && automation.over !== void 0) pending.over = drawTime(automation.over, scope2);
13814
+ }
13815
+ return pending;
13816
+ }
13817
+ function finishAutomation(pending, duration) {
13818
+ if (pending.from !== void 0 && pending.to === void 0) return { from: pending.from };
13819
+ if (pending.from !== void 0) return { from: pending.from, to: pending.to, curve: pending.curve ?? "linear", at: timeOf(pending.over ?? "duration", duration) };
13820
+ if (pending.to === void 0 || pending.over === void 0) return { value: pending.value };
13821
+ return { value: pending.value, to: pending.to, curve: pending.curve ?? "linear", at: timeOf(pending.over, duration) };
13822
+ }
13823
+ var startFrequency = (automation) => automation.from ?? automation.value ?? 0;
13824
+ function drawFilter(spec, scope2) {
13825
+ const pending = { spec, frequency: {} };
13826
+ for (const key of Object.keys(spec)) {
13827
+ if (key === "frequency") pending.frequency = drawAutomation(spec.frequency, scope2);
13828
+ else if (key === "q" && spec.q !== void 0) pending.q = draw(spec.q, scope2);
13829
+ else if (key === "gain" && spec.gain !== void 0) pending.gain = draw(spec.gain, scope2);
13830
+ else if (key === "level" && spec.level !== void 0) pending.level = draw(spec.level, scope2);
13831
+ }
13832
+ return pending;
13833
+ }
13834
+ function drawModulator(modulator, scope2) {
13835
+ const pending = { modulator, rate: 0 };
13836
+ for (const key of Object.keys(modulator)) {
13837
+ if (key === "rate") pending.rate = draw(modulator.rate, scope2);
13838
+ else if (key === "depth" && !(typeof modulator.depth === "object" && "scaleOf" in modulator.depth)) pending.depth = draw(modulator.depth, scope2);
13839
+ }
13840
+ return pending;
13841
+ }
13842
+ function drawVoice(source, scope2) {
13843
+ const pending = { frequency: {}, vibratoRate: 0, formants: [], breath: { frequency: 0, q: source.breath.q, gain: 0 } };
13844
+ for (const key of Object.keys(source)) {
13845
+ if (key === "frequency") pending.frequency = drawAutomation(source.frequency, scope2);
13846
+ else if (key === "vibrato") pending.vibratoRate = draw(source.vibrato.rate, scope2);
13847
+ else if (key === "formants") pending.formants = source.formants.map((formant) => ({ frequency: draw(formant.frequency, scope2), q: formant.q }));
13848
+ else if (key === "breath") pending.breath = { frequency: draw(source.breath.frequency, scope2), q: source.breath.q, gain: draw(source.breath.gain, scope2) };
13849
+ }
13850
+ return pending;
13851
+ }
13852
+ function drawLayer(layer, scope2) {
13853
+ const pending = { at: 0, gain: 1, filters: [], modulators: [] };
13854
+ for (const key of Object.keys(layer)) {
13855
+ if (key === "at" && layer.at !== void 0) pending.at = draw(layer.at, scope2);
13856
+ else if (key === "duration" && layer.duration !== void 0) pending.duration = draw(layer.duration, scope2);
13857
+ else if (key === "gain" && layer.gain !== void 0) pending.gain = draw(layer.gain, scope2);
13858
+ else if (key === "source" && layer.source.kind === "osc") pending.automation = drawAutomation(layer.source.frequency, scope2);
13859
+ else if (key === "source" && layer.source.kind === "formant_voice") pending.voice = drawVoice(layer.source, scope2);
13860
+ else if (key === "filters" && layer.filters) pending.filters = layer.filters.bank.map((spec) => drawFilter(spec, scope2));
13861
+ else if (key === "modulators" && layer.modulators) pending.modulators = layer.modulators.map((modulator) => drawModulator(modulator, scope2));
13862
+ }
13863
+ return pending;
13864
+ }
13865
+ function drawOffset(source, duration, scope2, noiseSeconds) {
13866
+ if (source.kind === "formant_voice") return scope2.random();
13867
+ if (source.kind !== "noise" || source.offset === void 0) return 0;
13868
+ if (typeof source.offset === "number") return source.offset;
13869
+ if (source.offset === "fit") return scope2.random() * Math.max(0, noiseSeconds - duration);
13870
+ return scope2.random() * (source.offsetScale ?? 1);
13871
+ }
13872
+ function finishSource(layer, pending, duration, offset) {
13873
+ const source = layer.source;
13874
+ if (source.kind === "noise") return { kind: "noise", offset, loop: source.loop === true };
13875
+ if (source.kind === "osc") return { kind: "osc", wave: source.wave, frequency: finishAutomation(pending.automation ?? {}, duration) };
13876
+ const voice = pending.voice;
13877
+ return {
13878
+ kind: "formant_voice",
13879
+ wave: source.wave,
13880
+ frequency: finishAutomation(voice.frequency, duration),
13881
+ vibrato: { rate: voice.vibratoRate, depth: startFrequency(voice.frequency) * source.vibrato.depth },
13882
+ formants: voice.formants,
13883
+ breath: { ...voice.breath, offset },
13884
+ mix: source.mix
13885
+ };
13886
+ }
13887
+ function finishModulator(pending, sourceFrequency) {
13888
+ const { modulator } = pending;
13889
+ const depth = typeof modulator.depth === "object" && "scaleOf" in modulator.depth ? sourceFrequency * modulator.depth.factor : pending.depth ?? 0;
13890
+ return { target: modulator.target, ...modulator.wave ? { wave: modulator.wave } : {}, rate: pending.rate, depth, ...modulator.base === void 0 ? {} : { base: modulator.base } };
13891
+ }
13892
+ function finishEnvelope(envelope, duration, level) {
13893
+ return envelope.map(([time, value, curve, fixed2]) => ({ time: timeOf(time, duration), value: fixed2 === "fixed" ? value : value * level, curve }));
13894
+ }
13895
+ function planLayer(layer, scope2, defaults, levelInEnvelope, soundLevel) {
13896
+ const pending = drawLayer(layer, scope2);
13897
+ const duration = pending.duration ?? defaults.duration;
13898
+ if (duration === void 0) throw new Error("game-audio: a layer needs a duration (its own or the recipe\u2019s)");
13899
+ const offset = drawOffset(layer.source, duration, scope2, defaults.noiseSeconds);
13900
+ const level = pending.gain * soundLevel;
13901
+ const sourceFrequency = layer.source.kind === "noise" ? 0 : startFrequency(pending.automation ?? pending.voice?.frequency ?? {});
13902
+ const planned = {
13903
+ at: pending.at,
13904
+ duration,
13905
+ tail: layer.tail ?? defaults.tail ?? DEFAULT_TAIL,
13906
+ level,
13907
+ ...layer.wet === void 0 ? {} : { wet: layer.wet },
13908
+ source: finishSource(layer, pending, duration, offset),
13909
+ ...layer.filters ? {
13910
+ filters: {
13911
+ mode: layer.filters.mode,
13912
+ bank: pending.filters.map(({ spec, frequency, q, gain, level: filterLevel }) => ({
13913
+ type: spec.type,
13914
+ frequency: finishAutomation(frequency, duration),
13915
+ ...q === void 0 ? {} : { q },
13916
+ ...gain === void 0 ? {} : { gain },
13917
+ ...filterLevel === void 0 ? {} : { level: filterLevel }
13918
+ }))
13919
+ }
13920
+ } : {},
13921
+ modulators: pending.modulators.map((modulator) => finishModulator(modulator, sourceFrequency)),
13922
+ envelope: finishEnvelope(layer.envelope, duration, levelInEnvelope ? level : 1)
13923
+ };
13924
+ if (!layer.repeat) return [planned];
13925
+ return Array.from({ length: layer.repeat.count }, (_, index) => ({ ...planned, at: planned.at + index * (layer.repeat?.spacing ?? 0) }));
13926
+ }
13927
+ function scaleAutomation(automation, divisor) {
13928
+ const scale = (value) => value === void 0 ? void 0 : value / divisor;
13929
+ const scaled = { ...automation };
13930
+ for (const key of ["value", "from", "to"]) if (automation[key] !== void 0) scaled[key] = scale(automation[key]);
13931
+ return scaled;
13932
+ }
13933
+ function muffle(layer, divisor) {
13934
+ if (divisor === 1 || !layer.filters) return layer;
13935
+ const bank = layer.filters.bank.map((filter) => filter.type === "lowpass" ? { ...filter, frequency: scaleAutomation(filter.frequency, divisor) } : filter);
13936
+ return { ...layer, filters: { ...layer.filters, bank } };
13937
+ }
13938
+ var layerEnd = (layer) => layer.at + layer.duration + layer.tail;
13939
+ function planSound(recipe, input = {}) {
13940
+ const listener = input.listener ?? DEFAULT_LISTENER;
13941
+ const distance2 = input.distance ?? (input.position ? listenerDistance(listener, input.position, input.distanceMode) : 0);
13942
+ const occlusion = Math.min(1, Math.max(0, input.occlusion ?? 0));
13943
+ const order = recipe.order ?? "layered";
13944
+ const falloffLevel = falloffGain(recipe.falloff, input.volume ?? 1, distance2);
13945
+ const level = occlusion > 0 ? falloffLevel * (1 - OCCLUSION_GAIN * occlusion) : falloffLevel;
13946
+ const silent = isSilent(recipe.falloff, falloffLevel);
13947
+ const base = {
13948
+ id: recipe.id,
13949
+ bus: recipe.bus,
13950
+ order,
13951
+ ...recipe.group === void 0 ? {} : { group: recipe.group },
13952
+ priority: recipe.priority ?? busPriority(recipe.bus),
13953
+ level,
13954
+ distance: distance2,
13955
+ ...recipe.pan ? { pan: input.position ? panFor(listener, input.position) : 0 } : {}
13956
+ };
13957
+ if (silent) return recipe.keepSilent ? { ...base, silent: true, length: 0, layers: [] } : null;
13958
+ const scope2 = { random: input.random ?? createRng2(1), params: { ...input.params } };
13959
+ for (const [name, range] of Object.entries(recipe.vars ?? {})) if (scope2.params[name] === void 0) scope2.params[name] = draw(range, scope2);
13960
+ const defaults = {
13961
+ ...recipe.duration === void 0 ? {} : { duration: draw(recipe.duration, scope2) },
13962
+ ...recipe.tail === void 0 ? {} : { tail: recipe.tail },
13963
+ noiseSeconds: input.noiseSeconds ?? DEFAULT_NOISE_SECONDS
13964
+ };
13965
+ const divisor = cutoffDivisor(recipe.falloff, distance2) / (1 - OCCLUSION_CUTOFF * occlusion);
13966
+ const layers = recipe.layers.flatMap((layer) => planLayer(layer, scope2, defaults, order !== "layered", level)).map((layer) => muffle(layer, divisor));
13967
+ return { ...base, silent: false, length: Math.max(0, ...layers.map(layerEnd)), layers };
13968
+ }
13969
+ var LOOP_START_ABOVE = 2e-3;
13970
+ var LOOP_SMOOTHING = { volume: 0.15, frequency: 0.08 };
13971
+ function createLoopState(recipe, open = false) {
13972
+ const startAbove = recipe.startAbove ?? LOOP_START_ABOVE;
13973
+ let created = open;
13974
+ let volume = recipe.volume ?? 0;
13975
+ return {
13976
+ set(state) {
13977
+ volume = state.volume ?? volume;
13978
+ const create = !created;
13979
+ if (create && !(volume >= startAbove)) return null;
13980
+ created = true;
13981
+ const frequency = recipe.source.kind === "osc" && state.frequency ? state.frequency : void 0;
13982
+ return { create, volume, ...frequency === void 0 ? {} : { frequency } };
13983
+ },
13984
+ get created() {
13985
+ return created;
13986
+ },
13987
+ reset() {
13988
+ created = false;
13989
+ }
13990
+ };
13991
+ }
13992
+ var mix_profiles_default = [
13993
+ {
13994
+ id: "game",
13995
+ master: 0.5,
13996
+ noise: { seconds: 1.5, create: "lazy" },
13997
+ sources: ["jogo:L62077-62081 master 0.5 to the output", "jogo:L62082-62088 noise 1.5 s made by the first sound that needs it"]
13998
+ },
13999
+ {
14000
+ id: "art_scene",
14001
+ master: 0.85,
14002
+ compressor: { threshold: -12, ratio: 5 },
14003
+ reverb: { seconds: 2.2, decay: 3.4, wet: 0.4 },
14004
+ noise: { seconds: 2, create: "eager" },
14005
+ sources: [
14006
+ "cena:L1308 compressor -12 dB, ratio 5",
14007
+ "cena:L1309 master 0.85",
14008
+ "cena:L1310 convolver, impulse 2.2 s of (1 - i/len)^3.4",
14009
+ "cena:L1311 wet 0.4",
14010
+ "cena:L1312 noise 2 s made with the chain"
14011
+ ]
14012
+ }
14013
+ ];
14014
+ var MIX_PROFILES = Object.fromEntries(
14015
+ mix_profiles_default.map((profile) => [profile.id, profile])
14016
+ );
14017
+ var DEFAULT_MIX = "art_scene";
14018
+ function resolveMix(mix2, profiles = MIX_PROFILES) {
14019
+ if (typeof mix2 !== "string") return mix2;
14020
+ const profile = profiles[mix2];
14021
+ if (!profile) throw new Error(`game-audio: unknown mix profile "${mix2}" (known: ${Object.keys(profiles).join(", ")})`);
14022
+ return profile;
14023
+ }
14024
+ function busGain(setting) {
14025
+ if (!setting) return 1;
14026
+ return setting.muted ? 0 : setting.gain ?? 1;
14027
+ }
14028
+ function isPlainObject2(value) {
14029
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14030
+ }
14031
+ function mergeRecords(base, patch) {
14032
+ const merged = { ...base };
14033
+ for (const [key, value] of Object.entries(patch)) {
14034
+ if (value === void 0) continue;
14035
+ const current = merged[key];
14036
+ merged[key] = isPlainObject2(current) && isPlainObject2(value) ? mergeRecords(current, value) : value;
14037
+ }
14038
+ return merged;
14039
+ }
14040
+ function createSoundRegistry(banks = []) {
14041
+ const sounds = /* @__PURE__ */ new Map();
14042
+ const loops = /* @__PURE__ */ new Map();
14043
+ const owners = /* @__PURE__ */ new Map();
14044
+ const registry = {
14045
+ register(bank) {
14046
+ for (const sound2 of bank.sounds ?? []) {
14047
+ sounds.set(sound2.id, sound2);
14048
+ owners.set(sound2.id, bank.id);
14049
+ }
14050
+ for (const loop of bank.loops ?? []) {
14051
+ loops.set(loop.id, loop);
14052
+ owners.set(loop.id, bank.id);
14053
+ }
14054
+ },
14055
+ patch(id, patch) {
14056
+ const sound2 = sounds.get(id);
14057
+ if (sound2) sounds.set(id, mergeRecords(sound2, patch));
14058
+ const loop = loops.get(id);
14059
+ if (loop) loops.set(id, mergeRecords(loop, patch));
14060
+ },
14061
+ remove(id) {
14062
+ sounds.delete(id);
14063
+ loops.delete(id);
14064
+ owners.delete(id);
14065
+ },
14066
+ sound: (id) => sounds.get(id),
14067
+ loop: (id) => loops.get(id),
14068
+ soundIds: () => [...sounds.keys()],
14069
+ loopIds: () => [...loops.keys()],
14070
+ bankOf: (id) => owners.get(id)
14071
+ };
14072
+ for (const bank of banks) registry.register(bank);
14073
+ return registry;
14074
+ }
14075
+ var game_sfx_default = {
14076
+ sources: ["jogo:L62089-62107"],
14077
+ note: "The game's sfx table P: [lowpass cutoff Hz, peak gain, seconds]. Played as a noise burst with the game's distance curve (falloff game_sfx) and a 0.05 s tail. Team names in `team`.",
14078
+ classes: [
14079
+ { id: "pistol", team: "pistola", bus: "weapons", cutoff: 1800, gain: 0.9, seconds: 0.18 },
14080
+ { id: "shotgun", team: "escopeta", bus: "weapons", cutoff: 900, gain: 1.2, seconds: 0.35 },
14081
+ { id: "rifle", team: "fuzil", bus: "weapons", cutoff: 1400, gain: 1.1, seconds: 0.28 },
14082
+ { id: "long_rifle", team: "rifle", bus: "weapons", cutoff: 1100, gain: 1.3, seconds: 0.45 },
14083
+ { id: "impact", team: "impacto", bus: "world", cutoff: 2500, gain: 0.25, seconds: 0.05 },
14084
+ { id: "flesh", team: "carne", bus: "world", cutoff: 600, gain: 0.35, seconds: 0.08 },
14085
+ { id: "step", team: "passo", bus: "world", cutoff: 400, gain: 0.08, seconds: 0.05 },
14086
+ { id: "glass", team: "vidro", bus: "world", cutoff: 5e3, gain: 0.5, seconds: 0.3 },
14087
+ { id: "wood", team: "madeira", bus: "world", cutoff: 700, gain: 0.5, seconds: 0.2 },
14088
+ { id: "reload", team: "recarga", bus: "weapons", cutoff: 3e3, gain: 0.15, seconds: 0.06 },
14089
+ { id: "empty", team: "vazio", bus: "weapons", cutoff: 4e3, gain: 0.12, seconds: 0.03 },
14090
+ { id: "explosion", team: "explosao", bus: "world", cutoff: 200, gain: 1.6, seconds: 1.4 }
14091
+ ],
14092
+ fallback: { id: "unknown", team: "(any other name)", bus: "world", cutoff: 1e3, gain: 0.5, seconds: 0.1 }
14093
+ };
14094
+ var GAME_SFX = game_sfx_default;
14095
+ var GAME_SFX_CLASSES = GAME_SFX.classes;
14096
+ var GAME_SFX_FALLBACK = GAME_SFX.fallback;
14097
+ function noiseClassRecipe(row, id = `sfx_${row.id}`) {
14098
+ return {
14099
+ id,
14100
+ bus: row.bus,
14101
+ order: "burst",
14102
+ duration: row.seconds,
14103
+ tail: 0.05,
14104
+ layers: [
14105
+ {
14106
+ source: { kind: "noise", offset: "random" },
14107
+ filters: { mode: "serial", bank: [{ type: "lowpass", frequency: { set: row.cutoff } }] },
14108
+ envelope: [
14109
+ [0, row.gain, "set"],
14110
+ ["1d", 1e-3, "exp", "fixed"]
14111
+ ]
14112
+ }
14113
+ ],
14114
+ falloff: DISTANCE_CURVES.game_sfx,
14115
+ sources: [...GAME_SFX.sources.map((source) => `${source} ${row.team}`)]
14116
+ };
14117
+ }
14118
+ var GAME_SFX_BANK = { id: "game_sfx", sounds: [...GAME_SFX_CLASSES, GAME_SFX_FALLBACK].map((row) => noiseClassRecipe(row)) };
14119
+ function fillNoise(random, length2) {
14120
+ const samples = new Float32Array(length2);
14121
+ for (let index = 0; index < length2; index++) samples[index] = random() * 2 - 1;
14122
+ return samples;
14123
+ }
14124
+ function fillImpulse(random, options) {
14125
+ const size = Math.floor(options.length);
14126
+ return Array.from({ length: options.channels }, () => {
14127
+ const samples = new Float32Array(size);
14128
+ for (let index = 0; index < options.length; index++) {
14129
+ const value = (random() * 2 - 1) * Math.exp(options.decay * Math.log(1 - index / options.length));
14130
+ if (index < size) samples[index] = value;
14131
+ }
14132
+ return samples;
14133
+ });
14134
+ }
14135
+ function bufferOf(context, channels, requestedLength) {
14136
+ const buffer = context.createBuffer(channels.length, requestedLength, context.sampleRate);
14137
+ channels.forEach((samples, channel) => buffer.getChannelData(channel).set(samples));
14138
+ return buffer;
14139
+ }
14140
+ function buildChain(context, profile, random) {
14141
+ let compressor;
14142
+ if (profile.compressor) {
14143
+ compressor = context.createDynamicsCompressor();
14144
+ compressor.threshold.value = profile.compressor.threshold;
14145
+ compressor.ratio.value = profile.compressor.ratio;
14146
+ if (profile.compressor.knee !== void 0) compressor.knee.value = profile.compressor.knee;
14147
+ if (profile.compressor.attack !== void 0) compressor.attack.value = profile.compressor.attack;
14148
+ if (profile.compressor.release !== void 0) compressor.release.value = profile.compressor.release;
14149
+ compressor.connect(context.destination);
14150
+ }
14151
+ const master = context.createGain();
14152
+ master.gain.value = profile.master;
14153
+ master.connect(compressor ?? context.destination);
14154
+ if (!profile.reverb) {
14155
+ return {
14156
+ master,
14157
+ disconnect: () => {
14158
+ master.disconnect();
14159
+ compressor?.disconnect();
14160
+ }
14161
+ };
14162
+ }
14163
+ const convolver = context.createConvolver();
14164
+ const length2 = context.sampleRate * profile.reverb.seconds;
14165
+ convolver.buffer = bufferOf(context, fillImpulse(random, { channels: 2, length: length2, decay: profile.reverb.decay }), length2);
14166
+ const wet = context.createGain();
14167
+ wet.gain.value = profile.reverb.wet;
14168
+ wet.connect(convolver);
14169
+ convolver.connect(master);
14170
+ return {
14171
+ master,
14172
+ wet,
14173
+ disconnect: () => {
14174
+ wet.disconnect();
14175
+ convolver.disconnect();
14176
+ master.disconnect();
14177
+ compressor?.disconnect();
14178
+ }
14179
+ };
14180
+ }
14181
+ function globalContext() {
14182
+ const scope2 = globalThis;
14183
+ const Constructor = scope2.AudioContext ?? scope2.webkitAudioContext;
14184
+ return Constructor ? new Constructor() : null;
14185
+ }
14186
+ function unlockContext(context) {
14187
+ if (context.state === "suspended") void context.resume();
14188
+ }
14189
+ function assign(param, automation, time) {
14190
+ if (automation.value !== void 0) param.value = automation.value;
14191
+ if (automation.from !== void 0) param.setValueAtTime(automation.from, time);
14192
+ }
14193
+ function ramp(param, curve, value, at) {
14194
+ if (curve === "set") param.setValueAtTime(value, at);
14195
+ else if (curve === "exp") param.exponentialRampToValueAtTime(value, at);
14196
+ else param.linearRampToValueAtTime(value, at);
14197
+ }
14198
+ function rampTo(param, automation, time) {
14199
+ if (automation.to !== void 0 && automation.at !== void 0) ramp(param, automation.curve ?? "linear", automation.to, time + automation.at);
14200
+ }
14201
+ function applyEnvelope(param, points, time) {
14202
+ for (const point of points) ramp(param, point.curve, point.value, time + point.time);
14203
+ }
14204
+ function noiseSource(target, loop) {
14205
+ const source = target.context.createBufferSource();
14206
+ source.buffer = target.noise();
14207
+ if (loop) source.loop = true;
14208
+ return source;
14209
+ }
14210
+ function filterNode(context, filter, time) {
14211
+ const node = context.createBiquadFilter();
14212
+ node.type = filter.type;
14213
+ assign(node.frequency, filter.frequency, time);
14214
+ if (filter.q !== void 0) node.Q.value = filter.q;
14215
+ if (filter.gain !== void 0) node.gain.value = filter.gain;
14216
+ return node;
14217
+ }
14218
+ function gainNode(context, value) {
14219
+ const node = context.createGain();
14220
+ node.gain.value = value;
14221
+ return node;
14222
+ }
14223
+ function voiceOf(sources) {
14224
+ return {
14225
+ sources,
14226
+ stop() {
14227
+ for (const source of sources) {
14228
+ try {
14229
+ source.stop();
14230
+ } catch {
14231
+ }
14232
+ }
14233
+ }
14234
+ };
14235
+ }
14236
+ var fixed = (range) => typeof range === "number" ? range : 0;
14237
+ var fixedFrequency = (automation) => fixed("set" in automation ? automation.set : automation.from);
14238
+ function sourceOf(recipe, target) {
14239
+ if (recipe.source.kind === "noise") return { source: noiseSource(target, true) };
14240
+ const oscillator = target.context.createOscillator();
14241
+ oscillator.type = recipe.source.wave;
14242
+ if (recipe.source.frequency) oscillator.frequency.value = fixedFrequency(recipe.source.frequency);
14243
+ return { source: oscillator, frequency: oscillator.frequency };
14244
+ }
14245
+ function stopAll(sources) {
14246
+ return () => {
14247
+ for (const source of sources) {
14248
+ try {
14249
+ source.stop();
14250
+ } catch {
14251
+ }
14252
+ }
14253
+ };
14254
+ }
14255
+ function realizeGameLoop(recipe, target) {
14256
+ const { context } = target;
14257
+ const gain = context.createGain();
14258
+ const filters = (recipe.filters?.bank ?? []).map(() => context.createBiquadFilter());
14259
+ gain.gain.value = 0;
14260
+ const last = filters.reduce((previous, filter) => previous ? previous.connect(filter) : filter, void 0);
14261
+ last?.connect(gain);
14262
+ gain.connect(target.output);
14263
+ const { source, frequency } = sourceOf(recipe, target);
14264
+ recipe.filters?.bank.forEach((spec, index) => {
14265
+ const filter = filters[index];
14266
+ filter.type = spec.type;
14267
+ filter.frequency.value = fixedFrequency(spec.frequency);
14268
+ if (spec.q !== void 0) filter.Q.value = fixed(spec.q);
14269
+ });
14270
+ const sources = [source];
14271
+ for (const modulator of recipe.modulators ?? []) {
14272
+ const lfo = context.createOscillator();
14273
+ const depth = context.createGain();
14274
+ const stage2 = context.createGain();
14275
+ if (modulator.wave) lfo.type = modulator.wave;
14276
+ lfo.frequency.value = fixed(modulator.rate);
14277
+ depth.gain.value = fixed(modulator.depth);
14278
+ stage2.gain.value = modulator.base ?? 0;
14279
+ lfo.connect(depth);
14280
+ depth.connect(stage2.gain);
14281
+ last?.disconnect();
14282
+ last?.connect(stage2);
14283
+ stage2.connect(gain);
14284
+ lfo.start();
14285
+ sources.push(lfo);
14286
+ }
14287
+ source.connect(filters[0] ?? gain);
14288
+ source.start();
14289
+ return { gain, source, ...frequency ? { frequency } : {}, stop: stopAll(sources) };
14290
+ }
14291
+ function realizeChainLoop(recipe, target, volume) {
14292
+ const { context } = target;
14293
+ const { source, frequency } = sourceOf(recipe, target);
14294
+ const filters = (recipe.filters?.bank ?? []).map(
14295
+ (spec) => filterNode(context, { type: spec.type, frequency: { value: fixedFrequency(spec.frequency) }, ...spec.q === void 0 ? {} : { q: fixed(spec.q) } }, target.time)
14296
+ );
14297
+ const gain = gainNode(context, volume);
14298
+ const lfos = (recipe.modulators ?? []).map((modulator) => {
14299
+ const lfo = context.createOscillator();
14300
+ if (modulator.wave) lfo.type = modulator.wave;
14301
+ lfo.frequency.value = fixed(modulator.rate);
14302
+ const depth = gainNode(context, fixed(modulator.depth));
14303
+ lfo.connect(depth);
14304
+ depth.connect(gain.gain);
14305
+ return lfo;
14306
+ });
14307
+ let tail = source;
14308
+ for (const filter of filters) tail = tail.connect(filter);
14309
+ tail.connect(gain);
14310
+ gain.connect(target.output);
14311
+ source.start();
14312
+ for (const lfo of lfos) lfo.start();
14313
+ return { gain, source, ...frequency ? { frequency } : {}, stop: stopAll([source, ...lfos]) };
14314
+ }
14315
+ var INERT = { set() {
14316
+ }, stop() {
14317
+ } };
14318
+ function createLoopHandle(recipe, host, volume) {
14319
+ if (!recipe) return INERT;
14320
+ const loopRecipe = recipe;
14321
+ const isChain = loopRecipe.order === "chain_loop";
14322
+ const opening = volume ?? loopRecipe.volume ?? 0;
14323
+ const smoothing = loopRecipe.smoothing ?? LOOP_SMOOTHING;
14324
+ let state = createLoopState(loopRecipe);
14325
+ let nodes;
14326
+ const entry = {
14327
+ get created() {
14328
+ return nodes !== void 0;
14329
+ },
14330
+ rewire() {
14331
+ nodes?.gain.disconnect();
14332
+ nodes?.gain.connect(host.output(loopRecipe.bus));
14333
+ },
14334
+ stop() {
14335
+ nodes?.stop();
14336
+ nodes?.gain.disconnect();
14337
+ nodes = void 0;
14338
+ state = createLoopState(loopRecipe);
14339
+ host.unregister(entry);
14340
+ }
14341
+ };
14342
+ const create = (context, startVolume) => {
14343
+ if (loopRecipe.source.kind === "noise") host.prepareNoise();
14344
+ const target = { context, output: host.output(loopRecipe.bus), noise: host.noise, time: context.currentTime };
14345
+ const made = isChain ? realizeChainLoop(loopRecipe, target, startVolume) : realizeGameLoop(loopRecipe, target);
14346
+ host.register(entry);
14347
+ return made;
14348
+ };
14349
+ const open = () => {
14350
+ const context = host.ready();
14351
+ if (!context) return;
14352
+ nodes = create(context, opening);
14353
+ state = createLoopState({ ...loopRecipe, volume: opening }, true);
14354
+ };
14355
+ if (isChain) open();
14356
+ return {
14357
+ set(next) {
14358
+ if (isChain && !nodes) open();
14359
+ const context = host.ready();
14360
+ if (!context) return;
14361
+ const action = state.set(next);
14362
+ if (!action) return;
14363
+ nodes ?? (nodes = create(context, 0));
14364
+ nodes.gain.gain.setTargetAtTime(action.volume, context.currentTime, smoothing.volume);
14365
+ if (action.frequency !== void 0 && nodes.frequency) nodes.frequency.setTargetAtTime(action.frequency, context.currentTime, smoothing.frequency);
14366
+ },
14367
+ stop: entry.stop
14368
+ };
14369
+ }
14370
+ function createNoiseStore(random) {
14371
+ const store = {
14372
+ random,
14373
+ reset() {
14374
+ store.samples = void 0;
14375
+ store.buffer = void 0;
14376
+ }
14377
+ };
14378
+ return store;
14379
+ }
14380
+ function prepareNoise(store, context, seconds) {
14381
+ store.samples ?? (store.samples = fillNoise(store.random, Math.floor(context.sampleRate * seconds)));
14382
+ return store.samples;
14383
+ }
14384
+ function noiseBuffer(store, context, seconds) {
14385
+ const samples = prepareNoise(store, context, seconds);
14386
+ store.buffer ?? (store.buffer = bufferOf(context, [samples], context.sampleRate * seconds));
14387
+ return store.buffer;
14388
+ }
14389
+ var end = (layer, start) => start + layer.duration + layer.tail;
14390
+ var firstFilter = (layer) => layer.filters?.bank[0];
14391
+ var offsetOf = (layer) => layer.source.kind === "noise" ? layer.source.offset : 0;
14392
+ function setFilter(node, filter, time) {
14393
+ if (!filter) return;
14394
+ node.type = filter.type;
14395
+ assign(node.frequency, filter.frequency, time);
14396
+ if (filter.q !== void 0) node.Q.value = filter.q;
14397
+ }
14398
+ function realizeBurst(plan, target) {
14399
+ const { context } = target;
14400
+ const layer = plan.layers[0];
14401
+ const time = target.time + layer.at;
14402
+ const gain = context.createGain();
14403
+ gain.connect(target.output);
14404
+ const source = noiseSource(target, layer.source.kind === "noise" && layer.source.loop);
14405
+ const filter = context.createBiquadFilter();
14406
+ source.connect(filter);
14407
+ filter.connect(gain);
14408
+ setFilter(filter, firstFilter(layer), time);
14409
+ applyEnvelope(gain.gain, layer.envelope, time);
14410
+ source.start(time, offsetOf(layer));
14411
+ source.stop(end(layer, time));
14412
+ return voiceOf([source]);
14413
+ }
14414
+ function realizeScream(plan, target) {
14415
+ const { context } = target;
14416
+ const layer = plan.layers[0];
14417
+ const time = target.time + layer.at;
14418
+ const source = noiseSource(target, layer.source.kind === "noise" && layer.source.loop);
14419
+ const gain = context.createGain();
14420
+ gain.connect(target.output);
14421
+ for (const band of layer.filters?.bank ?? []) {
14422
+ const filter = context.createBiquadFilter();
14423
+ filter.type = band.type;
14424
+ if (band.q !== void 0) filter.Q.value = band.q;
14425
+ assign(filter.frequency, band.frequency, time);
14426
+ rampTo(filter.frequency, band.frequency, time);
14427
+ const level = context.createGain();
14428
+ level.gain.value = band.level ?? 1;
14429
+ source.connect(filter);
14430
+ filter.connect(level);
14431
+ level.connect(gain);
14432
+ }
14433
+ const sources = [source];
14434
+ const lfos = layer.modulators.map((modulator) => {
14435
+ const lfo = context.createOscillator();
14436
+ const depth = context.createGain();
14437
+ if (modulator.wave) lfo.type = modulator.wave;
14438
+ lfo.frequency.value = modulator.rate;
14439
+ depth.gain.value = modulator.depth;
14440
+ lfo.connect(depth);
14441
+ depth.connect(gain.gain);
14442
+ return lfo;
14443
+ });
14444
+ applyEnvelope(gain.gain, layer.envelope, time);
14445
+ source.start(time, offsetOf(layer));
14446
+ source.stop(end(layer, time));
14447
+ for (const lfo of lfos) {
14448
+ lfo.start(time);
14449
+ lfo.stop(end(layer, time));
14450
+ sources.push(lfo);
14451
+ }
14452
+ return voiceOf(sources);
14453
+ }
14454
+ function realizeGroan(plan, target) {
14455
+ const { context } = target;
14456
+ const layer = plan.layers[0];
14457
+ const time = target.time + layer.at;
14458
+ const oscillator = context.createOscillator();
14459
+ const gain = context.createGain();
14460
+ const filter = context.createBiquadFilter();
14461
+ const band = firstFilter(layer);
14462
+ if (layer.source.kind === "osc") oscillator.type = layer.source.wave;
14463
+ if (band) {
14464
+ filter.type = band.type;
14465
+ if (band.q !== void 0) filter.Q.value = band.q;
14466
+ }
14467
+ if (layer.source.kind === "osc") {
14468
+ assign(oscillator.frequency, layer.source.frequency, time);
14469
+ rampTo(oscillator.frequency, layer.source.frequency, time);
14470
+ }
14471
+ if (band) assign(filter.frequency, band.frequency, time);
14472
+ applyEnvelope(gain.gain, layer.envelope, time);
14473
+ oscillator.connect(filter);
14474
+ filter.connect(gain);
14475
+ gain.connect(target.output);
14476
+ oscillator.start(time);
14477
+ oscillator.stop(end(layer, time));
14478
+ return voiceOf([oscillator]);
14479
+ }
14480
+ function realizeBoom(plan, target) {
14481
+ const { context } = target;
14482
+ const layer = plan.layers[0];
14483
+ const time = target.time + layer.at;
14484
+ const oscillator = context.createOscillator();
14485
+ const gain = context.createGain();
14486
+ if (layer.source.kind === "osc") {
14487
+ oscillator.type = layer.source.wave;
14488
+ assign(oscillator.frequency, layer.source.frequency, time);
14489
+ rampTo(oscillator.frequency, layer.source.frequency, time);
14490
+ }
14491
+ applyEnvelope(gain.gain, layer.envelope, time);
14492
+ oscillator.connect(gain);
14493
+ gain.connect(target.output);
14494
+ oscillator.start(time);
14495
+ oscillator.stop(end(layer, time));
14496
+ return voiceOf([oscillator]);
14497
+ }
14498
+ function realizeStatic(plan, target) {
14499
+ const { context } = target;
14500
+ const layer = plan.layers[0];
14501
+ const time = target.time + layer.at;
14502
+ const gain = context.createGain();
14503
+ const source = context.createBufferSource();
14504
+ const filter = context.createBiquadFilter();
14505
+ source.buffer = target.noise();
14506
+ setFilter(filter, firstFilter(layer), time);
14507
+ applyEnvelope(gain.gain, layer.envelope, time);
14508
+ source.connect(filter);
14509
+ filter.connect(gain);
14510
+ gain.connect(target.output);
14511
+ source.start(time, offsetOf(layer));
14512
+ source.stop(end(layer, time));
14513
+ return voiceOf([source]);
14514
+ }
14515
+ function realizeChord(plan, target) {
14516
+ const { context } = target;
14517
+ const layers = plan.layers;
14518
+ const oscillators = layers.map(() => context.createOscillator());
14519
+ const gain = context.createGain();
14520
+ for (let index = layers.length - 1; index >= 0; index--) {
14521
+ const source = layers[index].source;
14522
+ if (source.kind === "osc") oscillators[index].type = source.wave;
14523
+ }
14524
+ layers.forEach((layer, index) => {
14525
+ if (layer.source.kind === "osc") assign(oscillators[index].frequency, layer.source.frequency, target.time + layer.at);
14526
+ });
14527
+ applyEnvelope(gain.gain, layers[0].envelope, target.time + layers[0].at);
14528
+ for (const oscillator of oscillators) oscillator.connect(gain);
14529
+ gain.connect(target.output);
14530
+ layers.forEach((layer, index) => oscillators[index].start(target.time + layer.at));
14531
+ layers.forEach((layer, index) => oscillators[index].stop(end(layer, target.time + layer.at)));
14532
+ return voiceOf(oscillators);
14533
+ }
14534
+ function realizeCalls(plan, target) {
14535
+ const { context } = target;
14536
+ const gain = context.createGain();
14537
+ const filter = context.createBiquadFilter();
14538
+ if (plan.silent) return voiceOf([]);
14539
+ const band = firstFilter(plan.layers[0]);
14540
+ if (band) filter.type = band.type;
14541
+ filter.connect(gain);
14542
+ gain.connect(target.output);
14543
+ const oscillators = plan.layers.map((layer) => {
14544
+ const time = target.time + layer.at;
14545
+ const oscillator = context.createOscillator();
14546
+ if (layer.source.kind === "osc") {
14547
+ oscillator.type = layer.source.wave;
14548
+ assign(oscillator.frequency, layer.source.frequency, time);
14549
+ rampTo(oscillator.frequency, layer.source.frequency, time);
14550
+ }
14551
+ if (band) {
14552
+ assign(filter.frequency, band.frequency, time);
14553
+ if (band.q !== void 0) filter.Q.value = band.q;
14554
+ }
14555
+ const envelope = context.createGain();
14556
+ applyEnvelope(envelope.gain, layer.envelope, time);
14557
+ oscillator.connect(envelope);
14558
+ envelope.connect(filter);
14559
+ oscillator.start(time);
14560
+ oscillator.stop(end(layer, time));
14561
+ return oscillator;
14562
+ });
14563
+ return voiceOf(oscillators);
14564
+ }
14565
+ var end2 = (layer, start) => start + layer.duration + layer.tail;
14566
+ function outputStage(plan, layer, target) {
14567
+ const { context } = target;
14568
+ const level = gainNode(context, layer.level);
14569
+ let node = level;
14570
+ if (plan.pan !== void 0 && typeof context.createStereoPanner === "function") {
14571
+ const panner = context.createStereoPanner();
14572
+ panner.pan.value = plan.pan;
14573
+ level.connect(panner);
14574
+ node = panner;
14575
+ }
14576
+ node.connect(target.output);
14577
+ if (layer.wet && target.wet) {
14578
+ const send = gainNode(context, layer.wet);
14579
+ node.connect(send);
14580
+ send.connect(target.wet);
14581
+ }
14582
+ return level;
14583
+ }
14584
+ function realizeNoiseLayer(plan, layer, target) {
14585
+ const { context } = target;
14586
+ const time = target.time + layer.at;
14587
+ const source = noiseSource(target, layer.source.kind === "noise" && layer.source.loop);
14588
+ const filters = (layer.filters?.bank ?? []).map((filter) => {
14589
+ const node = filterNode(context, filter, time);
14590
+ rampTo(node.frequency, filter.frequency, time);
14591
+ return node;
14592
+ });
14593
+ const envelope = context.createGain();
14594
+ applyEnvelope(envelope.gain, layer.envelope, time);
14595
+ let tail = source;
14596
+ for (const filter of filters) tail = tail.connect(filter);
14597
+ tail.connect(envelope);
14598
+ envelope.connect(outputStage(plan, layer, target));
14599
+ source.start(time, layer.source.kind === "noise" ? layer.source.offset : 0);
14600
+ source.stop(end2(layer, time));
14601
+ return [source];
14602
+ }
14603
+ function realizeToneLayer(plan, layer, target) {
14604
+ const { context } = target;
14605
+ const time = target.time + layer.at;
14606
+ const oscillator = context.createOscillator();
14607
+ if (layer.source.kind === "osc") {
14608
+ oscillator.type = layer.source.wave;
14609
+ assign(oscillator.frequency, layer.source.frequency, time);
14610
+ rampTo(oscillator.frequency, layer.source.frequency, time);
14611
+ }
14612
+ const envelope = context.createGain();
14613
+ applyEnvelope(envelope.gain, layer.envelope, time);
14614
+ oscillator.connect(envelope);
14615
+ envelope.connect(outputStage(plan, layer, target));
14616
+ oscillator.start(time);
14617
+ oscillator.stop(end2(layer, time));
14618
+ return [oscillator];
14619
+ }
14620
+ function realizeVoiceLayer(plan, layer, target) {
14621
+ const source = layer.source;
14622
+ if (source.kind !== "formant_voice") return [];
14623
+ const { context } = target;
14624
+ const time = target.time + layer.at;
14625
+ const oscillator = context.createOscillator();
14626
+ oscillator.type = source.wave;
14627
+ assign(oscillator.frequency, source.frequency, time);
14628
+ rampTo(oscillator.frequency, source.frequency, time);
14629
+ const vibrato = context.createOscillator();
14630
+ vibrato.frequency.value = source.vibrato.rate;
14631
+ const vibratoDepth = gainNode(context, source.vibrato.depth);
14632
+ vibrato.connect(vibratoDepth);
14633
+ vibratoDepth.connect(oscillator.frequency);
14634
+ const formants = source.formants.map((formant) => filterNode(context, { type: "bandpass", frequency: { value: formant.frequency }, q: formant.q }, time));
14635
+ const breath = noiseSource(target, true);
14636
+ const breathBand = filterNode(context, { type: "bandpass", frequency: { value: source.breath.frequency }, q: source.breath.q }, time);
14637
+ const breathLevel = gainNode(context, source.breath.gain);
14638
+ const mix2 = gainNode(context, source.mix);
14639
+ for (const formant of formants) oscillator.connect(formant);
14640
+ breath.connect(breathBand);
14641
+ breathBand.connect(breathLevel);
14642
+ for (const formant of formants) formant.connect(mix2);
14643
+ breathLevel.connect(mix2);
14644
+ const envelope = context.createGain();
14645
+ applyEnvelope(envelope.gain, layer.envelope, time);
14646
+ mix2.connect(envelope);
14647
+ envelope.connect(outputStage(plan, layer, target));
14648
+ const stop = end2(layer, time);
14649
+ oscillator.start(time);
14650
+ vibrato.start(time);
14651
+ breath.start(time, source.breath.offset);
14652
+ oscillator.stop(stop);
14653
+ vibrato.stop(stop);
14654
+ breath.stop(stop);
14655
+ return [oscillator, vibrato, breath];
14656
+ }
14657
+ function realizeLayered(plan, target) {
14658
+ const sources = plan.layers.flatMap((layer) => {
14659
+ if (layer.source.kind === "noise") return realizeNoiseLayer(plan, layer, target);
14660
+ if (layer.source.kind === "osc") return realizeToneLayer(plan, layer, target);
14661
+ return realizeVoiceLayer(plan, layer, target);
14662
+ });
14663
+ return voiceOf(sources);
14664
+ }
14665
+ function realizeSiren(plan, target) {
14666
+ const { context } = target;
14667
+ const layer = plan.layers[0];
14668
+ const time = target.time + layer.at;
14669
+ const oscillator = context.createOscillator();
14670
+ if (layer.source.kind === "osc") {
14671
+ oscillator.type = layer.source.wave;
14672
+ assign(oscillator.frequency, layer.source.frequency, time);
14673
+ rampTo(oscillator.frequency, layer.source.frequency, time);
14674
+ }
14675
+ const depths = layer.modulators.map((modulator) => {
14676
+ const lfo = context.createOscillator();
14677
+ if (modulator.wave) lfo.type = modulator.wave;
14678
+ lfo.frequency.value = modulator.rate;
14679
+ const depth = gainNode(context, modulator.depth);
14680
+ lfo.connect(depth);
14681
+ if (modulator.target === "frequency") depth.connect(oscillator.frequency);
14682
+ return { lfo, depth, target: modulator.target };
14683
+ });
14684
+ const lfos = depths.map(({ lfo }) => lfo);
14685
+ const envelope = context.createGain();
14686
+ applyEnvelope(envelope.gain, layer.envelope, time);
14687
+ for (const { depth, target: modulated } of depths) if (modulated === "gain") depth.connect(envelope.gain);
14688
+ const filters = (layer.filters?.bank ?? []).map((filter) => filterNode(context, filter, time));
14689
+ let tail = oscillator;
14690
+ for (const filter of filters) tail = tail.connect(filter);
14691
+ tail.connect(envelope);
14692
+ envelope.connect(target.output);
14693
+ if (layer.wet && target.wet) {
14694
+ const send = gainNode(context, layer.wet);
14695
+ envelope.connect(send);
14696
+ send.connect(target.wet);
14697
+ }
14698
+ const stop = end2(layer, time);
14699
+ oscillator.start(time);
14700
+ for (const lfo of lfos) lfo.start(time);
14701
+ oscillator.stop(stop);
14702
+ for (const lfo of lfos) lfo.stop(stop);
14703
+ return voiceOf([oscillator, ...lfos]);
14704
+ }
14705
+ function noiseLayer(layer, target, time) {
14706
+ const { context } = target;
14707
+ const source = noiseSource(target, layer.source.kind === "noise" && layer.source.loop);
14708
+ const filters = (layer.filters?.bank ?? []).map((filter) => filterNode(context, filter, time));
14709
+ const envelope = context.createGain();
14710
+ applyEnvelope(envelope.gain, layer.envelope, time);
14711
+ let tail = source;
14712
+ for (const filter of filters) tail = tail.connect(filter);
14713
+ tail.connect(envelope).connect(target.output);
14714
+ source.start(time, layer.source.kind === "noise" ? layer.source.offset : 0);
14715
+ source.stop(time + layer.duration + layer.tail);
14716
+ return source;
14717
+ }
14718
+ function toneLayer(layer, target, time) {
14719
+ const { context } = target;
14720
+ const oscillator = context.createOscillator();
14721
+ if (layer.source.kind === "osc") {
14722
+ oscillator.type = layer.source.wave;
14723
+ assign(oscillator.frequency, layer.source.frequency, time);
14724
+ rampTo(oscillator.frequency, layer.source.frequency, time);
14725
+ }
14726
+ const envelope = context.createGain();
14727
+ applyEnvelope(envelope.gain, layer.envelope, time);
14728
+ oscillator.connect(envelope).connect(target.output);
14729
+ oscillator.start(time);
14730
+ oscillator.stop(time + layer.duration + layer.tail);
14731
+ return oscillator;
14732
+ }
14733
+ function realizeWeapon(plan, target) {
14734
+ const sources = plan.layers.map((layer) => {
14735
+ const time = target.time + layer.at;
14736
+ return layer.source.kind === "noise" ? noiseLayer(layer, target, time) : toneLayer(layer, target, time);
14737
+ });
14738
+ return voiceOf(sources);
14739
+ }
14740
+ var BUILDERS = {
14741
+ layered: realizeLayered,
14742
+ burst: realizeBurst,
14743
+ scream: realizeScream,
14744
+ groan: realizeGroan,
14745
+ boom: realizeBoom,
14746
+ static: realizeStatic,
14747
+ chord: realizeChord,
14748
+ calls: realizeCalls,
14749
+ siren: realizeSiren,
14750
+ weapon: realizeWeapon
14751
+ };
14752
+ function realize(plan, target) {
14753
+ return BUILDERS[plan.order](plan, target);
14754
+ }
14755
+ var usesNoise = (recipe) => recipe.layers.some((layer) => layer.source.kind !== "osc");
14756
+ var planUsesNoise = (plan) => plan.layers.some((layer) => layer.source.kind !== "osc");
14757
+ function createAudioEngine(options = {}) {
14758
+ const seed = options.seed ?? 1;
14759
+ const registry = createSoundRegistry(options.banks);
14760
+ const voices = createVoiceManager({ ...options.groups ? { groups: options.groups } : {}, ...options.maxVoices === void 0 ? {} : { maxVoices: options.maxVoices } });
14761
+ const engineRandom = createRng2(seed);
14762
+ const shared = createNoiseStore(engineRandom);
14763
+ const busSettings = /* @__PURE__ */ new Map();
14764
+ const busNodes = /* @__PURE__ */ new Map();
14765
+ const meters = /* @__PURE__ */ new Map();
14766
+ const active = /* @__PURE__ */ new Map();
14767
+ const loops = /* @__PURE__ */ new Set();
14768
+ const named = /* @__PURE__ */ new Map();
14769
+ let mix2 = resolveMix(options.mix ?? DEFAULT_MIX, options.profiles);
14770
+ let context = null;
14771
+ let chain = null;
14772
+ let listener = DEFAULT_LISTENER;
14773
+ let lastTime = 0;
14774
+ const outputFor = (bus) => busNodes.get(bus) ?? chain?.master;
14775
+ const watched = /* @__PURE__ */ new Set();
14776
+ const meterOf = (bus) => {
14777
+ const existing = meters.get(bus);
14778
+ if (existing) return existing;
14779
+ const node = busNodes.get(bus);
14780
+ if (!context || !node || typeof context.createAnalyser !== "function") return void 0;
14781
+ const analyser = context.createAnalyser();
14782
+ analyser.fftSize = 1024;
14783
+ node.connect(analyser);
14784
+ const made = { analyser, data: new Float32Array(analyser.fftSize) };
14785
+ meters.set(bus, made);
14786
+ return made;
14787
+ };
14788
+ const ensureBus = (bus) => {
14789
+ if (!context || !chain || busNodes.has(bus)) return;
14790
+ const node = context.createGain();
14791
+ node.connect(chain.master);
14792
+ busNodes.set(bus, node);
14793
+ if (watched.has(bus)) meterOf(bus);
14794
+ };
14795
+ const ready = () => {
14796
+ if (context) return context;
14797
+ const created = (options.createContext ?? globalContext)();
14798
+ if (!created) return null;
14799
+ context = created;
14800
+ chain = buildChain(created, mix2, engineRandom);
14801
+ if (mix2.noise.create === "eager") noiseBuffer(shared, created, mix2.noise.seconds);
14802
+ for (const [bus, setting] of Object.entries(mix2.buses ?? {})) {
14803
+ busSettings.set(bus, { ...setting, ...busSettings.get(bus) });
14804
+ }
14805
+ for (const [bus, setting] of busSettings) {
14806
+ ensureBus(bus);
14807
+ busNodes.get(bus).gain.value = busGain(setting);
14808
+ }
14809
+ lastTime = created.currentTime;
14810
+ return created;
14811
+ };
14812
+ const advance = () => {
14813
+ if (!context) return;
14814
+ const elapsed = context.currentTime - lastTime;
14815
+ lastTime = context.currentTime;
14816
+ if (elapsed > 0) for (const id of voices.advance(elapsed)) active.delete(id);
14817
+ };
14818
+ const inputFor = (recipe, playOptions, random) => ({
14819
+ random,
14820
+ listener,
14821
+ noiseSeconds: mix2.noise.seconds,
14822
+ ...options.distance ? { distanceMode: options.distance } : {},
14823
+ ...playOptions.volume === void 0 ? {} : { volume: playOptions.volume },
14824
+ ...playOptions.distance === void 0 ? {} : { distance: playOptions.distance },
14825
+ ...playOptions.position ? { position: playOptions.position } : {},
14826
+ ...playOptions.params ? { params: playOptions.params } : {},
14827
+ ...options.occlusion && playOptions.position && recipe.falloff?.model !== "none" ? { occlusion: options.occlusion(playOptions.position) } : {}
14828
+ });
14829
+ const start = (plan, store, voiceId, delay = 0) => {
14830
+ const audio = context;
14831
+ const realized = realize(plan, {
14832
+ context: audio,
14833
+ output: outputFor(plan.bus),
14834
+ ...chain?.wet ? { wet: chain.wet } : {},
14835
+ noise: () => noiseBuffer(store, audio, mix2.noise.seconds),
14836
+ time: audio.currentTime + delay
14837
+ });
14838
+ if (voiceId === void 0) return null;
14839
+ voices.setLength(voiceId, plan.length + delay);
14840
+ active.set(voiceId, realized);
14841
+ return {
14842
+ id: voiceId,
14843
+ plan,
14844
+ stop() {
14845
+ realized.stop();
14846
+ voices.release(voiceId);
14847
+ active.delete(voiceId);
14848
+ }
14849
+ };
14850
+ };
14851
+ const admit = (recipe, gain, distance2) => {
14852
+ const decision = voices.request({ ...recipe.group ? { group: recipe.group } : {}, priority: recipe.priority ?? busPriority(recipe.bus), gain, distance: distance2, length: 0 });
14853
+ if (!decision.accepted) return null;
14854
+ for (const id of decision.evicted) {
14855
+ active.get(id)?.stop();
14856
+ active.delete(id);
14857
+ }
14858
+ return decision.id;
14859
+ };
14860
+ const playWith = (id, playOptions, random, store) => {
14861
+ const recipe = registry.sound(id);
14862
+ if (!recipe || busSettings.get(recipe.bus)?.muted) return null;
14863
+ const distance2 = playOptions.distance ?? (playOptions.position ? listenerDistance(listener, playOptions.position, options.distance) : 0);
14864
+ const level = falloffGain(recipe.falloff, playOptions.volume ?? 1, distance2);
14865
+ const silent = isSilent(recipe.falloff, level);
14866
+ if (silent && !recipe.keepSilent) return null;
14867
+ const audio = ready();
14868
+ if (!audio) return null;
14869
+ unlockContext(audio);
14870
+ advance();
14871
+ const voiceId = silent ? void 0 : admit(recipe, level, distance2);
14872
+ if (voiceId === null) return null;
14873
+ if (!silent && usesNoise(recipe)) prepareNoise(store, audio, mix2.noise.seconds);
14874
+ const plan = planSound(recipe, inputFor(recipe, playOptions, playOptions.seed === void 0 ? random : createRng2(playOptions.seed)));
14875
+ return plan ? start(plan, store, voiceId, playOptions.delay) : null;
14876
+ };
14877
+ const randomFor = (playOptions) => {
14878
+ if (playOptions.channel === void 0) return engineRandom;
14879
+ const existing = named.get(playOptions.channel);
14880
+ if (existing) return existing;
14881
+ const stream = streamFor(seed, playOptions.channel);
14882
+ named.set(playOptions.channel, stream);
14883
+ return stream;
14884
+ };
14885
+ const planWith = (id, playOptions, random) => {
14886
+ const recipe = registry.sound(id);
14887
+ return recipe ? planSound(recipe, inputFor(recipe, playOptions, random)) : null;
14888
+ };
14889
+ const loopHost = {
14890
+ ready,
14891
+ output: outputFor,
14892
+ prepareNoise: () => context && prepareNoise(shared, context, mix2.noise.seconds),
14893
+ noise: () => noiseBuffer(shared, context, mix2.noise.seconds),
14894
+ register: (loop) => void loops.add(loop),
14895
+ unregister: (loop) => void loops.delete(loop)
14896
+ };
14897
+ return {
14898
+ registry,
14899
+ register: (bank) => registry.register(bank),
14900
+ plan: (id, playOptions = {}) => planWith(id, playOptions, createRng2(playOptions.seed ?? seed)),
14901
+ play: (id, playOptions = {}) => playWith(id, playOptions, randomFor(playOptions), shared),
14902
+ playPlan(plan, playOptions = {}) {
14903
+ const audio = ready();
14904
+ if (!audio) return null;
14905
+ advance();
14906
+ const recipe = { id: plan.id, bus: plan.bus, layers: [], sources: [], priority: plan.priority, ...plan.group ? { group: plan.group } : {} };
14907
+ const voiceId = plan.silent ? void 0 : admit(recipe, plan.level, plan.distance);
14908
+ if (voiceId === null) return null;
14909
+ if (planUsesNoise(plan)) prepareNoise(shared, audio, mix2.noise.seconds);
14910
+ return start(plan, shared, voiceId, playOptions.delay);
14911
+ },
14912
+ loop: (id, loopOptions = {}) => createLoopHandle(registry.loop(id), loopHost, loopOptions.volume),
14913
+ channel(channelOptions) {
14914
+ const random = channelOptions.random ?? (channelOptions.seed === void 0 ? engineRandom : createRng2(channelOptions.seed));
14915
+ const store = channelOptions.ownNoise ? createNoiseStore(random) : shared;
14916
+ return {
14917
+ random,
14918
+ play: (id, playOptions = {}) => playWith(id, playOptions, random, store),
14919
+ plan: (id, playOptions = {}) => planWith(id, playOptions, createRng2(playOptions.seed ?? seed))
14920
+ };
14921
+ },
14922
+ setListener(next) {
14923
+ listener = next;
14924
+ },
14925
+ setBus(id, setting) {
14926
+ busSettings.set(id, { ...busSettings.get(id), ...setting });
14927
+ if (!context) return;
14928
+ ensureBus(id);
14929
+ busNodes.get(id).gain.value = busGain(busSettings.get(id));
14930
+ },
14931
+ setMix(next) {
14932
+ const previous = mix2;
14933
+ mix2 = resolveMix(next, options.profiles);
14934
+ if (!context || !chain) return;
14935
+ const old = chain;
14936
+ chain = buildChain(context, mix2, engineRandom);
14937
+ if (mix2.noise.seconds !== previous.noise.seconds) shared.reset();
14938
+ for (const node of busNodes.values()) {
14939
+ node.disconnect();
14940
+ node.connect(chain.master);
14941
+ }
14942
+ for (const loop of loops) loop.rewire();
14943
+ old.disconnect();
14944
+ },
14945
+ setMaster(gain) {
14946
+ mix2 = { ...mix2, master: gain };
14947
+ if (chain) chain.master.gain.value = gain;
14948
+ },
14949
+ unlock() {
14950
+ const audio = ready();
14951
+ if (audio) unlockContext(audio);
14952
+ },
14953
+ level(bus) {
14954
+ watched.add(bus);
14955
+ ensureBus(bus);
14956
+ const meter = meterOf(bus);
14957
+ if (!meter) return 0;
14958
+ meter.analyser.getFloatTimeDomainData(meter.data);
14959
+ let sum2 = 0;
14960
+ for (const sample of meter.data) sum2 += sample * sample;
14961
+ return Math.sqrt(sum2 / meter.data.length);
14962
+ },
14963
+ stats() {
14964
+ advance();
14965
+ const voiceStats = voices.stats();
14966
+ return {
14967
+ context: context ? context.state : "none",
14968
+ voices: voiceStats.active,
14969
+ peak: voiceStats.peak,
14970
+ dropped: voiceStats.dropped,
14971
+ maxVoices: voiceStats.maxVoices,
14972
+ loops: [...loops].filter((loop) => loop.created).length
14973
+ };
14974
+ },
14975
+ dispose() {
14976
+ for (const id of active.keys()) voices.release(id);
14977
+ for (const loop of [...loops]) loop.stop();
14978
+ active.clear();
14979
+ void context?.close();
14980
+ context = null;
14981
+ chain = null;
14982
+ busNodes.clear();
14983
+ meters.clear();
14984
+ shared.reset();
14985
+ }
14986
+ };
14987
+ }
14988
+ var BENCH_BANK = {
14989
+ id: "audio_bench",
14990
+ sounds: [
14991
+ {
14992
+ id: "bench_ping",
14993
+ bus: "ui",
14994
+ pan: true,
14995
+ duration: 0.25,
14996
+ layers: [
14997
+ {
14998
+ gain: 0.5,
14999
+ wet: 0.3,
15000
+ source: { kind: "osc", wave: "sine", frequency: { from: 880 } },
15001
+ envelope: [
15002
+ [0, 1e-4, "set", "fixed"],
15003
+ [2e-3, 1, "exp"],
15004
+ ["1d", 1e-4, "exp", "fixed"]
15005
+ ]
15006
+ }
15007
+ ],
15008
+ falloff: DISTANCE_CURVES.art_scene,
15009
+ sources: ["sdk: bench probe, a 880 Hz ping through the art scene output stage"]
15010
+ },
15011
+ {
15012
+ id: "bench_hiss",
15013
+ bus: "ui",
15014
+ pan: true,
15015
+ duration: [0.3, 0.9],
15016
+ layers: [
15017
+ {
15018
+ gain: 0.4,
15019
+ source: { kind: "noise", offset: "random", offsetScale: 1.5, loop: true },
15020
+ filters: { mode: "serial", bank: [{ type: "highpass", frequency: { set: 3e3 }, q: 0.7 }] },
15021
+ envelope: [
15022
+ [0, 1e-4, "set", "fixed"],
15023
+ [2e-3, 1, "exp"],
15024
+ ["1d", 1e-4, "exp", "fixed"]
15025
+ ]
15026
+ }
15027
+ ],
15028
+ falloff: DISTANCE_CURVES.art_scene,
15029
+ sources: ["sdk: bench probe, a highpass hiss of random length"]
15030
+ }
15031
+ ]
15032
+ };
15033
+
13624
15034
  // src/audio/mechanical-sounds.ts
13625
15035
  var metal = (at, frequency, gain) => ({ layer: "metal", at, frequency, gain });
13626
15036
  var sound = (at, soundClass, volume) => ({ layer: "class", at, soundClass, volume });
13627
15037
  var thud = (at, gain, frequency) => ({ layer: "thud", at, gain, frequency });
15038
+ var sceneClick = (at, f, vol) => [
15039
+ { layer: "scene_tone", at, wave: "square", f, f2: f * 0.7, d: 0.02, vol },
15040
+ { layer: "scene_noise", at, type: "highpass", f: 3e3, q: 0.7, d: 0.03, vol: vol * 0.8 }
15041
+ ];
15042
+ var sceneNoise = (at, type, f, q, d, vol) => ({ layer: "scene_noise", at, type, f, q, d, vol });
15043
+ var sceneEmpty = sceneClick(0, 3200, 0.35);
15044
+ var sceneMagOut = [...sceneClick(0, 1600, 0.4), sceneNoise(0.03, "bandpass", 900, 2, 0.08, 0.3)];
15045
+ var sceneMagIn = [...sceneClick(0, 1100, 0.5), { layer: "scene_tone", at: 0, wave: "sine", f: 320, f2: 200, d: 0.05, vol: 0.35 }];
15046
+ var sceneBolt = [...sceneClick(0, 2200, 0.4), ...sceneClick(0.12, 2700, 0.4)];
15047
+ var sceneShell = [...sceneClick(0, 1400, 0.35), sceneNoise(0.02, "bandpass", 1200, 3, 0.05, 0.25)];
13628
15048
  var slide = [metal(0, 900, 0.35), metal(0.14, 1300, 0.35)];
13629
15049
  var seat = [metal(0, 1100, 0.45), sound(0, "reload", 0.8)];
13630
15050
  var MECHANICAL_SOUNDS = {
13631
- dry_fire: [sound(0, "empty", 1), metal(0, 2600, 0.25)],
15051
+ dry_fire: [sound(0, "empty", 1), ...sceneEmpty],
13632
15052
  misfire: [sound(0, "empty", 0.8), metal(0, 2200, 0.2)],
13633
15053
  feed: [metal(0, 1900, 0.2)],
13634
15054
  cycle: [metal(0.22, 900, 0.35), metal(0.36, 1300, 0.35)],
@@ -13641,11 +15061,11 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
13641
15061
  stock_fold: [metal(0, 800, 0.3), metal(0.18, 1200, 0.3), thud(0.3, 0.2, 600)],
13642
15062
  light_click: [metal(0, 2400, 0.2), metal(0.03, 1800, 0.12)],
13643
15063
  attach: [metal(0, 1400, 0.35), thud(0.04, 0.2, 700), metal(0.12, 1900, 0.25)],
13644
- magazine_out: [metal(0, 1500, 0.35), sound(0, "reload", 0.6)],
13645
- magazine_in: seat,
13646
- charge: slide,
15064
+ magazine_out: [...sceneMagOut, sound(0, "reload", 0.6)],
15065
+ magazine_in: [...sceneMagIn, sound(0, "reload", 0.8)],
15066
+ charge: sceneBolt,
13647
15067
  bolt_open: [metal(0, 900, 0.35)],
13648
- round_in: [metal(0, 2e3, 0.2), sound(0, "reload", 0.6)],
15068
+ round_in: [...sceneShell, sound(0, "reload", 0.6)],
13649
15069
  bolt_close: [metal(0, 1300, 0.35)],
13650
15070
  pump_cycle: slide,
13651
15071
  lever_cycle: [metal(0, 1e3, 0.35), metal(0.12, 1500, 0.35)],
@@ -13785,100 +15205,185 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
13785
15205
  };
13786
15206
  };
13787
15207
 
15208
+ // src/audio/recipes.ts
15209
+ var CLASS_BURST_ID = "weapon_class_burst";
15210
+ var BURST_IDS = { lowpass: "weapon_burst_lowpass", highpass: "weapon_burst_highpass" };
15211
+ var SCENE_NOISE_IDS = { lowpass: "scene_noise_lowpass", highpass: "scene_noise_highpass", bandpass: "scene_noise_bandpass" };
15212
+ var SCENE_TONE_IDS = { sine: "scene_tone_sine", square: "scene_tone_square" };
15213
+ var TONE_IDS = { sine: "weapon_tone_sine", sine_sweep: "weapon_tone_sine_sweep", square: "weapon_tone_square" };
15214
+ var SOURCE_CLASS_BURST = "jogo:L62097-62110 sfx: lowpass noise, gain set at once, exponential fall to 0.001, stopped 0.05 s later";
15215
+ var SOURCE_SDK = "sdk: the weapon synth of weapons-three (burst and tone), moved onto game-audio with the same nodes";
15216
+ var FIT = { kind: "noise", offset: "fit" };
15217
+ function classBurstId(curve) {
15218
+ const same = curve.gainFalloff === GAME_DISTANCE_CURVE.gainFalloff && curve.cutoffFalloff === GAME_DISTANCE_CURVE.cutoffFalloff && curve.minGain === GAME_DISTANCE_CURVE.minGain;
15219
+ return same ? CLASS_BURST_ID : `${CLASS_BURST_ID}@${curve.gainFalloff},${curve.cutoffFalloff},${curve.minGain}`;
15220
+ }
15221
+ function classBurstRecipe(curve) {
15222
+ return {
15223
+ id: classBurstId(curve),
15224
+ bus: "weapons",
15225
+ order: "weapon",
15226
+ duration: { param: "seconds" },
15227
+ layers: [
15228
+ {
15229
+ gain: { param: "gain" },
15230
+ source: FIT,
15231
+ filters: { mode: "serial", bank: [{ type: "lowpass", frequency: { set: { param: "cutoff" } } }] },
15232
+ envelope: [
15233
+ [0, 1, "set"],
15234
+ ["1d", 1e-3, "exp", "fixed"]
15235
+ ]
15236
+ }
15237
+ ],
15238
+ falloff: { model: "inverse_square", k: curve.gainFalloff, cutoffK: curve.cutoffFalloff, minGain: curve.minGain },
15239
+ sources: [SOURCE_CLASS_BURST]
15240
+ };
15241
+ }
15242
+ var burstLayer = (type) => ({
15243
+ gain: { param: "gain" },
15244
+ source: FIT,
15245
+ filters: { mode: "serial", bank: [{ type, frequency: { set: { param: "frequency" } }, q: { param: "q" } }] },
15246
+ envelope: [
15247
+ [0, 1e-4, "set", "fixed"],
15248
+ [4e-3, 1, "exp"],
15249
+ ["1d", 1e-4, "exp", "fixed"]
15250
+ ]
15251
+ });
15252
+ var burstRecipe = (type) => ({
15253
+ id: BURST_IDS[type],
15254
+ bus: "weapons",
15255
+ order: "weapon",
15256
+ duration: { param: "seconds" },
15257
+ layers: [burstLayer(type)],
15258
+ falloff: { model: "none" },
15259
+ sources: [SOURCE_SDK]
15260
+ });
15261
+ var toneLayer2 = (wave, sweep) => ({
15262
+ gain: { param: "gain" },
15263
+ source: { kind: "osc", wave, frequency: sweep ? { from: { param: "frequency" }, to: { param: "to" }, curve: "exp", over: "duration" } : { from: { param: "frequency" } } },
15264
+ envelope: [
15265
+ [0, 1e-4, "set", "fixed"],
15266
+ [3e-3, 1, "exp"],
15267
+ ["1d", 1e-4, "exp", "fixed"]
15268
+ ]
15269
+ });
15270
+ var toneRecipe = (id, wave, sweep) => ({
15271
+ id,
15272
+ bus: "weapons",
15273
+ order: "weapon",
15274
+ duration: { param: "seconds" },
15275
+ layers: [toneLayer2(wave, sweep)],
15276
+ falloff: { model: "none" },
15277
+ sources: [SOURCE_SDK]
15278
+ });
15279
+ var SOURCE_SCENE = "cena:L1323-L1324 sNoise and sTone, played at the listener (distance 0), through the art scene output stage";
15280
+ var SCENE_ATTACK = 2e-3;
15281
+ var sceneNoiseRecipe = (type, slide2) => ({
15282
+ id: slide2 ? `${SCENE_NOISE_IDS[type]}_slide` : SCENE_NOISE_IDS[type],
15283
+ bus: "weapons",
15284
+ order: "layered",
15285
+ duration: { param: "d", add: SCENE_ATTACK },
15286
+ pan: true,
15287
+ layers: [
15288
+ {
15289
+ gain: { param: "vol" },
15290
+ source: { kind: "noise", offset: "random", offsetScale: 1.5, loop: true },
15291
+ filters: { mode: "serial", bank: [{ type, frequency: slide2 ? { set: { param: "f" }, to: { param: "f2" }, curve: "exp", over: { param: "d" } } : { set: { param: "f" } }, q: { param: "q" } }] },
15292
+ envelope: [
15293
+ [0, 1e-4, "set", "fixed"],
15294
+ [SCENE_ATTACK, 1, "exp"],
15295
+ ["1d", 1e-4, "exp", "fixed"]
15296
+ ]
15297
+ }
15298
+ ],
15299
+ falloff: DISTANCE_CURVES.art_scene,
15300
+ sources: [SOURCE_SCENE]
15301
+ });
15302
+ var sceneToneRecipe = (wave) => ({
15303
+ id: SCENE_TONE_IDS[wave],
15304
+ bus: "weapons",
15305
+ order: "layered",
15306
+ duration: { param: "d", add: SCENE_ATTACK },
15307
+ pan: true,
15308
+ layers: [
15309
+ {
15310
+ gain: { param: "vol" },
15311
+ source: { kind: "osc", wave, frequency: { from: { param: "f" }, to: { param: "f2" }, curve: "exp", over: "duration" } },
15312
+ envelope: [
15313
+ [0, 1e-4, "set", "fixed"],
15314
+ [SCENE_ATTACK, 1, "exp"],
15315
+ ["1d", 1e-4, "exp", "fixed"]
15316
+ ]
15317
+ }
15318
+ ],
15319
+ falloff: DISTANCE_CURVES.art_scene,
15320
+ sources: [SOURCE_SCENE]
15321
+ });
15322
+ function weaponRecipes(curve) {
15323
+ return [
15324
+ classBurstRecipe(curve),
15325
+ burstRecipe("lowpass"),
15326
+ burstRecipe("highpass"),
15327
+ toneRecipe(TONE_IDS.sine, "sine", false),
15328
+ toneRecipe(TONE_IDS.sine_sweep, "sine", true),
15329
+ toneRecipe(TONE_IDS.square, "square", false),
15330
+ ...["lowpass", "highpass", "bandpass"].flatMap((type) => [sceneNoiseRecipe(type, false), sceneNoiseRecipe(type, true)]),
15331
+ sceneToneRecipe("sine"),
15332
+ sceneToneRecipe("square")
15333
+ ];
15334
+ }
15335
+
13788
15336
  // src/audio.ts
13789
15337
  var NOISE_SECONDS = 1.5;
13790
- var globalContext = () => {
13791
- const scope2 = globalThis;
13792
- const Ctor = scope2.AudioContext ?? scope2.webkitAudioContext;
13793
- return Ctor ? new Ctor() : null;
13794
- };
15338
+ var DEFAULT_SEED = 1;
13795
15339
  function createWeaponAudio(volume = 0.6, options = {}) {
13796
- const makeContext = options.createContext ?? globalContext;
13797
- const rng = createRng(options.seed ?? 1);
15340
+ const seed = options.seed ?? DEFAULT_SEED;
13798
15341
  const classes = { ...GAME_SOUND_CLASSES, ...options.classes };
13799
15342
  const distanceCurve = { ...GAME_DISTANCE_CURVE, ...options.distance };
13800
15343
  const mechanicalTable = { ...MECHANICAL_SOUNDS, ...options.mechanical };
13801
- let ctx = null;
13802
- let master = null;
13803
- let noise = null;
13804
- let level = volume;
13805
- const ready = () => {
13806
- if (!ctx) {
13807
- const created = makeContext();
13808
- if (!created) return null;
13809
- ctx = created;
13810
- master = ctx.createGain();
13811
- master.gain.value = level;
13812
- master.connect(ctx.destination);
13813
- noise = ctx.createBuffer(1, Math.floor(ctx.sampleRate * NOISE_SECONDS), ctx.sampleRate);
13814
- const data = noise.getChannelData(0);
13815
- for (let i = 0; i < data.length; i++) data[i] = rng() * 2 - 1;
13816
- }
13817
- if (ctx.state === "suspended") void ctx.resume();
13818
- return ctx;
13819
- };
15344
+ const shared = options.engine;
15345
+ const engine = shared ?? createAudioEngine({
15346
+ seed,
15347
+ mix: { ...MIX_PROFILES.game, master: volume, noise: { seconds: NOISE_SECONDS, create: "eager" } },
15348
+ maxVoices: Number.POSITIVE_INFINITY,
15349
+ ...options.createContext ? { createContext: options.createContext } : {}
15350
+ });
15351
+ const channel = shared && options.seed !== void 0 ? shared.channel({ seed }) : engine.channel({});
15352
+ engine.register({ id: `weapons_three@${classBurstId(distanceCurve)}`, sounds: weaponRecipes(distanceCurve) });
15353
+ const classBurst = classBurstId(distanceCurve);
13820
15354
  const burst = (config) => {
13821
- const audio = ready();
13822
- if (!audio || !noise || !master) return;
13823
- const start = audio.currentTime + (config.at ?? 0);
13824
- const source = audio.createBufferSource();
13825
- source.buffer = noise;
13826
- const filter = audio.createBiquadFilter();
13827
- filter.type = config.filter;
13828
- filter.frequency.value = config.frequency;
13829
- filter.Q.value = config.q ?? 0.8;
13830
- const gain = audio.createGain();
13831
- gain.gain.setValueAtTime(1e-4, start);
13832
- gain.gain.exponentialRampToValueAtTime(Math.max(config.gain, 2e-4), start + 4e-3);
13833
- gain.gain.exponentialRampToValueAtTime(1e-4, start + config.seconds);
13834
- source.connect(filter).connect(gain).connect(master);
13835
- source.start(start, rng() * Math.max(0, NOISE_SECONDS - config.seconds));
13836
- source.stop(start + config.seconds + 0.05);
15355
+ const id = config.filter === "highpass" ? BURST_IDS.highpass : BURST_IDS.lowpass;
15356
+ const params = { frequency: config.frequency, q: config.q ?? 0.8, gain: Math.max(config.gain, 2e-4), seconds: config.seconds };
15357
+ channel.play(id, { params, ...config.at ? { delay: config.at } : {} });
13837
15358
  };
13838
- const classBurst = (at, cutoff, gain, seconds) => {
13839
- const audio = ready();
13840
- if (!audio || !noise || !master) return;
13841
- const start = audio.currentTime + at;
13842
- const source = audio.createBufferSource();
13843
- source.buffer = noise;
13844
- const filter = audio.createBiquadFilter();
13845
- filter.type = "lowpass";
13846
- filter.frequency.value = cutoff;
13847
- const envelope = audio.createGain();
13848
- envelope.gain.setValueAtTime(gain, start);
13849
- envelope.gain.exponentialRampToValueAtTime(1e-3, start + seconds);
13850
- source.connect(filter).connect(envelope).connect(master);
13851
- source.start(start, rng() * Math.max(0, NOISE_SECONDS - seconds));
13852
- source.stop(start + seconds + 0.05);
15359
+ const classBurstAt = (at, cutoff, gain, seconds) => {
15360
+ channel.play(classBurst, { params: { cutoff, gain, seconds }, ...at ? { delay: at } : {} });
13853
15361
  };
13854
15362
  const tone = (config) => {
13855
- const audio = ready();
13856
- if (!audio || !master) return;
13857
- const start = audio.currentTime + (config.at ?? 0);
13858
- const osc = audio.createOscillator();
13859
- osc.type = config.type ?? "sine";
13860
- osc.frequency.setValueAtTime(config.frequency, start);
13861
- if (config.to) osc.frequency.exponentialRampToValueAtTime(config.to, start + config.seconds);
13862
- const gain = audio.createGain();
13863
- gain.gain.setValueAtTime(1e-4, start);
13864
- gain.gain.exponentialRampToValueAtTime(Math.max(config.gain, 2e-4), start + 3e-3);
13865
- gain.gain.exponentialRampToValueAtTime(1e-4, start + config.seconds);
13866
- osc.connect(gain).connect(master);
13867
- osc.start(start);
13868
- osc.stop(start + config.seconds + 0.05);
15363
+ const id = config.to ? TONE_IDS.sine_sweep : config.type === "square" ? TONE_IDS.square : TONE_IDS.sine;
15364
+ const params = { frequency: config.frequency, gain: Math.max(config.gain, 2e-4), seconds: config.seconds, ...config.to ? { to: config.to } : {} };
15365
+ channel.play(id, { params, ...config.at ? { delay: config.at } : {} });
13869
15366
  };
13870
15367
  const metal2 = (at, frequency, gain) => {
13871
15368
  burst({ at, seconds: 0.05, gain, filter: "highpass", frequency, q: 2 });
13872
15369
  tone({ at, seconds: 0.04, gain: gain * 0.6, frequency: frequency * 0.6, type: "square" });
13873
15370
  };
13874
15371
  const play = (soundClass, classVolume = 1, distance2 = 0, at = 0) => {
13875
- const attenuation = classVolume / (1 + distance2 * distance2 * distanceCurve.gainFalloff);
13876
- if (attenuation < distanceCurve.minGain) return;
13877
15372
  const [cutoff, gain, seconds] = classes[soundClass] ?? FALLBACK_SOUND_CLASS;
13878
- classBurst(at, cutoff / (1 + distance2 * distanceCurve.cutoffFalloff), gain * attenuation, seconds);
15373
+ channel.play(classBurst, { volume: classVolume, distance: distance2, params: { cutoff, gain, seconds }, ...at ? { delay: at } : {} });
15374
+ };
15375
+ const sceneTone = (entry) => {
15376
+ channel.play(SCENE_TONE_IDS[entry.wave], { params: { f: entry.f, f2: entry.f2 ?? entry.f, d: entry.d, vol: entry.vol }, ...entry.at ? { delay: entry.at } : {} });
15377
+ };
15378
+ const sceneNoise2 = (entry) => {
15379
+ const slide2 = entry.f2 !== void 0;
15380
+ const id = slide2 ? `${SCENE_NOISE_IDS[entry.type]}_slide` : SCENE_NOISE_IDS[entry.type];
15381
+ channel.play(id, { params: { f: entry.f, ...slide2 ? { f2: entry.f2 } : {}, q: entry.q, d: entry.d, vol: entry.vol }, ...entry.at ? { delay: entry.at } : {} });
13879
15382
  };
13880
15383
  const step = (entry) => {
13881
15384
  if (entry.layer === "metal") metal2(entry.at, entry.frequency, entry.gain);
15385
+ else if (entry.layer === "scene_tone") sceneTone(entry);
15386
+ else if (entry.layer === "scene_noise") sceneNoise2(entry);
13882
15387
  else if (entry.layer === "class") play(entry.soundClass, entry.volume, 0, entry.at);
13883
15388
  else burst({ at: entry.at, seconds: 0.09, gain: entry.gain, filter: "lowpass", frequency: entry.frequency });
13884
15389
  };
@@ -13890,7 +15395,7 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
13890
15395
  shot(energyJ, category, extras = {}) {
13891
15396
  const sound2 = shotSound({ ...extras, energyJ, category });
13892
15397
  if (!sound2.audible) return;
13893
- classBurst(0, sound2.cutoff, sound2.gain, sound2.duration);
15398
+ classBurstAt(0, sound2.cutoff, sound2.gain, sound2.duration);
13894
15399
  if (sound2.crack.gain > 0) burst({ seconds: sound2.crack.duration, gain: sound2.crack.gain, filter: "highpass", frequency: sound2.crack.cutoff });
13895
15400
  tone({ seconds: sound2.body.duration, gain: sound2.body.gain, frequency: sound2.body.frequency, to: sound2.body.to });
13896
15401
  },
@@ -13918,22 +15423,20 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
13918
15423
  mechanical("cycle");
13919
15424
  },
13920
15425
  clink() {
13921
- const f = 3200 + rng() * 2e3;
13922
- tone({ seconds: 0.09, gain: 0.12, frequency: f });
13923
- tone({ seconds: 0.06, gain: 0.06, frequency: f * 2.7 });
15426
+ const f = 4200 + channel.random() * 1500;
15427
+ const at = 0.25 + channel.random() * 0.2;
15428
+ sceneTone({ layer: "scene_tone", at, wave: "sine", f, f2: 3800, d: 0.06, vol: 0.06 });
13924
15429
  },
13925
15430
  thud() {
13926
15431
  burst({ seconds: 0.09, gain: 0.35, filter: "lowpass", frequency: 380 });
13927
15432
  },
13928
15433
  setVolume(next) {
13929
- level = Math.max(0, Math.min(1, next));
13930
- if (master) master.gain.value = level;
15434
+ const level = Math.max(0, Math.min(1, next));
15435
+ if (shared) shared.setBus("weapons", { gain: level });
15436
+ else engine.setMaster(level);
13931
15437
  },
13932
15438
  dispose() {
13933
- void ctx?.close();
13934
- ctx = null;
13935
- master = null;
13936
- noise = null;
15439
+ if (!shared) engine.dispose();
13937
15440
  }
13938
15441
  };
13939
15442
  }
@@ -16687,13 +18190,13 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
16687
18190
  }
16688
18191
 
16689
18192
  // src/models/capture.ts
16690
- function captured(parent, draw) {
18193
+ function captured(parent, draw2) {
16691
18194
  const before = parent.children.length;
16692
- draw();
18195
+ draw2();
16693
18196
  return parent.children.slice(before);
16694
18197
  }
16695
- function recordFitting(parts, id, parent, draw) {
16696
- parts.fittings = { ...parts.fittings, [id]: captured(parent, draw) };
18198
+ function recordFitting(parts, id, parent, draw2) {
18199
+ parts.fittings = { ...parts.fittings, [id]: captured(parent, draw2) };
16697
18200
  }
16698
18201
 
16699
18202
  // src/models/attachments/bipod.ts
@@ -17116,7 +18619,7 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
17116
18619
  }
17117
18620
  return chosen;
17118
18621
  })();
17119
- var BUILDERS = {
18622
+ var BUILDERS2 = {
17120
18623
  pistol: buildPistol,
17121
18624
  revolver: buildRevolver,
17122
18625
  rifle: buildRifle,
@@ -17146,7 +18649,7 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
17146
18649
  const kit = modelKit(THREE);
17147
18650
  const options = { L: entry.length, B: entry.barrel, cal: entry.caliber, ...entry.options };
17148
18651
  if (id in ARCHETYPE_MODELS) delete options.chambers;
17149
- const built = BUILDERS[entry.builder](kit, options);
18652
+ const built = BUILDERS2[entry.builder](kit, options);
17150
18653
  nameParts(built.parts);
17151
18654
  let root;
17152
18655
  if (entry.builder === "launcher") {
@@ -17540,12 +19043,12 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
17540
19043
  var toggleClip = () => composeClip("toggle", [turn(ROOT, "z", 0, TOGGLE_SECONDS / 2, 0, TOGGLE_TILT), turn(ROOT, "z", TOGGLE_SECONDS / 2, TOGGLE_SECONDS / 2, TOGGLE_TILT, 0)]);
17541
19044
  function launcherClips(context) {
17542
19045
  if (!context.parts.ugl) return [];
17543
- const { open, loaded, close, end, slide: slide2, back } = LAUNCHER_RELOAD;
19046
+ const { open, loaded, close, end: end3, slide: slide2, back } = LAUNCHER_RELOAD;
17544
19047
  const reload = composeClip(
17545
19048
  "reload_ugl",
17546
19049
  [move("ugl", "x", open, slide2, 0, LAUNCHER_SLIDE), move("ugl", "x", close, back, LAUNCHER_SLIDE, 0)],
17547
19050
  [stage("breech_open", open), stage("round_in", loaded), stage("breech_close", close)],
17548
- end
19051
+ end3
17549
19052
  );
17550
19053
  const shot = composeClip("shot_ugl", [move(ROOT, "x", 0, 0.04, 0, -LAUNCHER_KICK), move(ROOT, "x", 0.04, 0.2, -LAUNCHER_KICK, 0)], [cue(0, "muzzle_flash", { at: "ugl" })]);
17551
19054
  return [reload, shot];
@@ -18202,8 +19705,8 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
18202
19705
  if (!look || !slide2) return null;
18203
19706
  const { M, cylinderX } = kit;
18204
19707
  const length2 = attachment.dims?.length ?? 0.35;
18205
- const fixed = kit.group();
18206
- drawFixed(kit, fixed, look, length2);
19708
+ const fixed2 = kit.group();
19709
+ drawFixed(kit, fixed2, look, length2);
18207
19710
  const tubeLength = length2 * look.tubeFraction;
18208
19711
  const start = -0.02;
18209
19712
  cylinderX(slide2, look.tubeRadius, tubeLength, M.metal, start + tubeLength / 2, -look.tubeRadius, 0, 18);
@@ -18212,7 +19715,7 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
18212
19715
  const muzzle = kit.mark(slide2, start + tubeLength + 0.01, -look.tubeRadius, 0);
18213
19716
  muzzle.name = "ugl_muzzle";
18214
19717
  return {
18215
- object: fixed,
19718
+ object: fixed2,
18216
19719
  muzzle,
18217
19720
  dispose() {
18218
19721
  slide2.remove(...slide2.children);
@@ -18839,11 +20342,11 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
18839
20342
  if (!session.cartridge) return;
18840
20343
  const { group } = buildRound(kit, session.cartridge.dims, session.cartridge.shape);
18841
20344
  const start = root.worldToLocal(from.clone());
18842
- const end = root.worldToLocal(to.clone());
20345
+ const end3 = root.worldToLocal(to.clone());
18843
20346
  group.rotation.z = tilt;
18844
20347
  group.position.copy(start);
18845
20348
  root.add(group);
18846
- world.schedule(0, 0.24, (eased) => group.position.lerpVectors(start, end, eased), () => {
20349
+ world.schedule(0, 0.24, (eased) => group.position.lerpVectors(start, end3, eased), () => {
18847
20350
  group.removeFromParent();
18848
20351
  group.traverse((child) => {
18849
20352
  const mesh = child;
@@ -20227,7 +21730,8 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
20227
21730
  const arsenal = options.arsenal ?? createArsenal({ locale });
20228
21731
  const text = textOf(locale);
20229
21732
  const accessoryText = accessoryTextOf(locale);
20230
- const audio = options.sound === false ? null : createWeaponAudio(void 0, options.seed === void 0 ? {} : { seed: options.seed });
21733
+ const seedOption = options.seed === void 0 ? {} : { seed: options.seed };
21734
+ let audio = options.sound === false ? null : createWeaponAudio(void 0, seedOption);
20231
21735
  const weaponDefs = [...arsenal.weapons.values()].filter((def) => !BODY_ACTIONS.has(def.id));
20232
21736
  const thumbnailBase = options.thumbnailBase ?? DEFAULT_THUMBNAIL_BASE;
20233
21737
  const withThumbnail = (item, key) => {
@@ -20254,7 +21758,9 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
20254
21758
  id: "weapons_area",
20255
21759
  name: text.name,
20256
21760
  group: text.group,
20257
- audio,
21761
+ get audio() {
21762
+ return audio;
21763
+ },
20258
21764
  get fx() {
20259
21765
  return fx;
20260
21766
  },
@@ -20302,6 +21808,11 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
20302
21808
  ],
20303
21809
  attach(lent) {
20304
21810
  host = lent;
21811
+ const lentEngine = lent.audio?.engine;
21812
+ if (audio && lentEngine) {
21813
+ audio.dispose();
21814
+ audio = createWeaponAudio(void 0, { ...seedOption, engine: lentEngine });
21815
+ }
20305
21816
  const created = createWeaponFx({
20306
21817
  THREE: lent.THREE,
20307
21818
  scene: lent.scene,