@libraz/libcantus 0.9.0 → 0.9.1
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/README.md +10 -2
- package/dist/index.cjs +577 -225
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1533 -172
- package/dist/index.d.ts +1533 -172
- package/dist/index.js +577 -225
- package/dist/index.js.map +1 -1
- package/package.json +6 -1
- package/src/analysis/index.ts +73 -7
- package/src/arrange/index.ts +241 -79
- package/src/bass/index.ts +61 -12
- package/src/chord/index.ts +66 -3
- package/src/chordscale/index.ts +67 -9
- package/src/countermelody/index.ts +44 -14
- package/src/counterpoint/index.ts +21 -0
- package/src/detect/index.ts +79 -20
- package/src/drums/fills.ts +31 -20
- package/src/drums/index.ts +125 -7
- package/src/drums/internal.ts +7 -1
- package/src/drums/swing.ts +7 -2
- package/src/functional/index.ts +160 -51
- package/src/groove/index.ts +116 -20
- package/src/harmonize/index.ts +85 -12
- package/src/harmony/index.ts +19 -2
- package/src/index.ts +1 -0
- package/src/interval/index.ts +15 -1
- package/src/meter/index.ts +108 -5
- package/src/model/index.ts +161 -12
- package/src/motif/index.ts +87 -10
- package/src/pitch/index.ts +58 -3
- package/src/progression/index.ts +56 -8
- package/src/random/index.ts +13 -1
- package/src/reharmony/index.ts +35 -4
- package/src/rhythm/index.ts +45 -11
- package/src/safety/index.ts +58 -11
- package/src/scale/index.ts +135 -19
- package/src/spelling/index.ts +18 -0
- package/src/symbol/index.ts +84 -36
- package/src/timeline/index.ts +80 -13
- package/src/tuning/index.ts +28 -1
- package/src/voicing/index.ts +121 -21
package/src/detect/index.ts
CHANGED
|
@@ -6,10 +6,21 @@
|
|
|
6
6
|
|
|
7
7
|
import type { Chord, ChordQuality } from '../chord/index.js';
|
|
8
8
|
import { chordPitchClasses, chordQualities, makeChord } from '../chord/index.js';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
HARMONIC_MINOR_MASK,
|
|
11
|
+
MAJOR_MASK,
|
|
12
|
+
MELODIC_MINOR_MASK,
|
|
13
|
+
majorKey,
|
|
14
|
+
minorKey,
|
|
15
|
+
NATURAL_MINOR_MASK,
|
|
16
|
+
} from '../scale/index.js';
|
|
10
17
|
import type { KeyScale } from '../types.js';
|
|
11
18
|
|
|
12
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* A candidate chord interpretation of a pitch set.
|
|
21
|
+
*
|
|
22
|
+
* @category Recognition
|
|
23
|
+
*/
|
|
13
24
|
export type ChordMatch = {
|
|
14
25
|
rootPc: number;
|
|
15
26
|
quality: ChordQuality;
|
|
@@ -25,7 +36,11 @@ export type ChordMatch = {
|
|
|
25
36
|
bassPc?: number;
|
|
26
37
|
};
|
|
27
38
|
|
|
28
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* A candidate key interpretation of a pitch-class set.
|
|
41
|
+
*
|
|
42
|
+
* @category Recognition
|
|
43
|
+
*/
|
|
29
44
|
export type KeyMatch = {
|
|
30
45
|
key: KeyScale;
|
|
31
46
|
mode: 'major' | 'minor';
|
|
@@ -55,6 +70,13 @@ function uniquePitchClasses(pitches: number[]): number[] {
|
|
|
55
70
|
*
|
|
56
71
|
* @param pitches MIDI pitches or bare pitch classes (octave-agnostic).
|
|
57
72
|
* @returns Ranked chord interpretations (may be empty).
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* import { detectChord } from '@libraz/libcantus';
|
|
76
|
+
* const matches = detectChord([60, 64, 67]); // C E G
|
|
77
|
+
* matches[0]; // { rootPc: 0, quality: 'maj', exact: true, ... }
|
|
78
|
+
* ```
|
|
79
|
+
* @category Recognition
|
|
58
80
|
*/
|
|
59
81
|
export function detectChord(pitches: number[]): ChordMatch[] {
|
|
60
82
|
const input = uniquePitchClasses(pitches);
|
|
@@ -85,7 +107,7 @@ export function detectChord(pitches: number[]): ChordMatch[] {
|
|
|
85
107
|
quality,
|
|
86
108
|
missingPcs,
|
|
87
109
|
extraPcs,
|
|
88
|
-
exact: extraPcs.length === 0,
|
|
110
|
+
exact: extraPcs.length === 0 && missingPcs.length === 0,
|
|
89
111
|
inversion,
|
|
90
112
|
};
|
|
91
113
|
if (inversion > 0) {
|
|
@@ -117,6 +139,12 @@ export function detectChord(pitches: number[]): ChordMatch[] {
|
|
|
117
139
|
*
|
|
118
140
|
* @param pitches MIDI pitches or bare pitch classes.
|
|
119
141
|
* @returns The top-ranked chord, or null when nothing matches.
|
|
142
|
+
* @example
|
|
143
|
+
* ```ts
|
|
144
|
+
* import { detectChordBest } from '@libraz/libcantus';
|
|
145
|
+
* detectChordBest([60, 64, 67]); // C major triad: { rootPc: 0, quality: 'maj', ... }
|
|
146
|
+
* ```
|
|
147
|
+
* @category Recognition
|
|
120
148
|
*/
|
|
121
149
|
export function detectChordBest(pitches: number[]): Chord | null {
|
|
122
150
|
const best = detectChord(pitches)[0];
|
|
@@ -126,47 +154,78 @@ export function detectChordBest(pitches: number[]): Chord | null {
|
|
|
126
154
|
return makeChord(best.rootPc, best.quality, best.bassPc);
|
|
127
155
|
}
|
|
128
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Minor-scale variants scored for each minor-key candidate. Scoring against all
|
|
159
|
+
* three lets the raised sixth and seventh (e.g. the leading tone G# in A minor)
|
|
160
|
+
* count toward their own tonic instead of only penalizing it.
|
|
161
|
+
*/
|
|
162
|
+
const MINOR_MASK_VARIANTS = [NATURAL_MINOR_MASK, HARMONIC_MINOR_MASK, MELODIC_MINOR_MASK];
|
|
163
|
+
|
|
129
164
|
/**
|
|
130
165
|
* Rank major and minor keys by how well they contain a set of pitch classes.
|
|
131
166
|
*
|
|
132
167
|
* The tonic is weighted so that, among equally-fitting keys, the one whose root
|
|
133
|
-
* appears in the input is preferred.
|
|
168
|
+
* appears in the input is preferred. Minor candidates are scored against the
|
|
169
|
+
* natural, harmonic, and melodic minor variants and keep the best of the three,
|
|
170
|
+
* so a minor cadence containing the leading tone still resolves to its own
|
|
171
|
+
* tonic; the returned key is always the natural-minor scale. Returns all 24
|
|
172
|
+
* keys ranked best-first, or an empty array for an empty input (mirroring
|
|
173
|
+
* {@link detectChord}).
|
|
134
174
|
*
|
|
135
175
|
* @param pitches MIDI pitches or bare pitch classes.
|
|
136
|
-
* @returns Ranked key interpretations.
|
|
176
|
+
* @returns Ranked key interpretations (empty for an empty input).
|
|
177
|
+
* @example
|
|
178
|
+
* ```ts
|
|
179
|
+
* import { detectKey } from '@libraz/libcantus';
|
|
180
|
+
* const keys = detectKey([60, 62, 64, 65, 67, 69, 71]); // C major scale
|
|
181
|
+
* keys[0].mode; // 'major', with keys[0].key.rootPc === 0
|
|
182
|
+
* ```
|
|
183
|
+
* @category Recognition
|
|
137
184
|
*/
|
|
138
185
|
export function detectKey(pitches: number[]): KeyMatch[] {
|
|
139
186
|
const input = uniquePitchClasses(pitches);
|
|
187
|
+
if (input.length === 0) {
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
140
190
|
const counts = new Map<number, number>();
|
|
141
191
|
for (const pc of pitches.map(pitchClass)) {
|
|
142
192
|
counts.set(pc, (counts.get(pc) ?? 0) + 1);
|
|
143
193
|
}
|
|
144
|
-
const total = pitches.length
|
|
194
|
+
const total = pitches.length;
|
|
145
195
|
const results: (KeyMatch & { score: number })[] = [];
|
|
146
196
|
for (let tonic = 0; tonic < 12; tonic += 1) {
|
|
147
197
|
for (const mode of ['major', 'minor'] as const) {
|
|
148
198
|
const key = mode === 'major' ? majorKey(tonic) : minorKey(tonic);
|
|
199
|
+
const masks = mode === 'major' ? [MAJOR_MASK] : MINOR_MASK_VARIANTS;
|
|
200
|
+
// Score each scale variant and keep the best; ties keep the earlier
|
|
201
|
+
// (more diatonic) variant.
|
|
149
202
|
let inScale = 0;
|
|
150
|
-
let weighted =
|
|
151
|
-
for (const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
203
|
+
let weighted = Number.NEGATIVE_INFINITY;
|
|
204
|
+
for (const mask of masks) {
|
|
205
|
+
let variantInScale = 0;
|
|
206
|
+
let variantWeighted = 0;
|
|
207
|
+
for (const pc of input) {
|
|
208
|
+
const offset = (pc - tonic + 12) % 12;
|
|
209
|
+
if ((mask >> offset) & 1) {
|
|
210
|
+
variantInScale += 1;
|
|
211
|
+
}
|
|
155
212
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
213
|
+
for (const [pc, count] of counts) {
|
|
214
|
+
const offset = (pc - tonic + 12) % 12;
|
|
215
|
+
if ((mask >> offset) & 1) {
|
|
216
|
+
variantWeighted += count;
|
|
217
|
+
}
|
|
161
218
|
}
|
|
162
|
-
if (
|
|
163
|
-
weighted
|
|
219
|
+
if (variantWeighted > weighted) {
|
|
220
|
+
weighted = variantWeighted;
|
|
221
|
+
inScale = variantInScale;
|
|
164
222
|
}
|
|
165
223
|
}
|
|
224
|
+
weighted += (counts.get(tonic) ?? 0) * 0.5;
|
|
166
225
|
results.push({
|
|
167
226
|
key,
|
|
168
227
|
mode,
|
|
169
|
-
fit:
|
|
228
|
+
fit: inScale / input.length,
|
|
170
229
|
score: weighted / total,
|
|
171
230
|
});
|
|
172
231
|
}
|
package/src/drums/fills.ts
CHANGED
|
@@ -32,7 +32,19 @@ export function getFillStartBeat(energy: SectionEnergy): number {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Pick a fill archetype for a section transition.
|
|
37
|
+
*
|
|
38
|
+
* The transition-specific archetypes (into a chorus, out of an intro) are
|
|
39
|
+
* checked before the generic energy fills so every variation stays reachable;
|
|
40
|
+
* with `from === to` the caller effectively asks for a within-section fill.
|
|
41
|
+
*
|
|
42
|
+
* @param from Section the fill leaves.
|
|
43
|
+
* @param to Section the fill leads into.
|
|
44
|
+
* @param style Internal drum style.
|
|
45
|
+
* @param nextEnergy Energy of the section the fill leads into.
|
|
46
|
+
* @param rng Deterministic PRNG.
|
|
47
|
+
*/
|
|
36
48
|
export function selectFillType(
|
|
37
49
|
from: SectionType,
|
|
38
50
|
to: SectionType,
|
|
@@ -44,6 +56,11 @@ export function selectFillType(
|
|
|
44
56
|
return rng.range(0, 1) === 0 ? 'simpleCrash' : 'breakdownFill';
|
|
45
57
|
}
|
|
46
58
|
|
|
59
|
+
const toChorus = to === 'chorus';
|
|
60
|
+
const fromIntro = from === 'intro';
|
|
61
|
+
const highEnergy = style === 'rock' || style === 'fourOnFloor';
|
|
62
|
+
|
|
63
|
+
// Dropping into a low-energy section: keep the phrase end gentle.
|
|
47
64
|
if (nextEnergy === 'low') {
|
|
48
65
|
switch (rng.range(0, 2)) {
|
|
49
66
|
case 0:
|
|
@@ -55,25 +72,9 @@ export function selectFillType(
|
|
|
55
72
|
}
|
|
56
73
|
}
|
|
57
74
|
|
|
58
|
-
|
|
59
|
-
switch (rng.range(0, 3)) {
|
|
60
|
-
case 0:
|
|
61
|
-
return 'tomDescend';
|
|
62
|
-
case 1:
|
|
63
|
-
return 'snareRoll';
|
|
64
|
-
case 2:
|
|
65
|
-
return 'linearFill';
|
|
66
|
-
default:
|
|
67
|
-
return 'flamsAndDrags';
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const toChorus = to === 'chorus';
|
|
72
|
-
const fromIntro = from === 'intro';
|
|
73
|
-
const highEnergy = style === 'rock' || style === 'fourOnFloor';
|
|
74
|
-
const choice = rng.range(0, 7);
|
|
75
|
-
|
|
75
|
+
// Big lead-ins to a chorus take precedence over the generic energy fills.
|
|
76
76
|
if (toChorus) {
|
|
77
|
+
const choice = rng.range(0, 7);
|
|
77
78
|
if (highEnergy) {
|
|
78
79
|
switch (choice) {
|
|
79
80
|
case 0:
|
|
@@ -110,8 +111,9 @@ export function selectFillType(
|
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
// Leaving the intro: lighter, building character.
|
|
113
115
|
if (fromIntro) {
|
|
114
|
-
switch (
|
|
116
|
+
switch (rng.range(0, 5)) {
|
|
115
117
|
case 0:
|
|
116
118
|
case 1:
|
|
117
119
|
return 'snareRoll';
|
|
@@ -126,6 +128,8 @@ export function selectFillType(
|
|
|
126
128
|
}
|
|
127
129
|
}
|
|
128
130
|
|
|
131
|
+
// Generic medium/high/peak fills, split by style energy.
|
|
132
|
+
const choice = rng.range(0, 7);
|
|
129
133
|
if (highEnergy) {
|
|
130
134
|
switch (choice) {
|
|
131
135
|
case 0:
|
|
@@ -339,6 +343,13 @@ export function generateFill(
|
|
|
339
343
|
if (beat === 2) {
|
|
340
344
|
track.add(GM.SD, beatTick, 1, accentVel);
|
|
341
345
|
track.add(GM.BD, beatTick, 1, fillVel);
|
|
346
|
+
} else if (beat === 3) {
|
|
347
|
+
// At low energy the fill spans only beat 3, so this branch keeps the
|
|
348
|
+
// phrase end from going silent: a broad half-time backbeat snare with a
|
|
349
|
+
// light pickup into the next section.
|
|
350
|
+
track.add(GM.SD, beatTick, EIGHTH, accentVel);
|
|
351
|
+
track.add(GM.SD, beatTick + E, S, fillVel * 0.6);
|
|
352
|
+
track.add(GM.SD, beatTick + E + S, S, accentVel);
|
|
342
353
|
}
|
|
343
354
|
break;
|
|
344
355
|
}
|
package/src/drums/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
generateSnareForBeat,
|
|
8
8
|
type SectionCtx,
|
|
9
9
|
} from './beat.js';
|
|
10
|
+
import { euclideanRhythm, patternToMask } from './euclid.js';
|
|
10
11
|
import { type FillType, generateFill, getFillStartBeat, selectFillType } from './fills.js';
|
|
11
12
|
import {
|
|
12
13
|
footHiHatVelocity,
|
|
@@ -34,28 +35,106 @@ import {
|
|
|
34
35
|
sectionDensityMultiplier,
|
|
35
36
|
sectionEnergy,
|
|
36
37
|
} from './internal.js';
|
|
37
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
euclideanToKickPattern,
|
|
40
|
+
getKickPattern,
|
|
41
|
+
isInPreChorusLift,
|
|
42
|
+
type KickPattern,
|
|
43
|
+
} from './kick.js';
|
|
38
44
|
import { generateAuxPercussionForBar, getPercussionConfig } from './percussion.js';
|
|
39
45
|
import { createRng } from './rng.js';
|
|
40
46
|
|
|
47
|
+
/**
|
|
48
|
+
* A single drum onset emitted by {@link generateDrums}.
|
|
49
|
+
*
|
|
50
|
+
* @category Composition
|
|
51
|
+
*/
|
|
41
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
|
+
*/
|
|
42
59
|
export type GrooveFeel = Feel;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Drum voicing role and groove style identifiers for {@link generateDrums}.
|
|
63
|
+
*
|
|
64
|
+
* @category Composition
|
|
65
|
+
*/
|
|
43
66
|
export type { DrumRole, GrooveStyle } from './internal.js';
|
|
44
67
|
|
|
45
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Public section identifiers for {@link generateDrums}.
|
|
70
|
+
*
|
|
71
|
+
* @category Composition
|
|
72
|
+
*/
|
|
46
73
|
export type Section = PublicSection;
|
|
47
74
|
|
|
48
|
-
/**
|
|
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
|
+
*/
|
|
49
102
|
export type DrumGenOptions = {
|
|
50
103
|
bars: number;
|
|
51
104
|
bpm: number;
|
|
52
105
|
style: GrooveStyle;
|
|
53
106
|
section: Section;
|
|
54
107
|
density: number;
|
|
108
|
+
/**
|
|
109
|
+
* Replace the final bar with a fill.
|
|
110
|
+
*
|
|
111
|
+
* @defaultValue false
|
|
112
|
+
*/
|
|
55
113
|
fills?: boolean;
|
|
56
114
|
feel?: GrooveFeel;
|
|
115
|
+
/**
|
|
116
|
+
* Voicing role; `'fxOnly'` suppresses the main kick/snare/ghost/fill voices.
|
|
117
|
+
*
|
|
118
|
+
* @defaultValue `'full'`
|
|
119
|
+
*/
|
|
57
120
|
role?: DrumRole;
|
|
121
|
+
/**
|
|
122
|
+
* Seed for the deterministic PRNG.
|
|
123
|
+
*
|
|
124
|
+
* @defaultValue 0
|
|
125
|
+
*/
|
|
58
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;
|
|
59
138
|
};
|
|
60
139
|
|
|
61
140
|
/**
|
|
@@ -66,11 +145,23 @@ export type DrumGenOptions = {
|
|
|
66
145
|
* by its General MIDI pitch. Groove style selects an internal style and feel;
|
|
67
146
|
* `density` sets the backing-density level; `section` shapes kick, hi-hat, ghost,
|
|
68
147
|
* 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
|
|
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
|
|
70
152
|
* determined by the options plus `seed`.
|
|
71
153
|
*
|
|
72
154
|
* @param opts Generation options.
|
|
73
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
|
|
74
165
|
*/
|
|
75
166
|
export function generateDrums(opts: DrumGenOptions): DrumHit[] {
|
|
76
167
|
const track = new HitList();
|
|
@@ -114,6 +205,23 @@ export function generateDrums(opts: DrumGenOptions): DrumHit[] {
|
|
|
114
205
|
const fillStartBeat = getFillStartBeat(energy);
|
|
115
206
|
const percMood = percMoodCategory(style);
|
|
116
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
|
+
|
|
117
225
|
const reuseSectionKick = (section === 'b' || section === 'chorus') && style !== 'sparse';
|
|
118
226
|
let sectionKick: KickPattern | undefined;
|
|
119
227
|
|
|
@@ -133,7 +241,9 @@ export function generateDrums(opts: DrumGenOptions): DrumHit[] {
|
|
|
133
241
|
}
|
|
134
242
|
|
|
135
243
|
let kick: KickPattern;
|
|
136
|
-
if (
|
|
244
|
+
if (euclidKick) {
|
|
245
|
+
kick = euclidKick;
|
|
246
|
+
} else if (reuseSectionKick) {
|
|
137
247
|
sectionKick ??= getKickPattern(section, style, 0, rng);
|
|
138
248
|
kick = sectionKick;
|
|
139
249
|
} else {
|
|
@@ -149,12 +259,20 @@ export function generateDrums(opts: DrumGenOptions): DrumHit[] {
|
|
|
149
259
|
|
|
150
260
|
if (opts.fills && isLastBar && !inLift && beat >= fillStartBeat) {
|
|
151
261
|
if (beat === fillStartBeat) {
|
|
152
|
-
currentFill = selectFillType(section,
|
|
262
|
+
currentFill = selectFillType(section, nextSection, style, nextEnergy, rng);
|
|
153
263
|
}
|
|
154
264
|
if (playMainVoices) {
|
|
265
|
+
const before = track.hits.length;
|
|
155
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;
|
|
156
275
|
}
|
|
157
|
-
continue;
|
|
158
276
|
}
|
|
159
277
|
|
|
160
278
|
const ctx: BeatCtx = {
|
package/src/drums/internal.ts
CHANGED
|
@@ -67,7 +67,9 @@ export type GrooveStyle =
|
|
|
67
67
|
| 'bossa'
|
|
68
68
|
| 'trap'
|
|
69
69
|
| 'halftime'
|
|
70
|
-
| 'breakbeat'
|
|
70
|
+
| 'breakbeat'
|
|
71
|
+
| 'house'
|
|
72
|
+
| 'synthpop';
|
|
71
73
|
|
|
72
74
|
/** Public role identifiers gating which voices are present. */
|
|
73
75
|
export type DrumRole = 'full' | 'ambient' | 'minimal' | 'fxOnly';
|
|
@@ -92,6 +94,10 @@ const STYLE_MAP: Record<GrooveStyle, StyleMapping> = {
|
|
|
92
94
|
trap: { style: 'trap', feel: 'straight', ghostBoost: false, snareBeat3: true },
|
|
93
95
|
halftime: { style: 'sparse', feel: 'straight', ghostBoost: false, snareBeat3: true },
|
|
94
96
|
breakbeat: { style: 'rock', feel: 'straight', ghostBoost: false, snareBeat3: false },
|
|
97
|
+
// Four-on-the-floor dance kick with idol-style auxiliary percussion.
|
|
98
|
+
house: { style: 'fourOnFloor', feel: 'straight', ghostBoost: false, snareBeat3: false },
|
|
99
|
+
// Synth-pop pulse with denser 16th hi-hats and busy kick syncopation.
|
|
100
|
+
synthpop: { style: 'synth', feel: 'straight', ghostBoost: true, snareBeat3: false },
|
|
95
101
|
};
|
|
96
102
|
|
|
97
103
|
/** Resolve the internal style/feel parameters for a public groove style. */
|
package/src/drums/swing.ts
CHANGED
|
@@ -53,8 +53,13 @@ export function quantizeSwing(
|
|
|
53
53
|
return base + EIGHTH + DELTA_8 * clamped;
|
|
54
54
|
}
|
|
55
55
|
if (offset >= EIGHTH + HALF_16TH && offset < 3 * SIXTEENTH + HALF_16TH) {
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
// The "a" 16th is the midpoint of the compressed second half of the beat
|
|
57
|
+
// (between the swung "and" and the next downbeat), mirroring the "e" 16th
|
|
58
|
+
// in the first half. Because DELTA_8 = 2 * DELTA_16, that midpoint reduces
|
|
59
|
+
// to a single DELTA_16 delay off the straight position, so the "a" never
|
|
60
|
+
// overshoots the downbeat and needs no clamp. At the default shuffle feel
|
|
61
|
+
// (swing 0.75) this lands near 0.8125 rather than the doubly-swung 0.9375.
|
|
62
|
+
return base + 3 * SIXTEENTH + DELTA_16 * clamped;
|
|
58
63
|
}
|
|
59
64
|
return startBeat;
|
|
60
65
|
}
|