@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
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  negativeHarmonyMirror
3
- } from "./chunk-GQKN5KDT.js";
3
+ } from "./chunk-GQSGS2N4.js";
4
4
  import {
5
5
  analyzeChord,
6
6
  borrowedSource,
@@ -8,11 +8,13 @@ import {
8
8
  detectCadence,
9
9
  detectChord,
10
10
  detectChordBest,
11
+ detectKey,
12
+ detectKeyBest,
11
13
  functionOf,
12
14
  isBorrowedChord,
13
15
  isMinorKey,
14
16
  romanToChord
15
- } from "./chunk-LN7ZZMD2.js";
17
+ } from "./chunk-KMHOICIY.js";
16
18
  import {
17
19
  availableTensions,
18
20
  avoidNotes,
@@ -20,19 +22,13 @@ import {
20
22
  formatChordSymbol,
21
23
  parseChordSymbol,
22
24
  spellChord,
25
+ spellChordFromRoot,
26
+ spellPitchClass,
23
27
  spellScale,
24
28
  voiceChord,
25
29
  voiceChordStyled,
26
30
  voiceProgression
27
- } from "./chunk-XTC3X6MS.js";
28
- import {
29
- formatNote,
30
- midiToNote,
31
- noteToMidi,
32
- noteToPitchClass,
33
- parseNote,
34
- spelledInterval
35
- } from "./chunk-DYWFNC4A.js";
31
+ } from "./chunk-RK35RIN7.js";
36
32
  import {
37
33
  chordFromDegree,
38
34
  chordPitchClasses,
@@ -43,20 +39,189 @@ import {
43
39
  makeChord,
44
40
  minorKey,
45
41
  scaleByName,
46
- scaleTonesInDegreeOrder
47
- } from "./chunk-7Y7WF64D.js";
42
+ scaleTonesInDegreeOrder,
43
+ transposeChord
44
+ } from "./chunk-D2FAJBYL.js";
45
+ import {
46
+ InvalidInputError,
47
+ assertFiniteNumber,
48
+ formatNote,
49
+ intervalSemitones,
50
+ isConsonantInterval,
51
+ midiToNote,
52
+ noteToMidi,
53
+ noteToPitchClass,
54
+ parseInterval,
55
+ parseNote,
56
+ pitchClassOf,
57
+ spelledInterval,
58
+ transposeByInterval,
59
+ transposeNote
60
+ } from "./chunk-XEBG3G53.js";
48
61
 
