@libraz/libcantus 0.9.2 → 0.9.4

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 (200) hide show
  1. package/README.md +6 -5
  2. package/dist/analyze/index.cjs +41 -1676
  3. package/dist/analyze/index.cjs.map +1 -1
  4. package/dist/analyze/index.d.cts +194 -122
  5. package/dist/analyze/index.d.ts +194 -122
  6. package/dist/analyze/index.js +19 -8
  7. package/dist/chunk-36N73KZ3.cjs +246 -0
  8. package/dist/chunk-36N73KZ3.cjs.map +1 -0
  9. package/dist/chunk-4HLLZPXT.cjs +3077 -0
  10. package/dist/chunk-4HLLZPXT.cjs.map +1 -0
  11. package/dist/chunk-7MGZ7D62.cjs +498 -0
  12. package/dist/chunk-7MGZ7D62.cjs.map +1 -0
  13. package/dist/{chunk-R3S2LUBH.js → chunk-7NKDPF74.js} +660 -305
  14. package/dist/chunk-7NKDPF74.js.map +1 -0
  15. package/dist/chunk-CAN76NCU.cjs +679 -0
  16. package/dist/chunk-CAN76NCU.cjs.map +1 -0
  17. package/dist/{chunk-7Y7WF64D.js → chunk-D2FAJBYL.js} +104 -35
  18. package/dist/chunk-D2FAJBYL.js.map +1 -0
  19. package/dist/{chunk-COKTX2G5.js → chunk-EHVEBWM5.js} +1052 -510
  20. package/dist/chunk-EHVEBWM5.js.map +1 -0
  21. package/dist/chunk-FCANXBAT.cjs +30 -0
  22. package/dist/chunk-FCANXBAT.cjs.map +1 -0
  23. package/dist/chunk-FNDWXRRI.cjs +479 -0
  24. package/dist/chunk-FNDWXRRI.cjs.map +1 -0
  25. package/dist/{chunk-GQKN5KDT.js → chunk-GQSGS2N4.js} +43 -29
  26. package/dist/chunk-GQSGS2N4.js.map +1 -0
  27. package/dist/chunk-HKGYCQ34.js +53 -0
  28. package/dist/chunk-HKGYCQ34.js.map +1 -0
  29. package/dist/chunk-I24MV65U.js +106 -0
  30. package/dist/chunk-I24MV65U.js.map +1 -0
  31. package/dist/{chunk-LN7ZZMD2.js → chunk-KMHOICIY.js} +322 -173
  32. package/dist/chunk-KMHOICIY.js.map +1 -0
  33. package/dist/chunk-LW53AHQU.cjs +1117 -0
  34. package/dist/chunk-LW53AHQU.cjs.map +1 -0
  35. package/dist/chunk-N6IHQUOX.js +1 -0
  36. package/dist/chunk-N6IHQUOX.js.map +1 -0
  37. package/dist/chunk-NKIBQ6PT.js +30 -0
  38. package/dist/chunk-NKIBQ6PT.js.map +1 -0
  39. package/dist/chunk-NP4NA2FB.cjs +106 -0
  40. package/dist/chunk-NP4NA2FB.cjs.map +1 -0
  41. package/dist/chunk-NVE34YGZ.cjs +1449 -0
  42. package/dist/chunk-NVE34YGZ.cjs.map +1 -0
  43. package/dist/{chunk-A4532ZFR.js → chunk-NYYWMV2C.js} +115 -47
  44. package/dist/chunk-NYYWMV2C.js.map +1 -0
  45. package/dist/{chunk-5HTNMQW4.js → chunk-Q2XUP227.js} +310 -118
  46. package/dist/chunk-Q2XUP227.js.map +1 -0
  47. package/dist/chunk-Q6OP4U43.cjs +327 -0
  48. package/dist/chunk-Q6OP4U43.cjs.map +1 -0
  49. package/dist/chunk-RC6GBMCD.cjs +845 -0
  50. package/dist/chunk-RC6GBMCD.cjs.map +1 -0
  51. package/dist/{chunk-XTC3X6MS.js → chunk-RK35RIN7.js} +453 -152
  52. package/dist/chunk-RK35RIN7.js.map +1 -0
  53. package/dist/chunk-RUMU3UA6.cjs +53 -0
  54. package/dist/chunk-RUMU3UA6.cjs.map +1 -0
  55. package/dist/chunk-T64C7ZD7.cjs +1 -0
  56. package/dist/chunk-T64C7ZD7.cjs.map +1 -0
  57. package/dist/chunk-XEBG3G53.js +498 -0
  58. package/dist/chunk-XEBG3G53.js.map +1 -0
  59. package/dist/chunk-YLEMGEFH.js +246 -0
  60. package/dist/chunk-YLEMGEFH.js.map +1 -0
  61. package/dist/chunk-ZEJR4TKL.cjs +173 -0
  62. package/dist/chunk-ZEJR4TKL.cjs.map +1 -0
  63. package/dist/core/index.cjs +130 -431
  64. package/dist/core/index.cjs.map +1 -1
  65. package/dist/core/index.d.cts +331 -17
  66. package/dist/core/index.d.ts +331 -17
  67. package/dist/core/index.js +71 -13
  68. package/dist/{function-2bJW1Zp_.d.ts → function-BtiZb9i9.d.ts} +28 -7
  69. package/dist/{function-r2sh-jOO.d.cts → function-DNugAOrt.d.cts} +28 -7
  70. package/dist/generate/index.cjs +30 -3751
  71. package/dist/generate/index.cjs.map +1 -1
  72. package/dist/generate/index.d.cts +373 -59
  73. package/dist/generate/index.d.ts +373 -59
  74. package/dist/generate/index.js +23 -9
  75. package/dist/{index-DZ1M21do.d.cts → index-6VR8-kit.d.ts} +20 -4
  76. package/dist/index-70p168VM.d.ts +192 -0
  77. package/dist/{index-D_ZTUNKp.d.cts → index-Be74mcon.d.cts} +18 -17
  78. package/dist/index-BtXT6UQ0.d.cts +192 -0
  79. package/dist/{index-BMPxwjwV.d.ts → index-C5UBbQ4G.d.ts} +18 -17
  80. package/dist/{index-BOBXBNwP.d.cts → index-C83mZI1P.d.ts} +93 -8
  81. package/dist/{index-BdeQ1r_p.d.cts → index-D6b6na69.d.cts} +14 -11
  82. package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
  83. package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
  84. package/dist/index-OG65ocOr.d.cts +272 -0
  85. package/dist/index-OG65ocOr.d.ts +272 -0
  86. package/dist/{index-BreoJRw0.d.ts → index-tJhkmQgE.d.cts} +20 -4
  87. package/dist/{index-CP3wAfEH.d.ts → index-tSQh-xwY.d.ts} +14 -11
  88. package/dist/{index-CM5W4rcm.d.ts → index-y_ak9_Hi.d.cts} +93 -8
  89. package/dist/index.cjs +419 -6848
  90. package/dist/index.cjs.map +1 -1
  91. package/dist/index.d.cts +14 -13
  92. package/dist/index.d.ts +14 -13
  93. package/dist/index.js +132 -27
  94. package/dist/model/index.cjs +8 -2423
  95. package/dist/model/index.cjs.map +1 -1
  96. package/dist/model/index.d.cts +433 -74
  97. package/dist/model/index.d.ts +433 -74
  98. package/dist/model/index.js +6 -7
  99. package/dist/styled-DIRkk9Uz.d.ts +506 -0
  100. package/dist/styled-DJbkJQfj.d.cts +506 -0
  101. package/dist/theory/index.cjs +172 -1676
  102. package/dist/theory/index.cjs.map +1 -1
  103. package/dist/theory/index.d.cts +151 -233
  104. package/dist/theory/index.d.ts +151 -233
  105. package/dist/theory/index.js +31 -7
  106. package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
  107. package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
  108. package/package.json +73 -24
  109. package/dist/chunk-5HTNMQW4.js.map +0 -1
  110. package/dist/chunk-7Y7WF64D.js.map +0 -1
  111. package/dist/chunk-A4532ZFR.js.map +0 -1
  112. package/dist/chunk-BHUT2S5G.js +0 -22
  113. package/dist/chunk-BHUT2S5G.js.map +0 -1
  114. package/dist/chunk-CCBLVAPK.js +0 -42
  115. package/dist/chunk-CCBLVAPK.js.map +0 -1
  116. package/dist/chunk-COKTX2G5.js.map +0 -1
  117. package/dist/chunk-DYWFNC4A.js +0 -152
  118. package/dist/chunk-DYWFNC4A.js.map +0 -1
  119. package/dist/chunk-GQKN5KDT.js.map +0 -1
  120. package/dist/chunk-LN7ZZMD2.js.map +0 -1
  121. package/dist/chunk-MSWFZF7J.js +0 -118
  122. package/dist/chunk-MSWFZF7J.js.map +0 -1
  123. package/dist/chunk-R3S2LUBH.js.map +0 -1
  124. package/dist/chunk-SDM3VJJ5.js +0 -41
  125. package/dist/chunk-SDM3VJJ5.js.map +0 -1
  126. package/dist/chunk-XTC3X6MS.js.map +0 -1
  127. package/dist/chunk-ZXW5YIWK.js +0 -55
  128. package/dist/chunk-ZXW5YIWK.js.map +0 -1
  129. package/dist/index-CGMZNCx6.d.cts +0 -140
  130. package/dist/index-CGMZNCx6.d.ts +0 -140
  131. package/dist/styled-BjXtOFNk.d.cts +0 -282
  132. package/dist/styled-D7BAx3S3.d.ts +0 -282
  133. package/src/analyze/arrange/index.ts +0 -21
  134. package/src/analyze/arrange/internal.ts +0 -125
  135. package/src/analyze/arrange/tension.ts +0 -187
  136. package/src/analyze/arrange/tracks.ts +0 -279
  137. package/src/analyze/detect/index.ts +0 -235
  138. package/src/analyze/functional/borrowed.ts +0 -84
  139. package/src/analyze/functional/cadence.ts +0 -66
  140. package/src/analyze/functional/function.ts +0 -184
  141. package/src/analyze/functional/index.ts +0 -23
  142. package/src/analyze/functional/internal.ts +0 -29
  143. package/src/analyze/functional/roman.ts +0 -393
  144. package/src/analyze/index.ts +0 -49
  145. package/src/analyze/timeline/index.ts +0 -415
  146. package/src/analyze/voice/index.ts +0 -330
  147. package/src/core/index.ts +0 -47
  148. package/src/core/interval/index.ts +0 -81
  149. package/src/core/meter/index.ts +0 -282
  150. package/src/core/pitch/index.ts +0 -311
  151. package/src/core/random/index.ts +0 -53
  152. package/src/core/tuning/index.ts +0 -156
  153. package/src/core/types.ts +0 -29
  154. package/src/generate/bass/index.ts +0 -350
  155. package/src/generate/countermelody/index.ts +0 -379
  156. package/src/generate/drums/beat.ts +0 -266
  157. package/src/generate/drums/euclid.ts +0 -58
  158. package/src/generate/drums/fills.ts +0 -356
  159. package/src/generate/drums/ghost.ts +0 -119
  160. package/src/generate/drums/hihat.ts +0 -285
  161. package/src/generate/drums/hit.ts +0 -31
  162. package/src/generate/drums/index.ts +0 -329
  163. package/src/generate/drums/internal.ts +0 -257
  164. package/src/generate/drums/kick.ts +0 -160
  165. package/src/generate/drums/percussion.ts +0 -121
  166. package/src/generate/drums/rng.ts +0 -7
  167. package/src/generate/drums/swing.ts +0 -71
  168. package/src/generate/groove/index.ts +0 -326
  169. package/src/generate/harmonize/index.ts +0 -440
  170. package/src/generate/index.ts +0 -59
  171. package/src/generate/motif/index.ts +0 -398
  172. package/src/generate/progression/index.ts +0 -370
  173. package/src/generate/reharmony/index.ts +0 -330
  174. package/src/generate/rhythm/index.ts +0 -165
  175. package/src/index.ts +0 -247
  176. package/src/model/chord.ts +0 -423
  177. package/src/model/index.ts +0 -15
  178. package/src/model/interval.ts +0 -82
  179. package/src/model/key.ts +0 -266
  180. package/src/model/note.ts +0 -176
  181. package/src/model/progression.ts +0 -161
  182. package/src/model/shared.ts +0 -12
  183. package/src/theory/chord/index.ts +0 -373
  184. package/src/theory/chordscale/index.ts +0 -374
  185. package/src/theory/counterpoint/index.ts +0 -230
  186. package/src/theory/harmony/index.ts +0 -92
  187. package/src/theory/index.ts +0 -107
  188. package/src/theory/safety/index.ts +0 -419
  189. package/src/theory/scale/degrees.ts +0 -123
  190. package/src/theory/scale/index.ts +0 -28
  191. package/src/theory/scale/key.ts +0 -56
  192. package/src/theory/scale/masks.ts +0 -153
  193. package/src/theory/spelling/index.ts +0 -182
  194. package/src/theory/symbol/index.ts +0 -268
  195. package/src/theory/voicing/index.ts +0 -5
  196. package/src/theory/voicing/internal.ts +0 -168
  197. package/src/theory/voicing/leading.ts +0 -112
  198. package/src/theory/voicing/satb.ts +0 -195
  199. package/src/theory/voicing/styled.ts +0 -176
  200. package/tsconfig.json +0 -23
