Skip to content
73 changes: 73 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4681,6 +4681,79 @@ <h4>Speaker EQ - Room Correction</h4>
</div>
<div class="autoeq-graph-wrapper" id="autoeq-graph-wrapper">
<canvas id="autoeq-response-canvas" class="autoeq-response-canvas"></canvas>
<div class="eq-spectrum-range">
<button
type="button"
class="eq-spectrum-pill eq-spectrum-knob"
id="eq-spectrum-range-lo"
title="Range Lo — scroll or drag vertically"
>
<span class="pill-label">Lo</span>
<span class="pill-value" id="eq-spectrum-range-lo-value">-103</span>
</button>
</div>
<div class="eq-spectrum-controls">
<button
type="button"
class="eq-spectrum-pill"
id="eq-spectrum-hold"
title="Hold spectrum (freeze analyser)"
aria-pressed="false"
>
<svg
id="eq-spectrum-hold-icon"
width="10"
height="10"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<rect x="6" y="4" width="4" height="16" rx="1" />
<rect x="14" y="4" width="4" height="16" rx="1" />
</svg>
<span class="pill-value" id="eq-spectrum-hold-value">Hold</span>
</button>
<button
type="button"
class="eq-spectrum-pill"
id="eq-spectrum-speed"
title="Spectrum response speed"
>
<span class="pill-label">Speed</span>
<span class="pill-value" id="eq-spectrum-speed-value">Med</span>
</button>
<button
type="button"
class="eq-spectrum-pill"
id="eq-spectrum-fft"
title="FFT resolution"
>
<span class="pill-label">FFT</span>
<span class="pill-value" id="eq-spectrum-fft-value">8K</span>
</button>
<button
type="button"
class="eq-spectrum-toggle-in-graph"
id="eq-spectrum-toggle"
title="Toggle live spectrum analyzer (0 dBFS normalized)"
aria-pressed="false"
>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.4"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M3 20V10M7 20V5M11 20V13M15 20V8M19 20V15" />
</svg>
<span>Spectrum</span>
</button>
</div>
</div>
<div class="autoeq-auto-preamp">
<div class="autoeq-preamp-row">
Expand Down
42 changes: 42 additions & 0 deletions js/audio-context.js
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,31 @@ class AudioContextManager {
this.analyser.fftSize = 1024;
this.analyser.smoothingTimeConstant = 0.7;

// High-resolution spectrum analyser for EQ graph overlay.
// Large fftSize gives ~5.9 Hz/bin at 48 kHz — critical for bass detail.
// Settings.js does its own time-averaging, so smoothing is disabled here.
this.spectrumAnalyser = this.audioContext.createAnalyser();
try {
this.spectrumAnalyser.fftSize = 8192;
} catch {
// Older browsers cap fftSize — fall back gracefully
try {
this.spectrumAnalyser.fftSize = 4096;
} catch {
this.spectrumAnalyser.fftSize = 2048;
}
}
this.spectrumAnalyser.smoothingTimeConstant = 0;
// Widen decibel bounds to cover the overlay's Range Lo..Hi span so
// getFloatFrequencyData isn't clamped at ~[-100, -30] defaults and
// the Lo knob can reach -180 dBFS without silent saturation.
try {
this.spectrumAnalyser.minDecibels = -180;
this.spectrumAnalyser.maxDecibels = 0;
} catch {
/* some engines reject asymmetric ranges */
}

this._createEQ();
this._createGraphicEQ();
this._createMSNodes();
Expand Down Expand Up @@ -609,6 +634,13 @@ class AudioContextManager {
}
this.analyser.connect(this.volumeNode);
this.volumeNode.connect(this.audioContext.destination);
// Parallel tap for the hi-res EQ spectrum overlay (dead-end is fine
// for AnalyserNode — it only needs input to sample from).
try {
if (this.spectrumAnalyser) this.analyser.connect(this.spectrumAnalyser);
} catch {
/* ignore */
}
};

try {
Expand Down Expand Up @@ -656,6 +688,7 @@ class AudioContextManager {
this.geqFilters.forEach(safeDisconnect);
safeDisconnect(this.geqOutputNode);
safeDisconnect(this.analyser);
safeDisconnect(this.spectrumAnalyser);
safeDisconnect(this.volumeNode);

let lastNode = this.source;
Expand Down Expand Up @@ -776,6 +809,15 @@ class AudioContextManager {
return this.analyser;
}

/**
* Get the dedicated high-resolution analyser for the EQ spectrum overlay.
* Returns null when no dedicated node exists so callers never mutate the
* shared visualizer analyser (which would desync its cached binCount).
*/
getSpectrumAnalyser() {
return this.spectrumAnalyser || null;
}

/**
* Get the audio context
*/
Expand Down
Loading
Loading