@libraz/libcantus 0.9.3 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/README.md +2 -2
  2. package/dist/analyze/index.cjs +39 -1947
  3. package/dist/analyze/index.cjs.map +1 -1
  4. package/dist/analyze/index.d.cts +197 -37
  5. package/dist/analyze/index.d.ts +197 -37
  6. package/dist/analyze/index.js +19 -8
  7. package/dist/chunk-36N73KZ3.cjs +246 -0
  8. package/dist/chunk-36N73KZ3.cjs.map +1 -0
  9. package/dist/chunk-4HLLZPXT.cjs +3077 -0
  10. package/dist/chunk-4HLLZPXT.cjs.map +1 -0
  11. package/dist/chunk-7MGZ7D62.cjs +498 -0
  12. package/dist/chunk-7MGZ7D62.cjs.map +1 -0
  13. package/dist/{chunk-4MHA432G.js → chunk-7NKDPF74.js} +403 -180
  14. package/dist/chunk-7NKDPF74.js.map +1 -0
  15. package/dist/chunk-CAN76NCU.cjs +679 -0
  16. package/dist/chunk-CAN76NCU.cjs.map +1 -0
  17. package/dist/{chunk-MIWO425M.js → chunk-D2FAJBYL.js} +101 -34
  18. package/dist/chunk-D2FAJBYL.js.map +1 -0
  19. package/dist/{chunk-SGU7SPLK.js → chunk-EHVEBWM5.js} +1056 -531
  20. package/dist/chunk-EHVEBWM5.js.map +1 -0
  21. package/dist/chunk-FCANXBAT.cjs +30 -0
  22. package/dist/chunk-FCANXBAT.cjs.map +1 -0
  23. package/dist/chunk-FNDWXRRI.cjs +479 -0
  24. package/dist/chunk-FNDWXRRI.cjs.map +1 -0
  25. package/dist/{chunk-IT2O4XFE.js → chunk-GQSGS2N4.js} +43 -24
  26. package/dist/chunk-GQSGS2N4.js.map +1 -0
  27. package/dist/{chunk-62QZKCJQ.js → chunk-HKGYCQ34.js} +10 -6
  28. package/dist/chunk-HKGYCQ34.js.map +1 -0
  29. package/dist/{chunk-AKLYPWIN.js → chunk-I24MV65U.js} +29 -6
  30. package/dist/chunk-I24MV65U.js.map +1 -0
  31. package/dist/{chunk-CHMXELDS.js → chunk-KMHOICIY.js} +311 -181
  32. package/dist/chunk-KMHOICIY.js.map +1 -0
  33. package/dist/chunk-LW53AHQU.cjs +1117 -0
  34. package/dist/chunk-LW53AHQU.cjs.map +1 -0
  35. package/dist/{chunk-2ZWMSJH6.js → chunk-NKIBQ6PT.js} +2 -2
  36. package/dist/chunk-NKIBQ6PT.js.map +1 -0
  37. package/dist/chunk-NP4NA2FB.cjs +106 -0
  38. package/dist/chunk-NP4NA2FB.cjs.map +1 -0
  39. package/dist/chunk-NVE34YGZ.cjs +1449 -0
  40. package/dist/chunk-NVE34YGZ.cjs.map +1 -0
  41. package/dist/{chunk-EH6EM37U.js → chunk-NYYWMV2C.js} +61 -49
  42. package/dist/chunk-NYYWMV2C.js.map +1 -0
  43. package/dist/{chunk-GOAW4VYL.js → chunk-Q2XUP227.js} +243 -117
  44. package/dist/chunk-Q2XUP227.js.map +1 -0
  45. package/dist/chunk-Q6OP4U43.cjs +327 -0
  46. package/dist/chunk-Q6OP4U43.cjs.map +1 -0
  47. package/dist/chunk-RC6GBMCD.cjs +845 -0
  48. package/dist/chunk-RC6GBMCD.cjs.map +1 -0
  49. package/dist/{chunk-C5XYKUVG.js → chunk-RK35RIN7.js} +370 -157
  50. package/dist/chunk-RK35RIN7.js.map +1 -0
  51. package/dist/chunk-RUMU3UA6.cjs +53 -0
  52. package/dist/chunk-RUMU3UA6.cjs.map +1 -0
  53. package/dist/chunk-T64C7ZD7.cjs +1 -0
  54. package/dist/chunk-T64C7ZD7.cjs.map +1 -0
  55. package/dist/chunk-XEBG3G53.js +498 -0
  56. package/dist/chunk-XEBG3G53.js.map +1 -0
  57. package/dist/{chunk-I5R7ZBJS.js → chunk-YLEMGEFH.js} +103 -59
  58. package/dist/chunk-YLEMGEFH.js.map +1 -0
  59. package/dist/chunk-ZEJR4TKL.cjs +173 -0
  60. package/dist/chunk-ZEJR4TKL.cjs.map +1 -0
  61. package/dist/core/index.cjs +130 -647
  62. package/dist/core/index.cjs.map +1 -1
  63. package/dist/core/index.d.cts +301 -39
  64. package/dist/core/index.d.ts +301 -39
  65. package/dist/core/index.js +54 -16
  66. package/dist/{function-DYt4iKsn.d.cts → function-BtiZb9i9.d.ts} +28 -7
  67. package/dist/{function-DVH4Eusr.d.ts → function-DNugAOrt.d.cts} +28 -7
  68. package/dist/generate/index.cjs +28 -4085
  69. package/dist/generate/index.cjs.map +1 -1
  70. package/dist/generate/index.d.cts +370 -62
  71. package/dist/generate/index.d.ts +370 -62
  72. package/dist/generate/index.js +23 -9
  73. package/dist/{index-CxqBQtH1.d.cts → index-6VR8-kit.d.ts} +19 -4
  74. package/dist/index-70p168VM.d.ts +192 -0
  75. package/dist/{index-CpOsP1y9.d.cts → index-Be74mcon.d.cts} +18 -17
  76. package/dist/index-BtXT6UQ0.d.cts +192 -0
  77. package/dist/{index-DxvARIP2.d.ts → index-C5UBbQ4G.d.ts} +18 -17
  78. package/dist/{index-Bt4tOUD9.d.ts → index-C83mZI1P.d.ts} +93 -8
  79. package/dist/{index-Bg5dYMmZ.d.ts → index-D6b6na69.d.cts} +14 -11
  80. package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
  81. package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
  82. package/dist/index-OG65ocOr.d.cts +272 -0
  83. package/dist/index-OG65ocOr.d.ts +272 -0
  84. package/dist/{index-Jc0Ryi79.d.ts → index-tJhkmQgE.d.cts} +19 -4
  85. package/dist/{index-Dikst44v.d.cts → index-tSQh-xwY.d.ts} +14 -11
  86. package/dist/{index-BNlGP86A.d.cts → index-y_ak9_Hi.d.cts} +93 -8
  87. package/dist/index.cjs +419 -7383
  88. package/dist/index.cjs.map +1 -1
  89. package/dist/index.d.cts +14 -14
  90. package/dist/index.d.ts +14 -14
  91. package/dist/index.js +111 -27
  92. package/dist/model/index.cjs +8 -2562
  93. package/dist/model/index.cjs.map +1 -1
  94. package/dist/model/index.d.cts +433 -75
  95. package/dist/model/index.d.ts +433 -75
  96. package/dist/model/index.js +6 -7
  97. package/dist/styled-DIRkk9Uz.d.ts +506 -0
  98. package/dist/styled-DJbkJQfj.d.cts +506 -0
  99. package/dist/theory/index.cjs +172 -1822
  100. package/dist/theory/index.cjs.map +1 -1
  101. package/dist/theory/index.d.cts +151 -233
  102. package/dist/theory/index.d.ts +151 -233
  103. package/dist/theory/index.js +30 -7
  104. package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
  105. package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
  106. package/package.json +24 -6
  107. package/dist/chunk-2ZWMSJH6.js.map +0 -1
  108. package/dist/chunk-4MHA432G.js.map +0 -1
  109. package/dist/chunk-62QZKCJQ.js.map +0 -1
  110. package/dist/chunk-AKLYPWIN.js.map +0 -1
  111. package/dist/chunk-C5XYKUVG.js.map +0 -1
  112. package/dist/chunk-CHMXELDS.js.map +0 -1
  113. package/dist/chunk-DYWFNC4A.js +0 -152
  114. package/dist/chunk-DYWFNC4A.js.map +0 -1
  115. package/dist/chunk-EH6EM37U.js.map +0 -1
  116. package/dist/chunk-FBQK3ZQN.js +0 -131
  117. package/dist/chunk-FBQK3ZQN.js.map +0 -1
  118. package/dist/chunk-GOAW4VYL.js.map +0 -1
  119. package/dist/chunk-I5R7ZBJS.js.map +0 -1
  120. package/dist/chunk-IT2O4XFE.js.map +0 -1
  121. package/dist/chunk-MIWO425M.js.map +0 -1
  122. package/dist/chunk-SGU7SPLK.js.map +0 -1
  123. package/dist/index-CGMZNCx6.d.cts +0 -140
  124. package/dist/index-CGMZNCx6.d.ts +0 -140
  125. package/dist/index-CnZCo29u.d.cts +0 -108
  126. package/dist/index-JnF4v9bx.d.ts +0 -108
  127. package/dist/styled-DZ7GjShb.d.ts +0 -282
  128. package/dist/styled-ZTbdaVFM.d.cts +0 -282
  129. package/src/analyze/arrange/index.ts +0 -21
  130. package/src/analyze/arrange/internal.ts +0 -127
  131. package/src/analyze/arrange/tension.ts +0 -199
  132. package/src/analyze/arrange/tracks.ts +0 -312
  133. package/src/analyze/detect/index.ts +0 -272
  134. package/src/analyze/functional/borrowed.ts +0 -84
  135. package/src/analyze/functional/cadence.ts +0 -66
  136. package/src/analyze/functional/function.ts +0 -184
  137. package/src/analyze/functional/index.ts +0 -23
  138. package/src/analyze/functional/internal.ts +0 -29
  139. package/src/analyze/functional/roman.ts +0 -402
  140. package/src/analyze/index.ts +0 -49
  141. package/src/analyze/timeline/index.ts +0 -451
  142. package/src/analyze/voice/index.ts +0 -339
  143. package/src/core/event-index/index.ts +0 -104
  144. package/src/core/index.ts +0 -61
  145. package/src/core/interval/index.ts +0 -81
  146. package/src/core/meter/index.ts +0 -302
  147. package/src/core/pitch/index.ts +0 -311
  148. package/src/core/random/index.ts +0 -76
  149. package/src/core/tuning/index.ts +0 -186
  150. package/src/core/types.ts +0 -29
  151. package/src/core/validation/index.ts +0 -119
  152. package/src/generate/bass/index.ts +0 -373
  153. package/src/generate/countermelody/index.ts +0 -470
  154. package/src/generate/drums/beat.ts +0 -266
  155. package/src/generate/drums/euclid.ts +0 -65
  156. package/src/generate/drums/fills.ts +0 -356
  157. package/src/generate/drums/ghost.ts +0 -119
  158. package/src/generate/drums/hihat.ts +0 -285
  159. package/src/generate/drums/hit.ts +0 -31
  160. package/src/generate/drums/index.ts +0 -361
  161. package/src/generate/drums/internal.ts +0 -257
  162. package/src/generate/drums/kick.ts +0 -160
  163. package/src/generate/drums/percussion.ts +0 -121
  164. package/src/generate/drums/rng.ts +0 -7
  165. package/src/generate/drums/swing.ts +0 -71
  166. package/src/generate/groove/index.ts +0 -351
  167. package/src/generate/harmonize/index.ts +0 -451
  168. package/src/generate/index.ts +0 -58
  169. package/src/generate/motif/index.ts +0 -422
  170. package/src/generate/progression/index.ts +0 -362
  171. package/src/generate/reharmony/index.ts +0 -323
  172. package/src/generate/rhythm/index.ts +0 -175
  173. package/src/index.ts +0 -13
  174. package/src/model/chord.ts +0 -442
  175. package/src/model/index.ts +0 -15
  176. package/src/model/interval.ts +0 -82
  177. package/src/model/key.ts +0 -266
  178. package/src/model/note.ts +0 -176
  179. package/src/model/progression.ts +0 -161
  180. package/src/model/shared.ts +0 -12
  181. package/src/theory/chord/index.ts +0 -375
  182. package/src/theory/chordscale/index.ts +0 -374
  183. package/src/theory/counterpoint/index.ts +0 -230
  184. package/src/theory/harmony/index.ts +0 -77
  185. package/src/theory/index.ts +0 -112
  186. package/src/theory/safety/index.ts +0 -488
  187. package/src/theory/scale/degrees.ts +0 -123
  188. package/src/theory/scale/index.ts +0 -28
  189. package/src/theory/scale/key.ts +0 -56
  190. package/src/theory/scale/masks.ts +0 -153
  191. package/src/theory/spelling/index.ts +0 -249
  192. package/src/theory/symbol/index.ts +0 -268
  193. package/src/theory/voicing/index.ts +0 -5
  194. package/src/theory/voicing/internal.ts +0 -168
  195. package/src/theory/voicing/leading.ts +0 -112
  196. package/src/theory/voicing/satb.ts +0 -216
  197. package/src/theory/voicing/styled.ts +0 -199
  198. package/tsconfig.json +0 -23
