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,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 };
|
||||
}
|
||||
Reference in New Issue
Block a user