Skip to content

Add live spectrum analyzer overlay on the EQ graph#609

Open
tryptz wants to merge 9 commits into
monochrome-music:mainfrom
tryptz:main
Open

Add live spectrum analyzer overlay on the EQ graph#609
tryptz wants to merge 9 commits into
monochrome-music:mainfrom
tryptz:main

Conversation

@tryptz
Copy link
Copy Markdown

@tryptz tryptz commented Apr 22, 2026

Screenshot 2026-04-13 005909 Screenshot 2026-04-13 005552 Screenshot 2026-04-12 235802

Ports the fork's spectrum analyzer feature on top of the latest upstream.

  • Dedicated high-resolution AnalyserNode (fftSize 8192, ~5.9 Hz/bin at 48 kHz) tapped off the main analyser as a side-chain in js/audio-context.js.

  • Overlay drawn into the EQ graph canvas with 1/N-octave smoothing, pink-tilt slope compensation, and EMA time-averaging driven by a Speed preset.

  • In-graph control pills above the canvas (Lo range knob, Hold, Speed, FFT, Spectrum toggle). Lo/Hi knobs scroll-or-drag vertically to adjust the dB range; state persists in localStorage.

  • rAF loop restarts on EQ/mode changes so the overlay stays live.

  • Layout: grid with pills on top and canvas below on desktop; flex-column stack with flex-wrap on narrow mobile (<=600px) so the Spectrum pill no longer overlaps or wraps awkwardly over the graph.

Description

Type of Change

  • Bug fix
  • New feature
  • Style/UI update
  • Docs only

Checklist

  • I have read the Contributing Guidelines.
  • I understand every line of code I am submitting.
  • I have tested these changes locally, and they work as expected.
  • Is this Pull request Using AI/Is Vibecoded?

By submitting this PR, I agree to follow the guidelines. I understand that the final decision to merge rests with the maintainers and that not all contributions can be accepted.

Summary by CodeRabbit

  • New Features

    • Interactive live spectrum overlay in the Frequency Response graph with on/off toggle, Hold (freeze), Speed presets, FFT resolution cycling, and a vertical Lo range control (wheel/drag/keyboard; double-click reset). Optional spectrum tinting by active EQ bands. Controls and overlay state persist across sessions.
  • Style

    • Responsive layout and updated styling for graph and spectrum controls on small screens.
  • Documentation

    • Added comprehensive README and an INSTANCES.md notice about outdated content.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 22, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a live spectrum overlay to the AutoEQ frequency-response graph: new DOM controls and CSS, a dedicated high-resolution AnalyserNode with public accessor, and an rAF-driven sampling + render loop implementing EMA smoothing, pink-tilt compensation, FFT/speed presets, persistent settings, and safe listener teardown.

Changes

AutoEQ Spectrum Overlay

Layer / File(s) Summary
DOM Controls
index.html
Inserts spectrum UI adjacent to autoeq-response-canvas: eq-spectrum-range-lo knob + eq-spectrum-range-lo-value, pill buttons eq-spectrum-hold (eq-spectrum-hold-icon, eq-spectrum-hold-value), eq-spectrum-speed (eq-spectrum-speed-value), eq-spectrum-fft (eq-spectrum-fft-value), and eq-spectrum-toggle (aria-pressed=false) for JS wiring.
Audio Node / Public Accessor
js/audio-context.js
Creates/configures a dedicated spectrumAnalyser (large FFT, smoothing=0, expanded dB attempts), connects it in parallel to the graph (guarded), disconnects on teardown, and exposes getSpectrumAnalyser() returning the node or null.
Spectrum Runtime & Rendering
js/settings.js
Adds persisted overlay enablement and presets (Lo, speed, FFT), _spectrumListenersAbort for listener teardown, rAF lifecycle gating (shouldAnimateSpectrum), startSpectrumLoop/stopSpectrumLoop, drawSpectrumLayer() (samples analyser via getFloatFrequencyData, EMA smoothing, pink-tilt and octave-window smoothing, downsampled quadratic trace, optional EQ-based boost/cut tint), hold/freeze, speed/FFT cycling, attachRangeKnob() (wheel/drag/keyboard/dblclick), and integrates overlay rendering beneath grid/curves in drawAutoEQGraph().
Styles & Layout
styles.css
Adds --highlight-foreground theme variable and .eq-spectrum-* classes for controls/knobs/pills/toggle, refactors .autoeq-graph-wrapper to grid, positions .autoeq-response-canvas with min-height: 0, and responsive stacking at max-width 600px/480px with reduced pill sizing.

