equalizer changes
This commit is contained in:
parent
6a438a0551
commit
0b20caff69
6 changed files with 1755 additions and 401 deletions
373
index.html
373
index.html
|
|
@ -3396,23 +3396,38 @@
|
||||||
<div class="equalizer-preset-row">
|
<div class="equalizer-preset-row">
|
||||||
<label for="equalizer-preset-select">Preset</label>
|
<label for="equalizer-preset-select">Preset</label>
|
||||||
<select id="equalizer-preset-select">
|
<select id="equalizer-preset-select">
|
||||||
<option value="flat">Flat</option>
|
<optgroup label="Built-in Presets">
|
||||||
<option value="bass_boost">Bass Boost</option>
|
<option value="flat">Flat</option>
|
||||||
<option value="bass_reducer">Bass Reducer</option>
|
<option value="bass_boost">Bass Boost</option>
|
||||||
<option value="treble_boost">Treble Boost</option>
|
<option value="bass_reducer">Bass Reducer</option>
|
||||||
<option value="treble_reducer">Treble Reducer</option>
|
<option value="treble_boost">Treble Boost</option>
|
||||||
<option value="vocal_boost">Vocal Boost</option>
|
<option value="treble_reducer">Treble Reducer</option>
|
||||||
<option value="loudness">Loudness</option>
|
<option value="vocal_boost">Vocal Boost</option>
|
||||||
<option value="rock">Rock</option>
|
<option value="loudness">Loudness</option>
|
||||||
<option value="pop">Pop</option>
|
<option value="rock">Rock</option>
|
||||||
<option value="classical">Classical</option>
|
<option value="pop">Pop</option>
|
||||||
<option value="jazz">Jazz</option>
|
<option value="classical">Classical</option>
|
||||||
<option value="electronic">Electronic</option>
|
<option value="jazz">Jazz</option>
|
||||||
<option value="hip_hop">Hip-Hop</option>
|
<option value="electronic">Electronic</option>
|
||||||
<option value="r_and_b">R&B</option>
|
<option value="hip_hop">Hip-Hop</option>
|
||||||
<option value="acoustic">Acoustic</option>
|
<option value="r_and_b">R&B</option>
|
||||||
<option value="podcast">Podcast / Speech</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>
|
</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
|
<button
|
||||||
id="equalizer-reset-btn"
|
id="equalizer-reset-btn"
|
||||||
class="btn-secondary"
|
class="btn-secondary"
|
||||||
|
|
@ -3434,218 +3449,128 @@
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="equalizer-bands" id="equalizer-bands">
|
<div class="equalizer-bands" id="equalizer-bands">
|
||||||
<!-- Bands will be dynamically generated by JavaScript -->
|
<!-- 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>
|
||||||
|
|
||||||
<div class="equalizer-scale">
|
<div class="equalizer-scale">
|
||||||
|
|
|
||||||
|
|
@ -1,79 +1,95 @@
|
||||||
// js/audio-context.js
|
// js/audio-context.js
|
||||||
// Shared Audio Context Manager - handles EQ and provides context for visualizer
|
// Shared Audio Context Manager - handles EQ and provides context for visualizer
|
||||||
|
// Supports 3-32 parametric EQ bands
|
||||||
|
|
||||||
import { equalizerSettings, monoAudioSettings } from './storage.js';
|
import { equalizerSettings, monoAudioSettings } from './storage.js';
|
||||||
|
|
||||||
// Standard 16-band ISO center frequencies (Hz)
|
// Standard 16-band ISO center frequencies (Hz) - for reference
|
||||||
const EQ_FREQUENCIES = [25, 40, 63, 100, 160, 250, 400, 630, 1000, 1600, 2500, 4000, 6300, 10000, 16000, 20000];
|
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)
|
// Generate frequency array for given number of bands using logarithmic spacing
|
||||||
const EQ_PRESETS = {
|
function generateFrequencies(bandCount, minFreq = 20, maxFreq = 20000) {
|
||||||
flat: {
|
const frequencies = [];
|
||||||
name: 'Flat',
|
const safeMin = Math.max(10, minFreq);
|
||||||
gains: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
const safeMax = Math.min(96000, maxFreq);
|
||||||
},
|
|
||||||
bass_boost: {
|
for (let i = 0; i < bandCount; i++) {
|
||||||
name: 'Bass Boost',
|
// Logarithmic interpolation
|
||||||
gains: [6, 5, 4.5, 4, 3, 2, 1, 0.5, 0, 0, 0, 0, 0, 0, 0, 0],
|
const t = i / (bandCount - 1);
|
||||||
},
|
const freq = safeMin * Math.pow(safeMax / safeMin, t);
|
||||||
bass_reducer: {
|
frequencies.push(Math.round(freq));
|
||||||
name: 'Bass Reducer',
|
}
|
||||||
gains: [-6, -5, -4, -3, -2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
||||||
},
|
return frequencies;
|
||||||
treble_boost: {
|
}
|
||||||
name: 'Treble Boost',
|
|
||||||
gains: [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 5.5, 6],
|
// Generate frequency labels for display
|
||||||
},
|
function generateFrequencyLabels(frequencies) {
|
||||||
treble_reducer: {
|
return frequencies.map((freq) => {
|
||||||
name: 'Treble Reducer',
|
if (freq < 1000) {
|
||||||
gains: [0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -2, -3, -4, -5, -5.5, -6],
|
return freq.toString();
|
||||||
},
|
} else if (freq < 10000) {
|
||||||
vocal_boost: {
|
return (freq / 1000).toFixed(freq % 1000 === 0 ? 0 : 1) + 'K';
|
||||||
name: 'Vocal Boost',
|
} else {
|
||||||
gains: [-2, -1, 0, 0, 1, 2, 3, 4, 4, 3, 2, 1, 0, 0, -1, -2],
|
return (freq / 1000).toFixed(0) + 'K';
|
||||||
},
|
}
|
||||||
loudness: {
|
});
|
||||||
name: 'Loudness',
|
}
|
||||||
gains: [5, 4, 3, 1, 0, -1, -1, 0, 0, 1, 2, 3, 4, 4.5, 4, 3],
|
|
||||||
},
|
// EQ Presets (16-band default)
|
||||||
rock: {
|
const EQ_PRESETS_16 = {
|
||||||
name: 'Rock',
|
flat: { name: 'Flat', gains: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] },
|
||||||
gains: [4, 3.5, 3, 2, -1, -2, -1, 1, 2, 3, 3.5, 4, 4, 3, 2, 1],
|
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] },
|
||||||
pop: {
|
treble_boost: { name: 'Treble Boost', gains: [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 5.5, 6] },
|
||||||
name: 'Pop',
|
treble_reducer: { name: 'Treble Reducer', gains: [0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -2, -3, -4, -5, -5.5, -6] },
|
||||||
gains: [-1, 0, 1, 2, 3, 3, 2, 1, 0, 1, 2, 2, 2, 2, 1, 0],
|
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] },
|
||||||
classical: {
|
rock: { name: 'Rock', gains: [4, 3.5, 3, 2, -1, -2, -1, 1, 2, 3, 3.5, 4, 4, 3, 2, 1] },
|
||||||
name: 'Classical',
|
pop: { name: 'Pop', gains: [-1, 0, 1, 2, 3, 3, 2, 1, 0, 1, 2, 2, 2, 2, 1, 0] },
|
||||||
gains: [3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 3, 2],
|
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] },
|
||||||
jazz: {
|
electronic: { name: 'Electronic', gains: [4, 3.5, 3, 1, 0, -1, 0, 1, 2, 3, 3, 2, 2, 3, 4, 3.5] },
|
||||||
name: 'Jazz',
|
hip_hop: { name: 'Hip-Hop', gains: [5, 4.5, 4, 3, 1, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 2] },
|
||||||
gains: [3, 2, 1, 1, -1, -1, 0, 1, 2, 2, 2, 2, 2, 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] },
|
||||||
electronic: {
|
podcast: { name: 'Podcast / Speech', gains: [-3, -2, -1, 0, 1, 2, 3, 4, 4, 3, 2, 1, 0, -1, -2, -3] },
|
||||||
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 {
|
class AudioContextManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.audioContext = null;
|
this.audioContext = null;
|
||||||
|
|
@ -86,10 +102,15 @@ class AudioContextManager {
|
||||||
this.isEQEnabled = false;
|
this.isEQEnabled = false;
|
||||||
this.isMonoAudioEnabled = false;
|
this.isMonoAudioEnabled = false;
|
||||||
this.monoMergerNode = null;
|
this.monoMergerNode = null;
|
||||||
this.currentGains = new Array(16).fill(0);
|
|
||||||
this.audio = null;
|
this.audio = null;
|
||||||
this.currentVolume = 1.0;
|
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)
|
// Callbacks for audio graph changes (for visualizers like Butterchurn)
|
||||||
this._graphChangeCallbacks = [];
|
this._graphChangeCallbacks = [];
|
||||||
|
|
||||||
|
|
@ -97,6 +118,128 @@ class AudioContextManager {
|
||||||
this._loadSettings();
|
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
|
* Register a callback to be called when audio graph is reconnected
|
||||||
* @param {Function} callback - Function to call when graph changes
|
* @param {Function} callback - Function to call when graph changes
|
||||||
|
|
@ -159,15 +302,8 @@ class AudioContextManager {
|
||||||
this.analyser.fftSize = 512;
|
this.analyser.fftSize = 512;
|
||||||
this.analyser.smoothingTimeConstant = 0.7;
|
this.analyser.smoothingTimeConstant = 0.7;
|
||||||
|
|
||||||
// Create 16 biquad filters for EQ
|
// Create biquad filters for EQ with dynamic band count
|
||||||
this.filters = EQ_FREQUENCIES.map((freq, index) => {
|
this._createEQ();
|
||||||
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 output gain node
|
// Create output gain node
|
||||||
this.outputNode = this.audioContext.createGain();
|
this.outputNode = this.audioContext.createGain();
|
||||||
|
|
@ -180,17 +316,11 @@ class AudioContextManager {
|
||||||
// Create mono audio merger node
|
// Create mono audio merger node
|
||||||
this.monoMergerNode = this.audioContext.createChannelMerger(2);
|
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
|
// Connect the audio graph based on EQ and mono state
|
||||||
this._connectGraph();
|
this._connectGraph();
|
||||||
|
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
console.log('[AudioContext] Initialized with 16-band EQ');
|
console.log(`[AudioContext] Initialized with ${this.bandCount}-band EQ`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[AudioContext] Init failed:', e);
|
console.warn('[AudioContext] Init failed:', e);
|
||||||
}
|
}
|
||||||
|
|
@ -240,7 +370,13 @@ class AudioContextManager {
|
||||||
|
|
||||||
if (this.isEQEnabled && this.filters.length > 0) {
|
if (this.isEQEnabled && this.filters.length > 0) {
|
||||||
// EQ enabled: lastNode -> EQ filters -> output -> analyser -> volume -> destination
|
// 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]);
|
lastNode.connect(this.filters[0]);
|
||||||
|
this.filters[this.filters.length - 1].connect(this.outputNode);
|
||||||
this.outputNode.connect(this.analyser);
|
this.outputNode.connect(this.analyser);
|
||||||
this.analyser.connect(this.volumeNode);
|
this.analyser.connect(this.volumeNode);
|
||||||
this.volumeNode.connect(this.audioContext.destination);
|
this.volumeNode.connect(this.audioContext.destination);
|
||||||
|
|
@ -374,13 +510,28 @@ class AudioContextManager {
|
||||||
return this.isInitialized && this.isMonoAudioEnabled;
|
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
|
* Set gain for a specific band
|
||||||
*/
|
*/
|
||||||
setBandGain(bandIndex, gainDb) {
|
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;
|
this.currentGains[bandIndex] = clampedGain;
|
||||||
|
|
||||||
if (this.filters[bandIndex] && this.audioContext) {
|
if (this.filters[bandIndex] && this.audioContext) {
|
||||||
|
|
@ -395,12 +546,18 @@ class AudioContextManager {
|
||||||
* Set all band gains at once
|
* Set all band gains at once
|
||||||
*/
|
*/
|
||||||
setAllGains(gains) {
|
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;
|
const now = this.audioContext?.currentTime || 0;
|
||||||
|
|
||||||
gains.forEach((gain, index) => {
|
adjustedGains.forEach((gain, index) => {
|
||||||
const clampedGain = Math.max(-30, Math.min(30, gain));
|
const clampedGain = this._clampGain(gain);
|
||||||
this.currentGains[index] = clampedGain;
|
this.currentGains[index] = clampedGain;
|
||||||
|
|
||||||
if (this.filters[index]) {
|
if (this.filters[index]) {
|
||||||
|
|
@ -415,7 +572,8 @@ class AudioContextManager {
|
||||||
* Apply a preset
|
* Apply a preset
|
||||||
*/
|
*/
|
||||||
applyPreset(presetKey) {
|
applyPreset(presetKey) {
|
||||||
const preset = EQ_PRESETS[presetKey];
|
const presets = getPresetsForBandCount(this.bandCount);
|
||||||
|
const preset = presets[presetKey];
|
||||||
if (!preset) return;
|
if (!preset) return;
|
||||||
|
|
||||||
this.setAllGains(preset.gains);
|
this.setAllGains(preset.gains);
|
||||||
|
|
@ -426,7 +584,7 @@ class AudioContextManager {
|
||||||
* Reset all bands to flat
|
* Reset all bands to flat
|
||||||
*/
|
*/
|
||||||
reset() {
|
reset() {
|
||||||
this.setAllGains(new Array(16).fill(0));
|
this.setAllGains(new Array(this.bandCount).fill(0));
|
||||||
equalizerSettings.setPreset('flat');
|
equalizerSettings.setPreset('flat');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -437,12 +595,22 @@ class AudioContextManager {
|
||||||
return [...this.currentGains];
|
return [...this.currentGains];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current band count
|
||||||
|
*/
|
||||||
|
getBandCount() {
|
||||||
|
return this.bandCount;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load settings from storage
|
* Load settings from storage
|
||||||
*/
|
*/
|
||||||
_loadSettings() {
|
_loadSettings() {
|
||||||
this.isEQEnabled = equalizerSettings.isEnabled();
|
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();
|
this.isMonoAudioEnabled = monoAudioSettings.isEnabled();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -450,5 +618,12 @@ class AudioContextManager {
|
||||||
// Export singleton instance
|
// Export singleton instance
|
||||||
export const audioContextManager = new AudioContextManager();
|
export const audioContextManager = new AudioContextManager();
|
||||||
|
|
||||||
// Export presets for settings UI
|
// Export presets and helper functions for settings UI
|
||||||
export { EQ_PRESETS };
|
export {
|
||||||
|
EQ_PRESETS,
|
||||||
|
generateFrequencies,
|
||||||
|
generateFrequencyLabels,
|
||||||
|
getPresetsForBandCount,
|
||||||
|
interpolatePreset,
|
||||||
|
EQ_PRESETS_16,
|
||||||
|
};
|
||||||
|
|
|
||||||
259
js/equalizer.js
259
js/equalizer.js
|
|
@ -1,13 +1,13 @@
|
||||||
// js/equalizer.js
|
// 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';
|
import { equalizerSettings } from './storage.js';
|
||||||
|
|
||||||
// Standard 16-band ISO center frequencies (Hz)
|
// Standard 16-band ISO center frequencies (Hz) - kept for reference
|
||||||
const EQ_FREQUENCIES = [25, 40, 63, 100, 160, 250, 400, 630, 1000, 1600, 2500, 4000, 6300, 10000, 16000, 20000];
|
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
|
// Frequency labels for UI display
|
||||||
const FREQUENCY_LABELS = [
|
const DEFAULT_FREQUENCY_LABELS = [
|
||||||
'25',
|
'25',
|
||||||
'40',
|
'40',
|
||||||
'63',
|
'63',
|
||||||
|
|
@ -26,8 +26,37 @@ const FREQUENCY_LABELS = [
|
||||||
'20K',
|
'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)
|
// EQ Presets (gain values in dB for each of the 16 bands)
|
||||||
const EQ_PRESETS = {
|
const EQ_PRESETS_16BAND = {
|
||||||
flat: {
|
flat: {
|
||||||
name: 'Flat',
|
name: 'Flat',
|
||||||
gains: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
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 {
|
export class Equalizer {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.audioContext = null;
|
this.audioContext = null;
|
||||||
|
|
@ -105,13 +165,99 @@ export class Equalizer {
|
||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
this.audio = null;
|
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
|
// Store current gains
|
||||||
this.currentGains = new Array(16).fill(0);
|
this.currentGains = new Array(this.bandCount).fill(0);
|
||||||
|
|
||||||
// Load saved settings
|
// Load saved settings
|
||||||
this._loadSettings();
|
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
|
* Initialize the equalizer with a shared AudioContext
|
||||||
* This should be called after the visualizer creates the context
|
* This should be called after the visualizer creates the context
|
||||||
|
|
@ -127,15 +273,15 @@ export class Equalizer {
|
||||||
this.source = sourceNode;
|
this.source = sourceNode;
|
||||||
this.audio = audioElement;
|
this.audio = audioElement;
|
||||||
|
|
||||||
// Create 16 biquad filters for each frequency band
|
// Create biquad filters for each frequency band
|
||||||
this.filters = EQ_FREQUENCIES.map((freq, index) => {
|
this.filters = this.frequencies.map((freq, index) => {
|
||||||
const filter = this.audioContext.createBiquadFilter();
|
const filter = this.audioContext.createBiquadFilter();
|
||||||
|
|
||||||
// Use peaking filter for all bands (best for EQ)
|
// Use peaking filter for all bands (best for EQ)
|
||||||
filter.type = 'peaking';
|
filter.type = 'peaking';
|
||||||
filter.frequency.value = freq;
|
filter.frequency.value = freq;
|
||||||
filter.Q.value = this._calculateQ(index);
|
filter.Q.value = this._calculateQ(index);
|
||||||
filter.gain.value = this.currentGains[index];
|
filter.gain.value = this.currentGains[index] || 0;
|
||||||
|
|
||||||
return filter;
|
return filter;
|
||||||
});
|
});
|
||||||
|
|
@ -154,7 +300,7 @@ export class Equalizer {
|
||||||
this._enableFilters();
|
this._enableFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Equalizer] Initialized with 16 bands');
|
console.log(`[Equalizer] Initialized with ${this.bandCount} bands`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[Equalizer] Init failed:', e);
|
console.warn('[Equalizer] Init failed:', e);
|
||||||
}
|
}
|
||||||
|
|
@ -167,7 +313,10 @@ export class Equalizer {
|
||||||
_calculateQ(_index) {
|
_calculateQ(_index) {
|
||||||
// For 16-band 1/2 octave spacing, Q ≈ 2.87
|
// For 16-band 1/2 octave spacing, Q ≈ 2.87
|
||||||
// Slightly lower Q for smoother response
|
// 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;
|
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
|
* Set gain for a specific band
|
||||||
* @param {number} bandIndex - Band index (0-15)
|
* @param {number} bandIndex - Band index
|
||||||
* @param {number} gainDb - Gain in dB (-12 to +12)
|
* @param {number} gainDb - Gain in dB
|
||||||
*/
|
*/
|
||||||
setBandGain(bandIndex, gainDb) {
|
setBandGain(bandIndex, gainDb) {
|
||||||
if (bandIndex < 0 || bandIndex >= 16) return;
|
if (bandIndex < 0 || bandIndex >= this.bandCount) return;
|
||||||
|
|
||||||
// Clamp gain to valid range
|
// Clamp gain to valid range
|
||||||
const clampedGain = Math.max(-30, Math.min(30, gainDb));
|
const clampedGain = this._clampGain(gainDb);
|
||||||
this.currentGains[bandIndex] = clampedGain;
|
this.currentGains[bandIndex] = clampedGain;
|
||||||
|
|
||||||
if (this.filters[bandIndex]) {
|
if (this.filters[bandIndex]) {
|
||||||
|
|
@ -271,15 +435,21 @@ export class Equalizer {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set all band gains at once
|
* 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) {
|
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;
|
const now = this.audioContext?.currentTime || 0;
|
||||||
|
|
||||||
gains.forEach((gain, index) => {
|
adjustedGains.forEach((gain, index) => {
|
||||||
const clampedGain = Math.max(-30, Math.min(30, gain));
|
const clampedGain = this._clampGain(gain);
|
||||||
this.currentGains[index] = clampedGain;
|
this.currentGains[index] = clampedGain;
|
||||||
|
|
||||||
if (this.filters[index]) {
|
if (this.filters[index]) {
|
||||||
|
|
@ -295,7 +465,8 @@ export class Equalizer {
|
||||||
* @param {string} presetKey - Key from EQ_PRESETS
|
* @param {string} presetKey - Key from EQ_PRESETS
|
||||||
*/
|
*/
|
||||||
applyPreset(presetKey) {
|
applyPreset(presetKey) {
|
||||||
const preset = EQ_PRESETS[presetKey];
|
const presets = getPresetsForBandCount(this.bandCount);
|
||||||
|
const preset = presets[presetKey];
|
||||||
if (!preset) return;
|
if (!preset) return;
|
||||||
|
|
||||||
this.setAllGains(preset.gains);
|
this.setAllGains(preset.gains);
|
||||||
|
|
@ -306,37 +477,47 @@ export class Equalizer {
|
||||||
* Reset all bands to flat (0 dB)
|
* Reset all bands to flat (0 dB)
|
||||||
*/
|
*/
|
||||||
reset() {
|
reset() {
|
||||||
this.setAllGains(new Array(16).fill(0));
|
this.setAllGains(new Array(this.bandCount).fill(0));
|
||||||
equalizerSettings.setPreset('flat');
|
equalizerSettings.setPreset('flat');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current gains
|
* Get current gains
|
||||||
* @returns {number[]} Array of 16 gain values
|
* @returns {number[]} Array of gain values
|
||||||
*/
|
*/
|
||||||
getGains() {
|
getGains() {
|
||||||
return [...this.currentGains];
|
return [...this.currentGains];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get frequency labels
|
* Get current band count
|
||||||
|
* @returns {number} Number of bands
|
||||||
*/
|
*/
|
||||||
static getFrequencyLabels() {
|
getBandCount() {
|
||||||
return FREQUENCY_LABELS;
|
return this.bandCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get frequency labels for UI
|
||||||
|
* @returns {string[]} Array of frequency labels
|
||||||
|
*/
|
||||||
|
getFrequencyLabels() {
|
||||||
|
return this.frequencyLabels;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get frequencies
|
* Get frequencies
|
||||||
|
* @returns {number[]} Array of frequency values
|
||||||
*/
|
*/
|
||||||
static getFrequencies() {
|
getFrequencies() {
|
||||||
return EQ_FREQUENCIES;
|
return this.frequencies;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get available presets
|
* Get available presets (static method for default 16 bands)
|
||||||
*/
|
*/
|
||||||
static getPresets() {
|
static getPresets(bandCount = 16) {
|
||||||
return EQ_PRESETS;
|
return getPresetsForBandCount(bandCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -344,7 +525,11 @@ export class Equalizer {
|
||||||
*/
|
*/
|
||||||
_loadSettings() {
|
_loadSettings() {
|
||||||
this.isEnabled = equalizerSettings.isEnabled();
|
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 singleton instance
|
||||||
export const equalizer = new Equalizer();
|
export const equalizer = new Equalizer();
|
||||||
|
|
||||||
// Export constants
|
// Export helper functions and constants
|
||||||
export { EQ_FREQUENCIES, FREQUENCY_LABELS, EQ_PRESETS };
|
export {
|
||||||
|
generateFrequencies,
|
||||||
|
generateFrequencyLabels,
|
||||||
|
getPresetsForBandCount,
|
||||||
|
interpolatePreset,
|
||||||
|
DEFAULT_EQ_FREQUENCIES,
|
||||||
|
DEFAULT_FREQUENCY_LABELS,
|
||||||
|
EQ_PRESETS_16BAND as EQ_PRESETS,
|
||||||
|
};
|
||||||
|
|
|
||||||
614
js/settings.js
614
js/settings.js
|
|
@ -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 eqToggle = document.getElementById('equalizer-enabled-toggle');
|
||||||
const eqContainer = document.getElementById('equalizer-container');
|
const eqContainer = document.getElementById('equalizer-container');
|
||||||
const eqPresetSelect = document.getElementById('equalizer-preset-select');
|
const eqPresetSelect = document.getElementById('equalizer-preset-select');
|
||||||
const eqResetBtn = document.getElementById('equalizer-reset-btn');
|
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
|
* 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
|
* Update all band sliders and displays from an array of gains
|
||||||
*/
|
*/
|
||||||
const updateAllBandUI = (gains) => {
|
const updateAllBandUI = (gains) => {
|
||||||
|
const eqBands = eqBandsContainer?.querySelectorAll('.eq-band');
|
||||||
|
if (!eqBands) return;
|
||||||
|
|
||||||
eqBands.forEach((bandEl, index) => {
|
eqBands.forEach((bandEl, index) => {
|
||||||
const slider = bandEl.querySelector('.eq-slider');
|
const slider = bandEl.querySelector('.eq-slider');
|
||||||
if (slider && gains[index] !== undefined) {
|
if (slider && gains[index] !== undefined) {
|
||||||
|
|
@ -907,48 +1008,59 @@ export function initializeSettings(scrobbler, player, api, ui) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize EQ toggle
|
/**
|
||||||
if (eqToggle) {
|
* Populate custom presets in the dropdown
|
||||||
const isEnabled = equalizerSettings.isEnabled();
|
*/
|
||||||
eqToggle.checked = isEnabled;
|
const populateCustomPresets = () => {
|
||||||
updateEQContainerVisibility(isEnabled);
|
if (!customPresetsOptgroup) return;
|
||||||
|
|
||||||
eqToggle.addEventListener('change', (e) => {
|
// Clear existing custom presets
|
||||||
const enabled = e.target.checked;
|
customPresetsOptgroup.innerHTML = '';
|
||||||
audioContextManager.toggleEQ(enabled);
|
|
||||||
updateEQContainerVisibility(enabled);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize preset selector
|
const customPresets = equalizerSettings.getCustomPresets();
|
||||||
if (eqPresetSelect) {
|
const presetIds = Object.keys(customPresets);
|
||||||
eqPresetSelect.value = equalizerSettings.getPreset();
|
|
||||||
|
|
||||||
eqPresetSelect.addEventListener('change', (e) => {
|
if (presetIds.length === 0) {
|
||||||
const presetKey = e.target.value;
|
const emptyOption = document.createElement('option');
|
||||||
const preset = EQ_PRESETS[presetKey];
|
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);
|
* Check if a preset ID is a custom preset
|
||||||
updateAllBandUI(preset.gains);
|
*/
|
||||||
}
|
const isCustomPreset = (presetId) => {
|
||||||
});
|
return presetId && presetId.startsWith('custom_');
|
||||||
}
|
};
|
||||||
|
|
||||||
// Initialize reset button
|
/**
|
||||||
if (eqResetBtn) {
|
* Update delete button visibility based on selected preset
|
||||||
eqResetBtn.addEventListener('click', () => {
|
*/
|
||||||
audioContextManager.reset();
|
const updateDeleteButtonVisibility = () => {
|
||||||
updateAllBandUI(new Array(16).fill(0));
|
if (!deleteCustomPresetBtn || !eqPresetSelect) return;
|
||||||
if (eqPresetSelect) {
|
const isCustom = isCustomPreset(eqPresetSelect.value);
|
||||||
eqPresetSelect.value = 'flat';
|
deleteCustomPresetBtn.style.display = isCustom ? 'flex' : 'none';
|
||||||
}
|
};
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize all band sliders
|
/**
|
||||||
if (eqBands.length > 0) {
|
* Initialize band slider event listeners
|
||||||
const savedGains = equalizerSettings.getGains();
|
*/
|
||||||
|
const initializeBandSliders = () => {
|
||||||
|
const eqBands = eqBandsContainer?.querySelectorAll('.eq-band');
|
||||||
|
if (!eqBands || eqBands.length === 0) return;
|
||||||
|
|
||||||
|
const savedGains = equalizerSettings.getGains(currentBandCount);
|
||||||
|
|
||||||
eqBands.forEach((bandEl) => {
|
eqBands.forEach((bandEl) => {
|
||||||
const bandIndex = parseInt(bandEl.dataset.band, 10);
|
const bandIndex = parseInt(bandEl.dataset.band, 10);
|
||||||
|
|
@ -966,16 +1078,15 @@ export function initializeSettings(scrobbler, player, api, ui) {
|
||||||
audioContextManager.setBandGain(bandIndex, gain);
|
audioContextManager.setBandGain(bandIndex, gain);
|
||||||
updateBandValueDisplay(bandEl, gain);
|
updateBandValueDisplay(bandEl, gain);
|
||||||
|
|
||||||
// When manually adjusting, switch preset to 'flat' (custom)
|
// When manually adjusting, check if we should clear preset
|
||||||
// to indicate the user has made custom changes
|
|
||||||
if (eqPresetSelect && eqPresetSelect.value !== 'flat') {
|
if (eqPresetSelect && eqPresetSelect.value !== 'flat') {
|
||||||
// Check if current gains still match the selected preset
|
const currentGains = audioContextManager.getGains();
|
||||||
const currentPreset = EQ_PRESETS[eqPresetSelect.value];
|
const builtInPresets = EQ_PRESETS;
|
||||||
|
const currentPreset = builtInPresets[eqPresetSelect.value];
|
||||||
if (currentPreset) {
|
if (currentPreset) {
|
||||||
const currentGains = audioContextManager.getGains();
|
|
||||||
const matches = currentPreset.gains.every((g, i) => Math.abs(g - currentGains[i]) < 0.01);
|
const matches = currentPreset.gains.every((g, i) => Math.abs(g - currentGains[i]) < 0.01);
|
||||||
if (!matches) {
|
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
|
// Now Playing Mode
|
||||||
const nowPlayingMode = document.getElementById('now-playing-mode');
|
const nowPlayingMode = document.getElementById('now-playing-mode');
|
||||||
if (nowPlayingMode) {
|
if (nowPlayingMode) {
|
||||||
|
|
|
||||||
301
js/storage.js
301
js/storage.js
|
|
@ -791,6 +791,23 @@ export const equalizerSettings = {
|
||||||
ENABLED_KEY: 'equalizer-enabled',
|
ENABLED_KEY: 'equalizer-enabled',
|
||||||
GAINS_KEY: 'equalizer-gains',
|
GAINS_KEY: 'equalizer-gains',
|
||||||
PRESET_KEY: 'equalizer-preset',
|
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() {
|
isEnabled() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -805,25 +822,178 @@ export const equalizerSettings = {
|
||||||
localStorage.setItem(this.ENABLED_KEY, enabled ? 'true' : 'false');
|
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 {
|
try {
|
||||||
const stored = localStorage.getItem(this.GAINS_KEY);
|
const stored = localStorage.getItem(this.GAINS_KEY);
|
||||||
if (stored) {
|
if (stored) {
|
||||||
const gains = JSON.parse(stored);
|
const gains = JSON.parse(stored);
|
||||||
if (Array.isArray(gains) && gains.length === 16) {
|
if (Array.isArray(gains)) {
|
||||||
return 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 {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
// Return flat EQ (all zeros) by default
|
// Return flat EQ (all zeros) by default
|
||||||
return new Array(16).fill(0);
|
return new Array(count).fill(0);
|
||||||
},
|
},
|
||||||
|
|
||||||
setGains(gains) {
|
setGains(gains) {
|
||||||
try {
|
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));
|
localStorage.setItem(this.GAINS_KEY, JSON.stringify(gains));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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() {
|
getPreset() {
|
||||||
try {
|
try {
|
||||||
return localStorage.getItem(this.PRESET_KEY) || 'flat';
|
return localStorage.getItem(this.PRESET_KEY) || 'flat';
|
||||||
|
|
@ -842,6 +1037,102 @@ export const equalizerSettings = {
|
||||||
setPreset(preset) {
|
setPreset(preset) {
|
||||||
localStorage.setItem(this.PRESET_KEY, 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 = {
|
export const monoAudioSettings = {
|
||||||
|
|
|
||||||
244
styles.css
244
styles.css
|
|
@ -5805,6 +5805,42 @@ textarea:focus {
|
||||||
box-shadow: 0 0 0 3px rgb(var(--highlight-rgb), 0.2);
|
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 {
|
#equalizer-reset-btn {
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -5831,6 +5867,214 @@ textarea:focus {
|
||||||
transform: rotate(-45deg);
|
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 {
|
.equalizer-bands {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue