feat: configurable band count and frequency range for legacy graphic EQ

- Add Bands/Min Hz/Max Hz controls to legacy EQ section
- Dynamic frequency generation with log spacing and auto-scaling Q
- Import/export handles variable band counts, Q optional for shelves
- Custom presets interpolate across different band counts
- Update legacy EQ tutorial for new controls
This commit is contained in:
tryptz 2026-04-06 23:49:52 -04:00 committed by edideaur
parent 0cfff0b0b2
commit 91eaa1f1dc
5 changed files with 283 additions and 38 deletions

View file

@ -4174,6 +4174,54 @@
× ×
</button> </button>
<div
class="eq-howto-tab legacy"
id="eq-howto-legacy"
style="display: none"
>
<h4>Legacy EQ - Graphic Equalizer</h4>
<ol>
<li>
Set the <b>number of bands</b> (3-32) and
<b>frequency range</b> (Min/Max Hz) at the top to
customize the equalizer layout.
</li>
<li>
<b>Drag the sliders</b> to boost or cut each frequency
band. Bands are spaced logarithmically across your range.
</li>
<li>
Pick a <b>preset</b> (Bass Boost, Rock, Vocal, etc.) as a
starting point - presets auto-scale to your band count.
</li>
<li>
Adjust the <b>preamp</b> slider to raise or lower the overall
level - reduce it if you hear distortion from large boosts.
</li>
<li>
<b>Save</b> your own custom presets with a name so you can
recall them later.
</li>
<li>
<b>Export</b> saves the EQ in EqualizerAPO text format.
<b>Export APO</b> saves a GraphicEQ config line you can paste
directly into Equalizer APO's config.txt.
</li>
<li>
<b>Import</b> loads EQ settings from EqualizerAPO text files
or simple frequency/gain CSV files - points are mapped to
your current bands automatically.
</li>
<li>
Click <b>Reset</b> to flatten all bands back to 0 dB.
</li>
</ol>
<p class="eq-howto-tip">
Tip: Cut problem frequencies rather than boosting others - it
sounds cleaner and avoids clipping.
</p>
</div>
<div class="eq-howto-tab autoeq" id="eq-howto-autoeq"> <div class="eq-howto-tab autoeq" id="eq-howto-autoeq">
<h4>AutoEQ - Headphone Correction</h4> <h4>AutoEQ - Headphone Correction</h4>
<ol> <ol>
@ -4297,6 +4345,35 @@
<!-- Legacy 16-Band Graphic EQ (visible in legacy mode) --> <!-- Legacy 16-Band Graphic EQ (visible in legacy mode) -->
<div class="graphic-eq-section" id="graphic-eq-section" style="display: none"> <div class="graphic-eq-section" id="graphic-eq-section" style="display: none">
<div class="graphic-eq-config-row">
<label>Bands</label>
<input
type="number"
id="legacy-geq-band-count"
min="3"
max="32"
value="16"
class="geq-config-input"
/>
<label>Min Hz</label>
<input
type="number"
id="legacy-geq-freq-min"
min="10"
max="96000"
value="25"
class="geq-config-input"
/>
<label>Max Hz</label>
<input
type="number"
id="legacy-geq-freq-max"
min="10"
max="96000"
value="20000"
class="geq-config-input"
/>
</div>
<div class="graphic-eq-preset-row"> <div class="graphic-eq-preset-row">
<label for="legacy-graphic-eq-preset-select" class="graphic-eq-preset-label" <label for="legacy-graphic-eq-preset-select" class="graphic-eq-preset-label"
>Preset</label >Preset</label

View file

