@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.
Files changed (130) hide show
  1. package/NOTICE +63 -0
  2. package/README.md +3 -1
  3. package/README_ja.md +13 -11
  4. package/dist/analyze/index.cjs +7 -7
  5. package/dist/analyze/index.cjs.map +1 -1
  6. package/dist/analyze/index.d.cts +14 -12
  7. package/dist/analyze/index.d.ts +14 -12
  8. package/dist/analyze/index.js +6 -6
  9. package/dist/{chunk-NB25P3EH.js → chunk-2G5MCGYU.js} +33 -11
  10. package/dist/chunk-2G5MCGYU.js.map +1 -0
  11. package/dist/{chunk-NM7UAMC3.cjs → chunk-3FDBMIPR.cjs} +64 -64
  12. package/dist/chunk-3FDBMIPR.cjs.map +1 -0
  13. package/dist/{chunk-YFA7X2F6.cjs → chunk-3MDOEFIQ.cjs} +411 -327
  14. package/dist/chunk-3MDOEFIQ.cjs.map +1 -0
  15. package/dist/{chunk-IPEF6NWB.js → chunk-5MWRBYAT.js} +278 -154
  16. package/dist/chunk-5MWRBYAT.js.map +1 -0
  17. package/dist/{chunk-B2PW6EHN.cjs → chunk-6TTBEON3.cjs} +1253 -889
  18. package/dist/chunk-6TTBEON3.cjs.map +1 -0
  19. package/dist/{chunk-UMWZ4ZET.js → chunk-7NQ6AL3H.js} +4 -4
  20. package/dist/chunk-7NQ6AL3H.js.map +1 -0
  21. package/dist/{chunk-PWC4CRWT.cjs → chunk-E3QO3UET.cjs} +31 -23
  22. package/dist/chunk-E3QO3UET.cjs.map +1 -0
  23. package/dist/{chunk-GNVN4PPJ.js → chunk-FJFULJZE.js} +531 -213
  24. package/dist/chunk-FJFULJZE.js.map +1 -0
  25. package/dist/{chunk-CPMB7P75.js → chunk-FK3DGAIZ.js} +608 -244
  26. package/dist/chunk-FK3DGAIZ.js.map +1 -0
  27. package/dist/chunk-GEDTJHAC.cjs.map +1 -1
  28. package/dist/{chunk-RFIBF2RR.js → chunk-H7FVMBUC.js} +138 -65
  29. package/dist/chunk-H7FVMBUC.js.map +1 -0
  30. package/dist/{chunk-4R2LWV7E.cjs → chunk-HYSVSFNI.cjs} +664 -540
  31. package/dist/chunk-HYSVSFNI.cjs.map +1 -0
  32. package/dist/{chunk-35NBV2FT.cjs → chunk-KMYGRFZ6.cjs} +47 -24
  33. package/dist/chunk-KMYGRFZ6.cjs.map +1 -0
  34. package/dist/{chunk-IFZDWIKM.cjs → chunk-LQZQDBCD.cjs} +1048 -730
  35. package/dist/chunk-LQZQDBCD.cjs.map +1 -0
  36. package/dist/{chunk-PM4XJE2Y.cjs → chunk-P3CPXAJ5.cjs} +76 -54
  37. package/dist/chunk-P3CPXAJ5.cjs.map +1 -0
  38. package/dist/{chunk-S5BKZZGM.cjs → chunk-S3QELJUE.cjs} +72 -56
  39. package/dist/chunk-S3QELJUE.cjs.map +1 -0
  40. package/dist/{chunk-PUKJP2Z5.js → chunk-TXFWSU5D.js} +31 -8
  41. package/dist/chunk-TXFWSU5D.js.map +1 -0
  42. package/dist/{chunk-76ZBTRG2.cjs → chunk-U4EQTDGN.cjs} +258 -218
  43. package/dist/chunk-U4EQTDGN.cjs.map +1 -0
  44. package/dist/chunk-UGLFDG7L.cjs.map +1 -1
  45. package/dist/{chunk-MLBPXA4Z.js → chunk-UTE5A36R.js} +198 -114
  46. package/dist/chunk-UTE5A36R.js.map +1 -0
  47. package/dist/{chunk-ZBUROTJQ.js → chunk-UZKQISRF.js} +17 -9
  48. package/dist/chunk-UZKQISRF.js.map +1 -0
  49. package/dist/{chunk-7KG3RTYS.js → chunk-VL5NXVCN.js} +281 -155
  50. package/dist/chunk-VL5NXVCN.js.map +1 -0
  51. package/dist/{chunk-CGBADIO5.cjs → chunk-WOMCXN2W.cjs} +328 -202
  52. package/dist/chunk-WOMCXN2W.cjs.map +1 -0
  53. package/dist/{chunk-3R3QVKY5.cjs → chunk-YDGMWH43.cjs} +397 -324
  54. package/dist/chunk-YDGMWH43.cjs.map +1 -0
  55. package/dist/{chunk-AGOVW7U6.js → chunk-YYVXRV3V.js} +72 -56
  56. package/dist/chunk-YYVXRV3V.js.map +1 -0
  57. package/dist/{chunk-M5RYMAFI.js → chunk-ZLNSH4UL.js} +71 -31
  58. package/dist/chunk-ZLNSH4UL.js.map +1 -0
  59. package/dist/{coerce-DRERzdve.d.cts → coerce-CUhHc3Tu.d.ts} +16 -8
  60. package/dist/{coerce-DP0vYu-x.d.ts → coerce-Ytt2Eiq9.d.cts} +16 -8
  61. package/dist/core/index.cjs +5 -5
  62. package/dist/core/index.cjs.map +1 -1
  63. package/dist/core/index.d.cts +9 -8
  64. package/dist/core/index.d.ts +9 -8
  65. package/dist/core/index.js +4 -4
  66. package/dist/generate/index.cjs +8 -8
  67. package/dist/generate/index.cjs.map +1 -1
  68. package/dist/generate/index.d.cts +28 -18
  69. package/dist/generate/index.d.ts +28 -18
  70. package/dist/generate/index.js +7 -7
  71. package/dist/{index-BMweUlm5.d.cts → index-5VJgKz_Q.d.cts} +6 -2
  72. package/dist/{index-BHIUyjhZ.d.ts → index-BLuDesVG.d.ts} +20 -13
  73. package/dist/{index-B9QLziDe.d.cts → index-BV6APVa6.d.cts} +20 -13
  74. package/dist/{index-CL4b4gUQ.d.ts → index-Bf0Sv8a9.d.ts} +6 -2
  75. package/dist/{index-Dw2DP7vm.d.ts → index-BkT21EMv.d.cts} +3 -2
  76. package/dist/{index-BL5N2Oyz.d.cts → index-C7KCaTRz.d.ts} +3 -2
  77. package/dist/{index-Cy15oTvh.d.ts → index-C9MYbuKS.d.cts} +7 -3
  78. package/dist/{index-Cy15oTvh.d.cts → index-C9MYbuKS.d.ts} +7 -3
  79. package/dist/{index-CY-3NVfy.d.cts → index-DVInzCDS.d.cts} +53 -29
  80. package/dist/{index-9eoQs2C1.d.ts → index-Dg-SAfhz.d.ts} +6 -4
  81. package/dist/{index-zaZdmnU4.d.ts → index-jgeug9EA.d.ts} +53 -29
  82. package/dist/{index-cHvbYgJI.d.cts → index-pQihL4z7.d.cts} +6 -4
  83. package/dist/index.cjs +13 -13
  84. package/dist/index.cjs.map +1 -1
  85. package/dist/index.d.cts +11 -11
  86. package/dist/index.d.ts +11 -11
  87. package/dist/index.js +12 -12
  88. package/dist/{masks-Cc7-MzzY.d.ts → masks-BnzXnBj0.d.cts} +3 -2
  89. package/dist/{masks-pADfTOiD.d.cts → masks-tycmkrgs.d.ts} +3 -2
  90. package/dist/model/index.cjs +12 -12
  91. package/dist/model/index.cjs.map +1 -1
  92. package/dist/model/index.d.cts +63 -48
  93. package/dist/model/index.d.ts +63 -48
  94. package/dist/model/index.js +11 -11
  95. package/dist/{positional-DJgs6ltD.d.ts → positional-CscpxtlE.d.cts} +3 -2
  96. package/dist/{positional-DMZer-FM.d.cts → positional-DLnfUgqG.d.ts} +3 -2
  97. package/dist/{profile-DLdlIpS9.d.ts → profile-B4WqcM4H.d.ts} +25 -17
  98. package/dist/{profile-CQMEqdH9.d.cts → profile-BJd1XkPB.d.cts} +25 -17
  99. package/dist/theory/index.cjs +5 -5
  100. package/dist/theory/index.cjs.map +1 -1
  101. package/dist/theory/index.d.cts +18 -14
  102. package/dist/theory/index.d.ts +18 -14
  103. package/dist/theory/index.js +4 -4
  104. package/dist/{types-D-XgxGvp.d.cts → types-7mGFwvMm.d.ts} +45 -22
  105. package/dist/{types-Dh7C064C.d.ts → types-_ofpb2be.d.cts} +45 -22
  106. package/package.json +13 -13
  107. package/dist/chunk-35NBV2FT.cjs.map +0 -1
  108. package/dist/chunk-3R3QVKY5.cjs.map +0 -1
  109. package/dist/chunk-4R2LWV7E.cjs.map +0 -1
  110. package/dist/chunk-76ZBTRG2.cjs.map +0 -1
  111. package/dist/chunk-7KG3RTYS.js.map +0 -1
  112. package/dist/chunk-AGOVW7U6.js.map +0 -1
  113. package/dist/chunk-B2PW6EHN.cjs.map +0 -1
  114. package/dist/chunk-CGBADIO5.cjs.map +0 -1
  115. package/dist/chunk-CPMB7P75.js.map +0 -1
  116. package/dist/chunk-GNVN4PPJ.js.map +0 -1
  117. package/dist/chunk-IFZDWIKM.cjs.map +0 -1
  118. package/dist/chunk-IPEF6NWB.js.map +0 -1
  119. package/dist/chunk-M5RYMAFI.js.map +0 -1
  120. package/dist/chunk-MLBPXA4Z.js.map +0 -1
  121. package/dist/chunk-NB25P3EH.js.map +0 -1
  122. package/dist/chunk-NM7UAMC3.cjs.map +0 -1
  123. package/dist/chunk-PM4XJE2Y.cjs.map +0 -1
  124. package/dist/chunk-PUKJP2Z5.js.map +0 -1
  125. package/dist/chunk-PWC4CRWT.cjs.map +0 -1
  126. package/dist/chunk-RFIBF2RR.js.map +0 -1
  127. package/dist/chunk-S5BKZZGM.cjs.map +0 -1
  128. package/dist/chunk-UMWZ4ZET.js.map +0 -1
  129. package/dist/chunk-YFA7X2F6.cjs.map +0 -1
  130. 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-AGOVW7U6.js";
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
- for (const pitch of profile.overdub ?? []) {
87
- assertMidiPitch(pitch, `${profile.name} overdub pitch`);
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 isSimultaneous(anchorBeat, startBeat) {
249
- return startBeat - anchorBeat <= SIMULTANEITY_BEATS + BEAT_EPS;
258
+ function isReleased(endBeat, beat) {
259
+ return endBeat <= beat + LEGATO_OVERLAP_BEATS + BEAT_EPS;
250
260
  }
251
- function soundingGroups(state) {
252
- const groups = [];
261
+ function timelineOf(state) {
262
+ const sounding = new Set(soundingIndices(state.notes));
263
+ const attacks = [];
253
264
  let active = [];
254
- let cursor = 0;
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 (anchor !== void 0 && isSimultaneous(anchor, note.startBeat)) {
262
- continue;
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 at = note.startBeat;
265
- anchor = at;
266
- active = active.filter((member) => {
267
- const held = state.notes[member];
268
- return held !== void 0 && held.startBeat + held.durationBeat > at + BEAT_EPS;
269
- });
270
- while (cursor < state.order.length) {
271
- const candidateIndex = state.order[cursor];
272
- const candidate = candidateIndex === void 0 ? void 0 : state.notes[candidateIndex];
273
- if (!candidate || !isSimultaneous(at, candidate.startBeat)) {
274
- break;
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
- if (candidateIndex !== void 0 && candidate.startBeat + candidate.durationBeat > at + BEAT_EPS) {
277
- active.push(candidateIndex);
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
- groups.push({ startBeat: at, members: [...active] });
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 groups;
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 previous;
289
- let previousIndex;
290
- for (const index of state.order) {
291
- const note = state.notes[index];
292
- const placement = state.placements[index];
293
- if (!note || !placement) {
294
- continue;
295
- }
296
- const candidates = fingeringsIn(profile, note.pitch);
297
- if (candidates.length === 0) {
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 pool = free.length > 0 ? free : candidates;
315
- const chosen = pool.reduce(
316
- (best, candidate) => fingeringCost(candidate, previous) < fingeringCost(best, previous) ? candidate : best
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
- placement.fingering = chosen;
319
- stringFreeAt[chosen.string] = note.startBeat + note.durationBeat;
320
- stringHeldBy[chosen.string] = index;
321
- state.movement += 1;
322
- if (previous) {
323
- state.movement += Math.abs(chosen.fret - previous.fret) + Math.abs(chosen.string - previous.string);
324
- checkShiftSpeed(state, profile, previousIndex, index, previous, chosen);
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
- previous = chosen;
327
- previousIndex = index;
458
+ hand = shifted;
328
459
  }
329
- for (const group of soundingGroups(state)) {
330
- checkStretch(state, profile, group);
331
- checkPolyphony(state, profile, group);
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 fingeringCost(candidate, previous) {
335
- if (!previous) {
336
- return candidate.fret + candidate.string / 100;
337
- }
338
- return Math.abs(candidate.fret - previous.fret) + STRING_CROSSING_COST * Math.abs(candidate.string - previous.string) + candidate.string / 100;
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, fromIndex, toIndex, from, to) {
473
+ function checkShiftSpeed(state, profile, from, to) {
341
474
  const secondsPerBeat = state.secondsPerBeat;
342
- if (secondsPerBeat === void 0 || fromIndex === 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.startBeat - previousNote.startBeat) * secondsPerBeat;
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: [fromIndex, toIndex].sort((a, b) => a - b),
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 checkStretch(state, profile, group) {
367
- const frets = group.members.map((member) => state.placements[member]?.fingering).filter(
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: [...group.members].sort((a, b) => a - b),
379
- startBeat: group.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 index of members) {
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(index);
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(index);
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 index of members) {
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 previousIndex = memory.lastStroke.get(strokeKey);
464
- if (previousIndex !== void 0) {
465
- const previousNote = state.notes[previousIndex];
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, previousIndex, index, chosen);
601
+ checkStrokeSpeed(state, kit.profile, previousStroke, member, chosen);
470
602
  }
471
- memory.lastStroke.set(strokeKey, index);
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 group of onsetGroups(state)) {
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, group.members)) {
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: [...pass.members].sort((a, b) => a - b),
488
- startBeat: group.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, group);
627
+ checkPolyphony(state, kit.profile, { startBeat: attack.startBeat, members: strokes });
495
628
  }
496
629
  }
497
630
  }
498
- function checkStrokeSpeed(state, profile, fromIndex, toIndex, limb) {
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 previousNote = state.notes[fromIndex];
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: [fromIndex, toIndex].sort((a, b) => a - b),
516
- startBeat: note.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 onsetGroups(state) {
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 note of state.notes) {
541
- if (!note) {
542
- continue;
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 lo + next() * (hi - lo);
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-7KG3RTYS.js.map
903
+ //# sourceMappingURL=chunk-VL5NXVCN.js.map