fix: address all CodeRabbit review findings from PR #477
Engine & algorithm: - Use default shelf Q (1/√2) in calculateBiquadResponse for shelf filters - Compute normalization offset on measurement frequency grid to avoid bias - Try stale cache for all fetch errors in autoeq-importer, not just timeouts Audio pipeline: - Pass postProcessingQuality (preserves Dolby Atmos override) in api.js - Persist custom band frequencies in equalizerSettings storage - Restore custom frequencies on _loadSettings instead of regenerating defaults - Export clamped preamp value in applyAutoEQBands text output - Propagate filter type and Q values through equalizer import chain - Update freqRange after importing custom filter frequencies - Remove return in finally block that hid LOSSLESS fallback failures Data consistency: - Normalize artist IDs with String() in blockArtist/unblockArtist Lint & code quality: - Annotate empty catch blocks (Atmos codec probes) - Remove unused catch parameters Accessibility: - Add aria-label and for attributes to all AutoEQ form controls - Add role="status" aria-live="polite" to feedback spans - Update filter type documentation to reflect shelf support - Hide parametric-only sections by default to match active tab UI: - Move AutoEq button directly under graph - Hide shared button in Parametric/Speaker modes - Replace hardcoded white legend dot with theme-adaptive color-mix - Add pointer-events:none and focus-within to profile delete button
This commit is contained in:
parent
77f9e10fdc
commit
782e98061b
10 changed files with 149 additions and 50 deletions
34
index.html
34
index.html
|
|
@ -4046,8 +4046,8 @@
|
||||||
<h4>Parametric EQ — Manual Control</h4>
|
<h4>Parametric EQ — Manual Control</h4>
|
||||||
<ol>
|
<ol>
|
||||||
<li>
|
<li>
|
||||||
Each band is a <b>peaking filter</b> with frequency, gain, and Q
|
Each band supports <b>peaking, low-shelf, and high-shelf</b> filter
|
||||||
(width).
|
types with frequency, gain, and Q (width).
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<b>Drag nodes</b> on the graph to adjust frequency and gain
|
<b>Drag nodes</b> on the graph to adjust frequency and gain
|
||||||
|
|
@ -4162,6 +4162,7 @@
|
||||||
<span class="slider"></span>
|
<span class="slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<button id="autoeq-run-btn" class="autoeq-run-btn" disabled>AutoEq</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Database Browser -->
|
<!-- Database Browser -->
|
||||||
|
|
@ -4180,6 +4181,7 @@
|
||||||
id="autoeq-headphone-search"
|
id="autoeq-headphone-search"
|
||||||
placeholder="Search model (e.g. HD 600)..."
|
placeholder="Search model (e.g. HD 600)..."
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
aria-label="Search headphone model"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="autoeq-database-content">
|
<div class="autoeq-database-content">
|
||||||
|
|
@ -4194,11 +4196,10 @@
|
||||||
|
|
||||||
<!-- AutoEQ Controls -->
|
<!-- AutoEQ Controls -->
|
||||||
<div class="autoeq-controls-section">
|
<div class="autoeq-controls-section">
|
||||||
<button id="autoeq-run-btn" class="autoeq-run-btn" disabled>AutoEq</button>
|
|
||||||
<div class="autoeq-control-group">
|
<div class="autoeq-control-group">
|
||||||
<label class="autoeq-control-label">HEADPHONE MODEL</label>
|
<label class="autoeq-control-label" for="autoeq-headphone-select">HEADPHONE MODEL</label>
|
||||||
<div class="autoeq-select-wrapper">
|
<div class="autoeq-select-wrapper">
|
||||||
<select id="autoeq-headphone-select">
|
<select id="autoeq-headphone-select" aria-label="Headphone model">
|
||||||
<option value="">Select a headphone...</option>
|
<option value="">Select a headphone...</option>
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
|
|
@ -4257,8 +4258,8 @@
|
||||||
|
|
||||||
<div class="autoeq-controls-row">
|
<div class="autoeq-controls-row">
|
||||||
<div class="autoeq-control-mini">
|
<div class="autoeq-control-mini">
|
||||||
<label class="autoeq-control-label">FILTER BANDS</label>
|
<label class="autoeq-control-label" for="autoeq-band-count">FILTER BANDS</label>
|
||||||
<select id="autoeq-band-count">
|
<select id="autoeq-band-count" aria-label="Filter bands">
|
||||||
<option value="5">5</option>
|
<option value="5">5</option>
|
||||||
<option value="10" selected>10</option>
|
<option value="10" selected>10</option>
|
||||||
<option value="15">15</option>
|
<option value="15">15</option>
|
||||||
|
|
@ -4267,8 +4268,8 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="autoeq-control-mini">
|
<div class="autoeq-control-mini">
|
||||||
<label class="autoeq-control-label">MAX HZ</label>
|
<label class="autoeq-control-label" for="autoeq-max-freq">MAX HZ</label>
|
||||||
<select id="autoeq-max-freq">
|
<select id="autoeq-max-freq" aria-label="Maximum frequency">
|
||||||
<option value="6000">6k</option>
|
<option value="6000">6k</option>
|
||||||
<option value="8000">8k</option>
|
<option value="8000">8k</option>
|
||||||
<option value="10000">10k</option>
|
<option value="10000">10k</option>
|
||||||
|
|
@ -4278,8 +4279,8 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="autoeq-control-mini">
|
<div class="autoeq-control-mini">
|
||||||
<label class="autoeq-control-label">SAMPLE RATE</label>
|
<label class="autoeq-control-label" for="autoeq-sample-rate">SAMPLE RATE</label>
|
||||||
<select id="autoeq-sample-rate">
|
<select id="autoeq-sample-rate" aria-label="Sample rate">
|
||||||
<option value="44100">44.1k</option>
|
<option value="44100">44.1k</option>
|
||||||
<option value="48000" selected>48k</option>
|
<option value="48000" selected>48k</option>
|
||||||
<option value="96000">96k</option>
|
<option value="96000">96k</option>
|
||||||
|
|
@ -4297,7 +4298,7 @@
|
||||||
<use svg="!lucide/download.svg" size="20" />
|
<use svg="!lucide/download.svg" size="20" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<span id="autoeq-status" class="autoeq-status"></span>
|
<span id="autoeq-status" class="autoeq-status" role="status" aria-live="polite"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Saved Profiles -->
|
<!-- Saved Profiles -->
|
||||||
|
|
@ -4313,6 +4314,7 @@
|
||||||
id="autoeq-profile-name"
|
id="autoeq-profile-name"
|
||||||
class="autoeq-profile-name-input"
|
class="autoeq-profile-name-input"
|
||||||
placeholder="Profile name..."
|
placeholder="Profile name..."
|
||||||
|
aria-label="AutoEQ profile name"
|
||||||
maxlength="50"
|
maxlength="50"
|
||||||
/>
|
/>
|
||||||
<button id="autoeq-save-btn" class="btn-primary autoeq-save-btn">
|
<button id="autoeq-save-btn" class="btn-primary autoeq-save-btn">
|
||||||
|
|
@ -4524,7 +4526,7 @@
|
||||||
>
|
>
|
||||||
Measure All
|
Measure All
|
||||||
</button>
|
</button>
|
||||||
<span id="speaker-eq-status" class="autoeq-status"></span>
|
<span id="speaker-eq-status" class="autoeq-status" role="status" aria-live="polite"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -4542,6 +4544,7 @@
|
||||||
id="speaker-profile-name"
|
id="speaker-profile-name"
|
||||||
class="autoeq-profile-name-input"
|
class="autoeq-profile-name-input"
|
||||||
placeholder="Profile name..."
|
placeholder="Profile name..."
|
||||||
|
aria-label="Speaker EQ profile name"
|
||||||
maxlength="50"
|
maxlength="50"
|
||||||
/>
|
/>
|
||||||
<button id="speaker-save-btn" class="btn-primary autoeq-save-btn">
|
<button id="speaker-save-btn" class="btn-primary autoeq-save-btn">
|
||||||
|
|
@ -4577,7 +4580,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="autoeq-filters-content" id="autoeq-filters-content">
|
<div class="autoeq-filters-content" id="autoeq-filters-content">
|
||||||
<!-- Preset Selector (visible in parametric mode) -->
|
<!-- Preset Selector (visible in parametric mode) -->
|
||||||
<div class="autoeq-preset-row" id="autoeq-preset-row">
|
<div class="autoeq-preset-row" id="autoeq-preset-row" style="display: none">
|
||||||
<div class="autoeq-control-group">
|
<div class="autoeq-control-group">
|
||||||
<label class="autoeq-control-label">PRESET</label>
|
<label class="autoeq-control-label">PRESET</label>
|
||||||
<select id="parametric-preset-select">
|
<select id="parametric-preset-select">
|
||||||
|
|
@ -4603,7 +4606,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Saved Profiles for Parametric EQ -->
|
<!-- Saved Profiles for Parametric EQ -->
|
||||||
<div class="autoeq-parametric-profiles" id="autoeq-parametric-profiles">
|
<div class="autoeq-parametric-profiles" id="autoeq-parametric-profiles" style="display: none">
|
||||||
<div class="autoeq-saved-header">
|
<div class="autoeq-saved-header">
|
||||||
<div class="autoeq-saved-header-left">
|
<div class="autoeq-saved-header-left">
|
||||||
<span class="autoeq-saved-title">SAVED PROFILES</span>
|
<span class="autoeq-saved-title">SAVED PROFILES</span>
|
||||||
|
|
@ -4617,6 +4620,7 @@
|
||||||
id="parametric-profile-name"
|
id="parametric-profile-name"
|
||||||
class="autoeq-profile-name-input"
|
class="autoeq-profile-name-input"
|
||||||
placeholder="Profile name..."
|
placeholder="Profile name..."
|
||||||
|
aria-label="Parametric EQ profile name"
|
||||||
maxlength="50"
|
maxlength="50"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -1500,7 +1500,9 @@ export class LosslessAPI {
|
||||||
a.canPlayType('audio/mp4; codecs="ec-3"') || a.canPlayType('audio/mp4; codecs="eac3"')
|
a.canPlayType('audio/mp4; codecs="ec-3"') || a.canPlayType('audio/mp4; codecs="eac3"')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch {
|
||||||
|
// Atmos codec probe — intentionally swallowed; canPlayAtmos stays false
|
||||||
|
}
|
||||||
|
|
||||||
const paramsArray = [];
|
const paramsArray = [];
|
||||||
|
|
||||||
|
|
@ -1888,7 +1890,7 @@ export class LosslessAPI {
|
||||||
quality,
|
quality,
|
||||||
onProgress,
|
onProgress,
|
||||||
options.signal,
|
options.signal,
|
||||||
lookup.info?.audioQuality ?? null
|
postProcessingQuality
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -728,7 +728,8 @@ class AudioContextManager {
|
||||||
this.isEQEnabled = equalizerSettings.isEnabled();
|
this.isEQEnabled = equalizerSettings.isEnabled();
|
||||||
this.bandCount = equalizerSettings.getBandCount();
|
this.bandCount = equalizerSettings.getBandCount();
|
||||||
this.freqRange = equalizerSettings.getFreqRange();
|
this.freqRange = equalizerSettings.getFreqRange();
|
||||||
this.frequencies = generateFrequencies(this.bandCount, this.freqRange.min, this.freqRange.max);
|
const customFreqs = equalizerSettings.getCustomFrequencies(this.bandCount);
|
||||||
|
this.frequencies = customFreqs || generateFrequencies(this.bandCount, this.freqRange.min, this.freqRange.max);
|
||||||
this.currentGains = equalizerSettings.getGains(this.bandCount);
|
this.currentGains = equalizerSettings.getGains(this.bandCount);
|
||||||
this.currentTypes = equalizerSettings.getBandTypes(this.bandCount);
|
this.currentTypes = equalizerSettings.getBandTypes(this.bandCount);
|
||||||
this.currentQs = equalizerSettings.getBandQs(this.bandCount);
|
this.currentQs = equalizerSettings.getBandQs(this.bandCount);
|
||||||
|
|
@ -827,12 +828,13 @@ class AudioContextManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persist normalized band descriptors to settings store
|
// Persist normalized band descriptors to settings store
|
||||||
|
equalizerSettings.setCustomFrequencies(this.frequencies);
|
||||||
equalizerSettings.setGains(this.currentGains);
|
equalizerSettings.setGains(this.currentGains);
|
||||||
equalizerSettings.setBandTypes(this.currentTypes);
|
equalizerSettings.setBandTypes(this.currentTypes);
|
||||||
equalizerSettings.setBandQs(this.currentQs);
|
equalizerSettings.setBandQs(this.currentQs);
|
||||||
|
|
||||||
// Generate export text using clamped gain values
|
// Generate export text using the actual applied preamp value
|
||||||
const lines = [`Preamp: ${preamp.toFixed(1)} dB`];
|
const lines = [`Preamp: ${this.preamp.toFixed(1)} dB`];
|
||||||
sortedBands.forEach((band, index) => {
|
sortedBands.forEach((band, index) => {
|
||||||
if (index >= count) return;
|
if (index >= count) return;
|
||||||
const filterType = band.type === 'lowshelf' ? 'LS' : band.type === 'highshelf' ? 'HS' : 'PK';
|
const filterType = band.type === 'lowshelf' ? 'LS' : band.type === 'highshelf' ? 'HS' : 'PK';
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,10 @@ function calculateBiquadResponse(f, band, sr = DEFAULT_SR) {
|
||||||
if (!band.type || band.type.length === 0) return 0;
|
if (!band.type || band.type.length === 0) return 0;
|
||||||
const w = (2 * PI * band.freq) / sr;
|
const w = (2 * PI * band.freq) / sr;
|
||||||
const p = (2 * PI * f) / sr;
|
const p = (2 * PI * f) / sr;
|
||||||
const s = Math.sin(w) / (2 * band.q);
|
const t = band.type[0];
|
||||||
|
// WebAudio ignores Q for shelf filters; use 1/√2 (slope = 1) to match
|
||||||
|
const effectiveQ = t === 'l' || t === 'h' ? Math.SQRT1_2 : band.q;
|
||||||
|
const s = Math.sin(w) / (2 * effectiveQ);
|
||||||
const A = Math.pow(DB_BASE, band.gain / DB_DIVISOR);
|
const A = Math.pow(DB_BASE, band.gain / DB_DIVISOR);
|
||||||
const c = Math.cos(w);
|
const c = Math.cos(w);
|
||||||
let b0 = 0,
|
let b0 = 0,
|
||||||
|
|
@ -33,8 +36,6 @@ function calculateBiquadResponse(f, band, sr = DEFAULT_SR) {
|
||||||
a1 = 0,
|
a1 = 0,
|
||||||
a2 = 0;
|
a2 = 0;
|
||||||
|
|
||||||
const t = band.type[0];
|
|
||||||
|
|
||||||
if (t === 'p') {
|
if (t === 'p') {
|
||||||
b0 = 1 + s * A;
|
b0 = 1 + s * A;
|
||||||
b1 = -2 * c;
|
b1 = -2 * c;
|
||||||
|
|
@ -98,19 +99,37 @@ function interpolate(freq, data) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate normalization offset based on midrange average (250-2500 Hz)
|
* Calculate normalization offset based on midrange average (250-2500 Hz)
|
||||||
* @param {Array<{freq: number, gain: number}>} data - Frequency response data
|
* With one argument: returns the midrange average of that curve (for graph centering).
|
||||||
* @returns {number} Average gain in midrange
|
* With two arguments: evaluates both curves on the measurement frequency grid
|
||||||
|
* to avoid sampling-density bias, returning (avgTarget - avgMeasurement).
|
||||||
|
* @param {Array<{freq: number, gain: number}>} measurement - Measurement/data curve
|
||||||
|
* @param {Array<{freq: number, gain: number}>} [target] - Optional target curve
|
||||||
|
* @returns {number} Midrange average, or alignment offset when target is provided
|
||||||
*/
|
*/
|
||||||
function getNormalizationOffset(data) {
|
function getNormalizationOffset(measurement, target) {
|
||||||
let sum = 0,
|
if (!target) {
|
||||||
|
let sum = 0,
|
||||||
|
count = 0;
|
||||||
|
for (const p of measurement) {
|
||||||
|
if (p.freq >= 250 && p.freq <= 2500) {
|
||||||
|
sum += p.gain;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count > 0 ? sum / count : interpolate(1000, measurement);
|
||||||
|
}
|
||||||
|
let sumTarget = 0,
|
||||||
|
sumMeasurement = 0,
|
||||||
count = 0;
|
count = 0;
|
||||||
for (const p of data) {
|
for (const p of measurement) {
|
||||||
if (p.freq >= 250 && p.freq <= 2500) {
|
if (p.freq >= 250 && p.freq <= 2500) {
|
||||||
sum += p.gain;
|
sumTarget += interpolate(p.freq, target);
|
||||||
|
sumMeasurement += p.gain;
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return count > 0 ? sum / count : interpolate(1000, data);
|
if (count > 0) return sumTarget / count - sumMeasurement / count;
|
||||||
|
return interpolate(1000, target) - interpolate(1000, measurement);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -135,7 +154,7 @@ function runAutoEqAlgorithm(
|
||||||
sampleRate = DEFAULT_SR
|
sampleRate = DEFAULT_SR
|
||||||
) {
|
) {
|
||||||
if (minFreq > maxFreq) return [];
|
if (minFreq > maxFreq) return [];
|
||||||
const off = getNormalizationOffset(target) - getNormalizationOffset(measurement);
|
const off = getNormalizationOffset(measurement, target);
|
||||||
let err = measurement.map((p) => ({ freq: p.freq, gain: p.gain + off - interpolate(p.freq, target) }));
|
let err = measurement.map((p) => ({ freq: p.freq, gain: p.gain + off - interpolate(p.freq, target) }));
|
||||||
|
|
||||||
const hasInRangePoints = err.some((p) => p.freq >= minFreq && p.freq <= maxFreq);
|
const hasInRangePoints = err.some((p) => p.freq >= minFreq && p.freq <= maxFreq);
|
||||||
|
|
|
||||||
|
|
@ -211,15 +211,15 @@ async function fetchAutoEqIndex() {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.name === 'AbortError') {
|
if (err.name === 'AbortError') {
|
||||||
console.warn('[AutoEQ] GitHub API request timed out. Falling back to cache or fallback index.');
|
console.warn('[AutoEQ] GitHub API request timed out. Falling back to cache or fallback index.');
|
||||||
try {
|
|
||||||
const cached = await db.getSetting(CACHE_KEY);
|
|
||||||
if (cached?.data) return cached.data;
|
|
||||||
} catch {
|
|
||||||
/* ignore */
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.error('[AutoEQ] Failed to fetch index:', err);
|
console.error('[AutoEQ] Failed to fetch index:', err);
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
const cached = await db.getSetting(CACHE_KEY);
|
||||||
|
if (cached?.data) return cached.data;
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
return FALLBACK_INDEX;
|
return FALLBACK_INDEX;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -696,10 +696,38 @@ export class Equalizer {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract and apply gains
|
// Extract and apply gains, types, and Qs
|
||||||
const gains = sliced.map((f) => f.gain);
|
const gains = sliced.map((f) => f.gain);
|
||||||
this.setAllGains(gains);
|
this.setAllGains(gains);
|
||||||
|
|
||||||
|
// Apply filter types (PK/LS/HS -> peaking/lowshelf/highshelf)
|
||||||
|
const typeMap = { PK: 'peaking', LS: 'lowshelf', HS: 'highshelf', LSC: 'lowshelf', HSC: 'highshelf' };
|
||||||
|
const types = sliced.map((f) => typeMap[f.type] || 'peaking');
|
||||||
|
this.currentTypes = types;
|
||||||
|
if (this.filters.length === types.length) {
|
||||||
|
types.forEach((type, i) => {
|
||||||
|
if (this.filters[i]) this.filters[i].type = type;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
equalizerSettings.setBandTypes(types);
|
||||||
|
|
||||||
|
// Apply Q values
|
||||||
|
const qs = sliced.map((f) => f.q);
|
||||||
|
this.currentQs = qs;
|
||||||
|
if (this.filters.length === qs.length) {
|
||||||
|
qs.forEach((q, i) => {
|
||||||
|
if (this.filters[i]) this.filters[i].Q.value = q;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
equalizerSettings.setBandQs(qs);
|
||||||
|
|
||||||
|
// Persist custom frequencies and update freqRange
|
||||||
|
equalizerSettings.setCustomFrequencies(newFreqs);
|
||||||
|
const minFreq = Math.min(...newFreqs);
|
||||||
|
const maxFreq = Math.max(...newFreqs);
|
||||||
|
this.freqRange = { min: minFreq, max: maxFreq };
|
||||||
|
equalizerSettings.setFreqRange(minFreq, maxFreq);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[Equalizer] Failed to import settings:', e);
|
console.warn('[Equalizer] Failed to import settings:', e);
|
||||||
|
|
|
||||||
14
js/player.js
14
js/player.js
|
|
@ -177,7 +177,9 @@ export class Player {
|
||||||
if (this.video.readyState >= 2 && (this.audio.readyState > 0 || this.audio.src)) {
|
if (this.video.readyState >= 2 && (this.audio.readyState > 0 || this.audio.src)) {
|
||||||
this.audio.currentTime = this.video.currentTime;
|
this.audio.currentTime = this.video.currentTime;
|
||||||
}
|
}
|
||||||
} catch (err) {}
|
} catch {
|
||||||
|
// Video-to-audio time sync may fail if readyState is stale
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const syncedEvent = new Event(eventName, { bubbles: e.bubbles, cancelable: e.cancelable });
|
const syncedEvent = new Event(eventName, { bubbles: e.bubbles, cancelable: e.cancelable });
|
||||||
|
|
@ -1032,12 +1034,12 @@ export class Player {
|
||||||
try {
|
try {
|
||||||
await this.playTrackFromQueue(startTime, recursiveCount, true);
|
await this.playTrackFromQueue(startTime, recursiveCount, true);
|
||||||
return;
|
return;
|
||||||
} catch (retryError) {
|
} catch {
|
||||||
|
// LOSSLESS fallback also failed — fall through to error handling below
|
||||||
} finally {
|
} finally {
|
||||||
this.quality = originalQuality;
|
this.quality = originalQuality;
|
||||||
this.isFallbackRetry = false;
|
this.isFallbackRetry = false;
|
||||||
this.isFallbackInProgress = false;
|
this.isFallbackInProgress = false;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1746,7 +1748,9 @@ export class Player {
|
||||||
a.canPlayType('audio/mp4; codecs="ec-3"') || a.canPlayType('audio/mp4; codecs="eac3"')
|
a.canPlayType('audio/mp4; codecs="ec-3"') || a.canPlayType('audio/mp4; codecs="eac3"')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch {
|
||||||
|
// Atmos codec detection may fail on some browsers
|
||||||
|
}
|
||||||
|
|
||||||
let isAtmosPlaying = isTrackAtmos && deviceSupportsAtmos;
|
let isAtmosPlaying = isTrackAtmos && deviceSupportsAtmos;
|
||||||
const q = this.quality || localStorage.getItem('adaptive-playback-quality') || 'auto';
|
const q = this.quality || localStorage.getItem('adaptive-playback-quality') || 'auto';
|
||||||
|
|
@ -1814,7 +1818,7 @@ export class Player {
|
||||||
// Re-enable ABR so it can dynamically downgrade within that new codec family if needed
|
// Re-enable ABR so it can dynamically downgrade within that new codec family if needed
|
||||||
this.shakaPlayer.configure({ abr: { enabled: true } });
|
this.shakaPlayer.configure({ abr: { enabled: true } });
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch {
|
||||||
// fail silently on abr checks
|
// fail silently on abr checks
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1571,7 +1571,7 @@ export async function initializeSettings(scrobbler, player, api, ui) {
|
||||||
// Draw Original measurement (normalized + shifted)
|
// Draw Original measurement (normalized + shifted)
|
||||||
if (graphMeasurement) {
|
if (graphMeasurement) {
|
||||||
const normOff = targetData
|
const normOff = targetData
|
||||||
? getNormalizationOffset(targetData) - getNormalizationOffset(graphMeasurement)
|
? getNormalizationOffset(graphMeasurement, targetData)
|
||||||
: 0;
|
: 0;
|
||||||
const normalized = graphMeasurement.map((p) => ({ freq: p.freq, gain: p.gain + normOff + graphShift }));
|
const normalized = graphMeasurement.map((p) => ({ freq: p.freq, gain: p.gain + normOff + graphShift }));
|
||||||
drawCurve(normalized, originalColor, 1.5);
|
drawCurve(normalized, originalColor, 1.5);
|
||||||
|
|
@ -1746,7 +1746,7 @@ export async function initializeSettings(scrobbler, player, api, ui) {
|
||||||
}
|
}
|
||||||
const targetEntry = tList.find((t) => t.id === tId);
|
const targetEntry = tList.find((t) => t.id === tId);
|
||||||
const targetData = targetEntry?.data;
|
const targetData = targetEntry?.data;
|
||||||
const normOff = targetData ? getNormalizationOffset(targetData) - getNormalizationOffset(measurement) : 0;
|
const normOff = targetData ? getNormalizationOffset(measurement, targetData) : 0;
|
||||||
const sampleRate = autoeqSampleRate ? parseInt(autoeqSampleRate.value, 10) : 48000;
|
const sampleRate = autoeqSampleRate ? parseInt(autoeqSampleRate.value, 10) : 48000;
|
||||||
|
|
||||||
autoeqCorrectedCurve = measurement.map((p) => {
|
autoeqCorrectedCurve = measurement.map((p) => {
|
||||||
|
|
@ -3113,6 +3113,8 @@ export async function initializeSettings(scrobbler, player, api, ui) {
|
||||||
|
|
||||||
// Graph always visible in all modes
|
// Graph always visible in all modes
|
||||||
if (graphSection) graphSection.style.display = '';
|
if (graphSection) graphSection.style.display = '';
|
||||||
|
// Only show shared AutoEq button in AutoEQ mode
|
||||||
|
if (autoeqRunBtn) autoeqRunBtn.style.display = mode === 'autoeq' ? '' : 'none';
|
||||||
|
|
||||||
// Hide all mode-specific sections first
|
// Hide all mode-specific sections first
|
||||||
if (controlsSection) controlsSection.style.display = 'none';
|
if (controlsSection) controlsSection.style.display = 'none';
|
||||||
|
|
|
||||||
|
|
@ -1012,6 +1012,7 @@ export const equalizerSettings = {
|
||||||
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',
|
PREAMP_KEY: 'equalizer-preamp',
|
||||||
|
CUSTOM_FREQUENCIES_KEY: 'equalizer-custom-frequencies',
|
||||||
DEFAULT_BAND_COUNT: 16,
|
DEFAULT_BAND_COUNT: 16,
|
||||||
MIN_BANDS: 3,
|
MIN_BANDS: 3,
|
||||||
MAX_BANDS: 32,
|
MAX_BANDS: 32,
|
||||||
|
|
@ -1280,6 +1281,40 @@ export const equalizerSettings = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getCustomFrequencies(bandCount) {
|
||||||
|
const count = bandCount || this.getBandCount();
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(this.CUSTOM_FREQUENCIES_KEY);
|
||||||
|
if (stored) {
|
||||||
|
const freqs = JSON.parse(stored);
|
||||||
|
if (Array.isArray(freqs) && freqs.length === count) {
|
||||||
|
return freqs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
setCustomFrequencies(frequencies) {
|
||||||
|
try {
|
||||||
|
if (Array.isArray(frequencies) && frequencies.length >= this.MIN_BANDS && frequencies.length <= this.MAX_BANDS) {
|
||||||
|
localStorage.setItem(this.CUSTOM_FREQUENCIES_KEY, JSON.stringify(frequencies));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[EQ] Failed to save custom frequencies:', e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearCustomFrequencies() {
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(this.CUSTOM_FREQUENCIES_KEY);
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
getBandTypes(bandCount) {
|
getBandTypes(bandCount) {
|
||||||
const count = bandCount || this.getBandCount();
|
const count = bandCount || this.getBandCount();
|
||||||
try {
|
try {
|
||||||
|
|
@ -2731,7 +2766,7 @@ export const contentBlockingSettings = {
|
||||||
blockArtist(artist) {
|
blockArtist(artist) {
|
||||||
if (!artist || !artist.id) return;
|
if (!artist || !artist.id) return;
|
||||||
const blocked = this.getBlockedArtists();
|
const blocked = this.getBlockedArtists();
|
||||||
if (!blocked.some((a) => a.id === artist.id)) {
|
if (!blocked.some((a) => String(a.id) === String(artist.id))) {
|
||||||
blocked.push({
|
blocked.push({
|
||||||
id: artist.id,
|
id: artist.id,
|
||||||
name: artist.name || 'Unknown Artist',
|
name: artist.name || 'Unknown Artist',
|
||||||
|
|
@ -2742,7 +2777,7 @@ export const contentBlockingSettings = {
|
||||||
},
|
},
|
||||||
|
|
||||||
unblockArtist(artistId) {
|
unblockArtist(artistId) {
|
||||||
const blocked = this.getBlockedArtists().filter((a) => a.id !== artistId);
|
const blocked = this.getBlockedArtists().filter((a) => String(a.id) !== String(artistId));
|
||||||
this.setBlockedArtists(blocked);
|
this.setBlockedArtists(blocked);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7907,8 +7907,8 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend-target .legend-dot {
|
.legend-target .legend-dot {
|
||||||
background: rgb(255 255 255 / 0.5);
|
background: color-mix(in srgb, var(--foreground) 50%, transparent);
|
||||||
border: 1px dashed rgb(255 255 255 / 0.3);
|
border: 1px dashed color-mix(in srgb, var(--foreground) 30%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend-corrected .legend-dot {
|
.legend-corrected .legend-dot {
|
||||||
|
|
@ -8304,6 +8304,7 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
|
||||||
color: var(--muted-foreground);
|
color: var(--muted-foreground);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
transition: all var(--transition-fast);
|
transition: all var(--transition-fast);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
@ -8313,8 +8314,10 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
|
||||||
color: var(--destructive-foreground);
|
color: var(--destructive-foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
.autoeq-profile-card:hover .autoeq-profile-delete {
|
.autoeq-profile-card:hover .autoeq-profile-delete,
|
||||||
|
.autoeq-profile-card:focus-within .autoeq-profile-delete {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Database Browser --- */
|
/* --- Database Browser --- */
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue