Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | 6x 6x 6x 6x 1x 1x 5x 10x 5x 5x 5x 5x 5x 5x 5x 10x 10x 10x 10x 10x 10x 5x 10x 6x 5x 10x 5x 10x 10x 10x 7x 7x 7x 7x 10x 5x 5x 10x 10x 10x 2x 2x 8x 8x 1x 1x 7x 7x 10x 7x 2x 2x 2x 2x 5x 5x 5x 14x 14x 14x 13x 14x 6x 14x | import { generateMidi } from "./tracks.js";
/**
* Run the 3-step composition pipeline (form, chords, notes); pure orchestration via hooks.
* Step 2 + Step 3 results are cached by part label so repeated labels (e.g. A in A-B-A-C-A)
* solve each unique label only once. View layer assembles the display table from labelCache.
* Implements roughly the Thesis' Algorithm.
* @param {Object} parameters - Top-level parameters (theta) keyed step1/step2/step3.
* Sub-shapes per {@link Step1Parts#schema}, {@link Step2Chords#schema}, {@link Step3Notes#schema}.
* @param {Step[]} steps - Pipeline order [{@link Step1Parts}, {@link Step2Chords}, {@link Step3Notes}].
* @param {Object} hooks - Side-effect callbacks for solver runs and UI updates.
* @param {function(msg: string): void} hooks.log
* Status-line + console-log callback.
* @param {function(style: string): Promise<Object|null>} hooks.loadPreset
* Resolves a corpus preset id (pi) to a preset object; null on miss.
* @param {function(step: Step, partLabel: string, mznSource: string): Promise<Object|null>} hooks.solveStep
* Runs a model on the configured solver; returns the solution (null when no solution).
* @param {function(chordGraph: Object): void} hooks.updateGraph
* Renders the chord-graph data ({@link chordGraph}) into the view layer.
* @param {function(line: {label: string, chords: string[]}): void} hooks.onProgressionLine
* Emits one solved Roman-numeral progression line per Step 2 solve.
* @emits {progressionLine}
* @return {Promise<{
* parts: Object[],
* labelCache: Object,
* midi: ?Object,
* failedParts: string[],
* solvedParts: number,
* }|null>} Orchestration result; null when Step 1 produces no parts.
*/
export async function compose(parameters, steps, hooks) {
const t1 = performance.now();
const [step1, step2, step3] = steps;
const parts = parameters.step1.part.slice(0, parameters.step1.parts);
if (parts.length === 0) {
hooks.log("No parts defined.");
return null;
}
const summary = parts
.map((p) => `${p.label}: ${p.bars} bars, ${p.numerator}/${p.denominator}`)
.join("\n");
hooks.log(`${step1.name}: Form:\n${summary}`);
performance.measure("compose:step1", { start: t1, end: performance.now() });
const cache = {};
const failed = new Set();
const solveChords = chordsHandler(parameters, hooks, step2);
const solveNotes = notesHandler(parameters, hooks, step3);
for (let i = 0; i < parts.length; i++) {
const t2 = performance.now();
await solveChords(parts, cache, i);
performance.measure(`compose:step2:${i}`, {
start: t2,
end: performance.now(),
});
const t3 = performance.now();
await solveNotes(parts, cache, failed, i);
performance.measure(`compose:step3:${i}`, {
start: t3,
end: performance.now(),
});
}
const partSolutions = parts
.filter((p) => !failed.has(p.label))
.map((p) => cache[p.label]);
return {
parts,
labelCache: cache,
midi: generateMidi(partSolutions, parameters.step3, parameters.step1.tempo),
failedParts: parts.filter((p) => failed.has(p.label)).map((p) => p.label),
solvedParts: partSolutions.length,
};
}
/** @private Partial application: bind compose-run dependencies, return a per-iteration Step 2 handler. */
function chordsHandler(parameters, hooks, step2) {
return async (parts, cache, i) => {
const part = parts[i];
const label = part.label;
if (!cache[label]) {
const formPart = parameters.step2.part[i];
hooks.log(`${step2.name}: Part ${label} (${i + 1}/${parts.length})...`);
const out = await invokeStep(hooks, step2, label, {
...parameters.step2,
...formPart,
bars: part.bars,
preset: await hooks.loadPreset(formPart.style),
});
cache[label] = { ...out, formPart };
}
hooks.onProgressionLine({
label,
chords: cache[label].chords || [],
});
};
}
/** @private Partial application: bind compose-run dependencies, return a per-iteration Step 3 handler. */
function notesHandler(parameters, hooks, step3) {
const tempo = parameters.step1.tempo;
return async (parts, cache, failed, i) => {
const part = parts[i];
const label = part.label;
if (failed.has(label)) {
hooks.log(`${step3.name}: Part "${label}" - skipped (failed earlier).`);
return;
}
const c = cache[label];
if (c.solution) {
hooks.log(`${step3.name}: Part "${label}" - reused from cache.`);
return;
}
hooks.log(`${step3.name}: Part "${label}" (${i + 1}/${parts.length})...`);
const chords = c.chords || [];
const out = await invokeStep(hooks, step3, label, {
...parameters.step3,
key: c.formPart.key,
mode: c.formPart.mode,
numerator: part.numerator,
denominator: part.denominator,
bars: chords.length || part.bars,
chords,
tempo,
});
if (!out.solution) {
failed.add(label);
const msg = `Part "${label}" - no solution. Model may be overconstrained.`;
hooks.log(`${step3.name}: ${msg}`);
return;
}
Object.assign(c, out);
c.voicesB64 = generateMidi([c], parameters.step3, tempo).voicesB64;
hooks.log(`${step3.name}: Part "${label}" - solved.`);
};
}
/** @private Bind solver to (step,label), run step, log/render side-effects from the result. */
async function invokeStep(hooks, step, label, params) {
const solver = (model) => hooks.solveStep(step, label, model);
const out = (await step.solve(params, solver)) ?? {};
if (out.textOutput) {
hooks.log(`${step.name}: ${out.textOutput}`);
}
if (out.chordGraph) {
hooks.updateGraph(out.chordGraph);
}
return out;
}
|