Perlines v2: browser audio visualizer (WebAudio + WebGL2) #1

Merged
aqua merged 7 commits from web-v2 into main 2026-11-10 17:08:39 +00:00
Member

Adds web/: a zero-install static visualizer. File + mic input, band-split dB-flux onset detector, chroma/Krumhansl key estimate that tints the scene, three scenes (aurora disk, burning water, vector trails). Fresh code, ideas only from the 2013 Aethervis work; no GPL Vamp code.

Verified: cd web && npm test (10/10 synthetic clicks at two hop sizes, no false positives; silence/noise/tone give none; key reads C, Am, Bm on synthesized progressions). All three scenes render under headless Chrome (SwiftShader) with no console errors.

Not verified: no human has watched this run. No real-GPU frame-time number exists (web/BENCH.md has a software-rasterizer run only; burning water is 46 ms there and is the scene to check on a GPU). Mic path, real music, real display untested.

The existing Fensalir C# client is untouched; README front door now points at both.

Onset detector, update (commit 908e8dc). Rewritten against a deterministic synthetic bench, web/tools/onset-bench.mjs (npm run bench:onsets, about 30 s, no network). Corpus: 81 synthesized signals of 14 s (kick/snare/hat patterns at 90/120/150 bpm, three master levels 0/-20/-40 dB, three beds: none, bass drone + pink noise, louder). Scored at the page's 30 fps rate (hop 1600 at 48 kHz) and at full rate (hop 512). An onset counts as hit when a detection lands within -20..+90 ms of the frame-window end; coincident kick+snare count as one onset.

hit rate false positives median timing
old detector, 30 fps 47.8% (2611/5463) 95 (5.3/min) 42.7 ms
new detector, 30 fps 87.3% (4771/5463) 0 34.3 ms
old detector, full rate 47.2% (2578/5463) 51 (2.8/min) 18.7 ms
new detector, full rate 93.9% (5130/5463) 0 16.0 ms

The old detector's hit rate fell with level (-40 dB: 19.9% at 30 fps); the new one is flat across levels (87.2-87.5%). On the page's own DemoFeed at 30 fps (20 s): old 24/37 kicks, new 37/37 (and 74/74 quarter-beat events vs 24/74). That feed is synthetic and written by the same author as the detector, so it flatters.

Costs: the new detector's median timing at 30 fps is 8 ms worse than before it looked; it reads two previous frames, so a transient is picked a frame later. Remaining misses are mostly kicks under a loud bass bed and 16th hats at 150 bpm.

Still not verified (updated): all of the above is synthetic. The corpus was adjusted while it was built (recorded in the commit message), the tuning parameters were chosen on the same corpus they are scored on (no held-out set), and the test floors use a one-third subset of it. Nobody has heard this; no real music has been run through the detector; no real-GPU bench; no human has watched any scene. The numbers say nothing about whether onsets will feel right.

🤖 Generated with Claude Code

