@libraz/libcantus 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -65
- package/README_ja.md +28 -66
- package/dist/analyze/index.cjs +11 -7
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +79 -144
- package/dist/analyze/index.d.ts +79 -144
- package/dist/analyze/index.js +20 -16
- package/dist/chunk-35NBV2FT.cjs +145 -0
- package/dist/chunk-35NBV2FT.cjs.map +1 -0
- package/dist/{chunk-UADYAYAT.cjs → chunk-3R3QVKY5.cjs} +1353 -580
- package/dist/chunk-3R3QVKY5.cjs.map +1 -0
- package/dist/{chunk-WWURE4XT.cjs → chunk-76ZBTRG2.cjs} +624 -729
- package/dist/chunk-76ZBTRG2.cjs.map +1 -0
- package/dist/{chunk-FVSI36UV.js → chunk-7KG3RTYS.js} +197 -110
- package/dist/chunk-7KG3RTYS.js.map +1 -0
- package/dist/{chunk-VTI6FOLB.js → chunk-AGOVW7U6.js} +709 -133
- package/dist/chunk-AGOVW7U6.js.map +1 -0
- package/dist/{chunk-E3F4AHAH.cjs → chunk-CGBADIO5.cjs} +221 -134
- package/dist/chunk-CGBADIO5.cjs.map +1 -0
- package/dist/{chunk-6XH6QZYQ.cjs → chunk-DSMJRXCZ.cjs} +1455 -849
- package/dist/chunk-DSMJRXCZ.cjs.map +1 -0
- package/dist/{chunk-PKFNUHDY.js → chunk-FBH75DAY.js} +1218 -612
- package/dist/chunk-FBH75DAY.js.map +1 -0
- package/dist/chunk-H2KJHBW4.js +3329 -0
- package/dist/chunk-H2KJHBW4.js.map +1 -0
- package/dist/chunk-HPVSH7SF.cjs +2294 -0
- package/dist/chunk-HPVSH7SF.cjs.map +1 -0
- package/dist/{chunk-MXO72PLY.cjs → chunk-HWHGOVO6.cjs} +21 -19
- package/dist/chunk-HWHGOVO6.cjs.map +1 -0
- package/dist/chunk-HYJJ2V2I.js +2294 -0
- package/dist/chunk-HYJJ2V2I.js.map +1 -0
- package/dist/{chunk-AFM2ULJH.cjs → chunk-J7FHVZII.cjs} +1567 -1067
- package/dist/chunk-J7FHVZII.cjs.map +1 -0
- package/dist/chunk-K6OZXIR6.js +1 -0
- package/dist/{chunk-H3VORETI.js → chunk-M5RYMAFI.js} +487 -592
- package/dist/chunk-M5RYMAFI.js.map +1 -0
- package/dist/chunk-NB25P3EH.js +363 -0
- package/dist/chunk-NB25P3EH.js.map +1 -0
- package/dist/{chunk-TTCLJX6D.js → chunk-NFJ4P6ZZ.js} +10 -8
- package/dist/chunk-NFJ4P6ZZ.js.map +1 -0
- package/dist/chunk-NM7UAMC3.cjs +457 -0
- package/dist/chunk-NM7UAMC3.cjs.map +1 -0
- package/dist/chunk-PM4XJE2Y.cjs +363 -0
- package/dist/chunk-PM4XJE2Y.cjs.map +1 -0
- package/dist/chunk-PUKJP2Z5.js +145 -0
- package/dist/chunk-PUKJP2Z5.js.map +1 -0
- package/dist/{chunk-VOHZB45S.js → chunk-Q6LFSAKV.js} +1321 -821
- package/dist/chunk-Q6LFSAKV.js.map +1 -0
- package/dist/{chunk-INHVXP6Z.js → chunk-RFIBF2RR.js} +1203 -430
- package/dist/chunk-RFIBF2RR.js.map +1 -0
- package/dist/{chunk-Q2WGKJNN.cjs → chunk-S5BKZZGM.cjs} +699 -123
- package/dist/chunk-S5BKZZGM.cjs.map +1 -0
- package/dist/chunk-SGLEQVJH.cjs +3329 -0
- package/dist/chunk-SGLEQVJH.cjs.map +1 -0
- package/dist/chunk-UGLFDG7L.cjs +1 -0
- package/dist/chunk-UGLFDG7L.cjs.map +1 -0
- package/dist/{chunk-YH3BIKHH.js → chunk-UMWZ4ZET.js} +146 -49
- package/dist/chunk-UMWZ4ZET.js.map +1 -0
- package/dist/{index-szBv3496.d.ts → coerce-DP0vYu-x.d.ts} +286 -199
- package/dist/{index-C_8o--VE.d.cts → coerce-DRERzdve.d.cts} +286 -199
- package/dist/core/index.cjs +9 -5
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +122 -108
- package/dist/core/index.d.ts +122 -108
- package/dist/core/index.js +32 -28
- package/dist/generate/index.cjs +8 -8
- package/dist/generate/index.d.cts +168 -84
- package/dist/generate/index.d.ts +168 -84
- package/dist/generate/index.js +7 -7
- package/dist/{index-CGpRv5sl.d.cts → index-9eoQs2C1.d.ts} +34 -19
- package/dist/{index-CCNJRG-P.d.cts → index-B9QLziDe.d.cts} +293 -56
- package/dist/{index-Dzr13q3d.d.cts → index-BBWNZS0f.d.cts} +135 -58
- package/dist/{index-9wcMhG73.d.ts → index-BHIUyjhZ.d.ts} +293 -56
- package/dist/{index-BGFW-8P2.d.cts → index-BJgP97N1.d.ts} +196 -114
- package/dist/{index-CunYKLiu.d.ts → index-BL5N2Oyz.d.cts} +1 -1
- package/dist/{index-Cj49F892.d.ts → index-BMweUlm5.d.cts} +63 -23
- package/dist/{index-CgHqxdps.d.ts → index-BNSM-DSk.d.cts} +196 -114
- package/dist/{index-D57QRbVn.d.ts → index-BRMbPkHE.d.ts} +480 -254
- package/dist/{index-Chbst3Jx.d.ts → index-CJBBiAmU.d.ts} +135 -58
- package/dist/{index-BkGmlyI3.d.cts → index-CL4b4gUQ.d.ts} +63 -23
- package/dist/{index-B9MNZboQ.d.ts → index-Cy15oTvh.d.cts} +52 -22
- package/dist/{index-B9MNZboQ.d.cts → index-Cy15oTvh.d.ts} +52 -22
- package/dist/{index-BO6xE9YU.d.cts → index-DGCNKAUr.d.cts} +480 -254
- package/dist/{index-B4o55NDr.d.cts → index-Dw2DP7vm.d.ts} +1 -1
- package/dist/{index-G1uwGGWQ.d.ts → index-cHvbYgJI.d.cts} +34 -19
- package/dist/index.cjs +42 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -14
- package/dist/index.d.ts +14 -14
- package/dist/index.js +82 -54
- package/dist/{masks-DYbE485J.d.cts → masks-Cc7-MzzY.d.ts} +49 -2
- package/dist/{masks-DYbE485J.d.ts → masks-pADfTOiD.d.cts} +49 -2
- package/dist/model/index.cjs +19 -12
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +504 -182
- package/dist/model/index.d.ts +504 -182
- package/dist/model/index.js +18 -11
- package/dist/{positional-Cptlj-xs.d.ts → positional-DJgs6ltD.d.ts} +85 -8
- package/dist/{positional-DvH_sd8u.d.cts → positional-DMZer-FM.d.cts} +85 -8
- package/dist/theory/index.cjs +35 -6
- package/dist/theory/index.cjs.map +1 -1
- package/dist/theory/index.d.cts +257 -62
- package/dist/theory/index.d.ts +257 -62
- package/dist/theory/index.js +41 -12
- package/package.json +7 -5
- package/dist/chunk-2LCPM3EG.js +0 -3224
- package/dist/chunk-2LCPM3EG.js.map +0 -1
- package/dist/chunk-6XH6QZYQ.cjs.map +0 -1
- package/dist/chunk-6XPTBE7Y.cjs +0 -49
- package/dist/chunk-6XPTBE7Y.cjs.map +0 -1
- package/dist/chunk-AFM2ULJH.cjs.map +0 -1
- package/dist/chunk-B6XNKWZV.cjs +0 -3224
- package/dist/chunk-B6XNKWZV.cjs.map +0 -1
- package/dist/chunk-E3F4AHAH.cjs.map +0 -1
- package/dist/chunk-ELCTZUJJ.js +0 -379
- package/dist/chunk-ELCTZUJJ.js.map +0 -1
- package/dist/chunk-FVSI36UV.js.map +0 -1
- package/dist/chunk-H3VORETI.js.map +0 -1
- package/dist/chunk-H42UHPYW.js +0 -1577
- package/dist/chunk-H42UHPYW.js.map +0 -1
- package/dist/chunk-HFPS7JFD.js +0 -1
- package/dist/chunk-INHVXP6Z.js.map +0 -1
- package/dist/chunk-K4ZJUGF4.cjs +0 -379
- package/dist/chunk-K4ZJUGF4.cjs.map +0 -1
- package/dist/chunk-KNJEKLKV.js +0 -49
- package/dist/chunk-KNJEKLKV.js.map +0 -1
- package/dist/chunk-MXO72PLY.cjs.map +0 -1
- package/dist/chunk-NLEU4QSE.cjs +0 -360
- package/dist/chunk-NLEU4QSE.cjs.map +0 -1
- package/dist/chunk-OE6WK2Y2.cjs +0 -1577
- package/dist/chunk-OE6WK2Y2.cjs.map +0 -1
- package/dist/chunk-PKFNUHDY.js.map +0 -1
- package/dist/chunk-Q2WGKJNN.cjs.map +0 -1
- package/dist/chunk-RHIJ3S4I.cjs +0 -1
- package/dist/chunk-RHIJ3S4I.cjs.map +0 -1
- package/dist/chunk-TTCLJX6D.js.map +0 -1
- package/dist/chunk-UADYAYAT.cjs.map +0 -1
- package/dist/chunk-VOHZB45S.js.map +0 -1
- package/dist/chunk-VTI6FOLB.js.map +0 -1
- package/dist/chunk-WWURE4XT.cjs.map +0 -1
- package/dist/chunk-YH3BIKHH.js.map +0 -1
- /package/dist/{chunk-HFPS7JFD.js.map → chunk-K6OZXIR6.js.map} +0 -0
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
|
+
HUMANIZE_ADJACENCY,
|
|
3
|
+
assertChordTimeline,
|
|
2
4
|
borrowedSource,
|
|
3
5
|
chordToRoman,
|
|
4
6
|
detectChordBest,
|
|
5
7
|
functionOf,
|
|
6
8
|
isDiatonic,
|
|
9
|
+
isMinorKey,
|
|
7
10
|
parallelKey
|
|
8
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-H2KJHBW4.js";
|
|
9
12
|
import {
|
|
10
|
-
ARTICULATIONS,
|
|
11
13
|
LIMBS,
|
|
12
14
|
canSound,
|
|
13
15
|
createPositionalRng,
|
|
@@ -15,63 +17,143 @@ import {
|
|
|
15
17
|
foldIntoRange,
|
|
16
18
|
includeAt,
|
|
17
19
|
instrumentRange,
|
|
18
|
-
resolveAlgorithmVersion
|
|
19
|
-
|
|
20
|
+
resolveAlgorithmVersion,
|
|
21
|
+
toStringedProfile
|
|
22
|
+
} from "./chunk-7KG3RTYS.js";
|
|
20
23
|
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
formatTimeSignature,
|
|
24
|
-
isStrongBeat,
|
|
25
|
-
metricWeight,
|
|
26
|
-
pulseBeats
|
|
27
|
-
} from "./chunk-ELCTZUJJ.js";
|
|
24
|
+
createNoteEventIndex
|
|
25
|
+
} from "./chunk-PUKJP2Z5.js";
|
|
28
26
|
import {
|
|
27
|
+
chordScales,
|
|
29
28
|
roleOf
|
|
30
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-NB25P3EH.js";
|
|
31
30
|
import {
|
|
32
31
|
chordPitchClasses,
|
|
33
32
|
chordQualities,
|
|
34
33
|
chordToneRole,
|
|
35
34
|
createsParallelPerfect,
|
|
35
|
+
degreeOfInterval,
|
|
36
36
|
diatonicTriad,
|
|
37
37
|
enumerateSafePitches,
|
|
38
38
|
evaluateSafety,
|
|
39
|
+
fifthPcOf,
|
|
40
|
+
heptatonicFrameOf,
|
|
39
41
|
isForbiddenMelodicLeap,
|
|
40
42
|
isScaleTone,
|
|
43
|
+
leadingTonePcOf,
|
|
41
44
|
majorKey,
|
|
42
45
|
makeChord,
|
|
43
46
|
minorKey,
|
|
47
|
+
namedScaleMask,
|
|
44
48
|
nearestScaleTone,
|
|
45
49
|
profileWeights,
|
|
50
|
+
resolveKey,
|
|
46
51
|
scaleLadderPitch,
|
|
47
52
|
scaleLadderPosition,
|
|
53
|
+
scaleSystemOf,
|
|
48
54
|
scaleTonesInDegreeOrder,
|
|
49
55
|
shiftByScaleDegrees,
|
|
56
|
+
soundsDominantSeventh,
|
|
50
57
|
spellPitchClass,
|
|
51
|
-
spellScale,
|
|
52
58
|
toChordData,
|
|
53
|
-
toKeyScale
|
|
54
|
-
|
|
59
|
+
toKeyScale,
|
|
60
|
+
tonicizableDegrees
|
|
61
|
+
} from "./chunk-RFIBF2RR.js";
|
|
55
62
|
import {
|
|
63
|
+
ARTICULATIONS,
|
|
64
|
+
BEAT_EPS,
|
|
56
65
|
InvalidInputError,
|
|
66
|
+
assertArray,
|
|
57
67
|
assertDegree,
|
|
58
68
|
assertFiniteNumber,
|
|
69
|
+
assertFiniteSemitones,
|
|
70
|
+
assertFunction,
|
|
59
71
|
assertGenerationBudget,
|
|
60
72
|
assertInteger,
|
|
73
|
+
assertMidiPitch,
|
|
61
74
|
assertNoteEvents,
|
|
62
75
|
assertOneOf,
|
|
76
|
+
assertOptions,
|
|
63
77
|
assertPositiveInt,
|
|
64
78
|
assertRange,
|
|
79
|
+
assertRecord,
|
|
65
80
|
assertTimeSignature,
|
|
81
|
+
beatsPerBar,
|
|
66
82
|
clampToMidi,
|
|
83
|
+
describeRejected,
|
|
67
84
|
dropSilentNotes,
|
|
68
|
-
|
|
85
|
+
formatTimeSignature,
|
|
86
|
+
isCompound,
|
|
87
|
+
isStrongBeat,
|
|
88
|
+
meterAt,
|
|
89
|
+
metricWeight,
|
|
69
90
|
noteToPitchClass,
|
|
70
91
|
pitchClassOf,
|
|
92
|
+
pulseBeats,
|
|
93
|
+
resolveMeters,
|
|
71
94
|
soundingNotesOnly,
|
|
95
|
+
toMeterData,
|
|
72
96
|
toSpelledInterval,
|
|
73
97
|
transposeNote
|
|
74
|
-
} from "./chunk-
|
|
98
|
+
} from "./chunk-AGOVW7U6.js";
|
|
99
|
+
|
|
100
|
+
// src/generate/context/draw.ts
|
|
101
|
+
var DRAW_METHODS = ["at", "prob", "range", "float"];
|
|
102
|
+
function assertDraw(draw, name = "draw") {
|
|
103
|
+
const sampler = assertRecord(draw, name);
|
|
104
|
+
for (const method of DRAW_METHODS) {
|
|
105
|
+
assertFunction(sampler[method], `${name}.${method}`);
|
|
106
|
+
}
|
|
107
|
+
return sampler;
|
|
108
|
+
}
|
|
109
|
+
function assertSeedPath(path, name = "draw path") {
|
|
110
|
+
const steps = assertArray(path, name);
|
|
111
|
+
for (let index = 0; index < steps.length; index += 1) {
|
|
112
|
+
const step = steps[index];
|
|
113
|
+
if (typeof step !== "string" && !Number.isFinite(step)) {
|
|
114
|
+
throw new InvalidInputError(
|
|
115
|
+
`${name}[${index}] must be a string or a finite number; received ${describeRejected(step)}`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return steps;
|
|
120
|
+
}
|
|
121
|
+
function drawFrom(rng, path) {
|
|
122
|
+
const draw = rng.at(...path);
|
|
123
|
+
if (!(draw >= 0 && draw < 1)) {
|
|
124
|
+
throw new InvalidInputError(`positional draw must be in [0, 1); received ${draw}`);
|
|
125
|
+
}
|
|
126
|
+
return draw;
|
|
127
|
+
}
|
|
128
|
+
function drawsFrom(rng, ...prefix) {
|
|
129
|
+
const at2 = (...path) => drawFrom(rng, [...prefix, ...path]);
|
|
130
|
+
return {
|
|
131
|
+
at: at2,
|
|
132
|
+
prob: (p, ...path) => includeAt(rng, p, ...prefix, ...path),
|
|
133
|
+
range: (lo, hi, ...path) => {
|
|
134
|
+
assertInteger(lo, "range lower bound");
|
|
135
|
+
assertInteger(hi, "range upper bound");
|
|
136
|
+
if (lo > hi) {
|
|
137
|
+
throw new InvalidInputError(
|
|
138
|
+
`range lower bound must not exceed upper bound; received ${lo} > ${hi}`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
assertInteger(hi - lo, "range span", 0);
|
|
142
|
+
return lo + Math.floor(at2(...path) * (hi - lo + 1));
|
|
143
|
+
},
|
|
144
|
+
float: (lo, hi, ...path) => {
|
|
145
|
+
assertFiniteNumber(lo, "float lower bound");
|
|
146
|
+
assertFiniteNumber(hi, "float upper bound");
|
|
147
|
+
if (lo > hi) {
|
|
148
|
+
throw new InvalidInputError(
|
|
149
|
+
`float lower bound must not exceed upper bound; received ${lo} > ${hi}`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
assertFiniteNumber(hi - lo, "float span");
|
|
153
|
+
return lo + at2(...path) * (hi - lo);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
75
157
|
|
|
76
158
|
// src/generate/drums/internal.ts
|
|
77
159
|
var GM = {
|
|
@@ -115,6 +197,16 @@ var PUBLIC_SECTIONS = [
|
|
|
115
197
|
"bridge",
|
|
116
198
|
"outro"
|
|
117
199
|
];
|
|
200
|
+
function backbeatBeats(snareBeat3, barBeats = 4) {
|
|
201
|
+
if (snareBeat3) {
|
|
202
|
+
return barBeats > 2 ? [2] : [];
|
|
203
|
+
}
|
|
204
|
+
const beats = [];
|
|
205
|
+
for (let beat = 1; beat < barBeats; beat += 2) {
|
|
206
|
+
beats.push(beat);
|
|
207
|
+
}
|
|
208
|
+
return beats;
|
|
209
|
+
}
|
|
118
210
|
var STYLE_MAP = {
|
|
119
211
|
// Standard pop pulse: kick on beats 1 and 3, backbeat snare on 2 and 4.
|
|
120
212
|
standard: { style: "standard", feel: "straight", ghostBoost: false, snareBeat3: false },
|
|
@@ -286,50 +378,59 @@ function sameTs(a, b) {
|
|
|
286
378
|
return a.numerator === b.numerator && a.denominator === b.denominator;
|
|
287
379
|
}
|
|
288
380
|
function fitsQuery(entry, query) {
|
|
289
|
-
|
|
381
|
+
const candidate = assertRecord(entry, "vocabulary entry");
|
|
382
|
+
const asked = assertRecord(query, "vocabulary query");
|
|
383
|
+
if (asked.genre !== void 0 && candidate.genre !== asked.genre) {
|
|
290
384
|
return false;
|
|
291
385
|
}
|
|
292
|
-
if (
|
|
386
|
+
if (asked.section !== void 0 && candidate.sections && !candidate.sections.includes(asked.section)) {
|
|
293
387
|
return false;
|
|
294
388
|
}
|
|
295
|
-
if (
|
|
296
|
-
const [low, high] =
|
|
297
|
-
if (
|
|
389
|
+
if (asked.bpm !== void 0 && candidate.tempoRange) {
|
|
390
|
+
const [low, high] = candidate.tempoRange;
|
|
391
|
+
if (asked.bpm < low || asked.bpm > high) {
|
|
298
392
|
return false;
|
|
299
393
|
}
|
|
300
394
|
}
|
|
301
|
-
if (
|
|
395
|
+
if (asked.ts !== void 0 && candidate.ts && !sameTs(candidate.ts, meterAt(0, toMeterData(asked.ts, "ts")))) {
|
|
302
396
|
return false;
|
|
303
397
|
}
|
|
304
|
-
if (
|
|
398
|
+
if (asked.quality !== void 0 && candidate.fitsOver && !candidate.fitsOver.includes(asked.quality)) {
|
|
305
399
|
return false;
|
|
306
400
|
}
|
|
307
|
-
if (
|
|
308
|
-
const available =
|
|
309
|
-
if (
|
|
401
|
+
if (asked.articulations !== void 0) {
|
|
402
|
+
const available = asked.articulations;
|
|
403
|
+
if (candidate.articulations.some((articulation) => !available.includes(articulation))) {
|
|
310
404
|
return false;
|
|
311
405
|
}
|
|
312
406
|
}
|
|
313
|
-
return
|
|
407
|
+
return asked.difficulty === void 0 || candidate.difficulty <= asked.difficulty;
|
|
314
408
|
}
|
|
315
409
|
function selectVocabulary(dictionary, query) {
|
|
316
|
-
return dictionary.filter(
|
|
410
|
+
return assertArray(dictionary, "dictionary").filter(
|
|
411
|
+
(entry) => fitsQuery(entry, query)
|
|
412
|
+
);
|
|
317
413
|
}
|
|
318
414
|
function pickVocabulary(dictionary, query, draw, ...path) {
|
|
415
|
+
const sampler = assertDraw(draw);
|
|
416
|
+
assertSeedPath(path);
|
|
319
417
|
const candidates = selectVocabulary(dictionary, query);
|
|
320
418
|
if (candidates.length === 0) {
|
|
321
419
|
return void 0;
|
|
322
420
|
}
|
|
323
|
-
return candidates[
|
|
421
|
+
return candidates[sampler.range(0, candidates.length - 1, ...path)];
|
|
324
422
|
}
|
|
325
423
|
function mergeVocabulary(builtIn, supplied) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
|
|
424
|
+
const library = assertArray(builtIn, "builtIn");
|
|
425
|
+
const extra = supplied === void 0 || supplied === null ? [] : assertArray(supplied, "supplied");
|
|
426
|
+
if (extra.length === 0) {
|
|
427
|
+
return [...library];
|
|
428
|
+
}
|
|
429
|
+
const idOf = (entry, index, side) => assertRecord(entry, `${side}[${index}]`).id;
|
|
430
|
+
const byId = new Map(extra.map((entry, index) => [idOf(entry, index, "supplied"), entry]));
|
|
431
|
+
const merged = library.map((entry, index) => byId.get(idOf(entry, index, "builtIn")) ?? entry);
|
|
432
|
+
const replaced = new Set(library.map((entry, index) => idOf(entry, index, "builtIn")));
|
|
433
|
+
for (const entry of extra) {
|
|
333
434
|
if (!replaced.has(entry.id)) {
|
|
334
435
|
merged.push(entry);
|
|
335
436
|
}
|
|
@@ -338,30 +439,49 @@ function mergeVocabulary(builtIn, supplied) {
|
|
|
338
439
|
}
|
|
339
440
|
function vocabularyOfKind(entries, isMaterial) {
|
|
340
441
|
const out = [];
|
|
341
|
-
|
|
342
|
-
|
|
442
|
+
const recognises = assertFunction(isMaterial, "isMaterial");
|
|
443
|
+
for (const entry of assertArray(entries, "entries")) {
|
|
444
|
+
if (recognises(assertRecord(entry, "entry").material)) {
|
|
343
445
|
out.push(entry);
|
|
344
446
|
}
|
|
345
447
|
}
|
|
346
448
|
return out;
|
|
347
449
|
}
|
|
450
|
+
function assertNames(values, allowed, label) {
|
|
451
|
+
if (!Array.isArray(values)) {
|
|
452
|
+
throw new InvalidInputError(
|
|
453
|
+
`${label} must be an array of names; received ${describeRejected(values)}`
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
values.forEach((value, index) => {
|
|
457
|
+
assertOneOf(value, allowed, `${label}[${index}]`);
|
|
458
|
+
});
|
|
459
|
+
}
|
|
348
460
|
function assertVocabulary(entry, label = "vocabulary entry") {
|
|
461
|
+
if (typeof entry !== "object" || entry === null) {
|
|
462
|
+
throw new InvalidInputError(
|
|
463
|
+
`${label} must be a dictionary entry; received ${describeRejected(entry)}`
|
|
464
|
+
);
|
|
465
|
+
}
|
|
349
466
|
assertOneOf(entry.genre, GENRES, `${label} genre`);
|
|
350
467
|
assertOneOf(entry.provenance?.basis, PROVENANCE_BASES, `${label} provenance basis`);
|
|
351
|
-
entry.articulations
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
assertOneOf(quality, chordQualities(), `${label} fitsOver[${index}]`);
|
|
359
|
-
});
|
|
468
|
+
assertNames(entry.articulations, ARTICULATIONS, `${label} articulations`);
|
|
469
|
+
if (entry.sections !== void 0) {
|
|
470
|
+
assertNames(entry.sections, PUBLIC_SECTIONS, `${label} sections`);
|
|
471
|
+
}
|
|
472
|
+
if (entry.fitsOver !== void 0) {
|
|
473
|
+
assertNames(entry.fitsOver, chordQualities(), `${label} fitsOver`);
|
|
474
|
+
}
|
|
360
475
|
if (entry.ts !== void 0) {
|
|
361
476
|
assertTimeSignature(entry.ts, `${label} ts`);
|
|
362
477
|
}
|
|
363
478
|
assertRange(entry.difficulty, 1, 5, `${label} difficulty`);
|
|
364
|
-
if (entry.tempoRange) {
|
|
479
|
+
if (entry.tempoRange !== void 0) {
|
|
480
|
+
if (!Array.isArray(entry.tempoRange) || entry.tempoRange.length !== 2) {
|
|
481
|
+
throw new InvalidInputError(
|
|
482
|
+
`${label} tempoRange must be a low and a high tempo; received ${describeRejected(entry.tempoRange)}`
|
|
483
|
+
);
|
|
484
|
+
}
|
|
365
485
|
const [low, high] = entry.tempoRange;
|
|
366
486
|
assertRange(low, Number.MIN_VALUE, 1e3, `${label} tempoRange low`);
|
|
367
487
|
assertRange(high, low, 1e3, `${label} tempoRange high`);
|
|
@@ -389,60 +509,41 @@ function assertDifficulty(difficulty, label = "complexity difficulty") {
|
|
|
389
509
|
return assertRange(difficulty, MIN_DIFFICULTY, MAX_DIFFICULTY, label);
|
|
390
510
|
}
|
|
391
511
|
function sustainsStrokes(stepBeats, bpm, difficulty) {
|
|
392
|
-
if (difficulty === void 0 || bpm === void 0
|
|
512
|
+
if (difficulty === void 0 || bpm === void 0) {
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
assertDifficulty(difficulty);
|
|
516
|
+
assertFiniteNumber(bpm, "bpm");
|
|
517
|
+
if (assertFiniteNumber(stepBeats, "stepBeats") <= 0) {
|
|
393
518
|
return true;
|
|
394
519
|
}
|
|
395
520
|
const rate = 1 / (stepBeats * secondsPerBeat(bpm));
|
|
396
521
|
return rate <= atLevel(STROKES_PER_SECOND, difficulty);
|
|
397
522
|
}
|
|
398
523
|
function sustainsShift(semitones, stepBeats, bpm, difficulty) {
|
|
399
|
-
if (difficulty === void 0 || bpm === void 0
|
|
524
|
+
if (difficulty === void 0 || bpm === void 0) {
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
assertDifficulty(difficulty);
|
|
528
|
+
assertFiniteNumber(bpm, "bpm");
|
|
529
|
+
assertFiniteSemitones(semitones);
|
|
530
|
+
if (assertFiniteNumber(stepBeats, "stepBeats") <= 0) {
|
|
400
531
|
return true;
|
|
401
532
|
}
|
|
402
533
|
const seconds = stepBeats * secondsPerBeat(bpm);
|
|
403
534
|
return Math.abs(semitones) / seconds <= atLevel(FRETS_PER_SECOND, difficulty);
|
|
404
535
|
}
|
|
405
536
|
|
|
406
|
-
// src/generate/context/draw.ts
|
|
407
|
-
function drawFrom(rng, path) {
|
|
408
|
-
const draw = rng.at(...path);
|
|
409
|
-
if (!(draw >= 0 && draw < 1)) {
|
|
410
|
-
throw new InvalidInputError(`positional draw must be in [0, 1); received ${draw}`);
|
|
411
|
-
}
|
|
412
|
-
return draw;
|
|
413
|
-
}
|
|
414
|
-
function drawsFrom(rng, ...prefix) {
|
|
415
|
-
const at2 = (...path) => drawFrom(rng, [...prefix, ...path]);
|
|
416
|
-
return {
|
|
417
|
-
at: at2,
|
|
418
|
-
prob: (p, ...path) => includeAt(rng, p, ...prefix, ...path),
|
|
419
|
-
range: (lo, hi, ...path) => {
|
|
420
|
-
assertInteger(lo, "range lower bound");
|
|
421
|
-
assertInteger(hi, "range upper bound");
|
|
422
|
-
if (lo > hi) {
|
|
423
|
-
throw new InvalidInputError(
|
|
424
|
-
`range lower bound must not exceed upper bound; received ${lo} > ${hi}`
|
|
425
|
-
);
|
|
426
|
-
}
|
|
427
|
-
return lo + Math.floor(at2(...path) * (hi - lo + 1));
|
|
428
|
-
},
|
|
429
|
-
float: (lo, hi, ...path) => {
|
|
430
|
-
assertFiniteNumber(lo, "float lower bound");
|
|
431
|
-
assertFiniteNumber(hi, "float upper bound");
|
|
432
|
-
if (lo > hi) {
|
|
433
|
-
throw new InvalidInputError(
|
|
434
|
-
`float lower bound must not exceed upper bound; received ${lo} > ${hi}`
|
|
435
|
-
);
|
|
436
|
-
}
|
|
437
|
-
return lo + at2(...path) * (hi - lo);
|
|
438
|
-
}
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
|
|
442
537
|
// src/generate/context/index.ts
|
|
443
538
|
var DEFAULT_SEED = 0;
|
|
444
539
|
var VERSION_SEGMENT = "v";
|
|
445
540
|
function resolveContext(input) {
|
|
541
|
+
if (input !== null && typeof input !== "number" && input !== void 0) {
|
|
542
|
+
assertRecord(input, "ctx");
|
|
543
|
+
}
|
|
544
|
+
if (input === null) {
|
|
545
|
+
throw new InvalidInputError("ctx must be a seed or a generation context; received null");
|
|
546
|
+
}
|
|
446
547
|
const ctx = typeof input === "number" ? { seed: input } : input ?? { seed: DEFAULT_SEED };
|
|
447
548
|
const seed = assertInteger(ctx.seed ?? DEFAULT_SEED, "seed", 0, 4294967295);
|
|
448
549
|
const complexity = ctx.complexity ?? {};
|
|
@@ -472,24 +573,59 @@ function resolveContext(input) {
|
|
|
472
573
|
}
|
|
473
574
|
function resolveContextWith(input, sugar) {
|
|
474
575
|
const base = resolveContext(input);
|
|
475
|
-
const
|
|
576
|
+
const supplied = sugar.ornament === void 0 ? void 0 : assertRange(sugar.ornament, 0, 1, "complexity ornament");
|
|
577
|
+
const ornament2 = base.ornament ?? supplied;
|
|
476
578
|
return { ...base, ornament: ornament2 };
|
|
477
579
|
}
|
|
478
580
|
|
|
479
581
|
// src/generate/bass/internal.ts
|
|
480
582
|
var STRONG_VELOCITY = 100;
|
|
481
583
|
var WEAK_VELOCITY = 80;
|
|
482
|
-
var
|
|
584
|
+
var DEFAULT_TS = { numerator: 4, denominator: 4 };
|
|
585
|
+
var DEFAULT_OCTAVE = 2;
|
|
483
586
|
function placePc(pc, anchor, low) {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
587
|
+
return foldIntoBand(pc + 12 * Math.round((anchor - pc) / 12), low);
|
|
588
|
+
}
|
|
589
|
+
function placeAboveBass(pc, bassMidi) {
|
|
590
|
+
return foldIntoBand(pc + 12 * Math.round((bassMidi - pc) / 12), bassMidi);
|
|
591
|
+
}
|
|
592
|
+
function placeRoot(pc, low) {
|
|
593
|
+
return low + pitchClassOf(pc - low);
|
|
594
|
+
}
|
|
595
|
+
function barTiles(startBeat, endBeat, ts) {
|
|
596
|
+
const barBeats = beatsPerBar(ts);
|
|
597
|
+
const tiles = [];
|
|
598
|
+
for (let index = 0; ; index += 1) {
|
|
599
|
+
const tileStart = startBeat + index * barBeats;
|
|
600
|
+
if (tileStart >= endBeat - BEAT_EPS) {
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
tiles.push({ startBeat: tileStart, endBeat: Math.min(tileStart + barBeats, endBeat) });
|
|
488
604
|
}
|
|
489
|
-
|
|
490
|
-
|
|
605
|
+
return tiles;
|
|
606
|
+
}
|
|
607
|
+
function midBarPulse(barStart, ts) {
|
|
608
|
+
const pulse2 = pulseBeats(ts);
|
|
609
|
+
return barStart + Math.floor((beatsPerBar(ts) / 2 + BEAT_EPS) / pulse2) * pulse2;
|
|
610
|
+
}
|
|
611
|
+
function assertBassSegments(segments) {
|
|
612
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
613
|
+
const segment = segments[index];
|
|
614
|
+
if (segment === void 0) {
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
assertRange(segment.startBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].startBeat`);
|
|
618
|
+
assertRange(segment.endBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].endBeat`);
|
|
619
|
+
if (segment.endBeat <= segment.startBeat) {
|
|
620
|
+
throw new InvalidInputError(`segments[${index}] must have a positive duration`);
|
|
621
|
+
}
|
|
622
|
+
const previous = segments[index - 1];
|
|
623
|
+
if (previous !== void 0 && segment.startBeat < previous.endBeat - BEAT_EPS) {
|
|
624
|
+
throw new InvalidInputError(
|
|
625
|
+
`bass segments must not overlap: segments ${index - 1} and ${index} overlap`
|
|
626
|
+
);
|
|
627
|
+
}
|
|
491
628
|
}
|
|
492
|
-
return midi;
|
|
493
629
|
}
|
|
494
630
|
function chordTonePcs(chord) {
|
|
495
631
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -506,29 +642,33 @@ function chordTonePcs(chord) {
|
|
|
506
642
|
}
|
|
507
643
|
return pcs;
|
|
508
644
|
}
|
|
509
|
-
function
|
|
510
|
-
|
|
511
|
-
const chosen = fifths.has(7) ? 7 : fifths.has(6) ? 6 : fifths.has(8) ? 8 : void 0;
|
|
512
|
-
return chosen === void 0 ? pitchClassOf(chord.rootPc) : pitchClassOf(chord.rootPc + chosen);
|
|
645
|
+
function fifthPcOf2(chord) {
|
|
646
|
+
return fifthPcOf(chord) ?? pitchClassOf(chord.rootPc);
|
|
513
647
|
}
|
|
514
648
|
function bassPcOf(chord) {
|
|
515
649
|
return pitchClassOf(chord.bassPc ?? chord.rootPc);
|
|
516
650
|
}
|
|
651
|
+
function bassToneCycle(chord) {
|
|
652
|
+
const bass = bassPcOf(chord);
|
|
653
|
+
const tones = chordTonePcs(chord);
|
|
654
|
+
const at2 = tones.indexOf(bass);
|
|
655
|
+
return at2 < 0 ? [bass, ...tones] : [...tones.slice(at2), ...tones.slice(0, at2)];
|
|
656
|
+
}
|
|
517
657
|
function beatPositions(start, end, ts) {
|
|
518
658
|
const positions = [];
|
|
519
659
|
const barBeats = beatsPerBar(ts);
|
|
520
660
|
const pulse2 = pulseBeats(ts);
|
|
521
661
|
const firstBar = Math.floor(start / barBeats);
|
|
522
|
-
const lastBar = Math.floor((end -
|
|
662
|
+
const lastBar = Math.floor((end - BEAT_EPS) / barBeats);
|
|
523
663
|
for (let bar2 = firstBar; bar2 <= lastBar; bar2 += 1) {
|
|
524
664
|
const barStart = bar2 * barBeats;
|
|
525
|
-
const firstPulse = Math.max(0, Math.ceil((start - barStart -
|
|
665
|
+
const firstPulse = Math.max(0, Math.ceil((start - barStart - BEAT_EPS) / pulse2));
|
|
526
666
|
for (let index = firstPulse; ; index += 1) {
|
|
527
667
|
const position = barStart + index * pulse2;
|
|
528
|
-
if (position >= barStart + barBeats -
|
|
668
|
+
if (position >= barStart + barBeats - BEAT_EPS || position >= end - BEAT_EPS) {
|
|
529
669
|
break;
|
|
530
670
|
}
|
|
531
|
-
if (position >= start -
|
|
671
|
+
if (position >= start - BEAT_EPS) {
|
|
532
672
|
positions.push(position);
|
|
533
673
|
}
|
|
534
674
|
}
|
|
@@ -539,70 +679,88 @@ function beatPositions(start, end, ts) {
|
|
|
539
679
|
return positions;
|
|
540
680
|
}
|
|
541
681
|
function foldIntoBand(midi, low) {
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
result += 12;
|
|
545
|
-
}
|
|
546
|
-
while (result > low + 12) {
|
|
547
|
-
result -= 12;
|
|
548
|
-
}
|
|
549
|
-
return result;
|
|
682
|
+
const raised = midi + 12 * Math.max(0, Math.ceil((low - midi) / 12));
|
|
683
|
+
return raised - 12 * Math.max(0, Math.ceil((raised - low - 12) / 12));
|
|
550
684
|
}
|
|
551
685
|
function bandFloor(low, instrument) {
|
|
552
686
|
if (!instrument) {
|
|
553
687
|
return low;
|
|
554
688
|
}
|
|
555
689
|
const range = instrumentRange(instrument);
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
floor -= 12;
|
|
559
|
-
}
|
|
560
|
-
while (floor < range.low) {
|
|
561
|
-
floor += 12;
|
|
562
|
-
}
|
|
563
|
-
return floor;
|
|
690
|
+
const dropped = low - 12 * Math.max(0, Math.ceil((low + 12 - range.high) / 12));
|
|
691
|
+
return dropped + 12 * Math.max(0, Math.ceil((range.low - dropped) / 12));
|
|
564
692
|
}
|
|
565
|
-
function approachNote(target, from,
|
|
693
|
+
function approachNote(target, from, key, chromatic) {
|
|
566
694
|
const dir = from <= target ? -1 : 1;
|
|
695
|
+
const near = neighborOnSide(target, dir, key, chromatic);
|
|
696
|
+
const far = neighborOnSide(target, -dir, key, chromatic);
|
|
697
|
+
const playable = (midi) => midi >= 0 && midi <= 127;
|
|
698
|
+
if (playable(near) && near !== from) {
|
|
699
|
+
return near;
|
|
700
|
+
}
|
|
701
|
+
return playable(far) ? far : near;
|
|
702
|
+
}
|
|
703
|
+
function neighborOnSide(target, dir, key, chromatic) {
|
|
567
704
|
const semitone = target + dir;
|
|
568
705
|
if (chromatic) {
|
|
569
|
-
return
|
|
706
|
+
return semitone;
|
|
570
707
|
}
|
|
571
708
|
const cand = nearestScaleTone(target + dir * 2, key);
|
|
572
709
|
const step = Math.abs(cand - target);
|
|
573
|
-
return
|
|
710
|
+
return step >= 1 && step <= 2 ? cand : semitone;
|
|
711
|
+
}
|
|
712
|
+
function bassRegister(named, given, octave) {
|
|
713
|
+
const instrument = given === void 0 ? named !== void 0 && named.kind === "stringed" ? named : void 0 : toStringedProfile(given, "instrument");
|
|
714
|
+
return { instrument, low: bandFloor(octave * 12 + 12, instrument) };
|
|
574
715
|
}
|
|
575
716
|
|
|
576
717
|
// src/generate/vocabulary/transform.ts
|
|
577
718
|
var BAR_STEPS = 16;
|
|
578
719
|
var BEAT_STEPS = 4;
|
|
579
720
|
var STEP_BEATS = 1 / BEAT_STEPS;
|
|
721
|
+
function assertGridEvents(events) {
|
|
722
|
+
assertArray(events, "figure events");
|
|
723
|
+
events.forEach((event, index) => {
|
|
724
|
+
assertRecord(event, `figure events[${index}]`);
|
|
725
|
+
assertFiniteNumber(event.step, `figure events[${index}].step`);
|
|
726
|
+
});
|
|
727
|
+
return events;
|
|
728
|
+
}
|
|
729
|
+
function assertSpanSteps(spanSteps) {
|
|
730
|
+
return assertPositiveInt(spanSteps, "figure span steps");
|
|
731
|
+
}
|
|
580
732
|
function streamOf(event) {
|
|
581
733
|
return event.limb ?? event.voice ?? "";
|
|
582
734
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
735
|
+
var FIGURE_TS = { numerator: 4, denominator: 4 };
|
|
736
|
+
function subdivisionBeats(ts) {
|
|
737
|
+
const pulse2 = pulseBeats(ts);
|
|
738
|
+
return isCompound(ts) ? pulse2 / 3 : pulse2 / 2;
|
|
739
|
+
}
|
|
740
|
+
function gridMetricWeight(step, ts) {
|
|
741
|
+
assertFiniteNumber(step, "step");
|
|
742
|
+
return stepWeight(step, meterAt(0, toMeterData(ts, "ts")));
|
|
743
|
+
}
|
|
744
|
+
function stepWeight(step, ts) {
|
|
745
|
+
const beats = step * STEP_BEATS;
|
|
746
|
+
const weight = metricWeight(beats, ts);
|
|
747
|
+
if (weight > 0) {
|
|
748
|
+
return weight + 1;
|
|
596
749
|
}
|
|
597
|
-
|
|
750
|
+
const subdivision = subdivisionBeats(ts);
|
|
751
|
+
const divisions = beats / subdivision;
|
|
752
|
+
return Math.abs(divisions - Math.round(divisions)) < 1e-9 ? 1 : 0;
|
|
598
753
|
}
|
|
599
|
-
function thin(events, amount) {
|
|
754
|
+
function thin(events, amount, ts) {
|
|
755
|
+
const figure = assertGridEvents(events);
|
|
600
756
|
assertRange(amount, 0, 1, "thin amount");
|
|
757
|
+
const signature = meterAt(0, toMeterData(ts, "ts"));
|
|
601
758
|
const floor = Math.min(4, Math.floor(amount * 5));
|
|
602
|
-
return
|
|
759
|
+
return figure.filter((event) => stepWeight(event.step, signature) >= floor);
|
|
603
760
|
}
|
|
604
761
|
function double(events, spanSteps = BAR_STEPS) {
|
|
605
|
-
|
|
762
|
+
assertSpanSteps(spanSteps);
|
|
763
|
+
const sorted = [...assertGridEvents(events)].sort((a, b) => a.step - b.step);
|
|
606
764
|
const out = [];
|
|
607
765
|
for (let i = 0; i < sorted.length; i += 1) {
|
|
608
766
|
const event = sorted[i];
|
|
@@ -620,30 +778,46 @@ function double(events, spanSteps = BAR_STEPS) {
|
|
|
620
778
|
}
|
|
621
779
|
var ADDED_NOTE_VELOCITY = 0.75;
|
|
622
780
|
function halfTime(events, spanSteps = BAR_STEPS) {
|
|
623
|
-
|
|
781
|
+
assertSpanSteps(spanSteps);
|
|
782
|
+
return assertGridEvents(events).map((event) => ({ ...event, step: event.step * 2 })).filter((event) => event.step < spanSteps);
|
|
624
783
|
}
|
|
625
784
|
function doubleTime(events, spanSteps = BAR_STEPS) {
|
|
626
|
-
|
|
785
|
+
assertSpanSteps(spanSteps);
|
|
786
|
+
assertGridEvents(events);
|
|
787
|
+
const merged = /* @__PURE__ */ new Map();
|
|
788
|
+
const place = (event, step, velocity) => {
|
|
789
|
+
const key = `${streamOf(event)}@${step}`;
|
|
790
|
+
const held = merged.get(key);
|
|
791
|
+
if (held === void 0 || velocity > held.velocity) {
|
|
792
|
+
merged.set(key, { ...event, step, velocity });
|
|
793
|
+
}
|
|
794
|
+
};
|
|
627
795
|
for (const event of events) {
|
|
628
796
|
const step = Math.round(event.step / 2);
|
|
629
|
-
|
|
797
|
+
place(event, step, event.velocity);
|
|
630
798
|
const repeated = step + spanSteps / 2;
|
|
631
799
|
if (repeated < spanSteps) {
|
|
632
|
-
|
|
800
|
+
place(event, repeated, event.velocity * ADDED_NOTE_VELOCITY);
|
|
633
801
|
}
|
|
634
802
|
}
|
|
635
|
-
return
|
|
803
|
+
return [...merged.values()].sort((a, b) => a.step - b.step);
|
|
636
804
|
}
|
|
637
805
|
function syncopate(events, amount, draw, ...path) {
|
|
638
|
-
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
806
|
+
const figure = assertGridEvents(events);
|
|
807
|
+
const sampler = assertDraw(draw);
|
|
808
|
+
assertSeedPath(path);
|
|
809
|
+
const asked = typeof amount === "number" ? void 0 : assertRecord(amount, "amount");
|
|
810
|
+
const dial = asked === void 0 ? amount : asked.amount;
|
|
811
|
+
const signature = meterAt(0, toMeterData(asked?.ts ?? FIGURE_TS, "ts"));
|
|
812
|
+
assertRange(dial, 0, 1, "syncopate amount");
|
|
813
|
+
const occupied = new Set(figure.map((event) => `${streamOf(event)}@${event.step}`));
|
|
814
|
+
const out = [...figure];
|
|
815
|
+
for (const event of figure) {
|
|
642
816
|
const anticipated = event.step - 1;
|
|
643
|
-
if (
|
|
817
|
+
if (stepWeight(event.step, signature) < 2 || anticipated < 1 || occupied.has(`${streamOf(event)}@${anticipated}`)) {
|
|
644
818
|
continue;
|
|
645
819
|
}
|
|
646
|
-
if (!
|
|
820
|
+
if (!sampler.prob(dial, ...path, "syncopate", event.step)) {
|
|
647
821
|
continue;
|
|
648
822
|
}
|
|
649
823
|
out.push({ ...event, step: anticipated, velocity: event.velocity * ADDED_NOTE_VELOCITY });
|
|
@@ -651,35 +825,55 @@ function syncopate(events, amount, draw, ...path) {
|
|
|
651
825
|
return out.sort((a, b) => a.step - b.step);
|
|
652
826
|
}
|
|
653
827
|
function ornamentBy(events, isOrnament, amount, draw, ...path) {
|
|
828
|
+
const figure = assertGridEvents(events);
|
|
829
|
+
const marks = assertFunction(isOrnament, "isOrnament");
|
|
830
|
+
const sampler = assertDraw(draw);
|
|
831
|
+
assertSeedPath(path);
|
|
654
832
|
assertRange(amount, 0, 1, "ornament amount");
|
|
655
|
-
return
|
|
656
|
-
(event) => !
|
|
833
|
+
return figure.filter(
|
|
834
|
+
(event) => !marks(event) || sampler.prob(amount, ...path, "ornament", event.step)
|
|
657
835
|
);
|
|
658
836
|
}
|
|
659
837
|
var NEUTRAL_RHYTHMIC = 0.5;
|
|
660
838
|
function deform(events, opts, draw, ...path) {
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
839
|
+
const figure = assertGridEvents(events);
|
|
840
|
+
const asked = assertRecord(opts, "opts");
|
|
841
|
+
assertDraw(draw);
|
|
842
|
+
assertSeedPath(path);
|
|
843
|
+
const spanSteps = assertSpanSteps(asked.spanSteps === void 0 ? BAR_STEPS : asked.spanSteps);
|
|
844
|
+
const ts = asked.ts ?? FIGURE_TS;
|
|
845
|
+
let out = asked.rate === "half" ? halfTime(figure, spanSteps) : asked.rate === "double" ? doubleTime(figure, spanSteps) : [...figure];
|
|
846
|
+
if (asked.rhythmic !== void 0) {
|
|
847
|
+
const rhythmic = assertRange(asked.rhythmic, 0, 1, "complexity rhythmic");
|
|
665
848
|
if (rhythmic < NEUTRAL_RHYTHMIC) {
|
|
666
|
-
out = thin(out, (NEUTRAL_RHYTHMIC - rhythmic) / NEUTRAL_RHYTHMIC);
|
|
849
|
+
out = thin(out, (NEUTRAL_RHYTHMIC - rhythmic) / NEUTRAL_RHYTHMIC, ts);
|
|
667
850
|
} else if (rhythmic > NEUTRAL_RHYTHMIC) {
|
|
668
|
-
out = syncopate(
|
|
851
|
+
out = syncopate(
|
|
852
|
+
out,
|
|
853
|
+
{ amount: (rhythmic - NEUTRAL_RHYTHMIC) / NEUTRAL_RHYTHMIC, ts },
|
|
854
|
+
draw,
|
|
855
|
+
...path
|
|
856
|
+
);
|
|
669
857
|
}
|
|
670
858
|
}
|
|
671
|
-
if (
|
|
672
|
-
const isOrnament = opts.isOrnament;
|
|
673
|
-
out = ornamentBy(out, (event) => isOrnament(event),
|
|
859
|
+
if (asked.ornament !== void 0 && asked.isOrnament) {
|
|
860
|
+
const isOrnament = assertFunction(asked.isOrnament, "opts.isOrnament");
|
|
861
|
+
out = ornamentBy(out, (event) => isOrnament(event), asked.ornament, draw, ...path);
|
|
674
862
|
}
|
|
675
863
|
return out.sort((a, b) => a.step - b.step);
|
|
676
864
|
}
|
|
677
865
|
function withinCeiling(events, bpm, difficulty) {
|
|
678
|
-
|
|
866
|
+
const figure = assertGridEvents(events);
|
|
867
|
+
if (difficulty === void 0 || bpm === void 0) {
|
|
868
|
+
return true;
|
|
869
|
+
}
|
|
870
|
+
assertFiniteNumber(bpm, "bpm");
|
|
871
|
+
assertDifficulty(difficulty);
|
|
872
|
+
if (figure.length < 2) {
|
|
679
873
|
return true;
|
|
680
874
|
}
|
|
681
875
|
const byStream = /* @__PURE__ */ new Map();
|
|
682
|
-
for (const event of
|
|
876
|
+
for (const event of figure) {
|
|
683
877
|
const stream = streamOf(event);
|
|
684
878
|
const steps = byStream.get(stream);
|
|
685
879
|
if (steps) {
|
|
@@ -704,7 +898,79 @@ function withinCeiling(events, bpm, difficulty) {
|
|
|
704
898
|
return sustainsStrokes(closest * STEP_BEATS, bpm, difficulty);
|
|
705
899
|
}
|
|
706
900
|
|
|
707
|
-
// src/generate/bass/
|
|
901
|
+
// src/generate/bass/degrees.ts
|
|
902
|
+
var MAJOR_DEGREE_SEMITONES = [0, 2, 4, 5, 7, 9, 11];
|
|
903
|
+
var HEPTATONIC_SIZE = 7;
|
|
904
|
+
function impliedScaleTones(chord) {
|
|
905
|
+
if (chordDegreeSemitone(7, chord) === void 0) {
|
|
906
|
+
return void 0;
|
|
907
|
+
}
|
|
908
|
+
for (const match of chordScales(chord)) {
|
|
909
|
+
const mask = namedScaleMask(match.name);
|
|
910
|
+
if (mask === void 0) {
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
const tones = [];
|
|
914
|
+
for (let offset = 0; offset < 12; offset += 1) {
|
|
915
|
+
if ((mask >> offset & 1) === 1) {
|
|
916
|
+
tones.push(offset);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
if (tones.length === HEPTATONIC_SIZE) {
|
|
920
|
+
return tones;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
return void 0;
|
|
924
|
+
}
|
|
925
|
+
function replacedDegreeSemitone(degree, chord) {
|
|
926
|
+
if (degree !== 3) {
|
|
927
|
+
return void 0;
|
|
928
|
+
}
|
|
929
|
+
const sounds = (semitones) => chord.intervals.some((interval) => pitchClassOf(interval) === semitones);
|
|
930
|
+
if (sounds(3) || sounds(4)) {
|
|
931
|
+
return void 0;
|
|
932
|
+
}
|
|
933
|
+
return sounds(5) ? 5 : sounds(2) ? 2 : void 0;
|
|
934
|
+
}
|
|
935
|
+
function degreeSemitone(degree, alter, chord, key, implied) {
|
|
936
|
+
const octaves = Math.floor((degree - 1) / 7);
|
|
937
|
+
const within = (degree - 1) % 7 + 1;
|
|
938
|
+
const template = MAJOR_DEGREE_SEMITONES[within - 1] ?? 0;
|
|
939
|
+
const semitone = alter !== 0 ? template : chordDegreeSemitone(within, chord) ?? replacedDegreeSemitone(within, chord) ?? implied?.[within - 1] ?? // A degree neither the chord nor its quality settles is a passing tone,
|
|
940
|
+
// so the key decides it.
|
|
941
|
+
nearestScaleTone(chord.rootPc + template, key) - chord.rootPc;
|
|
942
|
+
return pitchClassOf(semitone) + 12 * octaves + alter;
|
|
943
|
+
}
|
|
944
|
+
function chordDegreeSemitone(degree, chord) {
|
|
945
|
+
for (const interval of chord.intervals) {
|
|
946
|
+
if (degreeOfInterval(interval, chord) === degree) {
|
|
947
|
+
return interval;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return void 0;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// src/generate/vocabulary/freeze.ts
|
|
954
|
+
function freezeReachable(value, seen) {
|
|
955
|
+
if (value === null || typeof value !== "object") {
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
const object = value;
|
|
959
|
+
if (seen.has(object)) {
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
seen.add(object);
|
|
963
|
+
Object.freeze(object);
|
|
964
|
+
for (const child of Object.values(object)) {
|
|
965
|
+
freezeReachable(child, seen);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
function deepFreeze(value) {
|
|
969
|
+
freezeReachable(value, /* @__PURE__ */ new WeakSet());
|
|
970
|
+
return value;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// src/generate/bass/licks-data.ts
|
|
708
974
|
function isLickMaterial(material) {
|
|
709
975
|
const candidate = material;
|
|
710
976
|
return typeof candidate === "object" && candidate !== null && typeof candidate.lengthSteps === "number" && Array.isArray(candidate.notes);
|
|
@@ -714,7 +980,7 @@ function note(degree, step, velocity, extra) {
|
|
|
714
980
|
}
|
|
715
981
|
var BAR = 4 * BEAT_STEPS;
|
|
716
982
|
var FOUR_FOUR = { numerator: 4, denominator: 4 };
|
|
717
|
-
var BASS_LICKS =
|
|
983
|
+
var BASS_LICKS = deepFreeze([
|
|
718
984
|
{
|
|
719
985
|
id: "motownWalkDown",
|
|
720
986
|
genre: "motown",
|
|
@@ -759,7 +1025,7 @@ var BASS_LICKS = Object.freeze([
|
|
|
759
1025
|
id: "funkSixteenthPop",
|
|
760
1026
|
genre: "funk",
|
|
761
1027
|
difficulty: 4,
|
|
762
|
-
articulations: ["mute"
|
|
1028
|
+
articulations: ["mute"],
|
|
763
1029
|
ts: FOUR_FOUR,
|
|
764
1030
|
tempoRange: [84, 124],
|
|
765
1031
|
material: {
|
|
@@ -842,7 +1108,7 @@ var BASS_LICKS = Object.freeze([
|
|
|
842
1108
|
id: "gospelPassing",
|
|
843
1109
|
genre: "gospel",
|
|
844
1110
|
difficulty: 4,
|
|
845
|
-
articulations: [
|
|
1111
|
+
articulations: [],
|
|
846
1112
|
ts: FOUR_FOUR,
|
|
847
1113
|
tempoRange: [60, 108],
|
|
848
1114
|
material: {
|
|
@@ -883,7 +1149,7 @@ var BASS_LICKS = Object.freeze([
|
|
|
883
1149
|
id: "reggaeOffbeatDrop",
|
|
884
1150
|
genre: "reggae",
|
|
885
1151
|
difficulty: 3,
|
|
886
|
-
articulations: [
|
|
1152
|
+
articulations: [],
|
|
887
1153
|
ts: FOUR_FOUR,
|
|
888
1154
|
tempoRange: [64, 104],
|
|
889
1155
|
material: {
|
|
@@ -902,103 +1168,61 @@ var BASS_LICKS = Object.freeze([
|
|
|
902
1168
|
}
|
|
903
1169
|
}
|
|
904
1170
|
]);
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
const octaves = Math.floor((degree - 1) / 7);
|
|
908
|
-
const within = (degree - 1) % 7 + 1;
|
|
909
|
-
const fromChord = chordDegreeSemitone(within, chord);
|
|
910
|
-
const template = MAJOR_DEGREE_SEMITONES[within - 1] ?? 0;
|
|
911
|
-
const semitone = fromChord ?? // A degree the chord does not name is a passing tone, so the key decides it.
|
|
912
|
-
nearestScaleTone(chord.rootPc + template, key) - chord.rootPc;
|
|
913
|
-
return pitchClassOf(semitone) + 12 * octaves + alter;
|
|
914
|
-
}
|
|
915
|
-
function chordDegreeSemitone(degree, chord) {
|
|
916
|
-
for (const interval of chord.intervals) {
|
|
917
|
-
if (degreeOfInterval(interval) === degree) {
|
|
918
|
-
return interval;
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
return void 0;
|
|
922
|
-
}
|
|
923
|
-
function degreeOfInterval(interval) {
|
|
924
|
-
const within = (interval % 12 + 12) % 12;
|
|
925
|
-
if (within === 0) {
|
|
926
|
-
return 1;
|
|
927
|
-
}
|
|
928
|
-
if (within === 1 || within === 2) {
|
|
929
|
-
return 2;
|
|
930
|
-
}
|
|
931
|
-
if (within === 3 || within === 4) {
|
|
932
|
-
return 3;
|
|
933
|
-
}
|
|
934
|
-
if (within === 5) {
|
|
935
|
-
return 4;
|
|
936
|
-
}
|
|
937
|
-
if (within === 6 || within === 7 || within === 8) {
|
|
938
|
-
return 5;
|
|
939
|
-
}
|
|
940
|
-
if (within === 9) {
|
|
941
|
-
return 6;
|
|
942
|
-
}
|
|
943
|
-
return 7;
|
|
944
|
-
}
|
|
945
|
-
var DEFAULT_TS = FOUR_FOUR;
|
|
946
|
-
var DEFAULT_OCTAVE = 2;
|
|
1171
|
+
|
|
1172
|
+
// src/generate/bass/licks.ts
|
|
947
1173
|
var DEFAULT_BPM = 120;
|
|
948
1174
|
var DEFAULT_LICK_DENSITY = 0.6;
|
|
949
|
-
var EPS2 = 1e-9;
|
|
950
1175
|
function placeLicks(timeline, key, opts) {
|
|
951
|
-
|
|
952
|
-
const
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1176
|
+
const asked = assertRecord(opts, "opts");
|
|
1177
|
+
const placement = assertArray(
|
|
1178
|
+
timeline,
|
|
1179
|
+
"timeline"
|
|
1180
|
+
);
|
|
1181
|
+
assertGenerationBudget(placement.length, "lick segments", asked.budget);
|
|
1182
|
+
const genre = assertOneOf(asked.genre, GENRES, "lick genre");
|
|
1183
|
+
const ts = meterAt(0, toMeterData(asked.ts ?? DEFAULT_TS, "ts"));
|
|
1184
|
+
const octave = asked.octave ?? DEFAULT_OCTAVE;
|
|
956
1185
|
assertInteger(octave, "lick octave", -1, 8);
|
|
957
1186
|
const scale = toKeyScale(key);
|
|
958
|
-
const segments =
|
|
959
|
-
|
|
960
|
-
assertRange(segment.startBeat, 0, Number.MAX_SAFE_INTEGER, "lick segment startBeat");
|
|
961
|
-
if (segment.endBeat <= segment.startBeat) {
|
|
962
|
-
throw new InvalidInputError(
|
|
963
|
-
`lick segment at beat ${segment.startBeat} must have a positive duration`
|
|
964
|
-
);
|
|
965
|
-
}
|
|
966
|
-
}
|
|
1187
|
+
const segments = placement.map((segment) => ({ ...segment, chord: toChordData(segment.chord) })).sort((a, b) => a.startBeat - b.startBeat);
|
|
1188
|
+
assertBassSegments(segments);
|
|
967
1189
|
if (segments.length === 0) {
|
|
968
1190
|
return [];
|
|
969
1191
|
}
|
|
970
|
-
const
|
|
1192
|
+
const barBeats = beatsPerBar(ts);
|
|
1193
|
+
const estimatedBars = segments.reduce(
|
|
1194
|
+
(count, segment) => count + Math.max(1, Math.ceil((segment.endBeat - segment.startBeat) / barBeats)),
|
|
1195
|
+
0
|
|
1196
|
+
);
|
|
1197
|
+
assertGenerationBudget(estimatedBars, "lick bars", asked.budget);
|
|
1198
|
+
const resolved = resolveContext(asked.ctx);
|
|
971
1199
|
const draw = resolved.part("bass");
|
|
972
1200
|
const bpm = resolved.bpm ?? DEFAULT_BPM;
|
|
973
1201
|
const density = resolved.rhythmic ?? DEFAULT_LICK_DENSITY;
|
|
974
|
-
const
|
|
975
|
-
const
|
|
976
|
-
const instrument =
|
|
977
|
-
const low = bandFloor(octave * 12 + 12, instrument);
|
|
1202
|
+
const supplied = asked.difficulty === void 0 ? void 0 : assertDifficulty(asked.difficulty, "lick difficulty");
|
|
1203
|
+
const difficulty = resolved.difficulty ?? supplied;
|
|
1204
|
+
const { instrument, low } = bassRegister(resolved.instrument("bass"), asked.instrument, octave);
|
|
978
1205
|
const dictionary = mergeVocabulary(
|
|
979
1206
|
BASS_LICKS,
|
|
980
1207
|
vocabularyOfKind(resolved.vocabulary, isLickMaterial)
|
|
981
1208
|
);
|
|
982
1209
|
const playable = instrument === void 0 ? {} : { articulations: instrument.articulations };
|
|
983
|
-
const barBeats = beatsPerBar(ts);
|
|
984
1210
|
const raw = [];
|
|
985
|
-
let anchor = low;
|
|
986
|
-
let rootAnchor = low;
|
|
987
1211
|
for (let index = 0; index < segments.length; index += 1) {
|
|
988
1212
|
const segment = segments[index];
|
|
989
1213
|
if (!segment) {
|
|
990
1214
|
continue;
|
|
991
1215
|
}
|
|
992
1216
|
const rootPc = bassPcOf(segment.chord);
|
|
993
|
-
const
|
|
1217
|
+
const rootMidi = placeRoot(rootPc, low);
|
|
1218
|
+
const implied = impliedScaleTones(segment.chord);
|
|
994
1219
|
let lastTilePlayed = false;
|
|
1220
|
+
let playedTile;
|
|
995
1221
|
let firstOnset = segment.startBeat;
|
|
996
|
-
|
|
997
|
-
|
|
1222
|
+
const tiles = barTiles(segment.startBeat, segment.endBeat, ts);
|
|
1223
|
+
for (let tile = 0; tile < tiles.length; tile += 1) {
|
|
1224
|
+
const tileStart = tiles[tile]?.startBeat ?? segment.startBeat;
|
|
998
1225
|
const remaining = segment.endBeat - tileStart;
|
|
999
|
-
if (remaining <= EPS2) {
|
|
1000
|
-
break;
|
|
1001
|
-
}
|
|
1002
1226
|
const entry = pickVocabulary(
|
|
1003
1227
|
dictionary,
|
|
1004
1228
|
{ genre, bpm, ts, difficulty, quality: segment.chord.quality, ...playable },
|
|
@@ -1016,28 +1240,34 @@ function placeLicks(timeline, key, opts) {
|
|
|
1016
1240
|
draw,
|
|
1017
1241
|
[index, tile],
|
|
1018
1242
|
bpm,
|
|
1019
|
-
difficulty
|
|
1243
|
+
difficulty,
|
|
1244
|
+
ts
|
|
1020
1245
|
) : void 0;
|
|
1021
1246
|
lastTilePlayed = notes !== void 0;
|
|
1247
|
+
if (notes !== void 0 && entry !== void 0) {
|
|
1248
|
+
playedTile = { material: entry.material, tileStart };
|
|
1249
|
+
}
|
|
1022
1250
|
const rootStep = entry === void 0 ? 0 : anchorStepOf(entry.material, remaining);
|
|
1023
1251
|
const rootAt = tileStart + rootStep * STEP_BEATS;
|
|
1024
1252
|
if (tile === 0) {
|
|
1025
1253
|
firstOnset = rootAt;
|
|
1026
1254
|
}
|
|
1027
|
-
|
|
1028
|
-
rootAnchor = placePc(rootPc, rootAnchor, low);
|
|
1029
|
-
anchor = rootAnchor;
|
|
1030
|
-
raw.push({ startBeat: rootAt, pitch: rootAnchor, velocity: STRONG_VELOCITY });
|
|
1255
|
+
raw.push({ startBeat: rootAt, pitch: rootMidi, velocity: STRONG_VELOCITY });
|
|
1031
1256
|
if (notes) {
|
|
1032
|
-
const figureRoot = placePc(pitchClassOf(segment.chord.rootPc),
|
|
1257
|
+
const figureRoot = segment.chord.bassPc === void 0 ? placePc(pitchClassOf(segment.chord.rootPc), rootMidi, low) : placeAboveBass(pitchClassOf(segment.chord.rootPc), rootMidi);
|
|
1033
1258
|
for (const lickNote of notes) {
|
|
1034
1259
|
const at2 = tileStart + lickNote.step * STEP_BEATS;
|
|
1035
|
-
const offset = degreeSemitone(
|
|
1036
|
-
|
|
1037
|
-
|
|
1260
|
+
const offset = degreeSemitone(
|
|
1261
|
+
lickNote.degree,
|
|
1262
|
+
lickNote.alter ?? 0,
|
|
1263
|
+
segment.chord,
|
|
1264
|
+
scale,
|
|
1265
|
+
implied
|
|
1266
|
+
);
|
|
1267
|
+
const pitch = Math.abs(at2 - rootAt) < BEAT_EPS && lickNote.degree === 1 && (lickNote.alter ?? 0) === 0 ? rootMidi : figureRoot + offset;
|
|
1038
1268
|
raw.push({
|
|
1039
1269
|
startBeat: at2,
|
|
1040
|
-
pitch
|
|
1270
|
+
pitch,
|
|
1041
1271
|
velocity: Math.round(
|
|
1042
1272
|
(isStrongBeat(at2, ts) ? STRONG_VELOCITY : WEAK_VELOCITY) * lickNote.velocity
|
|
1043
1273
|
),
|
|
@@ -1048,19 +1278,21 @@ function placeLicks(timeline, key, opts) {
|
|
|
1048
1278
|
}
|
|
1049
1279
|
const next = segments[index + 1];
|
|
1050
1280
|
if (next && lastTilePlayed) {
|
|
1051
|
-
const
|
|
1052
|
-
const
|
|
1053
|
-
if (approachAt > firstOnset +
|
|
1054
|
-
const nextRoot =
|
|
1055
|
-
const
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
scale,
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1281
|
+
const lastBeat = next.startBeat - STEP_BEATS * BEAT_STEPS;
|
|
1282
|
+
const approachAt = materialOnsetIn(playedTile, lastBeat, next.startBeat) ?? lastBeat;
|
|
1283
|
+
if (approachAt > firstOnset + BEAT_EPS) {
|
|
1284
|
+
const nextRoot = placeRoot(bassPcOf(next.chord), low);
|
|
1285
|
+
const occupied = soundingAt(raw, approachAt);
|
|
1286
|
+
const distance = occupied === void 0 ? 0 : Math.abs(occupied.pitch - nextRoot);
|
|
1287
|
+
if (occupied === void 0 || distance < 1 || distance > 2) {
|
|
1288
|
+
const from = lastPitchBefore(raw, approachAt, rootMidi);
|
|
1289
|
+
const midi = approachNote(nextRoot, from, scale, draw.prob(0.5, "lickApproach", index));
|
|
1290
|
+
if (occupied === void 0) {
|
|
1291
|
+
raw.push({ startBeat: approachAt, pitch: midi, velocity: WEAK_VELOCITY });
|
|
1292
|
+
} else {
|
|
1293
|
+
occupied.pitch = midi;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1064
1296
|
}
|
|
1065
1297
|
}
|
|
1066
1298
|
}
|
|
@@ -1078,7 +1310,7 @@ function placeLicks(timeline, key, opts) {
|
|
|
1078
1310
|
nextStart - item.startBeat,
|
|
1079
1311
|
written === void 0 ? Number.POSITIVE_INFINITY : written * STEP_BEATS
|
|
1080
1312
|
);
|
|
1081
|
-
if (durationBeat <=
|
|
1313
|
+
if (durationBeat <= BEAT_EPS) {
|
|
1082
1314
|
continue;
|
|
1083
1315
|
}
|
|
1084
1316
|
const placed = instrument ? foldIntoRange(item.pitch, instrument) : item.pitch;
|
|
@@ -1095,6 +1327,38 @@ function placeLicks(timeline, key, opts) {
|
|
|
1095
1327
|
}
|
|
1096
1328
|
return out;
|
|
1097
1329
|
}
|
|
1330
|
+
function soundingAt(raw, at2) {
|
|
1331
|
+
for (let index = raw.length - 1; index >= 0; index -= 1) {
|
|
1332
|
+
const item = raw[index];
|
|
1333
|
+
if (item !== void 0 && Math.abs(item.startBeat - at2) < BEAT_EPS) {
|
|
1334
|
+
return item;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
return void 0;
|
|
1338
|
+
}
|
|
1339
|
+
function materialOnsetIn(played, from, to) {
|
|
1340
|
+
if (played === void 0) {
|
|
1341
|
+
return void 0;
|
|
1342
|
+
}
|
|
1343
|
+
let last;
|
|
1344
|
+
for (const note2 of played.material.notes) {
|
|
1345
|
+
const at2 = played.tileStart + note2.step * STEP_BEATS;
|
|
1346
|
+
if (at2 < from - BEAT_EPS || at2 > to - BEAT_EPS) {
|
|
1347
|
+
continue;
|
|
1348
|
+
}
|
|
1349
|
+
last = last === void 0 ? at2 : Math.max(last, at2);
|
|
1350
|
+
}
|
|
1351
|
+
return last;
|
|
1352
|
+
}
|
|
1353
|
+
function lastPitchBefore(raw, at2, fallback) {
|
|
1354
|
+
for (let index = raw.length - 1; index >= 0; index -= 1) {
|
|
1355
|
+
const item = raw[index];
|
|
1356
|
+
if (item !== void 0 && item.startBeat < at2 - BEAT_EPS) {
|
|
1357
|
+
return item.pitch;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
return fallback;
|
|
1361
|
+
}
|
|
1098
1362
|
function anchorStepOf(material, spanBeats) {
|
|
1099
1363
|
let first;
|
|
1100
1364
|
for (const note2 of material.notes) {
|
|
@@ -1102,15 +1366,19 @@ function anchorStepOf(material, spanBeats) {
|
|
|
1102
1366
|
first = note2.step;
|
|
1103
1367
|
}
|
|
1104
1368
|
}
|
|
1105
|
-
if (first === void 0 || first < 0 || first * STEP_BEATS >= spanBeats -
|
|
1369
|
+
if (first === void 0 || first < 0 || first * STEP_BEATS >= spanBeats - BEAT_EPS) {
|
|
1106
1370
|
return 0;
|
|
1107
1371
|
}
|
|
1108
1372
|
return first;
|
|
1109
1373
|
}
|
|
1110
|
-
function fitToSegment(material, spanBeats, density, resolved, draw, path, bpm, difficulty) {
|
|
1374
|
+
function fitToSegment(material, spanBeats, density, resolved, draw, path, bpm, difficulty, ts) {
|
|
1111
1375
|
const deformed = deform(
|
|
1112
1376
|
material.notes,
|
|
1113
1377
|
{
|
|
1378
|
+
// The figure is thinned against the bar it is played in, not against a
|
|
1379
|
+
// four-four ladder: a downbeat of the meter in force must outrank an
|
|
1380
|
+
// offbeat of some other meter.
|
|
1381
|
+
ts,
|
|
1114
1382
|
// The one density the whole generator reads, defaults included: passing
|
|
1115
1383
|
// the documented default explicitly and leaving it out have to be the
|
|
1116
1384
|
// same request, and they were not while the figure was deformed by the
|
|
@@ -1125,7 +1393,7 @@ function fitToSegment(material, spanBeats, density, resolved, draw, path, bpm, d
|
|
|
1125
1393
|
...path
|
|
1126
1394
|
);
|
|
1127
1395
|
const spanSteps = spanBeats / STEP_BEATS;
|
|
1128
|
-
const inside = deformed.filter((lickNote) => lickNote.step < spanSteps -
|
|
1396
|
+
const inside = deformed.filter((lickNote) => lickNote.step < spanSteps - BEAT_EPS);
|
|
1129
1397
|
if (inside.length === 0 || !withinCeiling(inside, bpm, difficulty)) {
|
|
1130
1398
|
return void 0;
|
|
1131
1399
|
}
|
|
@@ -1140,28 +1408,32 @@ var BASS_STYLES = Object.freeze([
|
|
|
1140
1408
|
"walking",
|
|
1141
1409
|
"arpeggio"
|
|
1142
1410
|
]);
|
|
1143
|
-
var DEFAULT_TS2 = { numerator: 4, denominator: 4 };
|
|
1144
1411
|
var DEFAULT_STYLE = "root";
|
|
1145
|
-
var DEFAULT_OCTAVE2 = 2;
|
|
1146
1412
|
var DEFAULT_PICKUP_DENSITY = 0.35;
|
|
1413
|
+
function sounding(ctx, placed) {
|
|
1414
|
+
return ctx.instrument ? foldIntoRange(placed, ctx.instrument) : placed;
|
|
1415
|
+
}
|
|
1416
|
+
function placeTone(ctx, pc) {
|
|
1417
|
+
return ctx.overBass ? placeAboveBass(pc, ctx.rootMidi) : placePc(pc, ctx.rootMidi, ctx.low);
|
|
1418
|
+
}
|
|
1147
1419
|
function emit(ctx, pos, pc, midiOverride) {
|
|
1148
|
-
const placed = clampToMidi(
|
|
1149
|
-
|
|
1150
|
-
"generated bass pitch"
|
|
1151
|
-
);
|
|
1152
|
-
const midi = ctx.instrument ? foldIntoRange(placed, ctx.instrument) : placed;
|
|
1420
|
+
const placed = clampToMidi(midiOverride ?? placeTone(ctx, pc), "generated bass pitch");
|
|
1421
|
+
const midi = sounding(ctx, placed);
|
|
1153
1422
|
const velocity = isStrongBeat(pos, ctx.ts) ? STRONG_VELOCITY : WEAK_VELOCITY;
|
|
1154
1423
|
ctx.notes.push({ startBeat: pos, pitch: midi, velocity });
|
|
1424
|
+
ctx.prevMidi = midi;
|
|
1155
1425
|
return midi;
|
|
1156
1426
|
}
|
|
1157
1427
|
function buildRoot(ctx, seg) {
|
|
1158
|
-
|
|
1428
|
+
emit(ctx, seg.startBeat, bassPcOf(seg.chord));
|
|
1159
1429
|
}
|
|
1160
1430
|
function buildRootFifth(ctx, seg) {
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1431
|
+
for (const tile of barTiles(seg.startBeat, seg.endBeat, ctx.ts)) {
|
|
1432
|
+
emit(ctx, tile.startBeat, bassPcOf(seg.chord));
|
|
1433
|
+
const answer = midBarPulse(tile.startBeat, ctx.ts);
|
|
1434
|
+
if (answer > tile.startBeat + BEAT_EPS && answer < tile.endBeat - BEAT_EPS) {
|
|
1435
|
+
emit(ctx, answer, fifthPcOf2(seg.chord));
|
|
1436
|
+
}
|
|
1165
1437
|
}
|
|
1166
1438
|
}
|
|
1167
1439
|
function buildPop(ctx, seg, index) {
|
|
@@ -1170,21 +1442,37 @@ function buildPop(ctx, seg, index) {
|
|
|
1170
1442
|
let emitted = false;
|
|
1171
1443
|
for (const pos of positions) {
|
|
1172
1444
|
if (isStrongBeat(pos, ctx.ts)) {
|
|
1173
|
-
|
|
1445
|
+
emit(ctx, pos, rootPc);
|
|
1174
1446
|
emitted = true;
|
|
1175
1447
|
} else if (ctx.draw.prob(ctx.pickupDensity, "pickup", index, pos)) {
|
|
1176
|
-
|
|
1177
|
-
const base = placePc(rootPc, ctx.prevMidi, ctx.low);
|
|
1178
|
-
const dropped = base - 12;
|
|
1179
|
-
emit(ctx, pos, rootPc, dropped >= 0 ? dropped : base);
|
|
1180
|
-
} else {
|
|
1181
|
-
ctx.prevMidi = emit(ctx, pos, fifthPcOf(seg.chord));
|
|
1182
|
-
}
|
|
1448
|
+
emitted = pickup(ctx, seg, pos, index) || emitted;
|
|
1183
1449
|
}
|
|
1184
1450
|
}
|
|
1185
1451
|
if (!emitted) {
|
|
1186
|
-
|
|
1452
|
+
emit(ctx, seg.startBeat, rootPc);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
function pickup(ctx, seg, pos, index) {
|
|
1456
|
+
const octave = octavePickup(ctx);
|
|
1457
|
+
if (ctx.draw.prob(0.5, "pickupKind", index, pos) && octave !== void 0 && reachableLeap(ctx, pos, Math.abs(sounding(ctx, octave) - ctx.prevMidi))) {
|
|
1458
|
+
emit(ctx, pos, bassPcOf(seg.chord), octave);
|
|
1459
|
+
return true;
|
|
1460
|
+
}
|
|
1461
|
+
const fifthPc = fifthPcOf2(seg.chord);
|
|
1462
|
+
if (sounding(ctx, placeTone(ctx, fifthPc)) === sounding(ctx, ctx.rootMidi)) {
|
|
1463
|
+
return false;
|
|
1464
|
+
}
|
|
1465
|
+
emit(ctx, pos, fifthPc);
|
|
1466
|
+
return true;
|
|
1467
|
+
}
|
|
1468
|
+
function octavePickup(ctx) {
|
|
1469
|
+
const root = sounding(ctx, ctx.rootMidi);
|
|
1470
|
+
for (const candidate of [ctx.rootMidi - 12, ctx.rootMidi + 12]) {
|
|
1471
|
+
if (candidate >= 0 && candidate <= 127 && sounding(ctx, candidate) !== root) {
|
|
1472
|
+
return candidate;
|
|
1473
|
+
}
|
|
1187
1474
|
}
|
|
1475
|
+
return void 0;
|
|
1188
1476
|
}
|
|
1189
1477
|
function reachableLeap(ctx, pos, semitones) {
|
|
1190
1478
|
const previous = ctx.notes[ctx.notes.length - 1];
|
|
@@ -1192,63 +1480,47 @@ function reachableLeap(ctx, pos, semitones) {
|
|
|
1192
1480
|
return sustainsShift(semitones, available, ctx.bpm, ctx.difficulty);
|
|
1193
1481
|
}
|
|
1194
1482
|
function buildArpeggio(ctx, seg) {
|
|
1195
|
-
const tones =
|
|
1483
|
+
const tones = bassToneCycle(seg.chord);
|
|
1196
1484
|
const positions = beatPositions(seg.startBeat, seg.endBeat, ctx.ts);
|
|
1197
1485
|
positions.forEach((pos, i) => {
|
|
1198
1486
|
const pc = tones[i % tones.length] ?? bassPcOf(seg.chord);
|
|
1199
|
-
|
|
1487
|
+
emit(ctx, pos, pc);
|
|
1200
1488
|
});
|
|
1201
1489
|
}
|
|
1202
1490
|
function approachNote2(ctx, target, from, index) {
|
|
1203
|
-
return approachNote(target, from, ctx.
|
|
1491
|
+
return approachNote(target, from, ctx.key, ctx.draw.prob(0.5, "approach", index));
|
|
1204
1492
|
}
|
|
1205
1493
|
function buildWalking(ctx, seg, next, index) {
|
|
1206
|
-
const tones =
|
|
1494
|
+
const tones = bassToneCycle(seg.chord);
|
|
1207
1495
|
const positions = beatPositions(seg.startBeat, seg.endBeat, ctx.ts);
|
|
1208
1496
|
const count = positions.length;
|
|
1209
1497
|
positions.forEach((pos, i) => {
|
|
1210
1498
|
if (i === 0) {
|
|
1211
|
-
|
|
1499
|
+
emit(ctx, pos, bassPcOf(seg.chord));
|
|
1212
1500
|
return;
|
|
1213
1501
|
}
|
|
1214
1502
|
if (next && count > 1 && i === count - 1) {
|
|
1215
|
-
const nextBass =
|
|
1503
|
+
const nextBass = placeRoot(bassPcOf(next.chord), ctx.low);
|
|
1216
1504
|
const midi = approachNote2(ctx, nextBass, ctx.prevMidi, index);
|
|
1217
|
-
|
|
1505
|
+
emit(ctx, pos, pitchClassOf(midi), midi);
|
|
1218
1506
|
return;
|
|
1219
1507
|
}
|
|
1220
1508
|
const pc = tones[i % tones.length] ?? bassPcOf(seg.chord);
|
|
1221
|
-
|
|
1509
|
+
emit(ctx, pos, pc);
|
|
1222
1510
|
});
|
|
1223
1511
|
}
|
|
1224
1512
|
function generateBassLine(opts) {
|
|
1513
|
+
assertRecord(opts, "bass line options");
|
|
1514
|
+
assertArray(opts.segments, "bass segments");
|
|
1225
1515
|
assertGenerationBudget(opts.segments.length, "bass segments", opts.budget);
|
|
1226
|
-
for (let index = 0; index < opts.segments.length; index += 1) {
|
|
1227
|
-
const segment = opts.segments[index];
|
|
1228
|
-
if (!segment) continue;
|
|
1229
|
-
assertRange(segment.startBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].startBeat`);
|
|
1230
|
-
assertRange(segment.endBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].endBeat`);
|
|
1231
|
-
if (segment.endBeat <= segment.startBeat) {
|
|
1232
|
-
throw new InvalidInputError(`segments[${index}] must have a positive duration`);
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
1516
|
const segments = opts.segments.map((segment) => ({ ...segment, chord: toChordData(segment.chord) })).sort((a, b) => a.startBeat - b.startBeat);
|
|
1236
|
-
|
|
1237
|
-
const previous = segments[index - 1];
|
|
1238
|
-
const current = segments[index];
|
|
1239
|
-
if (previous !== void 0 && current !== void 0 && current.startBeat < previous.endBeat) {
|
|
1240
|
-
throw new InvalidInputError(
|
|
1241
|
-
`bass segments must not overlap: segments ${index - 1} and ${index} overlap`
|
|
1242
|
-
);
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1517
|
+
assertBassSegments(segments);
|
|
1245
1518
|
if (segments.length === 0) {
|
|
1246
1519
|
return [];
|
|
1247
1520
|
}
|
|
1248
|
-
const ts = opts.ts ??
|
|
1249
|
-
assertTimeSignature(ts);
|
|
1521
|
+
const ts = meterAt(0, toMeterData(opts.ts ?? DEFAULT_TS, "ts"));
|
|
1250
1522
|
const style = assertOneOf(opts.style ?? DEFAULT_STYLE, BASS_STYLES, "bass style");
|
|
1251
|
-
const octave = opts.octave ??
|
|
1523
|
+
const octave = opts.octave ?? DEFAULT_OCTAVE;
|
|
1252
1524
|
assertInteger(octave, "bass octave", -1, 8);
|
|
1253
1525
|
const estimatedNotes = segments.reduce(
|
|
1254
1526
|
(count, segment) => count + Math.max(1, Math.ceil(segment.endBeat - segment.startBeat)),
|
|
@@ -1256,15 +1528,15 @@ function generateBassLine(opts) {
|
|
|
1256
1528
|
);
|
|
1257
1529
|
assertGenerationBudget(estimatedNotes, "bass notes", opts.budget);
|
|
1258
1530
|
const resolved = resolveContext(opts.ctx);
|
|
1259
|
-
const
|
|
1260
|
-
const instrument = opts.instrument ?? (named !== void 0 && named.kind === "stringed" ? named : void 0);
|
|
1261
|
-
const low = bandFloor(octave * 12 + 12, instrument);
|
|
1531
|
+
const { instrument, low } = bassRegister(resolved.instrument("bass"), opts.instrument, octave);
|
|
1262
1532
|
const ctx = {
|
|
1263
1533
|
ts,
|
|
1264
1534
|
low,
|
|
1265
1535
|
key: toKeyScale(opts.key),
|
|
1266
1536
|
draw: resolved.part("bass"),
|
|
1267
1537
|
notes: [],
|
|
1538
|
+
rootMidi: low,
|
|
1539
|
+
overBass: false,
|
|
1268
1540
|
prevMidi: low,
|
|
1269
1541
|
instrument,
|
|
1270
1542
|
pickupDensity: resolved.rhythmic ?? DEFAULT_PICKUP_DENSITY,
|
|
@@ -1276,6 +1548,8 @@ function generateBassLine(opts) {
|
|
|
1276
1548
|
if (!seg) {
|
|
1277
1549
|
continue;
|
|
1278
1550
|
}
|
|
1551
|
+
ctx.rootMidi = placeRoot(bassPcOf(seg.chord), ctx.low);
|
|
1552
|
+
ctx.overBass = seg.chord.bassPc !== void 0;
|
|
1279
1553
|
switch (style) {
|
|
1280
1554
|
case "rootFifth":
|
|
1281
1555
|
buildRootFifth(ctx, seg);
|
|
@@ -1304,7 +1578,7 @@ function generateBassLine(opts) {
|
|
|
1304
1578
|
}
|
|
1305
1579
|
const nextStart = ctx.notes[i + 1]?.startBeat ?? lastEnd;
|
|
1306
1580
|
const durationBeat = nextStart - note2.startBeat;
|
|
1307
|
-
if (durationBeat <=
|
|
1581
|
+
if (durationBeat <= BEAT_EPS) {
|
|
1308
1582
|
continue;
|
|
1309
1583
|
}
|
|
1310
1584
|
out.push({
|
|
@@ -1331,6 +1605,7 @@ function assertPlayable(pitch) {
|
|
|
1331
1605
|
}
|
|
1332
1606
|
function imitate(lead, opts) {
|
|
1333
1607
|
assertNoteEvents(lead, "imitate lead", { allowNonPositiveDuration: true });
|
|
1608
|
+
assertRecord(opts, "opts");
|
|
1334
1609
|
assertFiniteNumber(opts.atBeat, "atBeat");
|
|
1335
1610
|
const interval = toSpelledInterval(opts.interval);
|
|
1336
1611
|
const key = toKeyScale(opts.key);
|
|
@@ -1371,9 +1646,9 @@ function imitate(lead, opts) {
|
|
|
1371
1646
|
}
|
|
1372
1647
|
pitch = tonal ? shiftByScaleDegrees(pitch, degrees, key) + offset : pitch + Math.round(interval.semitones);
|
|
1373
1648
|
const answer = {
|
|
1649
|
+
...note2,
|
|
1374
1650
|
pitch: assertPlayable(pitch),
|
|
1375
|
-
startBeat: note2.startBeat + shift
|
|
1376
|
-
durationBeat: note2.durationBeat
|
|
1651
|
+
startBeat: note2.startBeat + shift
|
|
1377
1652
|
};
|
|
1378
1653
|
if (note2.velocity !== void 0) {
|
|
1379
1654
|
answer.velocity = Math.max(1, Math.min(127, Math.round(note2.velocity * velocityScale)));
|
|
@@ -1383,8 +1658,6 @@ function imitate(lead, opts) {
|
|
|
1383
1658
|
}
|
|
1384
1659
|
|
|
1385
1660
|
// src/generate/countermelody/index.ts
|
|
1386
|
-
var DEFAULT_TS3 = { numerator: 4, denominator: 4 };
|
|
1387
|
-
var EPS3 = 1e-9;
|
|
1388
1661
|
var GRID_STEP = 0.5;
|
|
1389
1662
|
var MIN_ONSET_GAP = 1;
|
|
1390
1663
|
var REGISTER_OFFSET = 12;
|
|
@@ -1396,7 +1669,7 @@ var DEFAULT_MELODY_VELOCITY = 96;
|
|
|
1396
1669
|
var VELOCITY_DROP = 16;
|
|
1397
1670
|
var TIE_BREAK_JITTER = 1e-6;
|
|
1398
1671
|
function isWholeBeat(beat) {
|
|
1399
|
-
return Math.abs(beat - Math.round(beat)) <
|
|
1672
|
+
return Math.abs(beat - Math.round(beat)) < BEAT_EPS;
|
|
1400
1673
|
}
|
|
1401
1674
|
function melodyNoteAt(melody, beat) {
|
|
1402
1675
|
return melody.at(beat)?.note;
|
|
@@ -1409,8 +1682,8 @@ function melodySnapshotAt(melody, beat) {
|
|
|
1409
1682
|
if (current === void 0) {
|
|
1410
1683
|
return void 0;
|
|
1411
1684
|
}
|
|
1412
|
-
const attacked = Math.abs(current.startBeat - beat) <
|
|
1413
|
-
const previous = attacked ? melodyNoteAt(melody, beat -
|
|
1685
|
+
const attacked = Math.abs(current.startBeat - beat) < BEAT_EPS;
|
|
1686
|
+
const previous = attacked ? melodyNoteAt(melody, beat - BEAT_EPS * 2) : current;
|
|
1414
1687
|
const snapshot = { pitch: current.pitch };
|
|
1415
1688
|
if (previous !== void 0) {
|
|
1416
1689
|
snapshot.prevPitch = previous.pitch;
|
|
@@ -1420,22 +1693,24 @@ function melodySnapshotAt(melody, beat) {
|
|
|
1420
1693
|
function followOnsets(melody) {
|
|
1421
1694
|
return [...new Set(melody.notes.map(({ note: note2 }) => note2.startBeat))];
|
|
1422
1695
|
}
|
|
1696
|
+
var DEFAULT_TS2 = { numerator: 4, denominator: 4 };
|
|
1697
|
+
var SCORE_EPS = 1e-9;
|
|
1423
1698
|
function complementOnsets(melody, ts, spanStart, spanEnd, draw) {
|
|
1424
1699
|
const onsets = [];
|
|
1425
1700
|
const gridStart = Math.floor(spanStart / GRID_STEP) * GRID_STEP;
|
|
1426
1701
|
let last = Number.NEGATIVE_INFINITY;
|
|
1427
|
-
for (let beat = gridStart; beat < spanEnd -
|
|
1702
|
+
for (let beat = gridStart; beat < spanEnd - BEAT_EPS; beat += GRID_STEP) {
|
|
1428
1703
|
const attacked = melodyAttacksAt(melody, beat);
|
|
1429
|
-
const
|
|
1704
|
+
const sounding2 = melodyNoteAt(melody, beat) !== void 0;
|
|
1430
1705
|
let place = false;
|
|
1431
|
-
if (!attacked && !
|
|
1706
|
+
if (!attacked && !sounding2) {
|
|
1432
1707
|
place = isWholeBeat(beat);
|
|
1433
1708
|
} else if (!attacked) {
|
|
1434
1709
|
place = draw.prob(isWholeBeat(beat) ? HOLD_ON_BEAT_PROB : HOLD_OFF_BEAT_PROB, "hold", beat);
|
|
1435
1710
|
} else if (isStrongBeat(beat, ts)) {
|
|
1436
1711
|
place = draw.prob(REINFORCE_PROB, "reinforce", beat);
|
|
1437
1712
|
}
|
|
1438
|
-
if (place && beat - last >= MIN_ONSET_GAP -
|
|
1713
|
+
if (place && beat - last >= MIN_ONSET_GAP - BEAT_EPS) {
|
|
1439
1714
|
onsets.push(beat);
|
|
1440
1715
|
last = beat;
|
|
1441
1716
|
}
|
|
@@ -1451,6 +1726,19 @@ function keepsImperfectInterval(before, now) {
|
|
|
1451
1726
|
}
|
|
1452
1727
|
return before <= 4 === now <= 4;
|
|
1453
1728
|
}
|
|
1729
|
+
function assertWeightOverrides(overrides) {
|
|
1730
|
+
if (overrides === void 0) {
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
if (typeof overrides !== "object" || overrides === null) {
|
|
1734
|
+
throw new InvalidInputError(
|
|
1735
|
+
`countermelody weights must be an object of weight overrides; received ${describeRejected(overrides)}`
|
|
1736
|
+
);
|
|
1737
|
+
}
|
|
1738
|
+
for (const [field, value] of Object.entries(overrides)) {
|
|
1739
|
+
assertFiniteNumber(value, `countermelody weights.${field}`);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1454
1742
|
function scoreCandidate(pitch, melPitch, melPrev, prevPitch, chord, center, weights) {
|
|
1455
1743
|
let score = 0;
|
|
1456
1744
|
const ic = melPitch === void 0 ? void 0 : Math.abs(pitch - melPitch) % 12;
|
|
@@ -1498,12 +1786,12 @@ function heldNoteBoundaries(melody, startBeat, endBeat, chordChangeBeats) {
|
|
|
1498
1786
|
boundaries.add(onset);
|
|
1499
1787
|
}
|
|
1500
1788
|
for (const beat of chordChangeBeats) {
|
|
1501
|
-
if (beat > startBeat +
|
|
1789
|
+
if (beat > startBeat + BEAT_EPS && beat < endBeat - BEAT_EPS) {
|
|
1502
1790
|
boundaries.add(beat);
|
|
1503
1791
|
}
|
|
1504
1792
|
}
|
|
1505
1793
|
const firstGrid = Math.floor(startBeat / GRID_STEP + 1) * GRID_STEP;
|
|
1506
|
-
for (let beat = firstGrid; beat < endBeat -
|
|
1794
|
+
for (let beat = firstGrid; beat < endBeat - BEAT_EPS; beat += GRID_STEP) {
|
|
1507
1795
|
boundaries.add(beat);
|
|
1508
1796
|
}
|
|
1509
1797
|
return [...boundaries].sort((a, b) => a - b);
|
|
@@ -1517,7 +1805,7 @@ function boundaryContexts(melody, harmony, startBeat, endBeat, ts) {
|
|
|
1517
1805
|
otherVoices: melodyVoice === void 0 ? [] : [melodyVoice],
|
|
1518
1806
|
chord: harmony.chordAt(boundary),
|
|
1519
1807
|
strongBeat: isStrongBeat(boundary, ts),
|
|
1520
|
-
atCounterOnset: Math.abs(boundary - startBeat) <
|
|
1808
|
+
atCounterOnset: Math.abs(boundary - startBeat) < BEAT_EPS
|
|
1521
1809
|
};
|
|
1522
1810
|
});
|
|
1523
1811
|
}
|
|
@@ -1550,6 +1838,7 @@ function heldPitchSafety(pitch, contexts, prevPitch, key, profile, register, low
|
|
|
1550
1838
|
return worst;
|
|
1551
1839
|
}
|
|
1552
1840
|
function generateCounterMelody(opts) {
|
|
1841
|
+
assertRecord(opts, "countermelody options");
|
|
1553
1842
|
const timeline = opts.timeline;
|
|
1554
1843
|
const chordAt = timeline === void 0 ? opts.chordAt : (beat) => timeline.at(beat);
|
|
1555
1844
|
if (chordAt === void 0) {
|
|
@@ -1561,10 +1850,11 @@ function generateCounterMelody(opts) {
|
|
|
1561
1850
|
const melody = createNoteEventIndex(soundingNotesOnly(opts.melody), {
|
|
1562
1851
|
tieBreak: register === "below" ? "highest" : "lowest"
|
|
1563
1852
|
});
|
|
1564
|
-
const ts = opts.ts ??
|
|
1853
|
+
const ts = meterAt(0, toMeterData(opts.ts ?? DEFAULT_TS2, "ts"));
|
|
1565
1854
|
assertTimeSignature(ts);
|
|
1566
1855
|
const rhythm = assertOneOf(opts.rhythm ?? "complement", ["complement", "follow"], "rhythm");
|
|
1567
1856
|
const profile = assertOneOf(opts.profile ?? "pop", ["strict", "pop"], "safety profile");
|
|
1857
|
+
assertWeightOverrides(opts.weights);
|
|
1568
1858
|
const weights = profileWeights(profile, opts.weights);
|
|
1569
1859
|
const key = toKeyScale(opts.key);
|
|
1570
1860
|
const draw = resolveContext(opts.ctx).part("countermelody");
|
|
@@ -1662,8 +1952,8 @@ function generateCounterMelody(opts) {
|
|
|
1662
1952
|
}
|
|
1663
1953
|
}
|
|
1664
1954
|
const score = scoreCandidate(candidate, melPitch, melPrev, prevPitch, chord, center, weights) + (jitter[candidate - low] ?? 0);
|
|
1665
|
-
const tie = Math.abs(score - bestScore) <=
|
|
1666
|
-
if (worstSafety < bestWorstSafety || worstSafety === bestWorstSafety && (score > bestScore +
|
|
1955
|
+
const tie = Math.abs(score - bestScore) <= SCORE_EPS && candidate < (bestPitch ?? Number.POSITIVE_INFINITY);
|
|
1956
|
+
if (worstSafety < bestWorstSafety || worstSafety === bestWorstSafety && (score > bestScore + SCORE_EPS || tie)) {
|
|
1667
1957
|
bestWorstSafety = worstSafety;
|
|
1668
1958
|
bestScore = score;
|
|
1669
1959
|
bestPitch = candidate;
|
|
@@ -1742,8 +2032,8 @@ function getGhostVelocity(section, beatPosition) {
|
|
|
1742
2032
|
}
|
|
1743
2033
|
return Math.max(0.25, Math.min(0.65, base));
|
|
1744
2034
|
}
|
|
1745
|
-
function getGhostProbabilityAtPosition(beat, sixteenthInBeat) {
|
|
1746
|
-
const leadsIntoBackbeat = (beat
|
|
2035
|
+
function getGhostProbabilityAtPosition(beat, sixteenthInBeat, leadIns) {
|
|
2036
|
+
const leadsIntoBackbeat = leadIns.includes(beat) && sixteenthInBeat === 3;
|
|
1747
2037
|
return leadsIntoBackbeat ? 0.6 : 0.25;
|
|
1748
2038
|
}
|
|
1749
2039
|
function selectGhostPositions(mood) {
|
|
@@ -1973,6 +2263,12 @@ function shouldAddOpenHHAccent(section, beat, bar2, draw) {
|
|
|
1973
2263
|
function footHiHatVelocity(draw, bar2, beat) {
|
|
1974
2264
|
return draw.range(FHH_VEL_MIN, FHH_VEL_MAX, "fhhVelocity", bar2, beat);
|
|
1975
2265
|
}
|
|
2266
|
+
function usesOffbeatOpenHiHat(style) {
|
|
2267
|
+
return style === "fourOnFloor";
|
|
2268
|
+
}
|
|
2269
|
+
function offbeatOpenHiHatChance(bpm) {
|
|
2270
|
+
return Math.max(0.15, Math.min(0.8, 45 / bpm));
|
|
2271
|
+
}
|
|
1976
2272
|
function shouldUseRideForSection(section, style) {
|
|
1977
2273
|
if (style === "rock" && section === "chorus") {
|
|
1978
2274
|
return true;
|
|
@@ -1982,9 +2278,6 @@ function shouldUseRideForSection(section, style) {
|
|
|
1982
2278
|
}
|
|
1983
2279
|
return section === "bridge";
|
|
1984
2280
|
}
|
|
1985
|
-
function shouldUseBridgeCrossStick(section, beat) {
|
|
1986
|
-
return section === "bridge" && (beat === 1 || beat === 3);
|
|
1987
|
-
}
|
|
1988
2281
|
|
|
1989
2282
|
// src/generate/drums/swing.ts
|
|
1990
2283
|
var SIXTEENTH2 = 0.25;
|
|
@@ -2072,36 +2365,50 @@ function generateKickForBeat(ctx, sec, kick) {
|
|
|
2072
2365
|
ctx.track.add(GM.BD, tick, offBeat ? SIXTEENTH : EIGHTH, ctx.velocity * 0.85);
|
|
2073
2366
|
}
|
|
2074
2367
|
}
|
|
2075
|
-
function
|
|
2076
|
-
if (
|
|
2077
|
-
return;
|
|
2368
|
+
function roleBackbeatWeight(role) {
|
|
2369
|
+
if (role === "minimal") {
|
|
2370
|
+
return 0.65;
|
|
2078
2371
|
}
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2372
|
+
return role === "ambient" ? 0.8 : 1;
|
|
2373
|
+
}
|
|
2374
|
+
function backbeatVoice(sec) {
|
|
2375
|
+
if (sec.role === "fxOnly") {
|
|
2376
|
+
return void 0;
|
|
2082
2377
|
}
|
|
2083
|
-
|
|
2084
|
-
|
|
2378
|
+
const sideStick = sec.style === "latin" || sec.role === "ambient" || sec.role === "minimal";
|
|
2379
|
+
return sideStick ? GM.SIDESTICK : GM.SD;
|
|
2380
|
+
}
|
|
2381
|
+
function writesSnareGhosts(sec) {
|
|
2382
|
+
return backbeatVoice(sec) === GM.SD && sec.style !== "sparse";
|
|
2383
|
+
}
|
|
2384
|
+
function backbeatStroke(sec, velocity) {
|
|
2385
|
+
const pitch = backbeatVoice(sec);
|
|
2386
|
+
if (pitch === void 0) {
|
|
2387
|
+
return void 0;
|
|
2085
2388
|
}
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2389
|
+
return pitch === GM.SD ? { pitch, velocity: Math.min(127, velocity + BACKBEAT_LIFT) } : { pitch, velocity: velocity * roleBackbeatWeight(sec.role) };
|
|
2390
|
+
}
|
|
2391
|
+
var FOUR_FOUR_BEATS = 4;
|
|
2392
|
+
function generateSnareForBeat(ctx, sec, isIntroFirst) {
|
|
2393
|
+
if (ctx.inPrechorusLift) {
|
|
2089
2394
|
return;
|
|
2090
2395
|
}
|
|
2091
|
-
if (sec.
|
|
2092
|
-
ctx.track.add(GM.SIDESTICK, ctx.beatTick, EIGHTH, ctx.velocity * 0.65);
|
|
2396
|
+
if (!backbeatBeats(sec.snareBeat3).includes(ctx.beat) || isIntroFirst) {
|
|
2093
2397
|
return;
|
|
2094
2398
|
}
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
ctx.track.add(GM.SIDESTICK, ctx.beatTick, EIGHTH, ctx.velocity * 0.8);
|
|
2099
|
-
} else {
|
|
2100
|
-
ctx.track.add(GM.SD, ctx.beatTick, EIGHTH, backbeatVel);
|
|
2399
|
+
const stroke = backbeatStroke(sec, ctx.velocity);
|
|
2400
|
+
if (!stroke) {
|
|
2401
|
+
return;
|
|
2101
2402
|
}
|
|
2403
|
+
ctx.track.add(stroke.pitch, ctx.beatTick, EIGHTH, stroke.velocity);
|
|
2102
2404
|
}
|
|
2103
2405
|
function generateGhostNotesForBeat(ctx, sec) {
|
|
2104
|
-
if (
|
|
2406
|
+
if (!writesSnareGhosts(sec)) {
|
|
2407
|
+
return;
|
|
2408
|
+
}
|
|
2409
|
+
const backbeats = backbeatBeats(sec.snareBeat3);
|
|
2410
|
+
const leadIns = backbeats.map((beat) => (beat - 1 + FOUR_FOUR_BEATS) % FOUR_FOUR_BEATS);
|
|
2411
|
+
if (!leadIns.includes(ctx.beat)) {
|
|
2105
2412
|
return;
|
|
2106
2413
|
}
|
|
2107
2414
|
if (!sustainsStrokes(SIXTEENTH, ctx.bpm, sec.difficulty)) {
|
|
@@ -2114,7 +2421,7 @@ function generateGhostNotesForBeat(ctx, sec) {
|
|
|
2114
2421
|
}
|
|
2115
2422
|
for (const pos of positions) {
|
|
2116
2423
|
const sixteenthInBeat = pos === "e" ? 1 : 3;
|
|
2117
|
-
const posProb = getGhostProbabilityAtPosition(ctx.beat, sixteenthInBeat);
|
|
2424
|
+
const posProb = getGhostProbabilityAtPosition(ctx.beat, sixteenthInBeat, leadIns);
|
|
2118
2425
|
if (!ctx.draw.prob(ghostProb * posProb, "ghost", ctx.bar, ctx.beat, pos)) {
|
|
2119
2426
|
continue;
|
|
2120
2427
|
}
|
|
@@ -2154,40 +2461,47 @@ function generatePreChorusBuildup(ctx, sec, isSectionLastBar) {
|
|
|
2154
2461
|
);
|
|
2155
2462
|
}
|
|
2156
2463
|
if (isSectionLastBar && ctx.beat === 3) {
|
|
2157
|
-
ctx.track.add(
|
|
2464
|
+
ctx.track.add(
|
|
2465
|
+
GM.CRASH,
|
|
2466
|
+
swing16(ctx.beatTick + EIGHTH + SIXTEENTH, sec, ctx.swingAmount),
|
|
2467
|
+
SIXTEENTH,
|
|
2468
|
+
ctx.velocity * 1.1
|
|
2469
|
+
);
|
|
2158
2470
|
}
|
|
2159
2471
|
}
|
|
2472
|
+
function opensOnDownbeat(ctx, sec) {
|
|
2473
|
+
if (usesOffbeatOpenHiHat(sec.style)) {
|
|
2474
|
+
return false;
|
|
2475
|
+
}
|
|
2476
|
+
return shouldAddOpenHHAccent(ctx.section, ctx.beat, ctx.bar, ctx.draw);
|
|
2477
|
+
}
|
|
2478
|
+
function opensOffbeat(ctx, sec) {
|
|
2479
|
+
if (!usesOffbeatOpenHiHat(sec.style) || ctx.beat !== 1 && ctx.beat !== 3) {
|
|
2480
|
+
return false;
|
|
2481
|
+
}
|
|
2482
|
+
return ctx.draw.prob(offbeatOpenHiHatChance(ctx.bpm), "openHat", ctx.bar, ctx.beat, 1);
|
|
2483
|
+
}
|
|
2160
2484
|
function generateHiHatForBeat(ctx, sec) {
|
|
2161
2485
|
if (!shouldPlayHiHat(sec.role)) {
|
|
2162
2486
|
return;
|
|
2163
2487
|
}
|
|
2164
|
-
const
|
|
2165
|
-
const hhInstrument = crossStick ? GM.SIDESTICK : roleHiHatInstrument(sec.role, sec.useRide);
|
|
2488
|
+
const hhInstrument = roleHiHatInstrument(sec.role, sec.useRide);
|
|
2166
2489
|
const hhType = sectionHiHatType(ctx.section, sec.role);
|
|
2167
2490
|
const typeMult = hiHatTypeVelocityMultiplier(hhType);
|
|
2168
|
-
const isDynamicOpen = ctx.barHasOpenHh && ctx.beat === ctx.openHhBeat;
|
|
2169
2491
|
const allowsOpenHiHat = sec.role !== "ambient" && sec.role !== "minimal";
|
|
2492
|
+
const isDynamicOpen = ctx.barHasOpenHh && ctx.beat === ctx.openHhBeat && !usesOffbeatOpenHiHat(sec.style);
|
|
2170
2493
|
const dm = sec.densityMult;
|
|
2171
2494
|
if (ctx.hhLevel === "quarter") {
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
ctx.track.add(
|
|
2183
|
-
hhInstrument,
|
|
2184
|
-
ctx.beatTick,
|
|
2185
|
-
EIGHTH,
|
|
2186
|
-
Math.max(20, ctx.velocity * dm * 0.75 * typeMult)
|
|
2187
|
-
);
|
|
2188
|
-
}
|
|
2189
|
-
} else if (sec.useFootHh) {
|
|
2190
|
-
ctx.track.add(GM.FHH, ctx.beatTick, EIGHTH, footHiHatVelocity(ctx.draw, ctx.bar, ctx.beat));
|
|
2495
|
+
if (ctx.section === "intro" && ctx.beat !== 0) {
|
|
2496
|
+
return;
|
|
2497
|
+
}
|
|
2498
|
+
const hhVel = Math.max(20, ctx.velocity * dm * 0.75 * typeMult);
|
|
2499
|
+
if (allowsOpenHiHat && isDynamicOpen) {
|
|
2500
|
+
ctx.track.add(GM.OHH, ctx.beatTick, EIGHTH, hhVel + OHH_VEL_BOOST);
|
|
2501
|
+
} else if (allowsOpenHiHat && opensOnDownbeat(ctx, sec)) {
|
|
2502
|
+
ctx.track.add(hiHatNote("open"), ctx.beatTick, EIGHTH, Math.max(20, hhVel * 1.1));
|
|
2503
|
+
} else {
|
|
2504
|
+
ctx.track.add(hhInstrument, ctx.beatTick, EIGHTH, hhVel);
|
|
2191
2505
|
}
|
|
2192
2506
|
return;
|
|
2193
2507
|
}
|
|
@@ -2208,13 +2522,7 @@ function generateHiHatForBeat(ctx, sec) {
|
|
|
2208
2522
|
ctx.track.add(GM.OHH, hhTick, EIGHTH, hhVel + OHH_VEL_BOOST);
|
|
2209
2523
|
continue;
|
|
2210
2524
|
}
|
|
2211
|
-
|
|
2212
|
-
if (sec.style === "fourOnFloor" && eighth === 1) {
|
|
2213
|
-
const openProb = Math.max(0.15, Math.min(0.8, 45 / ctx.bpm));
|
|
2214
|
-
useOpen = (ctx.beat === 1 || ctx.beat === 3) && ctx.draw.prob(openProb, "openHat", ctx.bar, ctx.beat, eighth);
|
|
2215
|
-
} else if (eighth === 0) {
|
|
2216
|
-
useOpen = shouldAddOpenHHAccent(ctx.section, ctx.beat, ctx.bar, ctx.draw);
|
|
2217
|
-
}
|
|
2525
|
+
const useOpen = eighth === 0 ? opensOnDownbeat(ctx, sec) : opensOffbeat(ctx, sec);
|
|
2218
2526
|
if (useOpen && allowsOpenHiHat) {
|
|
2219
2527
|
ctx.track.add(hiHatNote("open"), hhTick, EIGHTH, Math.max(20, hhVel * 1.1));
|
|
2220
2528
|
} else {
|
|
@@ -2237,6 +2545,14 @@ function generateHiHatForBeat(ctx, sec) {
|
|
|
2237
2545
|
ctx.track.add(GM.OHH, hhTick, SIXTEENTH, hhVel + OHH_VEL_BOOST);
|
|
2238
2546
|
continue;
|
|
2239
2547
|
}
|
|
2548
|
+
if (allowsOpenHiHat && sixteenth === 0 && opensOnDownbeat(ctx, sec)) {
|
|
2549
|
+
ctx.track.add(hiHatNote("open"), hhTick, SIXTEENTH, Math.max(20, hhVel * 1.1));
|
|
2550
|
+
continue;
|
|
2551
|
+
}
|
|
2552
|
+
if (allowsOpenHiHat && sixteenth === 2 && opensOffbeat(ctx, sec)) {
|
|
2553
|
+
ctx.track.add(hiHatNote("open"), hhTick, SIXTEENTH, Math.max(20, hhVel * 1.1));
|
|
2554
|
+
continue;
|
|
2555
|
+
}
|
|
2240
2556
|
if (allowsOpenHiHat && ctx.beat === 3 && sixteenth === 3) {
|
|
2241
2557
|
const openProb = Math.max(0.1, Math.min(0.4, 30 / ctx.bpm));
|
|
2242
2558
|
if (ctx.draw.prob(openProb, "openHat", ctx.bar, ctx.beat, sixteenth)) {
|
|
@@ -2329,7 +2645,7 @@ function archetype(beat2, beat3, boundToBar) {
|
|
|
2329
2645
|
...boundToBar ? { boundToBar } : {}
|
|
2330
2646
|
};
|
|
2331
2647
|
}
|
|
2332
|
-
var FILL_ARCHETYPES =
|
|
2648
|
+
var FILL_ARCHETYPES = deepFreeze({
|
|
2333
2649
|
// One crescendo of seven strokes across the two beats, not two of four and
|
|
2334
2650
|
// three: the roll is a single gesture and its second half continues where its
|
|
2335
2651
|
// first half left off.
|
|
@@ -2432,8 +2748,9 @@ function getFillStartBeat(energy) {
|
|
|
2432
2748
|
case "medium":
|
|
2433
2749
|
return 2;
|
|
2434
2750
|
case "high":
|
|
2751
|
+
return 1;
|
|
2435
2752
|
case "peak":
|
|
2436
|
-
return
|
|
2753
|
+
return 0;
|
|
2437
2754
|
}
|
|
2438
2755
|
}
|
|
2439
2756
|
var SPARSE_FILLS = ["simpleCrash", "breakdownFill"];
|
|
@@ -2543,7 +2860,7 @@ function fillWithinCeiling(shape, fromBeat, barBeats, bpm, difficulty) {
|
|
|
2543
2860
|
}
|
|
2544
2861
|
return true;
|
|
2545
2862
|
}
|
|
2546
|
-
function generateFill(track, beatTick, beat, fillType, velocity) {
|
|
2863
|
+
function generateFill(track, beatTick, beat, fillType, velocity, place = (tick) => tick) {
|
|
2547
2864
|
const shape = typeof fillType === "string" ? FILL_ARCHETYPES[fillType] : fillType;
|
|
2548
2865
|
if (!shape) {
|
|
2549
2866
|
return;
|
|
@@ -2556,7 +2873,7 @@ function generateFill(track, beatTick, beat, fillType, velocity) {
|
|
|
2556
2873
|
const scaled = stroke.velocity.scale === void 0 ? base : base * stroke.velocity.scale;
|
|
2557
2874
|
track.add(
|
|
2558
2875
|
stroke.voice,
|
|
2559
|
-
beatTick + stroke.offset,
|
|
2876
|
+
place(beatTick + stroke.offset),
|
|
2560
2877
|
stroke.duration,
|
|
2561
2878
|
scaled + (stroke.velocity.offset ?? 0),
|
|
2562
2879
|
stroke.articulation
|
|
@@ -2584,17 +2901,30 @@ var DRUM_NOTES = Object.freeze({
|
|
|
2584
2901
|
var VOICE_BY_NOTE = new Map(
|
|
2585
2902
|
Object.entries(DRUM_NOTES).map(([voice, note2]) => [note2, voice])
|
|
2586
2903
|
);
|
|
2904
|
+
function drumNoteOf(voice) {
|
|
2905
|
+
return Object.hasOwn(DRUM_NOTES, voice) ? DRUM_NOTES[voice] : void 0;
|
|
2906
|
+
}
|
|
2587
2907
|
function drumVoiceOf(pitch) {
|
|
2588
|
-
return VOICE_BY_NOTE.get(pitch);
|
|
2908
|
+
return VOICE_BY_NOTE.get(assertMidiPitch(pitch));
|
|
2589
2909
|
}
|
|
2590
2910
|
var ONSET_KEY_SCALE = 128;
|
|
2591
2911
|
var HitList = class {
|
|
2592
2912
|
hits = [];
|
|
2593
|
-
/**
|
|
2594
|
-
#onsets = /* @__PURE__ */ new
|
|
2595
|
-
/**
|
|
2913
|
+
/** Voices already struck at each quantized beat. */
|
|
2914
|
+
#onsets = /* @__PURE__ */ new Map();
|
|
2915
|
+
/**
|
|
2916
|
+
* Append a hit, clamping velocity and dropping non-positive positions.
|
|
2917
|
+
*
|
|
2918
|
+
* A voice already struck at this instant is left as it stands: one drum
|
|
2919
|
+
* sounds once however many voices ask for it, and a second entry at the same
|
|
2920
|
+
* pitch and position is a note-on a reader answers with no note-off, heard as
|
|
2921
|
+
* a hung or cut stroke rather than as a louder one.
|
|
2922
|
+
*/
|
|
2596
2923
|
add(pitch, startBeat, durationBeat, velocity, articulation) {
|
|
2597
2924
|
const beat = Math.max(0, startBeat);
|
|
2925
|
+
if (this.hasOnset(pitch, beat)) {
|
|
2926
|
+
return;
|
|
2927
|
+
}
|
|
2598
2928
|
const hit2 = {
|
|
2599
2929
|
pitch,
|
|
2600
2930
|
startBeat: beat,
|
|
@@ -2605,23 +2935,34 @@ var HitList = class {
|
|
|
2605
2935
|
hit2.articulation = articulation;
|
|
2606
2936
|
}
|
|
2607
2937
|
this.hits.push(hit2);
|
|
2608
|
-
this.#onsets.
|
|
2938
|
+
const struck = this.#onsets.get(beatSlot(beat));
|
|
2939
|
+
if (struck) {
|
|
2940
|
+
struck.add(pitch);
|
|
2941
|
+
} else {
|
|
2942
|
+
this.#onsets.set(beatSlot(beat), /* @__PURE__ */ new Set([pitch]));
|
|
2943
|
+
}
|
|
2609
2944
|
}
|
|
2610
2945
|
/** Whether a hit of this pitch already starts exactly at `startBeat`. */
|
|
2611
2946
|
hasOnset(pitch, startBeat) {
|
|
2612
|
-
return this.#onsets.
|
|
2947
|
+
return this.#onsets.get(beatSlot(startBeat))?.has(pitch) ?? false;
|
|
2613
2948
|
}
|
|
2614
|
-
/**
|
|
2949
|
+
/**
|
|
2950
|
+
* True when a crash already sits less than a 16th from `startBeat`, on either
|
|
2951
|
+
* side of it.
|
|
2952
|
+
*/
|
|
2615
2953
|
hasCrashNear(startBeat) {
|
|
2616
2954
|
const steps = Math.round(0.25 * ONSET_KEY_SCALE);
|
|
2617
|
-
for (let step =
|
|
2618
|
-
if (this
|
|
2955
|
+
for (let step = -steps + 1; step < steps; step += 1) {
|
|
2956
|
+
if (this.hasOnset(GM.CRASH, startBeat + step / ONSET_KEY_SCALE)) {
|
|
2619
2957
|
return true;
|
|
2620
2958
|
}
|
|
2621
2959
|
}
|
|
2622
2960
|
return false;
|
|
2623
2961
|
}
|
|
2624
2962
|
};
|
|
2963
|
+
function beatSlot(startBeat) {
|
|
2964
|
+
return Math.round(startBeat * ONSET_KEY_SCALE);
|
|
2965
|
+
}
|
|
2625
2966
|
function onsetKey(pitch, startBeat) {
|
|
2626
2967
|
return `${pitch}:${Math.round(startBeat * ONSET_KEY_SCALE)}`;
|
|
2627
2968
|
}
|
|
@@ -2637,7 +2978,13 @@ var BEAT_3 = beatStep(2);
|
|
|
2637
2978
|
var BEAT_4 = beatStep(3);
|
|
2638
2979
|
var AND_2 = beatStep(1) + 2;
|
|
2639
2980
|
var AND_4 = beatStep(3) + 2;
|
|
2981
|
+
var A_OF_3 = beatStep(2) + 3;
|
|
2640
2982
|
var OUTRO_FIGURE = [{ step: BEAT_1 }, { step: BEAT_3 }];
|
|
2983
|
+
function statedSlots(figure) {
|
|
2984
|
+
return figure.filter(
|
|
2985
|
+
(slot) => slot.probability === void 0 && slot.sections === void 0 && slot.barParity === void 0
|
|
2986
|
+
);
|
|
2987
|
+
}
|
|
2641
2988
|
var DEFAULT_FIGURE = [
|
|
2642
2989
|
{ step: BEAT_1 },
|
|
2643
2990
|
{ step: BEAT_3 },
|
|
@@ -2645,7 +2992,7 @@ var DEFAULT_FIGURE = [
|
|
|
2645
2992
|
{ step: AND_2, probability: 0.55, slot: "beat2and", sections: ["chorus"] },
|
|
2646
2993
|
{ step: AND_4, probability: 0.35, slot: "beat4and", sections: ["chorus"] }
|
|
2647
2994
|
];
|
|
2648
|
-
var KICK_FIGURES =
|
|
2995
|
+
var KICK_FIGURES = deepFreeze({
|
|
2649
2996
|
sparse: [{ step: BEAT_1 }, { step: BEAT_3, sections: ["chorus"], barParity: 1 }],
|
|
2650
2997
|
fourOnFloor: [
|
|
2651
2998
|
{ step: BEAT_1 },
|
|
@@ -2663,9 +3010,13 @@ var KICK_FIGURES = Object.freeze({
|
|
|
2663
3010
|
rock: [
|
|
2664
3011
|
{ step: BEAT_1 },
|
|
2665
3012
|
{ step: BEAT_3 },
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
3013
|
+
// The broken beat: the "and" of two answers the snare and the "a" of three
|
|
3014
|
+
// pushes into the fourth, in every section. That displacement is the figure
|
|
3015
|
+
// rather than a slot a dial may or may not fill in a chorus, which is what
|
|
3016
|
+
// separates it from the plain pop pulse wherever it is played.
|
|
3017
|
+
{ step: AND_2 },
|
|
3018
|
+
{ step: A_OF_3 },
|
|
3019
|
+
{ step: AND_4, probability: 0.4, slot: "beat4and", sections: ["chorus"] }
|
|
2669
3020
|
],
|
|
2670
3021
|
synth: [
|
|
2671
3022
|
{ step: BEAT_1 },
|
|
@@ -2719,7 +3070,8 @@ function realiseKickFigure(figure, section, bar2, draw, rhythmic) {
|
|
|
2719
3070
|
return pattern;
|
|
2720
3071
|
}
|
|
2721
3072
|
function getKickPattern(section, style, bar2, draw, rhythmic) {
|
|
2722
|
-
const
|
|
3073
|
+
const own = KICK_FIGURES[style] ?? DEFAULT_FIGURE;
|
|
3074
|
+
const figure = section === "outro" ? [...OUTRO_FIGURE, ...statedSlots(own)] : own;
|
|
2723
3075
|
return realiseKickFigure(figure, section, bar2, draw, rhythmic);
|
|
2724
3076
|
}
|
|
2725
3077
|
|
|
@@ -2771,8 +3123,9 @@ function generateAuxPercussionForBar(track, barStart, config, role, densityMult,
|
|
|
2771
3123
|
if (role === "minimal") {
|
|
2772
3124
|
return;
|
|
2773
3125
|
}
|
|
3126
|
+
const backbeats = backbeatBeats(sec.snareBeat3, barBeats);
|
|
2774
3127
|
if (config.tambourine) {
|
|
2775
|
-
for (
|
|
3128
|
+
for (const beat of backbeats) {
|
|
2776
3129
|
const raw = 70 * densityMult * draw.float(0.9, 1.1, "tambourine", barStart, beat);
|
|
2777
3130
|
track.add(GM.TAMBOURINE, barStart + beat, EIGHTH, Math.max(40, Math.min(90, raw)));
|
|
2778
3131
|
}
|
|
@@ -2811,7 +3164,7 @@ function generateAuxPercussionForBar(track, barStart, config, role, densityMult,
|
|
|
2811
3164
|
}
|
|
2812
3165
|
}
|
|
2813
3166
|
if (config.handclap) {
|
|
2814
|
-
for (
|
|
3167
|
+
for (const beat of backbeats) {
|
|
2815
3168
|
const raw = 85 * densityMult * draw.float(0.9, 1.1, "handclap", barStart, beat);
|
|
2816
3169
|
track.add(GM.HANDCLAP, barStart + beat, EIGHTH, Math.max(50, Math.min(100, raw)));
|
|
2817
3170
|
}
|
|
@@ -2837,6 +3190,11 @@ var REACH_BY_VOICE = {
|
|
|
2837
3190
|
lowTom: ["rightHand", "leftHand"],
|
|
2838
3191
|
shaker: ["rightHand", "leftHand"]
|
|
2839
3192
|
};
|
|
3193
|
+
var OVERDUB_VOICES = Object.freeze([
|
|
3194
|
+
"handClap",
|
|
3195
|
+
"tambourine",
|
|
3196
|
+
"shaker"
|
|
3197
|
+
]);
|
|
2840
3198
|
function reachByNote() {
|
|
2841
3199
|
const reach = {};
|
|
2842
3200
|
for (const [voice, limbs] of Object.entries(REACH_BY_VOICE)) {
|
|
@@ -2859,9 +3217,11 @@ var DRUM_KIT = Object.freeze({
|
|
|
2859
3217
|
kind: "percussion",
|
|
2860
3218
|
name: "drum kit",
|
|
2861
3219
|
limbs: LIMBS,
|
|
2862
|
-
reach:
|
|
3220
|
+
reach: deepFreeze(reachByNote()),
|
|
3221
|
+
overdub: deepFreeze(OVERDUB_VOICES.map((voice) => DRUM_NOTES[voice])),
|
|
2863
3222
|
articulations: KIT_ARTICULATIONS,
|
|
2864
|
-
|
|
3223
|
+
// One stroke per limb, and one more per voice dubbed over them.
|
|
3224
|
+
polyphony: LIMBS.length + OVERDUB_VOICES.length
|
|
2865
3225
|
});
|
|
2866
3226
|
|
|
2867
3227
|
// src/generate/drums/vocabulary.ts
|
|
@@ -2869,6 +3229,15 @@ function isDrumPattern(material) {
|
|
|
2869
3229
|
const candidate = material;
|
|
2870
3230
|
return typeof candidate === "object" && candidate !== null && typeof candidate.steps === "number" && Array.isArray(candidate.strokes);
|
|
2871
3231
|
}
|
|
3232
|
+
function assertDrumPattern(material, label) {
|
|
3233
|
+
material.strokes.forEach((stroke, index) => {
|
|
3234
|
+
if (typeof stroke !== "object" || stroke === null || typeof stroke.voice !== "string" || drumNoteOf(stroke.voice) === void 0) {
|
|
3235
|
+
throw new InvalidInputError(
|
|
3236
|
+
`${label} strokes[${index}].voice must name a drum voice; received ${describeRejected(stroke?.voice)}`
|
|
3237
|
+
);
|
|
3238
|
+
}
|
|
3239
|
+
});
|
|
3240
|
+
}
|
|
2872
3241
|
function hit(voice, step, velocity, extra) {
|
|
2873
3242
|
return { voice, step, velocity, ...extra };
|
|
2874
3243
|
}
|
|
@@ -2886,7 +3255,7 @@ function bar(strokes) {
|
|
|
2886
3255
|
return { steps: BAR_STEPS, strokes };
|
|
2887
3256
|
}
|
|
2888
3257
|
var FOUR_FOUR2 = { numerator: 4, denominator: 4 };
|
|
2889
|
-
var DRUM_PATTERNS =
|
|
3258
|
+
var DRUM_PATTERNS = deepFreeze([
|
|
2890
3259
|
{
|
|
2891
3260
|
id: "motownBackbeat",
|
|
2892
3261
|
genre: "motown",
|
|
@@ -3056,12 +3425,12 @@ var DEFAULT_VELOCITY = 100;
|
|
|
3056
3425
|
var DEFAULT_BPM2 = 120;
|
|
3057
3426
|
var MAX_STROKES_PER_BAR = 64;
|
|
3058
3427
|
function placeDrumPattern(opts) {
|
|
3428
|
+
assertRecord(opts, "drum pattern options");
|
|
3059
3429
|
assertPositiveInt(opts.bars, "drum pattern bars");
|
|
3060
3430
|
assertGenerationBudget(opts.bars * MAX_STROKES_PER_BAR, "drum pattern hits", opts.budget);
|
|
3061
3431
|
const genre = assertOneOf(opts.genre, GENRES, "drum pattern genre");
|
|
3062
3432
|
const section = opts.section === void 0 ? void 0 : assertOneOf(opts.section, PUBLIC_SECTIONS, "drum pattern section");
|
|
3063
|
-
const ts = opts.ts ?? FOUR_FOUR2;
|
|
3064
|
-
assertTimeSignature(ts, "drum pattern time signature");
|
|
3433
|
+
const ts = meterAt(0, toMeterData(opts.ts ?? FOUR_FOUR2, "ts"));
|
|
3065
3434
|
if (opts.velocity !== void 0) {
|
|
3066
3435
|
assertRange(opts.velocity, 1, 127, "drum pattern velocity");
|
|
3067
3436
|
}
|
|
@@ -3073,10 +3442,16 @@ function placeDrumPattern(opts) {
|
|
|
3073
3442
|
const barBeats = beatsPerBar(ts);
|
|
3074
3443
|
const kit = resolved.instrument("drums");
|
|
3075
3444
|
const swing = effectiveSwing(feel, feelSwingAmount(feel));
|
|
3076
|
-
const
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
);
|
|
3445
|
+
const supplied = vocabularyOfKind(resolved.vocabulary, isDrumPattern);
|
|
3446
|
+
supplied.forEach((entry, index) => {
|
|
3447
|
+
assertDrumPattern(entry.material, `drum pattern vocabulary[${index}]`);
|
|
3448
|
+
});
|
|
3449
|
+
const dictionary = mergeVocabulary(DRUM_PATTERNS, supplied);
|
|
3450
|
+
if (selectVocabulary(dictionary, { ts }).length === 0) {
|
|
3451
|
+
throw new InvalidInputError(
|
|
3452
|
+
`drum pattern ts names a meter no figure is written in: ${formatTimeSignature(ts)}. The built-in figures are written in 4/4; supply vocabulary in another meter through the context.`
|
|
3453
|
+
);
|
|
3454
|
+
}
|
|
3080
3455
|
const track = new HitList();
|
|
3081
3456
|
for (let bar2 = 0; bar2 < opts.bars; bar2 += 1) {
|
|
3082
3457
|
const entry = pickVocabulary(
|
|
@@ -3106,7 +3481,10 @@ function placeDrumPattern(opts) {
|
|
|
3106
3481
|
...resolved.ornament === void 0 ? {} : { ornament: resolved.ornament },
|
|
3107
3482
|
isOrnament: (stroke) => stroke.articulation === "ghost",
|
|
3108
3483
|
rate: opts.rate ?? "straight",
|
|
3109
|
-
spanSteps: entry.material.steps
|
|
3484
|
+
spanSteps: entry.material.steps,
|
|
3485
|
+
// The dials rank the figure's positions against the bar it is written
|
|
3486
|
+
// in, which is the bar this call is placing it in.
|
|
3487
|
+
ts
|
|
3110
3488
|
},
|
|
3111
3489
|
draw,
|
|
3112
3490
|
"pattern",
|
|
@@ -3117,13 +3495,18 @@ function placeDrumPattern(opts) {
|
|
|
3117
3495
|
}
|
|
3118
3496
|
const barStart = bar2 * barBeats;
|
|
3119
3497
|
for (const stroke of strokes) {
|
|
3120
|
-
const pitch =
|
|
3498
|
+
const pitch = drumNoteOf(stroke.voice);
|
|
3121
3499
|
if (pitch === void 0) {
|
|
3122
3500
|
continue;
|
|
3123
3501
|
}
|
|
3124
3502
|
track.add(
|
|
3125
3503
|
pitch,
|
|
3126
|
-
|
|
3504
|
+
// The bar's own grid is what is swung, and the bar start is added
|
|
3505
|
+
// back after. Swinging the absolute beat takes the offset inside a
|
|
3506
|
+
// quarter note, which is the same thing only while a bar is a whole
|
|
3507
|
+
// number of them: in 7/8 every other bar begins half a beat into one,
|
|
3508
|
+
// so the downbeat was warped and the offbeats came out straight.
|
|
3509
|
+
barStart + quantizeSwing(stroke.step * STEP_BEATS, swing, "sixteenth"),
|
|
3127
3510
|
stroke.duration ?? STEP_BEATS,
|
|
3128
3511
|
baseVelocity * stroke.velocity,
|
|
3129
3512
|
stroke.articulation
|
|
@@ -3138,6 +3521,7 @@ function placeDrumPattern(opts) {
|
|
|
3138
3521
|
var MAX_HITS_PER_BAR = 128;
|
|
3139
3522
|
var DEFAULT_BPM3 = 120;
|
|
3140
3523
|
function generateDrums(opts) {
|
|
3524
|
+
assertRecord(opts, "drum options");
|
|
3141
3525
|
assertPositiveInt(opts.bars, "drum bars");
|
|
3142
3526
|
assertGenerationBudget(opts.bars * MAX_HITS_PER_BAR, "drum hits", opts.budget);
|
|
3143
3527
|
if (opts.euclideanKick) {
|
|
@@ -3162,13 +3546,15 @@ function generateDrums(opts) {
|
|
|
3162
3546
|
const style = mapping.style;
|
|
3163
3547
|
const feel = opts.feel === void 0 ? mapping.feel : assertOneOf(opts.feel, DRUM_FEELS, "drum feel");
|
|
3164
3548
|
const role = opts.role === void 0 ? "full" : assertOneOf(opts.role, DRUM_ROLES, "drum role");
|
|
3165
|
-
const
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3549
|
+
const meters = resolveMeters({ ts: opts.ts }, "ts");
|
|
3550
|
+
for (const change of meters) {
|
|
3551
|
+
if (change.ts.numerator !== 4 || change.ts.denominator !== 4) {
|
|
3552
|
+
throw new InvalidInputError(
|
|
3553
|
+
`drum time signature must be 4/4 throughout; received ${change.ts.numerator}/${change.ts.denominator} at beat ${change.startBeat}`
|
|
3554
|
+
);
|
|
3555
|
+
}
|
|
3171
3556
|
}
|
|
3557
|
+
const ts = meterAt(0, meters);
|
|
3172
3558
|
const barBeats = beatsPerBar(ts);
|
|
3173
3559
|
const playMainVoices = role !== "fxOnly";
|
|
3174
3560
|
const section = mapSection(publicSection);
|
|
@@ -3182,7 +3568,6 @@ function generateDrums(opts) {
|
|
|
3182
3568
|
rhythmic,
|
|
3183
3569
|
ornament: ornamentDial,
|
|
3184
3570
|
difficulty,
|
|
3185
|
-
useGhostNotes: (section === "b" || section === "chorus" || section === "bridge") && style !== "sparse",
|
|
3186
3571
|
ghostBoost: mapping.ghostBoost,
|
|
3187
3572
|
useRide: shouldUseRideForSection(section, style),
|
|
3188
3573
|
useFootHh: shouldUseFootHiHat(section, role),
|
|
@@ -3253,7 +3638,14 @@ function generateDrums(opts) {
|
|
|
3253
3638
|
if (playMainVoices) {
|
|
3254
3639
|
const before = track.hits.length;
|
|
3255
3640
|
if (currentFill) {
|
|
3256
|
-
generateFill(
|
|
3641
|
+
generateFill(
|
|
3642
|
+
track,
|
|
3643
|
+
beatTick,
|
|
3644
|
+
beat,
|
|
3645
|
+
currentFill,
|
|
3646
|
+
fillVelocity,
|
|
3647
|
+
(tick) => swing16(tick, sec, swingAmount)
|
|
3648
|
+
);
|
|
3257
3649
|
}
|
|
3258
3650
|
if (track.hits.length > before) {
|
|
3259
3651
|
continue;
|
|
@@ -3297,7 +3689,7 @@ function generateDrums(opts) {
|
|
|
3297
3689
|
generateKickForBeat(ctx, sec, kick);
|
|
3298
3690
|
}
|
|
3299
3691
|
generateSnareForBeat(ctx, sec, section === "intro" && bar2 === 0);
|
|
3300
|
-
if (
|
|
3692
|
+
if (!inLift) {
|
|
3301
3693
|
generateGhostNotesForBeat(ctx, sec);
|
|
3302
3694
|
}
|
|
3303
3695
|
}
|
|
@@ -3360,7 +3752,7 @@ function playableOn(kit, pitch) {
|
|
|
3360
3752
|
}
|
|
3361
3753
|
|
|
3362
3754
|
// src/generate/groove/index.ts
|
|
3363
|
-
var
|
|
3755
|
+
var DEFAULT_TS3 = { numerator: 4, denominator: 4 };
|
|
3364
3756
|
var DEFAULT_PART = "humanize";
|
|
3365
3757
|
var DEFAULT_GROOVE_SUBDIVISION = 4;
|
|
3366
3758
|
var DEFAULT_TIMING = 0.02;
|
|
@@ -3371,21 +3763,21 @@ var MAX_METRIC_WEIGHT = 3;
|
|
|
3371
3763
|
var MIN_VELOCITY = 1;
|
|
3372
3764
|
var MAX_VELOCITY = 127;
|
|
3373
3765
|
function humanize(events, opts = {}) {
|
|
3374
|
-
const
|
|
3375
|
-
|
|
3766
|
+
const asked = assertOptions(opts, "opts");
|
|
3767
|
+
const ts = meterAt(0, toMeterData(asked.ts ?? DEFAULT_TS3, "ts"));
|
|
3376
3768
|
assertNoteEvents(events, "humanize events", { allowNonPositiveDuration: true });
|
|
3377
|
-
const
|
|
3378
|
-
const timing =
|
|
3379
|
-
const velocityJitter =
|
|
3380
|
-
const accent2 =
|
|
3381
|
-
const baseVelocity =
|
|
3769
|
+
const sounding2 = dropSilentNotes(events);
|
|
3770
|
+
const timing = asked.timing ?? DEFAULT_TIMING;
|
|
3771
|
+
const velocityJitter = asked.velocity ?? DEFAULT_VELOCITY_JITTER;
|
|
3772
|
+
const accent2 = asked.accent ?? DEFAULT_ACCENT;
|
|
3773
|
+
const baseVelocity = asked.baseVelocity ?? DEFAULT_BASE_VELOCITY;
|
|
3382
3774
|
assertRange(timing, 0, Number.MAX_SAFE_INTEGER, "humanize timing");
|
|
3383
3775
|
assertRange(velocityJitter, 0, 127, "humanize velocity jitter");
|
|
3384
3776
|
assertRange(accent2, 0, 127, "humanize accent");
|
|
3385
3777
|
assertRange(baseVelocity, 0, 127, "humanize base velocity");
|
|
3386
|
-
const ctx = resolveContext(
|
|
3387
|
-
const draw = ctx.part(
|
|
3388
|
-
return
|
|
3778
|
+
const ctx = resolveContext(asked.ctx);
|
|
3779
|
+
const draw = ctx.part(asked.part ?? DEFAULT_PART);
|
|
3780
|
+
return sounding2.map((event) => {
|
|
3389
3781
|
const timingOffset = draw.float(-timing, timing, "timing", event.startBeat, event.pitch);
|
|
3390
3782
|
const startBeat = event.startBeat + timingOffset;
|
|
3391
3783
|
const weight = metricWeight(event.startBeat, ts);
|
|
@@ -3399,12 +3791,7 @@ function humanize(events, opts = {}) {
|
|
|
3399
3791
|
);
|
|
3400
3792
|
const rawVelocity = (event.velocity ?? baseVelocity) + accentBoost + velocityOffset;
|
|
3401
3793
|
const velocity = Math.min(MAX_VELOCITY, Math.max(MIN_VELOCITY, Math.round(rawVelocity)));
|
|
3402
|
-
return {
|
|
3403
|
-
pitch: event.pitch,
|
|
3404
|
-
startBeat,
|
|
3405
|
-
durationBeat: event.durationBeat,
|
|
3406
|
-
velocity
|
|
3407
|
-
};
|
|
3794
|
+
return { ...event, startBeat, velocity };
|
|
3408
3795
|
});
|
|
3409
3796
|
}
|
|
3410
3797
|
function quantizeToGrid(beat, barBeats, subdivision, slotsPerBar) {
|
|
@@ -3422,10 +3809,10 @@ function quantizeToGrid(beat, barBeats, subdivision, slotsPerBar) {
|
|
|
3422
3809
|
};
|
|
3423
3810
|
}
|
|
3424
3811
|
function extractGrooveTemplate(events, ts, subdivision = DEFAULT_GROOVE_SUBDIVISION) {
|
|
3425
|
-
|
|
3812
|
+
const meter = meterAt(0, toMeterData(ts, "ts"));
|
|
3426
3813
|
assertNoteEvents(events, "groove source events", { allowNonPositiveDuration: true });
|
|
3427
3814
|
assertPositiveInt(subdivision, "groove subdivision");
|
|
3428
|
-
const barBeats = beatsPerBar(
|
|
3815
|
+
const barBeats = beatsPerBar(meter);
|
|
3429
3816
|
const slotsPerBar = Math.ceil(barBeats * subdivision);
|
|
3430
3817
|
assertPositiveInt(slotsPerBar, "groove template slots");
|
|
3431
3818
|
assertGenerationBudget(slotsPerBar, "groove template slots");
|
|
@@ -3460,10 +3847,10 @@ function extractGrooveTemplate(events, ts, subdivision = DEFAULT_GROOVE_SUBDIVIS
|
|
|
3460
3847
|
velocity: velocityCount > 0 ? (velocitySums[i] ?? 0) / velocityCount : null
|
|
3461
3848
|
});
|
|
3462
3849
|
}
|
|
3463
|
-
return { subdivision, slotsPerBar, slots, ts };
|
|
3850
|
+
return { subdivision, slotsPerBar, slots, ts: meter };
|
|
3464
3851
|
}
|
|
3465
3852
|
function applyGrooveTemplate(events, template, ts) {
|
|
3466
|
-
|
|
3853
|
+
const meter = meterAt(0, toMeterData(ts, "ts"));
|
|
3467
3854
|
assertNoteEvents(events, "groove target events", { allowNonPositiveDuration: true });
|
|
3468
3855
|
assertPositiveInt(template.subdivision, "template subdivision");
|
|
3469
3856
|
assertPositiveInt(template.slotsPerBar, "template slotsPerBar");
|
|
@@ -3471,12 +3858,28 @@ function applyGrooveTemplate(events, template, ts) {
|
|
|
3471
3858
|
if (template.slots.length !== template.slotsPerBar) {
|
|
3472
3859
|
throw new InvalidInputError("template slots length must equal slotsPerBar");
|
|
3473
3860
|
}
|
|
3474
|
-
if (template.ts && (template.ts.numerator !==
|
|
3861
|
+
if (template.ts && (template.ts.numerator !== meter.numerator || template.ts.denominator !== meter.denominator)) {
|
|
3475
3862
|
throw new InvalidInputError(
|
|
3476
|
-
`Groove template meter ${formatTimeSignature(template.ts)} does not match apply-time meter ${formatTimeSignature(
|
|
3863
|
+
`Groove template meter ${formatTimeSignature(template.ts)} does not match apply-time meter ${formatTimeSignature(meter)}`
|
|
3477
3864
|
);
|
|
3478
3865
|
}
|
|
3479
|
-
const barBeats = beatsPerBar(
|
|
3866
|
+
const barBeats = beatsPerBar(meter);
|
|
3867
|
+
const expectedSlots = Math.ceil(barBeats * template.subdivision);
|
|
3868
|
+
if (template.slotsPerBar !== expectedSlots) {
|
|
3869
|
+
throw new InvalidInputError(
|
|
3870
|
+
`Groove template slotsPerBar ${template.slotsPerBar} does not match ${expectedSlots} slots per bar of ${formatTimeSignature(meter)} at subdivision ${template.subdivision}`
|
|
3871
|
+
);
|
|
3872
|
+
}
|
|
3873
|
+
for (let index = 0; index < template.slots.length; index += 1) {
|
|
3874
|
+
const slot = template.slots[index];
|
|
3875
|
+
if (slot === void 0) {
|
|
3876
|
+
continue;
|
|
3877
|
+
}
|
|
3878
|
+
assertFiniteNumber(slot.timingOffset, `template slots[${index}].timingOffset`);
|
|
3879
|
+
if (slot.velocity !== null && slot.velocity !== void 0) {
|
|
3880
|
+
assertRange(slot.velocity, 0, 127, `template slots[${index}].velocity`);
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3480
3883
|
return dropSilentNotes(events).map((event) => {
|
|
3481
3884
|
const { quantizedBeat, slotIndex } = quantizeToGrid(
|
|
3482
3885
|
event.startBeat,
|
|
@@ -3487,20 +3890,14 @@ function applyGrooveTemplate(events, template, ts) {
|
|
|
3487
3890
|
const slot = template.slots[slotIndex];
|
|
3488
3891
|
const startBeat = quantizedBeat + (slot?.timingOffset ?? 0);
|
|
3489
3892
|
const velocity = slot && slot.velocity !== null && slot.velocity !== void 0 ? Math.round(slot.velocity) : event.velocity;
|
|
3490
|
-
return {
|
|
3491
|
-
pitch: event.pitch,
|
|
3492
|
-
startBeat,
|
|
3493
|
-
durationBeat: event.durationBeat,
|
|
3494
|
-
velocity
|
|
3495
|
-
};
|
|
3893
|
+
return { ...event, startBeat, velocity };
|
|
3496
3894
|
});
|
|
3497
3895
|
}
|
|
3498
3896
|
|
|
3499
3897
|
// src/generate/harmonize/nct.ts
|
|
3500
|
-
var ADJACENCY_TOLERANCE = 0.05;
|
|
3501
3898
|
function snapToPulse(beat, pulse2) {
|
|
3502
3899
|
const snapped = Math.round(beat / pulse2) * pulse2;
|
|
3503
|
-
return Math.abs(snapped - beat) <=
|
|
3900
|
+
return Math.abs(snapped - beat) <= HUMANIZE_ADJACENCY ? snapped : beat;
|
|
3504
3901
|
}
|
|
3505
3902
|
var STEP_SEMITONES = 2;
|
|
3506
3903
|
function isStep(a, b) {
|
|
@@ -3514,11 +3911,11 @@ function sameDirection(prev, note2, next) {
|
|
|
3514
3911
|
return prev !== note2 && next !== note2 && note2 - prev > 0 === next - note2 > 0;
|
|
3515
3912
|
}
|
|
3516
3913
|
function classifyMelodyTones(melody, ts) {
|
|
3914
|
+
const meter = meterAt(0, toMeterData(ts, "ts"));
|
|
3517
3915
|
assertNoteEvents(melody, "melody notes", { allowNonPositiveDuration: true });
|
|
3518
|
-
|
|
3519
|
-
const pulse2 = pulseBeats(ts);
|
|
3916
|
+
const pulse2 = pulseBeats(meter);
|
|
3520
3917
|
return melody.map((_, index) => {
|
|
3521
|
-
const role = roleOfNote(melody, index,
|
|
3918
|
+
const role = roleOfNote(melody, index, meter, pulse2);
|
|
3522
3919
|
return { noteIndex: index, role, ornamental: role !== "structural" };
|
|
3523
3920
|
});
|
|
3524
3921
|
}
|
|
@@ -3531,7 +3928,7 @@ function roleOfNote(melody, index, ts, pulse2) {
|
|
|
3531
3928
|
}
|
|
3532
3929
|
const intoNote = Math.abs(prev.startBeat + prev.durationBeat - note2.startBeat);
|
|
3533
3930
|
const outOfNote = Math.abs(note2.startBeat + note2.durationBeat - next.startBeat);
|
|
3534
|
-
if (intoNote >
|
|
3931
|
+
if (intoNote > HUMANIZE_ADJACENCY || outOfNote > HUMANIZE_ADJACENCY) {
|
|
3535
3932
|
return "structural";
|
|
3536
3933
|
}
|
|
3537
3934
|
const weight = metricWeight(snapToPulse(note2.startBeat, pulse2), ts);
|
|
@@ -3562,7 +3959,7 @@ function roleOfNote(melody, index, ts, pulse2) {
|
|
|
3562
3959
|
return "structural";
|
|
3563
3960
|
}
|
|
3564
3961
|
|
|
3565
|
-
// src/generate/harmonize/
|
|
3962
|
+
// src/generate/harmonize/internal.ts
|
|
3566
3963
|
var FALLBACK = {
|
|
3567
3964
|
rootPc: 0,
|
|
3568
3965
|
quality: "maj",
|
|
@@ -3575,36 +3972,17 @@ function defaultHarmonicRhythm(ts) {
|
|
|
3575
3972
|
const half = bar2 / 2;
|
|
3576
3973
|
return half % pulseBeats(ts) === 0 ? half : bar2;
|
|
3577
3974
|
}
|
|
3578
|
-
var DEFAULT_PLACEMENT = {
|
|
3975
|
+
var DEFAULT_PLACEMENT = {
|
|
3976
|
+
transposeSearch: false,
|
|
3977
|
+
octaveSearch: false
|
|
3978
|
+
};
|
|
3579
3979
|
var DEFAULT_METER = { numerator: 4, denominator: 4 };
|
|
3580
|
-
var COMFORT_LOW = 55;
|
|
3581
|
-
var COMFORT_HIGH = 79;
|
|
3582
|
-
var TESSITURA_WEIGHT = 1e-3;
|
|
3583
|
-
var NON_CHORD_TONE_STRONG = 4;
|
|
3584
|
-
var NON_CHORD_TONE_WEAK = 1;
|
|
3585
|
-
var NON_SCALE_TONE = 0.5;
|
|
3586
|
-
var DEGREE_BASE = [0, 0.3, 0.45, 0.15, 0.1, 0.3, 0.9];
|
|
3587
|
-
var SECONDARY_DOMINANT_BASE = 0.5;
|
|
3588
|
-
var BORROWED_BASE = 0.5;
|
|
3589
|
-
var CHORD_CHANGE = 0.8;
|
|
3590
|
-
var DESCENDING_FIFTH = -0.4;
|
|
3591
|
-
var ASCENDING_FIFTH = 0;
|
|
3592
|
-
var DOMINANT_TO_TONIC = -0.3;
|
|
3593
|
-
var SECONDARY_UNRESOLVED = 1;
|
|
3594
|
-
var SECONDARY_AFTER_TARGET = 0.8;
|
|
3595
|
-
var ROOT_MOTION_COST = [0, 0.15, 0.15, 0.2, 0.2, 0, 0.4];
|
|
3596
|
-
var PHRASE_TONIC = -7;
|
|
3597
|
-
var PHRASE_AUTHENTIC = -0.5;
|
|
3598
|
-
var OUT_OF_KEY_PER_BEAT = 3;
|
|
3599
3980
|
var TIE_BREAK_JITTER2 = 1e-6;
|
|
3600
|
-
function
|
|
3601
|
-
return (key
|
|
3602
|
-
}
|
|
3603
|
-
function leadingTonePc(key) {
|
|
3604
|
-
return (pitchClassOf(key.rootPc) + 11) % 12;
|
|
3981
|
+
function cadencesThroughRaisedSeventh(key) {
|
|
3982
|
+
return isMinorKey(key) && scaleSystemOf(key) === "common-practice";
|
|
3605
3983
|
}
|
|
3606
3984
|
function isKeyTone(pitch, key) {
|
|
3607
|
-
return isScaleTone(pitch, key) ||
|
|
3985
|
+
return isScaleTone(pitch, key) || cadencesThroughRaisedSeventh(key) && pitchClassOf(pitch) === leadingTonePcOf(key);
|
|
3608
3986
|
}
|
|
3609
3987
|
function inferKey(melody) {
|
|
3610
3988
|
let best = majorKey(0);
|
|
@@ -3633,93 +4011,21 @@ function inferKey(melody) {
|
|
|
3633
4011
|
}
|
|
3634
4012
|
return best;
|
|
3635
4013
|
}
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
var
|
|
3639
|
-
var
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
const quality = diatonicTriad(degree, key).quality;
|
|
3652
|
-
return {
|
|
3653
|
-
rootPc,
|
|
3654
|
-
quality,
|
|
3655
|
-
degree,
|
|
3656
|
-
secondaryDominant: false,
|
|
3657
|
-
base: DEGREE_BASE[index] ?? 0.6,
|
|
3658
|
-
pcs: chordPitchClasses(makeChord(rootPc, quality))
|
|
3659
|
-
};
|
|
3660
|
-
});
|
|
3661
|
-
if (isMinorKey(key)) {
|
|
3662
|
-
const rootPc = (pitchClassOf(key.rootPc) + 7) % 12;
|
|
3663
|
-
candidates.push({
|
|
3664
|
-
rootPc,
|
|
3665
|
-
quality: "maj",
|
|
3666
|
-
degree: 5,
|
|
3667
|
-
secondaryDominant: false,
|
|
3668
|
-
base: DEGREE_BASE[4] ?? 0.6,
|
|
3669
|
-
pcs: chordPitchClasses(makeChord(rootPc, "maj"))
|
|
3670
|
-
});
|
|
3671
|
-
}
|
|
3672
|
-
const secondaryOpen = admittedCount(
|
|
3673
|
-
harmonic / SECONDARY_DOMINANT_BAND,
|
|
3674
|
-
SECONDARY_DOMINANT_TARGETS.length
|
|
3675
|
-
);
|
|
3676
|
-
for (const target of SECONDARY_DOMINANT_TARGETS) {
|
|
3677
|
-
if (SECONDARY_DOMINANT_ENTRY_ORDER.indexOf(target) >= secondaryOpen) {
|
|
3678
|
-
continue;
|
|
3679
|
-
}
|
|
3680
|
-
const targetRoot = tones[target - 1] ?? 0;
|
|
3681
|
-
const rootPc = (targetRoot + 7) % 12;
|
|
3682
|
-
candidates.push({
|
|
3683
|
-
rootPc,
|
|
3684
|
-
quality: "dom7",
|
|
3685
|
-
secondaryDominant: true,
|
|
3686
|
-
targetDegree: target,
|
|
3687
|
-
base: SECONDARY_DOMINANT_BASE,
|
|
3688
|
-
pcs: chordPitchClasses(makeChord(rootPc, "dom7"))
|
|
3689
|
-
});
|
|
3690
|
-
}
|
|
3691
|
-
const parallel = parallelKey(key);
|
|
3692
|
-
const parallelTones = scaleTonesInDegreeOrder(parallel);
|
|
3693
|
-
const borrowed = [];
|
|
3694
|
-
for (let degree = 1; degree <= parallelTones.length; degree += 1) {
|
|
3695
|
-
const chord = diatonicTriad(degree, parallel);
|
|
3696
|
-
if (!isDiatonic(chord, key)) {
|
|
3697
|
-
borrowed.push(chord);
|
|
3698
|
-
}
|
|
3699
|
-
}
|
|
3700
|
-
const borrowedOpen = admittedCount(
|
|
3701
|
-
(harmonic - SECONDARY_DOMINANT_BAND) / (1 - SECONDARY_DOMINANT_BAND),
|
|
3702
|
-
borrowed.length
|
|
3703
|
-
);
|
|
3704
|
-
for (const chord of borrowed.slice(0, borrowedOpen)) {
|
|
3705
|
-
candidates.push({
|
|
3706
|
-
rootPc: chord.rootPc,
|
|
3707
|
-
quality: chord.quality,
|
|
3708
|
-
secondaryDominant: false,
|
|
3709
|
-
base: BORROWED_BASE,
|
|
3710
|
-
pcs: chordPitchClasses(chord)
|
|
3711
|
-
});
|
|
3712
|
-
}
|
|
3713
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3714
|
-
return candidates.filter((candidate) => {
|
|
3715
|
-
const id = `${candidate.rootPc}:${candidate.quality}`;
|
|
3716
|
-
if (seen.has(id)) {
|
|
3717
|
-
return false;
|
|
3718
|
-
}
|
|
3719
|
-
seen.add(id);
|
|
3720
|
-
return true;
|
|
3721
|
-
});
|
|
3722
|
-
}
|
|
4014
|
+
|
|
4015
|
+
// src/generate/harmonize/cost.ts
|
|
4016
|
+
var NON_CHORD_TONE_STRONG = 4;
|
|
4017
|
+
var NON_CHORD_TONE_WEAK = 1;
|
|
4018
|
+
var NON_SCALE_TONE = 0.5;
|
|
4019
|
+
var CHORD_CHANGE = 0.8;
|
|
4020
|
+
var DESCENDING_FIFTH = -0.4;
|
|
4021
|
+
var ASCENDING_FIFTH = 0;
|
|
4022
|
+
var DOMINANT_TO_TONIC = -0.3;
|
|
4023
|
+
var SECONDARY_UNRESOLVED = 1;
|
|
4024
|
+
var SECONDARY_AFTER_TARGET = 0.8;
|
|
4025
|
+
var ROOT_MOTION_COST = [0, 0.15, 0.15, 0.2, 0.2, 0, 0.4];
|
|
4026
|
+
var PHRASE_TONIC = -7;
|
|
4027
|
+
var PHRASE_AUTHENTIC = -0.5;
|
|
4028
|
+
var OUT_OF_KEY_PER_BEAT = 3;
|
|
3723
4029
|
function emissionCost(seg, cand, melody, key) {
|
|
3724
4030
|
const pcs = cand.pcs;
|
|
3725
4031
|
let cost = cand.base * seg.weight;
|
|
@@ -3735,40 +4041,9 @@ function emissionCost(seg, cand, melody, key) {
|
|
|
3735
4041
|
}
|
|
3736
4042
|
return cost;
|
|
3737
4043
|
}
|
|
3738
|
-
function tessituraCost(melody) {
|
|
3739
|
-
let cost = 0;
|
|
3740
|
-
for (const n of melody) {
|
|
3741
|
-
if (n.pitch < COMFORT_LOW) {
|
|
3742
|
-
cost += (COMFORT_LOW - n.pitch) * TESSITURA_WEIGHT;
|
|
3743
|
-
} else if (n.pitch > COMFORT_HIGH) {
|
|
3744
|
-
cost += (n.pitch - COMFORT_HIGH) * TESSITURA_WEIGHT;
|
|
3745
|
-
}
|
|
3746
|
-
}
|
|
3747
|
-
return cost;
|
|
3748
|
-
}
|
|
3749
4044
|
function isDominantOf(cand, tonicPc) {
|
|
3750
4045
|
return cand.rootPc === (tonicPc + 7) % 12 && (cand.quality === "maj" || cand.quality === "dom7");
|
|
3751
4046
|
}
|
|
3752
|
-
function keyFitCost(melody, key) {
|
|
3753
|
-
let cost = 0;
|
|
3754
|
-
for (const n of melody) {
|
|
3755
|
-
if (!isKeyTone(n.pitch, key)) {
|
|
3756
|
-
cost += Math.max(0.25, n.durationBeat) * OUT_OF_KEY_PER_BEAT;
|
|
3757
|
-
}
|
|
3758
|
-
}
|
|
3759
|
-
return cost;
|
|
3760
|
-
}
|
|
3761
|
-
function mergeRepeats(spans) {
|
|
3762
|
-
const merged = [];
|
|
3763
|
-
for (const span of spans) {
|
|
3764
|
-
const prev = merged.at(-1);
|
|
3765
|
-
if (prev && prev.rootPc === span.rootPc && prev.quality === span.quality && prev.degree === span.degree && prev.secondaryDominant === span.secondaryDominant) {
|
|
3766
|
-
continue;
|
|
3767
|
-
}
|
|
3768
|
-
merged.push(span);
|
|
3769
|
-
}
|
|
3770
|
-
return merged;
|
|
3771
|
-
}
|
|
3772
4047
|
function transitionCost(prev, cur, tonicPc) {
|
|
3773
4048
|
let cost = prev.rootPc === cur.rootPc && prev.quality === cur.quality ? 0 : CHORD_CHANGE;
|
|
3774
4049
|
const down = (prev.rootPc - cur.rootPc + 12) % 12;
|
|
@@ -3801,7 +4076,92 @@ function phraseEndBonus(prev, cur, tonicPc, seg, melody) {
|
|
|
3801
4076
|
const weight = Math.max(seg.weight, seg.beats);
|
|
3802
4077
|
return (PHRASE_TONIC + (prev !== null && isDominantOf(prev, tonicPc) ? PHRASE_AUTHENTIC : 0)) * weight;
|
|
3803
4078
|
}
|
|
3804
|
-
|
|
4079
|
+
|
|
4080
|
+
// src/generate/harmonize/key-fit.ts
|
|
4081
|
+
var COMFORT_LOW = 55;
|
|
4082
|
+
var COMFORT_HIGH = 79;
|
|
4083
|
+
var TESSITURA_WEIGHT = 1e-3;
|
|
4084
|
+
function tessituraCost(melody) {
|
|
4085
|
+
let cost = 0;
|
|
4086
|
+
for (const n of melody) {
|
|
4087
|
+
if (n.pitch < COMFORT_LOW) {
|
|
4088
|
+
cost += (COMFORT_LOW - n.pitch) * TESSITURA_WEIGHT;
|
|
4089
|
+
} else if (n.pitch > COMFORT_HIGH) {
|
|
4090
|
+
cost += (n.pitch - COMFORT_HIGH) * TESSITURA_WEIGHT;
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
return cost;
|
|
4094
|
+
}
|
|
4095
|
+
function keyFitCost(melody, key) {
|
|
4096
|
+
let cost = 0;
|
|
4097
|
+
for (const n of melody) {
|
|
4098
|
+
if (!isKeyTone(n.pitch, key)) {
|
|
4099
|
+
cost += Math.max(0.25, n.durationBeat) * OUT_OF_KEY_PER_BEAT;
|
|
4100
|
+
}
|
|
4101
|
+
}
|
|
4102
|
+
return cost;
|
|
4103
|
+
}
|
|
4104
|
+
|
|
4105
|
+
// src/generate/harmonize/search.ts
|
|
4106
|
+
function sameHarmony(a, b) {
|
|
4107
|
+
return a.rootPc === b.rootPc && a.quality === b.quality;
|
|
4108
|
+
}
|
|
4109
|
+
function harmonizeOnce(melody, key, candidates, segments, jitter, phraseEnds, articulated) {
|
|
4110
|
+
const tonicPc = pitchClassOf(key.rootPc);
|
|
4111
|
+
const n = candidates.length;
|
|
4112
|
+
const candAt = (i) => candidates[i] ?? FALLBACK;
|
|
4113
|
+
const seg0 = segments[0];
|
|
4114
|
+
let dp = candidates.map(
|
|
4115
|
+
(c, ci) => (seg0 ? emissionCost(seg0, c, melody, key) : 0) + (seg0 && phraseEnds.has(0) ? phraseEndBonus(null, c, tonicPc, seg0, melody) : 0) + (jitter[ci] ?? 0)
|
|
4116
|
+
);
|
|
4117
|
+
const back = [];
|
|
4118
|
+
for (let s = 1; s < segments.length; s += 1) {
|
|
4119
|
+
const seg = segments[s];
|
|
4120
|
+
if (!seg) {
|
|
4121
|
+
continue;
|
|
4122
|
+
}
|
|
4123
|
+
const closes = phraseEnds.has(s);
|
|
4124
|
+
const articulates = articulated.has(s);
|
|
4125
|
+
const next = [];
|
|
4126
|
+
const ptr = [];
|
|
4127
|
+
for (let c = 0; c < n; c += 1) {
|
|
4128
|
+
let best = Number.POSITIVE_INFINITY;
|
|
4129
|
+
let bestPrev = 0;
|
|
4130
|
+
for (let p = 0; p < n; p += 1) {
|
|
4131
|
+
if (articulates && sameHarmony(candAt(p), candAt(c))) {
|
|
4132
|
+
continue;
|
|
4133
|
+
}
|
|
4134
|
+
const cost = (dp[p] ?? Number.POSITIVE_INFINITY) + transitionCost(candAt(p), candAt(c), tonicPc) * seg.beats + (closes ? phraseEndBonus(candAt(p), candAt(c), tonicPc, seg, melody) : 0);
|
|
4135
|
+
if (cost < best) {
|
|
4136
|
+
best = cost;
|
|
4137
|
+
bestPrev = p;
|
|
4138
|
+
}
|
|
4139
|
+
}
|
|
4140
|
+
next[c] = best + emissionCost(seg, candAt(c), melody, key) + (jitter[c] ?? 0);
|
|
4141
|
+
ptr[c] = bestPrev;
|
|
4142
|
+
}
|
|
4143
|
+
dp = next;
|
|
4144
|
+
back.push(ptr);
|
|
4145
|
+
}
|
|
4146
|
+
let bestCost = Number.POSITIVE_INFINITY;
|
|
4147
|
+
let bestEnd = 0;
|
|
4148
|
+
for (let c = 0; c < n; c += 1) {
|
|
4149
|
+
const total = dp[c] ?? Number.POSITIVE_INFINITY;
|
|
4150
|
+
if (total < bestCost) {
|
|
4151
|
+
bestCost = total;
|
|
4152
|
+
bestEnd = c;
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
4155
|
+
const path = new Array(segments.length).fill(bestEnd);
|
|
4156
|
+
for (let s = segments.length - 2; s >= 0; s -= 1) {
|
|
4157
|
+
const ptr = back[s];
|
|
4158
|
+
const nextIdx = path[s + 1] ?? bestEnd;
|
|
4159
|
+
path[s] = ptr ? ptr[nextIdx] ?? bestEnd : bestEnd;
|
|
4160
|
+
}
|
|
4161
|
+
return { cost: bestCost, path };
|
|
4162
|
+
}
|
|
4163
|
+
|
|
4164
|
+
// src/generate/harmonize/segments.ts
|
|
3805
4165
|
function gridBounds(segmentStart, hr, gridCount, ends) {
|
|
3806
4166
|
const bounds = [];
|
|
3807
4167
|
for (let s = 0; s <= gridCount; s += 1) {
|
|
@@ -3850,8 +4210,16 @@ function segmentIndexAt(bounds, beat) {
|
|
|
3850
4210
|
}
|
|
3851
4211
|
return found;
|
|
3852
4212
|
}
|
|
3853
|
-
function
|
|
3854
|
-
|
|
4213
|
+
function mergeRepeats(spans) {
|
|
4214
|
+
const merged = [];
|
|
4215
|
+
for (const span of spans) {
|
|
4216
|
+
const prev = merged.at(-1);
|
|
4217
|
+
if (prev && prev.rootPc === span.rootPc && prev.quality === span.quality && prev.degree === span.degree && prev.secondaryDominant === span.secondaryDominant) {
|
|
4218
|
+
continue;
|
|
4219
|
+
}
|
|
4220
|
+
merged.push(span);
|
|
4221
|
+
}
|
|
4222
|
+
return merged;
|
|
3855
4223
|
}
|
|
3856
4224
|
function phraseEndSegments(segments, ends) {
|
|
3857
4225
|
const closing = new Set(segments.length > 0 ? [segments.length - 1] : []);
|
|
@@ -3870,103 +4238,19 @@ function phraseEndSegments(segments, ends) {
|
|
|
3870
4238
|
}
|
|
3871
4239
|
return { closing, named };
|
|
3872
4240
|
}
|
|
3873
|
-
function
|
|
3874
|
-
const
|
|
3875
|
-
const
|
|
3876
|
-
const
|
|
3877
|
-
const seg0 = segments[0];
|
|
3878
|
-
let dp = candidates.map(
|
|
3879
|
-
(c, ci) => (seg0 ? emissionCost(seg0, c, melody, key) : 0) + (seg0 && phraseEnds.has(0) ? phraseEndBonus(null, c, tonicPc, seg0, melody) : 0) + (jitter[ci] ?? 0)
|
|
3880
|
-
);
|
|
3881
|
-
const back = [];
|
|
3882
|
-
for (let s = 1; s < segments.length; s += 1) {
|
|
3883
|
-
const seg = segments[s];
|
|
3884
|
-
if (!seg) {
|
|
3885
|
-
continue;
|
|
3886
|
-
}
|
|
3887
|
-
const closes = phraseEnds.has(s);
|
|
3888
|
-
const articulates = articulated.has(s);
|
|
3889
|
-
const next = [];
|
|
3890
|
-
const ptr = [];
|
|
3891
|
-
for (let c = 0; c < n; c += 1) {
|
|
3892
|
-
let best = Number.POSITIVE_INFINITY;
|
|
3893
|
-
let bestPrev = 0;
|
|
3894
|
-
for (let p = 0; p < n; p += 1) {
|
|
3895
|
-
if (articulates && sameHarmony(candAt(p), candAt(c))) {
|
|
3896
|
-
continue;
|
|
3897
|
-
}
|
|
3898
|
-
const cost = (dp[p] ?? Number.POSITIVE_INFINITY) + transitionCost(candAt(p), candAt(c), tonicPc) * seg.beats + (closes ? phraseEndBonus(candAt(p), candAt(c), tonicPc, seg, melody) : 0);
|
|
3899
|
-
if (cost < best) {
|
|
3900
|
-
best = cost;
|
|
3901
|
-
bestPrev = p;
|
|
3902
|
-
}
|
|
3903
|
-
}
|
|
3904
|
-
next[c] = best + emissionCost(seg, candAt(c), melody, key) + (jitter[c] ?? 0);
|
|
3905
|
-
ptr[c] = bestPrev;
|
|
3906
|
-
}
|
|
3907
|
-
dp = next;
|
|
3908
|
-
back.push(ptr);
|
|
3909
|
-
}
|
|
3910
|
-
let bestCost = Number.POSITIVE_INFINITY;
|
|
3911
|
-
let bestEnd = 0;
|
|
3912
|
-
for (let c = 0; c < n; c += 1) {
|
|
3913
|
-
const total = dp[c] ?? Number.POSITIVE_INFINITY;
|
|
3914
|
-
if (total < bestCost) {
|
|
3915
|
-
bestCost = total;
|
|
3916
|
-
bestEnd = c;
|
|
3917
|
-
}
|
|
3918
|
-
}
|
|
3919
|
-
const path = new Array(segments.length).fill(bestEnd);
|
|
3920
|
-
for (let s = segments.length - 2; s >= 0; s -= 1) {
|
|
3921
|
-
const ptr = back[s];
|
|
3922
|
-
const nextIdx = path[s + 1] ?? bestEnd;
|
|
3923
|
-
path[s] = ptr ? ptr[nextIdx] ?? bestEnd : bestEnd;
|
|
3924
|
-
}
|
|
3925
|
-
return { cost: bestCost, path };
|
|
3926
|
-
}
|
|
3927
|
-
function harmonizeMelody(opts) {
|
|
3928
|
-
assertNoteEvents(opts.melody, "harmonize melody", { allowNonPositiveDuration: true });
|
|
3929
|
-
const phraseEnds = opts.phraseEnds ?? [];
|
|
3930
|
-
for (const end of phraseEnds) {
|
|
3931
|
-
assertFiniteNumber(end, "harmonize phrase end");
|
|
3932
|
-
}
|
|
3933
|
-
const soundingMelody = soundingNotesOnly(opts.melody);
|
|
3934
|
-
const ts = opts.ts ?? DEFAULT_METER;
|
|
3935
|
-
assertTimeSignature(ts);
|
|
3936
|
-
const requestedKey = opts.key ?? "infer";
|
|
3937
|
-
const key = requestedKey === "infer" ? inferKey(soundingMelody) : toKeyScale(requestedKey);
|
|
3938
|
-
const placement = opts.placement ?? DEFAULT_PLACEMENT;
|
|
3939
|
-
const ctx = resolveContext(opts.ctx);
|
|
3940
|
-
const harmonic = ctx.harmonic ?? REHARMONIZE_DIAL[opts.reharmonize ?? "diatonic"];
|
|
3941
|
-
if (soundingMelody.length === 0) {
|
|
3942
|
-
return { transposeSemitones: 0, key, chords: [], melodyRoles: [] };
|
|
3943
|
-
}
|
|
3944
|
-
const candidates = buildCandidates(key, harmonic);
|
|
3945
|
-
const candAt = (i) => candidates[i] ?? FALLBACK;
|
|
3946
|
-
const draw = ctx.part("harmony");
|
|
3947
|
-
const jitter = candidates.map((_, index) => draw.at("jitter", index) * TIE_BREAK_JITTER2);
|
|
3948
|
-
const pulse2 = pulseBeats(ts);
|
|
3949
|
-
const spans = soundingMelody.map((note2) => ({
|
|
3950
|
-
startBeat: snapToPulse(note2.startBeat, pulse2),
|
|
3951
|
-
endBeat: snapToPulse(note2.startBeat + note2.durationBeat, pulse2)
|
|
3952
|
-
}));
|
|
3953
|
-
const melodyStart = spans.reduce(
|
|
3954
|
-
(start, span) => Math.min(start, span.startBeat),
|
|
3955
|
-
Number.POSITIVE_INFINITY
|
|
3956
|
-
);
|
|
3957
|
-
const melodyEnd = spans.reduce((end, span) => Math.max(end, span.endBeat), 0);
|
|
3958
|
-
const hr = assertRange(
|
|
3959
|
-
opts.harmonicRhythm ?? defaultHarmonicRhythm(ts),
|
|
3960
|
-
Number.MIN_VALUE,
|
|
3961
|
-
Number.MAX_SAFE_INTEGER,
|
|
3962
|
-
"harmonic rhythm"
|
|
3963
|
-
);
|
|
4241
|
+
function buildSegments(spans, melody, ts, grid, phraseEnds, budget) {
|
|
4242
|
+
const melodyStart = grid.start;
|
|
4243
|
+
const melodyEnd = grid.end;
|
|
4244
|
+
const hr = grid.harmonicRhythm;
|
|
3964
4245
|
const segmentStart = Math.floor(melodyStart / hr) * hr;
|
|
3965
4246
|
const gridCount = Math.max(1, Math.ceil((melodyEnd - segmentStart) / hr));
|
|
3966
|
-
assertGenerationBudget(gridCount, "harmonic segments");
|
|
3967
|
-
const
|
|
4247
|
+
assertGenerationBudget(gridCount, "harmonic segments", budget);
|
|
4248
|
+
const innerEnds = phraseEnds.filter(
|
|
4249
|
+
(end) => end > segmentStart + BEAT_EPS && end < melodyEnd - BEAT_EPS
|
|
4250
|
+
);
|
|
4251
|
+
const bounds = gridBounds(segmentStart, hr, gridCount, innerEnds);
|
|
3968
4252
|
const segCount = bounds.length - 1;
|
|
3969
|
-
assertGenerationBudget(segCount, "harmonic segments");
|
|
4253
|
+
assertGenerationBudget(segCount, "harmonic segments", budget);
|
|
3970
4254
|
const segments = Array.from({ length: segCount }, (_, s) => {
|
|
3971
4255
|
const startBeat = bounds[s] ?? segmentStart;
|
|
3972
4256
|
const endBeat = bounds[s + 1] ?? startBeat + hr;
|
|
@@ -3987,13 +4271,13 @@ function harmonizeMelody(opts) {
|
|
|
3987
4271
|
lastExclusive += 1;
|
|
3988
4272
|
}
|
|
3989
4273
|
memberships += Math.max(0, lastExclusive - first);
|
|
3990
|
-
assertGenerationBudget(memberships, "note-to-segment memberships");
|
|
4274
|
+
assertGenerationBudget(memberships, "note-to-segment memberships", budget);
|
|
3991
4275
|
for (let segment = first; segment < lastExclusive; segment += 1) {
|
|
3992
4276
|
segments[segment]?.noteIndices.push(index);
|
|
3993
4277
|
}
|
|
3994
4278
|
}
|
|
3995
|
-
const tones = classifyMelodyTones(
|
|
3996
|
-
const phraseClosingNotes = closingNoteIndices(spans,
|
|
4279
|
+
const tones = classifyMelodyTones(melody, ts);
|
|
4280
|
+
const phraseClosingNotes = closingNoteIndices(spans, innerEnds);
|
|
3997
4281
|
for (const segment of segments) {
|
|
3998
4282
|
const structural = segment.noteIndices.filter(
|
|
3999
4283
|
(idx) => tones[idx]?.ornamental !== true || phraseClosingNotes.has(idx)
|
|
@@ -4019,9 +4303,200 @@ function harmonizeMelody(opts) {
|
|
|
4019
4303
|
}
|
|
4020
4304
|
}
|
|
4021
4305
|
}
|
|
4306
|
+
return { segments, innerEnds, bounds, segmentStart };
|
|
4307
|
+
}
|
|
4308
|
+
|
|
4309
|
+
// src/generate/harmonize/candidates.ts
|
|
4310
|
+
var ROOT_WEIGHT_BY_SEMITONE = [0, 0.9, 0.3, 0.45, 0.45, 0.15, 0.9, 0.1, 0.3, 0.3, 0.3, 0.9];
|
|
4311
|
+
var UNSTABLE_TRIAD = 0.9;
|
|
4312
|
+
var UNSTABLE_QUALITIES = /* @__PURE__ */ new Set([
|
|
4313
|
+
"dim",
|
|
4314
|
+
"aug",
|
|
4315
|
+
"dim7",
|
|
4316
|
+
"m7b5",
|
|
4317
|
+
"aug7"
|
|
4318
|
+
]);
|
|
4319
|
+
function vocabularyBase(rootPc, quality, tonicPc) {
|
|
4320
|
+
const above = (pitchClassOf(rootPc) - tonicPc + 12) % 12;
|
|
4321
|
+
const root = ROOT_WEIGHT_BY_SEMITONE[above] ?? UNSTABLE_TRIAD;
|
|
4322
|
+
return UNSTABLE_QUALITIES.has(quality) ? Math.max(root, UNSTABLE_TRIAD) : root;
|
|
4323
|
+
}
|
|
4324
|
+
var SECONDARY_DOMINANT_BASE = 0.5;
|
|
4325
|
+
var BORROWED_BASE = 0.5;
|
|
4326
|
+
var SECONDARY_DOMINANT_TARGETS = [2, 4, 5, 6];
|
|
4327
|
+
function secondaryDominantTargets(key) {
|
|
4328
|
+
const tonicizable = new Map(
|
|
4329
|
+
tonicizableDegrees(key).map((degree) => [degree.degreeNumber, degree])
|
|
4330
|
+
);
|
|
4331
|
+
return SECONDARY_DOMINANT_TARGETS.map((degree) => tonicizable.get(degree)).filter(
|
|
4332
|
+
(degree) => degree !== void 0
|
|
4333
|
+
);
|
|
4334
|
+
}
|
|
4335
|
+
var SECONDARY_DOMINANT_ENTRY_ORDER = [5, 6, 2, 4];
|
|
4336
|
+
var BORROWED_ENTRY_ORDER = [5, 10, 8, 3, 7, 2, 0];
|
|
4337
|
+
var SECONDARY_DOMINANT_BAND = 0.5;
|
|
4338
|
+
var REHARMONIZE_STRENGTHS = ["diatonic", "secondaryDominant", "borrowed"];
|
|
4339
|
+
var REHARMONIZE_DIAL = {
|
|
4340
|
+
diatonic: 0,
|
|
4341
|
+
secondaryDominant: SECONDARY_DOMINANT_BAND,
|
|
4342
|
+
borrowed: 1
|
|
4343
|
+
};
|
|
4344
|
+
function admittedCount(fraction, size) {
|
|
4345
|
+
return Math.max(0, Math.min(size, Math.ceil(fraction * size)));
|
|
4346
|
+
}
|
|
4347
|
+
function buildCandidates(key, harmonic) {
|
|
4348
|
+
const tonicPc = pitchClassOf(key.rootPc);
|
|
4349
|
+
const frame = heptatonicFrameOf(key);
|
|
4350
|
+
const tones = scaleTonesInDegreeOrder(frame);
|
|
4351
|
+
const candidates = tones.map((rootPc, index) => {
|
|
4352
|
+
const degree = index + 1;
|
|
4353
|
+
const quality = diatonicTriad(degree, frame).quality;
|
|
4354
|
+
return {
|
|
4355
|
+
rootPc,
|
|
4356
|
+
quality,
|
|
4357
|
+
degree,
|
|
4358
|
+
secondaryDominant: false,
|
|
4359
|
+
base: vocabularyBase(rootPc, quality, tonicPc),
|
|
4360
|
+
pcs: chordPitchClasses(makeChord(rootPc, quality))
|
|
4361
|
+
};
|
|
4362
|
+
});
|
|
4363
|
+
if (cadencesThroughRaisedSeventh(key)) {
|
|
4364
|
+
const rootPc = (tonicPc + 7) % 12;
|
|
4365
|
+
candidates.push({
|
|
4366
|
+
rootPc,
|
|
4367
|
+
quality: "maj",
|
|
4368
|
+
degree: 5,
|
|
4369
|
+
secondaryDominant: false,
|
|
4370
|
+
base: vocabularyBase(rootPc, "maj", tonicPc),
|
|
4371
|
+
pcs: chordPitchClasses(makeChord(rootPc, "maj"))
|
|
4372
|
+
});
|
|
4373
|
+
}
|
|
4374
|
+
const targets = secondaryDominantTargets(key);
|
|
4375
|
+
const entryOrder = SECONDARY_DOMINANT_ENTRY_ORDER.filter(
|
|
4376
|
+
(degree) => targets.some((target) => target.degreeNumber === degree)
|
|
4377
|
+
);
|
|
4378
|
+
const secondaryOpen = admittedCount(harmonic / SECONDARY_DOMINANT_BAND, targets.length);
|
|
4379
|
+
for (const target of targets) {
|
|
4380
|
+
if (entryOrder.indexOf(target.degreeNumber) >= secondaryOpen) {
|
|
4381
|
+
continue;
|
|
4382
|
+
}
|
|
4383
|
+
const rootPc = (target.rootPc + 7) % 12;
|
|
4384
|
+
candidates.push({
|
|
4385
|
+
rootPc,
|
|
4386
|
+
quality: "dom7",
|
|
4387
|
+
secondaryDominant: true,
|
|
4388
|
+
targetDegree: target.degreeNumber,
|
|
4389
|
+
base: SECONDARY_DOMINANT_BASE,
|
|
4390
|
+
pcs: chordPitchClasses(makeChord(rootPc, "dom7"))
|
|
4391
|
+
});
|
|
4392
|
+
}
|
|
4393
|
+
const parallel = parallelKey(key);
|
|
4394
|
+
const parallelTones = scaleTonesInDegreeOrder(parallel);
|
|
4395
|
+
const borrowed = [];
|
|
4396
|
+
for (let degree = 1; degree <= parallelTones.length; degree += 1) {
|
|
4397
|
+
const chord = diatonicTriad(degree, parallel);
|
|
4398
|
+
if (!isDiatonic(chord, key)) {
|
|
4399
|
+
borrowed.push(chord);
|
|
4400
|
+
}
|
|
4401
|
+
}
|
|
4402
|
+
const borrowedRank = (chord) => {
|
|
4403
|
+
const offset = BORROWED_ENTRY_ORDER.indexOf(pitchClassOf(chord.rootPc - tonicPc));
|
|
4404
|
+
return offset < 0 ? BORROWED_ENTRY_ORDER.length : offset;
|
|
4405
|
+
};
|
|
4406
|
+
borrowed.sort((a, b) => borrowedRank(a) - borrowedRank(b));
|
|
4407
|
+
const borrowedOpen = admittedCount(
|
|
4408
|
+
(harmonic - SECONDARY_DOMINANT_BAND) / (1 - SECONDARY_DOMINANT_BAND),
|
|
4409
|
+
borrowed.length
|
|
4410
|
+
);
|
|
4411
|
+
for (const chord of borrowed.slice(0, borrowedOpen)) {
|
|
4412
|
+
candidates.push({
|
|
4413
|
+
rootPc: chord.rootPc,
|
|
4414
|
+
quality: chord.quality,
|
|
4415
|
+
secondaryDominant: false,
|
|
4416
|
+
// A diminished or augmented triad is charged for what it is wherever it
|
|
4417
|
+
// comes from: priced at the flat borrowing rate, the ii dim borrowed from
|
|
4418
|
+
// the parallel minor came out cheaper than the key's own vii dim, and
|
|
4419
|
+
// which of the two a phrase took was decided by the tie-break jitter
|
|
4420
|
+
// rather than by the melody.
|
|
4421
|
+
base: UNSTABLE_QUALITIES.has(chord.quality) ? Math.max(BORROWED_BASE, UNSTABLE_TRIAD) : BORROWED_BASE,
|
|
4422
|
+
pcs: chordPitchClasses(chord)
|
|
4423
|
+
});
|
|
4424
|
+
}
|
|
4425
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4426
|
+
return candidates.filter((candidate) => {
|
|
4427
|
+
const id = `${candidate.rootPc}:${candidate.quality}`;
|
|
4428
|
+
if (seen.has(id)) {
|
|
4429
|
+
return false;
|
|
4430
|
+
}
|
|
4431
|
+
seen.add(id);
|
|
4432
|
+
return true;
|
|
4433
|
+
});
|
|
4434
|
+
}
|
|
4435
|
+
|
|
4436
|
+
// src/generate/harmonize/index.ts
|
|
4437
|
+
function harmonizeMelody(opts) {
|
|
4438
|
+
assertRecord(opts, "harmonize options");
|
|
4439
|
+
assertNoteEvents(opts.melody, "harmonize melody", {
|
|
4440
|
+
allowNonPositiveDuration: true,
|
|
4441
|
+
budget: opts.budget
|
|
4442
|
+
});
|
|
4443
|
+
const phraseEnds = opts.phraseEnds ?? [];
|
|
4444
|
+
for (const end of phraseEnds) {
|
|
4445
|
+
assertFiniteNumber(end, "harmonize phrase end");
|
|
4446
|
+
}
|
|
4447
|
+
const soundingMelody = soundingNotesOnly(opts.melody);
|
|
4448
|
+
const ts = meterAt(0, toMeterData(opts.ts ?? DEFAULT_METER, "ts"));
|
|
4449
|
+
assertTimeSignature(ts);
|
|
4450
|
+
const requestedKey = opts.key ?? "infer";
|
|
4451
|
+
const resolvedKey = resolveKey(
|
|
4452
|
+
requestedKey === "infer" ? inferKey(soundingMelody) : requestedKey
|
|
4453
|
+
);
|
|
4454
|
+
const key = resolvedKey.scale;
|
|
4455
|
+
const placement = opts.placement ?? DEFAULT_PLACEMENT;
|
|
4456
|
+
const ctx = resolveContext(opts.ctx);
|
|
4457
|
+
const reharmonize = assertOneOf(
|
|
4458
|
+
opts.reharmonize ?? "diatonic",
|
|
4459
|
+
REHARMONIZE_STRENGTHS,
|
|
4460
|
+
"reharmonize"
|
|
4461
|
+
);
|
|
4462
|
+
const harmonic = ctx.harmonic ?? REHARMONIZE_DIAL[reharmonize];
|
|
4463
|
+
if (soundingMelody.length === 0) {
|
|
4464
|
+
return { transposeSemitones: 0, key: resolvedKey, chords: [], melodyRoles: [] };
|
|
4465
|
+
}
|
|
4466
|
+
const candidates = buildCandidates(key, harmonic);
|
|
4467
|
+
const candAt = (i) => candidates[i] ?? FALLBACK;
|
|
4468
|
+
const draw = ctx.part("harmony");
|
|
4469
|
+
const jitter = candidates.map((_, index) => draw.at("jitter", index) * TIE_BREAK_JITTER2);
|
|
4470
|
+
const pulse2 = pulseBeats(ts);
|
|
4471
|
+
const spans = soundingMelody.map((note2) => ({
|
|
4472
|
+
startBeat: snapToPulse(note2.startBeat, pulse2),
|
|
4473
|
+
endBeat: snapToPulse(note2.startBeat + note2.durationBeat, pulse2)
|
|
4474
|
+
}));
|
|
4475
|
+
const melodyStart = spans.reduce(
|
|
4476
|
+
(start, span) => Math.min(start, span.startBeat),
|
|
4477
|
+
Number.POSITIVE_INFINITY
|
|
4478
|
+
);
|
|
4479
|
+
const melodyEnd = spans.reduce(
|
|
4480
|
+
(end, span) => Math.max(end, span.endBeat),
|
|
4481
|
+
Number.NEGATIVE_INFINITY
|
|
4482
|
+
);
|
|
4483
|
+
const hr = assertRange(
|
|
4484
|
+
opts.harmonicRhythm ?? defaultHarmonicRhythm(ts),
|
|
4485
|
+
Number.MIN_VALUE,
|
|
4486
|
+
Number.MAX_SAFE_INTEGER,
|
|
4487
|
+
"harmonic rhythm"
|
|
4488
|
+
);
|
|
4489
|
+
const { segments, innerEnds, bounds, segmentStart } = buildSegments(
|
|
4490
|
+
spans,
|
|
4491
|
+
soundingMelody,
|
|
4492
|
+
ts,
|
|
4493
|
+
{ start: melodyStart, end: melodyEnd, harmonicRhythm: hr },
|
|
4494
|
+
phraseEnds,
|
|
4495
|
+
opts.budget
|
|
4496
|
+
);
|
|
4022
4497
|
const { closing: closingSegments, named: namedEndSegments } = phraseEndSegments(
|
|
4023
4498
|
segments,
|
|
4024
|
-
|
|
4499
|
+
innerEnds
|
|
4025
4500
|
);
|
|
4026
4501
|
const articulatedSegments = new Set(candidates.map((c) => `${c.rootPc}:${c.quality}`)).size > 1 ? namedEndSegments : /* @__PURE__ */ new Set();
|
|
4027
4502
|
const semitoneShifts = [0];
|
|
@@ -4035,8 +4510,9 @@ function harmonizeMelody(opts) {
|
|
|
4035
4510
|
const octaveShifts = placement.octaveSearch ? [0, -12, 12] : [0];
|
|
4036
4511
|
const transposes = semitoneShifts.flatMap((s) => octaveShifts.map((o) => s + o));
|
|
4037
4512
|
assertGenerationBudget(
|
|
4038
|
-
transposes.length *
|
|
4039
|
-
"harmonization placement search"
|
|
4513
|
+
transposes.length * segments.length * candidates.length * candidates.length,
|
|
4514
|
+
"harmonization placement search",
|
|
4515
|
+
opts.budget
|
|
4040
4516
|
);
|
|
4041
4517
|
let bestCost = Number.POSITIVE_INFINITY;
|
|
4042
4518
|
let bestTs = 0;
|
|
@@ -4077,21 +4553,29 @@ function harmonizeMelody(opts) {
|
|
|
4077
4553
|
})
|
|
4078
4554
|
);
|
|
4079
4555
|
const melodyRoles = opts.melody.map((note2, noteIndex) => {
|
|
4080
|
-
const segIdx = segmentIndexAt(bounds, note2.startBeat);
|
|
4556
|
+
const segIdx = segmentIndexAt(bounds, snapToPulse(note2.startBeat, pulse2));
|
|
4081
4557
|
const cand = candAt(bestPath[segIdx] ?? 0);
|
|
4082
4558
|
const chord = makeChord(cand.rootPc, cand.quality);
|
|
4083
4559
|
return { noteIndex, role: roleOf(note2.pitch + bestTs, chord).role };
|
|
4084
4560
|
});
|
|
4085
|
-
return { transposeSemitones: bestTs, key, chords, melodyRoles };
|
|
4561
|
+
return { transposeSemitones: bestTs, key: resolvedKey, chords, melodyRoles };
|
|
4086
4562
|
}
|
|
4087
4563
|
|
|
4088
4564
|
// src/generate/motif/index.ts
|
|
4565
|
+
var MOTIF_TRANSFORMS = Object.freeze([
|
|
4566
|
+
"transposeDiatonic",
|
|
4567
|
+
"transposeChromatic",
|
|
4568
|
+
"invert",
|
|
4569
|
+
"retrograde",
|
|
4570
|
+
"augment",
|
|
4571
|
+
"diminish",
|
|
4572
|
+
"sequence"
|
|
4573
|
+
]);
|
|
4089
4574
|
var MIN_TILE_SPAN = 1 / 256;
|
|
4090
|
-
var
|
|
4091
|
-
var EPS4 = 1e-9;
|
|
4575
|
+
var DEFAULT_TS4 = { numerator: 4, denominator: 4 };
|
|
4092
4576
|
function isDownbeat(startBeat, barBeats) {
|
|
4093
4577
|
const bar2 = startBeat / barBeats;
|
|
4094
|
-
return Math.abs(bar2 - Math.round(bar2)) <
|
|
4578
|
+
return Math.abs(bar2 - Math.round(bar2)) < BEAT_EPS;
|
|
4095
4579
|
}
|
|
4096
4580
|
function nearestChordTone(pitch, chord) {
|
|
4097
4581
|
const pcs = chordPitchClasses(chord);
|
|
@@ -4146,6 +4630,18 @@ function cellSpan(cell) {
|
|
|
4146
4630
|
function clone(cell) {
|
|
4147
4631
|
return { notes: cell.notes.map((n) => ({ ...n })) };
|
|
4148
4632
|
}
|
|
4633
|
+
function inTimeOrder(cell) {
|
|
4634
|
+
return { notes: [...cell.notes].sort((a, b) => a.startBeat - b.startBeat) };
|
|
4635
|
+
}
|
|
4636
|
+
function pivotPitch(notes) {
|
|
4637
|
+
let pivot;
|
|
4638
|
+
for (const note2 of notes) {
|
|
4639
|
+
if (pivot === void 0 || note2.startBeat < pivot.startBeat) {
|
|
4640
|
+
pivot = note2;
|
|
4641
|
+
}
|
|
4642
|
+
}
|
|
4643
|
+
return pivot?.pitch ?? 0;
|
|
4644
|
+
}
|
|
4149
4645
|
function contourOffsets(contour, count) {
|
|
4150
4646
|
const offsets = [];
|
|
4151
4647
|
for (let i = 0; i < count; i += 1) {
|
|
@@ -4167,9 +4663,10 @@ function contourOffsets(contour, count) {
|
|
|
4167
4663
|
return offsets;
|
|
4168
4664
|
}
|
|
4169
4665
|
function generateMotif(opts) {
|
|
4666
|
+
assertRecord(opts, "motif options");
|
|
4170
4667
|
const contour = opts.contour ?? "arch";
|
|
4171
4668
|
const bars = assertPositiveInt(opts.bars, "motif bars");
|
|
4172
|
-
const barBeats = beatsPerBar(opts.ts ??
|
|
4669
|
+
const barBeats = beatsPerBar(meterAt(0, toMeterData(opts.ts ?? DEFAULT_TS4, "ts")));
|
|
4173
4670
|
const totalBeats = bars * barBeats;
|
|
4174
4671
|
const noteCount = Math.max(3, bars * 2);
|
|
4175
4672
|
assertGenerationBudget(noteCount, "motif notes");
|
|
@@ -4201,13 +4698,18 @@ function generateMotif(opts) {
|
|
|
4201
4698
|
return { notes };
|
|
4202
4699
|
}
|
|
4203
4700
|
function motifToNoteEvents(cell) {
|
|
4204
|
-
return cell
|
|
4701
|
+
return assertCellNotes(cell, "motif cell notes").map((note2) => ({ ...note2 }));
|
|
4702
|
+
}
|
|
4703
|
+
function assertCellNotes(cell, name) {
|
|
4704
|
+
const read = assertRecord(cell, "motif cell");
|
|
4705
|
+
assertNoteEvents(read.notes, name);
|
|
4706
|
+
return read.notes;
|
|
4205
4707
|
}
|
|
4206
4708
|
function transformMotif(cell, t, amount, key) {
|
|
4207
4709
|
return transformCell(cell, t, amount, key === void 0 ? void 0 : toKeyScale(key));
|
|
4208
4710
|
}
|
|
4209
4711
|
function transformCell(cell, t, amount, key) {
|
|
4210
|
-
return assertInRange(transformUnchecked(cell, t, amount, key));
|
|
4712
|
+
return assertInRange(inTimeOrder(transformUnchecked(cell, t, amount, key)));
|
|
4211
4713
|
}
|
|
4212
4714
|
function assertInRange(cell) {
|
|
4213
4715
|
for (const note2 of cell.notes) {
|
|
@@ -4220,7 +4722,8 @@ function assertInRange(cell) {
|
|
|
4220
4722
|
return cell;
|
|
4221
4723
|
}
|
|
4222
4724
|
function transformUnchecked(cell, t, amount, key) {
|
|
4223
|
-
|
|
4725
|
+
const notes = assertCellNotes(cell, "motif notes");
|
|
4726
|
+
assertOneOf(t, MOTIF_TRANSFORMS, "motif transform");
|
|
4224
4727
|
if (amount !== void 0) {
|
|
4225
4728
|
assertFiniteNumber(amount, "motif transform amount");
|
|
4226
4729
|
}
|
|
@@ -4233,7 +4736,6 @@ function transformUnchecked(cell, t, amount, key) {
|
|
|
4233
4736
|
if ((t === "augment" || t === "diminish") && amount !== void 0 && amount <= 0) {
|
|
4234
4737
|
throw new InvalidInputError("time transform amount must be positive");
|
|
4235
4738
|
}
|
|
4236
|
-
const notes = cell.notes;
|
|
4237
4739
|
switch (t) {
|
|
4238
4740
|
case "transposeChromatic": {
|
|
4239
4741
|
const semis = amount ?? 0;
|
|
@@ -4255,7 +4757,7 @@ function transformUnchecked(cell, t, amount, key) {
|
|
|
4255
4757
|
};
|
|
4256
4758
|
}
|
|
4257
4759
|
case "invert": {
|
|
4258
|
-
const pivot = notes
|
|
4760
|
+
const pivot = pivotPitch(notes);
|
|
4259
4761
|
return { notes: notes.map((n) => ({ ...n, pitch: 2 * pivot - n.pitch })) };
|
|
4260
4762
|
}
|
|
4261
4763
|
case "retrograde": {
|
|
@@ -4295,26 +4797,28 @@ function scaleTime(cell, factor) {
|
|
|
4295
4797
|
}))
|
|
4296
4798
|
};
|
|
4297
4799
|
}
|
|
4298
|
-
function developMotif(cell, timeline, key, bars, ts
|
|
4800
|
+
function developMotif(cell, timeline, key, bars, ts) {
|
|
4801
|
+
const meter = meterAt(0, toMeterData(ts, "ts"));
|
|
4802
|
+
assertChordTimeline(timeline);
|
|
4299
4803
|
assertPositiveInt(bars, "development bars");
|
|
4300
|
-
|
|
4804
|
+
const cellNotes = assertCellNotes(cell, "motif notes");
|
|
4301
4805
|
const scale = toKeyScale(key);
|
|
4302
4806
|
const span = cellSpan(cell);
|
|
4303
|
-
const barBeats = beatsPerBar(
|
|
4807
|
+
const barBeats = beatsPerBar(meter);
|
|
4304
4808
|
const totalBeats = bars * barBeats;
|
|
4305
|
-
const origin = cellOrigin(
|
|
4809
|
+
const origin = cellOrigin(cellNotes);
|
|
4306
4810
|
const out = [];
|
|
4307
4811
|
if (span <= 0) {
|
|
4308
|
-
return
|
|
4812
|
+
return inTimeOrder(cell);
|
|
4309
4813
|
}
|
|
4310
4814
|
const tileSpan = Math.max(span, MIN_TILE_SPAN);
|
|
4311
4815
|
const tileCount = Math.ceil(totalBeats / tileSpan);
|
|
4312
|
-
assertGenerationBudget(tileCount *
|
|
4816
|
+
assertGenerationBudget(tileCount * cellNotes.length, "developed motif notes");
|
|
4313
4817
|
for (let k = 0; k < tileCount; k += 1) {
|
|
4314
4818
|
const offset = k * tileSpan;
|
|
4315
4819
|
const taken = /* @__PURE__ */ new Map();
|
|
4316
4820
|
let previous;
|
|
4317
|
-
for (const n of
|
|
4821
|
+
for (const n of cellNotes) {
|
|
4318
4822
|
const startBeat = offset + (n.startBeat - origin);
|
|
4319
4823
|
if (startBeat >= totalBeats) {
|
|
4320
4824
|
continue;
|
|
@@ -4330,12 +4834,12 @@ function developMotif(cell, timeline, key, bars, ts = DEFAULT_TS5) {
|
|
|
4330
4834
|
out.push({ pitch, startBeat, durationBeat: n.durationBeat });
|
|
4331
4835
|
}
|
|
4332
4836
|
}
|
|
4333
|
-
return { notes: out };
|
|
4837
|
+
return inTimeOrder({ notes: out });
|
|
4334
4838
|
}
|
|
4335
4839
|
|
|
4336
4840
|
// src/generate/ornament/index.ts
|
|
4337
4841
|
var ORNAMENT_STYLES = Object.freeze(["ghost", "flam", "drag", "slide", "accent"]);
|
|
4338
|
-
var
|
|
4842
|
+
var DEFAULT_TS5 = { numerator: 4, denominator: 4 };
|
|
4339
4843
|
var DEFAULT_STYLE2 = "ghost";
|
|
4340
4844
|
var DEFAULT_AMOUNT = 0.5;
|
|
4341
4845
|
var GHOST_VELOCITY_SCALE = 0.45;
|
|
@@ -4343,7 +4847,7 @@ var ACCENT_VELOCITY_BOOST = 12;
|
|
|
4343
4847
|
var DEFAULT_VELOCITY2 = 80;
|
|
4344
4848
|
var MIN_VELOCITY2 = 1;
|
|
4345
4849
|
var MAX_VELOCITY2 = 127;
|
|
4346
|
-
var SLIDE_MIN_SEMITONES =
|
|
4850
|
+
var SLIDE_MIN_SEMITONES = 1;
|
|
4347
4851
|
function eligible(style, note2, previous, next, ts) {
|
|
4348
4852
|
const strong = isStrongBeat(note2.startBeat, ts);
|
|
4349
4853
|
switch (style) {
|
|
@@ -4378,28 +4882,23 @@ function clampVelocity(velocity) {
|
|
|
4378
4882
|
return Math.min(MAX_VELOCITY2, Math.max(MIN_VELOCITY2, Math.round(velocity)));
|
|
4379
4883
|
}
|
|
4380
4884
|
function ornament(notes, opts = {}) {
|
|
4885
|
+
const asked = assertOptions(opts, "opts");
|
|
4381
4886
|
assertNoteEvents(notes, "ornament notes", { allowNonPositiveDuration: true });
|
|
4382
|
-
const style = assertOneOf(
|
|
4383
|
-
const ts =
|
|
4887
|
+
const style = assertOneOf(asked.style ?? DEFAULT_STYLE2, ORNAMENT_STYLES, "ornament style");
|
|
4888
|
+
const ts = meterAt(0, toMeterData(asked.ts ?? DEFAULT_TS5, "ts"));
|
|
4384
4889
|
assertTimeSignature(ts);
|
|
4385
|
-
if (
|
|
4386
|
-
assertRange(
|
|
4890
|
+
if (asked.amount !== void 0) {
|
|
4891
|
+
assertRange(asked.amount, 0, 1, "ornament amount");
|
|
4387
4892
|
}
|
|
4388
|
-
const ctx = resolveContextWith(
|
|
4893
|
+
const ctx = resolveContextWith(asked.ctx, { ornament: asked.amount });
|
|
4389
4894
|
const amount = ctx.ornament ?? DEFAULT_AMOUNT;
|
|
4390
4895
|
const draw = ctx.part("ornament");
|
|
4391
|
-
const
|
|
4392
|
-
const ordered = [...
|
|
4393
|
-
const previousOf =
|
|
4394
|
-
for (let index = 0; index < ordered.length; index += 1) {
|
|
4395
|
-
const note2 = ordered[index];
|
|
4396
|
-
if (note2) {
|
|
4397
|
-
previousOf.set(note2, index === 0 ? void 0 : ordered[index - 1]);
|
|
4398
|
-
}
|
|
4399
|
-
}
|
|
4896
|
+
const sounding2 = dropSilentNotes(notes);
|
|
4897
|
+
const ordered = [...sounding2].sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);
|
|
4898
|
+
const previousOf = previousOnsets(ordered);
|
|
4400
4899
|
const nextOf = nextOnsets(ordered);
|
|
4401
4900
|
const gaps = strokeGaps(ordered);
|
|
4402
|
-
return
|
|
4901
|
+
return sounding2.map((note2) => {
|
|
4403
4902
|
const decorated = { ...note2 };
|
|
4404
4903
|
if (note2.articulation !== void 0 || !eligible(style, note2, previousOf.get(note2), nextOf.get(note2), ts)) {
|
|
4405
4904
|
return decorated;
|
|
@@ -4418,6 +4917,19 @@ function ornament(notes, opts = {}) {
|
|
|
4418
4917
|
return decorated;
|
|
4419
4918
|
});
|
|
4420
4919
|
}
|
|
4920
|
+
function previousOnsets(ordered) {
|
|
4921
|
+
const previousOf = /* @__PURE__ */ new Map();
|
|
4922
|
+
let earlier;
|
|
4923
|
+
for (const note2 of ordered) {
|
|
4924
|
+
if (earlier !== void 0 && earlier.startBeat === note2.startBeat) {
|
|
4925
|
+
previousOf.set(note2, previousOf.get(earlier));
|
|
4926
|
+
} else {
|
|
4927
|
+
previousOf.set(note2, earlier);
|
|
4928
|
+
}
|
|
4929
|
+
earlier = note2;
|
|
4930
|
+
}
|
|
4931
|
+
return previousOf;
|
|
4932
|
+
}
|
|
4421
4933
|
function nextOnsets(ordered) {
|
|
4422
4934
|
const nextOf = /* @__PURE__ */ new Map();
|
|
4423
4935
|
let later;
|
|
@@ -4450,6 +4962,8 @@ function strokeGaps(ordered) {
|
|
|
4450
4962
|
}
|
|
4451
4963
|
|
|
4452
4964
|
// src/generate/progression/index.ts
|
|
4965
|
+
var DEFAULT_TS6 = { numerator: 4, denominator: 4 };
|
|
4966
|
+
var PROG_STYLES = Object.freeze(["minimal", "dance", "idol", "rock"]);
|
|
4453
4967
|
var BORROWED_DEGREES = Object.freeze({
|
|
4454
4968
|
/** Flat submediant: bVI. */
|
|
4455
4969
|
bVI: 8,
|
|
@@ -4633,23 +5147,23 @@ function progressions() {
|
|
|
4633
5147
|
return PRESETS.map((p) => ({ ...p, degrees: [...p.degrees], styles: [...p.styles] }));
|
|
4634
5148
|
}
|
|
4635
5149
|
function progressionsByStyle(style) {
|
|
4636
|
-
|
|
5150
|
+
const named = assertOneOf(style, PROG_STYLES, "style");
|
|
5151
|
+
return progressions().filter((p) => p.styles.includes(named));
|
|
4637
5152
|
}
|
|
4638
5153
|
function degreeToRootPc(degree, key) {
|
|
4639
5154
|
if (degree >= 1 && degree <= 7) {
|
|
4640
|
-
const tones = scaleTonesInDegreeOrder(key);
|
|
4641
|
-
return tones
|
|
5155
|
+
const tones = scaleTonesInDegreeOrder(heptatonicFrameOf(key));
|
|
5156
|
+
return tones[degree - 1] ?? key.rootPc % 12;
|
|
4642
5157
|
}
|
|
4643
5158
|
const offset = BORROWED_OFFSET[degree] ?? 0;
|
|
4644
5159
|
return ((key.rootPc % 12 + offset) % 12 + 12) % 12;
|
|
4645
5160
|
}
|
|
4646
5161
|
function autoQuality(degree, key, harmonicDominant) {
|
|
4647
5162
|
if (degree >= 1 && degree <= 7) {
|
|
4648
|
-
|
|
4649
|
-
if (harmonicDominant && isMinor && degree === 5) {
|
|
5163
|
+
if (harmonicDominant && isMinorKey(key) && degree === 5) {
|
|
4650
5164
|
return "maj";
|
|
4651
5165
|
}
|
|
4652
|
-
return diatonicTriad(degree, key).quality;
|
|
5166
|
+
return diatonicTriad(degree, heptatonicFrameOf(key)).quality;
|
|
4653
5167
|
}
|
|
4654
5168
|
if (degree === 14) {
|
|
4655
5169
|
return "dim";
|
|
@@ -4660,46 +5174,44 @@ function autoQuality(degree, key, harmonicDominant) {
|
|
|
4660
5174
|
return "maj";
|
|
4661
5175
|
}
|
|
4662
5176
|
function resolveCycle(degrees, key, ext, harmonicDominant) {
|
|
4663
|
-
const steps =
|
|
4664
|
-
for (const degree of degrees) {
|
|
5177
|
+
const steps = degrees.map((degree) => {
|
|
4665
5178
|
const step = {
|
|
4666
|
-
source: degree,
|
|
4667
5179
|
rootPc: degreeToRootPc(degree, key),
|
|
4668
5180
|
quality: ext !== void 0 && ext !== "auto" ? ext : autoQuality(degree, key, harmonicDominant)
|
|
4669
5181
|
};
|
|
4670
5182
|
if (degree >= 1 && degree <= 7) {
|
|
4671
5183
|
step.degree = degree;
|
|
4672
5184
|
}
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
}
|
|
4677
|
-
steps.push(step);
|
|
4678
|
-
}
|
|
4679
|
-
const first = steps[0];
|
|
4680
|
-
const last = steps[steps.length - 1];
|
|
4681
|
-
if (steps.length > 1 && first !== void 0 && last !== void 0 && first.source !== last.source && sameStep(first, last)) {
|
|
4682
|
-
steps.pop();
|
|
4683
|
-
}
|
|
4684
|
-
return steps.length > 0 ? steps : [{ source: 0, degree: 0, rootPc: degreeToRootPc(0, key), quality: "maj" }];
|
|
5185
|
+
return step;
|
|
5186
|
+
});
|
|
5187
|
+
return steps.length > 0 ? steps : [{ rootPc: degreeToRootPc(1, key), quality: "maj" }];
|
|
4685
5188
|
}
|
|
4686
|
-
function
|
|
4687
|
-
return
|
|
5189
|
+
function pickProgressionPreset(style, ctx = 0) {
|
|
5190
|
+
return presetFor(style, resolveContext(ctx));
|
|
4688
5191
|
}
|
|
4689
|
-
function
|
|
5192
|
+
function presetFor(style, ctx) {
|
|
4690
5193
|
const pool = PRESETS.filter((preset) => preset.styles.includes(style));
|
|
4691
5194
|
if (pool.length === 0) {
|
|
4692
5195
|
throw new InvalidInputError(`Unknown progression style: ${style}`);
|
|
4693
5196
|
}
|
|
4694
|
-
const index =
|
|
5197
|
+
const index = ctx.part("progression").range(0, pool.length - 1, "preset");
|
|
4695
5198
|
return pool[index] ?? PRESETS[0];
|
|
4696
5199
|
}
|
|
5200
|
+
function tonicizableRootPcs(key) {
|
|
5201
|
+
return new Set(tonicizableDegrees(key).map((degree) => degree.rootPc));
|
|
5202
|
+
}
|
|
5203
|
+
function canBeTonicized(span, tonicizable) {
|
|
5204
|
+
if (!tonicizable.has(span.rootPc)) {
|
|
5205
|
+
return false;
|
|
5206
|
+
}
|
|
5207
|
+
return fifthPcOf(makeChord(span.rootPc, span.quality)) === (span.rootPc + 7) % 12;
|
|
5208
|
+
}
|
|
4697
5209
|
function generateProgression(opts) {
|
|
5210
|
+
assertRecord(opts, "progression options");
|
|
4698
5211
|
assertPositiveInt(opts.bars, "progression bars");
|
|
4699
5212
|
assertGenerationBudget(opts.bars, "progression chords");
|
|
4700
5213
|
const key = toKeyScale(opts.key);
|
|
4701
5214
|
const ctx = resolveContext(opts.ctx);
|
|
4702
|
-
const seed = ctx.seed;
|
|
4703
5215
|
let preset;
|
|
4704
5216
|
if (opts.preset !== void 0) {
|
|
4705
5217
|
if (opts.preset.degrees.length === 0) {
|
|
@@ -4728,7 +5240,7 @@ function generateProgression(opts) {
|
|
|
4728
5240
|
throw new InvalidInputError(`Unknown progression preset: ${opts.presetId}`);
|
|
4729
5241
|
}
|
|
4730
5242
|
}
|
|
4731
|
-
preset ??=
|
|
5243
|
+
preset ??= presetFor(opts.style, ctx);
|
|
4732
5244
|
const ext = opts.ext === void 0 ? "auto" : assertOneOf(opts.ext, ["auto", ...chordQualities()], "progression extension");
|
|
4733
5245
|
const cycle = resolveCycle(
|
|
4734
5246
|
preset?.degrees ?? [0],
|
|
@@ -4736,13 +5248,14 @@ function generateProgression(opts) {
|
|
|
4736
5248
|
ext,
|
|
4737
5249
|
preset?.functional === "cadenceStrong"
|
|
4738
5250
|
);
|
|
5251
|
+
const barBeats = beatsPerBar(opts.ts ?? DEFAULT_TS6);
|
|
4739
5252
|
const chords = [];
|
|
4740
5253
|
for (let bar2 = 0; bar2 < opts.bars; bar2 += 1) {
|
|
4741
5254
|
const step = cycle[bar2 % cycle.length];
|
|
4742
5255
|
const chord = {
|
|
4743
5256
|
rootPc: step?.rootPc ?? 0,
|
|
4744
5257
|
quality: step?.quality ?? "maj",
|
|
4745
|
-
startBeat: bar2 *
|
|
5258
|
+
startBeat: bar2 * barBeats
|
|
4746
5259
|
};
|
|
4747
5260
|
if (step?.degree !== void 0) {
|
|
4748
5261
|
chord.degree = step.degree;
|
|
@@ -4752,6 +5265,7 @@ function generateProgression(opts) {
|
|
|
4752
5265
|
const harmonic = ctx.harmonic ?? DEFAULT_HARMONIC;
|
|
4753
5266
|
if (harmonic > 0) {
|
|
4754
5267
|
const tonicPc = (key.rootPc % 12 + 12) % 12;
|
|
5268
|
+
const tonicizable = tonicizableRootPcs(key);
|
|
4755
5269
|
const draw = ctx.part("progression");
|
|
4756
5270
|
let tonicStatements = chords.filter((chord) => chord.rootPc === tonicPc).length;
|
|
4757
5271
|
for (let i = 0; i < chords.length - 1; i += 1) {
|
|
@@ -4765,7 +5279,8 @@ function generateProgression(opts) {
|
|
|
4765
5279
|
const isLastTonicStatement = cur.rootPc === tonicPc && tonicStatements <= 1;
|
|
4766
5280
|
const alreadySecondary = cur.rootPc === domRoot && cur.quality === "dom7";
|
|
4767
5281
|
const isResolutionTarget = chords[i - 1]?.secondaryDominant === true;
|
|
4768
|
-
|
|
5282
|
+
const takesNoDominant = !canBeTonicized(next, tonicizable);
|
|
5283
|
+
if (targetsTonic || isLastTonicStatement || alreadySecondary || isResolutionTarget || takesNoDominant) {
|
|
4769
5284
|
continue;
|
|
4770
5285
|
}
|
|
4771
5286
|
if (draw.prob(harmonic, "reharmonize", i)) {
|
|
@@ -4785,12 +5300,6 @@ function generateProgression(opts) {
|
|
|
4785
5300
|
}
|
|
4786
5301
|
|
|
4787
5302
|
// src/generate/reharmony/index.ts
|
|
4788
|
-
function hasInterval(chord, semitone) {
|
|
4789
|
-
return chord.intervals.some((iv) => pitchClassOf(iv) === semitone);
|
|
4790
|
-
}
|
|
4791
|
-
function isDominantType(chord) {
|
|
4792
|
-
return hasInterval(chord, 4) && hasInterval(chord, 10);
|
|
4793
|
-
}
|
|
4794
5303
|
function commonToneCount(a, b) {
|
|
4795
5304
|
const set = new Set(b);
|
|
4796
5305
|
return a.filter((pc) => set.has(pc)).length;
|
|
@@ -4821,23 +5330,10 @@ function sameChord(a, b) {
|
|
|
4821
5330
|
return pitchClassOf(a.rootPc) === pitchClassOf(b.rootPc) && a.quality === b.quality;
|
|
4822
5331
|
}
|
|
4823
5332
|
var THIRD_OFFSETS = [3, 4, 8, 9];
|
|
4824
|
-
var MIDDLE_C = 60;
|
|
4825
5333
|
var MAX_HINT_ALTER = 2;
|
|
4826
5334
|
function bareSpelling(note2) {
|
|
4827
5335
|
return { letter: note2.letter, alter: note2.alter };
|
|
4828
5336
|
}
|
|
4829
|
-
function accidentalLoad(tonic, key) {
|
|
4830
|
-
return spellScale(tonic, key).reduce((sum, note2) => sum + Math.abs(note2.alter), 0);
|
|
4831
|
-
}
|
|
4832
|
-
function keyTonicOf(key) {
|
|
4833
|
-
const rootPc = pitchClassOf(key.rootPc);
|
|
4834
|
-
const sharp = bareSpelling(midiToNote(MIDDLE_C + rootPc, "sharp"));
|
|
4835
|
-
const flat = bareSpelling(midiToNote(MIDDLE_C + rootPc, "flat"));
|
|
4836
|
-
if (sharp.letter === flat.letter) {
|
|
4837
|
-
return sharp;
|
|
4838
|
-
}
|
|
4839
|
-
return accidentalLoad(flat, key) <= accidentalLoad(sharp, key) ? flat : sharp;
|
|
4840
|
-
}
|
|
4841
5337
|
function spelledOnRoot(chord, root, tonic, key) {
|
|
4842
5338
|
const usable = noteToPitchClass(root) === pitchClassOf(chord.rootPc) && Math.abs(root.alter) <= MAX_HINT_ALTER;
|
|
4843
5339
|
const spelled = {
|
|
@@ -4861,12 +5357,12 @@ function tritoneSubstituteRoot(dominant, tonic, key) {
|
|
|
4861
5357
|
return transposeNote(resolution, 1);
|
|
4862
5358
|
}
|
|
4863
5359
|
function substituteChord(chord, key, opts) {
|
|
5360
|
+
assertOptions(opts, "opts");
|
|
4864
5361
|
const source = toChordData(chord);
|
|
4865
|
-
const scale =
|
|
5362
|
+
const { scale, tonic } = resolveKey(key);
|
|
4866
5363
|
const originalTriad = chordPitchClasses(makeChord(source.rootPc, triadQualityOf(source)));
|
|
4867
5364
|
const candidates = [];
|
|
4868
|
-
|
|
4869
|
-
if (isDominantType(source)) {
|
|
5365
|
+
if (soundsDominantSeventh(source)) {
|
|
4870
5366
|
const substitute = makeChord(pitchClassOf(source.rootPc + 6), "dom7");
|
|
4871
5367
|
candidates.push({
|
|
4872
5368
|
chord: spelledOnRoot(substitute, tritoneSubstituteRoot(source, tonic, scale), tonic, scale),
|
|
@@ -4922,7 +5418,7 @@ function substituteChord(chord, key, opts) {
|
|
|
4922
5418
|
return results;
|
|
4923
5419
|
}
|
|
4924
5420
|
function modalInterchangePalette(key) {
|
|
4925
|
-
const scale =
|
|
5421
|
+
const { scale, tonic } = resolveKey(key);
|
|
4926
5422
|
const palette = [];
|
|
4927
5423
|
const seen = /* @__PURE__ */ new Set();
|
|
4928
5424
|
const add = (chord) => {
|
|
@@ -4937,7 +5433,6 @@ function modalInterchangePalette(key) {
|
|
|
4937
5433
|
source: borrowedSource(chord, scale)
|
|
4938
5434
|
});
|
|
4939
5435
|
};
|
|
4940
|
-
const tonic = keyTonicOf(scale);
|
|
4941
5436
|
const parallel = parallelKey(scale);
|
|
4942
5437
|
for (const triad of diatonicTriadsOf(parallel)) {
|
|
4943
5438
|
if (!isDiatonic(triad, scale) && borrowedSource(triad, scale) !== null) {
|
|
@@ -4952,13 +5447,12 @@ function modalInterchangePalette(key) {
|
|
|
4952
5447
|
}
|
|
4953
5448
|
function negativeHarmonyMirror(chord, key) {
|
|
4954
5449
|
const source = toChordData(chord);
|
|
4955
|
-
const scale =
|
|
5450
|
+
const { scale, tonic } = resolveKey(key);
|
|
4956
5451
|
const tonicPc = pitchClassOf(scale.rootPc);
|
|
4957
|
-
const tonic = keyTonicOf(scale);
|
|
4958
5452
|
const mirror = (p) => pitchClassOf(2 * tonicPc + 7 - p);
|
|
4959
5453
|
const pcs = chordPitchClasses(source).map(mirror);
|
|
4960
5454
|
const bassPc = source.bassPc !== void 0 ? mirror(source.bassPc) : void 0;
|
|
4961
|
-
const pitches = bassPc !== void 0 ? [bassPc
|
|
5455
|
+
const pitches = bassPc !== void 0 ? [bassPc, ...pcs.map((pc) => pc + 12)] : pcs;
|
|
4962
5456
|
const detected = detectChordBest(pitches);
|
|
4963
5457
|
if (detected) {
|
|
4964
5458
|
if (bassPc === void 0 && detected.bassPc !== void 0) {
|
|
@@ -4973,9 +5467,9 @@ function negativeHarmonyMirror(chord, key) {
|
|
|
4973
5467
|
// src/generate/rhythm/index.ts
|
|
4974
5468
|
var DEFAULT_RHYTHM_VELOCITY = 96;
|
|
4975
5469
|
function rhythmToNoteEvents(events, pitch, velocity = DEFAULT_RHYTHM_VELOCITY) {
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
return events.map((event) => ({
|
|
5470
|
+
assertMidiPitch(pitch, "pitch");
|
|
5471
|
+
assertInteger(velocity, "velocity", 0, 127);
|
|
5472
|
+
return assertArray(events, "events").map((event) => ({
|
|
4979
5473
|
pitch,
|
|
4980
5474
|
startBeat: event.position,
|
|
4981
5475
|
durationBeat: event.duration,
|
|
@@ -4986,6 +5480,7 @@ var DEFAULT_SUBDIVISION = 2;
|
|
|
4986
5480
|
var DEFAULT_DENSITY = 0.5;
|
|
4987
5481
|
var DEFAULT_BARS = 1;
|
|
4988
5482
|
function onsetWeightCurve(weight) {
|
|
5483
|
+
assertInteger(weight, "weight", 0, 3);
|
|
4989
5484
|
switch (weight) {
|
|
4990
5485
|
case 3:
|
|
4991
5486
|
return 1;
|
|
@@ -4998,13 +5493,15 @@ function onsetWeightCurve(weight) {
|
|
|
4998
5493
|
}
|
|
4999
5494
|
}
|
|
5000
5495
|
function generateRhythm(ts, opts = {}) {
|
|
5001
|
-
const
|
|
5002
|
-
const
|
|
5003
|
-
const
|
|
5496
|
+
const asked = assertOptions(opts, "opts");
|
|
5497
|
+
const meter = meterAt(0, toMeterData(ts, "ts"));
|
|
5498
|
+
const bars = asked.bars ?? DEFAULT_BARS;
|
|
5499
|
+
const subdivision = asked.subdivision ?? DEFAULT_SUBDIVISION;
|
|
5500
|
+
const ctx = resolveContext(asked.ctx);
|
|
5004
5501
|
const density = ctx.rhythmic ?? DEFAULT_DENSITY;
|
|
5005
5502
|
assertPositiveInt(bars, "rhythm bars");
|
|
5006
5503
|
assertPositiveInt(subdivision, "rhythm subdivision");
|
|
5007
|
-
const barBeats = beatsPerBar(
|
|
5504
|
+
const barBeats = beatsPerBar(meter);
|
|
5008
5505
|
const spanBeats = barBeats * bars;
|
|
5009
5506
|
const step = 1 / subdivision;
|
|
5010
5507
|
const slotsPerBar = Math.ceil(barBeats * subdivision);
|
|
@@ -5024,7 +5521,7 @@ function generateRhythm(ts, opts = {}) {
|
|
|
5024
5521
|
positions.push(position);
|
|
5025
5522
|
continue;
|
|
5026
5523
|
}
|
|
5027
|
-
const weight = metricWeight(position,
|
|
5524
|
+
const weight = metricWeight(position, meter);
|
|
5028
5525
|
const probability = onsetWeightCurve(weight) * density;
|
|
5029
5526
|
if (draw.prob(probability, "onset", bar2, slot)) {
|
|
5030
5527
|
positions.push(position);
|
|
@@ -5040,12 +5537,14 @@ function generateRhythm(ts, opts = {}) {
|
|
|
5040
5537
|
return events;
|
|
5041
5538
|
}
|
|
5042
5539
|
function rhythmDensity(events, ts) {
|
|
5540
|
+
assertArray(events, "events");
|
|
5541
|
+
const barBeats = beatsPerBar(ts);
|
|
5043
5542
|
if (events.length === 0) {
|
|
5044
5543
|
return 0;
|
|
5045
5544
|
}
|
|
5046
5545
|
const last = events[events.length - 1];
|
|
5047
5546
|
const spanBeats = (last?.position ?? 0) + (last?.duration ?? 0);
|
|
5048
|
-
const bars = spanBeats /
|
|
5547
|
+
const bars = spanBeats / barBeats;
|
|
5049
5548
|
return bars > 0 ? events.length / bars : 0;
|
|
5050
5549
|
}
|
|
5051
5550
|
|
|
@@ -5099,6 +5598,7 @@ export {
|
|
|
5099
5598
|
applyGrooveTemplate,
|
|
5100
5599
|
classifyMelodyTones,
|
|
5101
5600
|
harmonizeMelody,
|
|
5601
|
+
cellSpan,
|
|
5102
5602
|
generateMotif,
|
|
5103
5603
|
motifToNoteEvents,
|
|
5104
5604
|
transformMotif,
|
|
@@ -5118,4 +5618,4 @@ export {
|
|
|
5118
5618
|
generateRhythm,
|
|
5119
5619
|
rhythmDensity
|
|
5120
5620
|
};
|
|
5121
|
-
//# sourceMappingURL=chunk-
|
|
5621
|
+
//# sourceMappingURL=chunk-Q6LFSAKV.js.map
|