fluxthedev commited on
Commit
1aea5a5
·
verified ·
1 Parent(s): d678477

Add 2 files

Browse files
Files changed (2) hide show
  1. README.md +7 -5
  2. index.html +754 -19
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: Modular 66
3
- emoji: 🦀
4
- colorFrom: indigo
5
- colorTo: blue
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: modular-66
3
+ emoji: 🐳
4
+ colorFrom: pink
5
+ colorTo: pink
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,754 @@
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>Analog Synth Emulator</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/build/Tone.min.js"></script>
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
10
+ <style>
11
+ @import url('https://fonts.googleapis.com/css2?family=Major+Mono+Display&display=swap');
12
+
13
+ :root {
14
+ --knob-size: 50px;
15
+ --panel-color: #111827;
16
+ --accent-color: #8b5cf6;
17
+ --highlight-color: #c4b5fd;
18
+ }
19
+
20
+ body {
21
+ font-family: 'Major Mono Display', monospace;
22
+ background-color: #1f2937;
23
+ color: white;
24
+ overflow-x: hidden;
25
+ }
26
+
27
+ .synth-panel {
28
+ background: linear-gradient(145deg, #111827, #1e293b);
29
+ border: 1px solid #374151;
30
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
31
+ }
32
+
33
+ .knob {
34
+ width: var(--knob-size);
35
+ height: var(--knob-size);
36
+ border-radius: 50%;
37
+ background: radial-gradient(circle at 30% 30%, #4b5563, #1f2937 75%);
38
+ border: 2px solid #374151;
39
+ position: relative;
40
+ cursor: pointer;
41
+ box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5);
42
+ }
43
+
44
+ .knob::after {
45
+ content: '';
46
+ position: absolute;
47
+ top: 5px;
48
+ left: 50%;
49
+ width: 3px;
50
+ height: 15px;
51
+ background-color: var(--highlight-color);
52
+ transform-origin: bottom center;
53
+ transform: translateX(-50%) rotate(0deg);
54
+ border-radius: 3px;
55
+ }
56
+
57
+ .button {
58
+ width: var(--knob-size);
59
+ height: var(--knob-size);
60
+ border-radius: 50%;
61
+ background: radial-gradient(circle at 30% 30%, #4b5563, #1f2937 75%);
62
+ border: 2px solid #374151;
63
+ cursor: pointer;
64
+ box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5);
65
+ display: flex;
66
+ justify-content: center;
67
+ align-items: center;
68
+ transition: all 0.3s;
69
+ }
70
+
71
+ .button.active {
72
+ background: radial-gradient(circle at 30% 30%, var(--accent-color), #1f2937 75%);
73
+ box-shadow: 0 0 10px var(--highlight-color);
74
+ }
75
+
76
+ .slider {
77
+ -webkit-appearance: none;
78
+ width: 100%;
79
+ height: 6px;
80
+ background: linear-gradient(90deg, var(--accent-color), #4b5563);
81
+ border-radius: 3px;
82
+ outline: none;
83
+ }
84
+
85
+ .slider::-webkit-slider-thumb {
86
+ -webkit-appearance: none;
87
+ width: 18px;
88
+ height: 18px;
89
+ border-radius: 50%;
90
+ background: var(--highlight-color);
91
+ cursor: pointer;
92
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
93
+ }
94
+
95
+ .key {
96
+ flex: 1;
97
+ height: 120px;
98
+ border: 1px solid #111827;
99
+ position: relative;
100
+ display: flex;
101
+ justify-content: center;
102
+ align-items: flex-end;
103
+ padding-bottom: 10px;
104
+ cursor: pointer;
105
+ transition: all 0.1s;
106
+ user-select: none;
107
+ }
108
+
109
+ .key.white {
110
+ background: linear-gradient(180deg, #f9fafb, #d1d5db);
111
+ color: #111827;
112
+ z-index: 1;
113
+ box-shadow: inset 0 -5px 10px rgba(0, 0, 0, 0.2);
114
+ }
115
+
116
+ .key.black {
117
+ background: linear-gradient(180deg, #111827, #4b5563);
118
+ width: 60%;
119
+ height: 80px;
120
+ margin-left: -15%;
121
+ margin-right: -15%;
122
+ z-index: 2;
123
+ color: white;
124
+ }
125
+
126
+ .key:hover {
127
+ opacity: 0.9;
128
+ }
129
+
130
+ .key.white.active {
131
+ background: linear-gradient(180deg, #d1d5db, #9ca3af);
132
+ box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.3);
133
+ }
134
+
135
+ .key.black.active {
136
+ background: linear-gradient(180deg, #4b5563, #111827);
137
+ box-shadow: inset 0 0 5px rgba(255, 255, 255, 0.1);
138
+ }
139
+
140
+ .patch-cable {
141
+ position: absolute;
142
+ width: 100%;
143
+ height: 100%;
144
+ pointer-events: none;
145
+ }
146
+
147
+ .oscilloscope {
148
+ width: 100%;
149
+ height: 100px;
150
+ background-color: #111827;
151
+ border: 1px solid var(--accent-color);
152
+ position: relative;
153
+ overflow: hidden;
154
+ }
155
+
156
+ .waveform {
157
+ position: absolute;
158
+ bottom: 50%;
159
+ left: 0;
160
+ width: 100%;
161
+ height: 2px;
162
+ background-color: var(--highlight-color);
163
+ transform-origin: left center;
164
+ }
165
+
166
+ .lfo-indicator {
167
+ position: absolute;
168
+ top: 50%;
169
+ left: 0;
170
+ width: 10px;
171
+ height: 10px;
172
+ background-color: #f59e0b;
173
+ border-radius: 50%;
174
+ transform: translate(-5px, -5px);
175
+ z-index: 10;
176
+ }
177
+
178
+ .cable-port {
179
+ width: 12px;
180
+ height: 12px;
181
+ border-radius: 50%;
182
+ background-color: #111827;
183
+ border: 2px solid var(--accent-color);
184
+ cursor: pointer;
185
+ position: relative;
186
+ z-index: 5;
187
+ }
188
+
189
+ .cable-port.active {
190
+ background-color: var(--highlight-color);
191
+ }
192
+
193
+ @keyframes blink {
194
+ 0%, 100% { opacity: 1; }
195
+ 50% { opacity: 0.5; }
196
+ }
197
+
198
+ .blink {
199
+ animation: blink 1s infinite;
200
+ }
201
+ </style>
202
+ </head>
203
+ <body class="min-h-screen flex flex-col items-center justify-center p-4">
204
+ <div class="w-full max-w-5xl">
205
+ <div class="synth-panel rounded-xl p-6 mb-8">
206
+ <h1 class="text-3xl font-bold mb-2 text-center text-violet-400">MODULAR 66</h1>
207
+ <p class="text-xs text-gray-400 text-center mb-6">ANALOGUE MONOPHONIC SYNTHESIZER</p>
208
+
209
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
210
+ <!-- VCO Section -->
211
+ <div class="bg-gray-900/50 p-4 rounded-lg border border-gray-700">
212
+ <h2 class="text-lg mb-3 text-violet-300 border-b border-gray-700 pb-2 flex items-center">
213
+ <i class="fas fa-wave-square mr-2"></i> VCO
214
+ </h2>
215
+ <div class="grid grid-cols-3 gap-4">
216
+ <div class="flex flex-col items-center">
217
+ <label class="text-xs mb-1">WAVE</label>
218
+ <div class="flex flex-col space-y-2">
219
+ <div class="button" data-wave="sine" title="Sine">
220
+ <i class="fas fa-wave-sine"></i>
221
+ </div>
222
+ <div class="button" data-wave="square" title="Square">
223
+ <i class="fas fa-wave-square"></i>
224
+ </div>
225
+ <div class="button" data-wave="sawtooth" title="Sawtooth">
226
+ <i class="fas fa-wave-triangle"></i>
227
+ </div>
228
+ </div>
229
+ </div>
230
+ <div class="flex flex-col items-center">
231
+ <label class="text-xs mb-1">OCT</label>
232
+ <div class="knob" id="octave-knob"></div>
233
+ <span class="text-xs mt-1" id="octave-value">0</span>
234
+ </div>
235
+ <div class="flex flex-col items-center">
236
+ <label class="text-xs mb-1">DETUNE</label>
237
+ <div class="knob" id="detune-knob"></div>
238
+ <span class="text-xs mt-1" id="detune-value">0</span>
239
+ </div>
240
+ </div>
241
+ </div>
242
+
243
+ <!-- VCF Section -->
244
+ <div class="bg-gray-900/50 p-4 rounded-lg border border-gray-700">
245
+ <h2 class="text-lg mb-3 text-violet-300 border-b border-gray-700 pb-2 flex items-center">
246
+ <i class="fas fa-filter mr-2"></i> VCF
247
+ </h2>
248
+ <div class="grid grid-cols-3 gap-4">
249
+ <div class="flex flex-col items-center">
250
+ <label class="text-xs mb-1">CUTOFF</label>
251
+ <div class="knob" id="cutoff-knob"></div>
252
+ <span class="text-xs mt-1" id="cutoff-value">1000</span>
253
+ </div>
254
+ <div class="flex flex-col items-center">
255
+ <label class="text-xs mb-1">RES</label>
256
+ <div class="knob" id="resonance-knob"></div>
257
+ <span class="text-xs mt-1" id="resonance-value">0.5</span>
258
+ </div>
259
+ <div class="flex flex-col items-center">
260
+ <label class="text-xs mb-1">ENV</label>
261
+ <div class="knob" id="filter-env-knob"></div>
262
+ <span class="text-xs mt-1" id="filter-env-value">0.5</span>
263
+ </div>
264
+ </div>
265
+ </div>
266
+
267
+ <!-- ENV Section -->
268
+ <div class="bg-gray-900/50 p-4 rounded-lg border border-gray-700">
269
+ <h2 class="text-lg mb-3 text-violet-300 border-b border-gray-700 pb-2 flex items-center">
270
+ <i class="fas fa-project-diagram mr-2"></i> ENVELOPE
271
+ </h2>
272
+ <div class="grid grid-cols-4 gap-2">
273
+ <div class="flex flex-col items-center">
274
+ <label class="text-xs mb-1">A</label>
275
+ <div class="knob small" id="attack-knob"></div>
276
+ <span class="text-xs mt-1" id="attack-value">0.1</span>
277
+ </div>
278
+ <div class="flex flex-col items-center">
279
+ <label class="text-xs mb-1">D</label>
280
+ <div class="knob small" id="decay-knob"></div>
281
+ <span class="text-xs mt-1" id="decay-value">0.3</span>
282
+ </div>
283
+ <div class="flex flex-col items-center">
284
+ <label class="text-xs mb-1">S</label>
285
+ <div class="knob small" id="sustain-knob"></div>
286
+ <span class="text-xs mt-1" id="sustain-value">0.5</span>
287
+ </div>
288
+ <div class="flex flex-col items-center">
289
+ <label class="text-xs mb-1">R</label>
290
+ <div class="knob small" id="release-knob"></div>
291
+ <span class="text-xs mt-1" id="release-value">1.0</span>
292
+ </div>
293
+ </div>
294
+ </div>
295
+ </div>
296
+
297
+ <!-- LFO Section -->
298
+ <div class="mt-6 bg-gray-900/50 p-4 rounded-lg border border-gray-700">
299
+ <h2 class="text-lg mb-3 text-violet-300 border-b border-gray-700 pb-2 flex items-center">
300
+ <i class="fas fa-asterisk mr-2 blink"></i> LFO
301
+ </h2>
302
+ <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
303
+ <div class="flex flex-col items-center">
304
+ <label class="text-xs mb-1">WAVE</label>
305
+ <div class="flex space-x-2">
306
+ <div class="button" data-lfo="sine" title="Sine">
307
+ <i class="fas fa-wave-sine"></i>
308
+ </div>
309
+ <div class="button" data-lfo="square" title="Square">
310
+ <i class="fas fa-wave-square"></i>
311
+ </div>
312
+ </div>
313
+ </div>
314
+ <div class="flex flex-col items-center">
315
+ <label class="text-xs mb-1">RATE</label>
316
+ <div class="knob" id="lfo-rate-knob"></div>
317
+ <span class="text-xs mt-1" id="lfo-rate-value">1.0</span>
318
+ </div>
319
+ <div class="flex flex-col items-center">
320
+ <label class="text-xs mb-1">AMOUNT</label>
321
+ <div class="knob" id="lfo-amount-knob"></div>
322
+ <span class="text-xs mt-1" id="lfo-amount-value">0.5</span>
323
+ </div>
324
+ <div class="flex flex-col items-center">
325
+ <label class="text-xs mb-1">TARGET</label>
326
+ <select id="lfo-target" class="bg-gray-800 text-white text-xs p-1 rounded border border-gray-700 w-full">
327
+ <option value="cutoff">Cutoff</option>
328
+ <option value="detune">Detune</option>
329
+ </select>
330
+ </div>
331
+ </div>
332
+ </div>
333
+
334
+ <!-- Oscilloscope Display -->
335
+ <div class="mt-6 flex flex-col">
336
+ <div class="oscilloscope rounded-t-lg" id="oscilloscope">
337
+ <div class="waveform"></div>
338
+ <div class="lfo-indicator"></div>
339
+ </div>
340
+ <div class="bg-gray-900/50 p-2 rounded-b-lg border border-gray-700 border-t-0 flex justify-between">
341
+ <span class="text-xs text-violet-300">SIGNAL OUTPUT</span>
342
+ <div class="flex items-center space-x-2">
343
+ <div class="cable-port" data-target="vcf"></div>
344
+ <div class="cable-port" data-target="lfo"></div>
345
+ </div>
346
+ </div>
347
+ </div>
348
+
349
+ <!-- Keyboard -->
350
+ <div class="mt-6 flex h-32 bg-gray-900 rounded-b-lg">
351
+ <div class="key white" data-note="C"><span>C</span></div>
352
+ <div class="key black" data-note="C#"></div>
353
+ <div class="key white" data-note="D"><span>D</span></div>
354
+ <div class="key black" data-note="D#"></div>
355
+ <div class="key white" data-note="E"><span>E</span></div>
356
+ <div class="key white" data-note="F"><span>F</span></div>
357
+ <div class="key black" data-note="F#"></div>
358
+ <div class="key white" data-note="G"><span>G</span></div>
359
+ <div class="key black" data-note="G#"></div>
360
+ <div class="key white" data-note="A"><span>A</span></div>
361
+ <div class="key black" data-note="A#"></div>
362
+ <div class="key white" data-note="B"><span>B</span></div>
363
+ <div class="key white" data-note="C" data-octave="1"><span>C</span></div>
364
+ </div>
365
+ </div>
366
+ </div>
367
+
368
+ <div class="text-xs text-gray-400 mt-4">
369
+ MODULAR 66 ANALOG SYNTHESIZER EMULATOR • USE KEYBOARD WITH QWERTY: A,S,D...E,R...U,I,O
370
+ </div>
371
+
372
+ <svg id="patch-cable" class="patch-cable">
373
+ <path stroke="var(--accent-color)" stroke-width="2" fill="none" />
374
+ </svg>
375
+
376
+ <script>
377
+ document.addEventListener('DOMContentLoaded', function() {
378
+ // Initialize Tone.js
379
+ const synth = new Tone.MonoSynth({
380
+ oscillator: {
381
+ type: "sine"
382
+ },
383
+ envelope: {
384
+ attack: 0.1,
385
+ decay: 0.3,
386
+ sustain: 0.5,
387
+ release: 1
388
+ },
389
+ filter: {
390
+ Q: 1,
391
+ type: "lowpass",
392
+ rolloff: -12
393
+ },
394
+ filterEnvelope: {
395
+ attack: 0.001,
396
+ decay: 0.2,
397
+ sustain: 0.5,
398
+ release: 0.2,
399
+ baseFrequency: 300,
400
+ octaves: 4
401
+ }
402
+ }).toDestination();
403
+
404
+ // LFO
405
+ const lfo = new Tone.LFO({
406
+ type: "sine",
407
+ frequency: 1,
408
+ min: 0,
409
+ max: 1
410
+ }).start();
411
+
412
+ // Set up keyboard mapping
413
+ const keyMap = {
414
+ 'a': 'C',
415
+ 'w': 'C#',
416
+ 's': 'D',
417
+ 'e': 'D#',
418
+ 'd': 'E',
419
+ 'f': 'F',
420
+ 't': 'F#',
421
+ 'g': 'G',
422
+ 'y': 'G#',
423
+ 'h': 'A',
424
+ 'u': 'A#',
425
+ 'j': 'B',
426
+ 'k': 'C1'
427
+ };
428
+
429
+ // Current state
430
+ let currentWave = "sine";
431
+ let currentLfoWave = "sine";
432
+ let lfoTarget = "cutoff";
433
+ let activeKeys = {};
434
+ let draggingCable = false;
435
+ let cableStart = null;
436
+
437
+ // DOM elements
438
+ const keys = document.querySelectorAll('.key');
439
+ const waveButtons = document.querySelectorAll('[data-wave]');
440
+ const lfoButtons = document.querySelectorAll('[data-lfo]');
441
+ const cablePorts = document.querySelectorAll('.cable-port');
442
+ const patchCable = document.querySelector('#patch-cable path');
443
+
444
+ // Knob elements and their ranges
445
+ const knobs = {
446
+ 'octave': { element: document.getElementById('octave-knob'), valueElement: document.getElementById('octave-value'), min: -2, max: 2, step: 1, value: 0 },
447
+ 'detune': { element: document.getElementById('detune-knob'), valueElement: document.getElementById('detune-value'), min: -1200, max: 1200, step: 10, value: 0 },
448
+ 'cutoff': { element: document.getElementById('cutoff-knob'), valueElement: document.getElementById('cutoff-value'), min: 20, max: 20000, value: 1000 },
449
+ 'resonance': { element: document.getElementById('resonance-knob'), valueElement: document.getElementById('resonance-value'), min: 0.1, max: 10, value: 1 },
450
+ 'filter-env': { element: document.getElementById('filter-env-knob'), valueElement: document.getElementById('filter-env-value'), min: 0, max: 1, value: 0.5 },
451
+ 'attack': { element: document.getElementById('attack-knob'), valueElement: document.getElementById('attack-value'), min: 0.001, max: 2, value: 0.1 },
452
+ 'decay': { element: document.getElementById('decay-knob'), valueElement: document.getElementById('decay-value'), min: 0.001, max: 2, value: 0.3 },
453
+ 'sustain': { element: document.getElementById('sustain-knob'), valueElement: document.getElementById('sustain-value'), min: 0, max: 1, value: 0.5 },
454
+ 'release': { element: document.getElementById('release-knob'), valueElement: document.getElementById('release-value'), min: 0.001, max: 4, value: 1 },
455
+ 'lfo-rate': { element: document.getElementById('lfo-rate-knob'), valueElement: document.getElementById('lfo-rate-value'), min: 0.1, max: 10, value: 1 },
456
+ 'lfo-amount': { element: document.getElementById('lfo-amount-knob'), valueElement: document.getElementById('lfo-amount-value'), min: 0, max: 1, value: 0.5 }
457
+ };
458
+
459
+ // Initialize knobs
460
+ Object.keys(knobs).forEach(key => {
461
+ const knob = knobs[key];
462
+ let rotation = 0;
463
+ let isDragging = false;
464
+ let startY = 0;
465
+
466
+ knob.element.addEventListener('mousedown', (e) => {
467
+ isDragging = true;
468
+ startY = e.clientY;
469
+ knob.element.style.cursor = 'grabbing';
470
+ e.preventDefault();
471
+ });
472
+
473
+ document.addEventListener('mousemove', (e) => {
474
+ if (!isDragging) return;
475
+
476
+ const deltaY = startY - e.clientY;
477
+ const sensitivity = key === 'octave' ? 50 : 100;
478
+ rotation += deltaY / sensitivity;
479
+
480
+ // Limit rotation to -150 to 150 degrees
481
+ rotation = Math.max(-150, Math.min(150, rotation));
482
+
483
+ // Calculate value based on rotation
484
+ const normalized = (rotation + 150) / 300; // 0 to 1
485
+ const value = knob.min + (normalized * (knob.max - knob.min));
486
+
487
+ // For octave knob, round to nearest step
488
+ const finalValue = knob.step ?
489
+ Math.round(value / knob.step) * knob.step :
490
+ Number(value.toFixed(3));
491
+
492
+ knob.value = finalValue;
493
+ updateKnob(knob.element, rotation);
494
+ updateSynthParameter(key, finalValue);
495
+
496
+ startY = e.clientY;
497
+ });
498
+
499
+ document.addEventListener('mouseup', () => {
500
+ isDragging = false;
501
+ knob.element.style.cursor = 'pointer';
502
+ });
503
+ });
504
+
505
+ // Update knob visual rotation
506
+ function updateKnob(element, rotation) {
507
+ element.style.transform = `rotate(${rotation}deg)`;
508
+ }
509
+
510
+ // Update synth parameters based on knob values
511
+ function updateSynthParameter(param, value) {
512
+ switch(param) {
513
+ case 'octave':
514
+ knobs.octave.valueElement.textContent = value;
515
+ break;
516
+ case 'detune':
517
+ synth.oscillator.detune = value;
518
+ knobs.detune.valueElement.textContent = value;
519
+ break;
520
+ case 'cutoff':
521
+ synth.filter.frequency.value = value;
522
+ knobs.cutoff.valueElement.textContent = Math.round(value);
523
+ break;
524
+ case 'resonance':
525
+ synth.filter.Q.value = value;
526
+ knobs.resonance.valueElement.textContent = value.toFixed(1);
527
+ break;
528
+ case 'filter-env':
529
+ synth.filterEnvelope.baseFrequency = 300 + (value * 4000);
530
+ knobs['filter-env'].valueElement.textContent = value.toFixed(2);
531
+ break;
532
+ case 'attack':
533
+ synth.envelope.attack = value;
534
+ knobs.attack.valueElement.textContent = value.toFixed(3);
535
+ break;
536
+ case 'decay':
537
+ synth.envelope.decay = value;
538
+ knobs.decay.valueElement.textContent = value.toFixed(3);
539
+ break;
540
+ case 'sustain':
541
+ synth.envelope.sustain = value;
542
+ knobs.sustain.valueElement.textContent = value.toFixed(2);
543
+ break;
544
+ case 'release':
545
+ synth.envelope.release = value;
546
+ knobs.release.valueElement.textContent = value.toFixed(3);
547
+ break;
548
+ case 'lfo-rate':
549
+ lfo.frequency.value = value;
550
+ knobs['lfo-rate'].valueElement.textContent = value.toFixed(1);
551
+ break;
552
+ case 'lfo-amount':
553
+ lfo.max = value;
554
+ knobs['lfo-amount'].valueElement.textContent = value.toFixed(2);
555
+ break;
556
+ }
557
+ }
558
+
559
+ // Waveform selection
560
+ waveButtons.forEach(button => {
561
+ button.addEventListener('click', () => {
562
+ waveButtons.forEach(btn => btn.classList.remove('active'));
563
+ button.classList.add('active');
564
+ currentWave = button.dataset.wave;
565
+ synth.oscillator.type = currentWave;
566
+ });
567
+ });
568
+
569
+ // LFO controls
570
+ lfoButtons.forEach(button => {
571
+ button.addEventListener('click', () => {
572
+ lfoButtons.forEach(btn => btn.classList.remove('active'));
573
+ button.classList.add('active');
574
+ currentLfoWave = button.dataset.lfo;
575
+ lfo.type = currentLfoWave;
576
+ });
577
+ });
578
+
579
+ document.getElementById('lfo-target').addEventListener('change', (e) => {
580
+ lfoTarget = e.target.value;
581
+
582
+ // Disconnect previous target
583
+ lfo.disconnect();
584
+
585
+ // Connect to new target
586
+ if (lfoTarget === 'cutoff') {
587
+ lfo.connect(synth.filter.frequency);
588
+ } else if (lfoTarget === 'detune') {
589
+ lfo.connect(synth.oscillator.detune);
590
+ }
591
+ });
592
+
593
+ // Keyboard interaction
594
+ keys.forEach(key => {
595
+ key.addEventListener('mousedown', () => playNote(key.dataset.note));
596
+ key.addEventListener('mouseup', () => releaseNote(key.dataset.note));
597
+ key.addEventListener('mouseleave', () => {
598
+ if (key.classList.contains('active')) {
599
+ releaseNote(key.dataset.note);
600
+ }
601
+ });
602
+ });
603
+
604
+ // Computer keyboard interaction
605
+ document.addEventListener('keydown', (e) => {
606
+ if (keyMap[e.key] && !activeKeys[e.key]) {
607
+ activeKeys[e.key] = true;
608
+ const note = keyMap[e.key];
609
+ const keyElement = document.querySelector(`.key[data-note="${note}"]`);
610
+ if (keyElement) {
611
+ keyElement.classList.add('active');
612
+ playNote(note);
613
+ }
614
+ }
615
+ });
616
+
617
+ document.addEventListener('keyup', (e) => {
618
+ if (keyMap[e.key]) {
619
+ const note = keyMap[e.key];
620
+ const keyElement = document.querySelector(`.key[data-note="${note}"]`);
621
+ if (keyElement) {
622
+ keyElement.classList.remove('active');
623
+ releaseNote(note);
624
+ }
625
+ delete activeKeys[e.key];
626
+ }
627
+ });
628
+
629
+ // Play a note
630
+ function playNote(note) {
631
+ let octave = 4;
632
+ if (note.endsWith('1')) {
633
+ note = note.replace('1', '');
634
+ octave = 5;
635
+ }
636
+
637
+ synth.triggerAttack(`${note}${octave}`);
638
+
639
+ // Visual feedback for active note
640
+ const activeNoteIndicator = document.createElement('div');
641
+ activeNoteIndicator.className = 'absolute top-0 left-0 w-full h-full bg-violet-400/20 pointer-events-none';
642
+ document.querySelector(`.key[data-note="${note}"${note.endsWith('1') ? ' data-octave="1"' : ''}]`)
643
+ .appendChild(activeNoteIndicator);
644
+
645
+ setTimeout(() => {
646
+ activeNoteIndicator.style.opacity = '0';
647
+ setTimeout(() => activeNoteIndicator.remove(), 300);
648
+ }, 50);
649
+ }
650
+
651
+ // Release a note
652
+ function releaseNote(note) {
653
+ let octave = 4;
654
+ if (note.endsWith('1')) {
655
+ note = note.replace('1', '');
656
+ octave = 5;
657
+ }
658
+
659
+ synth.triggerRelease();
660
+ }
661
+
662
+ // Patch cable interaction
663
+ cablePorts.forEach(port => {
664
+ port.addEventListener('mousedown', (e) => {
665
+ draggingCable = true;
666
+ cableStart = {
667
+ x: e.clientX,
668
+ y: e.clientY,
669
+ element: port
670
+ };
671
+ port.classList.add('active');
672
+ e.stopPropagation();
673
+ });
674
+ });
675
+
676
+ document.addEventListener('mousemove', (e) => {
677
+ if (!draggingCable) return;
678
+
679
+ const startRect = cableStart.element.getBoundingClientRect();
680
+ const startX = startRect.left + startRect.width / 2;
681
+ const startY = startRect.top + startRect.height / 2;
682
+
683
+ // Update cable path
684
+ patchCable.setAttribute('d', `M${startX},${startY} L${e.clientX},${e.clientY}`);
685
+ });
686
+
687
+ document.addEventListener('mouseup', (e) => {
688
+ if (!draggingCable) return;
689
+
690
+ // Find if we're dropping on another port
691
+ const endElement = document.elementFromPoint(e.clientX, e.clientY);
692
+ const endPort = endElement?.closest('.cable-port');
693
+
694
+ if (endPort && endPort !== cableStart.element) {
695
+ // Connect between these ports (in a real app, we'd implement the actual connection)
696
+ const startRect = cableStart.element.getBoundingClientRect();
697
+ const endRect = endPort.getBoundingClientRect();
698
+
699
+ const startX = startRect.left + startRect.width / 2;
700
+ const startY = startRect.top + startRect.height / 2;
701
+ const endX = endRect.left + endRect.width / 2;
702
+ const endY = endRect.top + endRect.height / 2;
703
+
704
+ patchCable.setAttribute('d', `M${startX},${startY} L${endX},${endY}`);
705
+
706
+ // Visual feedback for connected ports
707
+ cableStart.element.classList.add('active');
708
+ endPort.classList.add('active');
709
+ } else {
710
+ // Reset cable if not dropped on another port
711
+ patchCable.setAttribute('d', '');
712
+ cableStart.element.classList.remove('active');
713
+ }
714
+
715
+ draggingCable = false;
716
+ cableStart = null;
717
+ });
718
+
719
+ // Oscilloscope visualization
720
+ const waveform = document.querySelector('.waveform');
721
+ const lfoIndicator = document.querySelector('.lfo-indicator');
722
+
723
+ function updateVisualization() {
724
+ const now = Tone.now();
725
+ const lfoValue = lfo.getValueAtTime(now);
726
+ const lfoPos = (lfoValue + 1) / 2 * 100; // Convert to 0-100%
727
+
728
+ lfoIndicator.style.left = `${lfoPos}%`;
729
+
730
+ // Very simple waveform visualization (in a real app, use Tone.Visualizer)
731
+ const sinValue = Math.sin(now * 10) * 40;
732
+ waveform.style.transform = `translateY(${sinValue}px)`;
733
+
734
+ requestAnimationFrame(updateVisualization);
735
+ }
736
+
737
+ // Start with sine wave selected
738
+ document.querySelector('[data-wave="sine"]').click();
739
+ document.querySelector('[data-lfo="sine"]').click();
740
+
741
+ // Start visualization
742
+ updateVisualization();
743
+
744
+ // Set initial knob rotations based on default values
745
+ Object.keys(knobs).forEach(key => {
746
+ const knob = knobs[key];
747
+ const normalized = (knob.value - knob.min) / (knob.max - knob.min);
748
+ const rotation = (normalized * 300) - 150; // -150 to 150 degrees
749
+ updateKnob(knob.element, rotation);
750
+ });
751
+ });
752
+ </script>
753
+ <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - <a href="https://enzostvs-deepsite.hf.space?remix=fluxthedev/modular-66" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body>
754
+ </html>