Finish the song bank: axis ranges that are real, and a collinearity check
Completes the bank whose first half went in with the scene commits. The remaining work was all in what "covered" means. Two axis ranges were wrong, and being wrong made the coverage report lie in both directions. Loudness is a raw mean spectral magnitude, not a normalised 0..1, so every song read as 0.00 and the axis looked dead. Centroid is mapped to a log frequency axis, so a track of nothing but sub-bass and a 55Hz pad still measures 0.28 and pure hiss measures 0.93 — against a nominal [0,1] the bank would have reported a permanent 50% gap that no synth change could close. Both now carry the range the statistic can really take on, with the reason. Spread on every axis is not coverage of the space, so the tool also measures correlation between axes. It immediately found brightness and noisiness moving together at r=0.95: noise colour had been tied to brightness, one axis wearing two names, leaving the dark-and-noisy quadrant unreachable. Noise colour is now its own parameter — hiss over a sub-bass pad is an ordinary record. That got r to 0.94, and no further. Flatness is the geometric mean of the spectrum over its arithmetic mean across the whole band, so a signal only measures flat if it has energy everywhere, which is the same thing as measuring bright; band-limiting the noise to hold the centroid down empties the top and drops the flatness with it. It is a property of the analyser and real material does the same, so it is allowlisted with its reason. Anything not on that list still fails — the check is there to catch a bank that has collapsed, not to relitigate physics every run. The output is gitignored. It is 172MB of deterministic audio derived from a table that will keep changing: rebuild it, do not carry it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
0c5b0a8d4d
commit
cfe77fd524
4
flow-state/.gitignore
vendored
4
flow-state/.gitignore
vendored
@ -6,3 +6,7 @@ out
|
||||
# Scratch probes: one-off harnesses for debugging decode/encode/mux issues.
|
||||
# They are throwaway by nature and were committed once by accident.
|
||||
_*.html
|
||||
|
||||
# The test song bank. Generated, deterministic, and ~170MB as audio — rebuild it
|
||||
# rather than carrying it: node tools/build-song-bank.js
|
||||
test/songs/
|
||||
|
||||
@ -9,7 +9,10 @@
|
||||
"preview": "vite preview",
|
||||
"lint:scenes": "node tools/lint-scenes.js",
|
||||
"test": "node --test test/*.test.js",
|
||||
"new:scene": "node tools/new-scene.js"
|
||||
"new:scene": "node tools/new-scene.js",
|
||||
"build:songs": "node tools/build-song-bank.js",
|
||||
"check:songs": "node tools/build-song-bank.js --check",
|
||||
"cast:census": "node tools/cast-census.js"
|
||||
},
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
|
||||
@ -136,6 +136,6 @@ export const AXES = [
|
||||
// section energy by it, so its scale is whatever the analyser produces.
|
||||
{ key: 'loudness', label: 'loudness', of: (t) => t.summary.meanLoudness, range: [0, 0.012],
|
||||
reads: 'section energy · temperament intensity' },
|
||||
{ key: 'sections', label: 'section count', of: (t) => t.sections.length, range: [2, 9],
|
||||
{ key: 'sections', label: 'section count', of: (t) => t.sections.length, range: [2, 8],
|
||||
reads: 'roster rotation · how often the image is allowed to change' },
|
||||
];
|
||||
|
||||
@ -152,10 +152,18 @@ for (const axis of AXES) {
|
||||
// move together mean whole combinations are unrepresentable, and a downstream
|
||||
// test would then attribute to the generator what is really a hole in the bank.
|
||||
//
|
||||
// The big one here is physical rather than fixable: spectral flatness measures
|
||||
// how noise-like a spectrum is, and a dark signal is one whose spectrum is
|
||||
// tilted, so "dark and very noisy" is only weakly reachable. Real material has
|
||||
// the same correlation. It is reported rather than engineered away.
|
||||
// One correlation here is a property of the ANALYSER and cannot be engineered
|
||||
// away. Flatness is the geometric mean of the spectrum over its arithmetic
|
||||
// mean, taken across the whole band, so a signal only measures flat if it has
|
||||
// energy everywhere — which is the same thing as measuring bright. Adding a
|
||||
// noise bed raises both, and band-limiting it to keep the centroid down empties
|
||||
// the top of the spectrum and drops the flatness with it. Two attempts at
|
||||
// decoupling (a steeper noise filter, then noise colour as its own axis) moved
|
||||
// r from 0.95 to 0.94. Real material behaves the same way.
|
||||
//
|
||||
// So it is allowlisted with its reason rather than failed on, and anything NOT
|
||||
// on the list still fails — the check is here to catch a bank that has
|
||||
// collapsed, not to relitigate physics on every run.
|
||||
console.log('\n AXIS CORRELATION — |r| near 1 means the bank cannot vary these independently\n');
|
||||
const corr = (a, b) => {
|
||||
const ma = a.reduce((x, y) => x + y, 0) / a.length;
|
||||
@ -170,13 +178,22 @@ const corr = (a, b) => {
|
||||
};
|
||||
const series = Object.fromEntries(AXES.map(
|
||||
(a) => [a.key, built.map((b) => b.measured[a.key])]));
|
||||
|
||||
const EXPECTED = {
|
||||
'brightness↔noisiness':
|
||||
'spectral flatness is measured across the whole band, so flat implies bright',
|
||||
};
|
||||
for (let i = 0; i < AXES.length; i++) {
|
||||
for (let j = i + 1; j < AXES.length; j++) {
|
||||
const r = corr(series[AXES[i].key], series[AXES[j].key]);
|
||||
if (Math.abs(r) < 0.6) continue;
|
||||
const key = `${AXES[i].label}↔${AXES[j].label}`;
|
||||
const expected = EXPECTED[key];
|
||||
console.log(` ${AXES[i].label} ↔ ${AXES[j].label}: r = ${r.toFixed(2)}` +
|
||||
(Math.abs(r) > 0.85 ? ' ← effectively one axis' : ''));
|
||||
if (Math.abs(r) > 0.85) problems.push(`${AXES[i].label} and ${AXES[j].label} are collinear (r ${r.toFixed(2)})`);
|
||||
(expected ? ` expected — ${expected}` : Math.abs(r) > 0.85 ? ' ← effectively one axis' : ''));
|
||||
if (Math.abs(r) > 0.85 && !expected) {
|
||||
problems.push(`${AXES[i].label} and ${AXES[j].label} are collinear (r ${r.toFixed(2)})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user