fix: address PR #523 review comments for EQ and binaural DSP
- Remove stale IIR coefficient JSDoc comment - Reset M/S channel state on EQ import to prevent stale assignments - Enforce strictly increasing GEQ frequencies to prevent rounding duplicates - Guard Q calculation against zero octave spacing (Infinity/NaN) - Export EQ from stored metadata instead of live BiquadFilterNode state - Accept .csv in legacy GEQ import file input - Expose public reconnect() on BinauralDSP instead of calling _connectInternal - Dispatch binaural-mode-changed on channel count change, not just mode change - Remove no-op channelCount/channelCountMode on MediaElementAudioSourceNode - Add void to floating promises (toggleBinaural, notifyBinauralChannelCount, _loadBinauralSettings) - Wrap binauralDspSettings._setAll in try/catch for QuotaExceededError - Make generateHRTF synchronous (no awaits, was misleadingly async)
This commit is contained in:
parent
88b01570f5
commit
79313e7a0a
8 changed files with 38 additions and 28 deletions
|
|
@ -4595,7 +4595,7 @@
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
id="legacy-geq-import-file"
|
id="legacy-geq-import-file"
|
||||||
accept=".txt"
|
accept=".txt,.csv"
|
||||||
style="display: none"
|
style="display: none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,6 @@ import { isIos } from './platform-detection.js';
|
||||||
import { equalizerSettings, monoAudioSettings, binauralDspSettings } from './storage.js';
|
import { equalizerSettings, monoAudioSettings, binauralDspSettings } from './storage.js';
|
||||||
import { BinauralDSP } from './binaural-dsp.js';
|
import { BinauralDSP } from './binaural-dsp.js';
|
||||||
|
|
||||||
/**
|
|
||||||
* Compute RBJ cookbook IIR coefficients for shelf filters with Q support.
|
|
||||||
* Web Audio API's BiquadFilterNode ignores Q for lowshelf/highshelf,
|
|
||||||
* so we use IIRFilterNode with these coefficients instead.
|
|
||||||
*/
|
|
||||||
// Generate frequency array for given number of bands using logarithmic spacing
|
// Generate frequency array for given number of bands using logarithmic spacing
|
||||||
function generateFrequencies(bandCount, minFreq = 20, maxFreq = 20000) {
|
function generateFrequencies(bandCount, minFreq = 20, maxFreq = 20000) {
|
||||||
const frequencies = [];
|
const frequencies = [];
|
||||||
|
|
@ -493,14 +488,6 @@ class AudioContextManager {
|
||||||
|
|
||||||
if (!this.sources.has(audioElement)) {
|
if (!this.sources.has(audioElement)) {
|
||||||
const src = this.audioContext.createMediaElementSource(audioElement);
|
const src = this.audioContext.createMediaElementSource(audioElement);
|
||||||
// Allow multichannel passthrough for Atmos/spatial audio
|
|
||||||
try {
|
|
||||||
src.channelCount = 6;
|
|
||||||
src.channelCountMode = 'max';
|
|
||||||
src.channelInterpretation = 'discrete';
|
|
||||||
} catch {
|
|
||||||
// Some browsers may not support this
|
|
||||||
}
|
|
||||||
this.sources.set(audioElement, src);
|
this.sources.set(audioElement, src);
|
||||||
}
|
}
|
||||||
this.source = this.sources.get(audioElement);
|
this.source = this.sources.get(audioElement);
|
||||||
|
|
@ -520,7 +507,7 @@ class AudioContextManager {
|
||||||
|
|
||||||
// Create binaural DSP processor
|
// Create binaural DSP processor
|
||||||
this.binauralDsp = new BinauralDSP(this.audioContext);
|
this.binauralDsp = new BinauralDSP(this.audioContext);
|
||||||
this._loadBinauralSettings();
|
void this._loadBinauralSettings();
|
||||||
|
|
||||||
this._createEQ();
|
this._createEQ();
|
||||||
this._createGraphicEQ();
|
this._createGraphicEQ();
|
||||||
|
|
@ -689,7 +676,7 @@ class AudioContextManager {
|
||||||
if (this.isBinauralEnabled && this.binauralDsp) {
|
if (this.isBinauralEnabled && this.binauralDsp) {
|
||||||
const { input, output } = this.binauralDsp.getNodes();
|
const { input, output } = this.binauralDsp.getNodes();
|
||||||
lastNode.connect(input);
|
lastNode.connect(input);
|
||||||
this.binauralDsp._connectInternal();
|
this.binauralDsp.reconnect();
|
||||||
lastNode = output;
|
lastNode = output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1432,6 +1419,10 @@ class AudioContextManager {
|
||||||
this.currentQs = qs;
|
this.currentQs = qs;
|
||||||
this.currentGains = gains;
|
this.currentGains = gains;
|
||||||
|
|
||||||
|
// Reset M/S channel assignments — imported config has no channel info
|
||||||
|
this.currentChannels = new Array(this.bandCount).fill('stereo');
|
||||||
|
this.msEnabled = false;
|
||||||
|
|
||||||
// Rebuild EQ chain to apply new frequencies, types, and Qs
|
// Rebuild EQ chain to apply new frequencies, types, and Qs
|
||||||
if (this.isInitialized && this.audioContext) {
|
if (this.isInitialized && this.audioContext) {
|
||||||
this._destroyMSFilters();
|
this._destroyMSFilters();
|
||||||
|
|
@ -1446,6 +1437,7 @@ class AudioContextManager {
|
||||||
equalizerSettings.setGains(this.currentGains);
|
equalizerSettings.setGains(this.currentGains);
|
||||||
equalizerSettings.setBandTypes(this.currentTypes);
|
equalizerSettings.setBandTypes(this.currentTypes);
|
||||||
equalizerSettings.setBandQs(this.currentQs);
|
equalizerSettings.setBandQs(this.currentQs);
|
||||||
|
equalizerSettings.setBandChannels(this.currentChannels);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,13 @@ export class BinauralDSP {
|
||||||
return { input: this.inputNode, output: this.outputNode };
|
return { input: this.inputNode, output: this.outputNode };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconnect internal graph (public API for external callers).
|
||||||
|
*/
|
||||||
|
reconnect() {
|
||||||
|
this._connectInternal();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect internal graph based on current state.
|
* Connect internal graph based on current state.
|
||||||
*/
|
*/
|
||||||
|
|
@ -595,6 +602,7 @@ export class BinauralDSP {
|
||||||
*/
|
*/
|
||||||
async detectAndConfigure(channelCount) {
|
async detectAndConfigure(channelCount) {
|
||||||
const prevMode = this.mode;
|
const prevMode = this.mode;
|
||||||
|
const prevChannels = this.channelCount;
|
||||||
this.channelCount = channelCount;
|
this.channelCount = channelCount;
|
||||||
|
|
||||||
if (channelCount > 2) {
|
if (channelCount > 2) {
|
||||||
|
|
@ -603,13 +611,13 @@ export class BinauralDSP {
|
||||||
this.mode = 'stereo';
|
this.mode = 'stereo';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.enabled && this.mode !== prevMode) {
|
if (this.enabled && (this.mode !== prevMode || channelCount !== prevChannels)) {
|
||||||
await this._ensureNodesCreated();
|
await this._ensureNodesCreated();
|
||||||
this._connectInternal();
|
this._connectInternal();
|
||||||
|
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent('binaural-mode-changed', {
|
new CustomEvent('binaural-mode-changed', {
|
||||||
detail: { mode: this.mode, channels: channelCount },
|
detail: { mode: this.mode, channels: this.channelCount },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -621,11 +621,10 @@ export class Equalizer {
|
||||||
|
|
||||||
this.frequencies.forEach((freq, index) => {
|
this.frequencies.forEach((freq, index) => {
|
||||||
const gain = this.currentGains[index] || 0;
|
const gain = this.currentGains[index] || 0;
|
||||||
const filter = this.filters[index];
|
const type = this.currentTypes[index] || 'peaking';
|
||||||
const type = filter ? filter.type : 'peaking';
|
|
||||||
const typeMap = { peaking: 'PK', lowshelf: 'LSC', highshelf: 'HSC' };
|
const typeMap = { peaking: 'PK', lowshelf: 'LSC', highshelf: 'HSC' };
|
||||||
const typeStr = typeMap[type] || 'PK';
|
const typeStr = typeMap[type] || 'PK';
|
||||||
const q = filter ? filter.Q.value : this._calculateQ(index);
|
const q = this.currentQs[index] || this._calculateQ(index);
|
||||||
const filterNum = index + 1;
|
const filterNum = index + 1;
|
||||||
lines.push(`Filter ${filterNum}: ON ${typeStr} Fc ${freq} Hz Gain ${gain.toFixed(1)} dB Q ${q.toFixed(2)}`);
|
lines.push(`Filter ${filterNum}: ON ${typeStr} Fc ${freq} Hz Gain ${gain.toFixed(1)} dB Q ${q.toFixed(2)}`);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ function calculateHeadShadow(frequency, azimuthRad) {
|
||||||
* @param {number} [elevationDeg=0] - Elevation in degrees (currently simplified)
|
* @param {number} [elevationDeg=0] - Elevation in degrees (currently simplified)
|
||||||
* @returns {Promise<AudioBuffer>} Stereo AudioBuffer with HRTF IR
|
* @returns {Promise<AudioBuffer>} Stereo AudioBuffer with HRTF IR
|
||||||
*/
|
*/
|
||||||
export async function generateHRTF(audioContext, azimuthDeg, elevationDeg = 0) {
|
export function generateHRTF(audioContext, azimuthDeg, elevationDeg = 0) {
|
||||||
const sampleRate = audioContext.sampleRate;
|
const sampleRate = audioContext.sampleRate;
|
||||||
const buffer = audioContext.createBuffer(2, IR_LENGTH, sampleRate);
|
const buffer = audioContext.createBuffer(2, IR_LENGTH, sampleRate);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1885,7 +1885,7 @@ export class Player {
|
||||||
if (isAtmosPlaying) {
|
if (isAtmosPlaying) {
|
||||||
// Auto-enable binaural DSP for spatial content
|
// Auto-enable binaural DSP for spatial content
|
||||||
if (binauralDspSettings.getAutoEnableForSpatial() && !binauralDspSettings.isEnabled()) {
|
if (binauralDspSettings.getAutoEnableForSpatial() && !binauralDspSettings.isEnabled()) {
|
||||||
audioContextManager.toggleBinaural(true);
|
void audioContextManager.toggleBinaural(true);
|
||||||
// Update toggle in settings UI if visible
|
// Update toggle in settings UI if visible
|
||||||
const toggle = document.getElementById('binaural-dsp-toggle');
|
const toggle = document.getElementById('binaural-dsp-toggle');
|
||||||
if (toggle) toggle.checked = true;
|
if (toggle) toggle.checked = true;
|
||||||
|
|
@ -1893,7 +1893,7 @@ export class Player {
|
||||||
if (container) container.style.display = 'block';
|
if (container) container.style.display = 'block';
|
||||||
}
|
}
|
||||||
// Notify binaural DSP of multichannel content (Atmos is typically 5.1+)
|
// Notify binaural DSP of multichannel content (Atmos is typically 5.1+)
|
||||||
audioContextManager.notifyBinauralChannelCount(6);
|
void audioContextManager.notifyBinauralChannelCount(6);
|
||||||
|
|
||||||
const binauralActive = audioContextManager.isBinauralActive();
|
const binauralActive = audioContextManager.isBinauralActive();
|
||||||
badgeEl.className = 'quality-badge quality-atmos shaka-quality-badge';
|
badgeEl.className = 'quality-badge quality-atmos shaka-quality-badge';
|
||||||
|
|
@ -1901,7 +1901,7 @@ export class Player {
|
||||||
SVG_ATMOS(20) + (binauralActive ? ' <span class="binaural-badge">Binaural</span>' : '');
|
SVG_ATMOS(20) + (binauralActive ? ' <span class="binaural-badge">Binaural</span>' : '');
|
||||||
} else {
|
} else {
|
||||||
// Notify binaural DSP that we're in stereo mode
|
// Notify binaural DSP that we're in stereo mode
|
||||||
audioContextManager.notifyBinauralChannelCount(2);
|
void audioContextManager.notifyBinauralChannelCount(2);
|
||||||
badgeEl.className = 'quality-badge quality-hires shaka-quality-badge';
|
badgeEl.className = 'quality-badge quality-hires shaka-quality-badge';
|
||||||
badgeEl.textContent = text;
|
badgeEl.textContent = text;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1353,7 +1353,12 @@ export async function initializeSettings(scrobbler, player, api, ui) {
|
||||||
const freqs = [];
|
const freqs = [];
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const t = i / (count - 1);
|
const t = i / (count - 1);
|
||||||
freqs.push(Math.round(min * Math.pow(max / min, t)));
|
let freq = Math.round(min * Math.pow(max / min, t));
|
||||||
|
// Ensure strictly increasing — rounding can produce duplicates at high band counts
|
||||||
|
if (freqs.length > 0 && freq <= freqs[freqs.length - 1]) {
|
||||||
|
freq = freqs[freqs.length - 1] + 1;
|
||||||
|
}
|
||||||
|
freqs.push(freq);
|
||||||
}
|
}
|
||||||
return freqs;
|
return freqs;
|
||||||
};
|
};
|
||||||
|
|
@ -1561,7 +1566,9 @@ export async function initializeSettings(scrobbler, player, api, ui) {
|
||||||
const prev = GEQ_FREQUENCIES[Math.max(0, i - 1)];
|
const prev = GEQ_FREQUENCIES[Math.max(0, i - 1)];
|
||||||
const next = GEQ_FREQUENCIES[Math.min(GEQ_FREQUENCIES.length - 1, i + 1)];
|
const next = GEQ_FREQUENCIES[Math.min(GEQ_FREQUENCIES.length - 1, i + 1)];
|
||||||
const octaves = Math.log2(next / prev);
|
const octaves = Math.log2(next / prev);
|
||||||
const q = (Math.SQRT2 / (2 * Math.sinh((Math.LN2 / 2) * octaves))).toFixed(2);
|
const q = octaves > 0
|
||||||
|
? (Math.SQRT2 / (2 * Math.sinh((Math.LN2 / 2) * octaves))).toFixed(2)
|
||||||
|
: Math.SQRT2.toFixed(2);
|
||||||
lines.push(`Filter ${i + 1}: ON PK Fc ${freq} Hz Gain ${geqGains[i].toFixed(1)} dB Q ${q}`);
|
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' });
|
||||||
|
|
|
||||||
|
|
@ -1878,7 +1878,11 @@ export const binauralDspSettings = {
|
||||||
},
|
},
|
||||||
|
|
||||||
_setAll(obj) {
|
_setAll(obj) {
|
||||||
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(obj));
|
try {
|
||||||
|
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(obj));
|
||||||
|
} catch {
|
||||||
|
// QuotaExceededError — storage full
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
isEnabled() {
|
isEnabled() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue