Files
Dejvino 13b0939221 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.
2026-10-24 08:23:33 +02:00

109 lines
4.1 KiB
JavaScript

// Determinism is the contract: same song, same seed, same frames -> same
// pixels. This holds even in processes started at different times, which is
// what makes preview === export possible.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { Rng, hashString, hashSamples } from '../src/rng.js';
import { Fft, hann } from '../src/audio/fft.js';
import { analyse } from '../src/audio/analyse.js';
import { decodeWav, mono } from '../src/audio/wav.js';
test('rng is seeded and repeatable', () => {
const a = new Rng(42);
const b = new Rng(42);
assert.deepEqual(Array.from({ length: 20 }, () => a.next()), Array.from({ length: 20 }, () => b.next()));
const c = new Rng(43);
assert.notDeepEqual(Array.from({ length: 20 }, () => a.next()), Array.from({ length: 20 }, () => c.next()));
assert.ok(hashString('drone') !== hashString('runner'));
});
test('fft finds a sine peak', () => {
const N = 2048;
const fft = new Fft(N);
const re = new Float32Array(N);
const im = new Float32Array(N);
const win = hann(N);
const freq = 440;
const sr = 44100;
for (let i = 0; i < N; i++) re[i] = Math.sin((2 * Math.PI * freq * i) / sr) * win[i];
fft.forward(re, im);
let best = 0;
let peakBin = 0;
for (let b = 10; b < N / 2; b++) {
const m = Math.hypot(re[b], im[b]);
if (m > best) {
best = m;
peakBin = b;
}
}
// Parabolic interpolation of the peak bin.
const m0 = Math.hypot(re[peakBin - 1], im[peakBin - 1]);
const m1 = best;
const m2 = Math.hypot(re[peakBin + 1], im[peakBin + 1]);
const delta = 0.5 * (m0 - m2) / (m0 - 2 * m1 + m2);
const detected = ((peakBin + delta) / N) * sr;
assert.ok(Math.abs(detected - freq) < 1, `detected ${detected.toFixed(2)} vs 440`);
});
test('analysis is deterministic for the same PCM', () => {
const pcm = synth(3);
const a = analyse(pcm, 44100);
const b = analyse(pcm, 44100);
assert.deepEqual(Array.from(a.energy), Array.from(b.energy));
assert.equal(a.summary.key, b.summary.key);
assert.ok(a.frames > 50);
});
test('two different signals analyse differently', () => {
const low = analyse(synth(3, 110), 44100);
const high = analyse(synth(3, 990), 44100);
assert.ok(Math.abs(low.summary.centroid - high.summary.centroid) > 0.1);
assert.ok(low.summary.flatness < 0.4, 'a pure tone is not noisy');
});
test('wav decode: mono pcm round-trips', () => {
// A tiny hand-built 8-sample 16-bit mono WAV.
const sr = 8000;
const samples = [0, 16384, 32767, -16384, -32768, 0, 1000, -1000];
const data = Buffer.alloc(44 + samples.length * 2);
data.write('RIFF', 0);
data.writeUInt32LE(36 + samples.length * 2, 4);
data.write('WAVEfmt ', 8);
data.writeUInt32LE(16, 16);
data.writeUInt16LE(1, 20);
data.writeUInt16LE(1, 22);
data.writeUInt32LE(sr, 24);
data.writeUInt32LE(sr * 2, 28);
data.writeUInt16LE(2, 32);
data.writeUInt16LE(16, 34);
data.write('data', 36);
data.writeUInt32LE(samples.length * 2, 40);
for (let i = 0; i < samples.length; i++) data.writeInt16LE(samples[i], 44 + i * 2);
const wav = decodeWav(new Uint8Array(data));
assert.ok(wav);
assert.equal(wav.sampleRate, sr);
const arr = Array.from(mono(wav.channels));
assert.equal(arr[2], 32767 / 32768);
assert.equal(arr[4], -32768 / 32768);
});
test('pcm hash is content-based and stable', () => {
// Long enough that the content stride sees both samples.
const make = (v) => {
const a = new Float32Array(3000);
a[1994] = v; // a location the stride actually visits (997 → 1994)
return a;
};
assert.equal(hashSamples(make(0.5)), hashSamples(make(0.5)));
assert.notEqual(hashSamples(make(0.5)), hashSamples(make(-0.5)));
});
/** A few seconds of low/high sine, enough for a stable beat free analysis. */
function synth(seconds, freq = 220) {
const sr = 44100;
const n = Math.floor(sr * seconds);
const out = new Float32Array(n);
for (let i = 0; i < n; i++) out[i] = Math.sin((2 * Math.PI * freq * i) / sr) * 0.5;
return out;
}