Home Manual Reference Source Test

src/pipeline/step2chords.js

  1. import { Step, Solvable } from "./step.js";
  2. import { modelChords } from "../model/constraints2.js";
  3. import { noteNames, chordGraph, chordName } from "../model/theory.js";
  4. /**
  5. * Step 2: chord-progression solving as shortest path of fixed length over the
  6. * weighted chord graph G = (V, E, w); emits Roman-numeral progression + SVG.
  7. * @implements {Step}
  8. */
  9. export class Step2Chords extends Solvable(Step) {
  10. /**
  11. * @override
  12. * @return {Promise<Object[]>} SCHEMA with the part.style preset options patched in.
  13. */
  14. async schema() {
  15. const presetOptions = ["manual"];
  16. const presetLabels = ["Manual"];
  17. try {
  18. const resp = await fetch("scripts/presets/index.json");
  19. const index = await resp.json();
  20. for (const p of index) {
  21. presetOptions.push(p.id);
  22. presetLabels.push(`${p.name} (${p.works})`);
  23. }
  24. } catch {
  25. console.warn("No preset index found, using manual only");
  26. }
  27. return SCHEMA.map((field) =>
  28. field.name === "part"
  29. ? {
  30. ...field,
  31. template: field.template.map((t) =>
  32. t.name === "style"
  33. ? { ...t, options: presetOptions, labels: presetLabels }
  34. : t,
  35. ),
  36. }
  37. : field,
  38. );
  39. }
  40. /**
  41. * @override
  42. * Build chord-progression model, solve, render result with chord-graph data plus
  43. * Roman-numeral progression (NC-filled when disabled, empty on UNSAT) and a concrete
  44. * chord-name display string.
  45. * @param {Object} parameters - Per-part form values; see {@link Step2Chords#schema}.
  46. * @param {function(model: string): Promise<Object|null>} solver
  47. * Pre-bound solver callback returning the MZN solution {path: number[], total_cost: number} or null.
  48. * @return {Promise<Object>}
  49. * Resolves to {textOutput, chordGraph: {graph, path, key, mode}, chords, concreteProgression};
  50. * path is {nodes: string[], edges: Set<string>} on success, null otherwise.
  51. */
  52. async solve(parameters, solver) {
  53. const { key, mode, enable, preset, bars } = parameters;
  54. const graph = chordGraph(mode, preset);
  55. if (!enable) {
  56. return {
  57. textOutput: "Chord progression disabled.",
  58. chordGraph: { graph, path: null, key, mode },
  59. chords: Array(bars).fill("NC"),
  60. concreteProgression: "",
  61. };
  62. }
  63. const solution = await solver(modelChords(parameters, graph));
  64. if (!solution) {
  65. return {
  66. textOutput: "No chord progression found.",
  67. chordGraph: { graph, path: null, key, mode },
  68. chords: [],
  69. concreteProgression: "",
  70. };
  71. }
  72. const pathNodes = solution.path.map((i) => graph.nodes[i - 1]);
  73. const concreteNames = pathNodes.map((n) => chordName(n, key));
  74. const pathEdges = new Set(
  75. pathNodes.slice(0, -1).map((n, i) => `${n}->${pathNodes[i + 1]}`),
  76. );
  77. const labeled = pathNodes
  78. .map((n, i) => `${n} (${concreteNames[i]})`)
  79. .join(" -> ");
  80. return {
  81. textOutput: `Chord Progression Found:\n${labeled}\nTotal cost: ${solution.total_cost}`,
  82. chordGraph: {
  83. graph,
  84. path: { nodes: pathNodes, edges: pathEdges },
  85. key,
  86. mode,
  87. },
  88. chords: pathNodes,
  89. concreteProgression: concreteNames.join(" -> "),
  90. };
  91. }
  92. }
  93. /** @private */
  94. const SCHEMA = (() => {
  95. return [
  96. {
  97. name: "enable",
  98. type: "bool",
  99. label: "Enable",
  100. tooltip: "Disable for atonal music without chord progression",
  101. default: true,
  102. ariaInvalid: "false",
  103. },
  104. {
  105. name: "creativity",
  106. type: "float",
  107. label: "Creativity",
  108. tooltip: "Higher values allow more unusual chord transitions",
  109. default: 0.5,
  110. min: 0,
  111. max: 1,
  112. step: 0.1,
  113. showIf: "enable",
  114. },
  115. {
  116. name: "part",
  117. type: "nested",
  118. label: "Part",
  119. tooltip: "Editable in Step 1",
  120. counter: "step1.parts",
  121. showIf: "enable",
  122. firstColumn: { type: "text", key: "label", readonly: true },
  123. defaults: ["A", "B", "A", "C", "A"].map((label) => ({
  124. key: label === "B" ? 7 : label === "C" ? 9 : 0,
  125. mode: label === "C" ? "minor" : "major",
  126. style: "manual",
  127. start_chord: label === "C" ? "i" : "I",
  128. end_chord: label === "B" ? "V" : label === "C" ? "V" : "I",
  129. })),
  130. template: [
  131. {
  132. name: "key",
  133. type: "select",
  134. label: "Key",
  135. tooltip: "Tonal center for this part",
  136. default: 0,
  137. options: Array.from({ length: 12 }, (_, i) => i),
  138. labels: noteNames(),
  139. },
  140. {
  141. name: "mode",
  142. type: "select",
  143. label: "Mode",
  144. tooltip: "Major or minor tonality",
  145. default: "major",
  146. options: ["major", "minor"],
  147. labels: ["Major", "Minor"],
  148. },
  149. {
  150. name: "style",
  151. type: "select",
  152. label: "Style",
  153. tooltip: "Chord transition costs learned from analyzed works",
  154. default: "manual",
  155. options: ["manual"],
  156. labels: ["Manual"],
  157. },
  158. {
  159. name: "start_chord",
  160. type: "select",
  161. label: "Start Chord",
  162. tooltip:
  163. "First chord of the progression. The label (e.g. Dominant) shows the harmonic function",
  164. default: "I",
  165. dynamic: "chord",
  166. },
  167. {
  168. name: "end_chord",
  169. type: "select",
  170. label: "End Chord",
  171. tooltip:
  172. "Last chord of the progression. I/i = resolved cadence, V = open half cadence",
  173. default: "I",
  174. dynamic: "chord",
  175. },
  176. ],
  177. },
  178. ];
  179. })();