Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Music Visualizer with Video Export</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| .visualizer-container { | |
| position: relative; | |
| background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); | |
| border-radius: 12px; | |
| overflow: hidden; | |
| box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3); | |
| } | |
| .visualizer-canvas { | |
| width: 100%; | |
| height: 100%; | |
| display: block; | |
| } | |
| .recording-indicator { | |
| position: absolute; | |
| top: 20px; | |
| right: 20px; | |
| background-color: rgba(255, 0, 0, 0.7); | |
| color: white; | |
| padding: 8px 12px; | |
| border-radius: 20px; | |
| font-size: 14px; | |
| display: none; | |
| align-items: center; | |
| gap: 8px; | |
| animation: pulse 1.5s infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { opacity: 0.7; } | |
| 50% { opacity: 1; } | |
| 100% { opacity: 0.7; } | |
| } | |
| .visualizer-overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.3); | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| color: white; | |
| transition: all 0.3s ease; | |
| } | |
| .visualizer-overlay.hidden { | |
| opacity: 0; | |
| pointer-events: none; | |
| } | |
| .file-input-label { | |
| display: inline-block; | |
| padding: 12px 24px; | |
| background: linear-gradient(45deg, #4a00e0, #8e2de2); | |
| color: white; | |
| border-radius: 30px; | |
| cursor: pointer; | |
| font-weight: 600; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); | |
| } | |
| .file-input-label:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); | |
| } | |
| .file-input-label:active { | |
| transform: translateY(0); | |
| } | |
| .audio-info { | |
| margin-top: 20px; | |
| text-align: center; | |
| font-size: 18px; | |
| } | |
| .visualizer-controls { | |
| position: absolute; | |
| bottom: 20px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| display: flex; | |
| gap: 15px; | |
| z-index: 10; | |
| } | |
| .control-btn { | |
| width: 50px; | |
| height: 50px; | |
| border-radius: 50%; | |
| background: rgba(255, 255, 255, 0.2); | |
| backdrop-filter: blur(10px); | |
| border: none; | |
| color: white; | |
| font-size: 20px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| .control-btn:hover { | |
| background: rgba(255, 255, 255, 0.3); | |
| transform: scale(1.1); | |
| } | |
| .control-btn:active { | |
| transform: scale(0.95); | |
| } | |
| .control-btn.primary { | |
| background: linear-gradient(45deg, #4a00e0, #8e2de2); | |
| width: 60px; | |
| height: 60px; | |
| font-size: 24px; | |
| } | |
| .progress-container { | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 4px; | |
| background: rgba(255, 255, 255, 0.1); | |
| } | |
| .progress-bar { | |
| height: 100%; | |
| background: linear-gradient(90deg, #4a00e0, #8e2de2); | |
| width: 0%; | |
| transition: width 0.1s linear; | |
| } | |
| .time-display { | |
| position: absolute; | |
| bottom: 10px; | |
| right: 20px; | |
| color: white; | |
| font-size: 12px; | |
| opacity: 0.8; | |
| } | |
| .visualizer-presets { | |
| position: absolute; | |
| top: 20px; | |
| left: 20px; | |
| display: flex; | |
| gap: 10px; | |
| z-index: 10; | |
| } | |
| .preset-btn { | |
| padding: 8px 15px; | |
| background: rgba(255, 255, 255, 0.1); | |
| color: white; | |
| border: none; | |
| border-radius: 20px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| transition: all 0.3s ease; | |
| } | |
| .preset-btn:hover { | |
| background: rgba(255, 255, 255, 0.2); | |
| } | |
| .preset-btn.active { | |
| background: linear-gradient(45deg, #4a00e0, #8e2de2); | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-900 min-h-screen flex flex-col items-center justify-center p-4"> | |
| <div class="max-w-4xl w-full"> | |
| <h1 class="text-4xl font-bold text-center text-white mb-2">Audio Visualizer</h1> | |
| <p class="text-center text-gray-400 mb-8">Visualize your music and export as video</p> | |
| <div class="visualizer-container aspect-video w-full mb-6"> | |
| <canvas id="visualizer" class="visualizer-canvas"></canvas> | |
| <div class="recording-indicator" id="recordingIndicator"> | |
| <i class="fas fa-circle"></i> | |
| <span>Recording</span> | |
| </div> | |
| <div class="visualizer-overlay" id="visualizerOverlay"> | |
| <label for="audioFile" class="file-input-label"> | |
| <i class="fas fa-music mr-2"></i> | |
| Select Audio File | |
| </label> | |
| <input type="file" id="audioFile" accept="audio/*" class="hidden"> | |
| <div class="audio-info mt-4" id="audioInfo">No file selected</div> | |
| </div> | |
| <div class="visualizer-presets"> | |
| <button class="preset-btn active" data-preset="bars">Bars</button> | |
| <button class="preset-btn" data-preset="wave">Wave</button> | |
| <button class="preset-btn" data-preset="particles">Particles</button> | |
| <button class="preset-btn" data-preset="circle">Circle</button> | |
| </div> | |
| <div class="visualizer-controls"> | |
| <button class="control-btn" id="prevBtn" title="Previous"> | |
| <i class="fas fa-step-backward"></i> | |
| </button> | |
| <button class="control-btn primary" id="playBtn" title="Play/Pause"> | |
| <i class="fas fa-play" id="playIcon"></i> | |
| </button> | |
| <button class="control-btn" id="nextBtn" title="Next"> | |
| <i class="fas fa-step-forward"></i> | |
| </button> | |
| <button class="control-btn" id="recordBtn" title="Record Video"> | |
| <i class="fas fa-video"></i> | |
| </button> | |
| </div> | |
| <div class="progress-container"> | |
| <div class="progress-bar" id="progressBar"></div> | |
| </div> | |
| <div class="time-display" id="timeDisplay">0:00 / 0:00</div> | |
| </div> | |
| <div class="flex justify-center gap-4 mb-8"> | |
| <div class="bg-gray-800 p-4 rounded-lg w-full max-w-xs"> | |
| <h3 class="text-white font-medium mb-2">Visualization Settings</h3> | |
| <div class="space-y-3"> | |
| <div> | |
| <label class="text-gray-300 text-sm block mb-1">Color 1</label> | |
| <input type="color" id="color1" value="#4a00e0" class="w-full h-10"> | |
| </div> | |
| <div> | |
| <label class="text-gray-300 text-sm block mb-1">Color 2</label> | |
| <input type="color" id="color2" value="#8e2de2" class="w-full h-10"> | |
| </div> | |
| <div> | |
| <label class="text-gray-300 text-sm block mb-1">Background</label> | |
| <input type="color" id="bgColor" value="#16213e" class="w-full h-10"> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-gray-800 p-4 rounded-lg w-full max-w-xs"> | |
| <h3 class="text-white font-medium mb-2">Audio Settings</h3> | |
| <div class="space-y-3"> | |
| <div> | |
| <label class="text-gray-300 text-sm block mb-1">Volume</label> | |
| <input type="range" id="volumeControl" min="0" max="1" step="0.01" value="0.7" class="w-full"> | |
| </div> | |
| <div> | |
| <label class="text-gray-300 text-sm block mb-1">Bass Boost</label> | |
| <input type="range" id="bassBoost" min="0" max="100" value="0" class="w-full"> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // DOM elements | |
| const canvas = document.getElementById('visualizer'); | |
| const ctx = canvas.getContext('2d'); | |
| const audioFileInput = document.getElementById('audioFile'); | |
| const audioInfo = document.getElementById('audioInfo'); | |
| const visualizerOverlay = document.getElementById('visualizerOverlay'); | |
| const playBtn = document.getElementById('playBtn'); | |
| const playIcon = document.getElementById('playIcon'); | |
| const prevBtn = document.getElementById('prevBtn'); | |
| const nextBtn = document.getElementById('nextBtn'); | |
| const recordBtn = document.getElementById('recordBtn'); | |
| const recordingIndicator = document.getElementById('recordingIndicator'); | |
| const progressBar = document.getElementById('progressBar'); | |
| const timeDisplay = document.getElementById('timeDisplay'); | |
| const presetButtons = document.querySelectorAll('.preset-btn'); | |
| const color1Input = document.getElementById('color1'); | |
| const color2Input = document.getElementById('color2'); | |
| const bgColorInput = document.getElementById('bgColor'); | |
| const volumeControl = document.getElementById('volumeControl'); | |
| const bassBoostControl = document.getElementById('bassBoost'); | |
| // Audio context and variables | |
| let audioContext; | |
| let analyser; | |
| let audioSource; | |
| let audioBuffer; | |
| let audioElement; | |
| let gainNode; | |
| let biquadFilter; | |
| let isPlaying = false; | |
| let isRecording = false; | |
| let mediaRecorder; | |
| let recordedChunks = []; | |
| let currentPreset = 'bars'; | |
| let animationId; | |
| let audioFiles = []; | |
| let currentFileIndex = 0; | |
| // Resize canvas to fit container | |
| function resizeCanvas() { | |
| const container = canvas.parentElement; | |
| canvas.width = container.clientWidth; | |
| canvas.height = container.clientHeight; | |
| } | |
| // Initialize audio context | |
| function initAudioContext() { | |
| if (!audioContext) { | |
| audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| analyser = audioContext.createAnalyser(); | |
| analyser.fftSize = 2048; | |
| gainNode = audioContext.createGain(); | |
| gainNode.gain.value = volumeControl.value; | |
| biquadFilter = audioContext.createBiquadFilter(); | |
| biquadFilter.type = "lowshelf"; | |
| biquadFilter.frequency.value = 100; | |
| biquadFilter.gain.value = 0; | |
| volumeControl.addEventListener('input', () => { | |
| gainNode.gain.value = volumeControl.value; | |
| }); | |
| bassBoostControl.addEventListener('input', () => { | |
| biquadFilter.gain.value = bassBoostControl.value; | |
| }); | |
| } | |
| } | |
| // Load audio file | |
| function loadAudioFile(file) { | |
| if (!file) return; | |
| initAudioContext(); | |
| if (audioElement) { | |
| audioElement.pause(); | |
| audioElement = null; | |
| } | |
| const fileURL = URL.createObjectURL(file); | |
| audioElement = new Audio(fileURL); | |
| audioElement.addEventListener('loadedmetadata', () => { | |
| audioInfo.textContent = `${file.name} • ${formatTime(audioElement.duration)}`; | |
| updateTimeDisplay(); | |
| }); | |
| audioElement.addEventListener('timeupdate', () => { | |
| updateTimeDisplay(); | |
| const progress = (audioElement.currentTime / audioElement.duration) * 100; | |
| progressBar.style.width = `${progress}%`; | |
| }); | |
| audioElement.addEventListener('ended', () => { | |
| playIcon.className = 'fas fa-play'; | |
| isPlaying = false; | |
| cancelAnimationFrame(animationId); | |
| drawStaticVisualizer(); | |
| }); | |
| // Connect audio nodes | |
| const source = audioContext.createMediaElementSource(audioElement); | |
| source.connect(biquadFilter); | |
| biquadFilter.connect(gainNode); | |
| gainNode.connect(analyser); | |
| analyser.connect(audioContext.destination); | |
| audioSource = source; | |
| audioBuffer = file; | |
| // Hide overlay if audio is loaded | |
| visualizerOverlay.classList.add('hidden'); | |
| // Start visualization | |
| if (isPlaying) { | |
| playAudio(); | |
| } else { | |
| drawStaticVisualizer(); | |
| } | |
| } | |
| // Play/pause audio | |
| function togglePlayPause() { | |
| if (!audioElement) return; | |
| if (isPlaying) { | |
| pauseAudio(); | |
| } else { | |
| playAudio(); | |
| } | |
| } | |
| function playAudio() { | |
| if (audioContext.state === 'suspended') { | |
| audioContext.resume(); | |
| } | |
| audioElement.play(); | |
| isPlaying = true; | |
| playIcon.className = 'fas fa-pause'; | |
| startVisualization(); | |
| } | |
| function pauseAudio() { | |
| audioElement.pause(); | |
| isPlaying = false; | |
| playIcon.className = 'fas fa-play'; | |
| cancelAnimationFrame(animationId); | |
| drawStaticVisualizer(); | |
| } | |
| // Format time (seconds to MM:SS) | |
| function formatTime(seconds) { | |
| const mins = Math.floor(seconds / 60); | |
| const secs = Math.floor(seconds % 60); | |
| return `${mins}:${secs < 10 ? '0' : ''}${secs}`; | |
| } | |
| // Update time display | |
| function updateTimeDisplay() { | |
| if (!audioElement) return; | |
| timeDisplay.textContent = `${formatTime(audioElement.currentTime)} / ${formatTime(audioElement.duration)}`; | |
| } | |
| // Visualization presets | |
| const visualizers = { | |
| bars: function() { | |
| const bufferLength = analyser.frequencyBinCount; | |
| const dataArray = new Uint8Array(bufferLength); | |
| analyser.getByteFrequencyData(dataArray); | |
| ctx.fillStyle = bgColorInput.value; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| const barWidth = (canvas.width / bufferLength) * 2.5; | |
| let x = 0; | |
| const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height); | |
| gradient.addColorStop(0, color1Input.value); | |
| gradient.addColorStop(1, color2Input.value); | |
| for (let i = 0; i < bufferLength; i++) { | |
| const barHeight = dataArray[i] / 2; | |
| ctx.fillStyle = gradient; | |
| ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight); | |
| x += barWidth + 1; | |
| } | |
| }, | |
| wave: function() { | |
| const bufferLength = analyser.frequencyBinCount; | |
| const dataArray = new Uint8Array(bufferLength); | |
| analyser.getByteTimeDomainData(dataArray); | |
| ctx.fillStyle = bgColorInput.value; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| ctx.lineWidth = 4; | |
| ctx.strokeStyle = color1Input.value; | |
| ctx.shadowBlur = 15; | |
| ctx.shadowColor = color2Input.value; | |
| ctx.beginPath(); | |
| const sliceWidth = canvas.width * 1.0 / bufferLength; | |
| let x = 0; | |
| for (let i = 0; i < bufferLength; i++) { | |
| const v = dataArray[i] / 128.0; | |
| const y = v * canvas.height / 2; | |
| if (i === 0) { | |
| ctx.moveTo(x, y); | |
| } else { | |
| ctx.lineTo(x, y); | |
| } | |
| x += sliceWidth; | |
| } | |
| ctx.lineTo(canvas.width, canvas.height / 2); | |
| ctx.stroke(); | |
| }, | |
| particles: function() { | |
| const bufferLength = analyser.frequencyBinCount; | |
| const dataArray = new Uint8Array(bufferLength); | |
| analyser.getByteFrequencyData(dataArray); | |
| ctx.fillStyle = bgColorInput.value; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| const particleCount = 150; | |
| const particles = []; | |
| for (let i = 0; i < particleCount; i++) { | |
| particles.push({ | |
| x: Math.random() * canvas.width, | |
| y: Math.random() * canvas.height, | |
| size: Math.random() * 5 + 2, | |
| speed: Math.random() * 2 + 1, | |
| angle: Math.random() * Math.PI * 2 | |
| }); | |
| } | |
| for (let i = 0; i < particles.length; i++) { | |
| const p = particles[i]; | |
| const freqIndex = Math.floor((i / particleCount) * bufferLength); | |
| const energy = dataArray[freqIndex] / 255; | |
| // Update particle position | |
| p.x += Math.cos(p.angle) * p.speed; | |
| p.y += Math.sin(p.angle) * p.speed; | |
| // Bounce off edges | |
| if (p.x < 0 || p.x > canvas.width) p.angle = Math.PI - p.angle; | |
| if (p.y < 0 || p.y > canvas.height) p.angle = -p.angle; | |
| // Draw particle | |
| const gradient = ctx.createRadialGradient( | |
| p.x, p.y, 0, | |
| p.x, p.y, p.size * (1 + energy) | |
| ); | |
| gradient.addColorStop(0, color1Input.value); | |
| gradient.addColorStop(1, color2Input.value); | |
| ctx.fillStyle = gradient; | |
| ctx.beginPath(); | |
| ctx.arc(p.x, p.y, p.size * (1 + energy * 2), 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| }, | |
| circle: function() { | |
| const bufferLength = analyser.frequencyBinCount; | |
| const dataArray = new Uint8Array(bufferLength); | |
| analyser.getByteFrequencyData(dataArray); | |
| ctx.fillStyle = bgColorInput.value; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| const centerX = canvas.width / 2; | |
| const centerY = canvas.height / 2; | |
| const radius = Math.min(canvas.width, canvas.height) * 0.4; | |
| ctx.lineWidth = 3; | |
| // Create circular gradient | |
| const gradient = ctx.createRadialGradient( | |
| centerX, centerY, radius * 0.5, | |
| centerX, centerY, radius * 1.5 | |
| ); | |
| gradient.addColorStop(0, color1Input.value); | |
| gradient.addColorStop(1, color2Input.value); | |
| ctx.strokeStyle = gradient; | |
| ctx.shadowBlur = 20; | |
| ctx.shadowColor = color2Input.value; | |
| ctx.beginPath(); | |
| for (let i = 0; i < bufferLength; i++) { | |
| const angle = (i / bufferLength) * Math.PI * 2; | |
| const energy = dataArray[i] / 255; | |
| const pointRadius = radius + (energy * radius * 0.5); | |
| const x = centerX + Math.cos(angle) * pointRadius; | |
| const y = centerY + Math.sin(angle) * pointRadius; | |
| if (i === 0) { | |
| ctx.moveTo(x, y); | |
| } else { | |
| ctx.lineTo(x, y); | |
| } | |
| } | |
| ctx.closePath(); | |
| ctx.stroke(); | |
| // Draw center circle | |
| const avgEnergy = dataArray.reduce((sum, val) => sum + val, 0) / bufferLength / 255; | |
| ctx.fillStyle = color1Input.value; | |
| ctx.beginPath(); | |
| ctx.arc(centerX, centerY, radius * 0.2 * (1 + avgEnergy * 0.5), 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| }; | |
| // Start visualization | |
| function startVisualization() { | |
| cancelAnimationFrame(animationId); | |
| function visualize() { | |
| visualizers[currentPreset](); | |
| animationId = requestAnimationFrame(visualize); | |
| // If recording, capture frame | |
| if (isRecording) { | |
| const stream = canvas.captureStream(30); | |
| if (!mediaRecorder) { | |
| mediaRecorder = new MediaRecorder(stream, { mimeType: 'video/webm' }); | |
| mediaRecorder.ondataavailable = (e) => { | |
| if (e.data.size > 0) { | |
| recordedChunks.push(e.data); | |
| } | |
| }; | |
| mediaRecorder.start(100); // Collect data every 100ms | |
| } | |
| } | |
| } | |
| visualize(); | |
| } | |
| // Draw static visualizer (when paused) | |
| function drawStaticVisualizer() { | |
| ctx.fillStyle = bgColorInput.value; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| ctx.fillStyle = color1Input.value; | |
| ctx.font = '20px Arial'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText('Select an audio file to visualize', canvas.width / 2, canvas.height / 2); | |
| } | |
| // Toggle recording | |
| function toggleRecording() { | |
| if (isRecording) { | |
| stopRecording(); | |
| } else { | |
| startRecording(); | |
| } | |
| } | |
| // Start recording | |
| function startRecording() { | |
| if (!audioElement) { | |
| alert('Please load an audio file first'); | |
| return; | |
| } | |
| recordedChunks = []; | |
| isRecording = true; | |
| recordingIndicator.style.display = 'flex'; | |
| recordBtn.innerHTML = '<i class="fas fa-stop"></i>'; | |
| recordBtn.title = 'Stop Recording'; | |
| // Start visualization if not already playing | |
| if (!isPlaying) { | |
| playAudio(); | |
| } | |
| } | |
| // Stop recording | |
| function stopRecording() { | |
| isRecording = false; | |
| recordingIndicator.style.display = 'none'; | |
| recordBtn.innerHTML = '<i class="fas fa-video"></i>'; | |
| recordBtn.title = 'Record Video'; | |
| if (mediaRecorder) { | |
| mediaRecorder.stop(); | |
| mediaRecorder = null; | |
| // Create video blob and download | |
| setTimeout(() => { | |
| const blob = new Blob(recordedChunks, { type: 'video/webm' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.style.display = 'none'; | |
| a.href = url; | |
| a.download = `visualizer-${new Date().toISOString().slice(0, 10)}.webm`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| setTimeout(() => { | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| }, 100); | |
| }, 500); | |
| } | |
| } | |
| // Change visualization preset | |
| function changePreset(preset) { | |
| currentPreset = preset; | |
| // Update active button | |
| presetButtons.forEach(btn => { | |
| if (btn.dataset.preset === preset) { | |
| btn.classList.add('active'); | |
| } else { | |
| btn.classList.remove('active'); | |
| } | |
| }); | |
| // Restart visualization if playing | |
| if (isPlaying) { | |
| startVisualization(); | |
| } else { | |
| drawStaticVisualizer(); | |
| } | |
| } | |
| // Event listeners | |
| window.addEventListener('resize', () => { | |
| resizeCanvas(); | |
| if (!isPlaying) drawStaticVisualizer(); | |
| }); | |
| audioFileInput.addEventListener('change', (e) => { | |
| const file = e.target.files[0]; | |
| if (file) { | |
| audioFiles = [file]; | |
| currentFileIndex = 0; | |
| loadAudioFile(file); | |
| } | |
| }); | |
| playBtn.addEventListener('click', togglePlayPause); | |
| prevBtn.addEventListener('click', () => { | |
| if (audioFiles.length === 0) return; | |
| currentFileIndex = (currentFileIndex - 1 + audioFiles.length) % audioFiles.length; | |
| loadAudioFile(audioFiles[currentFileIndex]); | |
| if (isPlaying) playAudio(); | |
| }); | |
| nextBtn.addEventListener('click', () => { | |
| if (audioFiles.length === 0) return; | |
| currentFileIndex = (currentFileIndex + 1) % audioFiles.length; | |
| loadAudioFile(audioFiles[currentFileIndex]); | |
| if (isPlaying) playAudio(); | |
| }); | |
| recordBtn.addEventListener('click', toggleRecording); | |
| presetButtons.forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| changePreset(btn.dataset.preset); | |
| }); | |
| }); | |
| // Color change listeners | |
| [color1Input, color2Input, bgColorInput].forEach(input => { | |
| input.addEventListener('input', () => { | |
| if (!isPlaying) drawStaticVisualizer(); | |
| }); | |
| }); | |
| // Drag and drop | |
| canvas.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| visualizerOverlay.style.backgroundColor = 'rgba(255, 255, 255, 0.2)'; | |
| }); | |
| canvas.addEventListener('dragleave', () => { | |
| visualizerOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.3)'; | |
| }); | |
| canvas.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| visualizerOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.3)'; | |
| const file = e.dataTransfer.files[0]; | |
| if (file && file.type.includes('audio')) { | |
| audioFiles = [file]; | |
| currentFileIndex = 0; | |
| loadAudioFile(file); | |
| } | |
| }); | |
| // Initialize | |
| resizeCanvas(); | |
| drawStaticVisualizer(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |