@libraz/libcantus 0.9.4 → 1.0.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 (186) hide show
  1. package/README.md +61 -261
  2. package/README_ja.md +115 -0
  3. package/dist/analyze/index.cjs +54 -7
  4. package/dist/analyze/index.cjs.map +1 -1
  5. package/dist/analyze/index.d.cts +213 -456
  6. package/dist/analyze/index.d.ts +213 -456
  7. package/dist/analyze/index.js +56 -9
  8. package/dist/chunk-3JXCW6DX.cjs +3220 -0
  9. package/dist/chunk-3JXCW6DX.cjs.map +1 -0
  10. package/dist/chunk-4A2RMUCC.js +7647 -0
  11. package/dist/chunk-4A2RMUCC.js.map +1 -0
  12. package/dist/chunk-6XPTBE7Y.cjs +49 -0
  13. package/dist/chunk-6XPTBE7Y.cjs.map +1 -0
  14. package/dist/chunk-AFM2ULJH.cjs +5121 -0
  15. package/dist/chunk-AFM2ULJH.cjs.map +1 -0
  16. package/dist/chunk-E3F4AHAH.cjs +690 -0
  17. package/dist/chunk-E3F4AHAH.cjs.map +1 -0
  18. package/dist/chunk-ELCTZUJJ.js +379 -0
  19. package/dist/chunk-ELCTZUJJ.js.map +1 -0
  20. package/dist/chunk-FVSI36UV.js +690 -0
  21. package/dist/chunk-FVSI36UV.js.map +1 -0
  22. package/dist/chunk-GEDTJHAC.cjs +1 -0
  23. package/dist/chunk-GEDTJHAC.cjs.map +1 -0
  24. package/dist/chunk-GOZG57ER.js +3220 -0
  25. package/dist/chunk-GOZG57ER.js.map +1 -0
  26. package/dist/chunk-H3VORETI.js +2243 -0
  27. package/dist/chunk-H3VORETI.js.map +1 -0
  28. package/dist/chunk-H42UHPYW.js +1577 -0
  29. package/dist/chunk-H42UHPYW.js.map +1 -0
  30. package/dist/chunk-HFPS7JFD.js +1 -0
  31. package/dist/chunk-INHVXP6Z.js +3053 -0
  32. package/dist/chunk-INHVXP6Z.js.map +1 -0
  33. package/dist/chunk-K4ZJUGF4.cjs +379 -0
  34. package/dist/chunk-K4ZJUGF4.cjs.map +1 -0
  35. package/dist/chunk-KNJEKLKV.js +49 -0
  36. package/dist/chunk-KNJEKLKV.js.map +1 -0
  37. package/dist/chunk-M4CWTJWJ.cjs +7647 -0
  38. package/dist/chunk-M4CWTJWJ.cjs.map +1 -0
  39. package/dist/chunk-MXO72PLY.cjs +211 -0
  40. package/dist/chunk-MXO72PLY.cjs.map +1 -0
  41. package/dist/chunk-NLEU4QSE.cjs +360 -0
  42. package/dist/chunk-NLEU4QSE.cjs.map +1 -0
  43. package/dist/chunk-OE6WK2Y2.cjs +1577 -0
  44. package/dist/chunk-OE6WK2Y2.cjs.map +1 -0
  45. package/dist/chunk-Q2WGKJNN.cjs +1064 -0
  46. package/dist/chunk-Q2WGKJNN.cjs.map +1 -0
  47. package/dist/chunk-QYQQ5J6E.js +1 -0
  48. package/dist/chunk-QYQQ5J6E.js.map +1 -0
  49. package/dist/chunk-RHIJ3S4I.cjs +1 -0
  50. package/dist/chunk-RHIJ3S4I.cjs.map +1 -0
  51. package/dist/chunk-TTCLJX6D.js +211 -0
  52. package/dist/chunk-TTCLJX6D.js.map +1 -0
  53. package/dist/chunk-UADYAYAT.cjs +3053 -0
  54. package/dist/chunk-UADYAYAT.cjs.map +1 -0
  55. package/dist/chunk-VOHZB45S.js +5121 -0
  56. package/dist/chunk-VOHZB45S.js.map +1 -0
  57. package/dist/chunk-VTI6FOLB.js +1064 -0
  58. package/dist/chunk-VTI6FOLB.js.map +1 -0
  59. package/dist/chunk-WWURE4XT.cjs +2243 -0
  60. package/dist/chunk-WWURE4XT.cjs.map +1 -0
  61. package/dist/chunk-YH3BIKHH.js +360 -0
  62. package/dist/chunk-YH3BIKHH.js.map +1 -0
  63. package/dist/core/index.cjs +103 -6
  64. package/dist/core/index.cjs.map +1 -1
  65. package/dist/core/index.d.cts +96 -345
  66. package/dist/core/index.d.ts +96 -345
  67. package/dist/core/index.js +104 -7
  68. package/dist/generate/index.cjs +89 -10
  69. package/dist/generate/index.cjs.map +1 -1
  70. package/dist/generate/index.d.cts +437 -1095
  71. package/dist/generate/index.d.ts +437 -1095
  72. package/dist/generate/index.js +94 -15
  73. package/dist/index-9wcMhG73.d.ts +1095 -0
  74. package/dist/{index-D6b6na69.d.cts → index-B4o55NDr.d.cts} +13 -9
  75. package/dist/index-B9MNZboQ.d.cts +770 -0
  76. package/dist/index-B9MNZboQ.d.ts +770 -0
  77. package/dist/index-BGFW-8P2.d.cts +2307 -0
  78. package/dist/index-BO6xE9YU.d.cts +1804 -0
  79. package/dist/index-BkGmlyI3.d.cts +470 -0
  80. package/dist/index-CCNJRG-P.d.cts +1095 -0
  81. package/dist/index-CGpRv5sl.d.cts +643 -0
  82. package/dist/index-C_8o--VE.d.cts +1242 -0
  83. package/dist/index-CgHqxdps.d.ts +2307 -0
  84. package/dist/index-Chbst3Jx.d.ts +1343 -0
  85. package/dist/index-Cj49F892.d.ts +470 -0
  86. package/dist/{index-tSQh-xwY.d.ts → index-CunYKLiu.d.ts} +13 -9
  87. package/dist/index-D57QRbVn.d.ts +1804 -0
  88. package/dist/index-DInQqT8Q.d.cts +66 -0
  89. package/dist/index-DInQqT8Q.d.ts +66 -0
  90. package/dist/index-Dzr13q3d.d.cts +1343 -0
  91. package/dist/index-G1uwGGWQ.d.ts +643 -0
  92. package/dist/index-szBv3496.d.ts +1242 -0
  93. package/dist/index.cjs +358 -15
  94. package/dist/index.cjs.map +1 -1
  95. package/dist/index.d.cts +17 -15
  96. package/dist/index.d.ts +17 -15
  97. package/dist/index.js +407 -64
  98. package/dist/masks-DYbE485J.d.cts +445 -0
  99. package/dist/masks-DYbE485J.d.ts +445 -0
  100. package/dist/model/index.cjs +39 -7
  101. package/dist/model/index.cjs.map +1 -1
  102. package/dist/model/index.d.cts +4685 -243
  103. package/dist/model/index.d.ts +4685 -243
  104. package/dist/model/index.js +40 -8
  105. package/dist/positional-Cptlj-xs.d.ts +287 -0
  106. package/dist/positional-DvH_sd8u.d.cts +287 -0
  107. package/dist/theory/index.cjs +101 -7
  108. package/dist/theory/index.cjs.map +1 -1
  109. package/dist/theory/index.d.cts +513 -173
  110. package/dist/theory/index.d.ts +513 -173
  111. package/dist/theory/index.js +118 -24
  112. package/package.json +16 -11
  113. package/dist/chunk-36N73KZ3.cjs +0 -246
  114. package/dist/chunk-36N73KZ3.cjs.map +0 -1
  115. package/dist/chunk-4HLLZPXT.cjs +0 -3077
  116. package/dist/chunk-4HLLZPXT.cjs.map +0 -1
  117. package/dist/chunk-7MGZ7D62.cjs +0 -498
  118. package/dist/chunk-7MGZ7D62.cjs.map +0 -1
  119. package/dist/chunk-7NKDPF74.js +0 -3077
  120. package/dist/chunk-7NKDPF74.js.map +0 -1
  121. package/dist/chunk-CAN76NCU.cjs +0 -679
  122. package/dist/chunk-CAN76NCU.cjs.map +0 -1
  123. package/dist/chunk-D2FAJBYL.js +0 -479
  124. package/dist/chunk-D2FAJBYL.js.map +0 -1
  125. package/dist/chunk-EHVEBWM5.js +0 -1449
  126. package/dist/chunk-EHVEBWM5.js.map +0 -1
  127. package/dist/chunk-FCANXBAT.cjs +0 -30
  128. package/dist/chunk-FCANXBAT.cjs.map +0 -1
  129. package/dist/chunk-FNDWXRRI.cjs +0 -479
  130. package/dist/chunk-FNDWXRRI.cjs.map +0 -1
  131. package/dist/chunk-GQSGS2N4.js +0 -173
  132. package/dist/chunk-GQSGS2N4.js.map +0 -1
  133. package/dist/chunk-HKGYCQ34.js +0 -53
  134. package/dist/chunk-HKGYCQ34.js.map +0 -1
  135. package/dist/chunk-I24MV65U.js +0 -106
  136. package/dist/chunk-I24MV65U.js.map +0 -1
  137. package/dist/chunk-KMHOICIY.js +0 -679
  138. package/dist/chunk-KMHOICIY.js.map +0 -1
  139. package/dist/chunk-LW53AHQU.cjs +0 -1117
  140. package/dist/chunk-LW53AHQU.cjs.map +0 -1
  141. package/dist/chunk-N6IHQUOX.js +0 -1
  142. package/dist/chunk-NKIBQ6PT.js +0 -30
  143. package/dist/chunk-NKIBQ6PT.js.map +0 -1
  144. package/dist/chunk-NP4NA2FB.cjs +0 -106
  145. package/dist/chunk-NP4NA2FB.cjs.map +0 -1
  146. package/dist/chunk-NVE34YGZ.cjs +0 -1449
  147. package/dist/chunk-NVE34YGZ.cjs.map +0 -1
  148. package/dist/chunk-NYYWMV2C.js +0 -327
  149. package/dist/chunk-NYYWMV2C.js.map +0 -1
  150. package/dist/chunk-Q2XUP227.js +0 -845
  151. package/dist/chunk-Q2XUP227.js.map +0 -1
  152. package/dist/chunk-Q6OP4U43.cjs +0 -327
  153. package/dist/chunk-Q6OP4U43.cjs.map +0 -1
  154. package/dist/chunk-RC6GBMCD.cjs +0 -845
  155. package/dist/chunk-RC6GBMCD.cjs.map +0 -1
  156. package/dist/chunk-RK35RIN7.js +0 -1117
  157. package/dist/chunk-RK35RIN7.js.map +0 -1
  158. package/dist/chunk-RUMU3UA6.cjs +0 -53
  159. package/dist/chunk-RUMU3UA6.cjs.map +0 -1
  160. package/dist/chunk-T64C7ZD7.cjs +0 -1
  161. package/dist/chunk-T64C7ZD7.cjs.map +0 -1
  162. package/dist/chunk-XEBG3G53.js +0 -498
  163. package/dist/chunk-XEBG3G53.js.map +0 -1
  164. package/dist/chunk-YLEMGEFH.js +0 -246
  165. package/dist/chunk-YLEMGEFH.js.map +0 -1
  166. package/dist/chunk-ZEJR4TKL.cjs +0 -173
  167. package/dist/chunk-ZEJR4TKL.cjs.map +0 -1
  168. package/dist/function-BtiZb9i9.d.ts +0 -208
  169. package/dist/function-DNugAOrt.d.cts +0 -208
  170. package/dist/index-6VR8-kit.d.ts +0 -143
  171. package/dist/index-70p168VM.d.ts +0 -192
  172. package/dist/index-Be74mcon.d.cts +0 -152
  173. package/dist/index-BtXT6UQ0.d.cts +0 -192
  174. package/dist/index-C5UBbQ4G.d.ts +0 -152
  175. package/dist/index-C83mZI1P.d.ts +0 -242
  176. package/dist/index-DXd9JikN.d.cts +0 -246
  177. package/dist/index-DXd9JikN.d.ts +0 -246
  178. package/dist/index-OG65ocOr.d.cts +0 -272
  179. package/dist/index-OG65ocOr.d.ts +0 -272
  180. package/dist/index-tJhkmQgE.d.cts +0 -143
  181. package/dist/index-y_ak9_Hi.d.cts +0 -242
  182. package/dist/styled-DIRkk9Uz.d.ts +0 -506
  183. package/dist/styled-DJbkJQfj.d.cts +0 -506
  184. package/dist/types-BN6_Q9Kl.d.cts +0 -39
  185. package/dist/types-BN6_Q9Kl.d.ts +0 -39
  186. /package/dist/{chunk-N6IHQUOX.js.map → chunk-HFPS7JFD.js.map} +0 -0
