src/controller/composer.js
- import { generateMidi } from "./tracks.js";
- 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,
- };
- }
- 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 || [],
- });
- };
- }
- 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.`);
- };
- }
- 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;
- }