@libraz/libcantus 0.9.2 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/README.md +6 -5
  2. package/dist/analyze/index.cjs +41 -1676
  3. package/dist/analyze/index.cjs.map +1 -1
  4. package/dist/analyze/index.d.cts +194 -122
  5. package/dist/analyze/index.d.ts +194 -122
  6. package/dist/analyze/index.js +19 -8
  7. package/dist/chunk-36N73KZ3.cjs +246 -0
  8. package/dist/chunk-36N73KZ3.cjs.map +1 -0
  9. package/dist/chunk-4HLLZPXT.cjs +3077 -0
  10. package/dist/chunk-4HLLZPXT.cjs.map +1 -0
  11. package/dist/chunk-7MGZ7D62.cjs +498 -0
  12. package/dist/chunk-7MGZ7D62.cjs.map +1 -0
  13. package/dist/{chunk-R3S2LUBH.js → chunk-7NKDPF74.js} +660 -305
  14. package/dist/chunk-7NKDPF74.js.map +1 -0
  15. package/dist/chunk-CAN76NCU.cjs +679 -0
  16. package/dist/chunk-CAN76NCU.cjs.map +1 -0
  17. package/dist/{chunk-7Y7WF64D.js → chunk-D2FAJBYL.js} +104 -35
  18. package/dist/chunk-D2FAJBYL.js.map +1 -0
  19. package/dist/{chunk-COKTX2G5.js → chunk-EHVEBWM5.js} +1052 -510
  20. package/dist/chunk-EHVEBWM5.js.map +1 -0
  21. package/dist/chunk-FCANXBAT.cjs +30 -0
  22. package/dist/chunk-FCANXBAT.cjs.map +1 -0
  23. package/dist/chunk-FNDWXRRI.cjs +479 -0
  24. package/dist/chunk-FNDWXRRI.cjs.map +1 -0
  25. package/dist/{chunk-GQKN5KDT.js → chunk-GQSGS2N4.js} +43 -29
  26. package/dist/chunk-GQSGS2N4.js.map +1 -0
  27. package/dist/chunk-HKGYCQ34.js +53 -0
  28. package/dist/chunk-HKGYCQ34.js.map +1 -0
  29. package/dist/chunk-I24MV65U.js +106 -0
  30. package/dist/chunk-I24MV65U.js.map +1 -0
  31. package/dist/{chunk-LN7ZZMD2.js → chunk-KMHOICIY.js} +322 -173
  32. package/dist/chunk-KMHOICIY.js.map +1 -0
  33. package/dist/chunk-LW53AHQU.cjs +1117 -0
  34. package/dist/chunk-LW53AHQU.cjs.map +1 -0
  35. package/dist/chunk-N6IHQUOX.js +1 -0
  36. package/dist/chunk-N6IHQUOX.js.map +1 -0
  37. package/dist/chunk-NKIBQ6PT.js +30 -0
  38. package/dist/chunk-NKIBQ6PT.js.map +1 -0
  39. package/dist/chunk-NP4NA2FB.cjs +106 -0
  40. package/dist/chunk-NP4NA2FB.cjs.map +1 -0
  41. package/dist/chunk-NVE34YGZ.cjs +1449 -0
  42. package/dist/chunk-NVE34YGZ.cjs.map +1 -0
  43. package/dist/{chunk-A4532ZFR.js → chunk-NYYWMV2C.js} +115 -47
  44. package/dist/chunk-NYYWMV2C.js.map +1 -0
  45. package/dist/{chunk-5HTNMQW4.js → chunk-Q2XUP227.js} +310 -118
  46. package/dist/chunk-Q2XUP227.js.map +1 -0
  47. package/dist/chunk-Q6OP4U43.cjs +327 -0
  48. package/dist/chunk-Q6OP4U43.cjs.map +1 -0
  49. package/dist/chunk-RC6GBMCD.cjs +845 -0
  50. package/dist/chunk-RC6GBMCD.cjs.map +1 -0
  51. package/dist/{chunk-XTC3X6MS.js → chunk-RK35RIN7.js} +453 -152
  52. package/dist/chunk-RK35RIN7.js.map +1 -0
  53. package/dist/chunk-RUMU3UA6.cjs +53 -0
  54. package/dist/chunk-RUMU3UA6.cjs.map +1 -0
  55. package/dist/chunk-T64C7ZD7.cjs +1 -0
  56. package/dist/chunk-T64C7ZD7.cjs.map +1 -0
  57. package/dist/chunk-XEBG3G53.js +498 -0
  58. package/dist/chunk-XEBG3G53.js.map +1 -0
  59. package/dist/chunk-YLEMGEFH.js +246 -0
  60. package/dist/chunk-YLEMGEFH.js.map +1 -0
  61. package/dist/chunk-ZEJR4TKL.cjs +173 -0
  62. package/dist/chunk-ZEJR4TKL.cjs.map +1 -0
  63. package/dist/core/index.cjs +130 -431
  64. package/dist/core/index.cjs.map +1 -1
  65. package/dist/core/index.d.cts +331 -17
  66. package/dist/core/index.d.ts +331 -17
  67. package/dist/core/index.js +71 -13
  68. package/dist/{function-2bJW1Zp_.d.ts → function-BtiZb9i9.d.ts} +28 -7
  69. package/dist/{function-r2sh-jOO.d.cts → function-DNugAOrt.d.cts} +28 -7
  70. package/dist/generate/index.cjs +30 -3751
  71. package/dist/generate/index.cjs.map +1 -1
  72. package/dist/generate/index.d.cts +373 -59
  73. package/dist/generate/index.d.ts +373 -59
  74. package/dist/generate/index.js +23 -9
  75. package/dist/{index-DZ1M21do.d.cts → index-6VR8-kit.d.ts} +20 -4
  76. package/dist/index-70p168VM.d.ts +192 -0
  77. package/dist/{index-D_ZTUNKp.d.cts → index-Be74mcon.d.cts} +18 -17
  78. package/dist/index-BtXT6UQ0.d.cts +192 -0
  79. package/dist/{index-BMPxwjwV.d.ts → index-C5UBbQ4G.d.ts} +18 -17
  80. package/dist/{index-BOBXBNwP.d.cts → index-C83mZI1P.d.ts} +93 -8
  81. package/dist/{index-BdeQ1r_p.d.cts → index-D6b6na69.d.cts} +14 -11
  82. package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
  83. package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
  84. package/dist/index-OG65ocOr.d.cts +272 -0
  85. package/dist/index-OG65ocOr.d.ts +272 -0
  86. package/dist/{index-BreoJRw0.d.ts → index-tJhkmQgE.d.cts} +20 -4
  87. package/dist/{index-CP3wAfEH.d.ts → index-tSQh-xwY.d.ts} +14 -11
  88. package/dist/{index-CM5W4rcm.d.ts → index-y_ak9_Hi.d.cts} +93 -8
  89. package/dist/index.cjs +419 -6848
  90. package/dist/index.cjs.map +1 -1
  91. package/dist/index.d.cts +14 -13
  92. package/dist/index.d.ts +14 -13
  93. package/dist/index.js +132 -27
  94. package/dist/model/index.cjs +8 -2423
  95. package/dist/model/index.cjs.map +1 -1
  96. package/dist/model/index.d.cts +433 -74
  97. package/dist/model/index.d.ts +433 -74
  98. package/dist/model/index.js +6 -7
  99. package/dist/styled-DIRkk9Uz.d.ts +506 -0
  100. package/dist/styled-DJbkJQfj.d.cts +506 -0
  101. package/dist/theory/index.cjs +172 -1676
  102. package/dist/theory/index.cjs.map +1 -1
  103. package/dist/theory/index.d.cts +151 -233
  104. package/dist/theory/index.d.ts +151 -233
  105. package/dist/theory/index.js +31 -7
  106. package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
  107. package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
  108. package/package.json +73 -24
  109. package/dist/chunk-5HTNMQW4.js.map +0 -1
  110. package/dist/chunk-7Y7WF64D.js.map +0 -1
  111. package/dist/chunk-A4532ZFR.js.map +0 -1
  112. package/dist/chunk-BHUT2S5G.js +0 -22
  113. package/dist/chunk-BHUT2S5G.js.map +0 -1
  114. package/dist/chunk-CCBLVAPK.js +0 -42
  115. package/dist/chunk-CCBLVAPK.js.map +0 -1
  116. package/dist/chunk-COKTX2G5.js.map +0 -1
  117. package/dist/chunk-DYWFNC4A.js +0 -152
  118. package/dist/chunk-DYWFNC4A.js.map +0 -1
  119. package/dist/chunk-GQKN5KDT.js.map +0 -1
  120. package/dist/chunk-LN7ZZMD2.js.map +0 -1
  121. package/dist/chunk-MSWFZF7J.js +0 -118
  122. package/dist/chunk-MSWFZF7J.js.map +0 -1
  123. package/dist/chunk-R3S2LUBH.js.map +0 -1
  124. package/dist/chunk-SDM3VJJ5.js +0 -41
  125. package/dist/chunk-SDM3VJJ5.js.map +0 -1
  126. package/dist/chunk-XTC3X6MS.js.map +0 -1
  127. package/dist/chunk-ZXW5YIWK.js +0 -55
  128. package/dist/chunk-ZXW5YIWK.js.map +0 -1
  129. package/dist/index-CGMZNCx6.d.cts +0 -140
  130. package/dist/index-CGMZNCx6.d.ts +0 -140
  131. package/dist/styled-BjXtOFNk.d.cts +0 -282
  132. package/dist/styled-D7BAx3S3.d.ts +0 -282
  133. package/src/analyze/arrange/index.ts +0 -21
  134. package/src/analyze/arrange/internal.ts +0 -125
  135. package/src/analyze/arrange/tension.ts +0 -187
  136. package/src/analyze/arrange/tracks.ts +0 -279
  137. package/src/analyze/detect/index.ts +0 -235
  138. package/src/analyze/functional/borrowed.ts +0 -84
  139. package/src/analyze/functional/cadence.ts +0 -66
  140. package/src/analyze/functional/function.ts +0 -184
  141. package/src/analyze/functional/index.ts +0 -23
  142. package/src/analyze/functional/internal.ts +0 -29
  143. package/src/analyze/functional/roman.ts +0 -393
  144. package/src/analyze/index.ts +0 -49
  145. package/src/analyze/timeline/index.ts +0 -415
  146. package/src/analyze/voice/index.ts +0 -330
  147. package/src/core/index.ts +0 -47
  148. package/src/core/interval/index.ts +0 -81
  149. package/src/core/meter/index.ts +0 -282
  150. package/src/core/pitch/index.ts +0 -311
  151. package/src/core/random/index.ts +0 -53
  152. package/src/core/tuning/index.ts +0 -156
  153. package/src/core/types.ts +0 -29
  154. package/src/generate/bass/index.ts +0 -350
  155. package/src/generate/countermelody/index.ts +0 -379
  156. package/src/generate/drums/beat.ts +0 -266
  157. package/src/generate/drums/euclid.ts +0 -58
  158. package/src/generate/drums/fills.ts +0 -356
  159. package/src/generate/drums/ghost.ts +0 -119
  160. package/src/generate/drums/hihat.ts +0 -285
  161. package/src/generate/drums/hit.ts +0 -31
  162. package/src/generate/drums/index.ts +0 -329
  163. package/src/generate/drums/internal.ts +0 -257
  164. package/src/generate/drums/kick.ts +0 -160
  165. package/src/generate/drums/percussion.ts +0 -121
  166. package/src/generate/drums/rng.ts +0 -7
  167. package/src/generate/drums/swing.ts +0 -71
  168. package/src/generate/groove/index.ts +0 -326
  169. package/src/generate/harmonize/index.ts +0 -440
  170. package/src/generate/index.ts +0 -59
  171. package/src/generate/motif/index.ts +0 -398
  172. package/src/generate/progression/index.ts +0 -370
  173. package/src/generate/reharmony/index.ts +0 -330
  174. package/src/generate/rhythm/index.ts +0 -165
  175. package/src/index.ts +0 -247
  176. package/src/model/chord.ts +0 -423
  177. package/src/model/index.ts +0 -15
  178. package/src/model/interval.ts +0 -82
  179. package/src/model/key.ts +0 -266
  180. package/src/model/note.ts +0 -176
  181. package/src/model/progression.ts +0 -161
  182. package/src/model/shared.ts +0 -12
  183. package/src/theory/chord/index.ts +0 -373
  184. package/src/theory/chordscale/index.ts +0 -374
  185. package/src/theory/counterpoint/index.ts +0 -230
  186. package/src/theory/harmony/index.ts +0 -92
  187. package/src/theory/index.ts +0 -107
  188. package/src/theory/safety/index.ts +0 -419
  189. package/src/theory/scale/degrees.ts +0 -123
  190. package/src/theory/scale/index.ts +0 -28
  191. package/src/theory/scale/key.ts +0 -56
  192. package/src/theory/scale/masks.ts +0 -153
  193. package/src/theory/spelling/index.ts +0 -182
  194. package/src/theory/symbol/index.ts +0 -268
  195. package/src/theory/voicing/index.ts +0 -5
  196. package/src/theory/voicing/internal.ts +0 -168
  197. package/src/theory/voicing/leading.ts +0 -112
  198. package/src/theory/voicing/satb.ts +0 -195
  199. package/src/theory/voicing/styled.ts +0 -176
  200. package/tsconfig.json +0 -23
