@libraz/libcantus 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +63 -0
- package/README.md +7 -1
- package/README_ja.md +17 -11
- package/dist/analyze/index.cjs +31 -7
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +161 -13
- package/dist/analyze/index.d.ts +161 -13
- package/dist/analyze/index.js +37 -13
- package/dist/{chunk-NB25P3EH.js → chunk-2G5MCGYU.js} +33 -11
- package/dist/chunk-2G5MCGYU.js.map +1 -0
- package/dist/{chunk-NM7UAMC3.cjs → chunk-3FDBMIPR.cjs} +64 -64
- package/dist/chunk-3FDBMIPR.cjs.map +1 -0
- package/dist/{chunk-HPVSH7SF.cjs → chunk-3MDOEFIQ.cjs} +618 -954
- package/dist/chunk-3MDOEFIQ.cjs.map +1 -0
- package/dist/{chunk-FBH75DAY.js → chunk-5MWRBYAT.js} +560 -159
- package/dist/chunk-5MWRBYAT.js.map +1 -0
- package/dist/{chunk-J7FHVZII.cjs → chunk-6TTBEON3.cjs} +3074 -740
- package/dist/chunk-6TTBEON3.cjs.map +1 -0
- package/dist/{chunk-UMWZ4ZET.js → chunk-7NQ6AL3H.js} +4 -4
- package/dist/chunk-7NQ6AL3H.js.map +1 -0
- package/dist/{chunk-HWHGOVO6.cjs → chunk-E3QO3UET.cjs} +31 -23
- package/dist/chunk-E3QO3UET.cjs.map +1 -0
- package/dist/{chunk-H2KJHBW4.js → chunk-FJFULJZE.js} +2375 -191
- package/dist/chunk-FJFULJZE.js.map +1 -0
- package/dist/{chunk-Q6LFSAKV.js → chunk-FK3DGAIZ.js} +2657 -323
- package/dist/chunk-FK3DGAIZ.js.map +1 -0
- package/dist/chunk-GEDTJHAC.cjs.map +1 -1
- package/dist/{chunk-RFIBF2RR.js → chunk-H7FVMBUC.js} +138 -65
- package/dist/chunk-H7FVMBUC.js.map +1 -0
- package/dist/{chunk-DSMJRXCZ.cjs → chunk-HYSVSFNI.cjs} +931 -530
- package/dist/chunk-HYSVSFNI.cjs.map +1 -0
- package/dist/{chunk-35NBV2FT.cjs → chunk-KMYGRFZ6.cjs} +47 -24
- package/dist/chunk-KMYGRFZ6.cjs.map +1 -0
- package/dist/chunk-LQZQDBCD.cjs +5513 -0
- package/dist/chunk-LQZQDBCD.cjs.map +1 -0
- package/dist/{chunk-PM4XJE2Y.cjs → chunk-P3CPXAJ5.cjs} +76 -54
- package/dist/chunk-P3CPXAJ5.cjs.map +1 -0
- package/dist/{chunk-S5BKZZGM.cjs → chunk-S3QELJUE.cjs} +72 -56
- package/dist/chunk-S3QELJUE.cjs.map +1 -0
- package/dist/{chunk-PUKJP2Z5.js → chunk-TXFWSU5D.js} +31 -8
- package/dist/chunk-TXFWSU5D.js.map +1 -0
- package/dist/{chunk-76ZBTRG2.cjs → chunk-U4EQTDGN.cjs} +258 -218
- package/dist/chunk-U4EQTDGN.cjs.map +1 -0
- package/dist/chunk-UGLFDG7L.cjs.map +1 -1
- package/dist/{chunk-HYJJ2V2I.js → chunk-UTE5A36R.js} +483 -819
- package/dist/chunk-UTE5A36R.js.map +1 -0
- package/dist/{chunk-NFJ4P6ZZ.js → chunk-UZKQISRF.js} +17 -9
- package/dist/chunk-UZKQISRF.js.map +1 -0
- package/dist/{chunk-7KG3RTYS.js → chunk-VL5NXVCN.js} +281 -155
- package/dist/chunk-VL5NXVCN.js.map +1 -0
- package/dist/{chunk-CGBADIO5.cjs → chunk-WOMCXN2W.cjs} +328 -202
- package/dist/chunk-WOMCXN2W.cjs.map +1 -0
- package/dist/{chunk-3R3QVKY5.cjs → chunk-YDGMWH43.cjs} +397 -324
- package/dist/chunk-YDGMWH43.cjs.map +1 -0
- package/dist/{chunk-AGOVW7U6.js → chunk-YYVXRV3V.js} +72 -56
- package/dist/chunk-YYVXRV3V.js.map +1 -0
- package/dist/{chunk-M5RYMAFI.js → chunk-ZLNSH4UL.js} +71 -31
- package/dist/chunk-ZLNSH4UL.js.map +1 -0
- package/dist/{coerce-DRERzdve.d.cts → coerce-CUhHc3Tu.d.ts} +16 -8
- package/dist/{coerce-DP0vYu-x.d.ts → coerce-Ytt2Eiq9.d.cts} +16 -8
- package/dist/core/index.cjs +5 -5
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +9 -8
- package/dist/core/index.d.ts +9 -8
- package/dist/core/index.js +4 -4
- package/dist/generate/index.cjs +20 -8
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +105 -17
- package/dist/generate/index.d.ts +105 -17
- package/dist/generate/index.js +19 -7
- package/dist/{index-CL4b4gUQ.d.ts → index-5VJgKz_Q.d.cts} +6 -2
- package/dist/{index-B9QLziDe.d.cts → index-BLuDesVG.d.ts} +20 -13
- package/dist/{index-BHIUyjhZ.d.ts → index-BV6APVa6.d.cts} +20 -13
- package/dist/{index-BMweUlm5.d.cts → index-Bf0Sv8a9.d.ts} +6 -2
- package/dist/{index-Dw2DP7vm.d.ts → index-BkT21EMv.d.cts} +3 -2
- package/dist/{index-BL5N2Oyz.d.cts → index-C7KCaTRz.d.ts} +3 -2
- package/dist/{index-Cy15oTvh.d.ts → index-C9MYbuKS.d.cts} +7 -3
- package/dist/{index-Cy15oTvh.d.cts → index-C9MYbuKS.d.ts} +7 -3
- package/dist/{index-BNSM-DSk.d.cts → index-DVInzCDS.d.cts} +533 -28
- package/dist/{index-9eoQs2C1.d.ts → index-Dg-SAfhz.d.ts} +6 -4
- package/dist/{index-BJgP97N1.d.ts → index-jgeug9EA.d.ts} +533 -28
- package/dist/{index-cHvbYgJI.d.cts → index-pQihL4z7.d.cts} +6 -4
- package/dist/index.cjs +49 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -13
- package/dist/index.d.ts +13 -13
- package/dist/index.js +55 -19
- package/dist/{masks-Cc7-MzzY.d.ts → masks-BnzXnBj0.d.cts} +3 -2
- package/dist/{masks-pADfTOiD.d.cts → masks-tycmkrgs.d.ts} +3 -2
- package/dist/model/index.cjs +12 -12
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +259 -48
- package/dist/model/index.d.ts +259 -48
- package/dist/model/index.js +11 -11
- package/dist/{positional-DJgs6ltD.d.ts → positional-CscpxtlE.d.cts} +3 -2
- package/dist/{positional-DMZer-FM.d.cts → positional-DLnfUgqG.d.ts} +3 -2
- package/dist/{index-BRMbPkHE.d.ts → profile-B4WqcM4H.d.ts} +85 -610
- package/dist/{index-DGCNKAUr.d.cts → profile-BJd1XkPB.d.cts} +85 -610
- package/dist/theory/index.cjs +5 -5
- package/dist/theory/index.cjs.map +1 -1
- package/dist/theory/index.d.cts +18 -14
- package/dist/theory/index.d.ts +18 -14
- package/dist/theory/index.js +4 -4
- package/dist/{index-CJBBiAmU.d.ts → types-7mGFwvMm.d.ts} +1210 -31
- package/dist/{index-BBWNZS0f.d.cts → types-_ofpb2be.d.cts} +1210 -31
- package/package.json +13 -13
- package/dist/chunk-35NBV2FT.cjs.map +0 -1
- package/dist/chunk-3R3QVKY5.cjs.map +0 -1
- package/dist/chunk-76ZBTRG2.cjs.map +0 -1
- package/dist/chunk-7KG3RTYS.js.map +0 -1
- package/dist/chunk-AGOVW7U6.js.map +0 -1
- package/dist/chunk-CGBADIO5.cjs.map +0 -1
- package/dist/chunk-DSMJRXCZ.cjs.map +0 -1
- package/dist/chunk-FBH75DAY.js.map +0 -1
- package/dist/chunk-H2KJHBW4.js.map +0 -1
- package/dist/chunk-HPVSH7SF.cjs.map +0 -1
- package/dist/chunk-HWHGOVO6.cjs.map +0 -1
- package/dist/chunk-HYJJ2V2I.js.map +0 -1
- package/dist/chunk-J7FHVZII.cjs.map +0 -1
- package/dist/chunk-M5RYMAFI.js.map +0 -1
- package/dist/chunk-NB25P3EH.js.map +0 -1
- package/dist/chunk-NFJ4P6ZZ.js.map +0 -1
- package/dist/chunk-NM7UAMC3.cjs.map +0 -1
- package/dist/chunk-PM4XJE2Y.cjs.map +0 -1
- package/dist/chunk-PUKJP2Z5.js.map +0 -1
- package/dist/chunk-Q6LFSAKV.js.map +0 -1
- package/dist/chunk-RFIBF2RR.js.map +0 -1
- package/dist/chunk-S5BKZZGM.cjs.map +0 -1
- package/dist/chunk-SGLEQVJH.cjs +0 -3329
- package/dist/chunk-SGLEQVJH.cjs.map +0 -1
- package/dist/chunk-UMWZ4ZET.js.map +0 -1
|
@@ -1,16 +1,39 @@
|
|
|
1
1
|
import {
|
|
2
|
+
CADENCE_TYPES,
|
|
3
|
+
DEFAULT_VARIATION_THRESHOLD,
|
|
2
4
|
HUMANIZE_ADJACENCY,
|
|
5
|
+
MELODIC_CONTOUR_SHAPES,
|
|
6
|
+
MOTIF_RELATION_KINDS,
|
|
7
|
+
RHYTHM_IOI_BINS,
|
|
8
|
+
analyzeRhythm,
|
|
9
|
+
analyzeVoice,
|
|
3
10
|
assertChordTimeline,
|
|
11
|
+
assertReferenceProfile,
|
|
12
|
+
barSpanOf,
|
|
4
13
|
borrowedSource,
|
|
14
|
+
chordTimelineFromChords,
|
|
5
15
|
chordToRoman,
|
|
16
|
+
contourSimilarity,
|
|
17
|
+
densitySimilarity,
|
|
6
18
|
detectChordBest,
|
|
19
|
+
durationSimilarity,
|
|
7
20
|
functionOf,
|
|
21
|
+
ioiBinIndex,
|
|
8
22
|
isDiatonic,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
23
|
+
keyLookup,
|
|
24
|
+
melodicSimilarity,
|
|
25
|
+
onsetSimilarity,
|
|
26
|
+
parallelKey,
|
|
27
|
+
phraseMelody,
|
|
28
|
+
registerSimilarity,
|
|
29
|
+
rhythmLevel,
|
|
30
|
+
romanToChord,
|
|
31
|
+
syncopationSimilarity
|
|
32
|
+
} from "./chunk-FJFULJZE.js";
|
|
12
33
|
import {
|
|
34
|
+
ALGORITHM_VERSION,
|
|
13
35
|
LIMBS,
|
|
36
|
+
MIN_ALGORITHM_VERSION,
|
|
14
37
|
canSound,
|
|
15
38
|
createPositionalRng,
|
|
16
39
|
deriveSeed,
|
|
@@ -18,16 +41,20 @@ import {
|
|
|
18
41
|
includeAt,
|
|
19
42
|
instrumentRange,
|
|
20
43
|
resolveAlgorithmVersion,
|
|
44
|
+
scaleUnitDraw,
|
|
21
45
|
toStringedProfile
|
|
22
|
-
} from "./chunk-
|
|
46
|
+
} from "./chunk-VL5NXVCN.js";
|
|
23
47
|
import {
|
|
24
|
-
|
|
25
|
-
|
|
48
|
+
canonicalNoteOrder,
|
|
49
|
+
createNoteEventIndex,
|
|
50
|
+
soundingIndices
|
|
51
|
+
} from "./chunk-TXFWSU5D.js";
|
|
26
52
|
import {
|
|
27
53
|
chordScales,
|
|
28
54
|
roleOf
|
|
29
|
-
} from "./chunk-
|
|
55
|
+
} from "./chunk-2G5MCGYU.js";
|
|
30
56
|
import {
|
|
57
|
+
cadencesThroughRaisedSeventh,
|
|
31
58
|
chordPitchClasses,
|
|
32
59
|
chordQualities,
|
|
33
60
|
chordToneRole,
|
|
@@ -39,6 +66,7 @@ import {
|
|
|
39
66
|
fifthPcOf,
|
|
40
67
|
heptatonicFrameOf,
|
|
41
68
|
isForbiddenMelodicLeap,
|
|
69
|
+
isNativeDominantSeventh,
|
|
42
70
|
isScaleTone,
|
|
43
71
|
leadingTonePcOf,
|
|
44
72
|
majorKey,
|
|
@@ -50,26 +78,32 @@ import {
|
|
|
50
78
|
resolveKey,
|
|
51
79
|
scaleLadderPitch,
|
|
52
80
|
scaleLadderPosition,
|
|
53
|
-
|
|
81
|
+
scaleOf,
|
|
54
82
|
scaleTonesInDegreeOrder,
|
|
55
83
|
shiftByScaleDegrees,
|
|
56
84
|
soundsDominantSeventh,
|
|
85
|
+
spanFromChord,
|
|
57
86
|
spellPitchClass,
|
|
58
87
|
toChordData,
|
|
59
88
|
toKeyScale,
|
|
60
89
|
tonicizableDegrees
|
|
61
|
-
} from "./chunk-
|
|
90
|
+
} from "./chunk-H7FVMBUC.js";
|
|
62
91
|
import {
|
|
63
92
|
ARTICULATIONS,
|
|
64
93
|
BEAT_EPS,
|
|
65
94
|
InvalidInputError,
|
|
95
|
+
NoSolutionError,
|
|
96
|
+
allocateCandidateTable,
|
|
97
|
+
allocateChoiceTable,
|
|
66
98
|
assertArray,
|
|
67
99
|
assertDegree,
|
|
68
100
|
assertFiniteNumber,
|
|
69
101
|
assertFiniteSemitones,
|
|
102
|
+
assertFlag,
|
|
70
103
|
assertFunction,
|
|
71
104
|
assertGenerationBudget,
|
|
72
105
|
assertInteger,
|
|
106
|
+
assertMeterMap,
|
|
73
107
|
assertMidiPitch,
|
|
74
108
|
assertNoteEvents,
|
|
75
109
|
assertOneOf,
|
|
@@ -78,7 +112,10 @@ import {
|
|
|
78
112
|
assertRange,
|
|
79
113
|
assertRecord,
|
|
80
114
|
assertTimeSignature,
|
|
115
|
+
barIndexAt,
|
|
116
|
+
barStartBeat,
|
|
81
117
|
beatsPerBar,
|
|
118
|
+
beatsPerBarAt,
|
|
82
119
|
clampToMidi,
|
|
83
120
|
describeRejected,
|
|
84
121
|
dropSilentNotes,
|
|
@@ -92,10 +129,12 @@ import {
|
|
|
92
129
|
pulseBeats,
|
|
93
130
|
resolveMeters,
|
|
94
131
|
soundingNotesOnly,
|
|
132
|
+
spelledInterval,
|
|
95
133
|
toMeterData,
|
|
96
134
|
toSpelledInterval,
|
|
135
|
+
transposeByInterval,
|
|
97
136
|
transposeNote
|
|
98
|
-
} from "./chunk-
|
|
137
|
+
} from "./chunk-YYVXRV3V.js";
|
|
99
138
|
|
|
100
139
|
// src/generate/context/draw.ts
|
|
101
140
|
var DRAW_METHODS = ["at", "prob", "range", "float"];
|
|
@@ -150,7 +189,7 @@ function drawsFrom(rng, ...prefix) {
|
|
|
150
189
|
);
|
|
151
190
|
}
|
|
152
191
|
assertFiniteNumber(hi - lo, "float span");
|
|
153
|
-
return
|
|
192
|
+
return scaleUnitDraw(at2(...path), lo, hi);
|
|
154
193
|
}
|
|
155
194
|
};
|
|
156
195
|
}
|
|
@@ -375,7 +414,9 @@ var GENRES = Object.freeze([
|
|
|
375
414
|
]);
|
|
376
415
|
var PROVENANCE_BASES = Object.freeze(["idiom", "construction", "traditional"]);
|
|
377
416
|
function sameTs(a, b) {
|
|
378
|
-
|
|
417
|
+
const grouping = a.grouping;
|
|
418
|
+
const theirs = b.grouping;
|
|
419
|
+
return a.numerator === b.numerator && a.denominator === b.denominator && (grouping === void 0 || theirs === void 0 ? grouping === theirs : Array.isArray(grouping) && grouping.length === theirs.length && grouping.every((entry, index) => entry === theirs[index]));
|
|
379
420
|
}
|
|
380
421
|
function fitsQuery(entry, query) {
|
|
381
422
|
const candidate = assertRecord(entry, "vocabulary entry");
|
|
@@ -536,6 +577,14 @@ function sustainsShift(semitones, stepBeats, bpm, difficulty) {
|
|
|
536
577
|
|
|
537
578
|
// src/generate/context/index.ts
|
|
538
579
|
var DEFAULT_SEED = 0;
|
|
580
|
+
function nullish(value) {
|
|
581
|
+
return value === void 0 || value === null;
|
|
582
|
+
}
|
|
583
|
+
function assertPositionalSource(rng) {
|
|
584
|
+
const source = assertRecord(rng, "ctx.rng");
|
|
585
|
+
assertFunction(source.at, "ctx.rng.at");
|
|
586
|
+
return source;
|
|
587
|
+
}
|
|
539
588
|
var VERSION_SEGMENT = "v";
|
|
540
589
|
function resolveContext(input) {
|
|
541
590
|
if (input !== null && typeof input !== "number" && input !== void 0) {
|
|
@@ -546,18 +595,16 @@ function resolveContext(input) {
|
|
|
546
595
|
}
|
|
547
596
|
const ctx = typeof input === "number" ? { seed: input } : input ?? { seed: DEFAULT_SEED };
|
|
548
597
|
const seed = assertInteger(ctx.seed ?? DEFAULT_SEED, "seed", 0, 4294967295);
|
|
549
|
-
const complexity = ctx.complexity
|
|
598
|
+
const complexity = nullish(ctx.complexity) ? {} : assertRecord(ctx.complexity, "ctx.complexity");
|
|
550
599
|
const rhythmic = complexity.rhythmic === void 0 ? void 0 : assertRange(complexity.rhythmic, 0, 1, "complexity rhythmic");
|
|
551
600
|
const harmonic = complexity.harmonic === void 0 ? void 0 : assertRange(complexity.harmonic, 0, 1, "complexity harmonic");
|
|
552
601
|
const ornament2 = complexity.ornament === void 0 ? void 0 : assertRange(complexity.ornament, 0, 1, "complexity ornament");
|
|
553
602
|
const difficulty = complexity.difficulty === void 0 ? void 0 : assertDifficulty(complexity.difficulty);
|
|
554
603
|
const bpm = ctx.bpm === void 0 ? void 0 : assertRange(ctx.bpm, Number.MIN_VALUE, 1e3, "bpm");
|
|
555
604
|
const algorithmVersion = resolveAlgorithmVersion(ctx.algorithmVersion);
|
|
556
|
-
const supplied = ctx.rng;
|
|
557
|
-
const instruments = ctx.instruments;
|
|
558
|
-
const vocabulary = (ctx.vocabulary
|
|
559
|
-
(entry, index) => assertVocabulary(entry, `vocabulary[${index}]`)
|
|
560
|
-
);
|
|
605
|
+
const supplied = ctx.rng ? assertPositionalSource(ctx.rng) : void 0;
|
|
606
|
+
const instruments = nullish(ctx.instruments) ? void 0 : assertRecord(ctx.instruments, "ctx.instruments");
|
|
607
|
+
const vocabulary = (nullish(ctx.vocabulary) ? [] : assertArray(ctx.vocabulary, "ctx.vocabulary")).map((entry, index) => assertVocabulary(entry, `vocabulary[${index}]`));
|
|
561
608
|
return {
|
|
562
609
|
seed,
|
|
563
610
|
rhythmic,
|
|
@@ -566,7 +613,7 @@ function resolveContext(input) {
|
|
|
566
613
|
difficulty,
|
|
567
614
|
bpm,
|
|
568
615
|
algorithmVersion,
|
|
569
|
-
instrument: (part) => instruments
|
|
616
|
+
instrument: (part) => instruments !== void 0 && Object.hasOwn(instruments, part) ? instruments[part] : void 0,
|
|
570
617
|
vocabulary,
|
|
571
618
|
part: (name) => supplied ? drawsFrom(supplied, name, VERSION_SEGMENT, algorithmVersion) : drawsFrom(createPositionalRng(deriveSeed(seed, name, VERSION_SEGMENT, algorithmVersion)))
|
|
572
619
|
};
|
|
@@ -718,6 +765,7 @@ function bassRegister(named, given, octave) {
|
|
|
718
765
|
var BAR_STEPS = 16;
|
|
719
766
|
var BEAT_STEPS = 4;
|
|
720
767
|
var STEP_BEATS = 1 / BEAT_STEPS;
|
|
768
|
+
var FIGURE_RATES = ["straight", "half", "double"];
|
|
721
769
|
function assertGridEvents(events) {
|
|
722
770
|
assertArray(events, "figure events");
|
|
723
771
|
events.forEach((event, index) => {
|
|
@@ -732,6 +780,16 @@ function assertSpanSteps(spanSteps) {
|
|
|
732
780
|
function streamOf(event) {
|
|
733
781
|
return event.limb ?? event.voice ?? "";
|
|
734
782
|
}
|
|
783
|
+
function positionKey(event, step) {
|
|
784
|
+
return `${streamOf(event)}@${step}`;
|
|
785
|
+
}
|
|
786
|
+
function drawKey(event) {
|
|
787
|
+
const stream = `${streamOf(event)}`;
|
|
788
|
+
return stream === "" ? [event.step] : [stream, event.step];
|
|
789
|
+
}
|
|
790
|
+
function halvedStep(step) {
|
|
791
|
+
return Number.isInteger(step) ? Math.round(step / 2) : step / 2;
|
|
792
|
+
}
|
|
735
793
|
var FIGURE_TS = { numerator: 4, denominator: 4 };
|
|
736
794
|
function subdivisionBeats(ts) {
|
|
737
795
|
const pulse2 = pulseBeats(ts);
|
|
@@ -754,46 +812,57 @@ function stepWeight(step, ts) {
|
|
|
754
812
|
function thin(events, amount, ts) {
|
|
755
813
|
const figure = assertGridEvents(events);
|
|
756
814
|
assertRange(amount, 0, 1, "thin amount");
|
|
757
|
-
|
|
815
|
+
return thinFigure(figure, amount, meterAt(0, toMeterData(ts, "ts")));
|
|
816
|
+
}
|
|
817
|
+
function thinFigure(figure, amount, ts) {
|
|
758
818
|
const floor = Math.min(4, Math.floor(amount * 5));
|
|
759
|
-
return figure.filter((event) => stepWeight(event.step,
|
|
819
|
+
return figure.filter((event) => stepWeight(event.step, ts) >= floor);
|
|
760
820
|
}
|
|
761
821
|
function double(events, spanSteps = BAR_STEPS) {
|
|
762
822
|
assertSpanSteps(spanSteps);
|
|
763
823
|
const sorted = [...assertGridEvents(events)].sort((a, b) => a.step - b.step);
|
|
764
|
-
const
|
|
765
|
-
|
|
824
|
+
const nextStep = [];
|
|
825
|
+
const following = /* @__PURE__ */ new Map();
|
|
826
|
+
for (let i = sorted.length - 1; i >= 0; i -= 1) {
|
|
766
827
|
const event = sorted[i];
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
828
|
+
const stream = streamOf(event);
|
|
829
|
+
nextStep[i] = following.get(stream) ?? spanSteps;
|
|
830
|
+
following.set(stream, event.step);
|
|
831
|
+
}
|
|
832
|
+
const out = [];
|
|
833
|
+
sorted.forEach((event, i) => {
|
|
770
834
|
out.push(event);
|
|
771
|
-
const next =
|
|
835
|
+
const next = nextStep[i] ?? spanSteps;
|
|
772
836
|
const mid = event.step + (next - event.step) / 2;
|
|
773
837
|
if (Number.isInteger(mid) && mid > event.step && mid < spanSteps) {
|
|
774
838
|
out.push({ ...event, step: mid, velocity: event.velocity * ADDED_NOTE_VELOCITY });
|
|
775
839
|
}
|
|
776
|
-
}
|
|
777
|
-
return out;
|
|
840
|
+
});
|
|
841
|
+
return out.sort((a, b) => a.step - b.step);
|
|
778
842
|
}
|
|
779
843
|
var ADDED_NOTE_VELOCITY = 0.75;
|
|
780
844
|
function halfTime(events, spanSteps = BAR_STEPS) {
|
|
781
845
|
assertSpanSteps(spanSteps);
|
|
782
|
-
return assertGridEvents(events)
|
|
846
|
+
return halfTimeFigure(assertGridEvents(events), spanSteps);
|
|
847
|
+
}
|
|
848
|
+
function halfTimeFigure(figure, spanSteps) {
|
|
849
|
+
return figure.map((event) => ({ ...event, step: event.step * 2 })).filter((event) => event.step < spanSteps);
|
|
783
850
|
}
|
|
784
851
|
function doubleTime(events, spanSteps = BAR_STEPS) {
|
|
785
852
|
assertSpanSteps(spanSteps);
|
|
786
|
-
assertGridEvents(events);
|
|
853
|
+
return doubleTimeFigure(assertGridEvents(events), spanSteps);
|
|
854
|
+
}
|
|
855
|
+
function doubleTimeFigure(figure, spanSteps) {
|
|
787
856
|
const merged = /* @__PURE__ */ new Map();
|
|
788
857
|
const place = (event, step, velocity) => {
|
|
789
|
-
const key =
|
|
858
|
+
const key = positionKey(event, step);
|
|
790
859
|
const held = merged.get(key);
|
|
791
860
|
if (held === void 0 || velocity > held.velocity) {
|
|
792
861
|
merged.set(key, { ...event, step, velocity });
|
|
793
862
|
}
|
|
794
863
|
};
|
|
795
|
-
for (const event of
|
|
796
|
-
const step =
|
|
864
|
+
for (const event of figure) {
|
|
865
|
+
const step = halvedStep(event.step);
|
|
797
866
|
place(event, step, event.velocity);
|
|
798
867
|
const repeated = step + spanSteps / 2;
|
|
799
868
|
if (repeated < spanSteps) {
|
|
@@ -810,14 +879,17 @@ function syncopate(events, amount, draw, ...path) {
|
|
|
810
879
|
const dial = asked === void 0 ? amount : asked.amount;
|
|
811
880
|
const signature = meterAt(0, toMeterData(asked?.ts ?? FIGURE_TS, "ts"));
|
|
812
881
|
assertRange(dial, 0, 1, "syncopate amount");
|
|
813
|
-
|
|
882
|
+
return syncopateFigure(figure, dial, signature, sampler, path);
|
|
883
|
+
}
|
|
884
|
+
function syncopateFigure(figure, dial, ts, sampler, path) {
|
|
885
|
+
const occupied = new Set(figure.map((event) => positionKey(event, event.step)));
|
|
814
886
|
const out = [...figure];
|
|
815
887
|
for (const event of figure) {
|
|
816
888
|
const anticipated = event.step - 1;
|
|
817
|
-
if (stepWeight(event.step,
|
|
889
|
+
if (stepWeight(event.step, ts) < 2 || anticipated < 1 || occupied.has(positionKey(event, anticipated))) {
|
|
818
890
|
continue;
|
|
819
891
|
}
|
|
820
|
-
if (!sampler.prob(dial, ...path, "syncopate", event
|
|
892
|
+
if (!sampler.prob(dial, ...path, "syncopate", ...drawKey(event))) {
|
|
821
893
|
continue;
|
|
822
894
|
}
|
|
823
895
|
out.push({ ...event, step: anticipated, velocity: event.velocity * ADDED_NOTE_VELOCITY });
|
|
@@ -830,35 +902,47 @@ function ornamentBy(events, isOrnament, amount, draw, ...path) {
|
|
|
830
902
|
const sampler = assertDraw(draw);
|
|
831
903
|
assertSeedPath(path);
|
|
832
904
|
assertRange(amount, 0, 1, "ornament amount");
|
|
905
|
+
return ornamentFigure(figure, marks, amount, sampler, path);
|
|
906
|
+
}
|
|
907
|
+
function ornamentFigure(figure, marks, amount, sampler, path) {
|
|
833
908
|
return figure.filter(
|
|
834
|
-
(event) => !marks(event) || sampler.prob(amount, ...path, "ornament", event
|
|
909
|
+
(event) => !marks(event) || sampler.prob(amount, ...path, "ornament", ...drawKey(event))
|
|
835
910
|
);
|
|
836
911
|
}
|
|
837
912
|
var NEUTRAL_RHYTHMIC = 0.5;
|
|
838
913
|
function deform(events, opts, draw, ...path) {
|
|
839
914
|
const figure = assertGridEvents(events);
|
|
840
915
|
const asked = assertRecord(opts, "opts");
|
|
841
|
-
assertDraw(draw);
|
|
916
|
+
const sampler = assertDraw(draw);
|
|
842
917
|
assertSeedPath(path);
|
|
843
918
|
const spanSteps = assertSpanSteps(asked.spanSteps === void 0 ? BAR_STEPS : asked.spanSteps);
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
919
|
+
return deformFigure(figure, spanSteps, resolveDials(asked), sampler, path);
|
|
920
|
+
}
|
|
921
|
+
function resolveDials(asked) {
|
|
922
|
+
const rate = assertOneOf(asked.rate ?? "straight", FIGURE_RATES, "rate");
|
|
923
|
+
const rhythmic = asked.rhythmic === void 0 ? void 0 : assertRange(asked.rhythmic, 0, 1, "complexity rhythmic");
|
|
924
|
+
const ts = meterAt(0, toMeterData(asked.ts ?? FIGURE_TS, "ts"));
|
|
925
|
+
const ornament2 = asked.ornament === void 0 ? void 0 : assertRange(asked.ornament, 0, 1, "ornament amount");
|
|
926
|
+
const isOrnament = asked.isOrnament ? assertFunction(asked.isOrnament, "opts.isOrnament") : void 0;
|
|
927
|
+
return { rate, rhythmic, ts, ornament: ornament2, isOrnament };
|
|
928
|
+
}
|
|
929
|
+
function deformFigure(figure, spanSteps, dials, sampler, path) {
|
|
930
|
+
let out = dials.rate === "half" ? halfTimeFigure(figure, spanSteps) : dials.rate === "double" ? doubleTimeFigure(figure, spanSteps) : [...figure];
|
|
931
|
+
const { rhythmic } = dials;
|
|
932
|
+
if (rhythmic !== void 0 && rhythmic < NEUTRAL_RHYTHMIC) {
|
|
933
|
+
out = thinFigure(out, (NEUTRAL_RHYTHMIC - rhythmic) / NEUTRAL_RHYTHMIC, dials.ts);
|
|
934
|
+
} else if (rhythmic !== void 0 && rhythmic > NEUTRAL_RHYTHMIC) {
|
|
935
|
+
out = syncopateFigure(
|
|
936
|
+
out,
|
|
937
|
+
(rhythmic - NEUTRAL_RHYTHMIC) / NEUTRAL_RHYTHMIC,
|
|
938
|
+
dials.ts,
|
|
939
|
+
sampler,
|
|
940
|
+
path
|
|
941
|
+
);
|
|
858
942
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
out =
|
|
943
|
+
const { ornament: ornament2, isOrnament } = dials;
|
|
944
|
+
if (ornament2 !== void 0 && isOrnament !== void 0) {
|
|
945
|
+
out = ornamentFigure(out, (event) => isOrnament(event), ornament2, sampler, path);
|
|
862
946
|
}
|
|
863
947
|
return out.sort((a, b) => a.step - b.step);
|
|
864
948
|
}
|
|
@@ -975,6 +1059,32 @@ function isLickMaterial(material) {
|
|
|
975
1059
|
const candidate = material;
|
|
976
1060
|
return typeof candidate === "object" && candidate !== null && typeof candidate.lengthSteps === "number" && Array.isArray(candidate.notes);
|
|
977
1061
|
}
|
|
1062
|
+
function assertLickMaterial(material, label) {
|
|
1063
|
+
material.notes.forEach((given, index) => {
|
|
1064
|
+
const name = `${label} notes[${index}]`;
|
|
1065
|
+
const lickNote = assertRecord(given, name);
|
|
1066
|
+
assertDegree(lickNote.degree, `${name}.degree`);
|
|
1067
|
+
assertFiniteNumber(lickNote.step, `${name}.step`);
|
|
1068
|
+
assertFiniteNumber(lickNote.velocity, `${name}.velocity`);
|
|
1069
|
+
if (lickNote.alter !== void 0) {
|
|
1070
|
+
assertInteger(lickNote.alter, `${name}.alter`);
|
|
1071
|
+
}
|
|
1072
|
+
if (lickNote.lengthSteps !== void 0) {
|
|
1073
|
+
assertFiniteNumber(lickNote.lengthSteps, `${name}.lengthSteps`);
|
|
1074
|
+
}
|
|
1075
|
+
if (lickNote.articulation !== void 0) {
|
|
1076
|
+
assertOneOf(lickNote.articulation, ARTICULATIONS, `${name}.articulation`);
|
|
1077
|
+
}
|
|
1078
|
+
for (const field of ["voice", "limb"]) {
|
|
1079
|
+
const value = lickNote[field];
|
|
1080
|
+
if (value !== void 0 && typeof value !== "string") {
|
|
1081
|
+
throw new InvalidInputError(
|
|
1082
|
+
`${name}.${field} must be a string; received ${describeRejected(value)}`
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
978
1088
|
function note(degree, step, velocity, extra) {
|
|
979
1089
|
return { degree, step, velocity, ...extra };
|
|
980
1090
|
}
|
|
@@ -1202,10 +1312,11 @@ function placeLicks(timeline, key, opts) {
|
|
|
1202
1312
|
const supplied = asked.difficulty === void 0 ? void 0 : assertDifficulty(asked.difficulty, "lick difficulty");
|
|
1203
1313
|
const difficulty = resolved.difficulty ?? supplied;
|
|
1204
1314
|
const { instrument, low } = bassRegister(resolved.instrument("bass"), asked.instrument, octave);
|
|
1205
|
-
const
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
);
|
|
1315
|
+
const callerLicks = vocabularyOfKind(resolved.vocabulary, isLickMaterial);
|
|
1316
|
+
callerLicks.forEach((entry, index) => {
|
|
1317
|
+
assertLickMaterial(entry.material, `lick vocabulary[${index}]`);
|
|
1318
|
+
});
|
|
1319
|
+
const dictionary = mergeVocabulary(BASS_LICKS, callerLicks);
|
|
1209
1320
|
const playable = instrument === void 0 ? {} : { articulations: instrument.articulations };
|
|
1210
1321
|
const raw = [];
|
|
1211
1322
|
for (let index = 0; index < segments.length; index += 1) {
|
|
@@ -1637,9 +1748,10 @@ function imitate(lead, opts) {
|
|
|
1637
1748
|
let offset = 0;
|
|
1638
1749
|
if (invert) {
|
|
1639
1750
|
if (tonal) {
|
|
1640
|
-
const
|
|
1641
|
-
|
|
1642
|
-
|
|
1751
|
+
const axis = scaleLadderPosition(pivot, key);
|
|
1752
|
+
const here = scaleLadderPosition(pitch - axis.offset, key);
|
|
1753
|
+
pitch = scaleLadderPitch(2 * axis.rung - here.rung, key);
|
|
1754
|
+
offset = axis.offset - here.offset;
|
|
1643
1755
|
} else {
|
|
1644
1756
|
pitch = 2 * pivot - pitch;
|
|
1645
1757
|
}
|
|
@@ -1845,7 +1957,7 @@ function generateCounterMelody(opts) {
|
|
|
1845
1957
|
throw new InvalidInputError("countermelody needs a timeline or a chordAt callback");
|
|
1846
1958
|
}
|
|
1847
1959
|
const chordChangeBeats = timeline === void 0 ? opts.chordChangeBeats ?? [] : timeline.segments.flatMap((segment) => [segment.startBeat, segment.endBeat]);
|
|
1848
|
-
const register = opts.register ?? "below";
|
|
1960
|
+
const register = assertOneOf(opts.register ?? "below", ["above", "below"], "register");
|
|
1849
1961
|
assertNoteEvents(opts.melody, "countermelody melody", { allowNonPositiveDuration: true });
|
|
1850
1962
|
const melody = createNoteEventIndex(soundingNotesOnly(opts.melody), {
|
|
1851
1963
|
tieBreak: register === "below" ? "highest" : "lowest"
|
|
@@ -2078,13 +2190,14 @@ function denser(level) {
|
|
|
2078
2190
|
}
|
|
2079
2191
|
return "sixteenth";
|
|
2080
2192
|
}
|
|
2193
|
+
var LEVEL_ORDER = ["quarter", "eighth", "sixteenth"];
|
|
2081
2194
|
function hiHatStep(level) {
|
|
2082
2195
|
if (level === "sixteenth") {
|
|
2083
2196
|
return 0.25;
|
|
2084
2197
|
}
|
|
2085
2198
|
return level === "eighth" ? 0.5 : 1;
|
|
2086
2199
|
}
|
|
2087
|
-
function
|
|
2200
|
+
function getHiHatBar(section, style, rhythmic, bpm, draw, difficulty, bar2) {
|
|
2088
2201
|
const allow16th = bpm < HH_16TH_BPM_THRESHOLD;
|
|
2089
2202
|
let base = "eighth";
|
|
2090
2203
|
let dialed = true;
|
|
@@ -2124,22 +2237,41 @@ function getHiHatLevel(section, style, rhythmic, bpm, draw, difficulty, bar2) {
|
|
|
2124
2237
|
break;
|
|
2125
2238
|
}
|
|
2126
2239
|
}
|
|
2240
|
+
const playable = (proposed) => {
|
|
2241
|
+
let level2 = !allow16th && proposed === "sixteenth" ? "eighth" : proposed;
|
|
2242
|
+
while (level2 !== "quarter" && !sustainsStrokes(hiHatStep(level2), bpm, difficulty)) {
|
|
2243
|
+
level2 = sparser(level2);
|
|
2244
|
+
}
|
|
2245
|
+
return level2;
|
|
2246
|
+
};
|
|
2127
2247
|
let level = base;
|
|
2248
|
+
let thinnedUpTo = Number.NEGATIVE_INFINITY;
|
|
2249
|
+
let filledAbove = Number.POSITIVE_INFINITY;
|
|
2128
2250
|
if (dialed) {
|
|
2129
|
-
|
|
2251
|
+
const thin2 = draw.at("hhThin", bar2);
|
|
2252
|
+
if (thin2 >= 2 * rhythmic) {
|
|
2130
2253
|
level = sparser(level);
|
|
2131
2254
|
}
|
|
2132
|
-
|
|
2255
|
+
const fill2 = draw.at("hhFill", bar2);
|
|
2256
|
+
if (fill2 < 2 * rhythmic - 1) {
|
|
2133
2257
|
level = denser(level);
|
|
2134
2258
|
}
|
|
2259
|
+
thinnedUpTo = thin2 / 2;
|
|
2260
|
+
filledAbove = (fill2 + 1) / 2;
|
|
2135
2261
|
}
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2262
|
+
const rank = (candidate) => LEVEL_ORDER.indexOf(candidate);
|
|
2263
|
+
const sparsest = rank(playable(dialed ? sparser(base) : base));
|
|
2264
|
+
const middle = rank(playable(base));
|
|
2265
|
+
const densest = rank(playable(dialed ? denser(base) : base));
|
|
2266
|
+
const entryOf = (grid) => sparsest >= rank(grid) ? Number.NEGATIVE_INFINITY : middle >= rank(grid) ? thinnedUpTo : densest >= rank(grid) ? filledAbove : Number.POSITIVE_INFINITY;
|
|
2267
|
+
return {
|
|
2268
|
+
level: playable(level),
|
|
2269
|
+
entries: {
|
|
2270
|
+
quarter: entryOf("quarter"),
|
|
2271
|
+
eighth: entryOf("eighth"),
|
|
2272
|
+
sixteenth: entryOf("sixteenth")
|
|
2273
|
+
}
|
|
2274
|
+
};
|
|
2143
2275
|
}
|
|
2144
2276
|
function hiHatVelocityMultiplier(sixteenth, jitter) {
|
|
2145
2277
|
switch (sixteenth) {
|
|
@@ -2350,11 +2482,12 @@ function generateKickForBeat(ctx, sec, kick) {
|
|
|
2350
2482
|
const sixteenths = kick.length / 4;
|
|
2351
2483
|
for (let step = 0; step < sixteenths; step += 1) {
|
|
2352
2484
|
const index = ctx.beat * sixteenths + step;
|
|
2353
|
-
|
|
2485
|
+
const entry = kick[index];
|
|
2486
|
+
if (entry === void 0) {
|
|
2354
2487
|
continue;
|
|
2355
2488
|
}
|
|
2356
2489
|
if (step === 0) {
|
|
2357
|
-
ctx.track.add(GM.BD, ctx.beatTick, EIGHTH, ctx.velocity);
|
|
2490
|
+
ctx.track.add(GM.BD, ctx.beatTick, EIGHTH, ctx.velocity, void 0, entry);
|
|
2358
2491
|
continue;
|
|
2359
2492
|
}
|
|
2360
2493
|
const offBeat = step % 2 === 1;
|
|
@@ -2362,7 +2495,7 @@ function generateKickForBeat(ctx, sec, kick) {
|
|
|
2362
2495
|
continue;
|
|
2363
2496
|
}
|
|
2364
2497
|
const tick = swing16(ctx.beatTick + step * SIXTEENTH, sec, ctx.swingAmount);
|
|
2365
|
-
ctx.track.add(GM.BD, tick, offBeat ? SIXTEENTH : EIGHTH, ctx.velocity * 0.85);
|
|
2498
|
+
ctx.track.add(GM.BD, tick, offBeat ? SIXTEENTH : EIGHTH, ctx.velocity * 0.85, void 0, entry);
|
|
2366
2499
|
}
|
|
2367
2500
|
}
|
|
2368
2501
|
function roleBackbeatWeight(role) {
|
|
@@ -2415,16 +2548,19 @@ function generateGhostNotesForBeat(ctx, sec) {
|
|
|
2415
2548
|
return;
|
|
2416
2549
|
}
|
|
2417
2550
|
const positions = selectGhostPositions(sec.ghostMood);
|
|
2418
|
-
|
|
2551
|
+
const ghostDensity = getGhostDensity(sec.ghostMood, ctx.section, ctx.bpm);
|
|
2552
|
+
let ghostProb = leanedBy(ghostDensity, sec.ornament);
|
|
2419
2553
|
if (sec.ghostBoost) {
|
|
2420
2554
|
ghostProb = Math.min(1, ghostProb * 1.4);
|
|
2421
2555
|
}
|
|
2422
2556
|
for (const pos of positions) {
|
|
2423
2557
|
const sixteenthInBeat = pos === "e" ? 1 : 3;
|
|
2424
2558
|
const posProb = getGhostProbabilityAtPosition(ctx.beat, sixteenthInBeat, leadIns);
|
|
2425
|
-
|
|
2559
|
+
const at2 = ctx.draw.at("ghost", ctx.bar, ctx.beat, pos);
|
|
2560
|
+
if (!(at2 < ghostProb * posProb)) {
|
|
2426
2561
|
continue;
|
|
2427
2562
|
}
|
|
2563
|
+
const entry = sec.ornamentFollowsRhythmic ? at2 / (posProb * (sec.ghostBoost ? 1.4 : 1) * ghostDensity) - 0.5 : Number.NEGATIVE_INFINITY;
|
|
2428
2564
|
const variation = ctx.draw.float(0.85, 1.15, "ghostVelocity", ctx.bar, ctx.beat, pos);
|
|
2429
2565
|
const ghostBase = getGhostVelocity(ctx.section, ctx.beat / 2);
|
|
2430
2566
|
let ghostVel = ctx.velocity * ghostBase * variation;
|
|
@@ -2433,7 +2569,7 @@ function generateGhostNotesForBeat(ctx, sec) {
|
|
|
2433
2569
|
}
|
|
2434
2570
|
const offset = pos === "e" ? SIXTEENTH : 3 * SIXTEENTH;
|
|
2435
2571
|
const tick = swing16(ctx.beatTick + offset, sec, ctx.swingAmount);
|
|
2436
|
-
ctx.track.add(GM.SD, tick, SIXTEENTH, ghostVel);
|
|
2572
|
+
ctx.track.add(GM.SD, tick, SIXTEENTH, ghostVel, void 0, entry);
|
|
2437
2573
|
}
|
|
2438
2574
|
}
|
|
2439
2575
|
function generatePreChorusBuildup(ctx, sec, isSectionLastBar) {
|
|
@@ -2491,17 +2627,25 @@ function generateHiHatForBeat(ctx, sec) {
|
|
|
2491
2627
|
const allowsOpenHiHat = sec.role !== "ambient" && sec.role !== "minimal";
|
|
2492
2628
|
const isDynamicOpen = ctx.barHasOpenHh && ctx.beat === ctx.openHhBeat && !usesOffbeatOpenHiHat(sec.style);
|
|
2493
2629
|
const dm = sec.densityMult;
|
|
2630
|
+
const beatEntry = ctx.section === "intro" && ctx.beat !== 0 ? ctx.hhEntries.eighth : ctx.hhEntries.quarter;
|
|
2494
2631
|
if (ctx.hhLevel === "quarter") {
|
|
2495
2632
|
if (ctx.section === "intro" && ctx.beat !== 0) {
|
|
2496
2633
|
return;
|
|
2497
2634
|
}
|
|
2498
2635
|
const hhVel = Math.max(20, ctx.velocity * dm * 0.75 * typeMult);
|
|
2499
2636
|
if (allowsOpenHiHat && isDynamicOpen) {
|
|
2500
|
-
ctx.track.add(GM.OHH, ctx.beatTick, EIGHTH, hhVel + OHH_VEL_BOOST);
|
|
2637
|
+
ctx.track.add(GM.OHH, ctx.beatTick, EIGHTH, hhVel + OHH_VEL_BOOST, void 0, beatEntry);
|
|
2501
2638
|
} else if (allowsOpenHiHat && opensOnDownbeat(ctx, sec)) {
|
|
2502
|
-
ctx.track.add(
|
|
2639
|
+
ctx.track.add(
|
|
2640
|
+
hiHatNote("open"),
|
|
2641
|
+
ctx.beatTick,
|
|
2642
|
+
EIGHTH,
|
|
2643
|
+
Math.max(20, hhVel * 1.1),
|
|
2644
|
+
void 0,
|
|
2645
|
+
beatEntry
|
|
2646
|
+
);
|
|
2503
2647
|
} else {
|
|
2504
|
-
ctx.track.add(hhInstrument, ctx.beatTick, EIGHTH, hhVel);
|
|
2648
|
+
ctx.track.add(hhInstrument, ctx.beatTick, EIGHTH, hhVel, void 0, beatEntry);
|
|
2505
2649
|
}
|
|
2506
2650
|
return;
|
|
2507
2651
|
}
|
|
@@ -2511,22 +2655,37 @@ function generateHiHatForBeat(ctx, sec) {
|
|
|
2511
2655
|
if (eighth === 1) {
|
|
2512
2656
|
hhTick = swing16(hhTick, sec, ctx.swingAmount);
|
|
2513
2657
|
}
|
|
2658
|
+
const entry = eighth === 0 ? beatEntry : ctx.hhEntries.eighth;
|
|
2514
2659
|
if (ctx.section === "intro" && eighth === 1) {
|
|
2515
2660
|
if (sec.useFootHh && ctx.beat % 2 === 0) {
|
|
2516
|
-
ctx.track.add(
|
|
2661
|
+
ctx.track.add(
|
|
2662
|
+
GM.FHH,
|
|
2663
|
+
hhTick,
|
|
2664
|
+
EIGHTH,
|
|
2665
|
+
footHiHatVelocity(ctx.draw, ctx.bar, ctx.beat),
|
|
2666
|
+
void 0,
|
|
2667
|
+
entry
|
|
2668
|
+
);
|
|
2517
2669
|
}
|
|
2518
2670
|
continue;
|
|
2519
2671
|
}
|
|
2520
2672
|
const hhVel = Math.max(20, ctx.velocity * dm * typeMult * (eighth === 0 ? 0.9 : 0.65));
|
|
2521
2673
|
if (isDynamicOpen && allowsOpenHiHat && eighth === 0) {
|
|
2522
|
-
ctx.track.add(GM.OHH, hhTick, EIGHTH, hhVel + OHH_VEL_BOOST);
|
|
2674
|
+
ctx.track.add(GM.OHH, hhTick, EIGHTH, hhVel + OHH_VEL_BOOST, void 0, entry);
|
|
2523
2675
|
continue;
|
|
2524
2676
|
}
|
|
2525
2677
|
const useOpen = eighth === 0 ? opensOnDownbeat(ctx, sec) : opensOffbeat(ctx, sec);
|
|
2526
2678
|
if (useOpen && allowsOpenHiHat) {
|
|
2527
|
-
ctx.track.add(
|
|
2679
|
+
ctx.track.add(
|
|
2680
|
+
hiHatNote("open"),
|
|
2681
|
+
hhTick,
|
|
2682
|
+
EIGHTH,
|
|
2683
|
+
Math.max(20, hhVel * 1.1),
|
|
2684
|
+
void 0,
|
|
2685
|
+
entry
|
|
2686
|
+
);
|
|
2528
2687
|
} else {
|
|
2529
|
-
ctx.track.add(hhInstrument, hhTick, EIGHTH / 2, hhVel);
|
|
2688
|
+
ctx.track.add(hhInstrument, hhTick, EIGHTH / 2, hhVel, void 0, entry);
|
|
2530
2689
|
}
|
|
2531
2690
|
}
|
|
2532
2691
|
return;
|
|
@@ -2541,26 +2700,41 @@ function generateHiHatForBeat(ctx, sec) {
|
|
|
2541
2700
|
ctx.draw.float(0.95, 1.05, "hatVelocity", ctx.bar, ctx.beat, sixteenth)
|
|
2542
2701
|
);
|
|
2543
2702
|
const hhVel = Math.max(20, ctx.velocity * dm * typeMult * metricVel);
|
|
2703
|
+
const entry = sixteenth === 0 ? beatEntry : sixteenth === 2 ? ctx.hhEntries.eighth : ctx.hhEntries.sixteenth;
|
|
2544
2704
|
if (isDynamicOpen && allowsOpenHiHat && sixteenth === 0) {
|
|
2545
|
-
ctx.track.add(GM.OHH, hhTick, SIXTEENTH, hhVel + OHH_VEL_BOOST);
|
|
2705
|
+
ctx.track.add(GM.OHH, hhTick, SIXTEENTH, hhVel + OHH_VEL_BOOST, void 0, entry);
|
|
2546
2706
|
continue;
|
|
2547
2707
|
}
|
|
2548
2708
|
if (allowsOpenHiHat && sixteenth === 0 && opensOnDownbeat(ctx, sec)) {
|
|
2549
|
-
ctx.track.add(
|
|
2709
|
+
ctx.track.add(
|
|
2710
|
+
hiHatNote("open"),
|
|
2711
|
+
hhTick,
|
|
2712
|
+
SIXTEENTH,
|
|
2713
|
+
Math.max(20, hhVel * 1.1),
|
|
2714
|
+
void 0,
|
|
2715
|
+
entry
|
|
2716
|
+
);
|
|
2550
2717
|
continue;
|
|
2551
2718
|
}
|
|
2552
2719
|
if (allowsOpenHiHat && sixteenth === 2 && opensOffbeat(ctx, sec)) {
|
|
2553
|
-
ctx.track.add(
|
|
2720
|
+
ctx.track.add(
|
|
2721
|
+
hiHatNote("open"),
|
|
2722
|
+
hhTick,
|
|
2723
|
+
SIXTEENTH,
|
|
2724
|
+
Math.max(20, hhVel * 1.1),
|
|
2725
|
+
void 0,
|
|
2726
|
+
entry
|
|
2727
|
+
);
|
|
2554
2728
|
continue;
|
|
2555
2729
|
}
|
|
2556
2730
|
if (allowsOpenHiHat && ctx.beat === 3 && sixteenth === 3) {
|
|
2557
2731
|
const openProb = Math.max(0.1, Math.min(0.4, 30 / ctx.bpm));
|
|
2558
2732
|
if (ctx.draw.prob(openProb, "openHat", ctx.bar, ctx.beat, sixteenth)) {
|
|
2559
|
-
ctx.track.add(GM.OHH, hhTick, SIXTEENTH, Math.max(20, hhVel * 1.2));
|
|
2733
|
+
ctx.track.add(GM.OHH, hhTick, SIXTEENTH, Math.max(20, hhVel * 1.2), void 0, entry);
|
|
2560
2734
|
continue;
|
|
2561
2735
|
}
|
|
2562
2736
|
}
|
|
2563
|
-
ctx.track.add(hhInstrument, hhTick, SIXTEENTH / 2, hhVel);
|
|
2737
|
+
ctx.track.add(hhInstrument, hhTick, SIXTEENTH / 2, hhVel, void 0, entry);
|
|
2564
2738
|
}
|
|
2565
2739
|
}
|
|
2566
2740
|
|
|
@@ -2587,7 +2761,7 @@ function bjorklund(pulses, steps) {
|
|
|
2587
2761
|
}
|
|
2588
2762
|
let head = Array.from({ length: pulses }, () => [true]);
|
|
2589
2763
|
let remainder = Array.from({ length: steps - pulses }, () => [false]);
|
|
2590
|
-
|
|
2764
|
+
do {
|
|
2591
2765
|
const count = Math.min(head.length, remainder.length);
|
|
2592
2766
|
const next = [];
|
|
2593
2767
|
for (let i = 0; i < count; i += 1) {
|
|
@@ -2595,7 +2769,7 @@ function bjorklund(pulses, steps) {
|
|
|
2595
2769
|
}
|
|
2596
2770
|
remainder = head.length > count ? head.slice(count) : remainder.slice(count);
|
|
2597
2771
|
head = next;
|
|
2598
|
-
}
|
|
2772
|
+
} while (remainder.length > 1);
|
|
2599
2773
|
return [...head.flat(), ...remainder.flat()];
|
|
2600
2774
|
}
|
|
2601
2775
|
|
|
@@ -2615,6 +2789,7 @@ var FILL_TYPES = Object.freeze([
|
|
|
2615
2789
|
"flamsAndDrags",
|
|
2616
2790
|
"halfTimeFill"
|
|
2617
2791
|
]);
|
|
2792
|
+
var FILL_VELOCITY_BASES = Object.freeze(["fill", "accent"]);
|
|
2618
2793
|
var S = SIXTEENTH;
|
|
2619
2794
|
var E = EIGHTH;
|
|
2620
2795
|
function fill(scale, offset) {
|
|
@@ -2738,6 +2913,29 @@ var FILL_ARCHETYPES = deepFreeze({
|
|
|
2738
2913
|
function isFillArchetype(material) {
|
|
2739
2914
|
return typeof material === "object" && material !== null && Array.isArray(material.atBeat);
|
|
2740
2915
|
}
|
|
2916
|
+
function assertFillArchetype(material, label) {
|
|
2917
|
+
material.atBeat.forEach((beat, beatIndex) => {
|
|
2918
|
+
const strokes = assertArray(beat, `${label} atBeat[${beatIndex}]`);
|
|
2919
|
+
strokes.forEach((given, strokeIndex) => {
|
|
2920
|
+
const name = `${label} atBeat[${beatIndex}][${strokeIndex}]`;
|
|
2921
|
+
const stroke = assertRecord(given, name);
|
|
2922
|
+
assertMidiPitch(stroke.voice, `${name}.voice`);
|
|
2923
|
+
assertFiniteNumber(stroke.offset, `${name}.offset`);
|
|
2924
|
+
assertFiniteNumber(stroke.duration, `${name}.duration`);
|
|
2925
|
+
const velocity = assertRecord(stroke.velocity, `${name}.velocity`);
|
|
2926
|
+
assertOneOf(velocity.base, FILL_VELOCITY_BASES, `${name}.velocity.base`);
|
|
2927
|
+
if (velocity.scale !== void 0) {
|
|
2928
|
+
assertFiniteNumber(velocity.scale, `${name}.velocity.scale`);
|
|
2929
|
+
}
|
|
2930
|
+
if (velocity.offset !== void 0) {
|
|
2931
|
+
assertRange(velocity.offset, -127, 127, `${name}.velocity.offset`);
|
|
2932
|
+
}
|
|
2933
|
+
if (stroke.articulation !== void 0) {
|
|
2934
|
+
assertOneOf(stroke.articulation, ARTICULATIONS, `${name}.articulation`);
|
|
2935
|
+
}
|
|
2936
|
+
});
|
|
2937
|
+
});
|
|
2938
|
+
}
|
|
2741
2939
|
function fillArchetypeFor(id, supplied) {
|
|
2742
2940
|
return supplied?.get(id) ?? FILL_ARCHETYPES[id];
|
|
2743
2941
|
}
|
|
@@ -2912,6 +3110,8 @@ var HitList = class {
|
|
|
2912
3110
|
hits = [];
|
|
2913
3111
|
/** Voices already struck at each quantized beat. */
|
|
2914
3112
|
#onsets = /* @__PURE__ */ new Map();
|
|
3113
|
+
/** The rhythmic-dial setting each onset enters at, by onset key; absent is always. */
|
|
3114
|
+
#entries = /* @__PURE__ */ new Map();
|
|
2915
3115
|
/**
|
|
2916
3116
|
* Append a hit, clamping velocity and dropping non-positive positions.
|
|
2917
3117
|
*
|
|
@@ -2919,12 +3119,23 @@ var HitList = class {
|
|
|
2919
3119
|
* sounds once however many voices ask for it, and a second entry at the same
|
|
2920
3120
|
* pitch and position is a note-on a reader answers with no note-off, heard as
|
|
2921
3121
|
* a hung or cut stroke rather than as a louder one.
|
|
3122
|
+
*
|
|
3123
|
+
* `entry` is the rhythmic-dial setting above which the stroke sounds, for a
|
|
3124
|
+
* stroke the dial adds; a stroke asked for twice sounds from the lower one.
|
|
2922
3125
|
*/
|
|
2923
|
-
add(pitch, startBeat, durationBeat, velocity, articulation) {
|
|
3126
|
+
add(pitch, startBeat, durationBeat, velocity, articulation, entry = Number.NEGATIVE_INFINITY) {
|
|
2924
3127
|
const beat = Math.max(0, startBeat);
|
|
3128
|
+
const key = onsetKey(pitch, beat);
|
|
3129
|
+
const held = this.#entries.get(key);
|
|
2925
3130
|
if (this.hasOnset(pitch, beat)) {
|
|
3131
|
+
if (held !== void 0 && entry < held) {
|
|
3132
|
+
this.#entries.set(key, entry);
|
|
3133
|
+
}
|
|
2926
3134
|
return;
|
|
2927
3135
|
}
|
|
3136
|
+
if (entry !== Number.NEGATIVE_INFINITY) {
|
|
3137
|
+
this.#entries.set(key, entry);
|
|
3138
|
+
}
|
|
2928
3139
|
const hit2 = {
|
|
2929
3140
|
pitch,
|
|
2930
3141
|
startBeat: beat,
|
|
@@ -2942,6 +3153,13 @@ var HitList = class {
|
|
|
2942
3153
|
this.#onsets.set(beatSlot(beat), /* @__PURE__ */ new Set([pitch]));
|
|
2943
3154
|
}
|
|
2944
3155
|
}
|
|
3156
|
+
/**
|
|
3157
|
+
* The rhythmic-dial setting above which a written stroke sounds: minus
|
|
3158
|
+
* infinity for one that sounds at every setting.
|
|
3159
|
+
*/
|
|
3160
|
+
entryOf(pitch, startBeat) {
|
|
3161
|
+
return this.#entries.get(onsetKey(pitch, startBeat)) ?? Number.NEGATIVE_INFINITY;
|
|
3162
|
+
}
|
|
2945
3163
|
/** Whether a hit of this pitch already starts exactly at `startBeat`. */
|
|
2946
3164
|
hasOnset(pitch, startBeat) {
|
|
2947
3165
|
return this.#onsets.get(beatSlot(startBeat))?.has(pitch) ?? false;
|
|
@@ -3038,17 +3256,14 @@ var KICK_FIGURES = deepFreeze({
|
|
|
3038
3256
|
],
|
|
3039
3257
|
standard: DEFAULT_FIGURE
|
|
3040
3258
|
});
|
|
3041
|
-
function emptyPattern() {
|
|
3042
|
-
return Array.from({ length: KICK_STEPS }, () => false);
|
|
3043
|
-
}
|
|
3044
3259
|
function isInPreChorusLift(section, bar2, sectionBars, nextIsChorus) {
|
|
3045
3260
|
if (section !== "b" || !nextIsChorus || sectionBars < 3) {
|
|
3046
3261
|
return false;
|
|
3047
3262
|
}
|
|
3048
3263
|
return bar2 >= sectionBars - 2;
|
|
3049
3264
|
}
|
|
3050
|
-
function
|
|
3051
|
-
const
|
|
3265
|
+
function realiseKickEntries(figure, section, bar2, draw, rhythmic) {
|
|
3266
|
+
const entries = Array.from({ length: KICK_STEPS }, () => void 0);
|
|
3052
3267
|
for (const slot of figure) {
|
|
3053
3268
|
if (slot.step < 0 || slot.step >= KICK_STEPS) {
|
|
3054
3269
|
continue;
|
|
@@ -3060,19 +3275,23 @@ function realiseKickFigure(figure, section, bar2, draw, rhythmic) {
|
|
|
3060
3275
|
continue;
|
|
3061
3276
|
}
|
|
3062
3277
|
if (slot.probability === void 0) {
|
|
3063
|
-
|
|
3278
|
+
entries[slot.step] = Number.NEGATIVE_INFINITY;
|
|
3064
3279
|
continue;
|
|
3065
3280
|
}
|
|
3066
|
-
|
|
3067
|
-
|
|
3281
|
+
const at2 = draw.at("kick", bar2, slot.slot ?? slot.step);
|
|
3282
|
+
if (at2 < leanedBy(slot.probability, rhythmic)) {
|
|
3283
|
+
entries[slot.step] = Math.min(
|
|
3284
|
+
entries[slot.step] ?? Number.POSITIVE_INFINITY,
|
|
3285
|
+
at2 / slot.probability - 0.5
|
|
3286
|
+
);
|
|
3068
3287
|
}
|
|
3069
3288
|
}
|
|
3070
|
-
return
|
|
3289
|
+
return entries;
|
|
3071
3290
|
}
|
|
3072
|
-
function
|
|
3291
|
+
function getKickEntries(section, style, bar2, draw, rhythmic) {
|
|
3073
3292
|
const own = KICK_FIGURES[style] ?? DEFAULT_FIGURE;
|
|
3074
3293
|
const figure = section === "outro" ? [...OUTRO_FIGURE, ...statedSlots(own)] : own;
|
|
3075
|
-
return
|
|
3294
|
+
return realiseKickEntries(figure, section, bar2, draw, rhythmic);
|
|
3076
3295
|
}
|
|
3077
3296
|
|
|
3078
3297
|
// src/generate/drums/percussion.ts
|
|
@@ -3109,6 +3328,8 @@ var PERC_TABLE = [
|
|
|
3109
3328
|
/* rock */
|
|
3110
3329
|
[F, F, F, [false, false, true], F, F, F, F, [false, false, true]]
|
|
3111
3330
|
];
|
|
3331
|
+
var SHAKER_SIXTEENTH_CURVE = [0.75, 0.45, 0.6, 0.45];
|
|
3332
|
+
var SHAKER_EIGHTH_CURVE = [0.75, 0.55];
|
|
3112
3333
|
function getPercussionConfig(mood, section) {
|
|
3113
3334
|
const row = PERC_TABLE[mood] ?? PERC_TABLE[1 /* Standard */];
|
|
3114
3335
|
const act = row?.[sectionIndex(section)] ?? F;
|
|
@@ -3133,10 +3354,11 @@ function generateAuxPercussionForBar(track, barStart, config, role, densityMult,
|
|
|
3133
3354
|
if (config.shaker) {
|
|
3134
3355
|
const use16th = config.shaker16th && bpm < HH_16TH_BPM_THRESHOLD && sustainsStrokes(SIXTEENTH, bpm, sec.difficulty);
|
|
3135
3356
|
if (use16th) {
|
|
3136
|
-
const velCurve =
|
|
3357
|
+
const velCurve = SHAKER_SIXTEENTH_CURVE;
|
|
3137
3358
|
for (let beat = 0; beat < barBeats; beat += 1) {
|
|
3138
3359
|
for (let sub = 0; sub < 4; sub += 1) {
|
|
3139
|
-
|
|
3360
|
+
const entry = sub % 2 === 1 ? draw.at("shaker16", barStart, beat, sub) : Number.NEGATIVE_INFINITY;
|
|
3361
|
+
if (!(entry < sec.rhythmic)) {
|
|
3140
3362
|
continue;
|
|
3141
3363
|
}
|
|
3142
3364
|
const raw = 80 * (velCurve[sub] ?? 0.5) * densityMult * draw.float(0.9, 1.1, "shaker", barStart, beat, sub);
|
|
@@ -3144,12 +3366,14 @@ function generateAuxPercussionForBar(track, barStart, config, role, densityMult,
|
|
|
3144
3366
|
GM.SHAKER,
|
|
3145
3367
|
swing16(barStart + beat + sub * SIXTEENTH, sec, swingAmount),
|
|
3146
3368
|
SIXTEENTH,
|
|
3147
|
-
Math.max(25, Math.min(85, raw))
|
|
3369
|
+
Math.max(25, Math.min(85, raw)),
|
|
3370
|
+
void 0,
|
|
3371
|
+
entry
|
|
3148
3372
|
);
|
|
3149
3373
|
}
|
|
3150
3374
|
}
|
|
3151
3375
|
} else {
|
|
3152
|
-
const velCurve =
|
|
3376
|
+
const velCurve = SHAKER_EIGHTH_CURVE;
|
|
3153
3377
|
for (let beat = 0; beat < barBeats; beat += 1) {
|
|
3154
3378
|
for (let sub = 0; sub < 2; sub += 1) {
|
|
3155
3379
|
const raw = 80 * (velCurve[sub] ?? 0.6) * densityMult * draw.float(0.9, 1.1, "shaker", barStart, beat, sub);
|
|
@@ -3435,6 +3659,7 @@ function placeDrumPattern(opts) {
|
|
|
3435
3659
|
assertRange(opts.velocity, 1, 127, "drum pattern velocity");
|
|
3436
3660
|
}
|
|
3437
3661
|
const feel = opts.feel === void 0 ? "straight" : assertOneOf(opts.feel, DRUM_FEELS, "feel");
|
|
3662
|
+
const rate = assertOneOf(opts.rate ?? "straight", FIGURE_RATES, "rate");
|
|
3438
3663
|
const resolved = resolveContext(opts.ctx);
|
|
3439
3664
|
const bpm = resolved.bpm ?? DEFAULT_BPM2;
|
|
3440
3665
|
const draw = resolved.part("drums");
|
|
@@ -3480,7 +3705,7 @@ function placeDrumPattern(opts) {
|
|
|
3480
3705
|
...resolved.rhythmic === void 0 ? {} : { rhythmic: resolved.rhythmic },
|
|
3481
3706
|
...resolved.ornament === void 0 ? {} : { ornament: resolved.ornament },
|
|
3482
3707
|
isOrnament: (stroke) => stroke.articulation === "ghost",
|
|
3483
|
-
rate
|
|
3708
|
+
rate,
|
|
3484
3709
|
spanSteps: entry.material.steps,
|
|
3485
3710
|
// The dials rank the figure's positions against the bar it is written
|
|
3486
3711
|
// in, which is the bar this call is placing it in.
|
|
@@ -3567,6 +3792,7 @@ function generateDrums(opts) {
|
|
|
3567
3792
|
densityMult,
|
|
3568
3793
|
rhythmic,
|
|
3569
3794
|
ornament: ornamentDial,
|
|
3795
|
+
ornamentFollowsRhythmic: resolved.ornament === void 0,
|
|
3570
3796
|
difficulty,
|
|
3571
3797
|
ghostBoost: mapping.ghostBoost,
|
|
3572
3798
|
useRide: shouldUseRideForSection(section, style),
|
|
@@ -3582,10 +3808,15 @@ function generateDrums(opts) {
|
|
|
3582
3808
|
const percMood = percMoodCategory(style);
|
|
3583
3809
|
const nextSection = opts.nextSection ? mapSection(opts.nextSection) : section;
|
|
3584
3810
|
const nextEnergy = sectionEnergy(nextSection);
|
|
3585
|
-
const
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
);
|
|
3811
|
+
const suppliedFills = vocabularyOfKind(resolved.vocabulary, isFillArchetype);
|
|
3812
|
+
suppliedFills.forEach((entry, index) => {
|
|
3813
|
+
assertFillArchetype(entry.material, `drum fill vocabulary[${index}]`);
|
|
3814
|
+
});
|
|
3815
|
+
const callerFillEntries = selectVocabulary(suppliedFills, {
|
|
3816
|
+
section: publicSection,
|
|
3817
|
+
bpm,
|
|
3818
|
+
difficulty
|
|
3819
|
+
});
|
|
3589
3820
|
const callerFills = new Map(callerFillEntries.map((entry) => [entry.id, entry.material]));
|
|
3590
3821
|
const callerFillIds = callerFillEntries.map((entry) => entry.id);
|
|
3591
3822
|
const euclidSteps = opts.euclideanKick?.steps ?? 16;
|
|
@@ -3596,7 +3827,7 @@ function generateDrums(opts) {
|
|
|
3596
3827
|
for (let bar2 = 0; bar2 < opts.bars; bar2 += 1) {
|
|
3597
3828
|
const barStart = bar2 * barBeats;
|
|
3598
3829
|
const isLastBar = bar2 === opts.bars - 1;
|
|
3599
|
-
const
|
|
3830
|
+
const hiHat = getHiHatBar(section, style, rhythmic, bpm, draw, difficulty, bar2);
|
|
3600
3831
|
if (bar2 === 0 && section === "chorus") {
|
|
3601
3832
|
track.add(GM.CRASH, barStart, 0.5, 100 * densityMult);
|
|
3602
3833
|
}
|
|
@@ -3608,10 +3839,10 @@ function generateDrums(opts) {
|
|
|
3608
3839
|
}
|
|
3609
3840
|
let kick;
|
|
3610
3841
|
if (!euclidKick && reuseSectionKick) {
|
|
3611
|
-
sectionKick ??=
|
|
3842
|
+
sectionKick ??= getKickEntries(section, style, 0, draw, rhythmic);
|
|
3612
3843
|
kick = sectionKick;
|
|
3613
3844
|
} else if (!euclidKick) {
|
|
3614
|
-
kick =
|
|
3845
|
+
kick = getKickEntries(section, style, bar2, draw, rhythmic);
|
|
3615
3846
|
}
|
|
3616
3847
|
const inLift = isInPreChorusLift(section, bar2, opts.bars, nextSection === "chorus");
|
|
3617
3848
|
let currentFill = FILL_ARCHETYPES.snareRoll;
|
|
@@ -3667,7 +3898,8 @@ function generateDrums(opts) {
|
|
|
3667
3898
|
swingAmount,
|
|
3668
3899
|
barHasOpenHh,
|
|
3669
3900
|
openHhBeat: openHhBeatIndex,
|
|
3670
|
-
hhLevel,
|
|
3901
|
+
hhLevel: hiHat.level,
|
|
3902
|
+
hhEntries: hiHat.entries,
|
|
3671
3903
|
draw
|
|
3672
3904
|
};
|
|
3673
3905
|
if (playMainVoices) {
|
|
@@ -3728,24 +3960,44 @@ function generateDrums(opts) {
|
|
|
3728
3960
|
const endBeat = opts.bars * barBeats;
|
|
3729
3961
|
const kit = resolved.instrument("drums");
|
|
3730
3962
|
const written = track.hits.filter((hit2) => hit2.startBeat < endBeat && playableOn(kit, hit2.pitch)).sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);
|
|
3731
|
-
return underCeiling(written, bpm, difficulty, exemptOnsets);
|
|
3963
|
+
return underCeiling(written, bpm, difficulty, exemptOnsets, track);
|
|
3732
3964
|
}
|
|
3733
|
-
function underCeiling(hits, bpm, difficulty, exempt) {
|
|
3965
|
+
function underCeiling(hits, bpm, difficulty, exempt, track) {
|
|
3734
3966
|
if (difficulty === void 0) {
|
|
3735
3967
|
return [...hits];
|
|
3736
3968
|
}
|
|
3737
|
-
const
|
|
3738
|
-
const
|
|
3739
|
-
|
|
3740
|
-
const
|
|
3741
|
-
|
|
3969
|
+
const entries = hits.map((hit2) => track.entryOf(hit2.pitch, hit2.startBeat));
|
|
3970
|
+
const order = hits.map((_, index) => index).sort((a, b) => {
|
|
3971
|
+
const left = entries[a];
|
|
3972
|
+
const right = entries[b];
|
|
3973
|
+
return left < right ? -1 : left > right ? 1 : a - b;
|
|
3974
|
+
});
|
|
3975
|
+
const keptBeats = /* @__PURE__ */ new Map();
|
|
3976
|
+
const kept = hits.map(() => false);
|
|
3977
|
+
for (const index of order) {
|
|
3978
|
+
const hit2 = hits[index];
|
|
3979
|
+
const beats = keptBeats.get(hit2.pitch) ?? [];
|
|
3980
|
+
let at2 = 0;
|
|
3981
|
+
let end = beats.length;
|
|
3982
|
+
while (at2 < end) {
|
|
3983
|
+
const mid = at2 + end >>> 1;
|
|
3984
|
+
if (beats[mid] > hit2.startBeat) {
|
|
3985
|
+
end = mid;
|
|
3986
|
+
} else {
|
|
3987
|
+
at2 = mid + 1;
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
const previous = beats[at2 - 1];
|
|
3991
|
+
const next = beats[at2];
|
|
3992
|
+
const reachable = exempt.has(onsetKey(hit2.pitch, hit2.startBeat)) || (previous === void 0 || sustainsStrokes(hit2.startBeat - previous, bpm, difficulty)) && (next === void 0 || sustainsStrokes(next - hit2.startBeat, bpm, difficulty));
|
|
3742
3993
|
if (!reachable) {
|
|
3743
3994
|
continue;
|
|
3744
3995
|
}
|
|
3745
|
-
kept
|
|
3746
|
-
|
|
3996
|
+
kept[index] = true;
|
|
3997
|
+
beats.splice(at2, 0, hit2.startBeat);
|
|
3998
|
+
keptBeats.set(hit2.pitch, beats);
|
|
3747
3999
|
}
|
|
3748
|
-
return kept;
|
|
4000
|
+
return hits.filter((_, index) => kept[index]);
|
|
3749
4001
|
}
|
|
3750
4002
|
function playableOn(kit, pitch) {
|
|
3751
4003
|
return kit === void 0 || canSound(kit, pitch);
|
|
@@ -3764,7 +4016,7 @@ var MIN_VELOCITY = 1;
|
|
|
3764
4016
|
var MAX_VELOCITY = 127;
|
|
3765
4017
|
function humanize(events, opts = {}) {
|
|
3766
4018
|
const asked = assertOptions(opts, "opts");
|
|
3767
|
-
const
|
|
4019
|
+
const meter = toMeterData(asked.ts ?? DEFAULT_TS3, "ts");
|
|
3768
4020
|
assertNoteEvents(events, "humanize events", { allowNonPositiveDuration: true });
|
|
3769
4021
|
const sounding2 = dropSilentNotes(events);
|
|
3770
4022
|
const timing = asked.timing ?? DEFAULT_TIMING;
|
|
@@ -3780,7 +4032,7 @@ function humanize(events, opts = {}) {
|
|
|
3780
4032
|
return sounding2.map((event) => {
|
|
3781
4033
|
const timingOffset = draw.float(-timing, timing, "timing", event.startBeat, event.pitch);
|
|
3782
4034
|
const startBeat = event.startBeat + timingOffset;
|
|
3783
|
-
const weight = metricWeight(event.startBeat,
|
|
4035
|
+
const weight = metricWeight(event.startBeat, meter);
|
|
3784
4036
|
const accentBoost = weight / MAX_METRIC_WEIGHT * accent2;
|
|
3785
4037
|
const velocityOffset = draw.float(
|
|
3786
4038
|
-velocityJitter,
|
|
@@ -3794,6 +4046,21 @@ function humanize(events, opts = {}) {
|
|
|
3794
4046
|
return { ...event, startBeat, velocity };
|
|
3795
4047
|
});
|
|
3796
4048
|
}
|
|
4049
|
+
function grooveSignature(ts) {
|
|
4050
|
+
const meter = toMeterData(ts, "ts");
|
|
4051
|
+
if (!Array.isArray(meter)) {
|
|
4052
|
+
return meter;
|
|
4053
|
+
}
|
|
4054
|
+
const opening = meterAt(0, meter);
|
|
4055
|
+
for (const change of meter) {
|
|
4056
|
+
if (change.ts.numerator !== opening.numerator || change.ts.denominator !== opening.denominator) {
|
|
4057
|
+
throw new InvalidInputError(
|
|
4058
|
+
`groove time signature must be one signature throughout; received ${change.ts.numerator}/${change.ts.denominator} at beat ${change.startBeat} in a meter that is ${opening.numerator}/${opening.denominator} at beat 0`
|
|
4059
|
+
);
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
4062
|
+
return opening;
|
|
4063
|
+
}
|
|
3797
4064
|
function quantizeToGrid(beat, barBeats, subdivision, slotsPerBar) {
|
|
3798
4065
|
const barIndex = Math.floor(beat / barBeats);
|
|
3799
4066
|
const modBeat = beat - barIndex * barBeats;
|
|
@@ -3809,7 +4076,7 @@ function quantizeToGrid(beat, barBeats, subdivision, slotsPerBar) {
|
|
|
3809
4076
|
};
|
|
3810
4077
|
}
|
|
3811
4078
|
function extractGrooveTemplate(events, ts, subdivision = DEFAULT_GROOVE_SUBDIVISION) {
|
|
3812
|
-
const meter =
|
|
4079
|
+
const meter = grooveSignature(ts);
|
|
3813
4080
|
assertNoteEvents(events, "groove source events", { allowNonPositiveDuration: true });
|
|
3814
4081
|
assertPositiveInt(subdivision, "groove subdivision");
|
|
3815
4082
|
const barBeats = beatsPerBar(meter);
|
|
@@ -3850,7 +4117,7 @@ function extractGrooveTemplate(events, ts, subdivision = DEFAULT_GROOVE_SUBDIVIS
|
|
|
3850
4117
|
return { subdivision, slotsPerBar, slots, ts: meter };
|
|
3851
4118
|
}
|
|
3852
4119
|
function applyGrooveTemplate(events, template, ts) {
|
|
3853
|
-
const meter =
|
|
4120
|
+
const meter = grooveSignature(ts);
|
|
3854
4121
|
assertNoteEvents(events, "groove target events", { allowNonPositiveDuration: true });
|
|
3855
4122
|
assertPositiveInt(template.subdivision, "template subdivision");
|
|
3856
4123
|
assertPositiveInt(template.slotsPerBar, "template slotsPerBar");
|
|
@@ -3914,15 +4181,27 @@ function classifyMelodyTones(melody, ts) {
|
|
|
3914
4181
|
const meter = meterAt(0, toMeterData(ts, "ts"));
|
|
3915
4182
|
assertNoteEvents(melody, "melody notes", { allowNonPositiveDuration: true });
|
|
3916
4183
|
const pulse2 = pulseBeats(meter);
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
4184
|
+
const sounding2 = new Set(soundingIndices(melody));
|
|
4185
|
+
const line = canonicalNoteOrder(melody).filter((index) => sounding2.has(index));
|
|
4186
|
+
const roles = melody.map(() => "structural");
|
|
4187
|
+
line.forEach((index, position) => {
|
|
4188
|
+
const before = line[position - 1];
|
|
4189
|
+
const after = line[position + 1];
|
|
4190
|
+
roles[index] = roleOfNote(
|
|
4191
|
+
before === void 0 ? void 0 : melody[before],
|
|
4192
|
+
melody[index],
|
|
4193
|
+
after === void 0 ? void 0 : melody[after],
|
|
4194
|
+
meter,
|
|
4195
|
+
pulse2
|
|
4196
|
+
);
|
|
3920
4197
|
});
|
|
4198
|
+
return roles.map((role, index) => ({
|
|
4199
|
+
noteIndex: index,
|
|
4200
|
+
role,
|
|
4201
|
+
ornamental: role !== "structural"
|
|
4202
|
+
}));
|
|
3921
4203
|
}
|
|
3922
|
-
function roleOfNote(
|
|
3923
|
-
const note2 = melody[index];
|
|
3924
|
-
const prev = melody[index - 1];
|
|
3925
|
-
const next = melody[index + 1];
|
|
4204
|
+
function roleOfNote(prev, note2, next, ts, pulse2) {
|
|
3926
4205
|
if (!note2 || !prev || !next) {
|
|
3927
4206
|
return "structural";
|
|
3928
4207
|
}
|
|
@@ -3978,13 +4257,11 @@ var DEFAULT_PLACEMENT = {
|
|
|
3978
4257
|
};
|
|
3979
4258
|
var DEFAULT_METER = { numerator: 4, denominator: 4 };
|
|
3980
4259
|
var TIE_BREAK_JITTER2 = 1e-6;
|
|
3981
|
-
function cadencesThroughRaisedSeventh(key) {
|
|
3982
|
-
return isMinorKey(key) && scaleSystemOf(key) === "common-practice";
|
|
3983
|
-
}
|
|
3984
4260
|
function isKeyTone(pitch, key) {
|
|
3985
4261
|
return isScaleTone(pitch, key) || cadencesThroughRaisedSeventh(key) && pitchClassOf(pitch) === leadingTonePcOf(key);
|
|
3986
4262
|
}
|
|
3987
4263
|
function inferKey(melody) {
|
|
4264
|
+
const line = inOnsetOrder(melody);
|
|
3988
4265
|
let best = majorKey(0);
|
|
3989
4266
|
let bestScore = Number.NEGATIVE_INFINITY;
|
|
3990
4267
|
const candidates = [];
|
|
@@ -3994,7 +4271,7 @@ function inferKey(melody) {
|
|
|
3994
4271
|
for (const key of candidates) {
|
|
3995
4272
|
const tonic = pitchClassOf(key.rootPc);
|
|
3996
4273
|
let score = 0;
|
|
3997
|
-
for (const n of
|
|
4274
|
+
for (const n of line) {
|
|
3998
4275
|
const w = Math.max(0.25, n.durationBeat);
|
|
3999
4276
|
const pc = pitchClassOf(n.pitch);
|
|
4000
4277
|
if (isKeyTone(n.pitch, key)) {
|
|
@@ -4011,6 +4288,12 @@ function inferKey(melody) {
|
|
|
4011
4288
|
}
|
|
4012
4289
|
return best;
|
|
4013
4290
|
}
|
|
4291
|
+
function inOnsetOrder(melody) {
|
|
4292
|
+
return canonicalNoteOrder(melody).flatMap((index) => {
|
|
4293
|
+
const note2 = melody[index];
|
|
4294
|
+
return note2 === void 0 ? [] : [note2];
|
|
4295
|
+
});
|
|
4296
|
+
}
|
|
4014
4297
|
|
|
4015
4298
|
// src/generate/harmonize/cost.ts
|
|
4016
4299
|
var NON_CHORD_TONE_STRONG = 4;
|
|
@@ -4307,15 +4590,23 @@ function buildSegments(spans, melody, ts, grid, phraseEnds, budget) {
|
|
|
4307
4590
|
}
|
|
4308
4591
|
|
|
4309
4592
|
// src/generate/harmonize/candidates.ts
|
|
4310
|
-
var ROOT_WEIGHT_BY_SEMITONE = [
|
|
4593
|
+
var ROOT_WEIGHT_BY_SEMITONE = [
|
|
4594
|
+
0,
|
|
4595
|
+
0.9,
|
|
4596
|
+
0.3,
|
|
4597
|
+
0.45,
|
|
4598
|
+
0.45,
|
|
4599
|
+
0.15,
|
|
4600
|
+
0.9,
|
|
4601
|
+
0.1,
|
|
4602
|
+
0.3,
|
|
4603
|
+
0.3,
|
|
4604
|
+
0.3,
|
|
4605
|
+
0.9
|
|
4606
|
+
];
|
|
4311
4607
|
var UNSTABLE_TRIAD = 0.9;
|
|
4312
|
-
var
|
|
4313
|
-
|
|
4314
|
-
"aug",
|
|
4315
|
-
"dim7",
|
|
4316
|
-
"m7b5",
|
|
4317
|
-
"aug7"
|
|
4318
|
-
]);
|
|
4608
|
+
var UNSTABLE_QUALITY_NAMES = ["dim", "aug", "dim7", "m7b5", "aug7"];
|
|
4609
|
+
var UNSTABLE_QUALITIES = new Set(UNSTABLE_QUALITY_NAMES);
|
|
4319
4610
|
function vocabularyBase(rootPc, quality, tonicPc) {
|
|
4320
4611
|
const above = (pitchClassOf(rootPc) - tonicPc + 12) % 12;
|
|
4321
4612
|
const root = ROOT_WEIGHT_BY_SEMITONE[above] ?? UNSTABLE_TRIAD;
|
|
@@ -4329,7 +4620,7 @@ function secondaryDominantTargets(key) {
|
|
|
4329
4620
|
tonicizableDegrees(key).map((degree) => [degree.degreeNumber, degree])
|
|
4330
4621
|
);
|
|
4331
4622
|
return SECONDARY_DOMINANT_TARGETS.map((degree) => tonicizable.get(degree)).filter(
|
|
4332
|
-
(degree) => degree !== void 0
|
|
4623
|
+
(degree) => degree !== void 0 && !isNativeDominantSeventh(degree.rootPc + 7, key)
|
|
4333
4624
|
);
|
|
4334
4625
|
}
|
|
4335
4626
|
var SECONDARY_DOMINANT_ENTRY_ORDER = [5, 6, 2, 4];
|
|
@@ -4344,13 +4635,14 @@ var REHARMONIZE_DIAL = {
|
|
|
4344
4635
|
function admittedCount(fraction, size) {
|
|
4345
4636
|
return Math.max(0, Math.min(size, Math.ceil(fraction * size)));
|
|
4346
4637
|
}
|
|
4347
|
-
function buildCandidates(
|
|
4638
|
+
function buildCandidates(keyLike, harmonic) {
|
|
4639
|
+
const { scale: key, tonic } = resolveKey(keyLike);
|
|
4348
4640
|
const tonicPc = pitchClassOf(key.rootPc);
|
|
4349
4641
|
const frame = heptatonicFrameOf(key);
|
|
4350
4642
|
const tones = scaleTonesInDegreeOrder(frame);
|
|
4351
4643
|
const candidates = tones.map((rootPc, index) => {
|
|
4352
4644
|
const degree = index + 1;
|
|
4353
|
-
const quality = diatonicTriad(degree, frame).quality;
|
|
4645
|
+
const quality = diatonicTriad(degree, { ...frame, tonic }).quality;
|
|
4354
4646
|
return {
|
|
4355
4647
|
rootPc,
|
|
4356
4648
|
quality,
|
|
@@ -4394,7 +4686,7 @@ function buildCandidates(key, harmonic) {
|
|
|
4394
4686
|
const parallelTones = scaleTonesInDegreeOrder(parallel);
|
|
4395
4687
|
const borrowed = [];
|
|
4396
4688
|
for (let degree = 1; degree <= parallelTones.length; degree += 1) {
|
|
4397
|
-
const chord = diatonicTriad(degree, parallel);
|
|
4689
|
+
const chord = diatonicTriad(degree, { ...parallel, tonic });
|
|
4398
4690
|
if (!isDiatonic(chord, key)) {
|
|
4399
4691
|
borrowed.push(chord);
|
|
4400
4692
|
}
|
|
@@ -4440,11 +4732,11 @@ function harmonizeMelody(opts) {
|
|
|
4440
4732
|
allowNonPositiveDuration: true,
|
|
4441
4733
|
budget: opts.budget
|
|
4442
4734
|
});
|
|
4443
|
-
const phraseEnds = opts.phraseEnds ?? [];
|
|
4735
|
+
const phraseEnds = assertArray(opts.phraseEnds ?? [], "harmonize phrase ends");
|
|
4444
4736
|
for (const end of phraseEnds) {
|
|
4445
4737
|
assertFiniteNumber(end, "harmonize phrase end");
|
|
4446
4738
|
}
|
|
4447
|
-
const soundingMelody = soundingNotesOnly(opts.melody);
|
|
4739
|
+
const soundingMelody = inOnsetOrder(soundingNotesOnly(opts.melody));
|
|
4448
4740
|
const ts = meterAt(0, toMeterData(opts.ts ?? DEFAULT_METER, "ts"));
|
|
4449
4741
|
assertTimeSignature(ts);
|
|
4450
4742
|
const requestedKey = opts.key ?? "infer";
|
|
@@ -4452,7 +4744,10 @@ function harmonizeMelody(opts) {
|
|
|
4452
4744
|
requestedKey === "infer" ? inferKey(soundingMelody) : requestedKey
|
|
4453
4745
|
);
|
|
4454
4746
|
const key = resolvedKey.scale;
|
|
4455
|
-
const placement =
|
|
4747
|
+
const placement = assertRecord(
|
|
4748
|
+
opts.placement ?? DEFAULT_PLACEMENT,
|
|
4749
|
+
"harmonize placement"
|
|
4750
|
+
);
|
|
4456
4751
|
const ctx = resolveContext(opts.ctx);
|
|
4457
4752
|
const reharmonize = assertOneOf(
|
|
4458
4753
|
opts.reharmonize ?? "diatonic",
|
|
@@ -4463,7 +4758,7 @@ function harmonizeMelody(opts) {
|
|
|
4463
4758
|
if (soundingMelody.length === 0) {
|
|
4464
4759
|
return { transposeSemitones: 0, key: resolvedKey, chords: [], melodyRoles: [] };
|
|
4465
4760
|
}
|
|
4466
|
-
const candidates = buildCandidates(
|
|
4761
|
+
const candidates = buildCandidates(resolvedKey, harmonic);
|
|
4467
4762
|
const candAt = (i) => candidates[i] ?? FALLBACK;
|
|
4468
4763
|
const draw = ctx.part("harmony");
|
|
4469
4764
|
const jitter = candidates.map((_, index) => draw.at("jitter", index) * TIE_BREAK_JITTER2);
|
|
@@ -4514,6 +4809,7 @@ function harmonizeMelody(opts) {
|
|
|
4514
4809
|
"harmonization placement search",
|
|
4515
4810
|
opts.budget
|
|
4516
4811
|
);
|
|
4812
|
+
let bestFit = Number.POSITIVE_INFINITY;
|
|
4517
4813
|
let bestCost = Number.POSITIVE_INFINITY;
|
|
4518
4814
|
let bestTs = 0;
|
|
4519
4815
|
let bestPath = [];
|
|
@@ -4528,8 +4824,10 @@ function harmonizeMelody(opts) {
|
|
|
4528
4824
|
closingSegments,
|
|
4529
4825
|
articulatedSegments
|
|
4530
4826
|
);
|
|
4531
|
-
const
|
|
4532
|
-
|
|
4827
|
+
const fit = keyFitCost(shifted, key);
|
|
4828
|
+
const total = cost + tessituraCost(shifted);
|
|
4829
|
+
if (fit < bestFit || fit === bestFit && total < bestCost) {
|
|
4830
|
+
bestFit = fit;
|
|
4533
4831
|
bestCost = total;
|
|
4534
4832
|
bestTs = shift;
|
|
4535
4833
|
bestPath = path;
|
|
@@ -4664,7 +4962,11 @@ function contourOffsets(contour, count) {
|
|
|
4664
4962
|
}
|
|
4665
4963
|
function generateMotif(opts) {
|
|
4666
4964
|
assertRecord(opts, "motif options");
|
|
4667
|
-
const contour =
|
|
4965
|
+
const contour = assertOneOf(
|
|
4966
|
+
opts.contour ?? "arch",
|
|
4967
|
+
["arch", "ascending", "descending", "wave"],
|
|
4968
|
+
"motif contour"
|
|
4969
|
+
);
|
|
4668
4970
|
const bars = assertPositiveInt(opts.bars, "motif bars");
|
|
4669
4971
|
const barBeats = beatsPerBar(meterAt(0, toMeterData(opts.ts ?? DEFAULT_TS4, "ts")));
|
|
4670
4972
|
const totalBeats = bars * barBeats;
|
|
@@ -4811,6 +5113,10 @@ function developMotif(cell, timeline, key, bars, ts) {
|
|
|
4811
5113
|
if (span <= 0) {
|
|
4812
5114
|
return inTimeOrder(cell);
|
|
4813
5115
|
}
|
|
5116
|
+
const line = canonicalNoteOrder(cellNotes).flatMap((index) => {
|
|
5117
|
+
const note2 = cellNotes[index];
|
|
5118
|
+
return note2 === void 0 ? [] : [note2];
|
|
5119
|
+
});
|
|
4814
5120
|
const tileSpan = Math.max(span, MIN_TILE_SPAN);
|
|
4815
5121
|
const tileCount = Math.ceil(totalBeats / tileSpan);
|
|
4816
5122
|
assertGenerationBudget(tileCount * cellNotes.length, "developed motif notes");
|
|
@@ -4818,7 +5124,7 @@ function developMotif(cell, timeline, key, bars, ts) {
|
|
|
4818
5124
|
const offset = k * tileSpan;
|
|
4819
5125
|
const taken = /* @__PURE__ */ new Map();
|
|
4820
5126
|
let previous;
|
|
4821
|
-
for (const n of
|
|
5127
|
+
for (const n of line) {
|
|
4822
5128
|
const startBeat = offset + (n.startBeat - origin);
|
|
4823
5129
|
if (startBeat >= totalBeats) {
|
|
4824
5130
|
continue;
|
|
@@ -4831,153 +5137,2143 @@ function developMotif(cell, timeline, key, bars, ts) {
|
|
|
4831
5137
|
const pitch = developedPitch(n.pitch, allows, direction, taken);
|
|
4832
5138
|
taken.set(pitch, n.pitch);
|
|
4833
5139
|
previous = n.pitch;
|
|
4834
|
-
out.push({
|
|
5140
|
+
out.push({
|
|
5141
|
+
pitch,
|
|
5142
|
+
startBeat,
|
|
5143
|
+
durationBeat: Math.min(n.durationBeat, totalBeats - startBeat)
|
|
5144
|
+
});
|
|
4835
5145
|
}
|
|
4836
5146
|
}
|
|
4837
5147
|
return inTimeOrder({ notes: out });
|
|
4838
5148
|
}
|
|
4839
5149
|
|
|
4840
|
-
// src/generate/
|
|
4841
|
-
var
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
var SLIDE_MIN_SEMITONES = 1;
|
|
4851
|
-
function eligible(style, note2, previous, next, ts) {
|
|
4852
|
-
const strong = isStrongBeat(note2.startBeat, ts);
|
|
4853
|
-
switch (style) {
|
|
4854
|
-
case "ghost":
|
|
4855
|
-
return !strong;
|
|
4856
|
-
case "flam":
|
|
4857
|
-
case "accent":
|
|
4858
|
-
return strong;
|
|
4859
|
-
case "drag":
|
|
4860
|
-
return !strong && next !== void 0 && isStrongBeat(next.startBeat, ts);
|
|
4861
|
-
case "slide":
|
|
4862
|
-
return previous !== void 0 && Math.abs(note2.pitch - previous.pitch) >= SLIDE_MIN_SEMITONES;
|
|
5150
|
+
// src/generate/plan/internal.ts
|
|
5151
|
+
var STRONG_WEIGHT = 2;
|
|
5152
|
+
function positionClass(beat, meters) {
|
|
5153
|
+
const weight = metricWeight(beat, meters);
|
|
5154
|
+
return weight >= STRONG_WEIGHT ? "strong" : weight > 0 ? "weak" : "off";
|
|
5155
|
+
}
|
|
5156
|
+
function finalDownbeat(phrase, meters) {
|
|
5157
|
+
let downbeat = barStartBeat(phrase.startBeat, meters);
|
|
5158
|
+
while (downbeat + beatsPerBarAt(downbeat, meters) < phrase.endBeat - BEAT_EPS) {
|
|
5159
|
+
downbeat += beatsPerBarAt(downbeat, meters);
|
|
4863
5160
|
}
|
|
5161
|
+
return Math.max(downbeat, phrase.startBeat);
|
|
4864
5162
|
}
|
|
4865
|
-
|
|
4866
|
-
|
|
5163
|
+
|
|
5164
|
+
// src/generate/plan/types.ts
|
|
5165
|
+
var COMPOSITION_PLAN_VERSION = 1;
|
|
5166
|
+
function tent(t, apex) {
|
|
5167
|
+
if (t <= apex) {
|
|
5168
|
+
return apex === 0 ? 1 : -1 + 2 * t / apex;
|
|
5169
|
+
}
|
|
5170
|
+
return apex === 1 ? 1 : 1 - 2 * (t - apex) / (1 - apex);
|
|
5171
|
+
}
|
|
5172
|
+
function plannedContourAt(shape, peakPosition, t) {
|
|
5173
|
+
switch (shape) {
|
|
5174
|
+
case "arch":
|
|
5175
|
+
return tent(t, peakPosition);
|
|
5176
|
+
case "ascending":
|
|
5177
|
+
return -1 + 2 * t;
|
|
5178
|
+
case "descending":
|
|
5179
|
+
return 1 - 2 * t;
|
|
5180
|
+
case "wave":
|
|
5181
|
+
return t < 0.5 ? tent(2 * t, 0.5) : tent(2 * t - 1, 0.5);
|
|
5182
|
+
case "static":
|
|
5183
|
+
return 0;
|
|
5184
|
+
}
|
|
4867
5185
|
}
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
5186
|
+
|
|
5187
|
+
// src/generate/plan/validate.ts
|
|
5188
|
+
var MASS_TOLERANCE = 1e-9;
|
|
5189
|
+
function assertString(value, name) {
|
|
5190
|
+
if (typeof value !== "string") {
|
|
5191
|
+
throw new InvalidInputError(`${name} must be a string; received ${describeRejected(value)}`);
|
|
4871
5192
|
}
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
5193
|
+
return value;
|
|
5194
|
+
}
|
|
5195
|
+
function assertSpanOrder(startBeat, endBeat, name) {
|
|
5196
|
+
if (endBeat < startBeat) {
|
|
5197
|
+
throw new InvalidInputError(
|
|
5198
|
+
`${name}.endBeat must be at least ${name}.startBeat; received ${endBeat} < ${startBeat}`
|
|
5199
|
+
);
|
|
4875
5200
|
}
|
|
4876
|
-
|
|
4877
|
-
|
|
5201
|
+
}
|
|
5202
|
+
function assertNullable(value, name, validate) {
|
|
5203
|
+
if (value === void 0) {
|
|
5204
|
+
throw new InvalidInputError(`${name} must be present; received undefined`);
|
|
4878
5205
|
}
|
|
4879
|
-
return
|
|
5206
|
+
return value === null ? null : validate(value, name);
|
|
4880
5207
|
}
|
|
4881
|
-
function
|
|
4882
|
-
|
|
5208
|
+
function assertMassDistribution(value, length, name) {
|
|
5209
|
+
const shares = assertArray(value, name);
|
|
5210
|
+
if (shares.length !== length) {
|
|
5211
|
+
throw new InvalidInputError(`${name} must have length ${length}; received ${shares.length}`);
|
|
5212
|
+
}
|
|
5213
|
+
let sum = 0;
|
|
5214
|
+
for (let index = 0; index < shares.length; index += 1) {
|
|
5215
|
+
const share = assertFiniteNumber(shares[index], `${name}[${index}]`);
|
|
5216
|
+
if (share < 0) {
|
|
5217
|
+
throw new InvalidInputError(`${name}[${index}] must be at least 0; received ${share}`);
|
|
5218
|
+
}
|
|
5219
|
+
sum += share;
|
|
5220
|
+
}
|
|
5221
|
+
if (Math.abs(sum - 1) > MASS_TOLERANCE) {
|
|
5222
|
+
throw new InvalidInputError(`${name} must sum to 1; received ${sum}`);
|
|
5223
|
+
}
|
|
5224
|
+
return shares;
|
|
4883
5225
|
}
|
|
4884
|
-
function
|
|
4885
|
-
const
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
5226
|
+
function assertResolvedKeyField(value, name) {
|
|
5227
|
+
const read = assertRecord(value, name);
|
|
5228
|
+
try {
|
|
5229
|
+
resolveKey(read);
|
|
5230
|
+
} catch (error) {
|
|
5231
|
+
if (error instanceof InvalidInputError) {
|
|
5232
|
+
throw new InvalidInputError(`${name} must be a valid key: ${error.message}`);
|
|
5233
|
+
}
|
|
5234
|
+
throw error;
|
|
4892
5235
|
}
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
const
|
|
4897
|
-
|
|
4898
|
-
const
|
|
4899
|
-
const
|
|
4900
|
-
|
|
4901
|
-
return
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
5236
|
+
return read;
|
|
5237
|
+
}
|
|
5238
|
+
function assertPlannedSection(value, name) {
|
|
5239
|
+
const read = assertRecord(value, name);
|
|
5240
|
+
assertString(read.label, `${name}.label`);
|
|
5241
|
+
const startBeat = assertFiniteNumber(read.startBeat, `${name}.startBeat`);
|
|
5242
|
+
const endBeat = assertFiniteNumber(read.endBeat, `${name}.endBeat`);
|
|
5243
|
+
assertSpanOrder(startBeat, endBeat, name);
|
|
5244
|
+
return read;
|
|
5245
|
+
}
|
|
5246
|
+
function assertPlannedPhrase(value, name, sectionCount, motifCount) {
|
|
5247
|
+
const read = assertRecord(value, name);
|
|
5248
|
+
const startBeat = assertFiniteNumber(read.startBeat, `${name}.startBeat`);
|
|
5249
|
+
const endBeat = assertFiniteNumber(read.endBeat, `${name}.endBeat`);
|
|
5250
|
+
assertSpanOrder(startBeat, endBeat, name);
|
|
5251
|
+
assertNullable(
|
|
5252
|
+
read.section,
|
|
5253
|
+
`${name}.section`,
|
|
5254
|
+
(v, n) => assertInteger(v, n, 0, Math.max(sectionCount - 1, 0))
|
|
5255
|
+
);
|
|
5256
|
+
assertNullable(read.cadence, `${name}.cadence`, (v, n) => assertOneOf(v, CADENCE_TYPES, n));
|
|
5257
|
+
assertOneOf(read.shape, MELODIC_CONTOUR_SHAPES, `${name}.shape`);
|
|
5258
|
+
assertRange(read.peakPosition, 0, 1, `${name}.peakPosition`);
|
|
5259
|
+
const register = assertRecord(
|
|
5260
|
+
read.register,
|
|
5261
|
+
`${name}.register`
|
|
5262
|
+
);
|
|
5263
|
+
assertMidiPitch(register.low, `${name}.register.low`);
|
|
5264
|
+
assertMidiPitch(register.high, `${name}.register.high`);
|
|
5265
|
+
assertRange(register.mean, 0, 127, `${name}.register.mean`);
|
|
5266
|
+
assertRange(read.onsetDensity, 0, Number.MAX_SAFE_INTEGER, `${name}.onsetDensity`);
|
|
5267
|
+
const motifs = assertArray(read.motifs, `${name}.motifs`);
|
|
5268
|
+
for (let index = 0; index < motifs.length; index += 1) {
|
|
5269
|
+
assertInteger(
|
|
5270
|
+
motifs[index],
|
|
5271
|
+
`${name}.motifs[${index}]`,
|
|
5272
|
+
0,
|
|
5273
|
+
Math.max(motifCount - 1, 0)
|
|
5274
|
+
);
|
|
5275
|
+
}
|
|
5276
|
+
return read;
|
|
5277
|
+
}
|
|
5278
|
+
function assertPlannedChord(value, name, keys) {
|
|
5279
|
+
const read = assertRecord(value, name);
|
|
5280
|
+
const startBeat = assertFiniteNumber(read.startBeat, `${name}.startBeat`);
|
|
5281
|
+
const endBeat = assertFiniteNumber(read.endBeat, `${name}.endBeat`);
|
|
5282
|
+
assertSpanOrder(startBeat, endBeat, name);
|
|
5283
|
+
const keyIndex = assertInteger(read.key, `${name}.key`, 0, Math.max(keys.length - 1, 0));
|
|
5284
|
+
const roman = assertString(read.roman, `${name}.roman`);
|
|
5285
|
+
try {
|
|
5286
|
+
romanToChord(roman, keys[keyIndex]);
|
|
5287
|
+
} catch (error) {
|
|
5288
|
+
if (error instanceof InvalidInputError) {
|
|
5289
|
+
throw new InvalidInputError(
|
|
5290
|
+
`${name}.roman must be readable by romanToChord; received ${describeRejected(roman)}: ${error.message}`
|
|
5291
|
+
);
|
|
4905
5292
|
}
|
|
4906
|
-
|
|
4907
|
-
|
|
5293
|
+
throw error;
|
|
5294
|
+
}
|
|
5295
|
+
return read;
|
|
5296
|
+
}
|
|
5297
|
+
function assertPlannedMotifRelation(value, name) {
|
|
5298
|
+
const read = assertRecord(value, name);
|
|
5299
|
+
assertOneOf(read.kind, MOTIF_RELATION_KINDS, `${name}.kind`);
|
|
5300
|
+
assertFlag(read.sequence, `${name}.sequence`);
|
|
5301
|
+
assertFiniteNumber(read.semitones, `${name}.semitones`);
|
|
5302
|
+
if (read.kind === "tonalTransposition") {
|
|
5303
|
+
assertFiniteNumber(read.degrees, `${name}.degrees`);
|
|
5304
|
+
} else if (read.degrees !== void 0) {
|
|
5305
|
+
assertFiniteNumber(read.degrees, `${name}.degrees`);
|
|
5306
|
+
}
|
|
5307
|
+
const timeRatio = assertFiniteNumber(read.timeRatio, `${name}.timeRatio`);
|
|
5308
|
+
if (timeRatio <= 0) {
|
|
5309
|
+
throw new InvalidInputError(`${name}.timeRatio must be positive; received ${timeRatio}`);
|
|
5310
|
+
}
|
|
5311
|
+
return read;
|
|
5312
|
+
}
|
|
5313
|
+
function assertPlannedMotif(value, name, ownIndex, motifCount, phraseCount) {
|
|
5314
|
+
const read = assertRecord(value, name);
|
|
5315
|
+
assertInteger(read.phrase, `${name}.phrase`, 0, Math.max(phraseCount - 1, 0));
|
|
5316
|
+
const startBeat = assertFiniteNumber(read.startBeat, `${name}.startBeat`);
|
|
5317
|
+
const endBeat = assertFiniteNumber(read.endBeat, `${name}.endBeat`);
|
|
5318
|
+
assertSpanOrder(startBeat, endBeat, name);
|
|
5319
|
+
assertPositiveInt(read.notes, `${name}.notes`);
|
|
5320
|
+
const from = assertNullable(
|
|
5321
|
+
read.from,
|
|
5322
|
+
`${name}.from`,
|
|
5323
|
+
(v, n) => assertInteger(v, n, 0, Math.max(motifCount - 1, 0))
|
|
5324
|
+
);
|
|
5325
|
+
if (from !== null && from >= ownIndex) {
|
|
5326
|
+
throw new InvalidInputError(
|
|
5327
|
+
`${name}.from must be less than this motif's own index (${ownIndex}); received ${from}`
|
|
5328
|
+
);
|
|
5329
|
+
}
|
|
5330
|
+
const relation = assertNullable(read.relation, `${name}.relation`, assertPlannedMotifRelation);
|
|
5331
|
+
if (from === null && relation !== null) {
|
|
5332
|
+
throw new InvalidInputError(
|
|
5333
|
+
`${name}.relation must be null for a root motif (from = null); received a relation`
|
|
5334
|
+
);
|
|
5335
|
+
}
|
|
5336
|
+
const rhythm = assertNullable(read.rhythm, `${name}.rhythm`, (v, n) => assertArray(v, n));
|
|
5337
|
+
if (rhythm !== null) {
|
|
5338
|
+
if (from !== null) {
|
|
5339
|
+
throw new InvalidInputError(`${name}.rhythm must be null for a derived statement`);
|
|
4908
5340
|
}
|
|
4909
|
-
if (
|
|
4910
|
-
|
|
5341
|
+
if (rhythm.length !== read.notes - 1) {
|
|
5342
|
+
throw new InvalidInputError(
|
|
5343
|
+
`${name}.rhythm must hold notes - 1 (${read.notes - 1}) gaps; received ${rhythm.length}`
|
|
5344
|
+
);
|
|
4911
5345
|
}
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
5346
|
+
let total = 0;
|
|
5347
|
+
for (let index = 0; index < rhythm.length; index += 1) {
|
|
5348
|
+
const gap = assertFiniteNumber(rhythm[index], `${name}.rhythm[${index}]`);
|
|
5349
|
+
if (gap <= 0) {
|
|
5350
|
+
throw new InvalidInputError(`${name}.rhythm[${index}] must be positive; received ${gap}`);
|
|
5351
|
+
}
|
|
5352
|
+
total += gap;
|
|
4916
5353
|
}
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
}
|
|
4920
|
-
|
|
4921
|
-
const previousOf = /* @__PURE__ */ new Map();
|
|
4922
|
-
let earlier;
|
|
4923
|
-
for (const note2 of ordered) {
|
|
4924
|
-
if (earlier !== void 0 && earlier.startBeat === note2.startBeat) {
|
|
4925
|
-
previousOf.set(note2, previousOf.get(earlier));
|
|
4926
|
-
} else {
|
|
4927
|
-
previousOf.set(note2, earlier);
|
|
5354
|
+
if (total >= endBeat - startBeat - BEAT_EPS) {
|
|
5355
|
+
throw new InvalidInputError(
|
|
5356
|
+
`${name}.rhythm must sum to less than the statement's length (${endBeat - startBeat}); received ${total}`
|
|
5357
|
+
);
|
|
4928
5358
|
}
|
|
4929
|
-
earlier = note2;
|
|
4930
5359
|
}
|
|
4931
|
-
return
|
|
5360
|
+
return read;
|
|
4932
5361
|
}
|
|
4933
|
-
function
|
|
4934
|
-
const
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
if (later !== void 0 && later.startBeat === note2.startBeat) {
|
|
4940
|
-
nextOf.set(note2, nextOf.get(later));
|
|
4941
|
-
} else {
|
|
4942
|
-
nextOf.set(note2, later);
|
|
4943
|
-
}
|
|
4944
|
-
later = note2;
|
|
4945
|
-
}
|
|
4946
|
-
return nextOf;
|
|
5362
|
+
function assertPlannedRhythm(value, name) {
|
|
5363
|
+
const read = assertRecord(value, name);
|
|
5364
|
+
assertMassDistribution(read.onsetLevels, 6, `${name}.onsetLevels`);
|
|
5365
|
+
assertMassDistribution(read.interOnsetShares, RHYTHM_IOI_BINS.length, `${name}.interOnsetShares`);
|
|
5366
|
+
assertRange(read.syncopation, 0, 1, `${name}.syncopation`);
|
|
5367
|
+
return read;
|
|
4947
5368
|
}
|
|
4948
|
-
function
|
|
4949
|
-
const
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
const after = index === distinct.length - 1 ? Number.POSITIVE_INFINITY : (distinct[index + 1] ?? 0) - beat;
|
|
4955
|
-
gapAt.set(beat, Math.min(before, after));
|
|
5369
|
+
function assertCompositionPlan(value, name = "composition plan") {
|
|
5370
|
+
const read = assertRecord(value, name);
|
|
5371
|
+
if (read.planVersion !== COMPOSITION_PLAN_VERSION) {
|
|
5372
|
+
throw new InvalidInputError(
|
|
5373
|
+
`${name}.planVersion must be ${COMPOSITION_PLAN_VERSION}; received ${describeRejected(read.planVersion)}`
|
|
5374
|
+
);
|
|
4956
5375
|
}
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
5376
|
+
assertInteger(read.seed, `${name}.seed`, 0, 4294967295);
|
|
5377
|
+
assertInteger(
|
|
5378
|
+
read.algorithmVersion,
|
|
5379
|
+
`${name}.algorithmVersion`,
|
|
5380
|
+
MIN_ALGORITHM_VERSION,
|
|
5381
|
+
ALGORITHM_VERSION
|
|
5382
|
+
);
|
|
5383
|
+
const keys = assertArray(read.keys, `${name}.keys`);
|
|
5384
|
+
if (keys.length === 0) {
|
|
5385
|
+
throw new InvalidInputError(`${name}.keys must hold at least one key; received an empty array`);
|
|
4960
5386
|
}
|
|
4961
|
-
|
|
5387
|
+
for (let index = 0; index < keys.length; index += 1) {
|
|
5388
|
+
assertResolvedKeyField(keys[index], `${name}.keys[${index}]`);
|
|
5389
|
+
}
|
|
5390
|
+
assertMeterMap(read.meters, `${name}.meters`);
|
|
5391
|
+
const span = assertRecord(
|
|
5392
|
+
read.span,
|
|
5393
|
+
`${name}.span`
|
|
5394
|
+
);
|
|
5395
|
+
const spanStart = assertFiniteNumber(span.startBeat, `${name}.span.startBeat`);
|
|
5396
|
+
const spanEnd = assertFiniteNumber(span.endBeat, `${name}.span.endBeat`);
|
|
5397
|
+
assertSpanOrder(spanStart, spanEnd, `${name}.span`);
|
|
5398
|
+
assertRange(span.bars, 0, Number.MAX_SAFE_INTEGER, `${name}.span.bars`);
|
|
5399
|
+
const sections = assertArray(read.sections, `${name}.sections`);
|
|
5400
|
+
for (let index = 0; index < sections.length; index += 1) {
|
|
5401
|
+
assertPlannedSection(sections[index], `${name}.sections[${index}]`);
|
|
5402
|
+
}
|
|
5403
|
+
const phrases = assertArray(read.phrases, `${name}.phrases`);
|
|
5404
|
+
const motifs = assertArray(read.motifs, `${name}.motifs`);
|
|
5405
|
+
for (let index = 0; index < phrases.length; index += 1) {
|
|
5406
|
+
assertPlannedPhrase(
|
|
5407
|
+
phrases[index],
|
|
5408
|
+
`${name}.phrases[${index}]`,
|
|
5409
|
+
sections.length,
|
|
5410
|
+
motifs.length
|
|
5411
|
+
);
|
|
5412
|
+
}
|
|
5413
|
+
const harmony = assertArray(read.harmony, `${name}.harmony`);
|
|
5414
|
+
for (let index = 0; index < harmony.length; index += 1) {
|
|
5415
|
+
assertPlannedChord(harmony[index], `${name}.harmony[${index}]`, keys);
|
|
5416
|
+
}
|
|
5417
|
+
for (let index = 0; index < motifs.length; index += 1) {
|
|
5418
|
+
assertPlannedMotif(
|
|
5419
|
+
motifs[index],
|
|
5420
|
+
`${name}.motifs[${index}]`,
|
|
5421
|
+
index,
|
|
5422
|
+
motifs.length,
|
|
5423
|
+
phrases.length
|
|
5424
|
+
);
|
|
5425
|
+
}
|
|
5426
|
+
assertPlannedRhythm(read.rhythm, `${name}.rhythm`);
|
|
5427
|
+
return read;
|
|
4962
5428
|
}
|
|
4963
5429
|
|
|
4964
|
-
// src/generate/
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
5430
|
+
// src/generate/plan/timeline.ts
|
|
5431
|
+
function planTimeline(plan) {
|
|
5432
|
+
return planChordTimeline(assertCompositionPlan(plan));
|
|
5433
|
+
}
|
|
5434
|
+
function planChordTimeline(plan) {
|
|
5435
|
+
const spans = plan.harmony.map((chord) => {
|
|
5436
|
+
const key = plan.keys[chord.key];
|
|
5437
|
+
return spanFromChord(romanToChord(chord.roman, key), chord.startBeat);
|
|
5438
|
+
});
|
|
5439
|
+
return chordTimelineFromChords(spans, plan.span.endBeat);
|
|
5440
|
+
}
|
|
5441
|
+
|
|
5442
|
+
// src/generate/plan/harmony.ts
|
|
5443
|
+
var HARMONY_OK_KIND_NAMES = [
|
|
5444
|
+
"chordTone",
|
|
5445
|
+
"tension",
|
|
5446
|
+
"suspension",
|
|
5447
|
+
"appoggiatura",
|
|
5448
|
+
"anticipation",
|
|
5449
|
+
"passing",
|
|
5450
|
+
"neighbor"
|
|
5451
|
+
];
|
|
5452
|
+
var HARMONY_OK_KINDS = new Set(HARMONY_OK_KIND_NAMES);
|
|
5453
|
+
var CADENCE_DEGREES = {
|
|
5454
|
+
authentic: [1, 3],
|
|
5455
|
+
plagal: [1, 3],
|
|
5456
|
+
half: [2, 5, 7],
|
|
5457
|
+
phrygian: [2, 5, 7],
|
|
5458
|
+
modal: [1]
|
|
5459
|
+
};
|
|
5460
|
+
function planKeyAt(plan) {
|
|
5461
|
+
const regions = [...plan.harmony].sort((a, b) => a.startBeat - b.startBeat).map((chord) => ({
|
|
5462
|
+
startBeat: chord.startBeat,
|
|
5463
|
+
endBeat: chord.endBeat,
|
|
5464
|
+
key: plan.keys[chord.key],
|
|
5465
|
+
confidence: 1
|
|
5466
|
+
}));
|
|
5467
|
+
return keyLookup(regions, plan.keys[0]);
|
|
5468
|
+
}
|
|
5469
|
+
function cadencePitchClasses(plan, timeline, cadence, beat) {
|
|
5470
|
+
if (cadence === "deceptive") {
|
|
5471
|
+
const chord = timeline.at(beat);
|
|
5472
|
+
return chord === null ? null : chordPitchClasses(chord);
|
|
5473
|
+
}
|
|
5474
|
+
const entry = plan.harmony.find((chord) => beat >= chord.startBeat && beat < chord.endBeat);
|
|
5475
|
+
const scale = scaleOf(plan.keys[entry?.key ?? 0]);
|
|
5476
|
+
const tones = scaleTonesInDegreeOrder(scale);
|
|
5477
|
+
const raised = cadencesThroughRaisedSeventh(scale);
|
|
5478
|
+
return CADENCE_DEGREES[cadence].map((degree) => {
|
|
5479
|
+
const tone = tones[degree - 1];
|
|
5480
|
+
return raised && degree === 7 && tone !== void 0 ? leadingTonePcOf(scale) : tone;
|
|
5481
|
+
}).filter((pc) => pc !== void 0);
|
|
5482
|
+
}
|
|
5483
|
+
function planHarmonyMisfits(plan, timeline = planChordTimeline(plan)) {
|
|
5484
|
+
const keyAt = planKeyAt(plan);
|
|
5485
|
+
const scaleAt2 = (beat) => scaleOf(keyAt(beat));
|
|
5486
|
+
return (line) => {
|
|
5487
|
+
const analyzed = analyzeVoice(line, timeline.at, scaleAt2);
|
|
5488
|
+
return line.map((note2, index) => {
|
|
5489
|
+
const position = positionClass(note2.startBeat, plan.meters);
|
|
5490
|
+
if (position === "off") {
|
|
5491
|
+
return null;
|
|
5492
|
+
}
|
|
5493
|
+
const kinds = (analyzed[index]?.labels ?? []).map((label) => label.kind);
|
|
5494
|
+
if (kinds.some((kind) => HARMONY_OK_KINDS.has(kind))) {
|
|
5495
|
+
return null;
|
|
5496
|
+
}
|
|
5497
|
+
const lenient = position === "weak" || timeline.at(note2.startBeat) === null;
|
|
5498
|
+
return lenient && isScaleTone(note2.pitch, scaleAt2(note2.startBeat)) ? null : kinds;
|
|
5499
|
+
});
|
|
5500
|
+
};
|
|
5501
|
+
}
|
|
5502
|
+
|
|
5503
|
+
// src/generate/melody/pitch-dp.ts
|
|
5504
|
+
var NO_LEAP = 0;
|
|
5505
|
+
var LEAP_UP = 1;
|
|
5506
|
+
var LEAP_DOWN = 2;
|
|
5507
|
+
var LEAP_STATES = 3;
|
|
5508
|
+
var SEARCH_LABEL = "melody pitch search";
|
|
5509
|
+
var WIDEST_THIRD = 4;
|
|
5510
|
+
var LEAP_CONTINUATION_COST = 0.5;
|
|
5511
|
+
function intervalCost(size) {
|
|
5512
|
+
if (size === 0) {
|
|
5513
|
+
return 0.2;
|
|
5514
|
+
}
|
|
5515
|
+
if (size <= 2) {
|
|
5516
|
+
return 0;
|
|
5517
|
+
}
|
|
5518
|
+
if (size <= WIDEST_THIRD) {
|
|
5519
|
+
return 0.3;
|
|
5520
|
+
}
|
|
5521
|
+
if (size <= 7) {
|
|
5522
|
+
return 0.6;
|
|
5523
|
+
}
|
|
5524
|
+
return 1;
|
|
5525
|
+
}
|
|
5526
|
+
function leapOf(delta) {
|
|
5527
|
+
if (Math.abs(delta) <= WIDEST_THIRD) {
|
|
5528
|
+
return NO_LEAP;
|
|
5529
|
+
}
|
|
5530
|
+
return delta > 0 ? LEAP_UP : LEAP_DOWN;
|
|
5531
|
+
}
|
|
5532
|
+
function stepCost(from, to, leap, bothFixed) {
|
|
5533
|
+
const delta = to - from;
|
|
5534
|
+
const size = Math.abs(delta);
|
|
5535
|
+
if (!bothFixed && isForbiddenMelodicLeap(from, to)) {
|
|
5536
|
+
return Number.POSITIVE_INFINITY;
|
|
5537
|
+
}
|
|
5538
|
+
let cost = intervalCost(size);
|
|
5539
|
+
if (leap === LEAP_UP && delta > 0 || leap === LEAP_DOWN && delta < 0) {
|
|
5540
|
+
cost += LEAP_CONTINUATION_COST;
|
|
5541
|
+
}
|
|
5542
|
+
return cost;
|
|
5543
|
+
}
|
|
5544
|
+
function searchPitches(slots, budget, extra) {
|
|
5545
|
+
const first = slots[0];
|
|
5546
|
+
if (first === void 0) {
|
|
5547
|
+
return [];
|
|
5548
|
+
}
|
|
5549
|
+
let previous = allocateCandidateTable(
|
|
5550
|
+
first.candidates.length,
|
|
5551
|
+
LEAP_STATES,
|
|
5552
|
+
SEARCH_LABEL,
|
|
5553
|
+
budget
|
|
5554
|
+
).fill(Number.POSITIVE_INFINITY);
|
|
5555
|
+
for (let j = 0; j < first.candidates.length; j += 1) {
|
|
5556
|
+
previous[j * LEAP_STATES + NO_LEAP] = first.cost(first.candidates[j]);
|
|
5557
|
+
}
|
|
5558
|
+
const backs = [];
|
|
5559
|
+
for (let k = 1; k < slots.length; k += 1) {
|
|
5560
|
+
const before = slots[k - 1];
|
|
5561
|
+
const slot = slots[k];
|
|
5562
|
+
const unary = slot.candidates.map((pitch) => slot.cost(pitch));
|
|
5563
|
+
const current = allocateCandidateTable(
|
|
5564
|
+
slot.candidates.length,
|
|
5565
|
+
LEAP_STATES,
|
|
5566
|
+
SEARCH_LABEL,
|
|
5567
|
+
budget
|
|
5568
|
+
).fill(Number.POSITIVE_INFINITY);
|
|
5569
|
+
const back = allocateChoiceTable(
|
|
5570
|
+
slot.candidates.length,
|
|
5571
|
+
LEAP_STATES,
|
|
5572
|
+
SEARCH_LABEL,
|
|
5573
|
+
budget
|
|
5574
|
+
).fill(-1);
|
|
5575
|
+
const bothFixed = before.fixed && slot.fixed;
|
|
5576
|
+
for (let state2 = 0; state2 < previous.length; state2 += 1) {
|
|
5577
|
+
const reached = previous[state2];
|
|
5578
|
+
if (reached === Number.POSITIVE_INFINITY) {
|
|
5579
|
+
continue;
|
|
5580
|
+
}
|
|
5581
|
+
const from = before.candidates[Math.floor(state2 / LEAP_STATES)];
|
|
5582
|
+
const leap = state2 % LEAP_STATES;
|
|
5583
|
+
for (let j = 0; j < slot.candidates.length; j += 1) {
|
|
5584
|
+
const to = slot.candidates[j];
|
|
5585
|
+
const step = stepCost(from, to, leap, bothFixed);
|
|
5586
|
+
if (step === Number.POSITIVE_INFINITY) {
|
|
5587
|
+
continue;
|
|
5588
|
+
}
|
|
5589
|
+
const total = reached + step + unary[j] + (extra ? extra(k, from, to) : 0);
|
|
5590
|
+
const index = j * LEAP_STATES + leapOf(to - from);
|
|
5591
|
+
if (total < current[index]) {
|
|
5592
|
+
current[index] = total;
|
|
5593
|
+
back[index] = state2;
|
|
5594
|
+
}
|
|
5595
|
+
}
|
|
5596
|
+
}
|
|
5597
|
+
backs.push(back);
|
|
5598
|
+
previous = current;
|
|
5599
|
+
}
|
|
5600
|
+
let best = -1;
|
|
5601
|
+
for (let state2 = 0; state2 < previous.length; state2 += 1) {
|
|
5602
|
+
if (previous[state2] < Number.POSITIVE_INFINITY && (best < 0 || previous[state2] < previous[best])) {
|
|
5603
|
+
best = state2;
|
|
5604
|
+
}
|
|
5605
|
+
}
|
|
5606
|
+
if (best < 0) {
|
|
5607
|
+
return null;
|
|
5608
|
+
}
|
|
5609
|
+
const pitches = new Array(slots.length);
|
|
5610
|
+
let state = best;
|
|
5611
|
+
for (let k = slots.length - 1; k >= 0; k -= 1) {
|
|
5612
|
+
pitches[k] = slots[k].candidates[Math.floor(state / LEAP_STATES)];
|
|
5613
|
+
if (k > 0) {
|
|
5614
|
+
state = backs[k - 1][state];
|
|
5615
|
+
}
|
|
5616
|
+
}
|
|
5617
|
+
return pitches;
|
|
5618
|
+
}
|
|
5619
|
+
|
|
5620
|
+
// src/generate/melody/derive.ts
|
|
5621
|
+
var VARIATION_STEP_COST = 0.2;
|
|
5622
|
+
var OCTAVE = 12;
|
|
5623
|
+
var LEVEL_TARGET_COST = 0.1;
|
|
5624
|
+
var LEVEL_PLAN_COST = 0.02;
|
|
5625
|
+
var LEVEL_REACH = 24;
|
|
5626
|
+
var TONAL_LEVEL_REACH = 14;
|
|
5627
|
+
function stretched(cell, timeRatio) {
|
|
5628
|
+
return timeRatio === 1 ? cell : transformMotif(cell, "augment", timeRatio);
|
|
5629
|
+
}
|
|
5630
|
+
function shaped(cell, relation) {
|
|
5631
|
+
switch (relation.kind) {
|
|
5632
|
+
case "repetition":
|
|
5633
|
+
return cell;
|
|
5634
|
+
case "transposition":
|
|
5635
|
+
case "tonalTransposition":
|
|
5636
|
+
return stretched(cell, relation.timeRatio);
|
|
5637
|
+
case "inversion":
|
|
5638
|
+
return stretched(transformMotif(cell, "invert"), relation.timeRatio);
|
|
5639
|
+
case "retrograde":
|
|
5640
|
+
return stretched(transformMotif(cell, "retrograde"), relation.timeRatio);
|
|
5641
|
+
case "retrogradeInversion":
|
|
5642
|
+
return stretched(
|
|
5643
|
+
transformMotif(transformMotif(cell, "invert"), "retrograde"),
|
|
5644
|
+
relation.timeRatio
|
|
5645
|
+
);
|
|
5646
|
+
case "augmentation":
|
|
5647
|
+
return transformMotif(cell, "augment", relation.timeRatio);
|
|
5648
|
+
case "diminution":
|
|
5649
|
+
return transformMotif(cell, "diminish", 1 / relation.timeRatio);
|
|
5650
|
+
}
|
|
5651
|
+
}
|
|
5652
|
+
function relationLevels(kind, low = -LEVEL_REACH, high = LEVEL_REACH) {
|
|
5653
|
+
const between = (from, to, skipZero) => {
|
|
5654
|
+
const levels = [];
|
|
5655
|
+
for (let level = Math.ceil(from); level <= to; level += 1) {
|
|
5656
|
+
if (!(skipZero && level === 0)) levels.push(level);
|
|
5657
|
+
}
|
|
5658
|
+
return levels;
|
|
5659
|
+
};
|
|
5660
|
+
switch (kind) {
|
|
5661
|
+
case "repetition":
|
|
5662
|
+
case "augmentation":
|
|
5663
|
+
case "diminution":
|
|
5664
|
+
return [-OCTAVE, 0, OCTAVE];
|
|
5665
|
+
case "transposition":
|
|
5666
|
+
return between(low, high, true);
|
|
5667
|
+
case "tonalTransposition":
|
|
5668
|
+
return between(-TONAL_LEVEL_REACH, TONAL_LEVEL_REACH, true);
|
|
5669
|
+
case "inversion":
|
|
5670
|
+
case "retrograde":
|
|
5671
|
+
case "retrogradeInversion":
|
|
5672
|
+
return between(low, high, false);
|
|
5673
|
+
}
|
|
5674
|
+
}
|
|
5675
|
+
function replayAt(source, relation, level, startBeat, key) {
|
|
5676
|
+
const head = source[0];
|
|
5677
|
+
const cell = shaped({ notes: source.map((n) => ({ ...n })) }, relation);
|
|
5678
|
+
const first = cell.notes[0];
|
|
5679
|
+
const offset = startBeat - first.startBeat;
|
|
5680
|
+
const shift = head.pitch + level - first.pitch;
|
|
5681
|
+
return cell.notes.map((n) => ({
|
|
5682
|
+
...n,
|
|
5683
|
+
pitch: relation.kind === "tonalTransposition" ? shiftByScaleDegrees(n.pitch, level, key) : n.pitch + shift,
|
|
5684
|
+
startBeat: n.startBeat + offset
|
|
5685
|
+
}));
|
|
5686
|
+
}
|
|
5687
|
+
function clipStatement(notes, endBeat) {
|
|
5688
|
+
return notes.filter((n) => n.startBeat < endBeat - BEAT_EPS).map((n) => ({ ...n, durationBeat: Math.min(n.durationBeat, endBeat - n.startBeat) }));
|
|
5689
|
+
}
|
|
5690
|
+
function outsideCount(notes, frame) {
|
|
5691
|
+
return notes.filter((n) => n.pitch < frame.low || n.pitch > frame.high).length;
|
|
5692
|
+
}
|
|
5693
|
+
function nearerLevel(level, best, planned) {
|
|
5694
|
+
const distance = Math.abs(level - planned);
|
|
5695
|
+
const bestDistance = Math.abs(best - planned);
|
|
5696
|
+
return distance < bestDistance || distance === bestDistance && level < best;
|
|
5697
|
+
}
|
|
5698
|
+
function repairStatement(notes, forced, frame) {
|
|
5699
|
+
const slots = notes.map(
|
|
5700
|
+
(n, index) => forced[index] ? {
|
|
5701
|
+
candidates: frame.candidatesAt(n.startBeat),
|
|
5702
|
+
cost: frame.costAt(n.startBeat),
|
|
5703
|
+
fixed: false
|
|
5704
|
+
} : { candidates: [n.pitch], cost: () => 0, fixed: true }
|
|
5705
|
+
);
|
|
5706
|
+
const pitches = searchPitches(slots, frame.budget);
|
|
5707
|
+
if (pitches === null) {
|
|
5708
|
+
throw new NoSolutionError(
|
|
5709
|
+
`no repair fits the statement into the register ${frame.low}..${frame.high}`,
|
|
5710
|
+
{ at: frame.at }
|
|
5711
|
+
);
|
|
5712
|
+
}
|
|
5713
|
+
return notes.map((n, index) => ({ ...n, pitch: pitches[index] }));
|
|
5714
|
+
}
|
|
5715
|
+
function placeDerived(source, relation, startBeat, endBeat, key, frame, before) {
|
|
5716
|
+
const planned = relation.kind === "tonalTransposition" ? relation.degrees ?? 0 : relation.semitones;
|
|
5717
|
+
const replay = (level) => clipStatement(replayAt(source, relation, level, startBeat, key), endBeat);
|
|
5718
|
+
const base = replay(0).map((n) => n.pitch);
|
|
5719
|
+
const levels = relationLevels(
|
|
5720
|
+
relation.kind,
|
|
5721
|
+
Math.max(-LEVEL_REACH, frame.low - Math.min(...base)),
|
|
5722
|
+
Math.min(LEVEL_REACH, frame.high - Math.max(...base))
|
|
5723
|
+
);
|
|
5724
|
+
let best = null;
|
|
5725
|
+
for (const level of levels) {
|
|
5726
|
+
const notes2 = replay(level);
|
|
5727
|
+
if (outsideCount(notes2, frame) > 0) {
|
|
5728
|
+
continue;
|
|
5729
|
+
}
|
|
5730
|
+
const distance = notes2.reduce((sum, n) => sum + Math.abs(n.pitch - frame.targetAt(n.startBeat)), 0) / notes2.length;
|
|
5731
|
+
const cost = frame.misfits(notes2, before) + LEVEL_TARGET_COST * distance + LEVEL_PLAN_COST * Math.abs(level - planned);
|
|
5732
|
+
if (best === null || cost < best.cost || cost === best.cost && nearerLevel(level, best.level, planned)) {
|
|
5733
|
+
best = { level, notes: notes2, cost };
|
|
5734
|
+
}
|
|
5735
|
+
}
|
|
5736
|
+
if (best !== null) {
|
|
5737
|
+
return best.notes;
|
|
5738
|
+
}
|
|
5739
|
+
let fallback = null;
|
|
5740
|
+
for (const level of relationLevels(relation.kind)) {
|
|
5741
|
+
const notes2 = replay(level);
|
|
5742
|
+
const outside = outsideCount(notes2, frame);
|
|
5743
|
+
if (fallback === null || outside < fallback.outside || outside === fallback.outside && nearerLevel(level, fallback.level, planned)) {
|
|
5744
|
+
fallback = { level, notes: notes2, outside };
|
|
5745
|
+
}
|
|
5746
|
+
}
|
|
5747
|
+
const notes = fallback.notes;
|
|
5748
|
+
return repairStatement(
|
|
5749
|
+
notes,
|
|
5750
|
+
notes.map((n) => n.pitch < frame.low || n.pitch > frame.high),
|
|
5751
|
+
frame
|
|
5752
|
+
);
|
|
5753
|
+
}
|
|
5754
|
+
function varyStatement(source, startBeat, frame) {
|
|
5755
|
+
const offset = startBeat - source[0].startBeat;
|
|
5756
|
+
const last = source.length - 1;
|
|
5757
|
+
const inside = (pitch) => pitch >= frame.low && pitch <= frame.high;
|
|
5758
|
+
const outer = [source[0].pitch, source[last].pitch];
|
|
5759
|
+
const shift = [0, OCTAVE, -OCTAVE].find((s) => outer.every((pitch) => inside(pitch + s)));
|
|
5760
|
+
const notes = source.map((n) => ({ ...n, startBeat: n.startBeat + offset }));
|
|
5761
|
+
const slots = notes.map(
|
|
5762
|
+
(n, index) => (index === 0 || index === last) && shift !== void 0 ? { candidates: [n.pitch + shift], cost: () => 0, fixed: true } : {
|
|
5763
|
+
candidates: frame.candidatesAt(n.startBeat),
|
|
5764
|
+
cost: frame.costAt(n.startBeat),
|
|
5765
|
+
fixed: false
|
|
5766
|
+
}
|
|
5767
|
+
);
|
|
5768
|
+
const pitches = searchPitches(
|
|
5769
|
+
slots,
|
|
5770
|
+
frame.budget,
|
|
5771
|
+
(index, from, to) => VARIATION_STEP_COST * Math.abs(
|
|
5772
|
+
to - from - (source[index].pitch - source[index - 1].pitch)
|
|
5773
|
+
)
|
|
5774
|
+
);
|
|
5775
|
+
if (pitches === null) {
|
|
5776
|
+
throw new NoSolutionError(`no variation line fits the register ${frame.low}..${frame.high}`, {
|
|
5777
|
+
at: frame.at
|
|
5778
|
+
});
|
|
5779
|
+
}
|
|
5780
|
+
return notes.map((n, index) => ({ ...n, pitch: pitches[index] }));
|
|
5781
|
+
}
|
|
5782
|
+
|
|
5783
|
+
// src/generate/melody/index.ts
|
|
5784
|
+
var RHYTHM_SUBDIVISION = 4;
|
|
5785
|
+
var OFF_SCALE_COST = 1;
|
|
5786
|
+
var TARGET_COST = 0.1;
|
|
5787
|
+
var TIE_BREAK = 1e-6;
|
|
5788
|
+
var SEARCH_STATES_SQUARED = 9;
|
|
5789
|
+
function melodyContext(plan, input) {
|
|
5790
|
+
const fromPlan = { seed: plan.seed, algorithmVersion: plan.algorithmVersion };
|
|
5791
|
+
if (input === void 0) {
|
|
5792
|
+
return resolveContext(fromPlan);
|
|
5793
|
+
}
|
|
5794
|
+
if (typeof input === "number") {
|
|
5795
|
+
return resolveContext({ ...fromPlan, seed: input });
|
|
5796
|
+
}
|
|
5797
|
+
const read = assertRecord(input, "ctx");
|
|
5798
|
+
if (read.rng !== void 0) {
|
|
5799
|
+
throw new InvalidInputError(
|
|
5800
|
+
"ctx.rng cannot drive a melody: every onset and tie-break is drawn at an address of phrase and beat under a seed; pass a seed instead"
|
|
5801
|
+
);
|
|
5802
|
+
}
|
|
5803
|
+
return resolveContext({
|
|
5804
|
+
...read,
|
|
5805
|
+
seed: read.seed ?? plan.seed,
|
|
5806
|
+
algorithmVersion: read.algorithmVersion ?? plan.algorithmVersion
|
|
5807
|
+
});
|
|
5808
|
+
}
|
|
5809
|
+
function chordEntryAt(plan, beat) {
|
|
5810
|
+
return plan.harmony.find((chord) => beat >= chord.startBeat && beat < chord.endBeat);
|
|
5811
|
+
}
|
|
5812
|
+
function scaleAt(scene, beat) {
|
|
5813
|
+
return scene.scales[chordEntryAt(scene.plan, beat)?.key ?? 0];
|
|
5814
|
+
}
|
|
5815
|
+
function targetPitch(phrase, beat) {
|
|
5816
|
+
const length = phrase.endBeat - phrase.startBeat;
|
|
5817
|
+
const t = length > 0 ? Math.min(Math.max((beat - phrase.startBeat) / length, 0), 1) : 0;
|
|
5818
|
+
const { low, high, mean } = phrase.register;
|
|
5819
|
+
return mean + (high - low) / 2 * plannedContourAt(phrase.shape, phrase.peakPosition, t);
|
|
5820
|
+
}
|
|
5821
|
+
function registerPitches(phrase) {
|
|
5822
|
+
const pitches = [];
|
|
5823
|
+
for (let pitch = phrase.register.low; pitch <= phrase.register.high; pitch += 1) {
|
|
5824
|
+
pitches.push(pitch);
|
|
5825
|
+
}
|
|
5826
|
+
return pitches;
|
|
5827
|
+
}
|
|
5828
|
+
function frameOf(scene, index) {
|
|
5829
|
+
const phrase = scene.plan.phrases[index];
|
|
5830
|
+
const { low, high } = phrase.register;
|
|
5831
|
+
const register = registerPitches(phrase);
|
|
5832
|
+
const draw = scene.ctx.part("melody");
|
|
5833
|
+
return {
|
|
5834
|
+
low,
|
|
5835
|
+
high,
|
|
5836
|
+
at: phrase.startBeat,
|
|
5837
|
+
budget: scene.budget,
|
|
5838
|
+
candidatesAt: (beat) => {
|
|
5839
|
+
const position = positionClass(beat, scene.plan.meters);
|
|
5840
|
+
const chord = scene.timeline.at(beat);
|
|
5841
|
+
const tones = position === "strong" && chord !== null ? chordPitchClasses(chord) : null;
|
|
5842
|
+
const scale = scaleAt(scene, beat);
|
|
5843
|
+
const candidates = position === "off" ? register : tones !== null ? register.filter((pitch) => tones.includes(pitchClassOf(pitch))) : register.filter((pitch) => isScaleTone(pitch, scale));
|
|
5844
|
+
if (candidates.length === 0) {
|
|
5845
|
+
throw new NoSolutionError(
|
|
5846
|
+
`no ${position} pitch at beat ${beat} fits the register ${low}..${high}`,
|
|
5847
|
+
{ at: phrase.startBeat }
|
|
5848
|
+
);
|
|
5849
|
+
}
|
|
5850
|
+
return candidates;
|
|
5851
|
+
},
|
|
5852
|
+
costAt: (beat) => {
|
|
5853
|
+
const off = positionClass(beat, scene.plan.meters) === "off";
|
|
5854
|
+
const scale = scaleAt(scene, beat);
|
|
5855
|
+
const target = targetPitch(phrase, beat);
|
|
5856
|
+
return (pitch) => {
|
|
5857
|
+
let cost = TARGET_COST * Math.abs(pitch - target);
|
|
5858
|
+
if (off && !isScaleTone(pitch, scale)) {
|
|
5859
|
+
cost += OFF_SCALE_COST;
|
|
5860
|
+
}
|
|
5861
|
+
return cost + draw.float(0, TIE_BREAK, index, "tie", beat, pitch);
|
|
5862
|
+
};
|
|
5863
|
+
},
|
|
5864
|
+
targetAt: (beat) => targetPitch(phrase, beat),
|
|
5865
|
+
misfits: (notes, before) => scene.harmonyMisfits([...before, ...notes]).slice(before.length).filter((kinds) => kinds !== null).length
|
|
5866
|
+
};
|
|
5867
|
+
}
|
|
5868
|
+
function membersOf(plan) {
|
|
5869
|
+
return plan.phrases.map((phrase, index) => {
|
|
5870
|
+
const found = new Set(phrase.motifs);
|
|
5871
|
+
plan.motifs.forEach((motif, m) => {
|
|
5872
|
+
if (motif.phrase === index) {
|
|
5873
|
+
found.add(m);
|
|
5874
|
+
}
|
|
5875
|
+
});
|
|
5876
|
+
return [...found].sort(
|
|
5877
|
+
(a, b) => (plan.motifs[a]?.startBeat ?? 0) - (plan.motifs[b]?.startBeat ?? 0) || a - b
|
|
5878
|
+
);
|
|
5879
|
+
});
|
|
5880
|
+
}
|
|
5881
|
+
function segmentsOf(scene, index) {
|
|
5882
|
+
const phrase = scene.plan.phrases[index];
|
|
5883
|
+
const segments = [];
|
|
5884
|
+
let cursor = phrase.startBeat;
|
|
5885
|
+
for (const m of scene.members[index]) {
|
|
5886
|
+
const motif = scene.plan.motifs[m];
|
|
5887
|
+
if (motif === void 0) {
|
|
5888
|
+
continue;
|
|
5889
|
+
}
|
|
5890
|
+
if (motif.startBeat > cursor + BEAT_EPS) {
|
|
5891
|
+
segments.push({ startBeat: cursor, endBeat: motif.startBeat, motif: null });
|
|
5892
|
+
}
|
|
5893
|
+
segments.push({ startBeat: motif.startBeat, endBeat: motif.endBeat, motif: m });
|
|
5894
|
+
cursor = Math.max(cursor, motif.endBeat);
|
|
5895
|
+
}
|
|
5896
|
+
if (phrase.endBeat > cursor + BEAT_EPS) {
|
|
5897
|
+
segments.push({ startBeat: cursor, endBeat: phrase.endBeat, motif: null });
|
|
5898
|
+
}
|
|
5899
|
+
return segments;
|
|
5900
|
+
}
|
|
5901
|
+
function notesAt(onsets, endBeat) {
|
|
5902
|
+
return onsets.map((startBeat, i) => ({
|
|
5903
|
+
pitch: 0,
|
|
5904
|
+
startBeat,
|
|
5905
|
+
durationBeat: (onsets[i + 1] ?? endBeat) - startBeat
|
|
5906
|
+
}));
|
|
5907
|
+
}
|
|
5908
|
+
function onsetSlots(scene, startBeat, endBeat) {
|
|
5909
|
+
const { meters } = scene.plan;
|
|
5910
|
+
const slots = [startBeat];
|
|
5911
|
+
let bar2 = barStartBeat(startBeat, meters);
|
|
5912
|
+
while (bar2 < endBeat - BEAT_EPS) {
|
|
5913
|
+
const barEnd = bar2 + beatsPerBarAt(bar2, meters);
|
|
5914
|
+
for (let k = 0; bar2 + k / RHYTHM_SUBDIVISION < barEnd - BEAT_EPS; k += 1) {
|
|
5915
|
+
const beat = bar2 + k / RHYTHM_SUBDIVISION;
|
|
5916
|
+
if (beat > startBeat + BEAT_EPS && beat < endBeat - BEAT_EPS) {
|
|
5917
|
+
slots.push(beat);
|
|
5918
|
+
}
|
|
5919
|
+
}
|
|
5920
|
+
assertGenerationBudget(slots.length, "melody onset slots", scene.budget);
|
|
5921
|
+
bar2 = barEnd;
|
|
5922
|
+
}
|
|
5923
|
+
return slots;
|
|
5924
|
+
}
|
|
5925
|
+
function drawOnsets(scene, phraseIndex, startBeat, endBeat, count) {
|
|
5926
|
+
const slots = onsetSlots(scene, startBeat, endBeat);
|
|
5927
|
+
assertGenerationBudget(slots.length * Math.max(1, count - 1), "melody onset slots", scene.budget);
|
|
5928
|
+
if (count - 1 > slots.length - 1) {
|
|
5929
|
+
throw new NoSolutionError(
|
|
5930
|
+
`${count} onsets do not fit the sixteenth grid of beats ${startBeat}..${endBeat}`,
|
|
5931
|
+
{ at: scene.plan.phrases[phraseIndex].startBeat }
|
|
5932
|
+
);
|
|
5933
|
+
}
|
|
5934
|
+
const levels = slots.map((beat) => rhythmLevel(beat, scene.plan.meters));
|
|
5935
|
+
const perLevel = /* @__PURE__ */ new Map();
|
|
5936
|
+
for (const level of levels) {
|
|
5937
|
+
perLevel.set(level, (perLevel.get(level) ?? 0) + 1);
|
|
5938
|
+
}
|
|
5939
|
+
const levelWeight = levels.map(
|
|
5940
|
+
(level) => (scene.plan.rhythm.onsetLevels[level] ?? 0) / perLevel.get(level)
|
|
5941
|
+
);
|
|
5942
|
+
const draw = scene.ctx.part("melody");
|
|
5943
|
+
const onsets = [startBeat];
|
|
5944
|
+
let current = 0;
|
|
5945
|
+
for (let k = 1; k < count; k += 1) {
|
|
5946
|
+
const last = slots.length - 1 - (count - 1 - k);
|
|
5947
|
+
const candidates = [];
|
|
5948
|
+
for (let j = current + 1; j <= last; j += 1) candidates.push(j);
|
|
5949
|
+
const gapShare = (j) => scene.plan.rhythm.interOnsetShares[ioiBinIndex(slots[j] - slots[current])] ?? 0;
|
|
5950
|
+
let weights = candidates.map((j) => levelWeight[j] * gapShare(j));
|
|
5951
|
+
if (!weights.some((w) => w > 0)) weights = candidates.map((j) => levelWeight[j]);
|
|
5952
|
+
if (!weights.some((w) => w > 0)) weights = candidates.map(() => 1);
|
|
5953
|
+
const total = weights.reduce((sum, w) => sum + w, 0);
|
|
5954
|
+
let target = draw.float(0, 1, phraseIndex, "onset", startBeat, k) * total;
|
|
5955
|
+
let pick = candidates.length - 1;
|
|
5956
|
+
for (let c = 0; c < candidates.length; c += 1) {
|
|
5957
|
+
target -= weights[c];
|
|
5958
|
+
if (target < 0) {
|
|
5959
|
+
pick = c;
|
|
5960
|
+
break;
|
|
5961
|
+
}
|
|
5962
|
+
}
|
|
5963
|
+
current = candidates[pick];
|
|
5964
|
+
onsets.push(slots[current]);
|
|
5965
|
+
}
|
|
5966
|
+
return notesAt(onsets, endBeat);
|
|
5967
|
+
}
|
|
5968
|
+
function placeRhythm(scene, motif) {
|
|
5969
|
+
const { rhythm, startBeat, endBeat, notes } = motif;
|
|
5970
|
+
if (rhythm === null) {
|
|
5971
|
+
return drawOnsets(scene, motif.phrase, startBeat, endBeat, notes);
|
|
5972
|
+
}
|
|
5973
|
+
const onsets = [startBeat];
|
|
5974
|
+
for (const gap of rhythm) {
|
|
5975
|
+
onsets.push(onsets.at(-1) + gap);
|
|
5976
|
+
}
|
|
5977
|
+
return notesAt(onsets, endBeat);
|
|
5978
|
+
}
|
|
5979
|
+
function searchedLine(rhythm, frame) {
|
|
5980
|
+
const slots = rhythm.map((note2) => ({
|
|
5981
|
+
candidates: frame.candidatesAt(note2.startBeat),
|
|
5982
|
+
cost: frame.costAt(note2.startBeat),
|
|
5983
|
+
fixed: false
|
|
5984
|
+
}));
|
|
5985
|
+
const pitches = searchPitches(slots, frame.budget);
|
|
5986
|
+
if (pitches === null) {
|
|
5987
|
+
throw new NoSolutionError(`no pitch line fits the register ${frame.low}..${frame.high}`, {
|
|
5988
|
+
at: frame.at
|
|
5989
|
+
});
|
|
5990
|
+
}
|
|
5991
|
+
return rhythm.map((note2, i) => ({ ...note2, pitch: pitches[i] }));
|
|
5992
|
+
}
|
|
5993
|
+
function statementOf(scene, m) {
|
|
5994
|
+
const known = scene.statements.get(m);
|
|
5995
|
+
if (known !== void 0) {
|
|
5996
|
+
return known;
|
|
5997
|
+
}
|
|
5998
|
+
scene.inProgress.add(m);
|
|
5999
|
+
const motif = scene.plan.motifs[m];
|
|
6000
|
+
const frame = scene.frames[motif.phrase];
|
|
6001
|
+
let notes;
|
|
6002
|
+
if (motif.from === null) {
|
|
6003
|
+
if (motif.endBeat - motif.startBeat <= BEAT_EPS) {
|
|
6004
|
+
throw new NoSolutionError(`root motif ${m} spans no time to place its notes in`, {
|
|
6005
|
+
at: frame.at
|
|
6006
|
+
});
|
|
6007
|
+
}
|
|
6008
|
+
notes = searchedLine(placeRhythm(scene, motif), frame);
|
|
6009
|
+
} else {
|
|
6010
|
+
const source = statementOf(scene, motif.from);
|
|
6011
|
+
notes = motif.relation === null ? varyStatement(source, motif.startBeat, frame) : placeDerived(
|
|
6012
|
+
source,
|
|
6013
|
+
motif.relation,
|
|
6014
|
+
motif.startBeat,
|
|
6015
|
+
motif.endBeat,
|
|
6016
|
+
scaleAt(scene, motif.startBeat),
|
|
6017
|
+
frame,
|
|
6018
|
+
lineBefore(scene, motif.phrase, motif.startBeat)
|
|
6019
|
+
);
|
|
6020
|
+
}
|
|
6021
|
+
scene.inProgress.delete(m);
|
|
6022
|
+
scene.statements.set(m, notes);
|
|
6023
|
+
return notes;
|
|
6024
|
+
}
|
|
6025
|
+
function freeNoteCount(scene, index, segment) {
|
|
6026
|
+
const phrase = scene.plan.phrases[index];
|
|
6027
|
+
const bars = barSpanOf(segment.startBeat, segment.endBeat, scene.plan.meters);
|
|
6028
|
+
return Math.max(1, Math.round(phrase.onsetDensity * bars));
|
|
6029
|
+
}
|
|
6030
|
+
function freeSpan(scene, index, segment) {
|
|
6031
|
+
const key = `${index}:${segment.startBeat}`;
|
|
6032
|
+
const known = scene.free.get(key);
|
|
6033
|
+
if (known !== void 0) {
|
|
6034
|
+
return known;
|
|
6035
|
+
}
|
|
6036
|
+
const rhythm = drawOnsets(
|
|
6037
|
+
scene,
|
|
6038
|
+
index,
|
|
6039
|
+
segment.startBeat,
|
|
6040
|
+
segment.endBeat,
|
|
6041
|
+
freeNoteCount(scene, index, segment)
|
|
6042
|
+
);
|
|
6043
|
+
const notes = searchedLine(rhythm, scene.frames[index]);
|
|
6044
|
+
scene.free.set(key, notes);
|
|
6045
|
+
return notes;
|
|
6046
|
+
}
|
|
6047
|
+
function lineBefore(scene, index, beat) {
|
|
6048
|
+
const line = [];
|
|
6049
|
+
for (const segment of segmentsOf(scene, index)) {
|
|
6050
|
+
if (segment.endBeat > beat + BEAT_EPS) {
|
|
6051
|
+
break;
|
|
6052
|
+
}
|
|
6053
|
+
if (segment.motif === null) {
|
|
6054
|
+
line.push(...freeSpan(scene, index, segment));
|
|
6055
|
+
} else if (!scene.inProgress.has(segment.motif)) {
|
|
6056
|
+
line.push(...statementOf(scene, segment.motif));
|
|
6057
|
+
}
|
|
6058
|
+
}
|
|
6059
|
+
return line.sort((a, b) => a.startBeat - b.startBeat);
|
|
6060
|
+
}
|
|
6061
|
+
function heldNoteIndex(scene, index, line, downbeat) {
|
|
6062
|
+
const phrase = scene.plan.phrases[index];
|
|
6063
|
+
const sounding2 = line.findIndex(
|
|
6064
|
+
(note2) => note2.startBeat <= downbeat + BEAT_EPS && note2.startBeat + note2.durationBeat > downbeat + BEAT_EPS
|
|
6065
|
+
);
|
|
6066
|
+
return sounding2 >= 0 ? sounding2 : line.findIndex(
|
|
6067
|
+
(note2) => note2.startBeat >= downbeat - BEAT_EPS && note2.startBeat < phrase.endBeat - BEAT_EPS
|
|
6068
|
+
);
|
|
6069
|
+
}
|
|
6070
|
+
function repairCadence(scene, index, line, held) {
|
|
6071
|
+
const phrase = scene.plan.phrases[index];
|
|
6072
|
+
if (phrase.cadence === null) {
|
|
6073
|
+
return;
|
|
6074
|
+
}
|
|
6075
|
+
let target = held;
|
|
6076
|
+
for (let i = line.length - 1; target < 0 && i >= 0; i -= 1) {
|
|
6077
|
+
if (positionClass(line[i].startBeat, scene.plan.meters) !== "off") {
|
|
6078
|
+
target = i;
|
|
6079
|
+
}
|
|
6080
|
+
}
|
|
6081
|
+
if (target < 0) {
|
|
6082
|
+
return;
|
|
6083
|
+
}
|
|
6084
|
+
const note2 = line[target];
|
|
6085
|
+
const allowed = cadencePitchClasses(scene.plan, scene.timeline, phrase.cadence, note2.startBeat);
|
|
6086
|
+
if (allowed === null) {
|
|
6087
|
+
return;
|
|
6088
|
+
}
|
|
6089
|
+
const frame = scene.frames[index];
|
|
6090
|
+
const candidates = registerPitches(phrase).filter(
|
|
6091
|
+
(pitch) => allowed.includes(pitchClassOf(pitch))
|
|
6092
|
+
);
|
|
6093
|
+
if (candidates.length === 0) {
|
|
6094
|
+
throw new NoSolutionError(
|
|
6095
|
+
`no pitch in the register ${frame.low}..${frame.high} can end the ${phrase.cadence} cadence`,
|
|
6096
|
+
{ at: frame.at }
|
|
6097
|
+
);
|
|
6098
|
+
}
|
|
6099
|
+
if (allowed.includes(pitchClassOf(note2.pitch))) {
|
|
6100
|
+
return;
|
|
6101
|
+
}
|
|
6102
|
+
const slots = line.map(
|
|
6103
|
+
(n, i) => i === target ? { candidates, cost: frame.costAt(n.startBeat), fixed: false } : { candidates: [n.pitch], cost: () => 0, fixed: true }
|
|
6104
|
+
);
|
|
6105
|
+
const pitches = searchPitches(slots, frame.budget);
|
|
6106
|
+
if (pitches === null) {
|
|
6107
|
+
throw new NoSolutionError(`no cadence pitch joins its neighbours without an augmented step`, {
|
|
6108
|
+
at: frame.at
|
|
6109
|
+
});
|
|
6110
|
+
}
|
|
6111
|
+
note2.pitch = pitches[target];
|
|
6112
|
+
}
|
|
6113
|
+
function repickCutOnset(scene, index, line, held) {
|
|
6114
|
+
const note2 = line[held];
|
|
6115
|
+
const derived = scene.plan.motifs.some(
|
|
6116
|
+
(motif) => motif.from !== null && note2.startBeat >= motif.startBeat - BEAT_EPS && note2.startBeat < motif.endBeat - BEAT_EPS
|
|
6117
|
+
);
|
|
6118
|
+
if (scene.plan.phrases[index].cadence !== null || derived) {
|
|
6119
|
+
return;
|
|
6120
|
+
}
|
|
6121
|
+
const frame = scene.frames[index];
|
|
6122
|
+
const candidates = frame.candidatesAt(note2.startBeat);
|
|
6123
|
+
if (candidates.includes(note2.pitch)) {
|
|
6124
|
+
return;
|
|
6125
|
+
}
|
|
6126
|
+
const slots = line.map(
|
|
6127
|
+
(n, i) => i === held ? { candidates, cost: frame.costAt(n.startBeat), fixed: false } : { candidates: [n.pitch], cost: () => 0, fixed: true }
|
|
6128
|
+
);
|
|
6129
|
+
const pitches = searchPitches(slots, frame.budget);
|
|
6130
|
+
if (pitches === null) {
|
|
6131
|
+
throw new NoSolutionError(`no pitch at the final downbeat joins its neighbours`, {
|
|
6132
|
+
at: frame.at
|
|
6133
|
+
});
|
|
6134
|
+
}
|
|
6135
|
+
line[held] = { ...note2, pitch: pitches[held] };
|
|
6136
|
+
}
|
|
6137
|
+
function phraseLine(scene, index) {
|
|
6138
|
+
const line = [];
|
|
6139
|
+
for (const segment of segmentsOf(scene, index)) {
|
|
6140
|
+
line.push(
|
|
6141
|
+
...segment.motif === null ? freeSpan(scene, index, segment) : statementOf(scene, segment.motif)
|
|
6142
|
+
);
|
|
6143
|
+
}
|
|
6144
|
+
return line.sort((a, b) => a.startBeat - b.startBeat);
|
|
6145
|
+
}
|
|
6146
|
+
function chargePitchSearch(scene) {
|
|
6147
|
+
const { plan } = scene;
|
|
6148
|
+
let estimate = 0;
|
|
6149
|
+
plan.phrases.forEach((phrase, index) => {
|
|
6150
|
+
let searched = 0;
|
|
6151
|
+
for (const segment of segmentsOf(scene, index)) {
|
|
6152
|
+
const motif = segment.motif === null ? void 0 : plan.motifs[segment.motif];
|
|
6153
|
+
if (motif === void 0) {
|
|
6154
|
+
searched += freeNoteCount(scene, index, segment);
|
|
6155
|
+
} else if (motif.relation === null) {
|
|
6156
|
+
searched += motif.notes;
|
|
6157
|
+
} else {
|
|
6158
|
+
estimate += motif.notes * relationLevels(motif.relation.kind).length;
|
|
6159
|
+
}
|
|
6160
|
+
}
|
|
6161
|
+
const register = Math.max(0, phrase.register.high - phrase.register.low + 1);
|
|
6162
|
+
estimate += searched * SEARCH_STATES_SQUARED * register * register;
|
|
6163
|
+
});
|
|
6164
|
+
assertGenerationBudget(estimate, "melody pitch search", scene.budget);
|
|
6165
|
+
}
|
|
6166
|
+
function generateMelody(plan, opts) {
|
|
6167
|
+
assertCompositionPlan(plan);
|
|
6168
|
+
const asked = assertOptions(opts, "opts");
|
|
6169
|
+
if (asked.budget !== void 0) {
|
|
6170
|
+
assertPositiveInt(asked.budget, "budget", Number.MAX_SAFE_INTEGER);
|
|
6171
|
+
}
|
|
6172
|
+
const timeline = planChordTimeline(plan);
|
|
6173
|
+
const scene = {
|
|
6174
|
+
plan,
|
|
6175
|
+
ctx: melodyContext(plan, asked.ctx),
|
|
6176
|
+
budget: asked.budget,
|
|
6177
|
+
timeline,
|
|
6178
|
+
harmonyMisfits: planHarmonyMisfits(plan, timeline),
|
|
6179
|
+
scales: plan.keys.map((key) => scaleOf(key)),
|
|
6180
|
+
members: membersOf(plan),
|
|
6181
|
+
frames: [],
|
|
6182
|
+
statements: /* @__PURE__ */ new Map(),
|
|
6183
|
+
free: /* @__PURE__ */ new Map(),
|
|
6184
|
+
inProgress: /* @__PURE__ */ new Set()
|
|
6185
|
+
};
|
|
6186
|
+
scene.frames = plan.phrases.map((_, index) => frameOf(scene, index));
|
|
6187
|
+
chargePitchSearch(scene);
|
|
6188
|
+
const melody = [];
|
|
6189
|
+
plan.phrases.forEach((_, index) => {
|
|
6190
|
+
const line = phraseLine(scene, index);
|
|
6191
|
+
const downbeat = finalDownbeat(plan.phrases[index], plan.meters);
|
|
6192
|
+
let held = heldNoteIndex(scene, index, line, downbeat);
|
|
6193
|
+
if (held >= 0) {
|
|
6194
|
+
line.length = held + 1;
|
|
6195
|
+
const note2 = line[held];
|
|
6196
|
+
if (note2.startBeat < downbeat - BEAT_EPS) {
|
|
6197
|
+
const cut = downbeat - note2.startBeat;
|
|
6198
|
+
line[held] = { ...note2, durationBeat: cut };
|
|
6199
|
+
line.push({ ...note2, startBeat: downbeat, durationBeat: note2.durationBeat - cut });
|
|
6200
|
+
held += 1;
|
|
6201
|
+
repickCutOnset(scene, index, line, held);
|
|
6202
|
+
}
|
|
6203
|
+
}
|
|
6204
|
+
repairCadence(scene, index, line, held);
|
|
6205
|
+
if (held >= 0) {
|
|
6206
|
+
const note2 = line[held];
|
|
6207
|
+
const phraseEnd = plan.phrases[index].endBeat;
|
|
6208
|
+
line[held] = { ...note2, durationBeat: phraseEnd - note2.startBeat };
|
|
6209
|
+
}
|
|
6210
|
+
melody.push(...line);
|
|
6211
|
+
});
|
|
6212
|
+
melody.sort((a, b) => a.startBeat - b.startBeat);
|
|
6213
|
+
return motifToNoteEvents({ notes: melody });
|
|
6214
|
+
}
|
|
6215
|
+
|
|
6216
|
+
// src/generate/ornament/index.ts
|
|
6217
|
+
var ORNAMENT_STYLES = Object.freeze(["ghost", "flam", "drag", "slide", "accent"]);
|
|
6218
|
+
var DEFAULT_TS5 = { numerator: 4, denominator: 4 };
|
|
6219
|
+
var DEFAULT_STYLE2 = "ghost";
|
|
6220
|
+
var DEFAULT_AMOUNT = 0.5;
|
|
6221
|
+
var GHOST_VELOCITY_SCALE = 0.45;
|
|
6222
|
+
var ACCENT_VELOCITY_BOOST = 12;
|
|
6223
|
+
var DEFAULT_VELOCITY2 = 80;
|
|
6224
|
+
var MIN_VELOCITY2 = 1;
|
|
6225
|
+
var MAX_VELOCITY2 = 127;
|
|
6226
|
+
var SLIDE_MIN_SEMITONES = 1;
|
|
6227
|
+
function eligible(style, note2, previous, next, meter) {
|
|
6228
|
+
const strong = isStrongBeat(note2.startBeat, meter);
|
|
6229
|
+
switch (style) {
|
|
6230
|
+
case "ghost":
|
|
6231
|
+
return !strong;
|
|
6232
|
+
case "flam":
|
|
6233
|
+
case "accent":
|
|
6234
|
+
return strong;
|
|
6235
|
+
case "drag":
|
|
6236
|
+
return !strong && next !== void 0 && isStrongBeat(next.startBeat, meter);
|
|
6237
|
+
case "slide":
|
|
6238
|
+
return previous !== void 0 && Math.abs(note2.pitch - previous.pitch) >= SLIDE_MIN_SEMITONES;
|
|
6239
|
+
}
|
|
6240
|
+
}
|
|
6241
|
+
function articulationOf(style) {
|
|
6242
|
+
return style;
|
|
6243
|
+
}
|
|
6244
|
+
function ornamentedVelocity(style, note2) {
|
|
6245
|
+
if (note2.velocity === void 0 && style !== "ghost" && style !== "accent") {
|
|
6246
|
+
return void 0;
|
|
6247
|
+
}
|
|
6248
|
+
const velocity = note2.velocity ?? DEFAULT_VELOCITY2;
|
|
6249
|
+
if (style === "ghost") {
|
|
6250
|
+
return clampVelocity(velocity * GHOST_VELOCITY_SCALE);
|
|
6251
|
+
}
|
|
6252
|
+
if (style === "accent") {
|
|
6253
|
+
return clampVelocity(velocity + ACCENT_VELOCITY_BOOST);
|
|
6254
|
+
}
|
|
6255
|
+
return velocity;
|
|
6256
|
+
}
|
|
6257
|
+
function clampVelocity(velocity) {
|
|
6258
|
+
return Math.min(MAX_VELOCITY2, Math.max(MIN_VELOCITY2, Math.round(velocity)));
|
|
6259
|
+
}
|
|
6260
|
+
function ornament(notes, opts = {}) {
|
|
6261
|
+
const asked = assertOptions(opts, "opts");
|
|
6262
|
+
assertNoteEvents(notes, "ornament notes", { allowNonPositiveDuration: true });
|
|
6263
|
+
const style = assertOneOf(asked.style ?? DEFAULT_STYLE2, ORNAMENT_STYLES, "ornament style");
|
|
6264
|
+
const meter = toMeterData(asked.ts ?? DEFAULT_TS5, "ts");
|
|
6265
|
+
if (asked.amount !== void 0) {
|
|
6266
|
+
assertRange(asked.amount, 0, 1, "ornament amount");
|
|
6267
|
+
}
|
|
6268
|
+
const ctx = resolveContextWith(asked.ctx, { ornament: asked.amount });
|
|
6269
|
+
const amount = ctx.ornament ?? DEFAULT_AMOUNT;
|
|
6270
|
+
const draw = ctx.part("ornament");
|
|
6271
|
+
const sounding2 = dropSilentNotes(notes);
|
|
6272
|
+
const ordered = [...sounding2].sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);
|
|
6273
|
+
const previousOf = previousOnsets(ordered);
|
|
6274
|
+
const nextOf = nextOnsets(ordered);
|
|
6275
|
+
const gaps = strokeGaps(ordered);
|
|
6276
|
+
return sounding2.map((note2) => {
|
|
6277
|
+
const decorated = { ...note2 };
|
|
6278
|
+
if (note2.articulation !== void 0 || !eligible(style, note2, previousOf.get(note2), nextOf.get(note2), meter)) {
|
|
6279
|
+
return decorated;
|
|
6280
|
+
}
|
|
6281
|
+
if (!sustainsStrokes(gaps.get(note2) ?? Number.POSITIVE_INFINITY, ctx.bpm, ctx.difficulty)) {
|
|
6282
|
+
return decorated;
|
|
6283
|
+
}
|
|
6284
|
+
if (!draw.prob(amount, style, note2.startBeat, note2.pitch)) {
|
|
6285
|
+
return decorated;
|
|
6286
|
+
}
|
|
6287
|
+
decorated.articulation = articulationOf(style);
|
|
6288
|
+
const velocity = ornamentedVelocity(style, note2);
|
|
6289
|
+
if (velocity !== void 0) {
|
|
6290
|
+
decorated.velocity = velocity;
|
|
6291
|
+
}
|
|
6292
|
+
return decorated;
|
|
6293
|
+
});
|
|
6294
|
+
}
|
|
6295
|
+
function previousOnsets(ordered) {
|
|
6296
|
+
const previousOf = /* @__PURE__ */ new Map();
|
|
6297
|
+
let earlier;
|
|
6298
|
+
for (const note2 of ordered) {
|
|
6299
|
+
if (earlier !== void 0 && earlier.startBeat === note2.startBeat) {
|
|
6300
|
+
previousOf.set(note2, previousOf.get(earlier));
|
|
6301
|
+
} else {
|
|
6302
|
+
previousOf.set(note2, earlier);
|
|
6303
|
+
}
|
|
6304
|
+
earlier = note2;
|
|
6305
|
+
}
|
|
6306
|
+
return previousOf;
|
|
6307
|
+
}
|
|
6308
|
+
function nextOnsets(ordered) {
|
|
6309
|
+
const nextOf = /* @__PURE__ */ new Map();
|
|
6310
|
+
let later;
|
|
6311
|
+
for (let index = ordered.length - 1; index >= 0; index -= 1) {
|
|
6312
|
+
const note2 = ordered[index];
|
|
6313
|
+
if (!note2) continue;
|
|
6314
|
+
if (later !== void 0 && later.startBeat === note2.startBeat) {
|
|
6315
|
+
nextOf.set(note2, nextOf.get(later));
|
|
6316
|
+
} else {
|
|
6317
|
+
nextOf.set(note2, later);
|
|
6318
|
+
}
|
|
6319
|
+
later = note2;
|
|
6320
|
+
}
|
|
6321
|
+
return nextOf;
|
|
6322
|
+
}
|
|
6323
|
+
function strokeGaps(ordered) {
|
|
6324
|
+
const distinct = [...new Set(ordered.map((note2) => note2.startBeat))].sort((a, b) => a - b);
|
|
6325
|
+
const gapAt = /* @__PURE__ */ new Map();
|
|
6326
|
+
for (let index = 0; index < distinct.length; index += 1) {
|
|
6327
|
+
const beat = distinct[index] ?? 0;
|
|
6328
|
+
const before = index === 0 ? Number.POSITIVE_INFINITY : beat - (distinct[index - 1] ?? 0);
|
|
6329
|
+
const after = index === distinct.length - 1 ? Number.POSITIVE_INFINITY : (distinct[index + 1] ?? 0) - beat;
|
|
6330
|
+
gapAt.set(beat, Math.min(before, after));
|
|
6331
|
+
}
|
|
6332
|
+
const gaps = /* @__PURE__ */ new Map();
|
|
6333
|
+
for (const note2 of ordered) {
|
|
6334
|
+
gaps.set(note2, gapAt.get(note2.startBeat) ?? Number.POSITIVE_INFINITY);
|
|
6335
|
+
}
|
|
6336
|
+
return gaps;
|
|
6337
|
+
}
|
|
6338
|
+
|
|
6339
|
+
// src/generate/plan/derive.ts
|
|
6340
|
+
var PRESERVE_ASPECTS = [
|
|
6341
|
+
"form",
|
|
6342
|
+
"phraseLengths",
|
|
6343
|
+
"harmonicFunction",
|
|
6344
|
+
"harmonicRhythm",
|
|
6345
|
+
"motifRelations",
|
|
6346
|
+
"registerShape",
|
|
6347
|
+
"rhythm"
|
|
6348
|
+
];
|
|
6349
|
+
var NEUTRAL_SHAPE = "arch";
|
|
6350
|
+
var NEUTRAL_PEAK_POSITION = 0.6;
|
|
6351
|
+
var SHAPE_KEEP_THRESHOLD = 0.5;
|
|
6352
|
+
var NEUTRAL_ONSET_LEVELS = Object.freeze([0, 1, 2, 3, 4, 5].map((level) => level / 15));
|
|
6353
|
+
var FUNCTION_DEGREES = {
|
|
6354
|
+
tonic: [1, 6, 3],
|
|
6355
|
+
subdominant: [4, 2],
|
|
6356
|
+
dominant: [5, 7]
|
|
6357
|
+
};
|
|
6358
|
+
var STRONG_WEIGHT2 = 2;
|
|
6359
|
+
var HEPTATONIC_DEGREES = 7;
|
|
6360
|
+
function mod12(value) {
|
|
6361
|
+
return (value % 12 + 12) % 12;
|
|
6362
|
+
}
|
|
6363
|
+
function lerp(weight, reference, neutral) {
|
|
6364
|
+
return weight * reference + (1 - weight) * neutral;
|
|
6365
|
+
}
|
|
6366
|
+
function readPreserve(value) {
|
|
6367
|
+
const read = value === void 0 ? {} : assertRecord(value, "opts.preserve");
|
|
6368
|
+
const weights = {};
|
|
6369
|
+
for (const aspect of PRESERVE_ASPECTS) {
|
|
6370
|
+
const weight = read[aspect];
|
|
6371
|
+
weights[aspect] = weight === void 0 ? 1 : assertRange(weight, 0, 1, `opts.preserve.${aspect}`);
|
|
6372
|
+
}
|
|
6373
|
+
return weights;
|
|
6374
|
+
}
|
|
6375
|
+
function readRegister(value) {
|
|
6376
|
+
const read = assertRecord(value, "opts.register");
|
|
6377
|
+
const low = assertMidiPitch(read.low, "opts.register.low");
|
|
6378
|
+
const high = assertMidiPitch(read.high, "opts.register.high");
|
|
6379
|
+
if (low > high) {
|
|
6380
|
+
throw new InvalidInputError(
|
|
6381
|
+
`opts.register.low must not exceed opts.register.high; received ${low} > ${high}`
|
|
6382
|
+
);
|
|
6383
|
+
}
|
|
6384
|
+
return { low, high };
|
|
6385
|
+
}
|
|
6386
|
+
function rejectSuppliedRng(ctx) {
|
|
6387
|
+
if (typeof ctx === "object" && ctx !== null && ctx.rng !== void 0) {
|
|
6388
|
+
throw new InvalidInputError(
|
|
6389
|
+
"opts.ctx.rng is not accepted: a composition plan records its seed, not a source"
|
|
6390
|
+
);
|
|
6391
|
+
}
|
|
6392
|
+
}
|
|
6393
|
+
function plannedKeys(profile, requested) {
|
|
6394
|
+
const regions = profile.harmony.keys.map(({ key }) => {
|
|
6395
|
+
const resolved = resolveKey(key);
|
|
6396
|
+
return {
|
|
6397
|
+
scale: { ...resolved.scale },
|
|
6398
|
+
tonic: { ...resolved.tonic },
|
|
6399
|
+
variant: resolved.variant
|
|
6400
|
+
};
|
|
6401
|
+
});
|
|
6402
|
+
const home = regions[0];
|
|
6403
|
+
if (requested === void 0) {
|
|
6404
|
+
return regions;
|
|
6405
|
+
}
|
|
6406
|
+
const target = resolveKey(requested);
|
|
6407
|
+
if (target.variant !== home.variant || target.scale.modeMask12 !== home.scale.modeMask12) {
|
|
6408
|
+
throw new InvalidInputError(
|
|
6409
|
+
"opts.key must be in the same mode as the reference's home key; a key change moves the tonic only"
|
|
6410
|
+
);
|
|
6411
|
+
}
|
|
6412
|
+
const step = spelledInterval(
|
|
6413
|
+
{ letter: home.tonic.letter, alter: home.tonic.alter },
|
|
6414
|
+
{ letter: target.tonic.letter, alter: target.tonic.alter }
|
|
6415
|
+
);
|
|
6416
|
+
const moved = regions.map(
|
|
6417
|
+
(key) => ({
|
|
6418
|
+
scale: {
|
|
6419
|
+
rootPc: mod12(key.scale.rootPc + step.semitones),
|
|
6420
|
+
modeMask12: key.scale.modeMask12
|
|
6421
|
+
},
|
|
6422
|
+
tonic: transposeByInterval({ letter: key.tonic.letter, alter: key.tonic.alter }, step),
|
|
6423
|
+
variant: key.variant
|
|
6424
|
+
})
|
|
6425
|
+
);
|
|
6426
|
+
moved[0] = target;
|
|
6427
|
+
return moved;
|
|
6428
|
+
}
|
|
6429
|
+
function nearestBoundary(beat, boundaries) {
|
|
6430
|
+
let best = beat;
|
|
6431
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
6432
|
+
for (const boundary of boundaries) {
|
|
6433
|
+
const distance = Math.abs(boundary - beat);
|
|
6434
|
+
if (distance < bestDistance - BEAT_EPS) {
|
|
6435
|
+
best = boundary;
|
|
6436
|
+
bestDistance = distance;
|
|
6437
|
+
}
|
|
6438
|
+
}
|
|
6439
|
+
return best;
|
|
6440
|
+
}
|
|
6441
|
+
function plannedSections(profile, weight, draw) {
|
|
6442
|
+
const sections = profile.form.sections;
|
|
6443
|
+
const boundaries = [profile.span.startBeat, profile.span.endBeat];
|
|
6444
|
+
for (const phrase of profile.form.phrases) {
|
|
6445
|
+
boundaries.push(phrase.startBeat, phrase.endBeat);
|
|
6446
|
+
}
|
|
6447
|
+
boundaries.sort((a, b) => a - b);
|
|
6448
|
+
const planned = [];
|
|
6449
|
+
sections.forEach((section, index) => {
|
|
6450
|
+
const keep = sections.length < 2 || draw.prob(weight, "form", index);
|
|
6451
|
+
const neighbour = index + 1 < sections.length ? index + 1 : index - 1;
|
|
6452
|
+
const startBeat = nearestBoundary(section.startBeat, boundaries);
|
|
6453
|
+
const endBeat = nearestBoundary(section.endBeat, boundaries);
|
|
6454
|
+
if (endBeat - startBeat > BEAT_EPS) {
|
|
6455
|
+
planned.push({
|
|
6456
|
+
label: keep ? section.label : sections[neighbour]?.label ?? section.label,
|
|
6457
|
+
startBeat,
|
|
6458
|
+
endBeat
|
|
6459
|
+
});
|
|
6460
|
+
}
|
|
6461
|
+
});
|
|
6462
|
+
return planned;
|
|
6463
|
+
}
|
|
6464
|
+
function beatAfterBars(startBeat, bars, meters) {
|
|
6465
|
+
let beat = startBeat;
|
|
6466
|
+
let left = bars;
|
|
6467
|
+
while (left > BEAT_EPS) {
|
|
6468
|
+
const step = Math.min(1, left);
|
|
6469
|
+
beat += step * beatsPerBarAt(beat, meters);
|
|
6470
|
+
left -= step;
|
|
6471
|
+
}
|
|
6472
|
+
return beat;
|
|
6473
|
+
}
|
|
6474
|
+
function barsBetween(startBeat, endBeat, meters) {
|
|
6475
|
+
let beat = startBeat;
|
|
6476
|
+
let bars = 0;
|
|
6477
|
+
while (beat < endBeat - BEAT_EPS) {
|
|
6478
|
+
const barBeats = beatsPerBarAt(beat, meters);
|
|
6479
|
+
const step = Math.min(barStartBeat(beat, meters) + barBeats, endBeat) - beat;
|
|
6480
|
+
bars += step / barBeats;
|
|
6481
|
+
beat += step;
|
|
6482
|
+
}
|
|
6483
|
+
return bars;
|
|
6484
|
+
}
|
|
6485
|
+
function spanSegments(profile, sections) {
|
|
6486
|
+
const { startBeat, endBeat } = profile.span;
|
|
6487
|
+
const cuts = [startBeat, endBeat];
|
|
6488
|
+
for (const section of sections) {
|
|
6489
|
+
for (const beat of [section.startBeat, section.endBeat]) {
|
|
6490
|
+
if (beat > startBeat + BEAT_EPS && beat < endBeat - BEAT_EPS) cuts.push(beat);
|
|
6491
|
+
}
|
|
6492
|
+
}
|
|
6493
|
+
cuts.sort((a, b) => a - b);
|
|
6494
|
+
const segments = [];
|
|
6495
|
+
for (let index = 1; index < cuts.length; index += 1) {
|
|
6496
|
+
const from = cuts[index - 1];
|
|
6497
|
+
const to = cuts[index];
|
|
6498
|
+
if (to - from > BEAT_EPS) segments.push([from, to]);
|
|
6499
|
+
}
|
|
6500
|
+
return segments;
|
|
6501
|
+
}
|
|
6502
|
+
function plannedPhraseSpans(profile, sections, weight, draw) {
|
|
6503
|
+
const segments = spanSegments(profile, sections);
|
|
6504
|
+
const pieces = profile.form.phrases.map((phrase, source) => ({
|
|
6505
|
+
startBeat: phrase.startBeat,
|
|
6506
|
+
endBeat: phrase.endBeat,
|
|
6507
|
+
source,
|
|
6508
|
+
cadence: phrase.cadence?.type ?? null
|
|
6509
|
+
}));
|
|
6510
|
+
const replaced = pieces.map((_, index) => !draw.prob(weight, "phraseLengths", index));
|
|
6511
|
+
const spans = [];
|
|
6512
|
+
for (const [segmentStart, segmentEnd] of segments) {
|
|
6513
|
+
const members = pieces.map((piece, index) => ({ piece, index })).filter(
|
|
6514
|
+
({ piece }) => piece.startBeat >= segmentStart - BEAT_EPS && piece.startBeat < segmentEnd - BEAT_EPS
|
|
6515
|
+
);
|
|
6516
|
+
const laid = [];
|
|
6517
|
+
let cursor = segmentStart;
|
|
6518
|
+
for (const { piece, index } of members) {
|
|
6519
|
+
const lengths = [];
|
|
6520
|
+
if (replaced[index]) {
|
|
6521
|
+
const bars = barsBetween(piece.startBeat, piece.endBeat, profile.meters);
|
|
6522
|
+
const rounded = 2 ** Math.round(Math.log2(bars));
|
|
6523
|
+
const overflows = beatAfterBars(cursor, rounded, profile.meters) > segmentEnd + BEAT_EPS;
|
|
6524
|
+
let partStart = cursor;
|
|
6525
|
+
for (const part of overflows ? [rounded / 2, rounded / 2] : [rounded]) {
|
|
6526
|
+
const partEnd = beatAfterBars(partStart, part, profile.meters);
|
|
6527
|
+
lengths.push(partEnd - partStart);
|
|
6528
|
+
partStart = partEnd;
|
|
6529
|
+
}
|
|
6530
|
+
} else {
|
|
6531
|
+
lengths.push(piece.endBeat - piece.startBeat);
|
|
6532
|
+
}
|
|
6533
|
+
lengths.forEach((length, part) => {
|
|
6534
|
+
if (cursor >= segmentEnd - BEAT_EPS) return;
|
|
6535
|
+
const end = cursor + length;
|
|
6536
|
+
laid.push({
|
|
6537
|
+
startBeat: cursor,
|
|
6538
|
+
endBeat: Math.min(end, segmentEnd),
|
|
6539
|
+
source: piece.source,
|
|
6540
|
+
cadence: part === lengths.length - 1 ? piece.cadence : null
|
|
6541
|
+
});
|
|
6542
|
+
cursor = end;
|
|
6543
|
+
});
|
|
6544
|
+
}
|
|
6545
|
+
const last = laid.at(-1);
|
|
6546
|
+
if (last) {
|
|
6547
|
+
last.endBeat = segmentEnd;
|
|
6548
|
+
} else {
|
|
6549
|
+
laid.push({
|
|
6550
|
+
startBeat: segmentStart,
|
|
6551
|
+
endBeat: segmentEnd,
|
|
6552
|
+
source: spans.at(-1)?.source ?? (profile.form.phrases.length > 0 ? 0 : -1),
|
|
6553
|
+
cadence: null
|
|
6554
|
+
});
|
|
6555
|
+
}
|
|
6556
|
+
spans.push(...laid);
|
|
6557
|
+
}
|
|
6558
|
+
return spans;
|
|
6559
|
+
}
|
|
6560
|
+
function sectionAt(sections, beat) {
|
|
6561
|
+
const index = sections.findIndex(
|
|
6562
|
+
(section) => beat >= section.startBeat - BEAT_EPS && beat < section.endBeat - BEAT_EPS
|
|
6563
|
+
);
|
|
6564
|
+
return index < 0 ? null : index;
|
|
6565
|
+
}
|
|
6566
|
+
function phraseHolding(phrases, startBeat, endBeat) {
|
|
6567
|
+
return phrases.findIndex(
|
|
6568
|
+
(phrase) => startBeat >= phrase.startBeat - BEAT_EPS && endBeat <= phrase.endBeat + BEAT_EPS
|
|
6569
|
+
);
|
|
6570
|
+
}
|
|
6571
|
+
function better(a, b) {
|
|
6572
|
+
for (let field = 0; field < a.length; field += 1) {
|
|
6573
|
+
if (a[field] !== b[field]) return a[field] > b[field];
|
|
6574
|
+
}
|
|
6575
|
+
return false;
|
|
6576
|
+
}
|
|
6577
|
+
function scheduleMotifs(candidates, meters) {
|
|
6578
|
+
const order = [...candidates].sort((a, b) => a.startBeat - b.startBeat || a.node - b.node);
|
|
6579
|
+
const count = order.length;
|
|
6580
|
+
const firstFrom = (from, beat) => {
|
|
6581
|
+
let low = from;
|
|
6582
|
+
let high = count;
|
|
6583
|
+
while (low < high) {
|
|
6584
|
+
const mid = low + high >> 1;
|
|
6585
|
+
if (order[mid].startBeat < beat - BEAT_EPS) low = mid + 1;
|
|
6586
|
+
else high = mid;
|
|
6587
|
+
}
|
|
6588
|
+
return low;
|
|
6589
|
+
};
|
|
6590
|
+
const next = order.map((candidate, index2) => firstFrom(index2 + 1, candidate.endBeat));
|
|
6591
|
+
const own = order.map(
|
|
6592
|
+
(candidate) => [
|
|
6593
|
+
candidate.notes,
|
|
6594
|
+
metricWeight(candidate.startBeat, meters) < STRONG_WEIGHT2 ? -1 : 0,
|
|
6595
|
+
1
|
|
6596
|
+
]
|
|
6597
|
+
);
|
|
6598
|
+
const taking = (index2) => {
|
|
6599
|
+
const rest = best[next[index2]];
|
|
6600
|
+
const mine = own[index2];
|
|
6601
|
+
return [mine[0] + rest[0], mine[1] + rest[1], mine[2] + rest[2]];
|
|
6602
|
+
};
|
|
6603
|
+
const best = new Array(count + 1).fill([0, 0, 0]);
|
|
6604
|
+
for (let index2 = count - 1; index2 >= 0; index2 -= 1) {
|
|
6605
|
+
const taken = taking(index2);
|
|
6606
|
+
const skipped = best[index2 + 1];
|
|
6607
|
+
best[index2] = better(taken, skipped) ? taken : skipped;
|
|
6608
|
+
}
|
|
6609
|
+
const chosen = [];
|
|
6610
|
+
let index = 0;
|
|
6611
|
+
while (index < count) {
|
|
6612
|
+
if (!better(best[index], taking(index))) {
|
|
6613
|
+
chosen.push(order[index]);
|
|
6614
|
+
index = next[index];
|
|
6615
|
+
} else {
|
|
6616
|
+
index += 1;
|
|
6617
|
+
}
|
|
6618
|
+
}
|
|
6619
|
+
return chosen.sort((a, b) => a.node - b.node);
|
|
6620
|
+
}
|
|
6621
|
+
function selectedDerivation(graph, parentEdge, selectedAt, node) {
|
|
6622
|
+
let plain = true;
|
|
6623
|
+
let semitones = 0;
|
|
6624
|
+
let steps = 0;
|
|
6625
|
+
for (let edge = parentEdge.get(node); edge !== void 0; edge = parentEdge.get(edge.from)) {
|
|
6626
|
+
const relation = edge.relation;
|
|
6627
|
+
steps += 1;
|
|
6628
|
+
plain &&= relation !== null && (relation.kind === "repetition" || relation.kind === "transposition" && Math.abs(relation.timeRatio - 1) <= BEAT_EPS);
|
|
6629
|
+
semitones += relation?.semitones ?? 0;
|
|
6630
|
+
const from = selectedAt.get(edge.from);
|
|
6631
|
+
if (from === void 0) continue;
|
|
6632
|
+
if (steps === 1) {
|
|
6633
|
+
return { from, relation: relation === null ? null : { ...relation } };
|
|
6634
|
+
}
|
|
6635
|
+
if (!plain) {
|
|
6636
|
+
return { from: null, relation: null };
|
|
6637
|
+
}
|
|
6638
|
+
const gap = (graph.nodes[node]?.startBeat ?? 0) - (graph.nodes[edge.from]?.endBeat ?? 0);
|
|
6639
|
+
return {
|
|
6640
|
+
from,
|
|
6641
|
+
relation: {
|
|
6642
|
+
kind: semitones === 0 ? "repetition" : "transposition",
|
|
6643
|
+
sequence: gap >= -HUMANIZE_ADJACENCY && gap <= HUMANIZE_ADJACENCY,
|
|
6644
|
+
semitones,
|
|
6645
|
+
timeRatio: 1
|
|
6646
|
+
}
|
|
6647
|
+
};
|
|
6648
|
+
}
|
|
6649
|
+
return { from: null, relation: null };
|
|
6650
|
+
}
|
|
6651
|
+
function functionTriad(fn, degree, key) {
|
|
6652
|
+
if (fn === "dominant" && cadencesThroughRaisedSeventh(key.scale)) {
|
|
6653
|
+
if (degree === 5) {
|
|
6654
|
+
return makeChord(pitchClassOf(key.scale.rootPc + 7), "maj");
|
|
6655
|
+
}
|
|
6656
|
+
if (degree === 7) {
|
|
6657
|
+
return makeChord(leadingTonePcOf(key.scale), "dim");
|
|
6658
|
+
}
|
|
6659
|
+
}
|
|
6660
|
+
return diatonicTriad(degree, key);
|
|
6661
|
+
}
|
|
6662
|
+
function functionCandidates(fn, key) {
|
|
6663
|
+
if (scaleTonesInDegreeOrder(key).length !== HEPTATONIC_DEGREES) {
|
|
6664
|
+
return null;
|
|
6665
|
+
}
|
|
6666
|
+
const numerals = FUNCTION_DEGREES[fn].map((degree) => functionTriad(fn, degree, key)).filter((chord) => functionOf(chord, key) === fn).map((chord) => chordToRoman(chord, key));
|
|
6667
|
+
return numerals.length === 0 ? null : numerals;
|
|
6668
|
+
}
|
|
6669
|
+
function plannedHarmony(profile, keys, preserve, draw) {
|
|
6670
|
+
const chords = profile.harmony.chords;
|
|
6671
|
+
const romans = chords.map((chord, index) => {
|
|
6672
|
+
if (chord.level !== "structural" || draw.prob(preserve.harmonicFunction, "harmonicFunction", index)) {
|
|
6673
|
+
return chord.roman;
|
|
6674
|
+
}
|
|
6675
|
+
const candidates = functionCandidates(chord.function, keys[chord.key]);
|
|
6676
|
+
if (candidates === null) {
|
|
6677
|
+
return chord.roman;
|
|
6678
|
+
}
|
|
6679
|
+
const pick = draw.range(0, candidates.length - 1, "harmonicFunction", index, "pick");
|
|
6680
|
+
return candidates[pick];
|
|
6681
|
+
});
|
|
6682
|
+
const starts = [];
|
|
6683
|
+
chords.forEach((chord, index) => {
|
|
6684
|
+
const keep = index === 0 || draw.prob(preserve.harmonicRhythm, "harmonicRhythm", index);
|
|
6685
|
+
const start = keep ? chord.startBeat : barStartBeat(chord.startBeat, profile.meters);
|
|
6686
|
+
starts.push(index === 0 ? start : Math.max(start, starts[index - 1]));
|
|
6687
|
+
});
|
|
6688
|
+
const planned = [];
|
|
6689
|
+
chords.forEach((chord, index) => {
|
|
6690
|
+
const next = chords[index + 1];
|
|
6691
|
+
const nextStart = starts[index + 1];
|
|
6692
|
+
let endBeat = chord.endBeat;
|
|
6693
|
+
if (next !== void 0 && nextStart !== void 0) {
|
|
6694
|
+
endBeat = Math.abs(chord.endBeat - next.startBeat) <= BEAT_EPS ? nextStart : Math.min(chord.endBeat, nextStart);
|
|
6695
|
+
}
|
|
6696
|
+
const startBeat = starts[index];
|
|
6697
|
+
if (endBeat - startBeat > BEAT_EPS) {
|
|
6698
|
+
planned.push({ startBeat, endBeat, key: chord.key, roman: romans[index] });
|
|
6699
|
+
}
|
|
6700
|
+
});
|
|
6701
|
+
return planned;
|
|
6702
|
+
}
|
|
6703
|
+
function plannedRhythm(profile, weight) {
|
|
6704
|
+
const reference = profile.melody.rhythm;
|
|
6705
|
+
return {
|
|
6706
|
+
onsetLevels: reference.onsetLevels.map(
|
|
6707
|
+
(share, level) => lerp(weight, share, NEUTRAL_ONSET_LEVELS[level])
|
|
6708
|
+
),
|
|
6709
|
+
interOnsetShares: [...reference.interOnsetShares],
|
|
6710
|
+
syncopation: lerp(weight, reference.syncopation, 0)
|
|
6711
|
+
};
|
|
6712
|
+
}
|
|
6713
|
+
function deriveCompositionPlan(reference, opts) {
|
|
6714
|
+
const profile = assertReferenceProfile(reference, "reference");
|
|
6715
|
+
const asked = assertOptions(opts, "opts");
|
|
6716
|
+
const preserve = readPreserve(asked.preserve);
|
|
6717
|
+
const requestedRegister = asked.register === void 0 ? void 0 : readRegister(asked.register);
|
|
6718
|
+
rejectSuppliedRng(asked.ctx);
|
|
6719
|
+
const ctx = resolveContext(asked.ctx);
|
|
6720
|
+
const draw = ctx.part("plan");
|
|
6721
|
+
const melodyRegister = profile.melody.register;
|
|
6722
|
+
if (profile.melody.rhythm.onsets === 0 || melodyRegister === null) {
|
|
6723
|
+
throw new InvalidInputError("reference must carry melody onsets to derive a plan from");
|
|
6724
|
+
}
|
|
6725
|
+
const keys = plannedKeys(profile, asked.key);
|
|
6726
|
+
const homeShift = mod12(
|
|
6727
|
+
keys[0].scale.rootPc - (profile.harmony.keys[0]?.key.scale.rootPc ?? 0)
|
|
6728
|
+
);
|
|
6729
|
+
const shift = homeShift >= 6 ? homeShift - 12 : homeShift;
|
|
6730
|
+
const source = { low: melodyRegister.low + shift, high: melodyRegister.high + shift };
|
|
6731
|
+
if (source.low < 0 || source.high > 127) {
|
|
6732
|
+
throw new InvalidInputError(
|
|
6733
|
+
`the reference melody range moved to the new key leaves the MIDI range; received ${source.low}..${source.high}`
|
|
6734
|
+
);
|
|
6735
|
+
}
|
|
6736
|
+
const target = requestedRegister ?? source;
|
|
6737
|
+
const scale = source.high === source.low ? 0 : (target.high - target.low) / (source.high - source.low);
|
|
6738
|
+
const offset = source.high === source.low ? (target.low + target.high) / 2 : target.low - source.low * scale;
|
|
6739
|
+
const mapPitch = (pitch) => (pitch + shift) * scale + offset;
|
|
6740
|
+
const neutralRegister = {
|
|
6741
|
+
low: target.low,
|
|
6742
|
+
high: target.high,
|
|
6743
|
+
mean: (target.low + target.high) / 2
|
|
6744
|
+
};
|
|
6745
|
+
const sections = plannedSections(profile, preserve.form, draw);
|
|
6746
|
+
const shapeWeight = preserve.registerShape;
|
|
6747
|
+
const phraseBars = profile.form.phrases.reduce(
|
|
6748
|
+
(bars, phrase) => bars + barIndexAt(phrase.endBeat, profile.meters) - barIndexAt(phrase.startBeat, profile.meters),
|
|
6749
|
+
0
|
|
6750
|
+
);
|
|
6751
|
+
assertGenerationBudget(phraseBars, "plan phrase bars");
|
|
6752
|
+
const spans = plannedPhraseSpans(profile, sections, preserve.phraseLengths, draw);
|
|
6753
|
+
const phrases = spans.map((span) => {
|
|
6754
|
+
const melody = profile.form.phrases[span.source]?.melody ?? null;
|
|
6755
|
+
const register = melody ? {
|
|
6756
|
+
low: Math.round(lerp(shapeWeight, mapPitch(melody.low), neutralRegister.low)),
|
|
6757
|
+
high: Math.round(lerp(shapeWeight, mapPitch(melody.high), neutralRegister.high)),
|
|
6758
|
+
mean: lerp(shapeWeight, mapPitch(melody.mean), neutralRegister.mean)
|
|
6759
|
+
} : { ...neutralRegister };
|
|
6760
|
+
return {
|
|
6761
|
+
startBeat: span.startBeat,
|
|
6762
|
+
endBeat: span.endBeat,
|
|
6763
|
+
section: sectionAt(sections, span.startBeat),
|
|
6764
|
+
cadence: span.cadence,
|
|
6765
|
+
shape: melody && shapeWeight >= SHAPE_KEEP_THRESHOLD ? melody.shape : NEUTRAL_SHAPE,
|
|
6766
|
+
peakPosition: melody ? lerp(shapeWeight, melody.peakPosition, NEUTRAL_PEAK_POSITION) : NEUTRAL_PEAK_POSITION,
|
|
6767
|
+
register,
|
|
6768
|
+
onsetDensity: melody ? melody.onsetDensity : profile.melody.rhythm.onsetDensity,
|
|
6769
|
+
motifs: []
|
|
6770
|
+
};
|
|
6771
|
+
});
|
|
6772
|
+
const graph = profile.melody.graph;
|
|
6773
|
+
const noteCount = (node) => (profile.melody.motifs[graph.nodes[node]?.motif ?? -1]?.intervals.length ?? 0) + 1;
|
|
6774
|
+
assertGenerationBudget(graph.nodes.length * phrases.length, "plan motif selection", asked.budget);
|
|
6775
|
+
const candidates = [];
|
|
6776
|
+
graph.nodes.forEach((node, index) => {
|
|
6777
|
+
const phrase = phraseHolding(phrases, node.startBeat, node.endBeat);
|
|
6778
|
+
if (phrase >= 0) {
|
|
6779
|
+
candidates.push({
|
|
6780
|
+
node: index,
|
|
6781
|
+
phrase,
|
|
6782
|
+
startBeat: node.startBeat,
|
|
6783
|
+
endBeat: node.endBeat,
|
|
6784
|
+
notes: noteCount(index)
|
|
6785
|
+
});
|
|
6786
|
+
}
|
|
6787
|
+
});
|
|
6788
|
+
const selected = scheduleMotifs(candidates, profile.meters);
|
|
6789
|
+
const selectedAt = new Map(selected.map((candidate, index) => [candidate.node, index]));
|
|
6790
|
+
const parentEdge = new Map(graph.edges.map((edge) => [edge.to, edge]));
|
|
6791
|
+
const motifs = selected.map((candidate, index) => {
|
|
6792
|
+
const { from, relation } = selectedDerivation(graph, parentEdge, selectedAt, candidate.node);
|
|
6793
|
+
const keepRelation = draw.prob(preserve.motifRelations, "motifRelations", index);
|
|
6794
|
+
const keepRhythm = from === null && draw.prob(preserve.rhythm, "rhythm", index);
|
|
6795
|
+
const cell = profile.melody.motifs[graph.nodes[candidate.node]?.motif ?? -1];
|
|
6796
|
+
phrases[candidate.phrase]?.motifs.push(index);
|
|
6797
|
+
return {
|
|
6798
|
+
phrase: candidate.phrase,
|
|
6799
|
+
startBeat: candidate.startBeat,
|
|
6800
|
+
endBeat: candidate.endBeat,
|
|
6801
|
+
notes: candidate.notes,
|
|
6802
|
+
rhythm: keepRhythm && cell !== void 0 ? cell.rhythm.map((ratio) => ratio * cell.unitBeats) : null,
|
|
6803
|
+
from,
|
|
6804
|
+
relation: keepRelation ? relation : null
|
|
6805
|
+
};
|
|
6806
|
+
});
|
|
6807
|
+
return {
|
|
6808
|
+
planVersion: COMPOSITION_PLAN_VERSION,
|
|
6809
|
+
seed: ctx.seed,
|
|
6810
|
+
algorithmVersion: ctx.algorithmVersion,
|
|
6811
|
+
keys,
|
|
6812
|
+
meters: profile.meters.map((entry) => ({
|
|
6813
|
+
startBeat: entry.startBeat,
|
|
6814
|
+
ts: entry.ts.grouping === void 0 ? { ...entry.ts } : { ...entry.ts, grouping: [...entry.ts.grouping] }
|
|
6815
|
+
})),
|
|
6816
|
+
span: { ...profile.span },
|
|
6817
|
+
sections,
|
|
6818
|
+
phrases,
|
|
6819
|
+
harmony: plannedHarmony(profile, keys, preserve, draw),
|
|
6820
|
+
motifs,
|
|
6821
|
+
rhythm: plannedRhythm(profile, preserve.rhythm)
|
|
6822
|
+
};
|
|
6823
|
+
}
|
|
6824
|
+
|
|
6825
|
+
// src/generate/plan/evaluate.ts
|
|
6826
|
+
var PEAK_POSITION_TOLERANCE = 0.2;
|
|
6827
|
+
var OUTLINE_POINTS = 8;
|
|
6828
|
+
var HELD_NOTE_BEATS = 1;
|
|
6829
|
+
function interiorBoundaries(plan) {
|
|
6830
|
+
const beats = /* @__PURE__ */ new Set();
|
|
6831
|
+
for (const phrase of plan.phrases) {
|
|
6832
|
+
for (const beat of [phrase.startBeat, phrase.endBeat]) {
|
|
6833
|
+
if (beat > plan.span.startBeat + BEAT_EPS && beat < plan.span.endBeat - BEAT_EPS) {
|
|
6834
|
+
beats.add(beat);
|
|
6835
|
+
}
|
|
6836
|
+
}
|
|
6837
|
+
}
|
|
6838
|
+
return [...beats].sort((a, b) => a - b);
|
|
6839
|
+
}
|
|
6840
|
+
function restsOf(melody, plan) {
|
|
6841
|
+
const rests = [];
|
|
6842
|
+
let reach = plan.span.startBeat;
|
|
6843
|
+
for (const note2 of [...melody].sort((a, b) => a.startBeat - b.startBeat)) {
|
|
6844
|
+
if (note2.startBeat > reach + BEAT_EPS) {
|
|
6845
|
+
rests.push({ startBeat: reach, endBeat: note2.startBeat });
|
|
6846
|
+
}
|
|
6847
|
+
reach = Math.max(reach, note2.startBeat + note2.durationBeat);
|
|
6848
|
+
}
|
|
6849
|
+
if (plan.span.endBeat > reach + BEAT_EPS) {
|
|
6850
|
+
rests.push({ startBeat: reach, endBeat: plan.span.endBeat });
|
|
6851
|
+
}
|
|
6852
|
+
return rests;
|
|
6853
|
+
}
|
|
6854
|
+
function phraseBoundaryViolations(melody, plan) {
|
|
6855
|
+
const rests = restsOf(melody, plan);
|
|
6856
|
+
const violations = [];
|
|
6857
|
+
for (const beat of interiorBoundaries(plan)) {
|
|
6858
|
+
const pulse2 = pulseBeats(meterAt(beat, plan.meters));
|
|
6859
|
+
const held = melody.some(
|
|
6860
|
+
(note2) => note2.durationBeat >= HELD_NOTE_BEATS - BEAT_EPS && Math.abs(note2.startBeat + note2.durationBeat - beat) <= pulse2 + BEAT_EPS
|
|
6861
|
+
);
|
|
6862
|
+
const rested = rests.some(
|
|
6863
|
+
(rest) => rest.endBeat - rest.startBeat >= pulse2 - BEAT_EPS && rest.startBeat <= beat + pulse2 + BEAT_EPS && rest.endBeat >= beat - pulse2 - BEAT_EPS
|
|
6864
|
+
);
|
|
6865
|
+
if (!held && !rested) {
|
|
6866
|
+
violations.push({
|
|
6867
|
+
kind: "phraseBoundary",
|
|
6868
|
+
severity: "error",
|
|
6869
|
+
atBeat: beat,
|
|
6870
|
+
expected: `a note of at least ${HELD_NOTE_BEATS} beat(s) ending, or a rest of at least ${pulse2} beat(s), within ${pulse2} beat(s)`,
|
|
6871
|
+
actual: "neither",
|
|
6872
|
+
rationale: `the plan places a phrase boundary at beat ${beat}, which the candidate does not mark`
|
|
6873
|
+
});
|
|
6874
|
+
}
|
|
6875
|
+
}
|
|
6876
|
+
return violations;
|
|
6877
|
+
}
|
|
6878
|
+
function spanViolation(melody, plan) {
|
|
6879
|
+
const end = melody.reduce((max, note2) => Math.max(max, note2.startBeat + note2.durationBeat), 0);
|
|
6880
|
+
if (end <= plan.span.endBeat + BEAT_EPS) {
|
|
6881
|
+
return [];
|
|
6882
|
+
}
|
|
6883
|
+
return [
|
|
6884
|
+
{
|
|
6885
|
+
kind: "span",
|
|
6886
|
+
severity: "error",
|
|
6887
|
+
atBeat: plan.span.endBeat,
|
|
6888
|
+
expected: `ending by beat ${plan.span.endBeat}`,
|
|
6889
|
+
actual: `ending at beat ${end}`,
|
|
6890
|
+
rationale: "the candidate's last note runs past the plan's span"
|
|
6891
|
+
}
|
|
6892
|
+
];
|
|
6893
|
+
}
|
|
6894
|
+
function phraseLines(melody, plan) {
|
|
6895
|
+
return plan.phrases.map(
|
|
6896
|
+
(phrase) => melody.flatMap(
|
|
6897
|
+
(note2, index) => note2.startBeat >= phrase.startBeat - BEAT_EPS && note2.startBeat < phrase.endBeat - BEAT_EPS ? [index] : []
|
|
6898
|
+
)
|
|
6899
|
+
);
|
|
6900
|
+
}
|
|
6901
|
+
function cadenceViolations(melody, plan, timeline, lines) {
|
|
6902
|
+
const violations = [];
|
|
6903
|
+
plan.phrases.forEach((phrase, index) => {
|
|
6904
|
+
if (phrase.cadence === null) {
|
|
6905
|
+
return;
|
|
6906
|
+
}
|
|
6907
|
+
const last = lines[index]?.at(-1);
|
|
6908
|
+
const note2 = last === void 0 ? void 0 : melody[last];
|
|
6909
|
+
const allowed = note2 === void 0 ? [] : cadencePitchClasses(plan, timeline, phrase.cadence, note2.startBeat);
|
|
6910
|
+
if (note2 !== void 0 && (allowed === null || allowed.includes(pitchClassOf(note2.pitch)))) {
|
|
6911
|
+
return;
|
|
6912
|
+
}
|
|
6913
|
+
violations.push({
|
|
6914
|
+
kind: "cadence",
|
|
6915
|
+
severity: "error",
|
|
6916
|
+
atBeat: phrase.endBeat,
|
|
6917
|
+
expected: `a closing note on pitch class ${(allowed ?? []).join(" or ")} for a ${phrase.cadence} cadence`,
|
|
6918
|
+
actual: note2 === void 0 ? "no note" : `pitch ${note2.pitch}`,
|
|
6919
|
+
rationale: `the phrase ending at beat ${phrase.endBeat} is planned to close on a ${phrase.cadence} cadence`
|
|
6920
|
+
});
|
|
6921
|
+
});
|
|
6922
|
+
return violations;
|
|
6923
|
+
}
|
|
6924
|
+
function harmonyViolations(melody, plan, timeline, lines) {
|
|
6925
|
+
const misfits = planHarmonyMisfits(plan, timeline);
|
|
6926
|
+
const violations = [];
|
|
6927
|
+
for (const line of lines) {
|
|
6928
|
+
const read = misfits(line.map((index) => melody[index]));
|
|
6929
|
+
read.forEach((kinds, at2) => {
|
|
6930
|
+
if (kinds === null) {
|
|
6931
|
+
return;
|
|
6932
|
+
}
|
|
6933
|
+
const note2 = melody[line[at2]];
|
|
6934
|
+
const derived = plan.motifs.some(
|
|
6935
|
+
(motif) => motif.from !== null && note2.startBeat >= motif.startBeat - BEAT_EPS && note2.startBeat < motif.endBeat - BEAT_EPS
|
|
6936
|
+
);
|
|
6937
|
+
const strong = positionClass(note2.startBeat, plan.meters) === "strong" && timeline.at(note2.startBeat) !== null;
|
|
6938
|
+
violations.push({
|
|
6939
|
+
kind: "harmony",
|
|
6940
|
+
severity: derived ? "warning" : "error",
|
|
6941
|
+
atBeat: note2.startBeat,
|
|
6942
|
+
expected: strong ? "a chord tone or an ornamental tone" : "a chord tone, an ornamental tone, or a scale tone",
|
|
6943
|
+
actual: kinds.length === 0 ? "no harmonic label" : kinds.join(", "),
|
|
6944
|
+
rationale: derived ? `the note at beat ${note2.startBeat} sits in a derived statement, whose transformation takes precedence over the harmony` : `the note at beat ${note2.startBeat} does not fit the planned harmony`
|
|
6945
|
+
});
|
|
6946
|
+
});
|
|
6947
|
+
}
|
|
6948
|
+
return violations;
|
|
6949
|
+
}
|
|
6950
|
+
function notesInRange(melody, startBeat, endBeat) {
|
|
6951
|
+
return melody.filter(
|
|
6952
|
+
(note2) => note2.startBeat >= startBeat - BEAT_EPS && note2.startBeat < endBeat - BEAT_EPS
|
|
6953
|
+
);
|
|
6954
|
+
}
|
|
6955
|
+
function keptEnd(statement, melody, plan) {
|
|
6956
|
+
const phrase = phraseAt(plan, statement.startBeat);
|
|
6957
|
+
if (phrase === void 0) {
|
|
6958
|
+
return statement.endBeat;
|
|
6959
|
+
}
|
|
6960
|
+
const downbeat = finalDownbeat(phrase, plan.meters);
|
|
6961
|
+
const held = notesInRange(melody, downbeat, phrase.endBeat).length === 1;
|
|
6962
|
+
return held ? Math.min(statement.endBeat, downbeat) : statement.endBeat;
|
|
6963
|
+
}
|
|
6964
|
+
function replayLevel(source, derived, statement, sourceEnd, derivedEnd, sourceCut, plan, key, closing) {
|
|
6965
|
+
const relation = statement.relation;
|
|
6966
|
+
const planned = relation.kind === "tonalTransposition" ? relation.degrees ?? 0 : relation.semitones;
|
|
6967
|
+
const { low, high } = plan.phrases[statement.phrase].register;
|
|
6968
|
+
const outside = (pitch) => pitch < low || pitch > high;
|
|
6969
|
+
const backward = relation.kind === "retrograde" || relation.kind === "retrogradeInversion";
|
|
6970
|
+
const cell = clipStatement(
|
|
6971
|
+
source.map((note2) => ({
|
|
6972
|
+
pitch: note2.pitch,
|
|
6973
|
+
startBeat: note2.startBeat,
|
|
6974
|
+
durationBeat: note2.durationBeat
|
|
6975
|
+
})),
|
|
6976
|
+
sourceEnd
|
|
6977
|
+
);
|
|
6978
|
+
const replays = relationLevels(relation.kind).map((level) => {
|
|
6979
|
+
const notes = clipStatement(
|
|
6980
|
+
replayAt(cell, relation, level, statement.startBeat, key),
|
|
6981
|
+
derivedEnd
|
|
6982
|
+
);
|
|
6983
|
+
return { level, notes, outside: notes.filter((note2) => outside(note2.pitch)).length };
|
|
6984
|
+
});
|
|
6985
|
+
const fewestOutside = Math.min(...replays.map((replay) => replay.outside));
|
|
6986
|
+
let best = null;
|
|
6987
|
+
for (const { level, notes: expected, outside: away } of replays) {
|
|
6988
|
+
const compared = sourceCut ? derived.slice(0, expected.length) : derived;
|
|
6989
|
+
if (expected.length !== compared.length) {
|
|
6990
|
+
continue;
|
|
6991
|
+
}
|
|
6992
|
+
let forced = 0;
|
|
6993
|
+
let holds = true;
|
|
6994
|
+
for (let i = 0; holds && i < expected.length; i += 1) {
|
|
6995
|
+
const want = expected[i];
|
|
6996
|
+
const got = compared[i];
|
|
6997
|
+
if (i > 0) {
|
|
6998
|
+
const gap = got.startBeat - compared[i - 1].startBeat;
|
|
6999
|
+
holds = Math.abs(gap - (want.startBeat - expected[i - 1].startBeat)) <= BEAT_EPS;
|
|
7000
|
+
}
|
|
7001
|
+
if (!holds || got.pitch === want.pitch) {
|
|
7002
|
+
continue;
|
|
7003
|
+
}
|
|
7004
|
+
const origin = source[backward ? cell.length - 1 - i : i];
|
|
7005
|
+
forced += 1;
|
|
7006
|
+
holds = closing.has(got) || origin !== void 0 && closing.has(origin) || away === fewestOutside && outside(want.pitch);
|
|
7007
|
+
}
|
|
7008
|
+
const distance = Math.abs(level - planned);
|
|
7009
|
+
const nearer = best === null || distance < Math.abs(best.level - planned) || distance === Math.abs(best.level - planned) && level < best.level;
|
|
7010
|
+
if (holds && nearer) {
|
|
7011
|
+
best = { level, forced };
|
|
7012
|
+
}
|
|
7013
|
+
}
|
|
7014
|
+
return best;
|
|
7015
|
+
}
|
|
7016
|
+
function motifDerivationViolations(melody, plan, closing) {
|
|
7017
|
+
const keyAt = planKeyAt(plan);
|
|
7018
|
+
const errors = [];
|
|
7019
|
+
const warnings = [];
|
|
7020
|
+
for (const statement of plan.motifs) {
|
|
7021
|
+
if (statement.from === null) {
|
|
7022
|
+
continue;
|
|
7023
|
+
}
|
|
7024
|
+
const source = plan.motifs[statement.from];
|
|
7025
|
+
const sourceNotes = notesInRange(melody, source.startBeat, source.endBeat);
|
|
7026
|
+
const derivedNotes = notesInRange(melody, statement.startBeat, statement.endBeat);
|
|
7027
|
+
const relation = statement.relation;
|
|
7028
|
+
const rationale = `the statement at beat ${statement.startBeat} does not derive from its source the way the plan names`;
|
|
7029
|
+
const sourceKept = keptEnd(source, melody, plan);
|
|
7030
|
+
const derivedKept = keptEnd(statement, melody, plan);
|
|
7031
|
+
if (sourceKept <= source.startBeat + BEAT_EPS || derivedKept <= statement.startBeat + BEAT_EPS) {
|
|
7032
|
+
continue;
|
|
7033
|
+
}
|
|
7034
|
+
if (sourceNotes.length === 0 || derivedNotes.length === 0) {
|
|
7035
|
+
errors.push({
|
|
7036
|
+
kind: "motifDerivation",
|
|
7037
|
+
severity: "error",
|
|
7038
|
+
atBeat: statement.startBeat,
|
|
7039
|
+
expected: relation === null ? "a variation" : relation.kind,
|
|
7040
|
+
actual: "no notes to compare",
|
|
7041
|
+
rationale
|
|
7042
|
+
});
|
|
7043
|
+
continue;
|
|
7044
|
+
}
|
|
7045
|
+
const sourceCut = sourceKept < source.endBeat - BEAT_EPS;
|
|
7046
|
+
const derivedCut = derivedKept < statement.endBeat - BEAT_EPS;
|
|
7047
|
+
if (relation === null) {
|
|
7048
|
+
const sourceEnd = derivedCut ? Math.min(sourceKept, source.startBeat + derivedKept - statement.startBeat) : sourceKept;
|
|
7049
|
+
const derivedEnd = sourceCut ? Math.min(derivedKept, statement.startBeat + sourceKept - source.startBeat) : derivedKept;
|
|
7050
|
+
const sourceLine = notesInRange(melody, source.startBeat, sourceEnd);
|
|
7051
|
+
const derivedLine = notesInRange(melody, statement.startBeat, derivedEnd);
|
|
7052
|
+
if (sourceLine.length === 0 || derivedLine.length === 0) {
|
|
7053
|
+
continue;
|
|
7054
|
+
}
|
|
7055
|
+
const similarity = melodicSimilarity(sourceLine, derivedLine);
|
|
7056
|
+
if (similarity < DEFAULT_VARIATION_THRESHOLD) {
|
|
7057
|
+
errors.push({
|
|
7058
|
+
kind: "motifDerivation",
|
|
7059
|
+
severity: "error",
|
|
7060
|
+
atBeat: statement.startBeat,
|
|
7061
|
+
expected: `a variation (melodicSimilarity >= ${DEFAULT_VARIATION_THRESHOLD})`,
|
|
7062
|
+
actual: `melodicSimilarity ${similarity.toFixed(3)}`,
|
|
7063
|
+
rationale
|
|
7064
|
+
});
|
|
7065
|
+
}
|
|
7066
|
+
continue;
|
|
7067
|
+
}
|
|
7068
|
+
const kept = notesInRange(melody, source.startBeat, sourceKept);
|
|
7069
|
+
const backward = relation.kind === "retrograde" || relation.kind === "retrogradeInversion";
|
|
7070
|
+
if (kept.length === 0 || sourceCut && backward) {
|
|
7071
|
+
continue;
|
|
7072
|
+
}
|
|
7073
|
+
const replay = replayLevel(
|
|
7074
|
+
kept,
|
|
7075
|
+
notesInRange(melody, statement.startBeat, derivedKept),
|
|
7076
|
+
statement,
|
|
7077
|
+
sourceKept,
|
|
7078
|
+
derivedKept,
|
|
7079
|
+
sourceCut,
|
|
7080
|
+
plan,
|
|
7081
|
+
scaleOf(keyAt(statement.startBeat)),
|
|
7082
|
+
closing
|
|
7083
|
+
);
|
|
7084
|
+
const planned = relation.kind === "tonalTransposition" ? relation.degrees ?? 0 : relation.semitones;
|
|
7085
|
+
if (replay === null) {
|
|
7086
|
+
errors.push({
|
|
7087
|
+
kind: "motifDerivation",
|
|
7088
|
+
severity: "error",
|
|
7089
|
+
atBeat: statement.startBeat,
|
|
7090
|
+
expected: relation.kind,
|
|
7091
|
+
actual: "no pitch level replays the source",
|
|
7092
|
+
rationale
|
|
7093
|
+
});
|
|
7094
|
+
} else if (replay.level !== planned || replay.forced > 0) {
|
|
7095
|
+
warnings.push({
|
|
7096
|
+
kind: "motifDisplaced",
|
|
7097
|
+
severity: "warning",
|
|
7098
|
+
atBeat: statement.startBeat,
|
|
7099
|
+
expected: `${relation.kind} at level ${planned} with every pitch replayed`,
|
|
7100
|
+
actual: `level ${replay.level}, ${replay.forced} excused note(s)`,
|
|
7101
|
+
rationale: `the statement at beat ${statement.startBeat} carries out its planned derivation at another pitch level or with notes excused`
|
|
7102
|
+
});
|
|
7103
|
+
}
|
|
7104
|
+
}
|
|
7105
|
+
return { errors, warnings };
|
|
7106
|
+
}
|
|
7107
|
+
function phraseAt(plan, beat) {
|
|
7108
|
+
return plan.phrases.find(
|
|
7109
|
+
(phrase) => beat >= phrase.startBeat - BEAT_EPS && beat < phrase.endBeat - BEAT_EPS
|
|
7110
|
+
);
|
|
7111
|
+
}
|
|
7112
|
+
function registerViolations(melody, plan) {
|
|
7113
|
+
const violations = [];
|
|
7114
|
+
for (const note2 of melody) {
|
|
7115
|
+
const phrase = phraseAt(plan, note2.startBeat);
|
|
7116
|
+
if (phrase === void 0) {
|
|
7117
|
+
continue;
|
|
7118
|
+
}
|
|
7119
|
+
if (note2.pitch < phrase.register.low || note2.pitch > phrase.register.high) {
|
|
7120
|
+
violations.push({
|
|
7121
|
+
kind: "register",
|
|
7122
|
+
severity: "error",
|
|
7123
|
+
atBeat: note2.startBeat,
|
|
7124
|
+
expected: `a pitch within [${phrase.register.low}, ${phrase.register.high}]`,
|
|
7125
|
+
actual: `pitch ${note2.pitch}`,
|
|
7126
|
+
rationale: `the note at beat ${note2.startBeat} falls outside its phrase's planned register`
|
|
7127
|
+
});
|
|
7128
|
+
}
|
|
7129
|
+
}
|
|
7130
|
+
return violations;
|
|
7131
|
+
}
|
|
7132
|
+
function planOutline(phrase) {
|
|
7133
|
+
const amplitude = (phrase.register.high - phrase.register.low) / 2;
|
|
7134
|
+
const outline = [];
|
|
7135
|
+
for (let k = 0; k < OUTLINE_POINTS; k += 1) {
|
|
7136
|
+
const t = (k + 0.5) / OUTLINE_POINTS;
|
|
7137
|
+
outline.push(
|
|
7138
|
+
phrase.register.mean + plannedContourAt(phrase.shape, phrase.peakPosition, t) * amplitude
|
|
7139
|
+
);
|
|
7140
|
+
}
|
|
7141
|
+
return outline;
|
|
7142
|
+
}
|
|
7143
|
+
function planContourPhrase(phrase) {
|
|
7144
|
+
return {
|
|
7145
|
+
shape: phrase.shape,
|
|
7146
|
+
low: phrase.register.low,
|
|
7147
|
+
high: phrase.register.high,
|
|
7148
|
+
outline: planOutline(phrase)
|
|
7149
|
+
};
|
|
7150
|
+
}
|
|
7151
|
+
function planRegisterPlan(plan) {
|
|
7152
|
+
const phrases = plan.phrases.map((phrase) => ({
|
|
7153
|
+
low: phrase.register.low,
|
|
7154
|
+
high: phrase.register.high,
|
|
7155
|
+
mean: phrase.register.mean,
|
|
7156
|
+
peakPosition: phrase.peakPosition
|
|
7157
|
+
}));
|
|
7158
|
+
let weighted = 0;
|
|
7159
|
+
let weight = 0;
|
|
7160
|
+
for (const phrase of plan.phrases) {
|
|
7161
|
+
const length = phrase.endBeat - phrase.startBeat;
|
|
7162
|
+
weighted += phrase.register.mean * length;
|
|
7163
|
+
weight += length;
|
|
7164
|
+
}
|
|
7165
|
+
return { phrases, mean: weight > 0 ? weighted / weight : null };
|
|
7166
|
+
}
|
|
7167
|
+
function planRhythmReading(plan) {
|
|
7168
|
+
const hasOnsets = plan.rhythm.onsetLevels.some((share) => share > 0);
|
|
7169
|
+
return {
|
|
7170
|
+
onsets: hasOnsets ? 1 : 0,
|
|
7171
|
+
onsetLevels: plan.rhythm.onsetLevels,
|
|
7172
|
+
barPositions: [],
|
|
7173
|
+
interOnsetShares: plan.rhythm.interOnsetShares,
|
|
7174
|
+
syncopation: plan.rhythm.syncopation
|
|
7175
|
+
};
|
|
7176
|
+
}
|
|
7177
|
+
function peakPositionWarnings(plan, readings) {
|
|
7178
|
+
const violations = [];
|
|
7179
|
+
plan.phrases.forEach((phrase, index) => {
|
|
7180
|
+
const reading = readings[index];
|
|
7181
|
+
if (reading == null) {
|
|
7182
|
+
return;
|
|
7183
|
+
}
|
|
7184
|
+
const drift = Math.abs(reading.peakPosition - phrase.peakPosition);
|
|
7185
|
+
if (drift > PEAK_POSITION_TOLERANCE) {
|
|
7186
|
+
violations.push({
|
|
7187
|
+
kind: "peakPosition",
|
|
7188
|
+
severity: "warning",
|
|
7189
|
+
atBeat: phrase.startBeat,
|
|
7190
|
+
expected: `a peak position within ${PEAK_POSITION_TOLERANCE} of ${phrase.peakPosition}`,
|
|
7191
|
+
actual: `${reading.peakPosition}`,
|
|
7192
|
+
rationale: `the phrase starting at beat ${phrase.startBeat} places its melodic peak further from the planned position than the tolerance allows`
|
|
7193
|
+
});
|
|
7194
|
+
}
|
|
7195
|
+
});
|
|
7196
|
+
return violations;
|
|
7197
|
+
}
|
|
7198
|
+
function lineMeanOf(melody) {
|
|
7199
|
+
let weighted = 0;
|
|
7200
|
+
let total = 0;
|
|
7201
|
+
for (const note2 of melody) {
|
|
7202
|
+
weighted += note2.pitch * note2.durationBeat;
|
|
7203
|
+
total += note2.durationBeat;
|
|
7204
|
+
}
|
|
7205
|
+
return total > 0 ? weighted / total : null;
|
|
7206
|
+
}
|
|
7207
|
+
function planDensity(plan) {
|
|
7208
|
+
let weighted = 0;
|
|
7209
|
+
let bars = 0;
|
|
7210
|
+
for (const phrase of plan.phrases) {
|
|
7211
|
+
const span = barSpanOf(phrase.startBeat, phrase.endBeat, plan.meters);
|
|
7212
|
+
weighted += phrase.onsetDensity * span;
|
|
7213
|
+
bars += span;
|
|
7214
|
+
}
|
|
7215
|
+
return { onsets: 1, onsetDensity: bars > 0 ? weighted / bars : 0 };
|
|
7216
|
+
}
|
|
7217
|
+
function evaluateComposition(melody, plan, opts) {
|
|
7218
|
+
assertCompositionPlan(plan, "evaluated plan");
|
|
7219
|
+
const { budget } = assertOptions(opts, "evaluate options");
|
|
7220
|
+
const notes = [
|
|
7221
|
+
...assertNoteEvents(melody, "evaluated melody", { budget, allowNonPositiveDuration: true })
|
|
7222
|
+
].sort((a, b) => a.startBeat - b.startBeat);
|
|
7223
|
+
const timeline = planChordTimeline(plan);
|
|
7224
|
+
const lines = phraseLines(notes, plan);
|
|
7225
|
+
const closing = new Set(
|
|
7226
|
+
lines.flatMap((line) => line.length === 0 ? [] : [notes[line.at(-1)]])
|
|
7227
|
+
);
|
|
7228
|
+
const derivations = motifDerivationViolations(notes, plan, closing);
|
|
7229
|
+
const lineMean = lineMeanOf(notes);
|
|
7230
|
+
const readings = plan.phrases.map(
|
|
7231
|
+
(phrase) => phraseMelody(notes, phrase.startBeat, phrase.endBeat, lineMean ?? 0, plan.meters, budget)
|
|
7232
|
+
);
|
|
7233
|
+
const violations = [
|
|
7234
|
+
...spanViolation(notes, plan),
|
|
7235
|
+
...phraseBoundaryViolations(notes, plan),
|
|
7236
|
+
...cadenceViolations(notes, plan, timeline, lines),
|
|
7237
|
+
...harmonyViolations(notes, plan, timeline, lines),
|
|
7238
|
+
...registerViolations(notes, plan),
|
|
7239
|
+
...derivations.errors,
|
|
7240
|
+
...derivations.warnings,
|
|
7241
|
+
...peakPositionWarnings(plan, readings)
|
|
7242
|
+
];
|
|
7243
|
+
const rhythm = analyzeRhythm(notes, {
|
|
7244
|
+
meters: plan.meters,
|
|
7245
|
+
totalBeats: plan.span.endBeat,
|
|
7246
|
+
budget
|
|
7247
|
+
});
|
|
7248
|
+
const target = planRhythmReading(plan);
|
|
7249
|
+
const fit = {
|
|
7250
|
+
contour: contourSimilarity(plan.phrases.map(planContourPhrase), readings),
|
|
7251
|
+
register: registerSimilarity(planRegisterPlan(plan), { phrases: readings, mean: lineMean }),
|
|
7252
|
+
onset: onsetSimilarity(target, rhythm),
|
|
7253
|
+
duration: durationSimilarity(target, rhythm),
|
|
7254
|
+
syncopation: syncopationSimilarity(target, rhythm),
|
|
7255
|
+
density: densitySimilarity(planDensity(plan), rhythm)
|
|
7256
|
+
};
|
|
7257
|
+
return { violations, fit };
|
|
7258
|
+
}
|
|
7259
|
+
|
|
7260
|
+
// src/generate/progression/index.ts
|
|
7261
|
+
var DEFAULT_TS6 = { numerator: 4, denominator: 4 };
|
|
7262
|
+
var PROG_STYLES = Object.freeze(["minimal", "dance", "idol", "rock"]);
|
|
7263
|
+
var BORROWED_DEGREES = Object.freeze({
|
|
7264
|
+
/** Flat submediant: bVI. */
|
|
7265
|
+
bVI: 8,
|
|
7266
|
+
/** Flat subtonic: bVII. */
|
|
7267
|
+
bVII: 10,
|
|
7268
|
+
/** Flat mediant: bIII. */
|
|
7269
|
+
bIII: 11,
|
|
7270
|
+
/** Minor subdominant borrowed from the parallel minor: iv. */
|
|
7271
|
+
iv: 12,
|
|
7272
|
+
/** Neapolitan: bII. */
|
|
7273
|
+
bII: 13,
|
|
7274
|
+
/** Sharp subdominant, the diminished #IV. */
|
|
7275
|
+
sharpIV: 14
|
|
7276
|
+
});
|
|
4981
7277
|
var PRESETS = [
|
|
4982
7278
|
{
|
|
4983
7279
|
id: "fourChordPop",
|
|
@@ -5158,12 +7454,12 @@ function degreeToRootPc(degree, key) {
|
|
|
5158
7454
|
const offset = BORROWED_OFFSET[degree] ?? 0;
|
|
5159
7455
|
return ((key.rootPc % 12 + offset) % 12 + 12) % 12;
|
|
5160
7456
|
}
|
|
5161
|
-
function autoQuality(degree, key, harmonicDominant) {
|
|
7457
|
+
function autoQuality(degree, key, tonic, harmonicDominant) {
|
|
5162
7458
|
if (degree >= 1 && degree <= 7) {
|
|
5163
|
-
if (harmonicDominant &&
|
|
7459
|
+
if (harmonicDominant && cadencesThroughRaisedSeventh(key) && degree === 5) {
|
|
5164
7460
|
return "maj";
|
|
5165
7461
|
}
|
|
5166
|
-
return diatonicTriad(degree, heptatonicFrameOf(key)).quality;
|
|
7462
|
+
return diatonicTriad(degree, { ...heptatonicFrameOf(key), tonic }).quality;
|
|
5167
7463
|
}
|
|
5168
7464
|
if (degree === 14) {
|
|
5169
7465
|
return "dim";
|
|
@@ -5173,11 +7469,11 @@ function autoQuality(degree, key, harmonicDominant) {
|
|
|
5173
7469
|
}
|
|
5174
7470
|
return "maj";
|
|
5175
7471
|
}
|
|
5176
|
-
function resolveCycle(degrees, key, ext, harmonicDominant) {
|
|
7472
|
+
function resolveCycle(degrees, key, tonic, ext, harmonicDominant) {
|
|
5177
7473
|
const steps = degrees.map((degree) => {
|
|
5178
7474
|
const step = {
|
|
5179
7475
|
rootPc: degreeToRootPc(degree, key),
|
|
5180
|
-
quality: ext !== void 0 && ext !== "auto" ? ext : autoQuality(degree, key, harmonicDominant)
|
|
7476
|
+
quality: ext !== void 0 && ext !== "auto" ? ext : autoQuality(degree, key, tonic, harmonicDominant)
|
|
5181
7477
|
};
|
|
5182
7478
|
if (degree >= 1 && degree <= 7) {
|
|
5183
7479
|
step.degree = degree;
|
|
@@ -5210,17 +7506,22 @@ function generateProgression(opts) {
|
|
|
5210
7506
|
assertRecord(opts, "progression options");
|
|
5211
7507
|
assertPositiveInt(opts.bars, "progression bars");
|
|
5212
7508
|
assertGenerationBudget(opts.bars, "progression chords");
|
|
5213
|
-
const key =
|
|
7509
|
+
const { scale: key, tonic } = resolveKey(opts.key);
|
|
5214
7510
|
const ctx = resolveContext(opts.ctx);
|
|
5215
7511
|
let preset;
|
|
5216
7512
|
if (opts.preset !== void 0) {
|
|
5217
|
-
|
|
7513
|
+
const custom = assertRecord(
|
|
7514
|
+
opts.preset,
|
|
7515
|
+
"progression preset"
|
|
7516
|
+
);
|
|
7517
|
+
const degrees = assertArray(custom.degrees, "progression preset degrees");
|
|
7518
|
+
if (degrees.length === 0) {
|
|
5218
7519
|
throw new InvalidInputError("progression preset must name at least one degree");
|
|
5219
7520
|
}
|
|
5220
7521
|
preset = {
|
|
5221
|
-
id:
|
|
5222
|
-
name:
|
|
5223
|
-
degrees:
|
|
7522
|
+
id: custom.id ?? "custom",
|
|
7523
|
+
name: custom.name ?? "Custom",
|
|
7524
|
+
degrees: degrees.map((degree, index) => {
|
|
5224
7525
|
assertDegree(degree, `progression preset degrees[${index}]`);
|
|
5225
7526
|
const supported = degree >= 1 && degree <= 7 || Object.values(BORROWED_DEGREES).includes(degree);
|
|
5226
7527
|
if (!supported) {
|
|
@@ -5230,8 +7531,12 @@ function generateProgression(opts) {
|
|
|
5230
7531
|
}
|
|
5231
7532
|
return degree;
|
|
5232
7533
|
}),
|
|
5233
|
-
functional:
|
|
5234
|
-
|
|
7534
|
+
functional: assertOneOf(
|
|
7535
|
+
custom.functional ?? "loop",
|
|
7536
|
+
["loop", "tensionBuild", "cadenceStrong", "stable"],
|
|
7537
|
+
"progression preset functional"
|
|
7538
|
+
),
|
|
7539
|
+
styles: custom.styles ?? [opts.style]
|
|
5235
7540
|
};
|
|
5236
7541
|
}
|
|
5237
7542
|
if (preset === void 0 && opts.presetId !== void 0) {
|
|
@@ -5245,6 +7550,7 @@ function generateProgression(opts) {
|
|
|
5245
7550
|
const cycle = resolveCycle(
|
|
5246
7551
|
preset?.degrees ?? [0],
|
|
5247
7552
|
key,
|
|
7553
|
+
tonic,
|
|
5248
7554
|
ext,
|
|
5249
7555
|
preset?.functional === "cadenceStrong"
|
|
5250
7556
|
);
|
|
@@ -5277,10 +7583,11 @@ function generateProgression(opts) {
|
|
|
5277
7583
|
const domRoot = (next.rootPc + 7) % 12;
|
|
5278
7584
|
const targetsTonic = (next.rootPc - tonicPc + 12) % 12 === 0;
|
|
5279
7585
|
const isLastTonicStatement = cur.rootPc === tonicPc && tonicStatements <= 1;
|
|
5280
|
-
const alreadySecondary = cur.rootPc === domRoot && cur.quality
|
|
7586
|
+
const alreadySecondary = cur.rootPc === domRoot && soundsDominantSeventh(makeChord(cur.rootPc, cur.quality));
|
|
7587
|
+
const isNativeDominant = isNativeDominantSeventh(domRoot, key);
|
|
5281
7588
|
const isResolutionTarget = chords[i - 1]?.secondaryDominant === true;
|
|
5282
7589
|
const takesNoDominant = !canBeTonicized(next, tonicizable);
|
|
5283
|
-
if (targetsTonic || isLastTonicStatement || alreadySecondary || isResolutionTarget || takesNoDominant) {
|
|
7590
|
+
if (targetsTonic || isLastTonicStatement || alreadySecondary || isResolutionTarget || takesNoDominant || isNativeDominant) {
|
|
5284
7591
|
continue;
|
|
5285
7592
|
}
|
|
5286
7593
|
if (draw.prob(harmonic, "reharmonize", i)) {
|
|
@@ -5318,11 +7625,11 @@ function triadQualityOf(chord) {
|
|
|
5318
7625
|
}
|
|
5319
7626
|
return hasThird;
|
|
5320
7627
|
}
|
|
5321
|
-
function diatonicTriadsOf(key) {
|
|
7628
|
+
function diatonicTriadsOf(key, tonic) {
|
|
5322
7629
|
const degrees = scaleTonesInDegreeOrder(key).length;
|
|
5323
7630
|
const triads = [];
|
|
5324
7631
|
for (let degree = 1; degree <= degrees; degree += 1) {
|
|
5325
|
-
triads.push(diatonicTriad(degree, key));
|
|
7632
|
+
triads.push(diatonicTriad(degree, { ...key, tonic }));
|
|
5326
7633
|
}
|
|
5327
7634
|
return triads;
|
|
5328
7635
|
}
|
|
@@ -5356,10 +7663,22 @@ function tritoneSubstituteRoot(dominant, tonic, key) {
|
|
|
5356
7663
|
const resolution = spellPitchClass(pitchClassOf(dominant.rootPc + 5), tonic, key);
|
|
5357
7664
|
return transposeNote(resolution, 1);
|
|
5358
7665
|
}
|
|
7666
|
+
var MEDIANT_THIRDS = {
|
|
7667
|
+
3: "m3",
|
|
7668
|
+
4: "M3",
|
|
7669
|
+
8: "-M3",
|
|
7670
|
+
9: "-m3"
|
|
7671
|
+
};
|
|
7672
|
+
function mediantRoot(source, offset, tonic, key) {
|
|
7673
|
+
const hint = source.rootSpelling;
|
|
7674
|
+
const root = hint !== void 0 && noteToPitchClass(hint) === pitchClassOf(source.rootPc) ? hint : spellPitchClass(source.rootPc, tonic, key);
|
|
7675
|
+
return transposeByInterval(bareSpelling(root), MEDIANT_THIRDS[offset]);
|
|
7676
|
+
}
|
|
5359
7677
|
function substituteChord(chord, key, opts) {
|
|
5360
7678
|
assertOptions(opts, "opts");
|
|
5361
7679
|
const source = toChordData(chord);
|
|
5362
|
-
const
|
|
7680
|
+
const resolved = resolveKey(key);
|
|
7681
|
+
const { scale, tonic } = resolved;
|
|
5363
7682
|
const originalTriad = chordPitchClasses(makeChord(source.rootPc, triadQualityOf(source)));
|
|
5364
7683
|
const candidates = [];
|
|
5365
7684
|
if (soundsDominantSeventh(source)) {
|
|
@@ -5369,24 +7688,27 @@ function substituteChord(chord, key, opts) {
|
|
|
5369
7688
|
type: "tritone"
|
|
5370
7689
|
});
|
|
5371
7690
|
}
|
|
5372
|
-
for (const triad of diatonicTriadsOf(scale)) {
|
|
7691
|
+
for (const triad of diatonicTriadsOf(scale, tonic)) {
|
|
5373
7692
|
const offset = pitchClassOf(triad.rootPc - source.rootPc);
|
|
5374
7693
|
if (THIRD_OFFSETS.includes(offset) && commonToneCount(originalTriad, chordPitchClasses(triad)) === 2) {
|
|
5375
7694
|
candidates.push({ chord: spelledInKey(triad, tonic, scale), type: "relative" });
|
|
5376
7695
|
}
|
|
5377
7696
|
}
|
|
5378
|
-
const targetFunction = functionOf(source,
|
|
7697
|
+
const targetFunction = functionOf(source, resolved);
|
|
5379
7698
|
const parallel = parallelKey(scale);
|
|
5380
|
-
for (const triad of diatonicTriadsOf(parallel)) {
|
|
5381
|
-
if (functionOf(triad,
|
|
7699
|
+
for (const triad of diatonicTriadsOf(parallel, tonic)) {
|
|
7700
|
+
if (functionOf(triad, resolved) === targetFunction && !isDiatonic(triad, resolved)) {
|
|
5382
7701
|
candidates.push({ chord: spelledInKey(triad, tonic, parallel), type: "borrowed" });
|
|
5383
7702
|
}
|
|
5384
7703
|
}
|
|
5385
7704
|
for (const offset of THIRD_OFFSETS) {
|
|
5386
7705
|
for (const quality of ["maj", "min"]) {
|
|
5387
7706
|
const mediant = makeChord(pitchClassOf(source.rootPc + offset), quality);
|
|
5388
|
-
if (commonToneCount(originalTriad, chordPitchClasses(mediant)) === 1 && !isDiatonic(mediant,
|
|
5389
|
-
candidates.push({
|
|
7707
|
+
if (commonToneCount(originalTriad, chordPitchClasses(mediant)) === 1 && !isDiatonic(mediant, resolved)) {
|
|
7708
|
+
candidates.push({
|
|
7709
|
+
chord: spelledOnRoot(mediant, mediantRoot(source, offset, tonic, scale), tonic, scale),
|
|
7710
|
+
type: "chromaticMediant"
|
|
7711
|
+
});
|
|
5390
7712
|
}
|
|
5391
7713
|
}
|
|
5392
7714
|
}
|
|
@@ -5411,14 +7733,15 @@ function substituteChord(chord, key, opts) {
|
|
|
5411
7733
|
results.push({
|
|
5412
7734
|
chord: candidate.chord,
|
|
5413
7735
|
type: candidate.type,
|
|
5414
|
-
roman: chordToRoman(candidate.chord,
|
|
5415
|
-
function: functionOf(candidate.chord,
|
|
7736
|
+
roman: chordToRoman(candidate.chord, resolved),
|
|
7737
|
+
function: functionOf(candidate.chord, resolved)
|
|
5416
7738
|
});
|
|
5417
7739
|
}
|
|
5418
7740
|
return results;
|
|
5419
7741
|
}
|
|
5420
7742
|
function modalInterchangePalette(key) {
|
|
5421
|
-
const
|
|
7743
|
+
const resolved = resolveKey(key);
|
|
7744
|
+
const { scale, tonic } = resolved;
|
|
5422
7745
|
const palette = [];
|
|
5423
7746
|
const seen = /* @__PURE__ */ new Set();
|
|
5424
7747
|
const add = (chord) => {
|
|
@@ -5429,18 +7752,18 @@ function modalInterchangePalette(key) {
|
|
|
5429
7752
|
seen.add(dedupeKey);
|
|
5430
7753
|
palette.push({
|
|
5431
7754
|
chord,
|
|
5432
|
-
roman: chordToRoman(chord,
|
|
5433
|
-
source: borrowedSource(chord,
|
|
7755
|
+
roman: chordToRoman(chord, resolved),
|
|
7756
|
+
source: borrowedSource(chord, resolved)
|
|
5434
7757
|
});
|
|
5435
7758
|
};
|
|
5436
7759
|
const parallel = parallelKey(scale);
|
|
5437
|
-
for (const triad of diatonicTriadsOf(parallel)) {
|
|
5438
|
-
if (!isDiatonic(triad,
|
|
7760
|
+
for (const triad of diatonicTriadsOf(parallel, tonic)) {
|
|
7761
|
+
if (!isDiatonic(triad, resolved) && borrowedSource(triad, resolved) !== null) {
|
|
5439
7762
|
add(spelledInKey(triad, tonic, parallel));
|
|
5440
7763
|
}
|
|
5441
7764
|
}
|
|
5442
7765
|
const neapolitan = makeChord(pitchClassOf(scale.rootPc + 1), "maj");
|
|
5443
|
-
if (!isDiatonic(neapolitan,
|
|
7766
|
+
if (!isDiatonic(neapolitan, resolved)) {
|
|
5444
7767
|
add(spelledOnRoot(neapolitan, transposeNote(tonic, 1), tonic, scale));
|
|
5445
7768
|
}
|
|
5446
7769
|
return palette;
|
|
@@ -5469,12 +7792,13 @@ var DEFAULT_RHYTHM_VELOCITY = 96;
|
|
|
5469
7792
|
function rhythmToNoteEvents(events, pitch, velocity = DEFAULT_RHYTHM_VELOCITY) {
|
|
5470
7793
|
assertMidiPitch(pitch, "pitch");
|
|
5471
7794
|
assertInteger(velocity, "velocity", 0, 127);
|
|
5472
|
-
return assertArray(events, "events").map((
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
7795
|
+
return assertArray(events, "events").map((given, index) => {
|
|
7796
|
+
const event = assertRecord(given, `events[${index}]`);
|
|
7797
|
+
const bound = Number.MAX_SAFE_INTEGER;
|
|
7798
|
+
assertRange(event.position, -bound, bound, `events[${index}].position`);
|
|
7799
|
+
assertRange(event.duration, -bound, bound, `events[${index}].duration`);
|
|
7800
|
+
return { pitch, startBeat: event.position, durationBeat: event.duration, velocity };
|
|
7801
|
+
});
|
|
5478
7802
|
}
|
|
5479
7803
|
var DEFAULT_SUBDIVISION = 2;
|
|
5480
7804
|
var DEFAULT_DENSITY = 0.5;
|
|
@@ -5569,10 +7893,14 @@ export {
|
|
|
5569
7893
|
thin,
|
|
5570
7894
|
double,
|
|
5571
7895
|
halfTime,
|
|
7896
|
+
halfTimeFigure,
|
|
5572
7897
|
doubleTime,
|
|
7898
|
+
doubleTimeFigure,
|
|
5573
7899
|
syncopate,
|
|
5574
7900
|
ornamentBy,
|
|
5575
7901
|
deform,
|
|
7902
|
+
resolveDials,
|
|
7903
|
+
deformFigure,
|
|
5576
7904
|
withinCeiling,
|
|
5577
7905
|
isLickMaterial,
|
|
5578
7906
|
BASS_LICKS,
|
|
@@ -5603,8 +7931,14 @@ export {
|
|
|
5603
7931
|
motifToNoteEvents,
|
|
5604
7932
|
transformMotif,
|
|
5605
7933
|
developMotif,
|
|
7934
|
+
COMPOSITION_PLAN_VERSION,
|
|
7935
|
+
assertCompositionPlan,
|
|
7936
|
+
planTimeline,
|
|
7937
|
+
generateMelody,
|
|
5606
7938
|
ORNAMENT_STYLES,
|
|
5607
7939
|
ornament,
|
|
7940
|
+
deriveCompositionPlan,
|
|
7941
|
+
evaluateComposition,
|
|
5608
7942
|
BORROWED_DEGREES,
|
|
5609
7943
|
progressions,
|
|
5610
7944
|
progressionsByStyle,
|
|
@@ -5618,4 +7952,4 @@ export {
|
|
|
5618
7952
|
generateRhythm,
|
|
5619
7953
|
rhythmDensity
|
|
5620
7954
|
};
|
|
5621
|
-
//# sourceMappingURL=chunk-
|
|
7955
|
+
//# sourceMappingURL=chunk-FK3DGAIZ.js.map
|