@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
package/src/model/key.ts DELETED
@@ -1,266 +0,0 @@
1
- import { isMinorKey, romanToChord } from '../analyze/functional/index.js';
2
- import type { Note as NoteData } from '../core/pitch/index.js';
3
- import type { KeyScale } from '../core/types.js';
4
- import {
5
- type ChordQuality,
6
- chordFromDegree,
7
- diatonicSeventh,
8
- diatonicTriad,
9
- } from '../theory/chord/index.js';
10
- import {
11
- isScaleTone,
12
- majorKey,
13
- minorKey,
14
- scaleByName,
15
- scaleTonesInDegreeOrder,
16
- } from '../theory/scale/index.js';
17
- import { spellScale } from '../theory/spelling/index.js';
18
- import { Chord } from './chord.js';
19
- import { Note } from './note.js';
20
- import { mod12, spellPitchClassBare } from './shared.js';
21
-
22
- /** Resolve a string or numeric key root into a spelled tonic note. */
23
- function resolveTonic(root: string | number, spelling: 'sharp' | 'flat'): Note {
24
- return typeof root === 'string' ? Note.of(root) : new Note(spellPitchClassBare(root, spelling));
25
- }
26
-
27
- /** Total accidentals a spelled tonic produces across a key's whole scale. */
28
- function accidentalLoad(tonic: NoteData, scale: KeyScale): number {
29
- return spellScale(tonic, scale).reduce((sum, note) => sum + Math.abs(note.alter), 0);
30
- }
31
-
32
- /**
33
- * Choose the tonic spelling (sharp- or flat-side) that spells `scale` with the
34
- * fewest accidentals, so a numeric root never yields a double-flat/double-sharp
35
- * scale (e.g. pitch class 6 minor spells as F# minor, not Gb minor with Bbb).
36
- */
37
- function bestTonicForScale(rootPc: number, scale: KeyScale): Note {
38
- const sharp = spellPitchClassBare(rootPc, 'sharp');
39
- const flat = spellPitchClassBare(rootPc, 'flat');
40
- if (sharp.letter === flat.letter && sharp.alter === flat.alter) {
41
- return new Note(sharp);
42
- }
43
- return accidentalLoad(flat, scale) <= accidentalLoad(sharp, scale)
44
- ? new Note(flat)
45
- : new Note(sharp);
46
- }
47
-
48
- /**
49
- * An immutable key/scale: a `KeyScale` (root pitch class plus mode mask) paired
50
- * with a spelled tonic that anchors letter-name spelling. Acts as the factory
51
- * for key-aware chords.
52
- *
53
- * @category Class API
54
- * @example
55
- * ```ts
56
- * import { Key } from '@libraz/libcantus';
57
- * Key.major('C').chord(4).symbol(); // 'G' (the diatonic triad on scale degree 4)
58
- * ```
59
- */
60
- export class Key {
61
- readonly #scale: KeyScale;
62
- readonly #tonic: Note;
63
-
64
- /**
65
- * Wrap a key/scale and its spelled tonic.
66
- *
67
- * @param scale The key/scale; its root is normalized to a pitch class.
68
- * @param tonic The spelled tonic anchoring letter-name spelling.
69
- */
70
- constructor(scale: KeyScale, tonic: Note) {
71
- this.#scale = { rootPc: mod12(scale.rootPc), modeMask12: scale.modeMask12 };
72
- this.#tonic = tonic;
73
- }
74
-
75
- /**
76
- * A major key.
77
- *
78
- * @param root Tonic as a note name (e.g. `'Eb'`) or a pitch class; a numeric
79
- * root is spelled with whichever accidental side yields the fewest
80
- * accidentals across the scale.
81
- * @returns The major key.
82
- */
83
- static major(root: string | number): Key {
84
- if (typeof root === 'string') {
85
- const tonic = Note.of(root);
86
- return new Key(majorKey(tonic.pitchClass), tonic);
87
- }
88
- const scale = majorKey(root);
89
- return new Key(scale, bestTonicForScale(root, scale));
90
- }
91
-
92
- /**
93
- * A natural-minor key.
94
- *
95
- * @param root Tonic as a note name or a pitch class; a numeric root is
96
- * spelled with whichever accidental side yields the fewest accidentals
97
- * across the scale.
98
- * @returns The minor key.
99
- */
100
- static minor(root: string | number): Key {
101
- if (typeof root === 'string') {
102
- const tonic = Note.of(root);
103
- return new Key(minorKey(tonic.pitchClass), tonic);
104
- }
105
- const scale = minorKey(root);
106
- return new Key(scale, bestTonicForScale(root, scale));
107
- }
108
-
109
- /**
110
- * A key on a named scale (e.g. `'dorian'`, `'harmonicMinor'`).
111
- *
112
- * @param name The scale name, a key of the scale module's named-scale table.
113
- * @param root Tonic as a note name or a pitch class.
114
- * @returns The key.
115
- * @throws If the name is not a known scale.
116
- */
117
- static named(name: string, root: string | number): Key {
118
- const tonic = resolveTonic(root, 'sharp');
119
- return new Key(scaleByName(name, tonic.pitchClass), tonic);
120
- }
121
-
122
- /**
123
- * Wrap an existing `KeyScale`, synthesizing a spelled tonic when none is
124
- * given (flat-preferred for minor-third scales, sharp-preferred otherwise).
125
- *
126
- * @param scale The key/scale to wrap.
127
- * @param tonic Optional spelled tonic.
128
- * @returns The key.
129
- */
130
- static of(scale: KeyScale, tonic?: Note): Key {
131
- const spelled =
132
- tonic ?? new Note(spellPitchClassBare(scale.rootPc, isMinorKey(scale) ? 'flat' : 'sharp'));
133
- return new Key(scale, spelled);
134
- }
135
-
136
- /** A copy of the underlying plain `KeyScale`. */
137
- get scale(): KeyScale {
138
- return { rootPc: this.#scale.rootPc, modeMask12: this.#scale.modeMask12 };
139
- }
140
-
141
- /** The spelled tonic. */
142
- get tonic(): Note {
143
- return this.#tonic;
144
- }
145
-
146
- /** The tonic pitch class (0..11). */
147
- get rootPc(): number {
148
- return this.#scale.rootPc;
149
- }
150
-
151
- /** Whether the scale has a minor third and no major third. */
152
- get isMinor(): boolean {
153
- return isMinorKey(this.#scale);
154
- }
155
-
156
- /**
157
- * The scale's pitch classes in ascending scale-degree order (degree 0 first).
158
- *
159
- * @returns One pitch class per scale degree.
160
- */
161
- pitchClasses(): number[] {
162
- return scaleTonesInDegreeOrder(this.#scale);
163
- }
164
-
165
- /**
166
- * The spelled scale, one note per degree (e.g. C D E F G A B for C major).
167
- *
168
- * @returns Spelled octave-less notes in scale-degree order.
169
- */
170
- notes(): Note[] {
171
- return spellScale(this.#tonic.data, this.#scale).map((note) => new Note(note));
172
- }
173
-
174
- /**
175
- * Alias of {@link Key.notes}.
176
- *
177
- * @returns Spelled octave-less notes in scale-degree order.
178
- */
179
- spell(): Note[] {
180
- return this.notes();
181
- }
182
-
183
- /**
184
- * The spelled scale as letter-name strings.
185
- *
186
- * @returns One name per scale degree.
187
- */
188
- noteNames(): string[] {
189
- return this.notes().map((note) => note.name);
190
- }
191
-
192
- /**
193
- * Build a chord on a scale degree, carrying this key as context.
194
- *
195
- * With an explicit quality the quality's interval template is attached to the
196
- * degree's diatonic root; without one the scale-correct diatonic triad is
197
- * stacked (e.g. a diminished triad on the leading tone of a major key).
198
- *
199
- * @param degree 0-based scale degree of the chord root.
200
- * @param quality Optional chord quality.
201
- * @returns The chord, with this key attached.
202
- */
203
- chord(degree: number, quality?: ChordQuality): Chord {
204
- const data =
205
- quality === undefined
206
- ? diatonicTriad(degree, this.#scale)
207
- : chordFromDegree(degree, quality, this.#scale);
208
- return new Chord(data, this);
209
- }
210
-
211
- /**
212
- * The diatonic triad on a scale degree, carrying this key as context.
213
- *
214
- * @param degree 0-based scale degree of the chord root.
215
- * @returns The triad, with this key attached.
216
- */
217
- diatonicTriad(degree: number): Chord {
218
- return new Chord(diatonicTriad(degree, this.#scale), this);
219
- }
220
-
221
- /**
222
- * The diatonic seventh chord on a scale degree, carrying this key as context.
223
- *
224
- * @param degree 0-based scale degree of the chord root.
225
- * @returns The seventh chord, with this key attached.
226
- */
227
- diatonicSeventh(degree: number): Chord {
228
- return new Chord(diatonicSeventh(degree, this.#scale), this);
229
- }
230
-
231
- /**
232
- * Build the chord denoted by a Roman numeral in this key (including applied
233
- * chords such as `'V7/V'`), carrying this key as context.
234
- *
235
- * @param text The Roman numeral.
236
- * @returns The chord, with this key attached.
237
- * @throws If the numeral is not valid.
238
- */
239
- roman(text: string): Chord {
240
- return new Chord(romanToChord(text, this.#scale), this);
241
- }
242
-
243
- /**
244
- * Whether a pitch belongs to the scale.
245
- *
246
- * @param x A MIDI pitch, bare pitch class, or note.
247
- * @returns True if the pitch class is a scale tone.
248
- */
249
- contains(x: number | Note): boolean {
250
- return isScaleTone(typeof x === 'number' ? x : x.pitchClass, this.#scale);
251
- }
252
-
253
- /**
254
- * The plain key data, for JSON serialization.
255
- *
256
- * Private class fields do not serialize, so an explicit `toJSON` keeps
257
- * `JSON.stringify(key)` from collapsing to `{}`. The result pairs the
258
- * `KeyScale` with the spelled tonic, enough to reconstruct the key via
259
- * {@link Key.of}.
260
- *
261
- * @returns The key/scale and its spelled tonic.
262
- */
263
- toJSON(): { scale: KeyScale; tonic: NoteData } {
264
- return { scale: this.scale, tonic: this.#tonic.data };
265
- }
266
- }
package/src/model/note.ts DELETED
@@ -1,176 +0,0 @@
1
- import {
2
- formatNote,
3
- midiToNote,
4
- type Note as NoteData,
5
- noteToMidi,
6
- noteToPitchClass,
7
- parseNote,
8
- type SpelledInterval,
9
- spelledInterval,
10
- } from '../core/pitch/index.js';
11
- import { spellPitchClassBare } from './shared.js';
12
-
13
- /** Defensive copy of a plain note. */
14
- function copyNote(data: NoteData): NoteData {
15
- const copy: NoteData = { letter: data.letter, alter: data.alter };
16
- if (data.octave !== undefined) {
17
- copy.octave = data.octave;
18
- }
19
- return copy;
20
- }
21
-
22
- /**
23
- * An immutable spelled note: a diatonic letter plus a chromatic alteration and
24
- * an optional octave. Wraps the plain note object and delegates to the pitch
25
- * module; every transformation returns a new instance.
26
- *
27
- * @category Class API
28
- * @example
29
- * ```ts
30
- * import { Note } from '@libraz/libcantus';
31
- * Note.of('C4').transpose(7).name; // 'G4'
32
- * ```
33
- */
34
- export class Note {
35
- readonly #data: NoteData;
36
-
37
- /**
38
- * Wrap a plain note object.
39
- *
40
- * @param data The spelled note; it is copied, never retained or mutated.
41
- */
42
- constructor(data: NoteData) {
43
- this.#data = copyNote(data);
44
- }
45
-
46
- /**
47
- * Parse scientific pitch notation (e.g. `'C#4'`, `'Bb'`, `'F##3'`).
48
- *
49
- * @param name The note text.
50
- * @returns The parsed note.
51
- * @throws If the text is not a valid note.
52
- */
53
- static of(name: string): Note {
54
- return new Note(parseNote(name));
55
- }
56
-
57
- /**
58
- * Name a MIDI number (middle C = C4 = 60) as a note.
59
- *
60
- * @param midi The MIDI number.
61
- * @param spelling Whether to prefer sharps or flats for black keys.
62
- * @returns The spelled note, with octave.
63
- */
64
- static fromMidi(midi: number, spelling: 'sharp' | 'flat' = 'sharp'): Note {
65
- return new Note(midiToNote(midi, spelling));
66
- }
67
-
68
- /**
69
- * Wrap an existing plain note object.
70
- *
71
- * @param data The plain note.
72
- * @returns The wrapped note.
73
- */
74
- static fromData(data: NoteData): Note {
75
- return new Note(data);
76
- }
77
-
78
- /** The note rendered as scientific pitch notation, e.g. `'G4'` or `'Bb'`. */
79
- get name(): string {
80
- return formatNote(this.#data);
81
- }
82
-
83
- /** The pitch class (0..11), ignoring octave. */
84
- get pitchClass(): number {
85
- return noteToPitchClass(this.#data);
86
- }
87
-
88
- /**
89
- * The MIDI number (middle C = C4 = 60).
90
- *
91
- * @throws If the note has no octave and therefore no fixed pitch.
92
- */
93
- get midi(): number {
94
- if (this.#data.octave === undefined) {
95
- throw new Error(`note ${this.name} has no octave, so it has no MIDI number`);
96
- }
97
- return noteToMidi(this.#data);
98
- }
99
-
100
- /** The diatonic letter number: 0..6 for C..B. */
101
- get letter(): number {
102
- return this.#data.letter;
103
- }
104
-
105
- /** The chromatic alteration in semitones: -1 flat, 0 natural, +1 sharp, ... */
106
- get alter(): number {
107
- return this.#data.alter;
108
- }
109
-
110
- /** The octave (scientific pitch notation), or undefined for a bare pitch class. */
111
- get octave(): number | undefined {
112
- return this.#data.octave;
113
- }
114
-
115
- /** A copy of the underlying plain note object. */
116
- get data(): NoteData {
117
- return copyNote(this.#data);
118
- }
119
-
120
- /**
121
- * Transpose by a signed number of semitones.
122
- *
123
- * When the note carries an octave the transposition happens in MIDI space and
124
- * the result is spelled with a sharp preference. An octave-less note stays
125
- * octave-less: only its pitch class is moved. Transposing by zero is the
126
- * identity: the original spelling is preserved (no enharmonic respelling).
127
- *
128
- * @param semitones The signed semitone offset.
129
- * @returns The transposed note.
130
- */
131
- transpose(semitones: number): Note {
132
- if (semitones === 0) {
133
- return new Note(this.#data);
134
- }
135
- if (this.#data.octave !== undefined) {
136
- return Note.fromMidi(this.midi + semitones);
137
- }
138
- return new Note(spellPitchClassBare(this.pitchClass + semitones, 'sharp'));
139
- }
140
-
141
- /**
142
- * The spelled interval from this note to another.
143
- *
144
- * @param other The second note.
145
- * @returns The diatonic number, quality, and semitone span.
146
- */
147
- intervalTo(other: Note): SpelledInterval {
148
- return spelledInterval(this.#data, other.#data);
149
- }
150
-
151
- /**
152
- * Whether another note has the same letter, alteration, and octave.
153
- *
154
- * @param other The note to compare.
155
- * @returns True if the spellings are identical.
156
- */
157
- equals(other: Note): boolean {
158
- return (
159
- this.#data.letter === other.#data.letter &&
160
- this.#data.alter === other.#data.alter &&
161
- this.#data.octave === other.#data.octave
162
- );
163
- }
164
-
165
- /**
166
- * The plain note data, for JSON serialization.
167
- *
168
- * Private class fields do not serialize, so an explicit `toJSON` keeps
169
- * `JSON.stringify(note)` from collapsing to `{}`.
170
- *
171
- * @returns A copy of the underlying plain note object.
172
- */
173
- toJSON(): NoteData {
174
- return this.data;
175
- }
176
- }
@@ -1,161 +0,0 @@
1
- import {
2
- type Cadence,
3
- type ChordAnalysis,
4
- detectCadence,
5
- type HarmonicFunction,
6
- } from '../analyze/functional/index.js';
7
- import type { Note as NoteData } from '../core/pitch/index.js';
8
- import type { KeyScale } from '../core/types.js';
9
- import type { Chord as ChordData } from '../theory/chord/index.js';
10
- import { type VoicingOptions, voiceProgression } from '../theory/voicing/index.js';
11
- import type { Chord } from './chord.js';
12
- import type { Key } from './key.js';
13
-
14
- /**
15
- * An immutable ordered sequence of chords, optionally carrying a {@link Key}
16
- * context shared by its analysis methods.
17
- *
18
- * @category Class API
19
- * @example
20
- * ```ts
21
- * import { Key } from '@libraz/libcantus';
22
- * const key = Key.major('C');
23
- * key.chord(1).progressionTo(key.chord(4), key.chord(0)).roman();
24
- * // ['ii', 'V', 'I']
25
- * ```
26
- */
27
- export class Progression {
28
- readonly #chords: readonly Chord[];
29
- readonly #key: Key | undefined;
30
-
31
- /**
32
- * Wrap a chord sequence.
33
- *
34
- * @param chords The chords in order; the array is copied.
35
- * @param key Optional key context for analysis methods.
36
- */
37
- constructor(chords: readonly Chord[], key?: Key) {
38
- this.#chords = [...chords];
39
- this.#key = key;
40
- }
41
-
42
- /** A copy of the chord sequence. */
43
- get chords(): readonly Chord[] {
44
- return [...this.#chords];
45
- }
46
-
47
- /** The carried key context, if any. */
48
- get key(): Key | undefined {
49
- return this.#key;
50
- }
51
-
52
- /** The number of chords. */
53
- get length(): number {
54
- return this.#chords.length;
55
- }
56
-
57
- /**
58
- * A copy of this progression with a chord appended.
59
- *
60
- * @param chord The chord to append.
61
- * @returns The new progression.
62
- */
63
- add(chord: Chord): Progression {
64
- return new Progression([...this.#chords, chord], this.#key);
65
- }
66
-
67
- /**
68
- * A copy of this progression carrying the given key context.
69
- *
70
- * @param key The key context to attach.
71
- * @returns The new progression.
72
- */
73
- withKey(key: Key): Progression {
74
- return new Progression(this.#chords, key);
75
- }
76
-
77
- /**
78
- * Voice the progression with smooth voice leading.
79
- *
80
- * @param opts Voicing options; defaults to four SATB voices.
81
- * @returns One ascending voicing (MIDI pitches) per chord.
82
- * @throws If any chord admits no voicing within the given ranges.
83
- */
84
- voice(opts?: VoicingOptions): number[][] {
85
- return voiceProgression(
86
- this.#chords.map((chord) => chord.data),
87
- opts,
88
- );
89
- }
90
-
91
- /**
92
- * The Roman numeral of each chord in a key.
93
- *
94
- * @param key Key to analyze in; falls back to the carried context.
95
- * @returns One numeral per chord.
96
- * @throws If no key is given and none is carried.
97
- */
98
- roman(key?: Key): string[] {
99
- const resolved = this.#resolveKey(key);
100
- return this.#chords.map((chord) => chord.roman(resolved));
101
- }
102
-
103
- /**
104
- * The harmonic function of each chord in a key.
105
- *
106
- * @param key Key to analyze in; falls back to the carried context.
107
- * @returns One function per chord.
108
- * @throws If no key is given and none is carried.
109
- */
110
- functions(key?: Key): HarmonicFunction[] {
111
- const resolved = this.#resolveKey(key);
112
- return this.#chords.map((chord) => chord.function(resolved));
113
- }
114
-
115
- /**
116
- * Analyze every chord and classify the closing cadence.
117
- *
118
- * The cadence is detected on the final chord pair and is null when the
119
- * progression has fewer than two chords.
120
- *
121
- * @param key Key to analyze in; falls back to the carried context.
122
- * @returns Per-chord analyses and the closing cadence.
123
- * @throws If no key is given and none is carried.
124
- */
125
- analyze(key?: Key): { chords: ChordAnalysis[]; cadence: Cadence } {
126
- const resolved = this.#resolveKey(key);
127
- const chords = this.#chords.map((chord) => chord.analyze(resolved));
128
- const from = this.#chords[this.#chords.length - 2];
129
- const to = this.#chords[this.#chords.length - 1];
130
- const cadence =
131
- from !== undefined && to !== undefined
132
- ? detectCadence(from.data, to.data, resolved.scale)
133
- : null;
134
- return { chords, cadence };
135
- }
136
-
137
- /**
138
- * The plain progression data, for JSON serialization.
139
- *
140
- * Private class fields do not serialize, so an explicit `toJSON` keeps
141
- * `JSON.stringify(progression)` from collapsing to `{}`. The chords are
142
- * emitted as plain data and the carried key, when present, as its own data.
143
- *
144
- * @returns The chord data sequence and the carried key, if any.
145
- */
146
- toJSON(): { chords: ChordData[]; key: { scale: KeyScale; tonic: NoteData } | undefined } {
147
- return {
148
- chords: this.#chords.map((chord) => chord.data),
149
- key: this.#key?.toJSON(),
150
- };
151
- }
152
-
153
- /** Resolve the key for an analysis method: explicit first, then carried. */
154
- #resolveKey(key?: Key): Key {
155
- const resolved = key ?? this.#key;
156
- if (resolved === undefined) {
157
- throw new Error('progression has no key context; pass a Key or attach one with withKey()');
158
- }
159
- return resolved;
160
- }
161
- }
@@ -1,12 +0,0 @@
1
- import { midiToNote, type Note as NoteData } from '../core/pitch/index.js';
2
-
3
- /** Reduce any integer to a pitch class in [0, 11]. */
4
- export function mod12(n: number): number {
5
- return ((n % 12) + 12) % 12;
6
- }
7
-
8
- /** Spell a bare pitch class as an octave-less note with a sharp/flat preference. */
9
- export function spellPitchClassBare(pc: number, spelling: 'sharp' | 'flat'): NoteData {
10
- const spelled = midiToNote(60 + mod12(pc), spelling);
11
- return { letter: spelled.letter, alter: spelled.alter };
12
- }