@@ -1,501 +1,258 @@
1
- import { T as TimeSignature } from '../index-DXd9JikN.js';
2
- import { K as KeyScale, N as NoteEvent } from '../types-BN6_Q9Kl.js';
3
- import { C as Chord, d as ChordToneRole } from '../index-C83mZI1P.js';
4
- export { a as ChordQuality, b as ChordSegment, c as ChordSpan, P as PitchSpelling } from '../index-C83mZI1P.js';
5
- import { V as VoiceSnapshot, N as NoteSafety, S as SafetyProfile } from '../index-6VR8-kit.js';
6
- import { a as ChordTimeline, C as CadenceHit } from '../index-C5UBbQ4G.js';
7
- export { b as ChordTimelineOptions, c as ChordTimelineResult, d as chordTimelineFromChords, e as chordTimelineFromNotes, f as detectCadences } from '../index-C5UBbQ4G.js';
8
- export { C as ChordMatch, D as DetectChordOptions, a as DetectKeyOptions, K as KeyMatch, b as KeyVariant, d as detectChord, c as detectChordBest, e as detectKey, f as detectKeyBest, g as detectKeyFromNotes } from '../index-70p168VM.js';
9
- export { B as BorrowedSource, C as Cadence, a as ChordAnalysis, H as HarmonicFunction, b as analyzeChord, c as borrowedSource, d as detectCadence, f as functionOf, i as isBorrowedChord, e as isDiatonic, g as isMinorKey, p as parallelKey, s as secondaryDominant } from '../function-BtiZb9i9.js';
1
+ import { h as NoteLike, f as Note, g as NoteEvent } from '../index-B9MNZboQ.js';
2
+ export { a as Articulation, b as IntervalQualityLabel, d as KeyScale, S as SpelledInterval } from '../index-B9MNZboQ.js';
3
+ export { M as MeterChange, b as MeterLike, c as MeterMap, T as TimeSignature } from '../index-Cj49F892.js';
4
+ import { K as KeyLike, b as Chord, c as ChordLike } from '../index-szBv3496.js';
5
+ export { d as ChordQuality, e as ChordSegment, g as ChordSpan, j as ChordToneRole, k as KeyRelation, N as NoteSafety, l as PitchSpelling, S as SafetyProfile, V as VoiceSnapshot } from '../index-szBv3496.js';
6
+ export { r as ScaleName } from '../masks-DYbE485J.js';
7
+ export { A as ArrangementAnalysis, a as ArrangementOptions, b as ArrangementSession, c as ArrangementTrack, C as ChordMatch, d as Conflict, e as ContourDirection, D as DetectChordOptions, f as DetectKeyOptions, E as ExtractMotifsOptions, F as FormSection, g as FormSectionOptions, H as Hypermeter, h as HypermeterOptions, K as KeyMatch, i as KeyVariant, M as MODAL_SCALE_NAMES, j as MelodicComparison, k as MelodicContour, l as MelodicContourShape, m as MelodicPhrase, n as ModalCandidate, o as ModalScaleName, p as MotifData, q as MotifOccurrence, r as MotifRelation, s as MotifRelationKind, P as Phrase, t as PhraseOptions, u as PhraseSignal, R as ReduceProgressionOptions, v as ReducedChord, w as ReductionBasis, x as ReductionLevel, S as StructuralCadence, T as TensionPoint, y as TrackAnalysis, z as TrackEdit, B as TrackRole, G as analyzeArrangement, I as compareMelodies, J as createArrangementSession, L as detectChord, N as detectChordBest, O as detectKey, Q as detectKeyBest, U as detectKeyFromNotes, V as extractMotifs, W as hypermeter, X as melodicContour, Y as melodicSimilarity, Z as motifFromNotes, _ as phrasesFromTimeline, $ as reduceProgression, a0 as relateMotifs, a1 as sectionsFromNotes, a2 as structuralCadences, a3 as tensionCurve, a4 as tensionCurveFrom } from '../index-D57QRbVn.js';
8
+ import { e as ChordTimeline } from '../index-Chbst3Jx.js';
9
+ export { A as AnalyzeChordOptions, a as AnalyzedNote, B as BorrowedSource, C as CadenceHit, b as CadenceResult, c as ChordAnalysis, d as ChordSegmentation, f as ChordTimelineOptions, g as ChordTimelineResult, h as ChordToRomanOptions, D as DetectCadenceOptions, E as ExplainRomanOptions, H as HarmonicFunction, I as IdentifiedVoiceNote, K as KeyContext, i as KeyProfileName, j as KeyProfilePair, k as KeyRegion, l as KeyTimelineOptions, P as PivotChord, R as RejectedCandidate, m as RomanExplanation, S as SuspensionFigure, T as TheoryLabel, V as VoiceNote, n as analyzeChord, o as analyzeVoice, p as borrowedSource, q as chordTimelineFromChords, r as chordTimelineFromNotes, s as chordToRoman, t as detectCadence, u as detectCadences, v as detectModulations, w as explainRoman, x as functionOf, y as isBorrowedChord, z as isDiatonic, F as isMinorKey, G as keyTimelineFromNotes, J as pivotChords, L as prevailingKeyOf, M as romanToChord, N as secondaryDominant, O as secondaryDominantOf, Q as toVoiceNotes } from '../index-Chbst3Jx.js';
10
10
 
