@libraz/libcantus 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +63 -0
- package/README.md +3 -1
- package/README_ja.md +13 -11
- package/dist/analyze/index.cjs +7 -7
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +14 -12
- package/dist/analyze/index.d.ts +14 -12
- package/dist/analyze/index.js +6 -6
- package/dist/{chunk-NB25P3EH.js → chunk-2G5MCGYU.js} +33 -11
- package/dist/chunk-2G5MCGYU.js.map +1 -0
- package/dist/{chunk-NM7UAMC3.cjs → chunk-3FDBMIPR.cjs} +64 -64
- package/dist/chunk-3FDBMIPR.cjs.map +1 -0
- package/dist/{chunk-YFA7X2F6.cjs → chunk-3MDOEFIQ.cjs} +411 -327
- package/dist/chunk-3MDOEFIQ.cjs.map +1 -0
- package/dist/{chunk-IPEF6NWB.js → chunk-5MWRBYAT.js} +278 -154
- package/dist/chunk-5MWRBYAT.js.map +1 -0
- package/dist/{chunk-B2PW6EHN.cjs → chunk-6TTBEON3.cjs} +1253 -889
- package/dist/chunk-6TTBEON3.cjs.map +1 -0
- package/dist/{chunk-UMWZ4ZET.js → chunk-7NQ6AL3H.js} +4 -4
- package/dist/chunk-7NQ6AL3H.js.map +1 -0
- package/dist/{chunk-PWC4CRWT.cjs → chunk-E3QO3UET.cjs} +31 -23
- package/dist/chunk-E3QO3UET.cjs.map +1 -0
- package/dist/{chunk-GNVN4PPJ.js → chunk-FJFULJZE.js} +531 -213
- package/dist/chunk-FJFULJZE.js.map +1 -0
- package/dist/{chunk-CPMB7P75.js → chunk-FK3DGAIZ.js} +608 -244
- package/dist/chunk-FK3DGAIZ.js.map +1 -0
- package/dist/chunk-GEDTJHAC.cjs.map +1 -1
- package/dist/{chunk-RFIBF2RR.js → chunk-H7FVMBUC.js} +138 -65
- package/dist/chunk-H7FVMBUC.js.map +1 -0
- package/dist/{chunk-4R2LWV7E.cjs → chunk-HYSVSFNI.cjs} +664 -540
- package/dist/chunk-HYSVSFNI.cjs.map +1 -0
- package/dist/{chunk-35NBV2FT.cjs → chunk-KMYGRFZ6.cjs} +47 -24
- package/dist/chunk-KMYGRFZ6.cjs.map +1 -0
- package/dist/{chunk-IFZDWIKM.cjs → chunk-LQZQDBCD.cjs} +1048 -730
- package/dist/chunk-LQZQDBCD.cjs.map +1 -0
- package/dist/{chunk-PM4XJE2Y.cjs → chunk-P3CPXAJ5.cjs} +76 -54
- package/dist/chunk-P3CPXAJ5.cjs.map +1 -0
- package/dist/{chunk-S5BKZZGM.cjs → chunk-S3QELJUE.cjs} +72 -56
- package/dist/chunk-S3QELJUE.cjs.map +1 -0
- package/dist/{chunk-PUKJP2Z5.js → chunk-TXFWSU5D.js} +31 -8
- package/dist/chunk-TXFWSU5D.js.map +1 -0
- package/dist/{chunk-76ZBTRG2.cjs → chunk-U4EQTDGN.cjs} +258 -218
- package/dist/chunk-U4EQTDGN.cjs.map +1 -0
- package/dist/chunk-UGLFDG7L.cjs.map +1 -1
- package/dist/{chunk-MLBPXA4Z.js → chunk-UTE5A36R.js} +198 -114
- package/dist/chunk-UTE5A36R.js.map +1 -0
- package/dist/{chunk-ZBUROTJQ.js → chunk-UZKQISRF.js} +17 -9
- package/dist/chunk-UZKQISRF.js.map +1 -0
- package/dist/{chunk-7KG3RTYS.js → chunk-VL5NXVCN.js} +281 -155
- package/dist/chunk-VL5NXVCN.js.map +1 -0
- package/dist/{chunk-CGBADIO5.cjs → chunk-WOMCXN2W.cjs} +328 -202
- package/dist/chunk-WOMCXN2W.cjs.map +1 -0
- package/dist/{chunk-3R3QVKY5.cjs → chunk-YDGMWH43.cjs} +397 -324
- package/dist/chunk-YDGMWH43.cjs.map +1 -0
- package/dist/{chunk-AGOVW7U6.js → chunk-YYVXRV3V.js} +72 -56
- package/dist/chunk-YYVXRV3V.js.map +1 -0
- package/dist/{chunk-M5RYMAFI.js → chunk-ZLNSH4UL.js} +71 -31
- package/dist/chunk-ZLNSH4UL.js.map +1 -0
- package/dist/{coerce-DRERzdve.d.cts → coerce-CUhHc3Tu.d.ts} +16 -8
- package/dist/{coerce-DP0vYu-x.d.ts → coerce-Ytt2Eiq9.d.cts} +16 -8
- package/dist/core/index.cjs +5 -5
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +9 -8
- package/dist/core/index.d.ts +9 -8
- package/dist/core/index.js +4 -4
- package/dist/generate/index.cjs +8 -8
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +28 -18
- package/dist/generate/index.d.ts +28 -18
- package/dist/generate/index.js +7 -7
- package/dist/{index-BMweUlm5.d.cts → index-5VJgKz_Q.d.cts} +6 -2
- package/dist/{index-BHIUyjhZ.d.ts → index-BLuDesVG.d.ts} +20 -13
- package/dist/{index-B9QLziDe.d.cts → index-BV6APVa6.d.cts} +20 -13
- package/dist/{index-CL4b4gUQ.d.ts → index-Bf0Sv8a9.d.ts} +6 -2
- package/dist/{index-Dw2DP7vm.d.ts → index-BkT21EMv.d.cts} +3 -2
- package/dist/{index-BL5N2Oyz.d.cts → index-C7KCaTRz.d.ts} +3 -2
- package/dist/{index-Cy15oTvh.d.ts → index-C9MYbuKS.d.cts} +7 -3
- package/dist/{index-Cy15oTvh.d.cts → index-C9MYbuKS.d.ts} +7 -3
- package/dist/{index-CY-3NVfy.d.cts → index-DVInzCDS.d.cts} +53 -29
- package/dist/{index-9eoQs2C1.d.ts → index-Dg-SAfhz.d.ts} +6 -4
- package/dist/{index-zaZdmnU4.d.ts → index-jgeug9EA.d.ts} +53 -29
- package/dist/{index-cHvbYgJI.d.cts → index-pQihL4z7.d.cts} +6 -4
- package/dist/index.cjs +13 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -11
- package/dist/index.d.ts +11 -11
- package/dist/index.js +12 -12
- package/dist/{masks-Cc7-MzzY.d.ts → masks-BnzXnBj0.d.cts} +3 -2
- package/dist/{masks-pADfTOiD.d.cts → masks-tycmkrgs.d.ts} +3 -2
- package/dist/model/index.cjs +12 -12
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +63 -48
- package/dist/model/index.d.ts +63 -48
- package/dist/model/index.js +11 -11
- package/dist/{positional-DJgs6ltD.d.ts → positional-CscpxtlE.d.cts} +3 -2
- package/dist/{positional-DMZer-FM.d.cts → positional-DLnfUgqG.d.ts} +3 -2
- package/dist/{profile-DLdlIpS9.d.ts → profile-B4WqcM4H.d.ts} +25 -17
- package/dist/{profile-CQMEqdH9.d.cts → profile-BJd1XkPB.d.cts} +25 -17
- package/dist/theory/index.cjs +5 -5
- package/dist/theory/index.cjs.map +1 -1
- package/dist/theory/index.d.cts +18 -14
- package/dist/theory/index.d.ts +18 -14
- package/dist/theory/index.js +4 -4
- package/dist/{types-D-XgxGvp.d.cts → types-7mGFwvMm.d.ts} +45 -22
- package/dist/{types-Dh7C064C.d.ts → types-_ofpb2be.d.cts} +45 -22
- package/package.json +13 -13
- package/dist/chunk-35NBV2FT.cjs.map +0 -1
- package/dist/chunk-3R3QVKY5.cjs.map +0 -1
- package/dist/chunk-4R2LWV7E.cjs.map +0 -1
- package/dist/chunk-76ZBTRG2.cjs.map +0 -1
- package/dist/chunk-7KG3RTYS.js.map +0 -1
- package/dist/chunk-AGOVW7U6.js.map +0 -1
- package/dist/chunk-B2PW6EHN.cjs.map +0 -1
- package/dist/chunk-CGBADIO5.cjs.map +0 -1
- package/dist/chunk-CPMB7P75.js.map +0 -1
- package/dist/chunk-GNVN4PPJ.js.map +0 -1
- package/dist/chunk-IFZDWIKM.cjs.map +0 -1
- package/dist/chunk-IPEF6NWB.js.map +0 -1
- package/dist/chunk-M5RYMAFI.js.map +0 -1
- package/dist/chunk-MLBPXA4Z.js.map +0 -1
- package/dist/chunk-NB25P3EH.js.map +0 -1
- package/dist/chunk-NM7UAMC3.cjs.map +0 -1
- package/dist/chunk-PM4XJE2Y.cjs.map +0 -1
- package/dist/chunk-PUKJP2Z5.js.map +0 -1
- package/dist/chunk-PWC4CRWT.cjs.map +0 -1
- package/dist/chunk-RFIBF2RR.js.map +0 -1
- package/dist/chunk-S5BKZZGM.cjs.map +0 -1
- package/dist/chunk-UMWZ4ZET.js.map +0 -1
- package/dist/chunk-YFA7X2F6.cjs.map +0 -1
- package/dist/chunk-ZBUROTJQ.js.map +0 -1
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
analyzeVoice,
|
|
7
7
|
assertChordTimeline,
|
|
8
8
|
assertReferenceProfile,
|
|
9
|
+
assertSpansInOrder,
|
|
9
10
|
attachPivots,
|
|
10
11
|
barGridStart,
|
|
11
12
|
barSpanOf,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
contourSimilarity,
|
|
18
19
|
densitySimilarity,
|
|
19
20
|
detectCadences,
|
|
21
|
+
detectModulations,
|
|
20
22
|
dirtySlotsFor,
|
|
21
23
|
durationSimilarity,
|
|
22
24
|
extractMotifs,
|
|
@@ -31,6 +33,7 @@ import {
|
|
|
31
33
|
keyPlanSimilarity,
|
|
32
34
|
keyTimelineFromNotes,
|
|
33
35
|
lastBarOf,
|
|
36
|
+
latestSoundingEnd,
|
|
34
37
|
melodicContour,
|
|
35
38
|
melodicSimilarity,
|
|
36
39
|
motifGraph,
|
|
@@ -43,16 +46,19 @@ import {
|
|
|
43
46
|
progressionSimilarity,
|
|
44
47
|
reduceProgression,
|
|
45
48
|
registerSimilarity,
|
|
49
|
+
resolveSpanEnd,
|
|
50
|
+
rhythmOver,
|
|
46
51
|
sameInstant,
|
|
47
52
|
sectionSequenceSimilarity,
|
|
48
53
|
segmentStartingAt,
|
|
49
54
|
sliceBars,
|
|
55
|
+
statedKeyRegions,
|
|
50
56
|
surfaceSimilarity,
|
|
51
57
|
syncopationSimilarity,
|
|
52
58
|
weightSimilarity,
|
|
53
59
|
weightedMean,
|
|
54
60
|
windowWeights
|
|
55
|
-
} from "./chunk-
|
|
61
|
+
} from "./chunk-FJFULJZE.js";
|
|
56
62
|
import {
|
|
57
63
|
PROFILE_WEIGHTS,
|
|
58
64
|
chordPitchClasses,
|
|
@@ -60,7 +66,7 @@ import {
|
|
|
60
66
|
majorKey,
|
|
61
67
|
resolveKey,
|
|
62
68
|
scaleOf
|
|
63
|
-
} from "./chunk-
|
|
69
|
+
} from "./chunk-H7FVMBUC.js";
|
|
64
70
|
import {
|
|
65
71
|
BEAT_EPS,
|
|
66
72
|
InvalidInputError,
|
|
@@ -82,7 +88,7 @@ import {
|
|
|
82
88
|
isStrongBeat,
|
|
83
89
|
pitchClassOf,
|
|
84
90
|
resolveMeters
|
|
85
|
-
} from "./chunk-
|
|
91
|
+
} from "./chunk-YYVXRV3V.js";
|
|
86
92
|
|
|
87
93
|
// src/analyze/arrange/internal.ts
|
|
88
94
|
var MAX_LANE_LEAP = 24;
|
|
@@ -115,7 +121,7 @@ function assertTrackNotes(tracks, options) {
|
|
|
115
121
|
const notes = tracks[index]?.notes;
|
|
116
122
|
if (!Array.isArray(notes)) {
|
|
117
123
|
throw new InvalidInputError(
|
|
118
|
-
`tracks[${index}].notes must be an array; received ${
|
|
124
|
+
`tracks[${index}].notes must be an array; received ${describeRejected(notes)}`
|
|
119
125
|
);
|
|
120
126
|
}
|
|
121
127
|
assertNoteEvents(notes, `tracks[${index}].notes`, options);
|
|
@@ -153,6 +159,7 @@ function assertGivenKeys(keys, name) {
|
|
|
153
159
|
}
|
|
154
160
|
resolveKey(region.key);
|
|
155
161
|
}
|
|
162
|
+
assertSpansInOrder(keys, name);
|
|
156
163
|
}
|
|
157
164
|
function harmonyTrackSet(harmonyTracks, tracks, name = "harmonyTracks") {
|
|
158
165
|
if (harmonyTracks === void 0) {
|
|
@@ -338,7 +345,12 @@ function lastStartingAtOrBefore(sounding, beat) {
|
|
|
338
345
|
function prepareTracks(tracks) {
|
|
339
346
|
let nextId = 0;
|
|
340
347
|
return tracks.map((track, index) => {
|
|
341
|
-
const ordered = track.notes.map((note, originalIndex) => ({ note, originalIndex })).filter(({ note }) => note.durationBeat > 0).sort((a, b) => a.note.startBeat - b.note.startBeat || a.note.pitch - b.note.pitch).map(({ note, originalIndex }) => ({
|
|
348
|
+
const ordered = track.notes.map((note, originalIndex) => ({ note, originalIndex })).filter(({ note }) => note.durationBeat > 0).sort((a, b) => a.note.startBeat - b.note.startBeat || a.note.pitch - b.note.pitch).map(({ note, originalIndex }) => ({
|
|
349
|
+
...note,
|
|
350
|
+
pitch: note.pitch === 0 ? 0 : note.pitch,
|
|
351
|
+
id: nextId++,
|
|
352
|
+
originalIndex
|
|
353
|
+
}));
|
|
342
354
|
const voices = splitIntoSubVoices(ordered).map((voice) => {
|
|
343
355
|
const sounding = voice.map((note, i) => {
|
|
344
356
|
const prev = i > 0 ? voice[i - 1] : void 0;
|
|
@@ -379,15 +391,16 @@ function withGivenKeys(result, given) {
|
|
|
379
391
|
return { ...result, keys: given, prevailingKey: prevailingKeyOf(given) ?? result.prevailingKey };
|
|
380
392
|
}
|
|
381
393
|
function callerTimeline(timeline, key, given, pooled, meters, budget) {
|
|
382
|
-
const chartEnd = timeline.segments.reduce(
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
0
|
|
394
|
+
const chartEnd = timeline.segments.reduce(
|
|
395
|
+
(end, segment) => Math.max(end, segment.endBeat),
|
|
396
|
+
Number.NEGATIVE_INFINITY
|
|
386
397
|
);
|
|
387
|
-
const
|
|
398
|
+
const soundingEnd = latestSoundingEnd(pooled, Number.NEGATIVE_INFINITY);
|
|
399
|
+
const spanEnd = Math.max(chartEnd, soundingEnd);
|
|
400
|
+
const totalBeats = Number.isFinite(spanEnd) ? spanEnd : 0;
|
|
388
401
|
const { startBeat: musicStart } = gridForNotes(pooled, beatsPerBarAt(0, meters));
|
|
389
402
|
const keys = given ?? attachPivots(
|
|
390
|
-
key !== void 0 ?
|
|
403
|
+
key !== void 0 ? statedKeyRegions(resolveKey(key), musicStart, totalBeats) : keyTimelineFromNotes(pooled, { meters, totalBeats, budget }),
|
|
391
404
|
timeline.segments
|
|
392
405
|
);
|
|
393
406
|
return {
|
|
@@ -738,7 +751,7 @@ function sessionOf(tracks, opts, analysis, evidence) {
|
|
|
738
751
|
}
|
|
739
752
|
if (!Array.isArray(edit.notes)) {
|
|
740
753
|
throw new InvalidInputError(
|
|
741
|
-
`edits[${index}].notes must be an array; received ${
|
|
754
|
+
`edits[${index}].notes must be an array; received ${describeRejected(edit.notes)}`
|
|
742
755
|
);
|
|
743
756
|
}
|
|
744
757
|
assertNoteEvents(edit.notes, `edits[${index}].notes`, {
|
|
@@ -773,6 +786,7 @@ var FUNCTION_WEIGHT = 0.5;
|
|
|
773
786
|
var DISSONANCE_WEIGHT = 0.35;
|
|
774
787
|
var SPAN_WEIGHT = 0.15;
|
|
775
788
|
var SPAN_SATURATION = 24;
|
|
789
|
+
var MAX_DISTINCT_PITCHES = 128;
|
|
776
790
|
function clamp012(value) {
|
|
777
791
|
return Math.min(1, Math.max(0, value));
|
|
778
792
|
}
|
|
@@ -804,19 +818,14 @@ function tensionCurve(tracks, opts = {}) {
|
|
|
804
818
|
const harmonyTracks = harmonyTrackSet(opts.harmonyTracks, tracks);
|
|
805
819
|
const pooled = poolNotes(tracks, harmonyTracks);
|
|
806
820
|
const all = poolNotes(tracks);
|
|
807
|
-
const totalBeats = all
|
|
821
|
+
const totalBeats = latestSoundingEnd(all);
|
|
808
822
|
const { origin: sampleStart, startBeat: musicStart } = gridForNotes(all, step);
|
|
809
|
-
const suppliedKeys = opts.timeline === void 0 ? void 0 : opts.keys ?? (opts.key !== void 0 ?
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
endBeat: totalBeats,
|
|
816
|
-
key: resolveKey(opts.key),
|
|
817
|
-
confidence: 1
|
|
818
|
-
}
|
|
819
|
-
] : keyTimelineFromNotes(pooled, { meters, totalBeats, budget }));
|
|
823
|
+
const suppliedKeys = opts.timeline === void 0 ? void 0 : opts.keys ?? (opts.key !== void 0 ? (
|
|
824
|
+
// The one region a named key holds starts where the music does, as
|
|
825
|
+
// the searched regions and the timeline's own key window do: a key
|
|
826
|
+
// cannot be in force over beats nothing sounds in.
|
|
827
|
+
statedKeyRegions(resolveKey(opts.key), musicStart, totalBeats)
|
|
828
|
+
) : keyTimelineFromNotes(pooled, { meters, totalBeats, budget }));
|
|
820
829
|
const { timeline, keys, prevailingKey } = opts.timeline === void 0 || suppliedKeys === void 0 ? chordTimelineFromNotes(pooled, {
|
|
821
830
|
key: opts.key,
|
|
822
831
|
meters,
|
|
@@ -836,7 +845,7 @@ function tensionCurve(tracks, opts = {}) {
|
|
|
836
845
|
assertGenerationBudget(sampleCount, "tension samples", budget);
|
|
837
846
|
const voiceCount = prepared.reduce((sum, track) => sum + track.voices.length, 0);
|
|
838
847
|
assertGenerationBudget(
|
|
839
|
-
sampleCount * Math.min(
|
|
848
|
+
sampleCount * Math.min(MAX_DISTINCT_PITCHES, voiceCount) * voiceCount,
|
|
840
849
|
"tension sample-voice comparisons",
|
|
841
850
|
budget
|
|
842
851
|
);
|
|
@@ -844,9 +853,8 @@ function tensionCurve(tracks, opts = {}) {
|
|
|
844
853
|
const beat = i === 0 ? Math.max(sampleStart, musicStart) : sampleStart + i * step;
|
|
845
854
|
points.push({
|
|
846
855
|
beat,
|
|
847
|
-
// The
|
|
848
|
-
|
|
849
|
-
tension: sampleTension(prepared, timeline, scaleOf(keyAt(beat)), meters, profile, beat)
|
|
856
|
+
// The key whole: an augmented sixth is told from a dominant by its letters.
|
|
857
|
+
tension: sampleTension(prepared, timeline, keyAt(beat), meters, profile, beat)
|
|
850
858
|
});
|
|
851
859
|
}
|
|
852
860
|
return points;
|
|
@@ -917,7 +925,7 @@ function sampleTension(prepared, timeline, key, meters, profile, beat) {
|
|
|
917
925
|
others.pop();
|
|
918
926
|
}
|
|
919
927
|
const result = evaluateSafety(
|
|
920
|
-
{ profile, candidatePitch: pitch, chord, key, otherVoices: others, strongBeat },
|
|
928
|
+
{ profile, candidatePitch: pitch, chord, key: key.scale, otherVoices: others, strongBeat },
|
|
921
929
|
{ suggestions: false }
|
|
922
930
|
);
|
|
923
931
|
if (held !== void 0 && tail !== void 0) {
|
|
@@ -943,19 +951,34 @@ function sampleTension(prepared, timeline, key, meters, profile, beat) {
|
|
|
943
951
|
|
|
944
952
|
// src/analyze/form/hypermeter.ts
|
|
945
953
|
var GROUP_CANDIDATES = [2, 3, 4, 6, 8];
|
|
946
|
-
var
|
|
954
|
+
var GROUP_PRIOR_LIST = [
|
|
947
955
|
[2, 0.9],
|
|
948
956
|
[3, 0.75],
|
|
949
957
|
[4, 1],
|
|
950
958
|
[6, 0.7],
|
|
951
959
|
[8, 0.85]
|
|
952
|
-
]
|
|
960
|
+
];
|
|
961
|
+
var GROUP_PRIOR = new Map(GROUP_PRIOR_LIST);
|
|
953
962
|
var CONTRAST_SHARE = 0.65;
|
|
954
963
|
var CADENCE_SHARE = 0.35;
|
|
955
964
|
var GROUP_FINAL_FIT = 1;
|
|
956
965
|
var ELIDED_FIT = 0.5;
|
|
957
966
|
var MIN_GROUPS = 2;
|
|
958
967
|
var MARGIN_FLOOR = 0.7;
|
|
968
|
+
function cadenceSpans(arrivals, segments) {
|
|
969
|
+
return arrivals.map(({ atBeat, strength }) => ({
|
|
970
|
+
atBeat,
|
|
971
|
+
endBeat: segmentStartingAt(segments, atBeat)?.endBeat ?? atBeat,
|
|
972
|
+
strength
|
|
973
|
+
}));
|
|
974
|
+
}
|
|
975
|
+
function cadenceBarsOf(spans, meter) {
|
|
976
|
+
return spans.map((span) => ({
|
|
977
|
+
arrival: barIndexAt(span.atBeat, meter),
|
|
978
|
+
last: lastBarOf(meter, span.atBeat, span.endBeat),
|
|
979
|
+
strength: span.strength
|
|
980
|
+
}));
|
|
981
|
+
}
|
|
959
982
|
function accentContrast(novelty, slices, groupBars, phase) {
|
|
960
983
|
let headSum = 0;
|
|
961
984
|
let headCount = 0;
|
|
@@ -980,22 +1003,41 @@ function accentContrast(novelty, slices, groupBars, phase) {
|
|
|
980
1003
|
}
|
|
981
1004
|
return headSum / headCount - otherSum / otherCount;
|
|
982
1005
|
}
|
|
983
|
-
function cadenceFitOf(
|
|
1006
|
+
function cadenceFitOf(cadences, slices, groupBars, phase) {
|
|
984
1007
|
const lastBar = slices[slices.length - 1]?.index ?? -1;
|
|
1008
|
+
const closing = /* @__PURE__ */ new Map();
|
|
1009
|
+
const eliding = /* @__PURE__ */ new Map();
|
|
1010
|
+
const credit = (bars, bar, strength) => {
|
|
1011
|
+
bars.set(bar, Math.max(bars.get(bar) ?? 0, strength));
|
|
1012
|
+
};
|
|
1013
|
+
for (const cadence of cadences) {
|
|
1014
|
+
const end = cadence.arrival + floorMod(groupBars - 1 + phase - cadence.arrival, groupBars);
|
|
1015
|
+
if (end <= cadence.last) {
|
|
1016
|
+
credit(closing, end, cadence.strength);
|
|
1017
|
+
} else if (floorMod(cadence.arrival - phase, groupBars) === 0 && // Elision needs a next group to arrive into. Past the last bar the span
|
|
1018
|
+
// holds there is none, so a cadence beat beyond the music is not evidence
|
|
1019
|
+
// about how the music that was read is grouped.
|
|
1020
|
+
cadence.arrival <= lastBar) {
|
|
1021
|
+
credit(eliding, cadence.arrival - 1, cadence.strength);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
985
1024
|
let ends = 0;
|
|
986
|
-
let
|
|
1025
|
+
let closed = 0;
|
|
1026
|
+
let weighted = 0;
|
|
987
1027
|
for (const slice of slices) {
|
|
988
1028
|
if (slice.index < 0 || floorMod(slice.index - phase, groupBars) !== groupBars - 1) {
|
|
989
1029
|
continue;
|
|
990
1030
|
}
|
|
991
1031
|
ends += 1;
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1032
|
+
const final = closing.get(slice.index);
|
|
1033
|
+
const elided = eliding.get(slice.index);
|
|
1034
|
+
closed += Math.max(
|
|
1035
|
+
final === void 0 ? 0 : GROUP_FINAL_FIT,
|
|
1036
|
+
elided === void 0 ? 0 : ELIDED_FIT
|
|
1037
|
+
);
|
|
1038
|
+
weighted += Math.max(GROUP_FINAL_FIT * (final ?? 0), ELIDED_FIT * (elided ?? 0));
|
|
997
1039
|
}
|
|
998
|
-
return ends === 0 ? 0 : clamp01(
|
|
1040
|
+
return ends === 0 ? { closed: 0, weighted: 0 } : { closed: clamp01(closed / ends), weighted: clamp01(weighted / ends) };
|
|
999
1041
|
}
|
|
1000
1042
|
function undecidedReading(slices) {
|
|
1001
1043
|
const fullBars = slices.filter((slice) => slice.index >= 0);
|
|
@@ -1009,6 +1051,17 @@ function undecidedReading(slices) {
|
|
|
1009
1051
|
};
|
|
1010
1052
|
}
|
|
1011
1053
|
function hypermeter(notes, meter, opts = {}) {
|
|
1054
|
+
return hypermeterOver(
|
|
1055
|
+
notes,
|
|
1056
|
+
meter,
|
|
1057
|
+
opts,
|
|
1058
|
+
(asked) => cadenceSpans(
|
|
1059
|
+
(asked.cadenceBeats ?? []).map((atBeat) => ({ atBeat, strength: 1 })),
|
|
1060
|
+
[]
|
|
1061
|
+
)
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
function hypermeterOver(notes, meter, opts, spansOf) {
|
|
1012
1065
|
const asked = assertOptions(opts, "opts");
|
|
1013
1066
|
const meters = resolveMeters({ meters: meter }, "meter");
|
|
1014
1067
|
assertNoteEvents(notes, "hypermeter notes", {
|
|
@@ -1017,10 +1070,16 @@ function hypermeter(notes, meter, opts = {}) {
|
|
|
1017
1070
|
});
|
|
1018
1071
|
const sounding = notes.filter((note) => note.durationBeat > 0);
|
|
1019
1072
|
const firstOnset = firstSoundingBeat(sounding);
|
|
1020
|
-
const
|
|
1021
|
-
|
|
1022
|
-
|
|
1073
|
+
const spanEnd = resolveSpanEnd(
|
|
1074
|
+
asked.totalBeats,
|
|
1075
|
+
firstOnset,
|
|
1076
|
+
latestSoundingEnd(sounding),
|
|
1077
|
+
"hypermeter totalBeats"
|
|
1078
|
+
);
|
|
1023
1079
|
const spanStart = barGridStart(firstOnset, meters);
|
|
1080
|
+
if (sounding.length === 0 || spanEnd <= spanStart + BEAT_EPS) {
|
|
1081
|
+
return undecidedReading([]);
|
|
1082
|
+
}
|
|
1024
1083
|
const slices = sliceBars(sounding, meters, spanStart, spanEnd, asked.budget);
|
|
1025
1084
|
const fullBars = slices.reduce((count, slice) => slice.index >= 0 ? count + 1 : count, 0);
|
|
1026
1085
|
const candidates = GROUP_CANDIDATES.filter((groupBars) => fullBars >= groupBars * MIN_GROUPS);
|
|
@@ -1033,20 +1092,21 @@ function hypermeter(notes, meter, opts = {}) {
|
|
|
1033
1092
|
asked.budget
|
|
1034
1093
|
);
|
|
1035
1094
|
const novelty = slices.map((_, index) => harmonicNovelty(slices, index));
|
|
1036
|
-
const cadenceBars =
|
|
1037
|
-
const hasCadences = cadenceBars.
|
|
1095
|
+
const cadenceBars = cadenceBarsOf(spansOf(asked), meters);
|
|
1096
|
+
const hasCadences = cadenceBars.length > 0;
|
|
1038
1097
|
const readings = [];
|
|
1039
1098
|
for (const groupBars of candidates) {
|
|
1040
1099
|
const phases = Array.from({ length: groupBars }, (_, phase) => ({
|
|
1041
1100
|
phase,
|
|
1042
1101
|
contrast: accentContrast(novelty, slices, groupBars, phase),
|
|
1043
|
-
|
|
1102
|
+
fit: cadenceFitOf(cadenceBars, slices, groupBars, phase)
|
|
1044
1103
|
}));
|
|
1045
|
-
const bestFit = phases.reduce((best2, reading) => Math.max(best2, reading.
|
|
1046
|
-
for (const { phase, contrast,
|
|
1047
|
-
if (hasCadences &&
|
|
1104
|
+
const bestFit = phases.reduce((best2, reading) => Math.max(best2, reading.fit.weighted), 0);
|
|
1105
|
+
for (const { phase, contrast, fit } of phases) {
|
|
1106
|
+
if (hasCadences && fit.weighted < bestFit - BEAT_EPS) {
|
|
1048
1107
|
continue;
|
|
1049
1108
|
}
|
|
1109
|
+
const cadenceFit = fit.closed;
|
|
1050
1110
|
const evidence = clamp01(
|
|
1051
1111
|
hasCadences ? (
|
|
1052
1112
|
// Floored at zero rather than summed raw: group heads that restate
|
|
@@ -1118,6 +1178,7 @@ var PHRASE_CUT_COST = 1.3;
|
|
|
1118
1178
|
var CLOSING_SHARE = 0.65;
|
|
1119
1179
|
var FINAL_PHRASE_BONUS = 0.15;
|
|
1120
1180
|
var HYPERMETRIC_CLOSE_BONUS = 0.1;
|
|
1181
|
+
var IMPERFECT_CLOSURE_FACTOR = 0.8;
|
|
1121
1182
|
var EVADED_FACTOR = 0.6;
|
|
1122
1183
|
var SIGNAL_ORDER = [
|
|
1123
1184
|
"cadence",
|
|
@@ -1143,16 +1204,22 @@ function readBoundary(boundary) {
|
|
|
1143
1204
|
);
|
|
1144
1205
|
return {
|
|
1145
1206
|
strength: combineEvidence(signals.map(([, strength]) => strength)),
|
|
1207
|
+
uncredited: combineEvidence(
|
|
1208
|
+
signals.filter(([signal]) => signal !== "cadence").map(([, strength]) => strength)
|
|
1209
|
+
),
|
|
1146
1210
|
signals: signals.map(([signal]) => signal)
|
|
1147
1211
|
};
|
|
1148
1212
|
}
|
|
1213
|
+
function closesPhrase(atBeat, startBeat, endBeat) {
|
|
1214
|
+
return atBeat >= startBeat - BEAT_EPS && atBeat < endBeat - BEAT_EPS;
|
|
1215
|
+
}
|
|
1149
1216
|
function lengthFit(length, expected) {
|
|
1150
1217
|
if (!(expected > 0)) {
|
|
1151
1218
|
return 0;
|
|
1152
1219
|
}
|
|
1153
1220
|
return Math.max(-1, 1 - Math.abs(length - expected) / expected);
|
|
1154
1221
|
}
|
|
1155
|
-
function choosePhrasePath(beats, strengths, minPhraseBeats, expected) {
|
|
1222
|
+
function choosePhrasePath(beats, strengths, minPhraseBeats, expected, held) {
|
|
1156
1223
|
const value = new Array(beats.length).fill(Number.NEGATIVE_INFINITY);
|
|
1157
1224
|
const cameFrom = new Array(beats.length).fill(-1);
|
|
1158
1225
|
value[0] = 0;
|
|
@@ -1166,7 +1233,9 @@ function choosePhrasePath(beats, strengths, minPhraseBeats, expected) {
|
|
|
1166
1233
|
if (length < minPhraseBeats - BEAT_EPS) {
|
|
1167
1234
|
continue;
|
|
1168
1235
|
}
|
|
1169
|
-
const
|
|
1236
|
+
const credited = held === void 0 || closesPhrase(held.arrivals[j] ?? endBeat, beats[i] ?? 0, endBeat);
|
|
1237
|
+
const strength = credited ? strengths[j] ?? 0 : held.uncredited[j] ?? 0;
|
|
1238
|
+
const gain = (value[i] ?? 0) + strength + lengthFit(length, expected) - PHRASE_CUT_COST;
|
|
1170
1239
|
if (gain > (value[j] ?? Number.NEGATIVE_INFINITY)) {
|
|
1171
1240
|
value[j] = gain;
|
|
1172
1241
|
cameFrom[j] = i;
|
|
@@ -1185,21 +1254,35 @@ function choosePhrasePath(beats, strengths, minPhraseBeats, expected) {
|
|
|
1185
1254
|
}
|
|
1186
1255
|
return path;
|
|
1187
1256
|
}
|
|
1188
|
-
function
|
|
1189
|
-
|
|
1257
|
+
function boundaryCadences(timeline, key) {
|
|
1258
|
+
const hits = detectCadences(timeline, key).filter((hit) => hit.cadence.type !== null);
|
|
1259
|
+
const spans = cadenceSpans(
|
|
1260
|
+
hits.map((hit) => ({
|
|
1261
|
+
atBeat: hit.atBeat,
|
|
1262
|
+
strength: CADENCE_BOUNDARY_STRENGTH[hit.cadence.type]
|
|
1263
|
+
})),
|
|
1264
|
+
timeline.segments
|
|
1265
|
+
);
|
|
1266
|
+
return { hits, spans };
|
|
1190
1267
|
}
|
|
1191
|
-
function keyContextFor(
|
|
1192
|
-
if (
|
|
1193
|
-
return
|
|
1268
|
+
function keyContextFor(key, timeline, meters, totalBeats, budget) {
|
|
1269
|
+
if (key !== void 0) {
|
|
1270
|
+
return key;
|
|
1194
1271
|
}
|
|
1195
|
-
const regions =
|
|
1196
|
-
meters,
|
|
1197
|
-
totalBeats,
|
|
1198
|
-
budget: opts.budget
|
|
1199
|
-
});
|
|
1272
|
+
const regions = detectModulations(timeline.segments, { meters, totalBeats, budget });
|
|
1200
1273
|
const keyAt = keyLookup(regions, prevailingKeyOf(regions) ?? resolveKey(majorKey(0)));
|
|
1201
1274
|
return (beat) => scaleOf(keyAt(beat));
|
|
1202
1275
|
}
|
|
1276
|
+
function heldCadenceBoundaries(boundaries, hit, span, meters, spanEnd) {
|
|
1277
|
+
const firstLine = barIndexAt(span.atBeat, meters) + 1;
|
|
1278
|
+
const lastLine = barIndexAt(Math.min(span.endBeat, spanEnd), meters);
|
|
1279
|
+
for (let bar = firstLine; bar <= lastLine; bar += 1) {
|
|
1280
|
+
const line = barPositionToBeat({ bar, beat: 0 }, meters);
|
|
1281
|
+
if (line < span.endBeat - BEAT_EPS) {
|
|
1282
|
+
addSignal(boundaries, line, "cadence", span.strength, hit);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1203
1286
|
function restBoundaries(boundaries, notes, meters, restBeats) {
|
|
1204
1287
|
const ordered = [...notes].sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);
|
|
1205
1288
|
let covered = Number.NEGATIVE_INFINITY;
|
|
@@ -1253,7 +1336,7 @@ function structuralWeightOf(cadence, confidence, aligned, isLast) {
|
|
|
1253
1336
|
if (result.strength === "perfect") {
|
|
1254
1337
|
base = 1;
|
|
1255
1338
|
} else if (result.strength === "imperfect") {
|
|
1256
|
-
base *=
|
|
1339
|
+
base *= IMPERFECT_CLOSURE_FACTOR;
|
|
1257
1340
|
}
|
|
1258
1341
|
}
|
|
1259
1342
|
if (result.evaded) {
|
|
@@ -1291,17 +1374,18 @@ function phrasesFromTimeline(timeline, notes, opts = {}) {
|
|
|
1291
1374
|
const segments = timeline.segments;
|
|
1292
1375
|
const firstSegment = segments[0];
|
|
1293
1376
|
const lastSegment = segments[segments.length - 1];
|
|
1294
|
-
const
|
|
1295
|
-
(
|
|
1296
|
-
firstSegment?.startBeat ??
|
|
1377
|
+
const earliest = Math.min(
|
|
1378
|
+
firstSoundingBeat(sounding, Number.POSITIVE_INFINITY),
|
|
1379
|
+
firstSegment?.startBeat ?? Number.POSITIVE_INFINITY
|
|
1297
1380
|
);
|
|
1381
|
+
const firstOnset = Number.isFinite(earliest) ? earliest : 0;
|
|
1298
1382
|
const spanStart = barGridStart(firstOnset, meters);
|
|
1299
|
-
const
|
|
1300
|
-
|
|
1301
|
-
|
|
1383
|
+
const latestEnd = Math.max(
|
|
1384
|
+
lastSegment?.endBeat ?? Number.NEGATIVE_INFINITY,
|
|
1385
|
+
latestSoundingEnd(sounding, Number.NEGATIVE_INFINITY)
|
|
1302
1386
|
);
|
|
1303
|
-
const
|
|
1304
|
-
|
|
1387
|
+
const notesEnd = Number.isFinite(latestEnd) ? latestEnd : 0;
|
|
1388
|
+
const spanEnd = resolveSpanEnd(opts.totalBeats, spanStart, notesEnd, "phrase totalBeats");
|
|
1305
1389
|
if (spanEnd <= spanStart + BEAT_EPS) {
|
|
1306
1390
|
return [];
|
|
1307
1391
|
}
|
|
@@ -1312,13 +1396,9 @@ function phrasesFromTimeline(timeline, notes, opts = {}) {
|
|
|
1312
1396
|
assertRange(longNoteBeats, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "phrase longNoteBeats");
|
|
1313
1397
|
const minPhraseBeats = opts.minPhraseBeats ?? barBeats;
|
|
1314
1398
|
assertRange(minPhraseBeats, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "phrase minPhraseBeats");
|
|
1315
|
-
const key = keyContextFor(opts,
|
|
1316
|
-
const cadences =
|
|
1317
|
-
const grouping = opts.hypermeter ??
|
|
1318
|
-
totalBeats: spanEnd,
|
|
1319
|
-
cadenceBeats: cadences.map((hit) => hit.atBeat),
|
|
1320
|
-
budget: opts.budget
|
|
1321
|
-
});
|
|
1399
|
+
const key = keyContextFor(opts.key, timeline, meters, spanEnd, opts.budget);
|
|
1400
|
+
const { hits: cadences, spans } = boundaryCadences(timeline, key);
|
|
1401
|
+
const grouping = opts.hypermeter ?? hypermeterOver(sounding, meters, { totalBeats: spanEnd, budget: opts.budget }, () => spans);
|
|
1322
1402
|
const firstBar = barIndexAt(Math.max(0, spanStart), meters);
|
|
1323
1403
|
const hyperBeats = Math.max(
|
|
1324
1404
|
barBeats,
|
|
@@ -1327,19 +1407,11 @@ function phrasesFromTimeline(timeline, notes, opts = {}) {
|
|
|
1327
1407
|
const expected = opts.expectedPhraseBeats ?? hyperBeats;
|
|
1328
1408
|
assertRange(expected, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "phrase expectedPhraseBeats");
|
|
1329
1409
|
const boundaries = /* @__PURE__ */ new Map();
|
|
1330
|
-
|
|
1331
|
-
const
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
const strength = CADENCE_BOUNDARY_STRENGTH[type];
|
|
1336
|
-
const chordEnd = arrivalEnd(timeline, hit.atBeat);
|
|
1337
|
-
addSignal(boundaries, chordEnd, "cadence", strength, hit);
|
|
1338
|
-
const barEnd = barPositionToBeat({ bar: barIndexAt(hit.atBeat, meters) + 1, beat: 0 }, meters);
|
|
1339
|
-
if (chordEnd > barEnd + BEAT_EPS) {
|
|
1340
|
-
addSignal(boundaries, barEnd, "cadence", strength, hit);
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1410
|
+
cadences.forEach((hit, index) => {
|
|
1411
|
+
const span = spans[index];
|
|
1412
|
+
addSignal(boundaries, span.endBeat, "cadence", span.strength, hit);
|
|
1413
|
+
heldCadenceBoundaries(boundaries, hit, span, meters, spanEnd);
|
|
1414
|
+
});
|
|
1343
1415
|
restBoundaries(boundaries, sounding, meters, restBeats);
|
|
1344
1416
|
for (const note of sounding) {
|
|
1345
1417
|
if (note.durationBeat >= longNoteBeats - BEAT_EPS) {
|
|
@@ -1370,7 +1442,11 @@ function phrasesFromTimeline(timeline, notes, opts = {}) {
|
|
|
1370
1442
|
stops.map((stop) => stop.beat),
|
|
1371
1443
|
readings.map((reading) => reading.strength),
|
|
1372
1444
|
minPhraseBeats,
|
|
1373
|
-
expected
|
|
1445
|
+
expected,
|
|
1446
|
+
{
|
|
1447
|
+
arrivals: stops.map((stop) => stop.cadence?.atBeat ?? stop.beat),
|
|
1448
|
+
uncredited: readings.map((reading) => reading.uncredited)
|
|
1449
|
+
}
|
|
1374
1450
|
);
|
|
1375
1451
|
const downbeatKeys = new Set(
|
|
1376
1452
|
grouping.downbeats.map((downbeat) => Math.round(downbeat / BEAT_EPS))
|
|
@@ -1382,14 +1458,15 @@ function phrasesFromTimeline(timeline, notes, opts = {}) {
|
|
|
1382
1458
|
const startBeat = stops[from]?.beat ?? spanStart;
|
|
1383
1459
|
const stop = stops[to];
|
|
1384
1460
|
const endBeat = stop?.beat ?? spanEnd;
|
|
1385
|
-
const reading = readings[to] ?? { strength: 0, signals: [] };
|
|
1461
|
+
const reading = readings[to] ?? { strength: 0, uncredited: 0, signals: [] };
|
|
1386
1462
|
const length = endBeat - startBeat;
|
|
1387
|
-
const confidence = clamp01(
|
|
1388
|
-
CLOSING_SHARE * reading.strength + (1 - CLOSING_SHARE) * Math.max(0, lengthFit(length, expected))
|
|
1389
|
-
);
|
|
1390
1463
|
const isLast = n === path.length - 1;
|
|
1391
1464
|
const closing = stop?.cadence ?? null;
|
|
1392
|
-
const cadence = closing !== null && closing.atBeat
|
|
1465
|
+
const cadence = closing !== null && closesPhrase(closing.atBeat, startBeat, endBeat) ? closing : null;
|
|
1466
|
+
const closingStrength = cadence === null ? reading.uncredited : reading.strength;
|
|
1467
|
+
const confidence = clamp01(
|
|
1468
|
+
CLOSING_SHARE * closingStrength + (1 - CLOSING_SHARE) * Math.max(0, lengthFit(length, expected))
|
|
1469
|
+
);
|
|
1393
1470
|
const structuralWeight = structuralWeightOf(
|
|
1394
1471
|
cadence,
|
|
1395
1472
|
confidence,
|
|
@@ -1463,13 +1540,17 @@ function sectionsFromNotes(notes, opts = {}) {
|
|
|
1463
1540
|
budget: opts.budget
|
|
1464
1541
|
});
|
|
1465
1542
|
const sounding = notes.filter((note) => note.durationBeat > 0);
|
|
1543
|
+
const firstSounding = firstSoundingBeat(sounding);
|
|
1544
|
+
const firstOnset = barGridStart(firstSounding, meters);
|
|
1545
|
+
const spanEnd = resolveSpanEnd(
|
|
1546
|
+
opts.totalBeats,
|
|
1547
|
+
firstOnset,
|
|
1548
|
+
latestSoundingEnd(sounding),
|
|
1549
|
+
"section totalBeats"
|
|
1550
|
+
);
|
|
1466
1551
|
if (sounding.length === 0) {
|
|
1467
1552
|
return [];
|
|
1468
1553
|
}
|
|
1469
|
-
const firstOnset = barGridStart(firstSoundingBeat(sounding), meters);
|
|
1470
|
-
const lastEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
|
|
1471
|
-
const spanEnd = Math.max(firstOnset, opts.totalBeats ?? lastEnd);
|
|
1472
|
-
assertRange(spanEnd, 0, Number.MAX_SAFE_INTEGER, "section totalBeats");
|
|
1473
1554
|
const threshold = opts.threshold ?? DEFAULT_THRESHOLD;
|
|
1474
1555
|
assertRange(threshold, 0, 1, "section threshold");
|
|
1475
1556
|
const unitBars = opts.unitBars ?? hypermeter(sounding, meters, { totalBeats: spanEnd, budget: opts.budget }).groupBars;
|
|
@@ -1485,8 +1566,9 @@ function sectionsFromNotes(notes, opts = {}) {
|
|
|
1485
1566
|
const barStart = startBar + index * unitBars;
|
|
1486
1567
|
const startBeat = index === 0 ? firstOnset : barPositionToBeat({ bar: barStart, beat: 0 }, meters);
|
|
1487
1568
|
const endBeat = index === unitCount - 1 ? spanEnd : barPositionToBeat({ bar: barStart + unitBars, beat: 0 }, meters);
|
|
1569
|
+
const fromBeat = index === 0 ? Math.min(firstSounding, startBeat) : startBeat;
|
|
1488
1570
|
const unitNotes = sounding.filter(
|
|
1489
|
-
(note) => note.startBeat >=
|
|
1571
|
+
(note) => note.startBeat >= fromBeat - BEAT_EPS && note.startBeat < endBeat - BEAT_EPS
|
|
1490
1572
|
).sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);
|
|
1491
1573
|
units.push({
|
|
1492
1574
|
startBeat,
|
|
@@ -1663,10 +1745,11 @@ function spanOf(sounding, timeline, meters, totalBeats) {
|
|
|
1663
1745
|
Math.min(firstOnset, firstSegment?.startBeat ?? firstOnset),
|
|
1664
1746
|
meters
|
|
1665
1747
|
);
|
|
1666
|
-
const
|
|
1667
|
-
|
|
1668
|
-
|
|
1748
|
+
const latestEnd = Math.max(
|
|
1749
|
+
lastSegment?.endBeat ?? Number.NEGATIVE_INFINITY,
|
|
1750
|
+
latestSoundingEnd(sounding, Number.NEGATIVE_INFINITY)
|
|
1669
1751
|
);
|
|
1752
|
+
const notesEnd = Number.isFinite(latestEnd) ? latestEnd : 0;
|
|
1670
1753
|
return { startBeat, endBeat: Math.max(startBeat, totalBeats ?? notesEnd) };
|
|
1671
1754
|
}
|
|
1672
1755
|
function sameKey(a, b) {
|
|
@@ -1710,14 +1793,7 @@ function readingsFor(notes, opts = {}) {
|
|
|
1710
1793
|
budget
|
|
1711
1794
|
}).filter((note) => note.durationBeat > 0);
|
|
1712
1795
|
const span = spanOf(sounding, timeline, meters, asked.totalBeats);
|
|
1713
|
-
keys =
|
|
1714
|
-
{
|
|
1715
|
-
startBeat: span.startBeat,
|
|
1716
|
-
endBeat: span.endBeat,
|
|
1717
|
-
key: resolveKey(asked.key),
|
|
1718
|
-
confidence: 1
|
|
1719
|
-
}
|
|
1720
|
-
];
|
|
1796
|
+
keys = statedKeyRegions(resolveKey(asked.key), span.startBeat, span.endBeat);
|
|
1721
1797
|
}
|
|
1722
1798
|
return { timeline, keys, prevailingKey: prevailingKeyOf(keys) ?? resolveKey(majorKey(0)) };
|
|
1723
1799
|
}
|
|
@@ -1732,7 +1808,9 @@ function referenceFromReadings(notes, readings, opts = {}) {
|
|
|
1732
1808
|
if (asked.totalBeats !== void 0) {
|
|
1733
1809
|
assertRange(asked.totalBeats, 0, Number.MAX_SAFE_INTEGER, "reference totalBeats");
|
|
1734
1810
|
}
|
|
1735
|
-
const line = orderedNotes(asked.melody ?? notes, "reference melody", budget)
|
|
1811
|
+
const line = orderedNotes(asked.melody ?? notes, "reference melody", budget).map(
|
|
1812
|
+
(note) => Object.is(note.pitch, -0) ? { ...note, pitch: 0 } : note
|
|
1813
|
+
);
|
|
1736
1814
|
const { timeline, prevailingKey } = readings;
|
|
1737
1815
|
const span = spanOf(sounding, timeline, meters, asked.totalBeats);
|
|
1738
1816
|
const spanEnd = span.endBeat;
|
|
@@ -1848,7 +1926,7 @@ function referenceFromReadings(notes, readings, opts = {}) {
|
|
|
1848
1926
|
shape: line.length < 2 ? null : melodicContour(line).shape,
|
|
1849
1927
|
motifs: referenceMotifs,
|
|
1850
1928
|
graph,
|
|
1851
|
-
rhythm:
|
|
1929
|
+
rhythm: rhythmOver(line, { meters, totalBeats: spanEnd, budget }, span.startBeat)
|
|
1852
1930
|
}
|
|
1853
1931
|
};
|
|
1854
1932
|
}
|
|
@@ -1858,12 +1936,18 @@ function analyzeReference(notes, opts) {
|
|
|
1858
1936
|
|
|
1859
1937
|
export {
|
|
1860
1938
|
trackRoleOf,
|
|
1939
|
+
arrangementProfile,
|
|
1940
|
+
assertTrackNotes,
|
|
1941
|
+
harmonyTrackSet,
|
|
1861
1942
|
analyzePolyphony,
|
|
1862
1943
|
analyzeArrangement,
|
|
1863
1944
|
createArrangementSession,
|
|
1864
1945
|
tensionCurve,
|
|
1865
1946
|
tensionCurveFrom,
|
|
1866
1947
|
hypermeter,
|
|
1948
|
+
hypermeterOver,
|
|
1949
|
+
boundaryCadences,
|
|
1950
|
+
keyContextFor,
|
|
1867
1951
|
phrasesFromTimeline,
|
|
1868
1952
|
structuralCadences,
|
|
1869
1953
|
sectionsFromNotes,
|
|
@@ -1871,4 +1955,4 @@ export {
|
|
|
1871
1955
|
referenceFromReadings,
|
|
1872
1956
|
analyzeReference
|
|
1873
1957
|
};
|
|
1874
|
-
//# sourceMappingURL=chunk-
|
|
1958
|
+
//# sourceMappingURL=chunk-UTE5A36R.js.map
|