@libraz/libcantus 0.9.3 → 0.9.5

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 (201) hide show
  1. package/README.md +29 -20
  2. package/README_ja.md +268 -0
  3. package/dist/analyze/index.cjs +39 -1947
  4. package/dist/analyze/index.cjs.map +1 -1
  5. package/dist/analyze/index.d.cts +161 -91
  6. package/dist/analyze/index.d.ts +161 -91
  7. package/dist/analyze/index.js +19 -8
  8. package/dist/chunk-2A2QMA74.cjs +261 -0
  9. package/dist/chunk-2A2QMA74.cjs.map +1 -0
  10. package/dist/chunk-2PTFGVD7.js +261 -0
  11. package/dist/chunk-2PTFGVD7.js.map +1 -0
  12. package/dist/chunk-357ZDPVU.cjs +162 -0
  13. package/dist/chunk-357ZDPVU.cjs.map +1 -0
  14. package/dist/chunk-3KX76RLG.js +1548 -0
  15. package/dist/chunk-3KX76RLG.js.map +1 -0
  16. package/dist/chunk-4VZ5MZAM.cjs +889 -0
  17. package/dist/chunk-4VZ5MZAM.cjs.map +1 -0
  18. package/dist/{chunk-C5XYKUVG.js → chunk-7EONYVI5.js} +471 -171
  19. package/dist/chunk-7EONYVI5.js.map +1 -0
  20. package/dist/{chunk-IT2O4XFE.js → chunk-7JEP3MFY.js} +42 -34
  21. package/dist/chunk-7JEP3MFY.js.map +1 -0
  22. package/dist/{chunk-4MHA432G.js → chunk-AID75RN3.js} +572 -250
  23. package/dist/chunk-AID75RN3.js.map +1 -0
  24. package/dist/chunk-BSLTLGM3.cjs +497 -0
  25. package/dist/chunk-BSLTLGM3.cjs.map +1 -0
  26. package/dist/chunk-CU6VG2YM.cjs +109 -0
  27. package/dist/chunk-CU6VG2YM.cjs.map +1 -0
  28. package/dist/{chunk-EH6EM37U.js → chunk-DZMHIGI4.js} +81 -59
  29. package/dist/chunk-DZMHIGI4.js.map +1 -0
  30. package/dist/{chunk-AKLYPWIN.js → chunk-EDOTEABK.js} +45 -19
  31. package/dist/chunk-EDOTEABK.js.map +1 -0
  32. package/dist/{chunk-2ZWMSJH6.js → chunk-HN2OEUN2.js} +7 -4
  33. package/dist/chunk-HN2OEUN2.js.map +1 -0
  34. package/dist/{chunk-MIWO425M.js → chunk-KBTTEX7Q.js} +128 -43
  35. package/dist/chunk-KBTTEX7Q.js.map +1 -0
  36. package/dist/chunk-KQN2ZYND.cjs +724 -0
  37. package/dist/chunk-KQN2ZYND.cjs.map +1 -0
  38. package/dist/chunk-MPTVJIVG.cjs +3176 -0
  39. package/dist/chunk-MPTVJIVG.cjs.map +1 -0
  40. package/dist/chunk-MTMZTMWM.cjs +53 -0
  41. package/dist/chunk-MTMZTMWM.cjs.map +1 -0
  42. package/dist/{chunk-CHMXELDS.js → chunk-NNB67XA2.js} +372 -197
  43. package/dist/chunk-NNB67XA2.js.map +1 -0
  44. package/dist/chunk-OL3IHHSJ.cjs +1204 -0
  45. package/dist/chunk-OL3IHHSJ.cjs.map +1 -0
  46. package/dist/chunk-PI3UXRWK.cjs +528 -0
  47. package/dist/chunk-PI3UXRWK.cjs.map +1 -0
  48. package/dist/chunk-R2VVFAIN.js +528 -0
  49. package/dist/chunk-R2VVFAIN.js.map +1 -0
  50. package/dist/chunk-T64C7ZD7.cjs +1 -0
  51. package/dist/chunk-T64C7ZD7.cjs.map +1 -0
  52. package/dist/chunk-WFDXOLUB.cjs +337 -0
  53. package/dist/chunk-WFDXOLUB.cjs.map +1 -0
  54. package/dist/chunk-WTAN4ACG.cjs +1548 -0
  55. package/dist/chunk-WTAN4ACG.cjs.map +1 -0
  56. package/dist/chunk-YFJXS3AU.cjs +33 -0
  57. package/dist/chunk-YFJXS3AU.cjs.map +1 -0
  58. package/dist/{chunk-GOAW4VYL.js → chunk-YUE6TJ4M.js} +315 -145
  59. package/dist/chunk-YUE6TJ4M.js.map +1 -0
  60. package/dist/{chunk-62QZKCJQ.js → chunk-ZGLJM4A4.js} +10 -6
  61. package/dist/chunk-ZGLJM4A4.js.map +1 -0
  62. package/dist/core/index.cjs +140 -647
  63. package/dist/core/index.cjs.map +1 -1
  64. package/dist/core/index.d.cts +313 -41
  65. package/dist/core/index.d.ts +313 -41
  66. package/dist/core/index.js +64 -16
  67. package/dist/{function-DVH4Eusr.d.ts → function-CxixTB02.d.cts} +122 -10
  68. package/dist/{function-DYt4iKsn.d.cts → function-DTCt675z.d.ts} +122 -10
  69. package/dist/generate/index.cjs +28 -4085
  70. package/dist/generate/index.cjs.map +1 -1
  71. package/dist/generate/index.d.cts +374 -66
  72. package/dist/generate/index.d.ts +374 -66
  73. package/dist/generate/index.js +23 -9
  74. package/dist/{index-Jc0Ryi79.d.ts → index-B2Ulxxpo.d.cts} +21 -5
  75. package/dist/{index-CpOsP1y9.d.cts → index-BkYJ0xMq.d.cts} +18 -17
  76. package/dist/{index-CxqBQtH1.d.cts → index-BuaB-ypt.d.ts} +21 -5
  77. package/dist/index-CAN7v-kw.d.ts +199 -0
  78. package/dist/{index-Bt4tOUD9.d.ts → index-COw1Ky9g.d.ts} +93 -8
  79. package/dist/{index-Dikst44v.d.cts → index-Cfx8xgH0.d.ts} +14 -11
  80. package/dist/{index-DxvARIP2.d.ts → index-DRtIhlJj.d.ts} +18 -17
  81. package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
  82. package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
  83. package/dist/index-alrRninU.d.cts +199 -0
  84. package/dist/{index-BNlGP86A.d.cts → index-oTQbYYIs.d.cts} +93 -8
  85. package/dist/index-tR9jTn26.d.cts +275 -0
  86. package/dist/index-tR9jTn26.d.ts +275 -0
  87. package/dist/{index-Bg5dYMmZ.d.ts → index-x2vcsduE.d.cts} +14 -11
  88. package/dist/index.cjs +429 -7383
  89. package/dist/index.cjs.map +1 -1
  90. package/dist/index.d.cts +15 -15
  91. package/dist/index.d.ts +15 -15
  92. package/dist/index.js +121 -27
  93. package/dist/model/index.cjs +8 -2562
  94. package/dist/model/index.cjs.map +1 -1
  95. package/dist/model/index.d.cts +471 -81
  96. package/dist/model/index.d.ts +471 -81
  97. package/dist/model/index.js +6 -7
  98. package/dist/styled-Bl0ZK28h.d.ts +543 -0
  99. package/dist/styled-Crv6SA4S.d.cts +543 -0
  100. package/dist/theory/index.cjs +172 -1822
  101. package/dist/theory/index.cjs.map +1 -1
  102. package/dist/theory/index.d.cts +151 -233
  103. package/dist/theory/index.d.ts +151 -233
  104. package/dist/theory/index.js +30 -7
  105. package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
  106. package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
  107. package/package.json +34 -13
  108. package/dist/chunk-2ZWMSJH6.js.map +0 -1
  109. package/dist/chunk-4MHA432G.js.map +0 -1
  110. package/dist/chunk-62QZKCJQ.js.map +0 -1
  111. package/dist/chunk-AKLYPWIN.js.map +0 -1
  112. package/dist/chunk-C5XYKUVG.js.map +0 -1
  113. package/dist/chunk-CHMXELDS.js.map +0 -1
  114. package/dist/chunk-DYWFNC4A.js +0 -152
  115. package/dist/chunk-DYWFNC4A.js.map +0 -1
  116. package/dist/chunk-EH6EM37U.js.map +0 -1
  117. package/dist/chunk-FBQK3ZQN.js +0 -131
  118. package/dist/chunk-FBQK3ZQN.js.map +0 -1
  119. package/dist/chunk-GOAW4VYL.js.map +0 -1
  120. package/dist/chunk-I5R7ZBJS.js +0 -202
  121. package/dist/chunk-I5R7ZBJS.js.map +0 -1
  122. package/dist/chunk-IT2O4XFE.js.map +0 -1
  123. package/dist/chunk-MIWO425M.js.map +0 -1
  124. package/dist/chunk-SGU7SPLK.js +0 -924
  125. package/dist/chunk-SGU7SPLK.js.map +0 -1
  126. package/dist/index-CGMZNCx6.d.cts +0 -140
  127. package/dist/index-CGMZNCx6.d.ts +0 -140
  128. package/dist/index-CnZCo29u.d.cts +0 -108
  129. package/dist/index-JnF4v9bx.d.ts +0 -108
  130. package/dist/styled-DZ7GjShb.d.ts +0 -282
  131. package/dist/styled-ZTbdaVFM.d.cts +0 -282
  132. package/src/analyze/arrange/index.ts +0 -21
  133. package/src/analyze/arrange/internal.ts +0 -127
  134. package/src/analyze/arrange/tension.ts +0 -199
  135. package/src/analyze/arrange/tracks.ts +0 -312
  136. package/src/analyze/detect/index.ts +0 -272
  137. package/src/analyze/functional/borrowed.ts +0 -84
  138. package/src/analyze/functional/cadence.ts +0 -66
  139. package/src/analyze/functional/function.ts +0 -184
  140. package/src/analyze/functional/index.ts +0 -23
  141. package/src/analyze/functional/internal.ts +0 -29
  142. package/src/analyze/functional/roman.ts +0 -402
  143. package/src/analyze/index.ts +0 -49
  144. package/src/analyze/timeline/index.ts +0 -451
  145. package/src/analyze/voice/index.ts +0 -339
  146. package/src/core/event-index/index.ts +0 -104
  147. package/src/core/index.ts +0 -61
  148. package/src/core/interval/index.ts +0 -81
  149. package/src/core/meter/index.ts +0 -302
  150. package/src/core/pitch/index.ts +0 -311
  151. package/src/core/random/index.ts +0 -76
  152. package/src/core/tuning/index.ts +0 -186
  153. package/src/core/types.ts +0 -29
  154. package/src/core/validation/index.ts +0 -119
  155. package/src/generate/bass/index.ts +0 -373
  156. package/src/generate/countermelody/index.ts +0 -470
  157. package/src/generate/drums/beat.ts +0 -266
  158. package/src/generate/drums/euclid.ts +0 -65
  159. package/src/generate/drums/fills.ts +0 -356
  160. package/src/generate/drums/ghost.ts +0 -119
  161. package/src/generate/drums/hihat.ts +0 -285
  162. package/src/generate/drums/hit.ts +0 -31
  163. package/src/generate/drums/index.ts +0 -361
  164. package/src/generate/drums/internal.ts +0 -257
  165. package/src/generate/drums/kick.ts +0 -160
  166. package/src/generate/drums/percussion.ts +0 -121
  167. package/src/generate/drums/rng.ts +0 -7
  168. package/src/generate/drums/swing.ts +0 -71
  169. package/src/generate/groove/index.ts +0 -351
  170. package/src/generate/harmonize/index.ts +0 -451
  171. package/src/generate/index.ts +0 -58
  172. package/src/generate/motif/index.ts +0 -422
  173. package/src/generate/progression/index.ts +0 -362
  174. package/src/generate/reharmony/index.ts +0 -323
  175. package/src/generate/rhythm/index.ts +0 -175
  176. package/src/index.ts +0 -13
  177. package/src/model/chord.ts +0 -442
  178. package/src/model/index.ts +0 -15
  179. package/src/model/interval.ts +0 -82
  180. package/src/model/key.ts +0 -266
  181. package/src/model/note.ts +0 -176
  182. package/src/model/progression.ts +0 -161
  183. package/src/model/shared.ts +0 -12
  184. package/src/theory/chord/index.ts +0 -375
  185. package/src/theory/chordscale/index.ts +0 -374
  186. package/src/theory/counterpoint/index.ts +0 -230
  187. package/src/theory/harmony/index.ts +0 -77
  188. package/src/theory/index.ts +0 -112
  189. package/src/theory/safety/index.ts +0 -488
  190. package/src/theory/scale/degrees.ts +0 -123
  191. package/src/theory/scale/index.ts +0 -28
  192. package/src/theory/scale/key.ts +0 -56
  193. package/src/theory/scale/masks.ts +0 -153
  194. package/src/theory/spelling/index.ts +0 -249
  195. package/src/theory/symbol/index.ts +0 -268
  196. package/src/theory/voicing/index.ts +0 -5
  197. package/src/theory/voicing/internal.ts +0 -168
  198. package/src/theory/voicing/leading.ts +0 -112
  199. package/src/theory/voicing/satb.ts +0 -216
  200. package/src/theory/voicing/styled.ts +0 -199
  201. package/tsconfig.json +0 -23
