Home Manual Reference Source Test

src/model/theory.js

  1. /**
  2. * Chromatic note-name array (12 entries starting from C);
  3. * flat-preferring by default (also used as MIDI key-signature labels),
  4. * sharp-preferring when keyIndex (kappa) is a sharp-favouring key.
  5. * @param {number} [keyIndex] - 0-11 (kappa); omitted means flat-preferring.
  6. * @return {string[]}
  7. * @example
  8. * noteNames(); // ["C", "Db", "D", "Eb", ...] flat default
  9. * noteNames(0); // ["C", "C#", "D", "D#", ...] C major prefers sharps
  10. * noteNames(5); // ["C", "Db", "D", "Eb", ...] F major prefers flats
  11. */
  12. export function noteNames(keyIndex) {
  13. return new Set([0, 2, 4, 7, 9, 11]).has(keyIndex)
  14. ? "C C# D D# E F F# G G# A A# B".split(" ")
  15. : "C Db D Eb E F Gb G Ab A Bb B".split(" ");
  16. }
  17. /**
  18. * Per-part time-grid; subdivision (tau) is the LCM of voice durations.
  19. * @param {{duration: string}[]} voice - Per-voice configs; only the first 'voices' entries are active.
  20. * @param {number} voices - Active voice count (|S|).
  21. * @param {number} numerator - Time-signature numerator (zeta).
  22. * @param {number} bars - Bar count for this part (iota).
  23. * @return {{subdivision: number, subBeatsPerBar: number, numSubBeats: number, activeBeats: Set<number>[]}}
  24. * Subdivision (tau); sub-beats per bar; T = iota * zeta * tau; per-voice onset sets (omicron).
  25. */
  26. export function timeGrid(voice, voices, numerator, bars) {
  27. const subdiv = {
  28. whole: 1,
  29. half: 1,
  30. quarter: 1,
  31. eighth: 2,
  32. triplet: 3,
  33. sixteenth: 4,
  34. };
  35. const gcd = (a, b) => (b === 0 ? a : gcd(b, a % b));
  36. const subdivision = voice.reduce((acc, v) => {
  37. const s = subdiv[v.duration] || 1;
  38. return (acc / gcd(acc, s)) * s;
  39. }, 1);
  40. const subBeatsPerBar = numerator * subdivision;
  41. const numSubBeats = bars * subBeatsPerBar;
  42. const stride = {
  43. whole: numerator * subdivision,
  44. half: Math.max(1, Math.round((numerator * subdivision) / 2)),
  45. quarter: subdivision,
  46. eighth: Math.max(1, Math.round(subdivision / 2)),
  47. triplet: Math.max(1, Math.round(subdivision / 3)),
  48. sixteenth: Math.max(1, Math.round(subdivision / 4)),
  49. };
  50. const activeBeats = voice.slice(0, voices).map((v) => {
  51. const s = stride[v.duration];
  52. const count = Math.floor(((bars - 1) * subBeatsPerBar) / s) + 1;
  53. return new Set(Array.from({ length: count }, (_, i) => i * s));
  54. });
  55. return { subdivision, subBeatsPerBar, numSubBeats, activeBeats };
  56. }
  57. /**
  58. * Chord tones per mode (mu): pitch classes (mod 12) for each Roman-numeral.
  59. * Insertion order = diatonic ordering. "NC" allows all classes.
  60. * V and NC are duplicated across major/minor (small DRY trade-off).
  61. * @type {{major: Object<string, number[]>, minor: Object<string, number[]>}}
  62. */
  63. export const CHORDS = {
  64. major: {
  65. I: [0, 4, 7],
  66. ii: [2, 5, 9],
  67. iii: [4, 7, 11],
  68. IV: [5, 9, 0],
  69. V: [7, 11, 2],
  70. vi: [9, 0, 4],
  71. viio: [11, 2, 5],
  72. NC: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
  73. },
  74. minor: {
  75. i: [0, 3, 7],
  76. iio: [2, 5, 8],
  77. III: [3, 7, 10],
  78. iv: [5, 8, 0],
  79. V: [7, 11, 2],
  80. bVI: [8, 0, 3],
  81. bVII: [10, 2, 5],
  82. NC: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
  83. },
  84. };
  85. /**
  86. * Roman-numeral display: with keyIndex returns the concrete chord name;
  87. * without keyIndex returns the function-label form for UI dropdowns.
  88. * @param {string} node - Roman-numeral chord symbol; "NC" and unknowns pass through.
  89. * @param {number} [keyIndex] - 0-11 (kappa); omitted means label form.
  90. * @return {string}
  91. * @example
  92. * chordName("I"); // "I: Tonic" (label form, key-independent)
  93. * chordName("V", 0); // "G" (dominant in C major)
  94. * chordName("vi", 0); // "Am" (minor from lowercase numeral)
  95. * chordName("viio", 0); // "Bo" (diminished from "o" suffix)
  96. * chordName("NC", 0); // "NC"
  97. */
  98. export function chordName(node, keyIndex) {
  99. if (keyIndex === undefined) {
  100. const labels = {
  101. I: "Tonic",
  102. ii: "Supertonic",
  103. iii: "Mediant",
  104. IV: "Subdominant",
  105. V: "Dominant",
  106. vi: "Submediant",
  107. viio: "Leading Tone",
  108. i: "Tonic",
  109. iio: "Supertonic",
  110. III: "Mediant",
  111. iv: "Subdominant",
  112. bVI: "Submediant",
  113. bVII: "Subtonic",
  114. NC: "No Chord",
  115. };
  116. return labels[node] ? `${node}: ${labels[node]}` : node;
  117. }
  118. if (node === "NC") {
  119. return "NC";
  120. }
  121. const root = (CHORDS.major[node] ?? CHORDS.minor[node])?.[0];
  122. if (root === undefined) {
  123. return node;
  124. }
  125. const suffix = /o$/.test(node) ? "o" : /^b?[a-z]/.test(node) ? "m" : "";
  126. return noteNames(keyIndex)[(root + keyIndex) % 12] + suffix;
  127. }
  128. /**
  129. * Creates the chord digraph G = (V, E, w) for a given mode, optionally tuned by a corpus preset (pi).
  130. * @param {'major'|'minor'} [mode='major'] - Tonal mode (mu); drives node set and default costs.
  131. * @param {?Object<string, {edges: Object<string, number>, chromatic: string[]}>} [preset]
  132. * Corpus preset (pi) keyed by mode (mu);
  133. * per mode edges (corpus-frequency nu thresholded by epsilon) and chromatic nodes;
  134. * null falls back to DEFAULTS.
  135. * @return {{
  136. * nodes: string[],
  137. * activeNodes: string[],
  138. * edges: [string,string][],
  139. * costs: Map<string,number>,
  140. * tonic: string,
  141. * }}
  142. * nodes (V, incl. chromatic), activeNodes (subset participating in edges),
  143. * edges (E, valid-edge keys of costs), costs (w, edge cost map), tonic ("I" or "i").
  144. */
  145. export function chordGraph(mode = "major", preset = null) {
  146. const tonic = mode === "minor" ? "i" : "I";
  147. const presetMode = preset?.[mode];
  148. const nodes = [
  149. ...Object.keys(CHORDS[mode]),
  150. ...(presetMode?.chromatic ?? []),
  151. ];
  152. const costs = edgeCosts(nodes, mode, presetMode);
  153. const edges = [...costs.keys()].map((k) => k.split("->"));
  154. const activeNodes = nodes.filter((n) => new Set(edges.flat()).has(n));
  155. return { nodes, activeNodes, edges, costs, tonic };
  156. }
  157. /**
  158. * Default chord-graph configuration:
  159. * structural costs (LOOP, NC, MISSING) and edge tables [cost, {major, minor}].
  160. * CADENCE (after NORMAL) overwrites shared edges.
  161. * MISSING is the sentinel cost for non-edges in the MZN cost matrix:
  162. * higher than any valid cost so the solver never traverses non-edge cells.
  163. * @type {{
  164. * CADENCE: [number, {major: string, minor: string}],
  165. * NORMAL: [number, {major: string, minor: string}],
  166. * NC: number,
  167. * LOOP: number,
  168. * MISSING: number,
  169. * }}
  170. */
  171. export const EDGES = {
  172. CADENCE: [
  173. 1,
  174. {
  175. major: `V->I IV->I ii->V`,
  176. minor: `V->i iv->i iio->V`,
  177. },
  178. ],
  179. NORMAL: [
  180. 2,
  181. {
  182. major: `
  183. I->ii I->iii I->IV I->V I->vi I->viio
  184. ii->V ii->viio
  185. iii->vi iii->IV
  186. IV->V IV->I IV->viio
  187. V->I V->vi
  188. vi->ii vi->IV
  189. viio->I viio->iii
  190. `,
  191. minor: `
  192. i->iv i->V i->bVI i->bVII i->III i->iio
  193. iv->V iv->i iv->iio iv->bVII
  194. V->i V->bVI
  195. iio->V
  196. III->bVI III->bVII III->iv
  197. bVI->bVII bVI->iv bVI->iio bVI->V
  198. bVII->III bVII->i bVII->bVI
  199. `,
  200. },
  201. ],
  202. NC: 4,
  203. LOOP: 8,
  204. MISSING: 16,
  205. };
  206. /**
  207. * @private Calculates costs with preset edges (skipping out-of-graph nodes) or
  208. * default NORMAL+CADENCE table for the mode.
  209. */
  210. function edgeCosts(nodes, mode, presetMode) {
  211. const costs = new Map();
  212. for (const v of nodes) {
  213. costs.set(`${v}->${v}`, EDGES.LOOP);
  214. if (v === "NC") {
  215. continue;
  216. }
  217. costs.set(`${v}->NC`, EDGES.NC);
  218. costs.set(`NC->${v}`, EDGES.NC);
  219. }
  220. if (presetMode) {
  221. const nodeSet = new Set(nodes);
  222. for (const [edge, cost] of Object.entries(presetMode.edges)) {
  223. const [from, to] = edge.split("->");
  224. if (!nodeSet.has(from) || !nodeSet.has(to)) {
  225. continue;
  226. }
  227. costs.set(edge, from === to ? Math.max(cost, EDGES.LOOP) : cost);
  228. }
  229. return costs; // skip defaults if preset edges exist
  230. }
  231. for (const [cost, byMode] of [EDGES.NORMAL, EDGES.CADENCE]) {
  232. for (const edge of byMode[mode].trim().split(/\s+/)) {
  233. costs.set(edge, cost);
  234. }
  235. }
  236. return costs;
  237. }