@libraz/libcantus 0.9.3 → 0.9.5
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 +29 -20
- package/README_ja.md +268 -0
- package/dist/analyze/index.cjs +39 -1947
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +161 -91
- package/dist/analyze/index.d.ts +161 -91
- package/dist/analyze/index.js +19 -8
- package/dist/chunk-2A2QMA74.cjs +261 -0
- package/dist/chunk-2A2QMA74.cjs.map +1 -0
- package/dist/chunk-2PTFGVD7.js +261 -0
- package/dist/chunk-2PTFGVD7.js.map +1 -0
- package/dist/chunk-357ZDPVU.cjs +162 -0
- package/dist/chunk-357ZDPVU.cjs.map +1 -0
- package/dist/chunk-3KX76RLG.js +1548 -0
- package/dist/chunk-3KX76RLG.js.map +1 -0
- package/dist/chunk-4VZ5MZAM.cjs +889 -0
- package/dist/chunk-4VZ5MZAM.cjs.map +1 -0
- package/dist/{chunk-C5XYKUVG.js → chunk-7EONYVI5.js} +471 -171
- package/dist/chunk-7EONYVI5.js.map +1 -0
- package/dist/{chunk-IT2O4XFE.js → chunk-7JEP3MFY.js} +42 -34
- package/dist/chunk-7JEP3MFY.js.map +1 -0
- package/dist/{chunk-4MHA432G.js → chunk-AID75RN3.js} +572 -250
- package/dist/chunk-AID75RN3.js.map +1 -0
- package/dist/chunk-BSLTLGM3.cjs +497 -0
- package/dist/chunk-BSLTLGM3.cjs.map +1 -0
- package/dist/chunk-CU6VG2YM.cjs +109 -0
- package/dist/chunk-CU6VG2YM.cjs.map +1 -0
- package/dist/{chunk-EH6EM37U.js → chunk-DZMHIGI4.js} +81 -59
- package/dist/chunk-DZMHIGI4.js.map +1 -0
- package/dist/{chunk-AKLYPWIN.js → chunk-EDOTEABK.js} +45 -19
- package/dist/chunk-EDOTEABK.js.map +1 -0
- package/dist/{chunk-2ZWMSJH6.js → chunk-HN2OEUN2.js} +7 -4
- package/dist/chunk-HN2OEUN2.js.map +1 -0
- package/dist/{chunk-MIWO425M.js → chunk-KBTTEX7Q.js} +128 -43
- package/dist/chunk-KBTTEX7Q.js.map +1 -0
- package/dist/chunk-KQN2ZYND.cjs +724 -0
- package/dist/chunk-KQN2ZYND.cjs.map +1 -0
- package/dist/chunk-MPTVJIVG.cjs +3176 -0
- package/dist/chunk-MPTVJIVG.cjs.map +1 -0
- package/dist/chunk-MTMZTMWM.cjs +53 -0
- package/dist/chunk-MTMZTMWM.cjs.map +1 -0
- package/dist/{chunk-CHMXELDS.js → chunk-NNB67XA2.js} +372 -197
- package/dist/chunk-NNB67XA2.js.map +1 -0
- package/dist/chunk-OL3IHHSJ.cjs +1204 -0
- package/dist/chunk-OL3IHHSJ.cjs.map +1 -0
- package/dist/chunk-PI3UXRWK.cjs +528 -0
- package/dist/chunk-PI3UXRWK.cjs.map +1 -0
- package/dist/chunk-R2VVFAIN.js +528 -0
- package/dist/chunk-R2VVFAIN.js.map +1 -0
- package/dist/chunk-T64C7ZD7.cjs +1 -0
- package/dist/chunk-T64C7ZD7.cjs.map +1 -0
- package/dist/chunk-WFDXOLUB.cjs +337 -0
- package/dist/chunk-WFDXOLUB.cjs.map +1 -0
- package/dist/chunk-WTAN4ACG.cjs +1548 -0
- package/dist/chunk-WTAN4ACG.cjs.map +1 -0
- package/dist/chunk-YFJXS3AU.cjs +33 -0
- package/dist/chunk-YFJXS3AU.cjs.map +1 -0
- package/dist/{chunk-GOAW4VYL.js → chunk-YUE6TJ4M.js} +315 -145
- package/dist/chunk-YUE6TJ4M.js.map +1 -0
- package/dist/{chunk-62QZKCJQ.js → chunk-ZGLJM4A4.js} +10 -6
- package/dist/chunk-ZGLJM4A4.js.map +1 -0
- package/dist/core/index.cjs +140 -647
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +313 -41
- package/dist/core/index.d.ts +313 -41
- package/dist/core/index.js +64 -16
- package/dist/{function-DVH4Eusr.d.ts → function-CxixTB02.d.cts} +122 -10
- package/dist/{function-DYt4iKsn.d.cts → function-DTCt675z.d.ts} +122 -10
- package/dist/generate/index.cjs +28 -4085
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +374 -66
- package/dist/generate/index.d.ts +374 -66
- package/dist/generate/index.js +23 -9
- package/dist/{index-Jc0Ryi79.d.ts → index-B2Ulxxpo.d.cts} +21 -5
- package/dist/{index-CpOsP1y9.d.cts → index-BkYJ0xMq.d.cts} +18 -17
- package/dist/{index-CxqBQtH1.d.cts → index-BuaB-ypt.d.ts} +21 -5
- package/dist/index-CAN7v-kw.d.ts +199 -0
- package/dist/{index-Bt4tOUD9.d.ts → index-COw1Ky9g.d.ts} +93 -8
- package/dist/{index-Dikst44v.d.cts → index-Cfx8xgH0.d.ts} +14 -11
- package/dist/{index-DxvARIP2.d.ts → index-DRtIhlJj.d.ts} +18 -17
- 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-alrRninU.d.cts +199 -0
- package/dist/{index-BNlGP86A.d.cts → index-oTQbYYIs.d.cts} +93 -8
- package/dist/index-tR9jTn26.d.cts +275 -0
- package/dist/index-tR9jTn26.d.ts +275 -0
- package/dist/{index-Bg5dYMmZ.d.ts → index-x2vcsduE.d.cts} +14 -11
- package/dist/index.cjs +429 -7383
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -15
- package/dist/index.d.ts +15 -15
- package/dist/index.js +121 -27
- package/dist/model/index.cjs +8 -2562
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +471 -81
- package/dist/model/index.d.ts +471 -81
- package/dist/model/index.js +6 -7
- package/dist/styled-Bl0ZK28h.d.ts +543 -0
- package/dist/styled-Crv6SA4S.d.cts +543 -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 +34 -13
- 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 +0 -202
- 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 +0 -924
- 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,27 +1,29 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createRng
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-ZGLJM4A4.js";
|
|
4
4
|
import {
|
|
5
5
|
beatsPerBar,
|
|
6
6
|
createNoteEventIndex,
|
|
7
7
|
formatTimeSignature,
|
|
8
8
|
isStrongBeat,
|
|
9
|
-
metricWeight
|
|
10
|
-
|
|
9
|
+
metricWeight,
|
|
10
|
+
pulseBeats
|
|
11
|
+
} from "./chunk-2PTFGVD7.js";
|
|
11
12
|
import {
|
|
12
13
|
isDiatonic,
|
|
13
14
|
parallelKey
|
|
14
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-NNB67XA2.js";
|
|
15
16
|
import {
|
|
16
17
|
roleOf
|
|
17
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-HN2OEUN2.js";
|
|
18
19
|
import {
|
|
19
20
|
enumerateSafePitches,
|
|
20
21
|
evaluateSafety
|
|
21
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-DZMHIGI4.js";
|
|
22
23
|
import {
|
|
23
24
|
NATURAL_MINOR_MASK,
|
|
24
25
|
chordPitchClasses,
|
|
26
|
+
chordQualities,
|
|
25
27
|
chordToneRole,
|
|
26
28
|
createsParallelPerfect,
|
|
27
29
|
diatonicTriad,
|
|
@@ -32,18 +34,32 @@ import {
|
|
|
32
34
|
minorKey,
|
|
33
35
|
nearestScaleTone,
|
|
34
36
|
scaleTonesInDegreeOrder
|
|
35
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-KBTTEX7Q.js";
|
|
36
38
|
import {
|
|
39
|
+
InvalidInputError,
|
|
40
|
+
assertDegree,
|
|
37
41
|
assertFiniteNumber,
|
|
38
42
|
assertGenerationBudget,
|
|
39
43
|
assertInteger,
|
|
40
44
|
assertNoteEvents,
|
|
45
|
+
assertOneOf,
|
|
41
46
|
assertPositiveInt,
|
|
42
47
|
assertRange,
|
|
43
|
-
assertTimeSignature
|
|
44
|
-
|
|
48
|
+
assertTimeSignature,
|
|
49
|
+
clampToMidi,
|
|
50
|
+
dropSilentNotes,
|
|
51
|
+
pitchClassOf,
|
|
52
|
+
soundingNotesOnly
|
|
53
|
+
} from "./chunk-R2VVFAIN.js";
|
|
45
54
|
|
|
46
55
|
// src/generate/bass/index.ts
|
|
56
|
+
var BASS_STYLES = Object.freeze([
|
|
57
|
+
"root",
|
|
58
|
+
"rootFifth",
|
|
59
|
+
"pop",
|
|
60
|
+
"walking",
|
|
61
|
+
"arpeggio"
|
|
62
|
+
]);
|
|
47
63
|
var DEFAULT_TS = { numerator: 4, denominator: 4 };
|
|
48
64
|
var DEFAULT_STYLE = "root";
|
|
49
65
|
var DEFAULT_OCTAVE = 2;
|
|
@@ -51,9 +67,6 @@ var STRONG_VELOCITY = 100;
|
|
|
51
67
|
var WEAK_VELOCITY = 80;
|
|
52
68
|
var PICKUP_PROB = 0.35;
|
|
53
69
|
var EPS = 1e-9;
|
|
54
|
-
function pitchClass(value) {
|
|
55
|
-
return (Math.trunc(value) % 12 + 12) % 12;
|
|
56
|
-
}
|
|
57
70
|
function placePc(pc, anchor, low) {
|
|
58
71
|
const high = low + 12;
|
|
59
72
|
let midi = pc + 12 * Math.round((anchor - pc) / 12);
|
|
@@ -69,29 +82,43 @@ function chordTonePcs(chord) {
|
|
|
69
82
|
const seen = /* @__PURE__ */ new Set();
|
|
70
83
|
const pcs = [];
|
|
71
84
|
for (const interval of chord.intervals) {
|
|
72
|
-
const pc =
|
|
85
|
+
const pc = pitchClassOf(chord.rootPc + interval);
|
|
73
86
|
if (!seen.has(pc)) {
|
|
74
87
|
seen.add(pc);
|
|
75
88
|
pcs.push(pc);
|
|
76
89
|
}
|
|
77
90
|
}
|
|
78
91
|
if (pcs.length === 0) {
|
|
79
|
-
pcs.push(
|
|
92
|
+
pcs.push(pitchClassOf(chord.rootPc));
|
|
80
93
|
}
|
|
81
94
|
return pcs;
|
|
82
95
|
}
|
|
83
96
|
function fifthPcOf(chord) {
|
|
84
|
-
const fifths = new Set(chord.intervals.map((i) =>
|
|
97
|
+
const fifths = new Set(chord.intervals.map((i) => pitchClassOf(i)));
|
|
85
98
|
const chosen = fifths.has(7) ? 7 : fifths.has(6) ? 6 : fifths.has(8) ? 8 : void 0;
|
|
86
|
-
return chosen === void 0 ?
|
|
99
|
+
return chosen === void 0 ? pitchClassOf(chord.rootPc) : pitchClassOf(chord.rootPc + chosen);
|
|
87
100
|
}
|
|
88
101
|
function bassPcOf(chord) {
|
|
89
|
-
return
|
|
102
|
+
return pitchClassOf(chord.bassPc ?? chord.rootPc);
|
|
90
103
|
}
|
|
91
|
-
function beatPositions(start, end) {
|
|
104
|
+
function beatPositions(start, end, ts) {
|
|
92
105
|
const positions = [];
|
|
93
|
-
|
|
94
|
-
|
|
106
|
+
const barBeats = beatsPerBar(ts);
|
|
107
|
+
const pulse = pulseBeats(ts);
|
|
108
|
+
const firstBar = Math.floor(start / barBeats);
|
|
109
|
+
const lastBar = Math.floor((end - EPS) / barBeats);
|
|
110
|
+
for (let bar = firstBar; bar <= lastBar; bar += 1) {
|
|
111
|
+
const barStart = bar * barBeats;
|
|
112
|
+
const firstPulse = Math.max(0, Math.ceil((start - barStart - EPS) / pulse));
|
|
113
|
+
for (let index = firstPulse; ; index += 1) {
|
|
114
|
+
const position = barStart + index * pulse;
|
|
115
|
+
if (position >= barStart + barBeats - EPS || position >= end - EPS) {
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
if (position >= start - EPS) {
|
|
119
|
+
positions.push(position);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
95
122
|
}
|
|
96
123
|
if (positions.length === 0) {
|
|
97
124
|
positions.push(start);
|
|
@@ -99,7 +126,10 @@ function beatPositions(start, end) {
|
|
|
99
126
|
return positions;
|
|
100
127
|
}
|
|
101
128
|
function emit(ctx, pos, pc, midiOverride) {
|
|
102
|
-
const midi =
|
|
129
|
+
const midi = clampToMidi(
|
|
130
|
+
midiOverride ?? placePc(pc, ctx.prevMidi, ctx.low),
|
|
131
|
+
"generated bass pitch"
|
|
132
|
+
);
|
|
103
133
|
const velocity = isStrongBeat(pos, ctx.ts) ? STRONG_VELOCITY : WEAK_VELOCITY;
|
|
104
134
|
ctx.notes.push({ startBeat: pos, pitch: midi, velocity });
|
|
105
135
|
return midi;
|
|
@@ -115,7 +145,7 @@ function buildRootFifth(ctx, seg) {
|
|
|
115
145
|
}
|
|
116
146
|
}
|
|
117
147
|
function buildPop(ctx, seg) {
|
|
118
|
-
const positions = beatPositions(seg.startBeat, seg.endBeat);
|
|
148
|
+
const positions = beatPositions(seg.startBeat, seg.endBeat, ctx.ts);
|
|
119
149
|
const rootPc = bassPcOf(seg.chord);
|
|
120
150
|
let emitted = false;
|
|
121
151
|
for (const pos of positions) {
|
|
@@ -125,7 +155,8 @@ function buildPop(ctx, seg) {
|
|
|
125
155
|
} else if (ctx.rng.prob(PICKUP_PROB)) {
|
|
126
156
|
if (ctx.rng.prob(0.5)) {
|
|
127
157
|
const base = placePc(rootPc, ctx.prevMidi, ctx.low);
|
|
128
|
-
|
|
158
|
+
const dropped = base - 12;
|
|
159
|
+
emit(ctx, pos, rootPc, dropped >= 0 ? dropped : base);
|
|
129
160
|
} else {
|
|
130
161
|
ctx.prevMidi = emit(ctx, pos, fifthPcOf(seg.chord));
|
|
131
162
|
}
|
|
@@ -137,7 +168,7 @@ function buildPop(ctx, seg) {
|
|
|
137
168
|
}
|
|
138
169
|
function buildArpeggio(ctx, seg) {
|
|
139
170
|
const tones = chordTonePcs(seg.chord);
|
|
140
|
-
const positions = beatPositions(seg.startBeat, seg.endBeat);
|
|
171
|
+
const positions = beatPositions(seg.startBeat, seg.endBeat, ctx.ts);
|
|
141
172
|
positions.forEach((pos, i) => {
|
|
142
173
|
const pc = tones[i % tones.length] ?? bassPcOf(seg.chord);
|
|
143
174
|
ctx.prevMidi = emit(ctx, pos, pc);
|
|
@@ -145,16 +176,27 @@ function buildArpeggio(ctx, seg) {
|
|
|
145
176
|
}
|
|
146
177
|
function approachNote(ctx, target, from) {
|
|
147
178
|
const dir = from <= target ? -1 : 1;
|
|
179
|
+
const chromatic = target + dir;
|
|
148
180
|
if (ctx.rng.prob(0.5)) {
|
|
149
|
-
return
|
|
181
|
+
return foldIntoBand(chromatic, ctx.low);
|
|
150
182
|
}
|
|
151
183
|
const cand = nearestScaleTone(target + dir * 2, ctx.key);
|
|
152
184
|
const step = Math.abs(cand - target);
|
|
153
|
-
return step >= 1 && step <= 2 ? cand :
|
|
185
|
+
return foldIntoBand(step >= 1 && step <= 2 ? cand : chromatic, ctx.low);
|
|
186
|
+
}
|
|
187
|
+
function foldIntoBand(midi, low) {
|
|
188
|
+
let result = midi;
|
|
189
|
+
while (result < low) {
|
|
190
|
+
result += 12;
|
|
191
|
+
}
|
|
192
|
+
while (result > low + 12) {
|
|
193
|
+
result -= 12;
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
154
196
|
}
|
|
155
197
|
function buildWalking(ctx, seg, next) {
|
|
156
198
|
const tones = chordTonePcs(seg.chord);
|
|
157
|
-
const positions = beatPositions(seg.startBeat, seg.endBeat);
|
|
199
|
+
const positions = beatPositions(seg.startBeat, seg.endBeat, ctx.ts);
|
|
158
200
|
const count = positions.length;
|
|
159
201
|
positions.forEach((pos, i) => {
|
|
160
202
|
if (i === 0) {
|
|
@@ -164,7 +206,7 @@ function buildWalking(ctx, seg, next) {
|
|
|
164
206
|
if (next && count > 1 && i === count - 1) {
|
|
165
207
|
const nextBass = placePc(bassPcOf(next.chord), ctx.prevMidi, ctx.low);
|
|
166
208
|
const midi = approachNote(ctx, nextBass, ctx.prevMidi);
|
|
167
|
-
ctx.prevMidi = emit(ctx, pos,
|
|
209
|
+
ctx.prevMidi = emit(ctx, pos, pitchClassOf(midi), midi);
|
|
168
210
|
return;
|
|
169
211
|
}
|
|
170
212
|
const pc = tones[i % tones.length] ?? bassPcOf(seg.chord);
|
|
@@ -179,18 +221,27 @@ function generateBassLine(opts) {
|
|
|
179
221
|
assertRange(segment.startBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].startBeat`);
|
|
180
222
|
assertRange(segment.endBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].endBeat`);
|
|
181
223
|
if (segment.endBeat <= segment.startBeat) {
|
|
182
|
-
throw new
|
|
224
|
+
throw new InvalidInputError(`segments[${index}] must have a positive duration`);
|
|
183
225
|
}
|
|
184
226
|
}
|
|
185
227
|
const segments = [...opts.segments].sort((a, b) => a.startBeat - b.startBeat);
|
|
228
|
+
for (let index = 1; index < segments.length; index += 1) {
|
|
229
|
+
const previous = segments[index - 1];
|
|
230
|
+
const current = segments[index];
|
|
231
|
+
if (previous !== void 0 && current !== void 0 && current.startBeat < previous.endBeat) {
|
|
232
|
+
throw new InvalidInputError(
|
|
233
|
+
`bass segments must not overlap: segments ${index - 1} and ${index} overlap`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
186
237
|
if (segments.length === 0) {
|
|
187
238
|
return [];
|
|
188
239
|
}
|
|
189
240
|
const ts = opts.ts ?? DEFAULT_TS;
|
|
190
241
|
assertTimeSignature(ts);
|
|
191
|
-
const style = opts.style ?? DEFAULT_STYLE;
|
|
242
|
+
const style = assertOneOf(opts.style ?? DEFAULT_STYLE, BASS_STYLES, "bass style");
|
|
192
243
|
const octave = opts.octave ?? DEFAULT_OCTAVE;
|
|
193
|
-
assertInteger(octave, "bass octave", -1,
|
|
244
|
+
assertInteger(octave, "bass octave", -1, 8);
|
|
194
245
|
const estimatedNotes = segments.reduce(
|
|
195
246
|
(count, segment) => count + Math.max(1, Math.ceil(segment.endBeat - segment.startBeat)),
|
|
196
247
|
0
|
|
@@ -242,7 +293,7 @@ function generateBassLine(opts) {
|
|
|
242
293
|
continue;
|
|
243
294
|
}
|
|
244
295
|
out.push({
|
|
245
|
-
pitch: note.pitch,
|
|
296
|
+
pitch: clampToMidi(note.pitch, "generated bass pitch"),
|
|
246
297
|
startBeat: note.startBeat,
|
|
247
298
|
durationBeat,
|
|
248
299
|
velocity: note.velocity
|
|
@@ -263,6 +314,7 @@ var HOLD_OFF_BEAT_PROB = 0.2;
|
|
|
263
314
|
var REINFORCE_PROB = 0.35;
|
|
264
315
|
var DEFAULT_MELODY_VELOCITY = 96;
|
|
265
316
|
var VELOCITY_DROP = 16;
|
|
317
|
+
var TIE_BREAK_JITTER = 1e-6;
|
|
266
318
|
function isWholeBeat(beat) {
|
|
267
319
|
return Math.abs(beat - Math.round(beat)) < EPS2;
|
|
268
320
|
}
|
|
@@ -365,26 +417,40 @@ function heldNoteBoundaries(melody, startBeat, endBeat, chordChangeBeats) {
|
|
|
365
417
|
}
|
|
366
418
|
return [...boundaries].sort((a, b) => a - b);
|
|
367
419
|
}
|
|
368
|
-
function
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
for (const boundary of boundaries) {
|
|
420
|
+
function boundaryContexts(melody, harmony, startBeat, endBeat, ts) {
|
|
421
|
+
const boundaries = heldNoteBoundaries(melody, startBeat, endBeat, harmony.chordChangeBeats);
|
|
422
|
+
return boundaries.map((boundary) => {
|
|
372
423
|
const melodyVoice = melodySnapshotAt(melody, boundary);
|
|
424
|
+
return {
|
|
425
|
+
melodyVoice,
|
|
426
|
+
otherVoices: melodyVoice === void 0 ? [] : [melodyVoice],
|
|
427
|
+
chord: harmony.chordAt(boundary),
|
|
428
|
+
strongBeat: isStrongBeat(boundary, ts),
|
|
429
|
+
atCounterOnset: Math.abs(boundary - startBeat) < EPS2
|
|
430
|
+
};
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
function heldPitchSafety(pitch, contexts, prevPitch, key, profile, register, low, high) {
|
|
434
|
+
let worst = 0 /* Safe */;
|
|
435
|
+
for (const context of contexts) {
|
|
436
|
+
const melodyVoice = context.melodyVoice;
|
|
373
437
|
if (melodyVoice !== void 0 && (register === "below" && pitch >= melodyVoice.pitch || register === "above" && pitch <= melodyVoice.pitch)) {
|
|
374
438
|
return null;
|
|
375
439
|
}
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
440
|
+
const result = evaluateSafety(
|
|
441
|
+
{
|
|
442
|
+
profile,
|
|
443
|
+
candidatePitch: pitch,
|
|
444
|
+
prevPitch: context.atCounterOnset ? prevPitch : pitch,
|
|
445
|
+
chord: context.chord,
|
|
446
|
+
key,
|
|
447
|
+
otherVoices: context.otherVoices,
|
|
448
|
+
strongBeat: context.strongBeat,
|
|
449
|
+
vocalLow: low,
|
|
450
|
+
vocalHigh: high
|
|
451
|
+
},
|
|
452
|
+
{ suggestions: false }
|
|
453
|
+
);
|
|
388
454
|
worst = Math.max(worst, result.safety);
|
|
389
455
|
if (worst === 2 /* Dissonant */) {
|
|
390
456
|
return worst;
|
|
@@ -393,35 +459,48 @@ function heldPitchSafety(pitch, startBeat, endBeat, prevPitch, melody, opts, pro
|
|
|
393
459
|
return worst;
|
|
394
460
|
}
|
|
395
461
|
function generateCounterMelody(opts) {
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
462
|
+
const timeline = opts.timeline;
|
|
463
|
+
const chordAt = timeline === void 0 ? opts.chordAt : (beat) => timeline.at(beat);
|
|
464
|
+
if (chordAt === void 0) {
|
|
465
|
+
throw new TypeError("countermelody needs a timeline or a chordAt callback");
|
|
399
466
|
}
|
|
467
|
+
const chordChangeBeats = timeline === void 0 ? opts.chordChangeBeats ?? [] : timeline.segments.flatMap((segment) => [segment.startBeat, segment.endBeat]);
|
|
468
|
+
const register = opts.register ?? "below";
|
|
469
|
+
assertNoteEvents(opts.melody, "countermelody melody", { allowNonPositiveDuration: true });
|
|
470
|
+
const melody = createNoteEventIndex(soundingNotesOnly(opts.melody), {
|
|
471
|
+
tieBreak: register === "below" ? "highest" : "lowest"
|
|
472
|
+
});
|
|
400
473
|
const ts = opts.ts ?? DEFAULT_TS2;
|
|
401
474
|
assertTimeSignature(ts);
|
|
402
|
-
const
|
|
403
|
-
const
|
|
404
|
-
const profile = opts.profile ?? "pop";
|
|
475
|
+
const rhythm = assertOneOf(opts.rhythm ?? "complement", ["complement", "follow"], "rhythm");
|
|
476
|
+
const profile = assertOneOf(opts.profile ?? "pop", ["strict", "pop"], "safety profile");
|
|
405
477
|
const rng = createRng(opts.seed ?? 0);
|
|
478
|
+
if (melody.notes.length === 0) {
|
|
479
|
+
return [];
|
|
480
|
+
}
|
|
406
481
|
const spanStart = melody.notes[0]?.note.startBeat ?? 0;
|
|
407
482
|
const spanEnd = melody.notes.reduce((end, n) => Math.max(end, n.endBeat), spanStart);
|
|
408
483
|
assertGenerationBudget(Math.ceil((spanEnd - spanStart) / GRID_STEP), "countermelody grid");
|
|
409
484
|
const meanPitch = melody.notes.reduce((sum, n) => sum + n.note.pitch, 0) / melody.notes.length;
|
|
410
485
|
const defaultCenter = register === "below" ? meanPitch - REGISTER_OFFSET : meanPitch + REGISTER_OFFSET;
|
|
411
|
-
const low =
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
486
|
+
const low = clampToMidi(
|
|
487
|
+
opts.pitchLow ?? Math.round(defaultCenter) - DEFAULT_HALF_RANGE,
|
|
488
|
+
"countermelody pitchLow"
|
|
489
|
+
);
|
|
490
|
+
const high = clampToMidi(
|
|
491
|
+
opts.pitchHigh ?? Math.round(defaultCenter) + DEFAULT_HALF_RANGE,
|
|
492
|
+
"countermelody pitchHigh"
|
|
493
|
+
);
|
|
415
494
|
if (low > high) {
|
|
416
|
-
throw new
|
|
495
|
+
throw new InvalidInputError(
|
|
417
496
|
`countermelody pitchLow must not exceed pitchHigh; received ${low} > ${high}`
|
|
418
497
|
);
|
|
419
498
|
}
|
|
420
499
|
assertGenerationBudget(high - low + 1, "countermelody pitch candidates");
|
|
421
|
-
assertGenerationBudget(
|
|
422
|
-
for (let index = 0; index <
|
|
500
|
+
assertGenerationBudget(chordChangeBeats.length, "chord change beats");
|
|
501
|
+
for (let index = 0; index < chordChangeBeats.length; index += 1) {
|
|
423
502
|
assertRange(
|
|
424
|
-
|
|
503
|
+
chordChangeBeats[index] ?? Number.NaN,
|
|
425
504
|
0,
|
|
426
505
|
Number.MAX_SAFE_INTEGER,
|
|
427
506
|
`chordChangeBeats[${index}]`
|
|
@@ -429,7 +508,9 @@ function generateCounterMelody(opts) {
|
|
|
429
508
|
}
|
|
430
509
|
const center = (low + high) / 2;
|
|
431
510
|
const onsets = rhythm === "follow" ? followOnsets(melody) : complementOnsets(melody, ts, spanStart, spanEnd, rng);
|
|
432
|
-
|
|
511
|
+
const boundaryEstimate = onsets.length + Math.ceil((spanEnd - spanStart) / GRID_STEP) + melody.notes.length + chordChangeBeats.length;
|
|
512
|
+
assertGenerationBudget(boundaryEstimate * (high - low + 1), "countermelody search");
|
|
513
|
+
const jitter = Array.from({ length: high - low + 1 }, () => rng.next() * TIE_BREAK_JITTER);
|
|
433
514
|
const out = [];
|
|
434
515
|
let prevPitch;
|
|
435
516
|
for (let onsetIndex = 0; onsetIndex < onsets.length; onsetIndex += 1) {
|
|
@@ -443,7 +524,8 @@ function generateCounterMelody(opts) {
|
|
|
443
524
|
const melPitch = melNote?.pitch;
|
|
444
525
|
const melodyVoice = melodySnapshotAt(melody, beat);
|
|
445
526
|
const melPrev = melodyVoice?.prevPitch;
|
|
446
|
-
const
|
|
527
|
+
const contexts = boundaryContexts(melody, { chordAt, chordChangeBeats }, beat, endBeat, ts);
|
|
528
|
+
const chord = contexts[0]?.chord ?? chordAt(beat);
|
|
447
529
|
const otherVoices = melodyVoice !== void 0 ? [melodyVoice] : [];
|
|
448
530
|
const candidates = enumerateSafePitches(
|
|
449
531
|
{
|
|
@@ -465,16 +547,13 @@ function generateCounterMelody(opts) {
|
|
|
465
547
|
for (const candidate of candidates) {
|
|
466
548
|
const worstSafety = heldPitchSafety(
|
|
467
549
|
candidate,
|
|
468
|
-
|
|
469
|
-
endBeat,
|
|
550
|
+
contexts,
|
|
470
551
|
prevPitch,
|
|
471
|
-
|
|
472
|
-
opts,
|
|
552
|
+
opts.key,
|
|
473
553
|
profile,
|
|
474
554
|
register,
|
|
475
555
|
low,
|
|
476
|
-
high
|
|
477
|
-
ts
|
|
556
|
+
high
|
|
478
557
|
);
|
|
479
558
|
if (worstSafety === null || worstSafety === 2 /* Dissonant */) {
|
|
480
559
|
continue;
|
|
@@ -486,7 +565,7 @@ function generateCounterMelody(opts) {
|
|
|
486
565
|
continue;
|
|
487
566
|
}
|
|
488
567
|
}
|
|
489
|
-
const score = scoreCandidate(candidate, melPitch, melPrev, prevPitch, chord, center);
|
|
568
|
+
const score = scoreCandidate(candidate, melPitch, melPrev, prevPitch, chord, center) + (jitter[candidate - low] ?? 0);
|
|
490
569
|
const tie = Math.abs(score - bestScore) <= EPS2 && candidate < (bestPitch ?? Number.POSITIVE_INFINITY);
|
|
491
570
|
if (worstSafety < bestWorstSafety || worstSafety === bestWorstSafety && (score > bestScore + EPS2 || tie)) {
|
|
492
571
|
bestWorstSafety = worstSafety;
|
|
@@ -503,7 +582,12 @@ function generateCounterMelody(opts) {
|
|
|
503
582
|
1,
|
|
504
583
|
Math.min(127, Math.round((melNote?.velocity ?? DEFAULT_MELODY_VELOCITY) - VELOCITY_DROP))
|
|
505
584
|
);
|
|
506
|
-
out.push({
|
|
585
|
+
out.push({
|
|
586
|
+
pitch: clampToMidi(pitch),
|
|
587
|
+
startBeat: beat,
|
|
588
|
+
durationBeat: endBeat - beat,
|
|
589
|
+
velocity
|
|
590
|
+
});
|
|
507
591
|
prevPitch = pitch;
|
|
508
592
|
}
|
|
509
593
|
return out;
|
|
@@ -528,6 +612,27 @@ var GM = {
|
|
|
528
612
|
};
|
|
529
613
|
var EIGHTH = 0.5;
|
|
530
614
|
var SIXTEENTH = 0.25;
|
|
615
|
+
var DRUM_FEELS = ["straight", "swing", "shuffle"];
|
|
616
|
+
var GROOVE_STYLES = [
|
|
617
|
+
"standard",
|
|
618
|
+
"funk",
|
|
619
|
+
"shuffle",
|
|
620
|
+
"bossa",
|
|
621
|
+
"trap",
|
|
622
|
+
"halftime",
|
|
623
|
+
"breakbeat",
|
|
624
|
+
"house",
|
|
625
|
+
"synthpop"
|
|
626
|
+
];
|
|
627
|
+
var DRUM_ROLES = ["full", "ambient", "minimal", "fxOnly"];
|
|
628
|
+
var PUBLIC_SECTIONS = [
|
|
629
|
+
"intro",
|
|
630
|
+
"verse",
|
|
631
|
+
"prechorus",
|
|
632
|
+
"chorus",
|
|
633
|
+
"bridge",
|
|
634
|
+
"outro"
|
|
635
|
+
];
|
|
531
636
|
var STYLE_MAP = {
|
|
532
637
|
// Standard pop pulse: kick on beats 1 and 3, backbeat snare on 2 and 4.
|
|
533
638
|
standard: { style: "standard", feel: "straight", ghostBoost: false, snareBeat3: false },
|
|
@@ -727,7 +832,7 @@ function getGhostDensity(mood, section, backingDensity, bpm) {
|
|
|
727
832
|
}
|
|
728
833
|
return levelToProbability(level);
|
|
729
834
|
}
|
|
730
|
-
function getGhostVelocity(section, beatPosition
|
|
835
|
+
function getGhostVelocity(section, beatPosition) {
|
|
731
836
|
let base = 0.4;
|
|
732
837
|
const even = beatPosition % 2 === 0 ? 0.05 : 0;
|
|
733
838
|
switch (section) {
|
|
@@ -748,22 +853,16 @@ function getGhostVelocity(section, beatPosition, isAfterSnare) {
|
|
|
748
853
|
base = 0.38;
|
|
749
854
|
break;
|
|
750
855
|
}
|
|
751
|
-
if (isAfterSnare) {
|
|
752
|
-
base += 0.1;
|
|
753
|
-
}
|
|
754
856
|
return Math.max(0.25, Math.min(0.65, base));
|
|
755
857
|
}
|
|
756
858
|
function getGhostProbabilityAtPosition(beat, sixteenthInBeat) {
|
|
757
|
-
const
|
|
758
|
-
return
|
|
859
|
+
const leadsIntoBackbeat = (beat === 0 || beat === 2) && sixteenthInBeat === 3;
|
|
860
|
+
return leadsIntoBackbeat ? 0.6 : 0.25;
|
|
759
861
|
}
|
|
760
|
-
function selectGhostPositions(mood
|
|
862
|
+
function selectGhostPositions(mood) {
|
|
761
863
|
if (mood === 2 /* Energetic */) {
|
|
762
864
|
return ["e", "a"];
|
|
763
865
|
}
|
|
764
|
-
if (mood === 0 /* Calm */) {
|
|
765
|
-
return rng.prob(0.5) ? ["e"] : [];
|
|
766
|
-
}
|
|
767
866
|
return ["e"];
|
|
768
867
|
}
|
|
769
868
|
|
|
@@ -779,6 +878,9 @@ function roleHiHatInstrument(role, useRide) {
|
|
|
779
878
|
if (role === "ambient") {
|
|
780
879
|
return GM.RIDE;
|
|
781
880
|
}
|
|
881
|
+
if (role === "minimal") {
|
|
882
|
+
return GM.FHH;
|
|
883
|
+
}
|
|
782
884
|
return useRide ? GM.RIDE : GM.CHH;
|
|
783
885
|
}
|
|
784
886
|
function sparser(level) {
|
|
@@ -1038,10 +1140,7 @@ function quantizeSwing(startBeat, swing, resolution) {
|
|
|
1038
1140
|
}
|
|
1039
1141
|
|
|
1040
1142
|
// src/generate/drums/beat.ts
|
|
1041
|
-
function
|
|
1042
|
-
return quantizeSwing(tick, effectiveSwing(feel, swingAmount), "sixteenth");
|
|
1043
|
-
}
|
|
1044
|
-
function hatSwingFactor(style) {
|
|
1143
|
+
function styleSwingFactor(style) {
|
|
1045
1144
|
if (style === "trap") {
|
|
1046
1145
|
return 0;
|
|
1047
1146
|
}
|
|
@@ -1050,6 +1149,12 @@ function hatSwingFactor(style) {
|
|
|
1050
1149
|
}
|
|
1051
1150
|
return 0.5;
|
|
1052
1151
|
}
|
|
1152
|
+
function sectionSwing(sec, swingAmount) {
|
|
1153
|
+
return effectiveSwing(sec.feel, swingAmount) * styleSwingFactor(sec.style);
|
|
1154
|
+
}
|
|
1155
|
+
function swing16(tick, sec, swingAmount) {
|
|
1156
|
+
return quantizeSwing(tick, sectionSwing(sec, swingAmount), "sixteenth");
|
|
1157
|
+
}
|
|
1053
1158
|
function generateKickForBeat(ctx, sec, kick) {
|
|
1054
1159
|
if (ctx.inPrechorusLift) {
|
|
1055
1160
|
return;
|
|
@@ -1060,7 +1165,7 @@ function generateKickForBeat(ctx, sec, kick) {
|
|
|
1060
1165
|
ctx.track.add(GM.BD, ctx.beatTick, EIGHTH, ctx.velocity);
|
|
1061
1166
|
}
|
|
1062
1167
|
if (and) {
|
|
1063
|
-
const andTick = swing16(ctx.beatTick + EIGHTH, sec
|
|
1168
|
+
const andTick = swing16(ctx.beatTick + EIGHTH, sec, ctx.swingAmount);
|
|
1064
1169
|
ctx.track.add(GM.BD, andTick, EIGHTH, ctx.velocity * 0.85);
|
|
1065
1170
|
}
|
|
1066
1171
|
}
|
|
@@ -1077,12 +1182,17 @@ function generateSnareForBeat(ctx, sec, isIntroFirst) {
|
|
|
1077
1182
|
}
|
|
1078
1183
|
const backbeatVel = Math.min(127, ctx.velocity + 16);
|
|
1079
1184
|
const promoteSparseChorus = sec.style === "sparse" && ctx.section === "chorus" && sec.role === "full";
|
|
1185
|
+
if (sec.role === "fxOnly") {
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
if (sec.role === "minimal") {
|
|
1189
|
+
ctx.track.add(GM.SIDESTICK, ctx.beatTick, EIGHTH, ctx.velocity * 0.65);
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1080
1192
|
if (promoteSparseChorus) {
|
|
1081
1193
|
ctx.track.add(GM.SD, ctx.beatTick, EIGHTH, backbeatVel);
|
|
1082
1194
|
} else if (sec.style === "sparse" || sec.role === "ambient") {
|
|
1083
|
-
|
|
1084
|
-
ctx.track.add(GM.SIDESTICK, ctx.beatTick, EIGHTH, ctx.velocity * 0.8);
|
|
1085
|
-
}
|
|
1195
|
+
ctx.track.add(GM.SIDESTICK, ctx.beatTick, EIGHTH, ctx.velocity * 0.8);
|
|
1086
1196
|
} else {
|
|
1087
1197
|
ctx.track.add(GM.SD, ctx.beatTick, EIGHTH, backbeatVel);
|
|
1088
1198
|
}
|
|
@@ -1091,7 +1201,7 @@ function generateGhostNotesForBeat(ctx, sec) {
|
|
|
1091
1201
|
if (ctx.beat !== 0 && ctx.beat !== 2) {
|
|
1092
1202
|
return;
|
|
1093
1203
|
}
|
|
1094
|
-
const positions = selectGhostPositions(sec.ghostMood
|
|
1204
|
+
const positions = selectGhostPositions(sec.ghostMood);
|
|
1095
1205
|
let ghostProb = getGhostDensity(sec.ghostMood, ctx.section, sec.backingDensity, ctx.bpm);
|
|
1096
1206
|
if (sec.ghostBoost) {
|
|
1097
1207
|
ghostProb = Math.min(1, ghostProb * 1.4);
|
|
@@ -1103,13 +1213,13 @@ function generateGhostNotesForBeat(ctx, sec) {
|
|
|
1103
1213
|
continue;
|
|
1104
1214
|
}
|
|
1105
1215
|
const variation = ctx.rng.float(0.85, 1.15);
|
|
1106
|
-
const ghostBase = getGhostVelocity(ctx.section, ctx.beat / 2
|
|
1216
|
+
const ghostBase = getGhostVelocity(ctx.section, ctx.beat / 2);
|
|
1107
1217
|
let ghostVel = ctx.velocity * ghostBase * variation;
|
|
1108
1218
|
if (pos === "a") {
|
|
1109
1219
|
ghostVel *= 0.9;
|
|
1110
1220
|
}
|
|
1111
1221
|
const offset = pos === "e" ? SIXTEENTH : 3 * SIXTEENTH;
|
|
1112
|
-
const tick = swing16(ctx.beatTick + offset, sec
|
|
1222
|
+
const tick = swing16(ctx.beatTick + offset, sec, ctx.swingAmount);
|
|
1113
1223
|
ctx.track.add(GM.SD, tick, SIXTEENTH, ghostVel);
|
|
1114
1224
|
}
|
|
1115
1225
|
}
|
|
@@ -1126,16 +1236,18 @@ function generatePreChorusBuildup(ctx, sec, isSectionLastBar) {
|
|
|
1126
1236
|
const progress = (barInLift * 4 + ctx.beat) / (barsInLift * 4);
|
|
1127
1237
|
const buildupVel = ctx.velocity * (0.5 + 0.5 * progress);
|
|
1128
1238
|
ctx.track.add(GM.SD, ctx.beatTick, EIGHTH, buildupVel);
|
|
1129
|
-
ctx.track.add(
|
|
1239
|
+
ctx.track.add(
|
|
1240
|
+
GM.SD,
|
|
1241
|
+
swing16(ctx.beatTick + EIGHTH, sec, ctx.swingAmount),
|
|
1242
|
+
EIGHTH,
|
|
1243
|
+
buildupVel * 0.85
|
|
1244
|
+
);
|
|
1130
1245
|
if (isSectionLastBar && ctx.beat === 3) {
|
|
1131
1246
|
ctx.track.add(GM.CRASH, ctx.beatTick + EIGHTH + SIXTEENTH, SIXTEENTH, ctx.velocity * 1.1);
|
|
1132
1247
|
}
|
|
1133
1248
|
}
|
|
1134
1249
|
function generateHiHatForBeat(ctx, sec) {
|
|
1135
1250
|
if (!shouldPlayHiHat(sec.role)) {
|
|
1136
|
-
if (sec.useFootHh && (ctx.beat === 0 || ctx.beat === 2)) {
|
|
1137
|
-
ctx.track.add(GM.FHH, ctx.beatTick, EIGHTH, footHiHatVelocity(ctx.rng));
|
|
1138
|
-
}
|
|
1139
1251
|
return;
|
|
1140
1252
|
}
|
|
1141
1253
|
const crossStick = sec.useRide && shouldUseBridgeCrossStick(ctx.section, ctx.beat);
|
|
@@ -1143,11 +1255,12 @@ function generateHiHatForBeat(ctx, sec) {
|
|
|
1143
1255
|
const hhType = sectionHiHatType(ctx.section, sec.role);
|
|
1144
1256
|
const typeMult = hiHatTypeVelocityMultiplier(hhType);
|
|
1145
1257
|
const isDynamicOpen = ctx.barHasOpenHh && ctx.beat === ctx.openHhBeat;
|
|
1258
|
+
const allowsOpenHiHat = sec.role !== "ambient" && sec.role !== "minimal";
|
|
1146
1259
|
const dm = sec.densityMult;
|
|
1147
1260
|
if (sec.hhLevel === "quarter") {
|
|
1148
1261
|
const introRest = ctx.section === "intro" && ctx.beat !== 0;
|
|
1149
1262
|
if (!introRest) {
|
|
1150
|
-
if (isDynamicOpen) {
|
|
1263
|
+
if (isDynamicOpen && allowsOpenHiHat) {
|
|
1151
1264
|
ctx.track.add(
|
|
1152
1265
|
GM.OHH,
|
|
1153
1266
|
ctx.beatTick,
|
|
@@ -1171,7 +1284,7 @@ function generateHiHatForBeat(ctx, sec) {
|
|
|
1171
1284
|
for (let eighth = 0; eighth < 2; eighth += 1) {
|
|
1172
1285
|
let hhTick = ctx.beatTick + eighth * EIGHTH;
|
|
1173
1286
|
if (eighth === 1) {
|
|
1174
|
-
hhTick = swing16(hhTick, sec
|
|
1287
|
+
hhTick = swing16(hhTick, sec, ctx.swingAmount);
|
|
1175
1288
|
}
|
|
1176
1289
|
if (ctx.section === "intro" && eighth === 1) {
|
|
1177
1290
|
if (sec.useFootHh && ctx.beat % 2 === 0) {
|
|
@@ -1180,7 +1293,7 @@ function generateHiHatForBeat(ctx, sec) {
|
|
|
1180
1293
|
continue;
|
|
1181
1294
|
}
|
|
1182
1295
|
const hhVel = Math.max(20, ctx.velocity * dm * typeMult * (eighth === 0 ? 0.9 : 0.65));
|
|
1183
|
-
if (isDynamicOpen && eighth === 0) {
|
|
1296
|
+
if (isDynamicOpen && allowsOpenHiHat && eighth === 0) {
|
|
1184
1297
|
ctx.track.add(GM.OHH, hhTick, EIGHTH, hhVel + OHH_VEL_BOOST);
|
|
1185
1298
|
continue;
|
|
1186
1299
|
}
|
|
@@ -1191,7 +1304,7 @@ function generateHiHatForBeat(ctx, sec) {
|
|
|
1191
1304
|
} else if (eighth === 0) {
|
|
1192
1305
|
useOpen = shouldAddOpenHHAccent(ctx.section, ctx.beat, ctx.bar, ctx.rng);
|
|
1193
1306
|
}
|
|
1194
|
-
if (useOpen) {
|
|
1307
|
+
if (useOpen && allowsOpenHiHat) {
|
|
1195
1308
|
ctx.track.add(hiHatNote("open"), hhTick, EIGHTH, Math.max(20, hhVel * 1.1));
|
|
1196
1309
|
} else {
|
|
1197
1310
|
ctx.track.add(hhInstrument, hhTick, EIGHTH / 2, hhVel);
|
|
@@ -1201,17 +1314,16 @@ function generateHiHatForBeat(ctx, sec) {
|
|
|
1201
1314
|
}
|
|
1202
1315
|
for (let sixteenth = 0; sixteenth < 4; sixteenth += 1) {
|
|
1203
1316
|
let hhTick = ctx.beatTick + sixteenth * SIXTEENTH;
|
|
1204
|
-
if (sixteenth
|
|
1205
|
-
|
|
1206
|
-
hhTick = quantizeSwing(hhTick, swung, "sixteenth");
|
|
1317
|
+
if (sixteenth !== 0) {
|
|
1318
|
+
hhTick = swing16(hhTick, sec, ctx.swingAmount);
|
|
1207
1319
|
}
|
|
1208
1320
|
const metricVel = hiHatVelocityMultiplier(sixteenth, ctx.rng);
|
|
1209
1321
|
const hhVel = Math.max(20, ctx.velocity * dm * typeMult * metricVel);
|
|
1210
|
-
if (isDynamicOpen && sixteenth === 0) {
|
|
1322
|
+
if (isDynamicOpen && allowsOpenHiHat && sixteenth === 0) {
|
|
1211
1323
|
ctx.track.add(GM.OHH, hhTick, SIXTEENTH, hhVel + OHH_VEL_BOOST);
|
|
1212
1324
|
continue;
|
|
1213
1325
|
}
|
|
1214
|
-
if (ctx.beat === 3 && sixteenth === 3) {
|
|
1326
|
+
if (allowsOpenHiHat && ctx.beat === 3 && sixteenth === 3) {
|
|
1215
1327
|
const openProb = Math.max(0.1, Math.min(0.4, 30 / ctx.bpm));
|
|
1216
1328
|
if (ctx.rng.prob(openProb)) {
|
|
1217
1329
|
ctx.track.add(GM.OHH, hhTick, SIXTEENTH, Math.max(20, hhVel * 1.2));
|
|
@@ -1266,7 +1378,7 @@ function getFillStartBeat(energy) {
|
|
|
1266
1378
|
return 2;
|
|
1267
1379
|
case "high":
|
|
1268
1380
|
case "peak":
|
|
1269
|
-
return
|
|
1381
|
+
return 2;
|
|
1270
1382
|
}
|
|
1271
1383
|
}
|
|
1272
1384
|
function selectFillType(from, to, style, nextEnergy, rng) {
|
|
@@ -1531,24 +1643,62 @@ function generateFill(track, beatTick, beat, fillType, velocity) {
|
|
|
1531
1643
|
}
|
|
1532
1644
|
|
|
1533
1645
|
// src/generate/drums/hit.ts
|
|
1646
|
+
var DRUM_NOTES = Object.freeze({
|
|
1647
|
+
kick: GM.BD,
|
|
1648
|
+
snare: GM.SD,
|
|
1649
|
+
sideStick: GM.SIDESTICK,
|
|
1650
|
+
handClap: GM.HANDCLAP,
|
|
1651
|
+
closedHiHat: GM.CHH,
|
|
1652
|
+
pedalHiHat: GM.FHH,
|
|
1653
|
+
openHiHat: GM.OHH,
|
|
1654
|
+
crash: GM.CRASH,
|
|
1655
|
+
ride: GM.RIDE,
|
|
1656
|
+
tambourine: GM.TAMBOURINE,
|
|
1657
|
+
highTom: GM.TOM_H,
|
|
1658
|
+
midTom: GM.TOM_M,
|
|
1659
|
+
lowTom: GM.TOM_L,
|
|
1660
|
+
shaker: GM.SHAKER
|
|
1661
|
+
});
|
|
1662
|
+
var VOICE_BY_NOTE = new Map(
|
|
1663
|
+
Object.entries(DRUM_NOTES).map(([voice, note]) => [note, voice])
|
|
1664
|
+
);
|
|
1665
|
+
function drumVoiceOf(pitch) {
|
|
1666
|
+
return VOICE_BY_NOTE.get(pitch);
|
|
1667
|
+
}
|
|
1668
|
+
var ONSET_KEY_SCALE = 128;
|
|
1534
1669
|
var HitList = class {
|
|
1535
1670
|
hits = [];
|
|
1671
|
+
/** Onset keys already occupied, as `pitch:quantizedBeat`. */
|
|
1672
|
+
#onsets = /* @__PURE__ */ new Set();
|
|
1536
1673
|
/** Append a hit, clamping velocity and dropping non-positive positions. */
|
|
1537
1674
|
add(pitch, startBeat, durationBeat, velocity) {
|
|
1675
|
+
const beat = Math.max(0, startBeat);
|
|
1538
1676
|
this.hits.push({
|
|
1539
1677
|
pitch,
|
|
1540
|
-
startBeat:
|
|
1678
|
+
startBeat: beat,
|
|
1541
1679
|
durationBeat,
|
|
1542
1680
|
velocity: clampVel(velocity)
|
|
1543
1681
|
});
|
|
1682
|
+
this.#onsets.add(onsetKey(pitch, beat));
|
|
1683
|
+
}
|
|
1684
|
+
/** Whether a hit of this pitch already starts exactly at `startBeat`. */
|
|
1685
|
+
hasOnset(pitch, startBeat) {
|
|
1686
|
+
return this.#onsets.has(onsetKey(pitch, startBeat));
|
|
1544
1687
|
}
|
|
1545
1688
|
/** True when a crash already sits within a 16th of `startBeat`. */
|
|
1546
1689
|
hasCrashNear(startBeat) {
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1690
|
+
const steps = Math.round(0.25 * ONSET_KEY_SCALE);
|
|
1691
|
+
for (let step = 0; step < steps; step += 1) {
|
|
1692
|
+
if (this.#onsets.has(onsetKey(GM.CRASH, startBeat + step / ONSET_KEY_SCALE))) {
|
|
1693
|
+
return true;
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
return false;
|
|
1550
1697
|
}
|
|
1551
1698
|
};
|
|
1699
|
+
function onsetKey(pitch, startBeat) {
|
|
1700
|
+
return `${pitch}:${Math.round(startBeat * ONSET_KEY_SCALE)}`;
|
|
1701
|
+
}
|
|
1552
1702
|
|
|
1553
1703
|
// src/generate/drums/kick.ts
|
|
1554
1704
|
var EMPTY = {
|
|
@@ -1687,12 +1837,12 @@ function getPercussionConfig(mood, section) {
|
|
|
1687
1837
|
shaker16th: act[1]
|
|
1688
1838
|
};
|
|
1689
1839
|
}
|
|
1690
|
-
function generateAuxPercussionForBar(track, barStart, config, role, densityMult, rng, bpm) {
|
|
1840
|
+
function generateAuxPercussionForBar(track, barStart, config, role, densityMult, rng, bpm, sec, swingAmount, barBeats = 4) {
|
|
1691
1841
|
if (role === "minimal") {
|
|
1692
1842
|
return;
|
|
1693
1843
|
}
|
|
1694
1844
|
if (config.tambourine) {
|
|
1695
|
-
for (let beat = 1; beat
|
|
1845
|
+
for (let beat = 1; beat < barBeats; beat += 2) {
|
|
1696
1846
|
const raw = 70 * densityMult * rng.float(0.9, 1.1);
|
|
1697
1847
|
track.add(GM.TAMBOURINE, barStart + beat, EIGHTH, Math.max(40, Math.min(90, raw)));
|
|
1698
1848
|
}
|
|
@@ -1701,12 +1851,12 @@ function generateAuxPercussionForBar(track, barStart, config, role, densityMult,
|
|
|
1701
1851
|
const use16th = config.shaker16th && bpm < HH_16TH_BPM_THRESHOLD;
|
|
1702
1852
|
if (use16th) {
|
|
1703
1853
|
const velCurve = [0.75, 0.45, 0.6, 0.45];
|
|
1704
|
-
for (let beat = 0; beat <
|
|
1854
|
+
for (let beat = 0; beat < barBeats; beat += 1) {
|
|
1705
1855
|
for (let sub = 0; sub < 4; sub += 1) {
|
|
1706
1856
|
const raw = 80 * (velCurve[sub] ?? 0.5) * densityMult * rng.float(0.9, 1.1);
|
|
1707
1857
|
track.add(
|
|
1708
1858
|
GM.SHAKER,
|
|
1709
|
-
barStart + beat + sub * SIXTEENTH,
|
|
1859
|
+
swing16(barStart + beat + sub * SIXTEENTH, sec, swingAmount),
|
|
1710
1860
|
SIXTEENTH,
|
|
1711
1861
|
Math.max(25, Math.min(85, raw))
|
|
1712
1862
|
);
|
|
@@ -1714,12 +1864,12 @@ function generateAuxPercussionForBar(track, barStart, config, role, densityMult,
|
|
|
1714
1864
|
}
|
|
1715
1865
|
} else {
|
|
1716
1866
|
const velCurve = [0.75, 0.55];
|
|
1717
|
-
for (let beat = 0; beat <
|
|
1867
|
+
for (let beat = 0; beat < barBeats; beat += 1) {
|
|
1718
1868
|
for (let sub = 0; sub < 2; sub += 1) {
|
|
1719
1869
|
const raw = 80 * (velCurve[sub] ?? 0.6) * densityMult * rng.float(0.9, 1.1);
|
|
1720
1870
|
track.add(
|
|
1721
1871
|
GM.SHAKER,
|
|
1722
|
-
barStart + beat + sub * EIGHTH,
|
|
1872
|
+
swing16(barStart + beat + sub * EIGHTH, sec, swingAmount),
|
|
1723
1873
|
EIGHTH,
|
|
1724
1874
|
Math.max(25, Math.min(85, raw))
|
|
1725
1875
|
);
|
|
@@ -1728,7 +1878,7 @@ function generateAuxPercussionForBar(track, barStart, config, role, densityMult,
|
|
|
1728
1878
|
}
|
|
1729
1879
|
}
|
|
1730
1880
|
if (config.handclap) {
|
|
1731
|
-
for (let beat = 1; beat
|
|
1881
|
+
for (let beat = 1; beat < barBeats; beat += 2) {
|
|
1732
1882
|
const raw = 85 * densityMult * rng.float(0.9, 1.1);
|
|
1733
1883
|
track.add(GM.HANDCLAP, barStart + beat, EIGHTH, Math.max(50, Math.min(100, raw)));
|
|
1734
1884
|
}
|
|
@@ -1736,25 +1886,34 @@ function generateAuxPercussionForBar(track, barStart, config, role, densityMult,
|
|
|
1736
1886
|
}
|
|
1737
1887
|
|
|
1738
1888
|
// src/generate/drums/index.ts
|
|
1889
|
+
var MAX_HITS_PER_BAR = 128;
|
|
1739
1890
|
function generateDrums(opts) {
|
|
1740
1891
|
assertPositiveInt(opts.bars, "drum bars");
|
|
1741
1892
|
assertRange(opts.bpm, Number.MIN_VALUE, 1e3, "drum bpm");
|
|
1742
1893
|
assertRange(opts.density, 0, 1, "drum density");
|
|
1743
|
-
assertGenerationBudget(opts.bars *
|
|
1894
|
+
assertGenerationBudget(opts.bars * MAX_HITS_PER_BAR, "drum hits");
|
|
1744
1895
|
if (opts.euclideanKick) {
|
|
1745
1896
|
const steps = opts.euclideanKick.steps ?? 16;
|
|
1746
1897
|
assertInteger(steps, "euclidean steps", 1, 16);
|
|
1747
1898
|
assertInteger(opts.euclideanKick.pulses, "euclidean pulses");
|
|
1748
1899
|
assertInteger(opts.euclideanKick.rotation ?? 0, "euclidean rotation");
|
|
1749
1900
|
}
|
|
1901
|
+
const publicStyle = assertOneOf(opts.style, GROOVE_STYLES, "drum style");
|
|
1902
|
+
const publicSection = assertOneOf(opts.section, PUBLIC_SECTIONS, "drum section");
|
|
1903
|
+
if (opts.nextSection !== void 0) {
|
|
1904
|
+
assertOneOf(opts.nextSection, PUBLIC_SECTIONS, "drum nextSection");
|
|
1905
|
+
}
|
|
1750
1906
|
const track = new HitList();
|
|
1751
1907
|
const rng = createRng(opts.seed ?? 0);
|
|
1752
|
-
const mapping = mapStyle(
|
|
1908
|
+
const mapping = mapStyle(publicStyle);
|
|
1753
1909
|
const style = mapping.style;
|
|
1754
|
-
const feel = opts.feel
|
|
1755
|
-
const role = opts.role
|
|
1910
|
+
const feel = opts.feel === void 0 ? mapping.feel : assertOneOf(opts.feel, DRUM_FEELS, "drum feel");
|
|
1911
|
+
const role = opts.role === void 0 ? "full" : assertOneOf(opts.role, DRUM_ROLES, "drum role");
|
|
1912
|
+
const ts = opts.ts ?? { numerator: 4, denominator: 4 };
|
|
1913
|
+
assertTimeSignature(ts, "drum time signature");
|
|
1914
|
+
const barBeats = beatsPerBar(ts);
|
|
1756
1915
|
const playMainVoices = role !== "fxOnly";
|
|
1757
|
-
const section = mapSection(
|
|
1916
|
+
const section = mapSection(publicSection);
|
|
1758
1917
|
const backingDensity = mapDensity(opts.density);
|
|
1759
1918
|
const swingAmount = feelSwingAmount(feel);
|
|
1760
1919
|
let densityMult = sectionDensityMultiplier(section);
|
|
@@ -1788,7 +1947,7 @@ function generateDrums(opts) {
|
|
|
1788
1947
|
const reuseSectionKick = (section === "b" || section === "chorus") && style !== "sparse";
|
|
1789
1948
|
let sectionKick;
|
|
1790
1949
|
for (let bar = 0; bar < opts.bars; bar += 1) {
|
|
1791
|
-
const barStart = bar *
|
|
1950
|
+
const barStart = bar * barBeats;
|
|
1792
1951
|
const isLastBar = bar === opts.bars - 1;
|
|
1793
1952
|
if (bar === 0 && section === "chorus") {
|
|
1794
1953
|
track.add(GM.CRASH, barStart, 0.5, 100 * densityMult);
|
|
@@ -1806,9 +1965,9 @@ function generateDrums(opts) {
|
|
|
1806
1965
|
} else if (!euclidKick) {
|
|
1807
1966
|
kick = getKickPattern(section, style, bar, rng);
|
|
1808
1967
|
}
|
|
1809
|
-
const inLift = isInPreChorusLift(section, bar, opts.bars,
|
|
1968
|
+
const inLift = isInPreChorusLift(section, bar, opts.bars, nextSection === "chorus");
|
|
1810
1969
|
let currentFill = "snareRoll";
|
|
1811
|
-
for (let beat = 0; beat <
|
|
1970
|
+
for (let beat = 0; beat < barBeats; beat += 1) {
|
|
1812
1971
|
const beatTick = barStart + beat;
|
|
1813
1972
|
const velocity = calculateVelocity(section, beat);
|
|
1814
1973
|
if (opts.fills && isLastBar && !inLift && beat >= fillStartBeat) {
|
|
@@ -1845,14 +2004,14 @@ function generateDrums(opts) {
|
|
|
1845
2004
|
generatePreChorusBuildup(ctx, sec, isLastBar);
|
|
1846
2005
|
}
|
|
1847
2006
|
if (euclidKick && !inLift) {
|
|
1848
|
-
const stepLength =
|
|
2007
|
+
const stepLength = barBeats / euclidSteps;
|
|
1849
2008
|
for (let step = 0; step < euclidKick.length; step += 1) {
|
|
1850
2009
|
const onset = step * stepLength;
|
|
1851
2010
|
if (euclidKick[step] && Math.floor(onset) === beat) {
|
|
1852
2011
|
const wholeBeat = Math.abs(onset - Math.round(onset)) < 1e-9;
|
|
1853
2012
|
track.add(
|
|
1854
2013
|
GM.BD,
|
|
1855
|
-
barStart + onset,
|
|
2014
|
+
swing16(barStart + onset, sec, swingAmount),
|
|
1856
2015
|
Math.min(0.5, stepLength),
|
|
1857
2016
|
velocity * (wholeBeat ? 1 : 0.85)
|
|
1858
2017
|
);
|
|
@@ -1869,10 +2028,9 @@ function generateDrums(opts) {
|
|
|
1869
2028
|
generateHiHatForBeat(ctx, sec);
|
|
1870
2029
|
}
|
|
1871
2030
|
if (sec.useFootHh && shouldPlayHiHat(role)) {
|
|
1872
|
-
for (let fhhBeat = 0; fhhBeat <
|
|
2031
|
+
for (let fhhBeat = 0; fhhBeat < barBeats; fhhBeat += 2) {
|
|
1873
2032
|
const fhhTick = barStart + fhhBeat;
|
|
1874
|
-
|
|
1875
|
-
if (!occupied) {
|
|
2033
|
+
if (!track.hasOnset(GM.FHH, fhhTick)) {
|
|
1876
2034
|
track.add(GM.FHH, fhhTick, 0.5, footHiHatVelocity(rng));
|
|
1877
2035
|
}
|
|
1878
2036
|
}
|
|
@@ -1893,14 +2051,19 @@ function generateDrums(opts) {
|
|
|
1893
2051
|
role,
|
|
1894
2052
|
densityMult,
|
|
1895
2053
|
rng,
|
|
1896
|
-
opts.bpm
|
|
2054
|
+
opts.bpm,
|
|
2055
|
+
sec,
|
|
2056
|
+
swingAmount,
|
|
2057
|
+
barBeats
|
|
1897
2058
|
);
|
|
1898
2059
|
}
|
|
1899
|
-
|
|
2060
|
+
const endBeat = opts.bars * barBeats;
|
|
2061
|
+
return track.hits.filter((hit) => hit.startBeat < endBeat).sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);
|
|
1900
2062
|
}
|
|
1901
2063
|
|
|
1902
2064
|
// src/generate/groove/index.ts
|
|
1903
2065
|
var DEFAULT_TS3 = { numerator: 4, denominator: 4 };
|
|
2066
|
+
var DEFAULT_GROOVE_SUBDIVISION = 4;
|
|
1904
2067
|
var DEFAULT_TIMING = 0.02;
|
|
1905
2068
|
var DEFAULT_VELOCITY_JITTER = 8;
|
|
1906
2069
|
var DEFAULT_ACCENT = 12;
|
|
@@ -1911,7 +2074,8 @@ var MAX_VELOCITY = 127;
|
|
|
1911
2074
|
function humanize(events, opts = {}) {
|
|
1912
2075
|
const ts = opts.ts ?? DEFAULT_TS3;
|
|
1913
2076
|
assertTimeSignature(ts);
|
|
1914
|
-
assertNoteEvents(events, "humanize events");
|
|
2077
|
+
assertNoteEvents(events, "humanize events", { allowNonPositiveDuration: true });
|
|
2078
|
+
const sounding = dropSilentNotes(events);
|
|
1915
2079
|
const timing = opts.timing ?? DEFAULT_TIMING;
|
|
1916
2080
|
const velocityJitter = opts.velocity ?? DEFAULT_VELOCITY_JITTER;
|
|
1917
2081
|
const accent = opts.accent ?? DEFAULT_ACCENT;
|
|
@@ -1921,7 +2085,7 @@ function humanize(events, opts = {}) {
|
|
|
1921
2085
|
assertRange(accent, 0, 127, "humanize accent");
|
|
1922
2086
|
assertRange(baseVelocity, 0, 127, "humanize base velocity");
|
|
1923
2087
|
const rng = createRng(opts.seed ?? 0);
|
|
1924
|
-
return
|
|
2088
|
+
return sounding.map((event) => {
|
|
1925
2089
|
const timingOffset = rng.float(-timing, timing);
|
|
1926
2090
|
const startBeat = Math.max(0, event.startBeat + timingOffset);
|
|
1927
2091
|
const weight = metricWeight(event.startBeat, ts);
|
|
@@ -1951,18 +2115,19 @@ function quantizeToGrid(beat, barBeats, subdivision, slotsPerBar) {
|
|
|
1951
2115
|
slotIndex
|
|
1952
2116
|
};
|
|
1953
2117
|
}
|
|
1954
|
-
function extractGrooveTemplate(events, ts, subdivision) {
|
|
2118
|
+
function extractGrooveTemplate(events, ts, subdivision = DEFAULT_GROOVE_SUBDIVISION) {
|
|
1955
2119
|
assertTimeSignature(ts);
|
|
1956
|
-
assertNoteEvents(events, "groove source events");
|
|
2120
|
+
assertNoteEvents(events, "groove source events", { allowNonPositiveDuration: true });
|
|
1957
2121
|
assertPositiveInt(subdivision, "groove subdivision");
|
|
1958
2122
|
const barBeats = beatsPerBar(ts);
|
|
1959
|
-
const slotsPerBar = Math.
|
|
2123
|
+
const slotsPerBar = Math.ceil(barBeats * subdivision);
|
|
2124
|
+
assertPositiveInt(slotsPerBar, "groove template slots");
|
|
1960
2125
|
assertGenerationBudget(slotsPerBar, "groove template slots");
|
|
1961
2126
|
const offsetSums = new Array(slotsPerBar).fill(0);
|
|
1962
2127
|
const velocitySums = new Array(slotsPerBar).fill(0);
|
|
1963
2128
|
const counts = new Array(slotsPerBar).fill(0);
|
|
1964
2129
|
const velocityCounts = new Array(slotsPerBar).fill(0);
|
|
1965
|
-
for (const event of events) {
|
|
2130
|
+
for (const event of dropSilentNotes(events)) {
|
|
1966
2131
|
const { quantizedBeat, slotIndex } = quantizeToGrid(
|
|
1967
2132
|
event.startBeat,
|
|
1968
2133
|
barBeats,
|
|
@@ -1993,20 +2158,20 @@ function extractGrooveTemplate(events, ts, subdivision) {
|
|
|
1993
2158
|
}
|
|
1994
2159
|
function applyGrooveTemplate(events, template, ts) {
|
|
1995
2160
|
assertTimeSignature(ts);
|
|
1996
|
-
assertNoteEvents(events, "groove target events");
|
|
2161
|
+
assertNoteEvents(events, "groove target events", { allowNonPositiveDuration: true });
|
|
1997
2162
|
assertPositiveInt(template.subdivision, "template subdivision");
|
|
1998
2163
|
assertPositiveInt(template.slotsPerBar, "template slotsPerBar");
|
|
1999
2164
|
assertGenerationBudget(template.slots.length, "template slots");
|
|
2000
2165
|
if (template.slots.length !== template.slotsPerBar) {
|
|
2001
|
-
throw new
|
|
2166
|
+
throw new InvalidInputError("template slots length must equal slotsPerBar");
|
|
2002
2167
|
}
|
|
2003
2168
|
if (template.ts && (template.ts.numerator !== ts.numerator || template.ts.denominator !== ts.denominator)) {
|
|
2004
|
-
throw new
|
|
2169
|
+
throw new InvalidInputError(
|
|
2005
2170
|
`Groove template meter ${formatTimeSignature(template.ts)} does not match apply-time meter ${formatTimeSignature(ts)}`
|
|
2006
2171
|
);
|
|
2007
2172
|
}
|
|
2008
2173
|
const barBeats = beatsPerBar(ts);
|
|
2009
|
-
return events.map((event) => {
|
|
2174
|
+
return dropSilentNotes(events).map((event) => {
|
|
2010
2175
|
const { quantizedBeat, slotIndex } = quantizeToGrid(
|
|
2011
2176
|
event.startBeat,
|
|
2012
2177
|
barBeats,
|
|
@@ -2014,7 +2179,7 @@ function applyGrooveTemplate(events, template, ts) {
|
|
|
2014
2179
|
template.slotsPerBar
|
|
2015
2180
|
);
|
|
2016
2181
|
const slot = template.slots[slotIndex];
|
|
2017
|
-
const startBeat = quantizedBeat + (slot?.timingOffset ?? 0);
|
|
2182
|
+
const startBeat = Math.max(0, quantizedBeat + (slot?.timingOffset ?? 0));
|
|
2018
2183
|
const velocity = slot && slot.velocity !== null && slot.velocity !== void 0 ? Math.round(slot.velocity) : event.velocity;
|
|
2019
2184
|
return {
|
|
2020
2185
|
pitch: event.pitch,
|
|
@@ -2026,15 +2191,20 @@ function applyGrooveTemplate(events, template, ts) {
|
|
|
2026
2191
|
}
|
|
2027
2192
|
|
|
2028
2193
|
// src/generate/harmonize/index.ts
|
|
2029
|
-
var FALLBACK = {
|
|
2194
|
+
var FALLBACK = {
|
|
2195
|
+
rootPc: 0,
|
|
2196
|
+
quality: "maj",
|
|
2197
|
+
secondaryDominant: false,
|
|
2198
|
+
base: 0,
|
|
2199
|
+
pcs: chordPitchClasses(makeChord(0, "maj"))
|
|
2200
|
+
};
|
|
2201
|
+
var DEFAULT_HARMONIC_RHYTHM = 2;
|
|
2202
|
+
var DEFAULT_PLACEMENT = { transposeSearch: false, octaveSearch: false };
|
|
2030
2203
|
var DEFAULT_METER = { numerator: 4, denominator: 4 };
|
|
2031
2204
|
var COMFORT_LOW = 55;
|
|
2032
2205
|
var COMFORT_HIGH = 79;
|
|
2033
2206
|
var TESSITURA_WEIGHT = 1e-3;
|
|
2034
|
-
var
|
|
2035
|
-
function pitchClass2(pitch) {
|
|
2036
|
-
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
2037
|
-
}
|
|
2207
|
+
var TIE_BREAK_JITTER2 = 1e-6;
|
|
2038
2208
|
function inferKey(melody) {
|
|
2039
2209
|
let best = majorKey(0);
|
|
2040
2210
|
let bestScore = Number.NEGATIVE_INFINITY;
|
|
@@ -2043,13 +2213,13 @@ function inferKey(melody) {
|
|
|
2043
2213
|
candidates.push(majorKey(tonic), minorKey(tonic));
|
|
2044
2214
|
}
|
|
2045
2215
|
for (const key of candidates) {
|
|
2046
|
-
const tonic =
|
|
2216
|
+
const tonic = pitchClassOf(key.rootPc);
|
|
2047
2217
|
const isMinor = key.modeMask12 === NATURAL_MINOR_MASK;
|
|
2048
2218
|
const leadingTone = (tonic + 11) % 12;
|
|
2049
2219
|
let score = 0;
|
|
2050
2220
|
for (const n of melody) {
|
|
2051
2221
|
const w = Math.max(0.25, n.durationBeat);
|
|
2052
|
-
const pc =
|
|
2222
|
+
const pc = pitchClassOf(n.pitch);
|
|
2053
2223
|
if (isScaleTone(n.pitch, key) || isMinor && pc === leadingTone) {
|
|
2054
2224
|
score += w;
|
|
2055
2225
|
}
|
|
@@ -2066,22 +2236,28 @@ function inferKey(melody) {
|
|
|
2066
2236
|
}
|
|
2067
2237
|
function buildCandidates(key, reharmonize) {
|
|
2068
2238
|
const tones = scaleTonesInDegreeOrder(key);
|
|
2069
|
-
const candidates = tones.map((rootPc, degree) =>
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2239
|
+
const candidates = tones.map((rootPc, degree) => {
|
|
2240
|
+
const quality = diatonicTriad(degree, key).quality;
|
|
2241
|
+
return {
|
|
2242
|
+
rootPc,
|
|
2243
|
+
quality,
|
|
2244
|
+
degree,
|
|
2245
|
+
secondaryDominant: false,
|
|
2246
|
+
base: 0,
|
|
2247
|
+
pcs: chordPitchClasses(makeChord(rootPc, quality))
|
|
2248
|
+
};
|
|
2249
|
+
});
|
|
2076
2250
|
if (reharmonize !== "diatonic") {
|
|
2077
2251
|
for (const target of [1, 3, 4, 5]) {
|
|
2078
2252
|
const targetRoot = tones[target] ?? 0;
|
|
2253
|
+
const rootPc = (targetRoot + 7) % 12;
|
|
2079
2254
|
candidates.push({
|
|
2080
|
-
rootPc
|
|
2255
|
+
rootPc,
|
|
2081
2256
|
quality: "dom7",
|
|
2082
2257
|
secondaryDominant: true,
|
|
2083
2258
|
targetDegree: target,
|
|
2084
|
-
base: 1.5
|
|
2259
|
+
base: 1.5,
|
|
2260
|
+
pcs: chordPitchClasses(makeChord(rootPc, "dom7"))
|
|
2085
2261
|
});
|
|
2086
2262
|
}
|
|
2087
2263
|
}
|
|
@@ -2095,7 +2271,8 @@ function buildCandidates(key, reharmonize) {
|
|
|
2095
2271
|
rootPc: chord.rootPc,
|
|
2096
2272
|
quality: chord.quality,
|
|
2097
2273
|
secondaryDominant: false,
|
|
2098
|
-
base: 1.25
|
|
2274
|
+
base: 1.25,
|
|
2275
|
+
pcs: chordPitchClasses(chord)
|
|
2099
2276
|
});
|
|
2100
2277
|
}
|
|
2101
2278
|
}
|
|
@@ -2110,8 +2287,8 @@ function buildCandidates(key, reharmonize) {
|
|
|
2110
2287
|
return true;
|
|
2111
2288
|
});
|
|
2112
2289
|
}
|
|
2113
|
-
function emissionCost(seg, cand, melody, key) {
|
|
2114
|
-
const pcs =
|
|
2290
|
+
function emissionCost(seg, cand, melody, key, ts) {
|
|
2291
|
+
const pcs = cand.pcs;
|
|
2115
2292
|
let cost = cand.base;
|
|
2116
2293
|
for (const idx of seg.noteIndices) {
|
|
2117
2294
|
const note = melody[idx];
|
|
@@ -2124,10 +2301,10 @@ function emissionCost(seg, cand, melody, key) {
|
|
|
2124
2301
|
continue;
|
|
2125
2302
|
}
|
|
2126
2303
|
const w = Math.max(0.25, overlap);
|
|
2127
|
-
if (pcs.includes(
|
|
2304
|
+
if (pcs.includes(pitchClassOf(note.pitch))) {
|
|
2128
2305
|
continue;
|
|
2129
2306
|
}
|
|
2130
|
-
const strong = isStrongBeat(overlapStart,
|
|
2307
|
+
const strong = isStrongBeat(overlapStart, ts);
|
|
2131
2308
|
cost += (strong ? 10 : 2) * w;
|
|
2132
2309
|
if (!isScaleTone(note.pitch, key)) {
|
|
2133
2310
|
cost += w;
|
|
@@ -2161,13 +2338,13 @@ function transitionCost(prev, cur, tonicPc) {
|
|
|
2161
2338
|
cost += dist * 0.3;
|
|
2162
2339
|
return cost;
|
|
2163
2340
|
}
|
|
2164
|
-
function harmonizeOnce(melody, key, candidates, segments, jitter) {
|
|
2165
|
-
const tonicPc =
|
|
2341
|
+
function harmonizeOnce(melody, key, candidates, segments, jitter, ts) {
|
|
2342
|
+
const tonicPc = pitchClassOf(key.rootPc);
|
|
2166
2343
|
const n = candidates.length;
|
|
2167
2344
|
const candAt = (i) => candidates[i] ?? FALLBACK;
|
|
2168
2345
|
const seg0 = segments[0];
|
|
2169
2346
|
let dp = candidates.map(
|
|
2170
|
-
(c, ci) => (seg0 ? emissionCost(seg0, c, melody, key) : 0) + (jitter[ci] ?? 0)
|
|
2347
|
+
(c, ci) => (seg0 ? emissionCost(seg0, c, melody, key, ts) : 0) + (jitter[ci] ?? 0)
|
|
2171
2348
|
);
|
|
2172
2349
|
const back = [];
|
|
2173
2350
|
for (let s = 1; s < segments.length; s += 1) {
|
|
@@ -2187,7 +2364,7 @@ function harmonizeOnce(melody, key, candidates, segments, jitter) {
|
|
|
2187
2364
|
bestPrev = p;
|
|
2188
2365
|
}
|
|
2189
2366
|
}
|
|
2190
|
-
next[c] = best + emissionCost(seg, candAt(c), melody, key) + (jitter[c] ?? 0);
|
|
2367
|
+
next[c] = best + emissionCost(seg, candAt(c), melody, key, ts) + (jitter[c] ?? 0);
|
|
2191
2368
|
ptr[c] = bestPrev;
|
|
2192
2369
|
}
|
|
2193
2370
|
dp = next;
|
|
@@ -2212,32 +2389,47 @@ function harmonizeOnce(melody, key, candidates, segments, jitter) {
|
|
|
2212
2389
|
return { cost: bestCost, path };
|
|
2213
2390
|
}
|
|
2214
2391
|
function harmonizeMelody(opts) {
|
|
2215
|
-
|
|
2216
|
-
const
|
|
2217
|
-
const
|
|
2392
|
+
assertNoteEvents(opts.melody, "harmonize melody", { allowNonPositiveDuration: true });
|
|
2393
|
+
const soundingMelody = soundingNotesOnly(opts.melody);
|
|
2394
|
+
const noteIndex = createNoteEventIndex(soundingMelody);
|
|
2395
|
+
const ts = opts.ts ?? DEFAULT_METER;
|
|
2396
|
+
assertTimeSignature(ts);
|
|
2397
|
+
const requestedKey = opts.key ?? "infer";
|
|
2398
|
+
const key = requestedKey === "infer" ? inferKey(soundingMelody) : requestedKey;
|
|
2399
|
+
const placement = opts.placement ?? DEFAULT_PLACEMENT;
|
|
2400
|
+
if (noteIndex.notes.length === 0) {
|
|
2401
|
+
return { transposeSemitones: 0, key, chords: [], melodyRoles: [] };
|
|
2402
|
+
}
|
|
2403
|
+
const candidates = buildCandidates(key, opts.reharmonize ?? "diatonic");
|
|
2218
2404
|
const candAt = (i) => candidates[i] ?? FALLBACK;
|
|
2219
2405
|
const rng = createRng(opts.seed ?? 0);
|
|
2220
|
-
const jitter = candidates.map(() => rng.next() *
|
|
2221
|
-
const
|
|
2222
|
-
|
|
2223
|
-
|
|
2406
|
+
const jitter = candidates.map(() => rng.next() * TIE_BREAK_JITTER2);
|
|
2407
|
+
const melodyStart = noteIndex.notes.reduce(
|
|
2408
|
+
(start, indexed) => Math.min(start, indexed.note.startBeat),
|
|
2409
|
+
Number.POSITIVE_INFINITY
|
|
2410
|
+
);
|
|
2411
|
+
const melodyEnd = noteIndex.notes.reduce((end, indexed) => Math.max(end, indexed.endBeat), 0);
|
|
2412
|
+
const hr = assertRange(
|
|
2413
|
+
opts.harmonicRhythm ?? DEFAULT_HARMONIC_RHYTHM,
|
|
2224
2414
|
Number.MIN_VALUE,
|
|
2225
2415
|
Number.MAX_SAFE_INTEGER,
|
|
2226
2416
|
"harmonic rhythm"
|
|
2227
2417
|
);
|
|
2228
|
-
const
|
|
2229
|
-
const segCount = Math.max(1, Math.ceil(melodyEnd / hr));
|
|
2418
|
+
const segmentStart = Math.floor(melodyStart / hr) * hr;
|
|
2419
|
+
const segCount = Math.max(1, Math.ceil((melodyEnd - segmentStart) / hr));
|
|
2230
2420
|
assertGenerationBudget(segCount, "harmonic segments");
|
|
2231
|
-
assertGenerationBudget(segCount * candidates.length * candidates.length, "harmonization search");
|
|
2232
2421
|
const segments = Array.from({ length: segCount }, (_, s) => ({
|
|
2233
|
-
startBeat: s * hr,
|
|
2234
|
-
endBeat: (s + 1) * hr,
|
|
2422
|
+
startBeat: segmentStart + s * hr,
|
|
2423
|
+
endBeat: segmentStart + (s + 1) * hr,
|
|
2235
2424
|
noteIndices: []
|
|
2236
2425
|
}));
|
|
2237
2426
|
let memberships = 0;
|
|
2238
2427
|
for (const indexed of noteIndex.notes) {
|
|
2239
|
-
const first = Math.max(0, Math.floor(indexed.note.startBeat / hr));
|
|
2240
|
-
const lastExclusive = Math.min(
|
|
2428
|
+
const first = Math.max(0, Math.floor((indexed.note.startBeat - segmentStart) / hr));
|
|
2429
|
+
const lastExclusive = Math.min(
|
|
2430
|
+
segCount,
|
|
2431
|
+
Math.ceil((indexed.endBeat - segmentStart) / hr - Number.EPSILON)
|
|
2432
|
+
);
|
|
2241
2433
|
memberships += Math.max(0, lastExclusive - first);
|
|
2242
2434
|
assertGenerationBudget(memberships, "note-to-segment memberships");
|
|
2243
2435
|
for (let segment = first; segment < lastExclusive; segment += 1) {
|
|
@@ -2245,31 +2437,35 @@ function harmonizeMelody(opts) {
|
|
|
2245
2437
|
}
|
|
2246
2438
|
}
|
|
2247
2439
|
const transposes = [0];
|
|
2248
|
-
if (
|
|
2440
|
+
if (placement.transposeSearch) {
|
|
2249
2441
|
for (let s = -6; s <= 6; s += 1) {
|
|
2250
2442
|
if (s !== 0) {
|
|
2251
2443
|
transposes.push(s);
|
|
2252
2444
|
}
|
|
2253
2445
|
}
|
|
2254
2446
|
}
|
|
2255
|
-
if (
|
|
2447
|
+
if (placement.octaveSearch) {
|
|
2256
2448
|
for (const o of [-12, 12]) {
|
|
2257
2449
|
if (!transposes.includes(o)) {
|
|
2258
2450
|
transposes.push(o);
|
|
2259
2451
|
}
|
|
2260
2452
|
}
|
|
2261
2453
|
}
|
|
2454
|
+
assertGenerationBudget(
|
|
2455
|
+
transposes.length * segCount * candidates.length * candidates.length,
|
|
2456
|
+
"harmonization placement search"
|
|
2457
|
+
);
|
|
2262
2458
|
let bestCost = Number.POSITIVE_INFINITY;
|
|
2263
2459
|
let bestTs = 0;
|
|
2264
2460
|
let bestPath = [];
|
|
2265
2461
|
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);
|
|
2462
|
+
for (const shift of transposes) {
|
|
2463
|
+
const shifted = opts.melody.map((n) => ({ ...n, pitch: n.pitch + shift }));
|
|
2464
|
+
const { cost, path } = harmonizeOnce(shifted, key, candidates, segments, jitter, ts);
|
|
2269
2465
|
const total = cost + tessituraCost(shifted);
|
|
2270
2466
|
if (total < bestCost) {
|
|
2271
2467
|
bestCost = total;
|
|
2272
|
-
bestTs =
|
|
2468
|
+
bestTs = shift;
|
|
2273
2469
|
bestPath = path;
|
|
2274
2470
|
bestMelody = shifted;
|
|
2275
2471
|
}
|
|
@@ -2279,7 +2475,7 @@ function harmonizeMelody(opts) {
|
|
|
2279
2475
|
const chord = {
|
|
2280
2476
|
rootPc: cand.rootPc,
|
|
2281
2477
|
quality: cand.quality,
|
|
2282
|
-
startBeat: segments[s]?.startBeat ?? s * hr
|
|
2478
|
+
startBeat: segments[s]?.startBeat ?? segmentStart + s * hr
|
|
2283
2479
|
};
|
|
2284
2480
|
if (cand.degree !== void 0) {
|
|
2285
2481
|
chord.degree = cand.degree;
|
|
@@ -2299,10 +2495,13 @@ function harmonizeMelody(opts) {
|
|
|
2299
2495
|
}
|
|
2300
2496
|
|
|
2301
2497
|
// src/generate/motif/index.ts
|
|
2302
|
-
function pitchClass3(pitch) {
|
|
2303
|
-
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
2304
|
-
}
|
|
2305
2498
|
var MIN_TILE_SPAN = 1 / 256;
|
|
2499
|
+
var DEFAULT_TS4 = { numerator: 4, denominator: 4 };
|
|
2500
|
+
var EPS3 = 1e-9;
|
|
2501
|
+
function isDownbeat(startBeat, barBeats) {
|
|
2502
|
+
const bar = startBeat / barBeats;
|
|
2503
|
+
return Math.abs(bar - Math.round(bar)) < EPS3;
|
|
2504
|
+
}
|
|
2306
2505
|
function upScaleTone(pitch, key) {
|
|
2307
2506
|
for (let d = 1; d <= 12; d += 1) {
|
|
2308
2507
|
if (isScaleTone(pitch + d, key)) {
|
|
@@ -2330,10 +2529,10 @@ function stepDiatonic(pitch, degrees, key) {
|
|
|
2330
2529
|
function nearestChordTone(pitch, chord) {
|
|
2331
2530
|
const pcs = chordPitchClasses(chord);
|
|
2332
2531
|
for (let d = 0; d <= 12; d += 1) {
|
|
2333
|
-
if (pcs.includes(
|
|
2532
|
+
if (pcs.includes(pitchClassOf(pitch - d))) {
|
|
2334
2533
|
return pitch - d;
|
|
2335
2534
|
}
|
|
2336
|
-
if (pcs.includes(
|
|
2535
|
+
if (pcs.includes(pitchClassOf(pitch + d))) {
|
|
2337
2536
|
return pitch + d;
|
|
2338
2537
|
}
|
|
2339
2538
|
}
|
|
@@ -2387,14 +2586,15 @@ function contourOffsets(contour, count) {
|
|
|
2387
2586
|
function generateMotif(opts) {
|
|
2388
2587
|
const contour = opts.contour ?? "arch";
|
|
2389
2588
|
const bars = assertPositiveInt(opts.bars, "motif bars");
|
|
2390
|
-
const
|
|
2589
|
+
const barBeats = beatsPerBar(opts.ts ?? DEFAULT_TS4);
|
|
2590
|
+
const totalBeats = bars * barBeats;
|
|
2391
2591
|
const noteCount = Math.max(3, bars * 2);
|
|
2392
2592
|
assertGenerationBudget(noteCount, "motif notes");
|
|
2393
2593
|
const beatsPerNote = totalBeats / noteCount;
|
|
2394
2594
|
const rng = createRng(opts.seed ?? 0);
|
|
2395
2595
|
const requestedJitter = assertFiniteNumber(opts.jitter ?? 0, "motif jitter");
|
|
2396
2596
|
const jitterProb = Math.min(1, Math.max(0, requestedJitter));
|
|
2397
|
-
const tonic =
|
|
2597
|
+
const tonic = pitchClassOf(opts.key.rootPc) + 60;
|
|
2398
2598
|
const offsets = contourOffsets(contour, noteCount);
|
|
2399
2599
|
const notes = [];
|
|
2400
2600
|
for (let i = 0; i < noteCount; i += 1) {
|
|
@@ -2404,23 +2604,46 @@ function generateMotif(opts) {
|
|
|
2404
2604
|
}
|
|
2405
2605
|
let pitch = stepDiatonic(tonic, (offsets[i] ?? 0) + jitter, opts.key);
|
|
2406
2606
|
const startBeat = i * beatsPerNote;
|
|
2407
|
-
if (opts.chord && startBeat
|
|
2607
|
+
if (opts.chord && isDownbeat(startBeat, barBeats)) {
|
|
2408
2608
|
pitch = nearestChordTone(pitch, opts.chord);
|
|
2409
2609
|
}
|
|
2410
|
-
notes.push({
|
|
2610
|
+
notes.push({
|
|
2611
|
+
pitch: clampToMidi(pitch, "generated motif pitch"),
|
|
2612
|
+
startBeat,
|
|
2613
|
+
durationBeat: beatsPerNote
|
|
2614
|
+
});
|
|
2411
2615
|
}
|
|
2412
2616
|
return { notes };
|
|
2413
2617
|
}
|
|
2618
|
+
function motifToNoteEvents(cell) {
|
|
2619
|
+
return cell.notes.map((note) => ({ ...note }));
|
|
2620
|
+
}
|
|
2414
2621
|
function transformMotif(cell, t, amount, key) {
|
|
2622
|
+
return assertInRange(transformUnchecked(cell, t, amount, key));
|
|
2623
|
+
}
|
|
2624
|
+
function assertInRange(cell) {
|
|
2625
|
+
for (const note of cell.notes) {
|
|
2626
|
+
if (note.pitch < 0 || note.pitch > 127) {
|
|
2627
|
+
throw new InvalidInputError(
|
|
2628
|
+
`transformed motif pitch ${note.pitch} is outside the MIDI range 0..127`
|
|
2629
|
+
);
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
return cell;
|
|
2633
|
+
}
|
|
2634
|
+
function transformUnchecked(cell, t, amount, key) {
|
|
2415
2635
|
assertNoteEvents(cell.notes, "motif notes");
|
|
2416
2636
|
if (amount !== void 0) {
|
|
2417
2637
|
assertFiniteNumber(amount, "motif transform amount");
|
|
2418
2638
|
}
|
|
2419
|
-
if ((t === "transposeDiatonic" || t === "sequence") && amount !== void 0) {
|
|
2420
|
-
assertInteger(
|
|
2639
|
+
if ((t === "transposeChromatic" || t === "transposeDiatonic" || t === "sequence") && amount !== void 0) {
|
|
2640
|
+
assertInteger(
|
|
2641
|
+
amount,
|
|
2642
|
+
`${t === "transposeChromatic" ? "chromatic" : "diatonic"} transform amount`
|
|
2643
|
+
);
|
|
2421
2644
|
}
|
|
2422
2645
|
if ((t === "augment" || t === "diminish") && amount !== void 0 && amount <= 0) {
|
|
2423
|
-
throw new
|
|
2646
|
+
throw new InvalidInputError("time transform amount must be positive");
|
|
2424
2647
|
}
|
|
2425
2648
|
const notes = cell.notes;
|
|
2426
2649
|
switch (t) {
|
|
@@ -2480,11 +2703,11 @@ function scaleTime(cell, factor) {
|
|
|
2480
2703
|
}))
|
|
2481
2704
|
};
|
|
2482
2705
|
}
|
|
2483
|
-
function developMotif(cell, timeline, key, bars) {
|
|
2706
|
+
function developMotif(cell, timeline, key, bars, ts = DEFAULT_TS4) {
|
|
2484
2707
|
assertPositiveInt(bars, "development bars");
|
|
2485
2708
|
assertNoteEvents(cell.notes, "motif notes");
|
|
2486
2709
|
const span = cellSpan(cell);
|
|
2487
|
-
const totalBeats = bars *
|
|
2710
|
+
const totalBeats = bars * beatsPerBar(ts);
|
|
2488
2711
|
const origin = cellOrigin(cell.notes);
|
|
2489
2712
|
const out = [];
|
|
2490
2713
|
if (span <= 0) {
|
|
@@ -2503,7 +2726,7 @@ function developMotif(cell, timeline, key, bars) {
|
|
|
2503
2726
|
const chord = timeline.at(startBeat);
|
|
2504
2727
|
let pitch = n.pitch;
|
|
2505
2728
|
if (chord) {
|
|
2506
|
-
const isMember = chordPitchClasses(chord).includes(
|
|
2729
|
+
const isMember = chordPitchClasses(chord).includes(pitchClassOf(pitch));
|
|
2507
2730
|
pitch = isMember ? pitch : nearestChordTone(pitch, chord);
|
|
2508
2731
|
} else {
|
|
2509
2732
|
pitch = nearestScaleTone(pitch, key);
|
|
@@ -2515,6 +2738,20 @@ function developMotif(cell, timeline, key, bars) {
|
|
|
2515
2738
|
}
|
|
2516
2739
|
|
|
2517
2740
|
// src/generate/progression/index.ts
|
|
2741
|
+
var BORROWED_DEGREES = Object.freeze({
|
|
2742
|
+
/** Flat submediant: bVI. */
|
|
2743
|
+
bVI: 8,
|
|
2744
|
+
/** Flat subtonic: bVII. */
|
|
2745
|
+
bVII: 10,
|
|
2746
|
+
/** Flat mediant: bIII. */
|
|
2747
|
+
bIII: 11,
|
|
2748
|
+
/** Minor subdominant borrowed from the parallel minor: iv. */
|
|
2749
|
+
iv: 12,
|
|
2750
|
+
/** Neapolitan: bII. */
|
|
2751
|
+
bII: 13,
|
|
2752
|
+
/** Sharp subdominant, the diminished #IV. */
|
|
2753
|
+
sharpIV: 14
|
|
2754
|
+
});
|
|
2518
2755
|
var PRESETS = [
|
|
2519
2756
|
{
|
|
2520
2757
|
id: "fourChordPop",
|
|
@@ -2672,18 +2909,12 @@ var PRESETS = [
|
|
|
2672
2909
|
}
|
|
2673
2910
|
];
|
|
2674
2911
|
var BORROWED_OFFSET = {
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
12: 5,
|
|
2682
|
-
// iv
|
|
2683
|
-
13: 1,
|
|
2684
|
-
// bII
|
|
2685
|
-
14: 6
|
|
2686
|
-
// #IV
|
|
2912
|
+
[BORROWED_DEGREES.bVI]: 8,
|
|
2913
|
+
[BORROWED_DEGREES.bVII]: 10,
|
|
2914
|
+
[BORROWED_DEGREES.bIII]: 3,
|
|
2915
|
+
[BORROWED_DEGREES.iv]: 5,
|
|
2916
|
+
[BORROWED_DEGREES.bII]: 1,
|
|
2917
|
+
[BORROWED_DEGREES.sharpIV]: 6
|
|
2687
2918
|
};
|
|
2688
2919
|
function progressions() {
|
|
2689
2920
|
return PRESETS.map((p) => ({ ...p, degrees: [...p.degrees], styles: [...p.styles] }));
|
|
@@ -2699,8 +2930,12 @@ function degreeToRootPc(degree, key) {
|
|
|
2699
2930
|
const offset = BORROWED_OFFSET[degree] ?? 0;
|
|
2700
2931
|
return ((key.rootPc % 12 + offset) % 12 + 12) % 12;
|
|
2701
2932
|
}
|
|
2702
|
-
function autoQuality(degree, key) {
|
|
2933
|
+
function autoQuality(degree, key, harmonicDominant) {
|
|
2703
2934
|
if (degree >= 0 && degree <= 6) {
|
|
2935
|
+
const isMinor = (key.modeMask12 >> 3 & 1) === 1 && (key.modeMask12 >> 4 & 1) === 0;
|
|
2936
|
+
if (harmonicDominant && isMinor && degree === 4) {
|
|
2937
|
+
return "maj";
|
|
2938
|
+
}
|
|
2704
2939
|
return diatonicTriad(degree, key).quality;
|
|
2705
2940
|
}
|
|
2706
2941
|
if (degree === 14) {
|
|
@@ -2711,40 +2946,99 @@ function autoQuality(degree, key) {
|
|
|
2711
2946
|
}
|
|
2712
2947
|
return "maj";
|
|
2713
2948
|
}
|
|
2949
|
+
function resolveCycle(degrees, key, ext, harmonicDominant) {
|
|
2950
|
+
const steps = [];
|
|
2951
|
+
for (const degree of degrees) {
|
|
2952
|
+
const step = {
|
|
2953
|
+
source: degree,
|
|
2954
|
+
rootPc: degreeToRootPc(degree, key),
|
|
2955
|
+
quality: ext !== void 0 && ext !== "auto" ? ext : autoQuality(degree, key, harmonicDominant)
|
|
2956
|
+
};
|
|
2957
|
+
if (degree >= 0 && degree <= 6) {
|
|
2958
|
+
step.degree = degree;
|
|
2959
|
+
}
|
|
2960
|
+
const previous = steps[steps.length - 1];
|
|
2961
|
+
if (previous !== void 0 && previous.source !== step.source && sameStep(previous, step)) {
|
|
2962
|
+
continue;
|
|
2963
|
+
}
|
|
2964
|
+
steps.push(step);
|
|
2965
|
+
}
|
|
2966
|
+
const first = steps[0];
|
|
2967
|
+
const last = steps[steps.length - 1];
|
|
2968
|
+
if (steps.length > 1 && first !== void 0 && last !== void 0 && first.source !== last.source && sameStep(first, last)) {
|
|
2969
|
+
steps.pop();
|
|
2970
|
+
}
|
|
2971
|
+
return steps.length > 0 ? steps : [{ source: 0, degree: 0, rootPc: degreeToRootPc(0, key), quality: "maj" }];
|
|
2972
|
+
}
|
|
2973
|
+
function sameStep(a, b) {
|
|
2974
|
+
return a.rootPc === b.rootPc && a.quality === b.quality;
|
|
2975
|
+
}
|
|
2976
|
+
function pickProgressionPreset(style, seed = 0) {
|
|
2977
|
+
const pool = PRESETS.filter((preset) => preset.styles.includes(style));
|
|
2978
|
+
if (pool.length === 0) {
|
|
2979
|
+
throw new InvalidInputError(`Unknown progression style: ${style}`);
|
|
2980
|
+
}
|
|
2981
|
+
const rng = createRng(seed);
|
|
2982
|
+
const index = Math.floor(rng.next() * pool.length) % pool.length;
|
|
2983
|
+
return pool[index] ?? PRESETS[0];
|
|
2984
|
+
}
|
|
2714
2985
|
function generateProgression(opts) {
|
|
2715
2986
|
assertPositiveInt(opts.bars, "progression bars");
|
|
2716
2987
|
assertGenerationBudget(opts.bars, "progression chords");
|
|
2717
2988
|
const seed = opts.seed ?? 0;
|
|
2718
2989
|
let preset;
|
|
2719
|
-
if (opts.
|
|
2990
|
+
if (opts.preset !== void 0) {
|
|
2991
|
+
if (opts.preset.degrees.length === 0) {
|
|
2992
|
+
throw new InvalidInputError("progression preset must name at least one degree");
|
|
2993
|
+
}
|
|
2994
|
+
preset = {
|
|
2995
|
+
id: opts.preset.id ?? "custom",
|
|
2996
|
+
name: opts.preset.name ?? "Custom",
|
|
2997
|
+
degrees: opts.preset.degrees.map((degree, index) => {
|
|
2998
|
+
assertDegree(degree, `progression preset degrees[${index}]`);
|
|
2999
|
+
const supported = degree >= 0 && degree <= 6 || Object.values(BORROWED_DEGREES).includes(degree);
|
|
3000
|
+
if (!supported) {
|
|
3001
|
+
throw new InvalidInputError(
|
|
3002
|
+
`progression preset degrees[${index}] is not a supported progression degree; received ${degree}`
|
|
3003
|
+
);
|
|
3004
|
+
}
|
|
3005
|
+
return degree;
|
|
3006
|
+
}),
|
|
3007
|
+
functional: opts.preset.functional ?? "loop",
|
|
3008
|
+
styles: opts.preset.styles ?? [opts.style]
|
|
3009
|
+
};
|
|
3010
|
+
}
|
|
3011
|
+
if (preset === void 0 && opts.presetId !== void 0) {
|
|
2720
3012
|
preset = PRESETS.find((p) => p.id === opts.presetId);
|
|
2721
3013
|
if (preset === void 0) {
|
|
2722
|
-
throw new
|
|
3014
|
+
throw new InvalidInputError(`Unknown progression preset: ${opts.presetId}`);
|
|
2723
3015
|
}
|
|
2724
3016
|
}
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
3017
|
+
preset ??= pickProgressionPreset(opts.style, seed);
|
|
3018
|
+
const ext = opts.ext === void 0 ? "auto" : assertOneOf(opts.ext, ["auto", ...chordQualities()], "progression extension");
|
|
3019
|
+
const cycle = resolveCycle(
|
|
3020
|
+
preset?.degrees ?? [0],
|
|
3021
|
+
opts.key,
|
|
3022
|
+
ext,
|
|
3023
|
+
preset?.functional === "cadenceStrong"
|
|
3024
|
+
);
|
|
2733
3025
|
const chords = [];
|
|
2734
3026
|
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);
|
|
3027
|
+
const step = cycle[bar % cycle.length];
|
|
2737
3028
|
const chord = {
|
|
2738
|
-
rootPc:
|
|
2739
|
-
quality,
|
|
2740
|
-
startBeat: bar * 4
|
|
2741
|
-
degree: degree >= 0 && degree <= 6 ? degree : void 0
|
|
3029
|
+
rootPc: step?.rootPc ?? 0,
|
|
3030
|
+
quality: step?.quality ?? "maj",
|
|
3031
|
+
startBeat: bar * 4
|
|
2742
3032
|
};
|
|
3033
|
+
if (step?.degree !== void 0) {
|
|
3034
|
+
chord.degree = step.degree;
|
|
3035
|
+
}
|
|
2743
3036
|
chords.push(chord);
|
|
2744
3037
|
}
|
|
2745
3038
|
if (opts.reharmonize) {
|
|
2746
3039
|
const tonicPc = (opts.key.rootPc % 12 + 12) % 12;
|
|
2747
3040
|
const rrng = createRng((seed ^ 2654435769) >>> 0);
|
|
3041
|
+
let tonicStatements = chords.filter((chord) => chord.rootPc === tonicPc).length;
|
|
2748
3042
|
for (let i = 0; i < chords.length - 1; i += 1) {
|
|
2749
3043
|
const cur = chords[i];
|
|
2750
3044
|
const next = chords[i + 1];
|
|
@@ -2753,12 +3047,16 @@ function generateProgression(opts) {
|
|
|
2753
3047
|
}
|
|
2754
3048
|
const domRoot = (next.rootPc + 7) % 12;
|
|
2755
3049
|
const targetsTonic = (next.rootPc - tonicPc + 12) % 12 === 0;
|
|
3050
|
+
const isLastTonicStatement = cur.rootPc === tonicPc && tonicStatements <= 1;
|
|
2756
3051
|
const alreadySecondary = cur.rootPc === domRoot && cur.quality === "dom7";
|
|
2757
3052
|
const isResolutionTarget = chords[i - 1]?.secondaryDominant === true;
|
|
2758
|
-
if (targetsTonic || alreadySecondary || isResolutionTarget) {
|
|
3053
|
+
if (targetsTonic || isLastTonicStatement || alreadySecondary || isResolutionTarget) {
|
|
2759
3054
|
continue;
|
|
2760
3055
|
}
|
|
2761
3056
|
if (rrng.next() < 0.5) {
|
|
3057
|
+
if (cur.rootPc === tonicPc) {
|
|
3058
|
+
tonicStatements -= 1;
|
|
3059
|
+
}
|
|
2762
3060
|
chords[i] = {
|
|
2763
3061
|
rootPc: domRoot,
|
|
2764
3062
|
quality: "dom7",
|
|
@@ -2772,6 +3070,17 @@ function generateProgression(opts) {
|
|
|
2772
3070
|
}
|
|
2773
3071
|
|
|
2774
3072
|
// src/generate/rhythm/index.ts
|
|
3073
|
+
var DEFAULT_RHYTHM_VELOCITY = 96;
|
|
3074
|
+
function rhythmToNoteEvents(events, pitch, velocity = DEFAULT_RHYTHM_VELOCITY) {
|
|
3075
|
+
assertFiniteNumber(pitch, "pitch");
|
|
3076
|
+
assertRange(velocity, 0, 127, "velocity");
|
|
3077
|
+
return events.map((event) => ({
|
|
3078
|
+
pitch,
|
|
3079
|
+
startBeat: event.position,
|
|
3080
|
+
durationBeat: event.duration,
|
|
3081
|
+
velocity
|
|
3082
|
+
}));
|
|
3083
|
+
}
|
|
2775
3084
|
var DEFAULT_SUBDIVISION = 2;
|
|
2776
3085
|
var DEFAULT_DENSITY = 0.5;
|
|
2777
3086
|
var DEFAULT_BARS = 1;
|
|
@@ -2791,28 +3100,34 @@ function generateRhythm(ts, opts = {}) {
|
|
|
2791
3100
|
const seed = opts.seed ?? 0;
|
|
2792
3101
|
const bars = opts.bars ?? DEFAULT_BARS;
|
|
2793
3102
|
const subdivision = opts.subdivision ?? DEFAULT_SUBDIVISION;
|
|
2794
|
-
const
|
|
2795
|
-
const density = Math.min(1, Math.max(0, requestedDensity));
|
|
3103
|
+
const density = assertRange(opts.density ?? DEFAULT_DENSITY, 0, 1, "rhythm density");
|
|
2796
3104
|
assertPositiveInt(bars, "rhythm bars");
|
|
2797
3105
|
assertPositiveInt(subdivision, "rhythm subdivision");
|
|
2798
3106
|
const barBeats = beatsPerBar(ts);
|
|
2799
3107
|
const spanBeats = barBeats * bars;
|
|
2800
3108
|
const step = 1 / subdivision;
|
|
2801
|
-
const
|
|
3109
|
+
const slotsPerBar = Math.ceil(barBeats * subdivision);
|
|
3110
|
+
const slotCount = slotsPerBar * bars;
|
|
2802
3111
|
assertGenerationBudget(slotCount, "rhythm grid slots");
|
|
2803
|
-
const slotsPerBar = Math.round(barBeats * subdivision);
|
|
2804
3112
|
const rng = createRng(seed);
|
|
2805
3113
|
const positions = [];
|
|
2806
|
-
for (let
|
|
2807
|
-
const
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
3114
|
+
for (let bar = 0; bar < bars; bar += 1) {
|
|
3115
|
+
const barStart = bar * barBeats;
|
|
3116
|
+
const barEnd = barStart + barBeats;
|
|
3117
|
+
for (let slot = 0; slot < slotsPerBar; slot += 1) {
|
|
3118
|
+
const position = barStart + slot * step;
|
|
3119
|
+
if (position >= barEnd - Number.EPSILON) {
|
|
3120
|
+
break;
|
|
3121
|
+
}
|
|
3122
|
+
if (slot === 0) {
|
|
3123
|
+
positions.push(position);
|
|
3124
|
+
continue;
|
|
3125
|
+
}
|
|
3126
|
+
const weight = metricWeight(position, ts);
|
|
3127
|
+
const probability = onsetWeightCurve(weight) * density;
|
|
3128
|
+
if (rng.prob(probability)) {
|
|
3129
|
+
positions.push(position);
|
|
3130
|
+
}
|
|
2816
3131
|
}
|
|
2817
3132
|
}
|
|
2818
3133
|
const events = [];
|
|
@@ -2834,21 +3149,28 @@ function rhythmDensity(events, ts) {
|
|
|
2834
3149
|
}
|
|
2835
3150
|
|
|
2836
3151
|
export {
|
|
3152
|
+
BASS_STYLES,
|
|
2837
3153
|
generateBassLine,
|
|
2838
3154
|
generateCounterMelody,
|
|
3155
|
+
DRUM_NOTES,
|
|
3156
|
+
drumVoiceOf,
|
|
2839
3157
|
generateDrums,
|
|
2840
3158
|
humanize,
|
|
2841
3159
|
extractGrooveTemplate,
|
|
2842
3160
|
applyGrooveTemplate,
|
|
2843
3161
|
harmonizeMelody,
|
|
2844
3162
|
generateMotif,
|
|
3163
|
+
motifToNoteEvents,
|
|
2845
3164
|
transformMotif,
|
|
2846
3165
|
developMotif,
|
|
3166
|
+
BORROWED_DEGREES,
|
|
2847
3167
|
progressions,
|
|
2848
3168
|
progressionsByStyle,
|
|
3169
|
+
pickProgressionPreset,
|
|
2849
3170
|
generateProgression,
|
|
3171
|
+
rhythmToNoteEvents,
|
|
2850
3172
|
onsetWeightCurve,
|
|
2851
3173
|
generateRhythm,
|
|
2852
3174
|
rhythmDensity
|
|
2853
3175
|
};
|
|
2854
|
-
//# sourceMappingURL=chunk-
|
|
3176
|
+
//# sourceMappingURL=chunk-AID75RN3.js.map
|