|
<!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> |