namelessai commited on
Commit
77a0842
·
verified ·
1 Parent(s): 9ca1adb

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +800 -19
index.html CHANGED
@@ -1,19 +1,800 @@
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>Music Visualizer with Video Export</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <style>
10
+ .visualizer-container {
11
+ position: relative;
12
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
13
+ border-radius: 12px;
14
+ overflow: hidden;
15
+ box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3);
16
+ }
17
+
18
+ .visualizer-canvas {
19
+ width: 100%;
20
+ height: 100%;
21
+ display: block;
22
+ }
23
+
24
+ .recording-indicator {
25
+ position: absolute;
26
+ top: 20px;
27
+ right: 20px;
28
+ background-color: rgba(255, 0, 0, 0.7);
29
+ color: white;
30
+ padding: 8px 12px;
31
+ border-radius: 20px;
32
+ font-size: 14px;
33
+ display: none;
34
+ align-items: center;
35
+ gap: 8px;
36
+ animation: pulse 1.5s infinite;
37
+ }
38
+
39
+ @keyframes pulse {
40
+ 0% { opacity: 0.7; }
41
+ 50% { opacity: 1; }
42
+ 100% { opacity: 0.7; }
43
+ }
44
+
45
+ .visualizer-overlay {
46
+ position: absolute;
47
+ top: 0;
48
+ left: 0;
49
+ width: 100%;
50
+ height: 100%;
51
+ background: rgba(0, 0, 0, 0.3);
52
+ display: flex;
53
+ flex-direction: column;
54
+ justify-content: center;
55
+ align-items: center;
56
+ color: white;
57
+ transition: all 0.3s ease;
58
+ }
59
+
60
+ .visualizer-overlay.hidden {
61
+ opacity: 0;
62
+ pointer-events: none;
63
+ }
64
+
65
+ .file-input-label {
66
+ display: inline-block;
67
+ padding: 12px 24px;
68
+ background: linear-gradient(45deg, #4a00e0, #8e2de2);
69
+ color: white;
70
+ border-radius: 30px;
71
+ cursor: pointer;
72
+ font-weight: 600;
73
+ transition: all 0.3s ease;
74
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
75
+ }
76
+
77
+ .file-input-label:hover {
78
+ transform: translateY(-2px);
79
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
80
+ }
81
+
82
+ .file-input-label:active {
83
+ transform: translateY(0);
84
+ }
85
+
86
+ .audio-info {
87
+ margin-top: 20px;
88
+ text-align: center;
89
+ font-size: 18px;
90
+ }
91
+
92
+ .visualizer-controls {
93
+ position: absolute;
94
+ bottom: 20px;
95
+ left: 50%;
96
+ transform: translateX(-50%);
97
+ display: flex;
98
+ gap: 15px;
99
+ z-index: 10;
100
+ }
101
+
102
+ .control-btn {
103
+ width: 50px;
104
+ height: 50px;
105
+ border-radius: 50%;
106
+ background: rgba(255, 255, 255, 0.2);
107
+ backdrop-filter: blur(10px);
108
+ border: none;
109
+ color: white;
110
+ font-size: 20px;
111
+ cursor: pointer;
112
+ transition: all 0.3s ease;
113
+ display: flex;
114
+ justify-content: center;
115
+ align-items: center;
116
+ }
117
+
118
+ .control-btn:hover {
119
+ background: rgba(255, 255, 255, 0.3);
120
+ transform: scale(1.1);
121
+ }
122
+
123
+ .control-btn:active {
124
+ transform: scale(0.95);
125
+ }
126
+
127
+ .control-btn.primary {
128
+ background: linear-gradient(45deg, #4a00e0, #8e2de2);
129
+ width: 60px;
130
+ height: 60px;
131
+ font-size: 24px;
132
+ }
133
+
134
+ .progress-container {
135
+ position: absolute;
136
+ bottom: 0;
137
+ left: 0;
138
+ width: 100%;
139
+ height: 4px;
140
+ background: rgba(255, 255, 255, 0.1);
141
+ }
142
+
143
+ .progress-bar {
144
+ height: 100%;
145
+ background: linear-gradient(90deg, #4a00e0, #8e2de2);
146
+ width: 0%;
147
+ transition: width 0.1s linear;
148
+ }
149
+
150
+ .time-display {
151
+ position: absolute;
152
+ bottom: 10px;
153
+ right: 20px;
154
+ color: white;
155
+ font-size: 12px;
156
+ opacity: 0.8;
157
+ }
158
+
159
+ .visualizer-presets {
160
+ position: absolute;
161
+ top: 20px;
162
+ left: 20px;
163
+ display: flex;
164
+ gap: 10px;
165
+ z-index: 10;
166
+ }
167
+
168
+ .preset-btn {
169
+ padding: 8px 15px;
170
+ background: rgba(255, 255, 255, 0.1);
171
+ color: white;
172
+ border: none;
173
+ border-radius: 20px;
174
+ cursor: pointer;
175
+ font-size: 14px;
176
+ transition: all 0.3s ease;
177
+ }
178
+
179
+ .preset-btn:hover {
180
+ background: rgba(255, 255, 255, 0.2);
181
+ }
182
+
183
+ .preset-btn.active {
184
+ background: linear-gradient(45deg, #4a00e0, #8e2de2);
185
+ }
186
+ </style>
187
+ </head>
188
+ <body class="bg-gray-900 min-h-screen flex flex-col items-center justify-center p-4">
189
+ <div class="max-w-4xl w-full">
190
+ <h1 class="text-4xl font-bold text-center text-white mb-2">Audio Visualizer</h1>
191
+ <p class="text-center text-gray-400 mb-8">Visualize your music and export as video</p>
192
+
193
+ <div class="visualizer-container aspect-video w-full mb-6">
194
+ <canvas id="visualizer" class="visualizer-canvas"></canvas>
195
+
196
+ <div class="recording-indicator" id="recordingIndicator">
197
+ <i class="fas fa-circle"></i>
198
+ <span>Recording</span>
199
+ </div>
200
+
201
+ <div class="visualizer-overlay" id="visualizerOverlay">
202
+ <label for="audioFile" class="file-input-label">
203
+ <i class="fas fa-music mr-2"></i>
204
+ Select Audio File
205
+ </label>
206
+ <input type="file" id="audioFile" accept="audio/*" class="hidden">
207
+ <div class="audio-info mt-4" id="audioInfo">No file selected</div>
208
+ </div>
209
+
210
+ <div class="visualizer-presets">
211
+ <button class="preset-btn active" data-preset="bars">Bars</button>
212
+ <button class="preset-btn" data-preset="wave">Wave</button>
213
+ <button class="preset-btn" data-preset="particles">Particles</button>
214
+ <button class="preset-btn" data-preset="circle">Circle</button>
215
+ </div>
216
+
217
+ <div class="visualizer-controls">
218
+ <button class="control-btn" id="prevBtn" title="Previous">
219
+ <i class="fas fa-step-backward"></i>
220
+ </button>
221
+ <button class="control-btn primary" id="playBtn" title="Play/Pause">
222
+ <i class="fas fa-play" id="playIcon"></i>
223
+ </button>
224
+ <button class="control-btn" id="nextBtn" title="Next">
225
+ <i class="fas fa-step-forward"></i>
226
+ </button>
227
+ <button class="control-btn" id="recordBtn" title="Record Video">
228
+ <i class="fas fa-video"></i>
229
+ </button>
230
+ </div>
231
+
232
+ <div class="progress-container">
233
+ <div class="progress-bar" id="progressBar"></div>
234
+ </div>
235
+
236
+ <div class="time-display" id="timeDisplay">0:00 / 0:00</div>
237
+ </div>
238
+
239
+ <div class="flex justify-center gap-4 mb-8">
240
+ <div class="bg-gray-800 p-4 rounded-lg w-full max-w-xs">
241
+ <h3 class="text-white font-medium mb-2">Visualization Settings</h3>
242
+ <div class="space-y-3">
243
+ <div>
244
+ <label class="text-gray-300 text-sm block mb-1">Color 1</label>
245
+ <input type="color" id="color1" value="#4a00e0" class="w-full h-10">
246
+ </div>
247
+ <div>
248
+ <label class="text-gray-300 text-sm block mb-1">Color 2</label>
249
+ <input type="color" id="color2" value="#8e2de2" class="w-full h-10">
250
+ </div>
251
+ <div>
252
+ <label class="text-gray-300 text-sm block mb-1">Background</label>
253
+ <input type="color" id="bgColor" value="#16213e" class="w-full h-10">
254
+ </div>
255
+ </div>
256
+ </div>
257
+
258
+ <div class="bg-gray-800 p-4 rounded-lg w-full max-w-xs">
259
+ <h3 class="text-white font-medium mb-2">Audio Settings</h3>
260
+ <div class="space-y-3">
261
+ <div>
262
+ <label class="text-gray-300 text-sm block mb-1">Volume</label>
263
+ <input type="range" id="volumeControl" min="0" max="1" step="0.01" value="0.7" class="w-full">
264
+ </div>
265
+ <div>
266
+ <label class="text-gray-300 text-sm block mb-1">Bass Boost</label>
267
+ <input type="range" id="bassBoost" min="0" max="100" value="0" class="w-full">
268
+ </div>
269
+ </div>
270
+ </div>
271
+ </div>
272
+ </div>
273
+
274
+ <script>
275
+ document.addEventListener('DOMContentLoaded', () => {
276
+ // DOM elements
277
+ const canvas = document.getElementById('visualizer');
278
+ const ctx = canvas.getContext('2d');
279
+ const audioFileInput = document.getElementById('audioFile');
280
+ const audioInfo = document.getElementById('audioInfo');
281
+ const visualizerOverlay = document.getElementById('visualizerOverlay');
282
+ const playBtn = document.getElementById('playBtn');
283
+ const playIcon = document.getElementById('playIcon');
284
+ const prevBtn = document.getElementById('prevBtn');
285
+ const nextBtn = document.getElementById('nextBtn');
286
+ const recordBtn = document.getElementById('recordBtn');
287
+ const recordingIndicator = document.getElementById('recordingIndicator');
288
+ const progressBar = document.getElementById('progressBar');
289
+ const timeDisplay = document.getElementById('timeDisplay');
290
+ const presetButtons = document.querySelectorAll('.preset-btn');
291
+ const color1Input = document.getElementById('color1');
292
+ const color2Input = document.getElementById('color2');
293
+ const bgColorInput = document.getElementById('bgColor');
294
+ const volumeControl = document.getElementById('volumeControl');
295
+ const bassBoostControl = document.getElementById('bassBoost');
296
+
297
+ // Audio context and variables
298
+ let audioContext;
299
+ let analyser;
300
+ let audioSource;
301
+ let audioBuffer;
302
+ let audioElement;
303
+ let gainNode;
304
+ let biquadFilter;
305
+ let isPlaying = false;
306
+ let isRecording = false;
307
+ let mediaRecorder;
308
+ let recordedChunks = [];
309
+ let currentPreset = 'bars';
310
+ let animationId;
311
+ let audioFiles = [];
312
+ let currentFileIndex = 0;
313
+
314
+ // Resize canvas to fit container
315
+ function resizeCanvas() {
316
+ const container = canvas.parentElement;
317
+ canvas.width = container.clientWidth;
318
+ canvas.height = container.clientHeight;
319
+ }
320
+
321
+ // Initialize audio context
322
+ function initAudioContext() {
323
+ if (!audioContext) {
324
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
325
+ analyser = audioContext.createAnalyser();
326
+ analyser.fftSize = 2048;
327
+
328
+ gainNode = audioContext.createGain();
329
+ gainNode.gain.value = volumeControl.value;
330
+
331
+ biquadFilter = audioContext.createBiquadFilter();
332
+ biquadFilter.type = "lowshelf";
333
+ biquadFilter.frequency.value = 100;
334
+ biquadFilter.gain.value = 0;
335
+
336
+ volumeControl.addEventListener('input', () => {
337
+ gainNode.gain.value = volumeControl.value;
338
+ });
339
+
340
+ bassBoostControl.addEventListener('input', () => {
341
+ biquadFilter.gain.value = bassBoostControl.value;
342
+ });
343
+ }
344
+ }
345
+
346
+ // Load audio file
347
+ function loadAudioFile(file) {
348
+ if (!file) return;
349
+
350
+ initAudioContext();
351
+
352
+ if (audioElement) {
353
+ audioElement.pause();
354
+ audioElement = null;
355
+ }
356
+
357
+ const fileURL = URL.createObjectURL(file);
358
+ audioElement = new Audio(fileURL);
359
+
360
+ audioElement.addEventListener('loadedmetadata', () => {
361
+ audioInfo.textContent = `${file.name} • ${formatTime(audioElement.duration)}`;
362
+ updateTimeDisplay();
363
+ });
364
+
365
+ audioElement.addEventListener('timeupdate', () => {
366
+ updateTimeDisplay();
367
+ const progress = (audioElement.currentTime / audioElement.duration) * 100;
368
+ progressBar.style.width = `${progress}%`;
369
+ });
370
+
371
+ audioElement.addEventListener('ended', () => {
372
+ playIcon.className = 'fas fa-play';
373
+ isPlaying = false;
374
+ cancelAnimationFrame(animationId);
375
+ drawStaticVisualizer();
376
+ });
377
+
378
+ // Connect audio nodes
379
+ const source = audioContext.createMediaElementSource(audioElement);
380
+ source.connect(biquadFilter);
381
+ biquadFilter.connect(gainNode);
382
+ gainNode.connect(analyser);
383
+ analyser.connect(audioContext.destination);
384
+
385
+ audioSource = source;
386
+ audioBuffer = file;
387
+
388
+ // Hide overlay if audio is loaded
389
+ visualizerOverlay.classList.add('hidden');
390
+
391
+ // Start visualization
392
+ if (isPlaying) {
393
+ playAudio();
394
+ } else {
395
+ drawStaticVisualizer();
396
+ }
397
+ }
398
+
399
+ // Play/pause audio
400
+ function togglePlayPause() {
401
+ if (!audioElement) return;
402
+
403
+ if (isPlaying) {
404
+ pauseAudio();
405
+ } else {
406
+ playAudio();
407
+ }
408
+ }
409
+
410
+ function playAudio() {
411
+ if (audioContext.state === 'suspended') {
412
+ audioContext.resume();
413
+ }
414
+
415
+ audioElement.play();
416
+ isPlaying = true;
417
+ playIcon.className = 'fas fa-pause';
418
+ startVisualization();
419
+ }
420
+
421
+ function pauseAudio() {
422
+ audioElement.pause();
423
+ isPlaying = false;
424
+ playIcon.className = 'fas fa-play';
425
+ cancelAnimationFrame(animationId);
426
+ drawStaticVisualizer();
427
+ }
428
+
429
+ // Format time (seconds to MM:SS)
430
+ function formatTime(seconds) {
431
+ const mins = Math.floor(seconds / 60);
432
+ const secs = Math.floor(seconds % 60);
433
+ return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
434
+ }
435
+
436
+ // Update time display
437
+ function updateTimeDisplay() {
438
+ if (!audioElement) return;
439
+ timeDisplay.textContent = `${formatTime(audioElement.currentTime)} / ${formatTime(audioElement.duration)}`;
440
+ }
441
+
442
+ // Visualization presets
443
+ const visualizers = {
444
+ bars: function() {
445
+ const bufferLength = analyser.frequencyBinCount;
446
+ const dataArray = new Uint8Array(bufferLength);
447
+ analyser.getByteFrequencyData(dataArray);
448
+
449
+ ctx.fillStyle = bgColorInput.value;
450
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
451
+
452
+ const barWidth = (canvas.width / bufferLength) * 2.5;
453
+ let x = 0;
454
+
455
+ const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
456
+ gradient.addColorStop(0, color1Input.value);
457
+ gradient.addColorStop(1, color2Input.value);
458
+
459
+ for (let i = 0; i < bufferLength; i++) {
460
+ const barHeight = dataArray[i] / 2;
461
+
462
+ ctx.fillStyle = gradient;
463
+ ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
464
+
465
+ x += barWidth + 1;
466
+ }
467
+ },
468
+
469
+ wave: function() {
470
+ const bufferLength = analyser.frequencyBinCount;
471
+ const dataArray = new Uint8Array(bufferLength);
472
+ analyser.getByteTimeDomainData(dataArray);
473
+
474
+ ctx.fillStyle = bgColorInput.value;
475
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
476
+
477
+ ctx.lineWidth = 4;
478
+ ctx.strokeStyle = color1Input.value;
479
+ ctx.shadowBlur = 15;
480
+ ctx.shadowColor = color2Input.value;
481
+
482
+ ctx.beginPath();
483
+
484
+ const sliceWidth = canvas.width * 1.0 / bufferLength;
485
+ let x = 0;
486
+
487
+ for (let i = 0; i < bufferLength; i++) {
488
+ const v = dataArray[i] / 128.0;
489
+ const y = v * canvas.height / 2;
490
+
491
+ if (i === 0) {
492
+ ctx.moveTo(x, y);
493
+ } else {
494
+ ctx.lineTo(x, y);
495
+ }
496
+
497
+ x += sliceWidth;
498
+ }
499
+
500
+ ctx.lineTo(canvas.width, canvas.height / 2);
501
+ ctx.stroke();
502
+ },
503
+
504
+ particles: function() {
505
+ const bufferLength = analyser.frequencyBinCount;
506
+ const dataArray = new Uint8Array(bufferLength);
507
+ analyser.getByteFrequencyData(dataArray);
508
+
509
+ ctx.fillStyle = bgColorInput.value;
510
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
511
+
512
+ const particleCount = 150;
513
+ const particles = [];
514
+
515
+ for (let i = 0; i < particleCount; i++) {
516
+ particles.push({
517
+ x: Math.random() * canvas.width,
518
+ y: Math.random() * canvas.height,
519
+ size: Math.random() * 5 + 2,
520
+ speed: Math.random() * 2 + 1,
521
+ angle: Math.random() * Math.PI * 2
522
+ });
523
+ }
524
+
525
+ for (let i = 0; i < particles.length; i++) {
526
+ const p = particles[i];
527
+ const freqIndex = Math.floor((i / particleCount) * bufferLength);
528
+ const energy = dataArray[freqIndex] / 255;
529
+
530
+ // Update particle position
531
+ p.x += Math.cos(p.angle) * p.speed;
532
+ p.y += Math.sin(p.angle) * p.speed;
533
+
534
+ // Bounce off edges
535
+ if (p.x < 0 || p.x > canvas.width) p.angle = Math.PI - p.angle;
536
+ if (p.y < 0 || p.y > canvas.height) p.angle = -p.angle;
537
+
538
+ // Draw particle
539
+ const gradient = ctx.createRadialGradient(
540
+ p.x, p.y, 0,
541
+ p.x, p.y, p.size * (1 + energy)
542
+ );
543
+
544
+ gradient.addColorStop(0, color1Input.value);
545
+ gradient.addColorStop(1, color2Input.value);
546
+
547
+ ctx.fillStyle = gradient;
548
+ ctx.beginPath();
549
+ ctx.arc(p.x, p.y, p.size * (1 + energy * 2), 0, Math.PI * 2);
550
+ ctx.fill();
551
+ }
552
+ },
553
+
554
+ circle: function() {
555
+ const bufferLength = analyser.frequencyBinCount;
556
+ const dataArray = new Uint8Array(bufferLength);
557
+ analyser.getByteFrequencyData(dataArray);
558
+
559
+ ctx.fillStyle = bgColorInput.value;
560
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
561
+
562
+ const centerX = canvas.width / 2;
563
+ const centerY = canvas.height / 2;
564
+ const radius = Math.min(canvas.width, canvas.height) * 0.4;
565
+
566
+ ctx.lineWidth = 3;
567
+
568
+ // Create circular gradient
569
+ const gradient = ctx.createRadialGradient(
570
+ centerX, centerY, radius * 0.5,
571
+ centerX, centerY, radius * 1.5
572
+ );
573
+ gradient.addColorStop(0, color1Input.value);
574
+ gradient.addColorStop(1, color2Input.value);
575
+
576
+ ctx.strokeStyle = gradient;
577
+ ctx.shadowBlur = 20;
578
+ ctx.shadowColor = color2Input.value;
579
+
580
+ ctx.beginPath();
581
+
582
+ for (let i = 0; i < bufferLength; i++) {
583
+ const angle = (i / bufferLength) * Math.PI * 2;
584
+ const energy = dataArray[i] / 255;
585
+ const pointRadius = radius + (energy * radius * 0.5);
586
+
587
+ const x = centerX + Math.cos(angle) * pointRadius;
588
+ const y = centerY + Math.sin(angle) * pointRadius;
589
+
590
+ if (i === 0) {
591
+ ctx.moveTo(x, y);
592
+ } else {
593
+ ctx.lineTo(x, y);
594
+ }
595
+ }
596
+
597
+ ctx.closePath();
598
+ ctx.stroke();
599
+
600
+ // Draw center circle
601
+ const avgEnergy = dataArray.reduce((sum, val) => sum + val, 0) / bufferLength / 255;
602
+ ctx.fillStyle = color1Input.value;
603
+ ctx.beginPath();
604
+ ctx.arc(centerX, centerY, radius * 0.2 * (1 + avgEnergy * 0.5), 0, Math.PI * 2);
605
+ ctx.fill();
606
+ }
607
+ };
608
+
609
+ // Start visualization
610
+ function startVisualization() {
611
+ cancelAnimationFrame(animationId);
612
+
613
+ function visualize() {
614
+ visualizers[currentPreset]();
615
+ animationId = requestAnimationFrame(visualize);
616
+
617
+ // If recording, capture frame
618
+ if (isRecording) {
619
+ const stream = canvas.captureStream(30);
620
+ if (!mediaRecorder) {
621
+ mediaRecorder = new MediaRecorder(stream, { mimeType: 'video/webm' });
622
+ mediaRecorder.ondataavailable = (e) => {
623
+ if (e.data.size > 0) {
624
+ recordedChunks.push(e.data);
625
+ }
626
+ };
627
+ mediaRecorder.start(100); // Collect data every 100ms
628
+ }
629
+ }
630
+ }
631
+
632
+ visualize();
633
+ }
634
+
635
+ // Draw static visualizer (when paused)
636
+ function drawStaticVisualizer() {
637
+ ctx.fillStyle = bgColorInput.value;
638
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
639
+
640
+ ctx.fillStyle = color1Input.value;
641
+ ctx.font = '20px Arial';
642
+ ctx.textAlign = 'center';
643
+ ctx.fillText('Select an audio file to visualize', canvas.width / 2, canvas.height / 2);
644
+ }
645
+
646
+ // Toggle recording
647
+ function toggleRecording() {
648
+ if (isRecording) {
649
+ stopRecording();
650
+ } else {
651
+ startRecording();
652
+ }
653
+ }
654
+
655
+ // Start recording
656
+ function startRecording() {
657
+ if (!audioElement) {
658
+ alert('Please load an audio file first');
659
+ return;
660
+ }
661
+
662
+ recordedChunks = [];
663
+ isRecording = true;
664
+ recordingIndicator.style.display = 'flex';
665
+ recordBtn.innerHTML = '<i class="fas fa-stop"></i>';
666
+ recordBtn.title = 'Stop Recording';
667
+
668
+ // Start visualization if not already playing
669
+ if (!isPlaying) {
670
+ playAudio();
671
+ }
672
+ }
673
+
674
+ // Stop recording
675
+ function stopRecording() {
676
+ isRecording = false;
677
+ recordingIndicator.style.display = 'none';
678
+ recordBtn.innerHTML = '<i class="fas fa-video"></i>';
679
+ recordBtn.title = 'Record Video';
680
+
681
+ if (mediaRecorder) {
682
+ mediaRecorder.stop();
683
+ mediaRecorder = null;
684
+
685
+ // Create video blob and download
686
+ setTimeout(() => {
687
+ const blob = new Blob(recordedChunks, { type: 'video/webm' });
688
+ const url = URL.createObjectURL(blob);
689
+
690
+ const a = document.createElement('a');
691
+ a.style.display = 'none';
692
+ a.href = url;
693
+ a.download = `visualizer-${new Date().toISOString().slice(0, 10)}.webm`;
694
+ document.body.appendChild(a);
695
+ a.click();
696
+
697
+ setTimeout(() => {
698
+ document.body.removeChild(a);
699
+ URL.revokeObjectURL(url);
700
+ }, 100);
701
+ }, 500);
702
+ }
703
+ }
704
+
705
+ // Change visualization preset
706
+ function changePreset(preset) {
707
+ currentPreset = preset;
708
+
709
+ // Update active button
710
+ presetButtons.forEach(btn => {
711
+ if (btn.dataset.preset === preset) {
712
+ btn.classList.add('active');
713
+ } else {
714
+ btn.classList.remove('active');
715
+ }
716
+ });
717
+
718
+ // Restart visualization if playing
719
+ if (isPlaying) {
720
+ startVisualization();
721
+ } else {
722
+ drawStaticVisualizer();
723
+ }
724
+ }
725
+
726
+ // Event listeners
727
+ window.addEventListener('resize', () => {
728
+ resizeCanvas();
729
+ if (!isPlaying) drawStaticVisualizer();
730
+ });
731
+
732
+ audioFileInput.addEventListener('change', (e) => {
733
+ const file = e.target.files[0];
734
+ if (file) {
735
+ audioFiles = [file];
736
+ currentFileIndex = 0;
737
+ loadAudioFile(file);
738
+ }
739
+ });
740
+
741
+ playBtn.addEventListener('click', togglePlayPause);
742
+
743
+ prevBtn.addEventListener('click', () => {
744
+ if (audioFiles.length === 0) return;
745
+ currentFileIndex = (currentFileIndex - 1 + audioFiles.length) % audioFiles.length;
746
+ loadAudioFile(audioFiles[currentFileIndex]);
747
+ if (isPlaying) playAudio();
748
+ });
749
+
750
+ nextBtn.addEventListener('click', () => {
751
+ if (audioFiles.length === 0) return;
752
+ currentFileIndex = (currentFileIndex + 1) % audioFiles.length;
753
+ loadAudioFile(audioFiles[currentFileIndex]);
754
+ if (isPlaying) playAudio();
755
+ });
756
+
757
+ recordBtn.addEventListener('click', toggleRecording);
758
+
759
+ presetButtons.forEach(btn => {
760
+ btn.addEventListener('click', () => {
761
+ changePreset(btn.dataset.preset);
762
+ });
763
+ });
764
+
765
+ // Color change listeners
766
+ [color1Input, color2Input, bgColorInput].forEach(input => {
767
+ input.addEventListener('input', () => {
768
+ if (!isPlaying) drawStaticVisualizer();
769
+ });
770
+ });
771
+
772
+ // Drag and drop
773
+ canvas.addEventListener('dragover', (e) => {
774
+ e.preventDefault();
775
+ visualizerOverlay.style.backgroundColor = 'rgba(255, 255, 255, 0.2)';
776
+ });
777
+
778
+ canvas.addEventListener('dragleave', () => {
779
+ visualizerOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.3)';
780
+ });
781
+
782
+ canvas.addEventListener('drop', (e) => {
783
+ e.preventDefault();
784
+ visualizerOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.3)';
785
+
786
+ const file = e.dataTransfer.files[0];
787
+ if (file && file.type.includes('audio')) {
788
+ audioFiles = [file];
789
+ currentFileIndex = 0;
790
+ loadAudioFile(file);
791
+ }
792
+ });
793
+
794
+ // Initialize
795
+ resizeCanvas();
796
+ drawStaticVisualizer();
797
+ });
798
+ </script>
799
+ </body>
800
+ </html>