Jorgy72 commited on
Commit
ca709b1
·
verified ·
1 Parent(s): 98225bc

Add 2 files

Browse files
Files changed (2) hide show
  1. README.md +7 -5
  2. index.html +1318 -19
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: Brain Tuner
3
- emoji: 🌍
4
- colorFrom: purple
5
- colorTo: gray
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: brain-tuner
3
+ emoji: 🐳
4
+ colorFrom: blue
5
+ colorTo: blue
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite
10
  ---
11
 
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
index.html CHANGED
@@ -1,19 +1,1318 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Spatial Binaural Beats Generator</title>
7
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
8
+ <style>
9
+ :root {
10
+ --primary-color: #4a6fa5;
11
+ --secondary-color: #166088;
12
+ --accent-color: #4fc3f7;
13
+ --dark-color: #0a2463;
14
+ --light-color: #e8f1f2;
15
+ --success-color: #4caf50;
16
+ --warning-color: #ff9800;
17
+ --danger-color: #f44336;
18
+ --pattern-color: #9c27b0;
19
+ }
20
+
21
+ * {
22
+ margin: 0;
23
+ padding: 0;
24
+ box-sizing: border-box;
25
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
26
+ }
27
+
28
+ body {
29
+ background: linear-gradient(135deg, #0a2463, #166088);
30
+ color: var(--light-color);
31
+ min-height: 100vh;
32
+ display: flex;
33
+ flex-direction: column;
34
+ padding: 20px;
35
+ }
36
+
37
+ header {
38
+ text-align: center;
39
+ margin-bottom: 30px;
40
+ padding: 20px;
41
+ background: rgba(10, 36, 99, 0.5);
42
+ border-radius: 15px;
43
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
44
+ backdrop-filter: blur(8px);
45
+ border: 1px solid rgba(255, 255, 255, 0.1);
46
+ }
47
+
48
+ h1 {
49
+ font-size: 2.5rem;
50
+ margin-bottom: 10px;
51
+ background: linear-gradient(90deg, var(--accent-color), #ffffff);
52
+ -webkit-background-clip: text;
53
+ background-clip: text;
54
+ color: transparent;
55
+ }
56
+
57
+ .subtitle {
58
+ font-size: 1.1rem;
59
+ opacity: 0.8;
60
+ }
61
+
62
+ .app-container {
63
+ display: grid;
64
+ grid-template-columns: 1fr 1.5fr;
65
+ gap: 20px;
66
+ flex: 1;
67
+ }
68
+
69
+ @media (max-width: 992px) {
70
+ .app-container {
71
+ grid-template-columns: 1fr;
72
+ }
73
+ }
74
+
75
+ .control-panel {
76
+ background: rgba(22, 96, 136, 0.5);
77
+ border-radius: 15px;
78
+ padding: 25px;
79
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
80
+ backdrop-filter: blur(8px);
81
+ border: 1px solid rgba(255, 255, 255, 0.1);
82
+ }
83
+
84
+ .visualization {
85
+ background: rgba(22, 96, 136, 0.5);
86
+ border-radius: 15px;
87
+ padding: 25px;
88
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
89
+ backdrop-filter: blur(8px);
90
+ border: 1px solid rgba(255, 255, 255, 0.1);
91
+ display: flex;
92
+ flex-direction: column;
93
+ }
94
+
95
+ .control-group {
96
+ margin-bottom: 25px;
97
+ }
98
+
99
+ .control-group h3 {
100
+ margin-bottom: 15px;
101
+ display: flex;
102
+ align-items: center;
103
+ gap: 10px;
104
+ }
105
+
106
+ .slider-container {
107
+ margin-bottom: 15px;
108
+ }
109
+
110
+ .slider-container label {
111
+ display: flex;
112
+ justify-content: space-between;
113
+ margin-bottom: 8px;
114
+ font-size: 0.9rem;
115
+ }
116
+
117
+ .slider-container input[type="range"] {
118
+ width: 100%;
119
+ -webkit-appearance: none;
120
+ height: 10px;
121
+ border-radius: 5px;
122
+ background: var(--light-color);
123
+ outline: none;
124
+ }
125
+
126
+ .slider-container input[type="range"]::-webkit-slider-thumb {
127
+ -webkit-appearance: none;
128
+ appearance: none;
129
+ width: 20px;
130
+ height: 20px;
131
+ border-radius: 50%;
132
+ background: var(--accent-color);
133
+ cursor: pointer;
134
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
135
+ }
136
+
137
+ .value-display {
138
+ display: inline-block;
139
+ min-width: 40px;
140
+ text-align: right;
141
+ }
142
+
143
+ .btn {
144
+ padding: 12px 20px;
145
+ border: none;
146
+ border-radius: 8px;
147
+ background: var(--primary-color);
148
+ color: white;
149
+ font-weight: bold;
150
+ cursor: pointer;
151
+ transition: all 0.3s ease;
152
+ display: inline-flex;
153
+ align-items: center;
154
+ justify-content: center;
155
+ gap: 8px;
156
+ }
157
+
158
+ .btn:hover {
159
+ transform: translateY(-2px);
160
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
161
+ }
162
+
163
+ .btn:active {
164
+ transform: translateY(0);
165
+ }
166
+
167
+ .btn-primary {
168
+ background: var(--primary-color);
169
+ }
170
+
171
+ .btn-danger {
172
+ background: var(--danger-color);
173
+ }
174
+
175
+ .btn-success {
176
+ background: var(--success-color);
177
+ }
178
+
179
+ .btn-pattern {
180
+ background: var(--pattern-color);
181
+ }
182
+
183
+ .btn-controls {
184
+ display: flex;
185
+ gap: 15px;
186
+ }
187
+
188
+ canvas {
189
+ background: rgba(10, 36, 99, 0.3);
190
+ border-radius: 10px;
191
+ width: 100%;
192
+ flex: 1;
193
+ }
194
+
195
+ .position-control {
196
+ position: relative;
197
+ width: 300px;
198
+ height: 300px;
199
+ margin: 30px auto;
200
+ background: rgba(10, 36, 99, 0.3);
201
+ border-radius: 50%;
202
+ border: 2px solid var(--accent-color);
203
+ overflow: hidden;
204
+ }
205
+
206
+ .position-indicator {
207
+ position: absolute;
208
+ width: 20px;
209
+ height: 20px;
210
+ background: var(--accent-color);
211
+ border-radius: 50%;
212
+ transform: translate(-50%, -50%);
213
+ cursor: grab;
214
+ box-shadow: 0 0 10px var(--accent-color);
215
+ transition: left 0.1s linear, top 0.1s linear;
216
+ z-index: 10;
217
+ }
218
+
219
+ .position-indicator:active {
220
+ cursor: grabbing;
221
+ }
222
+
223
+ .position-trail {
224
+ position: absolute;
225
+ width: 4px;
226
+ height: 4px;
227
+ background: rgba(79, 195, 247, 0.5);
228
+ border-radius: 50%;
229
+ transform: translate(-50%, -50%);
230
+ pointer-events: none;
231
+ z-index: 5;
232
+ }
233
+
234
+ .presets {
235
+ display: grid;
236
+ grid-template-columns: repeat(2, 1fr);
237
+ gap: 10px;
238
+ margin-top: 20px;
239
+ }
240
+
241
+ .pattern-controls {
242
+ display: grid;
243
+ grid-template-columns: repeat(2, 1fr);
244
+ gap: 10px;
245
+ margin-top: 15px;
246
+ }
247
+
248
+ .preset-btn, .pattern-btn {
249
+ padding: 8px;
250
+ border-radius: 6px;
251
+ background: rgba(74, 111, 165, 0.5);
252
+ border: 1px solid var(--accent-color);
253
+ color: var(--light-color);
254
+ cursor: pointer;
255
+ transition: all 0.2s ease;
256
+ }
257
+
258
+ .pattern-btn {
259
+ border-color: var(--pattern-color);
260
+ }
261
+
262
+ .preset-btn:hover {
263
+ background: var(--primary-color);
264
+ }
265
+
266
+ .pattern-btn:hover {
267
+ background: var(--pattern-color);
268
+ }
269
+
270
+ .audio-visualizer {
271
+ flex: 1;
272
+ display: flex;
273
+ align-items: center;
274
+ justify-content: center;
275
+ }
276
+
277
+ .footer {
278
+ text-align: center;
279
+ margin-top: 30px;
280
+ padding: 20px;
281
+ font-size: 0.9rem;
282
+ opacity: 0.7;
283
+ }
284
+
285
+ .tooltip {
286
+ position: relative;
287
+ display: inline-block;
288
+ margin-left: 5px;
289
+ cursor: pointer;
290
+ }
291
+
292
+ .tooltip .tooltip-text {
293
+ visibility: hidden;
294
+ width: 200px;
295
+ background-color: var(--dark-color);
296
+ color: var(--light-color);
297
+ text-align: center;
298
+ border-radius: 6px;
299
+ padding: 8px;
300
+ position: absolute;
301
+ z-index: 1;
302
+ bottom: 125%;
303
+ left: 50%;
304
+ transform: translateX(-50%);
305
+ opacity: 0;
306
+ transition: opacity 0.3s;
307
+ font-size: 0.8rem;
308
+ line-height: 1.4;
309
+ }
310
+
311
+ .tooltip:hover .tooltip-text {
312
+ visibility: visible;
313
+ opacity: 1;
314
+ }
315
+
316
+ /* Animation for the binaural effect */
317
+ @keyframes pulse {
318
+ 0%, 100% {
319
+ box-shadow: 0 0 5px var(--accent-color);
320
+ }
321
+ 50% {
322
+ box-shadow: 0 0 20px var(--accent-color);
323
+ }
324
+ }
325
+
326
+ .binaural-active {
327
+ animation: pulse 2s infinite;
328
+ }
329
+
330
+ /* Warning message */
331
+ .warning {
332
+ background-color: var(--warning-color);
333
+ color: white;
334
+ padding: 10px;
335
+ border-radius: 5px;
336
+ margin-bottom: 20px;
337
+ display: flex;
338
+ align-items: center;
339
+ gap: 10px;
340
+ animation: fadeIn 0.5s ease-in;
341
+ }
342
+
343
+ @keyframes fadeIn {
344
+ from { opacity: 0; }
345
+ to { opacity: 1; }
346
+ }
347
+
348
+ .pattern-active {
349
+ position: relative;
350
+ overflow: hidden;
351
+ }
352
+
353
+ .pattern-active::after {
354
+ content: '';
355
+ position: absolute;
356
+ top: 0;
357
+ left: 0;
358
+ width: 100%;
359
+ height: 100%;
360
+ background: rgba(156, 39, 176, 0.2);
361
+ pointer-events: none;
362
+ }
363
+
364
+ .pattern-speed-control {
365
+ display: flex;
366
+ align-items: center;
367
+ gap: 10px;
368
+ margin-top: 10px;
369
+ }
370
+
371
+ .pattern-speed-control input[type="range"] {
372
+ flex: 1;
373
+ }
374
+ </style>
375
+ </head>
376
+ <body>
377
+ <header>
378
+ <h1>Spatial Binaural Beats Generator</h1>
379
+ <p class="subtitle">Experience binaural beats with 3D spatial audio positioning and geometric patterns</p>
380
+ </header>
381
+
382
+ <div id="audioWarning" class="warning" style="display: none;">
383
+ <i class="fas fa-exclamation-triangle"></i>
384
+ <span>Press the Start button and allow audio when prompted. Use headphones for best experience.</span>
385
+ </div>
386
+
387
+ <div class="app-container">
388
+ <div class="control-panel">
389
+ <div class="control-group">
390
+ <h3><i class="fas fa-sliders-h"></i> Base Frequency</h3>
391
+ <div class="slider-container">
392
+ <label for="baseFrequency">
393
+ <span>Frequency: <span id="baseFrequencyValue" class="value-display">200</span> Hz</span>
394
+ <span class="tooltip">
395
+ <i class="fas fa-info-circle"></i>
396
+ <span class="tooltip-text">The carrier frequency that will be played in both ears. The difference between ears creates the binaural beat effect.</span>
397
+ </span>
398
+ </label>
399
+ <input type="range" id="baseFrequency" min="100" max="800" value="200" step="1">
400
+ </div>
401
+ </div>
402
+
403
+ <div class="control-group">
404
+ <h3><i class="fas fa-brain"></i> Binaural Beat</h3>
405
+ <div class="slider-container">
406
+ <label for="beatFrequency">
407
+ <span>Beat Frequency: <span id="beatFrequencyValue" class="value-display">10</span> Hz</span>
408
+ <span class="tooltip">
409
+ <i class="fas fa-info-circle"></i>
410
+ <span class="tooltip-text">The frequency difference between left and right ears. This creates the binaural beat effect (0.1-30Hz is most effective).</span>
411
+ </span>
412
+ </label>
413
+ <input type="range" id="beatFrequency" min="0.1" max="30" value="10" step="0.1">
414
+ </div>
415
+ <div class="slider-container">
416
+ <label for="beatVolume">
417
+ <span>Beat Volume: <span id="beatVolumeValue" class="value-display">0.7</span></span>
418
+ </label>
419
+ <input type="range" id="beatVolume" min="0" max="1" value="0.7" step="0.01">
420
+ </div>
421
+ </div>
422
+
423
+ <div class="control-group">
424
+ <h3><i class="fas fa-volume-up"></i> Spatial Audio</h3>
425
+ <div class="position-control" id="positionControl">
426
+ <div class="position-indicator" id="positionIndicator"></div>
427
+ </div>
428
+ <div class="slider-container">
429
+ <label for="distance">
430
+ <span>Distance: <span id="distanceValue" class="value-display">1</span> m</span>
431
+ <span class="tooltip">
432
+ <i class="fas fa-info-circle"></i>
433
+ <span class="tooltip-text">Distance from the sound source. Closer sounds will be louder and have more high frequencies.</span>
434
+ </span>
435
+ </label>
436
+ <input type="range" id="distance" min="0.1" max="10" value="1" step="0.1">
437
+ </div>
438
+ <div class="slider-container">
439
+ <label for="spatialVolume">
440
+ <span>Spatial Volume: <span id="spatialVolumeValue" class="value-display">0.7</span></span>
441
+ </label>
442
+ <input type="range" id="spatialVolume" min="0" max="1" value="0.7" step="0.01">
443
+ </div>
444
+ </div>
445
+
446
+ <div class="control-group">
447
+ <h3><i class="fas fa-project-diagram"></i> Geometric Patterns</h3>
448
+ <div class="pattern-controls">
449
+ <button class="pattern-btn" id="patternCircle"><i class="fas fa-circle"></i> Circle</button>
450
+ <button class="pattern-btn" id="patternSpiral"><i class="fas fa-spinner"></i> Spiral</button>
451
+ <button class="pattern-btn" id="patternPyramid"><i class="fas fa-mountain"></i> Pyramid</button>
452
+ <button class="pattern-btn" id="patternFigure8"><i class="fas fa-infinity"></i> Figure 8</button>
453
+ <button class="pattern-btn" id="patternRandom"><i class="fas fa-random"></i> Random</button>
454
+ <button class="pattern-btn" id="patternOff"><i class="fas fa-power-off"></i> Off</button>
455
+ </div>
456
+ <div class="pattern-speed-control">
457
+ <label for="patternSpeed">Speed:</label>
458
+ <input type="range" id="patternSpeed" min="0.1" max="3" value="1" step="0.1">
459
+ <span id="patternSpeedValue" class="value-display">1.0x</span>
460
+ </div>
461
+ </div>
462
+
463
+ <div class="control-group">
464
+ <h3><i class="fas fa-prescription-bottle"></i> Presets</h3>
465
+ <div class="presets">
466
+ <button class="preset-btn" data-base="200" data-beat="10">Focus (10Hz)</button>
467
+ <button class="preset-btn" data-base="220" data-beat="15">Creativity (15Hz)</button>
468
+ <button class="preset-btn" data-base="180" data-beat="6">Relaxation (6Hz)</button>
469
+ <button class="preset-btn" data-base="150" data-beat="4">Meditation (4Hz)</button>
470
+ <button class="preset-btn" data-base="180" data-beat="2">Deep Sleep (2Hz)</button>
471
+ <button class="preset-btn" data-base="300" data-beat="40">Awakening (40Hz)</button>
472
+ </div>
473
+ </div>
474
+
475
+ <div class="btn-controls">
476
+ <button id="toggleBtn" class="btn btn-success">
477
+ <i class="fas fa-play"></i> Start
478
+ </button>
479
+ <button id="stopBtn" class="btn btn-danger">
480
+ <i class="fas fa-stop"></i> Stop
481
+ </button>
482
+ <button id="patternPlayBtn" class="btn btn-pattern" style="display: none;">
483
+ <i class="fas fa-play"></i> Play Pattern
484
+ </button>
485
+ </div>
486
+ </div>
487
+
488
+ <div class="visualization">
489
+ <div style="display: flex; justify-content: space-between; align-items: center;">
490
+ <h3><i class="fas fa-chart-line"></i> Audio Analysis</h3>
491
+ <div style="display: flex; gap: 10px;">
492
+ <button id="clearTrailsBtn" class="btn" style="padding: 5px 10px; font-size: 0.8rem;">
493
+ <i class="fas fa-eraser"></i> Clear Trails
494
+ </button>
495
+ </div>
496
+ </div>
497
+ <div class="audio-visualizer">
498
+ <canvas id="waveformCanvas"></canvas>
499
+ </div>
500
+ <div class="audio-visualizer">
501
+ <canvas id="frequencyCanvas"></canvas>
502
+ </div>
503
+ </div>
504
+ </div>
505
+
506
+ <div class="footer">
507
+ <p>Spatial Binaural Beats Generator | Use headphones for best experience</p>
508
+ </div>
509
+
510
+ <script>
511
+ // Audio context setup
512
+ let audioContext;
513
+ let leftOscillator, rightOscillator;
514
+ let leftGain, rightGain;
515
+ let pannerLeft, pannerRight;
516
+ let analyserLeft, analyserRight;
517
+ let dataArrayLeft, dataArrayRight;
518
+ let isPlaying = false;
519
+ let animationId;
520
+ let analyserInitialized = false;
521
+ let audioStarted = false;
522
+ let patternInterval;
523
+ let activePattern = null;
524
+ let trailPoints = [];
525
+ let trailCanvas, trailCtx;
526
+
527
+ // Pattern parameters
528
+ const patterns = {
529
+ none: { name: "None", func: null },
530
+ circle: {
531
+ name: "Circle",
532
+ func: (t, speed) => {
533
+ const radius = 0.9;
534
+ const x = radius * Math.cos(t * speed);
535
+ const y = radius * Math.sin(t * speed);
536
+ return { x, y, z: -1 };
537
+ }
538
+ },
539
+ spiral: {
540
+ name: "Spiral",
541
+ func: (t, speed) => {
542
+ const radius = 0.8 * (0.5 + 0.5 * Math.sin(t * speed * 0.3));
543
+ const x = radius * Math.cos(t * speed);
544
+ const y = radius * Math.sin(t * speed);
545
+ return { x, y, z: -1 };
546
+ }
547
+ },
548
+ pyramid: {
549
+ name: "Pyramid",
550
+ func: (t, speed) => {
551
+ const sides = 4;
552
+ const angle = ((Math.floor(t * speed / (Math.PI/2)) % sides) + (t * speed % (Math.PI/2))/(Math.PI/2)) * (Math.PI*2/sides);
553
+ const x = 0.9 * Math.cos(angle);
554
+ const y = 0.9 * Math.sin(angle);
555
+ return { x, y, z: -1 + Math.sin(t * speed * 0.5) * 0.5 };
556
+ }
557
+ },
558
+ figure8: {
559
+ name: "Figure 8",
560
+ func: (t, speed) => {
561
+ const x = 0.8 * Math.sin(t * speed * 0.5);
562
+ const y = 0.8 * Math.sin(t * speed);
563
+ return { x, y, z: -1 };
564
+ }
565
+ },
566
+ random: {
567
+ name: "Random",
568
+ func: (t, speed) => {
569
+ if (t % 1 < 0.02) { // Change direction every ~second
570
+ return {
571
+ x: (Math.random() - 0.5) * 1.6,
572
+ y: (Math.random() - 0.5) * 1.6,
573
+ z: -1 + Math.random() * 0.5
574
+ };
575
+ }
576
+ // Continue moving in the same direction
577
+ const lastPoint = trailPoints[trailPoints.length - 1] || { x: 0, y: 0, z: -1 };
578
+ return {
579
+ x: lastPoint.x,
580
+ y: lastPoint.y,
581
+ z: lastPoint.z
582
+ };
583
+ }
584
+ }
585
+ };
586
+
587
+ // DOM elements
588
+ const baseFrequencySlider = document.getElementById('baseFrequency');
589
+ const baseFrequencyValue = document.getElementById('baseFrequencyValue');
590
+ const beatFrequencySlider = document.getElementById('beatFrequency');
591
+ const beatFrequencyValue = document.getElementById('beatFrequencyValue');
592
+ const beatVolumeSlider = document.getElementById('beatVolume');
593
+ const beatVolumeValue = document.getElementById('beatVolumeValue');
594
+ const distanceSlider = document.getElementById('distance');
595
+ const distanceValue = document.getElementById('distanceValue');
596
+ const spatialVolumeSlider = document.getElementById('spatialVolume');
597
+ const spatialVolumeValue = document.getElementById('spatialVolumeValue');
598
+ const toggleBtn = document.getElementById('toggleBtn');
599
+ const stopBtn = document.getElementById('stopBtn');
600
+ const positionControl = document.getElementById('positionControl');
601
+ const positionIndicator = document.getElementById('positionIndicator');
602
+ const presetButtons = document.querySelectorAll('.preset-btn');
603
+ const waveformCanvas = document.getElementById('waveformCanvas');
604
+ const frequencyCanvas = document.getElementById('frequencyCanvas');
605
+ const waveformCtx = waveformCanvas.getContext('2d');
606
+ const frequencyCtx = frequencyCanvas.getContext('2d');
607
+ const audioWarning = document.getElementById('audioWarning');
608
+ const patternSpeedSlider = document.getElementById('patternSpeed');
609
+ const patternSpeedValue = document.getElementById('patternSpeedValue');
610
+ const clearTrailsBtn = document.getElementById('clearTrailsBtn');
611
+ const patternPlayBtn = document.getElementById('patternPlayBtn');
612
+
613
+ // Pattern buttons
614
+ const patternButtons = {
615
+ circle: document.getElementById('patternCircle'),
616
+ spiral: document.getElementById('patternSpiral'),
617
+ pyramid: document.getElementById('patternPyramid'),
618
+ figure8: document.getElementById('patternFigure8'),
619
+ random: document.getElementById('patternRandom'),
620
+ off: document.getElementById('patternOff')
621
+ };
622
+
623
+ // Create trail canvas overlay
624
+ function createTrailCanvas() {
625
+ trailCanvas = document.createElement('canvas');
626
+ trailCanvas.style.position = 'absolute';
627
+ trailCanvas.style.top = '0';
628
+ trailCanvas.style.left = '0';
629
+ trailCanvas.style.pointerEvents = 'none';
630
+ trailCanvas.width = positionControl.offsetWidth;
631
+ trailCanvas.height = positionControl.offsetHeight;
632
+ positionControl.appendChild(trailCanvas);
633
+ trailCtx = trailCanvas.getContext('2d');
634
+ }
635
+
636
+ // Clear trail points
637
+ function clearTrails() {
638
+ trailPoints = [];
639
+ if (trailCanvas && trailCtx) {
640
+ trailCtx.clearRect(0, 0, trailCanvas.width, trailCanvas.height);
641
+ }
642
+ }
643
+
644
+ // Show warning message
645
+ function showAudioWarning() {
646
+ audioWarning.style.display = 'flex';
647
+ setTimeout(() => {
648
+ audioWarning.style.opacity = '1';
649
+ }, 10);
650
+ }
651
+
652
+ // Initialize audio context on first user interaction
653
+ function initAudioContext() {
654
+ if (!audioContext) {
655
+ try {
656
+ // Create audio context
657
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
658
+
659
+ // Setup the audio nodes
660
+ setupAudioNodes();
661
+
662
+ // Some browsers require a resume after creation
663
+ if (audioContext.state === 'suspended') {
664
+ audioContext.resume().then(() => {
665
+ console.log('AudioContext resumed successfully');
666
+ }).catch(err => {
667
+ console.error('Error resuming AudioContext:', err);
668
+ showAudioWarning();
669
+ });
670
+ }
671
+
672
+ // Show warning if context is suspended after creation
673
+ if (audioContext.state === 'suspended') {
674
+ showAudioWarning();
675
+ }
676
+
677
+ } catch (error) {
678
+ console.error('Error initializing audio context:', error);
679
+ showAudioWarning();
680
+ }
681
+ }
682
+ }
683
+
684
+ // Setup audio nodes
685
+ function setupAudioNodes() {
686
+ try {
687
+ // Create oscillators
688
+ leftOscillator = audioContext.createOscillator();
689
+ rightOscillator = audioContext.createOscillator();
690
+
691
+ // Set oscillator types to sine waves for smooth binaural beats
692
+ leftOscillator.type = 'sine';
693
+ rightOscillator.type = 'sine';
694
+
695
+ // Create gain nodes
696
+ leftGain = audioContext.createGain();
697
+ rightGain = audioContext.createGain();
698
+
699
+ // Create panners for spatial audio
700
+ pannerLeft = audioContext.createPanner();
701
+ pannerRight = audioContext.createPanner();
702
+
703
+ // Create analysers for visualization
704
+ analyserLeft = audioContext.createAnalyser();
705
+ analyserRight = audioContext.createAnalyser();
706
+
707
+ analyserLeft.fftSize = 2048;
708
+ analyserRight.fftSize = 2048;
709
+
710
+ dataArrayLeft = new Uint8Array(analyserLeft.frequencyBinCount);
711
+ dataArrayRight = new Uint8Array(analyserRight.frequencyBinCount);
712
+
713
+ // Connect nodes
714
+ leftOscillator.connect(leftGain);
715
+ rightOscillator.connect(rightGain);
716
+
717
+ leftGain.connect(pannerLeft);
718
+ rightGain.connect(pannerRight);
719
+
720
+ pannerLeft.connect(analyserLeft);
721
+ pannerRight.connect(analyserRight);
722
+
723
+ analyserLeft.connect(audioContext.destination);
724
+ analyserRight.connect(audioContext.destination);
725
+
726
+ // Set initial values
727
+ updateAudioParameters();
728
+
729
+ // Start oscillators
730
+ leftOscillator.start();
731
+ rightOscillator.start();
732
+
733
+ analyserInitialized = true;
734
+ } catch (error) {
735
+ console.error('Error setting up audio nodes:', error);
736
+ showAudioWarning();
737
+ }
738
+ }
739
+
740
+ // Update audio parameters based on UI controls
741
+ function updateAudioParameters() {
742
+ if (!audioContext || !audioStarted) return;
743
+
744
+ try {
745
+ // Base frequency - slightly different for each ear to create binaural beats
746
+ const baseFrequency = parseFloat(baseFrequencySlider.value);
747
+ const beatFrequency = parseFloat(beatFrequencySlider.value);
748
+
749
+ leftOscillator.frequency.setValueAtTime(baseFrequency - (beatFrequency / 2), audioContext.currentTime);
750
+ rightOscillator.frequency.setValueAtTime(baseFrequency + (beatFrequency / 2), audioContext.currentTime);
751
+
752
+ // Beat volume
753
+ const beatVolume = parseFloat(beatVolumeSlider.value);
754
+ leftGain.gain.setValueAtTime(beatVolume, audioContext.currentTime);
755
+ rightGain.gain.setValueAtTime(beatVolume, audioContext.currentTime);
756
+
757
+ // Spatial audio
758
+ updateSpatialPosition();
759
+
760
+ // Spatial volume
761
+ const spatialVolume = parseFloat(spatialVolumeSlider.value);
762
+ // Panner nodes handle some volume based on position, but we can adjust overall level
763
+ pannerLeft.setDistanceModel('linear');
764
+ pannerLeft.refDistance = 1;
765
+ pannerLeft.maxDistance = 10;
766
+ pannerLeft.rolloffFactor = 1;
767
+
768
+ pannerRight.setDistanceModel('linear');
769
+ pannerRight.refDistance = 1;
770
+ pannerRight.maxDistance = 10;
771
+ pannerRight.rolloffFactor = 1;
772
+ } catch (error) {
773
+ console.error('Error updating audio parameters:', error);
774
+ }
775
+ }
776
+
777
+ // Update spatial positioning
778
+ function updateSpatialPosition() {
779
+ if (!audioContext || !audioStarted) return;
780
+
781
+ try {
782
+ const distance = parseFloat(distanceSlider.value);
783
+ const x = parseFloat(positionIndicator.dataset.x) || 0;
784
+ const y = parseFloat(positionIndicator.dataset.y) || 0;
785
+ const z = Math.min(0, -(distance * 0.5)); // Position slightly behind to simulate natural listening
786
+
787
+ // Add trail point (if not already tracking this position)
788
+ const lastPoint = trailPoints[trailPoints.length - 1];
789
+ if (!lastPoint || lastPoint.x !== x || lastPoint.y !== y || lastPoint.z !== z) {
790
+ trailPoints.push({ x, y, z, timestamp: Date.now() });
791
+ drawTrail();
792
+ }
793
+
794
+ // Set positions with slight separation for stereo effect
795
+ pannerLeft.positionX.setValueAtTime(x - 0.1, audioContext.currentTime);
796
+ pannerLeft.positionY.setValueAtTime(y, audioContext.currentTime);
797
+ pannerLeft.positionZ.setValueAtTime(z, audioContext.currentTime);
798
+
799
+ pannerRight.positionX.setValueAtTime(x + 0.1, audioContext.currentTime);
800
+ pannerRight.positionY.setValueAtTime(y, audioContext.currentTime);
801
+ pannerRight.positionZ.setValueAtTime(z, audioContext.currentTime);
802
+
803
+ // Adjust distance - this affects volume and high frequency attenuation
804
+ pannerLeft.refDistance = distance;
805
+ pannerRight.refDistance = distance;
806
+ } catch (error) {
807
+ console.error('Error updating spatial position:', error);
808
+ }
809
+ }
810
+
811
+ // Draw trail of movement
812
+ function drawTrail() {
813
+ if (!trailCanvas || !trailCtx) return;
814
+
815
+ // Clear and redraw all trail points
816
+ trailCtx.clearRect(0, 0, trailCanvas.width, trailCanvas.height);
817
+
818
+ const centerX = trailCanvas.width / 2;
819
+ const centerY = trailCanvas.height / 2;
820
+ const radius = Math.min(trailCanvas.width, trailCanvas.height) / 2;
821
+
822
+ trailCtx.strokeStyle = 'rgba(79, 195, 247, 0.3)';
823
+ trailCtx.lineWidth = 1;
824
+ trailCtx.beginPath();
825
+
826
+ for (let i = 0; i < trailPoints.length; i++) {
827
+ const point = trailPoints[i];
828
+ const px = centerX + point.x * radius;
829
+ const py = centerY + point.y * radius;
830
+
831
+ if (i === 0) {
832
+ trailCtx.moveTo(px, py);
833
+ } else {
834
+ trailCtx.lineTo(px, py);
835
+ }
836
+ }
837
+
838
+ trailCtx.stroke();
839
+ }
840
+
841
+ // Visualization functions
842
+ function drawWaveform() {
843
+ if (!analyserInitialized || !isPlaying) return;
844
+
845
+ try {
846
+ analyserLeft.getByteTimeDomainData(dataArrayLeft);
847
+ analyserRight.getByteTimeDomainData(dataArrayRight);
848
+
849
+ waveformCanvas.width = waveformCanvas.clientWidth;
850
+ waveformCanvas.height = waveformCanvas.clientHeight;
851
+
852
+ const width = waveformCanvas.width;
853
+ const height = waveformCanvas.height;
854
+
855
+ waveformCtx.clearRect(0, 0, width, height);
856
+
857
+ // Draw left channel (top half)
858
+ waveformCtx.strokeStyle = '#4fc3f7';
859
+ waveformCtx.lineWidth = 2;
860
+ waveformCtx.beginPath();
861
+
862
+ const sliceWidthLeft = width / analyserLeft.frequencyBinCount;
863
+ let x = 0;
864
+
865
+ for (let i = 0; i < analyserLeft.frequencyBinCount; i++) {
866
+ const v = dataArrayLeft[i] / 128.0;
867
+ const y = v * (height / 4);
868
+
869
+ if (i === 0) {
870
+ waveformCtx.moveTo(x, y + (height / 4));
871
+ } else {
872
+ waveformCtx.lineTo(x, y + (height / 4));
873
+ }
874
+
875
+ x += sliceWidthLeft;
876
+ }
877
+
878
+ waveformCtx.stroke();
879
+
880
+ // Draw right channel (bottom half)
881
+ waveformCtx.strokeStyle = '#f44336';
882
+ waveformCtx.beginPath();
883
+
884
+ x = 0;
885
+
886
+ for (let i = 0; i < analyserRight.frequencyBinCount; i++) {
887
+ const v = dataArrayRight[i] / 128.0;
888
+ const y = v * (height / 4);
889
+
890
+ if (i === 0) {
891
+ waveformCtx.moveTo(x, y + (3 * height / 4));
892
+ } else {
893
+ waveformCtx.lineTo(x, y + (3 * height / 4));
894
+ }
895
+
896
+ x += sliceWidthLeft;
897
+ }
898
+
899
+ waveformCtx.stroke();
900
+
901
+ // Draw center line
902
+ waveformCtx.strokeStyle = 'rgba(255, 255, 255, 0.2)';
903
+ waveformCtx.lineWidth = 1;
904
+ waveformCtx.beginPath();
905
+ waveformCtx.moveTo(0, height / 2);
906
+ waveformCtx.lineTo(width, height / 2);
907
+ waveformCtx.stroke();
908
+
909
+ // Add labels
910
+ waveformCtx.fillStyle = 'white';
911
+ waveformCtx.font = '12px Arial';
912
+ waveformCtx.fillText('Left Channel', 10, 20);
913
+ waveformCtx.fillText('Right Channel', 10, height - 10);
914
+ } catch (error) {
915
+ console.error('Error drawing waveform:', error);
916
+ }
917
+ }
918
+
919
+ function drawFrequency() {
920
+ if (!analyserInitialized || !isPlaying) return;
921
+
922
+ try {
923
+ analyserLeft.getByteFrequencyData(dataArrayLeft);
924
+ analyserRight.getByteFrequencyData(dataArrayRight);
925
+
926
+ frequencyCanvas.width = frequencyCanvas.clientWidth;
927
+ frequencyCanvas.height = frequencyCanvas.clientHeight;
928
+
929
+ const width = frequencyCanvas.width;
930
+ const height = frequencyCanvas.height;
931
+
932
+ frequencyCtx.clearRect(0, 0, width, height);
933
+
934
+ // Draw left channel (blue)
935
+ const barWidthLeft = width / analyserLeft.frequencyBinCount;
936
+ let x = 0;
937
+
938
+ for (let i = 0; i < analyserLeft.frequencyBinCount; i++) {
939
+ const v = dataArrayLeft[i] / 255;
940
+ const barHeight = v * height;
941
+
942
+ frequencyCtx.fillStyle = `rgba(79, 195, 247, ${v})`;
943
+ frequencyCtx.fillRect(x, height - barHeight, barWidthLeft, barHeight);
944
+
945
+ x += barWidthLeft;
946
+ }
947
+
948
+ // Draw right channel (red, semi-transparent over blue)
949
+ const barWidthRight = width / analyserRight.frequencyBinCount;
950
+ x = 0;
951
+
952
+ for (let i = 0; i < analyserRight.frequencyBinCount; i++) {
953
+ const v = dataArrayRight[i] / 255;
954
+ const barHeight = v * height;
955
+
956
+ frequencyCtx.fillStyle = `rgba(244, 67, 54, ${v})`;
957
+ frequencyCtx.fillRect(x, height - barHeight, barWidthRight, barHeight);
958
+
959
+ x += barWidthRight;
960
+ }
961
+
962
+ // Add frequency markers
963
+ frequencyCtx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
964
+ frequencyCtx.lineWidth = 0.5;
965
+ frequencyCtx.font = '10px Arial';
966
+ frequencyCtx.fillStyle = 'white';
967
+
968
+ const freqLabels = [100, 200, 500, 1000, 2000, 4000, 8000];
969
+ const sampleRate = audioContext.sampleRate;
970
+
971
+ freqLabels.forEach(freq => {
972
+ const pos = (freq / (sampleRate / 2)) * width;
973
+
974
+ frequencyCtx.beginPath();
975
+ frequencyCtx.moveTo(pos, 0);
976
+ frequencyCtx.lineTo(pos, height);
977
+ frequencyCtx.stroke();
978
+
979
+ frequencyCtx.fillText(`${freq}Hz`, pos + 3, height - 5);
980
+ });
981
+ } catch (error) {
982
+ console.error('Error drawing frequency:', error);
983
+ }
984
+ }
985
+
986
+ function animate() {
987
+ drawWaveform();
988
+ drawFrequency();
989
+ animationId = requestAnimationFrame(animate);
990
+ }
991
+
992
+ // Start geometric pattern animation
993
+ function startPattern(patternName) {
994
+ stopPattern(); // Stop any existing pattern
995
+
996
+ if (!patternName || patternName === 'none') {
997
+ activePattern = null;
998
+ patternPlayBtn.style.display = 'none';
999
+ return;
1000
+ }
1001
+
1002
+ activePattern = patternName;
1003
+ const pattern = patterns[patternName];
1004
+ let t = 0;
1005
+ const speed = parseFloat(patternSpeedSlider.value);
1006
+ patternPlayBtn.style.display = 'flex';
1007
+ patternPlayBtn.innerHTML = `<i class="fas ${patternName === 'none' ? 'fa-play' : 'fa-pause'}"></i> ${pattern.name}`;
1008
+
1009
+ // Highlight active pattern button
1010
+ Object.values(patternButtons).forEach(btn => btn.classList.remove('pattern-active'));
1011
+ if (patternName !== 'none') {
1012
+ patternButtons[patternName].classList.add('pattern-active');
1013
+ }
1014
+
1015
+ patternInterval = setInterval(() => {
1016
+ t += 0.05;
1017
+ const pos = pattern.func(t, speed);
1018
+
1019
+ // Update position indicator data
1020
+ positionIndicator.dataset.x = pos.x.toFixed(4);
1021
+ positionIndicator.dataset.y = pos.y.toFixed(4);
1022
+
1023
+ // Update visual position
1024
+ const controlRect = positionControl.getBoundingClientRect();
1025
+ const centerX = controlRect.width / 2;
1026
+ const centerY = controlRect.height / 2;
1027
+
1028
+ positionIndicator.style.left = `${50 + pos.x * 50}%`;
1029
+ positionIndicator.style.top = `${50 + pos.y * 50}%`;
1030
+
1031
+ // Update audio
1032
+ updateAudioParameters();
1033
+ }, 50);
1034
+ }
1035
+
1036
+ // Stop geometric pattern animation
1037
+ function stopPattern() {
1038
+ if (patternInterval) {
1039
+ clearInterval(patternInterval);
1040
+ patternInterval = null;
1041
+ }
1042
+ Object.values(patternButtons).forEach(btn => btn.classList.remove('pattern-active'));
1043
+ }
1044
+
1045
+ // Toggle current pattern play/pause
1046
+ function togglePattern() {
1047
+ if (patternInterval) {
1048
+ stopPattern();
1049
+ patternPlayBtn.innerHTML = `<i class="fas fa-play"></i> Play ${patterns[activePattern].name}`;
1050
+ } else if (activePattern) {
1051
+ startPattern(activePattern);
1052
+ patternPlayBtn.innerHTML = `<i class="fas fa-pause"></i> Pause ${patterns[activePattern].name}`;
1053
+ }
1054
+ }
1055
+
1056
+ // Position control interaction
1057
+ function setupPositionControl() {
1058
+ createTrailCanvas();
1059
+ const controlRect = positionControl.getBoundingClientRect();
1060
+ const centerX = controlRect.width / 2;
1061
+ const centerY = controlRect.height / 2;
1062
+
1063
+ // Initialize position indicator in the center
1064
+ positionIndicator.dataset.x = '0';
1065
+ positionIndicator.dataset.y = '0';
1066
+ positionIndicator.style.left = '50%';
1067
+ positionIndicator.style.top = '50%';
1068
+
1069
+ // Set up drag interaction
1070
+ let isDragging = false;
1071
+
1072
+ positionIndicator.addEventListener('mousedown', (e) => {
1073
+ isDragging = true;
1074
+ stopPattern(); // Stop any active pattern when user moves manually
1075
+ activePattern = null;
1076
+ patternPlayBtn.style.display = 'none';
1077
+ e.stopPropagation();
1078
+ });
1079
+
1080
+ document.addEventListener('mousemove', (e) => {
1081
+ if (!isDragging) return;
1082
+
1083
+ const controlRect = positionControl.getBoundingClientRect();
1084
+ const centerX = controlRect.left + controlRect.width / 2;
1085
+ const centerY = controlRect.top + controlRect.height / 2;
1086
+
1087
+ // Calculate position relative to center
1088
+ const x = (e.clientX - centerX) / (controlRect.width / 2);
1089
+ const y = (e.clientY - centerY) / (controlRect.height / 2);
1090
+
1091
+ // Constrain to circular boundary
1092
+ const radius = Math.sqrt(x * x + y * y);
1093
+ if (radius > 1) {
1094
+ const angle = Math.atan2(y, x);
1095
+ positionIndicator.dataset.x = Math.cos(angle).toFixed(2);
1096
+ positionIndicator.dataset.y = Math.sin(angle).toFixed(2);
1097
+ } else {
1098
+ positionIndicator.dataset.x = x.toFixed(2);
1099
+ positionIndicator.dataset.y = y.toFixed(2);
1100
+ }
1101
+
1102
+ // Update visual position
1103
+ positionIndicator.style.left = `${50 + x * 50}%`;
1104
+ positionIndicator.style.top = `${50 + y * 50}%`;
1105
+
1106
+ // Update audio
1107
+ if (isPlaying && audioStarted) {
1108
+ updateAudioParameters();
1109
+ }
1110
+ });
1111
+
1112
+ document.addEventListener('mouseup', () => {
1113
+ isDragging = false;
1114
+ });
1115
+
1116
+ // Also allow clicking anywhere in the control
1117
+ positionControl.addEventListener('click', (e) => {
1118
+ stopPattern(); // Stop any active pattern when user moves manually
1119
+ activePattern = null;
1120
+ patternPlayBtn.style.display = 'none';
1121
+
1122
+ const controlRect = positionControl.getBoundingClientRect();
1123
+ const centerX = controlRect.width / 2;
1124
+ const centerY = controlRect.height / 2;
1125
+
1126
+ const x = (e.offsetX - centerX) / centerX;
1127
+ const y = (e.offsetY - centerY) / centerY;
1128
+
1129
+ // Constrain to circular boundary
1130
+ const radius = Math.sqrt(x * x + y * y);
1131
+ if (radius > 1) {
1132
+ const angle = Math.atan2(y, x);
1133
+ positionIndicator.dataset.x = Math.cos(angle).toFixed(2);
1134
+ positionIndicator.dataset.y = Math.sin(angle).toFixed(2);
1135
+
1136
+ positionIndicator.style.left = `${50 + Math.cos(angle) * 50}%`;
1137
+ positionIndicator.style.top = `${50 + Math.sin(angle) * 50}%`;
1138
+ } else {
1139
+ positionIndicator.dataset.x = x.toFixed(2);
1140
+ positionIndicator.dataset.y = y.toFixed(2);
1141
+
1142
+ positionIndicator.style.left = `${(x + 1) * 50}%`;
1143
+ positionIndicator.style.top = `${(y + 1) * 50}%`;
1144
+ }
1145
+
1146
+ // Update audio
1147
+ if (isPlaying && audioStarted) {
1148
+ updateAudioParameters();
1149
+ }
1150
+ });
1151
+ }
1152
+
1153
+ // Event listeners for UI controls
1154
+ baseFrequencySlider.addEventListener('input', function() {
1155
+ baseFrequencyValue.textContent = this.value;
1156
+ if (isPlaying && audioStarted) updateAudioParameters();
1157
+ });
1158
+
1159
+ beatFrequencySlider.addEventListener('input', function() {
1160
+ beatFrequencyValue.textContent = parseFloat(this.value).toFixed(1);
1161
+ if (isPlaying && audioStarted) updateAudioParameters();
1162
+ });
1163
+
1164
+ beatVolumeSlider.addEventListener('input', function() {
1165
+ beatVolumeValue.textContent = parseFloat(this.value).toFixed(2);
1166
+ if (isPlaying && audioStarted) updateAudioParameters();
1167
+ });
1168
+
1169
+ distanceSlider.addEventListener('input', function() {
1170
+ distanceValue.textContent = parseFloat(this.value).toFixed(1);
1171
+ if (isPlaying && audioStarted) updateAudioParameters();
1172
+ });
1173
+
1174
+ spatialVolumeSlider.addEventListener('input', function() {
1175
+ spatialVolumeValue.textContent = parseFloat(this.value).toFixed(2);
1176
+ if (isPlaying && audioStarted) updateAudioParameters();
1177
+ });
1178
+
1179
+ // Pattern speed control
1180
+ patternSpeedSlider.addEventListener('input', function() {
1181
+ patternSpeedValue.textContent = parseFloat(this.value).toFixed(1) + 'x';
1182
+ if (patternInterval && activePattern) {
1183
+ startPattern(activePattern); // Restart with new speed
1184
+ }
1185
+ });
1186
+
1187
+ // Clear trails button
1188
+ clearTrailsBtn.addEventListener('click', clearTrails);
1189
+
1190
+ // Pattern buttons
1191
+ Object.entries(patternButtons).forEach(([patternName, button]) => {
1192
+ button.addEventListener('click', () => {
1193
+ if (patternName === 'off') {
1194
+ stopPattern();
1195
+ activePattern = null;
1196
+ patternPlayBtn.style.display = 'none';
1197
+ } else {
1198
+ startPattern(patternName);
1199
+ }
1200
+ });
1201
+ });
1202
+
1203
+ // Pattern play/pause button
1204
+ patternPlayBtn.addEventListener('click', togglePattern);
1205
+
1206
+ // Toggle playback
1207
+ toggleBtn.addEventListener('click', async function() {
1208
+ try {
1209
+ if (!audioStarted) {
1210
+ // First click - initialize audio
1211
+ initAudioContext();
1212
+
1213
+ // Wait for audio to be ready
1214
+ await audioContext.resume();
1215
+ audioStarted = true;
1216
+ }
1217
+
1218
+ if (isPlaying) {
1219
+ // Pause audio
1220
+ await audioContext.suspend();
1221
+ toggleBtn.innerHTML = '<i class="fas fa-play"></i> Start';
1222
+ toggleBtn.classList.remove('binaural-active');
1223
+ cancelAnimationFrame(animationId);
1224
+ isPlaying = false;
1225
+ } else {
1226
+ // Start or resume audio
1227
+ await audioContext.resume();
1228
+ toggleBtn.innerHTML = '<i class="fas fa-pause"></i> Pause';
1229
+ toggleBtn.classList.add('binaural-active');
1230
+ animate();
1231
+ isPlaying = true;
1232
+ }
1233
+ } catch (error) {
1234
+ console.error('Error toggling playback:', error);
1235
+ showAudioWarning();
1236
+ }
1237
+ });
1238
+
1239
+ // Stop button - completely stops and resets
1240
+ stopBtn.addEventListener('click', function() {
1241
+ if (audioContext) {
1242
+ audioContext.suspend();
1243
+ cancelAnimationFrame(animationId);
1244
+ isPlaying = false;
1245
+ audioStarted = false;
1246
+ toggleBtn.innerHTML = '<i class="fas fa-play"></i> Start';
1247
+ toggleBtn.classList.remove('binaural-active');
1248
+
1249
+ // Clear visualizations
1250
+ waveformCtx.clearRect(0, 0, waveformCanvas.width, waveformCanvas.height);
1251
+ frequencyCtx.clearRect(0, 0, frequencyCanvas.width, frequencyCanvas.height);
1252
+
1253
+ // Stop pattern
1254
+ stopPattern();
1255
+ activePattern = null;
1256
+ patternPlayBtn.style.display = 'none';
1257
+
1258
+ // Show warning that audio needs to be started again
1259
+ showAudioWarning();
1260
+ }
1261
+ });
1262
+
1263
+ // Preset buttons
1264
+ presetButtons.forEach(btn => {
1265
+ btn.addEventListener('click', function() {
1266
+ const base = parseFloat(this.dataset.base);
1267
+ const beat = parseFloat(this.dataset.beat);
1268
+
1269
+ baseFrequencySlider.value = base;
1270
+ beatFrequencySlider.value = beat;
1271
+
1272
+ baseFrequencyValue.textContent = base;
1273
+ beatFrequencyValue.textContent = beat.toFixed(1);
1274
+
1275
+ // Reset position to center
1276
+ positionIndicator.dataset.x = '0';
1277
+ positionIndicator.dataset.y = '0';
1278
+ positionIndicator.style.left = '50%';
1279
+ positionIndicator.style.top = '50%';
1280
+
1281
+ distanceSlider.value = '1';
1282
+ distanceValue.textContent = '1';
1283
+
1284
+ if (isPlaying && audioStarted) {
1285
+ updateAudioParameters();
1286
+ }
1287
+ });
1288
+ });
1289
+
1290
+ // Initialize position control
1291
+ setupPositionControl();
1292
+
1293
+ // Handle window resize
1294
+ window.addEventListener('resize', function() {
1295
+ if (trailCanvas) {
1296
+ trailCanvas.width = positionControl.offsetWidth;
1297
+ trailCanvas.height = positionControl.offsetHeight;
1298
+ drawTrail();
1299
+ }
1300
+
1301
+ if (isPlaying) {
1302
+ drawWaveform();
1303
+ drawFrequency();
1304
+ }
1305
+ });
1306
+
1307
+ // Initialize on any user interaction
1308
+ document.addEventListener('click', function() {
1309
+ initAudioContext();
1310
+ }, { once: true });
1311
+
1312
+ // Add a help message for mobile users
1313
+ if (/Mobi|Android/i.test(navigator.userAgent)) {
1314
+ showAudioWarning();
1315
+ }
1316
+ </script>
1317
+ <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: absolute; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">This website has been generated by <a href="https://enzostvs-deepsite.hf.space" style="color: #fff;" target="_blank" >DeepSite</a> <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;"></p></body>
1318
+ </html>