@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,285 +0,0 @@
1
- import type { DrumRole, DrumStyle, SectionType } from './internal.js';
2
- import { GM } from './internal.js';
3
- import type { DrumRng } from './rng.js';
4
-
5
- /** Hi-hat subdivision level. */
6
- export type HiHatLevel = 'quarter' | 'eighth' | 'sixteenth';
7
-
8
- /** Hi-hat articulation type. */
9
- export type HiHatType = 'closed' | 'pedal' | 'open' | 'halfOpen' | 'ride';
10
-
11
- /** BPM at or above which 16th-note hi-hats become 8ths for playability. */
12
- export const HH_16TH_BPM_THRESHOLD = 150;
13
-
14
- const FHH_VEL_MIN = 45;
15
- const FHH_VEL_MAX = 60;
16
- /** Velocity boost applied to dynamic open hi-hat accents. */
17
- export const OHH_VEL_BOOST = 7;
18
-
19
- /** True unless the role suppresses timekeeping cymbals entirely. */
20
- export function shouldPlayHiHat(role: DrumRole): boolean {
21
- return role !== 'fxOnly';
22
- }
23
-
24
- /** Preferred timekeeping instrument for a role. */
25
- export function roleHiHatInstrument(role: DrumRole, useRide: boolean): number {
26
- if (role === 'ambient') {
27
- return GM.RIDE;
28
- }
29
- return useRide ? GM.RIDE : GM.CHH;
30
- }
31
-
32
- function sparser(level: HiHatLevel): HiHatLevel {
33
- if (level === 'sixteenth') {
34
- return 'eighth';
35
- }
36
- if (level === 'eighth') {
37
- return 'quarter';
38
- }
39
- return 'quarter';
40
- }
41
-
42
- function denser(level: HiHatLevel): HiHatLevel {
43
- if (level === 'quarter') {
44
- return 'eighth';
45
- }
46
- if (level === 'eighth') {
47
- return 'sixteenth';
48
- }
49
- return 'sixteenth';
50
- }
51
-
52
- /** Choose the hi-hat subdivision for a section/style/density/BPM. */
53
- export function getHiHatLevel(
54
- section: SectionType,
55
- style: DrumStyle,
56
- backingDensity: 'thin' | 'normal' | 'thick',
57
- bpm: number,
58
- rng: DrumRng,
59
- ): HiHatLevel {
60
- const allow16th = bpm < HH_16TH_BPM_THRESHOLD;
61
- let base: HiHatLevel = 'eighth';
62
-
63
- if (style === 'sparse') {
64
- base = section === 'chorus' ? 'eighth' : 'quarter';
65
- } else if (style === 'fourOnFloor') {
66
- if (allow16th && section === 'chorus' && rng.prob(0.25)) {
67
- return 'sixteenth';
68
- }
69
- return 'eighth';
70
- } else if (style === 'synth') {
71
- if (!allow16th) {
72
- return 'eighth';
73
- }
74
- if (section === 'a' && rng.prob(0.2)) {
75
- return 'eighth';
76
- }
77
- return 'sixteenth';
78
- } else if (style === 'trap') {
79
- return allow16th ? 'sixteenth' : 'eighth';
80
- } else if (style === 'latin') {
81
- if (allow16th && section === 'chorus' && rng.prob(0.3)) {
82
- return 'sixteenth';
83
- }
84
- return 'eighth';
85
- } else {
86
- switch (section) {
87
- case 'intro':
88
- base = 'quarter';
89
- break;
90
- case 'outro':
91
- base = 'eighth';
92
- break;
93
- case 'a':
94
- base = rng.prob(0.3) ? 'quarter' : 'eighth';
95
- break;
96
- case 'b':
97
- base = allow16th && rng.prob(0.25) ? 'sixteenth' : 'eighth';
98
- break;
99
- case 'chorus':
100
- base = allow16th && rng.prob(0.35) ? 'sixteenth' : 'eighth';
101
- break;
102
- case 'bridge':
103
- base = 'eighth';
104
- break;
105
- }
106
- }
107
-
108
- if (backingDensity === 'thin') {
109
- base = sparser(base);
110
- } else if (backingDensity === 'thick') {
111
- base = denser(base);
112
- }
113
-
114
- if (!allow16th && base === 'sixteenth') {
115
- base = 'eighth';
116
- }
117
- return base;
118
- }
119
-
120
- /** Metric velocity multiplier for a 16th position within a beat. */
121
- export function hiHatVelocityMultiplier(sixteenth: number, rng: DrumRng): number {
122
- let baseValue: number;
123
- switch (sixteenth) {
124
- case 0:
125
- baseValue = 0.95;
126
- break;
127
- case 2:
128
- baseValue = 0.75;
129
- break;
130
- case 1:
131
- baseValue = 0.55;
132
- break;
133
- default:
134
- baseValue = 0.5;
135
- break;
136
- }
137
- return baseValue * rng.float(0.95, 1.05);
138
- }
139
-
140
- /** Bars between dynamic open hi-hat accents (0 disables them). */
141
- export function openHiHatBarInterval(section: SectionType, style: DrumStyle): number {
142
- if (style === 'sparse') {
143
- return section === 'chorus' ? 4 : 0;
144
- }
145
- switch (section) {
146
- case 'intro':
147
- return style === 'fourOnFloor' ? 4 : 0;
148
- case 'a':
149
- return style === 'fourOnFloor' || style === 'upbeat' ? 2 : 4;
150
- case 'b':
151
- return 2;
152
- case 'chorus':
153
- return style === 'rock' || style === 'fourOnFloor' ? 1 : 2;
154
- case 'bridge':
155
- return 0;
156
- case 'outro':
157
- return 4;
158
- }
159
- }
160
-
161
- /** Beat that receives the dynamic open hi-hat within a bar. */
162
- export function openHiHatBeat(section: SectionType, rng: DrumRng): number {
163
- if (section === 'chorus') {
164
- const choice = rng.range(0, 3);
165
- if (choice < 2) {
166
- return 3;
167
- }
168
- if (choice < 3) {
169
- return 1;
170
- }
171
- return 2;
172
- }
173
- return 3;
174
- }
175
-
176
- /** Whether the section uses an independent foot hi-hat pulse. */
177
- export function shouldUseFootHiHat(section: SectionType, role: DrumRole): boolean {
178
- if (role === 'fxOnly') {
179
- return false;
180
- }
181
- switch (section) {
182
- case 'intro':
183
- case 'bridge':
184
- case 'outro':
185
- return true;
186
- default:
187
- return role === 'ambient' || role === 'minimal';
188
- }
189
- }
190
-
191
- /** Primary hi-hat articulation for a section. */
192
- export function sectionHiHatType(section: SectionType, role: DrumRole): HiHatType {
193
- if (role === 'ambient') {
194
- return 'ride';
195
- }
196
- if (role === 'minimal') {
197
- return 'pedal';
198
- }
199
- switch (section) {
200
- case 'intro':
201
- case 'a':
202
- return 'pedal';
203
- case 'b':
204
- return 'closed';
205
- case 'chorus':
206
- return 'open';
207
- case 'bridge':
208
- return 'ride';
209
- case 'outro':
210
- return 'halfOpen';
211
- }
212
- }
213
-
214
- /** GM note for a hi-hat articulation. */
215
- export function hiHatNote(type: HiHatType): number {
216
- switch (type) {
217
- case 'pedal':
218
- return GM.FHH;
219
- case 'open':
220
- return GM.OHH;
221
- case 'ride':
222
- return GM.RIDE;
223
- default:
224
- return GM.CHH;
225
- }
226
- }
227
-
228
- /** Velocity multiplier for a hi-hat articulation. */
229
- export function hiHatTypeVelocityMultiplier(type: HiHatType): number {
230
- switch (type) {
231
- case 'halfOpen':
232
- return 0.75;
233
- case 'pedal':
234
- return 0.65;
235
- case 'open':
236
- return 1.0;
237
- case 'ride':
238
- return 0.9;
239
- default:
240
- return 0.85;
241
- }
242
- }
243
-
244
- /** Whether to accent with an open hi-hat at a given beat. */
245
- export function shouldAddOpenHHAccent(
246
- section: SectionType,
247
- beat: number,
248
- bar: number,
249
- rng: DrumRng,
250
- ): boolean {
251
- if (section !== 'chorus' && section !== 'b') {
252
- return false;
253
- }
254
- if (section === 'chorus') {
255
- if (beat === 1 || beat === 3) {
256
- return rng.prob(0.6);
257
- }
258
- return false;
259
- }
260
- if (beat === 3 && bar % 2 === 1) {
261
- return rng.prob(0.4);
262
- }
263
- return false;
264
- }
265
-
266
- /** Foot hi-hat velocity with slight humanization. */
267
- export function footHiHatVelocity(rng: DrumRng): number {
268
- return rng.range(FHH_VEL_MIN, FHH_VEL_MAX);
269
- }
270
-
271
- /** Whether the section uses a ride cymbal instead of hi-hats. */
272
- export function shouldUseRideForSection(section: SectionType, style: DrumStyle): boolean {
273
- if (style === 'rock' && section === 'chorus') {
274
- return true;
275
- }
276
- if (style === 'sparse') {
277
- return false;
278
- }
279
- return section === 'bridge';
280
- }
281
-
282
- /** Whether a bridge beat is carried by a cross-stick instead of the hi-hat. */
283
- export function shouldUseBridgeCrossStick(section: SectionType, beat: number): boolean {
284
- return section === 'bridge' && (beat === 1 || beat === 3);
285
- }
@@ -1,31 +0,0 @@
1
- import { clampVel, GM } from './internal.js';
2
-
3
- /** A single percussion onset. */
4
- export type DrumHit = {
5
- pitch: number;
6
- startBeat: number;
7
- durationBeat: number;
8
- velocity: number;
9
- };
10
-
11
- /** Accumulator for drum onsets with velocity clamping and simple lookups. */
12
- export class HitList {
13
- readonly hits: DrumHit[] = [];
14
-
15
- /** Append a hit, clamping velocity and dropping non-positive positions. */
16
- add(pitch: number, startBeat: number, durationBeat: number, velocity: number): void {
17
- this.hits.push({
18
- pitch,
19
- startBeat: Math.max(0, startBeat),
20
- durationBeat,
21
- velocity: clampVel(velocity),
22
- });
23
- }
24
-
25
- /** True when a crash already sits within a 16th of `startBeat`. */
26
- hasCrashNear(startBeat: number): boolean {
27
- return this.hits.some(
28
- (h) => h.pitch === GM.CRASH && h.startBeat >= startBeat && h.startBeat < startBeat + 0.25,
29
- );
30
- }
31
- }
@@ -1,329 +0,0 @@
1
- import {
2
- type BeatCtx,
3
- generateGhostNotesForBeat,
4
- generateHiHatForBeat,
5
- generateKickForBeat,
6
- generatePreChorusBuildup,
7
- generateSnareForBeat,
8
- type SectionCtx,
9
- } from './beat.js';
10
- import { euclideanRhythm, patternToMask } from './euclid.js';
11
- import { type FillType, generateFill, getFillStartBeat, selectFillType } from './fills.js';
12
- import {
13
- footHiHatVelocity,
14
- getHiHatLevel,
15
- openHiHatBarInterval,
16
- openHiHatBeat,
17
- shouldPlayHiHat,
18
- shouldUseFootHiHat,
19
- shouldUseRideForSection,
20
- } from './hihat.js';
21
- import { type DrumHit, HitList } from './hit.js';
22
- import {
23
- calculateVelocity,
24
- type DrumRole,
25
- type Feel,
26
- feelSwingAmount,
27
- GM,
28
- type GrooveStyle,
29
- ghostMoodCategory,
30
- mapDensity,
31
- mapSection,
32
- mapStyle,
33
- type PublicSection,
34
- percMoodCategory,
35
- sectionDensityMultiplier,
36
- sectionEnergy,
37
- } from './internal.js';
38
- import {
39
- euclideanToKickPattern,
40
- getKickPattern,
41
- isInPreChorusLift,
42
- type KickPattern,
43
- } from './kick.js';
44
- import { generateAuxPercussionForBar, getPercussionConfig } from './percussion.js';
45
- import { createRng } from './rng.js';
46
-
47
- /**
48
- * A single drum onset emitted by {@link generateDrums}.
49
- *
50
- * @category Composition
51
- */
52
- export type { DrumHit } from './hit.js';
53
-
54
- /**
55
- * Groove feel (the swing/straight rhythmic character) for {@link generateDrums}.
56
- *
57
- * @category Composition
58
- */
59
- export type GrooveFeel = Feel;
60
-
61
- /**
62
- * Drum voicing role and groove style identifiers for {@link generateDrums}.
63
- *
64
- * @category Composition
65
- */
66
- export type { DrumRole, GrooveStyle } from './internal.js';
67
-
68
- /**
69
- * Public section identifiers for {@link generateDrums}.
70
- *
71
- * @category Composition
72
- */
73
- export type Section = PublicSection;
74
-
75
- /**
76
- * A Euclidean (Bjorklund) kick pattern for {@link generateDrums}.
77
- *
78
- * @category Composition
79
- */
80
- export type EuclideanKick = {
81
- /** Number of kick onsets, clamped to `[0, steps]`. */
82
- pulses: number;
83
- /**
84
- * Total steps in the bar (1..16).
85
- *
86
- * @defaultValue 16
87
- */
88
- steps?: number;
89
- /**
90
- * Steps to rotate the onsets toward later positions.
91
- *
92
- * @defaultValue 0
93
- */
94
- rotation?: number;
95
- };
96
-
97
- /**
98
- * Options controlling {@link generateDrums}.
99
- *
100
- * @category Composition
101
- */
102
- export type DrumsOptions = {
103
- bars: number;
104
- bpm: number;
105
- style: GrooveStyle;
106
- section: Section;
107
- density: number;
108
- /**
109
- * Replace the final bar with a fill.
110
- *
111
- * @defaultValue false
112
- */
113
- fills?: boolean;
114
- feel?: GrooveFeel;
115
- /**
116
- * Voicing role; `'fxOnly'` suppresses the main kick/snare/ghost/fill voices.
117
- *
118
- * @defaultValue `'full'`
119
- */
120
- role?: DrumRole;
121
- /**
122
- * Seed for the deterministic PRNG.
123
- *
124
- * @defaultValue 0
125
- */
126
- seed?: number;
127
- /**
128
- * Section the final-bar fill leads into. Shapes which fill archetype is
129
- * chosen (into-chorus and out-of-intro fills differ from generic ones).
130
- * Defaults to `section`, i.e. a within-section fill.
131
- */
132
- nextSection?: Section;
133
- /**
134
- * When set, the kick follows this Euclidean rhythm instead of the
135
- * style/section pattern, giving direct access to evenly spread onsets.
136
- */
137
- euclideanKick?: EuclideanKick;
138
- };
139
-
140
- /**
141
- * Generate a drum performance as a flat list of onsets.
142
- *
143
- * Every voice (kick, snare, ghost snares, closed/open/foot hi-hats, ride, toms,
144
- * crash, and auxiliary percussion) is emitted as a {@link DrumHit} distinguished
145
- * by its General MIDI pitch. Groove style selects an internal style and feel;
146
- * `density` sets the backing-density level; `section` shapes kick, hi-hat, ghost,
147
- * and percussion density. 16th-note hi-hats drop to 8ths at or above 150 BPM.
148
- * When `fills` is true the final bar is replaced with a fill whose archetype is
149
- * shaped by `nextSection`; a fill that would emit nothing on its beat falls back
150
- * to the normal groove so the phrase end is never silent. `euclideanKick`
151
- * overrides the kick with an evenly-spread Euclidean pattern. Output is fully
152
- * determined by the options plus `seed`.
153
- *
154
- * @param opts Generation options.
155
- * @returns Percussion onsets in bar order.
156
- *
157
- * @example
158
- * ```ts
159
- * import { generateDrums } from '@libraz/libcantus';
160
- * const hits = generateDrums({ bars: 4, bpm: 120, style: 'standard', section: 'chorus', density: 0.6, fills: true });
161
- * // Fully determined by the options plus seed (defaults to 0).
162
- * ```
163
- *
164
- * @category Composition
165
- */
166
- export function generateDrums(opts: DrumsOptions): DrumHit[] {
167
- const track = new HitList();
168
- const rng = createRng(opts.seed ?? 0);
169
- const mapping = mapStyle(opts.style);
170
- const style = mapping.style;
171
- const feel: Feel = opts.feel ?? mapping.feel;
172
- const role: DrumRole = opts.role ?? 'full';
173
- // fxOnly leaves only fx/aux voices: the main kick, snare, ghost, and fill
174
- // voices are suppressed just as timekeeping hi-hats already are.
175
- const playMainVoices = role !== 'fxOnly';
176
- const section = mapSection(opts.section);
177
- const backingDensity = mapDensity(opts.density);
178
- const swingAmount = feelSwingAmount(feel);
179
-
180
- let densityMult = sectionDensityMultiplier(section);
181
- if (backingDensity === 'thin') {
182
- densityMult *= 0.75;
183
- } else if (backingDensity === 'thick') {
184
- densityMult *= 1.15;
185
- }
186
-
187
- const sec: SectionCtx = {
188
- style,
189
- feel,
190
- densityMult,
191
- backingDensity,
192
- hhLevel: getHiHatLevel(section, style, backingDensity, opts.bpm, rng),
193
- useGhostNotes:
194
- (section === 'b' || section === 'chorus' || section === 'bridge') && style !== 'sparse',
195
- ghostBoost: mapping.ghostBoost,
196
- useRide: shouldUseRideForSection(section, style),
197
- useFootHh: shouldUseFootHiHat(section, role),
198
- role,
199
- ghostMood: ghostMoodCategory(style),
200
- snareBeat3: mapping.snareBeat3,
201
- };
202
-
203
- const ohhBarInterval = openHiHatBarInterval(section, style);
204
- const energy = sectionEnergy(section);
205
- const fillStartBeat = getFillStartBeat(energy);
206
- const percMood = percMoodCategory(style);
207
-
208
- // The final-bar fill is shaped by the section it leads into. When no next
209
- // section is given the fill is treated as within-section (from === to).
210
- const nextSection = opts.nextSection ? mapSection(opts.nextSection) : section;
211
- const nextEnergy = sectionEnergy(nextSection);
212
-
213
- const euclidKick: KickPattern | undefined = opts.euclideanKick
214
- ? euclideanToKickPattern(
215
- patternToMask(
216
- euclideanRhythm(
217
- opts.euclideanKick.pulses,
218
- opts.euclideanKick.steps ?? 16,
219
- opts.euclideanKick.rotation ?? 0,
220
- ),
221
- ),
222
- )
223
- : undefined;
224
-
225
- const reuseSectionKick = (section === 'b' || section === 'chorus') && style !== 'sparse';
226
- let sectionKick: KickPattern | undefined;
227
-
228
- for (let bar = 0; bar < opts.bars; bar += 1) {
229
- const barStart = bar * 4;
230
- const isLastBar = bar === opts.bars - 1;
231
-
232
- if (bar === 0 && section === 'chorus') {
233
- track.add(GM.CRASH, barStart, 0.5, 100 * densityMult);
234
- }
235
-
236
- let barHasOpenHh = false;
237
- let openHhBeatIndex = 3;
238
- if (ohhBarInterval > 0 && bar % ohhBarInterval === ohhBarInterval - 1) {
239
- openHhBeatIndex = openHiHatBeat(section, rng);
240
- barHasOpenHh = !track.hasCrashNear(barStart + openHhBeatIndex);
241
- }
242
-
243
- let kick: KickPattern;
244
- if (euclidKick) {
245
- kick = euclidKick;
246
- } else if (reuseSectionKick) {
247
- sectionKick ??= getKickPattern(section, style, 0, rng);
248
- kick = sectionKick;
249
- } else {
250
- kick = getKickPattern(section, style, bar, rng);
251
- }
252
-
253
- const inLift = isInPreChorusLift(section, bar, opts.bars, section === 'b');
254
- let currentFill: FillType = 'snareRoll';
255
-
256
- for (let beat = 0; beat < 4; beat += 1) {
257
- const beatTick = barStart + beat;
258
- const velocity = calculateVelocity(section, beat);
259
-
260
- if (opts.fills && isLastBar && !inLift && beat >= fillStartBeat) {
261
- if (beat === fillStartBeat) {
262
- currentFill = selectFillType(section, nextSection, style, nextEnergy, rng);
263
- }
264
- if (playMainVoices) {
265
- const before = track.hits.length;
266
- generateFill(track, beatTick, beat, currentFill, velocity);
267
- if (track.hits.length > before) {
268
- continue;
269
- }
270
- // Safety net: an archetype that contributes nothing on this beat would
271
- // leave a silent phrase end, so fall through to the normal groove.
272
- } else {
273
- // fxOnly deliberately emits no main voices on the fill beats.
274
- continue;
275
- }
276
- }
277
-
278
- const ctx: BeatCtx = {
279
- track,
280
- beatTick,
281
- beat,
282
- velocity,
283
- section,
284
- bpm: opts.bpm,
285
- bar,
286
- sectionBars: opts.bars,
287
- inPrechorusLift: inLift,
288
- swingAmount,
289
- barHasOpenHh,
290
- openHhBeat: openHhBeatIndex,
291
- rng,
292
- };
293
-
294
- if (playMainVoices) {
295
- if (inLift) {
296
- generatePreChorusBuildup(ctx, sec, isLastBar);
297
- }
298
- generateKickForBeat(ctx, sec, kick);
299
- generateSnareForBeat(ctx, sec, section === 'intro' && bar === 0);
300
- if (sec.useGhostNotes && !inLift) {
301
- generateGhostNotesForBeat(ctx, sec);
302
- }
303
- }
304
- generateHiHatForBeat(ctx, sec);
305
- }
306
-
307
- if (sec.useFootHh && shouldPlayHiHat(role)) {
308
- for (let fhhBeat = 0; fhhBeat < 4; fhhBeat += 2) {
309
- const fhhTick = barStart + fhhBeat;
310
- const occupied = track.hits.some((h) => h.pitch === GM.FHH && h.startBeat === fhhTick);
311
- if (!occupied) {
312
- track.add(GM.FHH, fhhTick, 0.5, footHiHatVelocity(rng));
313
- }
314
- }
315
- }
316
-
317
- generateAuxPercussionForBar(
318
- track,
319
- barStart,
320
- getPercussionConfig(percMood, section),
321
- role,
322
- densityMult,
323
- rng,
324
- opts.bpm,
325
- );
326
- }
327
-
328
- return track.hits;
329
- }