action-engine-js 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/LICENSE +45 -0
  2. package/README.md +348 -0
  3. package/actionengine/3rdparty/goblin/goblin.js +9609 -0
  4. package/actionengine/3rdparty/goblin/goblin.min.js +5 -0
  5. package/actionengine/camera/actioncamera.js +90 -0
  6. package/actionengine/camera/cameracollisionhandler.js +69 -0
  7. package/actionengine/character/actioncharacter.js +360 -0
  8. package/actionengine/character/actioncharacter3D.js +61 -0
  9. package/actionengine/core/app.js +430 -0
  10. package/actionengine/debug/basedebugpanel.js +858 -0
  11. package/actionengine/display/canvasmanager.js +75 -0
  12. package/actionengine/display/gl/programmanager.js +570 -0
  13. package/actionengine/display/gl/shaders/lineshader.js +118 -0
  14. package/actionengine/display/gl/shaders/objectshader.js +1756 -0
  15. package/actionengine/display/gl/shaders/particleshader.js +43 -0
  16. package/actionengine/display/gl/shaders/shadowshader.js +319 -0
  17. package/actionengine/display/gl/shaders/spriteshader.js +100 -0
  18. package/actionengine/display/gl/shaders/watershader.js +67 -0
  19. package/actionengine/display/graphics/actionmodel3D.js +191 -0
  20. package/actionengine/display/graphics/actionsprite3D.js +230 -0
  21. package/actionengine/display/graphics/lighting/actiondirectionalshadowlight.js +864 -0
  22. package/actionengine/display/graphics/lighting/actionlight.js +211 -0
  23. package/actionengine/display/graphics/lighting/actionomnidirectionalshadowlight.js +862 -0
  24. package/actionengine/display/graphics/lighting/lightingconstants.js +263 -0
  25. package/actionengine/display/graphics/lighting/lightmanager.js +789 -0
  26. package/actionengine/display/graphics/renderableobject.js +44 -0
  27. package/actionengine/display/graphics/renderers/actionrenderer2D.js +341 -0
  28. package/actionengine/display/graphics/renderers/actionrenderer3D/actionrenderer3D.js +655 -0
  29. package/actionengine/display/graphics/renderers/actionrenderer3D/canvasmanager3D.js +82 -0
  30. package/actionengine/display/graphics/renderers/actionrenderer3D/debugrenderer3D.js +493 -0
  31. package/actionengine/display/graphics/renderers/actionrenderer3D/objectrenderer3D.js +790 -0
  32. package/actionengine/display/graphics/renderers/actionrenderer3D/spriteRenderer3D.js +266 -0
  33. package/actionengine/display/graphics/renderers/actionrenderer3D/sunrenderer3D.js +140 -0
  34. package/actionengine/display/graphics/renderers/actionrenderer3D/waterrenderer3D.js +173 -0
  35. package/actionengine/display/graphics/renderers/actionrenderer3D/weatherrenderer3D.js +87 -0
  36. package/actionengine/display/graphics/texture/proceduraltexture.js +192 -0
  37. package/actionengine/display/graphics/texture/texturemanager.js +242 -0
  38. package/actionengine/display/graphics/texture/textureregistry.js +177 -0
  39. package/actionengine/input/actionscrollablearea.js +1405 -0
  40. package/actionengine/input/inputhandler.js +1647 -0
  41. package/actionengine/math/geometry/geometrybuilder.js +161 -0
  42. package/actionengine/math/geometry/glbexporter.js +364 -0
  43. package/actionengine/math/geometry/glbloader.js +722 -0
  44. package/actionengine/math/geometry/modelcodegenerator.js +97 -0
  45. package/actionengine/math/geometry/triangle.js +33 -0
  46. package/actionengine/math/geometry/triangleutils.js +34 -0
  47. package/actionengine/math/mathutils.js +25 -0
  48. package/actionengine/math/matrix4.js +785 -0
  49. package/actionengine/math/physics/actionphysics.js +108 -0
  50. package/actionengine/math/physics/actionphysicsobject3D.js +164 -0
  51. package/actionengine/math/physics/actionphysicsworld3D.js +238 -0
  52. package/actionengine/math/physics/actionraycast.js +129 -0
  53. package/actionengine/math/physics/shapes/actionphysicsbox3D.js +158 -0
  54. package/actionengine/math/physics/shapes/actionphysicscapsule3D.js +200 -0
  55. package/actionengine/math/physics/shapes/actionphysicscompoundshape3D.js +147 -0
  56. package/actionengine/math/physics/shapes/actionphysicscone3D.js +126 -0
  57. package/actionengine/math/physics/shapes/actionphysicsconvexshape3D.js +72 -0
  58. package/actionengine/math/physics/shapes/actionphysicscylinder3D.js +117 -0
  59. package/actionengine/math/physics/shapes/actionphysicsmesh3D.js +74 -0
  60. package/actionengine/math/physics/shapes/actionphysicsplane3D.js +100 -0
  61. package/actionengine/math/physics/shapes/actionphysicssphere3D.js +95 -0
  62. package/actionengine/math/quaternion.js +61 -0
  63. package/actionengine/math/vector2.js +277 -0
  64. package/actionengine/math/vector3.js +318 -0
  65. package/actionengine/math/viewfrustum.js +136 -0
  66. package/actionengine/network/ACTIONNETREADME.md +810 -0
  67. package/actionengine/network/client/ActionNetManager.js +802 -0
  68. package/actionengine/network/client/ActionNetManagerGUI.js +1709 -0
  69. package/actionengine/network/client/ActionNetManagerP2P.js +1537 -0
  70. package/actionengine/network/client/SyncSystem.js +422 -0
  71. package/actionengine/network/p2p/ActionNetPeer.js +142 -0
  72. package/actionengine/network/p2p/ActionNetTrackerClient.js +623 -0
  73. package/actionengine/network/p2p/DataConnection.js +282 -0
  74. package/actionengine/network/p2p/README.md +510 -0
  75. package/actionengine/network/p2p/example.html +502 -0
  76. package/actionengine/network/server/ActionNetServer.js +577 -0
  77. package/actionengine/network/server/ActionNetServerSSL.js +579 -0
  78. package/actionengine/network/server/ActionNetServerUtils.js +458 -0
  79. package/actionengine/network/server/SERVERREADME.md +314 -0
  80. package/actionengine/network/server/package-lock.json +35 -0
  81. package/actionengine/network/server/package.json +13 -0
  82. package/actionengine/network/server/start.bat +27 -0
  83. package/actionengine/network/server/start.sh +25 -0
  84. package/actionengine/network/server/startwss.bat +27 -0
  85. package/actionengine/sound/audiomanager.js +1589 -0
  86. package/actionengine/sound/soundfont/ACTIONSOUNDFONT_README.md +205 -0
  87. package/actionengine/sound/soundfont/actionparser.js +718 -0
  88. package/actionengine/sound/soundfont/actionreverb.js +252 -0
  89. package/actionengine/sound/soundfont/actionsoundfont.js +543 -0
  90. package/actionengine/sound/soundfont/sf2playerlicence.txt +29 -0
  91. package/actionengine/sound/soundfont/soundfont.js +2 -0
  92. package/dist/action-engine.min.js +328 -0
  93. package/package.json +35 -0
