EQ changes

This commit is contained in:
Eduard Prigoana 2026-02-17 20:22:47 +00:00
parent 2fce3e382e
commit f20f3dbb9d
6 changed files with 774 additions and 17 deletions

View file

@ -3770,6 +3770,28 @@
<path d="M3 3v5h5" /> <path d="M3 3v5h5" />
</svg> </svg>
</button> </button>
<button
id="eq-export-btn"
class="btn-secondary"
title="Export EQ settings to text"
style="font-size: 0.75rem; padding: 0.25rem 0.5rem"
>
Export
</button>
<button
id="eq-import-btn"
class="btn-secondary"
title="Import EQ settings from text or file"
style="font-size: 0.75rem; padding: 0.25rem 0.5rem"
>
Import
</button>
<input
type="file"
id="eq-import-file"
accept=".txt"
style="display: none"
/>
</div> </div>
<div class="custom-preset-controls"> <div class="custom-preset-controls">
@ -3889,10 +3911,41 @@
Reset Reset
</button> </button>
</div> </div>
<div
class="eq-preamp-controls"
style="display: flex; align-items: center; gap: 0.5rem; margin-top: 0.5rem"
>
<label style="font-size: 0.75rem; opacity: 0.8">Preamp:</label>
<input
type="range"
id="eq-preamp-slider"
min="-20"
max="20"
step="0.1"
value="0"
style="flex: 1; max-width: 120px"
title="Preamp gain in dB"
/>
<input
type="number"
id="eq-preamp-input"
min="-20"
max="20"
step="0.1"
value="0"
style="width: 60px; padding: 0.25rem; font-size: 0.75rem"
title="Preamp value in dB"
/>
<span style="font-size: 0.75rem">dB</span>
</div>
</div> </div>
<div class="equalizer-bands" id="equalizer-bands"> <div class="equalizer-bands-wrapper">
<!-- Bands will be dynamically generated by JavaScript --> <canvas id="eq-response-canvas" class="eq-response-canvas"></canvas>
<div class="equalizer-bands" id="equalizer-bands">
<!-- Bands will be dynamically generated by JavaScript -->
</div>
</div> </div>
<div class="equalizer-scale"> <div class="equalizer-scale">

View file

