@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/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/README.md +176 -0
- package/dist/index.d.mts +589 -0
- package/dist/index.d.ts +589 -0
- package/dist/index.js +1198 -0
- package/dist/index.mjs +1146 -0
- package/package.json +60 -0
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
|
+
});
|