export class Equalizer { constructor(audioElement, canvasElement) { this.audioElement = audioElement; this.canvas = canvasElement; this.canvasCtx = this.canvas.getContext('2d'); this.FREQUENCIES = [60, 170, 310, 600, 1000, 3000, 6000, 12000, 14000, 16000]; this.PRESETS = { flat: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], rock: [5, 4, -5, -8, -4, 4, 8, 9, 9, 9], pop: [-2, -1, 0, 2, 4, 4, 2, 0, -1, -2], jazz: [4, 3, 1, 2, -2, -2, 0, 1, 3, 4], classical: [5, 4, 3, -2, -3, -5, -2, 2, 4, 5], bass_boost:[9, 7, 4, 1, -2, -4, -4, -3, -2, -2] }; this.audioCtx = null; this.source = null; this.preamp = null; this.filters = []; this.analyser = null; this.animationFrameId = null; } init() { try { this.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); this.source = this.audioCtx.createMediaElementSource(this.audioElement); this.preamp = this.audioCtx.createGain(); this.filters = this.FREQUENCIES.map(freq => { const filter = this.audioCtx.createBiquadFilter(); filter.type = 'peaking'; filter.frequency.value = freq; filter.Q.value = 1.41; filter.gain.value = 0; return filter; }); this.analyser = this.audioCtx.createAnalyser(); this.analyser.fftSize = 256; // Connect nodes: source -> preamp -> filters -> analyser -> destination this.source.connect(this.preamp); let lastNode = this.preamp; this.filters.forEach(filter => { lastNode.connect(filter); lastNode = filter; }); lastNode.connect(this.analyser); this.analyser.connect(this.audioCtx.destination); this.drawVisualizer(); return true; } catch (e) { return false; } } changeGain(bandIndex, value) { if (this.filters[bandIndex]) { this.filters[bandIndex].gain.value = value; } } changePreamp(value) { if (this.preamp) { this.preamp.gain.value = Math.pow(10, value / 20); } } applyPreset(name) { const values = this.PRESETS[name]; if (!values) return; const bandSliders = document.querySelectorAll('.band-slider'); values.forEach((val, i) => { this.changeGain(i, val); const slider = bandSliders[i]; if (slider) { slider.value = val; slider.nextElementSibling.textContent = `${val} dB`; // GSAP animation for slider thumb gsap.to(slider, { duration: 0.4, value: val, ease: "power2.out" }); } }); } drawVisualizer() { this.animationFrameId = requestAnimationFrame(() => this.drawVisualizer()); if (!this.analyser) return; const bufferLength = this.analyser.frequencyBinCount; const dataArray = new Uint8Array(bufferLength); this.analyser.getByteFrequencyData(dataArray); this.canvasCtx.clearRect(0, 0, this.canvas.width, this.canvas.height); const barWidth = (this.canvas.width / bufferLength) * 2.5; let x = 0; for (let i = 0; i < bufferLength; i++) { const barHeight = dataArray[i] / 2; const r = barHeight + 25 * (i/bufferLength); const g = 250 * (i/bufferLength); const b = 50; this.canvasCtx.fillStyle = `rgb(${r},${g},${b})`; this.canvasCtx.fillRect(x, this.canvas.height - barHeight, barWidth, barHeight); x += barWidth + 1; } } destroy() { if (this.animationFrameId) { cancelAnimationFrame(this.animationFrameId); } if (this.audioCtx) { this.audioCtx.close(); } } }