@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,8 +1,8 @@
1
- import { d as KeyScale, g as NoteEvent, S as SpelledInterval } from './index-B9MNZboQ.cjs';
2
- import { d as ChordQuality, b as Chord, N as NoteSafety, S as SafetyProfile } from './index-C_8o--VE.cjs';
3
- import { r as ScaleName } from './masks-DYbE485J.cjs';
4
- import { i as KeyProfileName, j as KeyProfilePair, R as RejectedCandidate, k as KeyRegion, e as ChordTimeline, C as CadenceHit, a as AnalyzedNote, T as TheoryLabel, K as KeyContext, b as CadenceResult } from './index-Dzr13q3d.cjs';
5
- import { T as TimeSignature, c as MeterMap, b as MeterLike } from './index-BkGmlyI3.cjs';
1
+ import { e as KeyScale, h as NoteEvent, i as NoteLike, g as Note, S as SpelledInterval } from './index-Cy15oTvh.cjs';
2
+ import { d as ChordQuality, k as KeyVariant, b as Chord, r as SpelledKeyLike, c as ChordLike, K as KeyLike, n as ResolvedKey, N as NoteSafety, o as SafetyProfile } from './coerce-DRERzdve.cjs';
3
+ import { r as ScaleName } from './masks-pADfTOiD.cjs';
4
+ import { i as KeyProfileName, j as KeyProfilePair, R as RejectedCandidate, k as KeyRegion, e as ChordTimeline, C as CadenceHit, a as AnalyzedNote, T as TheoryLabel, K as KeyContext, b as CadenceResult } from './index-BBWNZS0f.cjs';
5
+ import { b as MeterLike } from './index-BMweUlm5.cjs';
6
6
 
7
7
  /**
8
8
  * Church-mode candidates for {@link detectKey}.
@@ -164,12 +164,7 @@ type KeyMatch = {
164
164
  */
165
165
  alternatives?: RejectedCandidate[];
166
166
  };
167
- /**
168
- * Which form of a scale a {@link KeyMatch} settled on.
169
- *
170
- * @category Recognition
171
- */
172
- type KeyVariant = 'major' | 'natural' | 'harmonic' | 'melodic' | 'modal';
167
+
173
168
  /**
174
169
  * Input weighting for {@link detectKey}.
175
170
  *
@@ -249,8 +244,13 @@ type DetectKeyOptions = {
249
244
  * A match is reported when all of the chord's tones are present in the input, or
250
245
  * when the only absent tone is the perfect fifth (a common omission in shell
251
246
  * voicings); matches are ranked best-first by fewest extra notes, then fewest
252
- * missing notes, then most specific (largest) chord. An exact match (no extras,
253
- * no missing) is flagged and ranked first.
247
+ * missing notes, then root position over an inversion, then most specific
248
+ * (largest) chord. An exact match (no extras, no missing) is flagged and ranked
249
+ * first.
250
+ *
251
+ * The root-position step decides between readings of the same pitches that are
252
+ * otherwise equally good: `[55, 59, 62, 64]` is read as G6 rather than Em7,
253
+ * because G is in the bass.
254
254
  *
255
255
  * @param pitches MIDI pitches or bare pitch classes (octave-agnostic).
256
256
  * @returns Ranked chord interpretations (may be empty).
@@ -329,7 +329,7 @@ declare function detectChordBest(pitches: readonly number[], opts?: DetectChordO
329
329
  * import { detectKey } from '@libraz/libcantus';
330
330
  * const keys = detectKey([60, 62, 64, 65, 67, 69, 71]); // C major scale
331
331
  * keys[0].mode; // 'major', with keys[0].key.rootPc === 0
332
- * keys[1].mode; // 'minor' on 9: A minor, the relative, ranked just below
332
+ * keys[1].mode; // 'minor' — on 9: A minor, the relative, ranked just below
333
333
  * const riff = detectKey([62, 62, 65, 69, 71, 69, 65, 62], { modes: true });
334
334
  * riff[0].scaleName; // 'dorian', on rootPc 2, still reported as mode 'minor'
335
335
  * ```
@@ -342,11 +342,21 @@ declare function detectKey(pitches: readonly number[], opts?: DetectKeyOptions):
342
342
  *
343
343
  * {@link detectKey} counts every pitch once, which lets a run of fast ornamental
344
344
  * notes outvote the sustained harmony that establishes the key. This weighs
345
- * each note by duration times velocity, the same measure chord inference uses,
346
- * so the two agree on what the music emphasises. Notes that never sound (zero
347
- * or negative duration) are ignored. The weighted distribution is then ranked
348
- * exactly as {@link detectKey} ranks it, by correlation against a key profile,
349
- * so `score` is a finite correlation in [-1, 1].
345
+ * each note by the same measure chord inference weighs its own histogram by, so
346
+ * the two agree on what the music emphasises: duration times
347
+ * velocity, with a note that carries no velocity counted at full weight rather
348
+ * than at an assumed one. Notes that never sound (zero or negative duration)
349
+ * are ignored. The weighted distribution is then ranked exactly as
350
+ * {@link detectKey} ranks it, by correlation against a key profile, so `score`
351
+ * is a finite correlation in [-1, 1].
352
+ *
353
+ * The one term of that measure this cannot share is the metric accent, which
354
+ * chord inference adds to an onset by the beat it falls on: the notes arrive
355
+ * here without the meter they are counted in, and reading them against an
356
+ * assumed one would rank the same notes differently under a signature the
357
+ * caller never named. Ranking a whole piece is the coarser question, and the
358
+ * accents are what separate one window from the next rather than one key from
359
+ * another.
350
360
  *
351
361
  * @param notes The note events to weigh.
352
362
  * @param opts Which profile to rank with, whether the modes take part, and the
@@ -398,6 +408,158 @@ declare function detectKeyFromNotes(notes: readonly NoteEvent[], opts?: Omit<Det
398
408
  */
399
409
  declare function detectKeyBest(pitches: readonly number[], opts?: DetectKeyOptions): KeyMatch | null;
400
410
 
