All files / src/controller composer.js

100% Statements 67/67
100% Branches 22/22
100% Functions 12/12
100% Lines 64/64

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;
}