document.addEventListener('DOMContentLoaded', function() { // Global variables let vocalAudioBuffer = null; let musicAudioBuffer = null; let audioContext = null; let vocalSource = null; let musicSource = null; let vocalGainNode = null; let musicGainNode = null; let isPlaying = false; let startTime = 0; let playStartTime = 0; let animationFrameId = null; let vocalStartTime = 0; let vocalEndTime = 0; let musicStartTime = 0; let musicEndTime = 0; let vocalOffsetX = 0; let musicOffsetX = 0; let isDragging = false; let currentDraggingElement = null; let dragStartX = 0; // DOM Elements const vocalUpload = document.getElementById('vocalUpload'); const musicUpload = document.getElementById('musicUpload'); const vocalFileName = document.getElementById('vocalFileName'); const musicFileName = document.getElementById('musicFileName'); const timelineEditor = document.getElementById('timelineEditor'); const playBtn = document.getElementById('playBtn'); const exportBtn = document.getElementById('exportBtn'); const exportSection = document.getElementById('exportSection'); const vocalVolume = document.getElementById('vocalVolume'); const musicVolume = document.getElementById('musicVolume'); const vocalTrack = document.getElementById('vocalTrack'); const musicTrack = document.getElementById('musicTrack'); const vocalStartHandle = document.getElementById('vocalStartHandle'); const vocalEndHandle = document.getElementById('vocalEndHandle'); const musicStartHandle = document.getElementById('musicStartHandle'); const musicEndHandle = document.getElementById('musicEndHandle'); const vocalProgress = document.getElementById('vocalProgress'); const musicProgress = document.getElementById('musicProgress'); const timelineMarkers = document.getElementById('timelineMarkers'); const progressContainer = document.getElementById('progressContainer'); const exportProgress = document.getElementById('exportProgress'); const progressText = document.getElementById('progressText'); // Initialize Audio Context function initAudioContext() { if (!audioContext) { audioContext = new (window.AudioContext || window.webkitAudioContext)(); vocalGainNode = audioContext.createGain(); musicGainNode = audioContext.createGain(); vocalGainNode.gain.value = vocalVolume.value; musicGainNode.gain.value = musicVolume.value; vocalGainNode.connect(audioContext.destination); musicGainNode.connect(audioContext.destination); } } // File Upload Handlers vocalUpload.addEventListener('change', function(e) { handleFileUpload(e.target.files[0], 'vocal'); }); musicUpload.addEventListener('change', function(e) { handleFileUpload(e.target.files[0], 'music'); }); function handleFileUpload(file, type) { if (!file) return; const fileNameElement = type === 'vocal' ? vocalFileName : musicFileName; fileNameElement.textContent = file.name; const reader = new FileReader(); reader.onload = function(e) { initAudioContext(); audioContext.decodeAudioData(e.target.result) .then(buffer => { if (type === 'vocal') { vocalAudioBuffer = buffer; vocalStartTime = 0; vocalEndTime = buffer.duration; updateTrackUI('vocal'); } else { musicAudioBuffer = buffer; musicStartTime = 0; musicEndTime = buffer.duration; updateTrackUI('music'); } // Show timeline editor if both files are loaded if (vocalAudioBuffer && musicAudioBuffer) { timelineEditor.classList.remove('hidden'); exportSection.classList.remove('hidden'); createTimelineMarkers(); } }) .catch(error => { console.error('Error decoding audio data', error); fileNameElement.textContent += ' (Error loading)'; }); }; reader.readAsArrayBuffer(file); } // Update track UI with waveform and handles function updateTrackUI(type) { const buffer = type === 'vocal' ? vocalAudioBuffer : musicAudioBuffer; const trackElement = type === 'vocal' ? vocalTrack : musicTrack; const startHandle = type === 'vocal' ? vocalStartHandle : musicStartHandle; const endHandle = type === 'voral' ? vocalEndHandle : musicEndHandle; if (!buffer) return; // Update handle positions const duration = buffer.duration; const containerWidth = trackElement.parentElement.offsetWidth; const startPosition = (type === 'vocal' ? vocalStartTime : musicStartTime) / duration * containerWidth; const endPosition = (type === 'voral' ? vocalEndTime : musicEndTime) / duration * containerWidth; startHandle.style.left = `${startPosition}px`; endHandle.style.left = `${endPosition}px`; // For simplicity, we're not drawing actual waveforms here // In a real app, you would use a library like wavesurfer.js } // Create timeline markers function createTimelineMarkers() { timelineMarkers.innerHTML = ''; const duration = Math.max(vocalAudioBuffer.duration, musicAudioBuffer.duration); const markerCount = Math.ceil(duration); for (let i = 0; i <= markerCount; i++) { const marker = document.createElement('div'); marker.className = 'relative h-6 w-px bg-gray-400'; const label = document.createElement('div'); label.className = 'absolute top-full text-xs text-gray-500'; label.textContent = `${i}s`; marker.appendChild(label); timelineMarkers.appendChild(marker); } } // Play/Pause functionality playBtn.addEventListener('click', function() { if (isPlaying) { stopPlayback(); } else { startPlayback(); } }); function startPlayback() { if (!vocalAudioBuffer || !musicAudioBuffer) return; stopPlayback(); // Stop any existing playback initAudioContext(); // Create sources vocalSource = audioContext.createBufferSource(); musicSource = audioContext.createBufferSource(); vocalSource.buffer = vocalAudioBuffer; musicSource.buffer = musicAudioBuffer; vocalSource.connect(vocalGainNode); musicSource.connect(musicGainNode); // Calculate offsets and durations const vocalDuration = vocalEndTime - vocalStartTime; const musicDuration = musicEndTime - musicStartTime; // Start playback with offsets vocalSource.start(0, vocalStartTime, vocalDuration); musicSource.start(0, musicStartTime, musicDuration); startTime = audioContext.currentTime; playStartTime = audioContext.currentTime; isPlaying = true; // Update play button const playIcon = playBtn.querySelector('i'); playIcon.setAttribute('data-feather', 'pause'); feather.replace(); // Start progress animation updatePlaybackProgress(); } function stopPlayback() { if (vocalSource) { vocalSource.stop(); vocalSource = null; } if (musicSource) { musicSource.stop(); musicSource = null; } if (animationFrameId) { cancelAnimationFrame(animationFrameId); animationFrameId = null; } isPlaying = false; // Update play button const playIcon = playBtn.querySelector('i'); playIcon.setAttribute('data-feather', 'play'); feather.replace(); // Reset progress bars vocalProgress.style.width = '0%'; musicProgress.style.width = '0%'; } function updatePlaybackProgress() { if (!isPlaying) return; const currentTime = audioContext.currentTime - playStartTime; const vocalDuration = vocalEndTime - vocalStartTime; const musicDuration = musicEndTime - musicStartTime; // Update progress bars const vocalProgressPercent = Math.min(100, (currentTime / vocalDuration) * 100); const musicProgressPercent = Math.min(100, (currentTime / musicDuration) * 100); vocalProgress.style.width = `${vocalProgressPercent}%`; musicProgress.style.width = `${musicProgressPercent}%`; // Continue animation animationFrameId = requestAnimationFrame(updatePlaybackProgress); // Stop when both tracks are finished if (currentTime >= vocalDuration && currentTime >= musicDuration) { stopPlayback(); } } // Volume controls vocalVolume.addEventListener('input', function() { if (vocalGainNode) { vocalGainNode.gain.value = this.value; } }); musicVolume.addEventListener('input', function() { if (musicGainNode) { musicGainNode.gain.value = this.value; } }); // Drag and drop for track positioning [vocalTrack, musicTrack].forEach(track => { track.addEventListener('mousedown', startDrag); }); function startDrag(e) { if (e.button !== 0) return; // Only left mouse button isDragging = true; currentDraggingElement = e.target; dragStartX = e.clientX; if (currentDraggingElement === vocalTrack) { vocalOffsetX = parseFloat(currentDraggingElement.style.transform?.replace('translateX(', '')?.replace('px)', '') || 0); } else { musicOffsetX = parseFloat(currentDraggingElement.style.transform?.replace('translateX(', '')?.replace('px)', '') || 0); } currentDraggingElement.classList.add('dragging'); document.addEventListener('mousemove', drag); document.addEventListener('mouseup', stopDrag); } function drag(e) { if (!isDragging) return; const deltaX = e.clientX - dragStartX; if (currentDraggingElement === vocalTrack) { const newOffset = vocalOffsetX + deltaX; currentDraggingElement.style.transform = `translateX(${newOffset}px)`; // Calculate new start time based on position const containerWidth = currentDraggingElement.parentElement.offsetWidth; const duration = vocalAudioBuffer.duration; vocalStartTime = Math.max(0, (deltaX / containerWidth) * duration); vocalEndTime = Math.min(duration, vocalEndTime + (deltaX / containerWidth) * duration); } else { const newOffset = musicOffsetX + deltaX; currentDraggingElement.style.transform = `translateX(${newOffset}px)`; // Calculate new start time based on position const containerWidth = currentDraggingElement.parentElement.offsetWidth; const duration = musicAudioBuffer.duration; musicStartTime = Math.max(0, (deltaX / containerWidth) * duration); musicEndTime = Math.min(duration, musicEndTime + (deltaX / containerWidth) * duration); } } function stopDrag() { if (!isDragging) return; isDragging = false; currentDraggingElement.classList.remove('dragging'); document.removeEventListener('mousemove', drag); document.removeEventListener('mouseup', stopDrag); } // Trim buttons functionality document.querySelectorAll('.trim-start-btn').forEach(btn => { btn.addEventListener('click', function() { const trackType = this.getAttribute('data-track'); if (trackType === 'vocal' && vocalAudioBuffer) { vocalStartTime = Math.min(vocalStartTime + 0.1, vocalEndTime - 0.1); updateTrackUI('vocal'); } else if (trackType === 'music' && musicAudioBuffer) { musicStartTime = Math.min(musicStartTime + 0.1, musicEndTime - 0.1); updateTrackUI('music'); } }); }); document.querySelectorAll('.trim-end-btn').forEach(btn => { btn.addEventListener('click', function() { const trackType = this.getAttribute('data-track'); if (trackType === 'vocal' && vocalAudioBuffer) { vocalEndTime = Math.max(vocalEndTime - 0.1, vocalStartTime + 0.1); updateTrackUI('vocal'); } else if (trackType === 'music' && musicAudioBuffer) { musicEndTime = Math.max(musicEndTime - 0.1, musicStartTime + 0.1); updateTrackUI('music'); } }); }); // Handle resize window.addEventListener('resize', function() { if (vocalAudioBuffer) updateTrackUI('vocal'); if (musicAudioBuffer) updateTrackUI('music'); }); // Export functionality exportBtn.addEventListener('click', function() { if (!vocalAudioBuffer || !musicAudioBuffer) return; progressContainer.classList.remove('hidden'); exportProgress.style.width = '0%'; progressText.textContent = 'Processing...'; // Simulate export progress let progress = 0; const interval = setInterval(() => { progress += 5; exportProgress.style.width = `${progress}%`; if (progress >= 100) { clearInterval(interval); progressText.textContent = 'Export complete!'; // Create a download link for the mixed audio (simplified) setTimeout(() => { const a = document.createElement('a'); a.href = '#'; a.download = 'mixed-audio.wav'; a.textContent = 'Download Mixed Audio'; a.className = 'text-indigo-600 hover:underline'; progressText.innerHTML = ''; progressText.appendChild(a); }, 500); } }, 100); }); // Drag and drop file handling const uploadContainers = document.querySelectorAll('.border-dashed'); uploadContainers.forEach(container => { container.addEventListener('dragover', function(e) { e.preventDefault(); this.classList.add('dragover'); }); container.addEventListener('dragleave', function() { this.classList.remove('dragover'); }); container.addEventListener('drop', function(e) { e.preventDefault(); this.classList.remove('dragover'); const files = e.dataTransfer.files; if (files.length > 0) { const inputId = this.querySelector('input[type="file"]').id; const fileInput = document.getElementById(inputId); // Create a new FileList (since we can't modify the existing one) const dataTransfer = new DataTransfer(); dataTransfer.items.add(files[0]); fileInput.files = dataTransfer.files; // Trigger change event const event = new Event('change'); fileInput.dispatchEvent(event); } }); }); });