Home Manual Reference Source Test

src/pipeline/step3notes.js

  1. import { Step, Solvable } from "./step.js";
  2. import { modelNotes } from "../model/constraints3.js";
  3. import { INSTRUMENTS } from "../model/instruments.js";
  4. import { noteNames, timeGrid } from "../model/theory.js";
  5. /**
  6. * Step 3: per-voice pitch assignment. Builds a MiniZinc CSP over note variables
  7. * note_s_t with domain {-2 (hold), -1 (rest), 0..127 (MIDI)}.
  8. * @implements {Step}
  9. */
  10. export class Step3Notes extends Solvable(Step) {
  11. /**
  12. * @override
  13. * @return {Object[]} Step 3 UI descriptors: voices, per-voice config, counterpoint controls.
  14. */
  15. schema() {
  16. return SCHEMA;
  17. }
  18. /**
  19. * @override
  20. * Build note-grid model, solve, return solver note-grid + time-grid metadata +
  21. * key/mode/time-signature pass-through on success, or {textOutput} on UNSAT.
  22. * The pass-through fields (numerator, denominator, key, mode) make the result
  23. * a self-contained part record for downstream MIDI generation.
  24. * @param {Object} parameters - Form values plus chords and tempo; see {@link Step3Notes#schema}.
  25. * @param {function(model: string): Promise<Object|null>} solver
  26. * Pre-bound solver callback returning the MZN note-grid solution or null.
  27. * @return {Promise<Object>}
  28. * On success: {textOutput, solution, numSubBeats, subdivision, numerator, denominator, key, mode}.
  29. * On UNSAT: {textOutput} only.
  30. */
  31. async solve(parameters, solver) {
  32. const { voice, voices, numerator, chords } = parameters;
  33. const bars = chords.length > 0 ? chords.length : parameters.bars;
  34. const time = timeGrid(voice, voices, numerator, bars);
  35. const solution = await solver(modelNotes(parameters, time));
  36. if (!solution) {
  37. return { textOutput: "No solution found for music grid." };
  38. }
  39. return {
  40. textOutput: "Music grid solution found.",
  41. solution,
  42. numSubBeats: time.numSubBeats,
  43. subdivision: time.subdivision,
  44. numerator: parameters.numerator,
  45. denominator: parameters.denominator,
  46. key: parameters.key,
  47. mode: parameters.mode,
  48. };
  49. }
  50. }
  51. /** @private */
  52. const SCHEMA = [
  53. {
  54. name: "voices",
  55. type: "stepper",
  56. label: "Number of Voices",
  57. tooltip: "Number of simultaneous melodic lines",
  58. default: 2,
  59. min: 1,
  60. max: 4,
  61. },
  62. {
  63. name: "voice",
  64. type: "nested",
  65. label: "Voice",
  66. tooltip: "Configure pitch range, instrument & options per voice",
  67. firstColumn: { type: "color", key: "color" },
  68. counter: "voices",
  69. defaults: [
  70. {
  71. color: "#0000ff",
  72. min_pitch: 60,
  73. max_pitch: 84,
  74. instrument: 0,
  75. chord_tones: "scale",
  76. duration: "eighth",
  77. allow_repetitions: true,
  78. },
  79. {
  80. color: "#00ff00",
  81. min_pitch: 52,
  82. max_pitch: 76,
  83. instrument: 0,
  84. chord_tones: "chord",
  85. duration: "quarter",
  86. allow_repetitions: true,
  87. },
  88. {
  89. color: "#ff0000",
  90. min_pitch: 45,
  91. max_pitch: 69,
  92. instrument: 0,
  93. chord_tones: "chord",
  94. duration: "half",
  95. allow_repetitions: true,
  96. },
  97. {
  98. color: "#888888",
  99. min_pitch: 36,
  100. max_pitch: 60,
  101. instrument: 0,
  102. chord_tones: "chord",
  103. duration: "whole",
  104. allow_repetitions: false,
  105. },
  106. ],
  107. template: [
  108. {
  109. name: "duration",
  110. type: "select",
  111. label: "Duration",
  112. tooltip: "How many notes per beat",
  113. default: "quarter",
  114. options: ["whole", "half", "quarter", "eighth", "triplet", "sixteenth"],
  115. labels: ["Whole", "Half", "Quarter", "Eighth", "Triplet", "Sixteenth"],
  116. },
  117. {
  118. name: "min_pitch",
  119. type: "int",
  120. label: "Min Pitch",
  121. tooltip: "Lowest allowed note for this voice",
  122. default: 48,
  123. min: 24,
  124. max: 96,
  125. step: 1,
  126. format: (v) =>
  127. `${noteNames()[v % 12]}<sub>${Math.floor(v / 12) - 1}</sub>`,
  128. },
  129. {
  130. name: "max_pitch",
  131. type: "int",
  132. label: "Max Pitch",
  133. tooltip: "Highest allowed note for this voice",
  134. default: 72,
  135. min: 24,
  136. max: 96,
  137. step: 1,
  138. format: (v) =>
  139. `${noteNames()[v % 12]}<sub>${Math.floor(v / 12) - 1}</sub>`,
  140. },
  141. {
  142. name: "instrument",
  143. type: "select",
  144. label: "Instrument",
  145. tooltip: "MIDI instrument (General MIDI)",
  146. default: 0,
  147. groups: Object.entries(INSTRUMENTS).map(([label, names], g) => ({
  148. label,
  149. options: names.map((name, i) => ({
  150. value: Object.values(INSTRUMENTS).slice(0, g).flat().length + i,
  151. label: name,
  152. })),
  153. })),
  154. },
  155. {
  156. name: "voice_options",
  157. type: "checkboxes",
  158. label: "Options",
  159. tooltip: "Per voice",
  160. items: [
  161. {
  162. name: "chord_tones",
  163. label: "Chord/Scale Tones",
  164. tooltip: "[ ]: atonal, [-]: scale tones, [v]: chord tones",
  165. default: "chord",
  166. tristate: true,
  167. },
  168. {
  169. name: "allow_repetitions",
  170. label: "Allow Repetitions",
  171. tooltip:
  172. "Allow consecutive notes of the same pitch (disables M9 for this voice)",
  173. default: true,
  174. ariaInvalid: "false",
  175. },
  176. ],
  177. },
  178. ],
  179. },
  180. {
  181. name: "seed_pct",
  182. type: "int",
  183. label: "Variety",
  184. tooltip: "% of notes randomly pre-assigned for melodic diversity",
  185. default: 25,
  186. min: 0,
  187. max: 50,
  188. step: 5,
  189. format: (v) => `${v}%`,
  190. },
  191. {
  192. name: "dissonance",
  193. type: "float",
  194. label: "Dissonance",
  195. tooltip: "0 = prefer consonance, 0.5 = neutral, 1 = prefer dissonance",
  196. default: 0.5,
  197. min: 0,
  198. max: 1,
  199. step: 0.1,
  200. },
  201. {
  202. name: "max_interval",
  203. type: "int",
  204. label: "Max Interval",
  205. tooltip: "Max jump between consecutive notes (in semitones)",
  206. default: 6,
  207. min: 0,
  208. max: 12,
  209. step: 1,
  210. format: (v) => `${v} <abbr title="Semitones">st</abbr>`,
  211. },
  212. {
  213. name: "rest_pct",
  214. type: "int",
  215. label: "Max Holds/Rests",
  216. tooltip:
  217. "Max % of beats that can be holds or rests per voice (0 = disable holds/rests entirely)",
  218. default: 25,
  219. min: 0,
  220. max: 50,
  221. step: 5,
  222. format: (v) => `${v}%`,
  223. },
  224. {
  225. name: "constraints",
  226. type: "checkboxes",
  227. label: "Counterpoint",
  228. tooltip: "Voice interdependency",
  229. ariaInvalid: "true",
  230. items: [
  231. {
  232. name: "forbid_unisons",
  233. label: "Forbid Unisons",
  234. tooltip: "Prevent two voices from playing the same pitch",
  235. default: true,
  236. },
  237. {
  238. name: "forbid_parallels",
  239. label: "Forbid Parallels",
  240. tooltip: "Prevent parallel 5ths and octaves between voices",
  241. default: true,
  242. },
  243. ],
  244. },
  245. ];