Commit Graph

15 Commits

Author SHA1 Message Date
Dejvino
045ba6a322 Add five visualizers to the scene library 2026-08-05 20:55:00 +02:00
Dejvino
7d59ef8e5f Add Block Mosh, Neon City and Flora visualizers
Three new scene shaders to widen the library, now that the main families are
settled. Each is a shader plus a params block, registered in the single
source of truth (scenes/registry.js), so casting, UI, arc and checks pick
them up automatically.

- Block Mosh (glitch): a datamosh. Blocks pull the feedback buffer along
  per-block strokes on a bar-quantized grid so the corruption steps like an
  edit rather than crawling, and spills on onsets. Fills the most iconic
  gap in the glitch family.
- Neon City (structural): a receding skyline with instanced lit windows,
  distinct from the rolling ridgelines and the road grid it sits alongside.
- Flora (organic): an abstract plant of swaying stalks, teardrop petals and
  a bloom corona, all stamped in the track's signature shape.

Also adds HOWTO-visualizers.md, a quick reference for building the next one.
2026-08-05 20:31:05 +02:00
Dejvino
ca5a68eb84 Phases 8 and 9: shots, and a production design per track
Both phases come out of the manual gate — watching whole tracks — and both
fix something no automated check was looking for.

Phase 8: shots. A section is a STAGE of the song and can run ninety
seconds; one scene held that long reads as a still image with a wobble on
it. Each section kind now gets a roster of three or four stage visuals
instead of one scene, and each section is cut into shots that rotate
between them on phrase lines, never holding past 22s. The roster stays per
kind, so a track's drops still cut between the same images and the video
keeps its identity; the anchor opens each section and the rotation returns
to it, and when a companion is due it is the least recently shown one.

The arc driver stopped working in sections and started working in cues, one
per shot, so a shot cut and a section change take the same code path and
differ only in transition length. The default transition is a slow
dissolve — two bars calm, one loud; a straight cut is reserved for
sections above the energy threshold, because on calm material a cut reads
as a glitch rather than as an edit.

Phase 9: production design. With cuts every fifteen seconds the next
problem was that the images being cut between shared nothing but the
palette. What a music video actually shares across shots is a location, a
cast, a camera operator and an art direction, so each track now generates a
personality in four traits (shape, camera, space, style) off the look seed.
The traits reach shaders as uniforms plus four helpers in the contract, and
each scene expresses them its own way: Classic Wave's rings take the
signature polygon, Metaballs merge as one, Floating Geometry no longer
picks between a box and a circle because the production already decided.

The part that makes it a design rather than a filter: scenes DECLARE which
traits they honour, a track is built on one or two, and a scene that does
not honour all of them is not cast in that track. The library shrinks per
track on purpose.

Two gates keep the declaration honest — lint greps each shader for evidence
of every trait it claims, and a render check measures that each declared
trait actually moves the image (41 scene/trait pairs, weakest response 64
of 255). A layer with no personality renders bit-identically to before,
which is what keeps every earlier sweep and regression valid.

Checks changed rather than added:
  - P4 scene-change and drift checks now measure per shot, not per section;
    the crossfade check reads its length off the cue.
  - P5 flash sweep runs per shot, so the visuals that only appear
    mid-section are measured too.
  - P6 preview/export parity primes first (as both real paths do) and
    compares at the one-LSB tolerance Phase 7 already uses. Measured over
    four consecutive shows: 3 frames at delta 1, then bit-exact — GPU
    variance on first render, not a divergence.
  - P2's contract-uniform list is derived from the contract instead of
    retyped, so the signature uniforms cannot fall out of sync.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 20:04:33 +02:00
Dejvino
9d15c3cf49 Phase 7: grow the scene library to 16
Ten new scenes, with 'minimal' first: the family was empty, so intros and
breakdowns fell through to flow/organic and every track opened at full
density. Quiet sections now land on a restful family 48/48 times across 24
seeds, 28 of them minimal.

New: Horizon Lines, Spectrum Sculpture, Slow Orb (minimal); Curl Flow
(flow); Plasma Bloom, Metaballs (organic); Kaleido Tunnel, Moiré Grid
(geometric); Ridge Terrain (structural); Scan Tear (glitch).

Four real bugs, three of which the existing gates could not have caught:

