@ultrst-armada/kloks 0.0.0-stage → 1.0.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.
Files changed (109) hide show
  1. package/README.md +2 -3
  2. package/index.d.ts +5 -0
  3. package/index.js +5 -0
  4. package/package.json +122 -4
  5. package/packages/audio-midi/dist/audioClipEngine-BOT33JKI.js +9 -0
  6. package/packages/audio-midi/dist/audioInputManager-BLDGGLAR.js +12 -0
  7. package/packages/audio-midi/dist/chunk-3GRGELA4.js +2533 -0
  8. package/packages/audio-midi/dist/chunk-N55BWOC6.js +439 -0
  9. package/packages/audio-midi/dist/index.d.ts +988 -0
  10. package/packages/audio-midi/dist/index.js +2591 -0
  11. package/packages/audio-midi/kloks.manifest.json +69 -0
  12. package/packages/audio-midi/package.json +33 -0
  13. package/packages/backend/dist/BaseAccountProvider-DduKf5HG.d.ts +66 -0
  14. package/packages/backend/dist/BaseStorageAdapter-tosOoSUg.d.ts +67 -0
  15. package/packages/backend/dist/IdempotencyManager-B5MjK871.d.ts +16 -0
  16. package/packages/backend/dist/WebSocketTransport-CxUzUfwl.d.ts +255 -0
  17. package/packages/backend/dist/api/index.d.ts +32 -0
  18. package/packages/backend/dist/api/index.js +12 -0
  19. package/packages/backend/dist/auth/index.d.ts +56 -0
  20. package/packages/backend/dist/auth/index.js +22 -0
  21. package/packages/backend/dist/chunk-25Y7AVTP.js +55 -0
  22. package/packages/backend/dist/chunk-3EPY34R6.js +67 -0
  23. package/packages/backend/dist/chunk-4QEMALXF.js +90 -0
  24. package/packages/backend/dist/chunk-4ZFXEFOE.js +265 -0
  25. package/packages/backend/dist/chunk-53WQNION.js +278 -0
  26. package/packages/backend/dist/chunk-5MUQSPG4.js +149 -0
  27. package/packages/backend/dist/chunk-FZKBTF34.js +15 -0
  28. package/packages/backend/dist/chunk-G3ZOSXUR.js +31 -0
  29. package/packages/backend/dist/chunk-J2J24JVC.js +731 -0
  30. package/packages/backend/dist/chunk-KPUGS64G.js +221 -0
  31. package/packages/backend/dist/chunk-KTLD7Z4L.js +124 -0
  32. package/packages/backend/dist/chunk-LS3HWJBX.js +0 -0
  33. package/packages/backend/dist/chunk-MN54ZIKZ.js +336 -0
  34. package/packages/backend/dist/chunk-NIP22V2G.js +828 -0
  35. package/packages/backend/dist/chunk-QGHDJGYS.js +92 -0
  36. package/packages/backend/dist/chunk-RCCQ7IRM.js +139 -0
  37. package/packages/backend/dist/chunk-T3NYNBDC.js +618 -0
  38. package/packages/backend/dist/chunk-TWCEVES2.js +93 -0
  39. package/packages/backend/dist/chunk-UPFKBBQI.js +138 -0
  40. package/packages/backend/dist/chunk-VQEFULWT.js +88 -0
  41. package/packages/backend/dist/client/index.d.ts +22 -0
  42. package/packages/backend/dist/client/index.js +30 -0
  43. package/packages/backend/dist/connectivity/index.d.ts +237 -0
  44. package/packages/backend/dist/connectivity/index.js +50 -0
  45. package/packages/backend/dist/control-plane/index.d.ts +56 -0
  46. package/packages/backend/dist/control-plane/index.js +16 -0
  47. package/packages/backend/dist/execution/index.d.ts +124 -0
  48. package/packages/backend/dist/execution/index.js +20 -0
  49. package/packages/backend/dist/index.d.ts +692 -0
  50. package/packages/backend/dist/index.js +3299 -0
  51. package/packages/backend/dist/krnl-services.d.ts +82 -0
  52. package/packages/backend/dist/krnl-services.js +38 -0
  53. package/packages/backend/dist/services/index.d.ts +66 -0
  54. package/packages/backend/dist/services/index.js +17 -0
  55. package/packages/backend/dist/srkl/index.d.ts +191 -0
  56. package/packages/backend/dist/srkl/index.js +19 -0
  57. package/packages/backend/dist/storage/index.d.ts +124 -0
  58. package/packages/backend/dist/storage/index.js +43 -0
  59. package/packages/backend/dist/types-BERZJ93O.d.ts +110 -0
  60. package/packages/backend/dist/workspace/index.d.ts +111 -0
  61. package/packages/backend/dist/workspace/index.js +16 -0
  62. package/packages/backend/kloks.manifest.json +195 -0
  63. package/packages/backend/package.json +125 -0
  64. package/packages/functions/dist/index.d.ts +388 -0
  65. package/packages/functions/dist/index.js +1201 -0
  66. package/packages/functions/kloks.manifest.json +20 -0
  67. package/packages/functions/package.json +33 -0
  68. package/packages/orchestrations/dist/index.d.ts +53 -0
  69. package/packages/orchestrations/dist/index.js +211 -0
  70. package/packages/orchestrations/kloks.manifest.json +23 -0
  71. package/packages/orchestrations/package.json +35 -0
  72. package/packages/services/dist/index.d.ts +334 -0
  73. package/packages/services/dist/index.js +1597 -0
  74. package/packages/services/kloks.manifest.json +59 -0
  75. package/packages/services/package.json +33 -0
  76. package/packages/state-graph/dist/BaseConnection-CMRrL6Fe.d.ts +43 -0
  77. package/packages/state-graph/dist/BaseGraph-Bl2f7Pqx.d.ts +98 -0
  78. package/packages/state-graph/dist/BaseNode-1HCEqmap.d.ts +123 -0
  79. package/packages/state-graph/dist/CompatibilityMatrix-DX307_vS.d.ts +29 -0
  80. package/packages/state-graph/dist/TraceEngine-CJVpUb80.d.ts +18 -0
  81. package/packages/state-graph/dist/chunk-3RG5ZIWI.js +10 -0
  82. package/packages/state-graph/dist/chunk-4UVGXFEM.js +465 -0
  83. package/packages/state-graph/dist/chunk-ISFNY7EZ.js +61 -0
  84. package/packages/state-graph/dist/chunk-KG6QYS26.js +167 -0
  85. package/packages/state-graph/dist/chunk-OV4JVTNY.js +147 -0
  86. package/packages/state-graph/dist/chunk-OYN4ZEZC.js +54 -0
  87. package/packages/state-graph/dist/chunk-Q5I42UX7.js +0 -0
  88. package/packages/state-graph/dist/chunk-QN74PLMF.js +241 -0
  89. package/packages/state-graph/dist/chunk-S22PMERU.js +223 -0
  90. package/packages/state-graph/dist/chunk-SHIBCPGQ.js +269 -0
  91. package/packages/state-graph/dist/chunk-YTWCH6P2.js +109 -0
  92. package/packages/state-graph/dist/chunk-ZTHBPV24.js +267 -0
  93. package/packages/state-graph/dist/domain/index.d.ts +117 -0
  94. package/packages/state-graph/dist/domain/index.js +45 -0
  95. package/packages/state-graph/dist/execution/index.d.ts +152 -0
  96. package/packages/state-graph/dist/execution/index.js +16 -0
  97. package/packages/state-graph/dist/index-CiekWEH3.d.ts +272 -0
  98. package/packages/state-graph/dist/index.d.ts +3170 -0
  99. package/packages/state-graph/dist/index.js +9574 -0
  100. package/packages/state-graph/dist/rules/index.d.ts +86 -0
  101. package/packages/state-graph/dist/rules/index.js +17 -0
  102. package/packages/state-graph/dist/spatial/index.d.ts +55 -0
  103. package/packages/state-graph/dist/spatial/index.js +21 -0
  104. package/packages/state-graph/dist/state/index.d.ts +109 -0
  105. package/packages/state-graph/dist/state/index.js +20 -0
  106. package/packages/state-graph/dist/store/index.d.ts +6 -0
  107. package/packages/state-graph/dist/store/index.js +15 -0
  108. package/packages/state-graph/kloks.manifest.json +40 -0
  109. package/packages/state-graph/package.json +73 -0