411
+ /**
412
+ * Augmented-sixth chords: the Italian, French, and German sixths.
413
+ *
414
+ * All three stand on the lowered submediant (b6) in the bass and carry an
415
+ * augmented sixth above it, and all three are predominants whose two outer
416
+ * voices resolve outward onto the dominant. The family is defined by that bass
417
+ * and that interval rather than by a stack of thirds, so a chord is recognized
418
+ * here from the tones sounding over that bass rather than through the
419
+ * Roman-numeral degree tables.
420
+ *
421
+ * The interval is spelled, so recognition reads letters and not only pitch
422
+ * classes: over an Ab bass the augmented sixth is F# and the minor seventh of
423
+ * the bVI7 it otherwise sounds exactly like is Gb. A caller holding pitch
424
+ * classes alone — an analysis of a MIDI track, which names no accidentals —
425
+ * makes that reading with {@link augmentedSixthFromPitchClasses}, and the chord
426
+ * it hands back carries the spelling that decided it.
427
+ */
428
+
429
+ /**
430
+ * The three augmented-sixth chords, under the nationalities convention gives
431
+ * them.
432
+ *
433
+ * Over an Ab bass in C: the Italian sixth is Ab C F#, the French sixth adds the
434
+ * supertonic (Ab C D F#), and the German sixth adds the lowered mediant
435
+ * (Ab C Eb F#).
436
+ *
437
+ * @category Functional Harmony
438
+ */
439
+ type AugmentedSixthKind = 'italian' | 'french' | 'german';
440
+ /**
441
+ * Build an augmented-sixth chord in a key.
442
+ *
443
+ * The chord always carries an explicit `bassPc` on the lowered submediant,
444
+ * which is what makes it an augmented sixth rather than the dominant seventh or
445
+ * the altered supertonic seventh it shares pitch classes with, and an explicit
446
+ * interval template naming exactly the tones that sound.
447
+ *
448
+ * It also carries its spelling: the tones as `toneSpellings`, and the root and
449
+ * bass as the usual hints. Those are what let the ordinary chord speller and
450
+ * the symbol formatter name the chord — `Ab C Eb F#` and `Ab7` for the German
451
+ * sixth of C major — since a stack of thirds over that root would put a Gb
452
+ * where the augmented sixth belongs. The spelling is the one the key is written
453
+ * on, so a flat-side key takes the double flats it genuinely needs.
454
+ *
455
+ * The lowered submediant is taken as eight semitones above the tonic in every
456
+ * mode, since the chord is a chromatic alteration rather than a degree of the
457
+ * prevailing scale.
458
+ *
459
+ * @param kind Which of the three augmented sixths to build.
460
+ * @param key The prevailing key, as a key name, a key/scale, or a `Key`.
461
+ * @returns The chord, with its bass on the lowered submediant.
462
+ * @throws If `kind` is not one of the three, or the key's root is not finite.
463
+ * @see {@link spellAugmentedSixth} to spell the family from a tonic without
464
+ * building a chord at all.
465
+ * @example
466
+ * ```ts
467
+ * import { augmentedSixthChord, majorKey } from '@libraz/libcantus';
468
+ * augmentedSixthChord('german', majorKey(0));
469
+ * // Ab C Eb F# in C: { rootPc: 8, quality: 'dom7', intervals: [0, 4, 7, 10], bassPc: 8, ... }
470
+ * ```
471
+ * @category Functional Harmony
472
+ */
473
+ declare function augmentedSixthChord(kind: AugmentedSixthKind, key: SpelledKeyLike): Chord;
474
+ /**
475
+ * Identify a chord as one of the augmented sixths, or null when it is none.
476
+ *
477
+ * A chord qualifies when it sounds the lowered submediant of `key` in the bass,
478
+ * its pitch classes are exactly one of the three sets built over that bass, and
479
+ * its own letters spell the augmented sixth above it. All three are needed: the
480
+ * same pitch classes over the same bass are an ordinary bVI7 when the tone ten
481
+ * semitones above the bass is written as a minor seventh, and the letters are
482
+ * the only place that distinction lives.
483
+ *
484
+ * A chord naming no `bassPc` sounds its root lowest, which is how the two
485
+ * conventions for that field — the sounding bass always, or only a bass that
486
+ * differs from the root — are read alike here.
487
+ *
488
+ * The root the chord is measured from is not consulted, so a chord voiced or
489
+ * rebuilt around any of its own tones still identifies as long as it keeps its
490
+ * spelling.
491
+ *
492
+ * @param chord The chord to test, as a chord symbol, chord data, or a `Chord`.
493
+ * @param key The prevailing key, as a key name, a key/scale, or a `Key`.
494
+ * @returns The kind of augmented sixth, or null.
495
+ * @see {@link augmentedSixthFromPitchClasses} to make the reading from pitch
496
+ * classes that carry no spelling at all.
497
+ * @example
498
+ * ```ts
499
+ * import { augmentedSixthKind, majorKey, romanToChord } from '@libraz/libcantus';
500
+ * augmentedSixthKind(romanToChord('Fr6', majorKey(0)), majorKey(0)); // 'french'
501
+ * ```
502
+ * @category Functional Harmony
503
+ */
504
+ declare function augmentedSixthKind(chord: ChordLike, key: KeyLike): AugmentedSixthKind | null;
505
+ /**
506
+ * Read a set of sounding pitch classes over a sounding bass as an augmented
507
+ * sixth, or null when they are none.
508
+ *
509
+ * The reading a caller holding no spelling has to make for itself: pitch
510
+ * classes name no accidentals, so nothing downstream can tell the German sixth
511
+ * from the bVI7 it sounds like once the letters are gone. The chord returned is
512
+ * the one {@link augmentedSixthChord} builds, spelling and all, so it keeps
513
+ * identifying as an augmented sixth everywhere it travels.
514
+ *
515
+ * Not every b6-1-b3-#4 over a b6 bass is an augmented sixth, and this cannot
516
+ * know which one is: it answers what the tones would spell, and the caller
517
+ * decides whether that reading is the one the music supports.
518
+ *
519
+ * @param pcs The sounding pitch classes; duplicates and unreduced values are
520
+ * fine.
521
+ * @param bassPc The pitch class sounding in the bass.
522
+ * @param key The prevailing key, as a key name, a key/scale, or a `Key`.
523
+ * @returns The augmented-sixth chord, or null.
524
+ * @see {@link augmentedSixthKind} to read a chord that already carries its
525
+ * spelling.
526
+ * @example
527
+ * ```ts
528
+ * import { augmentedSixthFromPitchClasses, majorKey } from '@libraz/libcantus';
529
+ * // Ab C Eb Gb sounding over Ab in C: the German sixth, spelled with its F#.
530
+ * augmentedSixthFromPitchClasses([8, 0, 3, 6], 8, majorKey(0))?.quality; // 'dom7'
531
+ * ```
532
+ * @category Functional Harmony
533
+ */
534
+ declare function augmentedSixthFromPitchClasses(pcs: readonly number[], bassPc: number, key: SpelledKeyLike): Chord | null;
535
+ /**
536
+ * Spell an augmented-sixth chord from a spelled tonic, lowest note first.
537
+ *
538
+ * This is the spelled counterpart of {@link augmentedSixthChord}, and the table
539
+ * the chord's own spelling is built from: every tone is placed by a named
540
+ * interval above the bass, so the outer interval is an augmented sixth in every
541
+ * key rather than the minor seventh its ten semitones would otherwise imply.
542
+ * Reach for it when a spelled tonic is all there is; spelling the chord itself
543
+ * with {@link spellChord} names the same notes, in the chord's own order.
544
+ *
545
+ * A tonic carrying an octave yields notes carrying octaves, ascending from the
546
+ * bass; an octave-less tonic yields octave-less notes.
547
+ *
548
+ * @param kind Which of the three augmented sixths to spell.
549
+ * @param tonic The spelled tonic of the key, as a note name, a MIDI number,
550
+ * note data, or a `Note`.
551
+ * @returns The spelled tones, bass first and ascending.
552
+ * @throws If `kind` is not one of the three, or `tonic` is not a note.
553
+ * @example
554
+ * ```ts
555
+ * import { noteNames, parseNote, spellAugmentedSixth } from '@libraz/libcantus';
556
+ * noteNames(spellAugmentedSixth('german', parseNote('C'))); // ['Ab', 'C', 'Eb', 'F#']
557
+ * noteNames(spellAugmentedSixth('italian', 'E')); // ['C', 'E', 'A#']
558
+ * ```
559
+ * @category Functional Harmony
560
+ */
561
+ declare function spellAugmentedSixth(kind: AugmentedSixthKind, tonic: NoteLike): Note[];
562
+
401
563
  /**
402
564
  * Arrangement track-role analysis: a single harmony is inferred from every
403
565
  * track's notes pooled together, each track is then analysed against that shared
@@ -441,6 +603,21 @@ type TrackAnalysis = {
441
603
  /** One {@link AnalyzedNote} per note, labelled against the inferred chords. */