1. SHADER PROGRAMS LINK ASYNCHRONOUSLY. three.js uses
   KHR_parallel_shader_compile, so draws against an unlinked program render
   wrong. The heaviest scene had its first TEN frames differ from every
   later render of the same frames. Preview hides this entirely; export
   renders each frame once, so those frames would ship broken. Added
   Engine.prime() — WebGLRenderer.compile() plus a discarded warm frame —
   and the exporter now primes before encoding. Rendering a throwaway frame
   and reading it back is NOT sufficient; measured, it left 3-5 frames wrong.

2. Moiré Grid declared a param on u_width, which the shader contract already
   uses for stereo width. GLSL redefinition, and the only symptom was a
   black frame. Lint now rejects any param uniform colliding with the
   contract.

3. Spectrum Sculpture strobed at 4 flashes/s. Two causes: rotation measured
   in turns meant bar-crossing frequency was bars x rate (82 bars put a
   slow-looking 0.12 turns/s at 10 Hz), and hard band-tier boundaries made
   every bar switch band simultaneously. Rotation is now in segment units so
   the rate IS the crossing frequency, bands interpolate, and the range is
   capped where the flash meter measures zero.

4. Particle Field was being chosen as a primary background despite being
   mostly empty by design. Scenes now declare role: 'accent'; those are
   never primary and are judged on variance rather than luminance.

Three checks were themselves wrong and were rebuilt: mean-distance metrics
unfairly fail sparse scenes for being tasteful rather than static, so
"animates" and "no duplicates" now use max channel delta.

PLAN.md §1 gains two refinements: programs must be primed before the first
frame, and even same-machine the heaviest shaders vary by one LSB under
differing GPU load — so the per-scene criterion is max delta <= 1 rather
than an identical hash. A real bug scores in the tens there.

Full suite 67/67 across all seven phases. Worst 4K frame 3.8ms,
worst flash rate 0/s, worst determinism delta 1/255. Adds README.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 18:40:56 +02:00
Dejvino
8a94a3f3a5 Phase 6: preview UI and export
Full preview surface plus WebCodecs export, both driving the same Show —
the exporter has no render path of its own, which is what makes parity
structural rather than something to keep in sync.

Preview: transport with section jumping and looping, drag-scrub, timeline
strip showing sections coloured by kind with bar ticks and lock state,
generated param controls, reroll (whole track or one section), lock,
draft/full quality, debug HUD, click-track download, and a 20s test render
at full export quality.

Export: probes for a supported H.264 config, warms up before a mid-track
range so the first frame carries the feedback state continuous playback
would have given it, and encodes audio from the decoded PCM.

One real bug found by the gate: AAC is absent from Chromium builds without
proprietary codecs, which still ship H.264 encoding — so video succeeded
and audio killed the whole export with "Cannot call 'encode' on a closed
codec". The exporter now probes AAC then Opus, and a failure mid-encode
degrades to video-only rather than losing a long render. Fallbacks are
surfaced in the UI; a video that quietly lost its audio is worse than one
that says so.

Also adds a dev-only window.__flowState handle. The render loop is
rAF-driven and rAF does not fire in headless/automated contexts, so this
provides a way to step the app by hand.

Gate 10/10 (one manual: upload a test render to the real platform once
before trusting a full export). Real mp4s verified — ftyp box, honoured
frame ranges, resolution restored, cancellation clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 18:20:36 +02:00
Dejvino
40188493ac Phase 6 WIP refactor: migrate track analysis UI from overlay to sidebar header 2026-08-05 12:39:44 +02:00
Dejvino
d0f93e5c55 Phase 6 WIP working 2026-08-05 12:20:55 +02:00
Dejvino
968101c532 Phase 6 WIP 2026-08-05 11:56:52 +02:00
Dejvino
59180948d0 Phase 5: compositing depth ("C" complete)
Multi-layer stacks with blend modes, feedback, post chain, and a 3D
particle layer proving the compositor is genuinely hybrid. Looks now
generate accent layers from a different family, composited additively at
low opacity, weighted by section energy so intros stay sparse.

Adds flash-rate safety (engine/flash.js), which was not in the original
plan and should have been. This generates beat-reactive video for
publication, and rapid light-dark cycling is the photosensitive-epilepsy
trigger; WCAG 2.3.1 caps it at three flashes per second. Classic Wave
measured 7-8/s at every output resolution from 96x54 to 1920x1080, so it
was a real hazard rather than a sampling artefact.

