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">
|
||||
<label for="equalizer-preset-select">Preset</label>
|
||||
<select id="equalizer-preset-select">
|
||||
<option value="flat">Flat</option>
|
||||
<option value="bass_boost">Bass Boost</option>
|
||||
<option value="bass_reducer">Bass Reducer</option>
|
||||
<option value="treble_boost">Treble Boost</option>
|
||||
<option value="treble_reducer">Treble Reducer</option>
|
||||
<option value="vocal_boost">Vocal Boost</option>
|
||||
<option value="loudness">Loudness</option>
|
||||
<option value="rock">Rock</option>
|
||||
<option value="pop">Pop</option>
|
||||
<option value="classical">Classical</option>
|
||||
<option value="jazz">Jazz</option>
|
||||
<option value="electronic">Electronic</option>
|
||||
<option value="hip_hop">Hip-Hop</option>
|
||||
<option value="r_and_b">R&B</option>
|
||||
<option value="acoustic">Acoustic</option>
|
||||
<option value="podcast">Podcast / Speech</option>
|
||||
<optgroup label="Built-in Presets">
|
||||
<option value="flat">Flat</option>
|
||||
<option value="bass_boost">Bass Boost</option>
|
||||
<option value="bass_reducer">Bass Reducer</option>
|
||||
<option value="treble_boost">Treble Boost</option>
|
||||
<option value="treble_reducer">Treble Reducer</option>
|
||||
<option value="vocal_boost">Vocal Boost</option>
|
||||
<option value="loudness">Loudness</option>
|
||||
<option value="rock">Rock</option>
|
||||
<option value="pop">Pop</option>
|
||||
<option value="classical">Classical</option>
|
||||
<option value="jazz">Jazz</option>
|
||||
<option value="electronic">Electronic</option>
|
||||
<option value="hip_hop">Hip-Hop</option>
|
||||
<option value="r_and_b">R&B</option>
|
||||
<option value="acoustic">Acoustic</option>
|
||||
<option value="podcast">Podcast / Speech</option>
|
||||
</optgroup>
|
||||
<optgroup label="Custom Presets" id="custom-presets-optgroup">
|
||||
<!-- Custom presets will be populated by JavaScript -->
|
||||
</optgroup>
|
||||
</select>
|
||||
<label for="eq-band-count">Bands</label>
|
||||
<input
|
||||
type="number"
|
||||
id="eq-band-count"
|
||||
class="eq-band-count-input"
|
||||
min="3"
|
||||
max="32"
|
||||
value="16"
|
||||
title="Number of EQ bands (3-32)"
|
||||
/>
|
||||
<button
|
||||
id="equalizer-reset-btn"
|
||||
class="btn-secondary"
|
||||
|
|
@ -3434,218 +3449,128 @@
|
|||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="custom-preset-controls">
|
||||
<div class="custom-preset-input-row">
|
||||
<input
|
||||
type="text"
|
||||
id="custom-preset-name"
|
||||
placeholder="Preset name (e.g., Home, Car, Work)"
|
||||
maxlength="50"
|
||||
/>
|
||||
<button
|
||||
id="save-custom-preset-btn"
|
||||
class="btn-primary"
|
||||
title="Save current EQ as custom preset"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
id="delete-custom-preset-btn"
|
||||
class="btn-secondary delete-preset-btn"
|
||||
style="display: none"
|
||||
title="Delete selected custom preset"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="3 6 5 6 21 6" />
|
||||
<path
|
||||
d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"
|
||||
/>
|
||||
</svg>
|
||||
Delete Preset
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="eq-range-controls">
|
||||
<label>DB Range:</label>
|
||||
<input
|
||||
type="number"
|
||||
id="eq-range-min"
|
||||
class="eq-range-input"
|
||||
min="-60"
|
||||
max="0"
|
||||
value="-30"
|
||||
title="Minimum gain in dB"
|
||||
/>
|
||||
<span>to</span>
|
||||
<input
|
||||
type="number"
|
||||
id="eq-range-max"
|
||||
class="eq-range-input"
|
||||
min="0"
|
||||
max="60"
|
||||
value="30"
|
||||
title="Maximum gain in dB"
|
||||
/>
|
||||
<span>dB</span>
|
||||
<button
|
||||
id="apply-eq-range-btn"
|
||||
class="btn-secondary"
|
||||
title="Apply new range to all bands"
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
<button
|
||||
id="reset-eq-range-btn"
|
||||
class="btn-secondary"
|
||||
title="Reset to default (-30 to +30 dB)"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="eq-freq-controls">
|
||||
<label>Freq Range:</label>
|
||||
<input
|
||||
type="number"
|
||||
id="eq-freq-min"
|
||||
class="eq-freq-input"
|
||||
min="10"
|
||||
max="20000"
|
||||
value="20"
|
||||
title="Minimum frequency in Hz"
|
||||
/>
|
||||
<span>Hz to</span>
|
||||
<input
|
||||
type="number"
|
||||
id="eq-freq-max"
|
||||
class="eq-freq-input"
|
||||
min="20"
|
||||
max="96000"
|
||||
value="20000"
|
||||
title="Maximum frequency in Hz"
|
||||
/>
|
||||
<span>Hz</span>
|
||||
<button
|
||||
id="apply-eq-freq-btn"
|
||||
class="btn-secondary"
|
||||
title="Apply new frequency range"
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
<button
|
||||
id="reset-eq-freq-btn"
|
||||
class="btn-secondary"
|
||||
title="Reset to default (20 Hz to 20 kHz)"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="equalizer-bands" id="equalizer-bands">
|
||||
<!-- Bands will be dynamically generated by JavaScript -->
|
||||
<div class="eq-band" data-band="0">
|
||||
<input
|
||||
type="range"
|
||||
class="eq-slider"
|
||||
min="-30"
|
||||
max="30"
|
||||
step="0.5"
|
||||
value="0"
|
||||
orient="vertical"
|
||||
/>
|
||||
<span class="eq-value">0</span>
|
||||
<span class="eq-freq">25</span>
|
||||
</div>
|
||||
<div class="eq-band" data-band="1">
|
||||
<input
|
||||
type="range"
|
||||
class="eq-slider"
|
||||
min="-30"
|
||||
max="30"
|
||||
step="0.5"
|
||||
value="0"
|
||||
orient="vertical"
|
||||
/>
|
||||
<span class="eq-value">0</span>
|
||||
<span class="eq-freq">40</span>
|
||||
</div>
|
||||
<div class="eq-band" data-band="2">
|
||||
<input
|
||||
type="range"
|
||||
class="eq-slider"
|
||||
min="-30"
|
||||
max="30"
|
||||
step="0.5"
|
||||
value="0"
|
||||
orient="vertical"
|
||||
/>
|
||||
<span class="eq-value">0</span>
|
||||
<span class="eq-freq">63</span>
|
||||
</div>
|
||||
<div class="eq-band" data-band="3">
|
||||
<input
|
||||
type="range"
|
||||
class="eq-slider"
|
||||
min="-30"
|
||||
max="30"
|
||||
step="0.5"
|
||||
value="0"
|
||||
orient="vertical"
|
||||
/>
|
||||
<span class="eq-value">0</span>
|
||||
<span class="eq-freq">100</span>
|
||||
</div>
|
||||
<div class="eq-band" data-band="4">
|
||||
<input
|
||||
type="range"
|
||||
class="eq-slider"
|
||||
min="-30"
|
||||
max="30"
|
||||
step="0.5"
|
||||
value="0"
|
||||
orient="vertical"
|
||||
/>
|
||||
<span class="eq-value">0</span>
|
||||
<span class="eq-freq">160</span>
|
||||
</div>
|
||||
<div class="eq-band" data-band="5">
|
||||
<input
|
||||
type="range"
|
||||
class="eq-slider"
|
||||
min="-30"
|
||||
max="30"
|
||||
step="0.5"
|
||||
value="0"
|
||||
orient="vertical"
|
||||
/>
|
||||
<span class="eq-value">0</span>
|
||||
<span class="eq-freq">250</span>
|
||||
</div>
|
||||
<div class="eq-band" data-band="6">
|
||||
<input
|
||||
type="range"
|
||||
class="eq-slider"
|
||||
min="-30"
|
||||
max="30"
|
||||
step="0.5"
|
||||
value="0"
|
||||
orient="vertical"
|
||||
/>
|
||||
<span class="eq-value">0</span>
|
||||
<span class="eq-freq">400</span>
|
||||
</div>
|
||||
<div class="eq-band" data-band="7">
|
||||
<input
|
||||
type="range"
|
||||
class="eq-slider"
|
||||
min="-30"
|
||||
max="30"
|
||||
step="0.5"
|
||||
value="0"
|
||||
orient="vertical"
|
||||
/>
|
||||
<span class="eq-value">0</span>
|
||||
<span class="eq-freq">630</span>
|
||||
</div>
|
||||
<div class="eq-band" data-band="8">
|
||||
<input
|
||||
type="range"
|
||||
class="eq-slider"
|
||||
min="-30"
|
||||
max="30"
|
||||
step="0.5"
|
||||
value="0"
|
||||
orient="vertical"
|
||||
/>
|
||||
<span class="eq-value">0</span>
|
||||
<span class="eq-freq">1K</span>
|
||||
</div>
|
||||
<div class="eq-band" data-band="9">
|
||||
<input
|
||||
type="range"
|
||||
class="eq-slider"
|
||||
min="-30"
|
||||
max="30"
|
||||
step="0.5"
|
||||
value="0"
|
||||
orient="vertical"
|
||||
/>
|
||||
<span class="eq-value">0</span>
|
||||
<span class="eq-freq">1.6K</span>
|
||||
</div>
|
||||
<div class="eq-band" data-band="10">
|
||||
<input
|
||||
type="range"
|
||||
class="eq-slider"
|
||||
min="-30"
|
||||
max="30"
|
||||
step="0.5"
|
||||
value="0"
|
||||
orient="vertical"
|
||||
/>
|
||||
<span class="eq-value">0</span>
|
||||
<span class="eq-freq">2.5K</span>
|
||||
</div>
|
||||
<div class="eq-band" data-band="11">
|
||||
<input
|
||||
type="range"
|
||||
class="eq-slider"
|
||||
min="-30"
|
||||
max="30"
|
||||
step="0.5"
|
||||
value="0"
|
||||
orient="vertical"
|
||||
/>
|
||||
<span class="eq-value">0</span>
|
||||
<span class="eq-freq">4K</span>
|
||||
</div>
|
||||
<div class="eq-band" data-band="12">
|
||||
<input
|
||||
type="range"
|
||||
class="eq-slider"
|
||||
min="-30"
|
||||
max="30"
|
||||
step="0.5"
|
||||
value="0"
|
||||
orient="vertical"
|
||||
/>
|
||||
<span class="eq-value">0</span>
|
||||
<span class="eq-freq">6.3K</span>
|
||||
</div>
|
||||
<div class="eq-band" data-band="13">
|
||||
<input
|
||||
type="range"
|
||||
class="eq-slider"
|
||||
min="-30"
|
||||
max="30"
|
||||
step="0.5"
|
||||
value="0"
|
||||
orient="vertical"
|
||||
/>
|
||||
<span class="eq-value">0</span>
|
||||
<span class="eq-freq">10K</span>
|
||||
</div>
|
||||
<div class="eq-band" data-band="14">
|
||||
<input
|
||||
type="range"
|
||||
class="eq-slider"
|
||||
min="-30"
|
||||
max="30"
|
||||
step="0.5"
|
||||
value="0"
|
||||
orient="vertical"
|
||||
/>
|
||||
<span class="eq-value">0</span>
|
||||
<span class="eq-freq">16K</span>
|
||||
</div>
|
||||
<div class="eq-band" data-band="15">
|
||||
<input
|
||||
type="range"
|
||||
class="eq-slider"
|
||||
min="-30"
|
||||
max="30"
|
||||
step="0.5"
|
||||
value="0"
|
||||
orient="vertical"
|
||||
/>
|
||||
<span class="eq-value">0</span>
|
||||
<span class="eq-freq">20K</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="equalizer-scale">
|
||||
|
|
|
|||
|
|
@ -1,79 +1,95 @@
|
|||
// js/audio-context.js
|
||||
// Shared Audio Context Manager - handles EQ and provides context for visualizer
|
||||
// Supports 3-32 parametric EQ bands
|
||||
|
||||
import { equalizerSettings, monoAudioSettings } from './storage.js';
|
||||
|
||||
// Standard 16-band ISO center frequencies (Hz)
|
||||
const EQ_FREQUENCIES = [25, 40, 63, 100, 160, 250, 400, 630, 1000, 1600, 2500, 4000, 6300, 10000, 16000, 20000];
|
||||
// Standard 16-band ISO center frequencies (Hz) - for reference
|
||||
const DEFAULT_EQ_FREQUENCIES = [25, 40, 63, 100, 160, 250, 400, 630, 1000, 1600, 2500, 4000, 6300, 10000, 16000, 20000];
|
||||
|
||||
// EQ Presets (gain values in dB for each of the 16 bands)
|
||||
const EQ_PRESETS = {
|
||||
flat: {
|
||||
name: 'Flat',
|
||||
gains: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
},
|
||||
bass_boost: {
|
||||
name: 'Bass Boost',
|
||||
gains: [6, 5, 4.5, 4, 3, 2, 1, 0.5, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
},
|
||||
bass_reducer: {
|
||||
name: 'Bass Reducer',
|
||||
gains: [-6, -5, -4, -3, -2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
},
|
||||
treble_boost: {
|
||||
name: 'Treble Boost',
|
||||
gains: [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 5.5, 6],
|
||||
},
|
||||
treble_reducer: {
|
||||
name: 'Treble Reducer',
|
||||
gains: [0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -2, -3, -4, -5, -5.5, -6],
|
||||
},
|
||||
vocal_boost: {
|
||||
name: 'Vocal Boost',
|
||||
gains: [-2, -1, 0, 0, 1, 2, 3, 4, 4, 3, 2, 1, 0, 0, -1, -2],
|
||||
},
|
||||
loudness: {
|
||||
name: 'Loudness',
|
||||
gains: [5, 4, 3, 1, 0, -1, -1, 0, 0, 1, 2, 3, 4, 4.5, 4, 3],
|
||||
},
|
||||
rock: {
|
||||
name: 'Rock',
|
||||
gains: [4, 3.5, 3, 2, -1, -2, -1, 1, 2, 3, 3.5, 4, 4, 3, 2, 1],
|
||||
},
|
||||
pop: {
|
||||
name: 'Pop',
|
||||
gains: [-1, 0, 1, 2, 3, 3, 2, 1, 0, 1, 2, 2, 2, 2, 1, 0],
|
||||
},
|
||||
classical: {
|
||||
name: 'Classical',
|
||||
gains: [3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 3, 2],
|
||||
},
|
||||
jazz: {
|
||||
name: 'Jazz',
|
||||
gains: [3, 2, 1, 1, -1, -1, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2],
|
||||
},
|
||||
electronic: {
|
||||
name: 'Electronic',
|
||||
gains: [4, 3.5, 3, 1, 0, -1, 0, 1, 2, 3, 3, 2, 2, 3, 4, 3.5],
|
||||
},
|
||||
hip_hop: {
|
||||
name: 'Hip-Hop',
|
||||
gains: [5, 4.5, 4, 3, 1, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 2],
|
||||
},
|
||||
r_and_b: {
|
||||
name: 'R&B',
|
||||
gains: [3, 5, 4, 2, 1, 0, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1],
|
||||
},
|
||||
acoustic: {
|
||||
name: 'Acoustic',
|
||||
gains: [3, 2, 1, 1, 2, 2, 1, 0, 0, 1, 1, 2, 3, 3, 2, 1],
|
||||
},
|
||||
podcast: {
|
||||
name: 'Podcast / Speech',
|
||||
gains: [-3, -2, -1, 0, 1, 2, 3, 4, 4, 3, 2, 1, 0, -1, -2, -3],
|
||||
},
|
||||
// Generate frequency array for given number of bands using logarithmic spacing
|
||||
function generateFrequencies(bandCount, minFreq = 20, maxFreq = 20000) {
|
||||
const frequencies = [];
|
||||
const safeMin = Math.max(10, minFreq);
|
||||
const safeMax = Math.min(96000, maxFreq);
|
||||
|
||||
for (let i = 0; i < bandCount; i++) {
|
||||
// Logarithmic interpolation
|
||||
const t = i / (bandCount - 1);
|
||||
const freq = safeMin * Math.pow(safeMax / safeMin, t);
|
||||
frequencies.push(Math.round(freq));
|
||||
}
|
||||
|
||||
return frequencies;
|
||||
}
|
||||
|
||||
// Generate frequency labels for display
|
||||
function generateFrequencyLabels(frequencies) {
|
||||
return frequencies.map((freq) => {
|
||||
if (freq < 1000) {
|
||||
return freq.toString();
|
||||
} else if (freq < 10000) {
|
||||
return (freq / 1000).toFixed(freq % 1000 === 0 ? 0 : 1) + 'K';
|
||||
} else {
|
||||
return (freq / 1000).toFixed(0) + 'K';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// EQ Presets (16-band default)
|
||||
const EQ_PRESETS_16 = {
|
||||
flat: { name: 'Flat', gains: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] },
|
||||
bass_boost: { name: 'Bass Boost', gains: [6, 5, 4.5, 4, 3, 2, 1, 0.5, 0, 0, 0, 0, 0, 0, 0, 0] },
|
||||
bass_reducer: { name: 'Bass Reducer', gains: [-6, -5, -4, -3, -2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] },
|
||||
treble_boost: { name: 'Treble Boost', gains: [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 5.5, 6] },
|
||||
treble_reducer: { name: 'Treble Reducer', gains: [0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -2, -3, -4, -5, -5.5, -6] },
|
||||
vocal_boost: { name: 'Vocal Boost', gains: [-2, -1, 0, 0, 1, 2, 3, 4, 4, 3, 2, 1, 0, 0, -1, -2] },
|
||||
loudness: { name: 'Loudness', gains: [5, 4, 3, 1, 0, -1, -1, 0, 0, 1, 2, 3, 4, 4.5, 4, 3] },
|
||||
rock: { name: 'Rock', gains: [4, 3.5, 3, 2, -1, -2, -1, 1, 2, 3, 3.5, 4, 4, 3, 2, 1] },
|
||||
pop: { name: 'Pop', gains: [-1, 0, 1, 2, 3, 3, 2, 1, 0, 1, 2, 2, 2, 2, 1, 0] },
|
||||
classical: { name: 'Classical', gains: [3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 3, 2] },
|
||||
jazz: { name: 'Jazz', gains: [3, 2, 1, 1, -1, -1, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2] },
|
||||
electronic: { name: 'Electronic', gains: [4, 3.5, 3, 1, 0, -1, 0, 1, 2, 3, 3, 2, 2, 3, 4, 3.5] },
|
||||
hip_hop: { name: 'Hip-Hop', gains: [5, 4.5, 4, 3, 1, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 2] },
|
||||
r_and_b: { name: 'R&B', gains: [3, 5, 4, 2, 1, 0, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1] },
|
||||
acoustic: { name: 'Acoustic', gains: [3, 2, 1, 1, 2, 2, 1, 0, 0, 1, 1, 2, 3, 3, 2, 1] },
|
||||
podcast: { name: 'Podcast / Speech', gains: [-3, -2, -1, 0, 1, 2, 3, 4, 4, 3, 2, 1, 0, -1, -2, -3] },
|
||||
};
|
||||
|
||||
// Interpolate 16-band preset to target band count
|
||||
function interpolatePreset(preset16, targetBands) {
|
||||
if (targetBands === 16) return [...preset16];
|
||||
|
||||
const result = [];
|
||||
for (let i = 0; i < targetBands; i++) {
|
||||
const sourceIndex = (i / (targetBands - 1)) * (preset16.length - 1);
|
||||
const indexLow = Math.floor(sourceIndex);
|
||||
const indexHigh = Math.min(Math.ceil(sourceIndex), preset16.length - 1);
|
||||
const fraction = sourceIndex - indexLow;
|
||||
|
||||
const lowValue = preset16[indexLow] || 0;
|
||||
const highValue = preset16[indexHigh] || 0;
|
||||
const interpolated = lowValue + (highValue - lowValue) * fraction;
|
||||
result.push(Math.round(interpolated * 10) / 10);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Get presets for given band count
|
||||
function getPresetsForBandCount(bandCount) {
|
||||
const presets = {};
|
||||
for (const [key, preset] of Object.entries(EQ_PRESETS_16)) {
|
||||
presets[key] = {
|
||||
name: preset.name,
|
||||
gains: interpolatePreset(preset.gains, bandCount),
|
||||
};
|
||||
}
|
||||
return presets;
|
||||
}
|
||||
|
||||
// Default export for backwards compatibility (16 bands)
|
||||
const EQ_PRESETS = EQ_PRESETS_16;
|
||||
|
||||
class AudioContextManager {
|
||||
constructor() {
|
||||
this.audioContext = null;
|
||||
|
|
@ -86,10 +102,15 @@ class AudioContextManager {
|
|||
this.isEQEnabled = false;
|
||||
this.isMonoAudioEnabled = false;
|
||||
this.monoMergerNode = null;
|
||||
this.currentGains = new Array(16).fill(0);
|
||||
this.audio = null;
|
||||
this.currentVolume = 1.0;
|
||||
|
||||
// Band configuration
|
||||
this.bandCount = equalizerSettings.getBandCount();
|
||||
this.freqRange = equalizerSettings.getFreqRange();
|
||||
this.frequencies = generateFrequencies(this.bandCount, this.freqRange.min, this.freqRange.max);
|
||||
this.currentGains = new Array(this.bandCount).fill(0);
|
||||
|
||||
// Callbacks for audio graph changes (for visualizers like Butterchurn)
|
||||
this._graphChangeCallbacks = [];
|
||||
|
||||
|
|
@ -97,6 +118,128 @@ class AudioContextManager {
|
|||
this._loadSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update band count and reinitialize EQ
|
||||
*/
|
||||
setBandCount(count) {
|
||||
const newCount = Math.max(
|
||||
equalizerSettings.MIN_BANDS,
|
||||
Math.min(equalizerSettings.MAX_BANDS, parseInt(count, 10) || 16)
|
||||
);
|
||||
|
||||
if (newCount === this.bandCount) return;
|
||||
|
||||
// Save new band count
|
||||
equalizerSettings.setBandCount(newCount);
|
||||
|
||||
// Update configuration
|
||||
this.bandCount = newCount;
|
||||
this.frequencies = generateFrequencies(newCount, this.freqRange.min, this.freqRange.max);
|
||||
|
||||
// Interpolate current gains to new band count
|
||||
const newGains = equalizerSettings._interpolateGains(this.currentGains, newCount);
|
||||
this.currentGains = newGains;
|
||||
equalizerSettings.setGains(newGains);
|
||||
|
||||
// Reinitialize EQ if already initialized
|
||||
if (this.isInitialized && this.audioContext) {
|
||||
this._destroyEQ();
|
||||
this._createEQ();
|
||||
}
|
||||
|
||||
// Dispatch event for UI update
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('equalizer-band-count-changed', {
|
||||
detail: { bandCount: newCount, frequencies: this.frequencies },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update frequency range and reinitialize EQ
|
||||
*/
|
||||
setFreqRange(minFreq, maxFreq) {
|
||||
const newMin = Math.max(10, Math.min(96000, parseInt(minFreq, 10) || 20));
|
||||
const newMax = Math.max(10, Math.min(96000, parseInt(maxFreq, 10) || 20000));
|
||||
|
||||
if (newMin >= newMax) {
|
||||
console.warn('[AudioContext] Invalid frequency range: min must be less than max');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (newMin === this.freqRange.min && newMax === this.freqRange.max) return true;
|
||||
|
||||
// Save new frequency range
|
||||
equalizerSettings.setFreqRange(newMin, newMax);
|
||||
|
||||
// Update configuration
|
||||
this.freqRange = { min: newMin, max: newMax };
|
||||
this.frequencies = generateFrequencies(this.bandCount, newMin, newMax);
|
||||
|
||||
// Reinitialize EQ if already initialized
|
||||
if (this.isInitialized && this.audioContext) {
|
||||
this._destroyEQ();
|
||||
this._createEQ();
|
||||
}
|
||||
|
||||
// Dispatch event for UI update
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('equalizer-freq-range-changed', {
|
||||
detail: { min: newMin, max: newMax, frequencies: this.frequencies },
|
||||
})
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy EQ filters
|
||||
*/
|
||||
_destroyEQ() {
|
||||
if (this.filters) {
|
||||
this.filters.forEach((filter) => {
|
||||
try {
|
||||
filter.disconnect();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
});
|
||||
}
|
||||
this.filters = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create EQ filters
|
||||
*/
|
||||
_createEQ() {
|
||||
if (!this.audioContext) return;
|
||||
|
||||
// Create biquad filters for each frequency band
|
||||
this.filters = this.frequencies.map((freq, index) => {
|
||||
const filter = this.audioContext.createBiquadFilter();
|
||||
filter.type = 'peaking';
|
||||
filter.frequency.value = freq;
|
||||
filter.Q.value = this._calculateQ(index);
|
||||
filter.gain.value = this.currentGains[index] || 0;
|
||||
return filter;
|
||||
});
|
||||
|
||||
// Create volume node if not exists
|
||||
if (!this.volumeNode) {
|
||||
this.volumeNode = this.audioContext.createGain();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Q factor for each band
|
||||
*/
|
||||
_calculateQ(_index) {
|
||||
// Scale Q based on band count for consistent sound
|
||||
const baseQ = 2.5;
|
||||
const scalingFactor = Math.sqrt(16 / this.bandCount);
|
||||
return baseQ * scalingFactor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a callback to be called when audio graph is reconnected
|
||||
* @param {Function} callback - Function to call when graph changes
|
||||
|
|
@ -159,15 +302,8 @@ class AudioContextManager {
|
|||
this.analyser.fftSize = 512;
|
||||
this.analyser.smoothingTimeConstant = 0.7;
|
||||
|
||||
// Create 16 biquad filters for EQ
|
||||
this.filters = EQ_FREQUENCIES.map((freq, index) => {
|
||||
const filter = this.audioContext.createBiquadFilter();
|
||||
filter.type = 'peaking';
|
||||
filter.frequency.value = freq;
|
||||
filter.Q.value = 2.5; // Constant-Q design
|
||||
filter.gain.value = this.currentGains[index];
|
||||
return filter;
|
||||
});
|
||||
// Create biquad filters for EQ with dynamic band count
|
||||
this._createEQ();
|
||||
|
||||
// Create output gain node
|
||||
this.outputNode = this.audioContext.createGain();
|
||||
|
|
@ -180,17 +316,11 @@ class AudioContextManager {
|
|||
// Create mono audio merger node
|
||||
this.monoMergerNode = this.audioContext.createChannelMerger(2);
|
||||
|
||||
// Connect filter chain: filter[0] -> filter[1] -> ... -> filter[15] -> outputNode
|
||||
for (let i = 0; i < this.filters.length - 1; i++) {
|
||||
this.filters[i].connect(this.filters[i + 1]);
|
||||
}
|
||||
this.filters[this.filters.length - 1].connect(this.outputNode);
|
||||
|
||||
// Connect the audio graph based on EQ and mono state
|
||||
this._connectGraph();
|
||||
|
||||
this.isInitialized = true;
|
||||
console.log('[AudioContext] Initialized with 16-band EQ');
|
||||
console.log(`[AudioContext] Initialized with ${this.bandCount}-band EQ`);
|
||||
} catch (e) {
|
||||
console.warn('[AudioContext] Init failed:', e);
|
||||
}
|
||||
|
|
@ -240,7 +370,13 @@ class AudioContextManager {
|
|||
|
||||
if (this.isEQEnabled && this.filters.length > 0) {
|
||||
// EQ enabled: lastNode -> EQ filters -> output -> analyser -> volume -> destination
|
||||
// Connect filter chain
|
||||
for (let i = 0; i < this.filters.length - 1; i++) {
|
||||
this.filters[i].connect(this.filters[i + 1]);
|
||||
}
|
||||
// Connect input to first filter and last filter to output
|
||||
lastNode.connect(this.filters[0]);
|
||||
this.filters[this.filters.length - 1].connect(this.outputNode);
|
||||
this.outputNode.connect(this.analyser);
|
||||
this.analyser.connect(this.volumeNode);
|
||||
this.volumeNode.connect(this.audioContext.destination);
|
||||
|
|
@ -374,13 +510,28 @@ class AudioContextManager {
|
|||
return this.isInitialized && this.isMonoAudioEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current gain range
|
||||
*/
|
||||
getRange() {
|
||||
return equalizerSettings.getRange();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamp gain to valid range
|
||||
*/
|
||||
_clampGain(gainDb) {
|
||||
const range = this.getRange();
|
||||
return Math.max(range.min, Math.min(range.max, gainDb));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set gain for a specific band
|
||||
*/
|
||||
setBandGain(bandIndex, gainDb) {
|
||||
if (bandIndex < 0 || bandIndex >= 16) return;
|
||||
if (bandIndex < 0 || bandIndex >= this.bandCount) return;
|
||||
|
||||
const clampedGain = Math.max(-30, Math.min(30, gainDb));
|
||||
const clampedGain = this._clampGain(gainDb);
|
||||
this.currentGains[bandIndex] = clampedGain;
|
||||
|
||||
if (this.filters[bandIndex] && this.audioContext) {
|
||||
|
|
@ -395,12 +546,18 @@ class AudioContextManager {
|
|||
* Set all band gains at once
|
||||
*/
|
||||
setAllGains(gains) {
|
||||
if (!Array.isArray(gains) || gains.length !== 16) return;
|
||||
if (!Array.isArray(gains)) return;
|
||||
|
||||
// Ensure gains array matches current band count
|
||||
let adjustedGains = gains;
|
||||
if (gains.length !== this.bandCount) {
|
||||
adjustedGains = equalizerSettings._interpolateGains(gains, this.bandCount);
|
||||
}
|
||||
|
||||
const now = this.audioContext?.currentTime || 0;
|
||||
|
||||
gains.forEach((gain, index) => {
|
||||
const clampedGain = Math.max(-30, Math.min(30, gain));
|
||||
adjustedGains.forEach((gain, index) => {
|
||||
const clampedGain = this._clampGain(gain);
|
||||
this.currentGains[index] = clampedGain;
|
||||
|
||||
if (this.filters[index]) {
|
||||
|
|
@ -415,7 +572,8 @@ class AudioContextManager {
|
|||
* Apply a preset
|
||||
*/
|
||||
applyPreset(presetKey) {
|
||||
const preset = EQ_PRESETS[presetKey];
|
||||
const presets = getPresetsForBandCount(this.bandCount);
|
||||
const preset = presets[presetKey];
|
||||
if (!preset) return;
|
||||
|
||||
this.setAllGains(preset.gains);
|
||||
|
|
@ -426,7 +584,7 @@ class AudioContextManager {
|
|||
* Reset all bands to flat
|
||||
*/
|
||||
reset() {
|
||||
this.setAllGains(new Array(16).fill(0));
|
||||
this.setAllGains(new Array(this.bandCount).fill(0));
|
||||
equalizerSettings.setPreset('flat');
|
||||
}
|
||||
|
||||
|
|
@ -437,12 +595,22 @@ class AudioContextManager {
|
|||
return [...this.currentGains];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current band count
|
||||
*/
|
||||
getBandCount() {
|
||||
return this.bandCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load settings from storage
|
||||
*/
|
||||
_loadSettings() {
|
||||
this.isEQEnabled = equalizerSettings.isEnabled();
|
||||
this.currentGains = equalizerSettings.getGains();
|
||||
this.bandCount = equalizerSettings.getBandCount();
|
||||
this.freqRange = equalizerSettings.getFreqRange();
|
||||
this.frequencies = generateFrequencies(this.bandCount, this.freqRange.min, this.freqRange.max);
|
||||
this.currentGains = equalizerSettings.getGains(this.bandCount);
|
||||
this.isMonoAudioEnabled = monoAudioSettings.isEnabled();
|
||||
}
|
||||
}
|
||||
|
|
@ -450,5 +618,12 @@ class AudioContextManager {
|
|||
// Export singleton instance
|
||||
export const audioContextManager = new AudioContextManager();
|
||||
|
||||
// Export presets for settings UI
|
||||
export { EQ_PRESETS };
|
||||
// Export presets and helper functions for settings UI
|
||||
export {
|
||||
EQ_PRESETS,
|
||||
generateFrequencies,
|
||||
generateFrequencyLabels,
|
||||
getPresetsForBandCount,
|
||||
interpolatePreset,
|
||||
EQ_PRESETS_16,
|
||||
};
|
||||
|
|
|
|||
259
js/equalizer.js
259
js/equalizer.js
|
|
@ -1,13 +1,13 @@
|
|||
// js/equalizer.js
|
||||
// 16-Band Parametric Equalizer with Web Audio API
|
||||
// Parametric Equalizer with Web Audio API - Supports 3-32 bands
|
||||
|
||||
import { equalizerSettings } from './storage.js';
|
||||
|
||||
// Standard 16-band ISO center frequencies (Hz)
|
||||
const EQ_FREQUENCIES = [25, 40, 63, 100, 160, 250, 400, 630, 1000, 1600, 2500, 4000, 6300, 10000, 16000, 20000];
|
||||
// Standard 16-band ISO center frequencies (Hz) - kept for reference
|
||||
const DEFAULT_EQ_FREQUENCIES = [25, 40, 63, 100, 160, 250, 400, 630, 1000, 1600, 2500, 4000, 6300, 10000, 16000, 20000];
|
||||
|
||||
// Frequency labels for UI display
|
||||
const FREQUENCY_LABELS = [
|
||||
const DEFAULT_FREQUENCY_LABELS = [
|
||||
'25',
|
||||
'40',
|
||||
'63',
|
||||
|
|
@ -26,8 +26,37 @@ const FREQUENCY_LABELS = [
|
|||
'20K',
|
||||
];
|
||||
|
||||
// Generate frequency array for given number of bands using logarithmic spacing
|
||||
function generateFrequencies(bandCount, minFreq = 20, maxFreq = 20000) {
|
||||
const frequencies = [];
|
||||
const safeMin = Math.max(10, minFreq);
|
||||
const safeMax = Math.min(96000, maxFreq);
|
||||
|
||||
for (let i = 0; i < bandCount; i++) {
|
||||
// Logarithmic interpolation
|
||||
const t = i / (bandCount - 1);
|
||||
const freq = safeMin * Math.pow(safeMax / safeMin, t);
|
||||
frequencies.push(Math.round(freq));
|
||||
}
|
||||
|
||||
return frequencies;
|
||||
}
|
||||
|
||||
// Generate frequency labels for display
|
||||
function generateFrequencyLabels(frequencies) {
|
||||
return frequencies.map((freq) => {
|
||||
if (freq < 1000) {
|
||||
return freq.toString();
|
||||
} else if (freq < 10000) {
|
||||
return (freq / 1000).toFixed(freq % 1000 === 0 ? 0 : 1) + 'K';
|
||||
} else {
|
||||
return (freq / 1000).toFixed(0) + 'K';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// EQ Presets (gain values in dB for each of the 16 bands)
|
||||
const EQ_PRESETS = {
|
||||
const EQ_PRESETS_16BAND = {
|
||||
flat: {
|
||||
name: 'Flat',
|
||||
gains: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
|
|
@ -94,6 +123,37 @@ const EQ_PRESETS = {
|
|||
},
|
||||
};
|
||||
|
||||
// Interpolate 16-band preset to target band count
|
||||
function interpolatePreset(preset16, targetBands) {
|
||||
if (targetBands === 16) return [...preset16];
|
||||
|
||||
const result = [];
|
||||
for (let i = 0; i < targetBands; i++) {
|
||||
const sourceIndex = (i / (targetBands - 1)) * (preset16.length - 1);
|
||||
const indexLow = Math.floor(sourceIndex);
|
||||
const indexHigh = Math.min(Math.ceil(sourceIndex), preset16.length - 1);
|
||||
const fraction = sourceIndex - indexLow;
|
||||
|
||||
const lowValue = preset16[indexLow] || 0;
|
||||
const highValue = preset16[indexHigh] || 0;
|
||||
const interpolated = lowValue + (highValue - lowValue) * fraction;
|
||||
result.push(Math.round(interpolated * 10) / 10);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Get presets for given band count
|
||||
function getPresetsForBandCount(bandCount) {
|
||||
const presets = {};
|
||||
for (const [key, preset] of Object.entries(EQ_PRESETS_16BAND)) {
|
||||
presets[key] = {
|
||||
name: preset.name,
|
||||
gains: interpolatePreset(preset.gains, bandCount),
|
||||
};
|
||||
}
|
||||
return presets;
|
||||
}
|
||||
|
||||
export class Equalizer {
|
||||
constructor() {
|
||||
this.audioContext = null;
|
||||
|
|
@ -105,13 +165,99 @@ export class Equalizer {
|
|||
this.isInitialized = false;
|
||||
this.audio = null;
|
||||
|
||||
// Band configuration
|
||||
this.bandCount = equalizerSettings.getBandCount();
|
||||
this.freqRange = equalizerSettings.getFreqRange();
|
||||
this.frequencies = generateFrequencies(this.bandCount, this.freqRange.min, this.freqRange.max);
|
||||
this.frequencyLabels = generateFrequencyLabels(this.frequencies);
|
||||
|
||||
// Store current gains
|
||||
this.currentGains = new Array(16).fill(0);
|
||||
this.currentGains = new Array(this.bandCount).fill(0);
|
||||
|
||||
// Load saved settings
|
||||
this._loadSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update band count and reinitialize
|
||||
*/
|
||||
setBandCount(count) {
|
||||
const newCount = Math.max(
|
||||
equalizerSettings.MIN_BANDS,
|
||||
Math.min(equalizerSettings.MAX_BANDS, parseInt(count, 10) || 16)
|
||||
);
|
||||
|
||||
if (newCount === this.bandCount) return;
|
||||
|
||||
// Save new band count
|
||||
equalizerSettings.setBandCount(newCount);
|
||||
|
||||
// Update configuration
|
||||
this.bandCount = newCount;
|
||||
this.frequencies = generateFrequencies(newCount, this.freqRange.min, this.freqRange.max);
|
||||
this.frequencyLabels = generateFrequencyLabels(this.frequencies);
|
||||
|
||||
// Interpolate current gains to new band count
|
||||
const newGains = equalizerSettings._interpolateGains(this.currentGains, newCount);
|
||||
this.currentGains = newGains;
|
||||
equalizerSettings.setGains(newGains);
|
||||
|
||||
// Reinitialize if already initialized
|
||||
if (this.isInitialized) {
|
||||
this.destroy();
|
||||
if (this.audioContext && this.source && this.audio) {
|
||||
this.init(this.audioContext, this.source, this.audio);
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch event for UI update
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('equalizer-band-count-changed', {
|
||||
detail: { bandCount: newCount, frequencies: this.frequencies, labels: this.frequencyLabels },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update frequency range and reinitialize
|
||||
*/
|
||||
setFreqRange(minFreq, maxFreq) {
|
||||
const newMin = Math.max(10, Math.min(96000, parseInt(minFreq, 10) || 20));
|
||||
const newMax = Math.max(10, Math.min(96000, parseInt(maxFreq, 10) || 20000));
|
||||
|
||||
if (newMin >= newMax) {
|
||||
console.warn('[Equalizer] Invalid frequency range: min must be less than max');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (newMin === this.freqRange.min && newMax === this.freqRange.max) return true;
|
||||
|
||||
// Save new frequency range
|
||||
equalizerSettings.setFreqRange(newMin, newMax);
|
||||
|
||||
// Update configuration
|
||||
this.freqRange = { min: newMin, max: newMax };
|
||||
this.frequencies = generateFrequencies(this.bandCount, newMin, newMax);
|
||||
this.frequencyLabels = generateFrequencyLabels(this.frequencies);
|
||||
|
||||
// Reinitialize if already initialized
|
||||
if (this.isInitialized) {
|
||||
this.destroy();
|
||||
if (this.audioContext && this.source && this.audio) {
|
||||
this.init(this.audioContext, this.source, this.audio);
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch event for UI update
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('equalizer-freq-range-changed', {
|
||||
detail: { min: newMin, max: newMax, frequencies: this.frequencies, labels: this.frequencyLabels },
|
||||
})
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the equalizer with a shared AudioContext
|
||||
* This should be called after the visualizer creates the context
|
||||
|
|
@ -127,15 +273,15 @@ export class Equalizer {
|
|||
this.source = sourceNode;
|
||||
this.audio = audioElement;
|
||||
|
||||
// Create 16 biquad filters for each frequency band
|
||||
this.filters = EQ_FREQUENCIES.map((freq, index) => {
|
||||
// Create biquad filters for each frequency band
|
||||
this.filters = this.frequencies.map((freq, index) => {
|
||||
const filter = this.audioContext.createBiquadFilter();
|
||||
|
||||
// Use peaking filter for all bands (best for EQ)
|
||||
filter.type = 'peaking';
|
||||
filter.frequency.value = freq;
|
||||
filter.Q.value = this._calculateQ(index);
|
||||
filter.gain.value = this.currentGains[index];
|
||||
filter.gain.value = this.currentGains[index] || 0;
|
||||
|
||||
return filter;
|
||||
});
|
||||
|
|
@ -154,7 +300,7 @@ export class Equalizer {
|
|||
this._enableFilters();
|
||||
}
|
||||
|
||||
console.log('[Equalizer] Initialized with 16 bands');
|
||||
console.log(`[Equalizer] Initialized with ${this.bandCount} bands`);
|
||||
} catch (e) {
|
||||
console.warn('[Equalizer] Init failed:', e);
|
||||
}
|
||||
|
|
@ -167,7 +313,10 @@ export class Equalizer {
|
|||
_calculateQ(_index) {
|
||||
// For 16-band 1/2 octave spacing, Q ≈ 2.87
|
||||
// Slightly lower Q for smoother response
|
||||
return 2.5;
|
||||
// Scale Q based on band count for consistent sound
|
||||
const baseQ = 2.5;
|
||||
const scalingFactor = Math.sqrt(16 / this.bandCount);
|
||||
return baseQ * scalingFactor;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -247,16 +396,31 @@ export class Equalizer {
|
|||
return this.isEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current gain range from settings
|
||||
*/
|
||||
getRange() {
|
||||
return equalizerSettings.getRange();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamp gain to current range
|
||||
*/
|
||||
_clampGain(gainDb) {
|
||||
const range = this.getRange();
|
||||
return Math.max(range.min, Math.min(range.max, gainDb));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set gain for a specific band
|
||||
* @param {number} bandIndex - Band index (0-15)
|
||||
* @param {number} gainDb - Gain in dB (-12 to +12)
|
||||
* @param {number} bandIndex - Band index
|
||||
* @param {number} gainDb - Gain in dB
|
||||
*/
|
||||
setBandGain(bandIndex, gainDb) {
|
||||
if (bandIndex < 0 || bandIndex >= 16) return;
|
||||
if (bandIndex < 0 || bandIndex >= this.bandCount) return;
|
||||
|
||||
// Clamp gain to valid range
|
||||
const clampedGain = Math.max(-30, Math.min(30, gainDb));
|
||||
const clampedGain = this._clampGain(gainDb);
|
||||
this.currentGains[bandIndex] = clampedGain;
|
||||
|
||||
if (this.filters[bandIndex]) {
|
||||
|
|
@ -271,15 +435,21 @@ export class Equalizer {
|
|||
|
||||
/**
|
||||
* Set all band gains at once
|
||||
* @param {number[]} gains - Array of 16 gain values in dB
|
||||
* @param {number[]} gains - Array of gain values in dB
|
||||
*/
|
||||
setAllGains(gains) {
|
||||
if (!Array.isArray(gains) || gains.length !== 16) return;
|
||||
if (!Array.isArray(gains)) return;
|
||||
|
||||
// Ensure gains array matches current band count
|
||||
let adjustedGains = gains;
|
||||
if (gains.length !== this.bandCount) {
|
||||
adjustedGains = equalizerSettings._interpolateGains(gains, this.bandCount);
|
||||
}
|
||||
|
||||
const now = this.audioContext?.currentTime || 0;
|
||||
|
||||
gains.forEach((gain, index) => {
|
||||
const clampedGain = Math.max(-30, Math.min(30, gain));
|
||||
adjustedGains.forEach((gain, index) => {
|
||||
const clampedGain = this._clampGain(gain);
|
||||
this.currentGains[index] = clampedGain;
|
||||
|
||||
if (this.filters[index]) {
|
||||
|
|
@ -295,7 +465,8 @@ export class Equalizer {
|
|||
* @param {string} presetKey - Key from EQ_PRESETS
|
||||
*/
|
||||
applyPreset(presetKey) {
|
||||
const preset = EQ_PRESETS[presetKey];
|
||||
const presets = getPresetsForBandCount(this.bandCount);
|
||||
const preset = presets[presetKey];
|
||||
if (!preset) return;
|
||||
|
||||
this.setAllGains(preset.gains);
|
||||
|
|
@ -306,37 +477,47 @@ export class Equalizer {
|
|||
* Reset all bands to flat (0 dB)
|
||||
*/
|
||||
reset() {
|
||||
this.setAllGains(new Array(16).fill(0));
|
||||
this.setAllGains(new Array(this.bandCount).fill(0));
|
||||
equalizerSettings.setPreset('flat');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current gains
|
||||
* @returns {number[]} Array of 16 gain values
|
||||
* @returns {number[]} Array of gain values
|
||||
*/
|
||||
getGains() {
|
||||
return [...this.currentGains];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get frequency labels
|
||||
* Get current band count
|
||||
* @returns {number} Number of bands
|
||||
*/
|
||||
static getFrequencyLabels() {
|
||||
return FREQUENCY_LABELS;
|
||||
getBandCount() {
|
||||
return this.bandCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get frequency labels for UI
|
||||
* @returns {string[]} Array of frequency labels
|
||||
*/
|
||||
getFrequencyLabels() {
|
||||
return this.frequencyLabels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get frequencies
|
||||
* @returns {number[]} Array of frequency values
|
||||
*/
|
||||
static getFrequencies() {
|
||||
return EQ_FREQUENCIES;
|
||||
getFrequencies() {
|
||||
return this.frequencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available presets
|
||||
* Get available presets (static method for default 16 bands)
|
||||
*/
|
||||
static getPresets() {
|
||||
return EQ_PRESETS;
|
||||
static getPresets(bandCount = 16) {
|
||||
return getPresetsForBandCount(bandCount);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -344,7 +525,11 @@ export class Equalizer {
|
|||
*/
|
||||
_loadSettings() {
|
||||
this.isEnabled = equalizerSettings.isEnabled();
|
||||
this.currentGains = equalizerSettings.getGains();
|
||||
this.bandCount = equalizerSettings.getBandCount();
|
||||
this.freqRange = equalizerSettings.getFreqRange();
|
||||
this.frequencies = generateFrequencies(this.bandCount, this.freqRange.min, this.freqRange.max);
|
||||
this.frequencyLabels = generateFrequencyLabels(this.frequencies);
|
||||
this.currentGains = equalizerSettings.getGains(this.bandCount);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -380,5 +565,13 @@ export class Equalizer {
|
|||
// Export singleton instance
|
||||
export const equalizer = new Equalizer();
|
||||
|
||||
// Export constants
|
||||
export { EQ_FREQUENCIES, FREQUENCY_LABELS, EQ_PRESETS };
|
||||
// Export helper functions and constants
|
||||
export {
|
||||
generateFrequencies,
|
||||
generateFrequencyLabels,
|
||||
getPresetsForBandCount,
|
||||
interpolatePreset,
|
||||
DEFAULT_EQ_FREQUENCIES,
|
||||
DEFAULT_FREQUENCY_LABELS,
|
||||
EQ_PRESETS_16BAND as EQ_PRESETS,
|
||||
};
|
||||
|
|
|
|||
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 eqContainer = document.getElementById('equalizer-container');
|
||||
const eqPresetSelect = document.getElementById('equalizer-preset-select');
|
||||
const eqResetBtn = document.getElementById('equalizer-reset-btn');
|
||||
const eqBands = document.querySelectorAll('.eq-band');
|
||||
const eqBandsContainer = document.getElementById('equalizer-bands');
|
||||
const customPresetsOptgroup = document.getElementById('custom-presets-optgroup');
|
||||
const customPresetNameInput = document.getElementById('custom-preset-name');
|
||||
const saveCustomPresetBtn = document.getElementById('save-custom-preset-btn');
|
||||
const deleteCustomPresetBtn = document.getElementById('delete-custom-preset-btn');
|
||||
const eqBandCountInput = document.getElementById('eq-band-count');
|
||||
const eqRangeMinInput = document.getElementById('eq-range-min');
|
||||
const eqRangeMaxInput = document.getElementById('eq-range-max');
|
||||
const applyEqRangeBtn = document.getElementById('apply-eq-range-btn');
|
||||
const eqFreqMinInput = document.getElementById('eq-freq-min');
|
||||
const eqFreqMaxInput = document.getElementById('eq-freq-max');
|
||||
const applyEqFreqBtn = document.getElementById('apply-eq-freq-btn');
|
||||
const resetEqFreqBtn = document.getElementById('reset-eq-freq-btn');
|
||||
const resetEqRangeBtn = document.getElementById('reset-eq-range-btn');
|
||||
const eqScaleContainer = document.querySelector('.equalizer-scale');
|
||||
|
||||
// Current settings
|
||||
let currentBandCount = equalizerSettings.getBandCount();
|
||||
let currentRange = equalizerSettings.getRange();
|
||||
let currentFreqRange = equalizerSettings.getFreqRange();
|
||||
|
||||
/**
|
||||
* Generate frequency labels for given band count and frequency range
|
||||
*/
|
||||
const generateFreqLabels = (count, minFreq = currentFreqRange.min, maxFreq = currentFreqRange.max) => {
|
||||
const labels = [];
|
||||
const safeMin = Math.max(10, minFreq);
|
||||
const safeMax = Math.min(96000, maxFreq);
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const t = i / (count - 1);
|
||||
const freq = safeMin * Math.pow(safeMax / safeMin, t);
|
||||
const rounded = Math.round(freq);
|
||||
|
||||
if (rounded < 1000) {
|
||||
labels.push(rounded.toString());
|
||||
} else if (rounded < 10000) {
|
||||
labels.push((rounded / 1000).toFixed(rounded % 1000 === 0 ? 0 : 1) + 'K');
|
||||
} else {
|
||||
labels.push((rounded / 1000).toFixed(0) + 'K');
|
||||
}
|
||||
}
|
||||
|
||||
return labels;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate EQ bands HTML
|
||||
*/
|
||||
const generateEQBands = (
|
||||
count,
|
||||
rangeMin = currentRange.min,
|
||||
rangeMax = currentRange.max,
|
||||
freqMin = currentFreqRange.min,
|
||||
freqMax = currentFreqRange.max
|
||||
) => {
|
||||
if (!eqBandsContainer) return;
|
||||
|
||||
const labels = generateFreqLabels(count, freqMin, freqMax);
|
||||
eqBandsContainer.innerHTML = '';
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const bandEl = document.createElement('div');
|
||||
bandEl.className = 'eq-band';
|
||||
bandEl.dataset.band = i;
|
||||
|
||||
bandEl.innerHTML = `
|
||||
<input
|
||||
type="range"
|
||||
class="eq-slider"
|
||||
min="${rangeMin}"
|
||||
max="${rangeMax}"
|
||||
step="0.5"
|
||||
value="0"
|
||||
orient="vertical"
|
||||
/>
|
||||
<span class="eq-value">0</span>
|
||||
<span class="eq-freq">${labels[i]}</span>
|
||||
`;
|
||||
|
||||
eqBandsContainer.appendChild(bandEl);
|
||||
}
|
||||
|
||||
// Re-initialize band sliders
|
||||
initializeBandSliders();
|
||||
};
|
||||
|
||||
/**
|
||||
* Update EQ scale display
|
||||
*/
|
||||
const updateEQScale = (min, max) => {
|
||||
if (!eqScaleContainer) return;
|
||||
const spans = eqScaleContainer.querySelectorAll('span');
|
||||
if (spans.length >= 3) {
|
||||
spans[0].textContent = `+${max} dB`;
|
||||
spans[1].textContent = '0 dB';
|
||||
spans[2].textContent = `${min} dB`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the visual display of a band value
|
||||
|
|
@ -889,6 +987,9 @@ export function initializeSettings(scrobbler, player, api, ui) {
|
|||
* Update all band sliders and displays from an array of gains
|
||||
*/
|
||||
const updateAllBandUI = (gains) => {
|
||||
const eqBands = eqBandsContainer?.querySelectorAll('.eq-band');
|
||||
if (!eqBands) return;
|
||||
|
||||
eqBands.forEach((bandEl, index) => {
|
||||
const slider = bandEl.querySelector('.eq-slider');
|
||||
if (slider && gains[index] !== undefined) {
|
||||
|
|
@ -907,48 +1008,59 @@ export function initializeSettings(scrobbler, player, api, ui) {
|
|||
}
|
||||
};
|
||||
|
||||
// Initialize EQ toggle
|
||||
if (eqToggle) {
|
||||
const isEnabled = equalizerSettings.isEnabled();
|
||||
eqToggle.checked = isEnabled;
|
||||
updateEQContainerVisibility(isEnabled);
|
||||
/**
|
||||
* Populate custom presets in the dropdown
|
||||
*/
|
||||
const populateCustomPresets = () => {
|
||||
if (!customPresetsOptgroup) return;
|
||||
|
||||
eqToggle.addEventListener('change', (e) => {
|
||||
const enabled = e.target.checked;
|
||||
audioContextManager.toggleEQ(enabled);
|
||||
updateEQContainerVisibility(enabled);
|
||||
});
|
||||
}
|
||||
// Clear existing custom presets
|
||||
customPresetsOptgroup.innerHTML = '';
|
||||
|
||||
// Initialize preset selector
|
||||
if (eqPresetSelect) {
|
||||
eqPresetSelect.value = equalizerSettings.getPreset();
|
||||
const customPresets = equalizerSettings.getCustomPresets();
|
||||
const presetIds = Object.keys(customPresets);
|
||||
|
||||
eqPresetSelect.addEventListener('change', (e) => {
|
||||
const presetKey = e.target.value;
|
||||
const preset = EQ_PRESETS[presetKey];
|
||||
if (presetIds.length === 0) {
|
||||
const emptyOption = document.createElement('option');
|
||||
emptyOption.value = '';
|
||||
emptyOption.textContent = 'No custom presets saved';
|
||||
emptyOption.disabled = true;
|
||||
customPresetsOptgroup.appendChild(emptyOption);
|
||||
} else {
|
||||
presetIds.forEach((presetId) => {
|
||||
const preset = customPresets[presetId];
|
||||
const option = document.createElement('option');
|
||||
option.value = presetId;
|
||||
option.textContent = preset.name;
|
||||
customPresetsOptgroup.appendChild(option);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (preset) {
|
||||
audioContextManager.applyPreset(presetKey);
|
||||
updateAllBandUI(preset.gains);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Check if a preset ID is a custom preset
|
||||
*/
|
||||
const isCustomPreset = (presetId) => {
|
||||
return presetId && presetId.startsWith('custom_');
|
||||
};
|
||||
|
||||
// Initialize reset button
|
||||
if (eqResetBtn) {
|
||||
eqResetBtn.addEventListener('click', () => {
|
||||
audioContextManager.reset();
|
||||
updateAllBandUI(new Array(16).fill(0));
|
||||
if (eqPresetSelect) {
|
||||
eqPresetSelect.value = 'flat';
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Update delete button visibility based on selected preset
|
||||
*/
|
||||
const updateDeleteButtonVisibility = () => {
|
||||
if (!deleteCustomPresetBtn || !eqPresetSelect) return;
|
||||
const isCustom = isCustomPreset(eqPresetSelect.value);
|
||||
deleteCustomPresetBtn.style.display = isCustom ? 'flex' : 'none';
|
||||
};
|
||||
|
||||
// Initialize all band sliders
|
||||
if (eqBands.length > 0) {
|
||||
const savedGains = equalizerSettings.getGains();
|
||||
/**
|
||||
* Initialize band slider event listeners
|
||||
*/
|
||||
const initializeBandSliders = () => {
|
||||
const eqBands = eqBandsContainer?.querySelectorAll('.eq-band');
|
||||
if (!eqBands || eqBands.length === 0) return;
|
||||
|
||||
const savedGains = equalizerSettings.getGains(currentBandCount);
|
||||
|
||||
eqBands.forEach((bandEl) => {
|
||||
const bandIndex = parseInt(bandEl.dataset.band, 10);
|
||||
|
|
@ -966,16 +1078,15 @@ export function initializeSettings(scrobbler, player, api, ui) {
|
|||
audioContextManager.setBandGain(bandIndex, gain);
|
||||
updateBandValueDisplay(bandEl, gain);
|
||||
|
||||
// When manually adjusting, switch preset to 'flat' (custom)
|
||||
// to indicate the user has made custom changes
|
||||
// When manually adjusting, check if we should clear preset
|
||||
if (eqPresetSelect && eqPresetSelect.value !== 'flat') {
|
||||
// Check if current gains still match the selected preset
|
||||
const currentPreset = EQ_PRESETS[eqPresetSelect.value];
|
||||
const currentGains = audioContextManager.getGains();
|
||||
const builtInPresets = EQ_PRESETS;
|
||||
const currentPreset = builtInPresets[eqPresetSelect.value];
|
||||
if (currentPreset) {
|
||||
const currentGains = audioContextManager.getGains();
|
||||
const matches = currentPreset.gains.every((g, i) => Math.abs(g - currentGains[i]) < 0.01);
|
||||
if (!matches) {
|
||||
// Don't change the select, but the preset will save as 'custom'
|
||||
// User has deviated from preset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -989,8 +1100,423 @@ export function initializeSettings(scrobbler, player, api, ui) {
|
|||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Initialize EQ toggle
|
||||
if (eqToggle) {
|
||||
const isEnabled = equalizerSettings.isEnabled();
|
||||
eqToggle.checked = isEnabled;
|
||||
updateEQContainerVisibility(isEnabled);
|
||||
|
||||
eqToggle.addEventListener('change', (e) => {
|
||||
const enabled = e.target.checked;
|
||||
audioContextManager.toggleEQ(enabled);
|
||||
updateEQContainerVisibility(enabled);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize band count input
|
||||
if (eqBandCountInput) {
|
||||
eqBandCountInput.value = currentBandCount;
|
||||
|
||||
eqBandCountInput.addEventListener('change', (e) => {
|
||||
const newCount = parseInt(e.target.value, 10);
|
||||
if (newCount >= equalizerSettings.MIN_BANDS && newCount <= equalizerSettings.MAX_BANDS) {
|
||||
currentBandCount = newCount;
|
||||
|
||||
// Save new band count and update audio context
|
||||
equalizerSettings.setBandCount(newCount);
|
||||
audioContextManager.setBandCount?.(newCount) || audioContextManager.reinitialize?.();
|
||||
|
||||
// Regenerate UI
|
||||
generateEQBands(
|
||||
newCount,
|
||||
currentRange.min,
|
||||
currentRange.max,
|
||||
currentFreqRange.min,
|
||||
currentFreqRange.max
|
||||
);
|
||||
|
||||
// Reset to flat and apply
|
||||
const flatGains = new Array(newCount).fill(0);
|
||||
audioContextManager.setAllGains(flatGains);
|
||||
updateAllBandUI(flatGains);
|
||||
|
||||
if (eqPresetSelect) {
|
||||
eqPresetSelect.value = 'flat';
|
||||
equalizerSettings.setPreset('flat');
|
||||
}
|
||||
updateDeleteButtonVisibility();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize preset selector
|
||||
if (eqPresetSelect) {
|
||||
populateCustomPresets();
|
||||
eqPresetSelect.value = equalizerSettings.getPreset();
|
||||
updateDeleteButtonVisibility();
|
||||
|
||||
eqPresetSelect.addEventListener('change', (e) => {
|
||||
const presetKey = e.target.value;
|
||||
|
||||
// Check if it's a custom preset
|
||||
if (isCustomPreset(presetKey)) {
|
||||
const customPresets = equalizerSettings.getCustomPresets();
|
||||
const customPreset = customPresets[presetKey];
|
||||
if (customPreset && customPreset.gains) {
|
||||
// Check if preset has different band count
|
||||
const presetBands = customPreset.bandCount || customPreset.gains.length;
|
||||
if (presetBands !== currentBandCount) {
|
||||
// Update band count to match preset
|
||||
currentBandCount = presetBands;
|
||||
equalizerSettings.setBandCount(presetBands);
|
||||
if (eqBandCountInput) eqBandCountInput.value = presetBands;
|
||||
generateEQBands(
|
||||
presetBands,
|
||||
currentRange.min,
|
||||
currentRange.max,
|
||||
currentFreqRange.min,
|
||||
currentFreqRange.max
|
||||
);
|
||||
}
|
||||
audioContextManager.setAllGains(customPreset.gains);
|
||||
updateAllBandUI(customPreset.gains);
|
||||
equalizerSettings.setPreset(presetKey);
|
||||
}
|
||||
} else {
|
||||
// Built-in preset - use current band count
|
||||
const presets = EQ_PRESETS;
|
||||
const preset = presets[presetKey];
|
||||
if (preset) {
|
||||
audioContextManager.applyPreset(presetKey);
|
||||
updateAllBandUI(preset.gains);
|
||||
}
|
||||
}
|
||||
updateDeleteButtonVisibility();
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize reset button
|
||||
if (eqResetBtn) {
|
||||
eqResetBtn.addEventListener('click', () => {
|
||||
audioContextManager.reset();
|
||||
updateAllBandUI(new Array(currentBandCount).fill(0));
|
||||
if (eqPresetSelect) {
|
||||
eqPresetSelect.value = 'flat';
|
||||
updateDeleteButtonVisibility();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize save custom preset button
|
||||
if (saveCustomPresetBtn && customPresetNameInput) {
|
||||
saveCustomPresetBtn.addEventListener('click', () => {
|
||||
const name = customPresetNameInput.value.trim();
|
||||
if (!name) {
|
||||
alert('Please enter a name for your preset');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentGains = audioContextManager.getGains();
|
||||
const presetId = equalizerSettings.saveCustomPreset(name, currentGains);
|
||||
|
||||
if (presetId) {
|
||||
populateCustomPresets();
|
||||
if (eqPresetSelect) {
|
||||
eqPresetSelect.value = presetId;
|
||||
equalizerSettings.setPreset(presetId);
|
||||
updateDeleteButtonVisibility();
|
||||
}
|
||||
customPresetNameInput.value = '';
|
||||
|
||||
// Show feedback
|
||||
const originalText = saveCustomPresetBtn.textContent;
|
||||
saveCustomPresetBtn.textContent = 'Saved!';
|
||||
setTimeout(() => {
|
||||
saveCustomPresetBtn.textContent = originalText;
|
||||
}, 1500);
|
||||
} else {
|
||||
alert('Failed to save preset. Please try again.');
|
||||
}
|
||||
});
|
||||
|
||||
// Allow saving with Enter key
|
||||
customPresetNameInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
saveCustomPresetBtn.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize delete custom preset button
|
||||
if (deleteCustomPresetBtn) {
|
||||
deleteCustomPresetBtn.addEventListener('click', () => {
|
||||
if (!eqPresetSelect) return;
|
||||
|
||||
const presetId = eqPresetSelect.value;
|
||||
if (!isCustomPreset(presetId)) return;
|
||||
|
||||
const customPresets = equalizerSettings.getCustomPresets();
|
||||
const presetName = customPresets[presetId]?.name || 'this preset';
|
||||
|
||||
if (confirm(`Are you sure you want to delete "${presetName}"?`)) {
|
||||
const success = equalizerSettings.deleteCustomPreset(presetId);
|
||||
if (success) {
|
||||
populateCustomPresets();
|
||||
eqPresetSelect.value = 'flat';
|
||||
audioContextManager.reset();
|
||||
updateAllBandUI(new Array(currentBandCount).fill(0));
|
||||
equalizerSettings.setPreset('flat');
|
||||
updateDeleteButtonVisibility();
|
||||
} else {
|
||||
alert('Failed to delete preset. Please try again.');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize range inputs
|
||||
if (eqRangeMinInput) {
|
||||
eqRangeMinInput.value = currentRange.min;
|
||||
}
|
||||
if (eqRangeMaxInput) {
|
||||
eqRangeMaxInput.value = currentRange.max;
|
||||
}
|
||||
updateEQScale(currentRange.min, currentRange.max);
|
||||
|
||||
// Initialize apply range button
|
||||
if (applyEqRangeBtn && eqRangeMinInput && eqRangeMaxInput) {
|
||||
applyEqRangeBtn.addEventListener('click', () => {
|
||||
const newMin = parseInt(eqRangeMinInput.value, 10);
|
||||
const newMax = parseInt(eqRangeMaxInput.value, 10);
|
||||
|
||||
// Validate range
|
||||
if (isNaN(newMin) || isNaN(newMax)) {
|
||||
alert('Please enter valid numbers for the range');
|
||||
return;
|
||||
}
|
||||
|
||||
if (newMin >= 0 || newMax <= 0) {
|
||||
alert('Minimum must be negative and maximum must be positive');
|
||||
return;
|
||||
}
|
||||
|
||||
if (newMin < equalizerSettings.ABSOLUTE_MIN || newMax > equalizerSettings.ABSOLUTE_MAX) {
|
||||
alert(
|
||||
`Range must be between ${equalizerSettings.ABSOLUTE_MIN} and ${equalizerSettings.ABSOLUTE_MAX} dB`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save new range
|
||||
equalizerSettings.setRange(newMin, newMax);
|
||||
currentRange = { min: newMin, max: newMax };
|
||||
|
||||
// Regenerate bands with new range
|
||||
generateEQBands(currentBandCount, newMin, newMax);
|
||||
|
||||
// Update scale display
|
||||
updateEQScale(newMin, newMax);
|
||||
|
||||
// Reset gains to flat
|
||||
const flatGains = new Array(currentBandCount).fill(0);
|
||||
audioContextManager.setAllGains(flatGains);
|
||||
updateAllBandUI(flatGains);
|
||||
|
||||
// Reset to flat preset
|
||||
if (eqPresetSelect) {
|
||||
eqPresetSelect.value = 'flat';
|
||||
equalizerSettings.setPreset('flat');
|
||||
}
|
||||
|
||||
// Show feedback
|
||||
const originalText = applyEqRangeBtn.textContent;
|
||||
applyEqRangeBtn.textContent = 'Applied!';
|
||||
setTimeout(() => {
|
||||
applyEqRangeBtn.textContent = originalText;
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize reset DB range button
|
||||
if (resetEqRangeBtn) {
|
||||
resetEqRangeBtn.addEventListener('click', () => {
|
||||
// Reset to default values
|
||||
const defaultMin = equalizerSettings.DEFAULT_RANGE_MIN;
|
||||
const defaultMax = equalizerSettings.DEFAULT_RANGE_MAX;
|
||||
|
||||
// Update inputs
|
||||
if (eqRangeMinInput) eqRangeMinInput.value = defaultMin;
|
||||
if (eqRangeMaxInput) eqRangeMaxInput.value = defaultMax;
|
||||
|
||||
// Save new range
|
||||
equalizerSettings.setRange(defaultMin, defaultMax);
|
||||
currentRange = { min: defaultMin, max: defaultMax };
|
||||
|
||||
// Regenerate bands with new range
|
||||
generateEQBands(currentBandCount, defaultMin, defaultMax, currentFreqRange.min, currentFreqRange.max);
|
||||
|
||||
// Update scale display
|
||||
updateEQScale(defaultMin, defaultMax);
|
||||
|
||||
// Reset gains to flat
|
||||
const flatGains = new Array(currentBandCount).fill(0);
|
||||
audioContextManager.setAllGains(flatGains);
|
||||
updateAllBandUI(flatGains);
|
||||
|
||||
// Reset to flat preset
|
||||
if (eqPresetSelect) {
|
||||
eqPresetSelect.value = 'flat';
|
||||
equalizerSettings.setPreset('flat');
|
||||
}
|
||||
|
||||
// Show feedback
|
||||
const originalText = resetEqRangeBtn.textContent;
|
||||
resetEqRangeBtn.textContent = 'Reset!';
|
||||
setTimeout(() => {
|
||||
resetEqRangeBtn.textContent = originalText;
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize frequency range inputs
|
||||
if (eqFreqMinInput) {
|
||||
eqFreqMinInput.value = currentFreqRange.min;
|
||||
}
|
||||
if (eqFreqMaxInput) {
|
||||
eqFreqMaxInput.value = currentFreqRange.max;
|
||||
}
|
||||
|
||||
// Initialize apply frequency range button
|
||||
if (applyEqFreqBtn && eqFreqMinInput && eqFreqMaxInput) {
|
||||
applyEqFreqBtn.addEventListener('click', () => {
|
||||
const newMin = parseInt(eqFreqMinInput.value, 10);
|
||||
const newMax = parseInt(eqFreqMaxInput.value, 10);
|
||||
|
||||
// Validate range
|
||||
if (isNaN(newMin) || isNaN(newMax)) {
|
||||
alert('Please enter valid numbers for the frequency range');
|
||||
return;
|
||||
}
|
||||
|
||||
if (newMin < equalizerSettings.ABSOLUTE_FREQ_MIN || newMax > equalizerSettings.ABSOLUTE_FREQ_MAX) {
|
||||
alert(
|
||||
`Frequency range must be between ${equalizerSettings.ABSOLUTE_FREQ_MIN} Hz and ${equalizerSettings.ABSOLUTE_FREQ_MAX} Hz`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newMin >= newMax) {
|
||||
alert('Minimum frequency must be less than maximum frequency');
|
||||
return;
|
||||
}
|
||||
|
||||
// Save new frequency range
|
||||
equalizerSettings.setFreqRange(newMin, newMax);
|
||||
currentFreqRange = { min: newMin, max: newMax };
|
||||
|
||||
// Update audio context
|
||||
audioContextManager.setFreqRange(newMin, newMax);
|
||||
|
||||
// Regenerate bands with new frequency range
|
||||
generateEQBands(currentBandCount, currentRange.min, currentRange.max, newMin, newMax);
|
||||
|
||||
// Reset gains to flat
|
||||
const flatGains = new Array(currentBandCount).fill(0);
|
||||
audioContextManager.setAllGains(flatGains);
|
||||
updateAllBandUI(flatGains);
|
||||
|
||||
// Reset to flat preset
|
||||
if (eqPresetSelect) {
|
||||
eqPresetSelect.value = 'flat';
|
||||
equalizerSettings.setPreset('flat');
|
||||
}
|
||||
|
||||
// Show feedback
|
||||
const originalText = applyEqFreqBtn.textContent;
|
||||
applyEqFreqBtn.textContent = 'Applied!';
|
||||
setTimeout(() => {
|
||||
applyEqFreqBtn.textContent = originalText;
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize reset frequency range button
|
||||
if (resetEqFreqBtn) {
|
||||
resetEqFreqBtn.addEventListener('click', () => {
|
||||
// Reset to default values
|
||||
const defaultMin = equalizerSettings.DEFAULT_FREQ_MIN;
|
||||
const defaultMax = equalizerSettings.DEFAULT_FREQ_MAX;
|
||||
|
||||
// Update inputs
|
||||
if (eqFreqMinInput) eqFreqMinInput.value = defaultMin;
|
||||
if (eqFreqMaxInput) eqFreqMaxInput.value = defaultMax;
|
||||
|
||||
// Save new frequency range
|
||||
equalizerSettings.setFreqRange(defaultMin, defaultMax);
|
||||
currentFreqRange = { min: defaultMin, max: defaultMax };
|
||||
|
||||
// Update audio context
|
||||
audioContextManager.setFreqRange(defaultMin, defaultMax);
|
||||
|
||||
// Regenerate bands with new frequency range
|
||||
generateEQBands(currentBandCount, currentRange.min, currentRange.max, defaultMin, defaultMax);
|
||||
|
||||
// Reset gains to flat
|
||||
const flatGains = new Array(currentBandCount).fill(0);
|
||||
audioContextManager.setAllGains(flatGains);
|
||||
updateAllBandUI(flatGains);
|
||||
|
||||
// Reset to flat preset
|
||||
if (eqPresetSelect) {
|
||||
eqPresetSelect.value = 'flat';
|
||||
equalizerSettings.setPreset('flat');
|
||||
}
|
||||
|
||||
// Show feedback
|
||||
const originalText = resetEqFreqBtn.textContent;
|
||||
resetEqFreqBtn.textContent = 'Reset!';
|
||||
setTimeout(() => {
|
||||
resetEqFreqBtn.textContent = originalText;
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
// Generate initial EQ bands with current ranges
|
||||
generateEQBands(currentBandCount, currentRange.min, currentRange.max, currentFreqRange.min, currentFreqRange.max);
|
||||
|
||||
// Listen for band count changes from other sources
|
||||
window.addEventListener('equalizer-band-count-changed', (e) => {
|
||||
if (e.detail && e.detail.bandCount) {
|
||||
currentBandCount = e.detail.bandCount;
|
||||
if (eqBandCountInput) eqBandCountInput.value = currentBandCount;
|
||||
generateEQBands(
|
||||
currentBandCount,
|
||||
currentRange.min,
|
||||
currentRange.max,
|
||||
currentFreqRange.min,
|
||||
currentFreqRange.max
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for frequency range changes from other sources
|
||||
window.addEventListener('equalizer-freq-range-changed', (e) => {
|
||||
if (e.detail && e.detail.min !== undefined && e.detail.max !== undefined) {
|
||||
currentFreqRange = { min: e.detail.min, max: e.detail.max };
|
||||
if (eqFreqMinInput) eqFreqMinInput.value = currentFreqRange.min;
|
||||
if (eqFreqMaxInput) eqFreqMaxInput.value = currentFreqRange.max;
|
||||
generateEQBands(
|
||||
currentBandCount,
|
||||
currentRange.min,
|
||||
currentRange.max,
|
||||
currentFreqRange.min,
|
||||
currentFreqRange.max
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Now Playing Mode
|
||||
const nowPlayingMode = document.getElementById('now-playing-mode');
|
||||
if (nowPlayingMode) {
|
||||
|
|
|
|||
301
js/storage.js
301
js/storage.js
|
|
@ -791,6 +791,23 @@ export const equalizerSettings = {
|
|||
ENABLED_KEY: 'equalizer-enabled',
|
||||
GAINS_KEY: 'equalizer-gains',
|
||||
PRESET_KEY: 'equalizer-preset',
|
||||
CUSTOM_PRESETS_KEY: 'equalizer-custom-presets',
|
||||
BAND_COUNT_KEY: 'equalizer-band-count',
|
||||
RANGE_MIN_KEY: 'equalizer-range-min',
|
||||
RANGE_MAX_KEY: 'equalizer-range-max',
|
||||
FREQ_MIN_KEY: 'equalizer-freq-min',
|
||||
FREQ_MAX_KEY: 'equalizer-freq-max',
|
||||
DEFAULT_BAND_COUNT: 16,
|
||||
MIN_BANDS: 3,
|
||||
MAX_BANDS: 32,
|
||||
DEFAULT_RANGE_MIN: -30,
|
||||
DEFAULT_RANGE_MAX: 30,
|
||||
ABSOLUTE_MIN: -60,
|
||||
ABSOLUTE_MAX: 60,
|
||||
DEFAULT_FREQ_MIN: 20,
|
||||
DEFAULT_FREQ_MAX: 20000,
|
||||
ABSOLUTE_FREQ_MIN: 10,
|
||||
ABSOLUTE_FREQ_MAX: 96000,
|
||||
|
||||
isEnabled() {
|
||||
try {
|
||||
|
|
@ -805,25 +822,178 @@ export const equalizerSettings = {
|
|||
localStorage.setItem(this.ENABLED_KEY, enabled ? 'true' : 'false');
|
||||
},
|
||||
|
||||
getGains() {
|
||||
getBandCount() {
|
||||
try {
|
||||
const stored = localStorage.getItem(this.BAND_COUNT_KEY);
|
||||
if (stored) {
|
||||
const count = parseInt(stored, 10);
|
||||
if (!isNaN(count) && count >= this.MIN_BANDS && count <= this.MAX_BANDS) {
|
||||
return count;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return this.DEFAULT_BAND_COUNT;
|
||||
},
|
||||
|
||||
setBandCount(count) {
|
||||
const validCount = Math.max(
|
||||
this.MIN_BANDS,
|
||||
Math.min(this.MAX_BANDS, parseInt(count, 10) || this.DEFAULT_BAND_COUNT)
|
||||
);
|
||||
localStorage.setItem(this.BAND_COUNT_KEY, validCount.toString());
|
||||
},
|
||||
|
||||
getRangeMin() {
|
||||
try {
|
||||
const stored = localStorage.getItem(this.RANGE_MIN_KEY);
|
||||
if (stored) {
|
||||
const val = parseInt(stored, 10);
|
||||
if (!isNaN(val) && val >= this.ABSOLUTE_MIN && val < 0) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return this.DEFAULT_RANGE_MIN;
|
||||
},
|
||||
|
||||
setRangeMin(value) {
|
||||
const val = parseInt(value, 10);
|
||||
if (!isNaN(val) && val >= this.ABSOLUTE_MIN && val < 0) {
|
||||
localStorage.setItem(this.RANGE_MIN_KEY, val.toString());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
getRangeMax() {
|
||||
try {
|
||||
const stored = localStorage.getItem(this.RANGE_MAX_KEY);
|
||||
if (stored) {
|
||||
const val = parseInt(stored, 10);
|
||||
if (!isNaN(val) && val > 0 && val <= this.ABSOLUTE_MAX) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return this.DEFAULT_RANGE_MAX;
|
||||
},
|
||||
|
||||
setRangeMax(value) {
|
||||
const val = parseInt(value, 10);
|
||||
if (!isNaN(val) && val > 0 && val <= this.ABSOLUTE_MAX) {
|
||||
localStorage.setItem(this.RANGE_MAX_KEY, val.toString());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
getRange() {
|
||||
return {
|
||||
min: this.getRangeMin(),
|
||||
max: this.getRangeMax(),
|
||||
};
|
||||
},
|
||||
|
||||
setRange(min, max) {
|
||||
const validMin = this.setRangeMin(min);
|
||||
const validMax = this.setRangeMax(max);
|
||||
return validMin && validMax;
|
||||
},
|
||||
|
||||
getFreqMin() {
|
||||
try {
|
||||
const stored = localStorage.getItem(this.FREQ_MIN_KEY);
|
||||
if (stored) {
|
||||
const val = parseInt(stored, 10);
|
||||
if (!isNaN(val) && val >= this.ABSOLUTE_FREQ_MIN && val < this.DEFAULT_FREQ_MAX) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return this.DEFAULT_FREQ_MIN;
|
||||
},
|
||||
|
||||
setFreqMin(value) {
|
||||
const val = parseInt(value, 10);
|
||||
if (!isNaN(val) && val >= this.ABSOLUTE_FREQ_MIN && val < this.getFreqMax()) {
|
||||
localStorage.setItem(this.FREQ_MIN_KEY, val.toString());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
getFreqMax() {
|
||||
try {
|
||||
const stored = localStorage.getItem(this.FREQ_MAX_KEY);
|
||||
if (stored) {
|
||||
const val = parseInt(stored, 10);
|
||||
if (!isNaN(val) && val > this.getFreqMin() && val <= this.ABSOLUTE_FREQ_MAX) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return this.DEFAULT_FREQ_MAX;
|
||||
},
|
||||
|
||||
setFreqMax(value) {
|
||||
const val = parseInt(value, 10);
|
||||
if (!isNaN(val) && val > this.getFreqMin() && val <= this.ABSOLUTE_FREQ_MAX) {
|
||||
localStorage.setItem(this.FREQ_MAX_KEY, val.toString());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
getFreqRange() {
|
||||
return {
|
||||
min: this.getFreqMin(),
|
||||
max: this.getFreqMax(),
|
||||
};
|
||||
},
|
||||
|
||||
setFreqRange(min, max) {
|
||||
const validMax = this.setFreqMax(max);
|
||||
const validMin = this.setFreqMin(min);
|
||||
return validMin && validMax;
|
||||
},
|
||||
|
||||
getGains(bandCount) {
|
||||
const count = bandCount || this.getBandCount();
|
||||
try {
|
||||
const stored = localStorage.getItem(this.GAINS_KEY);
|
||||
if (stored) {
|
||||
const gains = JSON.parse(stored);
|
||||
if (Array.isArray(gains) && gains.length === 16) {
|
||||
return gains;
|
||||
if (Array.isArray(gains)) {
|
||||
// If stored gains match current band count, return them
|
||||
if (gains.length === count) {
|
||||
return gains;
|
||||
}
|
||||
// If different band count, try to interpolate or return flat
|
||||
if (gains.length > 0) {
|
||||
return this._interpolateGains(gains, count);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
// Return flat EQ (all zeros) by default
|
||||
return new Array(16).fill(0);
|
||||
return new Array(count).fill(0);
|
||||
},
|
||||
|
||||
setGains(gains) {
|
||||
try {
|
||||
if (Array.isArray(gains) && gains.length === 16) {
|
||||
if (Array.isArray(gains) && gains.length >= this.MIN_BANDS && gains.length <= this.MAX_BANDS) {
|
||||
localStorage.setItem(this.GAINS_KEY, JSON.stringify(gains));
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
@ -831,6 +1001,31 @@ export const equalizerSettings = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Interpolate gains array to match target band count
|
||||
*/
|
||||
_interpolateGains(sourceGains, targetCount) {
|
||||
if (sourceGains.length === targetCount) {
|
||||
return [...sourceGains];
|
||||
}
|
||||
|
||||
const result = [];
|
||||
for (let i = 0; i < targetCount; i++) {
|
||||
// Map target index to source index
|
||||
const sourceIndex = (i / (targetCount - 1)) * (sourceGains.length - 1);
|
||||
const indexLow = Math.floor(sourceIndex);
|
||||
const indexHigh = Math.min(Math.ceil(sourceIndex), sourceGains.length - 1);
|
||||
const fraction = sourceIndex - indexLow;
|
||||
|
||||
// Linear interpolation
|
||||
const lowValue = sourceGains[indexLow] || 0;
|
||||
const highValue = sourceGains[indexHigh] || 0;
|
||||
const interpolated = lowValue + (highValue - lowValue) * fraction;
|
||||
result.push(Math.round(interpolated * 10) / 10);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
getPreset() {
|
||||
try {
|
||||
return localStorage.getItem(this.PRESET_KEY) || 'flat';
|
||||
|
|
@ -842,6 +1037,102 @@ export const equalizerSettings = {
|
|||
setPreset(preset) {
|
||||
localStorage.setItem(this.PRESET_KEY, preset);
|
||||
},
|
||||
|
||||
// Custom Preset Methods
|
||||
getCustomPresets() {
|
||||
try {
|
||||
const stored = localStorage.getItem(this.CUSTOM_PRESETS_KEY);
|
||||
if (stored) {
|
||||
const presets = JSON.parse(stored);
|
||||
if (typeof presets === 'object' && presets !== null) {
|
||||
return presets;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return {};
|
||||
},
|
||||
|
||||
saveCustomPreset(name, gains) {
|
||||
try {
|
||||
if (!name || !Array.isArray(gains) || gains.length < this.MIN_BANDS || gains.length > this.MAX_BANDS) {
|
||||
console.warn('[EQ] Invalid preset data');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sanitize name - remove special characters and limit length
|
||||
const sanitizedName = name
|
||||
.trim()
|
||||
.substring(0, 50)
|
||||
.replace(/[^\w\s-]/g, '');
|
||||
if (!sanitizedName) {
|
||||
console.warn('[EQ] Invalid preset name');
|
||||
return false;
|
||||
}
|
||||
|
||||
const presets = this.getCustomPresets();
|
||||
const presetId = 'custom_' + Date.now();
|
||||
|
||||
presets[presetId] = {
|
||||
name: sanitizedName,
|
||||
gains: gains.map((g) => Math.round(g * 10) / 10), // Round to 1 decimal place
|
||||
bandCount: gains.length,
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
|
||||
localStorage.setItem(this.CUSTOM_PRESETS_KEY, JSON.stringify(presets));
|
||||
return presetId;
|
||||
} catch (e) {
|
||||
console.warn('[EQ] Failed to save custom preset:', e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
deleteCustomPreset(presetId) {
|
||||
try {
|
||||
const presets = this.getCustomPresets();
|
||||
if (presets[presetId]) {
|
||||
delete presets[presetId];
|
||||
localStorage.setItem(this.CUSTOM_PRESETS_KEY, JSON.stringify(presets));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
console.warn('[EQ] Failed to delete custom preset:', e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
updateCustomPreset(presetId, name, gains) {
|
||||
try {
|
||||
const presets = this.getCustomPresets();
|
||||
if (!presets[presetId]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (name !== undefined) {
|
||||
const sanitizedName = name
|
||||
.trim()
|
||||
.substring(0, 50)
|
||||
.replace(/[^\w\s-]/g, '');
|
||||
if (sanitizedName) {
|
||||
presets[presetId].name = sanitizedName;
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(gains) && gains.length === 16) {
|
||||
presets[presetId].gains = gains.map((g) => Math.round(g * 10) / 10);
|
||||
presets[presetId].updatedAt = Date.now();
|
||||
}
|
||||
|
||||
localStorage.setItem(this.CUSTOM_PRESETS_KEY, JSON.stringify(presets));
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.warn('[EQ] Failed to update custom preset:', e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const monoAudioSettings = {
|
||||
|
|
|
|||
244
styles.css
244
styles.css
|
|
@ -5805,6 +5805,42 @@ textarea:focus {
|
|||
box-shadow: 0 0 0 3px rgb(var(--highlight-rgb), 0.2);
|
||||
}
|
||||
|
||||
.eq-band-count-input {
|
||||
width: 60px;
|
||||
padding: 0.5rem;
|
||||
background: var(--input);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: var(--foreground);
|
||||
font-size: 0.9rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color var(--transition-fast),
|
||||
box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.eq-band-count-input:hover {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.eq-band-count-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--ring);
|
||||
box-shadow: 0 0 0 3px rgb(var(--highlight-rgb), 0.2);
|
||||
}
|
||||
|
||||
/* Hide number input arrows */
|
||||
.eq-band-count-input::-webkit-outer-spin-button,
|
||||
.eq-band-count-input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.eq-band-count-input[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
#equalizer-reset-btn {
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
|
|
@ -5831,6 +5867,214 @@ textarea:focus {
|
|||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
/* Custom Preset Controls */
|
||||
.custom-preset-controls {
|
||||
margin-top: var(--spacing-md);
|
||||
padding-top: var(--spacing-md);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.custom-preset-input-row {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
#custom-preset-name {
|
||||
flex: 1;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--input);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: var(--foreground);
|
||||
font-size: 0.9rem;
|
||||
transition: border-color var(--transition-fast);
|
||||
}
|
||||
|
||||
#custom-preset-name:focus {
|
||||
outline: none;
|
||||
border-color: var(--ring);
|
||||
box-shadow: 0 0 0 3px rgb(var(--highlight-rgb), 0.2);
|
||||
}
|
||||
|
||||
#save-custom-preset-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
padding: 0.5rem 1rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#save-custom-preset-btn svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.delete-preset-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--destructive);
|
||||
border-color: var(--destructive);
|
||||
opacity: 0.8;
|
||||
transition: opacity var(--transition-fast);
|
||||
}
|
||||
|
||||
.delete-preset-btn:hover {
|
||||
opacity: 1;
|
||||
background: var(--destructive);
|
||||
color: var(--destructive-foreground);
|
||||
}
|
||||
|
||||
.delete-preset-btn svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* EQ Range Controls */
|
||||
.eq-range-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
margin-top: var(--spacing-sm);
|
||||
padding-top: var(--spacing-sm);
|
||||
border-top: 1px solid var(--border);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.eq-range-controls label {
|
||||
font-size: 0.9rem;
|
||||
color: var(--muted-foreground);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.eq-range-controls span {
|
||||
font-size: 0.9rem;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.eq-range-input {
|
||||
width: 60px;
|
||||
padding: 0.4rem 0.5rem;
|
||||
background: var(--input);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: var(--foreground);
|
||||
font-size: 0.9rem;
|
||||
text-align: center;
|
||||
transition: border-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.eq-range-input:hover {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.eq-range-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--ring);
|
||||
box-shadow: 0 0 0 3px rgb(var(--highlight-rgb), 0.2);
|
||||
}
|
||||
|
||||
/* Hide number input arrows */
|
||||
.eq-range-input::-webkit-outer-spin-button,
|
||||
.eq-range-input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.eq-range-input[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
#apply-eq-range-btn {
|
||||
padding: 0.4rem 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
#reset-eq-range-btn {
|
||||
padding: 0.4rem 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
margin-left: var(--spacing-xs);
|
||||
}
|
||||
|
||||
/* EQ Frequency Range Controls */
|
||||
.eq-freq-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
margin-top: var(--spacing-sm);
|
||||
padding-top: var(--spacing-sm);
|
||||
border-top: 1px solid var(--border);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.eq-freq-controls label {
|
||||
font-size: 0.9rem;
|
||||
color: var(--muted-foreground);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.eq-freq-controls span {
|
||||
font-size: 0.9rem;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.eq-freq-input {
|
||||
width: 70px;
|
||||
padding: 0.4rem 0.5rem;
|
||||
background: var(--input);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: var(--foreground);
|
||||
font-size: 0.9rem;
|
||||
text-align: center;
|
||||
transition: border-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.eq-freq-input:hover {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.eq-freq-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--ring);
|
||||
box-shadow: 0 0 0 3px rgb(var(--highlight-rgb), 0.2);
|
||||
}
|
||||
|
||||
/* Hide number input arrows */
|
||||
.eq-freq-input::-webkit-outer-spin-button,
|
||||
.eq-freq-input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.eq-freq-input[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
#apply-eq-freq-btn {
|
||||
padding: 0.4rem 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
#reset-eq-freq-btn {
|
||||
padding: 0.4rem 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
margin-left: var(--spacing-xs);
|
||||
}
|
||||
|
||||
/* Equalizer preset dropdown styling */
|
||||
.equalizer-preset-row select optgroup {
|
||||
font-weight: 600;
|
||||
color: var(--foreground);
|
||||
padding: var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
.equalizer-preset-row select optgroup option {
|
||||
font-weight: 400;
|
||||
padding-left: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.equalizer-bands {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
|
|||
Loading…
Reference in a new issue