@libraz/libcantus 0.9.2 → 0.9.3
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 -3
- package/dist/analyze/index.cjs +315 -42
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +11 -99
- package/dist/analyze/index.d.ts +11 -99
- package/dist/analyze/index.js +6 -6
- package/dist/chunk-2ZWMSJH6.js +30 -0
- package/dist/chunk-2ZWMSJH6.js.map +1 -0
- package/dist/{chunk-R3S2LUBH.js → chunk-4MHA432G.js} +316 -184
- package/dist/chunk-4MHA432G.js.map +1 -0
- package/dist/chunk-62QZKCJQ.js +49 -0
- package/dist/chunk-62QZKCJQ.js.map +1 -0
- package/dist/chunk-AKLYPWIN.js +83 -0
- package/dist/chunk-AKLYPWIN.js.map +1 -0
- package/dist/{chunk-XTC3X6MS.js → chunk-C5XYKUVG.js} +100 -12
- package/dist/chunk-C5XYKUVG.js.map +1 -0
- package/dist/{chunk-LN7ZZMD2.js → chunk-CHMXELDS.js} +33 -14
- package/dist/chunk-CHMXELDS.js.map +1 -0
- package/dist/{chunk-A4532ZFR.js → chunk-EH6EM37U.js} +60 -4
- package/dist/chunk-EH6EM37U.js.map +1 -0
- package/dist/chunk-FBQK3ZQN.js +131 -0
- package/dist/chunk-FBQK3ZQN.js.map +1 -0
- package/dist/{chunk-5HTNMQW4.js → chunk-GOAW4VYL.js} +87 -21
- package/dist/chunk-GOAW4VYL.js.map +1 -0
- package/dist/{chunk-MSWFZF7J.js → chunk-I5R7ZBJS.js} +90 -6
- package/dist/chunk-I5R7ZBJS.js.map +1 -0
- package/dist/{chunk-GQKN5KDT.js → chunk-IT2O4XFE.js} +4 -9
- package/dist/chunk-IT2O4XFE.js.map +1 -0
- package/dist/{chunk-7Y7WF64D.js → chunk-MIWO425M.js} +6 -4
- package/dist/chunk-MIWO425M.js.map +1 -0
- package/dist/chunk-N6IHQUOX.js +1 -0
- package/dist/chunk-N6IHQUOX.js.map +1 -0
- package/dist/{chunk-COKTX2G5.js → chunk-SGU7SPLK.js} +29 -12
- package/dist/chunk-SGU7SPLK.js.map +1 -0
- package/dist/core/index.cjs +230 -14
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +56 -4
- package/dist/core/index.d.ts +56 -4
- package/dist/core/index.js +24 -4
- package/dist/{function-2bJW1Zp_.d.ts → function-DVH4Eusr.d.ts} +1 -1
- package/dist/{function-r2sh-jOO.d.cts → function-DYt4iKsn.d.cts} +1 -1
- package/dist/generate/index.cjs +830 -494
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +11 -5
- package/dist/generate/index.d.ts +11 -5
- package/dist/generate/index.js +9 -9
- package/dist/{index-BOBXBNwP.d.cts → index-BNlGP86A.d.cts} +1 -1
- package/dist/{index-CP3wAfEH.d.ts → index-Bg5dYMmZ.d.ts} +1 -1
- package/dist/{index-CM5W4rcm.d.ts → index-Bt4tOUD9.d.ts} +1 -1
- package/dist/index-CnZCo29u.d.cts +108 -0
- package/dist/{index-D_ZTUNKp.d.cts → index-CpOsP1y9.d.cts} +2 -2
- package/dist/{index-DZ1M21do.d.cts → index-CxqBQtH1.d.cts} +2 -1
- package/dist/{index-BdeQ1r_p.d.cts → index-Dikst44v.d.cts} +1 -1
- package/dist/{index-BMPxwjwV.d.ts → index-DxvARIP2.d.ts} +2 -2
- package/dist/{index-BreoJRw0.d.ts → index-Jc0Ryi79.d.ts} +2 -1
- package/dist/index-JnF4v9bx.d.ts +108 -0
- package/dist/index.cjs +818 -283
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -8
- package/dist/index.d.ts +9 -8
- package/dist/index.js +34 -13
- package/dist/model/index.cjs +170 -31
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +6 -5
- package/dist/model/index.d.ts +6 -5
- package/dist/model/index.js +6 -6
- package/dist/{styled-D7BAx3S3.d.ts → styled-DZ7GjShb.d.ts} +1 -1
- package/dist/{styled-BjXtOFNk.d.cts → styled-ZTbdaVFM.d.cts} +1 -1
- package/dist/theory/index.cjs +180 -34
- package/dist/theory/index.cjs.map +1 -1
- package/dist/theory/index.d.cts +6 -6
- package/dist/theory/index.d.ts +6 -6
- package/dist/theory/index.js +6 -5
- package/package.json +50 -19
- package/src/analyze/arrange/internal.ts +3 -1
- package/src/analyze/arrange/tension.ts +15 -3
- package/src/analyze/arrange/tracks.ts +36 -3
- package/src/analyze/detect/index.ts +46 -9
- package/src/analyze/functional/roman.ts +14 -5
- package/src/analyze/index.ts +1 -1
- package/src/analyze/timeline/index.ts +43 -7
- package/src/analyze/voice/index.ts +11 -2
- package/src/core/event-index/index.ts +104 -0
- package/src/core/index.ts +14 -0
- package/src/core/meter/index.ts +24 -4
- package/src/core/random/index.ts +26 -3
- package/src/core/tuning/index.ts +36 -6
- package/src/core/validation/index.ts +119 -0
- package/src/generate/bass/index.ts +23 -0
- package/src/generate/countermelody/index.ts +167 -76
- package/src/generate/drums/euclid.ts +10 -3
- package/src/generate/drums/index.ts +56 -24
- package/src/generate/drums/percussion.ts +1 -1
- package/src/generate/groove/index.ts +25 -0
- package/src/generate/harmonize/index.ts +55 -44
- package/src/generate/index.ts +0 -1
- package/src/generate/motif/index.ts +45 -21
- package/src/generate/progression/index.ts +8 -16
- package/src/generate/reharmony/index.ts +1 -8
- package/src/generate/rhythm/index.ts +11 -1
- package/src/index.ts +13 -247
- package/src/model/chord.ts +29 -10
- package/src/theory/chord/index.ts +4 -2
- package/src/theory/harmony/index.ts +6 -21
- package/src/theory/index.ts +6 -1
- package/src/theory/safety/index.ts +71 -2
- package/src/theory/spelling/index.ts +70 -3
- package/src/theory/voicing/satb.ts +25 -4
- package/src/theory/voicing/styled.ts +26 -3
- package/dist/chunk-5HTNMQW4.js.map +0 -1
- package/dist/chunk-7Y7WF64D.js.map +0 -1
- package/dist/chunk-A4532ZFR.js.map +0 -1
- package/dist/chunk-BHUT2S5G.js +0 -22
- package/dist/chunk-BHUT2S5G.js.map +0 -1
- package/dist/chunk-CCBLVAPK.js +0 -42
- package/dist/chunk-CCBLVAPK.js.map +0 -1
- package/dist/chunk-COKTX2G5.js.map +0 -1
- package/dist/chunk-GQKN5KDT.js.map +0 -1
- package/dist/chunk-LN7ZZMD2.js.map +0 -1
- package/dist/chunk-MSWFZF7J.js.map +0 -1
- package/dist/chunk-R3S2LUBH.js.map +0 -1
- package/dist/chunk-SDM3VJJ5.js +0 -41
- package/dist/chunk-SDM3VJJ5.js.map +0 -1
- package/dist/chunk-XTC3X6MS.js.map +0 -1
- package/dist/chunk-ZXW5YIWK.js +0 -55
- package/dist/chunk-ZXW5YIWK.js.map +0 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ dependencies.
|
|
|
10
10
|
[](https://codecov.io/gh/libraz/libcantus)
|
|
11
11
|
[](https://github.com/libraz/libcantus/blob/main/LICENSE)
|
|
12
12
|
[](https://www.typescriptlang.org/)
|
|
13
|
-
[](https://nodejs.org/)
|
|
14
14
|
[](https://libraz.github.io/libcantus/)
|
|
15
15
|
|
|
16
16
|
## What can you do with it?
|
|
@@ -245,11 +245,12 @@ Seeded, deterministic generators for melody and accompaniment:
|
|
|
245
245
|
```ts
|
|
246
246
|
import {
|
|
247
247
|
generateBassLine, generateCounterMelody, generateDrums, generateRhythm,
|
|
248
|
-
generateMotif, parseTimeSignature,
|
|
248
|
+
generateMotif, majorKey, parseTimeSignature,
|
|
249
249
|
} from '@libraz/libcantus';
|
|
250
250
|
|
|
251
251
|
generateRhythm(parseTimeSignature('4/4'), { seed: 1, density: 0.5 }); // strong-beat-weighted onsets
|
|
252
|
-
generateMotif({ key: majorKey(0),
|
|
252
|
+
const motif = generateMotif({ key: majorKey(0), bars: 2, contour: 'arch', seed: 1 });
|
|
253
|
+
motif.notes.length > 0; // true — a deterministic short melodic cell
|
|
253
254
|
```
|
|
254
255
|
|
|
255
256
|
`humanize`, `extractGrooveTemplate`, and `applyGrooveTemplate` add a meter-aware
|
package/dist/analyze/index.cjs
CHANGED
|
@@ -43,6 +43,86 @@ __export(analyze_exports, {
|
|
|
43
43
|
});
|
|
44
44
|
module.exports = __toCommonJS(analyze_exports);
|
|
45
45
|
|
|
46
|
+
// src/core/validation/index.ts
|
|
47
|
+
var DEFAULT_GENERATION_BUDGET = 1e6;
|
|
48
|
+
function assertFiniteNumber(value, name) {
|
|
49
|
+
if (!Number.isFinite(value)) {
|
|
50
|
+
throw new RangeError(`${name} must be finite; received ${value}`);
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
function assertInteger(value, name, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) {
|
|
55
|
+
assertFiniteNumber(value, name);
|
|
56
|
+
if (!Number.isSafeInteger(value) || value < min || value > max) {
|
|
57
|
+
throw new RangeError(`${name} must be an integer in [${min}, ${max}]; received ${value}`);
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
function assertPositiveInt(value, name, max = DEFAULT_GENERATION_BUDGET) {
|
|
62
|
+
return assertInteger(value, name, 1, max);
|
|
63
|
+
}
|
|
64
|
+
function assertRange(value, min, max, name) {
|
|
65
|
+
assertFiniteNumber(value, name);
|
|
66
|
+
if (value < min || value > max) {
|
|
67
|
+
throw new RangeError(`${name} must be in [${min}, ${max}]; received ${value}`);
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
function assertGenerationBudget(estimated, name, limit = DEFAULT_GENERATION_BUDGET) {
|
|
72
|
+
assertFiniteNumber(estimated, name);
|
|
73
|
+
assertPositiveInt(limit, `${name} limit`, Number.MAX_SAFE_INTEGER);
|
|
74
|
+
if (estimated < 0 || estimated > limit) {
|
|
75
|
+
throw new RangeError(`${name} exceeds the generation budget ${limit}; received ${estimated}`);
|
|
76
|
+
}
|
|
77
|
+
return estimated;
|
|
78
|
+
}
|
|
79
|
+
function assertTimeSignature(ts, name = "time signature") {
|
|
80
|
+
assertPositiveInt(ts.numerator, `${name}.numerator`);
|
|
81
|
+
assertPositiveInt(ts.denominator, `${name}.denominator`);
|
|
82
|
+
if (ts.grouping !== void 0) {
|
|
83
|
+
if (ts.grouping.length === 0) {
|
|
84
|
+
throw new RangeError(`${name}.grouping must not be empty`);
|
|
85
|
+
}
|
|
86
|
+
let sum = 0;
|
|
87
|
+
for (let index = 0; index < ts.grouping.length; index += 1) {
|
|
88
|
+
sum += assertPositiveInt(ts.grouping[index] ?? Number.NaN, `${name}.grouping[${index}]`);
|
|
89
|
+
}
|
|
90
|
+
const compound = ts.numerator % 3 === 0 && ts.numerator > 3;
|
|
91
|
+
const pulses = compound ? ts.numerator / 3 : ts.numerator;
|
|
92
|
+
if (sum !== pulses) {
|
|
93
|
+
throw new RangeError(`${name}.grouping must sum to ${pulses}; received ${sum}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return ts;
|
|
97
|
+
}
|
|
98
|
+
function assertNoteEvent(event, name = "note event", options = {}) {
|
|
99
|
+
assertFiniteNumber(event.pitch, `${name}.pitch`);
|
|
100
|
+
assertRange(event.startBeat, 0, Number.MAX_SAFE_INTEGER, `${name}.startBeat`);
|
|
101
|
+
assertRange(
|
|
102
|
+
event.durationBeat,
|
|
103
|
+
options.allowNonPositiveDuration ? -Number.MAX_SAFE_INTEGER : Number.MIN_VALUE,
|
|
104
|
+
Number.MAX_SAFE_INTEGER,
|
|
105
|
+
`${name}.durationBeat`
|
|
106
|
+
);
|
|
107
|
+
if (!options.allowNonPositiveDuration && event.durationBeat <= 0) {
|
|
108
|
+
throw new RangeError(`${name}.durationBeat must be positive; received ${event.durationBeat}`);
|
|
109
|
+
}
|
|
110
|
+
if (event.velocity !== void 0) {
|
|
111
|
+
assertRange(event.velocity, 0, 127, `${name}.velocity`);
|
|
112
|
+
}
|
|
113
|
+
return event;
|
|
114
|
+
}
|
|
115
|
+
function assertNoteEvents(events, name = "note events", options = {}) {
|
|
116
|
+
assertGenerationBudget(events.length, `${name} count`, options.budget);
|
|
117
|
+
for (let index = 0; index < events.length; index += 1) {
|
|
118
|
+
const event = events[index];
|
|
119
|
+
if (event !== void 0) {
|
|
120
|
+
assertNoteEvent(event, `${name}[${index}]`, options);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return events;
|
|
124
|
+
}
|
|
125
|
+
|
|
46
126
|
// src/core/meter/index.ts
|
|
47
127
|
var EPS = 1e-9;
|
|
48
128
|
function isMultiple(value, unit) {
|
|
@@ -59,21 +139,24 @@ function parseTimeSignature(text) {
|
|
|
59
139
|
}
|
|
60
140
|
const numerator = Number.parseInt(match[1] ?? "", 10);
|
|
61
141
|
const denominator = Number.parseInt(match[2] ?? "", 10);
|
|
62
|
-
if (numerator <= 0 || denominator <= 0) {
|
|
142
|
+
if (!Number.isSafeInteger(numerator) || !Number.isSafeInteger(denominator) || numerator <= 0 || denominator <= 0) {
|
|
63
143
|
throw new Error(`Invalid time signature: ${text}`);
|
|
64
144
|
}
|
|
65
145
|
return { numerator, denominator };
|
|
66
146
|
}
|
|
67
147
|
function formatTimeSignature(ts) {
|
|
148
|
+
assertTimeSignature(ts);
|
|
68
149
|
return `${ts.numerator}/${ts.denominator}`;
|
|
69
150
|
}
|
|
70
151
|
function isCompound(ts) {
|
|
152
|
+
assertTimeSignature(ts);
|
|
71
153
|
return ts.numerator % 3 === 0 && ts.numerator > 3;
|
|
72
154
|
}
|
|
73
155
|
function unitBeats(ts) {
|
|
74
156
|
return 4 / ts.denominator;
|
|
75
157
|
}
|
|
76
158
|
function beatsPerBar(ts) {
|
|
159
|
+
assertTimeSignature(ts);
|
|
77
160
|
return ts.numerator * unitBeats(ts);
|
|
78
161
|
}
|
|
79
162
|
function pulseBeats(ts) {
|
|
@@ -106,11 +189,14 @@ function isGroupHead(grouping, pulseIndex) {
|
|
|
106
189
|
return false;
|
|
107
190
|
}
|
|
108
191
|
function beatToBarPosition(beatInQuarters, ts) {
|
|
192
|
+
assertFiniteNumber(beatInQuarters, "beat");
|
|
109
193
|
const barLen = beatsPerBar(ts);
|
|
110
194
|
const bar = Math.floor(beatInQuarters / barLen);
|
|
111
195
|
return { bar, beat: beatInQuarters - bar * barLen };
|
|
112
196
|
}
|
|
113
197
|
function metricWeight(beatInQuarters, ts) {
|
|
198
|
+
assertFiniteNumber(beatInQuarters, "beat");
|
|
199
|
+
assertTimeSignature(ts);
|
|
114
200
|
const { beat } = beatToBarPosition(beatInQuarters, ts);
|
|
115
201
|
const pulse = pulseBeats(ts);
|
|
116
202
|
if (!isMultiple(beat, pulse)) {
|
|
@@ -359,6 +445,31 @@ function isLeadingToneResolution(prev, cur, key) {
|
|
|
359
445
|
// src/theory/safety/index.ts
|
|
360
446
|
var SUGGESTION_WINDOW = 12;
|
|
361
447
|
var MAX_SUGGESTIONS = 3;
|
|
448
|
+
var REASON_SEVERITY = [
|
|
449
|
+
[2 /* Tension */, 1 /* Warning */, 1 /* Warning */],
|
|
450
|
+
[4 /* AvoidNote */, 1 /* Warning */, 2 /* Dissonant */],
|
|
451
|
+
[8 /* ScaleTone */, 1 /* Warning */, 1 /* Warning */],
|
|
452
|
+
[16 /* NonScale */, 2 /* Dissonant */, 2 /* Dissonant */],
|
|
453
|
+
[32 /* OutOfRange */, 1 /* Warning */, 1 /* Warning */],
|
|
454
|
+
[64 /* Tritone */, 2 /* Dissonant */, 2 /* Dissonant */],
|
|
455
|
+
[128 /* LargeLeap */, 1 /* Warning */, 2 /* Dissonant */],
|
|
456
|
+
[1024 /* VerticalDissonance */, 2 /* Dissonant */, 2 /* Dissonant */],
|
|
457
|
+
[2048 /* ParallelPerfect */, 1 /* Warning */, 2 /* Dissonant */],
|
|
458
|
+
[4096 /* HiddenParallel */, 1 /* Warning */, 2 /* Dissonant */],
|
|
459
|
+
[8192 /* VoiceCrossing */, 1 /* Warning */, 2 /* Dissonant */],
|
|
460
|
+
[16384 /* Suspension */, 2 /* Dissonant */, 2 /* Dissonant */],
|
|
461
|
+
[32768 /* NeedsResolution */, 1 /* Warning */, 2 /* Dissonant */]
|
|
462
|
+
];
|
|
463
|
+
function minimumSafetyForReasons(reasons, profile) {
|
|
464
|
+
let minimum = 0 /* Safe */;
|
|
465
|
+
const severityIndex = profile === "strict" ? 2 : 1;
|
|
466
|
+
for (const entry of REASON_SEVERITY) {
|
|
467
|
+
if (reasons & entry[0]) {
|
|
468
|
+
minimum = Math.max(minimum, entry[severityIndex]);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return minimum;
|
|
472
|
+
}
|
|
362
473
|
function pitchClass4(pitch) {
|
|
363
474
|
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
364
475
|
}
|
|
@@ -383,8 +494,27 @@ function stepResolution(pitch, chord) {
|
|
|
383
494
|
return void 0;
|
|
384
495
|
}
|
|
385
496
|
function evaluateSafety(q) {
|
|
497
|
+
assertSafetyContext(q);
|
|
498
|
+
assertFiniteNumber(q.candidatePitch, "candidatePitch");
|
|
386
499
|
return evaluateInternal(q, true);
|
|
387
500
|
}
|
|
501
|
+
function assertSafetyContext(q) {
|
|
502
|
+
if (q.prevPitch !== void 0) assertFiniteNumber(q.prevPitch, "prevPitch");
|
|
503
|
+
if (q.vocalLow !== void 0) assertFiniteNumber(q.vocalLow, "vocalLow");
|
|
504
|
+
if (q.vocalHigh !== void 0) assertFiniteNumber(q.vocalHigh, "vocalHigh");
|
|
505
|
+
if (q.vocalLow !== void 0 && q.vocalHigh !== void 0 && q.vocalLow > q.vocalHigh) {
|
|
506
|
+
throw new RangeError("vocalLow must not exceed vocalHigh");
|
|
507
|
+
}
|
|
508
|
+
assertGenerationBudget(q.otherVoices.length, "other voices");
|
|
509
|
+
for (let index = 0; index < q.otherVoices.length; index += 1) {
|
|
510
|
+
const voice = q.otherVoices[index];
|
|
511
|
+
if (!voice) continue;
|
|
512
|
+
assertFiniteNumber(voice.pitch, `otherVoices[${index}].pitch`);
|
|
513
|
+
if (voice.prevPitch !== void 0) {
|
|
514
|
+
assertFiniteNumber(voice.prevPitch, `otherVoices[${index}].prevPitch`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
388
518
|
function evaluateInternal(q, collectSuggestions) {
|
|
389
519
|
let reasons = 0;
|
|
390
520
|
let level = 0 /* Safe */;
|
|
@@ -445,9 +575,11 @@ function evaluateInternal(q, collectSuggestions) {
|
|
|
445
575
|
const pc = Math.abs(pitch - q.prevPitch) % 12;
|
|
446
576
|
if (pc === 6) {
|
|
447
577
|
reasons |= 64 /* Tritone */;
|
|
578
|
+
raise(2 /* Dissonant */);
|
|
448
579
|
}
|
|
449
580
|
if (isForbiddenMelodicLeap(q.prevPitch, pitch)) {
|
|
450
581
|
reasons |= 128 /* LargeLeap */;
|
|
582
|
+
raise(parallelSeverity);
|
|
451
583
|
}
|
|
452
584
|
if (pc === 1) {
|
|
453
585
|
reasons |= 256 /* MinorSecond */;
|
|
@@ -492,6 +624,7 @@ function evaluateInternal(q, collectSuggestions) {
|
|
|
492
624
|
}
|
|
493
625
|
}
|
|
494
626
|
}
|
|
627
|
+
raise(minimumSafetyForReasons(reasons, q.profile));
|
|
495
628
|
const result = { safety: level, reasons, rationale: describe(reasons, q) };
|
|
496
629
|
if (resolveTo !== void 0) {
|
|
497
630
|
result.resolveTo = resolveTo;
|
|
@@ -684,11 +817,16 @@ function parseSimpleRoman(text, key) {
|
|
|
684
817
|
if (canonical !== void 0) {
|
|
685
818
|
return { rootPc, quality: canonical, inversion: 0 };
|
|
686
819
|
}
|
|
687
|
-
const
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
const
|
|
820
|
+
const parsedSuffix = /^(o|°|dim|ø|\+|aug)?(maj7|M7)?(65|64|43|42|7|6|2)?$/.exec(suffix);
|
|
821
|
+
if (!parsedSuffix) {
|
|
822
|
+
throw new Error(`Unsupported suffix "${suffix}" in Roman numeral: ${text}`);
|
|
823
|
+
}
|
|
824
|
+
const qualityMarker = parsedSuffix[1] ?? "";
|
|
825
|
+
const isDim = qualityMarker === "o" || qualityMarker === "\xB0" || qualityMarker === "dim";
|
|
826
|
+
const isAug = qualityMarker === "+" || qualityMarker === "aug";
|
|
827
|
+
const isHalfDim = qualityMarker === "\xF8";
|
|
828
|
+
const explicitMaj7 = parsedSuffix[2] !== void 0;
|
|
829
|
+
const figures = parsedSuffix[3] ?? "";
|
|
692
830
|
if (figures !== "" && !INVERSION_FIGURES.has(figures)) {
|
|
693
831
|
throw new Error(`Unsupported figured-bass or extension "${figures}" in Roman numeral: ${text}`);
|
|
694
832
|
}
|
|
@@ -937,6 +1075,73 @@ function detectCadence(from, to, key) {
|
|
|
937
1075
|
return null;
|
|
938
1076
|
}
|
|
939
1077
|
|
|
1078
|
+
// src/core/event-index/index.ts
|
|
1079
|
+
var EPS2 = 1e-9;
|
|
1080
|
+
function upperBound(values, beat) {
|
|
1081
|
+
let low = 0;
|
|
1082
|
+
let high = values.length;
|
|
1083
|
+
while (low < high) {
|
|
1084
|
+
const middle = Math.floor((low + high) / 2);
|
|
1085
|
+
if ((values[middle]?.note.startBeat ?? Number.POSITIVE_INFINITY) <= beat) {
|
|
1086
|
+
low = middle + 1;
|
|
1087
|
+
} else {
|
|
1088
|
+
high = middle;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
return low;
|
|
1092
|
+
}
|
|
1093
|
+
function createNoteEventIndex(events, options = {}) {
|
|
1094
|
+
assertNoteEvents(events, "note events", options);
|
|
1095
|
+
const notes = events.map((note, originalIndex) => ({
|
|
1096
|
+
note,
|
|
1097
|
+
originalIndex,
|
|
1098
|
+
endBeat: note.startBeat + note.durationBeat
|
|
1099
|
+
})).sort((a, b) => a.note.startBeat - b.note.startBeat || a.originalIndex - b.originalIndex);
|
|
1100
|
+
const prefixMaxEnd = [];
|
|
1101
|
+
let maxEnd = Number.NEGATIVE_INFINITY;
|
|
1102
|
+
for (const indexed of notes) {
|
|
1103
|
+
maxEnd = Math.max(maxEnd, indexed.endBeat);
|
|
1104
|
+
prefixMaxEnd.push(maxEnd);
|
|
1105
|
+
}
|
|
1106
|
+
return {
|
|
1107
|
+
notes,
|
|
1108
|
+
at(beat) {
|
|
1109
|
+
let index = upperBound(notes, beat + EPS2) - 1;
|
|
1110
|
+
while (index >= 0) {
|
|
1111
|
+
if ((prefixMaxEnd[index] ?? Number.NEGATIVE_INFINITY) <= beat + EPS2) {
|
|
1112
|
+
return void 0;
|
|
1113
|
+
}
|
|
1114
|
+
const indexed = notes[index];
|
|
1115
|
+
if (indexed && indexed.note.durationBeat > 0 && indexed.note.startBeat - EPS2 <= beat && beat < indexed.endBeat - EPS2) {
|
|
1116
|
+
return indexed;
|
|
1117
|
+
}
|
|
1118
|
+
index -= 1;
|
|
1119
|
+
}
|
|
1120
|
+
return void 0;
|
|
1121
|
+
},
|
|
1122
|
+
attacksAt(beat) {
|
|
1123
|
+
const firstAfter = upperBound(notes, beat + EPS2);
|
|
1124
|
+
const candidate = notes[firstAfter - 1];
|
|
1125
|
+
return candidate !== void 0 && Math.abs(candidate.note.startBeat - beat) < EPS2;
|
|
1126
|
+
},
|
|
1127
|
+
onsetsBetween(startBeat, endBeat) {
|
|
1128
|
+
const result = [];
|
|
1129
|
+
let index = upperBound(notes, startBeat + EPS2);
|
|
1130
|
+
while (index < notes.length) {
|
|
1131
|
+
const onset = notes[index]?.note.startBeat;
|
|
1132
|
+
if (onset === void 0 || onset >= endBeat - EPS2) {
|
|
1133
|
+
break;
|
|
1134
|
+
}
|
|
1135
|
+
if (result[result.length - 1] !== onset) {
|
|
1136
|
+
result.push(onset);
|
|
1137
|
+
}
|
|
1138
|
+
index += 1;
|
|
1139
|
+
}
|
|
1140
|
+
return result;
|
|
1141
|
+
}
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
|
|
940
1145
|
// src/analyze/detect/index.ts
|
|
941
1146
|
function pitchClass5(value) {
|
|
942
1147
|
return (Math.trunc(value) % 12 + 12) % 12;
|
|
@@ -944,12 +1149,21 @@ function pitchClass5(value) {
|
|
|
944
1149
|
function uniquePitchClasses(pitches) {
|
|
945
1150
|
return [...new Set(pitches.map(pitchClass5))].sort((a, b) => a - b);
|
|
946
1151
|
}
|
|
947
|
-
function
|
|
1152
|
+
function assertPitches(pitches) {
|
|
1153
|
+
assertGenerationBudget(pitches.length, "detection pitches");
|
|
1154
|
+
for (let index = 0; index < pitches.length; index += 1) {
|
|
1155
|
+
assertFiniteNumber(pitches[index] ?? Number.NaN, `pitches[${index}]`);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
function detectChord(pitches, opts = {}) {
|
|
1159
|
+
assertPitches(pitches);
|
|
1160
|
+
if (opts.bassPc !== void 0) assertFiniteNumber(opts.bassPc, "bassPc");
|
|
948
1161
|
const input = uniquePitchClasses(pitches);
|
|
949
1162
|
if (input.length === 0) {
|
|
950
1163
|
return [];
|
|
951
1164
|
}
|
|
952
|
-
const
|
|
1165
|
+
const inputKind = opts.input === void 0 || opts.input === "auto" ? pitches.every((pitch) => Number.isInteger(pitch) && pitch >= 0 && pitch <= 11) ? "pitchClass" : "midi" : opts.input;
|
|
1166
|
+
const bassPc = opts.bassPc !== void 0 ? pitchClass5(opts.bassPc) : inputKind === "midi" ? pitchClass5(pitches.reduce((lowest, pitch) => Math.min(lowest, pitch), Infinity)) : void 0;
|
|
953
1167
|
const inputSet = new Set(input);
|
|
954
1168
|
const matches = [];
|
|
955
1169
|
const qualities = chordQualities();
|
|
@@ -964,8 +1178,8 @@ function detectChord(pitches) {
|
|
|
964
1178
|
continue;
|
|
965
1179
|
}
|
|
966
1180
|
const extraPcs = input.filter((pc) => !toneSet.has(pc));
|
|
967
|
-
const bassIndex = chord.intervals.findIndex((iv) => pitchClass5(rootPc + iv) === bassPc);
|
|
968
|
-
const inversion = bassIndex > 0 ? bassIndex : 0;
|
|
1181
|
+
const bassIndex = bassPc === void 0 ? -1 : chord.intervals.findIndex((iv) => pitchClass5(rootPc + iv) === bassPc);
|
|
1182
|
+
const inversion = bassPc === void 0 ? null : bassIndex > 0 ? bassIndex : 0;
|
|
969
1183
|
const match = {
|
|
970
1184
|
rootPc,
|
|
971
1185
|
quality,
|
|
@@ -974,7 +1188,7 @@ function detectChord(pitches) {
|
|
|
974
1188
|
exact: extraPcs.length === 0 && missingPcs.length === 0,
|
|
975
1189
|
inversion
|
|
976
1190
|
};
|
|
977
|
-
if (
|
|
1191
|
+
if (bassPc !== void 0 && bassPc !== rootPc) {
|
|
978
1192
|
match.bassPc = bassPc;
|
|
979
1193
|
}
|
|
980
1194
|
matches.push(match);
|
|
@@ -996,8 +1210,8 @@ function detectChord(pitches) {
|
|
|
996
1210
|
});
|
|
997
1211
|
return matches;
|
|
998
1212
|
}
|
|
999
|
-
function detectChordBest(pitches) {
|
|
1000
|
-
const best = detectChord(pitches)[0];
|
|
1213
|
+
function detectChordBest(pitches, opts = {}) {
|
|
1214
|
+
const best = detectChord(pitches, opts)[0];
|
|
1001
1215
|
if (!best) {
|
|
1002
1216
|
return null;
|
|
1003
1217
|
}
|
|
@@ -1005,6 +1219,7 @@ function detectChordBest(pitches) {
|
|
|
1005
1219
|
}
|
|
1006
1220
|
var MINOR_MASK_VARIANTS = [NATURAL_MINOR_MASK, HARMONIC_MINOR_MASK, MELODIC_MINOR_MASK];
|
|
1007
1221
|
function detectKey(pitches) {
|
|
1222
|
+
assertPitches(pitches);
|
|
1008
1223
|
const input = uniquePitchClasses(pitches);
|
|
1009
1224
|
if (input.length === 0) {
|
|
1010
1225
|
return [];
|
|
@@ -1056,10 +1271,21 @@ function detectKey(pitches) {
|
|
|
1056
1271
|
|
|
1057
1272
|
// src/analyze/timeline/index.ts
|
|
1058
1273
|
function chordTimelineFromChords(chords, totalBeats) {
|
|
1059
|
-
|
|
1274
|
+
assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, "timeline totalBeats");
|
|
1275
|
+
assertGenerationBudget(chords.length, "timeline chords");
|
|
1276
|
+
for (let index = 0; index < chords.length; index += 1) {
|
|
1277
|
+
assertRange(
|
|
1278
|
+
chords[index]?.startBeat ?? Number.NaN,
|
|
1279
|
+
0,
|
|
1280
|
+
Number.MAX_SAFE_INTEGER,
|
|
1281
|
+
`chords[${index}].startBeat`
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
const sorted = [...chords].filter((chord) => chord.startBeat < totalBeats).sort((a, b) => a.startBeat - b.startBeat);
|
|
1060
1285
|
const segments = sorted.map((gc, i) => {
|
|
1061
1286
|
const next = sorted[i + 1];
|
|
1062
|
-
const
|
|
1287
|
+
const nextBeat = next ? next.startBeat : totalBeats;
|
|
1288
|
+
const endBeat = Math.min(totalBeats, Math.max(gc.startBeat, nextBeat));
|
|
1063
1289
|
return {
|
|
1064
1290
|
startBeat: gc.startBeat,
|
|
1065
1291
|
endBeat,
|
|
@@ -1070,6 +1296,7 @@ function chordTimelineFromChords(chords, totalBeats) {
|
|
|
1070
1296
|
}
|
|
1071
1297
|
function segmentLookup(segments) {
|
|
1072
1298
|
return (beat) => {
|
|
1299
|
+
assertFiniteNumber(beat, "timeline query beat");
|
|
1073
1300
|
for (const seg of segments) {
|
|
1074
1301
|
if (beat >= seg.startBeat && beat < seg.endBeat) {
|
|
1075
1302
|
return seg.chord;
|
|
@@ -1078,7 +1305,7 @@ function segmentLookup(segments) {
|
|
|
1078
1305
|
return null;
|
|
1079
1306
|
};
|
|
1080
1307
|
}
|
|
1081
|
-
var
|
|
1308
|
+
var EPS3 = 1e-9;
|
|
1082
1309
|
var NOISE_THRESHOLD_RATIO = 0.2;
|
|
1083
1310
|
var MAX_DETECTION_PCS = 6;
|
|
1084
1311
|
var FALLBACK_PCS = 3;
|
|
@@ -1120,11 +1347,11 @@ function analyzeWindow(notes, windowStart, windowEnd, ts, key) {
|
|
|
1120
1347
|
for (const note of notes) {
|
|
1121
1348
|
const noteEnd = note.startBeat + note.durationBeat;
|
|
1122
1349
|
const overlap = Math.min(noteEnd, windowEnd) - Math.max(note.startBeat, windowStart);
|
|
1123
|
-
if (overlap <=
|
|
1350
|
+
if (overlap <= EPS3) {
|
|
1124
1351
|
continue;
|
|
1125
1352
|
}
|
|
1126
1353
|
const velocityFactor = note.velocity !== void 0 ? note.velocity / 127 : 1;
|
|
1127
|
-
const onsetInWindow = note.startBeat >= windowStart -
|
|
1354
|
+
const onsetInWindow = note.startBeat >= windowStart - EPS3 && note.startBeat < windowEnd - EPS3;
|
|
1128
1355
|
const accent = onsetInWindow ? 1 + metricWeight(note.startBeat, ts) / 3 : 1;
|
|
1129
1356
|
const pc = pitchClass6(note.pitch);
|
|
1130
1357
|
weights[pc] = (weights[pc] ?? 0) + overlap * velocityFactor * accent;
|
|
@@ -1138,7 +1365,7 @@ function analyzeWindow(notes, windowStart, windowEnd, ts, key) {
|
|
|
1138
1365
|
totalWeight += w;
|
|
1139
1366
|
maxWeight = Math.max(maxWeight, w);
|
|
1140
1367
|
}
|
|
1141
|
-
if (totalWeight <=
|
|
1368
|
+
if (totalWeight <= EPS3) {
|
|
1142
1369
|
return null;
|
|
1143
1370
|
}
|
|
1144
1371
|
const ranked = weights.map((weight, pc) => ({ pc, weight })).filter(({ weight }) => weight > 0).sort((a, b) => b.weight - a.weight || a.pc - b.pc);
|
|
@@ -1171,26 +1398,40 @@ function sameChord(a, b) {
|
|
|
1171
1398
|
}
|
|
1172
1399
|
function chordTimelineFromNotes(notes, opts = {}) {
|
|
1173
1400
|
const ts = opts.ts ?? parseTimeSignature("4/4");
|
|
1401
|
+
assertTimeSignature(ts);
|
|
1174
1402
|
const harmonicRhythm = opts.harmonicRhythm ?? beatsPerBar(ts);
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
const sounding = notes.
|
|
1403
|
+
assertRange(harmonicRhythm, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "harmonic rhythm");
|
|
1404
|
+
assertNoteEvents(notes, "timeline notes", { allowNonPositiveDuration: true });
|
|
1405
|
+
const soundingIndex = createNoteEventIndex(notes.filter((note) => note.durationBeat > 0));
|
|
1406
|
+
const sounding = soundingIndex.notes.map(({ note }) => note);
|
|
1179
1407
|
const lastNoteEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
|
|
1180
1408
|
const totalBeats = opts.totalBeats ?? lastNoteEnd;
|
|
1409
|
+
assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, "timeline totalBeats");
|
|
1181
1410
|
const key = opts.key ?? detectKey(sounding.map((n) => n.pitch))[0]?.key ?? majorKey(0);
|
|
1182
1411
|
const segments = [];
|
|
1183
1412
|
const segmentConfidence = [];
|
|
1184
|
-
const windowCount = Math.max(0, Math.ceil(totalBeats / harmonicRhythm -
|
|
1413
|
+
const windowCount = Math.max(0, Math.ceil(totalBeats / harmonicRhythm - EPS3));
|
|
1414
|
+
assertGenerationBudget(windowCount, "timeline windows");
|
|
1415
|
+
const windowNotes = Array.from({ length: windowCount }, () => []);
|
|
1416
|
+
let memberships = 0;
|
|
1417
|
+
for (const indexed of soundingIndex.notes) {
|
|
1418
|
+
const first = Math.max(0, Math.floor(indexed.note.startBeat / harmonicRhythm));
|
|
1419
|
+
const lastExclusive = Math.min(windowCount, Math.ceil(indexed.endBeat / harmonicRhythm));
|
|
1420
|
+
memberships += Math.max(0, lastExclusive - first);
|
|
1421
|
+
assertGenerationBudget(memberships, "timeline note-to-window memberships");
|
|
1422
|
+
for (let window = first; window < lastExclusive; window += 1) {
|
|
1423
|
+
windowNotes[window]?.push(indexed.note);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1185
1426
|
for (let i = 0; i < windowCount; i += 1) {
|
|
1186
1427
|
const start = i * harmonicRhythm;
|
|
1187
1428
|
const end = Math.min(start + harmonicRhythm, totalBeats);
|
|
1188
|
-
const inferred = analyzeWindow(
|
|
1429
|
+
const inferred = analyzeWindow(windowNotes[i] ?? [], start, end, ts, key);
|
|
1189
1430
|
if (!inferred) {
|
|
1190
1431
|
continue;
|
|
1191
1432
|
}
|
|
1192
1433
|
const last = segments[segments.length - 1];
|
|
1193
|
-
if (last && sameChord(last.chord, inferred.chord) && Math.abs(last.endBeat - start) <
|
|
1434
|
+
if (last && sameChord(last.chord, inferred.chord) && Math.abs(last.endBeat - start) < EPS3) {
|
|
1194
1435
|
const lastLength = last.endBeat - last.startBeat;
|
|
1195
1436
|
const length = end - start;
|
|
1196
1437
|
const lastConfidence = segmentConfidence[segmentConfidence.length - 1] ?? 0;
|
|
@@ -1215,7 +1456,7 @@ function detectCadences(timeline, key) {
|
|
|
1215
1456
|
if (!prev || !cur) {
|
|
1216
1457
|
continue;
|
|
1217
1458
|
}
|
|
1218
|
-
if (Math.abs(cur.startBeat - prev.endBeat) >
|
|
1459
|
+
if (Math.abs(cur.startBeat - prev.endBeat) > EPS3) {
|
|
1219
1460
|
continue;
|
|
1220
1461
|
}
|
|
1221
1462
|
const type = detectCadence(prev.chord, cur.chord, key);
|
|
@@ -1227,9 +1468,9 @@ function detectCadences(timeline, key) {
|
|
|
1227
1468
|
}
|
|
1228
1469
|
|
|
1229
1470
|
// src/analyze/arrange/internal.ts
|
|
1230
|
-
var
|
|
1471
|
+
var EPS4 = 1e-9;
|
|
1231
1472
|
function covers(note, beat) {
|
|
1232
|
-
return beat >= note.startBeat -
|
|
1473
|
+
return beat >= note.startBeat - EPS4 && beat < note.endBeat - EPS4;
|
|
1233
1474
|
}
|
|
1234
1475
|
function poolNotes(tracks) {
|
|
1235
1476
|
const notes = [];
|
|
@@ -1247,7 +1488,7 @@ function splitIntoSubVoices(ordered) {
|
|
|
1247
1488
|
for (const note of ordered) {
|
|
1248
1489
|
const lane = lanes.find((candidate) => {
|
|
1249
1490
|
const last = candidate[candidate.length - 1];
|
|
1250
|
-
return last !== void 0 && last.startBeat + last.durationBeat <= note.startBeat +
|
|
1491
|
+
return last !== void 0 && last.startBeat + last.durationBeat <= note.startBeat + EPS4;
|
|
1251
1492
|
});
|
|
1252
1493
|
if (lane) {
|
|
1253
1494
|
lane.push(note);
|
|
@@ -1265,9 +1506,11 @@ function prepareTracks(tracks) {
|
|
|
1265
1506
|
voice,
|
|
1266
1507
|
sounding: voice.map((note, i) => {
|
|
1267
1508
|
const prev = i > 0 ? voice[i - 1] : void 0;
|
|
1509
|
+
const prevEnd = prev === void 0 ? void 0 : prev.startBeat + prev.durationBeat;
|
|
1510
|
+
const contiguous = prevEnd !== void 0 && Math.abs(prevEnd - note.startBeat) <= EPS4;
|
|
1268
1511
|
return {
|
|
1269
1512
|
pitch: note.pitch,
|
|
1270
|
-
prevPitch: prev?.pitch,
|
|
1513
|
+
prevPitch: contiguous ? prev?.pitch : void 0,
|
|
1271
1514
|
startBeat: note.startBeat,
|
|
1272
1515
|
endBeat: note.startBeat + note.durationBeat
|
|
1273
1516
|
};
|
|
@@ -1299,11 +1542,16 @@ function clamp01(value) {
|
|
|
1299
1542
|
}
|
|
1300
1543
|
function tensionCurve(tracks, opts = {}) {
|
|
1301
1544
|
const ts = opts.ts ?? parseTimeSignature("4/4");
|
|
1545
|
+
assertTimeSignature(ts);
|
|
1546
|
+
assertGenerationBudget(tracks.length, "arrangement tracks");
|
|
1547
|
+
for (let index = 0; index < tracks.length; index += 1) {
|
|
1548
|
+
assertNoteEvents(tracks[index]?.notes ?? [], `tracks[${index}].notes`, {
|
|
1549
|
+
allowNonPositiveDuration: true
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1302
1552
|
const profile = opts.profile ?? "pop";
|
|
1303
1553
|
const step = opts.step ?? 1;
|
|
1304
|
-
|
|
1305
|
-
throw new Error(`Invalid tension sampling step: ${step}`);
|
|
1306
|
-
}
|
|
1554
|
+
assertRange(step, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "tension sampling step");
|
|
1307
1555
|
const pooled = poolNotes(tracks);
|
|
1308
1556
|
const totalBeats = pooled.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
|
|
1309
1557
|
const { timeline, key } = chordTimelineFromNotes(pooled, {
|
|
@@ -1313,7 +1561,8 @@ function tensionCurve(tracks, opts = {}) {
|
|
|
1313
1561
|
});
|
|
1314
1562
|
const prepared = prepareTracks(tracks);
|
|
1315
1563
|
const points = [];
|
|
1316
|
-
const sampleCount = Math.max(0, Math.ceil(totalBeats / step -
|
|
1564
|
+
const sampleCount = Math.max(0, Math.ceil(totalBeats / step - EPS4));
|
|
1565
|
+
assertGenerationBudget(sampleCount, "tension samples");
|
|
1317
1566
|
for (let i = 0; i < sampleCount; i += 1) {
|
|
1318
1567
|
const beat = i * step;
|
|
1319
1568
|
points.push({ beat, tension: sampleTension(prepared, timeline, key, ts, profile, beat) });
|
|
@@ -1383,7 +1632,7 @@ function sampleTension(prepared, timeline, key, ts, profile, beat) {
|
|
|
1383
1632
|
}
|
|
1384
1633
|
|
|
1385
1634
|
// src/analyze/voice/index.ts
|
|
1386
|
-
var
|
|
1635
|
+
var EPS5 = 1e-9;
|
|
1387
1636
|
function pitchClass8(pitch) {
|
|
1388
1637
|
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
1389
1638
|
}
|
|
@@ -1439,6 +1688,7 @@ function stepResolution2(pitch, chord) {
|
|
|
1439
1688
|
return void 0;
|
|
1440
1689
|
}
|
|
1441
1690
|
function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat) {
|
|
1691
|
+
assertNoteEvents(voice, "voice notes", { allowNonPositiveDuration: true });
|
|
1442
1692
|
const result = [];
|
|
1443
1693
|
for (let i = 0; i < voice.length; i += 1) {
|
|
1444
1694
|
const note = voice[i];
|
|
@@ -1447,8 +1697,10 @@ function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat) {
|
|
|
1447
1697
|
}
|
|
1448
1698
|
const prevNote = i > 0 ? voice[i - 1] : void 0;
|
|
1449
1699
|
const nextNote = i + 1 < voice.length ? voice[i + 1] : void 0;
|
|
1450
|
-
const
|
|
1451
|
-
const
|
|
1700
|
+
const prevEnd = prevNote === void 0 ? Number.NEGATIVE_INFINITY : prevNote.startBeat + prevNote.durationBeat;
|
|
1701
|
+
const noteEnd = note.startBeat + note.durationBeat;
|
|
1702
|
+
const prev = prevNote !== void 0 && Math.abs(prevEnd - note.startBeat) <= EPS5 ? prevNote : void 0;
|
|
1703
|
+
const next = nextNote !== void 0 && Math.abs(noteEnd - nextNote.startBeat) <= EPS5 ? nextNote : void 0;
|
|
1452
1704
|
const chord = chordAtBeat(note.startBeat);
|
|
1453
1705
|
const labels = [];
|
|
1454
1706
|
const member = isChordMember2(note.pitch, chord);
|
|
@@ -1588,8 +1840,9 @@ function otherVoicesSounding(prepared, excludeTrack, excludeVoice, beat) {
|
|
|
1588
1840
|
continue;
|
|
1589
1841
|
}
|
|
1590
1842
|
const snap = { pitch: note.pitch };
|
|
1591
|
-
|
|
1592
|
-
|
|
1843
|
+
const previous = Math.abs(note.startBeat - beat) <= EPS4 ? note.prevPitch : note.pitch;
|
|
1844
|
+
if (previous !== void 0) {
|
|
1845
|
+
snap.prevPitch = previous;
|
|
1593
1846
|
}
|
|
1594
1847
|
out.push(snap);
|
|
1595
1848
|
}
|
|
@@ -1601,7 +1854,7 @@ function evaluationBeats(note, timeline) {
|
|
|
1601
1854
|
const noteEnd = note.startBeat + note.durationBeat;
|
|
1602
1855
|
const beats = [note.startBeat];
|
|
1603
1856
|
for (const segment of timeline.segments) {
|
|
1604
|
-
if (segment.startBeat > note.startBeat +
|
|
1857
|
+
if (segment.startBeat > note.startBeat + EPS4 && segment.startBeat < noteEnd - EPS4) {
|
|
1605
1858
|
beats.push(segment.startBeat);
|
|
1606
1859
|
}
|
|
1607
1860
|
}
|
|
@@ -1609,6 +1862,13 @@ function evaluationBeats(note, timeline) {
|
|
|
1609
1862
|
}
|
|
1610
1863
|
function analyzeArrangement(tracks, opts = {}) {
|
|
1611
1864
|
const ts = opts.ts ?? parseTimeSignature("4/4");
|
|
1865
|
+
assertTimeSignature(ts);
|
|
1866
|
+
assertGenerationBudget(tracks.length, "arrangement tracks");
|
|
1867
|
+
for (let index = 0; index < tracks.length; index += 1) {
|
|
1868
|
+
assertNoteEvents(tracks[index]?.notes ?? [], `tracks[${index}].notes`, {
|
|
1869
|
+
allowNonPositiveDuration: true
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1612
1872
|
const profile = opts.profile ?? "pop";
|
|
1613
1873
|
const pooled = poolNotes(tracks);
|
|
1614
1874
|
const { timeline, key, segmentConfidence } = chordTimelineFromNotes(pooled, {
|
|
@@ -1639,11 +1899,18 @@ function analyzeArrangement(tracks, opts = {}) {
|
|
|
1639
1899
|
(beat) => otherVoicesSounding(prepared, t, v, beat)
|
|
1640
1900
|
)
|
|
1641
1901
|
);
|
|
1642
|
-
for (
|
|
1902
|
+
for (let noteIndex = 0; noteIndex < subVoice.voice.length; noteIndex += 1) {
|
|
1903
|
+
const note = subVoice.voice[noteIndex];
|
|
1904
|
+
const preparedNote = subVoice.sounding[noteIndex];
|
|
1905
|
+
if (note === void 0 || preparedNote === void 0) {
|
|
1906
|
+
continue;
|
|
1907
|
+
}
|
|
1643
1908
|
for (const beat of evaluationBeats(note, timeline)) {
|
|
1909
|
+
const atOnset = Math.abs(beat - note.startBeat) <= EPS4;
|
|
1644
1910
|
const result = evaluateSafety({
|
|
1645
1911
|
profile,
|
|
1646
1912
|
candidatePitch: note.pitch,
|
|
1913
|
+
prevPitch: atOnset ? preparedNote.prevPitch : note.pitch,
|
|
1647
1914
|
chord: timeline.at(beat),
|
|
1648
1915
|
key,
|
|
1649
1916
|
otherVoices: otherVoicesSounding(prepared, t, v, beat),
|
|
@@ -1660,6 +1927,12 @@ function analyzeArrangement(tracks, opts = {}) {
|
|
|
1660
1927
|
if (result.rationale !== void 0) {
|
|
1661
1928
|
conflict.rationale = result.rationale;
|
|
1662
1929
|
}
|
|
1930
|
+
if (result.resolveTo !== void 0) {
|
|
1931
|
+
conflict.resolveTo = result.resolveTo;
|
|
1932
|
+
}
|
|
1933
|
+
if (result.suggestions !== void 0) {
|
|
1934
|
+
conflict.suggestions = [...result.suggestions];
|
|
1935
|
+
}
|
|
1663
1936
|
conflicts.push(conflict);
|
|
1664
1937
|
}
|
|
1665
1938
|
}
|