@ -142,6 +142,8 @@ class AudioContextManager {
if (this.isInitialized && this.audioContext) { if (this.isInitialized && this.audioContext) {
this._destroyEQ(); this._destroyEQ();
this._createEQ(); this._createEQ();
// Reconnect the audio graph without interrupting playback
this._connectGraph();
} }
// Dispatch event for UI update // Dispatch event for UI update
@ -177,6 +179,8 @@ class AudioContextManager {
if (this.isInitialized && this.audioContext) { if (this.isInitialized && this.audioContext) {
this._destroyEQ(); this._destroyEQ();
this._createEQ(); this._createEQ();
// Reconnect the audio graph without interrupting playback
this._connectGraph();
} }
// Dispatch event for UI update // Dispatch event for UI update
@ -203,6 +207,16 @@ class AudioContextManager {
}); });
} }
this.filters = []; this.filters = [];
// Destroy preamp node
if (this.preampNode) {
try {
this.preampNode.disconnect();
} catch {
/* ignore */
}
this.preampNode = null;
}
} }
/** /**
@ -211,6 +225,15 @@ class AudioContextManager {
_createEQ() { _createEQ() {
if (!this.audioContext) return; if (!this.audioContext) return;
// Create preamp node
if (!this.preampNode) {
this.preampNode = this.audioContext.createGain();
}
// Set preamp gain
const preampValue = this.preamp || 0;
const gainValue = Math.pow(10, preampValue / 20);
this.preampNode.gain.value = gainValue;
// Create biquad filters for each frequency band // Create biquad filters for each frequency band
this.filters = this.frequencies.map((freq, index) => { this.filters = this.frequencies.map((freq, index) => {
const filter = this.audioContext.createBiquadFilter(); const filter = this.audioContext.createBiquadFilter();
@ -366,13 +389,18 @@ class AudioContextManager {
} }
if (this.isEQEnabled && this.filters.length > 0) { if (this.isEQEnabled && this.filters.length > 0) {
// EQ enabled: lastNode -> EQ filters -> output -> analyser -> volume -> destination // EQ enabled: lastNode -> preamp -> EQ filters -> output -> analyser -> volume -> destination
// Connect filter chain // Connect filter chain
for (let i = 0; i < this.filters.length - 1; i++) { for (let i = 0; i < this.filters.length - 1; i++) {
this.filters[i].connect(this.filters[i + 1]); this.filters[i].connect(this.filters[i + 1]);
} }
// Connect input to first filter and last filter to output // Connect preamp to first filter
lastNode.connect(this.filters[0]); if (this.preampNode) {
lastNode.connect(this.preampNode);
this.preampNode.connect(this.filters[0]);
} else {
lastNode.connect(this.filters[0]);
}
this.filters[this.filters.length - 1].connect(this.outputNode); this.filters[this.filters.length - 1].connect(this.outputNode);
this.outputNode.connect(this.analyser); this.outputNode.connect(this.analyser);
this.analyser.connect(this.volumeNode); this.analyser.connect(this.volumeNode);
@ -609,6 +637,119 @@ class AudioContextManager {
this.frequencies = generateFrequencies(this.bandCount, this.freqRange.min, this.freqRange.max); this.frequencies = generateFrequencies(this.bandCount, this.freqRange.min, this.freqRange.max);
this.currentGains = equalizerSettings.getGains(this.bandCount); this.currentGains = equalizerSettings.getGains(this.bandCount);
this.isMonoAudioEnabled = monoAudioSettings.isEnabled(); this.isMonoAudioEnabled = monoAudioSettings.isEnabled();
this.preamp = equalizerSettings.getPreamp();
}
/**
* Set preamp value in dB
* @param {number} db - Preamp value in dB (-20 to +20)
*/
setPreamp(db) {
const clampedDb = Math.max(-20, Math.min(20, parseFloat(db) || 0));
this.preamp = clampedDb;
equalizerSettings.setPreamp(clampedDb);
// Update preamp node if it exists
if (this.preampNode && this.audioContext) {
const gainValue = Math.pow(10, clampedDb / 20);
const now = this.audioContext.currentTime;
this.preampNode.gain.setTargetAtTime(gainValue, now, 0.01);
}
}
/**
* Get current preamp value
* @returns {number} Current preamp value in dB
*/
getPreamp() {
return this.preamp || 0;
}
/**
* Export equalizer settings to text format
* @returns {string} Exported settings in text format
*/
exportEQToText() {
const lines = [];
const preampValue = this.getPreamp();
lines.push(`Preamp: ${preampValue.toFixed(1)} dB`);
this.frequencies.forEach((freq, index) => {
const gain = this.currentGains[index] || 0;
const filterNum = index + 1;
lines.push(`Filter ${filterNum}: ON PK Fc ${freq} Hz Gain ${gain.toFixed(1)} dB Q 0.71`);
});
return lines.join('\n');
}
/**
* Import equalizer settings from text format
* @param {string} text - Text format settings
* @returns {boolean} True if import was successful
*/
importEQFromText(text) {
try {
const lines = text
.split('\n')
.map((line) => line.trim())
.filter((line) => line);
const filters = [];
let preamp = 0;
for (const line of lines) {
// Parse preamp
const preampMatch = line.match(/^Preamp:\s*([+-]?\d+\.?\d*)\s*dB$/i);
if (preampMatch) {
preamp = parseFloat(preampMatch[1]);
continue;
}
// Parse filter lines (handle "Filter:" and "Filter X:" formats)
const filterMatch = line.match(
/^Filter\s*\d*:\s*ON\s+(\w+)\s+Fc\s+(\d+)\s+Hz\s+Gain\s*([+-]?\d+\.?\d*)\s*dB\s+Q\s+(\d+\.?\d*)/i
);
if (filterMatch) {
const type = filterMatch[1].toUpperCase();
const freq = parseInt(filterMatch[2], 10);
const gain = parseFloat(filterMatch[3]);
const q = parseFloat(filterMatch[4]);
filters.push({ type, freq, gain, q });
}
}
if (filters.length === 0) {
console.warn('[AudioContext] No valid filters found in import text');
return false;
}
// Apply preamp
this.setPreamp(preamp);
// If different number of bands, adjust
if (filters.length !== this.bandCount) {
const newCount = Math.max(
equalizerSettings.MIN_BANDS,
Math.min(equalizerSettings.MAX_BANDS, filters.length)
);
this.setBandCount(newCount);
}
// Extract gains from filters
const gains = filters.slice(0, this.bandCount).map((f) => f.gain);
this.setAllGains(gains);
// Store filter frequencies if different
const newFreqs = filters.slice(0, this.bandCount).map((f) => f.freq);
if (JSON.stringify(newFreqs) !== JSON.stringify(this.frequencies)) {
equalizerSettings.setFreqRange(newFreqs[0], newFreqs[newFreqs.length - 1]);
}
return true;
} catch (e) {
console.warn('[AudioContext] Failed to import EQ settings:', e);
return false;
}
} }
} }

