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>
This commit is contained in:
Dejvino
2026-08-05 11:18:37 +02:00
co-authored by Claude Opus 5
parent c9b80308d2
commit 022c267888
4 changed files with 620 additions and 3 deletions
+5 -2
View File
@@ -65,6 +65,9 @@ export function synthesizeBeat({
hats = true,
pad = true,
kickGain = 1,
hatGain = 0.25,
padRoot = 110,
padGain = 0.12,
} = {}) {
const length = Math.round(duration * sampleRate);
const buffer = new MockAudioBuffer(2, length, sampleRate);
@@ -76,9 +79,9 @@ export function synthesizeBeat({
for (let t = 0; t < duration; t += beat, index++) {
// Accent the downbeat so the bar phase is detectable.
addKick(left, sampleRate, t, kickGain * (index % 4 === 0 ? 1.0 : 0.8));
if (hats) addHat(left, sampleRate, t + beat / 2, 0.25, index + 1);
if (hats) addHat(left, sampleRate, t + beat / 2, hatGain, index + 1);
}
if (pad) addPad(left, sampleRate, 0, duration, 0.12);
if (pad) addPad(left, sampleRate, 0, duration, padGain, padRoot);
for (let i = 0; i < length; i++) right[i] = left[i] * 0.98;
return buffer;