@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
@@ -42,6 +42,86 @@ __export(generate_exports, {
42
42
  });
43
43
  module.exports = __toCommonJS(generate_exports);
44
44
 
45
+ // src/core/validation/index.ts
46
+ var DEFAULT_GENERATION_BUDGET = 1e6;
47
+ function assertFiniteNumber(value, name) {
48
+ if (!Number.isFinite(value)) {
49
+ throw new RangeError(`${name} must be finite; received ${value}`);
50
+ }
51
+ return value;
52
+ }
53
+ function assertInteger(value, name, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) {
54
+ assertFiniteNumber(value, name);
55
+ if (!Number.isSafeInteger(value) || value < min || value > max) {
56
+ throw new RangeError(`${name} must be an integer in [${min}, ${max}]; received ${value}`);
57
+ }
58
+ return value;
59
+ }
60
+ function assertPositiveInt(value, name, max = DEFAULT_GENERATION_BUDGET) {
61
+ return assertInteger(value, name, 1, max);
62
+ }
63
+ function assertRange(value, min, max, name) {
64
+ assertFiniteNumber(value, name);
65
+ if (value < min || value > max) {
66
+ throw new RangeError(`${name} must be in [${min}, ${max}]; received ${value}`);
67
+ }
68
+ return value;
69
+ }
70
+ function assertGenerationBudget(estimated, name, limit = DEFAULT_GENERATION_BUDGET) {
71
+ assertFiniteNumber(estimated, name);
72
+ assertPositiveInt(limit, `${name} limit`, Number.MAX_SAFE_INTEGER);
73
+ if (estimated < 0 || estimated > limit) {
74
+ throw new RangeError(`${name} exceeds the generation budget ${limit}; received ${estimated}`);
75
+ }
76
+ return estimated;
77
+ }
78
+ function assertTimeSignature(ts, name = "time signature") {
79
+ assertPositiveInt(ts.numerator, `${name}.numerator`);
80
+ assertPositiveInt(ts.denominator, `${name}.denominator`);
81
+ if (ts.grouping !== void 0) {
82
+ if (ts.grouping.length === 0) {
83
+ throw new RangeError(`${name}.grouping must not be empty`);
84
+ }
85
+ let sum = 0;
86
+ for (let index = 0; index < ts.grouping.length; index += 1) {
87
+ sum += assertPositiveInt(ts.grouping[index] ?? Number.NaN, `${name}.grouping[${index}]`);
88
+ }
89
+ const compound = ts.numerator % 3 === 0 && ts.numerator > 3;
90
+ const pulses = compound ? ts.numerator / 3 : ts.numerator;
91
+ if (sum !== pulses) {
92
+ throw new RangeError(`${name}.grouping must sum to ${pulses}; received ${sum}`);
93
+ }
94
+ }
95
+ return ts;
96
+ }
97
+ function assertNoteEvent(event, name = "note event", options = {}) {
98
+ assertFiniteNumber(event.pitch, `${name}.pitch`);
99
+ assertRange(event.startBeat, 0, Number.MAX_SAFE_INTEGER, `${name}.startBeat`);
100
+ assertRange(
101
+ event.durationBeat,
102
+ options.allowNonPositiveDuration ? -Number.MAX_SAFE_INTEGER : Number.MIN_VALUE,
103
+ Number.MAX_SAFE_INTEGER,
104
+ `${name}.durationBeat`
105
+ );
106
+ if (!options.allowNonPositiveDuration && event.durationBeat <= 0) {
107
+ throw new RangeError(`${name}.durationBeat must be positive; received ${event.durationBeat}`);
108
+ }
109
+ if (event.velocity !== void 0) {
110
+ assertRange(event.velocity, 0, 127, `${name}.velocity`);
111
+ }
112
+ return event;
113
+ }
114
+ function assertNoteEvents(events, name = "note events", options = {}) {
115
+ assertGenerationBudget(events.length, `${name} count`, options.budget);
116
+ for (let index = 0; index < events.length; index += 1) {
117
+ const event = events[index];
118
+ if (event !== void 0) {
119
+ assertNoteEvent(event, `${name}[${index}]`, options);
120
+ }
121
+ }
122
+ return events;
123
+ }
124
+
45
125
  // src/core/meter/index.ts
46
126
  var EPS = 1e-9;
