// js/equalizer.js // 16-Band Parametric Equalizer with Web Audio API import { equalizerSettings } from './storage.js'; // Standard 16-band ISO center frequencies (Hz) const EQ_FREQUENCIES = [ 25, 40, 63, 100, 160, 250, 400, 630, 1000, 1600, 2500, 4000, 6300, 10000, 16000, 20000 ]; // Frequency labels for UI display const FREQUENCY_LABELS = [ '25', '40', '63', '100', '160', '250', '400', '630', '1K', '1.6K', '2.5K', '4K', '6.3K', '10K', '16K', '20K' ]; // EQ Presets (gain values in dB for each of the 16 bands) const EQ_PRESETS = { flat: { name: 'Flat', gains: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }, bass_boost: { name: 'Bass Boost', gains: [6, 5, 4.5, 4, 3, 2, 1, 0.5, 0, 0, 0, 0, 0, 0, 0, 0] }, bass_reducer: { name: 'Bass Reducer', gains: [-6, -5, -4, -3, -2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }, treble_boost: { name: 'Treble Boost', gains: [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 5.5, 6] }, treble_reducer: { name: 'Treble Reducer', gains: [0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -2, -3, -4, -5, -5.5, -6] }, vocal_boost: { name: 'Vocal Boost', gains: [-2, -1, 0, 0, 1, 2, 3, 4, 4, 3, 2, 1, 0, 0, -1, -2] }, loudness: { name: 'Loudness', gains: [5, 4, 3, 1, 0, -1, -1, 0, 0, 1, 2, 3, 4, 4.5, 4, 3] }, rock: { name: 'Rock', gains: [4, 3.5, 3, 2, -1, -2, -1, 1, 2, 3, 3.5, 4, 4, 3, 2, 1] }, pop: { name: 'Pop', gains: [-1, 0, 1, 2, 3, 3, 2, 1, 0, 1, 2, 2, 2, 2, 1, 0] }, classical: { name: 'Classical', gains: [3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 3, 2] }, jazz: { name: 'Jazz', gains: [3, 2, 1, 1, -1, -1, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2] }, electronic: { name: 'Electronic', gains: [4, 3.5, 3, 1, 0, -1, 0, 1, 2, 3, 3, 2, 2, 3, 4, 3.5] }, hip_hop: { name: 'Hip-Hop', gains: [5, 4.5, 4, 3, 1, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 2] }, r_and_b: { name: 'R&B', gains: [3, 5, 4, 2, 1, 0, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1] }, acoustic: { name: 'Acoustic', gains: [3, 2, 1, 1, 2, 2, 1, 0, 0, 1, 1, 2, 3, 3, 2, 1] }, podcast: { name: 'Podcast / Speech', gains: [-3, -2, -1, 0, 1, 2, 3, 4, 4, 3, 2, 1, 0, -1, -2, -3] } }; export class Equalizer { constructor() { this.audioContext = null; this.source = null; this.filters = []; this.inputNode = null; this.outputNode = null; this.isEnabled = false; this.isInitialized = false; this.audio = null; // Store current gains this.currentGains = new Array(16).fill(0); // Load saved settings this._loadSettings(); } /** * Initialize the equalizer with a shared AudioContext * This should be called after the visualizer creates the context * @param {AudioContext} audioContext - Shared audio context * @param {AudioNode} sourceNode - The MediaElementSource node * @param {HTMLAudioElement} audioElement - The audio element */ init(audioContext, sourceNode, audioElement) { if (this.isInitialized) return; try { this.audioContext = audioContext; this.source = sourceNode; this.audio = audioElement; // Create 16 biquad filters for each frequency band this.filters = EQ_FREQUENCIES.map((freq, index) => { const filter = this.audioContext.createBiquadFilter(); // Use peaking filter for all bands (best for EQ) filter.type = 'peaking'; filter.frequency.value = freq; filter.Q.value = this._calculateQ(index); filter.gain.value = this.currentGains[index]; return filter; }); // Create input/output gain nodes for bypass switching this.inputNode = this.audioContext.createGain(); this.outputNode = this.audioContext.createGain(); // Connect the filter chain this._connectFilters(); this.isInitialized = true; // Apply saved enabled state if (this.isEnabled) { this._enableFilters(); } console.log('[Equalizer] Initialized with 16 bands'); } catch (e) { console.warn('[Equalizer] Init failed:', e); } } /** * Calculate Q factor for each band * Using constant-Q design for consistent bandwidth */ _calculateQ(index) { // For 16-band 1/2 octave spacing, Q ≈ 2.87 // Slightly lower Q for smoother response return 2.5; } /** * Connect all filters in series */ _connectFilters() { if (!this.filters.length) return; // Chain filters together for (let i = 0; i < this.filters.length - 1; i++) { this.filters[i].connect(this.filters[i + 1]); } // Connect last filter to output this.filters[this.filters.length - 1].connect(this.outputNode); } /** * Enable the EQ processing */ _enableFilters() { if (!this.isInitialized || !this.source) return; // Note: The actual connection handling is done by the visualizer // This just marks the EQ as enabled this.isEnabled = true; } /** * Disable the EQ (bypass) */ _disableFilters() { this.isEnabled = false; } /** * Get the input node for external connection */ getInputNode() { return this.filters[0] || null; } /** * Get the output node */ getOutputNode() { return this.outputNode; } /** * Check if EQ is active (enabled and initialized) */ isActive() { return this.isInitialized && this.isEnabled; } /** * Toggle EQ on/off */ toggle(enabled) { this.isEnabled = enabled; equalizerSettings.setEnabled(enabled); if (enabled) { this._enableFilters(); } else { this._disableFilters(); } // Dispatch event for visualizer to reconnect window.dispatchEvent(new CustomEvent('equalizer-toggle', { detail: { enabled } })); return this.isEnabled; } /** * Set gain for a specific band * @param {number} bandIndex - Band index (0-15) * @param {number} gainDb - Gain in dB (-12 to +12) */ setBandGain(bandIndex, gainDb) { if (bandIndex < 0 || bandIndex >= 16) return; // Clamp gain to valid range const clampedGain = Math.max(-30, Math.min(30, gainDb)); this.currentGains[bandIndex] = clampedGain; if (this.filters[bandIndex]) { // Smooth transition for clicks prevention const now = this.audioContext?.currentTime || 0; this.filters[bandIndex].gain.setTargetAtTime(clampedGain, now, 0.01); } // Save to storage equalizerSettings.setGains(this.currentGains); } /** * Set all band gains at once * @param {number[]} gains - Array of 16 gain values in dB */ setAllGains(gains) { if (!Array.isArray(gains) || gains.length !== 16) return; const now = this.audioContext?.currentTime || 0; gains.forEach((gain, index) => { const clampedGain = Math.max(-30, Math.min(30, gain)); this.currentGains[index] = clampedGain; if (this.filters[index]) { this.filters[index].gain.setTargetAtTime(clampedGain, now, 0.01); } }); equalizerSettings.setGains(this.currentGains); } /** * Apply a preset * @param {string} presetKey - Key from EQ_PRESETS */ applyPreset(presetKey) { const preset = EQ_PRESETS[presetKey]; if (!preset) return; this.setAllGains(preset.gains); equalizerSettings.setPreset(presetKey); } /** * Reset all bands to flat (0 dB) */ reset() { this.setAllGains(new Array(16).fill(0)); equalizerSettings.setPreset('flat'); } /** * Get current gains * @returns {number[]} Array of 16 gain values */ getGains() { return [...this.currentGains]; } /** * Get frequency labels */ static getFrequencyLabels() { return FREQUENCY_LABELS; } /** * Get frequencies */ static getFrequencies() { return EQ_FREQUENCIES; } /** * Get available presets */ static getPresets() { return EQ_PRESETS; } /** * Load settings from storage */ _loadSettings() { this.isEnabled = equalizerSettings.isEnabled(); this.currentGains = equalizerSettings.getGains(); } /** * Destroy the equalizer */ destroy() { this.filters.forEach(filter => { try { filter.disconnect(); } catch { } }); try { this.inputNode?.disconnect(); } catch { } try { this.outputNode?.disconnect(); } catch { } this.filters = []; this.inputNode = null; this.outputNode = null; this.isInitialized = false; } } // Export singleton instance export const equalizer = new Equalizer(); // Export constants export { EQ_FREQUENCIES, FREQUENCY_LABELS, EQ_PRESETS };