11
11
  /**
12
- * Roman-numeral parsing and formatting.
13
- *
14
- * The {@link ROMAN_STYLE} table is the single source of truth for both
15
- * directions, so {@link chordToRoman} and {@link romanToChord} stay mutual
16
- * inverses by construction.
12
+ * Augmented-sixth chords: the Italian, French, and German sixths.
13
+ *
14
+ * All three stand on the lowered submediant (b6) in the bass and carry an
15
+ * augmented sixth above it, and all three are predominants whose two outer
16
+ * voices resolve outward onto the dominant. The family is defined by that bass
17
+ * and that interval rather than by a stack of thirds, so a chord is recognized
18
+ * here from the tones sounding over that bass rather than through the
19
+ * Roman-numeral degree tables.
20
+ *
21
+ * The interval is spelled, so recognition reads letters and not only pitch
22
+ * classes: over an Ab bass the augmented sixth is F# and the minor seventh of
23
+ * the bVI7 it otherwise sounds exactly like is Gb. A caller holding pitch
24
+ * classes alone — an analysis of a MIDI track, which names no accidentals —
25
+ * makes that reading with {@link augmentedSixthFromPitchClasses}, and the chord
26
+ * it hands back carries the spelling that decided it.
17
27
  */
18
28
 
19
29
  /**
20
- * Build the chord denoted by a Roman numeral in a key.
21
- *
22
- * Supports accidentals (`bVII`, `#iv`), case-based triad quality, the `o`/`ø`/`+`
23
- * suffixes, sevenths (`V7`, `viio7`, `Imaj7`), figured-bass inversions
24
- * (`V6`, `V64`, `V65`, `V43`, `V42`), and applied/secondary chords via a slash
25
- * (`V7/V`, `viio/ii`). The target after the slash is read as a scale degree
26
- * whose root becomes a local major tonic for the applied chord. Inverted chords
27
- * carry a `bassPc`.
30
+ * The three augmented-sixth chords, under the nationalities convention gives
31
+ * them.
28
32
  *
29
- * The `ø` glyph always denotes the half-diminished seventh (`iiø` == `iiø7` ==
30
- * `m7b5`), since no half-diminished triad exists. Extension and added-tone
31
- * suffixes are the canonical case-sensitive forms emitted by
32
- * {@link chordToRoman} and honor the numeral case: `V9` -> `dom9`, `ii9` ->
33
- * `min9`, `Imaj9` -> `maj9`, `V11` -> `11`, `V13` -> `13`, `Iadd6`/`iadd6` ->
34
- * `6`/`min6`, `I69` -> `6/9`, `Isus2`/`Isus4`, `Iadd9`/`Iadd11`, `Ib5` ->
35
- * `majb5`, `I5` -> `5`, and the altered dominants `V7b9`/`V7#9`/`V7#11`/`V7b13`.
36
- * A figure string that is neither a recognized inversion nor a supported
37
- * quality suffix throws rather than silently degrading to a triad.
33
+ * Over an Ab bass in C: the Italian sixth is Ab C F#, the French sixth adds the
34
+ * supertonic (Ab C D F#), and the German sixth adds the lowered mediant
35
+ * (Ab C Eb F#).
38
36
  *
39
- * @param text The Roman numeral.
40
- * @param key The prevailing key.
41
- * @returns The chord.
37
+ * @category Functional Harmony
38
+ */
39
+ type AugmentedSixthKind = 'italian' | 'french' | 'german';
40
+ /**
41
+ * Build an augmented-sixth chord in a key.
42
+ *
43
+ * The chord always carries an explicit `bassPc` on the lowered submediant,
44
+ * which is what makes it an augmented sixth rather than the dominant seventh or
45
+ * the altered supertonic seventh it shares pitch classes with, and an explicit
46
+ * interval template naming exactly the tones that sound.
47
+ *
48
+ * It also carries its spelling: the tones as `toneSpellings`, and the root and
49
+ * bass as the usual hints. Those are what let the ordinary chord speller and
50
+ * the symbol formatter name the chord — `Ab C Eb F#` and `Ab7` for the German
51
+ * sixth of C major — since a stack of thirds over that root would put a Gb
52
+ * where the augmented sixth belongs. The spelling is the one the key is written
53
+ * on, so a flat-side key takes the double flats it genuinely needs.
54
+ *
55
+ * The lowered submediant is taken as eight semitones above the tonic in every
56
+ * mode, since the chord is a chromatic alteration rather than a degree of the
57
+ * prevailing scale.
58
+ *
59
+ * @param kind Which of the three augmented sixths to build.
60
+ * @param key The prevailing key, as a key name, a key/scale, or a `Key`.
61
+ * @returns The chord, with its bass on the lowered submediant.
62
+ * @throws If `kind` is not one of the three, or the key's root is not finite.
63
+ * @see {@link spellAugmentedSixth} to spell the family from a tonic without
64
+ * building a chord at all.
42
65
  * @example
43
66
  * ```ts
44
- * import { romanToChord, majorKey } from '@libraz/libcantus';
45
- * romanToChord('V7', majorKey(0)); // G7 in C major: { rootPc: 7, quality: 'dom7', ... }
67
+ * import { augmentedSixthChord, majorKey } from '@libraz/libcantus';
68
+ * augmentedSixthChord('german', majorKey(0));
69
+ * // Ab C Eb F# in C: { rootPc: 8, quality: 'dom7', intervals: [0, 4, 7, 10], bassPc: 8, ... }
46
70
  * ```
47
71
  * @category Functional Harmony
48
72
  */