Root cause was general, not one bad shader: `u_time * u_speed` where speed
is reactively modulated. Phase is elapsed*rate, so changing the rate at
time T jumps phase by T*delta — sixty seconds in, a 0.05 wobble throws the
phase three whole units between consecutive frames, and it worsens as the
track runs. Fixed by introducing rate params:

- schema flag `rate: true` documents and marks them
- Layer.resolveParams skips reactivity on them
- ArcDriver skips drift on them
- validateModule rejects a reactive entry on one
- lint-scenes greps shaders for `u_time * u_X` and fails if X is unmarked,
  so no future scene can reintroduce it

Every rate param across the six scenes is now marked. Two checks were
needed to find this: a per-look flash check, and a per-SCENE sweep at
aggressive params, since the look generator only samples part of the space
and a scene can hide an unsafe region for a long time.

Gate 10/10. Worst flash rate now 1/s. Feedback stable over 10,000 frames
(luminance 0.17-0.59, no saturation or decay). 0.17ms/frame at 1280x720.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 11:38:26 +02:00
Dejvino
5f25437b89 Phase 4: arc driver ("C" brain)
Three timescales now stack: per-frame reactivity, per-section seeded LFO
drift, and whole-song scene changes with lookahead. Layer instances are
cached per section and reused across crossfades — rebuilding them per frame
would recompile shaders every transition.

Crossfades run forward from a boundary: the outgoing scene holds while the
incoming one fades in over it.

Three real bugs, each found by a check that had to be rewritten first:

1. A pop exactly at every transition. buildSlope is discontinuous by
   construction (~1 before a boundary, 0 after), and the outgoing layer is
   still on screen when it flips — collapsing its lookahead ramp in one
   frame. It now holds the slope it had entering the boundary.

2. FeatureTrack.at() returns a REUSED row object, and _boundarySlope()
   called at() again mid-render, rewriting the features the layer was about
   to read. Symptom: a frame correct on every repeat and wrong the first
   time — invisible to fresh-vs-fresh comparison, and wrong in every export,
   since export renders each frame exactly once. Now indexes the typed array
   directly, with the aliasing hazard documented on at(), and a new check
   covers the whole bug class.

3. Warm-up converged to 1%, leaving a visible 0.015 difference at heavy
   feedback settings. Now targets 0.1%.

Two checks were themselves wrong and were rebuilt: a raw delta threshold
and an outlier-vs-local-median test both flag beat flashes as pops, and a
control window taken from a different scene reads an ordinary busy scene as
a 9x spike. The working formulation A/Bs each boundary against the interior
of the two scenes adjacent to it.

PLAN.md §6 corrected: boundary seeks are NOT exact for free. Layer state is
re-seeded there but the feedback buffer is global and carries across.
Clearing it at boundaries would buy exactness for a visible flash at every
transition; warm-up is the better trade and applies everywhere.

Gate 9/9.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 11:30:50 +02:00
Dejvino
022c267888 Phase 3: look generation ("A" complete)
A track now yields a complete, coherent look with no input: palette,
per-section scene assignments, parameter sets, post and feedback settings.
Seeded from a hash of the decoded PCM, so a file always renders identically.

- palette.js builds in OKLCH, not HSL. HSL lightness is not perceptual, so
  evenly-stepped HSL palettes have colours that vanish and colours that
  dominate — which matters when nobody is supervising the choice.
  Regenerates until the contrast floor is cleared.
- Scenes are assigned per section KIND, not per section: a track's drops
  share a scene and the video reads as one piece instead of a shuffle.
- Family preference per kind keeps breakdowns off strobing glitch scenes.
- Section bias (energy/density/motion) carries track character into params
  without scenes knowing anything about audio.
- PaletteSource is the seam for cover art later; no scene would change.

Gate 9/9, including the look-space spread measurement (mean pairwise
distance 0.168 against a 0.08 floor) — the one check that catches a
generator that is deterministic and valid but visually collapsed.

Known gap, not a regression: all four battery tracks currently choose the
same two scenes. There are no 'minimal' family scenes yet, so intro and
outro sections fall through to flow/organic. Differentiation is presently
carried by palette alone. Phase 7 grows the library to fix it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 11:18:37 +02:00
Dejvino
c9b80308d2 Phase 2: param schema, auto-binding, generated UI
All five party-stage shaders now ported. Each declares its params
declaratively; uniform binding, UI controls, seeded sampling and arc
automation all derive from that one block, so a new scene costs a shader
and a schema and nothing else.