Sequence Diagram

sequenceDiagram
    participant User
    participant DOM as "DOM Controls"
    participant Settings as "Settings Manager"
    participant AudioCtx as "AudioContextManager"
    participant Analyser as "Spectrum Analyser"
    participant Canvas as "Graph Canvas"

    User->>DOM: Toggle spectrum / adjust controls
    DOM->>Settings: Update state / persist to localStorage
    Settings->>AudioCtx: getSpectrumAnalyser()
    AudioCtx-->>Settings: spectrumAnalyser (node)

    rect rgba(100, 150, 255, 0.5)
    Note over Settings,Canvas: RAF render loop (enabled)
    Settings->>Analyser: getFloatFrequencyData()
    Analyser-->>Settings: frequency data
    Settings->>Settings: EMA smoothing / pink-tilt / downsample
    Settings->>Canvas: draw spectrum overlay
    Canvas-->>User: updated visualization
    end

    User->>DOM: Change hold/speed/FFT/range
    DOM->>Settings: update render params
    Settings->>Canvas: refresh overlay with new params
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐇 I twitch my whiskers, scope the tune,

Pink-tilt hums beneath the moon,
Knobs to turn and pills to press,
Freeze the trace or speed the mess,
A rabbit cheers — the spectrum blooms!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change: adding a live spectrum analyzer overlay to the EQ graph, which aligns with the substantial implementation across js/audio-context.js, js/settings.js, index.html, and styles.css.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description provides a comprehensive overview with technical details, implementation specifics, and UI/UX changes. All required checklist items are marked as complete.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@js/settings.js`:
- Around line 2040-2113: The spectrum re-init leaves old animation and control
listeners active; update initializeSettings() / the spectrum teardown to cancel
any running RAF and attach all control listeners via the same abort signal used
by _spectrumListenersAbort so they get removed on re-initialization: ensure
_spectrumRafId is checked and cancelAnimationFrame(_spectrumRafId) (and set to
null) when creating a new _spectrumListenersAbort or in its abort handler, move
startSpectrumLoop/stopSpectrumLoop logic to honor that shared state, and replace
direct addEventListener calls on spectrumBtn, the range knob, hold, speed and
FFT buttons with addSpectrumListener(...) (preserving options like { passive:
false }) so controls aren’t duplicated and old
drawAutoEQGraph()/scheduleDrawAutoEQGraph() closures stop running after
re-initialize.

In `@styles.css`:
- Around line 8403-8409: The active spectrum pill selectors
(.eq-spectrum-toggle-in-graph.active and .eq-spectrum-pill.active) currently use
--primary-foreground which may not contrast with --highlight in some themes;
introduce and use a new CSS custom property --highlight-foreground in those
selectors instead of --primary-foreground, and add theme-specific definitions
for --highlight-foreground (fallback to --primary-foreground if not defined) in
the theme blocks so active pills always meet contrast requirements against
--highlight.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 42cc0b4c-711f-4dbc-8639-312fc589c260

📥 Commits

Reviewing files that changed from the base of the PR and between a3580a9 and 4932333.

📒 Files selected for processing (4)
  • index.html
  • js/audio-context.js
  • js/settings.js
  • styles.css

Comment thread js/settings.js
Comment thread styles.css
Ports the fork's spectrum analyzer feature on top of the latest upstream.

- Dedicated high-resolution AnalyserNode (fftSize 8192, ~5.9 Hz/bin at 48 kHz)
  tapped off the main analyser as a side-chain in js/audio-context.js.
- Overlay drawn into the EQ graph canvas with 1/N-octave smoothing, pink-tilt
  slope compensation, and EMA time-averaging driven by a Speed preset.
- In-graph control pills above the canvas (Lo range knob, Hold, Speed, FFT,
  Spectrum toggle). Lo/Hi knobs scroll-or-drag vertically to adjust the dB
  range; state persists in localStorage.
- rAF loop restarts on EQ/mode changes so the overlay stays live.
- Layout: grid with pills on top and canvas below on desktop; flex-column
  stack with flex-wrap on narrow mobile (<=600px) so the Spectrum pill no
  longer overlaps or wraps awkwardly over the graph.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
styles.css (1)

179-179: ⚠️ Potential issue | 🟠 Major

Make active spectrum foregrounds contrast-safe.

These tokens feed the active spectrum pills on Line 8415. White on the dark/purple highlights, and near-white on the latte blue, are below the 4.5:1 target for the 0.7rem pill text. The darker values also resolve the current stylelint hits on Lines 179 and 225 without shortening to low-contrast #fff.

🎨 Proposed fix
 :root[data-theme='dark'] {
-    --highlight-foreground: `#ffffff`;
+    --highlight-foreground: `#0a0a0a`;
 }

 :root[data-theme='purple'] {
-    --highlight-foreground: `#ffffff`;
+    --highlight-foreground: `#0f0514`;
 }

 :root[data-theme='latte'] {
-    --highlight-foreground: `#eff1f5`;
+    --highlight-foreground: `#fff`;
 }