49
- // src/model/shared.ts
50
- function mod12(n) {
51
- return (n % 12 + 12) % 12;
52
- }
53
- function spellPitchClassBare(pc, spelling) {
54
- const spelled = midiToNote(60 + mod12(pc), spelling);
55
- return { letter: spelled.letter, alter: spelled.alter };
62
+ // src/model/interval.ts
63
+ var INVERSION_SUM = 9;
64
+ function invertQuality(quality) {
65
+ if (quality === "P") {
66
+ return "P";
67
+ }
68
+ if (quality === "M") {
69
+ return "m";
70
+ }
71
+ if (quality === "m") {
72
+ return "M";
73
+ }
74
+ return quality.startsWith("A") ? `d${"d".repeat(quality.length - 1)}` : `A${"A".repeat(quality.length - 1)}`;
56
75
  }
76
+ var Interval = class _Interval {
77
+ #number;
78
+ #quality;
79
+ #semitones;
80
+ constructor(numberValue, quality, semitones) {
81
+ this.#number = numberValue;
82
+ this.#quality = quality;
83
+ this.#semitones = semitones;
84
+ }
85
+ /**
86
+ * The spelled interval between two notes.
87
+ *
88
+ * @param a The first note.
89
+ * @param b The second note.
90
+ * @returns The interval from `a` to `b`.
91
+ */
92
+ static between(a, b) {
93
+ return a.intervalTo(b);
94
+ }
95
+ /**
96
+ * Build an interval from explicit components.
97
+ *
98
+ * @param numberValue Diatonic size: 1 = unison, 2 = second, ... 8 = octave.
99
+ * @param quality Quality label: `'P'`, `'M'`, `'m'`, or repeated `'A'`/`'d'`.
100
+ * @param semitones Signed semitone span; its magnitude must be the span the
101
+ * number and quality describe.
102
+ * @returns The interval.
103
+ * @throws If the three components do not describe the same interval — a
104
+ * `P5` spanning 8 semitones is not a value any other method can produce.
105
+ */
106
+ static of(numberValue, quality, semitones) {
107
+ const expected = intervalSemitones(numberValue, quality);
108
+ if (Math.abs(semitones) !== expected) {
109
+ throw new InvalidInputError(
110
+ `${quality}${numberValue} spans ${expected} semitones; received ${semitones}`
111
+ );
112
+ }
113
+ return new _Interval(numberValue, quality, semitones);
114
+ }
115
+ /**
116
+ * Wrap a plain spelled interval, as returned by the pitch module.
117
+ *
118
+ * @param data The plain interval.
119
+ * @returns The wrapped interval.
120
+ */
121
+ static fromData(data) {
122
+ return new _Interval(data.number, data.quality, data.semitones);
123
+ }
124
+ /**
125
+ * Rebuild an interval from its {@link Interval.toJSON} output.
126
+ *
127
+ * @param data The serialized interval.
128
+ * @returns The wrapped interval.
129
+ */
130
+ static fromJSON(data) {
131
+ return _Interval.fromData(data);
132
+ }
133
+ /**
134
+ * Parse an interval name such as `'P5'`, `'m3'`, or `'AA4'`.
135
+ *
136
+ * @param name The interval name.
137
+ * @returns The ascending interval of that name.
138
+ * @throws If the name is not a quality label followed by a diatonic number.
139
+ */
140
+ static parse(name) {
141
+ return _Interval.fromData(parseInterval(name));
142
+ }
143
+ /** Diatonic size: 1 = unison, 2 = second, ... 8 = octave, and beyond. */
144
+ get number() {
145
+ return this.#number;
146
+ }
147
+ /** Quality label: `'P'`, `'M'`, `'m'`, or repeated `'A'`/`'d'`. */
148
+ get quality() {
149
+ return this.#quality;
150
+ }
151
+ /** Signed semitone span. */
152
+ get semitones() {
153
+ return this.#semitones;
154
+ }
155
+ /** A readable label composed of quality and number, e.g. `'P5'` or `'M3'`. */
156
+ get name() {
157
+ return `${this.#quality}${this.#number}`;
158
+ }
159
+ /**
160
+ * The interval's inversion: the complement that completes the octave.
161
+ *
162
+ * A compound interval is reduced to its simple form first, and the result is
163
+ * always ascending — an inversion answers "what is left of the octave",
164
+ * which has no direction of its own.
165
+ *
166
+ * @returns The inverted interval, e.g. `M3` becomes `m6`.
167
+ */
168
+ invert() {
169
+ const simple = (this.#number - 1) % 7 + 1;
170
+ const numberValue = INVERSION_SUM - simple;
171
+ const quality = invertQuality(this.#quality);
172
+ return _Interval.of(numberValue, quality, intervalSemitones(numberValue, quality));
173
+ }
174
+ /**
175
+ * Whether the interval is consonant.
176
+ *
177
+ * @param twoVoice When true, the perfect fourth counts as a dissonance,
178
+ * matching two-voice counterpoint.
179
+ * @returns True if the interval is consonant in that context.
180
+ */
181
+ isConsonant(twoVoice = true) {
182
+ return isConsonantInterval(this.#semitones, twoVoice);
183
+ }
184
+ /**
185
+ * Whether another interval is spelled identically.
186
+ *
187
+ * Enharmonic equivalents are not equal: an augmented second and a minor
188
+ * third span the same distance but are different intervals.
189
+ *
190
+ * @param other The interval to compare with.
191
+ * @returns True when number, quality, and span all match.
192
+ */
193
+ equals(other) {
194
+ return this.#number === other.number && this.#quality === other.quality && this.#semitones === other.semitones;
195
+ }
196
+ /**
197
+ * The plain interval data, for JSON serialization.
198
+ *
199
+ * Private class fields do not serialize, so an explicit `toJSON` keeps
200
+ * `JSON.stringify(interval)` from collapsing to `{}`.
201
+ *
202
+ * @returns The diatonic number, quality, and semitone span.
203
+ */
204
+ toJSON() {
205
+ return { number: this.#number, quality: this.#quality, semitones: this.#semitones };
206
+ }
207
+ /**
208
+ * The interval's name, so a template literal or a log line reads as the
209
+ * interval.
210
+ *
211
+ * @returns The name, e.g. `'M3'`.
212
+ */
213
+ toString() {
214
+ return this.name;
215
+ }
216
+ };
57
217
 
58
218
  // src/model/note.ts
59
219
  function copyNote(data) {
220
+ if (!Number.isInteger(data.letter) || data.letter < 0 || data.letter > 6) {
221
+ throw new InvalidInputError(
222
+ `note letter must be an integer in [0, 6]; received ${data.letter}`
223
+ );
224
+ }
60
225
  const copy = { letter: data.letter, alter: data.alter };
61
226
  if (data.octave !== void 0) {
62
227
  copy.octave = data.octave;
@@ -69,6 +234,7 @@ var Note = class _Note {
69
234
  * Wrap a plain note object.
70
235
  *
71
236
  * @param data The spelled note; it is copied, never retained or mutated.
237
+ * @throws If `letter` is not an integer in 0..6.
72
238
  */
73
239
  constructor(data) {
74
240
  this.#data = copyNote(data);
@@ -98,6 +264,7 @@ var Note = class _Note {
98
264
  *
99
265
  * @param data The plain note.
100
266
  * @returns The wrapped note.
267
+ * @throws If `letter` is not an integer in 0..6.
101
268
  */
102
269
  static fromData(data) {
103
270
  return new _Note(data);
@@ -117,7 +284,7 @@ var Note = class _Note {
117
284
  */
118
285
  get midi() {
119
286
  if (this.#data.octave === void 0) {
120
- throw new Error(`note ${this.name} has no octave, so it has no MIDI number`);
287
+ throw new InvalidInputError(`note ${this.name} has no octave, so it has no MIDI number`);
121
288
  }
122
289
  return noteToMidi(this.#data);
123
290
  }
@@ -138,33 +305,67 @@ var Note = class _Note {
138
305
  return copyNote(this.#data);
139
306
  }
140
307
  /**
141
- * Transpose by a signed number of semitones.
308
+ * Transpose by a signed number of semitones, keeping the spelling.
142
309
  *
143
- * When the note carries an octave the transposition happens in MIDI space and
144
- * the result is spelled with a sharp preference. An octave-less note stays
310
+ * The letter moves by the diatonic distance of the conventional interval for
311
+ * that many semitones, so `Ab4` up a major second is `Bb4` rather than `A#4`
312
+ * and a flat key stays on the flat side. An octave-less note stays
145
313
  * octave-less: only its pitch class is moved. Transposing by zero is the
146
314
  * identity: the original spelling is preserved (no enharmonic respelling).
147
315
  *
148
316
  * @param semitones The signed semitone offset.
317
+ * @param opts `spelling` forces the result onto the sharp or flat side
318
+ * instead of following this note's letter.
149
319
  * @returns The transposed note.
320
+ * @example
321
+ * ```ts
322
+ * import { Note } from '@libraz/libcantus';
323
+ * Note.of('Ab4').transpose(2).name; // 'Bb4'
324
+ * Note.of('Ab4').transpose(2, { spelling: 'sharp' }).name; // 'A#4'
325
+ * ```
150
326
  */
151
- transpose(semitones) {
152
- if (semitones === 0) {
327
+ transpose(semitones, opts) {
328
+ if (semitones === 0 && opts?.spelling === void 0) {
153
329
  return new _Note(this.#data);
154
330
  }
155
- if (this.#data.octave !== void 0) {
156
- return _Note.fromMidi(this.midi + semitones);
157
- }
158
- return new _Note(spellPitchClassBare(this.pitchClass + semitones, "sharp"));
331
+ return new _Note(transposeNote(this.#data, semitones, opts));
159
332
  }
160
333
  /**
161
334
  * The spelled interval from this note to another.
162
335
  *
163
336
  * @param other The second note.
164
- * @returns The diatonic number, quality, and semitone span.
337
+ * @returns The interval, which {@link Interval.toJSON} unwraps to plain data.
165
338
  */
166
339
  intervalTo(other) {
167
- return spelledInterval(this.#data, other.#data);
340
+ return Interval.fromData(spelledInterval(this.#data, other.data));
341
+ }
342
+ /**
343
+ * Transpose by a spelled interval, keeping the spelling the interval names.
344
+ *
345
+ * Unlike {@link Note.transpose}, which picks a letter from the semitone
346
+ * count, the interval's diatonic number decides the letter: C up an
347
+ * augmented second is D#, not Eb.
348
+ *
349
+ * @param interval The interval to apply; a descending interval moves down.
350
+ * @returns The transposed note.
351
+ * @example
352
+ * ```ts
353
+ * import { Interval, Note } from '@libraz/libcantus';
354
+ * Note.of('C4').transposeBy(Interval.parse('A2')).name; // 'D#4'
355
+ * ```
356
+ */
357
+ transposeBy(interval) {
358
+ return new _Note(transposeByInterval(this.#data, interval.toJSON()));
359
+ }
360
+ /**
361
+ * Rebuild a note from its {@link Note.toJSON} output.
362
+ *
363
+ * @param data The serialized note.
364
+ * @returns The wrapped note.
365
+ * @throws If `letter` is not an integer in 0..6.
366
+ */
367
+ static fromJSON(data) {
368
+ return new _Note(data);
168
369
  }
169
370
  /**
170
371
  * Whether another note has the same letter, alteration, and octave.
@@ -173,7 +374,8 @@ var Note = class _Note {
173
374
  * @returns True if the spellings are identical.
174
375
  */
175
376
  equals(other) {
176
- return this.#data.letter === other.#data.letter && this.#data.alter === other.#data.alter && this.#data.octave === other.#data.octave;
377
+ const b = other.data;
378
+ return this.#data.letter === b.letter && this.#data.alter === b.alter && this.#data.octave === b.octave;
177
379
  }
178
380
  /**
179
381
  * The plain note data, for JSON serialization.
@@ -186,722 +388,1062 @@ var Note = class _Note {
186
388
  toJSON() {
187
389
  return this.data;
188
390
  }
189
- };
190
-
191
- // src/model/progression.ts
192
- var Progression = class _Progression {
193
- #chords;
194
- #key;
195
391
  /**
196
- * Wrap a chord sequence.
392
+ * The note's name, so a template literal or a log line reads as the note.
197
393
  *
198
- * @param chords The chords in order; the array is copied.
199
- * @param key Optional key context for analysis methods.
394
+ * @returns The spelled name, e.g. `'Bb3'`.
200
395
  */
201
- constructor(chords, key) {
202
- this.#chords = [...chords];
203
- this.#key = key;
204
- }
205
- /** A copy of the chord sequence. */
206
- get chords() {
207
- return [...this.#chords];
396
+ toString() {
397
+ return this.name;
208
398
  }
209
- /** The carried key context, if any. */
210
- get key() {
211
- return this.#key;
399
+ };
400
+
401
+ // src/model/shared.ts
402
+ function spellPitchClassBare(pc, spelling) {
403
+ const spelled = midiToNote(60 + pitchClassOf(pc), spelling);
404
+ return { letter: spelled.letter, alter: spelled.alter };
405
+ }
406
+
407
+ // src/model/key.ts
408
+ function accidentalLoad(tonic, scale) {
409
+ return spellScale(tonic, scale).reduce((sum, note) => sum + Math.abs(note.alter), 0);
410
+ }
411
+ function bestTonicForScale(rootPc, scale) {
412
+ const sharp = spellPitchClassBare(rootPc, "sharp");
413
+ const flat = spellPitchClassBare(rootPc, "flat");
414
+ if (sharp.letter === flat.letter && sharp.alter === flat.alter) {
415
+ return new Note(sharp);
212
416
  }
213
- /** The number of chords. */
214
- get length() {
215
- return this.#chords.length;
417
+ return accidentalLoad(flat, scale) <= accidentalLoad(sharp, scale) ? new Note(flat) : new Note(sharp);
418
+ }
419
+ var Key = class _Key {
420
+ #scale;
421
+ #tonic;
422
+ /**
423
+ * Wrap a key/scale and its spelled tonic.
424
+ *
425
+ * @param scale The key/scale; its root is normalized to a pitch class.
426
+ * @param tonic The spelled tonic anchoring letter-name spelling.
427
+ */
428
+ constructor(scale, tonic) {
429
+ const rootPc = pitchClassOf(scale.rootPc);
430
+ if (tonic.pitchClass !== rootPc) {
431
+ throw new InvalidInputError(
432
+ `tonic ${tonic.name} does not match the scale root pitch class ${rootPc}; pass a tonic that spells the scale root, or omit it to have one chosen`
433
+ );
434
+ }
435
+ this.#scale = { rootPc, modeMask12: scale.modeMask12 };
436
+ this.#tonic = tonic;
216
437
  }
217
438
  /**
218
- * A copy of this progression with a chord appended.
439
+ * A major key.
219
440
  *
220
- * @param chord The chord to append.
221
- * @returns The new progression.
441
+ * @param root Tonic as a note name (e.g. `'Eb'`) or a pitch class; a numeric
442
+ * root is spelled with whichever accidental side yields the fewest
443
+ * accidentals across the scale.
444
+ * @returns The major key.
222
445
  */
223
- add(chord) {
224
- return new _Progression([...this.#chords, chord], this.#key);
446
+ static major(root) {
447
+ if (typeof root === "string") {
448
+ const tonic = Note.of(root);
449
+ return new _Key(majorKey(tonic.pitchClass), tonic);
450
+ }
451
+ const scale = majorKey(root);
452
+ return new _Key(scale, bestTonicForScale(root, scale));
225
453
  }
226
454
  /**
227
- * A copy of this progression carrying the given key context.
455
+ * A natural-minor key.
228
456
  *
229
- * @param key The key context to attach.
230
- * @returns The new progression.
457
+ * @param root Tonic as a note name or a pitch class; a numeric root is
458
+ * spelled with whichever accidental side yields the fewest accidentals
459
+ * across the scale.
460
+ * @returns The minor key.
231
461
  */
232
- withKey(key) {
233
- return new _Progression(this.#chords, key);
462
+ static minor(root) {
463
+ if (typeof root === "string") {
464
+ const tonic = Note.of(root);
465
+ return new _Key(minorKey(tonic.pitchClass), tonic);
466
+ }
467
+ const scale = minorKey(root);
468
+ return new _Key(scale, bestTonicForScale(root, scale));
234
469
  }
235
470
  /**
236
- * Voice the progression with smooth voice leading.
471
+ * A key on a named scale (e.g. `'dorian'`, `'harmonicMinor'`).
237
472
  *
238
- * @param opts Voicing options; defaults to four SATB voices.
239
- * @returns One ascending voicing (MIDI pitches) per chord.
240
- * @throws If any chord admits no voicing within the given ranges.
473
+ * @param name The scale name, a key of the scale module's named-scale table.
474
+ * @param root Tonic as a note name or a pitch class; a numeric root is
475
+ * spelled with whichever accidental side yields the fewest accidentals
476
+ * across the scale, exactly as {@link Key.major} does.
477
+ * @returns The key.
478
+ * @throws If the name is not a known scale.
241
479
  */
242
- voice(opts) {
243
- return voiceProgression(
244
- this.#chords.map((chord) => chord.data),
245
- opts
246
- );
480
+ static named(name, root) {
481
+ if (typeof root === "string") {
482
+ const tonic = Note.of(root);
483
+ return new _Key(scaleByName(name, tonic.pitchClass), tonic);
484
+ }
485
+ const scale = scaleByName(name, pitchClassOf(root));
486
+ return new _Key(scale, bestTonicForScale(pitchClassOf(root), scale));
247
487
  }
248
488
  /**
249
- * The Roman numeral of each chord in a key.
489
+ * Wrap an existing `KeyScale`, synthesizing a spelled tonic when none is
490
+ * given.
250
491
  *
251
- * @param key Key to analyze in; falls back to the carried context.
252
- * @returns One numeral per chord.
253
- * @throws If no key is given and none is carried.
492
+ * The synthesized tonic is chosen the same way as for a numeric root
493
+ * elsewhere: whichever accidental side spells the scale with the fewest
494
+ * accidentals. This is what keeps `Key.of(detectKey(...).scale)` from handing
495
+ * every downstream chord a double-sharp spelling.
496
+ *
497
+ * @param scale The key/scale to wrap.
498
+ * @param tonic Optional spelled tonic; must spell the scale's root pitch class.
499
+ * @returns The key.
500
+ * @throws If the given tonic is not the scale's root pitch class.
254
501
  */
255
- roman(key) {
256
- const resolved = this.#resolveKey(key);
257
- return this.#chords.map((chord) => chord.roman(resolved));
502
+ static of(scale, tonic) {
503
+ return new _Key(scale, tonic ?? bestTonicForScale(pitchClassOf(scale.rootPc), scale));
258
504
  }
259
505
  /**
260
- * The harmonic function of each chord in a key.
506
+ * Rebuild a key from its {@link Key.toJSON} output.
261
507
  *
262
- * @param key Key to analyze in; falls back to the carried context.
263
- * @returns One function per chord.
264
- * @throws If no key is given and none is carried.
508
+ * @param data The serialized key and tonic.
509
+ * @returns The key.
265
510
  */
266
- functions(key) {
267
- const resolved = this.#resolveKey(key);
268
- return this.#chords.map((chord) => chord.function(resolved));
511
+ static fromJSON(data) {
512
+ return new _Key(data.scale, new Note(data.tonic));
269
513
  }
270
514
  /**
271
- * Analyze every chord and classify the closing cadence.
515
+ * Identify the keys a set of pitches fits, best interpretation first.
272
516
  *
273
- * The cadence is detected on the final chord pair and is null when the
274
- * progression has fewer than two chords.
517
+ * The counterpart of {@link Chord.detect}.
275
518
  *
276
- * @param key Key to analyze in; falls back to the carried context.
277
- * @returns Per-chord analyses and the closing cadence.
278
- * @throws If no key is given and none is carried.
519
+ * @param pitches MIDI pitches or bare pitch classes.
520
+ * @param opts How to weigh the input; see {@link DetectKeyOptions}.
521
+ * @returns Ranked keys (may be empty).
279
522
  */
280
- analyze(key) {
281
- const resolved = this.#resolveKey(key);
282
- const chords = this.#chords.map((chord) => chord.analyze(resolved));
283
- const from = this.#chords[this.#chords.length - 2];
284
- const to = this.#chords[this.#chords.length - 1];
285
- const cadence = from !== void 0 && to !== void 0 ? detectCadence(from.data, to.data, resolved.scale) : null;
286
- return { chords, cadence };
523
+ static detect(pitches, opts) {
524
+ return detectKey(pitches, opts).map((match) => _Key.of(match.key));
287
525
  }
288
526
  /**
289
- * The plain progression data, for JSON serialization.
527
+ * The single best key interpretation of a pitch set.
290
528
  *
291
- * Private class fields do not serialize, so an explicit `toJSON` keeps
292
- * `JSON.stringify(progression)` from collapsing to `{}`. The chords are
293
- * emitted as plain data and the carried key, when present, as its own data.
529
+ * @param pitches MIDI pitches or bare pitch classes.
530
+ * @param opts How to weigh the input; see {@link DetectKeyOptions}.
531
+ * @returns The top-ranked key, or null when nothing matches.
532
+ * @example
533
+ * ```ts
534
+ * import { Key } from '@libraz/libcantus';
535
+ * Key.detectBest([0, 2, 4, 5, 7, 9, 11])?.toString(); // 'C major'
536
+ * ```
537
+ */
538
+ static detectBest(pitches, opts) {
539
+ const best = detectKeyBest(pitches, opts);
540
+ return best === null ? null : _Key.of(best.key);
541
+ }
542
+ /**
543
+ * Whether another key has the same tonic pitch class and mode.
294
544
  *
295
- * @returns The chord data sequence and the carried key, if any.
545
+ * The spelled tonic is not compared: C# major and Db major are the same key
546
+ * written two ways.
547
+ *
548
+ * @param other The key to compare.
549
+ * @returns True when tonic and mode mask both match.
296
550
  */
297
- toJSON() {
298
- return {
299
- chords: this.#chords.map((chord) => chord.data),
300
- key: this.#key?.toJSON()
301
- };
551
+ equals(other) {
552
+ const scale = other.scale;
553
+ return this.#scale.rootPc === scale.rootPc && this.#scale.modeMask12 === scale.modeMask12;
302
554
  }
303
- /** Resolve the key for an analysis method: explicit first, then carried. */
304
- #resolveKey(key) {
305
- const resolved = key ?? this.#key;
306
- if (resolved === void 0) {
307
- throw new Error("progression has no key context; pass a Key or attach one with withKey()");
308
- }
309
- return resolved;
555
+ /** A copy of the underlying plain `KeyScale`. */
556
+ get scale() {
557
+ return { rootPc: this.#scale.rootPc, modeMask12: this.#scale.modeMask12 };
310
558
  }
311
- };
312
-
313
- // src/model/chord.ts
314
- function copyChord(data) {
315
- const copy = {
316
- rootPc: data.rootPc,
317
- quality: data.quality,
318
- intervals: [...data.intervals]
319
- };
320
- if (data.bassPc !== void 0) {
321
- copy.bassPc = data.bassPc;
559
+ /** The spelled tonic. */
560
+ get tonic() {
561
+ return this.#tonic;
322
562
  }
323
- if (data.rootSpelling !== void 0) {
324
- copy.rootSpelling = { letter: data.rootSpelling.letter, alter: data.rootSpelling.alter };
563
+ /** The tonic pitch class (0..11). */
564
+ get rootPc() {
565
+ return this.#scale.rootPc;
325
566
  }
326
- if (data.bassSpelling !== void 0) {
327
- copy.bassSpelling = { letter: data.bassSpelling.letter, alter: data.bassSpelling.alter };
567
+ /** Whether the scale has a minor third and no major third. */
568
+ get isMinor() {
569
+ return isMinorKey(this.#scale);
328
570
  }
329
- return copy;
330
- }
331
- var Chord = class _Chord {
332
- #data;
333
- #key;
334
571
  /**
335
- * Wrap a plain chord object.
572
+ * The scale's pitch classes in ascending scale-degree order (degree 0 first).
336
573
  *
337
- * @param data The chord; it is copied, never retained or mutated.
338
- * @param key Optional key context for analysis methods.
574
+ * @returns One pitch class per scale degree.
339
575
  */
340
- constructor(data, key) {
341
- this.#data = copyChord(data);
342
- this.#key = key;
576
+ pitchClasses() {
577
+ return scaleTonesInDegreeOrder(this.#scale);
343
578
  }
344
579
  /**
345
- * Build a chord from a root and quality.
580
+ * Transpose the key by a number of semitones.
346
581
  *
347
- * @param root Root as a note name (e.g. `'Eb'`) or a pitch class.
348
- * @param quality The chord quality.
349
- * @param bass Optional slash-chord bass pitch class.
350
- * @returns The chord (without key context).
582
+ * The mode mask is unchanged, so the scale keeps its shape; only the tonic
583
+ * moves. The new tonic is spelled the way that key is conventionally written
584
+ * — D major, not C## major.
585
+ *
586
+ * @param semitones The signed semitone offset.
587
+ * @returns The transposed key.
588
+ * @example
589
+ * ```ts
590
+ * import { Key } from '@libraz/libcantus';
591
+ * Key.major('C').transpose(2).toString(); // 'D major'
592
+ * ```
351
593
  */
352
- static of(root, quality, bass) {
353
- const rootPc = typeof root === "string" ? Note.of(root).pitchClass : root;
354
- return new _Chord(makeChord(rootPc, quality, bass));
594
+ transpose(semitones) {
595
+ assertFiniteNumber(semitones, "semitones");
596
+ const rootPc = pitchClassOf(this.#scale.rootPc + Math.round(semitones));
597
+ return _Key.of({ rootPc, modeMask12: this.#scale.modeMask12 });
355
598
  }
356
599
  /**
357
- * Wrap an existing plain chord object.
600
+ * The spelled scale, one note per degree (e.g. C D E F G A B for C major).
358
601
  *
359
- * @param data The plain chord.
360
- * @returns The wrapped chord (without key context).
602
+ * @returns Spelled octave-less notes in scale-degree order.
361
603
  */
362
- static from(data) {
363
- return new _Chord(data);
604
+ notes() {
605
+ return spellScale(this.#tonic.data, this.#scale).map((note) => new Note(note));
364
606
  }
365
607
  /**
366
- * Parse a lead-sheet chord symbol (e.g. `'Cmaj7'`, `'F#m7b5'`, `'C/G'`).
608
+ * Alias of {@link Key.notes}.
367
609
  *
368
- * @param symbol The chord symbol.
369
- * @returns The chord (without key context).
370
- * @throws If the root or quality is not recognized.
610
+ * @returns Spelled octave-less notes in scale-degree order.
371
611
  */
372
- static parse(symbol) {
373
- return new _Chord(parseChordSymbol(symbol));
612
+ spell() {
613
+ return this.notes();
374
614
  }
375
615
  /**
376
- * Identify the chords matching a set of pitches, best interpretation first.
616
+ * The spelled scale as letter-name strings.
377
617
  *
378
- * @param pitches MIDI pitches or bare pitch classes.
379
- * @returns Ranked chord interpretations (may be empty).
618
+ * @returns One name per scale degree.
380
619
  */
381
- static detect(pitches) {
382
- return detectChord(pitches).map(
383
- (match) => new _Chord(makeChord(match.rootPc, match.quality, match.bassPc))
384
- );
620
+ noteNames() {
621
+ return this.notes().map((note) => note.name);
385
622
  }
386
623
  /**
387
- * The single best chord interpretation of a pitch set.
624
+ * Build a chord on a scale degree, carrying this key as context.
388
625
  *
389
- * @param pitches MIDI pitches or bare pitch classes.
390
- * @returns The top-ranked chord, or null when nothing matches.
626
+ * With an explicit quality the quality's interval template is attached to the
627
+ * degree's diatonic root; without one the scale-correct diatonic triad is
628
+ * stacked (e.g. a diminished triad on the leading tone of a major key).
629
+ *
630
+ * @param degree 0-based scale degree of the chord root.
631
+ * @param quality Optional chord quality.
632
+ * @returns The chord, with this key attached.
633
+ * @throws Without a `quality`, if this key's scale is not heptatonic —
634
+ * stacking thirds needs seven degrees, so the pentatonic, blues,
635
+ * whole-tone, octatonic and chromatic scales have no diatonic triad. Pass
636
+ * an explicit `quality` for those.
391
637
  */
392
- static detectBest(pitches) {
393
- const best = detectChordBest(pitches);
394
- return best === null ? null : new _Chord(best);
395
- }
396
- /** The root pitch class (0..11). */
397
- get rootPc() {
398
- return this.#data.rootPc;
399
- }
400
- /** The chord quality. */
401
- get quality() {
402
- return this.#data.quality;
403
- }
404
- /** A copy of the semitone offsets above the root. */
405
- get intervals() {
406
- return [...this.#data.intervals];
407
- }
408
- /** The slash-chord bass pitch class, or undefined in root position. */
409
- get bassPc() {
410
- return this.#data.bassPc;
411
- }
412
- /** A copy of the underlying plain chord object. */
413
- get data() {
414
- return copyChord(this.#data);
415
- }
416
- /** The carried key context, if any. */
417
- get key() {
418
- return this.#key;
638
+ chord(degree, quality) {
639
+ const data = quality === void 0 ? diatonicTriad(degree, this.#scale) : chordFromDegree(degree, quality, this.#scale);
640
+ return new Chord(data, this);
419
641
  }
420
642
  /**
421
- * A copy of this chord carrying the given key context.
643
+ * The diatonic triad on a scale degree, carrying this key as context.
422
644
  *
423
- * @param key The key context to attach.
424
- * @returns The new chord.
645
+ * @param degree 0-based scale degree of the chord root.
646
+ * @returns The triad, with this key attached.
647
+ * @throws If this key's scale is not heptatonic; stacking thirds needs seven
648
+ * degrees. Use {@link Key.chord} with an explicit quality instead.
425
649
  */
426
- withKey(key) {
427
- return new _Chord(this.#data, key);
650
+ diatonicTriad(degree) {
651
+ return new Chord(diatonicTriad(degree, this.#scale), this);
428
652
  }
429
653
  /**
430
- * The chord's sorted, deduplicated pitch classes.
654
+ * The diatonic seventh chord on a scale degree, carrying this key as context.
431
655
  *
432
- * @returns Pitch classes ascending in [0, 11].
656
+ * @param degree 0-based scale degree of the chord root.
657
+ * @returns The seventh chord, with this key attached.
658
+ * @throws If this key's scale is not heptatonic; stacking thirds needs seven
659
+ * degrees. Use {@link Key.chord} with an explicit quality instead.
433
660
  */
434
- pitchClasses() {
435
- return chordPitchClasses(this.#data);
661
+ diatonicSeventh(degree) {
662
+ return new Chord(diatonicSeventh(degree, this.#scale), this);
436
663
  }
437
664
  /**
438
- * The chord's Roman numeral in a key.
665
+ * Build the chord denoted by a Roman numeral in this key (including applied
666
+ * chords such as `'V7/V'`), carrying this key as context.
439
667
  *
440
- * @param key Key to analyze in; falls back to the carried context.
441
- * @returns The Roman numeral string.
442
- * @throws If no key is given and none is carried.
668
+ * @param text The Roman numeral.
669
+ * @returns The chord, with this key attached.
670
+ * @throws If the numeral is not valid.
443
671
  */
444
- roman(key) {
445
- return chordToRoman(this.#data, this.#resolveKey(key).scale);
672
+ roman(text) {
673
+ return new Chord(romanToChord(text, this.#scale), this);
446
674
  }
447
675
  /**
448
- * The chord's harmonic function (tonic / subdominant / dominant) in a key.
676
+ * Whether a pitch belongs to the scale.
449
677
  *
450
- * @param key Key to analyze in; falls back to the carried context.
451
- * @returns The harmonic function.
452
- * @throws If no key is given and none is carried.
678
+ * @param x A MIDI pitch, bare pitch class, or note.
679
+ * @returns True if the pitch class is a scale tone.
453
680
  */
454
- function(key) {
455
- return functionOf(this.#data, this.#resolveKey(key).scale);
681
+ contains(x) {
682
+ return isScaleTone(typeof x === "number" ? x : x.pitchClass, this.#scale);
456
683
  }
457
684
  /**
458
- * Full functional analysis: function, borrowing, and Roman numeral.
685
+ * The plain key data, for JSON serialization.
459
686
  *
460
- * @param key Key to analyze in; falls back to the carried context.
461
- * @returns The chord analysis.
462
- * @throws If no key is given and none is carried.
687
+ * Private class fields do not serialize, so an explicit `toJSON` keeps
688
+ * `JSON.stringify(key)` from collapsing to `{}`. The result pairs the
689
+ * `KeyScale` with the spelled tonic, enough to reconstruct the key via
690
+ * {@link Key.of}.
691
+ *
692
+ * @returns The key/scale and its spelled tonic.
463
693
  */
464
- analyze(key) {
465
- return analyzeChord(this.#data, this.#resolveKey(key).scale);
694
+ toJSON() {
695
+ return { scale: this.scale, tonic: this.#tonic.data };
466
696
  }
467
697
  /**
468
- * Whether the chord is borrowed from the parallel mode (modal interchange).
698
+ * The key's tonic and mode, so a template literal or a log line reads as the
699
+ * key. Only the major/minor distinction is named, since a mask does not carry
700
+ * a scale name.
469
701
  *
470
- * @param key Key to analyze in; falls back to the carried context.
471
- * @returns True if the chord is borrowed.
472
- * @throws If no key is given and none is carried.
702
+ * @returns The name, e.g. `'C major'` or `'A minor'`.
473
703
  */
474
- isBorrowed(key) {
475
- return isBorrowedChord(this.#data, this.#resolveKey(key).scale);
704
+ toString() {
705
+ return `${this.#tonic.name} ${this.isMinor ? "minor" : "major"}`;
476
706
  }
707
+ };
708
+
709
+ // src/model/progression.ts
710
+ var Progression = class _Progression {
711
+ #chords;
712
+ #key;
477
713
  /**
478
- * The origin of a non-diatonic chord (parallel mode or Neapolitan), or null.
714
+ * Wrap a chord sequence.
479
715
  *
480
- * @param key Key to analyze in; falls back to the carried context.
481
- * @returns The borrowing source, or null.
482
- * @throws If no key is given and none is carried.
716
+ * @param chords The chords in order; the array is copied.
717
+ * @param key Optional key context for analysis methods.
483
718
  */
484
- borrowedSource(key) {
485
- return borrowedSource(this.#data, this.#resolveKey(key).scale);
719
+ constructor(chords, key) {
720
+ this.#chords = Object.freeze([...chords]);
721
+ this.#key = key;
486
722
  }
487
723
  /**
488
- * The chord rendered as a lead-sheet symbol (e.g. `'Cmaj7'`, `'F#m7'`, `'C/G'`).
724
+ * Build a progression from the {@link ChordSpan} records the generators
725
+ * return, keeping their order.
489
726
  *
490
- * @param opts Set `flats: true` to spell the root/bass with flats.
491
- * @returns The chord symbol.
727
+ * The spans' `startBeat`, `degree`, and `secondaryDominant` are analysis
728
+ * annotations that a chord sequence does not carry; only the harmony crosses
729
+ * over. Keep the spans themselves if the timing matters.
730
+ *
731
+ * @param spans The chord spans, in order.
732
+ * @param key Optional key context for the analysis methods.
733
+ * @returns The progression.
734
+ * @example
735
+ * ```ts
736
+ * import { generateProgression, Key, Progression } from '@libraz/libcantus';
737
+ * const key = Key.major('C');
738
+ * const spans = generateProgression({ key: key.scale, style: 'dance', bars: 4 });
739
+ * Progression.fromSpans(spans, key).roman();
740
+ * ```
492
741
  */
493
- symbol(opts) {
494
- return formatChordSymbol(this.#data, opts);
742
+ static fromSpans(spans, key) {
743
+ const chords = spans.map((span) => {
744
+ const chord = Chord.from(makeChord(span.rootPc, span.quality, span.bassPc));
745
+ return key === void 0 ? chord : chord.withKey(key);
746
+ });
747
+ return key === void 0 ? new _Progression(chords) : new _Progression(chords, key);
495
748
  }
496
749
  /**
497
- * Realize the chord as one MIDI pitch per voice, ascending.
750
+ * Rebuild a progression from its {@link Progression.toJSON} output.
498
751
  *
499
- * @param opts Voicing options; defaults to four SATB voices.
500
- * @returns MIDI pitches, ascending, one per voice.
501
- * @throws If no voicing fits the given ranges.
752
+ * @param data The serialized chords and key.
753
+ * @returns The progression.
502
754
  */
503
- voice(opts) {
504
- return voiceChord(this.#data, opts);
755
+ static fromJSON(data) {
756
+ const key = data.key === void 0 ? void 0 : Key.fromJSON(data.key);
757
+ const chords = data.chords.map((chord) => Chord.fromJSON(chord));
758
+ return key === void 0 ? new _Progression(chords) : new _Progression(chords, key);
505
759
  }
506
760
  /**
507
- * Realize the chord as a single styled voicing (`close`, `drop2`, `drop3`,
508
- * `shell`, or `rootless`), optionally constraining the top voice.
761
+ * The chord sequence.
509
762
  *
510
- * @param opts Styled-voicing options; defaults to a close-position voicing.
511
- * @returns MIDI pitches, ascending.
763
+ * The array is the progression's own and is frozen rather than copied, so
764
+ * reading it in a loop stays linear.
512
765
  */
513
- styledVoicing(opts) {
514
- return voiceChordStyled(this.#data, opts);
766
+ get chords() {
767
+ return this.#chords;
768
+ }
769
+ /** The carried key context, if any. */
770
+ get key() {
771
+ return this.#key;
772
+ }
773
+ /** The number of chords. */
774
+ get length() {
775
+ return this.#chords.length;
515
776
  }
516
777
  /**
517
- * The negative-harmony mirror of the chord about the key's tonic–dominant
518
- * axis (major becomes minor and vice versa).
778
+ * The chord at an index.
519
779
  *
520
- * @param key Key providing the reflection axis; falls back to the carried
521
- * context.
522
- * @returns The mirrored chord, keeping any key context.
523
- * @throws If no key is given and none is carried.
780
+ * @param index 0-based position; a negative index counts from the end.
781
+ * @returns The chord, or undefined when the index is out of range.
524
782
  */
525
- negativeHarmony(key) {
526
- const resolved = this.#resolveKey(key);
527
- return new _Chord(negativeHarmonyMirror(this.#data, resolved.scale), key ?? this.#key);
783
+ at(index) {
784
+ return this.#chords.at(index);
785
+ }
786
+ /** Iterate the chords in order, so a progression works with `for...of`. */
787
+ [Symbol.iterator]() {
788
+ return this.#chords[Symbol.iterator]();
528
789
  }
529
790
  /**
530
- * The n-th inversion: a copy whose bass is the chord tone `n` steps above the
531
- * root in the interval template (`invert(1)` puts the third in the bass).
532
- * `n` wraps around the template length; negative values count backwards.
791
+ * Whether another progression holds the same chords in the same order.
533
792
  *
534
- * `invert(0)` (and any `n` that wraps to it) is root position, so it carries
535
- * no slash bass and equals the original chord.
793
+ * The key context is not compared: it is an analysis lens, not part of the
794
+ * harmony. Chord equality follows {@link Chord.equals}.
536
795
  *
537
- * @param n The inversion number.
538
- * @returns The inverted chord, keeping any key context.
539
- * @throws If the chord has no intervals.
796
+ * @param other The progression to compare.
797
+ * @returns True when the chord sequences match.
540
798
  */
541
- invert(n) {
542
- const intervals = this.#data.intervals;
543
- const length = intervals.length;
544
- if (length === 0) {
545
- throw new Error("cannot invert a chord with no intervals");
546
- }
547
- const index = (n % length + length) % length;
548
- const data = copyChord(this.#data);
549
- if (index === 0) {
550
- delete data.bassPc;
551
- } else {
552
- data.bassPc = mod12(this.#data.rootPc + (intervals[index] ?? 0));
799
+ equals(other) {
800
+ if (this.#chords.length !== other.length) {
801
+ return false;
553
802
  }
554
- return new _Chord(data, this.#key);
803
+ return this.#chords.every((chord, index) => {
804
+ const theirs = other.at(index);
805
+ return theirs !== void 0 && chord.equals(theirs);
806
+ });
555
807
  }
556
808
  /**
557
- * The named scales that fit over this chord, best fit first, rooted on the
558
- * chord root.
809
+ * A copy of this progression with a chord appended.
559
810
  *
560
- * @returns The matching scales.
811
+ * @param chord The chord to append.
812
+ * @returns The new progression.
561
813
  */
562
- scales() {
563
- return chordScales(this.#data);
814
+ add(chord) {
815
+ return new _Progression([...this.#chords, chord], this.#key);
564
816
  }
565
817
  /**
566
- * The available tensions (usable non-chord, non-avoid scale tones) of a
567
- * scale over this chord.
818
+ * A copy of this progression carrying the given key context.
568
819
  *
569
- * @param scaleName A named scale, rooted on the chord root.
570
- * @returns Tension pitch classes, ascending in [0, 11].
820
+ * @param key The key context to attach.
821
+ * @returns The new progression.
571
822
  */
572
- tensions(scaleName) {
573
- return availableTensions(this.#data, scaleName);
823
+ withKey(key) {
824
+ return new _Progression(this.#chords, key);
574
825
  }
575
826
  /**
576
- * The avoid notes (scale tones a semitone above a chord tone) of a scale
577
- * over this chord.
827
+ * Voice the progression with smooth voice leading.
578
828
  *
579
- * @param scaleName A named scale, rooted on the chord root.
580
- * @returns Avoid-note pitch classes, ascending in [0, 11].
829
+ * @param opts Voicing options; defaults to four SATB voices.
830
+ * @returns One ascending voicing (MIDI pitches) per chord.
831
+ * @throws If any chord admits no voicing within the given ranges.
581
832
  */
582
- avoidNotes(scaleName) {
583
- return avoidNotes(this.#data, scaleName);
833
+ voice(opts) {
834
+ return voiceProgression(
835
+ this.#chords.map((chord) => chord.data),
836
+ opts
837
+ );
584
838
  }
585
839
  /**
586
- * Spell the chord tones with letter names, root first, in the key's spelling.
840
+ * The Roman numeral of each chord in a key.
587
841
  *
588
- * @param key Key providing the spelled tonic; falls back to the carried
589
- * context.
590
- * @returns Spelled octave-less notes in the chord's own (tertian) order.
842
+ * @param key Key to analyze in; falls back to the carried context.
843
+ * @returns One numeral per chord.
591
844
  * @throws If no key is given and none is carried.
592
845
  */
593
- spell(key) {
846
+ roman(key) {
594
847
  const resolved = this.#resolveKey(key);
595
- return spellChord(this.#data, resolved.tonic.data, resolved.scale).map(
596
- (note) => new Note(note)
597
- );
848
+ return this.#chords.map((chord) => chord.roman(resolved));
598
849
  }
599
850
  /**
600
- * Start a progression with this chord followed by others, carrying this
601
- * chord's key context (if any).
851
+ * The harmonic function of each chord in a key.
602
852
  *
603
- * @param others The chords following this one.
604
- * @returns The progression.
853
+ * @param key Key to analyze in; falls back to the carried context.
854
+ * @returns One function per chord.
855
+ * @throws If no key is given and none is carried.
605
856
  */
606
- progressionTo(...others) {
607
- return new Progression([this, ...others], this.#key);
857
+ functions(key) {
858
+ const resolved = this.#resolveKey(key);
859
+ return this.#chords.map((chord) => chord.function(resolved));
608
860
  }
609
861
  /**
610
- * Whether another chord has the same root, quality, intervals, and bass.
611
- * Key context is not compared.
862
+ * Analyze every chord and classify the closing cadence.
612
863
  *
613
- * @param other The chord to compare.
614
- * @returns True if the chord data is identical.
864
+ * The cadence is detected on the final chord pair and is null when the
865
+ * progression has fewer than two chords.
866
+ *
867
+ * @param key Key to analyze in; falls back to the carried context.
868
+ * @returns Per-chord analyses and the closing cadence.
869
+ * @throws If no key is given and none is carried.
615
870
  */
616
- equals(other) {
617
- const a = this.#data;
618
- const b = other.#data;
619
- return a.rootPc === b.rootPc && a.quality === b.quality && a.bassPc === b.bassPc && a.intervals.length === b.intervals.length && a.intervals.every((interval, i) => interval === b.intervals[i]);
871
+ analyze(key) {
872
+ const resolved = this.#resolveKey(key);
873
+ const chords = this.#chords.map((chord) => chord.analyze(resolved));
874
+ const from = this.#chords[this.#chords.length - 2];
875
+ const to = this.#chords[this.#chords.length - 1];
876
+ const cadence = from !== void 0 && to !== void 0 ? detectCadence(from.data, to.data, resolved.scale) : null;
877
+ return { chords, cadence };
620
878
  }
621
879
  /**
622
- * The plain chord data, for JSON serialization.
880
+ * The plain progression data, for JSON serialization.
623
881
  *
624
- * @returns A copy of the underlying plain chord object.
882
+ * Private class fields do not serialize, so an explicit `toJSON` keeps
883
+ * `JSON.stringify(progression)` from collapsing to `{}`. The chords are
884
+ * emitted as plain data and the carried key, when present, as its own data.
885
+ *
886
+ * @returns The chord data sequence and the carried key, if any.
887
+ */
888
+ /**
889
+ * Transpose every chord by a number of semitones.
890
+ *
891
+ * A carried key moves with the chords, so the progression keeps its degrees
892
+ * and functions in the new key.
893
+ *
894
+ * @param semitones The signed semitone offset.
895
+ * @returns The transposed progression.
896
+ * @example
897
+ * ```ts
898
+ * import { Chord, Progression } from '@libraz/libcantus';
899
+ * new Progression([Chord.parse('C'), Chord.parse('G')]).transpose(2).toString(); // 'D A'
900
+ * ```
901
+ */
902
+ transpose(semitones) {
903
+ const key = this.#key?.transpose(semitones);
904
+ const chords = this.#chords.map((chord) => {
905
+ const moved = chord.transpose(semitones);
906
+ return key !== void 0 && moved.key === void 0 ? moved.withKey(key) : moved;
907
+ });
908
+ return key === void 0 ? new _Progression(chords) : new _Progression(chords, key);
909
+ }
910
+ /**
911
+ * The chord symbols separated by spaces, so a template literal or a log line
912
+ * reads as the progression.
913
+ *
914
+ * @returns The symbols in order, e.g. `'C Am F G'`.
625
915
  */
916
+ toString() {
917
+ return this.#chords.map((chord) => chord.symbol()).join(" ");
918
+ }
626
919
  toJSON() {
627
- return this.data;
920
+ return {
921
+ chords: this.#chords.map((chord) => chord.data),
922
+ key: this.#key?.toJSON()
923
+ };
628
924
  }
629
925
  /** Resolve the key for an analysis method: explicit first, then carried. */
630
926
  #resolveKey(key) {
631
927
  const resolved = key ?? this.#key;
632
928
  if (resolved === void 0) {
633
- throw new Error("chord has no key context; pass a Key or attach one with withKey()");
929
+ throw new InvalidInputError(
930
+ "progression has no key context; pass a Key or attach one with withKey()"
931
+ );
634
932
  }
635
933
  return resolved;
636
934
  }
637
935
  };
638
936
 
639
- // src/model/interval.ts
640
- var Interval = class _Interval {
641
- #number;
642
- #quality;
643
- #semitones;
644
- constructor(numberValue, quality, semitones) {
645
- this.#number = numberValue;
646
- this.#quality = quality;
647
- this.#semitones = semitones;
937
+ // src/model/chord.ts
938
+ function hintMatches(hint, pc) {
939
+ return hint !== void 0 && pc !== void 0 && pitchClassOf(noteToPitchClass(hint)) === pitchClassOf(pc);
940
+ }
941
+ function copyChord(data) {
942
+ const copy = {
943
+ rootPc: data.rootPc,
944
+ quality: data.quality,
945
+ intervals: [...data.intervals]
946
+ };
947
+ if (data.bassPc !== void 0) {
948
+ copy.bassPc = data.bassPc;
949
+ }
950
+ if (hintMatches(data.rootSpelling, data.rootPc) && data.rootSpelling !== void 0) {
951
+ copy.rootSpelling = { letter: data.rootSpelling.letter, alter: data.rootSpelling.alter };
952
+ }
953
+ if (hintMatches(data.bassSpelling, data.bassPc) && data.bassSpelling !== void 0) {
954
+ copy.bassSpelling = { letter: data.bassSpelling.letter, alter: data.bassSpelling.alter };
955
+ }
956
+ return copy;
957
+ }
958
+ function deriveBassSpelling(chord, key) {
959
+ const bassPc = chord.bassPc;
960
+ if (bassPc === void 0) {
961
+ return void 0;
962
+ }
963
+ const root = chord.rootSpelling;
964
+ if (root !== void 0) {
965
+ const index = chord.intervals.findIndex((i) => pitchClassOf(chord.rootPc + i) === bassPc);
966
+ const tone = index >= 0 ? spellChordFromRoot(chord, root)[index] : void 0;
967
+ if (tone !== void 0) {
968
+ return { letter: tone.letter, alter: tone.alter };
969
+ }
970
+ }
971
+ if (key === void 0) {
972
+ return void 0;
648
973
  }
974
+ const spelled = spellPitchClass(bassPc, key.tonic.data, key.scale);
975
+ return { letter: spelled.letter, alter: spelled.alter };
976
+ }
977
+ var Chord = class _Chord {
978
+ /** Exactly what the caller supplied: any spelling here is the caller's own. */
979
+ #given;
980
+ #key;
649
981
  /**
650
- * The spelled interval between two notes.
982
+ * Wrap a plain chord object.
651
983
  *
652
- * @param a The first note.
653
- * @param b The second note.
654
- * @returns The interval from `a` to `b`.
984
+ * @param data The chord; it is copied, never retained or mutated.
985
+ * @param key Optional key context for analysis methods.
655
986
  */
656
- static between(a, b) {
657
- const spelled = a.intervalTo(b);
658
- return new _Interval(spelled.number, spelled.quality, spelled.semitones);
987
+ constructor(data, key) {
988
+ this.#given = copyChord(data);
989
+ this.#key = key;
659
990
  }
660
991
  /**
661
- * Build an interval from explicit components.
992
+ * The chord data with any missing spelling hint filled in from the key.
662
993
  *
663
- * @param numberValue Diatonic size: 1 = unison, 2 = second, ... 8 = octave.
664
- * @param quality Quality label: `'P'`, `'M'`, `'m'`, or repeated `'A'`/`'d'`.
665
- * @param semitones Signed semitone span.
666
- * @returns The interval.
994
+ * Derived spellings are computed on read rather than baked in at construction,
995
+ * so re-attaching a different key re-spells the chord instead of carrying the
996
+ * first key's letters forever. A spelling the caller supplied always wins.
667
997
  */
668
- static of(numberValue, quality, semitones) {
669
- return new _Interval(numberValue, quality, semitones);
670
- }
671
- /** Diatonic size: 1 = unison, 2 = second, ... 8 = octave, and beyond. */
672
- get number() {
673
- return this.#number;
674
- }
675
- /** Quality label: `'P'`, `'M'`, `'m'`, or repeated `'A'`/`'d'`. */
676
- get quality() {
677
- return this.#quality;
678
- }
679
- /** Signed semitone span. */
680
- get semitones() {
681
- return this.#semitones;
682
- }
683
- /** A readable label composed of quality and number, e.g. `'P5'` or `'M3'`. */
684
- get name() {
685
- return `${this.#quality}${this.#number}`;
998
+ get #data() {
999
+ const out = copyChord(this.#given);
1000
+ const key = this.#key;
1001
+ if (out.rootSpelling === void 0 && key !== void 0) {
1002
+ out.rootSpelling = spellPitchClass(out.rootPc, key.tonic.data, key.scale);
1003
+ }
1004
+ if (out.bassSpelling === void 0) {
1005
+ const bass = deriveBassSpelling(out, key);
1006
+ if (bass !== void 0) {
1007
+ out.bassSpelling = bass;
1008
+ }
1009
+ }
1010
+ return out;
686
1011
  }
687
1012
  /**
688
- * The plain interval data, for JSON serialization.
689
- *
690
- * Private class fields do not serialize, so an explicit `toJSON` keeps
691
- * `JSON.stringify(interval)` from collapsing to `{}`.
1013
+ * Build a chord from a root and quality.
692
1014
  *
693
- * @returns The diatonic number, quality, and semitone span.
1015
+ * @param root Root as a note name (e.g. `'Eb'`) or a pitch class.
1016
+ * @param quality The chord quality.
1017
+ * @param bass Optional slash-chord bass, as a note name (e.g. `'Bb'`) or a
1018
+ * pitch class. A named bass keeps its own spelling.
1019
+ * @returns The chord (without key context).
694
1020
  */
695
- toJSON() {
696
- return { number: this.#number, quality: this.#quality, semitones: this.#semitones };
697
- }
698
- };
699
-
700
- // src/model/key.ts
701
- function resolveTonic(root, spelling) {
702
- return typeof root === "string" ? Note.of(root) : new Note(spellPitchClassBare(root, spelling));
703
- }
704
- function accidentalLoad(tonic, scale) {
705
- return spellScale(tonic, scale).reduce((sum, note) => sum + Math.abs(note.alter), 0);
706
- }
707
- function bestTonicForScale(rootPc, scale) {
708
- const sharp = spellPitchClassBare(rootPc, "sharp");
709
- const flat = spellPitchClassBare(rootPc, "flat");
710
- if (sharp.letter === flat.letter && sharp.alter === flat.alter) {
711
- return new Note(sharp);
1021
+ static of(root, quality, bass) {
1022
+ const bassNote = typeof bass === "string" ? Note.of(bass) : void 0;
1023
+ const bassPc = bassNote !== void 0 ? bassNote.pitchClass : bass;
1024
+ const rootNote = typeof root === "string" ? Note.of(root) : void 0;
1025
+ const data = makeChord(
1026
+ rootNote !== void 0 ? rootNote.pitchClass : root,
1027
+ quality,
1028
+ bassPc
1029
+ );
1030
+ if (rootNote !== void 0) {
1031
+ data.rootSpelling = { letter: rootNote.letter, alter: rootNote.alter };
1032
+ }
1033
+ if (bassNote !== void 0) {
1034
+ data.bassSpelling = { letter: bassNote.letter, alter: bassNote.alter };
1035
+ }
1036
+ return new _Chord(data);
712
1037
  }
713
- return accidentalLoad(flat, scale) <= accidentalLoad(sharp, scale) ? new Note(flat) : new Note(sharp);
714
- }
715
- var Key = class _Key {
716
- #scale;
717
- #tonic;
718
1038
  /**
719
- * Wrap a key/scale and its spelled tonic.
1039
+ * Wrap an existing plain chord object.
720
1040
  *
721
- * @param scale The key/scale; its root is normalized to a pitch class.
722
- * @param tonic The spelled tonic anchoring letter-name spelling.
1041
+ * @param data The plain chord.
1042
+ * @returns The wrapped chord (without key context).
723
1043
  */
724
- constructor(scale, tonic) {
725
- this.#scale = { rootPc: mod12(scale.rootPc), modeMask12: scale.modeMask12 };
726
- this.#tonic = tonic;
1044
+ static from(data) {
1045
+ return new _Chord(data);
727
1046
  }
728
1047
  /**
729
- * A major key.
1048
+ * Alias of {@link Chord.from}, matching the `fromData` factory on the other
1049
+ * classes.
730
1050
  *
731
- * @param root Tonic as a note name (e.g. `'Eb'`) or a pitch class; a numeric
732
- * root is spelled with whichever accidental side yields the fewest
733
- * accidentals across the scale.
734
- * @returns The major key.
1051
+ * @param data The plain chord.
1052
+ * @returns The wrapped chord (without key context).
735
1053
  */
736
- static major(root) {
737
- if (typeof root === "string") {
738
- const tonic = Note.of(root);
739
- return new _Key(majorKey(tonic.pitchClass), tonic);
740
- }
741
- const scale = majorKey(root);
742
- return new _Key(scale, bestTonicForScale(root, scale));
1054
+ static fromData(data) {
1055
+ return new _Chord(data);
743
1056
  }
744
1057
  /**
745
- * A natural-minor key.
1058
+ * Rebuild a chord from its {@link Chord.toJSON} output.
746
1059
  *
747
- * @param root Tonic as a note name or a pitch class; a numeric root is
748
- * spelled with whichever accidental side yields the fewest accidentals
749
- * across the scale.
750
- * @returns The minor key.
1060
+ * The key context is not serialized, so the result carries none; re-attach
1061
+ * one with {@link Chord.withKey}.
1062
+ *
1063
+ * @param data The serialized chord.
1064
+ * @returns The wrapped chord.
751
1065
  */
752
- static minor(root) {
753
- if (typeof root === "string") {
754
- const tonic = Note.of(root);
755
- return new _Key(minorKey(tonic.pitchClass), tonic);
756
- }
757
- const scale = minorKey(root);
758
- return new _Key(scale, bestTonicForScale(root, scale));
1066
+ static fromJSON(data) {
1067
+ return new _Chord(data);
759
1068
  }
760
1069
  /**
761
- * A key on a named scale (e.g. `'dorian'`, `'harmonicMinor'`).
1070
+ * Parse a lead-sheet chord symbol (e.g. `'Cmaj7'`, `'F#m7b5'`, `'C/G'`).
762
1071
  *
763
- * @param name The scale name, a key of the scale module's named-scale table.
764
- * @param root Tonic as a note name or a pitch class.
765
- * @returns The key.
766
- * @throws If the name is not a known scale.
1072
+ * @param symbol The chord symbol.
1073
+ * @returns The chord (without key context).
1074
+ * @throws If the root or quality is not recognized.
767
1075
  */
768
- static named(name, root) {
769
- const tonic = resolveTonic(root, "sharp");
770
- return new _Key(scaleByName(name, tonic.pitchClass), tonic);
1076
+ static parse(symbol) {
1077
+ return new _Chord(parseChordSymbol(symbol));
771
1078
  }
772
1079
  /**
773
- * Wrap an existing `KeyScale`, synthesizing a spelled tonic when none is
774
- * given (flat-preferred for minor-third scales, sharp-preferred otherwise).
1080
+ * Identify the chords matching a set of pitches, best interpretation first.
775
1081
  *
776
- * @param scale The key/scale to wrap.
777
- * @param tonic Optional spelled tonic.
778
- * @returns The key.
1082
+ * @param pitches MIDI pitches or bare pitch classes.
1083
+ * @param opts How to interpret the input; see {@link DetectChordOptions}.
1084
+ * @returns Ranked chord interpretations (may be empty).
779
1085
  */
780
- static of(scale, tonic) {
781
- const spelled = tonic ?? new Note(spellPitchClassBare(scale.rootPc, isMinorKey(scale) ? "flat" : "sharp"));
782
- return new _Key(scale, spelled);
1086
+ static detect(pitches, opts) {
1087
+ return detectChord(pitches, opts).map(
1088
+ (match) => new _Chord(makeChord(match.rootPc, match.quality, match.bassPc))
1089
+ );
783
1090
  }
784
- /** A copy of the underlying plain `KeyScale`. */
785
- get scale() {
786
- return { rootPc: this.#scale.rootPc, modeMask12: this.#scale.modeMask12 };
1091
+ /**
1092
+ * Identify the chords matching a set of pitches, keeping each match's
1093
+ * recognition metadata beside the chord.
1094
+ *
1095
+ * {@link Chord.detect} discards the confidence signals a recognition UI needs
1096
+ * — whether the set matched exactly, which chord tones were missing, which
1097
+ * input notes were foreign, and which inversion the bass implies.
1098
+ *
1099
+ * @param pitches MIDI pitches or bare pitch classes.
1100
+ * @param opts How to interpret the input; see {@link DetectChordOptions}.
1101
+ * @returns Ranked interpretations, each with its chord and its match record.
1102
+ * @example
1103
+ * ```ts
1104
+ * import { Chord } from '@libraz/libcantus';
1105
+ * const [best] = Chord.detectMatches([60, 64, 67]);
1106
+ * best?.match.exact; // true
1107
+ * ```
1108
+ */
1109
+ static detectMatches(pitches, opts) {
1110
+ return detectChord(pitches, opts).map((match) => ({
1111
+ chord: new _Chord(makeChord(match.rootPc, match.quality, match.bassPc)),
1112
+ match
1113
+ }));
787
1114
  }
788
- /** The spelled tonic. */
789
- get tonic() {
790
- return this.#tonic;
1115
+ /**
1116
+ * The single best chord interpretation of a pitch set.
1117
+ *
1118
+ * @param pitches MIDI pitches or bare pitch classes.
1119
+ * @param opts How to interpret the input; see {@link DetectChordOptions}.
1120
+ * @returns The top-ranked chord, or null when nothing matches.
1121
+ */
1122
+ static detectBest(pitches, opts) {
1123
+ const best = detectChordBest(pitches, opts);
1124
+ return best === null ? null : new _Chord(best);
791
1125
  }
792
- /** The tonic pitch class (0..11). */
1126
+ /** The root pitch class (0..11). */
793
1127
  get rootPc() {
794
- return this.#scale.rootPc;
1128
+ return this.#data.rootPc;
795
1129
  }
796
- /** Whether the scale has a minor third and no major third. */
797
- get isMinor() {
798
- return isMinorKey(this.#scale);
1130
+ /** The chord quality. */
1131
+ get quality() {
1132
+ return this.#data.quality;
1133
+ }
1134
+ /** A copy of the semitone offsets above the root. */
1135
+ get intervals() {
1136
+ return [...this.#data.intervals];
1137
+ }
1138
+ /** The slash-chord bass pitch class, or undefined in root position. */
1139
+ get bassPc() {
1140
+ return this.#data.bassPc;
799
1141
  }
800
1142
  /**
801
- * The scale's pitch classes in ascending scale-degree order (degree 0 first).
1143
+ * A copy of the underlying plain chord object, including the spellings
1144
+ * derived from any attached key.
1145
+ */
1146
+ get data() {
1147
+ return this.#data;
1148
+ }
1149
+ /** The carried key context, if any. */
1150
+ get key() {
1151
+ return this.#key;
1152
+ }
1153
+ /**
1154
+ * A copy of this chord carrying the given key context.
802
1155
  *
803
- * @returns One pitch class per scale degree.
1156
+ * Only a spelling the caller supplied (via `Chord.parse`, `Chord.of` with a
1157
+ * named root, or plain data carrying a hint) survives; a spelling that came
1158
+ * from a previously attached key is re-derived, so re-keying a progression
1159
+ * during a modulation does not keep the old key's letters and the order of
1160
+ * `withKey` calls does not affect the result.
1161
+ *
1162
+ * @param key The key context to attach.
1163
+ * @returns The new chord.
1164
+ */
1165
+ withKey(key) {
1166
+ return new _Chord(this.#given, key);
1167
+ }
1168
+ /**
1169
+ * The chord's sorted, deduplicated pitch classes.
1170
+ *
1171
+ * @returns Pitch classes ascending in [0, 11].
804
1172
  */
805
1173
  pitchClasses() {
806
- return scaleTonesInDegreeOrder(this.#scale);
1174
+ return chordPitchClasses(this.#data);
807
1175
  }
808
1176
  /**
809
- * The spelled scale, one note per degree (e.g. C D E F G A B for C major).
1177
+ * The chord's Roman numeral in a key.
810
1178
  *
811
- * @returns Spelled octave-less notes in scale-degree order.
1179
+ * @param key Key to analyze in; falls back to the carried context.
1180
+ * @returns The Roman numeral string.
1181
+ * @throws If no key is given and none is carried.
812
1182
  */
813
- notes() {
814
- return spellScale(this.#tonic.data, this.#scale).map((note) => new Note(note));
1183
+ roman(key) {
1184
+ return chordToRoman(this.#data, this.#resolveKey(key).scale);
815
1185
  }
816
1186
  /**
817
- * Alias of {@link Key.notes}.
1187
+ * The chord's harmonic function (tonic / subdominant / dominant) in a key.
818
1188
  *
819
- * @returns Spelled octave-less notes in scale-degree order.
1189
+ * @param key Key to analyze in; falls back to the carried context.
1190
+ * @returns The harmonic function.
1191
+ * @throws If no key is given and none is carried.
820
1192
  */
821
- spell() {
822
- return this.notes();
1193
+ function(key) {
1194
+ return functionOf(this.#data, this.#resolveKey(key).scale);
823
1195
  }
824
1196
  /**
825
- * The spelled scale as letter-name strings.
1197
+ * Full functional analysis: function, borrowing, and Roman numeral.
826
1198
  *
827
- * @returns One name per scale degree.
1199
+ * @param key Key to analyze in; falls back to the carried context.
1200
+ * @returns The chord analysis.
1201
+ * @throws If no key is given and none is carried.
828
1202
  */
829
- noteNames() {
830
- return this.notes().map((note) => note.name);
1203
+ analyze(key) {
1204
+ return analyzeChord(this.#data, this.#resolveKey(key).scale);
831
1205
  }
832
1206
  /**
833
- * Build a chord on a scale degree, carrying this key as context.
1207
+ * Whether the chord is borrowed from the parallel mode (modal interchange).
834
1208
  *
835
- * With an explicit quality the quality's interval template is attached to the
836
- * degree's diatonic root; without one the scale-correct diatonic triad is
837
- * stacked (e.g. a diminished triad on the leading tone of a major key).
1209
+ * @param key Key to analyze in; falls back to the carried context.
1210
+ * @returns True if the chord is borrowed.
1211
+ * @throws If no key is given and none is carried.
1212
+ */
1213
+ isBorrowed(key) {
1214
+ return isBorrowedChord(this.#data, this.#resolveKey(key).scale);
1215
+ }
1216
+ /**
1217
+ * The origin of a non-diatonic chord (parallel mode or Neapolitan), or null.
838
1218
  *
839
- * @param degree 0-based scale degree of the chord root.
840
- * @param quality Optional chord quality.
841
- * @returns The chord, with this key attached.
1219
+ * @param key Key to analyze in; falls back to the carried context.
1220
+ * @returns The borrowing source, or null.
1221
+ * @throws If no key is given and none is carried.
842
1222
  */
843
- chord(degree, quality) {
844
- const data = quality === void 0 ? diatonicTriad(degree, this.#scale) : chordFromDegree(degree, quality, this.#scale);
845
- return new Chord(data, this);
1223
+ borrowedSource(key) {
1224
+ return borrowedSource(this.#data, this.#resolveKey(key).scale);
846
1225
  }
847
1226
  /**
848
- * The diatonic triad on a scale degree, carrying this key as context.
1227
+ * The chord rendered as a lead-sheet symbol (e.g. `'Cmaj7'`, `'F#m7'`, `'C/G'`).
849
1228
  *
850
- * @param degree 0-based scale degree of the chord root.
851
- * @returns The triad, with this key attached.
1229
+ * @param opts Set `flats: true` to spell the root/bass with flats.
1230
+ * @returns The chord symbol.
852
1231
  */
853
- diatonicTriad(degree) {
854
- return new Chord(diatonicTriad(degree, this.#scale), this);
1232
+ symbol(opts) {
1233
+ return formatChordSymbol(this.#data, opts);
855
1234
  }
856
1235
  /**
857
- * The diatonic seventh chord on a scale degree, carrying this key as context.
1236
+ * Realize the chord as one MIDI pitch per voice, ascending.
858
1237
  *
859
- * @param degree 0-based scale degree of the chord root.
860
- * @returns The seventh chord, with this key attached.
1238
+ * A chord that carries a key passes it to the voicer, so the leading tone is
1239
+ * not doubled; an explicit `opts.key` overrides it.
1240
+ *
1241
+ * @param opts Voicing options; defaults to four SATB voices.
1242
+ * @returns MIDI pitches, ascending, one per voice.
1243
+ * @throws If no voicing fits the given ranges.
861
1244
  */
862
- diatonicSeventh(degree) {
863
- return new Chord(diatonicSeventh(degree, this.#scale), this);
1245
+ voice(opts) {
1246
+ const key = opts?.key ?? this.#key?.scale;
1247
+ return voiceChord(this.#data, key === void 0 ? opts : { ...opts, key });
864
1248
  }
865
1249
  /**
866
- * Build the chord denoted by a Roman numeral in this key (including applied
867
- * chords such as `'V7/V'`), carrying this key as context.
1250
+ * Realize the chord as a single styled voicing (`close`, `drop2`, `drop3`,
1251
+ * `shell`, or `rootless`), optionally constraining the top voice.
868
1252
  *
869
- * @param text The Roman numeral.
870
- * @returns The chord, with this key attached.
871
- * @throws If the numeral is not valid.
1253
+ * @param opts Styled-voicing options; defaults to a close-position voicing.
1254
+ * @returns MIDI pitches, ascending.
872
1255
  */
873
- roman(text) {
874
- return new Chord(romanToChord(text, this.#scale), this);
1256
+ styledVoicing(opts) {
1257
+ return voiceChordStyled(this.#data, opts);
875
1258
  }
876
1259
  /**
877
- * Whether a pitch belongs to the scale.
1260
+ * The negative-harmony mirror of the chord about the key's tonic–dominant
1261
+ * axis (major becomes minor and vice versa).
878
1262
  *
879
- * @param x A MIDI pitch, bare pitch class, or note.
880
- * @returns True if the pitch class is a scale tone.
1263
+ * @param key Key providing the reflection axis; falls back to the carried
1264
+ * context.
1265
+ * @returns The mirrored chord, keeping any key context.
1266
+ * @throws If no key is given and none is carried.
881
1267
  */
882
- contains(x) {
883
- return isScaleTone(typeof x === "number" ? x : x.pitchClass, this.#scale);
1268
+ negativeHarmony(key) {
1269
+ const resolved = this.#resolveKey(key);
1270
+ return new _Chord(negativeHarmonyMirror(this.#data, resolved.scale), key ?? this.#key);
884
1271
  }
885
1272
  /**
886
- * The plain key data, for JSON serialization.
1273
+ * The n-th inversion: a copy whose bass is the chord tone `n` steps above the
1274
+ * root in the interval template (`invert(1)` puts the third in the bass).
1275
+ * `n` wraps around the template length; negative values count backwards.
887
1276
  *
888
- * Private class fields do not serialize, so an explicit `toJSON` keeps
889
- * `JSON.stringify(key)` from collapsing to `{}`. The result pairs the
890
- * `KeyScale` with the spelled tonic, enough to reconstruct the key via
891
- * {@link Key.of}.
1277
+ * `invert(0)` (and any `n` that wraps to it) is root position, so it carries
1278
+ * no slash bass and equals the original chord.
892
1279
  *
893
- * @returns The key/scale and its spelled tonic.
1280
+ * @param n The inversion number.
1281
+ * @returns The inverted chord, keeping any key context.
1282
+ * @throws If the chord has no intervals.
1283
+ */
1284
+ invert(n) {
1285
+ const data = copyChord(this.#given);
1286
+ const intervals = data.intervals;
1287
+ const length = intervals.length;
1288
+ if (length === 0) {
1289
+ throw new InvalidInputError("cannot invert a chord with no intervals");
1290
+ }
1291
+ const index = (n % length + length) % length;
1292
+ delete data.bassSpelling;
1293
+ if (index === 0) {
1294
+ delete data.bassPc;
1295
+ } else {
1296
+ data.bassPc = pitchClassOf(data.rootPc + (intervals[index] ?? 0));
1297
+ }
1298
+ return new _Chord(data, this.#key);
1299
+ }
1300
+ /**
1301
+ * Transpose the chord by a number of semitones.
1302
+ *
1303
+ * The quality and interval template are carried over, so a chord that a
1304
+ * symbol round-trip could not express — a custom interval set, an inversion —
1305
+ * survives. A carried key moves with the chord, so the transposed chord keeps
1306
+ * the same degree and function inside the transposed key.
1307
+ *
1308
+ * @param semitones The signed semitone offset.
1309
+ * @returns The transposed chord, in the transposed key when one is carried.
1310
+ * @example
1311
+ * ```ts
1312
+ * import { Chord } from '@libraz/libcantus';
1313
+ * Chord.parse('C/G').transpose(2).symbol(); // 'D/A'
1314
+ * ```
1315
+ */
1316
+ transpose(semitones) {
1317
+ const moved = transposeChord(this.#given, semitones);
1318
+ return new _Chord(moved, this.#key?.transpose(semitones));
1319
+ }
1320
+ /**
1321
+ * The named scales that fit over this chord, best fit first, rooted on the
1322
+ * chord root.
1323
+ *
1324
+ * @returns The matching scales.
1325
+ */
1326
+ scales() {
1327
+ return chordScales(this.#data);
1328
+ }
1329
+ /**
1330
+ * The available tensions (usable non-chord, non-avoid scale tones) of a
1331
+ * scale over this chord.
1332
+ *
1333
+ * @param scaleName A named scale, rooted on the chord root.
1334
+ * @returns Tension pitch classes, ascending in [0, 11].
1335
+ * @throws If `scaleName` is not a built-in scale.
1336
+ */
1337
+ tensions(scaleName) {
1338
+ return availableTensions(this.#data, scaleName);
1339
+ }
1340
+ /**
1341
+ * The avoid notes (scale tones a semitone above a chord tone) of a scale
1342
+ * over this chord.
1343
+ *
1344
+ * @param scaleName A named scale, rooted on the chord root.
1345
+ * @returns Avoid-note pitch classes, ascending in [0, 11].
1346
+ * @throws If `scaleName` is not a built-in scale.
1347
+ */
1348
+ avoidNotes(scaleName) {
1349
+ return avoidNotes(this.#data, scaleName);
1350
+ }
1351
+ /**
1352
+ * Spell the chord tones with letter names, root first, in the key's spelling.
1353
+ *
1354
+ * A chord that already knows how its root is spelled — one from
1355
+ * `Chord.parse`, or `Chord.of` with a named root — can spell itself with no
1356
+ * key at all; a key is only needed to choose a spelling for a bare pitch
1357
+ * class.
1358
+ *
1359
+ * A slash bass outside the chord tones is appended, so the spelled notes
1360
+ * are the notes the chord sounds — the same set {@link Chord.pitchClasses}
1361
+ * reports.
1362
+ *
1363
+ * @param key Key providing the spelled tonic; falls back to the carried
1364
+ * context, then to the chord's own root spelling.
1365
+ * @returns Spelled octave-less notes in the chord's own (tertian) order,
1366
+ * with any foreign slash bass last.
1367
+ * @throws If no key is given, none is carried, and the chord has no root
1368
+ * spelling of its own.
1369
+ */
1370
+ spell(key) {
1371
+ const data = this.#data;
1372
+ const resolved = key ?? this.#key;
1373
+ let tones;
1374
+ if (resolved === void 0) {
1375
+ const root = data.rootSpelling;
1376
+ if (root === void 0) {
1377
+ throw new InvalidInputError(
1378
+ "chord has no key context and no root spelling; pass a Key, attach one with withKey(), or build the chord from a symbol"
1379
+ );
1380
+ }
1381
+ tones = spellChordFromRoot(data, root);
1382
+ } else {
1383
+ tones = spellChord(data, resolved.tonic.data, resolved.scale);
1384
+ }
1385
+ const bassPc = data.bassPc;
1386
+ const bass = data.bassSpelling;
1387
+ if (bassPc !== void 0 && bass !== void 0 && !tones.some((tone) => pitchClassOf(noteToPitchClass(tone)) === pitchClassOf(bassPc))) {
1388
+ tones = [...tones, { letter: bass.letter, alter: bass.alter }];
1389
+ }
1390
+ return tones.map((note) => new Note(note));
1391
+ }
1392
+ /**
1393
+ * Start a progression with this chord followed by others, carrying this
1394
+ * chord's key context (if any).
1395
+ *
1396
+ * @param others The chords following this one.
1397
+ * @returns The progression.
1398
+ */
1399
+ progressionTo(...others) {
1400
+ return new Progression([this, ...others], this.#key);
1401
+ }
1402
+ /**
1403
+ * Whether another chord has the same root, quality, intervals, and bass.
1404
+ * Key context is not compared.
1405
+ *
1406
+ * @param other The chord to compare.
1407
+ * @returns True if the chord data is identical.
1408
+ */
1409
+ equals(other) {
1410
+ const a = this.#data;
1411
+ const b = other.data;
1412
+ return a.rootPc === b.rootPc && a.quality === b.quality && a.bassPc === b.bassPc && a.intervals.length === b.intervals.length && a.intervals.every((interval, i) => interval === b.intervals[i]);
1413
+ }
1414
+ /**
1415
+ * The plain chord data, for JSON serialization.
1416
+ *
1417
+ * @returns A copy of the underlying plain chord object.
894
1418
  */
895
1419
  toJSON() {
896
- return { scale: this.scale, tonic: this.#tonic.data };
1420
+ return this.data;
1421
+ }
1422
+ /**
1423
+ * The chord symbol, so a template literal or a log line reads as the chord.
1424
+ *
1425
+ * @returns The symbol, e.g. `'Cmaj7'`.
1426
+ */
1427
+ toString() {
1428
+ return this.symbol();
1429
+ }
1430
+ /** Resolve the key for an analysis method: explicit first, then carried. */
1431
+ #resolveKey(key) {
1432
+ const resolved = key ?? this.#key;
1433
+ if (resolved === void 0) {
1434
+ throw new InvalidInputError(
1435
+ "chord has no key context; pass a Key or attach one with withKey()"
1436
+ );
1437
+ }
1438
+ return resolved;
897
1439
  }
898
1440
  };
899
1441
 
900
1442
  export {
1443
+ Interval,
901
1444
  Note,
1445
+ Key,
902
1446
  Progression,
903
- Chord,
904
- Interval,
905
- Key
1447
+ Chord
906
1448
  };
907
- //# sourceMappingURL=chunk-COKTX2G5.js.map
1449
+ //# sourceMappingURL=chunk-EHVEBWM5.js.map