@libraz/libcantus 0.9.3 → 0.9.4

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 (198) hide show
  1. package/README.md +2 -2
  2. package/dist/analyze/index.cjs +39 -1947
  3. package/dist/analyze/index.cjs.map +1 -1
  4. package/dist/analyze/index.d.cts +197 -37
  5. package/dist/analyze/index.d.ts +197 -37
  6. package/dist/analyze/index.js +19 -8
  7. package/dist/chunk-36N73KZ3.cjs +246 -0
  8. package/dist/chunk-36N73KZ3.cjs.map +1 -0
  9. package/dist/chunk-4HLLZPXT.cjs +3077 -0
  10. package/dist/chunk-4HLLZPXT.cjs.map +1 -0
  11. package/dist/chunk-7MGZ7D62.cjs +498 -0
  12. package/dist/chunk-7MGZ7D62.cjs.map +1 -0
  13. package/dist/{chunk-4MHA432G.js → chunk-7NKDPF74.js} +403 -180
  14. package/dist/chunk-7NKDPF74.js.map +1 -0
  15. package/dist/chunk-CAN76NCU.cjs +679 -0
  16. package/dist/chunk-CAN76NCU.cjs.map +1 -0
  17. package/dist/{chunk-MIWO425M.js → chunk-D2FAJBYL.js} +101 -34
  18. package/dist/chunk-D2FAJBYL.js.map +1 -0
  19. package/dist/{chunk-SGU7SPLK.js → chunk-EHVEBWM5.js} +1056 -531
  20. package/dist/chunk-EHVEBWM5.js.map +1 -0
  21. package/dist/chunk-FCANXBAT.cjs +30 -0
  22. package/dist/chunk-FCANXBAT.cjs.map +1 -0
  23. package/dist/chunk-FNDWXRRI.cjs +479 -0
  24. package/dist/chunk-FNDWXRRI.cjs.map +1 -0
  25. package/dist/{chunk-IT2O4XFE.js → chunk-GQSGS2N4.js} +43 -24
  26. package/dist/chunk-GQSGS2N4.js.map +1 -0
  27. package/dist/{chunk-62QZKCJQ.js → chunk-HKGYCQ34.js} +10 -6
  28. package/dist/chunk-HKGYCQ34.js.map +1 -0
  29. package/dist/{chunk-AKLYPWIN.js → chunk-I24MV65U.js} +29 -6
  30. package/dist/chunk-I24MV65U.js.map +1 -0
  31. package/dist/{chunk-CHMXELDS.js → chunk-KMHOICIY.js} +311 -181
  32. package/dist/chunk-KMHOICIY.js.map +1 -0
  33. package/dist/chunk-LW53AHQU.cjs +1117 -0
  34. package/dist/chunk-LW53AHQU.cjs.map +1 -0
  35. package/dist/{chunk-2ZWMSJH6.js → chunk-NKIBQ6PT.js} +2 -2
  36. package/dist/chunk-NKIBQ6PT.js.map +1 -0
  37. package/dist/chunk-NP4NA2FB.cjs +106 -0
  38. package/dist/chunk-NP4NA2FB.cjs.map +1 -0
  39. package/dist/chunk-NVE34YGZ.cjs +1449 -0
  40. package/dist/chunk-NVE34YGZ.cjs.map +1 -0
  41. package/dist/{chunk-EH6EM37U.js → chunk-NYYWMV2C.js} +61 -49
  42. package/dist/chunk-NYYWMV2C.js.map +1 -0
  43. package/dist/{chunk-GOAW4VYL.js → chunk-Q2XUP227.js} +243 -117
  44. package/dist/chunk-Q2XUP227.js.map +1 -0
  45. package/dist/chunk-Q6OP4U43.cjs +327 -0
  46. package/dist/chunk-Q6OP4U43.cjs.map +1 -0
  47. package/dist/chunk-RC6GBMCD.cjs +845 -0
  48. package/dist/chunk-RC6GBMCD.cjs.map +1 -0
  49. package/dist/{chunk-C5XYKUVG.js → chunk-RK35RIN7.js} +370 -157
  50. package/dist/chunk-RK35RIN7.js.map +1 -0
  51. package/dist/chunk-RUMU3UA6.cjs +53 -0
  52. package/dist/chunk-RUMU3UA6.cjs.map +1 -0
  53. package/dist/chunk-T64C7ZD7.cjs +1 -0
  54. package/dist/chunk-T64C7ZD7.cjs.map +1 -0
  55. package/dist/chunk-XEBG3G53.js +498 -0
  56. package/dist/chunk-XEBG3G53.js.map +1 -0
  57. package/dist/{chunk-I5R7ZBJS.js → chunk-YLEMGEFH.js} +103 -59
  58. package/dist/chunk-YLEMGEFH.js.map +1 -0
  59. package/dist/chunk-ZEJR4TKL.cjs +173 -0
  60. package/dist/chunk-ZEJR4TKL.cjs.map +1 -0
  61. package/dist/core/index.cjs +130 -647
  62. package/dist/core/index.cjs.map +1 -1
  63. package/dist/core/index.d.cts +301 -39
  64. package/dist/core/index.d.ts +301 -39
  65. package/dist/core/index.js +54 -16
  66. package/dist/{function-DYt4iKsn.d.cts → function-BtiZb9i9.d.ts} +28 -7
  67. package/dist/{function-DVH4Eusr.d.ts → function-DNugAOrt.d.cts} +28 -7
  68. package/dist/generate/index.cjs +28 -4085
  69. package/dist/generate/index.cjs.map +1 -1
  70. package/dist/generate/index.d.cts +370 -62
  71. package/dist/generate/index.d.ts +370 -62
  72. package/dist/generate/index.js +23 -9
  73. package/dist/{index-CxqBQtH1.d.cts → index-6VR8-kit.d.ts} +19 -4
  74. package/dist/index-70p168VM.d.ts +192 -0
  75. package/dist/{index-CpOsP1y9.d.cts → index-Be74mcon.d.cts} +18 -17
  76. package/dist/index-BtXT6UQ0.d.cts +192 -0
  77. package/dist/{index-DxvARIP2.d.ts → index-C5UBbQ4G.d.ts} +18 -17
  78. package/dist/{index-Bt4tOUD9.d.ts → index-C83mZI1P.d.ts} +93 -8
  79. package/dist/{index-Bg5dYMmZ.d.ts → index-D6b6na69.d.cts} +14 -11
  80. package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
  81. package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
  82. package/dist/index-OG65ocOr.d.cts +272 -0
  83. package/dist/index-OG65ocOr.d.ts +272 -0
  84. package/dist/{index-Jc0Ryi79.d.ts → index-tJhkmQgE.d.cts} +19 -4
  85. package/dist/{index-Dikst44v.d.cts → index-tSQh-xwY.d.ts} +14 -11
  86. package/dist/{index-BNlGP86A.d.cts → index-y_ak9_Hi.d.cts} +93 -8
  87. package/dist/index.cjs +419 -7383
  88. package/dist/index.cjs.map +1 -1
  89. package/dist/index.d.cts +14 -14
  90. package/dist/index.d.ts +14 -14
  91. package/dist/index.js +111 -27
  92. package/dist/model/index.cjs +8 -2562
  93. package/dist/model/index.cjs.map +1 -1
  94. package/dist/model/index.d.cts +433 -75
  95. package/dist/model/index.d.ts +433 -75
  96. package/dist/model/index.js +6 -7
  97. package/dist/styled-DIRkk9Uz.d.ts +506 -0
  98. package/dist/styled-DJbkJQfj.d.cts +506 -0
  99. package/dist/theory/index.cjs +172 -1822
  100. package/dist/theory/index.cjs.map +1 -1
  101. package/dist/theory/index.d.cts +151 -233
  102. package/dist/theory/index.d.ts +151 -233
  103. package/dist/theory/index.js +30 -7
  104. package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
  105. package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
  106. package/package.json +24 -6
  107. package/dist/chunk-2ZWMSJH6.js.map +0 -1
  108. package/dist/chunk-4MHA432G.js.map +0 -1
  109. package/dist/chunk-62QZKCJQ.js.map +0 -1
  110. package/dist/chunk-AKLYPWIN.js.map +0 -1
  111. package/dist/chunk-C5XYKUVG.js.map +0 -1
  112. package/dist/chunk-CHMXELDS.js.map +0 -1
  113. package/dist/chunk-DYWFNC4A.js +0 -152
  114. package/dist/chunk-DYWFNC4A.js.map +0 -1
  115. package/dist/chunk-EH6EM37U.js.map +0 -1
  116. package/dist/chunk-FBQK3ZQN.js +0 -131
  117. package/dist/chunk-FBQK3ZQN.js.map +0 -1
  118. package/dist/chunk-GOAW4VYL.js.map +0 -1
  119. package/dist/chunk-I5R7ZBJS.js.map +0 -1
  120. package/dist/chunk-IT2O4XFE.js.map +0 -1
  121. package/dist/chunk-MIWO425M.js.map +0 -1
  122. package/dist/chunk-SGU7SPLK.js.map +0 -1
  123. package/dist/index-CGMZNCx6.d.cts +0 -140
  124. package/dist/index-CGMZNCx6.d.ts +0 -140
  125. package/dist/index-CnZCo29u.d.cts +0 -108
  126. package/dist/index-JnF4v9bx.d.ts +0 -108
  127. package/dist/styled-DZ7GjShb.d.ts +0 -282
  128. package/dist/styled-ZTbdaVFM.d.cts +0 -282
  129. package/src/analyze/arrange/index.ts +0 -21
  130. package/src/analyze/arrange/internal.ts +0 -127
  131. package/src/analyze/arrange/tension.ts +0 -199
  132. package/src/analyze/arrange/tracks.ts +0 -312
  133. package/src/analyze/detect/index.ts +0 -272
  134. package/src/analyze/functional/borrowed.ts +0 -84
  135. package/src/analyze/functional/cadence.ts +0 -66
  136. package/src/analyze/functional/function.ts +0 -184
  137. package/src/analyze/functional/index.ts +0 -23
  138. package/src/analyze/functional/internal.ts +0 -29
  139. package/src/analyze/functional/roman.ts +0 -402
  140. package/src/analyze/index.ts +0 -49
  141. package/src/analyze/timeline/index.ts +0 -451
  142. package/src/analyze/voice/index.ts +0 -339
  143. package/src/core/event-index/index.ts +0 -104
  144. package/src/core/index.ts +0 -61
  145. package/src/core/interval/index.ts +0 -81
  146. package/src/core/meter/index.ts +0 -302
  147. package/src/core/pitch/index.ts +0 -311
  148. package/src/core/random/index.ts +0 -76
  149. package/src/core/tuning/index.ts +0 -186
  150. package/src/core/types.ts +0 -29
  151. package/src/core/validation/index.ts +0 -119
  152. package/src/generate/bass/index.ts +0 -373
  153. package/src/generate/countermelody/index.ts +0 -470
  154. package/src/generate/drums/beat.ts +0 -266
  155. package/src/generate/drums/euclid.ts +0 -65
  156. package/src/generate/drums/fills.ts +0 -356
  157. package/src/generate/drums/ghost.ts +0 -119
  158. package/src/generate/drums/hihat.ts +0 -285
  159. package/src/generate/drums/hit.ts +0 -31
  160. package/src/generate/drums/index.ts +0 -361
  161. package/src/generate/drums/internal.ts +0 -257
  162. package/src/generate/drums/kick.ts +0 -160
  163. package/src/generate/drums/percussion.ts +0 -121
  164. package/src/generate/drums/rng.ts +0 -7
  165. package/src/generate/drums/swing.ts +0 -71
  166. package/src/generate/groove/index.ts +0 -351
  167. package/src/generate/harmonize/index.ts +0 -451
  168. package/src/generate/index.ts +0 -58
  169. package/src/generate/motif/index.ts +0 -422
  170. package/src/generate/progression/index.ts +0 -362
  171. package/src/generate/reharmony/index.ts +0 -323
  172. package/src/generate/rhythm/index.ts +0 -175
  173. package/src/index.ts +0 -13
  174. package/src/model/chord.ts +0 -442
  175. package/src/model/index.ts +0 -15
  176. package/src/model/interval.ts +0 -82
  177. package/src/model/key.ts +0 -266
  178. package/src/model/note.ts +0 -176
  179. package/src/model/progression.ts +0 -161
  180. package/src/model/shared.ts +0 -12
  181. package/src/theory/chord/index.ts +0 -375
  182. package/src/theory/chordscale/index.ts +0 -374
  183. package/src/theory/counterpoint/index.ts +0 -230
  184. package/src/theory/harmony/index.ts +0 -77
  185. package/src/theory/index.ts +0 -112
  186. package/src/theory/safety/index.ts +0 -488
  187. package/src/theory/scale/degrees.ts +0 -123
  188. package/src/theory/scale/index.ts +0 -28
  189. package/src/theory/scale/key.ts +0 -56
  190. package/src/theory/scale/masks.ts +0 -153
  191. package/src/theory/spelling/index.ts +0 -249
  192. package/src/theory/symbol/index.ts +0 -268
  193. package/src/theory/voicing/index.ts +0 -5
  194. package/src/theory/voicing/internal.ts +0 -168
  195. package/src/theory/voicing/leading.ts +0 -112
  196. package/src/theory/voicing/satb.ts +0 -216
  197. package/src/theory/voicing/styled.ts +0 -199
  198. package/tsconfig.json +0 -23
