@holoscript/engine 6.0.3 → 6.0.4

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 (192) hide show
  1. package/dist/AutoMesher-CK47F6AV.js +17 -0
  2. package/dist/GPUBuffers-2LHBCD7X.js +9 -0
  3. package/dist/WebGPUContext-TNEUYU2Y.js +11 -0
  4. package/dist/animation/index.cjs +38 -38
  5. package/dist/animation/index.d.cts +1 -1
  6. package/dist/animation/index.d.ts +1 -1
  7. package/dist/animation/index.js +1 -1
  8. package/dist/audio/index.cjs +16 -6
  9. package/dist/audio/index.d.cts +1 -1
  10. package/dist/audio/index.d.ts +1 -1
  11. package/dist/audio/index.js +1 -1
  12. package/dist/camera/index.cjs +23 -23
  13. package/dist/camera/index.d.cts +1 -1
  14. package/dist/camera/index.d.ts +1 -1
  15. package/dist/camera/index.js +1 -1
  16. package/dist/character/index.cjs +6 -4
  17. package/dist/character/index.js +1 -1
  18. package/dist/choreography/index.cjs +1194 -0
  19. package/dist/choreography/index.d.cts +687 -0
  20. package/dist/choreography/index.d.ts +687 -0
  21. package/dist/choreography/index.js +1156 -0
  22. package/dist/chunk-2CSNRI2N.js +217 -0
  23. package/dist/chunk-33T2WINR.js +266 -0
  24. package/dist/chunk-35R73OFM.js +1257 -0
  25. package/dist/chunk-4MMDSUNP.js +1256 -0
  26. package/dist/chunk-5V6HOU72.js +319 -0
  27. package/dist/chunk-6QOP6PYF.js +1038 -0
  28. package/dist/chunk-7KMJVHIL.js +8944 -0
  29. package/dist/chunk-7VPUC62U.js +1106 -0
  30. package/dist/chunk-A2Y6RCAT.js +1878 -0
  31. package/dist/chunk-AHM42MK6.js +8944 -0
  32. package/dist/chunk-BL7IDTHE.js +218 -0
  33. package/dist/chunk-CITOMSWL.js +10462 -0
  34. package/dist/chunk-CXDPKW2K.js +8944 -0
  35. package/dist/chunk-CXZPLD4S.js +223 -0
  36. package/dist/chunk-CZYJE7IH.js +5169 -0
  37. package/dist/chunk-D2OP7YC7.js +6325 -0
  38. package/dist/chunk-EDRVQHUU.js +1544 -0
  39. package/dist/chunk-EJSLOOW2.js +3589 -0
  40. package/dist/chunk-F53SFGW5.js +1878 -0
  41. package/dist/chunk-HCFPELPY.js +919 -0
  42. package/dist/chunk-HNEE36PY.js +93 -0
  43. package/dist/chunk-HYXNV36F.js +1256 -0
  44. package/dist/chunk-IB7KHVFY.js +821 -0
  45. package/dist/chunk-IBBO7YYG.js +690 -0
  46. package/dist/chunk-ILIBGINU.js +5470 -0
  47. package/dist/chunk-IS4MHLKN.js +5479 -0
  48. package/dist/chunk-JT2PFKWD.js +5479 -0
  49. package/dist/chunk-K4CUB4NY.js +1038 -0
  50. package/dist/chunk-KATDQXRJ.js +10462 -0
  51. package/dist/chunk-KBQE6ZFJ.js +8944 -0
  52. package/dist/chunk-KBVD5K7E.js +560 -0
  53. package/dist/chunk-KCDPVQRY.js +4088 -0
  54. package/dist/chunk-KN4QJPKN.js +8944 -0
  55. package/dist/chunk-KWJ3ROSI.js +8944 -0
  56. package/dist/chunk-L45VF6DD.js +919 -0
  57. package/dist/chunk-LY4T37YK.js +307 -0
  58. package/dist/chunk-MDN5WZXA.js +1544 -0
  59. package/dist/chunk-MGCDP6VU.js +928 -0
  60. package/dist/chunk-NCX7X6G2.js +8681 -0
  61. package/dist/chunk-OF54BPVD.js +913 -0
  62. package/dist/chunk-OWSN2Q3Q.js +690 -0
  63. package/dist/chunk-PRRB5TTA.js +406 -0
  64. package/dist/chunk-PXWVQF76.js +4086 -0
  65. package/dist/chunk-PYCOIDT2.js +812 -0
  66. package/dist/chunk-PZCSADOV.js +928 -0
  67. package/dist/chunk-Q2XBVS2K.js +1038 -0
  68. package/dist/chunk-QDZRXWN5.js +1776 -0
  69. package/dist/chunk-RNWOZ6WQ.js +913 -0
  70. package/dist/chunk-ROLFT4CJ.js +1693 -0
  71. package/dist/chunk-SLTJRZ2N.js +266 -0
  72. package/dist/chunk-SRUS5XSU.js +4088 -0
  73. package/dist/chunk-TKCA3WZ5.js +5409 -0
  74. package/dist/chunk-TNRMXYI2.js +1650 -0
  75. package/dist/chunk-TQB3GJGM.js +9763 -0
  76. package/dist/chunk-TUFGXG6K.js +510 -0
  77. package/dist/chunk-U6KMTGQJ.js +632 -0
  78. package/dist/chunk-VMGJQST6.js +8681 -0
  79. package/dist/chunk-X4F4TCG4.js +5470 -0
  80. package/dist/chunk-ZIFROE75.js +1544 -0
  81. package/dist/chunk-ZIJQYHSQ.js +1204 -0
  82. package/dist/combat/index.cjs +4 -4
  83. package/dist/combat/index.d.cts +1 -1
  84. package/dist/combat/index.d.ts +1 -1
  85. package/dist/combat/index.js +1 -1
  86. package/dist/ecs/index.cjs +1 -1
  87. package/dist/ecs/index.js +1 -1
  88. package/dist/environment/index.cjs +14 -14
  89. package/dist/environment/index.d.cts +1 -1
  90. package/dist/environment/index.d.ts +1 -1
  91. package/dist/environment/index.js +1 -1
  92. package/dist/gpu/index.cjs +4810 -0
  93. package/dist/gpu/index.js +3714 -0
  94. package/dist/hologram/index.cjs +27 -1
  95. package/dist/hologram/index.js +1 -1
  96. package/dist/index-B2PIsAmR.d.cts +2180 -0
  97. package/dist/index-B2PIsAmR.d.ts +2180 -0
  98. package/dist/index-BHySEPX7.d.cts +2921 -0
  99. package/dist/index-BJV21zuy.d.cts +341 -0
  100. package/dist/index-BJV21zuy.d.ts +341 -0
  101. package/dist/index-BQutTphC.d.cts +790 -0
  102. package/dist/index-ByIq2XrS.d.cts +3910 -0
  103. package/dist/index-BysHjDSO.d.cts +224 -0
  104. package/dist/index-BysHjDSO.d.ts +224 -0
  105. package/dist/index-CKwAJGck.d.ts +455 -0
  106. package/dist/index-CUl3QstQ.d.cts +3006 -0
  107. package/dist/index-CUl3QstQ.d.ts +3006 -0
  108. package/dist/index-CmYtNiI-.d.cts +953 -0
  109. package/dist/index-CmYtNiI-.d.ts +953 -0
  110. package/dist/index-CnRzWxi_.d.cts +522 -0
  111. package/dist/index-CnRzWxi_.d.ts +522 -0
  112. package/dist/index-CwRWbSC7.d.ts +2921 -0
  113. package/dist/index-CxKIBstO.d.ts +790 -0
  114. package/dist/index-DJ6-R8vh.d.cts +455 -0
  115. package/dist/index-DQKisbcI.d.cts +4968 -0
  116. package/dist/index-DQKisbcI.d.ts +4968 -0
  117. package/dist/index-DRT2zJez.d.ts +3910 -0
  118. package/dist/index-DfNLiAka.d.cts +192 -0
  119. package/dist/index-DfNLiAka.d.ts +192 -0
  120. package/dist/index-nMvkoRm8.d.cts +405 -0
  121. package/dist/index-nMvkoRm8.d.ts +405 -0
  122. package/dist/index-s9yOFU37.d.cts +604 -0
  123. package/dist/index-s9yOFU37.d.ts +604 -0
  124. package/dist/index.cjs +22966 -6960
  125. package/dist/index.d.cts +864 -20
  126. package/dist/index.d.ts +864 -20
  127. package/dist/index.js +3062 -48
  128. package/dist/input/index.cjs +1 -1
  129. package/dist/input/index.js +1 -1
  130. package/dist/orbital/index.cjs +3 -3
  131. package/dist/orbital/index.d.cts +1 -1
  132. package/dist/orbital/index.d.ts +1 -1
  133. package/dist/orbital/index.js +1 -1
  134. package/dist/particles/index.cjs +16 -16
  135. package/dist/particles/index.d.cts +1 -1
  136. package/dist/particles/index.d.ts +1 -1
  137. package/dist/particles/index.js +1 -1
  138. package/dist/physics/index.cjs +2377 -21
  139. package/dist/physics/index.d.cts +1 -1
  140. package/dist/physics/index.d.ts +1 -1
  141. package/dist/physics/index.js +35 -1
  142. package/dist/postfx/index.cjs +3491 -0
  143. package/dist/postfx/index.js +93 -0
  144. package/dist/procedural/index.cjs +1 -1
  145. package/dist/procedural/index.js +1 -1
  146. package/dist/puppeteer-5VF6KDVO.js +52197 -0
  147. package/dist/puppeteer-IZVZ3SG4.js +52197 -0
  148. package/dist/rendering/index.cjs +33 -32
  149. package/dist/rendering/index.d.cts +1 -1
  150. package/dist/rendering/index.d.ts +1 -1
  151. package/dist/rendering/index.js +8 -6
  152. package/dist/runtime/index.cjs +23 -13
  153. package/dist/runtime/index.d.cts +1 -1
  154. package/dist/runtime/index.d.ts +1 -1
  155. package/dist/runtime/index.js +8 -6
  156. package/dist/runtime/protocols/index.cjs +349 -0
  157. package/dist/runtime/protocols/index.js +15 -0
  158. package/dist/scene/index.cjs +8 -8
  159. package/dist/scene/index.d.cts +1 -1
  160. package/dist/scene/index.d.ts +1 -1
  161. package/dist/scene/index.js +1 -1
  162. package/dist/shader/index.cjs +3087 -0
  163. package/dist/shader/index.js +3044 -0
  164. package/dist/simulation/index.cjs +10680 -0
  165. package/dist/simulation/index.d.cts +3 -0
  166. package/dist/simulation/index.d.ts +3 -0
  167. package/dist/simulation/index.js +307 -0
  168. package/dist/spatial/index.cjs +2443 -0
  169. package/dist/spatial/index.d.cts +1545 -0
  170. package/dist/spatial/index.d.ts +1545 -0
  171. package/dist/spatial/index.js +2400 -0
  172. package/dist/terrain/index.cjs +1 -1
  173. package/dist/terrain/index.d.cts +1 -1
  174. package/dist/terrain/index.d.ts +1 -1
  175. package/dist/terrain/index.js +1 -1
  176. package/dist/transformers.node-4NKAPD5U.js +45620 -0
  177. package/dist/vm/index.cjs +7 -8
  178. package/dist/vm/index.d.cts +1 -1
  179. package/dist/vm/index.d.ts +1 -1
  180. package/dist/vm/index.js +1 -1
  181. package/dist/vm-bridge/index.cjs +2 -2
  182. package/dist/vm-bridge/index.d.cts +2 -2
  183. package/dist/vm-bridge/index.d.ts +2 -2
  184. package/dist/vm-bridge/index.js +1 -1
  185. package/dist/vr/index.cjs +6 -6
  186. package/dist/vr/index.js +1 -1
  187. package/dist/world/index.cjs +3 -3
  188. package/dist/world/index.d.cts +1 -1
  189. package/dist/world/index.d.ts +1 -1
  190. package/dist/world/index.js +1 -1
  191. package/package.json +53 -21
  192. package/LICENSE +0 -21
