@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.
- package/README.md +6 -5
- package/dist/analyze/index.cjs +41 -1676
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +194 -122
- package/dist/analyze/index.d.ts +194 -122
- package/dist/analyze/index.js +19 -8
- package/dist/chunk-36N73KZ3.cjs +246 -0
- package/dist/chunk-36N73KZ3.cjs.map +1 -0
- package/dist/chunk-4HLLZPXT.cjs +3077 -0
- package/dist/chunk-4HLLZPXT.cjs.map +1 -0
- package/dist/chunk-7MGZ7D62.cjs +498 -0
- package/dist/chunk-7MGZ7D62.cjs.map +1 -0
- package/dist/{chunk-R3S2LUBH.js → chunk-7NKDPF74.js} +660 -305
- package/dist/chunk-7NKDPF74.js.map +1 -0
- package/dist/chunk-CAN76NCU.cjs +679 -0
- package/dist/chunk-CAN76NCU.cjs.map +1 -0
- package/dist/{chunk-7Y7WF64D.js → chunk-D2FAJBYL.js} +104 -35
- package/dist/chunk-D2FAJBYL.js.map +1 -0
- package/dist/{chunk-COKTX2G5.js → chunk-EHVEBWM5.js} +1052 -510
- package/dist/chunk-EHVEBWM5.js.map +1 -0
- package/dist/chunk-FCANXBAT.cjs +30 -0
- package/dist/chunk-FCANXBAT.cjs.map +1 -0
- package/dist/chunk-FNDWXRRI.cjs +479 -0
- package/dist/chunk-FNDWXRRI.cjs.map +1 -0
- package/dist/{chunk-GQKN5KDT.js → chunk-GQSGS2N4.js} +43 -29
- package/dist/chunk-GQSGS2N4.js.map +1 -0
- package/dist/chunk-HKGYCQ34.js +53 -0
- package/dist/chunk-HKGYCQ34.js.map +1 -0
- package/dist/chunk-I24MV65U.js +106 -0
- package/dist/chunk-I24MV65U.js.map +1 -0
- package/dist/{chunk-LN7ZZMD2.js → chunk-KMHOICIY.js} +322 -173
- package/dist/chunk-KMHOICIY.js.map +1 -0
- package/dist/chunk-LW53AHQU.cjs +1117 -0
- package/dist/chunk-LW53AHQU.cjs.map +1 -0
- package/dist/chunk-N6IHQUOX.js +1 -0
- package/dist/chunk-N6IHQUOX.js.map +1 -0
- package/dist/chunk-NKIBQ6PT.js +30 -0
- package/dist/chunk-NKIBQ6PT.js.map +1 -0
- package/dist/chunk-NP4NA2FB.cjs +106 -0
- package/dist/chunk-NP4NA2FB.cjs.map +1 -0
- package/dist/chunk-NVE34YGZ.cjs +1449 -0
- package/dist/chunk-NVE34YGZ.cjs.map +1 -0
- package/dist/{chunk-A4532ZFR.js → chunk-NYYWMV2C.js} +115 -47
- package/dist/chunk-NYYWMV2C.js.map +1 -0
- package/dist/{chunk-5HTNMQW4.js → chunk-Q2XUP227.js} +310 -118
- package/dist/chunk-Q2XUP227.js.map +1 -0
- package/dist/chunk-Q6OP4U43.cjs +327 -0
- package/dist/chunk-Q6OP4U43.cjs.map +1 -0
- package/dist/chunk-RC6GBMCD.cjs +845 -0
- package/dist/chunk-RC6GBMCD.cjs.map +1 -0
- package/dist/{chunk-XTC3X6MS.js → chunk-RK35RIN7.js} +453 -152
- package/dist/chunk-RK35RIN7.js.map +1 -0
- package/dist/chunk-RUMU3UA6.cjs +53 -0
- package/dist/chunk-RUMU3UA6.cjs.map +1 -0
- package/dist/chunk-T64C7ZD7.cjs +1 -0
- package/dist/chunk-T64C7ZD7.cjs.map +1 -0
- package/dist/chunk-XEBG3G53.js +498 -0
- package/dist/chunk-XEBG3G53.js.map +1 -0
- package/dist/chunk-YLEMGEFH.js +246 -0
- package/dist/chunk-YLEMGEFH.js.map +1 -0
- package/dist/chunk-ZEJR4TKL.cjs +173 -0
- package/dist/chunk-ZEJR4TKL.cjs.map +1 -0
- package/dist/core/index.cjs +130 -431
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +331 -17
- package/dist/core/index.d.ts +331 -17
- package/dist/core/index.js +71 -13
- package/dist/{function-2bJW1Zp_.d.ts → function-BtiZb9i9.d.ts} +28 -7
- package/dist/{function-r2sh-jOO.d.cts → function-DNugAOrt.d.cts} +28 -7
- package/dist/generate/index.cjs +30 -3751
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +373 -59
- package/dist/generate/index.d.ts +373 -59
- package/dist/generate/index.js +23 -9
- package/dist/{index-DZ1M21do.d.cts → index-6VR8-kit.d.ts} +20 -4
- package/dist/index-70p168VM.d.ts +192 -0
- package/dist/{index-D_ZTUNKp.d.cts → index-Be74mcon.d.cts} +18 -17
- package/dist/index-BtXT6UQ0.d.cts +192 -0
- package/dist/{index-BMPxwjwV.d.ts → index-C5UBbQ4G.d.ts} +18 -17
- package/dist/{index-BOBXBNwP.d.cts → index-C83mZI1P.d.ts} +93 -8
- package/dist/{index-BdeQ1r_p.d.cts → index-D6b6na69.d.cts} +14 -11
- package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
- package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
- package/dist/index-OG65ocOr.d.cts +272 -0
- package/dist/index-OG65ocOr.d.ts +272 -0
- package/dist/{index-BreoJRw0.d.ts → index-tJhkmQgE.d.cts} +20 -4
- package/dist/{index-CP3wAfEH.d.ts → index-tSQh-xwY.d.ts} +14 -11
- package/dist/{index-CM5W4rcm.d.ts → index-y_ak9_Hi.d.cts} +93 -8
- package/dist/index.cjs +419 -6848
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -13
- package/dist/index.d.ts +14 -13
- package/dist/index.js +132 -27
- package/dist/model/index.cjs +8 -2423
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +433 -74
- package/dist/model/index.d.ts +433 -74
- package/dist/model/index.js +6 -7
- package/dist/styled-DIRkk9Uz.d.ts +506 -0
- package/dist/styled-DJbkJQfj.d.cts +506 -0
- package/dist/theory/index.cjs +172 -1676
- package/dist/theory/index.cjs.map +1 -1
- package/dist/theory/index.d.cts +151 -233
- package/dist/theory/index.d.ts +151 -233
- package/dist/theory/index.js +31 -7
- package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
- package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
- package/package.json +73 -24
- package/dist/chunk-5HTNMQW4.js.map +0 -1
- package/dist/chunk-7Y7WF64D.js.map +0 -1
- package/dist/chunk-A4532ZFR.js.map +0 -1
- package/dist/chunk-BHUT2S5G.js +0 -22
- package/dist/chunk-BHUT2S5G.js.map +0 -1
- package/dist/chunk-CCBLVAPK.js +0 -42
- package/dist/chunk-CCBLVAPK.js.map +0 -1
- package/dist/chunk-COKTX2G5.js.map +0 -1
- package/dist/chunk-DYWFNC4A.js +0 -152
- package/dist/chunk-DYWFNC4A.js.map +0 -1
- package/dist/chunk-GQKN5KDT.js.map +0 -1
- package/dist/chunk-LN7ZZMD2.js.map +0 -1
- package/dist/chunk-MSWFZF7J.js +0 -118
- package/dist/chunk-MSWFZF7J.js.map +0 -1
- package/dist/chunk-R3S2LUBH.js.map +0 -1
- package/dist/chunk-SDM3VJJ5.js +0 -41
- package/dist/chunk-SDM3VJJ5.js.map +0 -1
- package/dist/chunk-XTC3X6MS.js.map +0 -1
- package/dist/chunk-ZXW5YIWK.js +0 -55
- package/dist/chunk-ZXW5YIWK.js.map +0 -1
- package/dist/index-CGMZNCx6.d.cts +0 -140
- package/dist/index-CGMZNCx6.d.ts +0 -140
- package/dist/styled-BjXtOFNk.d.cts +0 -282
- package/dist/styled-D7BAx3S3.d.ts +0 -282
- package/src/analyze/arrange/index.ts +0 -21
- package/src/analyze/arrange/internal.ts +0 -125
- package/src/analyze/arrange/tension.ts +0 -187
- package/src/analyze/arrange/tracks.ts +0 -279
- package/src/analyze/detect/index.ts +0 -235
- package/src/analyze/functional/borrowed.ts +0 -84
- package/src/analyze/functional/cadence.ts +0 -66
- package/src/analyze/functional/function.ts +0 -184
- package/src/analyze/functional/index.ts +0 -23
- package/src/analyze/functional/internal.ts +0 -29
- package/src/analyze/functional/roman.ts +0 -393
- package/src/analyze/index.ts +0 -49
- package/src/analyze/timeline/index.ts +0 -415
- package/src/analyze/voice/index.ts +0 -330
- package/src/core/index.ts +0 -47
- package/src/core/interval/index.ts +0 -81
- package/src/core/meter/index.ts +0 -282
- package/src/core/pitch/index.ts +0 -311
- package/src/core/random/index.ts +0 -53
- package/src/core/tuning/index.ts +0 -156
- package/src/core/types.ts +0 -29
- package/src/generate/bass/index.ts +0 -350
- package/src/generate/countermelody/index.ts +0 -379
- package/src/generate/drums/beat.ts +0 -266
- package/src/generate/drums/euclid.ts +0 -58
- package/src/generate/drums/fills.ts +0 -356
- package/src/generate/drums/ghost.ts +0 -119
- package/src/generate/drums/hihat.ts +0 -285
- package/src/generate/drums/hit.ts +0 -31
- package/src/generate/drums/index.ts +0 -329
- package/src/generate/drums/internal.ts +0 -257
- package/src/generate/drums/kick.ts +0 -160
- package/src/generate/drums/percussion.ts +0 -121
- package/src/generate/drums/rng.ts +0 -7
- package/src/generate/drums/swing.ts +0 -71
- package/src/generate/groove/index.ts +0 -326
- package/src/generate/harmonize/index.ts +0 -440
- package/src/generate/index.ts +0 -59
- package/src/generate/motif/index.ts +0 -398
- package/src/generate/progression/index.ts +0 -370
- package/src/generate/reharmony/index.ts +0 -330
- package/src/generate/rhythm/index.ts +0 -165
- package/src/index.ts +0 -247
- package/src/model/chord.ts +0 -423
- package/src/model/index.ts +0 -15
- package/src/model/interval.ts +0 -82
- package/src/model/key.ts +0 -266
- package/src/model/note.ts +0 -176
- package/src/model/progression.ts +0 -161
- package/src/model/shared.ts +0 -12
- package/src/theory/chord/index.ts +0 -373
- package/src/theory/chordscale/index.ts +0 -374
- package/src/theory/counterpoint/index.ts +0 -230
- package/src/theory/harmony/index.ts +0 -92
- package/src/theory/index.ts +0 -107
- package/src/theory/safety/index.ts +0 -419
- package/src/theory/scale/degrees.ts +0 -123
- package/src/theory/scale/index.ts +0 -28
- package/src/theory/scale/key.ts +0 -56
- package/src/theory/scale/masks.ts +0 -153
- package/src/theory/spelling/index.ts +0 -182
- package/src/theory/symbol/index.ts +0 -268
- package/src/theory/voicing/index.ts +0 -5
- package/src/theory/voicing/internal.ts +0 -168
- package/src/theory/voicing/leading.ts +0 -112
- package/src/theory/voicing/satb.ts +0 -195
- package/src/theory/voicing/styled.ts +0 -176
- package/tsconfig.json +0 -23
|
@@ -1,379 +0,0 @@
|
|
|
1
|
-
import type { TimeSignature } from '../../core/meter/index.js';
|
|
2
|
-
import { isStrongBeat } from '../../core/meter/index.js';
|
|
3
|
-
import type { Rng } from '../../core/random/index.js';
|
|
4
|
-
import { createRng } from '../../core/random/index.js';
|
|
5
|
-
import type { KeyScale, NoteEvent } from '../../core/types.js';
|
|
6
|
-
import type { Chord } from '../../theory/chord/index.js';
|
|
7
|
-
import { chordToneRole } from '../../theory/chord/index.js';
|
|
8
|
-
import { createsParallelPerfect, isForbiddenMelodicLeap } from '../../theory/counterpoint/index.js';
|
|
9
|
-
import type { VoiceSnapshot } from '../../theory/safety/index.js';
|
|
10
|
-
import { enumerateSafePitches } from '../../theory/safety/index.js';
|
|
11
|
-
import { nearestScaleTone } from '../../theory/scale/index.js';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Options controlling {@link generateCounterMelody}.
|
|
15
|
-
*
|
|
16
|
-
* @category Voicing & Counterpoint
|
|
17
|
-
*/
|
|
18
|
-
export type CounterMelodyOptions = {
|
|
19
|
-
/** The lead line to write against, in ascending onset order. */
|
|
20
|
-
melody: NoteEvent[];
|
|
21
|
-
/** Chord sounding at an absolute beat position, or null when none applies. */
|
|
22
|
-
chordAt: (beat: number) => Chord | null;
|
|
23
|
-
/** Key/scale context for scale-tone decisions. */
|
|
24
|
-
key: KeyScale;
|
|
25
|
-
/**
|
|
26
|
-
* Meter used for strong-beat decisions.
|
|
27
|
-
*
|
|
28
|
-
* @defaultValue 4/4
|
|
29
|
-
*/
|
|
30
|
-
ts?: TimeSignature;
|
|
31
|
-
/**
|
|
32
|
-
* Which side of the melody the counter line occupies.
|
|
33
|
-
*
|
|
34
|
-
* @defaultValue 'below'
|
|
35
|
-
*/
|
|
36
|
-
register?: 'above' | 'below';
|
|
37
|
-
/**
|
|
38
|
-
* Onset strategy: `'complement'` moves where the melody holds or rests and
|
|
39
|
-
* reinforces some strong beats; `'follow'` mirrors melody onsets.
|
|
40
|
-
*
|
|
41
|
-
* @defaultValue 'complement'
|
|
42
|
-
*/
|
|
43
|
-
rhythm?: 'complement' | 'follow';
|
|
44
|
-
/**
|
|
45
|
-
* Safety profile applied to candidate pitches.
|
|
46
|
-
*
|
|
47
|
-
* @defaultValue 'pop'
|
|
48
|
-
*/
|
|
49
|
-
profile?: 'strict' | 'pop';
|
|
50
|
-
/** Lowest MIDI pitch the counter line may use (default derived from `register`). */
|
|
51
|
-
pitchLow?: number;
|
|
52
|
-
/** Highest MIDI pitch the counter line may use (default derived from `register`). */
|
|
53
|
-
pitchHigh?: number;
|
|
54
|
-
/**
|
|
55
|
-
* PRNG seed; the same seed always yields the same line.
|
|
56
|
-
*
|
|
57
|
-
* @defaultValue 0
|
|
58
|
-
*/
|
|
59
|
-
seed?: number;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
/** Meter assumed when none is supplied. */
|
|
63
|
-
const DEFAULT_TS: TimeSignature = { numerator: 4, denominator: 4 };
|
|
64
|
-
|
|
65
|
-
/** Tolerance for beat-position comparisons. */
|
|
66
|
-
const EPS = 1e-9;
|
|
67
|
-
|
|
68
|
-
/** Grid resolution, in quarter-note beats, scanned for complement onsets. */
|
|
69
|
-
const GRID_STEP = 0.5;
|
|
70
|
-
|
|
71
|
-
/** Minimum spacing between counter onsets, keeping the line uncluttered. */
|
|
72
|
-
const MIN_ONSET_GAP = 1;
|
|
73
|
-
|
|
74
|
-
/** Semitone offset of the counter register's centre from the melody's mean pitch. */
|
|
75
|
-
const REGISTER_OFFSET = 12;
|
|
76
|
-
|
|
77
|
-
/** Half-width, in semitones, of the default counter register. */
|
|
78
|
-
const DEFAULT_HALF_RANGE = 10;
|
|
79
|
-
|
|
80
|
-
/** Probability of moving on a whole beat while the melody sustains. */
|
|
81
|
-
const HOLD_ON_BEAT_PROB = 0.55;
|
|
82
|
-
|
|
83
|
-
/** Probability of moving on an off-beat subdivision while the melody sustains. */
|
|
84
|
-
const HOLD_OFF_BEAT_PROB = 0.2;
|
|
85
|
-
|
|
86
|
-
/** Probability of reinforcing a strong beat that the melody also attacks. */
|
|
87
|
-
const REINFORCE_PROB = 0.35;
|
|
88
|
-
|
|
89
|
-
/** Velocity assumed for melody notes that carry none. */
|
|
90
|
-
const DEFAULT_MELODY_VELOCITY = 96;
|
|
91
|
-
|
|
92
|
-
/** How far below the concurrent melody velocity the counter line sits. */
|
|
93
|
-
const VELOCITY_DROP = 16;
|
|
94
|
-
|
|
95
|
-
/** Whether a beat position falls on a whole quarter-note beat. */
|
|
96
|
-
function isWholeBeat(beat: number): boolean {
|
|
97
|
-
return Math.abs(beat - Math.round(beat)) < EPS;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** The melody note sounding at a beat (latest onset wins on overlaps). */
|
|
101
|
-
function melodyNoteAt(melody: NoteEvent[], beat: number): NoteEvent | undefined {
|
|
102
|
-
let found: NoteEvent | undefined;
|
|
103
|
-
for (const note of melody) {
|
|
104
|
-
if (note.startBeat - EPS <= beat && beat < note.startBeat + note.durationBeat - EPS) {
|
|
105
|
-
if (!found || note.startBeat > found.startBeat) {
|
|
106
|
-
found = note;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return found;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** Whether any melody note attacks exactly at a beat. */
|
|
114
|
-
function melodyAttacksAt(melody: NoteEvent[], beat: number): boolean {
|
|
115
|
-
return melody.some((note) => Math.abs(note.startBeat - beat) < EPS);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** Counter onsets mirroring the melody's own onsets, deduplicated and sorted. */
|
|
119
|
-
function followOnsets(melody: NoteEvent[]): number[] {
|
|
120
|
-
return [...new Set(melody.map((note) => note.startBeat))].sort((a, b) => a - b);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Counter onsets complementing the melody: whole beats inside rests are always
|
|
125
|
-
* taken, positions where the melody sustains are taken probabilistically, and
|
|
126
|
-
* strong beats the melody also attacks are occasionally reinforced. A minimum
|
|
127
|
-
* gap between accepted onsets keeps the line sparse.
|
|
128
|
-
*/
|
|
129
|
-
function complementOnsets(
|
|
130
|
-
melody: NoteEvent[],
|
|
131
|
-
ts: TimeSignature,
|
|
132
|
-
spanStart: number,
|
|
133
|
-
spanEnd: number,
|
|
134
|
-
rng: Rng,
|
|
135
|
-
): number[] {
|
|
136
|
-
const onsets: number[] = [];
|
|
137
|
-
const gridStart = Math.floor(spanStart / GRID_STEP) * GRID_STEP;
|
|
138
|
-
let last = Number.NEGATIVE_INFINITY;
|
|
139
|
-
for (let beat = gridStart; beat < spanEnd - EPS; beat += GRID_STEP) {
|
|
140
|
-
const attacked = melodyAttacksAt(melody, beat);
|
|
141
|
-
const sounding = melodyNoteAt(melody, beat) !== undefined;
|
|
142
|
-
let place = false;
|
|
143
|
-
if (!attacked && !sounding) {
|
|
144
|
-
place = isWholeBeat(beat);
|
|
145
|
-
} else if (!attacked) {
|
|
146
|
-
place = rng.prob(isWholeBeat(beat) ? HOLD_ON_BEAT_PROB : HOLD_OFF_BEAT_PROB);
|
|
147
|
-
} else if (isStrongBeat(beat, ts)) {
|
|
148
|
-
place = rng.prob(REINFORCE_PROB);
|
|
149
|
-
}
|
|
150
|
-
if (place && beat - last >= MIN_ONSET_GAP - EPS) {
|
|
151
|
-
onsets.push(beat);
|
|
152
|
-
last = beat;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
return onsets;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Preference score for one candidate counter pitch. Rewards imperfect
|
|
160
|
-
* consonance with the melody, chord-tone membership, contrary or oblique
|
|
161
|
-
* motion, and stepwise movement; penalizes wide or forbidden leaps, weak-beat
|
|
162
|
-
* clashes, and drifting from the register centre.
|
|
163
|
-
*/
|
|
164
|
-
function scoreCandidate(
|
|
165
|
-
pitch: number,
|
|
166
|
-
melPitch: number | undefined,
|
|
167
|
-
melPrev: number | undefined,
|
|
168
|
-
prevPitch: number | undefined,
|
|
169
|
-
chord: Chord | null,
|
|
170
|
-
center: number,
|
|
171
|
-
): number {
|
|
172
|
-
let score = 0;
|
|
173
|
-
if (melPitch !== undefined) {
|
|
174
|
-
const ic = Math.abs(pitch - melPitch) % 12;
|
|
175
|
-
if (ic === 3 || ic === 4 || ic === 8 || ic === 9) {
|
|
176
|
-
score += 2; // imperfect consonance: thirds and sixths
|
|
177
|
-
} else if (ic === 0 || ic === 7) {
|
|
178
|
-
score += 0.5; // perfect consonance: allowed but less colourful
|
|
179
|
-
} else if (ic === 1 || ic === 2 || ic === 6 || ic === 10 || ic === 11) {
|
|
180
|
-
score -= 2; // dissonant even on weak beats
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
if (chord && chordToneRole(pitch, chord) !== null) {
|
|
184
|
-
score += 1;
|
|
185
|
-
}
|
|
186
|
-
if (prevPitch !== undefined) {
|
|
187
|
-
const move = pitch - prevPitch;
|
|
188
|
-
const dist = Math.abs(move);
|
|
189
|
-
score -= dist * 0.3;
|
|
190
|
-
if (dist > 0 && dist <= 2) {
|
|
191
|
-
score += 1; // stepwise motion
|
|
192
|
-
}
|
|
193
|
-
if (isForbiddenMelodicLeap(prevPitch, pitch)) {
|
|
194
|
-
score -= 6;
|
|
195
|
-
}
|
|
196
|
-
if (melPitch !== undefined && melPrev !== undefined) {
|
|
197
|
-
const melMove = melPitch - melPrev;
|
|
198
|
-
if ((move > 0 && melMove < 0) || (move < 0 && melMove > 0)) {
|
|
199
|
-
score += 2; // contrary motion
|
|
200
|
-
} else if (move === 0 || melMove === 0) {
|
|
201
|
-
score += 1; // oblique motion
|
|
202
|
-
} else {
|
|
203
|
-
score -= 1; // similar motion
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
score -= Math.abs(pitch - center) * 0.02;
|
|
208
|
-
return score;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Last-resort pitch when no safe candidate survives filtering: the scale tone
|
|
213
|
-
* nearest the previous pitch (or the register centre), pushed onto the correct
|
|
214
|
-
* side of the sounding melody note.
|
|
215
|
-
*/
|
|
216
|
-
function fallbackPitch(
|
|
217
|
-
melPitch: number | undefined,
|
|
218
|
-
prevPitch: number | undefined,
|
|
219
|
-
center: number,
|
|
220
|
-
register: 'above' | 'below',
|
|
221
|
-
low: number,
|
|
222
|
-
high: number,
|
|
223
|
-
key: KeyScale,
|
|
224
|
-
): number {
|
|
225
|
-
let target = prevPitch ?? Math.round(center);
|
|
226
|
-
if (melPitch !== undefined) {
|
|
227
|
-
if (register === 'below' && target >= melPitch) {
|
|
228
|
-
target = melPitch - 5;
|
|
229
|
-
} else if (register === 'above' && target <= melPitch) {
|
|
230
|
-
target = melPitch + 5;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
target = Math.min(Math.max(target, low), high);
|
|
234
|
-
let pitch = nearestScaleTone(target, key);
|
|
235
|
-
// Push onto the requested side of the melody, but only by whole octaves that
|
|
236
|
-
// keep the pitch inside [low, high]; the declared range is a hard bound while
|
|
237
|
-
// the register side is a soft preference. A final clamp guarantees the range.
|
|
238
|
-
if (melPitch !== undefined) {
|
|
239
|
-
while (register === 'below' && pitch >= melPitch && pitch - 12 >= low) {
|
|
240
|
-
pitch -= 12;
|
|
241
|
-
}
|
|
242
|
-
while (register === 'above' && pitch <= melPitch && pitch + 12 <= high) {
|
|
243
|
-
pitch += 12;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
return Math.min(Math.max(pitch, low), high);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Generate a counter melody against a lead line.
|
|
251
|
-
*
|
|
252
|
-
* Onsets are chosen per `rhythm`: `'follow'` places one counter note on each
|
|
253
|
-
* melody onset; `'complement'` fills melody rests and sustains and reinforces
|
|
254
|
-
* an occasional strong beat, spaced at least a beat apart. Each onset's pitch
|
|
255
|
-
* is picked from the safe pitches in the counter register (chord tones and
|
|
256
|
-
* consonant tensions against the sounding melody note), rejecting candidates
|
|
257
|
-
* that would form a parallel perfect interval with the melody or sit on the
|
|
258
|
-
* wrong side of it, then scored to favour contrary or oblique motion, imperfect
|
|
259
|
-
* consonance, and stepwise movement. Ties break toward the lower pitch, so a
|
|
260
|
-
* given seed always yields the same line. Notes extend to the next counter
|
|
261
|
-
* onset (the last to the melody's end) at a velocity slightly under the melody.
|
|
262
|
-
*
|
|
263
|
-
* @param opts The lead line, chord context callback, key, and generation knobs.
|
|
264
|
-
* @returns The counter line as note events sorted by onset; `[]` for an empty melody.
|
|
265
|
-
* @example
|
|
266
|
-
* ```ts
|
|
267
|
-
* import { generateCounterMelody, majorKey, parseChordSymbol } from '@libraz/libcantus';
|
|
268
|
-
* const melody = [{ pitch: 72, startBeat: 0, durationBeat: 2 }];
|
|
269
|
-
* const counter = generateCounterMelody({
|
|
270
|
-
* melody,
|
|
271
|
-
* chordAt: () => parseChordSymbol('C'),
|
|
272
|
-
* key: majorKey(0),
|
|
273
|
-
* }); // note events below the melody, in onset order
|
|
274
|
-
* ```
|
|
275
|
-
* @category Voicing & Counterpoint
|
|
276
|
-
*/
|
|
277
|
-
export function generateCounterMelody(opts: CounterMelodyOptions): NoteEvent[] {
|
|
278
|
-
const melody = [...opts.melody].sort((a, b) => a.startBeat - b.startBeat);
|
|
279
|
-
if (melody.length === 0) {
|
|
280
|
-
return [];
|
|
281
|
-
}
|
|
282
|
-
const ts = opts.ts ?? DEFAULT_TS;
|
|
283
|
-
const register = opts.register ?? 'below';
|
|
284
|
-
const rhythm = opts.rhythm ?? 'complement';
|
|
285
|
-
const profile = opts.profile ?? 'pop';
|
|
286
|
-
const rng = createRng(opts.seed ?? 0);
|
|
287
|
-
|
|
288
|
-
const spanStart = melody[0]?.startBeat ?? 0;
|
|
289
|
-
const spanEnd = melody.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), spanStart);
|
|
290
|
-
const meanPitch = melody.reduce((sum, n) => sum + n.pitch, 0) / melody.length;
|
|
291
|
-
const defaultCenter =
|
|
292
|
-
register === 'below' ? meanPitch - REGISTER_OFFSET : meanPitch + REGISTER_OFFSET;
|
|
293
|
-
const low = opts.pitchLow ?? Math.round(defaultCenter) - DEFAULT_HALF_RANGE;
|
|
294
|
-
const high = opts.pitchHigh ?? Math.round(defaultCenter) + DEFAULT_HALF_RANGE;
|
|
295
|
-
const center = (low + high) / 2;
|
|
296
|
-
|
|
297
|
-
const onsets =
|
|
298
|
-
rhythm === 'follow'
|
|
299
|
-
? followOnsets(melody)
|
|
300
|
-
: complementOnsets(melody, ts, spanStart, spanEnd, rng);
|
|
301
|
-
|
|
302
|
-
const out: NoteEvent[] = [];
|
|
303
|
-
let prevPitch: number | undefined;
|
|
304
|
-
let prevOnset: number | undefined;
|
|
305
|
-
for (const beat of onsets) {
|
|
306
|
-
const melNote = melodyNoteAt(melody, beat);
|
|
307
|
-
const melPitch = melNote?.pitch;
|
|
308
|
-
const melPrev = prevOnset !== undefined ? melodyNoteAt(melody, prevOnset)?.pitch : undefined;
|
|
309
|
-
const chord = opts.chordAt(beat);
|
|
310
|
-
const otherVoices: VoiceSnapshot[] =
|
|
311
|
-
melPitch !== undefined ? [{ pitch: melPitch, prevPitch: melPrev }] : [];
|
|
312
|
-
|
|
313
|
-
const candidates = enumerateSafePitches(
|
|
314
|
-
{
|
|
315
|
-
profile,
|
|
316
|
-
prevPitch,
|
|
317
|
-
chord,
|
|
318
|
-
key: opts.key,
|
|
319
|
-
otherVoices,
|
|
320
|
-
strongBeat: isStrongBeat(beat, ts),
|
|
321
|
-
vocalLow: low,
|
|
322
|
-
vocalHigh: high,
|
|
323
|
-
},
|
|
324
|
-
low,
|
|
325
|
-
high,
|
|
326
|
-
);
|
|
327
|
-
|
|
328
|
-
let bestPitch: number | undefined;
|
|
329
|
-
let bestScore = Number.NEGATIVE_INFINITY;
|
|
330
|
-
for (const candidate of candidates) {
|
|
331
|
-
if (melPitch !== undefined) {
|
|
332
|
-
if (register === 'below' && candidate >= melPitch) {
|
|
333
|
-
continue;
|
|
334
|
-
}
|
|
335
|
-
if (register === 'above' && candidate <= melPitch) {
|
|
336
|
-
continue;
|
|
337
|
-
}
|
|
338
|
-
if (
|
|
339
|
-
prevPitch !== undefined &&
|
|
340
|
-
melPrev !== undefined &&
|
|
341
|
-
// Parallel octaves are the perfect-class-zero case of this predicate,
|
|
342
|
-
// so a single check covers both parallel fifths and octaves.
|
|
343
|
-
createsParallelPerfect(prevPitch, candidate, melPrev, melPitch)
|
|
344
|
-
) {
|
|
345
|
-
continue;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
const score = scoreCandidate(candidate, melPitch, melPrev, prevPitch, chord, center);
|
|
349
|
-
const tie =
|
|
350
|
-
Math.abs(score - bestScore) <= EPS && candidate < (bestPitch ?? Number.POSITIVE_INFINITY);
|
|
351
|
-
if (score > bestScore + EPS || tie) {
|
|
352
|
-
bestScore = score;
|
|
353
|
-
bestPitch = candidate;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const pitch =
|
|
358
|
-
bestPitch ?? fallbackPitch(melPitch, prevPitch, center, register, low, high, opts.key);
|
|
359
|
-
const velocity = Math.max(
|
|
360
|
-
1,
|
|
361
|
-
Math.min(127, Math.round((melNote?.velocity ?? DEFAULT_MELODY_VELOCITY) - VELOCITY_DROP)),
|
|
362
|
-
);
|
|
363
|
-
out.push({ pitch, startBeat: beat, durationBeat: GRID_STEP, velocity });
|
|
364
|
-
prevPitch = pitch;
|
|
365
|
-
prevOnset = beat;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Extend each note to the next counter onset; the last to the melody's end.
|
|
369
|
-
for (let i = 0; i < out.length; i += 1) {
|
|
370
|
-
const note = out[i];
|
|
371
|
-
if (!note) {
|
|
372
|
-
continue;
|
|
373
|
-
}
|
|
374
|
-
const next = out[i + 1];
|
|
375
|
-
const end = next ? next.startBeat : Math.max(spanEnd, note.startBeat + GRID_STEP);
|
|
376
|
-
note.durationBeat = end - note.startBeat;
|
|
377
|
-
}
|
|
378
|
-
return out;
|
|
379
|
-
}
|
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getGhostDensity,
|
|
3
|
-
getGhostProbabilityAtPosition,
|
|
4
|
-
getGhostVelocity,
|
|
5
|
-
selectGhostPositions,
|
|
6
|
-
} from './ghost.js';
|
|
7
|
-
import {
|
|
8
|
-
footHiHatVelocity,
|
|
9
|
-
type HiHatLevel,
|
|
10
|
-
hiHatNote,
|
|
11
|
-
hiHatTypeVelocityMultiplier,
|
|
12
|
-
hiHatVelocityMultiplier,
|
|
13
|
-
OHH_VEL_BOOST,
|
|
14
|
-
roleHiHatInstrument,
|
|
15
|
-
sectionHiHatType,
|
|
16
|
-
shouldAddOpenHHAccent,
|
|
17
|
-
shouldPlayHiHat,
|
|
18
|
-
shouldUseBridgeCrossStick,
|
|
19
|
-
} from './hihat.js';
|
|
20
|
-
import type { HitList } from './hit.js';
|
|
21
|
-
import type { BackingDensity, DrumRole, DrumStyle, Feel, SectionType } from './internal.js';
|
|
22
|
-
import { EIGHTH, GM, type MoodCategory, SIXTEENTH } from './internal.js';
|
|
23
|
-
import type { KickPattern } from './kick.js';
|
|
24
|
-
import type { DrumRng } from './rng.js';
|
|
25
|
-
import { effectiveSwing, quantizeSwing } from './swing.js';
|
|
26
|
-
|
|
27
|
-
/** Per-section drum state shared across the bars of a section. */
|
|
28
|
-
export type SectionCtx = {
|
|
29
|
-
style: DrumStyle;
|
|
30
|
-
feel: Feel;
|
|
31
|
-
densityMult: number;
|
|
32
|
-
backingDensity: BackingDensity;
|
|
33
|
-
hhLevel: HiHatLevel;
|
|
34
|
-
useGhostNotes: boolean;
|
|
35
|
-
ghostBoost: boolean;
|
|
36
|
-
useRide: boolean;
|
|
37
|
-
useFootHh: boolean;
|
|
38
|
-
role: DrumRole;
|
|
39
|
-
ghostMood: MoodCategory;
|
|
40
|
-
snareBeat3: boolean;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
/** Per-beat context passed to every beat processor. */
|
|
44
|
-
export type BeatCtx = {
|
|
45
|
-
track: HitList;
|
|
46
|
-
beatTick: number;
|
|
47
|
-
beat: number;
|
|
48
|
-
velocity: number;
|
|
49
|
-
section: SectionType;
|
|
50
|
-
bpm: number;
|
|
51
|
-
bar: number;
|
|
52
|
-
sectionBars: number;
|
|
53
|
-
inPrechorusLift: boolean;
|
|
54
|
-
swingAmount: number;
|
|
55
|
-
barHasOpenHh: boolean;
|
|
56
|
-
openHhBeat: number;
|
|
57
|
-
rng: DrumRng;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
function swing16(tick: number, feel: Feel, swingAmount: number): number {
|
|
61
|
-
return quantizeSwing(tick, effectiveSwing(feel, swingAmount), 'sixteenth');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function hatSwingFactor(style: DrumStyle): number {
|
|
65
|
-
if (style === 'trap') {
|
|
66
|
-
return 0;
|
|
67
|
-
}
|
|
68
|
-
if (style === 'latin') {
|
|
69
|
-
return 0.35;
|
|
70
|
-
}
|
|
71
|
-
return 0.5;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Emit the kick for one beat. */
|
|
75
|
-
export function generateKickForBeat(ctx: BeatCtx, sec: SectionCtx, kick: KickPattern): void {
|
|
76
|
-
if (ctx.inPrechorusLift) {
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
const on = [kick.beat1, kick.beat2, kick.beat3, kick.beat4][ctx.beat] ?? false;
|
|
80
|
-
const and = [kick.beat1and, kick.beat2and, kick.beat3and, kick.beat4and][ctx.beat] ?? false;
|
|
81
|
-
|
|
82
|
-
if (on) {
|
|
83
|
-
ctx.track.add(GM.BD, ctx.beatTick, EIGHTH, ctx.velocity);
|
|
84
|
-
}
|
|
85
|
-
if (and) {
|
|
86
|
-
const andTick = swing16(ctx.beatTick + EIGHTH, sec.feel, ctx.swingAmount);
|
|
87
|
-
ctx.track.add(GM.BD, andTick, EIGHTH, ctx.velocity * 0.85);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Emit the backbeat snare (or side-stick) for one beat. */
|
|
92
|
-
export function generateSnareForBeat(ctx: BeatCtx, sec: SectionCtx, isIntroFirst: boolean): void {
|
|
93
|
-
if (ctx.inPrechorusLift) {
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
const snareOn = sec.snareBeat3 ? ctx.beat === 2 : ctx.beat === 1 || ctx.beat === 3;
|
|
97
|
-
if (!snareOn || isIntroFirst) {
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
if (sec.useRide && shouldUseBridgeCrossStick(ctx.section, ctx.beat)) {
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const backbeatVel = Math.min(127, ctx.velocity + 16);
|
|
105
|
-
const promoteSparseChorus =
|
|
106
|
-
sec.style === 'sparse' && ctx.section === 'chorus' && sec.role === 'full';
|
|
107
|
-
|
|
108
|
-
if (promoteSparseChorus) {
|
|
109
|
-
ctx.track.add(GM.SD, ctx.beatTick, EIGHTH, backbeatVel);
|
|
110
|
-
} else if (sec.style === 'sparse' || sec.role === 'ambient') {
|
|
111
|
-
if (sec.role !== 'fxOnly' && sec.role !== 'minimal') {
|
|
112
|
-
ctx.track.add(GM.SIDESTICK, ctx.beatTick, EIGHTH, ctx.velocity * 0.8);
|
|
113
|
-
}
|
|
114
|
-
} else {
|
|
115
|
-
ctx.track.add(GM.SD, ctx.beatTick, EIGHTH, backbeatVel);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** Emit ghost snares at the "e"/"a" 16ths of beats 1 and 3. */
|
|
120
|
-
export function generateGhostNotesForBeat(ctx: BeatCtx, sec: SectionCtx): void {
|
|
121
|
-
if (ctx.beat !== 0 && ctx.beat !== 2) {
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
const positions = selectGhostPositions(sec.ghostMood, ctx.rng);
|
|
125
|
-
let ghostProb = getGhostDensity(sec.ghostMood, ctx.section, sec.backingDensity, ctx.bpm);
|
|
126
|
-
if (sec.ghostBoost) {
|
|
127
|
-
ghostProb = Math.min(1, ghostProb * 1.4);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
for (const pos of positions) {
|
|
131
|
-
const sixteenthInBeat = pos === 'e' ? 1 : 3;
|
|
132
|
-
const posProb = getGhostProbabilityAtPosition(ctx.beat, sixteenthInBeat);
|
|
133
|
-
if (!ctx.rng.prob(ghostProb * posProb)) {
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
const variation = ctx.rng.float(0.85, 1.15);
|
|
137
|
-
const ghostBase = getGhostVelocity(ctx.section, ctx.beat / 2, false);
|
|
138
|
-
let ghostVel = ctx.velocity * ghostBase * variation;
|
|
139
|
-
if (pos === 'a') {
|
|
140
|
-
ghostVel *= 0.9;
|
|
141
|
-
}
|
|
142
|
-
const offset = pos === 'e' ? SIXTEENTH : 3 * SIXTEENTH;
|
|
143
|
-
const tick = swing16(ctx.beatTick + offset, sec.feel, ctx.swingAmount);
|
|
144
|
-
ctx.track.add(GM.SD, tick, SIXTEENTH, ghostVel);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/** Emit the pre-chorus lift buildup for one beat. */
|
|
149
|
-
export function generatePreChorusBuildup(
|
|
150
|
-
ctx: BeatCtx,
|
|
151
|
-
sec: SectionCtx,
|
|
152
|
-
isSectionLastBar: boolean,
|
|
153
|
-
): void {
|
|
154
|
-
if (sec.style === 'sparse') {
|
|
155
|
-
if (isSectionLastBar && ctx.beat === 3) {
|
|
156
|
-
ctx.track.add(GM.SD, ctx.beatTick, EIGHTH, Math.max(45, ctx.velocity * 0.75));
|
|
157
|
-
ctx.track.add(GM.CRASH, ctx.beatTick + EIGHTH + SIXTEENTH, SIXTEENTH, ctx.velocity * 0.9);
|
|
158
|
-
}
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
const barsInLift = 2;
|
|
162
|
-
const barInLift = ctx.bar - (ctx.sectionBars - barsInLift);
|
|
163
|
-
const progress = (barInLift * 4 + ctx.beat) / (barsInLift * 4);
|
|
164
|
-
const buildupVel = ctx.velocity * (0.5 + 0.5 * progress);
|
|
165
|
-
ctx.track.add(GM.SD, ctx.beatTick, EIGHTH, buildupVel);
|
|
166
|
-
ctx.track.add(GM.SD, ctx.beatTick + EIGHTH, EIGHTH, buildupVel * 0.85);
|
|
167
|
-
if (isSectionLastBar && ctx.beat === 3) {
|
|
168
|
-
ctx.track.add(GM.CRASH, ctx.beatTick + EIGHTH + SIXTEENTH, SIXTEENTH, ctx.velocity * 1.1);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/** Emit the timekeeping hi-hat (or ride/foot) for one beat. */
|
|
173
|
-
export function generateHiHatForBeat(ctx: BeatCtx, sec: SectionCtx): void {
|
|
174
|
-
if (!shouldPlayHiHat(sec.role)) {
|
|
175
|
-
if (sec.useFootHh && (ctx.beat === 0 || ctx.beat === 2)) {
|
|
176
|
-
ctx.track.add(GM.FHH, ctx.beatTick, EIGHTH, footHiHatVelocity(ctx.rng));
|
|
177
|
-
}
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const crossStick = sec.useRide && shouldUseBridgeCrossStick(ctx.section, ctx.beat);
|
|
182
|
-
const hhInstrument = crossStick ? GM.SIDESTICK : roleHiHatInstrument(sec.role, sec.useRide);
|
|
183
|
-
const hhType = sectionHiHatType(ctx.section, sec.role);
|
|
184
|
-
const typeMult = hiHatTypeVelocityMultiplier(hhType);
|
|
185
|
-
const isDynamicOpen = ctx.barHasOpenHh && ctx.beat === ctx.openHhBeat;
|
|
186
|
-
const dm = sec.densityMult;
|
|
187
|
-
|
|
188
|
-
if (sec.hhLevel === 'quarter') {
|
|
189
|
-
const introRest = ctx.section === 'intro' && ctx.beat !== 0;
|
|
190
|
-
if (!introRest) {
|
|
191
|
-
if (isDynamicOpen) {
|
|
192
|
-
ctx.track.add(
|
|
193
|
-
GM.OHH,
|
|
194
|
-
ctx.beatTick,
|
|
195
|
-
EIGHTH,
|
|
196
|
-
ctx.velocity * dm * 0.75 * typeMult + OHH_VEL_BOOST,
|
|
197
|
-
);
|
|
198
|
-
} else {
|
|
199
|
-
ctx.track.add(
|
|
200
|
-
hhInstrument,
|
|
201
|
-
ctx.beatTick,
|
|
202
|
-
EIGHTH,
|
|
203
|
-
Math.max(20, ctx.velocity * dm * 0.75 * typeMult),
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
} else if (sec.useFootHh) {
|
|
207
|
-
ctx.track.add(GM.FHH, ctx.beatTick, EIGHTH, footHiHatVelocity(ctx.rng));
|
|
208
|
-
}
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (sec.hhLevel === 'eighth') {
|
|
213
|
-
for (let eighth = 0; eighth < 2; eighth += 1) {
|
|
214
|
-
let hhTick = ctx.beatTick + eighth * EIGHTH;
|
|
215
|
-
if (eighth === 1) {
|
|
216
|
-
hhTick = swing16(hhTick, sec.feel, ctx.swingAmount);
|
|
217
|
-
}
|
|
218
|
-
if (ctx.section === 'intro' && eighth === 1) {
|
|
219
|
-
if (sec.useFootHh && ctx.beat % 2 === 0) {
|
|
220
|
-
ctx.track.add(GM.FHH, hhTick, EIGHTH, footHiHatVelocity(ctx.rng));
|
|
221
|
-
}
|
|
222
|
-
continue;
|
|
223
|
-
}
|
|
224
|
-
const hhVel = Math.max(20, ctx.velocity * dm * typeMult * (eighth === 0 ? 0.9 : 0.65));
|
|
225
|
-
if (isDynamicOpen && eighth === 0) {
|
|
226
|
-
ctx.track.add(GM.OHH, hhTick, EIGHTH, hhVel + OHH_VEL_BOOST);
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
|
-
let useOpen = false;
|
|
230
|
-
if (sec.style === 'fourOnFloor' && eighth === 1) {
|
|
231
|
-
const openProb = Math.max(0.15, Math.min(0.8, 45 / ctx.bpm));
|
|
232
|
-
useOpen = (ctx.beat === 1 || ctx.beat === 3) && ctx.rng.prob(openProb);
|
|
233
|
-
} else if (eighth === 0) {
|
|
234
|
-
useOpen = shouldAddOpenHHAccent(ctx.section, ctx.beat, ctx.bar, ctx.rng);
|
|
235
|
-
}
|
|
236
|
-
if (useOpen) {
|
|
237
|
-
ctx.track.add(hiHatNote('open'), hhTick, EIGHTH, Math.max(20, hhVel * 1.1));
|
|
238
|
-
} else {
|
|
239
|
-
ctx.track.add(hhInstrument, hhTick, EIGHTH / 2, hhVel);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
for (let sixteenth = 0; sixteenth < 4; sixteenth += 1) {
|
|
246
|
-
let hhTick = ctx.beatTick + sixteenth * SIXTEENTH;
|
|
247
|
-
if (sixteenth === 1 || sixteenth === 3) {
|
|
248
|
-
const swung = effectiveSwing(sec.feel, ctx.swingAmount) * hatSwingFactor(sec.style);
|
|
249
|
-
hhTick = quantizeSwing(hhTick, swung, 'sixteenth');
|
|
250
|
-
}
|
|
251
|
-
const metricVel = hiHatVelocityMultiplier(sixteenth, ctx.rng);
|
|
252
|
-
const hhVel = Math.max(20, ctx.velocity * dm * typeMult * metricVel);
|
|
253
|
-
if (isDynamicOpen && sixteenth === 0) {
|
|
254
|
-
ctx.track.add(GM.OHH, hhTick, SIXTEENTH, hhVel + OHH_VEL_BOOST);
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
if (ctx.beat === 3 && sixteenth === 3) {
|
|
258
|
-
const openProb = Math.max(0.1, Math.min(0.4, 30 / ctx.bpm));
|
|
259
|
-
if (ctx.rng.prob(openProb)) {
|
|
260
|
-
ctx.track.add(GM.OHH, hhTick, SIXTEENTH, Math.max(20, hhVel * 1.2));
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
ctx.track.add(hhInstrument, hhTick, SIXTEENTH / 2, hhVel);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Generate a euclidean onset pattern (Bjorklund's algorithm), distributing
|
|
3
|
-
* `pulses` as evenly as possible across `steps`, then rotating.
|
|
4
|
-
*
|
|
5
|
-
* @param pulses Number of onsets, clamped to [0, steps].
|
|
6
|
-
* @param steps Total steps (>= 1).
|
|
7
|
-
* @param rotation Steps to rotate onsets toward later positions.
|
|
8
|
-
* @returns One boolean per step; true marks an onset.
|
|
9
|
-
*/
|
|
10
|
-
export function euclideanRhythm(pulses: number, steps: number, rotation = 0): boolean[] {
|
|
11
|
-
if (steps < 1) {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
const p = Math.max(0, Math.min(pulses, steps));
|
|
15
|
-
const base = bjorklund(p, steps);
|
|
16
|
-
const shift = ((rotation % steps) + steps) % steps;
|
|
17
|
-
if (shift === 0) {
|
|
18
|
-
return base;
|
|
19
|
-
}
|
|
20
|
-
return base.map((_, i) => base[(i - shift + steps) % steps] ?? false);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** True when the 16-step bitmask has an onset at `step`. */
|
|
24
|
-
export function hasHit(mask: number, step: number): boolean {
|
|
25
|
-
return ((mask >> step) & 1) === 1;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Convert a euclidean boolean pattern to a 16-step bitmask (LSB = step 0). */
|
|
29
|
-
export function patternToMask(pattern: boolean[]): number {
|
|
30
|
-
let mask = 0;
|
|
31
|
-
for (let i = 0; i < pattern.length; i += 1) {
|
|
32
|
-
if (pattern[i]) {
|
|
33
|
-
mask |= 1 << i;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return mask;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function bjorklund(pulses: number, steps: number): boolean[] {
|
|
40
|
-
if (pulses === 0) {
|
|
41
|
-
return Array.from({ length: steps }, () => false);
|
|
42
|
-
}
|
|
43
|
-
if (pulses >= steps) {
|
|
44
|
-
return Array.from({ length: steps }, () => true);
|
|
45
|
-
}
|
|
46
|
-
let head: boolean[][] = Array.from({ length: pulses }, () => [true]);
|
|
47
|
-
let remainder: boolean[][] = Array.from({ length: steps - pulses }, () => [false]);
|
|
48
|
-
while (remainder.length > 1) {
|
|
49
|
-
const count = Math.min(head.length, remainder.length);
|
|
50
|
-
const next: boolean[][] = [];
|
|
51
|
-
for (let i = 0; i < count; i += 1) {
|
|
52
|
-
next.push([...(head[i] ?? []), ...(remainder[i] ?? [])]);
|
|
53
|
-
}
|
|
54
|
-
remainder = head.length > count ? head.slice(count) : remainder.slice(count);
|
|
55
|
-
head = next;
|
|
56
|
-
}
|
|
57
|
-
return [...head.flat(), ...remainder.flat()];
|
|
58
|
-
}
|