@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,272 +0,0 @@
1
- /**
2
- * Recognition: infer a chord name from a set of pitches, or a key from a set of
3
- * pitch classes. This is the inverse direction of the chord/scale builders,
4
- * which only go name -> notes.
5
- */
6
-
7
- import type { KeyScale } from '../../core/types.js';
8
- import { assertFiniteNumber, assertGenerationBudget } from '../../core/validation/index.js';
9
- import type { Chord, ChordQuality } from '../../theory/chord/index.js';
10
- import { chordPitchClasses, chordQualities, makeChord } from '../../theory/chord/index.js';
11
- import {
12
- HARMONIC_MINOR_MASK,
13
- MAJOR_MASK,
14
- MELODIC_MINOR_MASK,
15
- majorKey,
16
- minorKey,
17
- NATURAL_MINOR_MASK,
18
- } from '../../theory/scale/index.js';
19
-
20
- /**
21
- * A candidate chord interpretation of a pitch set.
22
- *
23
- * @category Recognition
24
- */
25
- export type ChordMatch = {
26
- rootPc: number;
27
- quality: ChordQuality;
28
- /** Chord tones absent from the input (an incomplete voicing). */
29
- missingPcs: number[];
30
- /** Input pitch classes not belonging to the chord. */
31
- extraPcs: number[];
32
- /** True when the input pitch-class set equals the chord exactly. */
33
- exact: boolean;
34
- /** Inversion implied by a known bass, or null for an unordered pitch-class set. */
35
- inversion: number | null;
36
- /** Bass pitch class when inverted (the lowest note is not the root). */
37
- bassPc?: number;
38
- };
39
-
40
- /** Input interpretation for {@link detectChord} and {@link detectChordBest}. */
41
- export type DetectChordOptions = {
42
- /**
43
- * `midi` uses the numerically lowest pitch as bass; `pitchClass` treats the
44
- * input as unordered. `auto` (default) selects pitch-class mode only when all
45
- * values lie in 0..11.
46
- */
47
- input?: 'auto' | 'midi' | 'pitchClass';
48
- /** Explicit bass pitch class, including for an unordered pitch-class set. */
49
- bassPc?: number;
50
- };
51
-
52
- /**
53
- * A candidate key interpretation of a pitch-class set.
54
- *
55
- * @category Recognition
56
- */
57
- export type KeyMatch = {
58
- key: KeyScale;
59
- mode: 'major' | 'minor';
60
- /** Fraction of input pitch classes that are in the scale, in [0, 1]. */
61
- fit: number;
62
- };
63
-
64
- /** Reduce any integer to a pitch class in [0, 11]. */
65
- function pitchClass(value: number): number {
66
- return ((Math.trunc(value) % 12) + 12) % 12;
67
- }
68
-
69
- /** Unique pitch classes of the input, sorted ascending. */
70
- function uniquePitchClasses(pitches: number[]): number[] {
71
- return [...new Set(pitches.map(pitchClass))].sort((a, b) => a - b);
72
- }
73
-
74
- function assertPitches(pitches: number[]): void {
75
- assertGenerationBudget(pitches.length, 'detection pitches');
76
- for (let index = 0; index < pitches.length; index += 1) {
77
- assertFiniteNumber(pitches[index] ?? Number.NaN, `pitches[${index}]`);
78
- }
79
- }
80
-
81
- /**
82
- * Identify chords matching a set of pitches.
83
- *
84
- * Every input pitch class is tried as a root against every known chord quality.
85
- * A match is reported when all of the chord's tones are present in the input, or
86
- * when the only absent tone is the perfect fifth (a common omission in shell
87
- * voicings); matches are ranked best-first by fewest extra notes, then fewest
88
- * missing notes, then most specific (largest) chord. An exact match (no extras,
89
- * no missing) is flagged and ranked first.
90
- *
91
- * @param pitches MIDI pitches or bare pitch classes (octave-agnostic).
92
- * @returns Ranked chord interpretations (may be empty).
93
- * @example
94
- * ```ts
95
- * import { detectChord } from '@libraz/libcantus';
96
- * const matches = detectChord([60, 64, 67]); // C E G
97
- * matches[0]; // { rootPc: 0, quality: 'maj', exact: true, ... }
98
- * ```
99
- * @category Recognition
100
- */
101
- export function detectChord(pitches: number[], opts: DetectChordOptions = {}): ChordMatch[] {
102
- assertPitches(pitches);
103
- if (opts.bassPc !== undefined) assertFiniteNumber(opts.bassPc, 'bassPc');
104
- const input = uniquePitchClasses(pitches);
105
- if (input.length === 0) {
106
- return [];
107
- }
108
- const inputKind =
109
- opts.input === undefined || opts.input === 'auto'
110
- ? pitches.every((pitch) => Number.isInteger(pitch) && pitch >= 0 && pitch <= 11)
111
- ? 'pitchClass'
112
- : 'midi'
113
- : opts.input;
114
- const bassPc =
115
- opts.bassPc !== undefined
116
- ? pitchClass(opts.bassPc)
117
- : inputKind === 'midi'
118
- ? pitchClass(pitches.reduce((lowest, pitch) => Math.min(lowest, pitch), Infinity))
119
- : undefined;
120
- const inputSet = new Set(input);
121
- const matches: ChordMatch[] = [];
122
- const qualities = chordQualities();
123
- for (const rootPc of input) {
124
- for (const quality of qualities) {
125
- const chord = makeChord(rootPc, quality);
126
- const tones = chordPitchClasses(chord);
127
- const toneSet = new Set(tones);
128
- const missingPcs = tones.filter((pc) => !inputSet.has(pc));
129
- // Accept an exact-tone match, or one whose only absent tone is the perfect
130
- // fifth; any other missing tone (third, sus tone, seventh, ...) rejects.
131
- const fifthPc = pitchClass(rootPc + 7);
132
- if (missingPcs.length > 1 || (missingPcs.length === 1 && missingPcs[0] !== fifthPc)) {
133
- continue;
134
- }
135
- const extraPcs = input.filter((pc) => !toneSet.has(pc));
136
- const bassIndex =
137
- bassPc === undefined
138
- ? -1
139
- : chord.intervals.findIndex((iv) => pitchClass(rootPc + iv) === bassPc);
140
- const inversion = bassPc === undefined ? null : bassIndex > 0 ? bassIndex : 0;
141
- const match: ChordMatch = {
142
- rootPc,
143
- quality,
144
- missingPcs,
145
- extraPcs,
146
- exact: extraPcs.length === 0 && missingPcs.length === 0,
147
- inversion,
148
- };
149
- if (bassPc !== undefined && bassPc !== rootPc) {
150
- match.bassPc = bassPc;
151
- }
152
- matches.push(match);
153
- }
154
- }
155
- matches.sort((a, b) => {
156
- if (a.extraPcs.length !== b.extraPcs.length) {
157
- return a.extraPcs.length - b.extraPcs.length;
158
- }
159
- if (a.missingPcs.length !== b.missingPcs.length) {
160
- return a.missingPcs.length - b.missingPcs.length;
161
- }
162
- // Prefer root position (the bass is the chord root) on a tie.
163
- if ((a.inversion === 0) !== (b.inversion === 0)) {
164
- return a.inversion === 0 ? -1 : 1;
165
- }
166
- const aSize = chordPitchClasses(makeChord(a.rootPc, a.quality)).length;
167
- const bSize = chordPitchClasses(makeChord(b.rootPc, b.quality)).length;
168
- return bSize - aSize;
169
- });
170
- return matches;
171
- }
172
-
173
- /**
174
- * The single best chord interpretation of a pitch set, or null if none.
175
- *
176
- * @param pitches MIDI pitches or bare pitch classes.
177
- * @returns The top-ranked chord, or null when nothing matches.
178
- * @example
179
- * ```ts
180
- * import { detectChordBest } from '@libraz/libcantus';
181
- * detectChordBest([60, 64, 67]); // C major triad: { rootPc: 0, quality: 'maj', ... }
182
- * ```
183
- * @category Recognition
184
- */
185
- export function detectChordBest(pitches: number[], opts: DetectChordOptions = {}): Chord | null {
186
- const best = detectChord(pitches, opts)[0];
187
- if (!best) {
188
- return null;
189
- }
190
- return makeChord(best.rootPc, best.quality, best.bassPc);
191
- }
192
-
193
- /**
194
- * Minor-scale variants scored for each minor-key candidate. Scoring against all
195
- * three lets the raised sixth and seventh (e.g. the leading tone G# in A minor)
196
- * count toward their own tonic instead of only penalizing it.
197
- */
198
- const MINOR_MASK_VARIANTS = [NATURAL_MINOR_MASK, HARMONIC_MINOR_MASK, MELODIC_MINOR_MASK];
199
-
200
- /**
201
- * Rank major and minor keys by how well they contain a set of pitch classes.
202
- *
203
- * The tonic is weighted so that, among equally-fitting keys, the one whose root
204
- * appears in the input is preferred. Minor candidates are scored against the
205
- * natural, harmonic, and melodic minor variants and keep the best of the three,
206
- * so a minor cadence containing the leading tone still resolves to its own
207
- * tonic; the returned key is always the natural-minor scale. Returns all 24
208
- * keys ranked best-first, or an empty array for an empty input (mirroring
209
- * {@link detectChord}).
210
- *
211
- * @param pitches MIDI pitches or bare pitch classes.
212
- * @returns Ranked key interpretations (empty for an empty input).
213
- * @example
214
- * ```ts
215
- * import { detectKey } from '@libraz/libcantus';
216
- * const keys = detectKey([60, 62, 64, 65, 67, 69, 71]); // C major scale
217
- * keys[0].mode; // 'major', with keys[0].key.rootPc === 0
218
- * ```
219
- * @category Recognition
220
- */
221
- export function detectKey(pitches: number[]): KeyMatch[] {
222
- assertPitches(pitches);
223
- const input = uniquePitchClasses(pitches);
224
- if (input.length === 0) {
225
- return [];
226
- }
227
- const counts = new Map<number, number>();
228
- for (const pc of pitches.map(pitchClass)) {
229
- counts.set(pc, (counts.get(pc) ?? 0) + 1);
230
- }
231
- const total = pitches.length;
232
- const results: (KeyMatch & { score: number })[] = [];
233
- for (let tonic = 0; tonic < 12; tonic += 1) {
234
- for (const mode of ['major', 'minor'] as const) {
235
- const key = mode === 'major' ? majorKey(tonic) : minorKey(tonic);
236
- const masks = mode === 'major' ? [MAJOR_MASK] : MINOR_MASK_VARIANTS;
237
- // Score each scale variant and keep the best; ties keep the earlier
238
- // (more diatonic) variant.
239
- let inScale = 0;
240
- let weighted = Number.NEGATIVE_INFINITY;
241
- for (const mask of masks) {
242
- let variantInScale = 0;
243
- let variantWeighted = 0;
244
- for (const pc of input) {
245
- const offset = (pc - tonic + 12) % 12;
246
- if ((mask >> offset) & 1) {
247
- variantInScale += 1;
248
- }
249
- }
250
- for (const [pc, count] of counts) {
251
- const offset = (pc - tonic + 12) % 12;
252
- if ((mask >> offset) & 1) {
253
- variantWeighted += count;
254
- }
255
- }
256
- if (variantWeighted > weighted) {
257
- weighted = variantWeighted;
258
- inScale = variantInScale;
259
- }
260
- }
261
- weighted += (counts.get(tonic) ?? 0) * 0.5;
262
- results.push({
263
- key,
264
- mode,
265
- fit: inScale / input.length,
266
- score: weighted / total,
267
- });
268
- }
269
- }
270
- results.sort((a, b) => b.score - a.score);
271
- return results.map(({ key, mode, fit }) => ({ key, mode, fit }));
272
- }
@@ -1,84 +0,0 @@
1
- /**
2
- * Borrowed chords (modal interchange) and the Neapolitan.
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 { HARMONIC_MINOR_MASK } from '../../theory/scale/index.js';
12
- import { isDiatonic, isMinorKey, parallelKey } from './function.js';
13
- import { isNeapolitan, mod12 } from './internal.js';
14
-
15
- /**
16
- * The origin of a recognized non-diatonic chord, or null when none applies.
17
- *
18
- * @category Functional Harmony
19
- */
20
- export type BorrowedSource = 'parallel-minor' | 'parallel-major' | 'neapolitan' | null;
21
-
22
- /**
23
- * In a minor key, whether a non-diatonic chord is explained by the harmonic
24
- * minor scale on the same tonic (major V, V7, the raised-leading-tone viio).
25
- * Such chords are in-key chromatic alterations, not modal interchange.
26
- */
27
- function isHarmonicMinorAlteration(chord: Chord, key: KeyScale): boolean {
28
- return (
29
- isMinorKey(key) &&
30
- isDiatonic(chord, { rootPc: mod12(key.rootPc), modeMask12: HARMONIC_MINOR_MASK })
31
- );
32
- }
33
-
34
- /**
35
- * Whether a chord is borrowed from the parallel mode (modal interchange).
36
- *
37
- * True when the chord is not diatonic to `key` but is diatonic to its
38
- * {@link parallelKey} — e.g. iv, bVI, or bVII in a major key, or the Picardy
39
- * tonic and major IV in a minor key. Two non-diatonic families are excluded:
40
- * chords diatonic to neither mode (they are chromatic, not borrowed), and, in
41
- * a minor key, chords explained by the harmonic minor scale (the major
42
- * dominant and raised-leading-tone chords), which are in-key alterations
43
- * rather than interchange even though they happen to fit the parallel major.
44
- *
45
- * @param chord The chord to test.
46
- * @param key The prevailing key.
47
- * @returns True if the chord is borrowed from the parallel mode.
48
- * @category Functional Harmony
49
- */
50
- export function isBorrowedChord(chord: Chord, key: KeyScale): boolean {
51
- if (isDiatonic(chord, key)) {
52
- return false;
53
- }
54
- if (isHarmonicMinorAlteration(chord, key)) {
55
- return false;
56
- }
57
- return isDiatonic(chord, parallelKey(key));
58
- }
59
-
60
- /**
61
- * Identify where a non-diatonic chord comes from.
62
- *
63
- * The Neapolitan (major triad on b2) is recognized first, since it sits
64
- * outside both parallel modes; parallel-mode borrowing follows the
65
- * {@link isBorrowedChord} rules. Diatonic chords, harmonic-minor alterations,
66
- * and unrecognized chromatic chords all yield null.
67
- *
68
- * @param chord The chord to classify.
69
- * @param key The prevailing key.
70
- * @returns The borrowing source, or null.
71
- * @category Functional Harmony
72
- */
73
- export function borrowedSource(chord: Chord, key: KeyScale): BorrowedSource {
74
- if (isDiatonic(chord, key)) {
75
- return null;
76
- }
77
- if (isNeapolitan(chord, key)) {
78
- return 'neapolitan';
79
- }
80
- if (isBorrowedChord(chord, key)) {
81
- return isMinorKey(key) ? 'parallel-major' : 'parallel-minor';
82
- }
83
- return null;
84
- }
@@ -1,66 +0,0 @@
1
- /**
2
- * Cadence detection between two successive chords.
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 { isMinorKey } from './function.js';
12
- import { mod12 } from './internal.js';
13
-
14
- /**
15
- * A recognized cadence type, or null when a chord pair forms none.
16
- *
17
- * @category Functional Harmony
18
- */
19
- export type Cadence = 'authentic' | 'plagal' | 'half' | 'deceptive' | null;
20
-
21
- /**
22
- * Classify the cadence formed by moving from one chord to the next.
23
- *
24
- * - authentic: V (dominant a fifth above the tonic) to I
25
- * - plagal: IV (a fourth above the tonic) to I
26
- * - deceptive: V to the submediant (diatonic vi or borrowed bVI in major, VI in
27
- * minor)
28
- * - half: any chord other than the dominant itself to V
29
- *
30
- * A static V-to-V repeat with no root motion is not a cadence and yields null.
31
- *
32
- * @param from The penultimate chord.
33
- * @param to The final chord.
34
- * @param key The prevailing key.
35
- * @returns The cadence type, or null.
36
- * @example
37
- * ```ts
38
- * import { detectCadence, makeChord, majorKey } from '@libraz/libcantus';
39
- * detectCadence(makeChord(7, 'maj'), makeChord(0, 'maj'), majorKey(0)); // => 'authentic'
40
- * ```
41
- * @category Functional Harmony
42
- */
43
- export function detectCadence(from: Chord, to: Chord, key: KeyScale): Cadence {
44
- const tonic = mod12(key.rootPc);
45
- const fromOffset = mod12(from.rootPc - tonic);
46
- const toOffset = mod12(to.rootPc - tonic);
47
- // Deceptive targets: the diatonic submediant (vi at offset 9 in major, VI at
48
- // offset 8 in minor) plus, in a major key, the borrowed flat-submediant bVI
49
- // at offset 8.
50
- const deceptiveTargets = isMinorKey(key) ? [8] : [8, 9];
51
- if (fromOffset === 7 && toOffset === 0) {
52
- return 'authentic';
53
- }
54
- if (fromOffset === 5 && toOffset === 0) {
55
- return 'plagal';
56
- }
57
- if (fromOffset === 7 && deceptiveTargets.includes(toOffset)) {
58
- return 'deceptive';
59
- }
60
- // A move to the dominant is a half cadence, but a no-root-motion V-to-V repeat
61
- // is not a cadence at all.
62
- if (toOffset === 7 && fromOffset !== 7) {
63
- return 'half';
64
- }
65
- return null;
66
- }
@@ -1,184 +0,0 @@
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
- }
@@ -1,23 +0,0 @@
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';
@@ -1,29 +0,0 @@
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
- }