@adatechnology/game-weapons-three 0.4.1 → 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 +55 -5
- package/dist/index.global.js +1681 -121
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +250 -97
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
package/dist/index.global.js
CHANGED
|
@@ -35,6 +35,7 @@ var GameWeaponsThree = (() => {
|
|
|
35
35
|
MOUNTED_M134_ITEM_ID: () => MOUNTED_M134_ITEM_ID,
|
|
36
36
|
WEAPON_ITEM_PREFIX: () => WEAPON_ITEM_PREFIX,
|
|
37
37
|
WEAPON_THUMBNAILS: () => WEAPON_THUMBNAILS,
|
|
38
|
+
WORLD_EFFECTS: () => WORLD_EFFECTS,
|
|
38
39
|
WORLD_EXPLOSIVE_IDS: () => WORLD_EXPLOSIVE_IDS,
|
|
39
40
|
WORLD_EXPLOSIVE_PREFIX: () => WORLD_EXPLOSIVE_PREFIX,
|
|
40
41
|
applyCondition: () => applyCondition,
|
|
@@ -50,6 +51,7 @@ var GameWeaponsThree = (() => {
|
|
|
50
51
|
createWeaponAudio: () => createWeaponAudio,
|
|
51
52
|
createWeaponFx: () => createWeaponFx,
|
|
52
53
|
createWeaponsTestModule: () => createWeaponsTestModule,
|
|
54
|
+
createWorldEffects: () => createWorldEffects,
|
|
53
55
|
deriveFrames: () => deriveFrames,
|
|
54
56
|
fitAttachmentModels: () => fitAttachmentModels,
|
|
55
57
|
hasWeaponModel: () => hasWeaponModel,
|
|
@@ -12935,10 +12937,10 @@ var GameWeaponsThree = (() => {
|
|
|
12935
12937
|
__publicField(this, "lastRoundAt", Number.NEGATIVE_INFINITY);
|
|
12936
12938
|
}
|
|
12937
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. */
|
|
12938
|
-
spunUpAt(spin, start,
|
|
12940
|
+
spunUpAt(spin, start, end3) {
|
|
12939
12941
|
if (!spin || start - this.lastRoundAt <= spin.holdSeconds + EPS3) return 0;
|
|
12940
12942
|
if (this.spinSince === void 0 || start - this.spinFrameEnd > SPIN_PULL_GAP) this.spinSince = start;
|
|
12941
|
-
this.spinFrameEnd =
|
|
12943
|
+
this.spinFrameEnd = end3;
|
|
12942
12944
|
return this.spinSince + spin.upSeconds;
|
|
12943
12945
|
}
|
|
12944
12946
|
oncePerPull(held) {
|
|
@@ -12947,7 +12949,7 @@ var GameWeaponsThree = (() => {
|
|
|
12947
12949
|
this.latched = true;
|
|
12948
12950
|
return true;
|
|
12949
12951
|
}
|
|
12950
|
-
fireFirearm(a, origin, dir, start,
|
|
12952
|
+
fireFirearm(a, origin, dir, start, end3, held, source) {
|
|
12951
12953
|
if (this._reloading) {
|
|
12952
12954
|
if (this.loadStep !== null && this._ammo > 0) this.stopReload(start);
|
|
12953
12955
|
else return result("reloading");
|
|
@@ -12957,10 +12959,10 @@ var GameWeaponsThree = (() => {
|
|
|
12957
12959
|
this.emit({ type: "empty", at: start });
|
|
12958
12960
|
return result("empty");
|
|
12959
12961
|
}
|
|
12960
|
-
const t0 = Math.max(start, this.readyAt, this.spunUpAt(a.spin, start,
|
|
12961
|
-
if (t0 >
|
|
12962
|
+
const t0 = Math.max(start, this.readyAt, this.spunUpAt(a.spin, start, end3));
|
|
12963
|
+
if (t0 > end3 + EPS3) return result("cooldown");
|
|
12962
12964
|
const res = result("ok");
|
|
12963
|
-
const times = planRounds(a, this.mode, this._ammo, t0,
|
|
12965
|
+
const times = planRounds(a, this.mode, this._ammo, t0, end3);
|
|
12964
12966
|
let lastAt = t0;
|
|
12965
12967
|
for (const t of times) {
|
|
12966
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 });
|
|
@@ -13076,7 +13078,7 @@ var GameWeaponsThree = (() => {
|
|
|
13076
13078
|
this.pend = { src: "trigger", amt: d };
|
|
13077
13079
|
}
|
|
13078
13080
|
const start = this.clock - overlap;
|
|
13079
|
-
const
|
|
13081
|
+
const end3 = start + d;
|
|
13080
13082
|
this.settle(start);
|
|
13081
13083
|
const dir = normalize(direction);
|
|
13082
13084
|
let res;
|
|
@@ -13087,10 +13089,10 @@ var GameWeaponsThree = (() => {
|
|
|
13087
13089
|
else if (launcher) res = launcher.trigger(origin, dir, d, held, source);
|
|
13088
13090
|
else switch (a.kind) {
|
|
13089
13091
|
case "firearm":
|
|
13090
|
-
res = this.fireFirearm(a, origin, dir, start,
|
|
13092
|
+
res = this.fireFirearm(a, origin, dir, start, end3, held, source);
|
|
13091
13093
|
break;
|
|
13092
13094
|
case "melee":
|
|
13093
|
-
res = this.fireMelee(a, origin, dir, start,
|
|
13095
|
+
res = this.fireMelee(a, origin, dir, start, end3);
|
|
13094
13096
|
break;
|
|
13095
13097
|
case "flame":
|
|
13096
13098
|
case "heat":
|
|
@@ -13100,7 +13102,7 @@ var GameWeaponsThree = (() => {
|
|
|
13100
13102
|
res = this.fireExtinguisher(a, origin, dir, d);
|
|
13101
13103
|
break;
|
|
13102
13104
|
case "throwable":
|
|
13103
|
-
res = this.fireThrowable(a, origin, dir, held, start,
|
|
13105
|
+
res = this.fireThrowable(a, origin, dir, held, start, end3);
|
|
13104
13106
|
break;
|
|
13105
13107
|
case "launcher":
|
|
13106
13108
|
res = this.fireLauncher(a, origin, dir, start, held);
|
|
@@ -13112,7 +13114,7 @@ var GameWeaponsThree = (() => {
|
|
|
13112
13114
|
res = this.oncePerPull(held) ? result("ok", { uses: [{ kind: "hand", reach: a.reach, origin, direction: dir }] }) : result("cooldown");
|
|
13113
13115
|
break;
|
|
13114
13116
|
}
|
|
13115
|
-
this.advance(Math.max(0,
|
|
13117
|
+
this.advance(Math.max(0, end3 - this.clock));
|
|
13116
13118
|
this.settle(this.clock);
|
|
13117
13119
|
return res;
|
|
13118
13120
|
}
|
|
@@ -13144,9 +13146,9 @@ var GameWeaponsThree = (() => {
|
|
|
13144
13146
|
this.emit({ type: "shot", shots: 0, rounds: 1, mode: this.mode, at: t });
|
|
13145
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] });
|
|
13146
13148
|
}
|
|
13147
|
-
fireMelee(a, origin, dir, start,
|
|
13149
|
+
fireMelee(a, origin, dir, start, end3) {
|
|
13148
13150
|
const t0 = Math.max(start, this.readyAt);
|
|
13149
|
-
if (t0 >
|
|
13151
|
+
if (t0 > end3 + EPS3) return result("cooldown");
|
|
13150
13152
|
this.readyAt = t0 + 60 / a.rpm;
|
|
13151
13153
|
const use = { kind: "melee", damage: a.damage, energy: a.energy, radius: a.radius, reach: a.reach, origin, direction: dir };
|
|
13152
13154
|
return result("ok", { uses: [use], noise: a.noise });
|
|
@@ -13180,8 +13182,8 @@ var GameWeaponsThree = (() => {
|
|
|
13180
13182
|
return result("ok", { uses: [throwableUse({ action: a, explosive: this.explosive, origin, direction: dir, fuse, rules: this.rules.explosives, ...extra })], noise: a.noise });
|
|
13181
13183
|
}
|
|
13182
13184
|
/** A timer throwable is cooked from the pull; impact, remote and wire ones (and a long fuse that sets down) act at once. */
|
|
13183
|
-
fireThrowable(a, origin, dir, held, start,
|
|
13184
|
-
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);
|
|
13185
13187
|
if (!this.oncePerPull(held)) return result("cooldown");
|
|
13186
13188
|
if (this._ammo <= 0) return result("empty");
|
|
13187
13189
|
const fuse = this.sampleThrowFuse(a);
|
|
@@ -13191,7 +13193,7 @@ var GameWeaponsThree = (() => {
|
|
|
13191
13193
|
}
|
|
13192
13194
|
this.cook = { start, fuse, origin, direction: dir };
|
|
13193
13195
|
this.emit({ type: "cook", phase: "start", fuse, at: start });
|
|
13194
|
-
return this.cookStep(a, origin, dir,
|
|
13196
|
+
return this.cookStep(a, origin, dir, end3);
|
|
13195
13197
|
}
|
|
13196
13198
|
/** The trigger stays down on a cooked throwable: nothing happens until the fuse runs out in the hand. */
|
|
13197
13199
|
cookStep(a, origin, dir, now) {
|
|
@@ -13619,14 +13621,1434 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
|
|
|
13619
13621
|
};
|
|
13620
13622
|
var accessoryTextOf = (locale) => ACCESSORY_TEXT[locale] ?? ACCESSORY_TEXT.en;
|
|
13621
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
|
+
|
|
13622
15034
|
// src/audio/mechanical-sounds.ts
|
|
13623
15035
|
var metal = (at, frequency, gain) => ({ layer: "metal", at, frequency, gain });
|
|
13624
15036
|
var sound = (at, soundClass, volume) => ({ layer: "class", at, soundClass, volume });
|
|
13625
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)];
|
|
13626
15048
|
var slide = [metal(0, 900, 0.35), metal(0.14, 1300, 0.35)];
|
|
13627
15049
|
var seat = [metal(0, 1100, 0.45), sound(0, "reload", 0.8)];
|
|
13628
15050
|
var MECHANICAL_SOUNDS = {
|
|
13629
|
-
dry_fire: [sound(0, "empty", 1),
|
|
15051
|
+
dry_fire: [sound(0, "empty", 1), ...sceneEmpty],
|
|
13630
15052
|
misfire: [sound(0, "empty", 0.8), metal(0, 2200, 0.2)],
|
|
13631
15053
|
feed: [metal(0, 1900, 0.2)],
|
|
13632
15054
|
cycle: [metal(0.22, 900, 0.35), metal(0.36, 1300, 0.35)],
|
|
@@ -13639,11 +15061,11 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
|
|
|
13639
15061
|
stock_fold: [metal(0, 800, 0.3), metal(0.18, 1200, 0.3), thud(0.3, 0.2, 600)],
|
|
13640
15062
|
light_click: [metal(0, 2400, 0.2), metal(0.03, 1800, 0.12)],
|
|
13641
15063
|
attach: [metal(0, 1400, 0.35), thud(0.04, 0.2, 700), metal(0.12, 1900, 0.25)],
|
|
13642
|
-
magazine_out: [
|
|
13643
|
-
magazine_in:
|
|
13644
|
-
charge:
|
|
15064
|
+
magazine_out: [...sceneMagOut, sound(0, "reload", 0.6)],
|
|
15065
|
+
magazine_in: [...sceneMagIn, sound(0, "reload", 0.8)],
|
|
15066
|
+
charge: sceneBolt,
|
|
13645
15067
|
bolt_open: [metal(0, 900, 0.35)],
|
|
13646
|
-
round_in: [
|
|
15068
|
+
round_in: [...sceneShell, sound(0, "reload", 0.6)],
|
|
13647
15069
|
bolt_close: [metal(0, 1300, 0.35)],
|
|
13648
15070
|
pump_cycle: slide,
|
|
13649
15071
|
lever_cycle: [metal(0, 1e3, 0.35), metal(0.12, 1500, 0.35)],
|
|
@@ -13783,100 +15205,185 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
|
|
|
13783
15205
|
};
|
|
13784
15206
|
};
|
|
13785
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
|
+
|
|
13786
15336
|
// src/audio.ts
|
|
13787
15337
|
var NOISE_SECONDS = 1.5;
|
|
13788
|
-
var
|
|
13789
|
-
const scope2 = globalThis;
|
|
13790
|
-
const Ctor = scope2.AudioContext ?? scope2.webkitAudioContext;
|
|
13791
|
-
return Ctor ? new Ctor() : null;
|
|
13792
|
-
};
|
|
15338
|
+
var DEFAULT_SEED = 1;
|
|
13793
15339
|
function createWeaponAudio(volume = 0.6, options = {}) {
|
|
13794
|
-
const
|
|
13795
|
-
const rng = createRng(options.seed ?? 1);
|
|
15340
|
+
const seed = options.seed ?? DEFAULT_SEED;
|
|
13796
15341
|
const classes = { ...GAME_SOUND_CLASSES, ...options.classes };
|
|
13797
15342
|
const distanceCurve = { ...GAME_DISTANCE_CURVE, ...options.distance };
|
|
13798
15343
|
const mechanicalTable = { ...MECHANICAL_SOUNDS, ...options.mechanical };
|
|
13799
|
-
|
|
13800
|
-
|
|
13801
|
-
|
|
13802
|
-
|
|
13803
|
-
|
|
13804
|
-
|
|
13805
|
-
|
|
13806
|
-
|
|
13807
|
-
|
|
13808
|
-
|
|
13809
|
-
master.gain.value = level;
|
|
13810
|
-
master.connect(ctx.destination);
|
|
13811
|
-
noise = ctx.createBuffer(1, Math.floor(ctx.sampleRate * NOISE_SECONDS), ctx.sampleRate);
|
|
13812
|
-
const data = noise.getChannelData(0);
|
|
13813
|
-
for (let i = 0; i < data.length; i++) data[i] = rng() * 2 - 1;
|
|
13814
|
-
}
|
|
13815
|
-
if (ctx.state === "suspended") void ctx.resume();
|
|
13816
|
-
return ctx;
|
|
13817
|
-
};
|
|
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);
|
|
13818
15354
|
const burst = (config) => {
|
|
13819
|
-
const
|
|
13820
|
-
|
|
13821
|
-
|
|
13822
|
-
const source = audio.createBufferSource();
|
|
13823
|
-
source.buffer = noise;
|
|
13824
|
-
const filter = audio.createBiquadFilter();
|
|
13825
|
-
filter.type = config.filter;
|
|
13826
|
-
filter.frequency.value = config.frequency;
|
|
13827
|
-
filter.Q.value = config.q ?? 0.8;
|
|
13828
|
-
const gain = audio.createGain();
|
|
13829
|
-
gain.gain.setValueAtTime(1e-4, start);
|
|
13830
|
-
gain.gain.exponentialRampToValueAtTime(Math.max(config.gain, 2e-4), start + 4e-3);
|
|
13831
|
-
gain.gain.exponentialRampToValueAtTime(1e-4, start + config.seconds);
|
|
13832
|
-
source.connect(filter).connect(gain).connect(master);
|
|
13833
|
-
source.start(start, rng() * Math.max(0, NOISE_SECONDS - config.seconds));
|
|
13834
|
-
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 } : {} });
|
|
13835
15358
|
};
|
|
13836
|
-
const
|
|
13837
|
-
|
|
13838
|
-
if (!audio || !noise || !master) return;
|
|
13839
|
-
const start = audio.currentTime + at;
|
|
13840
|
-
const source = audio.createBufferSource();
|
|
13841
|
-
source.buffer = noise;
|
|
13842
|
-
const filter = audio.createBiquadFilter();
|
|
13843
|
-
filter.type = "lowpass";
|
|
13844
|
-
filter.frequency.value = cutoff;
|
|
13845
|
-
const envelope = audio.createGain();
|
|
13846
|
-
envelope.gain.setValueAtTime(gain, start);
|
|
13847
|
-
envelope.gain.exponentialRampToValueAtTime(1e-3, start + seconds);
|
|
13848
|
-
source.connect(filter).connect(envelope).connect(master);
|
|
13849
|
-
source.start(start, rng() * Math.max(0, NOISE_SECONDS - seconds));
|
|
13850
|
-
source.stop(start + seconds + 0.05);
|
|
15359
|
+
const classBurstAt = (at, cutoff, gain, seconds) => {
|
|
15360
|
+
channel.play(classBurst, { params: { cutoff, gain, seconds }, ...at ? { delay: at } : {} });
|
|
13851
15361
|
};
|
|
13852
15362
|
const tone = (config) => {
|
|
13853
|
-
const
|
|
13854
|
-
|
|
13855
|
-
|
|
13856
|
-
const osc = audio.createOscillator();
|
|
13857
|
-
osc.type = config.type ?? "sine";
|
|
13858
|
-
osc.frequency.setValueAtTime(config.frequency, start);
|
|
13859
|
-
if (config.to) osc.frequency.exponentialRampToValueAtTime(config.to, start + config.seconds);
|
|
13860
|
-
const gain = audio.createGain();
|
|
13861
|
-
gain.gain.setValueAtTime(1e-4, start);
|
|
13862
|
-
gain.gain.exponentialRampToValueAtTime(Math.max(config.gain, 2e-4), start + 3e-3);
|
|
13863
|
-
gain.gain.exponentialRampToValueAtTime(1e-4, start + config.seconds);
|
|
13864
|
-
osc.connect(gain).connect(master);
|
|
13865
|
-
osc.start(start);
|
|
13866
|
-
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 } : {} });
|
|
13867
15366
|
};
|
|
13868
15367
|
const metal2 = (at, frequency, gain) => {
|
|
13869
15368
|
burst({ at, seconds: 0.05, gain, filter: "highpass", frequency, q: 2 });
|
|
13870
15369
|
tone({ at, seconds: 0.04, gain: gain * 0.6, frequency: frequency * 0.6, type: "square" });
|
|
13871
15370
|
};
|
|
13872
15371
|
const play = (soundClass, classVolume = 1, distance2 = 0, at = 0) => {
|
|
13873
|
-
const attenuation = classVolume / (1 + distance2 * distance2 * distanceCurve.gainFalloff);
|
|
13874
|
-
if (attenuation < distanceCurve.minGain) return;
|
|
13875
15372
|
const [cutoff, gain, seconds] = classes[soundClass] ?? FALLBACK_SOUND_CLASS;
|
|
13876
|
-
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 } : {} });
|
|
13877
15382
|
};
|
|
13878
15383
|
const step = (entry) => {
|
|
13879
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);
|
|
13880
15387
|
else if (entry.layer === "class") play(entry.soundClass, entry.volume, 0, entry.at);
|
|
13881
15388
|
else burst({ at: entry.at, seconds: 0.09, gain: entry.gain, filter: "lowpass", frequency: entry.frequency });
|
|
13882
15389
|
};
|
|
@@ -13888,7 +15395,7 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
|
|
|
13888
15395
|
shot(energyJ, category, extras = {}) {
|
|
13889
15396
|
const sound2 = shotSound({ ...extras, energyJ, category });
|
|
13890
15397
|
if (!sound2.audible) return;
|
|
13891
|
-
|
|
15398
|
+
classBurstAt(0, sound2.cutoff, sound2.gain, sound2.duration);
|
|
13892
15399
|
if (sound2.crack.gain > 0) burst({ seconds: sound2.crack.duration, gain: sound2.crack.gain, filter: "highpass", frequency: sound2.crack.cutoff });
|
|
13893
15400
|
tone({ seconds: sound2.body.duration, gain: sound2.body.gain, frequency: sound2.body.frequency, to: sound2.body.to });
|
|
13894
15401
|
},
|
|
@@ -13916,22 +15423,20 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
|
|
|
13916
15423
|
mechanical("cycle");
|
|
13917
15424
|
},
|
|
13918
15425
|
clink() {
|
|
13919
|
-
const f =
|
|
13920
|
-
|
|
13921
|
-
|
|
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 });
|
|
13922
15429
|
},
|
|
13923
15430
|
thud() {
|
|
13924
15431
|
burst({ seconds: 0.09, gain: 0.35, filter: "lowpass", frequency: 380 });
|
|
13925
15432
|
},
|
|
13926
15433
|
setVolume(next) {
|
|
13927
|
-
level = Math.max(0, Math.min(1, next));
|
|
13928
|
-
if (
|
|
15434
|
+
const level = Math.max(0, Math.min(1, next));
|
|
15435
|
+
if (shared) shared.setBus("weapons", { gain: level });
|
|
15436
|
+
else engine.setMaster(level);
|
|
13929
15437
|
},
|
|
13930
15438
|
dispose() {
|
|
13931
|
-
|
|
13932
|
-
ctx = null;
|
|
13933
|
-
master = null;
|
|
13934
|
-
noise = null;
|
|
15439
|
+
if (!shared) engine.dispose();
|
|
13935
15440
|
}
|
|
13936
15441
|
};
|
|
13937
15442
|
}
|
|
@@ -16685,13 +18190,13 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
|
|
|
16685
18190
|
}
|
|
16686
18191
|
|
|
16687
18192
|
// src/models/capture.ts
|
|
16688
|
-
function captured(parent,
|
|
18193
|
+
function captured(parent, draw2) {
|
|
16689
18194
|
const before = parent.children.length;
|
|
16690
|
-
|
|
18195
|
+
draw2();
|
|
16691
18196
|
return parent.children.slice(before);
|
|
16692
18197
|
}
|
|
16693
|
-
function recordFitting(parts, id, parent,
|
|
16694
|
-
parts.fittings = { ...parts.fittings, [id]: captured(parent,
|
|
18198
|
+
function recordFitting(parts, id, parent, draw2) {
|
|
18199
|
+
parts.fittings = { ...parts.fittings, [id]: captured(parent, draw2) };
|
|
16695
18200
|
}
|
|
16696
18201
|
|
|
16697
18202
|
// src/models/attachments/bipod.ts
|
|
@@ -17114,7 +18619,7 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
|
|
|
17114
18619
|
}
|
|
17115
18620
|
return chosen;
|
|
17116
18621
|
})();
|
|
17117
|
-
var
|
|
18622
|
+
var BUILDERS2 = {
|
|
17118
18623
|
pistol: buildPistol,
|
|
17119
18624
|
revolver: buildRevolver,
|
|
17120
18625
|
rifle: buildRifle,
|
|
@@ -17144,7 +18649,7 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
|
|
|
17144
18649
|
const kit = modelKit(THREE);
|
|
17145
18650
|
const options = { L: entry.length, B: entry.barrel, cal: entry.caliber, ...entry.options };
|
|
17146
18651
|
if (id in ARCHETYPE_MODELS) delete options.chambers;
|
|
17147
|
-
const built =
|
|
18652
|
+
const built = BUILDERS2[entry.builder](kit, options);
|
|
17148
18653
|
nameParts(built.parts);
|
|
17149
18654
|
let root;
|
|
17150
18655
|
if (entry.builder === "launcher") {
|
|
@@ -17538,12 +19043,12 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
|
|
|
17538
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)]);
|
|
17539
19044
|
function launcherClips(context) {
|
|
17540
19045
|
if (!context.parts.ugl) return [];
|
|
17541
|
-
const { open, loaded, close, end, slide: slide2, back } = LAUNCHER_RELOAD;
|
|
19046
|
+
const { open, loaded, close, end: end3, slide: slide2, back } = LAUNCHER_RELOAD;
|
|
17542
19047
|
const reload = composeClip(
|
|
17543
19048
|
"reload_ugl",
|
|
17544
19049
|
[move("ugl", "x", open, slide2, 0, LAUNCHER_SLIDE), move("ugl", "x", close, back, LAUNCHER_SLIDE, 0)],
|
|
17545
19050
|
[stage("breech_open", open), stage("round_in", loaded), stage("breech_close", close)],
|
|
17546
|
-
|
|
19051
|
+
end3
|
|
17547
19052
|
);
|
|
17548
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" })]);
|
|
17549
19054
|
return [reload, shot];
|
|
@@ -18200,8 +19705,8 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
|
|
|
18200
19705
|
if (!look || !slide2) return null;
|
|
18201
19706
|
const { M, cylinderX } = kit;
|
|
18202
19707
|
const length2 = attachment.dims?.length ?? 0.35;
|
|
18203
|
-
const
|
|
18204
|
-
drawFixed(kit,
|
|
19708
|
+
const fixed2 = kit.group();
|
|
19709
|
+
drawFixed(kit, fixed2, look, length2);
|
|
18205
19710
|
const tubeLength = length2 * look.tubeFraction;
|
|
18206
19711
|
const start = -0.02;
|
|
18207
19712
|
cylinderX(slide2, look.tubeRadius, tubeLength, M.metal, start + tubeLength / 2, -look.tubeRadius, 0, 18);
|
|
@@ -18210,7 +19715,7 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
|
|
|
18210
19715
|
const muzzle = kit.mark(slide2, start + tubeLength + 0.01, -look.tubeRadius, 0);
|
|
18211
19716
|
muzzle.name = "ugl_muzzle";
|
|
18212
19717
|
return {
|
|
18213
|
-
object:
|
|
19718
|
+
object: fixed2,
|
|
18214
19719
|
muzzle,
|
|
18215
19720
|
dispose() {
|
|
18216
19721
|
slide2.remove(...slide2.children);
|
|
@@ -18837,11 +20342,11 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
|
|
|
18837
20342
|
if (!session.cartridge) return;
|
|
18838
20343
|
const { group } = buildRound(kit, session.cartridge.dims, session.cartridge.shape);
|
|
18839
20344
|
const start = root.worldToLocal(from.clone());
|
|
18840
|
-
const
|
|
20345
|
+
const end3 = root.worldToLocal(to.clone());
|
|
18841
20346
|
group.rotation.z = tilt;
|
|
18842
20347
|
group.position.copy(start);
|
|
18843
20348
|
root.add(group);
|
|
18844
|
-
world.schedule(0, 0.24, (eased) => group.position.lerpVectors(start,
|
|
20349
|
+
world.schedule(0, 0.24, (eased) => group.position.lerpVectors(start, end3, eased), () => {
|
|
18845
20350
|
group.removeFromParent();
|
|
18846
20351
|
group.traverse((child) => {
|
|
18847
20352
|
const mesh = child;
|
|
@@ -19984,7 +21489,7 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
|
|
|
19984
21489
|
shape: shape.shape,
|
|
19985
21490
|
position: shape.position,
|
|
19986
21491
|
...SHELL,
|
|
19987
|
-
explodes: { energy: def.energy, radius: def.injuryRadius, ...def.chain ? { chain: { ...def.chain } } : {} }
|
|
21492
|
+
explodes: { energy: def.energy, radius: def.injuryRadius, effect: def.id, ...def.chain ? { chain: { ...def.chain } } : {} }
|
|
19988
21493
|
});
|
|
19989
21494
|
var meshOf = (THREE, shape) => {
|
|
19990
21495
|
const geometry = shape.shape.kind === "cylinder" ? new THREE.CylinderGeometry(shape.shape.radius, shape.shape.radius, shape.shape.height, 16) : shape.shape.kind === "box" ? new THREE.BoxGeometry(...shape.shape.size) : new THREE.SphereGeometry(0.2);
|
|
@@ -20012,6 +21517,51 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
|
|
|
20012
21517
|
};
|
|
20013
21518
|
};
|
|
20014
21519
|
|
|
21520
|
+
// src/data/world-effects.json
|
|
21521
|
+
var world_effects_default = {
|
|
21522
|
+
pistol: { muzzle: "muzzle_glock", casing: "casing_brass" },
|
|
21523
|
+
tiny: { muzzle: "muzzle_glock", casing: "casing_brass" },
|
|
21524
|
+
rifle: { muzzle: "muzzle_ak", casing: "casing_brass" },
|
|
21525
|
+
shell: { muzzle: "muzzle_r870", casing: "casing_shell" },
|
|
21526
|
+
big: { muzzle: "muzzle_barrett", casing: "casing_50" }
|
|
21527
|
+
};
|
|
21528
|
+
|
|
21529
|
+
// src/world-effects.ts
|
|
21530
|
+
var WORLD_EFFECTS = world_effects_default;
|
|
21531
|
+
var PORT_BACK = 0.35;
|
|
21532
|
+
var PORT_RIGHT = 0.04;
|
|
21533
|
+
function createWorldEffects(options) {
|
|
21534
|
+
const ids = options.ids ?? WORLD_EFFECTS;
|
|
21535
|
+
const caseTypeOf = options.caseTypeOf ?? ((weapon) => {
|
|
21536
|
+
const def = options.arsenal.weapons.get(weapon);
|
|
21537
|
+
return def && def.action.kind === "firearm" ? options.arsenal.calibers.get(def.action.caliber)?.caseType : void 0;
|
|
21538
|
+
});
|
|
21539
|
+
let counter = 0;
|
|
21540
|
+
return {
|
|
21541
|
+
/** Plays what the event asks for and answers how many effects started. */
|
|
21542
|
+
handle(event, sink) {
|
|
21543
|
+
if (!sink || event.type !== "shot" || !event.origin) return 0;
|
|
21544
|
+
const caseType = caseTypeOf(event.weapon) ?? "rifle";
|
|
21545
|
+
const entry = ids[caseType];
|
|
21546
|
+
if (!entry) return 0;
|
|
21547
|
+
const direction = event.direction ?? [0, 0, -1];
|
|
21548
|
+
const seed = ++counter;
|
|
21549
|
+
let started = 0;
|
|
21550
|
+
for (let shot = 0; shot < Math.max(1, event.rounds ?? 1); shot++) {
|
|
21551
|
+
if (sink.play({ effect: entry.muzzle, point: event.origin, direction, seed: seed * 8 + shot }) !== null) started++;
|
|
21552
|
+
}
|
|
21553
|
+
const side = [direction[2], 0, -direction[0]];
|
|
21554
|
+
const port = [
|
|
21555
|
+
event.origin[0] - direction[0] * PORT_BACK + side[0] * PORT_RIGHT,
|
|
21556
|
+
event.origin[1] - direction[1] * PORT_BACK,
|
|
21557
|
+
event.origin[2] - direction[2] * PORT_BACK + side[2] * PORT_RIGHT
|
|
21558
|
+
];
|
|
21559
|
+
if (sink.play({ effect: entry.casing, point: port, direction, seed: seed * 8 + 7 }) !== null) started++;
|
|
21560
|
+
return started;
|
|
21561
|
+
}
|
|
21562
|
+
};
|
|
21563
|
+
}
|
|
21564
|
+
|
|
20015
21565
|
// src/area.ts
|
|
20016
21566
|
var WEAPON_ITEM_PREFIX = "weapon_";
|
|
20017
21567
|
var COUNTER_ITEM_ID = "counter_weapons";
|
|
@@ -20180,7 +21730,8 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
|
|
|
20180
21730
|
const arsenal = options.arsenal ?? createArsenal({ locale });
|
|
20181
21731
|
const text = textOf(locale);
|
|
20182
21732
|
const accessoryText = accessoryTextOf(locale);
|
|
20183
|
-
const
|
|
21733
|
+
const seedOption = options.seed === void 0 ? {} : { seed: options.seed };
|
|
21734
|
+
let audio = options.sound === false ? null : createWeaponAudio(void 0, seedOption);
|
|
20184
21735
|
const weaponDefs = [...arsenal.weapons.values()].filter((def) => !BODY_ACTIONS.has(def.id));
|
|
20185
21736
|
const thumbnailBase = options.thumbnailBase ?? DEFAULT_THUMBNAIL_BASE;
|
|
20186
21737
|
const withThumbnail = (item, key) => {
|
|
@@ -20207,7 +21758,9 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
|
|
|
20207
21758
|
id: "weapons_area",
|
|
20208
21759
|
name: text.name,
|
|
20209
21760
|
group: text.group,
|
|
20210
|
-
audio
|
|
21761
|
+
get audio() {
|
|
21762
|
+
return audio;
|
|
21763
|
+
},
|
|
20211
21764
|
get fx() {
|
|
20212
21765
|
return fx;
|
|
20213
21766
|
},
|
|
@@ -20255,6 +21808,11 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
|
|
|
20255
21808
|
],
|
|
20256
21809
|
attach(lent) {
|
|
20257
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
|
+
}
|
|
20258
21816
|
const created = createWeaponFx({
|
|
20259
21817
|
THREE: lent.THREE,
|
|
20260
21818
|
scene: lent.scene,
|
|
@@ -20288,7 +21846,9 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
|
|
|
20288
21846
|
} else if (change.kind === "control" && change.id === "sound_distance" && typeof change.value === "number") soundDistance = change.value;
|
|
20289
21847
|
else if (change.kind === "control" && change.id === "fuse_length" && typeof change.value === "number") lent.configureTool?.({ fuse_length: String(change.value) });
|
|
20290
21848
|
});
|
|
21849
|
+
const worldEffects = createWorldEffects({ arsenal });
|
|
20291
21850
|
const offEvents = lent.on((event) => {
|
|
21851
|
+
worldEffects.handle(event, lent.effects);
|
|
20292
21852
|
if (event.type === "explosion") {
|
|
20293
21853
|
const away = Math.hypot(event.point[0] - camera.position.x, event.point[1] - camera.position.y, event.point[2] - camera.position.z);
|
|
20294
21854
|
audio?.play("explosion", Math.min(1, EXPLOSION_VOLUME_BASE + event.radius * EXPLOSION_VOLUME_PER_METRE), away);
|