@@ -0,0 +1,4088 @@
1
+ import {
2
+ __export
3
+ } from "./chunk-AKLW2MUS.js";
4
+
5
+ // src/audio/index.ts
6
+ var audio_exports = {};
7
+ __export(audio_exports, {
8
+ AUDIO_DEFAULTS: () => AUDIO_DEFAULTS,
9
+ AudioAnalyzer: () => AudioAnalyzer,
10
+ AudioContextImpl: () => AudioContextImpl,
11
+ AudioDiffractionSystem: () => AudioDiffractionSystem,
12
+ AudioDynamics: () => AudioDynamics,
13
+ AudioEngine: () => AudioEngine,
14
+ AudioEnvelope: () => AudioEnvelope,
15
+ AudioFilter: () => AudioFilter,
16
+ AudioGraph: () => AudioGraph,
17
+ AudioMixer: () => AudioMixer,
18
+ AudioOcclusionSystem: () => AudioOcclusionSystem,
19
+ AudioPresets: () => AudioPresets,
20
+ DEFAULT_BANDS: () => DEFAULT_BANDS,
21
+ MusicGenerator: () => MusicGenerator,
22
+ OCCLUSION_MATERIALS: () => OCCLUSION_MATERIALS,
23
+ REVERB_PRESETS: () => REVERB_PRESETS,
24
+ SequencerImpl: () => SequencerImpl,
25
+ SoundPool: () => SoundPool,
26
+ SpatialAudioSource: () => SpatialAudioSource,
27
+ SpatialAudioZoneSystem: () => SpatialAudioZoneSystem,
28
+ SynthEngine: () => SynthEngine,
29
+ VoiceManager: () => VoiceManager,
30
+ audioTraitHandler: () => audioTraitHandler,
31
+ bandpassFilter: () => bandpassFilter,
32
+ bufferSource: () => bufferSource,
33
+ compressorEffect: () => compressorEffect,
34
+ createAudioContext: () => createAudioContext,
35
+ createNote: () => createNote,
36
+ createPattern: () => createPattern,
37
+ createSequence: () => createSequence,
38
+ createSequencer: () => createSequencer,
39
+ createTrack: () => createTrack,
40
+ defaultOrientation: () => defaultOrientation,
41
+ delayEffect: () => delayEffect,
42
+ distortionEffect: () => distortionEffect,
43
+ eqBand: () => eqBand,
44
+ equalizerEffect: () => equalizerEffect,
45
+ filterEffect: () => filterEffect,
46
+ frequencyToMidi: () => frequencyToMidi,
47
+ gainEffect: () => gainEffect,
48
+ getSharedAudioEngine: () => getSharedAudioEngine,
49
+ highpassFilter: () => highpassFilter,
50
+ lowpassFilter: () => lowpassFilter,
51
+ midiToFrequency: () => midiToFrequency,
52
+ midiToNoteName: () => midiToNoteName,
53
+ noiseSource: () => noiseSource,
54
+ noteNameToMidi: () => noteNameToMidi,
55
+ oscillatorSource: () => oscillatorSource,
56
+ panEffect: () => panEffect,
57
+ reverbEffect: () => reverbEffect,
58
+ setSharedAudioEngine: () => setSharedAudioEngine,
59
+ spatialSource: () => spatialSource,
60
+ streamSource: () => streamSource,
61
+ zeroVector: () => zeroVector
62
+ });
63
+
64
+ // src/audio/AudioTypes.ts
65
+ var AUDIO_DEFAULTS = {
66
+ sampleRate: 44100,
67
+ maxSources: 32,
68
+ masterVolume: 1,
69
+ spatialEnabled: true,
70
+ defaultRolloff: "inverse",
71
+ defaultRefDistance: 1,
72
+ defaultMaxDistance: 1e4,
73
+ doppler: true,
74
+ dopplerFactor: 1,
75
+ speedOfSound: 343
76
+ };
77
+ function zeroVector() {
78
+ return { x: 0, y: 0, z: 0 };
79
+ }
80
+ function defaultOrientation() {
81
+ return {
82
+ forward: { x: 0, y: 0, z: -1 },
83
+ up: { x: 0, y: 1, z: 0 }
84
+ };
85
+ }
86
+ function bufferSource(id, url, options = {}) {
87
+ return {
88
+ id,
89
+ type: "buffer",
90
+ url,
91
+ volume: 1,
92
+ pitch: 1,
93
+ loop: false,
94
+ spatial: false,
95
+ position: zeroVector(),
96
+ ...options
97
+ };
98
+ }
99
+ function oscillatorSource(id, waveform, frequency, options = {}) {
100
+ return {
101
+ id,
102
+ type: "oscillator",
103
+ oscillatorType: waveform,
104
+ frequency,
105
+ volume: 1,
106
+ pitch: 1,
107
+ loop: true,
108
+ spatial: false,
109
+ position: zeroVector(),
110
+ ...options
111
+ };
112
+ }
113
+ function streamSource(id, url, options = {}) {
114
+ return {
115
+ id,
116
+ type: "stream",
117
+ url,
118
+ volume: 1,
119
+ pitch: 1,
120
+ loop: false,
121
+ spatial: false,
122
+ position: zeroVector(),
123
+ ...options
124
+ };
125
+ }
126
+ function noiseSource(id, noiseType, options = {}) {
127
+ return {
128
+ id,
129
+ type: "noise",
130
+ noiseType,
131
+ volume: 1,
132
+ pitch: 1,
133
+ loop: true,
134
+ spatial: false,
135
+ position: zeroVector(),
136
+ ...options
137
+ };
138
+ }
139
+ function spatialSource(config, position, options = {}) {
140
+ return {
141
+ ...config,
142
+ spatial: true,
143
+ position,
144
+ maxDistance: AUDIO_DEFAULTS.defaultMaxDistance,
145
+ refDistance: AUDIO_DEFAULTS.defaultRefDistance,
146
+ rolloffFactor: 1,
147
+ coneInnerAngle: 360,
148
+ coneOuterAngle: 360,
149
+ coneOuterGain: 0,
150
+ ...options
151
+ };
152
+ }
153
+ function gainEffect(id, gain = 1) {
154
+ return {
155
+ id,
156
+ type: "gain",
157
+ gain,
158
+ wet: 1,
159
+ bypass: false
160
+ };
161
+ }
162
+ function reverbEffect(id, decay = 2, wet = 0.5, options = {}) {
163
+ return {
164
+ id,
165
+ type: "reverb",
166
+ roomSize: 0.5,
167
+ decay,
168
+ damping: 0.5,
169
+ preDelay: 0.01,
170
+ wet,
171
+ bypass: false,
172
+ ...options
173
+ };
174
+ }
175
+ function delayEffect(id, time = 0.5, feedback = 0.3, options = {}) {
176
+ return {
177
+ id,
178
+ type: "delay",
179
+ time,
180
+ delayTime: time,
181
+ feedback,
182
+ maxDelay: 5,
183
+ wet: 0.5,
184
+ bypass: false,
185
+ ...options
186
+ };
187
+ }
188
+ function filterEffect(id, filterType, frequency, options = {}) {
189
+ return {
190
+ id,
191
+ type: "filter",
192
+ filterType,
193
+ frequency,
194
+ Q: 1,
195
+ gain: 0,
196
+ wet: 1,
197
+ bypass: false,
198
+ ...options
199
+ };
200
+ }
201
+ function compressorEffect(id, threshold = -24, ratio = 4, options = {}) {
202
+ return {
203
+ id,
204
+ type: "compressor",
205
+ threshold,
206
+ ratio,
207
+ knee: 10,
208
+ attack: 3e-3,
209
+ release: 0.25,
210
+ makeupGain: 0,
211
+ wet: 1,
212
+ bypass: false,
213
+ ...options
214
+ };
215
+ }
216
+ function midiToFrequency(note) {
217
+ return 440 * Math.pow(2, (note - 69) / 12);
218
+ }
219
+ function frequencyToMidi(frequency) {
220
+ return Math.round(69 + 12 * Math.log2(frequency / 440));
221
+ }
222
+ function noteNameToMidi(name) {
223
+ const match = name.match(/^([A-Ga-g])([#b]?)(-?[0-9])$/);
224
+ if (!match) return 60;
225
+ const notes = {
226
+ C: 0,
227
+ D: 2,
228
+ E: 4,
229
+ F: 5,
230
+ G: 7,
231
+ A: 9,
232
+ B: 11
233
+ };
234
+ const noteName = match[1].toUpperCase();
235
+ const accidental = match[2];
236
+ const octave = parseInt(match[3], 10);
237
+ let midi = notes[noteName] + (octave + 1) * 12;
238
+ if (accidental === "#") midi += 1;
239
+ if (accidental === "b") midi -= 1;
240
+ return midi;
241
+ }
242
+ function midiToNoteName(midi) {
243
+ const noteNames = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
244
+ const octave = Math.floor(midi / 12) - 1;
245
+ const note = noteNames[midi % 12];
246
+ return `${note}${octave}`;
247
+ }
248
+ function lowpassFilter(id, frequency, Q = 1) {
249
+ return filterEffect(id, "lowpass", frequency, { Q });
250
+ }
251
+ function highpassFilter(id, frequency, Q = 1) {
252
+ return filterEffect(id, "highpass", frequency, { Q });
253
+ }
254
+ function bandpassFilter(id, frequency, Q = 1) {
255
+ return filterEffect(id, "bandpass", frequency, { Q });
256
+ }
257
+ function distortionEffect(id, amount = 0.5, options = {}) {
258
+ return {
259
+ id,
260
+ type: "distortion",
261
+ amount,
262
+ oversample: "2x",
263
+ wet: 1,
264
+ bypass: false,
265
+ ...options
266
+ };
267
+ }
268
+ function panEffect(id, pan = 0) {
269
+ return {
270
+ id,
271
+ type: "pan",
272
+ pan,
273
+ wet: 1,
274
+ bypass: false
275
+ };
276
+ }
277
+ function eqBand(frequency, Q = 1, gain = 0) {
278
+ return { frequency, Q, gain };
279
+ }
280
+ function equalizerEffect(id, bands, options = {}) {
281
+ return {
282
+ id,
283
+ type: "equalizer",
284
+ bands,
285
+ wet: 1,
286
+ bypass: false,
287
+ ...options
288
+ };
289
+ }
290
+ function createNote(pitch, start, duration, velocity = 100) {
291
+ return {
292
+ pitch: typeof pitch === "string" ? noteNameToMidi(pitch) : pitch,
293
+ startBeat: start,
294
+ start,
295
+ duration,
296
+ velocity
297
+ };
298
+ }
299
+ function createPattern(id, notes, options = {}) {
300
+ const maxEnd = notes.reduce(
301
+ (max, n) => Math.max(max, (n.start ?? n.startBeat ?? 0) + n.duration),
302
+ 0
303
+ );
304
+ return {
305
+ id,
306
+ notes,
307
+ length: options.length ?? Math.ceil(maxEnd),
308
+ loop: options.loop ?? true,
309
+ ...options
310
+ };
311
+ }
312
+ function createTrack(id, patterns, options = {}) {
313
+ return {
314
+ id,
315
+ patterns,
316
+ sourceId: options.sourceId,
317
+ volume: options.volume ?? 1,
318
+ muted: options.muted ?? false,
319
+ solo: options.solo ?? false,
320
+ ...options
321
+ };
322
+ }
323
+ function createSequence(id, patternOrder, tracks, options = {}) {
324
+ return {
325
+ id,
326
+ patternOrder,
327
+ tracks,
328
+ bpm: options.bpm ?? 120,
329
+ timeSignature: options.timeSignature ?? [4, 4],
330
+ loop: options.loop ?? false,
331
+ ...options
332
+ };
333
+ }
334
+
335
+ // src/audio/AudioContextImpl.ts
336
+ var AudioContextImpl = class {
337
+ _state = "suspended";
338
+ _currentTime = 0;
339
+ _masterVolume = 1;
340
+ _isMuted = false;
341
+ _previousVolume = 1;
342
+ config;
343
+ sources = /* @__PURE__ */ new Map();
344
+ effects = /* @__PURE__ */ new Map();
345
+ groups = /* @__PURE__ */ new Map();
346
+ bufferCache = /* @__PURE__ */ new Map();
347
+ decodedBuffers = /* @__PURE__ */ new Map();
348
+ eventListeners = /* @__PURE__ */ new Map();
349
+ listenerPosition = zeroVector();
350
+ listenerOrientation = defaultOrientation();
351
+ listenerVelocity = zeroVector();
352
+ timeUpdateInterval = null;
353
+ constructor(config = {}) {
354
+ this.config = { ...AUDIO_DEFAULTS, ...config };
355
+ this._masterVolume = this.config.masterVolume;
356
+ }
357
+ // ==========================================================================
358
+ // Lifecycle
359
+ // ==========================================================================
360
+ async initialize() {
361
+ if (this._state === "running") return;
362
+ this._state = "running";
363
+ this._currentTime = 0;
364
+ this.timeUpdateInterval = setInterval(() => {
365
+ if (this._state === "running") {
366
+ this._currentTime += 0.01;
367
+ this.updatePlayingSources();
368
+ }
369
+ }, 10);
370
+ }
371
+ async suspend() {
372
+ if (this._state !== "running") return;
373
+ this._state = "suspended";
374
+ }
375
+ async resume() {
376
+ if (this._state !== "suspended") return;
377
+ this._state = "running";
378
+ }
379
+ dispose() {
380
+ if (this.timeUpdateInterval) {
381
+ clearInterval(this.timeUpdateInterval);
382
+ this.timeUpdateInterval = null;
383
+ }
384
+ this.sources.clear();
385
+ this.effects.clear();
386
+ this.groups.clear();
387
+ this.bufferCache.clear();
388
+ this.decodedBuffers.clear();
389
+ this.eventListeners.clear();
390
+ this._state = "closed";
391
+ }
392
+ get state() {
393
+ return this._state;
394
+ }
395
+ get currentTime() {
396
+ return this._currentTime;
397
+ }
398
+ get sampleRate() {
399
+ return this.config.sampleRate;
400
+ }
401
+ // ==========================================================================
402
+ // Master Controls
403
+ // ==========================================================================
404
+ setMasterVolume(volume) {
405
+ this._masterVolume = Math.max(0, Math.min(1, volume));
406
+ if (!this._isMuted) {
407
+ this._previousVolume = this._masterVolume;
408
+ }
409
+ }
410
+ getMasterVolume() {
411
+ return this._masterVolume;
412
+ }
413
+ mute() {
414
+ if (!this._isMuted) {
415
+ this._previousVolume = this._masterVolume;
416
+ this._masterVolume = 0;
417
+ this._isMuted = true;
418
+ }
419
+ }
420
+ unmute() {
421
+ if (this._isMuted) {
422
+ this._masterVolume = this._previousVolume;
423
+ this._isMuted = false;
424
+ }
425
+ }
426
+ get isMuted() {
427
+ return this._isMuted;
428
+ }
429
+ // ==========================================================================
430
+ // Listener
431
+ // ==========================================================================
432
+ setListenerPosition(position) {
433
+ this.listenerPosition = { ...position };
434
+ }
435
+ setListenerOrientation(orientation) {
436
+ this.listenerOrientation = {
437
+ forward: { ...orientation.forward },
438
+ up: { ...orientation.up }
439
+ };
440
+ }
441
+ setListenerVelocity(velocity) {
442
+ this.listenerVelocity = { ...velocity };
443
+ }
444
+ getListenerConfig() {
445
+ return {
446
+ position: { ...this.listenerPosition },
447
+ orientation: {
448
+ forward: { ...this.listenerOrientation.forward },
449
+ up: { ...this.listenerOrientation.up }
450
+ },
451
+ velocity: { ...this.listenerVelocity }
452
+ };
453
+ }
454
+ // ==========================================================================
455
+ // Source Management
456
+ // ==========================================================================
457
+ async createSource(config) {
458
+ if (this.sources.has(config.id)) {
459
+ throw new Error(`Source with id '${config.id}' already exists`);
460
+ }
461
+ if (this.sources.size >= this.config.maxSources) {
462
+ throw new Error(`Maximum sources (${this.config.maxSources}) reached`);
463
+ }
464
+ if (config.type === "buffer" && config.url) {
465
+ if (!this.bufferCache.has(config.url)) {
466
+ const buffer = await this.loadBuffer(config.url);
467
+ this.bufferCache.set(config.url, buffer);
468
+ }
469
+ }
470
+ const source = {
471
+ id: config.id,
472
+ config,
473
+ state: "stopped",
474
+ volume: config.volume ?? 1,
475
+ pitch: config.pitch ?? 1,
476
+ loop: config.loop ?? false,
477
+ position: config.position ?? zeroVector(),
478
+ currentTime: 0,
479
+ duration: config.duration ?? 0,
480
+ startedAt: 0,
481
+ pausedAt: 0,
482
+ effects: config.effects ?? [],
483
+ analyzerData: new Float32Array(1024),
484
+ frequencyData: new Uint8Array(512)
485
+ };
486
+ this.sources.set(config.id, source);
487
+ if (config.group) {
488
+ const group = this.groups.get(config.group);
489
+ if (group) {
490
+ group.sources.add(config.id);
491
+ }
492
+ }
493
+ return config.id;
494
+ }
495
+ getSource(id) {
496
+ const source = this.sources.get(id);
497
+ if (!source) return void 0;
498
+ return {
499
+ id: source.id,
500
+ type: source.config.type,
501
+ state: source.state,
502
+ volume: source.volume,
503
+ pitch: source.pitch,
504
+ loop: source.loop,
505
+ position: { ...source.position },
506
+ currentTime: source.currentTime,
507
+ duration: source.duration,
508
+ spatial: source.config.spatial ?? false
509
+ };
510
+ }
511
+ getAllSources() {
512
+ return Array.from(this.sources.values()).map((source) => ({
513
+ id: source.id,
514
+ type: source.config.type,
515
+ state: source.state,
516
+ volume: source.volume,
517
+ pitch: source.pitch,
518
+ loop: source.loop,
519
+ position: { ...source.position },
520
+ currentTime: source.currentTime,
521
+ duration: source.duration,
522
+ spatial: source.config.spatial ?? false
523
+ }));
524
+ }
525
+ removeSource(id) {
526
+ const source = this.sources.get(id);
527
+ if (!source) return false;
528
+ if (source.state === "playing") {
529
+ this.stop(id);
530
+ }
531
+ if (source.config.group) {
532
+ const group = this.groups.get(source.config.group);
533
+ if (group) {
534
+ group.sources.delete(id);
535
+ }
536
+ }
537
+ this.sources.delete(id);
538
+ return true;
539
+ }
540
+ // ==========================================================================
541
+ // Playback Control
542
+ // ==========================================================================
543
+ play(sourceId, when) {
544
+ const source = this.sources.get(sourceId);
545
+ if (!source) return;
546
+ if (when !== void 0 && when > this._currentTime) {
547
+ source.state = "scheduled";
548
+ source.startedAt = when;
549
+ } else {
550
+ source.state = "playing";
551
+ source.startedAt = this._currentTime;
552
+ source.currentTime = 0;
553
+ }
554
+ this.emit({
555
+ type: "sourceStarted",
556
+ sourceId,
557
+ timestamp: this._currentTime
558
+ });
559
+ }
560
+ stop(sourceId) {
561
+ const source = this.sources.get(sourceId);
562
+ if (!source) return;
563
+ source.state = "stopped";
564
+ source.currentTime = 0;
565
+ source.startedAt = 0;
566
+ source.pausedAt = 0;
567
+ this.emit({
568
+ type: "sourceStopped",
569
+ sourceId,
570
+ timestamp: this._currentTime
571
+ });
572
+ }
573
+ pause(sourceId) {
574
+ const source = this.sources.get(sourceId);
575
+ if (!source || source.state !== "playing") return;
576
+ source.state = "paused";
577
+ source.pausedAt = this._currentTime;
578
+ this.emit({
579
+ type: "sourcePaused",
580
+ sourceId,
581
+ timestamp: this._currentTime
582
+ });
583
+ }
584
+ resumeSource(sourceId) {
585
+ const source = this.sources.get(sourceId);
586
+ if (!source || source.state !== "paused") return;
587
+ const pauseDuration = this._currentTime - source.pausedAt;
588
+ source.startedAt += pauseDuration;
589
+ source.state = "playing";
590
+ source.pausedAt = 0;
591
+ this.emit({
592
+ type: "sourceResumed",
593
+ sourceId,
594
+ timestamp: this._currentTime
595
+ });
596
+ }
597
+ // ==========================================================================
598
+ // Source Properties
599
+ // ==========================================================================
600
+ setVolume(sourceId, volume) {
601
+ const source = this.sources.get(sourceId);
602
+ if (source) {
603
+ source.volume = Math.max(0, Math.min(1, volume));
604
+ }
605
+ }
606
+ setPitch(sourceId, pitch) {
607
+ const source = this.sources.get(sourceId);
608
+ if (source && pitch > 0) {
609
+ source.pitch = pitch;
610
+ }
611
+ }
612
+ setLoop(sourceId, loop, start, end) {
613
+ const source = this.sources.get(sourceId);
614
+ if (source) {
615
+ source.loop = loop;
616
+ if (start !== void 0) source.config.loopStart = start;
617
+ if (end !== void 0) source.config.loopEnd = end;
618
+ }
619
+ }
620
+ setPosition(sourceId, position) {
621
+ const source = this.sources.get(sourceId);
622
+ if (source) {
623
+ source.position = { ...position };
624
+ }
625
+ }
626
+ setOrientation(sourceId, orientation) {
627
+ const source = this.sources.get(sourceId);
628
+ if (source) {
629
+ source.config.orientation = { ...orientation };
630
+ }
631
+ }
632
+ // ==========================================================================
633
+ // Effects
634
+ // ==========================================================================
635
+ createEffect(config) {
636
+ if (this.effects.has(config.id)) {
637
+ throw new Error(`Effect with id '${config.id}' already exists`);
638
+ }
639
+ this.effects.set(config.id, { id: config.id, config });
640
+ return config.id;
641
+ }
642
+ getEffect(id) {
643
+ return this.effects.get(id)?.config;
644
+ }
645
+ removeEffect(id) {
646
+ if (!this.effects.has(id)) return false;
647
+ for (const source of this.sources.values()) {
648
+ const index = source.effects.indexOf(id);
649
+ if (index !== -1) {
650
+ source.effects.splice(index, 1);
651
+ }
652
+ }
653
+ this.effects.delete(id);
654
+ return true;
655
+ }
656
+ connectSourceToEffect(sourceId, effectId) {
657
+ const source = this.sources.get(sourceId);
658
+ const effect = this.effects.get(effectId);
659
+ if (source && effect && !source.effects.includes(effectId)) {
660
+ source.effects.push(effectId);
661
+ }
662
+ }
663
+ disconnectSourceFromEffect(sourceId, effectId) {
664
+ const source = this.sources.get(sourceId);
665
+ if (source) {
666
+ const index = source.effects.indexOf(effectId);
667
+ if (index !== -1) {
668
+ source.effects.splice(index, 1);
669
+ }
670
+ }
671
+ }
672
+ // ==========================================================================
673
+ // Groups
674
+ // ==========================================================================
675
+ createGroup(config) {
676
+ if (this.groups.has(config.id)) {
677
+ throw new Error(`Group with id '${config.id}' already exists`);
678
+ }
679
+ this.groups.set(config.id, {
680
+ id: config.id,
681
+ config,
682
+ sources: /* @__PURE__ */ new Set()
683
+ });
684
+ return config.id;
685
+ }
686
+ getGroup(id) {
687
+ return this.groups.get(id)?.config;
688
+ }
689
+ setGroupVolume(groupId, volume) {
690
+ const group = this.groups.get(groupId);
691
+ if (group) {
692
+ group.config.volume = Math.max(0, Math.min(1, volume));
693
+ }
694
+ }
695
+ setGroupMuted(groupId, muted) {
696
+ const group = this.groups.get(groupId);
697
+ if (group) {
698
+ group.config.muted = muted;
699
+ }
700
+ }
701
+ setGroupSolo(groupId, solo) {
702
+ const group = this.groups.get(groupId);
703
+ if (group) {
704
+ group.config.solo = solo;
705
+ }
706
+ }
707
+ // ==========================================================================
708
+ // Buffer Management
709
+ // ==========================================================================
710
+ async loadBuffer(url) {
711
+ const cached = this.bufferCache.get(url);
712
+ if (cached) return cached;
713
+ const buffer = new ArrayBuffer(44100 * 2 * 2);
714
+ this.bufferCache.set(url, buffer);
715
+ this.emit({
716
+ type: "bufferLoaded",
717
+ timestamp: this._currentTime,
718
+ data: { url }
719
+ });
720
+ return buffer;
721
+ }
722
+ async decodeBuffer(buffer) {
723
+ return {
724
+ length: buffer.byteLength / 4,
725
+ duration: buffer.byteLength / (44100 * 4),
726
+ sampleRate: 44100,
727
+ numberOfChannels: 2,
728
+ getChannelData: (_channel) => new Float32Array(buffer.byteLength / 4),
729
+ copyFromChannel: () => {
730
+ },
731
+ copyToChannel: () => {
732
+ }
733
+ };
734
+ }
735
+ getCachedBuffer(url) {
736
+ return this.bufferCache.get(url);
737
+ }
738
+ clearBufferCache() {
739
+ this.bufferCache.clear();
740
+ this.decodedBuffers.clear();
741
+ }
742
+ // ==========================================================================
743
+ // Events
744
+ // ==========================================================================
745
+ on(event, callback) {
746
+ let listeners = this.eventListeners.get(event);
747
+ if (!listeners) {
748
+ listeners = /* @__PURE__ */ new Set();
749
+ this.eventListeners.set(event, listeners);
750
+ }
751
+ listeners.add(callback);
752
+ }
753
+ off(event, callback) {
754
+ const listeners = this.eventListeners.get(event);
755
+ if (listeners) {
756
+ listeners.delete(callback);
757
+ }
758
+ }
759
+ emit(event) {
760
+ const listeners = this.eventListeners.get(event.type);
761
+ if (listeners) {
762
+ for (const callback of listeners) {
763
+ try {
764
+ callback(event);
765
+ } catch (e) {
766
+ console.error("Error in audio event callback:", e);
767
+ }
768
+ }
769
+ }
770
+ }
771
+ // ==========================================================================
772
+ // Analysis
773
+ // ==========================================================================
774
+ getAnalyzerData(sourceId) {
775
+ const source = this.sources.get(sourceId);
776
+ if (!source || !source.analyzerData) return void 0;
777
+ if (source.state === "playing") {
778
+ for (let i = 0; i < source.analyzerData.length; i++) {
779
+ source.analyzerData[i] = Math.sin(i * 0.1 + this._currentTime * 10) * 0.5;
780
+ }
781
+ }
782
+ return source.analyzerData;
783
+ }
784
+ getFrequencyData(sourceId) {
785
+ const source = this.sources.get(sourceId);
786
+ if (!source || !source.frequencyData) return void 0;
787
+ if (source.state === "playing") {
788
+ for (let i = 0; i < source.frequencyData.length; i++) {
789
+ const freq = i / source.frequencyData.length;
790
+ source.frequencyData[i] = Math.floor(Math.random() * 128 + 64 * (1 - freq));
791
+ }
792
+ }
793
+ return source.frequencyData;
794
+ }
795
+ // ==========================================================================
796
+ // Private Methods
797
+ // ==========================================================================
798
+ updatePlayingSources() {
799
+ for (const source of this.sources.values()) {
800
+ if (source.state === "scheduled" && this._currentTime >= source.startedAt) {
801
+ source.state = "playing";
802
+ this.emit({
803
+ type: "sourceStarted",
804
+ sourceId: source.id,
805
+ timestamp: this._currentTime
806
+ });
807
+ }
808
+ if (source.state === "playing") {
809
+ source.currentTime = (this._currentTime - source.startedAt) * source.pitch;
810
+ if (source.duration > 0 && source.currentTime >= source.duration) {
811
+ if (source.loop) {
812
+ source.currentTime = source.config.loopStart ?? 0;
813
+ source.startedAt = this._currentTime;
814
+ this.emit({
815
+ type: "sourceLooped",
816
+ sourceId: source.id,
817
+ timestamp: this._currentTime
818
+ });
819
+ } else {
820
+ source.state = "stopped";
821
+ source.currentTime = 0;
822
+ this.emit({
823
+ type: "sourceEnded",
824
+ sourceId: source.id,
825
+ timestamp: this._currentTime
826
+ });
827
+ }
828
+ }
829
+ }
830
+ }
831
+ }
832
+ /**
833
+ * Calculate spatial audio gain based on distance
834
+ */
835
+ calculateSpatialGain(sourcePosition) {
836
+ const dx = sourcePosition.x - this.listenerPosition.x;
837
+ const dy = sourcePosition.y - this.listenerPosition.y;
838
+ const dz = sourcePosition.z - this.listenerPosition.z;
839
+ const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
840
+ const refDistance = this.config.defaultRefDistance;
841
+ const maxDistance = this.config.defaultMaxDistance;
842
+ if (distance <= refDistance) return 1;
843
+ if (distance >= maxDistance) return 0;
844
+ switch (this.config.defaultRolloff) {
845
+ case "linear":
846
+ return 1 - (distance - refDistance) / (maxDistance - refDistance);
847
+ case "exponential":
848
+ return Math.pow(distance / refDistance, -1);
849
+ case "inverse":
850
+ default:
851
+ return refDistance / (refDistance + (distance - refDistance));
852
+ }
853
+ }
854
+ };
855
+ function createAudioContext(config) {
856
+ return new AudioContextImpl(config);
857
+ }
858
+
859
+ // src/audio/Sequencer.ts
860
+ var SequencerImpl = class {
861
+ _state = "stopped";
862
+ _bpm = 120;
863
+ _swing = 0;
864
+ _currentBeat = 0;
865
+ _currentBar = 0;
866
+ _currentPatternIndex = 0;
867
+ _loopMode = "pattern";
868
+ _loopStart = 0;
869
+ _loopEnd = 4;
870
+ _metronomeEnabled = false;
871
+ _countInBars = 0;
872
+ _countingIn = false;
873
+ context;
874
+ sequences = /* @__PURE__ */ new Map();
875
+ patterns = /* @__PURE__ */ new Map();
876
+ tracks = /* @__PURE__ */ new Map();
877
+ scheduledNotes = [];
878
+ eventListeners = /* @__PURE__ */ new Map();
879
+ triggerCallbacks = /* @__PURE__ */ new Map();
880
+ currentSequenceId = null;
881
+ startTime = 0;
882
+ pausedTime = 0;
883
+ lastScheduledBeat = -1;
884
+ scheduleAheadTime = 0.1;
885
+ // seconds
886
+ tickInterval = null;
887
+ constructor(context) {
888
+ this.context = context;
889
+ }
890
+ // ==========================================================================
891
+ // Lifecycle
892
+ // ==========================================================================
893
+ load(sequence) {
894
+ this.createSequence(sequence);
895
+ this.loadSequence(sequence.id);
896
+ }
897
+ unload() {
898
+ if (this.currentSequenceId) {
899
+ this.removeSequence(this.currentSequenceId);
900
+ }
901
+ }
902
+ play() {
903
+ this.start();
904
+ }
905
+ start() {
906
+ if (this._state === "playing") return;
907
+ if (this._state === "paused") {
908
+ const pauseDuration = this.context.currentTime - this.pausedTime;
909
+ this.startTime += pauseDuration;
910
+ } else {
911
+ this.startTime = this.context.currentTime;
912
+ this._currentBeat = 0;
913
+ this._currentBar = 0;
914
+ this._currentPatternIndex = 0;
915
+ this.lastScheduledBeat = -1;
916
+ this.scheduledNotes.length = 0;
917
+ if (this._countInBars > 0) {
918
+ this._countingIn = true;
919
+ this.startTime += this.barsToSeconds(this._countInBars);
920
+ }
921
+ }
922
+ this._state = "playing";
923
+ this.startScheduler();
924
+ this.emit({
925
+ type: "sequencerStarted",
926
+ timestamp: this.context.currentTime
927
+ });
928
+ }
929
+ stop() {
930
+ if (this._state === "stopped") return;
931
+ this._state = "stopped";
932
+ this.stopScheduler();
933
+ this._currentBeat = 0;
934
+ this._currentBar = 0;
935
+ this._currentPatternIndex = 0;
936
+ this.lastScheduledBeat = -1;
937
+ this.scheduledNotes.length = 0;
938
+ this._countingIn = false;
939
+ this.emit({
940
+ type: "sequencerStopped",
941
+ timestamp: this.context.currentTime
942
+ });
943
+ }
944
+ pause() {
945
+ if (this._state !== "playing") return;
946
+ this._state = "paused";
947
+ this.pausedTime = this.context.currentTime;
948
+ this.stopScheduler();
949
+ this.emit({
950
+ type: "sequencerPaused",
951
+ timestamp: this.context.currentTime
952
+ });
953
+ }
954
+ get state() {
955
+ return this._state;
956
+ }
957
+ getState() {
958
+ return {
959
+ isPlaying: this._state === "playing",
960
+ isPaused: this._state === "paused",
961
+ currentBeat: this._currentBeat,
962
+ currentBar: this._currentBar,
963
+ bpm: this._bpm,
964
+ looping: this._loopMode !== "none",
965
+ loopStart: this._loopStart,
966
+ loopEnd: this._loopEnd
967
+ };
968
+ }
969
+ get isPlaying() {
970
+ return this._state === "playing";
971
+ }
972
+ dispose() {
973
+ this.stop();
974
+ this.sequences.clear();
975
+ this.patterns.clear();
976
+ this.tracks.clear();
977
+ this.eventListeners.clear();
978
+ this.triggerCallbacks.clear();
979
+ }
980
+ // ==========================================================================
981
+ // Transport Properties
982
+ // ==========================================================================
983
+ get bpm() {
984
+ return this._bpm;
985
+ }
986
+ set bpm(value) {
987
+ if (value > 0 && value <= 999) {
988
+ this._bpm = value;
989
+ this.emit({
990
+ type: "bpmChanged",
991
+ timestamp: this.context.currentTime,
992
+ data: { bpm: value }
993
+ });
994
+ }
995
+ }
996
+ get swing() {
997
+ return this._swing;
998
+ }
999
+ set swing(value) {
1000
+ this._swing = Math.max(0, Math.min(1, value));
1001
+ }
1002
+ get currentBeat() {
1003
+ return this._currentBeat;
1004
+ }
1005
+ get currentBar() {
1006
+ return this._currentBar;
1007
+ }
1008
+ get currentPatternIndex() {
1009
+ return this._currentPatternIndex;
1010
+ }
1011
+ setBPM(bpm) {
1012
+ this.bpm = bpm;
1013
+ }
1014
+ getBPM() {
1015
+ return this.bpm;
1016
+ }
1017
+ get loopMode() {
1018
+ return this._loopMode;
1019
+ }
1020
+ set loopMode(value) {
1021
+ this._loopMode = value;
1022
+ }
1023
+ setLoop(enabled, startBeat, endBeat) {
1024
+ this._loopMode = enabled ? "sequence" : "none";
1025
+ if (startBeat !== void 0 && endBeat !== void 0) {
1026
+ this.setLoopRange(startBeat, endBeat);
1027
+ }
1028
+ }
1029
+ get metronomeEnabled() {
1030
+ return this._metronomeEnabled;
1031
+ }
1032
+ set metronomeEnabled(value) {
1033
+ this._metronomeEnabled = value;
1034
+ }
1035
+ get countInBars() {
1036
+ return this._countInBars;
1037
+ }
1038
+ set countInBars(value) {
1039
+ this._countInBars = Math.max(0, Math.floor(value));
1040
+ }
1041
+ get isCountingIn() {
1042
+ return this._countingIn;
1043
+ }
1044
+ // ==========================================================================
1045
+ // Transport Controls
1046
+ // ==========================================================================
1047
+ setLoopRange(start, end) {
1048
+ if (start >= 0 && end > start) {
1049
+ this._loopStart = start;
1050
+ this._loopEnd = end;
1051
+ }
1052
+ }
1053
+ getLoopRange() {
1054
+ return {
1055
+ start: this._loopStart,
1056
+ end: this._loopEnd
1057
+ };
1058
+ }
1059
+ seek(beat, bar) {
1060
+ if (beat < 0) return;
1061
+ const wasPlaying = this._state === "playing";
1062
+ if (wasPlaying) {
1063
+ this.pause();
1064
+ }
1065
+ this._currentBeat = beat;
1066
+ if (bar !== void 0) {
1067
+ this._currentBar = bar;
1068
+ }
1069
+ const elapsedBeats = this._currentBar * this.getBeatsPerBar() + this._currentBeat;
1070
+ this.startTime = this.context.currentTime - this.beatsToSeconds(elapsedBeats);
1071
+ this.lastScheduledBeat = Math.floor(this._currentBeat) - 1;
1072
+ this.scheduledNotes.length = 0;
1073
+ if (wasPlaying) {
1074
+ this._state = "stopped";
1075
+ this.start();
1076
+ }
1077
+ this.emit({
1078
+ type: "sequencerSeeked",
1079
+ timestamp: this.context.currentTime,
1080
+ data: { beat: this._currentBeat, bar: this._currentBar }
1081
+ });
1082
+ }
1083
+ getPlaybackPosition() {
1084
+ return {
1085
+ beat: this._currentBeat,
1086
+ bar: this._currentBar,
1087
+ pattern: this._currentPatternIndex
1088
+ };
1089
+ }
1090
+ // ==========================================================================
1091
+ // Sequence Management
1092
+ // ==========================================================================
1093
+ createSequence(config) {
1094
+ if (this.sequences.has(config.id)) {
1095
+ throw new Error(`Sequence with id '${config.id}' already exists`);
1096
+ }
1097
+ this.sequences.set(config.id, { ...config });
1098
+ return config.id;
1099
+ }
1100
+ getSequence(id) {
1101
+ const seq = this.sequences.get(id);
1102
+ return seq ? { ...seq } : void 0;
1103
+ }
1104
+ updateSequence(id, updates) {
1105
+ const seq = this.sequences.get(id);
1106
+ if (seq) {
1107
+ Object.assign(seq, updates);
1108
+ }
1109
+ }
1110
+ removeSequence(id) {
1111
+ if (this.currentSequenceId === id) {
1112
+ this.stop();
1113
+ this.currentSequenceId = null;
1114
+ }
1115
+ return this.sequences.delete(id);
1116
+ }
1117
+ loadSequence(id) {
1118
+ if (!this.sequences.has(id)) return false;
1119
+ const wasPlaying = this._state === "playing";
1120
+ this.stop();
1121
+ this.currentSequenceId = id;
1122
+ if (wasPlaying) {
1123
+ this.start();
1124
+ }
1125
+ return true;
1126
+ }
1127
+ getCurrentSequence() {
1128
+ return this.currentSequenceId ? this.getSequence(this.currentSequenceId) : void 0;
1129
+ }
1130
+ // ==========================================================================
1131
+ // Pattern Management
1132
+ // ==========================================================================
1133
+ createPattern(config) {
1134
+ if (this.patterns.has(config.id)) {
1135
+ throw new Error(`Pattern with id '${config.id}' already exists`);
1136
+ }
1137
+ this.patterns.set(config.id, { ...config, notes: [...config.notes] });
1138
+ return config.id;
1139
+ }
1140
+ addPattern(pattern) {
1141
+ this.createPattern(pattern);
1142
+ }
1143
+ getPattern(id) {
1144
+ const pattern = this.patterns.get(id);
1145
+ if (!pattern) return void 0;
1146
+ return { ...pattern, notes: [...pattern.notes] };
1147
+ }
1148
+ updatePattern(id, updates) {
1149
+ const pattern = this.patterns.get(id);
1150
+ if (pattern) {
1151
+ if (updates.notes) {
1152
+ pattern.notes = [...updates.notes];
1153
+ }
1154
+ if (updates.name) pattern.name = updates.name;
1155
+ if (updates.bars !== void 0) pattern.bars = updates.bars;
1156
+ if (updates.beatsPerBar !== void 0) pattern.beatsPerBar = updates.beatsPerBar;
1157
+ if (updates.subdivision !== void 0) pattern.subdivision = updates.subdivision;
1158
+ }
1159
+ }
1160
+ removePattern(id) {
1161
+ return this.patterns.delete(id);
1162
+ }
1163
+ addNoteToPattern(patternId, note) {
1164
+ const pattern = this.patterns.get(patternId);
1165
+ if (pattern) {
1166
+ pattern.notes.push({ ...note });
1167
+ }
1168
+ }
1169
+ removeNoteFromPattern(patternId, noteIndex) {
1170
+ const pattern = this.patterns.get(patternId);
1171
+ if (pattern && noteIndex >= 0 && noteIndex < pattern.notes.length) {
1172
+ pattern.notes.splice(noteIndex, 1);
1173
+ }
1174
+ }
1175
+ scheduleNote(trackId, note) {
1176
+ const track = this.tracks.get(trackId);
1177
+ const patternRef = track?.patterns[0];
1178
+ if (!patternRef) return;
1179
+ const pattern = this.patterns.get(patternRef.patternId);
1180
+ if (!pattern) return;
1181
+ pattern.notes.push({ ...note });
1182
+ }
1183
+ quantize(beat, grid) {
1184
+ if (grid <= 0) return beat;
1185
+ return Math.round(beat / grid) * grid;
1186
+ }
1187
+ quantizePattern(patternId, subdivision) {
1188
+ const pattern = this.patterns.get(patternId);
1189
+ if (!pattern || subdivision <= 0) return;
1190
+ const step = 1 / subdivision;
1191
+ for (const note of pattern.notes) {
1192
+ const start = note.start ?? note.startBeat ?? 0;
1193
+ note.start = Math.round(start / step) * step;
1194
+ note.startBeat = note.start;
1195
+ if (note.duration !== void 0) {
1196
+ note.duration = Math.max(step, Math.round(note.duration / step) * step);
1197
+ }
1198
+ }
1199
+ }
1200
+ // ==========================================================================
1201
+ // Track Management
1202
+ // ==========================================================================
1203
+ createTrack(config) {
1204
+ if (this.tracks.has(config.id)) {
1205
+ throw new Error(`Track with id '${config.id}' already exists`);
1206
+ }
1207
+ this.tracks.set(config.id, {
1208
+ ...config,
1209
+ patterns: [...config.patterns],
1210
+ effects: config.effects ? [...config.effects] : []
1211
+ });
1212
+ return config.id;
1213
+ }
1214
+ getTrack(id) {
1215
+ const track = this.tracks.get(id);
1216
+ if (!track) return void 0;
1217
+ return {
1218
+ ...track,
1219
+ patterns: [...track.patterns],
1220
+ effects: track.effects ? [...track.effects] : []
1221
+ };
1222
+ }
1223
+ updateTrack(id, updates) {
1224
+ const track = this.tracks.get(id);
1225
+ if (track) {
1226
+ if (updates.patterns) track.patterns = [...updates.patterns];
1227
+ if (updates.effects) track.effects = [...updates.effects];
1228
+ if (updates.name !== void 0) track.name = updates.name;
1229
+ if (updates.volume !== void 0) track.volume = updates.volume;
1230
+ if (updates.pan !== void 0) track.pan = updates.pan;
1231
+ if (updates.muted !== void 0) track.muted = updates.muted;
1232
+ if (updates.solo !== void 0) track.solo = updates.solo;
1233
+ if (updates.outputSource !== void 0) track.outputSource = updates.outputSource;
1234
+ }
1235
+ }
1236
+ removeTrack(id) {
1237
+ return this.tracks.delete(id);
1238
+ }
1239
+ setTrackVolume(trackId, volume) {
1240
+ const track = this.tracks.get(trackId);
1241
+ if (track) {
1242
+ track.volume = volume;
1243
+ }
1244
+ }
1245
+ setTrackMuted(trackId, muted) {
1246
+ const track = this.tracks.get(trackId);
1247
+ if (track) {
1248
+ track.muted = muted;
1249
+ }
1250
+ }
1251
+ setTrackSolo(trackId, solo) {
1252
+ const track = this.tracks.get(trackId);
1253
+ if (track) {
1254
+ track.solo = solo;
1255
+ }
1256
+ }
1257
+ // ==========================================================================
1258
+ // Note Triggers
1259
+ // ==========================================================================
1260
+ onNoteTrigger(trackId, callback) {
1261
+ let callbacks = this.triggerCallbacks.get(trackId);
1262
+ if (!callbacks) {
1263
+ callbacks = /* @__PURE__ */ new Set();
1264
+ this.triggerCallbacks.set(trackId, callbacks);
1265
+ }
1266
+ callbacks.add(callback);
1267
+ }
1268
+ offNoteTrigger(trackId, callback) {
1269
+ const callbacks = this.triggerCallbacks.get(trackId);
1270
+ if (callbacks) {
1271
+ callbacks.delete(callback);
1272
+ }
1273
+ }
1274
+ // ==========================================================================
1275
+ // Events
1276
+ // ==========================================================================
1277
+ on(event, callback) {
1278
+ let listeners = this.eventListeners.get(event);
1279
+ if (!listeners) {
1280
+ listeners = /* @__PURE__ */ new Set();
1281
+ this.eventListeners.set(event, listeners);
1282
+ }
1283
+ listeners.add(callback);
1284
+ }
1285
+ off(event, callback) {
1286
+ const listeners = this.eventListeners.get(event);
1287
+ if (listeners) {
1288
+ listeners.delete(callback);
1289
+ }
1290
+ }
1291
+ // ==========================================================================
1292
+ // Utilities
1293
+ // ==========================================================================
1294
+ beatsToSeconds(beats) {
1295
+ return beats * 60 / this._bpm;
1296
+ }
1297
+ secondsToBeats(seconds) {
1298
+ return seconds * this._bpm / 60;
1299
+ }
1300
+ barsToSeconds(bars) {
1301
+ return this.beatsToSeconds(bars * this.getBeatsPerBar());
1302
+ }
1303
+ secondsToBars(seconds) {
1304
+ return this.secondsToBeats(seconds) / this.getBeatsPerBar();
1305
+ }
1306
+ getMidiFrequency(midiNote) {
1307
+ return midiToFrequency(midiNote);
1308
+ }
1309
+ getNoteToMidi(noteName) {
1310
+ return noteNameToMidi(noteName);
1311
+ }
1312
+ // ==========================================================================
1313
+ // Private Methods
1314
+ // ==========================================================================
1315
+ getBeatsPerBar() {
1316
+ const currentPattern = this.getCurrentPattern();
1317
+ return currentPattern?.beatsPerBar ?? 4;
1318
+ }
1319
+ getCurrentPattern() {
1320
+ const sequence = this.getCurrentSequence();
1321
+ const patternOrder = sequence?.patternOrder ?? sequence?.patterns?.map((p) => p.id) ?? [];
1322
+ if (!sequence || patternOrder.length === 0) return void 0;
1323
+ const patternId = patternOrder[this._currentPatternIndex];
1324
+ return patternId ? this.patterns.get(patternId) : void 0;
1325
+ }
1326
+ startScheduler() {
1327
+ if (this.tickInterval) return;
1328
+ this.tickInterval = setInterval(() => {
1329
+ this.schedulerTick();
1330
+ }, 25);
1331
+ }
1332
+ stopScheduler() {
1333
+ if (this.tickInterval) {
1334
+ clearInterval(this.tickInterval);
1335
+ this.tickInterval = null;
1336
+ }
1337
+ }
1338
+ schedulerTick() {
1339
+ if (this._state !== "playing") return;
1340
+ const currentTime = this.context.currentTime;
1341
+ const elapsedSeconds = currentTime - this.startTime;
1342
+ const elapsedBeats = this.secondsToBeats(elapsedSeconds);
1343
+ const beatsPerBar = this.getBeatsPerBar();
1344
+ this._currentBeat = elapsedBeats % beatsPerBar;
1345
+ this._currentBar = Math.floor(elapsedBeats / beatsPerBar);
1346
+ if (this._countingIn && elapsedSeconds >= 0) {
1347
+ this._countingIn = false;
1348
+ }
1349
+ const currentBeatInt = Math.floor(elapsedBeats);
1350
+ if (currentBeatInt > this.lastScheduledBeat) {
1351
+ for (let beat = this.lastScheduledBeat + 1; beat <= currentBeatInt; beat++) {
1352
+ const beatInBar = beat % beatsPerBar;
1353
+ this.emit({
1354
+ type: "beat",
1355
+ timestamp: this.beatsToSeconds(beat) + this.startTime,
1356
+ data: { beat: beatInBar, bar: Math.floor(beat / beatsPerBar) }
1357
+ });
1358
+ if (beatInBar === 0) {
1359
+ this.emit({
1360
+ type: "bar",
1361
+ timestamp: this.beatsToSeconds(beat) + this.startTime,
1362
+ data: { bar: Math.floor(beat / beatsPerBar) }
1363
+ });
1364
+ }
1365
+ if (this._metronomeEnabled) {
1366
+ this.playMetronome(beatInBar === 0);
1367
+ }
1368
+ }
1369
+ this.lastScheduledBeat = currentBeatInt;
1370
+ }
1371
+ this.scheduleNotes(elapsedBeats);
1372
+ this.handleLooping();
1373
+ this.triggerScheduledNotes(currentTime);
1374
+ }
1375
+ scheduleNotes(elapsedBeats) {
1376
+ const sequence = this.getCurrentSequence();
1377
+ if (!sequence) return;
1378
+ const scheduleAheadBeats = this.secondsToBeats(this.scheduleAheadTime);
1379
+ const lookAheadEnd = elapsedBeats + scheduleAheadBeats;
1380
+ const pattern = this.getCurrentPattern();
1381
+ if (!pattern) return;
1382
+ for (const trackEntry of sequence.tracks) {
1383
+ const trackId = trackEntry.id;
1384
+ const track = this.tracks.get(trackId) ?? trackEntry;
1385
+ if (!track || track.muted) continue;
1386
+ const patternBars = pattern.bars ?? 1;
1387
+ const patternOffset = this._currentPatternIndex * patternBars * (pattern.beatsPerBar ?? 4);
1388
+ for (const note of pattern.notes) {
1389
+ const noteStartBeat = patternOffset + (note.start ?? note.startBeat ?? 0);
1390
+ const noteEndBeat = noteStartBeat + (note.duration ?? 0.25);
1391
+ if (noteStartBeat >= elapsedBeats && noteStartBeat < lookAheadEnd) {
1392
+ const alreadyScheduled = this.scheduledNotes.some(
1393
+ (sn) => sn.track === trackId && Math.abs(sn.startTime - this.beatsToSeconds(noteStartBeat)) < 1e-3 && sn.note.pitch === note.pitch
1394
+ );
1395
+ if (!alreadyScheduled) {
1396
+ this.scheduledNotes.push({
1397
+ track: trackId,
1398
+ note,
1399
+ startTime: this.startTime + this.beatsToSeconds(noteStartBeat),
1400
+ endTime: this.startTime + this.beatsToSeconds(noteEndBeat),
1401
+ triggered: false,
1402
+ released: false
1403
+ });
1404
+ }
1405
+ }
1406
+ }
1407
+ }
1408
+ }
1409
+ triggerScheduledNotes(currentTime) {
1410
+ for (const scheduled of this.scheduledNotes) {
1411
+ if (!scheduled.triggered && currentTime >= scheduled.startTime) {
1412
+ scheduled.triggered = true;
1413
+ const callbacks = this.triggerCallbacks.get(scheduled.track);
1414
+ if (callbacks) {
1415
+ for (const callback of callbacks) {
1416
+ try {
1417
+ callback(scheduled.note, scheduled.startTime);
1418
+ } catch (e) {
1419
+ console.error("Error in note trigger callback:", e);
1420
+ }
1421
+ }
1422
+ }
1423
+ this.emit({
1424
+ type: "noteTriggered",
1425
+ timestamp: scheduled.startTime,
1426
+ data: { track: scheduled.track, note: scheduled.note }
1427
+ });
1428
+ }
1429
+ if (!scheduled.released && currentTime >= scheduled.endTime) {
1430
+ scheduled.released = true;
1431
+ this.emit({
1432
+ type: "noteReleased",
1433
+ timestamp: scheduled.endTime,
1434
+ data: { track: scheduled.track, note: scheduled.note }
1435
+ });
1436
+ }
1437
+ }
1438
+ const cutoffTime = currentTime - 1;
1439
+ for (let i = this.scheduledNotes.length - 1; i >= 0; i--) {
1440
+ if (this.scheduledNotes[i].released && this.scheduledNotes[i].endTime < cutoffTime) {
1441
+ this.scheduledNotes.splice(i, 1);
1442
+ }
1443
+ }
1444
+ }
1445
+ handleLooping() {
1446
+ const sequence = this.getCurrentSequence();
1447
+ if (!sequence) return;
1448
+ const pattern = this.getCurrentPattern();
1449
+ if (!pattern) return;
1450
+ const patternBars = pattern.bars ?? 1;
1451
+ const patternLength = patternBars * (pattern.beatsPerBar ?? 4);
1452
+ const totalBeats = this._currentBar * this.getBeatsPerBar() + this._currentBeat;
1453
+ const patternEnd = (this._currentPatternIndex + 1) * patternLength;
1454
+ const patternOrder = sequence.patternOrder ?? sequence.patterns?.map((p) => p.id) ?? [];
1455
+ if (totalBeats >= patternEnd) {
1456
+ if (this._loopMode === "pattern") {
1457
+ this.seek(0, this._currentPatternIndex * (pattern.bars ?? 1));
1458
+ } else if (this._loopMode === "sequence") {
1459
+ this._currentPatternIndex++;
1460
+ if (this._currentPatternIndex >= patternOrder.length) {
1461
+ this._currentPatternIndex = 0;
1462
+ this.seek(0, 0);
1463
+ this.emit({
1464
+ type: "sequenceLooped",
1465
+ timestamp: this.context.currentTime
1466
+ });
1467
+ }
1468
+ } else if (this._loopMode === "none") {
1469
+ this._currentPatternIndex++;
1470
+ if (this._currentPatternIndex >= patternOrder.length) {
1471
+ this.stop();
1472
+ }
1473
+ }
1474
+ }
1475
+ }
1476
+ playMetronome(isDownbeat) {
1477
+ this.emit({
1478
+ type: "metronomeClick",
1479
+ timestamp: this.context.currentTime,
1480
+ data: { isDownbeat }
1481
+ });
1482
+ }
1483
+ emit(event) {
1484
+ const listeners = this.eventListeners.get(event.type);
1485
+ if (listeners) {
1486
+ for (const callback of listeners) {
1487
+ try {
1488
+ callback(event);
1489
+ } catch (e) {
1490
+ console.error("Error in sequencer event callback:", e);
1491
+ }
1492
+ }
1493
+ }
1494
+ }
1495
+ };
1496
+ function createSequencer(context) {
1497
+ return new SequencerImpl(context);
1498
+ }
1499
+
1500
+ // src/audio/AudioEngine.ts
1501
+ function computeAttenuation(distance, model, refDist, maxDist, rolloff) {
1502
+ const d = Math.max(distance, refDist);
1503
+ switch (model) {
1504
+ case "linear": {
1505
+ const clamped = Math.min(d, maxDist);
1506
+ return 1 - rolloff * (clamped - refDist) / (maxDist - refDist);
1507
+ }
1508
+ case "inverse":
1509
+ return refDist / (refDist + rolloff * (d - refDist));
1510
+ case "exponential":
1511
+ return Math.pow(d / refDist, -rolloff);
1512
+ default:
1513
+ return 1;
1514
+ }
1515
+ }
1516
+ function vec3Dist(a, b) {
1517
+ const dx = a.x - b.x;
1518
+ const dy = a.y - b.y;
1519
+ const dz = a.z - b.z;
1520
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
1521
+ }
1522
+ function computePan(listener, sourcePos) {
1523
+ const rx = listener.forward.y * listener.up.z - listener.forward.z * listener.up.y;
1524
+ const rz = listener.forward.x * listener.up.y - listener.forward.y * listener.up.x;
1525
+ const dx = sourcePos.x - listener.position.x;
1526
+ const dz = sourcePos.z - listener.position.z;
1527
+ const dist = Math.sqrt(dx * dx + dz * dz);
1528
+ if (dist < 1e-3) return 0;
1529
+ const dot = dx * rx + dz * rz;
1530
+ const rLen = Math.sqrt(rx * rx + rz * rz);
1531
+ if (rLen < 1e-3) return 0;
1532
+ return Math.max(-1, Math.min(1, dot / (dist * rLen)));
1533
+ }
1534
+ var AudioEngine = class {
1535
+ sources = /* @__PURE__ */ new Map();
1536
+ listener = {
1537
+ position: [0, 0, 0],
1538
+ forward: { x: 0, y: 0, z: -1 },
1539
+ up: { x: 0, y: 1, z: 0 }
1540
+ };
1541
+ masterVolume = 1;
1542
+ muted = false;
1543
+ /**
1544
+ * Update the listener position (typically from VR headset).
1545
+ */
1546
+ setListenerPosition(pos) {
1547
+ this.listener.position = { ...pos };
1548
+ }
1549
+ setListenerOrientation(forward, up) {
1550
+ this.listener.forward = { ...forward };
1551
+ this.listener.up = { ...up };
1552
+ }
1553
+ getListener() {
1554
+ return { ...this.listener };
1555
+ }
1556
+ /**
1557
+ * Create and play a new audio source.
1558
+ */
1559
+ play(soundId, config = {}) {
1560
+ const id = config.id || `src_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
1561
+ const fullConfig = {
1562
+ id,
1563
+ position: [0, 0, 0],
1564
+ volume: 1,
1565
+ pitch: 1,
1566
+ loop: false,
1567
+ maxDistance: 50,
1568
+ refDistance: 1,
1569
+ rolloffFactor: 1,
1570
+ distanceModel: "inverse",
1571
+ channel: "master",
1572
+ spatialize: true,
1573
+ ...config
1574
+ };
1575
+ const source = {
1576
+ config: fullConfig,
1577
+ isPlaying: true,
1578
+ currentTime: 0,
1579
+ computedVolume: fullConfig.volume,
1580
+ computedPan: 0,
1581
+ soundId
1582
+ };
1583
+ this.sources.set(id, source);
1584
+ return id;
1585
+ }
1586
+ /**
1587
+ * Stop a playing source.
1588
+ */
1589
+ stop(sourceId) {
1590
+ const source = this.sources.get(sourceId);
1591
+ if (source) {
1592
+ source.isPlaying = false;
1593
+ this.sources.delete(sourceId);
1594
+ }
1595
+ }
1596
+ /**
1597
+ * Update a source's position.
1598
+ */
1599
+ setSourcePosition(sourceId, pos) {
1600
+ const source = this.sources.get(sourceId);
1601
+ if (source) source.config.position = { ...pos };
1602
+ }
1603
+ /**
1604
+ * Update all sources. Call every frame.
1605
+ */
1606
+ update(delta) {
1607
+ const toRemove = [];
1608
+ for (const [id, source] of this.sources) {
1609
+ if (!source.isPlaying) {
1610
+ toRemove.push(id);
1611
+ continue;
1612
+ }
1613
+ source.currentTime += delta * source.config.pitch;
1614
+ if (source.config.spatialize) {
1615
+ const dist = vec3Dist(this.listener.position, source.config.position);
1616
+ const attenuation = computeAttenuation(
1617
+ dist,
1618
+ source.config.distanceModel,
1619
+ source.config.refDistance,
1620
+ source.config.maxDistance,
1621
+ source.config.rolloffFactor
1622
+ );
1623
+ source.computedVolume = source.config.volume * attenuation * this.masterVolume * (this.muted ? 0 : 1);
1624
+ source.computedPan = computePan(this.listener, source.config.position);
1625
+ } else {
1626
+ source.computedVolume = source.config.volume * this.masterVolume * (this.muted ? 0 : 1);
1627
+ source.computedPan = 0;
1628
+ }
1629
+ }
1630
+ for (const id of toRemove) this.sources.delete(id);
1631
+ }
1632
+ /**
1633
+ * Get a source by ID.
1634
+ */
1635
+ getSource(sourceId) {
1636
+ return this.sources.get(sourceId);
1637
+ }
1638
+ /**
1639
+ * Get all active sources.
1640
+ */
1641
+ getActiveSources() {
1642
+ return Array.from(this.sources.values()).filter((s) => s.isPlaying);
1643
+ }
1644
+ /**
1645
+ * Set master volume.
1646
+ */
1647
+ setMasterVolume(vol) {
1648
+ this.masterVolume = Math.max(0, Math.min(1, vol));
1649
+ }
1650
+ getMasterVolume() {
1651
+ return this.masterVolume;
1652
+ }
1653
+ /**
1654
+ * Mute/unmute all audio.
1655
+ */
1656
+ setMuted(muted) {
1657
+ this.muted = muted;
1658
+ }
1659
+ isMuted() {
1660
+ return this.muted;
1661
+ }
1662
+ /**
1663
+ * Get active source count.
1664
+ */
1665
+ getActiveCount() {
1666
+ return this.sources.size;
1667
+ }
1668
+ /**
1669
+ * Stop all sources.
1670
+ */
1671
+ stopAll() {
1672
+ this.sources.clear();
1673
+ }
1674
+ };
1675
+
1676
+ // src/audio/AudioAnalyzer.ts
1677
+ function simpleDFT(samples, binCount) {
1678
+ const magnitudes = new Float32Array(binCount);
1679
+ const N = samples.length;
1680
+ for (let k = 0; k < binCount; k++) {
1681
+ let real = 0;
1682
+ let imag = 0;
1683
+ for (let n = 0; n < N; n++) {
1684
+ const angle = 2 * Math.PI * k * n / N;
1685
+ real += samples[n] * Math.cos(angle);
1686
+ imag -= samples[n] * Math.sin(angle);
1687
+ }
1688
+ magnitudes[k] = Math.sqrt(real * real + imag * imag) / N;
1689
+ }
1690
+ return magnitudes;
1691
+ }
1692
+ var DEFAULT_BANDS = [
1693
+ { name: "sub", low: 20, high: 60, energy: 0 },
1694
+ { name: "bass", low: 60, high: 250, energy: 0 },
1695
+ { name: "lowMid", low: 250, high: 500, energy: 0 },
1696
+ { name: "mid", low: 500, high: 2e3, energy: 0 },
1697
+ { name: "highMid", low: 2e3, high: 4e3, energy: 0 },
1698
+ { name: "presence", low: 4e3, high: 6e3, energy: 0 },
1699
+ { name: "brilliance", low: 6e3, high: 2e4, energy: 0 }
1700
+ ];
1701
+ var AudioAnalyzer = class {
1702
+ fftSize;
1703
+ sampleRate;
1704
+ currentSpectrum = null;
1705
+ bands;
1706
+ beatConfig;
1707
+ beatHistory = [];
1708
+ energyHistory = [];
1709
+ lastBeatTime = 0;
1710
+ smoothedEnergy = 0;
1711
+ loudness = { rms: 0, peak: 0, lufs: -100, dynamicRange: 0 };
1712
+ constructor(fftSize = 256, sampleRate = 44100, beatConfig) {
1713
+ this.fftSize = fftSize;
1714
+ this.sampleRate = sampleRate;
1715
+ this.bands = DEFAULT_BANDS.map((b) => ({ ...b }));
1716
+ this.beatConfig = {
1717
+ sensitivity: 0.5,
1718
+ minInterval: 200,
1719
+ energyThreshold: 0.1,
1720
+ frequencyRange: { low: 60, high: 250 },
1721
+ ...beatConfig
1722
+ };
1723
+ }
1724
+ // ---------------------------------------------------------------------------
1725
+ // Analysis
1726
+ // ---------------------------------------------------------------------------
1727
+ /**
1728
+ * Analyze a buffer of audio samples.
1729
+ */
1730
+ analyze(samples, currentTimeMs) {
1731
+ const binCount = Math.min(this.fftSize / 2, samples.length / 2);
1732
+ const frequencies = simpleDFT(samples, binCount);
1733
+ let peakMag = 0;
1734
+ let peakBin = 0;
1735
+ for (let i = 0; i < frequencies.length; i++) {
1736
+ if (frequencies[i] > peakMag) {
1737
+ peakMag = frequencies[i];
1738
+ peakBin = i;
1739
+ }
1740
+ }
1741
+ this.currentSpectrum = {
1742
+ frequencies,
1743
+ binCount,
1744
+ sampleRate: this.sampleRate,
1745
+ peakFrequency: peakBin * this.sampleRate / (binCount * 2),
1746
+ peakMagnitude: peakMag
1747
+ };
1748
+ this.computeBands(frequencies, binCount);
1749
+ this.computeLoudness(samples);
1750
+ this.detectBeat(currentTimeMs);
1751
+ }
1752
+ computeBands(frequencies, binCount) {
1753
+ const binWidth = this.sampleRate / (binCount * 2);
1754
+ for (const band of this.bands) {
1755
+ const startBin = Math.floor(band.low / binWidth);
1756
+ const endBin = Math.min(Math.ceil(band.high / binWidth), binCount - 1);
1757
+ let sum = 0;
1758
+ let count = 0;
1759
+ for (let i = startBin; i <= endBin; i++) {
1760
+ sum += frequencies[i];
1761
+ count++;
1762
+ }
1763
+ band.energy = count > 0 ? sum / count : 0;
1764
+ }
1765
+ }
1766
+ computeLoudness(samples) {
1767
+ let sumSquares = 0;
1768
+ let peak = 0;
1769
+ for (let i = 0; i < samples.length; i++) {
1770
+ const abs = Math.abs(samples[i]);
1771
+ sumSquares += samples[i] * samples[i];
1772
+ if (abs > peak) peak = abs;
1773
+ }
1774
+ const rms = Math.sqrt(sumSquares / samples.length);
1775
+ const lufs = rms > 0 ? 20 * Math.log10(rms) : -100;
1776
+ const peakDb = peak > 0 ? 20 * Math.log10(peak) : -100;
1777
+ this.loudness = {
1778
+ rms,
1779
+ peak,
1780
+ lufs,
1781
+ dynamicRange: peakDb - lufs
1782
+ };
1783
+ }
1784
+ detectBeat(currentTimeMs) {
1785
+ const bassEnergy = this.getBandEnergy("bass");
1786
+ this.energyHistory.push(bassEnergy);
1787
+ if (this.energyHistory.length > 43) this.energyHistory.shift();
1788
+ const avgEnergy = this.energyHistory.reduce((a, b) => a + b, 0) / this.energyHistory.length;
1789
+ this.smoothedEnergy = this.smoothedEnergy * 0.8 + bassEnergy * 0.2;
1790
+ const threshold = avgEnergy * (1 + this.beatConfig.sensitivity * 2) + this.beatConfig.energyThreshold;
1791
+ const timeSinceLastBeat = currentTimeMs - this.lastBeatTime;
1792
+ if (bassEnergy > threshold && timeSinceLastBeat > this.beatConfig.minInterval) {
1793
+ const bpm = this.estimateBPM();
1794
+ const strength = Math.min(1, (bassEnergy - avgEnergy) / Math.max(avgEnergy, 0.01));
1795
+ const beat = {
1796
+ timestamp: currentTimeMs,
1797
+ energy: bassEnergy,
1798
+ bpm,
1799
+ strength
1800
+ };
1801
+ this.beatHistory.push(beat);
1802
+ if (this.beatHistory.length > 20) this.beatHistory.shift();
1803
+ this.lastBeatTime = currentTimeMs;
1804
+ }
1805
+ }
1806
+ estimateBPM() {
1807
+ if (this.beatHistory.length < 2) return 0;
1808
+ const intervals = [];
1809
+ for (let i = 1; i < this.beatHistory.length; i++) {
1810
+ intervals.push(this.beatHistory[i].timestamp - this.beatHistory[i - 1].timestamp);
1811
+ }
1812
+ const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
1813
+ return avgInterval > 0 ? 6e4 / avgInterval : 0;
1814
+ }
1815
+ // ---------------------------------------------------------------------------
1816
+ // Queries
1817
+ // ---------------------------------------------------------------------------
1818
+ getSpectrum() {
1819
+ return this.currentSpectrum;
1820
+ }
1821
+ getBands() {
1822
+ return this.bands.map((b) => ({ ...b }));
1823
+ }
1824
+ getBandEnergy(bandName) {
1825
+ return this.bands.find((b) => b.name === bandName)?.energy ?? 0;
1826
+ }
1827
+ getLoudness() {
1828
+ return { ...this.loudness };
1829
+ }
1830
+ getBeats() {
1831
+ return [...this.beatHistory];
1832
+ }
1833
+ getLastBeat() {
1834
+ return this.beatHistory.length > 0 ? this.beatHistory[this.beatHistory.length - 1] : null;
1835
+ }
1836
+ getEstimatedBPM() {
1837
+ return this.estimateBPM();
1838
+ }
1839
+ getSmoothedEnergy() {
1840
+ return this.smoothedEnergy;
1841
+ }
1842
+ /**
1843
+ * Get a normalized 0-1 value for audio-reactive use.
1844
+ * Combines bass energy with beat strength.
1845
+ */
1846
+ getReactiveValue() {
1847
+ const bass = this.getBandEnergy("bass");
1848
+ const lastBeat = this.getLastBeat();
1849
+ const beatBoost = lastBeat ? lastBeat.strength * 0.3 : 0;
1850
+ return Math.min(1, bass + beatBoost);
1851
+ }
1852
+ reset() {
1853
+ this.currentSpectrum = null;
1854
+ this.bands = DEFAULT_BANDS.map((b) => ({ ...b }));
1855
+ this.beatHistory = [];
1856
+ this.energyHistory = [];
1857
+ this.smoothedEnergy = 0;
1858
+ this.lastBeatTime = 0;
1859
+ this.loudness = { rms: 0, peak: 0, lufs: -100, dynamicRange: 0 };
1860
+ }
1861
+ };
1862
+
1863
+ // src/audio/AudioDiffraction.ts
1864
+ var AudioDiffractionSystem = class {
1865
+ config = {
1866
+ enabled: true,
1867
+ maxPaths: 2,
1868
+ // Compute up to 2 diffraction paths
1869
+ minDiffractionGain: 0.01,
1870
+ // Ignore paths with <1% contribution
1871
+ frequency: 1e3,
1872
+ // 1kHz reference frequency
1873
+ speedOfSound: 343
1874
+ // m/s at 20°C
1875
+ };
1876
+ edgeProvider = null;
1877
+ losProvider = null;
1878
+ cache = /* @__PURE__ */ new Map();
1879
+ // ---------------------------------------------------------------------------
1880
+ // Configuration
1881
+ // ---------------------------------------------------------------------------
1882
+ setConfig(config) {
1883
+ this.config = { ...this.config, ...config };
1884
+ }
1885
+ getConfig() {
1886
+ return { ...this.config };
1887
+ }
1888
+ setEdgeDetectionProvider(provider) {
1889
+ this.edgeProvider = provider;
1890
+ }
1891
+ setLineOfSightProvider(provider) {
1892
+ this.losProvider = provider;
1893
+ }
1894
+ // ---------------------------------------------------------------------------
1895
+ // Diffraction Computation
1896
+ // ---------------------------------------------------------------------------
1897
+ /**
1898
+ * Compute diffraction paths between source and listener.
1899
+ * Returns all valid diffraction paths sorted by coefficient (strongest first).
1900
+ */
1901
+ computeDiffraction(sourcePos, listenerPos, sourceId) {
1902
+ if (!this.config.enabled || !this.edgeProvider || !this.losProvider) {
1903
+ return {
1904
+ sourceId,
1905
+ hasDiffraction: false,
1906
+ paths: [],
1907
+ combinedCoefficient: 0,
1908
+ volumeMultiplier: 1
1909
+ };
1910
+ }
1911
+ const hasDirectPath = this.losProvider(sourcePos, listenerPos);
1912
+ if (hasDirectPath) {
1913
+ const result2 = {
1914
+ sourceId,
1915
+ hasDiffraction: false,
1916
+ paths: [],
1917
+ combinedCoefficient: 0,
1918
+ volumeMultiplier: 1
1919
+ };
1920
+ this.cache.set(sourceId, result2);
1921
+ return result2;
1922
+ }
1923
+ const edges = this.edgeProvider(sourcePos, listenerPos);
1924
+ const paths = [];
1925
+ for (const edge of edges) {
1926
+ const path = this.computeEdgeDiffraction(edge, sourcePos, listenerPos);
1927
+ if (path && path.diffractionCoefficient >= this.config.minDiffractionGain) {
1928
+ paths.push(path);
1929
+ }
1930
+ }
1931
+ paths.sort((a, b) => b.diffractionCoefficient - a.diffractionCoefficient);
1932
+ const validPaths = paths.slice(0, this.config.maxPaths);
1933
+ const combinedCoefficient = this.combineDiffractionPaths(validPaths);
1934
+ const result = {
1935
+ sourceId,
1936
+ hasDiffraction: validPaths.length > 0,
1937
+ paths: validPaths,
1938
+ combinedCoefficient,
1939
+ volumeMultiplier: combinedCoefficient
1940
+ };
1941
+ this.cache.set(sourceId, result);
1942
+ return result;
1943
+ }
1944
+ /**
1945
+ * Compute diffraction for a specific edge.
1946
+ * Returns null if edge is not valid for diffraction.
1947
+ */
1948
+ computeEdgeDiffraction(edge, sourcePos, listenerPos) {
1949
+ if (!this.losProvider) return null;
1950
+ const edgePoint = this.findDiffractionPoint(edge, sourcePos, listenerPos);
1951
+ const sourceToEdge = this.losProvider(sourcePos, edgePoint);
1952
+ const edgeToListener = this.losProvider(edgePoint, listenerPos);
1953
+ if (!sourceToEdge || !edgeToListener) {
1954
+ return null;
1955
+ }
1956
+ const d1 = this.distance3D(sourcePos, edgePoint);
1957
+ const d2 = this.distance3D(edgePoint, listenerPos);
1958
+ const totalDistance = d1 + d2;
1959
+ const directDistance = this.distance3D(sourcePos, listenerPos);
1960
+ const pathDifference = totalDistance - directDistance;
1961
+ const angle = this.calculateDiffractionAngle(sourcePos, edgePoint, listenerPos);
1962
+ const coefficient = this.calculateFresnelCoefficient(pathDifference, angle);
1963
+ return {
1964
+ edgeId: edge.id,
1965
+ diffractionPoint: edgePoint,
1966
+ totalDistance,
1967
+ directDistance,
1968
+ pathDifference,
1969
+ diffractionCoefficient: coefficient,
1970
+ angle
1971
+ };
1972
+ }
1973
+ /**
1974
+ * Find the optimal diffraction point on an edge.
1975
+ * Uses closest point on line segment to the midpoint between source and listener.
1976
+ */
1977
+ findDiffractionPoint(edge, sourcePos, listenerPos) {
1978
+ const midpoint = {
1979
+ x: (sourcePos.x + listenerPos.x) / 2,
1980
+ y: (sourcePos.y + listenerPos.y) / 2,
1981
+ z: (sourcePos.z + listenerPos.z) / 2
1982
+ };
1983
+ return this.closestPointOnSegment(edge.point1, edge.point2, midpoint);
1984
+ }
1985
+ /**
1986
+ * Calculate the diffraction angle in radians.
1987
+ * This is the angle between source->edge and edge->listener vectors.
1988
+ */
1989
+ calculateDiffractionAngle(sourcePos, edgePoint, listenerPos) {
1990
+ const v1 = {
1991
+ x: sourcePos.x - edgePoint.x,
1992
+ y: sourcePos.y - edgePoint.y,
1993
+ z: sourcePos.z - edgePoint.z
1994
+ };
1995
+ const v2 = {
1996
+ x: listenerPos.x - edgePoint.x,
1997
+ y: listenerPos.y - edgePoint.y,
1998
+ z: listenerPos.z - edgePoint.z
1999
+ };
2000
+ const len1 = Math.sqrt(v1.x * v1.x + v1.y * v1.y + v1.z * v1.z);
2001
+ const len2 = Math.sqrt(v2.x * v2.x + v2.y * v2.y + v2.z * v2.z);
2002
+ if (len1 === 0 || len2 === 0) return 0;
2003
+ v1.x /= len1;
2004
+ v1.y /= len1;
2005
+ v1.z /= len1;
2006
+ v2.x /= len2;
2007
+ v2.y /= len2;
2008
+ v2.z /= len2;
2009
+ const dot = v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
2010
+ const clampedDot = Math.max(-1, Math.min(1, dot));
2011
+ return Math.acos(clampedDot);
2012
+ }
2013
+ /**
2014
+ * Calculate Fresnel diffraction coefficient.
2015
+ * Based on Kirchhoff-Fresnel theory for edge diffraction.
2016
+ *
2017
+ * @param pathDifference - Extra distance traveled via edge (meters)
2018
+ * @param angle - Diffraction angle (radians)
2019
+ * @returns Attenuation coefficient (0-1)
2020
+ */
2021
+ calculateFresnelCoefficient(pathDifference, angle) {
2022
+ const wavelength = this.config.speedOfSound / this.config.frequency;
2023
+ const v = Math.sqrt(2 * pathDifference / wavelength);
2024
+ let coefficient;
2025
+ if (v <= -1) {
2026
+ coefficient = 1;
2027
+ } else if (v >= 1) {
2028
+ coefficient = 0.5 / (v * v);
2029
+ } else {
2030
+ coefficient = 0.5 * (1 - v);
2031
+ }
2032
+ const angleFactor = Math.sin(angle / 2);
2033
+ coefficient *= angleFactor;
2034
+ return Math.max(0, Math.min(1, coefficient));
2035
+ }
2036
+ /**
2037
+ * Combine multiple diffraction paths using energy-based summation.
2038
+ * Paths contribute independently (incoherent sum).
2039
+ */
2040
+ combineDiffractionPaths(paths) {
2041
+ if (paths.length === 0) return 0;
2042
+ const sumSquared = paths.reduce((sum, path) => {
2043
+ return sum + path.diffractionCoefficient * path.diffractionCoefficient;
2044
+ }, 0);
2045
+ return Math.sqrt(sumSquared);
2046
+ }
2047
+ // ---------------------------------------------------------------------------
2048
+ // Geometry Utilities
2049
+ // ---------------------------------------------------------------------------
2050
+ distance3D(p1, p2) {
2051
+ const dx = p2.x - p1.x;
2052
+ const dy = p2.y - p1.y;
2053
+ const dz = p2.z - p1.z;
2054
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
2055
+ }
2056
+ /**
2057
+ * Find closest point on line segment AB to point P.
2058
+ */
2059
+ closestPointOnSegment(a, b, p) {
2060
+ const ab = { x: b.x - a.x, y: b.y - a.y, z: b.z - a.z };
2061
+ const ap = { x: p.x - a.x, y: p.y - a.y, z: p.z - a.z };
2062
+ const abLenSq = ab.x * ab.x + ab.y * ab.y + ab.z * ab.z;
2063
+ if (abLenSq === 0) return { ...a };
2064
+ const t = Math.max(0, Math.min(1, (ap.x * ab.x + ap.y * ab.y + ap.z * ab.z) / abLenSq));
2065
+ return {
2066
+ x: a.x + t * ab.x,
2067
+ y: a.y + t * ab.y,
2068
+ z: a.z + t * ab.z
2069
+ };
2070
+ }
2071
+ // ---------------------------------------------------------------------------
2072
+ // Cache Management
2073
+ // ---------------------------------------------------------------------------
2074
+ getCachedResult(sourceId) {
2075
+ return this.cache.get(sourceId);
2076
+ }
2077
+ clearCache() {
2078
+ this.cache.clear();
2079
+ }
2080
+ // ---------------------------------------------------------------------------
2081
+ // Integration Helpers
2082
+ // ---------------------------------------------------------------------------
2083
+ /**
2084
+ * Get volume multiplier for a source based on diffraction.
2085
+ * Returns 1.0 if direct path exists, otherwise returns diffraction coefficient.
2086
+ */
2087
+ getVolumeMultiplier(sourceId) {
2088
+ const result = this.cache.get(sourceId);
2089
+ return result?.volumeMultiplier ?? 1;
2090
+ }
2091
+ /**
2092
+ * Check if a source has active diffraction paths.
2093
+ */
2094
+ hasDiffraction(sourceId) {
2095
+ const result = this.cache.get(sourceId);
2096
+ return result?.hasDiffraction ?? false;
2097
+ }
2098
+ /**
2099
+ * Get all diffraction paths for a source.
2100
+ */
2101
+ getDiffractionPaths(sourceId) {
2102
+ const result = this.cache.get(sourceId);
2103
+ return result?.paths ?? [];
2104
+ }
2105
+ };
2106
+
2107
+ // src/audio/AudioDynamics.ts
2108
+ var AudioDynamics = class {
2109
+ compressor;
2110
+ gate;
2111
+ gainReduction = 0;
2112
+ gateOpen = false;
2113
+ envelope = 0;
2114
+ sidechainLevel = 0;
2115
+ ducking = false;
2116
+ duckAmount = 0;
2117
+ // dB
2118
+ constructor() {
2119
+ this.compressor = { threshold: -20, ratio: 4, attack: 3e-3, release: 0.1, makeup: 0, knee: 6 };
2120
+ this.gate = { threshold: -40, attack: 1e-3, release: 0.05, range: -80 };
2121
+ }
2122
+ // ---------------------------------------------------------------------------
2123
+ // Configuration
2124
+ // ---------------------------------------------------------------------------
2125
+ setCompressor(config) {
2126
+ Object.assign(this.compressor, config);
2127
+ }
2128
+ setGate(config) {
2129
+ Object.assign(this.gate, config);
2130
+ }
2131
+ getCompressor() {
2132
+ return { ...this.compressor };
2133
+ }
2134
+ // ---------------------------------------------------------------------------
2135
+ // Processing
2136
+ // ---------------------------------------------------------------------------
2137
+ processCompressor(inputDb) {
2138
+ const c = this.compressor;
2139
+ let outputDb;
2140
+ if (inputDb <= c.threshold - c.knee / 2) {
2141
+ outputDb = inputDb;
2142
+ } else if (inputDb >= c.threshold + c.knee / 2) {
2143
+ outputDb = c.threshold + (inputDb - c.threshold) / c.ratio;
2144
+ } else {
2145
+ const x = inputDb - c.threshold + c.knee / 2;
2146
+ outputDb = inputDb + (1 / c.ratio - 1) * (x * x) / (2 * c.knee);
2147
+ }
2148
+ this.gainReduction = inputDb - outputDb;
2149
+ return outputDb + c.makeup;
2150
+ }
2151
+ processGate(inputDb, dt) {
2152
+ const g = this.gate;
2153
+ if (inputDb > g.threshold) {
2154
+ this.envelope = Math.min(1, this.envelope + dt / g.attack);
2155
+ this.gateOpen = true;
2156
+ } else {
2157
+ this.envelope = Math.max(0, this.envelope - dt / g.release);
2158
+ if (this.envelope <= 0) this.gateOpen = false;
2159
+ }
2160
+ const attenuation = g.range * (1 - this.envelope);
2161
+ return inputDb + attenuation;
2162
+ }
2163
+ // ---------------------------------------------------------------------------
2164
+ // Sidechain Ducking
2165
+ // ---------------------------------------------------------------------------
2166
+ setSidechainLevel(db) {
2167
+ this.sidechainLevel = db;
2168
+ }
2169
+ processDucking(inputDb, threshold, amount) {
2170
+ if (this.sidechainLevel > threshold) {
2171
+ this.ducking = true;
2172
+ this.duckAmount = amount;
2173
+ return inputDb - amount;
2174
+ }
2175
+ this.ducking = false;
2176
+ this.duckAmount = 0;
2177
+ return inputDb;
2178
+ }
2179
+ // ---------------------------------------------------------------------------
2180
+ // Limiter
2181
+ // ---------------------------------------------------------------------------
2182
+ limit(inputDb, ceiling) {
2183
+ return Math.min(inputDb, ceiling);
2184
+ }
2185
+ // ---------------------------------------------------------------------------
2186
+ // Queries
2187
+ // ---------------------------------------------------------------------------
2188
+ getGainReduction() {
2189
+ return this.gainReduction;
2190
+ }
2191
+ isGateOpen() {
2192
+ return this.gateOpen;
2193
+ }
2194
+ isDucking() {
2195
+ return this.ducking;
2196
+ }
2197
+ getDuckAmount() {
2198
+ return this.duckAmount;
2199
+ }
2200
+ };
2201
+
2202
+ // src/audio/AudioEnvelope.ts
2203
+ var AudioEnvelope = class {
2204
+ config;
2205
+ stage = "idle";
2206
+ level = 0;
2207
+ elapsed = 0;
2208
+ curve;
2209
+ constructor(config, curve = "linear") {
2210
+ this.config = { attack: 0.01, decay: 0.1, sustain: 0.7, release: 0.3, ...config };
2211
+ this.curve = curve;
2212
+ }
2213
+ // ---------------------------------------------------------------------------
2214
+ // Gate On/Off
2215
+ // ---------------------------------------------------------------------------
2216
+ noteOn() {
2217
+ this.stage = "attack";
2218
+ this.elapsed = 0;
2219
+ }
2220
+ noteOff() {
2221
+ if (this.stage !== "idle") {
2222
+ this.stage = "release";
2223
+ this.elapsed = 0;
2224
+ }
2225
+ }
2226
+ // ---------------------------------------------------------------------------
2227
+ // Process
2228
+ // ---------------------------------------------------------------------------
2229
+ process(dt) {
2230
+ this.elapsed += dt;
2231
+ switch (this.stage) {
2232
+ case "idle":
2233
+ this.level = 0;
2234
+ break;
2235
+ case "attack": {
2236
+ const t = Math.min(1, this.elapsed / this.config.attack);
2237
+ this.level = this.applyCurve(t);
2238
+ if (t >= 1) {
2239
+ this.stage = "decay";
2240
+ this.elapsed = 0;
2241
+ }
2242
+ break;
2243
+ }
2244
+ case "decay": {
2245
+ const t = Math.min(1, this.elapsed / this.config.decay);
2246
+ this.level = 1 - (1 - this.config.sustain) * this.applyCurve(t);
2247
+ if (t >= 1) {
2248
+ this.stage = "sustain";
2249
+ this.elapsed = 0;
2250
+ }
2251
+ break;
2252
+ }
2253
+ case "sustain":
2254
+ this.level = this.config.sustain;
2255
+ break;
2256
+ case "release": {
2257
+ const t = Math.min(1, this.elapsed / this.config.release);
2258
+ this.level = this.config.sustain * (1 - this.applyCurve(t));
2259
+ if (t >= 1) {
2260
+ this.stage = "idle";
2261
+ this.level = 0;
2262
+ }
2263
+ break;
2264
+ }
2265
+ }
2266
+ return this.level;
2267
+ }
2268
+ // ---------------------------------------------------------------------------
2269
+ // Curve Shaping
2270
+ // ---------------------------------------------------------------------------
2271
+ applyCurve(t) {
2272
+ switch (this.curve) {
2273
+ case "linear":
2274
+ return t;
2275
+ case "exponential":
2276
+ return t * t;
2277
+ case "logarithmic":
2278
+ return Math.sqrt(t);
2279
+ }
2280
+ }
2281
+ // ---------------------------------------------------------------------------
2282
+ // Queries
2283
+ // ---------------------------------------------------------------------------
2284
+ getLevel() {
2285
+ return this.level;
2286
+ }
2287
+ getStage() {
2288
+ return this.stage;
2289
+ }
2290
+ setConfig(config) {
2291
+ Object.assign(this.config, config);
2292
+ }
2293
+ getConfig() {
2294
+ return { ...this.config };
2295
+ }
2296
+ isActive() {
2297
+ return this.stage !== "idle";
2298
+ }
2299
+ };
2300
+
2301
+ // src/audio/AudioFilter.ts
2302
+ var AudioFilter = class {
2303
+ bands = /* @__PURE__ */ new Map();
2304
+ // ---------------------------------------------------------------------------
2305
+ // Band Management
2306
+ // ---------------------------------------------------------------------------
2307
+ addBand(id, config) {
2308
+ this.bands.set(id, { id, config, enabled: true });
2309
+ }
2310
+ removeBand(id) {
2311
+ this.bands.delete(id);
2312
+ }
2313
+ setBandEnabled(id, enabled) {
2314
+ const band = this.bands.get(id);
2315
+ if (band) band.enabled = enabled;
2316
+ }
2317
+ setFrequency(id, freq) {
2318
+ const band = this.bands.get(id);
2319
+ if (band) band.config.frequency = Math.max(20, Math.min(2e4, freq));
2320
+ }
2321
+ setQ(id, q) {
2322
+ const band = this.bands.get(id);
2323
+ if (band) band.config.q = Math.max(0.1, Math.min(30, q));
2324
+ }
2325
+ setGain(id, gain) {
2326
+ const band = this.bands.get(id);
2327
+ if (band) band.config.gain = Math.max(-24, Math.min(24, gain));
2328
+ }
2329
+ // ---------------------------------------------------------------------------
2330
+ // Processing (simplified magnitude response)
2331
+ // ---------------------------------------------------------------------------
2332
+ getResponse(frequency) {
2333
+ let totalGain = 0;
2334
+ for (const band of this.bands.values()) {
2335
+ if (!band.enabled) continue;
2336
+ totalGain += this.computeBandResponse(band.config, frequency);
2337
+ }
2338
+ return totalGain;
2339
+ }
2340
+ computeBandResponse(config, freq) {
2341
+ const ratio = freq / config.frequency;
2342
+ switch (config.type) {
2343
+ case "lowpass":
2344
+ return ratio <= 1 ? 0 : -12 * Math.log2(ratio) * config.q;
2345
+ case "highpass":
2346
+ return ratio >= 1 ? 0 : -12 * Math.log2(1 / ratio) * config.q;
2347
+ case "bandpass": {
2348
+ const bw = config.frequency / config.q;
2349
+ const dist = Math.abs(freq - config.frequency);
2350
+ return dist <= bw / 2 ? 0 : -(dist / bw) * 6;
2351
+ }
2352
+ case "notch": {
2353
+ const bw = config.frequency / config.q;
2354
+ const dist = Math.abs(freq - config.frequency);
2355
+ return dist <= bw / 2 ? -config.gain : 0;
2356
+ }
2357
+ case "peaking": {
2358
+ const bw = config.frequency / config.q;
2359
+ const dist = Math.abs(freq - config.frequency);
2360
+ if (dist >= bw) return 0;
2361
+ return config.gain * (1 - dist / bw);
2362
+ }
2363
+ }
2364
+ }
2365
+ // ---------------------------------------------------------------------------
2366
+ // Queries
2367
+ // ---------------------------------------------------------------------------
2368
+ getBand(id) {
2369
+ return this.bands.get(id);
2370
+ }
2371
+ getBandCount() {
2372
+ return this.bands.size;
2373
+ }
2374
+ getEnabledBandCount() {
2375
+ return [...this.bands.values()].filter((b) => b.enabled).length;
2376
+ }
2377
+ };
2378
+
2379
+ // src/audio/AudioGraph.ts
2380
+ var _nodeId = 0;
2381
+ var _connId = 0;
2382
+ var AudioGraph = class {
2383
+ nodes = /* @__PURE__ */ new Map();
2384
+ connections = /* @__PURE__ */ new Map();
2385
+ automations = [];
2386
+ processingOrder = [];
2387
+ dirty = true;
2388
+ // ---------------------------------------------------------------------------
2389
+ // Node Management
2390
+ // ---------------------------------------------------------------------------
2391
+ addNode(type, params) {
2392
+ const id = `anode_${_nodeId++}`;
2393
+ const node = {
2394
+ id,
2395
+ type,
2396
+ params: new Map(Object.entries(params ?? {})),
2397
+ inputs: [],
2398
+ outputs: [],
2399
+ bypassed: false
2400
+ };
2401
+ this.nodes.set(id, node);
2402
+ this.dirty = true;
2403
+ switch (type) {
2404
+ case "gain":
2405
+ if (!node.params.has("gain")) node.params.set("gain", 1);
2406
+ break;
2407
+ case "filter":
2408
+ if (!node.params.has("cutoff")) node.params.set("cutoff", 1e3);
2409
+ break;
2410
+ case "delay":
2411
+ if (!node.params.has("time")) node.params.set("time", 0.5);
2412
+ break;
2413
+ case "reverb":
2414
+ if (!node.params.has("decay")) node.params.set("decay", 1.5);
2415
+ break;
2416
+ case "compressor":
2417
+ if (!node.params.has("threshold")) node.params.set("threshold", -24);
2418
+ break;
2419
+ }
2420
+ return node;
2421
+ }
2422
+ removeNode(id) {
2423
+ for (const [connId, conn] of this.connections) {
2424
+ if (conn.sourceId === id || conn.targetId === id) {
2425
+ this.disconnect(connId);
2426
+ }
2427
+ }
2428
+ this.dirty = true;
2429
+ return this.nodes.delete(id);
2430
+ }
2431
+ // ---------------------------------------------------------------------------
2432
+ // Connections
2433
+ // ---------------------------------------------------------------------------
2434
+ connect(sourceId, targetId, sourcePort = 0, targetPort = 0) {
2435
+ const source = this.nodes.get(sourceId);
2436
+ const target = this.nodes.get(targetId);
2437
+ if (!source || !target) return null;
2438
+ const id = `conn_${_connId++}`;
2439
+ this.connections.set(id, { id, sourceId, targetId, sourcePort, targetPort });
2440
+ source.outputs.push(targetId);
2441
+ target.inputs.push(sourceId);
2442
+ this.dirty = true;
2443
+ return id;
2444
+ }
2445
+ disconnect(connId) {
2446
+ const conn = this.connections.get(connId);
2447
+ if (!conn) return false;
2448
+ const source = this.nodes.get(conn.sourceId);
2449
+ const target = this.nodes.get(conn.targetId);
2450
+ if (source) source.outputs = source.outputs.filter((id) => id !== conn.targetId);
2451
+ if (target) target.inputs = target.inputs.filter((id) => id !== conn.sourceId);
2452
+ this.dirty = true;
2453
+ return this.connections.delete(connId);
2454
+ }
2455
+ // ---------------------------------------------------------------------------
2456
+ // Automation
2457
+ // ---------------------------------------------------------------------------
2458
+ automate(nodeId, paramName, points) {
2459
+ this.automations.push({
2460
+ nodeId,
2461
+ paramName,
2462
+ points: [...points].sort((a, b) => a.time - b.time)
2463
+ });
2464
+ }
2465
+ applyAutomation(time) {
2466
+ for (const auto of this.automations) {
2467
+ const node = this.nodes.get(auto.nodeId);
2468
+ if (!node) continue;
2469
+ const value = this.evaluateAutomation(auto.points, time);
2470
+ if (value !== null) node.params.set(auto.paramName, value);
2471
+ }
2472
+ }
2473
+ evaluateAutomation(points, time) {
2474
+ if (points.length === 0) return null;
2475
+ if (time <= points[0].time) return points[0].value;
2476
+ if (time >= points[points.length - 1].time) return points[points.length - 1].value;
2477
+ for (let i = 0; i < points.length - 1; i++) {
2478
+ if (time >= points[i].time && time < points[i + 1].time) {
2479
+ const t = (time - points[i].time) / (points[i + 1].time - points[i].time);
2480
+ switch (points[i + 1].curve) {
2481
+ case "step":
2482
+ return points[i].value;
2483
+ case "exponential":
2484
+ return points[i].value * Math.pow(points[i + 1].value / points[i].value, t);
2485
+ case "linear":
2486
+ default:
2487
+ return points[i].value + (points[i + 1].value - points[i].value) * t;
2488
+ }
2489
+ }
2490
+ }
2491
+ return null;
2492
+ }
2493
+ // ---------------------------------------------------------------------------
2494
+ // Processing Order (topological sort)
2495
+ // ---------------------------------------------------------------------------
2496
+ getProcessingOrder() {
2497
+ if (!this.dirty) return [...this.processingOrder];
2498
+ const visited = /* @__PURE__ */ new Set();
2499
+ const order = [];
2500
+ const visit = (id) => {
2501
+ if (visited.has(id)) return;
2502
+ visited.add(id);
2503
+ const node = this.nodes.get(id);
2504
+ if (node) for (const inputId of node.inputs) visit(inputId);
2505
+ order.push(id);
2506
+ };
2507
+ for (const id of this.nodes.keys()) visit(id);
2508
+ this.processingOrder = order;
2509
+ this.dirty = false;
2510
+ return [...order];
2511
+ }
2512
+ // ---------------------------------------------------------------------------
2513
+ // Node Controls
2514
+ // ---------------------------------------------------------------------------
2515
+ setParam(nodeId, param, value) {
2516
+ this.nodes.get(nodeId)?.params.set(param, value);
2517
+ }
2518
+ getParam(nodeId, param) {
2519
+ return this.nodes.get(nodeId)?.params.get(param);
2520
+ }
2521
+ bypass(nodeId, bypassed) {
2522
+ const node = this.nodes.get(nodeId);
2523
+ if (node) node.bypassed = bypassed;
2524
+ }
2525
+ // ---------------------------------------------------------------------------
2526
+ // Queries
2527
+ // ---------------------------------------------------------------------------
2528
+ getNode(id) {
2529
+ return this.nodes.get(id);
2530
+ }
2531
+ getNodeCount() {
2532
+ return this.nodes.size;
2533
+ }
2534
+ getConnectionCount() {
2535
+ return this.connections.size;
2536
+ }
2537
+ };
2538
+
2539
+ // src/audio/AudioMixer.ts
2540
+ var AudioMixer = class {
2541
+ channels = /* @__PURE__ */ new Map();
2542
+ masterVolume = 1;
2543
+ masterMuted = false;
2544
+ // Advanced features
2545
+ duckingStates = /* @__PURE__ */ new Map();
2546
+ sidechains = /* @__PURE__ */ new Map();
2547
+ activeSources = /* @__PURE__ */ new Map();
2548
+ voiceStealingStrategy = { mode: "oldest" };
2549
+ currentContext = null;
2550
+ constructor() {
2551
+ this.createChannel("master", 1, 10, 0);
2552
+ this.createChannel("sfx", 1, 5, 32);
2553
+ this.createChannel("music", 0.5, 7, 4);
2554
+ this.createChannel("ambient", 0.6, 4, 16);
2555
+ this.createChannel("ui", 0.8, 8, 8);
2556
+ this.createChannel("voice", 1, 10, 8);
2557
+ }
2558
+ /**
2559
+ * Create or update a channel.
2560
+ */
2561
+ createChannel(name, volume = 1, priority = 5, maxVoices = 0) {
2562
+ const existing = this.channels.get(name);
2563
+ this.channels.set(name, {
2564
+ name,
2565
+ volume,
2566
+ muted: existing?.muted ?? false,
2567
+ priority,
2568
+ maxVoices,
2569
+ currentVoices: existing?.currentVoices ?? 0
2570
+ });
2571
+ }
2572
+ /**
2573
+ * Set channel volume.
2574
+ */
2575
+ setChannelVolume(name, volume) {
2576
+ const ch = this.channels.get(name);
2577
+ if (ch) ch.volume = Math.max(0, Math.min(1, volume));
2578
+ }
2579
+ /**
2580
+ * Get channel volume.
2581
+ */
2582
+ getChannelVolume(name) {
2583
+ return this.channels.get(name)?.volume ?? 1;
2584
+ }
2585
+ /**
2586
+ * Mute/unmute a channel.
2587
+ */
2588
+ setChannelMuted(name, muted) {
2589
+ const ch = this.channels.get(name);
2590
+ if (ch) ch.muted = muted;
2591
+ }
2592
+ /**
2593
+ * Check if a channel is muted.
2594
+ */
2595
+ isChannelMuted(name) {
2596
+ return this.channels.get(name)?.muted ?? false;
2597
+ }
2598
+ /**
2599
+ * Set master volume.
2600
+ */
2601
+ setMasterVolume(volume) {
2602
+ this.masterVolume = Math.max(0, Math.min(1, volume));
2603
+ }
2604
+ getMasterVolume() {
2605
+ return this.masterVolume;
2606
+ }
2607
+ /**
2608
+ * Mute/unmute all audio.
2609
+ */
2610
+ setMasterMuted(muted) {
2611
+ this.masterMuted = muted;
2612
+ }
2613
+ isMasterMuted() {
2614
+ return this.masterMuted;
2615
+ }
2616
+ /**
2617
+ * List all channels.
2618
+ */
2619
+ getChannels() {
2620
+ return Array.from(this.channels.values());
2621
+ }
2622
+ /**
2623
+ * Mute a group of channels.
2624
+ */
2625
+ muteGroup(channelNames) {
2626
+ for (const name of channelNames) {
2627
+ this.setChannelMuted(name, true);
2628
+ }
2629
+ }
2630
+ /**
2631
+ * Unmute a group of channels.
2632
+ */
2633
+ unmuteGroup(channelNames) {
2634
+ for (const name of channelNames) {
2635
+ this.setChannelMuted(name, false);
2636
+ }
2637
+ }
2638
+ // =========================================================================
2639
+ // DUCKING SYSTEM
2640
+ // =========================================================================
2641
+ /**
2642
+ * Configure ducking for a channel.
2643
+ * Example: Duck music and ambient when voice plays.
2644
+ */
2645
+ configureDucking(config) {
2646
+ const duckId = `${config.triggerChannel}_ducks_${config.targetChannels.join("_")}`;
2647
+ this.duckingStates.set(duckId, {
2648
+ config,
2649
+ isActive: false,
2650
+ currentDuckAmount: 0,
2651
+ targetDuckAmount: 0
2652
+ });
2653
+ }
2654
+ /**
2655
+ * Remove ducking configuration.
2656
+ */
2657
+ removeDucking(triggerChannel, targetChannels) {
2658
+ const duckId = `${triggerChannel}_ducks_${targetChannels.join("_")}`;
2659
+ this.duckingStates.delete(duckId);
2660
+ }
2661
+ /**
2662
+ * Update ducking state. Call every frame with delta time.
2663
+ */
2664
+ updateDucking(delta) {
2665
+ for (const [_id, state] of this.duckingStates) {
2666
+ if (!state.config.enabled) continue;
2667
+ const triggerActive = this.isChannelActiveAboveThreshold(
2668
+ state.config.triggerChannel,
2669
+ state.config.threshold
2670
+ );
2671
+ state.targetDuckAmount = triggerActive ? state.config.ratio : 0;
2672
+ const speed = triggerActive ? 1 / state.config.attackTime * delta : 1 / state.config.releaseTime * delta;
2673
+ if (state.currentDuckAmount < state.targetDuckAmount) {
2674
+ state.currentDuckAmount = Math.min(state.currentDuckAmount + speed, state.targetDuckAmount);
2675
+ } else if (state.currentDuckAmount > state.targetDuckAmount) {
2676
+ state.currentDuckAmount = Math.max(state.currentDuckAmount - speed, state.targetDuckAmount);
2677
+ }
2678
+ state.isActive = state.currentDuckAmount > 0.01;
2679
+ if (state.isActive) {
2680
+ for (const targetChannel of state.config.targetChannels) {
2681
+ const ch = this.channels.get(targetChannel);
2682
+ if (ch) {
2683
+ }
2684
+ }
2685
+ }
2686
+ }
2687
+ }
2688
+ /**
2689
+ * Get ducking attenuation for a channel (0-1, where 0 = no attenuation, 1 = full duck).
2690
+ */
2691
+ getDuckingAttenuation(channelName) {
2692
+ let maxDuck = 0;
2693
+ for (const state of this.duckingStates.values()) {
2694
+ if (!state.config.enabled || !state.isActive) continue;
2695
+ if (state.config.targetChannels.includes(channelName)) {
2696
+ maxDuck = Math.max(maxDuck, state.currentDuckAmount);
2697
+ }
2698
+ }
2699
+ return maxDuck;
2700
+ }
2701
+ /**
2702
+ * Check if a channel has active sources above a dB threshold.
2703
+ */
2704
+ isChannelActiveAboveThreshold(channelName, thresholdDb) {
2705
+ const sources = Array.from(this.activeSources.values()).filter(
2706
+ (s) => s.channel === channelName
2707
+ );
2708
+ if (sources.length === 0) return false;
2709
+ const maxVolume = Math.max(...sources.map((s) => s.volume));
2710
+ const db = 20 * Math.log10(maxVolume + 1e-4);
2711
+ return db >= thresholdDb;
2712
+ }
2713
+ // =========================================================================
2714
+ // SIDECHAIN COMPRESSION
2715
+ // =========================================================================
2716
+ /**
2717
+ * Configure sidechain compression.
2718
+ * Example: Compress SFX when music peaks.
2719
+ */
2720
+ configureSidechain(config) {
2721
+ const scId = `${config.sourceChannel}_to_${config.targetChannel}`;
2722
+ this.sidechains.set(scId, config);
2723
+ }
2724
+ /**
2725
+ * Remove sidechain configuration.
2726
+ */
2727
+ removeSidechain(sourceChannel, targetChannel) {
2728
+ const scId = `${sourceChannel}_to_${targetChannel}`;
2729
+ this.sidechains.delete(scId);
2730
+ }
2731
+ // =========================================================================
2732
+ // VOICE STEALING
2733
+ // =========================================================================
2734
+ /**
2735
+ * Set voice stealing strategy.
2736
+ */
2737
+ setVoiceStealingStrategy(strategy) {
2738
+ this.voiceStealingStrategy = strategy;
2739
+ }
2740
+ /**
2741
+ * Register a new audio source. Returns true if allowed, false if stolen.
2742
+ */
2743
+ registerSource(source) {
2744
+ const ch = this.channels.get(source.channel);
2745
+ if (!ch) return false;
2746
+ if (ch.maxVoices > 0 && ch.currentVoices >= ch.maxVoices) {
2747
+ const stolen = this.stealVoice(source.channel, source.priority);
2748
+ if (!stolen) return false;
2749
+ }
2750
+ this.activeSources.set(source.id, source);
2751
+ ch.currentVoices++;
2752
+ return true;
2753
+ }
2754
+ /**
2755
+ * Unregister an audio source.
2756
+ */
2757
+ unregisterSource(sourceId) {
2758
+ const source = this.activeSources.get(sourceId);
2759
+ if (!source) return;
2760
+ const ch = this.channels.get(source.channel);
2761
+ if (ch) {
2762
+ ch.currentVoices = Math.max(0, ch.currentVoices - 1);
2763
+ }
2764
+ this.activeSources.delete(sourceId);
2765
+ }
2766
+ /**
2767
+ * Steal a voice from a channel based on strategy.
2768
+ * Returns the ID of the stolen source, or null if no voice could be stolen.
2769
+ */
2770
+ stealVoice(channelName, newPriority) {
2771
+ const sources = Array.from(this.activeSources.values()).filter(
2772
+ (s) => s.channel === channelName
2773
+ );
2774
+ if (sources.length === 0) return null;
2775
+ let victimSource = null;
2776
+ switch (this.voiceStealingStrategy.mode) {
2777
+ case "oldest":
2778
+ victimSource = sources.reduce((oldest, s) => s.startTime < oldest.startTime ? s : oldest);
2779
+ break;
2780
+ case "quietest":
2781
+ victimSource = sources.reduce((quietest, s) => s.volume < quietest.volume ? s : quietest);
2782
+ break;
2783
+ case "lowest_priority":
2784
+ victimSource = sources.reduce((lowest, s) => s.priority < lowest.priority ? s : lowest);
2785
+ if (victimSource.priority >= newPriority) return null;
2786
+ break;
2787
+ }
2788
+ if (victimSource) {
2789
+ this.unregisterSource(victimSource.id);
2790
+ return victimSource.id;
2791
+ }
2792
+ return null;
2793
+ }
2794
+ /**
2795
+ * Get active source count for a channel.
2796
+ */
2797
+ getChannelVoiceCount(channelName) {
2798
+ return this.channels.get(channelName)?.currentVoices ?? 0;
2799
+ }
2800
+ /**
2801
+ * Get all active sources for a channel.
2802
+ */
2803
+ getChannelSources(channelName) {
2804
+ return Array.from(this.activeSources.values()).filter((s) => s.channel === channelName);
2805
+ }
2806
+ // =========================================================================
2807
+ // CONTEXT-AWARE MIXING
2808
+ // =========================================================================
2809
+ /**
2810
+ * Set mixing context (e.g., 'combat', 'dialogue', 'ambient').
2811
+ * Automatically adjusts channel volumes based on context.
2812
+ */
2813
+ setMixingContext(context) {
2814
+ this.currentContext = context;
2815
+ if (context) {
2816
+ for (const [channelName, volume] of Object.entries(context.channelVolumes)) {
2817
+ this.setChannelVolume(channelName, volume);
2818
+ }
2819
+ }
2820
+ }
2821
+ /**
2822
+ * Get current mixing context.
2823
+ */
2824
+ getMixingContext() {
2825
+ return this.currentContext;
2826
+ }
2827
+ // =========================================================================
2828
+ // ENHANCED VOLUME CALCULATION
2829
+ // =========================================================================
2830
+ /**
2831
+ * Compute the effective volume for a source on a given channel.
2832
+ * Now includes ducking, sidechain, and context-aware adjustments.
2833
+ */
2834
+ getEffectiveVolume(channelName, sourceVolume) {
2835
+ if (this.masterMuted) return 0;
2836
+ const ch = this.channels.get(channelName);
2837
+ if (!ch || ch.muted) return 0;
2838
+ let effectiveVolume = sourceVolume * ch.volume * this.masterVolume;
2839
+ const duckAmount = this.getDuckingAttenuation(channelName);
2840
+ if (duckAmount > 0) {
2841
+ effectiveVolume *= 1 - duckAmount;
2842
+ }
2843
+ return effectiveVolume;
2844
+ }
2845
+ };
2846
+
2847
+ // src/audio/AudioOcclusion.ts
2848
+ var OCCLUSION_MATERIALS = {
2849
+ glass: {
2850
+ id: "glass",
2851
+ name: "Glass",
2852
+ absorptionCoefficient: 0.1,
2853
+ transmissionLoss: 6,
2854
+ frequencyAbsorption: {
2855
+ 125: 0.35,
2856
+ 250: 0.25,
2857
+ 500: 0.18,
2858
+ 1e3: 0.12,
2859
+ 2e3: 0.07,
2860
+ 4e3: 0.04,
2861
+ 8e3: 0.03
2862
+ }
2863
+ },
2864
+ wood: {
2865
+ id: "wood",
2866
+ name: "Wood",
2867
+ absorptionCoefficient: 0.3,
2868
+ transmissionLoss: 12,
2869
+ frequencyAbsorption: {
2870
+ 125: 0.15,
2871
+ 250: 0.11,
2872
+ 500: 0.1,
2873
+ 1e3: 0.07,
2874
+ 2e3: 0.06,
2875
+ 4e3: 0.07,
2876
+ 8e3: 0.09
2877
+ }
2878
+ },
2879
+ drywall: {
2880
+ id: "drywall",
2881
+ name: "Drywall",
2882
+ absorptionCoefficient: 0.2,
2883
+ transmissionLoss: 10,
2884
+ frequencyAbsorption: {
2885
+ 125: 0.29,
2886
+ 250: 0.1,
2887
+ 500: 0.05,
2888
+ 1e3: 0.04,
2889
+ 2e3: 0.07,
2890
+ 4e3: 0.09,
2891
+ 8e3: 0.08
2892
+ }
2893
+ },
2894
+ brick: {
2895
+ id: "brick",
2896
+ name: "Brick",
2897
+ absorptionCoefficient: 0.4,
2898
+ transmissionLoss: 20,
2899
+ frequencyAbsorption: {
2900
+ 125: 0.03,
2901
+ 250: 0.03,
2902
+ 500: 0.03,
2903
+ 1e3: 0.04,
2904
+ 2e3: 0.05,
2905
+ 4e3: 0.07,
2906
+ 8e3: 0.07
2907
+ }
2908
+ },
2909
+ concrete: {
2910
+ id: "concrete",
2911
+ name: "Concrete",
2912
+ absorptionCoefficient: 0.5,
2913
+ transmissionLoss: 30,
2914
+ frequencyAbsorption: {
2915
+ 125: 0.01,
2916
+ 250: 0.01,
2917
+ 500: 0.02,
2918
+ 1e3: 0.02,
2919
+ 2e3: 0.02,
2920
+ 4e3: 0.03,
2921
+ 8e3: 0.04
2922
+ }
2923
+ },
2924
+ metal: {
2925
+ id: "metal",
2926
+ name: "Metal",
2927
+ absorptionCoefficient: 0.05,
2928
+ transmissionLoss: 35,
2929
+ frequencyAbsorption: {
2930
+ 125: 0.01,
2931
+ 250: 0.01,
2932
+ 500: 0.01,
2933
+ 1e3: 0.01,
2934
+ 2e3: 0.02,
2935
+ 4e3: 0.02,
2936
+ 8e3: 0.03
2937
+ }
2938
+ },
2939
+ fabric: {
2940
+ id: "fabric",
2941
+ name: "Fabric",
2942
+ absorptionCoefficient: 0.7,
2943
+ transmissionLoss: 3,
2944
+ frequencyAbsorption: {
2945
+ 125: 0.03,
2946
+ 250: 0.09,
2947
+ 500: 0.2,
2948
+ 1e3: 0.54,
2949
+ 2e3: 0.7,
2950
+ 4e3: 0.72,
2951
+ 8e3: 0.75
2952
+ }
2953
+ },
2954
+ water: {
2955
+ id: "water",
2956
+ name: "Water",
2957
+ absorptionCoefficient: 0.02,
2958
+ transmissionLoss: 8,
2959
+ frequencyAbsorption: {
2960
+ 125: 0.01,
2961
+ 250: 0.01,
2962
+ 500: 0.01,
2963
+ 1e3: 0.01,
2964
+ 2e3: 0.02,
2965
+ 4e3: 0.03,
2966
+ 8e3: 0.04
2967
+ }
2968
+ }
2969
+ };
2970
+ var AudioOcclusionSystem = class {
2971
+ materials = /* @__PURE__ */ new Map();
2972
+ raycastProvider = null;
2973
+ maxTransmissionLoss = 60;
2974
+ // dB cap
2975
+ cache = /* @__PURE__ */ new Map();
2976
+ enableFrequencyFiltering = true;
2977
+ // Enable frequency-dependent occlusion
2978
+ constructor() {
2979
+ for (const mat of Object.values(OCCLUSION_MATERIALS)) {
2980
+ this.materials.set(mat.id, mat);
2981
+ }
2982
+ }
2983
+ // ---------------------------------------------------------------------------
2984
+ // Configuration
2985
+ // ---------------------------------------------------------------------------
2986
+ setRaycastProvider(provider) {
2987
+ this.raycastProvider = provider;
2988
+ }
2989
+ registerMaterial(material) {
2990
+ this.materials.set(material.id, material);
2991
+ }
2992
+ getMaterial(id) {
2993
+ return this.materials.get(id);
2994
+ }
2995
+ setMaxTransmissionLoss(db) {
2996
+ this.maxTransmissionLoss = Math.max(0, db);
2997
+ }
2998
+ setFrequencyFilteringEnabled(enabled) {
2999
+ this.enableFrequencyFiltering = enabled;
3000
+ }
3001
+ isFrequencyFilteringEnabled() {
3002
+ return this.enableFrequencyFiltering;
3003
+ }
3004
+ // ---------------------------------------------------------------------------
3005
+ // Computation
3006
+ // ---------------------------------------------------------------------------
3007
+ /**
3008
+ * Compute occlusion between listener and a source.
3009
+ * Uses the registered raycast provider to detect obstacles.
3010
+ * Now includes frequency-dependent filtering for realistic muffled sound.
3011
+ */
3012
+ computeOcclusion(listenPos, sourcePos, sourceId) {
3013
+ if (!this.raycastProvider) {
3014
+ return {
3015
+ sourceId,
3016
+ occluded: false,
3017
+ occlusionFactor: 0,
3018
+ hitCount: 0,
3019
+ totalTransmissionLoss: 0,
3020
+ materials: [],
3021
+ lowPassCutoff: 22e3,
3022
+ frequencyAttenuation: {}
3023
+ };
3024
+ }
3025
+ const dx = sourcePos.x - listenPos.x;
3026
+ const dy = sourcePos.y - listenPos.y;
3027
+ const dz = sourcePos.z - listenPos.z;
3028
+ const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
3029
+ if (dist === 0) {
3030
+ return {
3031
+ sourceId,
3032
+ occluded: false,
3033
+ occlusionFactor: 0,
3034
+ hitCount: 0,
3035
+ totalTransmissionLoss: 0,
3036
+ materials: [],
3037
+ lowPassCutoff: 22e3,
3038
+ frequencyAttenuation: {}
3039
+ };
3040
+ }
3041
+ const ray = {
3042
+ origin: { ...listenPos },
3043
+ direction: { x: dx / dist, y: dy / dist, z: dz / dist },
3044
+ maxDistance: dist
3045
+ };
3046
+ const hits = this.raycastProvider(ray);
3047
+ if (hits.length === 0) {
3048
+ return {
3049
+ sourceId,
3050
+ occluded: false,
3051
+ occlusionFactor: 0,
3052
+ hitCount: 0,
3053
+ totalTransmissionLoss: 0,
3054
+ materials: [],
3055
+ lowPassCutoff: 22e3,
3056
+ frequencyAttenuation: {}
3057
+ };
3058
+ }
3059
+ let totalLoss = 0;
3060
+ const hitMaterials = [];
3061
+ const frequencyAttenuation = {};
3062
+ const freqBands = [125, 250, 500, 1e3, 2e3, 4e3, 8e3];
3063
+ for (const freq of freqBands) {
3064
+ frequencyAttenuation[freq] = 0;
3065
+ }
3066
+ for (const hit of hits) {
3067
+ const mat = this.materials.get(hit.materialId);
3068
+ if (mat) {
3069
+ totalLoss += mat.transmissionLoss;
3070
+ hitMaterials.push(hit.materialId);
3071
+ if (this.enableFrequencyFiltering && mat.frequencyAbsorption) {
3072
+ for (const freq of freqBands) {
3073
+ const absorp = mat.frequencyAbsorption[freq] || 0;
3074
+ frequencyAttenuation[freq] = Math.min(
3075
+ 1,
3076
+ (frequencyAttenuation[freq] || 0) + absorp * hit.thickness
3077
+ );
3078
+ }
3079
+ }
3080
+ }
3081
+ }
3082
+ totalLoss = Math.min(totalLoss, this.maxTransmissionLoss);
3083
+ const factor = totalLoss / this.maxTransmissionLoss;
3084
+ const lowPassCutoff = this.calculateLowPassCutoff(factor, frequencyAttenuation);
3085
+ const result = {
3086
+ sourceId,
3087
+ occluded: totalLoss > 0,
3088
+ occlusionFactor: factor,
3089
+ hitCount: hits.length,
3090
+ totalTransmissionLoss: totalLoss,
3091
+ materials: hitMaterials,
3092
+ lowPassCutoff,
3093
+ frequencyAttenuation: this.enableFrequencyFiltering ? frequencyAttenuation : {}
3094
+ };
3095
+ this.cache.set(sourceId, result);
3096
+ return result;
3097
+ }
3098
+ /**
3099
+ * Compute without raycasting — direct manual hit specification.
3100
+ * Useful for testing or pre-computed occlusion.
3101
+ */
3102
+ computeFromHits(sourceId, hits) {
3103
+ let totalLoss = 0;
3104
+ const hitMaterials = [];
3105
+ for (const hit of hits) {
3106
+ const mat = this.materials.get(hit.materialId);
3107
+ if (mat) {
3108
+ totalLoss += mat.transmissionLoss;
3109
+ hitMaterials.push(hit.materialId);
3110
+ }
3111
+ }
3112
+ totalLoss = Math.min(totalLoss, this.maxTransmissionLoss);
3113
+ const factor = totalLoss / this.maxTransmissionLoss;
3114
+ return {
3115
+ sourceId,
3116
+ occluded: totalLoss > 0,
3117
+ occlusionFactor: factor,
3118
+ hitCount: hits.length,
3119
+ totalTransmissionLoss: totalLoss,
3120
+ materials: hitMaterials,
3121
+ lowPassCutoff: this.calculateLowPassCutoff(factor, {}),
3122
+ frequencyAttenuation: {}
3123
+ };
3124
+ }
3125
+ /**
3126
+ * Get the volume multiplier for a given occlusion factor.
3127
+ * Converts transmission loss to a linear volume reduction.
3128
+ */
3129
+ getVolumeMultiplier(occlusionFactor) {
3130
+ const dbLoss = occlusionFactor * this.maxTransmissionLoss;
3131
+ return Math.pow(10, -dbLoss / 20);
3132
+ }
3133
+ getCachedResult(sourceId) {
3134
+ return this.cache.get(sourceId);
3135
+ }
3136
+ clearCache() {
3137
+ this.cache.clear();
3138
+ }
3139
+ // ---------------------------------------------------------------------------
3140
+ // Frequency-Dependent Occlusion
3141
+ // ---------------------------------------------------------------------------
3142
+ /**
3143
+ * Calculate low-pass filter cutoff frequency based on occlusion.
3144
+ * Returns cutoff in Hz (500Hz fully occluded, 22kHz no occlusion).
3145
+ */
3146
+ calculateLowPassCutoff(occlusionFactor, frequencyAttenuation) {
3147
+ if (!this.enableFrequencyFiltering || Object.keys(frequencyAttenuation).length === 0) {
3148
+ const minCutoff = 500;
3149
+ const maxCutoff = 22e3;
3150
+ return maxCutoff - occlusionFactor * (maxCutoff - minCutoff);
3151
+ }
3152
+ const freqBands = [125, 250, 500, 1e3, 2e3, 4e3, 8e3];
3153
+ for (let i = freqBands.length - 1; i >= 0; i--) {
3154
+ const freq = freqBands[i];
3155
+ const atten = frequencyAttenuation[freq] || 0;
3156
+ if (atten < 0.5) {
3157
+ const nextFreq = i < freqBands.length - 1 ? freqBands[i + 1] : 22e3;
3158
+ return (freq + nextFreq) / 2;
3159
+ }
3160
+ }
3161
+ return 500;
3162
+ }
3163
+ /**
3164
+ * Get frequency attenuation for a specific source.
3165
+ * Returns per-band attenuation (0-1, where 1 = fully absorbed).
3166
+ */
3167
+ getFrequencyAttenuation(sourceId) {
3168
+ const result = this.cache.get(sourceId);
3169
+ return result?.frequencyAttenuation || {};
3170
+ }
3171
+ /**
3172
+ * Get low-pass cutoff for a specific source.
3173
+ */
3174
+ getLowPassCutoff(sourceId) {
3175
+ const result = this.cache.get(sourceId);
3176
+ return result?.lowPassCutoff || 22e3;
3177
+ }
3178
+ };
3179
+
3180
+ // src/audio/AudioPresets.ts
3181
+ var AudioPresets = {
3182
+ /** Close-range UI click/tap */
3183
+ uiClick: {
3184
+ volume: 0.6,
3185
+ spatialize: false,
3186
+ loop: false,
3187
+ channel: "ui",
3188
+ maxDistance: 5
3189
+ },
3190
+ /** UI hover feedback */
3191
+ uiHover: {
3192
+ volume: 0.3,
3193
+ spatialize: false,
3194
+ loop: false,
3195
+ channel: "ui"
3196
+ },
3197
+ /** Ambient environmental loop */
3198
+ ambientLoop: {
3199
+ volume: 0.4,
3200
+ spatialize: false,
3201
+ loop: true,
3202
+ channel: "ambient",
3203
+ maxDistance: 100
3204
+ },
3205
+ /** Spatial object interaction (grab, drop) */
3206
+ objectInteraction: {
3207
+ volume: 0.8,
3208
+ spatialize: true,
3209
+ loop: false,
3210
+ refDistance: 0.5,
3211
+ maxDistance: 10,
3212
+ rolloffFactor: 2,
3213
+ channel: "sfx"
3214
+ },
3215
+ /** Distant environmental sound */
3216
+ distantAmbient: {
3217
+ volume: 0.5,
3218
+ spatialize: true,
3219
+ loop: true,
3220
+ refDistance: 5,
3221
+ maxDistance: 100,
3222
+ rolloffFactor: 0.5,
3223
+ channel: "ambient"
3224
+ },
3225
+ /** Footstep / movement */
3226
+ footstep: {
3227
+ volume: 0.5,
3228
+ spatialize: true,
3229
+ loop: false,
3230
+ refDistance: 1,
3231
+ maxDistance: 15,
3232
+ rolloffFactor: 1.5,
3233
+ channel: "sfx"
3234
+ },
3235
+ /** Alert / notification */
3236
+ notification: {
3237
+ volume: 0.7,
3238
+ spatialize: false,
3239
+ loop: false,
3240
+ channel: "ui"
3241
+ },
3242
+ /** Music background */
3243
+ music: {
3244
+ volume: 0.3,
3245
+ spatialize: false,
3246
+ loop: true,
3247
+ channel: "music"
3248
+ }
3249
+ };
3250
+
3251
+ // src/audio/AudioTrait.ts
3252
+ var nodeAudioSources = /* @__PURE__ */ new Map();
3253
+ var sharedAudioEngine = null;
3254
+ function setSharedAudioEngine(engine) {
3255
+ sharedAudioEngine = engine;
3256
+ }
3257
+ function getSharedAudioEngine() {
3258
+ if (!sharedAudioEngine) {
3259
+ sharedAudioEngine = new AudioEngine();
3260
+ }
3261
+ return sharedAudioEngine;
3262
+ }
3263
+ var defaultConfig = {
3264
+ soundId: "",
3265
+ volume: 1,
3266
+ loop: false,
3267
+ spatialize: true,
3268
+ maxDistance: 50,
3269
+ refDistance: 1,
3270
+ rolloffFactor: 1,
3271
+ channel: "master",
3272
+ autoPlay: true,
3273
+ pitch: 1
3274
+ };
3275
+ var audioTraitHandler = {
3276
+ name: "audio",
3277
+ defaultConfig,
3278
+ onAttach(node, config, _context) {
3279
+ if (!config.soundId || !config.autoPlay) return;
3280
+ const engine = getSharedAudioEngine();
3281
+ const nodeId = node.id ?? "unknown";
3282
+ const pos = node.properties?.position || {
3283
+ x: 0,
3284
+ y: 0,
3285
+ z: 0
3286
+ };
3287
+ const sourceId = engine.play(config.soundId, {
3288
+ position: pos,
3289
+ volume: config.volume,
3290
+ pitch: config.pitch,
3291
+ loop: config.loop,
3292
+ maxDistance: config.maxDistance,
3293
+ refDistance: config.refDistance,
3294
+ rolloffFactor: config.rolloffFactor,
3295
+ spatialize: config.spatialize,
3296
+ channel: config.channel
3297
+ });
3298
+ nodeAudioSources.set(nodeId, sourceId);
3299
+ },
3300
+ onDetach(node, _config, _context) {
3301
+ const nodeId = node.id ?? "unknown";
3302
+ const sourceId = nodeAudioSources.get(nodeId);
3303
+ if (sourceId) {
3304
+ getSharedAudioEngine().stop(sourceId);
3305
+ nodeAudioSources.delete(nodeId);
3306
+ }
3307
+ },
3308
+ onUpdate(node, _config, _context, _delta) {
3309
+ const nodeId = node.id ?? "unknown";
3310
+ const sourceId = nodeAudioSources.get(nodeId);
3311
+ if (!sourceId) return;
3312
+ const pos = node.properties?.position || {
3313
+ x: 0,
3314
+ y: 0,
3315
+ z: 0
3316
+ };
3317
+ getSharedAudioEngine().setSourcePosition(sourceId, pos);
3318
+ }
3319
+ };
3320
+
3321
+ // src/audio/MusicGenerator.ts
3322
+ var SCALES = {
3323
+ major: [0, 2, 4, 5, 7, 9, 11],
3324
+ minor: [0, 2, 3, 5, 7, 8, 10],
3325
+ pentatonic: [0, 2, 4, 7, 9],
3326
+ blues: [0, 3, 5, 6, 7, 10],
3327
+ dorian: [0, 2, 3, 5, 7, 9, 10],
3328
+ mixolydian: [0, 2, 4, 5, 7, 9, 10]
3329
+ };
3330
+ var CHORD_INTERVALS = {
3331
+ major: [0, 4, 7],
3332
+ minor: [0, 3, 7],
3333
+ dim: [0, 3, 6],
3334
+ aug: [0, 4, 8],
3335
+ sus2: [0, 2, 7],
3336
+ sus4: [0, 5, 7],
3337
+ "7th": [0, 4, 7, 10]
3338
+ };
3339
+ var MusicGenerator = class {
3340
+ scale = "major";
3341
+ rootNote = 60;
3342
+ // Middle C
3343
+ bpm = 120;
3344
+ seed = 42;
3345
+ rng;
3346
+ constructor(seed = 42) {
3347
+ this.seed = seed;
3348
+ this.rng = this.createRng(seed);
3349
+ }
3350
+ // ---------------------------------------------------------------------------
3351
+ // Configuration
3352
+ // ---------------------------------------------------------------------------
3353
+ setScale(scale) {
3354
+ this.scale = scale;
3355
+ }
3356
+ setRoot(note) {
3357
+ this.rootNote = note;
3358
+ }
3359
+ setBPM(bpm) {
3360
+ this.bpm = bpm;
3361
+ }
3362
+ getScale() {
3363
+ return this.scale;
3364
+ }
3365
+ getBPM() {
3366
+ return this.bpm;
3367
+ }
3368
+ // ---------------------------------------------------------------------------
3369
+ // Scale Helpers
3370
+ // ---------------------------------------------------------------------------
3371
+ getScaleNotes(octaves = 2) {
3372
+ const intervals = SCALES[this.scale];
3373
+ const notes = [];
3374
+ for (let oct = 0; oct < octaves; oct++) {
3375
+ for (const interval of intervals) {
3376
+ notes.push(this.rootNote + oct * 12 + interval);
3377
+ }
3378
+ }
3379
+ return notes;
3380
+ }
3381
+ isInScale(note) {
3382
+ const relative = ((note - this.rootNote) % 12 + 12) % 12;
3383
+ return SCALES[this.scale].includes(relative);
3384
+ }
3385
+ // ---------------------------------------------------------------------------
3386
+ // Chord Generation
3387
+ // ---------------------------------------------------------------------------
3388
+ generateChord(scaleDegree, quality = "major", duration = 4) {
3389
+ const intervals = SCALES[this.scale];
3390
+ const rootOffset = intervals[(scaleDegree - 1) % intervals.length];
3391
+ const root = this.rootNote + rootOffset;
3392
+ const chordIntervals = CHORD_INTERVALS[quality];
3393
+ const notes = chordIntervals.map((i) => root + i);
3394
+ return { root, quality, notes, duration };
3395
+ }
3396
+ generateProgression(degrees, qualities) {
3397
+ return degrees.map((deg, i) => {
3398
+ const quality = qualities?.[i] ?? "major";
3399
+ return this.generateChord(deg, quality);
3400
+ });
3401
+ }
3402
+ // ---------------------------------------------------------------------------
3403
+ // Rhythm Generation
3404
+ // ---------------------------------------------------------------------------
3405
+ generateRhythm(beats, density = 0.5, subdivision = 4) {
3406
+ const totalSlots = beats * subdivision;
3407
+ const hitSlots = [];
3408
+ for (let i = 0; i < totalSlots; i++) {
3409
+ hitSlots.push(this.rng() < density);
3410
+ }
3411
+ hitSlots[0] = true;
3412
+ return { name: "generated", beats: hitSlots, subdivision, swing: 0 };
3413
+ }
3414
+ // ---------------------------------------------------------------------------
3415
+ // Melody Generation
3416
+ // ---------------------------------------------------------------------------
3417
+ generateMelody(bars, noteDensity = 0.6) {
3418
+ const scaleNotes = this.getScaleNotes(2);
3419
+ const melody = [];
3420
+ const beatsPerBar = 4;
3421
+ const totalBeats = bars * beatsPerBar;
3422
+ let currentTime = 0;
3423
+ let lastNoteIndex = Math.floor(scaleNotes.length / 2);
3424
+ while (currentTime < totalBeats) {
3425
+ if (this.rng() < noteDensity) {
3426
+ const step = this.rng() < 0.7 ? this.rng() < 0.5 ? -1 : 1 : Math.floor(this.rng() * 5) - 2;
3427
+ lastNoteIndex = Math.max(0, Math.min(scaleNotes.length - 1, lastNoteIndex + step));
3428
+ const durations = [0.25, 0.5, 1, 2];
3429
+ const duration = durations[Math.floor(this.rng() * durations.length)];
3430
+ const velocity = 0.5 + this.rng() * 0.5;
3431
+ melody.push({
3432
+ pitch: scaleNotes[lastNoteIndex],
3433
+ duration,
3434
+ velocity,
3435
+ time: currentTime
3436
+ });
3437
+ currentTime += duration;
3438
+ } else {
3439
+ currentTime += 0.5;
3440
+ }
3441
+ }
3442
+ return melody;
3443
+ }
3444
+ // ---------------------------------------------------------------------------
3445
+ // Seeded RNG
3446
+ // ---------------------------------------------------------------------------
3447
+ createRng(seed) {
3448
+ let s = seed;
3449
+ return () => {
3450
+ s = s * 1664525 + 1013904223 & 2147483647;
3451
+ return s / 2147483647;
3452
+ };
3453
+ }
3454
+ reseed(seed) {
3455
+ this.seed = seed;
3456
+ this.rng = this.createRng(seed);
3457
+ }
3458
+ };
3459
+
3460
+ // src/audio/SoundPool.ts
3461
+ var SoundPool = class {
3462
+ sounds = /* @__PURE__ */ new Map();
3463
+ /**
3464
+ * Register a sound definition.
3465
+ */
3466
+ register(sound) {
3467
+ this.sounds.set(sound.id, sound);
3468
+ }
3469
+ /**
3470
+ * Register multiple sounds.
3471
+ */
3472
+ registerAll(sounds) {
3473
+ for (const s of sounds) this.register(s);
3474
+ }
3475
+ /**
3476
+ * Get a sound definition by ID.
3477
+ */
3478
+ get(id) {
3479
+ return this.sounds.get(id);
3480
+ }
3481
+ /**
3482
+ * Check if a sound is registered.
3483
+ */
3484
+ has(id) {
3485
+ return this.sounds.has(id);
3486
+ }
3487
+ /**
3488
+ * Get all sounds in a category.
3489
+ */
3490
+ getByCategory(category) {
3491
+ return Array.from(this.sounds.values()).filter((s) => s.category === category);
3492
+ }
3493
+ /**
3494
+ * Get a random sound from a category (for variation).
3495
+ */
3496
+ getRandomFromCategory(category) {
3497
+ const sounds = this.getByCategory(category);
3498
+ if (sounds.length === 0) return void 0;
3499
+ return sounds[Math.floor(Math.random() * sounds.length)];
3500
+ }
3501
+ /**
3502
+ * Get count of registered sounds.
3503
+ */
3504
+ get count() {
3505
+ return this.sounds.size;
3506
+ }
3507
+ /**
3508
+ * List all registered sound IDs.
3509
+ */
3510
+ listIds() {
3511
+ return Array.from(this.sounds.keys());
3512
+ }
3513
+ };
3514
+
3515
+ // src/audio/SpatialAudioSource.ts
3516
+ var SpatialAudioSource = class {
3517
+ config;
3518
+ playing = false;
3519
+ paused = false;
3520
+ time = 0;
3521
+ clipDuration = 0;
3522
+ gain = 1;
3523
+ // Computed gain after attenuation
3524
+ pan = 0;
3525
+ // -1 (left) to 1 (right)
3526
+ constructor(config) {
3527
+ this.config = {
3528
+ position: [0, 0, 0],
3529
+ velocity: { x: 0, y: 0, z: 0 },
3530
+ rolloff: "inverse",
3531
+ minDistance: 3,
3532
+ // Tuned for voice: audible within 3m
3533
+ maxDistance: 100,
3534
+ volume: 1,
3535
+ pitch: 1,
3536
+ loop: false,
3537
+ cone: null,
3538
+ dopplerFactor: 1,
3539
+ rolloffFactor: 1,
3540
+ spatialBlend: 1,
3541
+ ...config
3542
+ };
3543
+ }
3544
+ // ---------------------------------------------------------------------------
3545
+ // Playback
3546
+ // ---------------------------------------------------------------------------
3547
+ play(duration = 5) {
3548
+ this.playing = true;
3549
+ this.paused = false;
3550
+ this.time = 0;
3551
+ this.clipDuration = duration;
3552
+ }
3553
+ stop() {
3554
+ this.playing = false;
3555
+ this.paused = false;
3556
+ this.time = 0;
3557
+ }
3558
+ pause() {
3559
+ if (this.playing) this.paused = true;
3560
+ }
3561
+ resume() {
3562
+ this.paused = false;
3563
+ }
3564
+ isPlaying() {
3565
+ return this.playing && !this.paused;
3566
+ }
3567
+ // ---------------------------------------------------------------------------
3568
+ // Position / Properties
3569
+ // ---------------------------------------------------------------------------
3570
+ setPosition(x, y, z) {
3571
+ this.config.position = { x, y, z };
3572
+ }
3573
+ getPosition() {
3574
+ return { ...this.config.position };
3575
+ }
3576
+ setVelocity(x, y, z) {
3577
+ this.config.velocity = { x, y, z };
3578
+ }
3579
+ setVolume(v) {
3580
+ this.config.volume = Math.max(0, Math.min(1, v));
3581
+ }
3582
+ getVolume() {
3583
+ return this.config.volume;
3584
+ }
3585
+ setPitch(p) {
3586
+ this.config.pitch = Math.max(0.1, p);
3587
+ }
3588
+ setLoop(loop) {
3589
+ this.config.loop = loop;
3590
+ }
3591
+ setRolloff(model) {
3592
+ this.config.rolloff = model;
3593
+ }
3594
+ setMinDistance(d) {
3595
+ this.config.minDistance = Math.max(0.01, d);
3596
+ }
3597
+ setMaxDistance(d) {
3598
+ this.config.maxDistance = Math.max(this.config.minDistance, d);
3599
+ }
3600
+ setSpatialBlend(blend) {
3601
+ this.config.spatialBlend = Math.max(0, Math.min(1, blend));
3602
+ }
3603
+ setCone(cone) {
3604
+ this.config.cone = { ...cone };
3605
+ }
3606
+ // ---------------------------------------------------------------------------
3607
+ // Update
3608
+ // ---------------------------------------------------------------------------
3609
+ update(dt, listenerPos) {
3610
+ if (!this.playing || this.paused) return;
3611
+ this.time += dt * this.config.pitch;
3612
+ if (this.time >= this.clipDuration) {
3613
+ if (this.config.loop) {
3614
+ this.time %= this.clipDuration;
3615
+ } else {
3616
+ this.playing = false;
3617
+ return;
3618
+ }
3619
+ }
3620
+ const dx = this.config.position.x - listenerPos.x;
3621
+ const dy = this.config.position.y - listenerPos.y;
3622
+ const dz = this.config.position.z - listenerPos.z;
3623
+ const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
3624
+ this.gain = this.computeAttenuation(distance) * this.config.volume;
3625
+ if (distance > 1e-3) {
3626
+ this.pan = Math.max(-1, Math.min(1, dx / distance)) * this.config.spatialBlend;
3627
+ } else {
3628
+ this.pan = 0;
3629
+ }
3630
+ if (this.config.cone) {
3631
+ this.gain *= this.computeConeGain(listenerPos);
3632
+ }
3633
+ }
3634
+ // ---------------------------------------------------------------------------
3635
+ // Attenuation
3636
+ // ---------------------------------------------------------------------------
3637
+ computeAttenuation(distance) {
3638
+ const min = this.config.minDistance;
3639
+ const max = this.config.maxDistance;
3640
+ const clamped = Math.max(min, Math.min(max, distance));
3641
+ switch (this.config.rolloff) {
3642
+ case "linear": {
3643
+ return 1 - (clamped - min) / (max - min);
3644
+ }
3645
+ case "inverse": {
3646
+ return min / (min + this.config.rolloffFactor * (clamped - min));
3647
+ }
3648
+ case "exponential": {
3649
+ return Math.pow(clamped / min, -this.config.rolloffFactor);
3650
+ }
3651
+ default:
3652
+ return 1;
3653
+ }
3654
+ }
3655
+ computeConeGain(listenerPos) {
3656
+ if (!this.config.cone) return 1;
3657
+ const dx = listenerPos.x - this.config.position.x;
3658
+ const dz = listenerPos.z - this.config.position.z;
3659
+ const angle = Math.abs(Math.atan2(dz, dx) * (180 / Math.PI));
3660
+ const halfInner = this.config.cone.innerAngle / 2;
3661
+ const halfOuter = this.config.cone.outerAngle / 2;
3662
+ if (angle <= halfInner) return 1;
3663
+ if (angle >= halfOuter) return this.config.cone.outerGain;
3664
+ const t = (angle - halfInner) / (halfOuter - halfInner);
3665
+ return 1 - t * (1 - this.config.cone.outerGain);
3666
+ }
3667
+ // ---------------------------------------------------------------------------
3668
+ // Queries
3669
+ // ---------------------------------------------------------------------------
3670
+ getGain() {
3671
+ return this.gain;
3672
+ }
3673
+ getPan() {
3674
+ return this.pan;
3675
+ }
3676
+ getTime() {
3677
+ return this.time;
3678
+ }
3679
+ getConfig() {
3680
+ return {
3681
+ ...this.config,
3682
+ position: { ...this.config.position },
3683
+ velocity: { ...this.config.velocity }
3684
+ };
3685
+ }
3686
+ };
3687
+
3688
+ // src/audio/SpatialAudioZone.ts
3689
+ var REVERB_PRESETS = {
3690
+ outdoor: {
3691
+ name: "outdoor",
3692
+ decay: 0.3,
3693
+ density: 0.2,
3694
+ diffusion: 0.1,
3695
+ wetLevel: 0.1,
3696
+ earlyReflections: 5
3697
+ },
3698
+ room: {
3699
+ name: "room",
3700
+ decay: 0.8,
3701
+ density: 0.5,
3702
+ diffusion: 0.5,
3703
+ wetLevel: 0.3,
3704
+ earlyReflections: 10
3705
+ },
3706
+ hall: {
3707
+ name: "hall",
3708
+ decay: 2,
3709
+ density: 0.7,
3710
+ diffusion: 0.8,
3711
+ wetLevel: 0.5,
3712
+ earlyReflections: 20
3713
+ },
3714
+ cathedral: {
3715
+ name: "cathedral",
3716
+ decay: 4.5,
3717
+ density: 0.9,
3718
+ diffusion: 0.9,
3719
+ wetLevel: 0.7,
3720
+ earlyReflections: 40
3721
+ },
3722
+ cave: {
3723
+ name: "cave",
3724
+ decay: 3,
3725
+ density: 0.8,
3726
+ diffusion: 0.6,
3727
+ wetLevel: 0.6,
3728
+ earlyReflections: 30
3729
+ },
3730
+ underwater: {
3731
+ name: "underwater",
3732
+ decay: 1.5,
3733
+ density: 1,
3734
+ diffusion: 1,
3735
+ wetLevel: 0.9,
3736
+ earlyReflections: 5
3737
+ }
3738
+ };
3739
+ var SpatialAudioZoneSystem = class {
3740
+ zones = /* @__PURE__ */ new Map();
3741
+ portals = /* @__PURE__ */ new Map();
3742
+ activeZones = /* @__PURE__ */ new Map();
3743
+ listenerPos = { x: 0, y: 0, z: 0 };
3744
+ // ---------------------------------------------------------------------------
3745
+ // Zone Management
3746
+ // ---------------------------------------------------------------------------
3747
+ addZone(config) {
3748
+ this.zones.set(config.id, config);
3749
+ }
3750
+ removeZone(zoneId) {
3751
+ this.zones.delete(zoneId);
3752
+ this.activeZones.delete(zoneId);
3753
+ }
3754
+ getZone(zoneId) {
3755
+ return this.zones.get(zoneId);
3756
+ }
3757
+ getZoneCount() {
3758
+ return this.zones.size;
3759
+ }
3760
+ // ---------------------------------------------------------------------------
3761
+ // Portals
3762
+ // ---------------------------------------------------------------------------
3763
+ addPortal(portal) {
3764
+ this.portals.set(portal.id, portal);
3765
+ }
3766
+ removePortal(portalId) {
3767
+ this.portals.delete(portalId);
3768
+ }
3769
+ // ---------------------------------------------------------------------------
3770
+ // Update
3771
+ // ---------------------------------------------------------------------------
3772
+ updateListenerPosition(pos) {
3773
+ this.listenerPos = { ...pos };
3774
+ this.recalculate();
3775
+ }
3776
+ recalculate() {
3777
+ this.activeZones.clear();
3778
+ for (const [id, zone] of this.zones) {
3779
+ const distance = this.distanceToZone(zone);
3780
+ const isInside = distance <= 0;
3781
+ let blendWeight = 0;
3782
+ if (isInside) {
3783
+ blendWeight = 1;
3784
+ } else if (distance < zone.fadeDistance) {
3785
+ blendWeight = 1 - distance / zone.fadeDistance;
3786
+ }
3787
+ if (blendWeight > 0) {
3788
+ this.activeZones.set(id, { zoneId: id, blendWeight, isInside });
3789
+ }
3790
+ }
3791
+ }
3792
+ distanceToZone(zone) {
3793
+ const dx = this.listenerPos.x - zone.position.x;
3794
+ const dy = this.listenerPos.y - zone.position.y;
3795
+ const dz = this.listenerPos.z - zone.position.z;
3796
+ if (zone.shape === "sphere") {
3797
+ const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
3798
+ return Math.max(0, dist - zone.size.x);
3799
+ }
3800
+ const cx = Math.max(0, Math.abs(dx) - zone.size.x);
3801
+ const cy = Math.max(0, Math.abs(dy) - zone.size.y);
3802
+ const cz = Math.max(0, Math.abs(dz) - zone.size.z);
3803
+ return Math.sqrt(cx * cx + cy * cy + cz * cz);
3804
+ }
3805
+ // ---------------------------------------------------------------------------
3806
+ // Queries
3807
+ // ---------------------------------------------------------------------------
3808
+ getActiveZones() {
3809
+ return [...this.activeZones.values()].sort((a, b) => {
3810
+ const za = this.zones.get(a.zoneId);
3811
+ const zb = this.zones.get(b.zoneId);
3812
+ return zb.priority - za.priority;
3813
+ });
3814
+ }
3815
+ /**
3816
+ * Returns the highest-priority active reverb preset, blended by weight.
3817
+ */
3818
+ getEffectiveReverb() {
3819
+ const active = this.getActiveZones();
3820
+ if (active.length === 0) return null;
3821
+ const primary = active[0];
3822
+ const zone = this.zones.get(primary.zoneId);
3823
+ if (active.length === 1 || primary.blendWeight >= 1) {
3824
+ return { ...zone.reverb };
3825
+ }
3826
+ const secondary = active[1];
3827
+ const zone2 = this.zones.get(secondary.zoneId);
3828
+ const w = primary.blendWeight;
3829
+ return {
3830
+ name: `${zone.reverb.name}+${zone2.reverb.name}`,
3831
+ decay: zone.reverb.decay * w + zone2.reverb.decay * (1 - w),
3832
+ density: zone.reverb.density * w + zone2.reverb.density * (1 - w),
3833
+ diffusion: zone.reverb.diffusion * w + zone2.reverb.diffusion * (1 - w),
3834
+ wetLevel: zone.reverb.wetLevel * w + zone2.reverb.wetLevel * (1 - w),
3835
+ earlyReflections: zone.reverb.earlyReflections * w + zone2.reverb.earlyReflections * (1 - w)
3836
+ };
3837
+ }
3838
+ isListenerInsideZone(zoneId) {
3839
+ return this.activeZones.get(zoneId)?.isInside ?? false;
3840
+ }
3841
+ /**
3842
+ * Get portal attenuation between two zones.
3843
+ */
3844
+ getPortalAttenuation(fromZone, toZone) {
3845
+ for (const portal of this.portals.values()) {
3846
+ if (portal.fromZoneId === fromZone && portal.toZoneId === toZone || portal.fromZoneId === toZone && portal.toZoneId === fromZone) {
3847
+ return portal.attenuation;
3848
+ }
3849
+ }
3850
+ return 0;
3851
+ }
3852
+ };
3853
+
3854
+ // src/audio/SynthEngine.ts
3855
+ var _oscId = 0;
3856
+ var SynthEngine = class {
3857
+ voices = /* @__PURE__ */ new Map();
3858
+ maxPolyphony = 16;
3859
+ masterVolume = 1;
3860
+ filter = null;
3861
+ sampleRate = 44100;
3862
+ // ---------------------------------------------------------------------------
3863
+ // Voice Management
3864
+ // ---------------------------------------------------------------------------
3865
+ noteOn(frequency, waveform = "sine", envelope) {
3866
+ if (this.voices.size >= this.maxPolyphony) {
3867
+ const oldest = [...this.voices.entries()].sort((a, b) => a[1].elapsed - b[1].elapsed).pop();
3868
+ if (oldest) this.voices.delete(oldest[0]);
3869
+ }
3870
+ const id = `voice_${_oscId++}`;
3871
+ const osc = {
3872
+ id,
3873
+ waveform,
3874
+ frequency,
3875
+ amplitude: 1,
3876
+ phase: 0,
3877
+ detune: 0,
3878
+ envelope: { attack: 0.01, decay: 0.1, sustain: 0.7, release: 0.3, ...envelope }
3879
+ };
3880
+ this.voices.set(id, {
3881
+ id,
3882
+ oscillator: osc,
3883
+ noteOn: true,
3884
+ noteOnTime: 0,
3885
+ noteOffTime: -1,
3886
+ currentAmplitude: 0,
3887
+ elapsed: 0
3888
+ });
3889
+ return id;
3890
+ }
3891
+ noteOff(id) {
3892
+ const voice = this.voices.get(id);
3893
+ if (voice) {
3894
+ voice.noteOn = false;
3895
+ voice.noteOffTime = voice.elapsed;
3896
+ }
3897
+ }
3898
+ // ---------------------------------------------------------------------------
3899
+ // Sample Generation
3900
+ // ---------------------------------------------------------------------------
3901
+ generateSample(time) {
3902
+ let sample = 0;
3903
+ for (const voice of this.voices.values()) {
3904
+ const osc = voice.oscillator;
3905
+ const env = this.computeEnvelope(voice);
3906
+ voice.currentAmplitude = env;
3907
+ if (env <= 1e-3 && !voice.noteOn) {
3908
+ continue;
3909
+ }
3910
+ const freq = osc.frequency * Math.pow(2, osc.detune / 1200);
3911
+ const t = time + osc.phase;
3912
+ let wave = 0;
3913
+ switch (osc.waveform) {
3914
+ case "sine":
3915
+ wave = Math.sin(2 * Math.PI * freq * t);
3916
+ break;
3917
+ case "square":
3918
+ wave = Math.sin(2 * Math.PI * freq * t) >= 0 ? 1 : -1;
3919
+ break;
3920
+ case "saw":
3921
+ wave = 2 * (freq * t % 1) - 1;
3922
+ break;
3923
+ case "triangle":
3924
+ wave = 2 * Math.abs(2 * (freq * t % 1) - 1) - 1;
3925
+ break;
3926
+ case "noise":
3927
+ wave = Math.random() * 2 - 1;
3928
+ break;
3929
+ default:
3930
+ wave = 0;
3931
+ }
3932
+ sample += wave * osc.amplitude * env;
3933
+ }
3934
+ return Math.max(-1, Math.min(1, sample * this.masterVolume));
3935
+ }
3936
+ computeEnvelope(voice) {
3937
+ const env = voice.oscillator.envelope;
3938
+ const t = voice.elapsed;
3939
+ if (voice.noteOn) {
3940
+ if (t < env.attack) return t / env.attack;
3941
+ const decayT = t - env.attack;
3942
+ if (decayT < env.decay) return 1 - (1 - env.sustain) * (decayT / env.decay);
3943
+ return env.sustain;
3944
+ } else {
3945
+ const releaseT = voice.elapsed - voice.noteOffTime;
3946
+ if (releaseT >= env.release) return 0;
3947
+ return env.sustain * (1 - releaseT / env.release);
3948
+ }
3949
+ }
3950
+ // ---------------------------------------------------------------------------
3951
+ // Update
3952
+ // ---------------------------------------------------------------------------
3953
+ update(dt) {
3954
+ const toRemove = [];
3955
+ for (const [id, voice] of this.voices) {
3956
+ voice.elapsed += dt;
3957
+ if (!voice.noteOn && voice.currentAmplitude <= 1e-3) {
3958
+ toRemove.push(id);
3959
+ }
3960
+ }
3961
+ for (const id of toRemove) this.voices.delete(id);
3962
+ }
3963
+ // ---------------------------------------------------------------------------
3964
+ // Config
3965
+ // ---------------------------------------------------------------------------
3966
+ setMasterVolume(vol) {
3967
+ this.masterVolume = Math.max(0, Math.min(1, vol));
3968
+ }
3969
+ setMaxPolyphony(n) {
3970
+ this.maxPolyphony = n;
3971
+ }
3972
+ setFilter(filter) {
3973
+ this.filter = filter;
3974
+ }
3975
+ getFilter() {
3976
+ return this.filter;
3977
+ }
3978
+ // ---------------------------------------------------------------------------
3979
+ // Queries
3980
+ // ---------------------------------------------------------------------------
3981
+ getActiveVoiceCount() {
3982
+ return this.voices.size;
3983
+ }
3984
+ getVoice(id) {
3985
+ return this.voices.get(id);
3986
+ }
3987
+ getMasterVolume() {
3988
+ return this.masterVolume;
3989
+ }
3990
+ };
3991
+
3992
+ // src/audio/VoiceManager.ts
3993
+ var VoiceManager = class {
3994
+ constructor(input, transport) {
3995
+ this.input = input;
3996
+ this.transport = transport;
3997
+ }
3998
+ input;
3999
+ transport;
4000
+ isPttPressed = false;
4001
+ isMuted = false;
4002
+ mutedPeers = /* @__PURE__ */ new Set();
4003
+ update() {
4004
+ }
4005
+ /**
4006
+ * Call this when input state changes
4007
+ */
4008
+ setPushToTalkState(pressed) {
4009
+ this.isPttPressed = pressed;
4010
+ this.updateMicState();
4011
+ }
4012
+ toggleMute() {
4013
+ this.isMuted = !this.isMuted;
4014
+ this.updateMicState();
4015
+ }
4016
+ mutePeer(peerId, muted) {
4017
+ if (muted) {
4018
+ this.mutedPeers.add(peerId);
4019
+ } else {
4020
+ this.mutedPeers.delete(peerId);
4021
+ }
4022
+ }
4023
+ updateMicState() {
4024
+ const isActive = !this.isMuted && this.isPttPressed;
4025
+ this.transport.setMicrophoneEnabled(isActive);
4026
+ }
4027
+ isPeerMuted(peerId) {
4028
+ return this.mutedPeers.has(peerId);
4029
+ }
4030
+ };
4031
+
4032
+ export {
4033
+ AUDIO_DEFAULTS,
4034
+ zeroVector,
4035
+ defaultOrientation,
4036
+ bufferSource,
4037
+ oscillatorSource,
4038
+ streamSource,
4039
+ noiseSource,
4040
+ spatialSource,
4041
+ gainEffect,
4042
+ reverbEffect,
4043
+ delayEffect,
4044
+ filterEffect,
4045
+ compressorEffect,
4046
+ midiToFrequency,
4047
+ frequencyToMidi,
4048
+ noteNameToMidi,
4049
+ midiToNoteName,
4050
+ lowpassFilter,
4051
+ highpassFilter,
4052
+ bandpassFilter,
4053
+ distortionEffect,
4054
+ panEffect,
4055
+ eqBand,
4056
+ equalizerEffect,
4057
+ createNote,
4058
+ createPattern,
4059
+ createTrack,
4060
+ createSequence,
4061
+ AudioContextImpl,
4062
+ createAudioContext,
4063
+ SequencerImpl,
4064
+ createSequencer,
4065
+ AudioEngine,
4066
+ DEFAULT_BANDS,
4067
+ AudioAnalyzer,
4068
+ AudioDiffractionSystem,
4069
+ AudioDynamics,
4070
+ AudioEnvelope,
4071
+ AudioFilter,
4072
+ AudioGraph,
4073
+ AudioMixer,
4074
+ OCCLUSION_MATERIALS,
4075
+ AudioOcclusionSystem,
4076
+ AudioPresets,
4077
+ setSharedAudioEngine,
4078
+ getSharedAudioEngine,
4079
+ audioTraitHandler,
4080
+ MusicGenerator,
4081
+ SoundPool,
4082
+ SpatialAudioSource,
4083
+ REVERB_PRESETS,
4084
+ SpatialAudioZoneSystem,
4085
+ SynthEngine,
4086
+ VoiceManager,
4087
+ audio_exports
4088
+ };