49
- declare function romanToChord(text: string, key: KeyScale): Chord;
73
+ declare function augmentedSixthChord(kind: AugmentedSixthKind, key: KeyLike): Chord;
50
74
  /**
51
- * Options for {@link chordToRoman}.
52
- *
75
+ * Identify a chord as one of the augmented sixths, or null when it is none.
76
+ *
77
+ * A chord qualifies when it sounds the lowered submediant of `key` in the bass,
78
+ * its pitch classes are exactly one of the three sets built over that bass, and
79
+ * its own letters spell the augmented sixth above it. All three are needed: the
80
+ * same pitch classes over the same bass are an ordinary bVI7 when the tone ten
81
+ * semitones above the bass is written as a minor seventh, and the letters are
82
+ * the only place that distinction lives.
83
+ *
84
+ * A chord naming no `bassPc` sounds its root lowest, which is how the two
85
+ * conventions for that field — the sounding bass always, or only a bass that
86
+ * differs from the root — are read alike here.
87
+ *
88
+ * The root the chord is measured from is not consulted, so a chord voiced or
89
+ * rebuilt around any of its own tones still identifies as long as it keeps its
90
+ * spelling.
91
+ *
92
+ * @param chord The chord to test, as a chord symbol, chord data, or a `Chord`.
93
+ * @param key The prevailing key, as a key name, a key/scale, or a `Key`.
94
+ * @returns The kind of augmented sixth, or null.
95
+ * @see {@link augmentedSixthFromPitchClasses} to make the reading from pitch
96
+ * classes that carry no spelling at all.
97
+ * @example
98
+ * ```ts
99
+ * import { augmentedSixthKind, majorKey, romanToChord } from '@libraz/libcantus';
100
+ * augmentedSixthKind(romanToChord('Fr6', majorKey(0)), majorKey(0)); // 'french'
101
+ * ```
53
102
  * @category Functional Harmony
54
103
  */
