@libraz/libcantus 0.9.0
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.
- package/LICENSE +201 -0
- package/README.md +291 -0
- package/dist/index.cjs +6465 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2621 -0
- package/dist/index.d.ts +2621 -0
- package/dist/index.js +6297 -0
- package/dist/index.js.map +1 -0
- package/package.json +71 -0
- package/src/analysis/index.ts +261 -0
- package/src/arrange/index.ts +409 -0
- package/src/bass/index.ts +301 -0
- package/src/chord/index.ts +292 -0
- package/src/chordscale/index.ts +316 -0
- package/src/countermelody/index.ts +349 -0
- package/src/counterpoint/index.ts +209 -0
- package/src/detect/index.ts +176 -0
- package/src/drums/beat.ts +266 -0
- package/src/drums/euclid.ts +58 -0
- package/src/drums/fills.ts +345 -0
- package/src/drums/ghost.ts +119 -0
- package/src/drums/hihat.ts +285 -0
- package/src/drums/hit.ts +31 -0
- package/src/drums/index.ts +211 -0
- package/src/drums/internal.ts +251 -0
- package/src/drums/kick.ts +160 -0
- package/src/drums/percussion.ts +121 -0
- package/src/drums/rng.ts +7 -0
- package/src/drums/swing.ts +66 -0
- package/src/functional/index.ts +602 -0
- package/src/groove/index.ts +230 -0
- package/src/harmonize/index.ts +367 -0
- package/src/harmony/index.ts +75 -0
- package/src/index.ts +233 -0
- package/src/interval/index.ts +67 -0
- package/src/meter/index.ts +179 -0
- package/src/model/index.ts +957 -0
- package/src/motif/index.ts +321 -0
- package/src/pitch/index.ts +256 -0
- package/src/progression/index.ts +336 -0
- package/src/random/index.ts +41 -0
- package/src/reharmony/index.ts +299 -0
- package/src/rhythm/index.ts +131 -0
- package/src/safety/index.ts +372 -0
- package/src/scale/index.ts +215 -0
- package/src/spelling/index.ts +164 -0
- package/src/symbol/index.ts +213 -0
- package/src/timeline/index.ts +352 -0
- package/src/tuning/index.ts +129 -0
- package/src/types.ts +29 -0
- package/src/voicing/index.ts +523 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,285 @@
|
|
|
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
|
+
}
|
package/src/drums/hit.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type BeatCtx,
|
|
3
|
+
generateGhostNotesForBeat,
|
|
4
|
+
generateHiHatForBeat,
|
|
5
|
+
generateKickForBeat,
|
|
6
|
+
generatePreChorusBuildup,
|
|
7
|
+
generateSnareForBeat,
|
|
8
|
+
type SectionCtx,
|
|
9
|
+
} from './beat.js';
|
|
10
|
+
import { type FillType, generateFill, getFillStartBeat, selectFillType } from './fills.js';
|
|
11
|
+
import {
|
|
12
|
+
footHiHatVelocity,
|
|
13
|
+
getHiHatLevel,
|
|
14
|
+
openHiHatBarInterval,
|
|
15
|
+
openHiHatBeat,
|
|
16
|
+
shouldPlayHiHat,
|
|
17
|
+
shouldUseFootHiHat,
|
|
18
|
+
shouldUseRideForSection,
|
|
19
|
+
} from './hihat.js';
|
|
20
|
+
import { type DrumHit, HitList } from './hit.js';
|
|
21
|
+
import {
|
|
22
|
+
calculateVelocity,
|
|
23
|
+
type DrumRole,
|
|
24
|
+
type Feel,
|
|
25
|
+
feelSwingAmount,
|
|
26
|
+
GM,
|
|
27
|
+
type GrooveStyle,
|
|
28
|
+
ghostMoodCategory,
|
|
29
|
+
mapDensity,
|
|
30
|
+
mapSection,
|
|
31
|
+
mapStyle,
|
|
32
|
+
type PublicSection,
|
|
33
|
+
percMoodCategory,
|
|
34
|
+
sectionDensityMultiplier,
|
|
35
|
+
sectionEnergy,
|
|
36
|
+
} from './internal.js';
|
|
37
|
+
import { getKickPattern, isInPreChorusLift, type KickPattern } from './kick.js';
|
|
38
|
+
import { generateAuxPercussionForBar, getPercussionConfig } from './percussion.js';
|
|
39
|
+
import { createRng } from './rng.js';
|
|
40
|
+
|
|
41
|
+
export type { DrumHit } from './hit.js';
|
|
42
|
+
export type GrooveFeel = Feel;
|
|
43
|
+
export type { DrumRole, GrooveStyle } from './internal.js';
|
|
44
|
+
|
|
45
|
+
/** Public section identifiers for {@link generateDrums}. */
|
|
46
|
+
export type Section = PublicSection;
|
|
47
|
+
|
|
48
|
+
/** Options controlling {@link generateDrums}. */
|
|
49
|
+
export type DrumGenOptions = {
|
|
50
|
+
bars: number;
|
|
51
|
+
bpm: number;
|
|
52
|
+
style: GrooveStyle;
|
|
53
|
+
section: Section;
|
|
54
|
+
density: number;
|
|
55
|
+
fills?: boolean;
|
|
56
|
+
feel?: GrooveFeel;
|
|
57
|
+
role?: DrumRole;
|
|
58
|
+
seed?: number;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Generate a drum performance as a flat list of onsets.
|
|
63
|
+
*
|
|
64
|
+
* Every voice (kick, snare, ghost snares, closed/open/foot hi-hats, ride, toms,
|
|
65
|
+
* crash, and auxiliary percussion) is emitted as a {@link DrumHit} distinguished
|
|
66
|
+
* by its General MIDI pitch. Groove style selects an internal style and feel;
|
|
67
|
+
* `density` sets the backing-density level; `section` shapes kick, hi-hat, ghost,
|
|
68
|
+
* and percussion density. 16th-note hi-hats drop to 8ths at or above 150 BPM.
|
|
69
|
+
* When `fills` is true the final bar is replaced with a fill. Output is fully
|
|
70
|
+
* determined by the options plus `seed`.
|
|
71
|
+
*
|
|
72
|
+
* @param opts Generation options.
|
|
73
|
+
* @returns Percussion onsets in bar order.
|
|
74
|
+
*/
|
|
75
|
+
export function generateDrums(opts: DrumGenOptions): DrumHit[] {
|
|
76
|
+
const track = new HitList();
|
|
77
|
+
const rng = createRng(opts.seed ?? 0);
|
|
78
|
+
const mapping = mapStyle(opts.style);
|
|
79
|
+
const style = mapping.style;
|
|
80
|
+
const feel: Feel = opts.feel ?? mapping.feel;
|
|
81
|
+
const role: DrumRole = opts.role ?? 'full';
|
|
82
|
+
// fxOnly leaves only fx/aux voices: the main kick, snare, ghost, and fill
|
|
83
|
+
// voices are suppressed just as timekeeping hi-hats already are.
|
|
84
|
+
const playMainVoices = role !== 'fxOnly';
|
|
85
|
+
const section = mapSection(opts.section);
|
|
86
|
+
const backingDensity = mapDensity(opts.density);
|
|
87
|
+
const swingAmount = feelSwingAmount(feel);
|
|
88
|
+
|
|
89
|
+
let densityMult = sectionDensityMultiplier(section);
|
|
90
|
+
if (backingDensity === 'thin') {
|
|
91
|
+
densityMult *= 0.75;
|
|
92
|
+
} else if (backingDensity === 'thick') {
|
|
93
|
+
densityMult *= 1.15;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const sec: SectionCtx = {
|
|
97
|
+
style,
|
|
98
|
+
feel,
|
|
99
|
+
densityMult,
|
|
100
|
+
backingDensity,
|
|
101
|
+
hhLevel: getHiHatLevel(section, style, backingDensity, opts.bpm, rng),
|
|
102
|
+
useGhostNotes:
|
|
103
|
+
(section === 'b' || section === 'chorus' || section === 'bridge') && style !== 'sparse',
|
|
104
|
+
ghostBoost: mapping.ghostBoost,
|
|
105
|
+
useRide: shouldUseRideForSection(section, style),
|
|
106
|
+
useFootHh: shouldUseFootHiHat(section, role),
|
|
107
|
+
role,
|
|
108
|
+
ghostMood: ghostMoodCategory(style),
|
|
109
|
+
snareBeat3: mapping.snareBeat3,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const ohhBarInterval = openHiHatBarInterval(section, style);
|
|
113
|
+
const energy = sectionEnergy(section);
|
|
114
|
+
const fillStartBeat = getFillStartBeat(energy);
|
|
115
|
+
const percMood = percMoodCategory(style);
|
|
116
|
+
|
|
117
|
+
const reuseSectionKick = (section === 'b' || section === 'chorus') && style !== 'sparse';
|
|
118
|
+
let sectionKick: KickPattern | undefined;
|
|
119
|
+
|
|
120
|
+
for (let bar = 0; bar < opts.bars; bar += 1) {
|
|
121
|
+
const barStart = bar * 4;
|
|
122
|
+
const isLastBar = bar === opts.bars - 1;
|
|
123
|
+
|
|
124
|
+
if (bar === 0 && section === 'chorus') {
|
|
125
|
+
track.add(GM.CRASH, barStart, 0.5, 100 * densityMult);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let barHasOpenHh = false;
|
|
129
|
+
let openHhBeatIndex = 3;
|
|
130
|
+
if (ohhBarInterval > 0 && bar % ohhBarInterval === ohhBarInterval - 1) {
|
|
131
|
+
openHhBeatIndex = openHiHatBeat(section, rng);
|
|
132
|
+
barHasOpenHh = !track.hasCrashNear(barStart + openHhBeatIndex);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let kick: KickPattern;
|
|
136
|
+
if (reuseSectionKick) {
|
|
137
|
+
sectionKick ??= getKickPattern(section, style, 0, rng);
|
|
138
|
+
kick = sectionKick;
|
|
139
|
+
} else {
|
|
140
|
+
kick = getKickPattern(section, style, bar, rng);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const inLift = isInPreChorusLift(section, bar, opts.bars, section === 'b');
|
|
144
|
+
let currentFill: FillType = 'snareRoll';
|
|
145
|
+
|
|
146
|
+
for (let beat = 0; beat < 4; beat += 1) {
|
|
147
|
+
const beatTick = barStart + beat;
|
|
148
|
+
const velocity = calculateVelocity(section, beat);
|
|
149
|
+
|
|
150
|
+
if (opts.fills && isLastBar && !inLift && beat >= fillStartBeat) {
|
|
151
|
+
if (beat === fillStartBeat) {
|
|
152
|
+
currentFill = selectFillType(section, section, style, energy, rng);
|
|
153
|
+
}
|
|
154
|
+
if (playMainVoices) {
|
|
155
|
+
generateFill(track, beatTick, beat, currentFill, velocity);
|
|
156
|
+
}
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const ctx: BeatCtx = {
|
|
161
|
+
track,
|
|
162
|
+
beatTick,
|
|
163
|
+
beat,
|
|
164
|
+
velocity,
|
|
165
|
+
section,
|
|
166
|
+
bpm: opts.bpm,
|
|
167
|
+
bar,
|
|
168
|
+
sectionBars: opts.bars,
|
|
169
|
+
inPrechorusLift: inLift,
|
|
170
|
+
swingAmount,
|
|
171
|
+
barHasOpenHh,
|
|
172
|
+
openHhBeat: openHhBeatIndex,
|
|
173
|
+
rng,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
if (playMainVoices) {
|
|
177
|
+
if (inLift) {
|
|
178
|
+
generatePreChorusBuildup(ctx, sec, isLastBar);
|
|
179
|
+
}
|
|
180
|
+
generateKickForBeat(ctx, sec, kick);
|
|
181
|
+
generateSnareForBeat(ctx, sec, section === 'intro' && bar === 0);
|
|
182
|
+
if (sec.useGhostNotes && !inLift) {
|
|
183
|
+
generateGhostNotesForBeat(ctx, sec);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
generateHiHatForBeat(ctx, sec);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (sec.useFootHh && shouldPlayHiHat(role)) {
|
|
190
|
+
for (let fhhBeat = 0; fhhBeat < 4; fhhBeat += 2) {
|
|
191
|
+
const fhhTick = barStart + fhhBeat;
|
|
192
|
+
const occupied = track.hits.some((h) => h.pitch === GM.FHH && h.startBeat === fhhTick);
|
|
193
|
+
if (!occupied) {
|
|
194
|
+
track.add(GM.FHH, fhhTick, 0.5, footHiHatVelocity(rng));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
generateAuxPercussionForBar(
|
|
200
|
+
track,
|
|
201
|
+
barStart,
|
|
202
|
+
getPercussionConfig(percMood, section),
|
|
203
|
+
role,
|
|
204
|
+
densityMult,
|
|
205
|
+
rng,
|
|
206
|
+
opts.bpm,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return track.hits;
|
|
211
|
+
}
|