Also applies to: 225-225, 340-340

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@styles.css` at line 179, The --highlight-foreground token (used by the active
spectrum pills) currently uses near-white values that fail 4.5:1 contrast for
0.7rem pill text; update the variable definitions (the occurrences of
--highlight-foreground at the three locations shown) to use a darker hex that
meets a 4.5:1 contrast ratio against the dark/purple and latte-blue backgrounds
used by the active spectrum pills (referenced where the pills are rendered
around the active spectrum pills markup). Replace the three instances of
--highlight-foreground with a tested darker color that satisfies WCAG contrast
(and resolves the stylelint errors) so the pill text is contrast-safe.
js/settings.js (1)

2040-2113: ⚠️ Potential issue | 🟠 Major

Complete the spectrum teardown across re-initializations.

_spectrumRafId is still scoped to one initializeSettings() call, and the spectrum toggle/range/hold/speed/FFT listeners are still registered without the abort signal. Re-running settings can leave the old draw loop and duplicate control handlers alive.

Suggested direction
 let _graphAbortController = null;
 let _graphResizeObserver = null;
 // Persisted across initializeSettings() re-runs so listeners from a previous
 // call can be torn down before fresh ones register.
 let _spectrumListenersAbort = null;
+let _spectrumRafId = null;
-    let _spectrumRafId = null;
     let _spectrumData = null;
         if (_spectrumListenersAbort) _spectrumListenersAbort.abort();
+        if (_spectrumRafId) {
+            cancelAnimationFrame(_spectrumRafId);
+            _spectrumRafId = null;
+        }
         _spectrumListenersAbort = new AbortController();
         const sigOpts = { signal: _spectrumListenersAbort.signal };
+        const addSpectrumListener = (target, type, handler, options = {}) => {
+            target?.addEventListener(type, handler, {
+                ...options,
+                signal: _spectrumListenersAbort.signal,
+            });
+        };

Then route spectrumBtn, attachRangeKnob(...), hold, speed, and FFT listeners through that helper, preserving options like { passive: false }.

Also applies to: 4611-4854

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@js/settings.js` around lines 2040 - 2113, initializeSettings() doesn't fully
teardown spectrum state across re-initializations: _spectrumRafId can continue
running and spectrum controls (spectrumBtn, attachRangeKnob(...), hold, speed,
FFT listeners) are re-registered without being removed. Fix by creating an
AbortController (or similar) per initializeSettings run, store it (e.g.
spectrumAbortController), use it to addEventListener with signal for all
spectrum-related handlers (spectrumBtn clicks, attachRangeKnob callbacks, hold
toggle, spectrum speed/FFT selectors) and pass through existing options like {
passive: false }, and on re-init call abort() and ensure
startSpectrumLoop/stopSpectrumLoop always cancel any outstanding _spectrumRafId
(set to null after cancelAnimationFrame). Ensure listeners are added only via
this helper and that stopSpectrumLoop is idempotent so re-init won't leave the
old draw loop or duplicated handlers.
🧹 Nitpick comments (1)
js/audio-context.js (1)

