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:
parent
0cfff0b0b2
commit
91eaa1f1dc
5 changed files with 283 additions and 38 deletions
77
index.html
77
index.html
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
111
js/settings.js
111
js/settings.js
|
|
@ -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'),
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
23
styles.css
23
styles.css
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue