@libraz/libcantus 0.9.2 → 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 (200) hide show
  1. package/README.md +6 -5
  2. package/dist/analyze/index.cjs +41 -1676
  3. package/dist/analyze/index.cjs.map +1 -1
  4. package/dist/analyze/index.d.cts +194 -122
  5. package/dist/analyze/index.d.ts +194 -122
  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-R3S2LUBH.js → chunk-7NKDPF74.js} +660 -305
  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-7Y7WF64D.js → chunk-D2FAJBYL.js} +104 -35
  18. package/dist/chunk-D2FAJBYL.js.map +1 -0
  19. package/dist/{chunk-COKTX2G5.js → chunk-EHVEBWM5.js} +1052 -510
  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-GQKN5KDT.js → chunk-GQSGS2N4.js} +43 -29
  26. package/dist/chunk-GQSGS2N4.js.map +1 -0
  27. package/dist/chunk-HKGYCQ34.js +53 -0
  28. package/dist/chunk-HKGYCQ34.js.map +1 -0
  29. package/dist/chunk-I24MV65U.js +106 -0
  30. package/dist/chunk-I24MV65U.js.map +1 -0
  31. package/dist/{chunk-LN7ZZMD2.js → chunk-KMHOICIY.js} +322 -173
  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-N6IHQUOX.js +1 -0
  36. package/dist/chunk-N6IHQUOX.js.map +1 -0
  37. package/dist/chunk-NKIBQ6PT.js +30 -0
  38. package/dist/chunk-NKIBQ6PT.js.map +1 -0
  39. package/dist/chunk-NP4NA2FB.cjs +106 -0
  40. package/dist/chunk-NP4NA2FB.cjs.map +1 -0
  41. package/dist/chunk-NVE34YGZ.cjs +1449 -0
  42. package/dist/chunk-NVE34YGZ.cjs.map +1 -0
  43. package/dist/{chunk-A4532ZFR.js → chunk-NYYWMV2C.js} +115 -47
  44. package/dist/chunk-NYYWMV2C.js.map +1 -0
  45. package/dist/{chunk-5HTNMQW4.js → chunk-Q2XUP227.js} +310 -118
  46. package/dist/chunk-Q2XUP227.js.map +1 -0
  47. package/dist/chunk-Q6OP4U43.cjs +327 -0
  48. package/dist/chunk-Q6OP4U43.cjs.map +1 -0
  49. package/dist/chunk-RC6GBMCD.cjs +845 -0
  50. package/dist/chunk-RC6GBMCD.cjs.map +1 -0
  51. package/dist/{chunk-XTC3X6MS.js → chunk-RK35RIN7.js} +453 -152
  52. package/dist/chunk-RK35RIN7.js.map +1 -0
  53. package/dist/chunk-RUMU3UA6.cjs +53 -0
  54. package/dist/chunk-RUMU3UA6.cjs.map +1 -0
  55. package/dist/chunk-T64C7ZD7.cjs +1 -0
  56. package/dist/chunk-T64C7ZD7.cjs.map +1 -0
  57. package/dist/chunk-XEBG3G53.js +498 -0
  58. package/dist/chunk-XEBG3G53.js.map +1 -0
  59. package/dist/chunk-YLEMGEFH.js +246 -0
  60. package/dist/chunk-YLEMGEFH.js.map +1 -0
  61. package/dist/chunk-ZEJR4TKL.cjs +173 -0
  62. package/dist/chunk-ZEJR4TKL.cjs.map +1 -0
  63. package/dist/core/index.cjs +130 -431
  64. package/dist/core/index.cjs.map +1 -1
  65. package/dist/core/index.d.cts +331 -17
  66. package/dist/core/index.d.ts +331 -17
  67. package/dist/core/index.js +71 -13
  68. package/dist/{function-2bJW1Zp_.d.ts → function-BtiZb9i9.d.ts} +28 -7
  69. package/dist/{function-r2sh-jOO.d.cts → function-DNugAOrt.d.cts} +28 -7
  70. package/dist/generate/index.cjs +30 -3751
  71. package/dist/generate/index.cjs.map +1 -1
  72. package/dist/generate/index.d.cts +373 -59
  73. package/dist/generate/index.d.ts +373 -59
  74. package/dist/generate/index.js +23 -9
  75. package/dist/{index-DZ1M21do.d.cts → index-6VR8-kit.d.ts} +20 -4
  76. package/dist/index-70p168VM.d.ts +192 -0
  77. package/dist/{index-D_ZTUNKp.d.cts → index-Be74mcon.d.cts} +18 -17
  78. package/dist/index-BtXT6UQ0.d.cts +192 -0
  79. package/dist/{index-BMPxwjwV.d.ts → index-C5UBbQ4G.d.ts} +18 -17
  80. package/dist/{index-BOBXBNwP.d.cts → index-C83mZI1P.d.ts} +93 -8
  81. package/dist/{index-BdeQ1r_p.d.cts → index-D6b6na69.d.cts} +14 -11
  82. package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
  83. package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
  84. package/dist/index-OG65ocOr.d.cts +272 -0
  85. package/dist/index-OG65ocOr.d.ts +272 -0
  86. package/dist/{index-BreoJRw0.d.ts → index-tJhkmQgE.d.cts} +20 -4
  87. package/dist/{index-CP3wAfEH.d.ts → index-tSQh-xwY.d.ts} +14 -11
  88. package/dist/{index-CM5W4rcm.d.ts → index-y_ak9_Hi.d.cts} +93 -8
  89. package/dist/index.cjs +419 -6848
  90. package/dist/index.cjs.map +1 -1
  91. package/dist/index.d.cts +14 -13
  92. package/dist/index.d.ts +14 -13
  93. package/dist/index.js +132 -27
  94. package/dist/model/index.cjs +8 -2423
  95. package/dist/model/index.cjs.map +1 -1
  96. package/dist/model/index.d.cts +433 -74
  97. package/dist/model/index.d.ts +433 -74
  98. package/dist/model/index.js +6 -7
  99. package/dist/styled-DIRkk9Uz.d.ts +506 -0
  100. package/dist/styled-DJbkJQfj.d.cts +506 -0
  101. package/dist/theory/index.cjs +172 -1676
  102. package/dist/theory/index.cjs.map +1 -1
  103. package/dist/theory/index.d.cts +151 -233
  104. package/dist/theory/index.d.ts +151 -233
  105. package/dist/theory/index.js +31 -7
  106. package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
  107. package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
  108. package/package.json +73 -24
  109. package/dist/chunk-5HTNMQW4.js.map +0 -1
  110. package/dist/chunk-7Y7WF64D.js.map +0 -1
  111. package/dist/chunk-A4532ZFR.js.map +0 -1
  112. package/dist/chunk-BHUT2S5G.js +0 -22
  113. package/dist/chunk-BHUT2S5G.js.map +0 -1
  114. package/dist/chunk-CCBLVAPK.js +0 -42
  115. package/dist/chunk-CCBLVAPK.js.map +0 -1
  116. package/dist/chunk-COKTX2G5.js.map +0 -1
  117. package/dist/chunk-DYWFNC4A.js +0 -152
  118. package/dist/chunk-DYWFNC4A.js.map +0 -1
  119. package/dist/chunk-GQKN5KDT.js.map +0 -1
  120. package/dist/chunk-LN7ZZMD2.js.map +0 -1
  121. package/dist/chunk-MSWFZF7J.js +0 -118
  122. package/dist/chunk-MSWFZF7J.js.map +0 -1
  123. package/dist/chunk-R3S2LUBH.js.map +0 -1
  124. package/dist/chunk-SDM3VJJ5.js +0 -41
  125. package/dist/chunk-SDM3VJJ5.js.map +0 -1
  126. package/dist/chunk-XTC3X6MS.js.map +0 -1
  127. package/dist/chunk-ZXW5YIWK.js +0 -55
  128. package/dist/chunk-ZXW5YIWK.js.map +0 -1
  129. package/dist/index-CGMZNCx6.d.cts +0 -140
  130. package/dist/index-CGMZNCx6.d.ts +0 -140
  131. package/dist/styled-BjXtOFNk.d.cts +0 -282
  132. package/dist/styled-D7BAx3S3.d.ts +0 -282
  133. package/src/analyze/arrange/index.ts +0 -21
  134. package/src/analyze/arrange/internal.ts +0 -125
  135. package/src/analyze/arrange/tension.ts +0 -187
  136. package/src/analyze/arrange/tracks.ts +0 -279
  137. package/src/analyze/detect/index.ts +0 -235
  138. package/src/analyze/functional/borrowed.ts +0 -84
  139. package/src/analyze/functional/cadence.ts +0 -66
  140. package/src/analyze/functional/function.ts +0 -184
  141. package/src/analyze/functional/index.ts +0 -23
  142. package/src/analyze/functional/internal.ts +0 -29
  143. package/src/analyze/functional/roman.ts +0 -393
  144. package/src/analyze/index.ts +0 -49
  145. package/src/analyze/timeline/index.ts +0 -415
  146. package/src/analyze/voice/index.ts +0 -330
  147. package/src/core/index.ts +0 -47
  148. package/src/core/interval/index.ts +0 -81
  149. package/src/core/meter/index.ts +0 -282
  150. package/src/core/pitch/index.ts +0 -311
  151. package/src/core/random/index.ts +0 -53
  152. package/src/core/tuning/index.ts +0 -156
  153. package/src/core/types.ts +0 -29
  154. package/src/generate/bass/index.ts +0 -350
  155. package/src/generate/countermelody/index.ts +0 -379
  156. package/src/generate/drums/beat.ts +0 -266
  157. package/src/generate/drums/euclid.ts +0 -58
  158. package/src/generate/drums/fills.ts +0 -356
  159. package/src/generate/drums/ghost.ts +0 -119
  160. package/src/generate/drums/hihat.ts +0 -285
  161. package/src/generate/drums/hit.ts +0 -31
  162. package/src/generate/drums/index.ts +0 -329
  163. package/src/generate/drums/internal.ts +0 -257
  164. package/src/generate/drums/kick.ts +0 -160
  165. package/src/generate/drums/percussion.ts +0 -121
  166. package/src/generate/drums/rng.ts +0 -7
  167. package/src/generate/drums/swing.ts +0 -71
  168. package/src/generate/groove/index.ts +0 -326
  169. package/src/generate/harmonize/index.ts +0 -440
  170. package/src/generate/index.ts +0 -59
  171. package/src/generate/motif/index.ts +0 -398
  172. package/src/generate/progression/index.ts +0 -370
  173. package/src/generate/reharmony/index.ts +0 -330
  174. package/src/generate/rhythm/index.ts +0 -165
  175. package/src/index.ts +0 -247
  176. package/src/model/chord.ts +0 -423
  177. package/src/model/index.ts +0 -15
  178. package/src/model/interval.ts +0 -82
  179. package/src/model/key.ts +0 -266
  180. package/src/model/note.ts +0 -176
  181. package/src/model/progression.ts +0 -161
  182. package/src/model/shared.ts +0 -12
  183. package/src/theory/chord/index.ts +0 -373
  184. package/src/theory/chordscale/index.ts +0 -374
  185. package/src/theory/counterpoint/index.ts +0 -230
  186. package/src/theory/harmony/index.ts +0 -92
  187. package/src/theory/index.ts +0 -107
  188. package/src/theory/safety/index.ts +0 -419
  189. package/src/theory/scale/degrees.ts +0 -123
  190. package/src/theory/scale/index.ts +0 -28
  191. package/src/theory/scale/key.ts +0 -56
  192. package/src/theory/scale/masks.ts +0 -153
  193. package/src/theory/spelling/index.ts +0 -182
  194. package/src/theory/symbol/index.ts +0 -268
  195. package/src/theory/voicing/index.ts +0 -5
  196. package/src/theory/voicing/internal.ts +0 -168
  197. package/src/theory/voicing/leading.ts +0 -112
  198. package/src/theory/voicing/satb.ts +0 -195
  199. package/src/theory/voicing/styled.ts +0 -176
  200. package/tsconfig.json +0 -23
