@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,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
- }