@guinetik/gcanvas 1.0.5 → 2.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 (78) hide show
  1. package/dist/aizawa.html +27 -0
  2. package/dist/clifford.html +25 -0
  3. package/dist/cmb.html +24 -0
  4. package/dist/dadras.html +26 -0
  5. package/dist/dejong.html +25 -0
  6. package/dist/gcanvas.es.js +5130 -372
  7. package/dist/gcanvas.es.min.js +1 -1
  8. package/dist/gcanvas.umd.js +1 -1
  9. package/dist/gcanvas.umd.min.js +1 -1
  10. package/dist/halvorsen.html +27 -0
  11. package/dist/index.html +96 -48
  12. package/dist/js/aizawa.js +425 -0
  13. package/dist/js/bezier.js +5 -5
  14. package/dist/js/clifford.js +236 -0
  15. package/dist/js/cmb.js +594 -0
  16. package/dist/js/dadras.js +405 -0
  17. package/dist/js/dejong.js +257 -0
  18. package/dist/js/halvorsen.js +405 -0
  19. package/dist/js/isometric.js +34 -46
  20. package/dist/js/lorenz.js +425 -0
  21. package/dist/js/painter.js +8 -8
  22. package/dist/js/rossler.js +480 -0
  23. package/dist/js/schrodinger.js +314 -18
  24. package/dist/js/thomas.js +394 -0
  25. package/dist/lorenz.html +27 -0
  26. package/dist/rossler.html +27 -0
  27. package/dist/scene-interactivity-test.html +220 -0
  28. package/dist/thomas.html +27 -0
  29. package/package.json +1 -1
  30. package/readme.md +30 -22
  31. package/src/game/objects/go.js +7 -0
  32. package/src/game/objects/index.js +2 -0
  33. package/src/game/objects/isometric-scene.js +53 -3
  34. package/src/game/objects/layoutscene.js +57 -0
  35. package/src/game/objects/mask.js +241 -0
  36. package/src/game/objects/scene.js +19 -0
  37. package/src/game/objects/wrapper.js +14 -2
  38. package/src/game/pipeline.js +17 -0
  39. package/src/game/ui/button.js +101 -16
  40. package/src/game/ui/theme.js +0 -6
  41. package/src/game/ui/togglebutton.js +25 -14
  42. package/src/game/ui/tooltip.js +12 -4
  43. package/src/index.js +3 -0
  44. package/src/io/gesture.js +409 -0
  45. package/src/io/index.js +4 -1
  46. package/src/io/keys.js +9 -1
  47. package/src/io/screen.js +476 -0
  48. package/src/math/attractors.js +664 -0
  49. package/src/math/heat.js +106 -0
  50. package/src/math/index.js +1 -0
  51. package/src/mixins/draggable.js +15 -19
  52. package/src/painter/painter.shapes.js +11 -5
  53. package/src/particle/particle-system.js +165 -1
  54. package/src/physics/index.js +26 -0
  55. package/src/physics/physics-updaters.js +333 -0
  56. package/src/physics/physics.js +375 -0
  57. package/src/shapes/image.js +5 -5
  58. package/src/shapes/index.js +2 -0
  59. package/src/shapes/parallelogram.js +147 -0
  60. package/src/shapes/righttriangle.js +115 -0
  61. package/src/shapes/svg.js +281 -100
  62. package/src/shapes/text.js +22 -6
  63. package/src/shapes/transformable.js +5 -0
  64. package/src/sound/effects.js +807 -0
  65. package/src/sound/index.js +13 -0
  66. package/src/webgl/index.js +7 -0
  67. package/src/webgl/shaders/clifford-point-shaders.js +131 -0
  68. package/src/webgl/shaders/dejong-point-shaders.js +131 -0
  69. package/src/webgl/shaders/point-sprite-shaders.js +152 -0
  70. package/src/webgl/webgl-clifford-renderer.js +477 -0
  71. package/src/webgl/webgl-dejong-renderer.js +472 -0
  72. package/src/webgl/webgl-line-renderer.js +391 -0
  73. package/src/webgl/webgl-particle-renderer.js +410 -0
  74. package/types/index.d.ts +30 -2
  75. package/types/io.d.ts +217 -0
  76. package/types/physics.d.ts +299 -0
  77. package/types/shapes.d.ts +8 -0
  78. package/types/webgl.d.ts +188 -109
