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