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:
Dejvino
2026-10-24 08:23:33 +02:00
parent 6a2af2e45f
commit 13b0939221
26 changed files with 4469 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
// The evaluation layer must be well-behaved: every world gets a finite,
// clamped score, coupling and residual are separable, and the fitness
// discriminates between songs (in the sense that the spread across songs is
// clearly larger than the spread across seeds).
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { resolve, dirname } from 'node:path';
import { analyse } from '../src/audio/analyse.js';
import { decodeWav, mono } from '../src/audio/wav.js';
import { buildSong } from '../src/song/song.js';
import { Run } from '../src/film.js';
import { evaluate, fingerprint, divergence } from '../src/metrics.js';
const HERE = dirname(fileURLToPath(import.meta.url));
const BANK = resolve(HERE, '../..', 'flow-state/test/songs');
function grow(name, seed, seconds = 45) {
const wav = decodeWav(readFileSync(resolve(BANK, `${name}.wav`)));
const song = buildSong(analyse(mono(wav.channels), wav.sampleRate), seed);
const run = new Run(song, { fps: 30, seed, width: 240, height: 135 });
run.simulate();
return { world: run.world, song };
}
test('evaluation returns finite, clamped numbers', () => {
for (const name of ['drone', 'runner', 'dusk']) {
const { world, song } = grow(name, 12345);
const m = evaluate(world, song);
assert.ok(Number.isFinite(m.coupling), `${name} coupling`);
assert.ok(Number.isFinite(m.residual), `${name} residual`);
assert.ok(Number.isFinite(m.fitness), `${name} fitness`);
assert.ok(m.coupling >= 0 && m.coupling <= 1);
assert.ok(m.residual >= 0 && m.residual <= 1);
}
});
test('fitness is stable across seeds of one song', () => {
const songs = ['drone', 'runner', 'dusk'].map((n) => grow(n, 12345).song);
for (const song of songs) {
const s1 = evaluate(growSong(song, 111).world, growSong(song, 111).song);
const s2 = evaluate(growSong(song, 222).world, growSong(song, 222).song);
assert.ok(Math.abs(s1.fitness - s2.fitness) < 0.05, `${song.name}: ${s1.fitness} vs ${s2.fitness}`);
}
});
test('fingerprint distances: songs farther than seeds', () => {
const a = fingerprint(grow('drone', 12345).world);
const b = fingerprint(grow('runner', 12345).world);
const within = divergence(a, fingerprint(grow('drone', 42).world));
assert.ok(divergence(a, b) > within);
});
function growSong(song, seed) {
const run = new Run(song, { fps: 30, seed, width: 240, height: 135 });
run.simulate();
return { world: run.world, song };
}