Home Manual Reference Source Test

src/controller/composer.js

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