442
604
  notes: AnalyzedNote[];
443
605
  };
606
+ /**
607
+ * A note sounding with a {@link Conflict} that raises a pairwise flag against it.
608
+ *
609
+ * @category Arrangement & Analysis
610
+ */
611
+ type ConflictPartner = {
612
+ /** The partner's track index in the caller's input array. */
613
+ trackIndex: number;
614
+ /** The partner note's index in that track's own note array. */
615
+ originalIndex?: number;
616
+ /** The id the partner note carries in {@link TrackAnalysis.notes}. */
617
+ noteId: number;
618
+ /** The pairwise flags this note raises, as {@link SafetyPartner.reasons}. */
619
+ reasons: number;
620
+ };
444
621
  /**
445
622
  * A note that clashes with the harmony sounding beneath it.
446
623
  *
@@ -470,6 +647,11 @@ type Conflict = {
470
647
  resolveTo?: number;
471
648
  /** Nearby fully safe replacement pitches, nearest first. */
472
649
  suggestions?: number[];
650
+ /**
651
+ * The notes behind the pairwise flags in `reasons` — a parallel fifth, a
652
+ * crossing, a vertical clash — when any are set.
653
+ */
654
+ partners?: ConflictPartner[];
473
655
  rationale?: string;
474
656
  };
475
657
  /**
@@ -485,8 +667,11 @@ type ArrangementAnalysis = {
485
667
  * started in for every bar after it leaves.
486
668
  */
487
669
  keys: KeyRegion[];
488
- /** The key held longest across {@link ArrangementAnalysis.keys}. */
489
- prevailingKey: KeyScale;
670
+ /**
671
+ * The key held longest across {@link ArrangementAnalysis.keys}, whole: the
672
+ * pitch classes with the spelling and the scale form the regions carry.
673
+ */
674
+ prevailingKey: ResolvedKey;
490
675
  timeline: ChordTimeline;
491
676
  segmentConfidence: number[];
492
677
  cadences: CadenceHit[];