637-643: Consider adding a comment clarifying whether the post-EQ spectrum tap is intentional.

The spectrumAnalyser is tapped off this.analyser, which sits after the GEQ chain completes (line 631 or 633 feeds into this.analyser, then line 640 taps it). This means the overlay displays the post-EQ spectrum — what users hear after applying their EQ curve — rather than the input spectrum they're shaping.

If this is deliberate (showing the result of EQ tuning), a brief note at line 637 would prevent future confusion. If the intent is to show the raw input spectrum for reference, the tap should be moved earlier in the chain.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@js/audio-context.js` around lines 637 - 643, The post-EQ spectrum tap is
currently done by connecting this.analyser to this.spectrumAnalyser, which
captures the signal after the GEQ chain (e.g., the GEQ feed into this.analyser);
add a brief clarifying comment above the try block describing that this is an
intentional post-EQ tap used to show the resulting output spectrum, or if the
desired behavior is to show the raw input spectrum move the tap to the node
before the GEQ chain (referencing this.analyser, spectrumAnalyser and the GEQ
feed into this.analyser when updating the code).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@js/settings.js`:
- Around line 4602-4622: The re-evaluation that restarts the rAF spectrum loop
should be triggered from setEQMode as well as from button clicks: update the
setEQMode function to either call the same reevalSpectrumLoop helper (or
dispatch a dedicated 'equalizer-mode-changed' event) immediately after it
finishes updating visibility/state so programmatic mode changes restart the live
loop; keep using the existing applySpectrumState helper and the same
AbortController signal handling (_spectrumListenersAbort,
onSpectrumVisibilityChange) so you don't duplicate logic or listeners.

---

Duplicate comments:
In `@js/settings.js`:
- Around line 2040-2113: initializeSettings() doesn't fully teardown spectrum
state across re-initializations: _spectrumRafId can continue running and
spectrum controls (spectrumBtn, attachRangeKnob(...), hold, speed, FFT
listeners) are re-registered without being removed. Fix by creating an
AbortController (or similar) per initializeSettings run, store it (e.g.
spectrumAbortController), use it to addEventListener with signal for all
spectrum-related handlers (spectrumBtn clicks, attachRangeKnob callbacks, hold
toggle, spectrum speed/FFT selectors) and pass through existing options like {
passive: false }, and on re-init call abort() and ensure
startSpectrumLoop/stopSpectrumLoop always cancel any outstanding _spectrumRafId
(set to null after cancelAnimationFrame). Ensure listeners are added only via
this helper and that stopSpectrumLoop is idempotent so re-init won't leave the
old draw loop or duplicated handlers.

In `@styles.css`:
- Line 179: The --highlight-foreground token (used by the active spectrum pills)
currently uses near-white values that fail 4.5:1 contrast for 0.7rem pill text;
update the variable definitions (the occurrences of --highlight-foreground at
the three locations shown) to use a darker hex that meets a 4.5:1 contrast ratio
against the dark/purple and latte-blue backgrounds used by the active spectrum
pills (referenced where the pills are rendered around the active spectrum pills
markup). Replace the three instances of --highlight-foreground with a tested
darker color that satisfies WCAG contrast (and resolves the stylelint errors) so
the pill text is contrast-safe.

---

Nitpick comments:
In `@js/audio-context.js`:
- Around line 637-643: The post-EQ spectrum tap is currently done by connecting
this.analyser to this.spectrumAnalyser, which captures the signal after the GEQ
chain (e.g., the GEQ feed into this.analyser); add a brief clarifying comment
above the try block describing that this is an intentional post-EQ tap used to
show the resulting output spectrum, or if the desired behavior is to show the
raw input spectrum move the tap to the node before the GEQ chain (referencing
this.analyser, spectrumAnalyser and the GEQ feed into this.analyser when
updating the code).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e0770f49-f874-4aa0-9342-194ad3b7408c

