Stage 0: extract pure DSP (tested) + split dashboard.js by feature#1
Merged
Conversation
…modules GAP 1 (extract + test pure DSP): - New single-source dashboard-dsp.js (window.AbxDsp / globalThis) holding stabilize, resample, and a purified audioPhase(clk, nowMs). One body, consumed by the browser and by a new src/dsp.test.ts (no TS port, so no copy to drift); dashboard-dsp.d.ts gives the type contract. - 12 new cases: stabilize silence->null / sine->locks ~2 cycles / kick->raw / noise->raw; resample length+endpoints+interp+degenerate; audioPhase null/fractional/advance/lead. - Test runner widened to dist/*.test.js. 19 tests pass. GAP 2 (split the 672-line dashboard.js by feature): - Core shell + five modules behind namespaces: AbxScope (oscilloscope), AbxCheats (cheat sheet + sample browser), AbxCurves (modulation curves), AbxSeq (step grid + patterns + song), plus a shared Abx core surface (state/send/cmd/poll/color/esc/fnum/ clock.phase). clock.phase is a one-line wrapper over AbxDsp so the math stays testable. - dashboard.ts now serves any /dashboard-*.js. Core 672 -> 334 lines; every module <200. Behaviour-preserving: - One click/input/change dispatcher delegating to AbxX.handleClick/handleInput; branches stay mutually exclusive (distinct data-attrs), so dispatch order is unchanged. - Deep-link bootstrap deferred to DOMContentLoaded (calls now cross files). - Core render loop guards window.AbxSeq/AbxCurves against the inter-script load race. - Verified via a headless smoke test (panels + #steps/#cheats deep-links, 0 console errors).
There was a problem hiding this comment.
Pull request overview
This PR is a Stage 0 refactor that (1) extracts the dashboard’s DSP math into a single shared JS implementation with Node unit tests, and (2) splits the previously-monolithic dashboard UI script into a small core plus feature modules that consume a shared window.Abx surface. It also updates the dashboard HTTP server to serve any dashboard-*.js module from disk.
Changes:
- Add
dashboard-dsp.jsas the single-source DSP implementation and introducesrc/dsp.test.tsto unit-test it under Node. - Split
dashboard.jsinto feature modules (dashboard-scope.js,dashboard-cheats.js,dashboard-curves.js,dashboard-seq.js) and load them fromdashboard.html. - Update the dashboard server route to serve
dashboard.jsanddashboard-*.js, and broaden the test runner to execute all compiled*.test.jsfiles.
Reviewed changes
Copilot reviewed 10 out of 11 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| mcp/src/dsp.test.ts | Adds Node unit tests that import the browser DSP script for side effects and validate stabilize/resample/audioPhase. |
| mcp/dashboard-dsp.js | New single-source DSP implementation shared by browser and Node tests via globalThis.AbxDsp. |
| mcp/dashboard-dsp.d.ts | Adds a type contract for the global AbxDsp surface. |
| mcp/src/dashboard.ts | Expands static serving to include dashboard-*.js modules. |
| mcp/package.json | Updates npm test to run all compiled test files. |
| mcp/dashboard.html | Loads DSP + core dashboard + feature modules as separate scripts. |
| mcp/dashboard.js | Shrinks core dashboard shell and delegates features to AbxScope/AbxCheats/AbxCurves/AbxSeq. |
| mcp/dashboard-scope.js | Moves per-channel scope rendering and its rAF loop into a dedicated module. |
| mcp/dashboard-cheats.js | Moves cheat sheet + sample browser into a dedicated module. |
| mcp/dashboard-curves.js | Moves modulation curves UI and paint listeners into a dedicated module. |
| mcp/dashboard-seq.js | Moves sequencer/pattern/song logic and grid input handlers into a dedicated module. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…l beat selftest.ts binds a Meter on the master-meter UDP port and samples L/R across the 8s phase-B beat, asserting peak(L+R) > 0.05 — well above the noise floor, ~10x below a normal beat, so only a fully dead chain fails. On failure it dumps the sclang + tidal tails and exits non-zero. peakL/peakR are tracked and reported separately to expose a one-channel / mono-summing regression (diagnostic, not asserted). Stays a local-only gate (real SC + WASAPI): run via `npm run selftest`, never in CI. Engine surfaces no meter, so selftest binds Meter (meter.ts) directly on the UDP port (free since selftest runs standalone).
Closes a silent-green hole where `node --test dist/*.test.js` returns exit 0 with zero tests run when the glob matches nothing (e.g. unbuilt dist/). The new runner discovers tests via fs, fails loudly on zero-match, and enforces a test-count floor so future regressions in test discovery cannot pass silently. Investigated after a Copilot review flagged shell glob expansion on Windows cmd.exe. Node 21+ expands the glob itself so cmd was not the active issue, but the zero-match silent-green was. Copilot's suggested `node --test dist` directory form errors with 'Cannot find module' and was rejected as broken.
The loop/flush/render paths (added in the Stage 0 split) guard cross-namespace calls with `if(window.AbxX)`, with a comment claiming resilience if a module fails to load. The four dispatcher functions (click/input/change/keydown) and the deep-link bootstrap did not carry the same guards, so a missing module would throw ReferenceError on first relevant event and kill the entire dispatcher chain (including downstream core branches). Guards every cross-namespace dispatcher reference to match the loop pattern. The resilience property the comment promised is now delivered. Caught by Copilot review on PR #1 (threads 1 & 2).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stage 0: foundation — pure DSP extracted + tested,
dashboard.jssplit by feature, selftest verifies audioCloses all three Stage 0 gaps. Two commits:
d16633a— GAP 1 (extract + test pure DSP) and GAP 2 (splitdashboard.js)a79a2e5— GAP 3 (selftest asserts master-meter activity)What changed
stabilize/resample/audioPhaseextracted into a single-sourcedashboard-dsp.js(window.AbxDsp,globalThis.AbxDspunder Node). One body — the browser and the newsrc/dsp.test.tsimport the same file, so there is no TS copy to drift;dashboard-dsp.d.tsis the type contract.audioPhasepurified to(clk, nowMs). 12 new tests; suite is 19/19.dashboard.jssplit into a core shell + five feature modules:AbxScope,AbxCheats,AbxCurves,AbxSeq, and a sharedAbxcore surface (state/send/cmd/poll/color/esc/fnum/clock.phase).dashboard.tsserves any/dashboard-*.js. Core 672 → 334; every feature module < 200.selftest.tsnow binds aMeteron the master-meter UDP port, samples L/R across the 8s phase-B beat, and assertspeak(L+R) > 0.05; on failure it dumps sclang + tidal tails and exits non-zero. Local-only (npm run selftest), never CI.Standing review
1. Karpathy / process. Design alternatives were surfaced before code (the namespace gate presented both the literal four-file split and the core+features cut with concrete line budgets; you chose the latter). Each of the six steps was a surgical, behaviour-preserving move, verified before the next.
2. Tests. 12 new cases for the pure DSP, behaviour-focused (sine locks to ~2 cycles vs kick/noise return raw; resample endpoints+interp+degenerate; audioPhase null/fractional/advance/lead-compensation).
npm testran green before and after every step. Single source means no JS/TS drift. GAP 3 adds the live audio gate (local-only) so a silently dead chain fails loudly.3. Architecture. Core is 334 lines — the irreducible shell (render loop, the four dispatchers, master meters, transport, console). Intentionally over the 200 soft cap, as agreed; chasing sub-200 would mean shredding it into ~8 modules, a separate effort. Feature modules: dsp 28, scope 47, cheats 149, curves 91, seq 134. No duplication.
4. Security. No new user-controllable strings reach SC/Tidal. The new static-file route is regex-restricted (
/^\/(dashboard(?:-[a-z0-9-]+)?\.js)/i— lowercase alnum + dash, ends.js, no slashes/dots), so it cannot path-traverse. Host/Origin guards untouched. No new persisted state.5. Concurrency / lifecycle. No new long-lived timers: the existing 80ms debounce and rAF loops are reused;
AbxSeqowns its row/swing queue, drained viaflushPending()from the same interval. Each feature self-registers its own listeners. The core render loop guardswindow.AbxSeq/AbxCurvesso it can start before the later scripts parse and survives a module failing to load. selftest binds +stop()s its own meter socket (unref'd).6. Performance. Render-loop work is delegated, not added; guards are cheap truthy checks. No new poll endpoints or SSE streams.
7. Comments. Hard-won fixes carry the WHY: the load-race guard rationale, the noise-seed choice (a low-bit LCG faked a tone — mulberry32 instead), the DOMContentLoaded deferral, the
clock.phasepurity note, and the selftest threshold rationale (why 0.05).8. UX regressions. Dispatch branches are mutually exclusive (distinct data-attrs), so reordering them into delegates changes nothing. Listener-order note: the curve-paint vs seq-paint
pointerdownregistration order is restored to the original (curves loads before seq), and the two act on mutually-exclusive targets (.curvecvvs.pad), so order is immaterial regardless. No keyboard shortcut or debounce threshold changed.9. README / docs. No user-facing features added (pure refactor + a test), so the feature list, env-var table, and MCP tool list are unchanged.
10. Next-stage readiness. Stage 1 (mixer) can reuse
AbxScope.draw(exposed) for mini-scopes,Abx.colorfor per-channel colour,Abx.state().scopesfor meters, and theAbxX.handleClickdelegate pattern. Harder than expected: the inter-script load race affected the core loop's calls into later-loaded namespaces, not just the deep-link — now guarded. No known debt carried into Stage 1.Manual test checklist
Verified headlessly (Edge + puppeteer-core, 3 consecutive runs, zero console errors):
#stepsand#cheatsdeep-links open via the newDOMContentLoadedbootstrapNeeds your ears + hands (audio + drag gestures I can't drive headlessly):
npm run selftest(standalone — kills any live engine, makes ~8s of audio) printsDONE — audio verified.