23d3696adcdbd13eddc58b7b86492546d96897ff
13
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4d05250294 |
Clickable previews: exact app deep-links everywhere
Every rendered still links to the app frame that produced it (song + seed + frame + venue/viz pins + probe warm-up, new window), via shared src/ui/preview.js. Supporting work, each measured: app honors ?frame/?venueSel/?viz/?warm= with pre-build pins (Show.applyPins) and primes deep-link loads; shared pinViz/pinVenueShell paths for gallery, filmstrip and app; probes prime and carry their warm-up; bank probes analyse the bank PCM and file decode is fixed-rate with bit-exact WAV parsing so app and probes see identical samples; materials built mid-update compile synchronously (needsCompile). Round trip verified bit-identical gallery-versus-app. |
||
|
|
1ca9fa2f1f |
Let scenes layer on each other, and give the camera somewhere to look
Two devices that were built, wired up, and doing nothing. OVERLAYS. buildStack gated layering on `surfaceOf(m) === 'composable'`, and no scene in the library declared `surface` — so the only thing that could ever sit on top was the one scene declaring `role: 'accent'`, and the overlay roster excluded accents by construction. Empty intersection, every time: 0/144 stacks carried an overlay. Every layered frame in every song was the same particle field. Labelled the library from the phase 12 coverage gate rather than by eye: the 37 scenes painting under 30% of the frame are composable. Particle Field loses its privileged slot and becomes one of them, keeping only `background: false`, which is the honest part — points in empty space cannot carry a section alone. The reserved accent slot is gone; one roster, two passes at it. 1 distinct overlay scene becomes 27, and the rate lands at 34% of stacks after trimming a base chance that had been tuned while the branch was dead. THE CAMERA. framing.shift moved the frame by a median of 0.029 of a half-frame at a fresh random angle every shot, so successive offsets cancelled and the median jump at a cut was 0.014. Present in every frame, visible in none. look/Camera.js is the director's camera department: the story says tension, order and which act a section is in, and this turns that into where the frame looks and how it travels there. Each director names a camera. Jump distance follows tension and act, speed follows energy, and the curve is one of four. Cuts choose between reframing and matching, so two scenes can still read as one place. Median offset 0.190, median reframe 0.135, and 94% of shots now move during the shot rather than only at the cut. Four new gates, each with a FLOOR — the device this replaces passed every existing check while doing nothing, because the only bound was a ceiling. Also lands the in-progress Epic 4 story layer it builds on: Story.js, phase 13, and the direction statistic. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4217cca3d3 |
Rename clicktrack.js — a content blocker was eating it and killing the app
A file called `clicktrack.js` matches the click-tracking telemetry patterns that EasyPrivacy and similar lists block by substring. In a browser with a content blocker the request never completes, and because it is a module import that takes the entire graph with it: main.js never runs, no handler is ever bound, and every control in the UI sits there looking correct and doing nothing. The failure is unusually expensive to diagnose because everything else looks healthy. The dev server returns 200 with the right MIME type, curl fetches it fine, node imports it fine, and the app loads perfectly in any browser without a blocker — which is how it passed every check here. Only the console names it, and only as one line about a module that failed to load. Renamed to metronome.js, which is also the better name for what it does. The button id went with it, since cosmetic filter rules can hit ids too. The general rule, recorded at the top of the file: anything shipped to a browser and named like tracking will be treated as tracking. Avoid click, track, analytics, pixel, beacon and ad in filenames and URL paths, however honest the code behind them is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2bbf5bcf42 |
Run both variety tests against the song bank, and fix the ceiling twice more
The tests now run on real bank entries. The seed gates used to run on a two-section synthetic whose only kinds were intro and outro, so half the scene library was unreachable and the number was measuring that rather than the seed. Probes are labelled by section kind and occurrence rather than by index, which is what makes two different songs comparable at all — they have different section counts, so probe 3 of one is not probe 3 of the other, and matching by position would compare a drop against an outro and score the mismatch as variety. For two seeds of one song the labels are identical and this changes nothing, which is the point. The song test measures one thing the seed test does not: coupling, the rank correlation between how different two tracks sound and how different their videos look. Separation alone can be had by a generator that ignores the audio and hashes the file, and that would be a perfect score for a completely wrong video. Separation without coupling is not variety, it is a different seed per file. The ceiling took two more attempts. Recasting every layer at random averages a dozen scenes together and a dozen random scenes converge on the same generic busy image, so two references came out closer to each other than two real videos and blocks scored over 100% of achievable. Forcing one scene per reference collapsed the other way: a video that never changes scene has almost no internal variation, so the ceiling landed BELOW the floor, which is a within-video quantity. A reference has to match the structure of what it bounds. They now keep the real pipeline — rosters, shots, per-section sampling, so a reference rotates between three or four scenes exactly as a real video does — while drawing from disjoint slices of the library. Same complexity, nothing in common. The fingerprint measurement had the same shape of error: pooling every probe mixed in how much each video varies over its own length, which is large for everything, and washed the answer to a flat 100% while the separation score said almost everything was collapsed. One vector per video now. Both tests fail as committed. Seed separation 0.04, song separation 0.03, coupling -0.03 — two different songs differ from each other by about as much as one video differs from itself, and that difference has no relationship to the music. Colour scores 112%. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e0c0dd664a |
Give the sections shapes, and the songs an opinion about their own edges
Two complaints, one cause: stages were flat blocks and the noise bed was glued to the pad level, so the bed was a constant hiss under a track that never went anywhere. Sections have contours now — rise, fall, swell, dip, surge — sampled per note rather than per stage, so a build builds through its chords instead of stepping between two flat halves. The noise gets its own per-stage level instead of following the pad, which makes it an arrangement element: a riser through a build with its filter sweeping up, a wash under a drop, nearly absent in a breakdown. That is also the better test signal, since the segmenter classifies a section partly on its energy slope and a build that does not build is one it has to guess at. Transition hardness is a per-song axis. Genres differ on this more than they differ on tempo — an ambient record dissolves between its sections and a club record cuts — and until now every song in the bank cut. Soft songs crossfade across a couple of bars; the hardest get the pre-drop trick, where everything stops for most of a beat before the loud stage lands. Five soft, three mid, nine hard, and the builder fails if the bank ever loses either end. The contours immediately broke the axis they were layered onto: section-to- section contrast is dynamic range, so adding it put a floor of 0.50 under a statistic that had reached 0.18. That is what compression IS, so contour depth now scales with the track's dynamics — a limitered master has shallow section contrast as well as a shallow crest. Back to 0.29, which is as low as enveloped notes and hard cuts will go. One regression accepted rather than fixed: the soft fades cost the beatless entries their tempo detection, since the attack that gave a drone a pulse is exactly what a crossfade removes. `drone` reads 170bpm for a 62bpm source. A beatless track has no tempo and the detector is guessing either way; `ember` exists to anchor the low end with a beat that is actually there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d59dd9d5dd |
Give the song bank notes instead of a test tone
The tonal content was a sustained sine stack at a fixed root, and the stage multiplier scaled that root directly — so a drop arrived at roughly 1.2kHz with ten harmonics stacked on top of it and held there for thirty seconds. Every statistic it was built to control came out correct. It was also unlistenable, and a bank nobody can stand to play is a bank nobody audits. There is a scale, a chord progression, a bass on the chord roots and a seeded motif over the loud stages. Key, mode and progression come off the song's seed so two entries are not the same four chords at different tempos. This is the plainest thing that qualifies as music — not trying to be good, trying to be playable enough that a person will listen to the bank and notice what is wrong with it. Register is the real fix. `stage.root` used to multiply the fundamental and now adds voices upward instead: bass and chord stay where they belong, a lead octave arrives when the arrangement opens up. Brightness still moves the harmonic content and the key's register, but across an octave and a half rather than a piercing sweep. Harmonics above Nyquist are dropped rather than left to alias, since folded energy would corrupt the centroid and flatness the bank exists to control. Two measured consequences. Tempo detection on the beatless entries got better, not worse — the bass gives a pulse where the drone gave nothing, and `drone` now reads 62bpm instead of 88. And enveloped notes put a floor of 0.27 under the measured dynamic range, because the gaps between notes are gaps a limitered master does not have; note decay flattens toward a hold as dynamics falls, which takes the compressed end back to 0.18. Coverage still passes on all six axes and all six section kinds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
cfe77fd524 |
Finish the song bank: axis ranges that are real, and a collinearity check
Completes the bank whose first half went in with the scene commits. The remaining work was all in what "covered" means. Two axis ranges were wrong, and being wrong made the coverage report lie in both directions. Loudness is a raw mean spectral magnitude, not a normalised 0..1, so every song read as 0.00 and the axis looked dead. Centroid is mapped to a log frequency axis, so a track of nothing but sub-bass and a 55Hz pad still measures 0.28 and pure hiss measures 0.93 — against a nominal [0,1] the bank would have reported a permanent 50% gap that no synth change could close. Both now carry the range the statistic can really take on, with the reason. Spread on every axis is not coverage of the space, so the tool also measures correlation between axes. It immediately found brightness and noisiness moving together at r=0.95: noise colour had been tied to brightness, one axis wearing two names, leaving the dark-and-noisy quadrant unreachable. Noise colour is now its own parameter — hiss over a sub-bass pad is an ordinary record. That got r to 0.94, and no further. Flatness is the geometric mean of the spectrum over its arithmetic mean across the whole band, so a signal only measures flat if it has energy everywhere, which is the same thing as measuring bright; band-limiting the noise to hold the centroid down empties the top and drops the flatness with it. It is a property of the analyser and real material does the same, so it is allowlisted with its reason. Anything not on that list still fails — the check is there to catch a bank that has collapsed, not to relitigate physics every run. The output is gitignored. It is 172MB of deterministic audio derived from a table that will keep changing: rebuild it, do not carry it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0c5b0a8d4d |
Close out the twenty: what the gates taught
Three lessons worth more than the scenes: a slow axis has to move a large low-frequency area or the camera's own drift beats it, whole-frame luminance on the kick is the strobe the flash gate exists for and it arrives by accident, and a style trait expressed only as grain measures as no style at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
905c217193 |
Three glitch scenes: a press, a loop and a tape
Halftone Misprint is a printing fault rather than an electronic one — the image is never damaged, only separated and reassembled out of register, and nothing else in the library is made of dots. Droste Feedback scales the previous frame where Time Smear translates it, so the image never clears. Analog Wow is the continuous, wet counterpart to Scan Tear and Block Mosh: the error varies smoothly down the frame because every line was written at a different moment. Three things that had to be got right. The halftone ruling is a count of dots across the frame, not a pixel pitch, or a 4K export is the same dot on a bigger sheet. The Droste loop has to CONTRACT — expanding pushes every copy off the edge and leaves a plume instead of a corridor. And all three expressed style only through grain at first, which is the Side Quest 1 complaint: they now put the track's line weight and edge softness into the dot, the ring and the band boundary, taking the measured style response from 24/11/18 to 86/255/88. Neither feedback scene declares a slow axis, with the numbers recorded in the files: their own history moves the ten-second average further than any parameter does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
91e74ff167 |
A cast census, and the arrangement it needed to be true
"Twenty-three of sixty-one scenes are never cast" was an artefact of the song it was measured on. synthesizeSectioned has one change point, so it segments into exactly two sections and both are quiet kinds — and intro, breakdown and outro are restricted to the restful families for every director. Half the library was unreachable before a seed was drawn, and the measurement reported that as a casting failure. synthesizeArrangement builds a real one: intro, build, drop, breakdown, drop, outro, shaped to hit the segmenter's own classifier rather than to sound like anything. Measured against a bank of those, two scenes out of sixty-one are never a background, not twenty-three. The census exists because "never cast" without a reason is unactionable. A scene can die at the signature gate, at the director's family table, or in the roster draw, and those are three different repairs — a trait declaration, a table, and arithmetic that neither fixes. It reports which one, and it separates being cast as a background from appearing as a translucent overlay, because a library can look fully used while nine scenes carry every frame. What it finds is that eligibility is almost entirely a function of how many traits a scene declares. Four-trait scenes are eligible for every track and open half of all videos; two-trait scenes are eligible for one track in fourteen; the single scene declaring one trait is eligible for none, ever. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |