@libraz/libcantus 1.0.1 → 1.2.0

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 (142) hide show
  1. package/README.md +27 -65
  2. package/README_ja.md +28 -66
  3. package/dist/analyze/index.cjs +11 -7
  4. package/dist/analyze/index.cjs.map +1 -1
  5. package/dist/analyze/index.d.cts +79 -144
  6. package/dist/analyze/index.d.ts +79 -144
  7. package/dist/analyze/index.js +20 -16
  8. package/dist/chunk-35NBV2FT.cjs +145 -0
  9. package/dist/chunk-35NBV2FT.cjs.map +1 -0
  10. package/dist/{chunk-UADYAYAT.cjs → chunk-3R3QVKY5.cjs} +1353 -580
  11. package/dist/chunk-3R3QVKY5.cjs.map +1 -0
  12. package/dist/{chunk-WWURE4XT.cjs → chunk-76ZBTRG2.cjs} +624 -729
  13. package/dist/chunk-76ZBTRG2.cjs.map +1 -0
  14. package/dist/{chunk-FVSI36UV.js → chunk-7KG3RTYS.js} +197 -110
  15. package/dist/chunk-7KG3RTYS.js.map +1 -0
  16. package/dist/{chunk-VTI6FOLB.js → chunk-AGOVW7U6.js} +709 -133
  17. package/dist/chunk-AGOVW7U6.js.map +1 -0
  18. package/dist/{chunk-E3F4AHAH.cjs → chunk-CGBADIO5.cjs} +221 -134
  19. package/dist/chunk-CGBADIO5.cjs.map +1 -0
  20. package/dist/{chunk-6XH6QZYQ.cjs → chunk-DSMJRXCZ.cjs} +1455 -849
  21. package/dist/chunk-DSMJRXCZ.cjs.map +1 -0
  22. package/dist/{chunk-PKFNUHDY.js → chunk-FBH75DAY.js} +1218 -612
  23. package/dist/chunk-FBH75DAY.js.map +1 -0
  24. package/dist/chunk-H2KJHBW4.js +3329 -0
  25. package/dist/chunk-H2KJHBW4.js.map +1 -0
  26. package/dist/chunk-HPVSH7SF.cjs +2294 -0
  27. package/dist/chunk-HPVSH7SF.cjs.map +1 -0
  28. package/dist/{chunk-MXO72PLY.cjs → chunk-HWHGOVO6.cjs} +21 -19
  29. package/dist/chunk-HWHGOVO6.cjs.map +1 -0
  30. package/dist/chunk-HYJJ2V2I.js +2294 -0
  31. package/dist/chunk-HYJJ2V2I.js.map +1 -0
  32. package/dist/{chunk-AFM2ULJH.cjs → chunk-J7FHVZII.cjs} +1567 -1067
  33. package/dist/chunk-J7FHVZII.cjs.map +1 -0
  34. package/dist/chunk-K6OZXIR6.js +1 -0
  35. package/dist/{chunk-H3VORETI.js → chunk-M5RYMAFI.js} +487 -592
  36. package/dist/chunk-M5RYMAFI.js.map +1 -0
  37. package/dist/chunk-NB25P3EH.js +363 -0
  38. package/dist/chunk-NB25P3EH.js.map +1 -0
  39. package/dist/{chunk-TTCLJX6D.js → chunk-NFJ4P6ZZ.js} +10 -8
  40. package/dist/chunk-NFJ4P6ZZ.js.map +1 -0
  41. package/dist/chunk-NM7UAMC3.cjs +457 -0
  42. package/dist/chunk-NM7UAMC3.cjs.map +1 -0
  43. package/dist/chunk-PM4XJE2Y.cjs +363 -0
  44. package/dist/chunk-PM4XJE2Y.cjs.map +1 -0
  45. package/dist/chunk-PUKJP2Z5.js +145 -0
  46. package/dist/chunk-PUKJP2Z5.js.map +1 -0
  47. package/dist/{chunk-VOHZB45S.js → chunk-Q6LFSAKV.js} +1321 -821
  48. package/dist/chunk-Q6LFSAKV.js.map +1 -0
  49. package/dist/{chunk-INHVXP6Z.js → chunk-RFIBF2RR.js} +1203 -430
  50. package/dist/chunk-RFIBF2RR.js.map +1 -0
  51. package/dist/{chunk-Q2WGKJNN.cjs → chunk-S5BKZZGM.cjs} +699 -123
  52. package/dist/chunk-S5BKZZGM.cjs.map +1 -0
  53. package/dist/chunk-SGLEQVJH.cjs +3329 -0
  54. package/dist/chunk-SGLEQVJH.cjs.map +1 -0
  55. package/dist/chunk-UGLFDG7L.cjs +1 -0
  56. package/dist/chunk-UGLFDG7L.cjs.map +1 -0
  57. package/dist/{chunk-YH3BIKHH.js → chunk-UMWZ4ZET.js} +146 -49
  58. package/dist/chunk-UMWZ4ZET.js.map +1 -0
  59. package/dist/{index-szBv3496.d.ts → coerce-DP0vYu-x.d.ts} +286 -199
  60. package/dist/{index-C_8o--VE.d.cts → coerce-DRERzdve.d.cts} +286 -199
  61. package/dist/core/index.cjs +9 -5
  62. package/dist/core/index.cjs.map +1 -1
  63. package/dist/core/index.d.cts +122 -108
  64. package/dist/core/index.d.ts +122 -108
  65. package/dist/core/index.js +32 -28
  66. package/dist/generate/index.cjs +8 -8
  67. package/dist/generate/index.d.cts +168 -84
  68. package/dist/generate/index.d.ts +168 -84
  69. package/dist/generate/index.js +7 -7
  70. package/dist/{index-CGpRv5sl.d.cts → index-9eoQs2C1.d.ts} +34 -19
  71. package/dist/{index-CCNJRG-P.d.cts → index-B9QLziDe.d.cts} +293 -56
  72. package/dist/{index-Dzr13q3d.d.cts → index-BBWNZS0f.d.cts} +135 -58
  73. package/dist/{index-9wcMhG73.d.ts → index-BHIUyjhZ.d.ts} +293 -56
  74. package/dist/{index-BGFW-8P2.d.cts → index-BJgP97N1.d.ts} +196 -114
  75. package/dist/{index-CunYKLiu.d.ts → index-BL5N2Oyz.d.cts} +1 -1
  76. package/dist/{index-Cj49F892.d.ts → index-BMweUlm5.d.cts} +63 -23
  77. package/dist/{index-CgHqxdps.d.ts → index-BNSM-DSk.d.cts} +196 -114
  78. package/dist/{index-D57QRbVn.d.ts → index-BRMbPkHE.d.ts} +480 -254
  79. package/dist/{index-Chbst3Jx.d.ts → index-CJBBiAmU.d.ts} +135 -58
  80. package/dist/{index-BkGmlyI3.d.cts → index-CL4b4gUQ.d.ts} +63 -23
  81. package/dist/{index-B9MNZboQ.d.ts → index-Cy15oTvh.d.cts} +52 -22
  82. package/dist/{index-B9MNZboQ.d.cts → index-Cy15oTvh.d.ts} +52 -22
  83. package/dist/{index-BO6xE9YU.d.cts → index-DGCNKAUr.d.cts} +480 -254
  84. package/dist/{index-B4o55NDr.d.cts → index-Dw2DP7vm.d.ts} +1 -1
  85. package/dist/{index-G1uwGGWQ.d.ts → index-cHvbYgJI.d.cts} +34 -19
  86. package/dist/index.cjs +42 -14
  87. package/dist/index.cjs.map +1 -1
  88. package/dist/index.d.cts +14 -14
  89. package/dist/index.d.ts +14 -14
  90. package/dist/index.js +82 -54
  91. package/dist/{masks-DYbE485J.d.cts → masks-Cc7-MzzY.d.ts} +49 -2
  92. package/dist/{masks-DYbE485J.d.ts → masks-pADfTOiD.d.cts} +49 -2
  93. package/dist/model/index.cjs +19 -12
  94. package/dist/model/index.cjs.map +1 -1
  95. package/dist/model/index.d.cts +504 -182
  96. package/dist/model/index.d.ts +504 -182
  97. package/dist/model/index.js +18 -11
  98. package/dist/{positional-Cptlj-xs.d.ts → positional-DJgs6ltD.d.ts} +85 -8
  99. package/dist/{positional-DvH_sd8u.d.cts → positional-DMZer-FM.d.cts} +85 -8
  100. package/dist/theory/index.cjs +35 -6
  101. package/dist/theory/index.cjs.map +1 -1
  102. package/dist/theory/index.d.cts +257 -62
  103. package/dist/theory/index.d.ts +257 -62
  104. package/dist/theory/index.js +41 -12
  105. package/package.json +7 -5
  106. package/dist/chunk-2LCPM3EG.js +0 -3224
  107. package/dist/chunk-2LCPM3EG.js.map +0 -1
  108. package/dist/chunk-6XH6QZYQ.cjs.map +0 -1
  109. package/dist/chunk-6XPTBE7Y.cjs +0 -49
  110. package/dist/chunk-6XPTBE7Y.cjs.map +0 -1
  111. package/dist/chunk-AFM2ULJH.cjs.map +0 -1
  112. package/dist/chunk-B6XNKWZV.cjs +0 -3224
  113. package/dist/chunk-B6XNKWZV.cjs.map +0 -1
  114. package/dist/chunk-E3F4AHAH.cjs.map +0 -1
  115. package/dist/chunk-ELCTZUJJ.js +0 -379
  116. package/dist/chunk-ELCTZUJJ.js.map +0 -1
  117. package/dist/chunk-FVSI36UV.js.map +0 -1
  118. package/dist/chunk-H3VORETI.js.map +0 -1
  119. package/dist/chunk-H42UHPYW.js +0 -1577
  120. package/dist/chunk-H42UHPYW.js.map +0 -1
  121. package/dist/chunk-HFPS7JFD.js +0 -1
  122. package/dist/chunk-INHVXP6Z.js.map +0 -1
  123. package/dist/chunk-K4ZJUGF4.cjs +0 -379
  124. package/dist/chunk-K4ZJUGF4.cjs.map +0 -1
  125. package/dist/chunk-KNJEKLKV.js +0 -49
  126. package/dist/chunk-KNJEKLKV.js.map +0 -1
  127. package/dist/chunk-MXO72PLY.cjs.map +0 -1
  128. package/dist/chunk-NLEU4QSE.cjs +0 -360
  129. package/dist/chunk-NLEU4QSE.cjs.map +0 -1
  130. package/dist/chunk-OE6WK2Y2.cjs +0 -1577
  131. package/dist/chunk-OE6WK2Y2.cjs.map +0 -1
  132. package/dist/chunk-PKFNUHDY.js.map +0 -1
  133. package/dist/chunk-Q2WGKJNN.cjs.map +0 -1
  134. package/dist/chunk-RHIJ3S4I.cjs +0 -1
  135. package/dist/chunk-RHIJ3S4I.cjs.map +0 -1
  136. package/dist/chunk-TTCLJX6D.js.map +0 -1
  137. package/dist/chunk-UADYAYAT.cjs.map +0 -1
  138. package/dist/chunk-VOHZB45S.js.map +0 -1
  139. package/dist/chunk-VTI6FOLB.js.map +0 -1
  140. package/dist/chunk-WWURE4XT.cjs.map +0 -1
  141. package/dist/chunk-YH3BIKHH.js.map +0 -1
  142. /package/dist/{chunk-HFPS7JFD.js.map → chunk-K6OZXIR6.js.map} +0 -0
