// 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.11, // 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; }