vizzy / index.html
namelessai's picture
Update index.html
77a0842 verified
<!DOCTYPE html>
<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>