@libraz/libcantus 1.0.1 → 1.2.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 (142) hide show
  1. package/README.md +27 -65
  2. package/README_ja.md +28 -66
  3. package/dist/analyze/index.cjs +11 -7
  4. package/dist/analyze/index.cjs.map +1 -1
  5. package/dist/analyze/index.d.cts +79 -144
  6. package/dist/analyze/index.d.ts +79 -144
  7. package/dist/analyze/index.js +20 -16
  8. package/dist/chunk-35NBV2FT.cjs +145 -0
  9. package/dist/chunk-35NBV2FT.cjs.map +1 -0
  10. package/dist/{chunk-UADYAYAT.cjs → chunk-3R3QVKY5.cjs} +1353 -580
  11. package/dist/chunk-3R3QVKY5.cjs.map +1 -0
  12. package/dist/{chunk-WWURE4XT.cjs → chunk-76ZBTRG2.cjs} +624 -729
  13. package/dist/chunk-76ZBTRG2.cjs.map +1 -0
  14. package/dist/{chunk-FVSI36UV.js → chunk-7KG3RTYS.js} +197 -110
  15. package/dist/chunk-7KG3RTYS.js.map +1 -0
  16. package/dist/{chunk-VTI6FOLB.js → chunk-AGOVW7U6.js} +709 -133
  17. package/dist/chunk-AGOVW7U6.js.map +1 -0
  18. package/dist/{chunk-E3F4AHAH.cjs → chunk-CGBADIO5.cjs} +221 -134
  19. package/dist/chunk-CGBADIO5.cjs.map +1 -0
  20. package/dist/{chunk-6XH6QZYQ.cjs → chunk-DSMJRXCZ.cjs} +1455 -849
  21. package/dist/chunk-DSMJRXCZ.cjs.map +1 -0
  22. package/dist/{chunk-PKFNUHDY.js → chunk-FBH75DAY.js} +1218 -612
  23. package/dist/chunk-FBH75DAY.js.map +1 -0
  24. package/dist/chunk-H2KJHBW4.js +3329 -0
  25. package/dist/chunk-H2KJHBW4.js.map +1 -0
  26. package/dist/chunk-HPVSH7SF.cjs +2294 -0
  27. package/dist/chunk-HPVSH7SF.cjs.map +1 -0
  28. package/dist/{chunk-MXO72PLY.cjs → chunk-HWHGOVO6.cjs} +21 -19
  29. package/dist/chunk-HWHGOVO6.cjs.map +1 -0
  30. package/dist/chunk-HYJJ2V2I.js +2294 -0
  31. package/dist/chunk-HYJJ2V2I.js.map +1 -0
  32. package/dist/{chunk-AFM2ULJH.cjs → chunk-J7FHVZII.cjs} +1567 -1067
  33. package/dist/chunk-J7FHVZII.cjs.map +1 -0
  34. package/dist/chunk-K6OZXIR6.js +1 -0
  35. package/dist/{chunk-H3VORETI.js → chunk-M5RYMAFI.js} +487 -592
  36. package/dist/chunk-M5RYMAFI.js.map +1 -0
  37. package/dist/chunk-NB25P3EH.js +363 -0
  38. package/dist/chunk-NB25P3EH.js.map +1 -0
  39. package/dist/{chunk-TTCLJX6D.js → chunk-NFJ4P6ZZ.js} +10 -8
  40. package/dist/chunk-NFJ4P6ZZ.js.map +1 -0
  41. package/dist/chunk-NM7UAMC3.cjs +457 -0
  42. package/dist/chunk-NM7UAMC3.cjs.map +1 -0
  43. package/dist/chunk-PM4XJE2Y.cjs +363 -0
  44. package/dist/chunk-PM4XJE2Y.cjs.map +1 -0
  45. package/dist/chunk-PUKJP2Z5.js +145 -0
  46. package/dist/chunk-PUKJP2Z5.js.map +1 -0
  47. package/dist/{chunk-VOHZB45S.js → chunk-Q6LFSAKV.js} +1321 -821
  48. package/dist/chunk-Q6LFSAKV.js.map +1 -0
  49. package/dist/{chunk-INHVXP6Z.js → chunk-RFIBF2RR.js} +1203 -430
  50. package/dist/chunk-RFIBF2RR.js.map +1 -0
  51. package/dist/{chunk-Q2WGKJNN.cjs → chunk-S5BKZZGM.cjs} +699 -123
  52. package/dist/chunk-S5BKZZGM.cjs.map +1 -0
  53. package/dist/chunk-SGLEQVJH.cjs +3329 -0
  54. package/dist/chunk-SGLEQVJH.cjs.map +1 -0
  55. package/dist/chunk-UGLFDG7L.cjs +1 -0
  56. package/dist/chunk-UGLFDG7L.cjs.map +1 -0
  57. package/dist/{chunk-YH3BIKHH.js → chunk-UMWZ4ZET.js} +146 -49
  58. package/dist/chunk-UMWZ4ZET.js.map +1 -0
  59. package/dist/{index-szBv3496.d.ts → coerce-DP0vYu-x.d.ts} +286 -199
  60. package/dist/{index-C_8o--VE.d.cts → coerce-DRERzdve.d.cts} +286 -199
  61. package/dist/core/index.cjs +9 -5
  62. package/dist/core/index.cjs.map +1 -1
  63. package/dist/core/index.d.cts +122 -108
  64. package/dist/core/index.d.ts +122 -108
  65. package/dist/core/index.js +32 -28
  66. package/dist/generate/index.cjs +8 -8
  67. package/dist/generate/index.d.cts +168 -84
  68. package/dist/generate/index.d.ts +168 -84
  69. package/dist/generate/index.js +7 -7
  70. package/dist/{index-CGpRv5sl.d.cts → index-9eoQs2C1.d.ts} +34 -19
  71. package/dist/{index-CCNJRG-P.d.cts → index-B9QLziDe.d.cts} +293 -56
  72. package/dist/{index-Dzr13q3d.d.cts → index-BBWNZS0f.d.cts} +135 -58
  73. package/dist/{index-9wcMhG73.d.ts → index-BHIUyjhZ.d.ts} +293 -56
  74. package/dist/{index-BGFW-8P2.d.cts → index-BJgP97N1.d.ts} +196 -114
  75. package/dist/{index-CunYKLiu.d.ts → index-BL5N2Oyz.d.cts} +1 -1
  76. package/dist/{index-Cj49F892.d.ts → index-BMweUlm5.d.cts} +63 -23
  77. package/dist/{index-CgHqxdps.d.ts → index-BNSM-DSk.d.cts} +196 -114
  78. package/dist/{index-D57QRbVn.d.ts → index-BRMbPkHE.d.ts} +480 -254
  79. package/dist/{index-Chbst3Jx.d.ts → index-CJBBiAmU.d.ts} +135 -58
  80. package/dist/{index-BkGmlyI3.d.cts → index-CL4b4gUQ.d.ts} +63 -23
  81. package/dist/{index-B9MNZboQ.d.ts → index-Cy15oTvh.d.cts} +52 -22
  82. package/dist/{index-B9MNZboQ.d.cts → index-Cy15oTvh.d.ts} +52 -22
  83. package/dist/{index-BO6xE9YU.d.cts → index-DGCNKAUr.d.cts} +480 -254
  84. package/dist/{index-B4o55NDr.d.cts → index-Dw2DP7vm.d.ts} +1 -1
  85. package/dist/{index-G1uwGGWQ.d.ts → index-cHvbYgJI.d.cts} +34 -19
  86. package/dist/index.cjs +42 -14
  87. package/dist/index.cjs.map +1 -1
  88. package/dist/index.d.cts +14 -14
  89. package/dist/index.d.ts +14 -14
  90. package/dist/index.js +82 -54
  91. package/dist/{masks-DYbE485J.d.cts → masks-Cc7-MzzY.d.ts} +49 -2
  92. package/dist/{masks-DYbE485J.d.ts → masks-pADfTOiD.d.cts} +49 -2
  93. package/dist/model/index.cjs +19 -12
  94. package/dist/model/index.cjs.map +1 -1
  95. package/dist/model/index.d.cts +504 -182
  96. package/dist/model/index.d.ts +504 -182
  97. package/dist/model/index.js +18 -11
  98. package/dist/{positional-Cptlj-xs.d.ts → positional-DJgs6ltD.d.ts} +85 -8
  99. package/dist/{positional-DvH_sd8u.d.cts → positional-DMZer-FM.d.cts} +85 -8
  100. package/dist/theory/index.cjs +35 -6
  101. package/dist/theory/index.cjs.map +1 -1
  102. package/dist/theory/index.d.cts +257 -62
  103. package/dist/theory/index.d.ts +257 -62
  104. package/dist/theory/index.js +41 -12
  105. package/package.json +7 -5
  106. package/dist/chunk-2LCPM3EG.js +0 -3224
  107. package/dist/chunk-2LCPM3EG.js.map +0 -1
  108. package/dist/chunk-6XH6QZYQ.cjs.map +0 -1
  109. package/dist/chunk-6XPTBE7Y.cjs +0 -49
  110. package/dist/chunk-6XPTBE7Y.cjs.map +0 -1
  111. package/dist/chunk-AFM2ULJH.cjs.map +0 -1
  112. package/dist/chunk-B6XNKWZV.cjs +0 -3224
  113. package/dist/chunk-B6XNKWZV.cjs.map +0 -1
  114. package/dist/chunk-E3F4AHAH.cjs.map +0 -1
  115. package/dist/chunk-ELCTZUJJ.js +0 -379
  116. package/dist/chunk-ELCTZUJJ.js.map +0 -1
  117. package/dist/chunk-FVSI36UV.js.map +0 -1
  118. package/dist/chunk-H3VORETI.js.map +0 -1
  119. package/dist/chunk-H42UHPYW.js +0 -1577
  120. package/dist/chunk-H42UHPYW.js.map +0 -1
  121. package/dist/chunk-HFPS7JFD.js +0 -1
  122. package/dist/chunk-INHVXP6Z.js.map +0 -1
  123. package/dist/chunk-K4ZJUGF4.cjs +0 -379
  124. package/dist/chunk-K4ZJUGF4.cjs.map +0 -1
  125. package/dist/chunk-KNJEKLKV.js +0 -49
  126. package/dist/chunk-KNJEKLKV.js.map +0 -1
  127. package/dist/chunk-MXO72PLY.cjs.map +0 -1
  128. package/dist/chunk-NLEU4QSE.cjs +0 -360
  129. package/dist/chunk-NLEU4QSE.cjs.map +0 -1
  130. package/dist/chunk-OE6WK2Y2.cjs +0 -1577
  131. package/dist/chunk-OE6WK2Y2.cjs.map +0 -1
  132. package/dist/chunk-PKFNUHDY.js.map +0 -1
  133. package/dist/chunk-Q2WGKJNN.cjs.map +0 -1
  134. package/dist/chunk-RHIJ3S4I.cjs +0 -1
  135. package/dist/chunk-RHIJ3S4I.cjs.map +0 -1
  136. package/dist/chunk-TTCLJX6D.js.map +0 -1
  137. package/dist/chunk-UADYAYAT.cjs.map +0 -1
  138. package/dist/chunk-VOHZB45S.js.map +0 -1
  139. package/dist/chunk-VTI6FOLB.js.map +0 -1
  140. package/dist/chunk-WWURE4XT.cjs.map +0 -1
  141. package/dist/chunk-YH3BIKHH.js.map +0 -1
  142. /package/dist/{chunk-HFPS7JFD.js.map → chunk-K6OZXIR6.js.map} +0 -0
