@libraz/libcantus 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +63 -0
- package/README.md +7 -1
- package/README_ja.md +17 -11
- package/dist/analyze/index.cjs +31 -7
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +161 -13
- package/dist/analyze/index.d.ts +161 -13
- package/dist/analyze/index.js +37 -13
- package/dist/{chunk-NB25P3EH.js → chunk-2G5MCGYU.js} +33 -11
- package/dist/chunk-2G5MCGYU.js.map +1 -0
- package/dist/{chunk-NM7UAMC3.cjs → chunk-3FDBMIPR.cjs} +64 -64
- package/dist/chunk-3FDBMIPR.cjs.map +1 -0
- package/dist/{chunk-HPVSH7SF.cjs → chunk-3MDOEFIQ.cjs} +618 -954
- package/dist/chunk-3MDOEFIQ.cjs.map +1 -0
- package/dist/{chunk-FBH75DAY.js → chunk-5MWRBYAT.js} +560 -159
- package/dist/chunk-5MWRBYAT.js.map +1 -0
- package/dist/{chunk-J7FHVZII.cjs → chunk-6TTBEON3.cjs} +3074 -740
- package/dist/chunk-6TTBEON3.cjs.map +1 -0
- package/dist/{chunk-UMWZ4ZET.js → chunk-7NQ6AL3H.js} +4 -4
- package/dist/chunk-7NQ6AL3H.js.map +1 -0
- package/dist/{chunk-HWHGOVO6.cjs → chunk-E3QO3UET.cjs} +31 -23
- package/dist/chunk-E3QO3UET.cjs.map +1 -0
- package/dist/{chunk-H2KJHBW4.js → chunk-FJFULJZE.js} +2375 -191
- package/dist/chunk-FJFULJZE.js.map +1 -0
- package/dist/{chunk-Q6LFSAKV.js → chunk-FK3DGAIZ.js} +2657 -323
- package/dist/chunk-FK3DGAIZ.js.map +1 -0
- package/dist/chunk-GEDTJHAC.cjs.map +1 -1
- package/dist/{chunk-RFIBF2RR.js → chunk-H7FVMBUC.js} +138 -65
- package/dist/chunk-H7FVMBUC.js.map +1 -0
- package/dist/{chunk-DSMJRXCZ.cjs → chunk-HYSVSFNI.cjs} +931 -530
- package/dist/chunk-HYSVSFNI.cjs.map +1 -0
- package/dist/{chunk-35NBV2FT.cjs → chunk-KMYGRFZ6.cjs} +47 -24
- package/dist/chunk-KMYGRFZ6.cjs.map +1 -0
- package/dist/chunk-LQZQDBCD.cjs +5513 -0
- package/dist/chunk-LQZQDBCD.cjs.map +1 -0
- package/dist/{chunk-PM4XJE2Y.cjs → chunk-P3CPXAJ5.cjs} +76 -54
- package/dist/chunk-P3CPXAJ5.cjs.map +1 -0
- package/dist/{chunk-S5BKZZGM.cjs → chunk-S3QELJUE.cjs} +72 -56
- package/dist/chunk-S3QELJUE.cjs.map +1 -0
- package/dist/{chunk-PUKJP2Z5.js → chunk-TXFWSU5D.js} +31 -8
- package/dist/chunk-TXFWSU5D.js.map +1 -0
- package/dist/{chunk-76ZBTRG2.cjs → chunk-U4EQTDGN.cjs} +258 -218
- package/dist/chunk-U4EQTDGN.cjs.map +1 -0
- package/dist/chunk-UGLFDG7L.cjs.map +1 -1
- package/dist/{chunk-HYJJ2V2I.js → chunk-UTE5A36R.js} +483 -819
- package/dist/chunk-UTE5A36R.js.map +1 -0
- package/dist/{chunk-NFJ4P6ZZ.js → chunk-UZKQISRF.js} +17 -9
- package/dist/chunk-UZKQISRF.js.map +1 -0
- package/dist/{chunk-7KG3RTYS.js → chunk-VL5NXVCN.js} +281 -155
- package/dist/chunk-VL5NXVCN.js.map +1 -0
- package/dist/{chunk-CGBADIO5.cjs → chunk-WOMCXN2W.cjs} +328 -202
- package/dist/chunk-WOMCXN2W.cjs.map +1 -0
- package/dist/{chunk-3R3QVKY5.cjs → chunk-YDGMWH43.cjs} +397 -324
- package/dist/chunk-YDGMWH43.cjs.map +1 -0
- package/dist/{chunk-AGOVW7U6.js → chunk-YYVXRV3V.js} +72 -56
- package/dist/chunk-YYVXRV3V.js.map +1 -0
- package/dist/{chunk-M5RYMAFI.js → chunk-ZLNSH4UL.js} +71 -31
- package/dist/chunk-ZLNSH4UL.js.map +1 -0
- package/dist/{coerce-DRERzdve.d.cts → coerce-CUhHc3Tu.d.ts} +16 -8
- package/dist/{coerce-DP0vYu-x.d.ts → coerce-Ytt2Eiq9.d.cts} +16 -8
- package/dist/core/index.cjs +5 -5
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +9 -8
- package/dist/core/index.d.ts +9 -8
- package/dist/core/index.js +4 -4
- package/dist/generate/index.cjs +20 -8
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +105 -17
- package/dist/generate/index.d.ts +105 -17
- package/dist/generate/index.js +19 -7
- package/dist/{index-CL4b4gUQ.d.ts → index-5VJgKz_Q.d.cts} +6 -2
- package/dist/{index-B9QLziDe.d.cts → index-BLuDesVG.d.ts} +20 -13
- package/dist/{index-BHIUyjhZ.d.ts → index-BV6APVa6.d.cts} +20 -13
- package/dist/{index-BMweUlm5.d.cts → index-Bf0Sv8a9.d.ts} +6 -2
- package/dist/{index-Dw2DP7vm.d.ts → index-BkT21EMv.d.cts} +3 -2
- package/dist/{index-BL5N2Oyz.d.cts → index-C7KCaTRz.d.ts} +3 -2
- package/dist/{index-Cy15oTvh.d.ts → index-C9MYbuKS.d.cts} +7 -3
- package/dist/{index-Cy15oTvh.d.cts → index-C9MYbuKS.d.ts} +7 -3
- package/dist/{index-BNSM-DSk.d.cts → index-DVInzCDS.d.cts} +533 -28
- package/dist/{index-9eoQs2C1.d.ts → index-Dg-SAfhz.d.ts} +6 -4
- package/dist/{index-BJgP97N1.d.ts → index-jgeug9EA.d.ts} +533 -28
- package/dist/{index-cHvbYgJI.d.cts → index-pQihL4z7.d.cts} +6 -4
- package/dist/index.cjs +49 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -13
- package/dist/index.d.ts +13 -13
- package/dist/index.js +55 -19
- package/dist/{masks-Cc7-MzzY.d.ts → masks-BnzXnBj0.d.cts} +3 -2
- package/dist/{masks-pADfTOiD.d.cts → masks-tycmkrgs.d.ts} +3 -2
- package/dist/model/index.cjs +12 -12
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +259 -48
- package/dist/model/index.d.ts +259 -48
- package/dist/model/index.js +11 -11
- package/dist/{positional-DJgs6ltD.d.ts → positional-CscpxtlE.d.cts} +3 -2
- package/dist/{positional-DMZer-FM.d.cts → positional-DLnfUgqG.d.ts} +3 -2
- package/dist/{index-BRMbPkHE.d.ts → profile-B4WqcM4H.d.ts} +85 -610
- package/dist/{index-DGCNKAUr.d.cts → profile-BJd1XkPB.d.cts} +85 -610
- package/dist/theory/index.cjs +5 -5
- package/dist/theory/index.cjs.map +1 -1
- package/dist/theory/index.d.cts +18 -14
- package/dist/theory/index.d.ts +18 -14
- package/dist/theory/index.js +4 -4
- package/dist/{index-CJBBiAmU.d.ts → types-7mGFwvMm.d.ts} +1210 -31
- package/dist/{index-BBWNZS0f.d.cts → types-_ofpb2be.d.cts} +1210 -31
- package/package.json +13 -13
- package/dist/chunk-35NBV2FT.cjs.map +0 -1
- package/dist/chunk-3R3QVKY5.cjs.map +0 -1
- package/dist/chunk-76ZBTRG2.cjs.map +0 -1
- package/dist/chunk-7KG3RTYS.js.map +0 -1
- package/dist/chunk-AGOVW7U6.js.map +0 -1
- package/dist/chunk-CGBADIO5.cjs.map +0 -1
- package/dist/chunk-DSMJRXCZ.cjs.map +0 -1
- package/dist/chunk-FBH75DAY.js.map +0 -1
- package/dist/chunk-H2KJHBW4.js.map +0 -1
- package/dist/chunk-HPVSH7SF.cjs.map +0 -1
- package/dist/chunk-HWHGOVO6.cjs.map +0 -1
- package/dist/chunk-HYJJ2V2I.js.map +0 -1
- package/dist/chunk-J7FHVZII.cjs.map +0 -1
- package/dist/chunk-M5RYMAFI.js.map +0 -1
- package/dist/chunk-NB25P3EH.js.map +0 -1
- package/dist/chunk-NFJ4P6ZZ.js.map +0 -1
- package/dist/chunk-NM7UAMC3.cjs.map +0 -1
- package/dist/chunk-PM4XJE2Y.cjs.map +0 -1
- package/dist/chunk-PUKJP2Z5.js.map +0 -1
- package/dist/chunk-Q6LFSAKV.js.map +0 -1
- package/dist/chunk-RFIBF2RR.js.map +0 -1
- package/dist/chunk-S5BKZZGM.cjs.map +0 -1
- package/dist/chunk-SGLEQVJH.cjs +0 -3329
- package/dist/chunk-SGLEQVJH.cjs.map +0 -1
- package/dist/chunk-UMWZ4ZET.js.map +0 -1
|
@@ -1,47 +1,72 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
REFERENCE_PROFILE_VERSION,
|
|
3
3
|
adjacent,
|
|
4
|
+
analyzeRhythm,
|
|
4
5
|
analyzeTimeline,
|
|
5
6
|
analyzeVoice,
|
|
6
7
|
assertChordTimeline,
|
|
8
|
+
assertReferenceProfile,
|
|
9
|
+
assertSpansInOrder,
|
|
7
10
|
attachPivots,
|
|
8
11
|
barGridStart,
|
|
12
|
+
barSpanOf,
|
|
13
|
+
cadenceSimilarity,
|
|
9
14
|
chordTimelineFromNotes,
|
|
15
|
+
chordToRoman,
|
|
10
16
|
clamp01,
|
|
11
17
|
combineEvidence,
|
|
12
|
-
|
|
18
|
+
contourSimilarity,
|
|
19
|
+
densitySimilarity,
|
|
13
20
|
detectCadences,
|
|
21
|
+
detectModulations,
|
|
14
22
|
dirtySlotsFor,
|
|
23
|
+
durationSimilarity,
|
|
24
|
+
extractMotifs,
|
|
15
25
|
firstSoundingBeat,
|
|
16
26
|
floorMod,
|
|
17
27
|
functionOf,
|
|
28
|
+
functionSimilarity,
|
|
18
29
|
gridForNotes,
|
|
19
30
|
harmonicNovelty,
|
|
20
31
|
hasEnded,
|
|
21
|
-
isAppliedDominant,
|
|
22
|
-
isCadentialSixFour,
|
|
23
32
|
keyLookup,
|
|
24
|
-
|
|
33
|
+
keyPlanSimilarity,
|
|
25
34
|
keyTimelineFromNotes,
|
|
26
35
|
lastBarOf,
|
|
36
|
+
latestSoundingEnd,
|
|
37
|
+
melodicContour,
|
|
38
|
+
melodicSimilarity,
|
|
39
|
+
motifGraph,
|
|
40
|
+
motifStructureSimilarity,
|
|
41
|
+
onsetSimilarity,
|
|
42
|
+
orderedNotes,
|
|
43
|
+
phraseLengthSimilarity,
|
|
44
|
+
phraseMelody,
|
|
27
45
|
prevailingKeyOf,
|
|
46
|
+
progressionSimilarity,
|
|
47
|
+
reduceProgression,
|
|
48
|
+
registerSimilarity,
|
|
49
|
+
resolveSpanEnd,
|
|
50
|
+
rhythmOver,
|
|
28
51
|
sameInstant,
|
|
52
|
+
sectionSequenceSimilarity,
|
|
29
53
|
segmentStartingAt,
|
|
30
54
|
sliceBars,
|
|
55
|
+
statedKeyRegions,
|
|
56
|
+
surfaceSimilarity,
|
|
57
|
+
syncopationSimilarity,
|
|
31
58
|
weightSimilarity,
|
|
59
|
+
weightedMean,
|
|
32
60
|
windowWeights
|
|
33
|
-
} from "./chunk-
|
|
61
|
+
} from "./chunk-FJFULJZE.js";
|
|
34
62
|
import {
|
|
35
63
|
PROFILE_WEIGHTS,
|
|
36
64
|
chordPitchClasses,
|
|
37
65
|
evaluateSafety,
|
|
38
|
-
isDominantChordOf,
|
|
39
|
-
keySignatureFifths,
|
|
40
66
|
majorKey,
|
|
41
67
|
resolveKey,
|
|
42
|
-
scaleOf
|
|
43
|
-
|
|
44
|
-
} from "./chunk-RFIBF2RR.js";
|
|
68
|
+
scaleOf
|
|
69
|
+
} from "./chunk-H7FVMBUC.js";
|
|
45
70
|
import {
|
|
46
71
|
BEAT_EPS,
|
|
47
72
|
InvalidInputError,
|
|
@@ -61,11 +86,9 @@ import {
|
|
|
61
86
|
beatsPerBarAt,
|
|
62
87
|
describeRejected,
|
|
63
88
|
isStrongBeat,
|
|
64
|
-
midiToNote,
|
|
65
89
|
pitchClassOf,
|
|
66
|
-
resolveMeters
|
|
67
|
-
|
|
68
|
-
} from "./chunk-AGOVW7U6.js";
|
|
90
|
+
resolveMeters
|
|
91
|
+
} from "./chunk-YYVXRV3V.js";
|
|
69
92
|
|
|
70
93
|
// src/analyze/arrange/internal.ts
|
|
71
94
|
var MAX_LANE_LEAP = 24;
|
|
@@ -98,7 +121,7 @@ function assertTrackNotes(tracks, options) {
|
|
|
98
121
|
const notes = tracks[index]?.notes;
|
|
99
122
|
if (!Array.isArray(notes)) {
|
|
100
123
|
throw new InvalidInputError(
|
|
101
|
-
`tracks[${index}].notes must be an array; received ${
|
|
124
|
+
`tracks[${index}].notes must be an array; received ${describeRejected(notes)}`
|
|
102
125
|
);
|
|
103
126
|
}
|
|
104
127
|
assertNoteEvents(notes, `tracks[${index}].notes`, options);
|
|
@@ -136,6 +159,7 @@ function assertGivenKeys(keys, name) {
|
|
|
136
159
|
}
|
|
137
160
|
resolveKey(region.key);
|
|
138
161
|
}
|
|
162
|
+
assertSpansInOrder(keys, name);
|
|
139
163
|
}
|
|
140
164
|
function harmonyTrackSet(harmonyTracks, tracks, name = "harmonyTracks") {
|
|
141
165
|
if (harmonyTracks === void 0) {
|
|
@@ -321,7 +345,12 @@ function lastStartingAtOrBefore(sounding, beat) {
|
|
|
321
345
|
function prepareTracks(tracks) {
|
|
322
346
|
let nextId = 0;
|
|
323
347
|
return tracks.map((track, index) => {
|
|
324
|
-
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
|
+
}));
|
|
325
354
|
const voices = splitIntoSubVoices(ordered).map((voice) => {
|
|
326
355
|
const sounding = voice.map((note, i) => {
|
|
327
356
|
const prev = i > 0 ? voice[i - 1] : void 0;
|
|
@@ -362,15 +391,16 @@ function withGivenKeys(result, given) {
|
|
|
362
391
|
return { ...result, keys: given, prevailingKey: prevailingKeyOf(given) ?? result.prevailingKey };
|
|
363
392
|
}
|
|
364
393
|
function callerTimeline(timeline, key, given, pooled, meters, budget) {
|
|
365
|
-
const chartEnd = timeline.segments.reduce(
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
0
|
|
394
|
+
const chartEnd = timeline.segments.reduce(
|
|
395
|
+
(end, segment) => Math.max(end, segment.endBeat),
|
|
396
|
+
Number.NEGATIVE_INFINITY
|
|
369
397
|
);
|
|
370
|
-
const
|
|
398
|
+
const soundingEnd = latestSoundingEnd(pooled, Number.NEGATIVE_INFINITY);
|
|
399
|
+
const spanEnd = Math.max(chartEnd, soundingEnd);
|
|
400
|
+
const totalBeats = Number.isFinite(spanEnd) ? spanEnd : 0;
|
|
371
401
|
const { startBeat: musicStart } = gridForNotes(pooled, beatsPerBarAt(0, meters));
|
|
372
402
|
const keys = given ?? attachPivots(
|
|
373
|
-
key !== void 0 ?
|
|
403
|
+
key !== void 0 ? statedKeyRegions(resolveKey(key), musicStart, totalBeats) : keyTimelineFromNotes(pooled, { meters, totalBeats, budget }),
|
|
374
404
|
timeline.segments
|
|
375
405
|
);
|
|
376
406
|
return {
|
|
@@ -721,7 +751,7 @@ function sessionOf(tracks, opts, analysis, evidence) {
|
|
|
721
751
|
}
|
|
722
752
|
if (!Array.isArray(edit.notes)) {
|
|
723
753
|
throw new InvalidInputError(
|
|
724
|
-
`edits[${index}].notes must be an array; received ${
|
|
754
|
+
`edits[${index}].notes must be an array; received ${describeRejected(edit.notes)}`
|
|
725
755
|
);
|
|
726
756
|
}
|
|
727
757
|
assertNoteEvents(edit.notes, `edits[${index}].notes`, {
|
|
@@ -756,6 +786,7 @@ var FUNCTION_WEIGHT = 0.5;
|
|
|
756
786
|
var DISSONANCE_WEIGHT = 0.35;
|
|
757
787
|
var SPAN_WEIGHT = 0.15;
|
|
758
788
|
var SPAN_SATURATION = 24;
|
|
789
|
+
var MAX_DISTINCT_PITCHES = 128;
|
|
759
790
|
function clamp012(value) {
|
|
760
791
|
return Math.min(1, Math.max(0, value));
|
|
761
792
|
}
|
|
@@ -787,19 +818,14 @@ function tensionCurve(tracks, opts = {}) {
|
|
|
787
818
|
const harmonyTracks = harmonyTrackSet(opts.harmonyTracks, tracks);
|
|
788
819
|
const pooled = poolNotes(tracks, harmonyTracks);
|
|
789
820
|
const all = poolNotes(tracks);
|
|
790
|
-
const totalBeats = all
|
|
821
|
+
const totalBeats = latestSoundingEnd(all);
|
|
791
822
|
const { origin: sampleStart, startBeat: musicStart } = gridForNotes(all, step);
|
|
792
|
-
const suppliedKeys = opts.timeline === void 0 ? void 0 : opts.keys ?? (opts.key !== void 0 ?
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
endBeat: totalBeats,
|
|
799
|
-
key: resolveKey(opts.key),
|
|
800
|
-
confidence: 1
|
|
801
|
-
}
|
|
802
|
-
] : 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 }));
|
|
803
829
|
const { timeline, keys, prevailingKey } = opts.timeline === void 0 || suppliedKeys === void 0 ? chordTimelineFromNotes(pooled, {
|
|
804
830
|
key: opts.key,
|
|
805
831
|
meters,
|
|
@@ -819,7 +845,7 @@ function tensionCurve(tracks, opts = {}) {
|
|
|
819
845
|
assertGenerationBudget(sampleCount, "tension samples", budget);
|
|
820
846
|
const voiceCount = prepared.reduce((sum, track) => sum + track.voices.length, 0);
|
|
821
847
|
assertGenerationBudget(
|
|
822
|
-
sampleCount * Math.min(
|
|
848
|
+
sampleCount * Math.min(MAX_DISTINCT_PITCHES, voiceCount) * voiceCount,
|
|
823
849
|
"tension sample-voice comparisons",
|
|
824
850
|
budget
|
|
825
851
|
);
|
|
@@ -827,9 +853,8 @@ function tensionCurve(tracks, opts = {}) {
|
|
|
827
853
|
const beat = i === 0 ? Math.max(sampleStart, musicStart) : sampleStart + i * step;
|
|
828
854
|
points.push({
|
|
829
855
|
beat,
|
|
830
|
-
// The
|
|
831
|
-
|
|
832
|
-
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)
|
|
833
858
|
});
|
|
834
859
|
}
|
|
835
860
|
return points;
|
|
@@ -900,7 +925,7 @@ function sampleTension(prepared, timeline, key, meters, profile, beat) {
|
|
|
900
925
|
others.pop();
|
|
901
926
|
}
|
|
902
927
|
const result = evaluateSafety(
|
|
903
|
-
{ profile, candidatePitch: pitch, chord, key, otherVoices: others, strongBeat },
|
|
928
|
+
{ profile, candidatePitch: pitch, chord, key: key.scale, otherVoices: others, strongBeat },
|
|
904
929
|
{ suggestions: false }
|
|
905
930
|
);
|
|
906
931
|
if (held !== void 0 && tail !== void 0) {
|
|
@@ -926,19 +951,34 @@ function sampleTension(prepared, timeline, key, meters, profile, beat) {
|
|
|
926
951
|
|
|
927
952
|
// src/analyze/form/hypermeter.ts
|
|
928
953
|
var GROUP_CANDIDATES = [2, 3, 4, 6, 8];
|
|
929
|
-
var
|
|
954
|
+
var GROUP_PRIOR_LIST = [
|
|
930
955
|
[2, 0.9],
|
|
931
956
|
[3, 0.75],
|
|
932
957
|
[4, 1],
|
|
933
958
|
[6, 0.7],
|
|
934
959
|
[8, 0.85]
|
|
935
|
-
]
|
|
960
|
+
];
|
|
961
|
+
var GROUP_PRIOR = new Map(GROUP_PRIOR_LIST);
|
|
936
962
|
var CONTRAST_SHARE = 0.65;
|
|
937
963
|
var CADENCE_SHARE = 0.35;
|
|
938
964
|
var GROUP_FINAL_FIT = 1;
|
|
939
965
|
var ELIDED_FIT = 0.5;
|
|
940
966
|
var MIN_GROUPS = 2;
|
|
941
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
|
+
}
|
|
942
982
|
function accentContrast(novelty, slices, groupBars, phase) {
|
|
943
983
|
let headSum = 0;
|
|
944
984
|
let headCount = 0;
|
|
@@ -963,22 +1003,41 @@ function accentContrast(novelty, slices, groupBars, phase) {
|
|
|
963
1003
|
}
|
|
964
1004
|
return headSum / headCount - otherSum / otherCount;
|
|
965
1005
|
}
|
|
966
|
-
function cadenceFitOf(
|
|
1006
|
+
function cadenceFitOf(cadences, slices, groupBars, phase) {
|
|
967
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
|
+
}
|
|
968
1024
|
let ends = 0;
|
|
969
|
-
let
|
|
1025
|
+
let closed = 0;
|
|
1026
|
+
let weighted = 0;
|
|
970
1027
|
for (const slice of slices) {
|
|
971
1028
|
if (slice.index < 0 || floorMod(slice.index - phase, groupBars) !== groupBars - 1) {
|
|
972
1029
|
continue;
|
|
973
1030
|
}
|
|
974
1031
|
ends += 1;
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
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));
|
|
980
1039
|
}
|
|
981
|
-
return ends === 0 ? 0 : clamp01(
|
|
1040
|
+
return ends === 0 ? { closed: 0, weighted: 0 } : { closed: clamp01(closed / ends), weighted: clamp01(weighted / ends) };
|
|
982
1041
|
}
|
|
983
1042
|
function undecidedReading(slices) {
|
|
984
1043
|
const fullBars = slices.filter((slice) => slice.index >= 0);
|
|
@@ -992,6 +1051,17 @@ function undecidedReading(slices) {
|
|
|
992
1051
|
};
|
|
993
1052
|
}
|
|
994
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) {
|
|
995
1065
|
const asked = assertOptions(opts, "opts");
|
|
996
1066
|
const meters = resolveMeters({ meters: meter }, "meter");
|
|
997
1067
|
assertNoteEvents(notes, "hypermeter notes", {
|
|
@@ -1000,10 +1070,16 @@ function hypermeter(notes, meter, opts = {}) {
|
|
|
1000
1070
|
});
|
|
1001
1071
|
const sounding = notes.filter((note) => note.durationBeat > 0);
|
|
1002
1072
|
const firstOnset = firstSoundingBeat(sounding);
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1005
|
-
|
|
1073
|
+
const spanEnd = resolveSpanEnd(
|
|
1074
|
+
asked.totalBeats,
|
|
1075
|
+
firstOnset,
|
|
1076
|
+
latestSoundingEnd(sounding),
|
|
1077
|
+
"hypermeter totalBeats"
|
|
1078
|
+
);
|
|
1006
1079
|
const spanStart = barGridStart(firstOnset, meters);
|
|
1080
|
+
if (sounding.length === 0 || spanEnd <= spanStart + BEAT_EPS) {
|
|
1081
|
+
return undecidedReading([]);
|
|
1082
|
+
}
|
|
1007
1083
|
const slices = sliceBars(sounding, meters, spanStart, spanEnd, asked.budget);
|
|
1008
1084
|
const fullBars = slices.reduce((count, slice) => slice.index >= 0 ? count + 1 : count, 0);
|
|
1009
1085
|
const candidates = GROUP_CANDIDATES.filter((groupBars) => fullBars >= groupBars * MIN_GROUPS);
|
|
@@ -1016,20 +1092,21 @@ function hypermeter(notes, meter, opts = {}) {
|
|
|
1016
1092
|
asked.budget
|
|
1017
1093
|
);
|
|
1018
1094
|
const novelty = slices.map((_, index) => harmonicNovelty(slices, index));
|
|
1019
|
-
const cadenceBars =
|
|
1020
|
-
const hasCadences = cadenceBars.
|
|
1095
|
+
const cadenceBars = cadenceBarsOf(spansOf(asked), meters);
|
|
1096
|
+
const hasCadences = cadenceBars.length > 0;
|
|
1021
1097
|
const readings = [];
|
|
1022
1098
|
for (const groupBars of candidates) {
|
|
1023
1099
|
const phases = Array.from({ length: groupBars }, (_, phase) => ({
|
|
1024
1100
|
phase,
|
|
1025
1101
|
contrast: accentContrast(novelty, slices, groupBars, phase),
|
|
1026
|
-
|
|
1102
|
+
fit: cadenceFitOf(cadenceBars, slices, groupBars, phase)
|
|
1027
1103
|
}));
|
|
1028
|
-
const bestFit = phases.reduce((best2, reading) => Math.max(best2, reading.
|
|
1029
|
-
for (const { phase, contrast,
|
|
1030
|
-
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) {
|
|
1031
1107
|
continue;
|
|
1032
1108
|
}
|
|
1109
|
+
const cadenceFit = fit.closed;
|
|
1033
1110
|
const evidence = clamp01(
|
|
1034
1111
|
hasCadences ? (
|
|
1035
1112
|
// Floored at zero rather than summed raw: group heads that restate
|
|
@@ -1074,549 +1151,6 @@ function hypermeter(notes, meter, opts = {}) {
|
|
|
1074
1151
|
};
|
|
1075
1152
|
}
|
|
1076
1153
|
|
|
1077
|
-
// src/analyze/melody/internal.ts
|
|
1078
|
-
var RHYTHM_GRID = 64;
|
|
1079
|
-
function phraseNotes(phrase) {
|
|
1080
|
-
if (Array.isArray(phrase)) {
|
|
1081
|
-
return phrase;
|
|
1082
|
-
}
|
|
1083
|
-
return assertRecord(phrase, "melodic phrase").notes;
|
|
1084
|
-
}
|
|
1085
|
-
function orderedNotes(notes, name, budget) {
|
|
1086
|
-
assertNoteEvents(notes, name, { allowNonPositiveDuration: true, budget });
|
|
1087
|
-
const sounding = notes.filter((note) => note.durationBeat > 0).sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);
|
|
1088
|
-
const line = [];
|
|
1089
|
-
for (const note of sounding) {
|
|
1090
|
-
const previous = line[line.length - 1];
|
|
1091
|
-
if (previous !== void 0 && Math.abs(note.startBeat - previous.startBeat) <= BEAT_EPS) {
|
|
1092
|
-
line[line.length - 1] = note;
|
|
1093
|
-
continue;
|
|
1094
|
-
}
|
|
1095
|
-
line.push(note);
|
|
1096
|
-
}
|
|
1097
|
-
return line;
|
|
1098
|
-
}
|
|
1099
|
-
function intervalsOf(notes) {
|
|
1100
|
-
const out = [];
|
|
1101
|
-
for (let i = 1; i < notes.length; i += 1) {
|
|
1102
|
-
out.push((notes[i]?.pitch ?? 0) - (notes[i - 1]?.pitch ?? 0));
|
|
1103
|
-
}
|
|
1104
|
-
return out;
|
|
1105
|
-
}
|
|
1106
|
-
function onsetGaps(notes) {
|
|
1107
|
-
const out = [];
|
|
1108
|
-
for (let i = 1; i < notes.length; i += 1) {
|
|
1109
|
-
out.push((notes[i]?.startBeat ?? 0) - (notes[i - 1]?.startBeat ?? 0));
|
|
1110
|
-
}
|
|
1111
|
-
return out;
|
|
1112
|
-
}
|
|
1113
|
-
function retrogradeGaps(notes) {
|
|
1114
|
-
const out = [];
|
|
1115
|
-
for (let i = notes.length - 1; i >= 1; i -= 1) {
|
|
1116
|
-
const later = notes[i];
|
|
1117
|
-
const earlier = notes[i - 1];
|
|
1118
|
-
out.push(
|
|
1119
|
-
(later?.startBeat ?? 0) + (later?.durationBeat ?? 0) - ((earlier?.startBeat ?? 0) + (earlier?.durationBeat ?? 0))
|
|
1120
|
-
);
|
|
1121
|
-
}
|
|
1122
|
-
return out;
|
|
1123
|
-
}
|
|
1124
|
-
function rhythmProfile(gaps) {
|
|
1125
|
-
const unit = gaps[0] ?? 0;
|
|
1126
|
-
if (!(unit > BEAT_EPS)) {
|
|
1127
|
-
return gaps.length === 0 ? [] : null;
|
|
1128
|
-
}
|
|
1129
|
-
return gaps.map((gap) => Math.round(gap / unit * RHYTHM_GRID) / RHYTHM_GRID);
|
|
1130
|
-
}
|
|
1131
|
-
function sameNumbers(a, b) {
|
|
1132
|
-
if (a.length !== b.length) {
|
|
1133
|
-
return false;
|
|
1134
|
-
}
|
|
1135
|
-
for (let i = 0; i < a.length; i += 1) {
|
|
1136
|
-
if (Math.abs((a[i] ?? 0) - (b[i] ?? 0)) > BEAT_EPS) {
|
|
1137
|
-
return false;
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
return true;
|
|
1141
|
-
}
|
|
1142
|
-
function roundTo(value, digits) {
|
|
1143
|
-
const scale = 10 ** digits;
|
|
1144
|
-
return Math.round(value * scale) / scale;
|
|
1145
|
-
}
|
|
1146
|
-
function endBeatOf(notes) {
|
|
1147
|
-
let end = 0;
|
|
1148
|
-
for (const note of notes) {
|
|
1149
|
-
end = Math.max(end, note.startBeat + note.durationBeat);
|
|
1150
|
-
}
|
|
1151
|
-
return end;
|
|
1152
|
-
}
|
|
1153
|
-
function onsetSpan(notes) {
|
|
1154
|
-
if (notes.length < 2) {
|
|
1155
|
-
return 0;
|
|
1156
|
-
}
|
|
1157
|
-
return (notes[notes.length - 1]?.startBeat ?? 0) - (notes[0]?.startBeat ?? 0);
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
// src/analyze/melody/contour.ts
|
|
1161
|
-
var DIRECTIONAL_SHARE = 0.75;
|
|
1162
|
-
function extremes(notes) {
|
|
1163
|
-
if (notes.length === 0) {
|
|
1164
|
-
return { peak: -1, trough: -1 };
|
|
1165
|
-
}
|
|
1166
|
-
let peak = 0;
|
|
1167
|
-
let trough = 0;
|
|
1168
|
-
for (let i = 1; i < notes.length; i += 1) {
|
|
1169
|
-
if ((notes[i]?.pitch ?? 0) > (notes[peak]?.pitch ?? 0)) {
|
|
1170
|
-
peak = i;
|
|
1171
|
-
}
|
|
1172
|
-
if ((notes[i]?.pitch ?? 0) < (notes[trough]?.pitch ?? 0)) {
|
|
1173
|
-
trough = i;
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
return { peak, trough };
|
|
1177
|
-
}
|
|
1178
|
-
function melodicContour(notes) {
|
|
1179
|
-
const line = orderedNotes(phraseNotes(notes), "contour notes");
|
|
1180
|
-
const intervals = intervalsOf(line);
|
|
1181
|
-
const directions = intervals.map(
|
|
1182
|
-
(step) => step > 0 ? "up" : step < 0 ? "down" : "same"
|
|
1183
|
-
);
|
|
1184
|
-
const { peak, trough } = extremes(line);
|
|
1185
|
-
const range = (line[peak]?.pitch ?? 0) - (line[trough]?.pitch ?? 0);
|
|
1186
|
-
const motion = intervals.reduce((sum, step) => sum + Math.abs(step), 0);
|
|
1187
|
-
if (motion === 0) {
|
|
1188
|
-
return {
|
|
1189
|
-
directions,
|
|
1190
|
-
shape: "static",
|
|
1191
|
-
peakIndex: peak,
|
|
1192
|
-
troughIndex: trough,
|
|
1193
|
-
range: 0,
|
|
1194
|
-
rationale: line.length < 2 ? "Too short to have a direction, so the line is static" : "Every note at the same pitch, so the line is static"
|
|
1195
|
-
};
|
|
1196
|
-
}
|
|
1197
|
-
const first = line[0]?.pitch ?? 0;
|
|
1198
|
-
const last = line[line.length - 1]?.pitch ?? 0;
|
|
1199
|
-
const net = last - first;
|
|
1200
|
-
const directness = Math.abs(net) / motion;
|
|
1201
|
-
const share = Math.round(directness * 100);
|
|
1202
|
-
if (directness >= DIRECTIONAL_SHARE) {
|
|
1203
|
-
const shape = net > 0 ? "ascending" : "descending";
|
|
1204
|
-
return {
|
|
1205
|
-
directions,
|
|
1206
|
-
shape,
|
|
1207
|
-
peakIndex: peak,
|
|
1208
|
-
troughIndex: trough,
|
|
1209
|
-
range,
|
|
1210
|
-
rationale: `${share}% of the motion runs ${net > 0 ? "up" : "down"}, so the line is ${shape}`
|
|
1211
|
-
};
|
|
1212
|
-
}
|
|
1213
|
-
const peakPitch = line[peak]?.pitch ?? 0;
|
|
1214
|
-
const archMotion = peakPitch - first + (peakPitch - last);
|
|
1215
|
-
if (peak > 0 && peak < line.length - 1 && archMotion >= DIRECTIONAL_SHARE * motion) {
|
|
1216
|
-
return {
|
|
1217
|
-
directions,
|
|
1218
|
-
shape: "arch",
|
|
1219
|
-
peakIndex: peak,
|
|
1220
|
-
troughIndex: trough,
|
|
1221
|
-
range,
|
|
1222
|
-
rationale: `Rises to its peak at note ${peak + 1} of ${line.length} and falls back, so the line is an arch`
|
|
1223
|
-
};
|
|
1224
|
-
}
|
|
1225
|
-
return {
|
|
1226
|
-
directions,
|
|
1227
|
-
shape: "wave",
|
|
1228
|
-
peakIndex: peak,
|
|
1229
|
-
troughIndex: trough,
|
|
1230
|
-
range,
|
|
1231
|
-
rationale: `Changes direction without settling either way (${share}% of the motion nets out), so the line is a wave`
|
|
1232
|
-
};
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
// src/analyze/melody/motifs.ts
|
|
1236
|
-
var MIN_CELL_NOTES = 3;
|
|
1237
|
-
var MAX_CELL_NOTES = 8;
|
|
1238
|
-
var MIN_OCCURRENCES = 2;
|
|
1239
|
-
function windowSignature(notes) {
|
|
1240
|
-
const profile = rhythmProfile(onsetGaps(notes));
|
|
1241
|
-
if (profile === null) {
|
|
1242
|
-
return null;
|
|
1243
|
-
}
|
|
1244
|
-
return `${notes.length}|${intervalsOf(notes).join(",")}|${profile.join(",")}`;
|
|
1245
|
-
}
|
|
1246
|
-
function nonOverlapping(starts, length) {
|
|
1247
|
-
const picked = [];
|
|
1248
|
-
let lastEnd = Number.NEGATIVE_INFINITY;
|
|
1249
|
-
for (const start of starts) {
|
|
1250
|
-
if (start > lastEnd) {
|
|
1251
|
-
picked.push(start);
|
|
1252
|
-
lastEnd = start + length - 1;
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
return picked;
|
|
1256
|
-
}
|
|
1257
|
-
function subsumes(outer, inner) {
|
|
1258
|
-
let at = 0;
|
|
1259
|
-
for (const start of inner.starts) {
|
|
1260
|
-
while (at + 1 < outer.starts.length && (outer.starts[at + 1] ?? 0) <= start) {
|
|
1261
|
-
at += 1;
|
|
1262
|
-
}
|
|
1263
|
-
const from = outer.starts[at];
|
|
1264
|
-
if (from === void 0 || start < from || start + inner.length > from + outer.length) {
|
|
1265
|
-
return false;
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
return true;
|
|
1269
|
-
}
|
|
1270
|
-
function listBeats(occurrences) {
|
|
1271
|
-
const shown = occurrences.slice(0, 5).map((occurrence) => roundTo(occurrence.startBeat, 3));
|
|
1272
|
-
return occurrences.length > shown.length ? `${shown.join(", ")}, and ${occurrences.length - shown.length} more` : shown.join(", ");
|
|
1273
|
-
}
|
|
1274
|
-
function motifFromGroup(group, notes) {
|
|
1275
|
-
const first = group.starts[0] ?? 0;
|
|
1276
|
-
const prime = notes.slice(first, first + group.length).map((note) => ({ ...note }));
|
|
1277
|
-
const primePitch = prime[0]?.pitch ?? 0;
|
|
1278
|
-
const primeSpan = onsetSpan(prime);
|
|
1279
|
-
const occurrences = group.starts.map((start) => {
|
|
1280
|
-
const statement = notes.slice(start, start + group.length);
|
|
1281
|
-
const span = onsetSpan(statement);
|
|
1282
|
-
return {
|
|
1283
|
-
noteIndex: start,
|
|
1284
|
-
startBeat: statement[0]?.startBeat ?? 0,
|
|
1285
|
-
endBeat: endBeatOf(statement),
|
|
1286
|
-
transpose: (statement[0]?.pitch ?? 0) - primePitch,
|
|
1287
|
-
timeRatio: primeSpan > BEAT_EPS ? span / primeSpan : 1
|
|
1288
|
-
};
|
|
1289
|
-
});
|
|
1290
|
-
const shape = melodicContour(prime).shape;
|
|
1291
|
-
const transposed = occurrences.some((occurrence) => occurrence.transpose !== 0);
|
|
1292
|
-
const stretched = occurrences.some((occurrence) => Math.abs(occurrence.timeRatio - 1) > BEAT_EPS);
|
|
1293
|
-
const detail = [
|
|
1294
|
-
transposed ? "at more than one pitch level" : "at the same pitch level",
|
|
1295
|
-
stretched ? "and in more than one set of note values" : ""
|
|
1296
|
-
].filter((part) => part !== "").join(" ");
|
|
1297
|
-
return {
|
|
1298
|
-
notes: prime,
|
|
1299
|
-
intervals: intervalsOf(prime),
|
|
1300
|
-
rhythm: rhythmProfile(onsetGaps(prime)) ?? [],
|
|
1301
|
-
occurrences,
|
|
1302
|
-
rationale: `${group.length}-note ${shape} cell stated ${occurrences.length} ${occurrences.length === 1 ? "time" : "times"} (beats ${listBeats(occurrences)}), ${detail}`
|
|
1303
|
-
};
|
|
1304
|
-
}
|
|
1305
|
-
function extractMotifs(notes, opts = {}) {
|
|
1306
|
-
const asked = assertOptions(opts, "opts");
|
|
1307
|
-
const minNotes = assertInteger(asked.minNotes ?? MIN_CELL_NOTES, "motif minNotes", 2, 64);
|
|
1308
|
-
const maxNotes = assertInteger(asked.maxNotes ?? MAX_CELL_NOTES, "motif maxNotes", 2, 64);
|
|
1309
|
-
if (maxNotes < minNotes) {
|
|
1310
|
-
throw new InvalidInputError(
|
|
1311
|
-
`motif maxNotes must be at least minNotes ${minNotes}; received ${maxNotes}`
|
|
1312
|
-
);
|
|
1313
|
-
}
|
|
1314
|
-
const minOccurrences = assertInteger(
|
|
1315
|
-
asked.minOccurrences ?? MIN_OCCURRENCES,
|
|
1316
|
-
"motif minOccurrences",
|
|
1317
|
-
2
|
|
1318
|
-
);
|
|
1319
|
-
const sounding = orderedNotes(notes, "melody notes", asked.budget);
|
|
1320
|
-
if (sounding.length < minNotes) {
|
|
1321
|
-
return [];
|
|
1322
|
-
}
|
|
1323
|
-
assertGenerationBudget(
|
|
1324
|
-
sounding.length * (maxNotes - minNotes + 1),
|
|
1325
|
-
"motif windows",
|
|
1326
|
-
asked.budget
|
|
1327
|
-
);
|
|
1328
|
-
const groups = /* @__PURE__ */ new Map();
|
|
1329
|
-
for (let length = minNotes; length <= maxNotes; length += 1) {
|
|
1330
|
-
for (let start = 0; start + length <= sounding.length; start += 1) {
|
|
1331
|
-
const signature = windowSignature(sounding.slice(start, start + length));
|
|
1332
|
-
if (signature === null) {
|
|
1333
|
-
continue;
|
|
1334
|
-
}
|
|
1335
|
-
const group = groups.get(signature);
|
|
1336
|
-
if (group === void 0) {
|
|
1337
|
-
groups.set(signature, { signature, length, starts: [start] });
|
|
1338
|
-
} else {
|
|
1339
|
-
group.starts.push(start);
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
const candidates = [...groups.values()].map((group) => ({ ...group, starts: nonOverlapping(group.starts, group.length) })).filter((group) => group.starts.length >= minOccurrences).sort(
|
|
1344
|
-
(a, b) => b.length - a.length || b.starts.length - a.starts.length || (a.starts[0] ?? 0) - (b.starts[0] ?? 0) || (a.signature < b.signature ? -1 : a.signature > b.signature ? 1 : 0)
|
|
1345
|
-
);
|
|
1346
|
-
const keptByLength = /* @__PURE__ */ new Map();
|
|
1347
|
-
const kept = [];
|
|
1348
|
-
let comparisons = 0;
|
|
1349
|
-
for (const candidate of candidates) {
|
|
1350
|
-
const first = candidate.starts[0] ?? 0;
|
|
1351
|
-
let covered = false;
|
|
1352
|
-
for (let length = candidate.length + 1; length <= maxNotes && !covered; length += 1) {
|
|
1353
|
-
const byStart = keptByLength.get(length);
|
|
1354
|
-
if (byStart === void 0) {
|
|
1355
|
-
continue;
|
|
1356
|
-
}
|
|
1357
|
-
for (let from = Math.max(0, first - (length - candidate.length)); from <= first; from += 1) {
|
|
1358
|
-
comparisons += 1;
|
|
1359
|
-
const outer = byStart.get(from);
|
|
1360
|
-
if (outer === void 0 || outer.starts.length < candidate.starts.length) {
|
|
1361
|
-
continue;
|
|
1362
|
-
}
|
|
1363
|
-
comparisons += outer.starts.length + candidate.starts.length;
|
|
1364
|
-
if (subsumes(outer, candidate)) {
|
|
1365
|
-
covered = true;
|
|
1366
|
-
break;
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1370
|
-
assertGenerationBudget(comparisons, "motif subsumption comparisons", asked.budget);
|
|
1371
|
-
if (!covered) {
|
|
1372
|
-
kept.push(candidate);
|
|
1373
|
-
let byStart = keptByLength.get(candidate.length);
|
|
1374
|
-
if (byStart === void 0) {
|
|
1375
|
-
byStart = /* @__PURE__ */ new Map();
|
|
1376
|
-
keptByLength.set(candidate.length, byStart);
|
|
1377
|
-
}
|
|
1378
|
-
for (const start of candidate.starts) {
|
|
1379
|
-
byStart.set(start, candidate);
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1383
|
-
return kept.map((group) => motifFromGroup(group, sounding));
|
|
1384
|
-
}
|
|
1385
|
-
function motifFromNotes(notes) {
|
|
1386
|
-
const sounding = orderedNotes(notes, "motif notes").map((note) => ({ ...note }));
|
|
1387
|
-
return motifFromGroup(
|
|
1388
|
-
{ signature: windowSignature(sounding) ?? "", length: sounding.length, starts: [0] },
|
|
1389
|
-
sounding
|
|
1390
|
-
);
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
// src/analyze/melody/relation.ts
|
|
1394
|
-
function spellingOf(key) {
|
|
1395
|
-
if (key === void 0) {
|
|
1396
|
-
return "sharp";
|
|
1397
|
-
}
|
|
1398
|
-
return keySignatureFifths(key.tonic, key.scale) < 0 ? "flat" : "sharp";
|
|
1399
|
-
}
|
|
1400
|
-
function intervalBetween(from, to, key) {
|
|
1401
|
-
const spelling = spellingOf(key);
|
|
1402
|
-
return spelledInterval(midiToNote(from, spelling), midiToNote(to, spelling));
|
|
1403
|
-
}
|
|
1404
|
-
function intervalName(interval) {
|
|
1405
|
-
return `${interval.descending && interval.semitones !== 0 ? "-" : ""}${interval.quality}${interval.number}`;
|
|
1406
|
-
}
|
|
1407
|
-
function ladderIndices(notes, key) {
|
|
1408
|
-
const root = pitchClassOf(key.rootPc);
|
|
1409
|
-
const offsets = scaleTonesInDegreeOrder(key).map((pc) => pitchClassOf(pc - root));
|
|
1410
|
-
if (offsets.length === 0) {
|
|
1411
|
-
return null;
|
|
1412
|
-
}
|
|
1413
|
-
const out = [];
|
|
1414
|
-
for (const note of notes) {
|
|
1415
|
-
const semitones = note.pitch - root;
|
|
1416
|
-
const octave = Math.floor(semitones / 12);
|
|
1417
|
-
const degree = offsets.indexOf(semitones - octave * 12);
|
|
1418
|
-
if (degree < 0) {
|
|
1419
|
-
return null;
|
|
1420
|
-
}
|
|
1421
|
-
out.push(octave * offsets.length + degree);
|
|
1422
|
-
}
|
|
1423
|
-
return out;
|
|
1424
|
-
}
|
|
1425
|
-
function ladderSteps(indices) {
|
|
1426
|
-
const out = [];
|
|
1427
|
-
for (let i = 1; i < indices.length; i += 1) {
|
|
1428
|
-
out.push((indices[i] ?? 0) - (indices[i - 1] ?? 0));
|
|
1429
|
-
}
|
|
1430
|
-
return out;
|
|
1431
|
-
}
|
|
1432
|
-
function pitchShapes(a, b) {
|
|
1433
|
-
const shapes = [];
|
|
1434
|
-
if (sameNumbers(a, b)) {
|
|
1435
|
-
shapes.push("exact");
|
|
1436
|
-
}
|
|
1437
|
-
if (sameNumbers(
|
|
1438
|
-
b,
|
|
1439
|
-
a.map((step) => -step)
|
|
1440
|
-
)) {
|
|
1441
|
-
shapes.push("inversion");
|
|
1442
|
-
}
|
|
1443
|
-
const reversed = [...a].reverse();
|
|
1444
|
-
if (sameNumbers(
|
|
1445
|
-
b,
|
|
1446
|
-
reversed.map((step) => -step)
|
|
1447
|
-
)) {
|
|
1448
|
-
shapes.push("retrograde");
|
|
1449
|
-
}
|
|
1450
|
-
if (sameNumbers(b, reversed)) {
|
|
1451
|
-
shapes.push("retrogradeInversion");
|
|
1452
|
-
}
|
|
1453
|
-
return shapes;
|
|
1454
|
-
}
|
|
1455
|
-
function totalGap(gaps) {
|
|
1456
|
-
return gaps.reduce((sum, gap) => sum + gap, 0);
|
|
1457
|
-
}
|
|
1458
|
-
function stretchRatio(modelGaps, answerGaps) {
|
|
1459
|
-
const span = totalGap(modelGaps);
|
|
1460
|
-
return span > BEAT_EPS ? totalGap(answerGaps) / span : 1;
|
|
1461
|
-
}
|
|
1462
|
-
function stretchPhrase(ratio) {
|
|
1463
|
-
return `note values ${roundTo(ratio, 3)}x the model's`;
|
|
1464
|
-
}
|
|
1465
|
-
function relateMotifs(a, b, keyLike) {
|
|
1466
|
-
const key = keyLike === void 0 ? void 0 : resolveKey(keyLike);
|
|
1467
|
-
const model = assertArray(assertRecord(a, "a").notes, "a.notes");
|
|
1468
|
-
const answer = assertArray(assertRecord(b, "b").notes, "b.notes");
|
|
1469
|
-
if (model.length === 0 || model.length !== answer.length) {
|
|
1470
|
-
return null;
|
|
1471
|
-
}
|
|
1472
|
-
const modelGaps = onsetGaps(model);
|
|
1473
|
-
const answerGaps = onsetGaps(answer);
|
|
1474
|
-
const modelProfile = rhythmProfile(modelGaps);
|
|
1475
|
-
const answerProfile = rhythmProfile(answerGaps);
|
|
1476
|
-
const retroGaps = retrogradeGaps(model);
|
|
1477
|
-
const retroProfile = rhythmProfile(retroGaps);
|
|
1478
|
-
if (modelProfile === null || answerProfile === null) {
|
|
1479
|
-
return null;
|
|
1480
|
-
}
|
|
1481
|
-
const forward = sameNumbers(modelProfile, answerProfile);
|
|
1482
|
-
const backward = retroProfile !== null && sameNumbers(retroProfile, answerProfile);
|
|
1483
|
-
const semitones = (answer[0]?.pitch ?? 0) - (model[0]?.pitch ?? 0);
|
|
1484
|
-
const interval = intervalBetween(model[0]?.pitch ?? 0, answer[0]?.pitch ?? 0, key);
|
|
1485
|
-
const gap = (answer[0]?.startBeat ?? 0) - endBeatOf(model);
|
|
1486
|
-
const sequence = gap >= -HUMANIZE_ADJACENCY && gap <= HUMANIZE_ADJACENCY;
|
|
1487
|
-
const timeRatio = stretchRatio(modelGaps, answerGaps);
|
|
1488
|
-
const stretched = Math.abs(timeRatio - 1) > BEAT_EPS;
|
|
1489
|
-
const shapes = pitchShapes(intervalsOf(model), intervalsOf(answer));
|
|
1490
|
-
const base = { sequence, semitones, interval, timeRatio };
|
|
1491
|
-
const backwardBase = { ...base, timeRatio: stretchRatio(retroGaps, answerGaps) };
|
|
1492
|
-
if (forward && shapes.includes("exact")) {
|
|
1493
|
-
if (semitones === 0) {
|
|
1494
|
-
if (!stretched) {
|
|
1495
|
-
return {
|
|
1496
|
-
...base,
|
|
1497
|
-
kind: "repetition",
|
|
1498
|
-
rationale: "Exact repetition: the same pitches in the same rhythm"
|
|
1499
|
-
};
|
|
1500
|
-
}
|
|
1501
|
-
const augmented = timeRatio > 1;
|
|
1502
|
-
return {
|
|
1503
|
-
...base,
|
|
1504
|
-
kind: augmented ? "augmentation" : "diminution",
|
|
1505
|
-
rationale: `${augmented ? "Augmentation" : "Diminution"}: the same pitches in ${stretchPhrase(timeRatio)}`
|
|
1506
|
-
};
|
|
1507
|
-
}
|
|
1508
|
-
const name = intervalName(interval);
|
|
1509
|
-
return {
|
|
1510
|
-
...base,
|
|
1511
|
-
kind: "transposition",
|
|
1512
|
-
rationale: sequence ? `Real sequence: the cell restated ${name} away with every interval kept literally` + (stretched ? `, in ${stretchPhrase(timeRatio)}` : "") : `Transposition by ${name}: every interval kept literally` + (stretched ? `, in ${stretchPhrase(timeRatio)}` : "")
|
|
1513
|
-
};
|
|
1514
|
-
}
|
|
1515
|
-
if (forward && key !== void 0) {
|
|
1516
|
-
const modelLadder = ladderIndices(model, key.scale);
|
|
1517
|
-
const answerLadder = ladderIndices(answer, key.scale);
|
|
1518
|
-
if (modelLadder !== null && answerLadder !== null) {
|
|
1519
|
-
const degrees = (answerLadder[0] ?? 0) - (modelLadder[0] ?? 0);
|
|
1520
|
-
if (degrees !== 0 && sameNumbers(ladderSteps(modelLadder), ladderSteps(answerLadder))) {
|
|
1521
|
-
const direction = degrees > 0 ? "higher" : "lower";
|
|
1522
|
-
const count = Math.abs(degrees);
|
|
1523
|
-
return {
|
|
1524
|
-
...base,
|
|
1525
|
-
kind: "tonalTransposition",
|
|
1526
|
-
degrees,
|
|
1527
|
-
rationale: `${sequence ? "Tonal sequence" : "Tonal transposition"}: the cell restated ${count} scale ${count === 1 ? "degree" : "degrees"} ${direction} inside the key, so the key resizes its intervals \u2014 a real one would have kept them literal`
|
|
1528
|
-
};
|
|
1529
|
-
}
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
if (forward && shapes.includes("inversion")) {
|
|
1533
|
-
return {
|
|
1534
|
-
...base,
|
|
1535
|
-
kind: "inversion",
|
|
1536
|
-
rationale: "Inversion: every interval turned upside down about the first note" + (stretched ? `, in ${stretchPhrase(timeRatio)}` : "")
|
|
1537
|
-
};
|
|
1538
|
-
}
|
|
1539
|
-
if (backward && shapes.includes("retrograde")) {
|
|
1540
|
-
return {
|
|
1541
|
-
...backwardBase,
|
|
1542
|
-
kind: "retrograde",
|
|
1543
|
-
rationale: "Retrograde: the cell read back to front, rhythm included"
|
|
1544
|
-
};
|
|
1545
|
-
}
|
|
1546
|
-
if (backward && shapes.includes("retrogradeInversion")) {
|
|
1547
|
-
return {
|
|
1548
|
-
...backwardBase,
|
|
1549
|
-
kind: "retrogradeInversion",
|
|
1550
|
-
rationale: "Retrograde inversion: the cell read back to front and turned upside down"
|
|
1551
|
-
};
|
|
1552
|
-
}
|
|
1553
|
-
return null;
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
// src/analyze/melody/similarity.ts
|
|
1557
|
-
var INTERVAL_TOLERANCE = 6;
|
|
1558
|
-
var PITCH_WEIGHT = 0.75;
|
|
1559
|
-
var RHYTHM_WEIGHT = 0.25;
|
|
1560
|
-
function gradedEditDistance(a, b, cost) {
|
|
1561
|
-
let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
1562
|
-
for (let i = 1; i <= a.length; i += 1) {
|
|
1563
|
-
const row = new Array(b.length + 1);
|
|
1564
|
-
row[0] = i;
|
|
1565
|
-
for (let j = 1; j <= b.length; j += 1) {
|
|
1566
|
-
const substitution = (previous[j - 1] ?? 0) + cost(a[i - 1] ?? 0, b[j - 1] ?? 0);
|
|
1567
|
-
const deletion = (previous[j] ?? 0) + 1;
|
|
1568
|
-
const insertion = (row[j - 1] ?? 0) + 1;
|
|
1569
|
-
row[j] = Math.min(substitution, deletion, insertion);
|
|
1570
|
-
}
|
|
1571
|
-
previous = row;
|
|
1572
|
-
}
|
|
1573
|
-
return previous[b.length] ?? 0;
|
|
1574
|
-
}
|
|
1575
|
-
function distanceToSimilarity(distance, a, b) {
|
|
1576
|
-
const longest = Math.max(a, b);
|
|
1577
|
-
if (longest === 0) {
|
|
1578
|
-
return 1;
|
|
1579
|
-
}
|
|
1580
|
-
return Math.min(1, Math.max(0, 1 - distance / longest));
|
|
1581
|
-
}
|
|
1582
|
-
function intervalCost(x, y) {
|
|
1583
|
-
return Math.min(1, Math.abs(x - y) / INTERVAL_TOLERANCE);
|
|
1584
|
-
}
|
|
1585
|
-
function gapCost(x, y) {
|
|
1586
|
-
if (!(x > BEAT_EPS) || !(y > BEAT_EPS)) {
|
|
1587
|
-
return Math.abs(x - y) > BEAT_EPS ? 1 : 0;
|
|
1588
|
-
}
|
|
1589
|
-
return Math.min(1, Math.abs(Math.log2(x / y)));
|
|
1590
|
-
}
|
|
1591
|
-
function compareMelodies(a, b) {
|
|
1592
|
-
const left = orderedNotes(phraseNotes(a), "melody a");
|
|
1593
|
-
const right = orderedNotes(phraseNotes(b), "melody b");
|
|
1594
|
-
const leftIntervals = intervalsOf(left);
|
|
1595
|
-
const rightIntervals = intervalsOf(right);
|
|
1596
|
-
const pitchSimilarity = distanceToSimilarity(
|
|
1597
|
-
gradedEditDistance(leftIntervals, rightIntervals, intervalCost),
|
|
1598
|
-
leftIntervals.length,
|
|
1599
|
-
rightIntervals.length
|
|
1600
|
-
);
|
|
1601
|
-
const leftGaps = rhythmProfile(onsetGaps(left)) ?? onsetGaps(left);
|
|
1602
|
-
const rightGaps = rhythmProfile(onsetGaps(right)) ?? onsetGaps(right);
|
|
1603
|
-
const rhythmSimilarity = distanceToSimilarity(
|
|
1604
|
-
gradedEditDistance(leftGaps, rightGaps, gapCost),
|
|
1605
|
-
leftGaps.length,
|
|
1606
|
-
rightGaps.length
|
|
1607
|
-
);
|
|
1608
|
-
const similarity = PITCH_WEIGHT * pitchSimilarity + RHYTHM_WEIGHT * rhythmSimilarity;
|
|
1609
|
-
return {
|
|
1610
|
-
similarity,
|
|
1611
|
-
pitchSimilarity,
|
|
1612
|
-
rhythmSimilarity,
|
|
1613
|
-
rationale: `Interval shapes ${Math.round(pitchSimilarity * 100)}% alike, rhythms ${Math.round(rhythmSimilarity * 100)}% alike, weighted ${PITCH_WEIGHT}/${RHYTHM_WEIGHT} into ${roundTo(similarity, 3)}`
|
|
1614
|
-
};
|
|
1615
|
-
}
|
|
1616
|
-
function melodicSimilarity(a, b) {
|
|
1617
|
-
return compareMelodies(a, b).similarity;
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
1154
|
// src/analyze/form/phrase.ts
|
|
1621
1155
|
var CADENCE_BOUNDARY_STRENGTH = {
|
|
1622
1156
|
authentic: 0.9,
|
|
@@ -1644,6 +1178,7 @@ var PHRASE_CUT_COST = 1.3;
|
|
|
1644
1178
|
var CLOSING_SHARE = 0.65;
|
|
1645
1179
|
var FINAL_PHRASE_BONUS = 0.15;
|
|
1646
1180
|
var HYPERMETRIC_CLOSE_BONUS = 0.1;
|
|
1181
|
+
var IMPERFECT_CLOSURE_FACTOR = 0.8;
|
|
1647
1182
|
var EVADED_FACTOR = 0.6;
|
|
1648
1183
|
var SIGNAL_ORDER = [
|
|
1649
1184
|
"cadence",
|
|
@@ -1669,16 +1204,22 @@ function readBoundary(boundary) {
|
|
|
1669
1204
|
);
|
|
1670
1205
|
return {
|
|
1671
1206
|
strength: combineEvidence(signals.map(([, strength]) => strength)),
|
|
1207
|
+
uncredited: combineEvidence(
|
|
1208
|
+
signals.filter(([signal]) => signal !== "cadence").map(([, strength]) => strength)
|
|
1209
|
+
),
|
|
1672
1210
|
signals: signals.map(([signal]) => signal)
|
|
1673
1211
|
};
|
|
1674
1212
|
}
|
|
1213
|
+
function closesPhrase(atBeat, startBeat, endBeat) {
|
|
1214
|
+
return atBeat >= startBeat - BEAT_EPS && atBeat < endBeat - BEAT_EPS;
|
|
1215
|
+
}
|
|
1675
1216
|
function lengthFit(length, expected) {
|
|
1676
1217
|
if (!(expected > 0)) {
|
|
1677
1218
|
return 0;
|
|
1678
1219
|
}
|
|
1679
1220
|
return Math.max(-1, 1 - Math.abs(length - expected) / expected);
|
|
1680
1221
|
}
|
|
1681
|
-
function choosePhrasePath(beats, strengths, minPhraseBeats, expected) {
|
|
1222
|
+
function choosePhrasePath(beats, strengths, minPhraseBeats, expected, held) {
|
|
1682
1223
|
const value = new Array(beats.length).fill(Number.NEGATIVE_INFINITY);
|
|
1683
1224
|
const cameFrom = new Array(beats.length).fill(-1);
|
|
1684
1225
|
value[0] = 0;
|
|
@@ -1692,7 +1233,9 @@ function choosePhrasePath(beats, strengths, minPhraseBeats, expected) {
|
|
|
1692
1233
|
if (length < minPhraseBeats - BEAT_EPS) {
|
|
1693
1234
|
continue;
|
|
1694
1235
|
}
|
|
1695
|
-
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;
|
|
1696
1239
|
if (gain > (value[j] ?? Number.NEGATIVE_INFINITY)) {
|
|
1697
1240
|
value[j] = gain;
|
|
1698
1241
|
cameFrom[j] = i;
|
|
@@ -1711,32 +1254,35 @@ function choosePhrasePath(beats, strengths, minPhraseBeats, expected) {
|
|
|
1711
1254
|
}
|
|
1712
1255
|
return path;
|
|
1713
1256
|
}
|
|
1714
|
-
function
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
};
|
|
1725
|
-
const bars = lastBar - firstBar + partOf(endBeat, lastBar) - partOf(startBeat, firstBar);
|
|
1726
|
-
return Math.round(bars * 100) / 100;
|
|
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 };
|
|
1727
1267
|
}
|
|
1728
|
-
function keyContextFor(
|
|
1729
|
-
if (
|
|
1730
|
-
return
|
|
1268
|
+
function keyContextFor(key, timeline, meters, totalBeats, budget) {
|
|
1269
|
+
if (key !== void 0) {
|
|
1270
|
+
return key;
|
|
1731
1271
|
}
|
|
1732
|
-
const regions =
|
|
1733
|
-
meters,
|
|
1734
|
-
totalBeats,
|
|
1735
|
-
budget: opts.budget
|
|
1736
|
-
});
|
|
1272
|
+
const regions = detectModulations(timeline.segments, { meters, totalBeats, budget });
|
|
1737
1273
|
const keyAt = keyLookup(regions, prevailingKeyOf(regions) ?? resolveKey(majorKey(0)));
|
|
1738
1274
|
return (beat) => scaleOf(keyAt(beat));
|
|
1739
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
|
+
}
|
|
1740
1286
|
function restBoundaries(boundaries, notes, meters, restBeats) {
|
|
1741
1287
|
const ordered = [...notes].sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);
|
|
1742
1288
|
let covered = Number.NEGATIVE_INFINITY;
|
|
@@ -1790,7 +1336,7 @@ function structuralWeightOf(cadence, confidence, aligned, isLast) {
|
|
|
1790
1336
|
if (result.strength === "perfect") {
|
|
1791
1337
|
base = 1;
|
|
1792
1338
|
} else if (result.strength === "imperfect") {
|
|
1793
|
-
base *=
|
|
1339
|
+
base *= IMPERFECT_CLOSURE_FACTOR;
|
|
1794
1340
|
}
|
|
1795
1341
|
}
|
|
1796
1342
|
if (result.evaded) {
|
|
@@ -1828,17 +1374,18 @@ function phrasesFromTimeline(timeline, notes, opts = {}) {
|
|
|
1828
1374
|
const segments = timeline.segments;
|
|
1829
1375
|
const firstSegment = segments[0];
|
|
1830
1376
|
const lastSegment = segments[segments.length - 1];
|
|
1831
|
-
const
|
|
1832
|
-
(
|
|
1833
|
-
firstSegment?.startBeat ??
|
|
1377
|
+
const earliest = Math.min(
|
|
1378
|
+
firstSoundingBeat(sounding, Number.POSITIVE_INFINITY),
|
|
1379
|
+
firstSegment?.startBeat ?? Number.POSITIVE_INFINITY
|
|
1834
1380
|
);
|
|
1381
|
+
const firstOnset = Number.isFinite(earliest) ? earliest : 0;
|
|
1835
1382
|
const spanStart = barGridStart(firstOnset, meters);
|
|
1836
|
-
const
|
|
1837
|
-
|
|
1838
|
-
|
|
1383
|
+
const latestEnd = Math.max(
|
|
1384
|
+
lastSegment?.endBeat ?? Number.NEGATIVE_INFINITY,
|
|
1385
|
+
latestSoundingEnd(sounding, Number.NEGATIVE_INFINITY)
|
|
1839
1386
|
);
|
|
1840
|
-
const
|
|
1841
|
-
|
|
1387
|
+
const notesEnd = Number.isFinite(latestEnd) ? latestEnd : 0;
|
|
1388
|
+
const spanEnd = resolveSpanEnd(opts.totalBeats, spanStart, notesEnd, "phrase totalBeats");
|
|
1842
1389
|
if (spanEnd <= spanStart + BEAT_EPS) {
|
|
1843
1390
|
return [];
|
|
1844
1391
|
}
|
|
@@ -1849,13 +1396,9 @@ function phrasesFromTimeline(timeline, notes, opts = {}) {
|
|
|
1849
1396
|
assertRange(longNoteBeats, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "phrase longNoteBeats");
|
|
1850
1397
|
const minPhraseBeats = opts.minPhraseBeats ?? barBeats;
|
|
1851
1398
|
assertRange(minPhraseBeats, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "phrase minPhraseBeats");
|
|
1852
|
-
const key = keyContextFor(opts,
|
|
1853
|
-
const cadences =
|
|
1854
|
-
const grouping = opts.hypermeter ??
|
|
1855
|
-
totalBeats: spanEnd,
|
|
1856
|
-
cadenceBeats: cadences.map((hit) => hit.atBeat),
|
|
1857
|
-
budget: opts.budget
|
|
1858
|
-
});
|
|
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);
|
|
1859
1402
|
const firstBar = barIndexAt(Math.max(0, spanStart), meters);
|
|
1860
1403
|
const hyperBeats = Math.max(
|
|
1861
1404
|
barBeats,
|
|
@@ -1864,19 +1407,11 @@ function phrasesFromTimeline(timeline, notes, opts = {}) {
|
|
|
1864
1407
|
const expected = opts.expectedPhraseBeats ?? hyperBeats;
|
|
1865
1408
|
assertRange(expected, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "phrase expectedPhraseBeats");
|
|
1866
1409
|
const boundaries = /* @__PURE__ */ new Map();
|
|
1867
|
-
|
|
1868
|
-
const
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
const strength = CADENCE_BOUNDARY_STRENGTH[type];
|
|
1873
|
-
const chordEnd = arrivalEnd(timeline, hit.atBeat);
|
|
1874
|
-
addSignal(boundaries, chordEnd, "cadence", strength, hit);
|
|
1875
|
-
const barEnd = barPositionToBeat({ bar: barIndexAt(hit.atBeat, meters) + 1, beat: 0 }, meters);
|
|
1876
|
-
if (chordEnd > barEnd + BEAT_EPS) {
|
|
1877
|
-
addSignal(boundaries, barEnd, "cadence", strength, hit);
|
|
1878
|
-
}
|
|
1879
|
-
}
|
|
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
|
+
});
|
|
1880
1415
|
restBoundaries(boundaries, sounding, meters, restBeats);
|
|
1881
1416
|
for (const note of sounding) {
|
|
1882
1417
|
if (note.durationBeat >= longNoteBeats - BEAT_EPS) {
|
|
@@ -1907,7 +1442,11 @@ function phrasesFromTimeline(timeline, notes, opts = {}) {
|
|
|
1907
1442
|
stops.map((stop) => stop.beat),
|
|
1908
1443
|
readings.map((reading) => reading.strength),
|
|
1909
1444
|
minPhraseBeats,
|
|
1910
|
-
expected
|
|
1445
|
+
expected,
|
|
1446
|
+
{
|
|
1447
|
+
arrivals: stops.map((stop) => stop.cadence?.atBeat ?? stop.beat),
|
|
1448
|
+
uncredited: readings.map((reading) => reading.uncredited)
|
|
1449
|
+
}
|
|
1911
1450
|
);
|
|
1912
1451
|
const downbeatKeys = new Set(
|
|
1913
1452
|
grouping.downbeats.map((downbeat) => Math.round(downbeat / BEAT_EPS))
|
|
@@ -1919,14 +1458,15 @@ function phrasesFromTimeline(timeline, notes, opts = {}) {
|
|
|
1919
1458
|
const startBeat = stops[from]?.beat ?? spanStart;
|
|
1920
1459
|
const stop = stops[to];
|
|
1921
1460
|
const endBeat = stop?.beat ?? spanEnd;
|
|
1922
|
-
const reading = readings[to] ?? { strength: 0, signals: [] };
|
|
1461
|
+
const reading = readings[to] ?? { strength: 0, uncredited: 0, signals: [] };
|
|
1923
1462
|
const length = endBeat - startBeat;
|
|
1924
|
-
const confidence = clamp01(
|
|
1925
|
-
CLOSING_SHARE * reading.strength + (1 - CLOSING_SHARE) * Math.max(0, lengthFit(length, expected))
|
|
1926
|
-
);
|
|
1927
1463
|
const isLast = n === path.length - 1;
|
|
1928
1464
|
const closing = stop?.cadence ?? null;
|
|
1929
|
-
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
|
+
);
|
|
1930
1470
|
const structuralWeight = structuralWeightOf(
|
|
1931
1471
|
cadence,
|
|
1932
1472
|
confidence,
|
|
@@ -1934,7 +1474,7 @@ function phrasesFromTimeline(timeline, notes, opts = {}) {
|
|
|
1934
1474
|
isLast
|
|
1935
1475
|
);
|
|
1936
1476
|
const signals = cadence === null ? reading.signals.filter((signal) => signal !== "cadence") : reading.signals;
|
|
1937
|
-
const bars = barSpanOf(startBeat, endBeat, meters);
|
|
1477
|
+
const bars = Math.round(barSpanOf(startBeat, endBeat, meters) * 100) / 100;
|
|
1938
1478
|
phrases.push({
|
|
1939
1479
|
startBeat,
|
|
1940
1480
|
endBeat,
|
|
@@ -2000,13 +1540,17 @@ function sectionsFromNotes(notes, opts = {}) {
|
|
|
2000
1540
|
budget: opts.budget
|
|
2001
1541
|
});
|
|
2002
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
|
+
);
|
|
2003
1551
|
if (sounding.length === 0) {
|
|
2004
1552
|
return [];
|
|
2005
1553
|
}
|
|
2006
|
-
const firstOnset = barGridStart(firstSoundingBeat(sounding), meters);
|
|
2007
|
-
const lastEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
|
|
2008
|
-
const spanEnd = Math.max(firstOnset, opts.totalBeats ?? lastEnd);
|
|
2009
|
-
assertRange(spanEnd, 0, Number.MAX_SAFE_INTEGER, "section totalBeats");
|
|
2010
1554
|
const threshold = opts.threshold ?? DEFAULT_THRESHOLD;
|
|
2011
1555
|
assertRange(threshold, 0, 1, "section threshold");
|
|
2012
1556
|
const unitBars = opts.unitBars ?? hypermeter(sounding, meters, { totalBeats: spanEnd, budget: opts.budget }).groupBars;
|
|
@@ -2022,8 +1566,9 @@ function sectionsFromNotes(notes, opts = {}) {
|
|
|
2022
1566
|
const barStart = startBar + index * unitBars;
|
|
2023
1567
|
const startBeat = index === 0 ? firstOnset : barPositionToBeat({ bar: barStart, beat: 0 }, meters);
|
|
2024
1568
|
const endBeat = index === unitCount - 1 ? spanEnd : barPositionToBeat({ bar: barStart + unitBars, beat: 0 }, meters);
|
|
1569
|
+
const fromBeat = index === 0 ? Math.min(firstSounding, startBeat) : startBeat;
|
|
2025
1570
|
const unitNotes = sounding.filter(
|
|
2026
|
-
(note) => note.startBeat >=
|
|
1571
|
+
(note) => note.startBeat >= fromBeat - BEAT_EPS && note.startBeat < endBeat - BEAT_EPS
|
|
2027
1572
|
).sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);
|
|
2028
1573
|
units.push({
|
|
2029
1574
|
startBeat,
|
|
@@ -2120,175 +1665,294 @@ function sectionsFromNotes(notes, opts = {}) {
|
|
|
2120
1665
|
return sections;
|
|
2121
1666
|
}
|
|
2122
1667
|
|
|
2123
|
-
// src/analyze/
|
|
2124
|
-
var
|
|
2125
|
-
function
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
}
|
|
2132
|
-
function touches(before, after) {
|
|
2133
|
-
return Math.abs(after.startBeat - before.endBeat) <= BEAT_EPS;
|
|
2134
|
-
}
|
|
2135
|
-
function salienceFloors(durations) {
|
|
2136
|
-
const order = durations.map((_, index) => index).sort((a, b) => {
|
|
2137
|
-
const left = durations[a] ?? 0;
|
|
2138
|
-
const right = durations[b] ?? 0;
|
|
2139
|
-
return left - right || a - b;
|
|
2140
|
-
});
|
|
2141
|
-
const rankOf = new Array(durations.length).fill(0);
|
|
2142
|
-
for (let rank = 0; rank < order.length; rank += 1) {
|
|
2143
|
-
rankOf[order[rank] ?? 0] = rank;
|
|
2144
|
-
}
|
|
2145
|
-
const floors = [];
|
|
2146
|
-
const remaining = durations.length - 1;
|
|
2147
|
-
for (let index = 0; index < durations.length; index += 1) {
|
|
2148
|
-
if (remaining <= 0) {
|
|
2149
|
-
floors.push(0);
|
|
2150
|
-
continue;
|
|
1668
|
+
// src/analyze/reference/compare.ts
|
|
1669
|
+
var RATIONALE_FIELDS = 2;
|
|
1670
|
+
function rationaleOf(comparison) {
|
|
1671
|
+
const measured = [];
|
|
1672
|
+
for (const group of [comparison.form, comparison.harmony, comparison.melody, comparison.rhythm]) {
|
|
1673
|
+
for (const [name, value] of Object.entries(group)) {
|
|
1674
|
+
if (value !== null) {
|
|
1675
|
+
measured.push({ name, value, order: measured.length });
|
|
1676
|
+
}
|
|
2151
1677
|
}
|
|
2152
|
-
const skipped = rankOf[index] ?? 0;
|
|
2153
|
-
const at = (position) => durations[order[position < skipped ? position : position + 1] ?? 0] ?? 0;
|
|
2154
|
-
const middle = remaining >> 1;
|
|
2155
|
-
floors.push(remaining % 2 === 1 ? at(middle) : (at(middle - 1) + at(middle)) / 2);
|
|
2156
|
-
}
|
|
2157
|
-
return floors;
|
|
2158
|
-
}
|
|
2159
|
-
function isTonicChord(chord, key) {
|
|
2160
|
-
return pitchClassOf(chord.rootPc - key.rootPc) === 0 && !isAppliedDominant(chord, key);
|
|
2161
|
-
}
|
|
2162
|
-
function functionalReason(segment, before, after, keyAt) {
|
|
2163
|
-
const key = keyAt(segment.startBeat);
|
|
2164
|
-
if (after !== void 0 && isCadentialSixFour(segment.chord, after.chord, key)) {
|
|
2165
|
-
return { kind: "cadentialSixFour" };
|
|
2166
|
-
}
|
|
2167
|
-
if (isTonicChord(segment.chord, key)) {
|
|
2168
|
-
return { kind: "tonic" };
|
|
2169
|
-
}
|
|
2170
|
-
if (isDominantChordOf(segment.chord, key)) {
|
|
2171
|
-
return { kind: "dominant" };
|
|
2172
|
-
}
|
|
2173
|
-
if (before !== void 0) {
|
|
2174
|
-
const arrival = detectCadence(before.chord, segment.chord, key).type;
|
|
2175
|
-
if (arrival !== null) {
|
|
2176
|
-
return { kind: "cadenceArrival", cadence: arrival };
|
|
2177
|
-
}
|
|
2178
|
-
}
|
|
2179
|
-
if (after !== void 0) {
|
|
2180
|
-
const agent = detectCadence(segment.chord, after.chord, keyAt(after.startBeat)).type;
|
|
2181
|
-
if (agent !== null && TONIC_ARRIVING.has(agent)) {
|
|
2182
|
-
return { kind: "cadenceAgent", cadence: agent };
|
|
2183
|
-
}
|
|
2184
|
-
}
|
|
2185
|
-
return null;
|
|
2186
|
-
}
|
|
2187
|
-
function figureReason(segment, before, after) {
|
|
2188
|
-
if (before === void 0 || after === void 0) {
|
|
2189
|
-
return null;
|
|
2190
|
-
}
|
|
2191
|
-
const incoming = signedStep(before.chord.rootPc, segment.chord.rootPc);
|
|
2192
|
-
const outgoing = signedStep(segment.chord.rootPc, after.chord.rootPc);
|
|
2193
|
-
if (!isStep(incoming) || !isStep(outgoing)) {
|
|
2194
|
-
return null;
|
|
2195
|
-
}
|
|
2196
|
-
if (incoming > 0 === outgoing > 0) {
|
|
2197
|
-
return { kind: "passing" };
|
|
2198
|
-
}
|
|
2199
|
-
return pitchClassOf(before.chord.rootPc) === pitchClassOf(after.chord.rootPc) ? { kind: "neighbor" } : null;
|
|
2200
|
-
}
|
|
2201
|
-
function describe(reason) {
|
|
2202
|
-
switch (reason.kind) {
|
|
2203
|
-
case "tonic":
|
|
2204
|
-
return "Structural: the tonic of the key, which the progression is heard against";
|
|
2205
|
-
case "dominant":
|
|
2206
|
-
return "Structural: the dominant of the key, the chord its tonic is approached from";
|
|
2207
|
-
case "cadentialSixFour":
|
|
2208
|
-
return "Structural: a tonic six-four on the bass of the dominant that follows, which prolongs that dominant rather than framing the tonic";
|
|
2209
|
-
case "cadenceArrival":
|
|
2210
|
-
return `Structural: the arrival of a ${reason.cadence} cadence`;
|
|
2211
|
-
case "cadenceAgent":
|
|
2212
|
-
return `Structural: the chord that drives a ${reason.cadence} cadence`;
|
|
2213
|
-
case "salient":
|
|
2214
|
-
return "Structural: it holds the harmony longer than the chords around it";
|
|
2215
|
-
case "first":
|
|
2216
|
-
return "Structural: the first chord of the progression frames it";
|
|
2217
|
-
case "last":
|
|
2218
|
-
return "Structural: the last chord of the progression frames it";
|
|
2219
|
-
case "unfigured":
|
|
2220
|
-
return "Structural: its root neither steps through to the next chord nor returns to the last, so it embellishes nothing";
|
|
2221
|
-
case "passing":
|
|
2222
|
-
return "Passing: its root steps in from the previous chord and on to the next in the same direction";
|
|
2223
|
-
case "neighbor":
|
|
2224
|
-
return "Neighbor: its root steps away from the surrounding harmony and returns to it";
|
|
2225
|
-
}
|
|
2226
|
-
}
|
|
2227
|
-
var BASIS_READINGS = {
|
|
2228
|
-
function: ({ segment, before, after, keyAt }) => functionalReason(segment, before, after, keyAt),
|
|
2229
|
-
duration: ({ segment, salienceFloor }) => segment.endBeat - segment.startBeat > salienceFloor ? { kind: "salient" } : null
|
|
2230
|
-
};
|
|
2231
|
-
var REDUCTION_BASES = Object.keys(BASIS_READINGS);
|
|
2232
|
-
function levelOf(reason) {
|
|
2233
|
-
if (reason.kind === "passing") {
|
|
2234
|
-
return "passing";
|
|
2235
1678
|
}
|
|
2236
|
-
|
|
1679
|
+
if (measured.length === 0) {
|
|
1680
|
+
return "Nothing to compare: neither profile has material for any field";
|
|
1681
|
+
}
|
|
1682
|
+
const describe = (entries) => entries.map((entry) => `${entry.name} ${Math.round(entry.value * 100)}%`).join(", ");
|
|
1683
|
+
const highest = [...measured].sort((x, y) => y.value - x.value || x.order - y.order);
|
|
1684
|
+
const lowest = [...measured].sort((x, y) => x.value - y.value || x.order - y.order);
|
|
1685
|
+
return `Most alike: ${describe(highest.slice(0, RATIONALE_FIELDS))}; least alike: ${describe(lowest.slice(0, RATIONALE_FIELDS))}`;
|
|
1686
|
+
}
|
|
1687
|
+
function compareReferences(a, b, opts) {
|
|
1688
|
+
assertReferenceProfile(a, "reference profile a");
|
|
1689
|
+
assertReferenceProfile(b, "reference profile b");
|
|
1690
|
+
const { budget } = assertOptions(opts, "compare options");
|
|
1691
|
+
const structural = (profile) => profile.harmony.chords.filter((chord) => chord.level === "structural");
|
|
1692
|
+
const cadences = (profile) => profile.form.phrases.map((phrase) => phrase.cadence?.type ?? null);
|
|
1693
|
+
const phraseMelodies = (profile) => profile.form.phrases.map((phrase) => phrase.melody);
|
|
1694
|
+
const comparison = {
|
|
1695
|
+
form: {
|
|
1696
|
+
sectionSequenceSimilarity: sectionSequenceSimilarity(
|
|
1697
|
+
a.form.sections.map((section) => section.label),
|
|
1698
|
+
b.form.sections.map((section) => section.label)
|
|
1699
|
+
),
|
|
1700
|
+
phraseLengthSimilarity: phraseLengthSimilarity(
|
|
1701
|
+
a.form.phrases.map((phrase) => phrase.bars),
|
|
1702
|
+
b.form.phrases.map((phrase) => phrase.bars)
|
|
1703
|
+
)
|
|
1704
|
+
},
|
|
1705
|
+
harmony: {
|
|
1706
|
+
progressionSimilarity: progressionSimilarity(structural(a), structural(b), budget),
|
|
1707
|
+
functionSimilarity: functionSimilarity(a.harmony.chords, b.harmony.chords),
|
|
1708
|
+
cadenceSimilarity: cadenceSimilarity(cadences(a), cadences(b)),
|
|
1709
|
+
keyPlanSimilarity: keyPlanSimilarity(
|
|
1710
|
+
a.harmony.keys.map((region) => region.key),
|
|
1711
|
+
b.harmony.keys.map((region) => region.key)
|
|
1712
|
+
),
|
|
1713
|
+
harmonicRhythmOnsetSimilarity: onsetSimilarity(a.harmony.rhythm, b.harmony.rhythm),
|
|
1714
|
+
harmonicRhythmDensitySimilarity: densitySimilarity(a.harmony.rhythm, b.harmony.rhythm)
|
|
1715
|
+
},
|
|
1716
|
+
melody: {
|
|
1717
|
+
surfaceSimilarity: surfaceSimilarity(a.melody.motifs, b.melody.motifs),
|
|
1718
|
+
motifStructureSimilarity: motifStructureSimilarity(
|
|
1719
|
+
{ graph: a.melody.graph, spanBeats: a.span.endBeat - a.span.startBeat },
|
|
1720
|
+
{ graph: b.melody.graph, spanBeats: b.span.endBeat - b.span.startBeat }
|
|
1721
|
+
),
|
|
1722
|
+
contourSimilarity: contourSimilarity(phraseMelodies(a), phraseMelodies(b)),
|
|
1723
|
+
registerSimilarity: registerSimilarity(
|
|
1724
|
+
{ phrases: phraseMelodies(a), mean: a.melody.register?.mean ?? null },
|
|
1725
|
+
{ phrases: phraseMelodies(b), mean: b.melody.register?.mean ?? null }
|
|
1726
|
+
)
|
|
1727
|
+
},
|
|
1728
|
+
rhythm: {
|
|
1729
|
+
onsetSimilarity: onsetSimilarity(a.melody.rhythm, b.melody.rhythm),
|
|
1730
|
+
densitySimilarity: densitySimilarity(a.melody.rhythm, b.melody.rhythm),
|
|
1731
|
+
durationSimilarity: durationSimilarity(a.melody.rhythm, b.melody.rhythm),
|
|
1732
|
+
syncopationSimilarity: syncopationSimilarity(a.melody.rhythm, b.melody.rhythm)
|
|
1733
|
+
}
|
|
1734
|
+
};
|
|
1735
|
+
return { ...comparison, rationale: rationaleOf(comparison) };
|
|
2237
1736
|
}
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
const keyAt = keyScaleAt(key);
|
|
2242
|
-
const basis = assertOneOf(asked.basis ?? "function", REDUCTION_BASES, "reduction basis");
|
|
2243
|
-
const reading = BASIS_READINGS[basis];
|
|
1737
|
+
|
|
1738
|
+
// src/analyze/reference/profile.ts
|
|
1739
|
+
function spanOf(sounding, timeline, meters, totalBeats) {
|
|
2244
1740
|
const segments = timeline.segments;
|
|
2245
|
-
const
|
|
2246
|
-
const
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
1741
|
+
const firstSegment = segments[0];
|
|
1742
|
+
const lastSegment = segments[segments.length - 1];
|
|
1743
|
+
const firstOnset = firstSoundingBeat(sounding, firstSegment?.startBeat ?? 0);
|
|
1744
|
+
const startBeat = barGridStart(
|
|
1745
|
+
Math.min(firstOnset, firstSegment?.startBeat ?? firstOnset),
|
|
1746
|
+
meters
|
|
1747
|
+
);
|
|
1748
|
+
const latestEnd = Math.max(
|
|
1749
|
+
lastSegment?.endBeat ?? Number.NEGATIVE_INFINITY,
|
|
1750
|
+
latestSoundingEnd(sounding, Number.NEGATIVE_INFINITY)
|
|
1751
|
+
);
|
|
1752
|
+
const notesEnd = Number.isFinite(latestEnd) ? latestEnd : 0;
|
|
1753
|
+
return { startBeat, endBeat: Math.max(startBeat, totalBeats ?? notesEnd) };
|
|
1754
|
+
}
|
|
1755
|
+
function sameKey(a, b) {
|
|
1756
|
+
return pitchClassOf(a.scale.rootPc) === pitchClassOf(b.scale.rootPc) && a.scale.modeMask12 === b.scale.modeMask12;
|
|
1757
|
+
}
|
|
1758
|
+
function keyIndexAt(regions, beat, prevailing) {
|
|
1759
|
+
const holding = regions.findIndex((region) => region.startBeat <= beat && beat < region.endBeat);
|
|
1760
|
+
if (holding >= 0) {
|
|
1761
|
+
return holding;
|
|
1762
|
+
}
|
|
1763
|
+
return Math.max(
|
|
1764
|
+
0,
|
|
1765
|
+
regions.findIndex((region) => sameKey(region.key, prevailing))
|
|
1766
|
+
);
|
|
1767
|
+
}
|
|
1768
|
+
function sectionAt(sections, beat) {
|
|
1769
|
+
const index = sections.findIndex(
|
|
1770
|
+
(section) => beat >= section.startBeat - BEAT_EPS && beat < section.endBeat - BEAT_EPS
|
|
1771
|
+
);
|
|
1772
|
+
return index < 0 ? null : index;
|
|
1773
|
+
}
|
|
1774
|
+
function readingsFor(notes, opts = {}) {
|
|
1775
|
+
const asked = assertOptions(opts, "reference options");
|
|
1776
|
+
const meters = resolveMeters(asked, "reference meters");
|
|
1777
|
+
const budget = asked.budget;
|
|
1778
|
+
if (asked.timeline === void 0) {
|
|
1779
|
+
return chordTimelineFromNotes(notes, {
|
|
1780
|
+
meters,
|
|
1781
|
+
key: asked.key,
|
|
1782
|
+
totalBeats: asked.totalBeats,
|
|
1783
|
+
budget
|
|
2270
1784
|
});
|
|
2271
1785
|
}
|
|
2272
|
-
|
|
1786
|
+
const timeline = assertChordTimeline(asked.timeline, "reference timeline");
|
|
1787
|
+
let keys;
|
|
1788
|
+
if (asked.key === void 0) {
|
|
1789
|
+
keys = keyTimelineFromNotes(notes, { meters, totalBeats: asked.totalBeats, budget });
|
|
1790
|
+
} else {
|
|
1791
|
+
const sounding = assertNoteEvents(notes, "reference notes", {
|
|
1792
|
+
allowNonPositiveDuration: true,
|
|
1793
|
+
budget
|
|
1794
|
+
}).filter((note) => note.durationBeat > 0);
|
|
1795
|
+
const span = spanOf(sounding, timeline, meters, asked.totalBeats);
|
|
1796
|
+
keys = statedKeyRegions(resolveKey(asked.key), span.startBeat, span.endBeat);
|
|
1797
|
+
}
|
|
1798
|
+
return { timeline, keys, prevailingKey: prevailingKeyOf(keys) ?? resolveKey(majorKey(0)) };
|
|
1799
|
+
}
|
|
1800
|
+
function referenceFromReadings(notes, readings, opts = {}) {
|
|
1801
|
+
const asked = assertOptions(opts, "reference options");
|
|
1802
|
+
const meters = resolveMeters(asked, "reference meters");
|
|
1803
|
+
const budget = asked.budget;
|
|
1804
|
+
const sounding = assertNoteEvents(notes, "reference notes", {
|
|
1805
|
+
allowNonPositiveDuration: true,
|
|
1806
|
+
budget
|
|
1807
|
+
}).filter((note) => note.durationBeat > 0);
|
|
1808
|
+
if (asked.totalBeats !== void 0) {
|
|
1809
|
+
assertRange(asked.totalBeats, 0, Number.MAX_SAFE_INTEGER, "reference totalBeats");
|
|
1810
|
+
}
|
|
1811
|
+
const line = orderedNotes(asked.melody ?? notes, "reference melody", budget).map(
|
|
1812
|
+
(note) => Object.is(note.pitch, -0) ? { ...note, pitch: 0 } : note
|
|
1813
|
+
);
|
|
1814
|
+
const { timeline, prevailingKey } = readings;
|
|
1815
|
+
const span = spanOf(sounding, timeline, meters, asked.totalBeats);
|
|
1816
|
+
const spanEnd = span.endBeat;
|
|
1817
|
+
const keyAt = keyLookup(readings.keys, prevailingKey);
|
|
1818
|
+
const scaleAt = (beat) => scaleOf(keyAt(beat));
|
|
1819
|
+
const cadenceBeats = detectCadences(timeline, scaleAt).map((hit) => hit.atBeat);
|
|
1820
|
+
const grouping = hypermeter(sounding, meters, { cadenceBeats, totalBeats: spanEnd, budget });
|
|
1821
|
+
const phrases = phrasesFromTimeline(timeline, notes, {
|
|
1822
|
+
meters,
|
|
1823
|
+
key: scaleAt,
|
|
1824
|
+
hypermeter: grouping,
|
|
1825
|
+
totalBeats: spanEnd,
|
|
1826
|
+
budget
|
|
1827
|
+
});
|
|
1828
|
+
const cadences = /* @__PURE__ */ new Map();
|
|
1829
|
+
for (const closing of structuralCadences(phrases)) {
|
|
1830
|
+
const type = closing.cadence.type;
|
|
1831
|
+
if (type !== null) {
|
|
1832
|
+
cadences.set(closing.phraseIndex, {
|
|
1833
|
+
atBeat: closing.atBeat,
|
|
1834
|
+
type,
|
|
1835
|
+
weight: closing.weight
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
const sections = sectionsFromNotes(notes, {
|
|
1840
|
+
meters,
|
|
1841
|
+
unitBars: grouping.groupBars,
|
|
1842
|
+
totalBeats: spanEnd,
|
|
1843
|
+
budget
|
|
1844
|
+
});
|
|
1845
|
+
const keys = readings.keys.length === 0 && timeline.segments.length > 0 ? [
|
|
1846
|
+
{
|
|
1847
|
+
startBeat: span.startBeat,
|
|
1848
|
+
endBeat: spanEnd,
|
|
1849
|
+
key: resolveKey(prevailingKey),
|
|
1850
|
+
confidence: 0
|
|
1851
|
+
}
|
|
1852
|
+
] : readings.keys.map((region) => ({
|
|
1853
|
+
startBeat: region.startBeat,
|
|
1854
|
+
endBeat: region.endBeat,
|
|
1855
|
+
key: resolveKey(region.key),
|
|
1856
|
+
confidence: region.confidence
|
|
1857
|
+
}));
|
|
1858
|
+
const chords = reduceProgression(timeline, scaleAt).map((reduced) => {
|
|
1859
|
+
const index = keyIndexAt(keys, (reduced.startBeat + reduced.endBeat) / 2, prevailingKey);
|
|
1860
|
+
const key = keys[index].key;
|
|
1861
|
+
return {
|
|
1862
|
+
startBeat: reduced.startBeat,
|
|
1863
|
+
endBeat: reduced.endBeat,
|
|
1864
|
+
key: index,
|
|
1865
|
+
roman: chordToRoman(reduced.chord, key),
|
|
1866
|
+
function: functionOf(reduced.chord, key),
|
|
1867
|
+
level: reduced.level
|
|
1868
|
+
};
|
|
1869
|
+
});
|
|
1870
|
+
const chordOnsets = timeline.segments.map((segment) => ({
|
|
1871
|
+
pitch: 60,
|
|
1872
|
+
startBeat: segment.startBeat,
|
|
1873
|
+
durationBeat: segment.endBeat - segment.startBeat
|
|
1874
|
+
}));
|
|
1875
|
+
const harmonyRhythm = analyzeRhythm(chordOnsets, { meters, totalBeats: spanEnd, budget });
|
|
1876
|
+
const motifs = extractMotifs(line, { budget });
|
|
1877
|
+
const graph = motifGraph(line, motifs, { key: prevailingKey, budget });
|
|
1878
|
+
let register = null;
|
|
1879
|
+
if (line.length > 0) {
|
|
1880
|
+
const pitches = line.map((note) => note.pitch);
|
|
1881
|
+
register = {
|
|
1882
|
+
low: Math.min(...pitches),
|
|
1883
|
+
high: Math.max(...pitches),
|
|
1884
|
+
mean: weightedMean(line)
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
const lineMean = register?.mean ?? 0;
|
|
1888
|
+
const referenceMotifs = motifs.map((motif) => {
|
|
1889
|
+
const first = motif.occurrences[0];
|
|
1890
|
+
return {
|
|
1891
|
+
intervals: motif.intervals,
|
|
1892
|
+
rhythm: motif.rhythm,
|
|
1893
|
+
unitBeats: motif.notes.length < 2 ? 0 : motif.notes[1].startBeat - motif.notes[0].startBeat,
|
|
1894
|
+
spanBeats: first === void 0 ? 0 : first.endBeat - first.startBeat,
|
|
1895
|
+
occurrences: motif.occurrences.length
|
|
1896
|
+
};
|
|
1897
|
+
});
|
|
1898
|
+
const referencePhrases = phrases.map((phrase, index) => ({
|
|
1899
|
+
startBeat: phrase.startBeat,
|
|
1900
|
+
endBeat: phrase.endBeat,
|
|
1901
|
+
bars: barSpanOf(phrase.startBeat, phrase.endBeat, meters),
|
|
1902
|
+
section: sectionAt(sections, phrase.startBeat),
|
|
1903
|
+
confidence: phrase.confidence,
|
|
1904
|
+
cadence: cadences.get(index) ?? null,
|
|
1905
|
+
melody: phraseMelody(line, phrase.startBeat, phrase.endBeat, lineMean, meters, budget),
|
|
1906
|
+
motifNodes: graph.nodes.flatMap(
|
|
1907
|
+
(node, nodeIndex) => node.startBeat >= phrase.startBeat - BEAT_EPS && node.startBeat < phrase.endBeat - BEAT_EPS ? [nodeIndex] : []
|
|
1908
|
+
)
|
|
1909
|
+
}));
|
|
1910
|
+
return {
|
|
1911
|
+
profileVersion: REFERENCE_PROFILE_VERSION,
|
|
1912
|
+
span: {
|
|
1913
|
+
startBeat: span.startBeat,
|
|
1914
|
+
endBeat: spanEnd,
|
|
1915
|
+
bars: barSpanOf(span.startBeat, spanEnd, meters)
|
|
1916
|
+
},
|
|
1917
|
+
meters,
|
|
1918
|
+
form: {
|
|
1919
|
+
hypermeter: { groupBars: grouping.groupBars, confidence: grouping.confidence },
|
|
1920
|
+
sections,
|
|
1921
|
+
phrases: referencePhrases
|
|
1922
|
+
},
|
|
1923
|
+
harmony: { keys, chords, rhythm: harmonyRhythm },
|
|
1924
|
+
melody: {
|
|
1925
|
+
register,
|
|
1926
|
+
shape: line.length < 2 ? null : melodicContour(line).shape,
|
|
1927
|
+
motifs: referenceMotifs,
|
|
1928
|
+
graph,
|
|
1929
|
+
rhythm: rhythmOver(line, { meters, totalBeats: spanEnd, budget }, span.startBeat)
|
|
1930
|
+
}
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
function analyzeReference(notes, opts) {
|
|
1934
|
+
return referenceFromReadings(notes, readingsFor(notes, opts), opts);
|
|
2273
1935
|
}
|
|
2274
1936
|
|
|
2275
1937
|
export {
|
|
2276
1938
|
trackRoleOf,
|
|
1939
|
+
arrangementProfile,
|
|
1940
|
+
assertTrackNotes,
|
|
1941
|
+
harmonyTrackSet,
|
|
2277
1942
|
analyzePolyphony,
|
|
2278
1943
|
analyzeArrangement,
|
|
2279
1944
|
createArrangementSession,
|
|
2280
1945
|
tensionCurve,
|
|
2281
1946
|
tensionCurveFrom,
|
|
2282
1947
|
hypermeter,
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
relateMotifs,
|
|
2287
|
-
compareMelodies,
|
|
2288
|
-
melodicSimilarity,
|
|
1948
|
+
hypermeterOver,
|
|
1949
|
+
boundaryCadences,
|
|
1950
|
+
keyContextFor,
|
|
2289
1951
|
phrasesFromTimeline,
|
|
2290
1952
|
structuralCadences,
|
|
2291
1953
|
sectionsFromNotes,
|
|
2292
|
-
|
|
1954
|
+
compareReferences,
|
|
1955
|
+
referenceFromReadings,
|
|
1956
|
+
analyzeReference
|
|
2293
1957
|
};
|
|
2294
|
-
//# sourceMappingURL=chunk-
|
|
1958
|
+
//# sourceMappingURL=chunk-UTE5A36R.js.map
|