equalizer changes

This commit is contained in:
IsraelGPT 2026-02-15 15:01:34 +00:00
parent 6a438a0551
commit 0b20caff69
6 changed files with 1755 additions and 401 deletions

View file

@ -3396,23 +3396,38 @@
<div class="equalizer-preset-row">
<label for="equalizer-preset-select">Preset</label>
<select id="equalizer-preset-select">
<option value="flat">Flat</option>
<option value="bass_boost">Bass Boost</option>
<option value="bass_reducer">Bass Reducer</option>
<option value="treble_boost">Treble Boost</option>
<option value="treble_reducer">Treble Reducer</option>
<option value="vocal_boost">Vocal Boost</option>
<option value="loudness">Loudness</option>
<option value="rock">Rock</option>
<option value="pop">Pop</option>
<option value="classical">Classical</option>
<option value="jazz">Jazz</option>
<option value="electronic">Electronic</option>
<option value="hip_hop">Hip-Hop</option>
<option value="r_and_b">R&B</option>
<option value="acoustic">Acoustic</option>
<option value="podcast">Podcast / Speech</option>
<optgroup label="Built-in Presets">
<option value="flat">Flat</option>
<option value="bass_boost">Bass Boost</option>
<option value="bass_reducer">Bass Reducer</option>
<option value="treble_boost">Treble Boost</option>
<option value="treble_reducer">Treble Reducer</option>
<option value="vocal_boost">Vocal Boost</option>
<option value="loudness">Loudness</option>
<option value="rock">Rock</option>
<option value="pop">Pop</option>
<option value="classical">Classical</option>
<option value="jazz">Jazz</option>
<option value="electronic">Electronic</option>
<option value="hip_hop">Hip-Hop</option>
<option value="r_and_b">R&B</option>
<option value="acoustic">Acoustic</option>
<option value="podcast">Podcast / Speech</option>
</optgroup>
<optgroup label="Custom Presets" id="custom-presets-optgroup">
<!-- Custom presets will be populated by JavaScript -->
</optgroup>
</select>
<label for="eq-band-count">Bands</label>
<input
type="number"
id="eq-band-count"
class="eq-band-count-input"
min="3"
max="32"
value="16"
title="Number of EQ bands (3-32)"
/>
<button
id="equalizer-reset-btn"
class="btn-secondary"
@ -3434,218 +3449,128 @@
</svg>
</button>
</div>
<div class="custom-preset-controls">
<div class="custom-preset-input-row">
<input
type="text"
id="custom-preset-name"
placeholder="Preset name (e.g., Home, Car, Work)"
maxlength="50"
/>
<button
id="save-custom-preset-btn"
class="btn-primary"
title="Save current EQ as custom preset"
>
Save
</button>
</div>
<button
id="delete-custom-preset-btn"
class="btn-secondary delete-preset-btn"
style="display: none"
title="Delete selected custom preset"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="3 6 5 6 21 6" />
<path
d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"
/>
</svg>
Delete Preset
</button>
</div>
<div class="eq-range-controls">
<label>DB Range:</label>
<input
type="number"
id="eq-range-min"
class="eq-range-input"
min="-60"
max="0"
value="-30"
title="Minimum gain in dB"
/>
<span>to</span>
<input
type="number"
id="eq-range-max"
class="eq-range-input"
min="0"
max="60"
value="30"
title="Maximum gain in dB"
/>
<span>dB</span>
<button
id="apply-eq-range-btn"
class="btn-secondary"
title="Apply new range to all bands"
>
Apply
</button>
<button
id="reset-eq-range-btn"
class="btn-secondary"
title="Reset to default (-30 to +30 dB)"
>
Reset
</button>
</div>
<div class="eq-freq-controls">
<label>Freq Range:</label>
<input
type="number"
id="eq-freq-min"
class="eq-freq-input"
min="10"
max="20000"
value="20"
title="Minimum frequency in Hz"
/>
<span>Hz to</span>
<input
type="number"
id="eq-freq-max"
class="eq-freq-input"
min="20"
max="96000"
value="20000"
title="Maximum frequency in Hz"
/>
<span>Hz</span>
<button
id="apply-eq-freq-btn"
class="btn-secondary"
title="Apply new frequency range"
>
Apply
</button>
<button
id="reset-eq-freq-btn"
class="btn-secondary"
title="Reset to default (20 Hz to 20 kHz)"
>
Reset
</button>
</div>
</div>
<div class="equalizer-bands" id="equalizer-bands">
<!-- Bands will be dynamically generated by JavaScript -->
<div class="eq-band" data-band="0">
<input
type="range"
class="eq-slider"
min="-30"
max="30"
step="0.5"
value="0"
orient="vertical"
/>
<span class="eq-value">0</span>
<span class="eq-freq">25</span>
</div>
<div class="eq-band" data-band="1">
<input
type="range"
class="eq-slider"
min="-30"
max="30"
step="0.5"
value="0"
orient="vertical"
/>
<span class="eq-value">0</span>
<span class="eq-freq">40</span>
</div>
<div class="eq-band" data-band="2">
<input
type="range"
class="eq-slider"
min="-30"
max="30"
step="0.5"
value="0"
orient="vertical"
/>
<span class="eq-value">0</span>
<span class="eq-freq">63</span>
</div>
<div class="eq-band" data-band="3">
<input
type="range"
class="eq-slider"
min="-30"
max="30"
step="0.5"
value="0"
orient="vertical"
/>
<span class="eq-value">0</span>
<span class="eq-freq">100</span>
</div>
<div class="eq-band" data-band="4">
<input
type="range"
class="eq-slider"
min="-30"
max="30"
step="0.5"
value="0"
orient="vertical"
/>
<span class="eq-value">0</span>
<span class="eq-freq">160</span>
</div>
<div class="eq-band" data-band="5">
<input
type="range"
class="eq-slider"
min="-30"
max="30"
step="0.5"
value="0"
orient="vertical"
/>
<span class="eq-value">0</span>
<span class="eq-freq">250</span>
</div>
<div class="eq-band" data-band="6">
<input
type="range"
class="eq-slider"
min="-30"
max="30"
step="0.5"
value="0"
orient="vertical"
/>
<span class="eq-value">0</span>
<span class="eq-freq">400</span>
</div>
<div class="eq-band" data-band="7">
<input
type="range"
class="eq-slider"
min="-30"
max="30"
step="0.5"
value="0"
orient="vertical"
/>
<span class="eq-value">0</span>
<span class="eq-freq">630</span>
</div>
<div class="eq-band" data-band="8">
<input
type="range"
class="eq-slider"
min="-30"
max="30"
step="0.5"
value="0"
orient="vertical"
/>
<span class="eq-value">0</span>
<span class="eq-freq">1K</span>
</div>
<div class="eq-band" data-band="9">
<input
type="range"
class="eq-slider"
min="-30"
max="30"
step="0.5"
value="0"
orient="vertical"
/>
<span class="eq-value">0</span>
<span class="eq-freq">1.6K</span>
</div>
<div class="eq-band" data-band="10">
<input
type="range"
class="eq-slider"
min="-30"
max="30"
step="0.5"
value="0"
orient="vertical"
/>
<span class="eq-value">0</span>
<span class="eq-freq">2.5K</span>
</div>
<div class="eq-band" data-band="11">
<input
type="range"
class="eq-slider"
min="-30"
max="30"
step="0.5"
value="0"
orient="vertical"
/>
<span class="eq-value">0</span>
<span class="eq-freq">4K</span>
</div>
<div class="eq-band" data-band="12">
<input
type="range"
class="eq-slider"
min="-30"
max="30"
step="0.5"
value="0"
orient="vertical"
/>
<span class="eq-value">0</span>
<span class="eq-freq">6.3K</span>
</div>
<div class="eq-band" data-band="13">
<input
type="range"
class="eq-slider"
min="-30"
max="30"
step="0.5"
value="0"
orient="vertical"
/>
<span class="eq-value">0</span>
<span class="eq-freq">10K</span>
</div>
<div class="eq-band" data-band="14">
<input
type="range"
class="eq-slider"
min="-30"
max="30"
step="0.5"
value="0"
orient="vertical"
/>
<span class="eq-value">0</span>
<span class="eq-freq">16K</span>
</div>
<div class="eq-band" data-band="15">
<input
type="range"
class="eq-slider"
min="-30"
max="30"
step="0.5"
value="0"
orient="vertical"
/>
<span class="eq-value">0</span>
<span class="eq-freq">20K</span>
</div>
</div>
<div class="equalizer-scale">

