@libraz/libcantus 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/README_ja.md +4 -0
- package/dist/analyze/index.cjs +31 -7
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +155 -9
- package/dist/analyze/index.d.ts +155 -9
- package/dist/analyze/index.js +37 -13
- package/dist/{chunk-TT6PKMPR.cjs → chunk-35NBV2FT.cjs} +16 -16
- package/dist/{chunk-TT6PKMPR.cjs.map → chunk-35NBV2FT.cjs.map} +1 -1
- package/dist/{chunk-YPB3A3EY.cjs → chunk-3R3QVKY5.cjs} +314 -285
- package/dist/chunk-3R3QVKY5.cjs.map +1 -0
- package/dist/{chunk-GDKE6RQW.cjs → chunk-4R2LWV7E.cjs} +691 -413
- package/dist/chunk-4R2LWV7E.cjs.map +1 -0
- package/dist/{chunk-RVS4G4MR.cjs → chunk-76ZBTRG2.cjs} +198 -198
- package/dist/{chunk-RVS4G4MR.cjs.map → chunk-76ZBTRG2.cjs.map} +1 -1
- package/dist/{chunk-X6RJAHII.js → chunk-7KG3RTYS.js} +2 -2
- package/dist/{chunk-PQL7E5F6.js → chunk-AGOVW7U6.js} +1 -1
- package/dist/{chunk-PQL7E5F6.js.map → chunk-AGOVW7U6.js.map} +1 -1
- package/dist/{chunk-5KW7DNCJ.cjs → chunk-B2PW6EHN.cjs} +2423 -453
- package/dist/chunk-B2PW6EHN.cjs.map +1 -0
- package/dist/{chunk-J772DL3B.cjs → chunk-CGBADIO5.cjs} +53 -53
- package/dist/{chunk-J772DL3B.cjs.map → chunk-CGBADIO5.cjs.map} +1 -1
- package/dist/{chunk-R5VUXQMP.js → chunk-CPMB7P75.js} +1978 -8
- package/dist/chunk-CPMB7P75.js.map +1 -0
- package/dist/{chunk-VLE5R7PF.js → chunk-GNVN4PPJ.js} +1874 -8
- package/dist/chunk-GNVN4PPJ.js.map +1 -0
- package/dist/{chunk-Q6R3QAZD.cjs → chunk-IFZDWIKM.cjs} +2199 -333
- package/dist/chunk-IFZDWIKM.cjs.map +1 -0
- package/dist/{chunk-DKICR7AN.js → chunk-IPEF6NWB.js} +296 -18
- package/dist/chunk-IPEF6NWB.js.map +1 -0
- package/dist/{chunk-AWEMD747.js → chunk-M5RYMAFI.js} +3 -3
- package/dist/{chunk-HSY2VQXZ.js → chunk-MLBPXA4Z.js} +330 -728
- package/dist/chunk-MLBPXA4Z.js.map +1 -0
- package/dist/{chunk-H66ARFU5.js → chunk-NB25P3EH.js} +3 -3
- package/dist/{chunk-5JJ6EHEZ.cjs → chunk-NM7UAMC3.cjs} +62 -62
- package/dist/{chunk-5JJ6EHEZ.cjs.map → chunk-NM7UAMC3.cjs.map} +1 -1
- package/dist/{chunk-R7B5N2JK.cjs → chunk-PM4XJE2Y.cjs} +47 -47
- package/dist/{chunk-R7B5N2JK.cjs.map → chunk-PM4XJE2Y.cjs.map} +1 -1
- package/dist/{chunk-7GOKQJTQ.js → chunk-PUKJP2Z5.js} +2 -2
- package/dist/{chunk-QCXYKSZS.cjs → chunk-PWC4CRWT.cjs} +19 -19
- package/dist/{chunk-QCXYKSZS.cjs.map → chunk-PWC4CRWT.cjs.map} +1 -1
- package/dist/{chunk-FPBZFNCD.js → chunk-RFIBF2RR.js} +47 -18
- package/dist/{chunk-FPBZFNCD.js.map → chunk-RFIBF2RR.js.map} +1 -1
- package/dist/{chunk-XKSO2V5I.cjs → chunk-S5BKZZGM.cjs} +1 -1
- package/dist/{chunk-XKSO2V5I.cjs.map → chunk-S5BKZZGM.cjs.map} +1 -1
- package/dist/{chunk-HOHFYGKW.js → chunk-UMWZ4ZET.js} +2 -2
- package/dist/{chunk-XVMJO3QF.cjs → chunk-YFA7X2F6.cjs} +489 -887
- package/dist/chunk-YFA7X2F6.cjs.map +1 -0
- package/dist/{chunk-57WB2EDF.js → chunk-ZBUROTJQ.js} +3 -3
- package/dist/{coerce-wwdELNYS.d.ts → coerce-DP0vYu-x.d.ts} +77 -1
- package/dist/{coerce-CJlgULrQ.d.cts → coerce-DRERzdve.d.cts} +77 -1
- package/dist/core/index.cjs +5 -5
- package/dist/core/index.d.cts +2 -2
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +4 -4
- package/dist/generate/index.cjs +20 -8
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +87 -9
- package/dist/generate/index.d.ts +87 -9
- package/dist/generate/index.js +19 -7
- package/dist/{index-BaoKVVhE.d.cts → index-B9QLziDe.d.cts} +2 -2
- package/dist/{index-CnHQgqNh.d.ts → index-BHIUyjhZ.d.ts} +2 -2
- package/dist/{index-CBCP84rv.d.cts → index-BL5N2Oyz.d.cts} +1 -1
- package/dist/{index-D_hzxD8w.d.cts → index-BMweUlm5.d.cts} +3 -0
- package/dist/{index-Ci3xyows.d.ts → index-CL4b4gUQ.d.ts} +3 -0
- package/dist/{index-BzLNkmA7.d.cts → index-CY-3NVfy.d.cts} +486 -5
- package/dist/{index-CnIdP4EZ.d.ts → index-Dw2DP7vm.d.ts} +1 -1
- package/dist/{index-DpC-mDlV.d.ts → index-zaZdmnU4.d.ts} +486 -5
- package/dist/index.cjs +49 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -10
- package/dist/index.d.ts +10 -10
- package/dist/index.js +55 -19
- package/dist/{masks-O73Rhj5d.d.ts → masks-Cc7-MzzY.d.ts} +1 -1
- package/dist/{masks-C8TWLFIi.d.cts → masks-pADfTOiD.d.cts} +1 -1
- package/dist/model/index.cjs +12 -12
- package/dist/model/index.d.cts +213 -16
- package/dist/model/index.d.ts +213 -16
- package/dist/model/index.js +11 -11
- package/dist/{index-DTsKaPaC.d.cts → profile-CQMEqdH9.d.cts} +88 -601
- package/dist/{index-BUtIf2go.d.ts → profile-DLdlIpS9.d.ts} +88 -601
- package/dist/theory/index.cjs +5 -5
- package/dist/theory/index.d.cts +7 -7
- package/dist/theory/index.d.ts +7 -7
- package/dist/theory/index.js +4 -4
- package/dist/{index-BpWOhkkE.d.ts → types-D-XgxGvp.d.cts} +1170 -14
- package/dist/{index-B3WCj_Hm.d.cts → types-Dh7C064C.d.ts} +1170 -14
- package/package.json +6 -5
- package/dist/chunk-5KW7DNCJ.cjs.map +0 -1
- package/dist/chunk-DKICR7AN.js.map +0 -1
- package/dist/chunk-GDKE6RQW.cjs.map +0 -1
- package/dist/chunk-HSY2VQXZ.js.map +0 -1
- package/dist/chunk-Q6R3QAZD.cjs.map +0 -1
- package/dist/chunk-R5VUXQMP.js.map +0 -1
- package/dist/chunk-VLE5R7PF.js.map +0 -1
- package/dist/chunk-XVMJO3QF.cjs.map +0 -1
- package/dist/chunk-YPB3A3EY.cjs.map +0 -1
- /package/dist/{chunk-X6RJAHII.js.map → chunk-7KG3RTYS.js.map} +0 -0
- /package/dist/{chunk-AWEMD747.js.map → chunk-M5RYMAFI.js.map} +0 -0
- /package/dist/{chunk-H66ARFU5.js.map → chunk-NB25P3EH.js.map} +0 -0
- /package/dist/{chunk-7GOKQJTQ.js.map → chunk-PUKJP2Z5.js.map} +0 -0
- /package/dist/{chunk-HOHFYGKW.js.map → chunk-UMWZ4ZET.js.map} +0 -0
- /package/dist/{chunk-57WB2EDF.js.map → chunk-ZBUROTJQ.js.map} +0 -0
|
@@ -1,47 +1,66 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
REFERENCE_PROFILE_VERSION,
|
|
3
3
|
adjacent,
|
|
4
|
+
analyzeRhythm,
|
|
4
5
|
analyzeTimeline,
|
|
5
6
|
analyzeVoice,
|
|
6
7
|
assertChordTimeline,
|
|
8
|
+
assertReferenceProfile,
|
|
7
9
|
attachPivots,
|
|
8
10
|
barGridStart,
|
|
11
|
+
barSpanOf,
|
|
12
|
+
cadenceSimilarity,
|
|
9
13
|
chordTimelineFromNotes,
|
|
14
|
+
chordToRoman,
|
|
10
15
|
clamp01,
|
|
11
16
|
combineEvidence,
|
|
12
|
-
|
|
17
|
+
contourSimilarity,
|
|
18
|
+
densitySimilarity,
|
|
13
19
|
detectCadences,
|
|
14
20
|
dirtySlotsFor,
|
|
21
|
+
durationSimilarity,
|
|
22
|
+
extractMotifs,
|
|
15
23
|
firstSoundingBeat,
|
|
16
24
|
floorMod,
|
|
17
25
|
functionOf,
|
|
26
|
+
functionSimilarity,
|
|
18
27
|
gridForNotes,
|
|
19
28
|
harmonicNovelty,
|
|
20
29
|
hasEnded,
|
|
21
|
-
isAppliedDominant,
|
|
22
|
-
isCadentialSixFour,
|
|
23
30
|
keyLookup,
|
|
24
|
-
|
|
31
|
+
keyPlanSimilarity,
|
|
25
32
|
keyTimelineFromNotes,
|
|
26
33
|
lastBarOf,
|
|
34
|
+
melodicContour,
|
|
35
|
+
melodicSimilarity,
|
|
36
|
+
motifGraph,
|
|
37
|
+
motifStructureSimilarity,
|
|
38
|
+
onsetSimilarity,
|
|
39
|
+
orderedNotes,
|
|
40
|
+
phraseLengthSimilarity,
|
|
41
|
+
phraseMelody,
|
|
27
42
|
prevailingKeyOf,
|
|
43
|
+
progressionSimilarity,
|
|
44
|
+
reduceProgression,
|
|
45
|
+
registerSimilarity,
|
|
28
46
|
sameInstant,
|
|
47
|
+
sectionSequenceSimilarity,
|
|
29
48
|
segmentStartingAt,
|
|
30
49
|
sliceBars,
|
|
50
|
+
surfaceSimilarity,
|
|
51
|
+
syncopationSimilarity,
|
|
31
52
|
weightSimilarity,
|
|
53
|
+
weightedMean,
|
|
32
54
|
windowWeights
|
|
33
|
-
} from "./chunk-
|
|
55
|
+
} from "./chunk-GNVN4PPJ.js";
|
|
34
56
|
import {
|
|
35
57
|
PROFILE_WEIGHTS,
|
|
36
58
|
chordPitchClasses,
|
|
37
59
|
evaluateSafety,
|
|
38
|
-
isDominantChordOf,
|
|
39
|
-
keySignatureFifths,
|
|
40
60
|
majorKey,
|
|
41
61
|
resolveKey,
|
|
42
|
-
scaleOf
|
|
43
|
-
|
|
44
|
-
} from "./chunk-FPBZFNCD.js";
|
|
62
|
+
scaleOf
|
|
63
|
+
} from "./chunk-RFIBF2RR.js";
|
|
45
64
|
import {
|
|
46
65
|
BEAT_EPS,
|
|
47
66
|
InvalidInputError,
|
|
@@ -61,11 +80,9 @@ import {
|
|
|
61
80
|
beatsPerBarAt,
|
|
62
81
|
describeRejected,
|
|
63
82
|
isStrongBeat,
|
|
64
|
-
midiToNote,
|
|
65
83
|
pitchClassOf,
|
|
66
|
-
resolveMeters
|
|
67
|
-
|
|
68
|
-
} from "./chunk-PQL7E5F6.js";
|
|
84
|
+
resolveMeters
|
|
85
|
+
} from "./chunk-AGOVW7U6.js";
|
|
69
86
|
|
|
70
87
|
// src/analyze/arrange/internal.ts
|
|
71
88
|
var MAX_LANE_LEAP = 24;
|
|
@@ -328,6 +345,8 @@ function prepareTracks(tracks) {
|
|
|
328
345
|
const prevEnd = prev === void 0 ? void 0 : prev.startBeat + prev.durationBeat;
|
|
329
346
|
const contiguous = prevEnd !== void 0 && adjacent(prevEnd, note.startBeat);
|
|
330
347
|
return {
|
|
348
|
+
id: note.id,
|
|
349
|
+
originalIndex: note.originalIndex,
|
|
331
350
|
pitch: note.pitch,
|
|
332
351
|
prevPitch: contiguous ? prev?.pitch : void 0,
|
|
333
352
|
startBeat: note.startBeat,
|
|
@@ -395,7 +414,7 @@ function soundingVoicesAt(prepared, beat) {
|
|
|
395
414
|
if (previous !== void 0) {
|
|
396
415
|
snap.prevPitch = previous;
|
|
397
416
|
}
|
|
398
|
-
out.push({ track: t, voice: v, snapshot: snap });
|
|
417
|
+
out.push({ track: t, voice: v, trackIndex: track.trackIndex, note, snapshot: snap });
|
|
399
418
|
}
|
|
400
419
|
}
|
|
401
420
|
return out;
|
|
@@ -403,7 +422,21 @@ function soundingVoicesAt(prepared, beat) {
|
|
|
403
422
|
function otherVoicesSounding(prepared, excludeTrack, excludeVoice, beat, cache) {
|
|
404
423
|
const sounding = cache.get(beat) ?? soundingVoicesAt(prepared, beat);
|
|
405
424
|
cache.set(beat, sounding);
|
|
406
|
-
return sounding.filter(({ track, voice }) => track !== excludeTrack || voice !== excludeVoice)
|
|
425
|
+
return sounding.filter(({ track, voice }) => track !== excludeTrack || voice !== excludeVoice);
|
|
426
|
+
}
|
|
427
|
+
function conflictPartner(sounding, reasons) {
|
|
428
|
+
const partner = {
|
|
429
|
+
trackIndex: sounding.trackIndex,
|
|
430
|
+
noteId: sounding.note.id,
|
|
431
|
+
reasons
|
|
432
|
+
};
|
|
433
|
+
if (sounding.note.originalIndex !== void 0) {
|
|
434
|
+
partner.originalIndex = sounding.note.originalIndex;
|
|
435
|
+
}
|
|
436
|
+
return partner;
|
|
437
|
+
}
|
|
438
|
+
function snapshotsOf(voices) {
|
|
439
|
+
return voices.map(({ snapshot }) => snapshot);
|
|
407
440
|
}
|
|
408
441
|
function analyzePolyphony(notes, chordAtBeat, key) {
|
|
409
442
|
const passage = assertNoteEvents(notes, "notes", { allowNonPositiveDuration: true });
|
|
@@ -425,7 +458,7 @@ function analyzePolyphony(notes, chordAtBeat, key) {
|
|
|
425
458
|
subVoice.voice,
|
|
426
459
|
chordAtBeat,
|
|
427
460
|
key,
|
|
428
|
-
(beat) => otherVoicesSounding(prepared, 0, v, beat, soundingCache)
|
|
461
|
+
(beat) => snapshotsOf(otherVoicesSounding(prepared, 0, v, beat, soundingCache))
|
|
429
462
|
);
|
|
430
463
|
for (const note of analyzed) {
|
|
431
464
|
if (note.originalIndex !== void 0) {
|
|
@@ -549,7 +582,7 @@ function analyzeArrangementWith(tracks, opts = {}, previous, dirty) {
|
|
|
549
582
|
subVoice.voice,
|
|
550
583
|
timeline.at,
|
|
551
584
|
scaleAt,
|
|
552
|
-
(beat) => otherVoicesSounding(prepared, t, v, beat, soundingCache)
|
|
585
|
+
(beat) => snapshotsOf(otherVoicesSounding(prepared, t, v, beat, soundingCache))
|
|
553
586
|
);
|
|
554
587
|
for (const note of analyzed) {
|
|
555
588
|
notes.push({ ...note, trackIndex: track.trackIndex });
|
|
@@ -563,18 +596,19 @@ function analyzeArrangementWith(tracks, opts = {}, previous, dirty) {
|
|
|
563
596
|
}
|
|
564
597
|
for (const beat of evaluationBeats(note, timeline)) {
|
|
565
598
|
const atOnset = sameInstant(beat, note.startBeat);
|
|
599
|
+
const others = otherVoicesSounding(prepared, t, v, beat, soundingCache);
|
|
566
600
|
const safetyQuery = {
|
|
567
601
|
profile,
|
|
568
602
|
candidatePitch: note.pitch,
|
|
569
603
|
prevPitch: atOnset ? preparedNote.prevPitch : note.pitch,
|
|
570
604
|
chord: timeline.at(beat),
|
|
571
605
|
key: scaleAt(beat),
|
|
572
|
-
otherVoices:
|
|
606
|
+
otherVoices: snapshotsOf(others),
|
|
573
607
|
strongBeat: isStrongBeat(beat, meters)
|
|
574
608
|
};
|
|
575
609
|
let result = evaluateSafety(safetyQuery, { suggestions: false });
|
|
576
610
|
if (result.safety !== 0 /* Safe */ && result.safety >= minSeverity) {
|
|
577
|
-
result = evaluateSafety(safetyQuery);
|
|
611
|
+
result = evaluateSafety(safetyQuery, { partners: true });
|
|
578
612
|
const conflict = {
|
|
579
613
|
beat,
|
|
580
614
|
trackName: track.name,
|
|
@@ -597,6 +631,11 @@ function analyzeArrangementWith(tracks, opts = {}, previous, dirty) {
|
|
|
597
631
|
if (result.suggestions !== void 0) {
|
|
598
632
|
conflict.suggestions = [...result.suggestions];
|
|
599
633
|
}
|
|
634
|
+
if (result.partners !== void 0) {
|
|
635
|
+
conflict.partners = result.partners.map(
|
|
636
|
+
({ index, reasons }) => conflictPartner(others[index], reasons)
|
|
637
|
+
);
|
|
638
|
+
}
|
|
600
639
|
conflicts.push(conflict);
|
|
601
640
|
}
|
|
602
641
|
}
|
|
@@ -1052,549 +1091,6 @@ function hypermeter(notes, meter, opts = {}) {
|
|
|
1052
1091
|
};
|
|
1053
1092
|
}
|
|
1054
1093
|
|
|
1055
|
-
// src/analyze/melody/internal.ts
|
|
1056
|
-
var RHYTHM_GRID = 64;
|
|
1057
|
-
function phraseNotes(phrase) {
|
|
1058
|
-
if (Array.isArray(phrase)) {
|
|
1059
|
-
return phrase;
|
|
1060
|
-
}
|
|
1061
|
-
return assertRecord(phrase, "melodic phrase").notes;
|
|
1062
|
-
}
|
|
1063
|
-
function orderedNotes(notes, name, budget) {
|
|
1064
|
-
assertNoteEvents(notes, name, { allowNonPositiveDuration: true, budget });
|
|
1065
|
-
const sounding = notes.filter((note) => note.durationBeat > 0).sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);
|
|
1066
|
-
const line = [];
|
|
1067
|
-
for (const note of sounding) {
|
|
1068
|
-
const previous = line[line.length - 1];
|
|
1069
|
-
if (previous !== void 0 && Math.abs(note.startBeat - previous.startBeat) <= BEAT_EPS) {
|
|
1070
|
-
line[line.length - 1] = note;
|
|
1071
|
-
continue;
|
|
1072
|
-
}
|
|
1073
|
-
line.push(note);
|
|
1074
|
-
}
|
|
1075
|
-
return line;
|
|
1076
|
-
}
|
|
1077
|
-
function intervalsOf(notes) {
|
|
1078
|
-
const out = [];
|
|
1079
|
-
for (let i = 1; i < notes.length; i += 1) {
|
|
1080
|
-
out.push((notes[i]?.pitch ?? 0) - (notes[i - 1]?.pitch ?? 0));
|
|
1081
|
-
}
|
|
1082
|
-
return out;
|
|
1083
|
-
}
|
|
1084
|
-
function onsetGaps(notes) {
|
|
1085
|
-
const out = [];
|
|
1086
|
-
for (let i = 1; i < notes.length; i += 1) {
|
|
1087
|
-
out.push((notes[i]?.startBeat ?? 0) - (notes[i - 1]?.startBeat ?? 0));
|
|
1088
|
-
}
|
|
1089
|
-
return out;
|
|
1090
|
-
}
|
|
1091
|
-
function retrogradeGaps(notes) {
|
|
1092
|
-
const out = [];
|
|
1093
|
-
for (let i = notes.length - 1; i >= 1; i -= 1) {
|
|
1094
|
-
const later = notes[i];
|
|
1095
|
-
const earlier = notes[i - 1];
|
|
1096
|
-
out.push(
|
|
1097
|
-
(later?.startBeat ?? 0) + (later?.durationBeat ?? 0) - ((earlier?.startBeat ?? 0) + (earlier?.durationBeat ?? 0))
|
|
1098
|
-
);
|
|
1099
|
-
}
|
|
1100
|
-
return out;
|
|
1101
|
-
}
|
|
1102
|
-
function rhythmProfile(gaps) {
|
|
1103
|
-
const unit = gaps[0] ?? 0;
|
|
1104
|
-
if (!(unit > BEAT_EPS)) {
|
|
1105
|
-
return gaps.length === 0 ? [] : null;
|
|
1106
|
-
}
|
|
1107
|
-
return gaps.map((gap) => Math.round(gap / unit * RHYTHM_GRID) / RHYTHM_GRID);
|
|
1108
|
-
}
|
|
1109
|
-
function sameNumbers(a, b) {
|
|
1110
|
-
if (a.length !== b.length) {
|
|
1111
|
-
return false;
|
|
1112
|
-
}
|
|
1113
|
-
for (let i = 0; i < a.length; i += 1) {
|
|
1114
|
-
if (Math.abs((a[i] ?? 0) - (b[i] ?? 0)) > BEAT_EPS) {
|
|
1115
|
-
return false;
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
return true;
|
|
1119
|
-
}
|
|
1120
|
-
function roundTo(value, digits) {
|
|
1121
|
-
const scale = 10 ** digits;
|
|
1122
|
-
return Math.round(value * scale) / scale;
|
|
1123
|
-
}
|
|
1124
|
-
function endBeatOf(notes) {
|
|
1125
|
-
let end = 0;
|
|
1126
|
-
for (const note of notes) {
|
|
1127
|
-
end = Math.max(end, note.startBeat + note.durationBeat);
|
|
1128
|
-
}
|
|
1129
|
-
return end;
|
|
1130
|
-
}
|
|
1131
|
-
function onsetSpan(notes) {
|
|
1132
|
-
if (notes.length < 2) {
|
|
1133
|
-
return 0;
|
|
1134
|
-
}
|
|
1135
|
-
return (notes[notes.length - 1]?.startBeat ?? 0) - (notes[0]?.startBeat ?? 0);
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
// src/analyze/melody/contour.ts
|
|
1139
|
-
var DIRECTIONAL_SHARE = 0.75;
|
|
1140
|
-
function extremes(notes) {
|
|
1141
|
-
if (notes.length === 0) {
|
|
1142
|
-
return { peak: -1, trough: -1 };
|
|
1143
|
-
}
|
|
1144
|
-
let peak = 0;
|
|
1145
|
-
let trough = 0;
|
|
1146
|
-
for (let i = 1; i < notes.length; i += 1) {
|
|
1147
|
-
if ((notes[i]?.pitch ?? 0) > (notes[peak]?.pitch ?? 0)) {
|
|
1148
|
-
peak = i;
|
|
1149
|
-
}
|
|
1150
|
-
if ((notes[i]?.pitch ?? 0) < (notes[trough]?.pitch ?? 0)) {
|
|
1151
|
-
trough = i;
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
return { peak, trough };
|
|
1155
|
-
}
|
|
1156
|
-
function melodicContour(notes) {
|
|
1157
|
-
const line = orderedNotes(phraseNotes(notes), "contour notes");
|
|
1158
|
-
const intervals = intervalsOf(line);
|
|
1159
|
-
const directions = intervals.map(
|
|
1160
|
-
(step) => step > 0 ? "up" : step < 0 ? "down" : "same"
|
|
1161
|
-
);
|
|
1162
|
-
const { peak, trough } = extremes(line);
|
|
1163
|
-
const range = (line[peak]?.pitch ?? 0) - (line[trough]?.pitch ?? 0);
|
|
1164
|
-
const motion = intervals.reduce((sum, step) => sum + Math.abs(step), 0);
|
|
1165
|
-
if (motion === 0) {
|
|
1166
|
-
return {
|
|
1167
|
-
directions,
|
|
1168
|
-
shape: "static",
|
|
1169
|
-
peakIndex: peak,
|
|
1170
|
-
troughIndex: trough,
|
|
1171
|
-
range: 0,
|
|
1172
|
-
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"
|
|
1173
|
-
};
|
|
1174
|
-
}
|
|
1175
|
-
const first = line[0]?.pitch ?? 0;
|
|
1176
|
-
const last = line[line.length - 1]?.pitch ?? 0;
|
|
1177
|
-
const net = last - first;
|
|
1178
|
-
const directness = Math.abs(net) / motion;
|
|
1179
|
-
const share = Math.round(directness * 100);
|
|
1180
|
-
if (directness >= DIRECTIONAL_SHARE) {
|
|
1181
|
-
const shape = net > 0 ? "ascending" : "descending";
|
|
1182
|
-
return {
|
|
1183
|
-
directions,
|
|
1184
|
-
shape,
|
|
1185
|
-
peakIndex: peak,
|
|
1186
|
-
troughIndex: trough,
|
|
1187
|
-
range,
|
|
1188
|
-
rationale: `${share}% of the motion runs ${net > 0 ? "up" : "down"}, so the line is ${shape}`
|
|
1189
|
-
};
|
|
1190
|
-
}
|
|
1191
|
-
const peakPitch = line[peak]?.pitch ?? 0;
|
|
1192
|
-
const archMotion = peakPitch - first + (peakPitch - last);
|
|
1193
|
-
if (peak > 0 && peak < line.length - 1 && archMotion >= DIRECTIONAL_SHARE * motion) {
|
|
1194
|
-
return {
|
|
1195
|
-
directions,
|
|
1196
|
-
shape: "arch",
|
|
1197
|
-
peakIndex: peak,
|
|
1198
|
-
troughIndex: trough,
|
|
1199
|
-
range,
|
|
1200
|
-
rationale: `Rises to its peak at note ${peak + 1} of ${line.length} and falls back, so the line is an arch`
|
|
1201
|
-
};
|
|
1202
|
-
}
|
|
1203
|
-
return {
|
|
1204
|
-
directions,
|
|
1205
|
-
shape: "wave",
|
|
1206
|
-
peakIndex: peak,
|
|
1207
|
-
troughIndex: trough,
|
|
1208
|
-
range,
|
|
1209
|
-
rationale: `Changes direction without settling either way (${share}% of the motion nets out), so the line is a wave`
|
|
1210
|
-
};
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
// src/analyze/melody/motifs.ts
|
|
1214
|
-
var MIN_CELL_NOTES = 3;
|
|
1215
|
-
var MAX_CELL_NOTES = 8;
|
|
1216
|
-
var MIN_OCCURRENCES = 2;
|
|
1217
|
-
function windowSignature(notes) {
|
|
1218
|
-
const profile = rhythmProfile(onsetGaps(notes));
|
|
1219
|
-
if (profile === null) {
|
|
1220
|
-
return null;
|
|
1221
|
-
}
|
|
1222
|
-
return `${notes.length}|${intervalsOf(notes).join(",")}|${profile.join(",")}`;
|
|
1223
|
-
}
|
|
1224
|
-
function nonOverlapping(starts, length) {
|
|
1225
|
-
const picked = [];
|
|
1226
|
-
let lastEnd = Number.NEGATIVE_INFINITY;
|
|
1227
|
-
for (const start of starts) {
|
|
1228
|
-
if (start > lastEnd) {
|
|
1229
|
-
picked.push(start);
|
|
1230
|
-
lastEnd = start + length - 1;
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
return picked;
|
|
1234
|
-
}
|
|
1235
|
-
function subsumes(outer, inner) {
|
|
1236
|
-
let at = 0;
|
|
1237
|
-
for (const start of inner.starts) {
|
|
1238
|
-
while (at + 1 < outer.starts.length && (outer.starts[at + 1] ?? 0) <= start) {
|
|
1239
|
-
at += 1;
|
|
1240
|
-
}
|
|
1241
|
-
const from = outer.starts[at];
|
|
1242
|
-
if (from === void 0 || start < from || start + inner.length > from + outer.length) {
|
|
1243
|
-
return false;
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
return true;
|
|
1247
|
-
}
|
|
1248
|
-
function listBeats(occurrences) {
|
|
1249
|
-
const shown = occurrences.slice(0, 5).map((occurrence) => roundTo(occurrence.startBeat, 3));
|
|
1250
|
-
return occurrences.length > shown.length ? `${shown.join(", ")}, and ${occurrences.length - shown.length} more` : shown.join(", ");
|
|
1251
|
-
}
|
|
1252
|
-
function motifFromGroup(group, notes) {
|
|
1253
|
-
const first = group.starts[0] ?? 0;
|
|
1254
|
-
const prime = notes.slice(first, first + group.length).map((note) => ({ ...note }));
|
|
1255
|
-
const primePitch = prime[0]?.pitch ?? 0;
|
|
1256
|
-
const primeSpan = onsetSpan(prime);
|
|
1257
|
-
const occurrences = group.starts.map((start) => {
|
|
1258
|
-
const statement = notes.slice(start, start + group.length);
|
|
1259
|
-
const span = onsetSpan(statement);
|
|
1260
|
-
return {
|
|
1261
|
-
noteIndex: start,
|
|
1262
|
-
startBeat: statement[0]?.startBeat ?? 0,
|
|
1263
|
-
endBeat: endBeatOf(statement),
|
|
1264
|
-
transpose: (statement[0]?.pitch ?? 0) - primePitch,
|
|
1265
|
-
timeRatio: primeSpan > BEAT_EPS ? span / primeSpan : 1
|
|
1266
|
-
};
|
|
1267
|
-
});
|
|
1268
|
-
const shape = melodicContour(prime).shape;
|
|
1269
|
-
const transposed = occurrences.some((occurrence) => occurrence.transpose !== 0);
|
|
1270
|
-
const stretched = occurrences.some((occurrence) => Math.abs(occurrence.timeRatio - 1) > BEAT_EPS);
|
|
1271
|
-
const detail = [
|
|
1272
|
-
transposed ? "at more than one pitch level" : "at the same pitch level",
|
|
1273
|
-
stretched ? "and in more than one set of note values" : ""
|
|
1274
|
-
].filter((part) => part !== "").join(" ");
|
|
1275
|
-
return {
|
|
1276
|
-
notes: prime,
|
|
1277
|
-
intervals: intervalsOf(prime),
|
|
1278
|
-
rhythm: rhythmProfile(onsetGaps(prime)) ?? [],
|
|
1279
|
-
occurrences,
|
|
1280
|
-
rationale: `${group.length}-note ${shape} cell stated ${occurrences.length} ${occurrences.length === 1 ? "time" : "times"} (beats ${listBeats(occurrences)}), ${detail}`
|
|
1281
|
-
};
|
|
1282
|
-
}
|
|
1283
|
-
function extractMotifs(notes, opts = {}) {
|
|
1284
|
-
const asked = assertOptions(opts, "opts");
|
|
1285
|
-
const minNotes = assertInteger(asked.minNotes ?? MIN_CELL_NOTES, "motif minNotes", 2, 64);
|
|
1286
|
-
const maxNotes = assertInteger(asked.maxNotes ?? MAX_CELL_NOTES, "motif maxNotes", 2, 64);
|
|
1287
|
-
if (maxNotes < minNotes) {
|
|
1288
|
-
throw new InvalidInputError(
|
|
1289
|
-
`motif maxNotes must be at least minNotes ${minNotes}; received ${maxNotes}`
|
|
1290
|
-
);
|
|
1291
|
-
}
|
|
1292
|
-
const minOccurrences = assertInteger(
|
|
1293
|
-
asked.minOccurrences ?? MIN_OCCURRENCES,
|
|
1294
|
-
"motif minOccurrences",
|
|
1295
|
-
2
|
|
1296
|
-
);
|
|
1297
|
-
const sounding = orderedNotes(notes, "melody notes", asked.budget);
|
|
1298
|
-
if (sounding.length < minNotes) {
|
|
1299
|
-
return [];
|
|
1300
|
-
}
|
|
1301
|
-
assertGenerationBudget(
|
|
1302
|
-
sounding.length * (maxNotes - minNotes + 1),
|
|
1303
|
-
"motif windows",
|
|
1304
|
-
asked.budget
|
|
1305
|
-
);
|
|
1306
|
-
const groups = /* @__PURE__ */ new Map();
|
|
1307
|
-
for (let length = minNotes; length <= maxNotes; length += 1) {
|
|
1308
|
-
for (let start = 0; start + length <= sounding.length; start += 1) {
|
|
1309
|
-
const signature = windowSignature(sounding.slice(start, start + length));
|
|
1310
|
-
if (signature === null) {
|
|
1311
|
-
continue;
|
|
1312
|
-
}
|
|
1313
|
-
const group = groups.get(signature);
|
|
1314
|
-
if (group === void 0) {
|
|
1315
|
-
groups.set(signature, { signature, length, starts: [start] });
|
|
1316
|
-
} else {
|
|
1317
|
-
group.starts.push(start);
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
const candidates = [...groups.values()].map((group) => ({ ...group, starts: nonOverlapping(group.starts, group.length) })).filter((group) => group.starts.length >= minOccurrences).sort(
|
|
1322
|
-
(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)
|
|
1323
|
-
);
|
|
1324
|
-
const keptByLength = /* @__PURE__ */ new Map();
|
|
1325
|
-
const kept = [];
|
|
1326
|
-
let comparisons = 0;
|
|
1327
|
-
for (const candidate of candidates) {
|
|
1328
|
-
const first = candidate.starts[0] ?? 0;
|
|
1329
|
-
let covered = false;
|
|
1330
|
-
for (let length = candidate.length + 1; length <= maxNotes && !covered; length += 1) {
|
|
1331
|
-
const byStart = keptByLength.get(length);
|
|
1332
|
-
if (byStart === void 0) {
|
|
1333
|
-
continue;
|
|
1334
|
-
}
|
|
1335
|
-
for (let from = Math.max(0, first - (length - candidate.length)); from <= first; from += 1) {
|
|
1336
|
-
comparisons += 1;
|
|
1337
|
-
const outer = byStart.get(from);
|
|
1338
|
-
if (outer === void 0 || outer.starts.length < candidate.starts.length) {
|
|
1339
|
-
continue;
|
|
1340
|
-
}
|
|
1341
|
-
comparisons += outer.starts.length + candidate.starts.length;
|
|
1342
|
-
if (subsumes(outer, candidate)) {
|
|
1343
|
-
covered = true;
|
|
1344
|
-
break;
|
|
1345
|
-
}
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
assertGenerationBudget(comparisons, "motif subsumption comparisons", asked.budget);
|
|
1349
|
-
if (!covered) {
|
|
1350
|
-
kept.push(candidate);
|
|
1351
|
-
let byStart = keptByLength.get(candidate.length);
|
|
1352
|
-
if (byStart === void 0) {
|
|
1353
|
-
byStart = /* @__PURE__ */ new Map();
|
|
1354
|
-
keptByLength.set(candidate.length, byStart);
|
|
1355
|
-
}
|
|
1356
|
-
for (const start of candidate.starts) {
|
|
1357
|
-
byStart.set(start, candidate);
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
return kept.map((group) => motifFromGroup(group, sounding));
|
|
1362
|
-
}
|
|
1363
|
-
function motifFromNotes(notes) {
|
|
1364
|
-
const sounding = orderedNotes(notes, "motif notes").map((note) => ({ ...note }));
|
|
1365
|
-
return motifFromGroup(
|
|
1366
|
-
{ signature: windowSignature(sounding) ?? "", length: sounding.length, starts: [0] },
|
|
1367
|
-
sounding
|
|
1368
|
-
);
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
// src/analyze/melody/relation.ts
|
|
1372
|
-
function spellingOf(key) {
|
|
1373
|
-
if (key === void 0) {
|
|
1374
|
-
return "sharp";
|
|
1375
|
-
}
|
|
1376
|
-
return keySignatureFifths(key.tonic, key.scale) < 0 ? "flat" : "sharp";
|
|
1377
|
-
}
|
|
1378
|
-
function intervalBetween(from, to, key) {
|
|
1379
|
-
const spelling = spellingOf(key);
|
|
1380
|
-
return spelledInterval(midiToNote(from, spelling), midiToNote(to, spelling));
|
|
1381
|
-
}
|
|
1382
|
-
function intervalName(interval) {
|
|
1383
|
-
return `${interval.descending && interval.semitones !== 0 ? "-" : ""}${interval.quality}${interval.number}`;
|
|
1384
|
-
}
|
|
1385
|
-
function ladderIndices(notes, key) {
|
|
1386
|
-
const root = pitchClassOf(key.rootPc);
|
|
1387
|
-
const offsets = scaleTonesInDegreeOrder(key).map((pc) => pitchClassOf(pc - root));
|
|
1388
|
-
if (offsets.length === 0) {
|
|
1389
|
-
return null;
|
|
1390
|
-
}
|
|
1391
|
-
const out = [];
|
|
1392
|
-
for (const note of notes) {
|
|
1393
|
-
const semitones = note.pitch - root;
|
|
1394
|
-
const octave = Math.floor(semitones / 12);
|
|
1395
|
-
const degree = offsets.indexOf(semitones - octave * 12);
|
|
1396
|
-
if (degree < 0) {
|
|
1397
|
-
return null;
|
|
1398
|
-
}
|
|
1399
|
-
out.push(octave * offsets.length + degree);
|
|
1400
|
-
}
|
|
1401
|
-
return out;
|
|
1402
|
-
}
|
|
1403
|
-
function ladderSteps(indices) {
|
|
1404
|
-
const out = [];
|
|
1405
|
-
for (let i = 1; i < indices.length; i += 1) {
|
|
1406
|
-
out.push((indices[i] ?? 0) - (indices[i - 1] ?? 0));
|
|
1407
|
-
}
|
|
1408
|
-
return out;
|
|
1409
|
-
}
|
|
1410
|
-
function pitchShapes(a, b) {
|
|
1411
|
-
const shapes = [];
|
|
1412
|
-
if (sameNumbers(a, b)) {
|
|
1413
|
-
shapes.push("exact");
|
|
1414
|
-
}
|
|
1415
|
-
if (sameNumbers(
|
|
1416
|
-
b,
|
|
1417
|
-
a.map((step) => -step)
|
|
1418
|
-
)) {
|
|
1419
|
-
shapes.push("inversion");
|
|
1420
|
-
}
|
|
1421
|
-
const reversed = [...a].reverse();
|
|
1422
|
-
if (sameNumbers(
|
|
1423
|
-
b,
|
|
1424
|
-
reversed.map((step) => -step)
|
|
1425
|
-
)) {
|
|
1426
|
-
shapes.push("retrograde");
|
|
1427
|
-
}
|
|
1428
|
-
if (sameNumbers(b, reversed)) {
|
|
1429
|
-
shapes.push("retrogradeInversion");
|
|
1430
|
-
}
|
|
1431
|
-
return shapes;
|
|
1432
|
-
}
|
|
1433
|
-
function totalGap(gaps) {
|
|
1434
|
-
return gaps.reduce((sum, gap) => sum + gap, 0);
|
|
1435
|
-
}
|
|
1436
|
-
function stretchRatio(modelGaps, answerGaps) {
|
|
1437
|
-
const span = totalGap(modelGaps);
|
|
1438
|
-
return span > BEAT_EPS ? totalGap(answerGaps) / span : 1;
|
|
1439
|
-
}
|
|
1440
|
-
function stretchPhrase(ratio) {
|
|
1441
|
-
return `note values ${roundTo(ratio, 3)}x the model's`;
|
|
1442
|
-
}
|
|
1443
|
-
function relateMotifs(a, b, keyLike) {
|
|
1444
|
-
const key = keyLike === void 0 ? void 0 : resolveKey(keyLike);
|
|
1445
|
-
const model = assertArray(assertRecord(a, "a").notes, "a.notes");
|
|
1446
|
-
const answer = assertArray(assertRecord(b, "b").notes, "b.notes");
|
|
1447
|
-
if (model.length === 0 || model.length !== answer.length) {
|
|
1448
|
-
return null;
|
|
1449
|
-
}
|
|
1450
|
-
const modelGaps = onsetGaps(model);
|
|
1451
|
-
const answerGaps = onsetGaps(answer);
|
|
1452
|
-
const modelProfile = rhythmProfile(modelGaps);
|
|
1453
|
-
const answerProfile = rhythmProfile(answerGaps);
|
|
1454
|
-
const retroGaps = retrogradeGaps(model);
|
|
1455
|
-
const retroProfile = rhythmProfile(retroGaps);
|
|
1456
|
-
if (modelProfile === null || answerProfile === null) {
|
|
1457
|
-
return null;
|
|
1458
|
-
}
|
|
1459
|
-
const forward = sameNumbers(modelProfile, answerProfile);
|
|
1460
|
-
const backward = retroProfile !== null && sameNumbers(retroProfile, answerProfile);
|
|
1461
|
-
const semitones = (answer[0]?.pitch ?? 0) - (model[0]?.pitch ?? 0);
|
|
1462
|
-
const interval = intervalBetween(model[0]?.pitch ?? 0, answer[0]?.pitch ?? 0, key);
|
|
1463
|
-
const gap = (answer[0]?.startBeat ?? 0) - endBeatOf(model);
|
|
1464
|
-
const sequence = gap >= -HUMANIZE_ADJACENCY && gap <= HUMANIZE_ADJACENCY;
|
|
1465
|
-
const timeRatio = stretchRatio(modelGaps, answerGaps);
|
|
1466
|
-
const stretched = Math.abs(timeRatio - 1) > BEAT_EPS;
|
|
1467
|
-
const shapes = pitchShapes(intervalsOf(model), intervalsOf(answer));
|
|
1468
|
-
const base = { sequence, semitones, interval, timeRatio };
|
|
1469
|
-
const backwardBase = { ...base, timeRatio: stretchRatio(retroGaps, answerGaps) };
|
|
1470
|
-
if (forward && shapes.includes("exact")) {
|
|
1471
|
-
if (semitones === 0) {
|
|
1472
|
-
if (!stretched) {
|
|
1473
|
-
return {
|
|
1474
|
-
...base,
|
|
1475
|
-
kind: "repetition",
|
|
1476
|
-
rationale: "Exact repetition: the same pitches in the same rhythm"
|
|
1477
|
-
};
|
|
1478
|
-
}
|
|
1479
|
-
const augmented = timeRatio > 1;
|
|
1480
|
-
return {
|
|
1481
|
-
...base,
|
|
1482
|
-
kind: augmented ? "augmentation" : "diminution",
|
|
1483
|
-
rationale: `${augmented ? "Augmentation" : "Diminution"}: the same pitches in ${stretchPhrase(timeRatio)}`
|
|
1484
|
-
};
|
|
1485
|
-
}
|
|
1486
|
-
const name = intervalName(interval);
|
|
1487
|
-
return {
|
|
1488
|
-
...base,
|
|
1489
|
-
kind: "transposition",
|
|
1490
|
-
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)}` : "")
|
|
1491
|
-
};
|
|
1492
|
-
}
|
|
1493
|
-
if (forward && key !== void 0) {
|
|
1494
|
-
const modelLadder = ladderIndices(model, key.scale);
|
|
1495
|
-
const answerLadder = ladderIndices(answer, key.scale);
|
|
1496
|
-
if (modelLadder !== null && answerLadder !== null) {
|
|
1497
|
-
const degrees = (answerLadder[0] ?? 0) - (modelLadder[0] ?? 0);
|
|
1498
|
-
if (degrees !== 0 && sameNumbers(ladderSteps(modelLadder), ladderSteps(answerLadder))) {
|
|
1499
|
-
const direction = degrees > 0 ? "higher" : "lower";
|
|
1500
|
-
const count = Math.abs(degrees);
|
|
1501
|
-
return {
|
|
1502
|
-
...base,
|
|
1503
|
-
kind: "tonalTransposition",
|
|
1504
|
-
degrees,
|
|
1505
|
-
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`
|
|
1506
|
-
};
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
}
|
|
1510
|
-
if (forward && shapes.includes("inversion")) {
|
|
1511
|
-
return {
|
|
1512
|
-
...base,
|
|
1513
|
-
kind: "inversion",
|
|
1514
|
-
rationale: "Inversion: every interval turned upside down about the first note" + (stretched ? `, in ${stretchPhrase(timeRatio)}` : "")
|
|
1515
|
-
};
|
|
1516
|
-
}
|
|
1517
|
-
if (backward && shapes.includes("retrograde")) {
|
|
1518
|
-
return {
|
|
1519
|
-
...backwardBase,
|
|
1520
|
-
kind: "retrograde",
|
|
1521
|
-
rationale: "Retrograde: the cell read back to front, rhythm included"
|
|
1522
|
-
};
|
|
1523
|
-
}
|
|
1524
|
-
if (backward && shapes.includes("retrogradeInversion")) {
|
|
1525
|
-
return {
|
|
1526
|
-
...backwardBase,
|
|
1527
|
-
kind: "retrogradeInversion",
|
|
1528
|
-
rationale: "Retrograde inversion: the cell read back to front and turned upside down"
|
|
1529
|
-
};
|
|
1530
|
-
}
|
|
1531
|
-
return null;
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
// src/analyze/melody/similarity.ts
|
|
1535
|
-
var INTERVAL_TOLERANCE = 6;
|
|
1536
|
-
var PITCH_WEIGHT = 0.75;
|
|
1537
|
-
var RHYTHM_WEIGHT = 0.25;
|
|
1538
|
-
function gradedEditDistance(a, b, cost) {
|
|
1539
|
-
let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
1540
|
-
for (let i = 1; i <= a.length; i += 1) {
|
|
1541
|
-
const row = new Array(b.length + 1);
|
|
1542
|
-
row[0] = i;
|
|
1543
|
-
for (let j = 1; j <= b.length; j += 1) {
|
|
1544
|
-
const substitution = (previous[j - 1] ?? 0) + cost(a[i - 1] ?? 0, b[j - 1] ?? 0);
|
|
1545
|
-
const deletion = (previous[j] ?? 0) + 1;
|
|
1546
|
-
const insertion = (row[j - 1] ?? 0) + 1;
|
|
1547
|
-
row[j] = Math.min(substitution, deletion, insertion);
|
|
1548
|
-
}
|
|
1549
|
-
previous = row;
|
|
1550
|
-
}
|
|
1551
|
-
return previous[b.length] ?? 0;
|
|
1552
|
-
}
|
|
1553
|
-
function distanceToSimilarity(distance, a, b) {
|
|
1554
|
-
const longest = Math.max(a, b);
|
|
1555
|
-
if (longest === 0) {
|
|
1556
|
-
return 1;
|
|
1557
|
-
}
|
|
1558
|
-
return Math.min(1, Math.max(0, 1 - distance / longest));
|
|
1559
|
-
}
|
|
1560
|
-
function intervalCost(x, y) {
|
|
1561
|
-
return Math.min(1, Math.abs(x - y) / INTERVAL_TOLERANCE);
|
|
1562
|
-
}
|
|
1563
|
-
function gapCost(x, y) {
|
|
1564
|
-
if (!(x > BEAT_EPS) || !(y > BEAT_EPS)) {
|
|
1565
|
-
return Math.abs(x - y) > BEAT_EPS ? 1 : 0;
|
|
1566
|
-
}
|
|
1567
|
-
return Math.min(1, Math.abs(Math.log2(x / y)));
|
|
1568
|
-
}
|
|
1569
|
-
function compareMelodies(a, b) {
|
|
1570
|
-
const left = orderedNotes(phraseNotes(a), "melody a");
|
|
1571
|
-
const right = orderedNotes(phraseNotes(b), "melody b");
|
|
1572
|
-
const leftIntervals = intervalsOf(left);
|
|
1573
|
-
const rightIntervals = intervalsOf(right);
|
|
1574
|
-
const pitchSimilarity = distanceToSimilarity(
|
|
1575
|
-
gradedEditDistance(leftIntervals, rightIntervals, intervalCost),
|
|
1576
|
-
leftIntervals.length,
|
|
1577
|
-
rightIntervals.length
|
|
1578
|
-
);
|
|
1579
|
-
const leftGaps = rhythmProfile(onsetGaps(left)) ?? onsetGaps(left);
|
|
1580
|
-
const rightGaps = rhythmProfile(onsetGaps(right)) ?? onsetGaps(right);
|
|
1581
|
-
const rhythmSimilarity = distanceToSimilarity(
|
|
1582
|
-
gradedEditDistance(leftGaps, rightGaps, gapCost),
|
|
1583
|
-
leftGaps.length,
|
|
1584
|
-
rightGaps.length
|
|
1585
|
-
);
|
|
1586
|
-
const similarity = PITCH_WEIGHT * pitchSimilarity + RHYTHM_WEIGHT * rhythmSimilarity;
|
|
1587
|
-
return {
|
|
1588
|
-
similarity,
|
|
1589
|
-
pitchSimilarity,
|
|
1590
|
-
rhythmSimilarity,
|
|
1591
|
-
rationale: `Interval shapes ${Math.round(pitchSimilarity * 100)}% alike, rhythms ${Math.round(rhythmSimilarity * 100)}% alike, weighted ${PITCH_WEIGHT}/${RHYTHM_WEIGHT} into ${roundTo(similarity, 3)}`
|
|
1592
|
-
};
|
|
1593
|
-
}
|
|
1594
|
-
function melodicSimilarity(a, b) {
|
|
1595
|
-
return compareMelodies(a, b).similarity;
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1598
1094
|
// src/analyze/form/phrase.ts
|
|
1599
1095
|
var CADENCE_BOUNDARY_STRENGTH = {
|
|
1600
1096
|
authentic: 0.9,
|
|
@@ -1692,17 +1188,6 @@ function choosePhrasePath(beats, strengths, minPhraseBeats, expected) {
|
|
|
1692
1188
|
function arrivalEnd(timeline, atBeat) {
|
|
1693
1189
|
return segmentStartingAt(timeline.segments, atBeat)?.endBeat ?? atBeat;
|
|
1694
1190
|
}
|
|
1695
|
-
function barSpanOf(startBeat, endBeat, meters) {
|
|
1696
|
-
const firstBar = barIndexAt(startBeat, meters);
|
|
1697
|
-
const lastBar = barIndexAt(endBeat, meters);
|
|
1698
|
-
const partOf = (beat, bar) => {
|
|
1699
|
-
const barStart = barPositionToBeat({ bar, beat: 0 }, meters);
|
|
1700
|
-
const barLength = beatsPerBarAt(barStart, meters);
|
|
1701
|
-
return barLength > 0 ? (beat - barStart) / barLength : 0;
|
|
1702
|
-
};
|
|
1703
|
-
const bars = lastBar - firstBar + partOf(endBeat, lastBar) - partOf(startBeat, firstBar);
|
|
1704
|
-
return Math.round(bars * 100) / 100;
|
|
1705
|
-
}
|
|
1706
1191
|
function keyContextFor(opts, notes, meters, totalBeats) {
|
|
1707
1192
|
if (opts.key !== void 0) {
|
|
1708
1193
|
return opts.key;
|
|
@@ -1912,7 +1397,7 @@ function phrasesFromTimeline(timeline, notes, opts = {}) {
|
|
|
1912
1397
|
isLast
|
|
1913
1398
|
);
|
|
1914
1399
|
const signals = cadence === null ? reading.signals.filter((signal) => signal !== "cadence") : reading.signals;
|
|
1915
|
-
const bars = barSpanOf(startBeat, endBeat, meters);
|
|
1400
|
+
const bars = Math.round(barSpanOf(startBeat, endBeat, meters) * 100) / 100;
|
|
1916
1401
|
phrases.push({
|
|
1917
1402
|
startBeat,
|
|
1918
1403
|
endBeat,
|
|
@@ -2098,156 +1583,277 @@ function sectionsFromNotes(notes, opts = {}) {
|
|
|
2098
1583
|
return sections;
|
|
2099
1584
|
}
|
|
2100
1585
|
|
|
2101
|
-
// src/analyze/
|
|
2102
|
-
var
|
|
2103
|
-
function
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
}
|
|
2110
|
-
function touches(before, after) {
|
|
2111
|
-
return Math.abs(after.startBeat - before.endBeat) <= BEAT_EPS;
|
|
2112
|
-
}
|
|
2113
|
-
function salienceFloors(durations) {
|
|
2114
|
-
const order = durations.map((_, index) => index).sort((a, b) => {
|
|
2115
|
-
const left = durations[a] ?? 0;
|
|
2116
|
-
const right = durations[b] ?? 0;
|
|
2117
|
-
return left - right || a - b;
|
|
2118
|
-
});
|
|
2119
|
-
const rankOf = new Array(durations.length).fill(0);
|
|
2120
|
-
for (let rank = 0; rank < order.length; rank += 1) {
|
|
2121
|
-
rankOf[order[rank] ?? 0] = rank;
|
|
2122
|
-
}
|
|
2123
|
-
const floors = [];
|
|
2124
|
-
const remaining = durations.length - 1;
|
|
2125
|
-
for (let index = 0; index < durations.length; index += 1) {
|
|
2126
|
-
if (remaining <= 0) {
|
|
2127
|
-
floors.push(0);
|
|
2128
|
-
continue;
|
|
1586
|
+
// src/analyze/reference/compare.ts
|
|
1587
|
+
var RATIONALE_FIELDS = 2;
|
|
1588
|
+
function rationaleOf(comparison) {
|
|
1589
|
+
const measured = [];
|
|
1590
|
+
for (const group of [comparison.form, comparison.harmony, comparison.melody, comparison.rhythm]) {
|
|
1591
|
+
for (const [name, value] of Object.entries(group)) {
|
|
1592
|
+
if (value !== null) {
|
|
1593
|
+
measured.push({ name, value, order: measured.length });
|
|
1594
|
+
}
|
|
2129
1595
|
}
|
|
2130
|
-
const skipped = rankOf[index] ?? 0;
|
|
2131
|
-
const at = (position) => durations[order[position < skipped ? position : position + 1] ?? 0] ?? 0;
|
|
2132
|
-
const middle = remaining >> 1;
|
|
2133
|
-
floors.push(remaining % 2 === 1 ? at(middle) : (at(middle - 1) + at(middle)) / 2);
|
|
2134
|
-
}
|
|
2135
|
-
return floors;
|
|
2136
|
-
}
|
|
2137
|
-
function isTonicChord(chord, key) {
|
|
2138
|
-
return pitchClassOf(chord.rootPc - key.rootPc) === 0 && !isAppliedDominant(chord, key);
|
|
2139
|
-
}
|
|
2140
|
-
function functionalReason(segment, before, after, keyAt) {
|
|
2141
|
-
const key = keyAt(segment.startBeat);
|
|
2142
|
-
if (after !== void 0 && isCadentialSixFour(segment.chord, after.chord, key)) {
|
|
2143
|
-
return { kind: "cadentialSixFour" };
|
|
2144
|
-
}
|
|
2145
|
-
if (isTonicChord(segment.chord, key)) {
|
|
2146
|
-
return { kind: "tonic" };
|
|
2147
|
-
}
|
|
2148
|
-
if (isDominantChordOf(segment.chord, key)) {
|
|
2149
|
-
return { kind: "dominant" };
|
|
2150
|
-
}
|
|
2151
|
-
if (before !== void 0) {
|
|
2152
|
-
const arrival = detectCadence(before.chord, segment.chord, key).type;
|
|
2153
|
-
if (arrival !== null) {
|
|
2154
|
-
return { kind: "cadenceArrival", cadence: arrival };
|
|
2155
|
-
}
|
|
2156
|
-
}
|
|
2157
|
-
if (after !== void 0) {
|
|
2158
|
-
const agent = detectCadence(segment.chord, after.chord, keyAt(after.startBeat)).type;
|
|
2159
|
-
if (agent !== null && TONIC_ARRIVING.has(agent)) {
|
|
2160
|
-
return { kind: "cadenceAgent", cadence: agent };
|
|
2161
|
-
}
|
|
2162
|
-
}
|
|
2163
|
-
return null;
|
|
2164
|
-
}
|
|
2165
|
-
function figureReason(segment, before, after) {
|
|
2166
|
-
if (before === void 0 || after === void 0) {
|
|
2167
|
-
return null;
|
|
2168
|
-
}
|
|
2169
|
-
const incoming = signedStep(before.chord.rootPc, segment.chord.rootPc);
|
|
2170
|
-
const outgoing = signedStep(segment.chord.rootPc, after.chord.rootPc);
|
|
2171
|
-
if (!isStep(incoming) || !isStep(outgoing)) {
|
|
2172
|
-
return null;
|
|
2173
|
-
}
|
|
2174
|
-
if (incoming > 0 === outgoing > 0) {
|
|
2175
|
-
return { kind: "passing" };
|
|
2176
|
-
}
|
|
2177
|
-
return pitchClassOf(before.chord.rootPc) === pitchClassOf(after.chord.rootPc) ? { kind: "neighbor" } : null;
|
|
2178
|
-
}
|
|
2179
|
-
function describe(reason) {
|
|
2180
|
-
switch (reason.kind) {
|
|
2181
|
-
case "tonic":
|
|
2182
|
-
return "Structural: the tonic of the key, which the progression is heard against";
|
|
2183
|
-
case "dominant":
|
|
2184
|
-
return "Structural: the dominant of the key, the chord its tonic is approached from";
|
|
2185
|
-
case "cadentialSixFour":
|
|
2186
|
-
return "Structural: a tonic six-four on the bass of the dominant that follows, which prolongs that dominant rather than framing the tonic";
|
|
2187
|
-
case "cadenceArrival":
|
|
2188
|
-
return `Structural: the arrival of a ${reason.cadence} cadence`;
|
|
2189
|
-
case "cadenceAgent":
|
|
2190
|
-
return `Structural: the chord that drives a ${reason.cadence} cadence`;
|
|
2191
|
-
case "salient":
|
|
2192
|
-
return "Structural: it holds the harmony longer than the chords around it";
|
|
2193
|
-
case "first":
|
|
2194
|
-
return "Structural: the first chord of the progression frames it";
|
|
2195
|
-
case "last":
|
|
2196
|
-
return "Structural: the last chord of the progression frames it";
|
|
2197
|
-
case "unfigured":
|
|
2198
|
-
return "Structural: its root neither steps through to the next chord nor returns to the last, so it embellishes nothing";
|
|
2199
|
-
case "passing":
|
|
2200
|
-
return "Passing: its root steps in from the previous chord and on to the next in the same direction";
|
|
2201
|
-
case "neighbor":
|
|
2202
|
-
return "Neighbor: its root steps away from the surrounding harmony and returns to it";
|
|
2203
|
-
}
|
|
2204
|
-
}
|
|
2205
|
-
var BASIS_READINGS = {
|
|
2206
|
-
function: ({ segment, before, after, keyAt }) => functionalReason(segment, before, after, keyAt),
|
|
2207
|
-
duration: ({ segment, salienceFloor }) => segment.endBeat - segment.startBeat > salienceFloor ? { kind: "salient" } : null
|
|
2208
|
-
};
|
|
2209
|
-
var REDUCTION_BASES = Object.keys(BASIS_READINGS);
|
|
2210
|
-
function levelOf(reason) {
|
|
2211
|
-
if (reason.kind === "passing") {
|
|
2212
|
-
return "passing";
|
|
2213
1596
|
}
|
|
2214
|
-
|
|
1597
|
+
if (measured.length === 0) {
|
|
1598
|
+
return "Nothing to compare: neither profile has material for any field";
|
|
1599
|
+
}
|
|
1600
|
+
const describe = (entries) => entries.map((entry) => `${entry.name} ${Math.round(entry.value * 100)}%`).join(", ");
|
|
1601
|
+
const highest = [...measured].sort((x, y) => y.value - x.value || x.order - y.order);
|
|
1602
|
+
const lowest = [...measured].sort((x, y) => x.value - y.value || x.order - y.order);
|
|
1603
|
+
return `Most alike: ${describe(highest.slice(0, RATIONALE_FIELDS))}; least alike: ${describe(lowest.slice(0, RATIONALE_FIELDS))}`;
|
|
1604
|
+
}
|
|
1605
|
+
function compareReferences(a, b, opts) {
|
|
1606
|
+
assertReferenceProfile(a, "reference profile a");
|
|
1607
|
+
assertReferenceProfile(b, "reference profile b");
|
|
1608
|
+
const { budget } = assertOptions(opts, "compare options");
|
|
1609
|
+
const structural = (profile) => profile.harmony.chords.filter((chord) => chord.level === "structural");
|
|
1610
|
+
const cadences = (profile) => profile.form.phrases.map((phrase) => phrase.cadence?.type ?? null);
|
|
1611
|
+
const phraseMelodies = (profile) => profile.form.phrases.map((phrase) => phrase.melody);
|
|
1612
|
+
const comparison = {
|
|
1613
|
+
form: {
|
|
1614
|
+
sectionSequenceSimilarity: sectionSequenceSimilarity(
|
|
1615
|
+
a.form.sections.map((section) => section.label),
|
|
1616
|
+
b.form.sections.map((section) => section.label)
|
|
1617
|
+
),
|
|
1618
|
+
phraseLengthSimilarity: phraseLengthSimilarity(
|
|
1619
|
+
a.form.phrases.map((phrase) => phrase.bars),
|
|
1620
|
+
b.form.phrases.map((phrase) => phrase.bars)
|
|
1621
|
+
)
|
|
1622
|
+
},
|
|
1623
|
+
harmony: {
|
|
1624
|
+
progressionSimilarity: progressionSimilarity(structural(a), structural(b), budget),
|
|
1625
|
+
functionSimilarity: functionSimilarity(a.harmony.chords, b.harmony.chords),
|
|
1626
|
+
cadenceSimilarity: cadenceSimilarity(cadences(a), cadences(b)),
|
|
1627
|
+
keyPlanSimilarity: keyPlanSimilarity(
|
|
1628
|
+
a.harmony.keys.map((region) => region.key),
|
|
1629
|
+
b.harmony.keys.map((region) => region.key)
|
|
1630
|
+
),
|
|
1631
|
+
harmonicRhythmOnsetSimilarity: onsetSimilarity(a.harmony.rhythm, b.harmony.rhythm),
|
|
1632
|
+
harmonicRhythmDensitySimilarity: densitySimilarity(a.harmony.rhythm, b.harmony.rhythm)
|
|
1633
|
+
},
|
|
1634
|
+
melody: {
|
|
1635
|
+
surfaceSimilarity: surfaceSimilarity(a.melody.motifs, b.melody.motifs),
|
|
1636
|
+
motifStructureSimilarity: motifStructureSimilarity(
|
|
1637
|
+
{ graph: a.melody.graph, spanBeats: a.span.endBeat - a.span.startBeat },
|
|
1638
|
+
{ graph: b.melody.graph, spanBeats: b.span.endBeat - b.span.startBeat }
|
|
1639
|
+
),
|
|
1640
|
+
contourSimilarity: contourSimilarity(phraseMelodies(a), phraseMelodies(b)),
|
|
1641
|
+
registerSimilarity: registerSimilarity(
|
|
1642
|
+
{ phrases: phraseMelodies(a), mean: a.melody.register?.mean ?? null },
|
|
1643
|
+
{ phrases: phraseMelodies(b), mean: b.melody.register?.mean ?? null }
|
|
1644
|
+
)
|
|
1645
|
+
},
|
|
1646
|
+
rhythm: {
|
|
1647
|
+
onsetSimilarity: onsetSimilarity(a.melody.rhythm, b.melody.rhythm),
|
|
1648
|
+
densitySimilarity: densitySimilarity(a.melody.rhythm, b.melody.rhythm),
|
|
1649
|
+
durationSimilarity: durationSimilarity(a.melody.rhythm, b.melody.rhythm),
|
|
1650
|
+
syncopationSimilarity: syncopationSimilarity(a.melody.rhythm, b.melody.rhythm)
|
|
1651
|
+
}
|
|
1652
|
+
};
|
|
1653
|
+
return { ...comparison, rationale: rationaleOf(comparison) };
|
|
2215
1654
|
}
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
const keyAt = keyScaleAt(key);
|
|
2220
|
-
const basis = assertOneOf(asked.basis ?? "function", REDUCTION_BASES, "reduction basis");
|
|
2221
|
-
const reading = BASIS_READINGS[basis];
|
|
1655
|
+
|
|
1656
|
+
// src/analyze/reference/profile.ts
|
|
1657
|
+
function spanOf(sounding, timeline, meters, totalBeats) {
|
|
2222
1658
|
const segments = timeline.segments;
|
|
2223
|
-
const
|
|
2224
|
-
const
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
1659
|
+
const firstSegment = segments[0];
|
|
1660
|
+
const lastSegment = segments[segments.length - 1];
|
|
1661
|
+
const firstOnset = firstSoundingBeat(sounding, firstSegment?.startBeat ?? 0);
|
|
1662
|
+
const startBeat = barGridStart(
|
|
1663
|
+
Math.min(firstOnset, firstSegment?.startBeat ?? firstOnset),
|
|
1664
|
+
meters
|
|
1665
|
+
);
|
|
1666
|
+
const notesEnd = sounding.reduce(
|
|
1667
|
+
(end, note) => Math.max(end, note.startBeat + note.durationBeat),
|
|
1668
|
+
lastSegment?.endBeat ?? 0
|
|
1669
|
+
);
|
|
1670
|
+
return { startBeat, endBeat: Math.max(startBeat, totalBeats ?? notesEnd) };
|
|
1671
|
+
}
|
|
1672
|
+
function sameKey(a, b) {
|
|
1673
|
+
return pitchClassOf(a.scale.rootPc) === pitchClassOf(b.scale.rootPc) && a.scale.modeMask12 === b.scale.modeMask12;
|
|
1674
|
+
}
|
|
1675
|
+
function keyIndexAt(regions, beat, prevailing) {
|
|
1676
|
+
const holding = regions.findIndex((region) => region.startBeat <= beat && beat < region.endBeat);
|
|
1677
|
+
if (holding >= 0) {
|
|
1678
|
+
return holding;
|
|
1679
|
+
}
|
|
1680
|
+
return Math.max(
|
|
1681
|
+
0,
|
|
1682
|
+
regions.findIndex((region) => sameKey(region.key, prevailing))
|
|
1683
|
+
);
|
|
1684
|
+
}
|
|
1685
|
+
function sectionAt(sections, beat) {
|
|
1686
|
+
const index = sections.findIndex(
|
|
1687
|
+
(section) => beat >= section.startBeat - BEAT_EPS && beat < section.endBeat - BEAT_EPS
|
|
1688
|
+
);
|
|
1689
|
+
return index < 0 ? null : index;
|
|
1690
|
+
}
|
|
1691
|
+
function readingsFor(notes, opts = {}) {
|
|
1692
|
+
const asked = assertOptions(opts, "reference options");
|
|
1693
|
+
const meters = resolveMeters(asked, "reference meters");
|
|
1694
|
+
const budget = asked.budget;
|
|
1695
|
+
if (asked.timeline === void 0) {
|
|
1696
|
+
return chordTimelineFromNotes(notes, {
|
|
1697
|
+
meters,
|
|
1698
|
+
key: asked.key,
|
|
1699
|
+
totalBeats: asked.totalBeats,
|
|
1700
|
+
budget
|
|
2248
1701
|
});
|
|
2249
1702
|
}
|
|
2250
|
-
|
|
1703
|
+
const timeline = assertChordTimeline(asked.timeline, "reference timeline");
|
|
1704
|
+
let keys;
|
|
1705
|
+
if (asked.key === void 0) {
|
|
1706
|
+
keys = keyTimelineFromNotes(notes, { meters, totalBeats: asked.totalBeats, budget });
|
|
1707
|
+
} else {
|
|
1708
|
+
const sounding = assertNoteEvents(notes, "reference notes", {
|
|
1709
|
+
allowNonPositiveDuration: true,
|
|
1710
|
+
budget
|
|
1711
|
+
}).filter((note) => note.durationBeat > 0);
|
|
1712
|
+
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
|
+
];
|
|
1721
|
+
}
|
|
1722
|
+
return { timeline, keys, prevailingKey: prevailingKeyOf(keys) ?? resolveKey(majorKey(0)) };
|
|
1723
|
+
}
|
|
1724
|
+
function referenceFromReadings(notes, readings, opts = {}) {
|
|
1725
|
+
const asked = assertOptions(opts, "reference options");
|
|
1726
|
+
const meters = resolveMeters(asked, "reference meters");
|
|
1727
|
+
const budget = asked.budget;
|
|
1728
|
+
const sounding = assertNoteEvents(notes, "reference notes", {
|
|
1729
|
+
allowNonPositiveDuration: true,
|
|
1730
|
+
budget
|
|
1731
|
+
}).filter((note) => note.durationBeat > 0);
|
|
1732
|
+
if (asked.totalBeats !== void 0) {
|
|
1733
|
+
assertRange(asked.totalBeats, 0, Number.MAX_SAFE_INTEGER, "reference totalBeats");
|
|
1734
|
+
}
|
|
1735
|
+
const line = orderedNotes(asked.melody ?? notes, "reference melody", budget);
|
|
1736
|
+
const { timeline, prevailingKey } = readings;
|
|
1737
|
+
const span = spanOf(sounding, timeline, meters, asked.totalBeats);
|
|
1738
|
+
const spanEnd = span.endBeat;
|
|
1739
|
+
const keyAt = keyLookup(readings.keys, prevailingKey);
|
|
1740
|
+
const scaleAt = (beat) => scaleOf(keyAt(beat));
|
|
1741
|
+
const cadenceBeats = detectCadences(timeline, scaleAt).map((hit) => hit.atBeat);
|
|
1742
|
+
const grouping = hypermeter(sounding, meters, { cadenceBeats, totalBeats: spanEnd, budget });
|
|
1743
|
+
const phrases = phrasesFromTimeline(timeline, notes, {
|
|
1744
|
+
meters,
|
|
1745
|
+
key: scaleAt,
|
|
1746
|
+
hypermeter: grouping,
|
|
1747
|
+
totalBeats: spanEnd,
|
|
1748
|
+
budget
|
|
1749
|
+
});
|
|
1750
|
+
const cadences = /* @__PURE__ */ new Map();
|
|
1751
|
+
for (const closing of structuralCadences(phrases)) {
|
|
1752
|
+
const type = closing.cadence.type;
|
|
1753
|
+
if (type !== null) {
|
|
1754
|
+
cadences.set(closing.phraseIndex, {
|
|
1755
|
+
atBeat: closing.atBeat,
|
|
1756
|
+
type,
|
|
1757
|
+
weight: closing.weight
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
const sections = sectionsFromNotes(notes, {
|
|
1762
|
+
meters,
|
|
1763
|
+
unitBars: grouping.groupBars,
|
|
1764
|
+
totalBeats: spanEnd,
|
|
1765
|
+
budget
|
|
1766
|
+
});
|
|
1767
|
+
const keys = readings.keys.length === 0 && timeline.segments.length > 0 ? [
|
|
1768
|
+
{
|
|
1769
|
+
startBeat: span.startBeat,
|
|
1770
|
+
endBeat: spanEnd,
|
|
1771
|
+
key: resolveKey(prevailingKey),
|
|
1772
|
+
confidence: 0
|
|
1773
|
+
}
|
|
1774
|
+
] : readings.keys.map((region) => ({
|
|
1775
|
+
startBeat: region.startBeat,
|
|
1776
|
+
endBeat: region.endBeat,
|
|
1777
|
+
key: resolveKey(region.key),
|
|
1778
|
+
confidence: region.confidence
|
|
1779
|
+
}));
|
|
1780
|
+
const chords = reduceProgression(timeline, scaleAt).map((reduced) => {
|
|
1781
|
+
const index = keyIndexAt(keys, (reduced.startBeat + reduced.endBeat) / 2, prevailingKey);
|
|
1782
|
+
const key = keys[index].key;
|
|
1783
|
+
return {
|
|
1784
|
+
startBeat: reduced.startBeat,
|
|
1785
|
+
endBeat: reduced.endBeat,
|
|
1786
|
+
key: index,
|
|
1787
|
+
roman: chordToRoman(reduced.chord, key),
|
|
1788
|
+
function: functionOf(reduced.chord, key),
|
|
1789
|
+
level: reduced.level
|
|
1790
|
+
};
|
|
1791
|
+
});
|
|
1792
|
+
const chordOnsets = timeline.segments.map((segment) => ({
|
|
1793
|
+
pitch: 60,
|
|
1794
|
+
startBeat: segment.startBeat,
|
|
1795
|
+
durationBeat: segment.endBeat - segment.startBeat
|
|
1796
|
+
}));
|
|
1797
|
+
const harmonyRhythm = analyzeRhythm(chordOnsets, { meters, totalBeats: spanEnd, budget });
|
|
1798
|
+
const motifs = extractMotifs(line, { budget });
|
|
1799
|
+
const graph = motifGraph(line, motifs, { key: prevailingKey, budget });
|
|
1800
|
+
let register = null;
|
|
1801
|
+
if (line.length > 0) {
|
|
1802
|
+
const pitches = line.map((note) => note.pitch);
|
|
1803
|
+
register = {
|
|
1804
|
+
low: Math.min(...pitches),
|
|
1805
|
+
high: Math.max(...pitches),
|
|
1806
|
+
mean: weightedMean(line)
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
const lineMean = register?.mean ?? 0;
|
|
1810
|
+
const referenceMotifs = motifs.map((motif) => {
|
|
1811
|
+
const first = motif.occurrences[0];
|
|
1812
|
+
return {
|
|
1813
|
+
intervals: motif.intervals,
|
|
1814
|
+
rhythm: motif.rhythm,
|
|
1815
|
+
unitBeats: motif.notes.length < 2 ? 0 : motif.notes[1].startBeat - motif.notes[0].startBeat,
|
|
1816
|
+
spanBeats: first === void 0 ? 0 : first.endBeat - first.startBeat,
|
|
1817
|
+
occurrences: motif.occurrences.length
|
|
1818
|
+
};
|
|
1819
|
+
});
|
|
1820
|
+
const referencePhrases = phrases.map((phrase, index) => ({
|
|
1821
|
+
startBeat: phrase.startBeat,
|
|
1822
|
+
endBeat: phrase.endBeat,
|
|
1823
|
+
bars: barSpanOf(phrase.startBeat, phrase.endBeat, meters),
|
|
1824
|
+
section: sectionAt(sections, phrase.startBeat),
|
|
1825
|
+
confidence: phrase.confidence,
|
|
1826
|
+
cadence: cadences.get(index) ?? null,
|
|
1827
|
+
melody: phraseMelody(line, phrase.startBeat, phrase.endBeat, lineMean, meters, budget),
|
|
1828
|
+
motifNodes: graph.nodes.flatMap(
|
|
1829
|
+
(node, nodeIndex) => node.startBeat >= phrase.startBeat - BEAT_EPS && node.startBeat < phrase.endBeat - BEAT_EPS ? [nodeIndex] : []
|
|
1830
|
+
)
|
|
1831
|
+
}));
|
|
1832
|
+
return {
|
|
1833
|
+
profileVersion: REFERENCE_PROFILE_VERSION,
|
|
1834
|
+
span: {
|
|
1835
|
+
startBeat: span.startBeat,
|
|
1836
|
+
endBeat: spanEnd,
|
|
1837
|
+
bars: barSpanOf(span.startBeat, spanEnd, meters)
|
|
1838
|
+
},
|
|
1839
|
+
meters,
|
|
1840
|
+
form: {
|
|
1841
|
+
hypermeter: { groupBars: grouping.groupBars, confidence: grouping.confidence },
|
|
1842
|
+
sections,
|
|
1843
|
+
phrases: referencePhrases
|
|
1844
|
+
},
|
|
1845
|
+
harmony: { keys, chords, rhythm: harmonyRhythm },
|
|
1846
|
+
melody: {
|
|
1847
|
+
register,
|
|
1848
|
+
shape: line.length < 2 ? null : melodicContour(line).shape,
|
|
1849
|
+
motifs: referenceMotifs,
|
|
1850
|
+
graph,
|
|
1851
|
+
rhythm: analyzeRhythm(line, { meters, totalBeats: spanEnd, budget })
|
|
1852
|
+
}
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
function analyzeReference(notes, opts) {
|
|
1856
|
+
return referenceFromReadings(notes, readingsFor(notes, opts), opts);
|
|
2251
1857
|
}
|
|
2252
1858
|
|
|
2253
1859
|
export {
|
|
@@ -2258,15 +1864,11 @@ export {
|
|
|
2258
1864
|
tensionCurve,
|
|
2259
1865
|
tensionCurveFrom,
|
|
2260
1866
|
hypermeter,
|
|
2261
|
-
melodicContour,
|
|
2262
|
-
extractMotifs,
|
|
2263
|
-
motifFromNotes,
|
|
2264
|
-
relateMotifs,
|
|
2265
|
-
compareMelodies,
|
|
2266
|
-
melodicSimilarity,
|
|
2267
1867
|
phrasesFromTimeline,
|
|
2268
1868
|
structuralCadences,
|
|
2269
1869
|
sectionsFromNotes,
|
|
2270
|
-
|
|
1870
|
+
compareReferences,
|
|
1871
|
+
referenceFromReadings,
|
|
1872
|
+
analyzeReference
|
|
2271
1873
|
};
|
|
2272
|
-
//# sourceMappingURL=chunk-
|
|
1874
|
+
//# sourceMappingURL=chunk-MLBPXA4Z.js.map
|