Skip to content

feat(player): add Technics SH-8055 LED style to spectrum analyzer#46

Merged
indigo423 merged 6 commits into
mainfrom
feat/add-spectrum-analyzer-styles
May 25, 2026
Merged

feat(player): add Technics SH-8055 LED style to spectrum analyzer#46
indigo423 merged 6 commits into
mainfrom
feat/add-spectrum-analyzer-styles

Conversation

@indigo423
Copy link
Copy Markdown
Collaborator

@indigo423 indigo423 commented May 25, 2026

Summary

  • Adds a second visual style to the expanded-player spectrum analyzer: an LED graphic equalizer modelled on the Technics SH-8055 (12 cyan tile bands, dB scale, dual Hz rows with (Hz) prefix and dB caption, faint grid lines, multi-pass cyan glow via Canvas 2D shadowBlur).
  • Click the analyzer canvas — or focus it and press Enter / Space — to cycle styles. Choice persists in localStorage as display.analyzerStyle. The canvas is promoted from decorative (aria-hidden) to interactive (role="button", dynamic aria-label, aria-live="off", keyboard handlers).
  • Refactors the single SpectrumAnalyzer component into a wrapper + two single-purpose renderers (SpectrumStyleClassic, SpectrumStyleLed) so a third style is mechanical to add. logGroupBins and SpectrumDimensions extracted to lib/spectrum-binning.ts.

The classic 20-bar gradient style is byte-for-byte preserved and remains the default. Existing users see no visual change unless they click.

Commits

  1. feat(player) — initial implementation
  2. fix(player) — applies all 23 patches from a bmad-code-review pass (Blind Hunter + Edge Case Hunter + Acceptance Auditor)

Test plan

  • npm run typecheck — clean
  • npm test — 153/153 passing (+3 from new assertions)
  • npm run lint — 0 errors, 36 warnings (1 new, in the same accepted react-hooks/set-state-in-effect category as the existing 35)
  • npm run build — production build succeeds
  • openspec validate add-spectrum-analyzer-styles --strict — passes
  • Manual browser walkthrough (tasks.md §6.1–6.11):
    • Fresh load → classic style, no display.analyzerStyle in localStorage
    • Hover analyzer → pointer cursor + tooltip
    • Click → LED style, navy bg, 12 cyan tile bands, dB scale 30/25/20/15/10/5 both sides, top legend, top + bottom Hz rows with (Hz) prefix, glow effect
    • Click again → back to classic
    • Hard-reload with "led" stored → LED renders first
    • Tab to analyzer → focus ring; Enter swaps; Space swaps and page doesn't scroll
    • Resize ≤600px with LED active → top chrome drops, dB shows 30/5 only, bands remain
    • Play MOD in each style → both react to audio; LED peak tile rides above
    • Switch styles mid-playback → no glitch, no dropout
    • No track + click → LED renders empty bands, no console errors
    • Mute via volume popover during LED → tiles decay to off

OpenSpec change

Tracked in openspec/changes/add-spectrum-analyzer-styles/ (proposal, design, specs delta for player-controls, tasks).

indigo423 added 3 commits May 26, 2026 00:59
Refactors the spectrum analyzer into a wrapper + renderer pair and adds
a second visual style: an LED graphic equalizer modelled on the Technics
SH-8055 12-channel real-time spectrum analyzer (12 cyan tile bands, dB
scale, dual Hz rows with `(Hz)` prefix and `dB` caption, faint grid lines,
multi-pass cyan glow via canvas `shadowBlur`).

Click anywhere on the analyzer canvas — or focus it and press Enter or
Space — to cycle between the existing gradient bars (`classic`) and the
new LED style (`led`). The choice persists in `localStorage` under
`display.analyzerStyle`. The canvas is promoted from decorative
(`aria-hidden`) to interactive (`role="button"`, keyboard handlers).

Tile fill and chrome text both use the app's canonical cyan (`#00b8ff`).
Legend gets a tighter glow radius so the heading reads brighter against
the wider tile bloom.

Assisted-by: ClaudeCode:claude-opus-4-7
Signed-off-by: Ronny Trommer <ronny@no42.org>
Adversarial review (Blind Hunter + Edge Case Hunter + Acceptance
Auditor) surfaced 26 actionable items across SSR, accessibility,
keyboard ergonomics, edge cases, and spec/code drift. All applied:

