60 lines
2.4 KiB
JavaScript
60 lines
2.4 KiB
JavaScript
// 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;
|
|
}
|