@@ -1,26 +1,20 @@
1
1
  import {
2
2
  analyzeArrangement,
3
- analyzeVoice,
4
- chordTimelineFromChords,
5
- chordTimelineFromNotes,
3
+ analyzePolyphony,
6
4
  createArrangementSession,
7
- detectCadences,
8
5
  extractMotifs,
9
6
  hypermeter,
10
- keyLookup,
11
- keyTimelineFromNotes,
12
7
  melodicContour,
13
8
  melodicSimilarity,
14
9
  motifFromNotes,
15
10
  phrasesFromTimeline,
16
- prevailingKeyOf,
17
11
  reduceProgression,
18
12
  relateMotifs,
19
13
  sectionsFromNotes,
20
14
  structuralCadences,
21
15
  tensionCurveFrom,
22
- toVoiceNotes
23
- } from "./chunk-2LCPM3EG.js";
16
+ trackRoleOf
17
+ } from "./chunk-HYJJ2V2I.js";
24
18
  import {
25
19
  TWELVE_TET,
26
20
  beatsToDuration,
@@ -42,11 +36,13 @@ import {
42
36
  stepOf,
43
37
  stepsOfCents,
44
38
  tempoAt,
39
+ tempoReader,
45
40
  ticksToBeats
46
- } from "./chunk-YH3BIKHH.js";
41
+ } from "./chunk-UMWZ4ZET.js";
47
42
  import {
48
43
  STEP_BEATS,
49
44
  applyGrooveTemplate,
45
+ cellSpan,
50
46
  deform,
51
47
  developMotif,
52
48
  doubleTime,
@@ -73,67 +69,58 @@ import {
73
69
  thin,
74
70
  transformMotif,
75
71
  withinCeiling
76
- } from "./chunk-VOHZB45S.js";
72
+ } from "./chunk-Q6LFSAKV.js";
77
73
  import {
78
74
  analyzeChord,
75
+ augmentedSixthChord,
79
76
  borrowedSource,
77
+ chordAtBeat,
78
+ chordTimelineFromChords,
79
+ chordTimelineFromNotes,
80
80
  chordToRoman,
81
81
  detectCadence,
82
+ detectCadences,
82
83
  detectChord,
83
84
  detectChordBest,
84
85
  detectKey,
85
86
  detectKeyBest,
86
87
  detectKeyFromNotes,
88
+ detectModulations,
87
89
  explainRoman,
88
90
  functionOf,
89
91
  isBorrowedChord,
90
92
  isMinorKey,
93
+ keyLookup,
94
+ keyTimelineFromNotes,
95
+ pivotChords,
96
+ prevailingKeyOf,
91
97
  romanToChord,
92
- secondaryDominantOf
93
- } from "./chunk-H42UHPYW.js";
98
+ secondaryDominantOf,
99
+ toVoiceNotes
100
+ } from "./chunk-H2KJHBW4.js";
94
101
  import {
95
102
  BASS_4_STRING,
96
103
  BASS_5_STRING,
97
104
  GUITAR_DROP_D,
98
105
  GUITAR_STANDARD,
99
- canSound,
100
- fingeringsFor,
101
- foldIntoRange,
102
- instrumentRange,
106
+ fingeringsIn,
107
+ foldIn,
103
108
  playability,
109
+ rangeIn,
110
+ soundsIn,
104
111
  toInstrumentProfile,
105
112
  toStringedProfile
106
- } from "./chunk-FVSI36UV.js";
113
+ } from "./chunk-7KG3RTYS.js";
107
114
  import {
108
- beatToBarPosition,
109
- beatsPerBar,
110
- createNoteEventIndex,
111
- formatBarPosition,
112
- formatTimeSignature,
113
- isCompound,
114
- isStrongBeat,
115
- meterAt,
116
- metricWeight,
117
- parseTimeSignature,
118
- pulseBeats,
119
- pulsesPerBar,
120
- resolveMeters,
121
- toMeterData,
122
- tryParseTimeSignature,
123
- tuplet
124
- } from "./chunk-ELCTZUJJ.js";
115
+ createNoteEventIndex
116
+ } from "./chunk-PUKJP2Z5.js";
125
117
  import {
126
118
  SATB_RANGES,
127
- availableTensions,
128
- avoidNotes,
129
119
  checkPartWriting,
130
120
  checkSpecies,
131
- chordScaleReport,
132
- chordScales,
133
121
  figuredBassOf,
134
122
  nextVoicing,
135
123
  realizeFiguredBass,
136
- scalesForChanges,
137
124
  spellVoicing,
138
125
  toSoundingPitch,
139
126
  toWrittenPitch,
@@ -141,9 +128,18 @@ import {
141
128
  voiceChordStyled,
142
129
  voiceLeadingCost,
143
130
  voiceProgression
144
- } from "./chunk-H3VORETI.js";
131
+ } from "./chunk-M5RYMAFI.js";
132
+ import {
133
+ availableTensions,
134
+ avoidNotes,
135
+ chordScaleReport,
136
+ chordScales,
137
+ scalesForChanges
138
+ } from "./chunk-NB25P3EH.js";
145
139
  import {
146
140
  NAMED_SCALES,
141
+ assertChordQuality,
142
+ assertKeyVariant,
147
143
  chordFromDegree,
148
144
  chordFromSpan,
149
145
  chordPitchClasses,
@@ -160,7 +156,7 @@ import {
160
156
  formatChordSymbol,
161
157
  isChordMember,
162
158
  isScaleTone,
163
- isSignatureKey,
159
+ isWrittenTonic,
164
160
  keyFromFifths,
165
161
  keyRelationBetween,
166
162
  keySignatureFifths,
@@ -172,7 +168,10 @@ import {
172
168
  pitchToScaleDegree,
173
169
  relatedKeysOf,
174
170
  relativeKeyOf,
171
+ resolveKey,
175
172
  scaleByName,
173
+ scaleNameOfMask,
174
+ scaleOf,
176
175
  scaleSystemOf,
177
176
  scaleTonesInDegreeOrder,
178
177
  spanFromChord,
@@ -182,43 +181,61 @@ import {
182
181
  spellPitchClass,
183
182
  spellPitchClasses,
184
183
  spellScale,
185
- spelledKeyOf,
186
184
  subdominantKeyOf,
187
185
  supportsFunctionalHarmony,
188
186
  toChordData,
189
187
  toKeyScale,
190
188
  transposeChord,
189
+ transposeChordSpellings,
191
190
  tryParseChordSymbol,
191
+ tryResolveKeyName,
192
+ variantOfMask,
192
193
  voiceIndependence
193
- } from "./chunk-INHVXP6Z.js";
194
+ } from "./chunk-RFIBF2RR.js";
194
195
  import {
195
196
  InvalidInputError,
197
+ MAX_NAME_ACCIDENTALS,
196
198
  assertFiniteNumber,
197
199
  assertInteger,
198
200
  assertMidiPitch,
199
- assertNoteEvent,
201
+ assertNoteEvents,
200
202
  assertOneOf,
201
203
  assertPositiveInt,
202
204
  assertRange,
203
205
  assertTimeSignature,
206
+ beatToBarPosition,
207
+ beatsPerBar,
208
+ describeRejected,
209
+ formatBarPosition,
204
210
  formatKeyName,
205
211
  formatNote,
212
+ formatTimeSignature,
206
213
  intervalSemitones,
214
+ isCompound,
207
215
  isConsonantInterval,
216
+ isStrongBeat,
217
+ meterAt,
218
+ metricWeight,
208
219
  midiToNote,
209
220
  noteToMidi,
210
221
  noteToPitchClass,
222
+ parseTimeSignature,
211
223
  pitchClassOf,
224
+ pulseBeats,
225
+ pulsesPerBar,
226
+ resolveMeters,
212
227
  spelledInterval,
228
+ toMeterData,
213
229
  toNoteData,
214
230
  toSpelledInterval,
215
231
  transposeByInterval,
216
232
  transposeNote,
217
233
  tryParseInterval,
218
- tryParseKeyName,
219
234
  tryParseNote,
235
+ tryParseTimeSignature,
236
+ tuplet,
220
237
  unwrapParse
221
- } from "./chunk-VTI6FOLB.js";
238
+ } from "./chunk-AGOVW7U6.js";
222
239
 
223
240
  // src/model/interval.ts
224
241
  var INVERSION_SUM = 9;
@@ -288,12 +305,7 @@ var Interval = class _Interval {
288
305
  */
289
306
  static fromData(data) {
290
307
  const checked = toSpelledInterval(data);
291
- return new _Interval(
292
- checked.number,
293
- checked.quality,
294
- checked.semitones,
295
- checked.descending ?? false
296
- );
308
+ return new _Interval(checked.number, checked.quality, checked.semitones, checked.descending);
297
309
  }
298
310
  /**
299
311
  * Rebuild an interval from its {@link Interval.toJSON} output.
@@ -357,9 +369,16 @@ var Interval = class _Interval {
357
369
  get isDescending() {
358
370
  return this.#descending;
359
371
  }
360
- /** A readable label composed of quality and number, e.g. `'P5'` or `'M3'`. */
372
+ /**
373
+ * A readable label composed of quality and number, e.g. `'P5'` or `'M3'`,
374
+ * prefixed with `'-'` when the interval descends.
375
+ *
376
+ * The name is written in the grammar {@link Interval.parse} reads, so an
377
+ * interval that has been logged, written to a column, or interpolated into a
378
+ * template comes back as the same interval — direction included.
379
+ */
361
380
  get name() {
362
- return `${this.#quality}${this.#number}`;
381
+ return `${this.#descending ? "-" : ""}${this.#quality}${this.#number}`;
363
382
  }
364
383
  /** A copy of the underlying plain interval data. */
365
384
  get data() {
@@ -397,7 +416,7 @@ var Interval = class _Interval {
397
416
  * @example
398
417
  * ```ts
399
418
  * import { Interval } from '@libraz/libcantus';
400
- * Interval.parse('M3').negate().toString(); // 'M3', descending
419
+ * Interval.parse('M3').negate().toString(); // '-M3'
401
420
  * ```
402
421
  */
403
422
  negate() {
@@ -435,34 +454,168 @@ var Interval = class _Interval {
435
454
  *
436
455
  * Private class fields do not serialize, so an explicit `toJSON` keeps
437
456
  * `JSON.stringify(interval)` from collapsing to `{}`. The result is the
438
- * canonical shape the pitch module produces: `descending` appears only on an
439
- * interval that descends, so a measured interval, a parsed name, and this
440
- * method all serialize to the same object.
457
+ * canonical shape the pitch module produces: `descending` is always there, so
458
+ * a measured interval, a parsed name, and this method all serialize to the
459
+ * same object and none of them leaves the direction to be guessed.
441
460
  *
442
- * @returns The diatonic number, quality, and semitone span, with
443
- * `descending` when the interval descends.
461
+ * @returns The diatonic number, quality, and semitone span, with `descending`
462
+ * telling which way the interval goes.
444
463
  */
445
464
  toJSON() {
446
- return this.#descending ? {
465
+ return {
447
466
  number: this.#number,
448
467
  quality: this.#quality,
449
468
  semitones: this.#semitones,
450
- descending: true
451
- } : { number: this.#number, quality: this.#quality, semitones: this.#semitones };
469
+ descending: this.#descending
470
+ };
452
471
  }
453
472
  /**
454
473
  * The interval's name, so a template literal or a log line reads as the
455
474
  * interval.
456
475
  *
457
- * @returns The name, e.g. `'M3'`.
476
+ * @returns The name, e.g. `'M3'` or `'-M3'` for the same third taken
477
+ * downward.
458
478
  */
459
479
  toString() {
460
480
  return this.name;
461
481
  }
462
482
  };
463
483
 
484
+ // src/model/shared.ts
485
+ var STATED_KEY_CONFIDENCE = 1;
486
+ function describeShape(value) {
487
+ if (value === null) {
488
+ return "null";
489
+ }
490
+ return Array.isArray(value) ? "an array" : typeof value;
491
+ }
492
+ function assertDataObject(value, name) {
493
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
494
+ throw new InvalidInputError(`${name} must be an object; received ${describeShape(value)}`);
495
+ }
496
+ return value;
497
+ }
498
+ function assertDataArray(value, name) {
499
+ if (!Array.isArray(value)) {
500
+ throw new InvalidInputError(`${name} must be an array; received ${describeShape(value)}`);
501
+ }
502
+ return value;
503
+ }
504
+ function assertDataObjects(value, name) {
505
+ const items = assertDataArray(value, name);
506
+ for (let index = 0; index < items.length; index += 1) {
507
+ assertDataObject(items[index], `${name}[${index}]`);
508
+ }
509
+ return items;
510
+ }
511
+ function assertNoteEventArray(notes, name, options) {
512
+ return assertNoteEvents(assertDataArray(notes, name), name, options);
513
+ }
514
+ function assertKeyArgument(value, name) {
515
+ if (value === void 0 || typeof value === "string" || isKeyShaped(value)) {
516
+ return;
517
+ }
518
+ const received = value === null ? "null" : typeof value === "object" ? "an object" : `a ${typeof value}`;
519
+ throw new InvalidInputError(
520
+ `${name} must be a Key; received ${received}. A method that also takes options takes them after the key, not in its place`
521
+ );
522
+ }
523
+ function isKeyShaped(value) {
524
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
525
+ return false;
526
+ }
527
+ return "scale" in value || "toJSON" in value || "rootPc" in value && "modeMask12" in value;
528
+ }
529
+ function withoutNegativeZero(value) {
530
+ return value === 0 ? 0 : value;
531
+ }
532
+ function spanEnd(...runs) {
533
+ let end = 0;
534
+ for (const run of runs) {
535
+ for (const span of run) {
536
+ end = Math.max(end, span.endBeat);
537
+ }
538
+ }
539
+ return end;
540
+ }
541
+ function copyNoteEvent(note) {
542
+ const copy = {
543
+ pitch: withoutNegativeZero(note.pitch),
544
+ startBeat: withoutNegativeZero(note.startBeat),
545
+ durationBeat: withoutNegativeZero(note.durationBeat)
546
+ };
547
+ if (note.velocity !== void 0) {
548
+ copy.velocity = withoutNegativeZero(note.velocity);
549
+ }
550
+ if (note.articulation !== void 0) {
551
+ copy.articulation = note.articulation;
552
+ }
553
+ return copy;
554
+ }
555
+ function copyTimeSignature(ts, name) {
556
+ assertDataObject(ts, name ?? "time signature");
557
+ assertTimeSignature(ts, name);
558
+ const copy = { numerator: ts.numerator, denominator: ts.denominator };
559
+ if (ts.grouping !== void 0) {
560
+ copy.grouping = [...ts.grouping];
561
+ }
562
+ return copy;
563
+ }
564
+ var MAX_PLAIN_DEPTH = 64;
565
+ function copyPlain(value, name) {
566
+ return copyPlainAt(value, name, /* @__PURE__ */ new WeakSet(), 0);
567
+ }
568
+ function copyPlainAt(value, name, seen, depth) {
569
+ if (typeof value === "number") {
570
+ return assertFiniteNumber(value, name);
571
+ }
572
+ if (typeof value === "function" || typeof value === "symbol" || typeof value === "bigint") {
573
+ throw new InvalidInputError(`${name} must be plain data; received ${typeof value}`);
574
+ }
575
+ if (typeof value !== "object" || value === null) {
576
+ return value;
577
+ }
578
+ if (seen.has(value)) {
579
+ throw new InvalidInputError(`${name} must be plain data; received a value referring to itself`);
580
+ }
581
+ if (depth >= MAX_PLAIN_DEPTH) {
582
+ throw new InvalidInputError(
583
+ `${name} must be plain data; received a value nested more than ${MAX_PLAIN_DEPTH} levels deep`
584
+ );
585
+ }
586
+ seen.add(value);
587
+ try {
588
+ if (Array.isArray(value)) {
589
+ return value.map(
590
+ (item, index) => copyPlainAt(item, `${name}[${index}]`, seen, depth + 1)
591
+ );
592
+ }
593
+ const copy = {};
594
+ for (const [key, item] of Object.entries(value)) {
595
+ if (item !== void 0) {
596
+ copy[key] = copyPlainAt(item, `${name}.${key}`, seen, depth + 1);
597
+ }
598
+ }
599
+ return copy;
600
+ } finally {
601
+ seen.delete(value);
602
+ }
603
+ }
604
+ function samePlain(value, other) {
605
+ if (Array.isArray(value) || Array.isArray(other)) {
606
+ return Array.isArray(value) && Array.isArray(other) && value.length === other.length && value.every((item, index) => samePlain(item, other[index]));
607
+ }
608
+ if (typeof value === "object" && value !== null && typeof other === "object" && other !== null) {
609
+ const mine = Object.entries(value);
610
+ const theirs = other;
611
+ return mine.length === Object.keys(theirs).length && mine.every(([key, item]) => key in theirs && samePlain(item, theirs[key]));
612
+ }
613
+ return value === other;
614
+ }
615
+
464
616
  // src/model/note.ts
465
617
  function copyNote(data) {
618
+ assertDataObject(data, "note data");
466
619
  if (!Number.isInteger(data.letter) || data.letter < 0 || data.letter > 6) {
467
620
  throw new InvalidInputError(
468
621
  `note letter must be an integer in [0, 6]; received ${data.letter}`
@@ -482,13 +635,17 @@ function letterNumberOf(letter) {
482
635
  }
483
636
  if (!NATURAL_LETTER.test(letter)) {
484
637
  throw new InvalidInputError(
485
- `Note.of takes a letter number 0..6 or a bare letter name 'C'..'B'; received ${JSON.stringify(letter)}. Use Note.parse(${JSON.stringify(letter)}) to read a note name with an accidental or an octave.`
638
+ `Note.of takes a letter number 0..6 or a bare letter name 'C'..'B'; received ${describeRejected(letter)}. Use Note.parse(${describeRejected(letter)}) to read a note name with an accidental or an octave.`
486
639
  );
487
640
  }
488
641
  return unwrapParse(tryParseNote(letter)).letter;
489
642
  }
490
- var DOUBLE_ACCIDENTAL = 2;
491
- var DIMINISHED_SECOND = { number: 2, quality: "d", semitones: 0 };
643
+ var DIMINISHED_SECOND = {
644
+ number: 2,
645
+ quality: "d",
646
+ semitones: 0,
647
+ descending: false
648
+ };
492
649
  var DESCENDING_DIMINISHED_SECOND = {
493
650
  ...DIMINISHED_SECOND,
494
651
  descending: true
@@ -809,7 +966,7 @@ var Note = class _Note {
809
966
  * ```
810
967
  */
811
968
  enharmonic() {
812
- return [DIMINISHED_SECOND, DESCENDING_DIMINISHED_SECOND].map((interval) => transposeByInterval(this.#data, interval)).filter((note) => Math.abs(note.alter) <= DOUBLE_ACCIDENTAL).map((note) => new _Note(note));
969
+ return [DIMINISHED_SECOND, DESCENDING_DIMINISHED_SECOND].map((interval) => transposeByInterval(this.#data, interval)).filter((note) => Math.abs(note.alter) <= MAX_NAME_ACCIDENTALS).map((note) => new _Note(note));
813
970
  }
814
971
  /**
815
972
  * The scale degree this note occupies in a key, counted from 1 at the tonic.
@@ -830,7 +987,7 @@ var Note = class _Note {
830
987
  * ```
831
988
  */
832
989
  degreeIn(key) {
833
- const degree = pitchToScaleDegree(this.pitchClass, toKeyScale(key));
990
+ const degree = pitchToScaleDegree(this.pitchClass, key);
834
991
  return degree === -1 ? null : degree;
835
992
  }
836
993
  /**
@@ -907,65 +1064,9 @@ var Note = class _Note {
907
1064
  }
908
1065
  };
909
1066
 
910
- // src/model/shared.ts
911
- function assertKeyArgument(value, name) {
912
- if (value === void 0 || typeof value === "object" && value !== null && "scale" in value) {
913
- return;
914
- }
915
- const received = typeof value === "object" && value !== null ? "an object" : `a ${typeof value}`;
916
- throw new InvalidInputError(
917
- `${name} must be a Key; received ${received}. A method that also takes options takes them after the key, not in its place`
918
- );
919
- }
920
- function withoutNegativeZero(value) {
921
- return value === 0 ? 0 : value;
922
- }
923
- function spanEnd(...runs) {
924
- let end = 0;
925
- for (const run of runs) {
926
- for (const span of run) {
927
- end = Math.max(end, span.endBeat);
928
- }
929
- }
930
- return end;
931
- }
932
- function copyNoteEvent(note) {
933
- const copy = {
934
- pitch: withoutNegativeZero(note.pitch),
935
- startBeat: withoutNegativeZero(note.startBeat),
936
- durationBeat: withoutNegativeZero(note.durationBeat)
937
- };
938
- if (note.velocity !== void 0) {
939
- copy.velocity = withoutNegativeZero(note.velocity);
940
- }
941
- if (note.articulation !== void 0) {
942
- copy.articulation = note.articulation;
943
- }
944
- return copy;
945
- }
946
- function copyTimeSignature(ts, name) {
947
- assertTimeSignature(ts, name);
948
- const copy = { numerator: ts.numerator, denominator: ts.denominator };
949
- if (ts.grouping !== void 0) {
950
- copy.grouping = [...ts.grouping];
951
- }
952
- return copy;
953
- }
954
- function samePlain(value, other) {
955
- if (Array.isArray(value) || Array.isArray(other)) {
956
- return Array.isArray(value) && Array.isArray(other) && value.length === other.length && value.every((item, index) => samePlain(item, other[index]));
957
- }
958
- if (typeof value === "object" && value !== null && typeof other === "object" && other !== null) {
959
- const mine = Object.entries(value);
960
- const theirs = other;
961
- return mine.length === Object.keys(theirs).length && mine.every(([key, item]) => key in theirs && samePlain(item, theirs[key]));
962
- }
963
- return value === other;
964
- }
965
-
966
1067
  // src/model/timeline.ts
967
- var GIVEN_KEY_CONFIDENCE = 1;
968
1068
  function copySegment(segment) {
1069
+ assertDataObject(segment, "timeline segment");
969
1070
  return {
970
1071
  startBeat: assertFiniteNumber(segment.startBeat, "timeline segment startBeat"),
971
1072
  endBeat: assertFiniteNumber(segment.endBeat, "timeline segment endBeat"),
@@ -976,16 +1077,20 @@ function copySegment(segment) {
976
1077
  };
977
1078
  }
978
1079
  function copyKeyRegion(region) {
1080
+ assertDataObject(region, "key region");
979
1081
  const copy = {
980
1082
  startBeat: assertFiniteNumber(region.startBeat, "key region startBeat"),
981
1083
  endBeat: assertFiniteNumber(region.endBeat, "key region endBeat"),
982
- key: toKeyScale(region.key),
1084
+ // Read through the key itself, so a tonic that does not spell the region's
1085
+ // own root is refused here rather than spelling later notes wrongly.
1086
+ key: keyIdentity(toKey(region.key)),
983
1087
  confidence: assertFiniteNumber(region.confidence, "key region confidence")
984
1088
  };
985
1089
  if (region.modulation !== void 0) {
986
1090
  copy.modulation = region.modulation;
987
1091
  }
988
1092
  if (region.pivot !== void 0) {
1093
+ assertDataObject(region.pivot, "key region pivot");
989
1094
  copy.pivot = {
990
1095
  chord: new Chord(region.pivot.chord).toJSON(),
991
1096
  romanFrom: region.pivot.romanFrom,
@@ -998,6 +1103,7 @@ function copyConfidence(confidence, segments) {
998
1103
  if (confidence === void 0) {
999
1104
  return [];
1000
1105
  }
1106
+ assertDataArray(confidence, "timeline segmentConfidence");
1001
1107
  if (confidence.length !== segments) {
1002
1108
  throw new InvalidInputError(
1003
1109
  `timeline segmentConfidence must hold one value per segment; received ${confidence.length} for ${segments}`
@@ -1007,21 +1113,6 @@ function copyConfidence(confidence, segments) {
1007
1113
  (value, index) => assertRange(value, 0, 1, `timeline segmentConfidence[${index}]`)
1008
1114
  );
1009
1115
  }
1010
- function chordAtBeat(segments, beat) {
1011
- assertFiniteNumber(beat, "timeline query beat");
1012
- let low = 0;
1013
- let high = segments.length;
1014
- while (low < high) {
1015
- const middle = Math.floor((low + high) / 2);
1016
- if ((segments[middle]?.startBeat ?? Number.POSITIVE_INFINITY) <= beat) {
1017
- low = middle + 1;
1018
- } else {
1019
- high = middle;
1020
- }
1021
- }
1022
- const candidate = segments[low - 1];
1023
- return candidate !== void 0 && beat >= candidate.startBeat && beat < candidate.endBeat ? candidate.chord : null;
1024
- }
1025
1116
  function clipSpan(span, fromBeat, toBeat) {
1026
1117
  const startBeat = Math.max(span.startBeat, fromBeat);
1027
1118
  const endBeat = Math.min(span.endBeat, toBeat);
@@ -1041,14 +1132,19 @@ var Timeline = class _Timeline {
1041
1132
  * the confidences do not run one per segment.
1042
1133
  */
1043
1134
  constructor(data) {
1044
- this.#segments = Object.freeze(data.segments.map(copySegment));
1135
+ assertDataObject(data, "timeline data");
1136
+ this.#segments = Object.freeze(
1137
+ assertDataArray(data.segments, "timeline segments").map(copySegment)
1138
+ );
1045
1139
  this.#totalBeats = assertRange(
1046
1140
  data.totalBeats,
1047
1141
  0,
1048
1142
  Number.MAX_SAFE_INTEGER,
1049
1143
  "timeline totalBeats"
1050
1144
  );
1051
- this.#keys = Object.freeze((data.keys ?? []).map(copyKeyRegion));
1145
+ this.#keys = Object.freeze(
1146
+ assertDataArray(data.keys ?? [], "timeline keys").map(copyKeyRegion)
1147
+ );
1052
1148
  this.#segmentConfidence = Object.freeze(
1053
1149
  copyConfidence(data.segmentConfidence, this.#segments.length)
1054
1150
  );
@@ -1073,8 +1169,8 @@ var Timeline = class _Timeline {
1073
1169
  // so a chord placed in a pickup is covered by the key it sounds in.
1074
1170
  startBeat: timeline.segments[0]?.startBeat ?? 0,
1075
1171
  endBeat: totalBeats,
1076
- key: toKeyScale(key),
1077
- confidence: GIVEN_KEY_CONFIDENCE
1172
+ key: keyIdentity(toKey(key)),
1173
+ confidence: STATED_KEY_CONFIDENCE
1078
1174
  }
1079
1175
  ];
1080
1176
  }
@@ -1096,8 +1192,10 @@ var Timeline = class _Timeline {
1096
1192
  * Place a progression's chords on a regular grid.
1097
1193
  *
1098
1194
  * The chords cross over as harmony alone: a spelling one of them carried is
1099
- * dropped, and a carried key becomes the plain key/scale of the one region
1100
- * under the span, without its spelled tonic or its detected scale form.
1195
+ * dropped, since a chord placed on the grid is a `ChordSpan` and a span
1196
+ * carries none. A carried key becomes the one key region under the span,
1197
+ * spelled tonic and scale form included — an Ab minor comes back Ab minor,
1198
+ * which is what the numerals over the region are read from.
1101
1199
  *
1102
1200
  * @param progression The chords, in order.
1103
1201
  * @param beatsEach How long each chord sounds.
@@ -1151,9 +1249,18 @@ var Timeline = class _Timeline {
1151
1249
  /** The key held longest across the span, when the analysis found one. */
1152
1250
  get key() {
1153
1251
  const prevailing = prevailingKeyOf(this.#keys);
1154
- return prevailing === null ? void 0 : Key.of(prevailing);
1252
+ return prevailing === null ? void 0 : toKey(prevailing);
1155
1253
  }
1156
- /** Every key region under the span, in time order. */
1254
+ /**
1255
+ * Every key region under the span, in time order.
1256
+ *
1257
+ * These are the regions the timeline was built with: the ones
1258
+ * {@link Timeline.fromNotes} read off the notes, or the single region a key
1259
+ * given to {@link Timeline.fromChords} describes. A timeline holding chords
1260
+ * alone carries none until it is asked to read them —
1261
+ * {@link Timeline.modulations} is where the chords are searched for the keys
1262
+ * they imply.
1263
+ */
1157
1264
  get keys() {
1158
1265
  return this.#keys.map(copyKeyRegion);
1159
1266
  }
@@ -1228,6 +1335,38 @@ var Timeline = class _Timeline {
1228
1335
  cadences() {
1229
1336
  return detectCadences(this.chordTimeline, this.#keyContext());
1230
1337
  }
1338
+ /**
1339
+ * The keys the chords themselves imply, in time order.
1340
+ *
1341
+ * The chord route to key regions, for a timeline that holds harmony without
1342
+ * the notes it was played from — a lead sheet, or a progression laid out in
1343
+ * time. A chord argues for a key far more strongly than its three or four
1344
+ * pitch classes do, so a modulation is read off the chords rather than off a
1345
+ * pitch-class profile, and the chord that reads in both keys is reported as
1346
+ * the pivot the modulation turned on.
1347
+ *
1348
+ * The regions are searched for here rather than kept on the timeline:
1349
+ * {@link Timeline.keys} answers with what the timeline was built with, which
1350
+ * for placed chords is the key the caller stated or nothing at all.
1351
+ *
1352
+ * @param opts Analysis options; see {@link KeyTimelineOptions}.
1353
+ * @returns The key regions the chords imply; empty when there are no chords.
1354
+ * @example
1355
+ * ```ts
1356
+ * import { Chord, Key, Progression, Timeline } from '@libraz/libcantus';
1357
+ * const progression = new Progression(
1358
+ * [Chord.parse('C'), Chord.parse('G7'), Chord.parse('C')],
1359
+ * Key.major('C'),
1360
+ * );
1361
+ * Timeline.fromProgression(progression, 4).modulations().length; // 1
1362
+ * ```
1363
+ */
1364
+ modulations(opts) {
1365
+ return detectModulations(this.#copySegments(), {
1366
+ totalBeats: this.#totalBeats,
1367
+ ...opts
1368
+ }).map(copyKeyRegion);
1369
+ }
1231
1370
  /**
1232
1371
  * The chords as roman numerals in a key.
1233
1372
  *
@@ -1240,8 +1379,9 @@ var Timeline = class _Timeline {
1240
1379
  * @throws If no key is given and the timeline carries none.
1241
1380
  */
1242
1381
  roman(key, opts) {
1243
- const given = key === void 0 ? void 0 : toKeyScale(key);
1244
- const keyAt = given === void 0 ? this.#keyContext() : () => given;
1382
+ assertKeyArgument(key, "timeline key");
1383
+ const given = key === void 0 ? void 0 : resolveKey(key);
1384
+ const keyAt = given === void 0 ? this.#resolvedKeyAt() : () => given;
1245
1385
  return this.#segments.map((segment) => ({
1246
1386
  startBeat: segment.startBeat,
1247
1387
  endBeat: segment.endBeat,
@@ -1369,16 +1509,19 @@ var Timeline = class _Timeline {
1369
1509
  /** The key in force at a beat, or undefined when the timeline knows none. */
1370
1510
  #keyAt(beat) {
1371
1511
  const prevailing = prevailingKeyOf(this.#keys);
1372
- return prevailing === null ? void 0 : Key.of(keyLookup(this.#keys, prevailing)(beat));
1512
+ return prevailing === null ? void 0 : toKey(keyLookup(this.#keys, prevailing)(beat));
1373
1513
  }
1374
1514
  /**
1375
- * The key every beat of the timeline is read against.
1515
+ * The key every beat of the timeline is read against, spelling and all.
1376
1516
  *
1377
1517
  * A lookup rather than one key, so a timeline that modulates has each chord
1378
- * analyzed in the key it sounds in; beats outside every region fall back to
1379
- * the prevailing key.
1518
+ * read in the key it sounds in; beats outside every region fall back to the
1519
+ * prevailing key. The regions carry a spelled tonic and a scale form, and
1520
+ * this hands them on as they are: an Ab minor is where a German sixth is
1521
+ * named from — `Fb Ab Cb D` — and reading it through its pitch classes alone
1522
+ * would number it as the G# minor those read best as.
1380
1523
  */
1381
- #keyContext() {
1524
+ #resolvedKeyAt() {
1382
1525
  const prevailing = prevailingKeyOf(this.#keys);
1383
1526
  if (prevailing === null) {
1384
1527
  throw new InvalidInputError(
@@ -1387,6 +1530,18 @@ var Timeline = class _Timeline {
1387
1530
  }
1388
1531
  return keyLookup(this.#keys, prevailing);
1389
1532
  }
1533
+ /**
1534
+ * The pitch classes of that key, for the readings that want them.
1535
+ *
1536
+ * A reduction and a cadence are read from which notes are in the key rather
1537
+ * than from how the key is written, so they take the scale; the reduction is
1538
+ * where the drop is stated rather than in the lookup, which is what let the
1539
+ * numerals lose the spelling by sharing it.
1540
+ */
1541
+ #keyContext() {
1542
+ const keyAt = this.#resolvedKeyAt();
1543
+ return (beat) => scaleOf(keyAt(beat));
1544
+ }
1390
1545
  /** Move every chord and every key region by the same step. */
1391
1546
  #moved(moveChord, moveKey) {
1392
1547
  const segments = this.#segments.map((segment) => ({
@@ -1395,7 +1550,10 @@ var Timeline = class _Timeline {
1395
1550
  chord: moveChord(new Chord(segment.chord)).toJSON()
1396
1551
  }));
1397
1552
  const keys = this.#keys.map((region) => {
1398
- const moved = { ...region, key: moveKey(Key.of(region.key)).scale };
1553
+ const moved = {
1554
+ ...region,
1555
+ key: keyIdentity(moveKey(toKey(region.key)))
1556
+ };
1399
1557
  if (region.pivot !== void 0) {
1400
1558
  moved.pivot = {
1401
1559
  ...region.pivot,
@@ -1420,11 +1578,14 @@ var Progression = class _Progression {
1420
1578
  * Wrap a chord sequence.
1421
1579
  *
1422
1580
  * @param chords The chords in order; the array is copied.
1423
- * @param key Optional key context for analysis methods.
1581
+ * @param key Optional key context for analysis methods, in whatever form the
1582
+ * caller holds it: a key name, a plain key/scale, or a {@link Key}.
1424
1583
  */
1425
1584
  constructor(chords, key) {
1426
- this.#chords = Object.freeze(_Progression.#attachKey(chords, key));
1427
- this.#key = key;
1585
+ assertKeyArgument(key, "progression key");
1586
+ const resolved = key === void 0 ? void 0 : toKey(key);
1587
+ this.#chords = Object.freeze(_Progression.#attachKey(chords, resolved));
1588
+ this.#key = resolved;
1428
1589
  }
1429
1590
  /**
1430
1591
  * Attach the progression key to every chord member.
@@ -1437,7 +1598,8 @@ var Progression = class _Progression {
1437
1598
  * number of times a key was attached.
1438
1599
  */
1439
1600
  static #attachKey(chords, key) {
1440
- return key === void 0 ? [...chords] : chords.map((chord) => chord.withKey(key));
1601
+ const given = assertDataObjects(chords, "progression chords");
1602
+ return key === void 0 ? [...given] : given.map((chord) => chord.withKey(key));
1441
1603
  }
1442
1604
  /**
1443
1605
  * Start from a chord sequence, matching the `of` factory on the other
@@ -1474,12 +1636,14 @@ var Progression = class _Progression {
1474
1636
  * ```ts
1475
1637
  * import { generateProgression, Key, Progression } from '@libraz/libcantus';
1476
1638
  * const key = Key.major('C');
1477
- * const spans = generateProgression({ key: key.scale, style: 'dance', bars: 4 });
1639
+ * const spans = generateProgression({ key, style: 'dance', bars: 4 });
1478
1640
  * Progression.fromSpans(spans, key).roman();
1479
1641
  * ```
1480
1642
  */
1481
1643
  static fromSpans(spans, key) {
1482
- const chords = spans.map((span) => Chord.fromData(chordFromSpan(span)));
1644
+ const chords = assertDataObjects(spans, "progression spans").map(
1645
+ (span) => Chord.fromData(chordFromSpan(span))
1646
+ );
1483
1647
  return new _Progression(chords, key);
1484
1648
  }
1485
1649
  /**
@@ -1489,8 +1653,11 @@ var Progression = class _Progression {
1489
1653
  * @returns The progression.
1490
1654
  */
1491
1655
  static fromJSON(data) {
1656
+ assertDataObject(data, "progression data");
1492
1657
  const key = data.key === void 0 ? void 0 : Key.fromJSON(data.key);
1493
- const chords = data.chords.map((chord) => Chord.fromJSON(chord));
1658
+ const chords = assertDataArray(data.chords, "progression chords").map(
1659
+ (chord) => Chord.fromJSON(chord)
1660
+ );
1494
1661
  return key === void 0 ? new _Progression(chords) : new _Progression(chords, key);
1495
1662
  }
1496
1663
  /**
@@ -1687,7 +1854,8 @@ var Progression = class _Progression {
1687
1854
  /**
1688
1855
  * A copy of this progression carrying the given key context.
1689
1856
  *
1690
- * @param key The key context to attach.
1857
+ * @param key The key context to attach; a key name, a plain key/scale, or a
1858
+ * {@link Key}.
1691
1859
  * @returns The new progression.
1692
1860
  */
1693
1861
  withKey(key) {
@@ -1701,7 +1869,7 @@ var Progression = class _Progression {
1701
1869
  * @throws If any chord admits no voicing within the given ranges.
1702
1870
  */
1703
1871
  voice(opts) {
1704
- const key = opts?.key ?? this.#key?.scale;
1872
+ const key = opts?.key ?? this.#key;
1705
1873
  return voiceProgression(
1706
1874
  this.#chords.map((chord) => chord.data),
1707
1875
  key === void 0 ? opts : { ...opts, key }
@@ -1710,7 +1878,8 @@ var Progression = class _Progression {
1710
1878
  /**
1711
1879
  * The Roman numeral of each chord in a key.
1712
1880
  *
1713
- * @param key Key to analyze in; falls back to the carried context.
1881
+ * @param key Key to analyze in, as a key name, a plain key/scale, or a
1882
+ * {@link Key}; falls back to the carried context.
1714
1883
  * @param opts Applied-numeral rendering options.
1715
1884
  * @returns One numeral per chord.
1716
1885
  * @throws If no key is given and none is carried.
@@ -1722,7 +1891,8 @@ var Progression = class _Progression {
1722
1891
  /**
1723
1892
  * The harmonic function of each chord in a key.
1724
1893
  *
1725
- * @param key Key to analyze in; falls back to the carried context.
1894
+ * @param key Key to analyze in, as a key name, a plain key/scale, or a
1895
+ * {@link Key}; falls back to the carried context.
1726
1896
  * @returns One function per chord.
1727
1897
  * @throws If no key is given and none is carried.
1728
1898
  */
@@ -1737,12 +1907,15 @@ var Progression = class _Progression {
1737
1907
  * progression has fewer than two chords.
1738
1908
  *
1739
1909
  * The options reach both analyses: `applied` and `alternatives` go to every
1740
- * chord, and `voicing` — the pitches of the last two chords, as
1741
- * {@link Progression.voice} produces them — to the cadence, which cannot tell
1742
- * a perfect authentic cadence from an imperfect one without knowing the
1743
- * soprano.
1744
- *
1745
- * @param key Key to analyze in; falls back to the carried context.
1910
+ * chord, and `voicing` — the pitches sounding under the progression, one
1911
+ * voicing per chord as {@link Progression.voice} produces them — to the
1912
+ * cadence, which cannot tell a perfect authentic cadence from an imperfect
1913
+ * one without knowing the soprano. It is the same shape
1914
+ * {@link Progression.cadences} takes, and the closing pair is cut from it
1915
+ * here; a two-element array is read as that pair itself.
1916
+ *
1917
+ * @param key Key to analyze in, as a key name, a plain key/scale, or a
1918
+ * {@link Key}; falls back to the carried context.
1746
1919
  * @param opts Applied-numeral rendering options, `alternatives` for the
1747
1920
  * readings both analyses turned down, and `voicing` for the cadence's
1748
1921
  * voice-leading detail; see {@link AnalyzeChordOptions} and
@@ -1753,19 +1926,41 @@ var Progression = class _Progression {
1753
1926
  * ```ts
1754
1927
  * import { Chord, Key, Progression } from '@libraz/libcantus';
1755
1928
  * const progression = new Progression([Chord.parse('G7'), Chord.parse('C')], Key.major('C'));
1756
- * const voiced = progression.voice();
1757
- * progression.analyze(undefined, { voicing: [voiced[0] ?? [], voiced[1] ?? []] }).cadence
1758
- * ?.strength; // 'perfect'
1929
+ * progression.analyze(undefined, { voicing: progression.voice() }).cadence?.strength;
1930
+ * // 'perfect'
1759
1931
  * ```
1760
1932
  */
1761
1933
  analyze(key, opts) {
1762
1934
  const resolved = this.#resolveKey(key);
1763
1935
  const chords = this.#chords.map((chord) => chord.analyze(resolved, opts));
1936
+ const { voicing, ...cadenceOpts } = opts ?? {};
1764
1937
  const from = this.#chords[this.#chords.length - 2];
1765
1938
  const to = this.#chords[this.#chords.length - 1];
1766
- const cadence = from !== void 0 && to !== void 0 ? detectCadence(from.data, to.data, resolved.scale, opts) : null;
1939
+ const approach = this.#chords[this.#chords.length - 3];
1940
+ const cadence = from !== void 0 && to !== void 0 ? detectCadence(from.data, to.data, resolved, {
1941
+ ...cadenceOpts,
1942
+ ...approach === void 0 ? {} : { approach: approach.data },
1943
+ ...voicing === void 0 ? {} : { voicing: this.#closingVoicing(voicing) }
1944
+ }) : null;
1767
1945
  return { chords, cadence };
1768
1946
  }
1947
+ /**
1948
+ * The two voicings of the closing pair, cut from the progression's own.
1949
+ *
1950
+ * `cadences` takes one voicing per chord and gives each pair its own two, so
1951
+ * this entry takes that same array rather than a shape only it understands: a
1952
+ * caller holding what `voice()` returned can hand it to either method. A
1953
+ * two-element array is the closing pair already, which is what a progression
1954
+ * of two chords produces either way.
1955
+ */
1956
+ #closingVoicing(voicing) {
1957
+ if (voicing.length === this.#chords.length || voicing.length === 2) {
1958
+ return [voicing[voicing.length - 2] ?? [], voicing[voicing.length - 1] ?? []];
1959
+ }
1960
+ throw new InvalidInputError(
1961
+ `voicing must hold one voicing per chord (${this.#chords.length}) or the closing pair; received ${voicing.length}`
1962
+ );
1963
+ }
1769
1964
  /**
1770
1965
  * Choose one compatible scale for every chord, favoring smooth changes.
1771
1966
  *
@@ -1786,7 +1981,8 @@ var Progression = class _Progression {
1786
1981
  * The chord before each pair is supplied from the progression itself, so a
1787
1982
  * cadential six-four is recognized as one wherever it stands.
1788
1983
  *
1789
- * @param key Key to analyze in; falls back to the carried context.
1984
+ * @param key Key to analyze in, as a key name, a plain key/scale, or a
1985
+ * {@link Key}; falls back to the carried context.
1790
1986
  * @param opts `voicing` is the pitches sounding under the whole progression,
1791
1987
  * one voicing per chord as {@link Progression.voice} produces them, from
1792
1988
  * which each pair takes its own two — without it no authentic cadence can be
@@ -1815,12 +2011,12 @@ var Progression = class _Progression {
1815
2011
  if (from === void 0 || to === void 0) {
1816
2012
  continue;
1817
2013
  }
1818
- const approach = index >= 2 ? this.#chords[index - 2] : opts?.approach;
2014
+ const approach = index >= 2 ? this.#chords[index - 2]?.data : opts?.approach;
1819
2015
  const pair = opts?.voicing;
1820
2016
  found.push(
1821
- detectCadence(from.data, to.data, resolved.scale, {
2017
+ detectCadence(from.data, to.data, resolved, {
1822
2018
  ...opts?.alternatives === void 0 ? {} : { alternatives: opts.alternatives },
1823
- ...approach === void 0 ? {} : { approach: approach.data },
2019
+ ...approach === void 0 ? {} : { approach },
1824
2020
  ...pair === void 0 ? {} : { voicing: [pair[index - 1] ?? [], pair[index] ?? []] }
1825
2021
  })
1826
2022
  );
@@ -1860,7 +2056,7 @@ var Progression = class _Progression {
1860
2056
  );
1861
2057
  }
1862
2058
  const key = this.#resolveKey();
1863
- const chosen = substituteChord(target.data, key.scale, opts).find(
2059
+ const chosen = substituteChord(target.data, key, opts).find(
1864
2060
  (candidate) => candidate.type === type
1865
2061
  );
1866
2062
  if (chosen === void 0) {
@@ -1943,7 +2139,8 @@ var Progression = class _Progression {
1943
2139
  * a target in another mode — a relative, parallel, or modal key — is the key
1944
2140
  * the progression is then analyzed in.
1945
2141
  *
1946
- * @param target The key the transposed progression should be in.
2142
+ * @param target The key the transposed progression should be in, as a key
2143
+ * name, a plain key/scale, or a {@link Key}.
1947
2144
  * @returns The transposed progression, carrying `target` as its key.
1948
2145
  * @throws If the progression carries no key context to measure from.
1949
2146
  * @example
@@ -1960,9 +2157,11 @@ var Progression = class _Progression {
1960
2157
  "progression has no key context; attach one with withKey() before transposing to another key"
1961
2158
  );
1962
2159
  }
1963
- const interval = from.intervalTo(target);
2160
+ assertKeyArgument(target, "progression key");
2161
+ const resolved = toKey(target);
2162
+ const interval = from.intervalTo(resolved);
1964
2163
  const chords = this.#chords.map((chord) => chord.transposeBy(interval));
1965
- return new _Progression(chords, target);
2164
+ return new _Progression(chords, resolved);
1966
2165
  }
1967
2166
  /**
1968
2167
  * The chord symbols separated by spaces, so a template literal or a log line
@@ -1987,10 +2186,15 @@ var Progression = class _Progression {
1987
2186
  key: this.#key?.toJSON()
1988
2187
  };
1989
2188
  }
1990
- /** Resolve the key for an analysis method: explicit first, then carried. */
2189
+ /**
2190
+ * Resolve the key for an analysis method: explicit first, then carried.
2191
+ *
2192
+ * Every method that takes a key comes through here, so the shape check and
2193
+ * the fallback to the carried context are the same wherever a key is read.
2194
+ */
1991
2195
  #resolveKey(key) {
1992
2196
  assertKeyArgument(key, "progression key");
1993
- const resolved = key ?? this.#key;
2197
+ const resolved = key === void 0 ? this.#key : toKey(key);
1994
2198
  if (resolved === void 0) {
1995
2199
  throw new InvalidInputError(
1996
2200
  "progression has no key context; pass a Key or attach one with withKey()"
@@ -2004,13 +2208,6 @@ var Progression = class _Progression {
2004
2208
  function hintMatches(hint, pc) {
2005
2209
  return hint !== void 0 && pc !== void 0 && pitchClassOf(noteToPitchClass(hint)) === pitchClassOf(pc);
2006
2210
  }
2007
- function transposeHint(hint, semitones) {
2008
- if (hint === void 0) {
2009
- return void 0;
2010
- }
2011
- const moved = transposeNote(hint, semitones);
2012
- return { letter: moved.letter, alter: moved.alter };
2013
- }
2014
2211
  function transposeHintByInterval(hint, interval) {
2015
2212
  if (hint === void 0) {
2016
2213
  return void 0;
@@ -2018,12 +2215,6 @@ function transposeHintByInterval(hint, interval) {
2018
2215
  const moved = transposeByInterval(hint, interval);
2019
2216
  return { letter: moved.letter, alter: moved.alter };
2020
2217
  }
2021
- function transposeToneHints(hints, semitones) {
2022
- return hints?.map((hint) => {
2023
- const moved = transposeNote(hint, semitones);
2024
- return { letter: moved.letter, alter: moved.alter };
2025
- });
2026
- }
2027
2218
  function transposeToneHintsByInterval(hints, interval) {
2028
2219
  return hints?.map((hint) => {
2029
2220
  const moved = transposeByInterval(hint, interval);
@@ -2031,18 +2222,22 @@ function transposeToneHintsByInterval(hints, interval) {
2031
2222
  });
2032
2223
  }
2033
2224
  function copyChord(data) {
2225
+ assertDataObject(data, "chord data");
2226
+ assertChordQuality(data.quality);
2227
+ const rootPc = pitchClassOf(data.rootPc);
2034
2228
  const copy = {
2035
- rootPc: pitchClassOf(data.rootPc),
2229
+ rootPc,
2036
2230
  quality: data.quality,
2037
2231
  // Each offset is checked rather than copied blind: a chord holding a NaN
2038
2232
  // interval voices, spells and formats as a chord that looks real, and the
2039
2233
  // failure surfaces wherever the number is finally used.
2040
- intervals: data.intervals.map(
2234
+ intervals: assertDataArray(data.intervals, "chord intervals").map(
2041
2235
  (interval, index) => assertFiniteNumber(interval, `chord intervals[${index}]`)
2042
2236
  )
2043
2237
  };
2044
- if (data.bassPc !== void 0) {
2045
- copy.bassPc = pitchClassOf(data.bassPc);
2238
+ const bassPc = data.bassPc === void 0 ? void 0 : pitchClassOf(data.bassPc);
2239
+ if (bassPc !== void 0 && bassPc !== rootPc) {
2240
+ copy.bassPc = bassPc;
2046
2241
  }
2047
2242
  const tones = chordToneSpellings(data);
2048
2243
  if (tones !== void 0) {
@@ -2051,7 +2246,7 @@ function copyChord(data) {
2051
2246
  if (hintMatches(data.rootSpelling, data.rootPc) && data.rootSpelling !== void 0) {
2052
2247
  copy.rootSpelling = { letter: data.rootSpelling.letter, alter: data.rootSpelling.alter };
2053
2248
  }
2054
- if (hintMatches(data.bassSpelling, data.bassPc) && data.bassSpelling !== void 0) {
2249
+ if (hintMatches(data.bassSpelling, copy.bassPc) && data.bassSpelling !== void 0) {
2055
2250
  copy.bassSpelling = { letter: data.bassSpelling.letter, alter: data.bassSpelling.alter };
2056
2251
  }
2057
2252
  return copy;
@@ -2072,7 +2267,7 @@ function deriveBassSpelling(chord, key) {
2072
2267
  if (key === void 0) {
2073
2268
  return void 0;
2074
2269
  }
2075
- const spelled = spellPitchClass(bassPc, key.tonic.data, key.scale);
2270
+ const spelled = spellPitchClass(bassPc, key.tonic.data, key);
2076
2271
  return { letter: spelled.letter, alter: spelled.alter };
2077
2272
  }
2078
2273
  var Chord = class _Chord {
@@ -2083,11 +2278,13 @@ var Chord = class _Chord {
2083
2278
  * Wrap a plain chord object.
2084
2279
  *
2085
2280
  * @param data The chord; it is copied, never retained or mutated.
2086
- * @param key Optional key context for analysis methods.
2281
+ * @param key Optional key context for analysis methods, in whatever form the
2282
+ * caller holds it: a key name, a plain key/scale, or a {@link Key}.
2087
2283
  */
2088
2284
  constructor(data, key) {
2089
2285
  this.#given = copyChord(data);
2090
- this.#key = key;
2286
+ assertKeyArgument(key, "chord key");
2287
+ this.#key = key === void 0 ? void 0 : toKey(key);
2091
2288
  }
2092
2289
  /**
2093
2290
  * The chord data with any missing spelling hint filled in from the key.
@@ -2103,7 +2300,7 @@ var Chord = class _Chord {
2103
2300
  #dataWithKey(key) {
2104
2301
  const out = copyChord(this.#given);
2105
2302
  if (out.rootSpelling === void 0 && key !== void 0) {
2106
- out.rootSpelling = spellPitchClass(out.rootPc, key.tonic.data, key.scale);
2303
+ out.rootSpelling = spellPitchClass(out.rootPc, key.tonic.data, key);
2107
2304
  }
2108
2305
  if (out.bassSpelling === void 0) {
2109
2306
  const bass = deriveBassSpelling(out, key);
@@ -2188,11 +2385,11 @@ var Chord = class _Chord {
2188
2385
  * ```ts
2189
2386
  * import { Chord } from '@libraz/libcantus';
2190
2387
  * Chord.fromFiguredBass('B', '6', 'C major').symbol(); // 'G/B'
2191
- * Chord.fromFiguredBass('G', '#', 'C minor').symbol(); // 'G'
2388
+ * Chord.fromFiguredBass('G', 'n', 'C minor').symbol(); // 'G'
2192
2389
  * ```
2193
2390
  */
2194
2391
  static fromFiguredBass(bass, figures, key) {
2195
- return new _Chord(realizeFiguredBass(toNoteData(bass), figures, toKeyScale(key)));
2392
+ return new _Chord(realizeFiguredBass(toNoteData(bass), figures, key));
2196
2393
  }
2197
2394
  /**
2198
2395
  * Parse a lead-sheet chord symbol (e.g. `'Cmaj7'`, `'F#m7b5'`, `'C/G'`).
@@ -2344,7 +2541,8 @@ var Chord = class _Chord {
2344
2541
  * during a modulation does not keep the old key's letters and the order of
2345
2542
  * `withKey` calls does not affect the result.
2346
2543
  *
2347
- * @param key The key context to attach.
2544
+ * @param key The key context to attach; a key name, a plain key/scale, or a
2545
+ * {@link Key}.
2348
2546
  * @returns The new chord.
2349
2547
  */
2350
2548
  withKey(key) {
@@ -2412,13 +2610,14 @@ var Chord = class _Chord {
2412
2610
  /**
2413
2611
  * The chord's Roman numeral in a key.
2414
2612
  *
2415
- * @param key Key to analyze in; falls back to the carried context.
2613
+ * @param key Key to analyze in, as a key name, a plain key/scale, or a
2614
+ * {@link Key}; falls back to the carried context.
2416
2615
  * @param opts Applied-numeral rendering options.
2417
2616
  * @returns The Roman numeral string.
2418
2617
  * @throws If no key is given and none is carried.
2419
2618
  */
2420
2619
  roman(key, opts) {
2421
- return chordToRoman(this.#data, this.#resolveKey(key).scale, opts);
2620
+ return chordToRoman(this.#data, this.#resolveKey(key), opts);
2422
2621
  }
2423
2622
  /**
2424
2623
  * The chord's Roman numeral together with the reasoning behind it: the degree
@@ -2446,23 +2645,25 @@ var Chord = class _Chord {
2446
2645
  * ```
2447
2646
  */
2448
2647
  explain(key, opts) {
2449
- return explainRoman(this.#data, this.#resolveScale(key), opts);
2648
+ return explainRoman(this.#data, this.#resolveKey(key), opts);
2450
2649
  }
2451
2650
  /**
2452
2651
  * The chord's harmonic function (tonic / subdominant / dominant) in a key.
2453
2652
  *
2454
- * @param key Key to analyze in; falls back to the carried context.
2653
+ * @param key Key to analyze in, as a key name, a plain key/scale, or a
2654
+ * {@link Key}; falls back to the carried context.
2455
2655
  * @returns The harmonic function.
2456
2656
  * @throws If no key is given and none is carried.
2457
2657
  */
2458
2658
  function(key) {
2459
- return functionOf(this.#data, this.#resolveKey(key).scale);
2659
+ return functionOf(this.#data, this.#resolveKey(key));
2460
2660
  }
2461
2661
  /**
2462
2662
  * Full functional analysis: function, borrowing, Roman numeral, and the
2463
2663
  * rationale behind them.
2464
2664
  *
2465
- * @param key Key to analyze in; falls back to the carried context.
2665
+ * @param key Key to analyze in, as a key name, a plain key/scale, or a
2666
+ * {@link Key}; falls back to the carried context.
2466
2667
  * @param opts Applied-numeral rendering options, plus `alternatives` to
2467
2668
  * collect the readings this analysis turned down; see
2468
2669
  * {@link AnalyzeChordOptions}.
@@ -2475,27 +2676,29 @@ var Chord = class _Chord {
2475
2676
  * ```
2476
2677
  */
2477
2678
  analyze(key, opts) {
2478
- return analyzeChord(this.#data, this.#resolveKey(key).scale, opts);
2679
+ return analyzeChord(this.#data, this.#resolveKey(key), opts);
2479
2680
  }
2480
2681
  /**
2481
2682
  * Whether the chord is borrowed from the parallel mode (modal interchange).
2482
2683
  *
2483
- * @param key Key to analyze in; falls back to the carried context.
2684
+ * @param key Key to analyze in, as a key name, a plain key/scale, or a
2685
+ * {@link Key}; falls back to the carried context.
2484
2686
  * @returns True if the chord is borrowed.
2485
2687
  * @throws If no key is given and none is carried.
2486
2688
  */
2487
2689
  isBorrowed(key) {
2488
- return isBorrowedChord(this.#data, this.#resolveKey(key).scale);
2690
+ return isBorrowedChord(this.#data, this.#resolveKey(key));
2489
2691
  }
2490
2692
  /**
2491
2693
  * The origin of a non-diatonic chord (parallel mode or Neapolitan), or null.
2492
2694
  *
2493
- * @param key Key to analyze in; falls back to the carried context.
2695
+ * @param key Key to analyze in, as a key name, a plain key/scale, or a
2696
+ * {@link Key}; falls back to the carried context.
2494
2697
  * @returns The borrowing source, or null.
2495
2698
  * @throws If no key is given and none is carried.
2496
2699
  */
2497
2700
  borrowedSource(key) {
2498
- return borrowedSource(this.#data, this.#resolveKey(key).scale);
2701
+ return borrowedSource(this.#data, this.#resolveKey(key));
2499
2702
  }
2500
2703
  /**
2501
2704
  * The chord written as the figures a bass would carry under it: `6`, `64`,
@@ -2520,7 +2723,7 @@ var Chord = class _Chord {
2520
2723
  * ```
2521
2724
  */
2522
2725
  figuredBass(key) {
2523
- return figuredBassOf(this.#data, this.#resolveScale(key));
2726
+ return figuredBassOf(this.#data, this.#resolveKey(key));
2524
2727
  }
2525
2728
  /**
2526
2729
  * The chords that can stand in for this one in a key: its tritone substitute,
@@ -2547,7 +2750,7 @@ var Chord = class _Chord {
2547
2750
  * ```
2548
2751
  */
2549
2752
  substitutions(key, opts) {
2550
- return substituteChord(this.#data, this.#resolveScale(key), opts);
2753
+ return substituteChord(this.#data, this.#resolveKey(key), opts);
2551
2754
  }
2552
2755
  /**
2553
2756
  * The modal-interchange palette of the chord's key: the parallel mode's
@@ -2572,7 +2775,7 @@ var Chord = class _Chord {
2572
2775
  * ```
2573
2776
  */
2574
2777
  modalInterchange(key) {
2575
- return modalInterchangePalette(this.#resolveScale(key));
2778
+ return modalInterchangePalette(this.#resolveKey(key));
2576
2779
  }
2577
2780
  /**
2578
2781
  * The chord rendered as a lead-sheet symbol (e.g. `'Cmaj7'`, `'F#m7'`, `'C/G'`).
@@ -2600,7 +2803,7 @@ var Chord = class _Chord {
2600
2803
  * @throws If no voicing fits the given ranges.
2601
2804
  */
2602
2805
  voice(opts) {
2603
- const key = opts?.key ?? this.#key?.scale;
2806
+ const key = opts?.key ?? this.#key;
2604
2807
  return voiceChord(this.#data, key === void 0 ? opts : { ...opts, key });
2605
2808
  }
2606
2809
  /**
@@ -2617,14 +2820,14 @@ var Chord = class _Chord {
2617
2820
  * The negative-harmony mirror of the chord about the key's tonic–dominant
2618
2821
  * axis (major becomes minor and vice versa).
2619
2822
  *
2620
- * @param key Key providing the reflection axis; falls back to the carried
2621
- * context.
2823
+ * @param key Key providing the reflection axis, as a key name, a plain
2824
+ * key/scale, or a {@link Key}; falls back to the carried context.
2622
2825
  * @returns The mirrored chord, keeping any key context.
2623
2826
  * @throws If no key is given and none is carried.
2624
2827
  */
2625
2828
  negativeHarmony(key) {
2626
2829
  const resolved = this.#resolveKey(key);
2627
- return new _Chord(negativeHarmonyMirror(this.#data, resolved.scale), key ?? this.#key);
2830
+ return new _Chord(negativeHarmonyMirror(this.#data, resolved), resolved);
2628
2831
  }
2629
2832
  /**
2630
2833
  * The V7 that tonicizes this chord: a dominant seventh a perfect fifth above
@@ -2690,9 +2893,10 @@ var Chord = class _Chord {
2690
2893
  */
2691
2894
  transpose(semitones) {
2692
2895
  const moved = transposeChord(this.#given, semitones);
2693
- const rootSpelling = transposeHint(this.#given.rootSpelling, semitones);
2694
- const bassSpelling = transposeHint(this.#given.bassSpelling, semitones);
2695
- const toneSpellings = transposeToneHints(this.#given.toneSpellings, semitones);
2896
+ const { rootSpelling, bassSpelling, toneSpellings } = transposeChordSpellings(
2897
+ this.#given,
2898
+ semitones
2899
+ );
2696
2900
  if (rootSpelling !== void 0) {
2697
2901
  moved.rootSpelling = rootSpelling;
2698
2902
  }
@@ -2744,6 +2948,10 @@ var Chord = class _Chord {
2744
2948
  * The named scales that fit over this chord, best fit first, rooted on the
2745
2949
  * chord root.
2746
2950
  *
2951
+ * Only the scales of {@link NAMED_SCALES} are ranked: chord-scale theory is a
2952
+ * Western practice, so the scales of {@link WORLD_SCALES} are left out rather
2953
+ * than offered as answers over a chord.
2954
+ *
2747
2955
  * @returns The matching scales.
2748
2956
  */
2749
2957
  scales() {
@@ -2775,12 +2983,16 @@ var Chord = class _Chord {
2775
2983
  * The available tensions (usable non-chord, non-avoid scale tones) of a
2776
2984
  * scale over this chord.
2777
2985
  *
2778
- * @param scaleName A named scale, rooted on the chord root.
2986
+ * @param scaleName A scale of {@link NAMED_SCALES}, or one of its aliases,
2987
+ * rooted on the chord root. The scales of {@link WORLD_SCALES} are refused
2988
+ * here as they are by {@link Chord.scales}: chord-scale theory is a Western
2989
+ * practice, and a scale from another tradition offered over a chord would
2990
+ * read as an answer while being a category error.
2779
2991
  * @param opts Set `resolvesTo` to the plain chord this one resolves to, so a
2780
2992
  * dominant resolving down a fifth onto a minor tonic takes the tensions
2781
2993
  * that resolution makes available; see {@link AvailableTensionsOptions}.
2782
2994
  * @returns Tension pitch classes, ascending in [0, 11].
2783
- * @throws If `scaleName` is not a built-in scale.
2995
+ * @throws If `scaleName` is not one of the named scales or their aliases.
2784
2996
  * @example
2785
2997
  * ```ts
2786
2998
  * import { Chord } from '@libraz/libcantus';
@@ -2796,12 +3008,16 @@ var Chord = class _Chord {
2796
3008
  * The avoid notes (scale tones a semitone above a chord tone) of a scale
2797
3009
  * over this chord.
2798
3010
  *
2799
- * @param scaleName A named scale, rooted on the chord root.
3011
+ * @param scaleName A scale of {@link NAMED_SCALES}, or one of its aliases,
3012
+ * rooted on the chord root. The scales of {@link WORLD_SCALES} are refused
3013
+ * here as they are by {@link Chord.scales}: chord-scale theory is a Western
3014
+ * practice, and a scale from another tradition offered over a chord would
3015
+ * read as an answer while being a category error.
2800
3016
  * @param opts Set `use: 'melodic'` to judge a line rather than a voicing, so
2801
3017
  * a tone a line may pass through is not counted; see
2802
3018
  * {@link AvoidNotesOptions}.
2803
3019
  * @returns Avoid-note pitch classes, ascending in [0, 11].
2804
- * @throws If `scaleName` is not a built-in scale.
3020
+ * @throws If `scaleName` is not one of the named scales or their aliases.
2805
3021
  * @example
2806
3022
  * ```ts
2807
3023
  * import { Chord } from '@libraz/libcantus';
@@ -2824,15 +3040,18 @@ var Chord = class _Chord {
2824
3040
  * are the notes the chord sounds — the same set {@link Chord.pitchClasses}
2825
3041
  * reports.
2826
3042
  *
2827
- * @param key Key providing the spelled tonic; falls back to the carried
2828
- * context, then to the chord's own root spelling.
3043
+ * @param key Key providing the spelled tonic, as a key name, a plain
3044
+ * key/scale, or a {@link Key} — the tonic its letters are spelled from is
3045
+ * derived from the key rather than asked of the caller. Falls back to the
3046
+ * carried context, then to the chord's own root spelling.
2829
3047
  * @returns Spelled octave-less notes in the chord's own (tertian) order,
2830
3048
  * with any foreign slash bass last.
2831
3049
  * @throws If no key is given, none is carried, and the chord has no root
2832
3050
  * spelling of its own.
2833
3051
  */
2834
3052
  spell(key) {
2835
- const resolved = key ?? this.#key;
3053
+ assertKeyArgument(key, "chord key");
3054
+ const resolved = key === void 0 ? this.#key : toKey(key);
2836
3055
  const data = this.#dataWithKey(resolved);
2837
3056
  let tones;
2838
3057
  if (resolved === void 0) {
@@ -2844,7 +3063,7 @@ var Chord = class _Chord {
2844
3063
  }
2845
3064
  tones = spellChordFromRoot(data, root);
2846
3065
  } else {
2847
- tones = spellChord(data, resolved.tonic.data, resolved.scale);
3066
+ tones = spellChord(data, resolved.tonic.data, resolved);
2848
3067
  }
2849
3068
  const bassPc = data.bassPc;
2850
3069
  const bass = data.bassSpelling;
@@ -2913,16 +3132,14 @@ var Chord = class _Chord {
2913
3132
  return this.symbol();
2914
3133
  }
2915
3134
  /**
2916
- * Resolve the key/scale for a method that takes any key-shaped value:
2917
- * explicit first, then carried, by the same rule {@link Chord.roman} follows.
3135
+ * Resolve the key for an analysis method: explicit first, then carried.
3136
+ *
3137
+ * Every method that takes a key comes through here, so the shape check and
3138
+ * the fallback to the carried context are the same wherever a key is read.
2918
3139
  */
2919
- #resolveScale(key) {
2920
- return key === void 0 ? this.#resolveKey().scale : toKeyScale(key);
2921
- }
2922
- /** Resolve the key for an analysis method: explicit first, then carried. */
2923
3140
  #resolveKey(key) {
2924
3141
  assertKeyArgument(key, "chord key");
2925
- const resolved = key ?? this.#key;
3142
+ const resolved = key === void 0 ? this.#key : toKey(key);
2926
3143
  if (resolved === void 0) {
2927
3144
  throw new InvalidInputError(
2928
3145
  "chord has no key context; pass a Key or attach one with withKey()"
@@ -2933,8 +3150,23 @@ var Chord = class _Chord {
2933
3150
  };
2934
3151
 
2935
3152
  // src/model/key.ts
2936
- var MAX_CONVENTIONAL_FIFTHS = 7;
2937
- var DOUBLE_ACCIDENTAL2 = 2;
3153
+ function detectedKeyMatch(match) {
3154
+ const key = { ...resolveKey(match.key), variant: match.variant };
3155
+ return { ...match, key: toKey(key) };
3156
+ }
3157
+ function scaleWord(mask, isMinor) {
3158
+ const name = scaleNameOfMask(mask);
3159
+ if (name === void 0) {
3160
+ return isMinor ? "minor" : "major";
3161
+ }
3162
+ if (name === "major") {
3163
+ return "major";
3164
+ }
3165
+ if (name === "naturalMinor") {
3166
+ return "minor";
3167
+ }
3168
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
3169
+ }
2938
3170
  var HEPTATONIC_DEGREES = 7;
2939
3171
  var REFERENCE_TONIC = { letter: 0, alter: 0 };
2940
3172
  function bareInterval(from, to) {
@@ -2944,7 +3176,7 @@ function bareInterval(from, to) {
2944
3176
  );
2945
3177
  }
2946
3178
  function spelledTonicFor(scale) {
2947
- return new Note(spelledKeyOf(scale).tonic);
3179
+ return new Note(resolveKey(scale).tonic);
2948
3180
  }
2949
3181
  function isNoteInstance(value) {
2950
3182
  if (value === null || typeof value !== "object") {
@@ -2969,9 +3201,12 @@ var Key = class _Key {
2969
3201
  *
2970
3202
  * @param scale The key/scale; its root is normalized to a pitch class.
2971
3203
  * @param tonic The spelled tonic anchoring letter-name spelling.
2972
- * @param variant Optional detected scale form retained for display.
3204
+ * @param variant The scale form. Read from the mask when none is named: a
3205
+ * major mask is a major key whoever built it, and a key with no form at all
3206
+ * is a state a reader would have to guess about.
2973
3207
  */
2974
3208
  constructor(scale, tonic, variant) {
3209
+ assertDataObject(scale, "scale");
2975
3210
  assertInteger(scale.rootPc, "scale.rootPc");
2976
3211
  assertInteger(scale.modeMask12, "scale.modeMask12", 1, 4095);
2977
3212
  if ((scale.modeMask12 & 1) === 0) {
@@ -2988,14 +3223,16 @@ var Key = class _Key {
2988
3223
  `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`
2989
3224
  );
2990
3225
  }
3226
+ if (variant !== void 0) {
3227
+ assertKeyVariant(variant, scale.modeMask12);
3228
+ }
2991
3229
  this.#scale = { rootPc, modeMask12: scale.modeMask12 };
2992
3230
  this.#tonic = tonic;
2993
- this.#variant = variant;
3231
+ this.#variant = variant ?? variantOfMask(scale.modeMask12);
2994
3232
  }
2995
3233
  /** Preserve detection metadata while replacing its plain key with this API's Key. */
2996
3234
  static #fromMatch(match) {
2997
- const key = new _Key(match.key, spelledTonicFor(match.key), match.variant);
2998
- return { ...match, key };
3235
+ return detectedKeyMatch(match);
2999
3236
  }
3000
3237
  /**
3001
3238
  * Wrap a key the theory layer spelled, keeping its tonic exactly as given.
@@ -3005,7 +3242,7 @@ var Key = class _Key {
3005
3242
  * would throw that spelling away (Bb minor would come back as A# minor).
3006
3243
  */
3007
3244
  static #fromSpelled(spelled) {
3008
- return new _Key(spelled.key, new Note(spelled.tonic));
3245
+ return new _Key(spelled.scale, new Note(spelled.tonic), spelled.variant);
3009
3246
  }
3010
3247
  /**
3011
3248
  * A major key.
@@ -3067,8 +3304,12 @@ var Key = class _Key {
3067
3304
  * is G sharp major and `'gis'` G sharp minor; see {@link parseKeyName} for
3068
3305
  * how a name whose case and mode word disagree is read.
3069
3306
  *
3070
- * The result is always a plain major or minor key. Only the tonic's spelling
3071
- * survives from the name, so `'ges dur'` is G flat major, not F sharp major.
3307
+ * An English name may also name a built-in scale instead of a mode —
3308
+ * `'D harmonic minor'`, `'D dorian'`, `'C major pentatonic'` — which is what
3309
+ * {@link Key.toString} writes for a key that is neither a plain major nor a
3310
+ * plain minor, so a key survives being written down and read back. Only the
3311
+ * tonic's spelling survives from the name, so `'ges dur'` is G flat major,
3312
+ * not F sharp major.
3072
3313
  *
3073
3314
  * @param text The key name.
3074
3315
  * @param opts `system` reads the name in that notation system instead of
@@ -3107,15 +3348,18 @@ var Key = class _Key {
3107
3348
  * ```
3108
3349
  */
3109
3350
  static tryParse(text, opts) {
3110
- const parsed = tryParseKeyName(text, opts);
3111
- return parsed.ok ? { ok: true, value: _Key.#modeOn(parsed.value.mode, new Note(parsed.value.tonic)) } : parsed;
3351
+ const parsed = tryResolveKeyName(text, opts);
3352
+ return parsed.ok ? {
3353
+ ok: true,
3354
+ value: new _Key(parsed.value.scale, new Note(parsed.value.tonic), parsed.value.variant)
3355
+ } : parsed;
3112
3356
  }
3113
3357
  /**
3114
3358
  * The key a key signature denotes: the inverse of {@link Key.fifths}.
3115
3359
  *
3116
3360
  * A signature names two keys — three sharps is both A major and F# minor — so
3117
3361
  * the mode decides which one is built. The tonic is spelled the way that key
3118
- * is written, including the theoretical keys past ±7 (nine sharps is B#
3362
+ * is written, including the theoretical keys past ±7 (nine sharps is D#
3119
3363
  * major).
3120
3364
  *
3121
3365
  * @param fifths The signed number of sharps (positive) or flats (negative).
@@ -3142,13 +3386,20 @@ var Key = class _Key {
3142
3386
  * This is what keeps `Key.of(detectKey(...).scale)` from handing every
3143
3387
  * downstream chord a double-sharp spelling.
3144
3388
  *
3145
- * @param scale The key/scale to wrap.
3146
- * @param tonic Optional spelled tonic; must spell the scale's root pitch class.
3389
+ * A value that already carries a spelling keeps it — a key region's key, the
3390
+ * data a key serializes to — so wrapping one is not where an Ab minor becomes
3391
+ * a G# minor. Only a bare key/scale has a tonic chosen for it, because only a
3392
+ * bare key/scale is missing one.
3393
+ *
3394
+ * @param key The key to wrap, in any form a key is held in.
3395
+ * @param tonic Optional spelled tonic, overriding any the value carries; must
3396
+ * spell the scale's root pitch class.
3147
3397
  * @returns The key.
3148
3398
  * @throws If the given tonic is not the scale's root pitch class.
3149
3399
  */
3150
- static of(scale, tonic) {
3151
- return new _Key(scale, tonic ?? spelledTonicFor(scale));
3400
+ static of(key, tonic) {
3401
+ const resolved = resolveKey(key);
3402
+ return new _Key(resolved.scale, tonic ?? new Note(resolved.tonic), resolved.variant);
3152
3403
  }
3153
3404
  /**
3154
3405
  * Rebuild a key from its {@link Key.toJSON} output.
@@ -3157,6 +3408,7 @@ var Key = class _Key {
3157
3408
  * @returns The key.
3158
3409
  */
3159
3410
  static fromJSON(data) {
3411
+ assertDataObject(data, "key data");
3160
3412
  return new _Key(data.scale, new Note(data.tonic), data.variant);
3161
3413
  }
3162
3414
  /**
@@ -3355,12 +3607,18 @@ var Key = class _Key {
3355
3607
  return supportsFunctionalHarmony(this.#scale);
3356
3608
  }
3357
3609
  /**
3358
- * The relative key: the same key signature read in the other mode.
3610
+ * The relative key: for a plain major or minor key, the same key signature
3611
+ * read in the other mode.
3359
3612
  *
3360
3613
  * Because the new tonic is read off the circle of fifths rather than
3361
3614
  * transposed by semitones, the relative of Db major is Bb minor and not its
3362
3615
  * enharmonic A# minor.
3363
3616
  *
3617
+ * A key that is not a plain major or minor is read through the mode its third
3618
+ * names and stepped from where its tonic stands on the circle: the relative of
3619
+ * D dorian is F major. The shared signature does not carry over to those —
3620
+ * D dorian is written with no accidentals and F major with one.
3621
+ *
3364
3622
  * @returns The relative key, always a plain major or minor key.
3365
3623
  * @throws If this key's signature falls outside [-12, 12] fifths.
3366
3624
  * @example
@@ -3443,10 +3701,16 @@ var Key = class _Key {
3443
3701
  *
3444
3702
  * These are the keys the German and Japanese teaching tradition counts as a
3445
3703
  * key's near relations: the relative and parallel keys, the dominant and the
3446
- * subdominant, and the relatives of those two. Five of them are written
3447
- * within one accidental of this key's signature — the relative shares it
3448
- * exactly and the other four stand one away; the parallel key stands three
3449
- * away and belongs to the set for the tonic it shares instead.
3704
+ * subdominant, and the relatives of those two. For a plain major or minor key
3705
+ * five of them are written within one accidental of this key's signature — the
3706
+ * relative shares it exactly and the other four stand one away; the parallel
3707
+ * key stands three away and belongs to the set for the tonic it shares
3708
+ * instead.
3709
+ *
3710
+ * A key that is not a plain major or minor is read through the mode its third
3711
+ * names, so its relations are walked from where its tonic stands on the circle
3712
+ * rather than from its own signature, and those accidental counts do not
3713
+ * describe them.
3450
3714
  *
3451
3715
  * @returns The related keys in relation order.
3452
3716
  * @throws If a neighbouring signature falls outside [-12, 12] fifths.
@@ -3487,10 +3751,38 @@ var Key = class _Key {
3487
3751
  * ```
3488
3752
  */
3489
3753
  relationTo(other) {
3490
- return keyRelationBetween(
3491
- { tonic: this.#tonic.data, key: this.#scale },
3492
- { tonic: other.tonic.data, key: other.scale }
3493
- );
3754
+ return keyRelationBetween(this.toJSON(), other.toJSON());
3755
+ }
3756
+ /**
3757
+ * The chords a modulation from this key into another can pivot on.
3758
+ *
3759
+ * A pivot belongs to both keys at once, so it is heard as a degree of this
3760
+ * key and reinterpreted as a degree of the key being entered. The candidates
3761
+ * are this key's diatonic triads, kept when every one of their pitch classes
3762
+ * is also a scale tone of `other`; each is reported with its numeral in both
3763
+ * keys. A key that stacks no diatonic triads offers nothing to pivot on and
3764
+ * yields an empty list rather than an error.
3765
+ *
3766
+ * @param other The key being entered, as a key name, a plain key/scale, or a
3767
+ * {@link Key}.
3768
+ * @returns The shared triads, ascending by their degree in this key, each
3769
+ * chord carrying this key as its context.
3770
+ * @example
3771
+ * ```ts
3772
+ * import { Key } from '@libraz/libcantus';
3773
+ * Key.major('C')
3774
+ * .pivotsTo('G major')
3775
+ * .map((pivot) => `${pivot.romanFrom}=${pivot.romanTo}`);
3776
+ * // ['I=IV', 'iii=vi', 'V=I', 'vi=ii']
3777
+ * ```
3778
+ */
3779
+ pivotsTo(other) {
3780
+ assertKeyArgument(other, "key");
3781
+ return pivotChords(this.#scale, other).map(({ chord, romanFrom, romanTo }) => ({
3782
+ chord: new Chord(chord, this),
3783
+ romanFrom,
3784
+ romanTo
3785
+ }));
3494
3786
  }
3495
3787
  /**
3496
3788
  * A scale degree, counted from 1 the way musicians name degrees: `degree(1)`
@@ -3520,6 +3812,11 @@ var Key = class _Key {
3520
3812
  * of C major gives F major. A scale with no diatonic triads — anything that
3521
3813
  * is not heptatonic — falls back to this key's own mode.
3522
3814
  *
3815
+ * The tonic is derived rather than given, so the result is respelled to its
3816
+ * enharmonic key whenever the letter arithmetic lands past the signatures keys
3817
+ * are written with: the fourth degree of a C blues scale reads as F# minor,
3818
+ * not as the Gb minor and its nine flats.
3819
+ *
3523
3820
  * @param n The 1-based scale degree, as {@link Key.degree} counts them.
3524
3821
  * @param mode The mode of the resulting key; inferred when omitted.
3525
3822
  * @returns The key on that degree.
@@ -3569,10 +3866,17 @@ var Key = class _Key {
3569
3866
  );
3570
3867
  return _Key.#modeOn(resolved, tonic);
3571
3868
  }
3572
- /** A plain major or minor key on a spelled tonic. */
3869
+ /**
3870
+ * A plain major or minor key on a spelled tonic.
3871
+ *
3872
+ * The tonic is one this class derived — a scale degree of another key — so the
3873
+ * result is held to the signatures keys are written with: the fourth degree of
3874
+ * a C blues scale is written as an F# minor rather than as the Gb minor its
3875
+ * letter arithmetic gives, which would be nine flats.
3876
+ */
3573
3877
  static #modeOn(mode, tonic) {
3574
3878
  const rootPc = tonic.pitchClass;
3575
- return new _Key(mode === "minor" ? minorKey(rootPc) : majorKey(rootPc), tonic);
3879
+ return new _Key(mode === "minor" ? minorKey(rootPc) : majorKey(rootPc), tonic).#asWritten();
3576
3880
  }
3577
3881
  /**
3578
3882
  * The mode a scale degree implies, read off its diatonic triad. Scales that
@@ -3661,27 +3965,45 @@ var Key = class _Key {
3661
3965
  assertFiniteNumber(semitones, "semitones");
3662
3966
  const rootPc = pitchClassOf(this.#scale.rootPc + Math.round(semitones));
3663
3967
  const scale = { rootPc, modeMask12: this.#scale.modeMask12 };
3664
- const moved = new _Key(scale, this.#tonic.transpose(semitones), this.#variant);
3665
- if (moved.#isWritten()) {
3666
- return moved;
3968
+ return new _Key(scale, this.#tonic.transpose(semitones), this.#variant).#asWritten();
3969
+ }
3970
+ /**
3971
+ * This key respelled to the enharmonic key it would actually be written as,
3972
+ * or itself when it is already written that way.
3973
+ *
3974
+ * What every method that derives a tonic of its own ends on. A derived tonic
3975
+ * can land past the signatures music is written with — nine flats, a scale
3976
+ * needing double accidentals — and a key nobody would hand a player is not an
3977
+ * answer to "which key is this". A key the caller spelled is never put through
3978
+ * this: only a tonic the library chose is the library's to choose again.
3979
+ *
3980
+ * The enharmonic key supplies the tonic spelling only: the scale keeps this
3981
+ * key's own mask, so a harmonic minor or a pentatonic is respelled rather than
3982
+ * flattened into a plain major or minor key. A respelling that does not sound
3983
+ * the same root is no respelling at all, so the key stands — deriving a key is
3984
+ * total, and no spelling question may turn it into a throw.
3985
+ */
3986
+ #asWritten() {
3987
+ if (this.#isWritten()) {
3988
+ return this;
3667
3989
  }
3668
- const other = enharmonicKeyOf(moved.tonic.data, moved.scale);
3990
+ const other = enharmonicKeyOf(this.#tonic.data, this.#scale);
3669
3991
  const respelled = other === null ? null : new Note(other.tonic);
3670
- return respelled === null || respelled.pitchClass !== rootPc ? moved : new _Key(scale, respelled, this.#variant);
3992
+ return respelled === null || respelled.pitchClass !== pitchClassOf(this.#scale.rootPc) ? this : new _Key(this.#scale, respelled, this.#variant);
3671
3993
  }
3672
3994
  /**
3673
3995
  * Whether this key is spelled the way keys are actually written, which is
3674
- * what decides whether {@link Key.transpose} leaves a spelling alone.
3996
+ * what decides whether a key this class derived a tonic for keeps that
3997
+ * spelling — {@link Key.transpose}, {@link Key.keyOnDegree} and
3998
+ * {@link Key.forInstrument} all end on it.
3675
3999
  *
3676
- * A key with a signature of its own is written wherever that signature is,
3677
- * up to seven sharps or flats. A scale that only borrows the signature of its
3678
- * parallel major or minor is written wherever it reads: the borrowed count
3679
- * says nothing — the octatonic scale on Db borrows eight flats and is spelled
3680
- * Db all the same — so what counts there is that no note needs a double
3681
- * accidental.
4000
+ * The theory layer owns the answer and this asks it, so the class and the
4001
+ * functions name the same written spelling — including at the ends of the
4002
+ * church modes, where a window drawn from the major and minor keys and one
4003
+ * drawn from the mode itself disagree.
3682
4004
  */
3683
4005
  #isWritten() {
3684
- return isSignatureKey(this.#scale) ? Math.abs(this.fifths) <= MAX_CONVENTIONAL_FIFTHS : this.notes().every((note) => Math.abs(note.alter) < DOUBLE_ACCIDENTAL2);
4006
+ return isWrittenTonic(this.#tonic.data, this.#scale);
3685
4007
  }
3686
4008
  /**
3687
4009
  * The key a transposing instrument's part is written in, for this key at
@@ -3692,7 +4014,11 @@ var Key = class _Key {
3692
4014
  * lower than it reads — has its part written a major second higher, and a
3693
4015
  * concert C major becomes D major. The tonic is spelled by the interval, so
3694
4016
  * a concert E flat major reads as F major on that instrument rather than as
3695
- * E sharp major, and the mode mask is untouched.
4017
+ * E sharp major, and the mode mask is untouched. What the interval spells is
4018
+ * then respelled to its enharmonic key whenever it would not be written that
4019
+ * way, since a part is what a player is actually handed: a concert F# major
4020
+ * on a B flat clarinet reads as A flat major rather than as a G# major with an
4021
+ * F double sharp in its signature.
3696
4022
  *
3697
4023
  * The opposite reading, a written key back to the key it sounds in, is
3698
4024
  * {@link Key.transposeBy} applied to {@link instrumentTransposition} — the
@@ -3707,6 +4033,7 @@ var Key = class _Key {
3707
4033
  * import { Key, instrumentTransposition } from '@libraz/libcantus';
3708
4034
  * Key.major('C').forInstrument('clarinetBb').toString(); // 'D major'
3709
4035
  * Key.major('C').forInstrument('hornF').toString(); // 'G major'
4036
+ * Key.major('F#').forInstrument('clarinetBb').toString(); // 'Ab major'
3710
4037
  * // And back: what a part written in C major on a clarinet in A sounds as.
3711
4038
  * Key.major('C').transposeBy(instrumentTransposition('clarinetA')).toString(); // 'A major'
3712
4039
  * ```
@@ -3717,7 +4044,7 @@ var Key = class _Key {
3717
4044
  { rootPc: tonic.pitchClass, modeMask12: this.#scale.modeMask12 },
3718
4045
  tonic,
3719
4046
  this.#variant
3720
- );
4047
+ ).#asWritten();
3721
4048
  }
3722
4049
  /**
3723
4050
  * The spelled scale, one note per degree (e.g. C D E F G A B for C major).
@@ -3835,7 +4162,28 @@ var Key = class _Key {
3835
4162
  * @throws If the numeral is not valid.
3836
4163
  */
3837
4164
  roman(text) {
3838
- return new Chord(romanToChord(text, this.#scale), this);
4165
+ return new Chord(romanToChord(text, this), this);
4166
+ }
4167
+ /**
4168
+ * The augmented sixth chord of a kind, built in this key.
4169
+ *
4170
+ * The three kinds share the augmented sixth between the lowered sixth degree
4171
+ * and the raised fourth and differ in what fills it: the Italian doubles the
4172
+ * tonic, the French adds the second degree, and the German the third. The
4173
+ * chord is spelled as the interval demands rather than as its enharmonic
4174
+ * dominant seventh, which is what keeps its outward resolution readable.
4175
+ *
4176
+ * @param kind Which augmented sixth to build.
4177
+ * @returns The chord, with this key attached.
4178
+ * @throws If the kind names none of the three.
4179
+ * @example
4180
+ * ```ts
4181
+ * import { Key } from '@libraz/libcantus';
4182
+ * Key.major('C').augmentedSixth('german').rootPc; // 8 (the lowered sixth degree)
4183
+ * ```
4184
+ */
4185
+ augmentedSixth(kind) {
4186
+ return new Chord(augmentedSixthChord(kind, keyIdentity(this)), this);
3839
4187
  }
3840
4188
  /**
3841
4189
  * Build a progression from Roman numerals in this key.
@@ -3921,16 +4269,23 @@ var Key = class _Key {
3921
4269
  * one.
3922
4270
  */
3923
4271
  toJSON() {
3924
- return this.#variant === void 0 ? { scale: this.scale, tonic: this.#tonic.data } : { scale: this.scale, tonic: this.#tonic.data, variant: this.#variant };
4272
+ return { scale: this.scale, tonic: this.#tonic.data, variant: this.#variant };
3925
4273
  }
3926
4274
  /**
3927
- * The key's tonic and mode, so a template literal or a log line reads as the
3928
- * key. Detected harmonic and melodic minor keys retain their scale form.
4275
+ * The key's tonic and the scale it is read in, so a template literal or a log
4276
+ * line reads as the key.
4277
+ *
4278
+ * The scale word is a function of the scale alone — its root pitch class and
4279
+ * mode mask — rather than of how the key was built, so two keys holding the
4280
+ * same scale print the same name: a harmonic minor names itself one whether
4281
+ * it was detected or asked for by name, and a mode names its mode instead of
4282
+ * the major or minor key its third would make it. What is printed is what
4283
+ * {@link Key.parse} reads back.
3929
4284
  *
3930
4285
  * A `system` names the key the way that notation system writes it, including
3931
- * German's case convention. The scale form is an English-only qualifier —
3932
- * the other systems have no word for it — so a detected harmonic minor names
3933
- * its parallel plain minor there.
4286
+ * German's case convention. The scale word is an English-only qualifier —
4287
+ * the other systems have no word for it — so a harmonic minor names its
4288
+ * parallel plain minor there.
3934
4289
  *
3935
4290
  * @param opts `system` writes the name in that notation system instead of
3936
4291
  * English.
@@ -3939,8 +4294,8 @@ var Key = class _Key {
3939
4294
  * ```ts
3940
4295
  * import { Key } from '@libraz/libcantus';
3941
4296
  * Key.minor('G#').toString(); // 'G# minor'
4297
+ * Key.named('dorian', 'D').toString(); // 'D dorian'
3942
4298
  * Key.minor('G#').toString({ system: 'german' }); // 'gis moll'
3943
- * Key.minor('G#').toString({ system: 'japanese' }); // '嬰ト短調'
3944
4299
  * ```
3945
4300
  */
3946
4301
  toString(opts) {
@@ -3950,32 +4305,40 @@ var Key = class _Key {
3950
4305
  opts
3951
4306
  );
3952
4307
  }
3953
- if (this.#variant === "harmonic" || this.#variant === "melodic") {
3954
- return `${this.#tonic.name} ${this.#variant} minor`;
3955
- }
3956
- return `${this.#tonic.name} ${this.isMinor ? "minor" : "major"}`;
4308
+ return `${this.#tonic.name} ${scaleWord(this.#scale.modeMask12, this.isMinor)}`;
3957
4309
  }
3958
4310
  };
4311
+ function toKey(value) {
4312
+ if (typeof value === "string") {
4313
+ return Key.parse(value);
4314
+ }
4315
+ if (typeof value === "object" && value !== null) {
4316
+ const data = "toJSON" in value && typeof value.toJSON === "function" ? value.toJSON() : value;
4317
+ if (typeof data === "object" && data !== null) {
4318
+ const record = data;
4319
+ if (record.scale === void 0) {
4320
+ const scale = toKeyScale(data);
4321
+ return record.tonic === void 0 ? new Key(scale, spelledTonicFor(scale), record.variant) : new Key(scale, new Note(record.tonic), record.variant);
4322
+ }
4323
+ return record.tonic === void 0 ? Key.of(toKeyScale(record.scale)) : Key.fromJSON(record);
4324
+ }
4325
+ }
4326
+ throw new InvalidInputError(`key must be a key name or a key/scale; received ${typeof value}`);
4327
+ }
4328
+ function keyIdentity(key) {
4329
+ return { scale: key.scale, tonic: key.tonic.data, variant: key.variant };
4330
+ }
3959
4331
 
3960
4332
  // src/model/score.ts
3961
4333
  var DEFAULT_BPM = 120;
3962
4334
  function orderNotes(notes, name) {
3963
- if (!Array.isArray(notes)) {
3964
- throw new InvalidInputError(`${name} must be an array; received ${typeof notes}`);
3965
- }
3966
- return notes.map((note, index) => {
3967
- if (note === void 0) {
3968
- throw new InvalidInputError(`${name}[${index}] must be a note event; received undefined`);
3969
- }
3970
- return copyNoteEvent(
3971
- assertNoteEvent(note, `${name}[${index}]`, { allowNonPositiveDuration: true })
3972
- );
3973
- }).sort(
4335
+ return assertNoteEventArray(notes, name, { allowNonPositiveDuration: true }).map((note) => copyNoteEvent(note)).sort(
3974
4336
  (a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch || a.durationBeat - b.durationBeat
3975
4337
  );
3976
4338
  }
3977
4339
  function copyMeters(meters) {
3978
- return meters.map((change) => {
4340
+ return assertDataObjects(meters, "score meters").map((change) => {
4341
+ assertDataObject(change.ts, "score meters ts");
3979
4342
  const ts = {
3980
4343
  numerator: change.ts.numerator,
3981
4344
  denominator: change.ts.denominator
@@ -3987,6 +4350,7 @@ function copyMeters(meters) {
3987
4350
  });
3988
4351
  }
3989
4352
  function copyTempo(tempo) {
4353
+ assertDataObjects(tempo, "score tempo");
3990
4354
  tempoAt(0, tempo);
3991
4355
  return tempo.map((event) => ({
3992
4356
  startBeat: withoutNegativeZero(event.startBeat),
@@ -3994,6 +4358,7 @@ function copyTempo(tempo) {
3994
4358
  }));
3995
4359
  }
3996
4360
  function copyScore(data) {
4361
+ assertDataObject(data, "score data");
3997
4362
  const copy = {
3998
4363
  notes: orderNotes(data.notes, "score notes"),
3999
4364
  // The map is validated by the same resolver every meter-aware entry point
@@ -4006,33 +4371,46 @@ function copyScore(data) {
4006
4371
  }
4007
4372
  return copy;
4008
4373
  }
4009
- function metersFrom(meters) {
4010
- if (meters === void 0) {
4011
- return resolveMeters({}, "score meters");
4012
- }
4013
- const meter = toMeterData(meters, "score meters");
4014
- return Array.isArray(meter) ? resolveMeters({ meters: meter }, "score meters") : resolveMeters({ ts: meter }, "score meters");
4015
- }
4016
4374
  function tempoFrom(tempo) {
4017
4375
  if (tempo === void 0) {
4018
4376
  return [{ startBeat: 0, bpm: DEFAULT_BPM }];
4019
4377
  }
4020
4378
  return typeof tempo === "number" ? [{ startBeat: 0, bpm: tempo }] : tempo;
4021
4379
  }
4380
+ function statedKeyRegions(key, notes, totalBeats) {
4381
+ const first = notes[0];
4382
+ if (first === void 0 || totalBeats <= first.startBeat) {
4383
+ return [];
4384
+ }
4385
+ return [
4386
+ {
4387
+ // The region starts where the music does, as an inferred one does, so a
4388
+ // pickup is covered by the key it sounds in.
4389
+ startBeat: first.startBeat,
4390
+ endBeat: totalBeats,
4391
+ key: keyIdentity(key),
4392
+ confidence: STATED_KEY_CONFIDENCE
4393
+ }
4394
+ ];
4395
+ }
4022
4396
  function scoreData(notes, opts) {
4023
4397
  const data = {
4024
- notes: [...notes],
4025
- meters: metersFrom(opts?.meters),
4398
+ notes: [...assertDataArray(notes, "score notes")],
4399
+ meters: resolveMeters({ meters: opts?.meters }, "score meters"),
4026
4400
  tempo: tempoFrom(opts?.tempo)
4027
4401
  };
4028
4402
  if (opts?.key !== void 0) {
4029
- data.key = Key.of(toKeyScale(opts.key)).toJSON();
4403
+ data.key = toKey(opts.key).toJSON();
4030
4404
  }
4031
4405
  return data;
4032
4406
  }
4033
4407
  var Score = class _Score {
4034
4408
  #data;
4035
4409
  #key;
4410
+ /** The chord reading, made on the first member that needs it. */
4411
+ #chords;
4412
+ /** The key regions, made on the first member that needs them. */
4413
+ #regions;
4036
4414
  /**
4037
4415
  * Wrap plain score data.
4038
4416
  *
@@ -4091,7 +4469,7 @@ var Score = class _Score {
4091
4469
  * ```
4092
4470
  */
4093
4471
  static fromTicks(events, ppq, opts) {
4094
- const notes = events.map((event) => ({
4472
+ const notes = assertDataObjects(events, "score notes").map((event) => ({
4095
4473
  ...event,
4096
4474
  startBeat: ticksToBeats(event.startBeat, ppq),
4097
4475
  durationBeat: ticksToBeats(event.durationBeat, ppq)
@@ -4184,14 +4562,20 @@ var Score = class _Score {
4184
4562
  /**
4185
4563
  * The score moved by a number of semitones.
4186
4564
  *
4565
+ * A carried key moves with the notes, so the score that comes back is read in
4566
+ * the key it now sounds in and every analysis taken from it — its timeline,
4567
+ * its numerals, its phrases — is taken against that key rather than against
4568
+ * the one it was written in.
4569
+ *
4187
4570
  * @param semitones The signed semitone offset.
4188
4571
  * @returns The transposed score.
4189
4572
  * @throws If a note would leave the MIDI range.
4190
4573
  */
4191
4574
  transpose(semitones) {
4192
4575
  assertFiniteNumber(semitones, "transpose semitones");
4193
- return this.#withNotes(
4194
- this.#data.notes.map((note) => ({ ...note, pitch: note.pitch + semitones }))
4576
+ return this.#moved(
4577
+ this.#data.notes.map((note) => ({ ...note, pitch: note.pitch + semitones })),
4578
+ (key) => key.transpose(semitones)
4195
4579
  );
4196
4580
  }
4197
4581
  /**
@@ -4199,18 +4583,27 @@ var Score = class _Score {
4199
4583
  *
4200
4584
  * Note events carry a MIDI pitch and no spelling of their own, so the
4201
4585
  * interval contributes its size and its direction; a carried key is what
4202
- * keeps the spelling, and it moves with the notes.
4586
+ * keeps the spelling, and it moves with the notes — by the interval itself,
4587
+ * so an augmented fourth and a diminished fifth land on differently spelled
4588
+ * keys.
4203
4589
  *
4204
4590
  * @param interval An interval name (e.g. `'A4'`, `'-m3'`), plain interval
4205
4591
  * data, or an {@link Interval}; a descending interval moves down.
4206
4592
  * @returns The transposed score.
4207
4593
  */
4208
4594
  transposeBy(interval) {
4209
- return this.transpose(toSpelledInterval(interval).semitones);
4595
+ const step = toSpelledInterval(interval);
4596
+ return this.#moved(
4597
+ this.#data.notes.map((note) => ({ ...note, pitch: note.pitch + step.semitones })),
4598
+ // Moved by the interval rather than by its semitone count, so a key taken
4599
+ // up an augmented fourth and one taken up a diminished fifth are spelled
4600
+ // the way each interval demands.
4601
+ (key) => key.transposeBy(step)
4602
+ );
4210
4603
  }
4211
4604
  /** The same notes read against a different meter. */
4212
4605
  withMeters(meters) {
4213
- return new _Score({ ...this.#data, meters: metersFrom(meters) });
4606
+ return new _Score({ ...this.#data, meters: resolveMeters({ meters }, "score meters") });
4214
4607
  }
4215
4608
  /** The same notes read against a different tempo. */
4216
4609
  withTempo(tempo) {
@@ -4218,7 +4611,7 @@ var Score = class _Score {
4218
4611
  }
4219
4612
  /** The same notes read in a different key. */
4220
4613
  withKey(key) {
4221
- return new _Score({ ...this.#data, key: Key.of(toKeyScale(key)).toJSON() });
4614
+ return new _Score({ ...this.#data, key: toKey(key).toJSON() });
4222
4615
  }
4223
4616
  /**
4224
4617
  * The notes pulled onto a grid.
@@ -4324,7 +4717,7 @@ var Score = class _Score {
4324
4717
  * and how sure it is of each segment.
4325
4718
  */
4326
4719
  timeline(opts) {
4327
- const result = chordTimelineFromNotes(this.#data.notes, this.#analysisOptions(opts));
4720
+ const result = opts === void 0 ? this.#chordAnalysis() : chordTimelineFromNotes(this.#data.notes, this.#analysisOptions(opts));
4328
4721
  return Timeline.fromData({
4329
4722
  segments: result.timeline.segments,
4330
4723
  totalBeats: opts?.totalBeats ?? this.totalBeats,
@@ -4347,16 +4740,29 @@ var Score = class _Score {
4347
4740
  return this.#key;
4348
4741
  }
4349
4742
  const prevailing = prevailingKeyOf(this.#keyRegions());
4350
- return prevailing === null ? void 0 : Key.of(prevailing);
4743
+ return prevailing === null ? void 0 : toKey(prevailing);
4351
4744
  }
4352
4745
  /**
4353
4746
  * Every key region the score passes through, in time order.
4354
4747
  *
4748
+ * A score that was told its key answers with that key across the whole of it,
4749
+ * as {@link Score.key} and {@link Score.timeline} do: the caller has already
4750
+ * answered the question, and searching for a modulation away from a stated
4751
+ * key would make the option read as a hint where it was given as an
4752
+ * instruction. Only `totalBeats` is read from the options there, since the
4753
+ * rest tune a search that is not run.
4754
+ *
4355
4755
  * @param opts Analysis options; see {@link KeyTimelineOptions}.
4356
4756
  * @returns The regions; empty when nothing sounds.
4357
4757
  */
4358
4758
  keys(opts) {
4359
- return keyTimelineFromNotes(this.#data.notes, { meters: this.#data.meters, ...opts });
4759
+ if (opts !== void 0) {
4760
+ if (this.#key !== void 0) {
4761
+ return statedKeyRegions(this.#key, this.#data.notes, opts.totalBeats ?? this.totalBeats);
4762
+ }
4763
+ return keyTimelineFromNotes(this.#data.notes, { meters: this.#data.meters, ...opts });
4764
+ }
4765
+ return this.#keyRegions().map(copyKeyRegion);
4360
4766
  }
4361
4767
  /**
4362
4768
  * Every key the score could be in, ranked, read as one key across the whole
@@ -4382,7 +4788,7 @@ var Score = class _Score {
4382
4788
  * ```
4383
4789
  */
4384
4790
  detectKeys(opts) {
4385
- return detectKeyFromNotes(this.#data.notes, opts);
4791
+ return detectKeyFromNotes(this.#data.notes, opts).map(detectedKeyMatch);
4386
4792
  }
4387
4793
  /**
4388
4794
  * The phrases the notes fall into.
@@ -4396,7 +4802,7 @@ var Score = class _Score {
4396
4802
  phrases(opts) {
4397
4803
  return phrasesFromTimeline(this.#chordTimeline(), this.#data.notes, {
4398
4804
  meters: this.#data.meters,
4399
- ...this.#key === void 0 ? {} : { key: this.#key.scale },
4805
+ ...this.#key === void 0 ? {} : { key: keyIdentity(this.#key) },
4400
4806
  ...opts
4401
4807
  });
4402
4808
  }
@@ -4428,11 +4834,18 @@ var Score = class _Score {
4428
4834
  /**
4429
4835
  * The bar-level pulse above the meter.
4430
4836
  *
4837
+ * The cadences come from the score's own harmony unless the caller passes
4838
+ * their own, so this reads the same grouping {@link Score.phrases} builds its
4839
+ * phrases on rather than a second one that ignores where the music closes.
4840
+ *
4431
4841
  * @param opts Analysis options; see {@link HypermeterOptions}.
4432
4842
  * @returns The grouping, its downbeats, a confidence, and a rationale.
4433
4843
  */
4434
4844
  hypermeter(opts) {
4435
- return hypermeter(this.#data.notes, this.#data.meters, opts);
4845
+ return hypermeter(this.#data.notes, this.#data.meters, {
4846
+ cadenceBeats: this.#cadenceBeats(),
4847
+ ...opts
4848
+ });
4436
4849
  }
4437
4850
  /**
4438
4851
  * The motifs the melody repeats.
@@ -4445,6 +4858,12 @@ var Score = class _Score {
4445
4858
  * A record is a cell, so `Motif.fromData(found)` reaches the
4446
4859
  * transformations without anything being rebuilt by hand.
4447
4860
  *
4861
+ * A melody is one line, so a score holding several is read as its top voice:
4862
+ * notes struck together are one event and the highest of them stands for it.
4863
+ * A piano part read whole therefore answers for the voice a listener follows
4864
+ * rather than for a line assembled from the insides of its chords. Split the
4865
+ * score into lines first to have each of them answered for.
4866
+ *
4448
4867
  * @param opts Cell-length bounds and the recurrence threshold; see
4449
4868
  * {@link ExtractMotifsOptions}.
4450
4869
  * @returns The motifs found, longest first.
@@ -4467,7 +4886,15 @@ var Score = class _Score {
4467
4886
  motifs(opts) {
4468
4887
  return extractMotifs(this.#data.notes, opts);
4469
4888
  }
4470
- /** The shape the melody traces. */
4889
+ /**
4890
+ * The shape the melody traces.
4891
+ *
4892
+ * A contour is the shape of one line, so a score holding several is read as
4893
+ * its top voice: notes struck together are one event and the highest of them
4894
+ * stands for it, which keeps a chord from being measured as a climb through
4895
+ * its own notes. Split the score into lines first to have each of them
4896
+ * answered for; {@link Score.voices} is what reads the polyphony as polyphony.
4897
+ */
4471
4898
  contour() {
4472
4899
  return melodicContour(this.#data.notes);
4473
4900
  }
@@ -4481,12 +4908,19 @@ var Score = class _Score {
4481
4908
  * chord — reads every note against C major and no chord at all, which labels
4482
4909
  * the notes as the non-chord tones they are rather than failing.
4483
4910
  *
4484
- * @param key The key to read the notes in; defaults to the score's own.
4911
+ * A score holds a whole piece rather than a single line, so the notes are read
4912
+ * as the polyphony they are: each note is classified in its own voice, against
4913
+ * everything else sounding under it. That is what a suspension needs — a
4914
+ * dissonance is dissonant against something — and it is what keeps a note of
4915
+ * one voice from being heard as the passing tone of another.
4916
+ *
4917
+ * @param key The key to read the notes in, as a key name, a plain key/scale,
4918
+ * or a {@link Key}; defaults to the score's own.
4485
4919
  * @returns One annotation per note, in the score's own time order.
4486
4920
  */
4487
4921
  voices(key) {
4488
4922
  const timeline = this.#chordTimeline();
4489
- return analyzeVoice(this.#data.notes, timeline.at, this.#keyContext(key));
4923
+ return analyzePolyphony(this.#data.notes, timeline.at, this.#keyContext(key));
4490
4924
  }
4491
4925
  /**
4492
4926
  * The notes carrying the ids the voice analysis reports them back under.
@@ -4505,7 +4939,11 @@ var Score = class _Score {
4505
4939
  /**
4506
4940
  * Whether an instrument can play the score, and what makes it hard.
4507
4941
  *
4508
- * The tempo the third layer needs is the score's own, read at beat 0.
4942
+ * The third layer asks how much time separates two notes, and the score
4943
+ * answers it from its whole tempo map: a passage under an accelerando is
4944
+ * judged at the tempo actually in force across it, the same integral
4945
+ * {@link Score.secondsAt} reports. The beats the issues name are the score's
4946
+ * own.
4509
4947
  *
4510
4948
  * @param instrument The instrument to play it on: a plain
4511
4949
  * {@link InstrumentProfile} or an {@link Instrument}.
@@ -4520,11 +4958,33 @@ var Score = class _Score {
4520
4958
  * ```
4521
4959
  */
4522
4960
  playability(instrument) {
4523
- return playability(
4524
- this.#data.notes,
4525
- toInstrumentProfile(instrument),
4526
- tempoAt(0, this.#data.tempo)
4527
- );
4961
+ const profile = toInstrumentProfile(instrument);
4962
+ const tempo = this.#data.tempo;
4963
+ const openingBpm = tempoAt(0, tempo);
4964
+ if (tempo.length < 2) {
4965
+ return playability(this.#data.notes, profile, openingBpm);
4966
+ }
4967
+ const played = tempoReader(tempo);
4968
+ const evenly = tempoReader([{ startBeat: 0, bpm: openingBpm }]);
4969
+ const restate = (beat) => evenly.beatAt(played.secondsAt(beat));
4970
+ const scoreBeats = /* @__PURE__ */ new Map();
4971
+ const evenlyPaced = this.#data.notes.map((note) => {
4972
+ const startBeat = restate(note.startBeat);
4973
+ scoreBeats.set(startBeat, note.startBeat);
4974
+ return {
4975
+ ...note,
4976
+ startBeat,
4977
+ durationBeat: restate(note.startBeat + note.durationBeat) - startBeat
4978
+ };
4979
+ });
4980
+ const report = playability(evenlyPaced, profile, openingBpm);
4981
+ return {
4982
+ ...report,
4983
+ issues: report.issues.map((issue) => ({
4984
+ ...issue,
4985
+ startBeat: scoreBeats.get(issue.startBeat) ?? issue.startBeat
4986
+ }))
4987
+ };
4528
4988
  }
4529
4989
  /**
4530
4990
  * An index for repeated lookups over the notes.
@@ -4622,24 +5082,64 @@ var Score = class _Score {
4622
5082
  return new _Score({ ...this.#data, notes: [...notes] });
4623
5083
  }
4624
5084
  /**
4625
- * The chord timeline the score's own harmony describes.
5085
+ * The same context carrying another set of notes, with a carried key moved
5086
+ * the same way the notes were.
5087
+ *
5088
+ * Every path that moves the pitches goes through here, so a score cannot come
5089
+ * back sounding in one key and analysed in another.
5090
+ */
5091
+ #moved(notes, moveKey) {
5092
+ const data = { ...this.#data, notes: [...notes] };
5093
+ if (this.#key !== void 0) {
5094
+ data.key = moveKey(this.#key).toJSON();
5095
+ }
5096
+ return new _Score(data);
5097
+ }
5098
+ /**
5099
+ * The chord timeline the score's own harmony describes, read once.
4626
5100
  *
4627
5101
  * Private, because the options belong to the members that expose them: the
4628
5102
  * analyses that read harmony read the score's, and a caller who wants
4629
- * another builds it through {@link Score.timeline}.
5103
+ * another builds it through {@link Score.timeline}. The reading is kept
5104
+ * because a score is immutable and this takes no options: chord inference is
5105
+ * the most expensive thing the class does, and the natural order — timeline,
5106
+ * phrases, cadences, voices — would otherwise ask for it once per member.
4630
5107
  */
4631
5108
  #chordTimeline() {
4632
- return chordTimelineFromNotes(this.#data.notes, this.#analysisOptions()).timeline;
5109
+ return this.#chordAnalysis().timeline;
5110
+ }
5111
+ /**
5112
+ * The whole reading, not only the timeline it carries: the key regions and
5113
+ * the per-segment confidence come from the same pass, and {@link
5114
+ * Score.timeline} hands all three to the class it builds.
5115
+ */
5116
+ #chordAnalysis() {
5117
+ const analysis = this.#chords ?? chordTimelineFromNotes(this.#data.notes, this.#analysisOptions());
5118
+ this.#chords = analysis;
5119
+ return analysis;
4633
5120
  }
4634
- /** The key regions under the score, read against its own meter. */
5121
+ /**
5122
+ * The key regions under the score: the key it was told it is in across the
5123
+ * whole of it, or the ones read from the notes against its own meter.
5124
+ *
5125
+ * Kept on first reading for the same reason the chord timeline is.
5126
+ */
4635
5127
  #keyRegions() {
4636
- return keyTimelineFromNotes(this.#data.notes, { meters: this.#data.meters });
5128
+ const regions = this.#regions ?? (this.#key === void 0 ? keyTimelineFromNotes(this.#data.notes, { meters: this.#data.meters }) : statedKeyRegions(this.#key, this.#data.notes, this.totalBeats));
5129
+ this.#regions = regions;
5130
+ return regions;
5131
+ }
5132
+ /** Where the score's own harmony closes, as beats. */
5133
+ #cadenceBeats() {
5134
+ return detectCadences(this.#chordTimeline(), this.#keyContext(void 0)).map(
5135
+ (hit) => hit.atBeat
5136
+ );
4637
5137
  }
4638
5138
  /** The score's context as chord-timeline options, with the caller's on top. */
4639
5139
  #analysisOptions(opts) {
4640
5140
  return {
4641
5141
  meters: this.#data.meters,
4642
- ...this.#key === void 0 ? {} : { key: this.#key.scale },
5142
+ ...this.#key === void 0 ? {} : { key: keyIdentity(this.#key) },
4643
5143
  ...opts
4644
5144
  };
4645
5145
  }
@@ -4648,15 +5148,17 @@ var Score = class _Score {
4648
5148
  * then the key in force at the beat, and C major where nothing sounds.
4649
5149
  */
4650
5150
  #keyContext(key) {
5151
+ assertKeyArgument(key, "score key");
4651
5152
  if (key !== void 0) {
4652
- return toKeyScale(key);
5153
+ return keyIdentity(toKey(key));
4653
5154
  }
4654
5155
  if (this.#key !== void 0) {
4655
- return this.#key.scale;
5156
+ return keyIdentity(this.#key);
4656
5157
  }
4657
5158
  const regions = this.#keyRegions();
4658
- const prevailing = prevailingKeyOf(regions) ?? Key.major("C").scale;
4659
- return keyLookup(regions, prevailing);
5159
+ const prevailing = prevailingKeyOf(regions) ?? keyIdentity(Key.major("C"));
5160
+ const keyAt = keyLookup(regions, prevailing);
5161
+ return (beat) => scaleOf(keyAt(beat));
4660
5162
  }
4661
5163
  };
4662
5164
  function sameNote(note, other) {
@@ -4678,52 +5180,31 @@ function sameKey(key, other) {
4678
5180
  }
4679
5181
 
4680
5182
  // src/model/arrangement.ts
4681
- function copyPlain(value) {
4682
- if (Array.isArray(value)) {
4683
- return value.map((item) => copyPlain(item));
4684
- }
4685
- if (typeof value === "object" && value !== null) {
4686
- const copy = {};
4687
- for (const [key, item] of Object.entries(value)) {
4688
- copy[key] = copyPlain(item);
4689
- }
4690
- return copy;
4691
- }
4692
- return value;
4693
- }
4694
- function copyNote2(note, name) {
4695
- assertNoteEvent(note, name, { allowNonPositiveDuration: true });
4696
- return copyNoteEvent(note);
4697
- }
4698
- function copyNotes(notes, name) {
4699
- if (!Array.isArray(notes)) {
4700
- throw new InvalidInputError(`${name} must be an array; received ${typeof notes}`);
4701
- }
4702
- return notes.map((note, index) => {
4703
- if (note === void 0) {
4704
- throw new InvalidInputError(`${name}[${index}] must be a note event; received undefined`);
4705
- }
4706
- return copyNote2(note, `${name}[${index}]`);
4707
- });
5183
+ function copyNotes(notes, name, budget) {
5184
+ return assertNoteEventArray(notes, name, { allowNonPositiveDuration: true, budget }).map(
5185
+ copyNoteEvent
5186
+ );
4708
5187
  }
4709
- function copyTrack(track, index) {
4710
- if (track === void 0) {
4711
- throw new InvalidInputError(`tracks[${index}] must be a track; received undefined`);
4712
- }
4713
- const copy = { notes: copyNotes(track.notes, `tracks[${index}].notes`) };
5188
+ function copyTrack(track, index, budget) {
5189
+ assertDataObject(track, `tracks[${index}]`);
5190
+ const copy = {
5191
+ notes: copyNotes(track.notes, `tracks[${index}].notes`, budget)
5192
+ };
4714
5193
  if (track.name !== void 0) {
4715
5194
  copy.name = track.name;
4716
5195
  }
4717
5196
  if (track.role !== void 0) {
4718
- copy.role = track.role;
5197
+ copy.role = trackRoleOf(track.role, `tracks[${index}].role`);
4719
5198
  }
4720
5199
  return copy;
4721
5200
  }
4722
5201
  function copyKeyRegions(keys) {
4723
- return [...new Timeline({ segments: [], totalBeats: 0, keys: [...keys] }).keys];
5202
+ const given = assertDataObjects(keys, "arrangement keys");
5203
+ return [...new Timeline({ segments: [], totalBeats: 0, keys: [...given] }).keys];
4724
5204
  }
4725
5205
  function copySegments(segments) {
4726
- return [...new Timeline({ segments: [...segments], totalBeats: spanEnd(segments) }).segments];
5206
+ const given = assertDataObjects(segments, "arrangement timeline");
5207
+ return [...new Timeline({ segments: [...given], totalBeats: spanEnd(given) }).segments];
4727
5208
  }
4728
5209
  function chordTimelineOf(segments) {
4729
5210
  return new Timeline({ segments: [...segments], totalBeats: spanEnd(segments) }).chordTimeline;
@@ -4732,16 +5213,17 @@ function copySettings(settings, trackCount) {
4732
5213
  if (settings === void 0) {
4733
5214
  return void 0;
4734
5215
  }
5216
+ assertDataObject(settings, "arrangement settings");
4735
5217
  const copy = {};
4736
5218
  const meters = resolveMeters({ ts: settings.ts, meters: settings.meters }, "arrangement meters");
4737
5219
  if (settings.ts !== void 0) {
4738
- copy.ts = copyPlain(settings.ts);
5220
+ copy.ts = copyPlain(toMeterData(settings.ts, "arrangement ts"), "arrangement ts");
4739
5221
  }
4740
5222
  if (settings.meters !== void 0) {
4741
- copy.meters = copyPlain(meters);
5223
+ copy.meters = copyPlain(meters, "arrangement meters");
4742
5224
  }
4743
5225
  if (settings.key !== void 0) {
4744
- copy.key = toKeyScale(settings.key);
5226
+ copy.key = resolveKey(settings.key);
4745
5227
  }
4746
5228
  if (settings.keys !== void 0) {
4747
5229
  copy.keys = copyKeyRegions(settings.keys);
@@ -4750,7 +5232,10 @@ function copySettings(settings, trackCount) {
4750
5232
  copy.timeline = copySegments(settings.timeline);
4751
5233
  }
4752
5234
  if (settings.harmonyTracks !== void 0) {
4753
- copy.harmonyTracks = settings.harmonyTracks.map(
5235
+ copy.harmonyTracks = assertDataArray(
5236
+ settings.harmonyTracks,
5237
+ "arrangement harmonyTracks"
5238
+ ).map(
4754
5239
  (track, index) => assertInteger(track, `arrangement harmonyTracks[${index}]`, 0, trackCount - 1)
4755
5240
  );
4756
5241
  }
@@ -4787,40 +5272,41 @@ function copySettings(settings, trackCount) {
4787
5272
  return Object.keys(copy).length === 0 ? void 0 : copy;
4788
5273
  }
4789
5274
  function copyArrangement(data) {
4790
- if (!Array.isArray(data.tracks)) {
4791
- throw new InvalidInputError(
4792
- `arrangement tracks must be an array; received ${typeof data.tracks}`
4793
- );
4794
- }
4795
- const tracks = data.tracks.map(copyTrack);
5275
+ assertDataObject(data, "arrangement data");
5276
+ const budget = data.settings?.budget;
5277
+ const tracks = assertDataArray(data.tracks, "arrangement tracks").map(
5278
+ (track, index) => copyTrack(track, index, budget)
5279
+ );
4796
5280
  const settings = copySettings(data.settings, tracks.length);
4797
5281
  return settings === void 0 ? { tracks } : { tracks, settings };
4798
5282
  }
4799
5283
  function arrangementData(tracks, settings) {
4800
- const data = { tracks: [...tracks] };
5284
+ const data = {
5285
+ tracks: [...assertDataArray(tracks, "tracks")]
5286
+ };
4801
5287
  if (settings !== void 0) {
4802
5288
  data.settings = settings;
4803
5289
  }
4804
5290
  return data;
4805
5291
  }
4806
- function metersFrom2(meters) {
4807
- const meter = toMeterData(meters, "arrangement meters");
4808
- return Array.isArray(meter) ? resolveMeters({ meters: meter }, "arrangement meters") : resolveMeters({ ts: meter }, "arrangement meters");
4809
- }
4810
5292
  function settingsFrom(setup) {
4811
5293
  if (setup === void 0) {
4812
5294
  return void 0;
4813
5295
  }
5296
+ assertDataObject(setup, "arrangement options");
4814
5297
  const { key, meters, timeline, ...rest } = setup;
4815
5298
  const settings = { ...rest };
4816
5299
  if (key !== void 0) {
4817
- settings.key = toKeyScale(key);
5300
+ settings.key = resolveKey(key);
4818
5301
  }
4819
5302
  if (meters !== void 0) {
4820
- settings.meters = metersFrom2(meters);
5303
+ settings.meters = resolveMeters({ meters }, "arrangement meters");
4821
5304
  }
4822
5305
  if (timeline !== void 0) {
4823
- settings.timeline = [...timeline.segments];
5306
+ assertDataObject(timeline, "arrangement timeline");
5307
+ settings.timeline = [
5308
+ ...assertDataArray(timeline.segments, "arrangement timeline segments")
5309
+ ];
4824
5310
  }
4825
5311
  return settings;
4826
5312
  }
@@ -4828,13 +5314,14 @@ function analysisOptionsOf(setup) {
4828
5314
  if (setup === void 0) {
4829
5315
  return {};
4830
5316
  }
5317
+ assertDataObject(setup, "arrangement options");
4831
5318
  const { key, meters, ...rest } = setup;
4832
5319
  const opts = { ...rest };
4833
5320
  if (key !== void 0) {
4834
- opts.key = toKeyScale(key);
5321
+ opts.key = resolveKey(key);
4835
5322
  }
4836
5323
  if (meters !== void 0) {
4837
- opts.meters = metersFrom2(meters);
5324
+ opts.meters = resolveMeters({ meters }, "arrangement meters");
4838
5325
  }
4839
5326
  return opts;
4840
5327
  }
@@ -4897,7 +5384,7 @@ var Arrangement = class _Arrangement {
4897
5384
  }
4898
5385
  /** The tracks as they were given, in that order. */
4899
5386
  get tracks() {
4900
- return copyPlain(this.#data.tracks);
5387
+ return copyPlain(this.#data.tracks, "arrangement tracks");
4901
5388
  }
4902
5389
  /**
4903
5390
  * The reading of the whole arrangement: the key regions, the harmony, the
@@ -4907,7 +5394,7 @@ var Arrangement = class _Arrangement {
4907
5394
  */
4908
5395
  get analysis() {
4909
5396
  const { timeline, ...rest } = this.#session().analysis;
4910
- return { ...copyPlain(rest), timeline: this.timeline().chordTimeline };
5397
+ return { ...copyPlain(rest, "arrangement analysis"), timeline: this.timeline().chordTimeline };
4911
5398
  }
4912
5399
  /**
4913
5400
  * The notes clashing with the harmony sounding beneath them, worst severity
@@ -4919,7 +5406,7 @@ var Arrangement = class _Arrangement {
4919
5406
  * labels to tell those apart.
4920
5407
  */
4921
5408
  get conflicts() {
4922
- return copyPlain(this.#session().analysis.conflicts);
5409
+ return copyPlain(this.#session().analysis.conflicts, "arrangement conflicts");
4923
5410
  }
4924
5411
  /**
4925
5412
  * One track's notes as a score, or undefined when no track carries the name.
@@ -4976,7 +5463,7 @@ var Arrangement = class _Arrangement {
4976
5463
  }
4977
5464
  copied.push({
4978
5465
  trackIndex: edit.trackIndex,
4979
- notes: copyNotes(edit.notes, `edits[${index}].notes`)
5466
+ notes: copyNotes(edit.notes, `edits[${index}].notes`, this.#data.settings?.budget)
4980
5467
  });
4981
5468
  }
4982
5469
  const session = this.#session().update(copied);
@@ -4991,6 +5478,10 @@ var Arrangement = class _Arrangement {
4991
5478
  * annotations describe the same chords rather than two readings of the same
4992
5479
  * notes.
4993
5480
  *
5481
+ * Naming a key, its regions, or the tracks the harmony is read from asks for
5482
+ * a harmony this arrangement did not find, so the curve is read under the one
5483
+ * named rather than over the one it holds.
5484
+ *
4994
5485
  * @param opts The sampling step, and any setting to lay over the
4995
5486
  * arrangement's own; a meter named here replaces the arrangement's, since
4996
5487
  * `ts` and `meters` name the same thing.
@@ -5057,7 +5548,7 @@ var Arrangement = class _Arrangement {
5057
5548
  }
5058
5549
  /** A copy of the underlying plain data. */
5059
5550
  get data() {
5060
- return copyPlain(this.#data);
5551
+ return copyPlain(this.#data, "arrangement data");
5061
5552
  }
5062
5553
  /** The plain form of the arrangement, for `JSON.stringify`. */
5063
5554
  toJSON() {
@@ -5148,23 +5639,19 @@ function copyProfile(profile) {
5148
5639
  for (const [pitch, limbs] of Object.entries(profile.reach)) {
5149
5640
  reach[Number(pitch)] = [...limbs];
5150
5641
  }
5151
- return { kind: "percussion", ...common, limbs: [...profile.limbs], reach };
5642
+ const percussion = {
5643
+ kind: "percussion",
5644
+ ...common,
5645
+ limbs: [...profile.limbs],
5646
+ reach
5647
+ };
5648
+ if (profile.overdub !== void 0) {
5649
+ percussion.overdub = [...profile.overdub];
5650
+ }
5651
+ return percussion;
5152
5652
  }
5153
5653
  function checkedProfile(profile) {
5154
- const copy = copyProfile(toInstrumentProfile(profile));
5155
- assertPositiveInt(copy.polyphony, `${copy.name} polyphony`);
5156
- if (copy.kind === "stringed") {
5157
- for (const [index, open] of copy.tuning.entries()) {
5158
- assertInteger(open, `${copy.name} tuning[${index}]`);
5159
- }
5160
- assertInteger(copy.maxStretch, `${copy.name} maxStretch`, 0);
5161
- } else {
5162
- for (const pitch of Object.keys(copy.reach)) {
5163
- assertInteger(Number(pitch), `${copy.name} reach pitch`);
5164
- }
5165
- }
5166
- instrumentRange(copy);
5167
- return copy;
5654
+ return copyProfile(toInstrumentProfile(profile));
5168
5655
  }
5169
5656
  var Instrument = class _Instrument {
5170
5657
  #profile;
@@ -5186,7 +5673,7 @@ var Instrument = class _Instrument {
5186
5673
  * tuning, stretch or voice count that is not finite.
5187
5674
  * @example
5188
5675
  * ```ts
5189
- * import { GUITAR_DROP_D } from '../../src/core/index.js';
5676
+ * import { GUITAR_DROP_D } from '@libraz/libcantus/core';
5190
5677
  * import { Instrument } from '@libraz/libcantus';
5191
5678
  * Instrument.of(GUITAR_DROP_D).range().low; // 38
5192
5679
  * ```
@@ -5288,7 +5775,7 @@ var Instrument = class _Instrument {
5288
5775
  * ```
5289
5776
  */
5290
5777
  range() {
5291
- return instrumentRange(this.#profile);
5778
+ return rangeIn(this.#profile);
5292
5779
  }
5293
5780
  /**
5294
5781
  * Whether the instrument has this pitch at all.
@@ -5298,7 +5785,8 @@ var Instrument = class _Instrument {
5298
5785
  * produces it.
5299
5786
  */
5300
5787
  canSound(pitch) {
5301
- return canSound(this.#profile, pitch);
5788
+ assertMidiPitch(pitch, "pitch");
5789
+ return soundsIn(this.#profile, pitch);
5302
5790
  }
5303
5791
  /**
5304
5792
  * Every position on the neck that sounds a pitch, lowest string first.
@@ -5312,15 +5800,17 @@ var Instrument = class _Instrument {
5312
5800
  if (profile.kind !== "stringed") {
5313
5801
  throw new InvalidInputError(`${profile.name} has no strings to finger`);
5314
5802
  }
5315
- return fingeringsFor(profile, pitch);
5803
+ assertMidiPitch(pitch, "pitch");
5804
+ return fingeringsIn(profile, pitch);
5316
5805
  }
5317
5806
  /**
5318
5807
  * Move a pitch by whole octaves until the instrument can sound it, which is
5319
5808
  * what a player does with a line written below the instrument.
5320
5809
  *
5321
5810
  * @param pitch MIDI pitch to place.
5322
- * @returns The nearest octave transposition the instrument sounds, searching
5323
- * upward first; the pitch unchanged when no transposition is available.
5811
+ * @returns The nearest octave transposition the instrument sounds, taking the
5812
+ * upper one when two are equally near; the pitch unchanged when no
5813
+ * transposition is available.
5324
5814
  * @example
5325
5815
  * ```ts
5326
5816
  * import { Instrument } from '@libraz/libcantus';
@@ -5328,7 +5818,8 @@ var Instrument = class _Instrument {
5328
5818
  * ```
5329
5819
  */
5330
5820
  foldIntoRange(pitch) {
5331
- return foldIntoRange(pitch, this.#profile);
5821
+ assertMidiPitch(pitch, "pitch");
5822
+ return foldIn(pitch, this.#profile);
5332
5823
  }
5333
5824
  /**
5334
5825
  * The pitch this instrument sounds for a note written in its part.
@@ -5407,7 +5898,7 @@ var Instrument = class _Instrument {
5407
5898
  if (a.kind === "stringed") {
5408
5899
  return b.kind === "stringed" && a.frets === b.frets && a.maxStretch === b.maxStretch && (a.fretless ?? false) === (b.fretless ?? false) && sameList(a.tuning, b.tuning);
5409
5900
  }
5410
- return b.kind === "percussion" && sameList(a.limbs, b.limbs) && sameReach(a.reach, b.reach);
5901
+ return b.kind === "percussion" && sameList(a.limbs, b.limbs) && sameReach(a.reach, b.reach) && sameList(a.overdub ?? [], b.overdub ?? []);
5411
5902
  }
5412
5903
  /**
5413
5904
  * The plain profile, for JSON serialization.
@@ -5437,40 +5928,11 @@ var Instrument = class _Instrument {
5437
5928
  };
5438
5929
 
5439
5930
  // src/model/composer.ts
5440
- var PROGRESSION_BAR_BEATS = 4;
5441
- var DEFAULT_KEY = majorKey(0);
5442
- function copyPlain2(value, name) {
5443
- if (typeof value === "number") {
5444
- return assertFiniteNumber(value, name);
5445
- }
5446
- if (Array.isArray(value)) {
5447
- return value.map((item, index) => copyPlain2(item, `${name}[${index}]`));
5448
- }
5449
- if (typeof value === "object" && value !== null) {
5450
- const copy = {};
5451
- for (const [key, item] of Object.entries(value)) {
5452
- if (item !== void 0) {
5453
- copy[key] = copyPlain2(item, `${name}.${key}`);
5454
- }
5455
- }
5456
- return copy;
5457
- }
5458
- if (typeof value === "function" || typeof value === "symbol" || typeof value === "bigint") {
5459
- throw new InvalidInputError(`${name} must be plain data; received ${typeof value}`);
5460
- }
5461
- return value;
5462
- }
5463
5931
  function toChordTimeline(harmony) {
5464
5932
  return "chordTimeline" in harmony ? harmony.chordTimeline : harmony;
5465
5933
  }
5466
- function metersFrom3(meters) {
5467
- if (meters === void 0) {
5468
- return resolveMeters({}, "composer meters");
5469
- }
5470
- const meter = toMeterData(meters, "composer meters");
5471
- return Array.isArray(meter) ? resolveMeters({ meters: meter }, "composer meters") : resolveMeters({ ts: meter }, "composer meters");
5472
- }
5473
5934
  function copyComplexity(complexity) {
5935
+ assertDataObject(complexity, "composer complexity");
5474
5936
  const copy = {};
5475
5937
  for (const dial of ["rhythmic", "harmonic", "ornament", "difficulty"]) {
5476
5938
  const value = complexity[dial];
@@ -5481,6 +5943,7 @@ function copyComplexity(complexity) {
5481
5943
  return copy;
5482
5944
  }
5483
5945
  function copyInstruments(instruments) {
5946
+ assertDataObject(instruments, "composer instruments");
5484
5947
  const copy = {};
5485
5948
  for (const [part, profile] of Object.entries(instruments)) {
5486
5949
  copy[part] = Instrument.of(profile).data;
@@ -5488,9 +5951,12 @@ function copyInstruments(instruments) {
5488
5951
  return copy;
5489
5952
  }
5490
5953
  function copyOptions(options) {
5491
- const copy = { meters: metersFrom3(options.meters) };
5954
+ assertDataObject(options, "composer options");
5955
+ const copy = {
5956
+ meters: resolveMeters({ meters: options.meters }, "composer meters")
5957
+ };
5492
5958
  if (options.key !== void 0) {
5493
- copy.key = toKeyScale(options.key);
5959
+ copy.key = resolveKey(options.key);
5494
5960
  }
5495
5961
  if (options.bpm !== void 0) {
5496
5962
  copy.bpm = options.bpm;
@@ -5498,6 +5964,9 @@ function copyOptions(options) {
5498
5964
  if (options.seed !== void 0) {
5499
5965
  copy.seed = options.seed;
5500
5966
  }
5967
+ if (options.algorithmVersion !== void 0) {
5968
+ copy.algorithmVersion = options.algorithmVersion;
5969
+ }
5501
5970
  if (options.complexity !== void 0) {
5502
5971
  copy.complexity = copyComplexity(options.complexity);
5503
5972
  }
@@ -5505,10 +5974,17 @@ function copyOptions(options) {
5505
5974
  copy.instruments = copyInstruments(options.instruments);
5506
5975
  }
5507
5976
  if (options.vocabulary !== void 0) {
5508
- copy.vocabulary = options.vocabulary.map(
5509
- (entry, index) => copyPlain2(entry, `composer vocabulary[${index}]`)
5510
- );
5977
+ copy.vocabulary = assertDataObjects(
5978
+ options.vocabulary,
5979
+ "composer vocabulary"
5980
+ ).map((entry, index) => copyPlain(entry, `composer vocabulary[${index}]`));
5511
5981
  }
5982
+ if (options.rng !== void 0) {
5983
+ copy.rng = options.rng;
5984
+ }
5985
+ const resolved = resolveContext(contextOf(copy));
5986
+ copy.seed = resolved.seed;
5987
+ copy.algorithmVersion = resolved.algorithmVersion;
5512
5988
  return copy;
5513
5989
  }
5514
5990
  function contextOf(options) {
@@ -5516,6 +5992,12 @@ function contextOf(options) {
5516
5992
  if (options.seed !== void 0) {
5517
5993
  ctx.seed = options.seed;
5518
5994
  }
5995
+ if (options.algorithmVersion !== void 0) {
5996
+ ctx.algorithmVersion = options.algorithmVersion;
5997
+ }
5998
+ if (options.rng !== void 0) {
5999
+ ctx.rng = options.rng;
6000
+ }
5519
6001
  if (options.bpm !== void 0) {
5520
6002
  ctx.bpm = options.bpm;
5521
6003
  }
@@ -5526,7 +6008,10 @@ function contextOf(options) {
5526
6008
  ctx.instruments = copyInstruments(options.instruments);
5527
6009
  }
5528
6010
  if (options.vocabulary !== void 0) {
5529
- ctx.vocabulary = options.vocabulary.map((entry) => copyPlain2(entry, "composer vocabulary"));
6011
+ ctx.vocabulary = assertDataObjects(
6012
+ options.vocabulary,
6013
+ "composer vocabulary"
6014
+ ).map((entry, index) => copyPlain(entry, `composer vocabulary[${index}]`));
5530
6015
  }
5531
6016
  return ctx;
5532
6017
  }
@@ -5554,9 +6039,8 @@ var Composer = class _Composer {
5554
6039
  */
5555
6040
  constructor(options) {
5556
6041
  this.#options = copyOptions(options);
5557
- resolveContext(contextOf(this.#options));
5558
- this.#meters = metersFrom3(this.#options.meters);
5559
- this.#key = this.#options.key === void 0 ? void 0 : toKeyScale(this.#options.key);
6042
+ this.#meters = resolveMeters({ meters: this.#options.meters }, "composer meters");
6043
+ this.#key = this.#options.key === void 0 ? void 0 : resolveKey(this.#options.key);
5560
6044
  }
5561
6045
  /**
5562
6046
  * Start from a set of settings.
@@ -5594,18 +6078,27 @@ var Composer = class _Composer {
5594
6078
  /**
5595
6079
  * A chord progression, in time.
5596
6080
  *
5597
- * The chords are laid out one per four-beat bar, which is the grid
5598
- * {@link generateProgression} writes on whatever meter the composer holds, so
5599
- * the timeline spans `bars` of four beats and carries the composer's key.
6081
+ * The chords are laid out one per bar of the composer's own meter, so a chord
6082
+ * change falls on a bar line of the piece and the timeline spans `bars` of it:
6083
+ * four beats each in 4/4, three in 3/4, and three in 6/8. Every other part is
6084
+ * written against that grid, and harmony that is not on it drifts further from
6085
+ * the bar line with every repeat.
5600
6086
  *
5601
- * @param opts Everything the progression generator takes but the key and the
5602
- * context, which are the composer's.
6087
+ * @param opts Everything the progression generator takes but the key, the
6088
+ * meter and the context, which are the composer's.
5603
6089
  * @returns The chords over the beats they sound for.
6090
+ * @throws If the composer names no key: the chords are degrees of one, and
6091
+ * there is no melody here to read a key from the way
6092
+ * {@link Composer.harmonize} does.
6093
+ * @throws If the composer's meter changes: one chord per bar has no single bar
6094
+ * length to be laid out on, and the chords would leave the bar lines at the
6095
+ * first change rather than follow them.
5604
6096
  */
5605
6097
  progression(opts) {
5606
- const key = this.#keyScale();
5607
- const chords = generateProgression({ ...opts, key, ctx: this.context });
5608
- return Timeline.fromChords(chords, opts.bars * PROGRESSION_BAR_BEATS, key);
6098
+ const key = this.#requireKey("progression");
6099
+ const ts = this.#unchangingMeter("progression");
6100
+ const chords = generateProgression({ ...opts, key, ts, ctx: this.context });
6101
+ return Timeline.fromChords(chords, opts.bars * beatsPerBar(ts), key);
5609
6102
  }
5610
6103
  /**
5611
6104
  * A drum part.
@@ -5618,11 +6111,13 @@ var Composer = class _Composer {
5618
6111
  * @param opts Everything the drum generator takes but the meter and the
5619
6112
  * context, which are the composer's.
5620
6113
  * @returns The hits, as a score.
5621
- * @throws If the composer's meter is not 4/4, which is the only meter the
5622
- * drum patterns are written against.
6114
+ * @throws If the composer's meter is not 4/4 throughout, which is the only
6115
+ * meter the drum patterns are written against: a piece that opens in 4/4
6116
+ * and changes later has bars the patterns cannot be laid out on, so the
6117
+ * request is refused rather than answered with 4/4 bars over the change.
5623
6118
  */
5624
6119
  drums(opts) {
5625
- const hits = generateDrums({ ...opts, ts: this.#openingMeter(), ctx: this.context });
6120
+ const hits = generateDrums({ ...opts, ts: this.#unchangingMeter("drums"), ctx: this.context });
5626
6121
  return Score.of(hits, this.#scoreOptions());
5627
6122
  }
5628
6123
  /**
@@ -5633,6 +6128,8 @@ var Composer = class _Composer {
5633
6128
  * @param opts Everything the bass generator takes but the segments, the key,
5634
6129
  * the meter and the context; see {@link BassLineSettings}.
5635
6130
  * @returns The line, as a score in the composer's key.
6131
+ * @throws If the composer names no key: the line is written in one, and a
6132
+ * default key would put the part in a key the caller never asked for.
5636
6133
  * @throws If the instrument names a kit, which has no strings for a bass line
5637
6134
  * to be placed on.
5638
6135
  * @example
@@ -5645,7 +6142,7 @@ var Composer = class _Composer {
5645
6142
  */
5646
6143
  bass(source, opts) {
5647
6144
  const segments = Array.isArray(source) ? source : source.segments;
5648
- const key = this.#keyScale();
6145
+ const key = this.#requireKey("bass");
5649
6146
  const { instrument, ...rest } = opts ?? {};
5650
6147
  const notes = generateBassLine({
5651
6148
  ...rest,
@@ -5672,9 +6169,12 @@ var Composer = class _Composer {
5672
6169
  * @param opts Everything the counter-melody generator takes but the melody,
5673
6170
  * the key, the meter and the context; see {@link CounterMelodySettings}.
5674
6171
  * @returns The counter line, as a score in the composer's key.
6172
+ * @throws If the composer names no key: the line is written in one, and the
6173
+ * melody it answers is not read for a key here — {@link Composer.harmonize}
6174
+ * is the member that does that.
5675
6175
  */
5676
6176
  counterMelody(melody, opts) {
5677
- const key = this.#keyScale();
6177
+ const key = this.#requireKey("counterMelody");
5678
6178
  const { timeline, ...rest } = opts ?? {};
5679
6179
  const notes = generateCounterMelody({
5680
6180
  ...rest,
@@ -5700,8 +6200,7 @@ var Composer = class _Composer {
5700
6200
  * that melody and the one handed in.
5701
6201
  * @example
5702
6202
  * ```ts
5703
- * import { Composer } from '@libraz/libcantus';
5704
- * import { Score } from '../../src/model/score.js';
6203
+ * import { Composer, Score } from '@libraz/libcantus';
5705
6204
  * const composer = Composer.of({ key: 'C major' });
5706
6205
  * const melody = Score.of([{ pitch: 60, startBeat: 0, durationBeat: 4 }]);
5707
6206
  * composer.harmonize(melody).transposeSemitones; // 0
@@ -5733,29 +6232,59 @@ var Composer = class _Composer {
5733
6232
  * Whether another composer holds the same settings.
5734
6233
  *
5735
6234
  * The comparison is made through the other composer's public data, so two
5736
- * composers built by different copies of the module still compare.
6235
+ * composers built by different copies of the module still compare. Both sides
6236
+ * are projected the same way: comparing the settings held here against the
6237
+ * data the other hands out answered "different" for a composer holding a
6238
+ * source and "same" for two that draw from different ones.
6239
+ *
6240
+ * A source is compared by identity, being a handle on a stream rather than a
6241
+ * setting: two composers drawing from different sources write different
6242
+ * parts, and the numbers a source will hand out cannot be read from it.
5737
6243
  *
5738
6244
  * @param other The composer to compare.
5739
6245
  * @returns True when both would generate the same parts.
5740
6246
  */
5741
6247
  equals(other) {
5742
- return samePlain(this.#options, other.data);
6248
+ return samePlain(this.data, other.data) && this.context.rng === other.context.rng;
5743
6249
  }
5744
6250
  /** The resolved context, for handing back to the function API. */
5745
6251
  get context() {
5746
6252
  return contextOf(this.#options);
5747
6253
  }
5748
- /** A copy of the settings this composer holds. */
6254
+ /**
6255
+ * A copy of the settings this composer holds: the seed, the resolved
6256
+ * algorithm version and the rest of the recipe, so a piece can be written
6257
+ * again from what a project file stored.
6258
+ *
6259
+ * The source a composer was handed is left out, being a live handle rather
6260
+ * than plain data; a composer built with one keeps it across
6261
+ * {@link Composer.with} and its siblings.
6262
+ */
5749
6263
  get data() {
5750
- return copyOptions(this.#options);
6264
+ return copyOptions({ ...this.#options, rng: void 0 });
5751
6265
  }
5752
6266
  /** The plain form of the settings, for `JSON.stringify`. */
5753
6267
  toJSON() {
5754
6268
  return this.data;
5755
6269
  }
5756
- /** The key the parts are written in; C major where the composer names none. */
5757
- #keyScale() {
5758
- return this.#key ?? DEFAULT_KEY;
6270
+ /**
6271
+ * The key a pitched part is written in.
6272
+ *
6273
+ * A composer that names none is refused rather than written in C major: a
6274
+ * default key is an answer the caller never gave, and the part would come
6275
+ * back sounding like a piece nobody asked for with nothing to say it had
6276
+ * been substituted. {@link Composer.harmonize} is the one member that can
6277
+ * answer without a key, because it has a melody to read one from; the key it
6278
+ * found comes back with the harmonization, and `with({ key })` is how it is
6279
+ * handed to the parts written next.
6280
+ */
6281
+ #requireKey(part) {
6282
+ if (this.#key === void 0) {
6283
+ throw new InvalidInputError(
6284
+ `composer ${part} needs a key for the part it writes; the composer names none \u2014 give it one, or take the key harmonize reads off a melody and pass it on with with({ key })`
6285
+ );
6286
+ }
6287
+ return this.#key;
5759
6288
  }
5760
6289
  /**
5761
6290
  * The signature the parts are written against.
@@ -5767,6 +6296,22 @@ var Composer = class _Composer {
5767
6296
  #openingMeter() {
5768
6297
  return meterAt(0, this.#meters);
5769
6298
  }
6299
+ /**
6300
+ * The one signature a part laid out in bars is written against.
6301
+ *
6302
+ * A part that places material bar by bar has to know how long a bar is, and a
6303
+ * piece that changes meter has no one answer: the request is refused rather
6304
+ * than answered on the bar the piece opens in, which would put every bar after
6305
+ * the change somewhere the piece has no bar line.
6306
+ */
6307
+ #unchangingMeter(part) {
6308
+ if (this.#meters.length > 1) {
6309
+ throw new InvalidInputError(
6310
+ `composer ${part} needs one meter for the whole part; the composer changes meter`
6311
+ );
6312
+ }
6313
+ return this.#openingMeter();
6314
+ }
5770
6315
  /** The context every score a composer hands back is read against. */
5771
6316
  #scoreOptions(key) {
5772
6317
  const opts = { meters: this.#meters };
@@ -6072,7 +6617,7 @@ var Meter = class _Meter {
6072
6617
  get beatsPerBar() {
6073
6618
  return beatsPerBar(this.#ts);
6074
6619
  }
6075
- /** Main pulses (felt beats) in a bar: 3 in 6/8, 4 in 4/4. */
6620
+ /** Main pulses (felt beats) in a bar: 2 in 6/8, 4 in 4/4. */
6076
6621
  get pulsesPerBar() {
6077
6622
  return pulsesPerBar(this.#ts);
6078
6623
  }
@@ -6088,13 +6633,19 @@ var Meter = class _Meter {
6088
6633
  * additive grouping and one is asked for.
6089
6634
  *
6090
6635
  * @param opts Set `grouping: true` to render an additive grouping. A
6091
- * grouping counted in main pulses cannot be written additively and falls
6092
- * back to the plain form.
6636
+ * grouping counted in main pulses has no additive spelling: one whose
6637
+ * groups are all the same length states the division the bare signature
6638
+ * already has and falls back to the plain form, and one whose groups differ
6639
+ * is refused rather than printed as a bar that weighs its pulses otherwise.
6093
6640
  * @returns The formatted signature.
6641
+ * @throws If `grouping: true` is asked of a grouping that no signature text
6642
+ * can spell: one counted in main pulses, on a compound numerator, whose
6643
+ * groups are not all the same length — 12/8 as `[1, 1, 2]`.
6094
6644
  * @example
6095
6645
  * ```ts
6096
6646
  * import { Meter } from '@libraz/libcantus';
6097
6647
  * Meter.of(7, 8, [2, 2, 3]).format({ grouping: true }); // '2+2+3/8'
6648
+ * Meter.of(12, 8, [1, 1, 1, 1]).format({ grouping: true }); // '12/8'
6098
6649
  * ```
6099
6650
  */
6100
6651
  format(opts = {}) {
@@ -6140,13 +6691,18 @@ var Meter = class _Meter {
6140
6691
  *
6141
6692
  * @param beatInQuarters Position in quarter-note beats.
6142
6693
  * @param decimals Digits of the fractional beat to keep.
6143
- * @returns The formatted position, e.g. `'3.2'` for bar 3, felt beat 2.
6694
+ * @returns The formatted position in one of two forms: `bar.beat` on a felt
6695
+ * beat, e.g. `'3.2'` for bar 3, felt beat 2; and `bar.beat+fraction`
6696
+ * between felt beats, e.g. `'3.2+0.5'` halfway from the second felt beat to
6697
+ * the third. A reader of these strings has to take both, since most onsets
6698
+ * of an ordinary piece fall between pulses.
6144
6699
  * @throws If the position is not finite or `decimals` is not an integer in
6145
6700
  * 0..100.
6146
6701
  * @example
6147
6702
  * ```ts
6148
6703
  * import { Meter } from '@libraz/libcantus';
6149
6704
  * Meter.parse('6/8').formatPosition(7.5); // '3.2'
6705
+ * Meter.parse('6/8').formatPosition(8.25); // '3.2+0.5'
6150
6706
  * ```
6151
6707
  */
6152
6708
  formatPosition(beatInQuarters, decimals = 2) {
@@ -6207,7 +6763,7 @@ var Meter = class _Meter {
6207
6763
  };
6208
6764
 
6209
6765
  // src/model/motif.ts
6210
- function copyNote3(note) {
6766
+ function copyNote2(note) {
6211
6767
  return {
6212
6768
  pitch: withoutNegativeZero(note.pitch),
6213
6769
  startBeat: withoutNegativeZero(note.startBeat),
@@ -6215,17 +6771,8 @@ function copyNote3(note) {
6215
6771
  };
6216
6772
  }
6217
6773
  function copyCell(cell) {
6218
- if (!Array.isArray(cell.notes)) {
6219
- throw new InvalidInputError(`motif notes must be an array; received ${typeof cell.notes}`);
6220
- }
6221
- return {
6222
- notes: cell.notes.map((note, index) => {
6223
- if (note === void 0) {
6224
- throw new InvalidInputError(`motif notes[${index}] must be a note; received undefined`);
6225
- }
6226
- return copyNote3(assertNoteEvent(note, `motif notes[${index}]`));
6227
- })
6228
- };
6774
+ assertDataObject(cell, "motif cell");
6775
+ return { notes: assertNoteEventArray(cell.notes, "motif notes").map(copyNote2) };
6229
6776
  }
6230
6777
  function chordFrom(chord) {
6231
6778
  return chord === void 0 || chord === null ? chord : toChordData(chord);
@@ -6260,9 +6807,7 @@ var Motif = class _Motif {
6260
6807
  * ```
6261
6808
  */
6262
6809
  static generate(opts) {
6263
- return new _Motif(
6264
- generateMotif({ ...opts, key: toKeyScale(opts.key), chord: chordFrom(opts.chord) })
6265
- );
6810
+ return new _Motif(generateMotif({ ...opts, chord: chordFrom(opts.chord) }));
6266
6811
  }
6267
6812
  /**
6268
6813
  * Read a run of notes as a motif.
@@ -6288,30 +6833,29 @@ var Motif = class _Motif {
6288
6833
  }
6289
6834
  /** The notes of the cell, in the order the transforms read them. */
6290
6835
  get notes() {
6291
- return this.#cell.notes.map(copyNote3);
6836
+ return this.#cell.notes.map(copyNote2);
6292
6837
  }
6293
6838
  /** Where the cell stops sounding, measured from its own first onset. */
6294
6839
  get totalBeats() {
6295
- if (this.#cell.notes.length === 0) {
6296
- return 0;
6297
- }
6298
- const start = Math.min(...this.#cell.notes.map((note) => note.startBeat));
6299
- const end = Math.max(...this.#cell.notes.map((note) => note.startBeat + note.durationBeat));
6300
- return end - start;
6840
+ return cellSpan(this.#cell);
6301
6841
  }
6302
6842
  /** A copy of the underlying plain cell. */
6303
6843
  get data() {
6304
- return { notes: this.#cell.notes.map(copyNote3) };
6844
+ return { notes: this.#cell.notes.map(copyNote2) };
6305
6845
  }
6306
6846
  /**
6307
6847
  * The cell put through one of the classical transformations.
6308
6848
  *
6309
- * `invert` reflects the pitches about the first note and `retrograde` mirrors
6310
- * the onsets about the cell's span, both self-inverse; `augment` and
6849
+ * `invert` reflects the pitches about the earliest note and `retrograde`
6850
+ * mirrors the onsets about the cell's span, both self-inverse; `augment` and
6311
6851
  * `diminish` scale the note values by `amount` and its reciprocal;
6312
6852
  * `transposeChromatic` moves by semitones; `transposeDiatonic` and `sequence`
6313
6853
  * move by scale degrees with a key in hand, and by semitones without one.
6314
6854
  *
6855
+ * The result's notes run in time order whichever way the source cell was
6856
+ * written, so an inversion of a retrograde reflects about the note that now
6857
+ * sounds first.
6858
+ *
6315
6859
  * @param kind The transformation.
6316
6860
  * @param amount Semitones, scale degrees, or the time factor, as the
6317
6861
  * transformation reads it.
@@ -6330,9 +6874,7 @@ var Motif = class _Motif {
6330
6874
  * ```
6331
6875
  */
6332
6876
  transform(kind, amount, key) {
6333
- return new _Motif(
6334
- transformMotif(this.#cell, kind, amount, key === void 0 ? void 0 : toKeyScale(key))
6335
- );
6877
+ return new _Motif(transformMotif(this.#cell, kind, amount, key));
6336
6878
  }
6337
6879
  /**
6338
6880
  * The cell laid across a span and snapped to the harmony under it.
@@ -6345,29 +6887,30 @@ var Motif = class _Motif {
6345
6887
  * plain chord timeline the analysis layer hands out.
6346
6888
  * @param key The key the passing notes are kept in.
6347
6889
  * @param bars How many bars to fill.
6348
- * @param ts Meter the bars are counted in; 4/4 when none is named.
6890
+ * @param ts Meter the bars are counted in, in any form that names one. A
6891
+ * motif carries no meter of its own — it is a run of notes — so there is
6892
+ * nothing to fall back on, and a development laid on the wrong bar length
6893
+ * drifts off the part it plays under.
6349
6894
  * @returns The developed motif.
6350
6895
  * @throws If the bar count is not a positive integer.
6351
6896
  */
6352
6897
  develop(timeline, key, bars, ts) {
6353
6898
  const chords = "chordTimeline" in timeline ? timeline.chordTimeline : timeline;
6354
- return new _Motif(developMotif(this.#cell, chords, toKeyScale(key), bars, ts));
6899
+ return new _Motif(developMotif(this.#cell, chords, key, bars, ts));
6355
6900
  }
6356
6901
  /**
6357
6902
  * How this motif stands to another: the transformation that turns one into
6358
6903
  * the other, named.
6359
6904
  *
6360
6905
  * @param other The statement to name against this one.
6361
- * @param key Key context for the tonal reading; without it, an answer that
6362
- * only holds diatonically is not recognised.
6906
+ * @param key Key context for the tonal reading. Without one an answer holding
6907
+ * only diatonically is left unnamed unless its interval pattern also fits
6908
+ * the retrograde family, in which case it is named there; a motif carries no
6909
+ * key of its own, so this is the only way to have the tonal reading offered.
6363
6910
  * @returns The relation, or null when the two stand in none.
6364
6911
  */
6365
6912
  relateTo(other, key) {
6366
- return relateMotifs(
6367
- motifFromNotes(this.#cell.notes),
6368
- motifFromNotes(other.notes),
6369
- key === void 0 ? void 0 : toKeyScale(key)
6370
- );
6913
+ return relateMotifs(motifFromNotes(this.#cell.notes), motifFromNotes(other.notes), key);
6371
6914
  }
6372
6915
  /**
6373
6916
  * How alike two lines are, in [0, 1].
@@ -6418,7 +6961,6 @@ function sameNote2(note, other) {
6418
6961
  }
6419
6962
 
6420
6963
  // src/model/rhythm.ts
6421
- var DEFAULT_TS = { numerator: 4, denominator: 4 };
6422
6964
  var GRID_VELOCITY = 1;
6423
6965
  var PART = "rhythm";
6424
6966
  function copyEvent(event, name) {
@@ -6432,17 +6974,10 @@ function copyEvent(event, name) {
6432
6974
  };
6433
6975
  }
6434
6976
  function copyEvents(events, name) {
6435
- if (!Array.isArray(events)) {
6436
- throw new InvalidInputError(`${name} must be an array; received ${typeof events}`);
6437
- }
6438
- return events.map((event, index) => {
6439
- if (event === void 0) {
6440
- throw new InvalidInputError(`${name}[${index}] must be an onset; received undefined`);
6441
- }
6442
- return copyEvent(event, `${name}[${index}]`);
6443
- }).sort((a, b) => a.position - b.position || a.duration - b.duration);
6977
+ return assertDataObjects(events, name).map((event, index) => copyEvent(event, `${name}[${index}]`)).sort((a, b) => a.position - b.position || a.duration - b.duration);
6444
6978
  }
6445
6979
  function copyRhythm(data) {
6980
+ assertDataObject(data, "rhythm data");
6446
6981
  return {
6447
6982
  events: copyEvents(data.events, "rhythm events"),
6448
6983
  ts: copyTimeSignature(data.ts, "rhythm ts")
@@ -6482,16 +7017,16 @@ var Rhythm = class _Rhythm {
6482
7017
  /**
6483
7018
  * Generate a pattern over a meter.
6484
7019
  *
6485
- * @param ts The time signature the bars are counted in.
7020
+ * @param ts The time signature the bars are counted in, in any form that
7021
+ * names one; a meter map is read as the signature it opens in.
6486
7022
  * @param opts Length, grid resolution and the context; see
6487
7023
  * {@link RhythmOptions}. The context's `complexity.rhythmic` is the onset
6488
7024
  * density, and its `seed` fixes which grid slots are taken.
6489
7025
  * @returns The generated pattern.
6490
7026
  * @example
6491
7027
  * ```ts
6492
- * import { parseTimeSignature } from '@libraz/libcantus';
6493
7028
  * import { Rhythm } from '@libraz/libcantus';
6494
- * const rhythm = Rhythm.generate(parseTimeSignature('4/4'), {
7029
+ * const rhythm = Rhythm.generate('4/4', {
6495
7030
  * bars: 2,
6496
7031
  * ctx: { seed: 7, complexity: { rhythmic: 0.6 } },
6497
7032
  * });
@@ -6499,18 +7034,29 @@ var Rhythm = class _Rhythm {
6499
7034
  * ```
6500
7035
  */
6501
7036
  static generate(ts, opts) {
6502
- return new _Rhythm({ events: generateRhythm(ts, opts), ts });
7037
+ const meter = meterAt(0, toMeterData(ts, "ts"));
7038
+ return new _Rhythm({ events: generateRhythm(meter, opts), ts: meter });
6503
7039
  }
6504
7040
  /**
6505
7041
  * Build a pattern from onsets that already exist.
6506
7042
  *
7043
+ * The meter is asked for rather than assumed. A pattern in 6/8 read as 4/4
7044
+ * answers every metric question — which beats are strong, where the bars
7045
+ * fall, how dense it is — on the wrong pulse, and nothing downstream can tell
7046
+ * that reading from a meter the caller meant. Its two siblings,
7047
+ * {@link Rhythm.generate} and `generateRhythm`, ask for it the same way.
7048
+ *
6507
7049
  * @param events The onsets, in any order.
6508
- * @param ts The meter they are counted in; 4/4 when none is named, as every
6509
- * meter-aware function in the library assumes.
7050
+ * @param ts The meter they are counted in, in any form that names one.
6510
7051
  * @returns The pattern.
7052
+ * @throws If the meter names no signature, or an onset carries a value a
7053
+ * pattern cannot hold.
6511
7054
  */
6512
- static of(events, ts = DEFAULT_TS) {
6513
- return new _Rhythm({ events: [...events], ts });
7055
+ static of(events, ts) {
7056
+ return new _Rhythm({
7057
+ events: [...assertDataArray(events, "rhythm events")],
7058
+ ts: meterAt(0, toMeterData(ts, "ts"))
7059
+ });
6514
7060
  }
6515
7061
  /** Rebuild a pattern from the plain data {@link Rhythm.data} hands out. */
6516
7062
  static fromData(data) {
@@ -6550,19 +7096,26 @@ var Rhythm = class _Rhythm {
6550
7096
  /**
6551
7097
  * Drop the onsets carrying the least of the metre.
6552
7098
  *
7099
+ * The metre is the pattern's own: the ranks are read from {@link Rhythm.ts},
7100
+ * so a pattern in 6/8 keeps its dotted-quarter pulses and one in 3/4 keeps its
7101
+ * three-beat downbeats rather than the beats a four-beat bar would have.
7102
+ *
6553
7103
  * @param amount How much to thin, in [0, 1]; at 0 nothing goes, and a full
6554
7104
  * turn leaves the downbeats alone.
6555
7105
  * @returns The thinned pattern.
6556
7106
  * @throws If the amount is outside [0, 1].
6557
7107
  */
6558
7108
  thin(amount) {
6559
- return this.#withGrid(thin(toGrid(this.#data.events), amount));
7109
+ return this.#withGrid(thin(toGrid(this.#data.events), amount, this.#data.ts));
6560
7110
  }
6561
7111
  /**
6562
7112
  * Anticipate beats, one sixteenth early.
6563
7113
  *
6564
- * The anticipations are added rather than displaced, so raising the amount
6565
- * only ever adds onsets and the ones already sounding stay where they are.
7114
+ * The beats anticipated are the ones {@link Rhythm.ts} counts as main pulses
7115
+ * or stronger, so a pattern is not syncopated against accents its own meter
7116
+ * does not have. The anticipations are added rather than displaced, so raising
7117
+ * the amount only ever adds onsets and the ones already sounding stay where
7118
+ * they are.
6566
7119
  *
6567
7120
  * @param amount How much syncopation, in [0, 1].
6568
7121
  * @param ctx The context, or the seed alone; it fixes which beats are
@@ -6572,7 +7125,11 @@ var Rhythm = class _Rhythm {
6572
7125
  */
6573
7126
  syncopate(amount, ctx) {
6574
7127
  return this.#withGrid(
6575
- syncopate(toGrid(this.#data.events), amount, resolveContext(ctx).part(PART))
7128
+ syncopate(
7129
+ toGrid(this.#data.events),
7130
+ { amount, ts: this.#data.ts },
7131
+ resolveContext(ctx).part(PART)
7132
+ )
6576
7133
  );
6577
7134
  }
6578
7135
  /**
@@ -6642,6 +7199,7 @@ var Rhythm = class _Rhythm {
6642
7199
  ornament: resolved.ornament,
6643
7200
  rate: opts?.rate,
6644
7201
  spanSteps: this.#spanSteps(),
7202
+ ts: this.#data.ts,
6645
7203
  // The transform hands the predicate the events it was given, which carry
6646
7204
  // the onset each was built from; the parameter is declared over the bare
6647
7205
  // grid event the rule reads, so the field is named back here.
@@ -6674,7 +7232,9 @@ var Rhythm = class _Rhythm {
6674
7232
  * @param pitch The MIDI pitch to give every onset.
6675
7233
  * @param velocity Velocity for every onset; a mezzo-forte 96 by default.
6676
7234
  * @returns The score holding the pattern.
6677
- * @throws If the pitch is not finite or the velocity is outside [0, 127].
7235
+ * @throws If the pitch or the velocity is not a whole MIDI value in [0, 127];
7236
+ * both are the numbers a note event carries, and a fractional one is not a
7237
+ * note anything downstream can sound.
6678
7238
  * @example
6679
7239
  * ```ts
6680
7240
  * import { parseTimeSignature } from '@libraz/libcantus';
@@ -6729,10 +7289,11 @@ function sameSignature(ts, other) {
6729
7289
 
6730
7290
  // src/model/tempo.ts
6731
7291
  var MAX_BPM = 1e3;
7292
+ var MIN_BPM = 0.1;
6732
7293
  var Tempo = class _Tempo {
6733
7294
  #bpm;
6734
7295
  constructor(bpm) {
6735
- this.#bpm = assertRange(bpm, Number.MIN_VALUE, MAX_BPM, "bpm");
7296
+ this.#bpm = assertRange(bpm, MIN_BPM, MAX_BPM, "bpm");
6736
7297
  }
6737
7298
  /**
6738
7299
  * The one-event tempo map every conversion here is measured against.
@@ -6761,7 +7322,7 @@ var Tempo = class _Tempo {
6761
7322
  * @throws If the marking is not a positive, finite number the library holds.
6762
7323
  */
6763
7324
  static fromData(data) {
6764
- return new _Tempo(data.bpm);
7325
+ return new _Tempo(assertDataObject(data, "tempo data").bpm);
6765
7326
  }
6766
7327
  /**
6767
7328
  * Rebuild a tempo from its {@link Tempo.toJSON} output.
@@ -6771,7 +7332,7 @@ var Tempo = class _Tempo {
6771
7332
  * @throws If the marking is not a positive, finite number the library holds.
6772
7333
  */
6773
7334
  static fromJSON(data) {
6774
- return new _Tempo(data.bpm);
7335
+ return new _Tempo(assertDataObject(data, "tempo data").bpm);
6775
7336
  }
6776
7337
  /** Quarter-note beats per minute. */
6777
7338
  get bpm() {
@@ -6885,23 +7446,29 @@ var Tuning = class _Tuning {
6885
7446
  /**
6886
7447
  * Wrap a plain tuning table.
6887
7448
  *
6888
- * The table is checked the way {@link Tuning.edo} checks its arguments, so a
6889
- * deserialized or hand-written one cannot enter carrying a division count or
6890
- * a reference frequency no frequency follows from.
7449
+ * Every field is read off the table, so a deserialized or hand-written one
7450
+ * cannot enter carrying a division count or a reference frequency no
7451
+ * frequency follows from. None of the three has a default here, unlike
7452
+ * {@link Tuning.edo}: a table that lost its reference frequency on the way
7453
+ * through storage is refused rather than quietly retuned to A=440.
6891
7454
  *
6892
7455
  * @param table The plain tuning table.
6893
7456
  * @returns The wrapped tuning.
6894
- * @throws If the divisions are not a positive integer, or the reference step
6895
- * or frequency is not a finite positive number.
7457
+ * @throws If a field is missing, the divisions are not a positive integer,
7458
+ * the reference frequency is not a finite positive number, or the reference
7459
+ * step is not finite.
6896
7460
  * @example
6897
7461
  * ```ts
6898
- * import { TWELVE_TET } from '../../src/core/tuning/index.js';
6899
- * import { Tuning } from '@libraz/libcantus';
7462
+ * import { TWELVE_TET, Tuning } from '@libraz/libcantus';
6900
7463
  * Tuning.of(TWELVE_TET).divisions; // 12
6901
7464
  * ```
6902
7465
  */
6903
7466
  static of(table) {
6904
- return new _Tuning(edo(table.divisions, table.refFreq, table.refStep));
7467
+ const given = assertDataObject(table, "tuning table");
7468
+ assertFiniteNumber(given.divisions, "tuning table.divisions");
7469
+ assertFiniteNumber(given.refFreq, "tuning table.refFreq");
7470
+ assertFiniteNumber(given.refStep, "tuning table.refStep");
7471
+ return new _Tuning(edo(given.divisions, given.refFreq, given.refStep));
6905
7472
  }
6906
7473
  /**
6907
7474
  * Build an equal temperament with `divisions` divisions of the octave.
@@ -6911,8 +7478,10 @@ var Tuning = class _Tuning {
6911
7478
  * @param refStep Step index of the reference; 69 by default, the MIDI number
6912
7479
  * of A4.
6913
7480
  * @returns The tuning.
6914
- * @throws If the divisions are not a positive integer, or the reference step
6915
- * or frequency is not a finite positive number.
7481
+ * @throws If the divisions are not a positive integer, the reference
7482
+ * frequency is not a finite positive number, or the reference step is not
7483
+ * finite. A negative or fractional reference step is accepted: it is what a
7484
+ * tuner reading or a pitch-bend calibration lands on.
6916
7485
  * @example
6917
7486
  * ```ts
6918
7487
  * import { Tuning } from '@libraz/libcantus';
@@ -6981,7 +7550,8 @@ var Tuning = class _Tuning {
6981
7550
  *
6982
7551
  * @param cents The interval in cents.
6983
7552
  * @returns The frequency ratio.
6984
- * @throws If `cents` is not finite.
7553
+ * @throws If `cents` is not finite, or the ratio it spans falls outside the
7554
+ * range a number holds.
6985
7555
  * @example
6986
7556
  * ```ts
6987
7557
  * import { Tuning } from '@libraz/libcantus';
@@ -7219,20 +7789,34 @@ var Tuning = class _Tuning {
7219
7789
 
7220
7790
  // src/model/voicing.ts
7221
7791
  function copyPitches(pitches) {
7792
+ assertDataArray(pitches, "voicing pitches");
7222
7793
  if (pitches.length === 0) {
7223
7794
  throw new InvalidInputError("a voicing must contain at least one pitch");
7224
7795
  }
7225
7796
  return pitches.map((pitch, index) => assertMidiPitch(pitch, `pitches[${index}]`));
7226
7797
  }
7227
7798
  function spellLine(line, key) {
7228
- const { tonic } = spelledKeyOf(key);
7229
7799
  return line.map(
7230
- (value) => typeof value === "number" ? spellPitch(value, tonic, key) : toNoteData(value)
7800
+ (value) => typeof value === "number" ? spellPitch(value, key.tonic, key) : toNoteData(value)
7231
7801
  );
7232
7802
  }
7233
7803
  function lineOf(pitches, key) {
7234
7804
  return key === void 0 ? pitches.map((pitch) => toNoteData(pitch)) : spellLine(pitches, key);
7235
7805
  }
7806
+ function carriedKeyOf(chord) {
7807
+ if (typeof chord !== "object" || chord === null) {
7808
+ return void 0;
7809
+ }
7810
+ const key = chord.key;
7811
+ return typeof key === "object" && key !== null ? key : void 0;
7812
+ }
7813
+ function withCarriedKey(chord, opts) {
7814
+ if (opts?.key !== void 0) {
7815
+ return opts;
7816
+ }
7817
+ const key = carriedKeyOf(chord);
7818
+ return key === void 0 ? opts : { ...opts, key };
7819
+ }
7236
7820
  var Voicing = class _Voicing {
7237
7821
  #pitches;
7238
7822
  /**
@@ -7273,7 +7857,7 @@ var Voicing = class _Voicing {
7273
7857
  * ```
7274
7858
  */
7275
7859
  static forChord(chord, opts) {
7276
- return new _Voicing(voiceChordStyled(toChordData(chord), opts));
7860
+ return new _Voicing(voiceChordStyled(chord, opts));
7277
7861
  }
7278
7862
  /**
7279
7863
  * Realize a chord as one pitch per voice inside the voice ranges, defaulting
@@ -7282,6 +7866,10 @@ var Voicing = class _Voicing {
7282
7866
  * The bass takes the chord's slash bass when it has one, otherwise the root;
7283
7867
  * the result is compact, centred in its ranges, and free of voice crossing.
7284
7868
  *
7869
+ * A chord that carries a key passes it to the voicer, so the leading tone is
7870
+ * not doubled; an explicit `opts.key` overrides it. That is the order
7871
+ * {@link Chord.voice} resolves them in, and the two answer alike.
7872
+ *
7285
7873
  * @param chord A chord symbol, plain chord data, or a {@link Chord}.
7286
7874
  * @param opts Voicing options; defaults to four voices in {@link SATB_RANGES}.
7287
7875
  * @returns The voicing, ascending.
@@ -7293,7 +7881,7 @@ var Voicing = class _Voicing {
7293
7881
  * ```
7294
7882
  */
7295
7883
  static satb(chord, opts) {
7296
- return new _Voicing(voiceChord(toChordData(chord), opts));
7884
+ return new _Voicing(voiceChord(toChordData(chord), withCarriedKey(chord, opts)));
7297
7885
  }
7298
7886
  /**
7299
7887
  * Wrap a plain pitch array, as {@link Voicing.data} hands it out.
@@ -7353,7 +7941,7 @@ var Voicing = class _Voicing {
7353
7941
  * @param key A key name, a plain key/scale, or a {@link Key}.
7354
7942
  * @param chord The chord sounding, supplying the enharmonic evidence; without
7355
7943
  * it the key alone decides.
7356
- * @returns One spelled note per pitch, ascending.
7944
+ * @returns One spelled note per pitch, in voice order.
7357
7945
  * @throws If the value names no key or no chord.
7358
7946
  * @example
7359
7947
  * ```ts
@@ -7363,7 +7951,8 @@ var Voicing = class _Voicing {
7363
7951
  * ```
7364
7952
  */
7365
7953
  spell(key, chord) {
7366
- return this.#spelled(toKeyScale(key), chord).map((note) => new Note(note));
7954
+ assertKeyArgument(key, "voicing key");
7955
+ return this.#spelled(resolveKey(key), chord).map((note) => new Note(note));
7367
7956
  }
7368
7957
  /**
7369
7958
  * Voice a chord to follow smoothly from this voicing.
@@ -7372,7 +7961,13 @@ var Voicing = class _Voicing {
7372
7961
  * `voices` nor `ranges` is given — within a one-octave window around each of
7373
7962
  * these pitches, so the answer keeps this voicing's voice count. Each is
7374
7963
  * scored by structural quality, motion from here, and a large penalty per
7375
- * counterpoint violation.
7964
+ * counterpoint violation. A chord that carries a key passes it to the voicer
7965
+ * unless `opts.key` names another one.
7966
+ *
7967
+ * The rules read from the chord being left — a chordal seventh's resolution,
7968
+ * a leading tone's resolution, and the cross relation between the two chords
7969
+ * — are scored only when `opts.previousChord` names that chord: a voicing
7970
+ * does not say what it was written on.
7376
7971
  *
7377
7972
  * @param chord A chord symbol, plain chord data, or a {@link Chord}.
7378
7973
  * @param opts Voicing options; when omitted, the ranges follow this voicing.
@@ -7387,12 +7982,12 @@ var Voicing = class _Voicing {
7387
7982
  * ```
7388
7983
  */
7389
7984
  next(chord, opts) {
7390
- return new _Voicing(nextVoicing(this.pitches, toChordData(chord), opts));
7985
+ return new _Voicing(nextVoicing(this.pitches, toChordData(chord), withCarriedKey(chord, opts)));
7391
7986
  }
7392
7987
  /**
7393
7988
  * The voice-leading cost from this voicing to another: the summed absolute
7394
- * semitone motion across voices, plus a penalty when the outer voices reach a
7395
- * perfect fifth or octave by similar motion.
7989
+ * semitone motion across voices, and nothing besides. What the motion breaks
7990
+ * is {@link Voicing.checkTo}'s question.
7396
7991
  *
7397
7992
  * @param other The voicing moved to.
7398
7993
  * @returns The cost, or `Infinity` when the two hold a different number of
@@ -7429,14 +8024,15 @@ var Voicing = class _Voicing {
7429
8024
  * ```
7430
8025
  */
7431
8026
  checkTo(other, chords, key, opts) {
7432
- const scale = toKeyScale(key);
8027
+ assertKeyArgument(key, "voicing key");
8028
+ const resolved = resolveKey(key);
7433
8029
  const from = toChordData(chords[0]);
7434
8030
  const to = toChordData(chords[1]);
7435
8031
  const voicings = [
7436
- spellVoicing(this.pitches, from, scale),
7437
- spellVoicing(other.pitches, to, scale)
8032
+ spellVoicing(this.pitches, from, resolved),
8033
+ spellVoicing(other.pitches, to, resolved)
7438
8034
  ];
7439
- return checkPartWriting(voicings, [from, to], scale, opts);
8035
+ return checkPartWriting(voicings, [from, to], resolved, opts);
7440
8036
  }
7441
8037
  /**
7442
8038
  * Mark this voicing, read as a written counterpoint line, against a cantus
@@ -7465,12 +8061,13 @@ var Voicing = class _Voicing {
7465
8061
  * ```
7466
8062
  */
7467
8063
  species(cantusFirmus, species, mode, opts) {
7468
- const scale = toKeyScale(mode);
8064
+ assertKeyArgument(mode, "voicing mode");
8065
+ const resolved = resolveKey(mode);
7469
8066
  return checkSpecies(
7470
- spellLine(cantusFirmus, scale),
7471
- spellLine(this.#pitches, scale),
8067
+ spellLine(cantusFirmus, resolved),
8068
+ spellLine(this.#pitches, resolved),
7472
8069
  species,
7473
- scale,
8070
+ resolved,
7474
8071
  opts
7475
8072
  );
7476
8073
  }
@@ -7492,8 +8089,12 @@ var Voicing = class _Voicing {
7492
8089
  */
7493
8090
  independence(other, opts) {
7494
8091
  const { key, ...rest } = opts ?? {};
7495
- const scale = key === void 0 ? void 0 : toKeyScale(key);
7496
- return voiceIndependence(lineOf(this.#pitches, scale), lineOf(other.pitches, scale), rest);
8092
+ const resolved = key === void 0 ? void 0 : resolveKey(key);
8093
+ return voiceIndependence(
8094
+ lineOf(this.#pitches, resolved),
8095
+ lineOf(other.pitches, resolved),
8096
+ rest
8097
+ );
7497
8098
  }
7498
8099
  /**
7499
8100
  * Judge a candidate pitch placed against these voices.
@@ -7505,7 +8106,8 @@ var Voicing = class _Voicing {
7505
8106
  * @param pitch The candidate MIDI pitch.
7506
8107
  * @param query The candidate's harmonic and voice-leading context.
7507
8108
  * @param opts Set `suggestions: false` to skip the search for safe
7508
- * replacements when the verdict is not safe.
8109
+ * replacements when the verdict is not safe, and `partners: true` to learn
8110
+ * which voice of this voicing, by index, raised each pairwise flag.
7509
8111
  * @returns The verdict, reason bitmask, and optional resolution guidance.
7510
8112
  * @throws If the profile is unknown, a value names no chord or key, or a
7511
8113
  * pitch is not a MIDI pitch in 0..127.
@@ -7541,13 +8143,14 @@ var Voicing = class _Voicing {
7541
8143
  * times over.
7542
8144
  *
7543
8145
  * A negative `n` takes the highest voice down an octave as many times, and
7544
- * `invert(0)` is the voicing itself. Unlike {@link Chord.invert}, which
7545
- * rotates a chord's interval template, this moves real voices: inverting a
7546
- * four-voice texture four times leaves the same chord an octave higher. The
7547
- * result is ascending, since the voice that moved is no longer where it was.
8146
+ * `invert(0)` is the voicing itself, voice order and all. Unlike
8147
+ * {@link Chord.invert}, which rotates a chord's interval template, this moves
8148
+ * real voices: inverting a four-voice texture four times leaves the same chord
8149
+ * an octave higher. A voicing that actually inverts comes back ascending,
8150
+ * since the voice that moved is no longer where it was.
7548
8151
  *
7549
8152
  * @param n How many voices to move, upward when positive.
7550
- * @returns The inverted voicing, ascending.
8153
+ * @returns The inverted voicing, ascending; this voicing itself when `n` is 0.
7551
8154
  * @throws If `n` is not an integer in [-128, 128], or a voice would leave
7552
8155
  * MIDI 0..127.
7553
8156
  * @example
@@ -7558,6 +8161,9 @@ var Voicing = class _Voicing {
7558
8161
  */
7559
8162
  invert(n) {
7560
8163
  assertInteger(n, "voicing inversion", -128, 128);
8164
+ if (n === 0) {
8165
+ return this;
8166
+ }
7561
8167
  const pitches = [...this.#pitches].sort((a, b) => a - b);
7562
8168
  for (let moved = 0; moved < Math.abs(n); moved += 1) {
7563
8169
  const index = n > 0 ? 0 : pitches.length - 1;
@@ -7587,7 +8193,7 @@ var Voicing = class _Voicing {
7587
8193
  * Private class fields do not serialize, so an explicit `toJSON` keeps
7588
8194
  * `JSON.stringify(voicing)` from collapsing to `{}`.
7589
8195
  *
7590
- * @returns A copy of the sounding MIDI pitches, ascending.
8196
+ * @returns A copy of the sounding MIDI pitches, in voice order.
7591
8197
  */
7592
8198
  toJSON() {
7593
8199
  return [...this.#pitches];
@@ -7596,7 +8202,7 @@ var Voicing = class _Voicing {
7596
8202
  * The pitches separated by spaces, so a template literal or a log line reads
7597
8203
  * as the voicing.
7598
8204
  *
7599
- * @returns The pitches ascending, e.g. `'60 64 67'`.
8205
+ * @returns The pitches in voice order, e.g. `'60 64 67'`.
7600
8206
  */
7601
8207
  toString() {
7602
8208
  return this.#pitches.join(" ");
@@ -7615,7 +8221,7 @@ var Voicing = class _Voicing {
7615
8221
  return {
7616
8222
  profile: query.profile,
7617
8223
  chord: query.chord === null ? null : toChordData(query.chord),
7618
- key: toKeyScale(query.key),
8224
+ key: resolveKey(query.key).scale,
7619
8225
  otherVoices,
7620
8226
  strongBeat: query.strongBeat,
7621
8227
  prevPitch: query.prevPitch,
@@ -7644,4 +8250,4 @@ export {
7644
8250
  Tuning,
7645
8251
  Voicing
7646
8252
  };
7647
- //# sourceMappingURL=chunk-PKFNUHDY.js.map
8253
+ //# sourceMappingURL=chunk-FBH75DAY.js.map