@libraz/libcantus 1.0.1 → 1.1.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-TTCLJX6D.js → chunk-57WB2EDF.js} +10 -8
  9. package/dist/chunk-57WB2EDF.js.map +1 -0
  10. package/dist/chunk-5JJ6EHEZ.cjs +457 -0
  11. package/dist/chunk-5JJ6EHEZ.cjs.map +1 -0
  12. package/dist/{chunk-AFM2ULJH.cjs → chunk-5KW7DNCJ.cjs} +1567 -1067
  13. package/dist/chunk-5KW7DNCJ.cjs.map +1 -0
  14. package/dist/chunk-7GOKQJTQ.js +145 -0
  15. package/dist/chunk-7GOKQJTQ.js.map +1 -0
  16. package/dist/{chunk-H3VORETI.js → chunk-AWEMD747.js} +487 -592
  17. package/dist/chunk-AWEMD747.js.map +1 -0
  18. package/dist/{chunk-PKFNUHDY.js → chunk-DKICR7AN.js} +1216 -611
  19. package/dist/chunk-DKICR7AN.js.map +1 -0
  20. package/dist/{chunk-INHVXP6Z.js → chunk-FPBZFNCD.js} +1159 -415
  21. package/dist/chunk-FPBZFNCD.js.map +1 -0
  22. package/dist/{chunk-6XH6QZYQ.cjs → chunk-GDKE6RQW.cjs} +1453 -848
  23. package/dist/chunk-GDKE6RQW.cjs.map +1 -0
  24. package/dist/chunk-H66ARFU5.js +363 -0
  25. package/dist/chunk-H66ARFU5.js.map +1 -0
  26. package/dist/{chunk-YH3BIKHH.js → chunk-HOHFYGKW.js} +146 -49
  27. package/dist/chunk-HOHFYGKW.js.map +1 -0
  28. package/dist/chunk-HSY2VQXZ.js +2272 -0
  29. package/dist/chunk-HSY2VQXZ.js.map +1 -0
  30. package/dist/{chunk-E3F4AHAH.cjs → chunk-J772DL3B.cjs} +221 -134
  31. package/dist/chunk-J772DL3B.cjs.map +1 -0
  32. package/dist/chunk-K6OZXIR6.js +1 -0
  33. package/dist/{chunk-VTI6FOLB.js → chunk-PQL7E5F6.js} +709 -133
  34. package/dist/chunk-PQL7E5F6.js.map +1 -0
  35. package/dist/chunk-Q6R3QAZD.cjs +3329 -0
  36. package/dist/chunk-Q6R3QAZD.cjs.map +1 -0
  37. package/dist/{chunk-MXO72PLY.cjs → chunk-QCXYKSZS.cjs} +21 -19
  38. package/dist/chunk-QCXYKSZS.cjs.map +1 -0
  39. package/dist/{chunk-VOHZB45S.js → chunk-R5VUXQMP.js} +1321 -821
  40. package/dist/chunk-R5VUXQMP.js.map +1 -0
  41. package/dist/chunk-R7B5N2JK.cjs +363 -0
  42. package/dist/chunk-R7B5N2JK.cjs.map +1 -0
  43. package/dist/{chunk-WWURE4XT.cjs → chunk-RVS4G4MR.cjs} +624 -729
  44. package/dist/chunk-RVS4G4MR.cjs.map +1 -0
  45. package/dist/chunk-TT6PKMPR.cjs +145 -0
  46. package/dist/chunk-TT6PKMPR.cjs.map +1 -0
  47. package/dist/chunk-UGLFDG7L.cjs +1 -0
  48. package/dist/chunk-UGLFDG7L.cjs.map +1 -0
  49. package/dist/chunk-VLE5R7PF.js +3329 -0
  50. package/dist/chunk-VLE5R7PF.js.map +1 -0
  51. package/dist/{chunk-FVSI36UV.js → chunk-X6RJAHII.js} +197 -110
  52. package/dist/chunk-X6RJAHII.js.map +1 -0
  53. package/dist/{chunk-Q2WGKJNN.cjs → chunk-XKSO2V5I.cjs} +699 -123
  54. package/dist/chunk-XKSO2V5I.cjs.map +1 -0
  55. package/dist/chunk-XVMJO3QF.cjs +2272 -0
  56. package/dist/chunk-XVMJO3QF.cjs.map +1 -0
  57. package/dist/{chunk-UADYAYAT.cjs → chunk-YPB3A3EY.cjs} +1309 -565
  58. package/dist/chunk-YPB3A3EY.cjs.map +1 -0
  59. package/dist/{index-C_8o--VE.d.cts → coerce-CJlgULrQ.d.cts} +210 -199
  60. package/dist/{index-szBv3496.d.ts → coerce-wwdELNYS.d.ts} +210 -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-Dzr13q3d.d.cts → index-B3WCj_Hm.d.cts} +135 -58
  72. package/dist/{index-BO6xE9YU.d.cts → index-BUtIf2go.d.ts} +460 -254
  73. package/dist/{index-9wcMhG73.d.ts → index-BaoKVVhE.d.cts} +293 -56
  74. package/dist/{index-Chbst3Jx.d.ts → index-BpWOhkkE.d.ts} +135 -58
  75. package/dist/{index-CgHqxdps.d.ts → index-BzLNkmA7.d.cts} +196 -114
  76. package/dist/{index-CunYKLiu.d.ts → index-CBCP84rv.d.cts} +1 -1
  77. package/dist/{index-BkGmlyI3.d.cts → index-Ci3xyows.d.ts} +60 -23
  78. package/dist/{index-CCNJRG-P.d.cts → index-CnHQgqNh.d.ts} +293 -56
  79. package/dist/{index-B4o55NDr.d.cts → index-CnIdP4EZ.d.ts} +1 -1
  80. package/dist/{index-B9MNZboQ.d.ts → index-Cy15oTvh.d.cts} +52 -22
  81. package/dist/{index-B9MNZboQ.d.cts → index-Cy15oTvh.d.ts} +52 -22
  82. package/dist/{index-D57QRbVn.d.ts → index-DTsKaPaC.d.cts} +460 -254
  83. package/dist/{index-Cj49F892.d.ts → index-D_hzxD8w.d.cts} +60 -23
  84. package/dist/{index-BGFW-8P2.d.cts → index-DpC-mDlV.d.ts} +196 -114
  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.ts → masks-C8TWLFIi.d.cts} +49 -2
  92. package/dist/{masks-DYbE485J.d.cts → masks-O73Rhj5d.d.ts} +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 +502 -181
  96. package/dist/model/index.d.ts +502 -181
  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 +6 -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.js';