@@ -1,26 +1,16 @@
1
1
  import {
2
+ ARTICULATIONS,
3
+ BEAT_EPS,
2
4
  InvalidInputError,
3
5
  assertFiniteNumber,
4
6
  assertInteger,
7
+ assertMidiPitch,
5
8
  assertNoteEvents,
6
- assertRange
7
- } from "./chunk-VTI6FOLB.js";
8
-
9
- // src/core/instrument/articulation.ts
10
- var ARTICULATIONS = Object.freeze([
11
- "accent",
12
- "ghost",
13
- "staccato",
14
- "legato",
15
- "slide",
16
- "hammer",
17
- "mute",
18
- "flam",
19
- "drag",
20
- "roll",
21
- "choke",
22
- "open"
23
- ]);
9
+ assertOneOf,
10
+ assertPositiveInt,
11
+ assertRange,
12
+ describeRejected
13
+ } from "./chunk-AGOVW7U6.js";
24
14
 
25
15
  // src/core/instrument/profile.ts
26
16
  var LIMBS = Object.freeze(["rightHand", "leftHand", "rightFoot", "leftFoot"]);
@@ -47,19 +37,55 @@ function toStringedProfile(value, name = "instrument") {
47
37
  }
48
38
  return profile;
49
39
  }
40
+ var MAX_FRETS = 127;
41
+ var MAX_POLYPHONY = 128;
50
42
  function assertProfile(profile) {
43
+ if (typeof profile.name !== "string" || profile.name === "") {
44
+ throw new InvalidInputError(
45
+ `instrument name must be a non-empty string; received ${describeRejected(profile.name)}`
46
+ );
47
+ }
48
+ if (!Array.isArray(profile.articulations)) {
49
+ throw new InvalidInputError(
50
+ `${profile.name} articulations must be an array; received ${describeRejected(profile.articulations)}`
51
+ );
52
+ }
53
+ for (const [index, articulation] of profile.articulations.entries()) {
54
+ assertOneOf(articulation, ARTICULATIONS, `${profile.name} articulations[${index}]`);
55
+ }
56
+ assertPositiveInt(profile.polyphony, `${profile.name} polyphony`, MAX_POLYPHONY);
51
57
  if (profile.kind === "stringed") {
52
- if (profile.tuning.length === 0) {
58
+ if (!Array.isArray(profile.tuning) || profile.tuning.length === 0) {
53
59
  throw new InvalidInputError(`${profile.name} must have at least one string`);
54
60
  }
55
- if (!Number.isInteger(profile.frets) || profile.frets < 0) {
56
- throw new InvalidInputError(`${profile.name} frets must be a non-negative integer`);
61
+ for (const [index, open] of profile.tuning.entries()) {
62
+ assertMidiPitch(open, `${profile.name} tuning[${index}]`);
57
63
  }
64
+ assertInteger(profile.frets, `${profile.name} frets`, 0, MAX_FRETS);
65
+ assertInteger(profile.maxStretch, `${profile.name} maxStretch`, 0, MAX_FRETS);
58
66
  return;
59
67
  }
60
- if (Object.keys(profile.reach).length === 0) {
68
+ if (!Array.isArray(profile.limbs) || profile.limbs.length === 0) {
69
+ throw new InvalidInputError(`${profile.name} must have at least one limb`);
70
+ }
71
+ for (const [index, limb] of profile.limbs.entries()) {
72
+ assertOneOf(limb, LIMBS, `${profile.name} limbs[${index}]`);
73
+ }
74
+ if (typeof profile.reach !== "object" || profile.reach === null) {
75
+ throw new InvalidInputError(
76
+ `${profile.name} reach must be a table of limbs by voice; received ${describeRejected(profile.reach)}`
77
+ );
78
+ }
79
+ const voices = Object.keys(profile.reach);
80
+ for (const key of voices) {
81
+ assertMidiPitch(Number(key), `${profile.name} reach pitch`);
82
+ }
83
+ if (voices.every((key) => reachOf(profile, Number(key)).length === 0)) {
61
84
  throw new InvalidInputError(`${profile.name} must have at least one voice within reach`);
62
85
  }
86
+ for (const pitch of profile.overdub ?? []) {
87
+ assertMidiPitch(pitch, `${profile.name} overdub pitch`);
88
+ }
63
89
  }
64
90
  function reachOf(profile, pitch) {
65
91
  if (pitch === void 0) {
@@ -68,7 +94,9 @@ function reachOf(profile, pitch) {
68
94
  return (profile.reach[pitch] ?? []).filter((limb) => profile.limbs.includes(limb));
69
95
  }
70
96
  function instrumentRange(profile) {
71
- assertProfile(profile);
97
+ return rangeIn(toInstrumentProfile(profile));
98
+ }
99
+ function rangeIn(profile) {
72
100
  if (profile.kind === "stringed") {
73
101
  let low2 = Number.POSITIVE_INFINITY;
74
102
  let high2 = Number.NEGATIVE_INFINITY;
@@ -91,43 +119,56 @@ function instrumentRange(profile) {
91
119
  return { low, high };
92
120
  }
93
121
  function canSound(profile, pitch) {
94
- assertProfile(profile);
122
+ const resolved = toInstrumentProfile(profile, "instrument");
123
+ assertMidiPitch(pitch, "pitch");
124
+ return soundsIn(resolved, pitch);
125
+ }
126
+ function soundsIn(profile, pitch) {
95
127
  if (profile.kind === "stringed") {
96
128
  return profile.tuning.some((open) => pitch >= open && pitch - open <= profile.frets);
97
129
  }
98
130
  return reachOf(profile, pitch).length > 0;
99
131
  }
100
132
  function fingeringsFor(profile, pitch) {
101
- assertProfile(profile);
133
+ const stringed = toStringedProfile(profile, "instrument");
134
+ assertMidiPitch(pitch, "pitch");
135
+ return fingeringsIn(stringed, pitch);
136
+ }
137
+ function fingeringsIn(stringed, pitch) {
102
138
  const out = [];
103
- for (let string = 0; string < profile.tuning.length; string += 1) {
104
- const open = profile.tuning[string];
139
+ for (let string = 0; string < stringed.tuning.length; string += 1) {
140
+ const open = stringed.tuning[string];
105
141
  if (open === void 0) {
106
142
  continue;
107
143
  }
108
144
  const fret = pitch - open;
109
- if (fret >= 0 && fret <= profile.frets) {
145
+ if (fret >= 0 && fret <= stringed.frets) {
110
146
  out.push({ string, fret });
111
147
  }
112
148
  }
113
149
  return out;
114
150
  }
115
151
  function foldIntoRange(pitch, profile) {
116
- if (canSound(profile, pitch)) {
152
+ const resolved = toInstrumentProfile(profile, "instrument");
153
+ assertMidiPitch(pitch, "pitch");
154
+ return foldIn(pitch, resolved);
155
+ }
156
+ function foldIn(pitch, resolved) {
157
+ if (soundsIn(resolved, pitch)) {
117
158
  return pitch;
118
159
  }
119
- const { low, high } = instrumentRange(profile);
120
- for (let candidate = pitch + 12; candidate <= high; candidate += 12) {
121
- if (canSound(profile, candidate)) {
122
- return candidate;
160
+ const { low, high } = rangeIn(resolved);
161
+ const first = pitch + 12 * Math.ceil((low - pitch) / 12);
162
+ let best;
163
+ for (let candidate = first; candidate <= high; candidate += 12) {
164
+ if (!soundsIn(resolved, candidate)) {
165
+ continue;
123
166
  }
124
- }
125
- for (let candidate = pitch - 12; candidate >= low; candidate -= 12) {
126
- if (canSound(profile, candidate)) {
127
- return candidate;
167
+ if (best === void 0 || Math.abs(candidate - pitch) <= Math.abs(best - pitch)) {
168
+ best = candidate;
128
169
  }
129
170
  }
130
- return pitch;
171
+ return best ?? pitch;
131
172
  }
132
173
 
133
174
  // src/core/instrument/playability.ts
@@ -138,13 +179,13 @@ var DIFFICULTY_SCALE = 12;
138
179
  var MIN_DIFFICULTY = 1;
139
180
  var MAX_DIFFICULTY = 5;
140
181
  var STRING_CROSSING_COST = 2;
141
- var ONSET_GRID = 128;
142
- var EPS = 1e-9;
182
+ var SIMULTANEITY_BEATS = 1 / 128;
143
183
  function playability(notes, profile, bpm) {
144
184
  assertNoteEvents(notes, "playability notes", { allowNonPositiveDuration: true });
145
185
  if (bpm !== void 0) {
146
186
  assertRange(bpm, Number.MIN_VALUE, 1e3, "playability bpm");
147
187
  }
188
+ const instrument = toInstrumentProfile(profile, "playability profile");
148
189
  const order = notes.map((_, index) => index).sort((a, b) => {
149
190
  const left = notes[a];
150
191
  const right = notes[b];
@@ -161,11 +202,13 @@ function playability(notes, profile, bpm) {
161
202
  movement: 0,
162
203
  secondsPerBeat: bpm === void 0 ? void 0 : 60 / bpm
163
204
  };
164
- checkExistence(state, profile);
165
- if (profile.kind === "stringed") {
166
- placeOnNeck(state, profile);
205
+ if (instrument.kind === "stringed") {
206
+ checkExistence(state, instrument, (pitch) => soundsIn(instrument, pitch));
207
+ placeOnNeck(state, instrument);
167
208
  } else {
168
- placeOnKit(state, profile);
209
+ const kit = readKit(instrument);
210
+ checkExistence(state, instrument, (pitch) => limbsFor(kit, pitch).length > 0);
211
+ placeOnKit(state, kit);
169
212
  }
170
213
  return {
171
214
  difficulty: difficultyOf(state, bpm),
@@ -173,13 +216,13 @@ function playability(notes, profile, bpm) {
173
216
  placements: state.placements
174
217
  };
175
218
  }
176
- function checkExistence(state, profile) {
219
+ function checkExistence(state, profile, sounds) {
177
220
  for (const index of state.order) {
178
221
  const note = state.notes[index];
179
222
  if (!note) {
180
223
  continue;
181
224
  }
182
- if (!canSound(profile, note.pitch)) {
225
+ if (!sounds(note.pitch)) {
183
226
  state.issues.push({
184
227
  type: "noteOutOfRange",
185
228
  layer: 1,
@@ -202,33 +245,35 @@ function checkExistence(state, profile) {
202
245
  }
203
246
  }
204
247
  }
248
+ function isSimultaneous(anchorBeat, startBeat) {
249
+ return startBeat - anchorBeat <= SIMULTANEITY_BEATS + BEAT_EPS;
250
+ }
205
251
  function soundingGroups(state) {
206
252
  const groups = [];
207
253
  let active = [];
208
254
  let cursor = 0;
209
- let previousKey = Number.NaN;
255
+ let anchor;
210
256
  for (const index of state.order) {
211
257
  const note = state.notes[index];
212
258
  if (!note) {
213
259
  continue;
214
260
  }
215
- const key = Math.round(note.startBeat * ONSET_GRID);
216
- if (key === previousKey) {
261
+ if (anchor !== void 0 && isSimultaneous(anchor, note.startBeat)) {
217
262
  continue;
218
263
  }
219
- previousKey = key;
220
264
  const at = note.startBeat;
265
+ anchor = at;
221
266
  active = active.filter((member) => {
222
267
  const held = state.notes[member];
223
- return held !== void 0 && held.startBeat + held.durationBeat > at + EPS;
268
+ return held !== void 0 && held.startBeat + held.durationBeat > at + BEAT_EPS;
224
269
  });
225
270
  while (cursor < state.order.length) {
226
271
  const candidateIndex = state.order[cursor];
227
272
  const candidate = candidateIndex === void 0 ? void 0 : state.notes[candidateIndex];
228
- if (!candidate || candidate.startBeat > at + EPS) {
273
+ if (!candidate || !isSimultaneous(at, candidate.startBeat)) {
229
274
  break;
230
275
  }
231
- if (candidateIndex !== void 0 && candidate.startBeat + candidate.durationBeat > at + EPS) {
276
+ if (candidateIndex !== void 0 && candidate.startBeat + candidate.durationBeat > at + BEAT_EPS) {
232
277
  active.push(candidateIndex);
233
278
  }
234
279
  cursor += 1;
@@ -248,12 +293,12 @@ function placeOnNeck(state, profile) {
248
293
  if (!note || !placement) {
249
294
  continue;
250
295
  }
251
- const candidates = fingeringsFor(profile, note.pitch);
296
+ const candidates = fingeringsIn(profile, note.pitch);
252
297
  if (candidates.length === 0) {
253
298
  continue;
254
299
  }
255
300
  const free = candidates.filter(
256
- (candidate) => (stringFreeAt[candidate.string] ?? 0) <= note.startBeat + EPS
301
+ (candidate) => (stringFreeAt[candidate.string] ?? 0) <= note.startBeat + BEAT_EPS
257
302
  );
258
303
  if (free.length === 0) {
259
304
  const blocked = candidates.map((candidate) => stringHeldBy[candidate.string]).filter((held) => held !== void 0);
@@ -304,7 +349,7 @@ function checkShiftSpeed(state, profile, fromIndex, toIndex, from, to) {
304
349
  }
305
350
  const seconds = (note.startBeat - previousNote.startBeat) * secondsPerBeat;
306
351
  const shift = Math.abs(to.fret - from.fret);
307
- if (seconds <= EPS || shift === 0) {
352
+ if (seconds <= BEAT_EPS || shift === 0) {
308
353
  return;
309
354
  }
310
355
  if (shift / seconds > MAX_FRET_SHIFT_PER_SECOND) {
@@ -350,61 +395,103 @@ function checkPolyphony(state, profile, group) {
350
395
  message: `${group.members.length} notes sound at once; ${profile.name} has ${profile.polyphony}`
351
396
  });
352
397
  }
353
- function placeOnKit(state, profile) {
354
- const limbForVoice = /* @__PURE__ */ new Map();
355
- const lastStroke = /* @__PURE__ */ new Map();
398
+ var NO_LIMBS = Object.freeze([]);
399
+ function readKit(profile) {
400
+ const limbs = /* @__PURE__ */ new Map();
401
+ for (const key of Object.keys(profile.reach)) {
402
+ const pitch = Number(key);
403
+ limbs.set(pitch, reachOf(profile, pitch));
404
+ }
405
+ return { profile, limbs, overdub: new Set(profile.overdub ?? []) };
406
+ }
407
+ function limbsFor(kit, pitch) {
408
+ if (pitch === void 0) {
409
+ return NO_LIMBS;
410
+ }
411
+ return kit.limbs.get(pitch) ?? NO_LIMBS;
412
+ }
413
+ function passesAt(state, kit, members) {
414
+ const played = { id: "kit", members: [] };
415
+ const overdubs = /* @__PURE__ */ new Map();
416
+ for (const index of members) {
417
+ const pitch = state.notes[index]?.pitch;
418
+ if (pitch === void 0 || !kit.overdub.has(pitch)) {
419
+ played.members.push(index);
420
+ continue;
421
+ }
422
+ let pass = overdubs.get(pitch);
423
+ if (!pass) {
424
+ pass = { id: `overdub ${pitch}`, members: [] };
425
+ overdubs.set(pitch, pass);
426
+ }
427
+ pass.members.push(index);
428
+ }
429
+ return [played, ...overdubs.values()];
430
+ }
431
+ function placePass(state, kit, pass, memory) {
432
+ const taken = /* @__PURE__ */ new Set();
433
+ let conflicted = false;
434
+ 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;
438
+ });
439
+ for (const index of members) {
440
+ const note = state.notes[index];
441
+ const placement = state.placements[index];
442
+ if (!note || !placement) {
443
+ continue;
444
+ }
445
+ const candidates = limbsFor(kit, note.pitch);
446
+ if (candidates.length === 0) {
447
+ continue;
448
+ }
449
+ const preferred = memory.limbForVoice.get(note.pitch);
450
+ const free = candidates.filter((limb) => !taken.has(limb));
451
+ const chosen = free.length === 0 ? candidates[0] : preferred !== void 0 && free.includes(preferred) ? preferred : free[0];
452
+ if (free.length === 0) {
453
+ conflicted = true;
454
+ }
455
+ if (chosen === void 0) {
456
+ continue;
457
+ }
458
+ taken.add(chosen);
459
+ placement.limb = chosen;
460
+ memory.limbForVoice.set(note.pitch, chosen);
461
+ state.movement += 1;
462
+ const strokeKey = `${pass.id} ${chosen}`;
463
+ const previousIndex = memory.lastStroke.get(strokeKey);
464
+ if (previousIndex !== void 0) {
465
+ const previousNote = state.notes[previousIndex];
466
+ if (previousNote && previousNote.pitch !== note.pitch) {
467
+ state.movement += 1;
468
+ }
469
+ checkStrokeSpeed(state, kit.profile, previousIndex, index, chosen);
470
+ }
471
+ memory.lastStroke.set(strokeKey, index);
472
+ }
473
+ return conflicted;
474
+ }
475
+ function placeOnKit(state, kit) {
476
+ const memory = { limbForVoice: /* @__PURE__ */ new Map(), lastStroke: /* @__PURE__ */ new Map() };
356
477
  for (const group of onsetGroups(state)) {
357
- const taken = /* @__PURE__ */ new Set();
358
478
  let conflicted = false;
359
- const members = [...group.members].sort((a, b) => {
360
- const left = reachOf(profile, state.notes[a]?.pitch);
361
- const right = reachOf(profile, state.notes[b]?.pitch);
362
- return left.length - right.length || a - b;
363
- });
364
- for (const index of members) {
365
- const note = state.notes[index];
366
- const placement = state.placements[index];
367
- if (!note || !placement) {
368
- continue;
369
- }
370
- const candidates = reachOf(profile, note.pitch);
371
- if (candidates.length === 0) {
479
+ for (const pass of passesAt(state, kit, group.members)) {
480
+ if (!placePass(state, kit, pass, memory)) {
372
481
  continue;
373
482
  }
374
- const preferred = limbForVoice.get(note.pitch);
375
- const free = candidates.filter((limb) => !taken.has(limb));
376
- const chosen = free.length === 0 ? candidates[0] : preferred !== void 0 && free.includes(preferred) ? preferred : free[0];
377
- if (free.length === 0) {
378
- conflicted = true;
379
- }
380
- if (chosen === void 0) {
381
- continue;
382
- }
383
- taken.add(chosen);
384
- placement.limb = chosen;
385
- limbForVoice.set(note.pitch, chosen);
386
- state.movement += 1;
387
- const previousIndex = lastStroke.get(chosen);
388
- if (previousIndex !== void 0) {
389
- const previousNote = state.notes[previousIndex];
390
- if (previousNote && previousNote.pitch !== note.pitch) {
391
- state.movement += 1;
392
- }
393
- checkStrokeSpeed(state, profile, previousIndex, index, chosen);
394
- }
395
- lastStroke.set(chosen, index);
396
- }
397
- if (conflicted) {
483
+ conflicted = true;
398
484
  state.issues.push({
399
485
  type: "limbConflict",
400
486
  layer: 2,
401
- notes: [...group.members].sort((a, b) => a - b),
487
+ notes: [...pass.members].sort((a, b) => a - b),
402
488
  startBeat: group.startBeat,
403
489
  impossible: true,
404
- message: `${group.members.length} simultaneous strokes cannot be shared among the limbs of ${profile.name}`
490
+ message: `${pass.members.length} simultaneous strokes cannot be shared among the limbs of ${kit.profile.name}`
405
491
  });
406
- } else {
407
- checkPolyphony(state, profile, group);
492
+ }
493
+ if (!conflicted) {
494
+ checkPolyphony(state, kit.profile, group);
408
495
  }
409
496
  }
410
497
  }
@@ -419,7 +506,7 @@ function checkStrokeSpeed(state, profile, fromIndex, toIndex, limb) {
419
506
  return;
420
507
  }
421
508
  const seconds = (note.startBeat - previousNote.startBeat) * secondsPerBeat;
422
- if (seconds <= EPS || seconds >= MIN_LIMB_STROKE_SECONDS) {
509
+ if (seconds <= BEAT_EPS || seconds >= MIN_LIMB_STROKE_SECONDS) {
423
510
  return;
424
511
  }
425
512
  state.issues.push({
@@ -433,18 +520,15 @@ function checkStrokeSpeed(state, profile, fromIndex, toIndex, limb) {
433
520
  }
434
521
  function onsetGroups(state) {
435
522
  const groups = [];
436
- let key = Number.NaN;
437
523
  let current;
438
524
  for (const index of state.order) {
439
525
  const note = state.notes[index];
440
- if (!note) {
526
+ if (!note || note.durationBeat <= 0) {
441
527
  continue;
442
528
  }
443
- const onsetKey = Math.round(note.startBeat * ONSET_GRID);
444
- if (!current || onsetKey !== key) {
529
+ if (!current || !isSimultaneous(current.startBeat, note.startBeat)) {
445
530
  current = { startBeat: note.startBeat, members: [] };
446
531
  groups.push(current);
447
- key = onsetKey;
448
532
  }
449
533
  current.members.push(index);
450
534
  }
@@ -461,7 +545,7 @@ function difficultyOf(state, bpm) {
461
545
  last = Math.max(last, note.startBeat + Math.max(0, note.durationBeat));
462
546
  }
463
547
  const spanBeats = last - first;
464
- if (!Number.isFinite(spanBeats) || spanBeats <= EPS) {
548
+ if (!Number.isFinite(spanBeats) || spanBeats <= BEAT_EPS) {
465
549
  return MIN_DIFFICULTY;
466
550
  }
467
551
  const seconds = spanBeats * 60 / (bpm ?? REFERENCE_BPM);
@@ -666,14 +750,17 @@ function createRng(seed) {
666
750
  }
667
751
 
668
752
  export {
669
- ARTICULATIONS,
670
753
  LIMBS,
671
754
  toInstrumentProfile,
672
755
  toStringedProfile,
673
756
  instrumentRange,
757
+ rangeIn,
674
758
  canSound,
759
+ soundsIn,
675
760
  fingeringsFor,
761
+ fingeringsIn,
676
762
  foldIntoRange,
763
+ foldIn,
677
764
  playability,
678
765
  BASS_4_STRING,
679
766
  BASS_5_STRING,
@@ -687,4 +774,4 @@ export {
687
774
  resolveAlgorithmVersion,
688
775
  createRng
689
776
  };
690
- //# sourceMappingURL=chunk-FVSI36UV.js.map
777
+ //# sourceMappingURL=chunk-7KG3RTYS.js.map