55
- type ChordToRomanOptions = {
56
- /**
57
- * Render a tonicizing chord as an applied numeral (`V7/V`, `viio7/ii`)
58
- * instead of naming its root against the home key (`II7`, `#ivo7`).
59
- *
60
- * Off by default: naming the root is always a correct spelling, whereas
61
- * whether a chromatic dominant is genuinely applied is a reading only the
62
- * caller can make. Turning it on makes {@link chordToRoman} the exact inverse
63
- * of {@link romanToChord} for the chords {@link secondaryDominant} builds.
64
- */
65
- applied?: boolean;
66
- };
104
+ declare function augmentedSixthKind(chord: ChordLike, key: KeyLike): AugmentedSixthKind | null;
67
105
  /**
68
- * Render a chord as a Roman numeral relative to a key.
69
- *
70
- * Diatonic roots take their scale-degree numeral directly, so numerals are
71
- * correct in both major and minor keys (and any custom scale). The quality
72
- * selects the case and suffix; chromatic roots receive a flat/sharp spelling by
73
- * convention. A key that is not heptatonic has no numeral of its own for every
74
- * degree, so its roots are named against its parallel major.
75
- *
76
- * When the chord carries a `bassPc` on a chord tone, a figured-bass inversion
77
- * (`6`, `64`, `65`, `43`, `42`) is emitted for plain triads and true seventh
78
- * chords; added-tone and extended qualities have no lossless figure and render
79
- * in root position instead (the bass is dropped, pitch classes are preserved).
80
- *
81
- * A `bassPc` that is *not* a chord tone — a pedal or passing bass such as
82
- * `C/D` — is likewise dropped, and the numeral names the upper chord alone.
83
- * Roman-numeral notation spends its slash on applied chords (`V7/V`), so there
84
- * is no unambiguous way to write such a bass; a caller that must keep it should
85
- * read `chord.bassPc` alongside the numeral.
86
- *
87
- * @param chord The chord to name.
88
- * @param key The prevailing key.
89
- * @param opts `applied` renders tonicizing chords as `V7/V`-style numerals.
90
- * @returns The Roman numeral string.
106
+ * Read a set of sounding pitch classes over a sounding bass as an augmented
107
+ * sixth, or null when they are none.
108
+ *
109
+ * The reading a caller holding no spelling has to make for itself: pitch
110
+ * classes name no accidentals, so nothing downstream can tell the German sixth
111
+ * from the bVI7 it sounds like once the letters are gone. The chord returned is
112
+ * the one {@link augmentedSixthChord} builds, spelling and all, so it keeps
113
+ * identifying as an augmented sixth everywhere it travels.
114
+ *
115
+ * Not every b6-1-b3-#4 over a b6 bass is an augmented sixth, and this cannot
116
+ * know which one is: it answers what the tones would spell, and the caller
117
+ * decides whether that reading is the one the music supports.
118
+ *
119
+ * @param pcs The sounding pitch classes; duplicates and unreduced values are
120
+ * fine.
121
+ * @param bassPc The pitch class sounding in the bass.
122
+ * @param key The prevailing key, as a key name, a key/scale, or a `Key`.
123
+ * @returns The augmented-sixth chord, or null.
124
+ * @see {@link augmentedSixthKind} to read a chord that already carries its
125
+ * spelling.
91
126
  * @example
92
127
  * ```ts
93
- * import { chordToRoman, makeChord, majorKey } from '@libraz/libcantus';
94
- * chordToRoman(makeChord(7, 'dom7'), majorKey(0)); // => 'V7' (G7 in C major)
95
- * chordToRoman(makeChord(2, 'dom7'), majorKey(0), { applied: true }); // => 'V7/V'
128
+ * import { augmentedSixthFromPitchClasses, majorKey } from '@libraz/libcantus';
129
+ * // Ab C Eb Gb sounding over Ab in C: the German sixth, spelled with its F#.
130
+ * augmentedSixthFromPitchClasses([8, 0, 3, 6], 8, majorKey(0))?.quality; // 'dom7'
96
131
  * ```
97
132
  * @category Functional Harmony
98
133
  */
99
- declare function chordToRoman(chord: Chord, key: KeyScale, opts?: ChordToRomanOptions): string;
100
-
101
- /**
102
- * A suspension figure, named by the interval above the bass and the interval it
103
- * resolves to.
104
- *
105
- * @category Arrangement & Analysis
106
- */
107
- type SuspensionFigure = 'sus4-3' | 'sus6-5' | 'sus7-6' | 'sus9-8' | 'sus2-3';
134
+ declare function augmentedSixthFromPitchClasses(pcs: readonly number[], bassPc: number, key: KeyLike): Chord | null;
108
135
  /**
109
- * A theory annotation attached to a note.
110
- *
111
- * @category Arrangement & Analysis
112
- */
113
- type TheoryLabel = {
114
- kind: 'chordTone';
115
- role: ChordToneRole;
116
- } | {
117
- kind: 'tension';
118
- degree: 9 | 11 | 13;
119
- } | {
120
- kind: 'avoid';
121
- } | {
122
- kind: 'passing';
123
- } | {
124
- kind: 'neighbor';
125
- } | {
126
- kind: 'suspension';
127
- type: SuspensionFigure;
128
- resolveTo: number;
129
- } | {
130
- kind: 'anticipation';
131
- } | {
132
- kind: 'escape';
133
- } | {
134
- kind: 'needsResolution';
135
- resolveTo: number;
136
- } | {
137
- kind: 'leadingTone';
138
- resolveTo: number;
139
- };
140
- /**
141
- * A note with its theory labels and a short rationale.
142
- *
143
- * The labels describe the note against the chord sounding at its **onset**;
144
- * they are not re-derived for the later chords a long note sustains across.
145
- * `analyzeArrangement` does re-check every crossed chord change, and reports
146
- * what it finds as conflicts, so a note labelled a chord tone here can still
147
- * appear in `conflicts` at a later beat.
148
- *
149
- * The identity fields make an annotation mappable back to the caller's input:
150
- * `originalIndex` is the note's position in the array that was passed in, and
151
- * `trackIndex` is set by {@link analyzeArrangement}.
152
- *
153
- * @category Arrangement & Analysis
154
- */
155
- type AnalyzedNote = {
156
- noteId: number;
157
- /** The note's index in the caller's own array, when the caller supplied one. */
158
- originalIndex?: number;
159
- /** The note's track, set by `analyzeArrangement`. */
160
- trackIndex?: number;
161
- pitch: number;
162
- startBeat: number;
163
- durationBeat: number;
164
- labels: TheoryLabel[];
165
- rationale?: string;
166
- };
167
- /**
168
- * A single note in a monophonic voice: a {@link NoteEvent} with a stable id and,
169
- * optionally, its index in the caller's original array.
170
- *
171
- * @category Arrangement & Analysis
172
- */
173
- type VoiceNote = NoteEvent & {
174
- id?: number;
175
- originalIndex?: number;
176
- };
177
- /**
178
- * A voice note whose id has been assigned, as {@link toVoiceNotes} returns and
179
- * {@link analyzeArrangement} works in.
180
- *
181
- * @category Arrangement & Analysis
182
- */
183
- type IdentifiedVoiceNote = VoiceNote & {
184
- id: number;
185
- };
186
- /**
187
- * Give plain note events the ids {@link analyzeVoice} reports back.
188
- *
189
- * The id is only an identity handle, so the array position serves; this exists
190
- * so the mapping is a named call rather than a `map` every caller writes.
191
- *
192
- * @param events The note events, in time order.
193
- * @returns The same notes as voice notes, each carrying its index as its id.
194
- * @category Arrangement & Analysis
195
- */
196
- declare function toVoiceNotes(events: readonly NoteEvent[]): IdentifiedVoiceNote[];
197
- /**
198
- * Label every note of a voice with its theory roles.
199
- *
200
- * Each note is classified against the chord sounding at its beat. Chord tones
201
- * get a role label; non-chord tones are matched, in order, as suspensions
202
- * (prepared by an identical consonant pitch and resolving by step), passing
203
- * tones, neighbors, anticipations, and escape tones, then fall back to tension,
204
- * avoid, or an unresolved-dissonance label. Leading-tone resolutions are noted
205
- * additionally.
206
- *
207
- * The voice is expected to be monophonic (one note at a time). Notes sharing an
208
- * onset are treated as simultaneous cluster members, not melodic neighbors, so
209
- * they receive no melodic labels (suspension, passing, neighbor, anticipation,
210
- * escape) — only harmonic ones. Callers with truly polyphonic material should
211
- * split it into monophonic sub-voices first, as `analyzeArrangement` does.
212
- *
213
- * @param voice The monophonic voice, in time order. A note without an `id` is
214
- * identified by its position, so plain note events can be passed straight in.
215
- * @param chordAtBeat Chord sounding at a given beat, or null.
216
- * @param key Key context for leading-tone detection.
217
- * @param otherVoicesAtBeat Other sounding voices at a given beat; defaults to
218
- * none, which is the whole story for a solo line.
219
- * @returns One annotation per input note.
136
+ * Spell an augmented-sixth chord from a spelled tonic, lowest note first.
137
+ *
138
+ * This is the spelled counterpart of {@link augmentedSixthChord}, and the table
139
+ * the chord's own spelling is built from: every tone is placed by a named
140
+ * interval above the bass, so the outer interval is an augmented sixth in every
141
+ * key rather than the minor seventh its ten semitones would otherwise imply.
142
+ * Reach for it when a spelled tonic is all there is; spelling the chord itself
143
+ * with {@link spellChord} names the same notes, in the chord's own order.
144
+ *
145
+ * A tonic carrying an octave yields notes carrying octaves, ascending from the
146
+ * bass; an octave-less tonic yields octave-less notes.
147
+ *
148
+ * @param kind Which of the three augmented sixths to spell.
149
+ * @param tonic The spelled tonic of the key, as a note name, a MIDI number,
150
+ * note data, or a `Note`.
151
+ * @returns The spelled tones, bass first and ascending.
152
+ * @throws If `kind` is not one of the three, or `tonic` is not a note.
220
153
  * @example
221
154
  * ```ts
222
- * import { analyzeVoice, makeChord, majorKey } from '@libraz/libcantus';
223
- * const voice = [
224
- * { pitch: 60, startBeat: 0, durationBeat: 1 },
225
- * { pitch: 62, startBeat: 1, durationBeat: 1 },
226
- * { pitch: 64, startBeat: 2, durationBeat: 1 },
227
- * ];
228
- * const cMajor = makeChord(0, 'maj');
229
- * const labels = analyzeVoice(voice, () => cMajor, majorKey(0));
230
- * labels; // one AnalyzedNote per input note, in the same order
155
+ * import { noteNames, parseNote, spellAugmentedSixth } from '@libraz/libcantus';
156
+ * noteNames(spellAugmentedSixth('german', parseNote('C'))); // ['Ab', 'C', 'Eb', 'F#']
157
+ * noteNames(spellAugmentedSixth('italian', 'E')); // ['C', 'E', 'A#']
231
158
  * ```
232
- * @category Arrangement & Analysis
159
+ * @category Functional Harmony
233
160
  */