View file

@ -174,6 +174,9 @@ export class Equalizer {
// Store current gains // Store current gains
this.currentGains = new Array(this.bandCount).fill(0); this.currentGains = new Array(this.bandCount).fill(0);
// Store current preamp value
this.preamp = 0;
// Load saved settings // Load saved settings
this._loadSettings(); this._loadSettings();
} }
@ -290,6 +293,10 @@ export class Equalizer {
this.inputNode = this.audioContext.createGain(); this.inputNode = this.audioContext.createGain();
this.outputNode = this.audioContext.createGain(); this.outputNode = this.audioContext.createGain();
// Create preamp gain node
this.preampNode = this.audioContext.createGain();
this._updatePreampGain();
// Connect the filter chain // Connect the filter chain
this._connectFilters(); this._connectFilters();
@ -325,6 +332,11 @@ export class Equalizer {
_connectFilters() { _connectFilters() {
if (!this.filters.length) return; if (!this.filters.length) return;
// Connect preamp to first filter
if (this.preampNode) {
this.preampNode.connect(this.filters[0]);
}
// Chain filters together // Chain filters together
for (let i = 0; i < this.filters.length - 1; i++) { for (let i = 0; i < this.filters.length - 1; i++) {
this.filters[i].connect(this.filters[i + 1]); this.filters[i].connect(this.filters[i + 1]);
@ -356,7 +368,7 @@ export class Equalizer {
* Get the input node for external connection * Get the input node for external connection
*/ */
getInputNode() { getInputNode() {
return this.filters[0] || null; return this.preampNode || this.filters[0] || null;
} }
/** /**
@ -530,6 +542,38 @@ export class Equalizer {
this.frequencies = generateFrequencies(this.bandCount, this.freqRange.min, this.freqRange.max); this.frequencies = generateFrequencies(this.bandCount, this.freqRange.min, this.freqRange.max);
this.frequencyLabels = generateFrequencyLabels(this.frequencies); this.frequencyLabels = generateFrequencyLabels(this.frequencies);
this.currentGains = equalizerSettings.getGains(this.bandCount); this.currentGains = equalizerSettings.getGains(this.bandCount);
this.preamp = equalizerSettings.getPreamp();
}
/**
* Update preamp gain value
* @private
*/
_updatePreampGain() {
if (this.preampNode && this.audioContext) {
const gainValue = Math.pow(10, this.preamp / 20);
const now = this.audioContext.currentTime;
this.preampNode.gain.setTargetAtTime(gainValue, now, 0.01);
}
}
/**
* Set preamp value in dB
* @param {number} db - Preamp value in dB (-20 to +20)
*/
setPreamp(db) {
const clampedDb = Math.max(-20, Math.min(20, parseFloat(db) || 0));
this.preamp = clampedDb;
equalizerSettings.setPreamp(clampedDb);
this._updatePreampGain();
}
/**
* Get current preamp value
* @returns {number} Current preamp value in dB
*/
getPreamp() {
return this.preamp;
} }
/** /**
@ -554,12 +598,104 @@ export class Equalizer {
} catch { } catch {
/* ignore */ /* ignore */
} }
try {
this.preampNode?.disconnect();
} catch {
/* ignore */
}
this.filters = []; this.filters = [];
this.inputNode = null; this.inputNode = null;
this.outputNode = null; this.outputNode = null;
this.preampNode = null;
this.isInitialized = false; this.isInitialized = false;
} }
/**
* Export equalizer settings to text format
* @returns {string} Exported settings in text format
*/
exportToText() {
const lines = [];
lines.push(`Preamp: ${this.preamp.toFixed(1)} dB`);
this.frequencies.forEach((freq, index) => {
const gain = this.currentGains[index] || 0;
const filterNum = index + 1;
lines.push(`Filter ${filterNum}: ON PK Fc ${freq} Hz Gain ${gain.toFixed(1)} dB Q 0.71`);
});
return lines.join('\n');
}
/**
* Import equalizer settings from text format
* @param {string} text - Text format settings
* @returns {boolean} True if import was successful
*/
importFromText(text) {
try {
const lines = text
.split('\n')
.map((line) => line.trim())
.filter((line) => line);
const filters = [];
let preamp = 0;
for (const line of lines) {
// Parse preamp
const preampMatch = line.match(/^Preamp:\s*([+-]?\d+\.?\d*)\s*dB$/i);
if (preampMatch) {
preamp = parseFloat(preampMatch[1]);
continue;
}
// Parse filter lines (handle "Filter:" and "Filter X:" formats)
const filterMatch = line.match(
/^Filter\s*\d*:\s*ON\s+(\w+)\s+Fc\s+(\d+)\s+Hz\s+Gain\s*([+-]?\d+\.?\d*)\s*dB\s+Q\s+(\d+\.?\d*)/i
);
if (filterMatch) {
const type = filterMatch[1].toUpperCase();
const freq = parseInt(filterMatch[2], 10);
const gain = parseFloat(filterMatch[3]);
const q = parseFloat(filterMatch[4]);
filters.push({ type, freq, gain, q });
}
}
if (filters.length === 0) {
console.warn('[Equalizer] No valid filters found in import text');
return false;
}
// Apply preamp
this.setPreamp(preamp);
// If different number of bands, adjust
if (filters.length !== this.bandCount) {
const newCount = Math.max(
equalizerSettings.MIN_BANDS,
Math.min(equalizerSettings.MAX_BANDS, filters.length)
);
this.setBandCount(newCount);
}
// Extract gains from filters
const gains = filters.slice(0, this.bandCount).map((f) => f.gain);
this.setAllGains(gains);
// Store filter frequencies if different
const newFreqs = filters.slice(0, this.bandCount).map((f) => f.freq);
if (JSON.stringify(newFreqs) !== JSON.stringify(this.frequencies)) {
equalizerSettings.setFreqRange(newFreqs[0], newFreqs[newFreqs.length - 1]);
}
return true;
} catch (e) {
console.warn('[Equalizer] Failed to import settings:', e);
return false;
}
}
} }
// Export singleton instance // Export singleton instance

