AdityaAdaki
bug fixes1
63fc423
// Initialize Three.js scene and renderer
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true,
powerPreference: "high-performance"
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// Set initial background color based on theme
const savedTheme = localStorage.getItem('theme') || 'dark';
document.documentElement.setAttribute('data-theme', savedTheme);
renderer.setClearColor(savedTheme === 'light' ? 0xf5f5f5 : 0x000000, 0.9);
document.body.appendChild(renderer.domElement);
// Add fog to the scene for depth
scene.fog = new THREE.FogExp2(savedTheme === 'light' ? 0xf5f5f5 : 0x000000, 0.02);
// Initialize controls with enhanced settings
const controls = new THREE.OrbitControls(camera, renderer.domElement);
setupOrbitControls();
// Initialize other variables
let audioContext;
let analyser;
let audioElement;
let playlist = [];
let currentTrackIndex = 0;
let isPlaying = false;
let visualizationType = 'sphere';
let visualizers = {
bars: [],
sphere: null,
particles: null
};
let isShuffleActive = false;
let isRepeatActive = false;
// Add visualization-specific camera positions
const visualizerSettings = {
bars: {
cameraZ: 8,
baseRadius: 2
},
sphere: {
cameraZ: 5,
baseRadius: 1
},
particles: {
cameraZ: 20,
baseRadius: 8
}
};
// Theme button functionality
const themeBtn = document.querySelector('.theme-btn');
if (themeBtn) {
themeBtn.innerHTML = savedTheme === 'light' ?
'<i class="fas fa-moon"></i>' :
'<i class="fas fa-sun"></i>';
themeBtn.addEventListener('click', () => {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
themeBtn.innerHTML = newTheme === 'light' ?
'<i class="fas fa-moon"></i>' :
'<i class="fas fa-sun"></i>';
renderer.setClearColor(newTheme === 'light' ? 0xf5f5f5 : 0x000000, 0.9);
});
}
// Initialize audio context
function initAudio() {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
audioElement = new Audio();
const source = audioContext.createMediaElementSource(audioElement);
analyser = audioContext.createAnalyser();
analyser.fftSize = 2048;
source.connect(analyser);
analyser.connect(audioContext.destination);
}
// Visualization creation functions
function createBarsVisualization() {
const numBars = 180;
const geometry = new THREE.CylinderGeometry(0.05, 0.05, 1, 8);
geometry.translate(0, 0.5, 0); // Move pivot to bottom
// Create custom shader material for bars
const material = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0 },
color1: { value: new THREE.Color(0x4CAF50) },
color2: { value: new THREE.Color(0x2196F3) },
color3: { value: new THREE.Color(0xFF4081) }
},
vertexShader: `
varying vec3 vPosition;
varying vec3 vNormal;
void main() {
vPosition = position;
vNormal = normalize(normalMatrix * normal);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform float time;
uniform vec3 color1;
uniform vec3 color2;
uniform vec3 color3;
varying vec3 vPosition;
varying vec3 vNormal;
void main() {
float heightFactor = vPosition.y;
// Create dynamic color gradient
vec3 baseColor = mix(
mix(color1, color2, heightFactor),
color3,
sin(time * 0.5) * 0.5 + 0.5
);
// Add fresnel effect for edge glow
float fresnel = pow(1.0 - abs(dot(vNormal, vec3(0, 0, 1.0))), 2.0);
vec3 finalColor = mix(baseColor, vec3(1.0), fresnel * 0.5);
gl_FragColor = vec4(finalColor, 0.9);
}
`,
transparent: true,
side: THREE.DoubleSide
});
const radius = 4;
const angleStep = (Math.PI * 2) / numBars;
for (let i = 0; i < numBars; i++) {
const angle = i * angleStep;
const bar = new THREE.Mesh(geometry, material.clone());
// Position in a circle
bar.position.x = Math.cos(angle) * radius;
bar.position.z = Math.sin(angle) * radius;
// Rotate to face center
bar.rotation.y = -angle;
// Store initial properties
bar.userData.initialY = bar.position.y;
bar.userData.initialScale = 1;
bar.userData.angle = angle;
bar.userData.index = i;
scene.add(bar);
visualizers.bars.push(bar);
}
// Add ambient light
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambientLight);
// Add multiple point lights with different colors
const colors = [0xFF4081, 0x2196F3, 0x4CAF50];
colors.forEach((color, i) => {
const light = new THREE.PointLight(color, 1, 20);
const angle = (i / colors.length) * Math.PI * 2;
const lightRadius = radius * 1.5;
light.position.set(
Math.cos(angle) * lightRadius,
5,
Math.sin(angle) * lightRadius
);
scene.add(light);
});
}
function createSphereVisualization() {
const geometry = new THREE.IcosahedronGeometry(1, 4);
// Create a more complex material with gradient and glow effects
const material = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0 },
color1: { value: new THREE.Color(0x4CAF50) },
color2: { value: new THREE.Color(0x2196F3) },
color3: { value: new THREE.Color(0xFF4081) }
},
vertexShader: `
varying vec3 vNormal;
varying vec3 vPosition;
void main() {
vNormal = normalize(normalMatrix * normal);
vPosition = position;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform float time;
uniform vec3 color1;
uniform vec3 color2;
uniform vec3 color3;
varying vec3 vNormal;
varying vec3 vPosition;
void main() {
float noise = sin(vPosition.x * 10.0 + time) *
cos(vPosition.y * 10.0 + time) *
sin(vPosition.z * 10.0 + time);
vec3 color = mix(
mix(color1, color2, noise * 0.5 + 0.5),
color3,
sin(time * 0.5) * 0.5 + 0.5
);
float fresnel = pow(1.0 + dot(vNormal, vec3(0, 0, 1.0)), 3.0);
color = mix(color, vec3(1.0), fresnel * 0.7);
gl_FragColor = vec4(color, 0.9);
}
`,
transparent: true,
side: THREE.DoubleSide
});
visualizers.sphere = new THREE.Mesh(geometry, material);
// Store original vertex positions
visualizers.sphere.userData.originalPositions =
geometry.attributes.position.array.slice();
scene.add(visualizers.sphere);
// Add ambient light for base illumination
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambientLight);
// Add multiple point lights with different colors
const colors = [0xFF4081, 0x2196F3, 0x4CAF50];
const radius = 5;
colors.forEach((color, i) => {
const light = new THREE.PointLight(color, 1, 20);
const angle = (i / colors.length) * Math.PI * 2;
light.position.set(
Math.cos(angle) * radius,
Math.sin(angle) * radius,
radius
);
scene.add(light);
});
}
function createParticlesVisualization() {
const particleCount = 5000;
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3);
const scales = new Float32Array(particleCount);
const colors = new Float32Array(particleCount * 3);
const color1 = new THREE.Color(0x4CAF50);
const color2 = new THREE.Color(0x2196F3);
const color3 = new THREE.Color(0xFF4081);
// Create a spiral galaxy formation
for (let i = 0; i < particleCount; i++) {
const i3 = i * 3;
const radius = (Math.random() * 3) + 2;
const spinAngle = (i / particleCount) * Math.PI * 24;
const heightRange = Math.random() * Math.PI * 2;
// Create spiral arms
positions[i3] = Math.cos(spinAngle + radius) * radius;
positions[i3 + 1] = Math.sin(heightRange) * (radius * 0.2);
positions[i3 + 2] = Math.sin(spinAngle + radius) * radius;
// Vary particle sizes
scales[i] = Math.random() * 0.5 + 0.5;
// Create color gradient along the spiral
const colorMix = Math.abs(Math.sin(spinAngle));
const finalColor = new THREE.Color().lerpColors(
color1,
colorMix > 0.5 ? color2 : color3,
colorMix
);
colors[i3] = finalColor.r;
colors[i3 + 1] = finalColor.g;
colors[i3 + 2] = finalColor.b;
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('scale', new THREE.BufferAttribute(scales, 1));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
// Create custom shader material for particles
const material = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0 },
size: { value: 15.0 },
pixelRatio: { value: window.devicePixelRatio }
},
vertexShader: `
attribute float scale;
attribute vec3 color;
uniform float time;
uniform float size;
uniform float pixelRatio;
varying vec3 vColor;
void main() {
vColor = color;
vec3 pos = position;
// Add some movement
float angle = time * 0.2;
pos.x = position.x * cos(angle) - position.z * sin(angle);
pos.z = position.x * sin(angle) + position.z * cos(angle);
pos.y += sin(time + position.x * 0.5) * 0.3;
vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
gl_Position = projectionMatrix * mvPosition;
// Size attenuation
gl_PointSize = size * scale * pixelRatio * (1.0 / -mvPosition.z);
}
`,
fragmentShader: `
varying vec3 vColor;
void main() {
// Create circular particles
vec2 xy = gl_PointCoord.xy - vec2(0.5);
float radius = length(xy);
float alpha = 1.0 - smoothstep(0.45, 0.5, radius);
// Add glow effect
vec3 glow = vColor * (1.0 - radius * 2.0);
vec3 finalColor = mix(vColor, glow, 0.5);
gl_FragColor = vec4(finalColor, alpha);
}
`,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending
});
visualizers.particles = new THREE.Points(geometry, material);
scene.add(visualizers.particles);
}
// Visualization update functions
function updateBarsVisualization(dataArray) {
const time = Date.now() * 0.001;
const multiplier = 0.02;
visualizers.bars.forEach((bar, i) => {
const value = dataArray[i % dataArray.length] * multiplier;
// Update shader uniforms
bar.material.uniforms.time.value = time;
// Calculate dynamic height
const baseHeight = value + 0.1;
const wave = Math.sin(time * 2 + bar.userData.angle) * 0.1;
const finalHeight = baseHeight + wave;
// Update bar scale and position
bar.scale.y = finalHeight;
// Add floating effect
bar.position.y = Math.sin(time + bar.userData.angle) * 0.1;
// Add subtle rotation
bar.rotation.x = Math.sin(time * 0.5 + bar.userData.angle) * 0.1;
bar.rotation.z = Math.cos(time * 0.5 + bar.userData.angle) * 0.1;
});
}
function updateSphereVisualization(dataArray) {
if (!visualizers.sphere) return;
const positions = visualizers.sphere.geometry.attributes.position.array;
const originalPositions = visualizers.sphere.userData.originalPositions;
const time = Date.now() * 0.001;
// Update shader uniforms
visualizers.sphere.material.uniforms.time.value = time;
// Create more complex deformation based on audio data
for (let i = 0; i < positions.length; i += 3) {
const i3 = i / 3;
const value = dataArray[i3 % dataArray.length] / 255;
const deform = value * 0.5;
const noise = Math.sin(time + i3 * 0.1) * 0.2;
positions[i] = originalPositions[i] * (1 + deform * Math.sin(time + i3) + noise);
positions[i + 1] = originalPositions[i + 1] * (1 + deform * Math.cos(time + i3) + noise);
positions[i + 2] = originalPositions[i + 2] * (1 + deform * Math.sin(time * 0.5 + i3) + noise);
}
visualizers.sphere.geometry.attributes.position.needsUpdate = true;
// Add smooth rotation
visualizers.sphere.rotation.y += 0.002;
visualizers.sphere.rotation.x += 0.001;
}
function updateParticlesVisualization(dataArray) {
if (!visualizers.particles) return;
const time = Date.now() * 0.001;
const positions = visualizers.particles.geometry.attributes.position.array;
const scales = visualizers.particles.geometry.attributes.scale.array;
const colors = visualizers.particles.geometry.attributes.color.array;
// Update shader uniforms
visualizers.particles.material.uniforms.time.value = time;
for (let i = 0; i < positions.length; i += 3) {
const i3 = i / 3;
const value = dataArray[i3 % dataArray.length] / 255;
// Update particle scales based on audio
scales[i3] = (value * 0.5 + 0.5) * (Math.sin(time + i3) * 0.2 + 0.8);
// Update colors with audio reactivity
const hue = (i3 / positions.length) + time * 0.1;
const saturation = 0.7 + value * 0.3;
const lightness = 0.4 + value * 0.2;
const color = new THREE.Color().setHSL(hue, saturation, lightness);
colors[i] = color.r;
colors[i + 1] = color.g;
colors[i + 2] = color.b;
}
visualizers.particles.geometry.attributes.scale.needsUpdate = true;
visualizers.particles.geometry.attributes.color.needsUpdate = true;
}
// Animation loop
function animate() {
requestAnimationFrame(animate);
controls.update();
if (analyser && isPlaying) {
const dataArray = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(dataArray);
updateVisualization(dataArray);
}
renderer.render(scene, camera);
}
// Start animation
animate();
// Event listeners for window resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// Initialize the application
document.addEventListener('DOMContentLoaded', () => {
// Initialize loading screen
const loadingScreen = document.querySelector('.loading-screen');
function showLoading() {
if (loadingScreen) {
loadingScreen.classList.remove('hidden');
}
}
function hideLoading() {
if (loadingScreen) {
loadingScreen.classList.add('hidden');
}
}
// Show loading screen immediately
showLoading();
// Initialize all components
setupUploadHandlers();
setupPlayerControls();
setupToggleHandlers();
setupProgressBar();
setupPlaylistControls();
createVisualization();
// Hide loading screen after initialization
hideLoading();
});
// Export necessary functions and variables
window.playTrack = playTrack;
window.createPlaylist = createPlaylist;
window.updateNowPlayingInfo = updateNowPlayingInfo;
// Setup orbit controls
function setupOrbitControls() {
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.enableZoom = true;
controls.autoRotate = true;
controls.autoRotateSpeed = 1.5;
// Camera position is now set in createVisualization
}
// Enhanced file upload handling
function setupUploadHandlers() {
console.log('Setting up upload handlers...');
const uploadArea = document.getElementById('upload-area');
const fileInput = document.getElementById('audio-upload');
const uploadProgress = document.querySelector('.upload-progress');
const progressFill = uploadProgress?.querySelector('.progress-fill');
const progressText = uploadProgress?.querySelector('.progress-text');
const errorToast = document.querySelector('.error-toast');
if (!uploadArea || !fileInput) {
console.error('Upload elements not found');
return;
}
// Add logging for drag and drop events
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
console.log('File being dragged over upload area');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('dragover');
console.log('File drag left upload area');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
console.log('Files dropped:', e.dataTransfer.files.length, 'files');
handleFiles(e.dataTransfer.files);
});
uploadArea.addEventListener('click', () => {
console.log('Upload area clicked, triggering file input');
fileInput.click();
});
fileInput.addEventListener('change', (e) => {
console.log('Files selected:', e.target.files.length, 'files');
handleFiles(e.target.files);
});
}
// Enhanced toast notifications with null checks
function showError(message) {
const toast = document.querySelector('.error-toast');
if (!toast) return;
toast.textContent = message;
toast.className = 'error-toast error visible';
setTimeout(() => {
toast.classList.remove('visible');
}, 3000);
}
function showSuccess(message) {
const toast = document.querySelector('.error-toast');
if (!toast) return;
toast.textContent = message;
toast.className = 'error-toast success visible';
setTimeout(() => {
toast.classList.remove('visible');
}, 3000);
}
// Enhanced keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.code === 'Space' && !e.target.matches('input, textarea')) {
e.preventDefault();
togglePlayPause();
} else if (e.code === 'ArrowLeft') {
playPrevious();
} else if (e.code === 'ArrowRight') {
playNext();
} else if (e.code === 'KeyM') {
toggleMute();
}
});
// Volume control
function toggleMute() {
if (!audioElement) return;
const volumeBtn = document.querySelector('.volume-btn i');
if (audioElement.volume > 0) {
audioElement.volume = 0;
volumeBtn.className = 'fas fa-volume-mute';
} else {
audioElement.volume = 0.5;
volumeBtn.className = 'fas fa-volume-up';
}
updateVolumeUI();
}
function updateVolumeUI() {
const volumeProgress = document.querySelector('.volume-progress');
const volumeHandle = document.querySelector('.volume-handle');
const volumeSlider = document.getElementById('volume');
if (volumeProgress && volumeHandle && volumeSlider) {
const value = audioElement ? audioElement.volume : 0.5;
volumeProgress.style.width = `${value * 100}%`;
volumeHandle.style.left = `${value * 100}%`;
volumeSlider.value = value;
}
}
// Enhanced progress bar interaction
function setupProgressBar() {
const progressContainer = document.querySelector('.progress-container');
const progressBar = document.querySelector('.progress-bar');
const progress = document.querySelector('.progress');
const progressHandle = document.querySelector('.progress-handle');
const seekSlider = document.querySelector('.seek-slider');
// Return early if required elements are not found
if (!progressBar || !progress) {
console.error('Progress bar elements not found');
return;
}
let isDragging = false;
// Mouse events for desktop
progressBar.addEventListener('mousedown', startDragging);
document.addEventListener('mousemove', updateDragging);
document.addEventListener('mouseup', stopDragging);
// Touch events for mobile
progressBar.addEventListener('touchstart', handleTouchStart);
document.addEventListener('touchmove', handleTouchMove);
document.addEventListener('touchend', handleTouchEnd);
function handleTouchStart(e) {
e.preventDefault();
isDragging = true;
progressBar.classList.add('dragging');
updateProgress(e.touches[0]);
}
function handleTouchMove(e) {
if (!isDragging) return;
e.preventDefault();
updateProgress(e.touches[0]);
}
function handleTouchEnd() {
isDragging = false;
progressBar.classList.remove('dragging');
}
function startDragging(e) {
isDragging = true;
progressBar.classList.add('dragging');
updateProgress(e);
}
function updateDragging(e) {
if (!isDragging) return;
updateProgress(e);
}
function stopDragging() {
isDragging = false;
progressBar.classList.remove('dragging');
}
function updateProgress(e) {
if (!audioElement || !audioElement.duration) return;
try {
const rect = progressBar.getBoundingClientRect();
const x = e.clientX || e.pageX;
const percent = Math.min(Math.max((x - rect.left) / rect.width, 0), 1);
// Update progress bar and handle
progress.style.width = `${percent * 100}%`;
if (progressHandle) {
progressHandle.style.left = `${percent * 100}%`;
}
if (seekSlider) {
seekSlider.value = percent * 100;
}
// Update audio time
audioElement.currentTime = percent * audioElement.duration;
// Force time display update
updateTimeDisplay();
} catch (error) {
console.error('Error updating progress:', error);
}
}
// Add seek slider input handler
if (seekSlider) {
seekSlider.addEventListener('input', (e) => {
if (!audioElement || !audioElement.duration) return;
const percent = e.target.value;
progress.style.width = `${percent}%`;
if (progressHandle) {
progressHandle.style.left = `${percent}%`;
}
audioElement.currentTime = (percent / 100) * audioElement.duration;
updateTimeDisplay();
});
}
}
// Setup player controls
function setupPlayerControls() {
const playPauseBtn = document.getElementById('play-pause');
const prevBtn = document.querySelector('.previous-btn');
const nextBtn = document.querySelector('.next-btn');
const volumeSlider = document.getElementById('volume');
const seekSlider = document.querySelector('.seek-slider');
playPauseBtn.addEventListener('click', togglePlayPause);
prevBtn.addEventListener('click', playPrevious);
nextBtn.addEventListener('click', playNext);
volumeSlider.addEventListener('input', updateVolume);
// Add both input and change events for the seek slider
seekSlider.addEventListener('input', seekTo);
seekSlider.addEventListener('change', seekTo);
// Add keyboard controls
document.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
e.preventDefault();
togglePlayPause();
} else if (e.code === 'ArrowLeft') {
playPrevious();
} else if (e.code === 'ArrowRight') {
playNext();
}
});
// Update time display more frequently
if (audioElement) {
// Remove existing listeners first
audioElement.removeEventListener('timeupdate', updateTimeDisplay);
audioElement.removeEventListener('loadedmetadata', updateTimeDisplay);
audioElement.removeEventListener('ended', handleTrackEnd);
// Add listeners
audioElement.addEventListener('timeupdate', updateTimeDisplay);
audioElement.addEventListener('loadedmetadata', updateTimeDisplay);
audioElement.addEventListener('ended', handleTrackEnd);
// Force initial update
updateTimeDisplay();
}
}
// Toggle play/pause
function togglePlayPause() {
if (!audioElement) return;
if (audioElement.paused) {
audioElement.play();
isPlaying = true;
document.getElementById('play-pause').innerHTML = '<i class="fas fa-pause"></i>';
} else {
audioElement.pause();
isPlaying = false;
document.getElementById('play-pause').innerHTML = '<i class="fas fa-play"></i>';
}
}
// Play previous track
function playPrevious() {
if (playlist.length === 0) return;
let newIndex = currentTrackIndex - 1;
if (newIndex < 0) newIndex = playlist.length - 1;
playTrack(newIndex);
}
// Play next track
function playNext() {
if (playlist.length === 0) return;
let newIndex = currentTrackIndex + 1;
if (newIndex >= playlist.length) newIndex = 0;
playTrack(newIndex);
}
// Update volume
function updateVolume(e) {
if (audioElement) {
audioElement.volume = e.target.value;
const volumeProgress = document.querySelector('.volume-progress');
volumeProgress.style.width = `${e.target.value * 100}%`;
}
}
// Seek to position
function seekTo(e) {
if (!audioElement || !audioElement.duration) return;
const seekSlider = e.target;
const progress = document.querySelector('.progress');
const progressHandle = document.querySelector('.progress-handle');
const time = (seekSlider.value / 100) * audioElement.duration;
// Update audio time
audioElement.currentTime = time;
// Update progress bar and handle
if (progress) {
progress.style.width = `${seekSlider.value}%`;
}
if (progressHandle) {
progressHandle.style.left = `${seekSlider.value}%`;
}
// Force time display update
updateTimeDisplay();
}
// Update time display
function updateTimeDisplay() {
if (!audioElement) return;
const currentTimeEl = document.querySelector('.current-time');
const totalTimeEl = document.querySelector('.total-time');
const progress = document.querySelector('.progress');
const progressHandle = document.querySelector('.progress-handle');
const seekSlider = document.querySelector('.seek-slider');
// Only update if duration is available and not NaN
if (audioElement.duration && !isNaN(audioElement.duration)) {
const current = formatTime(audioElement.currentTime);
const total = formatTime(audioElement.duration);
const progressPercent = (audioElement.currentTime / audioElement.duration) * 100;
// Update time displays
if (currentTimeEl) currentTimeEl.textContent = current;
if (totalTimeEl) totalTimeEl.textContent = total;
// Update progress bar and handle
if (progress) {
progress.style.width = `${progressPercent}%`;
}
if (progressHandle) {
progressHandle.style.left = `${progressPercent}%`;
}
if (seekSlider && !seekSlider.matches(':active')) {
seekSlider.value = progressPercent;
}
} else {
// Reset displays if no duration available
if (currentTimeEl) currentTimeEl.textContent = '0:00';
if (totalTimeEl) totalTimeEl.textContent = '0:00';
if (progress) progress.style.width = '0%';
if (progressHandle) progressHandle.style.left = '0%';
if (seekSlider) seekSlider.value = 0;
}
}
// Format time helper function
function formatTime(seconds) {
if (!seconds || isNaN(seconds)) return '0:00';
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
// Handle track end
function handleTrackEnd() {
if (isRepeatActive) {
audioElement.currentTime = 0;
audioElement.play();
} else {
playNext();
}
}
// Create visualization
function createVisualization() {
// Clear existing visualization
while(scene.children.length > 0) {
scene.remove(scene.children[0]);
}
visualizers = {
bars: [],
sphere: null,
particles: null
};
// Set camera position based on visualization type
camera.position.z = visualizerSettings[visualizationType].cameraZ;
switch(visualizationType) {
case 'bars':
createBarsVisualization();
break;
case 'sphere':
createSphereVisualization();
break;
case 'particles':
createParticlesVisualization();
break;
}
}
// Update visualization
function updateVisualization(dataArray) {
switch(visualizationType) {
case 'bars':
updateBarsVisualization(dataArray);
break;
case 'sphere':
updateSphereVisualization(dataArray);
break;
case 'particles':
updateParticlesVisualization(dataArray);
break;
}
}
// Create playlist UI
function createPlaylist() {
const tracksList = document.querySelector('.tracks-list');
const noTracksMessage = document.querySelector('.no-tracks-message');
// Clear existing tracks
tracksList.innerHTML = '';
if (!playlist || playlist.length === 0) {
// Show no tracks message if playlist is empty
if (noTracksMessage) {
noTracksMessage.style.display = 'flex';
}
return;
}
// Hide no tracks message if we have tracks
if (noTracksMessage) {
noTracksMessage.style.display = 'none';
}
// Create track elements
playlist.forEach((track, index) => {
const metadata = track.metadata || {};
const duration = metadata.duration ? formatTime(metadata.duration) : '0:00';
const artist = metadata.artist || 'Unknown Artist';
const title = metadata.title || track.name;
const trackElement = document.createElement('div');
trackElement.className = 'playlist-item';
trackElement.innerHTML = `
<div class="track-info">
<span class="track-number">${index + 1}</span>
<div class="track-artwork">
${metadata.artwork ?
`<img src="data:${metadata.artwork.mime};base64,${metadata.artwork.data}" alt="Album artwork">` :
'<i class="fas fa-music"></i>'
}
</div>
<div class="track-content">
<div class="track-title">
${title}
<span class="track-duration">${duration}</span>
</div>
<div class="track-metadata">
${artist}
${metadata.album ? ` • ${metadata.album}` : ''}
</div>
</div>
</div>
`;
trackElement.addEventListener('click', () => playTrack(index));
tracksList.appendChild(trackElement);
});
}
// Update now playing information
function updateNowPlayingInfo() {
if (currentTrackIndex >= 0 && currentTrackIndex < playlist.length) {
const track = playlist[currentTrackIndex];
const metadata = track.metadata || {};
const nowPlayingTitle = document.querySelector('.now-playing-title');
const nowPlayingArtist = document.querySelector('.now-playing-artist');
const trackArtwork = document.querySelector('.now-playing-info .track-artwork');
if (nowPlayingTitle) {
nowPlayingTitle.textContent = metadata.title || track.name;
}
if (nowPlayingArtist) {
let artistInfo = metadata.artist || 'Unknown Artist';
if (metadata.album) {
artistInfo += ` • ${metadata.album}`;
}
nowPlayingArtist.textContent = artistInfo;
}
if (trackArtwork) {
trackArtwork.innerHTML = metadata.artwork ?
`<img src="data:${metadata.artwork.mime};base64,${metadata.artwork.data}" alt="Album artwork">` :
'<i class="fas fa-music"></i>';
}
}
}
// Update playTrack function to properly set up time updates
async function playTrack(index) {
if (index < 0 || index >= playlist.length) return;
try {
// Stop current playback
if (audioElement) {
audioElement.pause();
audioElement.currentTime = 0;
// Remove existing listeners
audioElement.removeEventListener('timeupdate', updateTimeDisplay);
audioElement.removeEventListener('loadedmetadata', updateTimeDisplay);
audioElement.removeEventListener('ended', handleTrackEnd);
}
currentTrackIndex = index;
const track = playlist[currentTrackIndex];
if (!audioElement) {
console.warn('Audio element not initialized');
return;
}
// Update source and load new track
audioElement.src = track.url;
await audioElement.load();
// Add event listeners for time updates
audioElement.addEventListener('timeupdate', updateTimeDisplay);
audioElement.addEventListener('loadedmetadata', updateTimeDisplay);
audioElement.addEventListener('ended', handleTrackEnd);
// Update playlist UI
document.querySelectorAll('.playlist-item').forEach((item, i) => {
item.classList.toggle('active', i === index);
});
// Force initial time display update
updateTimeDisplay();
// Attempt to play with retry logic
try {
await audioElement.play();
isPlaying = true;
updatePlayPauseButton();
updateNowPlayingInfo();
} catch (playError) {
console.warn('Play interrupted, retrying...', playError);
// Add a small delay before retrying
setTimeout(async () => {
try {
await audioElement.play();
isPlaying = true;
updatePlayPauseButton();
updateNowPlayingInfo();
} catch (retryError) {
console.error('Failed to play after retry:', retryError);
showError('Failed to play track. Please try again.');
}
}, 100);
}
} catch (error) {
console.error('Error playing track:', error);
showError('Error playing track');
}
}
// Add these functions after the existing initialization code
function setupToggleHandlers() {
const mainContent = document.querySelector('.main-content');
const uploadArea = document.querySelector('.upload-area');
const playlistContainer = document.querySelector('.playlist-container');
const uploadToggleBtn = document.querySelector('.upload-toggle-btn');
const playlistToggleBtn = document.querySelector('.playlist-toggle-btn');
const vizTypeBtn = document.querySelector('.viz-type-btn');
const vizTypeDropdown = document.querySelector('.viz-type-dropdown');
if (!mainContent || !uploadArea || !playlistContainer) {
console.error('Required elements not found');
return;
}
// Show playlist by default
mainContent.classList.add('visible');
playlistContainer.classList.add('visible');
if (playlistToggleBtn) playlistToggleBtn.classList.add('active');
// Upload button handler
uploadToggleBtn?.addEventListener('click', () => {
const isVisible = uploadArea.classList.contains('visible');
// Hide playlist if it's visible
playlistContainer.classList.remove('visible');
playlistToggleBtn?.classList.remove('active');
// Toggle upload area
uploadArea.classList.toggle('visible');
uploadToggleBtn.classList.toggle('active');
// Show/hide main content
mainContent.classList.toggle('visible', !isVisible || playlistContainer.classList.contains('visible'));
});
// Playlist button handler
playlistToggleBtn?.addEventListener('click', () => {
const isVisible = playlistContainer.classList.contains('visible');
// Hide upload area if it's visible
uploadArea.classList.remove('visible');
uploadToggleBtn?.classList.remove('active');
// Toggle playlist
playlistContainer.classList.toggle('visible');
playlistToggleBtn.classList.toggle('active');
// Show/hide main content
mainContent.classList.toggle('visible', !isVisible || uploadArea.classList.contains('visible'));
});
// Visualization type button handler
let isVizDropdownVisible = false;
vizTypeBtn?.addEventListener('click', (e) => {
e.stopPropagation();
isVizDropdownVisible = !isVizDropdownVisible;
if (isVizDropdownVisible) {
vizTypeDropdown?.classList.add('visible');
vizTypeBtn.classList.add('active');
} else {
vizTypeDropdown?.classList.remove('visible');
vizTypeBtn.classList.remove('active');
}
});
// Handle visualization type selection
const vizTypeOptions = document.querySelectorAll('.viz-type-options button');
vizTypeOptions?.forEach(button => {
button.addEventListener('click', (e) => {
e.stopPropagation();
const type = button.dataset.type;
// Update active state
vizTypeOptions.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
// Update visualization
visualizationType = type;
createVisualization();
// Close dropdown
isVizDropdownVisible = false;
vizTypeDropdown?.classList.remove('visible');
vizTypeBtn?.classList.remove('active');
});
});
// Close dropdowns when clicking outside
document.addEventListener('click', (e) => {
if (isVizDropdownVisible &&
vizTypeDropdown &&
!vizTypeDropdown.contains(e.target) &&
!vizTypeBtn?.contains(e.target)) {
isVizDropdownVisible = false;
vizTypeDropdown.classList.remove('visible');
vizTypeBtn?.classList.remove('active');
}
});
}
// Add updatePlayPauseButton function
function updatePlayPauseButton() {
const playPauseBtn = document.getElementById('play-pause');
if (!playPauseBtn) return;
playPauseBtn.innerHTML = isPlaying ?
'<i class="fas fa-pause"></i>' :
'<i class="fas fa-play"></i>';
// Update button state
playPauseBtn.disabled = !audioElement || !playlist.length;
}
// Setup shuffle and repeat buttons
function setupPlaylistControls() {
const shuffleBtn = document.querySelector('.shuffle-btn');
const repeatBtn = document.querySelector('.repeat-btn');
// Setup shuffle button
shuffleBtn?.addEventListener('click', () => {
isShuffleActive = !isShuffleActive;
shuffleBtn.classList.toggle('active', isShuffleActive);
if (isShuffleActive) {
// Save current track index
const currentTrack = playlist[currentTrackIndex];
// Shuffle playlist
for (let i = playlist.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[playlist[i], playlist[j]] = [playlist[j], playlist[i]];
}
// Find new index of current track
currentTrackIndex = playlist.findIndex(track => track === currentTrack);
// Update playlist UI
createPlaylist();
updateNowPlayingInfo();
} else {
// Restore original order if needed
playlist.sort((a, b) => a.originalIndex - b.originalIndex);
createPlaylist();
}
});
// Setup repeat button
repeatBtn?.addEventListener('click', () => {
isRepeatActive = !isRepeatActive;
repeatBtn.classList.toggle('active', isRepeatActive);
});
}
// Update handleFiles function for faster uploads
async function handleFiles(files) {
console.log('Handling files:', files.length, 'files');
if (!files || files.length === 0) {
console.warn('No files selected');
showError('No files selected');
return;
}
if (!audioContext) {
try {
console.log('Initializing audio context...');
initAudio();
} catch (error) {
console.error('Failed to initialize audio:', error);
showError('Failed to initialize audio system');
return;
}
}
const formData = new FormData();
const totalSize = Array.from(files).reduce((acc, file) => acc + file.size, 0);
let uploadedSize = 0;
// Add files to FormData with optimized chunk size
Array.from(files).forEach(file => {
console.log('Adding file to upload:', file.name);
formData.append('files[]', file);
});
// Show upload progress
const uploadProgress = document.querySelector('.upload-progress');
const progressFill = uploadProgress?.querySelector('.progress-fill');
const progressText = uploadProgress?.querySelector('.progress-text');
if (uploadProgress && progressFill && progressText) {
uploadProgress.classList.add('visible');
progressFill.style.width = '0%';
progressText.textContent = '0%';
}
try {
console.log('Starting file upload...');
const response = await fetch('/upload', {
method: 'POST',
body: formData,
headers: sessionId ? {
'X-Session-ID': sessionId
} : {}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('Upload response:', data);
if (data.success) {
console.log('Files uploaded successfully');
showSuccess('Files uploaded successfully');
const successfulFiles = data.files.filter(file => file.success);
console.log('Successful uploads:', successfulFiles.length, 'files');
if (successfulFiles.length === 0) {
console.warn('No files were uploaded successfully');
showError('No files were uploaded successfully');
return;
}
// Get the next index for new tracks
const startIndex = playlist.length;
// Create new track objects
const newTracks = successfulFiles.map((file, index) => ({
name: file.filename,
url: file.filepath,
metadata: file.metadata,
originalIndex: startIndex + index
}));
// Append new tracks to existing playlist
playlist = [...playlist, ...newTracks];
console.log('Updated playlist:', playlist);
createPlaylist();
// Only start playing if nothing is currently playing
if (playlist.length > 0 && !isPlaying) {
console.log('Playing first track...');
playTrack(0);
}
// Enable player controls
document.querySelectorAll('.control-btn').forEach(btn => {
btn.disabled = false;
});
// Highlight currently playing track if any
if (currentTrackIndex >= 0) {
document.querySelectorAll('.playlist-item').forEach((item, i) => {
item.classList.toggle('active', i === currentTrackIndex);
});
}
// Store session ID if we got one
if (data.session_id) {
sessionId = data.session_id;
localStorage.setItem('audioSessionId', sessionId);
}
} else {
console.error('Upload failed:', data.error);
showError(data.error || 'Upload failed');
}
} catch (error) {
console.error('Upload error:', error);
showError('Error uploading files');
} finally {
if (uploadProgress) {
uploadProgress.classList.remove('visible');
}
}
}
// Update volume control functions
function setupVolumeControl() {
const volumeBtn = document.querySelector('.volume-btn');
const volumeSlider = document.getElementById('volume');
const volumeProgress = document.querySelector('.volume-progress');
const volumeHandle = document.querySelector('.volume-handle');
if (!volumeBtn || !volumeSlider || !volumeProgress || !volumeHandle) {
console.error('Volume control elements not found');
return;
}
// Initialize volume
let currentVolume = localStorage.getItem('volume') || 0.5;
updateVolume(currentVolume);
// Update volume on slider change
volumeSlider.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
updateVolume(value);
});
// Update volume on button click (mute/unmute)
volumeBtn.addEventListener('click', () => {
if (audioElement) {
if (audioElement.volume > 0) {
// Store current volume before muting
localStorage.setItem('previousVolume', audioElement.volume);
updateVolume(0);
} else {
// Restore previous volume or default to 0.5
const previousVolume = localStorage.getItem('previousVolume') || 0.5;
updateVolume(previousVolume);
}
}
});
function updateVolume(value) {
// Update audio element
if (audioElement) {
audioElement.volume = value;
}
// Update UI
volumeProgress.style.width = `${value * 100}%`;
volumeHandle.style.left = `${value * 100}%`;
volumeSlider.value = value;
// Update button icon
const icon = volumeBtn.querySelector('i');
if (icon) {
if (value === 0) {
icon.className = 'fas fa-volume-mute';
} else if (value < 0.5) {
icon.className = 'fas fa-volume-down';
} else {
icon.className = 'fas fa-volume-up';
}
}
// Save volume to localStorage
localStorage.setItem('volume', value);
}
}
// Add to initialization
document.addEventListener('DOMContentLoaded', () => {
// ... other initialization code ...
setupVolumeControl();
});
// Add session management
let sessionId = localStorage.getItem('audioSessionId');
// Add cleanup on page unload
window.addEventListener('beforeunload', async () => {
if (sessionId) {
try {
await fetch('/end-session', {
method: 'POST',
headers: {
'X-Session-ID': sessionId
}
});
localStorage.removeItem('audioSessionId');
} catch (error) {
console.error('Error ending session:', error);
}
}
});