📥 Commits

Reviewing files that changed from the base of the PR and between 4932333 and 4b8ec7b.

📒 Files selected for processing (4)
  • index.html
  • js/audio-context.js
  • js/settings.js
  • styles.css
🚧 Files skipped from review as they are similar to previous changes (1)
  • index.html

Comment thread js/settings.js Outdated
Comment on lines +4602 to +4622
// Re-evaluate when EQ master toggle flips, when mode changes, or when
// the equalizer-container becomes visible again. Without this, the rAF
// loop self-stops via shouldAnimateSpectrum() and never restarts — the
// graph then only redraws from other triggers (~2 fps from stray events).
const reevalSpectrumLoop = () => {
// defer one frame so display:none transitions / mode swaps finish
requestAnimationFrame(applySpectrumState);
};

// Tear down listeners from a previous initializeSettings() call so we
// don't accumulate duplicate handlers across re-inits.
if (_spectrumListenersAbort) _spectrumListenersAbort.abort();
_spectrumListenersAbort = new AbortController();
const sigOpts = { signal: _spectrumListenersAbort.signal };

document.addEventListener('visibilitychange', onSpectrumVisibilityChange, sigOpts);
if (eqToggle) eqToggle.addEventListener('change', reevalSpectrumLoop, sigOpts);
window.addEventListener('equalizer-toggle', reevalSpectrumLoop, sigOpts);
document.querySelectorAll('.autoeq-mode-btn').forEach((b) =>
b.addEventListener('click', reevalSpectrumLoop, sigOpts)
);
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Restart the spectrum loop from setEQMode, not only mode button clicks.

This only handles user clicks on .autoeq-mode-btn; programmatic setEQMode(...) calls can switch back to a spectrum-capable graph without restarting the live rAF loop. Move the re-evaluation into setEQMode after visibility updates, or dispatch/listen for a dedicated EQ mode-change event there.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@js/settings.js` around lines 4602 - 4622, The re-evaluation that restarts the
rAF spectrum loop should be triggered from setEQMode as well as from button
clicks: update the setEQMode function to either call the same reevalSpectrumLoop
helper (or dispatch a dedicated 'equalizer-mode-changed' event) immediately
after it finishes updating visibility/state so programmatic mode changes restart
the live loop; keep using the existing applySpectrumState helper and the same
AbortController signal handling (_spectrumListenersAbort,
onSpectrumVisibilityChange) so you don't duplicate logic or listeners.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fine bro

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tryptz Got it—leaving it as-is. If we later run into a programmatic mode switch that doesn’t restart the loop, I can put up a tiny follow-up to dispatch a mode-change event or re-evaluate from setEQMode.


🧠 Learnings used
Learnt from: tryptz
Repo: monochrome-music/monochrome PR: 521
File: js/settings.js:1448-1474
Timestamp: 2026-04-07T02:59:21.853Z
Learning: In js/settings.js (Legacy 16‑band GEQ), the Equalizer APO single-line "GraphicEQ:" format is export-only by owner preference; do not implement import support for "GraphicEQ:" lines unless explicitly requested.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
styles.css (1)

172-193: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix stylelint hex-length (#ffffff#fff).

Static analysis is flagging --highlight-foreground: #ffffff`` as violating color-hex-length. This happens in both the `dark` and `purple` theme blocks. Update to the shorter `#fff` form.

🛠️ Proposed change
 :root[data-theme='dark'] {
     ...
-    --highlight-foreground: `#ffffff`;
+    --highlight-foreground: `#fff`;
     ...
 }

 :root[data-theme='purple'] {
     ...
-    --highlight-foreground: `#ffffff`;
+    --highlight-foreground: `#fff`;
     ...
 }