Port changes: LED-grid masks stripped, hardcoded colours replaced with
palette lookups, magic numbers lifted into params. Psychedelic Drift's
internal 15-second scene timer removed — keeping the visuals moving is the
arc driver's job, and it knows where the song's real transitions are.

ParamPanel generates controls from the schema alone and knows about no
specific scene; a hand-written control would be a bug.

Gate 8/8: schemas valid, uniforms accounted for in both directions, all
scenes compile and render, 128 range-sweep frames with no black, blown or
flat results, every param exposed in the UI, edits clamped, presets
round-trip and survive schema drift.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 11:14:04 +02:00
Dejvino
cb320557e6 Phase 1: offline audio pipeline
Whole-track analysis into a frame-indexed FeatureTrack. Nothing reads a
live AnalyserNode: realtime preview maps currentTime to a frame index,
export counts frames, both read the same rows.

- fft.js: radix-2 with precomputed tables, allocated once per track
- analyze.js: STFT at hop 1/60s with CENTRED windows (a window that starts
  at the timestamp reports energy arriving up to 23ms later, which reads as
  visuals lagging the music). Energy features normalised against the track's
  own percentiles; absolute stats kept in summary for the look generator.
- tempo.js: autocorrelation + grid F-measure, beat grid, downbeats
- segment.js: self-similarity novelty, boundaries snapped to the bar grid
- FeatureTrack: assembles everything, plus the lookahead fields. buildSlope
  rises through the bars leading into a higher-energy section, so a build can
  ramp into the drop rather than react after it lands.
- clicktrack.js: mixes clicks onto the detected grid for validation by ear

Three real bugs found and fixed by the tests:
- 174 BPM read as 87. Mean-energy-per-beat scores a half-tempo grid
  identically to the true one; only an F-measure penalises the missed
  onsets via recall.
- 90 BPM read as 180. Offbeat hi-hats make a double-tempo grid score
  perfectly on both precision and recall, so the grid is now interpreted
  metrically afterwards: a systematic strong/weak alternation means the
  real beat is every other grid point.
- Beat grid drifted ~30ms over 30s from integer-frame offsets. Onset peaks
  are now parabolically interpolated and the grid least-squares fitted.

Gates: 11/11 node tests against synthetic ground truth (tempo within 2%
across 90-174 BPM, beat alignment under half a frame, segmentation within
2s of a known boundary, graceful on silence, 6-minute analysis in 1.0s);
5/5 browser checks including audio-driven vs fixed-step frame parity.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 11:10:01 +02:00
Dejvino
7e31c19d6e Phase 0: determinism spine
Engine core: Timeline (fixed dt, audio-mastered in realtime), seeded Rng,
Renderer, Layer/ShaderLayer/SceneLayer, Compositor with blend modes,
feedback and post chain.

Shader scenes are compiled against a fixed uniform contract and define only
`vec4 scene(vec2 uv, vec2 p)`, so adding a scene costs a shader plus a
params block. Deep Nebula ported from party-stage as the first one.

Gate passes, 7/7 in checks.html:
- 300 frames rendered twice are bit-identical
- a fresh Engine reproduces the same frames
- simulated dropped frames change nothing (proves dt is fixed)
- seek matches sequential playback
- 320x180 vs 1280x720 agree within 0.010 (limit 0.06)
- seeded rng reproducible, forked streams independent
- compositor reset clears feedback history

Static gates: no wall-clock or unseeded randomness in deterministic
directories; scene schemas and shader sources agree in both directions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 10:59:30 +02:00
Dejvino
e8fb647f11 Plan: flow-state ambient/EDM video generator
Design document for a new generator that produces full-length, non-story
music videos from a track alone, with no sourced footage.

Core decisions:
- Deterministic engine: audio is analyzed up front into a frame-indexed
  feature table, so realtime preview and offline export render from the
  same code path and produce the same frames.
- Whole-song analysis enables lookahead, so builds can anticipate drops.
- Scenes declare their parameters in a schema, which drives uniform
  binding, generated UI, seeded per-track variation and arc automation.
- Hybrid compositor: fragment-shader and three.js layers in one stack.
- Preview is a first-class surface with section jumping, live param
  editing and segment test-renders.
- Forked from party-stage by copy, then fully detached.

Includes per-phase gates and the validation tooling they depend on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 10:49:14 +02:00