234
- declare function analyzeVoice(voice: readonly VoiceNote[], chordAtBeat: (beat: number) => Chord | null, key: KeyScale, otherVoicesAtBeat?: (beat: number) => VoiceSnapshot[]): AnalyzedNote[];
161
+ declare function spellAugmentedSixth(kind: AugmentedSixthKind, tonic: NoteLike): Note[];
235
162
 
236
163
  /**
237
- * Arrangement track-role analysis: a single harmony is inferred from every
238
- * track's notes pooled together, each track is then analysed against that shared
239
- * harmony, and notes that clash with the sounding chord are collected as
240
- * conflicts.
164
+ * Line spelling: name every note of a melodic line in one pass.
165
+ *
166
+ * {@link spellPitch} decides one note from the key, the root of the chord under
167
+ * it and its two immediate neighbours. That is enough for a single note, but a
168
+ * line spelled note by note can zig-zag between the two readings of one idea —
169
+ * a rising whole-tone figure comes out F# Ab Bb, with a diminished third in the
170
+ * middle of it.
171
+ *
172
+ * This module solves the whole line at once. The theory layer's own judgment
173
+ * becomes the cost of emitting a spelling, the melodic rules become the cost of
174
+ * moving from one spelling to the next, and a dynamic program takes the
175
+ * cheapest path across the line. Nothing here re-decides what
176
+ * {@link spellPitchClass} and {@link spellChord} already decide: it only weighs
177
+ * their answers against the shape of the line they sit in.
241
178
  */
242
179
 
243
180
  /**
244
- * The musical role a track plays in the arrangement.
245
- *
246
- * Only `drums` changes the analysis: its pitches select instruments rather than
247
- * naming harmony, so such a track is excluded from chord and key inference and
248
- * from every voice-leading comparison, and is reported back with no
249
- * annotations. `harmony` is meaningful through
250
- * {@link ArrangementOptions.harmonyTracks}, which names the tracks the chords
251
- * are inferred from. The rest are labels carried through to
252
- * {@link TrackAnalysis.role} for the caller's own use; nothing is inferred, so
253
- * a track left unlabelled reports `'other'` rather than a guess.
254
- *
255
- * @category Arrangement & Analysis
256
- */
257
- type TrackRole = 'melody' | 'harmony' | 'bass' | 'drums' | 'other';
258
- /**
259
- * One input track: its notes plus optional name and role.
260
- *
261
- * @category Arrangement & Analysis
262
- */
263
- type ArrangementTrack = {
264
- name?: string;
265
- role?: TrackRole;
266
- notes: readonly NoteEvent[];
267
- };
268
- /**
269
- * A track after analysis: its resolved name, role, and per-note annotations.
270
- *
271
- * @category Arrangement & Analysis
272
- */
273
- type TrackAnalysis = {
274
- name: string;
275
- role: TrackRole;
276
- /** One {@link AnalyzedNote} per note, labelled against the inferred chords. */
277
- notes: AnalyzedNote[];
278
- };
279
- /**
280
- * A note that clashes with the harmony sounding beneath it.
281
- *
282
- * @category Arrangement & Analysis
283
- */
284
- type Conflict = {
285
- beat: number;
286
- trackName: string;
287
- /** The clashing track's index in the caller's input array. */
288
- trackIndex: number;
289
- /** The note's index in that track's own note array. */
290
- originalIndex?: number;
291
- /** The id the note carries in {@link TrackAnalysis.notes}. */
292
- noteId: number;
293
- pitch: number;
294
- /**
295
- * The note's theory labels at its onset, so a caller can tell a genuine clash
296
- * from a passing tone, a neighbour, or a prepared suspension — all of which
297
- * are ordinary non-chord tones that still evaluate as unsafe.
298
- */
299
- labels: TheoryLabel[];
300
- /** The clash severity: {@link NoteSafety.Warning} or {@link NoteSafety.Dissonant}. */
301
- safety: NoteSafety;
302
- /** {@link SafetyResult.reasons} bitmask. */
303
- reasons: number;
304
- /** Preferred stepwise resolution, when the safety evaluator provides one. */
305
- resolveTo?: number;
306
- /** Nearby fully safe replacement pitches, nearest first. */
307
- suggestions?: number[];
308
- rationale?: string;
309
- };
310
- /**
311
- * The full result of {@link analyzeArrangement}.
181
+ * Options for {@link spellLine}.
312
182
  *
313
183
  * @category Arrangement & Analysis
314
184
  */
