@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
@@ -1,18 +1,24 @@
1
1
  import {
2
2
  createRng
3
- } from "./chunk-BHUT2S5G.js";
3
+ } from "./chunk-62QZKCJQ.js";
4
4
  import {
5
5
  beatsPerBar,
6
+ createNoteEventIndex,
6
7
  formatTimeSignature,
7
8
  isStrongBeat,
8
9
  metricWeight
9
- } from "./chunk-MSWFZF7J.js";
10
+ } from "./chunk-I5R7ZBJS.js";
11
+ import {
12
+ isDiatonic,
13
+ parallelKey
14
+ } from "./chunk-CHMXELDS.js";
10
15
  import {
11
16
  roleOf
12
- } from "./chunk-SDM3VJJ5.js";
17
+ } from "./chunk-2ZWMSJH6.js";
13
18
  import {
14
- enumerateSafePitches
15
- } from "./chunk-A4532ZFR.js";
19
+ enumerateSafePitches,
20
+ evaluateSafety
21
+ } from "./chunk-EH6EM37U.js";
16
22
  import {
17
23
  NATURAL_MINOR_MASK,
18
24
  chordPitchClasses,
@@ -26,7 +32,16 @@ import {
26
32
  minorKey,
27
33
  nearestScaleTone,
28
34
  scaleTonesInDegreeOrder
29
- } from "./chunk-7Y7WF64D.js";
35
+ } from "./chunk-MIWO425M.js";
36
+ import {
37
+ assertFiniteNumber,
38
+ assertGenerationBudget,
39
+ assertInteger,
40
+ assertNoteEvents,
41
+ assertPositiveInt,
42
+ assertRange,
43
+ assertTimeSignature
44
+ } from "./chunk-FBQK3ZQN.js";
30
45
 
31
46
  // src/generate/bass/index.ts
32
47
  var DEFAULT_TS = { numerator: 4, denominator: 4 };
@@ -157,13 +172,30 @@ function buildWalking(ctx, seg, next) {
157
172
  });
158
173
  }
159
174
  function generateBassLine(opts) {
175
+ assertGenerationBudget(opts.segments.length, "bass segments");
176
+ for (let index = 0; index < opts.segments.length; index += 1) {
177
+ const segment = opts.segments[index];
178
+ if (!segment) continue;
179
+ assertRange(segment.startBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].startBeat`);
180
+ assertRange(segment.endBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].endBeat`);
181
+ if (segment.endBeat <= segment.startBeat) {
182
+ throw new RangeError(`segments[${index}] must have a positive duration`);
183
+ }
184
+ }
160
185
  const segments = [...opts.segments].sort((a, b) => a.startBeat - b.startBeat);
161
186
  if (segments.length === 0) {
162
187
  return [];
163
188
  }
164
189
  const ts = opts.ts ?? DEFAULT_TS;
190
+ assertTimeSignature(ts);
165
191
  const style = opts.style ?? DEFAULT_STYLE;
166
192
  const octave = opts.octave ?? DEFAULT_OCTAVE;
193
+ assertInteger(octave, "bass octave", -1, 9);
194
+ const estimatedNotes = segments.reduce(
195
+ (count, segment) => count + Math.max(1, Math.ceil(segment.endBeat - segment.startBeat)),
196
+ 0
197
+ );
198
+ assertGenerationBudget(estimatedNotes, "bass notes");
167
199
  const low = octave * 12 + 12;