@@ -1,18 +1,24 @@
1
1
  import {
2
2
  createRng
3
- } from "./chunk-BHUT2S5G.js";
3
+ } from "./chunk-HKGYCQ34.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-YLEMGEFH.js";
11
+ import {
12
+ isDiatonic,
13
+ parallelKey
14
+ } from "./chunk-KMHOICIY.js";
10
15
  import {
11
16
  roleOf
12
- } from "./chunk-SDM3VJJ5.js";
17
+ } from "./chunk-NKIBQ6PT.js";
13
18
  import {
14
- enumerateSafePitches
15
- } from "./chunk-A4532ZFR.js";
19
+ enumerateSafePitches,
20
+ evaluateSafety
21
+ } from "./chunk-NYYWMV2C.js";
16
22
  import {
17
23
  NATURAL_MINOR_MASK,
18
24
  chordPitchClasses,
@@ -26,9 +32,23 @@ import {
26
32
  minorKey,
27
33
  nearestScaleTone,
28
34
  scaleTonesInDegreeOrder
29
- } from "./chunk-7Y7WF64D.js";
35
+ } from "./chunk-D2FAJBYL.js";
36
+ import {
37
+ InvalidInputError,
38
+ assertFiniteNumber,
39
+ assertGenerationBudget,
40
+ assertInteger,
41
+ assertNoteEvents,
42
+ assertOneOf,
43
+ assertPositiveInt,
44
+ assertRange,
45
+ assertTimeSignature,
46
+ dropSilentNotes,
47
+ pitchClassOf
48
+ } from "./chunk-XEBG3G53.js";
30
49
 
31
50
  // src/generate/bass/index.ts
51
+ var BASS_STYLES = ["root", "rootFifth", "pop", "walking", "arpeggio"];
32
52
  var DEFAULT_TS = { numerator: 4, denominator: 4 };
33
53
  var DEFAULT_STYLE = "root";
34
54
  var DEFAULT_OCTAVE = 2;
@@ -36,9 +56,6 @@ var STRONG_VELOCITY = 100;
36
56
  var WEAK_VELOCITY = 80;
37
57
  var PICKUP_PROB = 0.35;
38
58
  var EPS = 1e-9;
