@libraz/libcantus 1.2.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 +28 -4
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +151 -5
- package/dist/analyze/index.d.ts +151 -5
- package/dist/analyze/index.js +34 -10
- package/dist/{chunk-DSMJRXCZ.cjs → chunk-4R2LWV7E.cjs} +384 -107
- package/dist/chunk-4R2LWV7E.cjs.map +1 -0
- package/dist/{chunk-J7FHVZII.cjs → chunk-B2PW6EHN.cjs} +2000 -30
- package/dist/chunk-B2PW6EHN.cjs.map +1 -0
- package/dist/{chunk-Q6LFSAKV.js → chunk-CPMB7P75.js} +1973 -3
- package/dist/chunk-CPMB7P75.js.map +1 -0
- package/dist/{chunk-H2KJHBW4.js → chunk-GNVN4PPJ.js} +1871 -5
- package/dist/chunk-GNVN4PPJ.js.map +1 -0
- package/dist/{chunk-SGLEQVJH.cjs → chunk-IFZDWIKM.cjs} +1868 -2
- package/dist/chunk-IFZDWIKM.cjs.map +1 -0
- package/dist/{chunk-FBH75DAY.js → chunk-IPEF6NWB.js} +287 -10
- package/dist/chunk-IPEF6NWB.js.map +1 -0
- package/dist/{chunk-HYJJ2V2I.js → chunk-MLBPXA4Z.js} +300 -720
- package/dist/chunk-MLBPXA4Z.js.map +1 -0
- package/dist/{chunk-HWHGOVO6.cjs → chunk-PWC4CRWT.cjs} +1 -1
- package/dist/{chunk-HWHGOVO6.cjs.map → chunk-PWC4CRWT.cjs.map} +1 -1
- package/dist/{chunk-HPVSH7SF.cjs → chunk-YFA7X2F6.cjs} +350 -770
- package/dist/chunk-YFA7X2F6.cjs.map +1 -0
- package/dist/{chunk-NFJ4P6ZZ.js → chunk-ZBUROTJQ.js} +1 -1
- package/dist/generate/index.cjs +15 -3
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +84 -6
- package/dist/generate/index.d.ts +84 -6
- package/dist/generate/index.js +14 -2
- package/dist/{index-BNSM-DSk.d.cts → index-CY-3NVfy.d.cts} +484 -3
- package/dist/{index-BJgP97N1.d.ts → index-zaZdmnU4.d.ts} +484 -3
- package/dist/index.cjs +43 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +48 -12
- package/dist/model/index.cjs +5 -5
- package/dist/model/index.d.cts +203 -7
- package/dist/model/index.d.ts +203 -7
- package/dist/model/index.js +4 -4
- package/dist/{index-DGCNKAUr.d.cts → profile-CQMEqdH9.d.cts} +65 -598
- package/dist/{index-BRMbPkHE.d.ts → profile-DLdlIpS9.d.ts} +65 -598
- package/dist/{index-BBWNZS0f.d.cts → types-D-XgxGvp.d.cts} +1169 -13
- package/dist/{index-CJBBiAmU.d.ts → types-Dh7C064C.d.ts} +1169 -13
- package/package.json +5 -5
- 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-HYJJ2V2I.js.map +0 -1
- package/dist/chunk-J7FHVZII.cjs.map +0 -1
- package/dist/chunk-Q6LFSAKV.js.map +0 -1
- package/dist/chunk-SGLEQVJH.cjs.map +0 -1
- /package/dist/{chunk-NFJ4P6ZZ.js.map → chunk-ZBUROTJQ.js.map} +0 -0
|
@@ -45,6 +45,7 @@ var _chunk35NBV2FTcjs = require('./chunk-35NBV2FT.cjs');
|
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
|
|
48
49
|
|
|
49
50
|
|
|
50
51
|
var _chunk3R3QVKY5cjs = require('./chunk-3R3QVKY5.cjs');
|
|
@@ -68,6 +69,18 @@ var _chunk3R3QVKY5cjs = require('./chunk-3R3QVKY5.cjs');
|
|
|
68
69
|
|
|
69
70
|
|
|
70
71
|
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
71
84
|
|
|
72
85
|
|
|
73
86
|
|
|
@@ -779,6 +792,14 @@ function outerPitches(voicing) {
|
|
|
779
792
|
function bassPcOf2(chord, voicedBass) {
|
|
780
793
|
return _chunkS5BKZZGMcjs.pitchClassOf.call(void 0, _nullishCoalesce(_nullishCoalesce(voicedBass, () => ( chord.bassPc)), () => ( chord.rootPc)));
|
|
781
794
|
}
|
|
795
|
+
var CADENCE_TYPES = Object.freeze([
|
|
796
|
+
"authentic",
|
|
797
|
+
"plagal",
|
|
798
|
+
"half",
|
|
799
|
+
"deceptive",
|
|
800
|
+
"phrygian",
|
|
801
|
+
"modal"
|
|
802
|
+
]);
|
|
782
803
|
function cadenceType(from, to, key, fromBassPc, toBassPc) {
|
|
783
804
|
const tonic = _chunkS5BKZZGMcjs.pitchClassOf.call(void 0, key.rootPc);
|
|
784
805
|
const fromOffset = _chunkS5BKZZGMcjs.pitchClassOf.call(void 0, from.rootPc - tonic);
|
|
@@ -1466,6 +1487,7 @@ function capitalize(text) {
|
|
|
1466
1487
|
}
|
|
1467
1488
|
|
|
1468
1489
|
// src/analyze/functional/function.ts
|
|
1490
|
+
var HARMONIC_FUNCTIONS = Object.freeze(["tonic", "subdominant", "dominant"]);
|
|
1469
1491
|
var FUNCTION_BY_OFFSET = [
|
|
1470
1492
|
"tonic",
|
|
1471
1493
|
// 0 I
|
|
@@ -1669,6 +1691,16 @@ function stackedTriad(degree, key) {
|
|
|
1669
1691
|
}
|
|
1670
1692
|
|
|
1671
1693
|
// src/analyze/form/internal.ts
|
|
1694
|
+
function barSpanOf(startBeat, endBeat, meter) {
|
|
1695
|
+
const firstBar = _chunkS5BKZZGMcjs.barIndexAt.call(void 0, startBeat, meter);
|
|
1696
|
+
const lastBar = _chunkS5BKZZGMcjs.barIndexAt.call(void 0, endBeat, meter);
|
|
1697
|
+
const partOf = (beat, bar) => {
|
|
1698
|
+
const barStart = _chunkS5BKZZGMcjs.barPositionToBeat.call(void 0, { bar, beat: 0 }, meter);
|
|
1699
|
+
const barLength = _chunkS5BKZZGMcjs.beatsPerBarAt.call(void 0, barStart, meter);
|
|
1700
|
+
return barLength > 0 ? (beat - barStart) / barLength : 0;
|
|
1701
|
+
};
|
|
1702
|
+
return lastBar - firstBar + partOf(endBeat, lastBar) - partOf(startBeat, firstBar);
|
|
1703
|
+
}
|
|
1672
1704
|
function clamp01(value) {
|
|
1673
1705
|
return Math.min(1, Math.max(0, value));
|
|
1674
1706
|
}
|
|
@@ -3267,6 +3299,1840 @@ function detectCadences(timeline, key) {
|
|
|
3267
3299
|
return hits;
|
|
3268
3300
|
}
|
|
3269
3301
|
|
|
3302
|
+
// src/analyze/melody/internal.ts
|
|
3303
|
+
var RHYTHM_GRID = 64;
|
|
3304
|
+
function phraseNotes(phrase) {
|
|
3305
|
+
if (Array.isArray(phrase)) {
|
|
3306
|
+
return phrase;
|
|
3307
|
+
}
|
|
3308
|
+
return _chunkS5BKZZGMcjs.assertRecord.call(void 0, phrase, "melodic phrase").notes;
|
|
3309
|
+
}
|
|
3310
|
+
function orderedNotes(notes, name, budget) {
|
|
3311
|
+
_chunkS5BKZZGMcjs.assertNoteEvents.call(void 0, notes, name, { allowNonPositiveDuration: true, budget });
|
|
3312
|
+
const sounding = notes.filter((note) => note.durationBeat > 0).sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);
|
|
3313
|
+
const line = [];
|
|
3314
|
+
for (const note of sounding) {
|
|
3315
|
+
const previous = line[line.length - 1];
|
|
3316
|
+
if (previous !== void 0 && Math.abs(note.startBeat - previous.startBeat) <= _chunkS5BKZZGMcjs.BEAT_EPS) {
|
|
3317
|
+
line[line.length - 1] = note;
|
|
3318
|
+
continue;
|
|
3319
|
+
}
|
|
3320
|
+
line.push(note);
|
|
3321
|
+
}
|
|
3322
|
+
return line;
|
|
3323
|
+
}
|
|
3324
|
+
function intervalsOf(notes) {
|
|
3325
|
+
const out = [];
|
|
3326
|
+
for (let i = 1; i < notes.length; i += 1) {
|
|
3327
|
+
out.push((_nullishCoalesce(_optionalChain([notes, 'access', _57 => _57[i], 'optionalAccess', _58 => _58.pitch]), () => ( 0))) - (_nullishCoalesce(_optionalChain([notes, 'access', _59 => _59[i - 1], 'optionalAccess', _60 => _60.pitch]), () => ( 0))));
|
|
3328
|
+
}
|
|
3329
|
+
return out;
|
|
3330
|
+
}
|
|
3331
|
+
function onsetGaps(notes) {
|
|
3332
|
+
const out = [];
|
|
3333
|
+
for (let i = 1; i < notes.length; i += 1) {
|
|
3334
|
+
out.push((_nullishCoalesce(_optionalChain([notes, 'access', _61 => _61[i], 'optionalAccess', _62 => _62.startBeat]), () => ( 0))) - (_nullishCoalesce(_optionalChain([notes, 'access', _63 => _63[i - 1], 'optionalAccess', _64 => _64.startBeat]), () => ( 0))));
|
|
3335
|
+
}
|
|
3336
|
+
return out;
|
|
3337
|
+
}
|
|
3338
|
+
function retrogradeGaps(notes) {
|
|
3339
|
+
const out = [];
|
|
3340
|
+
for (let i = notes.length - 1; i >= 1; i -= 1) {
|
|
3341
|
+
const later = notes[i];
|
|
3342
|
+
const earlier = notes[i - 1];
|
|
3343
|
+
out.push(
|
|
3344
|
+
(_nullishCoalesce(_optionalChain([later, 'optionalAccess', _65 => _65.startBeat]), () => ( 0))) + (_nullishCoalesce(_optionalChain([later, 'optionalAccess', _66 => _66.durationBeat]), () => ( 0))) - ((_nullishCoalesce(_optionalChain([earlier, 'optionalAccess', _67 => _67.startBeat]), () => ( 0))) + (_nullishCoalesce(_optionalChain([earlier, 'optionalAccess', _68 => _68.durationBeat]), () => ( 0))))
|
|
3345
|
+
);
|
|
3346
|
+
}
|
|
3347
|
+
return out;
|
|
3348
|
+
}
|
|
3349
|
+
function rhythmProfile(gaps) {
|
|
3350
|
+
const unit = _nullishCoalesce(gaps[0], () => ( 0));
|
|
3351
|
+
if (!(unit > _chunkS5BKZZGMcjs.BEAT_EPS)) {
|
|
3352
|
+
return gaps.length === 0 ? [] : null;
|
|
3353
|
+
}
|
|
3354
|
+
return gaps.map((gap) => Math.round(gap / unit * RHYTHM_GRID) / RHYTHM_GRID);
|
|
3355
|
+
}
|
|
3356
|
+
function sameNumbers(a, b) {
|
|
3357
|
+
if (a.length !== b.length) {
|
|
3358
|
+
return false;
|
|
3359
|
+
}
|
|
3360
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
3361
|
+
if (Math.abs((_nullishCoalesce(a[i], () => ( 0))) - (_nullishCoalesce(b[i], () => ( 0)))) > _chunkS5BKZZGMcjs.BEAT_EPS) {
|
|
3362
|
+
return false;
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
return true;
|
|
3366
|
+
}
|
|
3367
|
+
function roundTo(value, digits) {
|
|
3368
|
+
const scale = 10 ** digits;
|
|
3369
|
+
return Math.round(value * scale) / scale;
|
|
3370
|
+
}
|
|
3371
|
+
function endBeatOf(notes) {
|
|
3372
|
+
let end = 0;
|
|
3373
|
+
for (const note of notes) {
|
|
3374
|
+
end = Math.max(end, note.startBeat + note.durationBeat);
|
|
3375
|
+
}
|
|
3376
|
+
return end;
|
|
3377
|
+
}
|
|
3378
|
+
function onsetSpan(notes) {
|
|
3379
|
+
if (notes.length < 2) {
|
|
3380
|
+
return 0;
|
|
3381
|
+
}
|
|
3382
|
+
return (_nullishCoalesce(_optionalChain([notes, 'access', _69 => _69[notes.length - 1], 'optionalAccess', _70 => _70.startBeat]), () => ( 0))) - (_nullishCoalesce(_optionalChain([notes, 'access', _71 => _71[0], 'optionalAccess', _72 => _72.startBeat]), () => ( 0)));
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3385
|
+
// src/analyze/melody/contour.ts
|
|
3386
|
+
var DIRECTIONAL_SHARE = 0.75;
|
|
3387
|
+
var MELODIC_CONTOUR_SHAPES = Object.freeze([
|
|
3388
|
+
"arch",
|
|
3389
|
+
"ascending",
|
|
3390
|
+
"descending",
|
|
3391
|
+
"wave",
|
|
3392
|
+
"static"
|
|
3393
|
+
]);
|
|
3394
|
+
function extremes(notes) {
|
|
3395
|
+
if (notes.length === 0) {
|
|
3396
|
+
return { peak: -1, trough: -1 };
|
|
3397
|
+
}
|
|
3398
|
+
let peak = 0;
|
|
3399
|
+
let trough = 0;
|
|
3400
|
+
for (let i = 1; i < notes.length; i += 1) {
|
|
3401
|
+
if ((_nullishCoalesce(_optionalChain([notes, 'access', _73 => _73[i], 'optionalAccess', _74 => _74.pitch]), () => ( 0))) > (_nullishCoalesce(_optionalChain([notes, 'access', _75 => _75[peak], 'optionalAccess', _76 => _76.pitch]), () => ( 0)))) {
|
|
3402
|
+
peak = i;
|
|
3403
|
+
}
|
|
3404
|
+
if ((_nullishCoalesce(_optionalChain([notes, 'access', _77 => _77[i], 'optionalAccess', _78 => _78.pitch]), () => ( 0))) < (_nullishCoalesce(_optionalChain([notes, 'access', _79 => _79[trough], 'optionalAccess', _80 => _80.pitch]), () => ( 0)))) {
|
|
3405
|
+
trough = i;
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
return { peak, trough };
|
|
3409
|
+
}
|
|
3410
|
+
function melodicContour(notes) {
|
|
3411
|
+
const line = orderedNotes(phraseNotes(notes), "contour notes");
|
|
3412
|
+
const intervals = intervalsOf(line);
|
|
3413
|
+
const directions = intervals.map(
|
|
3414
|
+
(step) => step > 0 ? "up" : step < 0 ? "down" : "same"
|
|
3415
|
+
);
|
|
3416
|
+
const { peak, trough } = extremes(line);
|
|
3417
|
+
const range = (_nullishCoalesce(_optionalChain([line, 'access', _81 => _81[peak], 'optionalAccess', _82 => _82.pitch]), () => ( 0))) - (_nullishCoalesce(_optionalChain([line, 'access', _83 => _83[trough], 'optionalAccess', _84 => _84.pitch]), () => ( 0)));
|
|
3418
|
+
const motion = intervals.reduce((sum, step) => sum + Math.abs(step), 0);
|
|
3419
|
+
if (motion === 0) {
|
|
3420
|
+
return {
|
|
3421
|
+
directions,
|
|
3422
|
+
shape: "static",
|
|
3423
|
+
peakIndex: peak,
|
|
3424
|
+
troughIndex: trough,
|
|
3425
|
+
range: 0,
|
|
3426
|
+
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"
|
|
3427
|
+
};
|
|
3428
|
+
}
|
|
3429
|
+
const first = _nullishCoalesce(_optionalChain([line, 'access', _85 => _85[0], 'optionalAccess', _86 => _86.pitch]), () => ( 0));
|
|
3430
|
+
const last = _nullishCoalesce(_optionalChain([line, 'access', _87 => _87[line.length - 1], 'optionalAccess', _88 => _88.pitch]), () => ( 0));
|
|
3431
|
+
const net = last - first;
|
|
3432
|
+
const directness = Math.abs(net) / motion;
|
|
3433
|
+
const share = Math.round(directness * 100);
|
|
3434
|
+
if (directness >= DIRECTIONAL_SHARE) {
|
|
3435
|
+
const shape = net > 0 ? "ascending" : "descending";
|
|
3436
|
+
return {
|
|
3437
|
+
directions,
|
|
3438
|
+
shape,
|
|
3439
|
+
peakIndex: peak,
|
|
3440
|
+
troughIndex: trough,
|
|
3441
|
+
range,
|
|
3442
|
+
rationale: `${share}% of the motion runs ${net > 0 ? "up" : "down"}, so the line is ${shape}`
|
|
3443
|
+
};
|
|
3444
|
+
}
|
|
3445
|
+
const peakPitch = _nullishCoalesce(_optionalChain([line, 'access', _89 => _89[peak], 'optionalAccess', _90 => _90.pitch]), () => ( 0));
|
|
3446
|
+
const archMotion = peakPitch - first + (peakPitch - last);
|
|
3447
|
+
if (peak > 0 && peak < line.length - 1 && archMotion >= DIRECTIONAL_SHARE * motion) {
|
|
3448
|
+
return {
|
|
3449
|
+
directions,
|
|
3450
|
+
shape: "arch",
|
|
3451
|
+
peakIndex: peak,
|
|
3452
|
+
troughIndex: trough,
|
|
3453
|
+
range,
|
|
3454
|
+
rationale: `Rises to its peak at note ${peak + 1} of ${line.length} and falls back, so the line is an arch`
|
|
3455
|
+
};
|
|
3456
|
+
}
|
|
3457
|
+
return {
|
|
3458
|
+
directions,
|
|
3459
|
+
shape: "wave",
|
|
3460
|
+
peakIndex: peak,
|
|
3461
|
+
troughIndex: trough,
|
|
3462
|
+
range,
|
|
3463
|
+
rationale: `Changes direction without settling either way (${share}% of the motion nets out), so the line is a wave`
|
|
3464
|
+
};
|
|
3465
|
+
}
|
|
3466
|
+
|
|
3467
|
+
// src/analyze/melody/motifs.ts
|
|
3468
|
+
var MIN_CELL_NOTES = 3;
|
|
3469
|
+
var MAX_CELL_NOTES = 8;
|
|
3470
|
+
var MIN_OCCURRENCES = 2;
|
|
3471
|
+
function windowSignature(notes) {
|
|
3472
|
+
const profile = rhythmProfile(onsetGaps(notes));
|
|
3473
|
+
if (profile === null) {
|
|
3474
|
+
return null;
|
|
3475
|
+
}
|
|
3476
|
+
return `${notes.length}|${intervalsOf(notes).join(",")}|${profile.join(",")}`;
|
|
3477
|
+
}
|
|
3478
|
+
function nonOverlapping(starts, length) {
|
|
3479
|
+
const picked = [];
|
|
3480
|
+
let lastEnd = Number.NEGATIVE_INFINITY;
|
|
3481
|
+
for (const start of starts) {
|
|
3482
|
+
if (start > lastEnd) {
|
|
3483
|
+
picked.push(start);
|
|
3484
|
+
lastEnd = start + length - 1;
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
return picked;
|
|
3488
|
+
}
|
|
3489
|
+
function subsumes(outer, inner) {
|
|
3490
|
+
let at = 0;
|
|
3491
|
+
for (const start of inner.starts) {
|
|
3492
|
+
while (at + 1 < outer.starts.length && (_nullishCoalesce(outer.starts[at + 1], () => ( 0))) <= start) {
|
|
3493
|
+
at += 1;
|
|
3494
|
+
}
|
|
3495
|
+
const from = outer.starts[at];
|
|
3496
|
+
if (from === void 0 || start < from || start + inner.length > from + outer.length) {
|
|
3497
|
+
return false;
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
return true;
|
|
3501
|
+
}
|
|
3502
|
+
function listBeats(occurrences) {
|
|
3503
|
+
const shown = occurrences.slice(0, 5).map((occurrence) => roundTo(occurrence.startBeat, 3));
|
|
3504
|
+
return occurrences.length > shown.length ? `${shown.join(", ")}, and ${occurrences.length - shown.length} more` : shown.join(", ");
|
|
3505
|
+
}
|
|
3506
|
+
function motifFromGroup(group, notes) {
|
|
3507
|
+
const first = _nullishCoalesce(group.starts[0], () => ( 0));
|
|
3508
|
+
const prime = notes.slice(first, first + group.length).map((note) => ({ ...note }));
|
|
3509
|
+
const primePitch = _nullishCoalesce(_optionalChain([prime, 'access', _91 => _91[0], 'optionalAccess', _92 => _92.pitch]), () => ( 0));
|
|
3510
|
+
const primeSpan = onsetSpan(prime);
|
|
3511
|
+
const occurrences = group.starts.map((start) => {
|
|
3512
|
+
const statement = notes.slice(start, start + group.length);
|
|
3513
|
+
const span = onsetSpan(statement);
|
|
3514
|
+
return {
|
|
3515
|
+
noteIndex: start,
|
|
3516
|
+
startBeat: _nullishCoalesce(_optionalChain([statement, 'access', _93 => _93[0], 'optionalAccess', _94 => _94.startBeat]), () => ( 0)),
|
|
3517
|
+
endBeat: endBeatOf(statement),
|
|
3518
|
+
transpose: (_nullishCoalesce(_optionalChain([statement, 'access', _95 => _95[0], 'optionalAccess', _96 => _96.pitch]), () => ( 0))) - primePitch,
|
|
3519
|
+
timeRatio: primeSpan > _chunkS5BKZZGMcjs.BEAT_EPS ? span / primeSpan : 1
|
|
3520
|
+
};
|
|
3521
|
+
});
|
|
3522
|
+
const shape = melodicContour(prime).shape;
|
|
3523
|
+
const transposed = occurrences.some((occurrence) => occurrence.transpose !== 0);
|
|
3524
|
+
const stretched = occurrences.some((occurrence) => Math.abs(occurrence.timeRatio - 1) > _chunkS5BKZZGMcjs.BEAT_EPS);
|
|
3525
|
+
const detail = [
|
|
3526
|
+
transposed ? "at more than one pitch level" : "at the same pitch level",
|
|
3527
|
+
stretched ? "and in more than one set of note values" : ""
|
|
3528
|
+
].filter((part) => part !== "").join(" ");
|
|
3529
|
+
return {
|
|
3530
|
+
notes: prime,
|
|
3531
|
+
intervals: intervalsOf(prime),
|
|
3532
|
+
rhythm: _nullishCoalesce(rhythmProfile(onsetGaps(prime)), () => ( [])),
|
|
3533
|
+
occurrences,
|
|
3534
|
+
rationale: `${group.length}-note ${shape} cell stated ${occurrences.length} ${occurrences.length === 1 ? "time" : "times"} (beats ${listBeats(occurrences)}), ${detail}`
|
|
3535
|
+
};
|
|
3536
|
+
}
|
|
3537
|
+
function extractMotifs(notes, opts = {}) {
|
|
3538
|
+
const asked = _chunkS5BKZZGMcjs.assertOptions.call(void 0, opts, "opts");
|
|
3539
|
+
const minNotes = _chunkS5BKZZGMcjs.assertInteger.call(void 0, _nullishCoalesce(asked.minNotes, () => ( MIN_CELL_NOTES)), "motif minNotes", 2, 64);
|
|
3540
|
+
const maxNotes = _chunkS5BKZZGMcjs.assertInteger.call(void 0, _nullishCoalesce(asked.maxNotes, () => ( MAX_CELL_NOTES)), "motif maxNotes", 2, 64);
|
|
3541
|
+
if (maxNotes < minNotes) {
|
|
3542
|
+
throw new (0, _chunkS5BKZZGMcjs.InvalidInputError)(
|
|
3543
|
+
`motif maxNotes must be at least minNotes ${minNotes}; received ${maxNotes}`
|
|
3544
|
+
);
|
|
3545
|
+
}
|
|
3546
|
+
const minOccurrences = _chunkS5BKZZGMcjs.assertInteger.call(void 0,
|
|
3547
|
+
_nullishCoalesce(asked.minOccurrences, () => ( MIN_OCCURRENCES)),
|
|
3548
|
+
"motif minOccurrences",
|
|
3549
|
+
2
|
|
3550
|
+
);
|
|
3551
|
+
const sounding = orderedNotes(notes, "melody notes", asked.budget);
|
|
3552
|
+
if (sounding.length < minNotes) {
|
|
3553
|
+
return [];
|
|
3554
|
+
}
|
|
3555
|
+
_chunkS5BKZZGMcjs.assertGenerationBudget.call(void 0,
|
|
3556
|
+
sounding.length * (maxNotes - minNotes + 1),
|
|
3557
|
+
"motif windows",
|
|
3558
|
+
asked.budget
|
|
3559
|
+
);
|
|
3560
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3561
|
+
for (let length = minNotes; length <= maxNotes; length += 1) {
|
|
3562
|
+
for (let start = 0; start + length <= sounding.length; start += 1) {
|
|
3563
|
+
const signature = windowSignature(sounding.slice(start, start + length));
|
|
3564
|
+
if (signature === null) {
|
|
3565
|
+
continue;
|
|
3566
|
+
}
|
|
3567
|
+
const group = groups.get(signature);
|
|
3568
|
+
if (group === void 0) {
|
|
3569
|
+
groups.set(signature, { signature, length, starts: [start] });
|
|
3570
|
+
} else {
|
|
3571
|
+
group.starts.push(start);
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
const candidates = [...groups.values()].map((group) => ({ ...group, starts: nonOverlapping(group.starts, group.length) })).filter((group) => group.starts.length >= minOccurrences).sort(
|
|
3576
|
+
(a, b) => b.length - a.length || b.starts.length - a.starts.length || (_nullishCoalesce(a.starts[0], () => ( 0))) - (_nullishCoalesce(b.starts[0], () => ( 0))) || (a.signature < b.signature ? -1 : a.signature > b.signature ? 1 : 0)
|
|
3577
|
+
);
|
|
3578
|
+
const keptByLength = /* @__PURE__ */ new Map();
|
|
3579
|
+
const kept = [];
|
|
3580
|
+
let comparisons = 0;
|
|
3581
|
+
for (const candidate of candidates) {
|
|
3582
|
+
const first = _nullishCoalesce(candidate.starts[0], () => ( 0));
|
|
3583
|
+
let covered = false;
|
|
3584
|
+
for (let length = candidate.length + 1; length <= maxNotes && !covered; length += 1) {
|
|
3585
|
+
const byStart = keptByLength.get(length);
|
|
3586
|
+
if (byStart === void 0) {
|
|
3587
|
+
continue;
|
|
3588
|
+
}
|
|
3589
|
+
for (let from = Math.max(0, first - (length - candidate.length)); from <= first; from += 1) {
|
|
3590
|
+
comparisons += 1;
|
|
3591
|
+
const outer = byStart.get(from);
|
|
3592
|
+
if (outer === void 0 || outer.starts.length < candidate.starts.length) {
|
|
3593
|
+
continue;
|
|
3594
|
+
}
|
|
3595
|
+
comparisons += outer.starts.length + candidate.starts.length;
|
|
3596
|
+
if (subsumes(outer, candidate)) {
|
|
3597
|
+
covered = true;
|
|
3598
|
+
break;
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
_chunkS5BKZZGMcjs.assertGenerationBudget.call(void 0, comparisons, "motif subsumption comparisons", asked.budget);
|
|
3603
|
+
if (!covered) {
|
|
3604
|
+
kept.push(candidate);
|
|
3605
|
+
let byStart = keptByLength.get(candidate.length);
|
|
3606
|
+
if (byStart === void 0) {
|
|
3607
|
+
byStart = /* @__PURE__ */ new Map();
|
|
3608
|
+
keptByLength.set(candidate.length, byStart);
|
|
3609
|
+
}
|
|
3610
|
+
for (const start of candidate.starts) {
|
|
3611
|
+
byStart.set(start, candidate);
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
return kept.map((group) => motifFromGroup(group, sounding));
|
|
3616
|
+
}
|
|
3617
|
+
function motifFromNotes(notes) {
|
|
3618
|
+
const sounding = orderedNotes(notes, "motif notes").map((note) => ({ ...note }));
|
|
3619
|
+
return motifFromGroup(
|
|
3620
|
+
{ signature: _nullishCoalesce(windowSignature(sounding), () => ( "")), length: sounding.length, starts: [0] },
|
|
3621
|
+
sounding
|
|
3622
|
+
);
|
|
3623
|
+
}
|
|
3624
|
+
|
|
3625
|
+
// src/analyze/melody/relation.ts
|
|
3626
|
+
var MOTIF_RELATION_KINDS = Object.freeze([
|
|
3627
|
+
"repetition",
|
|
3628
|
+
"transposition",
|
|
3629
|
+
"tonalTransposition",
|
|
3630
|
+
"inversion",
|
|
3631
|
+
"retrograde",
|
|
3632
|
+
"retrogradeInversion",
|
|
3633
|
+
"augmentation",
|
|
3634
|
+
"diminution"
|
|
3635
|
+
]);
|
|
3636
|
+
function spellingOf(key) {
|
|
3637
|
+
if (key === void 0) {
|
|
3638
|
+
return "sharp";
|
|
3639
|
+
}
|
|
3640
|
+
return _chunk3R3QVKY5cjs.keySignatureFifths.call(void 0, key.tonic, key.scale) < 0 ? "flat" : "sharp";
|
|
3641
|
+
}
|
|
3642
|
+
function intervalBetween(from, to, key) {
|
|
3643
|
+
const spelling = spellingOf(key);
|
|
3644
|
+
return _chunkS5BKZZGMcjs.spelledInterval.call(void 0, _chunkS5BKZZGMcjs.midiToNote.call(void 0, from, spelling), _chunkS5BKZZGMcjs.midiToNote.call(void 0, to, spelling));
|
|
3645
|
+
}
|
|
3646
|
+
function intervalName(interval) {
|
|
3647
|
+
return `${interval.descending && interval.semitones !== 0 ? "-" : ""}${interval.quality}${interval.number}`;
|
|
3648
|
+
}
|
|
3649
|
+
function ladderIndices(notes, key) {
|
|
3650
|
+
const root = _chunkS5BKZZGMcjs.pitchClassOf.call(void 0, key.rootPc);
|
|
3651
|
+
const offsets = _chunk3R3QVKY5cjs.scaleTonesInDegreeOrder.call(void 0, key).map((pc) => _chunkS5BKZZGMcjs.pitchClassOf.call(void 0, pc - root));
|
|
3652
|
+
if (offsets.length === 0) {
|
|
3653
|
+
return null;
|
|
3654
|
+
}
|
|
3655
|
+
const out = [];
|
|
3656
|
+
for (const note of notes) {
|
|
3657
|
+
const semitones = note.pitch - root;
|
|
3658
|
+
const octave = Math.floor(semitones / 12);
|
|
3659
|
+
const degree = offsets.indexOf(semitones - octave * 12);
|
|
3660
|
+
if (degree < 0) {
|
|
3661
|
+
return null;
|
|
3662
|
+
}
|
|
3663
|
+
out.push(octave * offsets.length + degree);
|
|
3664
|
+
}
|
|
3665
|
+
return out;
|
|
3666
|
+
}
|
|
3667
|
+
function ladderSteps(indices) {
|
|
3668
|
+
const out = [];
|
|
3669
|
+
for (let i = 1; i < indices.length; i += 1) {
|
|
3670
|
+
out.push((_nullishCoalesce(indices[i], () => ( 0))) - (_nullishCoalesce(indices[i - 1], () => ( 0))));
|
|
3671
|
+
}
|
|
3672
|
+
return out;
|
|
3673
|
+
}
|
|
3674
|
+
function pitchShapes(a, b) {
|
|
3675
|
+
const shapes = [];
|
|
3676
|
+
if (sameNumbers(a, b)) {
|
|
3677
|
+
shapes.push("exact");
|
|
3678
|
+
}
|
|
3679
|
+
if (sameNumbers(
|
|
3680
|
+
b,
|
|
3681
|
+
a.map((step) => -step)
|
|
3682
|
+
)) {
|
|
3683
|
+
shapes.push("inversion");
|
|
3684
|
+
}
|
|
3685
|
+
const reversed = [...a].reverse();
|
|
3686
|
+
if (sameNumbers(
|
|
3687
|
+
b,
|
|
3688
|
+
reversed.map((step) => -step)
|
|
3689
|
+
)) {
|
|
3690
|
+
shapes.push("retrograde");
|
|
3691
|
+
}
|
|
3692
|
+
if (sameNumbers(b, reversed)) {
|
|
3693
|
+
shapes.push("retrogradeInversion");
|
|
3694
|
+
}
|
|
3695
|
+
return shapes;
|
|
3696
|
+
}
|
|
3697
|
+
function totalGap(gaps) {
|
|
3698
|
+
return gaps.reduce((sum, gap) => sum + gap, 0);
|
|
3699
|
+
}
|
|
3700
|
+
function stretchRatio(modelGaps, answerGaps) {
|
|
3701
|
+
const span = totalGap(modelGaps);
|
|
3702
|
+
return span > _chunkS5BKZZGMcjs.BEAT_EPS ? totalGap(answerGaps) / span : 1;
|
|
3703
|
+
}
|
|
3704
|
+
function stretchPhrase(ratio2) {
|
|
3705
|
+
return `note values ${roundTo(ratio2, 3)}x the model's`;
|
|
3706
|
+
}
|
|
3707
|
+
function relateMotifs(a, b, keyLike) {
|
|
3708
|
+
const key = keyLike === void 0 ? void 0 : _chunk3R3QVKY5cjs.resolveKey.call(void 0, keyLike);
|
|
3709
|
+
const model = _chunkS5BKZZGMcjs.assertArray.call(void 0, _chunkS5BKZZGMcjs.assertRecord.call(void 0, a, "a").notes, "a.notes");
|
|
3710
|
+
const answer = _chunkS5BKZZGMcjs.assertArray.call(void 0, _chunkS5BKZZGMcjs.assertRecord.call(void 0, b, "b").notes, "b.notes");
|
|
3711
|
+
if (model.length === 0 || model.length !== answer.length) {
|
|
3712
|
+
return null;
|
|
3713
|
+
}
|
|
3714
|
+
const modelGaps = onsetGaps(model);
|
|
3715
|
+
const answerGaps = onsetGaps(answer);
|
|
3716
|
+
const modelProfile = rhythmProfile(modelGaps);
|
|
3717
|
+
const answerProfile = rhythmProfile(answerGaps);
|
|
3718
|
+
const retroGaps = retrogradeGaps(model);
|
|
3719
|
+
const retroProfile = rhythmProfile(retroGaps);
|
|
3720
|
+
if (modelProfile === null || answerProfile === null) {
|
|
3721
|
+
return null;
|
|
3722
|
+
}
|
|
3723
|
+
const forward = sameNumbers(modelProfile, answerProfile);
|
|
3724
|
+
const backward = retroProfile !== null && sameNumbers(retroProfile, answerProfile);
|
|
3725
|
+
const semitones = (_nullishCoalesce(_optionalChain([answer, 'access', _97 => _97[0], 'optionalAccess', _98 => _98.pitch]), () => ( 0))) - (_nullishCoalesce(_optionalChain([model, 'access', _99 => _99[0], 'optionalAccess', _100 => _100.pitch]), () => ( 0)));
|
|
3726
|
+
const interval = intervalBetween(_nullishCoalesce(_optionalChain([model, 'access', _101 => _101[0], 'optionalAccess', _102 => _102.pitch]), () => ( 0)), _nullishCoalesce(_optionalChain([answer, 'access', _103 => _103[0], 'optionalAccess', _104 => _104.pitch]), () => ( 0)), key);
|
|
3727
|
+
const gap = (_nullishCoalesce(_optionalChain([answer, 'access', _105 => _105[0], 'optionalAccess', _106 => _106.startBeat]), () => ( 0))) - endBeatOf(model);
|
|
3728
|
+
const sequence = gap >= -HUMANIZE_ADJACENCY && gap <= HUMANIZE_ADJACENCY;
|
|
3729
|
+
const timeRatio = stretchRatio(modelGaps, answerGaps);
|
|
3730
|
+
const stretched = Math.abs(timeRatio - 1) > _chunkS5BKZZGMcjs.BEAT_EPS;
|
|
3731
|
+
const shapes = pitchShapes(intervalsOf(model), intervalsOf(answer));
|
|
3732
|
+
const base = { sequence, semitones, interval, timeRatio };
|
|
3733
|
+
const backwardBase = { ...base, timeRatio: stretchRatio(retroGaps, answerGaps) };
|
|
3734
|
+
if (forward && shapes.includes("exact")) {
|
|
3735
|
+
if (semitones === 0) {
|
|
3736
|
+
if (!stretched) {
|
|
3737
|
+
return {
|
|
3738
|
+
...base,
|
|
3739
|
+
kind: "repetition",
|
|
3740
|
+
rationale: "Exact repetition: the same pitches in the same rhythm"
|
|
3741
|
+
};
|
|
3742
|
+
}
|
|
3743
|
+
const augmented = timeRatio > 1;
|
|
3744
|
+
return {
|
|
3745
|
+
...base,
|
|
3746
|
+
kind: augmented ? "augmentation" : "diminution",
|
|
3747
|
+
rationale: `${augmented ? "Augmentation" : "Diminution"}: the same pitches in ${stretchPhrase(timeRatio)}`
|
|
3748
|
+
};
|
|
3749
|
+
}
|
|
3750
|
+
const name = intervalName(interval);
|
|
3751
|
+
return {
|
|
3752
|
+
...base,
|
|
3753
|
+
kind: "transposition",
|
|
3754
|
+
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)}` : "")
|
|
3755
|
+
};
|
|
3756
|
+
}
|
|
3757
|
+
if (forward && key !== void 0) {
|
|
3758
|
+
const modelLadder = ladderIndices(model, key.scale);
|
|
3759
|
+
const answerLadder = ladderIndices(answer, key.scale);
|
|
3760
|
+
if (modelLadder !== null && answerLadder !== null) {
|
|
3761
|
+
const degrees = (_nullishCoalesce(answerLadder[0], () => ( 0))) - (_nullishCoalesce(modelLadder[0], () => ( 0)));
|
|
3762
|
+
if (degrees !== 0 && sameNumbers(ladderSteps(modelLadder), ladderSteps(answerLadder))) {
|
|
3763
|
+
const direction = degrees > 0 ? "higher" : "lower";
|
|
3764
|
+
const count = Math.abs(degrees);
|
|
3765
|
+
return {
|
|
3766
|
+
...base,
|
|
3767
|
+
kind: "tonalTransposition",
|
|
3768
|
+
degrees,
|
|
3769
|
+
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`
|
|
3770
|
+
};
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
if (forward && shapes.includes("inversion")) {
|
|
3775
|
+
return {
|
|
3776
|
+
...base,
|
|
3777
|
+
kind: "inversion",
|
|
3778
|
+
rationale: "Inversion: every interval turned upside down about the first note" + (stretched ? `, in ${stretchPhrase(timeRatio)}` : "")
|
|
3779
|
+
};
|
|
3780
|
+
}
|
|
3781
|
+
if (backward && shapes.includes("retrograde")) {
|
|
3782
|
+
return {
|
|
3783
|
+
...backwardBase,
|
|
3784
|
+
kind: "retrograde",
|
|
3785
|
+
rationale: "Retrograde: the cell read back to front, rhythm included"
|
|
3786
|
+
};
|
|
3787
|
+
}
|
|
3788
|
+
if (backward && shapes.includes("retrogradeInversion")) {
|
|
3789
|
+
return {
|
|
3790
|
+
...backwardBase,
|
|
3791
|
+
kind: "retrogradeInversion",
|
|
3792
|
+
rationale: "Retrograde inversion: the cell read back to front and turned upside down"
|
|
3793
|
+
};
|
|
3794
|
+
}
|
|
3795
|
+
return null;
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3798
|
+
// src/analyze/sequence.ts
|
|
3799
|
+
function gradedEditDistance(a, b, cost, gap = 1) {
|
|
3800
|
+
let previous = Array.from({ length: b.length + 1 }, (_, i) => i * gap);
|
|
3801
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
3802
|
+
const row = new Array(b.length + 1);
|
|
3803
|
+
row[0] = i * gap;
|
|
3804
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
3805
|
+
const ai = a[i - 1];
|
|
3806
|
+
const bj = b[j - 1];
|
|
3807
|
+
const substitution = (_nullishCoalesce(previous[j - 1], () => ( 0))) + cost(ai, bj);
|
|
3808
|
+
const deletion = (_nullishCoalesce(previous[j], () => ( 0))) + gap;
|
|
3809
|
+
const insertion = (_nullishCoalesce(row[j - 1], () => ( 0))) + gap;
|
|
3810
|
+
row[j] = Math.min(substitution, deletion, insertion);
|
|
3811
|
+
}
|
|
3812
|
+
previous = row;
|
|
3813
|
+
}
|
|
3814
|
+
return _nullishCoalesce(previous[b.length], () => ( 0));
|
|
3815
|
+
}
|
|
3816
|
+
function distanceToSimilarity(distance, a, b) {
|
|
3817
|
+
const longest = Math.max(a, b);
|
|
3818
|
+
if (longest === 0) {
|
|
3819
|
+
return 1;
|
|
3820
|
+
}
|
|
3821
|
+
return Math.min(1, Math.max(0, 1 - distance / longest));
|
|
3822
|
+
}
|
|
3823
|
+
|
|
3824
|
+
// src/analyze/melody/similarity.ts
|
|
3825
|
+
var INTERVAL_TOLERANCE = 6;
|
|
3826
|
+
var PITCH_WEIGHT = 0.75;
|
|
3827
|
+
var RHYTHM_WEIGHT = 0.25;
|
|
3828
|
+
function intervalCost(x, y) {
|
|
3829
|
+
return Math.min(1, Math.abs(x - y) / INTERVAL_TOLERANCE);
|
|
3830
|
+
}
|
|
3831
|
+
function gapCost(x, y) {
|
|
3832
|
+
if (!(x > _chunkS5BKZZGMcjs.BEAT_EPS) || !(y > _chunkS5BKZZGMcjs.BEAT_EPS)) {
|
|
3833
|
+
return Math.abs(x - y) > _chunkS5BKZZGMcjs.BEAT_EPS ? 1 : 0;
|
|
3834
|
+
}
|
|
3835
|
+
return Math.min(1, Math.abs(Math.log2(x / y)));
|
|
3836
|
+
}
|
|
3837
|
+
function compareMelodies(a, b) {
|
|
3838
|
+
const left = orderedNotes(phraseNotes(a), "melody a");
|
|
3839
|
+
const right = orderedNotes(phraseNotes(b), "melody b");
|
|
3840
|
+
const leftIntervals = intervalsOf(left);
|
|
3841
|
+
const rightIntervals = intervalsOf(right);
|
|
3842
|
+
const pitchSimilarity = distanceToSimilarity(
|
|
3843
|
+
gradedEditDistance(leftIntervals, rightIntervals, intervalCost),
|
|
3844
|
+
leftIntervals.length,
|
|
3845
|
+
rightIntervals.length
|
|
3846
|
+
);
|
|
3847
|
+
const leftGaps = _nullishCoalesce(rhythmProfile(onsetGaps(left)), () => ( onsetGaps(left)));
|
|
3848
|
+
const rightGaps = _nullishCoalesce(rhythmProfile(onsetGaps(right)), () => ( onsetGaps(right)));
|
|
3849
|
+
const rhythmSimilarity = distanceToSimilarity(
|
|
3850
|
+
gradedEditDistance(leftGaps, rightGaps, gapCost),
|
|
3851
|
+
leftGaps.length,
|
|
3852
|
+
rightGaps.length
|
|
3853
|
+
);
|
|
3854
|
+
const similarity = PITCH_WEIGHT * pitchSimilarity + RHYTHM_WEIGHT * rhythmSimilarity;
|
|
3855
|
+
return {
|
|
3856
|
+
similarity,
|
|
3857
|
+
pitchSimilarity,
|
|
3858
|
+
rhythmSimilarity,
|
|
3859
|
+
rationale: `Interval shapes ${Math.round(pitchSimilarity * 100)}% alike, rhythms ${Math.round(rhythmSimilarity * 100)}% alike, weighted ${PITCH_WEIGHT}/${RHYTHM_WEIGHT} into ${roundTo(similarity, 3)}`
|
|
3860
|
+
};
|
|
3861
|
+
}
|
|
3862
|
+
function melodicSimilarity(a, b) {
|
|
3863
|
+
return compareMelodies(a, b).similarity;
|
|
3864
|
+
}
|
|
3865
|
+
|
|
3866
|
+
// src/analyze/melody/graph.ts
|
|
3867
|
+
var DEFAULT_VARIATION_THRESHOLD = 0.75;
|
|
3868
|
+
function summarize(relation) {
|
|
3869
|
+
const { kind, sequence, semitones, degrees, timeRatio } = relation;
|
|
3870
|
+
return { kind, sequence, semitones, degrees, timeRatio };
|
|
3871
|
+
}
|
|
3872
|
+
function betterParent(candidate, current) {
|
|
3873
|
+
const candidateNamed = candidate.relation !== null;
|
|
3874
|
+
const currentNamed = current.relation !== null;
|
|
3875
|
+
if (candidateNamed !== currentNamed) {
|
|
3876
|
+
return candidateNamed;
|
|
3877
|
+
}
|
|
3878
|
+
if (candidate.similarity !== current.similarity) {
|
|
3879
|
+
return candidate.similarity > current.similarity;
|
|
3880
|
+
}
|
|
3881
|
+
if (candidate.startBeat !== current.startBeat) {
|
|
3882
|
+
return candidate.startBeat > current.startBeat;
|
|
3883
|
+
}
|
|
3884
|
+
return candidate.index < current.index;
|
|
3885
|
+
}
|
|
3886
|
+
function motifGraph(line, motifs, opts = {}) {
|
|
3887
|
+
const asked = _chunkS5BKZZGMcjs.assertOptions.call(void 0, opts, "opts");
|
|
3888
|
+
const variationThreshold = _nullishCoalesce(asked.variationThreshold, () => ( DEFAULT_VARIATION_THRESHOLD));
|
|
3889
|
+
_chunkS5BKZZGMcjs.assertRange.call(void 0, variationThreshold, 0, 1, "motif graph variationThreshold");
|
|
3890
|
+
const ordered = orderedNotes(line, "line", asked.budget);
|
|
3891
|
+
const motifList = _chunkS5BKZZGMcjs.assertArray.call(void 0, motifs, "motifs");
|
|
3892
|
+
const infos = [];
|
|
3893
|
+
for (let mi = 0; mi < motifList.length; mi += 1) {
|
|
3894
|
+
const motif = _chunkS5BKZZGMcjs.assertRecord.call(void 0, motifList[mi], `motifs[${mi}]`);
|
|
3895
|
+
const notes = _chunkS5BKZZGMcjs.assertArray.call(void 0, motif.notes, `motifs[${mi}].notes`);
|
|
3896
|
+
const occurrences = _chunkS5BKZZGMcjs.assertArray.call(void 0,
|
|
3897
|
+
motif.occurrences,
|
|
3898
|
+
`motifs[${mi}].occurrences`
|
|
3899
|
+
);
|
|
3900
|
+
for (let oi = 0; oi < occurrences.length; oi += 1) {
|
|
3901
|
+
const occurrence = _chunkS5BKZZGMcjs.assertRecord.call(void 0,
|
|
3902
|
+
occurrences[oi],
|
|
3903
|
+
`motifs[${mi}].occurrences[${oi}]`
|
|
3904
|
+
);
|
|
3905
|
+
const { noteIndex } = occurrence;
|
|
3906
|
+
const onLine = Number.isInteger(noteIndex) && noteIndex >= 0 && noteIndex + notes.length <= ordered.length && Math.abs((_nullishCoalesce(_optionalChain([ordered, 'access', _107 => _107[noteIndex], 'optionalAccess', _108 => _108.startBeat]), () => ( Number.NaN))) - occurrence.startBeat) <= _chunkS5BKZZGMcjs.BEAT_EPS;
|
|
3907
|
+
if (!onLine) {
|
|
3908
|
+
throw new (0, _chunkS5BKZZGMcjs.InvalidInputError)(
|
|
3909
|
+
`motif occurrence motifs[${mi}].occurrences[${oi}] does not lie on the line`
|
|
3910
|
+
);
|
|
3911
|
+
}
|
|
3912
|
+
infos.push({
|
|
3913
|
+
node: {
|
|
3914
|
+
motif: mi,
|
|
3915
|
+
occurrence: oi,
|
|
3916
|
+
startBeat: occurrence.startBeat,
|
|
3917
|
+
endBeat: occurrence.endBeat
|
|
3918
|
+
},
|
|
3919
|
+
data: motifFromNotes(ordered.slice(noteIndex, noteIndex + notes.length))
|
|
3920
|
+
});
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
infos.sort((a, b) => a.node.startBeat - b.node.startBeat || a.node.motif - b.node.motif);
|
|
3924
|
+
const nodeCount = infos.length;
|
|
3925
|
+
_chunkS5BKZZGMcjs.assertGenerationBudget.call(void 0, nodeCount * nodeCount, "motif graph comparisons", asked.budget);
|
|
3926
|
+
const edges = [];
|
|
3927
|
+
for (let to = 1; to < nodeCount; to += 1) {
|
|
3928
|
+
const toInfo = infos[to];
|
|
3929
|
+
let best = null;
|
|
3930
|
+
for (let from = 0; from < to; from += 1) {
|
|
3931
|
+
const fromInfo = infos[from];
|
|
3932
|
+
const candidate = {
|
|
3933
|
+
index: from,
|
|
3934
|
+
startBeat: fromInfo.node.startBeat,
|
|
3935
|
+
relation: relateMotifs(fromInfo.data, toInfo.data, asked.key),
|
|
3936
|
+
similarity: melodicSimilarity(fromInfo.data, toInfo.data)
|
|
3937
|
+
};
|
|
3938
|
+
if (best === null || betterParent(candidate, best)) {
|
|
3939
|
+
best = candidate;
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
if (best !== null && (best.relation !== null || best.similarity >= variationThreshold)) {
|
|
3943
|
+
edges.push({
|
|
3944
|
+
from: best.index,
|
|
3945
|
+
to,
|
|
3946
|
+
relation: best.relation === null ? null : summarize(best.relation),
|
|
3947
|
+
similarity: best.similarity
|
|
3948
|
+
});
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3951
|
+
return { nodes: infos.map((info) => info.node), edges };
|
|
3952
|
+
}
|
|
3953
|
+
|
|
3954
|
+
// src/analyze/reduction/index.ts
|
|
3955
|
+
var REDUCTION_LEVELS = Object.freeze(["structural", "passing", "neighbor"]);
|
|
3956
|
+
var TONIC_ARRIVING = /* @__PURE__ */ new Set(["authentic", "plagal", "modal", "deceptive"]);
|
|
3957
|
+
function signedStep(fromPc, toPc) {
|
|
3958
|
+
return (_chunkS5BKZZGMcjs.pitchClassOf.call(void 0, toPc) - _chunkS5BKZZGMcjs.pitchClassOf.call(void 0, fromPc) + 18) % 12 - 6;
|
|
3959
|
+
}
|
|
3960
|
+
function isStep2(distance) {
|
|
3961
|
+
const size = Math.abs(distance);
|
|
3962
|
+
return size === 1 || size === 2;
|
|
3963
|
+
}
|
|
3964
|
+
function touches(before, after) {
|
|
3965
|
+
return Math.abs(after.startBeat - before.endBeat) <= _chunkS5BKZZGMcjs.BEAT_EPS;
|
|
3966
|
+
}
|
|
3967
|
+
function salienceFloors(durations) {
|
|
3968
|
+
const order = durations.map((_, index) => index).sort((a, b) => {
|
|
3969
|
+
const left = _nullishCoalesce(durations[a], () => ( 0));
|
|
3970
|
+
const right = _nullishCoalesce(durations[b], () => ( 0));
|
|
3971
|
+
return left - right || a - b;
|
|
3972
|
+
});
|
|
3973
|
+
const rankOf = new Array(durations.length).fill(0);
|
|
3974
|
+
for (let rank = 0; rank < order.length; rank += 1) {
|
|
3975
|
+
rankOf[_nullishCoalesce(order[rank], () => ( 0))] = rank;
|
|
3976
|
+
}
|
|
3977
|
+
const floors = [];
|
|
3978
|
+
const remaining = durations.length - 1;
|
|
3979
|
+
for (let index = 0; index < durations.length; index += 1) {
|
|
3980
|
+
if (remaining <= 0) {
|
|
3981
|
+
floors.push(0);
|
|
3982
|
+
continue;
|
|
3983
|
+
}
|
|
3984
|
+
const skipped = _nullishCoalesce(rankOf[index], () => ( 0));
|
|
3985
|
+
const at = (position) => _nullishCoalesce(durations[_nullishCoalesce(order[position < skipped ? position : position + 1], () => ( 0))], () => ( 0));
|
|
3986
|
+
const middle = remaining >> 1;
|
|
3987
|
+
floors.push(remaining % 2 === 1 ? at(middle) : (at(middle - 1) + at(middle)) / 2);
|
|
3988
|
+
}
|
|
3989
|
+
return floors;
|
|
3990
|
+
}
|
|
3991
|
+
function isTonicChord(chord, key) {
|
|
3992
|
+
return _chunkS5BKZZGMcjs.pitchClassOf.call(void 0, chord.rootPc - key.rootPc) === 0 && !isAppliedDominant(chord, key);
|
|
3993
|
+
}
|
|
3994
|
+
function functionalReason(segment, before, after, keyAt) {
|
|
3995
|
+
const key = keyAt(segment.startBeat);
|
|
3996
|
+
if (after !== void 0 && isCadentialSixFour(segment.chord, after.chord, key)) {
|
|
3997
|
+
return { kind: "cadentialSixFour" };
|
|
3998
|
+
}
|
|
3999
|
+
if (isTonicChord(segment.chord, key)) {
|
|
4000
|
+
return { kind: "tonic" };
|
|
4001
|
+
}
|
|
4002
|
+
if (_chunk3R3QVKY5cjs.isDominantChordOf.call(void 0, segment.chord, key)) {
|
|
4003
|
+
return { kind: "dominant" };
|
|
4004
|
+
}
|
|
4005
|
+
if (before !== void 0) {
|
|
4006
|
+
const arrival = detectCadence(before.chord, segment.chord, key).type;
|
|
4007
|
+
if (arrival !== null) {
|
|
4008
|
+
return { kind: "cadenceArrival", cadence: arrival };
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4011
|
+
if (after !== void 0) {
|
|
4012
|
+
const agent = detectCadence(segment.chord, after.chord, keyAt(after.startBeat)).type;
|
|
4013
|
+
if (agent !== null && TONIC_ARRIVING.has(agent)) {
|
|
4014
|
+
return { kind: "cadenceAgent", cadence: agent };
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
4017
|
+
return null;
|
|
4018
|
+
}
|
|
4019
|
+
function figureReason(segment, before, after) {
|
|
4020
|
+
if (before === void 0 || after === void 0) {
|
|
4021
|
+
return null;
|
|
4022
|
+
}
|
|
4023
|
+
const incoming = signedStep(before.chord.rootPc, segment.chord.rootPc);
|
|
4024
|
+
const outgoing = signedStep(segment.chord.rootPc, after.chord.rootPc);
|
|
4025
|
+
if (!isStep2(incoming) || !isStep2(outgoing)) {
|
|
4026
|
+
return null;
|
|
4027
|
+
}
|
|
4028
|
+
if (incoming > 0 === outgoing > 0) {
|
|
4029
|
+
return { kind: "passing" };
|
|
4030
|
+
}
|
|
4031
|
+
return _chunkS5BKZZGMcjs.pitchClassOf.call(void 0, before.chord.rootPc) === _chunkS5BKZZGMcjs.pitchClassOf.call(void 0, after.chord.rootPc) ? { kind: "neighbor" } : null;
|
|
4032
|
+
}
|
|
4033
|
+
function describe2(reason) {
|
|
4034
|
+
switch (reason.kind) {
|
|
4035
|
+
case "tonic":
|
|
4036
|
+
return "Structural: the tonic of the key, which the progression is heard against";
|
|
4037
|
+
case "dominant":
|
|
4038
|
+
return "Structural: the dominant of the key, the chord its tonic is approached from";
|
|
4039
|
+
case "cadentialSixFour":
|
|
4040
|
+
return "Structural: a tonic six-four on the bass of the dominant that follows, which prolongs that dominant rather than framing the tonic";
|
|
4041
|
+
case "cadenceArrival":
|
|
4042
|
+
return `Structural: the arrival of a ${reason.cadence} cadence`;
|
|
4043
|
+
case "cadenceAgent":
|
|
4044
|
+
return `Structural: the chord that drives a ${reason.cadence} cadence`;
|
|
4045
|
+
case "salient":
|
|
4046
|
+
return "Structural: it holds the harmony longer than the chords around it";
|
|
4047
|
+
case "first":
|
|
4048
|
+
return "Structural: the first chord of the progression frames it";
|
|
4049
|
+
case "last":
|
|
4050
|
+
return "Structural: the last chord of the progression frames it";
|
|
4051
|
+
case "unfigured":
|
|
4052
|
+
return "Structural: its root neither steps through to the next chord nor returns to the last, so it embellishes nothing";
|
|
4053
|
+
case "passing":
|
|
4054
|
+
return "Passing: its root steps in from the previous chord and on to the next in the same direction";
|
|
4055
|
+
case "neighbor":
|
|
4056
|
+
return "Neighbor: its root steps away from the surrounding harmony and returns to it";
|
|
4057
|
+
}
|
|
4058
|
+
}
|
|
4059
|
+
var BASIS_READINGS = {
|
|
4060
|
+
function: ({ segment, before, after, keyAt }) => functionalReason(segment, before, after, keyAt),
|
|
4061
|
+
duration: ({ segment, salienceFloor }) => segment.endBeat - segment.startBeat > salienceFloor ? { kind: "salient" } : null
|
|
4062
|
+
};
|
|
4063
|
+
var REDUCTION_BASES = Object.keys(BASIS_READINGS);
|
|
4064
|
+
function levelOf(reason) {
|
|
4065
|
+
if (reason.kind === "passing") {
|
|
4066
|
+
return "passing";
|
|
4067
|
+
}
|
|
4068
|
+
return reason.kind === "neighbor" ? "neighbor" : "structural";
|
|
4069
|
+
}
|
|
4070
|
+
function reduceProgression(timeline, key, opts = {}) {
|
|
4071
|
+
assertChordTimeline(timeline);
|
|
4072
|
+
const asked = _chunkS5BKZZGMcjs.assertOptions.call(void 0, opts, "opts");
|
|
4073
|
+
const keyAt = keyScaleAt(key);
|
|
4074
|
+
const basis = _chunkS5BKZZGMcjs.assertOneOf.call(void 0, _nullishCoalesce(asked.basis, () => ( "function")), REDUCTION_BASES, "reduction basis");
|
|
4075
|
+
const reading = BASIS_READINGS[basis];
|
|
4076
|
+
const segments = timeline.segments;
|
|
4077
|
+
const floors = basis === "duration" ? salienceFloors(segments.map((segment) => segment.endBeat - segment.startBeat)) : [];
|
|
4078
|
+
const result = [];
|
|
4079
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
4080
|
+
const segment = segments[index];
|
|
4081
|
+
if (segment === void 0) {
|
|
4082
|
+
continue;
|
|
4083
|
+
}
|
|
4084
|
+
const previous = segments[index - 1];
|
|
4085
|
+
const following = segments[index + 1];
|
|
4086
|
+
const before = previous !== void 0 && touches(previous, segment) ? previous : void 0;
|
|
4087
|
+
const after = following !== void 0 && touches(segment, following) ? following : void 0;
|
|
4088
|
+
let reason = reading({ segment, before, after, keyAt, salienceFloor: _nullishCoalesce(floors[index], () => ( 0)) });
|
|
4089
|
+
if (reason === null && index === 0) {
|
|
4090
|
+
reason = { kind: "first" };
|
|
4091
|
+
}
|
|
4092
|
+
if (reason === null && index === segments.length - 1) {
|
|
4093
|
+
reason = { kind: "last" };
|
|
4094
|
+
}
|
|
4095
|
+
reason ??= _nullishCoalesce(figureReason(segment, before, after), () => ( { kind: "unfigured" }));
|
|
4096
|
+
result.push({
|
|
4097
|
+
chord: segment.chord,
|
|
4098
|
+
startBeat: segment.startBeat,
|
|
4099
|
+
endBeat: segment.endBeat,
|
|
4100
|
+
level: levelOf(reason),
|
|
4101
|
+
rationale: describe2(reason)
|
|
4102
|
+
});
|
|
4103
|
+
}
|
|
4104
|
+
return result;
|
|
4105
|
+
}
|
|
4106
|
+
|
|
4107
|
+
// src/analyze/rhythm/internal.ts
|
|
4108
|
+
function isMultipleOf(value, unit) {
|
|
4109
|
+
if (unit <= 0) {
|
|
4110
|
+
return false;
|
|
4111
|
+
}
|
|
4112
|
+
const ratio2 = value / unit;
|
|
4113
|
+
return Math.abs(ratio2 - Math.round(ratio2)) < _chunkS5BKZZGMcjs.BEAT_EPS;
|
|
4114
|
+
}
|
|
4115
|
+
function rhythmLevel(beatInQuarters, meters) {
|
|
4116
|
+
const ts = _chunkS5BKZZGMcjs.meterAt.call(void 0, beatInQuarters, meters);
|
|
4117
|
+
const offset = beatInQuarters - _chunkS5BKZZGMcjs.barStartBeat.call(void 0, beatInQuarters, meters);
|
|
4118
|
+
const pulse = _chunkS5BKZZGMcjs.pulseBeats.call(void 0, ts);
|
|
4119
|
+
if (isMultipleOf(offset, pulse)) {
|
|
4120
|
+
return _chunkS5BKZZGMcjs.metricWeight.call(void 0, beatInQuarters, meters) + 2;
|
|
4121
|
+
}
|
|
4122
|
+
const subUnit = pulse / (_chunkS5BKZZGMcjs.isCompound.call(void 0, ts) ? 3 : 2);
|
|
4123
|
+
return isMultipleOf(offset, subUnit) ? 1 : 0;
|
|
4124
|
+
}
|
|
4125
|
+
function ioiBinIndex(ioi) {
|
|
4126
|
+
const exponent = Math.min(4, Math.max(-4, Math.round(2 * Math.log2(ioi)) / 2));
|
|
4127
|
+
return Math.round((exponent + 4) / 0.5);
|
|
4128
|
+
}
|
|
4129
|
+
|
|
4130
|
+
// src/analyze/rhythm/index.ts
|
|
4131
|
+
var RHYTHM_SLOTS_PER_BEAT = 12;
|
|
4132
|
+
var SLOT_WIDTH = 1 / RHYTHM_SLOTS_PER_BEAT;
|
|
4133
|
+
var RHYTHM_OFF_GRID_TOLERANCE = 1 / 32;
|
|
4134
|
+
var RHYTHM_IOI_BINS = Object.freeze(
|
|
4135
|
+
Array.from({ length: 17 }, (_, i) => 2 ** (-4 + i * 0.5))
|
|
4136
|
+
);
|
|
4137
|
+
function emptyAnalysis() {
|
|
4138
|
+
return {
|
|
4139
|
+
startBeat: 0,
|
|
4140
|
+
endBeat: 0,
|
|
4141
|
+
bars: 0,
|
|
4142
|
+
onsets: 0,
|
|
4143
|
+
onsetDensity: 0,
|
|
4144
|
+
barOnsets: [],
|
|
4145
|
+
onsetLevels: new Array(6).fill(0),
|
|
4146
|
+
barPositions: [],
|
|
4147
|
+
interOnsetShares: new Array(RHYTHM_IOI_BINS.length).fill(0),
|
|
4148
|
+
restRatio: 0,
|
|
4149
|
+
syncopation: 0,
|
|
4150
|
+
offGridRatio: 0,
|
|
4151
|
+
rationale: "Nothing sounds, so there is no rhythm to place on a grid"
|
|
4152
|
+
};
|
|
4153
|
+
}
|
|
4154
|
+
function foldOnsets(sounding) {
|
|
4155
|
+
const sorted = [...sounding].sort((a, b) => a.startBeat - b.startBeat);
|
|
4156
|
+
const onsets = [];
|
|
4157
|
+
for (const note of sorted) {
|
|
4158
|
+
const noteEnd = note.startBeat + note.durationBeat;
|
|
4159
|
+
const current = onsets[onsets.length - 1];
|
|
4160
|
+
if (current !== void 0 && note.startBeat - current.beat <= HUMANIZE_ADJACENCY) {
|
|
4161
|
+
current.end = Math.max(current.end, noteEnd);
|
|
4162
|
+
} else {
|
|
4163
|
+
onsets.push({ beat: note.startBeat, end: noteEnd });
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
4166
|
+
return onsets;
|
|
4167
|
+
}
|
|
4168
|
+
function snapOnset(onset, meters) {
|
|
4169
|
+
const ts = _chunkS5BKZZGMcjs.meterAt.call(void 0, onset.beat, meters);
|
|
4170
|
+
const barStart = _chunkS5BKZZGMcjs.barStartBeat.call(void 0, onset.beat, meters);
|
|
4171
|
+
const offset = onset.beat - barStart;
|
|
4172
|
+
const slot = Math.round(offset / SLOT_WIDTH);
|
|
4173
|
+
const snapped = barStart + slot * SLOT_WIDTH;
|
|
4174
|
+
const offGrid = Math.abs(onset.beat - snapped) > RHYTHM_OFF_GRID_TOLERANCE;
|
|
4175
|
+
return { ...onset, ts, barStart, slot, snapped, level: rhythmLevel(snapped, meters), offGrid };
|
|
4176
|
+
}
|
|
4177
|
+
function tsKey(ts) {
|
|
4178
|
+
return ts.grouping === void 0 ? `${ts.numerator}/${ts.denominator}` : `${ts.numerator}/${ts.denominator}:${ts.grouping.join("+")}`;
|
|
4179
|
+
}
|
|
4180
|
+
function barGridBeats(bar, meters) {
|
|
4181
|
+
const barStart = _chunkS5BKZZGMcjs.barPositionToBeat.call(void 0, { bar, beat: 0 }, meters);
|
|
4182
|
+
const ts = _chunkS5BKZZGMcjs.meterAt.call(void 0, barStart, meters);
|
|
4183
|
+
const barLength = _chunkS5BKZZGMcjs.beatsPerBarAt.call(void 0, barStart, meters);
|
|
4184
|
+
const subUnit = _chunkS5BKZZGMcjs.pulseBeats.call(void 0, ts) / (_chunkS5BKZZGMcjs.isCompound.call(void 0, ts) ? 3 : 2);
|
|
4185
|
+
const steps = Math.max(1, Math.round(barLength / subUnit));
|
|
4186
|
+
return Array.from({ length: steps }, (_, k) => barStart + k * subUnit);
|
|
4187
|
+
}
|
|
4188
|
+
function restRatioOf(sounding, windowStart, windowEnd) {
|
|
4189
|
+
const span = windowEnd - windowStart;
|
|
4190
|
+
if (span <= 0) {
|
|
4191
|
+
return 0;
|
|
4192
|
+
}
|
|
4193
|
+
const intervals = sounding.map((note) => [
|
|
4194
|
+
Math.max(windowStart, note.startBeat),
|
|
4195
|
+
Math.min(windowEnd, note.startBeat + note.durationBeat)
|
|
4196
|
+
]).filter(([from, to]) => to > from).sort((a, b) => a[0] - b[0]);
|
|
4197
|
+
let covered = 0;
|
|
4198
|
+
let curStart = 0;
|
|
4199
|
+
let curEnd = Number.NEGATIVE_INFINITY;
|
|
4200
|
+
for (const [from, to] of intervals) {
|
|
4201
|
+
if (from - curEnd > HUMANIZE_ADJACENCY) {
|
|
4202
|
+
if (curEnd > Number.NEGATIVE_INFINITY) {
|
|
4203
|
+
covered += curEnd - curStart;
|
|
4204
|
+
}
|
|
4205
|
+
curStart = from;
|
|
4206
|
+
curEnd = to;
|
|
4207
|
+
} else {
|
|
4208
|
+
curEnd = Math.max(curEnd, to);
|
|
4209
|
+
}
|
|
4210
|
+
}
|
|
4211
|
+
if (curEnd > Number.NEGATIVE_INFINITY) {
|
|
4212
|
+
covered += curEnd - curStart;
|
|
4213
|
+
}
|
|
4214
|
+
return 1 - covered / span;
|
|
4215
|
+
}
|
|
4216
|
+
function syncopationOf(snapped, startBeat, endBeat, meters) {
|
|
4217
|
+
if (snapped.length === 0) {
|
|
4218
|
+
return 0;
|
|
4219
|
+
}
|
|
4220
|
+
const firstBar = _chunkS5BKZZGMcjs.barIndexAt.call(void 0, startBeat, meters);
|
|
4221
|
+
const lastBar = _chunkS5BKZZGMcjs.barIndexAt.call(void 0, endBeat, meters);
|
|
4222
|
+
const points = [];
|
|
4223
|
+
for (let bar = firstBar; bar <= lastBar; bar += 1) {
|
|
4224
|
+
for (const beat of barGridBeats(bar, meters)) {
|
|
4225
|
+
if (beat > startBeat + _chunkS5BKZZGMcjs.BEAT_EPS && beat <= endBeat + _chunkS5BKZZGMcjs.BEAT_EPS) {
|
|
4226
|
+
points.push({ beat, level: rhythmLevel(beat, meters) });
|
|
4227
|
+
}
|
|
4228
|
+
}
|
|
4229
|
+
}
|
|
4230
|
+
const normalizer = points.reduce((sum, p) => sum + p.level, 0);
|
|
4231
|
+
if (normalizer <= 0) {
|
|
4232
|
+
return 0;
|
|
4233
|
+
}
|
|
4234
|
+
let numerator = 0;
|
|
4235
|
+
let ptr = 0;
|
|
4236
|
+
for (let i = 0; i < snapped.length; i += 1) {
|
|
4237
|
+
const onset = snapped[i];
|
|
4238
|
+
if (onset === void 0) {
|
|
4239
|
+
continue;
|
|
4240
|
+
}
|
|
4241
|
+
const next = snapped[i + 1];
|
|
4242
|
+
const windowEnd = next !== void 0 ? next.snapped : onset.end;
|
|
4243
|
+
while (ptr < points.length && (_nullishCoalesce(_optionalChain([points, 'access', _109 => _109[ptr], 'optionalAccess', _110 => _110.beat]), () => ( Number.POSITIVE_INFINITY))) <= onset.snapped + _chunkS5BKZZGMcjs.BEAT_EPS) {
|
|
4244
|
+
ptr += 1;
|
|
4245
|
+
}
|
|
4246
|
+
let scan = ptr;
|
|
4247
|
+
let maxLevel = 0;
|
|
4248
|
+
while (scan < points.length && (_nullishCoalesce(_optionalChain([points, 'access', _111 => _111[scan], 'optionalAccess', _112 => _112.beat]), () => ( Number.POSITIVE_INFINITY))) < windowEnd - _chunkS5BKZZGMcjs.BEAT_EPS) {
|
|
4249
|
+
maxLevel = Math.max(maxLevel, _nullishCoalesce(_optionalChain([points, 'access', _113 => _113[scan], 'optionalAccess', _114 => _114.level]), () => ( 0)));
|
|
4250
|
+
scan += 1;
|
|
4251
|
+
}
|
|
4252
|
+
if (maxLevel > onset.level) {
|
|
4253
|
+
numerator += maxLevel - onset.level;
|
|
4254
|
+
}
|
|
4255
|
+
ptr = scan;
|
|
4256
|
+
}
|
|
4257
|
+
return numerator / normalizer;
|
|
4258
|
+
}
|
|
4259
|
+
function analyzeRhythm(notes, opts = {}) {
|
|
4260
|
+
const meters = _chunkS5BKZZGMcjs.resolveMeters.call(void 0, opts, "rhythm meters");
|
|
4261
|
+
_chunkS5BKZZGMcjs.assertNoteEvents.call(void 0, notes, "rhythm notes", { allowNonPositiveDuration: true, budget: opts.budget });
|
|
4262
|
+
const sounding = notes.filter((note) => note.durationBeat > 0);
|
|
4263
|
+
if (sounding.length === 0) {
|
|
4264
|
+
return emptyAnalysis();
|
|
4265
|
+
}
|
|
4266
|
+
const firstOnset = sounding.reduce(
|
|
4267
|
+
(min, n) => Math.min(min, n.startBeat),
|
|
4268
|
+
Number.POSITIVE_INFINITY
|
|
4269
|
+
);
|
|
4270
|
+
const startBeat = barGridStart(firstOnset, meters);
|
|
4271
|
+
const lastEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
|
|
4272
|
+
const endBeat = Math.max(startBeat, _nullishCoalesce(opts.totalBeats, () => ( lastEnd)));
|
|
4273
|
+
_chunkS5BKZZGMcjs.assertRange.call(void 0, endBeat, 0, Number.MAX_SAFE_INTEGER, "rhythm totalBeats");
|
|
4274
|
+
const bars = barSpanOf(startBeat, endBeat, meters);
|
|
4275
|
+
_chunkS5BKZZGMcjs.assertGenerationBudget.call(void 0, Math.ceil(bars) * 48, "rhythm analysis slots", opts.budget);
|
|
4276
|
+
const onsets = foldOnsets(sounding);
|
|
4277
|
+
const snapped = onsets.map((onset) => snapOnset(onset, meters));
|
|
4278
|
+
const onsetDensity = bars > 0 ? onsets.length / bars : 0;
|
|
4279
|
+
const firstBar = _chunkS5BKZZGMcjs.barIndexAt.call(void 0, startBeat, meters);
|
|
4280
|
+
const barOnsets = new Array(lastBarOf(meters, startBeat, endBeat) - firstBar + 1).fill(0);
|
|
4281
|
+
for (const onset of onsets) {
|
|
4282
|
+
const index = _chunkS5BKZZGMcjs.barIndexAt.call(void 0, onset.beat, meters) - firstBar;
|
|
4283
|
+
if (index >= 0 && index < barOnsets.length) {
|
|
4284
|
+
barOnsets[index] = (_nullishCoalesce(barOnsets[index], () => ( 0))) + 1;
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
4287
|
+
const onsetLevels = new Array(6).fill(0);
|
|
4288
|
+
let offGridCount = 0;
|
|
4289
|
+
for (const onset of snapped) {
|
|
4290
|
+
onsetLevels[onset.level] = (_nullishCoalesce(onsetLevels[onset.level], () => ( 0))) + 1;
|
|
4291
|
+
if (onset.offGrid) {
|
|
4292
|
+
offGridCount += 1;
|
|
4293
|
+
}
|
|
4294
|
+
}
|
|
4295
|
+
for (let i = 0; i < onsetLevels.length; i += 1) {
|
|
4296
|
+
onsetLevels[i] = (_nullishCoalesce(onsetLevels[i], () => ( 0))) / onsets.length;
|
|
4297
|
+
}
|
|
4298
|
+
const profiles = [];
|
|
4299
|
+
const profileIndex = /* @__PURE__ */ new Map();
|
|
4300
|
+
for (const onset of snapped) {
|
|
4301
|
+
const key = tsKey(onset.ts);
|
|
4302
|
+
let index = profileIndex.get(key);
|
|
4303
|
+
if (index === void 0) {
|
|
4304
|
+
index = profiles.length;
|
|
4305
|
+
profileIndex.set(key, index);
|
|
4306
|
+
const slotsPerBar = Math.ceil(_chunkS5BKZZGMcjs.beatsPerBar.call(void 0, onset.ts) * RHYTHM_SLOTS_PER_BEAT);
|
|
4307
|
+
profiles.push({
|
|
4308
|
+
ts: onset.ts,
|
|
4309
|
+
slotsPerBar,
|
|
4310
|
+
shares: new Array(slotsPerBar).fill(0),
|
|
4311
|
+
onsets: 0
|
|
4312
|
+
});
|
|
4313
|
+
}
|
|
4314
|
+
const profile = profiles[index];
|
|
4315
|
+
if (profile !== void 0) {
|
|
4316
|
+
const slotIndex = (onset.slot % profile.slotsPerBar + profile.slotsPerBar) % profile.slotsPerBar;
|
|
4317
|
+
profile.shares[slotIndex] = (_nullishCoalesce(profile.shares[slotIndex], () => ( 0))) + 1;
|
|
4318
|
+
profile.onsets += 1;
|
|
4319
|
+
}
|
|
4320
|
+
}
|
|
4321
|
+
for (const profile of profiles) {
|
|
4322
|
+
for (let i = 0; i < profile.shares.length; i += 1) {
|
|
4323
|
+
profile.shares[i] = (_nullishCoalesce(profile.shares[i], () => ( 0))) / profile.onsets;
|
|
4324
|
+
}
|
|
4325
|
+
}
|
|
4326
|
+
const interOnsetShares = new Array(RHYTHM_IOI_BINS.length).fill(0);
|
|
4327
|
+
for (let i = 0; i < onsets.length; i += 1) {
|
|
4328
|
+
const onset = onsets[i];
|
|
4329
|
+
if (onset === void 0) {
|
|
4330
|
+
continue;
|
|
4331
|
+
}
|
|
4332
|
+
const next = onsets[i + 1];
|
|
4333
|
+
const ioi = next !== void 0 ? next.beat - onset.beat : onset.end - onset.beat;
|
|
4334
|
+
const bin = ioiBinIndex(ioi);
|
|
4335
|
+
interOnsetShares[bin] = (_nullishCoalesce(interOnsetShares[bin], () => ( 0))) + 1;
|
|
4336
|
+
}
|
|
4337
|
+
for (let i = 0; i < interOnsetShares.length; i += 1) {
|
|
4338
|
+
interOnsetShares[i] = (_nullishCoalesce(interOnsetShares[i], () => ( 0))) / onsets.length;
|
|
4339
|
+
}
|
|
4340
|
+
const restRatio = restRatioOf(sounding, startBeat, endBeat);
|
|
4341
|
+
const syncopation = syncopationOf(snapped, startBeat, endBeat, meters);
|
|
4342
|
+
const offGridRatio = offGridCount / onsets.length;
|
|
4343
|
+
return {
|
|
4344
|
+
startBeat,
|
|
4345
|
+
endBeat,
|
|
4346
|
+
bars,
|
|
4347
|
+
onsets: onsets.length,
|
|
4348
|
+
onsetDensity,
|
|
4349
|
+
barOnsets,
|
|
4350
|
+
onsetLevels,
|
|
4351
|
+
barPositions: profiles,
|
|
4352
|
+
interOnsetShares,
|
|
4353
|
+
restRatio,
|
|
4354
|
+
syncopation,
|
|
4355
|
+
offGridRatio,
|
|
4356
|
+
rationale: `${onsets.length} onsets over ${bars.toFixed(2)} bars (${onsetDensity.toFixed(2)} per bar), ${Math.round(restRatio * 100)}% rest, syncopation ${syncopation.toFixed(2)}`
|
|
4357
|
+
};
|
|
4358
|
+
}
|
|
4359
|
+
|
|
4360
|
+
// src/analyze/reference/types.ts
|
|
4361
|
+
var REFERENCE_PROFILE_VERSION = 1;
|
|
4362
|
+
|
|
4363
|
+
// src/analyze/reference/validate.ts
|
|
4364
|
+
var MASS_TOLERANCE = 1e-9;
|
|
4365
|
+
function assertString(value, name) {
|
|
4366
|
+
if (typeof value !== "string") {
|
|
4367
|
+
throw new (0, _chunkS5BKZZGMcjs.InvalidInputError)(`${name} must be a string; received ${_chunkS5BKZZGMcjs.describeRejected.call(void 0, value)}`);
|
|
4368
|
+
}
|
|
4369
|
+
return value;
|
|
4370
|
+
}
|
|
4371
|
+
function assertSpanOrder(startBeat, endBeat, name) {
|
|
4372
|
+
if (endBeat < startBeat) {
|
|
4373
|
+
throw new (0, _chunkS5BKZZGMcjs.InvalidInputError)(
|
|
4374
|
+
`${name}.endBeat must be at least ${name}.startBeat; received ${endBeat} < ${startBeat}`
|
|
4375
|
+
);
|
|
4376
|
+
}
|
|
4377
|
+
}
|
|
4378
|
+
function assertNullable(value, name, validate) {
|
|
4379
|
+
if (value === void 0) {
|
|
4380
|
+
throw new (0, _chunkS5BKZZGMcjs.InvalidInputError)(`${name} must be present; received undefined`);
|
|
4381
|
+
}
|
|
4382
|
+
return value === null ? null : validate(value, name);
|
|
4383
|
+
}
|
|
4384
|
+
function assertMassDistribution(value, length, name, allowAllZero) {
|
|
4385
|
+
const shares = _chunkS5BKZZGMcjs.assertArray.call(void 0, value, name);
|
|
4386
|
+
if (shares.length !== length) {
|
|
4387
|
+
throw new (0, _chunkS5BKZZGMcjs.InvalidInputError)(`${name} must have length ${length}; received ${shares.length}`);
|
|
4388
|
+
}
|
|
4389
|
+
let sum = 0;
|
|
4390
|
+
for (let index = 0; index < shares.length; index += 1) {
|
|
4391
|
+
const share = _chunkS5BKZZGMcjs.assertFiniteNumber.call(void 0, shares[index], `${name}[${index}]`);
|
|
4392
|
+
if (share < 0) {
|
|
4393
|
+
throw new (0, _chunkS5BKZZGMcjs.InvalidInputError)(`${name}[${index}] must be at least 0; received ${share}`);
|
|
4394
|
+
}
|
|
4395
|
+
sum += share;
|
|
4396
|
+
}
|
|
4397
|
+
if (allowAllZero && sum === 0) {
|
|
4398
|
+
return shares;
|
|
4399
|
+
}
|
|
4400
|
+
if (Math.abs(sum - 1) > MASS_TOLERANCE) {
|
|
4401
|
+
throw new (0, _chunkS5BKZZGMcjs.InvalidInputError)(`${name} must sum to 1; received ${sum}`);
|
|
4402
|
+
}
|
|
4403
|
+
return shares;
|
|
4404
|
+
}
|
|
4405
|
+
function assertBarPositionProfile(value, name) {
|
|
4406
|
+
const read = _chunkS5BKZZGMcjs.assertRecord.call(void 0, value, name);
|
|
4407
|
+
_chunkS5BKZZGMcjs.assertTimeSignature.call(void 0, read.ts, `${name}.ts`);
|
|
4408
|
+
const slotsPerBar = _chunkS5BKZZGMcjs.assertPositiveInt.call(void 0, read.slotsPerBar, `${name}.slotsPerBar`);
|
|
4409
|
+
const onsets = _chunkS5BKZZGMcjs.assertInteger.call(void 0, read.onsets, `${name}.onsets`, 0);
|
|
4410
|
+
assertMassDistribution(read.shares, slotsPerBar, `${name}.shares`, onsets === 0);
|
|
4411
|
+
return read;
|
|
4412
|
+
}
|
|
4413
|
+
function assertRhythmAnalysis(value, name) {
|
|
4414
|
+
const read = _chunkS5BKZZGMcjs.assertRecord.call(void 0, value, name);
|
|
4415
|
+
const startBeat = _chunkS5BKZZGMcjs.assertFiniteNumber.call(void 0, read.startBeat, `${name}.startBeat`);
|
|
4416
|
+
const endBeat = _chunkS5BKZZGMcjs.assertFiniteNumber.call(void 0, read.endBeat, `${name}.endBeat`);
|
|
4417
|
+
assertSpanOrder(startBeat, endBeat, name);
|
|
4418
|
+
_chunkS5BKZZGMcjs.assertRange.call(void 0, read.bars, 0, Number.MAX_SAFE_INTEGER, `${name}.bars`);
|
|
4419
|
+
const onsets = _chunkS5BKZZGMcjs.assertInteger.call(void 0, read.onsets, `${name}.onsets`, 0);
|
|
4420
|
+
_chunkS5BKZZGMcjs.assertRange.call(void 0, read.onsetDensity, 0, Number.MAX_SAFE_INTEGER, `${name}.onsetDensity`);
|
|
4421
|
+
const barOnsets = _chunkS5BKZZGMcjs.assertArray.call(void 0, read.barOnsets, `${name}.barOnsets`);
|
|
4422
|
+
for (let index = 0; index < barOnsets.length; index += 1) {
|
|
4423
|
+
_chunkS5BKZZGMcjs.assertRange.call(void 0,
|
|
4424
|
+
barOnsets[index],
|
|
4425
|
+
0,
|
|
4426
|
+
Number.MAX_SAFE_INTEGER,
|
|
4427
|
+
`${name}.barOnsets[${index}]`
|
|
4428
|
+
);
|
|
4429
|
+
}
|
|
4430
|
+
assertMassDistribution(read.onsetLevels, 6, `${name}.onsetLevels`, onsets === 0);
|
|
4431
|
+
const barPositions = _chunkS5BKZZGMcjs.assertArray.call(void 0, read.barPositions, `${name}.barPositions`);
|
|
4432
|
+
for (let index = 0; index < barPositions.length; index += 1) {
|
|
4433
|
+
assertBarPositionProfile(barPositions[index], `${name}.barPositions[${index}]`);
|
|
4434
|
+
}
|
|
4435
|
+
assertMassDistribution(
|
|
4436
|
+
read.interOnsetShares,
|
|
4437
|
+
RHYTHM_IOI_BINS.length,
|
|
4438
|
+
`${name}.interOnsetShares`,
|
|
4439
|
+
onsets === 0
|
|
4440
|
+
);
|
|
4441
|
+
_chunkS5BKZZGMcjs.assertRange.call(void 0, read.restRatio, 0, 1, `${name}.restRatio`);
|
|
4442
|
+
_chunkS5BKZZGMcjs.assertRange.call(void 0, read.syncopation, 0, 1, `${name}.syncopation`);
|
|
4443
|
+
_chunkS5BKZZGMcjs.assertRange.call(void 0, read.offGridRatio, 0, 1, `${name}.offGridRatio`);
|
|
4444
|
+
assertString(read.rationale, `${name}.rationale`);
|
|
4445
|
+
return read;
|
|
4446
|
+
}
|
|
4447
|
+
function assertFormSection(value, name, sectionCount) {
|
|
4448
|
+
const read = _chunkS5BKZZGMcjs.assertRecord.call(void 0, value, name);
|
|
4449
|
+
assertString(read.label, `${name}.label`);
|
|
4450
|
+
const startBeat = _chunkS5BKZZGMcjs.assertFiniteNumber.call(void 0, read.startBeat, `${name}.startBeat`);
|
|
4451
|
+
const endBeat = _chunkS5BKZZGMcjs.assertFiniteNumber.call(void 0, read.endBeat, `${name}.endBeat`);
|
|
4452
|
+
assertSpanOrder(startBeat, endBeat, name);
|
|
4453
|
+
_chunkS5BKZZGMcjs.assertRange.call(void 0, read.bars, 0, Number.MAX_SAFE_INTEGER, `${name}.bars`);
|
|
4454
|
+
_chunkS5BKZZGMcjs.assertInteger.call(void 0, read.firstOccurrence, `${name}.firstOccurrence`, 0, Math.max(sectionCount - 1, 0));
|
|
4455
|
+
_chunkS5BKZZGMcjs.assertRange.call(void 0, read.similarity, 0, 1, `${name}.similarity`);
|
|
4456
|
+
assertString(read.rationale, `${name}.rationale`);
|
|
4457
|
+
return read;
|
|
4458
|
+
}
|
|
4459
|
+
function assertReferenceCadence(value, name) {
|
|
4460
|
+
const read = _chunkS5BKZZGMcjs.assertRecord.call(void 0, value, name);
|
|
4461
|
+
_chunkS5BKZZGMcjs.assertFiniteNumber.call(void 0, read.atBeat, `${name}.atBeat`);
|
|
4462
|
+
_chunkS5BKZZGMcjs.assertOneOf.call(void 0, read.type, CADENCE_TYPES, `${name}.type`);
|
|
4463
|
+
_chunkS5BKZZGMcjs.assertRange.call(void 0, read.weight, 0, 1, `${name}.weight`);
|
|
4464
|
+
return read;
|
|
4465
|
+
}
|
|
4466
|
+
function assertReferencePhraseMelody(value, name) {
|
|
4467
|
+
const read = _chunkS5BKZZGMcjs.assertRecord.call(void 0, value, name);
|
|
4468
|
+
_chunkS5BKZZGMcjs.assertOneOf.call(void 0, read.shape, MELODIC_CONTOUR_SHAPES, `${name}.shape`);
|
|
4469
|
+
_chunkS5BKZZGMcjs.assertMidiPitch.call(void 0, read.low, `${name}.low`);
|
|
4470
|
+
_chunkS5BKZZGMcjs.assertMidiPitch.call(void 0, read.high, `${name}.high`);
|
|
4471
|
+
_chunkS5BKZZGMcjs.assertRange.call(void 0, read.mean, 0, 127, `${name}.mean`);
|
|
4472
|
+
_chunkS5BKZZGMcjs.assertRange.call(void 0, read.peakPitch, 0, 127, `${name}.peakPitch`);
|
|
4473
|
+
_chunkS5BKZZGMcjs.assertRange.call(void 0, read.peakPosition, 0, 1, `${name}.peakPosition`);
|
|
4474
|
+
const outline = _chunkS5BKZZGMcjs.assertArray.call(void 0, read.outline, `${name}.outline`);
|
|
4475
|
+
if (outline.length !== 8) {
|
|
4476
|
+
throw new (0, _chunkS5BKZZGMcjs.InvalidInputError)(`${name}.outline must have length 8; received ${outline.length}`);
|
|
4477
|
+
}
|
|
4478
|
+
for (let index = 0; index < outline.length; index += 1) {
|
|
4479
|
+
_chunkS5BKZZGMcjs.assertFiniteNumber.call(void 0, outline[index], `${name}.outline[${index}]`);
|
|
4480
|
+
}
|
|
4481
|
+
_chunkS5BKZZGMcjs.assertRange.call(void 0, read.onsetDensity, 0, Number.MAX_SAFE_INTEGER, `${name}.onsetDensity`);
|
|
4482
|
+
_chunkS5BKZZGMcjs.assertRange.call(void 0, read.restRatio, 0, 1, `${name}.restRatio`);
|
|
4483
|
+
return read;
|
|
4484
|
+
}
|
|
4485
|
+
function assertReferencePhrase(value, name, sectionCount, nodeCount) {
|
|
4486
|
+
const read = _chunkS5BKZZGMcjs.assertRecord.call(void 0, value, name);
|
|
4487
|
+
const startBeat = _chunkS5BKZZGMcjs.assertFiniteNumber.call(void 0, read.startBeat, `${name}.startBeat`);
|
|
4488
|
+
const endBeat = _chunkS5BKZZGMcjs.assertFiniteNumber.call(void 0, read.endBeat, `${name}.endBeat`);
|
|
4489
|
+
assertSpanOrder(startBeat, endBeat, name);
|
|
4490
|
+
_chunkS5BKZZGMcjs.assertRange.call(void 0, read.bars, 0, Number.MAX_SAFE_INTEGER, `${name}.bars`);
|
|
4491
|
+
assertNullable(
|
|
4492
|
+
read.section,
|
|
4493
|
+
`${name}.section`,
|
|
4494
|
+
(v, n) => _chunkS5BKZZGMcjs.assertInteger.call(void 0, v, n, 0, Math.max(sectionCount - 1, 0))
|
|
4495
|
+
);
|
|
4496
|
+
_chunkS5BKZZGMcjs.assertRange.call(void 0, read.confidence, 0, 1, `${name}.confidence`);
|
|
4497
|
+
assertNullable(read.cadence, `${name}.cadence`, assertReferenceCadence);
|
|
4498
|
+
assertNullable(read.melody, `${name}.melody`, assertReferencePhraseMelody);
|
|
4499
|
+
const motifNodes = _chunkS5BKZZGMcjs.assertArray.call(void 0, read.motifNodes, `${name}.motifNodes`);
|
|
4500
|
+
for (let index = 0; index < motifNodes.length; index += 1) {
|
|
4501
|
+
_chunkS5BKZZGMcjs.assertInteger.call(void 0,
|
|
4502
|
+
motifNodes[index],
|
|
4503
|
+
`${name}.motifNodes[${index}]`,
|
|
4504
|
+
0,
|
|
4505
|
+
Math.max(nodeCount - 1, 0)
|
|
4506
|
+
);
|
|
4507
|
+
}
|
|
4508
|
+
return read;
|
|
4509
|
+
}
|
|
4510
|
+
function assertReferenceForm(value, name, nodeCount) {
|
|
4511
|
+
const read = _chunkS5BKZZGMcjs.assertRecord.call(void 0, value, name);
|
|
4512
|
+
const hypermeter = _chunkS5BKZZGMcjs.assertRecord.call(void 0,
|
|
4513
|
+
read.hypermeter,
|
|
4514
|
+
`${name}.hypermeter`
|
|
4515
|
+
);
|
|
4516
|
+
_chunkS5BKZZGMcjs.assertRange.call(void 0, hypermeter.groupBars, 0, Number.MAX_SAFE_INTEGER, `${name}.hypermeter.groupBars`);
|
|
4517
|
+
_chunkS5BKZZGMcjs.assertRange.call(void 0, hypermeter.confidence, 0, 1, `${name}.hypermeter.confidence`);
|
|
4518
|
+
const sections = _chunkS5BKZZGMcjs.assertArray.call(void 0, read.sections, `${name}.sections`);
|
|
4519
|
+
for (let index = 0; index < sections.length; index += 1) {
|
|
4520
|
+
assertFormSection(sections[index], `${name}.sections[${index}]`, sections.length);
|
|
4521
|
+
}
|
|
4522
|
+
const phrases = _chunkS5BKZZGMcjs.assertArray.call(void 0, read.phrases, `${name}.phrases`);
|
|
4523
|
+
for (let index = 0; index < phrases.length; index += 1) {
|
|
4524
|
+
assertReferencePhrase(phrases[index], `${name}.phrases[${index}]`, sections.length, nodeCount);
|
|
4525
|
+
}
|
|
4526
|
+
return read;
|
|
4527
|
+
}
|
|
4528
|
+
function assertResolvedKeyField(value, name) {
|
|
4529
|
+
const read = _chunkS5BKZZGMcjs.assertRecord.call(void 0, value, name);
|
|
4530
|
+
try {
|
|
4531
|
+
_chunk3R3QVKY5cjs.resolveKey.call(void 0, read);
|
|
4532
|
+
} catch (error) {
|
|
4533
|
+
if (error instanceof _chunkS5BKZZGMcjs.InvalidInputError) {
|
|
4534
|
+
throw new (0, _chunkS5BKZZGMcjs.InvalidInputError)(`${name} must be a valid key: ${error.message}`);
|
|
4535
|
+
}
|
|
4536
|
+
throw error;
|
|
4537
|
+
}
|
|
4538
|
+
return read;
|
|
4539
|
+
}
|
|
4540
|
+
function assertReferenceKeyRegion(value, name) {
|
|
4541
|
+
const read = _chunkS5BKZZGMcjs.assertRecord.call(void 0, value, name);
|
|
4542
|
+
const startBeat = _chunkS5BKZZGMcjs.assertFiniteNumber.call(void 0, read.startBeat, `${name}.startBeat`);
|
|
4543
|
+
const endBeat = _chunkS5BKZZGMcjs.assertFiniteNumber.call(void 0, read.endBeat, `${name}.endBeat`);
|
|
4544
|
+
assertSpanOrder(startBeat, endBeat, name);
|
|
4545
|
+
assertResolvedKeyField(read.key, `${name}.key`);
|
|
4546
|
+
_chunkS5BKZZGMcjs.assertRange.call(void 0, read.confidence, 0, 1, `${name}.confidence`);
|
|
4547
|
+
return read;
|
|
4548
|
+
}
|
|
4549
|
+
function assertReferenceChord(value, name, keys) {
|
|
4550
|
+
const read = _chunkS5BKZZGMcjs.assertRecord.call(void 0, value, name);
|
|
4551
|
+
const startBeat = _chunkS5BKZZGMcjs.assertFiniteNumber.call(void 0, read.startBeat, `${name}.startBeat`);
|
|
4552
|
+
const endBeat = _chunkS5BKZZGMcjs.assertFiniteNumber.call(void 0, read.endBeat, `${name}.endBeat`);
|
|
4553
|
+
assertSpanOrder(startBeat, endBeat, name);
|
|
4554
|
+
const keyIndex = _chunkS5BKZZGMcjs.assertInteger.call(void 0, read.key, `${name}.key`, 0, Math.max(keys.length - 1, 0));
|
|
4555
|
+
const roman = assertString(read.roman, `${name}.roman`);
|
|
4556
|
+
try {
|
|
4557
|
+
romanToChord(roman, keys[keyIndex]);
|
|
4558
|
+
} catch (error) {
|
|
4559
|
+
if (error instanceof _chunkS5BKZZGMcjs.InvalidInputError) {
|
|
4560
|
+
throw new (0, _chunkS5BKZZGMcjs.InvalidInputError)(
|
|
4561
|
+
`${name}.roman must be readable by romanToChord; received ${_chunkS5BKZZGMcjs.describeRejected.call(void 0, roman)}: ${error.message}`
|
|
4562
|
+
);
|
|
4563
|
+
}
|
|
4564
|
+
throw error;
|
|
4565
|
+
}
|
|
4566
|
+
_chunkS5BKZZGMcjs.assertOneOf.call(void 0, read.function, HARMONIC_FUNCTIONS, `${name}.function`);
|
|
4567
|
+
_chunkS5BKZZGMcjs.assertOneOf.call(void 0, read.level, REDUCTION_LEVELS, `${name}.level`);
|
|
4568
|
+
return read;
|
|
4569
|
+
}
|
|
4570
|
+
function assertReferenceHarmony(value, name) {
|
|
4571
|
+
const read = _chunkS5BKZZGMcjs.assertRecord.call(void 0, value, name);
|
|
4572
|
+
const keys = _chunkS5BKZZGMcjs.assertArray.call(void 0, read.keys, `${name}.keys`);
|
|
4573
|
+
for (let index = 0; index < keys.length; index += 1) {
|
|
4574
|
+
assertReferenceKeyRegion(keys[index], `${name}.keys[${index}]`);
|
|
4575
|
+
}
|
|
4576
|
+
const resolvedKeys = keys.map((region) => region.key);
|
|
4577
|
+
const chords = _chunkS5BKZZGMcjs.assertArray.call(void 0, read.chords, `${name}.chords`);
|
|
4578
|
+
for (let index = 0; index < chords.length; index += 1) {
|
|
4579
|
+
assertReferenceChord(chords[index], `${name}.chords[${index}]`, resolvedKeys);
|
|
4580
|
+
}
|
|
4581
|
+
assertRhythmAnalysis(read.rhythm, `${name}.rhythm`);
|
|
4582
|
+
return read;
|
|
4583
|
+
}
|
|
4584
|
+
function assertMotifRelationSummary(value, name) {
|
|
4585
|
+
const read = _chunkS5BKZZGMcjs.assertRecord.call(void 0, value, name);
|
|
4586
|
+
_chunkS5BKZZGMcjs.assertOneOf.call(void 0, read.kind, MOTIF_RELATION_KINDS, `${name}.kind`);
|
|
4587
|
+
_chunkS5BKZZGMcjs.assertFlag.call(void 0, read.sequence, `${name}.sequence`);
|
|
4588
|
+
_chunkS5BKZZGMcjs.assertFiniteNumber.call(void 0, read.semitones, `${name}.semitones`);
|
|
4589
|
+
if (read.degrees !== void 0) {
|
|
4590
|
+
_chunkS5BKZZGMcjs.assertFiniteNumber.call(void 0, read.degrees, `${name}.degrees`);
|
|
4591
|
+
}
|
|
4592
|
+
const timeRatio = _chunkS5BKZZGMcjs.assertFiniteNumber.call(void 0, read.timeRatio, `${name}.timeRatio`);
|
|
4593
|
+
if (timeRatio <= 0) {
|
|
4594
|
+
throw new (0, _chunkS5BKZZGMcjs.InvalidInputError)(`${name}.timeRatio must be positive; received ${timeRatio}`);
|
|
4595
|
+
}
|
|
4596
|
+
return read;
|
|
4597
|
+
}
|
|
4598
|
+
function assertMotifGraphNode(value, name, occurrenceCounts) {
|
|
4599
|
+
const read = _chunkS5BKZZGMcjs.assertRecord.call(void 0, value, name);
|
|
4600
|
+
const motif = _chunkS5BKZZGMcjs.assertInteger.call(void 0,
|
|
4601
|
+
read.motif,
|
|
4602
|
+
`${name}.motif`,
|
|
4603
|
+
0,
|
|
4604
|
+
Math.max(occurrenceCounts.length - 1, 0)
|
|
4605
|
+
);
|
|
4606
|
+
_chunkS5BKZZGMcjs.assertInteger.call(void 0,
|
|
4607
|
+
read.occurrence,
|
|
4608
|
+
`${name}.occurrence`,
|
|
4609
|
+
0,
|
|
4610
|
+
Math.max((_nullishCoalesce(occurrenceCounts[motif], () => ( 1))) - 1, 0)
|
|
4611
|
+
);
|
|
4612
|
+
const startBeat = _chunkS5BKZZGMcjs.assertFiniteNumber.call(void 0, read.startBeat, `${name}.startBeat`);
|
|
4613
|
+
const endBeat = _chunkS5BKZZGMcjs.assertFiniteNumber.call(void 0, read.endBeat, `${name}.endBeat`);
|
|
4614
|
+
assertSpanOrder(startBeat, endBeat, name);
|
|
4615
|
+
return read;
|
|
4616
|
+
}
|
|
4617
|
+
function assertMotifGraphEdge(value, name, nodeCount) {
|
|
4618
|
+
const read = _chunkS5BKZZGMcjs.assertRecord.call(void 0, value, name);
|
|
4619
|
+
const from = _chunkS5BKZZGMcjs.assertInteger.call(void 0, read.from, `${name}.from`, 0, Math.max(nodeCount - 1, 0));
|
|
4620
|
+
const to = _chunkS5BKZZGMcjs.assertInteger.call(void 0, read.to, `${name}.to`, 0, Math.max(nodeCount - 1, 0));
|
|
4621
|
+
if (from >= to) {
|
|
4622
|
+
throw new (0, _chunkS5BKZZGMcjs.InvalidInputError)(
|
|
4623
|
+
`${name}.from must be less than ${name}.to; received from=${from}, to=${to}`
|
|
4624
|
+
);
|
|
4625
|
+
}
|
|
4626
|
+
assertNullable(read.relation, `${name}.relation`, assertMotifRelationSummary);
|
|
4627
|
+
_chunkS5BKZZGMcjs.assertRange.call(void 0, read.similarity, 0, 1, `${name}.similarity`);
|
|
4628
|
+
return read;
|
|
4629
|
+
}
|
|
4630
|
+
function assertMotifGraph(value, name, occurrenceCounts) {
|
|
4631
|
+
const read = _chunkS5BKZZGMcjs.assertRecord.call(void 0, value, name);
|
|
4632
|
+
const nodes = _chunkS5BKZZGMcjs.assertArray.call(void 0, read.nodes, `${name}.nodes`);
|
|
4633
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
4634
|
+
assertMotifGraphNode(nodes[index], `${name}.nodes[${index}]`, occurrenceCounts);
|
|
4635
|
+
}
|
|
4636
|
+
const edges = _chunkS5BKZZGMcjs.assertArray.call(void 0, read.edges, `${name}.edges`);
|
|
4637
|
+
const seenTo = /* @__PURE__ */ new Set();
|
|
4638
|
+
for (let index = 0; index < edges.length; index += 1) {
|
|
4639
|
+
const edge = assertMotifGraphEdge(edges[index], `${name}.edges[${index}]`, nodes.length);
|
|
4640
|
+
if (seenTo.has(edge.to)) {
|
|
4641
|
+
throw new (0, _chunkS5BKZZGMcjs.InvalidInputError)(`${name}.edges: node ${edge.to} has more than one incoming edge`);
|
|
4642
|
+
}
|
|
4643
|
+
seenTo.add(edge.to);
|
|
4644
|
+
}
|
|
4645
|
+
return read;
|
|
4646
|
+
}
|
|
4647
|
+
function assertReferenceMotif(value, name) {
|
|
4648
|
+
const read = _chunkS5BKZZGMcjs.assertRecord.call(void 0, value, name);
|
|
4649
|
+
const intervals = _chunkS5BKZZGMcjs.assertArray.call(void 0, read.intervals, `${name}.intervals`);
|
|
4650
|
+
let pitch = 0;
|
|
4651
|
+
let low = 0;
|
|
4652
|
+
let high = 0;
|
|
4653
|
+
for (let index = 0; index < intervals.length; index += 1) {
|
|
4654
|
+
const interval = _chunkS5BKZZGMcjs.assertFiniteNumber.call(void 0, intervals[index], `${name}.intervals[${index}]`);
|
|
4655
|
+
if (!Number.isInteger(interval)) {
|
|
4656
|
+
throw new (0, _chunkS5BKZZGMcjs.InvalidInputError)(
|
|
4657
|
+
`${name}.intervals[${index}] must be a whole number of semitones; received ${interval}`
|
|
4658
|
+
);
|
|
4659
|
+
}
|
|
4660
|
+
pitch += interval;
|
|
4661
|
+
low = Math.min(low, pitch);
|
|
4662
|
+
high = Math.max(high, pitch);
|
|
4663
|
+
}
|
|
4664
|
+
if (high - low > 127) {
|
|
4665
|
+
throw new (0, _chunkS5BKZZGMcjs.InvalidInputError)(
|
|
4666
|
+
`${name}.intervals must span at most 127 semitones; received ${high - low}`
|
|
4667
|
+
);
|
|
4668
|
+
}
|
|
4669
|
+
const rhythm = _chunkS5BKZZGMcjs.assertArray.call(void 0, read.rhythm, `${name}.rhythm`);
|
|
4670
|
+
if (rhythm.length !== intervals.length) {
|
|
4671
|
+
throw new (0, _chunkS5BKZZGMcjs.InvalidInputError)(
|
|
4672
|
+
`${name}.rhythm must hold one ratio per interval (${intervals.length}); received ${rhythm.length}`
|
|
4673
|
+
);
|
|
4674
|
+
}
|
|
4675
|
+
for (let index = 0; index < rhythm.length; index += 1) {
|
|
4676
|
+
const ratio2 = _chunkS5BKZZGMcjs.assertFiniteNumber.call(void 0, rhythm[index], `${name}.rhythm[${index}]`);
|
|
4677
|
+
if (!(ratio2 > 0)) {
|
|
4678
|
+
throw new (0, _chunkS5BKZZGMcjs.InvalidInputError)(`${name}.rhythm[${index}] must be positive; received ${ratio2}`);
|
|
4679
|
+
}
|
|
4680
|
+
}
|
|
4681
|
+
const unitBeats = _chunkS5BKZZGMcjs.assertRange.call(void 0, read.unitBeats, 0, Number.MAX_SAFE_INTEGER, `${name}.unitBeats`);
|
|
4682
|
+
if (unitBeats > 0 !== rhythm.length > 0) {
|
|
4683
|
+
throw new (0, _chunkS5BKZZGMcjs.InvalidInputError)(
|
|
4684
|
+
`${name}.unitBeats must be positive exactly when rhythm is non-empty; received ${unitBeats} with ${rhythm.length} ratio(s)`
|
|
4685
|
+
);
|
|
4686
|
+
}
|
|
4687
|
+
_chunkS5BKZZGMcjs.assertRange.call(void 0, read.spanBeats, 0, Number.MAX_SAFE_INTEGER, `${name}.spanBeats`);
|
|
4688
|
+
_chunkS5BKZZGMcjs.assertPositiveInt.call(void 0, read.occurrences, `${name}.occurrences`);
|
|
4689
|
+
return read;
|
|
4690
|
+
}
|
|
4691
|
+
function assertReferenceMelody(value, name) {
|
|
4692
|
+
const read = _chunkS5BKZZGMcjs.assertRecord.call(void 0, value, name);
|
|
4693
|
+
assertNullable(read.register, `${name}.register`, (v, n) => {
|
|
4694
|
+
const register = _chunkS5BKZZGMcjs.assertRecord.call(void 0, v, n);
|
|
4695
|
+
_chunkS5BKZZGMcjs.assertMidiPitch.call(void 0, register.low, `${n}.low`);
|
|
4696
|
+
_chunkS5BKZZGMcjs.assertMidiPitch.call(void 0, register.high, `${n}.high`);
|
|
4697
|
+
_chunkS5BKZZGMcjs.assertRange.call(void 0, register.mean, 0, 127, `${n}.mean`);
|
|
4698
|
+
return register;
|
|
4699
|
+
});
|
|
4700
|
+
assertNullable(read.shape, `${name}.shape`, (v, n) => _chunkS5BKZZGMcjs.assertOneOf.call(void 0, v, MELODIC_CONTOUR_SHAPES, n));
|
|
4701
|
+
const motifs = _chunkS5BKZZGMcjs.assertArray.call(void 0, read.motifs, `${name}.motifs`);
|
|
4702
|
+
const occurrenceCounts = [];
|
|
4703
|
+
for (let index = 0; index < motifs.length; index += 1) {
|
|
4704
|
+
assertReferenceMotif(motifs[index], `${name}.motifs[${index}]`);
|
|
4705
|
+
occurrenceCounts.push(motifs[index].occurrences);
|
|
4706
|
+
}
|
|
4707
|
+
assertMotifGraph(read.graph, `${name}.graph`, occurrenceCounts);
|
|
4708
|
+
assertRhythmAnalysis(read.rhythm, `${name}.rhythm`);
|
|
4709
|
+
return read;
|
|
4710
|
+
}
|
|
4711
|
+
function assertReferenceProfile(value, name = "reference profile") {
|
|
4712
|
+
const read = _chunkS5BKZZGMcjs.assertRecord.call(void 0, value, name);
|
|
4713
|
+
if (read.profileVersion !== REFERENCE_PROFILE_VERSION) {
|
|
4714
|
+
throw new (0, _chunkS5BKZZGMcjs.InvalidInputError)(
|
|
4715
|
+
`${name}.profileVersion must be ${REFERENCE_PROFILE_VERSION}; received ${_chunkS5BKZZGMcjs.describeRejected.call(void 0, read.profileVersion)}`
|
|
4716
|
+
);
|
|
4717
|
+
}
|
|
4718
|
+
const span = _chunkS5BKZZGMcjs.assertRecord.call(void 0,
|
|
4719
|
+
read.span,
|
|
4720
|
+
`${name}.span`
|
|
4721
|
+
);
|
|
4722
|
+
const startBeat = _chunkS5BKZZGMcjs.assertFiniteNumber.call(void 0, span.startBeat, `${name}.span.startBeat`);
|
|
4723
|
+
const endBeat = _chunkS5BKZZGMcjs.assertFiniteNumber.call(void 0, span.endBeat, `${name}.span.endBeat`);
|
|
4724
|
+
assertSpanOrder(startBeat, endBeat, `${name}.span`);
|
|
4725
|
+
_chunkS5BKZZGMcjs.assertRange.call(void 0, span.bars, 0, Number.MAX_SAFE_INTEGER, `${name}.span.bars`);
|
|
4726
|
+
_chunkS5BKZZGMcjs.assertMeterMap.call(void 0, read.meters, `${name}.meters`);
|
|
4727
|
+
const melody = assertReferenceMelody(read.melody, `${name}.melody`);
|
|
4728
|
+
assertReferenceForm(read.form, `${name}.form`, melody.graph.nodes.length);
|
|
4729
|
+
assertReferenceHarmony(read.harmony, `${name}.harmony`);
|
|
4730
|
+
return read;
|
|
4731
|
+
}
|
|
4732
|
+
|
|
4733
|
+
// src/analyze/reference/measures.ts
|
|
4734
|
+
var PROGRESSION_GAP = 0.75;
|
|
4735
|
+
var SAME_DEGREE_COST = 0.25;
|
|
4736
|
+
var SAME_FUNCTION_COST = 0.5;
|
|
4737
|
+
var HALF_PHRYGIAN_COST = 0.5;
|
|
4738
|
+
var MODE_CHANGE_COST = 0.5;
|
|
4739
|
+
var SHAPE_MISMATCH_COST = 0.25;
|
|
4740
|
+
var REGISTER_SPAN = 24;
|
|
4741
|
+
var PEAK_POSITION_WEIGHT = 0.5;
|
|
4742
|
+
var FAMILY_SIZE_BINS = 5;
|
|
4743
|
+
var DEPTH_BINS = 4;
|
|
4744
|
+
var NAMED_CHORDS = ["Ger", "It", "Fr", "N"];
|
|
4745
|
+
var PITCH_PRESERVING = /* @__PURE__ */ new Set([
|
|
4746
|
+
"repetition",
|
|
4747
|
+
"augmentation",
|
|
4748
|
+
"diminution"
|
|
4749
|
+
]);
|
|
4750
|
+
var CELL_START_PITCH = 60;
|
|
4751
|
+
function whenBoth(hasA, hasB, measure) {
|
|
4752
|
+
if (!hasA && !hasB) {
|
|
4753
|
+
return null;
|
|
4754
|
+
}
|
|
4755
|
+
if (!hasA || !hasB) {
|
|
4756
|
+
return 0;
|
|
4757
|
+
}
|
|
4758
|
+
return measure();
|
|
4759
|
+
}
|
|
4760
|
+
function aligned(a, b, cost, gap = 1) {
|
|
4761
|
+
return distanceToSimilarity(gradedEditDistance(a, b, cost, gap), a.length, b.length);
|
|
4762
|
+
}
|
|
4763
|
+
function alignedSimilarity(a, b, cost, gap = 1) {
|
|
4764
|
+
return whenBoth(a.length > 0, b.length > 0, () => aligned(a, b, cost, gap));
|
|
4765
|
+
}
|
|
4766
|
+
function massOf(values) {
|
|
4767
|
+
let total = 0;
|
|
4768
|
+
for (const value of values) {
|
|
4769
|
+
total += value;
|
|
4770
|
+
}
|
|
4771
|
+
return total;
|
|
4772
|
+
}
|
|
4773
|
+
function intersection(a, b) {
|
|
4774
|
+
const massA = massOf(a);
|
|
4775
|
+
const massB = massOf(b);
|
|
4776
|
+
let shared2 = 0;
|
|
4777
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
4778
|
+
shared2 += Math.min((_nullishCoalesce(a[i], () => ( 0))) / massA, (_nullishCoalesce(b[i], () => ( 0))) / massB);
|
|
4779
|
+
}
|
|
4780
|
+
return Math.min(1, shared2);
|
|
4781
|
+
}
|
|
4782
|
+
function distributionSimilarity(a, b) {
|
|
4783
|
+
return whenBoth(massOf(a) > 0, massOf(b) > 0, () => intersection(a, b));
|
|
4784
|
+
}
|
|
4785
|
+
function ratio(a, b) {
|
|
4786
|
+
const larger = Math.max(a, b);
|
|
4787
|
+
return larger === 0 ? 1 : Math.min(a, b) / larger;
|
|
4788
|
+
}
|
|
4789
|
+
function sectionSequenceSimilarity(a, b) {
|
|
4790
|
+
return alignedSimilarity(a, b, (x, y) => x === y ? 0 : 1);
|
|
4791
|
+
}
|
|
4792
|
+
function phraseLengthSimilarity(a, b) {
|
|
4793
|
+
return alignedSimilarity(a, b, (x, y) => {
|
|
4794
|
+
const longer = Math.max(x, y);
|
|
4795
|
+
return longer === 0 ? 0 : Math.min(Math.abs(x - y) / longer, 1);
|
|
4796
|
+
});
|
|
4797
|
+
}
|
|
4798
|
+
function simpleDegreeToken(roman) {
|
|
4799
|
+
for (const name of NAMED_CHORDS) {
|
|
4800
|
+
if (roman.startsWith(name)) {
|
|
4801
|
+
return name;
|
|
4802
|
+
}
|
|
4803
|
+
}
|
|
4804
|
+
const match = roman.match(/^([b#]*)([IV]+)/i);
|
|
4805
|
+
return match ? `${match[1]}${match[2].toUpperCase()}` : roman;
|
|
4806
|
+
}
|
|
4807
|
+
function romanDegreeToken(roman) {
|
|
4808
|
+
const slash = roman.indexOf("/");
|
|
4809
|
+
if (slash < 0) {
|
|
4810
|
+
return simpleDegreeToken(roman);
|
|
4811
|
+
}
|
|
4812
|
+
return `${simpleDegreeToken(roman.slice(0, slash))}/${simpleDegreeToken(roman.slice(slash + 1))}`;
|
|
4813
|
+
}
|
|
4814
|
+
function chordCost(x, y) {
|
|
4815
|
+
if (x.roman === y.roman) {
|
|
4816
|
+
return 0;
|
|
4817
|
+
}
|
|
4818
|
+
if (romanDegreeToken(x.roman) === romanDegreeToken(y.roman)) {
|
|
4819
|
+
return SAME_DEGREE_COST;
|
|
4820
|
+
}
|
|
4821
|
+
return x.function === y.function ? SAME_FUNCTION_COST : 1;
|
|
4822
|
+
}
|
|
4823
|
+
function progressionSimilarity(a, b, budget) {
|
|
4824
|
+
_chunkS5BKZZGMcjs.assertGenerationBudget.call(void 0, a.length * b.length, "reference progression alignment", budget);
|
|
4825
|
+
return alignedSimilarity(a, b, chordCost, PROGRESSION_GAP);
|
|
4826
|
+
}
|
|
4827
|
+
function functionMass(chords) {
|
|
4828
|
+
const mass = HARMONIC_FUNCTIONS.map(() => 0);
|
|
4829
|
+
for (const chord of chords) {
|
|
4830
|
+
const bin = HARMONIC_FUNCTIONS.indexOf(chord.function);
|
|
4831
|
+
mass[bin] = (_nullishCoalesce(mass[bin], () => ( 0))) + Math.max(0, chord.endBeat - chord.startBeat);
|
|
4832
|
+
}
|
|
4833
|
+
return mass;
|
|
4834
|
+
}
|
|
4835
|
+
function functionSimilarity(a, b) {
|
|
4836
|
+
return distributionSimilarity(functionMass(a), functionMass(b));
|
|
4837
|
+
}
|
|
4838
|
+
function cadenceSimilarity(a, b) {
|
|
4839
|
+
return alignedSimilarity(a, b, (x, y) => {
|
|
4840
|
+
if (x === y) {
|
|
4841
|
+
return 0;
|
|
4842
|
+
}
|
|
4843
|
+
const halfAndPhrygian = x === "half" && y === "phrygian" || x === "phrygian" && y === "half";
|
|
4844
|
+
return halfAndPhrygian ? HALF_PHRYGIAN_COST : 1;
|
|
4845
|
+
});
|
|
4846
|
+
}
|
|
4847
|
+
function keyTokens(keys) {
|
|
4848
|
+
const home = _nullishCoalesce(_optionalChain([keys, 'access', _115 => _115[0], 'optionalAccess', _116 => _116.scale, 'access', _117 => _117.rootPc]), () => ( 0));
|
|
4849
|
+
return keys.map((key) => ({
|
|
4850
|
+
distance: ((key.scale.rootPc - home) % 12 + 12) % 12,
|
|
4851
|
+
mask: key.scale.modeMask12
|
|
4852
|
+
}));
|
|
4853
|
+
}
|
|
4854
|
+
function keyPlanSimilarity(a, b) {
|
|
4855
|
+
return alignedSimilarity(keyTokens(a), keyTokens(b), (x, y) => {
|
|
4856
|
+
if (x.distance !== y.distance) {
|
|
4857
|
+
return 1;
|
|
4858
|
+
}
|
|
4859
|
+
return x.mask === y.mask ? 0 : MODE_CHANGE_COST;
|
|
4860
|
+
});
|
|
4861
|
+
}
|
|
4862
|
+
function onsetSimilarity(a, b) {
|
|
4863
|
+
return whenBoth(a.onsets > 0, b.onsets > 0, () => {
|
|
4864
|
+
let weighted = 0;
|
|
4865
|
+
let weight = 0;
|
|
4866
|
+
for (const left of a.barPositions) {
|
|
4867
|
+
for (const right of b.barPositions) {
|
|
4868
|
+
const shared2 = left.ts.numerator === right.ts.numerator && left.ts.denominator === right.ts.denominator && left.onsets > 0 && right.onsets > 0;
|
|
4869
|
+
if (shared2) {
|
|
4870
|
+
const w = left.onsets + right.onsets;
|
|
4871
|
+
weighted += w * intersection(left.shares, right.shares);
|
|
4872
|
+
weight += w;
|
|
4873
|
+
}
|
|
4874
|
+
}
|
|
4875
|
+
}
|
|
4876
|
+
return weight > 0 ? weighted / weight : intersection(a.onsetLevels, b.onsetLevels);
|
|
4877
|
+
});
|
|
4878
|
+
}
|
|
4879
|
+
function densitySimilarity(a, b) {
|
|
4880
|
+
return whenBoth(a.onsets > 0, b.onsets > 0, () => ratio(a.onsetDensity, b.onsetDensity));
|
|
4881
|
+
}
|
|
4882
|
+
function durationSimilarity(a, b) {
|
|
4883
|
+
return whenBoth(
|
|
4884
|
+
a.onsets > 0,
|
|
4885
|
+
b.onsets > 0,
|
|
4886
|
+
() => intersection(a.interOnsetShares, b.interOnsetShares)
|
|
4887
|
+
);
|
|
4888
|
+
}
|
|
4889
|
+
function syncopationSimilarity(a, b) {
|
|
4890
|
+
return whenBoth(a.onsets > 0, b.onsets > 0, () => 1 - Math.abs(a.syncopation - b.syncopation));
|
|
4891
|
+
}
|
|
4892
|
+
function cellNotes(motif) {
|
|
4893
|
+
const notes = [];
|
|
4894
|
+
let low = 0;
|
|
4895
|
+
let high = 0;
|
|
4896
|
+
let offset = 0;
|
|
4897
|
+
for (const interval of motif.intervals) {
|
|
4898
|
+
offset += interval;
|
|
4899
|
+
low = Math.min(low, offset);
|
|
4900
|
+
high = Math.max(high, offset);
|
|
4901
|
+
}
|
|
4902
|
+
let pitch = Math.min(127 - high, Math.max(-low, CELL_START_PITCH));
|
|
4903
|
+
let beat = 0;
|
|
4904
|
+
for (let i = 0; i <= motif.intervals.length; i += 1) {
|
|
4905
|
+
const step = _nullishCoalesce(motif.rhythm[i], () => ( 1));
|
|
4906
|
+
notes.push({ pitch, startBeat: beat, durationBeat: i < motif.intervals.length ? step : 1 });
|
|
4907
|
+
pitch += _nullishCoalesce(motif.intervals[i], () => ( 0));
|
|
4908
|
+
beat += step;
|
|
4909
|
+
}
|
|
4910
|
+
return notes;
|
|
4911
|
+
}
|
|
4912
|
+
function bestMatchMean(from, to) {
|
|
4913
|
+
let weighted = 0;
|
|
4914
|
+
let weight = 0;
|
|
4915
|
+
for (const motif of from) {
|
|
4916
|
+
let best = 0;
|
|
4917
|
+
for (const other of to) {
|
|
4918
|
+
best = Math.max(best, compareMelodies(motif.cell, other.cell).pitchSimilarity);
|
|
4919
|
+
}
|
|
4920
|
+
weighted += motif.occurrences * best;
|
|
4921
|
+
weight += motif.occurrences;
|
|
4922
|
+
}
|
|
4923
|
+
return weighted / weight;
|
|
4924
|
+
}
|
|
4925
|
+
function surfaceSimilarity(a, b) {
|
|
4926
|
+
return whenBoth(a.length > 0, b.length > 0, () => {
|
|
4927
|
+
const left = a.map((motif) => ({ cell: cellNotes(motif), occurrences: motif.occurrences }));
|
|
4928
|
+
const right = b.map((motif) => ({ cell: cellNotes(motif), occurrences: motif.occurrences }));
|
|
4929
|
+
return (bestMatchMean(left, right) + bestMatchMean(right, left)) / 2;
|
|
4930
|
+
});
|
|
4931
|
+
}
|
|
4932
|
+
function coverageOf(graph, spanBeats) {
|
|
4933
|
+
if (!(spanBeats > 0) || graph.nodes.length === 0) {
|
|
4934
|
+
return 0;
|
|
4935
|
+
}
|
|
4936
|
+
const spans = graph.nodes.map((node) => [node.startBeat, node.endBeat]).sort((x, y) => x[0] - y[0] || x[1] - y[1]);
|
|
4937
|
+
let covered = 0;
|
|
4938
|
+
let [runStart, runEnd] = spans[0];
|
|
4939
|
+
for (const [start, end] of spans) {
|
|
4940
|
+
if (start > runEnd) {
|
|
4941
|
+
covered += runEnd - runStart;
|
|
4942
|
+
runStart = start;
|
|
4943
|
+
runEnd = end;
|
|
4944
|
+
} else {
|
|
4945
|
+
runEnd = Math.max(runEnd, end);
|
|
4946
|
+
}
|
|
4947
|
+
}
|
|
4948
|
+
covered += runEnd - runStart;
|
|
4949
|
+
return Math.min(1, covered / spanBeats);
|
|
4950
|
+
}
|
|
4951
|
+
function bump(histogram, bin) {
|
|
4952
|
+
histogram[bin] = (_nullishCoalesce(histogram[bin], () => ( 0))) + 1;
|
|
4953
|
+
}
|
|
4954
|
+
function summarise({ graph, spanBeats }) {
|
|
4955
|
+
const kinds = new Array(MOTIF_RELATION_KINDS.length + 1).fill(0);
|
|
4956
|
+
const parentEdge = /* @__PURE__ */ new Map();
|
|
4957
|
+
for (const edge of graph.edges) {
|
|
4958
|
+
const kind = _optionalChain([edge, 'access', _118 => _118.relation, 'optionalAccess', _119 => _119.kind]);
|
|
4959
|
+
bump(
|
|
4960
|
+
kinds,
|
|
4961
|
+
kind === void 0 ? MOTIF_RELATION_KINDS.length : MOTIF_RELATION_KINDS.indexOf(kind)
|
|
4962
|
+
);
|
|
4963
|
+
parentEdge.set(edge.to, edge);
|
|
4964
|
+
}
|
|
4965
|
+
const depth = new Array(graph.nodes.length).fill(0);
|
|
4966
|
+
const root = graph.nodes.map((_, i) => i);
|
|
4967
|
+
for (let node = 0; node < graph.nodes.length; node += 1) {
|
|
4968
|
+
const edge = parentEdge.get(node);
|
|
4969
|
+
if (edge !== void 0) {
|
|
4970
|
+
const kind = _optionalChain([edge, 'access', _120 => _120.relation, 'optionalAccess', _121 => _121.kind]);
|
|
4971
|
+
const keepsPitch = kind !== void 0 && PITCH_PRESERVING.has(kind);
|
|
4972
|
+
depth[node] = (_nullishCoalesce(depth[edge.from], () => ( 0))) + (keepsPitch ? 0 : 1);
|
|
4973
|
+
root[node] = _nullishCoalesce(root[edge.from], () => ( edge.from));
|
|
4974
|
+
}
|
|
4975
|
+
}
|
|
4976
|
+
const familySize = /* @__PURE__ */ new Map();
|
|
4977
|
+
for (const r of root) {
|
|
4978
|
+
familySize.set(r, (_nullishCoalesce(familySize.get(r), () => ( 0))) + 1);
|
|
4979
|
+
}
|
|
4980
|
+
const families = new Array(FAMILY_SIZE_BINS).fill(0);
|
|
4981
|
+
for (const size of familySize.values()) {
|
|
4982
|
+
bump(families, Math.min(size, FAMILY_SIZE_BINS) - 1);
|
|
4983
|
+
}
|
|
4984
|
+
const depths = new Array(DEPTH_BINS).fill(0);
|
|
4985
|
+
for (const d of depth) {
|
|
4986
|
+
bump(depths, Math.min(d, DEPTH_BINS - 1));
|
|
4987
|
+
}
|
|
4988
|
+
return { kinds, families, depths, coverage: coverageOf(graph, spanBeats) };
|
|
4989
|
+
}
|
|
4990
|
+
function motifStructureSimilarity(a, b) {
|
|
4991
|
+
const left = summarise(a);
|
|
4992
|
+
const right = summarise(b);
|
|
4993
|
+
const terms = [
|
|
4994
|
+
distributionSimilarity(left.kinds, right.kinds),
|
|
4995
|
+
distributionSimilarity(left.families, right.families),
|
|
4996
|
+
distributionSimilarity(left.depths, right.depths),
|
|
4997
|
+
whenBoth(
|
|
4998
|
+
left.coverage > 0,
|
|
4999
|
+
right.coverage > 0,
|
|
5000
|
+
() => 1 - Math.abs(left.coverage - right.coverage)
|
|
5001
|
+
)
|
|
5002
|
+
].filter((term) => term !== null);
|
|
5003
|
+
return terms.length === 0 ? null : massOf(terms) / terms.length;
|
|
5004
|
+
}
|
|
5005
|
+
function nullableCost(cost) {
|
|
5006
|
+
return (x, y) => {
|
|
5007
|
+
if (x === null || y === null) {
|
|
5008
|
+
return x === y ? 0 : 1;
|
|
5009
|
+
}
|
|
5010
|
+
return cost(x, y);
|
|
5011
|
+
};
|
|
5012
|
+
}
|
|
5013
|
+
function phraseSimilarity(a, b, cost) {
|
|
5014
|
+
const hasA = a.some((phrase) => phrase !== null);
|
|
5015
|
+
const hasB = b.some((phrase) => phrase !== null);
|
|
5016
|
+
return whenBoth(hasA, hasB, () => aligned(a, b, nullableCost(cost)));
|
|
5017
|
+
}
|
|
5018
|
+
function normalisedOutline(phrase) {
|
|
5019
|
+
const mean = massOf(phrase.outline) / (phrase.outline.length || 1);
|
|
5020
|
+
const range = phrase.high - phrase.low || 1;
|
|
5021
|
+
return phrase.outline.map((point) => (point - mean) / range);
|
|
5022
|
+
}
|
|
5023
|
+
function contourCost(x, y) {
|
|
5024
|
+
const left = normalisedOutline(x);
|
|
5025
|
+
const right = normalisedOutline(y);
|
|
5026
|
+
let difference = 0;
|
|
5027
|
+
for (let i = 0; i < left.length; i += 1) {
|
|
5028
|
+
difference += Math.abs((_nullishCoalesce(left[i], () => ( 0))) - (_nullishCoalesce(right[i], () => ( 0))));
|
|
5029
|
+
}
|
|
5030
|
+
const outlineCost = Math.min(1, difference / (left.length || 1));
|
|
5031
|
+
return Math.min(1, outlineCost + (x.shape === y.shape ? 0 : SHAPE_MISMATCH_COST));
|
|
5032
|
+
}
|
|
5033
|
+
function contourSimilarity(a, b) {
|
|
5034
|
+
return phraseSimilarity(a, b, contourCost);
|
|
5035
|
+
}
|
|
5036
|
+
function registerSimilarity(a, b) {
|
|
5037
|
+
const meanA = _nullishCoalesce(a.mean, () => ( 0));
|
|
5038
|
+
const meanB = _nullishCoalesce(b.mean, () => ( 0));
|
|
5039
|
+
return phraseSimilarity(a.phrases, b.phrases, (x, y) => {
|
|
5040
|
+
const offset = Math.abs(x.mean - meanA - (y.mean - meanB));
|
|
5041
|
+
const range = Math.abs(x.high - x.low - (y.high - y.low));
|
|
5042
|
+
const peak = Math.abs(x.peakPosition - y.peakPosition);
|
|
5043
|
+
return Math.min(1, (offset + range) / REGISTER_SPAN + PEAK_POSITION_WEIGHT * peak);
|
|
5044
|
+
});
|
|
5045
|
+
}
|
|
5046
|
+
|
|
5047
|
+
// src/analyze/reference/internal.ts
|
|
5048
|
+
function weightedMean(notes) {
|
|
5049
|
+
let weighted = 0;
|
|
5050
|
+
let total = 0;
|
|
5051
|
+
for (const note of notes) {
|
|
5052
|
+
weighted += note.pitch * note.durationBeat;
|
|
5053
|
+
total += note.durationBeat;
|
|
5054
|
+
}
|
|
5055
|
+
return total > 0 ? weighted / total : 0;
|
|
5056
|
+
}
|
|
5057
|
+
function phraseMelody(line, startBeat, endBeat, lineMean, meters, budget) {
|
|
5058
|
+
const inside = line.filter((note) => note.startBeat >= startBeat - _chunkS5BKZZGMcjs.BEAT_EPS && note.startBeat < endBeat - _chunkS5BKZZGMcjs.BEAT_EPS).map((note) => ({
|
|
5059
|
+
...note,
|
|
5060
|
+
durationBeat: Math.min(note.startBeat + note.durationBeat, endBeat) - note.startBeat
|
|
5061
|
+
}));
|
|
5062
|
+
const first = inside[0];
|
|
5063
|
+
if (first === void 0 || inside.length < 2) {
|
|
5064
|
+
return null;
|
|
5065
|
+
}
|
|
5066
|
+
let low = first.pitch;
|
|
5067
|
+
let peak = first;
|
|
5068
|
+
for (const note of inside) {
|
|
5069
|
+
low = Math.min(low, note.pitch);
|
|
5070
|
+
if (note.pitch > peak.pitch) {
|
|
5071
|
+
peak = note;
|
|
5072
|
+
}
|
|
5073
|
+
}
|
|
5074
|
+
const length = endBeat - startBeat;
|
|
5075
|
+
const outline = [];
|
|
5076
|
+
for (let k = 0; k < 8; k += 1) {
|
|
5077
|
+
const at = startBeat + (k + 0.5) * length / 8;
|
|
5078
|
+
let held = first;
|
|
5079
|
+
for (const note of inside) {
|
|
5080
|
+
if (note.startBeat > at + _chunkS5BKZZGMcjs.BEAT_EPS) {
|
|
5081
|
+
break;
|
|
5082
|
+
}
|
|
5083
|
+
held = note;
|
|
5084
|
+
}
|
|
5085
|
+
outline.push(held.pitch - lineMean);
|
|
5086
|
+
}
|
|
5087
|
+
const rhythm = analyzeRhythm(inside, { meters, totalBeats: endBeat, budget });
|
|
5088
|
+
return {
|
|
5089
|
+
shape: melodicContour(inside).shape,
|
|
5090
|
+
low,
|
|
5091
|
+
high: peak.pitch,
|
|
5092
|
+
mean: weightedMean(inside),
|
|
5093
|
+
peakPitch: peak.pitch,
|
|
5094
|
+
peakPosition: Math.min(1, Math.max(0, (peak.startBeat - startBeat) / length)),
|
|
5095
|
+
outline,
|
|
5096
|
+
onsetDensity: rhythm.onsetDensity,
|
|
5097
|
+
restRatio: rhythm.restRatio
|
|
5098
|
+
};
|
|
5099
|
+
}
|
|
5100
|
+
|
|
5101
|
+
|
|
5102
|
+
|
|
5103
|
+
|
|
5104
|
+
|
|
5105
|
+
|
|
5106
|
+
|
|
5107
|
+
|
|
5108
|
+
|
|
5109
|
+
|
|
5110
|
+
|
|
5111
|
+
|
|
5112
|
+
|
|
5113
|
+
|
|
5114
|
+
|
|
5115
|
+
|
|
5116
|
+
|
|
5117
|
+
|
|
5118
|
+
|
|
5119
|
+
|
|
5120
|
+
|
|
5121
|
+
|
|
5122
|
+
|
|
5123
|
+
|
|
5124
|
+
|
|
5125
|
+
|
|
5126
|
+
|
|
5127
|
+
|
|
5128
|
+
|
|
5129
|
+
|
|
5130
|
+
|
|
5131
|
+
|
|
5132
|
+
|
|
5133
|
+
|
|
5134
|
+
|
|
5135
|
+
|
|
3270
5136
|
|
|
3271
5137
|
|
|
3272
5138
|
|
|
@@ -3325,5 +5191,5 @@ function detectCadences(timeline, key) {
|
|
|
3325
5191
|
|
|
3326
5192
|
|
|
3327
5193
|
|
|
3328
|
-
exports.HUMANIZE_ADJACENCY = HUMANIZE_ADJACENCY; exports.sameInstant = sameInstant; exports.adjacent = adjacent; exports.hasEnded = hasEnded; exports.windowWeights = windowWeights; exports.MODAL_SCALE_NAMES = MODAL_SCALE_NAMES; exports.detectChord = detectChord; exports.detectChordBest = detectChordBest; exports.detectKey = detectKey; exports.detectKeyFromNotes = detectKeyFromNotes; exports.detectKeyBest = detectKeyBest; exports.augmentedSixthChord = augmentedSixthChord; exports.augmentedSixthKind = augmentedSixthKind; exports.augmentedSixthFromPitchClasses = augmentedSixthFromPitchClasses; exports.spellAugmentedSixth = spellAugmentedSixth; exports.
|
|
3329
|
-
//# sourceMappingURL=chunk-
|
|
5194
|
+
exports.HUMANIZE_ADJACENCY = HUMANIZE_ADJACENCY; exports.sameInstant = sameInstant; exports.adjacent = adjacent; exports.hasEnded = hasEnded; exports.windowWeights = windowWeights; exports.MODAL_SCALE_NAMES = MODAL_SCALE_NAMES; exports.detectChord = detectChord; exports.detectChordBest = detectChordBest; exports.detectKey = detectKey; exports.detectKeyFromNotes = detectKeyFromNotes; exports.detectKeyBest = detectKeyBest; exports.augmentedSixthChord = augmentedSixthChord; exports.augmentedSixthKind = augmentedSixthKind; exports.augmentedSixthFromPitchClasses = augmentedSixthFromPitchClasses; exports.spellAugmentedSixth = spellAugmentedSixth; exports.CADENCE_TYPES = CADENCE_TYPES; exports.detectCadence = detectCadence; exports.isBorrowedChord = isBorrowedChord; exports.borrowedSource = borrowedSource; exports.romanToChord = romanToChord; exports.chordToRoman = chordToRoman; exports.explainRoman = explainRoman; exports.HARMONIC_FUNCTIONS = HARMONIC_FUNCTIONS; exports.isMinorKey = isMinorKey; exports.functionOf = functionOf; exports.isDiatonic = isDiatonic; exports.parallelKey = parallelKey; exports.analyzeChord = analyzeChord; exports.secondaryDominant = secondaryDominant; exports.secondaryDominantOf = secondaryDominantOf; exports.pivotChords = pivotChords; exports.barSpanOf = barSpanOf; exports.clamp01 = clamp01; exports.floorMod = floorMod; exports.firstSoundingBeat = firstSoundingBeat; exports.lastBarOf = lastBarOf; exports.sliceBars = sliceBars; exports.weightSimilarity = weightSimilarity; exports.harmonicNovelty = harmonicNovelty; exports.combineEvidence = combineEvidence; exports.gridForNotes = gridForNotes; exports.barGridStart = barGridStart; exports.keyTimelineFromNotes = keyTimelineFromNotes; exports.detectModulations = detectModulations; exports.attachPivots = attachPivots; exports.keyLookup = keyLookup; exports.prevailingKeyOf = prevailingKeyOf; exports.toVoiceNotes = toVoiceNotes; exports.analyzeVoice = analyzeVoice; exports.assertChordTimeline = assertChordTimeline; exports.chordTimelineFromChords = chordTimelineFromChords; exports.chordAtBeat = chordAtBeat; exports.segmentStartingAt = segmentStartingAt; exports.chordTimelineFromNotes = chordTimelineFromNotes; exports.dirtySlotsFor = dirtySlotsFor; exports.analyzeTimeline = analyzeTimeline; exports.detectCadences = detectCadences; exports.orderedNotes = orderedNotes; exports.MELODIC_CONTOUR_SHAPES = MELODIC_CONTOUR_SHAPES; exports.melodicContour = melodicContour; exports.extractMotifs = extractMotifs; exports.motifFromNotes = motifFromNotes; exports.MOTIF_RELATION_KINDS = MOTIF_RELATION_KINDS; exports.relateMotifs = relateMotifs; exports.compareMelodies = compareMelodies; exports.melodicSimilarity = melodicSimilarity; exports.DEFAULT_VARIATION_THRESHOLD = DEFAULT_VARIATION_THRESHOLD; exports.motifGraph = motifGraph; exports.REDUCTION_LEVELS = REDUCTION_LEVELS; exports.reduceProgression = reduceProgression; exports.sectionSequenceSimilarity = sectionSequenceSimilarity; exports.phraseLengthSimilarity = phraseLengthSimilarity; exports.progressionSimilarity = progressionSimilarity; exports.functionSimilarity = functionSimilarity; exports.cadenceSimilarity = cadenceSimilarity; exports.keyPlanSimilarity = keyPlanSimilarity; exports.onsetSimilarity = onsetSimilarity; exports.densitySimilarity = densitySimilarity; exports.durationSimilarity = durationSimilarity; exports.syncopationSimilarity = syncopationSimilarity; exports.surfaceSimilarity = surfaceSimilarity; exports.motifStructureSimilarity = motifStructureSimilarity; exports.contourSimilarity = contourSimilarity; exports.registerSimilarity = registerSimilarity; exports.rhythmLevel = rhythmLevel; exports.ioiBinIndex = ioiBinIndex; exports.RHYTHM_IOI_BINS = RHYTHM_IOI_BINS; exports.analyzeRhythm = analyzeRhythm; exports.REFERENCE_PROFILE_VERSION = REFERENCE_PROFILE_VERSION; exports.assertReferenceProfile = assertReferenceProfile; exports.weightedMean = weightedMean; exports.phraseMelody = phraseMelody;
|
|
5195
|
+
//# sourceMappingURL=chunk-IFZDWIKM.cjs.map
|