@adatechnology/game-weapons-three 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.d.ts +32 -3
- package/dist/index.global.js +1631 -120
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +200 -96
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
package/dist/index.global.js
CHANGED
|
@@ -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,
|
|
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 =
|
|
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,
|
|
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,
|
|
12963
|
-
if (t0 >
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
13149
|
+
fireMelee(a, origin, dir, start, end3) {
|
|
13150
13150
|
const t0 = Math.max(start, this.readyAt);
|
|
13151
|
-
if (t0 >
|
|
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,
|
|
13186
|
-
if (this.cook) return this.cookStep(a, origin, dir,
|
|
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,
|
|
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),
|
|
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: [
|
|
13645
|
-
magazine_in:
|
|
13646
|
-
charge:
|
|
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: [
|
|
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
|
|
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
|
|
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
|
-
|
|
13802
|
-
|
|
13803
|
-
|
|
13804
|
-
|
|
13805
|
-
|
|
13806
|
-
|
|
13807
|
-
|
|
13808
|
-
|
|
13809
|
-
|
|
13810
|
-
|
|
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
|
|
13822
|
-
|
|
13823
|
-
|
|
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
|
|
13839
|
-
|
|
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
|
|
13856
|
-
|
|
13857
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
13922
|
-
|
|
13923
|
-
|
|
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 (
|
|
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
|
-
|
|
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,
|
|
18193
|
+
function captured(parent, draw2) {
|
|
16691
18194
|
const before = parent.children.length;
|
|
16692
|
-
|
|
18195
|
+
draw2();
|
|
16693
18196
|
return parent.children.slice(before);
|
|
16694
18197
|
}
|
|
16695
|
-
function recordFitting(parts, id, parent,
|
|
16696
|
-
parts.fittings = { ...parts.fittings, [id]: captured(parent,
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
18206
|
-
drawFixed(kit,
|
|
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:
|
|
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
|
|
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,
|
|
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
|
|
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,
|