@@ -1,56 +0,0 @@
1
- import type { KeyScale } from '../../core/types.js';
2
- import { MAJOR_MASK, NAMED_SCALES, NATURAL_MINOR_MASK } from './masks.js';
3
-
4
- /**
5
- * Build a `KeyScale` for a major key on the given root pitch class.
6
- *
7
- * @example
8
- * ```ts
9
- * import { majorKey } from '@libraz/libcantus';
10
- * const cMajor = majorKey(0); // C major: { rootPc: 0, modeMask12: MAJOR_MASK }
11
- * ```
12
- *
13
- * @category Scales
14
- */
15
- export function majorKey(rootPc: number): KeyScale {
16
- return { rootPc: ((rootPc % 12) + 12) % 12, modeMask12: MAJOR_MASK };
17
- }
18
-
19
- /**
20
- * Build a `KeyScale` for a natural-minor key on the given root pitch class.
21
- *
22
- * @example
23
- * ```ts
24
- * import { minorKey } from '@libraz/libcantus';
25
- * const aMinor = minorKey(9); // A natural minor: { rootPc: 9, modeMask12: NATURAL_MINOR_MASK }
26
- * ```
27
- *
28
- * @category Scales
29
- */
30
- export function minorKey(rootPc: number): KeyScale {
31
- return { rootPc: ((rootPc % 12) + 12) % 12, modeMask12: NATURAL_MINOR_MASK };
32
- }
33
-
34
- /**
35
- * Build a `KeyScale` from a named scale (see {@link NAMED_SCALES}).
36
- *
37
- * @param name The scale name, e.g. `'dorian'` or `'harmonicMinor'`.
38
- * @param rootPc The root pitch class.
39
- * @returns The key/scale.
40
- * @throws If the name is not a known scale.
41
- *
42
- * @example
43
- * ```ts
44
- * import { scaleByName } from '@libraz/libcantus';
45
- * const dDorian = scaleByName('dorian', 2); // D Dorian, rootPc 2
46
- * ```
47
- *
48
- * @category Scales
49
- */
50
- export function scaleByName(name: string, rootPc: number): KeyScale {
51
- const mask = NAMED_SCALES[name];
52
- if (mask === undefined) {
53
- throw new Error(`Unknown scale: ${name}`);
54
- }
55
- return { rootPc: ((rootPc % 12) + 12) % 12, modeMask12: mask };
56
- }
@@ -1,153 +0,0 @@
1
- /**
2
- * Build a 12-bit mode mask from a list of semitone offsets above the root.
3
- *
4
- * Bit 0 (the root) is always set, enforcing the `KeyScale` invariant that the
5
- * root is a scale tone even when the offset list omits 0.
6
- *
7
- * @category Scales
8
- */
9
- export function maskFromOffsets(offsets: readonly number[]): number {
10
- let mask = 1;
11
- for (const offset of offsets) {
12
- mask |= 1 << (((offset % 12) + 12) % 12);
13
- }
14
- return mask;
15
- }
16
-
17
- /**
18
- * Mode mask for the major (Ionian) scale: offsets {0, 2, 4, 5, 7, 9, 11}.
19
- *
20
- * @category Scales
21
- */
22
- export const MAJOR_MASK = 0b101010110101;
23
-
24
- /**
25
- * Mode mask for the natural minor (Aeolian) scale: offsets {0, 2, 3, 5, 7, 8, 10}.
26
- *
27
- * @category Scales
28
- */
29
- export const NATURAL_MINOR_MASK = 0b010110101101;
30
-
31
- /**
32
- * Harmonic minor: natural minor with a raised seventh — offsets {0,2,3,5,7,8,11}.
33
- *
34
- * @category Scales
35
- */
36
- export const HARMONIC_MINOR_MASK = maskFromOffsets([0, 2, 3, 5, 7, 8, 11]);
37
-
38
- /**
39
- * Ascending melodic minor: offsets {0,2,3,5,7,9,11}.
40
- *
41
- * @category Scales
42
- */
43
- export const MELODIC_MINOR_MASK = maskFromOffsets([0, 2, 3, 5, 7, 9, 11]);
44
-
45
- /**
46
- * Dorian mode: offsets {0,2,3,5,7,9,10}.
47
- *
48
- * @category Scales
49
- */
50
- export const DORIAN_MASK = maskFromOffsets([0, 2, 3, 5, 7, 9, 10]);
51
-
52
- /**
53
- * Phrygian mode: offsets {0,1,3,5,7,8,10}.
54
- *
55
- * @category Scales
56
- */
57
- export const PHRYGIAN_MASK = maskFromOffsets([0, 1, 3, 5, 7, 8, 10]);
58
-
59
- /**
60
- * Lydian mode: offsets {0,2,4,6,7,9,11}.
61
- *
62
- * @category Scales
63
- */
64
- export const LYDIAN_MASK = maskFromOffsets([0, 2, 4, 6, 7, 9, 11]);
65
-
66
- /**
67
- * Mixolydian mode: offsets {0,2,4,5,7,9,10}.
68
- *
69
- * @category Scales
70
- */
71
- export const MIXOLYDIAN_MASK = maskFromOffsets([0, 2, 4, 5, 7, 9, 10]);
72
-
73
- /**
74
- * Locrian mode: offsets {0,1,3,5,6,8,10}.
75
- *
76
- * @category Scales
77
- */
78
- export const LOCRIAN_MASK = maskFromOffsets([0, 1, 3, 5, 6, 8, 10]);
79
-
80
- /**
81
- * Major pentatonic: offsets {0,2,4,7,9}.
82
- *
83
- * @category Scales
84
- */
85
- export const MAJOR_PENTATONIC_MASK = maskFromOffsets([0, 2, 4, 7, 9]);
86
-
87
- /**
88
- * Minor pentatonic: offsets {0,3,5,7,10}.
89
- *
90
- * @category Scales
91
- */
92
- export const MINOR_PENTATONIC_MASK = maskFromOffsets([0, 3, 5, 7, 10]);
93
-
94
- /**
95
- * Hexatonic blues scale: minor pentatonic plus the flat-fifth — {0,3,5,6,7,10}.
96
- *
97
- * @category Scales
98
- */
99
- export const BLUES_MASK = maskFromOffsets([0, 3, 5, 6, 7, 10]);
100
-
101
- /**
102
- * Whole-tone scale: offsets {0,2,4,6,8,10}.
103
- *
104
- * @category Scales
105
- */
106
- export const WHOLE_TONE_MASK = maskFromOffsets([0, 2, 4, 6, 8, 10]);
107
-
108
- /**
109
- * Octatonic (half-step first): offsets {0,1,3,4,6,7,9,10}.
110
- *
111
- * @category Scales
112
- */
113
- export const OCTATONIC_HALF_WHOLE_MASK = maskFromOffsets([0, 1, 3, 4, 6, 7, 9, 10]);
114
-
115
- /**
116
- * Octatonic (whole-step first): offsets {0,2,3,5,6,8,9,11}.
117
- *
118
- * @category Scales
119
- */
120
- export const OCTATONIC_WHOLE_HALF_MASK = maskFromOffsets([0, 2, 3, 5, 6, 8, 9, 11]);
121
-
122
- /**
123
- * Chromatic scale: all twelve pitch classes.
124
- *
125
- * @category Scales
126
- */
127
- export const CHROMATIC_MASK = 0b111111111111;
128
-
129
- /**
130
- * Named scale masks addressable by {@link scaleByName}.
131
- *
132
- * @category Scales
133
- */
134
- export const NAMED_SCALES: Record<string, number> = {
135
- major: MAJOR_MASK,
136
- ionian: MAJOR_MASK,
137
- naturalMinor: NATURAL_MINOR_MASK,
138
- aeolian: NATURAL_MINOR_MASK,
139
- harmonicMinor: HARMONIC_MINOR_MASK,
140
- melodicMinor: MELODIC_MINOR_MASK,
141
- dorian: DORIAN_MASK,
142
- phrygian: PHRYGIAN_MASK,
143
- lydian: LYDIAN_MASK,
144
- mixolydian: MIXOLYDIAN_MASK,
145
- locrian: LOCRIAN_MASK,
146
- majorPentatonic: MAJOR_PENTATONIC_MASK,
147
- minorPentatonic: MINOR_PENTATONIC_MASK,
148
- blues: BLUES_MASK,
149
- wholeTone: WHOLE_TONE_MASK,
150
- octatonicHalfWhole: OCTATONIC_HALF_WHOLE_MASK,
151
- octatonicWholeHalf: OCTATONIC_WHOLE_HALF_MASK,
152
- chromatic: CHROMATIC_MASK,
153
- };
@@ -1,182 +0,0 @@
1
- /**
2
- * Spelling: derive letter-name notes for scales and chords from a spelled tonic.
3
- *
4
- * The theory core works in pitch classes, which cannot choose between (say) a
5
- * G# and an Ab. Given a spelled tonic, this module assigns diatonic letters to
6
- * scale degrees and chord tones so a C major scale spells as C D E F G A B and
7
- * A harmonic minor spells its seventh as G#.
8
- */
9
-
10
- import type { Note } from '../../core/pitch/index.js';
11
- import { formatNote } from '../../core/pitch/index.js';
12
- import type { KeyScale } from '../../core/types.js';
13
- import type { Chord } from '../chord/index.js';
14
- import { scaleTonesInDegreeOrder } from '../scale/index.js';
15
-
16
- /** Semitone offset of each natural letter above C: C D E F G A B. */
17
- const LETTER_SEMITONES = [0, 2, 4, 5, 7, 9, 11] as const;
18
-
19
- /**
20
- * Conventional degree assignment for each chromatic offset above the tonic.
21
- *
22
- * The letter is derived from the tonic's letter plus `degreeOffset` (so the
23
- * spelling follows the key's diatonic letters), and the accidental is recomputed
24
- * for that letter. This covers the flat-side alterations (b2, b3, b6, b7), the
25
- * raised fourth (#4), and the raised degrees of a minor key: the raised sixth
26
- * (#6) and the raised leading tone (#7). The latter two keep sharp-side minor
27
- * keys spelling their raised degrees with the key-correct letter (e.g. F# minor
28
- * spells its leading tone E#, not F).
29
- *
30
- * The `alter` field is illustrative only; `spellPitchClass` recomputes the
31
- * accidental from the chosen letter.
32
- */
33
- const CHROMATIC_SPELLING: Record<number, { degreeOffset: number; alter: number }> = {
34
- 1: { degreeOffset: 1, alter: -1 }, // b2
35
- 3: { degreeOffset: 2, alter: -1 }, // b3
36
- 6: { degreeOffset: 3, alter: 1 }, // #4
37
- 8: { degreeOffset: 5, alter: -1 }, // b6
38
- 9: { degreeOffset: 5, alter: 1 }, // #6 (raised sixth of a minor key)
39
- 10: { degreeOffset: 6, alter: -1 }, // b7
40
- 11: { degreeOffset: 6, alter: 1 }, // #7 (raised leading tone of a minor key)
41
- };
42
-
43
- function mod12(n: number): number {
44
- return ((n % 12) + 12) % 12;
45
- }
46
-
47
- function mod7(n: number): number {
48
- return ((n % 7) + 7) % 7;
49
- }
50
-
51
- /** Natural pitch class of a letter (0 = C .. 6 = B). */
52
- function naturalPc(letter: number): number {
53
- return LETTER_SEMITONES[mod7(letter)] ?? 0;
54
- }
55
-
56
- /** Shortest signed alteration (in [-6, 6]) taking a letter's natural pc to `pc`. */
57
- function alterFor(letter: number, pc: number): number {
58
- let d = mod12(pc - naturalPc(letter));
59
- if (d > 6) {
60
- d -= 12;
61
- }
62
- return d;
63
- }
64
-
65
- /** Whether the key's scale is a seven-note (heptatonic) scale. */
66
- function isHeptatonic(key: KeyScale): boolean {
67
- return scaleTonesInDegreeOrder(key).length === 7;
68
- }
69
-
70
- /**
71
- * Spell a single pitch class relative to a spelled tonic and key.
72
- *
73
- * Diatonic pitch classes take the scale's letter for their degree; the common
74
- * chromatic tones (b2, b3, #4, b6, b7) and a minor key's raised degrees (#6, #7)
75
- * take their conventional, key-correct accidental spelling; anything else falls
76
- * back to a sharp spelling of the nearest natural.
77
- *
78
- * @param pc The pitch class to spell.
79
- * @param tonic The spelled tonic (its letter anchors the spelling).
80
- * @param key The key/scale.
81
- * @returns The spelled note (without octave).
82
- * @category Pitch & Intervals
83
- */
84
- export function spellPitchClass(pc: number, tonic: Note, key: KeyScale): Note {
85
- const tonicPc = mod12(naturalPc(tonic.letter) + tonic.alter);
86
- const offset = mod12(pc - tonicPc);
87
- const tones = scaleTonesInDegreeOrder(key);
88
-
89
- if (isHeptatonic(key)) {
90
- const degree = tones.indexOf(mod12(pc));
91
- if (degree >= 0) {
92
- const letter = mod7(tonic.letter + degree);
93
- return { letter, alter: alterFor(letter, pc) };
94
- }
95
- const chromatic = CHROMATIC_SPELLING[offset];
96
- if (chromatic) {
97
- const letter = mod7(tonic.letter + chromatic.degreeOffset);
98
- return { letter, alter: alterFor(letter, pc) };
99
- }
100
- }
101
-
102
- // Fallback: name the pitch class from the nearest natural, preferring a sharp.
103
- for (let letter = 0; letter < 7; letter += 1) {
104
- if (naturalPc(letter) === mod12(pc)) {
105
- return { letter, alter: 0 };
106
- }
107
- }
108
- const belowLetter = mod7([0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6][mod12(pc)] ?? 0);
109
- return { letter: belowLetter, alter: alterFor(belowLetter, pc) };
110
- }
111
-
112
- /**
113
- * Spell every pitch class of a scale, in ascending scale-degree order.
114
- *
115
- * Correct for heptatonic scales (each degree gets the next letter). Non-standard
116
- * scales are spelled tone-by-tone with a sharp preference.
117
- *
118
- * @param tonic The spelled tonic.
119
- * @param key The key/scale.
120
- * @returns Spelled notes, one per scale degree.
121
- * @example
122
- * ```ts
123
- * import { spellScale, noteNames, parseNote, majorKey } from '@libraz/libcantus';
124
- * noteNames(spellScale(parseNote('C'), majorKey(0)));
125
- * // ['C', 'D', 'E', 'F', 'G', 'A', 'B']
126
- * ```
127
- * @category Pitch & Intervals
128
- */
129
- export function spellScale(tonic: Note, key: KeyScale): Note[] {
130
- return scaleTonesInDegreeOrder(key).map((pc) => spellPitchClass(pc, tonic, key));
131
- }
132
-
133
- /**
134
- * Spell an arbitrary list of pitch classes relative to a key.
135
- *
136
- * @param pcs The pitch classes.
137
- * @param tonic The spelled tonic.
138
- * @param key The key/scale.
139
- * @returns Spelled notes, in input order.
140
- * @category Pitch & Intervals
141
- */
142
- export function spellPitchClasses(pcs: number[], tonic: Note, key: KeyScale): Note[] {
143
- return pcs.map((pc) => spellPitchClass(pc, tonic, key));
144
- }
145
-
146
- /**
147
- * Spell a chord's tones, in the chord's own (tertian) order, relative to a key.
148
- *
149
- * Diatonic chords spell exactly (e.g. G7 in C major -> G B D F). Chromatic chord
150
- * tones take their conventional spelling; enharmonically ambiguous altered
151
- * tensions may be spelled by the general convention rather than by chord
152
- * function.
153
- *
154
- * @param chord The chord.
155
- * @param tonic The spelled tonic of the key.
156
- * @param key The key/scale.
157
- * @returns Spelled chord tones, root first.
158
- * @example
159
- * ```ts
160
- * import { spellChord, noteNames, parseNote, majorKey, makeChord } from '@libraz/libcantus';
161
- * // G7 in C major
162
- * noteNames(spellChord(makeChord(7, 'dom7'), parseNote('C'), majorKey(0)));
163
- * // ['G', 'B', 'D', 'F']
164
- * ```
165
- * @category Pitch & Intervals
166
- */
167
- export function spellChord(chord: Chord, tonic: Note, key: KeyScale): Note[] {
168
- return chord.intervals.map((interval) =>
169
- spellPitchClass(mod12(chord.rootPc + interval), tonic, key),
170
- );
171
- }
172
-
173
- /**
174
- * Convenience: render spelled notes as letter-name strings.
175
- *
176
- * @param notes The notes.
177
- * @returns Their formatted names.
178
- * @category Pitch & Intervals
179
- */
180
- export function noteNames(notes: Note[]): string[] {
181
- return notes.map(formatNote);
182
- }
@@ -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';