Adds `web/`: a zero-install static visualizer. File + mic input, band-split dB-flux onset detector, chroma/Krumhansl key estimate that tints the scene, three scenes (aurora disk, burning water, vector trails). Fresh code, ideas only from the 2013 Aethervis work; no GPL Vamp code. **Verified:** `cd web && npm test` (10/10 synthetic clicks at two hop sizes, no false positives; silence/noise/tone give none; key reads C, Am, Bm on synthesized progressions). All three scenes render under headless Chrome (SwiftShader) with no console errors. **Not verified:** no human has watched this run. No real-GPU frame-time number exists (`web/BENCH.md` has a software-rasterizer run only; burning water is 46 ms there and is the scene to check on a GPU). Mic path, real music, real display untested. The existing Fensalir C# client is untouched; README front door now points at both. **Onset detector, update (commit 908e8dc).** Rewritten against a deterministic synthetic bench, `web/tools/onset-bench.mjs` (`npm run bench:onsets`, about 30 s, no network). Corpus: 81 synthesized signals of 14 s (kick/snare/hat patterns at 90/120/150 bpm, three master levels 0/-20/-40 dB, three beds: none, bass drone + pink noise, louder). Scored at the page's 30 fps rate (hop 1600 at 48 kHz) and at full rate (hop 512). An onset counts as hit when a detection lands within -20..+90 ms of the frame-window end; coincident kick+snare count as one onset. | | hit rate | false positives | median timing | |---|---|---|---| | old detector, 30 fps | 47.8% (2611/5463) | 95 (5.3/min) | 42.7 ms | | new detector, 30 fps | 87.3% (4771/5463) | 0 | 34.3 ms | | old detector, full rate | 47.2% (2578/5463) | 51 (2.8/min) | 18.7 ms | | new detector, full rate | 93.9% (5130/5463) | 0 | 16.0 ms | The old detector's hit rate fell with level (-40 dB: 19.9% at 30 fps); the new one is flat across levels (87.2-87.5%). On the page's own `DemoFeed` at 30 fps (20 s): old 24/37 kicks, new 37/37 (and 74/74 quarter-beat events vs 24/74). That feed is synthetic and written by the same author as the detector, so it flatters. Costs: the new detector's median timing at 30 fps is 8 ms worse than before it looked; it reads two previous frames, so a transient is picked a frame later. Remaining misses are mostly kicks under a loud bass bed and 16th hats at 150 bpm. **Still not verified (updated):** all of the above is synthetic. The corpus was adjusted while it was built (recorded in the commit message), the tuning parameters were chosen on the same corpus they are scored on (no held-out set), and the test floors use a one-third subset of it. Nobody has heard this; no real music has been run through the detector; no real-GPU bench; no human has watched any scene. The numbers say nothing about whether onsets will feel right. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Spectral-flux onset detector (max-filtered reference, median threshold,
refractory) and peak-based chroma with Krumhansl profile key estimate.
Own FFT; no external dependencies.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Aurora disk (spectrum-to-mesh extrusion), burning-water spray (stateless
onset-driven particles), vector trails (Perlin curl paths). File, mic and
synthetic demo inputs; frame-time HUD and ?bench mode.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Replaces the log1p-compressed whole-spectrum flux with four-band flux in dB,
a two-frame max-filtered reference, a per-band median+8 MAD threshold with a
per-band minimum rise, and one refractory period. Level-independent.

tools/onset-bench.mjs synthesises kick/snare/hat patterns with known onset
times (3 patterns x 3 tempos x 3 levels x 3 beds = 81 signals), scores hit
rate, false positives and median timing at 30 fps and 512-sample hops.
tools/onset-legacy.mjs freezes the old detector for the before numbers.

Synthetic only. Nothing was listened to; no real music, no real GPU.
Corpus adjustments made after the first baseline run: coincident kick+snare
count as one onset; voices end in a 60 ms fade because truncation clicks read
as onsets in the near-silent gaps; hats raised relative to the snare tail.
Baseline was rerun after each change.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Author
Member

Maintainer pass (2026-11-10). Diff is additive under web/ plus a README edit; merges into main without conflict and touches nothing of the C# Fensalir client. web/test/dsp.test.mjs + onset.test.mjs: 7 passed (note: npm test runs only dsp.test.mjs). Falsification by reading: needs WebGL2 only (guard present, message shown otherwise), RGBA8 feedback targets (no float-texture or colour-buffer-float extension to be missing), optional EXT_disjoint_timer_query. NEVER RUN ON A REAL GPU: BENCH.md numbers are SwiftShader software rasterisation only (water scene 46 ms p50 there, the one to watch at 4K/high-dpi) and no human has watched it. Failure risks on a real GPU: frame-time budget for the water scene (3x fbm per pixel), shader precision on mobile, trail ghosting from 8-bit persistence. None blocks main: it is a self-contained static folder behind README text that already says unwatched. Merging with that caveat.

Maintainer pass (2026-11-10). Diff is additive under web/ plus a README edit; merges into main without conflict and touches nothing of the C# Fensalir client. `web/test/dsp.test.mjs` + `onset.test.mjs`: 7 passed (note: `npm test` runs only dsp.test.mjs). Falsification by reading: needs WebGL2 only (guard present, message shown otherwise), RGBA8 feedback targets (no float-texture or colour-buffer-float extension to be missing), optional EXT_disjoint_timer_query. NEVER RUN ON A REAL GPU: BENCH.md numbers are SwiftShader software rasterisation only (water scene 46 ms p50 there, the one to watch at 4K/high-dpi) and no human has watched it. Failure risks on a real GPU: frame-time budget for the water scene (3x fbm per pixel), shader precision on mobile, trail ghosting from 8-bit persistence. None blocks main: it is a self-contained static folder behind README text that already says unwatched. Merging with that caveat.
aqua merged commit 97e539f3b9 into main 2026-11-10 17:08:39 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
GameCult/Perlines!1
No description provided.