| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Audio Reactive Visualizer</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://cdn.jsdelivr.net/npm/p5@1.5.0/lib/p5.min.js"></script> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/RecordRTC/5.6.2/RecordRTC.min.js"></script> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| <style> |
| .custom-scrollbar::-webkit-scrollbar { |
| width: 8px; |
| height: 8px; |
| } |
| .custom-scrollbar::-webkit-scrollbar-track { |
| background: #1e293b; |
| } |
| .custom-scrollbar::-webkit-scrollbar-thumb { |
| background: #475569; |
| border-radius: 4px; |
| } |
| .custom-scrollbar::-webkit-scrollbar-thumb:hover { |
| background: #64748b; |
| } |
| .gradient-bg { |
| background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); |
| } |
| .glow { |
| box-shadow: 0 0 15px rgba(59, 130, 246, 0.5); |
| } |
| .glow:hover { |
| box-shadow: 0 0 20px rgba(59, 130, 246, 0.8); |
| } |
| canvas { |
| display: block; |
| border-radius: 8px; |
| } |
| .pattern-preview { |
| width: 100%; |
| height: 80px; |
| border-radius: 6px; |
| cursor: pointer; |
| transition: all 0.2s; |
| } |
| .pattern-preview:hover { |
| transform: scale(1.02); |
| } |
| .slider-thumb::-webkit-slider-thumb { |
| -webkit-appearance: none; |
| width: 18px; |
| height: 18px; |
| border-radius: 50%; |
| background: #3b82f6; |
| cursor: pointer; |
| } |
| .slider-thumb:focus::-webkit-slider-thumb { |
| box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3); |
| } |
| </style> |
| </head> |
| <body class="gradient-bg text-slate-100 min-h-screen"> |
| <div class="container mx-auto px-4 py-8"> |
| <header class="mb-8 text-center"> |
| <h1 class="text-4xl font-bold mb-2 bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-purple-500">Audio Reactive Visualizer</h1> |
| <p class="text-slate-300 max-w-2xl mx-auto">Create stunning audio-reactive visualizations with customizable patterns and real-time audio analysis. Import your music and export your creations.</p> |
| </header> |
|
|
| <div class="grid grid-cols-1 lg:grid-cols-4 gap-6"> |
| |
| <div class="lg:col-span-1 bg-slate-800/50 rounded-xl p-6 shadow-lg border border-slate-700/50 custom-scrollbar overflow-y-auto max-h-screen"> |
| <h2 class="text-xl font-semibold mb-4 flex items-center gap-2"> |
| <i class="fas fa-sliders-h"></i> Controls |
| </h2> |
| |
| |
| <div class="mb-6"> |
| <h3 class="text-sm font-medium mb-2 text-slate-300 flex items-center gap-2"> |
| <i class="fas fa-music"></i> Audio Source |
| </h3> |
| <div class="flex flex-col gap-2"> |
| <button id="fileInputBtn" class="bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-lg transition flex items-center justify-center gap-2 glow"> |
| <i class="fas fa-file-audio"></i> Import Audio |
| </button> |
| <input type="file" id="fileInput" accept="audio/*" class="hidden"> |
| <button id="micInputBtn" class="bg-purple-600 hover:bg-purple-700 text-white py-2 px-4 rounded-lg transition flex items-center justify-center gap-2 glow"> |
| <i class="fas fa-microphone"></i> Use Microphone |
| </button> |
| </div> |
| <div class="mt-3"> |
| <div class="flex justify-between text-xs text-slate-400"> |
| <span>Volume</span> |
| <span id="volumeValue">50%</span> |
| </div> |
| <input type="range" id="volumeSlider" min="0" max="100" value="50" class="w-full slider-thumb"> |
| </div> |
| </div> |
| |
| |
| <div class="mb-6"> |
| <h3 class="text-sm font-medium mb-2 text-slate-300 flex items-center gap-2"> |
| <i class="fas fa-shapes"></i> Visual Pattern |
| </h3> |
| <div class="grid grid-cols-2 gap-3"> |
| <div class="pattern-option" data-pattern="waveform"> |
| <div class="pattern-preview bg-gradient-to-r from-blue-500 to-purple-600"></div> |
| <p class="text-xs text-center mt-1">Waveform</p> |
| </div> |
| <div class="pattern-option" data-pattern="particles"> |
| <div class="pattern-preview bg-gradient-to-r from-green-500 to-teal-600"></div> |
| <p class="text-xs text-center mt-1">Particles</p> |
| </div> |
| <div class="pattern-option" data-pattern="rings"> |
| <div class="pattern-preview bg-gradient-to-r from-orange-500 to-pink-600"></div> |
| <p class="text-xs text-center mt-1">Concentric</p> |
| </div> |
| <div class="pattern-option" data-pattern="bars"> |
| <div class="pattern-preview bg-gradient-to-r from-yellow-500 to-red-600"></div> |
| <p class="text-xs text-center mt-1">Frequency</p> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="mb-6"> |
| <h3 class="text-sm font-medium mb-2 text-slate-300 flex items-center gap-2"> |
| <i class="fas fa-palette"></i> Colors |
| </h3> |
| <div class="grid grid-cols-2 gap-3"> |
| <div> |
| <label class="text-xs text-slate-400 block mb-1">Primary</label> |
| <input type="color" id="color1" value="#3b82f6" class="w-full h-8 rounded cursor-pointer"> |
| </div> |
| <div> |
| <label class="text-xs text-slate-400 block mb-1">Secondary</label> |
| <input type="color" id="color2" value="#8b5cf6" class="w-full h-8 rounded cursor-pointer"> |
| </div> |
| </div> |
| <div class="mt-3"> |
| <div class="flex justify-between text-xs text-slate-400"> |
| <span>Background Opacity</span> |
| <span id="bgOpacityValue">20%</span> |
| </div> |
| <input type="range" id="bgOpacitySlider" min="0" max="100" value="20" class="w-full slider-thumb"> |
| </div> |
| </div> |
| |
| |
| <div class="mb-6"> |
| <h3 class="text-sm font-medium mb-2 text-slate-300 flex items-center gap-2"> |
| <i class="fas fa-cog"></i> Pattern Settings |
| </h3> |
| <div id="waveformSettings"> |
| <div class="mb-2"> |
| <div class="flex justify-between text-xs text-slate-400"> |
| <span>Line Thickness</span> |
| <span id="waveThicknessValue">2</span> |
| </div> |
| <input type="range" id="waveThicknessSlider" min="1" max="10" value="2" class="w-full slider-thumb"> |
| </div> |
| <div class="mb-2"> |
| <div class="flex justify-between text-xs text-slate-400"> |
| <span>Smoothness</span> |
| <span id="waveSmoothnessValue">50%</span> |
| </div> |
| <input type="range" id="waveSmoothnessSlider" min="0" max="100" value="50" class="w-full slider-thumb"> |
| </div> |
| </div> |
| <div id="particlesSettings" class="hidden"> |
| <div class="mb-2"> |
| <div class="flex justify-between text-xs text-slate-400"> |
| <span>Particle Count</span> |
| <span id="particleCountValue">150</span> |
| </div> |
| <input type="range" id="particleCountSlider" min="50" max="500" value="150" class="w-full slider-thumb"> |
| </div> |
| <div class="mb-2"> |
| <div class="flex justify-between text-xs text-slate-400"> |
| <span>Particle Size</span> |
| <span id="particleSizeValue">3</span> |
| </div> |
| <input type="range" id="particleSizeSlider" min="1" max="10" value="3" class="w-full slider-thumb"> |
| </div> |
| </div> |
| <div id="ringsSettings" class="hidden"> |
| <div class="mb-2"> |
| <div class="flex justify-between text-xs text-slate-400"> |
| <span>Ring Count</span> |
| <span id="ringCountValue">8</span> |
| </div> |
| <input type="range" id="ringCountSlider" min="3" max="20" value="8" class="w-full slider-thumb"> |
| </div> |
| <div class="mb-2"> |
| <div class="flex justify-between text-xs text-slate-400"> |
| <span>Ring Spacing</span> |
| <span id="ringSpacingValue">30</span> |
| </div> |
| <input type="range" id="ringSpacingSlider" min="10" max="100" value="30" class="w-full slider-thumb"> |
| </div> |
| </div> |
| <div id="barsSettings" class="hidden"> |
| <div class="mb-2"> |
| <div class="flex justify-between text-xs text-slate-400"> |
| <span>Bar Count</span> |
| <span id="barCountValue">64</span> |
| </div> |
| <input type="range" id="barCountSlider" min="16" max="128" value="64" class="w-full slider-thumb"> |
| </div> |
| <div class="mb-2"> |
| <div class="flex justify-between text-xs text-slate-400"> |
| <span>Bar Width</span> |
| <span id="barWidthValue">8</span> |
| </div> |
| <input type="range" id="barWidthSlider" min="2" max="20" value="8" class="w-full slider-thumb"> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div> |
| <h3 class="text-sm font-medium mb-2 text-slate-300 flex items-center gap-2"> |
| <i class="fas fa-download"></i> Export |
| </h3> |
| <div class="flex flex-col gap-2"> |
| <button id="exportImageBtn" class="bg-emerald-600 hover:bg-emerald-700 text-white py-2 px-4 rounded-lg transition flex items-center justify-center gap-2 glow"> |
| <i class="fas fa-image"></i> Save Image |
| </button> |
| <button id="exportVideoBtn" class="bg-rose-600 hover:bg-rose-700 text-white py-2 px-4 rounded-lg transition flex items-center justify-center gap-2 glow"> |
| <i class="fas fa-video"></i> Record Video |
| </button> |
| </div> |
| <div id="recordingControls" class="hidden mt-3"> |
| <div class="flex justify-center gap-2"> |
| <button id="stopRecordingBtn" class="bg-rose-600 hover:bg-rose-700 text-white py-1 px-3 rounded-lg text-sm flex items-center gap-1"> |
| <i class="fas fa-stop"></i> Stop |
| </button> |
| <div id="recordingTimer" class="text-sm text-slate-300 flex items-center"> |
| <i class="fas fa-circle text-rose-500 mr-1 animate-pulse"></i> 00:00 |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="lg:col-span-3"> |
| <div id="visualizer-container" class="bg-slate-900/50 rounded-xl p-4 shadow-lg border border-slate-700/50"> |
| <div id="canvas-container" class="flex items-center justify-center"> |
| |
| </div> |
| <div class="mt-4 flex justify-between items-center"> |
| <div class="flex items-center gap-2"> |
| <button id="playBtn" class="bg-blue-600 hover:bg-blue-700 text-white p-2 rounded-full w-10 h-10 flex items-center justify-center glow"> |
| <i class="fas fa-play"></i> |
| </button> |
| <button id="pauseBtn" class="bg-slate-700 hover:bg-slate-600 text-white p-2 rounded-full w-10 h-10 flex items-center justify-center"> |
| <i class="fas fa-pause"></i> |
| </button> |
| <button id="stopBtn" class="bg-slate-700 hover:bg-slate-600 text-white p-2 rounded-full w-10 h-10 flex items-center justify-center"> |
| <i class="fas fa-stop"></i> |
| </button> |
| </div> |
| <div id="songInfo" class="text-sm text-slate-400 italic"> |
| No audio loaded |
| </div> |
| <div id="audioTime" class="text-sm text-slate-300"> |
| 0:00 / 0:00 |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="mt-6 bg-slate-800/50 rounded-xl p-4 shadow-lg border border-slate-700/50"> |
| <h3 class="text-sm font-medium mb-2 text-slate-300 flex items-center gap-2"> |
| <i class="fas fa-chart-bar"></i> Audio Spectrum |
| </h3> |
| <div id="spectrum-container" class="h-24"> |
| |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| |
| let audioContext, analyser, dataArray, source, audioElement; |
| let isPlaying = false; |
| let currentPattern = 'waveform'; |
| let color1 = '#3b82f6', color2 = '#8b5cf6'; |
| let bgOpacity = 20; |
| let waveThickness = 2, waveSmoothness = 50; |
| let particleCount = 150, particleSize = 3; |
| let ringCount = 8, ringSpacing = 30; |
| let barCount = 64, barWidth = 8; |
| let mediaRecorder, recordedChunks = [], recordingStartTime; |
| let recordingInterval; |
| |
| |
| const patternSettings = { |
| waveform: { |
| thickness: 2, |
| smoothness: 50 |
| }, |
| particles: { |
| count: 150, |
| size: 3 |
| }, |
| rings: { |
| count: 8, |
| spacing: 30 |
| }, |
| bars: { |
| count: 64, |
| width: 8 |
| } |
| }; |
| |
| |
| new p5(function(p) { |
| let canvas, spectrumCanvas; |
| let fft, waveform; |
| let particles = []; |
| |
| p.setup = function() { |
| |
| canvas = p.createCanvas(800, 450); |
| canvas.parent('canvas-container'); |
| p.colorMode(p.HSB, 360, 100, 100, 1); |
| |
| |
| spectrumCanvas = p.createCanvas(800, 100); |
| spectrumCanvas.parent('spectrum-container'); |
| |
| |
| fft = new p5.FFT(); |
| |
| |
| if (!audioContext) { |
| audioContext = new (window.AudioContext || window.webkitAudioContext)(); |
| analyser = audioContext.createAnalyser(); |
| analyser.fftSize = 2048; |
| const bufferLength = analyser.frequencyBinCount; |
| dataArray = new Uint8Array(bufferLength); |
| } |
| |
| |
| initParticles(); |
| }; |
| |
| p.draw = function() { |
| |
| drawVisualizer(); |
| |
| |
| drawSpectrum(); |
| |
| |
| updateAudioTime(); |
| }; |
| |
| function drawVisualizer() { |
| |
| waveform = fft.waveform(); |
| |
| |
| p.background(10, bgOpacity); |
| |
| |
| switch(currentPattern) { |
| case 'waveform': |
| drawWaveform(); |
| break; |
| case 'particles': |
| drawParticles(); |
| break; |
| case 'rings': |
| drawRings(); |
| break; |
| case 'bars': |
| drawBars(); |
| break; |
| } |
| } |
| |
| function drawWaveform() { |
| p.noFill(); |
| p.strokeWeight(waveThickness); |
| |
| |
| const gradient = p.drawingContext.createLinearGradient(0, 0, p.width, 0); |
| gradient.addColorStop(0, color1); |
| gradient.addColorStop(1, color2); |
| p.drawingContext.strokeStyle = gradient; |
| |
| p.beginShape(); |
| for (let i = 0; i < waveform.length; i++) { |
| |
| const x = p.map(i, 0, waveform.length, 0, p.width); |
| const y = p.map(waveform[i], -1, 1, p.height, 0); |
| |
| |
| if (i === 0) { |
| p.vertex(x, y); |
| } else { |
| const prevY = p.map(waveform[i-1], -1, 1, p.height, 0); |
| const smoothY = p.lerp(prevY, y, waveSmoothness / 100); |
| p.vertex(x, smoothY); |
| } |
| } |
| p.endShape(); |
| } |
| |
| function initParticles() { |
| particles = []; |
| for (let i = 0; i < particleCount; i++) { |
| particles.push({ |
| x: p.random(p.width), |
| y: p.random(p.height), |
| vx: p.random(-1, 1), |
| vy: p.random(-1, 1), |
| size: particleSize, |
| color: p.lerpColor(p.color(color1), p.color(color2), p.random(1)) |
| }); |
| } |
| } |
| |
| function drawParticles() { |
| const spectrum = fft.analyze(); |
| |
| for (let i = 0; i < particles.length; i++) { |
| const particle = particles[i]; |
| |
| |
| const audioImpact = spectrum[Math.floor(p.map(i, 0, particles.length, 0, spectrum.length))] / 255; |
| |
| |
| particle.x += particle.vx * (1 + audioImpact * 2); |
| particle.y += particle.vy * (1 + audioImpact * 2); |
| |
| |
| if (particle.x < 0) particle.x = p.width; |
| if (particle.x > p.width) particle.x = 0; |
| if (particle.y < 0) particle.y = p.height; |
| if (particle.y > p.height) particle.y = 0; |
| |
| |
| p.noStroke(); |
| p.fill(particle.color); |
| p.circle(particle.x, particle.y, particle.size * (1 + audioImpact * 2)); |
| } |
| } |
| |
| function drawRings() { |
| const spectrum = fft.analyze(); |
| const centerX = p.width / 2; |
| const centerY = p.height / 2; |
| |
| for (let i = 0; i < ringCount; i++) { |
| const radius = (i + 1) * ringSpacing; |
| const energy = spectrum[Math.floor(p.map(i, 0, ringCount, 0, spectrum.length))] / 255; |
| |
| |
| const gradient = p.drawingContext.createRadialGradient( |
| centerX, centerY, radius - 5, |
| centerX, centerY, radius + 5 |
| ); |
| gradient.addColorStop(0, color1); |
| gradient.addColorStop(1, color2); |
| p.drawingContext.strokeStyle = gradient; |
| |
| p.strokeWeight(5); |
| p.noFill(); |
| p.circle(centerX, centerY, radius * (1 + energy * 0.5)); |
| } |
| } |
| |
| function drawBars() { |
| const spectrum = fft.analyze(); |
| const binSize = Math.floor(spectrum.length / barCount); |
| |
| for (let i = 0; i < barCount; i++) { |
| let sum = 0; |
| for (let j = 0; j < binSize; j++) { |
| sum += spectrum[i * binSize + j]; |
| } |
| const avg = sum / binSize; |
| const energy = avg / 255; |
| |
| const x = p.map(i, 0, barCount, 0, p.width); |
| const h = p.map(energy, 0, 1, 0, p.height * 0.8); |
| |
| |
| const gradient = p.drawingContext.createLinearGradient(0, p.height - h, 0, p.height); |
| gradient.addColorStop(0, color1); |
| gradient.addColorStop(1, color2); |
| p.drawingContext.fillStyle = gradient; |
| |
| p.noStroke(); |
| p.rect(x, p.height - h, barWidth, h); |
| } |
| } |
| |
| function drawSpectrum() { |
| const spectrum = fft.analyze(); |
| p.background(15); |
| |
| |
| for (let i = 0; i < spectrum.length; i++) { |
| const x = p.map(i, 0, spectrum.length, 0, p.width); |
| const h = -p.height + p.map(spectrum[i], 0, 255, p.height, 0); |
| |
| |
| const gradient = p.drawingContext.createLinearGradient(0, 0, 0, p.height); |
| gradient.addColorStop(0, color1); |
| gradient.addColorStop(1, color2); |
| p.drawingContext.fillStyle = gradient; |
| |
| p.noStroke(); |
| p.rect(x, p.height, p.width / spectrum.length, h); |
| } |
| } |
| |
| function updateAudioTime() { |
| if (audioElement && !isNaN(audioElement.duration)) { |
| const currentTime = audioElement.currentTime || 0; |
| const duration = audioElement.duration; |
| |
| const currentMinutes = Math.floor(currentTime / 60); |
| const currentSeconds = Math.floor(currentTime % 60).toString().padStart(2, '0'); |
| const durationMinutes = Math.floor(duration / 60); |
| const durationSeconds = Math.floor(duration % 60).toString().padStart(2, '0'); |
| |
| document.getElementById('audioTime').textContent = |
| `${currentMinutes}:${currentSeconds} / ${durationMinutes}:${durationSeconds}`; |
| } else { |
| document.getElementById('audioTime').textContent = '0:00 / 0:00'; |
| } |
| } |
| }); |
| |
| |
| document.addEventListener('DOMContentLoaded', function() { |
| |
| document.getElementById('fileInputBtn').addEventListener('click', function() { |
| document.getElementById('fileInput').click(); |
| }); |
| |
| document.getElementById('fileInput').addEventListener('change', function(e) { |
| if (e.target.files.length) { |
| loadAudioFile(e.target.files[0]); |
| } |
| }); |
| |
| |
| document.getElementById('micInputBtn').addEventListener('click', function() { |
| initMicrophone(); |
| }); |
| |
| |
| document.getElementById('playBtn').addEventListener('click', playAudio); |
| document.getElementById('pauseBtn').addEventListener('click', pauseAudio); |
| document.getElementById('stopBtn').addEventListener('click', stopAudio); |
| |
| |
| document.getElementById('volumeSlider').addEventListener('input', function(e) { |
| const volume = e.target.value / 100; |
| if (audioElement) audioElement.volume = volume; |
| document.getElementById('volumeValue').textContent = `${e.target.value}%`; |
| }); |
| |
| |
| document.querySelectorAll('.pattern-option').forEach(option => { |
| option.addEventListener('click', function() { |
| currentPattern = this.getAttribute('data-pattern'); |
| |
| |
| document.querySelectorAll('.pattern-option').forEach(opt => { |
| opt.querySelector('.pattern-preview').classList.remove('glow'); |
| }); |
| this.querySelector('.pattern-preview').classList.add('glow'); |
| |
| |
| document.querySelectorAll('[id$="Settings"]').forEach(el => { |
| el.classList.add('hidden'); |
| }); |
| document.getElementById(`${currentPattern}Settings`).classList.remove('hidden'); |
| }); |
| }); |
| |
| |
| document.getElementById('color1').addEventListener('input', function(e) { |
| color1 = e.target.value; |
| }); |
| |
| document.getElementById('color2').addEventListener('input', function(e) { |
| color2 = e.target.value; |
| }); |
| |
| |
| document.getElementById('bgOpacitySlider').addEventListener('input', function(e) { |
| bgOpacity = e.target.value; |
| document.getElementById('bgOpacityValue').textContent = `${e.target.value}%`; |
| }); |
| |
| |
| |
| document.getElementById('waveThicknessSlider').addEventListener('input', function(e) { |
| waveThickness = parseInt(e.target.value); |
| document.getElementById('waveThicknessValue').textContent = e.target.value; |
| }); |
| |
| document.getElementById('waveSmoothnessSlider').addEventListener('input', function(e) { |
| waveSmoothness = parseInt(e.target.value); |
| document.getElementById('waveSmoothnessValue').textContent = `${e.target.value}%`; |
| }); |
| |
| |
| document.getElementById('particleCountSlider').addEventListener('input', function(e) { |
| particleCount = parseInt(e.target.value); |
| document.getElementById('particleCountValue').textContent = e.target.value; |
| initParticles(); |
| }); |
| |
| document.getElementById('particleSizeSlider').addEventListener('input', function(e) { |
| particleSize = parseInt(e.target.value); |
| document.getElementById('particleSizeValue').textContent = e.target.value; |
| }); |
| |
| |
| document.getElementById('ringCountSlider').addEventListener('input', function(e) { |
| ringCount = parseInt(e.target.value); |
| document.getElementById('ringCountValue').textContent = e.target.value; |
| }); |
| |
| document.getElementById('ringSpacingSlider').addEventListener('input', function(e) { |
| ringSpacing = parseInt(e.target.value); |
| document.getElementById('ringSpacingValue').textContent = e.target.value; |
| }); |
| |
| |
| document.getElementById('barCountSlider').addEventListener('input', function(e) { |
| barCount = parseInt(e.target.value); |
| document.getElementById('barCountValue').textContent = e.target.value; |
| }); |
| |
| document.getElementById('barWidthSlider').addEventListener('input', function(e) { |
| barWidth = parseInt(e.target.value); |
| document.getElementById('barWidthValue').textContent = e.target.value; |
| }); |
| |
| |
| document.getElementById('exportImageBtn').addEventListener('click', exportImage); |
| document.getElementById('exportVideoBtn').addEventListener('click', startRecording); |
| document.getElementById('stopRecordingBtn').addEventListener('click', stopRecording); |
| |
| |
| document.querySelector('.pattern-option[data-pattern="waveform"]').click(); |
| }); |
| |
| |
| function loadAudioFile(file) { |
| if (audioElement) { |
| audioElement.pause(); |
| if (source) source.disconnect(); |
| } |
| |
| const fileURL = URL.createObjectURL(file); |
| audioElement = new Audio(fileURL); |
| audioElement.volume = document.getElementById('volumeSlider').value / 100; |
| |
| |
| if (!audioContext) { |
| audioContext = new (window.AudioContext || window.webkitAudioContext)(); |
| } |
| |
| if (analyser) { |
| source.disconnect(); |
| } |
| |
| source = audioContext.createMediaElementSource(audioElement); |
| analyser = audioContext.createAnalyser(); |
| analyser.fftSize = 2048; |
| |
| source.connect(analyser); |
| analyser.connect(audioContext.destination); |
| |
| |
| document.getElementById('songInfo').textContent = file.name; |
| document.getElementById('playBtn').classList.add('glow'); |
| document.getElementById('pauseBtn').classList.remove('glow'); |
| document.getElementById('stopBtn').classList.remove('glow'); |
| |
| |
| audioElement.addEventListener('play', function() { |
| isPlaying = true; |
| if (audioContext.state === 'suspended') { |
| audioContext.resume(); |
| } |
| document.getElementById('playBtn').classList.add('glow'); |
| document.getElementById('pauseBtn').classList.remove('glow'); |
| document.getElementById('stopBtn').classList.remove('glow'); |
| }); |
| |
| audioElement.addEventListener('pause', function() { |
| isPlaying = false; |
| document.getElementById('playBtn').classList.remove('glow'); |
| document.getElementById('pauseBtn').classList.add('glow'); |
| document.getElementById('stopBtn').classList.remove('glow'); |
| }); |
| |
| audioElement.addEventListener('ended', function() { |
| isPlaying = false; |
| document.getElementById('playBtn').classList.remove('glow'); |
| document.getElementById('pauseBtn').classList.remove('glow'); |
| document.getElementById('stopBtn').classList.add('glow'); |
| }); |
| } |
| |
| function initMicrophone() { |
| if (audioElement) { |
| audioElement.pause(); |
| if (source) source.disconnect(); |
| } |
| |
| navigator.mediaDevices.getUserMedia({ audio: true, video: false }) |
| .then(function(stream) { |
| if (!audioContext) { |
| audioContext = new (window.AudioContext || window.webkitAudioContext)(); |
| } |
| |
| if (source) { |
| source.disconnect(); |
| } |
| |
| source = audioContext.createMediaStreamSource(stream); |
| analyser = audioContext.createAnalyser(); |
| analyser.fftSize = 2048; |
| |
| source.connect(analyser); |
| |
| |
| document.getElementById('songInfo').textContent = "Microphone Input"; |
| document.getElementById('playBtn').classList.remove('glow'); |
| document.getElementById('pauseBtn').classList.remove('glow'); |
| document.getElementById('stopBtn').classList.remove('glow'); |
| isPlaying = true; |
| |
| |
| if (audioContext.state === 'suspended') { |
| audioContext.resume(); |
| } |
| }) |
| .catch(function(err) { |
| console.error('Error accessing microphone:', err); |
| alert('Could not access microphone. Please check permissions.'); |
| }); |
| } |
| |
| function playAudio() { |
| if (audioElement && !isPlaying) { |
| audioElement.play(); |
| } else if (!audioElement && !isPlaying) { |
| alert('Please load an audio file or enable microphone first.'); |
| } |
| } |
| |
| function pauseAudio() { |
| if (audioElement && isPlaying) { |
| audioElement.pause(); |
| } |
| } |
| |
| function stopAudio() { |
| if (audioElement) { |
| audioElement.pause(); |
| audioElement.currentTime = 0; |
| isPlaying = false; |
| document.getElementById('playBtn').classList.remove('glow'); |
| document.getElementById('pauseBtn').classList.remove('glow'); |
| document.getElementById('stopBtn').classList.add('glow'); |
| } |
| } |
| |
| |
| function exportImage() { |
| const canvas = document.querySelector('#canvas-container canvas'); |
| const link = document.createElement('a'); |
| link.download = 'visualizer-snapshot.png'; |
| link.href = canvas.toDataURL('image/png'); |
| link.click(); |
| } |
| |
| function startRecording() { |
| const canvas = document.querySelector('#canvas-container canvas'); |
| const stream = canvas.captureStream(30); |
| |
| recordedChunks = []; |
| mediaRecorder = new MediaRecorder(stream, { |
| mimeType: 'video/webm;codecs=vp9' |
| }); |
| |
| mediaRecorder.ondataavailable = function(e) { |
| if (e.data.size > 0) { |
| recordedChunks.push(e.data); |
| } |
| }; |
| |
| mediaRecorder.onstop = function() { |
| const blob = new Blob(recordedChunks, { type: 'video/webm' }); |
| const url = URL.createObjectURL(blob); |
| const link = document.createElement('a'); |
| link.download = 'visualizer-recording.webm'; |
| link.href = url; |
| link.click(); |
| |
| document.getElementById('recordingControls').classList.add('hidden'); |
| clearInterval(recordingInterval); |
| }; |
| |
| mediaRecorder.start(); |
| recordingStartTime = Date.now(); |
| |
| |
| recordingInterval = setInterval(function() { |
| const elapsed = Math.floor((Date.now() - recordingStartTime) / 1000); |
| const minutes = Math.floor(elapsed / 60).toString().padStart(2, '0'); |
| const seconds = (elapsed % 60).toString().padStart(2, '0'); |
| document.getElementById('recordingTimer').innerHTML = |
| `<i class="fas fa-circle text-rose-500 mr-1 animate-pulse"></i> ${minutes}:${seconds}`; |
| }, 1000); |
| |
| document.getElementById('recordingControls').classList.remove('hidden'); |
| } |
| |
| function stopRecording() { |
| if (mediaRecorder && mediaRecorder.state === 'recording') { |
| mediaRecorder.stop(); |
| } |
| } |
| |
| |
| function initParticles() { |
| |
| } |
| </script> |
| <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=bibbler/audio-reactive-visualizer" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body> |
| </html> |