Audio Visualizer

 

`; container.appendChild(canvas); container.appendChild(controlsDiv); const ctx = canvas.getContext('2d'); const startButton = document.getElementById('start-btn'); const fullscreenButton = document.getElementById('fullscreen-btn'); const bgColorPicker = document.getElementById('bg-color'); const drawColorPicker = document.getElementById('draw-color'); const sensitivitySlider = document.getElementById('sensitivity-slider'); const visualizerSelect = document.getElementById('visualizer-select'); let audioContext, analyser, source; let currentIndex = 0; const config = { bgColor: '#000000', drawColor: '#ffffff', sensitivity: 1.0, cymaBands: 8, cymaRotationSpeed: 0.3 }; const visualizers = [ { name: 'VU Meter', draw: function() { const bufferLength = analyser.fftSize; const dataArray = new Uint8Array(bufferLength); analyser.getByteTimeDomainData(dataArray); const scaledData = dataArray.map(v => v * config.sensitivity); const rms = calculateRMS(scaledData); const barHeight = rms * canvas.height; ctx.fillStyle = config.drawColor; ctx.fillRect(0, canvas.height - barHeight, canvas.width, barHeight); } }, { name: 'Waveform', draw: function() { const bufferLength = analyser.fftSize; const dataArray = new Uint8Array(bufferLength); analyser.getByteTimeDomainData(dataArray); const scaledData = dataArray.map(v => (v * config.sensitivity) % 256); ctx.strokeStyle = config.drawColor; ctx.lineWidth = 2; ctx.beginPath(); const sliceWidth = canvas.width / bufferLength; let x = 0; for (let i = 0; i < bufferLength; i++) { const y = (scaledData[i] / 128.0) * canvas.height/2; i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); x += sliceWidth; } ctx.stroke(); } }, { name: 'Frequency Spectrum', draw: function() { const bufferLength = analyser.frequencyBinCount; const dataArray = new Uint8Array(bufferLength); analyser.getByteFrequencyData(dataArray); const scaledData = dataArray.map(v => Math.min(255, v * config.sensitivity)); const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0); gradient.addColorStop(0, config.drawColor); gradient.addColorStop(1, darkenColor(config.drawColor, 30)); const barWidth = (canvas.width / bufferLength) * 2.5; let x = 0; for (let i = 0; i < bufferLength; i++) { const barHeight = (scaledData[i] / 255) * canvas.height; ctx.fillStyle = gradient; ctx.fillRect(x, canvas.height - barHeight, barWidth - 1, barHeight); x += barWidth; } } }, { name: 'Pulsing Circle', draw: function() { const bufferLength = analyser.fftSize; const dataArray = new Uint8Array(bufferLength); analyser.getByteTimeDomainData(dataArray); const scaledData = dataArray.map(v => v * config.sensitivity); const rms = calculateRMS(scaledData); const radius = rms * Math.min(canvas.width, canvas.height) * 0.5; ctx.beginPath(); ctx.arc(canvas.width/2, canvas.height/2, radius, 0, Math.PI*2); ctx.fillStyle = config.drawColor; ctx.fill(); } }, { name: 'Cymascope', rings: [], connections: [], lastPeaks: [], draw: function() { const bufferLength = analyser.frequencyBinCount; const dataArray = new Uint8Array(bufferLength); analyser.getByteFrequencyData(dataArray); const scaledData = dataArray.map(v => Math.min(255, v * config.sensitivity)); const centerX = canvas.width/2; const centerY = canvas.height/2; const maxRadius = Math.min(centerX, centerY) * 0.8; // Frequency rings const frequencyRanges = [ { min: 20, max: 250 }, // Bass { min: 250, max: 2000 }, // Mid { min: 2000, max: 20000 } // Treble ]; frequencyRanges.forEach((range, i) => { const avg = averageInRange(scaledData, freqToIndex(range.min), freqToIndex(range.max) ); if(!this.rings[i]) this.rings[i] = { radius: 0, energy: 0 }; this.rings[i].energy = avg/255; this.rings[i].radius = maxRadius * (0.2 + (0.3 * this.rings[i].energy)); // Draw main ring ctx.beginPath(); ctx.arc(centerX, centerY, this.rings[i].radius, 0, Math.PI*2); ctx.strokeStyle = `${config.drawColor}${Math.round(0.8 * 255).toString(16).padStart(2, '0')}`; ctx.lineWidth = 2; ctx.stroke(); // Ripple effect if(this.rings[i].energy > 0.7) { ctx.beginPath(); ctx.arc(centerX, centerY, this.rings[i].radius * 1.5, 0, Math.PI*2); ctx.strokeStyle = `${config.drawColor}${Math.round(0.3 * 255).toString(16).padStart(2, '0')}`; ctx.lineWidth = 1; ctx.stroke(); } }); // Harmonic detection const peaks = detectPeaks(scaledData); this.connections = detectHarmonicRelations(peaks); // Draw connections this.connections.forEach(conn => { const angle1 = (conn.f1 / 22000) * Math.PI * 2; const angle2 = (conn.f2 / 22000) * Math.PI * 2; const radius1 = maxRadius * (0.2 + 0.6 * (conn.amp1/255)); const radius2 = maxRadius * (0.2 + 0.6 * (conn.amp2/255)); ctx.beginPath(); ctx.moveTo( centerX + Math.cos(angle1) * radius1, centerY + Math.sin(angle1) * radius1 ); ctx.lineTo( centerX + Math.cos(angle2) * radius2, centerY + Math.sin(angle2) * radius2 ); ctx.strokeStyle = config.drawColor; ctx.lineWidth = 1 + (conn.strength * 3); ctx.stroke(); }); // Interference patterns if(this.connections.length > 0) { const patternSize = maxRadius * 0.4; ctx.beginPath(); this.connections.forEach((conn, i) => { const angle = (conn.f1 / 22000) * Math.PI * 2; const x = centerX + Math.cos(angle) * patternSize; const y = centerY + Math.sin(angle) * patternSize; if(i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.closePath(); ctx.strokeStyle = config.drawColor; ctx.lineWidth = 2; ctx.stroke(); } } } ]; // Populate visualizer select options visualizers.forEach((vis, index) => { const option = document.createElement('option'); option.value = index; option.textContent = vis.name; visualizerSelect.appendChild(option); }); // Helper functions function calculateRMS(dataArray) { let sum = dataArray.reduce((acc, val) => { const v = (val - 128)/128; return acc + v*v; }, 0); return Math.sqrt(sum/dataArray.length); } function darkenColor(color, amount) { const col = color.replace('#', ''); const num = parseInt(col, 16); return '#' + [num >> 16, num >> 8 & 0xFF, num & 0xFF] .map(c => Math.max(c - amount, 0).toString(16).padStart(2, '0')) .join(''); } function averageInRange(arr, start, end) { let sum = 0; for(let i = start; i <= end; i++) sum += arr[i]; return sum/(end - start + 1); } function freqToIndex(freq) { return Math.floor((freq / (audioContext?.sampleRate / 2)) * analyser.frequencyBinCount); } function detectPeaks(dataArray, threshold = 0.5) { const peaks = []; const minDistance = 10; let lastPeakIndex = -minDistance; for(let i = 1; i < dataArray.length - 1; i++) { if(dataArray[i] > dataArray[i-1] && dataArray[i] > dataArray[i+1] && dataArray[i] > 255 * threshold && i - lastPeakIndex > minDistance) { peaks.push({ index: i, frequency: (i * (audioContext.sampleRate / 2)) / analyser.frequencyBinCount, amplitude: dataArray[i] }); lastPeakIndex = i; } } return peaks; } function detectHarmonicRelations(peaks) { const connections = []; const harmonicRatios = [ { ratio: 2/1, tolerance: 0.02 }, { ratio: 3/2, tolerance: 0.03 }, { ratio: 4/3, tolerance: 0.03 }, { ratio: 5/4, tolerance: 0.05 } ]; for(let i = 0; i < peaks.length; i++) { for(let j = i+1; j < peaks.length; j++) { const f1 = peaks[i].frequency; const f2 = peaks[j].frequency; const ratio = f1 > f2 ? f1/f2 : f2/f1; harmonicRatios.forEach(({ ratio: target, tolerance }) => { if(Math.abs(ratio - target) < tolerance) { connections.push({ f1: f1, f2: f2, amp1: peaks[i].amplitude, amp2: peaks[j].amplitude, strength: 1 - (Math.abs(ratio - target)/tolerance) }); } }); } } return connections; } // Event listeners startButton.addEventListener('click', async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); audioContext = new AudioContext(); source = audioContext.createMediaStreamSource(stream); analyser = audioContext.createAnalyser(); analyser.fftSize = 2048; source.connect(analyser); updateVisualizerSelection(); resizeCanvas(); drawVisualizer(); } catch (error) { alert('Microphone access required: ' + error.message); } }); fullscreenButton.addEventListener('click', () => { container.classList.toggle('fullscreen'); container.requestFullscreen?.().catch(console.log); resizeCanvas(); }); bgColorPicker.addEventListener('input', (e) => config.bgColor = e.target.value); drawColorPicker.addEventListener('input', (e) => config.drawColor = e.target.value); sensitivitySlider.addEventListener('input', (e) => { config.sensitivity = parseFloat(e.target.value); }); visualizerSelect.addEventListener('change', (e) => { currentIndex = parseInt(e.target.value); updateVisualizerSelection(); }); document.addEventListener('keydown', (e) => { if (e.key === 'ArrowLeft') { currentIndex = (currentIndex - 1 + visualizers.length) % visualizers.length; } else if (e.key === 'ArrowRight') { currentIndex = (currentIndex + 1) % visualizers.length; } updateVisualizerSelection(); }); // Core functionality function updateVisualizerSelection() { visualizerSelect.value = currentIndex; } function resizeCanvas() { canvas.width = container.clientWidth; canvas.height = container.clientHeight; } function drawVisualizer() { ctx.fillStyle = config.bgColor; ctx.fillRect(0, 0, canvas.width, canvas.height); if(visualizers[currentIndex].name !== 'Cymascope') { ctx.fillStyle = 'rgba(0, 0, 0, 0.1)'; ctx.fillRect(0, 0, canvas.width, canvas.height); } visualizers[currentIndex].draw(); requestAnimationFrame(drawVisualizer); } window.addEventListener('resize', resizeCanvas); resizeCanvas(); updateVisualizerSelection(); })();

Categories: Scrolls
Share on:

← Back Home