@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,18 +1,24 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createRng
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-HKGYCQ34.js";
|
|
4
4
|
import {
|
|
5
5
|
beatsPerBar,
|
|
6
|
+
createNoteEventIndex,
|
|
6
7
|
formatTimeSignature,
|
|
7
8
|
isStrongBeat,
|
|
8
9
|
metricWeight
|
|
9
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-YLEMGEFH.js";
|
|
11
|
+
import {
|
|
12
|
+
isDiatonic,
|
|
13
|
+
parallelKey
|
|
14
|
+
} from "./chunk-KMHOICIY.js";
|
|
10
15
|
import {
|
|
11
16
|
roleOf
|
|
12
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-NKIBQ6PT.js";
|
|
13
18
|
import {
|
|
14
|
-
enumerateSafePitches
|
|
15
|
-
|
|
19
|
+
enumerateSafePitches,
|
|
20
|
+
evaluateSafety
|
|
21
|
+
} from "./chunk-NYYWMV2C.js";
|
|
16
22
|
import {
|
|
17
23
|
NATURAL_MINOR_MASK,
|
|
18
24
|
chordPitchClasses,
|
|
@@ -26,9 +32,23 @@ import {
|
|
|
26
32
|
minorKey,
|
|
27
33
|
nearestScaleTone,
|
|
28
34
|
scaleTonesInDegreeOrder
|
|
29
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-D2FAJBYL.js";
|
|
36
|
+
import {
|
|
37
|
+
InvalidInputError,
|
|
38
|
+
assertFiniteNumber,
|
|
39
|
+
assertGenerationBudget,
|
|
40
|
+
assertInteger,
|
|
41
|
+
assertNoteEvents,
|
|
42
|
+
assertOneOf,
|
|
43
|
+
assertPositiveInt,
|
|
44
|
+
assertRange,
|
|
45
|
+
assertTimeSignature,
|
|
46
|
+
dropSilentNotes,
|
|
47
|
+
pitchClassOf
|
|
48
|
+
} from "./chunk-XEBG3G53.js";
|
|
30
49
|
|
|
31
50
|
// src/generate/bass/index.ts
|
|
51
|
+
var BASS_STYLES = ["root", "rootFifth", "pop", "walking", "arpeggio"];
|
|
32
52
|
var DEFAULT_TS = { numerator: 4, denominator: 4 };
|
|
33
53
|
var DEFAULT_STYLE = "root";
|
|
34
54
|
var DEFAULT_OCTAVE = 2;
|
|
@@ -36,9 +56,6 @@ var STRONG_VELOCITY = 100;
|
|
|
36
56
|
var WEAK_VELOCITY = 80;
|
|
37
57
|
var PICKUP_PROB = 0.35;
|
|
38
58
|
var EPS = 1e-9;
|
|
39
|
-
function pitchClass(value) {
|
|
40
|
-
return (Math.trunc(value) % 12 + 12) % 12;
|
|
41
|
-
}
|
|
42
59
|
function placePc(pc, anchor, low) {
|
|
43
60
|
const high = low + 12;
|
|
44
61
|
let midi = pc + 12 * Math.round((anchor - pc) / 12);
|
|
@@ -54,24 +71,24 @@ function chordTonePcs(chord) {
|
|
|
54
71
|
const seen = /* @__PURE__ */ new Set();
|
|
55
72
|
const pcs = [];
|
|
56
73
|
for (const interval of chord.intervals) {
|
|
57
|
-
const pc =
|
|
74
|
+
const pc = pitchClassOf(chord.rootPc + interval);
|
|
58
75
|
if (!seen.has(pc)) {
|
|
59
76
|
seen.add(pc);
|
|
60
77
|
pcs.push(pc);
|
|
61
78
|
}
|
|
62
79
|
}
|
|
63
80
|
if (pcs.length === 0) {
|
|
64
|
-
pcs.push(
|
|
81
|
+
pcs.push(pitchClassOf(chord.rootPc));
|
|
65
82
|
}
|
|
66
83
|
return pcs;
|
|
67
84
|
}
|
|
68
85
|
function fifthPcOf(chord) {
|
|
69
|
-
const fifths = new Set(chord.intervals.map((i) =>
|
|
86
|
+
const fifths = new Set(chord.intervals.map((i) => pitchClassOf(i)));
|
|
70
87
|
const chosen = fifths.has(7) ? 7 : fifths.has(6) ? 6 : fifths.has(8) ? 8 : void 0;
|
|
71
|
-
return chosen === void 0 ?
|
|
88
|
+
return chosen === void 0 ? pitchClassOf(chord.rootPc) : pitchClassOf(chord.rootPc + chosen);
|
|
72
89
|
}
|
|
73
90
|
function bassPcOf(chord) {
|
|
74
|
-
return
|
|
91
|
+
return pitchClassOf(chord.bassPc ?? chord.rootPc);
|
|
75
92
|
}
|
|
76
93
|
function beatPositions(start, end) {
|
|
77
94
|
const positions = [];
|
|
@@ -110,7 +127,8 @@ function buildPop(ctx, seg) {
|
|
|
110
127
|
} else if (ctx.rng.prob(PICKUP_PROB)) {
|
|
111
128
|
if (ctx.rng.prob(0.5)) {
|
|
112
129
|
const base = placePc(rootPc, ctx.prevMidi, ctx.low);
|
|
113
|
-
|
|
130
|
+
const dropped = base - 12;
|
|
131
|
+
emit(ctx, pos, rootPc, dropped >= 0 ? dropped : base);
|
|
114
132
|
} else {
|
|
115
133
|
ctx.prevMidi = emit(ctx, pos, fifthPcOf(seg.chord));
|
|
116
134
|
}
|
|
@@ -130,12 +148,23 @@ function buildArpeggio(ctx, seg) {
|
|
|
130
148
|
}
|
|
131
149
|
function approachNote(ctx, target, from) {
|
|
132
150
|
const dir = from <= target ? -1 : 1;
|
|
151
|
+
const chromatic = target + dir;
|
|
133
152
|
if (ctx.rng.prob(0.5)) {
|
|
134
|
-
return
|
|
153
|
+
return foldIntoBand(chromatic, ctx.low);
|
|
135
154
|
}
|
|
136
155
|
const cand = nearestScaleTone(target + dir * 2, ctx.key);
|
|
137
156
|
const step = Math.abs(cand - target);
|
|
138
|
-
return step >= 1 && step <= 2 ? cand :
|
|
157
|
+
return foldIntoBand(step >= 1 && step <= 2 ? cand : chromatic, ctx.low);
|
|
158
|
+
}
|
|
159
|
+
function foldIntoBand(midi, low) {
|
|
160
|
+
let result = midi;
|
|
161
|
+
while (result < low) {
|
|
162
|
+
result += 12;
|
|
163
|
+
}
|
|
164
|
+
while (result > low + 12) {
|
|
165
|
+
result -= 12;
|
|
166
|
+
}
|
|
167
|
+
return result;
|
|
139
168
|
}
|
|
140
169
|
function buildWalking(ctx, seg, next) {
|
|
141
170
|
const tones = chordTonePcs(seg.chord);
|
|
@@ -149,7 +178,7 @@ function buildWalking(ctx, seg, next) {
|
|
|
149
178
|
if (next && count > 1 && i === count - 1) {
|
|
150
179
|
const nextBass = placePc(bassPcOf(next.chord), ctx.prevMidi, ctx.low);
|
|
151
180
|
const midi = approachNote(ctx, nextBass, ctx.prevMidi);
|
|
152
|
-
ctx.prevMidi = emit(ctx, pos,
|
|
181
|
+
ctx.prevMidi = emit(ctx, pos, pitchClassOf(midi), midi);
|
|
153
182
|
return;
|
|
154
183
|
}
|
|
155
184
|
const pc = tones[i % tones.length] ?? bassPcOf(seg.chord);
|
|
@@ -157,13 +186,30 @@ function buildWalking(ctx, seg, next) {
|
|
|
157
186
|
});
|
|
158
187
|
}
|
|
159
188
|
function generateBassLine(opts) {
|
|
189
|
+
assertGenerationBudget(opts.segments.length, "bass segments");
|
|
190
|
+
for (let index = 0; index < opts.segments.length; index += 1) {
|
|
191
|
+
const segment = opts.segments[index];
|
|
192
|
+
if (!segment) continue;
|
|
193
|
+
assertRange(segment.startBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].startBeat`);
|
|
194
|
+
assertRange(segment.endBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].endBeat`);
|
|
195
|
+
if (segment.endBeat <= segment.startBeat) {
|
|
196
|
+
throw new InvalidInputError(`segments[${index}] must have a positive duration`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
160
199
|
const segments = [...opts.segments].sort((a, b) => a.startBeat - b.startBeat);
|
|
161
200
|
if (segments.length === 0) {
|
|
162
201
|
return [];
|
|
163
202
|
}
|
|
164
203
|
const ts = opts.ts ?? DEFAULT_TS;
|
|
165
|
-
|
|
204
|
+
assertTimeSignature(ts);
|
|
205
|
+
const style = assertOneOf(opts.style ?? DEFAULT_STYLE, BASS_STYLES, "bass style");
|
|
166
206
|
const octave = opts.octave ?? DEFAULT_OCTAVE;
|
|
207
|
+
assertInteger(octave, "bass octave", -1, 9);
|
|
208
|
+
const estimatedNotes = segments.reduce(
|
|
209
|
+
(count, segment) => count + Math.max(1, Math.ceil(segment.endBeat - segment.startBeat)),
|
|
210
|
+
0
|
|
211
|
+
);
|
|
212
|
+
assertGenerationBudget(estimatedNotes, "bass notes");
|
|
167
213
|
const low = octave * 12 + 12;
|
|
168
214
|
const ctx = {
|
|
169
215
|
ts,
|
|
@@ -231,25 +277,31 @@ var HOLD_OFF_BEAT_PROB = 0.2;
|
|
|
231
277
|
var REINFORCE_PROB = 0.35;
|
|
232
278
|
var DEFAULT_MELODY_VELOCITY = 96;
|
|
233
279
|
var VELOCITY_DROP = 16;
|
|
280
|
+
var TIE_BREAK_JITTER = 1e-6;
|
|
234
281
|
function isWholeBeat(beat) {
|
|
235
282
|
return Math.abs(beat - Math.round(beat)) < EPS2;
|
|
236
283
|
}
|
|
237
284
|
function melodyNoteAt(melody, beat) {
|
|
238
|
-
|
|
239
|
-
for (const note of melody) {
|
|
240
|
-
if (note.startBeat - EPS2 <= beat && beat < note.startBeat + note.durationBeat - EPS2) {
|
|
241
|
-
if (!found || note.startBeat > found.startBeat) {
|
|
242
|
-
found = note;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
return found;
|
|
285
|
+
return melody.at(beat)?.note;
|
|
247
286
|
}
|
|
248
287
|
function melodyAttacksAt(melody, beat) {
|
|
249
|
-
return melody.
|
|
288
|
+
return melody.attacksAt(beat);
|
|
289
|
+
}
|
|
290
|
+
function melodySnapshotAt(melody, beat) {
|
|
291
|
+
const current = melodyNoteAt(melody, beat);
|
|
292
|
+
if (current === void 0) {
|
|
293
|
+
return void 0;
|
|
294
|
+
}
|
|
295
|
+
const attacked = Math.abs(current.startBeat - beat) < EPS2;
|
|
296
|
+
const previous = attacked ? melodyNoteAt(melody, beat - EPS2 * 2) : current;
|
|
297
|
+
const snapshot = { pitch: current.pitch };
|
|
298
|
+
if (previous !== void 0) {
|
|
299
|
+
snapshot.prevPitch = previous.pitch;
|
|
300
|
+
}
|
|
301
|
+
return snapshot;
|
|
250
302
|
}
|
|
251
303
|
function followOnsets(melody) {
|
|
252
|
-
return [...new Set(melody.map((note) => note.startBeat))]
|
|
304
|
+
return [...new Set(melody.notes.map(({ note }) => note.startBeat))];
|
|
253
305
|
}
|
|
254
306
|
function complementOnsets(melody, ts, spanStart, spanEnd, rng) {
|
|
255
307
|
const onsets = [];
|
|
@@ -312,54 +364,134 @@ function scoreCandidate(pitch, melPitch, melPrev, prevPitch, chord, center) {
|
|
|
312
364
|
score -= Math.abs(pitch - center) * 0.02;
|
|
313
365
|
return score;
|
|
314
366
|
}
|
|
315
|
-
function
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
367
|
+
function heldNoteBoundaries(melody, startBeat, endBeat, chordChangeBeats) {
|
|
368
|
+
const boundaries = /* @__PURE__ */ new Set([startBeat]);
|
|
369
|
+
for (const onset of melody.onsetsBetween(startBeat, endBeat)) {
|
|
370
|
+
boundaries.add(onset);
|
|
371
|
+
}
|
|
372
|
+
for (const beat of chordChangeBeats) {
|
|
373
|
+
if (beat > startBeat + EPS2 && beat < endBeat - EPS2) {
|
|
374
|
+
boundaries.add(beat);
|
|
322
375
|
}
|
|
323
376
|
}
|
|
324
|
-
|
|
325
|
-
let
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
377
|
+
const firstGrid = Math.floor(startBeat / GRID_STEP + 1) * GRID_STEP;
|
|
378
|
+
for (let beat = firstGrid; beat < endBeat - EPS2; beat += GRID_STEP) {
|
|
379
|
+
boundaries.add(beat);
|
|
380
|
+
}
|
|
381
|
+
return [...boundaries].sort((a, b) => a - b);
|
|
382
|
+
}
|
|
383
|
+
function boundaryContexts(melody, harmony, startBeat, endBeat, ts) {
|
|
384
|
+
const boundaries = heldNoteBoundaries(melody, startBeat, endBeat, harmony.chordChangeBeats);
|
|
385
|
+
return boundaries.map((boundary) => {
|
|
386
|
+
const melodyVoice = melodySnapshotAt(melody, boundary);
|
|
387
|
+
return {
|
|
388
|
+
melodyVoice,
|
|
389
|
+
otherVoices: melodyVoice === void 0 ? [] : [melodyVoice],
|
|
390
|
+
chord: harmony.chordAt(boundary),
|
|
391
|
+
strongBeat: isStrongBeat(boundary, ts),
|
|
392
|
+
atCounterOnset: Math.abs(boundary - startBeat) < EPS2
|
|
393
|
+
};
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
function heldPitchSafety(pitch, contexts, prevPitch, key, profile, register, low, high) {
|
|
397
|
+
let worst = 0 /* Safe */;
|
|
398
|
+
for (const context of contexts) {
|
|
399
|
+
const melodyVoice = context.melodyVoice;
|
|
400
|
+
if (melodyVoice !== void 0 && (register === "below" && pitch >= melodyVoice.pitch || register === "above" && pitch <= melodyVoice.pitch)) {
|
|
401
|
+
return null;
|
|
329
402
|
}
|
|
330
|
-
|
|
331
|
-
|
|
403
|
+
const result = evaluateSafety(
|
|
404
|
+
{
|
|
405
|
+
profile,
|
|
406
|
+
candidatePitch: pitch,
|
|
407
|
+
prevPitch: context.atCounterOnset ? prevPitch : pitch,
|
|
408
|
+
chord: context.chord,
|
|
409
|
+
key,
|
|
410
|
+
otherVoices: context.otherVoices,
|
|
411
|
+
strongBeat: context.strongBeat,
|
|
412
|
+
vocalLow: low,
|
|
413
|
+
vocalHigh: high
|
|
414
|
+
},
|
|
415
|
+
{ suggestions: false }
|
|
416
|
+
);
|
|
417
|
+
worst = Math.max(worst, result.safety);
|
|
418
|
+
if (worst === 2 /* Dissonant */) {
|
|
419
|
+
return worst;
|
|
332
420
|
}
|
|
333
421
|
}
|
|
334
|
-
return
|
|
422
|
+
return worst;
|
|
335
423
|
}
|
|
336
424
|
function generateCounterMelody(opts) {
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
425
|
+
const timeline = opts.timeline;
|
|
426
|
+
const chordAt = timeline === void 0 ? opts.chordAt : (beat) => timeline.at(beat);
|
|
427
|
+
if (chordAt === void 0) {
|
|
428
|
+
throw new TypeError("countermelody needs a timeline or a chordAt callback");
|
|
340
429
|
}
|
|
341
|
-
const
|
|
430
|
+
const chordChangeBeats = timeline === void 0 ? opts.chordChangeBeats ?? [] : timeline.segments.flatMap((segment) => [segment.startBeat, segment.endBeat]);
|
|
342
431
|
const register = opts.register ?? "below";
|
|
343
|
-
const
|
|
344
|
-
|
|
432
|
+
const melody = createNoteEventIndex(opts.melody, {
|
|
433
|
+
allowNonPositiveDuration: true,
|
|
434
|
+
tieBreak: register === "below" ? "highest" : "lowest"
|
|
435
|
+
});
|
|
436
|
+
const ts = opts.ts ?? DEFAULT_TS2;
|
|
437
|
+
assertTimeSignature(ts);
|
|
438
|
+
const rhythm = assertOneOf(opts.rhythm ?? "complement", ["complement", "follow"], "rhythm");
|
|
439
|
+
const profile = assertOneOf(opts.profile ?? "pop", ["strict", "pop"], "safety profile");
|
|
345
440
|
const rng = createRng(opts.seed ?? 0);
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
441
|
+
if (opts.pitchLow !== void 0) {
|
|
442
|
+
assertInteger(opts.pitchLow, "countermelody pitchLow");
|
|
443
|
+
}
|
|
444
|
+
if (opts.pitchHigh !== void 0) {
|
|
445
|
+
assertInteger(opts.pitchHigh, "countermelody pitchHigh");
|
|
446
|
+
}
|
|
447
|
+
if (melody.notes.every((indexed) => indexed.note.durationBeat <= 0)) {
|
|
448
|
+
return [];
|
|
449
|
+
}
|
|
450
|
+
const spanStart = melody.notes[0]?.note.startBeat ?? 0;
|
|
451
|
+
const spanEnd = melody.notes.reduce((end, n) => Math.max(end, n.endBeat), spanStart);
|
|
452
|
+
assertGenerationBudget(Math.ceil((spanEnd - spanStart) / GRID_STEP), "countermelody grid");
|
|
453
|
+
const meanPitch = melody.notes.reduce((sum, n) => sum + n.note.pitch, 0) / melody.notes.length;
|
|
349
454
|
const defaultCenter = register === "below" ? meanPitch - REGISTER_OFFSET : meanPitch + REGISTER_OFFSET;
|
|
350
455
|
const low = opts.pitchLow ?? Math.round(defaultCenter) - DEFAULT_HALF_RANGE;
|
|
351
456
|
const high = opts.pitchHigh ?? Math.round(defaultCenter) + DEFAULT_HALF_RANGE;
|
|
457
|
+
assertInteger(low, "countermelody pitchLow");
|
|
458
|
+
assertInteger(high, "countermelody pitchHigh");
|
|
459
|
+
if (low > high) {
|
|
460
|
+
throw new InvalidInputError(
|
|
461
|
+
`countermelody pitchLow must not exceed pitchHigh; received ${low} > ${high}`
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
assertGenerationBudget(high - low + 1, "countermelody pitch candidates");
|
|
465
|
+
assertGenerationBudget(chordChangeBeats.length, "chord change beats");
|
|
466
|
+
for (let index = 0; index < chordChangeBeats.length; index += 1) {
|
|
467
|
+
assertRange(
|
|
468
|
+
chordChangeBeats[index] ?? Number.NaN,
|
|
469
|
+
0,
|
|
470
|
+
Number.MAX_SAFE_INTEGER,
|
|
471
|
+
`chordChangeBeats[${index}]`
|
|
472
|
+
);
|
|
473
|
+
}
|
|
352
474
|
const center = (low + high) / 2;
|
|
353
475
|
const onsets = rhythm === "follow" ? followOnsets(melody) : complementOnsets(melody, ts, spanStart, spanEnd, rng);
|
|
476
|
+
const boundaryEstimate = onsets.length + Math.ceil((spanEnd - spanStart) / GRID_STEP) + melody.notes.length + chordChangeBeats.length;
|
|
477
|
+
assertGenerationBudget(boundaryEstimate * (high - low + 1), "countermelody search");
|
|
478
|
+
const jitter = Array.from({ length: high - low + 1 }, () => rng.next() * TIE_BREAK_JITTER);
|
|
354
479
|
const out = [];
|
|
355
480
|
let prevPitch;
|
|
356
|
-
let
|
|
357
|
-
|
|
481
|
+
for (let onsetIndex = 0; onsetIndex < onsets.length; onsetIndex += 1) {
|
|
482
|
+
const beat = onsets[onsetIndex];
|
|
483
|
+
if (beat === void 0) {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const nextOnset = onsets[onsetIndex + 1];
|
|
487
|
+
const endBeat = nextOnset ?? Math.max(spanEnd, beat + GRID_STEP);
|
|
358
488
|
const melNote = melodyNoteAt(melody, beat);
|
|
359
489
|
const melPitch = melNote?.pitch;
|
|
360
|
-
const
|
|
361
|
-
const
|
|
362
|
-
const
|
|
490
|
+
const melodyVoice = melodySnapshotAt(melody, beat);
|
|
491
|
+
const melPrev = melodyVoice?.prevPitch;
|
|
492
|
+
const contexts = boundaryContexts(melody, { chordAt, chordChangeBeats }, beat, endBeat, ts);
|
|
493
|
+
const chord = contexts[0]?.chord ?? chordAt(beat);
|
|
494
|
+
const otherVoices = melodyVoice !== void 0 ? [melodyVoice] : [];
|
|
363
495
|
const candidates = enumerateSafePitches(
|
|
364
496
|
{
|
|
365
497
|
profile,
|
|
@@ -375,45 +507,48 @@ function generateCounterMelody(opts) {
|
|
|
375
507
|
high
|
|
376
508
|
);
|
|
377
509
|
let bestPitch;
|
|
510
|
+
let bestWorstSafety = 2 /* Dissonant */;
|
|
378
511
|
let bestScore = Number.NEGATIVE_INFINITY;
|
|
379
512
|
for (const candidate of candidates) {
|
|
513
|
+
const worstSafety = heldPitchSafety(
|
|
514
|
+
candidate,
|
|
515
|
+
contexts,
|
|
516
|
+
prevPitch,
|
|
517
|
+
opts.key,
|
|
518
|
+
profile,
|
|
519
|
+
register,
|
|
520
|
+
low,
|
|
521
|
+
high
|
|
522
|
+
);
|
|
523
|
+
if (worstSafety === null || worstSafety === 2 /* Dissonant */) {
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
380
526
|
if (melPitch !== void 0) {
|
|
381
|
-
if (register === "below" && candidate >= melPitch) {
|
|
382
|
-
continue;
|
|
383
|
-
}
|
|
384
|
-
if (register === "above" && candidate <= melPitch) {
|
|
385
|
-
continue;
|
|
386
|
-
}
|
|
387
527
|
if (prevPitch !== void 0 && melPrev !== void 0 && // Parallel octaves are the perfect-class-zero case of this predicate,
|
|
388
528
|
// so a single check covers both parallel fifths and octaves.
|
|
389
529
|
createsParallelPerfect(prevPitch, candidate, melPrev, melPitch)) {
|
|
390
530
|
continue;
|
|
391
531
|
}
|
|
392
532
|
}
|
|
393
|
-
const score = scoreCandidate(candidate, melPitch, melPrev, prevPitch, chord, center);
|
|
533
|
+
const score = scoreCandidate(candidate, melPitch, melPrev, prevPitch, chord, center) + (jitter[candidate - low] ?? 0);
|
|
394
534
|
const tie = Math.abs(score - bestScore) <= EPS2 && candidate < (bestPitch ?? Number.POSITIVE_INFINITY);
|
|
395
|
-
if (score > bestScore + EPS2 || tie) {
|
|
535
|
+
if (worstSafety < bestWorstSafety || worstSafety === bestWorstSafety && (score > bestScore + EPS2 || tie)) {
|
|
536
|
+
bestWorstSafety = worstSafety;
|
|
396
537
|
bestScore = score;
|
|
397
538
|
bestPitch = candidate;
|
|
398
539
|
}
|
|
399
540
|
}
|
|
400
|
-
|
|
541
|
+
if (bestPitch === void 0) {
|
|
542
|
+
prevPitch = void 0;
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
const pitch = bestPitch;
|
|
401
546
|
const velocity = Math.max(
|
|
402
547
|
1,
|
|
403
548
|
Math.min(127, Math.round((melNote?.velocity ?? DEFAULT_MELODY_VELOCITY) - VELOCITY_DROP))
|
|
404
549
|
);
|
|
405
|
-
out.push({ pitch, startBeat: beat, durationBeat:
|
|
550
|
+
out.push({ pitch, startBeat: beat, durationBeat: endBeat - beat, velocity });
|
|
406
551
|
prevPitch = pitch;
|
|
407
|
-
prevOnset = beat;
|
|
408
|
-
}
|
|
409
|
-
for (let i = 0; i < out.length; i += 1) {
|
|
410
|
-
const note = out[i];
|
|
411
|
-
if (!note) {
|
|
412
|
-
continue;
|
|
413
|
-
}
|
|
414
|
-
const next = out[i + 1];
|
|
415
|
-
const end = next ? next.startBeat : Math.max(spanEnd, note.startBeat + GRID_STEP);
|
|
416
|
-
note.durationBeat = end - note.startBeat;
|
|
417
552
|
}
|
|
418
553
|
return out;
|
|
419
554
|
}
|
|
@@ -437,6 +572,27 @@ var GM = {
|
|
|
437
572
|
};
|
|
438
573
|
var EIGHTH = 0.5;
|
|
439
574
|
var SIXTEENTH = 0.25;
|
|
575
|
+
var DRUM_FEELS = ["straight", "swing", "shuffle"];
|
|
576
|
+
var GROOVE_STYLES = [
|
|
577
|
+
"standard",
|
|
578
|
+
"funk",
|
|
579
|
+
"shuffle",
|
|
580
|
+
"bossa",
|
|
581
|
+
"trap",
|
|
582
|
+
"halftime",
|
|
583
|
+
"breakbeat",
|
|
584
|
+
"house",
|
|
585
|
+
"synthpop"
|
|
586
|
+
];
|
|
587
|
+
var DRUM_ROLES = ["full", "ambient", "minimal", "fxOnly"];
|
|
588
|
+
var PUBLIC_SECTIONS = [
|
|
589
|
+
"intro",
|
|
590
|
+
"verse",
|
|
591
|
+
"prechorus",
|
|
592
|
+
"chorus",
|
|
593
|
+
"bridge",
|
|
594
|
+
"outro"
|
|
595
|
+
];
|
|
440
596
|
var STYLE_MAP = {
|
|
441
597
|
// Standard pop pulse: kick on beats 1 and 3, backbeat snare on 2 and 4.
|
|
442
598
|
standard: { style: "standard", feel: "straight", ghostBoost: false, snareBeat3: false },
|
|
@@ -636,7 +792,7 @@ function getGhostDensity(mood, section, backingDensity, bpm) {
|
|
|
636
792
|
}
|
|
637
793
|
return levelToProbability(level);
|
|
638
794
|
}
|
|
639
|
-
function getGhostVelocity(section, beatPosition
|
|
795
|
+
function getGhostVelocity(section, beatPosition) {
|
|
640
796
|
let base = 0.4;
|
|
641
797
|
const even = beatPosition % 2 === 0 ? 0.05 : 0;
|
|
642
798
|
switch (section) {
|
|
@@ -657,14 +813,11 @@ function getGhostVelocity(section, beatPosition, isAfterSnare) {
|
|
|
657
813
|
base = 0.38;
|
|
658
814
|
break;
|
|
659
815
|
}
|
|
660
|
-
if (isAfterSnare) {
|
|
661
|
-
base += 0.1;
|
|
662
|
-
}
|
|
663
816
|
return Math.max(0.25, Math.min(0.65, base));
|
|
664
817
|
}
|
|
665
818
|
function getGhostProbabilityAtPosition(beat, sixteenthInBeat) {
|
|
666
|
-
const
|
|
667
|
-
return
|
|
819
|
+
const leadsIntoBackbeat = (beat === 0 || beat === 2) && sixteenthInBeat === 3;
|
|
820
|
+
return leadsIntoBackbeat ? 0.6 : 0.25;
|
|
668
821
|
}
|
|
669
822
|
function selectGhostPositions(mood, rng) {
|
|
670
823
|
if (mood === 2 /* Energetic */) {
|
|
@@ -947,10 +1100,7 @@ function quantizeSwing(startBeat, swing, resolution) {
|
|
|
947
1100
|
}
|
|
948
1101
|
|
|
949
1102
|
// src/generate/drums/beat.ts
|
|
950
|
-
function
|
|
951
|
-
return quantizeSwing(tick, effectiveSwing(feel, swingAmount), "sixteenth");
|
|
952
|
-
}
|
|
953
|
-
function hatSwingFactor(style) {
|
|
1103
|
+
function styleSwingFactor(style) {
|
|
954
1104
|
if (style === "trap") {
|
|
955
1105
|
return 0;
|
|
956
1106
|
}
|
|
@@ -959,6 +1109,12 @@ function hatSwingFactor(style) {
|
|
|
959
1109
|
}
|
|
960
1110
|
return 0.5;
|
|
961
1111
|
}
|
|
1112
|
+
function sectionSwing(sec, swingAmount) {
|
|
1113
|
+
return effectiveSwing(sec.feel, swingAmount) * styleSwingFactor(sec.style);
|
|
1114
|
+
}
|
|
1115
|
+
function swing16(tick, sec, swingAmount) {
|
|
1116
|
+
return quantizeSwing(tick, sectionSwing(sec, swingAmount), "sixteenth");
|
|
1117
|
+
}
|
|
962
1118
|
function generateKickForBeat(ctx, sec, kick) {
|
|
963
1119
|
if (ctx.inPrechorusLift) {
|
|
964
1120
|
return;
|
|
@@ -969,7 +1125,7 @@ function generateKickForBeat(ctx, sec, kick) {
|
|
|
969
1125
|
ctx.track.add(GM.BD, ctx.beatTick, EIGHTH, ctx.velocity);
|
|
970
1126
|
}
|
|
971
1127
|
if (and) {
|
|
972
|
-
const andTick = swing16(ctx.beatTick + EIGHTH, sec
|
|
1128
|
+
const andTick = swing16(ctx.beatTick + EIGHTH, sec, ctx.swingAmount);
|
|
973
1129
|
ctx.track.add(GM.BD, andTick, EIGHTH, ctx.velocity * 0.85);
|
|
974
1130
|
}
|
|
975
1131
|
}
|
|
@@ -986,12 +1142,17 @@ function generateSnareForBeat(ctx, sec, isIntroFirst) {
|
|
|
986
1142
|
}
|
|
987
1143
|
const backbeatVel = Math.min(127, ctx.velocity + 16);
|
|
988
1144
|
const promoteSparseChorus = sec.style === "sparse" && ctx.section === "chorus" && sec.role === "full";
|
|
1145
|
+
if (sec.role === "fxOnly") {
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
if (sec.role === "minimal") {
|
|
1149
|
+
ctx.track.add(GM.SIDESTICK, ctx.beatTick, EIGHTH, ctx.velocity * 0.65);
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
989
1152
|
if (promoteSparseChorus) {
|
|
990
1153
|
ctx.track.add(GM.SD, ctx.beatTick, EIGHTH, backbeatVel);
|
|
991
1154
|
} else if (sec.style === "sparse" || sec.role === "ambient") {
|
|
992
|
-
|
|
993
|
-
ctx.track.add(GM.SIDESTICK, ctx.beatTick, EIGHTH, ctx.velocity * 0.8);
|
|
994
|
-
}
|
|
1155
|
+
ctx.track.add(GM.SIDESTICK, ctx.beatTick, EIGHTH, ctx.velocity * 0.8);
|
|
995
1156
|
} else {
|
|
996
1157
|
ctx.track.add(GM.SD, ctx.beatTick, EIGHTH, backbeatVel);
|
|
997
1158
|
}
|
|
@@ -1012,13 +1173,13 @@ function generateGhostNotesForBeat(ctx, sec) {
|
|
|
1012
1173
|
continue;
|
|
1013
1174
|
}
|
|
1014
1175
|
const variation = ctx.rng.float(0.85, 1.15);
|
|
1015
|
-
const ghostBase = getGhostVelocity(ctx.section, ctx.beat / 2
|
|
1176
|
+
const ghostBase = getGhostVelocity(ctx.section, ctx.beat / 2);
|
|
1016
1177
|
let ghostVel = ctx.velocity * ghostBase * variation;
|
|
1017
1178
|
if (pos === "a") {
|
|
1018
1179
|
ghostVel *= 0.9;
|
|
1019
1180
|
}
|
|
1020
1181
|
const offset = pos === "e" ? SIXTEENTH : 3 * SIXTEENTH;
|
|
1021
|
-
const tick = swing16(ctx.beatTick + offset, sec
|
|
1182
|
+
const tick = swing16(ctx.beatTick + offset, sec, ctx.swingAmount);
|
|
1022
1183
|
ctx.track.add(GM.SD, tick, SIXTEENTH, ghostVel);
|
|
1023
1184
|
}
|
|
1024
1185
|
}
|
|
@@ -1080,7 +1241,7 @@ function generateHiHatForBeat(ctx, sec) {
|
|
|
1080
1241
|
for (let eighth = 0; eighth < 2; eighth += 1) {
|
|
1081
1242
|
let hhTick = ctx.beatTick + eighth * EIGHTH;
|
|
1082
1243
|
if (eighth === 1) {
|
|
1083
|
-
hhTick = swing16(hhTick, sec
|
|
1244
|
+
hhTick = swing16(hhTick, sec, ctx.swingAmount);
|
|
1084
1245
|
}
|
|
1085
1246
|
if (ctx.section === "intro" && eighth === 1) {
|
|
1086
1247
|
if (sec.useFootHh && ctx.beat % 2 === 0) {
|
|
@@ -1110,9 +1271,8 @@ function generateHiHatForBeat(ctx, sec) {
|
|
|
1110
1271
|
}
|
|
1111
1272
|
for (let sixteenth = 0; sixteenth < 4; sixteenth += 1) {
|
|
1112
1273
|
let hhTick = ctx.beatTick + sixteenth * SIXTEENTH;
|
|
1113
|
-
if (sixteenth
|
|
1114
|
-
|
|
1115
|
-
hhTick = quantizeSwing(hhTick, swung, "sixteenth");
|
|
1274
|
+
if (sixteenth !== 0) {
|
|
1275
|
+
hhTick = swing16(hhTick, sec, ctx.swingAmount);
|
|
1116
1276
|
}
|
|
1117
1277
|
const metricVel = hiHatVelocityMultiplier(sixteenth, ctx.rng);
|
|
1118
1278
|
const hhVel = Math.max(20, ctx.velocity * dm * typeMult * metricVel);
|
|
@@ -1133,9 +1293,10 @@ function generateHiHatForBeat(ctx, sec) {
|
|
|
1133
1293
|
|
|
1134
1294
|
// src/generate/drums/euclid.ts
|
|
1135
1295
|
function euclideanRhythm(pulses, steps, rotation = 0) {
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1296
|
+
assertPositiveInt(steps, "euclidean steps");
|
|
1297
|
+
assertInteger(pulses, "euclidean pulses");
|
|
1298
|
+
assertInteger(rotation, "euclidean rotation");
|
|
1299
|
+
assertGenerationBudget(steps, "euclidean pattern steps");
|
|
1139
1300
|
const p = Math.max(0, Math.min(pulses, steps));
|
|
1140
1301
|
const base = bjorklund(p, steps);
|
|
1141
1302
|
const shift = (rotation % steps + steps) % steps;
|
|
@@ -1144,18 +1305,6 @@ function euclideanRhythm(pulses, steps, rotation = 0) {
|
|
|
1144
1305
|
}
|
|
1145
1306
|
return base.map((_, i) => base[(i - shift + steps) % steps] ?? false);
|
|
1146
1307
|
}
|
|
1147
|
-
function hasHit(mask, step) {
|
|
1148
|
-
return (mask >> step & 1) === 1;
|
|
1149
|
-
}
|
|
1150
|
-
function patternToMask(pattern) {
|
|
1151
|
-
let mask = 0;
|
|
1152
|
-
for (let i = 0; i < pattern.length; i += 1) {
|
|
1153
|
-
if (pattern[i]) {
|
|
1154
|
-
mask |= 1 << i;
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
return mask;
|
|
1158
|
-
}
|
|
1159
1308
|
function bjorklund(pulses, steps) {
|
|
1160
1309
|
if (pulses === 0) {
|
|
1161
1310
|
return Array.from({ length: steps }, () => false);
|
|
@@ -1451,24 +1600,62 @@ function generateFill(track, beatTick, beat, fillType, velocity) {
|
|
|
1451
1600
|
}
|
|
1452
1601
|
|
|
1453
1602
|
// src/generate/drums/hit.ts
|
|
1603
|
+
var DRUM_NOTES = {
|
|
1604
|
+
kick: GM.BD,
|
|
1605
|
+
snare: GM.SD,
|
|
1606
|
+
sideStick: GM.SIDESTICK,
|
|
1607
|
+
handClap: GM.HANDCLAP,
|
|
1608
|
+
closedHiHat: GM.CHH,
|
|
1609
|
+
pedalHiHat: GM.FHH,
|
|
1610
|
+
openHiHat: GM.OHH,
|
|
1611
|
+
crash: GM.CRASH,
|
|
1612
|
+
ride: GM.RIDE,
|
|
1613
|
+
tambourine: GM.TAMBOURINE,
|
|
1614
|
+
highTom: GM.TOM_H,
|
|
1615
|
+
midTom: GM.TOM_M,
|
|
1616
|
+
lowTom: GM.TOM_L,
|
|
1617
|
+
shaker: GM.SHAKER
|
|
1618
|
+
};
|
|
1619
|
+
var VOICE_BY_NOTE = new Map(
|
|
1620
|
+
Object.entries(DRUM_NOTES).map(([voice, note]) => [note, voice])
|
|
1621
|
+
);
|
|
1622
|
+
function drumVoiceOf(pitch) {
|
|
1623
|
+
return VOICE_BY_NOTE.get(pitch);
|
|
1624
|
+
}
|
|
1625
|
+
var ONSET_KEY_SCALE = 128;
|
|
1454
1626
|
var HitList = class {
|
|
1455
1627
|
hits = [];
|
|
1628
|
+
/** Onset keys already occupied, as `pitch:quantizedBeat`. */
|
|
1629
|
+
#onsets = /* @__PURE__ */ new Set();
|
|
1456
1630
|
/** Append a hit, clamping velocity and dropping non-positive positions. */
|
|
1457
1631
|
add(pitch, startBeat, durationBeat, velocity) {
|
|
1632
|
+
const beat = Math.max(0, startBeat);
|
|
1458
1633
|
this.hits.push({
|
|
1459
1634
|
pitch,
|
|
1460
|
-
startBeat:
|
|
1635
|
+
startBeat: beat,
|
|
1461
1636
|
durationBeat,
|
|
1462
1637
|
velocity: clampVel(velocity)
|
|
1463
1638
|
});
|
|
1639
|
+
this.#onsets.add(onsetKey(pitch, beat));
|
|
1640
|
+
}
|
|
1641
|
+
/** Whether a hit of this pitch already starts exactly at `startBeat`. */
|
|
1642
|
+
hasOnset(pitch, startBeat) {
|
|
1643
|
+
return this.#onsets.has(onsetKey(pitch, startBeat));
|
|
1464
1644
|
}
|
|
1465
1645
|
/** True when a crash already sits within a 16th of `startBeat`. */
|
|
1466
1646
|
hasCrashNear(startBeat) {
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1647
|
+
const steps = Math.round(0.25 * ONSET_KEY_SCALE);
|
|
1648
|
+
for (let step = 0; step < steps; step += 1) {
|
|
1649
|
+
if (this.#onsets.has(onsetKey(GM.CRASH, startBeat + step / ONSET_KEY_SCALE))) {
|
|
1650
|
+
return true;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
return false;
|
|
1470
1654
|
}
|
|
1471
1655
|
};
|
|
1656
|
+
function onsetKey(pitch, startBeat) {
|
|
1657
|
+
return `${pitch}:${Math.round(startBeat * ONSET_KEY_SCALE)}`;
|
|
1658
|
+
}
|
|
1472
1659
|
|
|
1473
1660
|
// src/generate/drums/kick.ts
|
|
1474
1661
|
var EMPTY = {
|
|
@@ -1487,19 +1674,6 @@ function isInPreChorusLift(section, bar, sectionBars, nextIsChorus) {
|
|
|
1487
1674
|
}
|
|
1488
1675
|
return bar >= sectionBars - 2;
|
|
1489
1676
|
}
|
|
1490
|
-
function euclideanToKickPattern(mask) {
|
|
1491
|
-
const slot = (step) => hasHit(mask, step) || hasHit(mask, (step + 15) % 16);
|
|
1492
|
-
return {
|
|
1493
|
-
beat1: slot(0),
|
|
1494
|
-
beat1and: slot(2),
|
|
1495
|
-
beat2: slot(4),
|
|
1496
|
-
beat2and: slot(6),
|
|
1497
|
-
beat3: slot(8),
|
|
1498
|
-
beat3and: slot(10),
|
|
1499
|
-
beat4: slot(12),
|
|
1500
|
-
beat4and: slot(14)
|
|
1501
|
-
};
|
|
1502
|
-
}
|
|
1503
1677
|
function getKickPattern(section, style, bar, rng) {
|
|
1504
1678
|
const p = { ...EMPTY };
|
|
1505
1679
|
if (section === "outro") {
|
|
@@ -1621,7 +1795,7 @@ function getPercussionConfig(mood, section) {
|
|
|
1621
1795
|
};
|
|
1622
1796
|
}
|
|
1623
1797
|
function generateAuxPercussionForBar(track, barStart, config, role, densityMult, rng, bpm) {
|
|
1624
|
-
if (role === "minimal"
|
|
1798
|
+
if (role === "minimal") {
|
|
1625
1799
|
return;
|
|
1626
1800
|
}
|
|
1627
1801
|
if (config.tambourine) {
|
|
@@ -1669,15 +1843,31 @@ function generateAuxPercussionForBar(track, barStart, config, role, densityMult,
|
|
|
1669
1843
|
}
|
|
1670
1844
|
|
|
1671
1845
|
// src/generate/drums/index.ts
|
|
1846
|
+
var MAX_HITS_PER_BAR = 128;
|
|
1672
1847
|
function generateDrums(opts) {
|
|
1848
|
+
assertPositiveInt(opts.bars, "drum bars");
|
|
1849
|
+
assertRange(opts.bpm, Number.MIN_VALUE, 1e3, "drum bpm");
|
|
1850
|
+
assertRange(opts.density, 0, 1, "drum density");
|
|
1851
|
+
assertGenerationBudget(opts.bars * MAX_HITS_PER_BAR, "drum hits");
|
|
1852
|
+
if (opts.euclideanKick) {
|
|
1853
|
+
const steps = opts.euclideanKick.steps ?? 16;
|
|
1854
|
+
assertInteger(steps, "euclidean steps", 1, 16);
|
|
1855
|
+
assertInteger(opts.euclideanKick.pulses, "euclidean pulses");
|
|
1856
|
+
assertInteger(opts.euclideanKick.rotation ?? 0, "euclidean rotation");
|
|
1857
|
+
}
|
|
1858
|
+
const publicStyle = assertOneOf(opts.style, GROOVE_STYLES, "drum style");
|
|
1859
|
+
const publicSection = assertOneOf(opts.section, PUBLIC_SECTIONS, "drum section");
|
|
1860
|
+
if (opts.nextSection !== void 0) {
|
|
1861
|
+
assertOneOf(opts.nextSection, PUBLIC_SECTIONS, "drum nextSection");
|
|
1862
|
+
}
|
|
1673
1863
|
const track = new HitList();
|
|
1674
1864
|
const rng = createRng(opts.seed ?? 0);
|
|
1675
|
-
const mapping = mapStyle(
|
|
1865
|
+
const mapping = mapStyle(publicStyle);
|
|
1676
1866
|
const style = mapping.style;
|
|
1677
|
-
const feel = opts.feel
|
|
1678
|
-
const role = opts.role
|
|
1867
|
+
const feel = opts.feel === void 0 ? mapping.feel : assertOneOf(opts.feel, DRUM_FEELS, "drum feel");
|
|
1868
|
+
const role = opts.role === void 0 ? "full" : assertOneOf(opts.role, DRUM_ROLES, "drum role");
|
|
1679
1869
|
const playMainVoices = role !== "fxOnly";
|
|
1680
|
-
const section = mapSection(
|
|
1870
|
+
const section = mapSection(publicSection);
|
|
1681
1871
|
const backingDensity = mapDensity(opts.density);
|
|
1682
1872
|
const swingAmount = feelSwingAmount(feel);
|
|
1683
1873
|
let densityMult = sectionDensityMultiplier(section);
|
|
@@ -1706,15 +1896,8 @@ function generateDrums(opts) {
|
|
|
1706
1896
|
const percMood = percMoodCategory(style);
|
|
1707
1897
|
const nextSection = opts.nextSection ? mapSection(opts.nextSection) : section;
|
|
1708
1898
|
const nextEnergy = sectionEnergy(nextSection);
|
|
1709
|
-
const
|
|
1710
|
-
|
|
1711
|
-
euclideanRhythm(
|
|
1712
|
-
opts.euclideanKick.pulses,
|
|
1713
|
-
opts.euclideanKick.steps ?? 16,
|
|
1714
|
-
opts.euclideanKick.rotation ?? 0
|
|
1715
|
-
)
|
|
1716
|
-
)
|
|
1717
|
-
) : void 0;
|
|
1899
|
+
const euclidSteps = opts.euclideanKick?.steps ?? 16;
|
|
1900
|
+
const euclidKick = opts.euclideanKick ? euclideanRhythm(opts.euclideanKick.pulses, euclidSteps, opts.euclideanKick.rotation ?? 0) : void 0;
|
|
1718
1901
|
const reuseSectionKick = (section === "b" || section === "chorus") && style !== "sparse";
|
|
1719
1902
|
let sectionKick;
|
|
1720
1903
|
for (let bar = 0; bar < opts.bars; bar += 1) {
|
|
@@ -1730,15 +1913,13 @@ function generateDrums(opts) {
|
|
|
1730
1913
|
barHasOpenHh = !track.hasCrashNear(barStart + openHhBeatIndex);
|
|
1731
1914
|
}
|
|
1732
1915
|
let kick;
|
|
1733
|
-
if (euclidKick) {
|
|
1734
|
-
kick = euclidKick;
|
|
1735
|
-
} else if (reuseSectionKick) {
|
|
1916
|
+
if (!euclidKick && reuseSectionKick) {
|
|
1736
1917
|
sectionKick ??= getKickPattern(section, style, 0, rng);
|
|
1737
1918
|
kick = sectionKick;
|
|
1738
|
-
} else {
|
|
1919
|
+
} else if (!euclidKick) {
|
|
1739
1920
|
kick = getKickPattern(section, style, bar, rng);
|
|
1740
1921
|
}
|
|
1741
|
-
const inLift = isInPreChorusLift(section, bar, opts.bars,
|
|
1922
|
+
const inLift = isInPreChorusLift(section, bar, opts.bars, nextSection === "chorus");
|
|
1742
1923
|
let currentFill = "snareRoll";
|
|
1743
1924
|
for (let beat = 0; beat < 4; beat += 1) {
|
|
1744
1925
|
const beatTick = barStart + beat;
|
|
@@ -1776,7 +1957,23 @@ function generateDrums(opts) {
|
|
|
1776
1957
|
if (inLift) {
|
|
1777
1958
|
generatePreChorusBuildup(ctx, sec, isLastBar);
|
|
1778
1959
|
}
|
|
1779
|
-
|
|
1960
|
+
if (euclidKick && !inLift) {
|
|
1961
|
+
const stepLength = 4 / euclidSteps;
|
|
1962
|
+
for (let step = 0; step < euclidKick.length; step += 1) {
|
|
1963
|
+
const onset = step * stepLength;
|
|
1964
|
+
if (euclidKick[step] && Math.floor(onset) === beat) {
|
|
1965
|
+
const wholeBeat = Math.abs(onset - Math.round(onset)) < 1e-9;
|
|
1966
|
+
track.add(
|
|
1967
|
+
GM.BD,
|
|
1968
|
+
barStart + onset,
|
|
1969
|
+
Math.min(0.5, stepLength),
|
|
1970
|
+
velocity * (wholeBeat ? 1 : 0.85)
|
|
1971
|
+
);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
} else if (kick) {
|
|
1975
|
+
generateKickForBeat(ctx, sec, kick);
|
|
1976
|
+
}
|
|
1780
1977
|
generateSnareForBeat(ctx, sec, section === "intro" && bar === 0);
|
|
1781
1978
|
if (sec.useGhostNotes && !inLift) {
|
|
1782
1979
|
generateGhostNotesForBeat(ctx, sec);
|
|
@@ -1787,27 +1984,36 @@ function generateDrums(opts) {
|
|
|
1787
1984
|
if (sec.useFootHh && shouldPlayHiHat(role)) {
|
|
1788
1985
|
for (let fhhBeat = 0; fhhBeat < 4; fhhBeat += 2) {
|
|
1789
1986
|
const fhhTick = barStart + fhhBeat;
|
|
1790
|
-
|
|
1791
|
-
if (!occupied) {
|
|
1987
|
+
if (!track.hasOnset(GM.FHH, fhhTick)) {
|
|
1792
1988
|
track.add(GM.FHH, fhhTick, 0.5, footHiHatVelocity(rng));
|
|
1793
1989
|
}
|
|
1794
1990
|
}
|
|
1795
1991
|
}
|
|
1992
|
+
let percussionConfig = getPercussionConfig(percMood, section);
|
|
1993
|
+
if (role === "fxOnly" && !percussionConfig.tambourine && !percussionConfig.shaker && !percussionConfig.handclap) {
|
|
1994
|
+
percussionConfig = {
|
|
1995
|
+
tambourine: false,
|
|
1996
|
+
shaker: true,
|
|
1997
|
+
handclap: false,
|
|
1998
|
+
shaker16th: false
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
1796
2001
|
generateAuxPercussionForBar(
|
|
1797
2002
|
track,
|
|
1798
2003
|
barStart,
|
|
1799
|
-
|
|
2004
|
+
percussionConfig,
|
|
1800
2005
|
role,
|
|
1801
2006
|
densityMult,
|
|
1802
2007
|
rng,
|
|
1803
2008
|
opts.bpm
|
|
1804
2009
|
);
|
|
1805
2010
|
}
|
|
1806
|
-
return track.hits;
|
|
2011
|
+
return [...track.hits].sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);
|
|
1807
2012
|
}
|
|
1808
2013
|
|
|
1809
2014
|
// src/generate/groove/index.ts
|
|
1810
2015
|
var DEFAULT_TS3 = { numerator: 4, denominator: 4 };
|
|
2016
|
+
var DEFAULT_GROOVE_SUBDIVISION = 4;
|
|
1811
2017
|
var DEFAULT_TIMING = 0.02;
|
|
1812
2018
|
var DEFAULT_VELOCITY_JITTER = 8;
|
|
1813
2019
|
var DEFAULT_ACCENT = 12;
|
|
@@ -1817,12 +2023,19 @@ var MIN_VELOCITY = 1;
|
|
|
1817
2023
|
var MAX_VELOCITY = 127;
|
|
1818
2024
|
function humanize(events, opts = {}) {
|
|
1819
2025
|
const ts = opts.ts ?? DEFAULT_TS3;
|
|
2026
|
+
assertTimeSignature(ts);
|
|
2027
|
+
assertNoteEvents(events, "humanize events", { allowNonPositiveDuration: true });
|
|
2028
|
+
const sounding = dropSilentNotes(events);
|
|
1820
2029
|
const timing = opts.timing ?? DEFAULT_TIMING;
|
|
1821
2030
|
const velocityJitter = opts.velocity ?? DEFAULT_VELOCITY_JITTER;
|
|
1822
2031
|
const accent = opts.accent ?? DEFAULT_ACCENT;
|
|
1823
2032
|
const baseVelocity = opts.baseVelocity ?? DEFAULT_BASE_VELOCITY;
|
|
2033
|
+
assertRange(timing, 0, Number.MAX_SAFE_INTEGER, "humanize timing");
|
|
2034
|
+
assertRange(velocityJitter, 0, 127, "humanize velocity jitter");
|
|
2035
|
+
assertRange(accent, 0, 127, "humanize accent");
|
|
2036
|
+
assertRange(baseVelocity, 0, 127, "humanize base velocity");
|
|
1824
2037
|
const rng = createRng(opts.seed ?? 0);
|
|
1825
|
-
return
|
|
2038
|
+
return sounding.map((event) => {
|
|
1826
2039
|
const timingOffset = rng.float(-timing, timing);
|
|
1827
2040
|
const startBeat = Math.max(0, event.startBeat + timingOffset);
|
|
1828
2041
|
const weight = metricWeight(event.startBeat, ts);
|
|
@@ -1852,14 +2065,18 @@ function quantizeToGrid(beat, barBeats, subdivision, slotsPerBar) {
|
|
|
1852
2065
|
slotIndex
|
|
1853
2066
|
};
|
|
1854
2067
|
}
|
|
1855
|
-
function extractGrooveTemplate(events, ts, subdivision) {
|
|
2068
|
+
function extractGrooveTemplate(events, ts, subdivision = DEFAULT_GROOVE_SUBDIVISION) {
|
|
2069
|
+
assertTimeSignature(ts);
|
|
2070
|
+
assertNoteEvents(events, "groove source events", { allowNonPositiveDuration: true });
|
|
2071
|
+
assertPositiveInt(subdivision, "groove subdivision");
|
|
1856
2072
|
const barBeats = beatsPerBar(ts);
|
|
1857
2073
|
const slotsPerBar = Math.round(barBeats * subdivision);
|
|
2074
|
+
assertGenerationBudget(slotsPerBar, "groove template slots");
|
|
1858
2075
|
const offsetSums = new Array(slotsPerBar).fill(0);
|
|
1859
2076
|
const velocitySums = new Array(slotsPerBar).fill(0);
|
|
1860
2077
|
const counts = new Array(slotsPerBar).fill(0);
|
|
1861
2078
|
const velocityCounts = new Array(slotsPerBar).fill(0);
|
|
1862
|
-
for (const event of events) {
|
|
2079
|
+
for (const event of dropSilentNotes(events)) {
|
|
1863
2080
|
const { quantizedBeat, slotIndex } = quantizeToGrid(
|
|
1864
2081
|
event.startBeat,
|
|
1865
2082
|
barBeats,
|
|
@@ -1889,13 +2106,21 @@ function extractGrooveTemplate(events, ts, subdivision) {
|
|
|
1889
2106
|
return { subdivision, slotsPerBar, slots, ts };
|
|
1890
2107
|
}
|
|
1891
2108
|
function applyGrooveTemplate(events, template, ts) {
|
|
2109
|
+
assertTimeSignature(ts);
|
|
2110
|
+
assertNoteEvents(events, "groove target events", { allowNonPositiveDuration: true });
|
|
2111
|
+
assertPositiveInt(template.subdivision, "template subdivision");
|
|
2112
|
+
assertPositiveInt(template.slotsPerBar, "template slotsPerBar");
|
|
2113
|
+
assertGenerationBudget(template.slots.length, "template slots");
|
|
2114
|
+
if (template.slots.length !== template.slotsPerBar) {
|
|
2115
|
+
throw new InvalidInputError("template slots length must equal slotsPerBar");
|
|
2116
|
+
}
|
|
1892
2117
|
if (template.ts && (template.ts.numerator !== ts.numerator || template.ts.denominator !== ts.denominator)) {
|
|
1893
|
-
throw new
|
|
2118
|
+
throw new InvalidInputError(
|
|
1894
2119
|
`Groove template meter ${formatTimeSignature(template.ts)} does not match apply-time meter ${formatTimeSignature(ts)}`
|
|
1895
2120
|
);
|
|
1896
2121
|
}
|
|
1897
2122
|
const barBeats = beatsPerBar(ts);
|
|
1898
|
-
return events.map((event) => {
|
|
2123
|
+
return dropSilentNotes(events).map((event) => {
|
|
1899
2124
|
const { quantizedBeat, slotIndex } = quantizeToGrid(
|
|
1900
2125
|
event.startBeat,
|
|
1901
2126
|
barBeats,
|
|
@@ -1915,25 +2140,20 @@ function applyGrooveTemplate(events, template, ts) {
|
|
|
1915
2140
|
}
|
|
1916
2141
|
|
|
1917
2142
|
// src/generate/harmonize/index.ts
|
|
1918
|
-
var FALLBACK = {
|
|
2143
|
+
var FALLBACK = {
|
|
2144
|
+
rootPc: 0,
|
|
2145
|
+
quality: "maj",
|
|
2146
|
+
secondaryDominant: false,
|
|
2147
|
+
base: 0,
|
|
2148
|
+
pcs: chordPitchClasses(makeChord(0, "maj"))
|
|
2149
|
+
};
|
|
2150
|
+
var DEFAULT_HARMONIC_RHYTHM = 2;
|
|
2151
|
+
var DEFAULT_PLACEMENT = { transposeSearch: false, octaveSearch: false };
|
|
1919
2152
|
var DEFAULT_METER = { numerator: 4, denominator: 4 };
|
|
1920
2153
|
var COMFORT_LOW = 55;
|
|
1921
2154
|
var COMFORT_HIGH = 79;
|
|
1922
2155
|
var TESSITURA_WEIGHT = 1e-3;
|
|
1923
|
-
var
|
|
1924
|
-
function pitchClass2(pitch) {
|
|
1925
|
-
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
1926
|
-
}
|
|
1927
|
-
function mulberry32(seed) {
|
|
1928
|
-
let state = seed >>> 0;
|
|
1929
|
-
return () => {
|
|
1930
|
-
state = state + 1831565813 >>> 0;
|
|
1931
|
-
let t = state;
|
|
1932
|
-
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
1933
|
-
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
1934
|
-
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
1935
|
-
};
|
|
1936
|
-
}
|
|
2156
|
+
var TIE_BREAK_JITTER2 = 1e-6;
|
|
1937
2157
|
function inferKey(melody) {
|
|
1938
2158
|
let best = majorKey(0);
|
|
1939
2159
|
let bestScore = Number.NEGATIVE_INFINITY;
|
|
@@ -1942,13 +2162,13 @@ function inferKey(melody) {
|
|
|
1942
2162
|
candidates.push(majorKey(tonic), minorKey(tonic));
|
|
1943
2163
|
}
|
|
1944
2164
|
for (const key of candidates) {
|
|
1945
|
-
const tonic =
|
|
2165
|
+
const tonic = pitchClassOf(key.rootPc);
|
|
1946
2166
|
const isMinor = key.modeMask12 === NATURAL_MINOR_MASK;
|
|
1947
2167
|
const leadingTone = (tonic + 11) % 12;
|
|
1948
2168
|
let score = 0;
|
|
1949
2169
|
for (const n of melody) {
|
|
1950
2170
|
const w = Math.max(0.25, n.durationBeat);
|
|
1951
|
-
const pc =
|
|
2171
|
+
const pc = pitchClassOf(n.pitch);
|
|
1952
2172
|
if (isScaleTone(n.pitch, key) || isMinor && pc === leadingTone) {
|
|
1953
2173
|
score += w;
|
|
1954
2174
|
}
|
|
@@ -1965,50 +2185,59 @@ function inferKey(melody) {
|
|
|
1965
2185
|
}
|
|
1966
2186
|
function buildCandidates(key, reharmonize) {
|
|
1967
2187
|
const tones = scaleTonesInDegreeOrder(key);
|
|
1968
|
-
const candidates = tones.map((rootPc, degree) =>
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
2188
|
+
const candidates = tones.map((rootPc, degree) => {
|
|
2189
|
+
const quality = diatonicTriad(degree, key).quality;
|
|
2190
|
+
return {
|
|
2191
|
+
rootPc,
|
|
2192
|
+
quality,
|
|
2193
|
+
degree,
|
|
2194
|
+
secondaryDominant: false,
|
|
2195
|
+
base: 0,
|
|
2196
|
+
pcs: chordPitchClasses(makeChord(rootPc, quality))
|
|
2197
|
+
};
|
|
2198
|
+
});
|
|
1976
2199
|
if (reharmonize !== "diatonic") {
|
|
1977
2200
|
for (const target of [1, 3, 4, 5]) {
|
|
1978
2201
|
const targetRoot = tones[target] ?? 0;
|
|
2202
|
+
const rootPc = (targetRoot + 7) % 12;
|
|
1979
2203
|
candidates.push({
|
|
1980
|
-
rootPc
|
|
2204
|
+
rootPc,
|
|
1981
2205
|
quality: "dom7",
|
|
1982
2206
|
secondaryDominant: true,
|
|
1983
2207
|
targetDegree: target,
|
|
1984
|
-
base: 1.5
|
|
2208
|
+
base: 1.5,
|
|
2209
|
+
pcs: chordPitchClasses(makeChord(rootPc, "dom7"))
|
|
1985
2210
|
});
|
|
1986
2211
|
}
|
|
1987
2212
|
}
|
|
1988
2213
|
if (reharmonize === "borrowed") {
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
quality: "min",
|
|
2004
|
-
secondaryDominant: false,
|
|
2005
|
-
base: 2
|
|
2006
|
-
});
|
|
2214
|
+
const parallel = parallelKey(key);
|
|
2215
|
+
const parallelTones = scaleTonesInDegreeOrder(parallel);
|
|
2216
|
+
for (let degree = 0; degree < parallelTones.length; degree += 1) {
|
|
2217
|
+
const chord = diatonicTriad(degree, parallel);
|
|
2218
|
+
if (!isDiatonic(chord, key)) {
|
|
2219
|
+
candidates.push({
|
|
2220
|
+
rootPc: chord.rootPc,
|
|
2221
|
+
quality: chord.quality,
|
|
2222
|
+
secondaryDominant: false,
|
|
2223
|
+
base: 1.25,
|
|
2224
|
+
pcs: chordPitchClasses(chord)
|
|
2225
|
+
});
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2007
2228
|
}
|
|
2008
|
-
|
|
2229
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2230
|
+
return candidates.filter((candidate) => {
|
|
2231
|
+
const id = `${candidate.rootPc}:${candidate.quality}`;
|
|
2232
|
+
if (seen.has(id)) {
|
|
2233
|
+
return false;
|
|
2234
|
+
}
|
|
2235
|
+
seen.add(id);
|
|
2236
|
+
return true;
|
|
2237
|
+
});
|
|
2009
2238
|
}
|
|
2010
|
-
function emissionCost(seg, cand, melody, key) {
|
|
2011
|
-
const pcs =
|
|
2239
|
+
function emissionCost(seg, cand, melody, key, ts) {
|
|
2240
|
+
const pcs = cand.pcs;
|
|
2012
2241
|
let cost = cand.base;
|
|
2013
2242
|
for (const idx of seg.noteIndices) {
|
|
2014
2243
|
const note = melody[idx];
|
|
@@ -2021,10 +2250,10 @@ function emissionCost(seg, cand, melody, key) {
|
|
|
2021
2250
|
continue;
|
|
2022
2251
|
}
|
|
2023
2252
|
const w = Math.max(0.25, overlap);
|
|
2024
|
-
if (pcs.includes(
|
|
2253
|
+
if (pcs.includes(pitchClassOf(note.pitch))) {
|
|
2025
2254
|
continue;
|
|
2026
2255
|
}
|
|
2027
|
-
const strong = isStrongBeat(overlapStart,
|
|
2256
|
+
const strong = isStrongBeat(overlapStart, ts);
|
|
2028
2257
|
cost += (strong ? 10 : 2) * w;
|
|
2029
2258
|
if (!isScaleTone(note.pitch, key)) {
|
|
2030
2259
|
cost += w;
|
|
@@ -2058,13 +2287,13 @@ function transitionCost(prev, cur, tonicPc) {
|
|
|
2058
2287
|
cost += dist * 0.3;
|
|
2059
2288
|
return cost;
|
|
2060
2289
|
}
|
|
2061
|
-
function harmonizeOnce(melody, key, candidates, segments, jitter) {
|
|
2062
|
-
const tonicPc =
|
|
2290
|
+
function harmonizeOnce(melody, key, candidates, segments, jitter, ts) {
|
|
2291
|
+
const tonicPc = pitchClassOf(key.rootPc);
|
|
2063
2292
|
const n = candidates.length;
|
|
2064
2293
|
const candAt = (i) => candidates[i] ?? FALLBACK;
|
|
2065
2294
|
const seg0 = segments[0];
|
|
2066
2295
|
let dp = candidates.map(
|
|
2067
|
-
(c, ci) => (seg0 ? emissionCost(seg0, c, melody, key) : 0) + (jitter[ci] ?? 0)
|
|
2296
|
+
(c, ci) => (seg0 ? emissionCost(seg0, c, melody, key, ts) : 0) + (jitter[ci] ?? 0)
|
|
2068
2297
|
);
|
|
2069
2298
|
const back = [];
|
|
2070
2299
|
for (let s = 1; s < segments.length; s += 1) {
|
|
@@ -2084,7 +2313,7 @@ function harmonizeOnce(melody, key, candidates, segments, jitter) {
|
|
|
2084
2313
|
bestPrev = p;
|
|
2085
2314
|
}
|
|
2086
2315
|
}
|
|
2087
|
-
next[c] = best + emissionCost(seg, candAt(c), melody, key) + (jitter[c] ?? 0);
|
|
2316
|
+
next[c] = best + emissionCost(seg, candAt(c), melody, key, ts) + (jitter[c] ?? 0);
|
|
2088
2317
|
ptr[c] = bestPrev;
|
|
2089
2318
|
}
|
|
2090
2319
|
dp = next;
|
|
@@ -2109,30 +2338,56 @@ function harmonizeOnce(melody, key, candidates, segments, jitter) {
|
|
|
2109
2338
|
return { cost: bestCost, path };
|
|
2110
2339
|
}
|
|
2111
2340
|
function harmonizeMelody(opts) {
|
|
2112
|
-
const
|
|
2113
|
-
const
|
|
2341
|
+
const noteIndex = createNoteEventIndex(opts.melody, { allowNonPositiveDuration: true });
|
|
2342
|
+
const ts = opts.ts ?? DEFAULT_METER;
|
|
2343
|
+
assertTimeSignature(ts);
|
|
2344
|
+
const requestedKey = opts.key ?? "infer";
|
|
2345
|
+
const key = requestedKey === "infer" ? inferKey(opts.melody) : requestedKey;
|
|
2346
|
+
const placement = opts.placement ?? DEFAULT_PLACEMENT;
|
|
2347
|
+
if (noteIndex.notes.every((indexed) => indexed.note.durationBeat <= 0)) {
|
|
2348
|
+
return { transposeSemitones: 0, key, chords: [], melodyRoles: [] };
|
|
2349
|
+
}
|
|
2350
|
+
const candidates = buildCandidates(key, opts.reharmonize ?? "diatonic");
|
|
2114
2351
|
const candAt = (i) => candidates[i] ?? FALLBACK;
|
|
2115
|
-
const rng =
|
|
2116
|
-
const jitter = candidates.map(() => rng() *
|
|
2117
|
-
const melodyEnd =
|
|
2118
|
-
const hr =
|
|
2352
|
+
const rng = createRng(opts.seed ?? 0);
|
|
2353
|
+
const jitter = candidates.map(() => rng.next() * TIE_BREAK_JITTER2);
|
|
2354
|
+
const melodyEnd = noteIndex.notes.reduce((m, n) => Math.max(m, n.endBeat), 0);
|
|
2355
|
+
const hr = assertRange(
|
|
2356
|
+
opts.harmonicRhythm ?? DEFAULT_HARMONIC_RHYTHM,
|
|
2357
|
+
Number.MIN_VALUE,
|
|
2358
|
+
Number.MAX_SAFE_INTEGER,
|
|
2359
|
+
"harmonic rhythm"
|
|
2360
|
+
);
|
|
2119
2361
|
const segCount = Math.max(1, Math.ceil(melodyEnd / hr));
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2362
|
+
assertGenerationBudget(segCount, "harmonic segments");
|
|
2363
|
+
assertGenerationBudget(segCount * candidates.length * candidates.length, "harmonization search");
|
|
2364
|
+
const segments = Array.from({ length: segCount }, (_, s) => ({
|
|
2365
|
+
startBeat: s * hr,
|
|
2366
|
+
endBeat: (s + 1) * hr,
|
|
2367
|
+
noteIndices: []
|
|
2368
|
+
}));
|
|
2369
|
+
let memberships = 0;
|
|
2370
|
+
for (const indexed of noteIndex.notes) {
|
|
2371
|
+
if (indexed.note.durationBeat <= 0) {
|
|
2372
|
+
continue;
|
|
2373
|
+
}
|
|
2374
|
+
const first = Math.max(0, Math.floor(indexed.note.startBeat / hr));
|
|
2375
|
+
const lastExclusive = Math.min(segCount, Math.ceil(indexed.endBeat / hr - Number.EPSILON));
|
|
2376
|
+
memberships += Math.max(0, lastExclusive - first);
|
|
2377
|
+
assertGenerationBudget(memberships, "note-to-segment memberships");
|
|
2378
|
+
for (let segment = first; segment < lastExclusive; segment += 1) {
|
|
2379
|
+
segments[segment]?.noteIndices.push(indexed.originalIndex);
|
|
2380
|
+
}
|
|
2126
2381
|
}
|
|
2127
2382
|
const transposes = [0];
|
|
2128
|
-
if (
|
|
2383
|
+
if (placement.transposeSearch) {
|
|
2129
2384
|
for (let s = -6; s <= 6; s += 1) {
|
|
2130
2385
|
if (s !== 0) {
|
|
2131
2386
|
transposes.push(s);
|
|
2132
2387
|
}
|
|
2133
2388
|
}
|
|
2134
2389
|
}
|
|
2135
|
-
if (
|
|
2390
|
+
if (placement.octaveSearch) {
|
|
2136
2391
|
for (const o of [-12, 12]) {
|
|
2137
2392
|
if (!transposes.includes(o)) {
|
|
2138
2393
|
transposes.push(o);
|
|
@@ -2143,13 +2398,13 @@ function harmonizeMelody(opts) {
|
|
|
2143
2398
|
let bestTs = 0;
|
|
2144
2399
|
let bestPath = [];
|
|
2145
2400
|
let bestMelody = opts.melody;
|
|
2146
|
-
for (const
|
|
2147
|
-
const shifted = opts.melody.map((n) => ({ ...n, pitch: n.pitch +
|
|
2148
|
-
const { cost, path } = harmonizeOnce(shifted, key, candidates, segments, jitter);
|
|
2401
|
+
for (const shift of transposes) {
|
|
2402
|
+
const shifted = opts.melody.map((n) => ({ ...n, pitch: n.pitch + shift }));
|
|
2403
|
+
const { cost, path } = harmonizeOnce(shifted, key, candidates, segments, jitter, ts);
|
|
2149
2404
|
const total = cost + tessituraCost(shifted);
|
|
2150
2405
|
if (total < bestCost) {
|
|
2151
2406
|
bestCost = total;
|
|
2152
|
-
bestTs =
|
|
2407
|
+
bestTs = shift;
|
|
2153
2408
|
bestPath = path;
|
|
2154
2409
|
bestMelody = shifted;
|
|
2155
2410
|
}
|
|
@@ -2169,20 +2424,23 @@ function harmonizeMelody(opts) {
|
|
|
2169
2424
|
}
|
|
2170
2425
|
return chord;
|
|
2171
2426
|
});
|
|
2172
|
-
const melodyRoles = bestMelody.map((note,
|
|
2427
|
+
const melodyRoles = bestMelody.map((note, noteIndex2) => {
|
|
2173
2428
|
const segIdx = Math.min(segments.length - 1, Math.max(0, Math.floor(note.startBeat / hr)));
|
|
2174
2429
|
const cand = candAt(bestPath[segIdx] ?? 0);
|
|
2175
2430
|
const chord = makeChord(cand.rootPc, cand.quality);
|
|
2176
|
-
return { noteIndex, role: roleOf(note.pitch, chord).role };
|
|
2431
|
+
return { noteIndex: noteIndex2, role: roleOf(note.pitch, chord).role };
|
|
2177
2432
|
});
|
|
2178
2433
|
return { transposeSemitones: bestTs, key, chords, melodyRoles };
|
|
2179
2434
|
}
|
|
2180
2435
|
|
|
2181
2436
|
// src/generate/motif/index.ts
|
|
2182
|
-
function pitchClass3(pitch) {
|
|
2183
|
-
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
2184
|
-
}
|
|
2185
2437
|
var MIN_TILE_SPAN = 1 / 256;
|
|
2438
|
+
var DEFAULT_TS4 = { numerator: 4, denominator: 4 };
|
|
2439
|
+
var EPS3 = 1e-9;
|
|
2440
|
+
function isDownbeat(startBeat, barBeats) {
|
|
2441
|
+
const bar = startBeat / barBeats;
|
|
2442
|
+
return Math.abs(bar - Math.round(bar)) < EPS3;
|
|
2443
|
+
}
|
|
2186
2444
|
function upScaleTone(pitch, key) {
|
|
2187
2445
|
for (let d = 1; d <= 12; d += 1) {
|
|
2188
2446
|
if (isScaleTone(pitch + d, key)) {
|
|
@@ -2210,39 +2468,40 @@ function stepDiatonic(pitch, degrees, key) {
|
|
|
2210
2468
|
function nearestChordTone(pitch, chord) {
|
|
2211
2469
|
const pcs = chordPitchClasses(chord);
|
|
2212
2470
|
for (let d = 0; d <= 12; d += 1) {
|
|
2213
|
-
if (pcs.includes(
|
|
2471
|
+
if (pcs.includes(pitchClassOf(pitch - d))) {
|
|
2214
2472
|
return pitch - d;
|
|
2215
2473
|
}
|
|
2216
|
-
if (pcs.includes(
|
|
2474
|
+
if (pcs.includes(pitchClassOf(pitch + d))) {
|
|
2217
2475
|
return pitch + d;
|
|
2218
2476
|
}
|
|
2219
2477
|
}
|
|
2220
2478
|
return pitch;
|
|
2221
2479
|
}
|
|
2222
2480
|
function cellOrigin(notes) {
|
|
2223
|
-
|
|
2481
|
+
let origin = Number.POSITIVE_INFINITY;
|
|
2482
|
+
for (const note of notes) {
|
|
2483
|
+
origin = Math.min(origin, note.startBeat);
|
|
2484
|
+
}
|
|
2485
|
+
return Number.isFinite(origin) ? origin : 0;
|
|
2486
|
+
}
|
|
2487
|
+
function cellEnd(notes) {
|
|
2488
|
+
let end = Number.NEGATIVE_INFINITY;
|
|
2489
|
+
for (const note of notes) {
|
|
2490
|
+
end = Math.max(end, note.startBeat + note.durationBeat);
|
|
2491
|
+
}
|
|
2492
|
+
return Number.isFinite(end) ? end : 0;
|
|
2224
2493
|
}
|
|
2225
2494
|
function cellSpan(cell) {
|
|
2226
2495
|
if (cell.notes.length === 0) {
|
|
2227
2496
|
return 0;
|
|
2228
2497
|
}
|
|
2229
2498
|
const start = cellOrigin(cell.notes);
|
|
2230
|
-
const end =
|
|
2499
|
+
const end = cellEnd(cell.notes);
|
|
2231
2500
|
return end - start;
|
|
2232
2501
|
}
|
|
2233
2502
|
function clone(cell) {
|
|
2234
2503
|
return { notes: cell.notes.map((n) => ({ ...n })) };
|
|
2235
2504
|
}
|
|
2236
|
-
function mulberry322(seed) {
|
|
2237
|
-
let state = seed >>> 0;
|
|
2238
|
-
return () => {
|
|
2239
|
-
state = state + 1831565813 >>> 0;
|
|
2240
|
-
let t = state;
|
|
2241
|
-
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
2242
|
-
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
2243
|
-
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
2244
|
-
};
|
|
2245
|
-
}
|
|
2246
2505
|
function contourOffsets(contour, count) {
|
|
2247
2506
|
const offsets = [];
|
|
2248
2507
|
for (let i = 0; i < count; i += 1) {
|
|
@@ -2265,30 +2524,59 @@ function contourOffsets(contour, count) {
|
|
|
2265
2524
|
}
|
|
2266
2525
|
function generateMotif(opts) {
|
|
2267
2526
|
const contour = opts.contour ?? "arch";
|
|
2268
|
-
const bars =
|
|
2269
|
-
const
|
|
2527
|
+
const bars = assertPositiveInt(opts.bars, "motif bars");
|
|
2528
|
+
const barBeats = beatsPerBar(opts.ts ?? DEFAULT_TS4);
|
|
2529
|
+
const totalBeats = bars * barBeats;
|
|
2270
2530
|
const noteCount = Math.max(3, bars * 2);
|
|
2531
|
+
assertGenerationBudget(noteCount, "motif notes");
|
|
2271
2532
|
const beatsPerNote = totalBeats / noteCount;
|
|
2272
|
-
const rng =
|
|
2273
|
-
const
|
|
2274
|
-
const
|
|
2533
|
+
const rng = createRng(opts.seed ?? 0);
|
|
2534
|
+
const requestedJitter = assertFiniteNumber(opts.jitter ?? 0, "motif jitter");
|
|
2535
|
+
const jitterProb = Math.min(1, Math.max(0, requestedJitter));
|
|
2536
|
+
const tonic = pitchClassOf(opts.key.rootPc) + 60;
|
|
2275
2537
|
const offsets = contourOffsets(contour, noteCount);
|
|
2276
2538
|
const notes = [];
|
|
2277
2539
|
for (let i = 0; i < noteCount; i += 1) {
|
|
2278
2540
|
let jitter = 0;
|
|
2279
|
-
if (jitterProb > 0 && rng() < jitterProb) {
|
|
2280
|
-
jitter = rng() < 0.5 ? 1 : -1;
|
|
2541
|
+
if (jitterProb > 0 && rng.next() < jitterProb) {
|
|
2542
|
+
jitter = rng.next() < 0.5 ? 1 : -1;
|
|
2281
2543
|
}
|
|
2282
2544
|
let pitch = stepDiatonic(tonic, (offsets[i] ?? 0) + jitter, opts.key);
|
|
2283
2545
|
const startBeat = i * beatsPerNote;
|
|
2284
|
-
if (opts.chord && startBeat
|
|
2546
|
+
if (opts.chord && isDownbeat(startBeat, barBeats)) {
|
|
2285
2547
|
pitch = nearestChordTone(pitch, opts.chord);
|
|
2286
2548
|
}
|
|
2287
2549
|
notes.push({ pitch, startBeat, durationBeat: beatsPerNote });
|
|
2288
2550
|
}
|
|
2289
2551
|
return { notes };
|
|
2290
2552
|
}
|
|
2553
|
+
function motifToNoteEvents(cell) {
|
|
2554
|
+
return cell.notes.map((note) => ({ ...note }));
|
|
2555
|
+
}
|
|
2291
2556
|
function transformMotif(cell, t, amount, key) {
|
|
2557
|
+
return assertInRange(transformUnchecked(cell, t, amount, key));
|
|
2558
|
+
}
|
|
2559
|
+
function assertInRange(cell) {
|
|
2560
|
+
for (const note of cell.notes) {
|
|
2561
|
+
if (note.pitch < 0 || note.pitch > 127) {
|
|
2562
|
+
throw new InvalidInputError(
|
|
2563
|
+
`transformed motif pitch ${note.pitch} is outside the MIDI range 0..127`
|
|
2564
|
+
);
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
return cell;
|
|
2568
|
+
}
|
|
2569
|
+
function transformUnchecked(cell, t, amount, key) {
|
|
2570
|
+
assertNoteEvents(cell.notes, "motif notes");
|
|
2571
|
+
if (amount !== void 0) {
|
|
2572
|
+
assertFiniteNumber(amount, "motif transform amount");
|
|
2573
|
+
}
|
|
2574
|
+
if ((t === "transposeDiatonic" || t === "sequence") && amount !== void 0) {
|
|
2575
|
+
assertInteger(amount, "diatonic transform amount");
|
|
2576
|
+
}
|
|
2577
|
+
if ((t === "augment" || t === "diminish") && amount !== void 0 && amount <= 0) {
|
|
2578
|
+
throw new InvalidInputError("time transform amount must be positive");
|
|
2579
|
+
}
|
|
2292
2580
|
const notes = cell.notes;
|
|
2293
2581
|
switch (t) {
|
|
2294
2582
|
case "transposeChromatic": {
|
|
@@ -2315,7 +2603,7 @@ function transformMotif(cell, t, amount, key) {
|
|
|
2315
2603
|
return { notes: [] };
|
|
2316
2604
|
}
|
|
2317
2605
|
const start = cellOrigin(notes);
|
|
2318
|
-
const end =
|
|
2606
|
+
const end = cellEnd(notes);
|
|
2319
2607
|
return {
|
|
2320
2608
|
notes: notes.map((n) => ({
|
|
2321
2609
|
pitch: n.pitch,
|
|
@@ -2347,9 +2635,11 @@ function scaleTime(cell, factor) {
|
|
|
2347
2635
|
}))
|
|
2348
2636
|
};
|
|
2349
2637
|
}
|
|
2350
|
-
function developMotif(cell, timeline, key, bars) {
|
|
2638
|
+
function developMotif(cell, timeline, key, bars, ts = DEFAULT_TS4) {
|
|
2639
|
+
assertPositiveInt(bars, "development bars");
|
|
2640
|
+
assertNoteEvents(cell.notes, "motif notes");
|
|
2351
2641
|
const span = cellSpan(cell);
|
|
2352
|
-
const totalBeats =
|
|
2642
|
+
const totalBeats = bars * beatsPerBar(ts);
|
|
2353
2643
|
const origin = cellOrigin(cell.notes);
|
|
2354
2644
|
const out = [];
|
|
2355
2645
|
if (span <= 0) {
|
|
@@ -2357,6 +2647,7 @@ function developMotif(cell, timeline, key, bars) {
|
|
|
2357
2647
|
}
|
|
2358
2648
|
const tileSpan = Math.max(span, MIN_TILE_SPAN);
|
|
2359
2649
|
const tileCount = Math.ceil(totalBeats / tileSpan);
|
|
2650
|
+
assertGenerationBudget(tileCount * cell.notes.length, "developed motif notes");
|
|
2360
2651
|
for (let k = 0; k < tileCount; k += 1) {
|
|
2361
2652
|
const offset = k * tileSpan;
|
|
2362
2653
|
for (const n of cell.notes) {
|
|
@@ -2367,7 +2658,7 @@ function developMotif(cell, timeline, key, bars) {
|
|
|
2367
2658
|
const chord = timeline.at(startBeat);
|
|
2368
2659
|
let pitch = n.pitch;
|
|
2369
2660
|
if (chord) {
|
|
2370
|
-
const isMember = chordPitchClasses(chord).includes(
|
|
2661
|
+
const isMember = chordPitchClasses(chord).includes(pitchClassOf(pitch));
|
|
2371
2662
|
pitch = isMember ? pitch : nearestChordTone(pitch, chord);
|
|
2372
2663
|
} else {
|
|
2373
2664
|
pitch = nearestScaleTone(pitch, key);
|
|
@@ -2379,6 +2670,20 @@ function developMotif(cell, timeline, key, bars) {
|
|
|
2379
2670
|
}
|
|
2380
2671
|
|
|
2381
2672
|
// src/generate/progression/index.ts
|
|
2673
|
+
var BORROWED_DEGREES = {
|
|
2674
|
+
/** Flat submediant: bVI. */
|
|
2675
|
+
bVI: 8,
|
|
2676
|
+
/** Flat subtonic: bVII. */
|
|
2677
|
+
bVII: 10,
|
|
2678
|
+
/** Flat mediant: bIII. */
|
|
2679
|
+
bIII: 11,
|
|
2680
|
+
/** Minor subdominant borrowed from the parallel minor: iv. */
|
|
2681
|
+
iv: 12,
|
|
2682
|
+
/** Neapolitan: bII. */
|
|
2683
|
+
bII: 13,
|
|
2684
|
+
/** Sharp subdominant, the diminished #IV. */
|
|
2685
|
+
sharpIV: 14
|
|
2686
|
+
};
|
|
2382
2687
|
var PRESETS = [
|
|
2383
2688
|
{
|
|
2384
2689
|
id: "fourChordPop",
|
|
@@ -2536,18 +2841,12 @@ var PRESETS = [
|
|
|
2536
2841
|
}
|
|
2537
2842
|
];
|
|
2538
2843
|
var BORROWED_OFFSET = {
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
12: 5,
|
|
2546
|
-
// iv
|
|
2547
|
-
13: 1,
|
|
2548
|
-
// bII
|
|
2549
|
-
14: 6
|
|
2550
|
-
// #IV
|
|
2844
|
+
[BORROWED_DEGREES.bVI]: 8,
|
|
2845
|
+
[BORROWED_DEGREES.bVII]: 10,
|
|
2846
|
+
[BORROWED_DEGREES.bIII]: 3,
|
|
2847
|
+
[BORROWED_DEGREES.iv]: 5,
|
|
2848
|
+
[BORROWED_DEGREES.bII]: 1,
|
|
2849
|
+
[BORROWED_DEGREES.sharpIV]: 6
|
|
2551
2850
|
};
|
|
2552
2851
|
function progressions() {
|
|
2553
2852
|
return PRESETS.map((p) => ({ ...p, degrees: [...p.degrees], styles: [...p.styles] }));
|
|
@@ -2555,16 +2854,6 @@ function progressions() {
|
|
|
2555
2854
|
function progressionsByStyle(style) {
|
|
2556
2855
|
return progressions().filter((p) => p.styles.includes(style));
|
|
2557
2856
|
}
|
|
2558
|
-
function mulberry323(seed) {
|
|
2559
|
-
let state = seed >>> 0;
|
|
2560
|
-
return () => {
|
|
2561
|
-
state = state + 1831565813 >>> 0;
|
|
2562
|
-
let t = state;
|
|
2563
|
-
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
2564
|
-
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
2565
|
-
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
2566
|
-
};
|
|
2567
|
-
}
|
|
2568
2857
|
function degreeToRootPc(degree, key) {
|
|
2569
2858
|
if (degree >= 0 && degree <= 6) {
|
|
2570
2859
|
const tones = scaleTonesInDegreeOrder(key);
|
|
@@ -2585,38 +2874,83 @@ function autoQuality(degree, key) {
|
|
|
2585
2874
|
}
|
|
2586
2875
|
return "maj";
|
|
2587
2876
|
}
|
|
2877
|
+
function resolveCycle(degrees, key, ext) {
|
|
2878
|
+
const steps = [];
|
|
2879
|
+
for (const degree of degrees) {
|
|
2880
|
+
const step = {
|
|
2881
|
+
source: degree,
|
|
2882
|
+
rootPc: degreeToRootPc(degree, key),
|
|
2883
|
+
quality: ext !== void 0 && ext !== "auto" ? ext : autoQuality(degree, key)
|
|
2884
|
+
};
|
|
2885
|
+
if (degree >= 0 && degree <= 6) {
|
|
2886
|
+
step.degree = degree;
|
|
2887
|
+
}
|
|
2888
|
+
const previous = steps[steps.length - 1];
|
|
2889
|
+
if (previous !== void 0 && previous.source !== step.source && sameStep(previous, step)) {
|
|
2890
|
+
continue;
|
|
2891
|
+
}
|
|
2892
|
+
steps.push(step);
|
|
2893
|
+
}
|
|
2894
|
+
const first = steps[0];
|
|
2895
|
+
const last = steps[steps.length - 1];
|
|
2896
|
+
if (steps.length > 1 && first !== void 0 && last !== void 0 && first.source !== last.source && sameStep(first, last)) {
|
|
2897
|
+
steps.pop();
|
|
2898
|
+
}
|
|
2899
|
+
return steps.length > 0 ? steps : [{ source: 0, degree: 0, rootPc: degreeToRootPc(0, key), quality: "maj" }];
|
|
2900
|
+
}
|
|
2901
|
+
function sameStep(a, b) {
|
|
2902
|
+
return a.rootPc === b.rootPc && a.quality === b.quality;
|
|
2903
|
+
}
|
|
2904
|
+
function pickProgressionPreset(style, seed = 0) {
|
|
2905
|
+
const pool = PRESETS.filter((preset) => preset.styles.includes(style));
|
|
2906
|
+
if (pool.length === 0) {
|
|
2907
|
+
throw new InvalidInputError(`Unknown progression style: ${style}`);
|
|
2908
|
+
}
|
|
2909
|
+
const rng = createRng(seed);
|
|
2910
|
+
const index = Math.floor(rng.next() * pool.length) % pool.length;
|
|
2911
|
+
return pool[index] ?? PRESETS[0];
|
|
2912
|
+
}
|
|
2588
2913
|
function generateProgression(opts) {
|
|
2914
|
+
assertPositiveInt(opts.bars, "progression bars");
|
|
2915
|
+
assertGenerationBudget(opts.bars, "progression chords");
|
|
2589
2916
|
const seed = opts.seed ?? 0;
|
|
2590
2917
|
let preset;
|
|
2591
|
-
if (opts.
|
|
2918
|
+
if (opts.preset !== void 0) {
|
|
2919
|
+
if (opts.preset.degrees.length === 0) {
|
|
2920
|
+
throw new InvalidInputError("progression preset must name at least one degree");
|
|
2921
|
+
}
|
|
2922
|
+
preset = {
|
|
2923
|
+
id: opts.preset.id ?? "custom",
|
|
2924
|
+
name: opts.preset.name ?? "Custom",
|
|
2925
|
+
degrees: opts.preset.degrees,
|
|
2926
|
+
functional: opts.preset.functional ?? "loop",
|
|
2927
|
+
styles: opts.preset.styles ?? [opts.style]
|
|
2928
|
+
};
|
|
2929
|
+
}
|
|
2930
|
+
if (preset === void 0 && opts.presetId !== void 0) {
|
|
2592
2931
|
preset = PRESETS.find((p) => p.id === opts.presetId);
|
|
2593
2932
|
if (preset === void 0) {
|
|
2594
|
-
throw new
|
|
2933
|
+
throw new InvalidInputError(`Unknown progression preset: ${opts.presetId}`);
|
|
2595
2934
|
}
|
|
2596
2935
|
}
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
const candidates = pool.length > 0 ? pool : PRESETS;
|
|
2600
|
-
const rng = mulberry323(seed);
|
|
2601
|
-
const index = Math.floor(rng() * candidates.length) % candidates.length;
|
|
2602
|
-
preset = candidates[index] ?? PRESETS[0];
|
|
2603
|
-
}
|
|
2604
|
-
const degrees = preset?.degrees ?? [0];
|
|
2936
|
+
preset ??= pickProgressionPreset(opts.style, seed);
|
|
2937
|
+
const cycle = resolveCycle(preset?.degrees ?? [0], opts.key, opts.ext);
|
|
2605
2938
|
const chords = [];
|
|
2606
2939
|
for (let bar = 0; bar < opts.bars; bar += 1) {
|
|
2607
|
-
const
|
|
2608
|
-
const quality = opts.ext !== void 0 && opts.ext !== "auto" ? opts.ext : autoQuality(degree, opts.key);
|
|
2940
|
+
const step = cycle[bar % cycle.length];
|
|
2609
2941
|
const chord = {
|
|
2610
|
-
rootPc:
|
|
2611
|
-
quality,
|
|
2612
|
-
startBeat: bar * 4
|
|
2613
|
-
degree: degree >= 0 && degree <= 6 ? degree : void 0
|
|
2942
|
+
rootPc: step?.rootPc ?? 0,
|
|
2943
|
+
quality: step?.quality ?? "maj",
|
|
2944
|
+
startBeat: bar * 4
|
|
2614
2945
|
};
|
|
2946
|
+
if (step?.degree !== void 0) {
|
|
2947
|
+
chord.degree = step.degree;
|
|
2948
|
+
}
|
|
2615
2949
|
chords.push(chord);
|
|
2616
2950
|
}
|
|
2617
2951
|
if (opts.reharmonize) {
|
|
2618
2952
|
const tonicPc = (opts.key.rootPc % 12 + 12) % 12;
|
|
2619
|
-
const rrng =
|
|
2953
|
+
const rrng = createRng((seed ^ 2654435769) >>> 0);
|
|
2620
2954
|
for (let i = 0; i < chords.length - 1; i += 1) {
|
|
2621
2955
|
const cur = chords[i];
|
|
2622
2956
|
const next = chords[i + 1];
|
|
@@ -2630,7 +2964,7 @@ function generateProgression(opts) {
|
|
|
2630
2964
|
if (targetsTonic || alreadySecondary || isResolutionTarget) {
|
|
2631
2965
|
continue;
|
|
2632
2966
|
}
|
|
2633
|
-
if (rrng() < 0.5) {
|
|
2967
|
+
if (rrng.next() < 0.5) {
|
|
2634
2968
|
chords[i] = {
|
|
2635
2969
|
rootPc: domRoot,
|
|
2636
2970
|
quality: "dom7",
|
|
@@ -2644,6 +2978,17 @@ function generateProgression(opts) {
|
|
|
2644
2978
|
}
|
|
2645
2979
|
|
|
2646
2980
|
// src/generate/rhythm/index.ts
|
|
2981
|
+
var DEFAULT_RHYTHM_VELOCITY = 96;
|
|
2982
|
+
function rhythmToNoteEvents(events, pitch, velocity = DEFAULT_RHYTHM_VELOCITY) {
|
|
2983
|
+
assertFiniteNumber(pitch, "pitch");
|
|
2984
|
+
assertRange(velocity, 0, 127, "velocity");
|
|
2985
|
+
return events.map((event) => ({
|
|
2986
|
+
pitch,
|
|
2987
|
+
startBeat: event.position,
|
|
2988
|
+
durationBeat: event.duration,
|
|
2989
|
+
velocity
|
|
2990
|
+
}));
|
|
2991
|
+
}
|
|
2647
2992
|
var DEFAULT_SUBDIVISION = 2;
|
|
2648
2993
|
var DEFAULT_DENSITY = 0.5;
|
|
2649
2994
|
var DEFAULT_BARS = 1;
|
|
@@ -2663,11 +3008,14 @@ function generateRhythm(ts, opts = {}) {
|
|
|
2663
3008
|
const seed = opts.seed ?? 0;
|
|
2664
3009
|
const bars = opts.bars ?? DEFAULT_BARS;
|
|
2665
3010
|
const subdivision = opts.subdivision ?? DEFAULT_SUBDIVISION;
|
|
2666
|
-
const density =
|
|
3011
|
+
const density = assertRange(opts.density ?? DEFAULT_DENSITY, 0, 1, "rhythm density");
|
|
3012
|
+
assertPositiveInt(bars, "rhythm bars");
|
|
3013
|
+
assertPositiveInt(subdivision, "rhythm subdivision");
|
|
2667
3014
|
const barBeats = beatsPerBar(ts);
|
|
2668
3015
|
const spanBeats = barBeats * bars;
|
|
2669
3016
|
const step = 1 / subdivision;
|
|
2670
3017
|
const slotCount = Math.round(spanBeats * subdivision);
|
|
3018
|
+
assertGenerationBudget(slotCount, "rhythm grid slots");
|
|
2671
3019
|
const slotsPerBar = Math.round(barBeats * subdivision);
|
|
2672
3020
|
const rng = createRng(seed);
|
|
2673
3021
|
const positions = [];
|
|
@@ -2702,21 +3050,28 @@ function rhythmDensity(events, ts) {
|
|
|
2702
3050
|
}
|
|
2703
3051
|
|
|
2704
3052
|
export {
|
|
3053
|
+
BASS_STYLES,
|
|
2705
3054
|
generateBassLine,
|
|
2706
3055
|
generateCounterMelody,
|
|
3056
|
+
DRUM_NOTES,
|
|
3057
|
+
drumVoiceOf,
|
|
2707
3058
|
generateDrums,
|
|
2708
3059
|
humanize,
|
|
2709
3060
|
extractGrooveTemplate,
|
|
2710
3061
|
applyGrooveTemplate,
|
|
2711
3062
|
harmonizeMelody,
|
|
2712
3063
|
generateMotif,
|
|
3064
|
+
motifToNoteEvents,
|
|
2713
3065
|
transformMotif,
|
|
2714
3066
|
developMotif,
|
|
3067
|
+
BORROWED_DEGREES,
|
|
2715
3068
|
progressions,
|
|
2716
3069
|
progressionsByStyle,
|
|
3070
|
+
pickProgressionPreset,
|
|
2717
3071
|
generateProgression,
|
|
3072
|
+
rhythmToNoteEvents,
|
|
2718
3073
|
onsetWeightCurve,
|
|
2719
3074
|
generateRhythm,
|
|
2720
3075
|
rhythmDensity
|
|
2721
3076
|
};
|
|
2722
|
-
//# sourceMappingURL=chunk-
|
|
3077
|
+
//# sourceMappingURL=chunk-7NKDPF74.js.map
|