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,73 @@
|
||||
// Render one filmstrip per song: N frames evenly across the track, tiled.
|
||||
//
|
||||
// node tools/strip.js --songs drone,runner,dusk
|
||||
// node tools/strip.js --songs drone --cols 6 --rows 2 --w 480 --h 270
|
||||
//
|
||||
// Frames come from the same Run as a full render, and the sim is advanced
|
||||
// through every frame in between, so a strip cannot show a world that the
|
||||
// video would not have.
|
||||
|
||||
import { writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { resolve, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { loadSong } from './load.js';
|
||||
import { Run } from '../src/film.js';
|
||||
import { encodePNG, tile } from '../src/render/png.js';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = resolve(HERE, '..');
|
||||
const BANK = resolve(ROOT, '../flow-state/test/songs');
|
||||
|
||||
function args(argv) {
|
||||
const out = {};
|
||||
for (let i = 2; i < argv.length; i += 2) out[argv[i].replace(/^--/, '')] = argv[i + 1];
|
||||
return out;
|
||||
}
|
||||
|
||||
const opt = args(process.argv);
|
||||
const names = (opt.songs || 'drone,runner,dusk').split(',').map((s) => s.trim()).filter(Boolean);
|
||||
const seed = Number(opt.seed || 12345);
|
||||
const cols = Number(opt.cols || 4);
|
||||
const rows = Number(opt.rows || 3);
|
||||
const width = Number(opt.w || 480);
|
||||
const height = Number(opt.h || 270);
|
||||
const fps = 30;
|
||||
const everything = opt.combined === '1';
|
||||
const outDir = resolve(ROOT, opt.out || 'out');
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
|
||||
const count = cols * rows;
|
||||
const all = [];
|
||||
|
||||
for (const name of names) {
|
||||
const { song, analysis } = loadSong(resolve(BANK, `${name}.wav`), { fps, seed });
|
||||
const run = new Run(song, { fps, width, height, seed });
|
||||
const frames = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const t = (0.10 + 0.85 * (i / Math.max(1, count - 1))) * analysis.duration;
|
||||
frames.push(Math.min(analysis.frames - 1, Math.round(t * fps)));
|
||||
}
|
||||
const rendered = run.renderAt(frames);
|
||||
const shots = frames.map((f) => rendered.get(f));
|
||||
const zooms = shots.map((s) => `${s.cam.zoom.toFixed(1)}@${s.shot}`).join(' ');
|
||||
const sheet = tile(shots, cols, rows, 2);
|
||||
const file = resolve(outDir, `strip-${name}.png`);
|
||||
writeFileSync(file, encodePNG(sheet.w, sheet.h, sheet.data));
|
||||
const stats = run.world.stats();
|
||||
console.log(`${file} ${sheet.w}x${sheet.h} ${song.name} peak=${stats.peak} pop=${run.world.count} hue=${song.palette.hue.toFixed(2)} key=${song.summary.key}`);
|
||||
console.log(` shots: ${zooms}`);
|
||||
all.push({ name, song, shots, stats });
|
||||
}
|
||||
|
||||
if (everything) {
|
||||
// One row per song, same fractions of each track: the comparison sheet.
|
||||
const per = 4;
|
||||
const chosen = [];
|
||||
for (const entry of all) {
|
||||
for (let i = 0; i < per; i++) chosen.push(entry.shots[Math.round((i / (per - 1)) * (entry.shots.length - 1))]);
|
||||
}
|
||||
const sheet = tile(chosen, per, all.length, 3);
|
||||
const file = resolve(outDir, 'strip-all.png');
|
||||
writeFileSync(file, encodePNG(sheet.w, sheet.h, sheet.data));
|
||||
console.log(`${file} ${sheet.w}x${sheet.h}`);
|
||||
}
|
||||
Reference in New Issue
Block a user