@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.
Files changed (126) hide show
  1. package/README.md +4 -3
  2. package/dist/analyze/index.cjs +315 -42
  3. package/dist/analyze/index.cjs.map +1 -1
  4. package/dist/analyze/index.d.cts +11 -99
  5. package/dist/analyze/index.d.ts +11 -99
  6. package/dist/analyze/index.js +6 -6
  7. package/dist/chunk-2ZWMSJH6.js +30 -0
  8. package/dist/chunk-2ZWMSJH6.js.map +1 -0
  9. package/dist/{chunk-R3S2LUBH.js → chunk-4MHA432G.js} +316 -184
  10. package/dist/chunk-4MHA432G.js.map +1 -0
  11. package/dist/chunk-62QZKCJQ.js +49 -0
  12. package/dist/chunk-62QZKCJQ.js.map +1 -0
  13. package/dist/chunk-AKLYPWIN.js +83 -0
  14. package/dist/chunk-AKLYPWIN.js.map +1 -0
  15. package/dist/{chunk-XTC3X6MS.js → chunk-C5XYKUVG.js} +100 -12
  16. package/dist/chunk-C5XYKUVG.js.map +1 -0
  17. package/dist/{chunk-LN7ZZMD2.js → chunk-CHMXELDS.js} +33 -14
  18. package/dist/chunk-CHMXELDS.js.map +1 -0
  19. package/dist/{chunk-A4532ZFR.js → chunk-EH6EM37U.js} +60 -4
  20. package/dist/chunk-EH6EM37U.js.map +1 -0
  21. package/dist/chunk-FBQK3ZQN.js +131 -0
  22. package/dist/chunk-FBQK3ZQN.js.map +1 -0
  23. package/dist/{chunk-5HTNMQW4.js → chunk-GOAW4VYL.js} +87 -21
  24. package/dist/chunk-GOAW4VYL.js.map +1 -0
  25. package/dist/{chunk-MSWFZF7J.js → chunk-I5R7ZBJS.js} +90 -6
  26. package/dist/chunk-I5R7ZBJS.js.map +1 -0
  27. package/dist/{chunk-GQKN5KDT.js → chunk-IT2O4XFE.js} +4 -9
  28. package/dist/chunk-IT2O4XFE.js.map +1 -0
  29. package/dist/{chunk-7Y7WF64D.js → chunk-MIWO425M.js} +6 -4
  30. package/dist/chunk-MIWO425M.js.map +1 -0
  31. package/dist/chunk-N6IHQUOX.js +1 -0
  32. package/dist/chunk-N6IHQUOX.js.map +1 -0
  33. package/dist/{chunk-COKTX2G5.js → chunk-SGU7SPLK.js} +29 -12
  34. package/dist/chunk-SGU7SPLK.js.map +1 -0
  35. package/dist/core/index.cjs +230 -14
  36. package/dist/core/index.cjs.map +1 -1
  37. package/dist/core/index.d.cts +56 -4
  38. package/dist/core/index.d.ts +56 -4
  39. package/dist/core/index.js +24 -4
  40. package/dist/{function-2bJW1Zp_.d.ts → function-DVH4Eusr.d.ts} +1 -1
  41. package/dist/{function-r2sh-jOO.d.cts → function-DYt4iKsn.d.cts} +1 -1
  42. package/dist/generate/index.cjs +830 -494
  43. package/dist/generate/index.cjs.map +1 -1
  44. package/dist/generate/index.d.cts +11 -5
  45. package/dist/generate/index.d.ts +11 -5
  46. package/dist/generate/index.js +9 -9
  47. package/dist/{index-BOBXBNwP.d.cts → index-BNlGP86A.d.cts} +1 -1
  48. package/dist/{index-CP3wAfEH.d.ts → index-Bg5dYMmZ.d.ts} +1 -1
  49. package/dist/{index-CM5W4rcm.d.ts → index-Bt4tOUD9.d.ts} +1 -1
  50. package/dist/index-CnZCo29u.d.cts +108 -0
  51. package/dist/{index-D_ZTUNKp.d.cts → index-CpOsP1y9.d.cts} +2 -2
  52. package/dist/{index-DZ1M21do.d.cts → index-CxqBQtH1.d.cts} +2 -1
  53. package/dist/{index-BdeQ1r_p.d.cts → index-Dikst44v.d.cts} +1 -1
  54. package/dist/{index-BMPxwjwV.d.ts → index-DxvARIP2.d.ts} +2 -2
  55. package/dist/{index-BreoJRw0.d.ts → index-Jc0Ryi79.d.ts} +2 -1
  56. package/dist/index-JnF4v9bx.d.ts +108 -0
  57. package/dist/index.cjs +818 -283
  58. package/dist/index.cjs.map +1 -1
  59. package/dist/index.d.cts +9 -8
  60. package/dist/index.d.ts +9 -8
  61. package/dist/index.js +34 -13
  62. package/dist/model/index.cjs +170 -31
  63. package/dist/model/index.cjs.map +1 -1
  64. package/dist/model/index.d.cts +6 -5
  65. package/dist/model/index.d.ts +6 -5
  66. package/dist/model/index.js +6 -6
  67. package/dist/{styled-D7BAx3S3.d.ts → styled-DZ7GjShb.d.ts} +1 -1
  68. package/dist/{styled-BjXtOFNk.d.cts → styled-ZTbdaVFM.d.cts} +1 -1
  69. package/dist/theory/index.cjs +180 -34
  70. package/dist/theory/index.cjs.map +1 -1
  71. package/dist/theory/index.d.cts +6 -6
  72. package/dist/theory/index.d.ts +6 -6
  73. package/dist/theory/index.js +6 -5
  74. package/package.json +50 -19
  75. package/src/analyze/arrange/internal.ts +3 -1
  76. package/src/analyze/arrange/tension.ts +15 -3
  77. package/src/analyze/arrange/tracks.ts +36 -3
  78. package/src/analyze/detect/index.ts +46 -9
  79. package/src/analyze/functional/roman.ts +14 -5
  80. package/src/analyze/index.ts +1 -1
  81. package/src/analyze/timeline/index.ts +43 -7
  82. package/src/analyze/voice/index.ts +11 -2
  83. package/src/core/event-index/index.ts +104 -0
  84. package/src/core/index.ts +14 -0
  85. package/src/core/meter/index.ts +24 -4
  86. package/src/core/random/index.ts +26 -3
  87. package/src/core/tuning/index.ts +36 -6
  88. package/src/core/validation/index.ts +119 -0
  89. package/src/generate/bass/index.ts +23 -0
  90. package/src/generate/countermelody/index.ts +167 -76
  91. package/src/generate/drums/euclid.ts +10 -3
  92. package/src/generate/drums/index.ts +56 -24
  93. package/src/generate/drums/percussion.ts +1 -1
  94. package/src/generate/groove/index.ts +25 -0
  95. package/src/generate/harmonize/index.ts +55 -44
  96. package/src/generate/index.ts +0 -1
  97. package/src/generate/motif/index.ts +45 -21
  98. package/src/generate/progression/index.ts +8 -16
  99. package/src/generate/reharmony/index.ts +1 -8
  100. package/src/generate/rhythm/index.ts +11 -1
  101. package/src/index.ts +13 -247
  102. package/src/model/chord.ts +29 -10
  103. package/src/theory/chord/index.ts +4 -2
  104. package/src/theory/harmony/index.ts +6 -21
  105. package/src/theory/index.ts +6 -1
  106. package/src/theory/safety/index.ts +71 -2
  107. package/src/theory/spelling/index.ts +70 -3
  108. package/src/theory/voicing/satb.ts +25 -4
  109. package/src/theory/voicing/styled.ts +26 -3
  110. package/dist/chunk-5HTNMQW4.js.map +0 -1
  111. package/dist/chunk-7Y7WF64D.js.map +0 -1
  112. package/dist/chunk-A4532ZFR.js.map +0 -1
  113. package/dist/chunk-BHUT2S5G.js +0 -22
  114. package/dist/chunk-BHUT2S5G.js.map +0 -1
  115. package/dist/chunk-CCBLVAPK.js +0 -42
  116. package/dist/chunk-CCBLVAPK.js.map +0 -1
  117. package/dist/chunk-COKTX2G5.js.map +0 -1
  118. package/dist/chunk-GQKN5KDT.js.map +0 -1
  119. package/dist/chunk-LN7ZZMD2.js.map +0 -1
  120. package/dist/chunk-MSWFZF7J.js.map +0 -1
  121. package/dist/chunk-R3S2LUBH.js.map +0 -1
  122. package/dist/chunk-SDM3VJJ5.js +0 -41
  123. package/dist/chunk-SDM3VJJ5.js.map +0 -1
  124. package/dist/chunk-XTC3X6MS.js.map +0 -1
  125. package/dist/chunk-ZXW5YIWK.js +0 -55
  126. package/dist/chunk-ZXW5YIWK.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -23,6 +23,7 @@ __export(index_exports, {
23
23
  BLUES_MASK: () => BLUES_MASK,
24
24
  CHROMATIC_MASK: () => CHROMATIC_MASK,
25
25
  Chord: () => Chord,
26
+ DEFAULT_GENERATION_BUDGET: () => DEFAULT_GENERATION_BUDGET,
26
27
  DORIAN_MASK: () => DORIAN_MASK,
27
28
  HARMONIC_MINOR_MASK: () => HARMONIC_MINOR_MASK,
28
29
  Interval: () => Interval,
@@ -52,6 +53,14 @@ __export(index_exports, {
52
53
  analyzeChord: () => analyzeChord,
53
54
  analyzeVoice: () => analyzeVoice,
54
55
  applyGrooveTemplate: () => applyGrooveTemplate,
56
+ assertFiniteNumber: () => assertFiniteNumber,
57
+ assertGenerationBudget: () => assertGenerationBudget,
58
+ assertInteger: () => assertInteger,
59
+ assertNoteEvent: () => assertNoteEvent,
60
+ assertNoteEvents: () => assertNoteEvents,
61
+ assertPositiveInt: () => assertPositiveInt,
62
+ assertRange: () => assertRange,
63
+ assertTimeSignature: () => assertTimeSignature,
55
64
  availableTensions: () => availableTensions,
56
65
  avoidNotes: () => avoidNotes,
57
66
  barPositionToBeat: () => barPositionToBeat,
@@ -70,6 +79,7 @@ __export(index_exports, {
70
79
  chordToRoman: () => chordToRoman,
71
80
  chordToneRole: () => chordToneRole,
72
81
  classifyInterval: () => classifyInterval,
82
+ createNoteEventIndex: () => createNoteEventIndex,
73
83
  createRng: () => createRng,
74
84
  createsHiddenParallelPerfect: () => createsHiddenParallelPerfect,
75
85
  createsParallelOctave: () => createsParallelOctave,
@@ -165,6 +175,86 @@ __export(index_exports, {
165
175
  });
166
176
  module.exports = __toCommonJS(index_exports);
167
177
 
178
+ // src/core/validation/index.ts
179
+ var DEFAULT_GENERATION_BUDGET = 1e6;
180
+ function assertFiniteNumber(value, name) {
181
+ if (!Number.isFinite(value)) {
182
+ throw new RangeError(`${name} must be finite; received ${value}`);
183
+ }
184
+ return value;
185
+ }
186
+ function assertInteger(value, name, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) {
187
+ assertFiniteNumber(value, name);
188
+ if (!Number.isSafeInteger(value) || value < min || value > max) {
189
+ throw new RangeError(`${name} must be an integer in [${min}, ${max}]; received ${value}`);
190
+ }
191
+ return value;
192
+ }
193
+ function assertPositiveInt(value, name, max = DEFAULT_GENERATION_BUDGET) {
194
+ return assertInteger(value, name, 1, max);
195
+ }
196
+ function assertRange(value, min, max, name) {
197
+ assertFiniteNumber(value, name);
198
+ if (value < min || value > max) {
199
+ throw new RangeError(`${name} must be in [${min}, ${max}]; received ${value}`);
200
+ }
201
+ return value;
202
+ }
203
+ function assertGenerationBudget(estimated, name, limit = DEFAULT_GENERATION_BUDGET) {
204
+ assertFiniteNumber(estimated, name);
205
+ assertPositiveInt(limit, `${name} limit`, Number.MAX_SAFE_INTEGER);
206
+ if (estimated < 0 || estimated > limit) {
207
+ throw new RangeError(`${name} exceeds the generation budget ${limit}; received ${estimated}`);
208
+ }
209
+ return estimated;
210
+ }
211
+ function assertTimeSignature(ts, name = "time signature") {
212
+ assertPositiveInt(ts.numerator, `${name}.numerator`);
213
+ assertPositiveInt(ts.denominator, `${name}.denominator`);
214
+ if (ts.grouping !== void 0) {
215
+ if (ts.grouping.length === 0) {
216
+ throw new RangeError(`${name}.grouping must not be empty`);
217
+ }
218
+ let sum = 0;
219
+ for (let index = 0; index < ts.grouping.length; index += 1) {
220
+ sum += assertPositiveInt(ts.grouping[index] ?? Number.NaN, `${name}.grouping[${index}]`);
221
+ }
222
+ const compound = ts.numerator % 3 === 0 && ts.numerator > 3;
223
+ const pulses = compound ? ts.numerator / 3 : ts.numerator;
224
+ if (sum !== pulses) {
225
+ throw new RangeError(`${name}.grouping must sum to ${pulses}; received ${sum}`);
226
+ }
227
+ }
228
+ return ts;
229
+ }
230
+ function assertNoteEvent(event, name = "note event", options = {}) {
231
+ assertFiniteNumber(event.pitch, `${name}.pitch`);
232
+ assertRange(event.startBeat, 0, Number.MAX_SAFE_INTEGER, `${name}.startBeat`);
233
+ assertRange(
234
+ event.durationBeat,
235
+ options.allowNonPositiveDuration ? -Number.MAX_SAFE_INTEGER : Number.MIN_VALUE,
236
+ Number.MAX_SAFE_INTEGER,
237
+ `${name}.durationBeat`
238
+ );
239
+ if (!options.allowNonPositiveDuration && event.durationBeat <= 0) {
240
+ throw new RangeError(`${name}.durationBeat must be positive; received ${event.durationBeat}`);
241
+ }
242
+ if (event.velocity !== void 0) {
243
+ assertRange(event.velocity, 0, 127, `${name}.velocity`);
244
+ }
245
+ return event;
246
+ }
247
+ function assertNoteEvents(events, name = "note events", options = {}) {
248
+ assertGenerationBudget(events.length, `${name} count`, options.budget);
249
+ for (let index = 0; index < events.length; index += 1) {
250
+ const event = events[index];
251
+ if (event !== void 0) {
252
+ assertNoteEvent(event, `${name}[${index}]`, options);
253
+ }
254
+ }
255
+ return events;
256
+ }
257
+
168
258
  // src/core/meter/index.ts
169
259
  var EPS = 1e-9;
170
260
  function isMultiple(value, unit) {
@@ -181,21 +271,24 @@ function parseTimeSignature(text) {
181
271
  }
182
272
  const numerator = Number.parseInt(match[1] ?? "", 10);
183
273
  const denominator = Number.parseInt(match[2] ?? "", 10);
184
- if (numerator <= 0 || denominator <= 0) {
274
+ if (!Number.isSafeInteger(numerator) || !Number.isSafeInteger(denominator) || numerator <= 0 || denominator <= 0) {
185
275
  throw new Error(`Invalid time signature: ${text}`);
186
276
  }
187
277
  return { numerator, denominator };
188
278
  }
189
279
  function formatTimeSignature(ts) {
280
+ assertTimeSignature(ts);
190
281
  return `${ts.numerator}/${ts.denominator}`;
191
282
  }
192
283
  function isCompound(ts) {
284
+ assertTimeSignature(ts);
193
285
  return ts.numerator % 3 === 0 && ts.numerator > 3;
194
286
  }
195
287
  function unitBeats(ts) {
196
288
  return 4 / ts.denominator;
197
289
  }
198
290
  function beatsPerBar(ts) {
291
+ assertTimeSignature(ts);
199
292
  return ts.numerator * unitBeats(ts);
200
293
  }
201
294
  function pulseBeats(ts) {
@@ -228,14 +321,19 @@ function isGroupHead(grouping, pulseIndex) {
228
321
  return false;
229
322
  }
230
323
  function beatToBarPosition(beatInQuarters, ts) {
324
+ assertFiniteNumber(beatInQuarters, "beat");
231
325
  const barLen = beatsPerBar(ts);
232
326
  const bar = Math.floor(beatInQuarters / barLen);
233
327
  return { bar, beat: beatInQuarters - bar * barLen };
234
328
  }
235
329
  function barPositionToBeat(pos, ts) {
330
+ assertInteger(pos.bar, "bar position bar");
331
+ assertFiniteNumber(pos.beat, "bar position beat");
236
332
  return pos.bar * beatsPerBar(ts) + pos.beat;
237
333
  }
238
334
  function metricWeight(beatInQuarters, ts) {
335
+ assertFiniteNumber(beatInQuarters, "beat");
336
+ assertTimeSignature(ts);
239
337
  const { beat } = beatToBarPosition(beatInQuarters, ts);
240
338
  const pulse = pulseBeats(ts);
241
339
  if (!isMultiple(beat, pulse)) {
@@ -264,9 +362,8 @@ function isStrongBeat(beatInQuarters, ts) {
264
362
  return metricWeight(beatInQuarters, ts) >= 2;
265
363
  }
266
364
  function tuplet(totalBeats, count) {
267
- if (!Number.isInteger(count) || count <= 0) {
268
- throw new Error(`Invalid tuplet count: ${count}`);
269
- }
365
+ assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, "tuplet total beats");
366
+ assertPositiveInt(count, "tuplet count");
270
367
  return new Array(count).fill(totalBeats / count);
271
368
  }
272
369
 
@@ -531,8 +628,10 @@ function classifySeventh(thirdIc, fifthIc, seventhIc) {
531
628
  function stackThirds(degree, key, size) {
532
629
  const tones = scaleTonesInDegreeOrder(key);
533
630
  const length = tones.length;
534
- if (length === 0) {
535
- return { rootPc: pitchClass2(key.rootPc), quality: "maj", intervals: [0, 4, 7] };
631
+ if (length !== 7) {
632
+ throw new Error(
633
+ `diatonic chord stacking requires a heptatonic scale (received ${length} tones)`
634
+ );
536
635
  }
537
636
  const idx = (step) => ((degree + step) % length + length) % length;
538
637
  const rootPc = tones[idx(0)] ?? pitchClass2(key.rootPc);
@@ -693,6 +792,31 @@ var ReasonFlag = /* @__PURE__ */ ((ReasonFlag2) => {
693
792
  })(ReasonFlag || {});
694
793
  var SUGGESTION_WINDOW = 12;
695
794
  var MAX_SUGGESTIONS = 3;
795
+ var REASON_SEVERITY = [
796
+ [2 /* Tension */, 1 /* Warning */, 1 /* Warning */],
797
+ [4 /* AvoidNote */, 1 /* Warning */, 2 /* Dissonant */],
798
+ [8 /* ScaleTone */, 1 /* Warning */, 1 /* Warning */],
799
+ [16 /* NonScale */, 2 /* Dissonant */, 2 /* Dissonant */],
800
+ [32 /* OutOfRange */, 1 /* Warning */, 1 /* Warning */],
801
+ [64 /* Tritone */, 2 /* Dissonant */, 2 /* Dissonant */],
802
+ [128 /* LargeLeap */, 1 /* Warning */, 2 /* Dissonant */],
803
+ [1024 /* VerticalDissonance */, 2 /* Dissonant */, 2 /* Dissonant */],
804
+ [2048 /* ParallelPerfect */, 1 /* Warning */, 2 /* Dissonant */],
805
+ [4096 /* HiddenParallel */, 1 /* Warning */, 2 /* Dissonant */],
806
+ [8192 /* VoiceCrossing */, 1 /* Warning */, 2 /* Dissonant */],
807
+ [16384 /* Suspension */, 2 /* Dissonant */, 2 /* Dissonant */],
808
+ [32768 /* NeedsResolution */, 1 /* Warning */, 2 /* Dissonant */]
809
+ ];
810
+ function minimumSafetyForReasons(reasons, profile) {
811
+ let minimum = 0 /* Safe */;
812
+ const severityIndex = profile === "strict" ? 2 : 1;
813
+ for (const entry of REASON_SEVERITY) {
814
+ if (reasons & entry[0]) {
815
+ minimum = Math.max(minimum, entry[severityIndex]);
816
+ }
817
+ }
818
+ return minimum;
819
+ }
696
820
  function pitchClass4(pitch) {
697
821
  return (Math.trunc(pitch) % 12 + 12) % 12;
698
822
  }
@@ -717,8 +841,27 @@ function stepResolution(pitch, chord) {
717
841
  return void 0;
718
842
  }
719
843
  function evaluateSafety(q) {
844
+ assertSafetyContext(q);
845
+ assertFiniteNumber(q.candidatePitch, "candidatePitch");
720
846
  return evaluateInternal(q, true);
721
847
  }
848
+ function assertSafetyContext(q) {
849
+ if (q.prevPitch !== void 0) assertFiniteNumber(q.prevPitch, "prevPitch");
850
+ if (q.vocalLow !== void 0) assertFiniteNumber(q.vocalLow, "vocalLow");
851
+ if (q.vocalHigh !== void 0) assertFiniteNumber(q.vocalHigh, "vocalHigh");
852
+ if (q.vocalLow !== void 0 && q.vocalHigh !== void 0 && q.vocalLow > q.vocalHigh) {
853
+ throw new RangeError("vocalLow must not exceed vocalHigh");
854
+ }
855
+ assertGenerationBudget(q.otherVoices.length, "other voices");
856
+ for (let index = 0; index < q.otherVoices.length; index += 1) {
857
+ const voice = q.otherVoices[index];
858
+ if (!voice) continue;
859
+ assertFiniteNumber(voice.pitch, `otherVoices[${index}].pitch`);
860
+ if (voice.prevPitch !== void 0) {
861
+ assertFiniteNumber(voice.prevPitch, `otherVoices[${index}].prevPitch`);
862
+ }
863
+ }
864
+ }
722
865
  function evaluateInternal(q, collectSuggestions) {
723
866
  let reasons = 0;
724
867
  let level = 0 /* Safe */;
@@ -779,9 +922,11 @@ function evaluateInternal(q, collectSuggestions) {
779
922
  const pc = Math.abs(pitch - q.prevPitch) % 12;
780
923
  if (pc === 6) {
781
924
  reasons |= 64 /* Tritone */;
925
+ raise(2 /* Dissonant */);
782
926
  }
783
927
  if (isForbiddenMelodicLeap(q.prevPitch, pitch)) {
784
928
  reasons |= 128 /* LargeLeap */;
929
+ raise(parallelSeverity);
785
930
  }
786
931
  if (pc === 1) {
787
932
  reasons |= 256 /* MinorSecond */;
@@ -826,6 +971,7 @@ function evaluateInternal(q, collectSuggestions) {
826
971
  }
827
972
  }
828
973
  }
974
+ raise(minimumSafetyForReasons(reasons, q.profile));
829
975
  const result = { safety: level, reasons, rationale: describe(reasons, q) };
830
976
  if (resolveTo !== void 0) {
831
977
  result.resolveTo = resolveTo;
@@ -889,11 +1035,15 @@ function describe(reasons, q) {
889
1035
  return q.chord ? "Placeable" : "No chord context";
890
1036
  }
891
1037
  function enumerateSafePitches(q, pitchLow, pitchHigh) {
1038
+ assertSafetyContext(q);
892
1039
  const chordTones = [];
893
1040
  const others = [];
894
- if (!Number.isFinite(pitchLow) || !Number.isFinite(pitchHigh)) {
895
- return [];
1041
+ assertInteger(pitchLow, "pitchLow");
1042
+ assertInteger(pitchHigh, "pitchHigh");
1043
+ if (pitchLow > pitchHigh) {
1044
+ throw new RangeError(`pitchLow must not exceed pitchHigh; received ${pitchLow} > ${pitchHigh}`);
896
1045
  }
1046
+ assertGenerationBudget(pitchHigh - pitchLow + 1, "safe pitch candidates");
897
1047
  for (let pitch = pitchHigh; pitch >= pitchLow; pitch -= 1) {
898
1048
  const result = evaluateInternal({ ...q, candidatePitch: pitch }, false);
899
1049
  if (result.safety === 2 /* Dissonant */) {
@@ -1037,11 +1187,16 @@ function parseSimpleRoman(text, key) {
1037
1187
  if (canonical !== void 0) {
1038
1188
  return { rootPc, quality: canonical, inversion: 0 };
1039
1189
  }
1040
- const isDim = /o|°|dim/.test(suffix);
1041
- const isAug = /\+|aug/.test(suffix);
1042
- const isHalfDim = /ø/.test(suffix);
1043
- const explicitMaj7 = /maj7|M7/.test(suffix);
1044
- const figures = suffix.replace(/maj7|M7/g, "").replace(/[^0-9]/g, "");
1190
+ const parsedSuffix = /^(o|°|dim|ø|\+|aug)?(maj7|M7)?(65|64|43|42|7|6|2)?$/.exec(suffix);
1191
+ if (!parsedSuffix) {
1192
+ throw new Error(`Unsupported suffix "${suffix}" in Roman numeral: ${text}`);
1193
+ }
1194
+ const qualityMarker = parsedSuffix[1] ?? "";
1195
+ const isDim = qualityMarker === "o" || qualityMarker === "\xB0" || qualityMarker === "dim";
1196
+ const isAug = qualityMarker === "+" || qualityMarker === "aug";
1197
+ const isHalfDim = qualityMarker === "\xF8";
1198
+ const explicitMaj7 = parsedSuffix[2] !== void 0;
1199
+ const figures = parsedSuffix[3] ?? "";
1045
1200
  if (figures !== "" && !INVERSION_FIGURES.has(figures)) {
1046
1201
  throw new Error(`Unsupported figured-bass or extension "${figures}" in Roman numeral: ${text}`);
1047
1202
  }
@@ -1290,6 +1445,73 @@ function detectCadence(from, to, key) {
1290
1445
  return null;
1291
1446
  }
1292
1447
 
1448
+ // src/core/event-index/index.ts
1449
+ var EPS2 = 1e-9;
1450
+ function upperBound(values, beat) {
1451
+ let low = 0;
1452
+ let high = values.length;
1453
+ while (low < high) {
1454
+ const middle = Math.floor((low + high) / 2);
1455
+ if ((values[middle]?.note.startBeat ?? Number.POSITIVE_INFINITY) <= beat) {
1456
+ low = middle + 1;
1457
+ } else {
1458
+ high = middle;
1459
+ }
1460
+ }
1461
+ return low;
1462
+ }
1463
+ function createNoteEventIndex(events, options = {}) {
1464
+ assertNoteEvents(events, "note events", options);
1465
+ const notes = events.map((note, originalIndex) => ({
1466
+ note,
1467
+ originalIndex,
1468
+ endBeat: note.startBeat + note.durationBeat
1469
+ })).sort((a, b) => a.note.startBeat - b.note.startBeat || a.originalIndex - b.originalIndex);
1470
+ const prefixMaxEnd = [];
1471
+ let maxEnd = Number.NEGATIVE_INFINITY;
1472
+ for (const indexed of notes) {
1473
+ maxEnd = Math.max(maxEnd, indexed.endBeat);
1474
+ prefixMaxEnd.push(maxEnd);
1475
+ }
1476
+ return {
1477
+ notes,
1478
+ at(beat) {
1479
+ let index = upperBound(notes, beat + EPS2) - 1;
1480
+ while (index >= 0) {
1481
+ if ((prefixMaxEnd[index] ?? Number.NEGATIVE_INFINITY) <= beat + EPS2) {
1482
+ return void 0;
1483
+ }
1484
+ const indexed = notes[index];
1485
+ if (indexed && indexed.note.durationBeat > 0 && indexed.note.startBeat - EPS2 <= beat && beat < indexed.endBeat - EPS2) {
1486
+ return indexed;
1487
+ }
1488
+ index -= 1;
1489
+ }
1490
+ return void 0;
1491
+ },
1492
+ attacksAt(beat) {
1493
+ const firstAfter = upperBound(notes, beat + EPS2);
1494
+ const candidate = notes[firstAfter - 1];
1495
+ return candidate !== void 0 && Math.abs(candidate.note.startBeat - beat) < EPS2;
1496
+ },
1497
+ onsetsBetween(startBeat, endBeat) {
1498
+ const result = [];
1499
+ let index = upperBound(notes, startBeat + EPS2);
1500
+ while (index < notes.length) {
1501
+ const onset = notes[index]?.note.startBeat;
1502
+ if (onset === void 0 || onset >= endBeat - EPS2) {
1503
+ break;
1504
+ }
1505
+ if (result[result.length - 1] !== onset) {
1506
+ result.push(onset);
1507
+ }
1508
+ index += 1;
1509
+ }
1510
+ return result;
1511
+ }
1512
+ };
1513
+ }
1514
+
1293
1515
  // src/analyze/detect/index.ts
1294
1516
  function pitchClass5(value) {
1295
1517
  return (Math.trunc(value) % 12 + 12) % 12;
@@ -1297,12 +1519,21 @@ function pitchClass5(value) {
1297
1519
  function uniquePitchClasses(pitches) {
1298
1520
  return [...new Set(pitches.map(pitchClass5))].sort((a, b) => a - b);
1299
1521
  }
1300
- function detectChord(pitches) {
1522
+ function assertPitches(pitches) {
1523
+ assertGenerationBudget(pitches.length, "detection pitches");
1524
+ for (let index = 0; index < pitches.length; index += 1) {
1525
+ assertFiniteNumber(pitches[index] ?? Number.NaN, `pitches[${index}]`);
1526
+ }
1527
+ }
1528
+ function detectChord(pitches, opts = {}) {
1529
+ assertPitches(pitches);
1530
+ if (opts.bassPc !== void 0) assertFiniteNumber(opts.bassPc, "bassPc");
1301
1531
  const input = uniquePitchClasses(pitches);
1302
1532
  if (input.length === 0) {
1303
1533
  return [];
1304
1534
  }
1305
- const bassPc = pitchClass5(Math.min(...pitches));
1535
+ const inputKind = opts.input === void 0 || opts.input === "auto" ? pitches.every((pitch) => Number.isInteger(pitch) && pitch >= 0 && pitch <= 11) ? "pitchClass" : "midi" : opts.input;
1536
+ const bassPc = opts.bassPc !== void 0 ? pitchClass5(opts.bassPc) : inputKind === "midi" ? pitchClass5(pitches.reduce((lowest, pitch) => Math.min(lowest, pitch), Infinity)) : void 0;
1306
1537
  const inputSet = new Set(input);
1307
1538
  const matches = [];
1308
1539
  const qualities = chordQualities();
@@ -1317,8 +1548,8 @@ function detectChord(pitches) {
1317
1548
  continue;
1318
1549
  }
1319
1550
  const extraPcs = input.filter((pc) => !toneSet.has(pc));
1320
- const bassIndex = chord.intervals.findIndex((iv) => pitchClass5(rootPc + iv) === bassPc);
1321
- const inversion = bassIndex > 0 ? bassIndex : 0;
1551
+ const bassIndex = bassPc === void 0 ? -1 : chord.intervals.findIndex((iv) => pitchClass5(rootPc + iv) === bassPc);
1552
+ const inversion = bassPc === void 0 ? null : bassIndex > 0 ? bassIndex : 0;
1322
1553
  const match = {
1323
1554
  rootPc,
1324
1555
  quality,
@@ -1327,7 +1558,7 @@ function detectChord(pitches) {
1327
1558
  exact: extraPcs.length === 0 && missingPcs.length === 0,
1328
1559
  inversion
1329
1560
  };
1330
- if (inversion > 0) {
1561
+ if (bassPc !== void 0 && bassPc !== rootPc) {
1331
1562
  match.bassPc = bassPc;
1332
1563
  }
1333
1564
  matches.push(match);
@@ -1349,8 +1580,8 @@ function detectChord(pitches) {
1349
1580
  });
1350
1581
  return matches;
1351
1582
  }
1352
- function detectChordBest(pitches) {
1353
- const best = detectChord(pitches)[0];
1583
+ function detectChordBest(pitches, opts = {}) {
1584
+ const best = detectChord(pitches, opts)[0];
1354
1585
  if (!best) {
1355
1586
  return null;
1356
1587
  }
@@ -1358,6 +1589,7 @@ function detectChordBest(pitches) {
1358
1589
  }
1359
1590
  var MINOR_MASK_VARIANTS = [NATURAL_MINOR_MASK, HARMONIC_MINOR_MASK, MELODIC_MINOR_MASK];
1360
1591
  function detectKey(pitches) {
1592
+ assertPitches(pitches);
1361
1593
  const input = uniquePitchClasses(pitches);
1362
1594
  if (input.length === 0) {
1363
1595
  return [];
@@ -1409,10 +1641,21 @@ function detectKey(pitches) {
1409
1641
 
1410
1642
  // src/analyze/timeline/index.ts
1411
1643
  function chordTimelineFromChords(chords, totalBeats) {
1412
- const sorted = [...chords].sort((a, b) => a.startBeat - b.startBeat);
1644
+ assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, "timeline totalBeats");
1645
+ assertGenerationBudget(chords.length, "timeline chords");
1646
+ for (let index = 0; index < chords.length; index += 1) {
1647
+ assertRange(
1648
+ chords[index]?.startBeat ?? Number.NaN,
1649
+ 0,
1650
+ Number.MAX_SAFE_INTEGER,
1651
+ `chords[${index}].startBeat`
1652
+ );
1653
+ }
1654
+ const sorted = [...chords].filter((chord) => chord.startBeat < totalBeats).sort((a, b) => a.startBeat - b.startBeat);
1413
1655
  const segments = sorted.map((gc, i) => {
1414
1656
  const next = sorted[i + 1];
1415
- const endBeat = Math.max(gc.startBeat, next ? next.startBeat : totalBeats);
1657
+ const nextBeat = next ? next.startBeat : totalBeats;
1658
+ const endBeat = Math.min(totalBeats, Math.max(gc.startBeat, nextBeat));
1416
1659
  return {
1417
1660
  startBeat: gc.startBeat,
1418
1661
  endBeat,
@@ -1423,6 +1666,7 @@ function chordTimelineFromChords(chords, totalBeats) {
1423
1666
  }
1424
1667
  function segmentLookup(segments) {
1425
1668
  return (beat) => {
1669
+ assertFiniteNumber(beat, "timeline query beat");
1426
1670
  for (const seg of segments) {
1427
1671
  if (beat >= seg.startBeat && beat < seg.endBeat) {
1428
1672
  return seg.chord;
@@ -1431,7 +1675,7 @@ function segmentLookup(segments) {
1431
1675
  return null;
1432
1676
  };
1433
1677
  }
1434
- var EPS2 = 1e-9;
1678
+ var EPS3 = 1e-9;
1435
1679
  var NOISE_THRESHOLD_RATIO = 0.2;
1436
1680
  var MAX_DETECTION_PCS = 6;
1437
1681
  var FALLBACK_PCS = 3;
@@ -1473,11 +1717,11 @@ function analyzeWindow(notes, windowStart, windowEnd, ts, key) {
1473
1717
  for (const note of notes) {
1474
1718
  const noteEnd = note.startBeat + note.durationBeat;
1475
1719
  const overlap = Math.min(noteEnd, windowEnd) - Math.max(note.startBeat, windowStart);
1476
- if (overlap <= EPS2) {
1720
+ if (overlap <= EPS3) {
1477
1721
  continue;
1478
1722
  }
1479
1723
  const velocityFactor = note.velocity !== void 0 ? note.velocity / 127 : 1;
1480
- const onsetInWindow = note.startBeat >= windowStart - EPS2 && note.startBeat < windowEnd - EPS2;
1724
+ const onsetInWindow = note.startBeat >= windowStart - EPS3 && note.startBeat < windowEnd - EPS3;
1481
1725
  const accent = onsetInWindow ? 1 + metricWeight(note.startBeat, ts) / 3 : 1;
1482
1726
  const pc = pitchClass6(note.pitch);
1483
1727
  weights[pc] = (weights[pc] ?? 0) + overlap * velocityFactor * accent;
@@ -1491,7 +1735,7 @@ function analyzeWindow(notes, windowStart, windowEnd, ts, key) {
1491
1735
  totalWeight += w;
1492
1736
  maxWeight = Math.max(maxWeight, w);
1493
1737
  }
1494
- if (totalWeight <= EPS2) {
1738
+ if (totalWeight <= EPS3) {
1495
1739
  return null;
1496
1740
  }
1497
1741
  const ranked = weights.map((weight, pc) => ({ pc, weight })).filter(({ weight }) => weight > 0).sort((a, b) => b.weight - a.weight || a.pc - b.pc);
@@ -1524,26 +1768,40 @@ function sameChord(a, b) {
1524
1768
  }
1525
1769
  function chordTimelineFromNotes(notes, opts = {}) {
1526
1770
  const ts = opts.ts ?? parseTimeSignature("4/4");
1771
+ assertTimeSignature(ts);
1527
1772
  const harmonicRhythm = opts.harmonicRhythm ?? beatsPerBar(ts);
1528
- if (!(harmonicRhythm > 0)) {
1529
- throw new Error(`Invalid harmonic rhythm: ${harmonicRhythm}`);
1530
- }
1531
- const sounding = notes.filter((note) => note.durationBeat > 0);
1773
+ assertRange(harmonicRhythm, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "harmonic rhythm");
1774
+ assertNoteEvents(notes, "timeline notes", { allowNonPositiveDuration: true });
1775
+ const soundingIndex = createNoteEventIndex(notes.filter((note) => note.durationBeat > 0));
1776
+ const sounding = soundingIndex.notes.map(({ note }) => note);
1532
1777
  const lastNoteEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
1533
1778
  const totalBeats = opts.totalBeats ?? lastNoteEnd;
1779
+ assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, "timeline totalBeats");
1534
1780
  const key = opts.key ?? detectKey(sounding.map((n) => n.pitch))[0]?.key ?? majorKey(0);
1535
1781
  const segments = [];
1536
1782
  const segmentConfidence = [];
1537
- const windowCount = Math.max(0, Math.ceil(totalBeats / harmonicRhythm - EPS2));
1783
+ const windowCount = Math.max(0, Math.ceil(totalBeats / harmonicRhythm - EPS3));
1784
+ assertGenerationBudget(windowCount, "timeline windows");
1785
+ const windowNotes = Array.from({ length: windowCount }, () => []);
1786
+ let memberships = 0;
1787
+ for (const indexed of soundingIndex.notes) {
1788
+ const first = Math.max(0, Math.floor(indexed.note.startBeat / harmonicRhythm));
1789
+ const lastExclusive = Math.min(windowCount, Math.ceil(indexed.endBeat / harmonicRhythm));
1790
+ memberships += Math.max(0, lastExclusive - first);
1791
+ assertGenerationBudget(memberships, "timeline note-to-window memberships");
1792
+ for (let window = first; window < lastExclusive; window += 1) {
1793
+ windowNotes[window]?.push(indexed.note);
1794
+ }
1795
+ }
1538
1796
  for (let i = 0; i < windowCount; i += 1) {
1539
1797
  const start = i * harmonicRhythm;
1540
1798
  const end = Math.min(start + harmonicRhythm, totalBeats);
1541
- const inferred = analyzeWindow(sounding, start, end, ts, key);
1799
+ const inferred = analyzeWindow(windowNotes[i] ?? [], start, end, ts, key);
1542
1800
  if (!inferred) {
1543
1801
  continue;
1544
1802
  }
1545
1803
  const last = segments[segments.length - 1];
1546
- if (last && sameChord(last.chord, inferred.chord) && Math.abs(last.endBeat - start) < EPS2) {
1804
+ if (last && sameChord(last.chord, inferred.chord) && Math.abs(last.endBeat - start) < EPS3) {
1547
1805
  const lastLength = last.endBeat - last.startBeat;
1548
1806
  const length = end - start;
1549
1807
  const lastConfidence = segmentConfidence[segmentConfidence.length - 1] ?? 0;
@@ -1568,7 +1826,7 @@ function detectCadences(timeline, key) {
1568
1826
  if (!prev || !cur) {
1569
1827
  continue;
1570
1828
  }
1571
- if (Math.abs(cur.startBeat - prev.endBeat) > EPS2) {
1829
+ if (Math.abs(cur.startBeat - prev.endBeat) > EPS3) {
1572
1830
  continue;
1573
1831
  }
1574
1832
  const type = detectCadence(prev.chord, cur.chord, key);
@@ -1580,9 +1838,9 @@ function detectCadences(timeline, key) {
1580
1838
  }
1581
1839
 
1582
1840
  // src/analyze/arrange/internal.ts
1583
- var EPS3 = 1e-9;
1841
+ var EPS4 = 1e-9;
1584
1842
  function covers(note, beat) {
1585
- return beat >= note.startBeat - EPS3 && beat < note.endBeat - EPS3;
1843
+ return beat >= note.startBeat - EPS4 && beat < note.endBeat - EPS4;
1586
1844
  }
1587
1845
  function poolNotes(tracks) {
1588
1846
  const notes = [];
@@ -1600,7 +1858,7 @@ function splitIntoSubVoices(ordered) {
1600
1858
  for (const note of ordered) {
1601
1859
  const lane = lanes.find((candidate) => {
1602
1860
  const last = candidate[candidate.length - 1];
1603
- return last !== void 0 && last.startBeat + last.durationBeat <= note.startBeat + EPS3;
1861
+ return last !== void 0 && last.startBeat + last.durationBeat <= note.startBeat + EPS4;
1604
1862
  });
1605
1863
  if (lane) {
1606
1864
  lane.push(note);
@@ -1618,9 +1876,11 @@ function prepareTracks(tracks) {
1618
1876
  voice,
1619
1877
  sounding: voice.map((note, i) => {
1620
1878
  const prev = i > 0 ? voice[i - 1] : void 0;
1879
+ const prevEnd = prev === void 0 ? void 0 : prev.startBeat + prev.durationBeat;
1880
+ const contiguous = prevEnd !== void 0 && Math.abs(prevEnd - note.startBeat) <= EPS4;
1621
1881
  return {
1622
1882
  pitch: note.pitch,
1623
- prevPitch: prev?.pitch,
1883
+ prevPitch: contiguous ? prev?.pitch : void 0,
1624
1884
  startBeat: note.startBeat,
1625
1885
  endBeat: note.startBeat + note.durationBeat
1626
1886
  };
@@ -1652,11 +1912,16 @@ function clamp01(value) {
1652
1912
  }
1653
1913
  function tensionCurve(tracks, opts = {}) {
1654
1914
  const ts = opts.ts ?? parseTimeSignature("4/4");
1915
+ assertTimeSignature(ts);
1916
+ assertGenerationBudget(tracks.length, "arrangement tracks");
1917
+ for (let index = 0; index < tracks.length; index += 1) {
1918
+ assertNoteEvents(tracks[index]?.notes ?? [], `tracks[${index}].notes`, {
1919
+ allowNonPositiveDuration: true
1920
+ });
1921
+ }
1655
1922
  const profile = opts.profile ?? "pop";
1656
1923
  const step = opts.step ?? 1;
1657
- if (!(step > 0)) {
1658
- throw new Error(`Invalid tension sampling step: ${step}`);
1659
- }
1924
+ assertRange(step, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "tension sampling step");
1660
1925
  const pooled = poolNotes(tracks);
1661
1926
  const totalBeats = pooled.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
1662
1927
  const { timeline, key } = chordTimelineFromNotes(pooled, {
@@ -1666,7 +1931,8 @@ function tensionCurve(tracks, opts = {}) {
1666
1931
  });
1667
1932
  const prepared = prepareTracks(tracks);
1668
1933
  const points = [];
1669
- const sampleCount = Math.max(0, Math.ceil(totalBeats / step - EPS3));
1934
+ const sampleCount = Math.max(0, Math.ceil(totalBeats / step - EPS4));
1935
+ assertGenerationBudget(sampleCount, "tension samples");
1670
1936
  for (let i = 0; i < sampleCount; i += 1) {
1671
1937
  const beat = i * step;
1672
1938
  points.push({ beat, tension: sampleTension(prepared, timeline, key, ts, profile, beat) });
@@ -1736,7 +2002,7 @@ function sampleTension(prepared, timeline, key, ts, profile, beat) {
1736
2002
  }
1737
2003
 
1738
2004
  // src/analyze/voice/index.ts
1739
- var EPS4 = 1e-9;
2005
+ var EPS5 = 1e-9;
1740
2006
  function pitchClass8(pitch) {
1741
2007
  return (Math.trunc(pitch) % 12 + 12) % 12;
1742
2008
  }
@@ -1792,6 +2058,7 @@ function stepResolution2(pitch, chord) {
1792
2058
  return void 0;
1793
2059
  }
1794
2060
  function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat) {
2061
+ assertNoteEvents(voice, "voice notes", { allowNonPositiveDuration: true });
1795
2062
  const result = [];
1796
2063
  for (let i = 0; i < voice.length; i += 1) {
1797
2064
  const note = voice[i];
@@ -1800,8 +2067,10 @@ function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat) {
1800
2067
  }
1801
2068
  const prevNote = i > 0 ? voice[i - 1] : void 0;
1802
2069
  const nextNote = i + 1 < voice.length ? voice[i + 1] : void 0;
1803
- const prev = prevNote !== void 0 && prevNote.startBeat < note.startBeat - EPS4 ? prevNote : void 0;
1804
- const next = nextNote !== void 0 && nextNote.startBeat > note.startBeat + EPS4 ? nextNote : void 0;
2070
+ const prevEnd = prevNote === void 0 ? Number.NEGATIVE_INFINITY : prevNote.startBeat + prevNote.durationBeat;
2071
+ const noteEnd = note.startBeat + note.durationBeat;
2072
+ const prev = prevNote !== void 0 && Math.abs(prevEnd - note.startBeat) <= EPS5 ? prevNote : void 0;
2073
+ const next = nextNote !== void 0 && Math.abs(noteEnd - nextNote.startBeat) <= EPS5 ? nextNote : void 0;
1805
2074
  const chord = chordAtBeat(note.startBeat);
1806
2075
  const labels = [];
1807
2076
  const member = isChordMember2(note.pitch, chord);
@@ -1941,8 +2210,9 @@ function otherVoicesSounding(prepared, excludeTrack, excludeVoice, beat) {
1941
2210
  continue;
1942
2211
  }
1943
2212
  const snap = { pitch: note.pitch };
1944
- if (note.prevPitch !== void 0) {
1945
- snap.prevPitch = note.prevPitch;
2213
+ const previous = Math.abs(note.startBeat - beat) <= EPS4 ? note.prevPitch : note.pitch;
2214
+ if (previous !== void 0) {
2215
+ snap.prevPitch = previous;
1946
2216
  }
1947
2217
  out.push(snap);
1948
2218
  }
@@ -1954,7 +2224,7 @@ function evaluationBeats(note, timeline) {
1954
2224
  const noteEnd = note.startBeat + note.durationBeat;
1955
2225
  const beats = [note.startBeat];
1956
2226
  for (const segment of timeline.segments) {
1957
- if (segment.startBeat > note.startBeat + EPS3 && segment.startBeat < noteEnd - EPS3) {
2227
+ if (segment.startBeat > note.startBeat + EPS4 && segment.startBeat < noteEnd - EPS4) {
1958
2228
  beats.push(segment.startBeat);
1959
2229
  }
1960
2230
  }
@@ -1962,6 +2232,13 @@ function evaluationBeats(note, timeline) {
1962
2232
  }
1963
2233
  function analyzeArrangement(tracks, opts = {}) {
1964
2234
  const ts = opts.ts ?? parseTimeSignature("4/4");
2235
+ assertTimeSignature(ts);
2236
+ assertGenerationBudget(tracks.length, "arrangement tracks");
2237
+ for (let index = 0; index < tracks.length; index += 1) {
2238
+ assertNoteEvents(tracks[index]?.notes ?? [], `tracks[${index}].notes`, {
2239
+ allowNonPositiveDuration: true
2240
+ });
2241
+ }
1965
2242
  const profile = opts.profile ?? "pop";
1966
2243
  const pooled = poolNotes(tracks);
1967
2244
  const { timeline, key, segmentConfidence } = chordTimelineFromNotes(pooled, {
@@ -1992,11 +2269,18 @@ function analyzeArrangement(tracks, opts = {}) {
1992
2269
  (beat) => otherVoicesSounding(prepared, t, v, beat)
1993
2270
  )
1994
2271
  );
1995
- for (const note of subVoice.voice) {
2272
+ for (let noteIndex = 0; noteIndex < subVoice.voice.length; noteIndex += 1) {
2273
+ const note = subVoice.voice[noteIndex];
2274
+ const preparedNote = subVoice.sounding[noteIndex];
2275
+ if (note === void 0 || preparedNote === void 0) {
2276
+ continue;
2277
+ }
1996
2278
  for (const beat of evaluationBeats(note, timeline)) {
2279
+ const atOnset = Math.abs(beat - note.startBeat) <= EPS4;
1997
2280
  const result = evaluateSafety({
1998
2281
  profile,
1999
2282
  candidatePitch: note.pitch,
2283
+ prevPitch: atOnset ? preparedNote.prevPitch : note.pitch,
2000
2284
  chord: timeline.at(beat),
2001
2285
  key,
2002
2286
  otherVoices: otherVoicesSounding(prepared, t, v, beat),
@@ -2013,6 +2297,12 @@ function analyzeArrangement(tracks, opts = {}) {
2013
2297
  if (result.rationale !== void 0) {
2014
2298
  conflict.rationale = result.rationale;
2015
2299
  }
2300
+ if (result.resolveTo !== void 0) {
2301
+ conflict.resolveTo = result.resolveTo;
2302
+ }
2303
+ if (result.suggestions !== void 0) {
2304
+ conflict.suggestions = [...result.suggestions];
2305
+ }
2016
2306
  conflicts.push(conflict);
2017
2307
  }
2018
2308
  }
@@ -2170,6 +2460,7 @@ function spelledInterval(a, b) {
2170
2460
 
2171
2461
  // src/core/random/index.ts
2172
2462
  function createRng(seed) {
2463
+ assertFiniteNumber(seed, "seed");
2173
2464
  let state = seed >>> 0;
2174
2465
  const next = () => {
2175
2466
  state = state + 1831565813 >>> 0;
@@ -2180,31 +2471,71 @@ function createRng(seed) {
2180
2471
  };
2181
2472
  return {
2182
2473
  next,
2183
- prob: (p) => next() < p,
2184
- range: (lo, hi) => lo + Math.floor(next() * (hi - lo + 1)),
2185
- float: (lo, hi) => lo + next() * (hi - lo)
2474
+ prob: (p) => next() < assertRange(p, 0, 1, "probability"),
2475
+ range: (lo, hi) => {
2476
+ assertInteger(lo, "range lower bound");
2477
+ assertInteger(hi, "range upper bound");
2478
+ if (lo > hi) {
2479
+ throw new RangeError(
2480
+ `range lower bound must not exceed upper bound; received ${lo} > ${hi}`
2481
+ );
2482
+ }
2483
+ assertInteger(hi - lo, "range span", 0);
2484
+ return lo + Math.floor(next() * (hi - lo + 1));
2485
+ },
2486
+ float: (lo, hi) => {
2487
+ assertFiniteNumber(lo, "float lower bound");
2488
+ assertFiniteNumber(hi, "float upper bound");
2489
+ if (lo > hi) {
2490
+ throw new RangeError(
2491
+ `float lower bound must not exceed upper bound; received ${lo} > ${hi}`
2492
+ );
2493
+ }
2494
+ assertFiniteNumber(hi - lo, "float span");
2495
+ return lo + next() * (hi - lo);
2496
+ }
2186
2497
  };
2187
2498
  }
2188
2499
 
2189
2500
  // src/core/tuning/index.ts
2190
2501
  var TWELVE_TET = { refStep: 69, refFreq: 440, divisions: 12 };
2502
+ function assertTuning(tuning) {
2503
+ assertFiniteNumber(tuning.refStep, "tuning.refStep");
2504
+ assertRange(tuning.refFreq, Number.MIN_VALUE, Number.MAX_VALUE, "tuning.refFreq");
2505
+ assertPositiveInt(tuning.divisions, "tuning.divisions");
2506
+ return tuning;
2507
+ }
2191
2508
  function edo(n, refFreq = 440, refStep = 69) {
2509
+ assertPositiveInt(n, "EDO divisions");
2510
+ assertRange(refFreq, Number.MIN_VALUE, Number.MAX_VALUE, "reference frequency");
2511
+ assertFiniteNumber(refStep, "reference step");
2192
2512
  return { refStep, refFreq, divisions: n };
2193
2513
  }
2194
2514
  function frequencyOf(step, tuning = TWELVE_TET) {
2195
- return tuning.refFreq * 2 ** ((step - tuning.refStep) / tuning.divisions);
2515
+ assertFiniteNumber(step, "step");
2516
+ assertTuning(tuning);
2517
+ const result = tuning.refFreq * 2 ** ((step - tuning.refStep) / tuning.divisions);
2518
+ return assertRange(result, Number.MIN_VALUE, Number.MAX_VALUE, "frequency result");
2196
2519
  }
2197
2520
  function nearestStep(freq, tuning = TWELVE_TET) {
2521
+ assertRange(freq, Number.MIN_VALUE, Number.MAX_VALUE, "frequency");
2522
+ assertTuning(tuning);
2198
2523
  return Math.round(tuning.refStep + tuning.divisions * Math.log2(freq / tuning.refFreq));
2199
2524
  }
2200
2525
  function centsBetweenFreq(a, b) {
2201
- return 1200 * Math.log2(b / a);
2526
+ assertRange(a, Number.MIN_VALUE, Number.MAX_VALUE, "first frequency");
2527
+ assertRange(b, Number.MIN_VALUE, Number.MAX_VALUE, "second frequency");
2528
+ return 1200 * (Math.log2(b) - Math.log2(a));
2202
2529
  }
2203
2530
  function centsOfSteps(steps, tuning = TWELVE_TET) {
2204
- return steps * 1200 / tuning.divisions;
2531
+ assertFiniteNumber(steps, "steps");
2532
+ assertTuning(tuning);
2533
+ return assertFiniteNumber(steps * 1200 / tuning.divisions, "cents result");
2205
2534
  }
2206
2535
  function ratioToCents(numerator, denominator) {
2207
- return 1200 * Math.log2(numerator / denominator);
2536
+ assertRange(numerator, Number.MIN_VALUE, Number.MAX_VALUE, "ratio numerator");
2537
+ assertRange(denominator, Number.MIN_VALUE, Number.MAX_VALUE, "ratio denominator");
2538
+ return 1200 * (Math.log2(numerator) - Math.log2(denominator));
2208
2539
  }
2209
2540
  var JUST_RATIOS = {
2210
2541
  0: [1, 1],
@@ -2222,9 +2553,10 @@ var JUST_RATIOS = {
2222
2553
  12: [2, 1]
2223
2554
  };
2224
2555
  function justDeviationCents(semitoneClass) {
2556
+ assertInteger(semitoneClass, "semitone class", 0, 12);
2225
2557
  const ratio = JUST_RATIOS[semitoneClass];
2226
2558
  if (!ratio) {
2227
- return Number.NaN;
2559
+ throw new RangeError(`semitone class has no just ratio: ${semitoneClass}`);
2228
2560
  }
2229
2561
  return ratioToCents(ratio[0], ratio[1]) - semitoneClass * 100;
2230
2562
  }
@@ -2236,7 +2568,7 @@ var DEFAULT_OCTAVE = 2;
2236
2568
  var STRONG_VELOCITY = 100;
2237
2569
  var WEAK_VELOCITY = 80;
2238
2570
  var PICKUP_PROB = 0.35;
2239
- var EPS5 = 1e-9;
2571
+ var EPS6 = 1e-9;
2240
2572
  function pitchClass9(value) {
2241
2573
  return (Math.trunc(value) % 12 + 12) % 12;
2242
2574
  }
@@ -2276,7 +2608,7 @@ function bassPcOf(chord) {
2276
2608
  }
2277
2609
  function beatPositions(start, end) {
2278
2610
  const positions = [];
2279
- for (let p = start; p < end - EPS5; p += 1) {
2611
+ for (let p = start; p < end - EPS6; p += 1) {
2280
2612
  positions.push(p);
2281
2613
  }
2282
2614
  if (positions.length === 0) {
@@ -2296,7 +2628,7 @@ function buildRoot(ctx, seg) {
2296
2628
  function buildRootFifth(ctx, seg) {
2297
2629
  ctx.prevMidi = emit(ctx, seg.startBeat, bassPcOf(seg.chord));
2298
2630
  const mid = (seg.startBeat + seg.endBeat) / 2;
2299
- if (mid > seg.startBeat + EPS5 && mid < seg.endBeat - EPS5) {
2631
+ if (mid > seg.startBeat + EPS6 && mid < seg.endBeat - EPS6) {
2300
2632
  ctx.prevMidi = emit(ctx, mid, fifthPcOf(seg.chord));
2301
2633
  }
2302
2634
  }
@@ -2358,13 +2690,30 @@ function buildWalking(ctx, seg, next) {
2358
2690
  });
2359
2691
  }
2360
2692
  function generateBassLine(opts) {
2693
+ assertGenerationBudget(opts.segments.length, "bass segments");
2694
+ for (let index = 0; index < opts.segments.length; index += 1) {
2695
+ const segment = opts.segments[index];
2696
+ if (!segment) continue;
2697
+ assertRange(segment.startBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].startBeat`);
2698
+ assertRange(segment.endBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].endBeat`);
2699
+ if (segment.endBeat <= segment.startBeat) {
2700
+ throw new RangeError(`segments[${index}] must have a positive duration`);
2701
+ }
2702
+ }
2361
2703
  const segments = [...opts.segments].sort((a, b) => a.startBeat - b.startBeat);
2362
2704
  if (segments.length === 0) {
2363
2705
  return [];
2364
2706
  }
2365
2707
  const ts = opts.ts ?? DEFAULT_TS;
2708
+ assertTimeSignature(ts);
2366
2709
  const style = opts.style ?? DEFAULT_STYLE;
2367
2710
  const octave = opts.octave ?? DEFAULT_OCTAVE;
2711
+ assertInteger(octave, "bass octave", -1, 9);
2712
+ const estimatedNotes = segments.reduce(
2713
+ (count, segment) => count + Math.max(1, Math.ceil(segment.endBeat - segment.startBeat)),
2714
+ 0
2715
+ );
2716
+ assertGenerationBudget(estimatedNotes, "bass notes");
2368
2717
  const low = octave * 12 + 12;
2369
2718
  const ctx = {
2370
2719
  ts,
@@ -2407,7 +2756,7 @@ function generateBassLine(opts) {
2407
2756
  }
2408
2757
  const nextStart = ctx.notes[i + 1]?.startBeat ?? lastEnd;
2409
2758
  const durationBeat = nextStart - note.startBeat;
2410
- if (durationBeat <= EPS5) {
2759
+ if (durationBeat <= EPS6) {
2411
2760
  continue;
2412
2761
  }
2413
2762
  out.push({
@@ -2422,7 +2771,7 @@ function generateBassLine(opts) {
2422
2771
 
2423
2772
  // src/generate/countermelody/index.ts
2424
2773
  var DEFAULT_TS2 = { numerator: 4, denominator: 4 };
2425
- var EPS6 = 1e-9;
2774
+ var EPS7 = 1e-9;
2426
2775
  var GRID_STEP = 0.5;
2427
2776
  var MIN_ONSET_GAP = 1;
2428
2777
  var REGISTER_OFFSET = 12;
@@ -2433,30 +2782,35 @@ var REINFORCE_PROB = 0.35;
2433
2782
  var DEFAULT_MELODY_VELOCITY = 96;
2434
2783
  var VELOCITY_DROP = 16;
2435
2784
  function isWholeBeat(beat) {
2436
- return Math.abs(beat - Math.round(beat)) < EPS6;
2785
+ return Math.abs(beat - Math.round(beat)) < EPS7;
2437
2786
  }
2438
2787
  function melodyNoteAt(melody, beat) {
2439
- let found;
2440
- for (const note of melody) {
2441
- if (note.startBeat - EPS6 <= beat && beat < note.startBeat + note.durationBeat - EPS6) {
2442
- if (!found || note.startBeat > found.startBeat) {
2443
- found = note;
2444
- }
2445
- }
2446
- }
2447
- return found;
2788
+ return melody.at(beat)?.note;
2448
2789
  }
2449
2790
  function melodyAttacksAt(melody, beat) {
2450
- return melody.some((note) => Math.abs(note.startBeat - beat) < EPS6);
2791
+ return melody.attacksAt(beat);
2792
+ }
2793
+ function melodySnapshotAt(melody, beat) {
2794
+ const current = melodyNoteAt(melody, beat);
2795
+ if (current === void 0) {
2796
+ return void 0;
2797
+ }
2798
+ const attacked = Math.abs(current.startBeat - beat) < EPS7;
2799
+ const previous = attacked ? melodyNoteAt(melody, beat - EPS7 * 2) : current;
2800
+ const snapshot = { pitch: current.pitch };
2801
+ if (previous !== void 0) {
2802
+ snapshot.prevPitch = previous.pitch;
2803
+ }
2804
+ return snapshot;
2451
2805
  }
2452
2806
  function followOnsets(melody) {
2453
- return [...new Set(melody.map((note) => note.startBeat))].sort((a, b) => a - b);
2807
+ return [...new Set(melody.notes.map(({ note }) => note.startBeat))];
2454
2808
  }
2455
2809
  function complementOnsets(melody, ts, spanStart, spanEnd, rng) {
2456
2810
  const onsets = [];
2457
2811
  const gridStart = Math.floor(spanStart / GRID_STEP) * GRID_STEP;
2458
2812
  let last = Number.NEGATIVE_INFINITY;
2459
- for (let beat = gridStart; beat < spanEnd - EPS6; beat += GRID_STEP) {
2813
+ for (let beat = gridStart; beat < spanEnd - EPS7; beat += GRID_STEP) {
2460
2814
  const attacked = melodyAttacksAt(melody, beat);
2461
2815
  const sounding = melodyNoteAt(melody, beat) !== void 0;
2462
2816
  let place = false;
@@ -2467,7 +2821,7 @@ function complementOnsets(melody, ts, spanStart, spanEnd, rng) {
2467
2821
  } else if (isStrongBeat(beat, ts)) {
2468
2822
  place = rng.prob(REINFORCE_PROB);
2469
2823
  }
2470
- if (place && beat - last >= MIN_ONSET_GAP - EPS6) {
2824
+ if (place && beat - last >= MIN_ONSET_GAP - EPS7) {
2471
2825
  onsets.push(beat);
2472
2826
  last = beat;
2473
2827
  }
@@ -2513,54 +2867,102 @@ function scoreCandidate(pitch, melPitch, melPrev, prevPitch, chord, center) {
2513
2867
  score -= Math.abs(pitch - center) * 0.02;
2514
2868
  return score;
2515
2869
  }
2516
- function fallbackPitch(melPitch, prevPitch, center, register, low, high, key) {
2517
- let target = prevPitch ?? Math.round(center);
2518
- if (melPitch !== void 0) {
2519
- if (register === "below" && target >= melPitch) {
2520
- target = melPitch - 5;
2521
- } else if (register === "above" && target <= melPitch) {
2522
- target = melPitch + 5;
2870
+ function heldNoteBoundaries(melody, startBeat, endBeat, chordChangeBeats) {
2871
+ const boundaries = /* @__PURE__ */ new Set([startBeat]);
2872
+ for (const onset of melody.onsetsBetween(startBeat, endBeat)) {
2873
+ boundaries.add(onset);
2874
+ }
2875
+ for (const beat of chordChangeBeats) {
2876
+ if (beat > startBeat + EPS7 && beat < endBeat - EPS7) {
2877
+ boundaries.add(beat);
2523
2878
  }
2524
2879
  }
2525
- target = Math.min(Math.max(target, low), high);
2526
- let pitch = nearestScaleTone(target, key);
2527
- if (melPitch !== void 0) {
2528
- while (register === "below" && pitch >= melPitch && pitch - 12 >= low) {
2529
- pitch -= 12;
2880
+ const firstGrid = Math.floor(startBeat / GRID_STEP + 1) * GRID_STEP;
2881
+ for (let beat = firstGrid; beat < endBeat - EPS7; beat += GRID_STEP) {
2882
+ boundaries.add(beat);
2883
+ }
2884
+ return [...boundaries].sort((a, b) => a - b);
2885
+ }
2886
+ function heldPitchSafety(pitch, startBeat, endBeat, prevPitch, melody, opts, profile, register, low, high, ts) {
2887
+ let worst = 0 /* Safe */;
2888
+ const boundaries = heldNoteBoundaries(melody, startBeat, endBeat, opts.chordChangeBeats ?? []);
2889
+ for (const boundary of boundaries) {
2890
+ const melodyVoice = melodySnapshotAt(melody, boundary);
2891
+ if (melodyVoice !== void 0 && (register === "below" && pitch >= melodyVoice.pitch || register === "above" && pitch <= melodyVoice.pitch)) {
2892
+ return null;
2530
2893
  }
2531
- while (register === "above" && pitch <= melPitch && pitch + 12 <= high) {
2532
- pitch += 12;
2894
+ const atCounterOnset = Math.abs(boundary - startBeat) < EPS7;
2895
+ const result = evaluateSafety({
2896
+ profile,
2897
+ candidatePitch: pitch,
2898
+ prevPitch: atCounterOnset ? prevPitch : pitch,
2899
+ chord: opts.chordAt(boundary),
2900
+ key: opts.key,
2901
+ otherVoices: melodyVoice ? [melodyVoice] : [],
2902
+ strongBeat: isStrongBeat(boundary, ts),
2903
+ vocalLow: low,
2904
+ vocalHigh: high
2905
+ });
2906
+ worst = Math.max(worst, result.safety);
2907
+ if (worst === 2 /* Dissonant */) {
2908
+ return worst;
2533
2909
  }
2534
2910
  }
2535
- return Math.min(Math.max(pitch, low), high);
2911
+ return worst;
2536
2912
  }
2537
2913
  function generateCounterMelody(opts) {
2538
- const melody = [...opts.melody].sort((a, b) => a.startBeat - b.startBeat);
2539
- if (melody.length === 0) {
2914
+ const melody = createNoteEventIndex(opts.melody);
2915
+ if (melody.notes.length === 0) {
2540
2916
  return [];
2541
2917
  }
2542
2918
  const ts = opts.ts ?? DEFAULT_TS2;
2919
+ assertTimeSignature(ts);
2543
2920
  const register = opts.register ?? "below";
2544
2921
  const rhythm = opts.rhythm ?? "complement";
2545
2922
  const profile = opts.profile ?? "pop";
2546
2923
  const rng = createRng(opts.seed ?? 0);
2547
- const spanStart = melody[0]?.startBeat ?? 0;
2548
- const spanEnd = melody.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), spanStart);
2549
- const meanPitch = melody.reduce((sum, n) => sum + n.pitch, 0) / melody.length;
2924
+ const spanStart = melody.notes[0]?.note.startBeat ?? 0;
2925
+ const spanEnd = melody.notes.reduce((end, n) => Math.max(end, n.endBeat), spanStart);
2926
+ assertGenerationBudget(Math.ceil((spanEnd - spanStart) / GRID_STEP), "countermelody grid");
2927
+ const meanPitch = melody.notes.reduce((sum, n) => sum + n.note.pitch, 0) / melody.notes.length;
2550
2928
  const defaultCenter = register === "below" ? meanPitch - REGISTER_OFFSET : meanPitch + REGISTER_OFFSET;
2551
2929
  const low = opts.pitchLow ?? Math.round(defaultCenter) - DEFAULT_HALF_RANGE;
2552
2930
  const high = opts.pitchHigh ?? Math.round(defaultCenter) + DEFAULT_HALF_RANGE;
2931
+ assertInteger(low, "countermelody pitchLow");
2932
+ assertInteger(high, "countermelody pitchHigh");
2933
+ if (low > high) {
2934
+ throw new RangeError(
2935
+ `countermelody pitchLow must not exceed pitchHigh; received ${low} > ${high}`
2936
+ );
2937
+ }
2938
+ assertGenerationBudget(high - low + 1, "countermelody pitch candidates");
2939
+ assertGenerationBudget(opts.chordChangeBeats?.length ?? 0, "chord change beats");
2940
+ for (let index = 0; index < (opts.chordChangeBeats?.length ?? 0); index += 1) {
2941
+ assertRange(
2942
+ opts.chordChangeBeats?.[index] ?? Number.NaN,
2943
+ 0,
2944
+ Number.MAX_SAFE_INTEGER,
2945
+ `chordChangeBeats[${index}]`
2946
+ );
2947
+ }
2553
2948
  const center = (low + high) / 2;
2554
2949
  const onsets = rhythm === "follow" ? followOnsets(melody) : complementOnsets(melody, ts, spanStart, spanEnd, rng);
2950
+ assertGenerationBudget(onsets.length * (high - low + 1), "countermelody search");
2555
2951
  const out = [];
2556
2952
  let prevPitch;
2557
- let prevOnset;
2558
- for (const beat of onsets) {
2953
+ for (let onsetIndex = 0; onsetIndex < onsets.length; onsetIndex += 1) {
2954
+ const beat = onsets[onsetIndex];
2955
+ if (beat === void 0) {
2956
+ continue;
2957
+ }
2958
+ const nextOnset = onsets[onsetIndex + 1];
2959
+ const endBeat = nextOnset ?? Math.max(spanEnd, beat + GRID_STEP);
2559
2960
  const melNote = melodyNoteAt(melody, beat);
2560
2961
  const melPitch = melNote?.pitch;
2561
- const melPrev = prevOnset !== void 0 ? melodyNoteAt(melody, prevOnset)?.pitch : void 0;
2962
+ const melodyVoice = melodySnapshotAt(melody, beat);
2963
+ const melPrev = melodyVoice?.prevPitch;
2562
2964
  const chord = opts.chordAt(beat);
2563
- const otherVoices = melPitch !== void 0 ? [{ pitch: melPitch, prevPitch: melPrev }] : [];
2965
+ const otherVoices = melodyVoice !== void 0 ? [melodyVoice] : [];
2564
2966
  const candidates = enumerateSafePitches(
2565
2967
  {
2566
2968
  profile,
@@ -2576,15 +2978,26 @@ function generateCounterMelody(opts) {
2576
2978
  high
2577
2979
  );
2578
2980
  let bestPitch;
2981
+ let bestWorstSafety = 2 /* Dissonant */;
2579
2982
  let bestScore = Number.NEGATIVE_INFINITY;
2580
2983
  for (const candidate of candidates) {
2984
+ const worstSafety = heldPitchSafety(
2985
+ candidate,
2986
+ beat,
2987
+ endBeat,
2988
+ prevPitch,
2989
+ melody,
2990
+ opts,
2991
+ profile,
2992
+ register,
2993
+ low,
2994
+ high,
2995
+ ts
2996
+ );
2997
+ if (worstSafety === null || worstSafety === 2 /* Dissonant */) {
2998
+ continue;
2999
+ }
2581
3000
  if (melPitch !== void 0) {
2582
- if (register === "below" && candidate >= melPitch) {
2583
- continue;
2584
- }
2585
- if (register === "above" && candidate <= melPitch) {
2586
- continue;
2587
- }
2588
3001
  if (prevPitch !== void 0 && melPrev !== void 0 && // Parallel octaves are the perfect-class-zero case of this predicate,
2589
3002
  // so a single check covers both parallel fifths and octaves.
2590
3003
  createsParallelPerfect(prevPitch, candidate, melPrev, melPitch)) {
@@ -2592,29 +3005,24 @@ function generateCounterMelody(opts) {
2592
3005
  }
2593
3006
  }
2594
3007
  const score = scoreCandidate(candidate, melPitch, melPrev, prevPitch, chord, center);
2595
- const tie = Math.abs(score - bestScore) <= EPS6 && candidate < (bestPitch ?? Number.POSITIVE_INFINITY);
2596
- if (score > bestScore + EPS6 || tie) {
3008
+ const tie = Math.abs(score - bestScore) <= EPS7 && candidate < (bestPitch ?? Number.POSITIVE_INFINITY);
3009
+ if (worstSafety < bestWorstSafety || worstSafety === bestWorstSafety && (score > bestScore + EPS7 || tie)) {
3010
+ bestWorstSafety = worstSafety;
2597
3011
  bestScore = score;
2598
3012
  bestPitch = candidate;
2599
3013
  }
2600
3014
  }
2601
- const pitch = bestPitch ?? fallbackPitch(melPitch, prevPitch, center, register, low, high, opts.key);
3015
+ if (bestPitch === void 0) {
3016
+ prevPitch = void 0;
3017
+ continue;
3018
+ }
3019
+ const pitch = bestPitch;
2602
3020
  const velocity = Math.max(
2603
3021
  1,
2604
3022
  Math.min(127, Math.round((melNote?.velocity ?? DEFAULT_MELODY_VELOCITY) - VELOCITY_DROP))
2605
3023
  );
2606
- out.push({ pitch, startBeat: beat, durationBeat: GRID_STEP, velocity });
3024
+ out.push({ pitch, startBeat: beat, durationBeat: endBeat - beat, velocity });
2607
3025
  prevPitch = pitch;
2608
- prevOnset = beat;
2609
- }
2610
- for (let i = 0; i < out.length; i += 1) {
2611
- const note = out[i];
2612
- if (!note) {
2613
- continue;
2614
- }
2615
- const next = out[i + 1];
2616
- const end = next ? next.startBeat : Math.max(spanEnd, note.startBeat + GRID_STEP);
2617
- note.durationBeat = end - note.startBeat;
2618
3026
  }
2619
3027
  return out;
2620
3028
  }
@@ -3334,9 +3742,10 @@ function generateHiHatForBeat(ctx, sec) {
3334
3742
 
3335
3743
  // src/generate/drums/euclid.ts
3336
3744
  function euclideanRhythm(pulses, steps, rotation = 0) {
3337
- if (steps < 1) {
3338
- return [];
3339
- }
3745
+ assertPositiveInt(steps, "euclidean steps");
3746
+ assertInteger(pulses, "euclidean pulses");
3747
+ assertInteger(rotation, "euclidean rotation");
3748
+ assertGenerationBudget(steps, "euclidean pattern steps");
3340
3749
  const p = Math.max(0, Math.min(pulses, steps));
3341
3750
  const base = bjorklund(p, steps);
3342
3751
  const shift = (rotation % steps + steps) % steps;
@@ -3345,18 +3754,6 @@ function euclideanRhythm(pulses, steps, rotation = 0) {
3345
3754
  }
3346
3755
  return base.map((_, i) => base[(i - shift + steps) % steps] ?? false);
3347
3756
  }
3348
- function hasHit(mask, step) {
3349
- return (mask >> step & 1) === 1;
3350
- }
3351
- function patternToMask(pattern) {
3352
- let mask = 0;
3353
- for (let i = 0; i < pattern.length; i += 1) {
3354
- if (pattern[i]) {
3355
- mask |= 1 << i;
3356
- }
3357
- }
3358
- return mask;
3359
- }
3360
3757
  function bjorklund(pulses, steps) {
3361
3758
  if (pulses === 0) {
3362
3759
  return Array.from({ length: steps }, () => false);
@@ -3688,19 +4085,6 @@ function isInPreChorusLift(section, bar, sectionBars, nextIsChorus) {
3688
4085
  }
3689
4086
  return bar >= sectionBars - 2;
3690
4087
  }
3691
- function euclideanToKickPattern(mask) {
3692
- const slot = (step) => hasHit(mask, step) || hasHit(mask, (step + 15) % 16);
3693
- return {
3694
- beat1: slot(0),
3695
- beat1and: slot(2),
3696
- beat2: slot(4),
3697
- beat2and: slot(6),
3698
- beat3: slot(8),
3699
- beat3and: slot(10),
3700
- beat4: slot(12),
3701
- beat4and: slot(14)
3702
- };
3703
- }
3704
4088
  function getKickPattern(section, style, bar, rng) {
3705
4089
  const p = { ...EMPTY };
3706
4090
  if (section === "outro") {
@@ -3822,7 +4206,7 @@ function getPercussionConfig(mood, section) {
3822
4206
  };
3823
4207
  }
3824
4208
  function generateAuxPercussionForBar(track, barStart, config, role, densityMult, rng, bpm) {
3825
- if (role === "minimal" || role === "fxOnly") {
4209
+ if (role === "minimal") {
3826
4210
  return;
3827
4211
  }
3828
4212
  if (config.tambourine) {
@@ -3871,6 +4255,16 @@ function generateAuxPercussionForBar(track, barStart, config, role, densityMult,
3871
4255
 
3872
4256
  // src/generate/drums/index.ts
3873
4257
  function generateDrums(opts) {
4258
+ assertPositiveInt(opts.bars, "drum bars");
4259
+ assertRange(opts.bpm, Number.MIN_VALUE, 1e3, "drum bpm");
4260
+ assertRange(opts.density, 0, 1, "drum density");
4261
+ assertGenerationBudget(opts.bars * 128, "drum hits");
4262
+ if (opts.euclideanKick) {
4263
+ const steps = opts.euclideanKick.steps ?? 16;
4264
+ assertInteger(steps, "euclidean steps", 1, 16);
4265
+ assertInteger(opts.euclideanKick.pulses, "euclidean pulses");
4266
+ assertInteger(opts.euclideanKick.rotation ?? 0, "euclidean rotation");
4267
+ }
3874
4268
  const track = new HitList();
3875
4269
  const rng = createRng(opts.seed ?? 0);
3876
4270
  const mapping = mapStyle(opts.style);
@@ -3907,15 +4301,8 @@ function generateDrums(opts) {
3907
4301
  const percMood = percMoodCategory(style);
3908
4302
  const nextSection = opts.nextSection ? mapSection(opts.nextSection) : section;
3909
4303
  const nextEnergy = sectionEnergy(nextSection);
3910
- const euclidKick = opts.euclideanKick ? euclideanToKickPattern(
3911
- patternToMask(
3912
- euclideanRhythm(
3913
- opts.euclideanKick.pulses,
3914
- opts.euclideanKick.steps ?? 16,
3915
- opts.euclideanKick.rotation ?? 0
3916
- )
3917
- )
3918
- ) : void 0;
4304
+ const euclidSteps = opts.euclideanKick?.steps ?? 16;
4305
+ const euclidKick = opts.euclideanKick ? euclideanRhythm(opts.euclideanKick.pulses, euclidSteps, opts.euclideanKick.rotation ?? 0) : void 0;
3919
4306
  const reuseSectionKick = (section === "b" || section === "chorus") && style !== "sparse";
3920
4307
  let sectionKick;
3921
4308
  for (let bar = 0; bar < opts.bars; bar += 1) {
@@ -3931,12 +4318,10 @@ function generateDrums(opts) {
3931
4318
  barHasOpenHh = !track.hasCrashNear(barStart + openHhBeatIndex);
3932
4319
  }
3933
4320
  let kick;
3934
- if (euclidKick) {
3935
- kick = euclidKick;
3936
- } else if (reuseSectionKick) {
4321
+ if (!euclidKick && reuseSectionKick) {
3937
4322
  sectionKick ??= getKickPattern(section, style, 0, rng);
3938
4323
  kick = sectionKick;
3939
- } else {
4324
+ } else if (!euclidKick) {
3940
4325
  kick = getKickPattern(section, style, bar, rng);
3941
4326
  }
3942
4327
  const inLift = isInPreChorusLift(section, bar, opts.bars, section === "b");
@@ -3977,7 +4362,23 @@ function generateDrums(opts) {
3977
4362
  if (inLift) {
3978
4363
  generatePreChorusBuildup(ctx, sec, isLastBar);
3979
4364
  }
3980
- generateKickForBeat(ctx, sec, kick);
4365
+ if (euclidKick && !inLift) {
4366
+ const stepLength = 4 / euclidSteps;
4367
+ for (let step = 0; step < euclidKick.length; step += 1) {
4368
+ const onset = step * stepLength;
4369
+ if (euclidKick[step] && Math.floor(onset) === beat) {
4370
+ const wholeBeat = Math.abs(onset - Math.round(onset)) < 1e-9;
4371
+ track.add(
4372
+ GM.BD,
4373
+ barStart + onset,
4374
+ Math.min(0.5, stepLength),
4375
+ velocity * (wholeBeat ? 1 : 0.85)
4376
+ );
4377
+ }
4378
+ }
4379
+ } else if (kick) {
4380
+ generateKickForBeat(ctx, sec, kick);
4381
+ }
3981
4382
  generateSnareForBeat(ctx, sec, section === "intro" && bar === 0);
3982
4383
  if (sec.useGhostNotes && !inLift) {
3983
4384
  generateGhostNotesForBeat(ctx, sec);
@@ -3994,10 +4395,19 @@ function generateDrums(opts) {
3994
4395
  }
3995
4396
  }
3996
4397
  }
4398
+ let percussionConfig = getPercussionConfig(percMood, section);
4399
+ if (role === "fxOnly" && !percussionConfig.tambourine && !percussionConfig.shaker && !percussionConfig.handclap) {
4400
+ percussionConfig = {
4401
+ tambourine: false,
4402
+ shaker: true,
4403
+ handclap: false,
4404
+ shaker16th: false
4405
+ };
4406
+ }
3997
4407
  generateAuxPercussionForBar(
3998
4408
  track,
3999
4409
  barStart,
4000
- getPercussionConfig(percMood, section),
4410
+ percussionConfig,
4001
4411
  role,
4002
4412
  densityMult,
4003
4413
  rng,
@@ -4018,10 +4428,16 @@ var MIN_VELOCITY = 1;
4018
4428
  var MAX_VELOCITY = 127;
4019
4429
  function humanize(events, opts = {}) {
4020
4430
  const ts = opts.ts ?? DEFAULT_TS3;
4431
+ assertTimeSignature(ts);
4432
+ assertNoteEvents(events, "humanize events");
4021
4433
  const timing = opts.timing ?? DEFAULT_TIMING;
4022
4434
  const velocityJitter = opts.velocity ?? DEFAULT_VELOCITY_JITTER;
4023
4435
  const accent = opts.accent ?? DEFAULT_ACCENT;
4024
4436
  const baseVelocity = opts.baseVelocity ?? DEFAULT_BASE_VELOCITY;
4437
+ assertRange(timing, 0, Number.MAX_SAFE_INTEGER, "humanize timing");
4438
+ assertRange(velocityJitter, 0, 127, "humanize velocity jitter");
4439
+ assertRange(accent, 0, 127, "humanize accent");
4440
+ assertRange(baseVelocity, 0, 127, "humanize base velocity");
4025
4441
  const rng = createRng(opts.seed ?? 0);
4026
4442
  return events.map((event) => {
4027
4443
  const timingOffset = rng.float(-timing, timing);
@@ -4054,8 +4470,12 @@ function quantizeToGrid(beat, barBeats, subdivision, slotsPerBar) {
4054
4470
  };
4055
4471
  }
4056
4472
  function extractGrooveTemplate(events, ts, subdivision) {
4473
+ assertTimeSignature(ts);
4474
+ assertNoteEvents(events, "groove source events");
4475
+ assertPositiveInt(subdivision, "groove subdivision");
4057
4476
  const barBeats = beatsPerBar(ts);
4058
4477
  const slotsPerBar = Math.round(barBeats * subdivision);
4478
+ assertGenerationBudget(slotsPerBar, "groove template slots");
4059
4479
  const offsetSums = new Array(slotsPerBar).fill(0);
4060
4480
  const velocitySums = new Array(slotsPerBar).fill(0);
4061
4481
  const counts = new Array(slotsPerBar).fill(0);
@@ -4090,6 +4510,14 @@ function extractGrooveTemplate(events, ts, subdivision) {
4090
4510
  return { subdivision, slotsPerBar, slots, ts };
4091
4511
  }
4092
4512
  function applyGrooveTemplate(events, template, ts) {
4513
+ assertTimeSignature(ts);
4514
+ assertNoteEvents(events, "groove target events");
4515
+ assertPositiveInt(template.subdivision, "template subdivision");
4516
+ assertPositiveInt(template.slotsPerBar, "template slotsPerBar");
4517
+ assertGenerationBudget(template.slots.length, "template slots");
4518
+ if (template.slots.length !== template.slotsPerBar) {
4519
+ throw new RangeError("template slots length must equal slotsPerBar");
4520
+ }
4093
4521
  if (template.ts && (template.ts.numerator !== ts.numerator || template.ts.denominator !== ts.denominator)) {
4094
4522
  throw new Error(
4095
4523
  `Groove template meter ${formatTimeSignature(template.ts)} does not match apply-time meter ${formatTimeSignature(ts)}`
@@ -4119,32 +4547,17 @@ function applyGrooveTemplate(events, template, ts) {
4119
4547
  function roleOf(pitch, chord, chordId = 0) {
4120
4548
  const interval = ((Math.trunc(pitch) - chord.rootPc) % 12 + 12) % 12;
4121
4549
  const tones = new Set(chord.intervals.map((i) => (i % 12 + 12) % 12));
4122
- const hasHigherSeventh = tones.has(10) || tones.has(11);
4123
4550
  const hasThird = tones.has(3) || tones.has(4);
4124
4551
  const isSuspendedTone = !hasThird && (interval === 5 && tones.has(5) || interval === 2 && tones.has(2));
4552
+ const chordRole = chordToneRole(pitch, chord);
4125
4553
  let role;
4126
4554
  let lock;
4127
- if (interval === 0) {
4128
- role = "root";
4129
- lock = "identity";
4130
- } else if (isSuspendedTone) {
4131
- role = "third";
4132
- lock = "quality";
4133
- } else if (interval === 3 || interval === 4) {
4555
+ if (isSuspendedTone) {
4134
4556
  role = "third";
4135
4557
  lock = "quality";
4136
- } else if (interval === 6 || interval === 7 || interval === 8) {
4137
- role = "fifth";
4138
- lock = "voicing";
4139
- } else if (interval === 9 && tones.has(3) && tones.has(6) && !hasHigherSeventh) {
4140
- role = "seventh";
4141
- lock = "voicing";
4142
- } else if (interval === 9 && tones.has(9) && !hasHigherSeventh) {
4143
- role = "sixth";
4144
- lock = "voicing";
4145
- } else if (interval === 10 || interval === 11) {
4146
- role = "seventh";
4147
- lock = "voicing";
4558
+ } else if (chordRole !== null) {
4559
+ role = chordRole;
4560
+ lock = chordRole === "root" ? "identity" : chordRole === "third" ? "quality" : "voicing";
4148
4561
  } else {
4149
4562
  role = "tension";
4150
4563
  lock = "voicing";
@@ -4162,16 +4575,6 @@ var TIE_BREAK_JITTER = 1e-6;
4162
4575
  function pitchClass10(pitch) {
4163
4576
  return (Math.trunc(pitch) % 12 + 12) % 12;
4164
4577
  }
4165
- function mulberry32(seed) {
4166
- let state = seed >>> 0;
4167
- return () => {
4168
- state = state + 1831565813 >>> 0;
4169
- let t = state;
4170
- t = Math.imul(t ^ t >>> 15, t | 1);
4171
- t ^= t + Math.imul(t ^ t >>> 7, t | 61);
4172
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
4173
- };
4174
- }
4175
4578
  function inferKey(melody) {
4176
4579
  let best = majorKey(0);
4177
4580
  let bestScore = Number.NEGATIVE_INFINITY;
@@ -4210,7 +4613,6 @@ function buildCandidates(key, reharmonize) {
4210
4613
  secondaryDominant: false,
4211
4614
  base: 0
4212
4615
  }));
4213
- const tonic = tones[0] ?? 0;
4214
4616
  if (reharmonize !== "diatonic") {
4215
4617
  for (const target of [1, 3, 4, 5]) {
4216
4618
  const targetRoot = tones[target] ?? 0;
@@ -4224,26 +4626,29 @@ function buildCandidates(key, reharmonize) {
4224
4626
  }
4225
4627
  }
4226
4628
  if (reharmonize === "borrowed") {
4227
- candidates.push({
4228
- rootPc: (tonic + 10) % 12,
4229
- quality: "maj",
4230
- secondaryDominant: false,
4231
- base: 2
4232
- });
4233
- candidates.push({
4234
- rootPc: (tonic + 8) % 12,
4235
- quality: "maj",
4236
- secondaryDominant: false,
4237
- base: 2
4238
- });
4239
- candidates.push({
4240
- rootPc: (tonic + 5) % 12,
4241
- quality: "min",
4242
- secondaryDominant: false,
4243
- base: 2
4244
- });
4629
+ const parallel = parallelKey(key);
4630
+ const parallelTones = scaleTonesInDegreeOrder(parallel);
4631
+ for (let degree = 0; degree < parallelTones.length; degree += 1) {
4632
+ const chord = diatonicTriad(degree, parallel);
4633
+ if (!isDiatonic(chord, key)) {
4634
+ candidates.push({
4635
+ rootPc: chord.rootPc,
4636
+ quality: chord.quality,
4637
+ secondaryDominant: false,
4638
+ base: 1.25
4639
+ });
4640
+ }
4641
+ }
4245
4642
  }
4246
- return candidates;
4643
+ const seen = /* @__PURE__ */ new Set();
4644
+ return candidates.filter((candidate) => {
4645
+ const id = `${candidate.rootPc}:${candidate.quality}`;
4646
+ if (seen.has(id)) {
4647
+ return false;
4648
+ }
4649
+ seen.add(id);
4650
+ return true;
4651
+ });
4247
4652
  }
4248
4653
  function emissionCost(seg, cand, melody, key) {
4249
4654
  const pcs = chordPitchClasses(makeChord(cand.rootPc, cand.quality));
@@ -4347,20 +4752,37 @@ function harmonizeOnce(melody, key, candidates, segments, jitter) {
4347
4752
  return { cost: bestCost, path };
4348
4753
  }
4349
4754
  function harmonizeMelody(opts) {
4755
+ const noteIndex = createNoteEventIndex(opts.melody);
4350
4756
  const key = opts.key === "infer" ? inferKey(opts.melody) : opts.key;
4351
4757
  const candidates = buildCandidates(key, opts.reharmonize);
4352
4758
  const candAt = (i) => candidates[i] ?? FALLBACK;
4353
- const rng = mulberry32(opts.seed ?? 0);
4354
- const jitter = candidates.map(() => rng() * TIE_BREAK_JITTER);
4355
- const melodyEnd = opts.melody.reduce((m, n) => Math.max(m, n.startBeat + n.durationBeat), 0);
4356
- const hr = Math.max(0.25, opts.harmonicRhythm);
4759
+ const rng = createRng(opts.seed ?? 0);
4760
+ const jitter = candidates.map(() => rng.next() * TIE_BREAK_JITTER);
4761
+ const melodyEnd = noteIndex.notes.reduce((m, n) => Math.max(m, n.endBeat), 0);
4762
+ const requestedHr = assertRange(
4763
+ opts.harmonicRhythm,
4764
+ Number.MIN_VALUE,
4765
+ Number.MAX_SAFE_INTEGER,
4766
+ "harmonic rhythm"
4767
+ );
4768
+ const hr = Math.max(0.25, requestedHr);
4357
4769
  const segCount = Math.max(1, Math.ceil(melodyEnd / hr));
4358
- const segments = [];
4359
- for (let s = 0; s < segCount; s += 1) {
4360
- const startBeat = s * hr;
4361
- const endBeat = startBeat + hr;
4362
- const noteIndices = opts.melody.map((n, i) => ({ n, i })).filter(({ n }) => n.startBeat < endBeat && n.startBeat + n.durationBeat > startBeat).map(({ i }) => i);
4363
- segments.push({ startBeat, endBeat, noteIndices });
4770
+ assertGenerationBudget(segCount, "harmonic segments");
4771
+ assertGenerationBudget(segCount * candidates.length * candidates.length, "harmonization search");
4772
+ const segments = Array.from({ length: segCount }, (_, s) => ({
4773
+ startBeat: s * hr,
4774
+ endBeat: (s + 1) * hr,
4775
+ noteIndices: []
4776
+ }));
4777
+ let memberships = 0;
4778
+ for (const indexed of noteIndex.notes) {
4779
+ const first = Math.max(0, Math.floor(indexed.note.startBeat / hr));
4780
+ const lastExclusive = Math.min(segCount, Math.ceil(indexed.endBeat / hr - Number.EPSILON));
4781
+ memberships += Math.max(0, lastExclusive - first);
4782
+ assertGenerationBudget(memberships, "note-to-segment memberships");
4783
+ for (let segment = first; segment < lastExclusive; segment += 1) {
4784
+ segments[segment]?.noteIndices.push(indexed.originalIndex);
4785
+ }
4364
4786
  }
4365
4787
  const transposes = [0];
4366
4788
  if (opts.placement.transposeSearch) {
@@ -4407,11 +4829,11 @@ function harmonizeMelody(opts) {
4407
4829
  }
4408
4830
  return chord;
4409
4831
  });
4410
- const melodyRoles = bestMelody.map((note, noteIndex) => {
4832
+ const melodyRoles = bestMelody.map((note, noteIndex2) => {
4411
4833
  const segIdx = Math.min(segments.length - 1, Math.max(0, Math.floor(note.startBeat / hr)));
4412
4834
  const cand = candAt(bestPath[segIdx] ?? 0);
4413
4835
  const chord = makeChord(cand.rootPc, cand.quality);
4414
- return { noteIndex, role: roleOf(note.pitch, chord).role };
4836
+ return { noteIndex: noteIndex2, role: roleOf(note.pitch, chord).role };
4415
4837
  });
4416
4838
  return { transposeSemitones: bestTs, key, chords, melodyRoles };
4417
4839
  }
@@ -4458,29 +4880,30 @@ function nearestChordTone(pitch, chord) {
4458
4880
  return pitch;
4459
4881
  }
4460
4882
  function cellOrigin(notes) {
4461
- return notes.length === 0 ? 0 : Math.min(...notes.map((n) => n.startBeat));
4883
+ let origin = Number.POSITIVE_INFINITY;
4884
+ for (const note of notes) {
4885
+ origin = Math.min(origin, note.startBeat);
4886
+ }
4887
+ return Number.isFinite(origin) ? origin : 0;
4888
+ }
4889
+ function cellEnd(notes) {
4890
+ let end = Number.NEGATIVE_INFINITY;
4891
+ for (const note of notes) {
4892
+ end = Math.max(end, note.startBeat + note.durationBeat);
4893
+ }
4894
+ return Number.isFinite(end) ? end : 0;
4462
4895
  }
4463
4896
  function cellSpan(cell) {
4464
4897
  if (cell.notes.length === 0) {
4465
4898
  return 0;
4466
4899
  }
4467
4900
  const start = cellOrigin(cell.notes);
4468
- const end = Math.max(...cell.notes.map((n) => n.startBeat + n.durationBeat));
4901
+ const end = cellEnd(cell.notes);
4469
4902
  return end - start;
4470
4903
  }
4471
4904
  function clone(cell) {
4472
4905
  return { notes: cell.notes.map((n) => ({ ...n })) };
4473
4906
  }
4474
- function mulberry322(seed) {
4475
- let state = seed >>> 0;
4476
- return () => {
4477
- state = state + 1831565813 >>> 0;
4478
- let t = state;
4479
- t = Math.imul(t ^ t >>> 15, t | 1);
4480
- t ^= t + Math.imul(t ^ t >>> 7, t | 61);
4481
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
4482
- };
4483
- }
4484
4907
  function contourOffsets(contour, count) {
4485
4908
  const offsets = [];
4486
4909
  for (let i = 0; i < count; i += 1) {
@@ -4503,19 +4926,21 @@ function contourOffsets(contour, count) {
4503
4926
  }
4504
4927
  function generateMotif(opts) {
4505
4928
  const contour = opts.contour ?? "arch";
4506
- const bars = Math.max(1, Math.trunc(opts.bars));
4929
+ const bars = assertPositiveInt(opts.bars, "motif bars");
4507
4930
  const totalBeats = bars * 4;
4508
4931
  const noteCount = Math.max(3, bars * 2);
4932
+ assertGenerationBudget(noteCount, "motif notes");
4509
4933
  const beatsPerNote = totalBeats / noteCount;
4510
- const rng = mulberry322(opts.seed ?? 0);
4511
- const jitterProb = Math.min(1, Math.max(0, opts.jitter ?? 0));
4934
+ const rng = createRng(opts.seed ?? 0);
4935
+ const requestedJitter = assertFiniteNumber(opts.jitter ?? 0, "motif jitter");
4936
+ const jitterProb = Math.min(1, Math.max(0, requestedJitter));
4512
4937
  const tonic = pitchClass11(opts.key.rootPc) + 60;
4513
4938
  const offsets = contourOffsets(contour, noteCount);
4514
4939
  const notes = [];
4515
4940
  for (let i = 0; i < noteCount; i += 1) {
4516
4941
  let jitter = 0;
4517
- if (jitterProb > 0 && rng() < jitterProb) {
4518
- jitter = rng() < 0.5 ? 1 : -1;
4942
+ if (jitterProb > 0 && rng.next() < jitterProb) {
4943
+ jitter = rng.next() < 0.5 ? 1 : -1;
4519
4944
  }
4520
4945
  let pitch = stepDiatonic(tonic, (offsets[i] ?? 0) + jitter, opts.key);
4521
4946
  const startBeat = i * beatsPerNote;
@@ -4527,6 +4952,16 @@ function generateMotif(opts) {
4527
4952
  return { notes };
4528
4953
  }
4529
4954
  function transformMotif(cell, t, amount, key) {
4955
+ assertNoteEvents(cell.notes, "motif notes");
4956
+ if (amount !== void 0) {
4957
+ assertFiniteNumber(amount, "motif transform amount");
4958
+ }
4959
+ if ((t === "transposeDiatonic" || t === "sequence") && amount !== void 0) {
4960
+ assertInteger(amount, "diatonic transform amount");
4961
+ }
4962
+ if ((t === "augment" || t === "diminish") && amount !== void 0 && amount <= 0) {
4963
+ throw new RangeError("time transform amount must be positive");
4964
+ }
4530
4965
  const notes = cell.notes;
4531
4966
  switch (t) {
4532
4967
  case "transposeChromatic": {
@@ -4553,7 +4988,7 @@ function transformMotif(cell, t, amount, key) {
4553
4988
  return { notes: [] };
4554
4989
  }
4555
4990
  const start = cellOrigin(notes);
4556
- const end = Math.max(...notes.map((n) => n.startBeat + n.durationBeat));
4991
+ const end = cellEnd(notes);
4557
4992
  return {
4558
4993
  notes: notes.map((n) => ({
4559
4994
  pitch: n.pitch,
@@ -4586,8 +5021,10 @@ function scaleTime(cell, factor) {
4586
5021
  };
4587
5022
  }
4588
5023
  function developMotif(cell, timeline, key, bars) {
5024
+ assertPositiveInt(bars, "development bars");
5025
+ assertNoteEvents(cell.notes, "motif notes");
4589
5026
  const span = cellSpan(cell);
4590
- const totalBeats = Math.max(1, Math.trunc(bars)) * 4;
5027
+ const totalBeats = bars * 4;
4591
5028
  const origin = cellOrigin(cell.notes);
4592
5029
  const out = [];
4593
5030
  if (span <= 0) {
@@ -4595,6 +5032,7 @@ function developMotif(cell, timeline, key, bars) {
4595
5032
  }
4596
5033
  const tileSpan = Math.max(span, MIN_TILE_SPAN);
4597
5034
  const tileCount = Math.ceil(totalBeats / tileSpan);
5035
+ assertGenerationBudget(tileCount * cell.notes.length, "developed motif notes");
4598
5036
  for (let k = 0; k < tileCount; k += 1) {
4599
5037
  const offset = k * tileSpan;
4600
5038
  for (const n of cell.notes) {
@@ -4793,16 +5231,6 @@ function progressions() {
4793
5231
  function progressionsByStyle(style) {
4794
5232
  return progressions().filter((p) => p.styles.includes(style));
4795
5233
  }
4796
- function mulberry323(seed) {
4797
- let state = seed >>> 0;
4798
- return () => {
4799
- state = state + 1831565813 >>> 0;
4800
- let t = state;
4801
- t = Math.imul(t ^ t >>> 15, t | 1);
4802
- t ^= t + Math.imul(t ^ t >>> 7, t | 61);
4803
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
4804
- };
4805
- }
4806
5234
  function degreeToRootPc(degree, key) {
4807
5235
  if (degree >= 0 && degree <= 6) {
4808
5236
  const tones = scaleTonesInDegreeOrder(key);
@@ -4824,6 +5252,8 @@ function autoQuality(degree, key) {
4824
5252
  return "maj";
4825
5253
  }
4826
5254
  function generateProgression(opts) {
5255
+ assertPositiveInt(opts.bars, "progression bars");
5256
+ assertGenerationBudget(opts.bars, "progression chords");
4827
5257
  const seed = opts.seed ?? 0;
4828
5258
  let preset;
4829
5259
  if (opts.presetId !== void 0) {
@@ -4835,8 +5265,8 @@ function generateProgression(opts) {
4835
5265
  if (preset === void 0) {
4836
5266
  const pool = PRESETS.filter((p) => p.styles.includes(opts.style));
4837
5267
  const candidates = pool.length > 0 ? pool : PRESETS;
4838
- const rng = mulberry323(seed);
4839
- const index = Math.floor(rng() * candidates.length) % candidates.length;
5268
+ const rng = createRng(seed);
5269
+ const index = Math.floor(rng.next() * candidates.length) % candidates.length;
4840
5270
  preset = candidates[index] ?? PRESETS[0];
4841
5271
  }
4842
5272
  const degrees = preset?.degrees ?? [0];
@@ -4854,7 +5284,7 @@ function generateProgression(opts) {
4854
5284
  }
4855
5285
  if (opts.reharmonize) {
4856
5286
  const tonicPc = (opts.key.rootPc % 12 + 12) % 12;
4857
- const rrng = mulberry323((seed ^ 2654435769) >>> 0);
5287
+ const rrng = createRng((seed ^ 2654435769) >>> 0);
4858
5288
  for (let i = 0; i < chords.length - 1; i += 1) {
4859
5289
  const cur = chords[i];
4860
5290
  const next = chords[i + 1];
@@ -4868,7 +5298,7 @@ function generateProgression(opts) {
4868
5298
  if (targetsTonic || alreadySecondary || isResolutionTarget) {
4869
5299
  continue;
4870
5300
  }
4871
- if (rrng() < 0.5) {
5301
+ if (rrng.next() < 0.5) {
4872
5302
  chords[i] = {
4873
5303
  rootPc: domRoot,
4874
5304
  quality: "dom7",
@@ -4896,11 +5326,6 @@ function isStableTarget(chord) {
4896
5326
  const hasThird = hasInterval(chord, 3) || hasInterval(chord, 4);
4897
5327
  return hasFifth && hasThird && !isDominantType(chord);
4898
5328
  }
4899
- function triadPitchClasses(chord) {
4900
- const third = hasInterval(chord, 4) ? 4 : hasInterval(chord, 3) ? 3 : 4;
4901
- const fifth = hasInterval(chord, 8) ? 8 : hasInterval(chord, 6) ? 6 : 7;
4902
- return [mod123(chord.rootPc), mod123(chord.rootPc + third), mod123(chord.rootPc + fifth)];
4903
- }
4904
5329
  function commonToneCount(a, b) {
4905
5330
  const set = new Set(b);
4906
5331
  return a.filter((pc) => set.has(pc)).length;
@@ -4918,7 +5343,7 @@ function sameChord2(a, b) {
4918
5343
  }
4919
5344
  var THIRD_OFFSETS = [3, 4, 8, 9];
4920
5345
  function substituteChord(chord, key, opts) {
4921
- const original = triadPitchClasses(chord);
5346
+ const original = chordPitchClasses(chord);
4922
5347
  const candidates = [];
4923
5348
  if (isDominantType(chord)) {
4924
5349
  candidates.push({ chord: makeChord(mod123(chord.rootPc + 6), "dom7"), type: "tritone" });
@@ -5039,11 +5464,15 @@ function generateRhythm(ts, opts = {}) {
5039
5464
  const seed = opts.seed ?? 0;
5040
5465
  const bars = opts.bars ?? DEFAULT_BARS;
5041
5466
  const subdivision = opts.subdivision ?? DEFAULT_SUBDIVISION;
5042
- const density = Math.min(1, Math.max(0, opts.density ?? DEFAULT_DENSITY));
5467
+ const requestedDensity = assertFiniteNumber(opts.density ?? DEFAULT_DENSITY, "rhythm density");
5468
+ const density = Math.min(1, Math.max(0, requestedDensity));
5469
+ assertPositiveInt(bars, "rhythm bars");
5470
+ assertPositiveInt(subdivision, "rhythm subdivision");
5043
5471
  const barBeats = beatsPerBar(ts);
5044
5472
  const spanBeats = barBeats * bars;
5045
5473
  const step = 1 / subdivision;
5046
5474
  const slotCount = Math.round(spanBeats * subdivision);
5475
+ assertGenerationBudget(slotCount, "rhythm grid slots");
5047
5476
  const slotsPerBar = Math.round(barBeats * subdivision);
5048
5477
  const rng = createRng(seed);
5049
5478
  const positions = [];
@@ -5348,10 +5777,58 @@ function spellScale(tonic, key) {
5348
5777
  function spellPitchClasses(pcs, tonic, key) {
5349
5778
  return pcs.map((pc) => spellPitchClass(pc, tonic, key));
5350
5779
  }
5780
+ function chordLetterOffset(interval, chord) {
5781
+ switch (interval) {
5782
+ case 0:
5783
+ case 12:
5784
+ return 0;
5785
+ case 1:
5786
+ case 2:
5787
+ case 13:
5788
+ case 14:
5789
+ return 1;
5790
+ case 3:
5791
+ case 4:
5792
+ case 15:
5793
+ case 16:
5794
+ return 2;
5795
+ case 5:
5796
+ case 17:
5797
+ case 18:
5798
+ return 3;
5799
+ case 6:
5800
+ case 7:
5801
+ case 8:
5802
+ case 19:
5803
+ return 4;
5804
+ case 9:
5805
+ return chord.quality === "dim7" ? 6 : 5;
5806
+ case 20:
5807
+ case 21:
5808
+ return 5;
5809
+ case 10:
5810
+ case 11:
5811
+ case 22:
5812
+ case 23:
5813
+ return 6;
5814
+ default: {
5815
+ const octaves = Math.floor(Math.max(0, interval) / 12);
5816
+ const simple = mod124(interval);
5817
+ const simpleOffset = [0, 1, 1, 2, 2, 3, 4, 4, 4, 5, 6, 6][simple] ?? 0;
5818
+ return octaves * 7 + simpleOffset;
5819
+ }
5820
+ }
5821
+ }
5822
+ function spellChordFromRoot(chord, root) {
5823
+ return chord.intervals.map((interval) => {
5824
+ const letter = mod72(root.letter + chordLetterOffset(interval, chord));
5825
+ const pc = mod124(chord.rootPc + interval);
5826
+ return { letter, alter: alterFor(letter, pc) };
5827
+ });
5828
+ }
5351
5829
  function spellChord(chord, tonic, key) {
5352
- return chord.intervals.map(
5353
- (interval) => spellPitchClass(mod124(chord.rootPc + interval), tonic, key)
5354
- );
5830
+ const root = spellPitchClass(chord.rootPc, tonic, key);
5831
+ return spellChordFromRoot(chord, root);
5355
5832
  }
5356
5833
  function noteNames(notes) {
5357
5834
  return notes.map(formatNote);
@@ -5644,12 +6121,23 @@ function resolveRanges(opts) {
5644
6121
  if (opts.ranges.length === 0) {
5645
6122
  throw new Error("ranges must contain at least one voice range");
5646
6123
  }
5647
- return opts.ranges.map((range) => ({ ...range }));
6124
+ assertGenerationBudget(opts.ranges.length, "voice ranges", 128);
6125
+ return opts.ranges.map((range, index) => {
6126
+ assertFiniteNumber(range.min, `ranges[${index}].min`);
6127
+ assertFiniteNumber(range.max, `ranges[${index}].max`);
6128
+ if (range.min > range.max) {
6129
+ throw new RangeError(`ranges[${index}].min must not exceed max`);
6130
+ }
6131
+ assertGenerationBudget(
6132
+ Math.floor(range.max) - Math.ceil(range.min) + 1,
6133
+ `ranges[${index}] span`,
6134
+ 4096
6135
+ );
6136
+ return { ...range };
6137
+ });
5648
6138
  }
5649
6139
  const voices = opts?.voices ?? 4;
5650
- if (voices < 1) {
5651
- throw new Error("voices must be at least 1");
5652
- }
6140
+ assertPositiveInt(voices, "voices", 128);
5653
6141
  if (voices === 4) {
5654
6142
  return SATB_RANGES.map((range) => ({ ...range }));
5655
6143
  }
@@ -5668,6 +6156,8 @@ function resolveRanges(opts) {
5668
6156
  function voiceChord(chord, opts) {
5669
6157
  const ranges = resolveRanges(opts);
5670
6158
  const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;
6159
+ assertFiniteNumber(maxSpacing, "maxSpacing");
6160
+ if (maxSpacing < 0) throw new RangeError("maxSpacing must be non-negative");
5671
6161
  const candidates = enumerateVoicings(chord, ranges, maxSpacing);
5672
6162
  let best;
5673
6163
  let bestScore = Number.POSITIVE_INFINITY;
@@ -5692,8 +6182,11 @@ function voiceChord(chord, opts) {
5692
6182
  return best;
5693
6183
  }
5694
6184
  function voiceProgression(chords, opts) {
6185
+ assertGenerationBudget(chords.length * 4e3, "voiced progression search");
5695
6186
  const ranges = resolveRanges(opts);
5696
6187
  const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;
6188
+ assertFiniteNumber(maxSpacing, "maxSpacing");
6189
+ if (maxSpacing < 0) throw new RangeError("maxSpacing must be non-negative");
5697
6190
  const result = [];
5698
6191
  let prev;
5699
6192
  for (const chord of chords) {
@@ -5799,7 +6292,12 @@ function nearestPc(target, pcs) {
5799
6292
  }
5800
6293
  function voiceChordStyled(chord, opts) {
5801
6294
  const style = opts?.style ?? "close";
5802
- const base = 12 * ((opts?.octave ?? DEFAULT_STYLE_OCTAVE) + 1);
6295
+ const octave = opts?.octave ?? DEFAULT_STYLE_OCTAVE;
6296
+ assertInteger(octave, "styled voicing octave", -10, 10);
6297
+ if (opts?.topNote !== void 0) {
6298
+ assertFiniteNumber(opts.topNote, "styled voicing topNote");
6299
+ }
6300
+ const base = 12 * (octave + 1);
5803
6301
  const rootPc = pitchClass14(chord.rootPc);
5804
6302
  const bassPc = pitchClass14(chord.bassPc ?? chord.rootPc);
5805
6303
  const omitRoot = style === "rootless" || opts?.rootless === true;
@@ -5815,11 +6313,11 @@ function voiceChordStyled(chord, opts) {
5815
6313
  const guide = roles.includes("seventh") ? "seventh" : roles.includes("sixth") ? "sixth" : "fifth";
5816
6314
  order = order.filter((_pc, i) => {
5817
6315
  const role = roles[i];
5818
- return role === "root" || role === "third" || role === guide;
6316
+ return _pc === bassPc || role === "root" || role === "third" || role === guide;
5819
6317
  });
5820
6318
  }
5821
6319
  if (omitRoot) {
5822
- order = order.filter((pc) => pc !== rootPc);
6320
+ order = order.filter((pc) => pc !== rootPc || chord.bassPc !== void 0 && pc === bassPc);
5823
6321
  }
5824
6322
  if (order.length === 0) {
5825
6323
  return [];
@@ -5848,6 +6346,18 @@ function voiceChordStyled(chord, opts) {
5848
6346
  } else if (style === "drop3" && stack.length >= 3) {
5849
6347
  stack[stack.length - 3] = (stack[stack.length - 3] ?? 0) - 12;
5850
6348
  }
6349
+ if (chord.bassPc !== void 0) {
6350
+ const bassIndexInStack = stack.findIndex((pitch) => pitchClass14(pitch) === bassPc);
6351
+ if (bassIndexInStack >= 0) {
6352
+ const otherLowest = Math.min(
6353
+ ...stack.filter((_pitch, index) => index !== bassIndexInStack),
6354
+ Number.POSITIVE_INFINITY
6355
+ );
6356
+ while ((stack[bassIndexInStack] ?? 0) >= otherLowest) {
6357
+ stack[bassIndexInStack] = (stack[bassIndexInStack] ?? 0) - 12;
6358
+ }
6359
+ }
6360
+ }
5851
6361
  return stack.sort((a, b) => a - b);
5852
6362
  }
5853
6363
 
@@ -6143,7 +6653,11 @@ var Chord = class _Chord {
6143
6653
  * @param key Optional key context for analysis methods.
6144
6654
  */
6145
6655
  constructor(data, key) {
6146
- this.#data = copyChord(data);
6656
+ const copy = copyChord(data);
6657
+ if (copy.rootSpelling === void 0 && key !== void 0) {
6658
+ copy.rootSpelling = spellPitchClass(copy.rootPc, key.tonic.data, key.scale);
6659
+ }
6660
+ this.#data = copy;
6147
6661
  this.#key = key;
6148
6662
  }
6149
6663
  /**
@@ -6155,8 +6669,13 @@ var Chord = class _Chord {
6155
6669
  * @returns The chord (without key context).
6156
6670
  */
6157
6671
  static of(root, quality, bass) {
6158
- const rootPc = typeof root === "string" ? Note.of(root).pitchClass : root;
6159
- return new _Chord(makeChord(rootPc, quality, bass));
6672
+ if (typeof root === "string") {
6673
+ const rootNote = Note.of(root);
6674
+ const data = makeChord(rootNote.pitchClass, quality, bass);
6675
+ data.rootSpelling = { letter: rootNote.letter, alter: rootNote.alter };
6676
+ return new _Chord(data);
6677
+ }
6678
+ return new _Chord(makeChord(root, quality, bass));
6160
6679
  }
6161
6680
  /**
6162
6681
  * Wrap an existing plain chord object.
@@ -6183,8 +6702,8 @@ var Chord = class _Chord {
6183
6702
  * @param pitches MIDI pitches or bare pitch classes.
6184
6703
  * @returns Ranked chord interpretations (may be empty).
6185
6704
  */
6186
- static detect(pitches) {
6187
- return detectChord(pitches).map(
6705
+ static detect(pitches, opts) {
6706
+ return detectChord(pitches, opts).map(
6188
6707
  (match) => new _Chord(makeChord(match.rootPc, match.quality, match.bassPc))
6189
6708
  );
6190
6709
  }
@@ -6194,8 +6713,8 @@ var Chord = class _Chord {
6194
6713
  * @param pitches MIDI pitches or bare pitch classes.
6195
6714
  * @returns The top-ranked chord, or null when nothing matches.
6196
6715
  */
6197
- static detectBest(pitches) {
6198
- const best = detectChordBest(pitches);
6716
+ static detectBest(pitches, opts) {
6717
+ const best = detectChordBest(pitches, opts);
6199
6718
  return best === null ? null : new _Chord(best);
6200
6719
  }
6201
6720
  /** The root pitch class (0..11). */
@@ -6353,8 +6872,14 @@ var Chord = class _Chord {
6353
6872
  const data = copyChord(this.#data);
6354
6873
  if (index === 0) {
6355
6874
  delete data.bassPc;
6875
+ delete data.bassSpelling;
6356
6876
  } else {
6357
6877
  data.bassPc = mod125(this.#data.rootPc + (intervals[index] ?? 0));
6878
+ const rootSpelling = data.rootSpelling ?? (this.#key ? spellPitchClass(data.rootPc, this.#key.tonic.data, this.#key.scale) : spellPitchClassBare(data.rootPc, "sharp"));
6879
+ const bassSpelling = spellChordFromRoot(data, rootSpelling)[index];
6880
+ if (bassSpelling !== void 0) {
6881
+ data.bassSpelling = bassSpelling;
6882
+ }
6358
6883
  }
6359
6884
  return new _Chord(data, this.#key);
6360
6885
  }
@@ -6706,6 +7231,7 @@ var Key = class _Key {
6706
7231
  BLUES_MASK,
6707
7232
  CHROMATIC_MASK,
6708
7233
  Chord,
7234
+ DEFAULT_GENERATION_BUDGET,
6709
7235
  DORIAN_MASK,
6710
7236
  HARMONIC_MINOR_MASK,
6711
7237
  Interval,
@@ -6735,6 +7261,14 @@ var Key = class _Key {
6735
7261
  analyzeChord,
6736
7262
  analyzeVoice,
6737
7263
  applyGrooveTemplate,
7264
+ assertFiniteNumber,
7265
+ assertGenerationBudget,
7266
+ assertInteger,
7267
+ assertNoteEvent,
7268
+ assertNoteEvents,
7269
+ assertPositiveInt,
7270
+ assertRange,
7271
+ assertTimeSignature,
6738
7272
  availableTensions,
6739
7273
  avoidNotes,
6740
7274
  barPositionToBeat,
@@ -6753,6 +7287,7 @@ var Key = class _Key {
6753
7287
  chordToRoman,
6754
7288
  chordToneRole,
6755
7289
  classifyInterval,
7290
+ createNoteEventIndex,
6756
7291
  createRng,
6757
7292
  createsHiddenParallelPerfect,
6758
7293
  createsParallelOctave,