src/pipeline/step2chords.js
- import { Step, Solvable } from "./step.js";
- import { modelChords } from "../model/constraints2.js";
- import { noteNames, chordGraph, chordName } from "../model/theory.js";
- export class Step2Chords extends Solvable(Step) {
-
- async schema() {
- const presetOptions = ["manual"];
- const presetLabels = ["Manual"];
- try {
- const resp = await fetch("scripts/presets/index.json");
- const index = await resp.json();
- for (const p of index) {
- presetOptions.push(p.id);
- presetLabels.push(`${p.name} (${p.works})`);
- }
- } catch {
- console.warn("No preset index found, using manual only");
- }
- return SCHEMA.map((field) =>
- field.name === "part"
- ? {
- ...field,
- template: field.template.map((t) =>
- t.name === "style"
- ? { ...t, options: presetOptions, labels: presetLabels }
- : t,
- ),
- }
- : field,
- );
- }
-
- async solve(parameters, solver) {
- const { key, mode, enable, preset, bars } = parameters;
- const graph = chordGraph(mode, preset);
- if (!enable) {
- return {
- textOutput: "Chord progression disabled.",
- chordGraph: { graph, path: null, key, mode },
- chords: Array(bars).fill("NC"),
- concreteProgression: "",
- };
- }
- const solution = await solver(modelChords(parameters, graph));
- if (!solution) {
- return {
- textOutput: "No chord progression found.",
- chordGraph: { graph, path: null, key, mode },
- chords: [],
- concreteProgression: "",
- };
- }
- const pathNodes = solution.path.map((i) => graph.nodes[i - 1]);
- const concreteNames = pathNodes.map((n) => chordName(n, key));
- const pathEdges = new Set(
- pathNodes.slice(0, -1).map((n, i) => `${n}->${pathNodes[i + 1]}`),
- );
- const labeled = pathNodes
- .map((n, i) => `${n} (${concreteNames[i]})`)
- .join(" -> ");
- return {
- textOutput: `Chord Progression Found:\n${labeled}\nTotal cost: ${solution.total_cost}`,
- chordGraph: {
- graph,
- path: { nodes: pathNodes, edges: pathEdges },
- key,
- mode,
- },
- chords: pathNodes,
- concreteProgression: concreteNames.join(" -> "),
- };
- }
- }
- const SCHEMA = (() => {
- return [
- {
- name: "enable",
- type: "bool",
- label: "Enable",
- tooltip: "Disable for atonal music without chord progression",
- default: true,
- ariaInvalid: "false",
- },
- {
- name: "creativity",
- type: "float",
- label: "Creativity",
- tooltip: "Higher values allow more unusual chord transitions",
- default: 0.5,
- min: 0,
- max: 1,
- step: 0.1,
- showIf: "enable",
- },
- {
- name: "part",
- type: "nested",
- label: "Part",
- tooltip: "Editable in Step 1",
- counter: "step1.parts",
- showIf: "enable",
- firstColumn: { type: "text", key: "label", readonly: true },
- defaults: ["A", "B", "A", "C", "A"].map((label) => ({
- key: label === "B" ? 7 : label === "C" ? 9 : 0,
- mode: label === "C" ? "minor" : "major",
- style: "manual",
- start_chord: label === "C" ? "i" : "I",
- end_chord: label === "B" ? "V" : label === "C" ? "V" : "I",
- })),
- template: [
- {
- name: "key",
- type: "select",
- label: "Key",
- tooltip: "Tonal center for this part",
- default: 0,
- options: Array.from({ length: 12 }, (_, i) => i),
- labels: noteNames(),
- },
- {
- name: "mode",
- type: "select",
- label: "Mode",
- tooltip: "Major or minor tonality",
- default: "major",
- options: ["major", "minor"],
- labels: ["Major", "Minor"],
- },
- {
- name: "style",
- type: "select",
- label: "Style",
- tooltip: "Chord transition costs learned from analyzed works",
- default: "manual",
- options: ["manual"],
- labels: ["Manual"],
- },
- {
- name: "start_chord",
- type: "select",
- label: "Start Chord",
- tooltip:
- "First chord of the progression. The label (e.g. Dominant) shows the harmonic function",
- default: "I",
- dynamic: "chord",
- },
- {
- name: "end_chord",
- type: "select",
- label: "End Chord",
- tooltip:
- "Last chord of the progression. I/i = resolved cadence, V = open half cadence",
- default: "I",
- dynamic: "chord",
- },
- ],
- },
- ];
- })();