@@ -1,375 +0,0 @@
1
- import type { KeyScale } from '../../core/types.js';
2
- import { scaleTonesInDegreeOrder } from '../scale/index.js';
3
-
4
- /**
5
- * Chord quality identifiers understood by the chord builder.
6
- *
7
- * @category Chords
8
- */
9
- export type ChordQuality =
10
- | 'maj'
11
- | 'min'
12
- | 'dim'
13
- | 'aug'
14
- | 'maj7'
15
- | 'min7'
16
- | 'dom7'
17
- | 'dim7'
18
- | 'm7b5'
19
- | 'minMaj7'
20
- | 'aug7'
21
- | 'augMaj7'
22
- | 'majb5'
23
- | '6'
24
- | 'min6'
25
- | '6/9'
26
- | 'sus2'
27
- | 'sus4'
28
- | 'add9'
29
- | 'add11'
30
- | 'maj9'
31
- | 'min9'
32
- | 'dom9'
33
- | '7b9'
34
- | '7#9'
35
- | '7#11'
36
- | '7b13'
37
- | '11'
38
- | '13'
39
- | '5';
40
-
41
- /**
42
- * A bare spelled pitch used as an enharmonic hint: a diatonic letter
43
- * (0..6 = C..B) plus a chromatic alteration (-2 double-flat .. +2 double-sharp).
44
- * Mirrors the letter/alter half of the pitch module's `Note` without an octave.
45
- *
46
- * @category Chords
47
- */
48
- export type PitchSpelling = {
49
- letter: number;
50
- alter: number;
51
- };
52
-
53
- /**
54
- * A chord expressed as a root pitch class plus semitone offsets.
55
- *
56
- * `rootSpelling`/`bassSpelling` are optional enharmonic hints recorded by
57
- * parsers (e.g. `parseChordSymbol('Bb7')`) so formatters can reproduce the
58
- * original spelling instead of defaulting to sharps. Consumers may ignore
59
- * them; a hint is only trusted when its pitch class still matches the
60
- * corresponding `rootPc`/`bassPc`.
61
- *
62
- * @category Chords
63
- */
64
- export type Chord = {
65
- rootPc: number;
66
- quality: ChordQuality;
67
- intervals: number[];
68
- bassPc?: number;
69
- rootSpelling?: PitchSpelling;
70
- bassSpelling?: PitchSpelling;
71
- };
72
-
73
- /**
74
- * A chord positioned in time: a root pitch class and quality placed at a beat,
75
- * with optional harmonic-analysis hints. Serves as the neutral exchange record
76
- * between progression generators and timeline analysers.
77
- *
78
- * @category Composition
79
- */
80
- export type ChordSpan = {
81
- rootPc: number;
82
- quality: ChordQuality;
83
- startBeat: number;
84
- bassPc?: number;
85
- /** 0-based scale degree of the chord root, when known. */
86
- degree?: number;
87
- /** True when the chord is a secondary dominant tonicizing another degree. */
88
- secondaryDominant?: boolean;
89
- };
90
-
91
- /** Semitone offsets from the root for each supported chord quality. */
92
- const QUALITY_INTERVALS: Record<ChordQuality, number[]> = {
93
- maj: [0, 4, 7],
94
- min: [0, 3, 7],
95
- dim: [0, 3, 6],
96
- aug: [0, 4, 8],
97
- majb5: [0, 4, 6],
98
- maj7: [0, 4, 7, 11],
99
- min7: [0, 3, 7, 10],
100
- dom7: [0, 4, 7, 10],
101
- dim7: [0, 3, 6, 9],
102
- m7b5: [0, 3, 6, 10],
103
- minMaj7: [0, 3, 7, 11],
104
- aug7: [0, 4, 8, 10],
105
- augMaj7: [0, 4, 8, 11],
106
- '6': [0, 4, 7, 9],
107
- min6: [0, 3, 7, 9],
108
- '6/9': [0, 4, 7, 9, 14],
109
- sus2: [0, 2, 7],
110
- sus4: [0, 5, 7],
111
- add9: [0, 4, 7, 14],
112
- add11: [0, 4, 7, 17],
113
- maj9: [0, 4, 7, 11, 14],
114
- min9: [0, 3, 7, 10, 14],
115
- dom9: [0, 4, 7, 10, 14],
116
- '7b9': [0, 4, 7, 10, 13],
117
- '7#9': [0, 4, 7, 10, 15],
118
- '7#11': [0, 4, 7, 10, 18],
119
- '7b13': [0, 4, 7, 10, 20],
120
- '11': [0, 7, 10, 14, 17],
121
- '13': [0, 4, 7, 10, 14, 21],
122
- '5': [0, 7],
123
- };
124
-
125
- /** Reduce a value to a pitch class in [0, 11]. */
126
- function pitchClass(value: number): number {
127
- return ((Math.trunc(value) % 12) + 12) % 12;
128
- }
129
-
130
- /**
131
- * All chord qualities the builder understands, longest templates last.
132
- *
133
- * @category Chords
134
- */
135
- export function chordQualities(): ChordQuality[] {
136
- return Object.keys(QUALITY_INTERVALS) as ChordQuality[];
137
- }
138
-
139
- /**
140
- * Build a chord rooted on a diatonic scale degree.
141
- *
142
- * The root pitch class is the degree's diatonic pitch class in `key`; the
143
- * quality's interval template is attached unchanged. Degrees beyond the scale
144
- * length wrap around.
145
- *
146
- * @param degree 0-based scale degree of the chord root.
147
- * @param ext Chord quality to apply.
148
- * @param key Key context supplying the diatonic root.
149
- * @returns The constructed chord.
150
- *
151
- * @category Chords
152
- */
153
- export function chordFromDegree(degree: number, ext: ChordQuality, key: KeyScale): Chord {
154
- const tones = scaleTonesInDegreeOrder(key);
155
- const length = tones.length;
156
- const index = length > 0 ? ((degree % length) + length) % length : 0;
157
- const rootPc = tones[index] ?? pitchClass(key.rootPc);
158
- return { rootPc, quality: ext, intervals: [...QUALITY_INTERVALS[ext]] };
159
- }
160
-
161
- /**
162
- * Build a chord from an explicit root pitch class and quality.
163
- *
164
- * @param rootPc Root pitch class (0..11).
165
- * @param quality Chord quality supplying the interval template.
166
- * @param bassPc Optional slash-chord bass pitch class.
167
- * @returns The constructed chord.
168
- *
169
- * @example
170
- * ```ts
171
- * import { makeChord } from '@libraz/libcantus';
172
- * const cmaj7 = makeChord(0, 'maj7');
173
- * // { rootPc: 0, quality: 'maj7', intervals: [0, 4, 7, 11] }
174
- * ```
175
- *
176
- * @category Chords
177
- */
178
- export function makeChord(rootPc: number, quality: ChordQuality, bassPc?: number): Chord {
179
- const chord: Chord = {
180
- rootPc: pitchClass(rootPc),
181
- quality,
182
- intervals: [...QUALITY_INTERVALS[quality]],
183
- };
184
- if (bassPc !== undefined) {
185
- chord.bassPc = pitchClass(bassPc);
186
- }
187
- return chord;
188
- }
189
-
190
- /**
191
- * Get the sorted, deduplicated pitch classes of a chord.
192
- *
193
- * @param chord The chord to enumerate.
194
- * @returns The chord's pitch classes, sorted ascending in [0, 11].
195
- *
196
- * @example
197
- * ```ts
198
- * import { makeChord, chordPitchClasses } from '@libraz/libcantus';
199
- * chordPitchClasses(makeChord(0, 'maj7')); // [0, 4, 7, 11]
200
- * ```
201
- *
202
- * @category Chords
203
- */
204
- export function chordPitchClasses(chord: Chord): number[] {
205
- const set = new Set<number>();
206
- for (const interval of chord.intervals) {
207
- set.add(pitchClass(chord.rootPc + interval));
208
- }
209
- return [...set].sort((a, b) => a - b);
210
- }
211
-
212
- /** The chord-tone offsets present in a chord, reduced to pitch-class space. */
213
- function chordToneOffsets(chord: Chord): Set<number> {
214
- return new Set(chord.intervals.map((i) => ((i % 12) + 12) % 12));
215
- }
216
-
217
- /**
218
- * Determine a pitch's harmonic role within a chord.
219
- *
220
- * The role is derived from the pitch's interval above the chord root, reduced
221
- * modulo 12. A major sixth (9) reads as a `sixth` for sixth chords but as a
222
- * `seventh` for a diminished-seventh chord; ninths and other tensions have no
223
- * basic role and return null.
224
- *
225
- * @param pitch MIDI pitch or bare pitch class.
226
- * @param chord The chord providing the root reference.
227
- * @returns The chord-tone role, or null if the pitch has no basic role.
228
- *
229
- * @category Chords
230
- */
231
- export function chordToneRole(
232
- pitch: number,
233
- chord: Chord,
234
- ): 'root' | 'third' | 'fifth' | 'sixth' | 'seventh' | null {
235
- const interval = (pitchClass(pitch) - pitchClass(chord.rootPc) + 12) % 12;
236
- const tones = chordToneOffsets(chord);
237
- if (interval === 0) {
238
- return 'root';
239
- }
240
- if (interval === 3 || interval === 4) {
241
- return tones.has(interval) ? 'third' : null;
242
- }
243
- if (interval === 7) {
244
- return tones.has(7) ? 'fifth' : null;
245
- }
246
- if (interval === 6) {
247
- // A diminished fifth is the chord's fifth only when no perfect fifth is
248
- // present; alongside a perfect fifth it is a #11 tension, not a fifth.
249
- return tones.has(6) && !tones.has(7) ? 'fifth' : null;
250
- }
251
- if (interval === 8) {
252
- // An augmented fifth is the chord's fifth only without a perfect fifth;
253
- // alongside a perfect fifth it is a b13 tension, not a fifth.
254
- return tones.has(8) && !tones.has(7) ? 'fifth' : null;
255
- }
256
- if (interval === 9) {
257
- const hasHigherSeventh = tones.has(10) || tones.has(11);
258
- const isDiminishedSeventh = tones.has(3) && tones.has(6) && !hasHigherSeventh;
259
- if (isDiminishedSeventh) {
260
- return 'seventh';
261
- }
262
- if (tones.has(9) && !hasHigherSeventh) {
263
- return 'sixth';
264
- }
265
- return null;
266
- }
267
- if (interval === 10 || interval === 11) {
268
- return tones.has(interval) ? 'seventh' : null;
269
- }
270
- return null;
271
- }
272
-
273
- /** Classify a stacked-thirds triad into a chord quality from its interval set. */
274
- function classifyTriad(thirdIc: number, fifthIc: number): ChordQuality {
275
- if (thirdIc === 4 && fifthIc === 8) {
276
- return 'aug';
277
- }
278
- if (thirdIc === 4 && fifthIc === 6) {
279
- return 'majb5';
280
- }
281
- if (thirdIc === 3 && fifthIc === 6) {
282
- return 'dim';
283
- }
284
- if (thirdIc === 3) {
285
- return 'min';
286
- }
287
- return 'maj';
288
- }
289
-
290
- /** Classify a stacked-thirds seventh chord into a chord quality. */
291
- function classifySeventh(thirdIc: number, fifthIc: number, seventhIc: number): ChordQuality {
292
- if (thirdIc === 3 && fifthIc === 6 && seventhIc === 9) {
293
- return 'dim7';
294
- }
295
- if (thirdIc === 3 && fifthIc === 6 && seventhIc === 10) {
296
- return 'm7b5';
297
- }
298
- if (thirdIc === 3 && seventhIc === 11) {
299
- return 'minMaj7';
300
- }
301
- if (thirdIc === 3) {
302
- return 'min7';
303
- }
304
- if (fifthIc === 8 && seventhIc === 10) {
305
- return 'aug7';
306
- }
307
- if (fifthIc === 8 && seventhIc === 11) {
308
- return 'augMaj7';
309
- }
310
- if (seventhIc === 11) {
311
- return 'maj7';
312
- }
313
- return 'dom7';
314
- }
315
-
316
- /** Stack scale thirds from a scale degree into a chord of `size` notes. */
317
- function stackThirds(degree: number, key: KeyScale, size: 3 | 4): Chord {
318
- const tones = scaleTonesInDegreeOrder(key);
319
- const length = tones.length;
320
- if (length !== 7) {
321
- throw new Error(
322
- `diatonic chord stacking requires a heptatonic scale (received ${length} tones)`,
323
- );
324
- }
325
- const idx = (step: number) => (((degree + step) % length) + length) % length;
326
- const rootPc = tones[idx(0)] ?? pitchClass(key.rootPc);
327
- const offsets = [0, 2, 4, 6].slice(0, size).map((step) => {
328
- const pc = tones[idx(step)] ?? rootPc;
329
- return (((pc - rootPc) % 12) + 12) % 12;
330
- });
331
- const thirdIc = offsets[1] ?? 4;
332
- const fifthIc = offsets[2] ?? 7;
333
- const quality =
334
- size === 3
335
- ? classifyTriad(thirdIc, fifthIc)
336
- : classifySeventh(thirdIc, fifthIc, offsets[3] ?? 11);
337
- return { rootPc, quality, intervals: offsets };
338
- }
339
-
340
- /**
341
- * Build the diatonic triad rooted on a scale degree by stacking scale thirds.
342
- *
343
- * Unlike {@link chordFromDegree}, the chord quality is derived from the scale
344
- * rather than supplied, so degrees yield their scale-correct triads (e.g. a
345
- * diminished triad on the leading tone of a major key). Intended for heptatonic
346
- * scales; other scales stack by scale step regardless.
347
- *
348
- * @param degree 0-based scale degree of the chord root.
349
- * @param key Key/scale context.
350
- * @returns The diatonic triad.
351
- *
352
- * @example
353
- * ```ts
354
- * import { majorKey, diatonicTriad } from '@libraz/libcantus';
355
- * const tonic = diatonicTriad(0, majorKey(0)); // C major triad (degree 0 of C major)
356
- * ```
357
- *
358
- * @category Chords
359
- */
360
- export function diatonicTriad(degree: number, key: KeyScale): Chord {
361
- return stackThirds(degree, key, 3);
362
- }
363
-
364
- /**
365
- * Build the diatonic seventh chord rooted on a scale degree.
366
- *
367
- * @param degree 0-based scale degree of the chord root.
368
- * @param key Key/scale context.
369
- * @returns The diatonic seventh chord.
370
- *
371
- * @category Chords
372
- */
373
- export function diatonicSeventh(degree: number, key: KeyScale): Chord {
374
- return stackThirds(degree, key, 4);
375
- }