@@ -8,27 +8,202 @@ import {
8
8
  isScaleTone,
9
9
  majorKey,
10
10
  makeChord,
11
- minorKey,
12
11
  scaleTonesInDegreeOrder
13
- } from "./chunk-MIWO425M.js";
12
+ } from "./chunk-D2FAJBYL.js";
14
13
  import {
14
+ InvalidInputError,
15
15
  assertFiniteNumber,
16
- assertGenerationBudget
17
- } from "./chunk-FBQK3ZQN.js";
16
+ assertGenerationBudget,
17
+ assertInteger,
18
+ pitchClassOf
19
+ } from "./chunk-XEBG3G53.js";
18
20
 
19
- // src/analyze/functional/internal.ts
20
- function mod12(n) {
21
- return (n % 12 + 12) % 12;
21
+ // src/analyze/detect/index.ts
22
+ function uniquePitchClasses(pitches) {
23
+ return [...new Set(pitches.map(pitchClassOf))].sort((a, b) => a - b);
24
+ }
25
+ function assertPitches(pitches) {
26
+ assertGenerationBudget(pitches.length, "detection pitches");
27
+ for (let index = 0; index < pitches.length; index += 1) {
28
+ assertFiniteNumber(pitches[index] ?? Number.NaN, `pitches[${index}]`);
29
+ }
22
30
  }
31
+ function detectChord(pitches, opts = {}) {
32
+ assertPitches(pitches);
33
+ if (opts.bassPc !== void 0) assertFiniteNumber(opts.bassPc, "bassPc");
34
+ const input = uniquePitchClasses(pitches);
35
+ if (input.length === 0) {
36
+ return [];
37
+ }
38
+ const inputKind = opts.input === void 0 || opts.input === "auto" ? pitches.every((pitch) => Number.isInteger(pitch) && pitch >= 0 && pitch <= 11) ? "pitchClass" : "midi" : opts.input;
39
+ const bassPc = opts.bassPc !== void 0 ? pitchClassOf(opts.bassPc) : inputKind === "midi" ? pitchClassOf(pitches.reduce((lowest, pitch) => Math.min(lowest, pitch), Infinity)) : void 0;
40
+ const inputSet = new Set(input);
41
+ const matches = [];
42
+ const qualities = chordQualities();
43
+ for (const rootPc of input) {
44
+ for (const quality of qualities) {
45
+ const chord = makeChord(rootPc, quality);
46
+ const tones = chordPitchClasses(chord);
47
+ const toneSet = new Set(tones);
48
+ const missingPcs = tones.filter((pc) => !inputSet.has(pc));
49
+ const fifthPc = pitchClassOf(rootPc + 7);
50
+ if (missingPcs.length > 1 || missingPcs.length === 1 && missingPcs[0] !== fifthPc) {
51
+ continue;
52
+ }
53
+ const extraPcs = input.filter((pc) => !toneSet.has(pc));
54
+ const bassIndex = bassPc === void 0 ? -1 : chord.intervals.findIndex((iv) => pitchClassOf(rootPc + iv) === bassPc);
55
+ const inversion = bassPc === void 0 || bassIndex < 0 ? null : bassIndex;
56
+ const match = {
57
+ rootPc,
58
+ quality,
59
+ missingPcs,
60
+ extraPcs,
61
+ exact: extraPcs.length === 0 && missingPcs.length === 0,
62
+ inversion
63
+ };
64
+ if (bassPc !== void 0 && bassPc !== rootPc) {
65
+ match.bassPc = bassPc;
66
+ }
67
+ matches.push(match);
68
+ }
69
+ }
70
+ matches.sort((a, b) => {
71
+ if (a.extraPcs.length !== b.extraPcs.length) {
72
+ return a.extraPcs.length - b.extraPcs.length;
73
+ }
74
+ if (a.missingPcs.length !== b.missingPcs.length) {
75
+ return a.missingPcs.length - b.missingPcs.length;
76
+ }
77
+ if (a.inversion === 0 !== (b.inversion === 0)) {
78
+ return a.inversion === 0 ? -1 : 1;
79
+ }
80
+ const aSize = chordPitchClasses(makeChord(a.rootPc, a.quality)).length;
81
+ const bSize = chordPitchClasses(makeChord(b.rootPc, b.quality)).length;
82
+ return bSize - aSize;
83
+ });
84
+ return matches;
85
+ }
86
+ function detectChordBest(pitches, opts = {}) {
87
+ const best = detectChord(pitches, opts)[0];
88
+ if (!best) {
89
+ return null;
90
+ }
91
+ return makeChord(best.rootPc, best.quality, best.bassPc);
92
+ }
93
+ var MINOR_VARIANTS = [
94
+ { variant: "natural", mask: NATURAL_MINOR_MASK },
95
+ { variant: "harmonic", mask: HARMONIC_MINOR_MASK },
96
+ { variant: "melodic", mask: MELODIC_MINOR_MASK }
97
+ ];
98
+ var MAJOR_VARIANTS = [{ variant: "major", mask: MAJOR_MASK }];
99
+ function detectKey(pitches, opts = {}) {
100
+ assertPitches(pitches);
101
+ const input = uniquePitchClasses(pitches);
102
+ if (input.length === 0) {
103
+ return [];
104
+ }
105
+ const weights = opts.weights;
106
+ if (weights !== void 0 && weights.length !== pitches.length) {
107
+ throw new InvalidInputError("weights must have one entry per pitch");
108
+ }
109
+ const counts = /* @__PURE__ */ new Map();
110
+ let total = 0;
111
+ for (let index = 0; index < pitches.length; index += 1) {
112
+ const weight = weights === void 0 ? 1 : weights[index] ?? 0;
113
+ assertFiniteNumber(weight, `weights[${index}]`);
114
+ if (weight <= 0) {
115
+ continue;
116
+ }
117
+ const pc = pitchClassOf(pitches[index] ?? 0);
118
+ counts.set(pc, (counts.get(pc) ?? 0) + weight);
119
+ total += weight;
120
+ }
121
+ if (total === 0) {
122
+ return [];
123
+ }
124
+ const results = [];
125
+ for (let tonic = 0; tonic < 12; tonic += 1) {
126
+ for (const mode of ["major", "minor"]) {
127
+ const variants = mode === "major" ? MAJOR_VARIANTS : MINOR_VARIANTS;
128
+ let inScale = 0;
129
+ let weighted = Number.NEGATIVE_INFINITY;
130
+ let best = variants[0];
131
+ for (const candidate of variants) {
132
+ let variantInScale = 0;
133
+ let variantWeighted = 0;
134
+ for (const pc of input) {
135
+ const offset = (pc - tonic + 12) % 12;
136
+ if (candidate.mask >> offset & 1) {
137
+ variantInScale += 1;
138
+ }
139
+ }
140
+ for (const [pc, count] of counts) {
141
+ const offset = (pc - tonic + 12) % 12;
142
+ if (candidate.mask >> offset & 1) {
143
+ variantWeighted += count;
144
+ }
145
+ }
146
+ if (variantWeighted > weighted) {
147
+ weighted = variantWeighted;
148
+ inScale = variantInScale;
149
+ best = candidate;
150
+ }
151
+ }
152
+ weighted += (counts.get(tonic) ?? 0) * 0.5;
153
+ results.push({
154
+ key: { rootPc: tonic, modeMask12: best.mask },
155
+ mode,
156
+ variant: best.variant,
157
+ fit: inScale / input.length,
158
+ score: weighted / total
159
+ });
160
+ }
161
+ }
162
+ results.sort((a, b) => b.score - a.score);
163
+ return results;
164
+ }
165
+ var DEFAULT_VELOCITY = 100;
166
+ function detectKeyFromNotes(notes) {
167
+ const sounding = notes.filter((note) => note.durationBeat > 0);
168
+ return detectKey(
169
+ sounding.map((note) => note.pitch),
170
+ { weights: sounding.map((note) => note.durationBeat * (note.velocity ?? DEFAULT_VELOCITY)) }
171
+ );
172
+ }
173
+ function detectKeyBest(pitches, opts = {}) {
174
+ return detectKey(pitches, opts)[0] ?? null;
175
+ }
176
+
177
+ // src/analyze/functional/internal.ts
23
178
  function degreeRootPc(degreeNumber, key) {
24
179
  const tones = scaleTonesInDegreeOrder(key);
25
180
  if (tones.length === 0) {
26
- return mod12(key.rootPc);
181
+ return pitchClassOf(key.rootPc);
27
182
  }
28
- return tones[(degreeNumber - 1) % tones.length] ?? mod12(key.rootPc);
183
+ const index = ((degreeNumber - 1) % tones.length + tones.length) % tones.length;
184
+ return tones[index] ?? pitchClassOf(key.rootPc);
185
+ }
186
+ function loweredDegrees(key) {
187
+ const tones = scaleTonesInDegreeOrder(key);
188
+ const out = /* @__PURE__ */ new Set();
189
+ if (tones.length !== 7) {
190
+ return out;
191
+ }
192
+ const major = scaleTonesInDegreeOrder({ rootPc: pitchClassOf(key.rootPc), modeMask12: MAJOR_MASK });
193
+ for (let index = 0; index < 7; index += 1) {
194
+ const tone = tones[index];
195
+ const majorTone = major[index];
196
+ if (tone !== void 0 && majorTone !== void 0 && pitchClassOf(majorTone - tone) === 1) {
197
+ out.add(index + 1);
198
+ }
199
+ }
200
+ return out;
201
+ }
202
+ function romanReference(key) {
203
+ return scaleTonesInDegreeOrder(key).length === 7 ? key : majorKey(pitchClassOf(key.rootPc));
29
204
  }
30
205
  function isNeapolitan(chord, key) {
31
- return mod12(chord.rootPc - key.rootPc) === 1 && chord.quality === "maj";
206
+ return pitchClassOf(chord.rootPc - key.rootPc) === 1 && chord.quality === "maj";
32
207
  }
33
208
 
34
209
  // src/analyze/functional/roman.ts
@@ -99,7 +274,17 @@ var ROMAN_STYLE = {
99
274
  "7b13": { lower: false, suffix: "7b13" },
100
275
  "11": { lower: false, suffix: "11" },
101
276
  "13": { lower: false, suffix: "13" },
102
- "5": { lower: false, suffix: "5" }
277
+ "5": { lower: false, suffix: "5" },
278
+ "7sus4": { lower: false, suffix: "7sus4" },
279
+ "7b5": { lower: false, suffix: "7b5" },
280
+ "7alt": { lower: false, suffix: "7alt" },
281
+ "13b9": { lower: false, suffix: "13b9" },
282
+ maj13: { lower: false, suffix: "maj13" },
283
+ "maj7#11": { lower: false, suffix: "maj7#11" },
284
+ min11: { lower: true, suffix: "11" },
285
+ min13: { lower: true, suffix: "13" },
286
+ minAdd9: { lower: true, suffix: "add9" },
287
+ "min6/9": { lower: true, suffix: "69" }
103
288
  };
104
289
  var SUFFIX_QUALITY = new Map(
105
290
  Object.entries(ROMAN_STYLE).map(
@@ -130,24 +315,26 @@ function parseInversion(figures) {
130
315
  function parseSimpleRoman(text, key) {
131
316
  const match = /^([b#]?)([iIvV]+)(.*)$/.exec(text.trim());
132
317
  if (!match) {
133
- throw new Error(`Invalid Roman numeral: ${text}`);
318
+ throw new InvalidInputError(`Invalid Roman numeral: ${text}`);
134
319
  }
135
320
  const accidental = match[1] === "b" ? -1 : match[1] === "#" ? 1 : 0;
136
321
  const glyph = (match[2] ?? "").toUpperCase();
137
322
  const degreeNumber = ROMAN_TO_DEGREE[glyph];
138
323
  if (degreeNumber === void 0) {
139
- throw new Error(`Invalid Roman numeral: ${text}`);
324
+ throw new InvalidInputError(`Invalid Roman numeral: ${text}`);
140
325
  }
141
326
  const isUpper = (match[2] ?? "")[0] === (match[2] ?? "")[0]?.toUpperCase();
142
327
  const suffix = match[3] ?? "";
143
- const rootPc = mod12(degreeRootPc(degreeNumber, key) + accidental);
328
+ const frame = romanReference(key);
329
+ const redundantFlat = accidental === -1 && loweredDegrees(frame).has(degreeNumber);
330
+ const rootPc = pitchClassOf(degreeRootPc(degreeNumber, frame) + (redundantFlat ? 0 : accidental));
144
331
  const canonical = SUFFIX_QUALITY.get(`${isUpper ? "u" : "l"}:${suffix}`);
145
332
  if (canonical !== void 0) {
146
333
  return { rootPc, quality: canonical, inversion: 0 };
147
334
  }
148
335
  const parsedSuffix = /^(o|°|dim|ø|\+|aug)?(maj7|M7)?(65|64|43|42|7|6|2)?$/.exec(suffix);
149
336
  if (!parsedSuffix) {
150
- throw new Error(`Unsupported suffix "${suffix}" in Roman numeral: ${text}`);
337
+ throw new InvalidInputError(`Unsupported suffix "${suffix}" in Roman numeral: ${text}`);
151
338
  }
152
339
  const qualityMarker = parsedSuffix[1] ?? "";
153
340
  const isDim = qualityMarker === "o" || qualityMarker === "\xB0" || qualityMarker === "dim";
@@ -156,7 +343,9 @@ function parseSimpleRoman(text, key) {
156
343
  const explicitMaj7 = parsedSuffix[2] !== void 0;
157
344
  const figures = parsedSuffix[3] ?? "";
158
345
  if (figures !== "" && !INVERSION_FIGURES.has(figures)) {
159
- throw new Error(`Unsupported figured-bass or extension "${figures}" in Roman numeral: ${text}`);
346
+ throw new InvalidInputError(
347
+ `Unsupported figured-bass or extension "${figures}" in Roman numeral: ${text}`
348
+ );
160
349
  }
161
350
  const { inversion, seventh } = parseInversion(figures);
162
351
  const hasSeventh = explicitMaj7 || seventh || isHalfDim;
@@ -185,7 +374,7 @@ function parseSimpleRoman(text, key) {
185
374
  function chordFromParsed(parsed) {
186
375
  const chord = makeChord(parsed.rootPc, parsed.quality);
187
376
  if (parsed.inversion > 0 && parsed.inversion < chord.intervals.length) {
188
- chord.bassPc = mod12(parsed.rootPc + (chord.intervals[parsed.inversion] ?? 0));
377
+ chord.bassPc = pitchClassOf(parsed.rootPc + (chord.intervals[parsed.inversion] ?? 0));
189
378
  }
190
379
  return chord;
191
380
  }
@@ -205,26 +394,32 @@ function romanStyle(quality) {
205
394
  return ROMAN_STYLE[quality];
206
395
  }
207
396
  function romanSpelling(rootPc, key) {
208
- const tones = scaleTonesInDegreeOrder(key);
209
- const diatonic = tones.indexOf(mod12(rootPc));
397
+ const tones = scaleTonesInDegreeOrder(romanReference(key));
398
+ const diatonic = tones.indexOf(pitchClassOf(rootPc));
210
399
  if (diatonic >= 0) {
211
400
  return { degreeNumber: diatonic + 1, accidental: "" };
212
401
  }
402
+ const lowered = loweredDegrees(romanReference(key));
403
+ for (let i = 0; i < tones.length; i += 1) {
404
+ if (lowered.has(i + 1) && pitchClassOf((tones[i] ?? 0) + 1) === pitchClassOf(rootPc)) {
405
+ return { degreeNumber: i + 1, accidental: "#" };
406
+ }
407
+ }
213
408
  for (let i = 0; i < tones.length; i += 1) {
214
409
  const degreeNumber2 = i + 1;
215
410
  if (degreeNumber2 === 1 || degreeNumber2 === 5) {
216
411
  continue;
217
412
  }
218
- if (mod12((tones[i] ?? 0) - 1) === mod12(rootPc)) {
413
+ if (pitchClassOf((tones[i] ?? 0) - 1) === pitchClassOf(rootPc)) {
219
414
  return { degreeNumber: degreeNumber2, accidental: "b" };
220
415
  }
221
416
  }
222
417
  for (let i = 0; i < tones.length; i += 1) {
223
- if (mod12((tones[i] ?? 0) + 1) === mod12(rootPc)) {
418
+ if (pitchClassOf((tones[i] ?? 0) + 1) === pitchClassOf(rootPc)) {
224
419
  return { degreeNumber: i + 1, accidental: "#" };
225
420
  }
226
421
  }
227
- const [degreeNumber, accidental] = OFFSET_SPELLING[mod12(rootPc - key.rootPc)] ?? [1, ""];
422
+ const [degreeNumber, accidental] = OFFSET_SPELLING[pitchClassOf(rootPc - key.rootPc)] ?? [1, ""];
228
423
  return { degreeNumber, accidental };
229
424
  }
230
425
  function baseMarker(quality) {
@@ -258,14 +453,65 @@ var SEVENTH_FIGURE_QUALITIES = /* @__PURE__ */ new Set([
258
453
  "aug7",
259
454
  "augMaj7"
260
455
  ]);
261
- function chordToRoman(chord, key) {
456
+ function numeralFor(degreeNumber, lower) {
457
+ const glyph = ROMAN[degreeNumber - 1];
458
+ if (glyph === void 0) {
459
+ throw new InvalidInputError(`no Roman numeral for scale degree ${degreeNumber}`);
460
+ }
461
+ return lower ? glyph.toLowerCase() : glyph;
462
+ }
463
+ function isDiatonicChord(chord, key) {
464
+ return chordPitchClasses(chord).every((pc) => isScaleTone(pc, key));
465
+ }
466
+ function isAppliedDominantSonority(chord) {
467
+ const has = (semitones) => chord.intervals.some((i) => pitchClassOf(i) === semitones);
468
+ return chord.quality === "maj" || has(4) && has(10);
469
+ }
470
+ var LEADING_TONE_QUALITIES = /* @__PURE__ */ new Set(["dim", "dim7", "m7b5"]);
471
+ function degreeTriad(tones, index) {
472
+ const root = tones[index] ?? 0;
473
+ return {
474
+ third: pitchClassOf((tones[(index + 2) % tones.length] ?? 0) - root),
475
+ fifth: pitchClassOf((tones[(index + 4) % tones.length] ?? 0) - root)
476
+ };
477
+ }
478
+ function appliedTarget(chord, key) {
479
+ const dominant = isAppliedDominantSonority(chord);
480
+ const leadingTone = LEADING_TONE_QUALITIES.has(chord.quality);
481
+ if (!dominant && !leadingTone) {
482
+ return null;
483
+ }
484
+ const tones = scaleTonesInDegreeOrder(romanReference(key));
485
+ for (let index = 1; index < tones.length; index += 1) {
486
+ const targetRoot = tones[index];
487
+ if (targetRoot === void 0) {
488
+ continue;
489
+ }
490
+ const triad = degreeTriad(tones, index);
491
+ if (triad.fifth === 6) {
492
+ continue;
493
+ }
494
+ const expectedRoot = pitchClassOf(targetRoot + (dominant ? 7 : -1));
495
+ if (expectedRoot === pitchClassOf(chord.rootPc)) {
496
+ return { degreeNumber: index + 1, rootPc: targetRoot, lower: triad.third === 3 };
497
+ }
498
+ }
499
+ return null;
500
+ }
501
+ function chordToRoman(chord, key, opts = {}) {
502
+ if (opts.applied === true && !isDiatonicChord(chord, key)) {
503
+ const target = appliedTarget(chord, key);
504
+ if (target !== null) {
505
+ const local = chordToRoman(chord, majorKey(target.rootPc));
506
+ return `${local}/${numeralFor(target.degreeNumber, target.lower)}`;
507
+ }
508
+ }
262
509
  const { degreeNumber, accidental } = romanSpelling(chord.rootPc, key);
263
510
  const { lower, suffix } = romanStyle(chord.quality);
264
- const numeral = ROMAN[degreeNumber - 1] ?? "I";
265
- const cased = lower ? numeral.toLowerCase() : numeral;
511
+ const cased = numeralFor(degreeNumber, lower);
266
512
  let inversion = 0;
267
513
  if (chord.bassPc !== void 0) {
268
- const idx = chord.intervals.findIndex((iv) => mod12(chord.rootPc + iv) === chord.bassPc);
514
+ const idx = chord.intervals.findIndex((iv) => pitchClassOf(chord.rootPc + iv) === chord.bassPc);
269
515
  if (idx > 0) {
270
516
  inversion = idx;
271
517
  }
@@ -281,7 +527,7 @@ function chordToRoman(chord, key) {
281
527
 
282
528
  // src/analyze/functional/borrowed.ts
283
529
  function isHarmonicMinorAlteration(chord, key) {
284
- return isMinorKey(key) && isDiatonic(chord, { rootPc: mod12(key.rootPc), modeMask12: HARMONIC_MINOR_MASK });
530
+ return isMinorKey(key) && isDiatonic(chord, { rootPc: pitchClassOf(key.rootPc), modeMask12: HARMONIC_MINOR_MASK });
285
531
  }
286
532
  function isBorrowedChord(chord, key) {
287
533
  if (isDiatonic(chord, key)) {
@@ -300,7 +546,7 @@ function borrowedSource(chord, key) {
300
546
  return "neapolitan";
301
547
  }
302
548
  if (isBorrowedChord(chord, key)) {
303
- return isMinorKey(key) ? "parallel-major" : "parallel-minor";
549
+ return isMinorKey(key) ? "parallelMajor" : "parallelMinor";
304
550
  }
305
551
  return null;
306
552
  }
@@ -338,14 +584,35 @@ function isMinorKey(key) {
338
584
  return Boolean(hasMinorThird) && !hasMajorThird2;
339
585
  }
340
586
  function functionOf(chord, key) {
341
- return FUNCTION_BY_OFFSET[mod12(chord.rootPc - key.rootPc)] ?? "tonic";
587
+ const offset = pitchClassOf(chord.rootPc - key.rootPc);
588
+ if (isNeapolitan(chord, key)) {
589
+ return "subdominant";
590
+ }
591
+ if (isAppliedDominant(chord, key)) {
592
+ return "dominant";
593
+ }
594
+ if (!isMinorKey(key) && hasMajorThird(chord) && (offset === 8 || offset === 10)) {
595
+ return "subdominant";
596
+ }
597
+ return FUNCTION_BY_OFFSET[offset] ?? "tonic";
598
+ }
599
+ function isAppliedDominant(chord, key) {
600
+ if (isDiatonic(chord, key)) {
601
+ return false;
602
+ }
603
+ const root = pitchClassOf(chord.rootPc);
604
+ const resolvesTo = (step) => isScaleTone(pitchClassOf(root + step), key);
605
+ if (hasMajorThird(chord) && chord.intervals.some((i) => pitchClassOf(i) === 10)) {
606
+ return resolvesTo(5) || resolvesTo(11);
607
+ }
608
+ return isDiminishedQuality(chord.quality) && resolvesTo(1);
342
609
  }
343
610
  function isDiatonic(chord, key) {
344
611
  return chordPitchClasses(chord).every((pc) => isScaleTone(pc, key));
345
612
  }
346
613
  function parallelKey(key) {
347
614
  return {
348
- rootPc: mod12(key.rootPc),
615
+ rootPc: pitchClassOf(key.rootPc),
349
616
  modeMask12: isMinorKey(key) ? MAJOR_MASK : NATURAL_MINOR_MASK
350
617
  };
351
618
  }
@@ -353,40 +620,28 @@ function isDiminishedQuality(quality) {
353
620
  return quality === "dim" || quality === "dim7" || quality === "m7b5";
354
621
  }
355
622
  function hasMajorThird(chord) {
356
- return chord.intervals.some((interval) => mod12(interval) === 4);
357
- }
358
- function qualityAwareFunction(chord, key) {
359
- const offset = mod12(chord.rootPc - key.rootPc);
360
- if (isDiminishedQuality(chord.quality) && (offset === 11 || offset === 6)) {
361
- return "dominant";
362
- }
363
- if (isNeapolitan(chord, key)) {
364
- return "subdominant";
365
- }
366
- if (!isMinorKey(key) && hasMajorThird(chord) && (offset === 8 || offset === 10)) {
367
- return "subdominant";
368
- }
369
- return functionOf(chord, key);
623
+ return chord.intervals.some((interval) => pitchClassOf(interval) === 4);
370
624
  }
371
625
  function analyzeChord(chord, key) {
372
626
  const source = borrowedSource(chord, key);
373
627
  return {
374
- function: qualityAwareFunction(chord, key),
628
+ function: functionOf(chord, key),
375
629
  borrowed: source !== null,
376
630
  source,
377
631
  roman: chordToRoman(chord, key)
378
632
  };
379
633
  }
380
634
  function secondaryDominant(targetDegree, key) {
635
+ assertInteger(targetDegree, "targetDegree", 0, 6);
381
636
  const targetRoot = degreeRootPc(targetDegree + 1, key);
382
- return makeChord(mod12(targetRoot + 7), "dom7");
637
+ return makeChord(pitchClassOf(targetRoot + 7), "dom7");
383
638
  }
384
639
 
385
640
  // src/analyze/functional/cadence.ts
386
641
  function detectCadence(from, to, key) {
387
- const tonic = mod12(key.rootPc);
388
- const fromOffset = mod12(from.rootPc - tonic);
389
- const toOffset = mod12(to.rootPc - tonic);
642
+ const tonic = pitchClassOf(key.rootPc);
643
+ const fromOffset = pitchClassOf(from.rootPc - tonic);
644
+ const toOffset = pitchClassOf(to.rootPc - tonic);
390
645
  const deceptiveTargets = isMinorKey(key) ? [8] : [8, 9];
391
646
  if (fromOffset === 7 && toOffset === 0) {
392
647
  return "authentic";
@@ -403,134 +658,12 @@ function detectCadence(from, to, key) {
403
658
  return null;
404
659
  }
405
660
 
406
- // src/analyze/detect/index.ts
407
- function pitchClass(value) {
408
- return (Math.trunc(value) % 12 + 12) % 12;
409
- }
410
- function uniquePitchClasses(pitches) {
411
- return [...new Set(pitches.map(pitchClass))].sort((a, b) => a - b);
412
- }
413
- function assertPitches(pitches) {
414
- assertGenerationBudget(pitches.length, "detection pitches");
415
- for (let index = 0; index < pitches.length; index += 1) {
416
- assertFiniteNumber(pitches[index] ?? Number.NaN, `pitches[${index}]`);
417
- }
418
- }
419
- function detectChord(pitches, opts = {}) {
420
- assertPitches(pitches);
421
- if (opts.bassPc !== void 0) assertFiniteNumber(opts.bassPc, "bassPc");
422
- const input = uniquePitchClasses(pitches);
423
- if (input.length === 0) {
424
- return [];
425
- }
426
- const inputKind = opts.input === void 0 || opts.input === "auto" ? pitches.every((pitch) => Number.isInteger(pitch) && pitch >= 0 && pitch <= 11) ? "pitchClass" : "midi" : opts.input;
427
- const bassPc = opts.bassPc !== void 0 ? pitchClass(opts.bassPc) : inputKind === "midi" ? pitchClass(pitches.reduce((lowest, pitch) => Math.min(lowest, pitch), Infinity)) : void 0;
428
- const inputSet = new Set(input);
429
- const matches = [];
430
- const qualities = chordQualities();
431
- for (const rootPc of input) {
432
- for (const quality of qualities) {
433
- const chord = makeChord(rootPc, quality);
434
- const tones = chordPitchClasses(chord);
435
- const toneSet = new Set(tones);
436
- const missingPcs = tones.filter((pc) => !inputSet.has(pc));
437
- const fifthPc = pitchClass(rootPc + 7);
438
- if (missingPcs.length > 1 || missingPcs.length === 1 && missingPcs[0] !== fifthPc) {
439
- continue;
440
- }
441
- const extraPcs = input.filter((pc) => !toneSet.has(pc));
442
- const bassIndex = bassPc === void 0 ? -1 : chord.intervals.findIndex((iv) => pitchClass(rootPc + iv) === bassPc);
443
- const inversion = bassPc === void 0 ? null : bassIndex > 0 ? bassIndex : 0;
444
- const match = {
445
- rootPc,
446
- quality,
447
- missingPcs,
448
- extraPcs,
449
- exact: extraPcs.length === 0 && missingPcs.length === 0,
450
- inversion
451
- };
452
- if (bassPc !== void 0 && bassPc !== rootPc) {
453
- match.bassPc = bassPc;
454
- }
455
- matches.push(match);
456
- }
457
- }
458
- matches.sort((a, b) => {
459
- if (a.extraPcs.length !== b.extraPcs.length) {
460
- return a.extraPcs.length - b.extraPcs.length;
461
- }
462
- if (a.missingPcs.length !== b.missingPcs.length) {
463
- return a.missingPcs.length - b.missingPcs.length;
464
- }
465
- if (a.inversion === 0 !== (b.inversion === 0)) {
466
- return a.inversion === 0 ? -1 : 1;
467
- }
468
- const aSize = chordPitchClasses(makeChord(a.rootPc, a.quality)).length;
469
- const bSize = chordPitchClasses(makeChord(b.rootPc, b.quality)).length;
470
- return bSize - aSize;
471
- });
472
- return matches;
473
- }
474
- function detectChordBest(pitches, opts = {}) {
475
- const best = detectChord(pitches, opts)[0];
476
- if (!best) {
477
- return null;
478
- }
479
- return makeChord(best.rootPc, best.quality, best.bassPc);
480
- }
481
- var MINOR_MASK_VARIANTS = [NATURAL_MINOR_MASK, HARMONIC_MINOR_MASK, MELODIC_MINOR_MASK];
482
- function detectKey(pitches) {
483
- assertPitches(pitches);
484
- const input = uniquePitchClasses(pitches);
485
- if (input.length === 0) {
486
- return [];
487
- }
488
- const counts = /* @__PURE__ */ new Map();
489
- for (const pc of pitches.map(pitchClass)) {
490
- counts.set(pc, (counts.get(pc) ?? 0) + 1);
491
- }
492
- const total = pitches.length;
493
- const results = [];
494
- for (let tonic = 0; tonic < 12; tonic += 1) {
495
- for (const mode of ["major", "minor"]) {
496
- const key = mode === "major" ? majorKey(tonic) : minorKey(tonic);
497
- const masks = mode === "major" ? [MAJOR_MASK] : MINOR_MASK_VARIANTS;
498
- let inScale = 0;
499
- let weighted = Number.NEGATIVE_INFINITY;
500
- for (const mask of masks) {
501
- let variantInScale = 0;
502
- let variantWeighted = 0;
503
- for (const pc of input) {
504
- const offset = (pc - tonic + 12) % 12;
505
- if (mask >> offset & 1) {
506
- variantInScale += 1;
507
- }
508
- }
509
- for (const [pc, count] of counts) {
510
- const offset = (pc - tonic + 12) % 12;
511
- if (mask >> offset & 1) {
512
- variantWeighted += count;
513
- }
514
- }
515
- if (variantWeighted > weighted) {
516
- weighted = variantWeighted;
517
- inScale = variantInScale;
518
- }
519
- }
520
- weighted += (counts.get(tonic) ?? 0) * 0.5;
521
- results.push({
522
- key,
523
- mode,
524
- fit: inScale / input.length,
525
- score: weighted / total
526
- });
527
- }
528
- }
529
- results.sort((a, b) => b.score - a.score);
530
- return results.map(({ key, mode, fit }) => ({ key, mode, fit }));
531
- }
532
-
533
661
  export {
662
+ detectChord,
663
+ detectChordBest,
664
+ detectKey,
665
+ detectKeyFromNotes,
666
+ detectKeyBest,
534
667
  romanToChord,
535
668
  chordToRoman,
536
669
  isMinorKey,
@@ -541,9 +674,6 @@ export {
541
674
  secondaryDominant,
542
675
  isBorrowedChord,
543
676
  borrowedSource,
544
- detectCadence,
545
- detectChord,
546
- detectChordBest,
547
- detectKey
677
+ detectCadence
548
678
  };
549
- //# sourceMappingURL=chunk-CHMXELDS.js.map
679
+ //# sourceMappingURL=chunk-KMHOICIY.js.map