View file

@ -886,11 +886,17 @@ export function initializeSettings(scrobbler, player, api, ui) {
const resetEqFreqBtn = document.getElementById('reset-eq-freq-btn'); const resetEqFreqBtn = document.getElementById('reset-eq-freq-btn');
const resetEqRangeBtn = document.getElementById('reset-eq-range-btn'); const resetEqRangeBtn = document.getElementById('reset-eq-range-btn');
const eqScaleContainer = document.querySelector('.equalizer-scale'); const eqScaleContainer = document.querySelector('.equalizer-scale');
const eqPreampSlider = document.getElementById('eq-preamp-slider');
const eqPreampInput = document.getElementById('eq-preamp-input');
const eqExportBtn = document.getElementById('eq-export-btn');
const eqImportBtn = document.getElementById('eq-import-btn');
const eqImportFile = document.getElementById('eq-import-file');
// Current settings // Current settings
let currentBandCount = equalizerSettings.getBandCount(); let currentBandCount = equalizerSettings.getBandCount();
let currentRange = equalizerSettings.getRange(); let currentRange = equalizerSettings.getRange();
let currentFreqRange = equalizerSettings.getFreqRange(); let currentFreqRange = equalizerSettings.getFreqRange();
let currentPreamp = equalizerSettings.getPreamp();
/** /**
* Generate frequency labels for given band count and frequency range * Generate frequency labels for given band count and frequency range
@ -1004,6 +1010,9 @@ export function initializeSettings(scrobbler, player, api, ui) {
updateBandValueDisplay(bandEl, gains[index]); updateBandValueDisplay(bandEl, gains[index]);
} }
}); });
// Redraw the EQ curve after updating all bands
drawEQCurve();
}; };
/** /**
@ -1012,6 +1021,10 @@ export function initializeSettings(scrobbler, player, api, ui) {
const updateEQContainerVisibility = (enabled) => { const updateEQContainerVisibility = (enabled) => {
if (eqContainer) { if (eqContainer) {
eqContainer.style.display = enabled ? 'block' : 'none'; eqContainer.style.display = enabled ? 'block' : 'none';
if (enabled) {
// Redraw curve when container becomes visible
requestAnimationFrame(drawEQCurve);
}
} }
}; };
@ -1060,6 +1073,152 @@ export function initializeSettings(scrobbler, player, api, ui) {
deleteCustomPresetBtn.style.display = isCustom ? 'flex' : 'none'; deleteCustomPresetBtn.style.display = isCustom ? 'flex' : 'none';
}; };
/**
* Draw smooth EQ response curve on canvas
*/
const drawEQCurve = () => {
const canvas = document.getElementById('eq-response-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
// Skip if canvas has no size (not visible yet)
if (rect.width === 0 || rect.height === 0) return;
// Set canvas size accounting for DPR
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
const width = rect.width;
const height = rect.height;
// Clear canvas
ctx.clearRect(0, 0, width, height);
// Get all current gain values
const eqBands = eqBandsContainer?.querySelectorAll('.eq-band');
if (!eqBands || eqBands.length === 0) return;
// Get the actual highlight color from CSS
const tempEl = document.createElement('div');
tempEl.style.color = 'rgb(var(--highlight-rgb))';
document.body.appendChild(tempEl);
const highlightColor = getComputedStyle(tempEl).color;
document.body.removeChild(tempEl);
const gains = [];
const positions = [];
const range = currentRange;
const rangeTotal = range.max - range.min;
const canvasRect = canvas.getBoundingClientRect();
eqBands.forEach((bandEl) => {
const slider = bandEl.querySelector('.eq-slider');
const gain = slider ? parseFloat(slider.value) : 0;
gains.push(gain);
// Get actual center position of the band element relative to canvas
const bandRect = bandEl.getBoundingClientRect();
const x = bandRect.left + bandRect.width / 2 - canvasRect.left;
positions.push(x);
});
// Calculate y positions - account for slider thumb size (18px)
// The track is 120px, but thumb center moves within (120 - 18) = 102px range
const trackHeight = height;
const thumbSize = 18;
const usableTrack = trackHeight - thumbSize;
const trackOffset = thumbSize / 2;
const getY = (gain) => {
const normalized = (gain - range.min) / rangeTotal;
// Invert because canvas Y=0 is at top, slider max is at top
return trackOffset + (1 - normalized) * usableTrack;
};
// Create points array
const points = gains.map((gain, i) => ({
x: positions[i],
y: getY(gain),
}));
if (points.length < 2) return;
// Parse RGB values from color string
const rgbMatch = highlightColor.match(/\d+/g);
const r = rgbMatch ? parseInt(rgbMatch[0]) : 128;
const g = rgbMatch ? parseInt(rgbMatch[1]) : 128;
const b = rgbMatch ? parseInt(rgbMatch[2]) : 128;
// Calculate control points for smooth curve
const getControlPoints = (i) => {
const p0 = points[i === 0 ? i : i - 1];
const p1 = points[i];
const p2 = points[i + 1];
const p3 = points[i + 2] || p2;
const cp1x = p1.x + (p2.x - p0.x) / 6;
const cp1y = p1.y + (p2.y - p0.y) / 6;
const cp2x = p2.x - (p3.x - p1.x) / 6;
const cp2y = p2.y - (p3.y - p1.y) / 6;
return { cp1x, cp1y, cp2x, cp2y };
};
// Draw filled area from curve to bottom
const gradient = ctx.createLinearGradient(0, 0, 0, height);
gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, 0.3)`);
gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0.05)`);
ctx.beginPath();
ctx.moveTo(points[0].x, height);
ctx.lineTo(points[0].x, points[0].y);
for (let i = 0; i < points.length - 1; i++) {
const { cp1x, cp1y, cp2x, cp2y } = getControlPoints(i);
const p2 = points[i + 1];
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, p2.x, p2.y);
}
ctx.lineTo(points[points.length - 1].x, height);
ctx.closePath();
ctx.fillStyle = gradient;
ctx.fill();
// Draw the curve line
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (let i = 0; i < points.length - 1; i++) {
const { cp1x, cp1y, cp2x, cp2y } = getControlPoints(i);
const p2 = points[i + 1];
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, p2.x, p2.y);
}
ctx.strokeStyle = `rgb(${r}, ${g}, ${b})`;
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.stroke();
// Draw dots at each band point
points.forEach((point) => {
ctx.beginPath();
ctx.arc(point.x, point.y, 4, 0, Math.PI * 2);
ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
ctx.fill();
// Add white center to dots for visibility
ctx.beginPath();
ctx.arc(point.x, point.y, 2, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
ctx.fill();
});
};
/** /**
* Initialize band slider event listeners * Initialize band slider event listeners
*/ */
@ -1084,6 +1243,7 @@ export function initializeSettings(scrobbler, player, api, ui) {
const gain = parseFloat(e.target.value); const gain = parseFloat(e.target.value);
audioContextManager.setBandGain(bandIndex, gain); audioContextManager.setBandGain(bandIndex, gain);
updateBandValueDisplay(bandEl, gain); updateBandValueDisplay(bandEl, gain);
drawEQCurve();
// When manually adjusting, check if we should clear preset // When manually adjusting, check if we should clear preset
if (eqPresetSelect && eqPresetSelect.value !== 'flat') { if (eqPresetSelect && eqPresetSelect.value !== 'flat') {
@ -1104,9 +1264,15 @@ export function initializeSettings(scrobbler, player, api, ui) {
slider.value = 0; slider.value = 0;
audioContextManager.setBandGain(bandIndex, 0); audioContextManager.setBandGain(bandIndex, 0);
updateBandValueDisplay(bandEl, 0); updateBandValueDisplay(bandEl, 0);
drawEQCurve();
}); });
} }
}); });
// Initial curve draw with delay to ensure canvas has proper dimensions
setTimeout(() => {
drawEQCurve();
}, 100);
}; };
// Initialize EQ toggle // Initialize EQ toggle
@ -1119,6 +1285,13 @@ export function initializeSettings(scrobbler, player, api, ui) {
const enabled = e.target.checked; const enabled = e.target.checked;
audioContextManager.toggleEQ(enabled); audioContextManager.toggleEQ(enabled);
updateEQContainerVisibility(enabled); updateEQContainerVisibility(enabled);
// Redraw curve after a brief delay to allow container to become visible
if (enabled) {
setTimeout(() => {
drawEQCurve();
}, 50);
}
}); });
} }
@ -1131,9 +1304,9 @@ export function initializeSettings(scrobbler, player, api, ui) {
if (newCount >= equalizerSettings.MIN_BANDS && newCount <= equalizerSettings.MAX_BANDS) { if (newCount >= equalizerSettings.MIN_BANDS && newCount <= equalizerSettings.MAX_BANDS) {
currentBandCount = newCount; currentBandCount = newCount;
// Save new band count and update audio context // Save new band count and update audio context (interpolates gains automatically)
equalizerSettings.setBandCount(newCount); equalizerSettings.setBandCount(newCount);
audioContextManager.setBandCount?.(newCount) || audioContextManager.reinitialize?.(); audioContextManager.setBandCount?.(newCount);
// Regenerate UI // Regenerate UI
generateEQBands( generateEQBands(
@ -1144,16 +1317,25 @@ export function initializeSettings(scrobbler, player, api, ui) {
currentFreqRange.max currentFreqRange.max
); );
// Reset to flat and apply // Get interpolated gains from audio context
const flatGains = new Array(newCount).fill(0); const interpolatedGains = audioContextManager.getGains?.() || equalizerSettings.getGains(newCount);
audioContextManager.setAllGains(flatGains); updateAllBandUI(interpolatedGains);
updateAllBandUI(flatGains);
// Keep current preset or set to custom if modified
if (eqPresetSelect) { if (eqPresetSelect) {
eqPresetSelect.value = 'flat'; const currentPreset = eqPresetSelect.value;
equalizerSettings.setPreset('flat'); if (!currentPreset.startsWith('custom_')) {
eqPresetSelect.value = 'custom';
}
} }
updateDeleteButtonVisibility(); updateDeleteButtonVisibility();
// Show brief feedback
const originalText = eqBandCountInput.style.backgroundColor;
eqBandCountInput.style.backgroundColor = 'var(--highlight)';
setTimeout(() => {
eqBandCountInput.style.backgroundColor = originalText;
}, 300);
} }
}); });
} }
@ -1490,6 +1672,125 @@ export function initializeSettings(scrobbler, player, api, ui) {
}); });
} }
// Initialize preamp control
const updatePreampUI = (value) => {
currentPreamp = value;
if (eqPreampSlider) eqPreampSlider.value = value;
if (eqPreampInput) eqPreampInput.value = value;
audioContextManager.setPreamp?.(value);
};
if (eqPreampSlider) {
// Set initial value
eqPreampSlider.value = currentPreamp;
// Handle slider input
eqPreampSlider.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
updatePreampUI(value);
});
}
if (eqPreampInput) {
// Set initial value
eqPreampInput.value = currentPreamp;
// Handle text input
eqPreampInput.addEventListener('change', (e) => {
let value = parseFloat(e.target.value);
// Clamp to valid range
value = Math.max(-20, Math.min(20, value || 0));
updatePreampUI(value);
});
// Handle enter key
eqPreampInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
e.target.blur();
}
});
}
// Initialize import/export controls
if (eqExportBtn) {
eqExportBtn.addEventListener('click', () => {
const text = audioContextManager.exportEQToText?.();
if (text) {
navigator.clipboard
.writeText(text)
.then(() => {
eqExportBtn.textContent = 'Copied!';
setTimeout(() => {
eqExportBtn.textContent = 'Export';
}, 1500);
})
.catch(() => {
// Fallback: create and download file
const blob = new Blob([text], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'equalizer-settings.txt';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
}
});
}
if (eqImportBtn && eqImportFile) {
eqImportBtn.addEventListener('click', () => {
eqImportFile.click();
});
eqImportFile.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
const text = event.target.result;
const success = audioContextManager.importEQFromText?.(text);
if (success) {
// Update UI
currentPreamp = equalizerSettings.getPreamp();
updatePreampUI(currentPreamp);
// Update band count if changed
currentBandCount = equalizerSettings.getBandCount();
if (eqBandCountInput) eqBandCountInput.value = currentBandCount;
// Regenerate bands and update UI
generateEQBands(
currentBandCount,
currentRange.min,
currentRange.max,
currentFreqRange.min,
currentFreqRange.max
);
const gains = audioContextManager.getGains?.() || equalizerSettings.getGains(currentBandCount);
updateAllBandUI(gains);
eqImportBtn.textContent = 'Imported!';
setTimeout(() => {
eqImportBtn.textContent = 'Import';
}, 1500);
} else {
eqImportBtn.textContent = 'Invalid!';
setTimeout(() => {
eqImportBtn.textContent = 'Import';
}, 1500);
}
};
reader.readAsText(file);
// Reset file input
e.target.value = '';
});
}
// Generate initial EQ bands with current ranges // Generate initial EQ bands with current ranges
generateEQBands(currentBandCount, currentRange.min, currentRange.max, currentFreqRange.min, currentFreqRange.max); generateEQBands(currentBandCount, currentRange.min, currentRange.max, currentFreqRange.min, currentFreqRange.max);
@ -1524,6 +1825,22 @@ export function initializeSettings(scrobbler, player, api, ui) {
} }
}); });
// Redraw EQ curve on window resize
window.addEventListener('resize', () => {
requestAnimationFrame(drawEQCurve);
});
// Redraw EQ curve when a new track loads (audio metadata loaded)
const audioPlayer = document.getElementById('audio-player');
if (audioPlayer) {
audioPlayer.addEventListener('loadedmetadata', () => {
// Small delay to ensure the visualizer and EQ are fully ready
setTimeout(() => {
drawEQCurve();
}, 100);
});
}
// Now Playing Mode // Now Playing Mode
const nowPlayingMode = document.getElementById('now-playing-mode'); const nowPlayingMode = document.getElementById('now-playing-mode');
if (nowPlayingMode) { if (nowPlayingMode) {

View file

@ -797,6 +797,7 @@ export const equalizerSettings = {
RANGE_MAX_KEY: 'equalizer-range-max', RANGE_MAX_KEY: 'equalizer-range-max',
FREQ_MIN_KEY: 'equalizer-freq-min', FREQ_MIN_KEY: 'equalizer-freq-min',
FREQ_MAX_KEY: 'equalizer-freq-max', FREQ_MAX_KEY: 'equalizer-freq-max',
PREAMP_KEY: 'equalizer-preamp',
DEFAULT_BAND_COUNT: 16, DEFAULT_BAND_COUNT: 16,
MIN_BANDS: 3, MIN_BANDS: 3,
MAX_BANDS: 32, MAX_BANDS: 32,
@ -808,6 +809,9 @@ export const equalizerSettings = {
DEFAULT_FREQ_MAX: 20000, DEFAULT_FREQ_MAX: 20000,
ABSOLUTE_FREQ_MIN: 10, ABSOLUTE_FREQ_MIN: 10,
ABSOLUTE_FREQ_MAX: 96000, ABSOLUTE_FREQ_MAX: 96000,
DEFAULT_PREAMP: 0,
PREAMP_MIN: -20,
PREAMP_MAX: 20,
isEnabled() { isEnabled() {
try { try {
@ -967,6 +971,30 @@ export const equalizerSettings = {
return validMin && validMax; return validMin && validMax;
}, },
getPreamp() {
try {
const stored = localStorage.getItem(this.PREAMP_KEY);
if (stored) {
const val = parseFloat(stored);
if (!isNaN(val) && val >= this.PREAMP_MIN && val <= this.PREAMP_MAX) {
return val;
}
}
} catch {
/* ignore */
}
return this.DEFAULT_PREAMP;
},
setPreamp(value) {
const val = parseFloat(value);
if (!isNaN(val) && val >= this.PREAMP_MIN && val <= this.PREAMP_MAX) {
localStorage.setItem(this.PREAMP_KEY, val.toString());
return true;
}
return false;
},
getGains(bandCount) { getGains(bandCount) {
const count = bandCount || this.getBandCount(); const count = bandCount || this.getBandCount();
try { try {

View file

@ -6563,7 +6563,7 @@ textarea:focus {
.equalizer-preset-row { .equalizer-preset-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--spacing-md); gap: var(--spacing-sm);
flex-wrap: wrap; flex-wrap: wrap;
} }
@ -6857,6 +6857,72 @@ textarea:focus {
margin-left: var(--spacing-xs); margin-left: var(--spacing-xs);
} }
/* EQ Preamp Controls */
.eq-preamp-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-preamp-controls label {
font-size: 0.9rem;
color: var(--muted-foreground);
font-weight: 500;
}
#eq-preamp-slider {
flex: 1;
min-width: 120px;
max-width: 200px;
height: 4px;
appearance: none;
background: var(--border);
border-radius: 2px;
outline: none;
cursor: pointer;
}
#eq-preamp-slider::-webkit-slider-thumb {
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--primary);
cursor: pointer;
transition: transform var(--transition-fast);
}
#eq-preamp-slider::-webkit-slider-thumb:hover {
transform: scale(1.2);
}
#eq-preamp-slider::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--primary);
cursor: pointer;
border: none;
transition: transform var(--transition-fast);
}
#eq-preamp-slider::-moz-range-thumb:hover {
transform: scale(1.2);
}
/* EQ Import/Export Buttons (now inline) */
#eq-export-btn,
#eq-import-btn {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
white-space: nowrap;
gap: var(--spacing-xs);
}
/* Equalizer preset dropdown styling */ /* Equalizer preset dropdown styling */
.equalizer-preset-row select optgroup { .equalizer-preset-row select optgroup {
font-weight: 600; font-weight: 600;
@ -6869,11 +6935,15 @@ textarea:focus {
padding-left: var(--spacing-sm); padding-left: var(--spacing-sm);
} }
.equalizer-bands-wrapper {
position: relative;
padding: var(--spacing-md) 0;
}
.equalizer-bands { .equalizer-bands {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
gap: 4px; gap: 4px;
padding: var(--spacing-md) 0;
position: relative; position: relative;
} }
@ -6883,7 +6953,7 @@ textarea:focus {
position: absolute; position: absolute;
left: 0; left: 0;
right: 0; right: 0;
top: calc(var(--spacing-md) + 60px); top: 60px;
height: 1px; height: 1px;
background: var(--border); background: var(--border);
opacity: 0.5; opacity: 0.5;
@ -6891,6 +6961,18 @@ textarea:focus {
z-index: 0; z-index: 0;
} }
/* EQ Response Curve Canvas */
.eq-response-canvas {
position: absolute;
top: var(--spacing-md);
left: 4px;
width: calc(100% - 8px);
height: 120px;
pointer-events: none;
z-index: 2;
display: block;
}
.eq-band { .eq-band {
display: flex; display: flex;
flex-direction: column; flex-direction: column;