@libraz/libcantus 0.9.3 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/analyze/index.cjs +39 -1947
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +197 -37
- package/dist/analyze/index.d.ts +197 -37
- package/dist/analyze/index.js +19 -8
- package/dist/chunk-36N73KZ3.cjs +246 -0
- package/dist/chunk-36N73KZ3.cjs.map +1 -0
- package/dist/chunk-4HLLZPXT.cjs +3077 -0
- package/dist/chunk-4HLLZPXT.cjs.map +1 -0
- package/dist/chunk-7MGZ7D62.cjs +498 -0
- package/dist/chunk-7MGZ7D62.cjs.map +1 -0
- package/dist/{chunk-4MHA432G.js → chunk-7NKDPF74.js} +403 -180
- package/dist/chunk-7NKDPF74.js.map +1 -0
- package/dist/chunk-CAN76NCU.cjs +679 -0
- package/dist/chunk-CAN76NCU.cjs.map +1 -0
- package/dist/{chunk-MIWO425M.js → chunk-D2FAJBYL.js} +101 -34
- package/dist/chunk-D2FAJBYL.js.map +1 -0
- package/dist/{chunk-SGU7SPLK.js → chunk-EHVEBWM5.js} +1056 -531
- package/dist/chunk-EHVEBWM5.js.map +1 -0
- package/dist/chunk-FCANXBAT.cjs +30 -0
- package/dist/chunk-FCANXBAT.cjs.map +1 -0
- package/dist/chunk-FNDWXRRI.cjs +479 -0
- package/dist/chunk-FNDWXRRI.cjs.map +1 -0
- package/dist/{chunk-IT2O4XFE.js → chunk-GQSGS2N4.js} +43 -24
- package/dist/chunk-GQSGS2N4.js.map +1 -0
- package/dist/{chunk-62QZKCJQ.js → chunk-HKGYCQ34.js} +10 -6
- package/dist/chunk-HKGYCQ34.js.map +1 -0
- package/dist/{chunk-AKLYPWIN.js → chunk-I24MV65U.js} +29 -6
- package/dist/chunk-I24MV65U.js.map +1 -0
- package/dist/{chunk-CHMXELDS.js → chunk-KMHOICIY.js} +311 -181
- package/dist/chunk-KMHOICIY.js.map +1 -0
- package/dist/chunk-LW53AHQU.cjs +1117 -0
- package/dist/chunk-LW53AHQU.cjs.map +1 -0
- package/dist/{chunk-2ZWMSJH6.js → chunk-NKIBQ6PT.js} +2 -2
- package/dist/chunk-NKIBQ6PT.js.map +1 -0
- package/dist/chunk-NP4NA2FB.cjs +106 -0
- package/dist/chunk-NP4NA2FB.cjs.map +1 -0
- package/dist/chunk-NVE34YGZ.cjs +1449 -0
- package/dist/chunk-NVE34YGZ.cjs.map +1 -0
- package/dist/{chunk-EH6EM37U.js → chunk-NYYWMV2C.js} +61 -49
- package/dist/chunk-NYYWMV2C.js.map +1 -0
- package/dist/{chunk-GOAW4VYL.js → chunk-Q2XUP227.js} +243 -117
- package/dist/chunk-Q2XUP227.js.map +1 -0
- package/dist/chunk-Q6OP4U43.cjs +327 -0
- package/dist/chunk-Q6OP4U43.cjs.map +1 -0
- package/dist/chunk-RC6GBMCD.cjs +845 -0
- package/dist/chunk-RC6GBMCD.cjs.map +1 -0
- package/dist/{chunk-C5XYKUVG.js → chunk-RK35RIN7.js} +370 -157
- package/dist/chunk-RK35RIN7.js.map +1 -0
- package/dist/chunk-RUMU3UA6.cjs +53 -0
- package/dist/chunk-RUMU3UA6.cjs.map +1 -0
- package/dist/chunk-T64C7ZD7.cjs +1 -0
- package/dist/chunk-T64C7ZD7.cjs.map +1 -0
- package/dist/chunk-XEBG3G53.js +498 -0
- package/dist/chunk-XEBG3G53.js.map +1 -0
- package/dist/{chunk-I5R7ZBJS.js → chunk-YLEMGEFH.js} +103 -59
- package/dist/chunk-YLEMGEFH.js.map +1 -0
- package/dist/chunk-ZEJR4TKL.cjs +173 -0
- package/dist/chunk-ZEJR4TKL.cjs.map +1 -0
- package/dist/core/index.cjs +130 -647
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +301 -39
- package/dist/core/index.d.ts +301 -39
- package/dist/core/index.js +54 -16
- package/dist/{function-DYt4iKsn.d.cts → function-BtiZb9i9.d.ts} +28 -7
- package/dist/{function-DVH4Eusr.d.ts → function-DNugAOrt.d.cts} +28 -7
- package/dist/generate/index.cjs +28 -4085
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +370 -62
- package/dist/generate/index.d.ts +370 -62
- package/dist/generate/index.js +23 -9
- package/dist/{index-CxqBQtH1.d.cts → index-6VR8-kit.d.ts} +19 -4
- package/dist/index-70p168VM.d.ts +192 -0
- package/dist/{index-CpOsP1y9.d.cts → index-Be74mcon.d.cts} +18 -17
- package/dist/index-BtXT6UQ0.d.cts +192 -0
- package/dist/{index-DxvARIP2.d.ts → index-C5UBbQ4G.d.ts} +18 -17
- package/dist/{index-Bt4tOUD9.d.ts → index-C83mZI1P.d.ts} +93 -8
- package/dist/{index-Bg5dYMmZ.d.ts → index-D6b6na69.d.cts} +14 -11
- package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
- package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
- package/dist/index-OG65ocOr.d.cts +272 -0
- package/dist/index-OG65ocOr.d.ts +272 -0
- package/dist/{index-Jc0Ryi79.d.ts → index-tJhkmQgE.d.cts} +19 -4
- package/dist/{index-Dikst44v.d.cts → index-tSQh-xwY.d.ts} +14 -11
- package/dist/{index-BNlGP86A.d.cts → index-y_ak9_Hi.d.cts} +93 -8
- package/dist/index.cjs +419 -7383
- 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 +111 -27
- package/dist/model/index.cjs +8 -2562
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +433 -75
- package/dist/model/index.d.ts +433 -75
- package/dist/model/index.js +6 -7
- package/dist/styled-DIRkk9Uz.d.ts +506 -0
- package/dist/styled-DJbkJQfj.d.cts +506 -0
- package/dist/theory/index.cjs +172 -1822
- package/dist/theory/index.cjs.map +1 -1
- package/dist/theory/index.d.cts +151 -233
- package/dist/theory/index.d.ts +151 -233
- package/dist/theory/index.js +30 -7
- package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
- package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
- package/package.json +24 -6
- package/dist/chunk-2ZWMSJH6.js.map +0 -1
- package/dist/chunk-4MHA432G.js.map +0 -1
- package/dist/chunk-62QZKCJQ.js.map +0 -1
- package/dist/chunk-AKLYPWIN.js.map +0 -1
- package/dist/chunk-C5XYKUVG.js.map +0 -1
- package/dist/chunk-CHMXELDS.js.map +0 -1
- package/dist/chunk-DYWFNC4A.js +0 -152
- package/dist/chunk-DYWFNC4A.js.map +0 -1
- package/dist/chunk-EH6EM37U.js.map +0 -1
- package/dist/chunk-FBQK3ZQN.js +0 -131
- package/dist/chunk-FBQK3ZQN.js.map +0 -1
- package/dist/chunk-GOAW4VYL.js.map +0 -1
- package/dist/chunk-I5R7ZBJS.js.map +0 -1
- package/dist/chunk-IT2O4XFE.js.map +0 -1
- package/dist/chunk-MIWO425M.js.map +0 -1
- package/dist/chunk-SGU7SPLK.js.map +0 -1
- package/dist/index-CGMZNCx6.d.cts +0 -140
- package/dist/index-CGMZNCx6.d.ts +0 -140
- package/dist/index-CnZCo29u.d.cts +0 -108
- package/dist/index-JnF4v9bx.d.ts +0 -108
- package/dist/styled-DZ7GjShb.d.ts +0 -282
- package/dist/styled-ZTbdaVFM.d.cts +0 -282
- package/src/analyze/arrange/index.ts +0 -21
- package/src/analyze/arrange/internal.ts +0 -127
- package/src/analyze/arrange/tension.ts +0 -199
- package/src/analyze/arrange/tracks.ts +0 -312
- package/src/analyze/detect/index.ts +0 -272
- package/src/analyze/functional/borrowed.ts +0 -84
- package/src/analyze/functional/cadence.ts +0 -66
- package/src/analyze/functional/function.ts +0 -184
- package/src/analyze/functional/index.ts +0 -23
- package/src/analyze/functional/internal.ts +0 -29
- package/src/analyze/functional/roman.ts +0 -402
- package/src/analyze/index.ts +0 -49
- package/src/analyze/timeline/index.ts +0 -451
- package/src/analyze/voice/index.ts +0 -339
- package/src/core/event-index/index.ts +0 -104
- package/src/core/index.ts +0 -61
- package/src/core/interval/index.ts +0 -81
- package/src/core/meter/index.ts +0 -302
- package/src/core/pitch/index.ts +0 -311
- package/src/core/random/index.ts +0 -76
- package/src/core/tuning/index.ts +0 -186
- package/src/core/types.ts +0 -29
- package/src/core/validation/index.ts +0 -119
- package/src/generate/bass/index.ts +0 -373
- package/src/generate/countermelody/index.ts +0 -470
- package/src/generate/drums/beat.ts +0 -266
- package/src/generate/drums/euclid.ts +0 -65
- package/src/generate/drums/fills.ts +0 -356
- package/src/generate/drums/ghost.ts +0 -119
- package/src/generate/drums/hihat.ts +0 -285
- package/src/generate/drums/hit.ts +0 -31
- package/src/generate/drums/index.ts +0 -361
- package/src/generate/drums/internal.ts +0 -257
- package/src/generate/drums/kick.ts +0 -160
- package/src/generate/drums/percussion.ts +0 -121
- package/src/generate/drums/rng.ts +0 -7
- package/src/generate/drums/swing.ts +0 -71
- package/src/generate/groove/index.ts +0 -351
- package/src/generate/harmonize/index.ts +0 -451
- package/src/generate/index.ts +0 -58
- package/src/generate/motif/index.ts +0 -422
- package/src/generate/progression/index.ts +0 -362
- package/src/generate/reharmony/index.ts +0 -323
- package/src/generate/rhythm/index.ts +0 -175
- package/src/index.ts +0 -13
- package/src/model/chord.ts +0 -442
- package/src/model/index.ts +0 -15
- package/src/model/interval.ts +0 -82
- package/src/model/key.ts +0 -266
- package/src/model/note.ts +0 -176
- package/src/model/progression.ts +0 -161
- package/src/model/shared.ts +0 -12
- package/src/theory/chord/index.ts +0 -375
- package/src/theory/chordscale/index.ts +0 -374
- package/src/theory/counterpoint/index.ts +0 -230
- package/src/theory/harmony/index.ts +0 -77
- package/src/theory/index.ts +0 -112
- package/src/theory/safety/index.ts +0 -488
- package/src/theory/scale/degrees.ts +0 -123
- package/src/theory/scale/index.ts +0 -28
- package/src/theory/scale/key.ts +0 -56
- package/src/theory/scale/masks.ts +0 -153
- package/src/theory/spelling/index.ts +0 -249
- package/src/theory/symbol/index.ts +0 -268
- package/src/theory/voicing/index.ts +0 -5
- package/src/theory/voicing/internal.ts +0 -168
- package/src/theory/voicing/leading.ts +0 -112
- package/src/theory/voicing/satb.ts +0 -216
- package/src/theory/voicing/styled.ts +0 -199
- package/tsconfig.json +0 -23
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createRng
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-HKGYCQ34.js";
|
|
4
4
|
import {
|
|
5
5
|
beatsPerBar,
|
|
6
6
|
createNoteEventIndex,
|
|
7
7
|
formatTimeSignature,
|
|
8
8
|
isStrongBeat,
|
|
9
9
|
metricWeight
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-YLEMGEFH.js";
|
|
11
11
|
import {
|
|
12
12
|
isDiatonic,
|
|
13
13
|
parallelKey
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-KMHOICIY.js";
|
|
15
15
|
import {
|
|
16
16
|
roleOf
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-NKIBQ6PT.js";
|
|
18
18
|
import {
|
|
19
19
|
enumerateSafePitches,
|
|
20
20
|
evaluateSafety
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-NYYWMV2C.js";
|
|
22
22
|
import {
|
|
23
23
|
NATURAL_MINOR_MASK,
|
|
24
24
|
chordPitchClasses,
|
|
@@ -32,18 +32,23 @@ import {
|
|
|
32
32
|
minorKey,
|
|
33
33
|
nearestScaleTone,
|
|
34
34
|
scaleTonesInDegreeOrder
|
|
35
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-D2FAJBYL.js";
|
|
36
36
|
import {
|
|
37
|
+
InvalidInputError,
|
|
37
38
|
assertFiniteNumber,
|
|
38
39
|
assertGenerationBudget,
|
|
39
40
|
assertInteger,
|
|
40
41
|
assertNoteEvents,
|
|
42
|
+
assertOneOf,
|
|
41
43
|
assertPositiveInt,
|
|
42
44
|
assertRange,
|
|
43
|
-
assertTimeSignature
|
|
44
|
-
|
|
45
|
+
assertTimeSignature,
|
|
46
|
+
dropSilentNotes,
|
|
47
|
+
pitchClassOf
|
|
48
|
+
} from "./chunk-XEBG3G53.js";
|
|
45
49
|
|
|
46
50
|
// src/generate/bass/index.ts
|
|
51
|
+
var BASS_STYLES = ["root", "rootFifth", "pop", "walking", "arpeggio"];
|
|
47
52
|
var DEFAULT_TS = { numerator: 4, denominator: 4 };
|
|
48
53
|
var DEFAULT_STYLE = "root";
|
|
49
54
|
var DEFAULT_OCTAVE = 2;
|
|
@@ -51,9 +56,6 @@ var STRONG_VELOCITY = 100;
|
|
|
51
56
|
var WEAK_VELOCITY = 80;
|
|
52
57
|
var PICKUP_PROB = 0.35;
|
|
53
58
|
var EPS = 1e-9;
|
|
54
|
-
function pitchClass(value) {
|
|
55
|
-
return (Math.trunc(value) % 12 + 12) % 12;
|
|
56
|
-
}
|
|
57
59
|
function placePc(pc, anchor, low) {
|
|
58
60
|
const high = low + 12;
|
|
59
61
|
let midi = pc + 12 * Math.round((anchor - pc) / 12);
|
|
@@ -69,24 +71,24 @@ function chordTonePcs(chord) {
|
|
|
69
71
|
const seen = /* @__PURE__ */ new Set();
|
|
70
72
|
const pcs = [];
|
|
71
73
|
for (const interval of chord.intervals) {
|
|
72
|
-
const pc =
|
|
74
|
+
const pc = pitchClassOf(chord.rootPc + interval);
|
|
73
75
|
if (!seen.has(pc)) {
|
|
74
76
|
seen.add(pc);
|
|
75
77
|
pcs.push(pc);
|
|
76
78
|
}
|
|
77
79
|
}
|
|
78
80
|
if (pcs.length === 0) {
|
|
79
|
-
pcs.push(
|
|
81
|
+
pcs.push(pitchClassOf(chord.rootPc));
|
|
80
82
|
}
|
|
81
83
|
return pcs;
|
|
82
84
|
}
|
|
83
85
|
function fifthPcOf(chord) {
|
|
84
|
-
const fifths = new Set(chord.intervals.map((i) =>
|
|
86
|
+
const fifths = new Set(chord.intervals.map((i) => pitchClassOf(i)));
|
|
85
87
|
const chosen = fifths.has(7) ? 7 : fifths.has(6) ? 6 : fifths.has(8) ? 8 : void 0;
|
|
86
|
-
return chosen === void 0 ?
|
|
88
|
+
return chosen === void 0 ? pitchClassOf(chord.rootPc) : pitchClassOf(chord.rootPc + chosen);
|
|
87
89
|
}
|
|
88
90
|
function bassPcOf(chord) {
|
|
89
|
-
return
|
|
91
|
+
return pitchClassOf(chord.bassPc ?? chord.rootPc);
|
|
90
92
|
}
|
|
91
93
|
function beatPositions(start, end) {
|
|
92
94
|
const positions = [];
|
|
@@ -125,7 +127,8 @@ function buildPop(ctx, seg) {
|
|
|
125
127
|
} else if (ctx.rng.prob(PICKUP_PROB)) {
|
|
126
128
|
if (ctx.rng.prob(0.5)) {
|
|
127
129
|
const base = placePc(rootPc, ctx.prevMidi, ctx.low);
|
|
128
|
-
|
|
130
|
+
const dropped = base - 12;
|
|
131
|
+
emit(ctx, pos, rootPc, dropped >= 0 ? dropped : base);
|
|
129
132
|
} else {
|
|
130
133
|
ctx.prevMidi = emit(ctx, pos, fifthPcOf(seg.chord));
|
|
131
134
|
}
|
|
@@ -145,12 +148,23 @@ function buildArpeggio(ctx, seg) {
|
|
|
145
148
|
}
|
|
146
149
|
function approachNote(ctx, target, from) {
|
|
147
150
|
const dir = from <= target ? -1 : 1;
|
|
151
|
+
const chromatic = target + dir;
|
|
148
152
|
if (ctx.rng.prob(0.5)) {
|
|
149
|
-
return
|
|
153
|
+
return foldIntoBand(chromatic, ctx.low);
|
|
150
154
|
}
|
|
151
155
|
const cand = nearestScaleTone(target + dir * 2, ctx.key);
|
|
152
156
|
const step = Math.abs(cand - target);
|
|
153
|
-
return step >= 1 && step <= 2 ? cand :
|
|
157
|
+
return foldIntoBand(step >= 1 && step <= 2 ? cand : chromatic, ctx.low);
|
|
158
|
+
}
|
|
159
|
+
function foldIntoBand(midi, low) {
|
|
160
|
+
let result = midi;
|
|
161
|
+
while (result < low) {
|
|
162
|
+
result += 12;
|
|
163
|
+
}
|
|
164
|
+
while (result > low + 12) {
|
|
165
|
+
result -= 12;
|
|
166
|
+
}
|
|
167
|
+
return result;
|
|
154
168
|
}
|
|
155
169
|
function buildWalking(ctx, seg, next) {
|
|
156
170
|
const tones = chordTonePcs(seg.chord);
|
|
@@ -164,7 +178,7 @@ function buildWalking(ctx, seg, next) {
|
|
|
164
178
|
if (next && count > 1 && i === count - 1) {
|
|
165
179
|
const nextBass = placePc(bassPcOf(next.chord), ctx.prevMidi, ctx.low);
|
|
166
180
|
const midi = approachNote(ctx, nextBass, ctx.prevMidi);
|
|
167
|
-
ctx.prevMidi = emit(ctx, pos,
|
|
181
|
+
ctx.prevMidi = emit(ctx, pos, pitchClassOf(midi), midi);
|
|
168
182
|
return;
|
|
169
183
|
}
|
|
170
184
|
const pc = tones[i % tones.length] ?? bassPcOf(seg.chord);
|
|
@@ -179,7 +193,7 @@ function generateBassLine(opts) {
|
|
|
179
193
|
assertRange(segment.startBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].startBeat`);
|
|
180
194
|
assertRange(segment.endBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].endBeat`);
|
|
181
195
|
if (segment.endBeat <= segment.startBeat) {
|
|
182
|
-
throw new
|
|
196
|
+
throw new InvalidInputError(`segments[${index}] must have a positive duration`);
|
|
183
197
|
}
|
|
184
198
|
}
|
|
185
199
|
const segments = [...opts.segments].sort((a, b) => a.startBeat - b.startBeat);
|
|
@@ -188,7 +202,7 @@ function generateBassLine(opts) {
|
|
|
188
202
|
}
|
|
189
203
|
const ts = opts.ts ?? DEFAULT_TS;
|
|
190
204
|
assertTimeSignature(ts);
|
|
191
|
-
const style = opts.style ?? DEFAULT_STYLE;
|
|
205
|
+
const style = assertOneOf(opts.style ?? DEFAULT_STYLE, BASS_STYLES, "bass style");
|
|
192
206
|
const octave = opts.octave ?? DEFAULT_OCTAVE;
|
|
193
207
|
assertInteger(octave, "bass octave", -1, 9);
|
|
194
208
|
const estimatedNotes = segments.reduce(
|
|
@@ -263,6 +277,7 @@ var HOLD_OFF_BEAT_PROB = 0.2;
|
|
|
263
277
|
var REINFORCE_PROB = 0.35;
|
|
264
278
|
var DEFAULT_MELODY_VELOCITY = 96;
|
|
265
279
|
var VELOCITY_DROP = 16;
|
|
280
|
+
var TIE_BREAK_JITTER = 1e-6;
|
|
266
281
|
function isWholeBeat(beat) {
|
|
267
282
|
return Math.abs(beat - Math.round(beat)) < EPS2;
|
|
268
283
|
}
|
|
@@ -365,26 +380,40 @@ function heldNoteBoundaries(melody, startBeat, endBeat, chordChangeBeats) {
|
|
|
365
380
|
}
|
|
366
381
|
return [...boundaries].sort((a, b) => a - b);
|
|
367
382
|
}
|
|
368
|
-
function
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
for (const boundary of boundaries) {
|
|
383
|
+
function boundaryContexts(melody, harmony, startBeat, endBeat, ts) {
|
|
384
|
+
const boundaries = heldNoteBoundaries(melody, startBeat, endBeat, harmony.chordChangeBeats);
|
|
385
|
+
return boundaries.map((boundary) => {
|
|
372
386
|
const melodyVoice = melodySnapshotAt(melody, boundary);
|
|
387
|
+
return {
|
|
388
|
+
melodyVoice,
|
|
389
|
+
otherVoices: melodyVoice === void 0 ? [] : [melodyVoice],
|
|
390
|
+
chord: harmony.chordAt(boundary),
|
|
391
|
+
strongBeat: isStrongBeat(boundary, ts),
|
|
392
|
+
atCounterOnset: Math.abs(boundary - startBeat) < EPS2
|
|
393
|
+
};
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
function heldPitchSafety(pitch, contexts, prevPitch, key, profile, register, low, high) {
|
|
397
|
+
let worst = 0 /* Safe */;
|
|
398
|
+
for (const context of contexts) {
|
|
399
|
+
const melodyVoice = context.melodyVoice;
|
|
373
400
|
if (melodyVoice !== void 0 && (register === "below" && pitch >= melodyVoice.pitch || register === "above" && pitch <= melodyVoice.pitch)) {
|
|
374
401
|
return null;
|
|
375
402
|
}
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
403
|
+
const result = evaluateSafety(
|
|
404
|
+
{
|
|
405
|
+
profile,
|
|
406
|
+
candidatePitch: pitch,
|
|
407
|
+
prevPitch: context.atCounterOnset ? prevPitch : pitch,
|
|
408
|
+
chord: context.chord,
|
|
409
|
+
key,
|
|
410
|
+
otherVoices: context.otherVoices,
|
|
411
|
+
strongBeat: context.strongBeat,
|
|
412
|
+
vocalLow: low,
|
|
413
|
+
vocalHigh: high
|
|
414
|
+
},
|
|
415
|
+
{ suggestions: false }
|
|
416
|
+
);
|
|
388
417
|
worst = Math.max(worst, result.safety);
|
|
389
418
|
if (worst === 2 /* Dissonant */) {
|
|
390
419
|
return worst;
|
|
@@ -393,16 +422,31 @@ function heldPitchSafety(pitch, startBeat, endBeat, prevPitch, melody, opts, pro
|
|
|
393
422
|
return worst;
|
|
394
423
|
}
|
|
395
424
|
function generateCounterMelody(opts) {
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
425
|
+
const timeline = opts.timeline;
|
|
426
|
+
const chordAt = timeline === void 0 ? opts.chordAt : (beat) => timeline.at(beat);
|
|
427
|
+
if (chordAt === void 0) {
|
|
428
|
+
throw new TypeError("countermelody needs a timeline or a chordAt callback");
|
|
399
429
|
}
|
|
430
|
+
const chordChangeBeats = timeline === void 0 ? opts.chordChangeBeats ?? [] : timeline.segments.flatMap((segment) => [segment.startBeat, segment.endBeat]);
|
|
431
|
+
const register = opts.register ?? "below";
|
|
432
|
+
const melody = createNoteEventIndex(opts.melody, {
|
|
433
|
+
allowNonPositiveDuration: true,
|
|
434
|
+
tieBreak: register === "below" ? "highest" : "lowest"
|
|
435
|
+
});
|
|
400
436
|
const ts = opts.ts ?? DEFAULT_TS2;
|
|
401
437
|
assertTimeSignature(ts);
|
|
402
|
-
const
|
|
403
|
-
const
|
|
404
|
-
const profile = opts.profile ?? "pop";
|
|
438
|
+
const rhythm = assertOneOf(opts.rhythm ?? "complement", ["complement", "follow"], "rhythm");
|
|
439
|
+
const profile = assertOneOf(opts.profile ?? "pop", ["strict", "pop"], "safety profile");
|
|
405
440
|
const rng = createRng(opts.seed ?? 0);
|
|
441
|
+
if (opts.pitchLow !== void 0) {
|
|
442
|
+
assertInteger(opts.pitchLow, "countermelody pitchLow");
|
|
443
|
+
}
|
|
444
|
+
if (opts.pitchHigh !== void 0) {
|
|
445
|
+
assertInteger(opts.pitchHigh, "countermelody pitchHigh");
|
|
446
|
+
}
|
|
447
|
+
if (melody.notes.every((indexed) => indexed.note.durationBeat <= 0)) {
|
|
448
|
+
return [];
|
|
449
|
+
}
|
|
406
450
|
const spanStart = melody.notes[0]?.note.startBeat ?? 0;
|
|
407
451
|
const spanEnd = melody.notes.reduce((end, n) => Math.max(end, n.endBeat), spanStart);
|
|
408
452
|
assertGenerationBudget(Math.ceil((spanEnd - spanStart) / GRID_STEP), "countermelody grid");
|
|
@@ -413,15 +457,15 @@ function generateCounterMelody(opts) {
|
|
|
413
457
|
assertInteger(low, "countermelody pitchLow");
|
|
414
458
|
assertInteger(high, "countermelody pitchHigh");
|
|
415
459
|
if (low > high) {
|
|
416
|
-
throw new
|
|
460
|
+
throw new InvalidInputError(
|
|
417
461
|
`countermelody pitchLow must not exceed pitchHigh; received ${low} > ${high}`
|
|
418
462
|
);
|
|
419
463
|
}
|
|
420
464
|
assertGenerationBudget(high - low + 1, "countermelody pitch candidates");
|
|
421
|
-
assertGenerationBudget(
|
|
422
|
-
for (let index = 0; index <
|
|
465
|
+
assertGenerationBudget(chordChangeBeats.length, "chord change beats");
|
|
466
|
+
for (let index = 0; index < chordChangeBeats.length; index += 1) {
|
|
423
467
|
assertRange(
|
|
424
|
-
|
|
468
|
+
chordChangeBeats[index] ?? Number.NaN,
|
|
425
469
|
0,
|
|
426
470
|
Number.MAX_SAFE_INTEGER,
|
|
427
471
|
`chordChangeBeats[${index}]`
|
|
@@ -429,7 +473,9 @@ function generateCounterMelody(opts) {
|
|
|
429
473
|
}
|
|
430
474
|
const center = (low + high) / 2;
|
|
431
475
|
const onsets = rhythm === "follow" ? followOnsets(melody) : complementOnsets(melody, ts, spanStart, spanEnd, rng);
|
|
432
|
-
|
|
476
|
+
const boundaryEstimate = onsets.length + Math.ceil((spanEnd - spanStart) / GRID_STEP) + melody.notes.length + chordChangeBeats.length;
|
|
477
|
+
assertGenerationBudget(boundaryEstimate * (high - low + 1), "countermelody search");
|
|
478
|
+
const jitter = Array.from({ length: high - low + 1 }, () => rng.next() * TIE_BREAK_JITTER);
|
|
433
479
|
const out = [];
|
|
434
480
|
let prevPitch;
|
|
435
481
|
for (let onsetIndex = 0; onsetIndex < onsets.length; onsetIndex += 1) {
|
|
@@ -443,7 +489,8 @@ function generateCounterMelody(opts) {
|
|
|
443
489
|
const melPitch = melNote?.pitch;
|
|
444
490
|
const melodyVoice = melodySnapshotAt(melody, beat);
|
|
445
491
|
const melPrev = melodyVoice?.prevPitch;
|
|
446
|
-
const
|
|
492
|
+
const contexts = boundaryContexts(melody, { chordAt, chordChangeBeats }, beat, endBeat, ts);
|
|
493
|
+
const chord = contexts[0]?.chord ?? chordAt(beat);
|
|
447
494
|
const otherVoices = melodyVoice !== void 0 ? [melodyVoice] : [];
|
|
448
495
|
const candidates = enumerateSafePitches(
|
|
449
496
|
{
|
|
@@ -465,16 +512,13 @@ function generateCounterMelody(opts) {
|
|
|
465
512
|
for (const candidate of candidates) {
|
|
466
513
|
const worstSafety = heldPitchSafety(
|
|
467
514
|
candidate,
|
|
468
|
-
|
|
469
|
-
endBeat,
|
|
515
|
+
contexts,
|
|
470
516
|
prevPitch,
|
|
471
|
-
|
|
472
|
-
opts,
|
|
517
|
+
opts.key,
|
|
473
518
|
profile,
|
|
474
519
|
register,
|
|
475
520
|
low,
|
|
476
|
-
high
|
|
477
|
-
ts
|
|
521
|
+
high
|
|
478
522
|
);
|
|
479
523
|
if (worstSafety === null || worstSafety === 2 /* Dissonant */) {
|
|
480
524
|
continue;
|
|
@@ -486,7 +530,7 @@ function generateCounterMelody(opts) {
|
|
|
486
530
|
continue;
|
|
487
531
|
}
|
|
488
532
|
}
|
|
489
|
-
const score = scoreCandidate(candidate, melPitch, melPrev, prevPitch, chord, center);
|
|
533
|
+
const score = scoreCandidate(candidate, melPitch, melPrev, prevPitch, chord, center) + (jitter[candidate - low] ?? 0);
|
|
490
534
|
const tie = Math.abs(score - bestScore) <= EPS2 && candidate < (bestPitch ?? Number.POSITIVE_INFINITY);
|
|
491
535
|
if (worstSafety < bestWorstSafety || worstSafety === bestWorstSafety && (score > bestScore + EPS2 || tie)) {
|
|
492
536
|
bestWorstSafety = worstSafety;
|
|
@@ -528,6 +572,27 @@ var GM = {
|
|
|
528
572
|
};
|
|
529
573
|
var EIGHTH = 0.5;
|
|
530
574
|
var SIXTEENTH = 0.25;
|
|
575
|
+
var DRUM_FEELS = ["straight", "swing", "shuffle"];
|
|
576
|
+
var GROOVE_STYLES = [
|
|
577
|
+
"standard",
|
|
578
|
+
"funk",
|
|
579
|
+
"shuffle",
|
|
580
|
+
"bossa",
|
|
581
|
+
"trap",
|
|
582
|
+
"halftime",
|
|
583
|
+
"breakbeat",
|
|
584
|
+
"house",
|
|
585
|
+
"synthpop"
|
|
586
|
+
];
|
|
587
|
+
var DRUM_ROLES = ["full", "ambient", "minimal", "fxOnly"];
|
|
588
|
+
var PUBLIC_SECTIONS = [
|
|
589
|
+
"intro",
|
|
590
|
+
"verse",
|
|
591
|
+
"prechorus",
|
|
592
|
+
"chorus",
|
|
593
|
+
"bridge",
|
|
594
|
+
"outro"
|
|
595
|
+
];
|
|
531
596
|
var STYLE_MAP = {
|
|
532
597
|
// Standard pop pulse: kick on beats 1 and 3, backbeat snare on 2 and 4.
|
|
533
598
|
standard: { style: "standard", feel: "straight", ghostBoost: false, snareBeat3: false },
|
|
@@ -727,7 +792,7 @@ function getGhostDensity(mood, section, backingDensity, bpm) {
|
|
|
727
792
|
}
|
|
728
793
|
return levelToProbability(level);
|
|
729
794
|
}
|
|
730
|
-
function getGhostVelocity(section, beatPosition
|
|
795
|
+
function getGhostVelocity(section, beatPosition) {
|
|
731
796
|
let base = 0.4;
|
|
732
797
|
const even = beatPosition % 2 === 0 ? 0.05 : 0;
|
|
733
798
|
switch (section) {
|
|
@@ -748,14 +813,11 @@ function getGhostVelocity(section, beatPosition, isAfterSnare) {
|
|
|
748
813
|
base = 0.38;
|
|
749
814
|
break;
|
|
750
815
|
}
|
|
751
|
-
if (isAfterSnare) {
|
|
752
|
-
base += 0.1;
|
|
753
|
-
}
|
|
754
816
|
return Math.max(0.25, Math.min(0.65, base));
|
|
755
817
|
}
|
|
756
818
|
function getGhostProbabilityAtPosition(beat, sixteenthInBeat) {
|
|
757
|
-
const
|
|
758
|
-
return
|
|
819
|
+
const leadsIntoBackbeat = (beat === 0 || beat === 2) && sixteenthInBeat === 3;
|
|
820
|
+
return leadsIntoBackbeat ? 0.6 : 0.25;
|
|
759
821
|
}
|
|
760
822
|
function selectGhostPositions(mood, rng) {
|
|
761
823
|
if (mood === 2 /* Energetic */) {
|
|
@@ -1038,10 +1100,7 @@ function quantizeSwing(startBeat, swing, resolution) {
|
|
|
1038
1100
|
}
|
|
1039
1101
|
|
|
1040
1102
|
// src/generate/drums/beat.ts
|
|
1041
|
-
function
|
|
1042
|
-
return quantizeSwing(tick, effectiveSwing(feel, swingAmount), "sixteenth");
|
|
1043
|
-
}
|
|
1044
|
-
function hatSwingFactor(style) {
|
|
1103
|
+
function styleSwingFactor(style) {
|
|
1045
1104
|
if (style === "trap") {
|
|
1046
1105
|
return 0;
|
|
1047
1106
|
}
|
|
@@ -1050,6 +1109,12 @@ function hatSwingFactor(style) {
|
|
|
1050
1109
|
}
|
|
1051
1110
|
return 0.5;
|
|
1052
1111
|
}
|
|
1112
|
+
function sectionSwing(sec, swingAmount) {
|
|
1113
|
+
return effectiveSwing(sec.feel, swingAmount) * styleSwingFactor(sec.style);
|
|
1114
|
+
}
|
|
1115
|
+
function swing16(tick, sec, swingAmount) {
|
|
1116
|
+
return quantizeSwing(tick, sectionSwing(sec, swingAmount), "sixteenth");
|
|
1117
|
+
}
|
|
1053
1118
|
function generateKickForBeat(ctx, sec, kick) {
|
|
1054
1119
|
if (ctx.inPrechorusLift) {
|
|
1055
1120
|
return;
|
|
@@ -1060,7 +1125,7 @@ function generateKickForBeat(ctx, sec, kick) {
|
|
|
1060
1125
|
ctx.track.add(GM.BD, ctx.beatTick, EIGHTH, ctx.velocity);
|
|
1061
1126
|
}
|
|
1062
1127
|
if (and) {
|
|
1063
|
-
const andTick = swing16(ctx.beatTick + EIGHTH, sec
|
|
1128
|
+
const andTick = swing16(ctx.beatTick + EIGHTH, sec, ctx.swingAmount);
|
|
1064
1129
|
ctx.track.add(GM.BD, andTick, EIGHTH, ctx.velocity * 0.85);
|
|
1065
1130
|
}
|
|
1066
1131
|
}
|
|
@@ -1077,12 +1142,17 @@ function generateSnareForBeat(ctx, sec, isIntroFirst) {
|
|
|
1077
1142
|
}
|
|
1078
1143
|
const backbeatVel = Math.min(127, ctx.velocity + 16);
|
|
1079
1144
|
const promoteSparseChorus = sec.style === "sparse" && ctx.section === "chorus" && sec.role === "full";
|
|
1145
|
+
if (sec.role === "fxOnly") {
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
if (sec.role === "minimal") {
|
|
1149
|
+
ctx.track.add(GM.SIDESTICK, ctx.beatTick, EIGHTH, ctx.velocity * 0.65);
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1080
1152
|
if (promoteSparseChorus) {
|
|
1081
1153
|
ctx.track.add(GM.SD, ctx.beatTick, EIGHTH, backbeatVel);
|
|
1082
1154
|
} else if (sec.style === "sparse" || sec.role === "ambient") {
|
|
1083
|
-
|
|
1084
|
-
ctx.track.add(GM.SIDESTICK, ctx.beatTick, EIGHTH, ctx.velocity * 0.8);
|
|
1085
|
-
}
|
|
1155
|
+
ctx.track.add(GM.SIDESTICK, ctx.beatTick, EIGHTH, ctx.velocity * 0.8);
|
|
1086
1156
|
} else {
|
|
1087
1157
|
ctx.track.add(GM.SD, ctx.beatTick, EIGHTH, backbeatVel);
|
|
1088
1158
|
}
|
|
@@ -1103,13 +1173,13 @@ function generateGhostNotesForBeat(ctx, sec) {
|
|
|
1103
1173
|
continue;
|
|
1104
1174
|
}
|
|
1105
1175
|
const variation = ctx.rng.float(0.85, 1.15);
|
|
1106
|
-
const ghostBase = getGhostVelocity(ctx.section, ctx.beat / 2
|
|
1176
|
+
const ghostBase = getGhostVelocity(ctx.section, ctx.beat / 2);
|
|
1107
1177
|
let ghostVel = ctx.velocity * ghostBase * variation;
|
|
1108
1178
|
if (pos === "a") {
|
|
1109
1179
|
ghostVel *= 0.9;
|
|
1110
1180
|
}
|
|
1111
1181
|
const offset = pos === "e" ? SIXTEENTH : 3 * SIXTEENTH;
|
|
1112
|
-
const tick = swing16(ctx.beatTick + offset, sec
|
|
1182
|
+
const tick = swing16(ctx.beatTick + offset, sec, ctx.swingAmount);
|
|
1113
1183
|
ctx.track.add(GM.SD, tick, SIXTEENTH, ghostVel);
|
|
1114
1184
|
}
|
|
1115
1185
|
}
|
|
@@ -1171,7 +1241,7 @@ function generateHiHatForBeat(ctx, sec) {
|
|
|
1171
1241
|
for (let eighth = 0; eighth < 2; eighth += 1) {
|
|
1172
1242
|
let hhTick = ctx.beatTick + eighth * EIGHTH;
|
|
1173
1243
|
if (eighth === 1) {
|
|
1174
|
-
hhTick = swing16(hhTick, sec
|
|
1244
|
+
hhTick = swing16(hhTick, sec, ctx.swingAmount);
|
|
1175
1245
|
}
|
|
1176
1246
|
if (ctx.section === "intro" && eighth === 1) {
|
|
1177
1247
|
if (sec.useFootHh && ctx.beat % 2 === 0) {
|
|
@@ -1201,9 +1271,8 @@ function generateHiHatForBeat(ctx, sec) {
|
|
|
1201
1271
|
}
|
|
1202
1272
|
for (let sixteenth = 0; sixteenth < 4; sixteenth += 1) {
|
|
1203
1273
|
let hhTick = ctx.beatTick + sixteenth * SIXTEENTH;
|
|
1204
|
-
if (sixteenth
|
|
1205
|
-
|
|
1206
|
-
hhTick = quantizeSwing(hhTick, swung, "sixteenth");
|
|
1274
|
+
if (sixteenth !== 0) {
|
|
1275
|
+
hhTick = swing16(hhTick, sec, ctx.swingAmount);
|
|
1207
1276
|
}
|
|
1208
1277
|
const metricVel = hiHatVelocityMultiplier(sixteenth, ctx.rng);
|
|
1209
1278
|
const hhVel = Math.max(20, ctx.velocity * dm * typeMult * metricVel);
|
|
@@ -1531,24 +1600,62 @@ function generateFill(track, beatTick, beat, fillType, velocity) {
|
|
|
1531
1600
|
}
|
|
1532
1601
|
|
|
1533
1602
|
// src/generate/drums/hit.ts
|
|
1603
|
+
var DRUM_NOTES = {
|
|
1604
|
+
kick: GM.BD,
|
|
1605
|
+
snare: GM.SD,
|
|
1606
|
+
sideStick: GM.SIDESTICK,
|
|
1607
|
+
handClap: GM.HANDCLAP,
|
|
1608
|
+
closedHiHat: GM.CHH,
|
|
1609
|
+
pedalHiHat: GM.FHH,
|
|
1610
|
+
openHiHat: GM.OHH,
|
|
1611
|
+
crash: GM.CRASH,
|
|
1612
|
+
ride: GM.RIDE,
|
|
1613
|
+
tambourine: GM.TAMBOURINE,
|
|
1614
|
+
highTom: GM.TOM_H,
|
|
1615
|
+
midTom: GM.TOM_M,
|
|
1616
|
+
lowTom: GM.TOM_L,
|
|
1617
|
+
shaker: GM.SHAKER
|
|
1618
|
+
};
|
|
1619
|
+
var VOICE_BY_NOTE = new Map(
|
|
1620
|
+
Object.entries(DRUM_NOTES).map(([voice, note]) => [note, voice])
|
|
1621
|
+
);
|
|
1622
|
+
function drumVoiceOf(pitch) {
|
|
1623
|
+
return VOICE_BY_NOTE.get(pitch);
|
|
1624
|
+
}
|
|
1625
|
+
var ONSET_KEY_SCALE = 128;
|
|
1534
1626
|
var HitList = class {
|
|
1535
1627
|
hits = [];
|
|
1628
|
+
/** Onset keys already occupied, as `pitch:quantizedBeat`. */
|
|
1629
|
+
#onsets = /* @__PURE__ */ new Set();
|
|
1536
1630
|
/** Append a hit, clamping velocity and dropping non-positive positions. */
|
|
1537
1631
|
add(pitch, startBeat, durationBeat, velocity) {
|
|
1632
|
+
const beat = Math.max(0, startBeat);
|
|
1538
1633
|
this.hits.push({
|
|
1539
1634
|
pitch,
|
|
1540
|
-
startBeat:
|
|
1635
|
+
startBeat: beat,
|
|
1541
1636
|
durationBeat,
|
|
1542
1637
|
velocity: clampVel(velocity)
|
|
1543
1638
|
});
|
|
1639
|
+
this.#onsets.add(onsetKey(pitch, beat));
|
|
1640
|
+
}
|
|
1641
|
+
/** Whether a hit of this pitch already starts exactly at `startBeat`. */
|
|
1642
|
+
hasOnset(pitch, startBeat) {
|
|
1643
|
+
return this.#onsets.has(onsetKey(pitch, startBeat));
|
|
1544
1644
|
}
|
|
1545
1645
|
/** True when a crash already sits within a 16th of `startBeat`. */
|
|
1546
1646
|
hasCrashNear(startBeat) {
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1647
|
+
const steps = Math.round(0.25 * ONSET_KEY_SCALE);
|
|
1648
|
+
for (let step = 0; step < steps; step += 1) {
|
|
1649
|
+
if (this.#onsets.has(onsetKey(GM.CRASH, startBeat + step / ONSET_KEY_SCALE))) {
|
|
1650
|
+
return true;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
return false;
|
|
1550
1654
|
}
|
|
1551
1655
|
};
|
|
1656
|
+
function onsetKey(pitch, startBeat) {
|
|
1657
|
+
return `${pitch}:${Math.round(startBeat * ONSET_KEY_SCALE)}`;
|
|
1658
|
+
}
|
|
1552
1659
|
|
|
1553
1660
|
// src/generate/drums/kick.ts
|
|
1554
1661
|
var EMPTY = {
|
|
@@ -1736,25 +1843,31 @@ function generateAuxPercussionForBar(track, barStart, config, role, densityMult,
|
|
|
1736
1843
|
}
|
|
1737
1844
|
|
|
1738
1845
|
// src/generate/drums/index.ts
|
|
1846
|
+
var MAX_HITS_PER_BAR = 128;
|
|
1739
1847
|
function generateDrums(opts) {
|
|
1740
1848
|
assertPositiveInt(opts.bars, "drum bars");
|
|
1741
1849
|
assertRange(opts.bpm, Number.MIN_VALUE, 1e3, "drum bpm");
|
|
1742
1850
|
assertRange(opts.density, 0, 1, "drum density");
|
|
1743
|
-
assertGenerationBudget(opts.bars *
|
|
1851
|
+
assertGenerationBudget(opts.bars * MAX_HITS_PER_BAR, "drum hits");
|
|
1744
1852
|
if (opts.euclideanKick) {
|
|
1745
1853
|
const steps = opts.euclideanKick.steps ?? 16;
|
|
1746
1854
|
assertInteger(steps, "euclidean steps", 1, 16);
|
|
1747
1855
|
assertInteger(opts.euclideanKick.pulses, "euclidean pulses");
|
|
1748
1856
|
assertInteger(opts.euclideanKick.rotation ?? 0, "euclidean rotation");
|
|
1749
1857
|
}
|
|
1858
|
+
const publicStyle = assertOneOf(opts.style, GROOVE_STYLES, "drum style");
|
|
1859
|
+
const publicSection = assertOneOf(opts.section, PUBLIC_SECTIONS, "drum section");
|
|
1860
|
+
if (opts.nextSection !== void 0) {
|
|
1861
|
+
assertOneOf(opts.nextSection, PUBLIC_SECTIONS, "drum nextSection");
|
|
1862
|
+
}
|
|
1750
1863
|
const track = new HitList();
|
|
1751
1864
|
const rng = createRng(opts.seed ?? 0);
|
|
1752
|
-
const mapping = mapStyle(
|
|
1865
|
+
const mapping = mapStyle(publicStyle);
|
|
1753
1866
|
const style = mapping.style;
|
|
1754
|
-
const feel = opts.feel
|
|
1755
|
-
const role = opts.role
|
|
1867
|
+
const feel = opts.feel === void 0 ? mapping.feel : assertOneOf(opts.feel, DRUM_FEELS, "drum feel");
|
|
1868
|
+
const role = opts.role === void 0 ? "full" : assertOneOf(opts.role, DRUM_ROLES, "drum role");
|
|
1756
1869
|
const playMainVoices = role !== "fxOnly";
|
|
1757
|
-
const section = mapSection(
|
|
1870
|
+
const section = mapSection(publicSection);
|
|
1758
1871
|
const backingDensity = mapDensity(opts.density);
|
|
1759
1872
|
const swingAmount = feelSwingAmount(feel);
|
|
1760
1873
|
let densityMult = sectionDensityMultiplier(section);
|
|
@@ -1806,7 +1919,7 @@ function generateDrums(opts) {
|
|
|
1806
1919
|
} else if (!euclidKick) {
|
|
1807
1920
|
kick = getKickPattern(section, style, bar, rng);
|
|
1808
1921
|
}
|
|
1809
|
-
const inLift = isInPreChorusLift(section, bar, opts.bars,
|
|
1922
|
+
const inLift = isInPreChorusLift(section, bar, opts.bars, nextSection === "chorus");
|
|
1810
1923
|
let currentFill = "snareRoll";
|
|
1811
1924
|
for (let beat = 0; beat < 4; beat += 1) {
|
|
1812
1925
|
const beatTick = barStart + beat;
|
|
@@ -1871,8 +1984,7 @@ function generateDrums(opts) {
|
|
|
1871
1984
|
if (sec.useFootHh && shouldPlayHiHat(role)) {
|
|
1872
1985
|
for (let fhhBeat = 0; fhhBeat < 4; fhhBeat += 2) {
|
|
1873
1986
|
const fhhTick = barStart + fhhBeat;
|
|
1874
|
-
|
|
1875
|
-
if (!occupied) {
|
|
1987
|
+
if (!track.hasOnset(GM.FHH, fhhTick)) {
|
|
1876
1988
|
track.add(GM.FHH, fhhTick, 0.5, footHiHatVelocity(rng));
|
|
1877
1989
|
}
|
|
1878
1990
|
}
|
|
@@ -1896,11 +2008,12 @@ function generateDrums(opts) {
|
|
|
1896
2008
|
opts.bpm
|
|
1897
2009
|
);
|
|
1898
2010
|
}
|
|
1899
|
-
return track.hits;
|
|
2011
|
+
return [...track.hits].sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);
|
|
1900
2012
|
}
|
|
1901
2013
|
|
|
1902
2014
|
// src/generate/groove/index.ts
|
|
1903
2015
|
var DEFAULT_TS3 = { numerator: 4, denominator: 4 };
|
|
2016
|
+
var DEFAULT_GROOVE_SUBDIVISION = 4;
|
|
1904
2017
|
var DEFAULT_TIMING = 0.02;
|
|
1905
2018
|
var DEFAULT_VELOCITY_JITTER = 8;
|
|
1906
2019
|
var DEFAULT_ACCENT = 12;
|
|
@@ -1911,7 +2024,8 @@ var MAX_VELOCITY = 127;
|
|
|
1911
2024
|
function humanize(events, opts = {}) {
|
|
1912
2025
|
const ts = opts.ts ?? DEFAULT_TS3;
|
|
1913
2026
|
assertTimeSignature(ts);
|
|
1914
|
-
assertNoteEvents(events, "humanize events");
|
|
2027
|
+
assertNoteEvents(events, "humanize events", { allowNonPositiveDuration: true });
|
|
2028
|
+
const sounding = dropSilentNotes(events);
|
|
1915
2029
|
const timing = opts.timing ?? DEFAULT_TIMING;
|
|
1916
2030
|
const velocityJitter = opts.velocity ?? DEFAULT_VELOCITY_JITTER;
|
|
1917
2031
|
const accent = opts.accent ?? DEFAULT_ACCENT;
|
|
@@ -1921,7 +2035,7 @@ function humanize(events, opts = {}) {
|
|
|
1921
2035
|
assertRange(accent, 0, 127, "humanize accent");
|
|
1922
2036
|
assertRange(baseVelocity, 0, 127, "humanize base velocity");
|
|
1923
2037
|
const rng = createRng(opts.seed ?? 0);
|
|
1924
|
-
return
|
|
2038
|
+
return sounding.map((event) => {
|
|
1925
2039
|
const timingOffset = rng.float(-timing, timing);
|
|
1926
2040
|
const startBeat = Math.max(0, event.startBeat + timingOffset);
|
|
1927
2041
|
const weight = metricWeight(event.startBeat, ts);
|
|
@@ -1951,9 +2065,9 @@ function quantizeToGrid(beat, barBeats, subdivision, slotsPerBar) {
|
|
|
1951
2065
|
slotIndex
|
|
1952
2066
|
};
|
|
1953
2067
|
}
|
|
1954
|
-
function extractGrooveTemplate(events, ts, subdivision) {
|
|
2068
|
+
function extractGrooveTemplate(events, ts, subdivision = DEFAULT_GROOVE_SUBDIVISION) {
|
|
1955
2069
|
assertTimeSignature(ts);
|
|
1956
|
-
assertNoteEvents(events, "groove source events");
|
|
2070
|
+
assertNoteEvents(events, "groove source events", { allowNonPositiveDuration: true });
|
|
1957
2071
|
assertPositiveInt(subdivision, "groove subdivision");
|
|
1958
2072
|
const barBeats = beatsPerBar(ts);
|
|
1959
2073
|
const slotsPerBar = Math.round(barBeats * subdivision);
|
|
@@ -1962,7 +2076,7 @@ function extractGrooveTemplate(events, ts, subdivision) {
|
|
|
1962
2076
|
const velocitySums = new Array(slotsPerBar).fill(0);
|
|
1963
2077
|
const counts = new Array(slotsPerBar).fill(0);
|
|
1964
2078
|
const velocityCounts = new Array(slotsPerBar).fill(0);
|
|
1965
|
-
for (const event of events) {
|
|
2079
|
+
for (const event of dropSilentNotes(events)) {
|
|
1966
2080
|
const { quantizedBeat, slotIndex } = quantizeToGrid(
|
|
1967
2081
|
event.startBeat,
|
|
1968
2082
|
barBeats,
|
|
@@ -1993,20 +2107,20 @@ function extractGrooveTemplate(events, ts, subdivision) {
|
|
|
1993
2107
|
}
|
|
1994
2108
|
function applyGrooveTemplate(events, template, ts) {
|
|
1995
2109
|
assertTimeSignature(ts);
|
|
1996
|
-
assertNoteEvents(events, "groove target events");
|
|
2110
|
+
assertNoteEvents(events, "groove target events", { allowNonPositiveDuration: true });
|
|
1997
2111
|
assertPositiveInt(template.subdivision, "template subdivision");
|
|
1998
2112
|
assertPositiveInt(template.slotsPerBar, "template slotsPerBar");
|
|
1999
2113
|
assertGenerationBudget(template.slots.length, "template slots");
|
|
2000
2114
|
if (template.slots.length !== template.slotsPerBar) {
|
|
2001
|
-
throw new
|
|
2115
|
+
throw new InvalidInputError("template slots length must equal slotsPerBar");
|
|
2002
2116
|
}
|
|
2003
2117
|
if (template.ts && (template.ts.numerator !== ts.numerator || template.ts.denominator !== ts.denominator)) {
|
|
2004
|
-
throw new
|
|
2118
|
+
throw new InvalidInputError(
|
|
2005
2119
|
`Groove template meter ${formatTimeSignature(template.ts)} does not match apply-time meter ${formatTimeSignature(ts)}`
|
|
2006
2120
|
);
|
|
2007
2121
|
}
|
|
2008
2122
|
const barBeats = beatsPerBar(ts);
|
|
2009
|
-
return events.map((event) => {
|
|
2123
|
+
return dropSilentNotes(events).map((event) => {
|
|
2010
2124
|
const { quantizedBeat, slotIndex } = quantizeToGrid(
|
|
2011
2125
|
event.startBeat,
|
|
2012
2126
|
barBeats,
|
|
@@ -2026,15 +2140,20 @@ function applyGrooveTemplate(events, template, ts) {
|
|
|
2026
2140
|
}
|
|
2027
2141
|
|
|
2028
2142
|
// src/generate/harmonize/index.ts
|
|
2029
|
-
var FALLBACK = {
|
|
2143
|
+
var FALLBACK = {
|
|
2144
|
+
rootPc: 0,
|
|
2145
|
+
quality: "maj",
|
|
2146
|
+
secondaryDominant: false,
|
|
2147
|
+
base: 0,
|
|
2148
|
+
pcs: chordPitchClasses(makeChord(0, "maj"))
|
|
2149
|
+
};
|
|
2150
|
+
var DEFAULT_HARMONIC_RHYTHM = 2;
|
|
2151
|
+
var DEFAULT_PLACEMENT = { transposeSearch: false, octaveSearch: false };
|
|
2030
2152
|
var DEFAULT_METER = { numerator: 4, denominator: 4 };
|
|
2031
2153
|
var COMFORT_LOW = 55;
|
|
2032
2154
|
var COMFORT_HIGH = 79;
|
|
2033
2155
|
var TESSITURA_WEIGHT = 1e-3;
|
|
2034
|
-
var
|
|
2035
|
-
function pitchClass2(pitch) {
|
|
2036
|
-
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
2037
|
-
}
|
|
2156
|
+
var TIE_BREAK_JITTER2 = 1e-6;
|
|
2038
2157
|
function inferKey(melody) {
|
|
2039
2158
|
let best = majorKey(0);
|
|
2040
2159
|
let bestScore = Number.NEGATIVE_INFINITY;
|
|
@@ -2043,13 +2162,13 @@ function inferKey(melody) {
|
|
|
2043
2162
|
candidates.push(majorKey(tonic), minorKey(tonic));
|
|
2044
2163
|
}
|
|
2045
2164
|
for (const key of candidates) {
|
|
2046
|
-
const tonic =
|
|
2165
|
+
const tonic = pitchClassOf(key.rootPc);
|
|
2047
2166
|
const isMinor = key.modeMask12 === NATURAL_MINOR_MASK;
|
|
2048
2167
|
const leadingTone = (tonic + 11) % 12;
|
|
2049
2168
|
let score = 0;
|
|
2050
2169
|
for (const n of melody) {
|
|
2051
2170
|
const w = Math.max(0.25, n.durationBeat);
|
|
2052
|
-
const pc =
|
|
2171
|
+
const pc = pitchClassOf(n.pitch);
|
|
2053
2172
|
if (isScaleTone(n.pitch, key) || isMinor && pc === leadingTone) {
|
|
2054
2173
|
score += w;
|
|
2055
2174
|
}
|
|
@@ -2066,22 +2185,28 @@ function inferKey(melody) {
|
|
|
2066
2185
|
}
|
|
2067
2186
|
function buildCandidates(key, reharmonize) {
|
|
2068
2187
|
const tones = scaleTonesInDegreeOrder(key);
|
|
2069
|
-
const candidates = tones.map((rootPc, degree) =>
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2188
|
+
const candidates = tones.map((rootPc, degree) => {
|
|
2189
|
+
const quality = diatonicTriad(degree, key).quality;
|
|
2190
|
+
return {
|
|
2191
|
+
rootPc,
|
|
2192
|
+
quality,
|
|
2193
|
+
degree,
|
|
2194
|
+
secondaryDominant: false,
|
|
2195
|
+
base: 0,
|
|
2196
|
+
pcs: chordPitchClasses(makeChord(rootPc, quality))
|
|
2197
|
+
};
|
|
2198
|
+
});
|
|
2076
2199
|
if (reharmonize !== "diatonic") {
|
|
2077
2200
|
for (const target of [1, 3, 4, 5]) {
|
|
2078
2201
|
const targetRoot = tones[target] ?? 0;
|
|
2202
|
+
const rootPc = (targetRoot + 7) % 12;
|
|
2079
2203
|
candidates.push({
|
|
2080
|
-
rootPc
|
|
2204
|
+
rootPc,
|
|
2081
2205
|
quality: "dom7",
|
|
2082
2206
|
secondaryDominant: true,
|
|
2083
2207
|
targetDegree: target,
|
|
2084
|
-
base: 1.5
|
|
2208
|
+
base: 1.5,
|
|
2209
|
+
pcs: chordPitchClasses(makeChord(rootPc, "dom7"))
|
|
2085
2210
|
});
|
|
2086
2211
|
}
|
|
2087
2212
|
}
|
|
@@ -2095,7 +2220,8 @@ function buildCandidates(key, reharmonize) {
|
|
|
2095
2220
|
rootPc: chord.rootPc,
|
|
2096
2221
|
quality: chord.quality,
|
|
2097
2222
|
secondaryDominant: false,
|
|
2098
|
-
base: 1.25
|
|
2223
|
+
base: 1.25,
|
|
2224
|
+
pcs: chordPitchClasses(chord)
|
|
2099
2225
|
});
|
|
2100
2226
|
}
|
|
2101
2227
|
}
|
|
@@ -2110,8 +2236,8 @@ function buildCandidates(key, reharmonize) {
|
|
|
2110
2236
|
return true;
|
|
2111
2237
|
});
|
|
2112
2238
|
}
|
|
2113
|
-
function emissionCost(seg, cand, melody, key) {
|
|
2114
|
-
const pcs =
|
|
2239
|
+
function emissionCost(seg, cand, melody, key, ts) {
|
|
2240
|
+
const pcs = cand.pcs;
|
|
2115
2241
|
let cost = cand.base;
|
|
2116
2242
|
for (const idx of seg.noteIndices) {
|
|
2117
2243
|
const note = melody[idx];
|
|
@@ -2124,10 +2250,10 @@ function emissionCost(seg, cand, melody, key) {
|
|
|
2124
2250
|
continue;
|
|
2125
2251
|
}
|
|
2126
2252
|
const w = Math.max(0.25, overlap);
|
|
2127
|
-
if (pcs.includes(
|
|
2253
|
+
if (pcs.includes(pitchClassOf(note.pitch))) {
|
|
2128
2254
|
continue;
|
|
2129
2255
|
}
|
|
2130
|
-
const strong = isStrongBeat(overlapStart,
|
|
2256
|
+
const strong = isStrongBeat(overlapStart, ts);
|
|
2131
2257
|
cost += (strong ? 10 : 2) * w;
|
|
2132
2258
|
if (!isScaleTone(note.pitch, key)) {
|
|
2133
2259
|
cost += w;
|
|
@@ -2161,13 +2287,13 @@ function transitionCost(prev, cur, tonicPc) {
|
|
|
2161
2287
|
cost += dist * 0.3;
|
|
2162
2288
|
return cost;
|
|
2163
2289
|
}
|
|
2164
|
-
function harmonizeOnce(melody, key, candidates, segments, jitter) {
|
|
2165
|
-
const tonicPc =
|
|
2290
|
+
function harmonizeOnce(melody, key, candidates, segments, jitter, ts) {
|
|
2291
|
+
const tonicPc = pitchClassOf(key.rootPc);
|
|
2166
2292
|
const n = candidates.length;
|
|
2167
2293
|
const candAt = (i) => candidates[i] ?? FALLBACK;
|
|
2168
2294
|
const seg0 = segments[0];
|
|
2169
2295
|
let dp = candidates.map(
|
|
2170
|
-
(c, ci) => (seg0 ? emissionCost(seg0, c, melody, key) : 0) + (jitter[ci] ?? 0)
|
|
2296
|
+
(c, ci) => (seg0 ? emissionCost(seg0, c, melody, key, ts) : 0) + (jitter[ci] ?? 0)
|
|
2171
2297
|
);
|
|
2172
2298
|
const back = [];
|
|
2173
2299
|
for (let s = 1; s < segments.length; s += 1) {
|
|
@@ -2187,7 +2313,7 @@ function harmonizeOnce(melody, key, candidates, segments, jitter) {
|
|
|
2187
2313
|
bestPrev = p;
|
|
2188
2314
|
}
|
|
2189
2315
|
}
|
|
2190
|
-
next[c] = best + emissionCost(seg, candAt(c), melody, key) + (jitter[c] ?? 0);
|
|
2316
|
+
next[c] = best + emissionCost(seg, candAt(c), melody, key, ts) + (jitter[c] ?? 0);
|
|
2191
2317
|
ptr[c] = bestPrev;
|
|
2192
2318
|
}
|
|
2193
2319
|
dp = next;
|
|
@@ -2212,20 +2338,26 @@ function harmonizeOnce(melody, key, candidates, segments, jitter) {
|
|
|
2212
2338
|
return { cost: bestCost, path };
|
|
2213
2339
|
}
|
|
2214
2340
|
function harmonizeMelody(opts) {
|
|
2215
|
-
const noteIndex = createNoteEventIndex(opts.melody);
|
|
2216
|
-
const
|
|
2217
|
-
|
|
2341
|
+
const noteIndex = createNoteEventIndex(opts.melody, { allowNonPositiveDuration: true });
|
|
2342
|
+
const ts = opts.ts ?? DEFAULT_METER;
|
|
2343
|
+
assertTimeSignature(ts);
|
|
2344
|
+
const requestedKey = opts.key ?? "infer";
|
|
2345
|
+
const key = requestedKey === "infer" ? inferKey(opts.melody) : requestedKey;
|
|
2346
|
+
const placement = opts.placement ?? DEFAULT_PLACEMENT;
|
|
2347
|
+
if (noteIndex.notes.every((indexed) => indexed.note.durationBeat <= 0)) {
|
|
2348
|
+
return { transposeSemitones: 0, key, chords: [], melodyRoles: [] };
|
|
2349
|
+
}
|
|
2350
|
+
const candidates = buildCandidates(key, opts.reharmonize ?? "diatonic");
|
|
2218
2351
|
const candAt = (i) => candidates[i] ?? FALLBACK;
|
|
2219
2352
|
const rng = createRng(opts.seed ?? 0);
|
|
2220
|
-
const jitter = candidates.map(() => rng.next() *
|
|
2353
|
+
const jitter = candidates.map(() => rng.next() * TIE_BREAK_JITTER2);
|
|
2221
2354
|
const melodyEnd = noteIndex.notes.reduce((m, n) => Math.max(m, n.endBeat), 0);
|
|
2222
|
-
const
|
|
2223
|
-
opts.harmonicRhythm,
|
|
2355
|
+
const hr = assertRange(
|
|
2356
|
+
opts.harmonicRhythm ?? DEFAULT_HARMONIC_RHYTHM,
|
|
2224
2357
|
Number.MIN_VALUE,
|
|
2225
2358
|
Number.MAX_SAFE_INTEGER,
|
|
2226
2359
|
"harmonic rhythm"
|
|
2227
2360
|
);
|
|
2228
|
-
const hr = Math.max(0.25, requestedHr);
|
|
2229
2361
|
const segCount = Math.max(1, Math.ceil(melodyEnd / hr));
|
|
2230
2362
|
assertGenerationBudget(segCount, "harmonic segments");
|
|
2231
2363
|
assertGenerationBudget(segCount * candidates.length * candidates.length, "harmonization search");
|
|
@@ -2236,6 +2368,9 @@ function harmonizeMelody(opts) {
|
|
|
2236
2368
|
}));
|
|
2237
2369
|
let memberships = 0;
|
|
2238
2370
|
for (const indexed of noteIndex.notes) {
|
|
2371
|
+
if (indexed.note.durationBeat <= 0) {
|
|
2372
|
+
continue;
|
|
2373
|
+
}
|
|
2239
2374
|
const first = Math.max(0, Math.floor(indexed.note.startBeat / hr));
|
|
2240
2375
|
const lastExclusive = Math.min(segCount, Math.ceil(indexed.endBeat / hr - Number.EPSILON));
|
|
2241
2376
|
memberships += Math.max(0, lastExclusive - first);
|
|
@@ -2245,14 +2380,14 @@ function harmonizeMelody(opts) {
|
|
|
2245
2380
|
}
|
|
2246
2381
|
}
|
|
2247
2382
|
const transposes = [0];
|
|
2248
|
-
if (
|
|
2383
|
+
if (placement.transposeSearch) {
|
|
2249
2384
|
for (let s = -6; s <= 6; s += 1) {
|
|
2250
2385
|
if (s !== 0) {
|
|
2251
2386
|
transposes.push(s);
|
|
2252
2387
|
}
|
|
2253
2388
|
}
|
|
2254
2389
|
}
|
|
2255
|
-
if (
|
|
2390
|
+
if (placement.octaveSearch) {
|
|
2256
2391
|
for (const o of [-12, 12]) {
|
|
2257
2392
|
if (!transposes.includes(o)) {
|
|
2258
2393
|
transposes.push(o);
|
|
@@ -2263,13 +2398,13 @@ function harmonizeMelody(opts) {
|
|
|
2263
2398
|
let bestTs = 0;
|
|
2264
2399
|
let bestPath = [];
|
|
2265
2400
|
let bestMelody = opts.melody;
|
|
2266
|
-
for (const
|
|
2267
|
-
const shifted = opts.melody.map((n) => ({ ...n, pitch: n.pitch +
|
|
2268
|
-
const { cost, path } = harmonizeOnce(shifted, key, candidates, segments, jitter);
|
|
2401
|
+
for (const shift of transposes) {
|
|
2402
|
+
const shifted = opts.melody.map((n) => ({ ...n, pitch: n.pitch + shift }));
|
|
2403
|
+
const { cost, path } = harmonizeOnce(shifted, key, candidates, segments, jitter, ts);
|
|
2269
2404
|
const total = cost + tessituraCost(shifted);
|
|
2270
2405
|
if (total < bestCost) {
|
|
2271
2406
|
bestCost = total;
|
|
2272
|
-
bestTs =
|
|
2407
|
+
bestTs = shift;
|
|
2273
2408
|
bestPath = path;
|
|
2274
2409
|
bestMelody = shifted;
|
|
2275
2410
|
}
|
|
@@ -2299,10 +2434,13 @@ function harmonizeMelody(opts) {
|
|
|
2299
2434
|
}
|
|
2300
2435
|
|
|
2301
2436
|
// src/generate/motif/index.ts
|
|
2302
|
-
function pitchClass3(pitch) {
|
|
2303
|
-
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
2304
|
-
}
|
|
2305
2437
|
var MIN_TILE_SPAN = 1 / 256;
|
|
2438
|
+
var DEFAULT_TS4 = { numerator: 4, denominator: 4 };
|
|
2439
|
+
var EPS3 = 1e-9;
|
|
2440
|
+
function isDownbeat(startBeat, barBeats) {
|
|
2441
|
+
const bar = startBeat / barBeats;
|
|
2442
|
+
return Math.abs(bar - Math.round(bar)) < EPS3;
|
|
2443
|
+
}
|
|
2306
2444
|
function upScaleTone(pitch, key) {
|
|
2307
2445
|
for (let d = 1; d <= 12; d += 1) {
|
|
2308
2446
|
if (isScaleTone(pitch + d, key)) {
|
|
@@ -2330,10 +2468,10 @@ function stepDiatonic(pitch, degrees, key) {
|
|
|
2330
2468
|
function nearestChordTone(pitch, chord) {
|
|
2331
2469
|
const pcs = chordPitchClasses(chord);
|
|
2332
2470
|
for (let d = 0; d <= 12; d += 1) {
|
|
2333
|
-
if (pcs.includes(
|
|
2471
|
+
if (pcs.includes(pitchClassOf(pitch - d))) {
|
|
2334
2472
|
return pitch - d;
|
|
2335
2473
|
}
|
|
2336
|
-
if (pcs.includes(
|
|
2474
|
+
if (pcs.includes(pitchClassOf(pitch + d))) {
|
|
2337
2475
|
return pitch + d;
|
|
2338
2476
|
}
|
|
2339
2477
|
}
|
|
@@ -2387,14 +2525,15 @@ function contourOffsets(contour, count) {
|
|
|
2387
2525
|
function generateMotif(opts) {
|
|
2388
2526
|
const contour = opts.contour ?? "arch";
|
|
2389
2527
|
const bars = assertPositiveInt(opts.bars, "motif bars");
|
|
2390
|
-
const
|
|
2528
|
+
const barBeats = beatsPerBar(opts.ts ?? DEFAULT_TS4);
|
|
2529
|
+
const totalBeats = bars * barBeats;
|
|
2391
2530
|
const noteCount = Math.max(3, bars * 2);
|
|
2392
2531
|
assertGenerationBudget(noteCount, "motif notes");
|
|
2393
2532
|
const beatsPerNote = totalBeats / noteCount;
|
|
2394
2533
|
const rng = createRng(opts.seed ?? 0);
|
|
2395
2534
|
const requestedJitter = assertFiniteNumber(opts.jitter ?? 0, "motif jitter");
|
|
2396
2535
|
const jitterProb = Math.min(1, Math.max(0, requestedJitter));
|
|
2397
|
-
const tonic =
|
|
2536
|
+
const tonic = pitchClassOf(opts.key.rootPc) + 60;
|
|
2398
2537
|
const offsets = contourOffsets(contour, noteCount);
|
|
2399
2538
|
const notes = [];
|
|
2400
2539
|
for (let i = 0; i < noteCount; i += 1) {
|
|
@@ -2404,14 +2543,30 @@ function generateMotif(opts) {
|
|
|
2404
2543
|
}
|
|
2405
2544
|
let pitch = stepDiatonic(tonic, (offsets[i] ?? 0) + jitter, opts.key);
|
|
2406
2545
|
const startBeat = i * beatsPerNote;
|
|
2407
|
-
if (opts.chord && startBeat
|
|
2546
|
+
if (opts.chord && isDownbeat(startBeat, barBeats)) {
|
|
2408
2547
|
pitch = nearestChordTone(pitch, opts.chord);
|
|
2409
2548
|
}
|
|
2410
2549
|
notes.push({ pitch, startBeat, durationBeat: beatsPerNote });
|
|
2411
2550
|
}
|
|
2412
2551
|
return { notes };
|
|
2413
2552
|
}
|
|
2553
|
+
function motifToNoteEvents(cell) {
|
|
2554
|
+
return cell.notes.map((note) => ({ ...note }));
|
|
2555
|
+
}
|
|
2414
2556
|
function transformMotif(cell, t, amount, key) {
|
|
2557
|
+
return assertInRange(transformUnchecked(cell, t, amount, key));
|
|
2558
|
+
}
|
|
2559
|
+
function assertInRange(cell) {
|
|
2560
|
+
for (const note of cell.notes) {
|
|
2561
|
+
if (note.pitch < 0 || note.pitch > 127) {
|
|
2562
|
+
throw new InvalidInputError(
|
|
2563
|
+
`transformed motif pitch ${note.pitch} is outside the MIDI range 0..127`
|
|
2564
|
+
);
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
return cell;
|
|
2568
|
+
}
|
|
2569
|
+
function transformUnchecked(cell, t, amount, key) {
|
|
2415
2570
|
assertNoteEvents(cell.notes, "motif notes");
|
|
2416
2571
|
if (amount !== void 0) {
|
|
2417
2572
|
assertFiniteNumber(amount, "motif transform amount");
|
|
@@ -2420,7 +2575,7 @@ function transformMotif(cell, t, amount, key) {
|
|
|
2420
2575
|
assertInteger(amount, "diatonic transform amount");
|
|
2421
2576
|
}
|
|
2422
2577
|
if ((t === "augment" || t === "diminish") && amount !== void 0 && amount <= 0) {
|
|
2423
|
-
throw new
|
|
2578
|
+
throw new InvalidInputError("time transform amount must be positive");
|
|
2424
2579
|
}
|
|
2425
2580
|
const notes = cell.notes;
|
|
2426
2581
|
switch (t) {
|
|
@@ -2480,11 +2635,11 @@ function scaleTime(cell, factor) {
|
|
|
2480
2635
|
}))
|
|
2481
2636
|
};
|
|
2482
2637
|
}
|
|
2483
|
-
function developMotif(cell, timeline, key, bars) {
|
|
2638
|
+
function developMotif(cell, timeline, key, bars, ts = DEFAULT_TS4) {
|
|
2484
2639
|
assertPositiveInt(bars, "development bars");
|
|
2485
2640
|
assertNoteEvents(cell.notes, "motif notes");
|
|
2486
2641
|
const span = cellSpan(cell);
|
|
2487
|
-
const totalBeats = bars *
|
|
2642
|
+
const totalBeats = bars * beatsPerBar(ts);
|
|
2488
2643
|
const origin = cellOrigin(cell.notes);
|
|
2489
2644
|
const out = [];
|
|
2490
2645
|
if (span <= 0) {
|
|
@@ -2503,7 +2658,7 @@ function developMotif(cell, timeline, key, bars) {
|
|
|
2503
2658
|
const chord = timeline.at(startBeat);
|
|
2504
2659
|
let pitch = n.pitch;
|
|
2505
2660
|
if (chord) {
|
|
2506
|
-
const isMember = chordPitchClasses(chord).includes(
|
|
2661
|
+
const isMember = chordPitchClasses(chord).includes(pitchClassOf(pitch));
|
|
2507
2662
|
pitch = isMember ? pitch : nearestChordTone(pitch, chord);
|
|
2508
2663
|
} else {
|
|
2509
2664
|
pitch = nearestScaleTone(pitch, key);
|
|
@@ -2515,6 +2670,20 @@ function developMotif(cell, timeline, key, bars) {
|
|
|
2515
2670
|
}
|
|
2516
2671
|
|
|
2517
2672
|
// src/generate/progression/index.ts
|
|
2673
|
+
var BORROWED_DEGREES = {
|
|
2674
|
+
/** Flat submediant: bVI. */
|
|
2675
|
+
bVI: 8,
|
|
2676
|
+
/** Flat subtonic: bVII. */
|
|
2677
|
+
bVII: 10,
|
|
2678
|
+
/** Flat mediant: bIII. */
|
|
2679
|
+
bIII: 11,
|
|
2680
|
+
/** Minor subdominant borrowed from the parallel minor: iv. */
|
|
2681
|
+
iv: 12,
|
|
2682
|
+
/** Neapolitan: bII. */
|
|
2683
|
+
bII: 13,
|
|
2684
|
+
/** Sharp subdominant, the diminished #IV. */
|
|
2685
|
+
sharpIV: 14
|
|
2686
|
+
};
|
|
2518
2687
|
var PRESETS = [
|
|
2519
2688
|
{
|
|
2520
2689
|
id: "fourChordPop",
|
|
@@ -2672,18 +2841,12 @@ var PRESETS = [
|
|
|
2672
2841
|
}
|
|
2673
2842
|
];
|
|
2674
2843
|
var BORROWED_OFFSET = {
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
12: 5,
|
|
2682
|
-
// iv
|
|
2683
|
-
13: 1,
|
|
2684
|
-
// bII
|
|
2685
|
-
14: 6
|
|
2686
|
-
// #IV
|
|
2844
|
+
[BORROWED_DEGREES.bVI]: 8,
|
|
2845
|
+
[BORROWED_DEGREES.bVII]: 10,
|
|
2846
|
+
[BORROWED_DEGREES.bIII]: 3,
|
|
2847
|
+
[BORROWED_DEGREES.iv]: 5,
|
|
2848
|
+
[BORROWED_DEGREES.bII]: 1,
|
|
2849
|
+
[BORROWED_DEGREES.sharpIV]: 6
|
|
2687
2850
|
};
|
|
2688
2851
|
function progressions() {
|
|
2689
2852
|
return PRESETS.map((p) => ({ ...p, degrees: [...p.degrees], styles: [...p.styles] }));
|
|
@@ -2711,35 +2874,78 @@ function autoQuality(degree, key) {
|
|
|
2711
2874
|
}
|
|
2712
2875
|
return "maj";
|
|
2713
2876
|
}
|
|
2877
|
+
function resolveCycle(degrees, key, ext) {
|
|
2878
|
+
const steps = [];
|
|
2879
|
+
for (const degree of degrees) {
|
|
2880
|
+
const step = {
|
|
2881
|
+
source: degree,
|
|
2882
|
+
rootPc: degreeToRootPc(degree, key),
|
|
2883
|
+
quality: ext !== void 0 && ext !== "auto" ? ext : autoQuality(degree, key)
|
|
2884
|
+
};
|
|
2885
|
+
if (degree >= 0 && degree <= 6) {
|
|
2886
|
+
step.degree = degree;
|
|
2887
|
+
}
|
|
2888
|
+
const previous = steps[steps.length - 1];
|
|
2889
|
+
if (previous !== void 0 && previous.source !== step.source && sameStep(previous, step)) {
|
|
2890
|
+
continue;
|
|
2891
|
+
}
|
|
2892
|
+
steps.push(step);
|
|
2893
|
+
}
|
|
2894
|
+
const first = steps[0];
|
|
2895
|
+
const last = steps[steps.length - 1];
|
|
2896
|
+
if (steps.length > 1 && first !== void 0 && last !== void 0 && first.source !== last.source && sameStep(first, last)) {
|
|
2897
|
+
steps.pop();
|
|
2898
|
+
}
|
|
2899
|
+
return steps.length > 0 ? steps : [{ source: 0, degree: 0, rootPc: degreeToRootPc(0, key), quality: "maj" }];
|
|
2900
|
+
}
|
|
2901
|
+
function sameStep(a, b) {
|
|
2902
|
+
return a.rootPc === b.rootPc && a.quality === b.quality;
|
|
2903
|
+
}
|
|
2904
|
+
function pickProgressionPreset(style, seed = 0) {
|
|
2905
|
+
const pool = PRESETS.filter((preset) => preset.styles.includes(style));
|
|
2906
|
+
if (pool.length === 0) {
|
|
2907
|
+
throw new InvalidInputError(`Unknown progression style: ${style}`);
|
|
2908
|
+
}
|
|
2909
|
+
const rng = createRng(seed);
|
|
2910
|
+
const index = Math.floor(rng.next() * pool.length) % pool.length;
|
|
2911
|
+
return pool[index] ?? PRESETS[0];
|
|
2912
|
+
}
|
|
2714
2913
|
function generateProgression(opts) {
|
|
2715
2914
|
assertPositiveInt(opts.bars, "progression bars");
|
|
2716
2915
|
assertGenerationBudget(opts.bars, "progression chords");
|
|
2717
2916
|
const seed = opts.seed ?? 0;
|
|
2718
2917
|
let preset;
|
|
2719
|
-
if (opts.
|
|
2918
|
+
if (opts.preset !== void 0) {
|
|
2919
|
+
if (opts.preset.degrees.length === 0) {
|
|
2920
|
+
throw new InvalidInputError("progression preset must name at least one degree");
|
|
2921
|
+
}
|
|
2922
|
+
preset = {
|
|
2923
|
+
id: opts.preset.id ?? "custom",
|
|
2924
|
+
name: opts.preset.name ?? "Custom",
|
|
2925
|
+
degrees: opts.preset.degrees,
|
|
2926
|
+
functional: opts.preset.functional ?? "loop",
|
|
2927
|
+
styles: opts.preset.styles ?? [opts.style]
|
|
2928
|
+
};
|
|
2929
|
+
}
|
|
2930
|
+
if (preset === void 0 && opts.presetId !== void 0) {
|
|
2720
2931
|
preset = PRESETS.find((p) => p.id === opts.presetId);
|
|
2721
2932
|
if (preset === void 0) {
|
|
2722
|
-
throw new
|
|
2933
|
+
throw new InvalidInputError(`Unknown progression preset: ${opts.presetId}`);
|
|
2723
2934
|
}
|
|
2724
2935
|
}
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
const candidates = pool.length > 0 ? pool : PRESETS;
|
|
2728
|
-
const rng = createRng(seed);
|
|
2729
|
-
const index = Math.floor(rng.next() * candidates.length) % candidates.length;
|
|
2730
|
-
preset = candidates[index] ?? PRESETS[0];
|
|
2731
|
-
}
|
|
2732
|
-
const degrees = preset?.degrees ?? [0];
|
|
2936
|
+
preset ??= pickProgressionPreset(opts.style, seed);
|
|
2937
|
+
const cycle = resolveCycle(preset?.degrees ?? [0], opts.key, opts.ext);
|
|
2733
2938
|
const chords = [];
|
|
2734
2939
|
for (let bar = 0; bar < opts.bars; bar += 1) {
|
|
2735
|
-
const
|
|
2736
|
-
const quality = opts.ext !== void 0 && opts.ext !== "auto" ? opts.ext : autoQuality(degree, opts.key);
|
|
2940
|
+
const step = cycle[bar % cycle.length];
|
|
2737
2941
|
const chord = {
|
|
2738
|
-
rootPc:
|
|
2739
|
-
quality,
|
|
2740
|
-
startBeat: bar * 4
|
|
2741
|
-
degree: degree >= 0 && degree <= 6 ? degree : void 0
|
|
2942
|
+
rootPc: step?.rootPc ?? 0,
|
|
2943
|
+
quality: step?.quality ?? "maj",
|
|
2944
|
+
startBeat: bar * 4
|
|
2742
2945
|
};
|
|
2946
|
+
if (step?.degree !== void 0) {
|
|
2947
|
+
chord.degree = step.degree;
|
|
2948
|
+
}
|
|
2743
2949
|
chords.push(chord);
|
|
2744
2950
|
}
|
|
2745
2951
|
if (opts.reharmonize) {
|
|
@@ -2772,6 +2978,17 @@ function generateProgression(opts) {
|
|
|
2772
2978
|
}
|
|
2773
2979
|
|
|
2774
2980
|
// src/generate/rhythm/index.ts
|
|
2981
|
+
var DEFAULT_RHYTHM_VELOCITY = 96;
|
|
2982
|
+
function rhythmToNoteEvents(events, pitch, velocity = DEFAULT_RHYTHM_VELOCITY) {
|
|
2983
|
+
assertFiniteNumber(pitch, "pitch");
|
|
2984
|
+
assertRange(velocity, 0, 127, "velocity");
|
|
2985
|
+
return events.map((event) => ({
|
|
2986
|
+
pitch,
|
|
2987
|
+
startBeat: event.position,
|
|
2988
|
+
durationBeat: event.duration,
|
|
2989
|
+
velocity
|
|
2990
|
+
}));
|
|
2991
|
+
}
|
|
2775
2992
|
var DEFAULT_SUBDIVISION = 2;
|
|
2776
2993
|
var DEFAULT_DENSITY = 0.5;
|
|
2777
2994
|
var DEFAULT_BARS = 1;
|
|
@@ -2791,8 +3008,7 @@ function generateRhythm(ts, opts = {}) {
|
|
|
2791
3008
|
const seed = opts.seed ?? 0;
|
|
2792
3009
|
const bars = opts.bars ?? DEFAULT_BARS;
|
|
2793
3010
|
const subdivision = opts.subdivision ?? DEFAULT_SUBDIVISION;
|
|
2794
|
-
const
|
|
2795
|
-
const density = Math.min(1, Math.max(0, requestedDensity));
|
|
3011
|
+
const density = assertRange(opts.density ?? DEFAULT_DENSITY, 0, 1, "rhythm density");
|
|
2796
3012
|
assertPositiveInt(bars, "rhythm bars");
|
|
2797
3013
|
assertPositiveInt(subdivision, "rhythm subdivision");
|
|
2798
3014
|
const barBeats = beatsPerBar(ts);
|
|
@@ -2834,21 +3050,28 @@ function rhythmDensity(events, ts) {
|
|
|
2834
3050
|
}
|
|
2835
3051
|
|
|
2836
3052
|
export {
|
|
3053
|
+
BASS_STYLES,
|
|
2837
3054
|
generateBassLine,
|
|
2838
3055
|
generateCounterMelody,
|
|
3056
|
+
DRUM_NOTES,
|
|
3057
|
+
drumVoiceOf,
|
|
2839
3058
|
generateDrums,
|
|
2840
3059
|
humanize,
|
|
2841
3060
|
extractGrooveTemplate,
|
|
2842
3061
|
applyGrooveTemplate,
|
|
2843
3062
|
harmonizeMelody,
|
|
2844
3063
|
generateMotif,
|
|
3064
|
+
motifToNoteEvents,
|
|
2845
3065
|
transformMotif,
|
|
2846
3066
|
developMotif,
|
|
3067
|
+
BORROWED_DEGREES,
|
|
2847
3068
|
progressions,
|
|
2848
3069
|
progressionsByStyle,
|
|
3070
|
+
pickProgressionPreset,
|
|
2849
3071
|
generateProgression,
|
|
3072
|
+
rhythmToNoteEvents,
|
|
2850
3073
|
onsetWeightCurve,
|
|
2851
3074
|
generateRhythm,
|
|
2852
3075
|
rhythmDensity
|
|
2853
3076
|
};
|
|
2854
|
-
//# sourceMappingURL=chunk-
|
|
3077
|
+
//# sourceMappingURL=chunk-7NKDPF74.js.map
|