export class Visualizer { constructor(audioContext, canvasId) { this.canvas = document.getElementById(canvasId); this.ctx = this.canvas.getContext('2d'); this.analyser = audioContext.createAnalyser(); // Ensure fftSize is a valid power of two (16384 is the max standard value) this.analyser.fftSize = 16384; this.bufferLength = this.analyser.frequencyBinCount; this.dataArray = new Uint8Array(this.bufferLength); this.sampleRate = audioContext.sampleRate; this.binWidth = this.sampleRate / this.analyser.fftSize; // Adjusted frequency range this.startBin = Math.floor(20 / this.binWidth); this.endBin = Math.floor(200 / this.binWidth); this.usableBins = this.endBin - this.startBin; // Slightly reduced smoothing for faster response this.analyser.smoothingTimeConstant = 0.45; // Reduced from 0.5 // Peak tracking with slightly more dynamic range this.peakLevels = new Array(this.usableBins).fill(0); this.peakDecayRate = 0.85; // Slightly faster decay this.dynamicPeakMultiplier = 1.25; // Increased from 1.2 this.resize(); window.addEventListener('resize', () => this.resize()); } resize() { const dpr = window.devicePixelRatio || 1; this.canvas.width = this.canvas.offsetWidth * dpr; this.canvas.height = this.canvas.offsetHeight * dpr; this.ctx.scale(dpr, dpr); this.canvas.style.width = this.canvas.offsetWidth + 'px'; this.canvas.style.height = this.canvas.offsetHeight + 'px'; } getAverageAmplitude() { let sum = 0; for (let i = this.startBin; i < this.endBin; i++) { sum += this.dataArray[i]; } // Slightly increased sensitivity return Math.min((sum / this.usableBins / 255) * 0.75, 1); } draw() { this.analyser.getByteFrequencyData(this.dataArray); const width = this.canvas.offsetWidth; const height = this.canvas.offsetHeight; // Slightly more transparent fade effect with a color closer to the background this.ctx.fillStyle = 'rgba(31, 3, 107, 0.3)'; this.ctx.fillRect(0, 0, width, height); const barWidth = (width / this.usableBins) * 0.9; const spacing = barWidth * 0.1; let x = (width - (this.usableBins * (barWidth + spacing))) / 2; for (let i = this.startBin; i < this.endBin; i++) { const index = i - this.startBin; const currentValue = this.dataArray[i]; // More controlled peak tracking this.peakLevels[index] = Math.max( currentValue, Math.min( this.peakLevels[index] * this.peakDecayRate, currentValue * this.dynamicPeakMultiplier ) ); // Slightly reduced peak height scaling const barHeight = Math.min( (this.peakLevels[index] * 0.9) * (height / 255), height ); const gradient = this.ctx.createLinearGradient(0, height, 0, height - barHeight); gradient.addColorStop(0, 'rgba(106, 74, 194, 0.8)'); // Matching the end color of background gradient.addColorStop(1, 'rgba(31, 3, 107, 0.4)'); // Matching the start color of background this.ctx.fillStyle = gradient; this.ctx.beginPath(); this.ctx.roundRect( x, height - barHeight, barWidth, barHeight, 3 ); this.ctx.fill(); x += barWidth + spacing; } return this.getAverageAmplitude(); } }