@@ -1,402 +0,0 @@
1
- /**
2
- * Roman-numeral parsing and formatting.
3
- *
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.
7
- */
8
-
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';
14
-
15
- /** Roman numeral glyphs indexed by degree number - 1. */
16
- const ROMAN = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII'] as const;
17
-
18
- /** Roman glyph -> degree number (1..7). */
19
- const ROMAN_TO_DEGREE: Record<string, number> = {
20
- I: 1,
21
- II: 2,
22
- III: 3,
23
- IV: 4,
24
- V: 5,
25
- VI: 6,
26
- VII: 7,
27
- };
28
-
29
- /** Major-key Roman spelling for each semitone offset above the tonic. */
30
- const OFFSET_SPELLING: readonly [number, string][] = [
31
- [1, ''],
32
- [2, 'b'],
33
- [2, ''],
34
- [3, 'b'],
35
- [3, ''],
36
- [4, ''],
37
- [4, '#'],
38
- [5, ''],
39
- [6, 'b'],
40
- [6, ''],
41
- [7, 'b'],
42
- [7, ''],
43
- ];
44
-
45
- /** Choose a seventh-chord quality from a triad label and the numeral case. */
46
- function seventhQuality(base: 'maj' | 'min' | 'dim' | 'aug', halfDim: boolean): ChordQuality {
47
- if (halfDim) {
48
- return 'm7b5';
49
- }
50
- if (base === 'dim') {
51
- return 'dim7';
52
- }
53
- if (base === 'aug') {
54
- return 'aug7';
55
- }
56
- return base === 'maj' ? 'dom7' : 'min7';
57
- }
58
-
59
- /** Figure strings that denote a recognized figured-bass inversion. */
60
- const INVERSION_FIGURES = new Set(['6', '64', '65', '43', '42', '2', '7']);
61
-
62
- /**
63
- * Canonical Roman-numeral rendering (numeral case and quality suffix) for every
64
- * supported chord quality. The table is the single source of truth for both
65
- * directions: {@link chordToRoman} renders from it and {@link romanToChord}
66
- * recognizes its exact case-sensitive suffixes, so the two stay mutual
67
- * inverses by construction. Suffixes are chosen to never collide with
68
- * figured-bass inversion digits (`6`, `64`, `65`, `43`, `42`, `2`) — the added
69
- * sixth is `add6`, the six-nine chord is `69` — and each (case, suffix) pair is
70
- * unique.
71
- */
72
- const ROMAN_STYLE: Record<ChordQuality, { lower: boolean; suffix: string }> = {
73
- maj: { lower: false, suffix: '' },
74
- min: { lower: true, suffix: '' },
75
- dim: { lower: true, suffix: 'o' },
76
- aug: { lower: false, suffix: '+' },
77
- maj7: { lower: false, suffix: 'maj7' },
78
- min7: { lower: true, suffix: '7' },
79
- dom7: { lower: false, suffix: '7' },
80
- dim7: { lower: true, suffix: 'o7' },
81
- m7b5: { lower: true, suffix: 'ø7' },
82
- minMaj7: { lower: true, suffix: 'maj7' },
83
- aug7: { lower: false, suffix: '+7' },
84
- augMaj7: { lower: false, suffix: '+maj7' },
85
- majb5: { lower: false, suffix: 'b5' },
86
- '6': { lower: false, suffix: 'add6' },
87
- min6: { lower: true, suffix: 'add6' },
88
- '6/9': { lower: false, suffix: '69' },
89
- sus2: { lower: false, suffix: 'sus2' },
90
- sus4: { lower: false, suffix: 'sus4' },
91
- add9: { lower: false, suffix: 'add9' },
92
- add11: { lower: false, suffix: 'add11' },
93
- maj9: { lower: false, suffix: 'maj9' },
94
- min9: { lower: true, suffix: '9' },
95
- dom9: { lower: false, suffix: '9' },
96
- '7b9': { lower: false, suffix: '7b9' },
97
- '7#9': { lower: false, suffix: '7#9' },
98
- '7#11': { lower: false, suffix: '7#11' },
99
- '7b13': { lower: false, suffix: '7b13' },
100
- '11': { lower: false, suffix: '11' },
101
- '13': { lower: false, suffix: '13' },
102
- '5': { lower: false, suffix: '5' },
103
- };
104
-
105
- /** Reverse lookup of {@link ROMAN_STYLE}: `(case, exact suffix) -> quality`. */
106
- const SUFFIX_QUALITY: ReadonlyMap<string, ChordQuality> = new Map(
107
- (Object.entries(ROMAN_STYLE) as [ChordQuality, { lower: boolean; suffix: string }][]).map(
108
- ([quality, style]) => [`${style.lower ? 'l' : 'u'}:${style.suffix}`, quality],
109
- ),
110
- );
111
-
112
- /** Read figured-bass digits into a chord inversion and whether a seventh is implied. */
113
- function parseInversion(figures: string): { inversion: number; seventh: boolean } {
114
- if (figures === '65') {
115
- return { inversion: 1, seventh: true };
116
- }
117
- if (figures === '43') {
118
- return { inversion: 2, seventh: true };
119
- }
120
- if (figures === '42' || figures === '2') {
121
- return { inversion: 3, seventh: true };
122
- }
123
- if (figures === '7') {
124
- return { inversion: 0, seventh: true };
125
- }
126
- if (figures === '64') {
127
- return { inversion: 2, seventh: false };
128
- }
129
- if (figures === '6') {
130
- return { inversion: 1, seventh: false };
131
- }
132
- return { inversion: 0, seventh: false };
133
- }
134
-
135
- /** Parse a Roman numeral (no secondary '/') into a root, quality, and inversion. */
136
- function parseSimpleRoman(
137
- text: string,
138
- key: KeyScale,
139
- ): { rootPc: number; quality: ChordQuality; inversion: number } {
140
- const match = /^([b#]?)([iIvV]+)(.*)$/.exec(text.trim());
141
- if (!match) {
142
- throw new Error(`Invalid Roman numeral: ${text}`);
143
- }
144
- const accidental = match[1] === 'b' ? -1 : match[1] === '#' ? 1 : 0;
145
- const glyph = (match[2] ?? '').toUpperCase();
146
- const degreeNumber = ROMAN_TO_DEGREE[glyph];
147
- if (degreeNumber === undefined) {
148
- throw new Error(`Invalid Roman numeral: ${text}`);
149
- }
150
- const isUpper = (match[2] ?? '')[0] === (match[2] ?? '')[0]?.toUpperCase();
151
- const suffix = match[3] ?? '';
152
- const rootPc = mod12(degreeRootPc(degreeNumber, key) + accidental);
153
-
154
- // Canonical quality suffixes (exact match, case-sensitive on the numeral)
155
- // come first so every chordToRoman rendering re-parses to the same quality:
156
- // `V9` -> dom9 but `ii9` -> min9 and `Imaj9` -> maj9, and added-tone suffixes
157
- // whose digits would otherwise be misread as figured bass (`Iadd6`, `I69`,
158
- // `Isus2`, `Iadd11`) resolve to their qualities in root position.
159
- const canonical = SUFFIX_QUALITY.get(`${isUpper ? 'u' : 'l'}:${suffix}`);
160
- if (canonical !== undefined) {
161
- return { rootPc, quality: canonical, inversion: 0 };
162
- }
163
-
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] ?? '';
178
-
179
- // Reject figure strings that are neither a known figured-bass inversion nor a
180
- // canonical quality suffix rather than silently degrading to a root triad.
181
- if (figures !== '' && !INVERSION_FIGURES.has(figures)) {
182
- throw new Error(`Unsupported figured-bass or extension "${figures}" in Roman numeral: ${text}`);
183
- }
184
-
185
- const { inversion, seventh } = parseInversion(figures);
186
- // The ø glyph is the half-diminished seventh by definition (there is no
187
- // half-diminished triad), so it always implies a seventh.
188
- const hasSeventh = explicitMaj7 || seventh || isHalfDim;
189
-
190
- let base: 'maj' | 'min' | 'dim' | 'aug';
191
- if (isDim || isHalfDim) {
192
- base = 'dim';
193
- } else if (isAug) {
194
- base = 'aug';
195
- } else {
196
- base = isUpper ? 'maj' : 'min';
197
- }
198
-
199
- let quality: ChordQuality;
200
- if (explicitMaj7) {
201
- quality = base === 'min' ? 'minMaj7' : base === 'aug' ? 'augMaj7' : 'maj7';
202
- } else if (hasSeventh) {
203
- quality = seventhQuality(base, isHalfDim);
204
- } else if (base === 'dim') {
205
- quality = 'dim';
206
- } else if (base === 'aug') {
207
- quality = 'aug';
208
- } else {
209
- quality = base;
210
- }
211
- return { rootPc, quality, inversion };
212
- }
213
-
214
- /** Build a chord from a parsed Roman numeral, attaching a bass for inversions. */
215
- function chordFromParsed(parsed: {
216
- rootPc: number;
217
- quality: ChordQuality;
218
- inversion: number;
219
- }): Chord {
220
- const chord = makeChord(parsed.rootPc, parsed.quality);
221
- if (parsed.inversion > 0 && parsed.inversion < chord.intervals.length) {
222
- chord.bassPc = mod12(parsed.rootPc + (chord.intervals[parsed.inversion] ?? 0));
223
- }
224
- return chord;
225
- }
226
-
227
- /**
228
- * Build the chord denoted by a Roman numeral in a key.
229
- *
230
- * Supports accidentals (`bVII`, `#iv`), case-based triad quality, the `o`/`ø`/`+`
231
- * suffixes, sevenths (`V7`, `viio7`, `Imaj7`), figured-bass inversions
232
- * (`V6`, `V64`, `V65`, `V43`, `V42`), and applied/secondary chords via a slash
233
- * (`V7/V`, `viio/ii`). The target after the slash is read as a scale degree
234
- * whose root becomes a local major tonic for the applied chord. Inverted chords
235
- * carry a `bassPc`.
236
- *
237
- * The `ø` glyph always denotes the half-diminished seventh (`iiø` == `iiø7` ==
238
- * `m7b5`), since no half-diminished triad exists. Extension and added-tone
239
- * suffixes are the canonical case-sensitive forms emitted by
240
- * {@link chordToRoman} and honor the numeral case: `V9` -> `dom9`, `ii9` ->
241
- * `min9`, `Imaj9` -> `maj9`, `V11` -> `11`, `V13` -> `13`, `Iadd6`/`iadd6` ->
242
- * `6`/`min6`, `I69` -> `6/9`, `Isus2`/`Isus4`, `Iadd9`/`Iadd11`, `Ib5` ->
243
- * `majb5`, `I5` -> `5`, and the altered dominants `V7b9`/`V7#9`/`V7#11`/`V7b13`.
244
- * A figure string that is neither a recognized inversion nor a supported
245
- * quality suffix throws rather than silently degrading to a triad.
246
- *
247
- * @param text The Roman numeral.
248
- * @param key The prevailing key.
249
- * @returns The chord.
250
- * @example
251
- * ```ts
252
- * import { romanToChord, majorKey } from '@libraz/libcantus';
253
- * romanToChord('V7', majorKey(0)); // G7 in C major: { rootPc: 7, quality: 'dom7', ... }
254
- * ```
255
- * @category Functional Harmony
256
- */
257
- export function romanToChord(text: string, key: KeyScale): Chord {
258
- const trimmed = text.trim();
259
- const slash = trimmed.indexOf('/');
260
- if (slash >= 0) {
261
- const applied = trimmed.slice(0, slash);
262
- const target = trimmed.slice(slash + 1);
263
- const targetRoot = parseSimpleRoman(target, key).rootPc;
264
- const localKey = majorKey(targetRoot);
265
- return chordFromParsed(parseSimpleRoman(applied, localKey));
266
- }
267
- return chordFromParsed(parseSimpleRoman(trimmed, key));
268
- }
269
-
270
- /** Case and suffix for rendering a chord quality as a Roman numeral. */
271
- function romanStyle(quality: ChordQuality): { lower: boolean; suffix: string } {
272
- return ROMAN_STYLE[quality];
273
- }
274
-
275
- /** Choose the degree number and accidental to spell a root as a Roman numeral. */
276
- function romanSpelling(
277
- rootPc: number,
278
- key: KeyScale,
279
- ): { degreeNumber: number; accidental: string } {
280
- const tones = scaleTonesInDegreeOrder(key);
281
- const diatonic = tones.indexOf(mod12(rootPc));
282
- if (diatonic >= 0) {
283
- return { degreeNumber: diatonic + 1, accidental: '' };
284
- }
285
- // Chromatic root: prefer a flat of the diatonic degree a semitone above it,
286
- // then a sharp of the degree a semitone below. Degree 1 (the tonic) and
287
- // degree 5 (the dominant) are skipped in the flat pass, so a raised leading
288
- // tone spells as `#vii` rather than a flat tonic `bI`, and the tritone above
289
- // the tonic spells as `#iv` rather than a flat fifth `bV` (matching the
290
- // conventional `OFFSET_SPELLING` for offset 6).
291
- for (let i = 0; i < tones.length; i += 1) {
292
- const degreeNumber = i + 1;
293
- if (degreeNumber === 1 || degreeNumber === 5) {
294
- continue;
295
- }
296
- if (mod12((tones[i] ?? 0) - 1) === mod12(rootPc)) {
297
- return { degreeNumber, accidental: 'b' };
298
- }
299
- }
300
- for (let i = 0; i < tones.length; i += 1) {
301
- if (mod12((tones[i] ?? 0) + 1) === mod12(rootPc)) {
302
- return { degreeNumber: i + 1, accidental: '#' };
303
- }
304
- }
305
- const [degreeNumber, accidental] = OFFSET_SPELLING[mod12(rootPc - key.rootPc)] ?? [1, ''];
306
- return { degreeNumber, accidental };
307
- }
308
-
309
- /** Quality marker (without the seventh digit) used when a figure carries the 7. */
310
- function baseMarker(quality: ChordQuality): string {
311
- if (quality === 'dim' || quality === 'dim7') {
312
- return 'o';
313
- }
314
- if (quality === 'm7b5') {
315
- return 'ø';
316
- }
317
- if (quality === 'aug' || quality === 'aug7') {
318
- return '+';
319
- }
320
- if (quality === 'augMaj7') {
321
- return '+maj7';
322
- }
323
- if (quality === 'maj7' || quality === 'minMaj7') {
324
- return 'maj7';
325
- }
326
- return '';
327
- }
328
-
329
- const TRIAD_FIGURES: Record<number, string> = { 1: '6', 2: '64' };
330
- const SEVENTH_FIGURES: Record<number, string> = { 1: '65', 2: '43', 3: '42' };
331
-
332
- /** Qualities whose inversions render as lossless triad figures (6, 64). */
333
- const TRIAD_FIGURE_QUALITIES: ReadonlySet<ChordQuality> = new Set(['maj', 'min', 'dim', 'aug']);
334
-
335
- /**
336
- * Qualities carrying a chordal seventh, whose inversions render as seventh
337
- * figures (65, 43, 42). Membership is decided by the quality itself — a 7th
338
- * above the root (10 or 11 semitones, or the diminished 7th of `dim7`) — never
339
- * by interval count, which would turn added-tone chords into false sevenths.
340
- */
341
- const SEVENTH_FIGURE_QUALITIES: ReadonlySet<ChordQuality> = new Set([
342
- 'maj7',
343
- 'min7',
344
- 'dom7',
345
- 'dim7',
346
- 'm7b5',
347
- 'minMaj7',
348
- 'aug7',
349
- 'augMaj7',
350
- ]);
351
-
352
- /**
353
- * Render a chord as a Roman numeral relative to a key.
354
- *
355
- * Diatonic roots take their scale-degree numeral directly, so numerals are
356
- * correct in both major and minor keys (and any custom scale). The quality
357
- * selects the case and suffix; chromatic roots receive a flat/sharp spelling by
358
- * convention. When the chord carries a `bassPc` on a chord tone, a figured-bass
359
- * inversion (`6`, `64`, `65`, `43`, `42`) is emitted for plain triads and true
360
- * seventh chords; added-tone and extended qualities have no lossless figure and
361
- * render in root position instead (the bass is dropped, pitch classes are
362
- * preserved).
363
- *
364
- * @param chord The chord to name.
365
- * @param key The prevailing key.
366
- * @returns The Roman numeral string.
367
- * @example
368
- * ```ts
369
- * import { chordToRoman, makeChord, majorKey } from '@libraz/libcantus';
370
- * chordToRoman(makeChord(7, 'dom7'), majorKey(0)); // => 'V7' (G7 in C major)
371
- * ```
372
- * @category Functional Harmony
373
- */
374
- export function chordToRoman(chord: Chord, key: KeyScale): string {
375
- const { degreeNumber, accidental } = romanSpelling(chord.rootPc, key);
376
- const { lower, suffix } = romanStyle(chord.quality);
377
- const numeral: string = ROMAN[degreeNumber - 1] ?? 'I';
378
- const cased = lower ? numeral.toLowerCase() : numeral;
379
-
380
- let inversion = 0;
381
- if (chord.bassPc !== undefined) {
382
- const idx = chord.intervals.findIndex((iv) => mod12(chord.rootPc + iv) === chord.bassPc);
383
- if (idx > 0) {
384
- inversion = idx;
385
- }
386
- }
387
- if (inversion > 0) {
388
- const figure = SEVENTH_FIGURE_QUALITIES.has(chord.quality)
389
- ? SEVENTH_FIGURES[inversion]
390
- : TRIAD_FIGURE_QUALITIES.has(chord.quality)
391
- ? TRIAD_FIGURES[inversion]
392
- : undefined;
393
- if (figure !== undefined) {
394
- return `${accidental}${cased}${baseMarker(chord.quality)}${figure}`;
395
- }
396
- // No lossless figured-bass symbol exists — the quality is an added-tone or
397
- // extended chord, or the bass falls on a tension beyond the seventh — so
398
- // fall back to root-position rendering with the quality suffix rather than
399
- // emitting a figure that would re-parse as a different chord.
400
- }
401
- return `${accidental}${cased}${suffix}`;
402
- }
@@ -1,49 +0,0 @@
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';