@libraz/libcantus 0.9.1 → 0.9.3

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 (160) hide show
  1. package/README.md +19 -3
  2. package/dist/analyze/index.cjs +1970 -0
  3. package/dist/analyze/index.cjs.map +1 -0
  4. package/dist/analyze/index.d.cts +341 -0
  5. package/dist/analyze/index.d.ts +341 -0
  6. package/dist/analyze/index.js +51 -0
  7. package/dist/analyze/index.js.map +1 -0
  8. package/dist/chunk-2ZWMSJH6.js +30 -0
  9. package/dist/chunk-2ZWMSJH6.js.map +1 -0
  10. package/dist/chunk-4MHA432G.js +2854 -0
  11. package/dist/chunk-4MHA432G.js.map +1 -0
  12. package/dist/chunk-62QZKCJQ.js +49 -0
  13. package/dist/chunk-62QZKCJQ.js.map +1 -0
  14. package/dist/chunk-AKLYPWIN.js +83 -0
  15. package/dist/chunk-AKLYPWIN.js.map +1 -0
  16. package/dist/chunk-C5XYKUVG.js +904 -0
  17. package/dist/chunk-C5XYKUVG.js.map +1 -0
  18. package/dist/chunk-CHMXELDS.js +549 -0
  19. package/dist/chunk-CHMXELDS.js.map +1 -0
  20. package/dist/chunk-DYWFNC4A.js +152 -0
  21. package/dist/chunk-DYWFNC4A.js.map +1 -0
  22. package/dist/chunk-EH6EM37U.js +315 -0
  23. package/dist/chunk-EH6EM37U.js.map +1 -0
  24. package/dist/chunk-FBQK3ZQN.js +131 -0
  25. package/dist/chunk-FBQK3ZQN.js.map +1 -0
  26. package/dist/chunk-GOAW4VYL.js +719 -0
  27. package/dist/chunk-GOAW4VYL.js.map +1 -0
  28. package/dist/chunk-I5R7ZBJS.js +202 -0
  29. package/dist/chunk-I5R7ZBJS.js.map +1 -0
  30. package/dist/chunk-IT2O4XFE.js +154 -0
  31. package/dist/chunk-IT2O4XFE.js.map +1 -0
  32. package/dist/chunk-MIWO425M.js +412 -0
  33. package/dist/chunk-MIWO425M.js.map +1 -0
  34. package/dist/chunk-N6IHQUOX.js +1 -0
  35. package/dist/chunk-N6IHQUOX.js.map +1 -0
  36. package/dist/chunk-SGU7SPLK.js +924 -0
  37. package/dist/chunk-SGU7SPLK.js.map +1 -0
  38. package/dist/core/index.cjs +648 -0
  39. package/dist/core/index.cjs.map +1 -0
  40. package/dist/core/index.d.cts +262 -0
  41. package/dist/core/index.d.ts +262 -0
  42. package/dist/core/index.js +93 -0
  43. package/dist/core/index.js.map +1 -0
  44. package/dist/function-DVH4Eusr.d.ts +187 -0
  45. package/dist/function-DYt4iKsn.d.cts +187 -0
  46. package/dist/generate/index.cjs +4123 -0
  47. package/dist/generate/index.cjs.map +1 -0
  48. package/dist/generate/index.d.cts +986 -0
  49. package/dist/generate/index.d.ts +986 -0
  50. package/dist/generate/index.js +52 -0
  51. package/dist/generate/index.js.map +1 -0
  52. package/dist/index-BNlGP86A.d.cts +157 -0
  53. package/dist/index-Bg5dYMmZ.d.ts +56 -0
  54. package/dist/index-Bt4tOUD9.d.ts +157 -0
  55. package/dist/index-CGMZNCx6.d.cts +140 -0
  56. package/dist/index-CGMZNCx6.d.ts +140 -0
  57. package/dist/index-Cl7mhOFM.d.cts +164 -0
  58. package/dist/index-Cl7mhOFM.d.ts +164 -0
  59. package/dist/index-CnZCo29u.d.cts +108 -0
  60. package/dist/index-CpOsP1y9.d.cts +151 -0
  61. package/dist/index-CxqBQtH1.d.cts +128 -0
  62. package/dist/index-Dikst44v.d.cts +56 -0
  63. package/dist/index-DxvARIP2.d.ts +151 -0
  64. package/dist/index-Jc0Ryi79.d.ts +128 -0
  65. package/dist/index-JnF4v9bx.d.ts +108 -0
  66. package/dist/index.cjs +3710 -3143
  67. package/dist/index.cjs.map +1 -1
  68. package/dist/index.d.cts +15 -3982
  69. package/dist/index.d.ts +15 -3982
  70. package/dist/index.js +191 -6504
  71. package/dist/index.js.map +1 -1
  72. package/dist/model/index.cjs +2574 -0
  73. package/dist/model/index.cjs.map +1 -0
  74. package/dist/model/index.d.cts +631 -0
  75. package/dist/model/index.d.ts +631 -0
  76. package/dist/model/index.js +21 -0
  77. package/dist/model/index.js.map +1 -0
  78. package/dist/styled-DZ7GjShb.d.ts +282 -0
  79. package/dist/styled-ZTbdaVFM.d.cts +282 -0
  80. package/dist/theory/index.cjs +1823 -0
  81. package/dist/theory/index.cjs.map +1 -0
  82. package/dist/theory/index.d.cts +535 -0
  83. package/dist/theory/index.d.ts +535 -0
  84. package/dist/theory/index.js +150 -0
  85. package/dist/theory/index.js.map +1 -0
  86. package/dist/types-MGP29r3t.d.cts +30 -0
  87. package/dist/types-MGP29r3t.d.ts +30 -0
  88. package/package.json +61 -5
  89. package/src/analyze/arrange/index.ts +21 -0
  90. package/src/analyze/arrange/internal.ts +127 -0
  91. package/src/analyze/arrange/tension.ts +199 -0
  92. package/src/analyze/arrange/tracks.ts +312 -0
  93. package/src/{detect → analyze/detect}/index.ts +50 -13
  94. package/src/analyze/functional/borrowed.ts +84 -0
  95. package/src/analyze/functional/cadence.ts +66 -0
  96. package/src/analyze/functional/function.ts +184 -0
  97. package/src/analyze/functional/index.ts +23 -0
  98. package/src/analyze/functional/internal.ts +29 -0
  99. package/src/{functional/index.ts → analyze/functional/roman.ts} +23 -332
  100. package/src/analyze/index.ts +49 -0
  101. package/src/{timeline → analyze/timeline}/index.ts +50 -18
  102. package/src/{analysis → analyze/voice}/index.ts +19 -7
  103. package/src/core/event-index/index.ts +104 -0
  104. package/src/core/index.ts +61 -0
  105. package/src/{meter → core/meter}/index.ts +24 -4
  106. package/src/{random → core/random}/index.ts +26 -3
  107. package/src/{tuning → core/tuning}/index.ts +36 -6
  108. package/src/core/validation/index.ts +119 -0
  109. package/src/{bass → generate/bass}/index.ts +28 -5
  110. package/src/{countermelody → generate/countermelody}/index.ts +176 -85
  111. package/src/{drums → generate/drums}/euclid.ts +10 -3
  112. package/src/{drums → generate/drums}/index.ts +58 -26
  113. package/src/{drums → generate/drums}/percussion.ts +1 -1
  114. package/src/{drums → generate/drums}/rng.ts +2 -2
  115. package/src/{groove → generate/groove}/index.ts +28 -3
  116. package/src/{harmonize → generate/harmonize}/index.ts +68 -57
  117. package/src/generate/index.ts +58 -0
  118. package/src/{motif → generate/motif}/index.ts +52 -28
  119. package/src/{progression → generate/progression}/index.ts +19 -41
  120. package/src/{reharmony → generate/reharmony}/index.ts +12 -19
  121. package/src/{rhythm → generate/rhythm}/index.ts +13 -3
  122. package/src/index.ts +13 -234
  123. package/src/model/chord.ts +442 -0
  124. package/src/model/index.ts +5 -1096
  125. package/src/model/interval.ts +82 -0
  126. package/src/model/key.ts +266 -0
  127. package/src/model/note.ts +176 -0
  128. package/src/model/progression.ts +161 -0
  129. package/src/model/shared.ts +12 -0
  130. package/src/{chord → theory/chord}/index.ts +23 -3
  131. package/src/{counterpoint → theory/counterpoint}/index.ts +2 -2
  132. package/src/{harmony → theory/harmony}/index.ts +6 -21
  133. package/src/theory/index.ts +112 -0
  134. package/src/{safety → theory/safety}/index.ts +72 -3
  135. package/src/theory/scale/degrees.ts +123 -0
  136. package/src/theory/scale/index.ts +28 -0
  137. package/src/theory/scale/key.ts +56 -0
  138. package/src/theory/scale/masks.ts +153 -0
  139. package/src/{spelling → theory/spelling}/index.ts +73 -6
  140. package/src/{symbol → theory/symbol}/index.ts +8 -1
  141. package/src/theory/voicing/index.ts +5 -0
  142. package/src/theory/voicing/internal.ts +168 -0
  143. package/src/theory/voicing/leading.ts +112 -0
  144. package/src/theory/voicing/satb.ts +216 -0
  145. package/src/theory/voicing/styled.ts +199 -0
  146. package/src/arrange/index.ts +0 -571
  147. package/src/scale/index.ts +0 -331
  148. package/src/voicing/index.ts +0 -623
  149. /package/src/{interval → core/interval}/index.ts +0 -0
  150. /package/src/{pitch → core/pitch}/index.ts +0 -0
  151. /package/src/{types.ts → core/types.ts} +0 -0
  152. /package/src/{drums → generate/drums}/beat.ts +0 -0
  153. /package/src/{drums → generate/drums}/fills.ts +0 -0
  154. /package/src/{drums → generate/drums}/ghost.ts +0 -0
  155. /package/src/{drums → generate/drums}/hihat.ts +0 -0
  156. /package/src/{drums → generate/drums}/hit.ts +0 -0
  157. /package/src/{drums → generate/drums}/internal.ts +0 -0
  158. /package/src/{drums → generate/drums}/kick.ts +0 -0
  159. /package/src/{drums → generate/drums}/swing.ts +0 -0
  160. /package/src/{chordscale → theory/chordscale}/index.ts +0 -0
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Harmonic function (tonic / subdominant / dominant), diatonicity, parallel-key
3
+ * mirroring, and full chord analysis.
4
+ *
5
+ * Roots are pitch classes measured against the key tonic, so borrowed and
6
+ * chromatic chords are handled by their semitone offset rather than requiring a
7
+ * spelled key signature.
8
+ */
9
+
10
+ import type { KeyScale } from '../../core/types.js';
11
+ import type { Chord, ChordQuality } from '../../theory/chord/index.js';
12
+ import { chordPitchClasses, makeChord } from '../../theory/chord/index.js';
13
+ import { isScaleTone, MAJOR_MASK, NATURAL_MINOR_MASK } from '../../theory/scale/index.js';
14
+ import { type BorrowedSource, borrowedSource } from './borrowed.js';
15
+ import { degreeRootPc, isNeapolitan, mod12 } from './internal.js';
16
+ import { chordToRoman } from './roman.js';
17
+
18
+ /**
19
+ * The three broad harmonic functions of tonal music.
20
+ *
21
+ * @category Functional Harmony
22
+ */
23
+ export type HarmonicFunction = 'tonic' | 'subdominant' | 'dominant';
24
+
25
+ /** Harmonic function of each semitone offset above the tonic (major context). */
26
+ const FUNCTION_BY_OFFSET: readonly HarmonicFunction[] = [
27
+ 'tonic', // 0 I
28
+ 'subdominant', // 1 bII (Neapolitan)
29
+ 'subdominant', // 2 ii
30
+ 'tonic', // 3 bIII
31
+ 'tonic', // 4 iii
32
+ 'subdominant', // 5 IV
33
+ 'dominant', // 6 #IV / bV
34
+ 'dominant', // 7 V
35
+ 'subdominant', // 8 bVI
36
+ 'tonic', // 9 vi
37
+ 'subdominant', // 10 bVII
38
+ 'dominant', // 11 vii
39
+ ];
40
+
41
+ /**
42
+ * Whether a key's scale has a minor third and no major third (a minor key).
43
+ *
44
+ * @category Functional Harmony
45
+ */
46
+ export function isMinorKey(key: KeyScale): boolean {
47
+ const hasMinorThird = (key.modeMask12 >> 3) & 1;
48
+ const hasMajorThird = (key.modeMask12 >> 4) & 1;
49
+ return Boolean(hasMinorThird) && !hasMajorThird;
50
+ }
51
+
52
+ /**
53
+ * The harmonic function of a chord in a key, from its root's offset above the
54
+ * tonic. The mapping follows common-practice major-key function and is a useful
55
+ * approximation in minor and for borrowed chords.
56
+ *
57
+ * @param chord The chord.
58
+ * @param key The prevailing key.
59
+ * @returns The harmonic function.
60
+ * @category Functional Harmony
61
+ */
62
+ export function functionOf(chord: Chord, key: KeyScale): HarmonicFunction {
63
+ return FUNCTION_BY_OFFSET[mod12(chord.rootPc - key.rootPc)] ?? 'tonic';
64
+ }
65
+
66
+ /**
67
+ * The result of {@link analyzeChord}: function, borrowing, and Roman numeral.
68
+ *
69
+ * @category Functional Harmony
70
+ */
71
+ export type ChordAnalysis = {
72
+ function: HarmonicFunction;
73
+ borrowed: boolean;
74
+ source: BorrowedSource;
75
+ roman: string;
76
+ };
77
+
78
+ /**
79
+ * Whether every pitch class of a chord belongs to the key's scale.
80
+ *
81
+ * The test is strict against the key's own mode mask: in a natural-minor key
82
+ * the harmonic-minor dominant (major V) is *not* diatonic, since the raised
83
+ * leading tone lies outside the mask. Borrowing predicates treat that case as
84
+ * an in-key alteration separately (see {@link isBorrowedChord}).
85
+ *
86
+ * @param chord The chord to test.
87
+ * @param key The prevailing key.
88
+ * @returns True if all chord pitch classes are scale tones.
89
+ * @category Functional Harmony
90
+ */
91
+ export function isDiatonic(chord: Chord, key: KeyScale): boolean {
92
+ return chordPitchClasses(chord).every((pc) => isScaleTone(pc, key));
93
+ }
94
+
95
+ /**
96
+ * The parallel key: same tonic, opposite mode.
97
+ *
98
+ * A key with a minor third (natural/harmonic/melodic minor, dorian, phrygian)
99
+ * maps to the parallel major; any other key maps to the parallel natural minor.
100
+ *
101
+ * @param key The key to mirror.
102
+ * @returns The parallel major or natural-minor key on the same tonic.
103
+ * @category Functional Harmony
104
+ */
105
+ export function parallelKey(key: KeyScale): KeyScale {
106
+ return {
107
+ rootPc: mod12(key.rootPc),
108
+ modeMask12: isMinorKey(key) ? MAJOR_MASK : NATURAL_MINOR_MASK,
109
+ };
110
+ }
111
+
112
+ /** Diminished-family qualities: diminished triad, dim7, half-diminished. */
113
+ function isDiminishedQuality(quality: ChordQuality): boolean {
114
+ return quality === 'dim' || quality === 'dim7' || quality === 'm7b5';
115
+ }
116
+
117
+ /** Whether the chord's interval template carries a major third above the root. */
118
+ function hasMajorThird(chord: Chord): boolean {
119
+ return chord.intervals.some((interval) => mod12(interval) === 4);
120
+ }
121
+
122
+ /**
123
+ * Quality-aware harmonic function, refining the offset table where chord
124
+ * quality disambiguates: diminished chords on the leading tone or the raised
125
+ * subdominant resolve by semitone and act as dominants; the Neapolitan and the
126
+ * major-third chords on bVI/bVII borrowed from the parallel minor act as
127
+ * subdominant (predominant) harmony. All other chords keep {@link functionOf}.
128
+ */
129
+ function qualityAwareFunction(chord: Chord, key: KeyScale): HarmonicFunction {
130
+ const offset = mod12(chord.rootPc - key.rootPc);
131
+ if (isDiminishedQuality(chord.quality) && (offset === 11 || offset === 6)) {
132
+ return 'dominant';
133
+ }
134
+ if (isNeapolitan(chord, key)) {
135
+ return 'subdominant';
136
+ }
137
+ if (!isMinorKey(key) && hasMajorThird(chord) && (offset === 8 || offset === 10)) {
138
+ return 'subdominant';
139
+ }
140
+ return functionOf(chord, key);
141
+ }
142
+
143
+ /**
144
+ * Analyze a chord in a key: harmonic function, borrowing, and Roman numeral.
145
+ *
146
+ * The function is quality-aware (see the predicates behind it), the source
147
+ * follows {@link borrowedSource}, and the numeral comes from
148
+ * {@link chordToRoman}. `borrowed` is true whenever a source is identified —
149
+ * including the Neapolitan, which the stricter parallel-mode predicate
150
+ * {@link isBorrowedChord} does not count.
151
+ *
152
+ * @param chord The chord to analyze.
153
+ * @param key The prevailing key.
154
+ * @returns The chord analysis.
155
+ * @example
156
+ * ```ts
157
+ * import { analyzeChord, makeChord, majorKey } from '@libraz/libcantus';
158
+ * analyzeChord(makeChord(7, 'dom7'), majorKey(0));
159
+ * // { function: 'dominant', borrowed: false, source: null, roman: 'V7' }
160
+ * ```
161
+ * @category Functional Harmony
162
+ */
163
+ export function analyzeChord(chord: Chord, key: KeyScale): ChordAnalysis {
164
+ const source = borrowedSource(chord, key);
165
+ return {
166
+ function: qualityAwareFunction(chord, key),
167
+ borrowed: source !== null,
168
+ source,
169
+ roman: chordToRoman(chord, key),
170
+ };
171
+ }
172
+
173
+ /**
174
+ * The secondary dominant (V7) that tonicizes a scale degree.
175
+ *
176
+ * @param targetDegree 0-based scale degree to tonicize.
177
+ * @param key The prevailing key.
178
+ * @returns A dominant-seventh chord a fifth above the target's root.
179
+ * @category Functional Harmony
180
+ */
181
+ export function secondaryDominant(targetDegree: number, key: KeyScale): Chord {
182
+ const targetRoot = degreeRootPc(targetDegree + 1, key);
183
+ return makeChord(mod12(targetRoot + 7), 'dom7');
184
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Functional harmony: Roman-numeral parsing and formatting, harmonic function
3
+ * (tonic / subdominant / dominant), cadence detection, and borrowed chords.
4
+ *
5
+ * Roots are pitch classes measured against the key tonic, so borrowed and
6
+ * chromatic chords are handled by their semitone offset rather than requiring a
7
+ * spelled key signature.
8
+ */
9
+
10
+ export type { BorrowedSource } from './borrowed.js';
11
+ export { borrowedSource, isBorrowedChord } from './borrowed.js';
12
+ export type { Cadence } from './cadence.js';
13
+ export { detectCadence } from './cadence.js';
14
+ export type { ChordAnalysis, HarmonicFunction } from './function.js';
15
+ export {
16
+ analyzeChord,
17
+ functionOf,
18
+ isDiatonic,
19
+ isMinorKey,
20
+ parallelKey,
21
+ secondaryDominant,
22
+ } from './function.js';
23
+ export { chordToRoman, romanToChord } from './roman.js';
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Shared helpers for the functional-harmony modules.
3
+ *
4
+ * Roots are pitch classes measured against the key tonic, so borrowed and
5
+ * chromatic chords are handled by their semitone offset rather than requiring a
6
+ * spelled key signature.
7
+ */
8
+
9
+ import type { KeyScale } from '../../core/types.js';
10
+ import type { Chord } from '../../theory/chord/index.js';
11
+ import { scaleTonesInDegreeOrder } from '../../theory/scale/index.js';
12
+
13
+ export function mod12(n: number): number {
14
+ return ((n % 12) + 12) % 12;
15
+ }
16
+
17
+ /** Diatonic pitch class of a 1-based scale degree in a key. */
18
+ export function degreeRootPc(degreeNumber: number, key: KeyScale): number {
19
+ const tones = scaleTonesInDegreeOrder(key);
20
+ if (tones.length === 0) {
21
+ return mod12(key.rootPc);
22
+ }
23
+ return tones[(degreeNumber - 1) % tones.length] ?? mod12(key.rootPc);
24
+ }
25
+
26
+ /** Whether a chord is the Neapolitan: a major triad on the flat second degree. */
27
+ export function isNeapolitan(chord: Chord, key: KeyScale): boolean {
28
+ return mod12(chord.rootPc - key.rootPc) === 1 && chord.quality === 'maj';
29
+ }
@@ -1,37 +1,16 @@
1
1
  /**
2
- * Functional harmony: Roman-numeral parsing and formatting, harmonic function
3
- * (tonic / subdominant / dominant), and cadence detection.
2
+ * Roman-numeral parsing and formatting.
4
3
  *
5
- * Roots are pitch classes measured against the key tonic, so borrowed and
6
- * chromatic chords are handled by their semitone offset rather than requiring a
7
- * spelled key signature.
4
+ * The {@link ROMAN_STYLE} table is the single source of truth for both
5
+ * directions, so {@link chordToRoman} and {@link romanToChord} stay mutual
6
+ * inverses by construction.
8
7
  */
9
8
 
10
- import type { Chord, ChordQuality } from '../chord/index.js';
11
- import { chordPitchClasses, makeChord } from '../chord/index.js';
12
- import {
13
- HARMONIC_MINOR_MASK,
14
- isScaleTone,
15
- MAJOR_MASK,
16
- majorKey,
17
- NATURAL_MINOR_MASK,
18
- scaleTonesInDegreeOrder,
19
- } from '../scale/index.js';
20
- import type { KeyScale } from '../types.js';
21
-
22
- /**
23
- * The three broad harmonic functions of tonal music.
24
- *
25
- * @category Functional Harmony
26
- */
27
- export type HarmonicFunction = 'tonic' | 'subdominant' | 'dominant';
28
-
29
- /**
30
- * A recognized cadence type, or null when a chord pair forms none.
31
- *
32
- * @category Functional Harmony
33
- */
34
- export type Cadence = 'authentic' | 'plagal' | 'half' | 'deceptive' | null;
9
+ import type { KeyScale } from '../../core/types.js';
10
+ import type { Chord, ChordQuality } from '../../theory/chord/index.js';
11
+ import { makeChord } from '../../theory/chord/index.js';
12
+ import { majorKey, scaleTonesInDegreeOrder } from '../../theory/scale/index.js';
13
+ import { degreeRootPc, mod12 } from './internal.js';
35
14
 
36
15
  /** Roman numeral glyphs indexed by degree number - 1. */
37
16
  const ROMAN = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII'] as const;
@@ -63,46 +42,6 @@ const OFFSET_SPELLING: readonly [number, string][] = [
63
42
  [7, ''],
64
43
  ];
65
44
 
66
- /** Harmonic function of each semitone offset above the tonic (major context). */
67
- const FUNCTION_BY_OFFSET: readonly HarmonicFunction[] = [
68
- 'tonic', // 0 I
69
- 'subdominant', // 1 bII (Neapolitan)
70
- 'subdominant', // 2 ii
71
- 'tonic', // 3 bIII
72
- 'tonic', // 4 iii
73
- 'subdominant', // 5 IV
74
- 'dominant', // 6 #IV / bV
75
- 'dominant', // 7 V
76
- 'subdominant', // 8 bVI
77
- 'tonic', // 9 vi
78
- 'subdominant', // 10 bVII
79
- 'dominant', // 11 vii
80
- ];
81
-
82
- function mod12(n: number): number {
83
- return ((n % 12) + 12) % 12;
84
- }
85
-
86
- /**
87
- * Whether a key's scale has a minor third and no major third (a minor key).
88
- *
89
- * @category Functional Harmony
90
- */
91
- export function isMinorKey(key: KeyScale): boolean {
92
- const hasMinorThird = (key.modeMask12 >> 3) & 1;
93
- const hasMajorThird = (key.modeMask12 >> 4) & 1;
94
- return Boolean(hasMinorThird) && !hasMajorThird;
95
- }
96
-
97
- /** Diatonic pitch class of a 1-based scale degree in a key. */
98
- function degreeRootPc(degreeNumber: number, key: KeyScale): number {
99
- const tones = scaleTonesInDegreeOrder(key);
100
- if (tones.length === 0) {
101
- return mod12(key.rootPc);
102
- }
103
- return tones[(degreeNumber - 1) % tones.length] ?? mod12(key.rootPc);
104
- }
105
-
106
45
  /** Choose a seventh-chord quality from a triad label and the numeral case. */
107
46
  function seventhQuality(base: 'maj' | 'min' | 'dim' | 'aug', halfDim: boolean): ChordQuality {
108
47
  if (halfDim) {
@@ -222,11 +161,20 @@ function parseSimpleRoman(
222
161
  return { rootPc, quality: canonical, inversion: 0 };
223
162
  }
224
163
 
225
- const isDim = /o|°|dim/.test(suffix);
226
- const isAug = /\+|aug/.test(suffix);
227
- const isHalfDim = /ø/.test(suffix);
228
- const explicitMaj7 = /maj7|M7/.test(suffix);
229
- const figures = suffix.replace(/maj7|M7/g, '').replace(/[^0-9]/g, '');
164
+ // Non-canonical spellings are limited to a complete quality marker followed
165
+ // by an optional major-seventh marker and a recognized figured bass. Never
166
+ // search substrings: `Vfoo` must not become diminished merely because it
167
+ // contains an "o", and unknown trailing text must be rejected in full.
168
+ const parsedSuffix = /^(o|°|dim|ø|\+|aug)?(maj7|M7)?(65|64|43|42|7|6|2)?$/.exec(suffix);
169
+ if (!parsedSuffix) {
170
+ throw new Error(`Unsupported suffix "${suffix}" in Roman numeral: ${text}`);
171
+ }
172
+ const qualityMarker = parsedSuffix[1] ?? '';
173
+ const isDim = qualityMarker === 'o' || qualityMarker === '°' || qualityMarker === 'dim';
174
+ const isAug = qualityMarker === '+' || qualityMarker === 'aug';
175
+ const isHalfDim = qualityMarker === 'ø';
176
+ const explicitMaj7 = parsedSuffix[2] !== undefined;
177
+ const figures = parsedSuffix[3] ?? '';
230
178
 
231
179
  // Reject figure strings that are neither a known figured-bass inversion nor a
232
180
  // canonical quality suffix rather than silently degrading to a root triad.
@@ -452,260 +400,3 @@ export function chordToRoman(chord: Chord, key: KeyScale): string {
452
400
  }
453
401
  return `${accidental}${cased}${suffix}`;
454
402
  }
455
-
456
- /**
457
- * The harmonic function of a chord in a key, from its root's offset above the
458
- * tonic. The mapping follows common-practice major-key function and is a useful
459
- * approximation in minor and for borrowed chords.
460
- *
461
- * @param chord The chord.
462
- * @param key The prevailing key.
463
- * @returns The harmonic function.
464
- * @category Functional Harmony
465
- */
466
- export function functionOf(chord: Chord, key: KeyScale): HarmonicFunction {
467
- return FUNCTION_BY_OFFSET[mod12(chord.rootPc - key.rootPc)] ?? 'tonic';
468
- }
469
-
470
- /**
471
- * The origin of a recognized non-diatonic chord, or null when none applies.
472
- *
473
- * @category Functional Harmony
474
- */
475
- export type BorrowedSource = 'parallel-minor' | 'parallel-major' | 'neapolitan' | null;
476
-
477
- /**
478
- * The result of {@link analyzeChord}: function, borrowing, and Roman numeral.
479
- *
480
- * @category Functional Harmony
481
- */
482
- export type ChordAnalysis = {
483
- function: HarmonicFunction;
484
- borrowed: boolean;
485
- source: BorrowedSource;
486
- roman: string;
487
- };
488
-
489
- /**
490
- * Whether every pitch class of a chord belongs to the key's scale.
491
- *
492
- * The test is strict against the key's own mode mask: in a natural-minor key
493
- * the harmonic-minor dominant (major V) is *not* diatonic, since the raised
494
- * leading tone lies outside the mask. Borrowing predicates treat that case as
495
- * an in-key alteration separately (see {@link isBorrowedChord}).
496
- *
497
- * @param chord The chord to test.
498
- * @param key The prevailing key.
499
- * @returns True if all chord pitch classes are scale tones.
500
- * @category Functional Harmony
501
- */
502
- export function isDiatonic(chord: Chord, key: KeyScale): boolean {
503
- return chordPitchClasses(chord).every((pc) => isScaleTone(pc, key));
504
- }
505
-
506
- /**
507
- * The parallel key: same tonic, opposite mode.
508
- *
509
- * A key with a minor third (natural/harmonic/melodic minor, dorian, phrygian)
510
- * maps to the parallel major; any other key maps to the parallel natural minor.
511
- *
512
- * @param key The key to mirror.
513
- * @returns The parallel major or natural-minor key on the same tonic.
514
- * @category Functional Harmony
515
- */
516
- export function parallelKey(key: KeyScale): KeyScale {
517
- return {
518
- rootPc: mod12(key.rootPc),
519
- modeMask12: isMinorKey(key) ? MAJOR_MASK : NATURAL_MINOR_MASK,
520
- };
521
- }
522
-
523
- /**
524
- * In a minor key, whether a non-diatonic chord is explained by the harmonic
525
- * minor scale on the same tonic (major V, V7, the raised-leading-tone viio).
526
- * Such chords are in-key chromatic alterations, not modal interchange.
527
- */
528
- function isHarmonicMinorAlteration(chord: Chord, key: KeyScale): boolean {
529
- return (
530
- isMinorKey(key) &&
531
- isDiatonic(chord, { rootPc: mod12(key.rootPc), modeMask12: HARMONIC_MINOR_MASK })
532
- );
533
- }
534
-
535
- /** Whether a chord is the Neapolitan: a major triad on the flat second degree. */
536
- function isNeapolitan(chord: Chord, key: KeyScale): boolean {
537
- return mod12(chord.rootPc - key.rootPc) === 1 && chord.quality === 'maj';
538
- }
539
-
540
- /**
541
- * Whether a chord is borrowed from the parallel mode (modal interchange).
542
- *
543
- * True when the chord is not diatonic to `key` but is diatonic to its
544
- * {@link parallelKey} — e.g. iv, bVI, or bVII in a major key, or the Picardy
545
- * tonic and major IV in a minor key. Two non-diatonic families are excluded:
546
- * chords diatonic to neither mode (they are chromatic, not borrowed), and, in
547
- * a minor key, chords explained by the harmonic minor scale (the major
548
- * dominant and raised-leading-tone chords), which are in-key alterations
549
- * rather than interchange even though they happen to fit the parallel major.
550
- *
551
- * @param chord The chord to test.
552
- * @param key The prevailing key.
553
- * @returns True if the chord is borrowed from the parallel mode.
554
- * @category Functional Harmony
555
- */
556
- export function isBorrowedChord(chord: Chord, key: KeyScale): boolean {
557
- if (isDiatonic(chord, key)) {
558
- return false;
559
- }
560
- if (isHarmonicMinorAlteration(chord, key)) {
561
- return false;
562
- }
563
- return isDiatonic(chord, parallelKey(key));
564
- }
565
-
566
- /**
567
- * Identify where a non-diatonic chord comes from.
568
- *
569
- * The Neapolitan (major triad on b2) is recognized first, since it sits
570
- * outside both parallel modes; parallel-mode borrowing follows the
571
- * {@link isBorrowedChord} rules. Diatonic chords, harmonic-minor alterations,
572
- * and unrecognized chromatic chords all yield null.
573
- *
574
- * @param chord The chord to classify.
575
- * @param key The prevailing key.
576
- * @returns The borrowing source, or null.
577
- * @category Functional Harmony
578
- */
579
- export function borrowedSource(chord: Chord, key: KeyScale): BorrowedSource {
580
- if (isDiatonic(chord, key)) {
581
- return null;
582
- }
583
- if (isNeapolitan(chord, key)) {
584
- return 'neapolitan';
585
- }
586
- if (isBorrowedChord(chord, key)) {
587
- return isMinorKey(key) ? 'parallel-major' : 'parallel-minor';
588
- }
589
- return null;
590
- }
591
-
592
- /** Diminished-family qualities: diminished triad, dim7, half-diminished. */
593
- function isDiminishedQuality(quality: ChordQuality): boolean {
594
- return quality === 'dim' || quality === 'dim7' || quality === 'm7b5';
595
- }
596
-
597
- /** Whether the chord's interval template carries a major third above the root. */
598
- function hasMajorThird(chord: Chord): boolean {
599
- return chord.intervals.some((interval) => mod12(interval) === 4);
600
- }
601
-
602
- /**
603
- * Quality-aware harmonic function, refining the offset table where chord
604
- * quality disambiguates: diminished chords on the leading tone or the raised
605
- * subdominant resolve by semitone and act as dominants; the Neapolitan and the
606
- * major-third chords on bVI/bVII borrowed from the parallel minor act as
607
- * subdominant (predominant) harmony. All other chords keep {@link functionOf}.
608
- */
609
- function qualityAwareFunction(chord: Chord, key: KeyScale): HarmonicFunction {
610
- const offset = mod12(chord.rootPc - key.rootPc);
611
- if (isDiminishedQuality(chord.quality) && (offset === 11 || offset === 6)) {
612
- return 'dominant';
613
- }
614
- if (isNeapolitan(chord, key)) {
615
- return 'subdominant';
616
- }
617
- if (!isMinorKey(key) && hasMajorThird(chord) && (offset === 8 || offset === 10)) {
618
- return 'subdominant';
619
- }
620
- return functionOf(chord, key);
621
- }
622
-
623
- /**
624
- * Analyze a chord in a key: harmonic function, borrowing, and Roman numeral.
625
- *
626
- * The function is quality-aware (see the predicates behind it), the source
627
- * follows {@link borrowedSource}, and the numeral comes from
628
- * {@link chordToRoman}. `borrowed` is true whenever a source is identified —
629
- * including the Neapolitan, which the stricter parallel-mode predicate
630
- * {@link isBorrowedChord} does not count.
631
- *
632
- * @param chord The chord to analyze.
633
- * @param key The prevailing key.
634
- * @returns The chord analysis.
635
- * @example
636
- * ```ts
637
- * import { analyzeChord, makeChord, majorKey } from '@libraz/libcantus';
638
- * analyzeChord(makeChord(7, 'dom7'), majorKey(0));
639
- * // { function: 'dominant', borrowed: false, source: null, roman: 'V7' }
640
- * ```
641
- * @category Functional Harmony
642
- */
643
- export function analyzeChord(chord: Chord, key: KeyScale): ChordAnalysis {
644
- const source = borrowedSource(chord, key);
645
- return {
646
- function: qualityAwareFunction(chord, key),
647
- borrowed: source !== null,
648
- source,
649
- roman: chordToRoman(chord, key),
650
- };
651
- }
652
-
653
- /**
654
- * Classify the cadence formed by moving from one chord to the next.
655
- *
656
- * - authentic: V (dominant a fifth above the tonic) to I
657
- * - plagal: IV (a fourth above the tonic) to I
658
- * - deceptive: V to the submediant (diatonic vi or borrowed bVI in major, VI in
659
- * minor)
660
- * - half: any chord other than the dominant itself to V
661
- *
662
- * A static V-to-V repeat with no root motion is not a cadence and yields null.
663
- *
664
- * @param from The penultimate chord.
665
- * @param to The final chord.
666
- * @param key The prevailing key.
667
- * @returns The cadence type, or null.
668
- * @example
669
- * ```ts
670
- * import { detectCadence, makeChord, majorKey } from '@libraz/libcantus';
671
- * detectCadence(makeChord(7, 'maj'), makeChord(0, 'maj'), majorKey(0)); // => 'authentic'
672
- * ```
673
- * @category Functional Harmony
674
- */
675
- export function detectCadence(from: Chord, to: Chord, key: KeyScale): Cadence {
676
- const tonic = mod12(key.rootPc);
677
- const fromOffset = mod12(from.rootPc - tonic);
678
- const toOffset = mod12(to.rootPc - tonic);
679
- // Deceptive targets: the diatonic submediant (vi at offset 9 in major, VI at
680
- // offset 8 in minor) plus, in a major key, the borrowed flat-submediant bVI
681
- // at offset 8.
682
- const deceptiveTargets = isMinorKey(key) ? [8] : [8, 9];
683
- if (fromOffset === 7 && toOffset === 0) {
684
- return 'authentic';
685
- }
686
- if (fromOffset === 5 && toOffset === 0) {
687
- return 'plagal';
688
- }
689
- if (fromOffset === 7 && deceptiveTargets.includes(toOffset)) {
690
- return 'deceptive';
691
- }
692
- // A move to the dominant is a half cadence, but a no-root-motion V-to-V repeat
693
- // is not a cadence at all.
694
- if (toOffset === 7 && fromOffset !== 7) {
695
- return 'half';
696
- }
697
- return null;
698
- }
699
-
700
- /**
701
- * The secondary dominant (V7) that tonicizes a scale degree.
702
- *
703
- * @param targetDegree 0-based scale degree to tonicize.
704
- * @param key The prevailing key.
705
- * @returns A dominant-seventh chord a fifth above the target's root.
706
- * @category Functional Harmony
707
- */
708
- export function secondaryDominant(targetDegree: number, key: KeyScale): Chord {
709
- const targetRoot = degreeRootPc(targetDegree + 1, key);
710
- return makeChord(mod12(targetRoot + 7), 'dom7');
711
- }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Public API of the analyze layer. Re-exports every analyze-layer module;
3
+ * also available from the package root.
4
+ */
5
+ export type {
6
+ ArrangementAnalysis,
7
+ ArrangementOptions,
8
+ ArrangementTrack,
9
+ Conflict,
10
+ TensionPoint,
11
+ TrackAnalysis,
12
+ TrackRole,
13
+ } from './arrange/index.js';
14
+ export { analyzeArrangement, tensionCurve } from './arrange/index.js';
15
+ export type { ChordMatch, DetectChordOptions, KeyMatch } from './detect/index.js';
16
+ export { detectChord, detectChordBest, detectKey } from './detect/index.js';
17
+ export type {
18
+ BorrowedSource,
19
+ Cadence,
20
+ ChordAnalysis,
21
+ HarmonicFunction,
22
+ } from './functional/index.js';
23
+ export {
24
+ analyzeChord,
25
+ borrowedSource,
26
+ chordToRoman,
27
+ detectCadence,
28
+ functionOf,
29
+ isBorrowedChord,
30
+ isDiatonic,
31
+ isMinorKey,
32
+ parallelKey,
33
+ romanToChord,
34
+ secondaryDominant,
35
+ } from './functional/index.js';
36
+ export type {
37
+ CadenceHit,
38
+ ChordSegment,
39
+ ChordTimeline,
40
+ ChordTimelineOptions,
41
+ ChordTimelineResult,
42
+ } from './timeline/index.js';
43
+ export {
44
+ chordTimelineFromChords,
45
+ chordTimelineFromNotes,
46
+ detectCadences,
47
+ } from './timeline/index.js';
48
+ export type { AnalyzedNote, TheoryLabel, VoiceNote } from './voice/index.js';
49
+ export { analyzeVoice } from './voice/index.js';