@@ -0,0 +1,1589 @@
1
+ // actionengine/sound/audiomanager.js
2
+ class ActionAudioManager {
3
+ constructor() {
4
+ this.context = null;
5
+ this.enabled = true;
6
+ this.sounds = new Map();
7
+ this.samples = new Map();
8
+ this.sampleDefinitions = new Map();
9
+ this.masterGain = null;
10
+ this.baseVolume = 0.2;
11
+ this.initializeAudioContext();
12
+ // Add tracking for scheduled MIDI events
13
+ this.scheduledMIDIEvents = new Set();
14
+ // MIDI state tracking
15
+ this.midiReady = false;
16
+ this.sf2Player = null;
17
+ this.activeSounds = new Map();
18
+ // Enhanced sound tracking for new features
19
+ this.soundInstances = new Map(); // Track individual sound instances
20
+ this.soundVolumes = new Map(); // Individual sound volumes
21
+ this.repeatTimeouts = new Map(); // Track repeat timeouts for cleanup
22
+ this.midiChannels = new Array(16).fill(null).map(() => ({
23
+ volume: 127,
24
+ pan: 64,
25
+ expression: 127,
26
+ program: 0,
27
+ bank: 0,
28
+ pitchBend: 0,
29
+ pitchBendSensitivity: 2,
30
+ duration: 0.5 // Add default duration
31
+ }));
32
+
33
+ this.midiProgramMap = {
34
+ // Piano (0-7)
35
+ acoustic_grand_piano: 0,
36
+ bright_acoustic_piano: 1,
37
+ electric_grand_piano: 2,
38
+ honkytonk_piano: 3,
39
+ electric_piano_1: 4,
40
+ electric_piano_2: 5,
41
+ harpsichord: 6,
42
+ clavinet: 7,
43
+
44
+ // Chromatic Percussion (8-15)
45
+ celesta: 8,
46
+ glockenspiel: 9,
47
+ music_box: 10,
48
+ vibraphone: 11,
49
+ marimba: 12,
50
+ xylophone: 13,
51
+ tubular_bells: 14,
52
+ dulcimer: 15,
53
+
54
+ // Organ (16-23)
55
+ drawbar_organ: 16,
56
+ percussive_organ: 17,
57
+ rock_organ: 18,
58
+ church_organ: 19,
59
+ reed_organ: 20,
60
+ accordion: 21,
61
+ harmonica: 22,
62
+ tango_accordion: 23,
63
+
64
+ // Guitar (24-31)
65
+ acoustic_guitar_nylon: 24,
66
+ acoustic_guitar_steel: 25,
67
+ electric_guitar_jazz: 26,
68
+ electric_guitar_clean: 27,
69
+ electric_guitar_muted: 28,
70
+ overdriven_guitar: 29,
71
+ distortion_guitar: 30,
72
+ guitar_harmonics: 31,
73
+
74
+ // Bass (32-39)
75
+ acoustic_bass: 32,
76
+ electric_bass_finger: 33,
77
+ electric_bass_pick: 34,
78
+ fretless_bass: 35,
79
+ slap_bass_1: 36,
80
+ slap_bass_2: 37,
81
+ synth_bass_1: 38,
82
+ synth_bass_2: 39,
83
+
84
+ // Strings (40-47)
85
+ violin: 40,
86
+ viola: 41,
87
+ cello: 42,
88
+ contrabass: 43,
89
+ tremolo_strings: 44,
90
+ pizzicato_strings: 45,
91
+ orchestral_harp: 46,
92
+ timpani: 47,
93
+
94
+ // Ensemble (48-55)
95
+ string_ensemble_1: 48,
96
+ string_ensemble_2: 49,
97
+ synth_strings_1: 50,
98
+ synth_strings_2: 51,
99
+ choir_aahs: 52,
100
+ voice_oohs: 53,
101
+ synth_choir: 54,
102
+ orchestra_hit: 55,
103
+
104
+ // Brass (56-63)
105
+ trumpet: 56,
106
+ trombone: 57,
107
+ tuba: 58,
108
+ muted_trumpet: 59,
109
+ french_horn: 60,
110
+ brass_section: 61,
111
+ synth_brass_1: 62,
112
+ synth_brass_2: 63,
113
+
114
+ // Reed (64-71)
115
+ soprano_sax: 64,
116
+ alto_sax: 65,
117
+ tenor_sax: 66,
118
+ baritone_sax: 67,
119
+ oboe: 68,
120
+ english_horn: 69,
121
+ bassoon: 70,
122
+ clarinet: 71,
123
+
124
+ // Pipe (72-79)
125
+ piccolo: 72,
126
+ flute: 73,
127
+ recorder: 74,
128
+ pan_flute: 75,
129
+ blown_bottle: 76,
130
+ shakuhachi: 77,
131
+ whistle: 78,
132
+ ocarina: 79,
133
+
134
+ // Synth Lead (80-87)
135
+ lead_1_square: 80,
136
+ lead_2_sawtooth: 81,
137
+ lead_3_calliope: 82,
138
+ lead_4_chiff: 83,
139
+ lead_5_charang: 84,
140
+ lead_6_voice: 85,
141
+ lead_7_fifths: 86,
142
+ lead_8_bass_lead: 87,
143
+
144
+ // Synth Pad (88-95)
145
+ pad_1_new_age: 88,
146
+ pad_2_warm: 89,
147
+ pad_3_polysynth: 90,
148
+ pad_4_choir: 91,
149
+ pad_5_bowed: 92,
150
+ pad_6_metallic: 93,
151
+ pad_7_halo: 94,
152
+ pad_8_sweep: 95,
153
+
154
+ // Synth Effects (96-103)
155
+ fx_1_rain: 96,
156
+ fx_2_soundtrack: 97,
157
+ fx_3_crystal: 98,
158
+ fx_4_atmosphere: 99,
159
+ fx_5_brightness: 100,
160
+ fx_6_goblins: 101,
161
+ fx_7_echoes: 102,
162
+ fx_8_scifi: 103,
163
+
164
+ // Ethnic (104-111)
165
+ sitar: 104,
166
+ banjo: 105,
167
+ shamisen: 106,
168
+ koto: 107,
169
+ kalimba: 108,
170
+ bagpipe: 109,
171
+ fiddle: 110,
172
+ shanai: 111,
173
+
174
+ // Percussive (112-119)
175
+ tinkle_bell: 112,
176
+ agogo: 113,
177
+ steel_drums: 114,
178
+ woodblock: 115,
179
+ taiko_drum: 116,
180
+ melodic_tom: 117,
181
+ synth_drum: 118,
182
+ reverse_cymbal: 119,
183
+
184
+ // Sound Effects (120-127)
185
+ guitar_fret_noise: 120,
186
+ breath_noise: 121,
187
+ seashore: 122,
188
+ bird_tweet: 123,
189
+ telephone_ring: 124,
190
+ helicopter: 125,
191
+ applause: 126,
192
+ gunshot: 127
193
+ };
194
+ }
195
+
196
+ async initializeMIDI() {
197
+ if (this.sf2Player) return;
198
+
199
+ try {
200
+ this.sf2Player = new SoundFont(this.context); // Pass the context
201
+ await this.sf2Player.loadSoundFontFromBase64(window.TimGM6mb_BASE64);
202
+ this.midiReady = true;
203
+ } catch (error) {
204
+ console.error("[AudioManager] Failed to initialize SF2 player:", error);
205
+ }
206
+ }
207
+
208
+ // MIDI Control Methods
209
+ programChange(channel, program) {
210
+ if (!this.midiReady || !this.sf2Player) return;
211
+ this.midiChannels[channel].program = program;
212
+ this.sf2Player.channel = channel;
213
+ this.sf2Player.program = program;
214
+ }
215
+
216
+ bankSelect(channel, bank) {
217
+ if (!this.midiReady || !this.sf2Player) return;
218
+ this.midiChannels[channel].bank = bank;
219
+ this.sf2Player.channel = channel;
220
+ this.sf2Player.bank = bank;
221
+ }
222
+
223
+ noteOn(channel, note, velocity) {
224
+ if (!this.midiReady || !this.sf2Player) return;
225
+ this.sf2Player.noteOn(note, velocity, channel);
226
+ }
227
+
228
+ noteOff(channel, note) {
229
+ if (!this.midiReady || !this.sf2Player) return;
230
+ this.sf2Player.noteOff(note, 127, channel);
231
+ }
232
+
233
+ pitchBend(channel, value) {
234
+ if (!this.midiReady) return;
235
+ this.midiChannels[channel].pitchBend = value;
236
+ // TODO: Implement pitch bend with SF2 player if supported
237
+ }
238
+
239
+ setChannelVolume(channel, value) {
240
+ if (!this.midiReady) return;
241
+ this.midiChannels[channel].volume = value;
242
+ // TODO: Implement volume control with SF2 player if supported
243
+ }
244
+
245
+ setChannelPan(channel, value) {
246
+ if (!this.midiReady) return;
247
+ this.midiChannels[channel].pan = value;
248
+ // TODO: Implement pan with SF2 player if supported
249
+ }
250
+
251
+ allNotesOff(channel) {
252
+ if (!this.midiReady) return;
253
+ // Send noteOff for all possible notes on this channel
254
+ for (let note = 0; note < 128; note++) {
255
+ this.noteOff(channel, note);
256
+ }
257
+ }
258
+
259
+ resetAllControllers(channel) {
260
+ if (!this.midiReady) return;
261
+ const ch = this.midiChannels[channel];
262
+ ch.volume = 127;
263
+ ch.pan = 64;
264
+ ch.expression = 127;
265
+ ch.pitchBend = 0;
266
+ this.allNotesOff(channel);
267
+ }
268
+
269
+ // Original createSound maintained for compatibility
270
+ createSound(name, options, type = "simple") {
271
+ // Handle legacy format where options is just a frequency number
272
+ if (typeof options === "number") {
273
+ options = {
274
+ frequency: this.midiToFrequency(options),
275
+ oscillatorType: type !== "simple" ? type : "sine" // type parameter was oscillatorType in old format
276
+ };
277
+ type = "simple";
278
+ }
279
+
280
+ switch (type) {
281
+ case "sonicpi":
282
+ const parsed = this.parseSonicPi(options.script, options.samples);
283
+
284
+ if (options.samples) {
285
+ // Check once if we need MIDI
286
+ if (Object.values(options.samples).some((def) => def.soundType === "midi")) {
287
+ this.initializeMIDI();
288
+ }
289
+
290
+ // Then just store all the definitions
291
+ Object.entries(options.samples).forEach(([sampleName, definition]) => {
292
+ this.sampleDefinitions.set(sampleName, definition);
293
+ });
294
+ }
295
+
296
+ this.sounds.set(name, {
297
+ type: "sonicpi",
298
+ script: options.script,
299
+ parsedSequence: parsed.sequence,
300
+ samples: options.samples || {},
301
+ bpm: parsed.bpm
302
+ });
303
+ break;
304
+
305
+ case "simple":
306
+ default:
307
+ // Extract the envelope values with defaults
308
+ const envelope = options.envelope || {
309
+ attack: 0.1,
310
+ decay: 0.2,
311
+ sustain: 0.7,
312
+ release: 0.3
313
+ };
314
+
315
+ // Create the sound definition with all parameters
316
+ this.sounds.set(name, {
317
+ type: "simple",
318
+ oscillatorType: options.type || "sine", // This handles the waveform
319
+ frequency: options.frequency || 440,
320
+ amp: options.amp || 0.5,
321
+ duration: options.duration || 1,
322
+ envelope: envelope
323
+ });
324
+ break;
325
+ }
326
+ }
327
+
328
+ initializeAudioContext() {
329
+ const enableAudio = () => {
330
+ if (!this.context) {
331
+ this.context = new (window.AudioContext || window.webkitAudioContext)();
332
+ this.masterGain = this.context.createGain();
333
+ this.masterGain.connect(this.context.destination);
334
+ this.setVolume(0.5);
335
+
336
+ // Create sample buffers once context is available
337
+ this.createSampleBuffers();
338
+ }
339
+ document.removeEventListener("click", enableAudio);
340
+ document.removeEventListener("touchstart", enableAudio);
341
+ document.removeEventListener("keydown", enableAudio);
342
+ };
343
+ document.addEventListener("click", enableAudio);
344
+ document.addEventListener("touchstart", enableAudio);
345
+ document.addEventListener("keydown", enableAudio);
346
+ }
347
+
348
+ createSampleBuffers() {
349
+ this.sampleDefinitions.forEach((definition, sampleName) => {
350
+ // Skip buffer creation for MIDI samples
351
+ if (definition.soundType === "midi") {
352
+ return;
353
+ }
354
+ const bufferSize = this.context.sampleRate * 2;
355
+ const buffer = this.context.createBuffer(2, bufferSize, this.context.sampleRate);
356
+ const left = buffer.getChannelData(0);
357
+ const right = buffer.getChannelData(1);
358
+
359
+ for (let i = 0; i < bufferSize; i++) {
360
+ const t = i / this.context.sampleRate;
361
+ let value;
362
+
363
+ switch (definition.type) {
364
+ case "sin":
365
+ // Adjust decay to be less aggressive
366
+ const decayFactor = definition.decay * 5; // Slow down the decay
367
+ value = Math.sin(t * definition.frequency * 2 * Math.PI) * Math.exp(-t / decayFactor);
368
+ break;
369
+ case "square":
370
+ value = Math.sign(Math.sin(t * definition.frequency)) * Math.exp(-t * definition.decay);
371
+ break;
372
+ case "saw":
373
+ value = ((t * definition.frequency) % 1) * 2 - 1;
374
+ value *= Math.exp(-t * definition.decay);
375
+ break;
376
+ default:
377
+ value = Math.sin(t * definition.frequency) * Math.exp(-t * definition.decay);
378
+ }
379
+
380
+ left[i] = right[i] = value;
381
+ }
382
+
383
+ this.samples.set(sampleName, buffer);
384
+ });
385
+ }
386
+
387
+ preloadSample(name, definition) {
388
+ if (!this.context) return;
389
+
390
+ const bufferSize = this.context.sampleRate * 2;
391
+ const buffer = this.context.createBuffer(2, bufferSize, this.context.sampleRate);
392
+ const left = buffer.getChannelData(0);
393
+ const right = buffer.getChannelData(1);
394
+
395
+ for (let i = 0; i < bufferSize; i++) {
396
+ const t = i / this.context.sampleRate;
397
+ let value;
398
+
399
+ switch (definition.type) {
400
+ case "sin":
401
+ value = Math.sin(t * definition.frequency) * Math.exp(-t * definition.decay);
402
+ break;
403
+ case "square":
404
+ value = Math.sign(Math.sin(t * definition.frequency)) * Math.exp(-t * definition.decay);
405
+ break;
406
+ case "saw":
407
+ value = ((t * definition.frequency) % 1) * 2 - 1;
408
+ value *= Math.exp(-t * definition.decay);
409
+ break;
410
+ default:
411
+ value = Math.sin(t * definition.frequency) * Math.exp(-t * definition.decay);
412
+ }
413
+
414
+ left[i] = right[i] = value;
415
+ }
416
+
417
+ this.samples.set(name, buffer);
418
+ }
419
+
420
+ // New method for creating FM synthesis sounds
421
+ createFMSound(
422
+ name,
423
+ {
424
+ carrierFreq,
425
+ modulatorFreq,
426
+ modulationIndex = 100,
427
+ type = "sine",
428
+ duration,
429
+ envelope = {
430
+ attack: 0.01,
431
+ decay: 0.1,
432
+ sustain: 0.5,
433
+ release: 0.1
434
+ }
435
+ }
436
+ ) {
437
+ this.sounds.set(name, {
438
+ type: "fm",
439
+ carrierFreq,
440
+ modulatorFreq,
441
+ modulationIndex,
442
+ oscillatorType: type,
443
+ duration,
444
+ envelope
445
+ });
446
+ }
447
+
448
+ // Create multi-oscillator sound
449
+ createComplexSound(
450
+ name,
451
+ {
452
+ frequencies = [], // Array of frequency values
453
+ types = [], // Array of oscillator types
454
+ mix = [], // Array of volume levels
455
+ duration,
456
+ envelope = {
457
+ attack: 0.01,
458
+ decay: 0.1,
459
+ sustain: 0.5,
460
+ release: 0.1
461
+ }
462
+ }
463
+ ) {
464
+ this.sounds.set(name, {
465
+ type: "complex",
466
+ frequencies,
467
+ oscillatorTypes: types,
468
+ mix,
469
+ duration,
470
+ envelope
471
+ });
472
+ }
473
+
474
+ // Create noise-based sound
475
+ createNoiseSound(
476
+ name,
477
+ {
478
+ duration,
479
+ noiseType = "white", // 'white', 'pink', or 'brown'
480
+ envelope = {
481
+ attack: 0.01,
482
+ decay: 0.1,
483
+ sustain: 0.5,
484
+ release: 0.1
485
+ },
486
+ filterOptions = {
487
+ frequency: 1000,
488
+ Q: 1,
489
+ type: "lowpass"
490
+ }
491
+ }
492
+ ) {
493
+ this.sounds.set(name, {
494
+ type: "noise",
495
+ noiseType,
496
+ duration,
497
+ envelope,
498
+ filterOptions
499
+ });
500
+ }
501
+
502
+ // Create sweep effect sound
503
+ createSweepSound(
504
+ name,
505
+ {
506
+ startFreq,
507
+ endFreq,
508
+ type = "sine",
509
+ duration,
510
+ envelope = {
511
+ attack: 0.01,
512
+ decay: 0.1,
513
+ sustain: 0.5,
514
+ release: 0.1
515
+ }
516
+ }
517
+ ) {
518
+ this.sounds.set(name, {
519
+ type: "sweep",
520
+ startFreq,
521
+ endFreq,
522
+ oscillatorType: type,
523
+ duration,
524
+ envelope
525
+ });
526
+ }
527
+
528
+ // Helper to parse parameters like "attack: 0.1, release: 0.3"
529
+ parseParameters(paramsString) {
530
+ const params = {};
531
+ const pairs = paramsString.split(",");
532
+
533
+ for (const pair of pairs) {
534
+ const [key, value] = pair.split(":").map((s) => s.trim());
535
+ params[key] = parseFloat(value);
536
+ }
537
+
538
+ return params;
539
+ }
540
+
541
+ // Helper to convert MIDI note numbers to frequency
542
+ midiToFrequency(note) {
543
+ if (typeof note !== "number" || isNaN(note)) {
544
+ console.warn("[AudioManager] Invalid MIDI note:", note);
545
+ return 440; // Return A4 as default
546
+ }
547
+ return 440 * Math.pow(2, (note - 69) / 12);
548
+ }
549
+
550
+ /**
551
+ * Set individual sound volume
552
+ * @param {string} name - Sound name
553
+ * @param {number} volume - Volume level (0.0 to 1.0)
554
+ */
555
+ setSoundVolume(name, volume) {
556
+ volume = Math.max(0, Math.min(1, volume)); // Clamp between 0 and 1
557
+ this.soundVolumes.set(name, volume);
558
+
559
+ // Update any currently playing instances
560
+ const instances = this.soundInstances.get(name) || [];
561
+ instances.forEach(instance => {
562
+ if (instance.gainNode && instance.gainNode.gain) {
563
+ const currentTime = this.context.currentTime;
564
+ const masterVol = this.masterGain ? this.masterGain.gain.value / this.baseVolume : 1.0;
565
+ const finalVolume = volume * masterVol;
566
+ instance.gainNode.gain.setValueAtTime(finalVolume, currentTime);
567
+ }
568
+ });
569
+ }
570
+
571
+ /**
572
+ * Get individual sound volume
573
+ * @param {string} name - Sound name
574
+ * @returns {number} Volume level (0.0 to 1.0)
575
+ */
576
+ getSoundVolume(name) {
577
+ return this.soundVolumes.get(name) || 1.0;
578
+ }
579
+
580
+ // Enhanced play method to handle all sound types with new options
581
+ play(name, options = {}) {
582
+ if (!this.enabled || !this.context) return;
583
+
584
+ const sound = this.sounds.get(name);
585
+ if (!sound) return;
586
+
587
+ // Always prevent sound stacking - stop any existing instances of this sound
588
+ const instances = this.soundInstances.get(name) || [];
589
+ if (instances.length > 0) {
590
+ // Stop existing instances
591
+ instances.forEach(instance => this.stopSoundInstance(instance));
592
+ this.soundInstances.set(name, []);
593
+ }
594
+
595
+ // Set up repeat tracking
596
+ const repeatInfo = {
597
+ count: options.repeat || 1,
598
+ current: 0,
599
+ onEnd: options.onEnd || null
600
+ };
601
+
602
+ // Play the first instance
603
+ const controlObject = this.playInstance(name, sound, options, repeatInfo);
604
+
605
+ return controlObject;
606
+ }
607
+
608
+ /**
609
+ * Play a single instance of a sound
610
+ */
611
+ playInstance(name, sound, options, repeatInfo) {
612
+ let controlObject;
613
+
614
+ // Apply individual sound volume if set
615
+ const soundVolume = options.volume !== undefined ? options.volume : this.getSoundVolume(name);
616
+ const enhancedOptions = { ...options, volume: soundVolume };
617
+
618
+ switch (sound.type) {
619
+ case "simple":
620
+ controlObject = this.playSimple(sound, enhancedOptions);
621
+ break;
622
+ case "fm":
623
+ controlObject = this.playFM(sound, enhancedOptions);
624
+ break;
625
+ case "complex":
626
+ controlObject = this.playComplex(sound, enhancedOptions);
627
+ break;
628
+ case "noise":
629
+ controlObject = this.playNoise(sound, enhancedOptions);
630
+ break;
631
+ case "sweep":
632
+ controlObject = this.playSweep(sound, enhancedOptions);
633
+ break;
634
+ case "sonicpi":
635
+ controlObject = this.playSonicPi(sound, enhancedOptions);
636
+ break;
637
+ default:
638
+ console.warn(`[AudioManager] Unknown sound type for ${name}`);
639
+ return;
640
+ }
641
+
642
+ if (controlObject) {
643
+ // Track this instance
644
+ const instances = this.soundInstances.get(name) || [];
645
+ const instanceData = {
646
+ ...controlObject,
647
+ name: name,
648
+ startTime: this.context.currentTime,
649
+ repeatInfo: repeatInfo
650
+ };
651
+ instances.push(instanceData);
652
+ this.soundInstances.set(name, instances);
653
+
654
+ // Set up sound end detection for callbacks and repeats
655
+ this.setupSoundEndDetection(name, sound, instanceData);
656
+
657
+ this.activeSounds.set(name, controlObject);
658
+ }
659
+ return controlObject;
660
+ }
661
+
662
+ /**
663
+ * Set up sound end detection for callbacks and repeat functionality
664
+ */
665
+ setupSoundEndDetection(name, sound, instanceData) {
666
+ // Calculate sound duration
667
+ let duration = sound.duration || 1;
668
+ if (sound.envelope) {
669
+ const env = sound.envelope;
670
+ duration = (env.attack || 0.1) + (env.decay || 0.2) + (env.release || 0.3);
671
+ }
672
+
673
+ // Set timeout to handle sound end
674
+ const endTimeout = setTimeout(() => {
675
+ this.handleSoundEnd(name, instanceData);
676
+ }, duration * 1000);
677
+
678
+ instanceData.endTimeout = endTimeout;
679
+ }
680
+
681
+ /**
682
+ * Handle sound end - cleanup, callbacks, and repeat logic
683
+ */
684
+ handleSoundEnd(name, instanceData) {
685
+ // Remove this instance from tracking
686
+ const instances = this.soundInstances.get(name) || [];
687
+ const index = instances.indexOf(instanceData);
688
+ if (index > -1) {
689
+ instances.splice(index, 1);
690
+ this.soundInstances.set(name, instances);
691
+ }
692
+
693
+ const repeatInfo = instanceData.repeatInfo;
694
+ repeatInfo.current++;
695
+
696
+ // Check if we should repeat
697
+ const shouldRepeat = (repeatInfo.count === -1) || (repeatInfo.current < repeatInfo.count);
698
+
699
+ if (shouldRepeat) {
700
+ // Schedule next repeat
701
+ const repeatTimeout = setTimeout(() => {
702
+ const sound = this.sounds.get(name);
703
+ if (sound) {
704
+ this.playInstance(name, sound, instanceData.options || {}, repeatInfo);
705
+ }
706
+ }, 50); // Small delay between repeats
707
+
708
+ // Track repeat timeout for cleanup
709
+ const repeatTimeouts = this.repeatTimeouts.get(name) || [];
710
+ repeatTimeouts.push(repeatTimeout);
711
+ this.repeatTimeouts.set(name, repeatTimeouts);
712
+ } else {
713
+ // Sound is completely finished, call onEnd callback
714
+ if (repeatInfo.onEnd) {
715
+ try {
716
+ repeatInfo.onEnd({
717
+ soundName: name,
718
+ totalRepeats: repeatInfo.current
719
+ });
720
+ } catch (error) {
721
+ console.error(`[AudioManager] Error in onEnd callback for ${name}:`, error);
722
+ }
723
+ }
724
+ }
725
+ }
726
+
727
+ /**
728
+ * Stop a specific sound instance
729
+ */
730
+ stopSoundInstance(instanceData) {
731
+ const now = this.context.currentTime;
732
+
733
+ // Clear end timeout
734
+ if (instanceData.endTimeout) {
735
+ clearTimeout(instanceData.endTimeout);
736
+ }
737
+
738
+ // Stop audio nodes
739
+ if (instanceData.nodes) {
740
+ instanceData.nodes.forEach((node) => {
741
+ if (node.gain) {
742
+ node.gain.setValueAtTime(node.gain.value, now);
743
+ node.gain.linearRampToValueAtTime(0, now + 0.05);
744
+ }
745
+ setTimeout(() => {
746
+ try {
747
+ if (node.stop) node.stop();
748
+ } catch (e) {
749
+ // Node might have already stopped
750
+ }
751
+ }, 50);
752
+ });
753
+ }
754
+
755
+ // Handle MIDI notes
756
+ if (instanceData.midiNotes) {
757
+ instanceData.midiNotes.forEach((note) => {
758
+ if (this.sf2Player) {
759
+ this.sf2Player.noteOff(note.note, note.velocity || 127, note.channel || 0);
760
+ }
761
+ });
762
+ }
763
+ }
764
+
765
+ stopSound(name) {
766
+ // Stop all instances of this sound
767
+ const instances = this.soundInstances.get(name) || [];
768
+ instances.forEach(instance => {
769
+ this.stopSoundInstance(instance);
770
+ });
771
+ this.soundInstances.set(name, []);
772
+
773
+ // Clear any pending repeat timeouts
774
+ const repeatTimeouts = this.repeatTimeouts.get(name) || [];
775
+ repeatTimeouts.forEach(timeout => clearTimeout(timeout));
776
+ this.repeatTimeouts.set(name, []);
777
+
778
+ // Original stopSound logic for backward compatibility
779
+ const sound = this.activeSounds.get(name);
780
+ if (sound) {
781
+ const now = this.context.currentTime;
782
+
783
+ if (sound.nodes) {
784
+ // Handle WebAudio nodes
785
+ sound.nodes.forEach((node) => {
786
+ if (node.gain) {
787
+ node.gain.setValueAtTime(node.gain.value, now);
788
+ node.gain.linearRampToValueAtTime(0, now + 0.05);
789
+ }
790
+ setTimeout(() => {
791
+ try {
792
+ if (node.stop) {
793
+ node.stop();
794
+ }
795
+ } catch (e) {
796
+ // Node might have already stopped
797
+ }
798
+ }, 50);
799
+ });
800
+ }
801
+
802
+ // If this is a MIDI sound, handle note offs
803
+ if (sound.midiNotes) {
804
+ sound.midiNotes.forEach((note) => {
805
+ if (this.sf2Player) {
806
+ this.sf2Player.noteOff(note.note, note.velocity || 127, note.channel || 0);
807
+ }
808
+ });
809
+ }
810
+
811
+ this.activeSounds.delete(name);
812
+ }
813
+ }
814
+
815
+ stopAllSounds() {
816
+ // Stop all sound instances
817
+ this.soundInstances.forEach((instances, name) => {
818
+ instances.forEach(instance => {
819
+ this.stopSoundInstance(instance);
820
+ });
821
+ });
822
+ this.soundInstances.clear();
823
+
824
+ // Clear all repeat timeouts
825
+ this.repeatTimeouts.forEach((timeouts) => {
826
+ timeouts.forEach(timeout => clearTimeout(timeout));
827
+ });
828
+ this.repeatTimeouts.clear();
829
+
830
+ // Stop all active sounds (original logic)
831
+ this.activeSounds.forEach((sound, name) => {
832
+ this.stopSound(name);
833
+ });
834
+
835
+ // Clear all scheduled MIDI timeouts
836
+ this.scheduledMIDIEvents.forEach((timeoutId) => {
837
+ clearTimeout(timeoutId);
838
+ });
839
+ this.scheduledMIDIEvents.clear();
840
+
841
+ // Additionally ensure all MIDI notes are off on all channels
842
+ if (this.sf2Player) {
843
+ for (let channel = 0; channel < 16; channel++) {
844
+ this.allNotesOff(channel);
845
+ }
846
+ }
847
+
848
+ this.activeSounds.clear();
849
+ }
850
+
851
+ // Original simple oscillator playback (enhanced with volume support)
852
+ playSimple(sound, { pan = 0, volume = 1.0 } = {}) {
853
+ const oscillator = this.context.createOscillator();
854
+ oscillator.type = sound.oscillatorType;
855
+ const gainNode = this.context.createGain();
856
+ const stereoPanner = this.context.createStereoPanner();
857
+
858
+ // Set the oscillator type from the sound definition
859
+ oscillator.type = sound.oscillatorType;
860
+ oscillator.frequency.value = sound.frequency;
861
+
862
+ stereoPanner.pan.value = pan;
863
+
864
+ // Apply the ADSR envelope with individual volume
865
+ const now = this.context.currentTime;
866
+ const envelope = sound.envelope;
867
+ const finalAmp = sound.amp * volume; // Apply individual sound volume
868
+
869
+ gainNode.gain.setValueAtTime(0, now);
870
+ gainNode.gain.linearRampToValueAtTime(finalAmp, now + envelope.attack);
871
+ gainNode.gain.linearRampToValueAtTime(finalAmp * envelope.sustain, now + envelope.attack + envelope.decay);
872
+ gainNode.gain.linearRampToValueAtTime(0, now + envelope.attack + envelope.decay + envelope.release);
873
+
874
+ // Store gain node for potential volume changes
875
+ const controlObject = {
876
+ oscillator,
877
+ gainNode,
878
+ nodes: [oscillator, gainNode, stereoPanner]
879
+ };
880
+
881
+ oscillator.connect(gainNode);
882
+ gainNode.connect(stereoPanner);
883
+ stereoPanner.connect(this.masterGain);
884
+
885
+ oscillator.start();
886
+ oscillator.stop(now + envelope.attack + envelope.decay + envelope.release);
887
+
888
+ return controlObject;
889
+ }
890
+
891
+ // FM synthesis playback (enhanced with volume support)
892
+ playFM(sound, { pan = 0, volume = 1.0 } = {}) {
893
+ const carrier = this.context.createOscillator();
894
+ const modulator = this.context.createOscillator();
895
+ const modulatorGain = this.context.createGain();
896
+ const gainNode = this.context.createGain();
897
+ const stereoPanner = this.context.createStereoPanner();
898
+
899
+ carrier.type = sound.oscillatorType;
900
+ modulator.type = "sine";
901
+
902
+ carrier.frequency.value = sound.carrierFreq;
903
+ modulator.frequency.value = sound.modulatorFreq;
904
+ modulatorGain.gain.value = sound.modulationIndex;
905
+
906
+ stereoPanner.pan.value = pan;
907
+
908
+ modulator.connect(modulatorGain);
909
+ modulatorGain.connect(carrier.frequency);
910
+ carrier.connect(gainNode);
911
+ gainNode.connect(stereoPanner);
912
+ stereoPanner.connect(this.masterGain);
913
+
914
+ this.applyEnvelope(gainNode.gain, sound.envelope, sound.duration, volume);
915
+
916
+ carrier.start();
917
+ modulator.start();
918
+ carrier.stop(this.context.currentTime + sound.duration);
919
+ modulator.stop(this.context.currentTime + sound.duration);
920
+
921
+ return {
922
+ oscillator: carrier,
923
+ gainNode,
924
+ nodes: [carrier, modulator, gainNode, stereoPanner]
925
+ };
926
+ }
927
+
928
+ // Multi-oscillator playback (enhanced with volume support)
929
+ playComplex(sound, { pan = 0, volume = 1.0 } = {}) {
930
+ const stereoPanner = this.context.createStereoPanner();
931
+ const masterGain = this.context.createGain();
932
+ const oscillators = [];
933
+
934
+ stereoPanner.pan.value = pan;
935
+ masterGain.connect(stereoPanner);
936
+ stereoPanner.connect(this.masterGain);
937
+
938
+ sound.frequencies.forEach((freq, i) => {
939
+ const osc = this.context.createOscillator();
940
+ const gain = this.context.createGain();
941
+
942
+ osc.type = sound.oscillatorTypes[i] || "sine";
943
+ osc.frequency.value = freq;
944
+ gain.gain.value = sound.mix[i] || 0.5;
945
+
946
+ osc.connect(gain);
947
+ gain.connect(masterGain);
948
+ oscillators.push(osc);
949
+ });
950
+
951
+ this.applyEnvelope(masterGain.gain, sound.envelope, sound.duration, volume);
952
+
953
+ oscillators.forEach((osc) => {
954
+ osc.start();
955
+ osc.stop(this.context.currentTime + sound.duration);
956
+ });
957
+
958
+ return {
959
+ oscillator: oscillators[0], // For compatibility
960
+ gainNode: masterGain,
961
+ nodes: [masterGain, stereoPanner, ...oscillators]
962
+ };
963
+ }
964
+
965
+ // Noise generation and playback (enhanced with volume support)
966
+ playNoise(sound, { pan = 0, volume = 1.0 } = {}) {
967
+ const bufferSize = this.context.sampleRate * sound.duration;
968
+ const buffer = this.context.createBuffer(1, bufferSize, this.context.sampleRate);
969
+ const data = buffer.getChannelData(0);
970
+
971
+ // Generate noise based on type
972
+ switch (sound.noiseType) {
973
+ case "white":
974
+ for (let i = 0; i < bufferSize; i++) {
975
+ data[i] = Math.random() * 2 - 1;
976
+ }
977
+ break;
978
+ case "pink": {
979
+ let b0 = 0,
980
+ b1 = 0,
981
+ b2 = 0,
982
+ b3 = 0,
983
+ b4 = 0,
984
+ b5 = 0;
985
+ for (let i = 0; i < bufferSize; i++) {
986
+ const white = Math.random() * 2 - 1;
987
+ b0 = 0.99886 * b0 + white * 0.0555179;
988
+ b1 = 0.99332 * b1 + white * 0.0750759;
989
+ b2 = 0.969 * b2 + white * 0.153852;
990
+ b3 = 0.8665 * b3 + white * 0.3104856;
991
+ b4 = 0.55 * b4 + white * 0.5329522;
992
+ b5 = -0.7616 * b5 - white * 0.016898;
993
+ data[i] = b0 + b1 + b2 + b3 + b4 + b5;
994
+ }
995
+ break;
996
+ }
997
+ case "brown": {
998
+ let lastOut = 0;
999
+ for (let i = 0; i < bufferSize; i++) {
1000
+ const white = Math.random() * 2 - 1;
1001
+ data[i] = (lastOut + 0.02 * white) / 1.02;
1002
+ lastOut = data[i];
1003
+ }
1004
+ break;
1005
+ }
1006
+ }
1007
+
1008
+ const noise = this.context.createBufferSource();
1009
+ const filter = this.context.createBiquadFilter();
1010
+ const gainNode = this.context.createGain();
1011
+ const stereoPanner = this.context.createStereoPanner();
1012
+
1013
+ noise.buffer = buffer;
1014
+
1015
+ filter.type = sound.filterOptions.type;
1016
+ filter.frequency.value = sound.filterOptions.frequency;
1017
+ filter.Q.value = sound.filterOptions.Q;
1018
+
1019
+ stereoPanner.pan.value = pan;
1020
+
1021
+ noise.connect(filter);
1022
+ filter.connect(gainNode);
1023
+ gainNode.connect(stereoPanner);
1024
+ stereoPanner.connect(this.masterGain);
1025
+
1026
+ this.applyEnvelope(gainNode.gain, sound.envelope, sound.duration, volume);
1027
+
1028
+ noise.start();
1029
+ return {
1030
+ oscillator: noise,
1031
+ gainNode,
1032
+ nodes: [noise, filter, gainNode, stereoPanner]
1033
+ };
1034
+ }
1035
+
1036
+ // Frequency sweep playback (enhanced with volume support)
1037
+ playSweep(sound, { pan = 0, volume = 1.0 } = {}) {
1038
+ const oscillator = this.context.createOscillator();
1039
+ const gainNode = this.context.createGain();
1040
+ const stereoPanner = this.context.createStereoPanner();
1041
+
1042
+ oscillator.type = sound.oscillatorType;
1043
+ oscillator.frequency.setValueAtTime(sound.startFreq, this.context.currentTime);
1044
+ oscillator.frequency.exponentialRampToValueAtTime(sound.endFreq, this.context.currentTime + sound.duration);
1045
+
1046
+ stereoPanner.pan.value = pan;
1047
+
1048
+ oscillator.connect(gainNode);
1049
+ gainNode.connect(stereoPanner);
1050
+ stereoPanner.connect(this.masterGain);
1051
+
1052
+ this.applyEnvelope(gainNode.gain, sound.envelope, sound.duration, volume);
1053
+
1054
+ oscillator.start();
1055
+ oscillator.stop(this.context.currentTime + sound.duration);
1056
+
1057
+ return {
1058
+ oscillator,
1059
+ gainNode,
1060
+ nodes: [oscillator, gainNode, stereoPanner]
1061
+ };
1062
+ }
1063
+
1064
+ parseSonicPi(script, samples) {
1065
+ const parser = new SonicPiParser(this);
1066
+ return parser.parseScript(script, samples);
1067
+ }
1068
+
1069
+ playSonicPi(sound, options = {}) {
1070
+ if (!this.enabled || !this.context) return;
1071
+
1072
+ const startTime = this.context.currentTime;
1073
+ const allNodes = [];
1074
+ const midiNotes = [];
1075
+ let isPlaying = true;
1076
+
1077
+ // Get BPM from sound data
1078
+ const bpm = sound.bpm || 60;
1079
+ const beatDuration = 60 / bpm;
1080
+
1081
+ // Define synthConfigs first
1082
+ const synthConfigs = {
1083
+ fm: {
1084
+ type: "sine",
1085
+ modulation: {
1086
+ frequency: 0.5,
1087
+ gain: 50
1088
+ }
1089
+ },
1090
+ prophet: {
1091
+ type: "sawtooth",
1092
+ detune: 10,
1093
+ filterType: "lowpass"
1094
+ },
1095
+ saw: { type: "sawtooth" },
1096
+ mod_pulse: {
1097
+ type: "square",
1098
+ modulation: {
1099
+ frequency: 0.25,
1100
+ gain: 30
1101
+ }
1102
+ },
1103
+ piano: {
1104
+ type: "triangle",
1105
+ filterType: "bandpass"
1106
+ },
1107
+ pluck: {
1108
+ type: "triangle",
1109
+ filterType: "highpass"
1110
+ },
1111
+ kalimba: {
1112
+ type: "sine",
1113
+ filterType: "bandpass"
1114
+ },
1115
+ tb303: {
1116
+ type: "square",
1117
+ filterType: "lowpass",
1118
+ resonance: 10
1119
+ }
1120
+ };
1121
+
1122
+ // Effect processors
1123
+ const createEffect = {
1124
+ echo: (params) => {
1125
+ const delay = this.context.createDelay();
1126
+ const feedback = this.context.createGain();
1127
+ const wetGain = this.context.createGain();
1128
+
1129
+ delay.delayTime.value = params.phase || 0.25;
1130
+ feedback.gain.value = params.decay || 0.5;
1131
+ wetGain.gain.value = 0.5;
1132
+
1133
+ // Connect feedback loop
1134
+ delay.connect(feedback);
1135
+ feedback.connect(delay);
1136
+
1137
+ return [delay, feedback, wetGain];
1138
+ },
1139
+ lpf: (params) => {
1140
+ const filter = this.context.createBiquadFilter();
1141
+ filter.type = "lowpass";
1142
+ filter.frequency.value = params.cutoff || 1000;
1143
+ filter.Q.value = params.resonance || 1;
1144
+ return [filter];
1145
+ },
1146
+ reverb: (params) => {
1147
+ const convolver = this.context.createConvolver();
1148
+ const wetGain = this.context.createGain();
1149
+ wetGain.gain.value = params.room || 0.5;
1150
+
1151
+ // Create impulse response
1152
+ const length = this.context.sampleRate * (params.room || 0.5) * 3;
1153
+ const impulse = this.context.createBuffer(2, length, this.context.sampleRate);
1154
+ const left = impulse.getChannelData(0);
1155
+ const right = impulse.getChannelData(1);
1156
+
1157
+ for (let i = 0; i < length; i++) {
1158
+ const n = i / length;
1159
+ left[i] = (Math.random() * 2 - 1) * Math.pow(1 - n, params.room * 10 || 2);
1160
+ right[i] = (Math.random() * 2 - 1) * Math.pow(1 - n, params.room * 10 || 2);
1161
+ }
1162
+
1163
+ convolver.buffer = impulse;
1164
+ return [convolver, wetGain];
1165
+ },
1166
+ wobble: (params) => {
1167
+ const filter = this.context.createBiquadFilter();
1168
+ const lfo = this.context.createOscillator();
1169
+ const lfoGain = this.context.createGain();
1170
+
1171
+ filter.type = "lowpass";
1172
+ filter.frequency.value = 1000;
1173
+
1174
+ lfo.frequency.value = 1 / (params.phase || 6);
1175
+ lfoGain.gain.value = 2000;
1176
+
1177
+ lfo.connect(lfoGain);
1178
+ lfoGain.connect(filter.frequency);
1179
+ lfo.start();
1180
+
1181
+ return [filter];
1182
+ }
1183
+ };
1184
+
1185
+ // Apply effects chain
1186
+ const applyEffects = (source, effects) => {
1187
+ if (!effects || !effects.length) return source;
1188
+
1189
+ let currentNode = source;
1190
+ effects.forEach((effect) => {
1191
+ if (createEffect[effect.type]) {
1192
+ const nodes = createEffect[effect.type](effect.params);
1193
+ currentNode.connect(nodes[0]);
1194
+ currentNode = nodes[nodes.length - 1];
1195
+
1196
+ if (nodes.length > 1) {
1197
+ nodes[0].connect(nodes[1]);
1198
+ nodes[1].connect(nodes[0]);
1199
+ }
1200
+ }
1201
+ });
1202
+
1203
+ return currentNode;
1204
+ };
1205
+
1206
+ // Create synth with configurations
1207
+ const createSynth = (frequency, synthType, params = {}) => {
1208
+ // Use createSound() for the sound definition
1209
+ this.createSound("temp", frequency, "simple");
1210
+ const soundDef = this.sounds.get("temp");
1211
+
1212
+ // Create and connect nodes like createSynth() currently does
1213
+ const oscillator = this.context.createOscillator();
1214
+ const gainNode = this.context.createGain();
1215
+ const stereoPanner = this.context.createStereoPanner();
1216
+
1217
+ // Use sound definition from createSound()
1218
+ oscillator.type = soundDef.oscillatorType;
1219
+ oscillator.frequency.value = soundDef.frequency;
1220
+
1221
+ // Keep existing stereo and gain setup
1222
+ stereoPanner.pan.value = params.pan || 0;
1223
+
1224
+ // Keep existing gain envelope setup
1225
+ const duration = params.duration || 1;
1226
+ gainNode.gain.setValueAtTime(0, this.context.currentTime);
1227
+ gainNode.gain.linearRampToValueAtTime(1, this.context.currentTime + 0.01);
1228
+ gainNode.gain.linearRampToValueAtTime(0, this.context.currentTime + duration);
1229
+
1230
+ // Keep existing connections
1231
+ oscillator.connect(gainNode);
1232
+ gainNode.connect(stereoPanner);
1233
+ stereoPanner.connect(this.masterGain);
1234
+
1235
+ return { oscillator, gainNode, stereoPanner };
1236
+ };
1237
+ // Create sample functionality
1238
+ const createSample = (sampleName, params = {}) => {
1239
+ const definition = this.sampleDefinitions.get(sampleName);
1240
+
1241
+ if (definition?.soundType === "midi") {
1242
+ const gainNode = this.context.createGain();
1243
+
1244
+ const midiPlayer = {
1245
+ start: (scheduleTime) => {
1246
+ const noteToPlay = params.note || 60;
1247
+ const velocity = Math.floor(definition.amp * 127);
1248
+ const duration = params.duration || 0.1;
1249
+ const delayMs = (scheduleTime - this.context.currentTime) * 1000;
1250
+
1251
+ setTimeout(() => {
1252
+ this.sf2Player.program = this.midiProgramMap[definition.instrument];
1253
+ this.sf2Player.noteOn(noteToPlay, velocity);
1254
+ // Track the note
1255
+ midiNotes.push({
1256
+ note: noteToPlay,
1257
+ velocity: velocity,
1258
+ channel: 0 // Or whatever channel you're using
1259
+ });
1260
+ const stopTimeout = setTimeout(() => {
1261
+ this.sf2Player.noteOff(noteToPlay);
1262
+ this.scheduledMIDIEvents.delete(stopTimeout);
1263
+ }, duration * 1000);
1264
+ this.scheduledMIDIEvents.add(stopTimeout);
1265
+ }, delayMs);
1266
+
1267
+ gainNode.gain.setValueAtTime(0, scheduleTime);
1268
+ gainNode.gain.linearRampToValueAtTime(params.amp || 1, scheduleTime + (params.attack || 0.01));
1269
+ gainNode.gain.linearRampToValueAtTime(
1270
+ 0,
1271
+ scheduleTime + (params.attack || 0.01) + (params.release || 1)
1272
+ );
1273
+ },
1274
+ stop: () => {
1275
+ const noteToPlay = params.note || 60;
1276
+ this.sf2Player.noteOff(noteToPlay);
1277
+ },
1278
+ connect: (target) => {
1279
+ gainNode.connect(target);
1280
+ }
1281
+ };
1282
+
1283
+ return { oscillator: midiPlayer, gainNode };
1284
+ }
1285
+
1286
+ if (!this.samples.has(sampleName)) {
1287
+ console.warn(`[AudioManager] No synthetic sample found for ${sampleName}`);
1288
+ return createSynth(440, "sine", params);
1289
+ }
1290
+
1291
+ const source = this.context.createBufferSource();
1292
+ const gainNode = this.context.createGain();
1293
+
1294
+ source.buffer = this.samples.get(sampleName);
1295
+ source.connect(gainNode);
1296
+ gainNode.connect(this.masterGain);
1297
+
1298
+ source.playbackRate.value = params.rate || 1;
1299
+ gainNode.gain.value = params.amp || 0.5;
1300
+
1301
+ return { oscillator: source, gainNode };
1302
+ };
1303
+
1304
+ // Process all events sequentially
1305
+ sound.parsedSequence.forEach((event) => {
1306
+ const eventTime = startTime + event.time;
1307
+
1308
+ if (event.command !== "play" && event.command !== "sample") {
1309
+ return;
1310
+ }
1311
+
1312
+ // Create a timeout for this event in the sequence
1313
+ const sequenceTimeout = setTimeout(
1314
+ () => {
1315
+ if (!isPlaying) return; // Skip if we've been stopped
1316
+
1317
+ const {
1318
+ oscillator: source,
1319
+ gainNode,
1320
+ stereoPanner
1321
+ } = event.command === "sample"
1322
+ ? createSample(event.sample, event)
1323
+ : createSynth(event.note, event.synth || "simple", {
1324
+ oscillatorType: event.type || "sine",
1325
+ duration: event.duration || 1,
1326
+ pan: event.pan
1327
+ });
1328
+
1329
+ const attack = event.attack || 0.01;
1330
+ const release = event.release || 1;
1331
+ const amp = event.amp || 0.5;
1332
+
1333
+ let outputNode = gainNode;
1334
+ if (event.effects && event.effects.length) {
1335
+ outputNode = applyEffects(gainNode, event.effects);
1336
+ }
1337
+
1338
+ if (event.pan) {
1339
+ const panner = this.context.createStereoPanner();
1340
+ panner.pan.value = event.pan;
1341
+ outputNode.connect(panner);
1342
+ panner.connect(this.masterGain);
1343
+ } else {
1344
+ outputNode.connect(this.masterGain);
1345
+ }
1346
+
1347
+ gainNode.gain.setValueAtTime(0, eventTime);
1348
+ gainNode.gain.linearRampToValueAtTime(amp, eventTime + attack);
1349
+ gainNode.gain.linearRampToValueAtTime(0, eventTime + attack + release);
1350
+
1351
+ if (event.command === "sample") {
1352
+ source.start(eventTime);
1353
+ source.stop(eventTime + attack + release);
1354
+ } else {
1355
+ source.frequency.setValueAtTime(this.midiToFrequency(event.note), eventTime);
1356
+ source.start(eventTime);
1357
+ source.stop(eventTime + attack + release);
1358
+ }
1359
+
1360
+ allNodes.push(source);
1361
+ },
1362
+ (eventTime - this.context.currentTime) * 1000
1363
+ );
1364
+
1365
+ // Track this timeout
1366
+ this.scheduledMIDIEvents.add(sequenceTimeout);
1367
+ });
1368
+
1369
+ // Return the control object with everything needed to stop the sequence
1370
+ return {
1371
+ stop: () => {
1372
+ isPlaying = false;
1373
+ allNodes.forEach((node) => {
1374
+ try {
1375
+ if (node.stop) node.stop();
1376
+ } catch (e) {
1377
+ // Node might have already stopped
1378
+ }
1379
+ });
1380
+ // Stop any MIDI notes
1381
+ midiNotes.forEach((note) => {
1382
+ if (this.sf2Player) {
1383
+ this.sf2Player.noteOff(note.note, note.velocity || 127, note.channel || 0);
1384
+ }
1385
+ });
1386
+ // Clear any pending timeouts
1387
+ this.scheduledMIDIEvents.forEach((timeout) => {
1388
+ clearTimeout(timeout);
1389
+ });
1390
+ this.scheduledMIDIEvents.clear();
1391
+ },
1392
+ nodes: allNodes,
1393
+ midiNotes: midiNotes,
1394
+ isPlaying: () => isPlaying
1395
+ };
1396
+ }
1397
+
1398
+ // ADSR envelope utility (enhanced with volume support)
1399
+ applyEnvelope(gainParam, envelope, duration, volume = 1.0) {
1400
+ const { attack, decay, sustain, release } = envelope;
1401
+ const now = this.context.currentTime;
1402
+
1403
+ gainParam.setValueAtTime(0, now);
1404
+ gainParam.linearRampToValueAtTime(volume, now + attack);
1405
+ gainParam.linearRampToValueAtTime(volume * sustain, now + attack + decay);
1406
+ gainParam.linearRampToValueAtTime(0, now + duration);
1407
+ }
1408
+
1409
+ setVolume(value) {
1410
+ if (this.masterGain) {
1411
+ const scaledVolume = this.baseVolume * value;
1412
+ this.masterGain.gain.setValueAtTime(scaledVolume, this.context.currentTime);
1413
+ }
1414
+ }
1415
+
1416
+ toggle() {
1417
+ this.enabled = !this.enabled;
1418
+ if (this.context) {
1419
+ if (this.enabled) {
1420
+ this.context.resume();
1421
+ } else {
1422
+ this.context.suspend();
1423
+ }
1424
+ }
1425
+
1426
+ // Stop any active MIDI events if disabling
1427
+ if (!this.enabled) {
1428
+ this.scheduledMIDIEvents.forEach((timeout) => {
1429
+ clearTimeout(timeout);
1430
+ });
1431
+ this.scheduledMIDIEvents.clear();
1432
+
1433
+ // Stop MIDI notes if sf2Player exists
1434
+ if (this.sf2Player) {
1435
+ this.stopAllSounds();
1436
+ }
1437
+ }
1438
+
1439
+ return this.enabled;
1440
+ }
1441
+ }
1442
+
1443
+ class SonicPiParser {
1444
+ constructor(context) {
1445
+ this.context = context;
1446
+ this.functions = new Map();
1447
+ this.bpm = 60;
1448
+ this.beatDuration = 60 / this.bpm;
1449
+ this.effectsStack = [];
1450
+ }
1451
+
1452
+ parseScript(script) {
1453
+ const bpmMatch = script.match(/use_bpm\s+(\d+)/);
1454
+ if (bpmMatch) {
1455
+ this.bpm = parseInt(bpmMatch[1]);
1456
+ this.beatDuration = 60 / this.bpm;
1457
+ }
1458
+
1459
+ const functionMatches = script.matchAll(/define\s+:(\w+)\s+do\s*([\s\S]*?)\s*end/g);
1460
+ for (const match of functionMatches) {
1461
+ this.functions.set(match[1], match[2].trim());
1462
+ }
1463
+
1464
+ const sequence = [];
1465
+ let currentTime = 0;
1466
+
1467
+ const mainScript = script.replace(/define\s+:\w+\s+do[\s\S]*?end/g, "").trim();
1468
+ currentTime = this.processScript(mainScript, sequence, currentTime);
1469
+
1470
+ return {
1471
+ sequence: sequence.sort((a, b) => a.time - b.time),
1472
+ bpm: this.bpm,
1473
+ totalDuration: currentTime
1474
+ };
1475
+ }
1476
+
1477
+ processScript(script, sequence, startTime) {
1478
+ let currentTime = startTime;
1479
+ const lines = script.split("\n");
1480
+
1481
+ for (let i = 0; i < lines.length; i++) {
1482
+ let line = lines[i].trim();
1483
+ if (!line || line.startsWith("#")) continue;
1484
+
1485
+ const fxMatch = line.match(/with_fx\s+:(\w+)(?:\s*,\s*(.+))?\s+do/);
1486
+ if (fxMatch) {
1487
+ const [_, fxType, paramString] = fxMatch;
1488
+ const fxParams = this.parseParams(paramString || "");
1489
+
1490
+ this.effectsStack.push({
1491
+ type: fxType,
1492
+ params: fxParams
1493
+ });
1494
+
1495
+ let fxContent = "";
1496
+ let depth = 1;
1497
+ let j = i + 1;
1498
+
1499
+ while (depth > 0 && j < lines.length) {
1500
+ if (lines[j].includes("with_fx")) depth++;
1501
+ if (lines[j].trim() === "end") depth--;
1502
+ if (depth > 0) fxContent += lines[j] + "\n";
1503
+ j++;
1504
+ }
1505
+
1506
+ currentTime = this.processScript(fxContent, sequence, currentTime);
1507
+
1508
+ this.effectsStack.pop();
1509
+
1510
+ i = j - 1;
1511
+ continue;
1512
+ }
1513
+
1514
+ const timesMatch = line.match(/(\d+)\.times\s+do/);
1515
+ if (timesMatch) {
1516
+ const count = parseInt(timesMatch[1]);
1517
+ let loopContent = "";
1518
+ let depth = 1;
1519
+ let j = i + 1;
1520
+
1521
+ while (depth > 0 && j < lines.length) {
1522
+ if (lines[j].includes(".times do")) depth++;
1523
+ if (lines[j].trim() === "end") depth--;
1524
+ if (depth > 0) loopContent += lines[j] + "\n";
1525
+ j++;
1526
+ }
1527
+
1528
+ for (let k = 0; k < count; k++) {
1529
+ currentTime = this.processScript(loopContent, sequence, currentTime);
1530
+ }
1531
+
1532
+ i = j - 1;
1533
+ continue;
1534
+ }
1535
+
1536
+ const sleepMatch = line.match(/sleep\s+([\d.]+)/);
1537
+ if (sleepMatch) {
1538
+ currentTime += parseFloat(sleepMatch[1]) * this.beatDuration;
1539
+ continue;
1540
+ }
1541
+
1542
+ const sampleMatch = line.match(/sample\s+:(\w+)(?:\s*,\s*(.+))?/);
1543
+ if (sampleMatch) {
1544
+ const [_, name, paramString] = sampleMatch;
1545
+ const params = this.parseParams(paramString || "");
1546
+ sequence.push({
1547
+ command: "sample",
1548
+ time: currentTime,
1549
+ sample: name,
1550
+ effects: [...this.effectsStack],
1551
+ ...params
1552
+ });
1553
+ continue;
1554
+ }
1555
+
1556
+ const playMatch = line.match(/play\s+(\d+)(?:\s*,\s*(.+))?/);
1557
+ if (playMatch) {
1558
+ const [_, note, paramString] = playMatch;
1559
+ const params = this.parseParams(paramString || "");
1560
+ sequence.push({
1561
+ command: "play",
1562
+ time: currentTime,
1563
+ note: parseInt(note),
1564
+ effects: [...this.effectsStack],
1565
+ ...params
1566
+ });
1567
+ continue;
1568
+ }
1569
+
1570
+ const functionCall = line.match(/:?(\w+)/);
1571
+ if (functionCall && this.functions.has(functionCall[1])) {
1572
+ currentTime = this.processScript(this.functions.get(functionCall[1]), sequence, currentTime);
1573
+ }
1574
+ }
1575
+
1576
+ return currentTime;
1577
+ }
1578
+
1579
+ parseParams(paramString) {
1580
+ if (!paramString) return {};
1581
+ const params = {};
1582
+ const matches = paramString.match(/(\w+):\s*([^,\s]+)/g) || [];
1583
+ matches.forEach((match) => {
1584
+ const [key, value] = match.split(":").map((s) => s.trim());
1585
+ params[key] = isNaN(value) ? value : parseFloat(value);
1586
+ });
1587
+ return params;
1588
+ }
1589
+ }