src/app.js
- import { compose } from "./controller/composer.js";
- import {
- buildParameters,
- exportParameters,
- importParameters,
- } from "./controller/parameters.js";
- import { initSolvers, solveModel } from "./controller/solving.js";
- import { chordGraph, chordName } from "./model/theory.js";
- import { Step1Parts } from "./pipeline/step1parts.js";
- import { Step2Chords } from "./pipeline/step2chords.js";
- import { Step3Notes } from "./pipeline/step3notes.js";
- import { CONTROLS } from "./view/controls.js";
- import { submitFeedback } from "./view/feedback.js";
- import { downloadBlob } from "./view/download.js";
- import { PLOTTING } from "./view/plotting.js";
- document.addEventListener("alpine:init", () =>
- Alpine.data("app", () => ({
-
- isMiniZincReady: false,
- mznSolver: "",
- mznSolvers: [],
- statusText: "Initializing...",
-
- theme: "auto",
- isSystemDark: false,
-
- steps: [],
- schemas: {},
- parameters: {},
-
- mznModelText: [[], [], []],
- mznSolutionText: [[], [], []],
- mznStatisticsText: [[], [], []],
- musicB64Midi: null,
- chordProgressionText: [],
- resultTable: null,
-
- consoleLog: [],
- showConsole: false,
- importedFileName: "",
-
- chordGraph,
- chordName,
- ...APP_METHODS,
- })),
- );
- const APP_METHODS = {
- exportParameters,
- ...CONTROLS,
- downloadBlob,
- submitFeedback,
- ...PLOTTING,
-
- async init() {
- const mq = window.matchMedia("(prefers-color-scheme: dark)");
- this.isSystemDark = mq.matches;
- mq.addEventListener("change", (e) => (this.isSystemDark = e.matches));
- document.getElementById("midi-player")?.addEventListener("start", () => {
- const viz = document.getElementById("midi-visualizer");
- const sel = ".waterfall-notes-container";
- const c = viz?.querySelector(sel) ?? viz?.shadowRoot?.querySelector(sel);
- if (c) {
- c.scrollTop = c.scrollHeight;
- }
- if (viz) {
- viz.scrollTop = viz.scrollHeight;
- }
- });
- this.statusText = "Initializing MiniZinc...";
- try {
- const solverList = await initSolvers();
- this.mznSolvers = solverList;
- this.mznSolver = solverList[0].id;
- this.isMiniZincReady = true;
- const newSteps = [new Step1Parts(), new Step2Chords(), new Step3Notes()];
- Object.assign(this, await buildParameters(newSteps));
- this.$watch("parameters.step2.part", () => this.updateGraph());
- this.$nextTick(() => this.updateGraph());
- document
- .getElementById("import-params")
- ?.addEventListener("change", async (e) => {
- const file = e.target.files[0];
- if (!file) {
- return;
- }
- try {
- const merged = await importParameters(file);
- this.parameters = { ...this.parameters, ...merged };
- this.importedFileName = file.name;
- } catch (err) {
- this.log("Failed to import parameters", err);
- }
- });
- this.log("Ready to compose.");
- } catch (e) {
- this.log("Failed to load solvers", e);
- }
- },
-
- log(msg, err) {
- let line = msg;
- if (err) {
- console.error(msg, err);
- const detail =
- err?.message ??
- (typeof err === "object" ? JSON.stringify(err) : `${err}`);
- line = `Error: ${msg}: ${detail}`;
- }
- this.statusText = line;
- const t = new Date().toLocaleTimeString("de-DE");
- this.consoleLog.push(`[${t}] ${line}`);
- },
-
- async runSolver() {
- if (!this.isMiniZincReady) {
- return;
- }
- const btn = document.getElementById("run-optimization-button");
- btn.setAttribute("aria-busy", "true");
- btn.disabled = true;
- window.Tone?.start();
- this.log("Running solver for all steps...");
- this.resetResults();
- try {
- const lines = [];
- const result = await compose(this.parameters, this.steps, {
- log: this.log.bind(this),
- loadPreset: this.loadPreset.bind(this),
- solveStep: this.solveStep.bind(this),
- updateGraph: this.updateGraph.bind(this),
- onProgressionLine: (line) => {
- lines.push(line);
- this.chordProgressionText = [...lines];
- },
- });
- if (!result) {
- return;
- }
- this.resultTable = this.buildResultTable(result, this.parameters.step3);
- this.applyVoiceStyles(this.resultTable.voiceLabels);
- if (result.midi) {
- this.musicB64Midi = result.midi.combinedB64;
- await this.$nextTick();
- this.wireMidiToPlayer(result.midi);
- }
- const { parts, failedParts: fp, solvedParts: sp } = result;
- const u = [...new Set(fp)].join(", ");
- this.log(
- fp.length === 0
- ? `Done: ${sp}/${parts.length} parts solved.`
- : sp > 0
- ? `Partial: ${sp}/${parts.length} parts solved. Overconstrained: ${u}. Try again.`
- : "No solution: Model may be overconstrained.",
- );
- } catch (e) {
- this.log("Solve failed", e);
- } finally {
- btn.removeAttribute("aria-busy");
- btn.disabled = false;
- }
- },
-
- resetResults() {
- this.consoleLog = this.consoleLog.slice(-1);
- this.mznModelText = [[], [], []];
- this.mznSolutionText = [[], [], []];
- this.mznStatisticsText = [[], [], []];
- this.musicB64Midi = null;
- this.chordProgressionText = [];
- document.getElementById("graph")?.replaceChildren();
- const sel = "midi-visualizer, midi-player";
- document.querySelectorAll(sel).forEach((el) => el.removeAttribute("src"));
- },
-
- async loadPreset(style) {
- if (!style || style === "manual") {
- return null;
- }
- try {
- return await (await fetch(`scripts/presets/${style}.json`)).json();
- } catch {
- console.warn(`Could not load preset ${style}`);
- return null;
- }
- },
-
- async solveStep(step, partLabel, modelStr) {
- const idx = step.index - 1;
- const label = partLabel ? `${step.name}: ${partLabel}` : step.name;
- const file = `${step.id}_${Date.now()}.mzn`;
- this.mznModelText[idx].push({ label, content: modelStr });
- const r = await solveModel(file, modelStr, this.mznSolver);
- const json = r.solution?.output?.json;
- const stats = r.statistics
- ? JSON.stringify({ ...r.statistics }, null, 2)
- : "{}";
- const sol = JSON.stringify(json, null, 2);
- this.mznSolutionText[idx].push({ label, content: sol });
- this.mznStatisticsText[idx].push({ label, content: stats });
- return json;
- },
-
- updateGraph(svg) {
- const parts = this.parameters?.step2?.part;
- parts?.forEach((row) => {
- const { activeNodes, tonic } = chordGraph(row.mode);
- if (!activeNodes.includes(row.start_chord)) {
- row.start_chord = tonic;
- }
- if (!activeNodes.includes(row.end_chord)) {
- row.end_chord = tonic;
- }
- });
- const s2 = parts?.[0];
- const data =
- svg ??
- (s2 && {
- graph: chordGraph(s2.mode),
- path: null,
- key: Number(s2.key) || 0,
- mode: s2.mode,
- });
- if (!data) {
- return;
- }
- const el = document.getElementById("graph");
- if (el) {
- el.innerHTML = this.plotChordGraph(
- data.graph,
- data.path,
- data.key,
- data.mode,
- );
- }
- },
- };