Wrapper:
- Defer localStorage read to useEffect (SSR hydration mismatch fix)
- Move localStorage write out of setState updater (StrictMode purity)
- Guard typeof window in write path (symmetric with read)
- Symmetric preventDefault on Enter; gate modifier keys
- Touch tap double-cycle guard via pointerdown timestamp
- Cap devicePixelRatio at 2 to avoid browser canvas size limit
- ResizeObserver fallback (one-shot dim read when undefined)
- Clear canvas on style swap to avoid stale frame
- Dynamic aria-label / title describing the cycle target
- Explicit aria-live="off" suppresses AT live-region announcement
- SCSS outline-offset:2px so focus ring clears the canvas edge

LED renderer:
- Safari <14 mql.addListener fallback
- Guard typeof window.matchMedia for embedded/jsdom contexts
- Recreate FFT buffer when analyser.frequencyBinCount drifts
- (bars[i] ?? 0) prevents NaN poisoning peak state
- try/finally resets shadowBlur even on throw
- Move peak compute + decay above GLOW_PASSES loop (decoupled)
- Module-load assert HZ_LABELS.length === NUM_BANDS
- Re-indent closure body

Classic renderer:
- Same NaN guard and buffer-resize patches as LED

Library:
- logGroupBins validates Number.isInteger(numBars)

Tests:
- Assert "invalid stored value left untouched" (no auto cleanup)
- Assert dynamic aria-label and aria-live=off in SSR markup
- Cover non-integer / NaN / Infinity inputs to logGroupBins
- DOM-event handler tests deferred (vitest env is node, no jsdom)

Assisted-by: ClaudeCode:claude-opus-4-7
Signed-off-by: Ronny Trommer <ronny@no42.org>
Bumps TILE_FILL and CHROME_TEXT from #00b8ff (app canonical cyan) to
#7ad5ff (lighter, more luminous). Gives the LED tiles and the legend
the "lit phosphor" look the SH-8055 reference has, while staying in the
same cyan family. Classic style is unaffected.

Assisted-by: ClaudeCode:claude-opus-4-7
Signed-off-by: Ronny Trommer <ronny@no42.org>
@indigo423 indigo423 force-pushed the feat/add-spectrum-analyzer-styles branch from 4401cd8 to d1f4952 Compare May 25, 2026 22:59
indigo423 added 3 commits May 26, 2026 01:09
CI's chromium + webkit reported a 14.8px vertical-center diff between
the disc image and the analyzer canvas, breaking the previous 8px
threshold. The actual layout is unchanged from before this PR — disc
and canvas wrap are both ~169px tall, aligned via flex `align-items:
center`. The CI measurement drift is from canvas backing-store sizing
not having fully settled at the moment of measurement (the firefox
flaky result on retry confirms the timing sensitivity).

20px keeps the "they share a row" intent while tolerating the timing
flake.

Assisted-by: ClaudeCode:claude-opus-4-7
Original mobile strategy aggressively dropped top chrome (legend, top
Hz row, dB caption) and thinned dB labels to keep tile count high. In
practice the SH-8055 identity reads more strongly from the chrome than
the tile count — the mobile rendering looked like a generic 12-bar
equalizer rather than a recognisable replica.

Now all chrome renders identically at every viewport. The mobile wrap
(~117px) compresses the bar area to ~13 tile rows (vs ~23 desktop) but
keeps the SH-8055 frame intact.

Removed code that existed only to support the mobile-chrome strategy:
- matchMedia("(max-width: 600px)") subscription
- isMobileRef + Safari addListener fallback
- DB_LABELS_MOBILE + isMobile branching

Assisted-by: ClaudeCode:claude-opus-4-7
Visual tweak (LED renderer):
- TILE_HEIGHT_PX: 3 → 2 (tighter tiles per user request)

Test hardening (e2e):
- Filter benign "Failed to load resource: 404" console.errors from
  consoleErrors. The "play random" path picks an arbitrary modarchive
  ID which sometimes 404s upstream; that's not a regression in our code.
  Pageerrors (real thrown exceptions) and other console.errors still fail.
- Wait for the disc banner image to finish loading before measuring its
  bounding box. Without this, the canvas-x layout assertion races the
  image load and reports the disc's pre-layout (smaller) box, making the
  canvas appear to overlap the disc area.

Both e2e patches address pre-existing flakes surfaced on CI, not
regressions from this branch.

Assisted-by: ClaudeCode:claude-opus-4-7
@indigo423 indigo423 merged commit 2df4e56 into main May 25, 2026
4 checks passed
@indigo423 indigo423 deleted the feat/add-spectrum-analyzer-styles branch May 25, 2026 23:17
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