@@ -0,0 +1,2533 @@
1
+ // src/engine/audio/audioInputManager/types.ts
2
+ var DEFAULT_NAMED_BUSSES = [
3
+ {
4
+ id: "bus-in-1",
5
+ busType: "input",
6
+ deviceId: "default",
7
+ name: "Main Mic / Line In 1",
8
+ channel: 1,
9
+ gain: 1,
10
+ isMuted: false,
11
+ color: "#f59e0b"
12
+ },
13
+ {
14
+ id: "bus-in-2",
15
+ busType: "input",
16
+ deviceId: "default",
17
+ name: "Instrument / Line In 2",
18
+ channel: 2,
19
+ gain: 1,
20
+ isMuted: false,
21
+ color: "#06b6d4"
22
+ },
23
+ {
24
+ id: "bus-out-1",
25
+ busType: "output",
26
+ deviceId: "default",
27
+ name: "Main Monitors Out L/R",
28
+ outputChannels: "Ch 1 / 2 (Main L/R)",
29
+ gain: 1,
30
+ isMuted: false,
31
+ color: "#10b981"
32
+ }
33
+ ];
34
+
35
+ // src/engine/audio/synthEngine/synthEngine.ts
36
+ import { midiToFreq } from "@ultrst-armada/kloks/state-graph";
37
+
38
+ // src/engine/audio/outputRouter.ts
39
+ var MultiChannelOutputRouter = class {
40
+ ctx = null;
41
+ mergerNode = null;
42
+ splitterNode = null;
43
+ routeGainL = null;
44
+ routeGainR = null;
45
+ currentChannelRouting = "Ch 1 / 2 (Main L/R)";
46
+ maxChannels = 2;
47
+ init(ctx, initialChannels = 6) {
48
+ this.ctx = ctx;
49
+ this.maxChannels = Math.max(2, Math.min(32, initialChannels));
50
+ try {
51
+ if (this.ctx.destination.maxChannelCount >= this.maxChannels) {
52
+ this.ctx.destination.channelCount = this.maxChannels;
53
+ this.ctx.destination.channelCountMode = "explicit";
54
+ this.ctx.destination.channelInterpretation = "discrete";
55
+ }
56
+ } catch (e) {
57
+ console.warn("Could not set multi-channel destination mode:", e);
58
+ }
59
+ this.setupMerger();
60
+ }
61
+ setupMerger() {
62
+ if (!this.ctx) return;
63
+ try {
64
+ this.mergerNode = this.ctx.createChannelMerger(this.maxChannels);
65
+ this.splitterNode = this.ctx.createChannelSplitter(2);
66
+ this.routeGainL = this.ctx.createGain();
67
+ this.routeGainR = this.ctx.createGain();
68
+ this.splitterNode.connect(this.routeGainL, 0);
69
+ this.splitterNode.connect(this.routeGainR, 1);
70
+ this.mergerNode.connect(this.ctx.destination);
71
+ this.applyRouting(this.currentChannelRouting);
72
+ } catch (e) {
73
+ console.warn("Error setting up multi-channel output merger:", e);
74
+ }
75
+ }
76
+ connectInput(sourceNode) {
77
+ if (this.splitterNode) {
78
+ sourceNode.connect(this.splitterNode);
79
+ }
80
+ }
81
+ setMaxChannels(channels) {
82
+ this.maxChannels = Math.max(2, Math.min(32, channels));
83
+ if (this.ctx) {
84
+ try {
85
+ if (this.ctx.destination.maxChannelCount >= this.maxChannels) {
86
+ this.ctx.destination.channelCount = this.maxChannels;
87
+ }
88
+ } catch (e) {
89
+ }
90
+ this.setupMerger();
91
+ }
92
+ }
93
+ applyRouting(routingId) {
94
+ this.currentChannelRouting = routingId;
95
+ if (!this.routeGainL || !this.routeGainR || !this.mergerNode || !this.ctx) return;
96
+ try {
97
+ this.routeGainL.disconnect();
98
+ this.routeGainR.disconnect();
99
+ if (!routingId || routingId === "none") {
100
+ return;
101
+ }
102
+ let targetL = 0;
103
+ let targetR = 1;
104
+ if (routingId.includes("3 / 4") || routingId.includes("3/4")) {
105
+ targetL = Math.min(2, this.maxChannels - 1);
106
+ targetR = Math.min(3, this.maxChannels - 1);
107
+ } else if (routingId.includes("5 / 6") || routingId.includes("5/6")) {
108
+ targetL = Math.min(4, this.maxChannels - 1);
109
+ targetR = Math.min(5, this.maxChannels - 1);
110
+ } else if (routingId.includes("7 / 8") || routingId.includes("7/8")) {
111
+ targetL = Math.min(6, this.maxChannels - 1);
112
+ targetR = Math.min(7, this.maxChannels - 1);
113
+ } else if (routingId.includes("Ch 1 (Mono") || routingId === "Ch 1") {
114
+ targetL = 0;
115
+ targetR = 0;
116
+ } else if (routingId.includes("Ch 2 (Mono") || routingId === "Ch 2") {
117
+ targetL = Math.min(1, this.maxChannels - 1);
118
+ targetR = Math.min(1, this.maxChannels - 1);
119
+ } else if (routingId.includes("Ch 3")) {
120
+ targetL = Math.min(2, this.maxChannels - 1);
121
+ targetR = Math.min(2, this.maxChannels - 1);
122
+ } else if (routingId.includes("Ch 4")) {
123
+ targetL = Math.min(3, this.maxChannels - 1);
124
+ targetR = Math.min(3, this.maxChannels - 1);
125
+ } else if (routingId.includes("Ch 5")) {
126
+ targetL = Math.min(4, this.maxChannels - 1);
127
+ targetR = Math.min(4, this.maxChannels - 1);
128
+ } else if (routingId.includes("Ch 6")) {
129
+ targetL = Math.min(5, this.maxChannels - 1);
130
+ targetR = Math.min(5, this.maxChannels - 1);
131
+ }
132
+ this.routeGainL.connect(this.mergerNode, 0, targetL);
133
+ this.routeGainR.connect(this.mergerNode, 0, targetR);
134
+ } catch (e) {
135
+ console.warn("Could not re-route output channels:", e);
136
+ }
137
+ }
138
+ async setOutputSinkId(deviceId) {
139
+ if (this.ctx && typeof this.ctx.setSinkId === "function") {
140
+ try {
141
+ await this.ctx.setSinkId(deviceId === "default" ? "" : deviceId);
142
+ return true;
143
+ } catch (e) {
144
+ console.warn("setSinkId failed:", e);
145
+ return false;
146
+ }
147
+ }
148
+ return false;
149
+ }
150
+ };
151
+ var multiChannelOutputRouter = new MultiChannelOutputRouter();
152
+
153
+ // src/engine/audio/synthVoiceHelper.ts
154
+ function createCosmicImpulseBuffer(ctx, duration, decay) {
155
+ const rate = ctx.sampleRate;
156
+ const length = rate * duration;
157
+ const impulse = ctx.createBuffer(2, length, rate);
158
+ for (let channel = 0; channel < 2; channel++) {
159
+ const channelData = impulse.getChannelData(channel);
160
+ for (let i = 0; i < length; i++) {
161
+ const n = i / length;
162
+ channelData[i] = (Math.random() * 2 - 1) * Math.pow(1 - n, decay);
163
+ }
164
+ }
165
+ return impulse;
166
+ }
167
+ function playSyntheticDrumVoice(ctx, masterGain, noteNumber, vel, time) {
168
+ const osc = ctx.createOscillator();
169
+ const gain = ctx.createGain();
170
+ const isKick = noteNumber <= 38;
171
+ if (isKick) {
172
+ osc.frequency.setValueAtTime(150, time);
173
+ osc.frequency.exponentialRampToValueAtTime(38, time + 0.12);
174
+ gain.gain.setValueAtTime(vel * 0.8, time);
175
+ gain.gain.exponentialRampToValueAtTime(1e-3, time + 0.35);
176
+ osc.connect(gain);
177
+ gain.connect(masterGain);
178
+ osc.start(time);
179
+ osc.stop(time + 0.36);
180
+ } else {
181
+ osc.type = "triangle";
182
+ osc.frequency.setValueAtTime(280, time);
183
+ osc.frequency.exponentialRampToValueAtTime(80, time + 0.1);
184
+ gain.gain.setValueAtTime(vel * 0.5, time);
185
+ gain.gain.exponentialRampToValueAtTime(1e-3, time + 0.2);
186
+ osc.connect(gain);
187
+ gain.connect(masterGain);
188
+ osc.start(time);
189
+ osc.stop(time + 0.21);
190
+ }
191
+ }
192
+
193
+ // src/engine/audio/AudioGraphNodes.ts
194
+ var BaseAudioGraphNode = class {
195
+ /**
196
+ * Root capability flag.
197
+ * By rule, base nodes CANNOT emit directly to the master output.
198
+ */
199
+ canEmitDirectlyToMaster() {
200
+ return false;
201
+ }
202
+ /**
203
+ * Evaluates if this node is structurally authorized to emit audio.
204
+ * Standard nodes must prove reachability to a MasterAudioOutputGraphNode
205
+ * through the active graph connection topology.
206
+ */
207
+ isAuthorizedToEmit(graphReachabilityChecker) {
208
+ if (this.canEmitDirectlyToMaster()) {
209
+ return true;
210
+ }
211
+ return graphReachabilityChecker(this.id);
212
+ }
213
+ };
214
+ var MasterAudioOutputGraphNode = class extends BaseAudioGraphNode {
215
+ constructor(id, active = true) {
216
+ super();
217
+ this.id = id;
218
+ this.active = active;
219
+ }
220
+ id;
221
+ active;
222
+ nodeType = "output_strip";
223
+ canEmitDirectlyToMaster() {
224
+ return this.active;
225
+ }
226
+ };
227
+ var BaseSourceAudioGraphNode = class extends BaseAudioGraphNode {
228
+ isSourceNode = true;
229
+ };
230
+ var MidiInstrumentGraphNode = class extends BaseSourceAudioGraphNode {
231
+ constructor(id, pluginId = "grand_piano", volume = 1) {
232
+ super();
233
+ this.id = id;
234
+ this.pluginId = pluginId;
235
+ this.volume = volume;
236
+ }
237
+ id;
238
+ pluginId;
239
+ volume;
240
+ nodeType = "instrument";
241
+ };
242
+ var AudioContainerGraphNode = class extends BaseSourceAudioGraphNode {
243
+ constructor(id, audioFormat = "mono") {
244
+ super();
245
+ this.id = id;
246
+ this.audioFormat = audioFormat;
247
+ }
248
+ id;
249
+ audioFormat;
250
+ nodeType = "audio";
251
+ };
252
+ var MidiContainerGraphNode = class extends BaseSourceAudioGraphNode {
253
+ constructor(id) {
254
+ super();
255
+ this.id = id;
256
+ }
257
+ id;
258
+ nodeType = "midi";
259
+ };
260
+ var BaseProcessorAudioGraphNode = class extends BaseAudioGraphNode {
261
+ isProcessorNode = true;
262
+ };
263
+ var EffectProcessorGraphNode = class extends BaseProcessorAudioGraphNode {
264
+ constructor(id, effectType, active = true) {
265
+ super();
266
+ this.id = id;
267
+ this.effectType = effectType;
268
+ this.active = active;
269
+ }
270
+ id;
271
+ effectType;
272
+ active;
273
+ nodeType = "effect";
274
+ };
275
+ var SignalRouterGraphNode = class extends BaseProcessorAudioGraphNode {
276
+ constructor(id, type, active = true) {
277
+ super();
278
+ this.id = id;
279
+ this.active = active;
280
+ this.nodeType = type;
281
+ }
282
+ id;
283
+ active;
284
+ nodeType;
285
+ };
286
+ function createAudioGraphNode(node) {
287
+ switch (node.type) {
288
+ case "container": {
289
+ const cont = node;
290
+ const cType = cont.containerType || "midi";
291
+ if (cType === "instrument") {
292
+ return new MidiInstrumentGraphNode(cont.id, cont.selectedPluginId, cont.volume);
293
+ }
294
+ if (cType === "audio") {
295
+ return new AudioContainerGraphNode(cont.id, cont.audioFormat);
296
+ }
297
+ return new MidiContainerGraphNode(cont.id);
298
+ }
299
+ case "effect": {
300
+ const eff = node;
301
+ return new EffectProcessorGraphNode(eff.id, eff.effectType, eff.active !== false);
302
+ }
303
+ case "combiner":
304
+ case "multiplexer":
305
+ case "converter": {
306
+ return new SignalRouterGraphNode(node.id, node.type, node.active !== false);
307
+ }
308
+ case "output_strip": {
309
+ return new MasterAudioOutputGraphNode(node.id, node.active !== false);
310
+ }
311
+ default:
312
+ if (node.id === "output-strip-node") {
313
+ return new MasterAudioOutputGraphNode(node.id, node.active !== false);
314
+ }
315
+ return new class GenericGraphNode extends BaseAudioGraphNode {
316
+ id = node.id;
317
+ nodeType = node.type;
318
+ }();
319
+ }
320
+ }
321
+
322
+ // src/engine/audio/synthEngine/synthEngine.ts
323
+ import { getActiveEffectsForNode } from "@ultrst-armada/kloks/state-graph";
324
+
325
+ // src/engine/audio/dsp/saturationCurves.ts
326
+ function generateSaturationCurve(type, drive = 1.5, samples = 4096) {
327
+ const curve = new Float32Array(samples);
328
+ const k = Math.max(0.1, drive);
329
+ const deg = Math.PI / 180;
330
+ for (let i = 0; i < samples; i++) {
331
+ const x = i * 2 / samples - 1;
332
+ let y = x;
333
+ switch (type) {
334
+ case "soft_clip": {
335
+ const absX = Math.abs(x * k);
336
+ if (absX < 1) {
337
+ y = x * k - (x * k) ** 3 / 3;
338
+ } else {
339
+ y = x > 0 ? 2 / 3 : -2 / 3;
340
+ }
341
+ y *= 1.2;
342
+ break;
343
+ }
344
+ case "tube": {
345
+ const inVal = x * k;
346
+ if (inVal > 0) {
347
+ y = 1 - Math.exp(-inVal);
348
+ } else {
349
+ y = -(1 - Math.exp(inVal * 0.8));
350
+ }
351
+ y *= 1.1;
352
+ break;
353
+ }
354
+ case "hard_clip": {
355
+ const threshold = 1 / Math.max(1, k);
356
+ y = Math.max(-threshold, Math.min(threshold, x)) * k;
357
+ break;
358
+ }
359
+ case "foldback": {
360
+ y = Math.sin(x * k * Math.PI * 0.5);
361
+ break;
362
+ }
363
+ case "tape":
364
+ default: {
365
+ y = Math.tanh(x * k * 1.2);
366
+ break;
367
+ }
368
+ }
369
+ curve[i] = Math.max(-1, Math.min(1, y));
370
+ }
371
+ return curve;
372
+ }
373
+
374
+ // src/engine/audio/dsp/reverbImpulse.ts
375
+ function generateReverbImpulse(ctx, durationSec = 2.5, decayFactor = 2, dampingFreq = 6e3) {
376
+ const sampleRate = ctx.sampleRate;
377
+ const length = Math.floor(sampleRate * Math.max(0.2, Math.min(10, durationSec)));
378
+ const impulse = ctx.createBuffer(2, length, sampleRate);
379
+ const left = impulse.getChannelData(0);
380
+ const right = impulse.getChannelData(1);
381
+ const dampingRatio = Math.min(1, dampingFreq / (sampleRate / 2));
382
+ let prevL = 0;
383
+ let prevR = 0;
384
+ for (let i = 0; i < length; i++) {
385
+ const t = i / length;
386
+ const env = Math.pow(1 - t, decayFactor);
387
+ const noiseL = (Math.random() * 2 - 1) * env;
388
+ const noiseR = (Math.random() * 2 - 1) * env;
389
+ prevL = prevL + dampingRatio * (noiseL - prevL);
390
+ prevR = prevR + dampingRatio * (noiseR - prevR);
391
+ const earlyEcho = i < sampleRate * 0.04 && i % 400 === 0 ? 0.3 * env : 0;
392
+ left[i] = prevL + earlyEcho;
393
+ right[i] = prevR - earlyEcho;
394
+ }
395
+ return impulse;
396
+ }
397
+
398
+ // src/engine/audio/dsp/sidechainCompressor.ts
399
+ function createSidechainCompressor(ctx, initialConfig) {
400
+ const input = ctx.createGain();
401
+ const output = ctx.createGain();
402
+ const sidechainInput = ctx.createGain();
403
+ const comp = ctx.createDynamicsCompressor();
404
+ const vcaGain = ctx.createGain();
405
+ const makeupGainNode = ctx.createGain();
406
+ const dryGain = ctx.createGain();
407
+ const wetGain = ctx.createGain();
408
+ const scAnalyser = ctx.createAnalyser();
409
+ scAnalyser.fftSize = 256;
410
+ scAnalyser.smoothingTimeConstant = 0.3;
411
+ sidechainInput.connect(scAnalyser);
412
+ let isSidechainActive = false;
413
+ let duckAmount = 0;
414
+ let rafId = null;
415
+ let currentConfig = {
416
+ threshold: -20,
417
+ ratio: 4,
418
+ attack: 0.01,
419
+ release: 0.15,
420
+ knee: 5,
421
+ makeupGain: 0,
422
+ mix: 1,
423
+ sidechainEnabled: false,
424
+ ...initialConfig
425
+ };
426
+ input.connect(dryGain);
427
+ dryGain.connect(output);
428
+ input.connect(comp);
429
+ comp.connect(vcaGain);
430
+ vcaGain.connect(makeupGainNode);
431
+ makeupGainNode.connect(wetGain);
432
+ wetGain.connect(output);
433
+ const applyConfig = (cfg) => {
434
+ currentConfig = { ...currentConfig, ...cfg };
435
+ const now = ctx.currentTime;
436
+ comp.threshold.setValueAtTime(currentConfig.threshold, now);
437
+ comp.ratio.setValueAtTime(Math.max(1, currentConfig.ratio), now);
438
+ comp.attack.setValueAtTime(Math.max(1e-3, currentConfig.attack), now);
439
+ comp.release.setValueAtTime(Math.max(0.01, currentConfig.release), now);
440
+ comp.knee.setValueAtTime(Math.max(0, currentConfig.knee), now);
441
+ const makeupLinear = Math.pow(10, (currentConfig.makeupGain || 0) / 20);
442
+ makeupGainNode.gain.setValueAtTime(makeupLinear, now);
443
+ const mix = Math.max(0, Math.min(1, currentConfig.mix ?? 1));
444
+ wetGain.gain.setValueAtTime(mix, now);
445
+ dryGain.gain.setValueAtTime(1 - mix, now);
446
+ const prevSidechain = isSidechainActive;
447
+ isSidechainActive = !!currentConfig.sidechainEnabled;
448
+ if (isSidechainActive && !prevSidechain) {
449
+ startSidechainLoop();
450
+ } else if (!isSidechainActive && prevSidechain) {
451
+ stopSidechainLoop();
452
+ }
453
+ };
454
+ const pcmBuffer = new Float32Array(scAnalyser.fftSize);
455
+ let smoothedGain = 1;
456
+ const processSidechainTick = () => {
457
+ if (!isSidechainActive) {
458
+ stopSidechainLoop();
459
+ return;
460
+ }
461
+ scAnalyser.getFloatTimeDomainData(pcmBuffer);
462
+ let peak = 0;
463
+ for (let i = 0; i < pcmBuffer.length; i++) {
464
+ const abs = Math.abs(pcmBuffer[i]);
465
+ if (abs > peak) peak = abs;
466
+ }
467
+ const peakDb = peak > 1e-4 ? 20 * Math.log10(peak) : -100;
468
+ const threshold = currentConfig.threshold;
469
+ let targetGain = 1;
470
+ if (peakDb > threshold) {
471
+ const overDb = peakDb - threshold;
472
+ const reductionDb = overDb * (1 - 1 / Math.max(1, currentConfig.ratio));
473
+ duckAmount = reductionDb;
474
+ targetGain = Math.max(0.01, Math.pow(10, -reductionDb / 20));
475
+ } else {
476
+ duckAmount = 0;
477
+ }
478
+ const att = Math.max(5e-3, currentConfig.attack);
479
+ const rel = Math.max(0.02, currentConfig.release);
480
+ const coeff = targetGain < smoothedGain ? Math.min(1, 0.05 / att) : Math.min(1, 0.05 / rel);
481
+ smoothedGain += (targetGain - smoothedGain) * coeff;
482
+ vcaGain.gain.setValueAtTime(smoothedGain, ctx.currentTime);
483
+ rafId = requestAnimationFrame(processSidechainTick);
484
+ };
485
+ const startSidechainLoop = () => {
486
+ if (rafId !== null) return;
487
+ if (typeof window !== "undefined") {
488
+ rafId = requestAnimationFrame(processSidechainTick);
489
+ }
490
+ };
491
+ const stopSidechainLoop = () => {
492
+ if (rafId !== null) {
493
+ cancelAnimationFrame(rafId);
494
+ rafId = null;
495
+ }
496
+ duckAmount = 0;
497
+ smoothedGain = 1;
498
+ vcaGain.gain.setValueAtTime(1, ctx.currentTime);
499
+ };
500
+ applyConfig(currentConfig);
501
+ const getReductionDb = () => {
502
+ if (isSidechainActive) {
503
+ return duckAmount;
504
+ }
505
+ return Math.abs(comp.reduction);
506
+ };
507
+ const dispose = () => {
508
+ if (rafId) cancelAnimationFrame(rafId);
509
+ try {
510
+ input.disconnect();
511
+ dryGain.disconnect();
512
+ comp.disconnect();
513
+ vcaGain.disconnect();
514
+ makeupGainNode.disconnect();
515
+ wetGain.disconnect();
516
+ output.disconnect();
517
+ sidechainInput.disconnect();
518
+ scAnalyser.disconnect();
519
+ } catch {
520
+ }
521
+ };
522
+ return {
523
+ input,
524
+ output,
525
+ sidechainInput,
526
+ compressorNode: comp,
527
+ updateConfig: applyConfig,
528
+ getReductionDb,
529
+ dispose
530
+ };
531
+ }
532
+
533
+ // src/engine/audio/dsp/crossoverFilter.ts
534
+ function create3BandCrossover(ctx, initialLowFreq = 150, initialHighFreq = 2500) {
535
+ const input = ctx.createGain();
536
+ const sumOutput = ctx.createGain();
537
+ const lowFilter1 = ctx.createBiquadFilter();
538
+ lowFilter1.type = "lowpass";
539
+ lowFilter1.frequency.value = initialLowFreq;
540
+ lowFilter1.Q.value = 0.707;
541
+ const lowFilter2 = ctx.createBiquadFilter();
542
+ lowFilter2.type = "lowpass";
543
+ lowFilter2.frequency.value = initialLowFreq;
544
+ lowFilter2.Q.value = 0.707;
545
+ input.connect(lowFilter1);
546
+ lowFilter1.connect(lowFilter2);
547
+ const highFilter1 = ctx.createBiquadFilter();
548
+ highFilter1.type = "highpass";
549
+ highFilter1.frequency.value = initialHighFreq;
550
+ highFilter1.Q.value = 0.707;
551
+ const highFilter2 = ctx.createBiquadFilter();
552
+ highFilter2.type = "highpass";
553
+ highFilter2.frequency.value = initialHighFreq;
554
+ highFilter2.Q.value = 0.707;
555
+ input.connect(highFilter1);
556
+ highFilter1.connect(highFilter2);
557
+ const midHp = ctx.createBiquadFilter();
558
+ midHp.type = "highpass";
559
+ midHp.frequency.value = initialLowFreq;
560
+ midHp.Q.value = 0.707;
561
+ const midLp = ctx.createBiquadFilter();
562
+ midLp.type = "lowpass";
563
+ midLp.frequency.value = initialHighFreq;
564
+ midLp.Q.value = 0.707;
565
+ input.connect(midHp);
566
+ midHp.connect(midLp);
567
+ const setFrequencies = (lowFreq, highFreq) => {
568
+ const lF = Math.max(20, Math.min(highFreq - 50, lowFreq));
569
+ const hF = Math.max(lF + 50, Math.min(2e4, highFreq));
570
+ lowFilter1.frequency.setValueAtTime(lF, ctx.currentTime);
571
+ lowFilter2.frequency.setValueAtTime(lF, ctx.currentTime);
572
+ midHp.frequency.setValueAtTime(lF, ctx.currentTime);
573
+ highFilter1.frequency.setValueAtTime(hF, ctx.currentTime);
574
+ highFilter2.frequency.setValueAtTime(hF, ctx.currentTime);
575
+ midLp.frequency.setValueAtTime(hF, ctx.currentTime);
576
+ };
577
+ const dispose = () => {
578
+ try {
579
+ input.disconnect();
580
+ lowFilter1.disconnect();
581
+ lowFilter2.disconnect();
582
+ midHp.disconnect();
583
+ midLp.disconnect();
584
+ highFilter1.disconnect();
585
+ highFilter2.disconnect();
586
+ sumOutput.disconnect();
587
+ } catch {
588
+ }
589
+ };
590
+ return {
591
+ input,
592
+ lowOutput: lowFilter2,
593
+ midOutput: midLp,
594
+ highOutput: highFilter2,
595
+ sumOutput,
596
+ setFrequencies,
597
+ dispose
598
+ };
599
+ }
600
+
601
+ // src/engine/audio/dsp/sidechainMultibandCompressor.ts
602
+ function createMultibandCompressor(ctx, initialConfig) {
603
+ const input = ctx.createGain();
604
+ const output = ctx.createGain();
605
+ const sidechainInput = ctx.createGain();
606
+ const crossover = create3BandCrossover(
607
+ ctx,
608
+ initialConfig?.crossoverLow ?? 150,
609
+ initialConfig?.crossoverHigh ?? 2500
610
+ );
611
+ input.connect(crossover.input);
612
+ const lowComp = createSidechainCompressor(ctx, {
613
+ threshold: initialConfig?.lowBand?.threshold ?? -20,
614
+ ratio: initialConfig?.lowBand?.ratio ?? 3,
615
+ attack: initialConfig?.lowBand?.attack ?? 0.02,
616
+ release: initialConfig?.lowBand?.release ?? 0.15,
617
+ knee: initialConfig?.lowBand?.knee ?? 4,
618
+ makeupGain: initialConfig?.lowBand?.makeupGain ?? 0,
619
+ mix: 1
620
+ });
621
+ const midComp = createSidechainCompressor(ctx, {
622
+ threshold: initialConfig?.midBand?.threshold ?? -18,
623
+ ratio: initialConfig?.midBand?.ratio ?? 2.5,
624
+ attack: initialConfig?.midBand?.attack ?? 0.015,
625
+ release: initialConfig?.midBand?.release ?? 0.12,
626
+ knee: initialConfig?.midBand?.knee ?? 4,
627
+ makeupGain: initialConfig?.midBand?.makeupGain ?? 0,
628
+ mix: 1
629
+ });
630
+ const highComp = createSidechainCompressor(ctx, {
631
+ threshold: initialConfig?.highBand?.threshold ?? -16,
632
+ ratio: initialConfig?.highBand?.ratio ?? 2,
633
+ attack: initialConfig?.highBand?.attack ?? 8e-3,
634
+ release: initialConfig?.highBand?.release ?? 0.08,
635
+ knee: initialConfig?.highBand?.knee ?? 4,
636
+ makeupGain: initialConfig?.highBand?.makeupGain ?? 0,
637
+ mix: 1
638
+ });
639
+ crossover.lowOutput.connect(lowComp.input);
640
+ crossover.midOutput.connect(midComp.input);
641
+ crossover.highOutput.connect(highComp.input);
642
+ const lowGain = ctx.createGain();
643
+ const midGain = ctx.createGain();
644
+ const highGain = ctx.createGain();
645
+ lowComp.output.connect(lowGain);
646
+ midComp.output.connect(midGain);
647
+ highComp.output.connect(highGain);
648
+ const sumNode = ctx.createGain();
649
+ lowGain.connect(sumNode);
650
+ midGain.connect(sumNode);
651
+ highGain.connect(sumNode);
652
+ const dryGain = ctx.createGain();
653
+ const wetGain = ctx.createGain();
654
+ input.connect(dryGain);
655
+ dryGain.connect(output);
656
+ sumNode.connect(wetGain);
657
+ wetGain.connect(output);
658
+ const lowScGain = ctx.createGain();
659
+ const midScGain = ctx.createGain();
660
+ const highScGain = ctx.createGain();
661
+ sidechainInput.connect(lowScGain);
662
+ sidechainInput.connect(midScGain);
663
+ sidechainInput.connect(highScGain);
664
+ lowScGain.connect(lowComp.sidechainInput);
665
+ midScGain.connect(midComp.sidechainInput);
666
+ highScGain.connect(highComp.sidechainInput);
667
+ const applyConfig = (cfg) => {
668
+ const now = ctx.currentTime;
669
+ crossover.setFrequencies(cfg.crossoverLow, cfg.crossoverHigh);
670
+ const hasSolo = cfg.lowBand.solo || cfg.midBand.solo || cfg.highBand.solo;
671
+ const lowActive = !cfg.lowBand.muted && (!hasSolo || !!cfg.lowBand.solo);
672
+ lowGain.gain.setValueAtTime(lowActive ? 1 : 0, now);
673
+ lowComp.updateConfig({
674
+ ...cfg.lowBand,
675
+ knee: cfg.lowBand.knee ?? 4,
676
+ mix: 1,
677
+ sidechainEnabled: cfg.sidechainEnabled && (cfg.sidechainBands === "all" || cfg.sidechainBands === "low")
678
+ });
679
+ const midActive = !cfg.midBand.muted && (!hasSolo || !!cfg.midBand.solo);
680
+ midGain.gain.setValueAtTime(midActive ? 1 : 0, now);
681
+ midComp.updateConfig({
682
+ ...cfg.midBand,
683
+ knee: cfg.midBand.knee ?? 4,
684
+ mix: 1,
685
+ sidechainEnabled: cfg.sidechainEnabled && (cfg.sidechainBands === "all" || cfg.sidechainBands === "mid")
686
+ });
687
+ const highActive = !cfg.highBand.muted && (!hasSolo || !!cfg.highBand.solo);
688
+ highGain.gain.setValueAtTime(highActive ? 1 : 0, now);
689
+ highComp.updateConfig({
690
+ ...cfg.highBand,
691
+ knee: cfg.highBand.knee ?? 4,
692
+ mix: 1,
693
+ sidechainEnabled: cfg.sidechainEnabled && (cfg.sidechainBands === "all" || cfg.sidechainBands === "high")
694
+ });
695
+ const mix = Math.max(0, Math.min(1, cfg.mix ?? 1));
696
+ wetGain.gain.setValueAtTime(mix * (cfg.outputGain ?? 1), now);
697
+ dryGain.gain.setValueAtTime(1 - mix, now);
698
+ };
699
+ if (initialConfig) {
700
+ applyConfig(initialConfig);
701
+ }
702
+ const getReductionDb = () => ({
703
+ low: lowComp.getReductionDb(),
704
+ mid: midComp.getReductionDb(),
705
+ high: highComp.getReductionDb()
706
+ });
707
+ const dispose = () => {
708
+ try {
709
+ crossover.dispose();
710
+ lowComp.dispose();
711
+ midComp.dispose();
712
+ highComp.dispose();
713
+ input.disconnect();
714
+ output.disconnect();
715
+ sumNode.disconnect();
716
+ sidechainInput.disconnect();
717
+ } catch {
718
+ }
719
+ };
720
+ return {
721
+ input,
722
+ output,
723
+ sidechainInput,
724
+ updateConfig: applyConfig,
725
+ getReductionDb,
726
+ dispose
727
+ };
728
+ }
729
+
730
+ // src/engine/audio/dsp/effectProcessorManager/effectProcessorManager.ts
731
+ function createAudioEffectProcessor(ctx, effect) {
732
+ const masterIn = ctx.createGain();
733
+ const masterOut = ctx.createGain();
734
+ let sidechainInputNode;
735
+ let sidechainComp = null;
736
+ let multibandComp = null;
737
+ const nodesToClean = [masterIn, masterOut];
738
+ let eqFilters = [];
739
+ let limiterComp = null;
740
+ let limiterCeiling = null;
741
+ let reverbWetGain = null;
742
+ let reverbDryGain = null;
743
+ let delayNode = null;
744
+ let delayFeedbackGain = null;
745
+ let delayFilter = null;
746
+ let delayWetGain = null;
747
+ let delayDryGain = null;
748
+ let satShaper = null;
749
+ let satTone = null;
750
+ let satOut = null;
751
+ let gainNode = null;
752
+ switch (effect.effectType) {
753
+ case "eq": {
754
+ const defaultBands = effect.eqConfig?.bands || [
755
+ { id: "b1", type: "lowshelf", frequency: 100, gain: 0, q: 0.7, enabled: true },
756
+ { id: "b2", type: "peaking", frequency: 500, gain: 0, q: 1, enabled: true },
757
+ { id: "b3", type: "peaking", frequency: 2500, gain: 0, q: 1, enabled: true },
758
+ { id: "b4", type: "highshelf", frequency: 8e3, gain: 0, q: 0.7, enabled: true }
759
+ ];
760
+ eqFilters = defaultBands.map((band) => {
761
+ const f = ctx.createBiquadFilter();
762
+ f.type = band.type;
763
+ f.frequency.value = band.frequency;
764
+ f.gain.value = band.enabled ? band.gain : 0;
765
+ f.Q.value = band.q;
766
+ nodesToClean.push(f);
767
+ return { bandId: band.id, filter: f };
768
+ });
769
+ let prev = masterIn;
770
+ eqFilters.forEach((item) => {
771
+ prev.connect(item.filter);
772
+ prev = item.filter;
773
+ });
774
+ prev.connect(masterOut);
775
+ break;
776
+ }
777
+ case "compressor": {
778
+ sidechainComp = createSidechainCompressor(ctx, effect.compressorConfig);
779
+ sidechainInputNode = sidechainComp.sidechainInput;
780
+ masterIn.connect(sidechainComp.input);
781
+ sidechainComp.output.connect(masterOut);
782
+ break;
783
+ }
784
+ case "multiband_compressor": {
785
+ multibandComp = createMultibandCompressor(ctx, effect.multibandCompressorConfig);
786
+ sidechainInputNode = multibandComp.sidechainInput;
787
+ masterIn.connect(multibandComp.input);
788
+ multibandComp.output.connect(masterOut);
789
+ break;
790
+ }
791
+ case "limiter": {
792
+ limiterComp = ctx.createDynamicsCompressor();
793
+ limiterComp.threshold.value = effect.limiterConfig?.threshold ?? -2;
794
+ limiterComp.knee.value = 0;
795
+ limiterComp.ratio.value = 20;
796
+ limiterComp.attack.value = 2e-3;
797
+ limiterComp.release.value = effect.limiterConfig?.release ?? 0.05;
798
+ const shaper = ctx.createWaveShaper();
799
+ shaper.curve = generateSaturationCurve("soft_clip", 1);
800
+ shaper.oversample = "2x";
801
+ limiterCeiling = ctx.createGain();
802
+ const ceilingDb = effect.limiterConfig?.ceiling ?? -0.1;
803
+ limiterCeiling.gain.value = Math.pow(10, ceilingDb / 20);
804
+ masterIn.connect(limiterComp);
805
+ limiterComp.connect(shaper);
806
+ shaper.connect(limiterCeiling);
807
+ limiterCeiling.connect(masterOut);
808
+ nodesToClean.push(limiterComp, shaper, limiterCeiling);
809
+ break;
810
+ }
811
+ case "reverb": {
812
+ const convolver = ctx.createConvolver();
813
+ const impulse = generateReverbImpulse(
814
+ ctx,
815
+ effect.reverbConfig?.decayTime ?? 2.5,
816
+ 2,
817
+ effect.reverbConfig?.damping ?? 6e3
818
+ );
819
+ convolver.buffer = impulse;
820
+ reverbWetGain = ctx.createGain();
821
+ reverbDryGain = ctx.createGain();
822
+ const wetMix = effect.reverbConfig?.wetMix ?? 0.4;
823
+ reverbWetGain.gain.value = wetMix;
824
+ reverbDryGain.gain.value = 1 - wetMix;
825
+ masterIn.connect(reverbDryGain);
826
+ reverbDryGain.connect(masterOut);
827
+ masterIn.connect(convolver);
828
+ convolver.connect(reverbWetGain);
829
+ reverbWetGain.connect(masterOut);
830
+ nodesToClean.push(convolver, reverbWetGain, reverbDryGain);
831
+ break;
832
+ }
833
+ case "delay": {
834
+ delayNode = ctx.createDelay(5);
835
+ const delaySec = (effect.delayConfig?.delayBeats ?? 0.5) * 0.5;
836
+ delayNode.delayTime.value = delaySec;
837
+ delayFeedbackGain = ctx.createGain();
838
+ delayFeedbackGain.gain.value = Math.min(0.9, effect.delayConfig?.feedback ?? 0.4);
839
+ delayFilter = ctx.createBiquadFilter();
840
+ delayFilter.type = "lowpass";
841
+ delayFilter.frequency.value = effect.delayConfig?.highCut ?? 6e3;
842
+ delayWetGain = ctx.createGain();
843
+ delayDryGain = ctx.createGain();
844
+ const wetMix = effect.delayConfig?.wetMix ?? 0.5;
845
+ delayWetGain.gain.value = wetMix;
846
+ delayDryGain.gain.value = 1 - wetMix;
847
+ masterIn.connect(delayDryGain);
848
+ delayDryGain.connect(masterOut);
849
+ masterIn.connect(delayNode);
850
+ delayNode.connect(delayFilter);
851
+ delayFilter.connect(delayFeedbackGain);
852
+ delayFeedbackGain.connect(delayNode);
853
+ delayFilter.connect(delayWetGain);
854
+ delayWetGain.connect(masterOut);
855
+ nodesToClean.push(delayNode, delayFeedbackGain, delayFilter, delayWetGain, delayDryGain);
856
+ break;
857
+ }
858
+ case "saturation": {
859
+ satShaper = ctx.createWaveShaper();
860
+ const satType = effect.saturationConfig?.saturationType || "tape";
861
+ const drive = effect.saturationConfig?.drive ?? 1.5;
862
+ satShaper.curve = generateSaturationCurve(satType, drive);
863
+ satShaper.oversample = "4x";
864
+ satTone = ctx.createBiquadFilter();
865
+ satTone.type = "lowpass";
866
+ satTone.frequency.value = effect.saturationConfig?.tone ?? 8e3;
867
+ satOut = ctx.createGain();
868
+ satOut.gain.value = effect.saturationConfig?.outputGain ?? 1;
869
+ masterIn.connect(satShaper);
870
+ satShaper.connect(satTone);
871
+ satTone.connect(satOut);
872
+ satOut.connect(masterOut);
873
+ nodesToClean.push(satShaper, satTone, satOut);
874
+ break;
875
+ }
876
+ case "gain":
877
+ default: {
878
+ gainNode = ctx.createGain();
879
+ gainNode.gain.value = effect.gainConfig?.gain ?? 1;
880
+ masterIn.connect(gainNode);
881
+ gainNode.connect(masterOut);
882
+ nodesToClean.push(gainNode);
883
+ break;
884
+ }
885
+ }
886
+ const updateEffect = (updated) => {
887
+ if (sidechainComp && updated.compressorConfig) {
888
+ sidechainComp.updateConfig(updated.compressorConfig);
889
+ }
890
+ if (multibandComp && updated.multibandCompressorConfig) {
891
+ multibandComp.updateConfig(updated.multibandCompressorConfig);
892
+ }
893
+ if (updated.eqConfig?.bands && eqFilters.length > 0) {
894
+ updated.eqConfig.bands.forEach((b) => {
895
+ const found = eqFilters.find((f) => f.bandId === b.id);
896
+ if (found) {
897
+ found.filter.frequency.setValueAtTime(b.frequency, ctx.currentTime);
898
+ found.filter.gain.setValueAtTime(b.enabled ? b.gain : 0, ctx.currentTime);
899
+ found.filter.Q.setValueAtTime(b.q, ctx.currentTime);
900
+ }
901
+ });
902
+ }
903
+ if (limiterComp && updated.limiterConfig) {
904
+ limiterComp.threshold.setValueAtTime(updated.limiterConfig.threshold ?? -2, ctx.currentTime);
905
+ limiterComp.release.setValueAtTime(updated.limiterConfig.release ?? 0.05, ctx.currentTime);
906
+ if (limiterCeiling) {
907
+ const ceilingDb = updated.limiterConfig.ceiling ?? -0.1;
908
+ limiterCeiling.gain.setValueAtTime(Math.pow(10, ceilingDb / 20), ctx.currentTime);
909
+ }
910
+ }
911
+ if (updated.reverbConfig && reverbWetGain && reverbDryGain) {
912
+ const wet = updated.reverbConfig.wetMix ?? 0.4;
913
+ reverbWetGain.gain.setValueAtTime(wet, ctx.currentTime);
914
+ reverbDryGain.gain.setValueAtTime(1 - wet, ctx.currentTime);
915
+ }
916
+ if (updated.delayConfig && delayNode && delayFeedbackGain && delayFilter && delayWetGain && delayDryGain) {
917
+ delayNode.delayTime.setValueAtTime((updated.delayConfig.delayBeats ?? 0.5) * 0.5, ctx.currentTime);
918
+ delayFeedbackGain.gain.setValueAtTime(Math.min(0.9, updated.delayConfig.feedback ?? 0.4), ctx.currentTime);
919
+ delayFilter.frequency.setValueAtTime(updated.delayConfig.highCut ?? 6e3, ctx.currentTime);
920
+ const wet = updated.delayConfig.wetMix ?? 0.5;
921
+ delayWetGain.gain.setValueAtTime(wet, ctx.currentTime);
922
+ delayDryGain.gain.setValueAtTime(1 - wet, ctx.currentTime);
923
+ }
924
+ if (updated.saturationConfig && satShaper && satTone && satOut) {
925
+ const satType = updated.saturationConfig.saturationType || "tape";
926
+ const drive = updated.saturationConfig.drive ?? 1.5;
927
+ satShaper.curve = generateSaturationCurve(satType, drive);
928
+ satTone.frequency.setValueAtTime(updated.saturationConfig.tone ?? 8e3, ctx.currentTime);
929
+ satOut.gain.setValueAtTime(updated.saturationConfig.outputGain ?? 1, ctx.currentTime);
930
+ }
931
+ if (updated.gainConfig && gainNode) {
932
+ gainNode.gain.setValueAtTime(updated.gainConfig.gain ?? 1, ctx.currentTime);
933
+ }
934
+ };
935
+ const getReductionDb = () => {
936
+ if (sidechainComp) return sidechainComp.getReductionDb();
937
+ if (multibandComp) return multibandComp.getReductionDb();
938
+ return 0;
939
+ };
940
+ const dispose = () => {
941
+ sidechainComp?.dispose();
942
+ multibandComp?.dispose();
943
+ nodesToClean.forEach((n) => {
944
+ try {
945
+ n.disconnect();
946
+ } catch {
947
+ }
948
+ });
949
+ };
950
+ return {
951
+ input: masterIn,
952
+ output: masterOut,
953
+ sidechainInput: sidechainInputNode,
954
+ updateEffect,
955
+ getReductionDb,
956
+ dispose
957
+ };
958
+ }
959
+
960
+ // src/engine/audio/entities/AudioEffects/AudioEffects.ts
961
+ var AudioEffects = class {
962
+ state = {
963
+ activeEffectIds: [],
964
+ processors: /* @__PURE__ */ new Map()
965
+ };
966
+ eventListeners = /* @__PURE__ */ new Map();
967
+ on(event, listener) {
968
+ if (!this.eventListeners.has(event)) {
969
+ this.eventListeners.set(event, /* @__PURE__ */ new Set());
970
+ }
971
+ this.eventListeners.get(event).add(listener);
972
+ return () => this.off(event, listener);
973
+ }
974
+ off(event, listener) {
975
+ this.eventListeners.get(event)?.delete(listener);
976
+ }
977
+ emit(event, payload) {
978
+ this.eventListeners.get(event)?.forEach((l) => {
979
+ try {
980
+ l(payload);
981
+ } catch (err) {
982
+ console.error(`[AudioEffects] Error in event listener '${event}':`, err);
983
+ }
984
+ });
985
+ }
986
+ getOrCreateProcessor(ctx, effect) {
987
+ let proc = this.state.processors.get(effect.id);
988
+ if (!proc) {
989
+ proc = createAudioEffectProcessor(ctx, effect);
990
+ this.state.processors.set(effect.id, proc);
991
+ if (!this.state.activeEffectIds.includes(effect.id)) {
992
+ this.state.activeEffectIds.push(effect.id);
993
+ }
994
+ this.emit("processor:created", { effectId: effect.id, effectType: effect.effectType });
995
+ } else {
996
+ proc.updateEffect(effect);
997
+ }
998
+ return proc;
999
+ }
1000
+ removeProcessor(effectId) {
1001
+ const proc = this.state.processors.get(effectId);
1002
+ if (proc) {
1003
+ proc.dispose();
1004
+ this.state.processors.delete(effectId);
1005
+ this.state.activeEffectIds = this.state.activeEffectIds.filter((id) => id !== effectId);
1006
+ this.emit("processor:disposed", { effectId });
1007
+ }
1008
+ }
1009
+ getActiveEffectIds() {
1010
+ return [...this.state.activeEffectIds];
1011
+ }
1012
+ getReductionDb(effectId) {
1013
+ const proc = this.state.processors.get(effectId);
1014
+ return proc?.getReductionDb?.() ?? 0;
1015
+ }
1016
+ disposeAll() {
1017
+ this.state.processors.forEach((proc) => proc.dispose());
1018
+ this.state.processors.clear();
1019
+ this.state.activeEffectIds = [];
1020
+ }
1021
+ };
1022
+ var audioEffectsEntity = new AudioEffects();
1023
+
1024
+ // src/engine/audio/synthEngine/synthEngine.ts
1025
+ var STORAGE_KEY_AUDIO_CONFIG = "celestial_audio_engine_config";
1026
+ var DEFAULT_AUDIO_CONFIG = {
1027
+ sampleRate: 0,
1028
+ bufferSize: 256
1029
+ };
1030
+ var SynthEngine = class {
1031
+ ctx = null;
1032
+ masterGain = null;
1033
+ delayNode = null;
1034
+ delayFeedback = null;
1035
+ delayGain = null;
1036
+ reverbNode = null;
1037
+ reverbGain = null;
1038
+ analyser = null;
1039
+ analyserL = null;
1040
+ analyserR = null;
1041
+ analyserBuffer = new Uint8Array(256);
1042
+ analyserBufferL = new Uint8Array(256);
1043
+ analyserBufferR = new Uint8Array(256);
1044
+ masterSplitter = null;
1045
+ nodeVisuals = null;
1046
+ isInitialized = false;
1047
+ nodeActiveUntilMap = /* @__PURE__ */ new Map();
1048
+ audioEngineConfig = this.loadAudioConfig();
1049
+ graphNodeMap = /* @__PURE__ */ new Map();
1050
+ loadAudioConfig() {
1051
+ try {
1052
+ const raw = localStorage.getItem(STORAGE_KEY_AUDIO_CONFIG);
1053
+ if (raw) {
1054
+ const parsed = JSON.parse(raw);
1055
+ return {
1056
+ sampleRate: typeof parsed.sampleRate === "number" ? parsed.sampleRate : DEFAULT_AUDIO_CONFIG.sampleRate,
1057
+ bufferSize: typeof parsed.bufferSize === "number" ? parsed.bufferSize : DEFAULT_AUDIO_CONFIG.bufferSize
1058
+ };
1059
+ }
1060
+ } catch (e) {
1061
+ console.warn("Failed to load audio engine config:", e);
1062
+ }
1063
+ return { ...DEFAULT_AUDIO_CONFIG };
1064
+ }
1065
+ getAudioEngineConfig() {
1066
+ return { ...this.audioEngineConfig };
1067
+ }
1068
+ async updateAudioSettings(newConfig) {
1069
+ this.audioEngineConfig = { ...this.audioEngineConfig, ...newConfig };
1070
+ try {
1071
+ localStorage.setItem(STORAGE_KEY_AUDIO_CONFIG, JSON.stringify(this.audioEngineConfig));
1072
+ } catch (e) {
1073
+ }
1074
+ await this.reinitContext();
1075
+ }
1076
+ async reinitContext() {
1077
+ if (this.ctx) {
1078
+ try {
1079
+ await this.ctx.close();
1080
+ } catch (e) {
1081
+ }
1082
+ this.ctx = null;
1083
+ this.isInitialized = false;
1084
+ }
1085
+ this.init(true);
1086
+ try {
1087
+ const { audioInputManager: audioInputManager2 } = await import("./audioInputManager-BLDGGLAR.js");
1088
+ await audioInputManager2.reconnectStream();
1089
+ } catch (e) {
1090
+ }
1091
+ }
1092
+ activeNodes = [];
1093
+ activeConnections = [];
1094
+ lastPlayedMidiNote = null;
1095
+ updateActiveGraph(nodes, connections) {
1096
+ this.activeNodes = nodes;
1097
+ this.activeConnections = connections;
1098
+ this.graphNodeMap.clear();
1099
+ nodes.forEach((node) => {
1100
+ this.graphNodeMap.set(node.id, createAudioGraphNode(node));
1101
+ });
1102
+ const currentEffectIds = new Set(nodes.filter((n) => n.type === "effect").map((n) => n.id));
1103
+ for (const effId of audioEffectsEntity.getActiveEffectIds()) {
1104
+ if (!currentEffectIds.has(effId)) {
1105
+ audioEffectsEntity.removeProcessor(effId);
1106
+ }
1107
+ }
1108
+ }
1109
+ /**
1110
+ * Evaluates reachability from a given nodeId to an active, unmuted AudioOutputNode.
1111
+ * Traverses activeConnections, effect/routing chains, and module boundaries.
1112
+ */
1113
+ checkGraphReachabilityToOutput(nodeId) {
1114
+ const startNode = this.activeNodes.find((n) => n.id === nodeId);
1115
+ if (!startNode || startNode.active === false) return false;
1116
+ if (startNode.id === "output-strip-node" || startNode.type === "output_strip") {
1117
+ return true;
1118
+ }
1119
+ const outputStripNode = this.activeNodes.find(
1120
+ (n) => (n.id === "output-strip-node" || n.type === "output_strip") && n.active !== false
1121
+ );
1122
+ if (!outputStripNode) return false;
1123
+ const visited = /* @__PURE__ */ new Set([nodeId]);
1124
+ const queue = [nodeId];
1125
+ while (queue.length > 0) {
1126
+ const currentId = queue.shift();
1127
+ for (const conn of this.activeConnections) {
1128
+ if (conn.fromNodeId === currentId) {
1129
+ const nextId = conn.toNodeId;
1130
+ if (nextId === outputStripNode.id) {
1131
+ return true;
1132
+ }
1133
+ const nextNode = this.activeNodes.find((n) => n.id === nextId);
1134
+ if (nextNode && nextNode.active !== false && !visited.has(nextId)) {
1135
+ if (nextNode.id === "output-strip-node" || nextNode.type === "output_strip") {
1136
+ return true;
1137
+ }
1138
+ visited.add(nextId);
1139
+ queue.push(nextId);
1140
+ }
1141
+ }
1142
+ }
1143
+ }
1144
+ return false;
1145
+ }
1146
+ /**
1147
+ * Authority Check:
1148
+ * Queries the BaseAudioGraphNode instance. Structural rule enforces that no node
1149
+ * is allowed to emit sound to master unless authorized via an active connection to AudioOutputNode.
1150
+ */
1151
+ isNodeConnectedToOutput(nodeId) {
1152
+ let graphNode = this.graphNodeMap.get(nodeId);
1153
+ if (!graphNode) {
1154
+ const spaceNode = this.activeNodes.find((n) => n.id === nodeId);
1155
+ if (spaceNode) {
1156
+ graphNode = createAudioGraphNode(spaceNode);
1157
+ this.graphNodeMap.set(nodeId, graphNode);
1158
+ }
1159
+ }
1160
+ if (!graphNode) return false;
1161
+ return graphNode.isAuthorizedToEmit((id) => this.checkGraphReachabilityToOutput(id));
1162
+ }
1163
+ init(forceRecreate = false) {
1164
+ if (!forceRecreate && this.isInitialized && this.ctx) {
1165
+ if (this.ctx.state === "suspended") this.ctx.resume();
1166
+ return;
1167
+ }
1168
+ const AudioCtx = window.AudioContext || window.webkitAudioContext;
1169
+ const options = {};
1170
+ if (typeof this.audioEngineConfig.sampleRate === "number" && isFinite(this.audioEngineConfig.sampleRate) && this.audioEngineConfig.sampleRate > 0) {
1171
+ options.sampleRate = this.audioEngineConfig.sampleRate;
1172
+ }
1173
+ if (typeof this.audioEngineConfig.bufferSize === "number" && isFinite(this.audioEngineConfig.bufferSize) && this.audioEngineConfig.bufferSize > 0) {
1174
+ const targetRate = typeof this.audioEngineConfig.sampleRate === "number" && isFinite(this.audioEngineConfig.sampleRate) && this.audioEngineConfig.sampleRate > 0 ? this.audioEngineConfig.sampleRate : 44100;
1175
+ const hint = this.audioEngineConfig.bufferSize / targetRate;
1176
+ if (typeof hint === "number" && isFinite(hint) && hint > 0) {
1177
+ options.latencyHint = hint;
1178
+ }
1179
+ }
1180
+ try {
1181
+ this.ctx = new AudioCtx(options);
1182
+ } catch (err) {
1183
+ console.warn("Could not instantiate AudioContext with custom options, falling back to defaults:", err);
1184
+ this.ctx = new AudioCtx();
1185
+ }
1186
+ this.masterGain = this.ctx.createGain();
1187
+ const curTime = isFinite(this.ctx.currentTime) ? this.ctx.currentTime : 0;
1188
+ this.masterGain.gain.setValueAtTime(0.8, curTime);
1189
+ this.analyser = this.ctx.createAnalyser();
1190
+ this.analyser.fftSize = 256;
1191
+ this.masterSplitter = this.ctx.createChannelSplitter(2);
1192
+ this.analyserL = this.ctx.createAnalyser();
1193
+ this.analyserL.fftSize = 256;
1194
+ this.analyserR = this.ctx.createAnalyser();
1195
+ this.analyserR.fftSize = 256;
1196
+ this.delayNode = this.ctx.createDelay();
1197
+ this.delayNode.delayTime.value = 0.35;
1198
+ this.delayFeedback = this.ctx.createGain();
1199
+ this.delayFeedback.gain.value = 0.45;
1200
+ this.delayGain = this.ctx.createGain();
1201
+ this.delayGain.gain.value = 0.3;
1202
+ this.delayNode.connect(this.delayFeedback);
1203
+ this.delayFeedback.connect(this.delayNode);
1204
+ this.delayNode.connect(this.delayGain);
1205
+ this.delayGain.connect(this.masterGain);
1206
+ this.reverbNode = this.ctx.createConvolver();
1207
+ this.reverbNode.buffer = createCosmicImpulseBuffer(this.ctx, 2.5, 2);
1208
+ this.reverbGain = this.ctx.createGain();
1209
+ this.reverbGain.gain.value = 0.35;
1210
+ this.reverbNode.connect(this.reverbGain);
1211
+ this.reverbGain.connect(this.masterGain);
1212
+ this.masterGain.connect(this.analyser);
1213
+ this.masterGain.connect(this.masterSplitter);
1214
+ this.masterSplitter.connect(this.analyserL, 0);
1215
+ this.masterSplitter.connect(this.analyserR, 1);
1216
+ multiChannelOutputRouter.init(this.ctx, 6);
1217
+ multiChannelOutputRouter.connectInput(this.masterGain);
1218
+ this.isInitialized = true;
1219
+ }
1220
+ playAuditionTestPulse() {
1221
+ this.init();
1222
+ if (!this.ctx || !this.masterGain) return;
1223
+ const now = this.ctx.currentTime;
1224
+ const osc = this.ctx.createOscillator();
1225
+ const gain = this.ctx.createGain();
1226
+ osc.type = "sine";
1227
+ osc.frequency.setValueAtTime(440, now);
1228
+ osc.frequency.exponentialRampToValueAtTime(880, now + 0.15);
1229
+ gain.gain.setValueAtTime(1e-3, now);
1230
+ gain.gain.exponentialRampToValueAtTime(0.3, now + 0.02);
1231
+ gain.gain.exponentialRampToValueAtTime(1e-3, now + 0.25);
1232
+ osc.connect(gain);
1233
+ gain.connect(this.masterGain);
1234
+ osc.start(now);
1235
+ osc.stop(now + 0.26);
1236
+ }
1237
+ getAudioLevel() {
1238
+ if (!this.analyser) return 0;
1239
+ const binCount = this.analyser.frequencyBinCount || 128;
1240
+ if (this.analyserBuffer.length !== binCount) {
1241
+ this.analyserBuffer = new Uint8Array(binCount);
1242
+ }
1243
+ const data = this.analyserBuffer;
1244
+ this.analyser.getByteFrequencyData(data);
1245
+ let sum = 0;
1246
+ let max = 0;
1247
+ for (let i = 0; i < data.length; i++) {
1248
+ sum += data[i];
1249
+ if (data[i] > max) max = data[i];
1250
+ }
1251
+ const avg = sum / data.length;
1252
+ const peak = max / 255;
1253
+ const norm = avg / 128 * 0.4 + peak * 0.6;
1254
+ return Math.min(1, Math.max(0, norm));
1255
+ }
1256
+ getStereoLevels() {
1257
+ if (!this.analyserL || !this.analyserR) {
1258
+ if (this.analyser) {
1259
+ const lvl = this.getAudioLevel();
1260
+ return { left: lvl, right: lvl };
1261
+ }
1262
+ return { left: 0, right: 0 };
1263
+ }
1264
+ const binCountL = this.analyserL.frequencyBinCount || 128;
1265
+ const binCountR = this.analyserR.frequencyBinCount || 128;
1266
+ if (this.analyserBufferL.length !== binCountL) {
1267
+ this.analyserBufferL = new Uint8Array(binCountL);
1268
+ }
1269
+ if (this.analyserBufferR.length !== binCountR) {
1270
+ this.analyserBufferR = new Uint8Array(binCountR);
1271
+ }
1272
+ const dataL = this.analyserBufferL;
1273
+ const dataR = this.analyserBufferR;
1274
+ this.analyserL.getByteFrequencyData(dataL);
1275
+ this.analyserR.getByteFrequencyData(dataR);
1276
+ let sumL = 0;
1277
+ let maxL = 0;
1278
+ for (let i = 0; i < dataL.length; i++) {
1279
+ sumL += dataL[i];
1280
+ if (dataL[i] > maxL) maxL = dataL[i];
1281
+ }
1282
+ const avgL = sumL / dataL.length;
1283
+ let sumR = 0;
1284
+ let maxR = 0;
1285
+ for (let i = 0; i < dataR.length; i++) {
1286
+ sumR += dataR[i];
1287
+ if (dataR[i] > maxR) maxR = dataR[i];
1288
+ }
1289
+ const avgR = sumR / dataR.length;
1290
+ if (maxL < 2 && avgL < 0.5 && maxR < 2 && avgR < 0.5) {
1291
+ return { left: 0, right: 0 };
1292
+ }
1293
+ const peakL = maxL / 255;
1294
+ const peakR = maxR / 255;
1295
+ const normAvgL = avgL / 128;
1296
+ const normAvgR = avgR / 128;
1297
+ const leftVal = normAvgL * 0.35 + peakL * 0.65;
1298
+ const rightVal = normAvgR * 0.35 + peakR * 0.65;
1299
+ return {
1300
+ left: leftVal > 0.01 ? Math.min(1, leftVal) : 0,
1301
+ right: rightVal > 0.01 ? Math.min(1, rightVal) : 0
1302
+ };
1303
+ }
1304
+ isNodeActive(nodeId) {
1305
+ if (!this.ctx) return false;
1306
+ const until = this.nodeActiveUntilMap.get(nodeId) || 0;
1307
+ return this.ctx.currentTime < until;
1308
+ }
1309
+ markNodeActive(nodeId, durationSec = 0.5) {
1310
+ if (!this.ctx) this.init();
1311
+ const now = this.ctx?.currentTime || performance.now() / 1e3;
1312
+ this.nodeActiveUntilMap.set(nodeId, now + durationSec);
1313
+ }
1314
+ audioInputLevelProvider = null;
1315
+ registerAudioInputLevelProvider(provider) {
1316
+ this.audioInputLevelProvider = provider;
1317
+ }
1318
+ getWireSignalLevel(sourceNode, fromPort) {
1319
+ if (!sourceNode) return { left: 0, right: 0 };
1320
+ if (sourceNode.id === "input-strip-node" || sourceNode.type === "input_strip") {
1321
+ const inputs = sourceNode.mapIoConfig?.inputs || [];
1322
+ const matchedChannel = inputs.find(
1323
+ (c) => fromPort ? fromPort === `out-${c.id}` || fromPort === c.id : c.type === "audio"
1324
+ );
1325
+ const busId = matchedChannel?.sourceId || "bus-in-1";
1326
+ const lvl = this.audioInputLevelProvider ? this.audioInputLevelProvider(busId) : audioInputManager.getInputLevel(busId);
1327
+ return { left: lvl, right: matchedChannel?.audioFormat === "mono" ? 0 : lvl };
1328
+ }
1329
+ if (sourceNode.type === "output_strip") {
1330
+ return this.getStereoLevels();
1331
+ }
1332
+ if (this.isNodeActive(sourceNode.id)) {
1333
+ return this.getStereoLevels();
1334
+ }
1335
+ return { left: 0, right: 0 };
1336
+ }
1337
+ setOutputRouting(routingId) {
1338
+ this.init();
1339
+ multiChannelOutputRouter.applyRouting(routingId);
1340
+ }
1341
+ async setOutputDevice(deviceId) {
1342
+ this.init();
1343
+ return multiChannelOutputRouter.setOutputSinkId(deviceId);
1344
+ }
1345
+ setMasterVolume(vol) {
1346
+ if (this.masterGain && this.ctx) {
1347
+ const safeVol = typeof vol === "number" && isFinite(vol) ? Math.max(0, Math.min(1, vol)) : 0.8;
1348
+ const safeTime = isFinite(this.ctx.currentTime) ? this.ctx.currentTime : 0;
1349
+ this.masterGain.gain.setValueAtTime(safeVol, safeTime);
1350
+ }
1351
+ }
1352
+ getContext() {
1353
+ this.init();
1354
+ if (this.ctx?.state === "suspended") {
1355
+ this.ctx.resume().catch(() => {
1356
+ });
1357
+ }
1358
+ return this.ctx;
1359
+ }
1360
+ getMasterNode() {
1361
+ this.init();
1362
+ return this.masterGain;
1363
+ }
1364
+ getGainAndPanForNode(nodeId) {
1365
+ let volumeScale = 1;
1366
+ let panOffset = 0;
1367
+ const effectIds = [];
1368
+ const container = this.activeNodes.find((n) => n.id === nodeId);
1369
+ if (!container) return { volumeScale, panOffset, effectIds };
1370
+ const activeEffects = getActiveEffectsForNode(container, this.activeNodes, this.activeConnections);
1371
+ const gainEffects = activeEffects.filter((e) => e.effectType === "gain" && e.active !== false);
1372
+ for (const eff of gainEffects) {
1373
+ if (eff.gainConfig?.gain !== void 0) {
1374
+ volumeScale *= eff.gainConfig.gain;
1375
+ effectIds.push(eff.id);
1376
+ }
1377
+ }
1378
+ const panEffects = activeEffects.filter((e) => e.effectType === "pan" && e.active !== false);
1379
+ for (const eff of panEffects) {
1380
+ if (eff.panConfig?.pan !== void 0) {
1381
+ panOffset += eff.panConfig.pan;
1382
+ effectIds.push(eff.id);
1383
+ }
1384
+ }
1385
+ panOffset = Math.max(-1, Math.min(1, panOffset));
1386
+ return { volumeScale, panOffset, effectIds };
1387
+ }
1388
+ /**
1389
+ * Chains audio through all active DSP effect nodes (EQ, Compressor, Multiband, Limiter, Reverb, Delay, Saturation)
1390
+ * and fans out to any compressors/multiband compressors that have sidechain active with this node as source.
1391
+ */
1392
+ routeAudioThroughNodeEffects(audioNode, nodeId) {
1393
+ if (!this.ctx || !nodeId) return audioNode;
1394
+ const container = this.activeNodes.find((n) => n.id === nodeId);
1395
+ if (!container) return audioNode;
1396
+ const activeEffects = getActiveEffectsForNode(container, this.activeNodes, this.activeConnections);
1397
+ const dspEffects = activeEffects.filter(
1398
+ (e) => e.active !== false && ["eq", "compressor", "multiband_compressor", "limiter", "reverb", "delay", "saturation"].includes(
1399
+ e.effectType
1400
+ )
1401
+ );
1402
+ let currentOut = audioNode;
1403
+ for (const eff of dspEffects) {
1404
+ const proc = audioEffectsEntity.getOrCreateProcessor(this.ctx, eff);
1405
+ currentOut.connect(proc.input);
1406
+ currentOut = proc.output;
1407
+ this.nodeVisuals?.triggerNodeFlash(eff.id);
1408
+ }
1409
+ for (const node of this.activeNodes) {
1410
+ if (node.type === "effect") {
1411
+ const eff = node;
1412
+ if ((eff.effectType === "compressor" || eff.effectType === "multiband_compressor") && eff.active !== false) {
1413
+ const hasSidechainWire = this.activeConnections.some(
1414
+ (c) => c.fromNodeId === nodeId && c.toNodeId === eff.id && (c.toPort === "sidechain_in" || c.toSocketIndex === 1)
1415
+ );
1416
+ const hasConfigMatch = eff.compressorConfig?.sidechainEnabled && eff.compressorConfig?.sidechainSourceId === nodeId || eff.multibandCompressorConfig?.sidechainEnabled && eff.multibandCompressorConfig?.sidechainSourceId === nodeId;
1417
+ if (hasSidechainWire || hasConfigMatch) {
1418
+ const proc = audioEffectsEntity.getOrCreateProcessor(this.ctx, eff);
1419
+ if (proc.sidechainInput) {
1420
+ audioNode.connect(proc.sidechainInput);
1421
+ }
1422
+ }
1423
+ }
1424
+ }
1425
+ }
1426
+ return currentOut;
1427
+ }
1428
+ playNote(note, preset, startTimeOffset = 0, containerId) {
1429
+ this.init();
1430
+ if (!this.ctx || !this.masterGain) return;
1431
+ if (!containerId || !this.isNodeConnectedToOutput(containerId)) return;
1432
+ this.lastPlayedMidiNote = { note: note.note, velocity: note.velocity, timestamp: Date.now() };
1433
+ const triggeredNode = containerId ? this.activeNodes.find((n) => n.id === containerId) : void 0;
1434
+ if (triggeredNode && triggeredNode.type === "sampler") {
1435
+ this.playSamplerNote(note, triggeredNode, startTimeOffset);
1436
+ return;
1437
+ }
1438
+ const container = triggeredNode && triggeredNode.type === "container" ? triggeredNode : void 0;
1439
+ if (container && container.containerType === "instrument") {
1440
+ this.playInstrumentNote(note, container, startTimeOffset);
1441
+ return;
1442
+ }
1443
+ const now = this.ctx.currentTime + startTimeOffset;
1444
+ const freq = midiToFreq(note.note);
1445
+ const duration = Math.max(0.05, note.duration * 0.45);
1446
+ const att = Math.max(5e-3, preset.attack || 0.02);
1447
+ const dec = Math.max(0.05, preset.decay || 0.2);
1448
+ const sus = Math.max(0.01, preset.sustain || 0.5);
1449
+ const rel = Math.max(0.05, preset.release || 0.5);
1450
+ let volumeScale = 1;
1451
+ let panOffset = 0;
1452
+ if (containerId) {
1453
+ this.markNodeActive(containerId, duration + rel + 0.1);
1454
+ const recordEvent = this.nodeVisuals?.recordSocketSignalEvent;
1455
+ if (typeof recordEvent === "function") {
1456
+ recordEvent(containerId, "in_midi", { velocity: note.velocity, note: note.note });
1457
+ recordEvent(containerId, "out-audio", { velocity: note.velocity, note: note.note });
1458
+ recordEvent(containerId, "midi", { velocity: note.velocity, note: note.note });
1459
+ recordEvent(containerId, "audio", { velocity: note.velocity, note: note.note });
1460
+ }
1461
+ const g = this.getGainAndPanForNode(containerId);
1462
+ volumeScale = g.volumeScale;
1463
+ panOffset = g.panOffset;
1464
+ g.effectIds.forEach((id) => this.nodeVisuals?.triggerNodeFlash(id));
1465
+ }
1466
+ if (preset.synthType === "drums") {
1467
+ playSyntheticDrumVoice(this.ctx, this.masterGain, note.note, note.velocity * volumeScale, now);
1468
+ return;
1469
+ }
1470
+ const osc1 = this.ctx.createOscillator();
1471
+ const osc2 = this.ctx.createOscillator();
1472
+ const filter = this.ctx.createBiquadFilter();
1473
+ const voiceGain = this.ctx.createGain();
1474
+ const isPlucked = preset.synthType === "plucked";
1475
+ const isBass = preset.synthType === "bass";
1476
+ const isPad = preset.synthType === "pad";
1477
+ osc1.type = isBass ? "triangle" : isPad ? "sine" : isPlucked ? "triangle" : "sawtooth";
1478
+ osc2.type = isBass ? "sine" : isPad ? "triangle" : "square";
1479
+ osc1.frequency.setValueAtTime(freq, now);
1480
+ osc2.frequency.setValueAtTime(freq * 1.004, now);
1481
+ filter.type = "lowpass";
1482
+ filter.frequency.setValueAtTime(preset.cutoff || 2e3, now);
1483
+ filter.Q.setValueAtTime(preset.resonance || 2, now);
1484
+ const peakGain = note.velocity * 0.4 * volumeScale;
1485
+ voiceGain.gain.setValueAtTime(1e-4, now);
1486
+ voiceGain.gain.exponentialRampToValueAtTime(peakGain, now + att);
1487
+ voiceGain.gain.exponentialRampToValueAtTime(peakGain * sus, now + att + dec);
1488
+ voiceGain.gain.setValueAtTime(peakGain * sus, now + duration);
1489
+ voiceGain.gain.exponentialRampToValueAtTime(1e-4, now + duration + rel);
1490
+ osc1.connect(filter);
1491
+ osc2.connect(filter);
1492
+ filter.connect(voiceGain);
1493
+ const routedVoice = this.routeAudioThroughNodeEffects(voiceGain, containerId);
1494
+ if (panOffset !== 0 && this.ctx.createStereoPanner) {
1495
+ const panner = this.ctx.createStereoPanner();
1496
+ panner.pan.setValueAtTime(Math.max(-1, Math.min(1, panOffset)), now);
1497
+ routedVoice.connect(panner);
1498
+ panner.connect(this.masterGain);
1499
+ } else {
1500
+ routedVoice.connect(this.masterGain);
1501
+ }
1502
+ if (this.delayNode && preset.delay > 0.05) {
1503
+ const sendGain = this.ctx.createGain();
1504
+ sendGain.gain.value = preset.delay * 0.7;
1505
+ voiceGain.connect(sendGain);
1506
+ sendGain.connect(this.delayNode);
1507
+ }
1508
+ if (this.reverbNode && preset.reverb > 0.05) {
1509
+ const revSend = this.ctx.createGain();
1510
+ revSend.gain.value = preset.reverb * 0.8;
1511
+ voiceGain.connect(revSend);
1512
+ revSend.connect(this.reverbNode);
1513
+ }
1514
+ osc1.start(now);
1515
+ osc2.start(now);
1516
+ const stopTime = now + duration + rel + 0.1;
1517
+ osc1.stop(stopTime);
1518
+ osc2.stop(stopTime);
1519
+ }
1520
+ async playSamplerNote(note, container, startTimeOffset = 0) {
1521
+ if (!this.ctx || !this.masterGain) return;
1522
+ if (!this.isNodeConnectedToOutput(container.id)) return;
1523
+ const compConns = this.activeConnections.filter(
1524
+ (c) => c.toNodeId === container.id && (c.toPort === "component_in" || c.toPort === "store_in" || c.signalType === "audio_packet" || c.signalType === "component" || c.toSocketIndex === 0 || c.toSocketIndex === 2)
1525
+ );
1526
+ const connectedContainers = compConns.map((c) => this.activeNodes.find((n) => n.id === c.fromNodeId && n.type === "container" && n.containerType === "audio")).filter((n) => !!n);
1527
+ if (connectedContainers.length === 0) return;
1528
+ let targetTrack = void 0;
1529
+ let centerNote = 60;
1530
+ const zones = container.sampleZones || [];
1531
+ const matchingZone = zones.find(
1532
+ (z) => note.note >= z.minNote && note.note <= z.maxNote
1533
+ );
1534
+ if (matchingZone) {
1535
+ const matchingContainer = connectedContainers.find((c) => c.id === matchingZone.containerId);
1536
+ if (matchingContainer && matchingContainer.audioTrack) {
1537
+ targetTrack = matchingContainer.audioTrack;
1538
+ centerNote = matchingZone.centerNote ?? 60;
1539
+ } else if (matchingZone.sampleType) {
1540
+ targetTrack = {
1541
+ id: matchingZone.id,
1542
+ name: matchingZone.sampleName,
1543
+ sampleType: matchingZone.sampleType,
1544
+ durationBeats: 1,
1545
+ gain: 1,
1546
+ pan: 0,
1547
+ pitchShift: 0,
1548
+ filterCutoff: 2e4,
1549
+ fadeInBeats: 0,
1550
+ fadeOutBeats: 0,
1551
+ trimStartBeats: 0,
1552
+ trimEndBeats: 1,
1553
+ isReversed: false
1554
+ };
1555
+ centerNote = matchingZone.centerNote ?? 60;
1556
+ }
1557
+ } else {
1558
+ const fallbackContainer = connectedContainers[0];
1559
+ if (fallbackContainer && fallbackContainer.audioTrack) {
1560
+ targetTrack = fallbackContainer.audioTrack;
1561
+ centerNote = 60;
1562
+ }
1563
+ }
1564
+ if (!targetTrack) return;
1565
+ try {
1566
+ const { audioClipEngine } = await import("./audioClipEngine-BOT33JKI.js");
1567
+ const buffer = await audioClipEngine.getOrLoadBuffer(
1568
+ targetTrack.sampleType,
1569
+ targetTrack.customAudioUrl
1570
+ );
1571
+ if (!buffer) return;
1572
+ const now = this.ctx.currentTime + startTimeOffset;
1573
+ const source = this.ctx.createBufferSource();
1574
+ source.buffer = buffer;
1575
+ const startOffsetSec = Math.max(0, Math.min(buffer.duration, matchingZone?.startTime ?? 0));
1576
+ const endOffsetSec = Math.max(startOffsetSec + 0.01, Math.min(buffer.duration, matchingZone?.endTime ?? buffer.duration));
1577
+ const sampleDurationSec = endOffsetSec - startOffsetSec;
1578
+ const semitonesFromCenter = note.note - centerNote;
1579
+ const rate = Math.pow(2, semitonesFromCenter / 12);
1580
+ source.playbackRate.setValueAtTime(Math.max(0.05, Math.min(8, rate)), now);
1581
+ const playbackDurationSec = sampleDurationSec / rate;
1582
+ const noteCutoffSec = Math.min(playbackDurationSec, Math.max(0.1, (note.duration || 0.5) * 0.6));
1583
+ const { volumeScale, panOffset, effectIds } = this.getGainAndPanForNode(container.id);
1584
+ this.markNodeActive(container.id, noteCutoffSec + 0.1);
1585
+ effectIds.forEach((id) => this.nodeVisuals?.triggerNodeFlash(id));
1586
+ const voiceGain = this.ctx.createGain();
1587
+ const peakGain = (note.velocity || 0.85) * (targetTrack.gain || 1) * volumeScale * 0.75;
1588
+ voiceGain.gain.setValueAtTime(1e-3, now);
1589
+ voiceGain.gain.exponentialRampToValueAtTime(Math.max(1e-3, peakGain), now + 5e-3);
1590
+ voiceGain.gain.setValueAtTime(peakGain, now + Math.max(0.01, noteCutoffSec - 0.02));
1591
+ voiceGain.gain.exponentialRampToValueAtTime(1e-3, now + noteCutoffSec);
1592
+ source.connect(voiceGain);
1593
+ const routedSampler = this.routeAudioThroughNodeEffects(voiceGain, container.id);
1594
+ if (panOffset !== 0 && this.ctx.createStereoPanner) {
1595
+ const panner = this.ctx.createStereoPanner();
1596
+ panner.pan.setValueAtTime(Math.max(-1, Math.min(1, panOffset)), now);
1597
+ routedSampler.connect(panner);
1598
+ panner.connect(this.masterGain);
1599
+ } else {
1600
+ routedSampler.connect(this.masterGain);
1601
+ }
1602
+ source.start(now, startOffsetSec, sampleDurationSec);
1603
+ source.stop(now + noteCutoffSec);
1604
+ if (typeof window !== "undefined") {
1605
+ const event = new CustomEvent("sampler-note-triggered", {
1606
+ detail: {
1607
+ containerId: container.id,
1608
+ note: note.note,
1609
+ velocity: note.velocity || 0.85,
1610
+ duration: noteCutoffSec,
1611
+ // real playback time in seconds
1612
+ startTime: startOffsetSec,
1613
+ endTime: endOffsetSec,
1614
+ playbackRate: rate
1615
+ }
1616
+ });
1617
+ window.dispatchEvent(event);
1618
+ }
1619
+ } catch (err) {
1620
+ console.warn("Sampler playback failed:", err);
1621
+ }
1622
+ }
1623
+ playInstrumentNote(note, container, startTimeOffset = 0) {
1624
+ if (!this.ctx || !this.masterGain) return;
1625
+ if (!this.isNodeConnectedToOutput(container.id)) return;
1626
+ const now = this.ctx.currentTime + startTimeOffset;
1627
+ const freq = midiToFreq(note.note);
1628
+ const duration = Math.max(0.1, note.duration * 0.55);
1629
+ const velocity = note.velocity || 0.85;
1630
+ const pluginId = container.selectedPluginId || "grand_piano";
1631
+ const { volumeScale, panOffset, effectIds } = this.getGainAndPanForNode(container.id);
1632
+ this.markNodeActive(container.id, duration + 0.3);
1633
+ effectIds.forEach((id) => this.nodeVisuals?.triggerNodeFlash(id));
1634
+ const volumeCoeff = (container.volume !== void 0 ? container.volume : 1) * volumeScale;
1635
+ const connectToOutput = (vGainNode) => {
1636
+ const routedInstr = this.routeAudioThroughNodeEffects(vGainNode, container.id);
1637
+ if (panOffset !== 0 && this.ctx?.createStereoPanner) {
1638
+ const panner = this.ctx.createStereoPanner();
1639
+ panner.pan.setValueAtTime(Math.max(-1, Math.min(1, panOffset)), now);
1640
+ routedInstr.connect(panner);
1641
+ panner.connect(this.masterGain);
1642
+ } else {
1643
+ routedInstr.connect(this.masterGain);
1644
+ }
1645
+ };
1646
+ if (pluginId === "grand_piano") {
1647
+ const carrier = this.ctx.createOscillator();
1648
+ const overtone1 = this.ctx.createOscillator();
1649
+ const overtone2 = this.ctx.createOscillator();
1650
+ const vGain = this.ctx.createGain();
1651
+ carrier.type = "triangle";
1652
+ overtone1.type = "sine";
1653
+ overtone2.type = "sine";
1654
+ carrier.frequency.setValueAtTime(freq, now);
1655
+ overtone1.frequency.setValueAtTime(freq * 2, now);
1656
+ overtone2.frequency.setValueAtTime(freq * 3, now);
1657
+ const peak = velocity * volumeCoeff * 0.45;
1658
+ vGain.gain.setValueAtTime(1e-3, now);
1659
+ vGain.gain.linearRampToValueAtTime(peak, now + 4e-3);
1660
+ vGain.gain.exponentialRampToValueAtTime(peak * 0.4, now + 0.12);
1661
+ vGain.gain.exponentialRampToValueAtTime(peak * 0.05, now + duration);
1662
+ vGain.gain.exponentialRampToValueAtTime(1e-4, now + duration + 0.2);
1663
+ carrier.connect(vGain);
1664
+ overtone1.connect(vGain);
1665
+ overtone2.connect(vGain);
1666
+ connectToOutput(vGain);
1667
+ if (this.reverbNode) {
1668
+ const pianoHall = this.ctx.createGain();
1669
+ pianoHall.gain.value = 0.5;
1670
+ vGain.connect(pianoHall);
1671
+ pianoHall.connect(this.reverbNode);
1672
+ }
1673
+ carrier.start(now);
1674
+ overtone1.start(now);
1675
+ overtone2.start(now);
1676
+ carrier.stop(now + duration + 0.25);
1677
+ overtone1.stop(now + duration + 0.25);
1678
+ overtone2.stop(now + duration + 0.25);
1679
+ } else if (pluginId === "juno_pad") {
1680
+ const osc1 = this.ctx.createOscillator();
1681
+ const osc2 = this.ctx.createOscillator();
1682
+ const lfo = this.ctx.createOscillator();
1683
+ const lfoGain = this.ctx.createGain();
1684
+ const filter = this.ctx.createBiquadFilter();
1685
+ const vGain = this.ctx.createGain();
1686
+ osc1.type = "sawtooth";
1687
+ osc2.type = "sawtooth";
1688
+ lfo.type = "sine";
1689
+ osc1.frequency.setValueAtTime(freq * 0.996, now);
1690
+ osc2.frequency.setValueAtTime(freq * 1.004, now);
1691
+ lfo.frequency.setValueAtTime(4.8, now);
1692
+ lfoGain.gain.setValueAtTime(1.8, now);
1693
+ filter.type = "lowpass";
1694
+ filter.frequency.setValueAtTime(200, now);
1695
+ filter.frequency.exponentialRampToValueAtTime(1800, now + 0.8);
1696
+ filter.frequency.exponentialRampToValueAtTime(1200, now + duration);
1697
+ filter.Q.setValueAtTime(4, now);
1698
+ const peak = velocity * volumeCoeff * 0.25;
1699
+ vGain.gain.setValueAtTime(1e-3, now);
1700
+ vGain.gain.linearRampToValueAtTime(peak, now + 0.4);
1701
+ vGain.gain.setValueAtTime(peak, now + duration);
1702
+ vGain.gain.exponentialRampToValueAtTime(1e-3, now + duration + 1.2);
1703
+ lfo.connect(lfoGain);
1704
+ lfoGain.connect(osc1.frequency);
1705
+ lfoGain.connect(osc2.frequency);
1706
+ osc1.connect(filter);
1707
+ osc2.connect(filter);
1708
+ filter.connect(vGain);
1709
+ connectToOutput(vGain);
1710
+ if (this.reverbNode) {
1711
+ const revSend = this.ctx.createGain();
1712
+ revSend.gain.value = 0.65;
1713
+ vGain.connect(revSend);
1714
+ revSend.connect(this.reverbNode);
1715
+ }
1716
+ if (this.delayNode) {
1717
+ const delSend = this.ctx.createGain();
1718
+ delSend.gain.value = 0.45;
1719
+ vGain.connect(delSend);
1720
+ delSend.connect(this.delayNode);
1721
+ }
1722
+ lfo.start(now);
1723
+ osc1.start(now);
1724
+ osc2.start(now);
1725
+ lfo.stop(now + duration + 1.3);
1726
+ osc1.stop(now + duration + 1.3);
1727
+ osc2.stop(now + duration + 1.3);
1728
+ } else if (pluginId === "moog_bass") {
1729
+ const osc1 = this.ctx.createOscillator();
1730
+ const osc2 = this.ctx.createOscillator();
1731
+ const subOsc = this.ctx.createOscillator();
1732
+ const filter = this.ctx.createBiquadFilter();
1733
+ const vGain = this.ctx.createGain();
1734
+ osc1.type = "sawtooth";
1735
+ osc2.type = "sawtooth";
1736
+ subOsc.type = "square";
1737
+ osc1.frequency.setValueAtTime(freq * 0.991, now);
1738
+ osc2.frequency.setValueAtTime(freq * 1.009, now);
1739
+ subOsc.frequency.setValueAtTime(freq * 0.5, now);
1740
+ filter.type = "lowpass";
1741
+ filter.frequency.setValueAtTime(2800, now);
1742
+ filter.frequency.exponentialRampToValueAtTime(250, now + 0.14);
1743
+ filter.Q.setValueAtTime(6, now);
1744
+ const peak = velocity * volumeCoeff * 0.38;
1745
+ vGain.gain.setValueAtTime(1e-3, now);
1746
+ vGain.gain.linearRampToValueAtTime(peak, now + 5e-3);
1747
+ vGain.gain.exponentialRampToValueAtTime(peak * 0.6, now + 0.15);
1748
+ vGain.gain.setValueAtTime(peak * 0.6, now + duration);
1749
+ vGain.gain.exponentialRampToValueAtTime(1e-3, now + duration + 0.1);
1750
+ osc1.connect(filter);
1751
+ osc2.connect(filter);
1752
+ subOsc.connect(filter);
1753
+ filter.connect(vGain);
1754
+ connectToOutput(vGain);
1755
+ osc1.start(now);
1756
+ osc2.start(now);
1757
+ subOsc.start(now);
1758
+ osc1.stop(now + duration + 0.15);
1759
+ osc2.stop(now + duration + 0.15);
1760
+ subOsc.stop(now + duration + 0.15);
1761
+ } else if (pluginId === "dx7_tines") {
1762
+ const carrier = this.ctx.createOscillator();
1763
+ const modulator = this.ctx.createOscillator();
1764
+ const modGain = this.ctx.createGain();
1765
+ const vGain = this.ctx.createGain();
1766
+ carrier.type = "sine";
1767
+ modulator.type = "sine";
1768
+ carrier.frequency.setValueAtTime(freq, now);
1769
+ modulator.frequency.setValueAtTime(freq * 5, now);
1770
+ modGain.gain.setValueAtTime(2500, now);
1771
+ modGain.gain.exponentialRampToValueAtTime(10, now + 0.08);
1772
+ const peak = velocity * volumeCoeff * 0.45;
1773
+ vGain.gain.setValueAtTime(1e-3, now);
1774
+ vGain.gain.linearRampToValueAtTime(peak, now + 6e-3);
1775
+ vGain.gain.exponentialRampToValueAtTime(peak * 0.2, now + 0.6);
1776
+ vGain.gain.exponentialRampToValueAtTime(1e-3, now + duration + 0.15);
1777
+ modulator.connect(modGain);
1778
+ modGain.connect(carrier.frequency);
1779
+ carrier.connect(vGain);
1780
+ connectToOutput(vGain);
1781
+ if (this.reverbNode) {
1782
+ const revSend = this.ctx.createGain();
1783
+ revSend.gain.value = 0.45;
1784
+ vGain.connect(revSend);
1785
+ revSend.connect(this.reverbNode);
1786
+ }
1787
+ modulator.start(now);
1788
+ carrier.start(now);
1789
+ modulator.stop(now + duration + 0.2);
1790
+ carrier.stop(now + duration + 0.2);
1791
+ } else if (pluginId === "orchestral_strings") {
1792
+ const osc1 = this.ctx.createOscillator();
1793
+ const osc2 = this.ctx.createOscillator();
1794
+ const osc3 = this.ctx.createOscillator();
1795
+ const filter = this.ctx.createBiquadFilter();
1796
+ const lfo = this.ctx.createOscillator();
1797
+ const lfoGain = this.ctx.createGain();
1798
+ const vGain = this.ctx.createGain();
1799
+ osc1.type = "sawtooth";
1800
+ osc2.type = "sawtooth";
1801
+ osc3.type = "sawtooth";
1802
+ lfo.type = "sine";
1803
+ lfo.frequency.setValueAtTime(5.5, now);
1804
+ lfoGain.gain.setValueAtTime(2.2, now);
1805
+ osc1.frequency.setValueAtTime(freq * 0.995, now);
1806
+ osc2.frequency.setValueAtTime(freq * 1.005, now);
1807
+ osc3.frequency.setValueAtTime(freq * 2 * 1.001, now);
1808
+ filter.type = "lowpass";
1809
+ filter.frequency.setValueAtTime(1400, now);
1810
+ filter.Q.setValueAtTime(1, now);
1811
+ const peak = velocity * volumeCoeff * 0.22;
1812
+ vGain.gain.setValueAtTime(1e-3, now);
1813
+ vGain.gain.linearRampToValueAtTime(peak, now + 0.25);
1814
+ vGain.gain.setValueAtTime(peak, now + duration);
1815
+ vGain.gain.exponentialRampToValueAtTime(1e-3, now + duration + 0.7);
1816
+ lfo.connect(lfoGain);
1817
+ lfoGain.connect(osc1.frequency);
1818
+ lfoGain.connect(osc2.frequency);
1819
+ osc1.connect(filter);
1820
+ osc2.connect(filter);
1821
+ osc3.connect(filter);
1822
+ filter.connect(vGain);
1823
+ connectToOutput(vGain);
1824
+ if (this.reverbNode) {
1825
+ const stringsHall = this.ctx.createGain();
1826
+ stringsHall.gain.value = 0.6;
1827
+ vGain.connect(stringsHall);
1828
+ stringsHall.connect(this.reverbNode);
1829
+ }
1830
+ lfo.start(now);
1831
+ osc1.start(now);
1832
+ osc2.start(now);
1833
+ osc3.start(now);
1834
+ lfo.stop(now + duration + 0.85);
1835
+ osc1.stop(now + duration + 0.85);
1836
+ osc2.stop(now + duration + 0.85);
1837
+ osc3.stop(now + duration + 0.85);
1838
+ } else if (pluginId === "cyber_lead") {
1839
+ const osc1 = this.ctx.createOscillator();
1840
+ const osc2 = this.ctx.createOscillator();
1841
+ const filter = this.ctx.createBiquadFilter();
1842
+ const vGain = this.ctx.createGain();
1843
+ osc1.type = "sawtooth";
1844
+ osc2.type = "square";
1845
+ osc1.frequency.setValueAtTime(freq * 0.994, now);
1846
+ osc2.frequency.setValueAtTime(freq * 1.006, now);
1847
+ filter.type = "lowpass";
1848
+ filter.frequency.setValueAtTime(3200, now);
1849
+ filter.Q.setValueAtTime(4, now);
1850
+ const peak = velocity * volumeCoeff * 0.28;
1851
+ vGain.gain.setValueAtTime(1e-3, now);
1852
+ vGain.gain.linearRampToValueAtTime(peak, now + 0.02);
1853
+ vGain.gain.exponentialRampToValueAtTime(peak * 0.7, now + 0.25);
1854
+ vGain.gain.setValueAtTime(peak * 0.7, now + duration);
1855
+ vGain.gain.exponentialRampToValueAtTime(1e-3, now + duration + 0.35);
1856
+ osc1.connect(filter);
1857
+ osc2.connect(filter);
1858
+ filter.connect(vGain);
1859
+ connectToOutput(vGain);
1860
+ if (this.delayNode) {
1861
+ const delaySend = this.ctx.createGain();
1862
+ delaySend.gain.value = 0.55;
1863
+ vGain.connect(delaySend);
1864
+ delaySend.connect(this.delayNode);
1865
+ }
1866
+ osc1.start(now);
1867
+ osc2.start(now);
1868
+ osc1.stop(now + duration + 0.4);
1869
+ osc2.stop(now + duration + 0.4);
1870
+ }
1871
+ }
1872
+ };
1873
+ var synthEngine = new SynthEngine();
1874
+
1875
+ // src/engine/audio/devicePatterns.ts
1876
+ var KNOWN_PATTERNS = [
1877
+ {
1878
+ pattern: /scarlett\s*6i6/i,
1879
+ name: "Focusrite Scarlett 6i6",
1880
+ inChannels: 6,
1881
+ outChannels: 6,
1882
+ inNames: {
1883
+ 1: "Mic 1 / Line 1 (Front Combo)",
1884
+ 2: "Inst 2 / Line 2 (Front Combo)",
1885
+ 3: "Line 3 (Rear Balanced)",
1886
+ 4: "Line 4 (Rear Balanced)",
1887
+ 5: "S/PDIF 5 (Digital In L)",
1888
+ 6: "S/PDIF 6 (Digital In R)"
1889
+ },
1890
+ outNames: {
1891
+ "Ch 1 / 2 (Main L/R)": "Ch 1/2 (Main Monitor Out L/R)",
1892
+ "Ch 3 / 4 (Sub / Cue)": "Ch 3/4 (Headphone 1 / Line Out 3-4)",
1893
+ "Ch 5 / 6 (Aux Out)": "Ch 5/6 (Headphone 2 / S/PDIF Out 5-6)",
1894
+ "Ch 1 (Mono Left)": "Ch 1 (Direct Monitor Left)",
1895
+ "Ch 2 (Mono Right)": "Ch 2 (Direct Monitor Right)",
1896
+ "Ch 3 (Mono)": "Ch 3 (Line Out 3 Direct)",
1897
+ "Ch 4 (Mono)": "Ch 4 (Line Out 4 Direct)",
1898
+ "Ch 5 (Mono)": "Ch 5 (S/PDIF Out L)",
1899
+ "Ch 6 (Mono)": "Ch 6 (S/PDIF Out R)"
1900
+ }
1901
+ },
1902
+ {
1903
+ pattern: /scarlett\s*18i20/i,
1904
+ name: "Focusrite Scarlett 18i20",
1905
+ inChannels: 18,
1906
+ outChannels: 20
1907
+ },
1908
+ {
1909
+ pattern: /scarlett\s*18i8/i,
1910
+ name: "Focusrite Scarlett 18i8",
1911
+ inChannels: 18,
1912
+ outChannels: 8
1913
+ },
1914
+ {
1915
+ pattern: /scarlett\s*8i6/i,
1916
+ name: "Focusrite Scarlett 8i6",
1917
+ inChannels: 8,
1918
+ outChannels: 6
1919
+ },
1920
+ {
1921
+ pattern: /scarlett\s*4i4/i,
1922
+ name: "Focusrite Scarlett 4i4",
1923
+ inChannels: 4,
1924
+ outChannels: 4
1925
+ },
1926
+ {
1927
+ pattern: /scarlett\s*2i4/i,
1928
+ name: "Focusrite Scarlett 2i4",
1929
+ inChannels: 2,
1930
+ outChannels: 4
1931
+ },
1932
+ {
1933
+ pattern: /scarlett\s*(2i2|solo)/i,
1934
+ name: "Focusrite Scarlett 2i2",
1935
+ inChannels: 2,
1936
+ outChannels: 2
1937
+ },
1938
+ {
1939
+ pattern: /umc\s*1820/i,
1940
+ name: "Behringer U-Phoria UMC1820",
1941
+ inChannels: 18,
1942
+ outChannels: 20
1943
+ },
1944
+ {
1945
+ pattern: /umc\s*404(hd)?/i,
1946
+ name: "Behringer U-Phoria UMC404HD",
1947
+ inChannels: 4,
1948
+ outChannels: 4
1949
+ },
1950
+ {
1951
+ pattern: /umc\s*204(hd)?/i,
1952
+ name: "Behringer U-Phoria UMC204HD",
1953
+ inChannels: 2,
1954
+ outChannels: 4
1955
+ },
1956
+ {
1957
+ pattern: /apollo\s*(twin|x4|x8|x16)/i,
1958
+ name: "Universal Audio Apollo",
1959
+ inChannels: 10,
1960
+ outChannels: 6
1961
+ },
1962
+ {
1963
+ pattern: /motu\s*(m4|m2|ultralite)/i,
1964
+ name: "MOTU Audio Interface",
1965
+ inChannels: 4,
1966
+ outChannels: 4
1967
+ },
1968
+ {
1969
+ pattern: /komplete\s*audio\s*6/i,
1970
+ name: "Native Instruments Komplete Audio 6",
1971
+ inChannels: 6,
1972
+ outChannels: 6
1973
+ }
1974
+ ];
1975
+
1976
+ // src/engine/audio/deviceProfiles.ts
1977
+ function detectHardwareProfile(inputLabel, outputLabel, trackSettings, trackCapabilities, destMaxChannels) {
1978
+ const combinedLabel = `${inputLabel || "} ${outputLabel || "}`;
1979
+ let matchedPattern = null;
1980
+ for (const kp of KNOWN_PATTERNS) {
1981
+ if (kp.pattern.test(combinedLabel)) {
1982
+ matchedPattern = kp;
1983
+ break;
1984
+ }
1985
+ }
1986
+ const trackChannelCount = trackSettings?.channelCount || trackCapabilities?.channelCount?.max;
1987
+ let inCount = matchedPattern ? matchedPattern.inChannels : trackChannelCount && trackChannelCount > 1 ? trackChannelCount : 2;
1988
+ let outCount = matchedPattern ? matchedPattern.outChannels : destMaxChannels && destMaxChannels > 1 ? destMaxChannels : 2;
1989
+ if (!matchedPattern) {
1990
+ const channelMatch = combinedLabel.match(/(\d+)\s*(?:in|ch|channel|i)/i);
1991
+ if (channelMatch && channelMatch[1]) {
1992
+ const parsed = parseInt(channelMatch[1], 10);
1993
+ if (!isNaN(parsed) && parsed >= 2 && parsed <= 64) {
1994
+ inCount = Math.max(inCount, parsed);
1995
+ }
1996
+ }
1997
+ }
1998
+ return {
1999
+ detectedInputChannels: Math.max(2, inCount),
2000
+ detectedOutputChannels: Math.max(2, outCount),
2001
+ interfaceModelName: matchedPattern ? matchedPattern.name : inputLabel || "Standard Audio Interface",
2002
+ isKnownInterface: !!matchedPattern,
2003
+ channelDescriptions: matchedPattern?.inNames || {},
2004
+ outputDescriptions: matchedPattern?.outNames || {}
2005
+ };
2006
+ }
2007
+ function buildInputChannelList(totalChannels, descriptions = {}) {
2008
+ const count = Math.max(2, totalChannels);
2009
+ const result = [];
2010
+ for (let ch = 1; ch <= count; ch++) {
2011
+ const customDesc = descriptions[ch];
2012
+ const defaultRole = ch === 1 ? "Mic 1 / Line In 1" : ch === 2 ? "Inst 2 / Line In 2" : `Line In ${ch}`;
2013
+ const label = customDesc || `Input ${ch} (${defaultRole} - Mono)`;
2014
+ result.push({
2015
+ id: ch,
2016
+ label,
2017
+ shortLabel: `Ch ${ch}`,
2018
+ isStereo: false,
2019
+ channels: [ch]
2020
+ });
2021
+ }
2022
+ result.push({
2023
+ id: 0,
2024
+ label: `Stereo Pair 1+2 (${descriptions[1] ? "Main L/R Stereo" : "In 1/2 Stereo"})`,
2025
+ shortLabel: "Ch 1/2 Stereo",
2026
+ isStereo: true,
2027
+ channels: [1, 2]
2028
+ });
2029
+ for (let pairStart = 3; pairStart < count; pairStart += 2) {
2030
+ const pairEnd = pairStart + 1;
2031
+ const pairId = pairStart * 100 + pairEnd;
2032
+ result.push({
2033
+ id: pairId,
2034
+ label: `Stereo Pair ${pairStart}+${pairEnd} (In ${pairStart}/${pairEnd})`,
2035
+ shortLabel: `Ch ${pairStart}/${pairEnd}`,
2036
+ isStereo: true,
2037
+ channels: [pairStart, pairEnd]
2038
+ });
2039
+ }
2040
+ return result;
2041
+ }
2042
+ function buildOutputChannelList(totalChannels, descriptions = {}) {
2043
+ const count = Math.max(2, totalChannels);
2044
+ const list = [];
2045
+ list.push({
2046
+ id: "Ch 1 / 2 (Main L/R)",
2047
+ label: descriptions["Ch 1 / 2 (Main L/R)"] || "Ch 1/2 (Main Stereo Master Out)",
2048
+ shortLabel: "Ch 1/2 Main",
2049
+ isStereo: true,
2050
+ channels: [1, 2]
2051
+ });
2052
+ if (count >= 4) {
2053
+ list.push({
2054
+ id: "Ch 3 / 4 (Sub / Cue)",
2055
+ label: descriptions["Ch 3 / 4 (Sub / Cue)"] || "Ch 3/4 (Sub / Cue / Line Out 3-4)",
2056
+ shortLabel: "Ch 3/4 Cue",
2057
+ isStereo: true,
2058
+ channels: [3, 4]
2059
+ });
2060
+ }
2061
+ if (count >= 6) {
2062
+ list.push({
2063
+ id: "Ch 5 / 6 (Aux Out)",
2064
+ label: descriptions["Ch 5 / 6 (Aux Out)"] || "Ch 5/6 (Aux / Headphone 2 / S/PDIF Out)",
2065
+ shortLabel: "Ch 5/6 Aux",
2066
+ isStereo: true,
2067
+ channels: [5, 6]
2068
+ });
2069
+ }
2070
+ if (count >= 8) {
2071
+ list.push({
2072
+ id: "Ch 7 / 8 (Out 7/8)",
2073
+ label: descriptions["Ch 7 / 8 (Out 7/8)"] || "Ch 7/8 (Line 7/8 Direct Out)",
2074
+ shortLabel: "Ch 7/8 Out",
2075
+ isStereo: true,
2076
+ channels: [7, 8]
2077
+ });
2078
+ }
2079
+ for (let ch = 1; ch <= count; ch++) {
2080
+ const monoKey = `Ch ${ch} (Mono${ch === 1 ? " Left" : ch === 2 ? " Right" : ""})`;
2081
+ list.push({
2082
+ id: monoKey,
2083
+ label: descriptions[monoKey] || `Ch ${ch} (Mono ${ch === 1 ? "Left" : ch === 2 ? "Right" : `Direct Out ${ch}`})`,
2084
+ shortLabel: `Ch ${ch} Mono`,
2085
+ isStereo: false,
2086
+ channels: [ch]
2087
+ });
2088
+ }
2089
+ return list;
2090
+ }
2091
+
2092
+ // src/engine/audio/audioInputManager/audioInputManager.ts
2093
+ import { topLevelIoRegistry } from "@ultrst-armada/kloks/state-graph";
2094
+ import {
2095
+ loadStoredConfig,
2096
+ saveStoredConfig,
2097
+ enumerateSystemAudioDevices
2098
+ } from "@ultrst-armada/kloks/state-graph";
2099
+ import { acquireAudioInputStream, stopAudioMediaStream } from "@ultrst-armada/kloks/state-graph";
2100
+ import { AudioBusDriver } from "@ultrst-armada/kloks/state-graph";
2101
+ var DEFAULT_NAMED_INPUTS = DEFAULT_NAMED_BUSSES;
2102
+ var AudioInputManager = class {
2103
+ audioCtx = null;
2104
+ mediaStream = null;
2105
+ sourceNode = null;
2106
+ busDriver = new AudioBusDriver();
2107
+ isListening = false;
2108
+ listeners = /* @__PURE__ */ new Set();
2109
+ namedInputs = [...DEFAULT_NAMED_BUSSES];
2110
+ selectedDeviceId = "default";
2111
+ selectedOutputDeviceId = "default";
2112
+ hardwareChannelCount = 6;
2113
+ detectedTrackChannelCount = 2;
2114
+ isHardwareAutoDetected = false;
2115
+ detectedInterfaceName = "";
2116
+ activeProfile = null;
2117
+ availableDevices = [];
2118
+ availableOutputDevices = [];
2119
+ lastError = null;
2120
+ constructor() {
2121
+ this.loadConfig();
2122
+ this.initDeviceChangeListener();
2123
+ this.enumerateAudioDevices().catch((err) => {
2124
+ console.warn("Initial enumerateAudioDevices failed:", err);
2125
+ });
2126
+ this.registerWithSynthEngine();
2127
+ }
2128
+ registerWithSynthEngine() {
2129
+ queueMicrotask(() => {
2130
+ try {
2131
+ if (typeof synthEngine !== "undefined" && synthEngine?.registerAudioInputLevelProvider) {
2132
+ synthEngine.registerAudioInputLevelProvider((busId) => this.getInputLevel(busId));
2133
+ } else {
2134
+ setTimeout(() => {
2135
+ if (typeof synthEngine !== "undefined" && synthEngine?.registerAudioInputLevelProvider) {
2136
+ synthEngine.registerAudioInputLevelProvider((busId) => this.getInputLevel(busId));
2137
+ }
2138
+ }, 0);
2139
+ }
2140
+ } catch (e) {
2141
+ setTimeout(() => {
2142
+ try {
2143
+ if (typeof synthEngine !== "undefined" && synthEngine?.registerAudioInputLevelProvider) {
2144
+ synthEngine.registerAudioInputLevelProvider((busId) => this.getInputLevel(busId));
2145
+ }
2146
+ } catch (err) {
2147
+ }
2148
+ }, 0);
2149
+ }
2150
+ });
2151
+ }
2152
+ initDeviceChangeListener() {
2153
+ if (typeof navigator !== "undefined" && navigator.mediaDevices?.addEventListener) {
2154
+ navigator.mediaDevices.addEventListener("devicechange", async () => {
2155
+ await this.enumerateAudioDevices();
2156
+ this.notifyListeners();
2157
+ });
2158
+ }
2159
+ }
2160
+ subscribe(listener) {
2161
+ this.listeners.add(listener);
2162
+ return () => {
2163
+ this.listeners.delete(listener);
2164
+ };
2165
+ }
2166
+ notifyListeners() {
2167
+ this.listeners.forEach((fn) => {
2168
+ try {
2169
+ fn();
2170
+ } catch (err) {
2171
+ console.error("Error in audioInputManager listener:", err);
2172
+ }
2173
+ });
2174
+ }
2175
+ isConnected() {
2176
+ if (!this.isListening || !this.mediaStream) return false;
2177
+ const tracks = this.mediaStream.getAudioTracks();
2178
+ return tracks.length > 0 && tracks.some((t) => t.readyState === "live");
2179
+ }
2180
+ get isAudioActive() {
2181
+ return this.isConnected();
2182
+ }
2183
+ loadConfig() {
2184
+ const config = loadStoredConfig(this.namedInputs, this.selectedDeviceId, this.selectedOutputDeviceId);
2185
+ this.namedInputs = config.namedAudioInputs;
2186
+ this.selectedDeviceId = config.selectedAudioDeviceId;
2187
+ this.selectedOutputDeviceId = config.selectedAudioOutputDeviceId;
2188
+ return config;
2189
+ }
2190
+ saveConfig(config) {
2191
+ if (config?.namedAudioInputs !== void 0) this.namedInputs = config.namedAudioInputs;
2192
+ if (config?.selectedAudioDeviceId !== void 0) this.selectedDeviceId = config.selectedAudioDeviceId;
2193
+ if (config?.selectedAudioOutputDeviceId !== void 0) this.selectedOutputDeviceId = config.selectedAudioOutputDeviceId;
2194
+ saveStoredConfig({
2195
+ selectedAudioDeviceId: this.selectedDeviceId,
2196
+ selectedAudioOutputDeviceId: this.selectedOutputDeviceId,
2197
+ namedAudioInputs: this.namedInputs,
2198
+ selectedMidiDeviceId: null,
2199
+ midiConfigs: {}
2200
+ });
2201
+ }
2202
+ async enumerateAudioDevices() {
2203
+ const res = await enumerateSystemAudioDevices();
2204
+ this.availableDevices = res.inputs;
2205
+ this.availableOutputDevices = res.outputs;
2206
+ this.refreshHardwareProfile();
2207
+ return res;
2208
+ }
2209
+ refreshHardwareProfile() {
2210
+ const inputDev = this.availableDevices.find((d) => d.deviceId === this.selectedDeviceId);
2211
+ const outputDev = this.availableOutputDevices.find((d) => d.deviceId === this.selectedOutputDeviceId);
2212
+ let trackSettings;
2213
+ let trackCaps;
2214
+ if (this.mediaStream) {
2215
+ const trk = this.mediaStream.getAudioTracks()[0];
2216
+ if (trk) {
2217
+ trackSettings = trk.getSettings();
2218
+ trackCaps = trk.getCapabilities ? trk.getCapabilities() : void 0;
2219
+ }
2220
+ }
2221
+ const ctx = synthEngine.getContext();
2222
+ const rawDestMax = ctx?.destination?.maxChannelCount;
2223
+ const destMax = typeof rawDestMax === "number" && isFinite(rawDestMax) ? rawDestMax : 2;
2224
+ const profile = detectHardwareProfile(
2225
+ inputDev?.label || "",
2226
+ outputDev?.label || "",
2227
+ trackSettings,
2228
+ trackCaps,
2229
+ destMax
2230
+ );
2231
+ this.activeProfile = profile;
2232
+ this.detectedInterfaceName = profile.interfaceModelName;
2233
+ if (profile.isKnownInterface || trackSettings?.channelCount && trackSettings.channelCount > 2) {
2234
+ this.hardwareChannelCount = profile.detectedInputChannels;
2235
+ this.isHardwareAutoDetected = true;
2236
+ multiChannelOutputRouter.setMaxChannels(profile.detectedOutputChannels);
2237
+ }
2238
+ }
2239
+ async selectNativeAudioOutput() {
2240
+ const nav = navigator;
2241
+ if (nav.mediaDevices && typeof nav.mediaDevices.selectAudioOutput === "function") {
2242
+ try {
2243
+ const device = await nav.mediaDevices.selectAudioOutput();
2244
+ if (device && device.deviceId) {
2245
+ await this.setOutputDevice(device.deviceId);
2246
+ await this.enumerateAudioDevices();
2247
+ this.notifyListeners();
2248
+ return true;
2249
+ }
2250
+ } catch (err) {
2251
+ console.warn("selectAudioOutput was cancelled or failed:", err);
2252
+ }
2253
+ }
2254
+ return false;
2255
+ }
2256
+ getLastError() {
2257
+ return this.lastError;
2258
+ }
2259
+ clearError() {
2260
+ this.lastError = null;
2261
+ this.notifyListeners();
2262
+ }
2263
+ async initAudioStream(deviceId) {
2264
+ const targetDevId = deviceId || this.selectedDeviceId;
2265
+ stopAudioMediaStream(this.mediaStream);
2266
+ try {
2267
+ this.lastError = null;
2268
+ const acquired = await acquireAudioInputStream(targetDevId);
2269
+ if (!acquired) {
2270
+ this.lastError = "Audio hardware stream not available.";
2271
+ this.notifyListeners();
2272
+ return false;
2273
+ }
2274
+ this.mediaStream = acquired.mediaStream;
2275
+ this.detectedTrackChannelCount = acquired.detectedTrackChannelCount;
2276
+ this.selectedDeviceId = targetDevId;
2277
+ this.saveConfig({ selectedAudioDeviceId: targetDevId });
2278
+ this.audioCtx = synthEngine.getContext();
2279
+ if (this.audioCtx.state === "suspended") {
2280
+ await this.audioCtx.resume();
2281
+ }
2282
+ this.sourceNode = this.audioCtx.createMediaStreamSource(this.mediaStream);
2283
+ await this.enumerateAudioDevices();
2284
+ this.busDriver.setupSplitter(this.audioCtx, this.sourceNode, this.hardwareChannelCount);
2285
+ this.busDriver.setupNamedAnalysers(this.audioCtx, this.namedInputs);
2286
+ this.isListening = true;
2287
+ this.syncEndpointsToRegistry();
2288
+ topLevelIoRegistry.logTrace("AUDIO", "DEVICE_DETECTED", {
2289
+ deviceId: this.selectedDeviceId,
2290
+ label: this.getActiveDeviceLabel()
2291
+ });
2292
+ topLevelIoRegistry.logTrace("AUDIO", "AUDIO_STREAM_OPEN", {
2293
+ channelCount: this.hardwareChannelCount,
2294
+ streamId: this.mediaStream.id
2295
+ });
2296
+ this.lastError = null;
2297
+ this.notifyListeners();
2298
+ return true;
2299
+ } catch (err) {
2300
+ const errStr = String(err?.message || err?.name || err || "").toLowerCase();
2301
+ if (err?.name === "NotAllowedError" || err?.name === "SecurityError" || errStr.includes("permission denied") || errStr.includes("not allowed") || errStr.includes("blocked")) {
2302
+ this.lastError = "Hardware access requires allow='microphone; midi' or new tab";
2303
+ } else {
2304
+ this.lastError = `Audio stream init failed: ${err?.message || "Permission denied"}`;
2305
+ }
2306
+ console.warn("Audio hardware stream init failed:", this.lastError);
2307
+ this.notifyListeners();
2308
+ return false;
2309
+ }
2310
+ }
2311
+ async reconnectStream() {
2312
+ if (this.mediaStream && this.isListening) {
2313
+ try {
2314
+ if (this.sourceNode) {
2315
+ try {
2316
+ this.sourceNode.disconnect();
2317
+ } catch (e) {
2318
+ }
2319
+ }
2320
+ this.audioCtx = synthEngine.getContext();
2321
+ if (this.audioCtx.state === "suspended") {
2322
+ await this.audioCtx.resume();
2323
+ }
2324
+ this.sourceNode = this.audioCtx.createMediaStreamSource(this.mediaStream);
2325
+ this.busDriver.setupSplitter(this.audioCtx, this.sourceNode, this.hardwareChannelCount);
2326
+ this.busDriver.setupNamedAnalysers(this.audioCtx, this.namedInputs);
2327
+ } catch (err) {
2328
+ console.warn("Error reconnecting stream to new AudioContext:", err);
2329
+ }
2330
+ }
2331
+ }
2332
+ async setOutputDevice(deviceId) {
2333
+ this.selectedOutputDeviceId = deviceId;
2334
+ this.saveConfig({ selectedAudioOutputDeviceId: deviceId });
2335
+ await synthEngine.setOutputDevice(deviceId);
2336
+ this.refreshHardwareProfile();
2337
+ this.notifyListeners();
2338
+ return true;
2339
+ }
2340
+ getAvailableInputChannels() {
2341
+ return buildInputChannelList(this.hardwareChannelCount, this.activeProfile?.channelDescriptions);
2342
+ }
2343
+ getAvailableOutputChannels() {
2344
+ const outChannels = this.activeProfile?.detectedOutputChannels || this.hardwareChannelCount;
2345
+ return buildOutputChannelList(outChannels, this.activeProfile?.outputDescriptions);
2346
+ }
2347
+ getActiveDeviceLabel() {
2348
+ const dev = this.availableDevices.find((d) => d.deviceId === this.selectedDeviceId);
2349
+ return dev?.label || "Default System Audio Interface";
2350
+ }
2351
+ getActiveOutputDeviceLabel() {
2352
+ const dev = this.availableOutputDevices.find((d) => d.deviceId === this.selectedOutputDeviceId);
2353
+ return dev?.label || "Default System Audio Output";
2354
+ }
2355
+ getChannelLevel(channel) {
2356
+ if (channel === 0) {
2357
+ return Math.max(this.busDriver.getChannelLevel(1), this.busDriver.getChannelLevel(2));
2358
+ }
2359
+ if (channel > 100) {
2360
+ const ch1 = Math.floor(channel / 100);
2361
+ const ch2 = channel % 100;
2362
+ return Math.max(this.busDriver.getChannelLevel(ch1), this.busDriver.getChannelLevel(ch2));
2363
+ }
2364
+ return this.busDriver.getChannelLevel(channel);
2365
+ }
2366
+ syncEndpointsToRegistry() {
2367
+ const isLive = this.isConnected();
2368
+ this.namedInputs.forEach((bus) => {
2369
+ if (bus.busType === "input") {
2370
+ const ep = topLevelIoRegistry.getOrCreateAudioEndpoint(bus.id, bus.name, bus.channel ?? 1);
2371
+ ep.gain = bus.gain ?? 1;
2372
+ ep.isMuted = !!bus.isMuted;
2373
+ ep.analyser = this.busDriver.namedAnalyserMap.get(bus.id) || null;
2374
+ ep.audioNode = this.busDriver.namedGainMap.get(bus.id) || null;
2375
+ ep.setConnected(isLive);
2376
+ }
2377
+ });
2378
+ }
2379
+ getBusLevel(busId) {
2380
+ return this.getInputLevel(busId);
2381
+ }
2382
+ getInputLevel(inputId) {
2383
+ const bus = this.namedInputs.find((b) => b.id === inputId);
2384
+ if (!bus || bus.isMuted) return 0;
2385
+ const ep = topLevelIoRegistry.getAudioEndpoint(inputId);
2386
+ if (ep) {
2387
+ ep.gain = bus.gain ?? 1;
2388
+ ep.isMuted = !!bus.isMuted;
2389
+ if (!ep.analyser) {
2390
+ ep.analyser = this.busDriver.namedAnalyserMap.get(inputId) || null;
2391
+ }
2392
+ if (!ep.audioNode) {
2393
+ ep.audioNode = this.busDriver.namedGainMap.get(inputId) || null;
2394
+ }
2395
+ ep.sampleFromAnalyser();
2396
+ if (ep.level > 5e-3) {
2397
+ return Math.min(1, ep.level * (bus.gain ?? 1));
2398
+ }
2399
+ return 0;
2400
+ }
2401
+ const channelNum = bus.channel ?? 1;
2402
+ const rawLevel = this.busDriver.getNamedBusLevel(inputId, channelNum);
2403
+ if (rawLevel > 5e-3) {
2404
+ return Math.min(1, rawLevel * (bus.gain ?? 1));
2405
+ }
2406
+ return 0;
2407
+ }
2408
+ setNamedInputs(inputs) {
2409
+ this.namedInputs = [...inputs];
2410
+ this.saveConfig();
2411
+ if (this.audioCtx) this.busDriver.setupNamedAnalysers(this.audioCtx, this.namedInputs);
2412
+ this.syncEndpointsToRegistry();
2413
+ this.notifyListeners();
2414
+ }
2415
+ addNamedBus(bus) {
2416
+ this.namedInputs = [...this.namedInputs, bus];
2417
+ this.saveConfig();
2418
+ if (bus.busType === "input" && this.audioCtx) {
2419
+ this.busDriver.setupNamedAnalysers(this.audioCtx, this.namedInputs);
2420
+ }
2421
+ this.syncEndpointsToRegistry();
2422
+ this.notifyListeners();
2423
+ }
2424
+ addNamedInput(input) {
2425
+ this.addNamedBus(input);
2426
+ }
2427
+ addInputBus() {
2428
+ const existingInputs = this.namedInputs.filter((b) => b.busType === "input");
2429
+ const nextIdx = existingInputs.length + 1;
2430
+ const availChannels = this.getAvailableInputChannels();
2431
+ const defaultCh = availChannels[(nextIdx - 1) % availChannels.length]?.id ?? 1;
2432
+ const colors = ["#f59e0b", "#06b6d4", "#10b981", "#ec4899", "#8b5cf6", "#3b82f6"];
2433
+ const newBus = {
2434
+ id: `bus-in-${Date.now()}-${Math.random().toString(36).substr(2, 4)}`,
2435
+ busType: "input",
2436
+ deviceId: this.selectedDeviceId,
2437
+ name: `Input Bus ${nextIdx}`,
2438
+ channel: defaultCh,
2439
+ gain: 1,
2440
+ isMuted: false,
2441
+ color: colors[(nextIdx - 1) % colors.length]
2442
+ };
2443
+ this.addNamedBus(newBus);
2444
+ return newBus;
2445
+ }
2446
+ addOutputBus() {
2447
+ const existingOutputs = this.namedInputs.filter((b) => b.busType === "output");
2448
+ const nextIdx = existingOutputs.length + 1;
2449
+ const availChannels = this.getAvailableOutputChannels();
2450
+ const defaultOut = availChannels[(nextIdx - 1) % availChannels.length]?.id ?? "Ch 1 / 2 (Main L/R)";
2451
+ const colors = ["#10b981", "#3b82f6", "#8b5cf6", "#f59e0b", "#ec4899", "#06b6d4"];
2452
+ const newBus = {
2453
+ id: `bus-out-${Date.now()}-${Math.random().toString(36).substr(2, 4)}`,
2454
+ busType: "output",
2455
+ deviceId: this.selectedOutputDeviceId,
2456
+ name: `Output Bus ${nextIdx}`,
2457
+ outputChannels: defaultOut,
2458
+ gain: 1,
2459
+ isMuted: false,
2460
+ color: colors[(nextIdx - 1) % colors.length]
2461
+ };
2462
+ this.addNamedBus(newBus);
2463
+ return newBus;
2464
+ }
2465
+ updateNamedInput(updated) {
2466
+ this.namedInputs = this.namedInputs.map((i) => i.id === updated.id ? { ...updated } : i);
2467
+ const gainNode = this.busDriver.namedGainMap.get(updated.id);
2468
+ if (gainNode) {
2469
+ gainNode.gain.value = updated.isMuted ? 0 : updated.gain;
2470
+ }
2471
+ this.saveConfig();
2472
+ this.notifyListeners();
2473
+ }
2474
+ deleteNamedInput(id) {
2475
+ this.namedInputs = this.namedInputs.filter((i) => i.id !== id);
2476
+ this.saveConfig();
2477
+ if (this.audioCtx) this.busDriver.setupNamedAnalysers(this.audioCtx, this.namedInputs);
2478
+ this.notifyListeners();
2479
+ }
2480
+ updateMonitoringConnections(nodes, connections) {
2481
+ if (!this.audioCtx) return;
2482
+ this.busDriver.updateMonitoring(
2483
+ this.audioCtx,
2484
+ synthEngine.getMasterNode(),
2485
+ nodes,
2486
+ connections,
2487
+ (id) => synthEngine.isNodeConnectedToOutput(id)
2488
+ );
2489
+ }
2490
+ stopStream() {
2491
+ stopAudioMediaStream(this.mediaStream);
2492
+ this.mediaStream = null;
2493
+ this.isListening = false;
2494
+ this.lastError = null;
2495
+ this.notifyListeners();
2496
+ }
2497
+ };
2498
+ var audioInputManager = new AudioInputManager();
2499
+
2500
+ export {
2501
+ MultiChannelOutputRouter,
2502
+ multiChannelOutputRouter,
2503
+ createCosmicImpulseBuffer,
2504
+ playSyntheticDrumVoice,
2505
+ BaseAudioGraphNode,
2506
+ MasterAudioOutputGraphNode,
2507
+ BaseSourceAudioGraphNode,
2508
+ MidiInstrumentGraphNode,
2509
+ AudioContainerGraphNode,
2510
+ MidiContainerGraphNode,
2511
+ BaseProcessorAudioGraphNode,
2512
+ EffectProcessorGraphNode,
2513
+ SignalRouterGraphNode,
2514
+ createAudioGraphNode,
2515
+ generateSaturationCurve,
2516
+ generateReverbImpulse,
2517
+ createSidechainCompressor,
2518
+ create3BandCrossover,
2519
+ createMultibandCompressor,
2520
+ createAudioEffectProcessor,
2521
+ AudioEffects,
2522
+ audioEffectsEntity,
2523
+ DEFAULT_NAMED_BUSSES,
2524
+ KNOWN_PATTERNS,
2525
+ detectHardwareProfile,
2526
+ buildInputChannelList,
2527
+ buildOutputChannelList,
2528
+ DEFAULT_NAMED_INPUTS,
2529
+ AudioInputManager,
2530
+ audioInputManager,
2531
+ SynthEngine,
2532
+ synthEngine
2533
+ };