@libraz/libcantus 0.9.2 → 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 (200) hide show
  1. package/README.md +6 -5
  2. package/dist/analyze/index.cjs +41 -1676
  3. package/dist/analyze/index.cjs.map +1 -1
  4. package/dist/analyze/index.d.cts +194 -122
  5. package/dist/analyze/index.d.ts +194 -122
  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-R3S2LUBH.js → chunk-7NKDPF74.js} +660 -305
  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-7Y7WF64D.js → chunk-D2FAJBYL.js} +104 -35
  18. package/dist/chunk-D2FAJBYL.js.map +1 -0
  19. package/dist/{chunk-COKTX2G5.js → chunk-EHVEBWM5.js} +1052 -510
  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-GQKN5KDT.js → chunk-GQSGS2N4.js} +43 -29
  26. package/dist/chunk-GQSGS2N4.js.map +1 -0
  27. package/dist/chunk-HKGYCQ34.js +53 -0
  28. package/dist/chunk-HKGYCQ34.js.map +1 -0
  29. package/dist/chunk-I24MV65U.js +106 -0
  30. package/dist/chunk-I24MV65U.js.map +1 -0
  31. package/dist/{chunk-LN7ZZMD2.js → chunk-KMHOICIY.js} +322 -173
  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-N6IHQUOX.js +1 -0
  36. package/dist/chunk-N6IHQUOX.js.map +1 -0
  37. package/dist/chunk-NKIBQ6PT.js +30 -0
  38. package/dist/chunk-NKIBQ6PT.js.map +1 -0
  39. package/dist/chunk-NP4NA2FB.cjs +106 -0
  40. package/dist/chunk-NP4NA2FB.cjs.map +1 -0
  41. package/dist/chunk-NVE34YGZ.cjs +1449 -0
  42. package/dist/chunk-NVE34YGZ.cjs.map +1 -0
  43. package/dist/{chunk-A4532ZFR.js → chunk-NYYWMV2C.js} +115 -47
  44. package/dist/chunk-NYYWMV2C.js.map +1 -0
  45. package/dist/{chunk-5HTNMQW4.js → chunk-Q2XUP227.js} +310 -118
  46. package/dist/chunk-Q2XUP227.js.map +1 -0
  47. package/dist/chunk-Q6OP4U43.cjs +327 -0
  48. package/dist/chunk-Q6OP4U43.cjs.map +1 -0
  49. package/dist/chunk-RC6GBMCD.cjs +845 -0
  50. package/dist/chunk-RC6GBMCD.cjs.map +1 -0
  51. package/dist/{chunk-XTC3X6MS.js → chunk-RK35RIN7.js} +453 -152
  52. package/dist/chunk-RK35RIN7.js.map +1 -0
  53. package/dist/chunk-RUMU3UA6.cjs +53 -0
  54. package/dist/chunk-RUMU3UA6.cjs.map +1 -0
  55. package/dist/chunk-T64C7ZD7.cjs +1 -0
  56. package/dist/chunk-T64C7ZD7.cjs.map +1 -0
  57. package/dist/chunk-XEBG3G53.js +498 -0
  58. package/dist/chunk-XEBG3G53.js.map +1 -0
  59. package/dist/chunk-YLEMGEFH.js +246 -0
  60. package/dist/chunk-YLEMGEFH.js.map +1 -0
  61. package/dist/chunk-ZEJR4TKL.cjs +173 -0
  62. package/dist/chunk-ZEJR4TKL.cjs.map +1 -0
  63. package/dist/core/index.cjs +130 -431
  64. package/dist/core/index.cjs.map +1 -1
  65. package/dist/core/index.d.cts +331 -17
  66. package/dist/core/index.d.ts +331 -17
  67. package/dist/core/index.js +71 -13
  68. package/dist/{function-2bJW1Zp_.d.ts → function-BtiZb9i9.d.ts} +28 -7
  69. package/dist/{function-r2sh-jOO.d.cts → function-DNugAOrt.d.cts} +28 -7
  70. package/dist/generate/index.cjs +30 -3751
  71. package/dist/generate/index.cjs.map +1 -1
  72. package/dist/generate/index.d.cts +373 -59
  73. package/dist/generate/index.d.ts +373 -59
  74. package/dist/generate/index.js +23 -9
  75. package/dist/{index-DZ1M21do.d.cts → index-6VR8-kit.d.ts} +20 -4
  76. package/dist/index-70p168VM.d.ts +192 -0
  77. package/dist/{index-D_ZTUNKp.d.cts → index-Be74mcon.d.cts} +18 -17
  78. package/dist/index-BtXT6UQ0.d.cts +192 -0
  79. package/dist/{index-BMPxwjwV.d.ts → index-C5UBbQ4G.d.ts} +18 -17
  80. package/dist/{index-BOBXBNwP.d.cts → index-C83mZI1P.d.ts} +93 -8
  81. package/dist/{index-BdeQ1r_p.d.cts → index-D6b6na69.d.cts} +14 -11
  82. package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
  83. package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
  84. package/dist/index-OG65ocOr.d.cts +272 -0
  85. package/dist/index-OG65ocOr.d.ts +272 -0
  86. package/dist/{index-BreoJRw0.d.ts → index-tJhkmQgE.d.cts} +20 -4
  87. package/dist/{index-CP3wAfEH.d.ts → index-tSQh-xwY.d.ts} +14 -11
  88. package/dist/{index-CM5W4rcm.d.ts → index-y_ak9_Hi.d.cts} +93 -8
  89. package/dist/index.cjs +419 -6848
  90. package/dist/index.cjs.map +1 -1
  91. package/dist/index.d.cts +14 -13
  92. package/dist/index.d.ts +14 -13
  93. package/dist/index.js +132 -27
  94. package/dist/model/index.cjs +8 -2423
  95. package/dist/model/index.cjs.map +1 -1
  96. package/dist/model/index.d.cts +433 -74
  97. package/dist/model/index.d.ts +433 -74
  98. package/dist/model/index.js +6 -7
  99. package/dist/styled-DIRkk9Uz.d.ts +506 -0
  100. package/dist/styled-DJbkJQfj.d.cts +506 -0
  101. package/dist/theory/index.cjs +172 -1676
  102. package/dist/theory/index.cjs.map +1 -1
  103. package/dist/theory/index.d.cts +151 -233
  104. package/dist/theory/index.d.ts +151 -233
  105. package/dist/theory/index.js +31 -7
  106. package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
  107. package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
  108. package/package.json +73 -24
  109. package/dist/chunk-5HTNMQW4.js.map +0 -1
  110. package/dist/chunk-7Y7WF64D.js.map +0 -1
  111. package/dist/chunk-A4532ZFR.js.map +0 -1
  112. package/dist/chunk-BHUT2S5G.js +0 -22
  113. package/dist/chunk-BHUT2S5G.js.map +0 -1
  114. package/dist/chunk-CCBLVAPK.js +0 -42
  115. package/dist/chunk-CCBLVAPK.js.map +0 -1
  116. package/dist/chunk-COKTX2G5.js.map +0 -1
  117. package/dist/chunk-DYWFNC4A.js +0 -152
  118. package/dist/chunk-DYWFNC4A.js.map +0 -1
  119. package/dist/chunk-GQKN5KDT.js.map +0 -1
  120. package/dist/chunk-LN7ZZMD2.js.map +0 -1
  121. package/dist/chunk-MSWFZF7J.js +0 -118
  122. package/dist/chunk-MSWFZF7J.js.map +0 -1
  123. package/dist/chunk-R3S2LUBH.js.map +0 -1
  124. package/dist/chunk-SDM3VJJ5.js +0 -41
  125. package/dist/chunk-SDM3VJJ5.js.map +0 -1
  126. package/dist/chunk-XTC3X6MS.js.map +0 -1
  127. package/dist/chunk-ZXW5YIWK.js +0 -55
  128. package/dist/chunk-ZXW5YIWK.js.map +0 -1
  129. package/dist/index-CGMZNCx6.d.cts +0 -140
  130. package/dist/index-CGMZNCx6.d.ts +0 -140
  131. package/dist/styled-BjXtOFNk.d.cts +0 -282
  132. package/dist/styled-D7BAx3S3.d.ts +0 -282
  133. package/src/analyze/arrange/index.ts +0 -21
  134. package/src/analyze/arrange/internal.ts +0 -125
  135. package/src/analyze/arrange/tension.ts +0 -187
  136. package/src/analyze/arrange/tracks.ts +0 -279
  137. package/src/analyze/detect/index.ts +0 -235
  138. package/src/analyze/functional/borrowed.ts +0 -84
  139. package/src/analyze/functional/cadence.ts +0 -66
  140. package/src/analyze/functional/function.ts +0 -184
  141. package/src/analyze/functional/index.ts +0 -23
  142. package/src/analyze/functional/internal.ts +0 -29
  143. package/src/analyze/functional/roman.ts +0 -393
  144. package/src/analyze/index.ts +0 -49
  145. package/src/analyze/timeline/index.ts +0 -415
  146. package/src/analyze/voice/index.ts +0 -330
  147. package/src/core/index.ts +0 -47
  148. package/src/core/interval/index.ts +0 -81
  149. package/src/core/meter/index.ts +0 -282
  150. package/src/core/pitch/index.ts +0 -311
  151. package/src/core/random/index.ts +0 -53
  152. package/src/core/tuning/index.ts +0 -156
  153. package/src/core/types.ts +0 -29
  154. package/src/generate/bass/index.ts +0 -350
  155. package/src/generate/countermelody/index.ts +0 -379
  156. package/src/generate/drums/beat.ts +0 -266
  157. package/src/generate/drums/euclid.ts +0 -58
  158. package/src/generate/drums/fills.ts +0 -356
  159. package/src/generate/drums/ghost.ts +0 -119
  160. package/src/generate/drums/hihat.ts +0 -285
  161. package/src/generate/drums/hit.ts +0 -31
  162. package/src/generate/drums/index.ts +0 -329
  163. package/src/generate/drums/internal.ts +0 -257
  164. package/src/generate/drums/kick.ts +0 -160
  165. package/src/generate/drums/percussion.ts +0 -121
  166. package/src/generate/drums/rng.ts +0 -7
  167. package/src/generate/drums/swing.ts +0 -71
  168. package/src/generate/groove/index.ts +0 -326
  169. package/src/generate/harmonize/index.ts +0 -440
  170. package/src/generate/index.ts +0 -59
  171. package/src/generate/motif/index.ts +0 -398
  172. package/src/generate/progression/index.ts +0 -370
  173. package/src/generate/reharmony/index.ts +0 -330
  174. package/src/generate/rhythm/index.ts +0 -165
  175. package/src/index.ts +0 -247
  176. package/src/model/chord.ts +0 -423
  177. package/src/model/index.ts +0 -15
  178. package/src/model/interval.ts +0 -82
  179. package/src/model/key.ts +0 -266
  180. package/src/model/note.ts +0 -176
  181. package/src/model/progression.ts +0 -161
  182. package/src/model/shared.ts +0 -12
  183. package/src/theory/chord/index.ts +0 -373
  184. package/src/theory/chordscale/index.ts +0 -374
  185. package/src/theory/counterpoint/index.ts +0 -230
  186. package/src/theory/harmony/index.ts +0 -92
  187. package/src/theory/index.ts +0 -107
  188. package/src/theory/safety/index.ts +0 -419
  189. package/src/theory/scale/degrees.ts +0 -123
  190. package/src/theory/scale/index.ts +0 -28
  191. package/src/theory/scale/key.ts +0 -56
  192. package/src/theory/scale/masks.ts +0 -153
  193. package/src/theory/spelling/index.ts +0 -182
  194. package/src/theory/symbol/index.ts +0 -268
  195. package/src/theory/voicing/index.ts +0 -5
  196. package/src/theory/voicing/internal.ts +0 -168
  197. package/src/theory/voicing/leading.ts +0 -112
  198. package/src/theory/voicing/satb.ts +0 -195
  199. package/src/theory/voicing/styled.ts +0 -176
  200. package/tsconfig.json +0 -23