@@ -504,8 +689,11 @@ type ArrangementOptions = {
504
689
  * A single key held across the whole arrangement. Omit it, and `keys` with
505
690
  * it, to have the key searched for over time so a modulating piece is read
506
691
  * against the key actually in force.
692
+ *
693
+ * Taken in whatever form the caller holds a key, and kept whole: the region
694
+ * built from it carries the spelling and the scale form it was named with.
507
695
  */
508
- key?: KeyScale;
696
+ key?: KeyLike;
509
697
  /**
510
698
  * Key regions to judge against, when a previous pass already worked them out.
511
699
  * Takes precedence over `key`; supplying both is answering the same question
@@ -536,14 +724,14 @@ type ArrangementOptions = {
536
724
  *
537
725
  * @defaultValue `4/4`
538
726
  */
539
- ts?: TimeSignature;
727
+ ts?: MeterLike;
540
728
  /**
541
729
  * The meter as it changes over the arrangement. Bar lines, downbeats and
542
730
  * metric weight all follow the signature in force at the beat in question.
543
731
  *
544
732
  * @defaultValue 4/4 throughout
545
733
  */
546
- meters?: MeterMap;
734
+ meters?: MeterLike;
547
735
  /**
548
736
  * Length of the pickup in beats, when the piece starts with one.
549
737
  *
@@ -555,7 +743,13 @@ type ArrangementOptions = {
555
743
  */
556
744
  pickupBeats?: number;
557
745
  /**
558
- * Chord-slot length in beats; defaults to the length of the opening bar.
746
+ * Expected chord length in beats; defaults to the length of the opening bar.
747
+ *
748
+ * A prior rather than a grid: the arrangement analysis infers its harmony
749
+ * with dynamic segmentation, where the longer a chord is expected to last the
750
+ * more evidence a change needs before the search will place one. A chord may
751
+ * still begin at any position the notes argue for. Supply `timeline` to
752
+ * analyse against boundaries you have fixed yourself.
559
753
  *
560
754
  * @defaultValue the length of the opening bar
561
755
  */
@@ -578,6 +772,26 @@ type ArrangementOptions = {
578
772
  */
579
773
  budget?: number;
580
774
  };
775
+ /**
776
+ * Label every note of one polyphonic passage against the harmony under it and
777
+ * the voices around it.
778
+ *
779
+ * {@link analyzeVoice} reads a single line: it takes the note before and the note
780
+ * after each note as that note's melodic neighbours, and it is given no other
781
+ * voices, so over a flat array of everything that sounds it invents figures
782
+ * across parts — the alto between two soprano notes becomes a passing tone —
783
+ * and can never find a suspension, which is by definition a dissonance against
784
+ * something else sounding. So the passage is partitioned into monophonic
785
+ * sub-voices first and each is read with the others handed to it, which is what
786
+ * {@link analyzeArrangement} does with the tracks it is given.
787
+ *
788
+ * @param notes The passage, in any order; the ids are indices into this array.
789
+ * @param chordAtBeat Chord sounding at a given beat, or null.
790
+ * @param key The key, or the key in force at a given beat.
791
+ * @returns One annotation per note, in the order the notes were given, each
792
+ * carrying its position in that array as its id.
793
+ */
794
+ declare function analyzePolyphony(notes: readonly NoteEvent[], chordAtBeat: (beat: number) => Chord | null, key: KeyContext): AnalyzedNote[];
581
795
  /**
582
796
  * Analyse a whole arrangement against the harmony inferred from it.
583
797
  *
@@ -622,7 +836,7 @@ type ArrangementOptions = {
622
836
  * { pitch: 67, startBeat: 2, durationBeat: 2 },
623
837
  * ];
624
838
  * const { prevailingKey, conflicts } = analyzeArrangement([{ role: 'melody', notes: melody }]);
625
- * prevailingKey.rootPc; // the key held longest across the analysis
839
+ * prevailingKey.scale.rootPc; // the key held longest across the analysis
626
840
  * conflicts; // notes clashing with the inferred harmony, worst severity first
627
841
  * ```
628
842
  * @category Arrangement & Analysis
@@ -786,6 +1000,11 @@ declare function tensionCurve(tracks: ArrangementTrack[], opts?: ArrangementOpti
786
1000
  * twice over the same notes. This takes the analysis it already produced, so
787
1001
  * the curve and the annotations describe the same harmony by construction.
788
1002
  *
1003
+ * The analysis is the harmony to fall back on rather than one laid over the
1004
+ * caller: `key`, `keys` and `harmonyTracks` each name a harmony the analysis
1005
+ * does not hold, so naming one is asking for a different reading, and the curve
1006
+ * is taken under it. Nothing else about the analysis is re-derived.
1007
+ *
789
1008
  * @param tracks The same tracks that were analysed.
790
1009
  * @param analysis The result of {@link analyzeArrangement} for those tracks.
791
1010
  * @param opts Analysis options plus an optional `step` (default one beat).
@@ -875,9 +1094,11 @@ type HypermeterOptions = {
875
1094
  * Groupings of 2, 3, 4, 6, and 8 bars are tried at every phase, and each
876
1095
  * reading is scored on two counts: how much more harmonic change lands on the
877
1096
  * bars it calls group heads than on the bars between them, and how many of the
878
- * cadences it is given arrive where it says a hyperbar ends. The commonest
879
- * groupings are believed a little more readily than the rare ones, which
880
- * settles ties without letting a prior overrule the music.
1097
+ * hyperbar ends it predicts are closed by one of the cadences it is given.
1098
+ * Where cadences are given they settle the phase, since a hyperbar is what a
1099
+ * cadence ends; the harmony then chooses among the phases they agree with. The
1100
+ * commonest groupings are believed a little more readily than the rare ones,
1101
+ * which settles ties without letting a prior overrule the music.
881
1102
  *
882
1103
  * The meter is read at the beat in question rather than at the start, so a
883
1104
  * piece that changes metre keeps being cut at its own bar lines. Bar 0 is the
@@ -989,15 +1210,17 @@ type PhraseOptions = {
989
1210
  *
990
1211
  * @defaultValue `4/4`
991
1212
  */
992
- ts?: TimeSignature;
1213
+ ts?: MeterLike;
993
1214
  /**
994
- * The meter as it changes over the span. Bar lines, hypermetric downbeats and
995
- * the default phrase length all follow the signature in force at the beat in
996
- * question, so a piece that changes metre is not read in the one it opened in.
1215
+ * The meter as it changes over the span. Bar lines and hypermetric downbeats
1216
+ * follow the signature in force at the beat in question, so a piece that
1217
+ * changes metre is cut at its own bar lines; the length defaults below are
1218
+ * lengths rather than positions, and each is taken once from the meter the
1219
+ * span opens in.
997
1220
  *
998
1221
  * @defaultValue 4/4 throughout
999
1222
  */
1000
- meters?: MeterMap;
1223
+ meters?: MeterLike;
1001
1224
  /**
1002
1225
  * Key context for cadence detection: one key for the whole span, or the key
1003
1226
  * in force at a given beat. Omit it to have the key searched for over time,
@@ -1084,7 +1307,10 @@ type StructuralCadence = {
1084
1307
  *
1085
1308
  * Three kinds of evidence are gathered independently and then reconciled. The
1086
1309
  * harmony contributes its cadences, each closing at the end of the chord it
1087
- * arrives on. The melody contributes the rests it breathes at and the notes it
1310
+ * arrives on, and at the bar line where that chord is held across one — a tonic
1311
+ * still sounding into the next phrase closed this one at the bar line. The
1312
+ * cadence reported for a phrase is always one that arrived inside it. The
1313
+ * melody contributes the rests it breathes at and the notes it
1088
1314
  * holds long enough to read as arrivals. Repetition contributes the beats where
1089
1315
  * the music restarts material it has just played. The metre contributes its
1090
1316
  * hypermetric downbeats, which confirm a boundary but never declare one on
@@ -1107,18 +1333,25 @@ type StructuralCadence = {
1107
1333
  * that never sound are dropped.
1108
1334
  * @param opts Analysis options; see {@link PhraseOptions}.
1109
1335
  * @returns The phrases, in time order; one phrase covering the whole span when
1110
- * the span is too short to divide.
1336
+ * the span is too short to divide, and none at all when nothing sounds —
1337
+ * silence is no material to hear a phrase in.
1111
1338
  * @example
1112
1339
  * ```ts
1113
1340
  * import { chordTimelineFromNotes, phrasesFromTimeline } from '@libraz/libcantus';
1341
+ * // Four bars of C - F - G - C. Triads rather than single notes: one line at a
1342
+ * // time names no harmony, so nothing would cadence.
1343
+ * const triad = (pitches: number[], startBeat: number) =>
1344
+ * pitches.map((pitch) => ({ pitch, startBeat, durationBeat: 4 }));
1114
1345
  * const notes = [
1115
- * { pitch: 60, startBeat: 0, durationBeat: 4 },
1116
- * { pitch: 67, startBeat: 4, durationBeat: 4 },
1117
- * { pitch: 60, startBeat: 8, durationBeat: 8 },
1346
+ * ...triad([60, 64, 67], 0),
1347
+ * ...triad([65, 69, 72], 4),
1348
+ * ...triad([67, 71, 74], 8),
1349
+ * ...triad([60, 64, 67], 12),
1118
1350
  * ];
1119
1351
  * const { timeline } = chordTimelineFromNotes(notes);
1120
1352
  * const phrases = phrasesFromTimeline(timeline, notes);
1121
- * phrases[phrases.length - 1]?.cadence?.cadence.type; // 'authentic', when one closes it
1353
+ * phrases[phrases.length - 1]?.cadence?.cadence.type; // 'authentic'
1354
+ * phrases[phrases.length - 1]?.cadence?.atBeat; // 12, inside the phrase it closes
1122
1355
  * ```
1123
1356
  * @category Arrangement & Analysis
1124
1357
  */
@@ -1136,14 +1369,18 @@ declare function phrasesFromTimeline(timeline: ChordTimeline, notes: readonly No
1136
1369
  * @example
1137
1370
  * ```ts
1138
1371
  * import { chordTimelineFromNotes, phrasesFromTimeline, structuralCadences } from '@libraz/libcantus';
1372
+ * const triad = (pitches: number[], startBeat: number) =>
1373
+ * pitches.map((pitch) => ({ pitch, startBeat, durationBeat: 4 }));
1139
1374
  * const notes = [
1140
- * { pitch: 60, startBeat: 0, durationBeat: 4 },
1141
- * { pitch: 67, startBeat: 4, durationBeat: 4 },
1142
- * { pitch: 60, startBeat: 8, durationBeat: 8 },
1375
+ * ...triad([60, 64, 67], 0),
1376
+ * ...triad([65, 69, 72], 4),
1377
+ * ...triad([67, 71, 74], 8),
1378
+ * ...triad([60, 64, 67], 12),
1143
1379
  * ];
1144
1380
  * const { timeline } = chordTimelineFromNotes(notes);
1145
1381
  * const ranked = structuralCadences(phrasesFromTimeline(timeline, notes));
1146
- * ranked[0]; // the cadence carrying the most structural weight, if any
1382
+ * ranked[0]?.cadence.type; // 'authentic' — the cadence carrying the most weight
1383
+ * ranked[0]?.atBeat; // 12
1147
1384
  * ```
1148
1385
  * @category Arrangement & Analysis
1149
1386
  */
@@ -1206,14 +1443,14 @@ type FormSectionOptions = {
1206
1443
  *
1207
1444
  * @defaultValue `4/4`
1208
1445
  */
1209
- ts?: TimeSignature;
1446
+ ts?: MeterLike;
1210
1447
  /**
1211
1448
  * The meter as it changes over the span, so a piece that changes metre is cut
1212
1449
  * at its own bar lines.
1213
1450
  *
1214
1451
  * @defaultValue 4/4 throughout
1215
1452
  */
1216
- meters?: MeterMap;
1453
+ meters?: MeterLike;
1217
1454
  /**
1218
1455
  * Length of the unit spans are compared in, in bars; defaults to one hyperbar.
1219
1456
  *
@@ -1278,58 +1515,15 @@ type FormSectionOptions = {
1278
1515
  declare function sectionsFromNotes(notes: readonly NoteEvent[], opts?: FormSectionOptions): FormSection[];
1279
1516
 
1280
1517
  /**
1281
- * Melody as a shape, rather than as a run of notes.
1518
+ * The recurring cells a melody is built from.
1282
1519
  *
1283
- * A motif is held here as an interval contour plus a rhythmic profile, which is
1284
- * what makes a restatement at another pitch level the same motif; the absolute
1285
- * pitches only ever describe one statement of it. On that footing two statements
1286
- * can be compared and the transformation between them named — repetition,
1287
- * transposition, inversion, retrograde, augmentation — and phrases that stand in
1288
- * no such relation can still be scored for similarity.
1520
+ * A motif is the interval contour plus the rhythmic profile, so every statement
1521
+ * of it carries the same identity however far it has been transposed or however
1522
+ * wide its note values are written. What this module does is find them: every
1523
+ * window of the melody is reduced to that identity, windows that agree are
1524
+ * grouped, and a group stated often enough is a motif.
1289
1525
  */
1290
1526
 
1291
- /**
1292
- * Step direction between two consecutive notes.
1293
- *
1294
- * @category Arrangement & Analysis
1295
- */
1296
- type ContourDirection = 'up' | 'down' | 'same';
1297
- /**
1298
- * The overall shape of a line.
1299
- *
1300
- * The first four names are the vocabulary the motif generator's contours are
1301
- * written in, and mean the same here. `'static'` is the shape a generator never
1302
- * asks for and an analysis regularly meets: a line that does not move. A line
1303
- * that dips and comes back — an arch upside down — reads as `'wave'`, since the
1304
- * shared vocabulary has no name of its own for it.
1305
- *
1306
- * A wave has to turn more than once to be heard as one, so a cell the generator
1307
- * writes for `'wave'` reads back as `'wave'` only from three bars up; the one-
1308
- * and two-bar cells turn once and read as `'arch'`. The other three shapes come
1309
- * back under their own name at every length.
1310
- *
1311
- * @category Arrangement & Analysis
1312
- */
1313
- type MelodicContourShape = 'arch' | 'ascending' | 'descending' | 'wave' | 'static';
1314
- /**
1315
- * The abstract shape of a line: its step directions and what they add up to.
1316
- *
1317
- * @category Arrangement & Analysis
1318
- */
1319
- type MelodicContour = {
1320
- /** One direction per adjacent pair of notes, in time order. */
1321
- directions: ContourDirection[];
1322
- /** What the directions add up to. */
1323
- shape: MelodicContourShape;
1324
- /** Index of the highest note; ties keep the earliest. */
1325
- peakIndex: number;
1326
- /** Index of the lowest note; ties keep the earliest. */
1327
- troughIndex: number;
1328
- /** Semitone distance from the lowest note to the highest. */
1329
- range: number;
1330
- /** Why the line was given that shape. */
1331
- rationale: string;
1332
- };
1333
1527
  /**
1334
1528
  * One statement of a motif, addressed in the melody it was found in.
1335
1529
  *
@@ -1418,71 +1612,6 @@ type ExtractMotifsOptions = {
1418
1612
  */
1419
1613
  budget?: number;
1420
1614
  };
1421
- /**
1422
- * How one statement of a motif stands to another.
1423
- *
1424
- * `'transposition'` is the literal one — every interval preserved, which is what
1425
- * makes a sequence built on it a *real* sequence. `'tonalTransposition'` shifts
1426
- * by scale degrees instead and lets the key resize the intervals, which is what
1427
- * makes a sequence built on it a *tonal* sequence; the two are different devices
1428
- * and are never reported as one. `'augmentation'` and `'diminution'` name the
1429
- * relation only when the pitches are untouched, so an inversion in doubled note
1430
- * values reads as `'inversion'` with a `timeRatio` of 2 rather than losing the
1431
- * inversion to the stretch.
1432
- *
1433
- * @category Arrangement & Analysis
1434
- */
1435
- type MotifRelationKind = 'repetition' | 'transposition' | 'tonalTransposition' | 'inversion' | 'retrograde' | 'retrogradeInversion' | 'augmentation' | 'diminution';
1436
- /**
1437
- * The transformation that turns one motif into another.
1438
- *
1439
- * @category Arrangement & Analysis
1440
- */
1441
- type MotifRelation = {
1442
- /** The named transformation. */
1443
- kind: MotifRelationKind;
1444
- /**
1445
- * Whether the second statement begins where the first ends, which is what
1446
- * separates a sequence from a restatement elsewhere in the piece: a
1447
- * `'transposition'` that is one is a real sequence, a `'tonalTransposition'`
1448
- * that is one is a tonal sequence.
1449
- */
1450
- sequence: boolean;
1451
- /** Semitones from the first statement's first note to the second's. */
1452
- semitones: number;
1453
- /** The same distance named as a spelled interval. */
1454
- interval: SpelledInterval;
1455
- /** Scale degrees the second statement stands above the first, when tonal. */
1456
- degrees?: number;
1457
- /**
1458
- * How much the second statement is stretched against the first: 2 for doubled
1459
- * note values, 0.5 for halved, 1 for unchanged.
1460
- */
1461
- timeRatio: number;
1462
- /** Why the pair was given that name. */
1463
- rationale: string;
1464
- };
1465
- /**
1466
- * Anything that can be read as a melodic line: a motif, or plain note events.
1467
- *
1468
- * @category Arrangement & Analysis
1469
- */
1470
- type MelodicPhrase = MotifData | readonly NoteEvent[];
1471
- /**
1472
- * How alike two lines are, and how the answer was reached.
1473
- *
1474
- * @category Arrangement & Analysis
1475
- */
1476
- type MelodicComparison = {
1477
- /** Overall likeness in [0, 1]; 1 for the same line, transposition included. */
1478
- similarity: number;
1479
- /** Likeness of the interval sequences alone, in [0, 1]. */
1480
- pitchSimilarity: number;
1481
- /** Likeness of the rhythmic profiles alone, in [0, 1]. */
1482
- rhythmSimilarity: number;
1483
- /** Why the lines scored what they did. */
1484
- rationale: string;
1485
- };
1486
1615
  /**
1487
1616
  * Find the cells a melody keeps coming back to.
1488
1617
  *
@@ -1496,9 +1625,11 @@ type MelodicComparison = {
1496
1625
  * rather than every fragment of it.
1497
1626
  *
1498
1627
  * The melody is expected to be monophonic, as {@link analyzeVoice} expects a
1499
- * voice to be; split polyphonic material into lines first. Onsets are expected to
1500
- * be quantised, since two statements must agree on their rhythm to be recognised
1501
- * as one motif.
1628
+ * voice to be; split polyphonic material into lines first. Notes struck together
1629
+ * are read as one event, the highest of them standing for it, so a chord is
1630
+ * reduced to its top voice rather than read as a line of its own. Onsets are
1631
+ * expected to be quantised, since two statements must agree on their rhythm to
1632
+ * be recognised as one motif.
1502
1633
  *
1503
1634
  * @param notes The melody, in time order. Notes that never sound are dropped.
1504
1635
  * @param opts Cell-length bounds and the recurrence threshold; see
@@ -1529,7 +1660,13 @@ declare function extractMotifs(notes: readonly NoteEvent[], opts?: ExtractMotifs
1529
1660
  * caller names one it already has — a subject, an answer, a phrase lifted out of
1530
1661
  * a score — so that it can be handed to {@link relateMotifs}.
1531
1662
  *
1532
- * @param notes The cell, in time order. Notes that never sound are dropped.
1663
+ * The cell is expected to be monophonic, as {@link analyzeVoice} expects a voice
1664
+ * to be. Notes struck together are read as one event, the highest of them
1665
+ * standing for it, so a chord is reduced to its top voice rather than read as a
1666
+ * line of its own.
1667
+ *
1668
+ * @param notes The cell, in time order. Notes that never sound are dropped, and
1669
+ * notes sharing an onset are folded to their top voice.
1533
1670
  * @returns The motif, carrying exactly one occurrence.
1534
1671
  * @example
1535
1672
  * ```ts
@@ -1549,102 +1686,74 @@ declare function extractMotifs(notes: readonly NoteEvent[], opts?: ExtractMotifs
1549
1686
  * @category Arrangement & Analysis
1550
1687
  */
1551
1688
  declare function motifFromNotes(notes: readonly NoteEvent[]): MotifData;
1689
+
1552
1690
  /**
1553
- * Name how two statements of a motif relate.
1691
+ * What every reading of a melody has to agree on first.
1554
1692
  *
1555
- * The two are compared as shapes, not as pitches: the interval sequences decide
1556
- * the pitch transformation and the onset gaps decide the timing, so a statement
1557
- * a fourth higher in doubled note values is recognised for what it is. A pair
1558
- * whose rhythms do not correspond — the same way round for a repetition or a
1559
- * transposition, back to front for a retrograde — is not a transformation of the
1560
- * motif but a different figure, and answers null; {@link melodicSimilarity} is
1561
- * what scores those.
1562
- *
1563
- * With a `key` in hand the search also asks whether the second statement is the
1564
- * first moved by scale degrees rather than by semitones. That is the tonal
1565
- * answer to the real one, and the two are kept apart: a real transposition keeps
1566
- * every interval literally, a tonal one lets the key resize them, so a subject
1567
- * answered a step higher reads as `'transposition'` when its major second stays
1568
- * major and as `'tonalTransposition'` when the scale turns it minor.
1693
+ * A melody is compared, named and classified by three modules here, and all
1694
+ * three start from the same two questions: which notes sound, in what order,
1695
+ * and what the gaps between their onsets are. Answered separately they would
1696
+ * drift — one reading folding a chord to its top voice and another not — so
1697
+ * they are answered once, here.
1698
+ */
1699
+
1700
+ /**
1701
+ * Anything that can be read as a melodic line: a motif, or plain note events.
1569
1702
  *
1570
- * @param a The model statement.
1571
- * @param b The statement to name against it.
1572
- * @param key Key context for the tonal reading; without it, a statement that
1573
- * only holds diatonically is not recognised.
1574
- * @returns The relation, or null when the two stand in none.
1575
- * @example
1576
- * ```ts
1577
- * import { majorKey, motifFromNotes, relateMotifs } from '@libraz/libcantus';
1578
- * const model = motifFromNotes([
1579
- * { pitch: 60, startBeat: 0, durationBeat: 1 },
1580
- * { pitch: 62, startBeat: 1, durationBeat: 1 },
1581
- * { pitch: 64, startBeat: 2, durationBeat: 1 },
1582
- * ]);
1583
- * const answer = motifFromNotes([
1584
- * { pitch: 62, startBeat: 3, durationBeat: 1 },
1585
- * { pitch: 64, startBeat: 4, durationBeat: 1 },
1586
- * { pitch: 65, startBeat: 5, durationBeat: 1 },
1587
- * ]);
1588
- * relateMotifs(model, answer, majorKey(0))?.kind; // 'tonalTransposition'
1589
- * ```
1590
1703
  * @category Arrangement & Analysis
1591
1704
  */
1592
- declare function relateMotifs(a: MotifData, b: MotifData, key?: KeyScale): MotifRelation | null;
1593
- /**
1594
- * Compare two lines and say why they scored what they did.
1595
- *
1596
- * The metric is a normalised edit distance over the interval sequence, weighted
1597
- * against the same over the rhythmic profile. Intervals rather than pitches,
1598
- * because the same tune in another key is the same tune; edit distance rather
1599
- * than a step-by-step correlation, because a variant that adds a passing note or
1600
- * drops one still has to line up with its model, and only an alignment-based
1601
- * measure can do that. Substituting one interval for another costs by how far
1602
- * apart they are rather than a flat step, so an answer that widens a third to a
1603
- * fourth stays close to its model while one that replaces it with a leap does
1604
- * not. The rhythm carries the smaller share: a melody is recognised mostly by
1605
- * its pitch shape, but a figure in a wholly different rhythm is a different
1606
- * figure.
1607
- *
1608
- * @param a The first line: a motif, or plain note events.
1609
- * @param b The second line.
1610
- * @returns The overall likeness, the two terms behind it, and a rationale.
1611
- * @example
1612
- * ```ts
1613
- * import { compareMelodies } from '@libraz/libcantus';
1614
- * const line = [
1615
- * { pitch: 60, startBeat: 0, durationBeat: 1 },
1616
- * { pitch: 62, startBeat: 1, durationBeat: 1 },
1617
- * { pitch: 64, startBeat: 2, durationBeat: 1 },
1618
- * ];
1619
- * compareMelodies(line, line).similarity; // 1
1620
- * ```
1705
+ type MelodicPhrase = MotifData | readonly NoteEvent[];
1706
+
1707
+ /**
1708
+ * The shape of a line: which way it moves, and what that adds up to.
1709
+ *
1710
+ * A contour is the melody with its pitches forgotten — the sequence of ups and
1711
+ * downs alone — which is what lets a phrase be called an arch whatever key it
1712
+ * is in and whatever intervals it is built from.
1713
+ */
1714
+
1715
+ /**
1716
+ * Step direction between two consecutive notes.
1717
+ *
1621
1718
  * @category Arrangement & Analysis
1622
1719
  */
1623
- declare function compareMelodies(a: MelodicPhrase, b: MelodicPhrase): MelodicComparison;
1720
+ type ContourDirection = 'up' | 'down' | 'same';
1624
1721
  /**
1625
- * How alike two lines are, in [0, 1].
1722
+ * The overall shape of a line.
1626
1723
  *
1627
- * The number {@link compareMelodies} reaches, for callers that only want to rank
1628
- * phrases: 1 for the same line — a transposition of it included, since the
1629
- * measure is built on intervals — and lower the further two lines are from being
1630
- * each other's variant. Symmetric in its arguments, and deterministic.
1724
+ * The first four names are the vocabulary the motif generator's contours are
1725
+ * written in, and mean the same here. `'static'` is the shape a generator never
1726
+ * asks for and an analysis regularly meets: a line that does not move. A line
1727
+ * that dips and comes back — an arch upside down — reads as `'wave'`, since the
1728
+ * shared vocabulary has no name of its own for it.
1729
+ *
1730
+ * A wave has to turn more than once to be heard as one, so a cell the generator
1731
+ * writes for `'wave'` reads back as `'wave'` only from three bars up; the one-
1732
+ * and two-bar cells turn once and read as `'arch'`. The other three shapes come
1733
+ * back under their own name at every length.
1734
+ *
1735
+ * @category Arrangement & Analysis
1736
+ */
1737
+ type MelodicContourShape = 'arch' | 'ascending' | 'descending' | 'wave' | 'static';
1738
+ /**
1739
+ * The abstract shape of a line: its step directions and what they add up to.
1631
1740
  *
1632
- * @param a The first line: a motif, or plain note events.
1633
- * @param b The second line.
1634
- * @returns The likeness in [0, 1].
1635
- * @example
1636
- * ```ts
1637
- * import { melodicSimilarity } from '@libraz/libcantus';
1638
- * const line = [
1639
- * { pitch: 60, startBeat: 0, durationBeat: 1 },
1640
- * { pitch: 62, startBeat: 1, durationBeat: 1 },
1641
- * { pitch: 64, startBeat: 2, durationBeat: 1 },
1642
- * ];
1643
- * melodicSimilarity(line, line); // 1
1644
- * ```
1645
1741
  * @category Arrangement & Analysis
1646
1742
  */
1647
- declare function melodicSimilarity(a: MelodicPhrase, b: MelodicPhrase): number;
1743
+ type MelodicContour = {
1744
+ /** One direction per adjacent pair of notes, in time order. */
1745
+ directions: ContourDirection[];
1746
+ /** What the directions add up to. */
1747
+ shape: MelodicContourShape;
1748
+ /** Index of the highest note; ties keep the earliest, and -1 for a line with no notes. */
1749
+ peakIndex: number;
1750
+ /** Index of the lowest note; ties keep the earliest, and -1 for a line with no notes. */
1751
+ troughIndex: number;
1752
+ /** Semitone distance from the lowest note to the highest. */
1753
+ range: number;
1754
+ /** Why the line was given that shape. */
1755
+ rationale: string;
1756
+ };
1648
1757
  /**
1649
1758
  * Read the shape of a line.
1650
1759
  *
@@ -1655,9 +1764,17 @@ declare function melodicSimilarity(a: MelodicPhrase, b: MelodicPhrase): number;
1655
1764
  * Anything that keeps changing direction is a wave, and a line that does not move
1656
1765
  * is static.
1657
1766
  *
1767
+ * The line is expected to be monophonic, as {@link analyzeVoice} expects a voice
1768
+ * to be; split polyphonic material into lines first. Notes struck together are
1769
+ * read as one event, the highest of them standing for it — a chord and the note
1770
+ * after it trace one step, not a climb through the chord — so a piano part read
1771
+ * whole answers for its top voice rather than for a line no one plays.
1772
+ *
1658
1773
  * @param notes The line: a motif, or plain note events. Notes that never sound
1659
- * are dropped.
1660
- * @returns The directions, the shape, the peak and trough, and a rationale.
1774
+ * are dropped, and notes sharing an onset are folded to their top voice.
1775
+ * @returns The directions, the shape, the peak and trough, and a rationale. A
1776
+ * line with no sounding notes is static with a range of 0, and its peak and
1777
+ * trough are -1: there is no note for them to point at.
1661
1778
  * @example
1662
1779
  * ```ts
1663
1780
  * import { melodicContour } from '@libraz/libcantus';
@@ -1672,6 +1789,113 @@ declare function melodicSimilarity(a: MelodicPhrase, b: MelodicPhrase): number;
1672
1789
  */
1673
1790
  declare function melodicContour(notes: MelodicPhrase): MelodicContour;
1674
1791
 
1792
+ /**
1793
+ * The transformation between two statements of a motif, named.
1794
+ *
1795
+ * Two cells stand in a relation when one is the other put through a device the
1796
+ * repertoire has a name for — repetition, transposition, inversion, retrograde,
1797
+ * augmentation — and naming it is what turns a pair of similar phrases into an
1798
+ * account of how the second was made from the first.
1799
+ */
1800
+
1801
+ /**
1802
+ * How one statement of a motif stands to another.
1803
+ *
1804
+ * `'transposition'` is the literal one — every interval preserved, which is what
1805
+ * makes a sequence built on it a *real* sequence. `'tonalTransposition'` shifts
1806
+ * by scale degrees instead and lets the key resize the intervals, which is what
1807
+ * makes a sequence built on it a *tonal* sequence; the two are different devices
1808
+ * and are never reported as one. `'augmentation'` and `'diminution'` name the
1809
+ * relation only when the pitches are untouched, so an inversion in doubled note
1810
+ * values reads as `'inversion'` with a `timeRatio` of 2 rather than losing the
1811
+ * inversion to the stretch.
1812
+ *
1813
+ * @category Arrangement & Analysis
1814
+ */
1815
+ type MotifRelationKind = 'repetition' | 'transposition' | 'tonalTransposition' | 'inversion' | 'retrograde' | 'retrogradeInversion' | 'augmentation' | 'diminution';
1816
+ /**
1817
+ * The transformation that turns one motif into another.
1818
+ *
1819
+ * @category Arrangement & Analysis
1820
+ */
1821
+ type MotifRelation = {
1822
+ /** The named transformation. */
1823
+ kind: MotifRelationKind;
1824
+ /**
1825
+ * Whether the second statement begins where the first ends, which is what
1826
+ * separates a sequence from a restatement elsewhere in the piece: a
1827
+ * `'transposition'` that is one is a real sequence, a `'tonalTransposition'`
1828
+ * that is one is a tonal sequence.
1829
+ */
1830
+ sequence: boolean;
1831
+ /** Semitones from the first statement's first note to the second's. */
1832
+ semitones: number;
1833
+ /** The same distance named as a spelled interval. */
1834
+ interval: SpelledInterval;
1835
+ /** Scale degrees the second statement stands above the first, when tonal. */
1836
+ degrees?: number;
1837
+ /**
1838
+ * How much the second statement is stretched against the first: 2 for doubled
1839
+ * note values, 0.5 for halved, 1 for unchanged.
1840
+ *
1841
+ * Measured in the direction the relation names, so a retrograde that kept
1842
+ * every note value reports 1 however uneven those values are: it is compared
1843
+ * against the model played backwards, not against the model as written.
1844
+ */
1845
+ timeRatio: number;
1846
+ /** Why the pair was given that name. */
1847
+ rationale: string;
1848
+ };
1849
+ /**
1850
+ * Name how two statements of a motif relate.
1851
+ *
1852
+ * The two are compared as shapes, not as pitches: the interval sequences say
1853
+ * which transformations could name the pair and the onset gaps say which of
1854
+ * them does, so a statement a fourth higher in doubled note values is
1855
+ * recognised for what it is, and a cell that reads as both an inversion and a
1856
+ * retrograde is named by the way its rhythm runs rather than by which name came
1857
+ * to hand first. A pair whose rhythms correspond in neither direction — the same
1858
+ * way round for a repetition or a transposition, the model played backwards for
1859
+ * a retrograde, note values and all — is not a transformation of the motif but a
1860
+ * different figure, and answers null; {@link melodicSimilarity} is what scores
1861
+ * those.
1862
+ *
1863
+ * With a `key` in hand the search also asks whether the second statement is the
1864
+ * first moved by scale degrees rather than by semitones. That is the tonal
1865
+ * answer to the real one, and the two are kept apart: a real transposition keeps
1866
+ * every interval literally, a tonal one lets the key resize them, so a subject
1867
+ * answered a step higher reads as `'transposition'` when its major second stays
1868
+ * major and as `'tonalTransposition'` when the scale turns it minor.
1869
+ *
1870
+ * @param a The model statement.
1871
+ * @param b The statement to name against it.
1872
+ * @param keyLike Key context for the tonal reading. Without one that reading is
1873
+ * unavailable, and a statement holding only diatonically is left unnamed
1874
+ * unless its interval pattern also fits a member of the retrograde family, in
1875
+ * which case it is named there: a triad restated a degree higher swaps two
1876
+ * adjacent interval sizes, which is what a retrograde inversion does to it as
1877
+ * well, and equal note values read the same way round in both directions, so
1878
+ * the notes alone cannot separate the two. A key separates them.
1879
+ * @returns The relation, or null when the two stand in none.
1880
+ * @example
1881
+ * ```ts
1882
+ * import { majorKey, motifFromNotes, relateMotifs } from '@libraz/libcantus';
1883
+ * const model = motifFromNotes([
1884
+ * { pitch: 60, startBeat: 0, durationBeat: 1 },
1885
+ * { pitch: 62, startBeat: 1, durationBeat: 1 },
1886
+ * { pitch: 64, startBeat: 2, durationBeat: 1 },
1887
+ * ]);
1888
+ * const answer = motifFromNotes([
1889
+ * { pitch: 62, startBeat: 3, durationBeat: 1 },
1890
+ * { pitch: 64, startBeat: 4, durationBeat: 1 },
1891
+ * { pitch: 65, startBeat: 5, durationBeat: 1 },
1892
+ * ]);
1893
+ * relateMotifs(model, answer, majorKey(0))?.kind; // 'tonalTransposition'
1894
+ * ```
1895
+ * @category Arrangement & Analysis
1896
+ */
1897
+ declare function relateMotifs(a: MotifData, b: MotifData, keyLike?: SpelledKeyLike): MotifRelation | null;
1898
+
1675
1899
  /**
1676
1900
  * Harmonic reduction: which chords carry the progression and which decorate it.
1677
1901
  *
@@ -1734,9 +1958,11 @@ type ReducedChord = {
1734
1958
  * What a reduction treats as the frame.
1735
1959
  *
1736
1960
  * `'function'` is the functional reading: the tonic, the dominant and the
1737
- * chords that cadence are the frame. `'duration'` is the salience reading: the
1738
- * chords that hold the harmony at least as long as the median chord are the
1739
- * frame, whatever function they carry.
1961
+ * chords that cadence are the frame. `'duration'` is the salience reading: a
1962
+ * chord is the frame when it outlasts the chords around it — its length beats
1963
+ * the median of the other chords' — whatever function it carries. Nothing
1964
+ * outlasts anything in an even harmonic rhythm, so a loop or a vamp protects no
1965
+ * chord by length and the two figures decide, as they do under either reading.
1740
1966
  *
1741
1967
  * @category Arrangement & Analysis
1742
1968
  */
@@ -1801,4 +2027,4 @@ type ReduceProgressionOptions = {
1801
2027
  */
1802
2028
  declare function reduceProgression(timeline: ChordTimeline, key: KeyContext, opts?: ReduceProgressionOptions): ReducedChord[];
1803
2029
 
1804
- export { reduceProgression as $, type ArrangementAnalysis as A, type TrackRole as B, type ChordMatch as C, type DetectChordOptions as D, type ExtractMotifsOptions as E, type FormSection as F, analyzeArrangement as G, type Hypermeter as H, compareMelodies as I, createArrangementSession as J, type KeyMatch as K, detectChord as L, MODAL_SCALE_NAMES as M, detectChordBest as N, detectKey as O, type Phrase as P, detectKeyBest as Q, type ReduceProgressionOptions as R, type StructuralCadence as S, type TensionPoint as T, detectKeyFromNotes as U, extractMotifs as V, hypermeter as W, melodicContour as X, melodicSimilarity as Y, motifFromNotes as Z, phrasesFromTimeline as _, type ArrangementOptions as a, relateMotifs as a0, sectionsFromNotes as a1, structuralCadences as a2, tensionCurve as a3, tensionCurveFrom as a4, type ArrangementSession as b, type ArrangementTrack as c, type Conflict as d, type ContourDirection as e, type DetectKeyOptions as f, type FormSectionOptions as g, type HypermeterOptions as h, type KeyVariant as i, type MelodicComparison as j, type MelodicContour as k, type MelodicContourShape as l, type MelodicPhrase as m, type ModalCandidate as n, type ModalScaleName as o, type MotifData as p, type MotifOccurrence as q, type MotifRelation as r, type MotifRelationKind as s, type PhraseOptions as t, type PhraseSignal as u, type ReducedChord as v, type ReductionBasis as w, type ReductionLevel as x, type TrackAnalysis as y, type TrackEdit as z };
2030
+ export { motifFromNotes as $, type ArrangementAnalysis as A, type TrackRole as B, type ChordMatch as C, type DetectChordOptions as D, type ExtractMotifsOptions as E, type FormSection as F, analyzeArrangement as G, type Hypermeter as H, analyzePolyphony as I, augmentedSixthChord as J, type KeyMatch as K, augmentedSixthFromPitchClasses as L, MODAL_SCALE_NAMES as M, augmentedSixthKind as N, createArrangementSession as O, type Phrase as P, detectChord as Q, type ReduceProgressionOptions as R, type StructuralCadence as S, type TensionPoint as T, detectChordBest as U, detectKey as V, detectKeyBest as W, detectKeyFromNotes as X, extractMotifs as Y, hypermeter as Z, melodicContour as _, type ArrangementOptions as a, phrasesFromTimeline as a0, reduceProgression as a1, relateMotifs as a2, sectionsFromNotes as a3, spellAugmentedSixth as a4, structuralCadences as a5, tensionCurve as a6, tensionCurveFrom as a7, type ArrangementSession as b, type ArrangementTrack as c, type AugmentedSixthKind as d, type Conflict as e, type ConflictPartner as f, type ContourDirection as g, type DetectKeyOptions as h, type FormSectionOptions as i, type HypermeterOptions as j, type MelodicContour as k, type MelodicContourShape as l, type MelodicPhrase as m, type ModalCandidate as n, type ModalScaleName as o, type MotifData as p, type MotifOccurrence as q, type MotifRelation as r, type MotifRelationKind as s, type PhraseOptions as t, type PhraseSignal as u, type ReducedChord as v, type ReductionBasis as w, type ReductionLevel as x, type TrackAnalysis as y, type TrackEdit as z };