@@ -1,373 +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 === 0) {
321
- return { rootPc: pitchClass(key.rootPc), quality: 'maj', intervals: [0, 4, 7] };
322
- }
323
- const idx = (step: number) => (((degree + step) % length) + length) % length;
324
- const rootPc = tones[idx(0)] ?? pitchClass(key.rootPc);
325
- const offsets = [0, 2, 4, 6].slice(0, size).map((step) => {
326
- const pc = tones[idx(step)] ?? rootPc;
327
- return (((pc - rootPc) % 12) + 12) % 12;
328
- });
329
- const thirdIc = offsets[1] ?? 4;
330
- const fifthIc = offsets[2] ?? 7;
331
- const quality =
332
- size === 3
333
- ? classifyTriad(thirdIc, fifthIc)
334
- : classifySeventh(thirdIc, fifthIc, offsets[3] ?? 11);
335
- return { rootPc, quality, intervals: offsets };
336
- }
337
-
338
- /**
339
- * Build the diatonic triad rooted on a scale degree by stacking scale thirds.
340
- *
341
- * Unlike {@link chordFromDegree}, the chord quality is derived from the scale
342
- * rather than supplied, so degrees yield their scale-correct triads (e.g. a
343
- * diminished triad on the leading tone of a major key). Intended for heptatonic
344
- * scales; other scales stack by scale step regardless.
345
- *
346
- * @param degree 0-based scale degree of the chord root.
347
- * @param key Key/scale context.
348
- * @returns The diatonic triad.
349
- *
350
- * @example
351
- * ```ts
352
- * import { majorKey, diatonicTriad } from '@libraz/libcantus';
353
- * const tonic = diatonicTriad(0, majorKey(0)); // C major triad (degree 0 of C major)
354
- * ```
355
- *
356
- * @category Chords
357
- */
358
- export function diatonicTriad(degree: number, key: KeyScale): Chord {
359
- return stackThirds(degree, key, 3);
360
- }
361
-
362
- /**
363
- * Build the diatonic seventh chord rooted on a scale degree.
364
- *
365
- * @param degree 0-based scale degree of the chord root.
366
- * @param key Key/scale context.
367
- * @returns The diatonic seventh chord.
368
- *
369
- * @category Chords
370
- */
371
- export function diatonicSeventh(degree: number, key: KeyScale): Chord {
372
- return stackThirds(degree, key, 4);
373
- }