View file

@ -1,79 +1,95 @@
// js/audio-context.js
// Shared Audio Context Manager - handles EQ and provides context for visualizer
// Supports 3-32 parametric EQ bands
import { equalizerSettings, monoAudioSettings } 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];
// Standard 16-band ISO center frequencies (Hz) - for reference
const DEFAULT_EQ_FREQUENCIES = [25, 40, 63, 100, 160, 250, 400, 630, 1000, 1600, 2500, 4000, 6300, 10000, 16000, 20000];
// 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],
},
// Generate frequency array for given number of bands using logarithmic spacing
function generateFrequencies(bandCount, minFreq = 20, maxFreq = 20000) {
const frequencies = [];
const safeMin = Math.max(10, minFreq);
const safeMax = Math.min(96000, maxFreq);
for (let i = 0; i < bandCount; i++) {
// Logarithmic interpolation
const t = i / (bandCount - 1);
const freq = safeMin * Math.pow(safeMax / safeMin, t);
frequencies.push(Math.round(freq));
}
return frequencies;
}
// Generate frequency labels for display
function generateFrequencyLabels(frequencies) {
return frequencies.map((freq) => {
if (freq < 1000) {
return freq.toString();
} else if (freq < 10000) {
return (freq / 1000).toFixed(freq % 1000 === 0 ? 0 : 1) + 'K';
} else {
return (freq / 1000).toFixed(0) + 'K';
}
});
}
// EQ Presets (16-band default)
const EQ_PRESETS_16 = {
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] },
};
// Interpolate 16-band preset to target band count
function interpolatePreset(preset16, targetBands) {
if (targetBands === 16) return [...preset16];
const result = [];
for (let i = 0; i < targetBands; i++) {
const sourceIndex = (i / (targetBands - 1)) * (preset16.length - 1);
const indexLow = Math.floor(sourceIndex);
const indexHigh = Math.min(Math.ceil(sourceIndex), preset16.length - 1);
const fraction = sourceIndex - indexLow;
const lowValue = preset16[indexLow] || 0;
const highValue = preset16[indexHigh] || 0;
const interpolated = lowValue + (highValue - lowValue) * fraction;
result.push(Math.round(interpolated * 10) / 10);
}
return result;
}
// Get presets for given band count
function getPresetsForBandCount(bandCount) {
const presets = {};
for (const [key, preset] of Object.entries(EQ_PRESETS_16)) {
presets[key] = {
name: preset.name,
gains: interpolatePreset(preset.gains, bandCount),
};
}
return presets;
}
// Default export for backwards compatibility (16 bands)
const EQ_PRESETS = EQ_PRESETS_16;
class AudioContextManager {
constructor() {
this.audioContext = null;
@ -86,10 +102,15 @@ class AudioContextManager {
this.isEQEnabled = false;
this.isMonoAudioEnabled = false;
this.monoMergerNode = null;
this.currentGains = new Array(16).fill(0);
this.audio = null;
this.currentVolume = 1.0;
// Band configuration
this.bandCount = equalizerSettings.getBandCount();
this.freqRange = equalizerSettings.getFreqRange();
this.frequencies = generateFrequencies(this.bandCount, this.freqRange.min, this.freqRange.max);
this.currentGains = new Array(this.bandCount).fill(0);
// Callbacks for audio graph changes (for visualizers like Butterchurn)
this._graphChangeCallbacks = [];
@ -97,6 +118,128 @@ class AudioContextManager {
this._loadSettings();
}
/**
* Update band count and reinitialize EQ
*/
setBandCount(count) {
const newCount = Math.max(
equalizerSettings.MIN_BANDS,
Math.min(equalizerSettings.MAX_BANDS, parseInt(count, 10) || 16)
);
if (newCount === this.bandCount) return;
// Save new band count
equalizerSettings.setBandCount(newCount);
// Update configuration
this.bandCount = newCount;
this.frequencies = generateFrequencies(newCount, this.freqRange.min, this.freqRange.max);
// Interpolate current gains to new band count
const newGains = equalizerSettings._interpolateGains(this.currentGains, newCount);
this.currentGains = newGains;
equalizerSettings.setGains(newGains);
// Reinitialize EQ if already initialized
if (this.isInitialized && this.audioContext) {
this._destroyEQ();
this._createEQ();
}
// Dispatch event for UI update
window.dispatchEvent(
new CustomEvent('equalizer-band-count-changed', {
detail: { bandCount: newCount, frequencies: this.frequencies },
})
);
}
/**
* Update frequency range and reinitialize EQ
*/
setFreqRange(minFreq, maxFreq) {
const newMin = Math.max(10, Math.min(96000, parseInt(minFreq, 10) || 20));
const newMax = Math.max(10, Math.min(96000, parseInt(maxFreq, 10) || 20000));
if (newMin >= newMax) {
console.warn('[AudioContext] Invalid frequency range: min must be less than max');
return false;
}
if (newMin === this.freqRange.min && newMax === this.freqRange.max) return true;
// Save new frequency range
equalizerSettings.setFreqRange(newMin, newMax);
// Update configuration
this.freqRange = { min: newMin, max: newMax };
this.frequencies = generateFrequencies(this.bandCount, newMin, newMax);
// Reinitialize EQ if already initialized
if (this.isInitialized && this.audioContext) {
this._destroyEQ();
this._createEQ();
}
// Dispatch event for UI update
window.dispatchEvent(
new CustomEvent('equalizer-freq-range-changed', {
detail: { min: newMin, max: newMax, frequencies: this.frequencies },
})
);
return true;
}
/**
* Destroy EQ filters
*/
_destroyEQ() {
if (this.filters) {
this.filters.forEach((filter) => {
try {
filter.disconnect();
} catch {
/* ignore */
}
});
}
this.filters = [];
}
/**
* Create EQ filters
*/
_createEQ() {
if (!this.audioContext) return;
// Create biquad filters for each frequency band
this.filters = this.frequencies.map((freq, index) => {
const filter = this.audioContext.createBiquadFilter();
filter.type = 'peaking';
filter.frequency.value = freq;
filter.Q.value = this._calculateQ(index);
filter.gain.value = this.currentGains[index] || 0;
return filter;
});
// Create volume node if not exists
if (!this.volumeNode) {
this.volumeNode = this.audioContext.createGain();
}
}
/**
* Calculate Q factor for each band
*/
_calculateQ(_index) {
// Scale Q based on band count for consistent sound
const baseQ = 2.5;
const scalingFactor = Math.sqrt(16 / this.bandCount);
return baseQ * scalingFactor;
}
/**
* Register a callback to be called when audio graph is reconnected
* @param {Function} callback - Function to call when graph changes
@ -159,15 +302,8 @@ class AudioContextManager {
this.analyser.fftSize = 512;
this.analyser.smoothingTimeConstant = 0.7;
// Create 16 biquad filters for EQ
this.filters = EQ_FREQUENCIES.map((freq, index) => {
const filter = this.audioContext.createBiquadFilter();
filter.type = 'peaking';
filter.frequency.value = freq;
filter.Q.value = 2.5; // Constant-Q design
filter.gain.value = this.currentGains[index];
return filter;
});
// Create biquad filters for EQ with dynamic band count
this._createEQ();
// Create output gain node
this.outputNode = this.audioContext.createGain();
@ -180,17 +316,11 @@ class AudioContextManager {
// Create mono audio merger node
this.monoMergerNode = this.audioContext.createChannelMerger(2);
// Connect filter chain: filter[0] -> filter[1] -> ... -> filter[15] -> outputNode
for (let i = 0; i < this.filters.length - 1; i++) {
this.filters[i].connect(this.filters[i + 1]);
}
this.filters[this.filters.length - 1].connect(this.outputNode);
// Connect the audio graph based on EQ and mono state
this._connectGraph();
this.isInitialized = true;
console.log('[AudioContext] Initialized with 16-band EQ');
console.log(`[AudioContext] Initialized with ${this.bandCount}-band EQ`);
} catch (e) {
console.warn('[AudioContext] Init failed:', e);
}
@ -240,7 +370,13 @@ class AudioContextManager {
if (this.isEQEnabled && this.filters.length > 0) {
// EQ enabled: lastNode -> EQ filters -> output -> analyser -> volume -> destination
// Connect filter chain
for (let i = 0; i < this.filters.length - 1; i++) {
this.filters[i].connect(this.filters[i + 1]);
}
// Connect input to first filter and last filter to output
lastNode.connect(this.filters[0]);
this.filters[this.filters.length - 1].connect(this.outputNode);
this.outputNode.connect(this.analyser);
this.analyser.connect(this.volumeNode);
this.volumeNode.connect(this.audioContext.destination);
@ -374,13 +510,28 @@ class AudioContextManager {
return this.isInitialized && this.isMonoAudioEnabled;
}
/**
* Get current gain range
*/
getRange() {
return equalizerSettings.getRange();
}
/**
* Clamp gain to valid range
*/
_clampGain(gainDb) {
const range = this.getRange();
return Math.max(range.min, Math.min(range.max, gainDb));
}
/**
* Set gain for a specific band
*/
setBandGain(bandIndex, gainDb) {
if (bandIndex < 0 || bandIndex >= 16) return;
if (bandIndex < 0 || bandIndex >= this.bandCount) return;
const clampedGain = Math.max(-30, Math.min(30, gainDb));
const clampedGain = this._clampGain(gainDb);
this.currentGains[bandIndex] = clampedGain;
if (this.filters[bandIndex] && this.audioContext) {
@ -395,12 +546,18 @@ class AudioContextManager {
* Set all band gains at once
*/
setAllGains(gains) {
if (!Array.isArray(gains) || gains.length !== 16) return;
if (!Array.isArray(gains)) return;
// Ensure gains array matches current band count
let adjustedGains = gains;
if (gains.length !== this.bandCount) {
adjustedGains = equalizerSettings._interpolateGains(gains, this.bandCount);
}
const now = this.audioContext?.currentTime || 0;
gains.forEach((gain, index) => {
const clampedGain = Math.max(-30, Math.min(30, gain));
adjustedGains.forEach((gain, index) => {
const clampedGain = this._clampGain(gain);
this.currentGains[index] = clampedGain;
if (this.filters[index]) {
@ -415,7 +572,8 @@ class AudioContextManager {
* Apply a preset
*/
applyPreset(presetKey) {
const preset = EQ_PRESETS[presetKey];
const presets = getPresetsForBandCount(this.bandCount);
const preset = presets[presetKey];
if (!preset) return;
this.setAllGains(preset.gains);
@ -426,7 +584,7 @@ class AudioContextManager {
* Reset all bands to flat
*/
reset() {
this.setAllGains(new Array(16).fill(0));
this.setAllGains(new Array(this.bandCount).fill(0));
equalizerSettings.setPreset('flat');
}
@ -437,12 +595,22 @@ class AudioContextManager {
return [...this.currentGains];
}
/**
* Get current band count
*/
getBandCount() {
return this.bandCount;
}
/**
* Load settings from storage
*/
_loadSettings() {
this.isEQEnabled = equalizerSettings.isEnabled();
this.currentGains = equalizerSettings.getGains();
this.bandCount = equalizerSettings.getBandCount();
this.freqRange = equalizerSettings.getFreqRange();
this.frequencies = generateFrequencies(this.bandCount, this.freqRange.min, this.freqRange.max);
this.currentGains = equalizerSettings.getGains(this.bandCount);
this.isMonoAudioEnabled = monoAudioSettings.isEnabled();
}
}
@ -450,5 +618,12 @@ class AudioContextManager {
// Export singleton instance
export const audioContextManager = new AudioContextManager();
// Export presets for settings UI
export { EQ_PRESETS };
// Export presets and helper functions for settings UI
export {
EQ_PRESETS,
generateFrequencies,
generateFrequencyLabels,
getPresetsForBandCount,
interpolatePreset,
EQ_PRESETS_16,
};

View file

@ -1,13 +1,13 @@
// js/equalizer.js
// 16-Band Parametric Equalizer with Web Audio API
// Parametric Equalizer with Web Audio API - Supports 3-32 bands
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];
// Standard 16-band ISO center frequencies (Hz) - kept for reference
const DEFAULT_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 = [
const DEFAULT_FREQUENCY_LABELS = [
'25',
'40',
'63',
@ -26,8 +26,37 @@ const FREQUENCY_LABELS = [
'20K',
];
// Generate frequency array for given number of bands using logarithmic spacing
function generateFrequencies(bandCount, minFreq = 20, maxFreq = 20000) {
const frequencies = [];
const safeMin = Math.max(10, minFreq);
const safeMax = Math.min(96000, maxFreq);
for (let i = 0; i < bandCount; i++) {
// Logarithmic interpolation
const t = i / (bandCount - 1);
const freq = safeMin * Math.pow(safeMax / safeMin, t);
frequencies.push(Math.round(freq));
}
return frequencies;
}
// Generate frequency labels for display
function generateFrequencyLabels(frequencies) {
return frequencies.map((freq) => {
if (freq < 1000) {
return freq.toString();
} else if (freq < 10000) {
return (freq / 1000).toFixed(freq % 1000 === 0 ? 0 : 1) + 'K';
} else {
return (freq / 1000).toFixed(0) + 'K';
}
});
}
// EQ Presets (gain values in dB for each of the 16 bands)
const EQ_PRESETS = {
const EQ_PRESETS_16BAND = {
flat: {
name: 'Flat',
gains: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
@ -94,6 +123,37 @@ const EQ_PRESETS = {
},
};
// Interpolate 16-band preset to target band count
function interpolatePreset(preset16, targetBands) {
if (targetBands === 16) return [...preset16];
const result = [];
for (let i = 0; i < targetBands; i++) {
const sourceIndex = (i / (targetBands - 1)) * (preset16.length - 1);
const indexLow = Math.floor(sourceIndex);
const indexHigh = Math.min(Math.ceil(sourceIndex), preset16.length - 1);
const fraction = sourceIndex - indexLow;
const lowValue = preset16[indexLow] || 0;
const highValue = preset16[indexHigh] || 0;
const interpolated = lowValue + (highValue - lowValue) * fraction;
result.push(Math.round(interpolated * 10) / 10);
}
return result;
}
// Get presets for given band count
function getPresetsForBandCount(bandCount) {
const presets = {};
for (const [key, preset] of Object.entries(EQ_PRESETS_16BAND)) {
presets[key] = {
name: preset.name,
gains: interpolatePreset(preset.gains, bandCount),
};
}
return presets;
}
export class Equalizer {
constructor() {
this.audioContext = null;
@ -105,13 +165,99 @@ export class Equalizer {
this.isInitialized = false;
this.audio = null;
// Band configuration
this.bandCount = equalizerSettings.getBandCount();
this.freqRange = equalizerSettings.getFreqRange();
this.frequencies = generateFrequencies(this.bandCount, this.freqRange.min, this.freqRange.max);
this.frequencyLabels = generateFrequencyLabels(this.frequencies);
// Store current gains
this.currentGains = new Array(16).fill(0);
this.currentGains = new Array(this.bandCount).fill(0);
// Load saved settings
this._loadSettings();
}
/**
* Update band count and reinitialize
*/
setBandCount(count) {
const newCount = Math.max(
equalizerSettings.MIN_BANDS,
Math.min(equalizerSettings.MAX_BANDS, parseInt(count, 10) || 16)
);
if (newCount === this.bandCount) return;
// Save new band count
equalizerSettings.setBandCount(newCount);
// Update configuration
this.bandCount = newCount;
this.frequencies = generateFrequencies(newCount, this.freqRange.min, this.freqRange.max);
this.frequencyLabels = generateFrequencyLabels(this.frequencies);
// Interpolate current gains to new band count
const newGains = equalizerSettings._interpolateGains(this.currentGains, newCount);
this.currentGains = newGains;
equalizerSettings.setGains(newGains);
// Reinitialize if already initialized
if (this.isInitialized) {
this.destroy();
if (this.audioContext && this.source && this.audio) {
this.init(this.audioContext, this.source, this.audio);
}
}
// Dispatch event for UI update
window.dispatchEvent(
new CustomEvent('equalizer-band-count-changed', {
detail: { bandCount: newCount, frequencies: this.frequencies, labels: this.frequencyLabels },
})
);
}
/**
* Update frequency range and reinitialize
*/
setFreqRange(minFreq, maxFreq) {
const newMin = Math.max(10, Math.min(96000, parseInt(minFreq, 10) || 20));
const newMax = Math.max(10, Math.min(96000, parseInt(maxFreq, 10) || 20000));
if (newMin >= newMax) {
console.warn('[Equalizer] Invalid frequency range: min must be less than max');
return false;
}
if (newMin === this.freqRange.min && newMax === this.freqRange.max) return true;
// Save new frequency range
equalizerSettings.setFreqRange(newMin, newMax);
// Update configuration
this.freqRange = { min: newMin, max: newMax };
this.frequencies = generateFrequencies(this.bandCount, newMin, newMax);
this.frequencyLabels = generateFrequencyLabels(this.frequencies);
// Reinitialize if already initialized
if (this.isInitialized) {
this.destroy();
if (this.audioContext && this.source && this.audio) {
this.init(this.audioContext, this.source, this.audio);
}
}
// Dispatch event for UI update
window.dispatchEvent(
new CustomEvent('equalizer-freq-range-changed', {
detail: { min: newMin, max: newMax, frequencies: this.frequencies, labels: this.frequencyLabels },
})
);
return true;
}
/**
* Initialize the equalizer with a shared AudioContext
* This should be called after the visualizer creates the context
@ -127,15 +273,15 @@ export class Equalizer {
this.source = sourceNode;
this.audio = audioElement;
// Create 16 biquad filters for each frequency band
this.filters = EQ_FREQUENCIES.map((freq, index) => {
// Create biquad filters for each frequency band
this.filters = this.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];
filter.gain.value = this.currentGains[index] || 0;
return filter;
});
@ -154,7 +300,7 @@ export class Equalizer {
this._enableFilters();
}
console.log('[Equalizer] Initialized with 16 bands');
console.log(`[Equalizer] Initialized with ${this.bandCount} bands`);
} catch (e) {
console.warn('[Equalizer] Init failed:', e);
}
@ -167,7 +313,10 @@ export class Equalizer {
_calculateQ(_index) {
// For 16-band 1/2 octave spacing, Q ≈ 2.87
// Slightly lower Q for smoother response
return 2.5;
// Scale Q based on band count for consistent sound
const baseQ = 2.5;
const scalingFactor = Math.sqrt(16 / this.bandCount);
return baseQ * scalingFactor;
}
/**
@ -247,16 +396,31 @@ export class Equalizer {
return this.isEnabled;
}
/**
* Get current gain range from settings
*/
getRange() {
return equalizerSettings.getRange();
}
/**
* Clamp gain to current range
*/
_clampGain(gainDb) {
const range = this.getRange();
return Math.max(range.min, Math.min(range.max, gainDb));
}
/**
* Set gain for a specific band
* @param {number} bandIndex - Band index (0-15)
* @param {number} gainDb - Gain in dB (-12 to +12)
* @param {number} bandIndex - Band index
* @param {number} gainDb - Gain in dB
*/
setBandGain(bandIndex, gainDb) {
if (bandIndex < 0 || bandIndex >= 16) return;
if (bandIndex < 0 || bandIndex >= this.bandCount) return;
// Clamp gain to valid range
const clampedGain = Math.max(-30, Math.min(30, gainDb));
const clampedGain = this._clampGain(gainDb);
this.currentGains[bandIndex] = clampedGain;
if (this.filters[bandIndex]) {
@ -271,15 +435,21 @@ export class Equalizer {
/**
* Set all band gains at once
* @param {number[]} gains - Array of 16 gain values in dB
* @param {number[]} gains - Array of gain values in dB
*/
setAllGains(gains) {
if (!Array.isArray(gains) || gains.length !== 16) return;
if (!Array.isArray(gains)) return;
// Ensure gains array matches current band count
let adjustedGains = gains;
if (gains.length !== this.bandCount) {
adjustedGains = equalizerSettings._interpolateGains(gains, this.bandCount);
}
const now = this.audioContext?.currentTime || 0;
gains.forEach((gain, index) => {
const clampedGain = Math.max(-30, Math.min(30, gain));
adjustedGains.forEach((gain, index) => {
const clampedGain = this._clampGain(gain);
this.currentGains[index] = clampedGain;
if (this.filters[index]) {
@ -295,7 +465,8 @@ export class Equalizer {
* @param {string} presetKey - Key from EQ_PRESETS
*/
applyPreset(presetKey) {
const preset = EQ_PRESETS[presetKey];
const presets = getPresetsForBandCount(this.bandCount);
const preset = presets[presetKey];
if (!preset) return;
this.setAllGains(preset.gains);
@ -306,37 +477,47 @@ export class Equalizer {
* Reset all bands to flat (0 dB)
*/
reset() {
this.setAllGains(new Array(16).fill(0));
this.setAllGains(new Array(this.bandCount).fill(0));
equalizerSettings.setPreset('flat');
}
/**
* Get current gains
* @returns {number[]} Array of 16 gain values
* @returns {number[]} Array of gain values
*/
getGains() {
return [...this.currentGains];
}
/**
* Get frequency labels
* Get current band count
* @returns {number} Number of bands
*/
static getFrequencyLabels() {
return FREQUENCY_LABELS;
getBandCount() {
return this.bandCount;
}
/**
* Get frequency labels for UI
* @returns {string[]} Array of frequency labels
*/
getFrequencyLabels() {
return this.frequencyLabels;
}
/**
* Get frequencies
* @returns {number[]} Array of frequency values
*/
static getFrequencies() {
return EQ_FREQUENCIES;
getFrequencies() {
return this.frequencies;
}
/**
* Get available presets
* Get available presets (static method for default 16 bands)
*/
static getPresets() {
return EQ_PRESETS;
static getPresets(bandCount = 16) {
return getPresetsForBandCount(bandCount);
}
/**
@ -344,7 +525,11 @@ export class Equalizer {
*/
_loadSettings() {
this.isEnabled = equalizerSettings.isEnabled();
this.currentGains = equalizerSettings.getGains();
this.bandCount = equalizerSettings.getBandCount();
this.freqRange = equalizerSettings.getFreqRange();
this.frequencies = generateFrequencies(this.bandCount, this.freqRange.min, this.freqRange.max);
this.frequencyLabels = generateFrequencyLabels(this.frequencies);
this.currentGains = equalizerSettings.getGains(this.bandCount);
}
/**
@ -380,5 +565,13 @@ export class Equalizer {
// Export singleton instance
export const equalizer = new Equalizer();
// Export constants
export { EQ_FREQUENCIES, FREQUENCY_LABELS, EQ_PRESETS };
// Export helper functions and constants
export {
generateFrequencies,
generateFrequencyLabels,
getPresetsForBandCount,
interpolatePreset,
DEFAULT_EQ_FREQUENCIES,
DEFAULT_FREQUENCY_LABELS,
EQ_PRESETS_16BAND as EQ_PRESETS,
};

View file

@ -858,13 +858,111 @@ export function initializeSettings(scrobbler, player, api, ui) {
}
// ========================================
// 16-Band Equalizer Settings
// Parametric Equalizer Settings (3-32 bands with custom ranges)
// ========================================
const eqToggle = document.getElementById('equalizer-enabled-toggle');
const eqContainer = document.getElementById('equalizer-container');
const eqPresetSelect = document.getElementById('equalizer-preset-select');
const eqResetBtn = document.getElementById('equalizer-reset-btn');
const eqBands = document.querySelectorAll('.eq-band');
const eqBandsContainer = document.getElementById('equalizer-bands');
const customPresetsOptgroup = document.getElementById('custom-presets-optgroup');
const customPresetNameInput = document.getElementById('custom-preset-name');
const saveCustomPresetBtn = document.getElementById('save-custom-preset-btn');
const deleteCustomPresetBtn = document.getElementById('delete-custom-preset-btn');
const eqBandCountInput = document.getElementById('eq-band-count');
const eqRangeMinInput = document.getElementById('eq-range-min');
const eqRangeMaxInput = document.getElementById('eq-range-max');
const applyEqRangeBtn = document.getElementById('apply-eq-range-btn');
const eqFreqMinInput = document.getElementById('eq-freq-min');
const eqFreqMaxInput = document.getElementById('eq-freq-max');
const applyEqFreqBtn = document.getElementById('apply-eq-freq-btn');
const resetEqFreqBtn = document.getElementById('reset-eq-freq-btn');
const resetEqRangeBtn = document.getElementById('reset-eq-range-btn');
const eqScaleContainer = document.querySelector('.equalizer-scale');
// Current settings
let currentBandCount = equalizerSettings.getBandCount();
let currentRange = equalizerSettings.getRange();
let currentFreqRange = equalizerSettings.getFreqRange();
/**
* Generate frequency labels for given band count and frequency range
*/
const generateFreqLabels = (count, minFreq = currentFreqRange.min, maxFreq = currentFreqRange.max) => {
const labels = [];
const safeMin = Math.max(10, minFreq);
const safeMax = Math.min(96000, maxFreq);
for (let i = 0; i < count; i++) {
const t = i / (count - 1);
const freq = safeMin * Math.pow(safeMax / safeMin, t);
const rounded = Math.round(freq);
if (rounded < 1000) {
labels.push(rounded.toString());
} else if (rounded < 10000) {
labels.push((rounded / 1000).toFixed(rounded % 1000 === 0 ? 0 : 1) + 'K');
} else {
labels.push((rounded / 1000).toFixed(0) + 'K');
}
}
return labels;
};
/**
* Generate EQ bands HTML
*/
const generateEQBands = (
count,
rangeMin = currentRange.min,
rangeMax = currentRange.max,
freqMin = currentFreqRange.min,
freqMax = currentFreqRange.max
) => {
if (!eqBandsContainer) return;
const labels = generateFreqLabels(count, freqMin, freqMax);
eqBandsContainer.innerHTML = '';
for (let i = 0; i < count; i++) {
const bandEl = document.createElement('div');
bandEl.className = 'eq-band';
bandEl.dataset.band = i;
bandEl.innerHTML = `
<input
type="range"
class="eq-slider"
min="${rangeMin}"
max="${rangeMax}"
step="0.5"
value="0"
orient="vertical"
/>
<span class="eq-value">0</span>
<span class="eq-freq">${labels[i]}</span>
`;
eqBandsContainer.appendChild(bandEl);
}
// Re-initialize band sliders
initializeBandSliders();
};
/**
* Update EQ scale display
*/
const updateEQScale = (min, max) => {
if (!eqScaleContainer) return;
const spans = eqScaleContainer.querySelectorAll('span');
if (spans.length >= 3) {
spans[0].textContent = `+${max} dB`;
spans[1].textContent = '0 dB';
spans[2].textContent = `${min} dB`;
}
};
/**
* Update the visual display of a band value
@ -889,6 +987,9 @@ export function initializeSettings(scrobbler, player, api, ui) {
* Update all band sliders and displays from an array of gains
*/
const updateAllBandUI = (gains) => {
const eqBands = eqBandsContainer?.querySelectorAll('.eq-band');
if (!eqBands) return;
eqBands.forEach((bandEl, index) => {
const slider = bandEl.querySelector('.eq-slider');
if (slider && gains[index] !== undefined) {
@ -907,48 +1008,59 @@ export function initializeSettings(scrobbler, player, api, ui) {
}
};
// Initialize EQ toggle
if (eqToggle) {
const isEnabled = equalizerSettings.isEnabled();
eqToggle.checked = isEnabled;
updateEQContainerVisibility(isEnabled);
/**
* Populate custom presets in the dropdown
*/
const populateCustomPresets = () => {
if (!customPresetsOptgroup) return;
eqToggle.addEventListener('change', (e) => {
const enabled = e.target.checked;
audioContextManager.toggleEQ(enabled);
updateEQContainerVisibility(enabled);
});
}
// Clear existing custom presets
customPresetsOptgroup.innerHTML = '';
// Initialize preset selector
if (eqPresetSelect) {
eqPresetSelect.value = equalizerSettings.getPreset();
const customPresets = equalizerSettings.getCustomPresets();
const presetIds = Object.keys(customPresets);
eqPresetSelect.addEventListener('change', (e) => {
const presetKey = e.target.value;
const preset = EQ_PRESETS[presetKey];
if (presetIds.length === 0) {
const emptyOption = document.createElement('option');
emptyOption.value = '';
emptyOption.textContent = 'No custom presets saved';
emptyOption.disabled = true;
customPresetsOptgroup.appendChild(emptyOption);
} else {
presetIds.forEach((presetId) => {
const preset = customPresets[presetId];
const option = document.createElement('option');
option.value = presetId;
option.textContent = preset.name;
customPresetsOptgroup.appendChild(option);
});
}
};
if (preset) {
audioContextManager.applyPreset(presetKey);
updateAllBandUI(preset.gains);
}
});
}
/**
* Check if a preset ID is a custom preset
*/
const isCustomPreset = (presetId) => {
return presetId && presetId.startsWith('custom_');
};
// Initialize reset button
if (eqResetBtn) {
eqResetBtn.addEventListener('click', () => {
audioContextManager.reset();
updateAllBandUI(new Array(16).fill(0));
if (eqPresetSelect) {
eqPresetSelect.value = 'flat';
}
});
}
/**
* Update delete button visibility based on selected preset
*/
const updateDeleteButtonVisibility = () => {
if (!deleteCustomPresetBtn || !eqPresetSelect) return;
const isCustom = isCustomPreset(eqPresetSelect.value);
deleteCustomPresetBtn.style.display = isCustom ? 'flex' : 'none';
};
// Initialize all band sliders
if (eqBands.length > 0) {
const savedGains = equalizerSettings.getGains();
/**
* Initialize band slider event listeners
*/
const initializeBandSliders = () => {
const eqBands = eqBandsContainer?.querySelectorAll('.eq-band');
if (!eqBands || eqBands.length === 0) return;
const savedGains = equalizerSettings.getGains(currentBandCount);
eqBands.forEach((bandEl) => {
const bandIndex = parseInt(bandEl.dataset.band, 10);
@ -966,16 +1078,15 @@ export function initializeSettings(scrobbler, player, api, ui) {
audioContextManager.setBandGain(bandIndex, gain);
updateBandValueDisplay(bandEl, gain);
// When manually adjusting, switch preset to 'flat' (custom)
// to indicate the user has made custom changes
// When manually adjusting, check if we should clear preset
if (eqPresetSelect && eqPresetSelect.value !== 'flat') {
// Check if current gains still match the selected preset
const currentPreset = EQ_PRESETS[eqPresetSelect.value];
const currentGains = audioContextManager.getGains();
const builtInPresets = EQ_PRESETS;
const currentPreset = builtInPresets[eqPresetSelect.value];
if (currentPreset) {
const currentGains = audioContextManager.getGains();
const matches = currentPreset.gains.every((g, i) => Math.abs(g - currentGains[i]) < 0.01);
if (!matches) {
// Don't change the select, but the preset will save as 'custom'
// User has deviated from preset
}
}
}
@ -989,8 +1100,423 @@ export function initializeSettings(scrobbler, player, api, ui) {
});
}
});
};
// Initialize EQ toggle
if (eqToggle) {
const isEnabled = equalizerSettings.isEnabled();
eqToggle.checked = isEnabled;
updateEQContainerVisibility(isEnabled);
eqToggle.addEventListener('change', (e) => {
const enabled = e.target.checked;
audioContextManager.toggleEQ(enabled);
updateEQContainerVisibility(enabled);
});
}
// Initialize band count input
if (eqBandCountInput) {
eqBandCountInput.value = currentBandCount;
eqBandCountInput.addEventListener('change', (e) => {
const newCount = parseInt(e.target.value, 10);
if (newCount >= equalizerSettings.MIN_BANDS && newCount <= equalizerSettings.MAX_BANDS) {
currentBandCount = newCount;
// Save new band count and update audio context
equalizerSettings.setBandCount(newCount);
audioContextManager.setBandCount?.(newCount) || audioContextManager.reinitialize?.();
// Regenerate UI
generateEQBands(
newCount,
currentRange.min,
currentRange.max,
currentFreqRange.min,
currentFreqRange.max
);
// Reset to flat and apply
const flatGains = new Array(newCount).fill(0);
audioContextManager.setAllGains(flatGains);
updateAllBandUI(flatGains);
if (eqPresetSelect) {
eqPresetSelect.value = 'flat';
equalizerSettings.setPreset('flat');
}
updateDeleteButtonVisibility();
}
});
}
// Initialize preset selector
if (eqPresetSelect) {
populateCustomPresets();
eqPresetSelect.value = equalizerSettings.getPreset();
updateDeleteButtonVisibility();
eqPresetSelect.addEventListener('change', (e) => {
const presetKey = e.target.value;
// Check if it's a custom preset
if (isCustomPreset(presetKey)) {
const customPresets = equalizerSettings.getCustomPresets();
const customPreset = customPresets[presetKey];
if (customPreset && customPreset.gains) {
// Check if preset has different band count
const presetBands = customPreset.bandCount || customPreset.gains.length;
if (presetBands !== currentBandCount) {
// Update band count to match preset
currentBandCount = presetBands;
equalizerSettings.setBandCount(presetBands);
if (eqBandCountInput) eqBandCountInput.value = presetBands;
generateEQBands(
presetBands,
currentRange.min,
currentRange.max,
currentFreqRange.min,
currentFreqRange.max
);
}
audioContextManager.setAllGains(customPreset.gains);
updateAllBandUI(customPreset.gains);
equalizerSettings.setPreset(presetKey);
}
} else {
// Built-in preset - use current band count
const presets = EQ_PRESETS;
const preset = presets[presetKey];
if (preset) {
audioContextManager.applyPreset(presetKey);
updateAllBandUI(preset.gains);
}
}
updateDeleteButtonVisibility();
});
}
// Initialize reset button
if (eqResetBtn) {
eqResetBtn.addEventListener('click', () => {
audioContextManager.reset();
updateAllBandUI(new Array(currentBandCount).fill(0));
if (eqPresetSelect) {
eqPresetSelect.value = 'flat';
updateDeleteButtonVisibility();
}
});
}
// Initialize save custom preset button
if (saveCustomPresetBtn && customPresetNameInput) {
saveCustomPresetBtn.addEventListener('click', () => {
const name = customPresetNameInput.value.trim();
if (!name) {
alert('Please enter a name for your preset');
return;
}
const currentGains = audioContextManager.getGains();
const presetId = equalizerSettings.saveCustomPreset(name, currentGains);
if (presetId) {
populateCustomPresets();
if (eqPresetSelect) {
eqPresetSelect.value = presetId;
equalizerSettings.setPreset(presetId);
updateDeleteButtonVisibility();
}
customPresetNameInput.value = '';
// Show feedback
const originalText = saveCustomPresetBtn.textContent;
saveCustomPresetBtn.textContent = 'Saved!';
setTimeout(() => {
saveCustomPresetBtn.textContent = originalText;
}, 1500);
} else {
alert('Failed to save preset. Please try again.');
}
});
// Allow saving with Enter key
customPresetNameInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
saveCustomPresetBtn.click();
}
});
}
// Initialize delete custom preset button
if (deleteCustomPresetBtn) {
deleteCustomPresetBtn.addEventListener('click', () => {
if (!eqPresetSelect) return;
const presetId = eqPresetSelect.value;
if (!isCustomPreset(presetId)) return;
const customPresets = equalizerSettings.getCustomPresets();
const presetName = customPresets[presetId]?.name || 'this preset';
if (confirm(`Are you sure you want to delete "${presetName}"?`)) {
const success = equalizerSettings.deleteCustomPreset(presetId);
if (success) {
populateCustomPresets();
eqPresetSelect.value = 'flat';
audioContextManager.reset();
updateAllBandUI(new Array(currentBandCount).fill(0));
equalizerSettings.setPreset('flat');
updateDeleteButtonVisibility();
} else {
alert('Failed to delete preset. Please try again.');
}
}
});
}
// Initialize range inputs
if (eqRangeMinInput) {
eqRangeMinInput.value = currentRange.min;
}
if (eqRangeMaxInput) {
eqRangeMaxInput.value = currentRange.max;
}
updateEQScale(currentRange.min, currentRange.max);
// Initialize apply range button
if (applyEqRangeBtn && eqRangeMinInput && eqRangeMaxInput) {
applyEqRangeBtn.addEventListener('click', () => {
const newMin = parseInt(eqRangeMinInput.value, 10);
const newMax = parseInt(eqRangeMaxInput.value, 10);
// Validate range
if (isNaN(newMin) || isNaN(newMax)) {
alert('Please enter valid numbers for the range');
return;
}
if (newMin >= 0 || newMax <= 0) {
alert('Minimum must be negative and maximum must be positive');
return;
}
if (newMin < equalizerSettings.ABSOLUTE_MIN || newMax > equalizerSettings.ABSOLUTE_MAX) {
alert(
`Range must be between ${equalizerSettings.ABSOLUTE_MIN} and ${equalizerSettings.ABSOLUTE_MAX} dB`
);
return;
}
// Save new range
equalizerSettings.setRange(newMin, newMax);
currentRange = { min: newMin, max: newMax };
// Regenerate bands with new range
generateEQBands(currentBandCount, newMin, newMax);
// Update scale display
updateEQScale(newMin, newMax);
// Reset gains to flat
const flatGains = new Array(currentBandCount).fill(0);
audioContextManager.setAllGains(flatGains);
updateAllBandUI(flatGains);
// Reset to flat preset
if (eqPresetSelect) {
eqPresetSelect.value = 'flat';
equalizerSettings.setPreset('flat');
}
// Show feedback
const originalText = applyEqRangeBtn.textContent;
applyEqRangeBtn.textContent = 'Applied!';
setTimeout(() => {
applyEqRangeBtn.textContent = originalText;
}, 1500);
});
}
// Initialize reset DB range button
if (resetEqRangeBtn) {
resetEqRangeBtn.addEventListener('click', () => {
// Reset to default values
const defaultMin = equalizerSettings.DEFAULT_RANGE_MIN;
const defaultMax = equalizerSettings.DEFAULT_RANGE_MAX;
// Update inputs
if (eqRangeMinInput) eqRangeMinInput.value = defaultMin;
if (eqRangeMaxInput) eqRangeMaxInput.value = defaultMax;
// Save new range
equalizerSettings.setRange(defaultMin, defaultMax);
currentRange = { min: defaultMin, max: defaultMax };
// Regenerate bands with new range
generateEQBands(currentBandCount, defaultMin, defaultMax, currentFreqRange.min, currentFreqRange.max);
// Update scale display
updateEQScale(defaultMin, defaultMax);
// Reset gains to flat
const flatGains = new Array(currentBandCount).fill(0);
audioContextManager.setAllGains(flatGains);
updateAllBandUI(flatGains);
// Reset to flat preset
if (eqPresetSelect) {
eqPresetSelect.value = 'flat';
equalizerSettings.setPreset('flat');
}
// Show feedback
const originalText = resetEqRangeBtn.textContent;
resetEqRangeBtn.textContent = 'Reset!';
setTimeout(() => {
resetEqRangeBtn.textContent = originalText;
}, 1500);
});
}
// Initialize frequency range inputs
if (eqFreqMinInput) {
eqFreqMinInput.value = currentFreqRange.min;
}
if (eqFreqMaxInput) {
eqFreqMaxInput.value = currentFreqRange.max;
}
// Initialize apply frequency range button
if (applyEqFreqBtn && eqFreqMinInput && eqFreqMaxInput) {
applyEqFreqBtn.addEventListener('click', () => {
const newMin = parseInt(eqFreqMinInput.value, 10);
const newMax = parseInt(eqFreqMaxInput.value, 10);
// Validate range
if (isNaN(newMin) || isNaN(newMax)) {
alert('Please enter valid numbers for the frequency range');
return;
}
if (newMin < equalizerSettings.ABSOLUTE_FREQ_MIN || newMax > equalizerSettings.ABSOLUTE_FREQ_MAX) {
alert(
`Frequency range must be between ${equalizerSettings.ABSOLUTE_FREQ_MIN} Hz and ${equalizerSettings.ABSOLUTE_FREQ_MAX} Hz`
);
return;
}
if (newMin >= newMax) {
alert('Minimum frequency must be less than maximum frequency');
return;
}
// Save new frequency range
equalizerSettings.setFreqRange(newMin, newMax);
currentFreqRange = { min: newMin, max: newMax };
// Update audio context
audioContextManager.setFreqRange(newMin, newMax);
// Regenerate bands with new frequency range
generateEQBands(currentBandCount, currentRange.min, currentRange.max, newMin, newMax);
// Reset gains to flat
const flatGains = new Array(currentBandCount).fill(0);
audioContextManager.setAllGains(flatGains);
updateAllBandUI(flatGains);
// Reset to flat preset
if (eqPresetSelect) {
eqPresetSelect.value = 'flat';
equalizerSettings.setPreset('flat');
}
// Show feedback
const originalText = applyEqFreqBtn.textContent;
applyEqFreqBtn.textContent = 'Applied!';
setTimeout(() => {
applyEqFreqBtn.textContent = originalText;
}, 1500);
});
}
// Initialize reset frequency range button
if (resetEqFreqBtn) {
resetEqFreqBtn.addEventListener('click', () => {
// Reset to default values
const defaultMin = equalizerSettings.DEFAULT_FREQ_MIN;
const defaultMax = equalizerSettings.DEFAULT_FREQ_MAX;
// Update inputs
if (eqFreqMinInput) eqFreqMinInput.value = defaultMin;
if (eqFreqMaxInput) eqFreqMaxInput.value = defaultMax;
// Save new frequency range
equalizerSettings.setFreqRange(defaultMin, defaultMax);
currentFreqRange = { min: defaultMin, max: defaultMax };
// Update audio context
audioContextManager.setFreqRange(defaultMin, defaultMax);
// Regenerate bands with new frequency range
generateEQBands(currentBandCount, currentRange.min, currentRange.max, defaultMin, defaultMax);
// Reset gains to flat
const flatGains = new Array(currentBandCount).fill(0);
audioContextManager.setAllGains(flatGains);
updateAllBandUI(flatGains);
// Reset to flat preset
if (eqPresetSelect) {
eqPresetSelect.value = 'flat';
equalizerSettings.setPreset('flat');
}
// Show feedback
const originalText = resetEqFreqBtn.textContent;
resetEqFreqBtn.textContent = 'Reset!';
setTimeout(() => {
resetEqFreqBtn.textContent = originalText;
}, 1500);
});
}
// Generate initial EQ bands with current ranges
generateEQBands(currentBandCount, currentRange.min, currentRange.max, currentFreqRange.min, currentFreqRange.max);
// Listen for band count changes from other sources
window.addEventListener('equalizer-band-count-changed', (e) => {
if (e.detail && e.detail.bandCount) {
currentBandCount = e.detail.bandCount;
if (eqBandCountInput) eqBandCountInput.value = currentBandCount;
generateEQBands(
currentBandCount,
currentRange.min,
currentRange.max,
currentFreqRange.min,
currentFreqRange.max
);
}
});
// Listen for frequency range changes from other sources
window.addEventListener('equalizer-freq-range-changed', (e) => {
if (e.detail && e.detail.min !== undefined && e.detail.max !== undefined) {
currentFreqRange = { min: e.detail.min, max: e.detail.max };
if (eqFreqMinInput) eqFreqMinInput.value = currentFreqRange.min;
if (eqFreqMaxInput) eqFreqMaxInput.value = currentFreqRange.max;
generateEQBands(
currentBandCount,
currentRange.min,
currentRange.max,
currentFreqRange.min,
currentFreqRange.max
);
}
});
// Now Playing Mode
const nowPlayingMode = document.getElementById('now-playing-mode');
if (nowPlayingMode) {

View file

@ -791,6 +791,23 @@ export const equalizerSettings = {
ENABLED_KEY: 'equalizer-enabled',
GAINS_KEY: 'equalizer-gains',
PRESET_KEY: 'equalizer-preset',
CUSTOM_PRESETS_KEY: 'equalizer-custom-presets',
BAND_COUNT_KEY: 'equalizer-band-count',
RANGE_MIN_KEY: 'equalizer-range-min',
RANGE_MAX_KEY: 'equalizer-range-max',
FREQ_MIN_KEY: 'equalizer-freq-min',
FREQ_MAX_KEY: 'equalizer-freq-max',
DEFAULT_BAND_COUNT: 16,
MIN_BANDS: 3,
MAX_BANDS: 32,
DEFAULT_RANGE_MIN: -30,
DEFAULT_RANGE_MAX: 30,
ABSOLUTE_MIN: -60,
ABSOLUTE_MAX: 60,
DEFAULT_FREQ_MIN: 20,
DEFAULT_FREQ_MAX: 20000,
ABSOLUTE_FREQ_MIN: 10,
ABSOLUTE_FREQ_MAX: 96000,
isEnabled() {
try {
@ -805,25 +822,178 @@ export const equalizerSettings = {
localStorage.setItem(this.ENABLED_KEY, enabled ? 'true' : 'false');
},
getGains() {
getBandCount() {
try {
const stored = localStorage.getItem(this.BAND_COUNT_KEY);
if (stored) {
const count = parseInt(stored, 10);
if (!isNaN(count) && count >= this.MIN_BANDS && count <= this.MAX_BANDS) {
return count;
}
}
} catch {
/* ignore */
}
return this.DEFAULT_BAND_COUNT;
},
setBandCount(count) {
const validCount = Math.max(
this.MIN_BANDS,
Math.min(this.MAX_BANDS, parseInt(count, 10) || this.DEFAULT_BAND_COUNT)
);
localStorage.setItem(this.BAND_COUNT_KEY, validCount.toString());
},
getRangeMin() {
try {
const stored = localStorage.getItem(this.RANGE_MIN_KEY);
if (stored) {
const val = parseInt(stored, 10);
if (!isNaN(val) && val >= this.ABSOLUTE_MIN && val < 0) {
return val;
}
}
} catch {
/* ignore */
}
return this.DEFAULT_RANGE_MIN;
},
setRangeMin(value) {
const val = parseInt(value, 10);
if (!isNaN(val) && val >= this.ABSOLUTE_MIN && val < 0) {
localStorage.setItem(this.RANGE_MIN_KEY, val.toString());
return true;
}
return false;
},
getRangeMax() {
try {
const stored = localStorage.getItem(this.RANGE_MAX_KEY);
if (stored) {
const val = parseInt(stored, 10);
if (!isNaN(val) && val > 0 && val <= this.ABSOLUTE_MAX) {
return val;
}
}
} catch {
/* ignore */
}
return this.DEFAULT_RANGE_MAX;
},
setRangeMax(value) {
const val = parseInt(value, 10);
if (!isNaN(val) && val > 0 && val <= this.ABSOLUTE_MAX) {
localStorage.setItem(this.RANGE_MAX_KEY, val.toString());
return true;
}
return false;
},
getRange() {
return {
min: this.getRangeMin(),
max: this.getRangeMax(),
};
},
setRange(min, max) {
const validMin = this.setRangeMin(min);
const validMax = this.setRangeMax(max);
return validMin && validMax;
},
getFreqMin() {
try {
const stored = localStorage.getItem(this.FREQ_MIN_KEY);
if (stored) {
const val = parseInt(stored, 10);
if (!isNaN(val) && val >= this.ABSOLUTE_FREQ_MIN && val < this.DEFAULT_FREQ_MAX) {
return val;
}
}
} catch {
/* ignore */
}
return this.DEFAULT_FREQ_MIN;
},
setFreqMin(value) {
const val = parseInt(value, 10);
if (!isNaN(val) && val >= this.ABSOLUTE_FREQ_MIN && val < this.getFreqMax()) {
localStorage.setItem(this.FREQ_MIN_KEY, val.toString());
return true;
}
return false;
},
getFreqMax() {
try {
const stored = localStorage.getItem(this.FREQ_MAX_KEY);
if (stored) {
const val = parseInt(stored, 10);
if (!isNaN(val) && val > this.getFreqMin() && val <= this.ABSOLUTE_FREQ_MAX) {
return val;
}
}
} catch {
/* ignore */
}
return this.DEFAULT_FREQ_MAX;
},
setFreqMax(value) {
const val = parseInt(value, 10);
if (!isNaN(val) && val > this.getFreqMin() && val <= this.ABSOLUTE_FREQ_MAX) {
localStorage.setItem(this.FREQ_MAX_KEY, val.toString());
return true;
}
return false;
},
getFreqRange() {
return {
min: this.getFreqMin(),
max: this.getFreqMax(),
};
},
setFreqRange(min, max) {
const validMax = this.setFreqMax(max);
const validMin = this.setFreqMin(min);
return validMin && validMax;
},
getGains(bandCount) {
const count = bandCount || this.getBandCount();
try {
const stored = localStorage.getItem(this.GAINS_KEY);
if (stored) {
const gains = JSON.parse(stored);
if (Array.isArray(gains) && gains.length === 16) {
return gains;
if (Array.isArray(gains)) {
// If stored gains match current band count, return them
if (gains.length === count) {
return gains;
}
// If different band count, try to interpolate or return flat
if (gains.length > 0) {
return this._interpolateGains(gains, count);
}
}
}
} catch {
/* ignore */
}
// Return flat EQ (all zeros) by default
return new Array(16).fill(0);
return new Array(count).fill(0);
},
setGains(gains) {
try {
if (Array.isArray(gains) && gains.length === 16) {
if (Array.isArray(gains) && gains.length >= this.MIN_BANDS && gains.length <= this.MAX_BANDS) {
localStorage.setItem(this.GAINS_KEY, JSON.stringify(gains));
}
} catch (e) {
@ -831,6 +1001,31 @@ export const equalizerSettings = {
}
},
/**
* Interpolate gains array to match target band count
*/
_interpolateGains(sourceGains, targetCount) {
if (sourceGains.length === targetCount) {
return [...sourceGains];
}
const result = [];
for (let i = 0; i < targetCount; i++) {
// Map target index to source index
const sourceIndex = (i / (targetCount - 1)) * (sourceGains.length - 1);
const indexLow = Math.floor(sourceIndex);
const indexHigh = Math.min(Math.ceil(sourceIndex), sourceGains.length - 1);
const fraction = sourceIndex - indexLow;
// Linear interpolation
const lowValue = sourceGains[indexLow] || 0;
const highValue = sourceGains[indexHigh] || 0;
const interpolated = lowValue + (highValue - lowValue) * fraction;
result.push(Math.round(interpolated * 10) / 10);
}
return result;
},
getPreset() {
try {
return localStorage.getItem(this.PRESET_KEY) || 'flat';
@ -842,6 +1037,102 @@ export const equalizerSettings = {
setPreset(preset) {
localStorage.setItem(this.PRESET_KEY, preset);
},
// Custom Preset Methods
getCustomPresets() {
try {
const stored = localStorage.getItem(this.CUSTOM_PRESETS_KEY);
if (stored) {
const presets = JSON.parse(stored);
if (typeof presets === 'object' && presets !== null) {
return presets;
}
}
} catch {
/* ignore */
}
return {};
},
saveCustomPreset(name, gains) {
try {
if (!name || !Array.isArray(gains) || gains.length < this.MIN_BANDS || gains.length > this.MAX_BANDS) {
console.warn('[EQ] Invalid preset data');
return false;
}
// Sanitize name - remove special characters and limit length
const sanitizedName = name
.trim()
.substring(0, 50)
.replace(/[^\w\s-]/g, '');
if (!sanitizedName) {
console.warn('[EQ] Invalid preset name');
return false;
}
const presets = this.getCustomPresets();
const presetId = 'custom_' + Date.now();
presets[presetId] = {
name: sanitizedName,
gains: gains.map((g) => Math.round(g * 10) / 10), // Round to 1 decimal place
bandCount: gains.length,
createdAt: Date.now(),
};
localStorage.setItem(this.CUSTOM_PRESETS_KEY, JSON.stringify(presets));
return presetId;
} catch (e) {
console.warn('[EQ] Failed to save custom preset:', e);
return false;
}
},
deleteCustomPreset(presetId) {
try {
const presets = this.getCustomPresets();
if (presets[presetId]) {
delete presets[presetId];
localStorage.setItem(this.CUSTOM_PRESETS_KEY, JSON.stringify(presets));
return true;
}
return false;
} catch (e) {
console.warn('[EQ] Failed to delete custom preset:', e);
return false;
}
},
updateCustomPreset(presetId, name, gains) {
try {
const presets = this.getCustomPresets();
if (!presets[presetId]) {
return false;
}
if (name !== undefined) {
const sanitizedName = name
.trim()
.substring(0, 50)
.replace(/[^\w\s-]/g, '');
if (sanitizedName) {
presets[presetId].name = sanitizedName;
}
}
if (Array.isArray(gains) && gains.length === 16) {
presets[presetId].gains = gains.map((g) => Math.round(g * 10) / 10);
presets[presetId].updatedAt = Date.now();
}
localStorage.setItem(this.CUSTOM_PRESETS_KEY, JSON.stringify(presets));
return true;
} catch (e) {
console.warn('[EQ] Failed to update custom preset:', e);
return false;
}
},
};
export const monoAudioSettings = {

View file

@ -5805,6 +5805,42 @@ textarea:focus {
box-shadow: 0 0 0 3px rgb(var(--highlight-rgb), 0.2);
}
.eq-band-count-input {
width: 60px;
padding: 0.5rem;
background: var(--input);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--foreground);
font-size: 0.9rem;
text-align: center;
cursor: pointer;
transition:
border-color var(--transition-fast),
box-shadow var(--transition-fast);
}
.eq-band-count-input:hover {
border-color: var(--primary);
}
.eq-band-count-input:focus {
outline: none;
border-color: var(--ring);
box-shadow: 0 0 0 3px rgb(var(--highlight-rgb), 0.2);
}
/* Hide number input arrows */
.eq-band-count-input::-webkit-outer-spin-button,
.eq-band-count-input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.eq-band-count-input[type='number'] {
-moz-appearance: textfield;
}
#equalizer-reset-btn {
padding: 0.5rem;
display: flex;
@ -5831,6 +5867,214 @@ textarea:focus {
transform: rotate(-45deg);
}
/* Custom Preset Controls */
.custom-preset-controls {
margin-top: var(--spacing-md);
padding-top: var(--spacing-md);
border-top: 1px solid var(--border);
}
.custom-preset-input-row {
display: flex;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-sm);
}
#custom-preset-name {
flex: 1;
padding: 0.5rem 0.75rem;
background: var(--input);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--foreground);
font-size: 0.9rem;
transition: border-color var(--transition-fast);
}
#custom-preset-name:focus {
outline: none;
border-color: var(--ring);
box-shadow: 0 0 0 3px rgb(var(--highlight-rgb), 0.2);
}
#save-custom-preset-btn {
display: flex;
align-items: center;
gap: var(--spacing-xs);
padding: 0.5rem 1rem;
white-space: nowrap;
}
#save-custom-preset-btn svg {
flex-shrink: 0;
}
.delete-preset-btn {
display: flex;
align-items: center;
gap: var(--spacing-xs);
padding: 0.5rem 1rem;
font-size: 0.85rem;
color: var(--destructive);
border-color: var(--destructive);
opacity: 0.8;
transition: opacity var(--transition-fast);
}
.delete-preset-btn:hover {
opacity: 1;
background: var(--destructive);
color: var(--destructive-foreground);
}
.delete-preset-btn svg {
flex-shrink: 0;
}
/* EQ Range Controls */
.eq-range-controls {
display: flex;
align-items: center;
gap: var(--spacing-sm);
margin-top: var(--spacing-sm);
padding-top: var(--spacing-sm);
border-top: 1px solid var(--border);
flex-wrap: wrap;
}
.eq-range-controls label {
font-size: 0.9rem;
color: var(--muted-foreground);
font-weight: 500;
}
.eq-range-controls span {
font-size: 0.9rem;
color: var(--muted-foreground);
}
.eq-range-input {
width: 60px;
padding: 0.4rem 0.5rem;
background: var(--input);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--foreground);
font-size: 0.9rem;
text-align: center;
transition: border-color var(--transition-fast);
}
.eq-range-input:hover {
border-color: var(--primary);
}
.eq-range-input:focus {
outline: none;
border-color: var(--ring);
box-shadow: 0 0 0 3px rgb(var(--highlight-rgb), 0.2);
}
/* Hide number input arrows */
.eq-range-input::-webkit-outer-spin-button,
.eq-range-input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.eq-range-input[type='number'] {
-moz-appearance: textfield;
}
#apply-eq-range-btn {
padding: 0.4rem 0.75rem;
font-size: 0.85rem;
}
#reset-eq-range-btn {
padding: 0.4rem 0.75rem;
font-size: 0.85rem;
margin-left: var(--spacing-xs);
}
/* EQ Frequency Range Controls */
.eq-freq-controls {
display: flex;
align-items: center;
gap: var(--spacing-sm);
margin-top: var(--spacing-sm);
padding-top: var(--spacing-sm);
border-top: 1px solid var(--border);
flex-wrap: wrap;
}
.eq-freq-controls label {
font-size: 0.9rem;
color: var(--muted-foreground);
font-weight: 500;
}
.eq-freq-controls span {
font-size: 0.9rem;
color: var(--muted-foreground);
}
.eq-freq-input {
width: 70px;
padding: 0.4rem 0.5rem;
background: var(--input);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--foreground);
font-size: 0.9rem;
text-align: center;
transition: border-color var(--transition-fast);
}
.eq-freq-input:hover {
border-color: var(--primary);
}
.eq-freq-input:focus {
outline: none;
border-color: var(--ring);
box-shadow: 0 0 0 3px rgb(var(--highlight-rgb), 0.2);
}
/* Hide number input arrows */
.eq-freq-input::-webkit-outer-spin-button,
.eq-freq-input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.eq-freq-input[type='number'] {
-moz-appearance: textfield;
}
#apply-eq-freq-btn {
padding: 0.4rem 0.75rem;
font-size: 0.85rem;
}
#reset-eq-freq-btn {
padding: 0.4rem 0.75rem;
font-size: 0.85rem;
margin-left: var(--spacing-xs);
}
/* Equalizer preset dropdown styling */
.equalizer-preset-row select optgroup {
font-weight: 600;
color: var(--foreground);
padding: var(--spacing-xs) 0;
}
.equalizer-preset-row select optgroup option {
font-weight: 400;
padding-left: var(--spacing-sm);
}
.equalizer-bands {
display: flex;
justify-content: space-between;