@affectively/entrainment-audio 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.
package/dist/index.mjs ADDED
@@ -0,0 +1,1146 @@
1
+ // src/presets.ts
2
+ var DELTA_PRESET = {
3
+ id: "delta",
4
+ name: "Delta",
5
+ description: "Deep sleep, unconsciousness, restoration. Stage 3-4 NREM sleep.",
6
+ frequencyRange: {
7
+ min: 0.5,
8
+ max: 4
9
+ },
10
+ targetFrequency: 2,
11
+ // Hz - middle of delta range
12
+ carrierFrequency: 450,
13
+ // Hz - within Oster Curve (400-500 Hz)
14
+ color: "#1e3a8a",
15
+ // Deep indigo/blue
16
+ useCase: "Sleep aids, deep restoration modules"
17
+ };
18
+ var THETA_PRESET = {
19
+ id: "theta",
20
+ name: "Theta",
21
+ description: "Deep meditation, creativity, memory encoding, hypnagogic state",
22
+ frequencyRange: {
23
+ min: 4,
24
+ max: 8
25
+ },
26
+ targetFrequency: 6,
27
+ // Hz - middle of theta range
28
+ carrierFrequency: 450,
29
+ // Hz - within Oster Curve
30
+ color: "#7c3aed",
31
+ // Purple/violet
32
+ useCase: "Meditation guides, creative brainstorming tools"
33
+ };
34
+ var ALPHA_PRESET = {
35
+ id: "alpha",
36
+ name: "Alpha",
37
+ description: 'Relaxed alertness, "flow" state, visual cortex idling',
38
+ frequencyRange: {
39
+ min: 8,
40
+ max: 12
41
+ },
42
+ targetFrequency: 10,
43
+ // Hz - middle of alpha range
44
+ carrierFrequency: 450,
45
+ // Hz - within Oster Curve
46
+ color: "#14b8a6",
47
+ // Teal/cyan
48
+ useCase: 'Stress relief, light focus, "calm" modes'
49
+ };
50
+ var LOW_BETA_PRESET = {
51
+ id: "low-beta",
52
+ name: "Low Beta (SMR)",
53
+ description: "Calm focus, stillness, sensorimotor rhythm. Used in ADHD neurofeedback",
54
+ frequencyRange: {
55
+ min: 12,
56
+ max: 15
57
+ },
58
+ targetFrequency: 13.5,
59
+ // Hz - middle of low beta range
60
+ carrierFrequency: 450,
61
+ // Hz - within Oster Curve
62
+ color: "#10b981",
63
+ // Green
64
+ useCase: "ADHD support, reading assistants"
65
+ };
66
+ var MID_BETA_PRESET = {
67
+ id: "mid-beta",
68
+ name: "Mid Beta",
69
+ description: "Active focus, problem-solving, sustained attention",
70
+ frequencyRange: {
71
+ min: 15,
72
+ max: 20
73
+ },
74
+ targetFrequency: 17.5,
75
+ // Hz - middle of mid beta range
76
+ carrierFrequency: 450,
77
+ // Hz - within Oster Curve
78
+ color: "#f59e0b",
79
+ // Yellow/amber
80
+ useCase: "Productivity timers, study aids"
81
+ };
82
+ var HIGH_BETA_PRESET = {
83
+ id: "high-beta",
84
+ name: "High Beta",
85
+ description: "High energy, excitement, complex thought. Can induce anxiety if prolonged",
86
+ frequencyRange: {
87
+ min: 20,
88
+ max: 30
89
+ },
90
+ targetFrequency: 25,
91
+ // Hz - middle of high beta range
92
+ carrierFrequency: 450,
93
+ // Hz - within Oster Curve
94
+ color: "#f97316",
95
+ // Orange
96
+ useCase: "Pre-workout energy; short-term alertness"
97
+ };
98
+ var GAMMA_PRESET = {
99
+ id: "gamma",
100
+ name: "Gamma",
101
+ description: "Peak performance, insight, cognitive binding. Cross-modal synchronization",
102
+ frequencyRange: {
103
+ min: 30,
104
+ max: 100
105
+ },
106
+ targetFrequency: 40,
107
+ // Hz - common gamma entrainment target
108
+ carrierFrequency: 450,
109
+ // Hz - within Oster Curve
110
+ color: "#e0e7ff",
111
+ // White/light blue
112
+ useCase: "Cognitive boosters, complex synthesis tasks"
113
+ };
114
+ var BRAINWAVE_PRESETS = {
115
+ delta: DELTA_PRESET,
116
+ theta: THETA_PRESET,
117
+ alpha: ALPHA_PRESET,
118
+ "low-beta": LOW_BETA_PRESET,
119
+ "mid-beta": MID_BETA_PRESET,
120
+ "high-beta": HIGH_BETA_PRESET,
121
+ gamma: GAMMA_PRESET
122
+ };
123
+ function getPreset(band) {
124
+ return BRAINWAVE_PRESETS[band];
125
+ }
126
+ function getPresetByFrequency(frequency) {
127
+ for (const preset of Object.values(BRAINWAVE_PRESETS)) {
128
+ if (frequency >= preset.frequencyRange.min && frequency <= preset.frequencyRange.max) {
129
+ return preset;
130
+ }
131
+ }
132
+ return null;
133
+ }
134
+ function validateOsterCurve(carrierFrequency) {
135
+ return carrierFrequency >= 400 && carrierFrequency <= 500;
136
+ }
137
+ function getRecommendedCarrierFrequency() {
138
+ return 450;
139
+ }
140
+
141
+ // src/safety.ts
142
+ var EPILEPSY_WARNING = {
143
+ id: "epilepsy",
144
+ title: "Epilepsy Warning",
145
+ message: "If you have a history of epilepsy or seizures, consult your healthcare provider before using this tool. Flashing visuals synchronized to audio frequencies may trigger seizures in some individuals.",
146
+ severity: "warning"
147
+ };
148
+ var DRIVING_WARNING = {
149
+ id: "driving",
150
+ title: "Do Not Use While Driving",
151
+ message: "Do not use this tool while operating a vehicle or machinery. Entrainment to low frequencies (Alpha, Theta, Delta) can induce drowsiness and altered states of consciousness, significantly slowing reaction times.",
152
+ severity: "critical"
153
+ };
154
+ var PACEMAKER_WARNING = {
155
+ id: "pacemaker",
156
+ title: "Pacemaker Warning",
157
+ message: "If you have a pacemaker or other implanted medical device, consult your healthcare provider before using headphones with strong magnetic drivers near the device.",
158
+ severity: "info"
159
+ };
160
+ var MENTAL_HEALTH_WARNING = {
161
+ id: "mental-health",
162
+ title: "Mental Health Considerations",
163
+ message: "If you have a history of severe mental health disorders (schizophrenia, psychosis, dissociation), use this tool with caution or under supervision. Altered states can sometimes exacerbate symptoms of dissociation or paranoia.",
164
+ severity: "warning"
165
+ };
166
+ var STROBING_WARNING = {
167
+ id: "strobing",
168
+ title: "Visual Strobing Warning",
169
+ message: "Visual effects synchronized to frequencies between 3-30 Hz may trigger photosensitive epilepsy. This tool uses smooth transitions only, but if you experience any discomfort, stop immediately.",
170
+ severity: "warning"
171
+ };
172
+ function getSafetyWarnings(config) {
173
+ const warnings = [];
174
+ if (config.hasVisualizer) {
175
+ warnings.push(EPILEPSY_WARNING);
176
+ warnings.push(STROBING_WARNING);
177
+ }
178
+ if (config.currentPreset) {
179
+ const preset = BRAINWAVE_PRESETS[config.currentPreset];
180
+ if (preset.id === "delta" || preset.id === "theta" || preset.id === "alpha") {
181
+ warnings.push(DRIVING_WARNING);
182
+ }
183
+ } else if (config.frequency) {
184
+ if (config.frequency <= 12) {
185
+ warnings.push(DRIVING_WARNING);
186
+ }
187
+ }
188
+ warnings.push(MENTAL_HEALTH_WARNING);
189
+ warnings.push(PACEMAKER_WARNING);
190
+ return warnings;
191
+ }
192
+ function isPhotosensitiveTriggerRange(frequency) {
193
+ return frequency >= 3 && frequency <= 30;
194
+ }
195
+ function getAllSafetyWarnings() {
196
+ return [
197
+ EPILEPSY_WARNING,
198
+ DRIVING_WARNING,
199
+ PACEMAKER_WARNING,
200
+ MENTAL_HEALTH_WARNING,
201
+ STROBING_WARNING
202
+ ];
203
+ }
204
+
205
+ // src/BinauralGenerator.ts
206
+ var BinauralGenerator = class {
207
+ constructor(ctx) {
208
+ this.nodes = null;
209
+ this.config = null;
210
+ this.ctx = ctx;
211
+ }
212
+ /**
213
+ * Start binaural beat generation
214
+ *
215
+ * @param config - Binaural generator configuration
216
+ * @param masterGain - Master gain node to connect to
217
+ */
218
+ start(config, masterGain) {
219
+ if (!validateOsterCurve(config.carrierFrequency)) {
220
+ throw new Error(
221
+ `Binaural beats require carrier frequency between 400-500 Hz (Oster Curve). Got ${config.carrierFrequency} Hz.`
222
+ );
223
+ }
224
+ this.stop();
225
+ this.config = config;
226
+ const leftOsc = this.ctx.createOscillator();
227
+ const rightOsc = this.ctx.createOscillator();
228
+ leftOsc.frequency.value = config.carrierFrequency;
229
+ rightOsc.frequency.value = config.carrierFrequency + config.beatFrequency;
230
+ const leftGain = this.ctx.createGain();
231
+ const rightGain = this.ctx.createGain();
232
+ leftGain.gain.value = config.volume;
233
+ rightGain.gain.value = config.volume;
234
+ const leftPan = this.ctx.createStereoPanner();
235
+ const rightPan = this.ctx.createStereoPanner();
236
+ leftPan.pan.value = -1;
237
+ rightPan.pan.value = 1;
238
+ leftOsc.connect(leftGain).connect(leftPan).connect(masterGain);
239
+ rightOsc.connect(rightGain).connect(rightPan).connect(masterGain);
240
+ leftOsc.start();
241
+ rightOsc.start();
242
+ this.nodes = {
243
+ leftOsc,
244
+ rightOsc,
245
+ leftPan,
246
+ rightPan,
247
+ leftGain,
248
+ rightGain
249
+ };
250
+ }
251
+ /**
252
+ * Update generator parameters
253
+ */
254
+ updateConfig(config) {
255
+ if (!this.nodes || !this.config) {
256
+ return;
257
+ }
258
+ const newConfig = { ...this.config, ...config };
259
+ this.config = newConfig;
260
+ if (config.volume !== void 0) {
261
+ this.nodes.leftGain.gain.value = config.volume;
262
+ this.nodes.rightGain.gain.value = config.volume;
263
+ }
264
+ if (config.carrierFrequency !== void 0 || config.beatFrequency !== void 0) {
265
+ const carrierFreq = config.carrierFrequency ?? this.config.carrierFrequency;
266
+ const beatFreq = config.beatFrequency ?? this.config.beatFrequency;
267
+ if (!validateOsterCurve(carrierFreq)) {
268
+ console.warn(
269
+ `Binaural beats require carrier frequency between 400-500 Hz. Got ${carrierFreq} Hz.`
270
+ );
271
+ }
272
+ this.nodes.leftOsc.frequency.value = carrierFreq;
273
+ this.nodes.rightOsc.frequency.value = carrierFreq + beatFreq;
274
+ }
275
+ }
276
+ /**
277
+ * Stop binaural beat generation
278
+ */
279
+ stop() {
280
+ if (this.nodes) {
281
+ try {
282
+ this.nodes.leftOsc.stop();
283
+ this.nodes.rightOsc.stop();
284
+ } catch {
285
+ }
286
+ this.nodes.leftOsc.disconnect();
287
+ this.nodes.rightOsc.disconnect();
288
+ this.nodes.leftGain.disconnect();
289
+ this.nodes.rightGain.disconnect();
290
+ this.nodes.leftPan.disconnect();
291
+ this.nodes.rightPan.disconnect();
292
+ this.nodes = null;
293
+ }
294
+ this.config = null;
295
+ }
296
+ /**
297
+ * Check if generator is active
298
+ */
299
+ isActive() {
300
+ return this.nodes !== null;
301
+ }
302
+ };
303
+
304
+ // src/IsochronicGenerator.ts
305
+ var IsochronicGenerator = class {
306
+ // Schedule 100ms ahead
307
+ constructor(ctx) {
308
+ this.nodes = null;
309
+ this.config = null;
310
+ this.isPlaying = false;
311
+ this.lookaheadTimer = null;
312
+ this.nextEventTime = 0;
313
+ this.scheduleAheadTime = 0.1;
314
+ this.ctx = ctx;
315
+ }
316
+ /**
317
+ * Start isochronic tone generation
318
+ *
319
+ * @param config - Isochronic generator configuration
320
+ * @param masterGain - Master gain node to connect to
321
+ */
322
+ start(config, masterGain) {
323
+ this.stop();
324
+ this.config = config;
325
+ this.isPlaying = true;
326
+ const carrier = this.ctx.createOscillator();
327
+ const gainNode = this.ctx.createGain();
328
+ carrier.frequency.value = config.carrierFrequency;
329
+ gainNode.gain.value = 0;
330
+ carrier.connect(gainNode).connect(masterGain);
331
+ carrier.start();
332
+ this.nodes = {
333
+ carrier,
334
+ gainNode,
335
+ scheduledEvents: []
336
+ };
337
+ this.nextEventTime = this.ctx.currentTime;
338
+ this.schedulePulses();
339
+ }
340
+ /**
341
+ * Schedule isochronic pulses using lookahead scheduling
342
+ * Prevents CPU overload by scheduling 1-2 seconds ahead
343
+ */
344
+ schedulePulses() {
345
+ if (!this.nodes || !this.config || !this.isPlaying) {
346
+ return;
347
+ }
348
+ const cycleDuration = 1 / this.config.beatFrequency;
349
+ const attackTime = this.config.attackTime / 1e3;
350
+ const releaseTime = this.config.releaseTime / 1e3;
351
+ const holdTime = cycleDuration * this.config.dutyCycle;
352
+ while (this.nextEventTime < this.ctx.currentTime + this.scheduleAheadTime) {
353
+ const pulseStart = this.nextEventTime;
354
+ const pulseEnd = pulseStart + holdTime;
355
+ this.nodes.gainNode.gain.setTargetAtTime(
356
+ this.config.volume,
357
+ pulseStart,
358
+ attackTime / 3
359
+ );
360
+ this.nodes.gainNode.gain.setTargetAtTime(0, pulseEnd, releaseTime / 3);
361
+ this.nextEventTime += cycleDuration;
362
+ }
363
+ this.lookaheadTimer = setTimeout(() => {
364
+ this.schedulePulses();
365
+ }, this.scheduleAheadTime * 1e3);
366
+ }
367
+ /**
368
+ * Update generator parameters
369
+ */
370
+ updateConfig(config) {
371
+ if (!this.nodes || !this.config) {
372
+ return;
373
+ }
374
+ const newConfig = { ...this.config, ...config };
375
+ this.config = newConfig;
376
+ if (config.carrierFrequency !== void 0) {
377
+ this.nodes.carrier.frequency.value = config.carrierFrequency;
378
+ }
379
+ if (config.volume !== void 0) {
380
+ this.nodes.gainNode.gain.setTargetAtTime(
381
+ config.volume,
382
+ this.ctx.currentTime,
383
+ 0.01
384
+ // 10ms transition
385
+ );
386
+ }
387
+ }
388
+ /**
389
+ * Stop isochronic tone generation
390
+ */
391
+ stop() {
392
+ this.isPlaying = false;
393
+ if (this.lookaheadTimer !== null) {
394
+ clearTimeout(this.lookaheadTimer);
395
+ this.lookaheadTimer = null;
396
+ }
397
+ if (this.nodes) {
398
+ try {
399
+ this.nodes.gainNode.gain.setTargetAtTime(0, this.ctx.currentTime, 0.01);
400
+ setTimeout(() => {
401
+ if (this.nodes) {
402
+ try {
403
+ this.nodes.carrier.stop();
404
+ } catch {
405
+ }
406
+ }
407
+ }, 20);
408
+ } catch {
409
+ }
410
+ this.nodes.carrier.disconnect();
411
+ this.nodes.gainNode.disconnect();
412
+ this.nodes = null;
413
+ }
414
+ this.config = null;
415
+ this.nextEventTime = 0;
416
+ }
417
+ /**
418
+ * Check if generator is active
419
+ */
420
+ isActive() {
421
+ return this.nodes !== null && this.isPlaying;
422
+ }
423
+ };
424
+
425
+ // src/MonauralGenerator.ts
426
+ var MonauralGenerator = class {
427
+ constructor(ctx) {
428
+ this.nodes = null;
429
+ this.config = null;
430
+ this.ctx = ctx;
431
+ }
432
+ /**
433
+ * Start monaural beat generation
434
+ *
435
+ * @param config - Monaural generator configuration
436
+ * @param masterGain - Master gain node to connect to
437
+ */
438
+ start(config, masterGain) {
439
+ this.stop();
440
+ this.config = config;
441
+ const frequency1 = config.frequency1;
442
+ const frequency2 = frequency1 + config.beatFrequency;
443
+ const osc1 = this.ctx.createOscillator();
444
+ const osc2 = this.ctx.createOscillator();
445
+ osc1.frequency.value = frequency1;
446
+ osc2.frequency.value = frequency2;
447
+ const gainNode = this.ctx.createGain();
448
+ gainNode.gain.value = config.volume;
449
+ osc1.connect(gainNode);
450
+ osc2.connect(gainNode);
451
+ gainNode.connect(masterGain);
452
+ osc1.start();
453
+ osc2.start();
454
+ this.nodes = {
455
+ osc1,
456
+ osc2,
457
+ gainNode
458
+ };
459
+ }
460
+ /**
461
+ * Update generator parameters
462
+ */
463
+ updateConfig(config) {
464
+ if (!this.nodes || !this.config) {
465
+ return;
466
+ }
467
+ const newConfig = { ...this.config, ...config };
468
+ this.config = newConfig;
469
+ if (config.volume !== void 0) {
470
+ this.nodes.gainNode.gain.setTargetAtTime(
471
+ config.volume,
472
+ this.ctx.currentTime,
473
+ 0.01
474
+ );
475
+ }
476
+ if (config.frequency1 !== void 0 || config.beatFrequency !== void 0) {
477
+ const freq1 = config.frequency1 ?? this.config.frequency1;
478
+ const beatFreq = config.beatFrequency ?? this.config.beatFrequency;
479
+ const freq2 = freq1 + beatFreq;
480
+ this.nodes.osc1.frequency.value = freq1;
481
+ this.nodes.osc2.frequency.value = freq2;
482
+ }
483
+ }
484
+ /**
485
+ * Stop monaural beat generation
486
+ */
487
+ stop() {
488
+ if (this.nodes) {
489
+ try {
490
+ this.nodes.osc1.stop();
491
+ this.nodes.osc2.stop();
492
+ } catch {
493
+ }
494
+ this.nodes.osc1.disconnect();
495
+ this.nodes.osc2.disconnect();
496
+ this.nodes.gainNode.disconnect();
497
+ this.nodes = null;
498
+ }
499
+ this.config = null;
500
+ }
501
+ /**
502
+ * Check if generator is active
503
+ */
504
+ isActive() {
505
+ return this.nodes !== null;
506
+ }
507
+ };
508
+
509
+ // src/BrownNoiseGenerator.ts
510
+ var BrownNoiseGenerator = class {
511
+ constructor(ctx) {
512
+ this.nodes = null;
513
+ this.volume = 0.15;
514
+ // Default subtle volume
515
+ this.buffer = null;
516
+ this.ctx = ctx;
517
+ }
518
+ /**
519
+ * Create brown noise buffer
520
+ * Generates a seamless loop of brown noise
521
+ */
522
+ createBrownNoiseBuffer() {
523
+ const bufferSize = this.ctx.sampleRate * 5;
524
+ const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
525
+ const data = buffer.getChannelData(0);
526
+ let lastOut = 0;
527
+ for (let i = 0; i < bufferSize; i++) {
528
+ const white = Math.random() * 2 - 1;
529
+ data[i] = (lastOut + 0.02 * white) / 1.02;
530
+ lastOut = data[i];
531
+ data[i] *= 3.5;
532
+ }
533
+ return buffer;
534
+ }
535
+ /**
536
+ * Start brown noise generation
537
+ *
538
+ * @param volume - Volume level (0.0 to 1.0)
539
+ * @param masterGain - Master gain node to connect to
540
+ */
541
+ start(volume, masterGain) {
542
+ this.volume = volume;
543
+ this.stop();
544
+ if (!this.buffer) {
545
+ this.buffer = this.createBrownNoiseBuffer();
546
+ }
547
+ const bufferSource = this.ctx.createBufferSource();
548
+ bufferSource.buffer = this.buffer;
549
+ bufferSource.loop = true;
550
+ const gainNode = this.ctx.createGain();
551
+ gainNode.gain.value = volume;
552
+ bufferSource.connect(gainNode).connect(masterGain);
553
+ bufferSource.start();
554
+ this.nodes = {
555
+ bufferSource,
556
+ gainNode
557
+ };
558
+ }
559
+ /**
560
+ * Update volume
561
+ */
562
+ updateVolume(volume) {
563
+ this.volume = volume;
564
+ if (this.nodes) {
565
+ this.nodes.gainNode.gain.setTargetAtTime(
566
+ volume,
567
+ this.ctx.currentTime,
568
+ 0.01
569
+ );
570
+ }
571
+ }
572
+ /**
573
+ * Stop brown noise generation
574
+ */
575
+ stop() {
576
+ if (this.nodes) {
577
+ try {
578
+ this.nodes.bufferSource.stop();
579
+ } catch {
580
+ }
581
+ this.nodes.bufferSource.disconnect();
582
+ this.nodes.gainNode.disconnect();
583
+ this.nodes = null;
584
+ }
585
+ }
586
+ /**
587
+ * Check if generator is active
588
+ */
589
+ isActive() {
590
+ return this.nodes !== null;
591
+ }
592
+ };
593
+
594
+ // src/PinkNoiseGenerator.ts
595
+ var PinkNoiseGenerator = class {
596
+ constructor(ctx) {
597
+ this.nodes = null;
598
+ this.volume = 0.15;
599
+ // Default subtle volume
600
+ this.buffer = null;
601
+ this.ctx = ctx;
602
+ }
603
+ /**
604
+ * Create pink noise buffer
605
+ * Generates a seamless loop of pink noise using Voss-McCartney algorithm
606
+ */
607
+ createPinkNoiseBuffer() {
608
+ const bufferSize = this.ctx.sampleRate * 5;
609
+ const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
610
+ const data = buffer.getChannelData(0);
611
+ const numRows = 16;
612
+ const rowValues = new Array(numRows).fill(0);
613
+ let index = 0;
614
+ let indexMask = 0;
615
+ let sum = 0;
616
+ for (let i = 0; i < numRows; i++) {
617
+ rowValues[i] = Math.random() * 2 - 1;
618
+ sum += rowValues[i];
619
+ }
620
+ for (let i = 0; i < bufferSize; i++) {
621
+ index = index + 1 & indexMask;
622
+ if (index === 0) {
623
+ indexMask = indexMask << 1 | 1;
624
+ const numRowsToUpdate = Math.min(
625
+ numRows,
626
+ Math.floor(Math.log2(i + 1)) + 1
627
+ );
628
+ for (let j = 0; j < numRowsToUpdate; j++) {
629
+ rowValues[j] = Math.random() * 2 - 1;
630
+ }
631
+ }
632
+ let rowToUpdate = 0;
633
+ let temp = index;
634
+ while ((temp & 1) === 0 && rowToUpdate < numRows - 1) {
635
+ temp >>= 1;
636
+ rowToUpdate++;
637
+ }
638
+ sum -= rowValues[rowToUpdate];
639
+ rowValues[rowToUpdate] = Math.random() * 2 - 1;
640
+ sum += rowValues[rowToUpdate];
641
+ data[i] = sum / numRows;
642
+ data[i] *= 0.5;
643
+ }
644
+ return buffer;
645
+ }
646
+ /**
647
+ * Start pink noise generation
648
+ *
649
+ * @param volume - Volume level (0.0 to 1.0)
650
+ * @param masterGain - Master gain node to connect to
651
+ */
652
+ start(volume, masterGain) {
653
+ this.volume = volume;
654
+ this.stop();
655
+ if (!this.buffer) {
656
+ this.buffer = this.createPinkNoiseBuffer();
657
+ }
658
+ const bufferSource = this.ctx.createBufferSource();
659
+ bufferSource.buffer = this.buffer;
660
+ bufferSource.loop = true;
661
+ const gainNode = this.ctx.createGain();
662
+ gainNode.gain.value = volume;
663
+ bufferSource.connect(gainNode).connect(masterGain);
664
+ bufferSource.start();
665
+ this.nodes = {
666
+ bufferSource,
667
+ gainNode
668
+ };
669
+ }
670
+ /**
671
+ * Update volume
672
+ */
673
+ updateVolume(volume) {
674
+ this.volume = volume;
675
+ if (this.nodes) {
676
+ this.nodes.gainNode.gain.setTargetAtTime(
677
+ volume,
678
+ this.ctx.currentTime,
679
+ 0.01
680
+ );
681
+ }
682
+ }
683
+ /**
684
+ * Stop pink noise generation
685
+ */
686
+ stop() {
687
+ if (this.nodes) {
688
+ try {
689
+ this.nodes.bufferSource.stop();
690
+ } catch {
691
+ }
692
+ this.nodes.bufferSource.disconnect();
693
+ this.nodes.gainNode.disconnect();
694
+ this.nodes = null;
695
+ }
696
+ }
697
+ /**
698
+ * Check if generator is active
699
+ */
700
+ isActive() {
701
+ return this.nodes !== null;
702
+ }
703
+ };
704
+
705
+ // src/EntrainmentEngine.ts
706
+ var EntrainmentEngine = class {
707
+ constructor() {
708
+ this.ctx = null;
709
+ this.masterGain = null;
710
+ this.binauralGenerator = null;
711
+ this.isochronicGenerator = null;
712
+ this.monauralGenerator = null;
713
+ this.brownNoiseGenerator = null;
714
+ this.pinkNoiseGenerator = null;
715
+ this.config = null;
716
+ this.state = "stopped";
717
+ this.sessionStartTime = 0;
718
+ this.currentFrequency = 0;
719
+ this.targetFrequency = 0;
720
+ this.rampingInterval = null;
721
+ }
722
+ /**
723
+ * Initialize AudioContext
724
+ * Must be called from user gesture (button click) due to browser autoplay policy
725
+ */
726
+ async initialize() {
727
+ if (this.ctx) {
728
+ return;
729
+ }
730
+ const AudioContextClass = window.AudioContext || window.webkitAudioContext;
731
+ if (!AudioContextClass) {
732
+ throw new Error("Web Audio API is not supported in this browser");
733
+ }
734
+ this.ctx = new AudioContextClass();
735
+ this.masterGain = this.ctx.createGain();
736
+ this.masterGain.gain.value = 0.8;
737
+ this.masterGain.connect(this.ctx.destination);
738
+ this.binauralGenerator = new BinauralGenerator(this.ctx);
739
+ this.isochronicGenerator = new IsochronicGenerator(this.ctx);
740
+ this.monauralGenerator = new MonauralGenerator(this.ctx);
741
+ this.brownNoiseGenerator = new BrownNoiseGenerator(this.ctx);
742
+ this.pinkNoiseGenerator = new PinkNoiseGenerator(this.ctx);
743
+ }
744
+ /**
745
+ * Resume AudioContext if suspended
746
+ * Required due to browser autoplay policy
747
+ */
748
+ async resume() {
749
+ if (!this.ctx) {
750
+ await this.initialize();
751
+ }
752
+ if (this.ctx && this.ctx.state === "suspended") {
753
+ await this.ctx.resume();
754
+ }
755
+ }
756
+ /**
757
+ * Start entrainment with configuration
758
+ */
759
+ async start(config) {
760
+ if (!this.ctx || !this.masterGain) {
761
+ await this.initialize();
762
+ }
763
+ if (!this.ctx || !this.masterGain) {
764
+ throw new Error("AudioContext not initialized");
765
+ }
766
+ await this.resume();
767
+ this.config = config;
768
+ let newTargetFrequency;
769
+ if (config.preset && !config.manualMode) {
770
+ const preset = getPreset(config.preset);
771
+ newTargetFrequency = preset.targetFrequency;
772
+ } else if (config.manualBeatFrequency) {
773
+ newTargetFrequency = config.manualBeatFrequency;
774
+ } else {
775
+ throw new Error("No preset or manual frequency specified");
776
+ }
777
+ if (config.progressiveRamping && this.currentFrequency > 0) {
778
+ const frequencyDiff = Math.abs(
779
+ newTargetFrequency - this.currentFrequency
780
+ );
781
+ const maxJump = Math.max(2, this.currentFrequency * 0.2);
782
+ if (frequencyDiff > maxJump) {
783
+ this.targetFrequency = newTargetFrequency;
784
+ this.startProgressiveRamp(config);
785
+ return;
786
+ }
787
+ }
788
+ this.targetFrequency = newTargetFrequency;
789
+ this.currentFrequency = newTargetFrequency;
790
+ if (config.mode === "headphones" && config.generators.binaural.enabled) {
791
+ const binauralConfig = config.generators.binaural;
792
+ const carrierFreq = config.manualMode && config.manualCarrierFrequency ? config.manualCarrierFrequency : binauralConfig.carrierFrequency;
793
+ if (!validateOsterCurve(carrierFreq)) {
794
+ console.warn(
795
+ `Binaural beats work best with carrier frequency 400-500 Hz (Oster Curve). Got ${carrierFreq} Hz.`
796
+ );
797
+ }
798
+ this.binauralGenerator.start(
799
+ {
800
+ ...binauralConfig,
801
+ carrierFrequency: carrierFreq,
802
+ beatFrequency: this.currentFrequency
803
+ },
804
+ this.masterGain
805
+ );
806
+ } else if (config.mode === "speaker") {
807
+ if (config.generators.isochronic.enabled) {
808
+ const isochronicConfig = config.generators.isochronic;
809
+ const carrierFreq = config.manualMode && config.manualCarrierFrequency ? config.manualCarrierFrequency : isochronicConfig.carrierFrequency || 200;
810
+ this.isochronicGenerator.start(
811
+ {
812
+ ...isochronicConfig,
813
+ carrierFrequency: carrierFreq,
814
+ beatFrequency: this.currentFrequency
815
+ },
816
+ this.masterGain
817
+ );
818
+ } else if (config.generators.monaural?.enabled) {
819
+ const monauralConfig = config.generators.monaural;
820
+ const frequency1 = config.manualMode && config.manualCarrierFrequency ? config.manualCarrierFrequency : monauralConfig.frequency1 || 200;
821
+ this.monauralGenerator.start(
822
+ {
823
+ ...monauralConfig,
824
+ frequency1,
825
+ frequency2: frequency1 + this.currentFrequency,
826
+ beatFrequency: this.currentFrequency
827
+ },
828
+ this.masterGain
829
+ );
830
+ }
831
+ }
832
+ if (config.generators.brownNoise.enabled) {
833
+ this.brownNoiseGenerator.start(
834
+ config.generators.brownNoise.volume,
835
+ this.masterGain
836
+ );
837
+ }
838
+ if (config.generators.pinkNoise?.enabled) {
839
+ this.pinkNoiseGenerator.start(
840
+ config.generators.pinkNoise.volume,
841
+ this.masterGain
842
+ );
843
+ }
844
+ this.masterGain.gain.value = config.masterVolume * 0.8;
845
+ this.state = "playing";
846
+ this.sessionStartTime = Date.now();
847
+ }
848
+ /**
849
+ * Progressive ramping system for gradual frequency transitions
850
+ * Prevents jumping too far from current state (entrainment best practice)
851
+ */
852
+ startProgressiveRamp(config) {
853
+ if (this.rampingInterval !== null) {
854
+ clearInterval(this.rampingInterval);
855
+ }
856
+ const startFreq = this.currentFrequency;
857
+ const endFreq = this.targetFrequency;
858
+ const duration = config.rampingDuration || 30;
859
+ const steps = 60;
860
+ const stepDuration = duration * 1e3 / steps;
861
+ let currentStep = 0;
862
+ this.startGeneratorsWithFrequency(config, startFreq);
863
+ this.rampingInterval = setInterval(() => {
864
+ currentStep++;
865
+ const progress = currentStep / steps;
866
+ this.currentFrequency = startFreq + (endFreq - startFreq) * progress;
867
+ this.updateFrequencyInGenerators(this.currentFrequency);
868
+ if (currentStep >= steps) {
869
+ this.currentFrequency = endFreq;
870
+ this.updateFrequencyInGenerators(endFreq);
871
+ if (this.rampingInterval !== null) {
872
+ clearInterval(this.rampingInterval);
873
+ this.rampingInterval = null;
874
+ }
875
+ }
876
+ }, stepDuration);
877
+ }
878
+ /**
879
+ * Start generators with specific frequency
880
+ */
881
+ startGeneratorsWithFrequency(config, frequency) {
882
+ if (config.mode === "headphones" && config.generators.binaural.enabled) {
883
+ const binauralConfig = config.generators.binaural;
884
+ const carrierFreq = config.manualMode && config.manualCarrierFrequency ? config.manualCarrierFrequency : binauralConfig.carrierFrequency;
885
+ this.binauralGenerator.start(
886
+ {
887
+ ...binauralConfig,
888
+ carrierFrequency: carrierFreq,
889
+ beatFrequency: frequency
890
+ },
891
+ this.masterGain
892
+ );
893
+ } else if (config.mode === "speaker") {
894
+ if (config.generators.isochronic.enabled) {
895
+ const isochronicConfig = config.generators.isochronic;
896
+ const carrierFreq = config.manualMode && config.manualCarrierFrequency ? config.manualCarrierFrequency : isochronicConfig.carrierFrequency || 200;
897
+ this.isochronicGenerator.start(
898
+ {
899
+ ...isochronicConfig,
900
+ carrierFrequency: carrierFreq,
901
+ beatFrequency: frequency
902
+ },
903
+ this.masterGain
904
+ );
905
+ } else if (config.generators.monaural?.enabled) {
906
+ const monauralConfig = config.generators.monaural;
907
+ const frequency1 = config.manualMode && config.manualCarrierFrequency ? config.manualCarrierFrequency : monauralConfig.frequency1 || 200;
908
+ this.monauralGenerator.start(
909
+ {
910
+ ...monauralConfig,
911
+ frequency1,
912
+ frequency2: frequency1 + frequency,
913
+ beatFrequency: frequency
914
+ },
915
+ this.masterGain
916
+ );
917
+ }
918
+ }
919
+ if (config.generators.brownNoise.enabled) {
920
+ this.brownNoiseGenerator.start(
921
+ config.generators.brownNoise.volume,
922
+ this.masterGain
923
+ );
924
+ }
925
+ if (config.generators.pinkNoise?.enabled) {
926
+ this.pinkNoiseGenerator.start(
927
+ config.generators.pinkNoise.volume,
928
+ this.masterGain
929
+ );
930
+ }
931
+ this.masterGain.gain.value = config.masterVolume * 0.8;
932
+ }
933
+ /**
934
+ * Update frequency in active generators
935
+ */
936
+ updateFrequencyInGenerators(frequency) {
937
+ if (!this.config) {
938
+ return;
939
+ }
940
+ if (this.binauralGenerator?.isActive()) {
941
+ this.binauralGenerator.updateConfig({
942
+ beatFrequency: frequency
943
+ });
944
+ }
945
+ if (this.isochronicGenerator?.isActive()) {
946
+ this.isochronicGenerator.updateConfig({
947
+ beatFrequency: frequency
948
+ });
949
+ }
950
+ if (this.monauralGenerator?.isActive()) {
951
+ this.monauralGenerator.updateConfig({
952
+ beatFrequency: frequency
953
+ });
954
+ }
955
+ }
956
+ /**
957
+ * Stop entrainment
958
+ */
959
+ stop() {
960
+ if (this.rampingInterval !== null) {
961
+ clearInterval(this.rampingInterval);
962
+ this.rampingInterval = null;
963
+ }
964
+ this.binauralGenerator?.stop();
965
+ this.isochronicGenerator?.stop();
966
+ this.monauralGenerator?.stop();
967
+ this.brownNoiseGenerator?.stop();
968
+ this.pinkNoiseGenerator?.stop();
969
+ this.state = "stopped";
970
+ this.sessionStartTime = 0;
971
+ this.currentFrequency = 0;
972
+ this.targetFrequency = 0;
973
+ }
974
+ /**
975
+ * Pause entrainment
976
+ */
977
+ pause() {
978
+ if (this.state === "playing") {
979
+ if (this.masterGain) {
980
+ this.masterGain.gain.setTargetAtTime(0, this.ctx.currentTime, 0.1);
981
+ }
982
+ this.state = "paused";
983
+ }
984
+ }
985
+ /**
986
+ * Resume entrainment
987
+ */
988
+ async resumePlayback() {
989
+ if (this.state === "paused" && this.config) {
990
+ await this.resume();
991
+ if (this.masterGain) {
992
+ this.masterGain.gain.setTargetAtTime(
993
+ this.config.masterVolume * 0.8,
994
+ this.ctx.currentTime,
995
+ 0.1
996
+ );
997
+ }
998
+ this.state = "playing";
999
+ }
1000
+ }
1001
+ /**
1002
+ * Update configuration (for real-time parameter changes)
1003
+ */
1004
+ updateConfig(config) {
1005
+ if (!this.config) {
1006
+ return;
1007
+ }
1008
+ const newConfig = { ...this.config, ...config };
1009
+ if (config.masterVolume !== void 0 && this.masterGain) {
1010
+ this.masterGain.gain.setTargetAtTime(
1011
+ config.masterVolume * 0.8,
1012
+ this.ctx.currentTime,
1013
+ 0.01
1014
+ );
1015
+ }
1016
+ if (config.generators) {
1017
+ if (config.generators.binaural && this.binauralGenerator?.isActive()) {
1018
+ this.binauralGenerator.updateConfig(config.generators.binaural);
1019
+ }
1020
+ if (config.generators.isochronic && this.isochronicGenerator?.isActive()) {
1021
+ this.isochronicGenerator.updateConfig(config.generators.isochronic);
1022
+ }
1023
+ if (config.generators.monaural) {
1024
+ if (config.generators.monaural.enabled && !this.monauralGenerator?.isActive()) {
1025
+ const monauralConfig = config.generators.monaural;
1026
+ const frequency1 = this.config.manualMode && this.config.manualCarrierFrequency ? this.config.manualCarrierFrequency : monauralConfig.frequency1 || 200;
1027
+ this.monauralGenerator.start(
1028
+ {
1029
+ ...monauralConfig,
1030
+ frequency1,
1031
+ frequency2: frequency1 + this.currentFrequency,
1032
+ beatFrequency: this.currentFrequency
1033
+ },
1034
+ this.masterGain
1035
+ );
1036
+ } else if (!config.generators.monaural.enabled && this.monauralGenerator?.isActive()) {
1037
+ this.monauralGenerator.stop();
1038
+ } else if (this.monauralGenerator?.isActive()) {
1039
+ this.monauralGenerator.updateConfig(config.generators.monaural);
1040
+ }
1041
+ }
1042
+ if (config.generators.brownNoise) {
1043
+ if (config.generators.brownNoise.enabled && !this.brownNoiseGenerator?.isActive()) {
1044
+ this.brownNoiseGenerator.start(
1045
+ config.generators.brownNoise.volume,
1046
+ this.masterGain
1047
+ );
1048
+ } else if (!config.generators.brownNoise.enabled && this.brownNoiseGenerator?.isActive()) {
1049
+ this.brownNoiseGenerator.stop();
1050
+ } else if (config.generators.brownNoise.volume !== void 0) {
1051
+ this.brownNoiseGenerator.updateVolume(
1052
+ config.generators.brownNoise.volume
1053
+ );
1054
+ }
1055
+ }
1056
+ if (config.generators.pinkNoise) {
1057
+ if (config.generators.pinkNoise.enabled && !this.pinkNoiseGenerator?.isActive()) {
1058
+ this.pinkNoiseGenerator.start(
1059
+ config.generators.pinkNoise.volume,
1060
+ this.masterGain
1061
+ );
1062
+ } else if (!config.generators.pinkNoise.enabled && this.pinkNoiseGenerator?.isActive()) {
1063
+ this.pinkNoiseGenerator.stop();
1064
+ } else if (config.generators.pinkNoise.volume !== void 0) {
1065
+ this.pinkNoiseGenerator.updateVolume(
1066
+ config.generators.pinkNoise.volume
1067
+ );
1068
+ }
1069
+ }
1070
+ }
1071
+ this.config = newConfig;
1072
+ }
1073
+ /**
1074
+ * Get current session information
1075
+ */
1076
+ getSessionInfo() {
1077
+ const duration = this.sessionStartTime > 0 ? Math.floor((Date.now() - this.sessionStartTime) / 1e3) : 0;
1078
+ return {
1079
+ startTime: this.sessionStartTime,
1080
+ duration,
1081
+ currentFrequency: this.currentFrequency,
1082
+ targetFrequency: this.targetFrequency,
1083
+ state: this.state
1084
+ };
1085
+ }
1086
+ /**
1087
+ * Get AudioContext (for visualization/analysis)
1088
+ */
1089
+ getContext() {
1090
+ return this.ctx;
1091
+ }
1092
+ /**
1093
+ * Get master gain node (for visualization/analysis)
1094
+ */
1095
+ getMasterGain() {
1096
+ return this.masterGain;
1097
+ }
1098
+ /**
1099
+ * Cleanup - disconnect all nodes and close AudioContext
1100
+ */
1101
+ cleanup() {
1102
+ this.stop();
1103
+ if (this.masterGain) {
1104
+ this.masterGain.disconnect();
1105
+ this.masterGain = null;
1106
+ }
1107
+ if (this.ctx && this.ctx.state !== "closed") {
1108
+ this.ctx.close();
1109
+ this.ctx = null;
1110
+ }
1111
+ this.binauralGenerator = null;
1112
+ this.isochronicGenerator = null;
1113
+ this.monauralGenerator = null;
1114
+ this.brownNoiseGenerator = null;
1115
+ this.pinkNoiseGenerator = null;
1116
+ this.config = null;
1117
+ }
1118
+ };
1119
+ export {
1120
+ ALPHA_PRESET,
1121
+ BRAINWAVE_PRESETS,
1122
+ BinauralGenerator,
1123
+ BrownNoiseGenerator,
1124
+ DELTA_PRESET,
1125
+ DRIVING_WARNING,
1126
+ EPILEPSY_WARNING,
1127
+ EntrainmentEngine,
1128
+ GAMMA_PRESET,
1129
+ HIGH_BETA_PRESET,
1130
+ IsochronicGenerator,
1131
+ LOW_BETA_PRESET,
1132
+ MENTAL_HEALTH_WARNING,
1133
+ MID_BETA_PRESET,
1134
+ MonauralGenerator,
1135
+ PACEMAKER_WARNING,
1136
+ PinkNoiseGenerator,
1137
+ STROBING_WARNING,
1138
+ THETA_PRESET,
1139
+ getAllSafetyWarnings,
1140
+ getPreset,
1141
+ getPresetByFrequency,
1142
+ getRecommendedCarrierFrequency,
1143
+ getSafetyWarnings,
1144
+ isPhotosensitiveTriggerRange,
1145
+ validateOsterCurve
1146
+ };