VideoEncoder.encode() silently drops frames once its internal queue
saturates; the previous loop only polled encodeQueueSize every 10 frames,
so a lagging hardware encoder lost frames in bursts (the "few frames every
beat" bug). Gate every encode on the dequeue event instead, keeping the
queue small but non-empty.
Also resample PCM to 48 kHz when encoding opus so the muxer timescale,
chunk timestamps and bitstream all agree (opus is natively 48k; a 44.1k
source previously produced a file claiming 44100 while the stream was 48k).
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>
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>