Metabolism: song as substrate, ecology grown from the track
A new top-level section building the metabolism concept: the track supplies the food field, event impulses and per-section laws; the seed picks the species (a rule set, not a parameter vector). Same song + seed => bit-identical frames; different songs => different worlds. - analysis: single FFT pass -> per-frame feature table; score (onsets folded into world geometry via voice ring + pitch-class sector), wells (food placed by the harmony), graph (band correlations -> interaction matrix), laws (per-section regimes) - sim: typed-array world, 3 castes (builder/orbit, forager/commute, spark/orbit-wide) with causality rings, persistence trails, consequence field; fully deterministic - lens: software framebuffer renderer (PNG in node, ImageData in browser) + event-reactive camera (cuts on activity change, alternating shot sizes, drift-and-push) - metrics: coupling x residual fitness (not per-frame descriptors) plus a grown-structure fingerprint; report + strip tools; 15 tests green - DESIGN.md captures the concept, invariants and build order 3 reference test signals (drone/runner/dusk) grow measurably distinct worlds while seeds of one song stay in the same biome.
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
// Onsets become places. The song does not move the agents directly; it puts
|
||||
// food down, and the agents grow on it.
|
||||
//
|
||||
// Three rules make the geography legible:
|
||||
// - the voice picks the ring: kick near the centre, body in the middle,
|
||||
// air at the rim, so a track's mix is visible as an annulus;
|
||||
// - the dominant pitch picks the angle, so twelve pitch classes become
|
||||
// twelve sectors and harmony becomes direction;
|
||||
// - strength sets how much food and how wide, so a hit is a place and a
|
||||
// fill is a region.
|
||||
|
||||
const TAU = Math.PI * 2;
|
||||
const RINGS = [0.24, 0.54, 0.82];
|
||||
|
||||
export function buildEvents(analysis, rng) {
|
||||
const events = [];
|
||||
const keys = analysis.summary.chroma ? 12 : 0;
|
||||
for (const o of analysis.onsets.all) {
|
||||
const f = o.frame;
|
||||
const hz = analysis.dominantHz[f] || 440;
|
||||
const semis = Math.round(12 * Math.log2(hz / 440));
|
||||
const pc = ((semis % 12) + 12 + 9) % 12;
|
||||
// A quiet, noisy band has an unstable dominant bin. Pull the angle
|
||||
// toward the event's own hash when the band is flat.
|
||||
const confidence = 0.55 + 0.45 * (1 - (analysis.flatness[f] ?? 0.3));
|
||||
const angle = ((pc / (keys || 12)) * TAU) * confidence +
|
||||
rng.range(-0.16, 0.16) +
|
||||
(o.voice - 1) * 0.05;
|
||||
const radius = RINGS[o.voice] * (0.82 + 0.36 * rng.next());
|
||||
const strength = Math.max(0, Math.min(1, 0.18 + 0.82 * o.strength));
|
||||
|
||||
events.push({
|
||||
frame: f,
|
||||
t: o.t,
|
||||
voice: o.voice,
|
||||
strength,
|
||||
x: 0.5 + Math.cos(angle) * radius * 0.5,
|
||||
y: 0.5 + Math.sin(angle) * radius * 0.5,
|
||||
// World units. A wide event is a region, not a point.
|
||||
spread: 0.014 + 0.040 * strength * strength,
|
||||
// Food units. Scaled so a dense arrangement supports roughly a
|
||||
// thousand agents and a sparse one a few hundred; see the report.
|
||||
amount: (10 + 34 * strength * strength) * 0.07,
|
||||
// Strong events seed life as well as food.
|
||||
spawn: strength > 0.62 ? (o.voice === 0 ? 3 : o.voice === 1 ? 2 : 1) : 0,
|
||||
born: 0,
|
||||
});
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
/** Frame -> events, so the world steps in O(events this frame). */
|
||||
export function indexEvents(events, frames) {
|
||||
const index = Array.from({ length: frames }, () => []);
|
||||
for (const e of events) {
|
||||
if (e.frame >= 0 && e.frame < frames) index[e.frame].push(e);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
Reference in New Issue
Block a user