315
- type ArrangementAnalysis = {
316
- key: KeyScale;
317
- timeline: ChordTimeline;
318
- segmentConfidence: number[];
319
- cadences: CadenceHit[];
320
- tracks: TrackAnalysis[];
321
- /** Notes clashing with the sounding harmony, worst severity first. */
322
- conflicts: Conflict[];
323
- };
324
- /**
325
- * Options controlling {@link analyzeArrangement} and {@link tensionCurve}.
326
- *
327
- * @category Arrangement & Analysis
328
- */
329
- type ArrangementOptions = {
330
- /** Key context; inferred from the pooled notes when omitted. */
331
- key?: KeyScale;
332
- /**
333
- * A chord timeline to analyse against, instead of inferring one from the
334
- * notes. Supply it to analyse a hand-written progression, or to reuse the
335
- * timeline of an earlier call rather than re-running chord inference.
336
- */
337
- timeline?: ChordTimeline;
338
- /**
339
- * Indices of the tracks the harmony is inferred from. Defaults to every
340
- * pitched track. Percussion tracks are excluded either way.
341
- */
342
- harmonyTracks?: number[];
343
- /**
344
- * Lowest severity reported in `conflicts`.
345
- *
346
- * @defaultValue {@link NoteSafety.Warning}
347
- */
348
- minSeverity?: NoteSafety;
349
- /**
350
- * Time signature; defaults to 4/4.
351
- *
352
- * @defaultValue `4/4`
353
- */
354
- ts?: TimeSignature;
355
- /**
356
- * Chord-slot length in beats; defaults to one bar of `ts`.
357
- *
358
- * @defaultValue one bar of `ts`
359
- */
360
- harmonicRhythm?: number;
361
- /**
362
- * Safety profile used for conflict detection; defaults to `pop`.
363
- *
364
- * @defaultValue `'pop'`
365
- */
366
- profile?: SafetyProfile;
185
+ type SpellLineOptions = {
367
186
  /**
368
- * Upper bound on the work this call may do — note counts, windows, and
369
- * candidate counts are each checked against it before anything is allocated.
370
- *
371
- * Raise it to analyse a piece larger than the default allows; the default is
372
- * {@link DEFAULT_GENERATION_BUDGET}, chosen so a runaway input fails fast
373
- * rather than blocking the thread.
374
- *
375
- * @defaultValue {@link DEFAULT_GENERATION_BUDGET}
187
+ * Tonic spelling to anchor the key, for a caller that already knows how the
188
+ * piece is written. It must sound the key's own root pitch class; anything
189
+ * else is rejected rather than spelled. Defaults to the conventional
190
+ * spelling `spelledKeyOf` gives the key. Accepted as a note name, a MIDI
191
+ * number, note data, or a `Note`.
376
192
  */
193
+ tonic?: NoteLike;
194
+ /** Upper bound on the work the line may cost; defaults to the generation budget. */
377
195
  budget?: number;
378
196
  };
379
197
  /**
380
- * Analyse a whole arrangement against a single inferred harmony.
381
- *
382
- * The chord timeline and key are inferred from the pooled notes of every
383
- * pitched track (see {@link chordTimelineFromNotes}); pooling all voices is
384
- * robust even when roles are absent or a track doubles the harmony, so it is
385
- * preferred over deriving the harmony from a subset. A track marked
386
- * `role: 'drums'` is excluded — its pitches name instruments, not harmony, and
387
- * pooling them would corrupt the key, the timeline and every reading built on
388
- * them. Pass `harmonyTracks` to restrict inference further, or `timeline` to
389
- * analyse against a progression you already have. Zero- and negative-length
390
- * notes never sound, so they are dropped at ingest and appear in neither the
391
- * annotations nor the conflicts.
392
- *
393
- * Voice analysis assumes one monophonic voice at a time, so each track is
394
- * first partitioned into monophonic sub-voices (a polyphonic block-chord track
395
- * fans out into one sub-voice per simultaneous note; a monophonic track is
396
- * unaffected). Each sub-voice is labelled note-by-note against the harmony
397
- * with {@link analyzeVoice}, seeing every other sub-voice — including siblings
398
- * within the same track — as its accompaniment. Every note is then re-checked
399
- * with {@link evaluateSafety} at its onset and again at each chord change it
400
- * sustains across, so a held note that clashes with a later chord is caught:
401
- * evaluations that are not {@link NoteSafety.Safe} become conflicts, one per
402
- * clashing beat, sorted worst severity first and then by beat.
403
- *
404
- * Every ordinary non-chord tone — a passing note, a neighbour, a prepared
405
- * suspension — is by definition unsafe against the chord under it, so the
406
- * conflict list is a report, not a fault list. Each conflict carries the note's
407
- * `labels` and its `trackIndex`/`originalIndex`, so a caller can tell those
408
- * apart and map a conflict back to the note it passed in; `minSeverity` narrows
409
- * the list to outright dissonance.
410
- *
411
- * @param tracks The tracks to analyse.
412
- * @param opts Analysis options; see {@link ArrangementOptions}.
413
- * @returns The inferred harmony, per-track annotations, cadences, and conflicts.
414
- * @example
415
- * ```ts
416
- * import { analyzeArrangement } from '@libraz/libcantus';
417
- * const melody = [
418
- * { pitch: 60, startBeat: 0, durationBeat: 2 },
419
- * { pitch: 67, startBeat: 2, durationBeat: 2 },
420
- * ];
421
- * const { key, conflicts } = analyzeArrangement([{ role: 'melody', notes: melody }]);
422
- * conflicts; // notes clashing with the inferred harmony, worst severity first
423
- * ```
424
- * @category Arrangement & Analysis
425
- */
426
- declare function analyzeArrangement(tracks: ArrangementTrack[], opts?: ArrangementOptions): ArrangementAnalysis;
427
-
428
- /**
429
- * Arrangement tension analysis: a coarse harmonic-tension curve sampled at
430
- * regular beats across a whole multi-track piece, combining the sounding
431
- * chord's harmonic function, the dissonance of the sounding notes, and their
432
- * registral span into a single normalized reading.
433
- */
434
-
435
- /**
436
- * A tension reading sampled at a beat.
437
- *
438
- * @category Arrangement & Analysis
439
- */
440
- type TensionPoint = {
441
- beat: number;
442
- /** Combined tension in [0, 1]. */
443
- tension: number;
444
- };
445
- /**
446
- * Sample the harmonic tension of an arrangement at regular beats.
447
- *
448
- * At each sample beat the tension combines three normalized terms:
449
- * - the sounding chord's harmonic function (dominant `1`, subdominant `0.5`,
450
- * tonic or no chord `0`), weighted `FUNCTION_WEIGHT`;
451
- * - the dissonance of the sounding notes — the larger of the share of sounding
452
- * pitches that are not chord tones and the share that {@link evaluateSafety}
453
- * rates {@link NoteSafety.Dissonant} — weighted `DISSONANCE_WEIGHT`;
454
- * - the registral span of the sounding notes, saturating at
455
- * `SPAN_SATURATION` semitones, weighted `SPAN_WEIGHT`.
456
- *
457
- * The weighted sum is clamped to [0, 1]. The harmony is inferred from all tracks
458
- * pooled together, so the result is deterministic and self-contained.
459
- *
460
- * @param tracks The tracks to sample.
461
- * @param opts Analysis options plus an optional `step` (default one beat).
462
- * @returns One {@link TensionPoint} per sampled beat, in beat order.
463
- * @throws If `step` is not positive.
464
- * @example
465
- * ```ts
466
- * import { tensionCurve } from '@libraz/libcantus';
467
- * const notes = [{ pitch: 60, startBeat: 0, durationBeat: 4 }];
468
- * const curve = tensionCurve([{ notes }], { step: 1 });
469
- * curve; // one { beat, tension } sample per beat, in beat order
470
- * ```
471
- * @category Arrangement & Analysis
472
- */
473
- declare function tensionCurve(tracks: ArrangementTrack[], opts?: ArrangementOptions & {
474
- step?: number;
475
- }): TensionPoint[];
476
- /**
477
- * Sample the tension of an arrangement already analysed by
478
- * {@link analyzeArrangement}, reusing its harmony instead of inferring it again.
479
- *
480
- * `analyzeArrangement` followed by `tensionCurve` runs chord and key inference
481
- * twice over the same notes. This takes the analysis it already produced, so
482
- * the curve and the annotations describe the same harmony by construction.
483
- *
484
- * @param tracks The same tracks that were analysed.
485
- * @param analysis The result of {@link analyzeArrangement} for those tracks.
486
- * @param opts Analysis options plus an optional `step` (default one beat).
487
- * @returns One {@link TensionPoint} per sampled beat, in beat order.
198
+ * Spell a whole melodic line at once, so its notes agree with each other.
199
+ *
200
+ * Every note is spelled together with the rest of the line rather than one at a
201
+ * time: each pitch offers the three spellings a double accidental allows, the
202
+ * chord and the key price them, the intervals between neighbours price the
203
+ * pairs, and the cheapest reading of the whole line wins. Ties fall to the
204
+ * spelling {@link spellPitchClass} would have chosen alone, so the answer never
205
+ * drifts from the theory layer without a reason.
206
+ *
207
+ * Four rules decide, in this order:
208
+ *
209
+ * 1. A note the sounding chord names takes that chord's spelling of it, even
210
+ * when the key spells the pitch otherwise — the seventh of a Db7 reads Cb in
211
+ * C major, not B.
212
+ * 2. Anything else the key's scale spells keeps its key spelling, exactly as
213
+ * the note-by-note path keeps it.
214
+ * 3. A chromatic note is spelled so the written intervals around it match the
215
+ * sounding ones, and then so a rising step carries a sharp and a falling
216
+ * step a flat.
217
+ * 4. Failing all of that, the key's own chromatic spelling stands, and the
218
+ * fewest accidentals win.
219
+ *
220
+ * Without a timeline — pass `null` — rules 2 to 4 still apply in full, so a
221
+ * diatonic line and a chromatic run come out the same. What is lost is rule 1:
222
+ * a chromatic chord can no longer name its own tones, so the tones of a
223
+ * borrowed or altered chord fall back to the key's convention (that Cb becomes
224
+ * B), and an enharmonically ambiguous tone under a chord that would have fixed
225
+ * it is decided by the line's direction instead.
226
+ *
227
+ * @param notes The line, monophonic and in time order. Notes sharing an onset
228
+ * are read as consecutive, so split polyphonic material into voices first.
229
+ * @param timeline The chords sounding under the line, or null when only the key
230
+ * is known. Each note is judged against the chord at its own onset.
231
+ * @param key The key the line is written in, as a key name, a key/scale, or a
232
+ * `Key`.
233
+ * @param opts Optional tonic spelling and work budget.
234
+ * @returns One spelled note per input note, in input order, each carrying the
235
+ * octave that reproduces its pitch.
236
+ * @throws If `opts.tonic` does not sound the key's root pitch class, if a note
237
+ * event is malformed, or if the line is longer than the budget allows.
488
238
  * @example
489
239
  * ```ts
490
- * import { analyzeArrangement, tensionCurveFrom } from '@libraz/libcantus';
491
- * const tracks = [{ notes: [{ pitch: 60, startBeat: 0, durationBeat: 4 }] }];
492
- * const analysis = analyzeArrangement(tracks);
493
- * tensionCurveFrom(tracks, analysis, { step: 1 });
240
+ * import { chordTimelineFromChords, majorKey, noteNames, spellLine } from '@libraz/libcantus';
241
+ * // A chromatic ascent spells with sharps, whatever the key would say alone.
242
+ * const rising = [60, 61, 62, 63, 64].map((pitch, index) => ({
243
+ * pitch,
244
+ * startBeat: index,
245
+ * durationBeat: 1,
246
+ * }));
247
+ * noteNames(spellLine(rising, null, majorKey(0)));
248
+ * // ['C4', 'C#4', 'D4', 'D#4', 'E4']
249
+ * // Under a Db7 the same B sounds as the chord's seventh.
250
+ * const timeline = chordTimelineFromChords([{ rootPc: 1, quality: 'dom7', startBeat: 0 }], 4);
251
+ * noteNames(spellLine([{ pitch: 71, startBeat: 0, durationBeat: 1 }], timeline, majorKey(0)));
252
+ * // ['Cb5']
494
253
  * ```
495
254
  * @category Arrangement & Analysis
496
255
  */
497
- declare function tensionCurveFrom(tracks: ArrangementTrack[], analysis: ArrangementAnalysis, opts?: ArrangementOptions & {
498
- step?: number;
499
- }): TensionPoint[];
256
+ declare function spellLine(notes: readonly NoteEvent[], timeline: ChordTimeline | null, key: KeyLike, opts?: SpellLineOptions): Note[];
500
257
 
501
- export { type AnalyzedNote, type ArrangementAnalysis, type ArrangementOptions, type ArrangementTrack, CadenceHit, Chord as ChordData, ChordTimeline, type ChordToRomanOptions, ChordToneRole, type Conflict, type IdentifiedVoiceNote, KeyScale, NoteEvent, NoteSafety, SafetyProfile, type SuspensionFigure, type TensionPoint, type TheoryLabel, TimeSignature, type TrackAnalysis, type TrackRole, type VoiceNote, VoiceSnapshot, analyzeArrangement, analyzeVoice, chordToRoman, romanToChord, tensionCurve, tensionCurveFrom, toVoiceNotes };
258
+ export { type AugmentedSixthKind, Chord as ChordData, ChordLike, ChordTimeline, KeyLike, Note as NoteData, NoteEvent, NoteLike, type SpellLineOptions, augmentedSixthChord, augmentedSixthFromPitchClasses, augmentedSixthKind, spellAugmentedSixth, spellLine };