/** * AUDIO VISUALIZER - Waveform & Spectrum Analysis * Creates stunning audio-reactive visualizations */ class AudioVisualizer { constructor(canvasId) { this.canvas = document.getElementById(canvasId); if (!this.canvas) { console.error('Canvas not found:', canvasId); return; } this.ctx = this.canvas.getContext('2d'); this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); this.analyser = this.audioContext.createAnalyser(); this.analyser.fftSize = 2048; this.bufferLength = this.analyser.frequencyBinCount; this.dataArray = new Uint8Array(this.bufferLength); this.waveformData = new Uint8Array(this.bufferLength); this.visualizationMode = 'spectrum'; // spectrum, waveform, circular, bars this.colors = { primary: [0, 255, 204], secondary: [255, 0, 255], accent: [255, 255, 0] }; this.particles = []; this.isActive = false; this.resize(); window.addEventListener('resize', () => this.resize()); console.log('🎨 Audio Visualizer initialized'); } /** * Connect audio source (microphone, audio element, etc.) */ async connectMicrophone() { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const source = this.audioContext.createMediaStreamSource(stream); source.connect(this.analyser); this.isActive = true; this.render(); console.log('🎤 Microphone connected'); } catch (error) { console.error('Microphone access denied:', error); } } connectAudioElement(audioElement) { const source = this.audioContext.createMediaElementSource(audioElement); source.connect(this.analyser); this.analyser.connect(this.audioContext.destination); this.isActive = true; this.render(); console.log('🔊 Audio element connected'); } /** * Render visualizations */ render() { if (!this.isActive) return; requestAnimationFrame(() => this.render()); this.analyser.getByteFrequencyData(this.dataArray); this.analyser.getByteTimeDomainData(this.waveformData); this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); switch (this.visualizationMode) { case 'spectrum': this.drawSpectrum(); break; case 'waveform': this.drawWaveform(); break; case 'circular': this.drawCircular(); break; case 'bars': this.drawBars(); break; case 'particles': this.drawParticles(); break; } } /** * Spectrum Analyzer - Frequency bars */ drawSpectrum() { const width = this.canvas.width; const height = this.canvas.height; const barWidth = width / this.bufferLength * 2.5; let x = 0; for (let i = 0; i < this.bufferLength; i++) { const barHeight = (this.dataArray[i] / 255) * height; const hue = (i / this.bufferLength) * 360; this.ctx.fillStyle = `hsla(${hue}, 100%, 50%, 0.8)`; // Draw bar with gradient const gradient = this.ctx.createLinearGradient(0, height, 0, height - barHeight); gradient.addColorStop(0, `hsla(${hue}, 100%, 50%, 0.8)`); gradient.addColorStop(1, `hsla(${hue}, 100%, 70%, 0.4)`); this.ctx.fillStyle = gradient; this.ctx.fillRect(x, height - barHeight, barWidth, barHeight); x += barWidth + 1; } } /** * Waveform - Time-domain visualization */ drawWaveform() { const width = this.canvas.width; const height = this.canvas.height; this.ctx.lineWidth = 3; this.ctx.strokeStyle = `rgba(${this.colors.primary.join(',')}, 0.8)`; this.ctx.beginPath(); const sliceWidth = width / this.bufferLength; let x = 0; for (let i = 0; i < this.bufferLength; i++) { const v = this.waveformData[i] / 128.0; const y = v * height / 2; if (i === 0) { this.ctx.moveTo(x, y); } else { this.ctx.lineTo(x, y); } x += sliceWidth; } this.ctx.lineTo(width, height / 2); this.ctx.stroke(); // Add glow effect this.ctx.shadowBlur = 20; this.ctx.shadowColor = `rgba(${this.colors.primary.join(',')}, 0.8)`; this.ctx.stroke(); this.ctx.shadowBlur = 0; } /** * Circular Visualization - Radial spectrum */ drawCircular() { const width = this.canvas.width; const height = this.canvas.height; const centerX = width / 2; const centerY = height / 2; const radius = Math.min(width, height) / 3; for (let i = 0; i < this.bufferLength; i++) { const value = this.dataArray[i]; const percent = value / 255; const angle = (i / this.bufferLength) * Math.PI * 2; const barLength = radius * percent; const x1 = centerX + Math.cos(angle) * radius; const y1 = centerY + Math.sin(angle) * radius; const x2 = centerX + Math.cos(angle) * (radius + barLength); const y2 = centerY + Math.sin(angle) * (radius + barLength); const hue = (i / this.bufferLength) * 360; this.ctx.strokeStyle = `hsla(${hue}, 100%, 50%, 0.8)`; this.ctx.lineWidth = 3; this.ctx.beginPath(); this.ctx.moveTo(x1, y1); this.ctx.lineTo(x2, y2); this.ctx.stroke(); } // Draw center circle this.ctx.beginPath(); this.ctx.arc(centerX, centerY, radius, 0, Math.PI * 2); this.ctx.strokeStyle = `rgba(${this.colors.primary.join(',')}, 0.5)`; this.ctx.lineWidth = 2; this.ctx.stroke(); } /** * 3D-style Bars */ drawBars() { const width = this.canvas.width; const height = this.canvas.height; const barCount = 64; const barWidth = width / barCount; for (let i = 0; i < barCount; i++) { const value = this.dataArray[i * Math.floor(this.bufferLength / barCount)]; const barHeight = (value / 255) * height; const x = i * barWidth; const hue = (i / barCount) * 360; // 3D effect with multiple layers for (let layer = 0; layer < 3; layer++) { const offset = layer * 3; const alpha = 1 - (layer * 0.3); const gradient = this.ctx.createLinearGradient(0, height, 0, height - barHeight); gradient.addColorStop(0, `hsla(${hue}, 100%, 50%, ${alpha})`); gradient.addColorStop(1, `hsla(${hue}, 100%, 70%, ${alpha * 0.5})`); this.ctx.fillStyle = gradient; this.ctx.fillRect( x + offset, height - barHeight + offset, barWidth - 2, barHeight ); } } } /** * Particle System - Audio-reactive particles */ drawParticles() { const width = this.canvas.width; const height = this.canvas.height; // Create new particles based on audio const avg = this.dataArray.reduce((a, b) => a + b, 0) / this.bufferLength; if (avg > 50 && this.particles.length < 200) { for (let i = 0; i < 5; i++) { this.particles.push({ x: Math.random() * width, y: height, vx: (Math.random() - 0.5) * 4, vy: -Math.random() * 5 - 2, size: Math.random() * 4 + 2, life: 1.0, color: [ Math.floor(Math.random() * 255), Math.floor(Math.random() * 255), Math.floor(Math.random() * 255) ] }); } } // Update and draw particles this.particles = this.particles.filter(particle => { particle.x += particle.vx; particle.y += particle.vy; particle.vy += 0.1; // Gravity particle.life -= 0.01; if (particle.life <= 0 || particle.y > height) return false; this.ctx.beginPath(); this.ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2); this.ctx.fillStyle = `rgba(${particle.color.join(',')}, ${particle.life})`; this.ctx.fill(); // Glow effect this.ctx.shadowBlur = 15; this.ctx.shadowColor = `rgba(${particle.color.join(',')}, ${particle.life})`; this.ctx.fill(); this.ctx.shadowBlur = 0; return true; }); // Draw waveform background this.ctx.globalAlpha = 0.3; this.drawWaveform(); this.ctx.globalAlpha = 1.0; } /** * Change visualization mode */ setMode(mode) { this.visualizationMode = mode; } /** * Get current audio levels */ getAudioLevel() { const avg = this.dataArray.reduce((a, b) => a + b, 0) / this.bufferLength; return avg / 255; } /** * Get dominant frequency */ getDominantFrequency() { let maxIndex = 0; let maxValue = 0; for (let i = 0; i < this.bufferLength; i++) { if (this.dataArray[i] > maxValue) { maxValue = this.dataArray[i]; maxIndex = i; } } const nyquist = this.audioContext.sampleRate / 2; const frequency = (maxIndex / this.bufferLength) * nyquist; return frequency; } /** * Resize canvas */ resize() { this.canvas.width = this.canvas.clientWidth * window.devicePixelRatio; this.canvas.height = this.canvas.clientHeight * window.devicePixelRatio; } /** * Start/stop visualization */ start() { this.isActive = true; this.render(); } stop() { this.isActive = false; } } // Export if (typeof module !== 'undefined' && module.exports) { module.exports = AudioVisualizer; }