Also applies to: 218-239

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@styles.css` around lines 172 - 193, Update the 6-digit hex color values
flagged by stylelint to their 3-digit shorthand: locate the theme blocks (e.g.,
:root[data-theme='dark'] and the purple theme block around the other occurrence)
and change the CSS custom property --highlight-foreground from `#ffffff` to `#fff`
(and any other similar `#ffffff` instances in those theme blocks) so they comply
with the color-hex-length rule.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@js/settings.js`:
- Around line 2107-2120: drawSpectrumLayer currently returns early while
getSpectrumAnalyser() is null so a previously saved FFT selection
(spectrumFftKey) never gets reapplied; after you obtain a non-null analyser in
drawSpectrumLayer (the block using audioContextManager?.getSpectrumAnalyser?.()
and after the if (!analyser) return), call applyFft() (or the same routine that
applies spectrumFftKey) to reapply the saved FFT selection so the saved
2K/4K/16K choice takes effect once the dedicated analyser exists; reference
drawSpectrumLayer, audioContextManager.getSpectrumAnalyser, spectrumFftKey and
applyFft when adding this one-time reapply.
- Around line 4640-4749: The knob handlers update state and persist but never
trigger the spectrum redraw; after each change call the project's spectrum
redraw function (e.g. requestRedraw or redrawSpectrum) immediately after
render() and persist(). Update the wheel, dblclick, keydown cases and the
pointer handlers' onMove/onUp to invoke that redraw function right after
render() (and persist() where present) so the overlay updates when
set()/get()/render() change the value.

---

Outside diff comments:
In `@styles.css`:
- Around line 172-193: Update the 6-digit hex color values flagged by stylelint
to their 3-digit shorthand: locate the theme blocks (e.g.,
:root[data-theme='dark'] and the purple theme block around the other occurrence)
and change the CSS custom property --highlight-foreground from `#ffffff` to `#fff`
(and any other similar `#ffffff` instances in those theme blocks) so they comply
with the color-hex-length rule.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8b69a241-67cc-4879-a039-733a491eb25d

📥 Commits

Reviewing files that changed from the base of the PR and between 4b01be9 and 94ff148.

📒 Files selected for processing (3)
  • index.html
  • js/settings.js
  • styles.css
🚧 Files skipped from review as they are similar to previous changes (1)
  • index.html

