A cast census, and the arrangement it needed to be true
"Twenty-three of sixty-one scenes are never cast" was an artefact of the song it was measured on. synthesizeSectioned has one change point, so it segments into exactly two sections and both are quiet kinds — and intro, breakdown and outro are restricted to the restful families for every director. Half the library was unreachable before a seed was drawn, and the measurement reported that as a casting failure. synthesizeArrangement builds a real one: intro, build, drop, breakdown, drop, outro, shaped to hit the segmenter's own classifier rather than to sound like anything. Measured against a bank of those, two scenes out of sixty-one are never a background, not twenty-three. The census exists because "never cast" without a reason is unactionable. A scene can die at the signature gate, at the director's family table, or in the roster draw, and those are three different repairs — a trait declaration, a table, and arithmetic that neither fixes. It reports which one, and it separates being cast as a background from appearing as a translucent overlay, because a library can look fully used while nine scenes carry every frame. What it finds is that eligibility is almost entirely a function of how many traits a scene declares. Four-trait scenes are eligible for every track and open half of all videos; two-trait scenes are eligible for one track in fourteen; the single scene declaring one trait is eligible for none, ever. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -119,6 +119,71 @@ export function synthesizeSectioned({
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* A track with a full ARRANGEMENT — intro, build, drop, breakdown, drop, outro.
|
||||
*
|
||||
* `synthesizeSectioned` has one change point, so it segments into exactly two
|
||||
* sections and both of them are quiet kinds. That is fine for testing that the
|
||||
* segmenter finds a boundary, and it was quietly useless for anything that
|
||||
* measures what the generator DOES with a song: intro, breakdown and outro are
|
||||
* restricted to the restful families for every director, so a two-section track
|
||||
* cannot reach geometric, glitch or structural scenes at all. Half the library
|
||||
* is unreachable before the seed is even drawn, and a test built on it will
|
||||
* report that as a casting failure.
|
||||
*
|
||||
* The stages here are shaped to hit the segmenter's own classifier: a build
|
||||
* needs a rising energy slope, a drop needs energy and flux together, and a
|
||||
* breakdown needs to fall well below the median.
|
||||
*/
|
||||
export function synthesizeArrangement({
|
||||
bpm = 128,
|
||||
duration = 120,
|
||||
sampleRate = 44100,
|
||||
brightness = 1,
|
||||
density = 1,
|
||||
} = {}) {
|
||||
const length = Math.round(duration * sampleRate);
|
||||
const buffer = new MockAudioBuffer(2, length, sampleRate);
|
||||
const left = buffer.getChannelData(0);
|
||||
const right = buffer.getChannelData(1);
|
||||
|
||||
// Proportions of the track, in order. Kick gain, hat gain, pad root, pad gain.
|
||||
const stages = [
|
||||
{ span: 0.12, kick: 0.00, hat: 0.00, root: 110, pad: 0.09 }, // intro
|
||||
{ span: 0.16, kick: 0.55, hat: 0.20, root: 165, pad: 0.14, ramp: true }, // build
|
||||
{ span: 0.22, kick: 1.00, hat: 0.50, root: 440, pad: 0.24 }, // drop
|
||||
{ span: 0.14, kick: 0.05, hat: 0.02, root: 110, pad: 0.07 }, // breakdown
|
||||
{ span: 0.24, kick: 1.00, hat: 0.55, root: 440, pad: 0.26 }, // drop
|
||||
{ span: 0.12, kick: 0.25, hat: 0.08, root: 110, pad: 0.08 }, // outro
|
||||
];
|
||||
|
||||
const beat = 60 / bpm;
|
||||
let at = 0;
|
||||
for (const stage of stages) {
|
||||
const from = at;
|
||||
const to = Math.min(duration, at + stage.span * duration);
|
||||
at = to;
|
||||
|
||||
let index = Math.round(from / beat);
|
||||
for (let t = from; t < to; t += beat, index++) {
|
||||
// A build ramps across its own span so the energy slope is positive
|
||||
// enough for the classifier to call it one.
|
||||
const ramp = stage.ramp ? (t - from) / Math.max(1e-6, to - from) : 1;
|
||||
const gain = stage.kick * density * (0.25 + ramp * 0.75);
|
||||
if (gain > 0.02) addKick(left, sampleRate, t, gain * (index % 4 === 0 ? 1 : 0.8));
|
||||
if (stage.hat > 0.02) {
|
||||
const h = stage.hat * density * ramp;
|
||||
addHat(left, sampleRate, t + beat / 2, h, index + 1);
|
||||
if (h > 0.3) addHat(left, sampleRate, t + beat / 4, h * 0.6, index + 7);
|
||||
}
|
||||
}
|
||||
addPad(left, sampleRate, from, to, stage.pad, stage.root * brightness);
|
||||
}
|
||||
|
||||
for (let i = 0; i < length; i++) right[i] = left[i] * 0.98;
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/** Silence, for degenerate-input checks. */
|
||||
export function synthesizeSilence({ duration = 10, sampleRate = 44100 } = {}) {
|
||||
return new MockAudioBuffer(2, Math.round(duration * sampleRate), sampleRate);
|
||||
|
||||
Reference in New Issue
Block a user