@libraz/libcantus 0.9.2 → 0.9.3
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 +4 -3
- package/dist/analyze/index.cjs +315 -42
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +11 -99
- package/dist/analyze/index.d.ts +11 -99
- package/dist/analyze/index.js +6 -6
- package/dist/chunk-2ZWMSJH6.js +30 -0
- package/dist/chunk-2ZWMSJH6.js.map +1 -0
- package/dist/{chunk-R3S2LUBH.js → chunk-4MHA432G.js} +316 -184
- package/dist/chunk-4MHA432G.js.map +1 -0
- package/dist/chunk-62QZKCJQ.js +49 -0
- package/dist/chunk-62QZKCJQ.js.map +1 -0
- package/dist/chunk-AKLYPWIN.js +83 -0
- package/dist/chunk-AKLYPWIN.js.map +1 -0
- package/dist/{chunk-XTC3X6MS.js → chunk-C5XYKUVG.js} +100 -12
- package/dist/chunk-C5XYKUVG.js.map +1 -0
- package/dist/{chunk-LN7ZZMD2.js → chunk-CHMXELDS.js} +33 -14
- package/dist/chunk-CHMXELDS.js.map +1 -0
- package/dist/{chunk-A4532ZFR.js → chunk-EH6EM37U.js} +60 -4
- package/dist/chunk-EH6EM37U.js.map +1 -0
- package/dist/chunk-FBQK3ZQN.js +131 -0
- package/dist/chunk-FBQK3ZQN.js.map +1 -0
- package/dist/{chunk-5HTNMQW4.js → chunk-GOAW4VYL.js} +87 -21
- package/dist/chunk-GOAW4VYL.js.map +1 -0
- package/dist/{chunk-MSWFZF7J.js → chunk-I5R7ZBJS.js} +90 -6
- package/dist/chunk-I5R7ZBJS.js.map +1 -0
- package/dist/{chunk-GQKN5KDT.js → chunk-IT2O4XFE.js} +4 -9
- package/dist/chunk-IT2O4XFE.js.map +1 -0
- package/dist/{chunk-7Y7WF64D.js → chunk-MIWO425M.js} +6 -4
- package/dist/chunk-MIWO425M.js.map +1 -0
- package/dist/chunk-N6IHQUOX.js +1 -0
- package/dist/chunk-N6IHQUOX.js.map +1 -0
- package/dist/{chunk-COKTX2G5.js → chunk-SGU7SPLK.js} +29 -12
- package/dist/chunk-SGU7SPLK.js.map +1 -0
- package/dist/core/index.cjs +230 -14
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +56 -4
- package/dist/core/index.d.ts +56 -4
- package/dist/core/index.js +24 -4
- package/dist/{function-2bJW1Zp_.d.ts → function-DVH4Eusr.d.ts} +1 -1
- package/dist/{function-r2sh-jOO.d.cts → function-DYt4iKsn.d.cts} +1 -1
- package/dist/generate/index.cjs +830 -494
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +11 -5
- package/dist/generate/index.d.ts +11 -5
- package/dist/generate/index.js +9 -9
- package/dist/{index-BOBXBNwP.d.cts → index-BNlGP86A.d.cts} +1 -1
- package/dist/{index-CP3wAfEH.d.ts → index-Bg5dYMmZ.d.ts} +1 -1
- package/dist/{index-CM5W4rcm.d.ts → index-Bt4tOUD9.d.ts} +1 -1
- package/dist/index-CnZCo29u.d.cts +108 -0
- package/dist/{index-D_ZTUNKp.d.cts → index-CpOsP1y9.d.cts} +2 -2
- package/dist/{index-DZ1M21do.d.cts → index-CxqBQtH1.d.cts} +2 -1
- package/dist/{index-BdeQ1r_p.d.cts → index-Dikst44v.d.cts} +1 -1
- package/dist/{index-BMPxwjwV.d.ts → index-DxvARIP2.d.ts} +2 -2
- package/dist/{index-BreoJRw0.d.ts → index-Jc0Ryi79.d.ts} +2 -1
- package/dist/index-JnF4v9bx.d.ts +108 -0
- package/dist/index.cjs +818 -283
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -8
- package/dist/index.d.ts +9 -8
- package/dist/index.js +34 -13
- package/dist/model/index.cjs +170 -31
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +6 -5
- package/dist/model/index.d.ts +6 -5
- package/dist/model/index.js +6 -6
- package/dist/{styled-D7BAx3S3.d.ts → styled-DZ7GjShb.d.ts} +1 -1
- package/dist/{styled-BjXtOFNk.d.cts → styled-ZTbdaVFM.d.cts} +1 -1
- package/dist/theory/index.cjs +180 -34
- package/dist/theory/index.cjs.map +1 -1
- package/dist/theory/index.d.cts +6 -6
- package/dist/theory/index.d.ts +6 -6
- package/dist/theory/index.js +6 -5
- package/package.json +50 -19
- package/src/analyze/arrange/internal.ts +3 -1
- package/src/analyze/arrange/tension.ts +15 -3
- package/src/analyze/arrange/tracks.ts +36 -3
- package/src/analyze/detect/index.ts +46 -9
- package/src/analyze/functional/roman.ts +14 -5
- package/src/analyze/index.ts +1 -1
- package/src/analyze/timeline/index.ts +43 -7
- package/src/analyze/voice/index.ts +11 -2
- package/src/core/event-index/index.ts +104 -0
- package/src/core/index.ts +14 -0
- package/src/core/meter/index.ts +24 -4
- package/src/core/random/index.ts +26 -3
- package/src/core/tuning/index.ts +36 -6
- package/src/core/validation/index.ts +119 -0
- package/src/generate/bass/index.ts +23 -0
- package/src/generate/countermelody/index.ts +167 -76
- package/src/generate/drums/euclid.ts +10 -3
- package/src/generate/drums/index.ts +56 -24
- package/src/generate/drums/percussion.ts +1 -1
- package/src/generate/groove/index.ts +25 -0
- package/src/generate/harmonize/index.ts +55 -44
- package/src/generate/index.ts +0 -1
- package/src/generate/motif/index.ts +45 -21
- package/src/generate/progression/index.ts +8 -16
- package/src/generate/reharmony/index.ts +1 -8
- package/src/generate/rhythm/index.ts +11 -1
- package/src/index.ts +13 -247
- package/src/model/chord.ts +29 -10
- package/src/theory/chord/index.ts +4 -2
- package/src/theory/harmony/index.ts +6 -21
- package/src/theory/index.ts +6 -1
- package/src/theory/safety/index.ts +71 -2
- package/src/theory/spelling/index.ts +70 -3
- package/src/theory/voicing/satb.ts +25 -4
- package/src/theory/voicing/styled.ts +26 -3
- package/dist/chunk-5HTNMQW4.js.map +0 -1
- package/dist/chunk-7Y7WF64D.js.map +0 -1
- package/dist/chunk-A4532ZFR.js.map +0 -1
- package/dist/chunk-BHUT2S5G.js +0 -22
- package/dist/chunk-BHUT2S5G.js.map +0 -1
- package/dist/chunk-CCBLVAPK.js +0 -42
- package/dist/chunk-CCBLVAPK.js.map +0 -1
- package/dist/chunk-COKTX2G5.js.map +0 -1
- package/dist/chunk-GQKN5KDT.js.map +0 -1
- package/dist/chunk-LN7ZZMD2.js.map +0 -1
- package/dist/chunk-MSWFZF7J.js.map +0 -1
- package/dist/chunk-R3S2LUBH.js.map +0 -1
- package/dist/chunk-SDM3VJJ5.js +0 -41
- package/dist/chunk-SDM3VJJ5.js.map +0 -1
- package/dist/chunk-XTC3X6MS.js.map +0 -1
- package/dist/chunk-ZXW5YIWK.js +0 -55
- package/dist/chunk-ZXW5YIWK.js.map +0 -1
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createRng
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-62QZKCJQ.js";
|
|
4
4
|
import {
|
|
5
5
|
beatsPerBar,
|
|
6
|
+
createNoteEventIndex,
|
|
6
7
|
formatTimeSignature,
|
|
7
8
|
isStrongBeat,
|
|
8
9
|
metricWeight
|
|
9
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-I5R7ZBJS.js";
|
|
11
|
+
import {
|
|
12
|
+
isDiatonic,
|
|
13
|
+
parallelKey
|
|
14
|
+
} from "./chunk-CHMXELDS.js";
|
|
10
15
|
import {
|
|
11
16
|
roleOf
|
|
12
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-2ZWMSJH6.js";
|
|
13
18
|
import {
|
|
14
|
-
enumerateSafePitches
|
|
15
|
-
|
|
19
|
+
enumerateSafePitches,
|
|
20
|
+
evaluateSafety
|
|
21
|
+
} from "./chunk-EH6EM37U.js";
|
|
16
22
|
import {
|
|
17
23
|
NATURAL_MINOR_MASK,
|
|
18
24
|
chordPitchClasses,
|
|
@@ -26,7 +32,16 @@ import {
|
|
|
26
32
|
minorKey,
|
|
27
33
|
nearestScaleTone,
|
|
28
34
|
scaleTonesInDegreeOrder
|
|
29
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-MIWO425M.js";
|
|
36
|
+
import {
|
|
37
|
+
assertFiniteNumber,
|
|
38
|
+
assertGenerationBudget,
|
|
39
|
+
assertInteger,
|
|
40
|
+
assertNoteEvents,
|
|
41
|
+
assertPositiveInt,
|
|
42
|
+
assertRange,
|
|
43
|
+
assertTimeSignature
|
|
44
|
+
} from "./chunk-FBQK3ZQN.js";
|
|
30
45
|
|
|
31
46
|
// src/generate/bass/index.ts
|
|
32
47
|
var DEFAULT_TS = { numerator: 4, denominator: 4 };
|
|
@@ -157,13 +172,30 @@ function buildWalking(ctx, seg, next) {
|
|
|
157
172
|
});
|
|
158
173
|
}
|
|
159
174
|
function generateBassLine(opts) {
|
|
175
|
+
assertGenerationBudget(opts.segments.length, "bass segments");
|
|
176
|
+
for (let index = 0; index < opts.segments.length; index += 1) {
|
|
177
|
+
const segment = opts.segments[index];
|
|
178
|
+
if (!segment) continue;
|
|
179
|
+
assertRange(segment.startBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].startBeat`);
|
|
180
|
+
assertRange(segment.endBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].endBeat`);
|
|
181
|
+
if (segment.endBeat <= segment.startBeat) {
|
|
182
|
+
throw new RangeError(`segments[${index}] must have a positive duration`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
160
185
|
const segments = [...opts.segments].sort((a, b) => a.startBeat - b.startBeat);
|
|
161
186
|
if (segments.length === 0) {
|
|
162
187
|
return [];
|
|
163
188
|
}
|
|
164
189
|
const ts = opts.ts ?? DEFAULT_TS;
|
|
190
|
+
assertTimeSignature(ts);
|
|
165
191
|
const style = opts.style ?? DEFAULT_STYLE;
|
|
166
192
|
const octave = opts.octave ?? DEFAULT_OCTAVE;
|
|
193
|
+
assertInteger(octave, "bass octave", -1, 9);
|
|
194
|
+
const estimatedNotes = segments.reduce(
|
|
195
|
+
(count, segment) => count + Math.max(1, Math.ceil(segment.endBeat - segment.startBeat)),
|
|
196
|
+
0
|
|
197
|
+
);
|
|
198
|
+
assertGenerationBudget(estimatedNotes, "bass notes");
|
|
167
199
|
const low = octave * 12 + 12;
|
|
168
200
|
const ctx = {
|
|
169
201
|
ts,
|
|
@@ -235,21 +267,26 @@ function isWholeBeat(beat) {
|
|
|
235
267
|
return Math.abs(beat - Math.round(beat)) < EPS2;
|
|
236
268
|
}
|
|
237
269
|
function melodyNoteAt(melody, beat) {
|
|
238
|
-
|
|
239
|
-
for (const note of melody) {
|
|
240
|
-
if (note.startBeat - EPS2 <= beat && beat < note.startBeat + note.durationBeat - EPS2) {
|
|
241
|
-
if (!found || note.startBeat > found.startBeat) {
|
|
242
|
-
found = note;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
return found;
|
|
270
|
+
return melody.at(beat)?.note;
|
|
247
271
|
}
|
|
248
272
|
function melodyAttacksAt(melody, beat) {
|
|
249
|
-
return melody.
|
|
273
|
+
return melody.attacksAt(beat);
|
|
274
|
+
}
|
|
275
|
+
function melodySnapshotAt(melody, beat) {
|
|
276
|
+
const current = melodyNoteAt(melody, beat);
|
|
277
|
+
if (current === void 0) {
|
|
278
|
+
return void 0;
|
|
279
|
+
}
|
|
280
|
+
const attacked = Math.abs(current.startBeat - beat) < EPS2;
|
|
281
|
+
const previous = attacked ? melodyNoteAt(melody, beat - EPS2 * 2) : current;
|
|
282
|
+
const snapshot = { pitch: current.pitch };
|
|
283
|
+
if (previous !== void 0) {
|
|
284
|
+
snapshot.prevPitch = previous.pitch;
|
|
285
|
+
}
|
|
286
|
+
return snapshot;
|
|
250
287
|
}
|
|
251
288
|
function followOnsets(melody) {
|
|
252
|
-
return [...new Set(melody.map((note) => note.startBeat))]
|
|
289
|
+
return [...new Set(melody.notes.map(({ note }) => note.startBeat))];
|
|
253
290
|
}
|
|
254
291
|
function complementOnsets(melody, ts, spanStart, spanEnd, rng) {
|
|
255
292
|
const onsets = [];
|
|
@@ -312,54 +349,102 @@ function scoreCandidate(pitch, melPitch, melPrev, prevPitch, chord, center) {
|
|
|
312
349
|
score -= Math.abs(pitch - center) * 0.02;
|
|
313
350
|
return score;
|
|
314
351
|
}
|
|
315
|
-
function
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
352
|
+
function heldNoteBoundaries(melody, startBeat, endBeat, chordChangeBeats) {
|
|
353
|
+
const boundaries = /* @__PURE__ */ new Set([startBeat]);
|
|
354
|
+
for (const onset of melody.onsetsBetween(startBeat, endBeat)) {
|
|
355
|
+
boundaries.add(onset);
|
|
356
|
+
}
|
|
357
|
+
for (const beat of chordChangeBeats) {
|
|
358
|
+
if (beat > startBeat + EPS2 && beat < endBeat - EPS2) {
|
|
359
|
+
boundaries.add(beat);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const firstGrid = Math.floor(startBeat / GRID_STEP + 1) * GRID_STEP;
|
|
363
|
+
for (let beat = firstGrid; beat < endBeat - EPS2; beat += GRID_STEP) {
|
|
364
|
+
boundaries.add(beat);
|
|
365
|
+
}
|
|
366
|
+
return [...boundaries].sort((a, b) => a - b);
|
|
367
|
+
}
|
|
368
|
+
function heldPitchSafety(pitch, startBeat, endBeat, prevPitch, melody, opts, profile, register, low, high, ts) {
|
|
369
|
+
let worst = 0 /* Safe */;
|
|
370
|
+
const boundaries = heldNoteBoundaries(melody, startBeat, endBeat, opts.chordChangeBeats ?? []);
|
|
371
|
+
for (const boundary of boundaries) {
|
|
372
|
+
const melodyVoice = melodySnapshotAt(melody, boundary);
|
|
373
|
+
if (melodyVoice !== void 0 && (register === "below" && pitch >= melodyVoice.pitch || register === "above" && pitch <= melodyVoice.pitch)) {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
const atCounterOnset = Math.abs(boundary - startBeat) < EPS2;
|
|
377
|
+
const result = evaluateSafety({
|
|
378
|
+
profile,
|
|
379
|
+
candidatePitch: pitch,
|
|
380
|
+
prevPitch: atCounterOnset ? prevPitch : pitch,
|
|
381
|
+
chord: opts.chordAt(boundary),
|
|
382
|
+
key: opts.key,
|
|
383
|
+
otherVoices: melodyVoice ? [melodyVoice] : [],
|
|
384
|
+
strongBeat: isStrongBeat(boundary, ts),
|
|
385
|
+
vocalLow: low,
|
|
386
|
+
vocalHigh: high
|
|
387
|
+
});
|
|
388
|
+
worst = Math.max(worst, result.safety);
|
|
389
|
+
if (worst === 2 /* Dissonant */) {
|
|
390
|
+
return worst;
|
|
332
391
|
}
|
|
333
392
|
}
|
|
334
|
-
return
|
|
393
|
+
return worst;
|
|
335
394
|
}
|
|
336
395
|
function generateCounterMelody(opts) {
|
|
337
|
-
const melody =
|
|
338
|
-
if (melody.length === 0) {
|
|
396
|
+
const melody = createNoteEventIndex(opts.melody);
|
|
397
|
+
if (melody.notes.length === 0) {
|
|
339
398
|
return [];
|
|
340
399
|
}
|
|
341
400
|
const ts = opts.ts ?? DEFAULT_TS2;
|
|
401
|
+
assertTimeSignature(ts);
|
|
342
402
|
const register = opts.register ?? "below";
|
|
343
403
|
const rhythm = opts.rhythm ?? "complement";
|
|
344
404
|
const profile = opts.profile ?? "pop";
|
|
345
405
|
const rng = createRng(opts.seed ?? 0);
|
|
346
|
-
const spanStart = melody[0]?.startBeat ?? 0;
|
|
347
|
-
const spanEnd = melody.reduce((end, n) => Math.max(end, n.
|
|
348
|
-
|
|
406
|
+
const spanStart = melody.notes[0]?.note.startBeat ?? 0;
|
|
407
|
+
const spanEnd = melody.notes.reduce((end, n) => Math.max(end, n.endBeat), spanStart);
|
|
408
|
+
assertGenerationBudget(Math.ceil((spanEnd - spanStart) / GRID_STEP), "countermelody grid");
|
|
409
|
+
const meanPitch = melody.notes.reduce((sum, n) => sum + n.note.pitch, 0) / melody.notes.length;
|
|
349
410
|
const defaultCenter = register === "below" ? meanPitch - REGISTER_OFFSET : meanPitch + REGISTER_OFFSET;
|
|
350
411
|
const low = opts.pitchLow ?? Math.round(defaultCenter) - DEFAULT_HALF_RANGE;
|
|
351
412
|
const high = opts.pitchHigh ?? Math.round(defaultCenter) + DEFAULT_HALF_RANGE;
|
|
413
|
+
assertInteger(low, "countermelody pitchLow");
|
|
414
|
+
assertInteger(high, "countermelody pitchHigh");
|
|
415
|
+
if (low > high) {
|
|
416
|
+
throw new RangeError(
|
|
417
|
+
`countermelody pitchLow must not exceed pitchHigh; received ${low} > ${high}`
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
assertGenerationBudget(high - low + 1, "countermelody pitch candidates");
|
|
421
|
+
assertGenerationBudget(opts.chordChangeBeats?.length ?? 0, "chord change beats");
|
|
422
|
+
for (let index = 0; index < (opts.chordChangeBeats?.length ?? 0); index += 1) {
|
|
423
|
+
assertRange(
|
|
424
|
+
opts.chordChangeBeats?.[index] ?? Number.NaN,
|
|
425
|
+
0,
|
|
426
|
+
Number.MAX_SAFE_INTEGER,
|
|
427
|
+
`chordChangeBeats[${index}]`
|
|
428
|
+
);
|
|
429
|
+
}
|
|
352
430
|
const center = (low + high) / 2;
|
|
353
431
|
const onsets = rhythm === "follow" ? followOnsets(melody) : complementOnsets(melody, ts, spanStart, spanEnd, rng);
|
|
432
|
+
assertGenerationBudget(onsets.length * (high - low + 1), "countermelody search");
|
|
354
433
|
const out = [];
|
|
355
434
|
let prevPitch;
|
|
356
|
-
let
|
|
357
|
-
|
|
435
|
+
for (let onsetIndex = 0; onsetIndex < onsets.length; onsetIndex += 1) {
|
|
436
|
+
const beat = onsets[onsetIndex];
|
|
437
|
+
if (beat === void 0) {
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
const nextOnset = onsets[onsetIndex + 1];
|
|
441
|
+
const endBeat = nextOnset ?? Math.max(spanEnd, beat + GRID_STEP);
|
|
358
442
|
const melNote = melodyNoteAt(melody, beat);
|
|
359
443
|
const melPitch = melNote?.pitch;
|
|
360
|
-
const
|
|
444
|
+
const melodyVoice = melodySnapshotAt(melody, beat);
|
|
445
|
+
const melPrev = melodyVoice?.prevPitch;
|
|
361
446
|
const chord = opts.chordAt(beat);
|
|
362
|
-
const otherVoices =
|
|
447
|
+
const otherVoices = melodyVoice !== void 0 ? [melodyVoice] : [];
|
|
363
448
|
const candidates = enumerateSafePitches(
|
|
364
449
|
{
|
|
365
450
|
profile,
|
|
@@ -375,15 +460,26 @@ function generateCounterMelody(opts) {
|
|
|
375
460
|
high
|
|
376
461
|
);
|
|
377
462
|
let bestPitch;
|
|
463
|
+
let bestWorstSafety = 2 /* Dissonant */;
|
|
378
464
|
let bestScore = Number.NEGATIVE_INFINITY;
|
|
379
465
|
for (const candidate of candidates) {
|
|
466
|
+
const worstSafety = heldPitchSafety(
|
|
467
|
+
candidate,
|
|
468
|
+
beat,
|
|
469
|
+
endBeat,
|
|
470
|
+
prevPitch,
|
|
471
|
+
melody,
|
|
472
|
+
opts,
|
|
473
|
+
profile,
|
|
474
|
+
register,
|
|
475
|
+
low,
|
|
476
|
+
high,
|
|
477
|
+
ts
|
|
478
|
+
);
|
|
479
|
+
if (worstSafety === null || worstSafety === 2 /* Dissonant */) {
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
380
482
|
if (melPitch !== void 0) {
|
|
381
|
-
if (register === "below" && candidate >= melPitch) {
|
|
382
|
-
continue;
|
|
383
|
-
}
|
|
384
|
-
if (register === "above" && candidate <= melPitch) {
|
|
385
|
-
continue;
|
|
386
|
-
}
|
|
387
483
|
if (prevPitch !== void 0 && melPrev !== void 0 && // Parallel octaves are the perfect-class-zero case of this predicate,
|
|
388
484
|
// so a single check covers both parallel fifths and octaves.
|
|
389
485
|
createsParallelPerfect(prevPitch, candidate, melPrev, melPitch)) {
|
|
@@ -392,28 +488,23 @@ function generateCounterMelody(opts) {
|
|
|
392
488
|
}
|
|
393
489
|
const score = scoreCandidate(candidate, melPitch, melPrev, prevPitch, chord, center);
|
|
394
490
|
const tie = Math.abs(score - bestScore) <= EPS2 && candidate < (bestPitch ?? Number.POSITIVE_INFINITY);
|
|
395
|
-
if (score > bestScore + EPS2 || tie) {
|
|
491
|
+
if (worstSafety < bestWorstSafety || worstSafety === bestWorstSafety && (score > bestScore + EPS2 || tie)) {
|
|
492
|
+
bestWorstSafety = worstSafety;
|
|
396
493
|
bestScore = score;
|
|
397
494
|
bestPitch = candidate;
|
|
398
495
|
}
|
|
399
496
|
}
|
|
400
|
-
|
|
497
|
+
if (bestPitch === void 0) {
|
|
498
|
+
prevPitch = void 0;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
const pitch = bestPitch;
|
|
401
502
|
const velocity = Math.max(
|
|
402
503
|
1,
|
|
403
504
|
Math.min(127, Math.round((melNote?.velocity ?? DEFAULT_MELODY_VELOCITY) - VELOCITY_DROP))
|
|
404
505
|
);
|
|
405
|
-
out.push({ pitch, startBeat: beat, durationBeat:
|
|
506
|
+
out.push({ pitch, startBeat: beat, durationBeat: endBeat - beat, velocity });
|
|
406
507
|
prevPitch = pitch;
|
|
407
|
-
prevOnset = beat;
|
|
408
|
-
}
|
|
409
|
-
for (let i = 0; i < out.length; i += 1) {
|
|
410
|
-
const note = out[i];
|
|
411
|
-
if (!note) {
|
|
412
|
-
continue;
|
|
413
|
-
}
|
|
414
|
-
const next = out[i + 1];
|
|
415
|
-
const end = next ? next.startBeat : Math.max(spanEnd, note.startBeat + GRID_STEP);
|
|
416
|
-
note.durationBeat = end - note.startBeat;
|
|
417
508
|
}
|
|
418
509
|
return out;
|
|
419
510
|
}
|
|
@@ -1133,9 +1224,10 @@ function generateHiHatForBeat(ctx, sec) {
|
|
|
1133
1224
|
|
|
1134
1225
|
// src/generate/drums/euclid.ts
|
|
1135
1226
|
function euclideanRhythm(pulses, steps, rotation = 0) {
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1227
|
+
assertPositiveInt(steps, "euclidean steps");
|
|
1228
|
+
assertInteger(pulses, "euclidean pulses");
|
|
1229
|
+
assertInteger(rotation, "euclidean rotation");
|
|
1230
|
+
assertGenerationBudget(steps, "euclidean pattern steps");
|
|
1139
1231
|
const p = Math.max(0, Math.min(pulses, steps));
|
|
1140
1232
|
const base = bjorklund(p, steps);
|
|
1141
1233
|
const shift = (rotation % steps + steps) % steps;
|
|
@@ -1144,18 +1236,6 @@ function euclideanRhythm(pulses, steps, rotation = 0) {
|
|
|
1144
1236
|
}
|
|
1145
1237
|
return base.map((_, i) => base[(i - shift + steps) % steps] ?? false);
|
|
1146
1238
|
}
|
|
1147
|
-
function hasHit(mask, step) {
|
|
1148
|
-
return (mask >> step & 1) === 1;
|
|
1149
|
-
}
|
|
1150
|
-
function patternToMask(pattern) {
|
|
1151
|
-
let mask = 0;
|
|
1152
|
-
for (let i = 0; i < pattern.length; i += 1) {
|
|
1153
|
-
if (pattern[i]) {
|
|
1154
|
-
mask |= 1 << i;
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
return mask;
|
|
1158
|
-
}
|
|
1159
1239
|
function bjorklund(pulses, steps) {
|
|
1160
1240
|
if (pulses === 0) {
|
|
1161
1241
|
return Array.from({ length: steps }, () => false);
|
|
@@ -1487,19 +1567,6 @@ function isInPreChorusLift(section, bar, sectionBars, nextIsChorus) {
|
|
|
1487
1567
|
}
|
|
1488
1568
|
return bar >= sectionBars - 2;
|
|
1489
1569
|
}
|
|
1490
|
-
function euclideanToKickPattern(mask) {
|
|
1491
|
-
const slot = (step) => hasHit(mask, step) || hasHit(mask, (step + 15) % 16);
|
|
1492
|
-
return {
|
|
1493
|
-
beat1: slot(0),
|
|
1494
|
-
beat1and: slot(2),
|
|
1495
|
-
beat2: slot(4),
|
|
1496
|
-
beat2and: slot(6),
|
|
1497
|
-
beat3: slot(8),
|
|
1498
|
-
beat3and: slot(10),
|
|
1499
|
-
beat4: slot(12),
|
|
1500
|
-
beat4and: slot(14)
|
|
1501
|
-
};
|
|
1502
|
-
}
|
|
1503
1570
|
function getKickPattern(section, style, bar, rng) {
|
|
1504
1571
|
const p = { ...EMPTY };
|
|
1505
1572
|
if (section === "outro") {
|
|
@@ -1621,7 +1688,7 @@ function getPercussionConfig(mood, section) {
|
|
|
1621
1688
|
};
|
|
1622
1689
|
}
|
|
1623
1690
|
function generateAuxPercussionForBar(track, barStart, config, role, densityMult, rng, bpm) {
|
|
1624
|
-
if (role === "minimal"
|
|
1691
|
+
if (role === "minimal") {
|
|
1625
1692
|
return;
|
|
1626
1693
|
}
|
|
1627
1694
|
if (config.tambourine) {
|
|
@@ -1670,6 +1737,16 @@ function generateAuxPercussionForBar(track, barStart, config, role, densityMult,
|
|
|
1670
1737
|
|
|
1671
1738
|
// src/generate/drums/index.ts
|
|
1672
1739
|
function generateDrums(opts) {
|
|
1740
|
+
assertPositiveInt(opts.bars, "drum bars");
|
|
1741
|
+
assertRange(opts.bpm, Number.MIN_VALUE, 1e3, "drum bpm");
|
|
1742
|
+
assertRange(opts.density, 0, 1, "drum density");
|
|
1743
|
+
assertGenerationBudget(opts.bars * 128, "drum hits");
|
|
1744
|
+
if (opts.euclideanKick) {
|
|
1745
|
+
const steps = opts.euclideanKick.steps ?? 16;
|
|
1746
|
+
assertInteger(steps, "euclidean steps", 1, 16);
|
|
1747
|
+
assertInteger(opts.euclideanKick.pulses, "euclidean pulses");
|
|
1748
|
+
assertInteger(opts.euclideanKick.rotation ?? 0, "euclidean rotation");
|
|
1749
|
+
}
|
|
1673
1750
|
const track = new HitList();
|
|
1674
1751
|
const rng = createRng(opts.seed ?? 0);
|
|
1675
1752
|
const mapping = mapStyle(opts.style);
|
|
@@ -1706,15 +1783,8 @@ function generateDrums(opts) {
|
|
|
1706
1783
|
const percMood = percMoodCategory(style);
|
|
1707
1784
|
const nextSection = opts.nextSection ? mapSection(opts.nextSection) : section;
|
|
1708
1785
|
const nextEnergy = sectionEnergy(nextSection);
|
|
1709
|
-
const
|
|
1710
|
-
|
|
1711
|
-
euclideanRhythm(
|
|
1712
|
-
opts.euclideanKick.pulses,
|
|
1713
|
-
opts.euclideanKick.steps ?? 16,
|
|
1714
|
-
opts.euclideanKick.rotation ?? 0
|
|
1715
|
-
)
|
|
1716
|
-
)
|
|
1717
|
-
) : void 0;
|
|
1786
|
+
const euclidSteps = opts.euclideanKick?.steps ?? 16;
|
|
1787
|
+
const euclidKick = opts.euclideanKick ? euclideanRhythm(opts.euclideanKick.pulses, euclidSteps, opts.euclideanKick.rotation ?? 0) : void 0;
|
|
1718
1788
|
const reuseSectionKick = (section === "b" || section === "chorus") && style !== "sparse";
|
|
1719
1789
|
let sectionKick;
|
|
1720
1790
|
for (let bar = 0; bar < opts.bars; bar += 1) {
|
|
@@ -1730,12 +1800,10 @@ function generateDrums(opts) {
|
|
|
1730
1800
|
barHasOpenHh = !track.hasCrashNear(barStart + openHhBeatIndex);
|
|
1731
1801
|
}
|
|
1732
1802
|
let kick;
|
|
1733
|
-
if (euclidKick) {
|
|
1734
|
-
kick = euclidKick;
|
|
1735
|
-
} else if (reuseSectionKick) {
|
|
1803
|
+
if (!euclidKick && reuseSectionKick) {
|
|
1736
1804
|
sectionKick ??= getKickPattern(section, style, 0, rng);
|
|
1737
1805
|
kick = sectionKick;
|
|
1738
|
-
} else {
|
|
1806
|
+
} else if (!euclidKick) {
|
|
1739
1807
|
kick = getKickPattern(section, style, bar, rng);
|
|
1740
1808
|
}
|
|
1741
1809
|
const inLift = isInPreChorusLift(section, bar, opts.bars, section === "b");
|
|
@@ -1776,7 +1844,23 @@ function generateDrums(opts) {
|
|
|
1776
1844
|
if (inLift) {
|
|
1777
1845
|
generatePreChorusBuildup(ctx, sec, isLastBar);
|
|
1778
1846
|
}
|
|
1779
|
-
|
|
1847
|
+
if (euclidKick && !inLift) {
|
|
1848
|
+
const stepLength = 4 / euclidSteps;
|
|
1849
|
+
for (let step = 0; step < euclidKick.length; step += 1) {
|
|
1850
|
+
const onset = step * stepLength;
|
|
1851
|
+
if (euclidKick[step] && Math.floor(onset) === beat) {
|
|
1852
|
+
const wholeBeat = Math.abs(onset - Math.round(onset)) < 1e-9;
|
|
1853
|
+
track.add(
|
|
1854
|
+
GM.BD,
|
|
1855
|
+
barStart + onset,
|
|
1856
|
+
Math.min(0.5, stepLength),
|
|
1857
|
+
velocity * (wholeBeat ? 1 : 0.85)
|
|
1858
|
+
);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
} else if (kick) {
|
|
1862
|
+
generateKickForBeat(ctx, sec, kick);
|
|
1863
|
+
}
|
|
1780
1864
|
generateSnareForBeat(ctx, sec, section === "intro" && bar === 0);
|
|
1781
1865
|
if (sec.useGhostNotes && !inLift) {
|
|
1782
1866
|
generateGhostNotesForBeat(ctx, sec);
|
|
@@ -1793,10 +1877,19 @@ function generateDrums(opts) {
|
|
|
1793
1877
|
}
|
|
1794
1878
|
}
|
|
1795
1879
|
}
|
|
1880
|
+
let percussionConfig = getPercussionConfig(percMood, section);
|
|
1881
|
+
if (role === "fxOnly" && !percussionConfig.tambourine && !percussionConfig.shaker && !percussionConfig.handclap) {
|
|
1882
|
+
percussionConfig = {
|
|
1883
|
+
tambourine: false,
|
|
1884
|
+
shaker: true,
|
|
1885
|
+
handclap: false,
|
|
1886
|
+
shaker16th: false
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1796
1889
|
generateAuxPercussionForBar(
|
|
1797
1890
|
track,
|
|
1798
1891
|
barStart,
|
|
1799
|
-
|
|
1892
|
+
percussionConfig,
|
|
1800
1893
|
role,
|
|
1801
1894
|
densityMult,
|
|
1802
1895
|
rng,
|
|
@@ -1817,10 +1910,16 @@ var MIN_VELOCITY = 1;
|
|
|
1817
1910
|
var MAX_VELOCITY = 127;
|
|
1818
1911
|
function humanize(events, opts = {}) {
|
|
1819
1912
|
const ts = opts.ts ?? DEFAULT_TS3;
|
|
1913
|
+
assertTimeSignature(ts);
|
|
1914
|
+
assertNoteEvents(events, "humanize events");
|
|
1820
1915
|
const timing = opts.timing ?? DEFAULT_TIMING;
|
|
1821
1916
|
const velocityJitter = opts.velocity ?? DEFAULT_VELOCITY_JITTER;
|
|
1822
1917
|
const accent = opts.accent ?? DEFAULT_ACCENT;
|
|
1823
1918
|
const baseVelocity = opts.baseVelocity ?? DEFAULT_BASE_VELOCITY;
|
|
1919
|
+
assertRange(timing, 0, Number.MAX_SAFE_INTEGER, "humanize timing");
|
|
1920
|
+
assertRange(velocityJitter, 0, 127, "humanize velocity jitter");
|
|
1921
|
+
assertRange(accent, 0, 127, "humanize accent");
|
|
1922
|
+
assertRange(baseVelocity, 0, 127, "humanize base velocity");
|
|
1824
1923
|
const rng = createRng(opts.seed ?? 0);
|
|
1825
1924
|
return events.map((event) => {
|
|
1826
1925
|
const timingOffset = rng.float(-timing, timing);
|
|
@@ -1853,8 +1952,12 @@ function quantizeToGrid(beat, barBeats, subdivision, slotsPerBar) {
|
|
|
1853
1952
|
};
|
|
1854
1953
|
}
|
|
1855
1954
|
function extractGrooveTemplate(events, ts, subdivision) {
|
|
1955
|
+
assertTimeSignature(ts);
|
|
1956
|
+
assertNoteEvents(events, "groove source events");
|
|
1957
|
+
assertPositiveInt(subdivision, "groove subdivision");
|
|
1856
1958
|
const barBeats = beatsPerBar(ts);
|
|
1857
1959
|
const slotsPerBar = Math.round(barBeats * subdivision);
|
|
1960
|
+
assertGenerationBudget(slotsPerBar, "groove template slots");
|
|
1858
1961
|
const offsetSums = new Array(slotsPerBar).fill(0);
|
|
1859
1962
|
const velocitySums = new Array(slotsPerBar).fill(0);
|
|
1860
1963
|
const counts = new Array(slotsPerBar).fill(0);
|
|
@@ -1889,6 +1992,14 @@ function extractGrooveTemplate(events, ts, subdivision) {
|
|
|
1889
1992
|
return { subdivision, slotsPerBar, slots, ts };
|
|
1890
1993
|
}
|
|
1891
1994
|
function applyGrooveTemplate(events, template, ts) {
|
|
1995
|
+
assertTimeSignature(ts);
|
|
1996
|
+
assertNoteEvents(events, "groove target events");
|
|
1997
|
+
assertPositiveInt(template.subdivision, "template subdivision");
|
|
1998
|
+
assertPositiveInt(template.slotsPerBar, "template slotsPerBar");
|
|
1999
|
+
assertGenerationBudget(template.slots.length, "template slots");
|
|
2000
|
+
if (template.slots.length !== template.slotsPerBar) {
|
|
2001
|
+
throw new RangeError("template slots length must equal slotsPerBar");
|
|
2002
|
+
}
|
|
1892
2003
|
if (template.ts && (template.ts.numerator !== ts.numerator || template.ts.denominator !== ts.denominator)) {
|
|
1893
2004
|
throw new Error(
|
|
1894
2005
|
`Groove template meter ${formatTimeSignature(template.ts)} does not match apply-time meter ${formatTimeSignature(ts)}`
|
|
@@ -1924,16 +2035,6 @@ var TIE_BREAK_JITTER = 1e-6;
|
|
|
1924
2035
|
function pitchClass2(pitch) {
|
|
1925
2036
|
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
1926
2037
|
}
|
|
1927
|
-
function mulberry32(seed) {
|
|
1928
|
-
let state = seed >>> 0;
|
|
1929
|
-
return () => {
|
|
1930
|
-
state = state + 1831565813 >>> 0;
|
|
1931
|
-
let t = state;
|
|
1932
|
-
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
1933
|
-
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
1934
|
-
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
1935
|
-
};
|
|
1936
|
-
}
|
|
1937
2038
|
function inferKey(melody) {
|
|
1938
2039
|
let best = majorKey(0);
|
|
1939
2040
|
let bestScore = Number.NEGATIVE_INFINITY;
|
|
@@ -1972,7 +2073,6 @@ function buildCandidates(key, reharmonize) {
|
|
|
1972
2073
|
secondaryDominant: false,
|
|
1973
2074
|
base: 0
|
|
1974
2075
|
}));
|
|
1975
|
-
const tonic = tones[0] ?? 0;
|
|
1976
2076
|
if (reharmonize !== "diatonic") {
|
|
1977
2077
|
for (const target of [1, 3, 4, 5]) {
|
|
1978
2078
|
const targetRoot = tones[target] ?? 0;
|
|
@@ -1986,26 +2086,29 @@ function buildCandidates(key, reharmonize) {
|
|
|
1986
2086
|
}
|
|
1987
2087
|
}
|
|
1988
2088
|
if (reharmonize === "borrowed") {
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
rootPc: (tonic + 5) % 12,
|
|
2003
|
-
quality: "min",
|
|
2004
|
-
secondaryDominant: false,
|
|
2005
|
-
base: 2
|
|
2006
|
-
});
|
|
2089
|
+
const parallel = parallelKey(key);
|
|
2090
|
+
const parallelTones = scaleTonesInDegreeOrder(parallel);
|
|
2091
|
+
for (let degree = 0; degree < parallelTones.length; degree += 1) {
|
|
2092
|
+
const chord = diatonicTriad(degree, parallel);
|
|
2093
|
+
if (!isDiatonic(chord, key)) {
|
|
2094
|
+
candidates.push({
|
|
2095
|
+
rootPc: chord.rootPc,
|
|
2096
|
+
quality: chord.quality,
|
|
2097
|
+
secondaryDominant: false,
|
|
2098
|
+
base: 1.25
|
|
2099
|
+
});
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2007
2102
|
}
|
|
2008
|
-
|
|
2103
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2104
|
+
return candidates.filter((candidate) => {
|
|
2105
|
+
const id = `${candidate.rootPc}:${candidate.quality}`;
|
|
2106
|
+
if (seen.has(id)) {
|
|
2107
|
+
return false;
|
|
2108
|
+
}
|
|
2109
|
+
seen.add(id);
|
|
2110
|
+
return true;
|
|
2111
|
+
});
|
|
2009
2112
|
}
|
|
2010
2113
|
function emissionCost(seg, cand, melody, key) {
|
|
2011
2114
|
const pcs = chordPitchClasses(makeChord(cand.rootPc, cand.quality));
|
|
@@ -2109,20 +2212,37 @@ function harmonizeOnce(melody, key, candidates, segments, jitter) {
|
|
|
2109
2212
|
return { cost: bestCost, path };
|
|
2110
2213
|
}
|
|
2111
2214
|
function harmonizeMelody(opts) {
|
|
2215
|
+
const noteIndex = createNoteEventIndex(opts.melody);
|
|
2112
2216
|
const key = opts.key === "infer" ? inferKey(opts.melody) : opts.key;
|
|
2113
2217
|
const candidates = buildCandidates(key, opts.reharmonize);
|
|
2114
2218
|
const candAt = (i) => candidates[i] ?? FALLBACK;
|
|
2115
|
-
const rng =
|
|
2116
|
-
const jitter = candidates.map(() => rng() * TIE_BREAK_JITTER);
|
|
2117
|
-
const melodyEnd =
|
|
2118
|
-
const
|
|
2219
|
+
const rng = createRng(opts.seed ?? 0);
|
|
2220
|
+
const jitter = candidates.map(() => rng.next() * TIE_BREAK_JITTER);
|
|
2221
|
+
const melodyEnd = noteIndex.notes.reduce((m, n) => Math.max(m, n.endBeat), 0);
|
|
2222
|
+
const requestedHr = assertRange(
|
|
2223
|
+
opts.harmonicRhythm,
|
|
2224
|
+
Number.MIN_VALUE,
|
|
2225
|
+
Number.MAX_SAFE_INTEGER,
|
|
2226
|
+
"harmonic rhythm"
|
|
2227
|
+
);
|
|
2228
|
+
const hr = Math.max(0.25, requestedHr);
|
|
2119
2229
|
const segCount = Math.max(1, Math.ceil(melodyEnd / hr));
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2230
|
+
assertGenerationBudget(segCount, "harmonic segments");
|
|
2231
|
+
assertGenerationBudget(segCount * candidates.length * candidates.length, "harmonization search");
|
|
2232
|
+
const segments = Array.from({ length: segCount }, (_, s) => ({
|
|
2233
|
+
startBeat: s * hr,
|
|
2234
|
+
endBeat: (s + 1) * hr,
|
|
2235
|
+
noteIndices: []
|
|
2236
|
+
}));
|
|
2237
|
+
let memberships = 0;
|
|
2238
|
+
for (const indexed of noteIndex.notes) {
|
|
2239
|
+
const first = Math.max(0, Math.floor(indexed.note.startBeat / hr));
|
|
2240
|
+
const lastExclusive = Math.min(segCount, Math.ceil(indexed.endBeat / hr - Number.EPSILON));
|
|
2241
|
+
memberships += Math.max(0, lastExclusive - first);
|
|
2242
|
+
assertGenerationBudget(memberships, "note-to-segment memberships");
|
|
2243
|
+
for (let segment = first; segment < lastExclusive; segment += 1) {
|
|
2244
|
+
segments[segment]?.noteIndices.push(indexed.originalIndex);
|
|
2245
|
+
}
|
|
2126
2246
|
}
|
|
2127
2247
|
const transposes = [0];
|
|
2128
2248
|
if (opts.placement.transposeSearch) {
|
|
@@ -2169,11 +2289,11 @@ function harmonizeMelody(opts) {
|
|
|
2169
2289
|
}
|
|
2170
2290
|
return chord;
|
|
2171
2291
|
});
|
|
2172
|
-
const melodyRoles = bestMelody.map((note,
|
|
2292
|
+
const melodyRoles = bestMelody.map((note, noteIndex2) => {
|
|
2173
2293
|
const segIdx = Math.min(segments.length - 1, Math.max(0, Math.floor(note.startBeat / hr)));
|
|
2174
2294
|
const cand = candAt(bestPath[segIdx] ?? 0);
|
|
2175
2295
|
const chord = makeChord(cand.rootPc, cand.quality);
|
|
2176
|
-
return { noteIndex, role: roleOf(note.pitch, chord).role };
|
|
2296
|
+
return { noteIndex: noteIndex2, role: roleOf(note.pitch, chord).role };
|
|
2177
2297
|
});
|
|
2178
2298
|
return { transposeSemitones: bestTs, key, chords, melodyRoles };
|
|
2179
2299
|
}
|
|
@@ -2220,29 +2340,30 @@ function nearestChordTone(pitch, chord) {
|
|
|
2220
2340
|
return pitch;
|
|
2221
2341
|
}
|
|
2222
2342
|
function cellOrigin(notes) {
|
|
2223
|
-
|
|
2343
|
+
let origin = Number.POSITIVE_INFINITY;
|
|
2344
|
+
for (const note of notes) {
|
|
2345
|
+
origin = Math.min(origin, note.startBeat);
|
|
2346
|
+
}
|
|
2347
|
+
return Number.isFinite(origin) ? origin : 0;
|
|
2348
|
+
}
|
|
2349
|
+
function cellEnd(notes) {
|
|
2350
|
+
let end = Number.NEGATIVE_INFINITY;
|
|
2351
|
+
for (const note of notes) {
|
|
2352
|
+
end = Math.max(end, note.startBeat + note.durationBeat);
|
|
2353
|
+
}
|
|
2354
|
+
return Number.isFinite(end) ? end : 0;
|
|
2224
2355
|
}
|
|
2225
2356
|
function cellSpan(cell) {
|
|
2226
2357
|
if (cell.notes.length === 0) {
|
|
2227
2358
|
return 0;
|
|
2228
2359
|
}
|
|
2229
2360
|
const start = cellOrigin(cell.notes);
|
|
2230
|
-
const end =
|
|
2361
|
+
const end = cellEnd(cell.notes);
|
|
2231
2362
|
return end - start;
|
|
2232
2363
|
}
|
|
2233
2364
|
function clone(cell) {
|
|
2234
2365
|
return { notes: cell.notes.map((n) => ({ ...n })) };
|
|
2235
2366
|
}
|
|
2236
|
-
function mulberry322(seed) {
|
|
2237
|
-
let state = seed >>> 0;
|
|
2238
|
-
return () => {
|
|
2239
|
-
state = state + 1831565813 >>> 0;
|
|
2240
|
-
let t = state;
|
|
2241
|
-
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
2242
|
-
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
2243
|
-
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
2244
|
-
};
|
|
2245
|
-
}
|
|
2246
2367
|
function contourOffsets(contour, count) {
|
|
2247
2368
|
const offsets = [];
|
|
2248
2369
|
for (let i = 0; i < count; i += 1) {
|
|
@@ -2265,19 +2386,21 @@ function contourOffsets(contour, count) {
|
|
|
2265
2386
|
}
|
|
2266
2387
|
function generateMotif(opts) {
|
|
2267
2388
|
const contour = opts.contour ?? "arch";
|
|
2268
|
-
const bars =
|
|
2389
|
+
const bars = assertPositiveInt(opts.bars, "motif bars");
|
|
2269
2390
|
const totalBeats = bars * 4;
|
|
2270
2391
|
const noteCount = Math.max(3, bars * 2);
|
|
2392
|
+
assertGenerationBudget(noteCount, "motif notes");
|
|
2271
2393
|
const beatsPerNote = totalBeats / noteCount;
|
|
2272
|
-
const rng =
|
|
2273
|
-
const
|
|
2394
|
+
const rng = createRng(opts.seed ?? 0);
|
|
2395
|
+
const requestedJitter = assertFiniteNumber(opts.jitter ?? 0, "motif jitter");
|
|
2396
|
+
const jitterProb = Math.min(1, Math.max(0, requestedJitter));
|
|
2274
2397
|
const tonic = pitchClass3(opts.key.rootPc) + 60;
|
|
2275
2398
|
const offsets = contourOffsets(contour, noteCount);
|
|
2276
2399
|
const notes = [];
|
|
2277
2400
|
for (let i = 0; i < noteCount; i += 1) {
|
|
2278
2401
|
let jitter = 0;
|
|
2279
|
-
if (jitterProb > 0 && rng() < jitterProb) {
|
|
2280
|
-
jitter = rng() < 0.5 ? 1 : -1;
|
|
2402
|
+
if (jitterProb > 0 && rng.next() < jitterProb) {
|
|
2403
|
+
jitter = rng.next() < 0.5 ? 1 : -1;
|
|
2281
2404
|
}
|
|
2282
2405
|
let pitch = stepDiatonic(tonic, (offsets[i] ?? 0) + jitter, opts.key);
|
|
2283
2406
|
const startBeat = i * beatsPerNote;
|
|
@@ -2289,6 +2412,16 @@ function generateMotif(opts) {
|
|
|
2289
2412
|
return { notes };
|
|
2290
2413
|
}
|
|
2291
2414
|
function transformMotif(cell, t, amount, key) {
|
|
2415
|
+
assertNoteEvents(cell.notes, "motif notes");
|
|
2416
|
+
if (amount !== void 0) {
|
|
2417
|
+
assertFiniteNumber(amount, "motif transform amount");
|
|
2418
|
+
}
|
|
2419
|
+
if ((t === "transposeDiatonic" || t === "sequence") && amount !== void 0) {
|
|
2420
|
+
assertInteger(amount, "diatonic transform amount");
|
|
2421
|
+
}
|
|
2422
|
+
if ((t === "augment" || t === "diminish") && amount !== void 0 && amount <= 0) {
|
|
2423
|
+
throw new RangeError("time transform amount must be positive");
|
|
2424
|
+
}
|
|
2292
2425
|
const notes = cell.notes;
|
|
2293
2426
|
switch (t) {
|
|
2294
2427
|
case "transposeChromatic": {
|
|
@@ -2315,7 +2448,7 @@ function transformMotif(cell, t, amount, key) {
|
|
|
2315
2448
|
return { notes: [] };
|
|
2316
2449
|
}
|
|
2317
2450
|
const start = cellOrigin(notes);
|
|
2318
|
-
const end =
|
|
2451
|
+
const end = cellEnd(notes);
|
|
2319
2452
|
return {
|
|
2320
2453
|
notes: notes.map((n) => ({
|
|
2321
2454
|
pitch: n.pitch,
|
|
@@ -2348,8 +2481,10 @@ function scaleTime(cell, factor) {
|
|
|
2348
2481
|
};
|
|
2349
2482
|
}
|
|
2350
2483
|
function developMotif(cell, timeline, key, bars) {
|
|
2484
|
+
assertPositiveInt(bars, "development bars");
|
|
2485
|
+
assertNoteEvents(cell.notes, "motif notes");
|
|
2351
2486
|
const span = cellSpan(cell);
|
|
2352
|
-
const totalBeats =
|
|
2487
|
+
const totalBeats = bars * 4;
|
|
2353
2488
|
const origin = cellOrigin(cell.notes);
|
|
2354
2489
|
const out = [];
|
|
2355
2490
|
if (span <= 0) {
|
|
@@ -2357,6 +2492,7 @@ function developMotif(cell, timeline, key, bars) {
|
|
|
2357
2492
|
}
|
|
2358
2493
|
const tileSpan = Math.max(span, MIN_TILE_SPAN);
|
|
2359
2494
|
const tileCount = Math.ceil(totalBeats / tileSpan);
|
|
2495
|
+
assertGenerationBudget(tileCount * cell.notes.length, "developed motif notes");
|
|
2360
2496
|
for (let k = 0; k < tileCount; k += 1) {
|
|
2361
2497
|
const offset = k * tileSpan;
|
|
2362
2498
|
for (const n of cell.notes) {
|
|
@@ -2555,16 +2691,6 @@ function progressions() {
|
|
|
2555
2691
|
function progressionsByStyle(style) {
|
|
2556
2692
|
return progressions().filter((p) => p.styles.includes(style));
|
|
2557
2693
|
}
|
|
2558
|
-
function mulberry323(seed) {
|
|
2559
|
-
let state = seed >>> 0;
|
|
2560
|
-
return () => {
|
|
2561
|
-
state = state + 1831565813 >>> 0;
|
|
2562
|
-
let t = state;
|
|
2563
|
-
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
2564
|
-
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
2565
|
-
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
2566
|
-
};
|
|
2567
|
-
}
|
|
2568
2694
|
function degreeToRootPc(degree, key) {
|
|
2569
2695
|
if (degree >= 0 && degree <= 6) {
|
|
2570
2696
|
const tones = scaleTonesInDegreeOrder(key);
|
|
@@ -2586,6 +2712,8 @@ function autoQuality(degree, key) {
|
|
|
2586
2712
|
return "maj";
|
|
2587
2713
|
}
|
|
2588
2714
|
function generateProgression(opts) {
|
|
2715
|
+
assertPositiveInt(opts.bars, "progression bars");
|
|
2716
|
+
assertGenerationBudget(opts.bars, "progression chords");
|
|
2589
2717
|
const seed = opts.seed ?? 0;
|
|
2590
2718
|
let preset;
|
|
2591
2719
|
if (opts.presetId !== void 0) {
|
|
@@ -2597,8 +2725,8 @@ function generateProgression(opts) {
|
|
|
2597
2725
|
if (preset === void 0) {
|
|
2598
2726
|
const pool = PRESETS.filter((p) => p.styles.includes(opts.style));
|
|
2599
2727
|
const candidates = pool.length > 0 ? pool : PRESETS;
|
|
2600
|
-
const rng =
|
|
2601
|
-
const index = Math.floor(rng() * candidates.length) % candidates.length;
|
|
2728
|
+
const rng = createRng(seed);
|
|
2729
|
+
const index = Math.floor(rng.next() * candidates.length) % candidates.length;
|
|
2602
2730
|
preset = candidates[index] ?? PRESETS[0];
|
|
2603
2731
|
}
|
|
2604
2732
|
const degrees = preset?.degrees ?? [0];
|
|
@@ -2616,7 +2744,7 @@ function generateProgression(opts) {
|
|
|
2616
2744
|
}
|
|
2617
2745
|
if (opts.reharmonize) {
|
|
2618
2746
|
const tonicPc = (opts.key.rootPc % 12 + 12) % 12;
|
|
2619
|
-
const rrng =
|
|
2747
|
+
const rrng = createRng((seed ^ 2654435769) >>> 0);
|
|
2620
2748
|
for (let i = 0; i < chords.length - 1; i += 1) {
|
|
2621
2749
|
const cur = chords[i];
|
|
2622
2750
|
const next = chords[i + 1];
|
|
@@ -2630,7 +2758,7 @@ function generateProgression(opts) {
|
|
|
2630
2758
|
if (targetsTonic || alreadySecondary || isResolutionTarget) {
|
|
2631
2759
|
continue;
|
|
2632
2760
|
}
|
|
2633
|
-
if (rrng() < 0.5) {
|
|
2761
|
+
if (rrng.next() < 0.5) {
|
|
2634
2762
|
chords[i] = {
|
|
2635
2763
|
rootPc: domRoot,
|
|
2636
2764
|
quality: "dom7",
|
|
@@ -2663,11 +2791,15 @@ function generateRhythm(ts, opts = {}) {
|
|
|
2663
2791
|
const seed = opts.seed ?? 0;
|
|
2664
2792
|
const bars = opts.bars ?? DEFAULT_BARS;
|
|
2665
2793
|
const subdivision = opts.subdivision ?? DEFAULT_SUBDIVISION;
|
|
2666
|
-
const
|
|
2794
|
+
const requestedDensity = assertFiniteNumber(opts.density ?? DEFAULT_DENSITY, "rhythm density");
|
|
2795
|
+
const density = Math.min(1, Math.max(0, requestedDensity));
|
|
2796
|
+
assertPositiveInt(bars, "rhythm bars");
|
|
2797
|
+
assertPositiveInt(subdivision, "rhythm subdivision");
|
|
2667
2798
|
const barBeats = beatsPerBar(ts);
|
|
2668
2799
|
const spanBeats = barBeats * bars;
|
|
2669
2800
|
const step = 1 / subdivision;
|
|
2670
2801
|
const slotCount = Math.round(spanBeats * subdivision);
|
|
2802
|
+
assertGenerationBudget(slotCount, "rhythm grid slots");
|
|
2671
2803
|
const slotsPerBar = Math.round(barBeats * subdivision);
|
|
2672
2804
|
const rng = createRng(seed);
|
|
2673
2805
|
const positions = [];
|
|
@@ -2719,4 +2851,4 @@ export {
|
|
|
2719
2851
|
generateRhythm,
|
|
2720
2852
|
rhythmDensity
|
|
2721
2853
|
};
|
|
2722
|
-
//# sourceMappingURL=chunk-
|
|
2854
|
+
//# sourceMappingURL=chunk-4MHA432G.js.map
|