Comment thread js/settings.js
Comment on lines +2107 to +2120
const drawSpectrumLayer = (ctx, padLeft, padTop, w, h, sampleRate) => {
// Only use the dedicated spectrum analyser. If unavailable, bail so we
// never mutate fftSize on the shared visualizer node.
let analyser = null;
try {
analyser = audioContextManager?.getSpectrumAnalyser?.() || null;
} catch {
return;
}
if (!analyser) return;
if (!_spectrumData || _spectrumData.length !== analyser.frequencyBinCount) {
_spectrumData = new Float32Array(analyser.frequencyBinCount);
_spectrumEma = null;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Re-apply the saved FFT once the analyser actually exists.

applyFft() silently no-ops when getSpectrumAnalyser() is still null during initialization, and nothing later reapplies spectrumFftKey. After a reload, a saved 2K/4K/16K choice can fall back to the analyser default until the user clicks the FFT pill again.

Suggested fix
         } catch {
             return;
         }
         if (!analyser) return;
+        const desiredFftSize = SPECTRUM_FFT_PRESETS[spectrumFftKey];
+        if (analyser.fftSize !== desiredFftSize) {
+            analyser.fftSize = desiredFftSize;
+        }
         if (!_spectrumData || _spectrumData.length !== analyser.frequencyBinCount) {
             _spectrumData = new Float32Array(analyser.frequencyBinCount);
             _spectrumEma = null;
         }

Also applies to: 4820-4833

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@js/settings.js` around lines 2107 - 2120, drawSpectrumLayer currently returns
early while getSpectrumAnalyser() is null so a previously saved FFT selection
(spectrumFftKey) never gets reapplied; after you obtain a non-null analyser in
drawSpectrumLayer (the block using audioContextManager?.getSpectrumAnalyser?.()
and after the if (!analyser) return), call applyFft() (or the same routine that
applies spectrumFftKey) to reapply the saved FFT selection so the saved
2K/4K/16K choice takes effect once the dedicated analyser exists; reference
drawSpectrumLayer, audioContextManager.getSpectrumAnalyser, spectrumFftKey and
applyFft when adding this one-time reapply.

Comment thread js/settings.js
Comment on lines +4640 to +4749
const render = () => {
const v = Math.round(get());
valueEl.textContent = String(v);
btn.setAttribute('aria-valuenow', String(v));
btn.setAttribute('aria-valuetext', `${v} dBFS`);
};
const persist = () => {
try {
localStorage.setItem(storageKey, String(get()));
} catch {
/* ignore */
}
};
render();

btn.addEventListener(
'wheel',
(e) => {
e.preventDefault();
const delta = e.deltaY > 0 ? -1 : 1;
set(clamp(get() + delta));
render();
persist();
},
{ passive: false }
);

btn.addEventListener('dblclick', (e) => {
e.preventDefault();
set(defaultValue);
render();
persist();
});

// Keyboard: arrows adjust, Home/End jump to bounds, Shift for coarse
btn.addEventListener('keydown', (e) => {
const coarse = e.shiftKey ? 6 : 1;
let handled = true;
switch (e.key) {
case 'ArrowUp':
case 'ArrowRight':
set(clamp(get() + coarse));
break;
case 'ArrowDown':
case 'ArrowLeft':
set(clamp(get() - coarse));
break;
case 'PageUp':
set(clamp(get() + 10));
break;
case 'PageDown':
set(clamp(get() - 10));
break;
case 'Home':
set(min);
break;
case 'End':
set(max);
break;
case 'Enter':
case ' ':
set(defaultValue);
break;
default:
handled = false;
}
if (handled) {
e.preventDefault();
render();
persist();
}
});

btn.addEventListener('pointerdown', (e) => {
if (e.button !== 0) return;
e.preventDefault();
try {
btn.setPointerCapture(e.pointerId);
} catch {
/* capture may fail on synthetic events */
}
btn.classList.add('dragging');
const startY = e.clientY;
const startVal = get();
const onMove = (ev) => {
const dy = startY - ev.clientY;
set(clamp(startVal + dy * 0.4));
render();
};
const onUp = (ev) => {
// Guard releasePointerCapture: capture may already be lost
// (e.g. pointercancel fired before pointerup)
try {
if (btn.hasPointerCapture?.(e.pointerId)) {
btn.releasePointerCapture(e.pointerId);
}
} catch {
/* ignore */
}
btn.classList.remove('dragging');
btn.removeEventListener('pointermove', onMove);
btn.removeEventListener('pointerup', onUp);
btn.removeEventListener('pointercancel', onUp);
persist();
ev.preventDefault();
};
btn.addEventListener('pointermove', onMove);
btn.addEventListener('pointerup', onUp);
btn.addEventListener('pointercancel', onUp);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Redraw the graph when the Lo range knob changes.

These handlers update state and localStorage, but they never request a redraw. When the spectrum is held or the loop is otherwise stopped, the pill changes while the overlay stays stale until some unrelated repaint.

Suggested fix
         const render = () => {
             const v = Math.round(get());
             valueEl.textContent = String(v);
             btn.setAttribute('aria-valuenow', String(v));
             btn.setAttribute('aria-valuetext', `${v} dBFS`);
+            scheduleDrawAutoEQGraph();
         };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@js/settings.js` around lines 4640 - 4749, The knob handlers update state and
persist but never trigger the spectrum redraw; after each change call the
project's spectrum redraw function (e.g. requestRedraw or redrawSpectrum)
immediately after render() and persist(). Update the wheel, dblclick, keydown
cases and the pointer handlers' onMove/onUp to invoke that redraw function right
after render() (and persist() where present) so the overlay updates when
set()/get()/render() change the value.

tryptz and others added 6 commits May 3, 2026 21:46
Brings in 31 commits from upstream while preserving the AutoEQ fork's
custom commits (live spectrum analyzer overlay 4b8ec7b plus housekeeping).

Conflicts resolved:
- extension/README.md: upstream removed the extension folder (post-Q-DL
  migration); accept the deletion.
- js/storage.js: take upstream's expanded Qobuz default list which adds
  qdl-api.monochrome.tf alongside qobuz.kennyy.com.br.
am-lyrics bumped to ^1.5.2 in upstream; refresh lockfile so
bun install --frozen-lockfile in CI lint workflow passes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant