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