@@ -0,0 +1,807 @@
1
+ /**
2
+ * Audio Effects Primitives
3
+ * Standalone audio effect processors for Web Audio API
4
+ * @module sound/effects
5
+ */
6
+
7
+ /**
8
+ * Flanger effect - Short delay with LFO modulation for jet sweep sound
9
+ * @class Flanger
10
+ */
11
+ export class Flanger {
12
+ /**
13
+ * Create a flanger effect
14
+ * @param {AudioContext} audioContext - The audio context
15
+ * @param {Object} options - Flanger options
16
+ * @param {number} [options.baseDelay=0.005] - Base delay time in seconds (default: 5ms)
17
+ * @param {number} [options.maxDelay=0.02] - Maximum delay time in seconds (default: 20ms)
18
+ * @param {number} [options.lfoFrequency=0.5] - LFO frequency in Hz (default: 0.5Hz)
19
+ * @param {number} [options.lfoDepth=0.002] - LFO modulation depth in seconds (default: 2ms)
20
+ * @param {number} [options.feedback=0.5] - Feedback amount (0-1, default: 0.5)
21
+ * @param {number} [options.wet=0] - Wet signal level (0-1, default: 0)
22
+ * @param {number} [options.dry=1.0] - Dry signal level (0-1, default: 1.0)
23
+ */
24
+ constructor(audioContext, options = {}) {
25
+ this.audioContext = audioContext;
26
+ const {
27
+ baseDelay = 0.005,
28
+ maxDelay = 0.02,
29
+ lfoFrequency = 0.5,
30
+ lfoDepth = 0.002,
31
+ feedback = 0.5,
32
+ wet = 0,
33
+ dry = 1.0,
34
+ } = options;
35
+
36
+ // Delay node
37
+ this.delay = audioContext.createDelay(maxDelay);
38
+ this.delay.delayTime.value = baseDelay;
39
+
40
+ // LFO for modulation
41
+ this.lfo = audioContext.createOscillator();
42
+ this.lfo.type = 'sine';
43
+ this.lfo.frequency.value = lfoFrequency;
44
+
45
+ // LFO depth control
46
+ this.lfoDepth = audioContext.createGain();
47
+ this.lfoDepth.gain.value = lfoDepth;
48
+
49
+ // Feedback
50
+ this.feedback = audioContext.createGain();
51
+ this.feedback.gain.value = feedback;
52
+
53
+ // Wet/dry mix
54
+ this.wetGain = audioContext.createGain();
55
+ this.wetGain.gain.value = wet;
56
+ this.dryGain = audioContext.createGain();
57
+ this.dryGain.gain.value = dry;
58
+
59
+ // Output merger
60
+ this.output = audioContext.createGain();
61
+ this.output.gain.value = 1.0;
62
+
63
+ // Connect LFO to delay time
64
+ this.lfo.connect(this.lfoDepth);
65
+ this.lfoDepth.connect(this.delay.delayTime);
66
+
67
+ // Feedback loop
68
+ this.delay.connect(this.feedback);
69
+ this.feedback.connect(this.delay);
70
+
71
+ // Start LFO
72
+ this.lfo.start();
73
+
74
+ // Input node (connect your source here)
75
+ this.input = audioContext.createGain();
76
+ }
77
+
78
+ /**
79
+ * Connect the flanger to an audio node
80
+ * @param {AudioNode} source - Source node to connect
81
+ */
82
+ connect(source) {
83
+ // Dry path
84
+ source.connect(this.dryGain);
85
+ this.dryGain.connect(this.output);
86
+
87
+ // Wet path through flanger
88
+ source.connect(this.delay);
89
+ this.delay.connect(this.wetGain);
90
+ this.wetGain.connect(this.output);
91
+ }
92
+
93
+ /**
94
+ * Set wet/dry mix
95
+ * @param {number} wet - Wet level (0-1)
96
+ * @param {number} dry - Dry level (0-1)
97
+ */
98
+ setMix(wet, dry) {
99
+ this.wetGain.gain.value = wet;
100
+ this.dryGain.gain.value = dry;
101
+ }
102
+
103
+ /**
104
+ * Set LFO frequency
105
+ * @param {number} frequency - LFO frequency in Hz
106
+ */
107
+ setLFOFrequency(frequency) {
108
+ this.lfo.frequency.value = frequency;
109
+ }
110
+
111
+ /**
112
+ * Set feedback amount
113
+ * @param {number} feedback - Feedback amount (0-1)
114
+ */
115
+ setFeedback(feedback) {
116
+ this.feedback.gain.value = feedback;
117
+ }
118
+
119
+ /**
120
+ * Disconnect and clean up
121
+ */
122
+ disconnect() {
123
+ try {
124
+ this.lfo.stop();
125
+ this.lfo.disconnect();
126
+ this.delay.disconnect();
127
+ this.feedback.disconnect();
128
+ this.wetGain.disconnect();
129
+ this.dryGain.disconnect();
130
+ this.lfoDepth.disconnect();
131
+ this.input.disconnect();
132
+ this.output.disconnect();
133
+ } catch (e) {
134
+ // Ignore errors
135
+ }
136
+ }
137
+ }
138
+
139
+ /**
140
+ * DJ Filter - Sweeping lowpass/highpass filter
141
+ * @class DJFilter
142
+ */
143
+ export class DJFilter {
144
+ /**
145
+ * Create a DJ filter
146
+ * @param {AudioContext} audioContext - The audio context
147
+ * @param {Object} options - Filter options
148
+ * @param {string} [options.type='lowpass'] - Filter type ('lowpass' or 'highpass')
149
+ * @param {number} [options.frequency=20000] - Initial frequency in Hz (default: 20000)
150
+ * @param {number} [options.Q=1] - Q factor/resonance (default: 1)
151
+ */
152
+ constructor(audioContext, options = {}) {
153
+ this.audioContext = audioContext;
154
+ const {
155
+ type = 'lowpass',
156
+ frequency = 20000,
157
+ Q = 1,
158
+ } = options;
159
+
160
+ this.filter = audioContext.createBiquadFilter();
161
+ this.filter.type = type;
162
+ this.filter.frequency.value = frequency;
163
+ this.filter.Q.value = Q;
164
+ }
165
+
166
+ /**
167
+ * Connect the filter to an audio node
168
+ * @param {AudioNode} source - Source node to connect
169
+ */
170
+ connect(source) {
171
+ source.connect(this.filter);
172
+ }
173
+
174
+ /**
175
+ * Set filter frequency
176
+ * @param {number} frequency - Frequency in Hz
177
+ */
178
+ setFrequency(frequency) {
179
+ this.filter.frequency.value = frequency;
180
+ }
181
+
182
+ /**
183
+ * Set Q factor
184
+ * @param {number} Q - Q factor/resonance
185
+ */
186
+ setQ(Q) {
187
+ this.filter.Q.value = Q;
188
+ }
189
+
190
+ /**
191
+ * Set filter type
192
+ * @param {string} type - Filter type ('lowpass' or 'highpass')
193
+ */
194
+ setType(type) {
195
+ this.filter.type = type;
196
+ }
197
+
198
+ /**
199
+ * Get the filter node (for chaining)
200
+ * @returns {BiquadFilterNode}
201
+ */
202
+ getNode() {
203
+ return this.filter;
204
+ }
205
+
206
+ /**
207
+ * Disconnect and clean up
208
+ */
209
+ disconnect() {
210
+ try {
211
+ this.filter.disconnect();
212
+ } catch (e) {
213
+ // Ignore errors
214
+ }
215
+ }
216
+ }
217
+
218
+ /**
219
+ * EQ Filter Bank - Multiple peaking filters for equalization
220
+ * @class EQFilterBank
221
+ */
222
+ export class EQFilterBank {
223
+ /**
224
+ * Create an EQ filter bank
225
+ * @param {AudioContext} audioContext - The audio context
226
+ * @param {Object} options - EQ options
227
+ * @param {number} [options.numBands=16] - Number of bands (default: 16)
228
+ * @param {number} [options.minFreq=40] - Minimum frequency in Hz (default: 40)
229
+ * @param {number} [options.maxFreq=12000] - Maximum frequency in Hz (default: 12000)
230
+ * @param {number} [options.Q=4] - Q factor/resonance (default: 4)
231
+ */
232
+ constructor(audioContext, options = {}) {
233
+ this.audioContext = audioContext;
234
+ const {
235
+ numBands = 16,
236
+ minFreq = 40,
237
+ maxFreq = 12000,
238
+ Q = 4,
239
+ } = options;
240
+
241
+ this.filters = [];
242
+ this.numBands = numBands;
243
+
244
+ // Create peaking filters
245
+ for (let i = 0; i < numBands; i++) {
246
+ const filter = audioContext.createBiquadFilter();
247
+ filter.type = 'peaking';
248
+
249
+ // Logarithmic frequency distribution
250
+ const freq = numBands > 1
251
+ ? minFreq * Math.pow(maxFreq / minFreq, i / (numBands - 1))
252
+ : (minFreq + maxFreq) / 2;
253
+
254
+ filter.frequency.value = freq;
255
+ filter.Q.value = Q;
256
+ filter.gain.value = 0; // Start flat
257
+
258
+ this.filters.push(filter);
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Connect the EQ bank to an audio node
264
+ * Chains all filters together
265
+ * @param {AudioNode} source - Source node to connect
266
+ */
267
+ connect(source) {
268
+ let currentNode = source;
269
+ for (const filter of this.filters) {
270
+ currentNode.connect(filter);
271
+ currentNode = filter;
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Set gain for a specific band
277
+ * @param {number} bandIndex - Band index (0 to numBands-1)
278
+ * @param {number} gain - Gain in dB
279
+ */
280
+ setBandGain(bandIndex, gain) {
281
+ if (bandIndex >= 0 && bandIndex < this.filters.length) {
282
+ this.filters[bandIndex].gain.value = gain;
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Set gain for multiple bands
288
+ * @param {Array<number>} gains - Array of gain values in dB
289
+ */
290
+ setBandGains(gains) {
291
+ for (let i = 0; i < Math.min(gains.length, this.filters.length); i++) {
292
+ this.filters[i].gain.value = gains[i];
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Get filter node at index (for chaining)
298
+ * @param {number} index - Filter index
299
+ * @returns {BiquadFilterNode}
300
+ */
301
+ getFilter(index) {
302
+ return this.filters[index];
303
+ }
304
+
305
+ /**
306
+ * Get all filters (for chaining)
307
+ * @returns {Array<BiquadFilterNode>}
308
+ */
309
+ getFilters() {
310
+ return this.filters;
311
+ }
312
+
313
+ /**
314
+ * Disconnect and clean up
315
+ */
316
+ disconnect() {
317
+ for (const filter of this.filters) {
318
+ try {
319
+ filter.disconnect();
320
+ } catch (e) {
321
+ // Ignore errors
322
+ }
323
+ }
324
+ }
325
+ }
326
+
327
+ /**
328
+ * High Shelf Filter - High frequency boost/clarity
329
+ * @class HighShelf
330
+ */
331
+ export class HighShelf {
332
+ /**
333
+ * Create a high shelf filter
334
+ * @param {AudioContext} audioContext - The audio context
335
+ * @param {Object} options - Filter options
336
+ * @param {number} [options.frequency=4000] - Shelf frequency in Hz (default: 4000)
337
+ * @param {number} [options.gain=3] - Gain in dB (default: 3)
338
+ */
339
+ constructor(audioContext, options = {}) {
340
+ this.audioContext = audioContext;
341
+ const {
342
+ frequency = 4000,
343
+ gain = 3,
344
+ } = options;
345
+
346
+ this.filter = audioContext.createBiquadFilter();
347
+ this.filter.type = 'highshelf';
348
+ this.filter.frequency.value = frequency;
349
+ this.filter.gain.value = gain;
350
+ }
351
+
352
+ /**
353
+ * Connect the filter to an audio node
354
+ * @param {AudioNode} source - Source node to connect
355
+ */
356
+ connect(source) {
357
+ source.connect(this.filter);
358
+ }
359
+
360
+ /**
361
+ * Set shelf frequency
362
+ * @param {number} frequency - Frequency in Hz
363
+ */
364
+ setFrequency(frequency) {
365
+ this.filter.frequency.value = frequency;
366
+ }
367
+
368
+ /**
369
+ * Set gain
370
+ * @param {number} gain - Gain in dB
371
+ */
372
+ setGain(gain) {
373
+ this.filter.gain.value = gain;
374
+ }
375
+
376
+ /**
377
+ * Get the filter node (for chaining)
378
+ * @returns {BiquadFilterNode}
379
+ */
380
+ getNode() {
381
+ return this.filter;
382
+ }
383
+
384
+ /**
385
+ * Disconnect and clean up
386
+ */
387
+ disconnect() {
388
+ try {
389
+ this.filter.disconnect();
390
+ } catch (e) {
391
+ // Ignore errors
392
+ }
393
+ }
394
+ }
395
+
396
+ /**
397
+ * Advanced Delay - Delay with wet/dry control and feedback
398
+ * @class AdvancedDelay
399
+ */
400
+ export class AdvancedDelay {
401
+ /**
402
+ * Create an advanced delay effect
403
+ * @param {AudioContext} audioContext - The audio context
404
+ * @param {Object} options - Delay options
405
+ * @param {number} [options.delayTime=0.15] - Delay time in seconds (default: 0.15)
406
+ * @param {number} [options.maxDelay=0.5] - Maximum delay time in seconds (default: 0.5)
407
+ * @param {number} [options.feedback=0.2] - Feedback amount (0-1, default: 0.2)
408
+ * @param {number} [options.wet=0.15] - Wet signal level (0-1, default: 0.15)
409
+ * @param {number} [options.dry=0.85] - Dry signal level (0-1, default: 0.85)
410
+ */
411
+ constructor(audioContext, options = {}) {
412
+ this.audioContext = audioContext;
413
+ const {
414
+ delayTime = 0.15,
415
+ maxDelay = 0.5,
416
+ feedback = 0.2,
417
+ wet = 0.15,
418
+ dry = 0.85,
419
+ } = options;
420
+
421
+ // Delay node
422
+ this.delay = audioContext.createDelay(maxDelay);
423
+ this.delay.delayTime.value = delayTime;
424
+
425
+ // Feedback
426
+ this.feedbackGain = audioContext.createGain();
427
+ this.feedbackGain.gain.value = feedback;
428
+
429
+ // Wet/dry mix
430
+ this.wetGain = audioContext.createGain();
431
+ this.wetGain.gain.value = wet;
432
+ this.dryGain = audioContext.createGain();
433
+ this.dryGain.gain.value = dry;
434
+
435
+ // Output merger
436
+ this.output = audioContext.createGain();
437
+ this.output.gain.value = 1.0;
438
+
439
+ // Feedback loop
440
+ this.delay.connect(this.feedbackGain);
441
+ this.feedbackGain.connect(this.delay);
442
+
443
+ // Input node (connect your source here)
444
+ this.input = audioContext.createGain();
445
+ }
446
+
447
+ /**
448
+ * Connect the delay to an audio node
449
+ * @param {AudioNode} source - Source node to connect
450
+ */
451
+ connect(source) {
452
+ // Dry path
453
+ source.connect(this.dryGain);
454
+ this.dryGain.connect(this.output);
455
+
456
+ // Wet path
457
+ source.connect(this.delay);
458
+ this.delay.connect(this.wetGain);
459
+ this.wetGain.connect(this.output);
460
+ }
461
+
462
+ /**
463
+ * Set delay time
464
+ * @param {number} time - Delay time in seconds
465
+ */
466
+ setDelayTime(time) {
467
+ this.delay.delayTime.value = time;
468
+ }
469
+
470
+ /**
471
+ * Set wet/dry mix
472
+ * @param {number} wet - Wet level (0-1)
473
+ * @param {number} dry - Dry level (0-1)
474
+ */
475
+ setMix(wet, dry) {
476
+ this.wetGain.gain.value = wet;
477
+ this.dryGain.gain.value = dry;
478
+ }
479
+
480
+ /**
481
+ * Set feedback amount
482
+ * @param {number} feedback - Feedback amount (0-1)
483
+ */
484
+ setFeedback(feedback) {
485
+ this.feedbackGain.gain.value = feedback;
486
+ }
487
+
488
+ /**
489
+ * Get the output node (for chaining)
490
+ * @returns {GainNode}
491
+ */
492
+ getOutput() {
493
+ return this.output;
494
+ }
495
+
496
+ /**
497
+ * Disconnect and clean up
498
+ */
499
+ disconnect() {
500
+ try {
501
+ this.delay.disconnect();
502
+ this.feedbackGain.disconnect();
503
+ this.wetGain.disconnect();
504
+ this.dryGain.disconnect();
505
+ this.input.disconnect();
506
+ this.output.disconnect();
507
+ } catch (e) {
508
+ // Ignore errors
509
+ }
510
+ }
511
+ }
512
+
513
+ /**
514
+ * Advanced Distortion - WaveShaper with dynamic curve updates
515
+ * @class AdvancedDistortion
516
+ */
517
+ export class AdvancedDistortion {
518
+ /**
519
+ * Create an advanced distortion effect
520
+ * @param {AudioContext} audioContext - The audio context
521
+ * @param {Object} options - Distortion options
522
+ * @param {number} [options.amount=0] - Initial distortion amount (0-1, default: 0)
523
+ * @param {string} [options.oversample='4x'] - Oversampling ('none', '2x', '4x', default: '4x')
524
+ */
525
+ constructor(audioContext, options = {}) {
526
+ this.audioContext = audioContext;
527
+ const {
528
+ amount = 0,
529
+ oversample = '4x',
530
+ } = options;
531
+
532
+ this.shaper = audioContext.createWaveShaper();
533
+ this.shaper.oversample = oversample;
534
+ this.amount = amount;
535
+ this.updateCurve(amount);
536
+ }
537
+
538
+ /**
539
+ * Update the distortion curve
540
+ * @param {number} amount - Distortion amount (0-1)
541
+ */
542
+ updateCurve(amount) {
543
+ const samples = 44100;
544
+ const curve = new Float32Array(samples);
545
+ const deg = Math.PI / 180;
546
+
547
+ // Blend between clean and distorted based on amount
548
+ const k = amount * 400; // More extreme distortion
549
+
550
+ for (let i = 0; i < samples; i++) {
551
+ const x = (i * 2) / samples - 1;
552
+ if (k === 0) {
553
+ curve[i] = x;
554
+ } else {
555
+ curve[i] = ((3 + k) * x * 20 * deg) / (Math.PI + k * Math.abs(x));
556
+ }
557
+ }
558
+
559
+ this.shaper.curve = curve;
560
+ this.amount = amount;
561
+ }
562
+
563
+ /**
564
+ * Connect the distortion to an audio node
565
+ * @param {AudioNode} source - Source node to connect
566
+ */
567
+ connect(source) {
568
+ source.connect(this.shaper);
569
+ }
570
+
571
+ /**
572
+ * Set distortion amount
573
+ * @param {number} amount - Distortion amount (0-1)
574
+ */
575
+ setAmount(amount) {
576
+ this.updateCurve(amount);
577
+ }
578
+
579
+ /**
580
+ * Get the shaper node (for chaining)
581
+ * @returns {WaveShaperNode}
582
+ */
583
+ getNode() {
584
+ return this.shaper;
585
+ }
586
+
587
+ /**
588
+ * Disconnect and clean up
589
+ */
590
+ disconnect() {
591
+ try {
592
+ this.shaper.disconnect();
593
+ } catch (e) {
594
+ // Ignore errors
595
+ }
596
+ }
597
+ }
598
+
599
+ /**
600
+ * Advanced Tremolo - LFO amplitude modulation
601
+ * @class AdvancedTremolo
602
+ */
603
+ export class AdvancedTremolo {
604
+ /**
605
+ * Create an advanced tremolo effect
606
+ * @param {AudioContext} audioContext - The audio context
607
+ * @param {Object} options - Tremolo options
608
+ * @param {number} [options.rate=4] - Tremolo rate in Hz (default: 4)
609
+ * @param {number} [options.depth=0] - Tremolo depth (0-1, default: 0)
610
+ * @param {string} [options.lfoType='sine'] - LFO waveform type (default: 'sine')
611
+ */
612
+ constructor(audioContext, options = {}) {
613
+ this.audioContext = audioContext;
614
+ const {
615
+ rate = 4,
616
+ depth = 0,
617
+ lfoType = 'sine',
618
+ } = options;
619
+
620
+ // Gain node for amplitude modulation
621
+ this.gain = audioContext.createGain();
622
+ this.gain.gain.value = 1.0;
623
+
624
+ // LFO
625
+ this.lfo = audioContext.createOscillator();
626
+ this.lfo.type = lfoType;
627
+ this.lfo.frequency.value = rate;
628
+
629
+ // LFO depth control
630
+ this.depthGain = audioContext.createGain();
631
+ this.depthGain.gain.value = depth;
632
+
633
+ // Connect LFO to gain
634
+ this.lfo.connect(this.depthGain);
635
+ this.depthGain.connect(this.gain.gain);
636
+
637
+ // Start LFO
638
+ this.lfo.start();
639
+ }
640
+
641
+ /**
642
+ * Connect the tremolo to an audio node
643
+ * @param {AudioNode} source - Source node to connect
644
+ */
645
+ connect(source) {
646
+ source.connect(this.gain);
647
+ }
648
+
649
+ /**
650
+ * Set tremolo rate
651
+ * @param {number} rate - Rate in Hz
652
+ */
653
+ setRate(rate) {
654
+ this.lfo.frequency.value = rate;
655
+ }
656
+
657
+ /**
658
+ * Set tremolo depth
659
+ * @param {number} depth - Depth (0-1)
660
+ */
661
+ setDepth(depth) {
662
+ this.depthGain.gain.value = depth;
663
+ }
664
+
665
+ /**
666
+ * Get the gain node (for chaining)
667
+ * @returns {GainNode}
668
+ */
669
+ getNode() {
670
+ return this.gain;
671
+ }
672
+
673
+ /**
674
+ * Disconnect and clean up
675
+ */
676
+ disconnect() {
677
+ try {
678
+ this.lfo.stop();
679
+ this.lfo.disconnect();
680
+ this.depthGain.disconnect();
681
+ this.gain.disconnect();
682
+ } catch (e) {
683
+ // Ignore errors
684
+ }
685
+ }
686
+ }
687
+
688
+ /**
689
+ * Limiter - Compressor configured as a limiter to prevent clipping
690
+ * @class Limiter
691
+ */
692
+ export class Limiter {
693
+ /**
694
+ * Create a limiter
695
+ * @param {AudioContext} audioContext - The audio context
696
+ * @param {Object} options - Limiter options
697
+ * @param {number} [options.threshold=-3] - Threshold in dB (default: -3)
698
+ * @param {number} [options.knee=0] - Knee in dB (default: 0)
699
+ * @param {number} [options.ratio=20] - Ratio (default: 20, hard limit)
700
+ * @param {number} [options.attack=0.001] - Attack time in seconds (default: 0.001)
701
+ * @param {number} [options.release=0.1] - Release time in seconds (default: 0.1)
702
+ */
703
+ constructor(audioContext, options = {}) {
704
+ this.audioContext = audioContext;
705
+ const {
706
+ threshold = -3,
707
+ knee = 0,
708
+ ratio = 20,
709
+ attack = 0.001,
710
+ release = 0.1,
711
+ } = options;
712
+
713
+ this.compressor = audioContext.createDynamicsCompressor();
714
+ this.compressor.threshold.value = threshold;
715
+ this.compressor.knee.value = knee;
716
+ this.compressor.ratio.value = ratio;
717
+ this.compressor.attack.value = attack;
718
+ this.compressor.release.value = release;
719
+ }
720
+
721
+ /**
722
+ * Connect the limiter to an audio node
723
+ * @param {AudioNode} source - Source node to connect
724
+ */
725
+ connect(source) {
726
+ source.connect(this.compressor);
727
+ }
728
+
729
+ /**
730
+ * Set threshold
731
+ * @param {number} threshold - Threshold in dB
732
+ */
733
+ setThreshold(threshold) {
734
+ this.compressor.threshold.value = threshold;
735
+ }
736
+
737
+ /**
738
+ * Get the compressor node (for chaining)
739
+ * @returns {DynamicsCompressorNode}
740
+ */
741
+ getNode() {
742
+ return this.compressor;
743
+ }
744
+
745
+ /**
746
+ * Disconnect and clean up
747
+ */
748
+ disconnect() {
749
+ try {
750
+ this.compressor.disconnect();
751
+ } catch (e) {
752
+ // Ignore errors
753
+ }
754
+ }
755
+ }
756
+
757
+ /**
758
+ * Master Gain - Simple gain node for overall volume control
759
+ * @class MasterGain
760
+ */
761
+ export class MasterGain {
762
+ /**
763
+ * Create a master gain node
764
+ * @param {AudioContext} audioContext - The audio context
765
+ * @param {number} [volume=1.0] - Initial volume (0-1, default: 1.0)
766
+ */
767
+ constructor(audioContext, volume = 1.0) {
768
+ this.audioContext = audioContext;
769
+ this.gain = audioContext.createGain();
770
+ this.gain.gain.value = volume;
771
+ }
772
+
773
+ /**
774
+ * Connect the gain to an audio node
775
+ * @param {AudioNode} source - Source node to connect
776
+ */
777
+ connect(source) {
778
+ source.connect(this.gain);
779
+ }
780
+
781
+ /**
782
+ * Set volume
783
+ * @param {number} volume - Volume (0-1)
784
+ */
785
+ setVolume(volume) {
786
+ this.gain.gain.value = volume;
787
+ }
788
+
789
+ /**
790
+ * Get the gain node (for chaining)
791
+ * @returns {GainNode}
792
+ */
793
+ getNode() {
794
+ return this.gain;
795
+ }
796
+
797
+ /**
798
+ * Disconnect and clean up
799
+ */
800
+ disconnect() {
801
+ try {
802
+ this.gain.disconnect();
803
+ } catch (e) {
804
+ // Ignore errors
805
+ }
806
+ }
807
+ }