39
- function pitchClass(value) {
40
- return (Math.trunc(value) % 12 + 12) % 12;
41
- }
42
59
  function placePc(pc, anchor, low) {
43
60
  const high = low + 12;
44
61
  let midi = pc + 12 * Math.round((anchor - pc) / 12);
@@ -54,24 +71,24 @@ function chordTonePcs(chord) {
54
71
  const seen = /* @__PURE__ */ new Set();
55
72
  const pcs = [];
56
73
  for (const interval of chord.intervals) {
57
- const pc = pitchClass(chord.rootPc + interval);
74
+ const pc = pitchClassOf(chord.rootPc + interval);
58
75
  if (!seen.has(pc)) {
59
76
  seen.add(pc);
60
77
  pcs.push(pc);
61
78
  }
62
79
  }
63
80
  if (pcs.length === 0) {
64
- pcs.push(pitchClass(chord.rootPc));
81
+ pcs.push(pitchClassOf(chord.rootPc));
65
82
  }
66
83
  return pcs;
67
84
  }
68
85
  function fifthPcOf(chord) {
69
- const fifths = new Set(chord.intervals.map((i) => pitchClass(i)));
86
+ const fifths = new Set(chord.intervals.map((i) => pitchClassOf(i)));
70
87
  const chosen = fifths.has(7) ? 7 : fifths.has(6) ? 6 : fifths.has(8) ? 8 : void 0;
71
- return chosen === void 0 ? pitchClass(chord.rootPc) : pitchClass(chord.rootPc + chosen);
88
+ return chosen === void 0 ? pitchClassOf(chord.rootPc) : pitchClassOf(chord.rootPc + chosen);
72
89
  }
73
90
  function bassPcOf(chord) {
74
- return pitchClass(chord.bassPc ?? chord.rootPc);
91
+ return pitchClassOf(chord.bassPc ?? chord.rootPc);
75
92
  }
76
93
  function beatPositions(start, end) {
77
94
  const positions = [];
@@ -110,7 +127,8 @@ function buildPop(ctx, seg) {
110
127
  } else if (ctx.rng.prob(PICKUP_PROB)) {
111
128
  if (ctx.rng.prob(0.5)) {
112
129
  const base = placePc(rootPc, ctx.prevMidi, ctx.low);
113
- emit(ctx, pos, rootPc, Math.max(ctx.low, base - 12));
130
+ const dropped = base - 12;
131
+ emit(ctx, pos, rootPc, dropped >= 0 ? dropped : base);
114
132
  } else {
115
133
  ctx.prevMidi = emit(ctx, pos, fifthPcOf(seg.chord));
116
134
  }
@@ -130,12 +148,23 @@ function buildArpeggio(ctx, seg) {
130
148
  }
131
149
  function approachNote(ctx, target, from) {
132
150
  const dir = from <= target ? -1 : 1;
151
+ const chromatic = target + dir;
133
152
  if (ctx.rng.prob(0.5)) {
134
- return target + dir;
153
+ return foldIntoBand(chromatic, ctx.low);
135
154
  }
136
155
  const cand = nearestScaleTone(target + dir * 2, ctx.key);
137
156
  const step = Math.abs(cand - target);
138
- return step >= 1 && step <= 2 ? cand : target + dir;
157
+ return foldIntoBand(step >= 1 && step <= 2 ? cand : chromatic, ctx.low);
158
+ }
159
+ function foldIntoBand(midi, low) {
160
+ let result = midi;
161
+ while (result < low) {
162
+ result += 12;
163
+ }
164
+ while (result > low + 12) {
165
+ result -= 12;
166
+ }
167
+ return result;
139
168
  }
140
169
  function buildWalking(ctx, seg, next) {
141
170
  const tones = chordTonePcs(seg.chord);
@@ -149,7 +178,7 @@ function buildWalking(ctx, seg, next) {
149
178
  if (next && count > 1 && i === count - 1) {
150
179
  const nextBass = placePc(bassPcOf(next.chord), ctx.prevMidi, ctx.low);
151
180
  const midi = approachNote(ctx, nextBass, ctx.prevMidi);
152
- ctx.prevMidi = emit(ctx, pos, pitchClass(midi), midi);
181
+ ctx.prevMidi = emit(ctx, pos, pitchClassOf(midi), midi);
153
182
  return;
154
183
  }
155
184
  const pc = tones[i % tones.length] ?? bassPcOf(seg.chord);
@@ -157,13 +186,30 @@ function buildWalking(ctx, seg, next) {
157
186
  });
158
187
  }
159
188
  function generateBassLine(opts) {
189
+ assertGenerationBudget(opts.segments.length, "bass segments");
190
+ for (let index = 0; index < opts.segments.length; index += 1) {
191
+ const segment = opts.segments[index];
192
+ if (!segment) continue;
193
+ assertRange(segment.startBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].startBeat`);
194
+ assertRange(segment.endBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].endBeat`);
195
+ if (segment.endBeat <= segment.startBeat) {
196
+ throw new InvalidInputError(`segments[${index}] must have a positive duration`);
197
+ }
198
+ }
160
199
  const segments = [...opts.segments].sort((a, b) => a.startBeat - b.startBeat);
161
200
  if (segments.length === 0) {
162
201
  return [];
163
202
  }
164
203
  const ts = opts.ts ?? DEFAULT_TS;
165
- const style = opts.style ?? DEFAULT_STYLE;
204
+ assertTimeSignature(ts);
205
+ const style = assertOneOf(opts.style ?? DEFAULT_STYLE, BASS_STYLES, "bass style");
166
206
  const octave = opts.octave ?? DEFAULT_OCTAVE;
207
+ assertInteger(octave, "bass octave", -1, 9);
208
+ const estimatedNotes = segments.reduce(
209
+ (count, segment) => count + Math.max(1, Math.ceil(segment.endBeat - segment.startBeat)),
210
+ 0
211
+ );
212
+ assertGenerationBudget(estimatedNotes, "bass notes");
167
213
  const low = octave * 12 + 12;
168
214
  const ctx = {
169
215
  ts,
@@ -231,25 +277,31 @@ var HOLD_OFF_BEAT_PROB = 0.2;
231
277
  var REINFORCE_PROB = 0.35;
232
278
  var DEFAULT_MELODY_VELOCITY = 96;
233
279
  var VELOCITY_DROP = 16;
280
+ var TIE_BREAK_JITTER = 1e-6;
234
281
  function isWholeBeat(beat) {
235
282
  return Math.abs(beat - Math.round(beat)) < EPS2;
236
283
  }
237
284
  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;
285
+ return melody.at(beat)?.note;
247
286
  }
248
287
  function melodyAttacksAt(melody, beat) {
249
- return melody.some((note) => Math.abs(note.startBeat - beat) < EPS2);
288
+ return melody.attacksAt(beat);
289
+ }
290
+ function melodySnapshotAt(melody, beat) {
291
+ const current = melodyNoteAt(melody, beat);
292
+ if (current === void 0) {
293
+ return void 0;
294
+ }
295
+ const attacked = Math.abs(current.startBeat - beat) < EPS2;
296
+ const previous = attacked ? melodyNoteAt(melody, beat - EPS2 * 2) : current;
297
+ const snapshot = { pitch: current.pitch };
298
+ if (previous !== void 0) {
299
+ snapshot.prevPitch = previous.pitch;
300
+ }
301
+ return snapshot;
250
302
  }
251
303
  function followOnsets(melody) {
252
- return [...new Set(melody.map((note) => note.startBeat))].sort((a, b) => a - b);
304
+ return [...new Set(melody.notes.map(({ note }) => note.startBeat))];
253
305
  }
254
306
  function complementOnsets(melody, ts, spanStart, spanEnd, rng) {
255
307
  const onsets = [];
@@ -312,54 +364,134 @@ function scoreCandidate(pitch, melPitch, melPrev, prevPitch, chord, center) {
312
364
  score -= Math.abs(pitch - center) * 0.02;
313
365
  return score;
314
366
  }
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;
367
+ function heldNoteBoundaries(melody, startBeat, endBeat, chordChangeBeats) {
368
+ const boundaries = /* @__PURE__ */ new Set([startBeat]);
369
+ for (const onset of melody.onsetsBetween(startBeat, endBeat)) {
370
+ boundaries.add(onset);
371
+ }
372
+ for (const beat of chordChangeBeats) {
373
+ if (beat > startBeat + EPS2 && beat < endBeat - EPS2) {
374
+ boundaries.add(beat);
322
375
  }
323
376
  }
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;
377
+ const firstGrid = Math.floor(startBeat / GRID_STEP + 1) * GRID_STEP;
378
+ for (let beat = firstGrid; beat < endBeat - EPS2; beat += GRID_STEP) {
379
+ boundaries.add(beat);
380
+ }
381
+ return [...boundaries].sort((a, b) => a - b);
382
+ }
383
+ function boundaryContexts(melody, harmony, startBeat, endBeat, ts) {
384
+ const boundaries = heldNoteBoundaries(melody, startBeat, endBeat, harmony.chordChangeBeats);
385
+ return boundaries.map((boundary) => {
386
+ const melodyVoice = melodySnapshotAt(melody, boundary);
387
+ return {
388
+ melodyVoice,
389
+ otherVoices: melodyVoice === void 0 ? [] : [melodyVoice],
390
+ chord: harmony.chordAt(boundary),
391
+ strongBeat: isStrongBeat(boundary, ts),
392
+ atCounterOnset: Math.abs(boundary - startBeat) < EPS2
393
+ };
394
+ });
395
+ }
396
+ function heldPitchSafety(pitch, contexts, prevPitch, key, profile, register, low, high) {
397
+ let worst = 0 /* Safe */;
398
+ for (const context of contexts) {
399
+ const melodyVoice = context.melodyVoice;
400
+ if (melodyVoice !== void 0 && (register === "below" && pitch >= melodyVoice.pitch || register === "above" && pitch <= melodyVoice.pitch)) {
401
+ return null;
329
402
  }
330
- while (register === "above" && pitch <= melPitch && pitch + 12 <= high) {
331
- pitch += 12;
403
+ const result = evaluateSafety(
404
+ {
405
+ profile,
406
+ candidatePitch: pitch,
407
+ prevPitch: context.atCounterOnset ? prevPitch : pitch,
408
+ chord: context.chord,
409
+ key,
410
+ otherVoices: context.otherVoices,
411
+ strongBeat: context.strongBeat,
412
+ vocalLow: low,
413
+ vocalHigh: high
414
+ },
415
+ { suggestions: false }
416
+ );
417
+ worst = Math.max(worst, result.safety);
418
+ if (worst === 2 /* Dissonant */) {
419
+ return worst;
332
420
  }
333
421
  }
334
- return Math.min(Math.max(pitch, low), high);
422
+ return worst;
335
423
  }
336
424
  function generateCounterMelody(opts) {
337
- const melody = [...opts.melody].sort((a, b) => a.startBeat - b.startBeat);
338
- if (melody.length === 0) {
339
- return [];
425
+ const timeline = opts.timeline;
426
+ const chordAt = timeline === void 0 ? opts.chordAt : (beat) => timeline.at(beat);
427
+ if (chordAt === void 0) {
428
+ throw new TypeError("countermelody needs a timeline or a chordAt callback");
340
429
  }
341
- const ts = opts.ts ?? DEFAULT_TS2;
430
+ const chordChangeBeats = timeline === void 0 ? opts.chordChangeBeats ?? [] : timeline.segments.flatMap((segment) => [segment.startBeat, segment.endBeat]);
342
431
  const register = opts.register ?? "below";
343
- const rhythm = opts.rhythm ?? "complement";
344
- const profile = opts.profile ?? "pop";
432
+ const melody = createNoteEventIndex(opts.melody, {
433
+ allowNonPositiveDuration: true,
434
+ tieBreak: register === "below" ? "highest" : "lowest"
435
+ });
436
+ const ts = opts.ts ?? DEFAULT_TS2;
437
+ assertTimeSignature(ts);
438
+ const rhythm = assertOneOf(opts.rhythm ?? "complement", ["complement", "follow"], "rhythm");
439
+ const profile = assertOneOf(opts.profile ?? "pop", ["strict", "pop"], "safety profile");
345
440
  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;
441
+ if (opts.pitchLow !== void 0) {
442
+ assertInteger(opts.pitchLow, "countermelody pitchLow");
443
+ }
444
+ if (opts.pitchHigh !== void 0) {
445
+ assertInteger(opts.pitchHigh, "countermelody pitchHigh");
446
+ }
447
+ if (melody.notes.every((indexed) => indexed.note.durationBeat <= 0)) {
448
+ return [];
449
+ }
450
+ const spanStart = melody.notes[0]?.note.startBeat ?? 0;
451
+ const spanEnd = melody.notes.reduce((end, n) => Math.max(end, n.endBeat), spanStart);
452
+ assertGenerationBudget(Math.ceil((spanEnd - spanStart) / GRID_STEP), "countermelody grid");
453
+ const meanPitch = melody.notes.reduce((sum, n) => sum + n.note.pitch, 0) / melody.notes.length;
349
454
  const defaultCenter = register === "below" ? meanPitch - REGISTER_OFFSET : meanPitch + REGISTER_OFFSET;
350
455
  const low = opts.pitchLow ?? Math.round(defaultCenter) - DEFAULT_HALF_RANGE;
351
456
  const high = opts.pitchHigh ?? Math.round(defaultCenter) + DEFAULT_HALF_RANGE;
457
+ assertInteger(low, "countermelody pitchLow");
458
+ assertInteger(high, "countermelody pitchHigh");
459
+ if (low > high) {
460
+ throw new InvalidInputError(
461
+ `countermelody pitchLow must not exceed pitchHigh; received ${low} > ${high}`
462
+ );
463
+ }
464
+ assertGenerationBudget(high - low + 1, "countermelody pitch candidates");
465
+ assertGenerationBudget(chordChangeBeats.length, "chord change beats");
466
+ for (let index = 0; index < chordChangeBeats.length; index += 1) {
467
+ assertRange(
468
+ chordChangeBeats[index] ?? Number.NaN,
469
+ 0,
470
+ Number.MAX_SAFE_INTEGER,
471
+ `chordChangeBeats[${index}]`
472
+ );
473
+ }
352
474
  const center = (low + high) / 2;
353
475
  const onsets = rhythm === "follow" ? followOnsets(melody) : complementOnsets(melody, ts, spanStart, spanEnd, rng);
476
+ const boundaryEstimate = onsets.length + Math.ceil((spanEnd - spanStart) / GRID_STEP) + melody.notes.length + chordChangeBeats.length;
477
+ assertGenerationBudget(boundaryEstimate * (high - low + 1), "countermelody search");
478
+ const jitter = Array.from({ length: high - low + 1 }, () => rng.next() * TIE_BREAK_JITTER);
354
479
  const out = [];
355
480
  let prevPitch;
356
- let prevOnset;
357
- for (const beat of onsets) {
481
+ for (let onsetIndex = 0; onsetIndex < onsets.length; onsetIndex += 1) {
482
+ const beat = onsets[onsetIndex];
483
+ if (beat === void 0) {
484
+ continue;
485
+ }
486
+ const nextOnset = onsets[onsetIndex + 1];
487
+ const endBeat = nextOnset ?? Math.max(spanEnd, beat + GRID_STEP);
358
488
  const melNote = melodyNoteAt(melody, beat);
359
489
  const melPitch = melNote?.pitch;
360
- const melPrev = prevOnset !== void 0 ? melodyNoteAt(melody, prevOnset)?.pitch : void 0;
361
- const chord = opts.chordAt(beat);
362
- const otherVoices = melPitch !== void 0 ? [{ pitch: melPitch, prevPitch: melPrev }] : [];
490
+ const melodyVoice = melodySnapshotAt(melody, beat);
491
+ const melPrev = melodyVoice?.prevPitch;
492
+ const contexts = boundaryContexts(melody, { chordAt, chordChangeBeats }, beat, endBeat, ts);
493
+ const chord = contexts[0]?.chord ?? chordAt(beat);
494
+ const otherVoices = melodyVoice !== void 0 ? [melodyVoice] : [];
363
495
  const candidates = enumerateSafePitches(
364
496
  {
365
497
  profile,
@@ -375,45 +507,48 @@ function generateCounterMelody(opts) {
375
507
  high
376
508
  );
377
509
  let bestPitch;
510
+ let bestWorstSafety = 2 /* Dissonant */;
378
511
  let bestScore = Number.NEGATIVE_INFINITY;
379
512
  for (const candidate of candidates) {
513
+ const worstSafety = heldPitchSafety(
514
+ candidate,
515
+ contexts,
516
+ prevPitch,
517
+ opts.key,
518
+ profile,
519
+ register,
520
+ low,
521
+ high
522
+ );
523
+ if (worstSafety === null || worstSafety === 2 /* Dissonant */) {
524
+ continue;
525
+ }
380
526
  if (melPitch !== void 0) {
381
- if (register === "below" && candidate >= melPitch) {
382
- continue;
383
- }
384
- if (register === "above" && candidate <= melPitch) {
385
- continue;
386
- }
387
527
  if (prevPitch !== void 0 && melPrev !== void 0 && // Parallel octaves are the perfect-class-zero case of this predicate,
388
528
  // so a single check covers both parallel fifths and octaves.
389
529
  createsParallelPerfect(prevPitch, candidate, melPrev, melPitch)) {
390
530
  continue;
391
531
  }
392
532
  }
393
- const score = scoreCandidate(candidate, melPitch, melPrev, prevPitch, chord, center);
533
+ const score = scoreCandidate(candidate, melPitch, melPrev, prevPitch, chord, center) + (jitter[candidate - low] ?? 0);
394
534
  const tie = Math.abs(score - bestScore) <= EPS2 && candidate < (bestPitch ?? Number.POSITIVE_INFINITY);
395
- if (score > bestScore + EPS2 || tie) {
535
+ if (worstSafety < bestWorstSafety || worstSafety === bestWorstSafety && (score > bestScore + EPS2 || tie)) {
536
+ bestWorstSafety = worstSafety;
396
537
  bestScore = score;
397
538
  bestPitch = candidate;
398
539
  }
399
540
  }
400
- const pitch = bestPitch ?? fallbackPitch(melPitch, prevPitch, center, register, low, high, opts.key);
541
+ if (bestPitch === void 0) {
542
+ prevPitch = void 0;
543
+ continue;
544
+ }
545
+ const pitch = bestPitch;
401
546
  const velocity = Math.max(
402
547
  1,
403
548
  Math.min(127, Math.round((melNote?.velocity ?? DEFAULT_MELODY_VELOCITY) - VELOCITY_DROP))
404
549
  );
405
- out.push({ pitch, startBeat: beat, durationBeat: GRID_STEP, velocity });
550
+ out.push({ pitch, startBeat: beat, durationBeat: endBeat - beat, velocity });
406
551
  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
552
  }
418
553
  return out;
419
554
  }
@@ -437,6 +572,27 @@ var GM = {
437
572
  };
438
573
  var EIGHTH = 0.5;
439
574
  var SIXTEENTH = 0.25;
575
+ var DRUM_FEELS = ["straight", "swing", "shuffle"];
576
+ var GROOVE_STYLES = [
577
+ "standard",
578
+ "funk",
579
+ "shuffle",
580
+ "bossa",
581
+ "trap",
582
+ "halftime",
583
+ "breakbeat",
584
+ "house",
585
+ "synthpop"
586
+ ];
587
+ var DRUM_ROLES = ["full", "ambient", "minimal", "fxOnly"];
588
+ var PUBLIC_SECTIONS = [
589
+ "intro",
590
+ "verse",
591
+ "prechorus",
592
+ "chorus",
593
+ "bridge",
594
+ "outro"
595
+ ];
440
596
  var STYLE_MAP = {
441
597
  // Standard pop pulse: kick on beats 1 and 3, backbeat snare on 2 and 4.
442
598
  standard: { style: "standard", feel: "straight", ghostBoost: false, snareBeat3: false },
@@ -636,7 +792,7 @@ function getGhostDensity(mood, section, backingDensity, bpm) {
636
792
  }
637
793
  return levelToProbability(level);
638
794
  }
639
- function getGhostVelocity(section, beatPosition, isAfterSnare) {
795
+ function getGhostVelocity(section, beatPosition) {
640
796
  let base = 0.4;
641
797
  const even = beatPosition % 2 === 0 ? 0.05 : 0;
642
798
  switch (section) {
@@ -657,14 +813,11 @@ function getGhostVelocity(section, beatPosition, isAfterSnare) {
657
813
  base = 0.38;
658
814
  break;
659
815
  }
660
- if (isAfterSnare) {
661
- base += 0.1;
662
- }
663
816
  return Math.max(0.25, Math.min(0.65, base));
664
817
  }
665
818
  function getGhostProbabilityAtPosition(beat, sixteenthInBeat) {
666
- const nearSnare = beat === 0 && sixteenthInBeat === 3 || beat === 1 && sixteenthInBeat === 1 || beat === 2 && sixteenthInBeat === 3 || beat === 3 && sixteenthInBeat === 1;
667
- return nearSnare ? 0.6 : 0.25;
819
+ const leadsIntoBackbeat = (beat === 0 || beat === 2) && sixteenthInBeat === 3;
820
+ return leadsIntoBackbeat ? 0.6 : 0.25;
668
821
  }
669
822
  function selectGhostPositions(mood, rng) {
670
823
  if (mood === 2 /* Energetic */) {
@@ -947,10 +1100,7 @@ function quantizeSwing(startBeat, swing, resolution) {
947
1100
  }
948
1101
 
949
1102
  // src/generate/drums/beat.ts
950
- function swing16(tick, feel, swingAmount) {
951
- return quantizeSwing(tick, effectiveSwing(feel, swingAmount), "sixteenth");
952
- }
953
- function hatSwingFactor(style) {
1103
+ function styleSwingFactor(style) {
954
1104
  if (style === "trap") {
955
1105
  return 0;
956
1106
  }
@@ -959,6 +1109,12 @@ function hatSwingFactor(style) {
959
1109
  }
960
1110
  return 0.5;
961
1111
  }
1112
+ function sectionSwing(sec, swingAmount) {
1113
+ return effectiveSwing(sec.feel, swingAmount) * styleSwingFactor(sec.style);
1114
+ }
1115
+ function swing16(tick, sec, swingAmount) {
1116
+ return quantizeSwing(tick, sectionSwing(sec, swingAmount), "sixteenth");
1117
+ }
962
1118
  function generateKickForBeat(ctx, sec, kick) {
963
1119
  if (ctx.inPrechorusLift) {
964
1120
  return;
@@ -969,7 +1125,7 @@ function generateKickForBeat(ctx, sec, kick) {
969
1125
  ctx.track.add(GM.BD, ctx.beatTick, EIGHTH, ctx.velocity);
970
1126
  }
971
1127
  if (and) {
972
- const andTick = swing16(ctx.beatTick + EIGHTH, sec.feel, ctx.swingAmount);
1128
+ const andTick = swing16(ctx.beatTick + EIGHTH, sec, ctx.swingAmount);
973
1129
  ctx.track.add(GM.BD, andTick, EIGHTH, ctx.velocity * 0.85);
974
1130
  }
975
1131
  }
@@ -986,12 +1142,17 @@ function generateSnareForBeat(ctx, sec, isIntroFirst) {
986
1142
  }
987
1143
  const backbeatVel = Math.min(127, ctx.velocity + 16);
988
1144
  const promoteSparseChorus = sec.style === "sparse" && ctx.section === "chorus" && sec.role === "full";
1145
+ if (sec.role === "fxOnly") {
1146
+ return;
1147
+ }
1148
+ if (sec.role === "minimal") {
1149
+ ctx.track.add(GM.SIDESTICK, ctx.beatTick, EIGHTH, ctx.velocity * 0.65);
1150
+ return;
1151
+ }
989
1152
  if (promoteSparseChorus) {
990
1153
  ctx.track.add(GM.SD, ctx.beatTick, EIGHTH, backbeatVel);
991
1154
  } else if (sec.style === "sparse" || sec.role === "ambient") {
992
- if (sec.role !== "fxOnly" && sec.role !== "minimal") {
993
- ctx.track.add(GM.SIDESTICK, ctx.beatTick, EIGHTH, ctx.velocity * 0.8);
994
- }
1155
+ ctx.track.add(GM.SIDESTICK, ctx.beatTick, EIGHTH, ctx.velocity * 0.8);
995
1156
  } else {
996
1157
  ctx.track.add(GM.SD, ctx.beatTick, EIGHTH, backbeatVel);
997
1158
  }
@@ -1012,13 +1173,13 @@ function generateGhostNotesForBeat(ctx, sec) {
1012
1173
  continue;
1013
1174
  }
1014
1175
  const variation = ctx.rng.float(0.85, 1.15);
1015
- const ghostBase = getGhostVelocity(ctx.section, ctx.beat / 2, false);
1176
+ const ghostBase = getGhostVelocity(ctx.section, ctx.beat / 2);
1016
1177
  let ghostVel = ctx.velocity * ghostBase * variation;
1017
1178
  if (pos === "a") {
1018
1179
  ghostVel *= 0.9;
1019
1180
  }
1020
1181
  const offset = pos === "e" ? SIXTEENTH : 3 * SIXTEENTH;
1021
- const tick = swing16(ctx.beatTick + offset, sec.feel, ctx.swingAmount);
1182
+ const tick = swing16(ctx.beatTick + offset, sec, ctx.swingAmount);
1022
1183
  ctx.track.add(GM.SD, tick, SIXTEENTH, ghostVel);
1023
1184
  }
1024
1185
  }
@@ -1080,7 +1241,7 @@ function generateHiHatForBeat(ctx, sec) {
1080
1241
  for (let eighth = 0; eighth < 2; eighth += 1) {
1081
1242
  let hhTick = ctx.beatTick + eighth * EIGHTH;
1082
1243
  if (eighth === 1) {
1083
- hhTick = swing16(hhTick, sec.feel, ctx.swingAmount);
1244
+ hhTick = swing16(hhTick, sec, ctx.swingAmount);
1084
1245
  }
1085
1246
  if (ctx.section === "intro" && eighth === 1) {
1086
1247
  if (sec.useFootHh && ctx.beat % 2 === 0) {
@@ -1110,9 +1271,8 @@ function generateHiHatForBeat(ctx, sec) {
1110
1271
  }
1111
1272
  for (let sixteenth = 0; sixteenth < 4; sixteenth += 1) {
1112
1273
  let hhTick = ctx.beatTick + sixteenth * SIXTEENTH;
1113
- if (sixteenth === 1 || sixteenth === 3) {
1114
- const swung = effectiveSwing(sec.feel, ctx.swingAmount) * hatSwingFactor(sec.style);
1115
- hhTick = quantizeSwing(hhTick, swung, "sixteenth");
1274
+ if (sixteenth !== 0) {
1275
+ hhTick = swing16(hhTick, sec, ctx.swingAmount);
1116
1276
  }
1117
1277
  const metricVel = hiHatVelocityMultiplier(sixteenth, ctx.rng);
1118
1278
  const hhVel = Math.max(20, ctx.velocity * dm * typeMult * metricVel);
@@ -1133,9 +1293,10 @@ function generateHiHatForBeat(ctx, sec) {
1133
1293
 
1134
1294
  // src/generate/drums/euclid.ts
1135
1295
  function euclideanRhythm(pulses, steps, rotation = 0) {
1136
- if (steps < 1) {
1137
- return [];
1138
- }
1296
+ assertPositiveInt(steps, "euclidean steps");
1297
+ assertInteger(pulses, "euclidean pulses");
1298
+ assertInteger(rotation, "euclidean rotation");
1299
+ assertGenerationBudget(steps, "euclidean pattern steps");
1139
1300
  const p = Math.max(0, Math.min(pulses, steps));
1140
1301
  const base = bjorklund(p, steps);
1141
1302
  const shift = (rotation % steps + steps) % steps;
@@ -1144,18 +1305,6 @@ function euclideanRhythm(pulses, steps, rotation = 0) {
1144
1305
  }
1145
1306
  return base.map((_, i) => base[(i - shift + steps) % steps] ?? false);
1146
1307
  }
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
1308
  function bjorklund(pulses, steps) {
1160
1309
  if (pulses === 0) {
1161
1310
  return Array.from({ length: steps }, () => false);
@@ -1451,24 +1600,62 @@ function generateFill(track, beatTick, beat, fillType, velocity) {
1451
1600
  }
1452
1601
 
1453
1602
  // src/generate/drums/hit.ts
1603
+ var DRUM_NOTES = {
1604
+ kick: GM.BD,
1605
+ snare: GM.SD,
1606
+ sideStick: GM.SIDESTICK,
1607
+ handClap: GM.HANDCLAP,
1608
+ closedHiHat: GM.CHH,
1609
+ pedalHiHat: GM.FHH,
1610
+ openHiHat: GM.OHH,
1611
+ crash: GM.CRASH,
1612
+ ride: GM.RIDE,
1613
+ tambourine: GM.TAMBOURINE,
1614
+ highTom: GM.TOM_H,
1615
+ midTom: GM.TOM_M,
1616
+ lowTom: GM.TOM_L,
1617
+ shaker: GM.SHAKER
1618
+ };
1619
+ var VOICE_BY_NOTE = new Map(
1620
+ Object.entries(DRUM_NOTES).map(([voice, note]) => [note, voice])
1621
+ );
1622
+ function drumVoiceOf(pitch) {
1623
+ return VOICE_BY_NOTE.get(pitch);
1624
+ }
1625
+ var ONSET_KEY_SCALE = 128;
1454
1626
  var HitList = class {
1455
1627
  hits = [];
1628
+ /** Onset keys already occupied, as `pitch:quantizedBeat`. */
1629
+ #onsets = /* @__PURE__ */ new Set();
1456
1630
  /** Append a hit, clamping velocity and dropping non-positive positions. */
1457
1631
  add(pitch, startBeat, durationBeat, velocity) {
1632
+ const beat = Math.max(0, startBeat);
1458
1633
  this.hits.push({
1459
1634
  pitch,
1460
- startBeat: Math.max(0, startBeat),
1635
+ startBeat: beat,
1461
1636
  durationBeat,
1462
1637
  velocity: clampVel(velocity)
1463
1638
  });
1639
+ this.#onsets.add(onsetKey(pitch, beat));
1640
+ }
1641
+ /** Whether a hit of this pitch already starts exactly at `startBeat`. */
1642
+ hasOnset(pitch, startBeat) {
1643
+ return this.#onsets.has(onsetKey(pitch, startBeat));
1464
1644
  }
1465
1645
  /** True when a crash already sits within a 16th of `startBeat`. */
1466
1646
  hasCrashNear(startBeat) {
1467
- return this.hits.some(
1468
- (h) => h.pitch === GM.CRASH && h.startBeat >= startBeat && h.startBeat < startBeat + 0.25
1469
- );
1647
+ const steps = Math.round(0.25 * ONSET_KEY_SCALE);
1648
+ for (let step = 0; step < steps; step += 1) {
1649
+ if (this.#onsets.has(onsetKey(GM.CRASH, startBeat + step / ONSET_KEY_SCALE))) {
1650
+ return true;
1651
+ }
1652
+ }
1653
+ return false;
1470
1654
  }
1471
1655
  };
1656
+ function onsetKey(pitch, startBeat) {
1657
+ return `${pitch}:${Math.round(startBeat * ONSET_KEY_SCALE)}`;
1658
+ }
1472
1659
 
1473
1660
  // src/generate/drums/kick.ts
1474
1661
  var EMPTY = {
@@ -1487,19 +1674,6 @@ function isInPreChorusLift(section, bar, sectionBars, nextIsChorus) {
1487
1674
  }
1488
1675
  return bar >= sectionBars - 2;
1489
1676
  }
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
1677
  function getKickPattern(section, style, bar, rng) {
1504
1678
  const p = { ...EMPTY };
1505
1679
  if (section === "outro") {
@@ -1621,7 +1795,7 @@ function getPercussionConfig(mood, section) {
1621
1795
  };
1622
1796
  }
1623
1797
  function generateAuxPercussionForBar(track, barStart, config, role, densityMult, rng, bpm) {
1624
- if (role === "minimal" || role === "fxOnly") {
1798
+ if (role === "minimal") {
1625
1799
  return;
1626
1800
  }
1627
1801
  if (config.tambourine) {
@@ -1669,15 +1843,31 @@ function generateAuxPercussionForBar(track, barStart, config, role, densityMult,
1669
1843
  }
1670
1844
 
1671
1845
  // src/generate/drums/index.ts
1846
+ var MAX_HITS_PER_BAR = 128;
1672
1847
  function generateDrums(opts) {
1848
+ assertPositiveInt(opts.bars, "drum bars");
1849
+ assertRange(opts.bpm, Number.MIN_VALUE, 1e3, "drum bpm");
1850
+ assertRange(opts.density, 0, 1, "drum density");
1851
+ assertGenerationBudget(opts.bars * MAX_HITS_PER_BAR, "drum hits");
1852
+ if (opts.euclideanKick) {
1853
+ const steps = opts.euclideanKick.steps ?? 16;
1854
+ assertInteger(steps, "euclidean steps", 1, 16);
1855
+ assertInteger(opts.euclideanKick.pulses, "euclidean pulses");
1856
+ assertInteger(opts.euclideanKick.rotation ?? 0, "euclidean rotation");
1857
+ }
1858
+ const publicStyle = assertOneOf(opts.style, GROOVE_STYLES, "drum style");
1859
+ const publicSection = assertOneOf(opts.section, PUBLIC_SECTIONS, "drum section");
1860
+ if (opts.nextSection !== void 0) {
1861
+ assertOneOf(opts.nextSection, PUBLIC_SECTIONS, "drum nextSection");
1862
+ }
1673
1863
  const track = new HitList();
1674
1864
  const rng = createRng(opts.seed ?? 0);
1675
- const mapping = mapStyle(opts.style);
1865
+ const mapping = mapStyle(publicStyle);
1676
1866
  const style = mapping.style;
1677
- const feel = opts.feel ?? mapping.feel;
1678
- const role = opts.role ?? "full";
1867
+ const feel = opts.feel === void 0 ? mapping.feel : assertOneOf(opts.feel, DRUM_FEELS, "drum feel");
1868
+ const role = opts.role === void 0 ? "full" : assertOneOf(opts.role, DRUM_ROLES, "drum role");
1679
1869
  const playMainVoices = role !== "fxOnly";
1680
- const section = mapSection(opts.section);
1870
+ const section = mapSection(publicSection);
1681
1871
  const backingDensity = mapDensity(opts.density);
1682
1872
  const swingAmount = feelSwingAmount(feel);
1683
1873
  let densityMult = sectionDensityMultiplier(section);
@@ -1706,15 +1896,8 @@ function generateDrums(opts) {
1706
1896
  const percMood = percMoodCategory(style);
1707
1897
  const nextSection = opts.nextSection ? mapSection(opts.nextSection) : section;
1708
1898
  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;
1899
+ const euclidSteps = opts.euclideanKick?.steps ?? 16;
1900
+ const euclidKick = opts.euclideanKick ? euclideanRhythm(opts.euclideanKick.pulses, euclidSteps, opts.euclideanKick.rotation ?? 0) : void 0;
1718
1901
  const reuseSectionKick = (section === "b" || section === "chorus") && style !== "sparse";
1719
1902
  let sectionKick;
1720
1903
  for (let bar = 0; bar < opts.bars; bar += 1) {
@@ -1730,15 +1913,13 @@ function generateDrums(opts) {
1730
1913
  barHasOpenHh = !track.hasCrashNear(barStart + openHhBeatIndex);
1731
1914
  }
1732
1915
  let kick;
1733
- if (euclidKick) {
1734
- kick = euclidKick;
1735
- } else if (reuseSectionKick) {
1916
+ if (!euclidKick && reuseSectionKick) {
1736
1917
  sectionKick ??= getKickPattern(section, style, 0, rng);
1737
1918
  kick = sectionKick;
1738
- } else {
1919
+ } else if (!euclidKick) {
1739
1920
  kick = getKickPattern(section, style, bar, rng);
1740
1921
  }
1741
- const inLift = isInPreChorusLift(section, bar, opts.bars, section === "b");
1922
+ const inLift = isInPreChorusLift(section, bar, opts.bars, nextSection === "chorus");
1742
1923
  let currentFill = "snareRoll";
1743
1924
  for (let beat = 0; beat < 4; beat += 1) {
1744
1925
  const beatTick = barStart + beat;
@@ -1776,7 +1957,23 @@ function generateDrums(opts) {
1776
1957
  if (inLift) {
1777
1958
  generatePreChorusBuildup(ctx, sec, isLastBar);
1778
1959
  }
1779
- generateKickForBeat(ctx, sec, kick);
1960
+ if (euclidKick && !inLift) {
1961
+ const stepLength = 4 / euclidSteps;
1962
+ for (let step = 0; step < euclidKick.length; step += 1) {
1963
+ const onset = step * stepLength;
1964
+ if (euclidKick[step] && Math.floor(onset) === beat) {
1965
+ const wholeBeat = Math.abs(onset - Math.round(onset)) < 1e-9;
1966
+ track.add(
1967
+ GM.BD,
1968
+ barStart + onset,
1969
+ Math.min(0.5, stepLength),
1970
+ velocity * (wholeBeat ? 1 : 0.85)
1971
+ );
1972
+ }
1973
+ }
1974
+ } else if (kick) {
1975
+ generateKickForBeat(ctx, sec, kick);
1976
+ }
1780
1977
  generateSnareForBeat(ctx, sec, section === "intro" && bar === 0);
1781
1978
  if (sec.useGhostNotes && !inLift) {
1782
1979
  generateGhostNotesForBeat(ctx, sec);
@@ -1787,27 +1984,36 @@ function generateDrums(opts) {
1787
1984
  if (sec.useFootHh && shouldPlayHiHat(role)) {
1788
1985
  for (let fhhBeat = 0; fhhBeat < 4; fhhBeat += 2) {
1789
1986
  const fhhTick = barStart + fhhBeat;
1790
- const occupied = track.hits.some((h) => h.pitch === GM.FHH && h.startBeat === fhhTick);
1791
- if (!occupied) {
1987
+ if (!track.hasOnset(GM.FHH, fhhTick)) {
1792
1988
  track.add(GM.FHH, fhhTick, 0.5, footHiHatVelocity(rng));
1793
1989
  }
1794
1990
  }
1795
1991
  }
1992
+ let percussionConfig = getPercussionConfig(percMood, section);
1993
+ if (role === "fxOnly" && !percussionConfig.tambourine && !percussionConfig.shaker && !percussionConfig.handclap) {
1994
+ percussionConfig = {
1995
+ tambourine: false,
1996
+ shaker: true,
1997
+ handclap: false,
1998
+ shaker16th: false
1999
+ };
2000
+ }
1796
2001
  generateAuxPercussionForBar(
1797
2002
  track,
1798
2003
  barStart,
1799
- getPercussionConfig(percMood, section),
2004
+ percussionConfig,
1800
2005
  role,
1801
2006
  densityMult,
1802
2007
  rng,
1803
2008
  opts.bpm
1804
2009
  );
1805
2010
  }
1806
- return track.hits;
2011
+ return [...track.hits].sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);
1807
2012
  }
1808
2013
 
1809
2014
  // src/generate/groove/index.ts
1810
2015
  var DEFAULT_TS3 = { numerator: 4, denominator: 4 };
2016
+ var DEFAULT_GROOVE_SUBDIVISION = 4;
1811
2017
  var DEFAULT_TIMING = 0.02;
1812
2018
  var DEFAULT_VELOCITY_JITTER = 8;
1813
2019
  var DEFAULT_ACCENT = 12;
@@ -1817,12 +2023,19 @@ var MIN_VELOCITY = 1;
1817
2023
  var MAX_VELOCITY = 127;
1818
2024
  function humanize(events, opts = {}) {
1819
2025
  const ts = opts.ts ?? DEFAULT_TS3;
2026
+ assertTimeSignature(ts);
2027
+ assertNoteEvents(events, "humanize events", { allowNonPositiveDuration: true });
2028
+ const sounding = dropSilentNotes(events);
1820
2029
  const timing = opts.timing ?? DEFAULT_TIMING;
1821
2030
  const velocityJitter = opts.velocity ?? DEFAULT_VELOCITY_JITTER;
1822
2031
  const accent = opts.accent ?? DEFAULT_ACCENT;
1823
2032
  const baseVelocity = opts.baseVelocity ?? DEFAULT_BASE_VELOCITY;
2033
+ assertRange(timing, 0, Number.MAX_SAFE_INTEGER, "humanize timing");
2034
+ assertRange(velocityJitter, 0, 127, "humanize velocity jitter");
2035
+ assertRange(accent, 0, 127, "humanize accent");
2036
+ assertRange(baseVelocity, 0, 127, "humanize base velocity");
1824
2037
  const rng = createRng(opts.seed ?? 0);
1825
- return events.map((event) => {
2038
+ return sounding.map((event) => {
1826
2039
  const timingOffset = rng.float(-timing, timing);
1827
2040
  const startBeat = Math.max(0, event.startBeat + timingOffset);
1828
2041
  const weight = metricWeight(event.startBeat, ts);
@@ -1852,14 +2065,18 @@ function quantizeToGrid(beat, barBeats, subdivision, slotsPerBar) {
1852
2065
  slotIndex
1853
2066
  };
1854
2067
  }
1855
- function extractGrooveTemplate(events, ts, subdivision) {
2068
+ function extractGrooveTemplate(events, ts, subdivision = DEFAULT_GROOVE_SUBDIVISION) {
2069
+ assertTimeSignature(ts);
2070
+ assertNoteEvents(events, "groove source events", { allowNonPositiveDuration: true });
2071
+ assertPositiveInt(subdivision, "groove subdivision");
1856
2072
  const barBeats = beatsPerBar(ts);
1857
2073
  const slotsPerBar = Math.round(barBeats * subdivision);
2074
+ assertGenerationBudget(slotsPerBar, "groove template slots");
1858
2075
  const offsetSums = new Array(slotsPerBar).fill(0);
1859
2076
  const velocitySums = new Array(slotsPerBar).fill(0);
1860
2077
  const counts = new Array(slotsPerBar).fill(0);
1861
2078
  const velocityCounts = new Array(slotsPerBar).fill(0);
1862
- for (const event of events) {
2079
+ for (const event of dropSilentNotes(events)) {
1863
2080
  const { quantizedBeat, slotIndex } = quantizeToGrid(
1864
2081
  event.startBeat,
1865
2082
  barBeats,
@@ -1889,13 +2106,21 @@ function extractGrooveTemplate(events, ts, subdivision) {
1889
2106
  return { subdivision, slotsPerBar, slots, ts };
1890
2107
  }
1891
2108
  function applyGrooveTemplate(events, template, ts) {
2109
+ assertTimeSignature(ts);
2110
+ assertNoteEvents(events, "groove target events", { allowNonPositiveDuration: true });
2111
+ assertPositiveInt(template.subdivision, "template subdivision");
2112
+ assertPositiveInt(template.slotsPerBar, "template slotsPerBar");
2113
+ assertGenerationBudget(template.slots.length, "template slots");
2114
+ if (template.slots.length !== template.slotsPerBar) {
2115
+ throw new InvalidInputError("template slots length must equal slotsPerBar");
2116
+ }
1892
2117
  if (template.ts && (template.ts.numerator !== ts.numerator || template.ts.denominator !== ts.denominator)) {
1893
- throw new Error(
2118
+ throw new InvalidInputError(
1894
2119
  `Groove template meter ${formatTimeSignature(template.ts)} does not match apply-time meter ${formatTimeSignature(ts)}`
1895
2120
  );
1896
2121
  }
1897
2122
  const barBeats = beatsPerBar(ts);
1898
- return events.map((event) => {
2123
+ return dropSilentNotes(events).map((event) => {
1899
2124
  const { quantizedBeat, slotIndex } = quantizeToGrid(
1900
2125
  event.startBeat,
1901
2126
  barBeats,
@@ -1915,25 +2140,20 @@ function applyGrooveTemplate(events, template, ts) {
1915
2140
  }
1916
2141
 
1917
2142
  // src/generate/harmonize/index.ts
1918
- var FALLBACK = { rootPc: 0, quality: "maj", secondaryDominant: false, base: 0 };
2143
+ var FALLBACK = {
2144
+ rootPc: 0,
2145
+ quality: "maj",
2146
+ secondaryDominant: false,
2147
+ base: 0,
2148
+ pcs: chordPitchClasses(makeChord(0, "maj"))
2149
+ };
2150
+ var DEFAULT_HARMONIC_RHYTHM = 2;
2151
+ var DEFAULT_PLACEMENT = { transposeSearch: false, octaveSearch: false };
1919
2152
  var DEFAULT_METER = { numerator: 4, denominator: 4 };
1920
2153
  var COMFORT_LOW = 55;
1921
2154
  var COMFORT_HIGH = 79;
1922
2155
  var TESSITURA_WEIGHT = 1e-3;
1923
- var TIE_BREAK_JITTER = 1e-6;
1924
- function pitchClass2(pitch) {
1925
- return (Math.trunc(pitch) % 12 + 12) % 12;
1926
- }
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
- }
2156
+ var TIE_BREAK_JITTER2 = 1e-6;
1937
2157
  function inferKey(melody) {
1938
2158
  let best = majorKey(0);
1939
2159
  let bestScore = Number.NEGATIVE_INFINITY;
@@ -1942,13 +2162,13 @@ function inferKey(melody) {
1942
2162
  candidates.push(majorKey(tonic), minorKey(tonic));
1943
2163
  }
1944
2164
  for (const key of candidates) {
1945
- const tonic = pitchClass2(key.rootPc);
2165
+ const tonic = pitchClassOf(key.rootPc);
1946
2166
  const isMinor = key.modeMask12 === NATURAL_MINOR_MASK;
1947
2167
  const leadingTone = (tonic + 11) % 12;
1948
2168
  let score = 0;
1949
2169
  for (const n of melody) {
1950
2170
  const w = Math.max(0.25, n.durationBeat);
1951
- const pc = pitchClass2(n.pitch);
2171
+ const pc = pitchClassOf(n.pitch);
1952
2172
  if (isScaleTone(n.pitch, key) || isMinor && pc === leadingTone) {
1953
2173
  score += w;
1954
2174
  }
@@ -1965,50 +2185,59 @@ function inferKey(melody) {
1965
2185
  }
1966
2186
  function buildCandidates(key, reharmonize) {
1967
2187
  const tones = scaleTonesInDegreeOrder(key);
1968
- const candidates = tones.map((rootPc, degree) => ({
1969
- rootPc,
1970
- quality: diatonicTriad(degree, key).quality,
1971
- degree,
1972
- secondaryDominant: false,
1973
- base: 0
1974
- }));
1975
- const tonic = tones[0] ?? 0;
2188
+ const candidates = tones.map((rootPc, degree) => {
2189
+ const quality = diatonicTriad(degree, key).quality;
2190
+ return {
2191
+ rootPc,
2192
+ quality,
2193
+ degree,
2194
+ secondaryDominant: false,
2195
+ base: 0,
2196
+ pcs: chordPitchClasses(makeChord(rootPc, quality))
2197
+ };
2198
+ });
1976
2199
  if (reharmonize !== "diatonic") {
1977
2200
  for (const target of [1, 3, 4, 5]) {
1978
2201
  const targetRoot = tones[target] ?? 0;
2202
+ const rootPc = (targetRoot + 7) % 12;
1979
2203
  candidates.push({
1980
- rootPc: (targetRoot + 7) % 12,
2204
+ rootPc,
1981
2205
  quality: "dom7",
1982
2206
  secondaryDominant: true,
1983
2207
  targetDegree: target,
1984
- base: 1.5
2208
+ base: 1.5,
2209
+ pcs: chordPitchClasses(makeChord(rootPc, "dom7"))
1985
2210
  });
1986
2211
  }
1987
2212
  }
1988
2213
  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
- });
2214
+ const parallel = parallelKey(key);
2215
+ const parallelTones = scaleTonesInDegreeOrder(parallel);
2216
+ for (let degree = 0; degree < parallelTones.length; degree += 1) {
2217
+ const chord = diatonicTriad(degree, parallel);
2218
+ if (!isDiatonic(chord, key)) {
2219
+ candidates.push({
2220
+ rootPc: chord.rootPc,
2221
+ quality: chord.quality,
2222
+ secondaryDominant: false,
2223
+ base: 1.25,
2224
+ pcs: chordPitchClasses(chord)
2225
+ });
2226
+ }
2227
+ }
2007
2228
  }
2008
- return candidates;
2229
+ const seen = /* @__PURE__ */ new Set();
2230
+ return candidates.filter((candidate) => {
2231
+ const id = `${candidate.rootPc}:${candidate.quality}`;
2232
+ if (seen.has(id)) {
2233
+ return false;
2234
+ }
2235
+ seen.add(id);
2236
+ return true;
2237
+ });
2009
2238
  }
2010
- function emissionCost(seg, cand, melody, key) {
2011
- const pcs = chordPitchClasses(makeChord(cand.rootPc, cand.quality));
2239
+ function emissionCost(seg, cand, melody, key, ts) {
2240
+ const pcs = cand.pcs;
2012
2241
  let cost = cand.base;
2013
2242
  for (const idx of seg.noteIndices) {
2014
2243
  const note = melody[idx];
@@ -2021,10 +2250,10 @@ function emissionCost(seg, cand, melody, key) {
2021
2250
  continue;
2022
2251
  }
2023
2252
  const w = Math.max(0.25, overlap);
2024
- if (pcs.includes(pitchClass2(note.pitch))) {
2253
+ if (pcs.includes(pitchClassOf(note.pitch))) {
2025
2254
  continue;
2026
2255
  }
2027
- const strong = isStrongBeat(overlapStart, DEFAULT_METER);
2256
+ const strong = isStrongBeat(overlapStart, ts);
2028
2257
  cost += (strong ? 10 : 2) * w;
2029
2258
  if (!isScaleTone(note.pitch, key)) {
2030
2259
  cost += w;
@@ -2058,13 +2287,13 @@ function transitionCost(prev, cur, tonicPc) {
2058
2287
  cost += dist * 0.3;
2059
2288
  return cost;
2060
2289
  }
2061
- function harmonizeOnce(melody, key, candidates, segments, jitter) {
2062
- const tonicPc = pitchClass2(key.rootPc);
2290
+ function harmonizeOnce(melody, key, candidates, segments, jitter, ts) {
2291
+ const tonicPc = pitchClassOf(key.rootPc);
2063
2292
  const n = candidates.length;
2064
2293
  const candAt = (i) => candidates[i] ?? FALLBACK;
2065
2294
  const seg0 = segments[0];
2066
2295
  let dp = candidates.map(
2067
- (c, ci) => (seg0 ? emissionCost(seg0, c, melody, key) : 0) + (jitter[ci] ?? 0)
2296
+ (c, ci) => (seg0 ? emissionCost(seg0, c, melody, key, ts) : 0) + (jitter[ci] ?? 0)
2068
2297
  );
2069
2298
  const back = [];
2070
2299
  for (let s = 1; s < segments.length; s += 1) {
@@ -2084,7 +2313,7 @@ function harmonizeOnce(melody, key, candidates, segments, jitter) {
2084
2313
  bestPrev = p;
2085
2314
  }
2086
2315
  }
2087
- next[c] = best + emissionCost(seg, candAt(c), melody, key) + (jitter[c] ?? 0);
2316
+ next[c] = best + emissionCost(seg, candAt(c), melody, key, ts) + (jitter[c] ?? 0);
2088
2317
  ptr[c] = bestPrev;
2089
2318
  }
2090
2319
  dp = next;
@@ -2109,30 +2338,56 @@ function harmonizeOnce(melody, key, candidates, segments, jitter) {
2109
2338
  return { cost: bestCost, path };
2110
2339
  }
2111
2340
  function harmonizeMelody(opts) {
2112
- const key = opts.key === "infer" ? inferKey(opts.melody) : opts.key;
2113
- const candidates = buildCandidates(key, opts.reharmonize);
2341
+ const noteIndex = createNoteEventIndex(opts.melody, { allowNonPositiveDuration: true });
2342
+ const ts = opts.ts ?? DEFAULT_METER;
2343
+ assertTimeSignature(ts);
2344
+ const requestedKey = opts.key ?? "infer";
2345
+ const key = requestedKey === "infer" ? inferKey(opts.melody) : requestedKey;
2346
+ const placement = opts.placement ?? DEFAULT_PLACEMENT;
2347
+ if (noteIndex.notes.every((indexed) => indexed.note.durationBeat <= 0)) {
2348
+ return { transposeSemitones: 0, key, chords: [], melodyRoles: [] };
2349
+ }
2350
+ const candidates = buildCandidates(key, opts.reharmonize ?? "diatonic");
2114
2351
  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);
2352
+ const rng = createRng(opts.seed ?? 0);
2353
+ const jitter = candidates.map(() => rng.next() * TIE_BREAK_JITTER2);
2354
+ const melodyEnd = noteIndex.notes.reduce((m, n) => Math.max(m, n.endBeat), 0);
2355
+ const hr = assertRange(
2356
+ opts.harmonicRhythm ?? DEFAULT_HARMONIC_RHYTHM,
2357
+ Number.MIN_VALUE,
2358
+ Number.MAX_SAFE_INTEGER,
2359
+ "harmonic rhythm"
2360
+ );
2119
2361
  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 });
2362
+ assertGenerationBudget(segCount, "harmonic segments");
2363
+ assertGenerationBudget(segCount * candidates.length * candidates.length, "harmonization search");
2364
+ const segments = Array.from({ length: segCount }, (_, s) => ({
2365
+ startBeat: s * hr,
2366
+ endBeat: (s + 1) * hr,
2367
+ noteIndices: []
2368
+ }));
2369
+ let memberships = 0;
2370
+ for (const indexed of noteIndex.notes) {
2371
+ if (indexed.note.durationBeat <= 0) {
2372
+ continue;
2373
+ }
2374
+ const first = Math.max(0, Math.floor(indexed.note.startBeat / hr));
2375
+ const lastExclusive = Math.min(segCount, Math.ceil(indexed.endBeat / hr - Number.EPSILON));
2376
+ memberships += Math.max(0, lastExclusive - first);
2377
+ assertGenerationBudget(memberships, "note-to-segment memberships");
2378
+ for (let segment = first; segment < lastExclusive; segment += 1) {
2379
+ segments[segment]?.noteIndices.push(indexed.originalIndex);
2380
+ }
2126
2381
  }
2127
2382
  const transposes = [0];
2128
- if (opts.placement.transposeSearch) {
2383
+ if (placement.transposeSearch) {
2129
2384
  for (let s = -6; s <= 6; s += 1) {
2130
2385
  if (s !== 0) {
2131
2386
  transposes.push(s);
2132
2387
  }
2133
2388
  }
2134
2389
  }
2135
- if (opts.placement.octaveSearch) {
2390
+ if (placement.octaveSearch) {
2136
2391
  for (const o of [-12, 12]) {
2137
2392
  if (!transposes.includes(o)) {
2138
2393
  transposes.push(o);
@@ -2143,13 +2398,13 @@ function harmonizeMelody(opts) {
2143
2398
  let bestTs = 0;
2144
2399
  let bestPath = [];
2145
2400
  let bestMelody = opts.melody;
2146
- for (const ts of transposes) {
2147
- const shifted = opts.melody.map((n) => ({ ...n, pitch: n.pitch + ts }));
2148
- const { cost, path } = harmonizeOnce(shifted, key, candidates, segments, jitter);
2401
+ for (const shift of transposes) {
2402
+ const shifted = opts.melody.map((n) => ({ ...n, pitch: n.pitch + shift }));
2403
+ const { cost, path } = harmonizeOnce(shifted, key, candidates, segments, jitter, ts);
2149
2404
  const total = cost + tessituraCost(shifted);
2150
2405
  if (total < bestCost) {
2151
2406
  bestCost = total;
2152
- bestTs = ts;
2407
+ bestTs = shift;
2153
2408
  bestPath = path;
2154
2409
  bestMelody = shifted;
2155
2410
  }
@@ -2169,20 +2424,23 @@ function harmonizeMelody(opts) {
2169
2424
  }
2170
2425
  return chord;
2171
2426
  });
2172
- const melodyRoles = bestMelody.map((note, noteIndex) => {
2427
+ const melodyRoles = bestMelody.map((note, noteIndex2) => {
2173
2428
  const segIdx = Math.min(segments.length - 1, Math.max(0, Math.floor(note.startBeat / hr)));
2174
2429
  const cand = candAt(bestPath[segIdx] ?? 0);
2175
2430
  const chord = makeChord(cand.rootPc, cand.quality);
2176
- return { noteIndex, role: roleOf(note.pitch, chord).role };
2431
+ return { noteIndex: noteIndex2, role: roleOf(note.pitch, chord).role };
2177
2432
  });
2178
2433
  return { transposeSemitones: bestTs, key, chords, melodyRoles };
2179
2434
  }
2180
2435
 
2181
2436
  // src/generate/motif/index.ts
2182
- function pitchClass3(pitch) {
2183
- return (Math.trunc(pitch) % 12 + 12) % 12;
2184
- }
2185
2437
  var MIN_TILE_SPAN = 1 / 256;
2438
+ var DEFAULT_TS4 = { numerator: 4, denominator: 4 };
2439
+ var EPS3 = 1e-9;
2440
+ function isDownbeat(startBeat, barBeats) {
2441
+ const bar = startBeat / barBeats;
2442
+ return Math.abs(bar - Math.round(bar)) < EPS3;
2443
+ }
2186
2444
  function upScaleTone(pitch, key) {
2187
2445
  for (let d = 1; d <= 12; d += 1) {
2188
2446
  if (isScaleTone(pitch + d, key)) {
@@ -2210,39 +2468,40 @@ function stepDiatonic(pitch, degrees, key) {
2210
2468
  function nearestChordTone(pitch, chord) {
2211
2469
  const pcs = chordPitchClasses(chord);
2212
2470
  for (let d = 0; d <= 12; d += 1) {
2213
- if (pcs.includes(pitchClass3(pitch - d))) {
2471
+ if (pcs.includes(pitchClassOf(pitch - d))) {
2214
2472
  return pitch - d;
2215
2473
  }
2216
- if (pcs.includes(pitchClass3(pitch + d))) {
2474
+ if (pcs.includes(pitchClassOf(pitch + d))) {
2217
2475
  return pitch + d;
2218
2476
  }
2219
2477
  }
2220
2478
  return pitch;
2221
2479
  }
2222
2480
  function cellOrigin(notes) {
2223
- return notes.length === 0 ? 0 : Math.min(...notes.map((n) => n.startBeat));
2481
+ let origin = Number.POSITIVE_INFINITY;
2482
+ for (const note of notes) {
2483
+ origin = Math.min(origin, note.startBeat);
2484
+ }
2485
+ return Number.isFinite(origin) ? origin : 0;
2486
+ }
2487
+ function cellEnd(notes) {
2488
+ let end = Number.NEGATIVE_INFINITY;
2489
+ for (const note of notes) {
2490
+ end = Math.max(end, note.startBeat + note.durationBeat);
2491
+ }
2492
+ return Number.isFinite(end) ? end : 0;
2224
2493
  }
2225
2494
  function cellSpan(cell) {
2226
2495
  if (cell.notes.length === 0) {
2227
2496
  return 0;
2228
2497
  }
2229
2498
  const start = cellOrigin(cell.notes);
2230
- const end = Math.max(...cell.notes.map((n) => n.startBeat + n.durationBeat));
2499
+ const end = cellEnd(cell.notes);
2231
2500
  return end - start;
2232
2501
  }
2233
2502
  function clone(cell) {
2234
2503
  return { notes: cell.notes.map((n) => ({ ...n })) };
2235
2504
  }
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
2505
  function contourOffsets(contour, count) {
2247
2506
  const offsets = [];
2248
2507
  for (let i = 0; i < count; i += 1) {
@@ -2265,30 +2524,59 @@ function contourOffsets(contour, count) {
2265
2524
  }
2266
2525
  function generateMotif(opts) {
2267
2526
  const contour = opts.contour ?? "arch";
2268
- const bars = Math.max(1, Math.trunc(opts.bars));
2269
- const totalBeats = bars * 4;
2527
+ const bars = assertPositiveInt(opts.bars, "motif bars");
2528
+ const barBeats = beatsPerBar(opts.ts ?? DEFAULT_TS4);
2529
+ const totalBeats = bars * barBeats;
2270
2530
  const noteCount = Math.max(3, bars * 2);
2531
+ assertGenerationBudget(noteCount, "motif notes");
2271
2532
  const beatsPerNote = totalBeats / noteCount;
2272
- const rng = mulberry322(opts.seed ?? 0);
2273
- const jitterProb = Math.min(1, Math.max(0, opts.jitter ?? 0));
2274
- const tonic = pitchClass3(opts.key.rootPc) + 60;
2533
+ const rng = createRng(opts.seed ?? 0);
2534
+ const requestedJitter = assertFiniteNumber(opts.jitter ?? 0, "motif jitter");
2535
+ const jitterProb = Math.min(1, Math.max(0, requestedJitter));
2536
+ const tonic = pitchClassOf(opts.key.rootPc) + 60;
2275
2537
  const offsets = contourOffsets(contour, noteCount);
2276
2538
  const notes = [];
2277
2539
  for (let i = 0; i < noteCount; i += 1) {
2278
2540
  let jitter = 0;
2279
- if (jitterProb > 0 && rng() < jitterProb) {
2280
- jitter = rng() < 0.5 ? 1 : -1;
2541
+ if (jitterProb > 0 && rng.next() < jitterProb) {
2542
+ jitter = rng.next() < 0.5 ? 1 : -1;
2281
2543
  }
2282
2544
  let pitch = stepDiatonic(tonic, (offsets[i] ?? 0) + jitter, opts.key);
2283
2545
  const startBeat = i * beatsPerNote;
2284
- if (opts.chord && startBeat % 4 === 0) {
2546
+ if (opts.chord && isDownbeat(startBeat, barBeats)) {
2285
2547
  pitch = nearestChordTone(pitch, opts.chord);
2286
2548
  }
2287
2549
  notes.push({ pitch, startBeat, durationBeat: beatsPerNote });
2288
2550
  }
2289
2551
  return { notes };
2290
2552
  }
2553
+ function motifToNoteEvents(cell) {
2554
+ return cell.notes.map((note) => ({ ...note }));
2555
+ }
2291
2556
  function transformMotif(cell, t, amount, key) {
2557
+ return assertInRange(transformUnchecked(cell, t, amount, key));
2558
+ }
2559
+ function assertInRange(cell) {
2560
+ for (const note of cell.notes) {
2561
+ if (note.pitch < 0 || note.pitch > 127) {
2562
+ throw new InvalidInputError(
2563
+ `transformed motif pitch ${note.pitch} is outside the MIDI range 0..127`
2564
+ );
2565
+ }
2566
+ }
2567
+ return cell;
2568
+ }
2569
+ function transformUnchecked(cell, t, amount, key) {
2570
+ assertNoteEvents(cell.notes, "motif notes");
2571
+ if (amount !== void 0) {
2572
+ assertFiniteNumber(amount, "motif transform amount");
2573
+ }
2574
+ if ((t === "transposeDiatonic" || t === "sequence") && amount !== void 0) {
2575
+ assertInteger(amount, "diatonic transform amount");
2576
+ }
2577
+ if ((t === "augment" || t === "diminish") && amount !== void 0 && amount <= 0) {
2578
+ throw new InvalidInputError("time transform amount must be positive");
2579
+ }
2292
2580
  const notes = cell.notes;
2293
2581
  switch (t) {
2294
2582
  case "transposeChromatic": {
@@ -2315,7 +2603,7 @@ function transformMotif(cell, t, amount, key) {
2315
2603
  return { notes: [] };
2316
2604
  }
2317
2605
  const start = cellOrigin(notes);
2318
- const end = Math.max(...notes.map((n) => n.startBeat + n.durationBeat));
2606
+ const end = cellEnd(notes);
2319
2607
  return {
2320
2608
  notes: notes.map((n) => ({
2321
2609
  pitch: n.pitch,
@@ -2347,9 +2635,11 @@ function scaleTime(cell, factor) {
2347
2635
  }))
2348
2636
  };
2349
2637
  }
2350
- function developMotif(cell, timeline, key, bars) {
2638
+ function developMotif(cell, timeline, key, bars, ts = DEFAULT_TS4) {
2639
+ assertPositiveInt(bars, "development bars");
2640
+ assertNoteEvents(cell.notes, "motif notes");
2351
2641
  const span = cellSpan(cell);
2352
- const totalBeats = Math.max(1, Math.trunc(bars)) * 4;
2642
+ const totalBeats = bars * beatsPerBar(ts);
2353
2643
  const origin = cellOrigin(cell.notes);
2354
2644
  const out = [];
2355
2645
  if (span <= 0) {
@@ -2357,6 +2647,7 @@ function developMotif(cell, timeline, key, bars) {
2357
2647
  }
2358
2648
  const tileSpan = Math.max(span, MIN_TILE_SPAN);
2359
2649
  const tileCount = Math.ceil(totalBeats / tileSpan);
2650
+ assertGenerationBudget(tileCount * cell.notes.length, "developed motif notes");
2360
2651
  for (let k = 0; k < tileCount; k += 1) {
2361
2652
  const offset = k * tileSpan;
2362
2653
  for (const n of cell.notes) {
@@ -2367,7 +2658,7 @@ function developMotif(cell, timeline, key, bars) {
2367
2658
  const chord = timeline.at(startBeat);
2368
2659
  let pitch = n.pitch;
2369
2660
  if (chord) {
2370
- const isMember = chordPitchClasses(chord).includes(pitchClass3(pitch));
2661
+ const isMember = chordPitchClasses(chord).includes(pitchClassOf(pitch));
2371
2662
  pitch = isMember ? pitch : nearestChordTone(pitch, chord);
2372
2663
  } else {
2373
2664
  pitch = nearestScaleTone(pitch, key);
@@ -2379,6 +2670,20 @@ function developMotif(cell, timeline, key, bars) {
2379
2670
  }
2380
2671
 
2381
2672
  // src/generate/progression/index.ts
2673
+ var BORROWED_DEGREES = {
2674
+ /** Flat submediant: bVI. */
2675
+ bVI: 8,
2676
+ /** Flat subtonic: bVII. */
2677
+ bVII: 10,
2678
+ /** Flat mediant: bIII. */
2679
+ bIII: 11,
2680
+ /** Minor subdominant borrowed from the parallel minor: iv. */
2681
+ iv: 12,
2682
+ /** Neapolitan: bII. */
2683
+ bII: 13,
2684
+ /** Sharp subdominant, the diminished #IV. */
2685
+ sharpIV: 14
2686
+ };
2382
2687
  var PRESETS = [
2383
2688
  {
2384
2689
  id: "fourChordPop",
@@ -2536,18 +2841,12 @@ var PRESETS = [
2536
2841
  }
2537
2842
  ];
2538
2843
  var BORROWED_OFFSET = {
2539
- 8: 8,
2540
- // bVI
2541
- 10: 10,
2542
- // bVII
2543
- 11: 3,
2544
- // bIII
2545
- 12: 5,
2546
- // iv
2547
- 13: 1,
2548
- // bII
2549
- 14: 6
2550
- // #IV
2844
+ [BORROWED_DEGREES.bVI]: 8,
2845
+ [BORROWED_DEGREES.bVII]: 10,
2846
+ [BORROWED_DEGREES.bIII]: 3,
2847
+ [BORROWED_DEGREES.iv]: 5,
2848
+ [BORROWED_DEGREES.bII]: 1,
2849
+ [BORROWED_DEGREES.sharpIV]: 6
2551
2850
  };
2552
2851
  function progressions() {
2553
2852
  return PRESETS.map((p) => ({ ...p, degrees: [...p.degrees], styles: [...p.styles] }));
@@ -2555,16 +2854,6 @@ function progressions() {
2555
2854
  function progressionsByStyle(style) {
2556
2855
  return progressions().filter((p) => p.styles.includes(style));
2557
2856
  }
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
2857
  function degreeToRootPc(degree, key) {
2569
2858
  if (degree >= 0 && degree <= 6) {
2570
2859
  const tones = scaleTonesInDegreeOrder(key);
@@ -2585,38 +2874,83 @@ function autoQuality(degree, key) {
2585
2874
  }
2586
2875
  return "maj";
2587
2876
  }
2877
+ function resolveCycle(degrees, key, ext) {
2878
+ const steps = [];
2879
+ for (const degree of degrees) {
2880
+ const step = {
2881
+ source: degree,
2882
+ rootPc: degreeToRootPc(degree, key),
2883
+ quality: ext !== void 0 && ext !== "auto" ? ext : autoQuality(degree, key)
2884
+ };
2885
+ if (degree >= 0 && degree <= 6) {
2886
+ step.degree = degree;
2887
+ }
2888
+ const previous = steps[steps.length - 1];
2889
+ if (previous !== void 0 && previous.source !== step.source && sameStep(previous, step)) {
2890
+ continue;
2891
+ }
2892
+ steps.push(step);
2893
+ }
2894
+ const first = steps[0];
2895
+ const last = steps[steps.length - 1];
2896
+ if (steps.length > 1 && first !== void 0 && last !== void 0 && first.source !== last.source && sameStep(first, last)) {
2897
+ steps.pop();
2898
+ }
2899
+ return steps.length > 0 ? steps : [{ source: 0, degree: 0, rootPc: degreeToRootPc(0, key), quality: "maj" }];
2900
+ }
2901
+ function sameStep(a, b) {
2902
+ return a.rootPc === b.rootPc && a.quality === b.quality;
2903
+ }
2904
+ function pickProgressionPreset(style, seed = 0) {
2905
+ const pool = PRESETS.filter((preset) => preset.styles.includes(style));
2906
+ if (pool.length === 0) {
2907
+ throw new InvalidInputError(`Unknown progression style: ${style}`);
2908
+ }
2909
+ const rng = createRng(seed);
2910
+ const index = Math.floor(rng.next() * pool.length) % pool.length;
2911
+ return pool[index] ?? PRESETS[0];
2912
+ }
2588
2913
  function generateProgression(opts) {
2914
+ assertPositiveInt(opts.bars, "progression bars");
2915
+ assertGenerationBudget(opts.bars, "progression chords");
2589
2916
  const seed = opts.seed ?? 0;
2590
2917
  let preset;
2591
- if (opts.presetId !== void 0) {
2918
+ if (opts.preset !== void 0) {
2919
+ if (opts.preset.degrees.length === 0) {
2920
+ throw new InvalidInputError("progression preset must name at least one degree");
2921
+ }
2922
+ preset = {
2923
+ id: opts.preset.id ?? "custom",
2924
+ name: opts.preset.name ?? "Custom",
2925
+ degrees: opts.preset.degrees,
2926
+ functional: opts.preset.functional ?? "loop",
2927
+ styles: opts.preset.styles ?? [opts.style]
2928
+ };
2929
+ }
2930
+ if (preset === void 0 && opts.presetId !== void 0) {
2592
2931
  preset = PRESETS.find((p) => p.id === opts.presetId);
2593
2932
  if (preset === void 0) {
2594
- throw new Error(`Unknown progression preset: ${opts.presetId}`);
2933
+ throw new InvalidInputError(`Unknown progression preset: ${opts.presetId}`);
2595
2934
  }
2596
2935
  }
2597
- if (preset === void 0) {
2598
- const pool = PRESETS.filter((p) => p.styles.includes(opts.style));
2599
- const candidates = pool.length > 0 ? pool : PRESETS;
2600
- const rng = mulberry323(seed);
2601
- const index = Math.floor(rng() * candidates.length) % candidates.length;
2602
- preset = candidates[index] ?? PRESETS[0];
2603
- }
2604
- const degrees = preset?.degrees ?? [0];
2936
+ preset ??= pickProgressionPreset(opts.style, seed);
2937
+ const cycle = resolveCycle(preset?.degrees ?? [0], opts.key, opts.ext);
2605
2938
  const chords = [];
2606
2939
  for (let bar = 0; bar < opts.bars; bar += 1) {
2607
- const degree = degrees[bar % degrees.length] ?? 0;
2608
- const quality = opts.ext !== void 0 && opts.ext !== "auto" ? opts.ext : autoQuality(degree, opts.key);
2940
+ const step = cycle[bar % cycle.length];
2609
2941
  const chord = {
2610
- rootPc: degreeToRootPc(degree, opts.key),
2611
- quality,
2612
- startBeat: bar * 4,
2613
- degree: degree >= 0 && degree <= 6 ? degree : void 0
2942
+ rootPc: step?.rootPc ?? 0,
2943
+ quality: step?.quality ?? "maj",
2944
+ startBeat: bar * 4
2614
2945
  };
2946
+ if (step?.degree !== void 0) {
2947
+ chord.degree = step.degree;
2948
+ }
2615
2949
  chords.push(chord);
2616
2950
  }
2617
2951
  if (opts.reharmonize) {
2618
2952
  const tonicPc = (opts.key.rootPc % 12 + 12) % 12;
2619
- const rrng = mulberry323((seed ^ 2654435769) >>> 0);
2953
+ const rrng = createRng((seed ^ 2654435769) >>> 0);
2620
2954
  for (let i = 0; i < chords.length - 1; i += 1) {
2621
2955
  const cur = chords[i];
2622
2956
  const next = chords[i + 1];
@@ -2630,7 +2964,7 @@ function generateProgression(opts) {
2630
2964
  if (targetsTonic || alreadySecondary || isResolutionTarget) {
2631
2965
  continue;
2632
2966
  }
2633
- if (rrng() < 0.5) {
2967
+ if (rrng.next() < 0.5) {
2634
2968
  chords[i] = {
2635
2969
  rootPc: domRoot,
2636
2970
  quality: "dom7",
@@ -2644,6 +2978,17 @@ function generateProgression(opts) {
2644
2978
  }
2645
2979
 
2646
2980
  // src/generate/rhythm/index.ts
2981
+ var DEFAULT_RHYTHM_VELOCITY = 96;
2982
+ function rhythmToNoteEvents(events, pitch, velocity = DEFAULT_RHYTHM_VELOCITY) {
2983
+ assertFiniteNumber(pitch, "pitch");
2984
+ assertRange(velocity, 0, 127, "velocity");
2985
+ return events.map((event) => ({
2986
+ pitch,
2987
+ startBeat: event.position,
2988
+ durationBeat: event.duration,
2989
+ velocity
2990
+ }));
2991
+ }
2647
2992
  var DEFAULT_SUBDIVISION = 2;
2648
2993
  var DEFAULT_DENSITY = 0.5;
2649
2994
  var DEFAULT_BARS = 1;
@@ -2663,11 +3008,14 @@ function generateRhythm(ts, opts = {}) {
2663
3008
  const seed = opts.seed ?? 0;
2664
3009
  const bars = opts.bars ?? DEFAULT_BARS;
2665
3010
  const subdivision = opts.subdivision ?? DEFAULT_SUBDIVISION;
2666
- const density = Math.min(1, Math.max(0, opts.density ?? DEFAULT_DENSITY));
3011
+ const density = assertRange(opts.density ?? DEFAULT_DENSITY, 0, 1, "rhythm density");
3012
+ assertPositiveInt(bars, "rhythm bars");
3013
+ assertPositiveInt(subdivision, "rhythm subdivision");
2667
3014
  const barBeats = beatsPerBar(ts);
2668
3015
  const spanBeats = barBeats * bars;
2669
3016
  const step = 1 / subdivision;
2670
3017
  const slotCount = Math.round(spanBeats * subdivision);
3018
+ assertGenerationBudget(slotCount, "rhythm grid slots");
2671
3019
  const slotsPerBar = Math.round(barBeats * subdivision);
2672
3020
  const rng = createRng(seed);
2673
3021
  const positions = [];
@@ -2702,21 +3050,28 @@ function rhythmDensity(events, ts) {
2702
3050
  }
2703
3051
 
2704
3052
  export {
3053
+ BASS_STYLES,
2705
3054
  generateBassLine,
2706
3055
  generateCounterMelody,
3056
+ DRUM_NOTES,
3057
+ drumVoiceOf,
2707
3058
  generateDrums,
2708
3059
  humanize,
2709
3060
  extractGrooveTemplate,
2710
3061
  applyGrooveTemplate,
2711
3062
  harmonizeMelody,
2712
3063
  generateMotif,
3064
+ motifToNoteEvents,
2713
3065
  transformMotif,
2714
3066
  developMotif,
3067
+ BORROWED_DEGREES,
2715
3068
  progressions,
2716
3069
  progressionsByStyle,
3070
+ pickProgressionPreset,
2717
3071
  generateProgression,
3072
+ rhythmToNoteEvents,
2718
3073
  onsetWeightCurve,
2719
3074
  generateRhythm,
2720
3075
  rhythmDensity
2721
3076
  };
2722
- //# sourceMappingURL=chunk-R3S2LUBH.js.map
3077
+ //# sourceMappingURL=chunk-7NKDPF74.js.map