@libraz/libcantus 0.9.3 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/README.md +2 -2
  2. package/dist/analyze/index.cjs +39 -1947
  3. package/dist/analyze/index.cjs.map +1 -1
  4. package/dist/analyze/index.d.cts +197 -37
  5. package/dist/analyze/index.d.ts +197 -37
  6. package/dist/analyze/index.js +19 -8
  7. package/dist/chunk-36N73KZ3.cjs +246 -0
  8. package/dist/chunk-36N73KZ3.cjs.map +1 -0
  9. package/dist/chunk-4HLLZPXT.cjs +3077 -0
  10. package/dist/chunk-4HLLZPXT.cjs.map +1 -0
  11. package/dist/chunk-7MGZ7D62.cjs +498 -0
  12. package/dist/chunk-7MGZ7D62.cjs.map +1 -0
  13. package/dist/{chunk-4MHA432G.js → chunk-7NKDPF74.js} +403 -180
  14. package/dist/chunk-7NKDPF74.js.map +1 -0
  15. package/dist/chunk-CAN76NCU.cjs +679 -0
  16. package/dist/chunk-CAN76NCU.cjs.map +1 -0
  17. package/dist/{chunk-MIWO425M.js → chunk-D2FAJBYL.js} +101 -34
  18. package/dist/chunk-D2FAJBYL.js.map +1 -0
  19. package/dist/{chunk-SGU7SPLK.js → chunk-EHVEBWM5.js} +1056 -531
  20. package/dist/chunk-EHVEBWM5.js.map +1 -0
  21. package/dist/chunk-FCANXBAT.cjs +30 -0
  22. package/dist/chunk-FCANXBAT.cjs.map +1 -0
  23. package/dist/chunk-FNDWXRRI.cjs +479 -0
  24. package/dist/chunk-FNDWXRRI.cjs.map +1 -0
  25. package/dist/{chunk-IT2O4XFE.js → chunk-GQSGS2N4.js} +43 -24
  26. package/dist/chunk-GQSGS2N4.js.map +1 -0
  27. package/dist/{chunk-62QZKCJQ.js → chunk-HKGYCQ34.js} +10 -6
  28. package/dist/chunk-HKGYCQ34.js.map +1 -0
  29. package/dist/{chunk-AKLYPWIN.js → chunk-I24MV65U.js} +29 -6
  30. package/dist/chunk-I24MV65U.js.map +1 -0
  31. package/dist/{chunk-CHMXELDS.js → chunk-KMHOICIY.js} +311 -181
  32. package/dist/chunk-KMHOICIY.js.map +1 -0
  33. package/dist/chunk-LW53AHQU.cjs +1117 -0
  34. package/dist/chunk-LW53AHQU.cjs.map +1 -0
  35. package/dist/{chunk-2ZWMSJH6.js → chunk-NKIBQ6PT.js} +2 -2
  36. package/dist/chunk-NKIBQ6PT.js.map +1 -0
  37. package/dist/chunk-NP4NA2FB.cjs +106 -0
  38. package/dist/chunk-NP4NA2FB.cjs.map +1 -0
  39. package/dist/chunk-NVE34YGZ.cjs +1449 -0
  40. package/dist/chunk-NVE34YGZ.cjs.map +1 -0
  41. package/dist/{chunk-EH6EM37U.js → chunk-NYYWMV2C.js} +61 -49
  42. package/dist/chunk-NYYWMV2C.js.map +1 -0
  43. package/dist/{chunk-GOAW4VYL.js → chunk-Q2XUP227.js} +243 -117
  44. package/dist/chunk-Q2XUP227.js.map +1 -0
  45. package/dist/chunk-Q6OP4U43.cjs +327 -0
  46. package/dist/chunk-Q6OP4U43.cjs.map +1 -0
  47. package/dist/chunk-RC6GBMCD.cjs +845 -0
  48. package/dist/chunk-RC6GBMCD.cjs.map +1 -0
  49. package/dist/{chunk-C5XYKUVG.js → chunk-RK35RIN7.js} +370 -157
  50. package/dist/chunk-RK35RIN7.js.map +1 -0
  51. package/dist/chunk-RUMU3UA6.cjs +53 -0
  52. package/dist/chunk-RUMU3UA6.cjs.map +1 -0
  53. package/dist/chunk-T64C7ZD7.cjs +1 -0
  54. package/dist/chunk-T64C7ZD7.cjs.map +1 -0
  55. package/dist/chunk-XEBG3G53.js +498 -0
  56. package/dist/chunk-XEBG3G53.js.map +1 -0
  57. package/dist/{chunk-I5R7ZBJS.js → chunk-YLEMGEFH.js} +103 -59
  58. package/dist/chunk-YLEMGEFH.js.map +1 -0
  59. package/dist/chunk-ZEJR4TKL.cjs +173 -0
  60. package/dist/chunk-ZEJR4TKL.cjs.map +1 -0
  61. package/dist/core/index.cjs +130 -647
  62. package/dist/core/index.cjs.map +1 -1
  63. package/dist/core/index.d.cts +301 -39
  64. package/dist/core/index.d.ts +301 -39
  65. package/dist/core/index.js +54 -16
  66. package/dist/{function-DYt4iKsn.d.cts → function-BtiZb9i9.d.ts} +28 -7
  67. package/dist/{function-DVH4Eusr.d.ts → function-DNugAOrt.d.cts} +28 -7
  68. package/dist/generate/index.cjs +28 -4085
  69. package/dist/generate/index.cjs.map +1 -1
  70. package/dist/generate/index.d.cts +370 -62
  71. package/dist/generate/index.d.ts +370 -62
  72. package/dist/generate/index.js +23 -9
  73. package/dist/{index-CxqBQtH1.d.cts → index-6VR8-kit.d.ts} +19 -4
  74. package/dist/index-70p168VM.d.ts +192 -0
  75. package/dist/{index-CpOsP1y9.d.cts → index-Be74mcon.d.cts} +18 -17
  76. package/dist/index-BtXT6UQ0.d.cts +192 -0
  77. package/dist/{index-DxvARIP2.d.ts → index-C5UBbQ4G.d.ts} +18 -17
  78. package/dist/{index-Bt4tOUD9.d.ts → index-C83mZI1P.d.ts} +93 -8
  79. package/dist/{index-Bg5dYMmZ.d.ts → index-D6b6na69.d.cts} +14 -11
  80. package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
  81. package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
  82. package/dist/index-OG65ocOr.d.cts +272 -0
  83. package/dist/index-OG65ocOr.d.ts +272 -0
  84. package/dist/{index-Jc0Ryi79.d.ts → index-tJhkmQgE.d.cts} +19 -4
  85. package/dist/{index-Dikst44v.d.cts → index-tSQh-xwY.d.ts} +14 -11
  86. package/dist/{index-BNlGP86A.d.cts → index-y_ak9_Hi.d.cts} +93 -8
  87. package/dist/index.cjs +419 -7383
  88. package/dist/index.cjs.map +1 -1
  89. package/dist/index.d.cts +14 -14
  90. package/dist/index.d.ts +14 -14
  91. package/dist/index.js +111 -27
  92. package/dist/model/index.cjs +8 -2562
  93. package/dist/model/index.cjs.map +1 -1
  94. package/dist/model/index.d.cts +433 -75
  95. package/dist/model/index.d.ts +433 -75
  96. package/dist/model/index.js +6 -7
  97. package/dist/styled-DIRkk9Uz.d.ts +506 -0
  98. package/dist/styled-DJbkJQfj.d.cts +506 -0
  99. package/dist/theory/index.cjs +172 -1822
  100. package/dist/theory/index.cjs.map +1 -1
  101. package/dist/theory/index.d.cts +151 -233
  102. package/dist/theory/index.d.ts +151 -233
  103. package/dist/theory/index.js +30 -7
  104. package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
  105. package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
  106. package/package.json +24 -6
  107. package/dist/chunk-2ZWMSJH6.js.map +0 -1
  108. package/dist/chunk-4MHA432G.js.map +0 -1
  109. package/dist/chunk-62QZKCJQ.js.map +0 -1
  110. package/dist/chunk-AKLYPWIN.js.map +0 -1
  111. package/dist/chunk-C5XYKUVG.js.map +0 -1
  112. package/dist/chunk-CHMXELDS.js.map +0 -1
  113. package/dist/chunk-DYWFNC4A.js +0 -152
  114. package/dist/chunk-DYWFNC4A.js.map +0 -1
  115. package/dist/chunk-EH6EM37U.js.map +0 -1
  116. package/dist/chunk-FBQK3ZQN.js +0 -131
  117. package/dist/chunk-FBQK3ZQN.js.map +0 -1
  118. package/dist/chunk-GOAW4VYL.js.map +0 -1
  119. package/dist/chunk-I5R7ZBJS.js.map +0 -1
  120. package/dist/chunk-IT2O4XFE.js.map +0 -1
  121. package/dist/chunk-MIWO425M.js.map +0 -1
  122. package/dist/chunk-SGU7SPLK.js.map +0 -1
  123. package/dist/index-CGMZNCx6.d.cts +0 -140
  124. package/dist/index-CGMZNCx6.d.ts +0 -140
  125. package/dist/index-CnZCo29u.d.cts +0 -108
  126. package/dist/index-JnF4v9bx.d.ts +0 -108
  127. package/dist/styled-DZ7GjShb.d.ts +0 -282
  128. package/dist/styled-ZTbdaVFM.d.cts +0 -282
  129. package/src/analyze/arrange/index.ts +0 -21
  130. package/src/analyze/arrange/internal.ts +0 -127
  131. package/src/analyze/arrange/tension.ts +0 -199
  132. package/src/analyze/arrange/tracks.ts +0 -312
  133. package/src/analyze/detect/index.ts +0 -272
  134. package/src/analyze/functional/borrowed.ts +0 -84
  135. package/src/analyze/functional/cadence.ts +0 -66
  136. package/src/analyze/functional/function.ts +0 -184
  137. package/src/analyze/functional/index.ts +0 -23
  138. package/src/analyze/functional/internal.ts +0 -29
  139. package/src/analyze/functional/roman.ts +0 -402
  140. package/src/analyze/index.ts +0 -49
  141. package/src/analyze/timeline/index.ts +0 -451
  142. package/src/analyze/voice/index.ts +0 -339
  143. package/src/core/event-index/index.ts +0 -104
  144. package/src/core/index.ts +0 -61
  145. package/src/core/interval/index.ts +0 -81
  146. package/src/core/meter/index.ts +0 -302
  147. package/src/core/pitch/index.ts +0 -311
  148. package/src/core/random/index.ts +0 -76
  149. package/src/core/tuning/index.ts +0 -186
  150. package/src/core/types.ts +0 -29
  151. package/src/core/validation/index.ts +0 -119
  152. package/src/generate/bass/index.ts +0 -373
  153. package/src/generate/countermelody/index.ts +0 -470
  154. package/src/generate/drums/beat.ts +0 -266
  155. package/src/generate/drums/euclid.ts +0 -65
  156. package/src/generate/drums/fills.ts +0 -356
  157. package/src/generate/drums/ghost.ts +0 -119
  158. package/src/generate/drums/hihat.ts +0 -285
  159. package/src/generate/drums/hit.ts +0 -31
  160. package/src/generate/drums/index.ts +0 -361
  161. package/src/generate/drums/internal.ts +0 -257
  162. package/src/generate/drums/kick.ts +0 -160
  163. package/src/generate/drums/percussion.ts +0 -121
  164. package/src/generate/drums/rng.ts +0 -7
  165. package/src/generate/drums/swing.ts +0 -71
  166. package/src/generate/groove/index.ts +0 -351
  167. package/src/generate/harmonize/index.ts +0 -451
  168. package/src/generate/index.ts +0 -58
  169. package/src/generate/motif/index.ts +0 -422
  170. package/src/generate/progression/index.ts +0 -362
  171. package/src/generate/reharmony/index.ts +0 -323
  172. package/src/generate/rhythm/index.ts +0 -175
  173. package/src/index.ts +0 -13
  174. package/src/model/chord.ts +0 -442
  175. package/src/model/index.ts +0 -15
  176. package/src/model/interval.ts +0 -82
  177. package/src/model/key.ts +0 -266
  178. package/src/model/note.ts +0 -176
  179. package/src/model/progression.ts +0 -161
  180. package/src/model/shared.ts +0 -12
  181. package/src/theory/chord/index.ts +0 -375
  182. package/src/theory/chordscale/index.ts +0 -374
  183. package/src/theory/counterpoint/index.ts +0 -230
  184. package/src/theory/harmony/index.ts +0 -77
  185. package/src/theory/index.ts +0 -112
  186. package/src/theory/safety/index.ts +0 -488
  187. package/src/theory/scale/degrees.ts +0 -123
  188. package/src/theory/scale/index.ts +0 -28
  189. package/src/theory/scale/key.ts +0 -56
  190. package/src/theory/scale/masks.ts +0 -153
  191. package/src/theory/spelling/index.ts +0 -249
  192. package/src/theory/symbol/index.ts +0 -268
  193. package/src/theory/voicing/index.ts +0 -5
  194. package/src/theory/voicing/internal.ts +0 -168
  195. package/src/theory/voicing/leading.ts +0 -112
  196. package/src/theory/voicing/satb.ts +0 -216
  197. package/src/theory/voicing/styled.ts +0 -199
  198. package/tsconfig.json +0 -23
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  negativeHarmonyMirror
3
- } from "./chunk-IT2O4XFE.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-CHMXELDS.js";
17
+ } from "./chunk-KMHOICIY.js";
16
18
  import {
17
19
  availableTensions,
18
20
  avoidNotes,
@@ -26,15 +28,7 @@ import {
26
28
  voiceChord,
27
29
  voiceChordStyled,
28
30
  voiceProgression
29
- } from "./chunk-C5XYKUVG.js";
30
- import {
31
- formatNote,
32
- midiToNote,
33
- noteToMidi,
34
- noteToPitchClass,
35
- parseNote,
36
- spelledInterval
37
- } from "./chunk-DYWFNC4A.js";
31
+ } from "./chunk-RK35RIN7.js";
38
32
  import {
39
33
  chordFromDegree,
40
34
  chordPitchClasses,
@@ -45,20 +39,189 @@ import {
45
39
  makeChord,
46
40
  minorKey,
47
41
  scaleByName,
48
- scaleTonesInDegreeOrder
49
- } from "./chunk-MIWO425M.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";
50
61
 
51
- // src/model/shared.ts
52
- function mod12(n) {
53
- return (n % 12 + 12) % 12;
54
- }
55
- function spellPitchClassBare(pc, spelling) {
56
- const spelled = midiToNote(60 + mod12(pc), spelling);
57
- 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)}`;
58
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
+ };
59
217
 
60
218
  // src/model/note.ts
61
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
+ }
62
225
  const copy = { letter: data.letter, alter: data.alter };
63
226
  if (data.octave !== void 0) {
64
227
  copy.octave = data.octave;
@@ -71,6 +234,7 @@ var Note = class _Note {
71
234
  * Wrap a plain note object.
72
235
  *
73
236
  * @param data The spelled note; it is copied, never retained or mutated.
237
+ * @throws If `letter` is not an integer in 0..6.
74
238
  */
75
239
  constructor(data) {
76
240
  this.#data = copyNote(data);
@@ -100,6 +264,7 @@ var Note = class _Note {
100
264
  *
101
265
  * @param data The plain note.
102
266
  * @returns The wrapped note.
267
+ * @throws If `letter` is not an integer in 0..6.
103
268
  */
104
269
  static fromData(data) {
105
270
  return new _Note(data);
@@ -119,7 +284,7 @@ var Note = class _Note {
119
284
  */
120
285
  get midi() {
121
286
  if (this.#data.octave === void 0) {
122
- 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`);
123
288
  }
