@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,302 +0,0 @@
1
- /**
2
- * Meter and time signatures: bar/beat arithmetic, simple vs. compound meters,
3
- * metric-accent hierarchy, and tuplet subdivision.
4
- *
5
- * Positions and durations are measured in quarter-note beats, matching the rest
6
- * of the library's beat convention (four quarter-note beats per bar in 4/4).
7
- */
8
-
9
- import {
10
- assertFiniteNumber,
11
- assertInteger,
12
- assertPositiveInt,
13
- assertRange,
14
- assertTimeSignature,
15
- } from '../validation/index.js';
16
-
17
- /**
18
- * A time signature as a numerator over a note-value denominator.
19
- *
20
- * @category Rhythm & Meter
21
- */
22
- export type TimeSignature = {
23
- numerator: number;
24
- denominator: number;
25
- /**
26
- * Optional additive grouping of the bar's main pulses, as the felt-beat
27
- * lengths in pulses — e.g. `[2, 2, 3]` for a 2+2+3 reading of 7/8, or
28
- * `[3, 2]` for 5/8. The entries must be positive integers summing to
29
- * {@link pulsesPerBar}. When present, the head pulse of each group (other
30
- * than the downbeat) is treated as a secondary strong pulse by
31
- * {@link metricWeight}/{@link isStrongBeat}; when absent, all main pulses
32
- * are weighted equally as flat, evenly divided pulses.
33
- */
34
- grouping?: number[];
35
- };
36
-
37
- /**
38
- * A position expressed as a bar index and a quarter-note offset within the bar.
39
- *
40
- * @category Rhythm & Meter
41
- */
42
- export type BarPosition = {
43
- bar: number;
44
- /** Quarter-note offset from the start of the bar. */
45
- beat: number;
46
- };
47
-
48
- const EPS = 1e-9;
49
-
50
- /** Whether `value` is an integer multiple of `unit` (within a float tolerance). */
51
- function isMultiple(value: number, unit: number): boolean {
52
- if (unit === 0) {
53
- return false;
54
- }
55
- const ratio = value / unit;
56
- return Math.abs(ratio - Math.round(ratio)) < EPS;
57
- }
58
-
59
- /**
60
- * Parse a time signature such as `"4/4"` or `"6/8"`.
61
- *
62
- * @param text The signature text.
63
- * @returns The parsed time signature.
64
- * @throws If the text is not `n/d` with positive integers.
65
- * @example
66
- * ```ts
67
- * import { parseTimeSignature } from '@libraz/libcantus';
68
- * parseTimeSignature('6/8'); // { numerator: 6, denominator: 8 }
69
- * ```
70
- * @category Rhythm & Meter
71
- */
72
- export function parseTimeSignature(text: string): TimeSignature {
73
- const match = /^\s*(\d+)\s*\/\s*(\d+)\s*$/.exec(text);
74
- if (!match) {
75
- throw new Error(`Invalid time signature: ${text}`);
76
- }
77
- const numerator = Number.parseInt(match[1] ?? '', 10);
78
- const denominator = Number.parseInt(match[2] ?? '', 10);
79
- if (
80
- !Number.isSafeInteger(numerator) ||
81
- !Number.isSafeInteger(denominator) ||
82
- numerator <= 0 ||
83
- denominator <= 0
84
- ) {
85
- throw new Error(`Invalid time signature: ${text}`);
86
- }
87
- return { numerator, denominator };
88
- }
89
-
90
- /**
91
- * Render a time signature as `"n/d"`.
92
- *
93
- * @category Rhythm & Meter
94
- */
95
- export function formatTimeSignature(ts: TimeSignature): string {
96
- assertTimeSignature(ts);
97
- return `${ts.numerator}/${ts.denominator}`;
98
- }
99
-
100
- /**
101
- * Whether a meter is compound: its main pulses each divide into three, as in
102
- * 6/8, 9/8, 12/8, or 6/4. Compound meters are those whose numerator is a
103
- * multiple of three greater than three, independent of the denominator. Meters
104
- * like 3/8 and 3/4 (simple triples, numerator 3) are not compound.
105
- *
106
- * @param ts The time signature.
107
- * @returns True for compound meters.
108
- * @category Rhythm & Meter
109
- */
110
- export function isCompound(ts: TimeSignature): boolean {
111
- assertTimeSignature(ts);
112
- return ts.numerator % 3 === 0 && ts.numerator > 3;
113
- }
114
-
115
- /** Length of one denominator unit in quarter-note beats. */
116
- function unitBeats(ts: TimeSignature): number {
117
- return 4 / ts.denominator;
118
- }
119
-
120
- /**
121
- * Length of a bar in quarter-note beats.
122
- *
123
- * @param ts The time signature.
124
- * @returns The bar length in quarter notes.
125
- * @category Rhythm & Meter
126
- */
127
- export function beatsPerBar(ts: TimeSignature): number {
128
- assertTimeSignature(ts);
129
- return ts.numerator * unitBeats(ts);
130
- }
131
-
132
- /** Length of one main pulse (a dotted value in compound meters) in quarter notes. */
133
- function pulseBeats(ts: TimeSignature): number {
134
- return isCompound(ts) ? 3 * unitBeats(ts) : unitBeats(ts);
135
- }
136
-
137
- /**
138
- * Number of main pulses (felt beats) per bar: the numerator for simple meters,
139
- * a third of it for compound meters.
140
- *
141
- * For additive/irregular meters such as 7/8 or 5/8 this counts the raw pulses
142
- * (7 and 5 respectively), which are otherwise felt as flat, evenly divided and
143
- * equally accented pulses. To recover a 2+2+3 or 3+2 felt-beat grouping, set
144
- * {@link TimeSignature.grouping}; {@link metricWeight} then accents each
145
- * group's head pulse.
146
- *
147
- * @param ts The time signature.
148
- * @returns The pulse count per bar.
149
- * @category Rhythm & Meter
150
- */
151
- export function pulsesPerBar(ts: TimeSignature): number {
152
- return beatsPerBar(ts) / pulseBeats(ts);
153
- }
154
-
155
- /**
156
- * Whether `grouping` is a valid additive grouping for a bar of `pulses` main
157
- * pulses: a non-empty list of positive integers summing to `pulses`.
158
- */
159
- function isValidGrouping(grouping: number[], pulses: number): boolean {
160
- if (grouping.length === 0) {
161
- return false;
162
- }
163
- let sum = 0;
164
- for (const g of grouping) {
165
- if (!Number.isInteger(g) || g <= 0) {
166
- return false;
167
- }
168
- sum += g;
169
- }
170
- return sum === pulses;
171
- }
172
-
173
- /** Whether `pulseIndex` is the head pulse of one of the additive groups. */
174
- function isGroupHead(grouping: number[], pulseIndex: number): boolean {
175
- let acc = 0;
176
- for (const g of grouping) {
177
- if (acc === pulseIndex) {
178
- return true;
179
- }
180
- acc += g;
181
- }
182
- return false;
183
- }
184
-
185
- /**
186
- * Convert an absolute quarter-note position to a bar index and in-bar offset.
187
- *
188
- * @param beatInQuarters Absolute position in quarter-note beats.
189
- * @param ts The time signature.
190
- * @returns The bar and in-bar quarter-note offset.
191
- * @example
192
- * ```ts
193
- * import { parseTimeSignature, beatToBarPosition } from '@libraz/libcantus';
194
- * const ts = parseTimeSignature('4/4');
195
- * beatToBarPosition(5, ts); // { bar: 1, beat: 1 }
196
- * ```
197
- * @category Rhythm & Meter
198
- */
199
- export function beatToBarPosition(beatInQuarters: number, ts: TimeSignature): BarPosition {
200
- assertFiniteNumber(beatInQuarters, 'beat');
201
- const barLen = beatsPerBar(ts);
202
- const bar = Math.floor(beatInQuarters / barLen);
203
- return { bar, beat: beatInQuarters - bar * barLen };
204
- }
205
-
206
- /**
207
- * Convert a bar index and in-bar offset back to an absolute quarter-note
208
- * position.
209
- *
210
- * @param pos The bar position.
211
- * @param ts The time signature.
212
- * @returns The absolute position in quarter-note beats.
213
- * @category Rhythm & Meter
214
- */
215
- export function barPositionToBeat(pos: BarPosition, ts: TimeSignature): number {
216
- assertInteger(pos.bar, 'bar position bar');
217
- assertFiniteNumber(pos.beat, 'bar position beat');
218
- return pos.bar * beatsPerBar(ts) + pos.beat;
219
- }
220
-
221
- /**
222
- * Metric weight of a position within its bar, on a 0–3 scale:
223
- * 3 the downbeat, 2 a secondary strong pulse, 1 any other main pulse, and 0 an
224
- * off-pulse subdivision.
225
- *
226
- * The secondary strong pulse is the bar's midpoint in even simple/compound
227
- * meters. For an additive/irregular meter with {@link TimeSignature.grouping}
228
- * set (e.g. `[2, 2, 3]` for 7/8), each group's head pulse other than the
229
- * downbeat is the secondary strong pulse instead; without a grouping every
230
- * non-downbeat main pulse of such a meter weighs 1 (flat, evenly divided
231
- * pulses).
232
- *
233
- * @param beatInQuarters Absolute or in-bar quarter-note position.
234
- * @param ts The time signature.
235
- * @returns The metric weight (0–3).
236
- * @throws If `ts.grouping` is present but is not a positive-integer list
237
- * summing to {@link pulsesPerBar}.
238
- * @example
239
- * ```ts
240
- * import { parseTimeSignature, metricWeight } from '@libraz/libcantus';
241
- * const ts = parseTimeSignature('4/4');
242
- * metricWeight(0, ts); // 3 (the downbeat)
243
- * ```
244
- * @category Rhythm & Meter
245
- */
246
- export function metricWeight(beatInQuarters: number, ts: TimeSignature): number {
247
- assertFiniteNumber(beatInQuarters, 'beat');
248
- assertTimeSignature(ts);
249
- const { beat } = beatToBarPosition(beatInQuarters, ts);
250
- const pulse = pulseBeats(ts);
251
- if (!isMultiple(beat, pulse)) {
252
- return 0;
253
- }
254
- const pulses = pulsesPerBar(ts);
255
- const pulseIndex = Math.round(beat / pulse) % pulses;
256
- if (pulseIndex === 0) {
257
- return 3;
258
- }
259
- const grouping = ts.grouping;
260
- if (grouping !== undefined) {
261
- if (!isValidGrouping(grouping, pulses)) {
262
- throw new Error(
263
- `Invalid grouping [${grouping.join(', ')}] for ${formatTimeSignature(ts)}: ` +
264
- `entries must be positive integers summing to ${pulses}`,
265
- );
266
- }
267
- return isGroupHead(grouping, pulseIndex) ? 2 : 1;
268
- }
269
- if (pulses % 2 === 0 && pulseIndex === pulses / 2) {
270
- return 2;
271
- }
272
- return 1;
273
- }
274
-
275
- /**
276
- * Whether a position is metrically accented (weight 2 or more — a downbeat or a
277
- * secondary strong pulse).
278
- *
279
- * @param beatInQuarters Absolute or in-bar quarter-note position.
280
- * @param ts The time signature.
281
- * @returns True on strong beats.
282
- * @category Rhythm & Meter
283
- */
284
- export function isStrongBeat(beatInQuarters: number, ts: TimeSignature): boolean {
285
- return metricWeight(beatInQuarters, ts) >= 2;
286
- }
287
-
288
- /**
289
- * Subdivide a span into `count` equal tuplet durations (e.g. an eighth-note
290
- * triplet is `tuplet(1, 3)`).
291
- *
292
- * @param totalBeats Total span in quarter-note beats.
293
- * @param count Number of equal parts.
294
- * @returns `count` equal durations summing to `totalBeats`.
295
- * @throws If `count` is not a positive integer.
296
- * @category Rhythm & Meter
297
- */
298
- export function tuplet(totalBeats: number, count: number): number[] {
299
- assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, 'tuplet total beats');
300
- assertPositiveInt(count, 'tuplet count');
301
- return new Array<number>(count).fill(totalBeats / count);
302
- }
@@ -1,311 +0,0 @@
1
- /**
2
- * Pitch spelling: letter-name notes, enharmonic-aware conversion, and spelled
3
- * intervals.
4
- *
5
- * The rest of the library works in pitch classes (0..11), which cannot tell a
6
- * G# from an Ab or an augmented fourth from a diminished fifth. This module adds
7
- * an explicit spelling layer — a note is a diatonic letter plus a chromatic
8
- * alteration — so interval quality (P/M/m/A/d) and enharmonic identity are
9
- * preserved.
10
- */
11
-
12
- /** Semitone offset of each natural letter above C: C D E F G A B. */
13
- const LETTER_SEMITONES = [0, 2, 4, 5, 7, 9, 11] as const;
14
-
15
- /** Letter glyphs indexed by letter number (0 = C .. 6 = B). */
16
- const LETTER_NAMES = ['C', 'D', 'E', 'F', 'G', 'A', 'B'] as const;
17
-
18
- /**
19
- * A spelled note: a diatonic letter, a chromatic alteration, and an optional
20
- * octave.
21
- *
22
- * - `letter` is 0..6 for C..B.
23
- * - `alter` is the number of semitones of alteration: -2 double-flat, -1 flat,
24
- * 0 natural, +1 sharp, +2 double-sharp.
25
- * - `octave` follows scientific pitch notation (middle C = C4 = MIDI 60). When
26
- * absent, the note denotes a bare pitch class.
27
- *
28
- * @category Pitch & Intervals
29
- */
30
- export type Note = {
31
- letter: number;
32
- alter: number;
33
- octave?: number;
34
- };
35
-
36
- /**
37
- * A spelled interval: a diatonic number, a quality label, and its semitone span.
38
- *
39
- * @category Pitch & Intervals
40
- */
41
- export type SpelledInterval = {
42
- /** Diatonic size: 1 = unison, 2 = second, ... 8 = octave, and beyond. */
43
- number: number;
44
- /** Quality label: 'P', 'M', 'm', or repeated 'A'/'d' for (multiply) aug/dim. */
45
- quality: string;
46
- /** Signed semitone distance from the first note to the second. */
47
- semitones: number;
48
- };
49
-
50
- /** Reduce any integer to a pitch class in [0, 11]. */
51
- function mod12(n: number): number {
52
- return ((n % 12) + 12) % 12;
53
- }
54
-
55
- /** Reduce any integer to [0, 7). */
56
- function mod7(n: number): number {
57
- return ((n % 7) + 7) % 7;
58
- }
59
-
60
- /**
61
- * Parse scientific pitch notation into a {@link Note}.
62
- *
63
- * Accepts a letter (case-insensitive) followed by any number of same-direction
64
- * accidentals and an optional octave, e.g. `"C"`, `"C#4"`, `"Bb"`, `"F##3"`,
65
- * `"Ebb2"`. Both `#` and `x` (double-sharp) are accepted for sharps; `b` for
66
- * flats. Mixing sharps and flats (e.g. `"C#b"`) is rejected.
67
- *
68
- * @param text The note text.
69
- * @returns The parsed note.
70
- * @throws If the text is not a valid note.
71
- * @example
72
- * ```ts
73
- * import { parseNote, noteToMidi } from '@libraz/libcantus';
74
- * const n = parseNote('C#4');
75
- * noteToMidi(n); // 61
76
- * ```
77
- * @category Pitch & Intervals
78
- */
79
- export function parseNote(text: string): Note {
80
- const match = /^([A-Ga-g])([#x]*|b*)(-?\d+)?$/.exec(text.trim());
81
- if (!match) {
82
- throw new Error(`Invalid note: ${text}`);
83
- }
84
- const letterGlyph = (match[1] ?? '').toUpperCase();
85
- const letter = LETTER_NAMES.indexOf(letterGlyph as (typeof LETTER_NAMES)[number]);
86
- let alter = 0;
87
- for (const ch of match[2] ?? '') {
88
- if (ch === '#') {
89
- alter += 1;
90
- } else if (ch === 'x') {
91
- alter += 2;
92
- } else if (ch === 'b') {
93
- alter -= 1;
94
- }
95
- }
96
- const note: Note = { letter, alter };
97
- if (match[3] !== undefined) {
98
- note.octave = Number.parseInt(match[3], 10);
99
- }
100
- return note;
101
- }
102
-
103
- /** Render an alteration as accidental glyphs (`##`, `b`, empty for natural). */
104
- function formatAlter(alter: number): string {
105
- if (alter > 0) {
106
- return '#'.repeat(alter);
107
- }
108
- if (alter < 0) {
109
- return 'b'.repeat(-alter);
110
- }
111
- return '';
112
- }
113
-
114
- /**
115
- * Render a {@link Note} as scientific pitch notation.
116
- *
117
- * @param note The note to format.
118
- * @returns The note text, including the octave when present.
119
- * @example
120
- * ```ts
121
- * import { formatNote, parseNote } from '@libraz/libcantus';
122
- * formatNote(parseNote('C#4')); // 'C#4'
123
- * ```
124
- * @category Pitch & Intervals
125
- */
126
- export function formatNote(note: Note): string {
127
- const glyph = LETTER_NAMES[mod7(note.letter)] ?? 'C';
128
- const octave = note.octave === undefined ? '' : String(note.octave);
129
- return `${glyph}${formatAlter(note.alter)}${octave}`;
130
- }
131
-
132
- /**
133
- * Pitch class (0..11) of a note, ignoring octave.
134
- *
135
- * @param note The note.
136
- * @returns The pitch class.
137
- * @example
138
- * ```ts
139
- * import { noteToPitchClass, parseNote } from '@libraz/libcantus';
140
- * noteToPitchClass(parseNote('Db')); // 1
141
- * ```
142
- * @category Pitch & Intervals
143
- */
144
- export function noteToPitchClass(note: Note): number {
145
- const natural = LETTER_SEMITONES[mod7(note.letter)] ?? 0;
146
- return mod12(natural + note.alter);
147
- }
148
-
149
- /**
150
- * MIDI number of a note (middle C = C4 = 60).
151
- *
152
- * @param note The note; must carry an octave.
153
- * @returns The MIDI number.
154
- * @throws If the note has no octave.
155
- * @example
156
- * ```ts
157
- * import { noteToMidi, parseNote } from '@libraz/libcantus';
158
- * noteToMidi(parseNote('A4')); // 69
159
- * ```
160
- * @category Pitch & Intervals
161
- */
162
- export function noteToMidi(note: Note): number {
163
- if (note.octave === undefined) {
164
- throw new Error('noteToMidi requires an octave');
165
- }
166
- const natural = LETTER_SEMITONES[mod7(note.letter)] ?? 0;
167
- return (note.octave + 1) * 12 + natural + note.alter;
168
- }
169
-
170
- /** Preferred spelling when naming a black key from a bare MIDI number. */
171
- const SHARP_SPELLING: readonly [number, number][] = [
172
- [0, 0],
173
- [0, 1],
174
- [1, 0],
175
- [1, 1],
176
- [2, 0],
177
- [3, 0],
178
- [3, 1],
179
- [4, 0],
180
- [4, 1],
181
- [5, 0],
182
- [5, 1],
183
- [6, 0],
184
- ];
185
-
186
- const FLAT_SPELLING: readonly [number, number][] = [
187
- [0, 0],
188
- [1, -1],
189
- [1, 0],
190
- [2, -1],
191
- [2, 0],
192
- [3, 0],
193
- [4, -1],
194
- [4, 0],
195
- [5, -1],
196
- [5, 0],
197
- [6, -1],
198
- [6, 0],
199
- ];
200
-
201
- /**
202
- * Name a MIDI number as a {@link Note}, choosing sharp or flat spelling.
203
- *
204
- * The input is rounded to the nearest integer but is not clamped to the MIDI
205
- * range 0..127: out-of-range values extrapolate linearly (e.g. `-1` -> `B-2`,
206
- * `128` -> `G#9`) and remain an exact inverse of {@link noteToMidi}.
207
- *
208
- * @param midi The MIDI number.
209
- * @param spelling Whether to prefer sharps or flats for black keys.
210
- * @returns The spelled note, with octave.
211
- * @example
212
- * ```ts
213
- * import { midiToNote, formatNote } from '@libraz/libcantus';
214
- * formatNote(midiToNote(61)); // 'C#4'
215
- * formatNote(midiToNote(61, 'flat')); // 'Db4'
216
- * ```
217
- * @category Pitch & Intervals
218
- */
219
- export function midiToNote(midi: number, spelling: 'sharp' | 'flat' = 'sharp'): Note {
220
- const rounded = Math.round(midi);
221
- const pc = mod12(rounded);
222
- const octave = Math.floor(rounded / 12) - 1;
223
- const table = spelling === 'sharp' ? SHARP_SPELLING : FLAT_SPELLING;
224
- const entry = table[pc] ?? [0, 0];
225
- return { letter: entry[0], alter: entry[1], octave };
226
- }
227
-
228
- /** Reference semitone span of a perfect/major simple interval by diatonic number. */
229
- const SIMPLE_REFERENCE = [0, 0, 2, 4, 5, 7, 9, 11] as const;
230
-
231
- /** Whether a simple diatonic number (1..7 within an octave) is a perfect kind. */
232
- function isPerfectNumber(simpleNumber: number): boolean {
233
- return simpleNumber === 1 || simpleNumber === 4 || simpleNumber === 5;
234
- }
235
-
236
- /** Quality label from a diatonic number and its actual semitone span. */
237
- function qualityFromSpan(numberValue: number, semitones: number): string {
238
- const octaves = Math.floor((numberValue - 1) / 7);
239
- const simple = numberValue - 7 * octaves;
240
- const reference = (SIMPLE_REFERENCE[simple] ?? 0) + 12 * octaves;
241
- const delta = semitones - reference;
242
- if (isPerfectNumber(simple)) {
243
- if (delta === 0) {
244
- return 'P';
245
- }
246
- return delta > 0 ? 'A'.repeat(delta) : 'd'.repeat(-delta);
247
- }
248
- if (delta === 0) {
249
- return 'M';
250
- }
251
- if (delta === -1) {
252
- return 'm';
253
- }
254
- return delta > 0 ? 'A'.repeat(delta) : 'd'.repeat(-delta - 1);
255
- }
256
-
257
- /** Diatonic ladder index of a note (letter + 7 * octave when octave-bearing). */
258
- function diatonicIndex(note: Note): number {
259
- return mod7(note.letter) + 7 * (note.octave ?? 0);
260
- }
261
-
262
- /**
263
- * The spelled interval from one note to another.
264
- *
265
- * When both notes carry octaves, the interval reflects their full signed
266
- * distance (so `C4 -> C5` is a perfect octave). When either lacks an octave the
267
- * interval is measured within a single ascending octave from `a` to `b`.
268
- *
269
- * @param a The lower/first note.
270
- * @param b The upper/second note.
271
- * @returns The diatonic number, quality, and semitone span.
272
- * @example
273
- * ```ts
274
- * import { spelledInterval, parseNote } from '@libraz/libcantus';
275
- * spelledInterval(parseNote('C4'), parseNote('G4'));
276
- * // { number: 5, quality: 'P', semitones: 7 }
277
- * ```
278
- * @category Pitch & Intervals
279
- */
280
- export function spelledInterval(a: Note, b: Note): SpelledInterval {
281
- const octaved = a.octave !== undefined && b.octave !== undefined;
282
- let letterSteps: number;
283
- let semitones: number;
284
- if (octaved) {
285
- letterSteps = diatonicIndex(b) - diatonicIndex(a);
286
- // Signed pitch distance from a to b, so a note below a yields a negative span.
287
- semitones = noteToMidi(b) - noteToMidi(a);
288
- } else {
289
- letterSteps = mod7(b.letter - a.letter);
290
- let rawSemis = mod12(noteToPitchClass(b) - noteToPitchClass(a));
291
- // Lift the chromatic span into the octave nearest the diatonic reference so
292
- // wraparound intervals stay consistent with the diatonic number instead of
293
- // collapsing modulo 12. The correction is bidirectional: an upward wrap
294
- // (e.g. Ab -> G# = augmented seventh) adds an octave, while a same-letter or
295
- // downward chromatic step (e.g. E -> Eb, F# -> F, C -> Cb = descending
296
- // diminished/augmented unison) subtracts one so the sign follows pitch
297
- // direction instead of returning a garbage stack of augmentations.
298
- const reference = SIMPLE_REFERENCE[letterSteps + 1] ?? 0;
299
- if (reference - rawSemis > 6) {
300
- rawSemis += 12;
301
- } else if (rawSemis - reference > 6) {
302
- rawSemis -= 12;
303
- }
304
- semitones = rawSemis;
305
- }
306
- const absSteps = Math.abs(letterSteps);
307
- const number = absSteps + 1;
308
- // Quality comes from the absolute chromatic span; semitones keeps its sign.
309
- const quality = qualityFromSpan(number, Math.abs(semitones));
310
- return { number, quality, semitones };
311
- }
@@ -1,76 +0,0 @@
1
- /**
2
- * Deterministic seeded pseudo-random number generation shared across the
3
- * generative modules (rhythm, drums, bass, groove, counter-melody). A single
4
- * mulberry32 core exposes the small set of samplers those modules need; given a
5
- * seed the stream is fully reproducible.
6
- */
7
-
8
- import { assertFiniteNumber, assertInteger, assertRange } from '../validation/index.js';
9
-
10
- /**
11
- * A deterministic PRNG with the sampling helpers the generators need.
12
- *
13
- * @category Utilities
14
- */
15
- export type Rng = {
16
- /** Next float in [0, 1). */
17
- next: () => number;
18
- /** True with probability `p`. */
19
- prob: (p: number) => boolean;
20
- /** Integer in the inclusive range [lo, hi]. */
21
- range: (lo: number, hi: number) => number;
22
- /** Float in [lo, hi). */
23
- float: (lo: number, hi: number) => number;
24
- };
25
-
26
- /**
27
- * Create a seeded PRNG (mulberry32) exposing the shared samplers.
28
- *
29
- * @param seed The 32-bit seed; the same seed always yields the same stream.
30
- * @returns The seeded generator.
31
- * @example
32
- * ```ts
33
- * import { createRng } from '@libraz/libcantus';
34
- * const rng = createRng(42);
35
- * rng.next(); // deterministic float in [0, 1)
36
- * rng.range(1, 6); // deterministic integer in [1, 6]
37
- * ```
38
- * @category Utilities
39
- */
40
- export function createRng(seed: number): Rng {
41
- assertFiniteNumber(seed, 'seed');
42
- let state = seed >>> 0;
43
- const next = () => {
44
- state = (state + 0x6d2b79f5) >>> 0;
45
- let t = state;
46
- t = Math.imul(t ^ (t >>> 15), t | 1);
47
- t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
48
- return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
49
- };
50
- return {
51
- next,
52
- prob: (p) => next() < assertRange(p, 0, 1, 'probability'),
53
- range: (lo, hi) => {
54
- assertInteger(lo, 'range lower bound');
55
- assertInteger(hi, 'range upper bound');
56
- if (lo > hi) {
57
- throw new RangeError(
58
- `range lower bound must not exceed upper bound; received ${lo} > ${hi}`,
59
- );
60
- }
61
- assertInteger(hi - lo, 'range span', 0);
62
- return lo + Math.floor(next() * (hi - lo + 1));
63
- },
64
- float: (lo, hi) => {
65
- assertFiniteNumber(lo, 'float lower bound');
66
- assertFiniteNumber(hi, 'float upper bound');
67
- if (lo > hi) {
68
- throw new RangeError(
69
- `float lower bound must not exceed upper bound; received ${lo} > ${hi}`,
70
- );
71
- }
72
- assertFiniteNumber(hi - lo, 'float span');
73
- return lo + next() * (hi - lo);
74
- },
75
- };
76
- }