@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,268 +0,0 @@
1
- /**
2
- * Lead-sheet chord symbol parsing and formatting.
3
- *
4
- * Bridges the informal `<root><quality>[/<bass>]` notation musicians write
5
- * (`Cmaj7`, `F#m7b5`, `Bb7`, `C/G`) and the library's structured {@link Chord}
6
- * type, so callers can accept/emit plain text at the edges of the API.
7
- */
8
-
9
- import {
10
- formatNote,
11
- midiToNote,
12
- type Note,
13
- noteToPitchClass,
14
- parseNote,
15
- } from '../../core/pitch/index.js';
16
- import { type Chord, type ChordQuality, makeChord, type PitchSpelling } from '../chord/index.js';
17
-
18
- /**
19
- * Matches a chord root: a letter A-G (case-insensitive, like {@link parseNote})
20
- * with an optional single-direction accidental.
21
- */
22
- const ROOT_RE = /^[A-Ga-g](?:#{1,2}|b{1,2})?/;
23
-
24
- /** Matches a bare note usable as a slash-chord bass (no octave). */
25
- const BASS_RE = /^[A-Ga-g](?:#{1,2}|b{1,2})?$/;
26
-
27
- /** Recognized quality suffixes, keyed by their exact lead-sheet spelling. */
28
- const QUALITY_MAP: Record<string, ChordQuality> = {
29
- '': 'maj',
30
- m: 'min',
31
- min: 'min',
32
- '-': 'min',
33
- dim: 'dim',
34
- '°': 'dim',
35
- aug: 'aug',
36
- '+': 'aug',
37
- maj7: 'maj7',
38
- M7: 'maj7',
39
- Δ: 'maj7',
40
- Δ7: 'maj7',
41
- m7: 'min7',
42
- min7: 'min7',
43
- '-7': 'min7',
44
- '7': 'dom7',
45
- dim7: 'dim7',
46
- '°7': 'dim7',
47
- m7b5: 'm7b5',
48
- ø: 'm7b5',
49
- ø7: 'm7b5',
50
- min7b5: 'm7b5',
51
- mMaj7: 'minMaj7',
52
- mM7: 'minMaj7',
53
- minMaj7: 'minMaj7',
54
- aug7: 'aug7',
55
- '+7': 'aug7',
56
- '7#5': 'aug7',
57
- augMaj7: 'augMaj7',
58
- augM7: 'augMaj7',
59
- '+maj7': 'augMaj7',
60
- '+M7': 'augMaj7',
61
- majb5: 'majb5',
62
- 'maj(b5)': 'majb5',
63
- '(b5)': 'majb5',
64
- '6': '6',
65
- m6: 'min6',
66
- min6: 'min6',
67
- '6/9': '6/9',
68
- '69': '6/9',
69
- sus2: 'sus2',
70
- sus: 'sus4',
71
- sus4: 'sus4',
72
- add9: 'add9',
73
- add11: 'add11',
74
- maj9: 'maj9',
75
- M9: 'maj9',
76
- m9: 'min9',
77
- min9: 'min9',
78
- '9': 'dom9',
79
- '7b9': '7b9',
80
- '7#9': '7#9',
81
- '7#11': '7#11',
82
- '7b13': '7b13',
83
- '11': '11',
84
- '13': '13',
85
- '5': '5',
86
- };
87
-
88
- /** Canonical lead-sheet suffix emitted for each chord quality when formatting. */
89
- const CANONICAL_SUFFIX: Record<ChordQuality, string> = {
90
- maj: '',
91
- min: 'm',
92
- dim: 'dim',
93
- aug: 'aug',
94
- maj7: 'maj7',
95
- min7: 'm7',
96
- dom7: '7',
97
- dim7: 'dim7',
98
- m7b5: 'm7b5',
99
- minMaj7: 'mMaj7',
100
- aug7: 'aug7',
101
- augMaj7: 'augMaj7',
102
- majb5: 'maj(b5)',
103
- '6': '6',
104
- min6: 'm6',
105
- '6/9': '6/9',
106
- sus2: 'sus2',
107
- sus4: 'sus4',
108
- add9: 'add9',
109
- add11: 'add11',
110
- maj9: 'maj9',
111
- min9: 'm9',
112
- dom9: '9',
113
- '7b9': '7b9',
114
- '7#9': '7#9',
115
- '7#11': '7#11',
116
- '7b13': '7b13',
117
- '11': '11',
118
- '13': '13',
119
- '5': '5',
120
- };
121
-
122
- /** Reduce a value to a pitch class in [0, 11]. */
123
- function pitchClass(value: number): number {
124
- return ((Math.trunc(value) % 12) + 12) % 12;
125
- }
126
-
127
- /** Build a chord and record the parsed spellings as enharmonic hints. */
128
- function makeSpelledChord(root: Note, quality: ChordQuality, bass?: Note): Chord {
129
- const chord = makeChord(
130
- noteToPitchClass(root),
131
- quality,
132
- bass === undefined ? undefined : noteToPitchClass(bass),
133
- );
134
- chord.rootSpelling = { letter: root.letter, alter: root.alter };
135
- if (bass !== undefined) {
136
- chord.bassSpelling = { letter: bass.letter, alter: bass.alter };
137
- }
138
- return chord;
139
- }
140
-
141
- /**
142
- * Parse a lead-sheet chord symbol into a {@link Chord}.
143
- *
144
- * Accepts `<root><quality>[/<bass>]`, e.g. `Cmaj7`, `F#m7b5`, `Bb7`, `C/G`,
145
- * `C6/9`, `C6/9/E`. Root and bass letters are case-insensitive, matching
146
- * {@link parseNote}. A `/` is ambiguous between a slash bass and the `6/9`
147
- * quality; the text splits at the last `/` whose right-hand side is a valid
148
- * bare note preceded by a recognized quality, so `6/9` (with or without a
149
- * further slash bass) parses as a quality.
150
- *
151
- * The parsed chord carries the root/bass spellings as enharmonic hints
152
- * (`rootSpelling`/`bassSpelling`) so {@link formatChordSymbol} can reproduce
153
- * flat spellings such as `Bbmaj7` instead of respelling them with sharps.
154
- *
155
- * @param text The chord symbol text.
156
- * @returns The parsed chord.
157
- * @throws If the root or quality is not recognized.
158
- * @example
159
- * ```ts
160
- * import { parseChordSymbol, formatChordSymbol } from '@libraz/libcantus';
161
- * const c = parseChordSymbol('Bbmaj7');
162
- * formatChordSymbol(c); // 'Bbmaj7' — flat spelling is preserved on round-trip
163
- * ```
164
- * @category Chords
165
- */
166
- export function parseChordSymbol(text: string): Chord {
167
- const trimmed = text.trim();
168
- const rootMatch = ROOT_RE.exec(trimmed);
169
- if (!rootMatch) {
170
- throw new Error(`Invalid chord symbol: ${text}`);
171
- }
172
- const rootToken = rootMatch[0];
173
- const rootNote = parseNote(rootToken);
174
- const rest = trimmed.slice(rootToken.length);
175
-
176
- // Split at the last '/' so the '6/9' quality's own slash never masks a
177
- // trailing slash bass (e.g. 'C6/9/E').
178
- const slashIndex = rest.lastIndexOf('/');
179
- if (slashIndex !== -1) {
180
- const before = rest.slice(0, slashIndex);
181
- const after = rest.slice(slashIndex + 1);
182
- const beforeQuality = QUALITY_MAP[before];
183
- if (beforeQuality !== undefined && BASS_RE.test(after)) {
184
- return makeSpelledChord(rootNote, beforeQuality, parseNote(after));
185
- }
186
- }
187
-
188
- const quality = QUALITY_MAP[rest];
189
- if (quality === undefined) {
190
- throw new Error(`Unrecognized chord quality: ${text}`);
191
- }
192
- return makeSpelledChord(rootNote, quality);
193
- }
194
-
195
- /**
196
- * Name a pitch class, preferring a spelling hint when it is still valid.
197
- *
198
- * A hint is used only when no explicit sharp/flat preference was given and the
199
- * hint still resolves to `pc` (a stale hint left over after transposition is
200
- * ignored). Otherwise the pitch class is respelled from the requested table.
201
- */
202
- function pitchClassName(pc: number, hint: PitchSpelling | undefined, flats?: boolean): string {
203
- if (flats === undefined && hint !== undefined && noteToPitchClass(hint) === pc) {
204
- return formatNote(hint);
205
- }
206
- const note = midiToNote(60 + pc, flats ? 'flat' : 'sharp');
207
- return formatNote({ letter: note.letter, alter: note.alter });
208
- }
209
-
210
- /**
211
- * Format a {@link Chord} as a lead-sheet chord symbol.
212
- *
213
- * The inverse of {@link parseChordSymbol}: each quality maps to one canonical
214
- * suffix spelling, and a slash bass is appended only when it differs from the
215
- * root. When the chord carries `rootSpelling`/`bassSpelling` hints (as chords
216
- * from {@link parseChordSymbol} do) and no explicit `flats` preference is
217
- * given, the hints are reused so flat symbols round-trip unchanged.
218
- *
219
- * @param chord The chord to format.
220
- * @param opts Formatting options.
221
- * @param opts.flats Prefer flat spellings over sharps for altered roots/basses;
222
- * passing either `true` or `false` overrides any spelling hints.
223
- * @returns The chord symbol text.
224
- * @example
225
- * ```ts
226
- * import { makeChord, formatChordSymbol } from '@libraz/libcantus';
227
- * formatChordSymbol(makeChord(0, 'min7')); // 'Cm7'
228
- * ```
229
- * @category Chords
230
- */
231
- export function formatChordSymbol(chord: Chord, opts?: { flats?: boolean }): string {
232
- const rootPc = pitchClass(chord.rootPc);
233
- const rootName = pitchClassName(rootPc, chord.rootSpelling, opts?.flats);
234
- const suffix = CANONICAL_SUFFIX[chord.quality];
235
- let symbol = `${rootName}${suffix}`;
236
- if (chord.bassPc !== undefined && pitchClass(chord.bassPc) !== rootPc) {
237
- symbol += `/${pitchClassName(pitchClass(chord.bassPc), chord.bassSpelling, opts?.flats)}`;
238
- }
239
- return symbol;
240
- }
241
-
242
- /**
243
- * Transpose a chord symbol by a number of semitones.
244
- *
245
- * @param text The chord symbol text.
246
- * @param semitones Signed semitone offset to apply to the root and bass.
247
- * @param opts Formatting options for the result; see {@link formatChordSymbol}.
248
- * @returns The transposed chord symbol text.
249
- * @throws If `text` does not parse as a chord symbol.
250
- * @example
251
- * ```ts
252
- * import { transposeChordSymbol } from '@libraz/libcantus';
253
- * transposeChordSymbol('C/G', 2); // 'D/A'
254
- * ```
255
- * @category Chords
256
- */
257
- export function transposeChordSymbol(
258
- text: string,
259
- semitones: number,
260
- opts?: { flats?: boolean },
261
- ): string {
262
- const chord = parseChordSymbol(text);
263
- const transposed: Chord = { ...chord, rootPc: pitchClass(chord.rootPc + semitones) };
264
- if (chord.bassPc !== undefined) {
265
- transposed.bassPc = pitchClass(chord.bassPc + semitones);
266
- }
267
- return formatChordSymbol(transposed, opts);
268
- }
@@ -1,5 +0,0 @@
1
- export { nextVoicing, voiceLeadingCost } from './leading.js';
2
- export type { VoiceRange, VoicingOptions } from './satb.js';
3
- export { SATB_RANGES, voiceChord, voiceProgression } from './satb.js';
4
- export type { StyledVoicingOptions, VoicingStyle } from './styled.js';
5
- export { voiceChordStyled } from './styled.js';
@@ -1,168 +0,0 @@
1
- import type { Chord } from '../chord/index.js';
2
- import { chordPitchClasses } from '../chord/index.js';
3
- import {
4
- createsParallelPerfect,
5
- createsVoiceCrossing,
6
- createsVoiceOverlap,
7
- exceedsSpacing,
8
- } from '../counterpoint/index.js';
9
- import type { VoiceRange } from './satb.js';
10
-
11
- /** Default maximum spacing between adjacent upper voices (one octave). */
12
- export const DEFAULT_MAX_SPACING = 12;
13
-
14
- /** Score penalty per counterpoint violation between consecutive voicings. */
15
- export const VIOLATION_PENALTY = 1000;
16
- /** Score penalty per chord tone absent from a voicing. */
17
- const MISSING_TONE_PENALTY = 500;
18
- /** Score penalty per doubled tone that is neither the root nor the fifth. */
19
- const POOR_DOUBLING_PENALTY = 4;
20
- /** Hard cap on candidate voicings evaluated per chord, keeping the search bounded. */
21
- const MAX_CANDIDATES = 4000;
22
-
23
- /** Reduce a value to a pitch class in [0, 11]. */
24
- export function pitchClass(value: number): number {
25
- return ((Math.trunc(value) % 12) + 12) % 12;
26
- }
27
-
28
- /**
29
- * All MIDI pitches of a pitch class inside an inclusive range, ordered from the
30
- * centre of the range outward (ties break toward the lower pitch). Enumerating
31
- * centre-outward keeps the candidate set balanced around the register when it
32
- * is truncated at {@link MAX_CANDIDATES}, instead of skewing to the low octaves
33
- * that a plain ascending scan would visit first.
34
- */
35
- function pitchesForPc(pc: number, range: VoiceRange): number[] {
36
- const result: number[] = [];
37
- for (let midi = Math.ceil(range.min); midi <= range.max; midi += 1) {
38
- if (pitchClass(midi) === pc) {
39
- result.push(midi);
40
- }
41
- }
42
- const center = (range.min + range.max) / 2;
43
- return result.sort((a, b) => {
44
- const da = Math.abs(a - center);
45
- const db = Math.abs(b - center);
46
- return da === db ? a - b : da - db;
47
- });
48
- }
49
-
50
- /**
51
- * Enumerate candidate voicings for a chord: the bass takes the chord's bass
52
- * (or root) pitch class at each available octave, and each upper voice takes
53
- * any chord pitch class within its range. Candidates are built in ascending
54
- * voice order and pruned to exclude voice crossings and over-wide adjacent
55
- * spacing (the bass–tenor pair is allowed an extra octave, per convention).
56
- * Enumeration is deterministic and capped at {@link MAX_CANDIDATES}.
57
- */
58
- export function enumerateVoicings(
59
- chord: Chord,
60
- ranges: VoiceRange[],
61
- maxSpacing: number,
62
- ): number[][] {
63
- const chordPcs = chordPitchClasses(chord);
64
- const bassPc = pitchClass(chord.bassPc ?? chord.rootPc);
65
- const results: number[][] = [];
66
- const current: number[] = [];
67
- const build = (voice: number): void => {
68
- if (results.length >= MAX_CANDIDATES) {
69
- return;
70
- }
71
- if (voice === ranges.length) {
72
- results.push([...current]);
73
- return;
74
- }
75
- const range = ranges[voice];
76
- if (range === undefined) {
77
- return;
78
- }
79
- const pcs = voice === 0 ? [bassPc] : chordPcs;
80
- const prev = current[voice - 1];
81
- for (const pc of pcs) {
82
- for (const pitch of pitchesForPc(pc, range)) {
83
- if (prev !== undefined) {
84
- if (pitch < prev) {
85
- continue; // would cross below the next lower voice
86
- }
87
- const spacingLimit = voice === 1 ? maxSpacing + 12 : maxSpacing;
88
- if (pitch - prev > spacingLimit) {
89
- continue;
90
- }
91
- }
92
- current.push(pitch);
93
- build(voice + 1);
94
- current.pop();
95
- }
96
- }
97
- };
98
- build(0);
99
- return results;
100
- }
101
-
102
- /**
103
- * Structural quality penalty of a single voicing: heavily penalize missing
104
- * chord tones, mildly penalize doubling anything other than the root or fifth.
105
- */
106
- export function structuralPenalty(pitches: number[], chord: Chord): number {
107
- const counts = new Map<number, number>();
108
- for (const pitch of pitches) {
109
- const pc = pitchClass(pitch);
110
- counts.set(pc, (counts.get(pc) ?? 0) + 1);
111
- }
112
- let penalty = 0;
113
- for (const pc of chordPitchClasses(chord)) {
114
- if (!counts.has(pc)) {
115
- penalty += MISSING_TONE_PENALTY;
116
- }
117
- }
118
- const rootPc = pitchClass(chord.rootPc);
119
- const fifthPc = pitchClass(chord.rootPc + 7);
120
- for (const [pc, count] of counts) {
121
- if (count > 1 && pc !== rootPc && pc !== fifthPc) {
122
- penalty += (count - 1) * POOR_DOUBLING_PENALTY;
123
- }
124
- }
125
- return penalty;
126
- }
127
-
128
- /**
129
- * Count counterpoint violations between two consecutive voicings of equal
130
- * length: parallel perfects (fifths and octaves alike, since octaves are the
131
- * perfect-class-zero case of {@link createsParallelPerfect}) on every voice
132
- * pair, voice crossings in the new voicing, and — on adjacent pairs — voice
133
- * overlaps and over-wide spacing between upper voices.
134
- */
135
- export function violationCount(prev: number[], cur: number[], maxSpacing: number): number {
136
- let count = 0;
137
- for (let lower = 0; lower < cur.length; lower += 1) {
138
- for (let upper = lower + 1; upper < cur.length; upper += 1) {
139
- const prevLower = prev[lower];
140
- const prevUpper = prev[upper];
141
- const curLower = cur[lower];
142
- const curUpper = cur[upper];
143
- if (
144
- prevLower === undefined ||
145
- prevUpper === undefined ||
146
- curLower === undefined ||
147
- curUpper === undefined
148
- ) {
149
- continue;
150
- }
151
- if (createsParallelPerfect(prevUpper, curUpper, prevLower, curLower)) {
152
- count += 1;
153
- }
154
- if (createsVoiceCrossing(curUpper, curLower)) {
155
- count += 1;
156
- }
157
- if (upper === lower + 1) {
158
- if (createsVoiceOverlap(prevUpper, curUpper, prevLower, curLower)) {
159
- count += 1;
160
- }
161
- if (lower > 0 && exceedsSpacing(curUpper, curLower, maxSpacing)) {
162
- count += 1;
163
- }
164
- }
165
- }
166
- }
167
- return count;
168
- }
@@ -1,112 +0,0 @@
1
- import type { Chord } from '../chord/index.js';
2
- import { createsHiddenParallelPerfect } from '../counterpoint/index.js';
3
- import {
4
- DEFAULT_MAX_SPACING,
5
- enumerateVoicings,
6
- structuralPenalty,
7
- VIOLATION_PENALTY,
8
- violationCount,
9
- } from './internal.js';
10
- import type { VoicingOptions } from './satb.js';
11
- import { resolveRanges } from './satb.js';
12
-
13
- /**
14
- * Moderate penalty for a hidden/direct perfect fifth or octave reached on the
15
- * outer-voice (bass–soprano) pair. Unlike a true parallel perfect it is
16
- * discouraged rather than forbidden, so the weight sits alongside voice-leading
17
- * motion rather than the hard {@link VIOLATION_PENALTY}.
18
- */
19
- const HIDDEN_PERFECT_PENALTY = 6;
20
-
21
- /**
22
- * Total voice-leading cost between two voicings: the sum of absolute semitone
23
- * motion across voices, plus a moderate {@link HIDDEN_PERFECT_PENALTY} when the
24
- * outer-voice (bass–soprano) pair reaches a hidden/direct perfect fifth or
25
- * octave by similar motion. The arrays must be the same length; when they
26
- * differ the voicings are not comparable and the cost is `Infinity`.
27
- *
28
- * @param from The previous voicing, one MIDI pitch per voice.
29
- * @param to The next voicing, one MIDI pitch per voice.
30
- * @returns The summed absolute motion (plus any hidden-perfect penalty), or
31
- * `Infinity` when lengths differ.
32
- * @category Voicing & Counterpoint
33
- */
34
- export function voiceLeadingCost(from: number[], to: number[]): number {
35
- if (from.length !== to.length) {
36
- return Number.POSITIVE_INFINITY;
37
- }
38
- let total = 0;
39
- for (let i = 0; i < from.length; i += 1) {
40
- const a = from[i];
41
- const b = to[i];
42
- if (a === undefined || b === undefined) {
43
- continue;
44
- }
45
- total += Math.abs(b - a);
46
- }
47
- // Discourage hidden/direct perfects between the outermost voices, where they
48
- // are most audible. True parallels are handled (and forbidden) elsewhere.
49
- if (from.length >= 2) {
50
- const bassPrev = from[0];
51
- const bassCur = to[0];
52
- const sopPrev = from[from.length - 1];
53
- const sopCur = to[to.length - 1];
54
- if (
55
- bassPrev !== undefined &&
56
- bassCur !== undefined &&
57
- sopPrev !== undefined &&
58
- sopCur !== undefined &&
59
- createsHiddenParallelPerfect(bassPrev, bassCur, sopPrev, sopCur)
60
- ) {
61
- total += HIDDEN_PERFECT_PENALTY;
62
- }
63
- }
64
- return total;
65
- }
66
-
67
- /**
68
- * Voice a single chord to follow smoothly from an arbitrary current voicing.
69
- *
70
- * Candidate voicings of `chord` are enumerated within ranges taken from `opts`,
71
- * or — when neither `voices` nor `ranges` is given — from a one-octave window
72
- * around each pitch of `current` (so the result matches `current`'s voice
73
- * count). Each candidate is scored by structural quality, voice-leading motion
74
- * from `current`, and a large penalty per counterpoint violation; the lowest
75
- * scoring candidate is returned, ascending.
76
- *
77
- * @param current The current voicing to lead from, ascending (index 0 = lowest).
78
- * @param chord The next chord to voice.
79
- * @param opts Voicing options; when omitted, ranges follow `current`'s span.
80
- * @returns The chosen voicing, ascending with one MIDI pitch per voice.
81
- * @throws If no voicing fits the derived ranges.
82
- * @category Voicing & Counterpoint
83
- */
84
- export function nextVoicing(current: number[], chord: Chord, opts?: VoicingOptions): number[] {
85
- const ranges =
86
- opts?.ranges !== undefined || opts?.voices !== undefined
87
- ? resolveRanges(opts)
88
- : current.map((pitch) => {
89
- // Window one octave around each current pitch, clamped so extreme-low
90
- // or extreme-high input can never yield MIDI outside [0, 127].
91
- const centre = Math.min(127, Math.max(0, pitch));
92
- return { min: Math.max(0, centre - 12), max: Math.min(127, centre + 12) };
93
- });
94
- const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;
95
- const candidates = enumerateVoicings(chord, ranges, maxSpacing);
96
- let best: number[] | undefined;
97
- let bestScore = Number.POSITIVE_INFINITY;
98
- for (const candidate of candidates) {
99
- const score =
100
- structuralPenalty(candidate, chord) +
101
- voiceLeadingCost(current, candidate) +
102
- VIOLATION_PENALTY * violationCount(current, candidate, maxSpacing);
103
- if (score < bestScore) {
104
- bestScore = score;
105
- best = candidate;
106
- }
107
- }
108
- if (best === undefined) {
109
- throw new Error('no voicing satisfies the given ranges');
110
- }
111
- return best;
112
- }