124
289
  return noteToMidi(this.#data);
125
290
  }
@@ -140,33 +305,67 @@ var Note = class _Note {
140
305
  return copyNote(this.#data);
141
306
  }
142
307
  /**
143
- * Transpose by a signed number of semitones.
308
+ * Transpose by a signed number of semitones, keeping the spelling.
144
309
  *
145
- * When the note carries an octave the transposition happens in MIDI space and
146
- * 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
147
313
  * octave-less: only its pitch class is moved. Transposing by zero is the
148
314
  * identity: the original spelling is preserved (no enharmonic respelling).
149
315
  *
150
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.
151
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
+ * ```
152
326
  */
153
- transpose(semitones) {
154
- if (semitones === 0) {
327
+ transpose(semitones, opts) {
328
+ if (semitones === 0 && opts?.spelling === void 0) {
155
329
  return new _Note(this.#data);
156
330
  }
157
- if (this.#data.octave !== void 0) {
158
- return _Note.fromMidi(this.midi + semitones);
159
- }
160
- return new _Note(spellPitchClassBare(this.pitchClass + semitones, "sharp"));
331
+ return new _Note(transposeNote(this.#data, semitones, opts));
161
332
  }
162
333
  /**
163
334
  * The spelled interval from this note to another.
164
335
  *
165
336
  * @param other The second note.
166
- * @returns The diatonic number, quality, and semitone span.
337
+ * @returns The interval, which {@link Interval.toJSON} unwraps to plain data.
167
338
  */
168
339
  intervalTo(other) {
169
- 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);
170
369
  }
171
370
  /**
172
371
  * Whether another note has the same letter, alteration, and octave.
@@ -175,7 +374,8 @@ var Note = class _Note {
175
374
  * @returns True if the spellings are identical.
176
375
  */
177
376
  equals(other) {
178
- 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;
179
379
  }
180
380
  /**
181
381
  * The plain note data, for JSON serialization.
@@ -188,737 +388,1062 @@ var Note = class _Note {
188
388
  toJSON() {
189
389
  return this.data;
190
390
  }
191
- };
192
-
193
- // src/model/progression.ts
194
- var Progression = class _Progression {
195
- #chords;
196
- #key;
197
391
  /**
198
- * Wrap a chord sequence.
392
+ * The note's name, so a template literal or a log line reads as the note.
199
393
  *
200
- * @param chords The chords in order; the array is copied.
201
- * @param key Optional key context for analysis methods.
394
+ * @returns The spelled name, e.g. `'Bb3'`.
202
395
  */
203
- constructor(chords, key) {
204
- this.#chords = [...chords];
205
- this.#key = key;
206
- }
207
- /** A copy of the chord sequence. */
208
- get chords() {
209
- return [...this.#chords];
396
+ toString() {
397
+ return this.name;
210
398
  }
211
- /** The carried key context, if any. */
212
- get key() {
213
- 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);
214
416
  }
215
- /** The number of chords. */
216
- get length() {
217
- 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;
218
437
  }
219
438
  /**
220
- * A copy of this progression with a chord appended.
439
+ * A major key.
221
440
  *
222
- * @param chord The chord to append.
223
- * @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.
224
445
  */
225
- add(chord) {
226
- 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));
227
453
  }
228
454
  /**
229
- * A copy of this progression carrying the given key context.
455
+ * A natural-minor key.
230
456
  *
231
- * @param key The key context to attach.
232
- * @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.
233
461
  */
234
- withKey(key) {
235
- 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));
236
469
  }
237
470
  /**
238
- * Voice the progression with smooth voice leading.
471
+ * A key on a named scale (e.g. `'dorian'`, `'harmonicMinor'`).
239
472
  *
240
- * @param opts Voicing options; defaults to four SATB voices.
241
- * @returns One ascending voicing (MIDI pitches) per chord.
242
- * @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.
243
479
  */
244
- voice(opts) {
245
- return voiceProgression(
246
- this.#chords.map((chord) => chord.data),
247
- opts
248
- );
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));
249
487
  }