@@ -8,23 +8,202 @@ import {
8
8
  isScaleTone,
9
9
  majorKey,
10
10
  makeChord,
11
- minorKey,
12
11
  scaleTonesInDegreeOrder
13
- } from "./chunk-7Y7WF64D.js";
12
+ } from "./chunk-D2FAJBYL.js";
13
+ import {
14
+ InvalidInputError,
15
+ assertFiniteNumber,
16
+ assertGenerationBudget,
17
+ assertInteger,
18
+ pitchClassOf
19
+ } from "./chunk-XEBG3G53.js";
14
20
 
15
- // src/analyze/functional/internal.ts
16
- function mod12(n) {
17
- 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
+ }
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
+ );
18
172
  }
173
+ function detectKeyBest(pitches, opts = {}) {
174
+ return detectKey(pitches, opts)[0] ?? null;
175
+ }
176
+
177
+ // src/analyze/functional/internal.ts
19
178
  function degreeRootPc(degreeNumber, key) {
20
179
  const tones = scaleTonesInDegreeOrder(key);
21
180
  if (tones.length === 0) {
22
- return mod12(key.rootPc);
181
+ return pitchClassOf(key.rootPc);
182
+ }
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
+ }
23
199
  }
24
- return tones[(degreeNumber - 1) % tones.length] ?? mod12(key.rootPc);
200
+ return out;
201
+ }
202
+ function romanReference(key) {
203
+ return scaleTonesInDegreeOrder(key).length === 7 ? key : majorKey(pitchClassOf(key.rootPc));
25
204
  }