47
127
  function isMultiple(value, unit) {
@@ -52,15 +132,18 @@ function isMultiple(value, unit) {
52
132
  return Math.abs(ratio - Math.round(ratio)) < EPS;
53
133
  }
54
134
  function formatTimeSignature(ts) {
135
+ assertTimeSignature(ts);
55
136
  return `${ts.numerator}/${ts.denominator}`;
56
137
  }
57
138
  function isCompound(ts) {
139
+ assertTimeSignature(ts);
58
140
  return ts.numerator % 3 === 0 && ts.numerator > 3;
59
141
  }
60
142
  function unitBeats(ts) {
61
143
  return 4 / ts.denominator;
62
144
  }
63
145
  function beatsPerBar(ts) {
146
+ assertTimeSignature(ts);
64
147
  return ts.numerator * unitBeats(ts);
65
148
  }
66
149
  function pulseBeats(ts) {
@@ -93,11 +176,14 @@ function isGroupHead(grouping, pulseIndex) {
93
176
  return false;
94
177
  }
95
178
  function beatToBarPosition(beatInQuarters, ts) {
179
+ assertFiniteNumber(beatInQuarters, "beat");
96
180
  const barLen = beatsPerBar(ts);
97
181
  const bar = Math.floor(beatInQuarters / barLen);
98
182
  return { bar, beat: beatInQuarters - bar * barLen };
99
183
  }
100
184
  function metricWeight(beatInQuarters, ts) {
185
+ assertFiniteNumber(beatInQuarters, "beat");
186
+ assertTimeSignature(ts);
101
187
  const { beat } = beatToBarPosition(beatInQuarters, ts);
102
188
  const pulse = pulseBeats(ts);
103
189
  if (!isMultiple(beat, pulse)) {
@@ -128,6 +214,7 @@ function isStrongBeat(beatInQuarters, ts) {
128
214
 
129
215
  // src/core/random/index.ts
130
216
  function createRng(seed) {
217
+ assertFiniteNumber(seed, "seed");
131
218
  let state = seed >>> 0;
132
219
  const next = () => {
133
220
  state = state + 1831565813 >>> 0;
@@ -138,9 +225,29 @@ function createRng(seed) {
138
225
  };
139
226
  return {
140
227
  next,
141
- prob: (p) => next() < p,
142
- range: (lo, hi) => lo + Math.floor(next() * (hi - lo + 1)),
143
- float: (lo, hi) => lo + next() * (hi - lo)
228
+ prob: (p) => next() < assertRange(p, 0, 1, "probability"),
229
+ range: (lo, hi) => {
230
+ assertInteger(lo, "range lower bound");
231
+ assertInteger(hi, "range upper bound");
232
+ if (lo > hi) {
233
+ throw new RangeError(
234
+ `range lower bound must not exceed upper bound; received ${lo} > ${hi}`
235
+ );
236
+ }
237
+ assertInteger(hi - lo, "range span", 0);
238
+ return lo + Math.floor(next() * (hi - lo + 1));
239
+ },
240
+ float: (lo, hi) => {
241
+ assertFiniteNumber(lo, "float lower bound");
242
+ assertFiniteNumber(hi, "float upper bound");
243
+ if (lo > hi) {
244
+ throw new RangeError(
245
+ `float lower bound must not exceed upper bound; received ${lo} > ${hi}`
246
+ );
247
+ }
248
+ assertFiniteNumber(hi - lo, "float span");
249
+ return lo + next() * (hi - lo);
250
+ }
144
251
  };
145
252
  }
146
253
 
@@ -340,13 +447,30 @@ function buildWalking(ctx, seg, next) {
340
447
  });
341
448
  }
342
449
  function generateBassLine(opts) {
450
+ assertGenerationBudget(opts.segments.length, "bass segments");
451
+ for (let index = 0; index < opts.segments.length; index += 1) {
452
+ const segment = opts.segments[index];
453
+ if (!segment) continue;
454
+ assertRange(segment.startBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].startBeat`);
455
+ assertRange(segment.endBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].endBeat`);
456
+ if (segment.endBeat <= segment.startBeat) {
457
+ throw new RangeError(`segments[${index}] must have a positive duration`);
458
+ }
459
+ }
343
460
  const segments = [...opts.segments].sort((a, b) => a.startBeat - b.startBeat);
344
461
  if (segments.length === 0) {
345
462
  return [];
346
463
  }
347
464
  const ts = opts.ts ?? DEFAULT_TS;
465
+ assertTimeSignature(ts);
348
466
  const style = opts.style ?? DEFAULT_STYLE;
349
467
  const octave = opts.octave ?? DEFAULT_OCTAVE;
468
+ assertInteger(octave, "bass octave", -1, 9);
469
+ const estimatedNotes = segments.reduce(
470
+ (count, segment) => count + Math.max(1, Math.ceil(segment.endBeat - segment.startBeat)),
471
+ 0
472
+ );
473
+ assertGenerationBudget(estimatedNotes, "bass notes");
350
474
  const low = octave * 12 + 12;
351
475
  const ctx = {
352
476
  ts,
@@ -402,6 +526,73 @@ function generateBassLine(opts) {
402
526
  return out;
403
527
  }
404
528
 
529
+ // src/core/event-index/index.ts
530
+ var EPS3 = 1e-9;
531
+ function upperBound(values, beat) {
532
+ let low = 0;
533
+ let high = values.length;
534
+ while (low < high) {
535
+ const middle = Math.floor((low + high) / 2);
536
+ if ((values[middle]?.note.startBeat ?? Number.POSITIVE_INFINITY) <= beat) {
537
+ low = middle + 1;
538
+ } else {
539
+ high = middle;
540
+ }
541
+ }
542
+ return low;
543
+ }
544
+ function createNoteEventIndex(events, options = {}) {
545
+ assertNoteEvents(events, "note events", options);
546
+ const notes = events.map((note, originalIndex) => ({
547
+ note,
548
+ originalIndex,
549
+ endBeat: note.startBeat + note.durationBeat
550
+ })).sort((a, b) => a.note.startBeat - b.note.startBeat || a.originalIndex - b.originalIndex);
551
+ const prefixMaxEnd = [];
552
+ let maxEnd = Number.NEGATIVE_INFINITY;
553
+ for (const indexed of notes) {
554
+ maxEnd = Math.max(maxEnd, indexed.endBeat);
555
+ prefixMaxEnd.push(maxEnd);
556
+ }
557
+ return {
558
+ notes,
559
+ at(beat) {
560
+ let index = upperBound(notes, beat + EPS3) - 1;
561
+ while (index >= 0) {
562
+ if ((prefixMaxEnd[index] ?? Number.NEGATIVE_INFINITY) <= beat + EPS3) {
563
+ return void 0;
564
+ }
565
+ const indexed = notes[index];
566
+ if (indexed && indexed.note.durationBeat > 0 && indexed.note.startBeat - EPS3 <= beat && beat < indexed.endBeat - EPS3) {
567
+ return indexed;
568
+ }
569
+ index -= 1;
570
+ }
571
+ return void 0;
572
+ },
573
+ attacksAt(beat) {
574
+ const firstAfter = upperBound(notes, beat + EPS3);
575
+ const candidate = notes[firstAfter - 1];
576
+ return candidate !== void 0 && Math.abs(candidate.note.startBeat - beat) < EPS3;
577
+ },
578
+ onsetsBetween(startBeat, endBeat) {
579
+ const result = [];
580
+ let index = upperBound(notes, startBeat + EPS3);
581
+ while (index < notes.length) {
582
+ const onset = notes[index]?.note.startBeat;
583
+ if (onset === void 0 || onset >= endBeat - EPS3) {
584
+ break;
585
+ }
586
+ if (result[result.length - 1] !== onset) {
587
+ result.push(onset);
588
+ }
589
+ index += 1;
590
+ }
591
+ return result;
592
+ }
593
+ };
594
+ }
595
+
405
596
  // src/theory/chord/index.ts
406
597
  var QUALITY_INTERVALS = {
407
598
  maj: [0, 4, 7],
@@ -538,8 +729,10 @@ function classifySeventh(thirdIc, fifthIc, seventhIc) {
538
729
  function stackThirds(degree, key, size) {
539
730
  const tones = scaleTonesInDegreeOrder(key);
540
731
  const length = tones.length;
541
- if (length === 0) {
542
- return { rootPc: pitchClass3(key.rootPc), quality: "maj", intervals: [0, 4, 7] };
732
+ if (length !== 7) {
733
+ throw new Error(
734
+ `diatonic chord stacking requires a heptatonic scale (received ${length} tones)`
735
+ );
543
736
  }
544
737
  const idx = (step) => ((degree + step) % length + length) % length;
545
738
  const rootPc = tones[idx(0)] ?? pitchClass3(key.rootPc);
@@ -617,6 +810,31 @@ function createsHiddenParallelPerfect(aPrev, aCur, bPrev, bCur) {
617
810
  // src/theory/safety/index.ts
618
811
  var SUGGESTION_WINDOW = 12;
619
812
  var MAX_SUGGESTIONS = 3;
813
+ var REASON_SEVERITY = [
814
+ [2 /* Tension */, 1 /* Warning */, 1 /* Warning */],
815
+ [4 /* AvoidNote */, 1 /* Warning */, 2 /* Dissonant */],
816
+ [8 /* ScaleTone */, 1 /* Warning */, 1 /* Warning */],
817
+ [16 /* NonScale */, 2 /* Dissonant */, 2 /* Dissonant */],
818
+ [32 /* OutOfRange */, 1 /* Warning */, 1 /* Warning */],
819
+ [64 /* Tritone */, 2 /* Dissonant */, 2 /* Dissonant */],
820
+ [128 /* LargeLeap */, 1 /* Warning */, 2 /* Dissonant */],
821
+ [1024 /* VerticalDissonance */, 2 /* Dissonant */, 2 /* Dissonant */],
822
+ [2048 /* ParallelPerfect */, 1 /* Warning */, 2 /* Dissonant */],
823
+ [4096 /* HiddenParallel */, 1 /* Warning */, 2 /* Dissonant */],
824
+ [8192 /* VoiceCrossing */, 1 /* Warning */, 2 /* Dissonant */],
825
+ [16384 /* Suspension */, 2 /* Dissonant */, 2 /* Dissonant */],
826
+ [32768 /* NeedsResolution */, 1 /* Warning */, 2 /* Dissonant */]
827
+ ];
828
+ function minimumSafetyForReasons(reasons, profile) {
829
+ let minimum = 0 /* Safe */;
830
+ const severityIndex = profile === "strict" ? 2 : 1;
831
+ for (const entry of REASON_SEVERITY) {
832
+ if (reasons & entry[0]) {
833
+ minimum = Math.max(minimum, entry[severityIndex]);
834
+ }
835
+ }
836
+ return minimum;
837
+ }
620
838
  function pitchClass4(pitch) {
621
839
  return (Math.trunc(pitch) % 12 + 12) % 12;
622
840
  }
@@ -640,6 +858,28 @@ function stepResolution(pitch, chord) {
640
858
  }
641
859
  return void 0;
642
860
  }
861
+ function evaluateSafety(q) {
862
+ assertSafetyContext(q);
863
+ assertFiniteNumber(q.candidatePitch, "candidatePitch");
864
+ return evaluateInternal(q, true);
865
+ }
866
+ function assertSafetyContext(q) {
867
+ if (q.prevPitch !== void 0) assertFiniteNumber(q.prevPitch, "prevPitch");
868
+ if (q.vocalLow !== void 0) assertFiniteNumber(q.vocalLow, "vocalLow");
869
+ if (q.vocalHigh !== void 0) assertFiniteNumber(q.vocalHigh, "vocalHigh");
870
+ if (q.vocalLow !== void 0 && q.vocalHigh !== void 0 && q.vocalLow > q.vocalHigh) {
871
+ throw new RangeError("vocalLow must not exceed vocalHigh");
872
+ }
873
+ assertGenerationBudget(q.otherVoices.length, "other voices");
874
+ for (let index = 0; index < q.otherVoices.length; index += 1) {
875
+ const voice = q.otherVoices[index];
876
+ if (!voice) continue;
877
+ assertFiniteNumber(voice.pitch, `otherVoices[${index}].pitch`);
878
+ if (voice.prevPitch !== void 0) {
879
+ assertFiniteNumber(voice.prevPitch, `otherVoices[${index}].prevPitch`);
880
+ }
881
+ }
882
+ }
643
883
  function evaluateInternal(q, collectSuggestions) {
644
884
  let reasons = 0;
645
885
  let level = 0 /* Safe */;
@@ -700,9 +940,11 @@ function evaluateInternal(q, collectSuggestions) {
700
940
  const pc = Math.abs(pitch - q.prevPitch) % 12;
701
941
  if (pc === 6) {
702
942
  reasons |= 64 /* Tritone */;
943
+ raise(2 /* Dissonant */);
703
944
  }
704
945
  if (isForbiddenMelodicLeap(q.prevPitch, pitch)) {
705
946
  reasons |= 128 /* LargeLeap */;
947
+ raise(parallelSeverity);
706
948
  }
707
949
  if (pc === 1) {
708
950
  reasons |= 256 /* MinorSecond */;
@@ -747,6 +989,7 @@ function evaluateInternal(q, collectSuggestions) {
747
989
  }
748
990
  }
749
991
  }
992
+ raise(minimumSafetyForReasons(reasons, q.profile));
750
993
  const result = { safety: level, reasons, rationale: describe(reasons, q) };
751
994
  if (resolveTo !== void 0) {
752
995
  result.resolveTo = resolveTo;
@@ -810,11 +1053,15 @@ function describe(reasons, q) {
810
1053
  return q.chord ? "Placeable" : "No chord context";
811
1054
  }
812
1055
  function enumerateSafePitches(q, pitchLow, pitchHigh) {
1056
+ assertSafetyContext(q);
813
1057
  const chordTones = [];
814
1058
  const others = [];
815
- if (!Number.isFinite(pitchLow) || !Number.isFinite(pitchHigh)) {
816
- return [];
1059
+ assertInteger(pitchLow, "pitchLow");
1060
+ assertInteger(pitchHigh, "pitchHigh");
1061
+ if (pitchLow > pitchHigh) {
1062
+ throw new RangeError(`pitchLow must not exceed pitchHigh; received ${pitchLow} > ${pitchHigh}`);
817
1063
  }
1064
+ assertGenerationBudget(pitchHigh - pitchLow + 1, "safe pitch candidates");
818
1065
  for (let pitch = pitchHigh; pitch >= pitchLow; pitch -= 1) {
819
1066
  const result = evaluateInternal({ ...q, candidatePitch: pitch }, false);
820
1067
  if (result.safety === 2 /* Dissonant */) {
@@ -831,7 +1078,7 @@ function enumerateSafePitches(q, pitchLow, pitchHigh) {
831
1078
 
832
1079
  // src/generate/countermelody/index.ts
833
1080
  var DEFAULT_TS2 = { numerator: 4, denominator: 4 };
834
- var EPS3 = 1e-9;
1081
+ var EPS4 = 1e-9;
835
1082
  var GRID_STEP = 0.5;
836
1083
  var MIN_ONSET_GAP = 1;
837
1084
  var REGISTER_OFFSET = 12;
@@ -842,30 +1089,35 @@ var REINFORCE_PROB = 0.35;
842
1089
  var DEFAULT_MELODY_VELOCITY = 96;
843
1090
  var VELOCITY_DROP = 16;
844
1091
  function isWholeBeat(beat) {
845
- return Math.abs(beat - Math.round(beat)) < EPS3;
1092
+ return Math.abs(beat - Math.round(beat)) < EPS4;
846
1093
  }
847
1094
  function melodyNoteAt(melody, beat) {
848
- let found;
849
- for (const note of melody) {
850
- if (note.startBeat - EPS3 <= beat && beat < note.startBeat + note.durationBeat - EPS3) {
851
- if (!found || note.startBeat > found.startBeat) {
852
- found = note;
853
- }
854
- }
855
- }
856
- return found;
1095
+ return melody.at(beat)?.note;
857
1096
  }
858
1097
  function melodyAttacksAt(melody, beat) {
859
- return melody.some((note) => Math.abs(note.startBeat - beat) < EPS3);
1098
+ return melody.attacksAt(beat);
1099
+ }
1100
+ function melodySnapshotAt(melody, beat) {
1101
+ const current = melodyNoteAt(melody, beat);
1102
+ if (current === void 0) {
1103
+ return void 0;
1104
+ }
1105
+ const attacked = Math.abs(current.startBeat - beat) < EPS4;
1106
+ const previous = attacked ? melodyNoteAt(melody, beat - EPS4 * 2) : current;
1107
+ const snapshot = { pitch: current.pitch };
1108
+ if (previous !== void 0) {
1109
+ snapshot.prevPitch = previous.pitch;
1110
+ }
1111
+ return snapshot;
860
1112
  }
861
1113
  function followOnsets(melody) {
862
- return [...new Set(melody.map((note) => note.startBeat))].sort((a, b) => a - b);
1114
+ return [...new Set(melody.notes.map(({ note }) => note.startBeat))];
863
1115
  }
864
1116
  function complementOnsets(melody, ts, spanStart, spanEnd, rng) {
865
1117
  const onsets = [];
866
1118
  const gridStart = Math.floor(spanStart / GRID_STEP) * GRID_STEP;
867
1119
  let last = Number.NEGATIVE_INFINITY;
868
- for (let beat = gridStart; beat < spanEnd - EPS3; beat += GRID_STEP) {
1120
+ for (let beat = gridStart; beat < spanEnd - EPS4; beat += GRID_STEP) {
869
1121
  const attacked = melodyAttacksAt(melody, beat);
870
1122
  const sounding = melodyNoteAt(melody, beat) !== void 0;
871
1123
  let place = false;
@@ -876,7 +1128,7 @@ function complementOnsets(melody, ts, spanStart, spanEnd, rng) {
876
1128
  } else if (isStrongBeat(beat, ts)) {
877
1129
  place = rng.prob(REINFORCE_PROB);
878
1130
  }
879
- if (place && beat - last >= MIN_ONSET_GAP - EPS3) {
1131
+ if (place && beat - last >= MIN_ONSET_GAP - EPS4) {
880
1132
  onsets.push(beat);
881
1133
  last = beat;
882
1134
  }
@@ -922,54 +1174,102 @@ function scoreCandidate(pitch, melPitch, melPrev, prevPitch, chord, center) {
922
1174
  score -= Math.abs(pitch - center) * 0.02;
923
1175
  return score;
924
1176
  }
925
- function fallbackPitch(melPitch, prevPitch, center, register, low, high, key) {
926
- let target = prevPitch ?? Math.round(center);
927
- if (melPitch !== void 0) {
928
- if (register === "below" && target >= melPitch) {
929
- target = melPitch - 5;
930
- } else if (register === "above" && target <= melPitch) {
931
- target = melPitch + 5;
932
- }
933
- }
934
- target = Math.min(Math.max(target, low), high);
935
- let pitch = nearestScaleTone(target, key);
936
- if (melPitch !== void 0) {
937
- while (register === "below" && pitch >= melPitch && pitch - 12 >= low) {
938
- pitch -= 12;
939
- }
940
- while (register === "above" && pitch <= melPitch && pitch + 12 <= high) {
941
- pitch += 12;
1177
+ function heldNoteBoundaries(melody, startBeat, endBeat, chordChangeBeats) {
1178
+ const boundaries = /* @__PURE__ */ new Set([startBeat]);
1179
+ for (const onset of melody.onsetsBetween(startBeat, endBeat)) {
1180
+ boundaries.add(onset);
1181
+ }
1182
+ for (const beat of chordChangeBeats) {
1183
+ if (beat > startBeat + EPS4 && beat < endBeat - EPS4) {
1184
+ boundaries.add(beat);
1185
+ }
1186
+ }
1187
+ const firstGrid = Math.floor(startBeat / GRID_STEP + 1) * GRID_STEP;
1188
+ for (let beat = firstGrid; beat < endBeat - EPS4; beat += GRID_STEP) {
1189
+ boundaries.add(beat);
1190
+ }
1191
+ return [...boundaries].sort((a, b) => a - b);
1192
+ }
1193
+ function heldPitchSafety(pitch, startBeat, endBeat, prevPitch, melody, opts, profile, register, low, high, ts) {
1194
+ let worst = 0 /* Safe */;
1195
+ const boundaries = heldNoteBoundaries(melody, startBeat, endBeat, opts.chordChangeBeats ?? []);
1196
+ for (const boundary of boundaries) {
1197
+ const melodyVoice = melodySnapshotAt(melody, boundary);
1198
+ if (melodyVoice !== void 0 && (register === "below" && pitch >= melodyVoice.pitch || register === "above" && pitch <= melodyVoice.pitch)) {
1199
+ return null;
1200
+ }
1201
+ const atCounterOnset = Math.abs(boundary - startBeat) < EPS4;
1202
+ const result = evaluateSafety({
1203
+ profile,
1204
+ candidatePitch: pitch,
1205
+ prevPitch: atCounterOnset ? prevPitch : pitch,
1206
+ chord: opts.chordAt(boundary),
1207
+ key: opts.key,
1208
+ otherVoices: melodyVoice ? [melodyVoice] : [],
1209
+ strongBeat: isStrongBeat(boundary, ts),
1210
+ vocalLow: low,
1211
+ vocalHigh: high
1212
+ });
1213
+ worst = Math.max(worst, result.safety);
1214
+ if (worst === 2 /* Dissonant */) {
1215
+ return worst;
942
1216
  }
943
1217
  }
944
- return Math.min(Math.max(pitch, low), high);
1218
+ return worst;
945
1219
  }
946
1220
  function generateCounterMelody(opts) {
947
- const melody = [...opts.melody].sort((a, b) => a.startBeat - b.startBeat);
948
- if (melody.length === 0) {
1221
+ const melody = createNoteEventIndex(opts.melody);
1222
+ if (melody.notes.length === 0) {
949
1223
  return [];
950
1224
  }
951
1225
  const ts = opts.ts ?? DEFAULT_TS2;
1226
+ assertTimeSignature(ts);
952
1227
  const register = opts.register ?? "below";
953
1228
  const rhythm = opts.rhythm ?? "complement";
954
1229
  const profile = opts.profile ?? "pop";
955
1230
  const rng = createRng(opts.seed ?? 0);
956
- const spanStart = melody[0]?.startBeat ?? 0;
957
- const spanEnd = melody.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), spanStart);
958
- const meanPitch = melody.reduce((sum, n) => sum + n.pitch, 0) / melody.length;
1231
+ const spanStart = melody.notes[0]?.note.startBeat ?? 0;
1232
+ const spanEnd = melody.notes.reduce((end, n) => Math.max(end, n.endBeat), spanStart);
1233
+ assertGenerationBudget(Math.ceil((spanEnd - spanStart) / GRID_STEP), "countermelody grid");
1234
+ const meanPitch = melody.notes.reduce((sum, n) => sum + n.note.pitch, 0) / melody.notes.length;
959
1235
  const defaultCenter = register === "below" ? meanPitch - REGISTER_OFFSET : meanPitch + REGISTER_OFFSET;
960
1236
  const low = opts.pitchLow ?? Math.round(defaultCenter) - DEFAULT_HALF_RANGE;
961
1237
  const high = opts.pitchHigh ?? Math.round(defaultCenter) + DEFAULT_HALF_RANGE;
1238
+ assertInteger(low, "countermelody pitchLow");
1239
+ assertInteger(high, "countermelody pitchHigh");
1240
+ if (low > high) {
1241
+ throw new RangeError(
1242
+ `countermelody pitchLow must not exceed pitchHigh; received ${low} > ${high}`
1243
+ );
1244
+ }
1245
+ assertGenerationBudget(high - low + 1, "countermelody pitch candidates");
1246
+ assertGenerationBudget(opts.chordChangeBeats?.length ?? 0, "chord change beats");
1247
+ for (let index = 0; index < (opts.chordChangeBeats?.length ?? 0); index += 1) {
1248
+ assertRange(
1249
+ opts.chordChangeBeats?.[index] ?? Number.NaN,
1250
+ 0,
1251
+ Number.MAX_SAFE_INTEGER,
1252
+ `chordChangeBeats[${index}]`
1253
+ );
1254
+ }
962
1255
  const center = (low + high) / 2;
963
1256
  const onsets = rhythm === "follow" ? followOnsets(melody) : complementOnsets(melody, ts, spanStart, spanEnd, rng);
1257
+ assertGenerationBudget(onsets.length * (high - low + 1), "countermelody search");
964
1258
  const out = [];
965
1259
  let prevPitch;
966
- let prevOnset;
967
- for (const beat of onsets) {
1260
+ for (let onsetIndex = 0; onsetIndex < onsets.length; onsetIndex += 1) {
1261
+ const beat = onsets[onsetIndex];
1262
+ if (beat === void 0) {
1263
+ continue;
1264
+ }
1265
+ const nextOnset = onsets[onsetIndex + 1];
1266
+ const endBeat = nextOnset ?? Math.max(spanEnd, beat + GRID_STEP);
968
1267
  const melNote = melodyNoteAt(melody, beat);
969
1268
  const melPitch = melNote?.pitch;
970
- const melPrev = prevOnset !== void 0 ? melodyNoteAt(melody, prevOnset)?.pitch : void 0;
1269
+ const melodyVoice = melodySnapshotAt(melody, beat);
1270
+ const melPrev = melodyVoice?.prevPitch;
971
1271
  const chord = opts.chordAt(beat);
972
- const otherVoices = melPitch !== void 0 ? [{ pitch: melPitch, prevPitch: melPrev }] : [];
1272
+ const otherVoices = melodyVoice !== void 0 ? [melodyVoice] : [];
973
1273
  const candidates = enumerateSafePitches(
974
1274
  {
975
1275
  profile,
@@ -985,15 +1285,26 @@ function generateCounterMelody(opts) {
985
1285
  high
986
1286
  );
987
1287
  let bestPitch;
1288
+ let bestWorstSafety = 2 /* Dissonant */;
988
1289
  let bestScore = Number.NEGATIVE_INFINITY;
989
1290
  for (const candidate of candidates) {
1291
+ const worstSafety = heldPitchSafety(
1292
+ candidate,
1293
+ beat,
1294
+ endBeat,
1295
+ prevPitch,
1296
+ melody,
1297
+ opts,
1298
+ profile,
1299
+ register,
1300
+ low,
1301
+ high,
1302
+ ts
1303
+ );
1304
+ if (worstSafety === null || worstSafety === 2 /* Dissonant */) {
1305
+ continue;
1306
+ }
990
1307
  if (melPitch !== void 0) {
991
- if (register === "below" && candidate >= melPitch) {
992
- continue;
993
- }
994
- if (register === "above" && candidate <= melPitch) {
995
- continue;
996
- }
997
1308
  if (prevPitch !== void 0 && melPrev !== void 0 && // Parallel octaves are the perfect-class-zero case of this predicate,
998
1309
  // so a single check covers both parallel fifths and octaves.
999
1310
  createsParallelPerfect(prevPitch, candidate, melPrev, melPitch)) {
@@ -1001,29 +1312,24 @@ function generateCounterMelody(opts) {
1001
1312
  }
1002
1313
  }
1003
1314
  const score = scoreCandidate(candidate, melPitch, melPrev, prevPitch, chord, center);
1004
- const tie = Math.abs(score - bestScore) <= EPS3 && candidate < (bestPitch ?? Number.POSITIVE_INFINITY);
1005
- if (score > bestScore + EPS3 || tie) {
1315
+ const tie = Math.abs(score - bestScore) <= EPS4 && candidate < (bestPitch ?? Number.POSITIVE_INFINITY);
1316
+ if (worstSafety < bestWorstSafety || worstSafety === bestWorstSafety && (score > bestScore + EPS4 || tie)) {
1317
+ bestWorstSafety = worstSafety;
1006
1318
  bestScore = score;
1007
1319
  bestPitch = candidate;
1008
1320
  }
1009
1321
  }
1010
- const pitch = bestPitch ?? fallbackPitch(melPitch, prevPitch, center, register, low, high, opts.key);
1322
+ if (bestPitch === void 0) {
1323
+ prevPitch = void 0;
1324
+ continue;
1325
+ }
1326
+ const pitch = bestPitch;
1011
1327
  const velocity = Math.max(
1012
1328
  1,
1013
1329
  Math.min(127, Math.round((melNote?.velocity ?? DEFAULT_MELODY_VELOCITY) - VELOCITY_DROP))
1014
1330
  );
1015
- out.push({ pitch, startBeat: beat, durationBeat: GRID_STEP, velocity });
1331
+ out.push({ pitch, startBeat: beat, durationBeat: endBeat - beat, velocity });
1016
1332
  prevPitch = pitch;
1017
- prevOnset = beat;
1018
- }
1019
- for (let i = 0; i < out.length; i += 1) {
1020
- const note = out[i];
1021
- if (!note) {
1022
- continue;
1023
- }
1024
- const next = out[i + 1];
1025
- const end = next ? next.startBeat : Math.max(spanEnd, note.startBeat + GRID_STEP);
1026
- note.durationBeat = end - note.startBeat;
1027
1333
  }
1028
1334
  return out;
1029
1335
  }
@@ -1743,9 +2049,10 @@ function generateHiHatForBeat(ctx, sec) {
1743
2049
 
1744
2050
  // src/generate/drums/euclid.ts
1745
2051
  function euclideanRhythm(pulses, steps, rotation = 0) {
1746
- if (steps < 1) {
1747
- return [];
1748
- }
2052
+ assertPositiveInt(steps, "euclidean steps");
2053
+ assertInteger(pulses, "euclidean pulses");
2054
+ assertInteger(rotation, "euclidean rotation");
2055
+ assertGenerationBudget(steps, "euclidean pattern steps");
1749
2056
  const p = Math.max(0, Math.min(pulses, steps));
1750
2057
  const base = bjorklund(p, steps);
1751
2058
  const shift = (rotation % steps + steps) % steps;
@@ -1754,18 +2061,6 @@ function euclideanRhythm(pulses, steps, rotation = 0) {
1754
2061
  }
1755
2062
  return base.map((_, i) => base[(i - shift + steps) % steps] ?? false);
1756
2063
  }
1757
- function hasHit(mask, step) {
1758
- return (mask >> step & 1) === 1;
1759
- }
1760
- function patternToMask(pattern) {
1761
- let mask = 0;
1762
- for (let i = 0; i < pattern.length; i += 1) {
1763
- if (pattern[i]) {
1764
- mask |= 1 << i;
1765
- }
1766
- }
1767
- return mask;
1768
- }
1769
2064
  function bjorklund(pulses, steps) {
1770
2065
  if (pulses === 0) {
1771
2066
  return Array.from({ length: steps }, () => false);
@@ -2097,19 +2392,6 @@ function isInPreChorusLift(section, bar, sectionBars, nextIsChorus) {
2097
2392
  }
2098
2393
  return bar >= sectionBars - 2;
2099
2394
  }
2100
- function euclideanToKickPattern(mask) {
2101
- const slot = (step) => hasHit(mask, step) || hasHit(mask, (step + 15) % 16);
2102
- return {
2103
- beat1: slot(0),
2104
- beat1and: slot(2),
2105
- beat2: slot(4),
2106
- beat2and: slot(6),
2107
- beat3: slot(8),
2108
- beat3and: slot(10),
2109
- beat4: slot(12),
2110
- beat4and: slot(14)
2111
- };
2112
- }
2113
2395
  function getKickPattern(section, style, bar, rng) {
2114
2396
  const p = { ...EMPTY };
2115
2397
  if (section === "outro") {
@@ -2231,7 +2513,7 @@ function getPercussionConfig(mood, section) {
2231
2513
  };
2232
2514
  }
2233
2515
  function generateAuxPercussionForBar(track, barStart, config, role, densityMult, rng, bpm) {
2234
- if (role === "minimal" || role === "fxOnly") {
2516
+ if (role === "minimal") {
2235
2517
  return;
2236
2518
  }
2237
2519
  if (config.tambourine) {
@@ -2280,6 +2562,16 @@ function generateAuxPercussionForBar(track, barStart, config, role, densityMult,
2280
2562
 
2281
2563
  // src/generate/drums/index.ts
2282
2564
  function generateDrums(opts) {
2565
+ assertPositiveInt(opts.bars, "drum bars");
2566
+ assertRange(opts.bpm, Number.MIN_VALUE, 1e3, "drum bpm");
2567
+ assertRange(opts.density, 0, 1, "drum density");
2568
+ assertGenerationBudget(opts.bars * 128, "drum hits");
2569
+ if (opts.euclideanKick) {
2570
+ const steps = opts.euclideanKick.steps ?? 16;
2571
+ assertInteger(steps, "euclidean steps", 1, 16);
2572
+ assertInteger(opts.euclideanKick.pulses, "euclidean pulses");
2573
+ assertInteger(opts.euclideanKick.rotation ?? 0, "euclidean rotation");
2574
+ }
2283
2575
  const track = new HitList();
2284
2576
  const rng = createRng(opts.seed ?? 0);
2285
2577
  const mapping = mapStyle(opts.style);
@@ -2316,15 +2608,8 @@ function generateDrums(opts) {
2316
2608
  const percMood = percMoodCategory(style);
2317
2609
  const nextSection = opts.nextSection ? mapSection(opts.nextSection) : section;
2318
2610
  const nextEnergy = sectionEnergy(nextSection);
2319
- const euclidKick = opts.euclideanKick ? euclideanToKickPattern(
2320
- patternToMask(
2321
- euclideanRhythm(
2322
- opts.euclideanKick.pulses,
2323
- opts.euclideanKick.steps ?? 16,
2324
- opts.euclideanKick.rotation ?? 0
2325
- )
2326
- )
2327
- ) : void 0;
2611
+ const euclidSteps = opts.euclideanKick?.steps ?? 16;
2612
+ const euclidKick = opts.euclideanKick ? euclideanRhythm(opts.euclideanKick.pulses, euclidSteps, opts.euclideanKick.rotation ?? 0) : void 0;
2328
2613
  const reuseSectionKick = (section === "b" || section === "chorus") && style !== "sparse";
2329
2614
  let sectionKick;
2330
2615
  for (let bar = 0; bar < opts.bars; bar += 1) {
@@ -2340,12 +2625,10 @@ function generateDrums(opts) {
2340
2625
  barHasOpenHh = !track.hasCrashNear(barStart + openHhBeatIndex);
2341
2626
  }
2342
2627
  let kick;
2343
- if (euclidKick) {
2344
- kick = euclidKick;
2345
- } else if (reuseSectionKick) {
2628
+ if (!euclidKick && reuseSectionKick) {
2346
2629
  sectionKick ??= getKickPattern(section, style, 0, rng);
2347
2630
  kick = sectionKick;
2348
- } else {
2631
+ } else if (!euclidKick) {
2349
2632
  kick = getKickPattern(section, style, bar, rng);
2350
2633
  }
2351
2634
  const inLift = isInPreChorusLift(section, bar, opts.bars, section === "b");
@@ -2386,7 +2669,23 @@ function generateDrums(opts) {
2386
2669
  if (inLift) {
2387
2670
  generatePreChorusBuildup(ctx, sec, isLastBar);
2388
2671
  }
2389
- generateKickForBeat(ctx, sec, kick);
2672
+ if (euclidKick && !inLift) {
2673
+ const stepLength = 4 / euclidSteps;
2674
+ for (let step = 0; step < euclidKick.length; step += 1) {
2675
+ const onset = step * stepLength;
2676
+ if (euclidKick[step] && Math.floor(onset) === beat) {
2677
+ const wholeBeat = Math.abs(onset - Math.round(onset)) < 1e-9;
2678
+ track.add(
2679
+ GM.BD,
2680
+ barStart + onset,
2681
+ Math.min(0.5, stepLength),
2682
+ velocity * (wholeBeat ? 1 : 0.85)
2683
+ );
2684
+ }
2685
+ }
2686
+ } else if (kick) {
2687
+ generateKickForBeat(ctx, sec, kick);
2688
+ }
2390
2689
  generateSnareForBeat(ctx, sec, section === "intro" && bar === 0);
2391
2690
  if (sec.useGhostNotes && !inLift) {
2392
2691
  generateGhostNotesForBeat(ctx, sec);
@@ -2403,10 +2702,19 @@ function generateDrums(opts) {
2403
2702
  }
2404
2703
  }
2405
2704
  }
2705
+ let percussionConfig = getPercussionConfig(percMood, section);
2706
+ if (role === "fxOnly" && !percussionConfig.tambourine && !percussionConfig.shaker && !percussionConfig.handclap) {
2707
+ percussionConfig = {
2708
+ tambourine: false,
2709
+ shaker: true,
2710
+ handclap: false,
2711
+ shaker16th: false
2712
+ };
2713
+ }
2406
2714
  generateAuxPercussionForBar(
2407
2715
  track,
2408
2716
  barStart,
2409
- getPercussionConfig(percMood, section),
2717
+ percussionConfig,
2410
2718
  role,
2411
2719
  densityMult,
2412
2720
  rng,
@@ -2427,10 +2735,16 @@ var MIN_VELOCITY = 1;
2427
2735
  var MAX_VELOCITY = 127;
2428
2736
  function humanize(events, opts = {}) {
2429
2737
  const ts = opts.ts ?? DEFAULT_TS3;
2738
+ assertTimeSignature(ts);
2739
+ assertNoteEvents(events, "humanize events");
2430
2740
  const timing = opts.timing ?? DEFAULT_TIMING;
2431
2741
  const velocityJitter = opts.velocity ?? DEFAULT_VELOCITY_JITTER;
2432
2742
  const accent = opts.accent ?? DEFAULT_ACCENT;
2433
2743
  const baseVelocity = opts.baseVelocity ?? DEFAULT_BASE_VELOCITY;
2744
+ assertRange(timing, 0, Number.MAX_SAFE_INTEGER, "humanize timing");
2745
+ assertRange(velocityJitter, 0, 127, "humanize velocity jitter");
2746
+ assertRange(accent, 0, 127, "humanize accent");
2747
+ assertRange(baseVelocity, 0, 127, "humanize base velocity");
2434
2748
  const rng = createRng(opts.seed ?? 0);
2435
2749
  return events.map((event) => {
2436
2750
  const timingOffset = rng.float(-timing, timing);
@@ -2463,8 +2777,12 @@ function quantizeToGrid(beat, barBeats, subdivision, slotsPerBar) {
2463
2777
  };
2464
2778
  }
2465
2779
  function extractGrooveTemplate(events, ts, subdivision) {
2780
+ assertTimeSignature(ts);
2781
+ assertNoteEvents(events, "groove source events");
2782
+ assertPositiveInt(subdivision, "groove subdivision");
2466
2783
  const barBeats = beatsPerBar(ts);
2467
2784
  const slotsPerBar = Math.round(barBeats * subdivision);
2785
+ assertGenerationBudget(slotsPerBar, "groove template slots");
2468
2786
  const offsetSums = new Array(slotsPerBar).fill(0);
2469
2787
  const velocitySums = new Array(slotsPerBar).fill(0);
2470
2788
  const counts = new Array(slotsPerBar).fill(0);
@@ -2499,6 +2817,14 @@ function extractGrooveTemplate(events, ts, subdivision) {
2499
2817
  return { subdivision, slotsPerBar, slots, ts };
2500
2818
  }
2501
2819
  function applyGrooveTemplate(events, template, ts) {
2820
+ assertTimeSignature(ts);
2821
+ assertNoteEvents(events, "groove target events");
2822
+ assertPositiveInt(template.subdivision, "template subdivision");
2823
+ assertPositiveInt(template.slotsPerBar, "template slotsPerBar");
2824
+ assertGenerationBudget(template.slots.length, "template slots");
2825
+ if (template.slots.length !== template.slotsPerBar) {
2826
+ throw new RangeError("template slots length must equal slotsPerBar");
2827
+ }
2502
2828
  if (template.ts && (template.ts.numerator !== ts.numerator || template.ts.denominator !== ts.denominator)) {
2503
2829
  throw new Error(
2504
2830
  `Groove template meter ${formatTimeSignature(template.ts)} does not match apply-time meter ${formatTimeSignature(ts)}`
@@ -2524,107 +2850,291 @@ function applyGrooveTemplate(events, template, ts) {
2524
2850
  });
2525
2851
  }
2526
2852
 
2527
- // src/theory/harmony/index.ts
2528
- function roleOf(pitch, chord, chordId = 0) {
2529
- const interval = ((Math.trunc(pitch) - chord.rootPc) % 12 + 12) % 12;
2530
- const tones = new Set(chord.intervals.map((i) => (i % 12 + 12) % 12));
2531
- const hasHigherSeventh = tones.has(10) || tones.has(11);
2532
- const hasThird = tones.has(3) || tones.has(4);
2533
- const isSuspendedTone = !hasThird && (interval === 5 && tones.has(5) || interval === 2 && tones.has(2));
2534
- let role;
2535
- let lock;
2536
- if (interval === 0) {
2537
- role = "root";
2538
- lock = "identity";
2539
- } else if (isSuspendedTone) {
2540
- role = "third";
2541
- lock = "quality";
2542
- } else if (interval === 3 || interval === 4) {
2543
- role = "third";
2544
- lock = "quality";
2545
- } else if (interval === 6 || interval === 7 || interval === 8) {
2546
- role = "fifth";
2547
- lock = "voicing";
2548
- } else if (interval === 9 && tones.has(3) && tones.has(6) && !hasHigherSeventh) {
2549
- role = "seventh";
2550
- lock = "voicing";
2551
- } else if (interval === 9 && tones.has(9) && !hasHigherSeventh) {
2552
- role = "sixth";
2553
- lock = "voicing";
2554
- } else if (interval === 10 || interval === 11) {
2555
- role = "seventh";
2556
- lock = "voicing";
2557
- } else {
2558
- role = "tension";
2559
- lock = "voicing";
2560
- }
2561
- return { role, lock, belongsToChordId: chordId };
2853
+ // src/analyze/functional/internal.ts
2854
+ function mod12(n) {
2855
+ return (n % 12 + 12) % 12;
2562
2856
  }
2563
-
2564
- // src/generate/harmonize/index.ts
2565
- var FALLBACK = { rootPc: 0, quality: "maj", secondaryDominant: false, base: 0 };
2566
- var DEFAULT_METER = { numerator: 4, denominator: 4 };
2567
- var COMFORT_LOW = 55;
2568
- var COMFORT_HIGH = 79;
2569
- var TESSITURA_WEIGHT = 1e-3;
2570
- var TIE_BREAK_JITTER = 1e-6;
2571
- function pitchClass5(pitch) {
2572
- return (Math.trunc(pitch) % 12 + 12) % 12;
2857
+ function isNeapolitan(chord, key) {
2858
+ return mod12(chord.rootPc - key.rootPc) === 1 && chord.quality === "maj";
2573
2859
  }
2574
- function mulberry32(seed) {
2575
- let state = seed >>> 0;
2576
- return () => {
2577
- state = state + 1831565813 >>> 0;
2578
- let t = state;
2579
- t = Math.imul(t ^ t >>> 15, t | 1);
2580
- t ^= t + Math.imul(t ^ t >>> 7, t | 61);
2581
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
2582
- };
2860
+
2861
+ // src/analyze/functional/roman.ts
2862
+ var ROMAN = ["I", "II", "III", "IV", "V", "VI", "VII"];
2863
+ var OFFSET_SPELLING = [
2864
+ [1, ""],
2865
+ [2, "b"],
2866
+ [2, ""],
2867
+ [3, "b"],
2868
+ [3, ""],
2869
+ [4, ""],
2870
+ [4, "#"],
2871
+ [5, ""],
2872
+ [6, "b"],
2873
+ [6, ""],
2874
+ [7, "b"],
2875
+ [7, ""]
2876
+ ];
2877
+ var ROMAN_STYLE = {
2878
+ maj: { lower: false, suffix: "" },
2879
+ min: { lower: true, suffix: "" },
2880
+ dim: { lower: true, suffix: "o" },
2881
+ aug: { lower: false, suffix: "+" },
2882
+ maj7: { lower: false, suffix: "maj7" },
2883
+ min7: { lower: true, suffix: "7" },
2884
+ dom7: { lower: false, suffix: "7" },
2885
+ dim7: { lower: true, suffix: "o7" },
2886
+ m7b5: { lower: true, suffix: "\xF87" },
2887
+ minMaj7: { lower: true, suffix: "maj7" },
2888
+ aug7: { lower: false, suffix: "+7" },
2889
+ augMaj7: { lower: false, suffix: "+maj7" },
2890
+ majb5: { lower: false, suffix: "b5" },
2891
+ "6": { lower: false, suffix: "add6" },
2892
+ min6: { lower: true, suffix: "add6" },
2893
+ "6/9": { lower: false, suffix: "69" },
2894
+ sus2: { lower: false, suffix: "sus2" },
2895
+ sus4: { lower: false, suffix: "sus4" },
2896
+ add9: { lower: false, suffix: "add9" },
2897
+ add11: { lower: false, suffix: "add11" },
2898
+ maj9: { lower: false, suffix: "maj9" },
2899
+ min9: { lower: true, suffix: "9" },
2900
+ dom9: { lower: false, suffix: "9" },
2901
+ "7b9": { lower: false, suffix: "7b9" },
2902
+ "7#9": { lower: false, suffix: "7#9" },
2903
+ "7#11": { lower: false, suffix: "7#11" },
2904
+ "7b13": { lower: false, suffix: "7b13" },
2905
+ "11": { lower: false, suffix: "11" },
2906
+ "13": { lower: false, suffix: "13" },
2907
+ "5": { lower: false, suffix: "5" }
2908
+ };
2909
+ var SUFFIX_QUALITY = new Map(
2910
+ Object.entries(ROMAN_STYLE).map(
2911
+ ([quality, style]) => [`${style.lower ? "l" : "u"}:${style.suffix}`, quality]
2912
+ )
2913
+ );
2914
+ function romanStyle(quality) {
2915
+ return ROMAN_STYLE[quality];
2583
2916
  }
2584
- function inferKey(melody) {
2585
- let best = majorKey(0);
2586
- let bestScore = Number.NEGATIVE_INFINITY;
2587
- const candidates = [];
2588
- for (let tonic = 0; tonic < 12; tonic += 1) {
2589
- candidates.push(majorKey(tonic), minorKey(tonic));
2917
+ function romanSpelling(rootPc, key) {
2918
+ const tones = scaleTonesInDegreeOrder(key);
2919
+ const diatonic = tones.indexOf(mod12(rootPc));
2920
+ if (diatonic >= 0) {
2921
+ return { degreeNumber: diatonic + 1, accidental: "" };
2590
2922
  }
2591
- for (const key of candidates) {
2592
- const tonic = pitchClass5(key.rootPc);
2593
- const isMinor = key.modeMask12 === NATURAL_MINOR_MASK;
2594
- const leadingTone = (tonic + 11) % 12;
2595
- let score = 0;
2596
- for (const n of melody) {
2597
- const w = Math.max(0.25, n.durationBeat);
2598
- const pc = pitchClass5(n.pitch);
2599
- if (isScaleTone(n.pitch, key) || isMinor && pc === leadingTone) {
2600
- score += w;
2601
- }
2602
- if (pc === tonic) {
2603
- score += 0.5 * w;
2604
- }
2923
+ for (let i = 0; i < tones.length; i += 1) {
2924
+ const degreeNumber2 = i + 1;
2925
+ if (degreeNumber2 === 1 || degreeNumber2 === 5) {
2926
+ continue;
2605
2927
  }
2606
- if (score > bestScore) {
2607
- bestScore = score;
2608
- best = key;
2928
+ if (mod12((tones[i] ?? 0) - 1) === mod12(rootPc)) {
2929
+ return { degreeNumber: degreeNumber2, accidental: "b" };
2609
2930
  }
2610
2931
  }
2611
- return best;
2932
+ for (let i = 0; i < tones.length; i += 1) {
2933
+ if (mod12((tones[i] ?? 0) + 1) === mod12(rootPc)) {
2934
+ return { degreeNumber: i + 1, accidental: "#" };
2935
+ }
2936
+ }
2937
+ const [degreeNumber, accidental] = OFFSET_SPELLING[mod12(rootPc - key.rootPc)] ?? [1, ""];
2938
+ return { degreeNumber, accidental };
2612
2939
  }
2613
- function buildCandidates(key, reharmonize) {
2614
- const tones = scaleTonesInDegreeOrder(key);
2615
- const candidates = tones.map((rootPc, degree) => ({
2616
- rootPc,
2617
- quality: diatonicTriad(degree, key).quality,
2618
- degree,
2619
- secondaryDominant: false,
2620
- base: 0
2621
- }));
2622
- const tonic = tones[0] ?? 0;
2623
- if (reharmonize !== "diatonic") {
2624
- for (const target of [1, 3, 4, 5]) {
2625
- const targetRoot = tones[target] ?? 0;
2626
- candidates.push({
2627
- rootPc: (targetRoot + 7) % 12,
2940
+ function baseMarker(quality) {
2941
+ if (quality === "dim" || quality === "dim7") {
2942
+ return "o";
2943
+ }
2944
+ if (quality === "m7b5") {
2945
+ return "\xF8";
2946
+ }
2947
+ if (quality === "aug" || quality === "aug7") {
2948
+ return "+";
2949
+ }
2950
+ if (quality === "augMaj7") {
2951
+ return "+maj7";
2952
+ }
2953
+ if (quality === "maj7" || quality === "minMaj7") {
2954
+ return "maj7";
2955
+ }
2956
+ return "";
2957
+ }
2958
+ var TRIAD_FIGURES = { 1: "6", 2: "64" };
2959
+ var SEVENTH_FIGURES = { 1: "65", 2: "43", 3: "42" };
2960
+ var TRIAD_FIGURE_QUALITIES = /* @__PURE__ */ new Set(["maj", "min", "dim", "aug"]);
2961
+ var SEVENTH_FIGURE_QUALITIES = /* @__PURE__ */ new Set([
2962
+ "maj7",
2963
+ "min7",
2964
+ "dom7",
2965
+ "dim7",
2966
+ "m7b5",
2967
+ "minMaj7",
2968
+ "aug7",
2969
+ "augMaj7"
2970
+ ]);
2971
+ function chordToRoman(chord, key) {
2972
+ const { degreeNumber, accidental } = romanSpelling(chord.rootPc, key);
2973
+ const { lower, suffix } = romanStyle(chord.quality);
2974
+ const numeral = ROMAN[degreeNumber - 1] ?? "I";
2975
+ const cased = lower ? numeral.toLowerCase() : numeral;
2976
+ let inversion = 0;
2977
+ if (chord.bassPc !== void 0) {
2978
+ const idx = chord.intervals.findIndex((iv) => mod12(chord.rootPc + iv) === chord.bassPc);
2979
+ if (idx > 0) {
2980
+ inversion = idx;
2981
+ }
2982
+ }
2983
+ if (inversion > 0) {
2984
+ const figure = SEVENTH_FIGURE_QUALITIES.has(chord.quality) ? SEVENTH_FIGURES[inversion] : TRIAD_FIGURE_QUALITIES.has(chord.quality) ? TRIAD_FIGURES[inversion] : void 0;
2985
+ if (figure !== void 0) {
2986
+ return `${accidental}${cased}${baseMarker(chord.quality)}${figure}`;
2987
+ }
2988
+ }
2989
+ return `${accidental}${cased}${suffix}`;
2990
+ }
2991
+
2992
+ // src/analyze/functional/function.ts
2993
+ var FUNCTION_BY_OFFSET = [
2994
+ "tonic",
2995
+ // 0 I
2996
+ "subdominant",
2997
+ // 1 bII (Neapolitan)
2998
+ "subdominant",
2999
+ // 2 ii
3000
+ "tonic",
3001
+ // 3 bIII
3002
+ "tonic",
3003
+ // 4 iii
3004
+ "subdominant",
3005
+ // 5 IV
3006
+ "dominant",
3007
+ // 6 #IV / bV
3008
+ "dominant",
3009
+ // 7 V
3010
+ "subdominant",
3011
+ // 8 bVI
3012
+ "tonic",
3013
+ // 9 vi
3014
+ "subdominant",
3015
+ // 10 bVII
3016
+ "dominant"
3017
+ // 11 vii
3018
+ ];
3019
+ function isMinorKey(key) {
3020
+ const hasMinorThird = key.modeMask12 >> 3 & 1;
3021
+ const hasMajorThird = key.modeMask12 >> 4 & 1;
3022
+ return Boolean(hasMinorThird) && !hasMajorThird;
3023
+ }
3024
+ function functionOf(chord, key) {
3025
+ return FUNCTION_BY_OFFSET[mod12(chord.rootPc - key.rootPc)] ?? "tonic";
3026
+ }
3027
+ function isDiatonic(chord, key) {
3028
+ return chordPitchClasses(chord).every((pc) => isScaleTone(pc, key));
3029
+ }
3030
+ function parallelKey(key) {
3031
+ return {
3032
+ rootPc: mod12(key.rootPc),
3033
+ modeMask12: isMinorKey(key) ? MAJOR_MASK : NATURAL_MINOR_MASK
3034
+ };
3035
+ }
3036
+
3037
+ // src/analyze/functional/borrowed.ts
3038
+ function isHarmonicMinorAlteration(chord, key) {
3039
+ return isMinorKey(key) && isDiatonic(chord, { rootPc: mod12(key.rootPc), modeMask12: HARMONIC_MINOR_MASK });
3040
+ }
3041
+ function isBorrowedChord(chord, key) {
3042
+ if (isDiatonic(chord, key)) {
3043
+ return false;
3044
+ }
3045
+ if (isHarmonicMinorAlteration(chord, key)) {
3046
+ return false;
3047
+ }
3048
+ return isDiatonic(chord, parallelKey(key));
3049
+ }
3050
+ function borrowedSource(chord, key) {
3051
+ if (isDiatonic(chord, key)) {
3052
+ return null;
3053
+ }
3054
+ if (isNeapolitan(chord, key)) {
3055
+ return "neapolitan";
3056
+ }
3057
+ if (isBorrowedChord(chord, key)) {
3058
+ return isMinorKey(key) ? "parallel-major" : "parallel-minor";
3059
+ }
3060
+ return null;
3061
+ }
3062
+
3063
+ // src/theory/harmony/index.ts
3064
+ function roleOf(pitch, chord, chordId = 0) {
3065
+ const interval = ((Math.trunc(pitch) - chord.rootPc) % 12 + 12) % 12;
3066
+ const tones = new Set(chord.intervals.map((i) => (i % 12 + 12) % 12));
3067
+ const hasThird = tones.has(3) || tones.has(4);
3068
+ const isSuspendedTone = !hasThird && (interval === 5 && tones.has(5) || interval === 2 && tones.has(2));
3069
+ const chordRole = chordToneRole(pitch, chord);
3070
+ let role;
3071
+ let lock;
3072
+ if (isSuspendedTone) {
3073
+ role = "third";
3074
+ lock = "quality";
3075
+ } else if (chordRole !== null) {
3076
+ role = chordRole;
3077
+ lock = chordRole === "root" ? "identity" : chordRole === "third" ? "quality" : "voicing";
3078
+ } else {
3079
+ role = "tension";
3080
+ lock = "voicing";
3081
+ }
3082
+ return { role, lock, belongsToChordId: chordId };
3083
+ }
3084
+
3085
+ // src/generate/harmonize/index.ts
3086
+ var FALLBACK = { rootPc: 0, quality: "maj", secondaryDominant: false, base: 0 };
3087
+ var DEFAULT_METER = { numerator: 4, denominator: 4 };
3088
+ var COMFORT_LOW = 55;
3089
+ var COMFORT_HIGH = 79;
3090
+ var TESSITURA_WEIGHT = 1e-3;
3091
+ var TIE_BREAK_JITTER = 1e-6;
3092
+ function pitchClass5(pitch) {
3093
+ return (Math.trunc(pitch) % 12 + 12) % 12;
3094
+ }
3095
+ function inferKey(melody) {
3096
+ let best = majorKey(0);
3097
+ let bestScore = Number.NEGATIVE_INFINITY;
3098
+ const candidates = [];
3099
+ for (let tonic = 0; tonic < 12; tonic += 1) {
3100
+ candidates.push(majorKey(tonic), minorKey(tonic));
3101
+ }
3102
+ for (const key of candidates) {
3103
+ const tonic = pitchClass5(key.rootPc);
3104
+ const isMinor = key.modeMask12 === NATURAL_MINOR_MASK;
3105
+ const leadingTone = (tonic + 11) % 12;
3106
+ let score = 0;
3107
+ for (const n of melody) {
3108
+ const w = Math.max(0.25, n.durationBeat);
3109
+ const pc = pitchClass5(n.pitch);
3110
+ if (isScaleTone(n.pitch, key) || isMinor && pc === leadingTone) {
3111
+ score += w;
3112
+ }
3113
+ if (pc === tonic) {
3114
+ score += 0.5 * w;
3115
+ }
3116
+ }
3117
+ if (score > bestScore) {
3118
+ bestScore = score;
3119
+ best = key;
3120
+ }
3121
+ }
3122
+ return best;
3123
+ }
3124
+ function buildCandidates(key, reharmonize) {
3125
+ const tones = scaleTonesInDegreeOrder(key);
3126
+ const candidates = tones.map((rootPc, degree) => ({
3127
+ rootPc,
3128
+ quality: diatonicTriad(degree, key).quality,
3129
+ degree,
3130
+ secondaryDominant: false,
3131
+ base: 0
3132
+ }));
3133
+ if (reharmonize !== "diatonic") {
3134
+ for (const target of [1, 3, 4, 5]) {
3135
+ const targetRoot = tones[target] ?? 0;
3136
+ candidates.push({
3137
+ rootPc: (targetRoot + 7) % 12,
2628
3138
  quality: "dom7",
2629
3139
  secondaryDominant: true,
2630
3140
  targetDegree: target,
@@ -2633,26 +3143,29 @@ function buildCandidates(key, reharmonize) {
2633
3143
  }
2634
3144
  }
2635
3145
  if (reharmonize === "borrowed") {
2636
- candidates.push({
2637
- rootPc: (tonic + 10) % 12,
2638
- quality: "maj",
2639
- secondaryDominant: false,
2640
- base: 2
2641
- });
2642
- candidates.push({
2643
- rootPc: (tonic + 8) % 12,
2644
- quality: "maj",
2645
- secondaryDominant: false,
2646
- base: 2
2647
- });
2648
- candidates.push({
2649
- rootPc: (tonic + 5) % 12,
2650
- quality: "min",
2651
- secondaryDominant: false,
2652
- base: 2
2653
- });
3146
+ const parallel = parallelKey(key);
3147
+ const parallelTones = scaleTonesInDegreeOrder(parallel);
3148
+ for (let degree = 0; degree < parallelTones.length; degree += 1) {
3149
+ const chord = diatonicTriad(degree, parallel);
3150
+ if (!isDiatonic(chord, key)) {
3151
+ candidates.push({
3152
+ rootPc: chord.rootPc,
3153
+ quality: chord.quality,
3154
+ secondaryDominant: false,
3155
+ base: 1.25
3156
+ });
3157
+ }
3158
+ }
2654
3159
  }
2655
- return candidates;
3160
+ const seen = /* @__PURE__ */ new Set();
3161
+ return candidates.filter((candidate) => {
3162
+ const id = `${candidate.rootPc}:${candidate.quality}`;
3163
+ if (seen.has(id)) {
3164
+ return false;
3165
+ }
3166
+ seen.add(id);
3167
+ return true;
3168
+ });
2656
3169
  }
2657
3170
  function emissionCost(seg, cand, melody, key) {
2658
3171
  const pcs = chordPitchClasses(makeChord(cand.rootPc, cand.quality));
@@ -2756,20 +3269,37 @@ function harmonizeOnce(melody, key, candidates, segments, jitter) {
2756
3269
  return { cost: bestCost, path };
2757
3270
  }
2758
3271
  function harmonizeMelody(opts) {
3272
+ const noteIndex = createNoteEventIndex(opts.melody);
2759
3273
  const key = opts.key === "infer" ? inferKey(opts.melody) : opts.key;
2760
3274
  const candidates = buildCandidates(key, opts.reharmonize);
2761
3275
  const candAt = (i) => candidates[i] ?? FALLBACK;
2762
- const rng = mulberry32(opts.seed ?? 0);
2763
- const jitter = candidates.map(() => rng() * TIE_BREAK_JITTER);
2764
- const melodyEnd = opts.melody.reduce((m, n) => Math.max(m, n.startBeat + n.durationBeat), 0);
2765
- const hr = Math.max(0.25, opts.harmonicRhythm);
3276
+ const rng = createRng(opts.seed ?? 0);
3277
+ const jitter = candidates.map(() => rng.next() * TIE_BREAK_JITTER);
3278
+ const melodyEnd = noteIndex.notes.reduce((m, n) => Math.max(m, n.endBeat), 0);
3279
+ const requestedHr = assertRange(
3280
+ opts.harmonicRhythm,
3281
+ Number.MIN_VALUE,
3282
+ Number.MAX_SAFE_INTEGER,
3283
+ "harmonic rhythm"
3284
+ );
3285
+ const hr = Math.max(0.25, requestedHr);
2766
3286
  const segCount = Math.max(1, Math.ceil(melodyEnd / hr));
2767
- const segments = [];
2768
- for (let s = 0; s < segCount; s += 1) {
2769
- const startBeat = s * hr;
2770
- const endBeat = startBeat + hr;
2771
- const noteIndices = opts.melody.map((n, i) => ({ n, i })).filter(({ n }) => n.startBeat < endBeat && n.startBeat + n.durationBeat > startBeat).map(({ i }) => i);
2772
- segments.push({ startBeat, endBeat, noteIndices });
3287
+ assertGenerationBudget(segCount, "harmonic segments");
3288
+ assertGenerationBudget(segCount * candidates.length * candidates.length, "harmonization search");
3289
+ const segments = Array.from({ length: segCount }, (_, s) => ({
3290
+ startBeat: s * hr,
3291
+ endBeat: (s + 1) * hr,
3292
+ noteIndices: []
3293
+ }));
3294
+ let memberships = 0;
3295
+ for (const indexed of noteIndex.notes) {
3296
+ const first = Math.max(0, Math.floor(indexed.note.startBeat / hr));
3297
+ const lastExclusive = Math.min(segCount, Math.ceil(indexed.endBeat / hr - Number.EPSILON));
3298
+ memberships += Math.max(0, lastExclusive - first);
3299
+ assertGenerationBudget(memberships, "note-to-segment memberships");
3300
+ for (let segment = first; segment < lastExclusive; segment += 1) {
3301
+ segments[segment]?.noteIndices.push(indexed.originalIndex);
3302
+ }
2773
3303
  }
2774
3304
  const transposes = [0];
2775
3305
  if (opts.placement.transposeSearch) {
@@ -2816,11 +3346,11 @@ function harmonizeMelody(opts) {
2816
3346
  }
2817
3347
  return chord;
2818
3348
  });
2819
- const melodyRoles = bestMelody.map((note, noteIndex) => {
3349
+ const melodyRoles = bestMelody.map((note, noteIndex2) => {
2820
3350
  const segIdx = Math.min(segments.length - 1, Math.max(0, Math.floor(note.startBeat / hr)));
2821
3351
  const cand = candAt(bestPath[segIdx] ?? 0);
2822
3352
  const chord = makeChord(cand.rootPc, cand.quality);
2823
- return { noteIndex, role: roleOf(note.pitch, chord).role };
3353
+ return { noteIndex: noteIndex2, role: roleOf(note.pitch, chord).role };
2824
3354
  });
2825
3355
  return { transposeSemitones: bestTs, key, chords, melodyRoles };
2826
3356
  }
@@ -2867,29 +3397,30 @@ function nearestChordTone(pitch, chord) {
2867
3397
  return pitch;
2868
3398
  }
2869
3399
  function cellOrigin(notes) {
2870
- return notes.length === 0 ? 0 : Math.min(...notes.map((n) => n.startBeat));
3400
+ let origin = Number.POSITIVE_INFINITY;
3401
+ for (const note of notes) {
3402
+ origin = Math.min(origin, note.startBeat);
3403
+ }
3404
+ return Number.isFinite(origin) ? origin : 0;
3405
+ }
3406
+ function cellEnd(notes) {
3407
+ let end = Number.NEGATIVE_INFINITY;
3408
+ for (const note of notes) {
3409
+ end = Math.max(end, note.startBeat + note.durationBeat);
3410
+ }
3411
+ return Number.isFinite(end) ? end : 0;
2871
3412
  }
2872
3413
  function cellSpan(cell) {
2873
3414
  if (cell.notes.length === 0) {
2874
3415
  return 0;
2875
3416
  }
2876
3417
  const start = cellOrigin(cell.notes);
2877
- const end = Math.max(...cell.notes.map((n) => n.startBeat + n.durationBeat));
3418
+ const end = cellEnd(cell.notes);
2878
3419
  return end - start;
2879
3420
  }
2880
3421
  function clone(cell) {
2881
3422
  return { notes: cell.notes.map((n) => ({ ...n })) };
2882
3423
  }
2883
- function mulberry322(seed) {
2884
- let state = seed >>> 0;
2885
- return () => {
2886
- state = state + 1831565813 >>> 0;
2887
- let t = state;
2888
- t = Math.imul(t ^ t >>> 15, t | 1);
2889
- t ^= t + Math.imul(t ^ t >>> 7, t | 61);
2890
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
2891
- };
2892
- }
2893
3424
  function contourOffsets(contour, count) {
2894
3425
  const offsets = [];
2895
3426
  for (let i = 0; i < count; i += 1) {
@@ -2912,19 +3443,21 @@ function contourOffsets(contour, count) {
2912
3443
  }
2913
3444
  function generateMotif(opts) {
2914
3445
  const contour = opts.contour ?? "arch";
2915
- const bars = Math.max(1, Math.trunc(opts.bars));
3446
+ const bars = assertPositiveInt(opts.bars, "motif bars");
2916
3447
  const totalBeats = bars * 4;
2917
3448
  const noteCount = Math.max(3, bars * 2);
3449
+ assertGenerationBudget(noteCount, "motif notes");
2918
3450
  const beatsPerNote = totalBeats / noteCount;
2919
- const rng = mulberry322(opts.seed ?? 0);
2920
- const jitterProb = Math.min(1, Math.max(0, opts.jitter ?? 0));
3451
+ const rng = createRng(opts.seed ?? 0);
3452
+ const requestedJitter = assertFiniteNumber(opts.jitter ?? 0, "motif jitter");
3453
+ const jitterProb = Math.min(1, Math.max(0, requestedJitter));
2921
3454
  const tonic = pitchClass6(opts.key.rootPc) + 60;
2922
3455
  const offsets = contourOffsets(contour, noteCount);
2923
3456
  const notes = [];
2924
3457
  for (let i = 0; i < noteCount; i += 1) {
2925
3458
  let jitter = 0;
2926
- if (jitterProb > 0 && rng() < jitterProb) {
2927
- jitter = rng() < 0.5 ? 1 : -1;
3459
+ if (jitterProb > 0 && rng.next() < jitterProb) {
3460
+ jitter = rng.next() < 0.5 ? 1 : -1;
2928
3461
  }
2929
3462
  let pitch = stepDiatonic(tonic, (offsets[i] ?? 0) + jitter, opts.key);
2930
3463
  const startBeat = i * beatsPerNote;
@@ -2936,6 +3469,16 @@ function generateMotif(opts) {
2936
3469
  return { notes };
2937
3470
  }
2938
3471
  function transformMotif(cell, t, amount, key) {
3472
+ assertNoteEvents(cell.notes, "motif notes");
3473
+ if (amount !== void 0) {
3474
+ assertFiniteNumber(amount, "motif transform amount");
3475
+ }
3476
+ if ((t === "transposeDiatonic" || t === "sequence") && amount !== void 0) {
3477
+ assertInteger(amount, "diatonic transform amount");
3478
+ }
3479
+ if ((t === "augment" || t === "diminish") && amount !== void 0 && amount <= 0) {
3480
+ throw new RangeError("time transform amount must be positive");
3481
+ }
2939
3482
  const notes = cell.notes;
2940
3483
  switch (t) {
2941
3484
  case "transposeChromatic": {
@@ -2962,7 +3505,7 @@ function transformMotif(cell, t, amount, key) {
2962
3505
  return { notes: [] };
2963
3506
  }
2964
3507
  const start = cellOrigin(notes);
2965
- const end = Math.max(...notes.map((n) => n.startBeat + n.durationBeat));
3508
+ const end = cellEnd(notes);
2966
3509
  return {
2967
3510
  notes: notes.map((n) => ({
2968
3511
  pitch: n.pitch,
@@ -2995,8 +3538,10 @@ function scaleTime(cell, factor) {
2995
3538
  };
2996
3539
  }
2997
3540
  function developMotif(cell, timeline, key, bars) {
3541
+ assertPositiveInt(bars, "development bars");
3542
+ assertNoteEvents(cell.notes, "motif notes");
2998
3543
  const span = cellSpan(cell);
2999
- const totalBeats = Math.max(1, Math.trunc(bars)) * 4;
3544
+ const totalBeats = bars * 4;
3000
3545
  const origin = cellOrigin(cell.notes);
3001
3546
  const out = [];
3002
3547
  if (span <= 0) {
@@ -3004,6 +3549,7 @@ function developMotif(cell, timeline, key, bars) {
3004
3549
  }
3005
3550
  const tileSpan = Math.max(span, MIN_TILE_SPAN);
3006
3551
  const tileCount = Math.ceil(totalBeats / tileSpan);
3552
+ assertGenerationBudget(tileCount * cell.notes.length, "developed motif notes");
3007
3553
  for (let k = 0; k < tileCount; k += 1) {
3008
3554
  const offset = k * tileSpan;
3009
3555
  for (const n of cell.notes) {
@@ -3202,16 +3748,6 @@ function progressions() {
3202
3748
  function progressionsByStyle(style) {
3203
3749
  return progressions().filter((p) => p.styles.includes(style));
3204
3750
  }
3205
- function mulberry323(seed) {
3206
- let state = seed >>> 0;
3207
- return () => {
3208
- state = state + 1831565813 >>> 0;
3209
- let t = state;
3210
- t = Math.imul(t ^ t >>> 15, t | 1);
3211
- t ^= t + Math.imul(t ^ t >>> 7, t | 61);
3212
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
3213
- };
3214
- }
3215
3751
  function degreeToRootPc(degree, key) {
3216
3752
  if (degree >= 0 && degree <= 6) {
3217
3753
  const tones = scaleTonesInDegreeOrder(key);
@@ -3233,6 +3769,8 @@ function autoQuality(degree, key) {
3233
3769
  return "maj";
3234
3770
  }
3235
3771
  function generateProgression(opts) {
3772
+ assertPositiveInt(opts.bars, "progression bars");
3773
+ assertGenerationBudget(opts.bars, "progression chords");
3236
3774
  const seed = opts.seed ?? 0;
3237
3775
  let preset;
3238
3776
  if (opts.presetId !== void 0) {
@@ -3244,8 +3782,8 @@ function generateProgression(opts) {
3244
3782
  if (preset === void 0) {
3245
3783
  const pool = PRESETS.filter((p) => p.styles.includes(opts.style));
3246
3784
  const candidates = pool.length > 0 ? pool : PRESETS;
3247
- const rng = mulberry323(seed);
3248
- const index = Math.floor(rng() * candidates.length) % candidates.length;
3785
+ const rng = createRng(seed);
3786
+ const index = Math.floor(rng.next() * candidates.length) % candidates.length;
3249
3787
  preset = candidates[index] ?? PRESETS[0];
3250
3788
  }
3251
3789
  const degrees = preset?.degrees ?? [0];
@@ -3263,7 +3801,7 @@ function generateProgression(opts) {
3263
3801
  }
3264
3802
  if (opts.reharmonize) {
3265
3803
  const tonicPc = (opts.key.rootPc % 12 + 12) % 12;
3266
- const rrng = mulberry323((seed ^ 2654435769) >>> 0);
3804
+ const rrng = createRng((seed ^ 2654435769) >>> 0);
3267
3805
  for (let i = 0; i < chords.length - 1; i += 1) {
3268
3806
  const cur = chords[i];
3269
3807
  const next = chords[i + 1];
@@ -3277,7 +3815,7 @@ function generateProgression(opts) {
3277
3815
  if (targetsTonic || alreadySecondary || isResolutionTarget) {
3278
3816
  continue;
3279
3817
  }
3280
- if (rrng() < 0.5) {
3818
+ if (rrng.next() < 0.5) {
3281
3819
  chords[i] = {
3282
3820
  rootPc: domRoot,
3283
3821
  quality: "dom7",
@@ -3297,12 +3835,21 @@ function pitchClass7(value) {
3297
3835
  function uniquePitchClasses(pitches) {
3298
3836
  return [...new Set(pitches.map(pitchClass7))].sort((a, b) => a - b);
3299
3837
  }
3300
- function detectChord(pitches) {
3838
+ function assertPitches(pitches) {
3839
+ assertGenerationBudget(pitches.length, "detection pitches");
3840
+ for (let index = 0; index < pitches.length; index += 1) {
3841
+ assertFiniteNumber(pitches[index] ?? Number.NaN, `pitches[${index}]`);
3842
+ }
3843
+ }
3844
+ function detectChord(pitches, opts = {}) {
3845
+ assertPitches(pitches);
3846
+ if (opts.bassPc !== void 0) assertFiniteNumber(opts.bassPc, "bassPc");
3301
3847
  const input = uniquePitchClasses(pitches);
3302
3848
  if (input.length === 0) {
3303
3849
  return [];
3304
3850
  }
3305
- const bassPc = pitchClass7(Math.min(...pitches));
3851
+ const inputKind = opts.input === void 0 || opts.input === "auto" ? pitches.every((pitch) => Number.isInteger(pitch) && pitch >= 0 && pitch <= 11) ? "pitchClass" : "midi" : opts.input;
3852
+ const bassPc = opts.bassPc !== void 0 ? pitchClass7(opts.bassPc) : inputKind === "midi" ? pitchClass7(pitches.reduce((lowest, pitch) => Math.min(lowest, pitch), Infinity)) : void 0;
3306
3853
  const inputSet = new Set(input);
3307
3854
  const matches = [];
3308
3855
  const qualities = chordQualities();
@@ -3317,8 +3864,8 @@ function detectChord(pitches) {
3317
3864
  continue;
3318
3865
  }
3319
3866
  const extraPcs = input.filter((pc) => !toneSet.has(pc));
3320
- const bassIndex = chord.intervals.findIndex((iv) => pitchClass7(rootPc + iv) === bassPc);
3321
- const inversion = bassIndex > 0 ? bassIndex : 0;
3867
+ const bassIndex = bassPc === void 0 ? -1 : chord.intervals.findIndex((iv) => pitchClass7(rootPc + iv) === bassPc);
3868
+ const inversion = bassPc === void 0 ? null : bassIndex > 0 ? bassIndex : 0;
3322
3869
  const match = {
3323
3870
  rootPc,
3324
3871
  quality,
@@ -3327,7 +3874,7 @@ function detectChord(pitches) {
3327
3874
  exact: extraPcs.length === 0 && missingPcs.length === 0,
3328
3875
  inversion
3329
3876
  };
3330
- if (inversion > 0) {
3877
+ if (bassPc !== void 0 && bassPc !== rootPc) {
3331
3878
  match.bassPc = bassPc;
3332
3879
  }
3333
3880
  matches.push(match);
@@ -3349,224 +3896,14 @@ function detectChord(pitches) {
3349
3896
  });
3350
3897
  return matches;
3351
3898
  }
3352
- function detectChordBest(pitches) {
3353
- const best = detectChord(pitches)[0];
3899
+ function detectChordBest(pitches, opts = {}) {
3900
+ const best = detectChord(pitches, opts)[0];
3354
3901
  if (!best) {
3355
3902
  return null;
3356
3903
  }
3357
3904
  return makeChord(best.rootPc, best.quality, best.bassPc);
3358
3905
  }
3359
3906
 
3360
- // src/analyze/functional/internal.ts
3361
- function mod12(n) {
3362
- return (n % 12 + 12) % 12;
3363
- }
3364
- function isNeapolitan(chord, key) {
3365
- return mod12(chord.rootPc - key.rootPc) === 1 && chord.quality === "maj";
3366
- }
3367
-
3368
- // src/analyze/functional/roman.ts
3369
- var ROMAN = ["I", "II", "III", "IV", "V", "VI", "VII"];
3370
- var OFFSET_SPELLING = [
3371
- [1, ""],
3372
- [2, "b"],
3373
- [2, ""],
3374
- [3, "b"],
3375
- [3, ""],
3376
- [4, ""],
3377
- [4, "#"],
3378
- [5, ""],
3379
- [6, "b"],
3380
- [6, ""],
3381
- [7, "b"],
3382
- [7, ""]
3383
- ];
3384
- var ROMAN_STYLE = {
3385
- maj: { lower: false, suffix: "" },
3386
- min: { lower: true, suffix: "" },
3387
- dim: { lower: true, suffix: "o" },
3388
- aug: { lower: false, suffix: "+" },
3389
- maj7: { lower: false, suffix: "maj7" },
3390
- min7: { lower: true, suffix: "7" },
3391
- dom7: { lower: false, suffix: "7" },
3392
- dim7: { lower: true, suffix: "o7" },
3393
- m7b5: { lower: true, suffix: "\xF87" },
3394
- minMaj7: { lower: true, suffix: "maj7" },
3395
- aug7: { lower: false, suffix: "+7" },
3396
- augMaj7: { lower: false, suffix: "+maj7" },
3397
- majb5: { lower: false, suffix: "b5" },
3398
- "6": { lower: false, suffix: "add6" },
3399
- min6: { lower: true, suffix: "add6" },
3400
- "6/9": { lower: false, suffix: "69" },
3401
- sus2: { lower: false, suffix: "sus2" },
3402
- sus4: { lower: false, suffix: "sus4" },
3403
- add9: { lower: false, suffix: "add9" },
3404
- add11: { lower: false, suffix: "add11" },
3405
- maj9: { lower: false, suffix: "maj9" },
3406
- min9: { lower: true, suffix: "9" },
3407
- dom9: { lower: false, suffix: "9" },
3408
- "7b9": { lower: false, suffix: "7b9" },
3409
- "7#9": { lower: false, suffix: "7#9" },
3410
- "7#11": { lower: false, suffix: "7#11" },
3411
- "7b13": { lower: false, suffix: "7b13" },
3412
- "11": { lower: false, suffix: "11" },
3413
- "13": { lower: false, suffix: "13" },
3414
- "5": { lower: false, suffix: "5" }
3415
- };
3416
- var SUFFIX_QUALITY = new Map(
3417
- Object.entries(ROMAN_STYLE).map(
3418
- ([quality, style]) => [`${style.lower ? "l" : "u"}:${style.suffix}`, quality]
3419
- )
3420
- );
3421
- function romanStyle(quality) {
3422
- return ROMAN_STYLE[quality];
3423
- }
3424
- function romanSpelling(rootPc, key) {
3425
- const tones = scaleTonesInDegreeOrder(key);
3426
- const diatonic = tones.indexOf(mod12(rootPc));
3427
- if (diatonic >= 0) {
3428
- return { degreeNumber: diatonic + 1, accidental: "" };
3429
- }
3430
- for (let i = 0; i < tones.length; i += 1) {
3431
- const degreeNumber2 = i + 1;
3432
- if (degreeNumber2 === 1 || degreeNumber2 === 5) {
3433
- continue;
3434
- }
3435
- if (mod12((tones[i] ?? 0) - 1) === mod12(rootPc)) {
3436
- return { degreeNumber: degreeNumber2, accidental: "b" };
3437
- }
3438
- }
3439
- for (let i = 0; i < tones.length; i += 1) {
3440
- if (mod12((tones[i] ?? 0) + 1) === mod12(rootPc)) {
3441
- return { degreeNumber: i + 1, accidental: "#" };
3442
- }
3443
- }
3444
- const [degreeNumber, accidental] = OFFSET_SPELLING[mod12(rootPc - key.rootPc)] ?? [1, ""];
3445
- return { degreeNumber, accidental };
3446
- }
3447
- function baseMarker(quality) {
3448
- if (quality === "dim" || quality === "dim7") {
3449
- return "o";
3450
- }
3451
- if (quality === "m7b5") {
3452
- return "\xF8";
3453
- }
3454
- if (quality === "aug" || quality === "aug7") {
3455
- return "+";
3456
- }
3457
- if (quality === "augMaj7") {
3458
- return "+maj7";
3459
- }
3460
- if (quality === "maj7" || quality === "minMaj7") {
3461
- return "maj7";
3462
- }
3463
- return "";
3464
- }
3465
- var TRIAD_FIGURES = { 1: "6", 2: "64" };
3466
- var SEVENTH_FIGURES = { 1: "65", 2: "43", 3: "42" };
3467
- var TRIAD_FIGURE_QUALITIES = /* @__PURE__ */ new Set(["maj", "min", "dim", "aug"]);
3468
- var SEVENTH_FIGURE_QUALITIES = /* @__PURE__ */ new Set([
3469
- "maj7",
3470
- "min7",
3471
- "dom7",
3472
- "dim7",
3473
- "m7b5",
3474
- "minMaj7",
3475
- "aug7",
3476
- "augMaj7"
3477
- ]);
3478
- function chordToRoman(chord, key) {
3479
- const { degreeNumber, accidental } = romanSpelling(chord.rootPc, key);
3480
- const { lower, suffix } = romanStyle(chord.quality);
3481
- const numeral = ROMAN[degreeNumber - 1] ?? "I";
3482
- const cased = lower ? numeral.toLowerCase() : numeral;
3483
- let inversion = 0;
3484
- if (chord.bassPc !== void 0) {
3485
- const idx = chord.intervals.findIndex((iv) => mod12(chord.rootPc + iv) === chord.bassPc);
3486
- if (idx > 0) {
3487
- inversion = idx;
3488
- }
3489
- }
3490
- if (inversion > 0) {
3491
- const figure = SEVENTH_FIGURE_QUALITIES.has(chord.quality) ? SEVENTH_FIGURES[inversion] : TRIAD_FIGURE_QUALITIES.has(chord.quality) ? TRIAD_FIGURES[inversion] : void 0;
3492
- if (figure !== void 0) {
3493
- return `${accidental}${cased}${baseMarker(chord.quality)}${figure}`;
3494
- }
3495
- }
3496
- return `${accidental}${cased}${suffix}`;
3497
- }
3498
-
3499
- // src/analyze/functional/function.ts
3500
- var FUNCTION_BY_OFFSET = [
3501
- "tonic",
3502
- // 0 I
3503
- "subdominant",
3504
- // 1 bII (Neapolitan)
3505
- "subdominant",
3506
- // 2 ii
3507
- "tonic",
3508
- // 3 bIII
3509
- "tonic",
3510
- // 4 iii
3511
- "subdominant",
3512
- // 5 IV
3513
- "dominant",
3514
- // 6 #IV / bV
3515
- "dominant",
3516
- // 7 V
3517
- "subdominant",
3518
- // 8 bVI
3519
- "tonic",
3520
- // 9 vi
3521
- "subdominant",
3522
- // 10 bVII
3523
- "dominant"
3524
- // 11 vii
3525
- ];
3526
- function isMinorKey(key) {
3527
- const hasMinorThird = key.modeMask12 >> 3 & 1;
3528
- const hasMajorThird = key.modeMask12 >> 4 & 1;
3529
- return Boolean(hasMinorThird) && !hasMajorThird;
3530
- }
3531
- function functionOf(chord, key) {
3532
- return FUNCTION_BY_OFFSET[mod12(chord.rootPc - key.rootPc)] ?? "tonic";
3533
- }
3534
- function isDiatonic(chord, key) {
3535
- return chordPitchClasses(chord).every((pc) => isScaleTone(pc, key));
3536
- }
3537
- function parallelKey(key) {
3538
- return {
3539
- rootPc: mod12(key.rootPc),
3540
- modeMask12: isMinorKey(key) ? MAJOR_MASK : NATURAL_MINOR_MASK
3541
- };
3542
- }
3543
-
3544
- // src/analyze/functional/borrowed.ts
3545
- function isHarmonicMinorAlteration(chord, key) {
3546
- return isMinorKey(key) && isDiatonic(chord, { rootPc: mod12(key.rootPc), modeMask12: HARMONIC_MINOR_MASK });
3547
- }
3548
- function isBorrowedChord(chord, key) {
3549
- if (isDiatonic(chord, key)) {
3550
- return false;
3551
- }
3552
- if (isHarmonicMinorAlteration(chord, key)) {
3553
- return false;
3554
- }
3555
- return isDiatonic(chord, parallelKey(key));
3556
- }
3557
- function borrowedSource(chord, key) {
3558
- if (isDiatonic(chord, key)) {
3559
- return null;
3560
- }
3561
- if (isNeapolitan(chord, key)) {
3562
- return "neapolitan";
3563
- }
3564
- if (isBorrowedChord(chord, key)) {
3565
- return isMinorKey(key) ? "parallel-major" : "parallel-minor";
3566
- }
3567
- return null;
3568
- }
3569
-
3570
3907
  // src/generate/reharmony/index.ts
3571
3908
  function mod122(n) {
3572
3909
  return (n % 12 + 12) % 12;
@@ -3582,11 +3919,6 @@ function isStableTarget(chord) {
3582
3919
  const hasThird = hasInterval(chord, 3) || hasInterval(chord, 4);
3583
3920
  return hasFifth && hasThird && !isDominantType(chord);
3584
3921
  }
3585
- function triadPitchClasses(chord) {
3586
- const third = hasInterval(chord, 4) ? 4 : hasInterval(chord, 3) ? 3 : 4;
3587
- const fifth = hasInterval(chord, 8) ? 8 : hasInterval(chord, 6) ? 6 : 7;
3588
- return [mod122(chord.rootPc), mod122(chord.rootPc + third), mod122(chord.rootPc + fifth)];
3589
- }
3590
3922
  function commonToneCount(a, b) {
3591
3923
  const set = new Set(b);
3592
3924
  return a.filter((pc) => set.has(pc)).length;
@@ -3604,7 +3936,7 @@ function sameChord(a, b) {
3604
3936
  }
3605
3937
  var THIRD_OFFSETS = [3, 4, 8, 9];
3606
3938
  function substituteChord(chord, key, opts) {
3607
- const original = triadPitchClasses(chord);
3939
+ const original = chordPitchClasses(chord);
3608
3940
  const candidates = [];
3609
3941
  if (isDominantType(chord)) {
3610
3942
  candidates.push({ chord: makeChord(mod122(chord.rootPc + 6), "dom7"), type: "tritone" });
@@ -3725,11 +4057,15 @@ function generateRhythm(ts, opts = {}) {
3725
4057
  const seed = opts.seed ?? 0;
3726
4058
  const bars = opts.bars ?? DEFAULT_BARS;
3727
4059
  const subdivision = opts.subdivision ?? DEFAULT_SUBDIVISION;
3728
- const density = Math.min(1, Math.max(0, opts.density ?? DEFAULT_DENSITY));
4060
+ const requestedDensity = assertFiniteNumber(opts.density ?? DEFAULT_DENSITY, "rhythm density");
4061
+ const density = Math.min(1, Math.max(0, requestedDensity));
4062
+ assertPositiveInt(bars, "rhythm bars");
4063
+ assertPositiveInt(subdivision, "rhythm subdivision");
3729
4064
  const barBeats = beatsPerBar(ts);
3730
4065
  const spanBeats = barBeats * bars;
3731
4066
  const step = 1 / subdivision;
3732
4067
  const slotCount = Math.round(spanBeats * subdivision);
4068
+ assertGenerationBudget(slotCount, "rhythm grid slots");
3733
4069
  const slotsPerBar = Math.round(barBeats * subdivision);
3734
4070
  const rng = createRng(seed);
3735
4071
  const positions = [];