250
488
  /**
251
- * The Roman numeral of each chord in a key.
489
+ * Wrap an existing `KeyScale`, synthesizing a spelled tonic when none is
490
+ * given.
252
491
  *
253
- * @param key Key to analyze in; falls back to the carried context.
254
- * @returns One numeral per chord.
255
- * @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.
256
501
  */
257
- roman(key) {
258
- const resolved = this.#resolveKey(key);
259
- return this.#chords.map((chord) => chord.roman(resolved));
502
+ static of(scale, tonic) {
503
+ return new _Key(scale, tonic ?? bestTonicForScale(pitchClassOf(scale.rootPc), scale));
260
504
  }
261
505
  /**
262
- * The harmonic function of each chord in a key.
506
+ * Rebuild a key from its {@link Key.toJSON} output.
263
507
  *
264
- * @param key Key to analyze in; falls back to the carried context.
265
- * @returns One function per chord.
266
- * @throws If no key is given and none is carried.
508
+ * @param data The serialized key and tonic.
509
+ * @returns The key.
267
510
  */
268
- functions(key) {
269
- const resolved = this.#resolveKey(key);
270
- return this.#chords.map((chord) => chord.function(resolved));
511
+ static fromJSON(data) {
512
+ return new _Key(data.scale, new Note(data.tonic));
271
513
  }
272
514
  /**
273
- * Analyze every chord and classify the closing cadence.
515
+ * Identify the keys a set of pitches fits, best interpretation first.
274
516
  *
275
- * The cadence is detected on the final chord pair and is null when the
276
- * progression has fewer than two chords.
517
+ * The counterpart of {@link Chord.detect}.
277
518
  *
278
- * @param key Key to analyze in; falls back to the carried context.
279
- * @returns Per-chord analyses and the closing cadence.
280
- * @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).
281
522
  */
282
- analyze(key) {
283
- const resolved = this.#resolveKey(key);
284
- const chords = this.#chords.map((chord) => chord.analyze(resolved));
285
- const from = this.#chords[this.#chords.length - 2];
286
- const to = this.#chords[this.#chords.length - 1];
287
- const cadence = from !== void 0 && to !== void 0 ? detectCadence(from.data, to.data, resolved.scale) : null;
288
- return { chords, cadence };
523
+ static detect(pitches, opts) {
524
+ return detectKey(pitches, opts).map((match) => _Key.of(match.key));
289
525
  }
290
526
  /**
291
- * The plain progression data, for JSON serialization.
527
+ * The single best key interpretation of a pitch set.
292
528
  *
293
- * Private class fields do not serialize, so an explicit `toJSON` keeps
294
- * `JSON.stringify(progression)` from collapsing to `{}`. The chords are
295
- * 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.
296
544
  *
297
- * @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.
298
550
  */
299
- toJSON() {
300
- return {
301
- chords: this.#chords.map((chord) => chord.data),
302
- key: this.#key?.toJSON()
303
- };
551
+ equals(other) {
552
+ const scale = other.scale;
553
+ return this.#scale.rootPc === scale.rootPc && this.#scale.modeMask12 === scale.modeMask12;
304
554
  }
305
- /** Resolve the key for an analysis method: explicit first, then carried. */
306
- #resolveKey(key) {
307
- const resolved = key ?? this.#key;
308
- if (resolved === void 0) {
309
- throw new Error("progression has no key context; pass a Key or attach one with withKey()");
310
- }
311
- return resolved;
555
+ /** A copy of the underlying plain `KeyScale`. */
556
+ get scale() {
557
+ return { rootPc: this.#scale.rootPc, modeMask12: this.#scale.modeMask12 };
312
558
  }
313
- };
314
-
315
- // src/model/chord.ts
316
- function copyChord(data) {
317
- const copy = {
318
- rootPc: data.rootPc,
319
- quality: data.quality,
320
- intervals: [...data.intervals]
321
- };
322
- if (data.bassPc !== void 0) {
323
- copy.bassPc = data.bassPc;
559
+ /** The spelled tonic. */
560
+ get tonic() {
561
+ return this.#tonic;
324
562
  }
325
- if (data.rootSpelling !== void 0) {
326
- 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;
327
566
  }
328
- if (data.bassSpelling !== void 0) {
329
- 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);
330
570
  }
331
- return copy;
332
- }
333
- var Chord = class _Chord {
334
- #data;
335
- #key;
336
571
  /**
337
- * Wrap a plain chord object.
572
+ * The scale's pitch classes in ascending scale-degree order (degree 0 first).
338
573
  *
339
- * @param data The chord; it is copied, never retained or mutated.
340
- * @param key Optional key context for analysis methods.
574
+ * @returns One pitch class per scale degree.
341
575
  */
342
- constructor(data, key) {
343
- const copy = copyChord(data);
344
- if (copy.rootSpelling === void 0 && key !== void 0) {
345
- copy.rootSpelling = spellPitchClass(copy.rootPc, key.tonic.data, key.scale);
346
- }
347
- this.#data = copy;
348
- this.#key = key;
576
+ pitchClasses() {
577
+ return scaleTonesInDegreeOrder(this.#scale);
349
578
  }
350
579
  /**
351
- * Build a chord from a root and quality.
580
+ * Transpose the key by a number of semitones.
352
581
  *
353
- * @param root Root as a note name (e.g. `'Eb'`) or a pitch class.
354
- * @param quality The chord quality.
355
- * @param bass Optional slash-chord bass pitch class.
356
- * @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
+ * ```
357
593
  */
358
- static of(root, quality, bass) {
359
- if (typeof root === "string") {
360
- const rootNote = Note.of(root);
361
- const data = makeChord(rootNote.pitchClass, quality, bass);
362
- data.rootSpelling = { letter: rootNote.letter, alter: rootNote.alter };
363
- return new _Chord(data);
364
- }
365
- return new _Chord(makeChord(root, 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 });
366
598
  }
367
599
  /**
368
- * 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).
369
601
  *
370
- * @param data The plain chord.
371
- * @returns The wrapped chord (without key context).
602
+ * @returns Spelled octave-less notes in scale-degree order.
372
603
  */
373
- static from(data) {
374
- return new _Chord(data);
604
+ notes() {
605
+ return spellScale(this.#tonic.data, this.#scale).map((note) => new Note(note));
375
606
  }
376
607
  /**
377
- * Parse a lead-sheet chord symbol (e.g. `'Cmaj7'`, `'F#m7b5'`, `'C/G'`).
608
+ * Alias of {@link Key.notes}.
378
609
  *
379
- * @param symbol The chord symbol.
380
- * @returns The chord (without key context).
381
- * @throws If the root or quality is not recognized.
610
+ * @returns Spelled octave-less notes in scale-degree order.
382
611
  */
383
- static parse(symbol) {
384
- return new _Chord(parseChordSymbol(symbol));
612
+ spell() {
613
+ return this.notes();
385
614
  }
386
615
  /**
387
- * Identify the chords matching a set of pitches, best interpretation first.
616
+ * The spelled scale as letter-name strings.
388
617
  *
389
- * @param pitches MIDI pitches or bare pitch classes.
390
- * @returns Ranked chord interpretations (may be empty).
618
+ * @returns One name per scale degree.
391
619
  */
392
- static detect(pitches, opts) {
393
- return detectChord(pitches, opts).map(
394
- (match) => new _Chord(makeChord(match.rootPc, match.quality, match.bassPc))
395
- );
620
+ noteNames() {
621
+ return this.notes().map((note) => note.name);
396
622
  }
397
623
  /**
398
- * The single best chord interpretation of a pitch set.
624
+ * Build a chord on a scale degree, carrying this key as context.
399
625
  *
400
- * @param pitches MIDI pitches or bare pitch classes.
401
- * @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.
402
637
  */
403
- static detectBest(pitches, opts) {
404
- const best = detectChordBest(pitches, opts);
405
- return best === null ? null : new _Chord(best);
406
- }
407
- /** The root pitch class (0..11). */
408
- get rootPc() {
409
- return this.#data.rootPc;
410
- }
411
- /** The chord quality. */
412
- get quality() {
413
- return this.#data.quality;
414
- }
415
- /** A copy of the semitone offsets above the root. */
416
- get intervals() {
417
- return [...this.#data.intervals];
418
- }
419
- /** The slash-chord bass pitch class, or undefined in root position. */
420
- get bassPc() {
421
- return this.#data.bassPc;
422
- }
423
- /** A copy of the underlying plain chord object. */
424
- get data() {
425
- return copyChord(this.#data);
426
- }
427
- /** The carried key context, if any. */
428
- get key() {
429
- 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);
430
641
  }
431
642
  /**
432
- * A copy of this chord carrying the given key context.
643
+ * The diatonic triad on a scale degree, carrying this key as context.
433
644
  *
434
- * @param key The key context to attach.
435
- * @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.
436
649
  */
437
- withKey(key) {
438
- return new _Chord(this.#data, key);
650
+ diatonicTriad(degree) {
651
+ return new Chord(diatonicTriad(degree, this.#scale), this);
439
652
  }
440
653
  /**
441
- * The chord's sorted, deduplicated pitch classes.
654
+ * The diatonic seventh chord on a scale degree, carrying this key as context.
442
655
  *
443
- * @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.
444
660
  */
445
- pitchClasses() {
446
- return chordPitchClasses(this.#data);
661
+ diatonicSeventh(degree) {
662
+ return new Chord(diatonicSeventh(degree, this.#scale), this);
447
663
  }
448
664
  /**
449
- * 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.
450
667
  *
451
- * @param key Key to analyze in; falls back to the carried context.
452
- * @returns The Roman numeral string.
453
- * @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.
454
671
  */
455
- roman(key) {
456
- return chordToRoman(this.#data, this.#resolveKey(key).scale);
672
+ roman(text) {
673
+ return new Chord(romanToChord(text, this.#scale), this);
457
674
  }
458
675
  /**
459
- * The chord's harmonic function (tonic / subdominant / dominant) in a key.
676
+ * Whether a pitch belongs to the scale.
460
677
  *
461
- * @param key Key to analyze in; falls back to the carried context.
462
- * @returns The harmonic function.
463
- * @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.
464
680
  */
465
- function(key) {
466
- return functionOf(this.#data, this.#resolveKey(key).scale);
681
+ contains(x) {
682
+ return isScaleTone(typeof x === "number" ? x : x.pitchClass, this.#scale);
467
683
  }
468
684
  /**
469
- * Full functional analysis: function, borrowing, and Roman numeral.
685
+ * The plain key data, for JSON serialization.
470
686
  *
471
- * @param key Key to analyze in; falls back to the carried context.
472
- * @returns The chord analysis.
473
- * @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.
474
693
  */
475
- analyze(key) {
476
- return analyzeChord(this.#data, this.#resolveKey(key).scale);
694
+ toJSON() {
695
+ return { scale: this.scale, tonic: this.#tonic.data };
477
696
  }
478
697
  /**
479
- * 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.
480
701
  *
481
- * @param key Key to analyze in; falls back to the carried context.
482
- * @returns True if the chord is borrowed.
483
- * @throws If no key is given and none is carried.
702
+ * @returns The name, e.g. `'C major'` or `'A minor'`.
484
703
  */
485
- isBorrowed(key) {
486
- return isBorrowedChord(this.#data, this.#resolveKey(key).scale);
704
+ toString() {
705
+ return `${this.#tonic.name} ${this.isMinor ? "minor" : "major"}`;
487
706
  }
707
+ };
708
+
709
+ // src/model/progression.ts
710
+ var Progression = class _Progression {
711
+ #chords;
712
+ #key;
488
713
  /**
489
- * The origin of a non-diatonic chord (parallel mode or Neapolitan), or null.
714
+ * Wrap a chord sequence.
490
715
  *
491
- * @param key Key to analyze in; falls back to the carried context.
492
- * @returns The borrowing source, or null.
493
- * @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.
494
718
  */
495
- borrowedSource(key) {
496
- return borrowedSource(this.#data, this.#resolveKey(key).scale);
719
+ constructor(chords, key) {
720
+ this.#chords = Object.freeze([...chords]);
721
+ this.#key = key;
497
722
  }
498
723
  /**
499
- * 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.
500
726
  *
501
- * @param opts Set `flats: true` to spell the root/bass with flats.
502
- * @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
+ * ```
503
741
  */
504
- symbol(opts) {
505
- 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);
506
748
  }
507
749
  /**
508
- * Realize the chord as one MIDI pitch per voice, ascending.
750
+ * Rebuild a progression from its {@link Progression.toJSON} output.
509
751
  *
510
- * @param opts Voicing options; defaults to four SATB voices.
511
- * @returns MIDI pitches, ascending, one per voice.
512
- * @throws If no voicing fits the given ranges.
752
+ * @param data The serialized chords and key.
753
+ * @returns The progression.
513
754
  */
514
- voice(opts) {
515
- 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);
516
759
  }
517
760
  /**
518
- * Realize the chord as a single styled voicing (`close`, `drop2`, `drop3`,
519
- * `shell`, or `rootless`), optionally constraining the top voice.
761
+ * The chord sequence.
520
762
  *
521
- * @param opts Styled-voicing options; defaults to a close-position voicing.
522
- * @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.
523
765
  */
524
- styledVoicing(opts) {
525
- return voiceChordStyled(this.#data, opts);
766
+ get chords() {
767
+ return this.#chords;
526
768
  }
527
- /**
528
- * The negative-harmony mirror of the chord about the key's tonic–dominant
529
- * axis (major becomes minor and vice versa).
530
- *
531
- * @param key Key providing the reflection axis; falls back to the carried
532
- * context.
533
- * @returns The mirrored chord, keeping any key context.
534
- * @throws If no key is given and none is carried.
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;
776
+ }
777
+ /**
778
+ * The chord at an index.
779
+ *
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.
535
782
  */
536
- negativeHarmony(key) {
537
- const resolved = this.#resolveKey(key);
538
- 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]();
539
789
  }
540
790
  /**
541
- * The n-th inversion: a copy whose bass is the chord tone `n` steps above the
542
- * root in the interval template (`invert(1)` puts the third in the bass).
543
- * `n` wraps around the template length; negative values count backwards.
791
+ * Whether another progression holds the same chords in the same order.
544
792
  *
545
- * `invert(0)` (and any `n` that wraps to it) is root position, so it carries
546
- * 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}.
547
795
  *
548
- * @param n The inversion number.
549
- * @returns The inverted chord, keeping any key context.
550
- * @throws If the chord has no intervals.
796
+ * @param other The progression to compare.
797
+ * @returns True when the chord sequences match.
551
798
  */
552
- invert(n) {
553
- const intervals = this.#data.intervals;
554
- const length = intervals.length;
555
- if (length === 0) {
556
- throw new Error("cannot invert a chord with no intervals");
557
- }
558
- const index = (n % length + length) % length;
559
- const data = copyChord(this.#data);
560
- if (index === 0) {
561
- delete data.bassPc;
562
- delete data.bassSpelling;
563
- } else {
564
- data.bassPc = mod12(this.#data.rootPc + (intervals[index] ?? 0));
565
- const rootSpelling = data.rootSpelling ?? (this.#key ? spellPitchClass(data.rootPc, this.#key.tonic.data, this.#key.scale) : spellPitchClassBare(data.rootPc, "sharp"));
566
- const bassSpelling = spellChordFromRoot(data, rootSpelling)[index];
567
- if (bassSpelling !== void 0) {
568
- data.bassSpelling = bassSpelling;
569
- }
799
+ equals(other) {
800
+ if (this.#chords.length !== other.length) {
801
+ return false;
570
802
  }
571
- 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
+ });
572
807
  }
573
808
  /**
574
- * The named scales that fit over this chord, best fit first, rooted on the
575
- * chord root.
809
+ * A copy of this progression with a chord appended.
576
810
  *
577
- * @returns The matching scales.
811
+ * @param chord The chord to append.
812
+ * @returns The new progression.
578
813
  */
579
- scales() {
580
- return chordScales(this.#data);
814
+ add(chord) {
815
+ return new _Progression([...this.#chords, chord], this.#key);
581
816
  }
582
817
  /**
583
- * The available tensions (usable non-chord, non-avoid scale tones) of a
584
- * scale over this chord.
818
+ * A copy of this progression carrying the given key context.
585
819
  *
586
- * @param scaleName A named scale, rooted on the chord root.
587
- * @returns Tension pitch classes, ascending in [0, 11].
820
+ * @param key The key context to attach.
821
+ * @returns The new progression.
588
822
  */
589
- tensions(scaleName) {
590
- return availableTensions(this.#data, scaleName);
823
+ withKey(key) {
824
+ return new _Progression(this.#chords, key);
591
825
  }
592
826
  /**
593
- * The avoid notes (scale tones a semitone above a chord tone) of a scale
594
- * over this chord.
827
+ * Voice the progression with smooth voice leading.
595
828
  *
596
- * @param scaleName A named scale, rooted on the chord root.
597
- * @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.
598
832
  */
599
- avoidNotes(scaleName) {
600
- return avoidNotes(this.#data, scaleName);
833
+ voice(opts) {
834
+ return voiceProgression(
835
+ this.#chords.map((chord) => chord.data),
836
+ opts
837
+ );
601
838
  }
602
839
  /**
603
- * Spell the chord tones with letter names, root first, in the key's spelling.
840
+ * The Roman numeral of each chord in a key.
604
841
  *
605
- * @param key Key providing the spelled tonic; falls back to the carried
606
- * context.
607
- * @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.
608
844
  * @throws If no key is given and none is carried.
609
845
  */
610
- spell(key) {
846
+ roman(key) {
611
847
  const resolved = this.#resolveKey(key);
612
- return spellChord(this.#data, resolved.tonic.data, resolved.scale).map(
613
- (note) => new Note(note)
614
- );
848
+ return this.#chords.map((chord) => chord.roman(resolved));
615
849
  }
616
850
  /**
617
- * Start a progression with this chord followed by others, carrying this
618
- * chord's key context (if any).
851
+ * The harmonic function of each chord in a key.
619
852
  *
620
- * @param others The chords following this one.
621
- * @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.
622
856
  */
623
- progressionTo(...others) {
624
- 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));
625
860
  }
626
861
  /**
627
- * Whether another chord has the same root, quality, intervals, and bass.
628
- * Key context is not compared.
862
+ * Analyze every chord and classify the closing cadence.
629
863
  *
630
- * @param other The chord to compare.
631
- * @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.
632
870
  */
633
- equals(other) {
634
- const a = this.#data;
635
- const b = other.#data;
636
- 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 };
637
878
  }
638
879
  /**
639
- * The plain chord data, for JSON serialization.
880
+ * The plain progression data, for JSON serialization.
640
881
  *
641
- * @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
+ * ```
642
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'`.
915
+ */
916
+ toString() {
917
+ return this.#chords.map((chord) => chord.symbol()).join(" ");
918
+ }
643
919
  toJSON() {
644
- return this.data;
920
+ return {
921
+ chords: this.#chords.map((chord) => chord.data),
922
+ key: this.#key?.toJSON()
923
+ };
645
924
  }
646
925
  /** Resolve the key for an analysis method: explicit first, then carried. */
647
926
  #resolveKey(key) {
648
927
  const resolved = key ?? this.#key;
649
928
  if (resolved === void 0) {
650
- 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
+ );
651
932
  }
652
933
  return resolved;
653
934
  }
654
935
  };
655
936
 
656
- // src/model/interval.ts
657
- var Interval = class _Interval {
658
- #number;
659
- #quality;
660
- #semitones;
661
- constructor(numberValue, quality, semitones) {
662
- this.#number = numberValue;
663
- this.#quality = quality;
664
- 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;
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;
981
+ /**
982
+ * Wrap a plain chord object.
983
+ *
984
+ * @param data The chord; it is copied, never retained or mutated.
985
+ * @param key Optional key context for analysis methods.
986
+ */
987
+ constructor(data, key) {
988
+ this.#given = copyChord(data);
989
+ this.#key = key;
665
990
  }
666
991
  /**
667
- * The spelled interval between two notes.
992
+ * The chord data with any missing spelling hint filled in from the key.
668
993
  *
669
- * @param a The first note.
670
- * @param b The second note.
671
- * @returns The interval from `a` to `b`.
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.
672
997
  */
673
- static between(a, b) {
674
- const spelled = a.intervalTo(b);
675
- return new _Interval(spelled.number, spelled.quality, spelled.semitones);
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;
676
1011
  }
677
1012
  /**
678
- * Build an interval from explicit components.
1013
+ * Build a chord from a root and quality.
679
1014
  *
680
- * @param numberValue Diatonic size: 1 = unison, 2 = second, ... 8 = octave.
681
- * @param quality Quality label: `'P'`, `'M'`, `'m'`, or repeated `'A'`/`'d'`.
682
- * @param semitones Signed semitone span.
683
- * @returns The interval.
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).
684
1020
  */
685
- static of(numberValue, quality, semitones) {
686
- return new _Interval(numberValue, quality, semitones);
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);
687
1037
  }
688
- /** Diatonic size: 1 = unison, 2 = second, ... 8 = octave, and beyond. */
689
- get number() {
690
- return this.#number;
1038
+ /**
1039
+ * Wrap an existing plain chord object.
1040
+ *
1041
+ * @param data The plain chord.
1042
+ * @returns The wrapped chord (without key context).
1043
+ */
1044
+ static from(data) {
1045
+ return new _Chord(data);
691
1046
  }
692
- /** Quality label: `'P'`, `'M'`, `'m'`, or repeated `'A'`/`'d'`. */
1047
+ /**
1048
+ * Alias of {@link Chord.from}, matching the `fromData` factory on the other
1049
+ * classes.
1050
+ *
1051
+ * @param data The plain chord.
1052
+ * @returns The wrapped chord (without key context).
1053
+ */
1054
+ static fromData(data) {
1055
+ return new _Chord(data);
1056
+ }
1057
+ /**
1058
+ * Rebuild a chord from its {@link Chord.toJSON} output.
1059
+ *
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.
1065
+ */
1066
+ static fromJSON(data) {
1067
+ return new _Chord(data);
1068
+ }
1069
+ /**
1070
+ * Parse a lead-sheet chord symbol (e.g. `'Cmaj7'`, `'F#m7b5'`, `'C/G'`).
1071
+ *
1072
+ * @param symbol The chord symbol.
1073
+ * @returns The chord (without key context).
1074
+ * @throws If the root or quality is not recognized.
1075
+ */
1076
+ static parse(symbol) {
1077
+ return new _Chord(parseChordSymbol(symbol));
1078
+ }
1079
+ /**
1080
+ * Identify the chords matching a set of pitches, best interpretation first.
1081
+ *
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).
1085
+ */
1086
+ static detect(pitches, opts) {
1087
+ return detectChord(pitches, opts).map(
1088
+ (match) => new _Chord(makeChord(match.rootPc, match.quality, match.bassPc))
1089
+ );
1090
+ }
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
+ }));
1114
+ }
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);
1125
+ }
1126
+ /** The root pitch class (0..11). */
1127
+ get rootPc() {
1128
+ return this.#data.rootPc;
1129
+ }
1130
+ /** The chord quality. */
693
1131
  get quality() {
694
- return this.#quality;
1132
+ return this.#data.quality;
695
1133
  }
696
- /** Signed semitone span. */
697
- get semitones() {
698
- return this.#semitones;
1134
+ /** A copy of the semitone offsets above the root. */
1135
+ get intervals() {
1136
+ return [...this.#data.intervals];
699
1137
  }
700
- /** A readable label composed of quality and number, e.g. `'P5'` or `'M3'`. */
701
- get name() {
702
- return `${this.#quality}${this.#number}`;
1138
+ /** The slash-chord bass pitch class, or undefined in root position. */
1139
+ get bassPc() {
1140
+ return this.#data.bassPc;
1141
+ }
1142
+ /**
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.
1155
+ *
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].
1172
+ */
1173
+ pitchClasses() {
1174
+ return chordPitchClasses(this.#data);
1175
+ }
1176
+ /**
1177
+ * The chord's Roman numeral in a key.
1178
+ *
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.
1182
+ */
1183
+ roman(key) {
1184
+ return chordToRoman(this.#data, this.#resolveKey(key).scale);
1185
+ }
1186
+ /**
1187
+ * The chord's harmonic function (tonic / subdominant / dominant) in a key.
1188
+ *
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.
1192
+ */
1193
+ function(key) {
1194
+ return functionOf(this.#data, this.#resolveKey(key).scale);
1195
+ }
1196
+ /**
1197
+ * Full functional analysis: function, borrowing, and Roman numeral.
1198
+ *
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.
1202
+ */
1203
+ analyze(key) {
1204
+ return analyzeChord(this.#data, this.#resolveKey(key).scale);
703
1205
  }
704
1206
  /**
705
- * The plain interval data, for JSON serialization.
706
- *
707
- * Private class fields do not serialize, so an explicit `toJSON` keeps
708
- * `JSON.stringify(interval)` from collapsing to `{}`.
1207
+ * Whether the chord is borrowed from the parallel mode (modal interchange).
709
1208
  *
710
- * @returns The diatonic number, quality, and semitone span.
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.
711
1212
  */
712
- toJSON() {
713
- return { number: this.#number, quality: this.#quality, semitones: this.#semitones };
714
- }
715
- };
716
-
717
- // src/model/key.ts
718
- function resolveTonic(root, spelling) {
719
- return typeof root === "string" ? Note.of(root) : new Note(spellPitchClassBare(root, spelling));
720
- }
721
- function accidentalLoad(tonic, scale) {
722
- return spellScale(tonic, scale).reduce((sum, note) => sum + Math.abs(note.alter), 0);
723
- }
724
- function bestTonicForScale(rootPc, scale) {
725
- const sharp = spellPitchClassBare(rootPc, "sharp");
726
- const flat = spellPitchClassBare(rootPc, "flat");
727
- if (sharp.letter === flat.letter && sharp.alter === flat.alter) {
728
- return new Note(sharp);
1213
+ isBorrowed(key) {
1214
+ return isBorrowedChord(this.#data, this.#resolveKey(key).scale);
729
1215
  }
730
- return accidentalLoad(flat, scale) <= accidentalLoad(sharp, scale) ? new Note(flat) : new Note(sharp);
731
- }
732
- var Key = class _Key {
733
- #scale;
734
- #tonic;
735
1216
  /**
736
- * Wrap a key/scale and its spelled tonic.
1217
+ * The origin of a non-diatonic chord (parallel mode or Neapolitan), or null.
737
1218
  *
738
- * @param scale The key/scale; its root is normalized to a pitch class.
739
- * @param tonic The spelled tonic anchoring letter-name spelling.
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.
740
1222
  */
741
- constructor(scale, tonic) {
742
- this.#scale = { rootPc: mod12(scale.rootPc), modeMask12: scale.modeMask12 };
743
- this.#tonic = tonic;
1223
+ borrowedSource(key) {
1224
+ return borrowedSource(this.#data, this.#resolveKey(key).scale);
744
1225
  }
745
1226
  /**
746
- * A major key.
1227
+ * The chord rendered as a lead-sheet symbol (e.g. `'Cmaj7'`, `'F#m7'`, `'C/G'`).
747
1228
  *
748
- * @param root Tonic as a note name (e.g. `'Eb'`) or a pitch class; a numeric
749
- * root is spelled with whichever accidental side yields the fewest
750
- * accidentals across the scale.
751
- * @returns The major key.
1229
+ * @param opts Set `flats: true` to spell the root/bass with flats.
1230
+ * @returns The chord symbol.
752
1231
  */
753
- static major(root) {
754
- if (typeof root === "string") {
755
- const tonic = Note.of(root);
756
- return new _Key(majorKey(tonic.pitchClass), tonic);
757
- }
758
- const scale = majorKey(root);
759
- return new _Key(scale, bestTonicForScale(root, scale));
1232
+ symbol(opts) {
1233
+ return formatChordSymbol(this.#data, opts);
760
1234
  }
761
1235
  /**
762
- * A natural-minor key.
1236
+ * Realize the chord as one MIDI pitch per voice, ascending.
763
1237
  *
764
- * @param root Tonic as a note name or a pitch class; a numeric root is
765
- * spelled with whichever accidental side yields the fewest accidentals
766
- * across the scale.
767
- * @returns The minor key.
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.
768
1244
  */
769
- static minor(root) {
770
- if (typeof root === "string") {
771
- const tonic = Note.of(root);
772
- return new _Key(minorKey(tonic.pitchClass), tonic);
773
- }
774
- const scale = minorKey(root);
775
- return new _Key(scale, bestTonicForScale(root, scale));
1245
+ voice(opts) {
1246
+ const key = opts?.key ?? this.#key?.scale;
1247
+ return voiceChord(this.#data, key === void 0 ? opts : { ...opts, key });
776
1248
  }
777
1249
  /**
778
- * A key on a named scale (e.g. `'dorian'`, `'harmonicMinor'`).
1250
+ * Realize the chord as a single styled voicing (`close`, `drop2`, `drop3`,
1251
+ * `shell`, or `rootless`), optionally constraining the top voice.
779
1252
  *
780
- * @param name The scale name, a key of the scale module's named-scale table.
781
- * @param root Tonic as a note name or a pitch class.
782
- * @returns The key.
783
- * @throws If the name is not a known scale.
1253
+ * @param opts Styled-voicing options; defaults to a close-position voicing.
1254
+ * @returns MIDI pitches, ascending.
784
1255
  */
785
- static named(name, root) {
786
- const tonic = resolveTonic(root, "sharp");
787
- return new _Key(scaleByName(name, tonic.pitchClass), tonic);
1256
+ styledVoicing(opts) {
1257
+ return voiceChordStyled(this.#data, opts);
788
1258
  }
789
1259
  /**
790
- * Wrap an existing `KeyScale`, synthesizing a spelled tonic when none is
791
- * given (flat-preferred for minor-third scales, sharp-preferred otherwise).
1260
+ * The negative-harmony mirror of the chord about the key's tonic–dominant
1261
+ * axis (major becomes minor and vice versa).
792
1262
  *
793
- * @param scale The key/scale to wrap.
794
- * @param tonic Optional spelled tonic.
795
- * @returns The key.
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.
796
1267
  */
797
- static of(scale, tonic) {
798
- const spelled = tonic ?? new Note(spellPitchClassBare(scale.rootPc, isMinorKey(scale) ? "flat" : "sharp"));
799
- return new _Key(scale, spelled);
800
- }
801
- /** A copy of the underlying plain `KeyScale`. */
802
- get scale() {
803
- return { rootPc: this.#scale.rootPc, modeMask12: this.#scale.modeMask12 };
804
- }
805
- /** The spelled tonic. */
806
- get tonic() {
807
- return this.#tonic;
808
- }
809
- /** The tonic pitch class (0..11). */
810
- get rootPc() {
811
- return this.#scale.rootPc;
812
- }
813
- /** Whether the scale has a minor third and no major third. */
814
- get isMinor() {
815
- return isMinorKey(this.#scale);
1268
+ negativeHarmony(key) {
1269
+ const resolved = this.#resolveKey(key);
1270
+ return new _Chord(negativeHarmonyMirror(this.#data, resolved.scale), key ?? this.#key);
816
1271
  }
817
1272
  /**
818
- * The scale's pitch classes in ascending scale-degree order (degree 0 first).
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.
819
1276
  *
820
- * @returns One pitch class per scale degree.
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.
1279
+ *
1280
+ * @param n The inversion number.
1281
+ * @returns The inverted chord, keeping any key context.
1282
+ * @throws If the chord has no intervals.
821
1283
  */
822
- pitchClasses() {
823
- return scaleTonesInDegreeOrder(this.#scale);
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);
824
1299
  }
825
1300
  /**
826
- * The spelled scale, one note per degree (e.g. C D E F G A B for C major).
1301
+ * Transpose the chord by a number of semitones.
827
1302
  *
828
- * @returns Spelled octave-less notes in scale-degree order.
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
+ * ```
829
1315
  */
830
- notes() {
831
- return spellScale(this.#tonic.data, this.#scale).map((note) => new Note(note));
1316
+ transpose(semitones) {
1317
+ const moved = transposeChord(this.#given, semitones);
1318
+ return new _Chord(moved, this.#key?.transpose(semitones));
832
1319
  }
833
1320
  /**
834
- * Alias of {@link Key.notes}.
1321
+ * The named scales that fit over this chord, best fit first, rooted on the
1322
+ * chord root.
835
1323
  *
836
- * @returns Spelled octave-less notes in scale-degree order.
1324
+ * @returns The matching scales.
837
1325
  */
838
- spell() {
839
- return this.notes();
1326
+ scales() {
1327
+ return chordScales(this.#data);
840
1328
  }
841
1329
  /**
842
- * The spelled scale as letter-name strings.
1330
+ * The available tensions (usable non-chord, non-avoid scale tones) of a
1331
+ * scale over this chord.
843
1332
  *
844
- * @returns One name per scale degree.
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.
845
1336
  */
846
- noteNames() {
847
- return this.notes().map((note) => note.name);
1337
+ tensions(scaleName) {
1338
+ return availableTensions(this.#data, scaleName);
848
1339
  }
849
1340
  /**
850
- * Build a chord on a scale degree, carrying this key as context.
851
- *
852
- * With an explicit quality the quality's interval template is attached to the
853
- * degree's diatonic root; without one the scale-correct diatonic triad is
854
- * stacked (e.g. a diminished triad on the leading tone of a major key).
1341
+ * The avoid notes (scale tones a semitone above a chord tone) of a scale
1342
+ * over this chord.
855
1343
  *
856
- * @param degree 0-based scale degree of the chord root.
857
- * @param quality Optional chord quality.
858
- * @returns The chord, with this key attached.
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.
859
1347
  */
860
- chord(degree, quality) {
861
- const data = quality === void 0 ? diatonicTriad(degree, this.#scale) : chordFromDegree(degree, quality, this.#scale);
862
- return new Chord(data, this);
1348
+ avoidNotes(scaleName) {
1349
+ return avoidNotes(this.#data, scaleName);
863
1350
  }
864
1351
  /**
865
- * The diatonic triad on a scale degree, carrying this key as context.
1352
+ * Spell the chord tones with letter names, root first, in the key's spelling.
866
1353
  *
867
- * @param degree 0-based scale degree of the chord root.
868
- * @returns The triad, with this key attached.
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.
869
1369
  */
870
- diatonicTriad(degree) {
871
- return new Chord(diatonicTriad(degree, this.#scale), this);
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));
872
1391
  }
873
1392
  /**
874
- * The diatonic seventh chord on a scale degree, carrying this key as context.
1393
+ * Start a progression with this chord followed by others, carrying this
1394
+ * chord's key context (if any).
875
1395
  *
876
- * @param degree 0-based scale degree of the chord root.
877
- * @returns The seventh chord, with this key attached.
1396
+ * @param others The chords following this one.
1397
+ * @returns The progression.
878
1398
  */
879
- diatonicSeventh(degree) {
880
- return new Chord(diatonicSeventh(degree, this.#scale), this);
1399
+ progressionTo(...others) {
1400
+ return new Progression([this, ...others], this.#key);
881
1401
  }
882
1402
  /**
883
- * Build the chord denoted by a Roman numeral in this key (including applied
884
- * chords such as `'V7/V'`), carrying this key as context.
1403
+ * Whether another chord has the same root, quality, intervals, and bass.
1404
+ * Key context is not compared.
885
1405
  *
886
- * @param text The Roman numeral.
887
- * @returns The chord, with this key attached.
888
- * @throws If the numeral is not valid.
1406
+ * @param other The chord to compare.
1407
+ * @returns True if the chord data is identical.
889
1408
  */
890
- roman(text) {
891
- return new Chord(romanToChord(text, this.#scale), this);
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]);
892
1413
  }
893
1414
  /**
894
- * Whether a pitch belongs to the scale.
1415
+ * The plain chord data, for JSON serialization.
895
1416
  *
896
- * @param x A MIDI pitch, bare pitch class, or note.
897
- * @returns True if the pitch class is a scale tone.
1417
+ * @returns A copy of the underlying plain chord object.
898
1418
  */
899
- contains(x) {
900
- return isScaleTone(typeof x === "number" ? x : x.pitchClass, this.#scale);
1419
+ toJSON() {
1420
+ return this.data;
901
1421
  }
902
1422
  /**
903
- * The plain key data, for JSON serialization.
904
- *
905
- * Private class fields do not serialize, so an explicit `toJSON` keeps
906
- * `JSON.stringify(key)` from collapsing to `{}`. The result pairs the
907
- * `KeyScale` with the spelled tonic, enough to reconstruct the key via
908
- * {@link Key.of}.
1423
+ * The chord symbol, so a template literal or a log line reads as the chord.
909
1424
  *
910
- * @returns The key/scale and its spelled tonic.
1425
+ * @returns The symbol, e.g. `'Cmaj7'`.
911
1426
  */
912
- toJSON() {
913
- return { scale: this.scale, tonic: this.#tonic.data };
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;
914
1439
  }
915
1440
  };
916
1441
 
917
1442
  export {
1443
+ Interval,
918
1444
  Note,
1445
+ Key,
919
1446
  Progression,
920
- Chord,
921
- Interval,
922
- Key
1447
+ Chord
923
1448
  };
924
- //# sourceMappingURL=chunk-SGU7SPLK.js.map
1449
+ //# sourceMappingURL=chunk-EHVEBWM5.js.map