26
205
  function isNeapolitan(chord, key) {
27
- return mod12(chord.rootPc - key.rootPc) === 1 && chord.quality === "maj";
206
+ return pitchClassOf(chord.rootPc - key.rootPc) === 1 && chord.quality === "maj";
28
207
  }
29
208
 
30
209
  // src/analyze/functional/roman.ts
@@ -95,7 +274,17 @@ var ROMAN_STYLE = {
95
274
  "7b13": { lower: false, suffix: "7b13" },
96
275
  "11": { lower: false, suffix: "11" },
97
276
  "13": { lower: false, suffix: "13" },
98
- "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" }
99
288
  };
100
289
  var SUFFIX_QUALITY = new Map(
101
290
  Object.entries(ROMAN_STYLE).map(
@@ -126,28 +315,37 @@ function parseInversion(figures) {
126
315
  function parseSimpleRoman(text, key) {
127
316
  const match = /^([b#]?)([iIvV]+)(.*)$/.exec(text.trim());
128
317
  if (!match) {
129
- throw new Error(`Invalid Roman numeral: ${text}`);
318
+ throw new InvalidInputError(`Invalid Roman numeral: ${text}`);
130
319
  }
131
320
  const accidental = match[1] === "b" ? -1 : match[1] === "#" ? 1 : 0;
132
321
  const glyph = (match[2] ?? "").toUpperCase();
133
322
  const degreeNumber = ROMAN_TO_DEGREE[glyph];
134
323
  if (degreeNumber === void 0) {
135
- throw new Error(`Invalid Roman numeral: ${text}`);
324
+ throw new InvalidInputError(`Invalid Roman numeral: ${text}`);
136
325
  }
137
326
  const isUpper = (match[2] ?? "")[0] === (match[2] ?? "")[0]?.toUpperCase();
138
327
  const suffix = match[3] ?? "";
139
- 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));
140
331
  const canonical = SUFFIX_QUALITY.get(`${isUpper ? "u" : "l"}:${suffix}`);
141
332
  if (canonical !== void 0) {
142
333
  return { rootPc, quality: canonical, inversion: 0 };
143
334
  }
144
- const isDim = /o|°|dim/.test(suffix);
145
- const isAug = /\+|aug/.test(suffix);
146
- const isHalfDim = /ø/.test(suffix);
147
- const explicitMaj7 = /maj7|M7/.test(suffix);
148
- const figures = suffix.replace(/maj7|M7/g, "").replace(/[^0-9]/g, "");
335
+ const parsedSuffix = /^(o|°|dim|ø|\+|aug)?(maj7|M7)?(65|64|43|42|7|6|2)?$/.exec(suffix);
336
+ if (!parsedSuffix) {
337
+ throw new InvalidInputError(`Unsupported suffix "${suffix}" in Roman numeral: ${text}`);
338
+ }
339
+ const qualityMarker = parsedSuffix[1] ?? "";
340
+ const isDim = qualityMarker === "o" || qualityMarker === "\xB0" || qualityMarker === "dim";
341
+ const isAug = qualityMarker === "+" || qualityMarker === "aug";
342
+ const isHalfDim = qualityMarker === "\xF8";
343
+ const explicitMaj7 = parsedSuffix[2] !== void 0;
344
+ const figures = parsedSuffix[3] ?? "";
149
345
  if (figures !== "" && !INVERSION_FIGURES.has(figures)) {
150
- 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
+ );
151
349
  }
152
350
  const { inversion, seventh } = parseInversion(figures);
153
351
  const hasSeventh = explicitMaj7 || seventh || isHalfDim;
@@ -176,7 +374,7 @@ function parseSimpleRoman(text, key) {
176
374
  function chordFromParsed(parsed) {
177
375
  const chord = makeChord(parsed.rootPc, parsed.quality);
178
376
  if (parsed.inversion > 0 && parsed.inversion < chord.intervals.length) {
179
- chord.bassPc = mod12(parsed.rootPc + (chord.intervals[parsed.inversion] ?? 0));
377
+ chord.bassPc = pitchClassOf(parsed.rootPc + (chord.intervals[parsed.inversion] ?? 0));
180
378
  }
181
379
  return chord;
182
380
  }
@@ -196,26 +394,32 @@ function romanStyle(quality) {
196
394
  return ROMAN_STYLE[quality];
197
395
  }
198
396
  function romanSpelling(rootPc, key) {
199
- const tones = scaleTonesInDegreeOrder(key);
200
- const diatonic = tones.indexOf(mod12(rootPc));
397
+ const tones = scaleTonesInDegreeOrder(romanReference(key));
398
+ const diatonic = tones.indexOf(pitchClassOf(rootPc));
201
399
  if (diatonic >= 0) {
202
400
  return { degreeNumber: diatonic + 1, accidental: "" };
203
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
+ }
204
408
  for (let i = 0; i < tones.length; i += 1) {
205
409
  const degreeNumber2 = i + 1;
206
410
  if (degreeNumber2 === 1 || degreeNumber2 === 5) {
207
411
  continue;
208
412
  }
209
- if (mod12((tones[i] ?? 0) - 1) === mod12(rootPc)) {
413
+ if (pitchClassOf((tones[i] ?? 0) - 1) === pitchClassOf(rootPc)) {
210
414
  return { degreeNumber: degreeNumber2, accidental: "b" };
211
415
  }
212
416
  }
213
417
  for (let i = 0; i < tones.length; i += 1) {
214
- if (mod12((tones[i] ?? 0) + 1) === mod12(rootPc)) {
418
+ if (pitchClassOf((tones[i] ?? 0) + 1) === pitchClassOf(rootPc)) {
215
419
  return { degreeNumber: i + 1, accidental: "#" };
216
420
  }
217
421
  }
218
- const [degreeNumber, accidental] = OFFSET_SPELLING[mod12(rootPc - key.rootPc)] ?? [1, ""];
422
+ const [degreeNumber, accidental] = OFFSET_SPELLING[pitchClassOf(rootPc - key.rootPc)] ?? [1, ""];
219
423
  return { degreeNumber, accidental };
220
424
  }
221
425
  function baseMarker(quality) {
@@ -249,14 +453,65 @@ var SEVENTH_FIGURE_QUALITIES = /* @__PURE__ */ new Set([
249
453
  "aug7",
250
454
  "augMaj7"
251
455
  ]);
252
- 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
+ }
253
509
  const { degreeNumber, accidental } = romanSpelling(chord.rootPc, key);
254
510
  const { lower, suffix } = romanStyle(chord.quality);
255
- const numeral = ROMAN[degreeNumber - 1] ?? "I";
256
- const cased = lower ? numeral.toLowerCase() : numeral;
511
+ const cased = numeralFor(degreeNumber, lower);
257
512
  let inversion = 0;
258
513
  if (chord.bassPc !== void 0) {
259
- 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);
260
515
  if (idx > 0) {
261
516
  inversion = idx;
262
517
  }
@@ -272,7 +527,7 @@ function chordToRoman(chord, key) {
272
527
 
273
528
  // src/analyze/functional/borrowed.ts
274
529
  function isHarmonicMinorAlteration(chord, key) {
275
- 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 });
276
531
  }
277
532
  function isBorrowedChord(chord, key) {
278
533
  if (isDiatonic(chord, key)) {
@@ -291,7 +546,7 @@ function borrowedSource(chord, key) {
291
546
  return "neapolitan";
292
547
  }
293
548
  if (isBorrowedChord(chord, key)) {
294
- return isMinorKey(key) ? "parallel-major" : "parallel-minor";
549
+ return isMinorKey(key) ? "parallelMajor" : "parallelMinor";
295
550
  }
296
551
  return null;
297
552
  }
@@ -329,14 +584,35 @@ function isMinorKey(key) {
329
584
  return Boolean(hasMinorThird) && !hasMajorThird2;
330
585
  }
331
586
  function functionOf(chord, key) {
332
- 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);
333
609
  }
334
610
  function isDiatonic(chord, key) {
335
611
  return chordPitchClasses(chord).every((pc) => isScaleTone(pc, key));
336
612
  }
337
613
  function parallelKey(key) {
338
614
  return {
339
- rootPc: mod12(key.rootPc),
615
+ rootPc: pitchClassOf(key.rootPc),
340
616
  modeMask12: isMinorKey(key) ? MAJOR_MASK : NATURAL_MINOR_MASK
341
617
  };
342
618
  }
@@ -344,40 +620,28 @@ function isDiminishedQuality(quality) {
344
620
  return quality === "dim" || quality === "dim7" || quality === "m7b5";
345
621
  }
346
622
  function hasMajorThird(chord) {
347
- return chord.intervals.some((interval) => mod12(interval) === 4);
348
- }
349
- function qualityAwareFunction(chord, key) {
350
- const offset = mod12(chord.rootPc - key.rootPc);
351
- if (isDiminishedQuality(chord.quality) && (offset === 11 || offset === 6)) {
352
- return "dominant";
353
- }
354
- if (isNeapolitan(chord, key)) {
355
- return "subdominant";
356
- }
357
- if (!isMinorKey(key) && hasMajorThird(chord) && (offset === 8 || offset === 10)) {
358
- return "subdominant";
359
- }
360
- return functionOf(chord, key);
623
+ return chord.intervals.some((interval) => pitchClassOf(interval) === 4);
361
624
  }
362
625
  function analyzeChord(chord, key) {
363
626
  const source = borrowedSource(chord, key);
364
627
  return {
365
- function: qualityAwareFunction(chord, key),
628
+ function: functionOf(chord, key),
366
629
  borrowed: source !== null,
367
630
  source,
368
631
  roman: chordToRoman(chord, key)
369
632
  };
370
633
  }
371
634
  function secondaryDominant(targetDegree, key) {
635
+ assertInteger(targetDegree, "targetDegree", 0, 6);
372
636
  const targetRoot = degreeRootPc(targetDegree + 1, key);
373
- return makeChord(mod12(targetRoot + 7), "dom7");
637
+ return makeChord(pitchClassOf(targetRoot + 7), "dom7");
374
638
  }
375
639
 
376
640
  // src/analyze/functional/cadence.ts
377
641
  function detectCadence(from, to, key) {
378
- const tonic = mod12(key.rootPc);
379
- const fromOffset = mod12(from.rootPc - tonic);
380
- 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);
381
645
  const deceptiveTargets = isMinorKey(key) ? [8] : [8, 9];
382
646
  if (fromOffset === 7 && toOffset === 0) {
383
647
  return "authentic";
@@ -394,124 +658,12 @@ function detectCadence(from, to, key) {
394
658
  return null;
395
659
  }
396
660
 
397
- // src/analyze/detect/index.ts
398
- function pitchClass(value) {
399
- return (Math.trunc(value) % 12 + 12) % 12;
400
- }
401
- function uniquePitchClasses(pitches) {
402
- return [...new Set(pitches.map(pitchClass))].sort((a, b) => a - b);
403
- }
404
- function detectChord(pitches) {
405
- const input = uniquePitchClasses(pitches);
406
- if (input.length === 0) {
407
- return [];
408
- }
409
- const bassPc = pitchClass(Math.min(...pitches));
410
- const inputSet = new Set(input);
411
- const matches = [];
412
- const qualities = chordQualities();
413
- for (const rootPc of input) {
414
- for (const quality of qualities) {
415
- const chord = makeChord(rootPc, quality);
416
- const tones = chordPitchClasses(chord);
417
- const toneSet = new Set(tones);
418
- const missingPcs = tones.filter((pc) => !inputSet.has(pc));
419
- const fifthPc = pitchClass(rootPc + 7);
420
- if (missingPcs.length > 1 || missingPcs.length === 1 && missingPcs[0] !== fifthPc) {
421
- continue;
422
- }
423
- const extraPcs = input.filter((pc) => !toneSet.has(pc));
424
- const bassIndex = chord.intervals.findIndex((iv) => pitchClass(rootPc + iv) === bassPc);
425
- const inversion = bassIndex > 0 ? bassIndex : 0;
426
- const match = {
427
- rootPc,
428
- quality,
429
- missingPcs,
430
- extraPcs,
431
- exact: extraPcs.length === 0 && missingPcs.length === 0,
432
- inversion
433
- };
434
- if (inversion > 0) {
435
- match.bassPc = bassPc;
436
- }
437
- matches.push(match);
438
- }
439
- }
440
- matches.sort((a, b) => {
441
- if (a.extraPcs.length !== b.extraPcs.length) {
442
- return a.extraPcs.length - b.extraPcs.length;
443
- }
444
- if (a.missingPcs.length !== b.missingPcs.length) {
445
- return a.missingPcs.length - b.missingPcs.length;
446
- }
447
- if (a.inversion === 0 !== (b.inversion === 0)) {
448
- return a.inversion === 0 ? -1 : 1;
449
- }
450
- const aSize = chordPitchClasses(makeChord(a.rootPc, a.quality)).length;
451
- const bSize = chordPitchClasses(makeChord(b.rootPc, b.quality)).length;
452
- return bSize - aSize;
453
- });
454
- return matches;
455
- }
456
- function detectChordBest(pitches) {
457
- const best = detectChord(pitches)[0];
458
- if (!best) {
459
- return null;
460
- }
461
- return makeChord(best.rootPc, best.quality, best.bassPc);
462
- }
463
- var MINOR_MASK_VARIANTS = [NATURAL_MINOR_MASK, HARMONIC_MINOR_MASK, MELODIC_MINOR_MASK];
464
- function detectKey(pitches) {
465
- const input = uniquePitchClasses(pitches);
466
- if (input.length === 0) {
467
- return [];
468
- }
469
- const counts = /* @__PURE__ */ new Map();
470
- for (const pc of pitches.map(pitchClass)) {
471
- counts.set(pc, (counts.get(pc) ?? 0) + 1);
472
- }
473
- const total = pitches.length;
474
- const results = [];
475
- for (let tonic = 0; tonic < 12; tonic += 1) {
476
- for (const mode of ["major", "minor"]) {
477
- const key = mode === "major" ? majorKey(tonic) : minorKey(tonic);
478
- const masks = mode === "major" ? [MAJOR_MASK] : MINOR_MASK_VARIANTS;
479
- let inScale = 0;
480
- let weighted = Number.NEGATIVE_INFINITY;
481
- for (const mask of masks) {
482
- let variantInScale = 0;
483
- let variantWeighted = 0;
484
- for (const pc of input) {
485
- const offset = (pc - tonic + 12) % 12;
486
- if (mask >> offset & 1) {
487
- variantInScale += 1;
488
- }
489
- }
490
- for (const [pc, count] of counts) {
491
- const offset = (pc - tonic + 12) % 12;
492
- if (mask >> offset & 1) {
493
- variantWeighted += count;
494
- }
495
- }
496
- if (variantWeighted > weighted) {
497
- weighted = variantWeighted;
498
- inScale = variantInScale;
499
- }
500
- }
501
- weighted += (counts.get(tonic) ?? 0) * 0.5;
502
- results.push({
503
- key,
504
- mode,
505
- fit: inScale / input.length,
506
- score: weighted / total
507
- });
508
- }
509
- }
510
- results.sort((a, b) => b.score - a.score);
511
- return results.map(({ key, mode, fit }) => ({ key, mode, fit }));
512
- }
513
-
514
661
  export {
662
+ detectChord,
663
+ detectChordBest,
664
+ detectKey,
665
+ detectKeyFromNotes,
666
+ detectKeyBest,
515
667
  romanToChord,
516
668
  chordToRoman,
517
669
  isMinorKey,
@@ -522,9 +674,6 @@ export {
522
674
  secondaryDominant,
523
675
  isBorrowedChord,
524
676
  borrowedSource,
525
- detectCadence,
526
- detectChord,
527
- detectChordBest,
528
- detectKey
677
+ detectCadence
529
678
  };
530
- //# sourceMappingURL=chunk-LN7ZZMD2.js.map
679
+ //# sourceMappingURL=chunk-KMHOICIY.js.map