@libraz/libcantus 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/README_ja.md +4 -0
- package/dist/analyze/index.cjs +31 -7
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +155 -9
- package/dist/analyze/index.d.ts +155 -9
- package/dist/analyze/index.js +37 -13
- package/dist/{chunk-TT6PKMPR.cjs → chunk-35NBV2FT.cjs} +16 -16
- package/dist/{chunk-TT6PKMPR.cjs.map → chunk-35NBV2FT.cjs.map} +1 -1
- package/dist/{chunk-YPB3A3EY.cjs → chunk-3R3QVKY5.cjs} +314 -285
- package/dist/chunk-3R3QVKY5.cjs.map +1 -0
- package/dist/{chunk-GDKE6RQW.cjs → chunk-4R2LWV7E.cjs} +691 -413
- package/dist/chunk-4R2LWV7E.cjs.map +1 -0
- package/dist/{chunk-RVS4G4MR.cjs → chunk-76ZBTRG2.cjs} +198 -198
- package/dist/{chunk-RVS4G4MR.cjs.map → chunk-76ZBTRG2.cjs.map} +1 -1
- package/dist/{chunk-X6RJAHII.js → chunk-7KG3RTYS.js} +2 -2
- package/dist/{chunk-PQL7E5F6.js → chunk-AGOVW7U6.js} +1 -1
- package/dist/{chunk-PQL7E5F6.js.map → chunk-AGOVW7U6.js.map} +1 -1
- package/dist/{chunk-5KW7DNCJ.cjs → chunk-B2PW6EHN.cjs} +2423 -453
- package/dist/chunk-B2PW6EHN.cjs.map +1 -0
- package/dist/{chunk-J772DL3B.cjs → chunk-CGBADIO5.cjs} +53 -53
- package/dist/{chunk-J772DL3B.cjs.map → chunk-CGBADIO5.cjs.map} +1 -1
- package/dist/{chunk-R5VUXQMP.js → chunk-CPMB7P75.js} +1978 -8
- package/dist/chunk-CPMB7P75.js.map +1 -0
- package/dist/{chunk-VLE5R7PF.js → chunk-GNVN4PPJ.js} +1874 -8
- package/dist/chunk-GNVN4PPJ.js.map +1 -0
- package/dist/{chunk-Q6R3QAZD.cjs → chunk-IFZDWIKM.cjs} +2199 -333
- package/dist/chunk-IFZDWIKM.cjs.map +1 -0
- package/dist/{chunk-DKICR7AN.js → chunk-IPEF6NWB.js} +296 -18
- package/dist/chunk-IPEF6NWB.js.map +1 -0
- package/dist/{chunk-AWEMD747.js → chunk-M5RYMAFI.js} +3 -3
- package/dist/{chunk-HSY2VQXZ.js → chunk-MLBPXA4Z.js} +330 -728
- package/dist/chunk-MLBPXA4Z.js.map +1 -0
- package/dist/{chunk-H66ARFU5.js → chunk-NB25P3EH.js} +3 -3
- package/dist/{chunk-5JJ6EHEZ.cjs → chunk-NM7UAMC3.cjs} +62 -62
- package/dist/{chunk-5JJ6EHEZ.cjs.map → chunk-NM7UAMC3.cjs.map} +1 -1
- package/dist/{chunk-R7B5N2JK.cjs → chunk-PM4XJE2Y.cjs} +47 -47
- package/dist/{chunk-R7B5N2JK.cjs.map → chunk-PM4XJE2Y.cjs.map} +1 -1
- package/dist/{chunk-7GOKQJTQ.js → chunk-PUKJP2Z5.js} +2 -2
- package/dist/{chunk-QCXYKSZS.cjs → chunk-PWC4CRWT.cjs} +19 -19
- package/dist/{chunk-QCXYKSZS.cjs.map → chunk-PWC4CRWT.cjs.map} +1 -1
- package/dist/{chunk-FPBZFNCD.js → chunk-RFIBF2RR.js} +47 -18
- package/dist/{chunk-FPBZFNCD.js.map → chunk-RFIBF2RR.js.map} +1 -1
- package/dist/{chunk-XKSO2V5I.cjs → chunk-S5BKZZGM.cjs} +1 -1
- package/dist/{chunk-XKSO2V5I.cjs.map → chunk-S5BKZZGM.cjs.map} +1 -1
- package/dist/{chunk-HOHFYGKW.js → chunk-UMWZ4ZET.js} +2 -2
- package/dist/{chunk-XVMJO3QF.cjs → chunk-YFA7X2F6.cjs} +489 -887
- package/dist/chunk-YFA7X2F6.cjs.map +1 -0
- package/dist/{chunk-57WB2EDF.js → chunk-ZBUROTJQ.js} +3 -3
- package/dist/{coerce-wwdELNYS.d.ts → coerce-DP0vYu-x.d.ts} +77 -1
- package/dist/{coerce-CJlgULrQ.d.cts → coerce-DRERzdve.d.cts} +77 -1
- package/dist/core/index.cjs +5 -5
- package/dist/core/index.d.cts +2 -2
- package/dist/core/index.d.ts +2 -2
- 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 +87 -9
- package/dist/generate/index.d.ts +87 -9
- package/dist/generate/index.js +19 -7
- package/dist/{index-BaoKVVhE.d.cts → index-B9QLziDe.d.cts} +2 -2
- package/dist/{index-CnHQgqNh.d.ts → index-BHIUyjhZ.d.ts} +2 -2
- package/dist/{index-CBCP84rv.d.cts → index-BL5N2Oyz.d.cts} +1 -1
- package/dist/{index-D_hzxD8w.d.cts → index-BMweUlm5.d.cts} +3 -0
- package/dist/{index-Ci3xyows.d.ts → index-CL4b4gUQ.d.ts} +3 -0
- package/dist/{index-BzLNkmA7.d.cts → index-CY-3NVfy.d.cts} +486 -5
- package/dist/{index-CnIdP4EZ.d.ts → index-Dw2DP7vm.d.ts} +1 -1
- package/dist/{index-DpC-mDlV.d.ts → index-zaZdmnU4.d.ts} +486 -5
- package/dist/index.cjs +49 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -10
- package/dist/index.d.ts +10 -10
- package/dist/index.js +55 -19
- package/dist/{masks-O73Rhj5d.d.ts → masks-Cc7-MzzY.d.ts} +1 -1
- package/dist/{masks-C8TWLFIi.d.cts → masks-pADfTOiD.d.cts} +1 -1
- package/dist/model/index.cjs +12 -12
- package/dist/model/index.d.cts +213 -16
- package/dist/model/index.d.ts +213 -16
- package/dist/model/index.js +11 -11
- package/dist/{index-DTsKaPaC.d.cts → profile-CQMEqdH9.d.cts} +88 -601
- package/dist/{index-BUtIf2go.d.ts → profile-DLdlIpS9.d.ts} +88 -601
- package/dist/theory/index.cjs +5 -5
- package/dist/theory/index.d.cts +7 -7
- package/dist/theory/index.d.ts +7 -7
- package/dist/theory/index.js +4 -4
- package/dist/{index-BpWOhkkE.d.ts → types-D-XgxGvp.d.cts} +1170 -14
- package/dist/{index-B3WCj_Hm.d.cts → types-Dh7C064C.d.ts} +1170 -14
- package/package.json +6 -5
- package/dist/chunk-5KW7DNCJ.cjs.map +0 -1
- package/dist/chunk-DKICR7AN.js.map +0 -1
- package/dist/chunk-GDKE6RQW.cjs.map +0 -1
- package/dist/chunk-HSY2VQXZ.js.map +0 -1
- package/dist/chunk-Q6R3QAZD.cjs.map +0 -1
- package/dist/chunk-R5VUXQMP.js.map +0 -1
- package/dist/chunk-VLE5R7PF.js.map +0 -1
- package/dist/chunk-XVMJO3QF.cjs.map +0 -1
- package/dist/chunk-YPB3A3EY.cjs.map +0 -1
- /package/dist/{chunk-X6RJAHII.js.map → chunk-7KG3RTYS.js.map} +0 -0
- /package/dist/{chunk-AWEMD747.js.map → chunk-M5RYMAFI.js.map} +0 -0
- /package/dist/{chunk-H66ARFU5.js.map → chunk-NB25P3EH.js.map} +0 -0
- /package/dist/{chunk-7GOKQJTQ.js.map → chunk-PUKJP2Z5.js.map} +0 -0
- /package/dist/{chunk-HOHFYGKW.js.map → chunk-UMWZ4ZET.js.map} +0 -0
- /package/dist/{chunk-57WB2EDF.js.map → chunk-ZBUROTJQ.js.map} +0 -0
|
@@ -1,16 +1,40 @@
|
|
|
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
23
|
isMinorKey,
|
|
10
|
-
|
|
11
|
-
|
|
24
|
+
keyLookup,
|
|
25
|
+
melodicSimilarity,
|
|
26
|
+
onsetSimilarity,
|
|
27
|
+
parallelKey,
|
|
28
|
+
phraseMelody,
|
|
29
|
+
registerSimilarity,
|
|
30
|
+
rhythmLevel,
|
|
31
|
+
romanToChord,
|
|
32
|
+
syncopationSimilarity
|
|
33
|
+
} from "./chunk-GNVN4PPJ.js";
|
|
12
34
|
import {
|
|
35
|
+
ALGORITHM_VERSION,
|
|
13
36
|
LIMBS,
|
|
37
|
+
MIN_ALGORITHM_VERSION,
|
|
14
38
|
canSound,
|
|
15
39
|
createPositionalRng,
|
|
16
40
|
deriveSeed,
|
|
@@ -19,14 +43,14 @@ import {
|
|
|
19
43
|
instrumentRange,
|
|
20
44
|
resolveAlgorithmVersion,
|
|
21
45
|
toStringedProfile
|
|
22
|
-
} from "./chunk-
|
|
46
|
+
} from "./chunk-7KG3RTYS.js";
|
|
23
47
|
import {
|
|
24
48
|
createNoteEventIndex
|
|
25
|
-
} from "./chunk-
|
|
49
|
+
} from "./chunk-PUKJP2Z5.js";
|
|
26
50
|
import {
|
|
27
51
|
chordScales,
|
|
28
52
|
roleOf
|
|
29
|
-
} from "./chunk-
|
|
53
|
+
} from "./chunk-NB25P3EH.js";
|
|
30
54
|
import {
|
|
31
55
|
chordPitchClasses,
|
|
32
56
|
chordQualities,
|
|
@@ -50,26 +74,33 @@ import {
|
|
|
50
74
|
resolveKey,
|
|
51
75
|
scaleLadderPitch,
|
|
52
76
|
scaleLadderPosition,
|
|
77
|
+
scaleOf,
|
|
53
78
|
scaleSystemOf,
|
|
54
79
|
scaleTonesInDegreeOrder,
|
|
55
80
|
shiftByScaleDegrees,
|
|
56
81
|
soundsDominantSeventh,
|
|
82
|
+
spanFromChord,
|
|
57
83
|
spellPitchClass,
|
|
58
84
|
toChordData,
|
|
59
85
|
toKeyScale,
|
|
60
86
|
tonicizableDegrees
|
|
61
|
-
} from "./chunk-
|
|
87
|
+
} from "./chunk-RFIBF2RR.js";
|
|
62
88
|
import {
|
|
63
89
|
ARTICULATIONS,
|
|
64
90
|
BEAT_EPS,
|
|
65
91
|
InvalidInputError,
|
|
92
|
+
NoSolutionError,
|
|
93
|
+
allocateCandidateTable,
|
|
94
|
+
allocateChoiceTable,
|
|
66
95
|
assertArray,
|
|
67
96
|
assertDegree,
|
|
68
97
|
assertFiniteNumber,
|
|
69
98
|
assertFiniteSemitones,
|
|
99
|
+
assertFlag,
|
|
70
100
|
assertFunction,
|
|
71
101
|
assertGenerationBudget,
|
|
72
102
|
assertInteger,
|
|
103
|
+
assertMeterMap,
|
|
73
104
|
assertMidiPitch,
|
|
74
105
|
assertNoteEvents,
|
|
75
106
|
assertOneOf,
|
|
@@ -78,7 +109,9 @@ import {
|
|
|
78
109
|
assertRange,
|
|
79
110
|
assertRecord,
|
|
80
111
|
assertTimeSignature,
|
|
112
|
+
barStartBeat,
|
|
81
113
|
beatsPerBar,
|
|
114
|
+
beatsPerBarAt,
|
|
82
115
|
clampToMidi,
|
|
83
116
|
describeRejected,
|
|
84
117
|
dropSilentNotes,
|
|
@@ -92,10 +125,12 @@ import {
|
|
|
92
125
|
pulseBeats,
|
|
93
126
|
resolveMeters,
|
|
94
127
|
soundingNotesOnly,
|
|
128
|
+
spelledInterval,
|
|
95
129
|
toMeterData,
|
|
96
130
|
toSpelledInterval,
|
|
131
|
+
transposeByInterval,
|
|
97
132
|
transposeNote
|
|
98
|
-
} from "./chunk-
|
|
133
|
+
} from "./chunk-AGOVW7U6.js";
|
|
99
134
|
|
|
100
135
|
// src/generate/context/draw.ts
|
|
101
136
|
var DRAW_METHODS = ["at", "prob", "range", "float"];
|
|
@@ -4837,6 +4872,1063 @@ function developMotif(cell, timeline, key, bars, ts) {
|
|
|
4837
4872
|
return inTimeOrder({ notes: out });
|
|
4838
4873
|
}
|
|
4839
4874
|
|
|
4875
|
+
// src/generate/plan/internal.ts
|
|
4876
|
+
var STRONG_WEIGHT = 2;
|
|
4877
|
+
function positionClass(beat, meters) {
|
|
4878
|
+
const weight = metricWeight(beat, meters);
|
|
4879
|
+
return weight >= STRONG_WEIGHT ? "strong" : weight > 0 ? "weak" : "off";
|
|
4880
|
+
}
|
|
4881
|
+
|
|
4882
|
+
// src/generate/plan/types.ts
|
|
4883
|
+
var COMPOSITION_PLAN_VERSION = 1;
|
|
4884
|
+
function tent(t, apex) {
|
|
4885
|
+
if (t <= apex) {
|
|
4886
|
+
return apex === 0 ? 1 : -1 + 2 * t / apex;
|
|
4887
|
+
}
|
|
4888
|
+
return apex === 1 ? 1 : 1 - 2 * (t - apex) / (1 - apex);
|
|
4889
|
+
}
|
|
4890
|
+
function plannedContourAt(shape, peakPosition, t) {
|
|
4891
|
+
switch (shape) {
|
|
4892
|
+
case "arch":
|
|
4893
|
+
return tent(t, peakPosition);
|
|
4894
|
+
case "ascending":
|
|
4895
|
+
return -1 + 2 * t;
|
|
4896
|
+
case "descending":
|
|
4897
|
+
return 1 - 2 * t;
|
|
4898
|
+
case "wave":
|
|
4899
|
+
return t < 0.5 ? tent(2 * t, 0.5) : tent(2 * t - 1, 0.5);
|
|
4900
|
+
case "static":
|
|
4901
|
+
return 0;
|
|
4902
|
+
}
|
|
4903
|
+
}
|
|
4904
|
+
|
|
4905
|
+
// src/generate/plan/validate.ts
|
|
4906
|
+
var MASS_TOLERANCE = 1e-9;
|
|
4907
|
+
function assertString(value, name) {
|
|
4908
|
+
if (typeof value !== "string") {
|
|
4909
|
+
throw new InvalidInputError(`${name} must be a string; received ${describeRejected(value)}`);
|
|
4910
|
+
}
|
|
4911
|
+
return value;
|
|
4912
|
+
}
|
|
4913
|
+
function assertSpanOrder(startBeat, endBeat, name) {
|
|
4914
|
+
if (endBeat < startBeat) {
|
|
4915
|
+
throw new InvalidInputError(
|
|
4916
|
+
`${name}.endBeat must be at least ${name}.startBeat; received ${endBeat} < ${startBeat}`
|
|
4917
|
+
);
|
|
4918
|
+
}
|
|
4919
|
+
}
|
|
4920
|
+
function assertNullable(value, name, validate) {
|
|
4921
|
+
if (value === void 0) {
|
|
4922
|
+
throw new InvalidInputError(`${name} must be present; received undefined`);
|
|
4923
|
+
}
|
|
4924
|
+
return value === null ? null : validate(value, name);
|
|
4925
|
+
}
|
|
4926
|
+
function assertMassDistribution(value, length, name) {
|
|
4927
|
+
const shares = assertArray(value, name);
|
|
4928
|
+
if (shares.length !== length) {
|
|
4929
|
+
throw new InvalidInputError(`${name} must have length ${length}; received ${shares.length}`);
|
|
4930
|
+
}
|
|
4931
|
+
let sum = 0;
|
|
4932
|
+
for (let index = 0; index < shares.length; index += 1) {
|
|
4933
|
+
const share = assertFiniteNumber(shares[index], `${name}[${index}]`);
|
|
4934
|
+
if (share < 0) {
|
|
4935
|
+
throw new InvalidInputError(`${name}[${index}] must be at least 0; received ${share}`);
|
|
4936
|
+
}
|
|
4937
|
+
sum += share;
|
|
4938
|
+
}
|
|
4939
|
+
if (Math.abs(sum - 1) > MASS_TOLERANCE) {
|
|
4940
|
+
throw new InvalidInputError(`${name} must sum to 1; received ${sum}`);
|
|
4941
|
+
}
|
|
4942
|
+
return shares;
|
|
4943
|
+
}
|
|
4944
|
+
function assertResolvedKeyField(value, name) {
|
|
4945
|
+
const read = assertRecord(value, name);
|
|
4946
|
+
try {
|
|
4947
|
+
resolveKey(read);
|
|
4948
|
+
} catch (error) {
|
|
4949
|
+
if (error instanceof InvalidInputError) {
|
|
4950
|
+
throw new InvalidInputError(`${name} must be a valid key: ${error.message}`);
|
|
4951
|
+
}
|
|
4952
|
+
throw error;
|
|
4953
|
+
}
|
|
4954
|
+
return read;
|
|
4955
|
+
}
|
|
4956
|
+
function assertPlannedSection(value, name) {
|
|
4957
|
+
const read = assertRecord(value, name);
|
|
4958
|
+
assertString(read.label, `${name}.label`);
|
|
4959
|
+
const startBeat = assertFiniteNumber(read.startBeat, `${name}.startBeat`);
|
|
4960
|
+
const endBeat = assertFiniteNumber(read.endBeat, `${name}.endBeat`);
|
|
4961
|
+
assertSpanOrder(startBeat, endBeat, name);
|
|
4962
|
+
return read;
|
|
4963
|
+
}
|
|
4964
|
+
function assertPlannedPhrase(value, name, sectionCount, motifCount) {
|
|
4965
|
+
const read = assertRecord(value, name);
|
|
4966
|
+
const startBeat = assertFiniteNumber(read.startBeat, `${name}.startBeat`);
|
|
4967
|
+
const endBeat = assertFiniteNumber(read.endBeat, `${name}.endBeat`);
|
|
4968
|
+
assertSpanOrder(startBeat, endBeat, name);
|
|
4969
|
+
assertNullable(
|
|
4970
|
+
read.section,
|
|
4971
|
+
`${name}.section`,
|
|
4972
|
+
(v, n) => assertInteger(v, n, 0, Math.max(sectionCount - 1, 0))
|
|
4973
|
+
);
|
|
4974
|
+
assertNullable(read.cadence, `${name}.cadence`, (v, n) => assertOneOf(v, CADENCE_TYPES, n));
|
|
4975
|
+
assertOneOf(read.shape, MELODIC_CONTOUR_SHAPES, `${name}.shape`);
|
|
4976
|
+
assertRange(read.peakPosition, 0, 1, `${name}.peakPosition`);
|
|
4977
|
+
const register = assertRecord(
|
|
4978
|
+
read.register,
|
|
4979
|
+
`${name}.register`
|
|
4980
|
+
);
|
|
4981
|
+
assertMidiPitch(register.low, `${name}.register.low`);
|
|
4982
|
+
assertMidiPitch(register.high, `${name}.register.high`);
|
|
4983
|
+
assertRange(register.mean, 0, 127, `${name}.register.mean`);
|
|
4984
|
+
assertRange(read.onsetDensity, 0, Number.MAX_SAFE_INTEGER, `${name}.onsetDensity`);
|
|
4985
|
+
const motifs = assertArray(read.motifs, `${name}.motifs`);
|
|
4986
|
+
for (let index = 0; index < motifs.length; index += 1) {
|
|
4987
|
+
assertInteger(
|
|
4988
|
+
motifs[index],
|
|
4989
|
+
`${name}.motifs[${index}]`,
|
|
4990
|
+
0,
|
|
4991
|
+
Math.max(motifCount - 1, 0)
|
|
4992
|
+
);
|
|
4993
|
+
}
|
|
4994
|
+
return read;
|
|
4995
|
+
}
|
|
4996
|
+
function assertPlannedChord(value, name, keys) {
|
|
4997
|
+
const read = assertRecord(value, name);
|
|
4998
|
+
const startBeat = assertFiniteNumber(read.startBeat, `${name}.startBeat`);
|
|
4999
|
+
const endBeat = assertFiniteNumber(read.endBeat, `${name}.endBeat`);
|
|
5000
|
+
assertSpanOrder(startBeat, endBeat, name);
|
|
5001
|
+
const keyIndex = assertInteger(read.key, `${name}.key`, 0, Math.max(keys.length - 1, 0));
|
|
5002
|
+
const roman = assertString(read.roman, `${name}.roman`);
|
|
5003
|
+
try {
|
|
5004
|
+
romanToChord(roman, keys[keyIndex]);
|
|
5005
|
+
} catch (error) {
|
|
5006
|
+
if (error instanceof InvalidInputError) {
|
|
5007
|
+
throw new InvalidInputError(
|
|
5008
|
+
`${name}.roman must be readable by romanToChord; received ${describeRejected(roman)}: ${error.message}`
|
|
5009
|
+
);
|
|
5010
|
+
}
|
|
5011
|
+
throw error;
|
|
5012
|
+
}
|
|
5013
|
+
return read;
|
|
5014
|
+
}
|
|
5015
|
+
function assertPlannedMotifRelation(value, name) {
|
|
5016
|
+
const read = assertRecord(value, name);
|
|
5017
|
+
assertOneOf(read.kind, MOTIF_RELATION_KINDS, `${name}.kind`);
|
|
5018
|
+
assertFlag(read.sequence, `${name}.sequence`);
|
|
5019
|
+
assertFiniteNumber(read.semitones, `${name}.semitones`);
|
|
5020
|
+
if (read.kind === "tonalTransposition") {
|
|
5021
|
+
assertFiniteNumber(read.degrees, `${name}.degrees`);
|
|
5022
|
+
} else if (read.degrees !== void 0) {
|
|
5023
|
+
assertFiniteNumber(read.degrees, `${name}.degrees`);
|
|
5024
|
+
}
|
|
5025
|
+
const timeRatio = assertFiniteNumber(read.timeRatio, `${name}.timeRatio`);
|
|
5026
|
+
if (timeRatio <= 0) {
|
|
5027
|
+
throw new InvalidInputError(`${name}.timeRatio must be positive; received ${timeRatio}`);
|
|
5028
|
+
}
|
|
5029
|
+
return read;
|
|
5030
|
+
}
|
|
5031
|
+
function assertPlannedMotif(value, name, ownIndex, motifCount, phraseCount) {
|
|
5032
|
+
const read = assertRecord(value, name);
|
|
5033
|
+
assertInteger(read.phrase, `${name}.phrase`, 0, Math.max(phraseCount - 1, 0));
|
|
5034
|
+
const startBeat = assertFiniteNumber(read.startBeat, `${name}.startBeat`);
|
|
5035
|
+
const endBeat = assertFiniteNumber(read.endBeat, `${name}.endBeat`);
|
|
5036
|
+
assertSpanOrder(startBeat, endBeat, name);
|
|
5037
|
+
assertPositiveInt(read.notes, `${name}.notes`);
|
|
5038
|
+
const from = assertNullable(
|
|
5039
|
+
read.from,
|
|
5040
|
+
`${name}.from`,
|
|
5041
|
+
(v, n) => assertInteger(v, n, 0, Math.max(motifCount - 1, 0))
|
|
5042
|
+
);
|
|
5043
|
+
if (from !== null && from >= ownIndex) {
|
|
5044
|
+
throw new InvalidInputError(
|
|
5045
|
+
`${name}.from must be less than this motif's own index (${ownIndex}); received ${from}`
|
|
5046
|
+
);
|
|
5047
|
+
}
|
|
5048
|
+
const relation = assertNullable(read.relation, `${name}.relation`, assertPlannedMotifRelation);
|
|
5049
|
+
if (from === null && relation !== null) {
|
|
5050
|
+
throw new InvalidInputError(
|
|
5051
|
+
`${name}.relation must be null for a root motif (from = null); received a relation`
|
|
5052
|
+
);
|
|
5053
|
+
}
|
|
5054
|
+
const rhythm = assertNullable(read.rhythm, `${name}.rhythm`, (v, n) => assertArray(v, n));
|
|
5055
|
+
if (rhythm !== null) {
|
|
5056
|
+
if (from !== null) {
|
|
5057
|
+
throw new InvalidInputError(`${name}.rhythm must be null for a derived statement`);
|
|
5058
|
+
}
|
|
5059
|
+
if (rhythm.length !== read.notes - 1) {
|
|
5060
|
+
throw new InvalidInputError(
|
|
5061
|
+
`${name}.rhythm must hold notes - 1 (${read.notes - 1}) gaps; received ${rhythm.length}`
|
|
5062
|
+
);
|
|
5063
|
+
}
|
|
5064
|
+
let total = 0;
|
|
5065
|
+
for (let index = 0; index < rhythm.length; index += 1) {
|
|
5066
|
+
const gap = assertFiniteNumber(rhythm[index], `${name}.rhythm[${index}]`);
|
|
5067
|
+
if (gap <= 0) {
|
|
5068
|
+
throw new InvalidInputError(`${name}.rhythm[${index}] must be positive; received ${gap}`);
|
|
5069
|
+
}
|
|
5070
|
+
total += gap;
|
|
5071
|
+
}
|
|
5072
|
+
if (total >= endBeat - startBeat - BEAT_EPS) {
|
|
5073
|
+
throw new InvalidInputError(
|
|
5074
|
+
`${name}.rhythm must sum to less than the statement's length (${endBeat - startBeat}); received ${total}`
|
|
5075
|
+
);
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
5078
|
+
return read;
|
|
5079
|
+
}
|
|
5080
|
+
function assertPlannedRhythm(value, name) {
|
|
5081
|
+
const read = assertRecord(value, name);
|
|
5082
|
+
assertMassDistribution(read.onsetLevels, 6, `${name}.onsetLevels`);
|
|
5083
|
+
assertMassDistribution(read.interOnsetShares, RHYTHM_IOI_BINS.length, `${name}.interOnsetShares`);
|
|
5084
|
+
assertRange(read.syncopation, 0, 1, `${name}.syncopation`);
|
|
5085
|
+
return read;
|
|
5086
|
+
}
|
|
5087
|
+
function assertCompositionPlan(value, name = "composition plan") {
|
|
5088
|
+
const read = assertRecord(value, name);
|
|
5089
|
+
if (read.planVersion !== COMPOSITION_PLAN_VERSION) {
|
|
5090
|
+
throw new InvalidInputError(
|
|
5091
|
+
`${name}.planVersion must be ${COMPOSITION_PLAN_VERSION}; received ${describeRejected(read.planVersion)}`
|
|
5092
|
+
);
|
|
5093
|
+
}
|
|
5094
|
+
assertInteger(read.seed, `${name}.seed`, 0, 4294967295);
|
|
5095
|
+
assertInteger(
|
|
5096
|
+
read.algorithmVersion,
|
|
5097
|
+
`${name}.algorithmVersion`,
|
|
5098
|
+
MIN_ALGORITHM_VERSION,
|
|
5099
|
+
ALGORITHM_VERSION
|
|
5100
|
+
);
|
|
5101
|
+
const keys = assertArray(read.keys, `${name}.keys`);
|
|
5102
|
+
if (keys.length === 0) {
|
|
5103
|
+
throw new InvalidInputError(`${name}.keys must hold at least one key; received an empty array`);
|
|
5104
|
+
}
|
|
5105
|
+
for (let index = 0; index < keys.length; index += 1) {
|
|
5106
|
+
assertResolvedKeyField(keys[index], `${name}.keys[${index}]`);
|
|
5107
|
+
}
|
|
5108
|
+
assertMeterMap(read.meters, `${name}.meters`);
|
|
5109
|
+
const span = assertRecord(
|
|
5110
|
+
read.span,
|
|
5111
|
+
`${name}.span`
|
|
5112
|
+
);
|
|
5113
|
+
const spanStart = assertFiniteNumber(span.startBeat, `${name}.span.startBeat`);
|
|
5114
|
+
const spanEnd = assertFiniteNumber(span.endBeat, `${name}.span.endBeat`);
|
|
5115
|
+
assertSpanOrder(spanStart, spanEnd, `${name}.span`);
|
|
5116
|
+
assertRange(span.bars, 0, Number.MAX_SAFE_INTEGER, `${name}.span.bars`);
|
|
5117
|
+
const sections = assertArray(read.sections, `${name}.sections`);
|
|
5118
|
+
for (let index = 0; index < sections.length; index += 1) {
|
|
5119
|
+
assertPlannedSection(sections[index], `${name}.sections[${index}]`);
|
|
5120
|
+
}
|
|
5121
|
+
const phrases = assertArray(read.phrases, `${name}.phrases`);
|
|
5122
|
+
const motifs = assertArray(read.motifs, `${name}.motifs`);
|
|
5123
|
+
for (let index = 0; index < phrases.length; index += 1) {
|
|
5124
|
+
assertPlannedPhrase(
|
|
5125
|
+
phrases[index],
|
|
5126
|
+
`${name}.phrases[${index}]`,
|
|
5127
|
+
sections.length,
|
|
5128
|
+
motifs.length
|
|
5129
|
+
);
|
|
5130
|
+
}
|
|
5131
|
+
const harmony = assertArray(read.harmony, `${name}.harmony`);
|
|
5132
|
+
for (let index = 0; index < harmony.length; index += 1) {
|
|
5133
|
+
assertPlannedChord(harmony[index], `${name}.harmony[${index}]`, keys);
|
|
5134
|
+
}
|
|
5135
|
+
for (let index = 0; index < motifs.length; index += 1) {
|
|
5136
|
+
assertPlannedMotif(
|
|
5137
|
+
motifs[index],
|
|
5138
|
+
`${name}.motifs[${index}]`,
|
|
5139
|
+
index,
|
|
5140
|
+
motifs.length,
|
|
5141
|
+
phrases.length
|
|
5142
|
+
);
|
|
5143
|
+
}
|
|
5144
|
+
assertPlannedRhythm(read.rhythm, `${name}.rhythm`);
|
|
5145
|
+
return read;
|
|
5146
|
+
}
|
|
5147
|
+
|
|
5148
|
+
// src/generate/plan/timeline.ts
|
|
5149
|
+
function planTimeline(plan) {
|
|
5150
|
+
return planChordTimeline(assertCompositionPlan(plan));
|
|
5151
|
+
}
|
|
5152
|
+
function planChordTimeline(plan) {
|
|
5153
|
+
const spans = plan.harmony.map((chord) => {
|
|
5154
|
+
const key = plan.keys[chord.key];
|
|
5155
|
+
return spanFromChord(romanToChord(chord.roman, key), chord.startBeat);
|
|
5156
|
+
});
|
|
5157
|
+
return chordTimelineFromChords(spans, plan.span.endBeat);
|
|
5158
|
+
}
|
|
5159
|
+
|
|
5160
|
+
// src/generate/plan/harmony.ts
|
|
5161
|
+
var HARMONY_OK_KINDS = /* @__PURE__ */ new Set([
|
|
5162
|
+
"chordTone",
|
|
5163
|
+
"tension",
|
|
5164
|
+
"suspension",
|
|
5165
|
+
"appoggiatura",
|
|
5166
|
+
"anticipation",
|
|
5167
|
+
"passing",
|
|
5168
|
+
"neighbor"
|
|
5169
|
+
]);
|
|
5170
|
+
var CADENCE_DEGREES = {
|
|
5171
|
+
authentic: [1, 3],
|
|
5172
|
+
plagal: [1, 3],
|
|
5173
|
+
half: [2, 5, 7],
|
|
5174
|
+
phrygian: [2, 5, 7],
|
|
5175
|
+
modal: [1]
|
|
5176
|
+
};
|
|
5177
|
+
function planKeyAt(plan) {
|
|
5178
|
+
const regions = [...plan.harmony].sort((a, b) => a.startBeat - b.startBeat).map((chord) => ({
|
|
5179
|
+
startBeat: chord.startBeat,
|
|
5180
|
+
endBeat: chord.endBeat,
|
|
5181
|
+
key: plan.keys[chord.key],
|
|
5182
|
+
confidence: 1
|
|
5183
|
+
}));
|
|
5184
|
+
return keyLookup(regions, plan.keys[0]);
|
|
5185
|
+
}
|
|
5186
|
+
function cadencePitchClasses(plan, timeline, cadence, beat) {
|
|
5187
|
+
if (cadence === "deceptive") {
|
|
5188
|
+
const chord = timeline.at(beat);
|
|
5189
|
+
return chord === null ? null : chordPitchClasses(chord);
|
|
5190
|
+
}
|
|
5191
|
+
const entry = plan.harmony.find((chord) => beat >= chord.startBeat && beat < chord.endBeat);
|
|
5192
|
+
const tones = scaleTonesInDegreeOrder(scaleOf(plan.keys[entry?.key ?? 0]));
|
|
5193
|
+
return CADENCE_DEGREES[cadence].map((degree) => tones[degree - 1]).filter((pc) => pc !== void 0);
|
|
5194
|
+
}
|
|
5195
|
+
function planHarmonyMisfits(plan, timeline = planChordTimeline(plan)) {
|
|
5196
|
+
const keyAt = planKeyAt(plan);
|
|
5197
|
+
const scaleAt2 = (beat) => scaleOf(keyAt(beat));
|
|
5198
|
+
return (line) => {
|
|
5199
|
+
const analyzed = analyzeVoice(line, timeline.at, scaleAt2);
|
|
5200
|
+
return line.map((note2, index) => {
|
|
5201
|
+
const position = positionClass(note2.startBeat, plan.meters);
|
|
5202
|
+
if (position === "off") {
|
|
5203
|
+
return null;
|
|
5204
|
+
}
|
|
5205
|
+
const kinds = (analyzed[index]?.labels ?? []).map((label) => label.kind);
|
|
5206
|
+
if (kinds.some((kind) => HARMONY_OK_KINDS.has(kind))) {
|
|
5207
|
+
return null;
|
|
5208
|
+
}
|
|
5209
|
+
return position === "weak" && isScaleTone(note2.pitch, scaleAt2(note2.startBeat)) ? null : kinds;
|
|
5210
|
+
});
|
|
5211
|
+
};
|
|
5212
|
+
}
|
|
5213
|
+
|
|
5214
|
+
// src/generate/melody/pitch-dp.ts
|
|
5215
|
+
var NO_LEAP = 0;
|
|
5216
|
+
var LEAP_UP = 1;
|
|
5217
|
+
var LEAP_DOWN = 2;
|
|
5218
|
+
var LEAP_STATES = 3;
|
|
5219
|
+
var SEARCH_LABEL = "melody pitch search";
|
|
5220
|
+
var WIDEST_THIRD = 4;
|
|
5221
|
+
var LEAP_CONTINUATION_COST = 0.5;
|
|
5222
|
+
function intervalCost(size) {
|
|
5223
|
+
if (size === 0) {
|
|
5224
|
+
return 0.2;
|
|
5225
|
+
}
|
|
5226
|
+
if (size <= 2) {
|
|
5227
|
+
return 0;
|
|
5228
|
+
}
|
|
5229
|
+
if (size <= WIDEST_THIRD) {
|
|
5230
|
+
return 0.3;
|
|
5231
|
+
}
|
|
5232
|
+
if (size <= 7) {
|
|
5233
|
+
return 0.6;
|
|
5234
|
+
}
|
|
5235
|
+
return 1;
|
|
5236
|
+
}
|
|
5237
|
+
function leapOf(delta) {
|
|
5238
|
+
if (Math.abs(delta) <= WIDEST_THIRD) {
|
|
5239
|
+
return NO_LEAP;
|
|
5240
|
+
}
|
|
5241
|
+
return delta > 0 ? LEAP_UP : LEAP_DOWN;
|
|
5242
|
+
}
|
|
5243
|
+
function stepCost(from, to, leap, bothFixed) {
|
|
5244
|
+
const delta = to - from;
|
|
5245
|
+
const size = Math.abs(delta);
|
|
5246
|
+
if (!bothFixed && isForbiddenMelodicLeap(from, to)) {
|
|
5247
|
+
return Number.POSITIVE_INFINITY;
|
|
5248
|
+
}
|
|
5249
|
+
let cost = intervalCost(size);
|
|
5250
|
+
if (leap === LEAP_UP && delta > 0 || leap === LEAP_DOWN && delta < 0) {
|
|
5251
|
+
cost += LEAP_CONTINUATION_COST;
|
|
5252
|
+
}
|
|
5253
|
+
return cost;
|
|
5254
|
+
}
|
|
5255
|
+
function searchPitches(slots, budget, extra) {
|
|
5256
|
+
const first = slots[0];
|
|
5257
|
+
if (first === void 0) {
|
|
5258
|
+
return [];
|
|
5259
|
+
}
|
|
5260
|
+
let previous = allocateCandidateTable(
|
|
5261
|
+
first.candidates.length,
|
|
5262
|
+
LEAP_STATES,
|
|
5263
|
+
SEARCH_LABEL,
|
|
5264
|
+
budget
|
|
5265
|
+
).fill(Number.POSITIVE_INFINITY);
|
|
5266
|
+
for (let j = 0; j < first.candidates.length; j += 1) {
|
|
5267
|
+
previous[j * LEAP_STATES + NO_LEAP] = first.cost(first.candidates[j]);
|
|
5268
|
+
}
|
|
5269
|
+
const backs = [];
|
|
5270
|
+
for (let k = 1; k < slots.length; k += 1) {
|
|
5271
|
+
const before = slots[k - 1];
|
|
5272
|
+
const slot = slots[k];
|
|
5273
|
+
const unary = slot.candidates.map((pitch) => slot.cost(pitch));
|
|
5274
|
+
const current = allocateCandidateTable(
|
|
5275
|
+
slot.candidates.length,
|
|
5276
|
+
LEAP_STATES,
|
|
5277
|
+
SEARCH_LABEL,
|
|
5278
|
+
budget
|
|
5279
|
+
).fill(Number.POSITIVE_INFINITY);
|
|
5280
|
+
const back = allocateChoiceTable(
|
|
5281
|
+
slot.candidates.length,
|
|
5282
|
+
LEAP_STATES,
|
|
5283
|
+
SEARCH_LABEL,
|
|
5284
|
+
budget
|
|
5285
|
+
).fill(-1);
|
|
5286
|
+
const bothFixed = before.fixed && slot.fixed;
|
|
5287
|
+
for (let state2 = 0; state2 < previous.length; state2 += 1) {
|
|
5288
|
+
const reached = previous[state2];
|
|
5289
|
+
if (reached === Number.POSITIVE_INFINITY) {
|
|
5290
|
+
continue;
|
|
5291
|
+
}
|
|
5292
|
+
const from = before.candidates[Math.floor(state2 / LEAP_STATES)];
|
|
5293
|
+
const leap = state2 % LEAP_STATES;
|
|
5294
|
+
for (let j = 0; j < slot.candidates.length; j += 1) {
|
|
5295
|
+
const to = slot.candidates[j];
|
|
5296
|
+
const step = stepCost(from, to, leap, bothFixed);
|
|
5297
|
+
if (step === Number.POSITIVE_INFINITY) {
|
|
5298
|
+
continue;
|
|
5299
|
+
}
|
|
5300
|
+
const total = reached + step + unary[j] + (extra ? extra(k, from, to) : 0);
|
|
5301
|
+
const index = j * LEAP_STATES + leapOf(to - from);
|
|
5302
|
+
if (total < current[index]) {
|
|
5303
|
+
current[index] = total;
|
|
5304
|
+
back[index] = state2;
|
|
5305
|
+
}
|
|
5306
|
+
}
|
|
5307
|
+
}
|
|
5308
|
+
backs.push(back);
|
|
5309
|
+
previous = current;
|
|
5310
|
+
}
|
|
5311
|
+
let best = -1;
|
|
5312
|
+
for (let state2 = 0; state2 < previous.length; state2 += 1) {
|
|
5313
|
+
if (previous[state2] < Number.POSITIVE_INFINITY && (best < 0 || previous[state2] < previous[best])) {
|
|
5314
|
+
best = state2;
|
|
5315
|
+
}
|
|
5316
|
+
}
|
|
5317
|
+
if (best < 0) {
|
|
5318
|
+
return null;
|
|
5319
|
+
}
|
|
5320
|
+
const pitches = new Array(slots.length);
|
|
5321
|
+
let state = best;
|
|
5322
|
+
for (let k = slots.length - 1; k >= 0; k -= 1) {
|
|
5323
|
+
pitches[k] = slots[k].candidates[Math.floor(state / LEAP_STATES)];
|
|
5324
|
+
if (k > 0) {
|
|
5325
|
+
state = backs[k - 1][state];
|
|
5326
|
+
}
|
|
5327
|
+
}
|
|
5328
|
+
return pitches;
|
|
5329
|
+
}
|
|
5330
|
+
|
|
5331
|
+
// src/generate/melody/derive.ts
|
|
5332
|
+
var VARIATION_STEP_COST = 0.2;
|
|
5333
|
+
var OCTAVE = 12;
|
|
5334
|
+
var LEVEL_TARGET_COST = 0.1;
|
|
5335
|
+
var LEVEL_PLAN_COST = 0.02;
|
|
5336
|
+
var LEVEL_REACH = 24;
|
|
5337
|
+
var TONAL_LEVEL_REACH = 14;
|
|
5338
|
+
function stretched(cell, timeRatio) {
|
|
5339
|
+
return timeRatio === 1 ? cell : transformMotif(cell, "augment", timeRatio);
|
|
5340
|
+
}
|
|
5341
|
+
function shaped(cell, relation) {
|
|
5342
|
+
switch (relation.kind) {
|
|
5343
|
+
case "repetition":
|
|
5344
|
+
case "tonalTransposition":
|
|
5345
|
+
return cell;
|
|
5346
|
+
case "transposition":
|
|
5347
|
+
return stretched(cell, relation.timeRatio);
|
|
5348
|
+
case "inversion":
|
|
5349
|
+
return stretched(transformMotif(cell, "invert"), relation.timeRatio);
|
|
5350
|
+
case "retrograde":
|
|
5351
|
+
return transformMotif(cell, "retrograde");
|
|
5352
|
+
case "retrogradeInversion":
|
|
5353
|
+
return transformMotif(transformMotif(cell, "invert"), "retrograde");
|
|
5354
|
+
case "augmentation":
|
|
5355
|
+
return transformMotif(cell, "augment", relation.timeRatio);
|
|
5356
|
+
case "diminution":
|
|
5357
|
+
return transformMotif(cell, "diminish", 1 / relation.timeRatio);
|
|
5358
|
+
}
|
|
5359
|
+
}
|
|
5360
|
+
function relationLevels(kind, low = -LEVEL_REACH, high = LEVEL_REACH) {
|
|
5361
|
+
const between = (from, to, skipZero) => {
|
|
5362
|
+
const levels = [];
|
|
5363
|
+
for (let level = Math.ceil(from); level <= to; level += 1) {
|
|
5364
|
+
if (!(skipZero && level === 0)) levels.push(level);
|
|
5365
|
+
}
|
|
5366
|
+
return levels;
|
|
5367
|
+
};
|
|
5368
|
+
switch (kind) {
|
|
5369
|
+
case "repetition":
|
|
5370
|
+
case "augmentation":
|
|
5371
|
+
case "diminution":
|
|
5372
|
+
return [-OCTAVE, 0, OCTAVE];
|
|
5373
|
+
case "transposition":
|
|
5374
|
+
return between(low, high, true);
|
|
5375
|
+
case "tonalTransposition":
|
|
5376
|
+
return between(-TONAL_LEVEL_REACH, TONAL_LEVEL_REACH, true);
|
|
5377
|
+
case "inversion":
|
|
5378
|
+
case "retrograde":
|
|
5379
|
+
case "retrogradeInversion":
|
|
5380
|
+
return between(low, high, false);
|
|
5381
|
+
}
|
|
5382
|
+
}
|
|
5383
|
+
function replayAt(source, relation, level, startBeat, key) {
|
|
5384
|
+
const head = source[0];
|
|
5385
|
+
const cell = shaped({ notes: source.map((n) => ({ ...n })) }, relation);
|
|
5386
|
+
const first = cell.notes[0];
|
|
5387
|
+
const offset = startBeat - first.startBeat;
|
|
5388
|
+
const shift = head.pitch + level - first.pitch;
|
|
5389
|
+
return cell.notes.map((n) => ({
|
|
5390
|
+
...n,
|
|
5391
|
+
pitch: relation.kind === "tonalTransposition" ? shiftByScaleDegrees(n.pitch, level, key) : n.pitch + shift,
|
|
5392
|
+
startBeat: n.startBeat + offset
|
|
5393
|
+
}));
|
|
5394
|
+
}
|
|
5395
|
+
function clipStatement(notes, endBeat) {
|
|
5396
|
+
return notes.filter((n) => n.startBeat < endBeat - BEAT_EPS).map((n) => ({ ...n, durationBeat: Math.min(n.durationBeat, endBeat - n.startBeat) }));
|
|
5397
|
+
}
|
|
5398
|
+
function outsideCount(notes, frame) {
|
|
5399
|
+
return notes.filter((n) => n.pitch < frame.low || n.pitch > frame.high).length;
|
|
5400
|
+
}
|
|
5401
|
+
function nearerLevel(level, best, planned) {
|
|
5402
|
+
const distance = Math.abs(level - planned);
|
|
5403
|
+
const bestDistance = Math.abs(best - planned);
|
|
5404
|
+
return distance < bestDistance || distance === bestDistance && level < best;
|
|
5405
|
+
}
|
|
5406
|
+
function repairStatement(notes, forced, frame) {
|
|
5407
|
+
const slots = notes.map(
|
|
5408
|
+
(n, index) => forced[index] ? {
|
|
5409
|
+
candidates: frame.candidatesAt(n.startBeat),
|
|
5410
|
+
cost: frame.costAt(n.startBeat),
|
|
5411
|
+
fixed: false
|
|
5412
|
+
} : { candidates: [n.pitch], cost: () => 0, fixed: true }
|
|
5413
|
+
);
|
|
5414
|
+
const pitches = searchPitches(slots, frame.budget);
|
|
5415
|
+
if (pitches === null) {
|
|
5416
|
+
throw new NoSolutionError(
|
|
5417
|
+
`no repair fits the statement into the register ${frame.low}..${frame.high}`,
|
|
5418
|
+
{ at: frame.at }
|
|
5419
|
+
);
|
|
5420
|
+
}
|
|
5421
|
+
return notes.map((n, index) => ({ ...n, pitch: pitches[index] }));
|
|
5422
|
+
}
|
|
5423
|
+
function placeDerived(source, relation, startBeat, endBeat, key, frame, before) {
|
|
5424
|
+
const planned = relation.kind === "tonalTransposition" ? relation.degrees ?? 0 : relation.semitones;
|
|
5425
|
+
const replay = (level) => clipStatement(replayAt(source, relation, level, startBeat, key), endBeat);
|
|
5426
|
+
const base = replay(0).map((n) => n.pitch);
|
|
5427
|
+
const levels = relationLevels(
|
|
5428
|
+
relation.kind,
|
|
5429
|
+
Math.max(-LEVEL_REACH, frame.low - Math.min(...base)),
|
|
5430
|
+
Math.min(LEVEL_REACH, frame.high - Math.max(...base))
|
|
5431
|
+
);
|
|
5432
|
+
let best = null;
|
|
5433
|
+
for (const level of levels) {
|
|
5434
|
+
const notes2 = replay(level);
|
|
5435
|
+
if (outsideCount(notes2, frame) > 0) {
|
|
5436
|
+
continue;
|
|
5437
|
+
}
|
|
5438
|
+
const distance = notes2.reduce((sum, n) => sum + Math.abs(n.pitch - frame.targetAt(n.startBeat)), 0) / notes2.length;
|
|
5439
|
+
const cost = frame.misfits(notes2, before) + LEVEL_TARGET_COST * distance + LEVEL_PLAN_COST * Math.abs(level - planned);
|
|
5440
|
+
if (best === null || cost < best.cost || cost === best.cost && nearerLevel(level, best.level, planned)) {
|
|
5441
|
+
best = { level, notes: notes2, cost };
|
|
5442
|
+
}
|
|
5443
|
+
}
|
|
5444
|
+
if (best !== null) {
|
|
5445
|
+
return best.notes;
|
|
5446
|
+
}
|
|
5447
|
+
let fallback = null;
|
|
5448
|
+
for (const level of relationLevels(relation.kind)) {
|
|
5449
|
+
const notes2 = replay(level);
|
|
5450
|
+
const outside = outsideCount(notes2, frame);
|
|
5451
|
+
if (fallback === null || outside < fallback.outside || outside === fallback.outside && nearerLevel(level, fallback.level, planned)) {
|
|
5452
|
+
fallback = { level, notes: notes2, outside };
|
|
5453
|
+
}
|
|
5454
|
+
}
|
|
5455
|
+
const notes = fallback.notes;
|
|
5456
|
+
return repairStatement(
|
|
5457
|
+
notes,
|
|
5458
|
+
notes.map((n) => n.pitch < frame.low || n.pitch > frame.high),
|
|
5459
|
+
frame
|
|
5460
|
+
);
|
|
5461
|
+
}
|
|
5462
|
+
function varyStatement(source, startBeat, frame) {
|
|
5463
|
+
const offset = startBeat - source[0].startBeat;
|
|
5464
|
+
const last = source.length - 1;
|
|
5465
|
+
const inside = (pitch) => pitch >= frame.low && pitch <= frame.high;
|
|
5466
|
+
const outer = [source[0].pitch, source[last].pitch];
|
|
5467
|
+
const shift = [0, OCTAVE, -OCTAVE].find((s) => outer.every((pitch) => inside(pitch + s)));
|
|
5468
|
+
const notes = source.map((n) => ({ ...n, startBeat: n.startBeat + offset }));
|
|
5469
|
+
const slots = notes.map(
|
|
5470
|
+
(n, index) => (index === 0 || index === last) && shift !== void 0 ? { candidates: [n.pitch + shift], cost: () => 0, fixed: true } : {
|
|
5471
|
+
candidates: frame.candidatesAt(n.startBeat),
|
|
5472
|
+
cost: frame.costAt(n.startBeat),
|
|
5473
|
+
fixed: false
|
|
5474
|
+
}
|
|
5475
|
+
);
|
|
5476
|
+
const pitches = searchPitches(
|
|
5477
|
+
slots,
|
|
5478
|
+
frame.budget,
|
|
5479
|
+
(index, from, to) => VARIATION_STEP_COST * Math.abs(
|
|
5480
|
+
to - from - (source[index].pitch - source[index - 1].pitch)
|
|
5481
|
+
)
|
|
5482
|
+
);
|
|
5483
|
+
if (pitches === null) {
|
|
5484
|
+
throw new NoSolutionError(`no variation line fits the register ${frame.low}..${frame.high}`, {
|
|
5485
|
+
at: frame.at
|
|
5486
|
+
});
|
|
5487
|
+
}
|
|
5488
|
+
return notes.map((n, index) => ({ ...n, pitch: pitches[index] }));
|
|
5489
|
+
}
|
|
5490
|
+
|
|
5491
|
+
// src/generate/melody/index.ts
|
|
5492
|
+
var RHYTHM_SUBDIVISION = 4;
|
|
5493
|
+
var OFF_SCALE_COST = 1;
|
|
5494
|
+
var TARGET_COST = 0.1;
|
|
5495
|
+
var TIE_BREAK = 1e-6;
|
|
5496
|
+
var SEARCH_STATES_SQUARED = 9;
|
|
5497
|
+
function melodyContext(plan, input) {
|
|
5498
|
+
const fromPlan = { seed: plan.seed, algorithmVersion: plan.algorithmVersion };
|
|
5499
|
+
if (input === void 0) {
|
|
5500
|
+
return resolveContext(fromPlan);
|
|
5501
|
+
}
|
|
5502
|
+
if (typeof input === "number") {
|
|
5503
|
+
return resolveContext({ ...fromPlan, seed: input });
|
|
5504
|
+
}
|
|
5505
|
+
const read = assertRecord(input, "ctx");
|
|
5506
|
+
if (read.rng !== void 0) {
|
|
5507
|
+
throw new InvalidInputError(
|
|
5508
|
+
"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"
|
|
5509
|
+
);
|
|
5510
|
+
}
|
|
5511
|
+
return resolveContext({
|
|
5512
|
+
...read,
|
|
5513
|
+
seed: read.seed ?? plan.seed,
|
|
5514
|
+
algorithmVersion: read.algorithmVersion ?? plan.algorithmVersion
|
|
5515
|
+
});
|
|
5516
|
+
}
|
|
5517
|
+
function chordEntryAt(plan, beat) {
|
|
5518
|
+
return plan.harmony.find((chord) => beat >= chord.startBeat && beat < chord.endBeat);
|
|
5519
|
+
}
|
|
5520
|
+
function scaleAt(scene, beat) {
|
|
5521
|
+
return scene.scales[chordEntryAt(scene.plan, beat)?.key ?? 0];
|
|
5522
|
+
}
|
|
5523
|
+
function targetPitch(phrase, beat) {
|
|
5524
|
+
const length = phrase.endBeat - phrase.startBeat;
|
|
5525
|
+
const t = length > 0 ? Math.min(Math.max((beat - phrase.startBeat) / length, 0), 1) : 0;
|
|
5526
|
+
const { low, high, mean } = phrase.register;
|
|
5527
|
+
return mean + (high - low) / 2 * plannedContourAt(phrase.shape, phrase.peakPosition, t);
|
|
5528
|
+
}
|
|
5529
|
+
function registerPitches(phrase) {
|
|
5530
|
+
const pitches = [];
|
|
5531
|
+
for (let pitch = phrase.register.low; pitch <= phrase.register.high; pitch += 1) {
|
|
5532
|
+
pitches.push(pitch);
|
|
5533
|
+
}
|
|
5534
|
+
return pitches;
|
|
5535
|
+
}
|
|
5536
|
+
function frameOf(scene, index) {
|
|
5537
|
+
const phrase = scene.plan.phrases[index];
|
|
5538
|
+
const { low, high } = phrase.register;
|
|
5539
|
+
const register = registerPitches(phrase);
|
|
5540
|
+
const draw = scene.ctx.part("melody");
|
|
5541
|
+
return {
|
|
5542
|
+
low,
|
|
5543
|
+
high,
|
|
5544
|
+
at: phrase.startBeat,
|
|
5545
|
+
budget: scene.budget,
|
|
5546
|
+
candidatesAt: (beat) => {
|
|
5547
|
+
const position = positionClass(beat, scene.plan.meters);
|
|
5548
|
+
const chord = scene.timeline.at(beat);
|
|
5549
|
+
const tones = position === "strong" && chord !== null ? chordPitchClasses(chord) : null;
|
|
5550
|
+
const scale = scaleAt(scene, beat);
|
|
5551
|
+
const candidates = position === "off" ? register : tones !== null ? register.filter((pitch) => tones.includes(pitchClassOf(pitch))) : register.filter((pitch) => isScaleTone(pitch, scale));
|
|
5552
|
+
if (candidates.length === 0) {
|
|
5553
|
+
throw new NoSolutionError(
|
|
5554
|
+
`no ${position} pitch at beat ${beat} fits the register ${low}..${high}`,
|
|
5555
|
+
{ at: phrase.startBeat }
|
|
5556
|
+
);
|
|
5557
|
+
}
|
|
5558
|
+
return candidates;
|
|
5559
|
+
},
|
|
5560
|
+
costAt: (beat) => {
|
|
5561
|
+
const off = positionClass(beat, scene.plan.meters) === "off";
|
|
5562
|
+
const scale = scaleAt(scene, beat);
|
|
5563
|
+
const target = targetPitch(phrase, beat);
|
|
5564
|
+
return (pitch) => {
|
|
5565
|
+
let cost = TARGET_COST * Math.abs(pitch - target);
|
|
5566
|
+
if (off && !isScaleTone(pitch, scale)) {
|
|
5567
|
+
cost += OFF_SCALE_COST;
|
|
5568
|
+
}
|
|
5569
|
+
return cost + draw.float(0, TIE_BREAK, index, "tie", beat, pitch);
|
|
5570
|
+
};
|
|
5571
|
+
},
|
|
5572
|
+
targetAt: (beat) => targetPitch(phrase, beat),
|
|
5573
|
+
misfits: (notes, before) => scene.harmonyMisfits([...before, ...notes]).slice(before.length).filter((kinds) => kinds !== null).length
|
|
5574
|
+
};
|
|
5575
|
+
}
|
|
5576
|
+
function membersOf(plan) {
|
|
5577
|
+
return plan.phrases.map((phrase, index) => {
|
|
5578
|
+
const found = new Set(phrase.motifs);
|
|
5579
|
+
plan.motifs.forEach((motif, m) => {
|
|
5580
|
+
if (motif.phrase === index) {
|
|
5581
|
+
found.add(m);
|
|
5582
|
+
}
|
|
5583
|
+
});
|
|
5584
|
+
return [...found].sort(
|
|
5585
|
+
(a, b) => (plan.motifs[a]?.startBeat ?? 0) - (plan.motifs[b]?.startBeat ?? 0) || a - b
|
|
5586
|
+
);
|
|
5587
|
+
});
|
|
5588
|
+
}
|
|
5589
|
+
function segmentsOf(scene, index) {
|
|
5590
|
+
const phrase = scene.plan.phrases[index];
|
|
5591
|
+
const segments = [];
|
|
5592
|
+
let cursor = phrase.startBeat;
|
|
5593
|
+
for (const m of scene.members[index]) {
|
|
5594
|
+
const motif = scene.plan.motifs[m];
|
|
5595
|
+
if (motif === void 0) {
|
|
5596
|
+
continue;
|
|
5597
|
+
}
|
|
5598
|
+
if (motif.startBeat > cursor + BEAT_EPS) {
|
|
5599
|
+
segments.push({ startBeat: cursor, endBeat: motif.startBeat, motif: null });
|
|
5600
|
+
}
|
|
5601
|
+
segments.push({ startBeat: motif.startBeat, endBeat: motif.endBeat, motif: m });
|
|
5602
|
+
cursor = Math.max(cursor, motif.endBeat);
|
|
5603
|
+
}
|
|
5604
|
+
if (phrase.endBeat > cursor + BEAT_EPS) {
|
|
5605
|
+
segments.push({ startBeat: cursor, endBeat: phrase.endBeat, motif: null });
|
|
5606
|
+
}
|
|
5607
|
+
return segments;
|
|
5608
|
+
}
|
|
5609
|
+
function notesAt(onsets, endBeat) {
|
|
5610
|
+
return onsets.map((startBeat, i) => ({
|
|
5611
|
+
pitch: 0,
|
|
5612
|
+
startBeat,
|
|
5613
|
+
durationBeat: (onsets[i + 1] ?? endBeat) - startBeat
|
|
5614
|
+
}));
|
|
5615
|
+
}
|
|
5616
|
+
function onsetSlots(scene, startBeat, endBeat) {
|
|
5617
|
+
const { meters } = scene.plan;
|
|
5618
|
+
const slots = [startBeat];
|
|
5619
|
+
let bar2 = barStartBeat(startBeat, meters);
|
|
5620
|
+
while (bar2 < endBeat - BEAT_EPS) {
|
|
5621
|
+
const barEnd = bar2 + beatsPerBarAt(bar2, meters);
|
|
5622
|
+
for (let k = 0; bar2 + k / RHYTHM_SUBDIVISION < barEnd - BEAT_EPS; k += 1) {
|
|
5623
|
+
const beat = bar2 + k / RHYTHM_SUBDIVISION;
|
|
5624
|
+
if (beat > startBeat + BEAT_EPS && beat < endBeat - BEAT_EPS) {
|
|
5625
|
+
slots.push(beat);
|
|
5626
|
+
}
|
|
5627
|
+
}
|
|
5628
|
+
bar2 = barEnd;
|
|
5629
|
+
}
|
|
5630
|
+
return slots;
|
|
5631
|
+
}
|
|
5632
|
+
function drawOnsets(scene, phraseIndex, startBeat, endBeat, count) {
|
|
5633
|
+
const slots = onsetSlots(scene, startBeat, endBeat);
|
|
5634
|
+
assertGenerationBudget(slots.length * Math.max(1, count - 1), "melody onset slots", scene.budget);
|
|
5635
|
+
if (count - 1 > slots.length - 1) {
|
|
5636
|
+
throw new NoSolutionError(
|
|
5637
|
+
`${count} onsets do not fit the sixteenth grid of beats ${startBeat}..${endBeat}`,
|
|
5638
|
+
{ at: scene.plan.phrases[phraseIndex].startBeat }
|
|
5639
|
+
);
|
|
5640
|
+
}
|
|
5641
|
+
const levels = slots.map((beat) => rhythmLevel(beat, scene.plan.meters));
|
|
5642
|
+
const perLevel = /* @__PURE__ */ new Map();
|
|
5643
|
+
for (const level of levels) {
|
|
5644
|
+
perLevel.set(level, (perLevel.get(level) ?? 0) + 1);
|
|
5645
|
+
}
|
|
5646
|
+
const levelWeight = levels.map(
|
|
5647
|
+
(level) => (scene.plan.rhythm.onsetLevels[level] ?? 0) / perLevel.get(level)
|
|
5648
|
+
);
|
|
5649
|
+
const draw = scene.ctx.part("melody");
|
|
5650
|
+
const onsets = [startBeat];
|
|
5651
|
+
let current = 0;
|
|
5652
|
+
for (let k = 1; k < count; k += 1) {
|
|
5653
|
+
const last = slots.length - 1 - (count - 1 - k);
|
|
5654
|
+
const candidates = [];
|
|
5655
|
+
for (let j = current + 1; j <= last; j += 1) candidates.push(j);
|
|
5656
|
+
const gapShare = (j) => scene.plan.rhythm.interOnsetShares[ioiBinIndex(slots[j] - slots[current])] ?? 0;
|
|
5657
|
+
let weights = candidates.map((j) => levelWeight[j] * gapShare(j));
|
|
5658
|
+
if (!weights.some((w) => w > 0)) weights = candidates.map((j) => levelWeight[j]);
|
|
5659
|
+
if (!weights.some((w) => w > 0)) weights = candidates.map(() => 1);
|
|
5660
|
+
const total = weights.reduce((sum, w) => sum + w, 0);
|
|
5661
|
+
let target = draw.float(0, 1, phraseIndex, "onset", startBeat, k) * total;
|
|
5662
|
+
let pick = candidates.length - 1;
|
|
5663
|
+
for (let c = 0; c < candidates.length; c += 1) {
|
|
5664
|
+
target -= weights[c];
|
|
5665
|
+
if (target < 0) {
|
|
5666
|
+
pick = c;
|
|
5667
|
+
break;
|
|
5668
|
+
}
|
|
5669
|
+
}
|
|
5670
|
+
current = candidates[pick];
|
|
5671
|
+
onsets.push(slots[current]);
|
|
5672
|
+
}
|
|
5673
|
+
return notesAt(onsets, endBeat);
|
|
5674
|
+
}
|
|
5675
|
+
function placeRhythm(scene, motif) {
|
|
5676
|
+
const { rhythm, startBeat, endBeat, notes } = motif;
|
|
5677
|
+
if (rhythm === null) {
|
|
5678
|
+
return drawOnsets(scene, motif.phrase, startBeat, endBeat, notes);
|
|
5679
|
+
}
|
|
5680
|
+
const onsets = [startBeat];
|
|
5681
|
+
for (const gap of rhythm) {
|
|
5682
|
+
onsets.push(onsets.at(-1) + gap);
|
|
5683
|
+
}
|
|
5684
|
+
return notesAt(onsets, endBeat);
|
|
5685
|
+
}
|
|
5686
|
+
function searchedLine(rhythm, frame) {
|
|
5687
|
+
const slots = rhythm.map((note2) => ({
|
|
5688
|
+
candidates: frame.candidatesAt(note2.startBeat),
|
|
5689
|
+
cost: frame.costAt(note2.startBeat),
|
|
5690
|
+
fixed: false
|
|
5691
|
+
}));
|
|
5692
|
+
const pitches = searchPitches(slots, frame.budget);
|
|
5693
|
+
if (pitches === null) {
|
|
5694
|
+
throw new NoSolutionError(`no pitch line fits the register ${frame.low}..${frame.high}`, {
|
|
5695
|
+
at: frame.at
|
|
5696
|
+
});
|
|
5697
|
+
}
|
|
5698
|
+
return rhythm.map((note2, i) => ({ ...note2, pitch: pitches[i] }));
|
|
5699
|
+
}
|
|
5700
|
+
function statementOf(scene, m) {
|
|
5701
|
+
const known = scene.statements.get(m);
|
|
5702
|
+
if (known !== void 0) {
|
|
5703
|
+
return known;
|
|
5704
|
+
}
|
|
5705
|
+
scene.inProgress.add(m);
|
|
5706
|
+
const motif = scene.plan.motifs[m];
|
|
5707
|
+
const frame = scene.frames[motif.phrase];
|
|
5708
|
+
let notes;
|
|
5709
|
+
if (motif.from === null) {
|
|
5710
|
+
if (motif.endBeat - motif.startBeat <= BEAT_EPS) {
|
|
5711
|
+
throw new NoSolutionError(`root motif ${m} spans no time to place its notes in`, {
|
|
5712
|
+
at: frame.at
|
|
5713
|
+
});
|
|
5714
|
+
}
|
|
5715
|
+
notes = searchedLine(placeRhythm(scene, motif), frame);
|
|
5716
|
+
} else {
|
|
5717
|
+
const source = statementOf(scene, motif.from);
|
|
5718
|
+
notes = motif.relation === null ? varyStatement(source, motif.startBeat, frame) : placeDerived(
|
|
5719
|
+
source,
|
|
5720
|
+
motif.relation,
|
|
5721
|
+
motif.startBeat,
|
|
5722
|
+
motif.endBeat,
|
|
5723
|
+
scaleAt(scene, motif.startBeat),
|
|
5724
|
+
frame,
|
|
5725
|
+
lineBefore(scene, motif.phrase, motif.startBeat)
|
|
5726
|
+
);
|
|
5727
|
+
}
|
|
5728
|
+
scene.inProgress.delete(m);
|
|
5729
|
+
scene.statements.set(m, notes);
|
|
5730
|
+
return notes;
|
|
5731
|
+
}
|
|
5732
|
+
function freeNoteCount(scene, index, segment) {
|
|
5733
|
+
const phrase = scene.plan.phrases[index];
|
|
5734
|
+
const bars = barSpanOf(segment.startBeat, segment.endBeat, scene.plan.meters);
|
|
5735
|
+
return Math.max(1, Math.round(phrase.onsetDensity * bars));
|
|
5736
|
+
}
|
|
5737
|
+
function freeSpan(scene, index, segment) {
|
|
5738
|
+
const key = `${index}:${segment.startBeat}`;
|
|
5739
|
+
const known = scene.free.get(key);
|
|
5740
|
+
if (known !== void 0) {
|
|
5741
|
+
return known;
|
|
5742
|
+
}
|
|
5743
|
+
const rhythm = drawOnsets(
|
|
5744
|
+
scene,
|
|
5745
|
+
index,
|
|
5746
|
+
segment.startBeat,
|
|
5747
|
+
segment.endBeat,
|
|
5748
|
+
freeNoteCount(scene, index, segment)
|
|
5749
|
+
);
|
|
5750
|
+
const notes = searchedLine(rhythm, scene.frames[index]);
|
|
5751
|
+
scene.free.set(key, notes);
|
|
5752
|
+
return notes;
|
|
5753
|
+
}
|
|
5754
|
+
function lineBefore(scene, index, beat) {
|
|
5755
|
+
const line = [];
|
|
5756
|
+
for (const segment of segmentsOf(scene, index)) {
|
|
5757
|
+
if (segment.endBeat > beat + BEAT_EPS) {
|
|
5758
|
+
break;
|
|
5759
|
+
}
|
|
5760
|
+
if (segment.motif === null) {
|
|
5761
|
+
line.push(...freeSpan(scene, index, segment));
|
|
5762
|
+
} else if (!scene.inProgress.has(segment.motif)) {
|
|
5763
|
+
line.push(...statementOf(scene, segment.motif));
|
|
5764
|
+
}
|
|
5765
|
+
}
|
|
5766
|
+
return line.sort((a, b) => a.startBeat - b.startBeat);
|
|
5767
|
+
}
|
|
5768
|
+
function finalDownbeat(scene, index) {
|
|
5769
|
+
const phrase = scene.plan.phrases[index];
|
|
5770
|
+
const { meters } = scene.plan;
|
|
5771
|
+
let downbeat = barStartBeat(phrase.startBeat, meters);
|
|
5772
|
+
while (downbeat + beatsPerBarAt(downbeat, meters) < phrase.endBeat - BEAT_EPS) {
|
|
5773
|
+
downbeat += beatsPerBarAt(downbeat, meters);
|
|
5774
|
+
}
|
|
5775
|
+
return Math.max(downbeat, phrase.startBeat);
|
|
5776
|
+
}
|
|
5777
|
+
function heldNoteIndex(scene, index, line, downbeat) {
|
|
5778
|
+
const phrase = scene.plan.phrases[index];
|
|
5779
|
+
const sounding2 = line.findIndex(
|
|
5780
|
+
(note2) => note2.startBeat <= downbeat + BEAT_EPS && note2.startBeat + note2.durationBeat > downbeat + BEAT_EPS
|
|
5781
|
+
);
|
|
5782
|
+
return sounding2 >= 0 ? sounding2 : line.findIndex(
|
|
5783
|
+
(note2) => note2.startBeat >= downbeat - BEAT_EPS && note2.startBeat < phrase.endBeat - BEAT_EPS
|
|
5784
|
+
);
|
|
5785
|
+
}
|
|
5786
|
+
function repairCadence(scene, index, line, held) {
|
|
5787
|
+
const phrase = scene.plan.phrases[index];
|
|
5788
|
+
if (phrase.cadence === null) {
|
|
5789
|
+
return;
|
|
5790
|
+
}
|
|
5791
|
+
let target = held;
|
|
5792
|
+
for (let i = line.length - 1; target < 0 && i >= 0; i -= 1) {
|
|
5793
|
+
if (positionClass(line[i].startBeat, scene.plan.meters) !== "off") {
|
|
5794
|
+
target = i;
|
|
5795
|
+
}
|
|
5796
|
+
}
|
|
5797
|
+
if (target < 0) {
|
|
5798
|
+
return;
|
|
5799
|
+
}
|
|
5800
|
+
const note2 = line[target];
|
|
5801
|
+
const allowed = cadencePitchClasses(scene.plan, scene.timeline, phrase.cadence, note2.startBeat);
|
|
5802
|
+
if (allowed === null) {
|
|
5803
|
+
return;
|
|
5804
|
+
}
|
|
5805
|
+
const frame = scene.frames[index];
|
|
5806
|
+
const candidates = registerPitches(phrase).filter(
|
|
5807
|
+
(pitch) => allowed.includes(pitchClassOf(pitch))
|
|
5808
|
+
);
|
|
5809
|
+
if (candidates.length === 0) {
|
|
5810
|
+
throw new NoSolutionError(
|
|
5811
|
+
`no pitch in the register ${frame.low}..${frame.high} can end the ${phrase.cadence} cadence`,
|
|
5812
|
+
{ at: frame.at }
|
|
5813
|
+
);
|
|
5814
|
+
}
|
|
5815
|
+
if (allowed.includes(pitchClassOf(note2.pitch))) {
|
|
5816
|
+
return;
|
|
5817
|
+
}
|
|
5818
|
+
const slots = line.map(
|
|
5819
|
+
(n, i) => i === target ? { candidates, cost: frame.costAt(n.startBeat), fixed: false } : { candidates: [n.pitch], cost: () => 0, fixed: true }
|
|
5820
|
+
);
|
|
5821
|
+
const pitches = searchPitches(slots, frame.budget);
|
|
5822
|
+
if (pitches === null) {
|
|
5823
|
+
throw new NoSolutionError(`no cadence pitch joins its neighbours without an augmented step`, {
|
|
5824
|
+
at: frame.at
|
|
5825
|
+
});
|
|
5826
|
+
}
|
|
5827
|
+
note2.pitch = pitches[target];
|
|
5828
|
+
}
|
|
5829
|
+
function repickCutOnset(scene, index, line, held) {
|
|
5830
|
+
const note2 = line[held];
|
|
5831
|
+
const derived = scene.plan.motifs.some(
|
|
5832
|
+
(motif) => motif.from !== null && note2.startBeat >= motif.startBeat - BEAT_EPS && note2.startBeat < motif.endBeat - BEAT_EPS
|
|
5833
|
+
);
|
|
5834
|
+
if (scene.plan.phrases[index].cadence !== null || derived) {
|
|
5835
|
+
return;
|
|
5836
|
+
}
|
|
5837
|
+
const frame = scene.frames[index];
|
|
5838
|
+
const candidates = frame.candidatesAt(note2.startBeat);
|
|
5839
|
+
if (candidates.includes(note2.pitch)) {
|
|
5840
|
+
return;
|
|
5841
|
+
}
|
|
5842
|
+
const slots = line.map(
|
|
5843
|
+
(n, i) => i === held ? { candidates, cost: frame.costAt(n.startBeat), fixed: false } : { candidates: [n.pitch], cost: () => 0, fixed: true }
|
|
5844
|
+
);
|
|
5845
|
+
const pitches = searchPitches(slots, frame.budget);
|
|
5846
|
+
if (pitches === null) {
|
|
5847
|
+
throw new NoSolutionError(`no pitch at the final downbeat joins its neighbours`, {
|
|
5848
|
+
at: frame.at
|
|
5849
|
+
});
|
|
5850
|
+
}
|
|
5851
|
+
line[held] = { ...note2, pitch: pitches[held] };
|
|
5852
|
+
}
|
|
5853
|
+
function phraseLine(scene, index) {
|
|
5854
|
+
const line = [];
|
|
5855
|
+
for (const segment of segmentsOf(scene, index)) {
|
|
5856
|
+
line.push(
|
|
5857
|
+
...segment.motif === null ? freeSpan(scene, index, segment) : statementOf(scene, segment.motif)
|
|
5858
|
+
);
|
|
5859
|
+
}
|
|
5860
|
+
return line.sort((a, b) => a.startBeat - b.startBeat);
|
|
5861
|
+
}
|
|
5862
|
+
function chargePitchSearch(scene) {
|
|
5863
|
+
const { plan } = scene;
|
|
5864
|
+
let estimate = 0;
|
|
5865
|
+
plan.phrases.forEach((phrase, index) => {
|
|
5866
|
+
let searched = 0;
|
|
5867
|
+
for (const segment of segmentsOf(scene, index)) {
|
|
5868
|
+
const motif = segment.motif === null ? void 0 : plan.motifs[segment.motif];
|
|
5869
|
+
if (motif === void 0) {
|
|
5870
|
+
searched += freeNoteCount(scene, index, segment);
|
|
5871
|
+
} else if (motif.relation === null) {
|
|
5872
|
+
searched += motif.notes;
|
|
5873
|
+
} else {
|
|
5874
|
+
estimate += motif.notes * relationLevels(motif.relation.kind).length;
|
|
5875
|
+
}
|
|
5876
|
+
}
|
|
5877
|
+
const register = Math.max(0, phrase.register.high - phrase.register.low + 1);
|
|
5878
|
+
estimate += searched * SEARCH_STATES_SQUARED * register * register;
|
|
5879
|
+
});
|
|
5880
|
+
assertGenerationBudget(estimate, "melody pitch search", scene.budget);
|
|
5881
|
+
}
|
|
5882
|
+
function generateMelody(plan, opts) {
|
|
5883
|
+
assertCompositionPlan(plan);
|
|
5884
|
+
const asked = assertOptions(opts, "opts");
|
|
5885
|
+
if (asked.budget !== void 0) {
|
|
5886
|
+
assertPositiveInt(asked.budget, "budget", Number.MAX_SAFE_INTEGER);
|
|
5887
|
+
}
|
|
5888
|
+
const timeline = planChordTimeline(plan);
|
|
5889
|
+
const scene = {
|
|
5890
|
+
plan,
|
|
5891
|
+
ctx: melodyContext(plan, asked.ctx),
|
|
5892
|
+
budget: asked.budget,
|
|
5893
|
+
timeline,
|
|
5894
|
+
harmonyMisfits: planHarmonyMisfits(plan, timeline),
|
|
5895
|
+
scales: plan.keys.map((key) => scaleOf(key)),
|
|
5896
|
+
members: membersOf(plan),
|
|
5897
|
+
frames: [],
|
|
5898
|
+
statements: /* @__PURE__ */ new Map(),
|
|
5899
|
+
free: /* @__PURE__ */ new Map(),
|
|
5900
|
+
inProgress: /* @__PURE__ */ new Set()
|
|
5901
|
+
};
|
|
5902
|
+
scene.frames = plan.phrases.map((_, index) => frameOf(scene, index));
|
|
5903
|
+
chargePitchSearch(scene);
|
|
5904
|
+
const melody = [];
|
|
5905
|
+
plan.phrases.forEach((_, index) => {
|
|
5906
|
+
const line = phraseLine(scene, index);
|
|
5907
|
+
const downbeat = finalDownbeat(scene, index);
|
|
5908
|
+
let held = heldNoteIndex(scene, index, line, downbeat);
|
|
5909
|
+
if (held >= 0) {
|
|
5910
|
+
line.length = held + 1;
|
|
5911
|
+
const note2 = line[held];
|
|
5912
|
+
if (note2.startBeat < downbeat - BEAT_EPS) {
|
|
5913
|
+
const cut = downbeat - note2.startBeat;
|
|
5914
|
+
line[held] = { ...note2, durationBeat: cut };
|
|
5915
|
+
line.push({ ...note2, startBeat: downbeat, durationBeat: note2.durationBeat - cut });
|
|
5916
|
+
held += 1;
|
|
5917
|
+
repickCutOnset(scene, index, line, held);
|
|
5918
|
+
}
|
|
5919
|
+
}
|
|
5920
|
+
repairCadence(scene, index, line, held);
|
|
5921
|
+
if (held >= 0) {
|
|
5922
|
+
const note2 = line[held];
|
|
5923
|
+
const phraseEnd = plan.phrases[index].endBeat;
|
|
5924
|
+
line[held] = { ...note2, durationBeat: phraseEnd - note2.startBeat };
|
|
5925
|
+
}
|
|
5926
|
+
melody.push(...line);
|
|
5927
|
+
});
|
|
5928
|
+
melody.sort((a, b) => a.startBeat - b.startBeat);
|
|
5929
|
+
return motifToNoteEvents({ notes: melody });
|
|
5930
|
+
}
|
|
5931
|
+
|
|
4840
5932
|
// src/generate/ornament/index.ts
|
|
4841
5933
|
var ORNAMENT_STYLES = Object.freeze(["ghost", "flam", "drag", "slide", "accent"]);
|
|
4842
5934
|
var DEFAULT_TS5 = { numerator: 4, denominator: 4 };
|
|
@@ -4961,6 +6053,878 @@ function strokeGaps(ordered) {
|
|
|
4961
6053
|
return gaps;
|
|
4962
6054
|
}
|
|
4963
6055
|
|
|
6056
|
+
// src/generate/plan/derive.ts
|
|
6057
|
+
var PRESERVE_ASPECTS = [
|
|
6058
|
+
"form",
|
|
6059
|
+
"phraseLengths",
|
|
6060
|
+
"harmonicFunction",
|
|
6061
|
+
"harmonicRhythm",
|
|
6062
|
+
"motifRelations",
|
|
6063
|
+
"registerShape",
|
|
6064
|
+
"rhythm"
|
|
6065
|
+
];
|
|
6066
|
+
var NEUTRAL_SHAPE = "arch";
|
|
6067
|
+
var NEUTRAL_PEAK_POSITION = 0.6;
|
|
6068
|
+
var SHAPE_KEEP_THRESHOLD = 0.5;
|
|
6069
|
+
var NEUTRAL_ONSET_LEVELS = Object.freeze([0, 1, 2, 3, 4, 5].map((level) => level / 15));
|
|
6070
|
+
var FUNCTION_DEGREES = {
|
|
6071
|
+
tonic: [1, 6, 3],
|
|
6072
|
+
subdominant: [4, 2],
|
|
6073
|
+
dominant: [5, 7]
|
|
6074
|
+
};
|
|
6075
|
+
var STRONG_WEIGHT2 = 2;
|
|
6076
|
+
var HEPTATONIC_DEGREES = 7;
|
|
6077
|
+
function mod12(value) {
|
|
6078
|
+
return (value % 12 + 12) % 12;
|
|
6079
|
+
}
|
|
6080
|
+
function lerp(weight, reference, neutral) {
|
|
6081
|
+
return weight * reference + (1 - weight) * neutral;
|
|
6082
|
+
}
|
|
6083
|
+
function readPreserve(value) {
|
|
6084
|
+
const read = value === void 0 ? {} : assertRecord(value, "opts.preserve");
|
|
6085
|
+
const weights = {};
|
|
6086
|
+
for (const aspect of PRESERVE_ASPECTS) {
|
|
6087
|
+
const weight = read[aspect];
|
|
6088
|
+
weights[aspect] = weight === void 0 ? 1 : assertRange(weight, 0, 1, `opts.preserve.${aspect}`);
|
|
6089
|
+
}
|
|
6090
|
+
return weights;
|
|
6091
|
+
}
|
|
6092
|
+
function readRegister(value) {
|
|
6093
|
+
const read = assertRecord(value, "opts.register");
|
|
6094
|
+
const low = assertMidiPitch(read.low, "opts.register.low");
|
|
6095
|
+
const high = assertMidiPitch(read.high, "opts.register.high");
|
|
6096
|
+
if (low > high) {
|
|
6097
|
+
throw new InvalidInputError(
|
|
6098
|
+
`opts.register.low must not exceed opts.register.high; received ${low} > ${high}`
|
|
6099
|
+
);
|
|
6100
|
+
}
|
|
6101
|
+
return { low, high };
|
|
6102
|
+
}
|
|
6103
|
+
function rejectSuppliedRng(ctx) {
|
|
6104
|
+
if (typeof ctx === "object" && ctx !== null && ctx.rng !== void 0) {
|
|
6105
|
+
throw new InvalidInputError(
|
|
6106
|
+
"opts.ctx.rng is not accepted: a composition plan records its seed, not a source"
|
|
6107
|
+
);
|
|
6108
|
+
}
|
|
6109
|
+
}
|
|
6110
|
+
function plannedKeys(profile, requested) {
|
|
6111
|
+
const regions = profile.harmony.keys.map(
|
|
6112
|
+
({ key }) => ({
|
|
6113
|
+
scale: { ...key.scale },
|
|
6114
|
+
tonic: { ...key.tonic },
|
|
6115
|
+
variant: key.variant
|
|
6116
|
+
})
|
|
6117
|
+
);
|
|
6118
|
+
const home = regions[0];
|
|
6119
|
+
if (requested === void 0) {
|
|
6120
|
+
return regions;
|
|
6121
|
+
}
|
|
6122
|
+
const target = resolveKey(requested);
|
|
6123
|
+
if (target.variant !== home.variant || target.scale.modeMask12 !== home.scale.modeMask12) {
|
|
6124
|
+
throw new InvalidInputError(
|
|
6125
|
+
"opts.key must be in the same mode as the reference's home key; a key change moves the tonic only"
|
|
6126
|
+
);
|
|
6127
|
+
}
|
|
6128
|
+
const step = spelledInterval(
|
|
6129
|
+
{ letter: home.tonic.letter, alter: home.tonic.alter },
|
|
6130
|
+
{ letter: target.tonic.letter, alter: target.tonic.alter }
|
|
6131
|
+
);
|
|
6132
|
+
const moved = regions.map(
|
|
6133
|
+
(key) => ({
|
|
6134
|
+
scale: {
|
|
6135
|
+
rootPc: mod12(key.scale.rootPc + step.semitones),
|
|
6136
|
+
modeMask12: key.scale.modeMask12
|
|
6137
|
+
},
|
|
6138
|
+
tonic: transposeByInterval({ letter: key.tonic.letter, alter: key.tonic.alter }, step),
|
|
6139
|
+
variant: key.variant
|
|
6140
|
+
})
|
|
6141
|
+
);
|
|
6142
|
+
moved[0] = target;
|
|
6143
|
+
return moved;
|
|
6144
|
+
}
|
|
6145
|
+
function nearestBoundary(beat, boundaries) {
|
|
6146
|
+
let best = beat;
|
|
6147
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
6148
|
+
for (const boundary of boundaries) {
|
|
6149
|
+
const distance = Math.abs(boundary - beat);
|
|
6150
|
+
if (distance < bestDistance - BEAT_EPS) {
|
|
6151
|
+
best = boundary;
|
|
6152
|
+
bestDistance = distance;
|
|
6153
|
+
}
|
|
6154
|
+
}
|
|
6155
|
+
return best;
|
|
6156
|
+
}
|
|
6157
|
+
function plannedSections(profile, weight, draw) {
|
|
6158
|
+
const sections = profile.form.sections;
|
|
6159
|
+
const boundaries = [profile.span.startBeat, profile.span.endBeat];
|
|
6160
|
+
for (const phrase of profile.form.phrases) {
|
|
6161
|
+
boundaries.push(phrase.startBeat, phrase.endBeat);
|
|
6162
|
+
}
|
|
6163
|
+
boundaries.sort((a, b) => a - b);
|
|
6164
|
+
const planned = [];
|
|
6165
|
+
sections.forEach((section, index) => {
|
|
6166
|
+
const keep = sections.length < 2 || draw.prob(weight, "form", index);
|
|
6167
|
+
const neighbour = index + 1 < sections.length ? index + 1 : index - 1;
|
|
6168
|
+
const startBeat = nearestBoundary(section.startBeat, boundaries);
|
|
6169
|
+
const endBeat = nearestBoundary(section.endBeat, boundaries);
|
|
6170
|
+
if (endBeat - startBeat > BEAT_EPS) {
|
|
6171
|
+
planned.push({
|
|
6172
|
+
label: keep ? section.label : sections[neighbour]?.label ?? section.label,
|
|
6173
|
+
startBeat,
|
|
6174
|
+
endBeat
|
|
6175
|
+
});
|
|
6176
|
+
}
|
|
6177
|
+
});
|
|
6178
|
+
return planned;
|
|
6179
|
+
}
|
|
6180
|
+
function beatAfterBars(startBeat, bars, meters) {
|
|
6181
|
+
let beat = startBeat;
|
|
6182
|
+
let left = bars;
|
|
6183
|
+
while (left > BEAT_EPS) {
|
|
6184
|
+
const step = Math.min(1, left);
|
|
6185
|
+
beat += step * beatsPerBarAt(beat, meters);
|
|
6186
|
+
left -= step;
|
|
6187
|
+
}
|
|
6188
|
+
return beat;
|
|
6189
|
+
}
|
|
6190
|
+
function barsBetween(startBeat, endBeat, meters) {
|
|
6191
|
+
let beat = startBeat;
|
|
6192
|
+
let bars = 0;
|
|
6193
|
+
while (beat < endBeat - BEAT_EPS) {
|
|
6194
|
+
const barBeats = beatsPerBarAt(beat, meters);
|
|
6195
|
+
const step = Math.min(barStartBeat(beat, meters) + barBeats, endBeat) - beat;
|
|
6196
|
+
bars += step / barBeats;
|
|
6197
|
+
beat += step;
|
|
6198
|
+
}
|
|
6199
|
+
return bars;
|
|
6200
|
+
}
|
|
6201
|
+
function spanSegments(profile, sections) {
|
|
6202
|
+
const { startBeat, endBeat } = profile.span;
|
|
6203
|
+
const cuts = [startBeat, endBeat];
|
|
6204
|
+
for (const section of sections) {
|
|
6205
|
+
for (const beat of [section.startBeat, section.endBeat]) {
|
|
6206
|
+
if (beat > startBeat + BEAT_EPS && beat < endBeat - BEAT_EPS) cuts.push(beat);
|
|
6207
|
+
}
|
|
6208
|
+
}
|
|
6209
|
+
cuts.sort((a, b) => a - b);
|
|
6210
|
+
const segments = [];
|
|
6211
|
+
for (let index = 1; index < cuts.length; index += 1) {
|
|
6212
|
+
const from = cuts[index - 1];
|
|
6213
|
+
const to = cuts[index];
|
|
6214
|
+
if (to - from > BEAT_EPS) segments.push([from, to]);
|
|
6215
|
+
}
|
|
6216
|
+
return segments;
|
|
6217
|
+
}
|
|
6218
|
+
function plannedPhraseSpans(profile, sections, weight, draw) {
|
|
6219
|
+
const segments = spanSegments(profile, sections);
|
|
6220
|
+
const pieces = profile.form.phrases.map((phrase, source) => ({
|
|
6221
|
+
startBeat: phrase.startBeat,
|
|
6222
|
+
endBeat: phrase.endBeat,
|
|
6223
|
+
source,
|
|
6224
|
+
cadence: phrase.cadence?.type ?? null
|
|
6225
|
+
}));
|
|
6226
|
+
const replaced = pieces.map((_, index) => !draw.prob(weight, "phraseLengths", index));
|
|
6227
|
+
const spans = [];
|
|
6228
|
+
for (const [segmentStart, segmentEnd] of segments) {
|
|
6229
|
+
const members = pieces.map((piece, index) => ({ piece, index })).filter(
|
|
6230
|
+
({ piece }) => piece.startBeat >= segmentStart - BEAT_EPS && piece.startBeat < segmentEnd - BEAT_EPS
|
|
6231
|
+
);
|
|
6232
|
+
const laid = [];
|
|
6233
|
+
let cursor = segmentStart;
|
|
6234
|
+
for (const { piece, index } of members) {
|
|
6235
|
+
const lengths = [];
|
|
6236
|
+
if (replaced[index]) {
|
|
6237
|
+
const bars = barsBetween(piece.startBeat, piece.endBeat, profile.meters);
|
|
6238
|
+
const rounded = 2 ** Math.round(Math.log2(bars));
|
|
6239
|
+
const overflows = beatAfterBars(cursor, rounded, profile.meters) > segmentEnd + BEAT_EPS;
|
|
6240
|
+
let partStart = cursor;
|
|
6241
|
+
for (const part of overflows ? [rounded / 2, rounded / 2] : [rounded]) {
|
|
6242
|
+
const partEnd = beatAfterBars(partStart, part, profile.meters);
|
|
6243
|
+
lengths.push(partEnd - partStart);
|
|
6244
|
+
partStart = partEnd;
|
|
6245
|
+
}
|
|
6246
|
+
} else {
|
|
6247
|
+
lengths.push(piece.endBeat - piece.startBeat);
|
|
6248
|
+
}
|
|
6249
|
+
lengths.forEach((length, part) => {
|
|
6250
|
+
if (cursor >= segmentEnd - BEAT_EPS) return;
|
|
6251
|
+
const end = cursor + length;
|
|
6252
|
+
laid.push({
|
|
6253
|
+
startBeat: cursor,
|
|
6254
|
+
endBeat: Math.min(end, segmentEnd),
|
|
6255
|
+
source: piece.source,
|
|
6256
|
+
cadence: part === lengths.length - 1 ? piece.cadence : null
|
|
6257
|
+
});
|
|
6258
|
+
cursor = end;
|
|
6259
|
+
});
|
|
6260
|
+
}
|
|
6261
|
+
const last = laid.at(-1);
|
|
6262
|
+
if (last) {
|
|
6263
|
+
last.endBeat = segmentEnd;
|
|
6264
|
+
} else {
|
|
6265
|
+
laid.push({
|
|
6266
|
+
startBeat: segmentStart,
|
|
6267
|
+
endBeat: segmentEnd,
|
|
6268
|
+
source: spans.at(-1)?.source ?? (profile.form.phrases.length > 0 ? 0 : -1),
|
|
6269
|
+
cadence: null
|
|
6270
|
+
});
|
|
6271
|
+
}
|
|
6272
|
+
spans.push(...laid);
|
|
6273
|
+
}
|
|
6274
|
+
return spans;
|
|
6275
|
+
}
|
|
6276
|
+
function sectionAt(sections, beat) {
|
|
6277
|
+
const index = sections.findIndex(
|
|
6278
|
+
(section) => beat >= section.startBeat - BEAT_EPS && beat < section.endBeat - BEAT_EPS
|
|
6279
|
+
);
|
|
6280
|
+
return index < 0 ? null : index;
|
|
6281
|
+
}
|
|
6282
|
+
function phraseHolding(phrases, startBeat, endBeat) {
|
|
6283
|
+
return phrases.findIndex(
|
|
6284
|
+
(phrase) => startBeat >= phrase.startBeat - BEAT_EPS && endBeat <= phrase.endBeat + BEAT_EPS
|
|
6285
|
+
);
|
|
6286
|
+
}
|
|
6287
|
+
function better(a, b) {
|
|
6288
|
+
for (let field = 0; field < a.length; field += 1) {
|
|
6289
|
+
if (a[field] !== b[field]) return a[field] > b[field];
|
|
6290
|
+
}
|
|
6291
|
+
return false;
|
|
6292
|
+
}
|
|
6293
|
+
function scheduleMotifs(candidates, meters) {
|
|
6294
|
+
const order = [...candidates].sort((a, b) => a.startBeat - b.startBeat || a.node - b.node);
|
|
6295
|
+
const count = order.length;
|
|
6296
|
+
const firstFrom = (from, beat) => {
|
|
6297
|
+
let low = from;
|
|
6298
|
+
let high = count;
|
|
6299
|
+
while (low < high) {
|
|
6300
|
+
const mid = low + high >> 1;
|
|
6301
|
+
if (order[mid].startBeat < beat - BEAT_EPS) low = mid + 1;
|
|
6302
|
+
else high = mid;
|
|
6303
|
+
}
|
|
6304
|
+
return low;
|
|
6305
|
+
};
|
|
6306
|
+
const next = order.map((candidate, index2) => firstFrom(index2 + 1, candidate.endBeat));
|
|
6307
|
+
const own = order.map(
|
|
6308
|
+
(candidate) => [
|
|
6309
|
+
candidate.notes,
|
|
6310
|
+
metricWeight(candidate.startBeat, meters) < STRONG_WEIGHT2 ? -1 : 0,
|
|
6311
|
+
1
|
|
6312
|
+
]
|
|
6313
|
+
);
|
|
6314
|
+
const taking = (index2) => {
|
|
6315
|
+
const rest = best[next[index2]];
|
|
6316
|
+
const mine = own[index2];
|
|
6317
|
+
return [mine[0] + rest[0], mine[1] + rest[1], mine[2] + rest[2]];
|
|
6318
|
+
};
|
|
6319
|
+
const best = new Array(count + 1).fill([0, 0, 0]);
|
|
6320
|
+
for (let index2 = count - 1; index2 >= 0; index2 -= 1) {
|
|
6321
|
+
const taken = taking(index2);
|
|
6322
|
+
const skipped = best[index2 + 1];
|
|
6323
|
+
best[index2] = better(taken, skipped) ? taken : skipped;
|
|
6324
|
+
}
|
|
6325
|
+
const chosen = [];
|
|
6326
|
+
let index = 0;
|
|
6327
|
+
while (index < count) {
|
|
6328
|
+
if (!better(best[index], taking(index))) {
|
|
6329
|
+
chosen.push(order[index]);
|
|
6330
|
+
index = next[index];
|
|
6331
|
+
} else {
|
|
6332
|
+
index += 1;
|
|
6333
|
+
}
|
|
6334
|
+
}
|
|
6335
|
+
return chosen.sort((a, b) => a.node - b.node);
|
|
6336
|
+
}
|
|
6337
|
+
function selectedDerivation(graph, parentEdge, selectedAt, node) {
|
|
6338
|
+
let plain = true;
|
|
6339
|
+
let semitones = 0;
|
|
6340
|
+
let steps = 0;
|
|
6341
|
+
for (let edge = parentEdge.get(node); edge !== void 0; edge = parentEdge.get(edge.from)) {
|
|
6342
|
+
const relation = edge.relation;
|
|
6343
|
+
steps += 1;
|
|
6344
|
+
plain &&= relation !== null && (relation.kind === "repetition" || relation.kind === "transposition" && Math.abs(relation.timeRatio - 1) <= BEAT_EPS);
|
|
6345
|
+
semitones += relation?.semitones ?? 0;
|
|
6346
|
+
const from = selectedAt.get(edge.from);
|
|
6347
|
+
if (from === void 0) continue;
|
|
6348
|
+
if (steps === 1) {
|
|
6349
|
+
return { from, relation: relation === null ? null : { ...relation } };
|
|
6350
|
+
}
|
|
6351
|
+
if (!plain) {
|
|
6352
|
+
return { from: null, relation: null };
|
|
6353
|
+
}
|
|
6354
|
+
const gap = (graph.nodes[node]?.startBeat ?? 0) - (graph.nodes[edge.from]?.endBeat ?? 0);
|
|
6355
|
+
return {
|
|
6356
|
+
from,
|
|
6357
|
+
relation: {
|
|
6358
|
+
kind: semitones === 0 ? "repetition" : "transposition",
|
|
6359
|
+
sequence: gap >= -HUMANIZE_ADJACENCY && gap <= HUMANIZE_ADJACENCY,
|
|
6360
|
+
semitones,
|
|
6361
|
+
timeRatio: 1
|
|
6362
|
+
}
|
|
6363
|
+
};
|
|
6364
|
+
}
|
|
6365
|
+
return { from: null, relation: null };
|
|
6366
|
+
}
|
|
6367
|
+
function functionCandidates(fn, key) {
|
|
6368
|
+
if (scaleTonesInDegreeOrder(key).length !== HEPTATONIC_DEGREES) {
|
|
6369
|
+
return null;
|
|
6370
|
+
}
|
|
6371
|
+
return FUNCTION_DEGREES[fn].map((degree) => chordToRoman(diatonicTriad(degree, key), key));
|
|
6372
|
+
}
|
|
6373
|
+
function plannedHarmony(profile, keys, preserve, draw) {
|
|
6374
|
+
const chords = profile.harmony.chords;
|
|
6375
|
+
const romans = chords.map((chord, index) => {
|
|
6376
|
+
if (chord.level !== "structural" || draw.prob(preserve.harmonicFunction, "harmonicFunction", index)) {
|
|
6377
|
+
return chord.roman;
|
|
6378
|
+
}
|
|
6379
|
+
const candidates = functionCandidates(chord.function, keys[chord.key]);
|
|
6380
|
+
if (candidates === null) {
|
|
6381
|
+
return chord.roman;
|
|
6382
|
+
}
|
|
6383
|
+
const pick = draw.range(0, candidates.length - 1, "harmonicFunction", index, "pick");
|
|
6384
|
+
return candidates[pick];
|
|
6385
|
+
});
|
|
6386
|
+
const starts = [];
|
|
6387
|
+
chords.forEach((chord, index) => {
|
|
6388
|
+
const keep = index === 0 || draw.prob(preserve.harmonicRhythm, "harmonicRhythm", index);
|
|
6389
|
+
const start = keep ? chord.startBeat : barStartBeat(chord.startBeat, profile.meters);
|
|
6390
|
+
starts.push(index === 0 ? start : Math.max(start, starts[index - 1]));
|
|
6391
|
+
});
|
|
6392
|
+
const planned = [];
|
|
6393
|
+
chords.forEach((chord, index) => {
|
|
6394
|
+
const next = chords[index + 1];
|
|
6395
|
+
const nextStart = starts[index + 1];
|
|
6396
|
+
let endBeat = chord.endBeat;
|
|
6397
|
+
if (next !== void 0 && nextStart !== void 0) {
|
|
6398
|
+
endBeat = Math.abs(chord.endBeat - next.startBeat) <= BEAT_EPS ? nextStart : Math.min(chord.endBeat, nextStart);
|
|
6399
|
+
}
|
|
6400
|
+
const startBeat = starts[index];
|
|
6401
|
+
if (endBeat - startBeat > BEAT_EPS) {
|
|
6402
|
+
planned.push({ startBeat, endBeat, key: chord.key, roman: romans[index] });
|
|
6403
|
+
}
|
|
6404
|
+
});
|
|
6405
|
+
return planned;
|
|
6406
|
+
}
|
|
6407
|
+
function plannedRhythm(profile, weight) {
|
|
6408
|
+
const reference = profile.melody.rhythm;
|
|
6409
|
+
return {
|
|
6410
|
+
onsetLevels: reference.onsetLevels.map(
|
|
6411
|
+
(share, level) => lerp(weight, share, NEUTRAL_ONSET_LEVELS[level])
|
|
6412
|
+
),
|
|
6413
|
+
interOnsetShares: [...reference.interOnsetShares],
|
|
6414
|
+
syncopation: lerp(weight, reference.syncopation, 0)
|
|
6415
|
+
};
|
|
6416
|
+
}
|
|
6417
|
+
function deriveCompositionPlan(reference, opts) {
|
|
6418
|
+
const profile = assertReferenceProfile(reference, "reference");
|
|
6419
|
+
const asked = assertOptions(opts, "opts");
|
|
6420
|
+
const preserve = readPreserve(asked.preserve);
|
|
6421
|
+
const requestedRegister = asked.register === void 0 ? void 0 : readRegister(asked.register);
|
|
6422
|
+
rejectSuppliedRng(asked.ctx);
|
|
6423
|
+
const ctx = resolveContext(asked.ctx);
|
|
6424
|
+
const draw = ctx.part("plan");
|
|
6425
|
+
const melodyRegister = profile.melody.register;
|
|
6426
|
+
if (profile.melody.rhythm.onsets === 0 || melodyRegister === null) {
|
|
6427
|
+
throw new InvalidInputError("reference must carry melody onsets to derive a plan from");
|
|
6428
|
+
}
|
|
6429
|
+
const keys = plannedKeys(profile, asked.key);
|
|
6430
|
+
const homeShift = mod12(
|
|
6431
|
+
keys[0].scale.rootPc - (profile.harmony.keys[0]?.key.scale.rootPc ?? 0)
|
|
6432
|
+
);
|
|
6433
|
+
const shift = homeShift >= 6 ? homeShift - 12 : homeShift;
|
|
6434
|
+
const source = { low: melodyRegister.low + shift, high: melodyRegister.high + shift };
|
|
6435
|
+
if (source.low < 0 || source.high > 127) {
|
|
6436
|
+
throw new InvalidInputError(
|
|
6437
|
+
`the reference melody range moved to the new key leaves the MIDI range; received ${source.low}..${source.high}`
|
|
6438
|
+
);
|
|
6439
|
+
}
|
|
6440
|
+
const target = requestedRegister ?? source;
|
|
6441
|
+
const scale = source.high === source.low ? 0 : (target.high - target.low) / (source.high - source.low);
|
|
6442
|
+
const offset = source.high === source.low ? (target.low + target.high) / 2 : target.low - source.low * scale;
|
|
6443
|
+
const mapPitch = (pitch) => (pitch + shift) * scale + offset;
|
|
6444
|
+
const neutralRegister = {
|
|
6445
|
+
low: target.low,
|
|
6446
|
+
high: target.high,
|
|
6447
|
+
mean: (target.low + target.high) / 2
|
|
6448
|
+
};
|
|
6449
|
+
const sections = plannedSections(profile, preserve.form, draw);
|
|
6450
|
+
const shapeWeight = preserve.registerShape;
|
|
6451
|
+
const spans = plannedPhraseSpans(profile, sections, preserve.phraseLengths, draw);
|
|
6452
|
+
const phrases = spans.map((span) => {
|
|
6453
|
+
const melody = profile.form.phrases[span.source]?.melody ?? null;
|
|
6454
|
+
const register = melody ? {
|
|
6455
|
+
low: Math.round(lerp(shapeWeight, mapPitch(melody.low), neutralRegister.low)),
|
|
6456
|
+
high: Math.round(lerp(shapeWeight, mapPitch(melody.high), neutralRegister.high)),
|
|
6457
|
+
mean: lerp(shapeWeight, mapPitch(melody.mean), neutralRegister.mean)
|
|
6458
|
+
} : { ...neutralRegister };
|
|
6459
|
+
return {
|
|
6460
|
+
startBeat: span.startBeat,
|
|
6461
|
+
endBeat: span.endBeat,
|
|
6462
|
+
section: sectionAt(sections, span.startBeat),
|
|
6463
|
+
cadence: span.cadence,
|
|
6464
|
+
shape: melody && shapeWeight >= SHAPE_KEEP_THRESHOLD ? melody.shape : NEUTRAL_SHAPE,
|
|
6465
|
+
peakPosition: melody ? lerp(shapeWeight, melody.peakPosition, NEUTRAL_PEAK_POSITION) : NEUTRAL_PEAK_POSITION,
|
|
6466
|
+
register,
|
|
6467
|
+
onsetDensity: melody ? melody.onsetDensity : profile.melody.rhythm.onsetDensity,
|
|
6468
|
+
motifs: []
|
|
6469
|
+
};
|
|
6470
|
+
});
|
|
6471
|
+
const graph = profile.melody.graph;
|
|
6472
|
+
const noteCount = (node) => (profile.melody.motifs[graph.nodes[node]?.motif ?? -1]?.intervals.length ?? 0) + 1;
|
|
6473
|
+
assertGenerationBudget(graph.nodes.length * phrases.length, "plan motif selection", asked.budget);
|
|
6474
|
+
const candidates = [];
|
|
6475
|
+
graph.nodes.forEach((node, index) => {
|
|
6476
|
+
const phrase = phraseHolding(phrases, node.startBeat, node.endBeat);
|
|
6477
|
+
if (phrase >= 0) {
|
|
6478
|
+
candidates.push({
|
|
6479
|
+
node: index,
|
|
6480
|
+
phrase,
|
|
6481
|
+
startBeat: node.startBeat,
|
|
6482
|
+
endBeat: node.endBeat,
|
|
6483
|
+
notes: noteCount(index)
|
|
6484
|
+
});
|
|
6485
|
+
}
|
|
6486
|
+
});
|
|
6487
|
+
const selected = scheduleMotifs(candidates, profile.meters);
|
|
6488
|
+
const selectedAt = new Map(selected.map((candidate, index) => [candidate.node, index]));
|
|
6489
|
+
const parentEdge = new Map(graph.edges.map((edge) => [edge.to, edge]));
|
|
6490
|
+
const motifs = selected.map((candidate, index) => {
|
|
6491
|
+
const { from, relation } = selectedDerivation(graph, parentEdge, selectedAt, candidate.node);
|
|
6492
|
+
const keepRelation = draw.prob(preserve.motifRelations, "motifRelations", index);
|
|
6493
|
+
const keepRhythm = from === null && draw.prob(preserve.rhythm, "rhythm", index);
|
|
6494
|
+
const cell = profile.melody.motifs[graph.nodes[candidate.node]?.motif ?? -1];
|
|
6495
|
+
phrases[candidate.phrase]?.motifs.push(index);
|
|
6496
|
+
return {
|
|
6497
|
+
phrase: candidate.phrase,
|
|
6498
|
+
startBeat: candidate.startBeat,
|
|
6499
|
+
endBeat: candidate.endBeat,
|
|
6500
|
+
notes: candidate.notes,
|
|
6501
|
+
rhythm: keepRhythm && cell !== void 0 ? cell.rhythm.map((ratio) => ratio * cell.unitBeats) : null,
|
|
6502
|
+
from,
|
|
6503
|
+
relation: keepRelation ? relation : null
|
|
6504
|
+
};
|
|
6505
|
+
});
|
|
6506
|
+
return {
|
|
6507
|
+
planVersion: COMPOSITION_PLAN_VERSION,
|
|
6508
|
+
seed: ctx.seed,
|
|
6509
|
+
algorithmVersion: ctx.algorithmVersion,
|
|
6510
|
+
keys,
|
|
6511
|
+
meters: profile.meters.map((entry) => ({
|
|
6512
|
+
startBeat: entry.startBeat,
|
|
6513
|
+
ts: entry.ts.grouping === void 0 ? { ...entry.ts } : { ...entry.ts, grouping: [...entry.ts.grouping] }
|
|
6514
|
+
})),
|
|
6515
|
+
span: { ...profile.span },
|
|
6516
|
+
sections,
|
|
6517
|
+
phrases,
|
|
6518
|
+
harmony: plannedHarmony(profile, keys, preserve, draw),
|
|
6519
|
+
motifs,
|
|
6520
|
+
rhythm: plannedRhythm(profile, preserve.rhythm)
|
|
6521
|
+
};
|
|
6522
|
+
}
|
|
6523
|
+
|
|
6524
|
+
// src/generate/plan/evaluate.ts
|
|
6525
|
+
var PEAK_POSITION_TOLERANCE = 0.2;
|
|
6526
|
+
var OUTLINE_POINTS = 8;
|
|
6527
|
+
var HELD_NOTE_BEATS = 1;
|
|
6528
|
+
function interiorBoundaries(plan) {
|
|
6529
|
+
const beats = /* @__PURE__ */ new Set();
|
|
6530
|
+
for (const phrase of plan.phrases) {
|
|
6531
|
+
for (const beat of [phrase.startBeat, phrase.endBeat]) {
|
|
6532
|
+
if (beat > plan.span.startBeat + BEAT_EPS && beat < plan.span.endBeat - BEAT_EPS) {
|
|
6533
|
+
beats.add(beat);
|
|
6534
|
+
}
|
|
6535
|
+
}
|
|
6536
|
+
}
|
|
6537
|
+
return [...beats].sort((a, b) => a - b);
|
|
6538
|
+
}
|
|
6539
|
+
function restsOf(melody, plan) {
|
|
6540
|
+
const rests = [];
|
|
6541
|
+
let reach = plan.span.startBeat;
|
|
6542
|
+
for (const note2 of [...melody].sort((a, b) => a.startBeat - b.startBeat)) {
|
|
6543
|
+
if (note2.startBeat > reach + BEAT_EPS) {
|
|
6544
|
+
rests.push({ startBeat: reach, endBeat: note2.startBeat });
|
|
6545
|
+
}
|
|
6546
|
+
reach = Math.max(reach, note2.startBeat + note2.durationBeat);
|
|
6547
|
+
}
|
|
6548
|
+
if (plan.span.endBeat > reach + BEAT_EPS) {
|
|
6549
|
+
rests.push({ startBeat: reach, endBeat: plan.span.endBeat });
|
|
6550
|
+
}
|
|
6551
|
+
return rests;
|
|
6552
|
+
}
|
|
6553
|
+
function phraseBoundaryViolations(melody, plan) {
|
|
6554
|
+
const rests = restsOf(melody, plan);
|
|
6555
|
+
const violations = [];
|
|
6556
|
+
for (const beat of interiorBoundaries(plan)) {
|
|
6557
|
+
const pulse2 = pulseBeats(meterAt(beat, plan.meters));
|
|
6558
|
+
const held = melody.some(
|
|
6559
|
+
(note2) => note2.durationBeat >= HELD_NOTE_BEATS - BEAT_EPS && Math.abs(note2.startBeat + note2.durationBeat - beat) <= pulse2 + BEAT_EPS
|
|
6560
|
+
);
|
|
6561
|
+
const rested = rests.some(
|
|
6562
|
+
(rest) => rest.endBeat - rest.startBeat >= pulse2 - BEAT_EPS && rest.startBeat <= beat + pulse2 + BEAT_EPS && rest.endBeat >= beat - pulse2 - BEAT_EPS
|
|
6563
|
+
);
|
|
6564
|
+
if (!held && !rested) {
|
|
6565
|
+
violations.push({
|
|
6566
|
+
kind: "phraseBoundary",
|
|
6567
|
+
severity: "error",
|
|
6568
|
+
atBeat: beat,
|
|
6569
|
+
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)`,
|
|
6570
|
+
actual: "neither",
|
|
6571
|
+
rationale: `the plan places a phrase boundary at beat ${beat}, which the candidate does not mark`
|
|
6572
|
+
});
|
|
6573
|
+
}
|
|
6574
|
+
}
|
|
6575
|
+
return violations;
|
|
6576
|
+
}
|
|
6577
|
+
function spanViolation(melody, plan) {
|
|
6578
|
+
const end = melody.reduce((max, note2) => Math.max(max, note2.startBeat + note2.durationBeat), 0);
|
|
6579
|
+
if (end <= plan.span.endBeat + BEAT_EPS) {
|
|
6580
|
+
return [];
|
|
6581
|
+
}
|
|
6582
|
+
return [
|
|
6583
|
+
{
|
|
6584
|
+
kind: "span",
|
|
6585
|
+
severity: "error",
|
|
6586
|
+
atBeat: plan.span.endBeat,
|
|
6587
|
+
expected: `ending by beat ${plan.span.endBeat}`,
|
|
6588
|
+
actual: `ending at beat ${end}`,
|
|
6589
|
+
rationale: "the candidate's last note runs past the plan's span"
|
|
6590
|
+
}
|
|
6591
|
+
];
|
|
6592
|
+
}
|
|
6593
|
+
function phraseLines(melody, plan) {
|
|
6594
|
+
return plan.phrases.map(
|
|
6595
|
+
(phrase) => melody.flatMap(
|
|
6596
|
+
(note2, index) => note2.startBeat >= phrase.startBeat - BEAT_EPS && note2.startBeat < phrase.endBeat - BEAT_EPS ? [index] : []
|
|
6597
|
+
)
|
|
6598
|
+
);
|
|
6599
|
+
}
|
|
6600
|
+
function cadenceViolations(melody, plan, timeline, lines) {
|
|
6601
|
+
const violations = [];
|
|
6602
|
+
plan.phrases.forEach((phrase, index) => {
|
|
6603
|
+
if (phrase.cadence === null) {
|
|
6604
|
+
return;
|
|
6605
|
+
}
|
|
6606
|
+
const last = lines[index]?.at(-1);
|
|
6607
|
+
const note2 = last === void 0 ? void 0 : melody[last];
|
|
6608
|
+
const allowed = note2 === void 0 ? [] : cadencePitchClasses(plan, timeline, phrase.cadence, note2.startBeat);
|
|
6609
|
+
if (note2 !== void 0 && (allowed === null || allowed.includes(pitchClassOf(note2.pitch)))) {
|
|
6610
|
+
return;
|
|
6611
|
+
}
|
|
6612
|
+
violations.push({
|
|
6613
|
+
kind: "cadence",
|
|
6614
|
+
severity: "error",
|
|
6615
|
+
atBeat: phrase.endBeat,
|
|
6616
|
+
expected: `a closing note on pitch class ${(allowed ?? []).join(" or ")} for a ${phrase.cadence} cadence`,
|
|
6617
|
+
actual: note2 === void 0 ? "no note" : `pitch ${note2.pitch}`,
|
|
6618
|
+
rationale: `the phrase ending at beat ${phrase.endBeat} is planned to close on a ${phrase.cadence} cadence`
|
|
6619
|
+
});
|
|
6620
|
+
});
|
|
6621
|
+
return violations;
|
|
6622
|
+
}
|
|
6623
|
+
function harmonyViolations(melody, plan, timeline, lines) {
|
|
6624
|
+
const misfits = planHarmonyMisfits(plan, timeline);
|
|
6625
|
+
const violations = [];
|
|
6626
|
+
for (const line of lines) {
|
|
6627
|
+
const read = misfits(line.map((index) => melody[index]));
|
|
6628
|
+
read.forEach((kinds, at2) => {
|
|
6629
|
+
if (kinds === null) {
|
|
6630
|
+
return;
|
|
6631
|
+
}
|
|
6632
|
+
const note2 = melody[line[at2]];
|
|
6633
|
+
const derived = plan.motifs.some(
|
|
6634
|
+
(motif) => motif.from !== null && note2.startBeat >= motif.startBeat - BEAT_EPS && note2.startBeat < motif.endBeat - BEAT_EPS
|
|
6635
|
+
);
|
|
6636
|
+
const strong = positionClass(note2.startBeat, plan.meters) === "strong";
|
|
6637
|
+
violations.push({
|
|
6638
|
+
kind: "harmony",
|
|
6639
|
+
severity: derived ? "warning" : "error",
|
|
6640
|
+
atBeat: note2.startBeat,
|
|
6641
|
+
expected: strong ? "a chord tone or an ornamental tone" : "a chord tone, an ornamental tone, or a scale tone",
|
|
6642
|
+
actual: kinds.length === 0 ? "no harmonic label" : kinds.join(", "),
|
|
6643
|
+
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`
|
|
6644
|
+
});
|
|
6645
|
+
});
|
|
6646
|
+
}
|
|
6647
|
+
return violations;
|
|
6648
|
+
}
|
|
6649
|
+
function notesInRange(melody, startBeat, endBeat) {
|
|
6650
|
+
return melody.filter(
|
|
6651
|
+
(note2) => note2.startBeat >= startBeat - BEAT_EPS && note2.startBeat < endBeat - BEAT_EPS
|
|
6652
|
+
);
|
|
6653
|
+
}
|
|
6654
|
+
function replayLevel(source, derived, statement, sourceEnd, plan, key, closing) {
|
|
6655
|
+
const relation = statement.relation;
|
|
6656
|
+
const planned = relation.kind === "tonalTransposition" ? relation.degrees ?? 0 : relation.semitones;
|
|
6657
|
+
const { low, high } = plan.phrases[statement.phrase].register;
|
|
6658
|
+
const outside = (pitch) => pitch < low || pitch > high;
|
|
6659
|
+
const backward = relation.kind === "retrograde" || relation.kind === "retrogradeInversion";
|
|
6660
|
+
const cell = clipStatement(
|
|
6661
|
+
source.map((note2) => ({
|
|
6662
|
+
pitch: note2.pitch,
|
|
6663
|
+
startBeat: note2.startBeat,
|
|
6664
|
+
durationBeat: note2.durationBeat
|
|
6665
|
+
})),
|
|
6666
|
+
sourceEnd
|
|
6667
|
+
);
|
|
6668
|
+
const replays = relationLevels(relation.kind).map((level) => {
|
|
6669
|
+
const notes = clipStatement(
|
|
6670
|
+
replayAt(cell, relation, level, statement.startBeat, key),
|
|
6671
|
+
statement.endBeat
|
|
6672
|
+
);
|
|
6673
|
+
return { level, notes, outside: notes.filter((note2) => outside(note2.pitch)).length };
|
|
6674
|
+
});
|
|
6675
|
+
const fewestOutside = Math.min(...replays.map((replay) => replay.outside));
|
|
6676
|
+
let best = null;
|
|
6677
|
+
for (const { level, notes: expected, outside: away } of replays) {
|
|
6678
|
+
if (expected.length !== derived.length) {
|
|
6679
|
+
continue;
|
|
6680
|
+
}
|
|
6681
|
+
let forced = 0;
|
|
6682
|
+
let holds = true;
|
|
6683
|
+
for (let i = 0; holds && i < expected.length; i += 1) {
|
|
6684
|
+
const want = expected[i];
|
|
6685
|
+
const got = derived[i];
|
|
6686
|
+
if (i > 0) {
|
|
6687
|
+
const gap = got.startBeat - derived[i - 1].startBeat;
|
|
6688
|
+
holds = Math.abs(gap - (want.startBeat - expected[i - 1].startBeat)) <= BEAT_EPS;
|
|
6689
|
+
}
|
|
6690
|
+
if (!holds || got.pitch === want.pitch) {
|
|
6691
|
+
continue;
|
|
6692
|
+
}
|
|
6693
|
+
const origin = source[backward ? cell.length - 1 - i : i];
|
|
6694
|
+
forced += 1;
|
|
6695
|
+
holds = closing.has(got) || origin !== void 0 && closing.has(origin) || away === fewestOutside && outside(want.pitch);
|
|
6696
|
+
}
|
|
6697
|
+
const distance = Math.abs(level - planned);
|
|
6698
|
+
const nearer = best === null || distance < Math.abs(best.level - planned) || distance === Math.abs(best.level - planned) && level < best.level;
|
|
6699
|
+
if (holds && nearer) {
|
|
6700
|
+
best = { level, forced };
|
|
6701
|
+
}
|
|
6702
|
+
}
|
|
6703
|
+
return best;
|
|
6704
|
+
}
|
|
6705
|
+
function motifDerivationViolations(melody, plan, closing) {
|
|
6706
|
+
const keyAt = planKeyAt(plan);
|
|
6707
|
+
const errors = [];
|
|
6708
|
+
const warnings = [];
|
|
6709
|
+
for (const statement of plan.motifs) {
|
|
6710
|
+
if (statement.from === null) {
|
|
6711
|
+
continue;
|
|
6712
|
+
}
|
|
6713
|
+
const source = plan.motifs[statement.from];
|
|
6714
|
+
const sourceNotes = notesInRange(melody, source.startBeat, source.endBeat);
|
|
6715
|
+
const derivedNotes = notesInRange(melody, statement.startBeat, statement.endBeat);
|
|
6716
|
+
const relation = statement.relation;
|
|
6717
|
+
const rationale = `the statement at beat ${statement.startBeat} does not derive from its source the way the plan names`;
|
|
6718
|
+
if (sourceNotes.length === 0 || derivedNotes.length === 0) {
|
|
6719
|
+
errors.push({
|
|
6720
|
+
kind: "motifDerivation",
|
|
6721
|
+
severity: "error",
|
|
6722
|
+
atBeat: statement.startBeat,
|
|
6723
|
+
expected: relation === null ? "a variation" : relation.kind,
|
|
6724
|
+
actual: "no notes to compare",
|
|
6725
|
+
rationale
|
|
6726
|
+
});
|
|
6727
|
+
continue;
|
|
6728
|
+
}
|
|
6729
|
+
if (relation === null) {
|
|
6730
|
+
const similarity = melodicSimilarity(sourceNotes, derivedNotes);
|
|
6731
|
+
if (similarity < DEFAULT_VARIATION_THRESHOLD) {
|
|
6732
|
+
errors.push({
|
|
6733
|
+
kind: "motifDerivation",
|
|
6734
|
+
severity: "error",
|
|
6735
|
+
atBeat: statement.startBeat,
|
|
6736
|
+
expected: `a variation (melodicSimilarity >= ${DEFAULT_VARIATION_THRESHOLD})`,
|
|
6737
|
+
actual: `melodicSimilarity ${similarity.toFixed(3)}`,
|
|
6738
|
+
rationale
|
|
6739
|
+
});
|
|
6740
|
+
}
|
|
6741
|
+
continue;
|
|
6742
|
+
}
|
|
6743
|
+
const replay = replayLevel(
|
|
6744
|
+
sourceNotes,
|
|
6745
|
+
derivedNotes,
|
|
6746
|
+
statement,
|
|
6747
|
+
source.endBeat,
|
|
6748
|
+
plan,
|
|
6749
|
+
scaleOf(keyAt(statement.startBeat)),
|
|
6750
|
+
closing
|
|
6751
|
+
);
|
|
6752
|
+
const planned = relation.kind === "tonalTransposition" ? relation.degrees ?? 0 : relation.semitones;
|
|
6753
|
+
if (replay === null) {
|
|
6754
|
+
errors.push({
|
|
6755
|
+
kind: "motifDerivation",
|
|
6756
|
+
severity: "error",
|
|
6757
|
+
atBeat: statement.startBeat,
|
|
6758
|
+
expected: relation.kind,
|
|
6759
|
+
actual: "no pitch level replays the source",
|
|
6760
|
+
rationale
|
|
6761
|
+
});
|
|
6762
|
+
} else if (replay.level !== planned || replay.forced > 0) {
|
|
6763
|
+
warnings.push({
|
|
6764
|
+
kind: "motifDisplaced",
|
|
6765
|
+
severity: "warning",
|
|
6766
|
+
atBeat: statement.startBeat,
|
|
6767
|
+
expected: `${relation.kind} at level ${planned} with every pitch replayed`,
|
|
6768
|
+
actual: `level ${replay.level}, ${replay.forced} excused note(s)`,
|
|
6769
|
+
rationale: `the statement at beat ${statement.startBeat} carries out its planned derivation at another pitch level or with notes excused`
|
|
6770
|
+
});
|
|
6771
|
+
}
|
|
6772
|
+
}
|
|
6773
|
+
return { errors, warnings };
|
|
6774
|
+
}
|
|
6775
|
+
function phraseAt(plan, beat) {
|
|
6776
|
+
return plan.phrases.find(
|
|
6777
|
+
(phrase) => beat >= phrase.startBeat - BEAT_EPS && beat < phrase.endBeat - BEAT_EPS
|
|
6778
|
+
);
|
|
6779
|
+
}
|
|
6780
|
+
function registerViolations(melody, plan) {
|
|
6781
|
+
const violations = [];
|
|
6782
|
+
for (const note2 of melody) {
|
|
6783
|
+
const phrase = phraseAt(plan, note2.startBeat);
|
|
6784
|
+
if (phrase === void 0) {
|
|
6785
|
+
continue;
|
|
6786
|
+
}
|
|
6787
|
+
if (note2.pitch < phrase.register.low || note2.pitch > phrase.register.high) {
|
|
6788
|
+
violations.push({
|
|
6789
|
+
kind: "register",
|
|
6790
|
+
severity: "error",
|
|
6791
|
+
atBeat: note2.startBeat,
|
|
6792
|
+
expected: `a pitch within [${phrase.register.low}, ${phrase.register.high}]`,
|
|
6793
|
+
actual: `pitch ${note2.pitch}`,
|
|
6794
|
+
rationale: `the note at beat ${note2.startBeat} falls outside its phrase's planned register`
|
|
6795
|
+
});
|
|
6796
|
+
}
|
|
6797
|
+
}
|
|
6798
|
+
return violations;
|
|
6799
|
+
}
|
|
6800
|
+
function planOutline(phrase) {
|
|
6801
|
+
const amplitude = (phrase.register.high - phrase.register.low) / 2;
|
|
6802
|
+
const outline = [];
|
|
6803
|
+
for (let k = 0; k < OUTLINE_POINTS; k += 1) {
|
|
6804
|
+
const t = (k + 0.5) / OUTLINE_POINTS;
|
|
6805
|
+
outline.push(
|
|
6806
|
+
phrase.register.mean + plannedContourAt(phrase.shape, phrase.peakPosition, t) * amplitude
|
|
6807
|
+
);
|
|
6808
|
+
}
|
|
6809
|
+
return outline;
|
|
6810
|
+
}
|
|
6811
|
+
function planContourPhrase(phrase) {
|
|
6812
|
+
return {
|
|
6813
|
+
shape: phrase.shape,
|
|
6814
|
+
low: phrase.register.low,
|
|
6815
|
+
high: phrase.register.high,
|
|
6816
|
+
outline: planOutline(phrase)
|
|
6817
|
+
};
|
|
6818
|
+
}
|
|
6819
|
+
function planRegisterPlan(plan) {
|
|
6820
|
+
const phrases = plan.phrases.map((phrase) => ({
|
|
6821
|
+
low: phrase.register.low,
|
|
6822
|
+
high: phrase.register.high,
|
|
6823
|
+
mean: phrase.register.mean,
|
|
6824
|
+
peakPosition: phrase.peakPosition
|
|
6825
|
+
}));
|
|
6826
|
+
let weighted = 0;
|
|
6827
|
+
let weight = 0;
|
|
6828
|
+
for (const phrase of plan.phrases) {
|
|
6829
|
+
const length = phrase.endBeat - phrase.startBeat;
|
|
6830
|
+
weighted += phrase.register.mean * length;
|
|
6831
|
+
weight += length;
|
|
6832
|
+
}
|
|
6833
|
+
return { phrases, mean: weight > 0 ? weighted / weight : null };
|
|
6834
|
+
}
|
|
6835
|
+
function planRhythmReading(plan) {
|
|
6836
|
+
const hasOnsets = plan.rhythm.onsetLevels.some((share) => share > 0);
|
|
6837
|
+
return {
|
|
6838
|
+
onsets: hasOnsets ? 1 : 0,
|
|
6839
|
+
onsetLevels: plan.rhythm.onsetLevels,
|
|
6840
|
+
barPositions: [],
|
|
6841
|
+
interOnsetShares: plan.rhythm.interOnsetShares,
|
|
6842
|
+
syncopation: plan.rhythm.syncopation
|
|
6843
|
+
};
|
|
6844
|
+
}
|
|
6845
|
+
function peakPositionWarnings(plan, readings) {
|
|
6846
|
+
const violations = [];
|
|
6847
|
+
plan.phrases.forEach((phrase, index) => {
|
|
6848
|
+
const reading = readings[index];
|
|
6849
|
+
if (reading == null) {
|
|
6850
|
+
return;
|
|
6851
|
+
}
|
|
6852
|
+
const drift = Math.abs(reading.peakPosition - phrase.peakPosition);
|
|
6853
|
+
if (drift > PEAK_POSITION_TOLERANCE) {
|
|
6854
|
+
violations.push({
|
|
6855
|
+
kind: "peakPosition",
|
|
6856
|
+
severity: "warning",
|
|
6857
|
+
atBeat: phrase.startBeat,
|
|
6858
|
+
expected: `a peak position within ${PEAK_POSITION_TOLERANCE} of ${phrase.peakPosition}`,
|
|
6859
|
+
actual: `${reading.peakPosition}`,
|
|
6860
|
+
rationale: `the phrase starting at beat ${phrase.startBeat} places its melodic peak further from the planned position than the tolerance allows`
|
|
6861
|
+
});
|
|
6862
|
+
}
|
|
6863
|
+
});
|
|
6864
|
+
return violations;
|
|
6865
|
+
}
|
|
6866
|
+
function lineMeanOf(melody) {
|
|
6867
|
+
let weighted = 0;
|
|
6868
|
+
let total = 0;
|
|
6869
|
+
for (const note2 of melody) {
|
|
6870
|
+
weighted += note2.pitch * note2.durationBeat;
|
|
6871
|
+
total += note2.durationBeat;
|
|
6872
|
+
}
|
|
6873
|
+
return total > 0 ? weighted / total : null;
|
|
6874
|
+
}
|
|
6875
|
+
function planDensity(plan) {
|
|
6876
|
+
let weighted = 0;
|
|
6877
|
+
let bars = 0;
|
|
6878
|
+
for (const phrase of plan.phrases) {
|
|
6879
|
+
const span = barSpanOf(phrase.startBeat, phrase.endBeat, plan.meters);
|
|
6880
|
+
weighted += phrase.onsetDensity * span;
|
|
6881
|
+
bars += span;
|
|
6882
|
+
}
|
|
6883
|
+
return { onsets: 1, onsetDensity: bars > 0 ? weighted / bars : 0 };
|
|
6884
|
+
}
|
|
6885
|
+
function evaluateComposition(melody, plan, opts) {
|
|
6886
|
+
assertCompositionPlan(plan, "evaluated plan");
|
|
6887
|
+
const { budget } = assertOptions(opts, "evaluate options");
|
|
6888
|
+
const notes = [
|
|
6889
|
+
...assertNoteEvents(melody, "evaluated melody", { budget, allowNonPositiveDuration: true })
|
|
6890
|
+
].sort((a, b) => a.startBeat - b.startBeat);
|
|
6891
|
+
const timeline = planChordTimeline(plan);
|
|
6892
|
+
const lines = phraseLines(notes, plan);
|
|
6893
|
+
const closing = new Set(
|
|
6894
|
+
lines.flatMap((line) => line.length === 0 ? [] : [notes[line.at(-1)]])
|
|
6895
|
+
);
|
|
6896
|
+
const derivations = motifDerivationViolations(notes, plan, closing);
|
|
6897
|
+
const lineMean = lineMeanOf(notes);
|
|
6898
|
+
const readings = plan.phrases.map(
|
|
6899
|
+
(phrase) => phraseMelody(notes, phrase.startBeat, phrase.endBeat, lineMean ?? 0, plan.meters, budget)
|
|
6900
|
+
);
|
|
6901
|
+
const violations = [
|
|
6902
|
+
...spanViolation(notes, plan),
|
|
6903
|
+
...phraseBoundaryViolations(notes, plan),
|
|
6904
|
+
...cadenceViolations(notes, plan, timeline, lines),
|
|
6905
|
+
...harmonyViolations(notes, plan, timeline, lines),
|
|
6906
|
+
...registerViolations(notes, plan),
|
|
6907
|
+
...derivations.errors,
|
|
6908
|
+
...derivations.warnings,
|
|
6909
|
+
...peakPositionWarnings(plan, readings)
|
|
6910
|
+
];
|
|
6911
|
+
const rhythm = analyzeRhythm(notes, {
|
|
6912
|
+
meters: plan.meters,
|
|
6913
|
+
totalBeats: plan.span.endBeat,
|
|
6914
|
+
budget
|
|
6915
|
+
});
|
|
6916
|
+
const target = planRhythmReading(plan);
|
|
6917
|
+
const fit = {
|
|
6918
|
+
contour: contourSimilarity(plan.phrases.map(planContourPhrase), readings),
|
|
6919
|
+
register: registerSimilarity(planRegisterPlan(plan), { phrases: readings, mean: lineMean }),
|
|
6920
|
+
onset: onsetSimilarity(target, rhythm),
|
|
6921
|
+
duration: durationSimilarity(target, rhythm),
|
|
6922
|
+
syncopation: syncopationSimilarity(target, rhythm),
|
|
6923
|
+
density: densitySimilarity(planDensity(plan), rhythm)
|
|
6924
|
+
};
|
|
6925
|
+
return { violations, fit };
|
|
6926
|
+
}
|
|
6927
|
+
|
|
4964
6928
|
// src/generate/progression/index.ts
|
|
4965
6929
|
var DEFAULT_TS6 = { numerator: 4, denominator: 4 };
|
|
4966
6930
|
var PROG_STYLES = Object.freeze(["minimal", "dance", "idol", "rock"]);
|
|
@@ -5603,8 +7567,14 @@ export {
|
|
|
5603
7567
|
motifToNoteEvents,
|
|
5604
7568
|
transformMotif,
|
|
5605
7569
|
developMotif,
|
|
7570
|
+
COMPOSITION_PLAN_VERSION,
|
|
7571
|
+
assertCompositionPlan,
|
|
7572
|
+
planTimeline,
|
|
7573
|
+
generateMelody,
|
|
5606
7574
|
ORNAMENT_STYLES,
|
|
5607
7575
|
ornament,
|
|
7576
|
+
deriveCompositionPlan,
|
|
7577
|
+
evaluateComposition,
|
|
5608
7578
|
BORROWED_DEGREES,
|
|
5609
7579
|
progressions,
|
|
5610
7580
|
progressionsByStyle,
|
|
@@ -5618,4 +7588,4 @@ export {
|
|
|
5618
7588
|
generateRhythm,
|
|
5619
7589
|
rhythmDensity
|
|
5620
7590
|
};
|
|
5621
|
-
//# sourceMappingURL=chunk-
|
|
7591
|
+
//# sourceMappingURL=chunk-CPMB7P75.js.map
|