@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,216 +0,0 @@
1
- import {
2
- assertFiniteNumber,
3
- assertGenerationBudget,
4
- assertPositiveInt,
5
- } from '../../core/validation/index.js';
6
- import type { Chord } from '../chord/index.js';
7
- import {
8
- DEFAULT_MAX_SPACING,
9
- enumerateVoicings,
10
- structuralPenalty,
11
- VIOLATION_PENALTY,
12
- violationCount,
13
- } from './internal.js';
14
- import { voiceLeadingCost } from './leading.js';
15
-
16
- /**
17
- * An inclusive MIDI pitch range for a single voice.
18
- *
19
- * @category Voicing & Counterpoint
20
- */
21
- export type VoiceRange = {
22
- min: number;
23
- max: number;
24
- };
25
-
26
- /**
27
- * Default four-voice SATB ranges, ascending (index 0 = lowest):
28
- * bass E2–C4 (40–60), tenor C3–G4 (48–67), alto G3–D5 (55–74),
29
- * soprano C4–G5 (60–79).
30
- *
31
- * @category Voicing & Counterpoint
32
- */
33
- export const SATB_RANGES: readonly VoiceRange[] = [
34
- { min: 40, max: 60 },
35
- { min: 48, max: 67 },
36
- { min: 55, max: 74 },
37
- { min: 60, max: 79 },
38
- ];
39
-
40
- /**
41
- * Options controlling {@link voiceChord} and {@link voiceProgression}.
42
- *
43
- * @category Voicing & Counterpoint
44
- */
45
- export type VoicingOptions = {
46
- /**
47
- * Number of voices to realize. Ignored when `ranges` is given.
48
- *
49
- * @defaultValue 4
50
- */
51
- voices?: number;
52
- /** Explicit per-voice ranges, ascending (index 0 = lowest). Takes precedence over `voices`. */
53
- ranges?: VoiceRange[];
54
- /**
55
- * Maximum spacing in semitones between adjacent upper voices.
56
- *
57
- * @defaultValue 12
58
- */
59
- maxSpacing?: number;
60
- };
61
-
62
- /** Overall pitch floor/ceiling used when deriving ranges for arbitrary voice counts. */
63
- const DERIVED_LOW = 40;
64
- const DERIVED_HIGH = 79;
65
- /** Span of each derived per-voice range in semitones. */
66
- const DERIVED_SPAN = 19;
67
-
68
- /**
69
- * Resolve the per-voice ranges implied by the options: explicit `ranges` win,
70
- * four voices use {@link SATB_RANGES}, and other counts get evenly spaced
71
- * ranges spanning roughly the bass-to-soprano compass.
72
- */
73
- export function resolveRanges(opts?: VoicingOptions): VoiceRange[] {
74
- if (opts?.ranges !== undefined) {
75
- if (opts.ranges.length === 0) {
76
- throw new Error('ranges must contain at least one voice range');
77
- }
78
- assertGenerationBudget(opts.ranges.length, 'voice ranges', 128);
79
- return opts.ranges.map((range, index) => {
80
- assertFiniteNumber(range.min, `ranges[${index}].min`);
81
- assertFiniteNumber(range.max, `ranges[${index}].max`);
82
- if (range.min > range.max) {
83
- throw new RangeError(`ranges[${index}].min must not exceed max`);
84
- }
85
- assertGenerationBudget(
86
- Math.floor(range.max) - Math.ceil(range.min) + 1,
87
- `ranges[${index}] span`,
88
- 4096,
89
- );
90
- return { ...range };
91
- });
92
- }
93
- const voices = opts?.voices ?? 4;
94
- assertPositiveInt(voices, 'voices', 128);
95
- if (voices === 4) {
96
- return SATB_RANGES.map((range) => ({ ...range }));
97
- }
98
- if (voices === 1) {
99
- return [{ min: DERIVED_LOW, max: DERIVED_HIGH }];
100
- }
101
- const ranges: VoiceRange[] = [];
102
- for (let i = 0; i < voices; i += 1) {
103
- const min = Math.round(
104
- DERIVED_LOW + (i * (DERIVED_HIGH - DERIVED_SPAN - DERIVED_LOW)) / (voices - 1),
105
- );
106
- ranges.push({ min, max: min + DERIVED_SPAN });
107
- }
108
- return ranges;
109
- }
110
-
111
- /**
112
- * Realize a single chord as one MIDI pitch per voice, ascending (index 0 =
113
- * lowest). The bass voice takes the chord's `bassPc` when set, otherwise the
114
- * root; upper voices take chord pitch classes, doubling the root or fifth as
115
- * needed to fill all voices. The result stays inside each voice's range, keeps
116
- * adjacent upper voices within `maxSpacing`, avoids voice crossing, and is
117
- * deterministic: a compact close-position voicing centered in the ranges.
118
- *
119
- * @param chord The chord to voice.
120
- * @param opts Voicing options; defaults to four voices in {@link SATB_RANGES}.
121
- * @returns MIDI pitches, ascending, one per voice.
122
- * @throws If no voicing fits the given ranges.
123
- * @example
124
- * ```ts
125
- * import { parseChordSymbol, voiceChord } from '@libraz/libcantus';
126
- * const chord = parseChordSymbol('Cmaj7');
127
- * voiceChord(chord); // four ascending MIDI pitches within the SATB ranges
128
- * ```
129
- * @category Voicing & Counterpoint
130
- */
131
- export function voiceChord(chord: Chord, opts?: VoicingOptions): number[] {
132
- const ranges = resolveRanges(opts);
133
- const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;
134
- assertFiniteNumber(maxSpacing, 'maxSpacing');
135
- if (maxSpacing < 0) throw new RangeError('maxSpacing must be non-negative');
136
- const candidates = enumerateVoicings(chord, ranges, maxSpacing);
137
- let best: number[] | undefined;
138
- let bestScore = Number.POSITIVE_INFINITY;
139
- for (const candidate of candidates) {
140
- let score = structuralPenalty(candidate, chord);
141
- for (let i = 0; i < candidate.length; i += 1) {
142
- const pitch = candidate[i];
143
- const range = ranges[i];
144
- if (pitch === undefined || range === undefined) {
145
- continue;
146
- }
147
- // Prefer pitches near the middle of each voice's range for a centered,
148
- // compact default voicing.
149
- score += Math.abs(pitch - (range.min + range.max) / 2);
150
- }
151
- if (score < bestScore) {
152
- bestScore = score;
153
- best = candidate;
154
- }
155
- }
156
- if (best === undefined) {
157
- throw new Error('no voicing satisfies the given ranges');
158
- }
159
- return best;
160
- }
161
-
162
- /**
163
- * Voice a chord progression with smooth voice leading. The first chord is
164
- * voiced with {@link voiceChord}; each subsequent chord picks, from a bounded
165
- * deterministic candidate set, the voicing minimizing the voice-leading cost
166
- * from the previous voicing plus a large penalty per counterpoint violation
167
- * (parallel perfects/octaves, voice crossing, voice overlap, and over-wide
168
- * upper-voice spacing).
169
- *
170
- * @param chords The chords to voice in order.
171
- * @param opts Voicing options; defaults to four voices in {@link SATB_RANGES}.
172
- * @returns One voicing per chord, each ascending with one MIDI pitch per voice.
173
- * @throws If any chord admits no voicing within the given ranges.
174
- * @example
175
- * ```ts
176
- * import { parseChordSymbol, voiceProgression } from '@libraz/libcantus';
177
- * const chords = ['C', 'Am', 'F', 'G'].map((s) => parseChordSymbol(s));
178
- * voiceProgression(chords); // one four-voice voicing per chord, smoothly led
179
- * ```
180
- * @category Voicing & Counterpoint
181
- */
182
- export function voiceProgression(chords: Chord[], opts?: VoicingOptions): number[][] {
183
- assertGenerationBudget(chords.length * 4000, 'voiced progression search');
184
- const ranges = resolveRanges(opts);
185
- const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;
186
- assertFiniteNumber(maxSpacing, 'maxSpacing');
187
- if (maxSpacing < 0) throw new RangeError('maxSpacing must be non-negative');
188
- const result: number[][] = [];
189
- let prev: number[] | undefined;
190
- for (const chord of chords) {
191
- if (prev === undefined) {
192
- prev = voiceChord(chord, opts);
193
- result.push(prev);
194
- continue;
195
- }
196
- const candidates = enumerateVoicings(chord, ranges, maxSpacing);
197
- let best: number[] | undefined;
198
- let bestScore = Number.POSITIVE_INFINITY;
199
- for (const candidate of candidates) {
200
- const score =
201
- structuralPenalty(candidate, chord) +
202
- voiceLeadingCost(prev, candidate) +
203
- VIOLATION_PENALTY * violationCount(prev, candidate, maxSpacing);
204
- if (score < bestScore) {
205
- bestScore = score;
206
- best = candidate;
207
- }
208
- }
209
- if (best === undefined) {
210
- throw new Error('no voicing satisfies the given ranges');
211
- }
212
- result.push(best);
213
- prev = best;
214
- }
215
- return result;
216
- }
@@ -1,199 +0,0 @@
1
- import { assertFiniteNumber, assertInteger } from '../../core/validation/index.js';
2
- import type { Chord } from '../chord/index.js';
3
- import { chordToneRole } from '../chord/index.js';
4
- import { pitchClass } from './internal.js';
5
-
6
- /**
7
- * A tertian voicing style for {@link voiceChordStyled}.
8
- *
9
- * - `close`: the plain close-position tertian stack.
10
- * - `drop2`: the second voice from the top dropped an octave (drop-2 voicing).
11
- * - `drop3`: the third voice from the top dropped an octave (drop-3 voicing).
12
- * - `shell`: root plus guide tones (third and seventh) for seventh chords, or
13
- * root/third/fifth for triads; the fifth and tensions are omitted.
14
- * - `rootless`: the root omitted, keeping third/fifth/seventh and tensions
15
- * (a typical left-hand jazz voicing).
16
- *
17
- * @category Voicing & Counterpoint
18
- */
19
- export type VoicingStyle = 'close' | 'drop2' | 'drop3' | 'shell' | 'rootless';
20
-
21
- /**
22
- * Options controlling {@link voiceChordStyled}.
23
- *
24
- * @category Voicing & Counterpoint
25
- */
26
- export type StyledVoicingOptions = {
27
- /**
28
- * Voicing style to build.
29
- *
30
- * @defaultValue 'close'
31
- */
32
- style?: VoicingStyle;
33
- /** Constrain the highest voice to this pitch class (0..11) when given. */
34
- topNote?: number;
35
- /**
36
- * Base octave for the close stack; the stack begins near `12 * octave`.
37
- *
38
- * @defaultValue 4
39
- */
40
- octave?: number;
41
- /**
42
- * Omit the root regardless of style (relevant for jazz voicings).
43
- *
44
- * @defaultValue false
45
- */
46
- rootless?: boolean;
47
- };
48
-
49
- /** Default base octave for a styled close-position stack. */
50
- const DEFAULT_STYLE_OCTAVE = 4;
51
-
52
- /** Return the pitch classes of a list in order, without duplicates. */
53
- function dedupePcs(pcs: number[]): number[] {
54
- const seen = new Set<number>();
55
- const result: number[] = [];
56
- for (const pc of pcs) {
57
- if (!seen.has(pc)) {
58
- seen.add(pc);
59
- result.push(pc);
60
- }
61
- }
62
- return result;
63
- }
64
-
65
- /** The lowest MIDI pitch of a pitch class at or above a floor. */
66
- function lowestPitchAtOrAbove(pc: number, floor: number): number {
67
- return floor + ((((pc - pitchClass(floor)) % 12) + 12) % 12);
68
- }
69
-
70
- /** The pitch class in `pcs` whose circular distance to `target` is smallest. */
71
- function nearestPc(target: number, pcs: number[]): number {
72
- let best = pcs[0] ?? target;
73
- let bestDist = 12;
74
- for (const pc of pcs) {
75
- const dist = Math.min((((pc - target) % 12) + 12) % 12, (((target - pc) % 12) + 12) % 12);
76
- if (dist < bestDist) {
77
- bestDist = dist;
78
- best = pc;
79
- }
80
- }
81
- return best;
82
- }
83
-
84
- /**
85
- * Realize a single chord as an explicit tertian voicing in a chosen style,
86
- * independent of the SATB range search used by {@link voiceChord}.
87
- *
88
- * The chord tones are stacked in close position from the bass (`bassPc`, else
89
- * `rootPc`) in the given scientific-pitch `octave` upward (octave 4 starts the
90
- * bass near C4 = MIDI 60, matching {@link midiToNote} and the bass module).
91
- * When `topNote` is given the stack is rotated so the highest voice's pitch
92
- * class equals that note (or the nearest chord tone). The chosen
93
- * {@link VoicingStyle} then transforms the stack, and the result is returned as
94
- * ascending MIDI pitches (index 0 = lowest).
95
- *
96
- * @param chord The chord to voice.
97
- * @param opts Styled voicing options; defaults to a close voicing at octave 4.
98
- * @returns MIDI pitches, ascending, one per retained voice.
99
- * @example
100
- * ```ts
101
- * import { parseChordSymbol, voiceChordStyled } from '@libraz/libcantus';
102
- * const chord = parseChordSymbol('Dm7');
103
- * voiceChordStyled(chord, { style: 'drop2' }); // ascending MIDI pitches, drop-2 voicing
104
- * ```
105
- * @category Voicing & Counterpoint
106
- */
107
- export function voiceChordStyled(chord: Chord, opts?: StyledVoicingOptions): number[] {
108
- const style = opts?.style ?? 'close';
109
- const octave = opts?.octave ?? DEFAULT_STYLE_OCTAVE;
110
- assertInteger(octave, 'styled voicing octave', -10, 10);
111
- if (opts?.topNote !== undefined) {
112
- assertFiniteNumber(opts.topNote, 'styled voicing topNote');
113
- }
114
- const base = 12 * (octave + 1);
115
- const rootPc = pitchClass(chord.rootPc);
116
- const bassPc = pitchClass(chord.bassPc ?? chord.rootPc);
117
- const omitRoot = style === 'rootless' || opts?.rootless === true;
118
-
119
- // Tertian chord tones in order, with the bass tone rotated to the bottom.
120
- let order = dedupePcs(chord.intervals.map((interval) => pitchClass(chord.rootPc + interval)));
121
- const bassIndex = order.indexOf(bassPc);
122
- if (bassIndex > 0) {
123
- order = [...order.slice(bassIndex), ...order.slice(0, bassIndex)];
124
- } else if (bassIndex < 0) {
125
- order = [bassPc, ...order];
126
- }
127
-
128
- if (style === 'shell') {
129
- const roles = order.map((pc) => chordToneRole(pc, chord));
130
- // The guide tone is the seventh when present; on a sixth chord the sixth
131
- // stands in for it, and otherwise the voicing falls back to the fifth.
132
- const guide = roles.includes('seventh')
133
- ? 'seventh'
134
- : roles.includes('sixth')
135
- ? 'sixth'
136
- : 'fifth';
137
- order = order.filter((_pc, i) => {
138
- const role = roles[i];
139
- return _pc === bassPc || role === 'root' || role === 'third' || role === guide;
140
- });
141
- }
142
-
143
- if (omitRoot) {
144
- order = order.filter((pc) => pc !== rootPc || (chord.bassPc !== undefined && pc === bassPc));
145
- }
146
-
147
- if (order.length === 0) {
148
- return [];
149
- }
150
-
151
- if (opts?.topNote !== undefined) {
152
- const target = order.includes(pitchClass(opts.topNote))
153
- ? pitchClass(opts.topNote)
154
- : nearestPc(pitchClass(opts.topNote), order);
155
- const targetIndex = order.indexOf(target);
156
- order = [...order.slice(targetIndex + 1), ...order.slice(0, targetIndex + 1)];
157
- }
158
-
159
- // Stack the ordered pitch classes upward in close position.
160
- const stack: number[] = [];
161
- let prev: number | undefined;
162
- for (const pc of order) {
163
- if (prev === undefined) {
164
- prev = lowestPitchAtOrAbove(pc, base);
165
- } else {
166
- let delta = (((pc - pitchClass(prev)) % 12) + 12) % 12;
167
- if (delta === 0) {
168
- delta = 12;
169
- }
170
- prev += delta;
171
- }
172
- stack.push(prev);
173
- }
174
-
175
- if (style === 'drop2' && stack.length >= 2) {
176
- stack[stack.length - 2] = (stack[stack.length - 2] ?? 0) - 12;
177
- } else if (style === 'drop3' && stack.length >= 3) {
178
- stack[stack.length - 3] = (stack[stack.length - 3] ?? 0) - 12;
179
- }
180
-
181
- // An explicit slash bass is a structural requirement, including when it is
182
- // not a chord member. Drop voicings and top-note rotation may have moved a
183
- // different tone below it, so lower the retained bass by octaves before the
184
- // final sort. This gives the bass priority over a conflicting top-note hint.
185
- if (chord.bassPc !== undefined) {
186
- const bassIndexInStack = stack.findIndex((pitch) => pitchClass(pitch) === bassPc);
187
- if (bassIndexInStack >= 0) {
188
- const otherLowest = Math.min(
189
- ...stack.filter((_pitch, index) => index !== bassIndexInStack),
190
- Number.POSITIVE_INFINITY,
191
- );
192
- while ((stack[bassIndexInStack] ?? 0) >= otherLowest) {
193
- stack[bassIndexInStack] = (stack[bassIndexInStack] ?? 0) - 12;
194
- }
195
- }
196
- }
197
-
198
- return stack.sort((a, b) => a - b);
199
- }
package/tsconfig.json DELETED
@@ -1,23 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "lib": ["ES2022"],
5
- "module": "NodeNext",
6
- "moduleResolution": "NodeNext",
7
- "strict": true,
8
- "ignoreDeprecations": "6.0",
9
- "verbatimModuleSyntax": true,
10
- "noUncheckedIndexedAccess": true,
11
- "exactOptionalPropertyTypes": false,
12
- "declaration": true,
13
- "sourceMap": true,
14
- "outDir": "dist",
15
- "rootDir": "src",
16
- "skipLibCheck": true,
17
- "forceConsistentCasingInFileNames": true,
18
- "esModuleInterop": true,
19
- "isolatedModules": true
20
- },
21
- "include": ["src"],
22
- "exclude": ["dist", "node_modules"]
23
- }