@libraz/libcantus 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +63 -0
- package/README.md +3 -1
- package/README_ja.md +13 -11
- package/dist/analyze/index.cjs +7 -7
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +14 -12
- package/dist/analyze/index.d.ts +14 -12
- package/dist/analyze/index.js +6 -6
- package/dist/{chunk-NB25P3EH.js → chunk-2G5MCGYU.js} +33 -11
- package/dist/chunk-2G5MCGYU.js.map +1 -0
- package/dist/{chunk-NM7UAMC3.cjs → chunk-3FDBMIPR.cjs} +64 -64
- package/dist/chunk-3FDBMIPR.cjs.map +1 -0
- package/dist/{chunk-YFA7X2F6.cjs → chunk-3MDOEFIQ.cjs} +411 -327
- package/dist/chunk-3MDOEFIQ.cjs.map +1 -0
- package/dist/{chunk-IPEF6NWB.js → chunk-5MWRBYAT.js} +278 -154
- package/dist/chunk-5MWRBYAT.js.map +1 -0
- package/dist/{chunk-B2PW6EHN.cjs → chunk-6TTBEON3.cjs} +1253 -889
- package/dist/chunk-6TTBEON3.cjs.map +1 -0
- package/dist/{chunk-UMWZ4ZET.js → chunk-7NQ6AL3H.js} +4 -4
- package/dist/chunk-7NQ6AL3H.js.map +1 -0
- package/dist/{chunk-PWC4CRWT.cjs → chunk-E3QO3UET.cjs} +31 -23
- package/dist/chunk-E3QO3UET.cjs.map +1 -0
- package/dist/{chunk-GNVN4PPJ.js → chunk-FJFULJZE.js} +531 -213
- package/dist/chunk-FJFULJZE.js.map +1 -0
- package/dist/{chunk-CPMB7P75.js → chunk-FK3DGAIZ.js} +608 -244
- package/dist/chunk-FK3DGAIZ.js.map +1 -0
- package/dist/chunk-GEDTJHAC.cjs.map +1 -1
- package/dist/{chunk-RFIBF2RR.js → chunk-H7FVMBUC.js} +138 -65
- package/dist/chunk-H7FVMBUC.js.map +1 -0
- package/dist/{chunk-4R2LWV7E.cjs → chunk-HYSVSFNI.cjs} +664 -540
- package/dist/chunk-HYSVSFNI.cjs.map +1 -0
- package/dist/{chunk-35NBV2FT.cjs → chunk-KMYGRFZ6.cjs} +47 -24
- package/dist/chunk-KMYGRFZ6.cjs.map +1 -0
- package/dist/{chunk-IFZDWIKM.cjs → chunk-LQZQDBCD.cjs} +1048 -730
- package/dist/chunk-LQZQDBCD.cjs.map +1 -0
- package/dist/{chunk-PM4XJE2Y.cjs → chunk-P3CPXAJ5.cjs} +76 -54
- package/dist/chunk-P3CPXAJ5.cjs.map +1 -0
- package/dist/{chunk-S5BKZZGM.cjs → chunk-S3QELJUE.cjs} +72 -56
- package/dist/chunk-S3QELJUE.cjs.map +1 -0
- package/dist/{chunk-PUKJP2Z5.js → chunk-TXFWSU5D.js} +31 -8
- package/dist/chunk-TXFWSU5D.js.map +1 -0
- package/dist/{chunk-76ZBTRG2.cjs → chunk-U4EQTDGN.cjs} +258 -218
- package/dist/chunk-U4EQTDGN.cjs.map +1 -0
- package/dist/chunk-UGLFDG7L.cjs.map +1 -1
- package/dist/{chunk-MLBPXA4Z.js → chunk-UTE5A36R.js} +198 -114
- package/dist/chunk-UTE5A36R.js.map +1 -0
- package/dist/{chunk-ZBUROTJQ.js → chunk-UZKQISRF.js} +17 -9
- package/dist/chunk-UZKQISRF.js.map +1 -0
- package/dist/{chunk-7KG3RTYS.js → chunk-VL5NXVCN.js} +281 -155
- package/dist/chunk-VL5NXVCN.js.map +1 -0
- package/dist/{chunk-CGBADIO5.cjs → chunk-WOMCXN2W.cjs} +328 -202
- package/dist/chunk-WOMCXN2W.cjs.map +1 -0
- package/dist/{chunk-3R3QVKY5.cjs → chunk-YDGMWH43.cjs} +397 -324
- package/dist/chunk-YDGMWH43.cjs.map +1 -0
- package/dist/{chunk-AGOVW7U6.js → chunk-YYVXRV3V.js} +72 -56
- package/dist/chunk-YYVXRV3V.js.map +1 -0
- package/dist/{chunk-M5RYMAFI.js → chunk-ZLNSH4UL.js} +71 -31
- package/dist/chunk-ZLNSH4UL.js.map +1 -0
- package/dist/{coerce-DRERzdve.d.cts → coerce-CUhHc3Tu.d.ts} +16 -8
- package/dist/{coerce-DP0vYu-x.d.ts → coerce-Ytt2Eiq9.d.cts} +16 -8
- package/dist/core/index.cjs +5 -5
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +9 -8
- package/dist/core/index.d.ts +9 -8
- package/dist/core/index.js +4 -4
- package/dist/generate/index.cjs +8 -8
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +28 -18
- package/dist/generate/index.d.ts +28 -18
- package/dist/generate/index.js +7 -7
- package/dist/{index-BMweUlm5.d.cts → index-5VJgKz_Q.d.cts} +6 -2
- package/dist/{index-BHIUyjhZ.d.ts → index-BLuDesVG.d.ts} +20 -13
- package/dist/{index-B9QLziDe.d.cts → index-BV6APVa6.d.cts} +20 -13
- package/dist/{index-CL4b4gUQ.d.ts → index-Bf0Sv8a9.d.ts} +6 -2
- package/dist/{index-Dw2DP7vm.d.ts → index-BkT21EMv.d.cts} +3 -2
- package/dist/{index-BL5N2Oyz.d.cts → index-C7KCaTRz.d.ts} +3 -2
- package/dist/{index-Cy15oTvh.d.ts → index-C9MYbuKS.d.cts} +7 -3
- package/dist/{index-Cy15oTvh.d.cts → index-C9MYbuKS.d.ts} +7 -3
- package/dist/{index-CY-3NVfy.d.cts → index-DVInzCDS.d.cts} +53 -29
- package/dist/{index-9eoQs2C1.d.ts → index-Dg-SAfhz.d.ts} +6 -4
- package/dist/{index-zaZdmnU4.d.ts → index-jgeug9EA.d.ts} +53 -29
- package/dist/{index-cHvbYgJI.d.cts → index-pQihL4z7.d.cts} +6 -4
- package/dist/index.cjs +13 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -11
- package/dist/index.d.ts +11 -11
- package/dist/index.js +12 -12
- package/dist/{masks-Cc7-MzzY.d.ts → masks-BnzXnBj0.d.cts} +3 -2
- package/dist/{masks-pADfTOiD.d.cts → masks-tycmkrgs.d.ts} +3 -2
- package/dist/model/index.cjs +12 -12
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +63 -48
- package/dist/model/index.d.ts +63 -48
- package/dist/model/index.js +11 -11
- package/dist/{positional-DJgs6ltD.d.ts → positional-CscpxtlE.d.cts} +3 -2
- package/dist/{positional-DMZer-FM.d.cts → positional-DLnfUgqG.d.ts} +3 -2
- package/dist/{profile-DLdlIpS9.d.ts → profile-B4WqcM4H.d.ts} +25 -17
- package/dist/{profile-CQMEqdH9.d.cts → profile-BJd1XkPB.d.cts} +25 -17
- package/dist/theory/index.cjs +5 -5
- package/dist/theory/index.cjs.map +1 -1
- package/dist/theory/index.d.cts +18 -14
- package/dist/theory/index.d.ts +18 -14
- package/dist/theory/index.js +4 -4
- package/dist/{types-D-XgxGvp.d.cts → types-7mGFwvMm.d.ts} +45 -22
- package/dist/{types-Dh7C064C.d.ts → types-_ofpb2be.d.cts} +45 -22
- package/package.json +13 -13
- package/dist/chunk-35NBV2FT.cjs.map +0 -1
- package/dist/chunk-3R3QVKY5.cjs.map +0 -1
- package/dist/chunk-4R2LWV7E.cjs.map +0 -1
- package/dist/chunk-76ZBTRG2.cjs.map +0 -1
- package/dist/chunk-7KG3RTYS.js.map +0 -1
- package/dist/chunk-AGOVW7U6.js.map +0 -1
- package/dist/chunk-B2PW6EHN.cjs.map +0 -1
- package/dist/chunk-CGBADIO5.cjs.map +0 -1
- package/dist/chunk-CPMB7P75.js.map +0 -1
- package/dist/chunk-GNVN4PPJ.js.map +0 -1
- package/dist/chunk-IFZDWIKM.cjs.map +0 -1
- package/dist/chunk-IPEF6NWB.js.map +0 -1
- package/dist/chunk-M5RYMAFI.js.map +0 -1
- package/dist/chunk-MLBPXA4Z.js.map +0 -1
- package/dist/chunk-NB25P3EH.js.map +0 -1
- package/dist/chunk-NM7UAMC3.cjs.map +0 -1
- package/dist/chunk-PM4XJE2Y.cjs.map +0 -1
- package/dist/chunk-PUKJP2Z5.js.map +0 -1
- package/dist/chunk-PWC4CRWT.cjs.map +0 -1
- package/dist/chunk-RFIBF2RR.js.map +0 -1
- package/dist/chunk-S5BKZZGM.cjs.map +0 -1
- package/dist/chunk-UMWZ4ZET.js.map +0 -1
- package/dist/chunk-YFA7X2F6.cjs.map +0 -1
- package/dist/chunk-ZBUROTJQ.js.map +0 -1
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canonicalNoteOrder,
|
|
3
|
+
soundingIndices
|
|
4
|
+
} from "./chunk-TXFWSU5D.js";
|
|
1
5
|
import {
|
|
2
6
|
ARTICULATIONS,
|
|
3
7
|
BEAT_EPS,
|
|
4
8
|
InvalidInputError,
|
|
9
|
+
assertArray,
|
|
5
10
|
assertFiniteNumber,
|
|
6
11
|
assertInteger,
|
|
7
12
|
assertMidiPitch,
|
|
@@ -10,7 +15,7 @@ import {
|
|
|
10
15
|
assertPositiveInt,
|
|
11
16
|
assertRange,
|
|
12
17
|
describeRejected
|
|
13
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-YYVXRV3V.js";
|
|
14
19
|
|
|
15
20
|
// src/core/instrument/profile.ts
|
|
16
21
|
var LIMBS = Object.freeze(["rightHand", "leftHand", "rightFoot", "leftFoot"]);
|
|
@@ -80,11 +85,22 @@ function assertProfile(profile) {
|
|
|
80
85
|
for (const key of voices) {
|
|
81
86
|
assertMidiPitch(Number(key), `${profile.name} reach pitch`);
|
|
82
87
|
}
|
|
88
|
+
for (const key of voices) {
|
|
89
|
+
const label = `${profile.name} reach[${key}]`;
|
|
90
|
+
for (const [index, limb] of assertArray(
|
|
91
|
+
profile.reach[key],
|
|
92
|
+
label
|
|
93
|
+
).entries()) {
|
|
94
|
+
assertOneOf(limb, LIMBS, `${label}[${index}]`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
83
97
|
if (voices.every((key) => reachOf(profile, Number(key)).length === 0)) {
|
|
84
98
|
throw new InvalidInputError(`${profile.name} must have at least one voice within reach`);
|
|
85
99
|
}
|
|
86
|
-
|
|
87
|
-
|
|
100
|
+
if (profile.overdub !== void 0) {
|
|
101
|
+
for (const pitch of assertArray(profile.overdub, `${profile.name} overdub`)) {
|
|
102
|
+
assertMidiPitch(pitch, `${profile.name} overdub pitch`);
|
|
103
|
+
}
|
|
88
104
|
}
|
|
89
105
|
}
|
|
90
106
|
function reachOf(profile, pitch) {
|
|
@@ -180,38 +196,32 @@ var MIN_DIFFICULTY = 1;
|
|
|
180
196
|
var MAX_DIFFICULTY = 5;
|
|
181
197
|
var STRING_CROSSING_COST = 2;
|
|
182
198
|
var SIMULTANEITY_BEATS = 1 / 128;
|
|
199
|
+
var LEGATO_OVERLAP_BEATS = 1 / 16;
|
|
183
200
|
function playability(notes, profile, bpm) {
|
|
184
201
|
assertNoteEvents(notes, "playability notes", { allowNonPositiveDuration: true });
|
|
185
202
|
if (bpm !== void 0) {
|
|
186
203
|
assertRange(bpm, Number.MIN_VALUE, 1e3, "playability bpm");
|
|
187
204
|
}
|
|
188
205
|
const instrument = toInstrumentProfile(profile, "playability profile");
|
|
189
|
-
const order = notes.map((_, index) => index).sort((a, b) => {
|
|
190
|
-
const left = notes[a];
|
|
191
|
-
const right = notes[b];
|
|
192
|
-
if (!left || !right) {
|
|
193
|
-
return a - b;
|
|
194
|
-
}
|
|
195
|
-
return left.startBeat - right.startBeat || left.pitch - right.pitch || a - b;
|
|
196
|
-
});
|
|
197
206
|
const state = {
|
|
198
207
|
notes,
|
|
199
|
-
order,
|
|
208
|
+
order: canonicalNoteOrder(notes),
|
|
200
209
|
placements: notes.map((_, index) => ({ note: index })),
|
|
201
210
|
issues: [],
|
|
202
211
|
movement: 0,
|
|
203
212
|
secondsPerBeat: bpm === void 0 ? void 0 : 60 / bpm
|
|
204
213
|
};
|
|
214
|
+
const attacks = timelineOf(state);
|
|
205
215
|
if (instrument.kind === "stringed") {
|
|
206
216
|
checkExistence(state, instrument, (pitch) => soundsIn(instrument, pitch));
|
|
207
|
-
placeOnNeck(state, instrument);
|
|
217
|
+
placeOnNeck(state, instrument, attacks);
|
|
208
218
|
} else {
|
|
209
219
|
const kit = readKit(instrument);
|
|
210
220
|
checkExistence(state, instrument, (pitch) => limbsFor(kit, pitch).length > 0);
|
|
211
|
-
placeOnKit(state, kit);
|
|
221
|
+
placeOnKit(state, kit, attacks);
|
|
212
222
|
}
|
|
213
223
|
return {
|
|
214
|
-
difficulty: difficultyOf(state, bpm),
|
|
224
|
+
difficulty: difficultyOf(state, attacks, bpm),
|
|
215
225
|
issues: state.issues,
|
|
216
226
|
placements: state.placements
|
|
217
227
|
};
|
|
@@ -245,110 +255,228 @@ function checkExistence(state, profile, sounds) {
|
|
|
245
255
|
}
|
|
246
256
|
}
|
|
247
257
|
}
|
|
248
|
-
function
|
|
249
|
-
return
|
|
258
|
+
function isReleased(endBeat, beat) {
|
|
259
|
+
return endBeat <= beat + LEGATO_OVERLAP_BEATS + BEAT_EPS;
|
|
250
260
|
}
|
|
251
|
-
function
|
|
252
|
-
const
|
|
261
|
+
function timelineOf(state) {
|
|
262
|
+
const sounding = new Set(soundingIndices(state.notes));
|
|
263
|
+
const attacks = [];
|
|
253
264
|
let active = [];
|
|
254
|
-
let
|
|
255
|
-
let anchor;
|
|
265
|
+
let current;
|
|
256
266
|
for (const index of state.order) {
|
|
257
267
|
const note = state.notes[index];
|
|
258
|
-
if (!note) {
|
|
268
|
+
if (!note || !sounding.has(index)) {
|
|
259
269
|
continue;
|
|
260
270
|
}
|
|
261
|
-
if (
|
|
262
|
-
|
|
271
|
+
if (!current || note.startBeat - current.startBeat > SIMULTANEITY_BEATS + BEAT_EPS) {
|
|
272
|
+
const at = note.startBeat;
|
|
273
|
+
active = active.filter((held) => !isReleased(held.endBeat, at));
|
|
274
|
+
current = { startBeat: at, members: [], held: [...active] };
|
|
275
|
+
attacks.push(current);
|
|
263
276
|
}
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
277
|
+
const timed = {
|
|
278
|
+
index,
|
|
279
|
+
startBeat: note.startBeat,
|
|
280
|
+
endBeat: note.startBeat + note.durationBeat,
|
|
281
|
+
attackBeat: current.startBeat
|
|
282
|
+
};
|
|
283
|
+
current.members.push(timed);
|
|
284
|
+
active.push(timed);
|
|
285
|
+
}
|
|
286
|
+
for (const attack of attacks) {
|
|
287
|
+
attack.members.sort(
|
|
288
|
+
(a, b) => (state.notes[a.index]?.pitch ?? 0) - (state.notes[b.index]?.pitch ?? 0) || a.index - b.index
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
return attacks;
|
|
292
|
+
}
|
|
293
|
+
var MAX_JOINT_PLACEMENT_NOTES = 12;
|
|
294
|
+
var MAX_PLACEMENT_SEARCH_NODES = 2e4;
|
|
295
|
+
function fitsHand(frets, maxStretch) {
|
|
296
|
+
let low = Number.POSITIVE_INFINITY;
|
|
297
|
+
let high = Number.NEGATIVE_INFINITY;
|
|
298
|
+
let count = 0;
|
|
299
|
+
for (const fret of frets) {
|
|
300
|
+
if (fret > 0) {
|
|
301
|
+
low = Math.min(low, fret);
|
|
302
|
+
high = Math.max(high, fret);
|
|
303
|
+
count += 1;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return count < 2 || high - low <= maxStretch;
|
|
307
|
+
}
|
|
308
|
+
function placeGreedily(members, previous, freeAt, heldBy) {
|
|
309
|
+
const free = [...freeAt];
|
|
310
|
+
const held = [...heldBy];
|
|
311
|
+
const picks = [];
|
|
312
|
+
const conflicts = [];
|
|
313
|
+
let from = previous;
|
|
314
|
+
for (const member of members) {
|
|
315
|
+
const open = member.candidates.filter(
|
|
316
|
+
(candidate) => isReleased(free[candidate.string] ?? Number.NEGATIVE_INFINITY, member.attackBeat)
|
|
317
|
+
);
|
|
318
|
+
if (open.length === 0) {
|
|
319
|
+
conflicts.push({
|
|
320
|
+
member,
|
|
321
|
+
blocked: member.candidates.map((candidate) => held[candidate.string]).filter((value) => value !== void 0)
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
const pool = open.length > 0 ? open : member.candidates;
|
|
325
|
+
const origin = from;
|
|
326
|
+
const chosen = pool.reduce(
|
|
327
|
+
(best, candidate) => fingeringCost(candidate, origin) < fingeringCost(best, origin) ? candidate : best
|
|
328
|
+
);
|
|
329
|
+
picks.push(chosen);
|
|
330
|
+
free[chosen.string] = Number.POSITIVE_INFINITY;
|
|
331
|
+
held[chosen.string] = member.index;
|
|
332
|
+
from = movedTo(from, chosen);
|
|
333
|
+
}
|
|
334
|
+
return { picks, conflicts };
|
|
335
|
+
}
|
|
336
|
+
function searchPlacement(members, previous, freeAt, heldFrets, maxStretch) {
|
|
337
|
+
const free = [...freeAt];
|
|
338
|
+
const frets = heldFrets === void 0 ? void 0 : [...heldFrets];
|
|
339
|
+
const picks = [];
|
|
340
|
+
let best;
|
|
341
|
+
let bestCost = Number.POSITIVE_INFINITY;
|
|
342
|
+
let nodes = 0;
|
|
343
|
+
const visit = (depth, from, cost) => {
|
|
344
|
+
if (cost >= bestCost || nodes >= MAX_PLACEMENT_SEARCH_NODES) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const member = members[depth];
|
|
348
|
+
if (member === void 0) {
|
|
349
|
+
best = [...picks];
|
|
350
|
+
bestCost = cost;
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
for (const candidate of member.candidates) {
|
|
354
|
+
const previousFree = free[candidate.string] ?? Number.NEGATIVE_INFINITY;
|
|
355
|
+
if (!isReleased(previousFree, member.attackBeat)) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
nodes += 1;
|
|
359
|
+
if (frets !== void 0) {
|
|
360
|
+
frets.push(candidate.fret);
|
|
361
|
+
if (!fitsHand(frets, maxStretch)) {
|
|
362
|
+
frets.pop();
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
275
365
|
}
|
|
276
|
-
|
|
277
|
-
|
|
366
|
+
free[candidate.string] = Number.POSITIVE_INFINITY;
|
|
367
|
+
picks.push(candidate);
|
|
368
|
+
visit(depth + 1, movedTo(from, candidate), cost + fingeringCost(candidate, from));
|
|
369
|
+
picks.pop();
|
|
370
|
+
free[candidate.string] = previousFree;
|
|
371
|
+
if (frets !== void 0) {
|
|
372
|
+
frets.pop();
|
|
278
373
|
}
|
|
279
|
-
cursor += 1;
|
|
280
374
|
}
|
|
281
|
-
|
|
375
|
+
};
|
|
376
|
+
visit(0, previous, 0);
|
|
377
|
+
return best;
|
|
378
|
+
}
|
|
379
|
+
function placeAttack(members, previous, freeAt, heldBy, heldFrets, maxStretch) {
|
|
380
|
+
const greedy = placeGreedily(members, previous, freeAt, heldBy);
|
|
381
|
+
const soundingFrets = [...heldFrets, ...greedy.picks.map((pick) => pick.fret)];
|
|
382
|
+
if (greedy.conflicts.length === 0 && fitsHand(soundingFrets, maxStretch)) {
|
|
383
|
+
return greedy;
|
|
384
|
+
}
|
|
385
|
+
if (members.length > MAX_JOINT_PLACEMENT_NOTES) {
|
|
386
|
+
return greedy;
|
|
387
|
+
}
|
|
388
|
+
const fitted = searchPlacement(members, previous, freeAt, heldFrets, maxStretch);
|
|
389
|
+
if (fitted !== void 0) {
|
|
390
|
+
return { picks: fitted, conflicts: [] };
|
|
391
|
+
}
|
|
392
|
+
if (greedy.conflicts.length > 0) {
|
|
393
|
+
const loose = searchPlacement(members, previous, freeAt, void 0, maxStretch);
|
|
394
|
+
if (loose !== void 0) {
|
|
395
|
+
return { picks: loose, conflicts: [] };
|
|
396
|
+
}
|
|
282
397
|
}
|
|
283
|
-
return
|
|
398
|
+
return greedy;
|
|
284
399
|
}
|
|
285
|
-
function placeOnNeck(state, profile) {
|
|
400
|
+
function placeOnNeck(state, profile, attacks) {
|
|
286
401
|
const stringFreeAt = profile.tuning.map(() => Number.NEGATIVE_INFINITY);
|
|
287
402
|
const stringHeldBy = profile.tuning.map(() => void 0);
|
|
288
|
-
let
|
|
289
|
-
let
|
|
290
|
-
for (const
|
|
291
|
-
const
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
const free = candidates.filter(
|
|
301
|
-
(candidate) => (stringFreeAt[candidate.string] ?? 0) <= note.startBeat + BEAT_EPS
|
|
302
|
-
);
|
|
303
|
-
if (free.length === 0) {
|
|
304
|
-
const blocked = candidates.map((candidate) => stringHeldBy[candidate.string]).filter((held) => held !== void 0);
|
|
305
|
-
state.issues.push({
|
|
306
|
-
type: "stringConflict",
|
|
307
|
-
layer: 2,
|
|
308
|
-
notes: [.../* @__PURE__ */ new Set([...blocked, index])].sort((a, b) => a - b),
|
|
309
|
-
startBeat: note.startBeat,
|
|
310
|
-
impossible: true,
|
|
311
|
-
message: `MIDI ${note.pitch} needs a string already sounding on ${profile.name}`
|
|
312
|
-
});
|
|
403
|
+
let origin = {};
|
|
404
|
+
let hand;
|
|
405
|
+
for (const attack of attacks) {
|
|
406
|
+
const members = [];
|
|
407
|
+
for (const timed of attack.members) {
|
|
408
|
+
const note = state.notes[timed.index];
|
|
409
|
+
const candidates = note === void 0 ? [] : fingeringsIn(profile, note.pitch);
|
|
410
|
+
if (candidates.length > 0) {
|
|
411
|
+
members.push({ ...timed, candidates });
|
|
412
|
+
}
|
|
313
413
|
}
|
|
314
|
-
const
|
|
315
|
-
const
|
|
316
|
-
|
|
414
|
+
const heldFrets = attack.held.map((held) => state.placements[held.index]?.fingering?.fret).filter((fret) => fret !== void 0);
|
|
415
|
+
const { picks, conflicts } = placeAttack(
|
|
416
|
+
members,
|
|
417
|
+
origin,
|
|
418
|
+
stringFreeAt,
|
|
419
|
+
stringHeldBy,
|
|
420
|
+
heldFrets,
|
|
421
|
+
profile.maxStretch
|
|
317
422
|
);
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
423
|
+
let shifted = hand;
|
|
424
|
+
members.forEach((member, k) => {
|
|
425
|
+
const chosen = picks[k];
|
|
426
|
+
const placement = state.placements[member.index];
|
|
427
|
+
if (!chosen || !placement) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const conflict = conflicts.find((entry) => entry.member === member);
|
|
431
|
+
const note = state.notes[member.index];
|
|
432
|
+
if (conflict && note) {
|
|
433
|
+
state.issues.push({
|
|
434
|
+
type: "stringConflict",
|
|
435
|
+
layer: 2,
|
|
436
|
+
notes: [.../* @__PURE__ */ new Set([...conflict.blocked, member.index])].sort((a, b) => a - b),
|
|
437
|
+
startBeat: member.startBeat,
|
|
438
|
+
impossible: true,
|
|
439
|
+
message: `MIDI ${note.pitch} needs a string already sounding on ${profile.name}`
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
placement.fingering = chosen;
|
|
443
|
+
stringFreeAt[chosen.string] = member.endBeat;
|
|
444
|
+
stringHeldBy[chosen.string] = member.index;
|
|
445
|
+
state.movement += 1;
|
|
446
|
+
if (origin.string !== void 0) {
|
|
447
|
+
state.movement += Math.abs(chosen.string - origin.string);
|
|
448
|
+
}
|
|
449
|
+
origin = movedTo(origin, chosen);
|
|
450
|
+
if (chosen.fret > 0) {
|
|
451
|
+
shifted = { note: member, fingering: chosen };
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
if (hand && shifted && shifted !== hand) {
|
|
455
|
+
state.movement += Math.abs(shifted.fingering.fret - hand.fingering.fret);
|
|
456
|
+
checkShiftSpeed(state, profile, hand, shifted);
|
|
325
457
|
}
|
|
326
|
-
|
|
327
|
-
previousIndex = index;
|
|
458
|
+
hand = shifted;
|
|
328
459
|
}
|
|
329
|
-
for (const
|
|
330
|
-
checkStretch(state, profile,
|
|
331
|
-
checkPolyphony(state, profile,
|
|
460
|
+
for (const attack of attacks) {
|
|
461
|
+
checkStretch(state, profile, attack);
|
|
462
|
+
checkPolyphony(state, profile, { startBeat: attack.startBeat, members: soundingAt(attack) });
|
|
332
463
|
}
|
|
333
464
|
}
|
|
334
|
-
function
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
465
|
+
function movedTo(origin, chosen) {
|
|
466
|
+
return { string: chosen.string, fret: chosen.fret > 0 ? chosen.fret : origin.fret };
|
|
467
|
+
}
|
|
468
|
+
function fingeringCost(candidate, origin) {
|
|
469
|
+
const stopped = candidate.fret === 0 ? 0 : origin.fret === void 0 ? candidate.fret : Math.abs(candidate.fret - origin.fret);
|
|
470
|
+
const crossing = origin.string === void 0 ? 0 : STRING_CROSSING_COST * Math.abs(candidate.string - origin.string);
|
|
471
|
+
return stopped + crossing + candidate.string / 100;
|
|
339
472
|
}
|
|
340
|
-
function checkShiftSpeed(state, profile,
|
|
473
|
+
function checkShiftSpeed(state, profile, from, to) {
|
|
341
474
|
const secondsPerBeat = state.secondsPerBeat;
|
|
342
|
-
if (secondsPerBeat === void 0
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
const previousNote = state.notes[fromIndex];
|
|
346
|
-
const note = state.notes[toIndex];
|
|
347
|
-
if (!previousNote || !note) {
|
|
475
|
+
if (secondsPerBeat === void 0) {
|
|
348
476
|
return;
|
|
349
477
|
}
|
|
350
|
-
const seconds = (note.
|
|
351
|
-
const shift = Math.abs(to.fret - from.fret);
|
|
478
|
+
const seconds = (to.note.attackBeat - from.note.attackBeat) * secondsPerBeat;
|
|
479
|
+
const shift = Math.abs(to.fingering.fret - from.fingering.fret);
|
|
352
480
|
if (seconds <= BEAT_EPS || shift === 0) {
|
|
353
481
|
return;
|
|
354
482
|
}
|
|
@@ -356,15 +484,19 @@ function checkShiftSpeed(state, profile, fromIndex, toIndex, from, to) {
|
|
|
356
484
|
state.issues.push({
|
|
357
485
|
type: "tooFast",
|
|
358
486
|
layer: 3,
|
|
359
|
-
notes: [
|
|
360
|
-
startBeat: note.startBeat,
|
|
487
|
+
notes: [from.note.index, to.note.index].sort((a, b) => a - b),
|
|
488
|
+
startBeat: to.note.startBeat,
|
|
361
489
|
impossible: false,
|
|
362
490
|
message: `a ${shift}-fret shift in ${seconds.toFixed(3)}s is beyond ${profile.name}`
|
|
363
491
|
});
|
|
364
492
|
}
|
|
365
493
|
}
|
|
366
|
-
function
|
|
367
|
-
|
|
494
|
+
function soundingAt(attack) {
|
|
495
|
+
return [...attack.held, ...attack.members].map((timed) => timed.index);
|
|
496
|
+
}
|
|
497
|
+
function checkStretch(state, profile, attack) {
|
|
498
|
+
const sounding = soundingAt(attack);
|
|
499
|
+
const frets = sounding.map((member) => state.placements[member]?.fingering).filter(
|
|
368
500
|
(fingering) => fingering !== void 0 && fingering.fret > 0
|
|
369
501
|
).map((fingering) => fingering.fret);
|
|
370
502
|
if (frets.length < 2) {
|
|
@@ -375,8 +507,8 @@ function checkStretch(state, profile, group) {
|
|
|
375
507
|
state.issues.push({
|
|
376
508
|
type: "stretchTooWide",
|
|
377
509
|
layer: 2,
|
|
378
|
-
notes:
|
|
379
|
-
startBeat:
|
|
510
|
+
notes: sounding.sort((a, b) => a - b),
|
|
511
|
+
startBeat: attack.startBeat,
|
|
380
512
|
impossible: true,
|
|
381
513
|
message: `a ${span}-fret stretch exceeds the ${profile.maxStretch} frets one hand spans on ${profile.name}`
|
|
382
514
|
});
|
|
@@ -413,10 +545,10 @@ function limbsFor(kit, pitch) {
|
|
|
413
545
|
function passesAt(state, kit, members) {
|
|
414
546
|
const played = { id: "kit", members: [] };
|
|
415
547
|
const overdubs = /* @__PURE__ */ new Map();
|
|
416
|
-
for (const
|
|
417
|
-
const pitch = state.notes[index]?.pitch;
|
|
548
|
+
for (const member of members) {
|
|
549
|
+
const pitch = state.notes[member.index]?.pitch;
|
|
418
550
|
if (pitch === void 0 || !kit.overdub.has(pitch)) {
|
|
419
|
-
played.members.push(
|
|
551
|
+
played.members.push(member);
|
|
420
552
|
continue;
|
|
421
553
|
}
|
|
422
554
|
let pass = overdubs.get(pitch);
|
|
@@ -424,7 +556,7 @@ function passesAt(state, kit, members) {
|
|
|
424
556
|
pass = { id: `overdub ${pitch}`, members: [] };
|
|
425
557
|
overdubs.set(pitch, pass);
|
|
426
558
|
}
|
|
427
|
-
pass.members.push(
|
|
559
|
+
pass.members.push(member);
|
|
428
560
|
}
|
|
429
561
|
return [played, ...overdubs.values()];
|
|
430
562
|
}
|
|
@@ -432,13 +564,13 @@ function placePass(state, kit, pass, memory) {
|
|
|
432
564
|
const taken = /* @__PURE__ */ new Set();
|
|
433
565
|
let conflicted = false;
|
|
434
566
|
const members = [...pass.members].sort((a, b) => {
|
|
435
|
-
const left = limbsFor(kit, state.notes[a]?.pitch);
|
|
436
|
-
const right = limbsFor(kit, state.notes[b]?.pitch);
|
|
437
|
-
return left.length - right.length || a - b;
|
|
567
|
+
const left = limbsFor(kit, state.notes[a.index]?.pitch);
|
|
568
|
+
const right = limbsFor(kit, state.notes[b.index]?.pitch);
|
|
569
|
+
return left.length - right.length || a.index - b.index;
|
|
438
570
|
});
|
|
439
|
-
for (const
|
|
440
|
-
const note = state.notes[index];
|
|
441
|
-
const placement = state.placements[index];
|
|
571
|
+
for (const member of members) {
|
|
572
|
+
const note = state.notes[member.index];
|
|
573
|
+
const placement = state.placements[member.index];
|
|
442
574
|
if (!note || !placement) {
|
|
443
575
|
continue;
|
|
444
576
|
}
|
|
@@ -460,23 +592,24 @@ function placePass(state, kit, pass, memory) {
|
|
|
460
592
|
memory.limbForVoice.set(note.pitch, chosen);
|
|
461
593
|
state.movement += 1;
|
|
462
594
|
const strokeKey = `${pass.id} ${chosen}`;
|
|
463
|
-
const
|
|
464
|
-
if (
|
|
465
|
-
const previousNote = state.notes[
|
|
595
|
+
const previousStroke = memory.lastStroke.get(strokeKey);
|
|
596
|
+
if (previousStroke !== void 0) {
|
|
597
|
+
const previousNote = state.notes[previousStroke.index];
|
|
466
598
|
if (previousNote && previousNote.pitch !== note.pitch) {
|
|
467
599
|
state.movement += 1;
|
|
468
600
|
}
|
|
469
|
-
checkStrokeSpeed(state, kit.profile,
|
|
601
|
+
checkStrokeSpeed(state, kit.profile, previousStroke, member, chosen);
|
|
470
602
|
}
|
|
471
|
-
memory.lastStroke.set(strokeKey,
|
|
603
|
+
memory.lastStroke.set(strokeKey, member);
|
|
472
604
|
}
|
|
473
605
|
return conflicted;
|
|
474
606
|
}
|
|
475
|
-
function placeOnKit(state, kit) {
|
|
607
|
+
function placeOnKit(state, kit, attacks) {
|
|
476
608
|
const memory = { limbForVoice: /* @__PURE__ */ new Map(), lastStroke: /* @__PURE__ */ new Map() };
|
|
477
|
-
for (const
|
|
609
|
+
for (const attack of attacks) {
|
|
610
|
+
const strokes = attack.members.map((member) => member.index);
|
|
478
611
|
let conflicted = false;
|
|
479
|
-
for (const pass of passesAt(state, kit,
|
|
612
|
+
for (const pass of passesAt(state, kit, attack.members)) {
|
|
480
613
|
if (!placePass(state, kit, pass, memory)) {
|
|
481
614
|
continue;
|
|
482
615
|
}
|
|
@@ -484,65 +617,43 @@ function placeOnKit(state, kit) {
|
|
|
484
617
|
state.issues.push({
|
|
485
618
|
type: "limbConflict",
|
|
486
619
|
layer: 2,
|
|
487
|
-
notes:
|
|
488
|
-
startBeat:
|
|
620
|
+
notes: pass.members.map((member) => member.index).sort((a, b) => a - b),
|
|
621
|
+
startBeat: attack.startBeat,
|
|
489
622
|
impossible: true,
|
|
490
623
|
message: `${pass.members.length} simultaneous strokes cannot be shared among the limbs of ${kit.profile.name}`
|
|
491
624
|
});
|
|
492
625
|
}
|
|
493
626
|
if (!conflicted) {
|
|
494
|
-
checkPolyphony(state, kit.profile,
|
|
627
|
+
checkPolyphony(state, kit.profile, { startBeat: attack.startBeat, members: strokes });
|
|
495
628
|
}
|
|
496
629
|
}
|
|
497
630
|
}
|
|
498
|
-
function checkStrokeSpeed(state, profile,
|
|
631
|
+
function checkStrokeSpeed(state, profile, from, to, limb) {
|
|
499
632
|
const secondsPerBeat = state.secondsPerBeat;
|
|
500
633
|
if (secondsPerBeat === void 0) {
|
|
501
634
|
return;
|
|
502
635
|
}
|
|
503
|
-
const
|
|
504
|
-
const note = state.notes[toIndex];
|
|
505
|
-
if (!previousNote || !note) {
|
|
506
|
-
return;
|
|
507
|
-
}
|
|
508
|
-
const seconds = (note.startBeat - previousNote.startBeat) * secondsPerBeat;
|
|
636
|
+
const seconds = (to.attackBeat - from.attackBeat) * secondsPerBeat;
|
|
509
637
|
if (seconds <= BEAT_EPS || seconds >= MIN_LIMB_STROKE_SECONDS) {
|
|
510
638
|
return;
|
|
511
639
|
}
|
|
512
640
|
state.issues.push({
|
|
513
641
|
type: "tooFast",
|
|
514
642
|
layer: 3,
|
|
515
|
-
notes: [
|
|
516
|
-
startBeat:
|
|
643
|
+
notes: [from.index, to.index].sort((a, b) => a - b),
|
|
644
|
+
startBeat: to.startBeat,
|
|
517
645
|
impossible: false,
|
|
518
646
|
message: `${limb} strikes twice ${seconds.toFixed(3)}s apart on ${profile.name}`
|
|
519
647
|
});
|
|
520
648
|
}
|
|
521
|
-
function
|
|
522
|
-
const groups = [];
|
|
523
|
-
let current;
|
|
524
|
-
for (const index of state.order) {
|
|
525
|
-
const note = state.notes[index];
|
|
526
|
-
if (!note || note.durationBeat <= 0) {
|
|
527
|
-
continue;
|
|
528
|
-
}
|
|
529
|
-
if (!current || !isSimultaneous(current.startBeat, note.startBeat)) {
|
|
530
|
-
current = { startBeat: note.startBeat, members: [] };
|
|
531
|
-
groups.push(current);
|
|
532
|
-
}
|
|
533
|
-
current.members.push(index);
|
|
534
|
-
}
|
|
535
|
-
return groups;
|
|
536
|
-
}
|
|
537
|
-
function difficultyOf(state, bpm) {
|
|
649
|
+
function difficultyOf(state, attacks, bpm) {
|
|
538
650
|
let first = Number.POSITIVE_INFINITY;
|
|
539
651
|
let last = Number.NEGATIVE_INFINITY;
|
|
540
|
-
for (const
|
|
541
|
-
|
|
542
|
-
|
|
652
|
+
for (const attack of attacks) {
|
|
653
|
+
for (const member of attack.members) {
|
|
654
|
+
first = Math.min(first, member.startBeat);
|
|
655
|
+
last = Math.max(last, member.endBeat);
|
|
543
656
|
}
|
|
544
|
-
first = Math.min(first, note.startBeat);
|
|
545
|
-
last = Math.max(last, note.startBeat + Math.max(0, note.durationBeat));
|
|
546
657
|
}
|
|
547
658
|
const spanBeats = last - first;
|
|
548
659
|
if (!Number.isFinite(spanBeats) || spanBeats <= BEAT_EPS) {
|
|
@@ -668,6 +779,20 @@ function hashPath(domain, root, path) {
|
|
|
668
779
|
}
|
|
669
780
|
return fmix32(hash);
|
|
670
781
|
}
|
|
782
|
+
var stepBytes = new DataView(new ArrayBuffer(8));
|
|
783
|
+
function scaleUnitDraw(unit, lo, hi) {
|
|
784
|
+
const value = lo + unit * (hi - lo);
|
|
785
|
+
if (value < hi || lo === hi) {
|
|
786
|
+
return value;
|
|
787
|
+
}
|
|
788
|
+
if (hi === 0) {
|
|
789
|
+
return -Number.MIN_VALUE;
|
|
790
|
+
}
|
|
791
|
+
stepBytes.setFloat64(0, hi);
|
|
792
|
+
const bits = stepBytes.getBigUint64(0);
|
|
793
|
+
stepBytes.setBigUint64(0, hi > 0 ? bits - 1n : bits + 1n);
|
|
794
|
+
return stepBytes.getFloat64(0);
|
|
795
|
+
}
|
|
671
796
|
|
|
672
797
|
// src/core/random/positional.ts
|
|
673
798
|
function createPositionalRng(seed) {
|
|
@@ -744,7 +869,7 @@ function createRng(seed) {
|
|
|
744
869
|
);
|
|
745
870
|
}
|
|
746
871
|
assertFiniteNumber(hi - lo, "float span");
|
|
747
|
-
return
|
|
872
|
+
return scaleUnitDraw(next(), lo, hi);
|
|
748
873
|
}
|
|
749
874
|
};
|
|
750
875
|
}
|
|
@@ -766,6 +891,7 @@ export {
|
|
|
766
891
|
BASS_5_STRING,
|
|
767
892
|
GUITAR_STANDARD,
|
|
768
893
|
GUITAR_DROP_D,
|
|
894
|
+
scaleUnitDraw,
|
|
769
895
|
createPositionalRng,
|
|
770
896
|
includeAt,
|
|
771
897
|
deriveSeed,
|
|
@@ -774,4 +900,4 @@ export {
|
|
|
774
900
|
resolveAlgorithmVersion,
|
|
775
901
|
createRng
|
|
776
902
|
};
|
|
777
|
-
//# sourceMappingURL=chunk-
|
|
903
|
+
//# sourceMappingURL=chunk-VL5NXVCN.js.map
|