This guide covers the architecture, key concepts, and development workflows for Spectrum Aura.
src/
├── App.tsx
├── main.tsx
├── components/
│ └── analyser/
│ ├── Analyser.tsx # Thin orchestrator (overlays + hook wiring)
│ ├── BarTimingHud.tsx # BPM / bar grid overlay (M toggle)
│ ├── TempoReadout.tsx # Shared BPM + grid (overlay + panel)
│ ├── theme.ts # Colors, swatches, bpmGlowStyle, LOCAL_DEV_URL
│ ├── ControlPanel.tsx # Settings sheet + flyout tabs
│ ├── control-panel/
│ │ ├── primitives.tsx # Sw, Bn, Row, ToggleRow, S
│ │ └── ViewSettings.tsx # Per-view slider/toggle blocks (14 views)
│ ├── hooks/
│ │ ├── useAnalyserEngine.ts # rAF/XR loop, audio, SongClock, scene
│ │ ├── useBeatHintBridge.ts # T / ⇧T manual sync bridge
│ │ ├── useAnalyserCommandEvents.ts
│ │ └── usePresetActions.ts # Shared preset load/save/cursor
│ ├── overlays/
│ │ ├── AudioSourcePrompt.tsx
│ │ ├── FreqLabels.tsx
│ │ ├── LatencyHud.tsx
│ │ ├── StatsPanel.tsx
│ │ └── stats-types.ts
│ ├── Shortcuts.tsx
│ ├── store.ts
│ ├── visuals.ts
│ ├── visuals/ # Optional *.visual.ts manifests
│ ├── __tests__/ # Component + store + UI tests
│ │ ├── helpers/test-helpers.ts
│ │ ├── Analyser.regression.test.ts
│ │ ├── ControlPanel.regression.test.ts
│ │ ├── usePresetActions.test.ts
│ │ ├── theme.test.ts
│ │ ├── Shortcuts.test.tsx
│ │ └── store.*.test.ts
│ └── engine/
│ ├── audio.ts # FFT read, BeatMatcher, BPMDetector feed
│ ├── beat-matcher.ts # Multi-band onset detection
│ ├── bpm-detector.ts # Tempo estimation (not grid owner)
│ ├── song-clock.ts # Authoritative bar/beat clock
│ ├── bar-clock.ts # BarTiming types + SongClock re-export
│ ├── live-tempo.ts # Per-frame HUD bus
│ ├── latency-metrics.ts # Render latency measurement (tested)
│ ├── view-cycle-controller.ts
│ ├── loudness.ts # Shared perceptual-loudness gamma curve (all views)
│ ├── evolution.ts # Dynamic Mode drift engine
│ ├── scene.ts
│ ├── composer.ts # Post-FX chain + auto quality tiers
│ ├── shaders.ts
│ └── __tests__/ # Engine unit + sync invariant tests
│ ├── helpers/song-clock.harness.ts
│ ├── song-clock.test.ts
│ ├── sync-invariants.test.ts
│ └── …
│ └── ui/ # label, sheet, slider, switch only
AGENTS.md # Agent guide (Cursor / Claude)
docs/
├── README.md # Documentation index
├── audio-analysis-pipeline.md
├── song-clock-and-sync.md
├── rendering-and-latency.md
├── fft-and-beat-detection.md # Short overview (links to above)
└── screenshots/
- Capture (
engine/audio.ts) — mic or tab →AnalyserNode(default FFT 2048) - Features — bass / mid / high / centroid each frame
- BeatMatcher — fused onset detection for accents and signal-change timing
- BPMDetector — tempo estimate + confidence (feeds SongClock, does not own grid after manual tap)
- SongClock (
engine/song-clock.ts) — authoritative bar grid,beatPhase, phrase boundaries - Consumers —
scene.ts, post-FX,BarTimingHud,view-cycle-controller
Deep dive: audio-analysis-pipeline.md, song-clock-and-sync.md, rendering-and-latency.md.
Three.js Scene (engine/scene.ts):
- 15 visualization modes: combo, classic, ripple, datastream, nebula, monolith, mandala, terrain, obsidian, torus, soundwall, geometrynebula, reztube (On-rails Tube), assetflow, stagelights
- InstancedMesh: Used for efficient bar/block rendering (bars, classic, monolith)
- ShaderMaterial: Custom shaders for effects (datastream, nebula, terrain)
- Post-processing: Bloom, chromatic aberration, glitch, god rays, lens flare, etc. via Composer
- Fat lines:
Line2/LineGeometry/LineMaterial(mandala ribbons) andLineSegments2/LineSegmentsGeometry/LineMaterial(On-rails Tube) fromthree/examples/jsm/lines/for screen-space pixel-width lines that respectlinewidthin WebGL
Each View Has:
buildXXX(size): Initialize geometry/materialsupdateXXX(dt, audio, opts): Per-frame animation driven by audio + settings
- Settings: 40+ properties controlling every aspect
- Presets: built-in curated looks plus a dynamic local save list
- Normalization: Enforces limits on amplitude, vignette, bloom
- Randomization: Smart background picker using WCAG contrast scoring
Recent controls:
postFxEnabled: Master switch that bypasses the post-processing pipeline at render time.- Global Wireframe control in the View panel: writes to the active view-specific wireframe setting.
randomizeViewSettings: Keeps randomize constrained to post FX by default; when enabled it also touches view-specific geometry and palette controls.lensFlare/lensFlareAmount: Anamorphic horizontal streak + coloured lens-ghost reflections layered after god rays in the composer chain. Enabled 45% of the time by randomize.monolithBrightness: Dedicated lighting/visibility control for Monolith independent of amplitude.reztubeLineWidth: Line thickness in screen-space pixels for the On-rails Tube view (0.5–6 px). Rendered viaLineSegments2+LineMaterial(worldUnits: false) so the width is invariant to camera distance. The visual's display label was updated to "On-rails Tube"; the internalidstaysreztubeto preserve saved settings.assetflowMovement: Global motion intensity multiplier for Asset-Flow model pathing, bounce, and spin.assetflowBackgroundDrift: Speed/intensity control for Asset-Flow's layered 2D background drift.assetflowSpriteAmount: Controls layered 2D background presence/intensity in Asset-Flow.- Asset-Flow actor motion is intentionally biased toward vertical movement with smoothed horizontal follow to prevent visual left/right flicker.
- Stage Lights view: a row of overhead spotlight beams (LOW/MID/HIGH-aligned by position,
stagelightsFixtureCount3/5/7) sweeping and fanning out on strong beats, plus an optionalstagelightsLasersfan of thin upward beams from a downstage projector. Per-fixture tuning (sweep speed/phase/amplitude/width/color) is a continuous blend across the LOW/MID/HIGH anchors by each fixture's position (stagelightsTent3inscene.ts) rather than a fixed 3-way lookup, so extra fixtures land between bands instead of repeating them. kaleidoscope/kaleidoscopeSides/kaleidoscopeAngle: mirrored radial wedge post-FX pass.mirrorFx/mirrorMode/mirrorOffset: directional mirror post-FX pass.crtFx/crtScanlineIntensity/crtCurvature/crtVignette: retro CRT post-FX pass.projectorFilmFx/projectorFilmAmount/projectorFilmJitter/projectorFilmFlicker: random film projector artifact pass.glitchIntensity: duty-cycle fraction (0-1) of each ~1 s window the Glitch pass actually renders — three.js's GlitchPass has no continuous intensity knob.evolveEnabled/evolveAmount: "Dynamic Mode" — a curated 1-3 settings per view drift slowly over musical phrases (wall-clock fallback when unsynced), bounded around the user's own values (engine/evolution.ts).- ControlPanel additions: dedicated Saves tab (list with focus/load/overwrite/delete via
usePresetActions), Dynamic Mode toggle, an amber warning banner in the Post FX tab when Performance Mode is bypassing the pipeline, retro/novelty FX grouped under a collapsedDisclosure, and a phone-first layout viauseIsMobile()(full-width sheet, bottom tab bar, full-screen flyout).
Key Settings Limits:
- Amplitude floor: 0.5 (MIN_VIEW_AMPLITUDE)
- Vignette amount: [0.5, 1.25]
- Bloom strength: ≤ 0.25 (unless bloomExtreme)
- Asset-Flow movement intensity: [0.35, 1.8]
- Stage Lights sweep speed: [0.2, 3]; fixture count snaps to 3/5/7
Current keyboard shortcuts are defined in Shortcuts.tsx and mirrored by the bottom shortcut bar:
−/+or=: Decrease / increase visual responseR: Randomize←/→: Prev / Next Visual (B/Vkept as aliases)[/]orShift+←/Shift+→: Prev / Next SaveX: Audio Source (stops the current audio engine so a new source can be selected)F: Toggle fullscreenN: Toggle Stats panel;Shift+N: Toggle full-page statsG: Show/Hide shortcut hintsA: Play Saves (save-list auto-cycle mode)S: Toggle settings panelT: Beat tap (manual SongClock sync)Shift+T: Downbeat tap (reset phrase to 1/16)M: Toggle BPM & bar grid HUDL: Toggle latency HUDC: Toggle music-reactive view cycle (every 4-bar phrase)1to5: Load save slotShift+1toShift+5: Save to slot
Shortcut reliability note:
- Keyboard shortcut handlers in
Shortcuts.tsxdispatch through a liveactionsRefso keys likeR/Balways execute the latest callbacks and don't drift when React state changes.
Shortcut rail clusters:
- Visual cluster: view count, Prev Visual (B), Randomize, View Cycle (C),
inc,fx, Next Visual (V) - Save cluster: save count, Prev, Play Saves, Next, Random, Save, Delete
- Panel cluster: Post FX, Scene, Settings (S), BPM Grid (M), Latency (L)
- Response panel: sensitivity slider, neutral reset and Auto balance
- Utility cluster: Audio Source (X), Fullscreen (F), Stats (N), FPS output, Beat Tap (T), Hide Controls (G)
When hidden (G), a shortcuts · g pill at the bottom restores the bar (or press G again).
Tooltip behavior:
- Shortcut buttons use a shared styled tooltip treatment rather than native browser
titlehovers. incexplains randomize scope: post FX only vs post FX + view settings.fxexplains the post-processing master pipeline state.
Settings panel and flyout behavior:
- The
Sshortcut toggles the panel even when focus is inside a text input/slider control. - Clicking the
Sshortcut button toggles open/close (no immediate re-open). - Flyout tabs (
Audio,Scene,Post FX) delay slightly on open and slide from the sheet edge.
- Add a visual manifest to
packages/engine/src/visuals.tsor a newsrc/components/analyser/visuals/*.visual.tsfile. - Create
buildXXX()andupdateXXX()inscene.tsand bind the manifestsceneGroupKeyto the owningTHREE.Group. - Add settings in
store.tsfor customization, including the...Fullscreenor...Wireframekeys referenced by the manifest when needed. - Add per-view controls in
control-panel/ViewSettings.tsx. Shell layout and flyout tabs stay inControlPanel.tsx. View labels, cycle order, and fullscreen wiring come from the visual registry automatically. - Verify the shortcut rail still reads correctly: current label,
3D/2Dstate, and visual cycling all depend on the registry metadata.
Runtime registry notes:
packages/engine/src/visuals.tsis the canonical visual registry used by the control panel, shortcuts, and XR view cycle.- Any
src/components/analyser/visuals/*.visual.tsmodule exportingvisualor a default object with anidis loaded at startup viaimport.meta.glob(..., { eager: true }). - Runtime manifests can override labels, settings labels, fullscreen keys, wireframe keys, scene group keys, and order without re-editing the UI lists.
- Rendering logic still lives in
scene.ts, so new runtime-picked visuals need a matching scene group and update branch before they can draw. - Review rule: if a visual manifest changes label/order/fullscreen metadata, check
Shortcuts.tsx, XR cycle behavior, and the View section inControlPanel.tsxtogether.
- Edit feature extraction in
engine/audio.ts - Update
AudioBandstype if adding new properties - Connect to visualization in
scene.tsupdate methods - Expose via settings in
store.tsif user-tunable
- Create slider in
control-panel/ViewSettings.tsxor a flyout tab inControlPanel.tsxusing<S>fromcontrol-panel/primitives - Bind to store setting:
set({ settingName: value }) - Ensure setting has entry in
DEFAULT_SETTINGSinstore.ts - Test in dev mode with
npm run dev
Notes for mapped/global controls:
- For global controls that map to view-specific settings (for example wireframe), keep UI mapping logic in
ControlPanel.tsxand preserve underlying per-view keys inSettings. - For master bypass toggles (for example post FX), apply the bypass in
hooks/useAnalyserEngine.tsrender flow so individual effect settings remain unchanged.
npm run test:run # Single run
npm run test # Watch modeAll tests live under __tests__/ (see vitest.config.ts):
| Location | Suites |
|---|---|
analyser/__tests__/ |
Analyser.regression, ControlPanel.regression, usePresetActions, theme, Shortcuts, store.*, visuals, BarTimingHud |
packages/engine/src/__tests__/ |
song-clock, sync-invariants, bpm-detector, bpm-detector.realistic, beat-matcher, latency-metrics, view-cycle-controller, latency-benchmark, loudness, evolution, composer, xr |
Shared fixtures: __tests__/helpers/test-helpers.ts, engine/__tests__/helpers/song-clock.harness.ts.
- sync-invariants.test.ts — manual anchor, misaligned audio drift guard, manual→auto release, phrase view cycle
- Analyser.regression.test.ts — render-loop TDZ guard (
useAnalyserEngine) - ControlPanel.regression.test.ts — ViewSettings delegation, per-view keys, theme swatches
- usePresetActions.test.ts — wrap/random helpers, slot cursor rules
- song-clock.test.ts — tap/auto modes, beat index math
- store.* — normalization, slots, randomize
- Shortcuts.test.tsx — rail UI and keyboard behavior
- latency-metrics.test.ts — signal latency only on transient frames; no stale timestamp growth
Asset documentation:
- Keep
docs/THIRD_PARTY_ASSETS.mdin sync whenever adding/removing runtime files underpublic/assets/. - Validate third-party model URLs and licenses before wiring new
model-xx.glbpaths inscene.ts.
- Place new files under
src/**/__tests__/(not beside source modules) - Import implementation with relative paths (
../store,../song-clock, etc.) - Use
vi.resetModules()and a localStorage mock for deterministic store tests - Test behavior through public APIs (
settingsStore,SongClock,BPMDetector) - For sync changes, extend
sync-invariants.test.tsandsong-clock.test.ts
Example:
it("enforces constraint X", async () => {
const { settingsStore } = await import("./store");
settingsStore.set({ myValue: 999 });
const state = settingsStore.get();
expect(state.myValue).toBeLessThanOrEqual(LIMIT);
});npm install # Install deps
npm run dev # Start Vite dev server (localhost:6789)Use Run and Debug → Launch Chrome (Spectrum Aura) (.vscode/launch.json). This starts Vite and opens Chrome at http://localhost:6789 with source maps.
Do not use Cursor's Simple Browser or a LAN IP URL for audio work — getUserMedia / getDisplayMedia need a secure context (localhost or HTTPS).
npm run build # Builds to dist/
npm run preview # Preview production build locally- Large FFT size (4096) = better frequency resolution but higher latency
- Instanced meshes (bars/monolith) render efficiently with 1000+ objects
- Post-FX (bloom) can be expensive; tune
bloomStrengthandbloomRadius - Point clouds (datastream) limited to ~30k points for 60fps on typical hardware
- TypeScript: Strict mode, full type coverage
- React: Functional components, hooks (useSyncExternalStore for store)
- Three.js: Use
THREE.namespace prefix consistently - Shaders: Documented with
/* glsl */comment, uniform names prefixedu - Settings: Always add to
DEFAULT_SETTINGSand set sane limits
- Keep documentation inside
docs/unless it is a top-level product artifact (README.md,AGENTS.md, license, tooling config). - Keep tests in
__tests__/folders; Vitest only includessrc/**/__tests__/**/*.{test,spec}.{ts,tsx}. - Agents: read AGENTS.md before changing sync or render-loop code.
- Prefer concern-specific test files (
store.normalization.test.ts,store.slots.test.ts) over catch-all files. - When adding a new setting, update all required layers in one change set:
Settingstype +DEFAULT_SETTINGSinstore.ts- pass-through in
hooks/useAnalyserEngine.tsandScene.update()options - controls in
control-panel/ViewSettings.tsxorControlPanel.tsxflyouts (if user-facing) - at least one targeted test
- When changing shortcut-bar behavior, update all three together:
- keyboard handling + rail UI in
Shortcuts.tsx - tooltip copy / active-state affordances in the same file
Shortcuts.test.tsxexpectations
- keyboard handling + rail UI in
- Mobile: Not optimized for touch, works best on desktop
- Audio: Requires user gesture (click) to start, due to browser autoplay policy
- Cross-origin: Tab audio capture only works on same-origin (security)
- Browsers: WebGL 2 required (most modern browsers)
- Enable stats panel: Press
Nfor live FPS/draw calls/memory - Visual debug: Add
console.log()inscene.update()to inspect audio values - Settings debug: Store uses localStorage; inspect via DevTools
Application > Storage > Local Storage - Shader errors: Check browser console for THREE.WebGLProgram validation errors
- Performance: Use DevTools Performance tab, look for GPU bottleneck vs CPU
Render-loop safety note:
- In
hooks/useAnalyserEngine.ts, initialize render-loop state variables before deriving values from them. A prior TDZ bug (Cannot access 'displayedView' before initialization) occurred when a composer reset key was built beforedisplayedViewwas declared.
- Keep changes scoped (one feature per PR)
- Update relevant tests when adding features
- Verify build passes:
npm run build - All tests must pass:
npm run test:run
See response and balance for the feedback controller
and default/persistence contract. visual-response.test.ts verifies that gain
never modifies detector input or timing. visual-balance.test.ts covers sustained
brightness/activity, silence, bounds and reset. visual-balance-monitor.test.ts
checks readback cadence, backoff and failure handling.
Shortcuts.test.tsx covers response keys, limits, typing/browser-zoom exclusions,
VR hints and tooltip survival through live updates. store.normalization.test.ts
checks legacy defaults and a persisted Auto balance opt-out. Saved-look comparison
lives in saved-look.ts; use usePresetActions for both toolbar and settings flows.
preset-state.test.tsx and store.slots.test.ts cover action availability and
calibration preservation when loading saves.
StatsPanel.test.tsx covers panel layout and persistence. stats-history.test.ts
checks graph smoothing/ranges. Panel primitives must remain stable across live
updates so focused controls and open tooltips are not remounted.