|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
|
|
|
const uploadBtn = document.getElementById('upload-btn'); |
|
|
const audioUpload = document.getElementById('audio-upload'); |
|
|
const audioControls = document.getElementById('audio-controls'); |
|
|
const songTitle = document.getElementById('song-title'); |
|
|
const songDuration = document.getElementById('song-duration'); |
|
|
const currentTimeEl = document.getElementById('current-time'); |
|
|
const totalTimeEl = document.getElementById('total-time'); |
|
|
const playBtn = document.getElementById('play-btn'); |
|
|
const visualPreview = document.getElementById('visual-preview'); |
|
|
const styleOptions = document.getElementById('style-options'); |
|
|
const renderSection = document.getElementById('render-section'); |
|
|
const generateBtn = document.getElementById('generate-btn'); |
|
|
const progressContainer = document.getElementById('progress-container'); |
|
|
const progressBar = document.getElementById('progress-bar'); |
|
|
const progressPercent = document.getElementById('progress-percent'); |
|
|
const downloadSection = document.getElementById('download-section'); |
|
|
const downloadBtn = document.getElementById('download-btn'); |
|
|
const visualizerCanvas = document.getElementById('visualizer'); |
|
|
const videoCanvas = document.getElementById('video-canvas'); |
|
|
|
|
|
|
|
|
let audioContext; |
|
|
let analyser; |
|
|
let audioBuffer; |
|
|
let audioElement; |
|
|
let wavesurfer; |
|
|
let isPlaying = false; |
|
|
let selectedStyle = 'abstract'; |
|
|
let animationId; |
|
|
|
|
|
|
|
|
function initWavesurfer() { |
|
|
wavesurfer = WaveSurfer.create({ |
|
|
container: '#waveform', |
|
|
waveColor: '#8B5CF6', |
|
|
progressColor: '#EC4899', |
|
|
cursorColor: '#EC4899', |
|
|
barWidth: 2, |
|
|
barRadius: 3, |
|
|
cursorWidth: 1, |
|
|
height: 60, |
|
|
barGap: 2, |
|
|
responsive: true, |
|
|
}); |
|
|
|
|
|
wavesurfer.on('ready', () => { |
|
|
audioElement = wavesurfer.backend.getAudioElement(); |
|
|
setupAudioAnalysis(); |
|
|
updateDuration(); |
|
|
playBtn.innerHTML = '<i data-feather="play" class="w-5 h-5"></i>'; |
|
|
feather.replace(); |
|
|
}); |
|
|
|
|
|
wavesurfer.on('audioprocess', updateCurrentTime); |
|
|
wavesurfer.on('seek', updateCurrentTime); |
|
|
wavesurfer.on('finish', () => { |
|
|
isPlaying = false; |
|
|
playBtn.innerHTML = '<i data-feather="play" class="w-5 h-5"></i>'; |
|
|
feather.replace(); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function setupAudioAnalysis() { |
|
|
audioContext = new (window.AudioContext || window.webkitAudioContext)(); |
|
|
analyser = audioContext.createAnalyser(); |
|
|
analyser.fftSize = 256; |
|
|
|
|
|
const source = audioContext.createMediaElementSource(audioElement); |
|
|
source.connect(analyser); |
|
|
analyser.connect(audioContext.destination); |
|
|
|
|
|
startVisualizer(); |
|
|
} |
|
|
|
|
|
|
|
|
function startVisualizer() { |
|
|
const canvasCtx = visualizerCanvas.getContext('2d'); |
|
|
const WIDTH = visualizerCanvas.width = visualizerCanvas.offsetWidth; |
|
|
const HEIGHT = visualizerCanvas.height = visualizerCanvas.offsetHeight; |
|
|
|
|
|
const bufferLength = analyser.frequencyBinCount; |
|
|
const dataArray = new Uint8Array(bufferLength); |
|
|
|
|
|
function draw() { |
|
|
animationId = requestAnimationFrame(draw); |
|
|
|
|
|
analyser.getByteFrequencyData(dataArray); |
|
|
|
|
|
canvasCtx.fillStyle = 'rgba(0, 0, 0, 0.1)'; |
|
|
canvasCtx.fillRect(0, 0, WIDTH, HEIGHT); |
|
|
|
|
|
const barWidth = (WIDTH / bufferLength) * 2.5; |
|
|
let x = 0; |
|
|
|
|
|
for (let i = 0; i < bufferLength; i++) { |
|
|
const barHeight = (dataArray[i] / 255) * HEIGHT; |
|
|
const hue = i / bufferLength * 360; |
|
|
|
|
|
canvasCtx.fillStyle = `hsla(${hue}, 100%, 50%, 0.8)`; |
|
|
canvasCtx.fillRect(x, HEIGHT - barHeight, barWidth, barHeight); |
|
|
|
|
|
x += barWidth + 1; |
|
|
} |
|
|
} |
|
|
|
|
|
draw(); |
|
|
} |
|
|
|
|
|
|
|
|
function updateCurrentTime() { |
|
|
if (audioElement) { |
|
|
const currentTime = formatTime(audioElement.currentTime); |
|
|
currentTimeEl.textContent = currentTime; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function updateDuration() { |
|
|
if (audioElement) { |
|
|
const duration = formatTime(audioElement.duration); |
|
|
totalTimeEl.textContent = duration; |
|
|
songDuration.textContent = duration; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function formatTime(seconds) { |
|
|
const mins = Math.floor(seconds / 60); |
|
|
const secs = Math.floor(seconds % 60); |
|
|
return `${mins}:${secs < 10 ? '0' : ''}${secs}`; |
|
|
} |
|
|
|
|
|
|
|
|
function generateBackgroundImage(style) { |
|
|
|
|
|
|
|
|
|
|
|
let gradient; |
|
|
switch (style) { |
|
|
case 'nature': |
|
|
gradient = 'linear-gradient(135deg, #10B981 0%, #3B82F6 100%)'; |
|
|
break; |
|
|
case 'cosmic': |
|
|
gradient = 'linear-gradient(135deg, #8B5CF6 0%, #EC4899 100%)'; |
|
|
break; |
|
|
default: |
|
|
gradient = 'linear-gradient(135deg, #6366F1 0%, #A78BFA 50%, #F472B6 100%)'; |
|
|
} |
|
|
|
|
|
visualPreview.style.backgroundImage = gradient; |
|
|
} |
|
|
|
|
|
|
|
|
uploadBtn.addEventListener('click', () => audioUpload.click()); |
|
|
|
|
|
audioUpload.addEventListener('change', (e) => { |
|
|
const file = e.target.files[0]; |
|
|
if (!file) return; |
|
|
|
|
|
songTitle.textContent = file.name.replace('.mp3', ''); |
|
|
|
|
|
|
|
|
if (!wavesurfer) { |
|
|
initWavesurfer(); |
|
|
} |
|
|
|
|
|
const blobUrl = URL.createObjectURL(file); |
|
|
wavesurfer.load(blobUrl); |
|
|
|
|
|
audioControls.classList.remove('hidden'); |
|
|
styleOptions.classList.remove('hidden'); |
|
|
renderSection.classList.remove('hidden'); |
|
|
downloadSection.classList.add('hidden'); |
|
|
|
|
|
|
|
|
generateBackgroundImage(selectedStyle); |
|
|
}); |
|
|
|
|
|
playBtn.addEventListener('click', () => { |
|
|
if (!wavesurfer) return; |
|
|
|
|
|
if (isPlaying) { |
|
|
wavesurfer.pause(); |
|
|
isPlaying = false; |
|
|
playBtn.innerHTML = '<i data-feather="play" class="w-5 h-5"></i>'; |
|
|
} else { |
|
|
wavesurfer.play(); |
|
|
isPlaying = true; |
|
|
playBtn.innerHTML = '<i data-feather="pause" class="w-5 h-5"></i>'; |
|
|
} |
|
|
feather.replace(); |
|
|
}); |
|
|
|
|
|
|
|
|
document.querySelectorAll('.style-option').forEach(btn => { |
|
|
btn.addEventListener('click', () => { |
|
|
selectedStyle = btn.dataset.style; |
|
|
document.querySelectorAll('.style-option').forEach(b => { |
|
|
b.classList.remove('border-purple-500', 'border-2'); |
|
|
}); |
|
|
btn.classList.add('border-purple-500', 'border-2'); |
|
|
generateBackgroundImage(selectedStyle); |
|
|
}); |
|
|
}); |
|
|
|
|
|
generateBtn.addEventListener('click', async () => { |
|
|
if (!wavesurfer || wavesurfer.isLoading()) { |
|
|
alert('Please wait for the audio to finish loading first'); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
if (animationId) { |
|
|
cancelAnimationFrame(animationId); |
|
|
} |
|
|
|
|
|
generateBtn.disabled = true; |
|
|
generateBtn.innerHTML = '<span class="animate-pulse">Generating...</span>'; |
|
|
progressContainer.classList.remove('hidden'); |
|
|
|
|
|
|
|
|
videoCanvas.width = 1280; |
|
|
videoCanvas.height = 720; |
|
|
const videoCtx = videoCanvas.getContext('2d'); |
|
|
videoCanvas.classList.remove('hidden'); |
|
|
|
|
|
|
|
|
const stream = videoCanvas.captureStream(30); |
|
|
const mediaRecorder = new MediaRecorder(stream, { |
|
|
mimeType: 'video/webm;codecs=vp9' |
|
|
}); |
|
|
|
|
|
const chunks = []; |
|
|
mediaRecorder.ondataavailable = (event) => { |
|
|
chunks.push(event.data); |
|
|
}; |
|
|
|
|
|
mediaRecorder.onstop = () => { |
|
|
const blob = new Blob(chunks, { type: 'video/webm' }); |
|
|
const url = URL.createObjectURL(blob); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
progressBar.style.width = '100%'; |
|
|
progressPercent.textContent = '100%'; |
|
|
progressPercent.previousElementSibling.textContent = 'Finalizing...'; |
|
|
|
|
|
setTimeout(() => { |
|
|
generateBtn.disabled = false; |
|
|
generateBtn.innerHTML = 'Generate Video'; |
|
|
progressContainer.classList.add('hidden'); |
|
|
downloadSection.classList.remove('hidden'); |
|
|
|
|
|
downloadBtn.href = url; |
|
|
downloadBtn.download = `${songTitle.textContent || 'audio-visualization'}.mp4`; |
|
|
}, 500); |
|
|
}, 1000); |
|
|
}; |
|
|
|
|
|
|
|
|
mediaRecorder.start(100); |
|
|
|
|
|
|
|
|
let startTime = Date.now(); |
|
|
const duration = wavesurfer.getDuration() * 1000; |
|
|
let lastFrameTime = 0; |
|
|
const frameRate = 30; |
|
|
const frameInterval = 1000 / frameRate; |
|
|
|
|
|
function renderVideoFrame() { |
|
|
const currentTime = Date.now() - startTime; |
|
|
const progress = Math.min(100, (currentTime / duration) * 100); |
|
|
|
|
|
progressBar.style.width = `${progress}%`; |
|
|
progressPercent.textContent = `${Math.round(progress)}%`; |
|
|
progressPercent.previousElementSibling.textContent = 'Rendering...'; |
|
|
|
|
|
|
|
|
const analyserData = new Uint8Array(analyser.frequencyBinCount); |
|
|
analyser.getByteFrequencyData(analyserData); |
|
|
|
|
|
|
|
|
videoCtx.fillStyle = 'rgba(0, 0, 0, 1)'; |
|
|
videoCtx.fillRect(0, 0, videoCanvas.width, videoCanvas.height); |
|
|
|
|
|
|
|
|
let gradient; |
|
|
switch(selectedStyle) { |
|
|
case 'nature': |
|
|
gradient = videoCtx.createLinearGradient(0, 0, videoCanvas.width, videoCanvas.height); |
|
|
gradient.addColorStop(0, '#10B981'); |
|
|
gradient.addColorStop(1, '#3B82F6'); |
|
|
break; |
|
|
case 'cosmic': |
|
|
gradient = videoCtx.createLinearGradient(0, 0, videoCanvas.width, videoCanvas.height); |
|
|
gradient.addColorStop(0, '#8B5CF6'); |
|
|
gradient.addColorStop(1, '#EC4899'); |
|
|
break; |
|
|
default: |
|
|
gradient = videoCtx.createLinearGradient(0, 0, videoCanvas.width, videoCanvas.height); |
|
|
gradient.addColorStop(0, '#6366F1'); |
|
|
gradient.addColorStop(0.5, '#A78BFA'); |
|
|
gradient.addColorStop(1, '#F472B6'); |
|
|
} |
|
|
|
|
|
|
|
|
const barWidth = videoCanvas.width / analyserData.length; |
|
|
for (let i = 0; i < analyserData.length; i++) { |
|
|
const barHeight = (analyserData[i] / 255) * videoCanvas.height; |
|
|
videoCtx.fillStyle = gradient; |
|
|
videoCtx.fillRect(i * barWidth, videoCanvas.height - barHeight, barWidth, barHeight); |
|
|
} |
|
|
|
|
|
|
|
|
videoCtx.fillStyle = 'white'; |
|
|
videoCtx.font = 'bold 48px Poppins'; |
|
|
videoCtx.textAlign = 'center'; |
|
|
videoCtx.fillText(songTitle.textContent || 'Audio Visualization', videoCanvas.width/2, videoCanvas.height/2); |
|
|
|
|
|
if (currentTime < duration) { |
|
|
animationId = requestAnimationFrame(renderVideoFrame); |
|
|
} else { |
|
|
mediaRecorder.stop(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
renderVideoFrame(); |
|
|
}); |
|
|
|
|
|
window.addEventListener('beforeunload', () => { |
|
|
if (animationId) { |
|
|
cancelAnimationFrame(animationId); |
|
|
} |
|
|
if (audioContext && audioContext.state !== 'closed') { |
|
|
audioContext.close(); |
|
|
} |
|
|
|
|
|
const streams = videoCanvas.captureStream().getTracks(); |
|
|
streams.forEach(stream => stream.stop()); |
|
|
}); |
|
|
}); |