@ -148,13 +148,15 @@ class AudioContextManager {
// Callbacks for audio graph changes (for visualizers like Butterchurn) // Callbacks for audio graph changes (for visualizers like Butterchurn)
this._graphChangeCallbacks = []; this._graphChangeCallbacks = [];
// --- Graphic EQ (16-band, separate chain) --- // --- Graphic EQ (configurable bands, separate chain) ---
this.geqFilters = []; this.geqFilters = [];
this.geqPreampNode = null; this.geqPreampNode = null;
this.geqOutputNode = null; this.geqOutputNode = null;
this.isGraphicEQEnabled = equalizerSettings.isGraphicEqEnabled(); this.isGraphicEQEnabled = equalizerSettings.isGraphicEqEnabled();
this.geqFrequencies = [25, 40, 63, 100, 160, 250, 400, 630, 1000, 1600, 2500, 4000, 6300, 10000, 16000, 20000]; this.geqBandCount = equalizerSettings.getGraphicEqBandCount();
this.geqGains = equalizerSettings.getGraphicEqGains(); this.geqFreqRange = equalizerSettings.getGraphicEqFreqRange();
this.geqFrequencies = generateFrequencies(this.geqBandCount, this.geqFreqRange.min, this.geqFreqRange.max);
this.geqGains = equalizerSettings.getGraphicEqGains(this.geqBandCount);
this.geqPreamp = equalizerSettings.getGraphicEqPreamp(); this.geqPreamp = equalizerSettings.getGraphicEqPreamp();
// Load saved settings // Load saved settings
@ -1090,11 +1092,12 @@ class AudioContextManager {
this.geqOutputNode = this.audioContext.createGain(); this.geqOutputNode = this.audioContext.createGain();
this.geqOutputNode.gain.value = 1; this.geqOutputNode.gain.value = 1;
const geqQ = 2.5 * Math.sqrt(16 / this.geqBandCount);
this.geqFilters = this.geqFrequencies.map((freq, i) => { this.geqFilters = this.geqFrequencies.map((freq, i) => {
const filter = this.audioContext.createBiquadFilter(); const filter = this.audioContext.createBiquadFilter();
filter.type = 'peaking'; filter.type = 'peaking';
filter.frequency.value = freq; filter.frequency.value = freq;
filter.Q.value = 2.5; // constant Q for 16-band filter.Q.value = geqQ;
filter.gain.value = this.geqGains[i] || 0; filter.gain.value = this.geqGains[i] || 0;
return filter; return filter;
}); });
@ -1136,7 +1139,7 @@ class AudioContextManager {
} }
setGraphicEqBandGain(bandIndex, gainDb) { setGraphicEqBandGain(bandIndex, gainDb) {
if (bandIndex < 0 || bandIndex >= 16) return; if (bandIndex < 0 || bandIndex >= this.geqBandCount) return;
this.geqGains[bandIndex] = Math.max(-30, Math.min(30, gainDb)); this.geqGains[bandIndex] = Math.max(-30, Math.min(30, gainDb));
if (this.geqFilters[bandIndex] && this.audioContext) { if (this.geqFilters[bandIndex] && this.audioContext) {
const now = this.audioContext.currentTime; const now = this.audioContext.currentTime;
@ -1149,7 +1152,7 @@ class AudioContextManager {
if (!Array.isArray(gains)) return; if (!Array.isArray(gains)) return;
const now = this.audioContext?.currentTime || 0; const now = this.audioContext?.currentTime || 0;
gains.forEach((g, i) => { gains.forEach((g, i) => {
if (i >= 16) return; if (i >= this.geqBandCount) return;
this.geqGains[i] = Math.max(-30, Math.min(30, g)); this.geqGains[i] = Math.max(-30, Math.min(30, g));
if (this.geqFilters[i]) { if (this.geqFilters[i]) {
this.geqFilters[i].gain.setTargetAtTime(this.geqGains[i], now, 0.01); this.geqFilters[i].gain.setTargetAtTime(this.geqGains[i], now, 0.01);
@ -1158,6 +1161,51 @@ class AudioContextManager {
equalizerSettings.setGraphicEqGains([...this.geqGains]); equalizerSettings.setGraphicEqGains([...this.geqGains]);
} }
setGraphicEqBandCount(count) {
const newCount = Math.max(3, Math.min(32, parseInt(count, 10) || 16));
if (newCount === this.geqBandCount) return;
const oldGains = this.geqGains;
this.geqBandCount = newCount;
this.geqFrequencies = generateFrequencies(newCount, this.geqFreqRange.min, this.geqFreqRange.max);
this.geqGains = equalizerSettings._interpolateGains(oldGains, newCount);
equalizerSettings.setGraphicEqBandCount(newCount);
equalizerSettings.setGraphicEqGains(this.geqGains);
if (this.isInitialized && this.audioContext) {
this._destroyGraphicEQ();
this._createGraphicEQ();
this._connectGraph();
}
}
setGraphicEqFreqRange(minFreq, maxFreq) {
const newMin = Math.max(10, Math.min(96000, parseInt(minFreq, 10) || 25));
const newMax = Math.max(10, Math.min(96000, parseInt(maxFreq, 10) || 20000));
if (newMin >= newMax) return;
if (newMin === this.geqFreqRange.min && newMax === this.geqFreqRange.max) return;
this.geqFreqRange = { min: newMin, max: newMax };
this.geqFrequencies = generateFrequencies(this.geqBandCount, newMin, newMax);
equalizerSettings.setGraphicEqFreqRange(newMin, newMax);
if (this.isInitialized && this.audioContext) {
this._destroyGraphicEQ();
this._createGraphicEQ();
this._connectGraph();
}
}
getGraphicEqFrequencies() {
return this.geqFrequencies;
}
getGraphicEqBandCount() {
return this.geqBandCount;
}
setGraphicEqPreamp(db) { setGraphicEqPreamp(db) {
this.geqPreamp = Math.max(-20, Math.min(20, parseFloat(db) || 0)); this.geqPreamp = Math.max(-20, Math.min(20, parseFloat(db) || 0));
if (this.geqPreampNode && this.audioContext) { if (this.geqPreampNode && this.audioContext) {

View file

@ -1236,26 +1236,29 @@ export async function initializeSettings(scrobbler, player, api, ui) {
} }
// ======================================== // ========================================
// 16-Band Graphic Equalizer (Legacy EQ mode) // Graphic Equalizer (Legacy EQ mode) - Configurable Bands
// ======================================== // ========================================
const GEQ_LABELS = [ let geqBandCount = equalizerSettings.getGraphicEqBandCount();
'25', let geqFreqRange = equalizerSettings.getGraphicEqFreqRange();
'40',
'63', const formatGeqFreq = (freq) => {
'100', if (freq >= 10000) return (freq / 1000).toFixed(0) + 'K';
'160', if (freq >= 1000) return (freq / 1000).toFixed(freq % 1000 === 0 ? 0 : 1) + 'K';
'250', return freq.toString();
'400', };
'630',
'1K', const generateGeqFrequencies = (count, min, max) => {
'1.6K', const freqs = [];
'2.5K', for (let i = 0; i < count; i++) {
'4K', const t = i / (count - 1);
'6.3K', freqs.push(Math.round(min * Math.pow(max / min, t)));
'10K', }
'16K', return freqs;
'20K', };
];
let GEQ_FREQUENCIES = generateGeqFrequencies(geqBandCount, geqFreqRange.min, geqFreqRange.max);
let GEQ_LABELS = GEQ_FREQUENCIES.map(formatGeqFreq);
const geqBandsContainer = document.getElementById('graphic-eq-bands'); const geqBandsContainer = document.getElementById('graphic-eq-bands');
const geqPreampSlider = document.getElementById('graphic-eq-preamp-slider'); const geqPreampSlider = document.getElementById('graphic-eq-preamp-slider');
const geqPreampValue = document.getElementById('graphic-eq-preamp-value'); const geqPreampValue = document.getElementById('graphic-eq-preamp-value');
@ -1268,11 +1271,15 @@ export async function initializeSettings(scrobbler, player, api, ui) {
const legacyGeqPresetSelect = document.getElementById('legacy-graphic-eq-preset-select'); const legacyGeqPresetSelect = document.getElementById('legacy-graphic-eq-preset-select');
const legacyGeqResetBtn = document.getElementById('legacy-graphic-eq-reset-btn'); const legacyGeqResetBtn = document.getElementById('legacy-graphic-eq-reset-btn');
const geqBandCountInput = document.getElementById('legacy-geq-band-count');
const geqFreqMinInput = document.getElementById('legacy-geq-freq-min');
const geqFreqMaxInput = document.getElementById('legacy-geq-freq-max');
const geqPreampSliders = [geqPreampSlider, legacyGeqPreampSlider].filter(Boolean); const geqPreampSliders = [geqPreampSlider, legacyGeqPreampSlider].filter(Boolean);
const geqPreampValues = [geqPreampValue, legacyGeqPreampValue].filter(Boolean); const geqPreampValues = [geqPreampValue, legacyGeqPreampValue].filter(Boolean);
const geqPresetSelects = [geqPresetSelect, legacyGeqPresetSelect].filter(Boolean); const geqPresetSelects = [geqPresetSelect, legacyGeqPresetSelect].filter(Boolean);
let geqGains = equalizerSettings.getGraphicEqGains() || new Array(16).fill(0); let geqGains = equalizerSettings.getGraphicEqGains(geqBandCount) || new Array(geqBandCount).fill(0);
let geqPreamp = equalizerSettings.getGraphicEqPreamp() || 0; let geqPreamp = equalizerSettings.getGraphicEqPreamp() || 0;
const geqRange = equalizerSettings.getRange(); const geqRange = equalizerSettings.getRange();
@ -1288,7 +1295,7 @@ export async function initializeSettings(scrobbler, player, api, ui) {
}); });
}; };
// Build 16 vertical slider bands into a container // Build vertical slider bands into a container
const buildGeqBands = (container, idPrefix) => { const buildGeqBands = (container, idPrefix) => {
if (!container) return; if (!container) return;
container.innerHTML = ''; container.innerHTML = '';
@ -1359,7 +1366,7 @@ export async function initializeSettings(scrobbler, player, api, ui) {
select.addEventListener('change', () => { select.addEventListener('change', () => {
const key = select.value; const key = select.value;
if (!key) return; if (!key) return;
const presets = getPresetsForBandCount(16); const presets = getPresetsForBandCount(geqBandCount);
const preset = presets[key]; const preset = presets[key];
if (!preset) return; if (!preset) return;
geqGains = [...preset.gains]; geqGains = [...preset.gains];
@ -1375,7 +1382,7 @@ export async function initializeSettings(scrobbler, player, api, ui) {
// Wire up reset buttons // Wire up reset buttons
[geqResetBtn, legacyGeqResetBtn].filter(Boolean).forEach((btn) => { [geqResetBtn, legacyGeqResetBtn].filter(Boolean).forEach((btn) => {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
geqGains = new Array(16).fill(0); geqGains = new Array(geqBandCount).fill(0);
equalizerSettings.setGraphicEqGains(geqGains); equalizerSettings.setGraphicEqGains(geqGains);
audioContextManager.setGraphicEqAllGains(geqGains); audioContextManager.setGraphicEqAllGains(geqGains);
geqSyncAllSliders(); geqSyncAllSliders();
@ -1383,6 +1390,49 @@ export async function initializeSettings(scrobbler, player, api, ui) {
}); });
}); });
// Band count and frequency range controls
const rebuildGeq = () => {
GEQ_FREQUENCIES = generateGeqFrequencies(geqBandCount, geqFreqRange.min, geqFreqRange.max);
GEQ_LABELS = GEQ_FREQUENCIES.map(formatGeqFreq);
buildGeqBands(geqBandsContainer, 'geq');
buildGeqBands(legacyGeqBandsContainer, 'legacy-geq');
geqSyncAllSliders();
};
if (geqBandCountInput) {
geqBandCountInput.value = geqBandCount;
geqBandCountInput.addEventListener('change', () => {
const newCount = Math.max(3, Math.min(32, parseInt(geqBandCountInput.value, 10) || 16));
geqBandCountInput.value = newCount;
if (newCount === geqBandCount) return;
geqGains = equalizerSettings._interpolateGains(geqGains, newCount);
geqBandCount = newCount;
audioContextManager.setGraphicEqBandCount(newCount);
rebuildGeq();
geqPresetSelects.forEach((s) => (s.value = ''));
});
}
if (geqFreqMinInput && geqFreqMaxInput) {
geqFreqMinInput.value = geqFreqRange.min;
geqFreqMaxInput.value = geqFreqRange.max;
const handleFreqRangeChange = () => {
const newMin = Math.max(10, Math.min(96000, parseInt(geqFreqMinInput.value, 10) || 25));
const newMax = Math.max(10, Math.min(96000, parseInt(geqFreqMaxInput.value, 10) || 20000));
geqFreqMinInput.value = newMin;
geqFreqMaxInput.value = newMax;
if (newMin >= newMax) return;
if (newMin === geqFreqRange.min && newMax === geqFreqRange.max) return;
geqFreqRange = { min: newMin, max: newMax };
audioContextManager.setGraphicEqFreqRange(newMin, newMax);
rebuildGeq();
};
geqFreqMinInput.addEventListener('change', handleFreqRangeChange);
geqFreqMaxInput.addEventListener('change', handleFreqRangeChange);
}
// Legacy EQ Import / Export // Legacy EQ Import / Export
const parseGeqLabelFrequency = (label) => { const parseGeqLabelFrequency = (label) => {
const normalized = String(label).trim().toLowerCase().replace(/\s+/g, ''); const normalized = String(label).trim().toLowerCase().replace(/\s+/g, '');
@ -1395,7 +1445,6 @@ export async function initializeSettings(scrobbler, player, api, ui) {
} }
return Number.parseFloat(withoutHz); return Number.parseFloat(withoutHz);
}; };
const GEQ_FREQUENCIES = GEQ_LABELS.map((label) => parseGeqLabelFrequency(label));
const legacyGeqExportBtn = document.getElementById('legacy-geq-export-btn'); const legacyGeqExportBtn = document.getElementById('legacy-geq-export-btn');
const legacyGeqExportCsvBtn = document.getElementById('legacy-geq-export-csv-btn'); const legacyGeqExportCsvBtn = document.getElementById('legacy-geq-export-csv-btn');
const legacyGeqImportBtn = document.getElementById('legacy-geq-import-btn'); const legacyGeqImportBtn = document.getElementById('legacy-geq-import-btn');
@ -1405,7 +1454,8 @@ export async function initializeSettings(scrobbler, player, api, ui) {
legacyGeqExportBtn.addEventListener('click', () => { legacyGeqExportBtn.addEventListener('click', () => {
const lines = [`Preamp: ${geqPreamp.toFixed(1)} dB`]; const lines = [`Preamp: ${geqPreamp.toFixed(1)} dB`];
GEQ_FREQUENCIES.forEach((freq, i) => { GEQ_FREQUENCIES.forEach((freq, i) => {
lines.push(`Filter ${i + 1}: ON PK Fc ${freq} Hz Gain ${geqGains[i].toFixed(1)} dB Q 1.41`); const q = (2.5 * Math.sqrt(16 / geqBandCount)).toFixed(2);
lines.push(`Filter ${i + 1}: ON PK Fc ${freq} Hz Gain ${geqGains[i].toFixed(1)} dB Q ${q}`);
}); });
const blob = new Blob([lines.join('\n')], { type: 'text/plain' }); const blob = new Blob([lines.join('\n')], { type: 'text/plain' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
@ -1484,7 +1534,7 @@ export async function initializeSettings(scrobbler, player, api, ui) {
// Sort by frequency // Sort by frequency
validPoints.sort((a, b) => a.freq - b.freq); validPoints.sort((a, b) => a.freq - b.freq);
// Map imported points to the 16 GEQ bands using nearest-frequency matching // Map imported points to the GEQ bands using nearest-frequency matching
const newGains = GEQ_FREQUENCIES.map((targetFreq) => { const newGains = GEQ_FREQUENCIES.map((targetFreq) => {
// Find the closest imported point by log-frequency distance // Find the closest imported point by log-frequency distance
let closest = validPoints[0]; let closest = validPoints[0];
@ -1608,11 +1658,14 @@ export async function initializeSettings(scrobbler, player, api, ui) {
const customPresets = getLegacyGeqCustomPresets(); const customPresets = getLegacyGeqCustomPresets();
if (customPresets[key]) { if (customPresets[key]) {
const gains = customPresets[key]?.gains; const gains = customPresets[key]?.gains;
if (!Array.isArray(gains) || gains.length !== GEQ_FREQUENCIES.length) { if (!Array.isArray(gains) || gains.length === 0) {
updateDeleteBtnVisibility(); updateDeleteBtnVisibility();
return; return;
} }
geqGains = gains.map((g) => { const adjusted = gains.length !== geqBandCount
? equalizerSettings._interpolateGains(gains, geqBandCount)
: gains;
geqGains = adjusted.map((g) => {
const n = Number(g); const n = Number(g);
return Number.isFinite(n) return Number.isFinite(n)
? Math.max(parseFloat(geqRange.min), Math.min(parseFloat(geqRange.max), n)) ? Math.max(parseFloat(geqRange.min), Math.min(parseFloat(geqRange.max), n))
@ -3812,6 +3865,7 @@ export async function initializeSettings(scrobbler, player, api, ui) {
const hp = document.getElementById('eq-howto-panel'); const hp = document.getElementById('eq-howto-panel');
if (hp && hp.style.display !== 'none') { if (hp && hp.style.display !== 'none') {
const tabs = { const tabs = {
legacy: document.getElementById('eq-howto-legacy'),
autoeq: document.getElementById('eq-howto-autoeq'), autoeq: document.getElementById('eq-howto-autoeq'),
parametric: document.getElementById('eq-howto-parametric'), parametric: document.getElementById('eq-howto-parametric'),
speaker: document.getElementById('eq-howto-speaker'), speaker: document.getElementById('eq-howto-speaker'),
@ -3834,6 +3888,7 @@ export async function initializeSettings(scrobbler, player, api, ui) {
const howtoPanel = document.getElementById('eq-howto-panel'); const howtoPanel = document.getElementById('eq-howto-panel');
const howtoClose = document.getElementById('eq-howto-close'); const howtoClose = document.getElementById('eq-howto-close');
const howtoTabs = { const howtoTabs = {
legacy: document.getElementById('eq-howto-legacy'),
autoeq: document.getElementById('eq-howto-autoeq'), autoeq: document.getElementById('eq-howto-autoeq'),
parametric: document.getElementById('eq-howto-parametric'), parametric: document.getElementById('eq-howto-parametric'),
speaker: document.getElementById('eq-howto-speaker'), speaker: document.getElementById('eq-howto-speaker'),

View file

@ -1700,10 +1700,12 @@ export const equalizerSettings = {
localStorage.removeItem(this.AUTOEQ_LAST_HEADPHONE_KEY); localStorage.removeItem(this.AUTOEQ_LAST_HEADPHONE_KEY);
}, },
// --- Graphic EQ (16-band) separate storage --- // --- Graphic EQ separate storage ---
GEQ_ENABLED_KEY: 'graphic-eq-enabled', GEQ_ENABLED_KEY: 'graphic-eq-enabled',
GEQ_GAINS_KEY: 'graphic-eq-gains', GEQ_GAINS_KEY: 'graphic-eq-gains',
GEQ_PREAMP_KEY: 'graphic-eq-preamp', GEQ_PREAMP_KEY: 'graphic-eq-preamp',
GEQ_BAND_COUNT_KEY: 'graphic-eq-band-count',
GEQ_FREQ_RANGE_KEY: 'graphic-eq-freq-range',
isGraphicEqEnabled() { isGraphicEqEnabled() {
try { try {
@ -1721,19 +1723,59 @@ export const equalizerSettings = {
} }
}, },
getGraphicEqGains() { getGraphicEqBandCount() {
try {
const val = localStorage.getItem(this.GEQ_BAND_COUNT_KEY);
if (val !== null) {
const num = parseInt(val, 10);
if (num >= 3 && num <= 32) return num;
}
} catch { /* ignore */ }
return 16;
},
setGraphicEqBandCount(count) {
try {
localStorage.setItem(this.GEQ_BAND_COUNT_KEY, String(count));
} catch { /* ignore */ }
},
getGraphicEqFreqRange() {
try {
const stored = localStorage.getItem(this.GEQ_FREQ_RANGE_KEY);
if (stored) {
const parsed = JSON.parse(stored);
if (parsed && Number.isFinite(parsed.min) && Number.isFinite(parsed.max)) {
return parsed;
}
}
} catch { /* ignore */ }
return { min: 25, max: 20000 };
},
setGraphicEqFreqRange(min, max) {
try {
localStorage.setItem(this.GEQ_FREQ_RANGE_KEY, JSON.stringify({ min, max }));
} catch { /* ignore */ }
},
getGraphicEqGains(bandCount) {
try { try {
const stored = localStorage.getItem(this.GEQ_GAINS_KEY); const stored = localStorage.getItem(this.GEQ_GAINS_KEY);
if (stored) { if (stored) {
const parsed = JSON.parse(stored); const parsed = JSON.parse(stored);
if (Array.isArray(parsed) && parsed.length === 16) { const expectedCount = bandCount || this.getGraphicEqBandCount();
if (Array.isArray(parsed) && parsed.length === expectedCount) {
return parsed.map((v) => (Number.isFinite(v) ? v : 0)); return parsed.map((v) => (Number.isFinite(v) ? v : 0));
} }
if (Array.isArray(parsed) && parsed.length > 0) {
return this._interpolateGains(parsed, expectedCount);
}
} }
} catch { } catch {
/* ignore */ /* ignore */
} }
return new Array(16).fill(0); return new Array(bandCount || this.getGraphicEqBandCount()).fill(0);
}, },
setGraphicEqGains(gains) { setGraphicEqGains(gains) {

View file

@ -7902,6 +7902,29 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
gap: var(--spacing-md); gap: var(--spacing-md);
} }
.graphic-eq-config-row {
display: flex;
align-items: center;
gap: var(--spacing-sm);
flex-wrap: wrap;
}
.graphic-eq-config-row label {
font-size: 0.8rem;
font-weight: 600;
color: var(--foreground);
}
.geq-config-input {
width: 70px;
padding: 4px 6px;
border: 1px solid var(--border-color, #444);
border-radius: 4px;
background: var(--bg-secondary, #222);
color: inherit;
font-size: 0.85rem;
}
.graphic-eq-preset-row { .graphic-eq-preset-row {
display: flex; display: flex;
align-items: center; align-items: center;