168
200
  const ctx = {
169
201
  ts,
@@ -235,21 +267,26 @@ function isWholeBeat(beat) {
235
267
  return Math.abs(beat - Math.round(beat)) < EPS2;
236
268
  }
237
269
  function melodyNoteAt(melody, beat) {
238
- let found;
239
- for (const note of melody) {
240
- if (note.startBeat - EPS2 <= beat && beat < note.startBeat + note.durationBeat - EPS2) {
241
- if (!found || note.startBeat > found.startBeat) {
242
- found = note;
243
- }
244
- }
245
- }
246
- return found;
270
+ return melody.at(beat)?.note;
247
271
  }
248
272
  function melodyAttacksAt(melody, beat) {
249
- return melody.some((note) => Math.abs(note.startBeat - beat) < EPS2);
273
+ return melody.attacksAt(beat);
274
+ }
275
+ function melodySnapshotAt(melody, beat) {
276
+ const current = melodyNoteAt(melody, beat);
277
+ if (current === void 0) {
278
+ return void 0;
279
+ }
280
+ const attacked = Math.abs(current.startBeat - beat) < EPS2;
281
+ const previous = attacked ? melodyNoteAt(melody, beat - EPS2 * 2) : current;
282
+ const snapshot = { pitch: current.pitch };
283
+ if (previous !== void 0) {
284
+ snapshot.prevPitch = previous.pitch;
285
+ }
286
+ return snapshot;
250
287
  }
251
288
  function followOnsets(melody) {
252
- return [...new Set(melody.map((note) => note.startBeat))].sort((a, b) => a - b);
289
+ return [...new Set(melody.notes.map(({ note }) => note.startBeat))];
253
290
  }
254
291
  function complementOnsets(melody, ts, spanStart, spanEnd, rng) {
255
292
  const onsets = [];
@@ -312,54 +349,102 @@ function scoreCandidate(pitch, melPitch, melPrev, prevPitch, chord, center) {
312
349
  score -= Math.abs(pitch - center) * 0.02;
313
350
  return score;
314
351
  }
315
- function fallbackPitch(melPitch, prevPitch, center, register, low, high, key) {
316
- let target = prevPitch ?? Math.round(center);
317
- if (melPitch !== void 0) {
318
- if (register === "below" && target >= melPitch) {
319
- target = melPitch - 5;
320
- } else if (register === "above" && target <= melPitch) {
321
- target = melPitch + 5;
322
- }
323
- }
324
- target = Math.min(Math.max(target, low), high);
325
- let pitch = nearestScaleTone(target, key);
326
- if (melPitch !== void 0) {
327
- while (register === "below" && pitch >= melPitch && pitch - 12 >= low) {
328
- pitch -= 12;
329
- }
330
- while (register === "above" && pitch <= melPitch && pitch + 12 <= high) {
331
- pitch += 12;
352
+ function heldNoteBoundaries(melody, startBeat, endBeat, chordChangeBeats) {
353
+ const boundaries = /* @__PURE__ */ new Set([startBeat]);
354
+ for (const onset of melody.onsetsBetween(startBeat, endBeat)) {
355
+ boundaries.add(onset);
356
+ }
357
+ for (const beat of chordChangeBeats) {
358
+ if (beat > startBeat + EPS2 && beat < endBeat - EPS2) {
359
+ boundaries.add(beat);
360
+ }
361
+ }
362
+ const firstGrid = Math.floor(startBeat / GRID_STEP + 1) * GRID_STEP;
363
+ for (let beat = firstGrid; beat < endBeat - EPS2; beat += GRID_STEP) {
364
+ boundaries.add(beat);
365
+ }
366
+ return [...boundaries].sort((a, b) => a - b);
367
+ }
368
+ function heldPitchSafety(pitch, startBeat, endBeat, prevPitch, melody, opts, profile, register, low, high, ts) {
369
+ let worst = 0 /* Safe */;
370
+ const boundaries = heldNoteBoundaries(melody, startBeat, endBeat, opts.chordChangeBeats ?? []);
371
+ for (const boundary of boundaries) {
372
+ const melodyVoice = melodySnapshotAt(melody, boundary);
373
+ if (melodyVoice !== void 0 && (register === "below" && pitch >= melodyVoice.pitch || register === "above" && pitch <= melodyVoice.pitch)) {
374
+ return null;
375
+ }
376
+ const atCounterOnset = Math.abs(boundary - startBeat) < EPS2;
377
+ const result = evaluateSafety({
378
+ profile,
379
+ candidatePitch: pitch,
380
+ prevPitch: atCounterOnset ? prevPitch : pitch,
381
+ chord: opts.chordAt(boundary),
382
+ key: opts.key,
383
+ otherVoices: melodyVoice ? [melodyVoice] : [],
384
+ strongBeat: isStrongBeat(boundary, ts),
385
+ vocalLow: low,
386
+ vocalHigh: high
387
+ });
388
+ worst = Math.max(worst, result.safety);
389
+ if (worst === 2 /* Dissonant */) {
390
+ return worst;
332
391
  }
333
392
  }
334
- return Math.min(Math.max(pitch, low), high);
393
+ return worst;
335
394
  }
336
395
  function generateCounterMelody(opts) {
337
- const melody = [...opts.melody].sort((a, b) => a.startBeat - b.startBeat);
338
- if (melody.length === 0) {
396
+ const melody = createNoteEventIndex(opts.melody);
397
+ if (melody.notes.length === 0) {
339
398
  return [];
340
399
  }
341
400
  const ts = opts.ts ?? DEFAULT_TS2;
401
+ assertTimeSignature(ts);
342
402
  const register = opts.register ?? "below";
343
403
  const rhythm = opts.rhythm ?? "complement";
344
404
  const profile = opts.profile ?? "pop";
345
405
  const rng = createRng(opts.seed ?? 0);
346
- const spanStart = melody[0]?.startBeat ?? 0;
347
- const spanEnd = melody.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), spanStart);
348
- const meanPitch = melody.reduce((sum, n) => sum + n.pitch, 0) / melody.length;
406
+ const spanStart = melody.notes[0]?.note.startBeat ?? 0;
407
+ const spanEnd = melody.notes.reduce((end, n) => Math.max(end, n.endBeat), spanStart);
408
+ assertGenerationBudget(Math.ceil((spanEnd - spanStart) / GRID_STEP), "countermelody grid");
409
+ const meanPitch = melody.notes.reduce((sum, n) => sum + n.note.pitch, 0) / melody.notes.length;
349
410
  const defaultCenter = register === "below" ? meanPitch - REGISTER_OFFSET : meanPitch + REGISTER_OFFSET;
350
411
  const low = opts.pitchLow ?? Math.round(defaultCenter) - DEFAULT_HALF_RANGE;
351
412
  const high = opts.pitchHigh ?? Math.round(defaultCenter) + DEFAULT_HALF_RANGE;
413
+ assertInteger(low, "countermelody pitchLow");
414
+ assertInteger(high, "countermelody pitchHigh");
415
+ if (low > high) {
416
+ throw new RangeError(
417
+ `countermelody pitchLow must not exceed pitchHigh; received ${low} > ${high}`
418
+ );
419
+ }
420
+ assertGenerationBudget(high - low + 1, "countermelody pitch candidates");
421
+ assertGenerationBudget(opts.chordChangeBeats?.length ?? 0, "chord change beats");
422
+ for (let index = 0; index < (opts.chordChangeBeats?.length ?? 0); index += 1) {
423
+ assertRange(
424
+ opts.chordChangeBeats?.[index] ?? Number.NaN,
425
+ 0,
426
+ Number.MAX_SAFE_INTEGER,
427
+ `chordChangeBeats[${index}]`
428
+ );
429
+ }
352
430
  const center = (low + high) / 2;
353
431
  const onsets = rhythm === "follow" ? followOnsets(melody) : complementOnsets(melody, ts, spanStart, spanEnd, rng);
432
+ assertGenerationBudget(onsets.length * (high - low + 1), "countermelody search");
354
433
  const out = [];
355
434
  let prevPitch;
356
- let prevOnset;
357
- for (const beat of onsets) {
435
+ for (let onsetIndex = 0; onsetIndex < onsets.length; onsetIndex += 1) {
436
+ const beat = onsets[onsetIndex];
437
+ if (beat === void 0) {
438
+ continue;
439
+ }
440
+ const nextOnset = onsets[onsetIndex + 1];
441
+ const endBeat = nextOnset ?? Math.max(spanEnd, beat + GRID_STEP);
358
442
  const melNote = melodyNoteAt(melody, beat);
359
443
  const melPitch = melNote?.pitch;
360
- const melPrev = prevOnset !== void 0 ? melodyNoteAt(melody, prevOnset)?.pitch : void 0;
444
+ const melodyVoice = melodySnapshotAt(melody, beat);
445
+ const melPrev = melodyVoice?.prevPitch;
361
446
  const chord = opts.chordAt(beat);
362
- const otherVoices = melPitch !== void 0 ? [{ pitch: melPitch, prevPitch: melPrev }] : [];
447
+ const otherVoices = melodyVoice !== void 0 ? [melodyVoice] : [];
363
448
  const candidates = enumerateSafePitches(
364
449
  {
365
450
  profile,
@@ -375,15 +460,26 @@ function generateCounterMelody(opts) {
375
460
  high
376
461
  );
377
462
  let bestPitch;
463
+ let bestWorstSafety = 2 /* Dissonant */;
378
464
  let bestScore = Number.NEGATIVE_INFINITY;
379
465
  for (const candidate of candidates) {
466
+ const worstSafety = heldPitchSafety(
467
+ candidate,
468
+ beat,
469
+ endBeat,
470
+ prevPitch,
471
+ melody,
472
+ opts,
473
+ profile,
474
+ register,
475
+ low,
476
+ high,
477
+ ts
478
+ );
479
+ if (worstSafety === null || worstSafety === 2 /* Dissonant */) {
480
+ continue;
481
+ }
380
482
  if (melPitch !== void 0) {
381
- if (register === "below" && candidate >= melPitch) {
382
- continue;
383
- }
384
- if (register === "above" && candidate <= melPitch) {
385
- continue;
386
- }
387
483
  if (prevPitch !== void 0 && melPrev !== void 0 && // Parallel octaves are the perfect-class-zero case of this predicate,
388
484
  // so a single check covers both parallel fifths and octaves.
389
485
  createsParallelPerfect(prevPitch, candidate, melPrev, melPitch)) {
@@ -392,28 +488,23 @@ function generateCounterMelody(opts) {
392
488
  }
393
489
  const score = scoreCandidate(candidate, melPitch, melPrev, prevPitch, chord, center);
394
490
  const tie = Math.abs(score - bestScore) <= EPS2 && candidate < (bestPitch ?? Number.POSITIVE_INFINITY);
395
- if (score > bestScore + EPS2 || tie) {
491
+ if (worstSafety < bestWorstSafety || worstSafety === bestWorstSafety && (score > bestScore + EPS2 || tie)) {
492
+ bestWorstSafety = worstSafety;
396
493
  bestScore = score;
397
494
  bestPitch = candidate;
398
495
  }
399
496
  }
400
- const pitch = bestPitch ?? fallbackPitch(melPitch, prevPitch, center, register, low, high, opts.key);
497
+ if (bestPitch === void 0) {
498
+ prevPitch = void 0;
499
+ continue;
500
+ }
501
+ const pitch = bestPitch;
401
502
  const velocity = Math.max(
402
503
  1,
403
504
  Math.min(127, Math.round((melNote?.velocity ?? DEFAULT_MELODY_VELOCITY) - VELOCITY_DROP))
404
505
  );
405
- out.push({ pitch, startBeat: beat, durationBeat: GRID_STEP, velocity });
506
+ out.push({ pitch, startBeat: beat, durationBeat: endBeat - beat, velocity });
406
507
  prevPitch = pitch;
407
- prevOnset = beat;
408
- }
409
- for (let i = 0; i < out.length; i += 1) {
410
- const note = out[i];
411
- if (!note) {
412
- continue;
413
- }
414
- const next = out[i + 1];
415
- const end = next ? next.startBeat : Math.max(spanEnd, note.startBeat + GRID_STEP);
416
- note.durationBeat = end - note.startBeat;
417
508
  }
418
509
  return out;
419
510
  }
@@ -1133,9 +1224,10 @@ function generateHiHatForBeat(ctx, sec) {
1133
1224
 
1134
1225
  // src/generate/drums/euclid.ts
1135
1226
  function euclideanRhythm(pulses, steps, rotation = 0) {
1136
- if (steps < 1) {
1137
- return [];
1138
- }
1227
+ assertPositiveInt(steps, "euclidean steps");
1228
+ assertInteger(pulses, "euclidean pulses");
1229
+ assertInteger(rotation, "euclidean rotation");
1230
+ assertGenerationBudget(steps, "euclidean pattern steps");
1139
1231
  const p = Math.max(0, Math.min(pulses, steps));
1140
1232
  const base = bjorklund(p, steps);
1141
1233
  const shift = (rotation % steps + steps) % steps;
@@ -1144,18 +1236,6 @@ function euclideanRhythm(pulses, steps, rotation = 0) {
1144
1236
  }
1145
1237
  return base.map((_, i) => base[(i - shift + steps) % steps] ?? false);
1146
1238
  }
1147
- function hasHit(mask, step) {
1148
- return (mask >> step & 1) === 1;
1149
- }
1150
- function patternToMask(pattern) {
1151
- let mask = 0;
1152
- for (let i = 0; i < pattern.length; i += 1) {
1153
- if (pattern[i]) {
1154
- mask |= 1 << i;
1155
- }
1156
- }
1157
- return mask;
1158
- }
1159
1239
  function bjorklund(pulses, steps) {
1160
1240
  if (pulses === 0) {
1161
1241
  return Array.from({ length: steps }, () => false);
@@ -1487,19 +1567,6 @@ function isInPreChorusLift(section, bar, sectionBars, nextIsChorus) {
1487
1567
  }
1488
1568
  return bar >= sectionBars - 2;
1489
1569
  }
1490
- function euclideanToKickPattern(mask) {
1491
- const slot = (step) => hasHit(mask, step) || hasHit(mask, (step + 15) % 16);
1492
- return {
1493
- beat1: slot(0),
1494
- beat1and: slot(2),
1495
- beat2: slot(4),
1496
- beat2and: slot(6),
1497
- beat3: slot(8),
1498
- beat3and: slot(10),
1499
- beat4: slot(12),
1500
- beat4and: slot(14)
1501
- };
1502
- }
1503
1570
  function getKickPattern(section, style, bar, rng) {
1504
1571
  const p = { ...EMPTY };
1505
1572
  if (section === "outro") {
@@ -1621,7 +1688,7 @@ function getPercussionConfig(mood, section) {
1621
1688
  };
1622
1689
  }
1623
1690
  function generateAuxPercussionForBar(track, barStart, config, role, densityMult, rng, bpm) {
1624
- if (role === "minimal" || role === "fxOnly") {
1691
+ if (role === "minimal") {
1625
1692
  return;
1626
1693
  }
1627
1694
  if (config.tambourine) {
@@ -1670,6 +1737,16 @@ function generateAuxPercussionForBar(track, barStart, config, role, densityMult,
1670
1737
 
1671
1738
  // src/generate/drums/index.ts
1672
1739
  function generateDrums(opts) {
1740
+ assertPositiveInt(opts.bars, "drum bars");
1741
+ assertRange(opts.bpm, Number.MIN_VALUE, 1e3, "drum bpm");
1742
+ assertRange(opts.density, 0, 1, "drum density");
1743
+ assertGenerationBudget(opts.bars * 128, "drum hits");
1744
+ if (opts.euclideanKick) {
1745
+ const steps = opts.euclideanKick.steps ?? 16;
1746
+ assertInteger(steps, "euclidean steps", 1, 16);
1747
+ assertInteger(opts.euclideanKick.pulses, "euclidean pulses");
1748
+ assertInteger(opts.euclideanKick.rotation ?? 0, "euclidean rotation");
1749
+ }
1673
1750
  const track = new HitList();
1674
1751
  const rng = createRng(opts.seed ?? 0);
1675
1752
  const mapping = mapStyle(opts.style);
@@ -1706,15 +1783,8 @@ function generateDrums(opts) {
1706
1783
  const percMood = percMoodCategory(style);
1707
1784
  const nextSection = opts.nextSection ? mapSection(opts.nextSection) : section;
1708
1785
  const nextEnergy = sectionEnergy(nextSection);
1709
- const euclidKick = opts.euclideanKick ? euclideanToKickPattern(
1710
- patternToMask(
1711
- euclideanRhythm(
1712
- opts.euclideanKick.pulses,
1713
- opts.euclideanKick.steps ?? 16,
1714
- opts.euclideanKick.rotation ?? 0
1715
- )
1716
- )
1717
- ) : void 0;
1786
+ const euclidSteps = opts.euclideanKick?.steps ?? 16;
1787
+ const euclidKick = opts.euclideanKick ? euclideanRhythm(opts.euclideanKick.pulses, euclidSteps, opts.euclideanKick.rotation ?? 0) : void 0;
1718
1788
  const reuseSectionKick = (section === "b" || section === "chorus") && style !== "sparse";
1719
1789
  let sectionKick;
1720
1790
  for (let bar = 0; bar < opts.bars; bar += 1) {
@@ -1730,12 +1800,10 @@ function generateDrums(opts) {
1730
1800
  barHasOpenHh = !track.hasCrashNear(barStart + openHhBeatIndex);
1731
1801
  }
1732
1802
  let kick;
1733
- if (euclidKick) {
1734
- kick = euclidKick;
1735
- } else if (reuseSectionKick) {
1803
+ if (!euclidKick && reuseSectionKick) {
1736
1804
  sectionKick ??= getKickPattern(section, style, 0, rng);
1737
1805
  kick = sectionKick;
1738
- } else {
1806
+ } else if (!euclidKick) {
1739
1807
  kick = getKickPattern(section, style, bar, rng);
1740
1808
  }
1741
1809
  const inLift = isInPreChorusLift(section, bar, opts.bars, section === "b");
@@ -1776,7 +1844,23 @@ function generateDrums(opts) {
1776
1844
  if (inLift) {
1777
1845
  generatePreChorusBuildup(ctx, sec, isLastBar);
1778
1846
  }
1779
- generateKickForBeat(ctx, sec, kick);
1847
+ if (euclidKick && !inLift) {
1848
+ const stepLength = 4 / euclidSteps;
1849
+ for (let step = 0; step < euclidKick.length; step += 1) {
1850
+ const onset = step * stepLength;
1851
+ if (euclidKick[step] && Math.floor(onset) === beat) {
1852
+ const wholeBeat = Math.abs(onset - Math.round(onset)) < 1e-9;
1853
+ track.add(
1854
+ GM.BD,
1855
+ barStart + onset,
1856
+ Math.min(0.5, stepLength),
1857
+ velocity * (wholeBeat ? 1 : 0.85)
1858
+ );
1859
+ }
1860
+ }
1861
+ } else if (kick) {
1862
+ generateKickForBeat(ctx, sec, kick);
1863
+ }
1780
1864
  generateSnareForBeat(ctx, sec, section === "intro" && bar === 0);
1781
1865
  if (sec.useGhostNotes && !inLift) {
1782
1866
  generateGhostNotesForBeat(ctx, sec);
@@ -1793,10 +1877,19 @@ function generateDrums(opts) {
1793
1877
  }
1794
1878
  }
1795
1879
  }
1880
+ let percussionConfig = getPercussionConfig(percMood, section);
1881
+ if (role === "fxOnly" && !percussionConfig.tambourine && !percussionConfig.shaker && !percussionConfig.handclap) {
1882
+ percussionConfig = {
1883
+ tambourine: false,
1884
+ shaker: true,
1885
+ handclap: false,
1886
+ shaker16th: false
1887
+ };
1888
+ }
1796
1889
  generateAuxPercussionForBar(
1797
1890
  track,
1798
1891
  barStart,
1799
- getPercussionConfig(percMood, section),
1892
+ percussionConfig,
1800
1893
  role,
1801
1894
  densityMult,
1802
1895
  rng,
@@ -1817,10 +1910,16 @@ var MIN_VELOCITY = 1;
1817
1910
  var MAX_VELOCITY = 127;
1818
1911
  function humanize(events, opts = {}) {
1819
1912
  const ts = opts.ts ?? DEFAULT_TS3;
1913
+ assertTimeSignature(ts);
1914
+ assertNoteEvents(events, "humanize events");
1820
1915
  const timing = opts.timing ?? DEFAULT_TIMING;
1821
1916
  const velocityJitter = opts.velocity ?? DEFAULT_VELOCITY_JITTER;
1822
1917
  const accent = opts.accent ?? DEFAULT_ACCENT;
1823
1918
  const baseVelocity = opts.baseVelocity ?? DEFAULT_BASE_VELOCITY;
1919
+ assertRange(timing, 0, Number.MAX_SAFE_INTEGER, "humanize timing");
1920
+ assertRange(velocityJitter, 0, 127, "humanize velocity jitter");
1921
+ assertRange(accent, 0, 127, "humanize accent");
1922
+ assertRange(baseVelocity, 0, 127, "humanize base velocity");
1824
1923
  const rng = createRng(opts.seed ?? 0);
1825
1924
  return events.map((event) => {
1826
1925
  const timingOffset = rng.float(-timing, timing);
@@ -1853,8 +1952,12 @@ function quantizeToGrid(beat, barBeats, subdivision, slotsPerBar) {
1853
1952
  };
1854
1953
  }
1855
1954
  function extractGrooveTemplate(events, ts, subdivision) {
1955
+ assertTimeSignature(ts);
1956
+ assertNoteEvents(events, "groove source events");
1957
+ assertPositiveInt(subdivision, "groove subdivision");
1856
1958
  const barBeats = beatsPerBar(ts);
1857
1959
  const slotsPerBar = Math.round(barBeats * subdivision);
1960
+ assertGenerationBudget(slotsPerBar, "groove template slots");
1858
1961
  const offsetSums = new Array(slotsPerBar).fill(0);
1859
1962
  const velocitySums = new Array(slotsPerBar).fill(0);
1860
1963
  const counts = new Array(slotsPerBar).fill(0);
@@ -1889,6 +1992,14 @@ function extractGrooveTemplate(events, ts, subdivision) {
1889
1992
  return { subdivision, slotsPerBar, slots, ts };
1890
1993
  }
1891
1994
  function applyGrooveTemplate(events, template, ts) {
1995
+ assertTimeSignature(ts);
1996
+ assertNoteEvents(events, "groove target events");
1997
+ assertPositiveInt(template.subdivision, "template subdivision");
1998
+ assertPositiveInt(template.slotsPerBar, "template slotsPerBar");
1999
+ assertGenerationBudget(template.slots.length, "template slots");
2000
+ if (template.slots.length !== template.slotsPerBar) {
2001
+ throw new RangeError("template slots length must equal slotsPerBar");
2002
+ }
1892
2003
  if (template.ts && (template.ts.numerator !== ts.numerator || template.ts.denominator !== ts.denominator)) {
1893
2004
  throw new Error(
1894
2005
  `Groove template meter ${formatTimeSignature(template.ts)} does not match apply-time meter ${formatTimeSignature(ts)}`
@@ -1924,16 +2035,6 @@ var TIE_BREAK_JITTER = 1e-6;
1924
2035
  function pitchClass2(pitch) {
1925
2036
  return (Math.trunc(pitch) % 12 + 12) % 12;
1926
2037
  }
1927
- function mulberry32(seed) {
1928
- let state = seed >>> 0;
1929
- return () => {
1930
- state = state + 1831565813 >>> 0;
1931
- let t = state;
1932
- t = Math.imul(t ^ t >>> 15, t | 1);
1933
- t ^= t + Math.imul(t ^ t >>> 7, t | 61);
1934
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
1935
- };
1936
- }
1937
2038
  function inferKey(melody) {
1938
2039
  let best = majorKey(0);
1939
2040
  let bestScore = Number.NEGATIVE_INFINITY;
@@ -1972,7 +2073,6 @@ function buildCandidates(key, reharmonize) {
1972
2073
  secondaryDominant: false,
1973
2074
  base: 0
1974
2075
  }));
1975
- const tonic = tones[0] ?? 0;
1976
2076
  if (reharmonize !== "diatonic") {
1977
2077
  for (const target of [1, 3, 4, 5]) {
1978
2078
  const targetRoot = tones[target] ?? 0;
@@ -1986,26 +2086,29 @@ function buildCandidates(key, reharmonize) {
1986
2086
  }
1987
2087
  }
1988
2088
  if (reharmonize === "borrowed") {
1989
- candidates.push({
1990
- rootPc: (tonic + 10) % 12,
1991
- quality: "maj",
1992
- secondaryDominant: false,
1993
- base: 2
1994
- });
1995
- candidates.push({
1996
- rootPc: (tonic + 8) % 12,
1997
- quality: "maj",
1998
- secondaryDominant: false,
1999
- base: 2
2000
- });
2001
- candidates.push({
2002
- rootPc: (tonic + 5) % 12,
2003
- quality: "min",
2004
- secondaryDominant: false,
2005
- base: 2
2006
- });
2089
+ const parallel = parallelKey(key);
2090
+ const parallelTones = scaleTonesInDegreeOrder(parallel);
2091
+ for (let degree = 0; degree < parallelTones.length; degree += 1) {
2092
+ const chord = diatonicTriad(degree, parallel);
2093
+ if (!isDiatonic(chord, key)) {
2094
+ candidates.push({
2095
+ rootPc: chord.rootPc,
2096
+ quality: chord.quality,
2097
+ secondaryDominant: false,
2098
+ base: 1.25
2099
+ });
2100
+ }
2101
+ }
2007
2102
  }
2008
- return candidates;
2103
+ const seen = /* @__PURE__ */ new Set();
2104
+ return candidates.filter((candidate) => {
2105
+ const id = `${candidate.rootPc}:${candidate.quality}`;
2106
+ if (seen.has(id)) {
2107
+ return false;
2108
+ }
2109
+ seen.add(id);
2110
+ return true;
2111
+ });
2009
2112
  }
2010
2113
  function emissionCost(seg, cand, melody, key) {
2011
2114
  const pcs = chordPitchClasses(makeChord(cand.rootPc, cand.quality));
@@ -2109,20 +2212,37 @@ function harmonizeOnce(melody, key, candidates, segments, jitter) {
2109
2212
  return { cost: bestCost, path };
2110
2213
  }
2111
2214
  function harmonizeMelody(opts) {
2215
+ const noteIndex = createNoteEventIndex(opts.melody);
2112
2216
  const key = opts.key === "infer" ? inferKey(opts.melody) : opts.key;
2113
2217
  const candidates = buildCandidates(key, opts.reharmonize);
2114
2218
  const candAt = (i) => candidates[i] ?? FALLBACK;
2115
- const rng = mulberry32(opts.seed ?? 0);
2116
- const jitter = candidates.map(() => rng() * TIE_BREAK_JITTER);
2117
- const melodyEnd = opts.melody.reduce((m, n) => Math.max(m, n.startBeat + n.durationBeat), 0);
2118
- const hr = Math.max(0.25, opts.harmonicRhythm);
2219
+ const rng = createRng(opts.seed ?? 0);
2220
+ const jitter = candidates.map(() => rng.next() * TIE_BREAK_JITTER);
2221
+ const melodyEnd = noteIndex.notes.reduce((m, n) => Math.max(m, n.endBeat), 0);
2222
+ const requestedHr = assertRange(
2223
+ opts.harmonicRhythm,
2224
+ Number.MIN_VALUE,
2225
+ Number.MAX_SAFE_INTEGER,
2226
+ "harmonic rhythm"
2227
+ );
2228
+ const hr = Math.max(0.25, requestedHr);
2119
2229
  const segCount = Math.max(1, Math.ceil(melodyEnd / hr));
2120
- const segments = [];
2121
- for (let s = 0; s < segCount; s += 1) {
2122
- const startBeat = s * hr;
2123
- const endBeat = startBeat + hr;
2124
- const noteIndices = opts.melody.map((n, i) => ({ n, i })).filter(({ n }) => n.startBeat < endBeat && n.startBeat + n.durationBeat > startBeat).map(({ i }) => i);
2125
- segments.push({ startBeat, endBeat, noteIndices });
2230
+ assertGenerationBudget(segCount, "harmonic segments");
2231
+ assertGenerationBudget(segCount * candidates.length * candidates.length, "harmonization search");
2232
+ const segments = Array.from({ length: segCount }, (_, s) => ({
2233
+ startBeat: s * hr,
2234
+ endBeat: (s + 1) * hr,
2235
+ noteIndices: []
2236
+ }));
2237
+ let memberships = 0;
2238
+ for (const indexed of noteIndex.notes) {
2239
+ const first = Math.max(0, Math.floor(indexed.note.startBeat / hr));
2240
+ const lastExclusive = Math.min(segCount, Math.ceil(indexed.endBeat / hr - Number.EPSILON));
2241
+ memberships += Math.max(0, lastExclusive - first);
2242
+ assertGenerationBudget(memberships, "note-to-segment memberships");
2243
+ for (let segment = first; segment < lastExclusive; segment += 1) {
2244
+ segments[segment]?.noteIndices.push(indexed.originalIndex);
2245
+ }
2126
2246
  }
2127
2247
  const transposes = [0];
2128
2248
  if (opts.placement.transposeSearch) {
@@ -2169,11 +2289,11 @@ function harmonizeMelody(opts) {
2169
2289
  }
2170
2290
  return chord;
2171
2291
  });
2172
- const melodyRoles = bestMelody.map((note, noteIndex) => {
2292
+ const melodyRoles = bestMelody.map((note, noteIndex2) => {
2173
2293
  const segIdx = Math.min(segments.length - 1, Math.max(0, Math.floor(note.startBeat / hr)));
2174
2294
  const cand = candAt(bestPath[segIdx] ?? 0);
2175
2295
  const chord = makeChord(cand.rootPc, cand.quality);
2176
- return { noteIndex, role: roleOf(note.pitch, chord).role };
2296
+ return { noteIndex: noteIndex2, role: roleOf(note.pitch, chord).role };
2177
2297
  });
2178
2298
  return { transposeSemitones: bestTs, key, chords, melodyRoles };
2179
2299
  }
@@ -2220,29 +2340,30 @@ function nearestChordTone(pitch, chord) {
2220
2340
  return pitch;
2221
2341
  }
2222
2342
  function cellOrigin(notes) {
2223
- return notes.length === 0 ? 0 : Math.min(...notes.map((n) => n.startBeat));
2343
+ let origin = Number.POSITIVE_INFINITY;
2344
+ for (const note of notes) {
2345
+ origin = Math.min(origin, note.startBeat);
2346
+ }
2347
+ return Number.isFinite(origin) ? origin : 0;
2348
+ }
2349
+ function cellEnd(notes) {
2350
+ let end = Number.NEGATIVE_INFINITY;
2351
+ for (const note of notes) {
2352
+ end = Math.max(end, note.startBeat + note.durationBeat);
2353
+ }
2354
+ return Number.isFinite(end) ? end : 0;
2224
2355
  }
2225
2356
  function cellSpan(cell) {
2226
2357
  if (cell.notes.length === 0) {
2227
2358
  return 0;
2228
2359
  }
2229
2360
  const start = cellOrigin(cell.notes);
2230
- const end = Math.max(...cell.notes.map((n) => n.startBeat + n.durationBeat));
2361
+ const end = cellEnd(cell.notes);
2231
2362
  return end - start;
2232
2363
  }
2233
2364
  function clone(cell) {
2234
2365
  return { notes: cell.notes.map((n) => ({ ...n })) };
2235
2366
  }
2236
- function mulberry322(seed) {
2237
- let state = seed >>> 0;
2238
- return () => {
2239
- state = state + 1831565813 >>> 0;
2240
- let t = state;
2241
- t = Math.imul(t ^ t >>> 15, t | 1);
2242
- t ^= t + Math.imul(t ^ t >>> 7, t | 61);
2243
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
2244
- };
2245
- }
2246
2367
  function contourOffsets(contour, count) {
2247
2368
  const offsets = [];
2248
2369
  for (let i = 0; i < count; i += 1) {
@@ -2265,19 +2386,21 @@ function contourOffsets(contour, count) {
2265
2386
  }
2266
2387
  function generateMotif(opts) {
2267
2388
  const contour = opts.contour ?? "arch";
2268
- const bars = Math.max(1, Math.trunc(opts.bars));
2389
+ const bars = assertPositiveInt(opts.bars, "motif bars");
2269
2390
  const totalBeats = bars * 4;
2270
2391
  const noteCount = Math.max(3, bars * 2);
2392
+ assertGenerationBudget(noteCount, "motif notes");
2271
2393
  const beatsPerNote = totalBeats / noteCount;
2272
- const rng = mulberry322(opts.seed ?? 0);
2273
- const jitterProb = Math.min(1, Math.max(0, opts.jitter ?? 0));
2394
+ const rng = createRng(opts.seed ?? 0);
2395
+ const requestedJitter = assertFiniteNumber(opts.jitter ?? 0, "motif jitter");
2396
+ const jitterProb = Math.min(1, Math.max(0, requestedJitter));
2274
2397
  const tonic = pitchClass3(opts.key.rootPc) + 60;
2275
2398
  const offsets = contourOffsets(contour, noteCount);
2276
2399
  const notes = [];
2277
2400
  for (let i = 0; i < noteCount; i += 1) {
2278
2401
  let jitter = 0;
2279
- if (jitterProb > 0 && rng() < jitterProb) {
2280
- jitter = rng() < 0.5 ? 1 : -1;
2402
+ if (jitterProb > 0 && rng.next() < jitterProb) {
2403
+ jitter = rng.next() < 0.5 ? 1 : -1;
2281
2404
  }
2282
2405
  let pitch = stepDiatonic(tonic, (offsets[i] ?? 0) + jitter, opts.key);
2283
2406
  const startBeat = i * beatsPerNote;
@@ -2289,6 +2412,16 @@ function generateMotif(opts) {
2289
2412
  return { notes };
2290
2413
  }
2291
2414
  function transformMotif(cell, t, amount, key) {
2415
+ assertNoteEvents(cell.notes, "motif notes");
2416
+ if (amount !== void 0) {
2417
+ assertFiniteNumber(amount, "motif transform amount");
2418
+ }
2419
+ if ((t === "transposeDiatonic" || t === "sequence") && amount !== void 0) {
2420
+ assertInteger(amount, "diatonic transform amount");
2421
+ }
2422
+ if ((t === "augment" || t === "diminish") && amount !== void 0 && amount <= 0) {
2423
+ throw new RangeError("time transform amount must be positive");
2424
+ }
2292
2425
  const notes = cell.notes;
2293
2426
  switch (t) {
2294
2427
  case "transposeChromatic": {
@@ -2315,7 +2448,7 @@ function transformMotif(cell, t, amount, key) {
2315
2448
  return { notes: [] };
2316
2449
  }
2317
2450
  const start = cellOrigin(notes);
2318
- const end = Math.max(...notes.map((n) => n.startBeat + n.durationBeat));
2451
+ const end = cellEnd(notes);
2319
2452
  return {
2320
2453
  notes: notes.map((n) => ({
2321
2454
  pitch: n.pitch,
@@ -2348,8 +2481,10 @@ function scaleTime(cell, factor) {
2348
2481
  };
2349
2482
  }
2350
2483
  function developMotif(cell, timeline, key, bars) {
2484
+ assertPositiveInt(bars, "development bars");
2485
+ assertNoteEvents(cell.notes, "motif notes");
2351
2486
  const span = cellSpan(cell);
2352
- const totalBeats = Math.max(1, Math.trunc(bars)) * 4;
2487
+ const totalBeats = bars * 4;
2353
2488
  const origin = cellOrigin(cell.notes);
2354
2489
  const out = [];
2355
2490
  if (span <= 0) {
@@ -2357,6 +2492,7 @@ function developMotif(cell, timeline, key, bars) {
2357
2492
  }
2358
2493
  const tileSpan = Math.max(span, MIN_TILE_SPAN);
2359
2494
  const tileCount = Math.ceil(totalBeats / tileSpan);
2495
+ assertGenerationBudget(tileCount * cell.notes.length, "developed motif notes");
2360
2496
  for (let k = 0; k < tileCount; k += 1) {
2361
2497
  const offset = k * tileSpan;
2362
2498
  for (const n of cell.notes) {
@@ -2555,16 +2691,6 @@ function progressions() {
2555
2691
  function progressionsByStyle(style) {
2556
2692
  return progressions().filter((p) => p.styles.includes(style));
2557
2693
  }
2558
- function mulberry323(seed) {
2559
- let state = seed >>> 0;
2560
- return () => {
2561
- state = state + 1831565813 >>> 0;
2562
- let t = state;
2563
- t = Math.imul(t ^ t >>> 15, t | 1);
2564
- t ^= t + Math.imul(t ^ t >>> 7, t | 61);
2565
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
2566
- };
2567
- }
2568
2694
  function degreeToRootPc(degree, key) {
2569
2695
  if (degree >= 0 && degree <= 6) {
2570
2696
  const tones = scaleTonesInDegreeOrder(key);
@@ -2586,6 +2712,8 @@ function autoQuality(degree, key) {
2586
2712
  return "maj";
2587
2713
  }
2588
2714
  function generateProgression(opts) {
2715
+ assertPositiveInt(opts.bars, "progression bars");
2716
+ assertGenerationBudget(opts.bars, "progression chords");
2589
2717
  const seed = opts.seed ?? 0;
2590
2718
  let preset;
2591
2719
  if (opts.presetId !== void 0) {
@@ -2597,8 +2725,8 @@ function generateProgression(opts) {
2597
2725
  if (preset === void 0) {
2598
2726
  const pool = PRESETS.filter((p) => p.styles.includes(opts.style));
2599
2727
  const candidates = pool.length > 0 ? pool : PRESETS;
2600
- const rng = mulberry323(seed);
2601
- const index = Math.floor(rng() * candidates.length) % candidates.length;
2728
+ const rng = createRng(seed);
2729
+ const index = Math.floor(rng.next() * candidates.length) % candidates.length;
2602
2730
  preset = candidates[index] ?? PRESETS[0];
2603
2731
  }
2604
2732
  const degrees = preset?.degrees ?? [0];
@@ -2616,7 +2744,7 @@ function generateProgression(opts) {
2616
2744
  }
2617
2745
  if (opts.reharmonize) {
2618
2746
  const tonicPc = (opts.key.rootPc % 12 + 12) % 12;
2619
- const rrng = mulberry323((seed ^ 2654435769) >>> 0);
2747
+ const rrng = createRng((seed ^ 2654435769) >>> 0);
2620
2748
  for (let i = 0; i < chords.length - 1; i += 1) {
2621
2749
  const cur = chords[i];
2622
2750
  const next = chords[i + 1];
@@ -2630,7 +2758,7 @@ function generateProgression(opts) {
2630
2758
  if (targetsTonic || alreadySecondary || isResolutionTarget) {
2631
2759
  continue;
2632
2760
  }
2633
- if (rrng() < 0.5) {
2761
+ if (rrng.next() < 0.5) {
2634
2762
  chords[i] = {
2635
2763
  rootPc: domRoot,
2636
2764
  quality: "dom7",
@@ -2663,11 +2791,15 @@ function generateRhythm(ts, opts = {}) {
2663
2791
  const seed = opts.seed ?? 0;
2664
2792
  const bars = opts.bars ?? DEFAULT_BARS;
2665
2793
  const subdivision = opts.subdivision ?? DEFAULT_SUBDIVISION;
2666
- const density = Math.min(1, Math.max(0, opts.density ?? DEFAULT_DENSITY));
2794
+ const requestedDensity = assertFiniteNumber(opts.density ?? DEFAULT_DENSITY, "rhythm density");
2795
+ const density = Math.min(1, Math.max(0, requestedDensity));
2796
+ assertPositiveInt(bars, "rhythm bars");
2797
+ assertPositiveInt(subdivision, "rhythm subdivision");
2667
2798
  const barBeats = beatsPerBar(ts);
2668
2799
  const spanBeats = barBeats * bars;
2669
2800
  const step = 1 / subdivision;
2670
2801
  const slotCount = Math.round(spanBeats * subdivision);
2802
+ assertGenerationBudget(slotCount, "rhythm grid slots");
2671
2803
  const slotsPerBar = Math.round(barBeats * subdivision);
2672
2804
  const rng = createRng(seed);
2673
2805
  const positions = [];
@@ -2719,4 +2851,4 @@ export {
2719
2851
  generateRhythm,
2720
2852
  rhythmDensity
2721
2853
  };
2722
- //# sourceMappingURL=chunk-R3S2LUBH.js.map
2854
+ //# sourceMappingURL=chunk-4MHA432G.js.map