2
+ import { d as ChordQuality, k as KeyVariant, b as Chord, q as SpelledKeyLike, c as ChordLike, K as KeyLike, n as ResolvedKey, N as NoteSafety, S as SafetyProfile } from './coerce-wwdELNYS.js';
3
+ import { r as ScaleName } from './masks-O73Rhj5d.js';
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-BpWOhkkE.js';
5
+ import { b as MeterLike } from './index-Ci3xyows.js';
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
@@ -485,8 +647,11 @@ type ArrangementAnalysis = {
485
647
  * started in for every bar after it leaves.
486
648
  */
487
649
  keys: KeyRegion[];
488
- /** The key held longest across {@link ArrangementAnalysis.keys}. */
489
- prevailingKey: KeyScale;
650
+ /**
651
+ * The key held longest across {@link ArrangementAnalysis.keys}, whole: the
652
+ * pitch classes with the spelling and the scale form the regions carry.
653
+ */
654
+ prevailingKey: ResolvedKey;
490
655
  timeline: ChordTimeline;
491
656
  segmentConfidence: number[];
492
657
  cadences: CadenceHit[];
@@ -504,8 +669,11 @@ type ArrangementOptions = {
504
669
  * A single key held across the whole arrangement. Omit it, and `keys` with
505
670
  * it, to have the key searched for over time so a modulating piece is read
506
671
  * against the key actually in force.
672
+ *
673
+ * Taken in whatever form the caller holds a key, and kept whole: the region
674
+ * built from it carries the spelling and the scale form it was named with.
507
675
  */
508
- key?: KeyScale;
676
+ key?: KeyLike;
509
677
  /**
510
678
  * Key regions to judge against, when a previous pass already worked them out.
511
679
  * Takes precedence over `key`; supplying both is answering the same question
@@ -536,14 +704,14 @@ type ArrangementOptions = {
536
704
  *
537
705
  * @defaultValue `4/4`
538
706
  */
539
- ts?: TimeSignature;
707
+ ts?: MeterLike;
540
708
  /**
541
709
  * The meter as it changes over the arrangement. Bar lines, downbeats and
542
710
  * metric weight all follow the signature in force at the beat in question.
543
711
  *
544
712
  * @defaultValue 4/4 throughout
545
713
  */
546
- meters?: MeterMap;
714
+ meters?: MeterLike;
547
715
  /**
548
716
  * Length of the pickup in beats, when the piece starts with one.
549
717
  *
@@ -555,7 +723,13 @@ type ArrangementOptions = {
555
723
  */
556
724
  pickupBeats?: number;
557
725
  /**
558
- * Chord-slot length in beats; defaults to the length of the opening bar.
726
+ * Expected chord length in beats; defaults to the length of the opening bar.
727
+ *
728
+ * A prior rather than a grid: the arrangement analysis infers its harmony
729
+ * with dynamic segmentation, where the longer a chord is expected to last the
730
+ * more evidence a change needs before the search will place one. A chord may
731
+ * still begin at any position the notes argue for. Supply `timeline` to
732
+ * analyse against boundaries you have fixed yourself.
559
733
  *
560
734
  * @defaultValue the length of the opening bar
561
735
  */
@@ -578,6 +752,26 @@ type ArrangementOptions = {
578
752
  */
579
753
  budget?: number;
580
754
  };
755
+ /**
756
+ * Label every note of one polyphonic passage against the harmony under it and
757
+ * the voices around it.
758
+ *
759
+ * {@link analyzeVoice} reads a single line: it takes the note before and the note
760
+ * after each note as that note's melodic neighbours, and it is given no other
761
+ * voices, so over a flat array of everything that sounds it invents figures
762
+ * across parts — the alto between two soprano notes becomes a passing tone —
763
+ * and can never find a suspension, which is by definition a dissonance against
764
+ * something else sounding. So the passage is partitioned into monophonic
765
+ * sub-voices first and each is read with the others handed to it, which is what
766
+ * {@link analyzeArrangement} does with the tracks it is given.
767
+ *
768
+ * @param notes The passage, in any order; the ids are indices into this array.
769
+ * @param chordAtBeat Chord sounding at a given beat, or null.
770
+ * @param key The key, or the key in force at a given beat.
771
+ * @returns One annotation per note, in the order the notes were given, each
772
+ * carrying its position in that array as its id.
773
+ */
774
+ declare function analyzePolyphony(notes: readonly NoteEvent[], chordAtBeat: (beat: number) => Chord | null, key: KeyContext): AnalyzedNote[];
581
775
  /**
582
776
  * Analyse a whole arrangement against the harmony inferred from it.
583
777
  *
@@ -622,7 +816,7 @@ type ArrangementOptions = {
622
816
  * { pitch: 67, startBeat: 2, durationBeat: 2 },
623
817
  * ];
624
818
  * const { prevailingKey, conflicts } = analyzeArrangement([{ role: 'melody', notes: melody }]);
625
- * prevailingKey.rootPc; // the key held longest across the analysis
819
+ * prevailingKey.scale.rootPc; // the key held longest across the analysis
626
820
  * conflicts; // notes clashing with the inferred harmony, worst severity first
627
821
  * ```
628
822
  * @category Arrangement & Analysis
@@ -786,6 +980,11 @@ declare function tensionCurve(tracks: ArrangementTrack[], opts?: ArrangementOpti
786
980
  * twice over the same notes. This takes the analysis it already produced, so
787
981
  * the curve and the annotations describe the same harmony by construction.
788
982
  *
983
+ * The analysis is the harmony to fall back on rather than one laid over the
984
+ * caller: `key`, `keys` and `harmonyTracks` each name a harmony the analysis
985
+ * does not hold, so naming one is asking for a different reading, and the curve
986
+ * is taken under it. Nothing else about the analysis is re-derived.
987
+ *
789
988
  * @param tracks The same tracks that were analysed.
790
989
  * @param analysis The result of {@link analyzeArrangement} for those tracks.
791
990
  * @param opts Analysis options plus an optional `step` (default one beat).
@@ -875,9 +1074,11 @@ type HypermeterOptions = {
875
1074
  * Groupings of 2, 3, 4, 6, and 8 bars are tried at every phase, and each
876
1075
  * reading is scored on two counts: how much more harmonic change lands on the
877
1076
  * 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.
1077
+ * hyperbar ends it predicts are closed by one of the cadences it is given.
1078
+ * Where cadences are given they settle the phase, since a hyperbar is what a
1079
+ * cadence ends; the harmony then chooses among the phases they agree with. The
1080
+ * commonest groupings are believed a little more readily than the rare ones,
1081
+ * which settles ties without letting a prior overrule the music.
881
1082
  *
882
1083
  * The meter is read at the beat in question rather than at the start, so a
883
1084
  * piece that changes metre keeps being cut at its own bar lines. Bar 0 is the
@@ -989,15 +1190,17 @@ type PhraseOptions = {
989
1190
  *
990
1191
  * @defaultValue `4/4`
991
1192
  */
992
- ts?: TimeSignature;
1193
+ ts?: MeterLike;
993
1194
  /**
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.
1195
+ * The meter as it changes over the span. Bar lines and hypermetric downbeats
1196
+ * follow the signature in force at the beat in question, so a piece that
1197
+ * changes metre is cut at its own bar lines; the length defaults below are
1198
+ * lengths rather than positions, and each is taken once from the meter the
1199
+ * span opens in.
997
1200
  *
998
1201
  * @defaultValue 4/4 throughout
999
1202
  */
1000
- meters?: MeterMap;
1203
+ meters?: MeterLike;
1001
1204
  /**
1002
1205
  * Key context for cadence detection: one key for the whole span, or the key
1003
1206
  * in force at a given beat. Omit it to have the key searched for over time,
@@ -1084,7 +1287,10 @@ type StructuralCadence = {
1084
1287
  *
1085
1288
  * Three kinds of evidence are gathered independently and then reconciled. The
1086
1289
  * 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
1290
+ * arrives on, and at the bar line where that chord is held across one — a tonic
1291
+ * still sounding into the next phrase closed this one at the bar line. The
1292
+ * cadence reported for a phrase is always one that arrived inside it. The
1293
+ * melody contributes the rests it breathes at and the notes it
1088
1294
  * holds long enough to read as arrivals. Repetition contributes the beats where
1089
1295
  * the music restarts material it has just played. The metre contributes its
1090
1296
  * hypermetric downbeats, which confirm a boundary but never declare one on
@@ -1107,18 +1313,25 @@ type StructuralCadence = {
1107
1313
  * that never sound are dropped.
1108
1314
  * @param opts Analysis options; see {@link PhraseOptions}.
1109
1315
  * @returns The phrases, in time order; one phrase covering the whole span when
1110
- * the span is too short to divide.
1316
+ * the span is too short to divide, and none at all when nothing sounds —
1317
+ * silence is no material to hear a phrase in.
1111
1318
  * @example
1112
1319
  * ```ts
1113
1320
  * import { chordTimelineFromNotes, phrasesFromTimeline } from '@libraz/libcantus';
1321
+ * // Four bars of C - F - G - C. Triads rather than single notes: one line at a
1322
+ * // time names no harmony, so nothing would cadence.
1323
+ * const triad = (pitches: number[], startBeat: number) =>
1324
+ * pitches.map((pitch) => ({ pitch, startBeat, durationBeat: 4 }));
1114
1325
  * const notes = [
1115
- * { pitch: 60, startBeat: 0, durationBeat: 4 },
1116
- * { pitch: 67, startBeat: 4, durationBeat: 4 },
1117
- * { pitch: 60, startBeat: 8, durationBeat: 8 },
1326
+ * ...triad([60, 64, 67], 0),
1327
+ * ...triad([65, 69, 72], 4),
1328
+ * ...triad([67, 71, 74], 8),
1329
+ * ...triad([60, 64, 67], 12),
1118
1330
  * ];
1119
1331
  * const { timeline } = chordTimelineFromNotes(notes);
1120
1332
  * const phrases = phrasesFromTimeline(timeline, notes);
1121
- * phrases[phrases.length - 1]?.cadence?.cadence.type; // 'authentic', when one closes it
1333
+ * phrases[phrases.length - 1]?.cadence?.cadence.type; // 'authentic'
1334
+ * phrases[phrases.length - 1]?.cadence?.atBeat; // 12, inside the phrase it closes
1122
1335
  * ```
1123
1336
  * @category Arrangement & Analysis
1124
1337
  */
@@ -1136,14 +1349,18 @@ declare function phrasesFromTimeline(timeline: ChordTimeline, notes: readonly No
1136
1349
  * @example
1137
1350
  * ```ts
1138
1351
  * import { chordTimelineFromNotes, phrasesFromTimeline, structuralCadences } from '@libraz/libcantus';
1352
+ * const triad = (pitches: number[], startBeat: number) =>
1353
+ * pitches.map((pitch) => ({ pitch, startBeat, durationBeat: 4 }));
1139
1354
  * const notes = [
1140
- * { pitch: 60, startBeat: 0, durationBeat: 4 },
1141
- * { pitch: 67, startBeat: 4, durationBeat: 4 },
1142
- * { pitch: 60, startBeat: 8, durationBeat: 8 },
1355
+ * ...triad([60, 64, 67], 0),
1356
+ * ...triad([65, 69, 72], 4),
1357
+ * ...triad([67, 71, 74], 8),
1358
+ * ...triad([60, 64, 67], 12),
1143
1359
  * ];
1144
1360
  * const { timeline } = chordTimelineFromNotes(notes);
1145
1361
  * const ranked = structuralCadences(phrasesFromTimeline(timeline, notes));
1146
- * ranked[0]; // the cadence carrying the most structural weight, if any
1362
+ * ranked[0]?.cadence.type; // 'authentic' — the cadence carrying the most weight
1363
+ * ranked[0]?.atBeat; // 12
1147
1364
  * ```
1148
1365
  * @category Arrangement & Analysis
1149
1366
  */
@@ -1206,14 +1423,14 @@ type FormSectionOptions = {
1206
1423
  *
1207
1424
  * @defaultValue `4/4`
1208
1425
  */
1209
- ts?: TimeSignature;
1426
+ ts?: MeterLike;
1210
1427
  /**
1211
1428
  * The meter as it changes over the span, so a piece that changes metre is cut
1212
1429
  * at its own bar lines.
1213
1430
  *
1214
1431
  * @defaultValue 4/4 throughout
1215
1432
  */
1216
- meters?: MeterMap;
1433
+ meters?: MeterLike;
1217
1434
  /**
1218
1435
  * Length of the unit spans are compared in, in bars; defaults to one hyperbar.
1219
1436
  *
@@ -1278,58 +1495,15 @@ type FormSectionOptions = {
1278
1495
  declare function sectionsFromNotes(notes: readonly NoteEvent[], opts?: FormSectionOptions): FormSection[];
1279
1496
 
1280
1497
  /**
1281
- * Melody as a shape, rather than as a run of notes.
1498
+ * The recurring cells a melody is built from.
1282
1499
  *
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.
1500
+ * A motif is the interval contour plus the rhythmic profile, so every statement
1501
+ * of it carries the same identity however far it has been transposed or however
1502
+ * wide its note values are written. What this module does is find them: every
1503
+ * window of the melody is reduced to that identity, windows that agree are
1504
+ * grouped, and a group stated often enough is a motif.
1289
1505
  */
1290
1506
 
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
1507
  /**
1334
1508
  * One statement of a motif, addressed in the melody it was found in.
1335
1509
  *
@@ -1418,71 +1592,6 @@ type ExtractMotifsOptions = {
1418
1592
  */
1419
1593
  budget?: number;
1420
1594
  };
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
1595
  /**
1487
1596
  * Find the cells a melody keeps coming back to.
1488
1597
  *
@@ -1496,9 +1605,11 @@ type MelodicComparison = {
1496
1605
  * rather than every fragment of it.
1497
1606
  *
1498
1607
  * 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.
1608
+ * voice to be; split polyphonic material into lines first. Notes struck together
1609
+ * are read as one event, the highest of them standing for it, so a chord is
1610
+ * reduced to its top voice rather than read as a line of its own. Onsets are
1611
+ * expected to be quantised, since two statements must agree on their rhythm to
1612
+ * be recognised as one motif.
1502
1613
  *
1503
1614
  * @param notes The melody, in time order. Notes that never sound are dropped.
1504
1615
  * @param opts Cell-length bounds and the recurrence threshold; see
@@ -1529,7 +1640,13 @@ declare function extractMotifs(notes: readonly NoteEvent[], opts?: ExtractMotifs
1529
1640
  * caller names one it already has — a subject, an answer, a phrase lifted out of
1530
1641
  * a score — so that it can be handed to {@link relateMotifs}.
1531
1642
  *
1532
- * @param notes The cell, in time order. Notes that never sound are dropped.
1643
+ * The cell is expected to be monophonic, as {@link analyzeVoice} expects a voice
1644
+ * to be. Notes struck together are read as one event, the highest of them
1645
+ * standing for it, so a chord is reduced to its top voice rather than read as a
1646
+ * line of its own.
1647
+ *
1648
+ * @param notes The cell, in time order. Notes that never sound are dropped, and
1649
+ * notes sharing an onset are folded to their top voice.
1533
1650
  * @returns The motif, carrying exactly one occurrence.
1534
1651
  * @example
1535
1652
  * ```ts
@@ -1549,102 +1666,74 @@ declare function extractMotifs(notes: readonly NoteEvent[], opts?: ExtractMotifs
1549
1666
  * @category Arrangement & Analysis
1550
1667
  */
1551
1668
  declare function motifFromNotes(notes: readonly NoteEvent[]): MotifData;
1669
+
1552
1670
  /**
1553
- * Name how two statements of a motif relate.
1554
- *
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.
1671
+ * What every reading of a melody has to agree on first.
1562
1672
  *
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.
1673
+ * A melody is compared, named and classified by three modules here, and all
1674
+ * three start from the same two questions: which notes sound, in what order,
1675
+ * and what the gaps between their onsets are. Answered separately they would
1676
+ * drift — one reading folding a chord to its top voice and another not — so
1677
+ * they are answered once, here.
1678
+ */
1679
+
1680
+ /**
1681
+ * Anything that can be read as a melodic line: a motif, or plain note events.
1569
1682
  *
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
1683
  * @category Arrangement & Analysis
1591
1684
  */
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
- * ```
1685
+ type MelodicPhrase = MotifData | readonly NoteEvent[];
1686
+
1687
+ /**
1688
+ * The shape of a line: which way it moves, and what that adds up to.
1689
+ *
1690
+ * A contour is the melody with its pitches forgotten — the sequence of ups and
1691
+ * downs alone — which is what lets a phrase be called an arch whatever key it
1692
+ * is in and whatever intervals it is built from.
1693
+ */
1694
+
1695
+ /**
1696
+ * Step direction between two consecutive notes.
1697
+ *
1621
1698
  * @category Arrangement & Analysis
1622
1699
  */
1623
- declare function compareMelodies(a: MelodicPhrase, b: MelodicPhrase): MelodicComparison;
1700
+ type ContourDirection = 'up' | 'down' | 'same';
1624
1701
  /**
1625
- * How alike two lines are, in [0, 1].
1702
+ * The overall shape of a line.
1626
1703
  *
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.
1704
+ * The first four names are the vocabulary the motif generator's contours are
1705
+ * written in, and mean the same here. `'static'` is the shape a generator never
1706
+ * asks for and an analysis regularly meets: a line that does not move. A line
1707
+ * that dips and comes back — an arch upside down — reads as `'wave'`, since the
1708
+ * shared vocabulary has no name of its own for it.
1709
+ *
1710
+ * A wave has to turn more than once to be heard as one, so a cell the generator
1711
+ * writes for `'wave'` reads back as `'wave'` only from three bars up; the one-
1712
+ * and two-bar cells turn once and read as `'arch'`. The other three shapes come
1713
+ * back under their own name at every length.
1714
+ *
1715
+ * @category Arrangement & Analysis
1716
+ */
1717
+ type MelodicContourShape = 'arch' | 'ascending' | 'descending' | 'wave' | 'static';
1718
+ /**
1719
+ * The abstract shape of a line: its step directions and what they add up to.
1631
1720
  *
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
1721
  * @category Arrangement & Analysis
1646
1722
  */
1647
- declare function melodicSimilarity(a: MelodicPhrase, b: MelodicPhrase): number;
1723
+ type MelodicContour = {
1724
+ /** One direction per adjacent pair of notes, in time order. */
1725
+ directions: ContourDirection[];
1726
+ /** What the directions add up to. */
1727
+ shape: MelodicContourShape;
1728
+ /** Index of the highest note; ties keep the earliest, and -1 for a line with no notes. */
1729
+ peakIndex: number;
1730
+ /** Index of the lowest note; ties keep the earliest, and -1 for a line with no notes. */
1731
+ troughIndex: number;
1732
+ /** Semitone distance from the lowest note to the highest. */
1733
+ range: number;
1734
+ /** Why the line was given that shape. */
1735
+ rationale: string;
1736
+ };
1648
1737
  /**
1649
1738
  * Read the shape of a line.
1650
1739
  *
@@ -1655,9 +1744,17 @@ declare function melodicSimilarity(a: MelodicPhrase, b: MelodicPhrase): number;
1655
1744
  * Anything that keeps changing direction is a wave, and a line that does not move
1656
1745
  * is static.
1657
1746
  *
1747
+ * The line is expected to be monophonic, as {@link analyzeVoice} expects a voice
1748
+ * to be; split polyphonic material into lines first. Notes struck together are
1749
+ * read as one event, the highest of them standing for it — a chord and the note
1750
+ * after it trace one step, not a climb through the chord — so a piano part read
1751
+ * whole answers for its top voice rather than for a line no one plays.
1752
+ *
1658
1753
  * @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.
1754
+ * are dropped, and notes sharing an onset are folded to their top voice.
1755
+ * @returns The directions, the shape, the peak and trough, and a rationale. A
1756
+ * line with no sounding notes is static with a range of 0, and its peak and
1757
+ * trough are -1: there is no note for them to point at.
1661
1758
  * @example
1662
1759
  * ```ts
1663
1760
  * import { melodicContour } from '@libraz/libcantus';
@@ -1672,6 +1769,113 @@ declare function melodicSimilarity(a: MelodicPhrase, b: MelodicPhrase): number;
1672
1769
  */
1673
1770
  declare function melodicContour(notes: MelodicPhrase): MelodicContour;
1674
1771
 
1772
+ /**
1773
+ * The transformation between two statements of a motif, named.
1774
+ *
1775
+ * Two cells stand in a relation when one is the other put through a device the
1776
+ * repertoire has a name for — repetition, transposition, inversion, retrograde,
1777
+ * augmentation — and naming it is what turns a pair of similar phrases into an
1778
+ * account of how the second was made from the first.
1779
+ */
1780
+
1781
+ /**
1782
+ * How one statement of a motif stands to another.
1783
+ *
1784
+ * `'transposition'` is the literal one — every interval preserved, which is what
1785
+ * makes a sequence built on it a *real* sequence. `'tonalTransposition'` shifts
1786
+ * by scale degrees instead and lets the key resize the intervals, which is what
1787
+ * makes a sequence built on it a *tonal* sequence; the two are different devices
1788
+ * and are never reported as one. `'augmentation'` and `'diminution'` name the
1789
+ * relation only when the pitches are untouched, so an inversion in doubled note
1790
+ * values reads as `'inversion'` with a `timeRatio` of 2 rather than losing the
1791
+ * inversion to the stretch.
1792
+ *
1793
+ * @category Arrangement & Analysis
1794
+ */
1795
+ type MotifRelationKind = 'repetition' | 'transposition' | 'tonalTransposition' | 'inversion' | 'retrograde' | 'retrogradeInversion' | 'augmentation' | 'diminution';
1796
+ /**
1797
+ * The transformation that turns one motif into another.
1798
+ *
1799
+ * @category Arrangement & Analysis
1800
+ */
1801
+ type MotifRelation = {
1802
+ /** The named transformation. */
1803
+ kind: MotifRelationKind;
1804
+ /**
1805
+ * Whether the second statement begins where the first ends, which is what
1806
+ * separates a sequence from a restatement elsewhere in the piece: a
1807
+ * `'transposition'` that is one is a real sequence, a `'tonalTransposition'`
1808
+ * that is one is a tonal sequence.
1809
+ */
1810
+ sequence: boolean;
1811
+ /** Semitones from the first statement's first note to the second's. */
1812
+ semitones: number;
1813
+ /** The same distance named as a spelled interval. */
1814
+ interval: SpelledInterval;
1815
+ /** Scale degrees the second statement stands above the first, when tonal. */
1816
+ degrees?: number;
1817
+ /**
1818
+ * How much the second statement is stretched against the first: 2 for doubled
1819
+ * note values, 0.5 for halved, 1 for unchanged.
1820
+ *
1821
+ * Measured in the direction the relation names, so a retrograde that kept
1822
+ * every note value reports 1 however uneven those values are: it is compared
1823
+ * against the model played backwards, not against the model as written.
1824
+ */
1825
+ timeRatio: number;
1826
+ /** Why the pair was given that name. */
1827
+ rationale: string;
1828
+ };
1829
+ /**
1830
+ * Name how two statements of a motif relate.
1831
+ *
1832
+ * The two are compared as shapes, not as pitches: the interval sequences say
1833
+ * which transformations could name the pair and the onset gaps say which of
1834
+ * them does, so a statement a fourth higher in doubled note values is
1835
+ * recognised for what it is, and a cell that reads as both an inversion and a
1836
+ * retrograde is named by the way its rhythm runs rather than by which name came
1837
+ * to hand first. A pair whose rhythms correspond in neither direction — the same
1838
+ * way round for a repetition or a transposition, the model played backwards for
1839
+ * a retrograde, note values and all — is not a transformation of the motif but a
1840
+ * different figure, and answers null; {@link melodicSimilarity} is what scores
1841
+ * those.
1842
+ *
1843
+ * With a `key` in hand the search also asks whether the second statement is the
1844
+ * first moved by scale degrees rather than by semitones. That is the tonal
1845
+ * answer to the real one, and the two are kept apart: a real transposition keeps
1846
+ * every interval literally, a tonal one lets the key resize them, so a subject
1847
+ * answered a step higher reads as `'transposition'` when its major second stays
1848
+ * major and as `'tonalTransposition'` when the scale turns it minor.
1849
+ *
1850
+ * @param a The model statement.
1851
+ * @param b The statement to name against it.
1852
+ * @param keyLike Key context for the tonal reading. Without one that reading is
1853
+ * unavailable, and a statement holding only diatonically is left unnamed
1854
+ * unless its interval pattern also fits a member of the retrograde family, in
1855
+ * which case it is named there: a triad restated a degree higher swaps two
1856
+ * adjacent interval sizes, which is what a retrograde inversion does to it as
1857
+ * well, and equal note values read the same way round in both directions, so
1858
+ * the notes alone cannot separate the two. A key separates them.
1859
+ * @returns The relation, or null when the two stand in none.
1860
+ * @example
1861
+ * ```ts
1862
+ * import { majorKey, motifFromNotes, relateMotifs } from '@libraz/libcantus';
1863
+ * const model = motifFromNotes([
1864
+ * { pitch: 60, startBeat: 0, durationBeat: 1 },
1865
+ * { pitch: 62, startBeat: 1, durationBeat: 1 },
1866
+ * { pitch: 64, startBeat: 2, durationBeat: 1 },
1867
+ * ]);
1868
+ * const answer = motifFromNotes([
1869
+ * { pitch: 62, startBeat: 3, durationBeat: 1 },
1870
+ * { pitch: 64, startBeat: 4, durationBeat: 1 },
1871
+ * { pitch: 65, startBeat: 5, durationBeat: 1 },
1872
+ * ]);
1873
+ * relateMotifs(model, answer, majorKey(0))?.kind; // 'tonalTransposition'
1874
+ * ```
1875
+ * @category Arrangement & Analysis
1876
+ */
1877
+ declare function relateMotifs(a: MotifData, b: MotifData, keyLike?: SpelledKeyLike): MotifRelation | null;
1878
+
1675
1879
  /**
1676
1880
  * Harmonic reduction: which chords carry the progression and which decorate it.
1677
1881
  *
@@ -1734,9 +1938,11 @@ type ReducedChord = {
1734
1938
  * What a reduction treats as the frame.
1735
1939
  *
1736
1940
  * `'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.
1941
+ * chords that cadence are the frame. `'duration'` is the salience reading: a
1942
+ * chord is the frame when it outlasts the chords around it — its length beats
1943
+ * the median of the other chords' — whatever function it carries. Nothing
1944
+ * outlasts anything in an even harmonic rhythm, so a loop or a vamp protects no
1945
+ * chord by length and the two figures decide, as they do under either reading.
1740
1946
  *
1741
1947
  * @category Arrangement & Analysis
1742
1948
  */
@@ -1801,4 +2007,4 @@ type ReduceProgressionOptions = {
1801
2007
  */
1802
2008
  declare function reduceProgression(timeline: ChordTimeline, key: KeyContext, opts?: ReduceProgressionOptions): ReducedChord[];
1803
2009
 
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 };
2010
+ export { phrasesFromTimeline as $, type ArrangementAnalysis as A, analyzeArrangement as B, type ChordMatch as C, type DetectChordOptions as D, type ExtractMotifsOptions as E, type FormSection as F, analyzePolyphony as G, type Hypermeter as H, augmentedSixthChord as I, augmentedSixthFromPitchClasses as J, type KeyMatch as K, augmentedSixthKind as L, MODAL_SCALE_NAMES as M, createArrangementSession as N, detectChord as O, type Phrase as P, detectChordBest as Q, type ReduceProgressionOptions as R, type StructuralCadence as S, type TensionPoint as T, detectKey as U, detectKeyBest as V, detectKeyFromNotes as W, extractMotifs as X, hypermeter as Y, melodicContour as Z, motifFromNotes as _, type ArrangementOptions as a, reduceProgression as a0, relateMotifs as a1, sectionsFromNotes as a2, spellAugmentedSixth as a3, structuralCadences as a4, tensionCurve as a5, tensionCurveFrom as a6, type ArrangementSession as b, type ArrangementTrack as c, type AugmentedSixthKind as d, type Conflict as e, type ContourDirection as f, type DetectKeyOptions as g, type FormSectionOptions as h, type HypermeterOptions as i, type MelodicContour as j, type MelodicContourShape as k, type MelodicPhrase as l, type ModalCandidate as m, type ModalScaleName as n, type MotifData as o, type MotifOccurrence as p, type MotifRelation as q, type MotifRelationKind as r, type PhraseOptions as s, type PhraseSignal as t, type ReducedChord as u, type ReductionBasis as v, type ReductionLevel as w, type TrackAnalysis as x, type TrackEdit as y, type TrackRole as z };