@libraz/libcantus 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +27 -65
  2. package/README_ja.md +28 -66
  3. package/dist/analyze/index.cjs +11 -7
  4. package/dist/analyze/index.cjs.map +1 -1
  5. package/dist/analyze/index.d.cts +79 -144
  6. package/dist/analyze/index.d.ts +79 -144
  7. package/dist/analyze/index.js +20 -16
  8. package/dist/{chunk-TTCLJX6D.js → chunk-57WB2EDF.js} +10 -8
  9. package/dist/chunk-57WB2EDF.js.map +1 -0
  10. package/dist/chunk-5JJ6EHEZ.cjs +457 -0
  11. package/dist/chunk-5JJ6EHEZ.cjs.map +1 -0
  12. package/dist/{chunk-AFM2ULJH.cjs → chunk-5KW7DNCJ.cjs} +1567 -1067
  13. package/dist/chunk-5KW7DNCJ.cjs.map +1 -0
  14. package/dist/chunk-7GOKQJTQ.js +145 -0
  15. package/dist/chunk-7GOKQJTQ.js.map +1 -0
  16. package/dist/{chunk-H3VORETI.js → chunk-AWEMD747.js} +487 -592
  17. package/dist/chunk-AWEMD747.js.map +1 -0
  18. package/dist/{chunk-PKFNUHDY.js → chunk-DKICR7AN.js} +1216 -611
  19. package/dist/chunk-DKICR7AN.js.map +1 -0
  20. package/dist/{chunk-INHVXP6Z.js → chunk-FPBZFNCD.js} +1159 -415
  21. package/dist/chunk-FPBZFNCD.js.map +1 -0
  22. package/dist/{chunk-6XH6QZYQ.cjs → chunk-GDKE6RQW.cjs} +1453 -848
  23. package/dist/chunk-GDKE6RQW.cjs.map +1 -0
  24. package/dist/chunk-H66ARFU5.js +363 -0
  25. package/dist/chunk-H66ARFU5.js.map +1 -0
  26. package/dist/{chunk-YH3BIKHH.js → chunk-HOHFYGKW.js} +146 -49
  27. package/dist/chunk-HOHFYGKW.js.map +1 -0
  28. package/dist/chunk-HSY2VQXZ.js +2272 -0
  29. package/dist/chunk-HSY2VQXZ.js.map +1 -0
  30. package/dist/{chunk-E3F4AHAH.cjs → chunk-J772DL3B.cjs} +221 -134
  31. package/dist/chunk-J772DL3B.cjs.map +1 -0
  32. package/dist/chunk-K6OZXIR6.js +1 -0
  33. package/dist/{chunk-VTI6FOLB.js → chunk-PQL7E5F6.js} +709 -133
  34. package/dist/chunk-PQL7E5F6.js.map +1 -0
  35. package/dist/chunk-Q6R3QAZD.cjs +3329 -0
  36. package/dist/chunk-Q6R3QAZD.cjs.map +1 -0
  37. package/dist/{chunk-MXO72PLY.cjs → chunk-QCXYKSZS.cjs} +21 -19
  38. package/dist/chunk-QCXYKSZS.cjs.map +1 -0
  39. package/dist/{chunk-VOHZB45S.js → chunk-R5VUXQMP.js} +1321 -821
  40. package/dist/chunk-R5VUXQMP.js.map +1 -0
  41. package/dist/chunk-R7B5N2JK.cjs +363 -0
  42. package/dist/chunk-R7B5N2JK.cjs.map +1 -0
  43. package/dist/{chunk-WWURE4XT.cjs → chunk-RVS4G4MR.cjs} +624 -729
  44. package/dist/chunk-RVS4G4MR.cjs.map +1 -0
  45. package/dist/chunk-TT6PKMPR.cjs +145 -0
  46. package/dist/chunk-TT6PKMPR.cjs.map +1 -0
  47. package/dist/chunk-UGLFDG7L.cjs +1 -0
  48. package/dist/chunk-UGLFDG7L.cjs.map +1 -0
  49. package/dist/chunk-VLE5R7PF.js +3329 -0
  50. package/dist/chunk-VLE5R7PF.js.map +1 -0
  51. package/dist/{chunk-FVSI36UV.js → chunk-X6RJAHII.js} +197 -110
  52. package/dist/chunk-X6RJAHII.js.map +1 -0
  53. package/dist/{chunk-Q2WGKJNN.cjs → chunk-XKSO2V5I.cjs} +699 -123
  54. package/dist/chunk-XKSO2V5I.cjs.map +1 -0
  55. package/dist/chunk-XVMJO3QF.cjs +2272 -0
  56. package/dist/chunk-XVMJO3QF.cjs.map +1 -0
  57. package/dist/{chunk-UADYAYAT.cjs → chunk-YPB3A3EY.cjs} +1309 -565
  58. package/dist/chunk-YPB3A3EY.cjs.map +1 -0
  59. package/dist/{index-C_8o--VE.d.cts → coerce-CJlgULrQ.d.cts} +210 -199
  60. package/dist/{index-szBv3496.d.ts → coerce-wwdELNYS.d.ts} +210 -199
  61. package/dist/core/index.cjs +9 -5
  62. package/dist/core/index.cjs.map +1 -1
  63. package/dist/core/index.d.cts +122 -108
  64. package/dist/core/index.d.ts +122 -108
  65. package/dist/core/index.js +32 -28
  66. package/dist/generate/index.cjs +8 -8
  67. package/dist/generate/index.d.cts +168 -84
  68. package/dist/generate/index.d.ts +168 -84
  69. package/dist/generate/index.js +7 -7
  70. package/dist/{index-CGpRv5sl.d.cts → index-9eoQs2C1.d.ts} +34 -19
  71. package/dist/{index-Dzr13q3d.d.cts → index-B3WCj_Hm.d.cts} +135 -58
  72. package/dist/{index-BO6xE9YU.d.cts → index-BUtIf2go.d.ts} +460 -254
  73. package/dist/{index-9wcMhG73.d.ts → index-BaoKVVhE.d.cts} +293 -56
  74. package/dist/{index-Chbst3Jx.d.ts → index-BpWOhkkE.d.ts} +135 -58
  75. package/dist/{index-CgHqxdps.d.ts → index-BzLNkmA7.d.cts} +196 -114
  76. package/dist/{index-CunYKLiu.d.ts → index-CBCP84rv.d.cts} +1 -1
  77. package/dist/{index-BkGmlyI3.d.cts → index-Ci3xyows.d.ts} +60 -23
  78. package/dist/{index-CCNJRG-P.d.cts → index-CnHQgqNh.d.ts} +293 -56
  79. package/dist/{index-B4o55NDr.d.cts → index-CnIdP4EZ.d.ts} +1 -1
  80. package/dist/{index-B9MNZboQ.d.ts → index-Cy15oTvh.d.cts} +52 -22
  81. package/dist/{index-B9MNZboQ.d.cts → index-Cy15oTvh.d.ts} +52 -22
  82. package/dist/{index-D57QRbVn.d.ts → index-DTsKaPaC.d.cts} +460 -254
  83. package/dist/{index-Cj49F892.d.ts → index-D_hzxD8w.d.cts} +60 -23
  84. package/dist/{index-BGFW-8P2.d.cts → index-DpC-mDlV.d.ts} +196 -114
  85. package/dist/{index-G1uwGGWQ.d.ts → index-cHvbYgJI.d.cts} +34 -19
  86. package/dist/index.cjs +42 -14
  87. package/dist/index.cjs.map +1 -1
  88. package/dist/index.d.cts +14 -14
  89. package/dist/index.d.ts +14 -14
  90. package/dist/index.js +82 -54
  91. package/dist/{masks-DYbE485J.d.ts → masks-C8TWLFIi.d.cts} +49 -2
  92. package/dist/{masks-DYbE485J.d.cts → masks-O73Rhj5d.d.ts} +49 -2
  93. package/dist/model/index.cjs +19 -12
  94. package/dist/model/index.cjs.map +1 -1
  95. package/dist/model/index.d.cts +502 -181
  96. package/dist/model/index.d.ts +502 -181
  97. package/dist/model/index.js +18 -11
  98. package/dist/{positional-Cptlj-xs.d.ts → positional-DJgs6ltD.d.ts} +85 -8
  99. package/dist/{positional-DvH_sd8u.d.cts → positional-DMZer-FM.d.cts} +85 -8
  100. package/dist/theory/index.cjs +35 -6
  101. package/dist/theory/index.cjs.map +1 -1
  102. package/dist/theory/index.d.cts +257 -62
  103. package/dist/theory/index.d.ts +257 -62
  104. package/dist/theory/index.js +41 -12
  105. package/package.json +6 -5
  106. package/dist/chunk-2LCPM3EG.js +0 -3224
  107. package/dist/chunk-2LCPM3EG.js.map +0 -1
  108. package/dist/chunk-6XH6QZYQ.cjs.map +0 -1
  109. package/dist/chunk-6XPTBE7Y.cjs +0 -49
  110. package/dist/chunk-6XPTBE7Y.cjs.map +0 -1
  111. package/dist/chunk-AFM2ULJH.cjs.map +0 -1
  112. package/dist/chunk-B6XNKWZV.cjs +0 -3224
  113. package/dist/chunk-B6XNKWZV.cjs.map +0 -1
  114. package/dist/chunk-E3F4AHAH.cjs.map +0 -1
  115. package/dist/chunk-ELCTZUJJ.js +0 -379
  116. package/dist/chunk-ELCTZUJJ.js.map +0 -1
  117. package/dist/chunk-FVSI36UV.js.map +0 -1
  118. package/dist/chunk-H3VORETI.js.map +0 -1
  119. package/dist/chunk-H42UHPYW.js +0 -1577
  120. package/dist/chunk-H42UHPYW.js.map +0 -1
  121. package/dist/chunk-HFPS7JFD.js +0 -1
  122. package/dist/chunk-INHVXP6Z.js.map +0 -1
  123. package/dist/chunk-K4ZJUGF4.cjs +0 -379
  124. package/dist/chunk-K4ZJUGF4.cjs.map +0 -1
  125. package/dist/chunk-KNJEKLKV.js +0 -49
  126. package/dist/chunk-KNJEKLKV.js.map +0 -1
  127. package/dist/chunk-MXO72PLY.cjs.map +0 -1
  128. package/dist/chunk-NLEU4QSE.cjs +0 -360
  129. package/dist/chunk-NLEU4QSE.cjs.map +0 -1
  130. package/dist/chunk-OE6WK2Y2.cjs +0 -1577
  131. package/dist/chunk-OE6WK2Y2.cjs.map +0 -1
  132. package/dist/chunk-PKFNUHDY.js.map +0 -1
  133. package/dist/chunk-Q2WGKJNN.cjs.map +0 -1
  134. package/dist/chunk-RHIJ3S4I.cjs +0 -1
  135. package/dist/chunk-RHIJ3S4I.cjs.map +0 -1
  136. package/dist/chunk-TTCLJX6D.js.map +0 -1
  137. package/dist/chunk-UADYAYAT.cjs.map +0 -1
  138. package/dist/chunk-VOHZB45S.js.map +0 -1
  139. package/dist/chunk-VTI6FOLB.js.map +0 -1
  140. package/dist/chunk-WWURE4XT.cjs.map +0 -1
  141. package/dist/chunk-YH3BIKHH.js.map +0 -1
  142. /package/dist/{chunk-HFPS7JFD.js.map → chunk-K6OZXIR6.js.map} +0 -0
@@ -1,13 +1,15 @@
1
1
  import {
2
+ HUMANIZE_ADJACENCY,
3
+ assertChordTimeline,
2
4
  borrowedSource,
3
5
  chordToRoman,
4
6
  detectChordBest,
5
7
  functionOf,
6
8
  isDiatonic,
9
+ isMinorKey,
7
10
  parallelKey
8
- } from "./chunk-H42UHPYW.js";
11
+ } from "./chunk-VLE5R7PF.js";
9
12
  import {
10
- ARTICULATIONS,
11
13
  LIMBS,
12
14
  canSound,
13
15
  createPositionalRng,
@@ -15,63 +17,143 @@ import {
15
17
  foldIntoRange,
16
18
  includeAt,
17
19
  instrumentRange,
18
- resolveAlgorithmVersion
19
- } from "./chunk-FVSI36UV.js";
20
+ resolveAlgorithmVersion,
21
+ toStringedProfile
22
+ } from "./chunk-X6RJAHII.js";
20
23
  import {
21
- beatsPerBar,
22
- createNoteEventIndex,
23
- formatTimeSignature,
24
- isStrongBeat,
25
- metricWeight,
26
- pulseBeats
27
- } from "./chunk-ELCTZUJJ.js";
24
+ createNoteEventIndex
25
+ } from "./chunk-7GOKQJTQ.js";
28
26
  import {
27
+ chordScales,
29
28
  roleOf
30
- } from "./chunk-KNJEKLKV.js";
29
+ } from "./chunk-H66ARFU5.js";
31
30
  import {
32
31
  chordPitchClasses,
33
32
  chordQualities,
34
33
  chordToneRole,
35
34
  createsParallelPerfect,
35
+ degreeOfInterval,
36
36
  diatonicTriad,
37
37
  enumerateSafePitches,
38
38
  evaluateSafety,
39
+ fifthPcOf,
40
+ heptatonicFrameOf,
39
41
  isForbiddenMelodicLeap,
40
42
  isScaleTone,
43
+ leadingTonePcOf,
41
44
  majorKey,
42
45
  makeChord,
43
46
  minorKey,
47
+ namedScaleMask,
44
48
  nearestScaleTone,
45
49
  profileWeights,
50
+ resolveKey,
46
51
  scaleLadderPitch,
47
52
  scaleLadderPosition,
53
+ scaleSystemOf,
48
54
  scaleTonesInDegreeOrder,
49
55
  shiftByScaleDegrees,
56
+ soundsDominantSeventh,
50
57
  spellPitchClass,
51
- spellScale,
52
58
  toChordData,
53
- toKeyScale
54
- } from "./chunk-INHVXP6Z.js";
59
+ toKeyScale,
60
+ tonicizableDegrees
61
+ } from "./chunk-FPBZFNCD.js";
55
62
  import {
63
+ ARTICULATIONS,
64
+ BEAT_EPS,
56
65
  InvalidInputError,
66
+ assertArray,
57
67
  assertDegree,
58
68
  assertFiniteNumber,
69
+ assertFiniteSemitones,
70
+ assertFunction,
59
71
  assertGenerationBudget,
60
72
  assertInteger,
73
+ assertMidiPitch,
61
74
  assertNoteEvents,
62
75
  assertOneOf,
76
+ assertOptions,
63
77
  assertPositiveInt,
64
78
  assertRange,
79
+ assertRecord,
65
80
  assertTimeSignature,
81
+ beatsPerBar,
66
82
  clampToMidi,
83
+ describeRejected,
67
84
  dropSilentNotes,
68
- midiToNote,
85
+ formatTimeSignature,
86
+ isCompound,
87
+ isStrongBeat,
88
+ meterAt,
89
+ metricWeight,
69
90
  noteToPitchClass,
70
91
  pitchClassOf,
92
+ pulseBeats,
93
+ resolveMeters,
71
94
  soundingNotesOnly,
95
+ toMeterData,
72
96
  toSpelledInterval,
73
97
  transposeNote
74
- } from "./chunk-VTI6FOLB.js";
98
+ } from "./chunk-PQL7E5F6.js";
99
+
100
+ // src/generate/context/draw.ts
101
+ var DRAW_METHODS = ["at", "prob", "range", "float"];
102
+ function assertDraw(draw, name = "draw") {
103
+ const sampler = assertRecord(draw, name);
104
+ for (const method of DRAW_METHODS) {
105
+ assertFunction(sampler[method], `${name}.${method}`);
106
+ }
107
+ return sampler;
108
+ }
109
+ function assertSeedPath(path, name = "draw path") {
110
+ const steps = assertArray(path, name);
111
+ for (let index = 0; index < steps.length; index += 1) {
112
+ const step = steps[index];
113
+ if (typeof step !== "string" && !Number.isFinite(step)) {
114
+ throw new InvalidInputError(
115
+ `${name}[${index}] must be a string or a finite number; received ${describeRejected(step)}`
116
+ );
117
+ }
118
+ }
119
+ return steps;
120
+ }
121
+ function drawFrom(rng, path) {
122
+ const draw = rng.at(...path);
123
+ if (!(draw >= 0 && draw < 1)) {
124
+ throw new InvalidInputError(`positional draw must be in [0, 1); received ${draw}`);
125
+ }
126
+ return draw;
127
+ }
128
+ function drawsFrom(rng, ...prefix) {
129
+ const at2 = (...path) => drawFrom(rng, [...prefix, ...path]);
130
+ return {
131
+ at: at2,
132
+ prob: (p, ...path) => includeAt(rng, p, ...prefix, ...path),
133
+ range: (lo, hi, ...path) => {
134
+ assertInteger(lo, "range lower bound");
135
+ assertInteger(hi, "range upper bound");
136
+ if (lo > hi) {
137
+ throw new InvalidInputError(
138
+ `range lower bound must not exceed upper bound; received ${lo} > ${hi}`
139
+ );
140
+ }
141
+ assertInteger(hi - lo, "range span", 0);
142
+ return lo + Math.floor(at2(...path) * (hi - lo + 1));
143
+ },
144
+ float: (lo, hi, ...path) => {
145
+ assertFiniteNumber(lo, "float lower bound");
146
+ assertFiniteNumber(hi, "float upper bound");
147
+ if (lo > hi) {
148
+ throw new InvalidInputError(
149
+ `float lower bound must not exceed upper bound; received ${lo} > ${hi}`
150
+ );
151
+ }
152
+ assertFiniteNumber(hi - lo, "float span");
153
+ return lo + at2(...path) * (hi - lo);
154
+ }
155
+ };
156
+ }
75
157
 
76
158
  // src/generate/drums/internal.ts
77
159
  var GM = {
@@ -115,6 +197,16 @@ var PUBLIC_SECTIONS = [
115
197
  "bridge",
116
198
  "outro"
117
199
  ];
200
+ function backbeatBeats(snareBeat3, barBeats = 4) {
201
+ if (snareBeat3) {
202
+ return barBeats > 2 ? [2] : [];
203
+ }
204
+ const beats = [];
205
+ for (let beat = 1; beat < barBeats; beat += 2) {
206
+ beats.push(beat);
207
+ }
208
+ return beats;
209
+ }
118
210
  var STYLE_MAP = {
119
211
  // Standard pop pulse: kick on beats 1 and 3, backbeat snare on 2 and 4.
120
212
  standard: { style: "standard", feel: "straight", ghostBoost: false, snareBeat3: false },
@@ -286,50 +378,59 @@ function sameTs(a, b) {
286
378
  return a.numerator === b.numerator && a.denominator === b.denominator;
287
379
  }
288
380
  function fitsQuery(entry, query) {
289
- if (query.genre !== void 0 && entry.genre !== query.genre) {
381
+ const candidate = assertRecord(entry, "vocabulary entry");
382
+ const asked = assertRecord(query, "vocabulary query");
383
+ if (asked.genre !== void 0 && candidate.genre !== asked.genre) {
290
384
  return false;
291
385
  }
292
- if (query.section !== void 0 && entry.sections && !entry.sections.includes(query.section)) {
386
+ if (asked.section !== void 0 && candidate.sections && !candidate.sections.includes(asked.section)) {
293
387
  return false;
294
388
  }
295
- if (query.bpm !== void 0 && entry.tempoRange) {
296
- const [low, high] = entry.tempoRange;
297
- if (query.bpm < low || query.bpm > high) {
389
+ if (asked.bpm !== void 0 && candidate.tempoRange) {
390
+ const [low, high] = candidate.tempoRange;
391
+ if (asked.bpm < low || asked.bpm > high) {
298
392
  return false;
299
393
  }
300
394
  }
301
- if (query.ts !== void 0 && entry.ts && !sameTs(entry.ts, query.ts)) {
395
+ if (asked.ts !== void 0 && candidate.ts && !sameTs(candidate.ts, meterAt(0, toMeterData(asked.ts, "ts")))) {
302
396
  return false;
303
397
  }
304
- if (query.quality !== void 0 && entry.fitsOver && !entry.fitsOver.includes(query.quality)) {
398
+ if (asked.quality !== void 0 && candidate.fitsOver && !candidate.fitsOver.includes(asked.quality)) {
305
399
  return false;
306
400
  }
307
- if (query.articulations !== void 0) {
308
- const available = query.articulations;
309
- if (entry.articulations.some((articulation) => !available.includes(articulation))) {
401
+ if (asked.articulations !== void 0) {
402
+ const available = asked.articulations;
403
+ if (candidate.articulations.some((articulation) => !available.includes(articulation))) {
310
404
  return false;
311
405
  }
312
406
  }
313
- return query.difficulty === void 0 || entry.difficulty <= query.difficulty;
407
+ return asked.difficulty === void 0 || candidate.difficulty <= asked.difficulty;
314
408
  }
315
409
  function selectVocabulary(dictionary, query) {
316
- return dictionary.filter((entry) => fitsQuery(entry, query));
410
+ return assertArray(dictionary, "dictionary").filter(
411
+ (entry) => fitsQuery(entry, query)
412
+ );
317
413
  }
318
414
  function pickVocabulary(dictionary, query, draw, ...path) {
415
+ const sampler = assertDraw(draw);
416
+ assertSeedPath(path);
319
417
  const candidates = selectVocabulary(dictionary, query);
320
418
  if (candidates.length === 0) {
321
419
  return void 0;
322
420
  }
323
- return candidates[draw.range(0, candidates.length - 1, ...path)];
421
+ return candidates[sampler.range(0, candidates.length - 1, ...path)];
324
422
  }
325
423
  function mergeVocabulary(builtIn, supplied) {
326
- if (!supplied || supplied.length === 0) {
327
- return [...builtIn];
328
- }
329
- const byId = new Map(supplied.map((entry) => [entry.id, entry]));
330
- const merged = builtIn.map((entry) => byId.get(entry.id) ?? entry);
331
- const replaced = new Set(builtIn.map((entry) => entry.id));
332
- for (const entry of supplied) {
424
+ const library = assertArray(builtIn, "builtIn");
425
+ const extra = supplied === void 0 || supplied === null ? [] : assertArray(supplied, "supplied");
426
+ if (extra.length === 0) {
427
+ return [...library];
428
+ }
429
+ const idOf = (entry, index, side) => assertRecord(entry, `${side}[${index}]`).id;
430
+ const byId = new Map(extra.map((entry, index) => [idOf(entry, index, "supplied"), entry]));
431
+ const merged = library.map((entry, index) => byId.get(idOf(entry, index, "builtIn")) ?? entry);
432
+ const replaced = new Set(library.map((entry, index) => idOf(entry, index, "builtIn")));
433
+ for (const entry of extra) {
333
434
  if (!replaced.has(entry.id)) {
334
435
  merged.push(entry);
335
436
  }
@@ -338,30 +439,49 @@ function mergeVocabulary(builtIn, supplied) {
338
439
  }
339
440
  function vocabularyOfKind(entries, isMaterial) {
340
441
  const out = [];
341
- for (const entry of entries) {
342
- if (isMaterial(entry.material)) {
442
+ const recognises = assertFunction(isMaterial, "isMaterial");
443
+ for (const entry of assertArray(entries, "entries")) {
444
+ if (recognises(assertRecord(entry, "entry").material)) {
343
445
  out.push(entry);
344
446
  }
345
447
  }
346
448
  return out;
347
449
  }
450
+ function assertNames(values, allowed, label) {
451
+ if (!Array.isArray(values)) {
452
+ throw new InvalidInputError(
453
+ `${label} must be an array of names; received ${describeRejected(values)}`
454
+ );
455
+ }
456
+ values.forEach((value, index) => {
457
+ assertOneOf(value, allowed, `${label}[${index}]`);
458
+ });
459
+ }
348
460
  function assertVocabulary(entry, label = "vocabulary entry") {
461
+ if (typeof entry !== "object" || entry === null) {
462
+ throw new InvalidInputError(
463
+ `${label} must be a dictionary entry; received ${describeRejected(entry)}`
464
+ );
465
+ }
349
466
  assertOneOf(entry.genre, GENRES, `${label} genre`);
350
467
  assertOneOf(entry.provenance?.basis, PROVENANCE_BASES, `${label} provenance basis`);
351
- entry.articulations.forEach((articulation, index) => {
352
- assertOneOf(articulation, ARTICULATIONS, `${label} articulations[${index}]`);
353
- });
354
- entry.sections?.forEach((section, index) => {
355
- assertOneOf(section, PUBLIC_SECTIONS, `${label} sections[${index}]`);
356
- });
357
- entry.fitsOver?.forEach((quality, index) => {
358
- assertOneOf(quality, chordQualities(), `${label} fitsOver[${index}]`);
359
- });
468
+ assertNames(entry.articulations, ARTICULATIONS, `${label} articulations`);
469
+ if (entry.sections !== void 0) {
470
+ assertNames(entry.sections, PUBLIC_SECTIONS, `${label} sections`);
471
+ }
472
+ if (entry.fitsOver !== void 0) {
473
+ assertNames(entry.fitsOver, chordQualities(), `${label} fitsOver`);
474
+ }
360
475
  if (entry.ts !== void 0) {
361
476
  assertTimeSignature(entry.ts, `${label} ts`);
362
477
  }
363
478
  assertRange(entry.difficulty, 1, 5, `${label} difficulty`);
364
- if (entry.tempoRange) {
479
+ if (entry.tempoRange !== void 0) {
480
+ if (!Array.isArray(entry.tempoRange) || entry.tempoRange.length !== 2) {
481
+ throw new InvalidInputError(
482
+ `${label} tempoRange must be a low and a high tempo; received ${describeRejected(entry.tempoRange)}`
483
+ );
484
+ }
365
485
  const [low, high] = entry.tempoRange;
366
486
  assertRange(low, Number.MIN_VALUE, 1e3, `${label} tempoRange low`);
367
487
  assertRange(high, low, 1e3, `${label} tempoRange high`);
@@ -389,60 +509,41 @@ function assertDifficulty(difficulty, label = "complexity difficulty") {
389
509
  return assertRange(difficulty, MIN_DIFFICULTY, MAX_DIFFICULTY, label);
390
510
  }
391
511
  function sustainsStrokes(stepBeats, bpm, difficulty) {
392
- if (difficulty === void 0 || bpm === void 0 || stepBeats <= 0) {
512
+ if (difficulty === void 0 || bpm === void 0) {
513
+ return true;
514
+ }
515
+ assertDifficulty(difficulty);
516
+ assertFiniteNumber(bpm, "bpm");
517
+ if (assertFiniteNumber(stepBeats, "stepBeats") <= 0) {
393
518
  return true;
394
519
  }
395
520
  const rate = 1 / (stepBeats * secondsPerBeat(bpm));
396
521
  return rate <= atLevel(STROKES_PER_SECOND, difficulty);
397
522
  }
398
523
  function sustainsShift(semitones, stepBeats, bpm, difficulty) {
399
- if (difficulty === void 0 || bpm === void 0 || stepBeats <= 0) {
524
+ if (difficulty === void 0 || bpm === void 0) {
525
+ return true;
526
+ }
527
+ assertDifficulty(difficulty);
528
+ assertFiniteNumber(bpm, "bpm");
529
+ assertFiniteSemitones(semitones);
530
+ if (assertFiniteNumber(stepBeats, "stepBeats") <= 0) {
400
531
  return true;
401
532
  }
402
533
  const seconds = stepBeats * secondsPerBeat(bpm);
403
534
  return Math.abs(semitones) / seconds <= atLevel(FRETS_PER_SECOND, difficulty);
404
535
  }
405
536
 
406
- // src/generate/context/draw.ts
407
- function drawFrom(rng, path) {
408
- const draw = rng.at(...path);
409
- if (!(draw >= 0 && draw < 1)) {
410
- throw new InvalidInputError(`positional draw must be in [0, 1); received ${draw}`);
411
- }
412
- return draw;
413
- }
414
- function drawsFrom(rng, ...prefix) {
415
- const at2 = (...path) => drawFrom(rng, [...prefix, ...path]);
416
- return {
417
- at: at2,
418
- prob: (p, ...path) => includeAt(rng, p, ...prefix, ...path),
419
- range: (lo, hi, ...path) => {
420
- assertInteger(lo, "range lower bound");
421
- assertInteger(hi, "range upper bound");
422
- if (lo > hi) {
423
- throw new InvalidInputError(
424
- `range lower bound must not exceed upper bound; received ${lo} > ${hi}`
425
- );
426
- }
427
- return lo + Math.floor(at2(...path) * (hi - lo + 1));
428
- },
429
- float: (lo, hi, ...path) => {
430
- assertFiniteNumber(lo, "float lower bound");
431
- assertFiniteNumber(hi, "float upper bound");
432
- if (lo > hi) {
433
- throw new InvalidInputError(
434
- `float lower bound must not exceed upper bound; received ${lo} > ${hi}`
435
- );
436
- }
437
- return lo + at2(...path) * (hi - lo);
438
- }
439
- };
440
- }
441
-
442
537
  // src/generate/context/index.ts
443
538
  var DEFAULT_SEED = 0;
444
539
  var VERSION_SEGMENT = "v";
445
540
  function resolveContext(input) {
541
+ if (input !== null && typeof input !== "number" && input !== void 0) {
542
+ assertRecord(input, "ctx");
543
+ }
544
+ if (input === null) {
545
+ throw new InvalidInputError("ctx must be a seed or a generation context; received null");
546
+ }
446
547
  const ctx = typeof input === "number" ? { seed: input } : input ?? { seed: DEFAULT_SEED };
447
548
  const seed = assertInteger(ctx.seed ?? DEFAULT_SEED, "seed", 0, 4294967295);
448
549
  const complexity = ctx.complexity ?? {};
@@ -472,24 +573,59 @@ function resolveContext(input) {
472
573
  }
473
574
  function resolveContextWith(input, sugar) {
474
575
  const base = resolveContext(input);
475
- const ornament2 = base.ornament ?? (sugar.ornament === void 0 ? void 0 : assertRange(sugar.ornament, 0, 1, "complexity ornament"));
576
+ const supplied = sugar.ornament === void 0 ? void 0 : assertRange(sugar.ornament, 0, 1, "complexity ornament");
577
+ const ornament2 = base.ornament ?? supplied;
476
578
  return { ...base, ornament: ornament2 };
477
579
  }
478
580
 
479
581
  // src/generate/bass/internal.ts
480
582
  var STRONG_VELOCITY = 100;
481
583
  var WEAK_VELOCITY = 80;
482
- var EPS = 1e-9;
584
+ var DEFAULT_TS = { numerator: 4, denominator: 4 };
585
+ var DEFAULT_OCTAVE = 2;
483
586
  function placePc(pc, anchor, low) {
484
- const high = low + 12;
485
- let midi = pc + 12 * Math.round((anchor - pc) / 12);
486
- while (midi < low) {
487
- midi += 12;
587
+ return foldIntoBand(pc + 12 * Math.round((anchor - pc) / 12), low);
588
+ }
589
+ function placeAboveBass(pc, bassMidi) {
590
+ return foldIntoBand(pc + 12 * Math.round((bassMidi - pc) / 12), bassMidi);
591
+ }
592
+ function placeRoot(pc, low) {
593
+ return low + pitchClassOf(pc - low);
594
+ }
595
+ function barTiles(startBeat, endBeat, ts) {
596
+ const barBeats = beatsPerBar(ts);
597
+ const tiles = [];
598
+ for (let index = 0; ; index += 1) {
599
+ const tileStart = startBeat + index * barBeats;
600
+ if (tileStart >= endBeat - BEAT_EPS) {
601
+ break;
602
+ }
603
+ tiles.push({ startBeat: tileStart, endBeat: Math.min(tileStart + barBeats, endBeat) });
488
604
  }
489
- while (midi > high) {
490
- midi -= 12;
605
+ return tiles;
606
+ }
607
+ function midBarPulse(barStart, ts) {
608
+ const pulse2 = pulseBeats(ts);
609
+ return barStart + Math.floor((beatsPerBar(ts) / 2 + BEAT_EPS) / pulse2) * pulse2;
610
+ }
611
+ function assertBassSegments(segments) {
612
+ for (let index = 0; index < segments.length; index += 1) {
613
+ const segment = segments[index];
614
+ if (segment === void 0) {
615
+ continue;
616
+ }
617
+ assertRange(segment.startBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].startBeat`);
618
+ assertRange(segment.endBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].endBeat`);
619
+ if (segment.endBeat <= segment.startBeat) {
620
+ throw new InvalidInputError(`segments[${index}] must have a positive duration`);
621
+ }
622
+ const previous = segments[index - 1];
623
+ if (previous !== void 0 && segment.startBeat < previous.endBeat - BEAT_EPS) {
624
+ throw new InvalidInputError(
625
+ `bass segments must not overlap: segments ${index - 1} and ${index} overlap`
626
+ );
627
+ }
491
628
  }
492
- return midi;
493
629
  }
494
630
  function chordTonePcs(chord) {
495
631
  const seen = /* @__PURE__ */ new Set();
@@ -506,29 +642,33 @@ function chordTonePcs(chord) {
506
642
  }
507
643
  return pcs;
508
644
  }
509
- function fifthPcOf(chord) {
510
- const fifths = new Set(chord.intervals.map((i) => pitchClassOf(i)));
511
- const chosen = fifths.has(7) ? 7 : fifths.has(6) ? 6 : fifths.has(8) ? 8 : void 0;
512
- return chosen === void 0 ? pitchClassOf(chord.rootPc) : pitchClassOf(chord.rootPc + chosen);
645
+ function fifthPcOf2(chord) {
646
+ return fifthPcOf(chord) ?? pitchClassOf(chord.rootPc);
513
647
  }
514
648
  function bassPcOf(chord) {
515
649
  return pitchClassOf(chord.bassPc ?? chord.rootPc);
516
650
  }
651
+ function bassToneCycle(chord) {
652
+ const bass = bassPcOf(chord);
653
+ const tones = chordTonePcs(chord);
654
+ const at2 = tones.indexOf(bass);
655
+ return at2 < 0 ? [bass, ...tones] : [...tones.slice(at2), ...tones.slice(0, at2)];
656
+ }
517
657
  function beatPositions(start, end, ts) {
518
658
  const positions = [];
519
659
  const barBeats = beatsPerBar(ts);
520
660
  const pulse2 = pulseBeats(ts);
521
661
  const firstBar = Math.floor(start / barBeats);
522
- const lastBar = Math.floor((end - EPS) / barBeats);
662
+ const lastBar = Math.floor((end - BEAT_EPS) / barBeats);
523
663
  for (let bar2 = firstBar; bar2 <= lastBar; bar2 += 1) {
524
664
  const barStart = bar2 * barBeats;
525
- const firstPulse = Math.max(0, Math.ceil((start - barStart - EPS) / pulse2));
665
+ const firstPulse = Math.max(0, Math.ceil((start - barStart - BEAT_EPS) / pulse2));
526
666
  for (let index = firstPulse; ; index += 1) {
527
667
  const position = barStart + index * pulse2;
528
- if (position >= barStart + barBeats - EPS || position >= end - EPS) {
668
+ if (position >= barStart + barBeats - BEAT_EPS || position >= end - BEAT_EPS) {
529
669
  break;
530
670
  }
531
- if (position >= start - EPS) {
671
+ if (position >= start - BEAT_EPS) {
532
672
  positions.push(position);
533
673
  }
534
674
  }
@@ -539,70 +679,88 @@ function beatPositions(start, end, ts) {
539
679
  return positions;
540
680
  }
541
681
  function foldIntoBand(midi, low) {
542
- let result = midi;
543
- while (result < low) {
544
- result += 12;
545
- }
546
- while (result > low + 12) {
547
- result -= 12;
548
- }
549
- return result;
682
+ const raised = midi + 12 * Math.max(0, Math.ceil((low - midi) / 12));
683
+ return raised - 12 * Math.max(0, Math.ceil((raised - low - 12) / 12));
550
684
  }
551
685
  function bandFloor(low, instrument) {
552
686
  if (!instrument) {
553
687
  return low;
554
688
  }
555
689
  const range = instrumentRange(instrument);
556
- let floor = low;
557
- while (floor + 12 > range.high) {
558
- floor -= 12;
559
- }
560
- while (floor < range.low) {
561
- floor += 12;
562
- }
563
- return floor;
690
+ const dropped = low - 12 * Math.max(0, Math.ceil((low + 12 - range.high) / 12));
691
+ return dropped + 12 * Math.max(0, Math.ceil((range.low - dropped) / 12));
564
692
  }
565
- function approachNote(target, from, low, key, chromatic) {
693
+ function approachNote(target, from, key, chromatic) {
566
694
  const dir = from <= target ? -1 : 1;
695
+ const near = neighborOnSide(target, dir, key, chromatic);
696
+ const far = neighborOnSide(target, -dir, key, chromatic);
697
+ const playable = (midi) => midi >= 0 && midi <= 127;
698
+ if (playable(near) && near !== from) {
699
+ return near;
700
+ }
701
+ return playable(far) ? far : near;
702
+ }
703
+ function neighborOnSide(target, dir, key, chromatic) {
567
704
  const semitone = target + dir;
568
705
  if (chromatic) {
569
- return foldIntoBand(semitone, low);
706
+ return semitone;
570
707
  }
571
708
  const cand = nearestScaleTone(target + dir * 2, key);
572
709
  const step = Math.abs(cand - target);
573
- return foldIntoBand(step >= 1 && step <= 2 ? cand : semitone, low);
710
+ return step >= 1 && step <= 2 ? cand : semitone;
711
+ }
712
+ function bassRegister(named, given, octave) {
713
+ const instrument = given === void 0 ? named !== void 0 && named.kind === "stringed" ? named : void 0 : toStringedProfile(given, "instrument");
714
+ return { instrument, low: bandFloor(octave * 12 + 12, instrument) };
574
715
  }
575
716
 
576
717
  // src/generate/vocabulary/transform.ts
577
718
  var BAR_STEPS = 16;
578
719
  var BEAT_STEPS = 4;
579
720
  var STEP_BEATS = 1 / BEAT_STEPS;
721
+ function assertGridEvents(events) {
722
+ assertArray(events, "figure events");
723
+ events.forEach((event, index) => {
724
+ assertRecord(event, `figure events[${index}]`);
725
+ assertFiniteNumber(event.step, `figure events[${index}].step`);
726
+ });
727
+ return events;
728
+ }
729
+ function assertSpanSteps(spanSteps) {
730
+ return assertPositiveInt(spanSteps, "figure span steps");
731
+ }
580
732
  function streamOf(event) {
581
733
  return event.limb ?? event.voice ?? "";
582
734
  }
583
- function gridMetricWeight(step) {
584
- const inBar = (step % BAR_STEPS + BAR_STEPS) % BAR_STEPS;
585
- if (inBar === 0) {
586
- return 4;
587
- }
588
- if (inBar % 8 === 0) {
589
- return 3;
590
- }
591
- if (inBar % 4 === 0) {
592
- return 2;
593
- }
594
- if (inBar % 2 === 0) {
595
- return 1;
735
+ var FIGURE_TS = { numerator: 4, denominator: 4 };
736
+ function subdivisionBeats(ts) {
737
+ const pulse2 = pulseBeats(ts);
738
+ return isCompound(ts) ? pulse2 / 3 : pulse2 / 2;
739
+ }
740
+ function gridMetricWeight(step, ts) {
741
+ assertFiniteNumber(step, "step");
742
+ return stepWeight(step, meterAt(0, toMeterData(ts, "ts")));
743
+ }
744
+ function stepWeight(step, ts) {
745
+ const beats = step * STEP_BEATS;
746
+ const weight = metricWeight(beats, ts);
747
+ if (weight > 0) {
748
+ return weight + 1;
596
749
  }
597
- return 0;
750
+ const subdivision = subdivisionBeats(ts);
751
+ const divisions = beats / subdivision;
752
+ return Math.abs(divisions - Math.round(divisions)) < 1e-9 ? 1 : 0;
598
753
  }
599
- function thin(events, amount) {
754
+ function thin(events, amount, ts) {
755
+ const figure = assertGridEvents(events);
600
756
  assertRange(amount, 0, 1, "thin amount");
757
+ const signature = meterAt(0, toMeterData(ts, "ts"));
601
758
  const floor = Math.min(4, Math.floor(amount * 5));
602
- return events.filter((event) => gridMetricWeight(event.step) >= floor);
759
+ return figure.filter((event) => stepWeight(event.step, signature) >= floor);
603
760
  }
604
761
  function double(events, spanSteps = BAR_STEPS) {
605
- const sorted = [...events].sort((a, b) => a.step - b.step);
762
+ assertSpanSteps(spanSteps);
763
+ const sorted = [...assertGridEvents(events)].sort((a, b) => a.step - b.step);
606
764
  const out = [];
607
765
  for (let i = 0; i < sorted.length; i += 1) {
608
766
  const event = sorted[i];
@@ -620,30 +778,46 @@ function double(events, spanSteps = BAR_STEPS) {
620
778
  }
621
779
  var ADDED_NOTE_VELOCITY = 0.75;
622
780
  function halfTime(events, spanSteps = BAR_STEPS) {
623
- return events.map((event) => ({ ...event, step: event.step * 2 })).filter((event) => event.step < spanSteps);
781
+ assertSpanSteps(spanSteps);
782
+ return assertGridEvents(events).map((event) => ({ ...event, step: event.step * 2 })).filter((event) => event.step < spanSteps);
624
783
  }
625
784
  function doubleTime(events, spanSteps = BAR_STEPS) {
626
- const out = [];
785
+ assertSpanSteps(spanSteps);
786
+ assertGridEvents(events);
787
+ const merged = /* @__PURE__ */ new Map();
788
+ const place = (event, step, velocity) => {
789
+ const key = `${streamOf(event)}@${step}`;
790
+ const held = merged.get(key);
791
+ if (held === void 0 || velocity > held.velocity) {
792
+ merged.set(key, { ...event, step, velocity });
793
+ }
794
+ };
627
795
  for (const event of events) {
628
796
  const step = Math.round(event.step / 2);
629
- out.push({ ...event, step });
797
+ place(event, step, event.velocity);
630
798
  const repeated = step + spanSteps / 2;
631
799
  if (repeated < spanSteps) {
632
- out.push({ ...event, step: repeated, velocity: event.velocity * ADDED_NOTE_VELOCITY });
800
+ place(event, repeated, event.velocity * ADDED_NOTE_VELOCITY);
633
801
  }
634
802
  }
635
- return out.sort((a, b) => a.step - b.step);
803
+ return [...merged.values()].sort((a, b) => a.step - b.step);
636
804
  }
637
805
  function syncopate(events, amount, draw, ...path) {
638
- assertRange(amount, 0, 1, "syncopate amount");
639
- const occupied = new Set(events.map((event) => `${streamOf(event)}@${event.step}`));
640
- const out = [...events];
641
- for (const event of events) {
806
+ const figure = assertGridEvents(events);
807
+ const sampler = assertDraw(draw);
808
+ assertSeedPath(path);
809
+ const asked = typeof amount === "number" ? void 0 : assertRecord(amount, "amount");
810
+ const dial = asked === void 0 ? amount : asked.amount;
811
+ const signature = meterAt(0, toMeterData(asked?.ts ?? FIGURE_TS, "ts"));
812
+ assertRange(dial, 0, 1, "syncopate amount");
813
+ const occupied = new Set(figure.map((event) => `${streamOf(event)}@${event.step}`));
814
+ const out = [...figure];
815
+ for (const event of figure) {
642
816
  const anticipated = event.step - 1;
643
- if (gridMetricWeight(event.step) < 2 || anticipated < 1 || occupied.has(`${streamOf(event)}@${anticipated}`)) {
817
+ if (stepWeight(event.step, signature) < 2 || anticipated < 1 || occupied.has(`${streamOf(event)}@${anticipated}`)) {
644
818
  continue;
645
819
  }
646
- if (!draw.prob(amount, ...path, "syncopate", event.step)) {
820
+ if (!sampler.prob(dial, ...path, "syncopate", event.step)) {
647
821
  continue;
648
822
  }
649
823
  out.push({ ...event, step: anticipated, velocity: event.velocity * ADDED_NOTE_VELOCITY });
@@ -651,35 +825,55 @@ function syncopate(events, amount, draw, ...path) {
651
825
  return out.sort((a, b) => a.step - b.step);
652
826
  }
653
827
  function ornamentBy(events, isOrnament, amount, draw, ...path) {
828
+ const figure = assertGridEvents(events);
829
+ const marks = assertFunction(isOrnament, "isOrnament");
830
+ const sampler = assertDraw(draw);
831
+ assertSeedPath(path);
654
832
  assertRange(amount, 0, 1, "ornament amount");
655
- return events.filter(
656
- (event) => !isOrnament(event) || draw.prob(amount, ...path, "ornament", event.step)
833
+ return figure.filter(
834
+ (event) => !marks(event) || sampler.prob(amount, ...path, "ornament", event.step)
657
835
  );
658
836
  }
659
837
  var NEUTRAL_RHYTHMIC = 0.5;
660
838
  function deform(events, opts, draw, ...path) {
661
- const spanSteps = opts.spanSteps ?? BAR_STEPS;
662
- let out = opts.rate === "half" ? halfTime(events, spanSteps) : opts.rate === "double" ? doubleTime(events, spanSteps) : [...events];
663
- if (opts.rhythmic !== void 0) {
664
- const rhythmic = assertRange(opts.rhythmic, 0, 1, "complexity rhythmic");
839
+ const figure = assertGridEvents(events);
840
+ const asked = assertRecord(opts, "opts");
841
+ assertDraw(draw);
842
+ assertSeedPath(path);
843
+ const spanSteps = assertSpanSteps(asked.spanSteps === void 0 ? BAR_STEPS : asked.spanSteps);
844
+ const ts = asked.ts ?? FIGURE_TS;
845
+ let out = asked.rate === "half" ? halfTime(figure, spanSteps) : asked.rate === "double" ? doubleTime(figure, spanSteps) : [...figure];
846
+ if (asked.rhythmic !== void 0) {
847
+ const rhythmic = assertRange(asked.rhythmic, 0, 1, "complexity rhythmic");
665
848
  if (rhythmic < NEUTRAL_RHYTHMIC) {
666
- out = thin(out, (NEUTRAL_RHYTHMIC - rhythmic) / NEUTRAL_RHYTHMIC);
849
+ out = thin(out, (NEUTRAL_RHYTHMIC - rhythmic) / NEUTRAL_RHYTHMIC, ts);
667
850
  } else if (rhythmic > NEUTRAL_RHYTHMIC) {
668
- out = syncopate(out, (rhythmic - NEUTRAL_RHYTHMIC) / NEUTRAL_RHYTHMIC, draw, ...path);
851
+ out = syncopate(
852
+ out,
853
+ { amount: (rhythmic - NEUTRAL_RHYTHMIC) / NEUTRAL_RHYTHMIC, ts },
854
+ draw,
855
+ ...path
856
+ );
669
857
  }
670
858
  }
671
- if (opts.ornament !== void 0 && opts.isOrnament) {
672
- const isOrnament = opts.isOrnament;
673
- out = ornamentBy(out, (event) => isOrnament(event), opts.ornament, draw, ...path);
859
+ if (asked.ornament !== void 0 && asked.isOrnament) {
860
+ const isOrnament = assertFunction(asked.isOrnament, "opts.isOrnament");
861
+ out = ornamentBy(out, (event) => isOrnament(event), asked.ornament, draw, ...path);
674
862
  }
675
863
  return out.sort((a, b) => a.step - b.step);
676
864
  }
677
865
  function withinCeiling(events, bpm, difficulty) {
678
- if (difficulty === void 0 || bpm === void 0 || events.length < 2) {
866
+ const figure = assertGridEvents(events);
867
+ if (difficulty === void 0 || bpm === void 0) {
868
+ return true;
869
+ }
870
+ assertFiniteNumber(bpm, "bpm");
871
+ assertDifficulty(difficulty);
872
+ if (figure.length < 2) {
679
873
  return true;
680
874
  }
681
875
  const byStream = /* @__PURE__ */ new Map();
682
- for (const event of events) {
876
+ for (const event of figure) {
683
877
  const stream = streamOf(event);
684
878
  const steps = byStream.get(stream);
685
879
  if (steps) {
@@ -704,7 +898,79 @@ function withinCeiling(events, bpm, difficulty) {
704
898
  return sustainsStrokes(closest * STEP_BEATS, bpm, difficulty);
705
899
  }
706
900
 
707
- // src/generate/bass/licks.ts
901
+ // src/generate/bass/degrees.ts
902
+ var MAJOR_DEGREE_SEMITONES = [0, 2, 4, 5, 7, 9, 11];
903
+ var HEPTATONIC_SIZE = 7;
904
+ function impliedScaleTones(chord) {
905
+ if (chordDegreeSemitone(7, chord) === void 0) {
906
+ return void 0;
907
+ }
908
+ for (const match of chordScales(chord)) {
909
+ const mask = namedScaleMask(match.name);
910
+ if (mask === void 0) {
911
+ continue;
912
+ }
913
+ const tones = [];
914
+ for (let offset = 0; offset < 12; offset += 1) {
915
+ if ((mask >> offset & 1) === 1) {
916
+ tones.push(offset);
917
+ }
918
+ }
919
+ if (tones.length === HEPTATONIC_SIZE) {
920
+ return tones;
921
+ }
922
+ }
923
+ return void 0;
924
+ }
925
+ function replacedDegreeSemitone(degree, chord) {
926
+ if (degree !== 3) {
927
+ return void 0;
928
+ }
929
+ const sounds = (semitones) => chord.intervals.some((interval) => pitchClassOf(interval) === semitones);
930
+ if (sounds(3) || sounds(4)) {
931
+ return void 0;
932
+ }
933
+ return sounds(5) ? 5 : sounds(2) ? 2 : void 0;
934
+ }
935
+ function degreeSemitone(degree, alter, chord, key, implied) {
936
+ const octaves = Math.floor((degree - 1) / 7);
937
+ const within = (degree - 1) % 7 + 1;
938
+ const template = MAJOR_DEGREE_SEMITONES[within - 1] ?? 0;
939
+ const semitone = alter !== 0 ? template : chordDegreeSemitone(within, chord) ?? replacedDegreeSemitone(within, chord) ?? implied?.[within - 1] ?? // A degree neither the chord nor its quality settles is a passing tone,
940
+ // so the key decides it.
941
+ nearestScaleTone(chord.rootPc + template, key) - chord.rootPc;
942
+ return pitchClassOf(semitone) + 12 * octaves + alter;
943
+ }
944
+ function chordDegreeSemitone(degree, chord) {
945
+ for (const interval of chord.intervals) {
946
+ if (degreeOfInterval(interval, chord) === degree) {
947
+ return interval;
948
+ }
949
+ }
950
+ return void 0;
951
+ }
952
+
953
+ // src/generate/vocabulary/freeze.ts
954
+ function freezeReachable(value, seen) {
955
+ if (value === null || typeof value !== "object") {
956
+ return;
957
+ }
958
+ const object = value;
959
+ if (seen.has(object)) {
960
+ return;
961
+ }
962
+ seen.add(object);
963
+ Object.freeze(object);
964
+ for (const child of Object.values(object)) {
965
+ freezeReachable(child, seen);
966
+ }
967
+ }
968
+ function deepFreeze(value) {
969
+ freezeReachable(value, /* @__PURE__ */ new WeakSet());
970
+ return value;
971
+ }
972
+
973
+ // src/generate/bass/licks-data.ts
708
974
  function isLickMaterial(material) {
709
975
  const candidate = material;
710
976
  return typeof candidate === "object" && candidate !== null && typeof candidate.lengthSteps === "number" && Array.isArray(candidate.notes);
@@ -714,7 +980,7 @@ function note(degree, step, velocity, extra) {
714
980
  }
715
981
  var BAR = 4 * BEAT_STEPS;
716
982
  var FOUR_FOUR = { numerator: 4, denominator: 4 };
717
- var BASS_LICKS = Object.freeze([
983
+ var BASS_LICKS = deepFreeze([
718
984
  {
719
985
  id: "motownWalkDown",
720
986
  genre: "motown",
@@ -759,7 +1025,7 @@ var BASS_LICKS = Object.freeze([
759
1025
  id: "funkSixteenthPop",
760
1026
  genre: "funk",
761
1027
  difficulty: 4,
762
- articulations: ["mute", "slide"],
1028
+ articulations: ["mute"],
763
1029
  ts: FOUR_FOUR,
764
1030
  tempoRange: [84, 124],
765
1031
  material: {
@@ -842,7 +1108,7 @@ var BASS_LICKS = Object.freeze([
842
1108
  id: "gospelPassing",
843
1109
  genre: "gospel",
844
1110
  difficulty: 4,
845
- articulations: ["slide"],
1111
+ articulations: [],
846
1112
  ts: FOUR_FOUR,
847
1113
  tempoRange: [60, 108],
848
1114
  material: {
@@ -883,7 +1149,7 @@ var BASS_LICKS = Object.freeze([
883
1149
  id: "reggaeOffbeatDrop",
884
1150
  genre: "reggae",
885
1151
  difficulty: 3,
886
- articulations: ["mute"],
1152
+ articulations: [],
887
1153
  ts: FOUR_FOUR,
888
1154
  tempoRange: [64, 104],
889
1155
  material: {
@@ -902,103 +1168,61 @@ var BASS_LICKS = Object.freeze([
902
1168
  }
903
1169
  }
904
1170
  ]);
905
- var MAJOR_DEGREE_SEMITONES = [0, 2, 4, 5, 7, 9, 11];
906
- function degreeSemitone(degree, alter, chord, key) {
907
- const octaves = Math.floor((degree - 1) / 7);
908
- const within = (degree - 1) % 7 + 1;
909
- const fromChord = chordDegreeSemitone(within, chord);
910
- const template = MAJOR_DEGREE_SEMITONES[within - 1] ?? 0;
911
- const semitone = fromChord ?? // A degree the chord does not name is a passing tone, so the key decides it.
912
- nearestScaleTone(chord.rootPc + template, key) - chord.rootPc;
913
- return pitchClassOf(semitone) + 12 * octaves + alter;
914
- }
915
- function chordDegreeSemitone(degree, chord) {
916
- for (const interval of chord.intervals) {
917
- if (degreeOfInterval(interval) === degree) {
918
- return interval;
919
- }
920
- }
921
- return void 0;
922
- }
923
- function degreeOfInterval(interval) {
924
- const within = (interval % 12 + 12) % 12;
925
- if (within === 0) {
926
- return 1;
927
- }
928
- if (within === 1 || within === 2) {
929
- return 2;
930
- }
931
- if (within === 3 || within === 4) {
932
- return 3;
933
- }
934
- if (within === 5) {
935
- return 4;
936
- }
937
- if (within === 6 || within === 7 || within === 8) {
938
- return 5;
939
- }
940
- if (within === 9) {
941
- return 6;
942
- }
943
- return 7;
944
- }
945
- var DEFAULT_TS = FOUR_FOUR;
946
- var DEFAULT_OCTAVE = 2;
1171
+
1172
+ // src/generate/bass/licks.ts
947
1173
  var DEFAULT_BPM = 120;
948
1174
  var DEFAULT_LICK_DENSITY = 0.6;
949
- var EPS2 = 1e-9;
950
1175
  function placeLicks(timeline, key, opts) {
951
- assertGenerationBudget(timeline.length, "lick segments", opts.budget);
952
- const genre = assertOneOf(opts.genre, GENRES, "lick genre");
953
- const ts = opts.ts ?? DEFAULT_TS;
954
- assertTimeSignature(ts, "lick time signature");
955
- const octave = opts.octave ?? DEFAULT_OCTAVE;
1176
+ const asked = assertRecord(opts, "opts");
1177
+ const placement = assertArray(
1178
+ timeline,
1179
+ "timeline"
1180
+ );
1181
+ assertGenerationBudget(placement.length, "lick segments", asked.budget);
1182
+ const genre = assertOneOf(asked.genre, GENRES, "lick genre");
1183
+ const ts = meterAt(0, toMeterData(asked.ts ?? DEFAULT_TS, "ts"));
1184
+ const octave = asked.octave ?? DEFAULT_OCTAVE;
956
1185
  assertInteger(octave, "lick octave", -1, 8);
957
1186
  const scale = toKeyScale(key);
958
- const segments = timeline.map((segment) => ({ ...segment, chord: toChordData(segment.chord) })).sort((a, b) => a.startBeat - b.startBeat);
959
- for (const segment of segments) {
960
- assertRange(segment.startBeat, 0, Number.MAX_SAFE_INTEGER, "lick segment startBeat");
961
- if (segment.endBeat <= segment.startBeat) {
962
- throw new InvalidInputError(
963
- `lick segment at beat ${segment.startBeat} must have a positive duration`
964
- );
965
- }
966
- }
1187
+ const segments = placement.map((segment) => ({ ...segment, chord: toChordData(segment.chord) })).sort((a, b) => a.startBeat - b.startBeat);
1188
+ assertBassSegments(segments);
967
1189
  if (segments.length === 0) {
968
1190
  return [];
969
1191
  }
970
- const resolved = resolveContext(opts.ctx);
1192
+ const barBeats = beatsPerBar(ts);
1193
+ const estimatedBars = segments.reduce(
1194
+ (count, segment) => count + Math.max(1, Math.ceil((segment.endBeat - segment.startBeat) / barBeats)),
1195
+ 0
1196
+ );
1197
+ assertGenerationBudget(estimatedBars, "lick bars", asked.budget);
1198
+ const resolved = resolveContext(asked.ctx);
971
1199
  const draw = resolved.part("bass");
972
1200
  const bpm = resolved.bpm ?? DEFAULT_BPM;
973
1201
  const density = resolved.rhythmic ?? DEFAULT_LICK_DENSITY;
974
- const difficulty = resolved.difficulty ?? (opts.difficulty === void 0 ? void 0 : assertDifficulty(opts.difficulty, "lick difficulty"));
975
- const named = resolved.instrument("bass");
976
- const instrument = opts.instrument ?? (named !== void 0 && named.kind === "stringed" ? named : void 0);
977
- const low = bandFloor(octave * 12 + 12, instrument);
1202
+ const supplied = asked.difficulty === void 0 ? void 0 : assertDifficulty(asked.difficulty, "lick difficulty");
1203
+ const difficulty = resolved.difficulty ?? supplied;
1204
+ const { instrument, low } = bassRegister(resolved.instrument("bass"), asked.instrument, octave);
978
1205
  const dictionary = mergeVocabulary(
979
1206
  BASS_LICKS,
980
1207
  vocabularyOfKind(resolved.vocabulary, isLickMaterial)
981
1208
  );
982
1209
  const playable = instrument === void 0 ? {} : { articulations: instrument.articulations };
983
- const barBeats = beatsPerBar(ts);
984
1210
  const raw = [];
985
- let anchor = low;
986
- let rootAnchor = low;
987
1211
  for (let index = 0; index < segments.length; index += 1) {
988
1212
  const segment = segments[index];
989
1213
  if (!segment) {
990
1214
  continue;
991
1215
  }
992
1216
  const rootPc = bassPcOf(segment.chord);
993
- const onsets = [];
1217
+ const rootMidi = placeRoot(rootPc, low);
1218
+ const implied = impliedScaleTones(segment.chord);
994
1219
  let lastTilePlayed = false;
1220
+ let playedTile;
995
1221
  let firstOnset = segment.startBeat;
996
- for (let tile = 0; ; tile += 1) {
997
- const tileStart = segment.startBeat + tile * barBeats;
1222
+ const tiles = barTiles(segment.startBeat, segment.endBeat, ts);
1223
+ for (let tile = 0; tile < tiles.length; tile += 1) {
1224
+ const tileStart = tiles[tile]?.startBeat ?? segment.startBeat;
998
1225
  const remaining = segment.endBeat - tileStart;
999
- if (remaining <= EPS2) {
1000
- break;
1001
- }
1002
1226
  const entry = pickVocabulary(
1003
1227
  dictionary,
1004
1228
  { genre, bpm, ts, difficulty, quality: segment.chord.quality, ...playable },
@@ -1016,28 +1240,34 @@ function placeLicks(timeline, key, opts) {
1016
1240
  draw,
1017
1241
  [index, tile],
1018
1242
  bpm,
1019
- difficulty
1243
+ difficulty,
1244
+ ts
1020
1245
  ) : void 0;
1021
1246
  lastTilePlayed = notes !== void 0;
1247
+ if (notes !== void 0 && entry !== void 0) {
1248
+ playedTile = { material: entry.material, tileStart };
1249
+ }
1022
1250
  const rootStep = entry === void 0 ? 0 : anchorStepOf(entry.material, remaining);
1023
1251
  const rootAt = tileStart + rootStep * STEP_BEATS;
1024
1252
  if (tile === 0) {
1025
1253
  firstOnset = rootAt;
1026
1254
  }
1027
- onsets.push(rootAt);
1028
- rootAnchor = placePc(rootPc, rootAnchor, low);
1029
- anchor = rootAnchor;
1030
- raw.push({ startBeat: rootAt, pitch: rootAnchor, velocity: STRONG_VELOCITY });
1255
+ raw.push({ startBeat: rootAt, pitch: rootMidi, velocity: STRONG_VELOCITY });
1031
1256
  if (notes) {
1032
- const figureRoot = placePc(pitchClassOf(segment.chord.rootPc), rootAnchor, low);
1257
+ const figureRoot = segment.chord.bassPc === void 0 ? placePc(pitchClassOf(segment.chord.rootPc), rootMidi, low) : placeAboveBass(pitchClassOf(segment.chord.rootPc), rootMidi);
1033
1258
  for (const lickNote of notes) {
1034
1259
  const at2 = tileStart + lickNote.step * STEP_BEATS;
1035
- const offset = degreeSemitone(lickNote.degree, lickNote.alter ?? 0, segment.chord, scale);
1036
- anchor = figureRoot + offset;
1037
- onsets.push(at2);
1260
+ const offset = degreeSemitone(
1261
+ lickNote.degree,
1262
+ lickNote.alter ?? 0,
1263
+ segment.chord,
1264
+ scale,
1265
+ implied
1266
+ );
1267
+ const pitch = Math.abs(at2 - rootAt) < BEAT_EPS && lickNote.degree === 1 && (lickNote.alter ?? 0) === 0 ? rootMidi : figureRoot + offset;
1038
1268
  raw.push({
1039
1269
  startBeat: at2,
1040
- pitch: anchor,
1270
+ pitch,
1041
1271
  velocity: Math.round(
1042
1272
  (isStrongBeat(at2, ts) ? STRONG_VELOCITY : WEAK_VELOCITY) * lickNote.velocity
1043
1273
  ),
@@ -1048,19 +1278,21 @@ function placeLicks(timeline, key, opts) {
1048
1278
  }
1049
1279
  const next = segments[index + 1];
1050
1280
  if (next && lastTilePlayed) {
1051
- const approachAt = next.startBeat - STEP_BEATS * BEAT_STEPS;
1052
- const alreadySounds = onsets.some((onset) => Math.abs(onset - approachAt) < EPS2);
1053
- if (approachAt > firstOnset + EPS2 && !alreadySounds) {
1054
- const nextRoot = placePc(bassPcOf(next.chord), rootAnchor, low);
1055
- const midi = approachNote(
1056
- nextRoot,
1057
- anchor,
1058
- low,
1059
- scale,
1060
- draw.prob(0.5, "lickApproach", index)
1061
- );
1062
- anchor = midi;
1063
- raw.push({ startBeat: approachAt, pitch: midi, velocity: WEAK_VELOCITY });
1281
+ const lastBeat = next.startBeat - STEP_BEATS * BEAT_STEPS;
1282
+ const approachAt = materialOnsetIn(playedTile, lastBeat, next.startBeat) ?? lastBeat;
1283
+ if (approachAt > firstOnset + BEAT_EPS) {
1284
+ const nextRoot = placeRoot(bassPcOf(next.chord), low);
1285
+ const occupied = soundingAt(raw, approachAt);
1286
+ const distance = occupied === void 0 ? 0 : Math.abs(occupied.pitch - nextRoot);
1287
+ if (occupied === void 0 || distance < 1 || distance > 2) {
1288
+ const from = lastPitchBefore(raw, approachAt, rootMidi);
1289
+ const midi = approachNote(nextRoot, from, scale, draw.prob(0.5, "lickApproach", index));
1290
+ if (occupied === void 0) {
1291
+ raw.push({ startBeat: approachAt, pitch: midi, velocity: WEAK_VELOCITY });
1292
+ } else {
1293
+ occupied.pitch = midi;
1294
+ }
1295
+ }
1064
1296
  }
1065
1297
  }
1066
1298
  }
@@ -1078,7 +1310,7 @@ function placeLicks(timeline, key, opts) {
1078
1310
  nextStart - item.startBeat,
1079
1311
  written === void 0 ? Number.POSITIVE_INFINITY : written * STEP_BEATS
1080
1312
  );
1081
- if (durationBeat <= EPS2) {
1313
+ if (durationBeat <= BEAT_EPS) {
1082
1314
  continue;
1083
1315
  }
1084
1316
  const placed = instrument ? foldIntoRange(item.pitch, instrument) : item.pitch;
@@ -1095,6 +1327,38 @@ function placeLicks(timeline, key, opts) {
1095
1327
  }
1096
1328
  return out;
1097
1329
  }
1330
+ function soundingAt(raw, at2) {
1331
+ for (let index = raw.length - 1; index >= 0; index -= 1) {
1332
+ const item = raw[index];
1333
+ if (item !== void 0 && Math.abs(item.startBeat - at2) < BEAT_EPS) {
1334
+ return item;
1335
+ }
1336
+ }
1337
+ return void 0;
1338
+ }
1339
+ function materialOnsetIn(played, from, to) {
1340
+ if (played === void 0) {
1341
+ return void 0;
1342
+ }
1343
+ let last;
1344
+ for (const note2 of played.material.notes) {
1345
+ const at2 = played.tileStart + note2.step * STEP_BEATS;
1346
+ if (at2 < from - BEAT_EPS || at2 > to - BEAT_EPS) {
1347
+ continue;
1348
+ }
1349
+ last = last === void 0 ? at2 : Math.max(last, at2);
1350
+ }
1351
+ return last;
1352
+ }
1353
+ function lastPitchBefore(raw, at2, fallback) {
1354
+ for (let index = raw.length - 1; index >= 0; index -= 1) {
1355
+ const item = raw[index];
1356
+ if (item !== void 0 && item.startBeat < at2 - BEAT_EPS) {
1357
+ return item.pitch;
1358
+ }
1359
+ }
1360
+ return fallback;
1361
+ }
1098
1362
  function anchorStepOf(material, spanBeats) {
1099
1363
  let first;
1100
1364
  for (const note2 of material.notes) {
@@ -1102,15 +1366,19 @@ function anchorStepOf(material, spanBeats) {
1102
1366
  first = note2.step;
1103
1367
  }
1104
1368
  }
1105
- if (first === void 0 || first < 0 || first * STEP_BEATS >= spanBeats - EPS2) {
1369
+ if (first === void 0 || first < 0 || first * STEP_BEATS >= spanBeats - BEAT_EPS) {
1106
1370
  return 0;
1107
1371
  }
1108
1372
  return first;
1109
1373
  }
1110
- function fitToSegment(material, spanBeats, density, resolved, draw, path, bpm, difficulty) {
1374
+ function fitToSegment(material, spanBeats, density, resolved, draw, path, bpm, difficulty, ts) {
1111
1375
  const deformed = deform(
1112
1376
  material.notes,
1113
1377
  {
1378
+ // The figure is thinned against the bar it is played in, not against a
1379
+ // four-four ladder: a downbeat of the meter in force must outrank an
1380
+ // offbeat of some other meter.
1381
+ ts,
1114
1382
  // The one density the whole generator reads, defaults included: passing
1115
1383
  // the documented default explicitly and leaving it out have to be the
1116
1384
  // same request, and they were not while the figure was deformed by the
@@ -1125,7 +1393,7 @@ function fitToSegment(material, spanBeats, density, resolved, draw, path, bpm, d
1125
1393
  ...path
1126
1394
  );
1127
1395
  const spanSteps = spanBeats / STEP_BEATS;
1128
- const inside = deformed.filter((lickNote) => lickNote.step < spanSteps - EPS2);
1396
+ const inside = deformed.filter((lickNote) => lickNote.step < spanSteps - BEAT_EPS);
1129
1397
  if (inside.length === 0 || !withinCeiling(inside, bpm, difficulty)) {
1130
1398
  return void 0;
1131
1399
  }
@@ -1140,28 +1408,32 @@ var BASS_STYLES = Object.freeze([
1140
1408
  "walking",
1141
1409
  "arpeggio"
1142
1410
  ]);
1143
- var DEFAULT_TS2 = { numerator: 4, denominator: 4 };
1144
1411
  var DEFAULT_STYLE = "root";
1145
- var DEFAULT_OCTAVE2 = 2;
1146
1412
  var DEFAULT_PICKUP_DENSITY = 0.35;
1413
+ function sounding(ctx, placed) {
1414
+ return ctx.instrument ? foldIntoRange(placed, ctx.instrument) : placed;
1415
+ }
1416
+ function placeTone(ctx, pc) {
1417
+ return ctx.overBass ? placeAboveBass(pc, ctx.rootMidi) : placePc(pc, ctx.rootMidi, ctx.low);
1418
+ }
1147
1419
  function emit(ctx, pos, pc, midiOverride) {
1148
- const placed = clampToMidi(
1149
- midiOverride ?? placePc(pc, ctx.prevMidi, ctx.low),
1150
- "generated bass pitch"
1151
- );
1152
- const midi = ctx.instrument ? foldIntoRange(placed, ctx.instrument) : placed;
1420
+ const placed = clampToMidi(midiOverride ?? placeTone(ctx, pc), "generated bass pitch");
1421
+ const midi = sounding(ctx, placed);
1153
1422
  const velocity = isStrongBeat(pos, ctx.ts) ? STRONG_VELOCITY : WEAK_VELOCITY;
1154
1423
  ctx.notes.push({ startBeat: pos, pitch: midi, velocity });
1424
+ ctx.prevMidi = midi;
1155
1425
  return midi;
1156
1426
  }
1157
1427
  function buildRoot(ctx, seg) {
1158
- ctx.prevMidi = emit(ctx, seg.startBeat, bassPcOf(seg.chord));
1428
+ emit(ctx, seg.startBeat, bassPcOf(seg.chord));
1159
1429
  }
1160
1430
  function buildRootFifth(ctx, seg) {
1161
- ctx.prevMidi = emit(ctx, seg.startBeat, bassPcOf(seg.chord));
1162
- const mid = (seg.startBeat + seg.endBeat) / 2;
1163
- if (mid > seg.startBeat + EPS && mid < seg.endBeat - EPS) {
1164
- ctx.prevMidi = emit(ctx, mid, fifthPcOf(seg.chord));
1431
+ for (const tile of barTiles(seg.startBeat, seg.endBeat, ctx.ts)) {
1432
+ emit(ctx, tile.startBeat, bassPcOf(seg.chord));
1433
+ const answer = midBarPulse(tile.startBeat, ctx.ts);
1434
+ if (answer > tile.startBeat + BEAT_EPS && answer < tile.endBeat - BEAT_EPS) {
1435
+ emit(ctx, answer, fifthPcOf2(seg.chord));
1436
+ }
1165
1437
  }
1166
1438
  }
1167
1439
  function buildPop(ctx, seg, index) {
@@ -1170,21 +1442,37 @@ function buildPop(ctx, seg, index) {
1170
1442
  let emitted = false;
1171
1443
  for (const pos of positions) {
1172
1444
  if (isStrongBeat(pos, ctx.ts)) {
1173
- ctx.prevMidi = emit(ctx, pos, rootPc);
1445
+ emit(ctx, pos, rootPc);
1174
1446
  emitted = true;
1175
1447
  } else if (ctx.draw.prob(ctx.pickupDensity, "pickup", index, pos)) {
1176
- if (ctx.draw.prob(0.5, "pickupKind", index, pos) && reachableLeap(ctx, pos, 12)) {
1177
- const base = placePc(rootPc, ctx.prevMidi, ctx.low);
1178
- const dropped = base - 12;
1179
- emit(ctx, pos, rootPc, dropped >= 0 ? dropped : base);
1180
- } else {
1181
- ctx.prevMidi = emit(ctx, pos, fifthPcOf(seg.chord));
1182
- }
1448
+ emitted = pickup(ctx, seg, pos, index) || emitted;
1183
1449
  }
1184
1450
  }
1185
1451
  if (!emitted) {
1186
- ctx.prevMidi = emit(ctx, seg.startBeat, rootPc);
1452
+ emit(ctx, seg.startBeat, rootPc);
1453
+ }
1454
+ }
1455
+ function pickup(ctx, seg, pos, index) {
1456
+ const octave = octavePickup(ctx);
1457
+ if (ctx.draw.prob(0.5, "pickupKind", index, pos) && octave !== void 0 && reachableLeap(ctx, pos, Math.abs(sounding(ctx, octave) - ctx.prevMidi))) {
1458
+ emit(ctx, pos, bassPcOf(seg.chord), octave);
1459
+ return true;
1460
+ }
1461
+ const fifthPc = fifthPcOf2(seg.chord);
1462
+ if (sounding(ctx, placeTone(ctx, fifthPc)) === sounding(ctx, ctx.rootMidi)) {
1463
+ return false;
1464
+ }
1465
+ emit(ctx, pos, fifthPc);
1466
+ return true;
1467
+ }
1468
+ function octavePickup(ctx) {
1469
+ const root = sounding(ctx, ctx.rootMidi);
1470
+ for (const candidate of [ctx.rootMidi - 12, ctx.rootMidi + 12]) {
1471
+ if (candidate >= 0 && candidate <= 127 && sounding(ctx, candidate) !== root) {
1472
+ return candidate;
1473
+ }
1187
1474
  }
1475
+ return void 0;
1188
1476
  }
1189
1477
  function reachableLeap(ctx, pos, semitones) {
1190
1478
  const previous = ctx.notes[ctx.notes.length - 1];
@@ -1192,63 +1480,47 @@ function reachableLeap(ctx, pos, semitones) {
1192
1480
  return sustainsShift(semitones, available, ctx.bpm, ctx.difficulty);
1193
1481
  }
1194
1482
  function buildArpeggio(ctx, seg) {
1195
- const tones = chordTonePcs(seg.chord);
1483
+ const tones = bassToneCycle(seg.chord);
1196
1484
  const positions = beatPositions(seg.startBeat, seg.endBeat, ctx.ts);
1197
1485
  positions.forEach((pos, i) => {
1198
1486
  const pc = tones[i % tones.length] ?? bassPcOf(seg.chord);
1199
- ctx.prevMidi = emit(ctx, pos, pc);
1487
+ emit(ctx, pos, pc);
1200
1488
  });
1201
1489
  }
1202
1490
  function approachNote2(ctx, target, from, index) {
1203
- return approachNote(target, from, ctx.low, ctx.key, ctx.draw.prob(0.5, "approach", index));
1491
+ return approachNote(target, from, ctx.key, ctx.draw.prob(0.5, "approach", index));
1204
1492
  }
1205
1493
  function buildWalking(ctx, seg, next, index) {
1206
- const tones = chordTonePcs(seg.chord);
1494
+ const tones = bassToneCycle(seg.chord);
1207
1495
  const positions = beatPositions(seg.startBeat, seg.endBeat, ctx.ts);
1208
1496
  const count = positions.length;
1209
1497
  positions.forEach((pos, i) => {
1210
1498
  if (i === 0) {
1211
- ctx.prevMidi = emit(ctx, pos, bassPcOf(seg.chord));
1499
+ emit(ctx, pos, bassPcOf(seg.chord));
1212
1500
  return;
1213
1501
  }
1214
1502
  if (next && count > 1 && i === count - 1) {
1215
- const nextBass = placePc(bassPcOf(next.chord), ctx.prevMidi, ctx.low);
1503
+ const nextBass = placeRoot(bassPcOf(next.chord), ctx.low);
1216
1504
  const midi = approachNote2(ctx, nextBass, ctx.prevMidi, index);
1217
- ctx.prevMidi = emit(ctx, pos, pitchClassOf(midi), midi);
1505
+ emit(ctx, pos, pitchClassOf(midi), midi);
1218
1506
  return;
1219
1507
  }
1220
1508
  const pc = tones[i % tones.length] ?? bassPcOf(seg.chord);
1221
- ctx.prevMidi = emit(ctx, pos, pc);
1509
+ emit(ctx, pos, pc);
1222
1510
  });
1223
1511
  }
1224
1512
  function generateBassLine(opts) {
1513
+ assertRecord(opts, "bass line options");
1514
+ assertArray(opts.segments, "bass segments");
1225
1515
  assertGenerationBudget(opts.segments.length, "bass segments", opts.budget);
1226
- for (let index = 0; index < opts.segments.length; index += 1) {
1227
- const segment = opts.segments[index];
1228
- if (!segment) continue;
1229
- assertRange(segment.startBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].startBeat`);
1230
- assertRange(segment.endBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].endBeat`);
1231
- if (segment.endBeat <= segment.startBeat) {
1232
- throw new InvalidInputError(`segments[${index}] must have a positive duration`);
1233
- }
1234
- }
1235
1516
  const segments = opts.segments.map((segment) => ({ ...segment, chord: toChordData(segment.chord) })).sort((a, b) => a.startBeat - b.startBeat);
1236
- for (let index = 1; index < segments.length; index += 1) {
1237
- const previous = segments[index - 1];
1238
- const current = segments[index];
1239
- if (previous !== void 0 && current !== void 0 && current.startBeat < previous.endBeat) {
1240
- throw new InvalidInputError(
1241
- `bass segments must not overlap: segments ${index - 1} and ${index} overlap`
1242
- );
1243
- }
1244
- }
1517
+ assertBassSegments(segments);
1245
1518
  if (segments.length === 0) {
1246
1519
  return [];
1247
1520
  }
1248
- const ts = opts.ts ?? DEFAULT_TS2;
1249
- assertTimeSignature(ts);
1521
+ const ts = meterAt(0, toMeterData(opts.ts ?? DEFAULT_TS, "ts"));
1250
1522
  const style = assertOneOf(opts.style ?? DEFAULT_STYLE, BASS_STYLES, "bass style");
1251
- const octave = opts.octave ?? DEFAULT_OCTAVE2;
1523
+ const octave = opts.octave ?? DEFAULT_OCTAVE;
1252
1524
  assertInteger(octave, "bass octave", -1, 8);
1253
1525
  const estimatedNotes = segments.reduce(
1254
1526
  (count, segment) => count + Math.max(1, Math.ceil(segment.endBeat - segment.startBeat)),
@@ -1256,15 +1528,15 @@ function generateBassLine(opts) {
1256
1528
  );
1257
1529
  assertGenerationBudget(estimatedNotes, "bass notes", opts.budget);
1258
1530
  const resolved = resolveContext(opts.ctx);
1259
- const named = resolved.instrument("bass");
1260
- const instrument = opts.instrument ?? (named !== void 0 && named.kind === "stringed" ? named : void 0);
1261
- const low = bandFloor(octave * 12 + 12, instrument);
1531
+ const { instrument, low } = bassRegister(resolved.instrument("bass"), opts.instrument, octave);
1262
1532
  const ctx = {
1263
1533
  ts,
1264
1534
  low,
1265
1535
  key: toKeyScale(opts.key),
1266
1536
  draw: resolved.part("bass"),
1267
1537
  notes: [],
1538
+ rootMidi: low,
1539
+ overBass: false,
1268
1540
  prevMidi: low,
1269
1541
  instrument,
1270
1542
  pickupDensity: resolved.rhythmic ?? DEFAULT_PICKUP_DENSITY,
@@ -1276,6 +1548,8 @@ function generateBassLine(opts) {
1276
1548
  if (!seg) {
1277
1549
  continue;
1278
1550
  }
1551
+ ctx.rootMidi = placeRoot(bassPcOf(seg.chord), ctx.low);
1552
+ ctx.overBass = seg.chord.bassPc !== void 0;
1279
1553
  switch (style) {
1280
1554
  case "rootFifth":
1281
1555
  buildRootFifth(ctx, seg);
@@ -1304,7 +1578,7 @@ function generateBassLine(opts) {
1304
1578
  }
1305
1579
  const nextStart = ctx.notes[i + 1]?.startBeat ?? lastEnd;
1306
1580
  const durationBeat = nextStart - note2.startBeat;
1307
- if (durationBeat <= EPS) {
1581
+ if (durationBeat <= BEAT_EPS) {
1308
1582
  continue;
1309
1583
  }
1310
1584
  out.push({
@@ -1331,6 +1605,7 @@ function assertPlayable(pitch) {
1331
1605
  }
1332
1606
  function imitate(lead, opts) {
1333
1607
  assertNoteEvents(lead, "imitate lead", { allowNonPositiveDuration: true });
1608
+ assertRecord(opts, "opts");
1334
1609
  assertFiniteNumber(opts.atBeat, "atBeat");
1335
1610
  const interval = toSpelledInterval(opts.interval);
1336
1611
  const key = toKeyScale(opts.key);
@@ -1371,9 +1646,9 @@ function imitate(lead, opts) {
1371
1646
  }
1372
1647
  pitch = tonal ? shiftByScaleDegrees(pitch, degrees, key) + offset : pitch + Math.round(interval.semitones);
1373
1648
  const answer = {
1649
+ ...note2,
1374
1650
  pitch: assertPlayable(pitch),
1375
- startBeat: note2.startBeat + shift,
1376
- durationBeat: note2.durationBeat
1651
+ startBeat: note2.startBeat + shift
1377
1652
  };
1378
1653
  if (note2.velocity !== void 0) {
1379
1654
  answer.velocity = Math.max(1, Math.min(127, Math.round(note2.velocity * velocityScale)));
@@ -1383,8 +1658,6 @@ function imitate(lead, opts) {
1383
1658
  }
1384
1659
 
1385
1660
  // src/generate/countermelody/index.ts
1386
- var DEFAULT_TS3 = { numerator: 4, denominator: 4 };
1387
- var EPS3 = 1e-9;
1388
1661
  var GRID_STEP = 0.5;
1389
1662
  var MIN_ONSET_GAP = 1;
1390
1663
  var REGISTER_OFFSET = 12;
@@ -1396,7 +1669,7 @@ var DEFAULT_MELODY_VELOCITY = 96;
1396
1669
  var VELOCITY_DROP = 16;
1397
1670
  var TIE_BREAK_JITTER = 1e-6;
1398
1671
  function isWholeBeat(beat) {
1399
- return Math.abs(beat - Math.round(beat)) < EPS3;
1672
+ return Math.abs(beat - Math.round(beat)) < BEAT_EPS;
1400
1673
  }
1401
1674
  function melodyNoteAt(melody, beat) {
1402
1675
  return melody.at(beat)?.note;
@@ -1409,8 +1682,8 @@ function melodySnapshotAt(melody, beat) {
1409
1682
  if (current === void 0) {
1410
1683
  return void 0;
1411
1684
  }
1412
- const attacked = Math.abs(current.startBeat - beat) < EPS3;
1413
- const previous = attacked ? melodyNoteAt(melody, beat - EPS3 * 2) : current;
1685
+ const attacked = Math.abs(current.startBeat - beat) < BEAT_EPS;
1686
+ const previous = attacked ? melodyNoteAt(melody, beat - BEAT_EPS * 2) : current;
1414
1687
  const snapshot = { pitch: current.pitch };
1415
1688
  if (previous !== void 0) {
1416
1689
  snapshot.prevPitch = previous.pitch;
@@ -1420,22 +1693,24 @@ function melodySnapshotAt(melody, beat) {
1420
1693
  function followOnsets(melody) {
1421
1694
  return [...new Set(melody.notes.map(({ note: note2 }) => note2.startBeat))];
1422
1695
  }
1696
+ var DEFAULT_TS2 = { numerator: 4, denominator: 4 };
1697
+ var SCORE_EPS = 1e-9;
1423
1698
  function complementOnsets(melody, ts, spanStart, spanEnd, draw) {
1424
1699
  const onsets = [];
1425
1700
  const gridStart = Math.floor(spanStart / GRID_STEP) * GRID_STEP;
1426
1701
  let last = Number.NEGATIVE_INFINITY;
1427
- for (let beat = gridStart; beat < spanEnd - EPS3; beat += GRID_STEP) {
1702
+ for (let beat = gridStart; beat < spanEnd - BEAT_EPS; beat += GRID_STEP) {
1428
1703
  const attacked = melodyAttacksAt(melody, beat);
1429
- const sounding = melodyNoteAt(melody, beat) !== void 0;
1704
+ const sounding2 = melodyNoteAt(melody, beat) !== void 0;
1430
1705
  let place = false;
1431
- if (!attacked && !sounding) {
1706
+ if (!attacked && !sounding2) {
1432
1707
  place = isWholeBeat(beat);
1433
1708
  } else if (!attacked) {
1434
1709
  place = draw.prob(isWholeBeat(beat) ? HOLD_ON_BEAT_PROB : HOLD_OFF_BEAT_PROB, "hold", beat);
1435
1710
  } else if (isStrongBeat(beat, ts)) {
1436
1711
  place = draw.prob(REINFORCE_PROB, "reinforce", beat);
1437
1712
  }
1438
- if (place && beat - last >= MIN_ONSET_GAP - EPS3) {
1713
+ if (place && beat - last >= MIN_ONSET_GAP - BEAT_EPS) {
1439
1714
  onsets.push(beat);
1440
1715
  last = beat;
1441
1716
  }
@@ -1451,6 +1726,19 @@ function keepsImperfectInterval(before, now) {
1451
1726
  }
1452
1727
  return before <= 4 === now <= 4;
1453
1728
  }
1729
+ function assertWeightOverrides(overrides) {
1730
+ if (overrides === void 0) {
1731
+ return;
1732
+ }
1733
+ if (typeof overrides !== "object" || overrides === null) {
1734
+ throw new InvalidInputError(
1735
+ `countermelody weights must be an object of weight overrides; received ${describeRejected(overrides)}`
1736
+ );
1737
+ }
1738
+ for (const [field, value] of Object.entries(overrides)) {
1739
+ assertFiniteNumber(value, `countermelody weights.${field}`);
1740
+ }
1741
+ }
1454
1742
  function scoreCandidate(pitch, melPitch, melPrev, prevPitch, chord, center, weights) {
1455
1743
  let score = 0;
1456
1744
  const ic = melPitch === void 0 ? void 0 : Math.abs(pitch - melPitch) % 12;
@@ -1498,12 +1786,12 @@ function heldNoteBoundaries(melody, startBeat, endBeat, chordChangeBeats) {
1498
1786
  boundaries.add(onset);
1499
1787
  }
1500
1788
  for (const beat of chordChangeBeats) {
1501
- if (beat > startBeat + EPS3 && beat < endBeat - EPS3) {
1789
+ if (beat > startBeat + BEAT_EPS && beat < endBeat - BEAT_EPS) {
1502
1790
  boundaries.add(beat);
1503
1791
  }
1504
1792
  }
1505
1793
  const firstGrid = Math.floor(startBeat / GRID_STEP + 1) * GRID_STEP;
1506
- for (let beat = firstGrid; beat < endBeat - EPS3; beat += GRID_STEP) {
1794
+ for (let beat = firstGrid; beat < endBeat - BEAT_EPS; beat += GRID_STEP) {
1507
1795
  boundaries.add(beat);
1508
1796
  }
1509
1797
  return [...boundaries].sort((a, b) => a - b);
@@ -1517,7 +1805,7 @@ function boundaryContexts(melody, harmony, startBeat, endBeat, ts) {
1517
1805
  otherVoices: melodyVoice === void 0 ? [] : [melodyVoice],
1518
1806
  chord: harmony.chordAt(boundary),
1519
1807
  strongBeat: isStrongBeat(boundary, ts),
1520
- atCounterOnset: Math.abs(boundary - startBeat) < EPS3
1808
+ atCounterOnset: Math.abs(boundary - startBeat) < BEAT_EPS
1521
1809
  };
1522
1810
  });
1523
1811
  }
@@ -1550,6 +1838,7 @@ function heldPitchSafety(pitch, contexts, prevPitch, key, profile, register, low
1550
1838
  return worst;
1551
1839
  }
1552
1840
  function generateCounterMelody(opts) {
1841
+ assertRecord(opts, "countermelody options");
1553
1842
  const timeline = opts.timeline;
1554
1843
  const chordAt = timeline === void 0 ? opts.chordAt : (beat) => timeline.at(beat);
1555
1844
  if (chordAt === void 0) {
@@ -1561,10 +1850,11 @@ function generateCounterMelody(opts) {
1561
1850
  const melody = createNoteEventIndex(soundingNotesOnly(opts.melody), {
1562
1851
  tieBreak: register === "below" ? "highest" : "lowest"
1563
1852
  });
1564
- const ts = opts.ts ?? DEFAULT_TS3;
1853
+ const ts = meterAt(0, toMeterData(opts.ts ?? DEFAULT_TS2, "ts"));
1565
1854
  assertTimeSignature(ts);
1566
1855
  const rhythm = assertOneOf(opts.rhythm ?? "complement", ["complement", "follow"], "rhythm");
1567
1856
  const profile = assertOneOf(opts.profile ?? "pop", ["strict", "pop"], "safety profile");
1857
+ assertWeightOverrides(opts.weights);
1568
1858
  const weights = profileWeights(profile, opts.weights);
1569
1859
  const key = toKeyScale(opts.key);
1570
1860
  const draw = resolveContext(opts.ctx).part("countermelody");
@@ -1662,8 +1952,8 @@ function generateCounterMelody(opts) {
1662
1952
  }
1663
1953
  }
1664
1954
  const score = scoreCandidate(candidate, melPitch, melPrev, prevPitch, chord, center, weights) + (jitter[candidate - low] ?? 0);
1665
- const tie = Math.abs(score - bestScore) <= EPS3 && candidate < (bestPitch ?? Number.POSITIVE_INFINITY);
1666
- if (worstSafety < bestWorstSafety || worstSafety === bestWorstSafety && (score > bestScore + EPS3 || tie)) {
1955
+ const tie = Math.abs(score - bestScore) <= SCORE_EPS && candidate < (bestPitch ?? Number.POSITIVE_INFINITY);
1956
+ if (worstSafety < bestWorstSafety || worstSafety === bestWorstSafety && (score > bestScore + SCORE_EPS || tie)) {
1667
1957
  bestWorstSafety = worstSafety;
1668
1958
  bestScore = score;
1669
1959
  bestPitch = candidate;
@@ -1742,8 +2032,8 @@ function getGhostVelocity(section, beatPosition) {
1742
2032
  }
1743
2033
  return Math.max(0.25, Math.min(0.65, base));
1744
2034
  }
1745
- function getGhostProbabilityAtPosition(beat, sixteenthInBeat) {
1746
- const leadsIntoBackbeat = (beat === 0 || beat === 2) && sixteenthInBeat === 3;
2035
+ function getGhostProbabilityAtPosition(beat, sixteenthInBeat, leadIns) {
2036
+ const leadsIntoBackbeat = leadIns.includes(beat) && sixteenthInBeat === 3;
1747
2037
  return leadsIntoBackbeat ? 0.6 : 0.25;
1748
2038
  }
1749
2039
  function selectGhostPositions(mood) {
@@ -1973,6 +2263,12 @@ function shouldAddOpenHHAccent(section, beat, bar2, draw) {
1973
2263
  function footHiHatVelocity(draw, bar2, beat) {
1974
2264
  return draw.range(FHH_VEL_MIN, FHH_VEL_MAX, "fhhVelocity", bar2, beat);
1975
2265
  }
2266
+ function usesOffbeatOpenHiHat(style) {
2267
+ return style === "fourOnFloor";
2268
+ }
2269
+ function offbeatOpenHiHatChance(bpm) {
2270
+ return Math.max(0.15, Math.min(0.8, 45 / bpm));
2271
+ }
1976
2272
  function shouldUseRideForSection(section, style) {
1977
2273
  if (style === "rock" && section === "chorus") {
1978
2274
  return true;
@@ -1982,9 +2278,6 @@ function shouldUseRideForSection(section, style) {
1982
2278
  }
1983
2279
  return section === "bridge";
1984
2280
  }
1985
- function shouldUseBridgeCrossStick(section, beat) {
1986
- return section === "bridge" && (beat === 1 || beat === 3);
1987
- }
1988
2281
 
1989
2282
  // src/generate/drums/swing.ts
1990
2283
  var SIXTEENTH2 = 0.25;
@@ -2072,36 +2365,50 @@ function generateKickForBeat(ctx, sec, kick) {
2072
2365
  ctx.track.add(GM.BD, tick, offBeat ? SIXTEENTH : EIGHTH, ctx.velocity * 0.85);
2073
2366
  }
2074
2367
  }
2075
- function generateSnareForBeat(ctx, sec, isIntroFirst) {
2076
- if (ctx.inPrechorusLift) {
2077
- return;
2368
+ function roleBackbeatWeight(role) {
2369
+ if (role === "minimal") {
2370
+ return 0.65;
2078
2371
  }
2079
- const snareOn = sec.snareBeat3 ? ctx.beat === 2 : ctx.beat === 1 || ctx.beat === 3;
2080
- if (!snareOn || isIntroFirst) {
2081
- return;
2372
+ return role === "ambient" ? 0.8 : 1;
2373
+ }
2374
+ function backbeatVoice(sec) {
2375
+ if (sec.role === "fxOnly") {
2376
+ return void 0;
2082
2377
  }
2083
- if (sec.useRide && shouldUseBridgeCrossStick(ctx.section, ctx.beat)) {
2084
- return;
2378
+ const sideStick = sec.style === "latin" || sec.role === "ambient" || sec.role === "minimal";
2379
+ return sideStick ? GM.SIDESTICK : GM.SD;
2380
+ }
2381
+ function writesSnareGhosts(sec) {
2382
+ return backbeatVoice(sec) === GM.SD && sec.style !== "sparse";
2383
+ }
2384
+ function backbeatStroke(sec, velocity) {
2385
+ const pitch = backbeatVoice(sec);
2386
+ if (pitch === void 0) {
2387
+ return void 0;
2085
2388
  }
2086
- const backbeatVel = Math.min(127, ctx.velocity + BACKBEAT_LIFT);
2087
- const promoteSparseChorus = sec.style === "sparse" && ctx.section === "chorus" && sec.role === "full";
2088
- if (sec.role === "fxOnly") {
2389
+ return pitch === GM.SD ? { pitch, velocity: Math.min(127, velocity + BACKBEAT_LIFT) } : { pitch, velocity: velocity * roleBackbeatWeight(sec.role) };
2390
+ }
2391
+ var FOUR_FOUR_BEATS = 4;
2392
+ function generateSnareForBeat(ctx, sec, isIntroFirst) {
2393
+ if (ctx.inPrechorusLift) {
2089
2394
  return;
2090
2395
  }
2091
- if (sec.role === "minimal") {
2092
- ctx.track.add(GM.SIDESTICK, ctx.beatTick, EIGHTH, ctx.velocity * 0.65);
2396
+ if (!backbeatBeats(sec.snareBeat3).includes(ctx.beat) || isIntroFirst) {
2093
2397
  return;
2094
2398
  }
2095
- if (promoteSparseChorus) {
2096
- ctx.track.add(GM.SD, ctx.beatTick, EIGHTH, backbeatVel);
2097
- } else if (sec.style === "sparse" || sec.role === "ambient") {
2098
- ctx.track.add(GM.SIDESTICK, ctx.beatTick, EIGHTH, ctx.velocity * 0.8);
2099
- } else {
2100
- ctx.track.add(GM.SD, ctx.beatTick, EIGHTH, backbeatVel);
2399
+ const stroke = backbeatStroke(sec, ctx.velocity);
2400
+ if (!stroke) {
2401
+ return;
2101
2402
  }
2403
+ ctx.track.add(stroke.pitch, ctx.beatTick, EIGHTH, stroke.velocity);
2102
2404
  }
2103
2405
  function generateGhostNotesForBeat(ctx, sec) {
2104
- if (ctx.beat !== 0 && ctx.beat !== 2) {
2406
+ if (!writesSnareGhosts(sec)) {
2407
+ return;
2408
+ }
2409
+ const backbeats = backbeatBeats(sec.snareBeat3);
2410
+ const leadIns = backbeats.map((beat) => (beat - 1 + FOUR_FOUR_BEATS) % FOUR_FOUR_BEATS);
2411
+ if (!leadIns.includes(ctx.beat)) {
2105
2412
  return;
2106
2413
  }
2107
2414
  if (!sustainsStrokes(SIXTEENTH, ctx.bpm, sec.difficulty)) {
@@ -2114,7 +2421,7 @@ function generateGhostNotesForBeat(ctx, sec) {
2114
2421
  }
2115
2422
  for (const pos of positions) {
2116
2423
  const sixteenthInBeat = pos === "e" ? 1 : 3;
2117
- const posProb = getGhostProbabilityAtPosition(ctx.beat, sixteenthInBeat);
2424
+ const posProb = getGhostProbabilityAtPosition(ctx.beat, sixteenthInBeat, leadIns);
2118
2425
  if (!ctx.draw.prob(ghostProb * posProb, "ghost", ctx.bar, ctx.beat, pos)) {
2119
2426
  continue;
2120
2427
  }
@@ -2154,40 +2461,47 @@ function generatePreChorusBuildup(ctx, sec, isSectionLastBar) {
2154
2461
  );
2155
2462
  }
2156
2463
  if (isSectionLastBar && ctx.beat === 3) {
2157
- ctx.track.add(GM.CRASH, ctx.beatTick + EIGHTH + SIXTEENTH, SIXTEENTH, ctx.velocity * 1.1);
2464
+ ctx.track.add(
2465
+ GM.CRASH,
2466
+ swing16(ctx.beatTick + EIGHTH + SIXTEENTH, sec, ctx.swingAmount),
2467
+ SIXTEENTH,
2468
+ ctx.velocity * 1.1
2469
+ );
2158
2470
  }
2159
2471
  }
2472
+ function opensOnDownbeat(ctx, sec) {
2473
+ if (usesOffbeatOpenHiHat(sec.style)) {
2474
+ return false;
2475
+ }
2476
+ return shouldAddOpenHHAccent(ctx.section, ctx.beat, ctx.bar, ctx.draw);
2477
+ }
2478
+ function opensOffbeat(ctx, sec) {
2479
+ if (!usesOffbeatOpenHiHat(sec.style) || ctx.beat !== 1 && ctx.beat !== 3) {
2480
+ return false;
2481
+ }
2482
+ return ctx.draw.prob(offbeatOpenHiHatChance(ctx.bpm), "openHat", ctx.bar, ctx.beat, 1);
2483
+ }
2160
2484
  function generateHiHatForBeat(ctx, sec) {
2161
2485
  if (!shouldPlayHiHat(sec.role)) {
2162
2486
  return;
2163
2487
  }
2164
- const crossStick = sec.useRide && shouldUseBridgeCrossStick(ctx.section, ctx.beat);
2165
- const hhInstrument = crossStick ? GM.SIDESTICK : roleHiHatInstrument(sec.role, sec.useRide);
2488
+ const hhInstrument = roleHiHatInstrument(sec.role, sec.useRide);
2166
2489
  const hhType = sectionHiHatType(ctx.section, sec.role);
2167
2490
  const typeMult = hiHatTypeVelocityMultiplier(hhType);
2168
- const isDynamicOpen = ctx.barHasOpenHh && ctx.beat === ctx.openHhBeat;
2169
2491
  const allowsOpenHiHat = sec.role !== "ambient" && sec.role !== "minimal";
2492
+ const isDynamicOpen = ctx.barHasOpenHh && ctx.beat === ctx.openHhBeat && !usesOffbeatOpenHiHat(sec.style);
2170
2493
  const dm = sec.densityMult;
2171
2494
  if (ctx.hhLevel === "quarter") {
2172
- const introRest = ctx.section === "intro" && ctx.beat !== 0;
2173
- if (!introRest) {
2174
- if (isDynamicOpen && allowsOpenHiHat) {
2175
- ctx.track.add(
2176
- GM.OHH,
2177
- ctx.beatTick,
2178
- EIGHTH,
2179
- ctx.velocity * dm * 0.75 * typeMult + OHH_VEL_BOOST
2180
- );
2181
- } else {
2182
- ctx.track.add(
2183
- hhInstrument,
2184
- ctx.beatTick,
2185
- EIGHTH,
2186
- Math.max(20, ctx.velocity * dm * 0.75 * typeMult)
2187
- );
2188
- }
2189
- } else if (sec.useFootHh) {
2190
- ctx.track.add(GM.FHH, ctx.beatTick, EIGHTH, footHiHatVelocity(ctx.draw, ctx.bar, ctx.beat));
2495
+ if (ctx.section === "intro" && ctx.beat !== 0) {
2496
+ return;
2497
+ }
2498
+ const hhVel = Math.max(20, ctx.velocity * dm * 0.75 * typeMult);
2499
+ if (allowsOpenHiHat && isDynamicOpen) {
2500
+ ctx.track.add(GM.OHH, ctx.beatTick, EIGHTH, hhVel + OHH_VEL_BOOST);
2501
+ } else if (allowsOpenHiHat && opensOnDownbeat(ctx, sec)) {
2502
+ ctx.track.add(hiHatNote("open"), ctx.beatTick, EIGHTH, Math.max(20, hhVel * 1.1));
2503
+ } else {
2504
+ ctx.track.add(hhInstrument, ctx.beatTick, EIGHTH, hhVel);
2191
2505
  }
2192
2506
  return;
2193
2507
  }
@@ -2208,13 +2522,7 @@ function generateHiHatForBeat(ctx, sec) {
2208
2522
  ctx.track.add(GM.OHH, hhTick, EIGHTH, hhVel + OHH_VEL_BOOST);
2209
2523
  continue;
2210
2524
  }
2211
- let useOpen = false;
2212
- if (sec.style === "fourOnFloor" && eighth === 1) {
2213
- const openProb = Math.max(0.15, Math.min(0.8, 45 / ctx.bpm));
2214
- useOpen = (ctx.beat === 1 || ctx.beat === 3) && ctx.draw.prob(openProb, "openHat", ctx.bar, ctx.beat, eighth);
2215
- } else if (eighth === 0) {
2216
- useOpen = shouldAddOpenHHAccent(ctx.section, ctx.beat, ctx.bar, ctx.draw);
2217
- }
2525
+ const useOpen = eighth === 0 ? opensOnDownbeat(ctx, sec) : opensOffbeat(ctx, sec);
2218
2526
  if (useOpen && allowsOpenHiHat) {
2219
2527
  ctx.track.add(hiHatNote("open"), hhTick, EIGHTH, Math.max(20, hhVel * 1.1));
2220
2528
  } else {
@@ -2237,6 +2545,14 @@ function generateHiHatForBeat(ctx, sec) {
2237
2545
  ctx.track.add(GM.OHH, hhTick, SIXTEENTH, hhVel + OHH_VEL_BOOST);
2238
2546
  continue;
2239
2547
  }
2548
+ if (allowsOpenHiHat && sixteenth === 0 && opensOnDownbeat(ctx, sec)) {
2549
+ ctx.track.add(hiHatNote("open"), hhTick, SIXTEENTH, Math.max(20, hhVel * 1.1));
2550
+ continue;
2551
+ }
2552
+ if (allowsOpenHiHat && sixteenth === 2 && opensOffbeat(ctx, sec)) {
2553
+ ctx.track.add(hiHatNote("open"), hhTick, SIXTEENTH, Math.max(20, hhVel * 1.1));
2554
+ continue;
2555
+ }
2240
2556
  if (allowsOpenHiHat && ctx.beat === 3 && sixteenth === 3) {
2241
2557
  const openProb = Math.max(0.1, Math.min(0.4, 30 / ctx.bpm));
2242
2558
  if (ctx.draw.prob(openProb, "openHat", ctx.bar, ctx.beat, sixteenth)) {
@@ -2329,7 +2645,7 @@ function archetype(beat2, beat3, boundToBar) {
2329
2645
  ...boundToBar ? { boundToBar } : {}
2330
2646
  };
2331
2647
  }
2332
- var FILL_ARCHETYPES = Object.freeze({
2648
+ var FILL_ARCHETYPES = deepFreeze({
2333
2649
  // One crescendo of seven strokes across the two beats, not two of four and
2334
2650
  // three: the roll is a single gesture and its second half continues where its
2335
2651
  // first half left off.
@@ -2432,8 +2748,9 @@ function getFillStartBeat(energy) {
2432
2748
  case "medium":
2433
2749
  return 2;
2434
2750
  case "high":
2751
+ return 1;
2435
2752
  case "peak":
2436
- return 2;
2753
+ return 0;
2437
2754
  }
2438
2755
  }
2439
2756
  var SPARSE_FILLS = ["simpleCrash", "breakdownFill"];
@@ -2543,7 +2860,7 @@ function fillWithinCeiling(shape, fromBeat, barBeats, bpm, difficulty) {
2543
2860
  }
2544
2861
  return true;
2545
2862
  }
2546
- function generateFill(track, beatTick, beat, fillType, velocity) {
2863
+ function generateFill(track, beatTick, beat, fillType, velocity, place = (tick) => tick) {
2547
2864
  const shape = typeof fillType === "string" ? FILL_ARCHETYPES[fillType] : fillType;
2548
2865
  if (!shape) {
2549
2866
  return;
@@ -2556,7 +2873,7 @@ function generateFill(track, beatTick, beat, fillType, velocity) {
2556
2873
  const scaled = stroke.velocity.scale === void 0 ? base : base * stroke.velocity.scale;
2557
2874
  track.add(
2558
2875
  stroke.voice,
2559
- beatTick + stroke.offset,
2876
+ place(beatTick + stroke.offset),
2560
2877
  stroke.duration,
2561
2878
  scaled + (stroke.velocity.offset ?? 0),
2562
2879
  stroke.articulation
@@ -2584,17 +2901,30 @@ var DRUM_NOTES = Object.freeze({
2584
2901
  var VOICE_BY_NOTE = new Map(
2585
2902
  Object.entries(DRUM_NOTES).map(([voice, note2]) => [note2, voice])
2586
2903
  );
2904
+ function drumNoteOf(voice) {
2905
+ return Object.hasOwn(DRUM_NOTES, voice) ? DRUM_NOTES[voice] : void 0;
2906
+ }
2587
2907
  function drumVoiceOf(pitch) {
2588
- return VOICE_BY_NOTE.get(pitch);
2908
+ return VOICE_BY_NOTE.get(assertMidiPitch(pitch));
2589
2909
  }
2590
2910
  var ONSET_KEY_SCALE = 128;
2591
2911
  var HitList = class {
2592
2912
  hits = [];
2593
- /** Onset keys already occupied, as `pitch:quantizedBeat`. */
2594
- #onsets = /* @__PURE__ */ new Set();
2595
- /** Append a hit, clamping velocity and dropping non-positive positions. */
2913
+ /** Voices already struck at each quantized beat. */
2914
+ #onsets = /* @__PURE__ */ new Map();
2915
+ /**
2916
+ * Append a hit, clamping velocity and dropping non-positive positions.
2917
+ *
2918
+ * A voice already struck at this instant is left as it stands: one drum
2919
+ * sounds once however many voices ask for it, and a second entry at the same
2920
+ * pitch and position is a note-on a reader answers with no note-off, heard as
2921
+ * a hung or cut stroke rather than as a louder one.
2922
+ */
2596
2923
  add(pitch, startBeat, durationBeat, velocity, articulation) {
2597
2924
  const beat = Math.max(0, startBeat);
2925
+ if (this.hasOnset(pitch, beat)) {
2926
+ return;
2927
+ }
2598
2928
  const hit2 = {
2599
2929
  pitch,
2600
2930
  startBeat: beat,
@@ -2605,23 +2935,34 @@ var HitList = class {
2605
2935
  hit2.articulation = articulation;
2606
2936
  }
2607
2937
  this.hits.push(hit2);
2608
- this.#onsets.add(onsetKey(pitch, beat));
2938
+ const struck = this.#onsets.get(beatSlot(beat));
2939
+ if (struck) {
2940
+ struck.add(pitch);
2941
+ } else {
2942
+ this.#onsets.set(beatSlot(beat), /* @__PURE__ */ new Set([pitch]));
2943
+ }
2609
2944
  }
2610
2945
  /** Whether a hit of this pitch already starts exactly at `startBeat`. */
2611
2946
  hasOnset(pitch, startBeat) {
2612
- return this.#onsets.has(onsetKey(pitch, startBeat));
2947
+ return this.#onsets.get(beatSlot(startBeat))?.has(pitch) ?? false;
2613
2948
  }
2614
- /** True when a crash already sits within a 16th of `startBeat`. */
2949
+ /**
2950
+ * True when a crash already sits less than a 16th from `startBeat`, on either
2951
+ * side of it.
2952
+ */
2615
2953
  hasCrashNear(startBeat) {
2616
2954
  const steps = Math.round(0.25 * ONSET_KEY_SCALE);
2617
- for (let step = 0; step < steps; step += 1) {
2618
- if (this.#onsets.has(onsetKey(GM.CRASH, startBeat + step / ONSET_KEY_SCALE))) {
2955
+ for (let step = -steps + 1; step < steps; step += 1) {
2956
+ if (this.hasOnset(GM.CRASH, startBeat + step / ONSET_KEY_SCALE)) {
2619
2957
  return true;
2620
2958
  }
2621
2959
  }
2622
2960
  return false;
2623
2961
  }
2624
2962
  };
2963
+ function beatSlot(startBeat) {
2964
+ return Math.round(startBeat * ONSET_KEY_SCALE);
2965
+ }
2625
2966
  function onsetKey(pitch, startBeat) {
2626
2967
  return `${pitch}:${Math.round(startBeat * ONSET_KEY_SCALE)}`;
2627
2968
  }
@@ -2637,7 +2978,13 @@ var BEAT_3 = beatStep(2);
2637
2978
  var BEAT_4 = beatStep(3);
2638
2979
  var AND_2 = beatStep(1) + 2;
2639
2980
  var AND_4 = beatStep(3) + 2;
2981
+ var A_OF_3 = beatStep(2) + 3;
2640
2982
  var OUTRO_FIGURE = [{ step: BEAT_1 }, { step: BEAT_3 }];
2983
+ function statedSlots(figure) {
2984
+ return figure.filter(
2985
+ (slot) => slot.probability === void 0 && slot.sections === void 0 && slot.barParity === void 0
2986
+ );
2987
+ }
2641
2988
  var DEFAULT_FIGURE = [
2642
2989
  { step: BEAT_1 },
2643
2990
  { step: BEAT_3 },
@@ -2645,7 +2992,7 @@ var DEFAULT_FIGURE = [
2645
2992
  { step: AND_2, probability: 0.55, slot: "beat2and", sections: ["chorus"] },
2646
2993
  { step: AND_4, probability: 0.35, slot: "beat4and", sections: ["chorus"] }
2647
2994
  ];
2648
- var KICK_FIGURES = Object.freeze({
2995
+ var KICK_FIGURES = deepFreeze({
2649
2996
  sparse: [{ step: BEAT_1 }, { step: BEAT_3, sections: ["chorus"], barParity: 1 }],
2650
2997
  fourOnFloor: [
2651
2998
  { step: BEAT_1 },
@@ -2663,9 +3010,13 @@ var KICK_FIGURES = Object.freeze({
2663
3010
  rock: [
2664
3011
  { step: BEAT_1 },
2665
3012
  { step: BEAT_3 },
2666
- { step: AND_2, probability: 0.65, slot: "beat2and", sections: ["chorus"] },
2667
- { step: AND_4, probability: 0.4, slot: "beat4and", sections: ["chorus"] },
2668
- { step: AND_2, probability: 0.3, slot: "beat2and", sections: ["prechorus"] }
3013
+ // The broken beat: the "and" of two answers the snare and the "a" of three
3014
+ // pushes into the fourth, in every section. That displacement is the figure
3015
+ // rather than a slot a dial may or may not fill in a chorus, which is what
3016
+ // separates it from the plain pop pulse wherever it is played.
3017
+ { step: AND_2 },
3018
+ { step: A_OF_3 },
3019
+ { step: AND_4, probability: 0.4, slot: "beat4and", sections: ["chorus"] }
2669
3020
  ],
2670
3021
  synth: [
2671
3022
  { step: BEAT_1 },
@@ -2719,7 +3070,8 @@ function realiseKickFigure(figure, section, bar2, draw, rhythmic) {
2719
3070
  return pattern;
2720
3071
  }
2721
3072
  function getKickPattern(section, style, bar2, draw, rhythmic) {
2722
- const figure = section === "outro" ? OUTRO_FIGURE : KICK_FIGURES[style] ?? DEFAULT_FIGURE;
3073
+ const own = KICK_FIGURES[style] ?? DEFAULT_FIGURE;
3074
+ const figure = section === "outro" ? [...OUTRO_FIGURE, ...statedSlots(own)] : own;
2723
3075
  return realiseKickFigure(figure, section, bar2, draw, rhythmic);
2724
3076
  }
2725
3077
 
@@ -2771,8 +3123,9 @@ function generateAuxPercussionForBar(track, barStart, config, role, densityMult,
2771
3123
  if (role === "minimal") {
2772
3124
  return;
2773
3125
  }
3126
+ const backbeats = backbeatBeats(sec.snareBeat3, barBeats);
2774
3127
  if (config.tambourine) {
2775
- for (let beat = 1; beat < barBeats; beat += 2) {
3128
+ for (const beat of backbeats) {
2776
3129
  const raw = 70 * densityMult * draw.float(0.9, 1.1, "tambourine", barStart, beat);
2777
3130
  track.add(GM.TAMBOURINE, barStart + beat, EIGHTH, Math.max(40, Math.min(90, raw)));
2778
3131
  }
@@ -2811,7 +3164,7 @@ function generateAuxPercussionForBar(track, barStart, config, role, densityMult,
2811
3164
  }
2812
3165
  }
2813
3166
  if (config.handclap) {
2814
- for (let beat = 1; beat < barBeats; beat += 2) {
3167
+ for (const beat of backbeats) {
2815
3168
  const raw = 85 * densityMult * draw.float(0.9, 1.1, "handclap", barStart, beat);
2816
3169
  track.add(GM.HANDCLAP, barStart + beat, EIGHTH, Math.max(50, Math.min(100, raw)));
2817
3170
  }
@@ -2837,6 +3190,11 @@ var REACH_BY_VOICE = {
2837
3190
  lowTom: ["rightHand", "leftHand"],
2838
3191
  shaker: ["rightHand", "leftHand"]
2839
3192
  };
3193
+ var OVERDUB_VOICES = Object.freeze([
3194
+ "handClap",
3195
+ "tambourine",
3196
+ "shaker"
3197
+ ]);
2840
3198
  function reachByNote() {
2841
3199
  const reach = {};
2842
3200
  for (const [voice, limbs] of Object.entries(REACH_BY_VOICE)) {
@@ -2859,9 +3217,11 @@ var DRUM_KIT = Object.freeze({
2859
3217
  kind: "percussion",
2860
3218
  name: "drum kit",
2861
3219
  limbs: LIMBS,
2862
- reach: Object.freeze(reachByNote()),
3220
+ reach: deepFreeze(reachByNote()),
3221
+ overdub: deepFreeze(OVERDUB_VOICES.map((voice) => DRUM_NOTES[voice])),
2863
3222
  articulations: KIT_ARTICULATIONS,
2864
- polyphony: 4
3223
+ // One stroke per limb, and one more per voice dubbed over them.
3224
+ polyphony: LIMBS.length + OVERDUB_VOICES.length
2865
3225
  });
2866
3226
 
2867
3227
  // src/generate/drums/vocabulary.ts
@@ -2869,6 +3229,15 @@ function isDrumPattern(material) {
2869
3229
  const candidate = material;
2870
3230
  return typeof candidate === "object" && candidate !== null && typeof candidate.steps === "number" && Array.isArray(candidate.strokes);
2871
3231
  }
3232
+ function assertDrumPattern(material, label) {
3233
+ material.strokes.forEach((stroke, index) => {
3234
+ if (typeof stroke !== "object" || stroke === null || typeof stroke.voice !== "string" || drumNoteOf(stroke.voice) === void 0) {
3235
+ throw new InvalidInputError(
3236
+ `${label} strokes[${index}].voice must name a drum voice; received ${describeRejected(stroke?.voice)}`
3237
+ );
3238
+ }
3239
+ });
3240
+ }
2872
3241
  function hit(voice, step, velocity, extra) {
2873
3242
  return { voice, step, velocity, ...extra };
2874
3243
  }
@@ -2886,7 +3255,7 @@ function bar(strokes) {
2886
3255
  return { steps: BAR_STEPS, strokes };
2887
3256
  }
2888
3257
  var FOUR_FOUR2 = { numerator: 4, denominator: 4 };
2889
- var DRUM_PATTERNS = Object.freeze([
3258
+ var DRUM_PATTERNS = deepFreeze([
2890
3259
  {
2891
3260
  id: "motownBackbeat",
2892
3261
  genre: "motown",
@@ -3056,12 +3425,12 @@ var DEFAULT_VELOCITY = 100;
3056
3425
  var DEFAULT_BPM2 = 120;
3057
3426
  var MAX_STROKES_PER_BAR = 64;
3058
3427
  function placeDrumPattern(opts) {
3428
+ assertRecord(opts, "drum pattern options");
3059
3429
  assertPositiveInt(opts.bars, "drum pattern bars");
3060
3430
  assertGenerationBudget(opts.bars * MAX_STROKES_PER_BAR, "drum pattern hits", opts.budget);
3061
3431
  const genre = assertOneOf(opts.genre, GENRES, "drum pattern genre");
3062
3432
  const section = opts.section === void 0 ? void 0 : assertOneOf(opts.section, PUBLIC_SECTIONS, "drum pattern section");
3063
- const ts = opts.ts ?? FOUR_FOUR2;
3064
- assertTimeSignature(ts, "drum pattern time signature");
3433
+ const ts = meterAt(0, toMeterData(opts.ts ?? FOUR_FOUR2, "ts"));
3065
3434
  if (opts.velocity !== void 0) {
3066
3435
  assertRange(opts.velocity, 1, 127, "drum pattern velocity");
3067
3436
  }
@@ -3073,10 +3442,16 @@ function placeDrumPattern(opts) {
3073
3442
  const barBeats = beatsPerBar(ts);
3074
3443
  const kit = resolved.instrument("drums");
3075
3444
  const swing = effectiveSwing(feel, feelSwingAmount(feel));
3076
- const dictionary = mergeVocabulary(
3077
- DRUM_PATTERNS,
3078
- vocabularyOfKind(resolved.vocabulary, isDrumPattern)
3079
- );
3445
+ const supplied = vocabularyOfKind(resolved.vocabulary, isDrumPattern);
3446
+ supplied.forEach((entry, index) => {
3447
+ assertDrumPattern(entry.material, `drum pattern vocabulary[${index}]`);
3448
+ });
3449
+ const dictionary = mergeVocabulary(DRUM_PATTERNS, supplied);
3450
+ if (selectVocabulary(dictionary, { ts }).length === 0) {
3451
+ throw new InvalidInputError(
3452
+ `drum pattern ts names a meter no figure is written in: ${formatTimeSignature(ts)}. The built-in figures are written in 4/4; supply vocabulary in another meter through the context.`
3453
+ );
3454
+ }
3080
3455
  const track = new HitList();
3081
3456
  for (let bar2 = 0; bar2 < opts.bars; bar2 += 1) {
3082
3457
  const entry = pickVocabulary(
@@ -3106,7 +3481,10 @@ function placeDrumPattern(opts) {
3106
3481
  ...resolved.ornament === void 0 ? {} : { ornament: resolved.ornament },
3107
3482
  isOrnament: (stroke) => stroke.articulation === "ghost",
3108
3483
  rate: opts.rate ?? "straight",
3109
- spanSteps: entry.material.steps
3484
+ spanSteps: entry.material.steps,
3485
+ // The dials rank the figure's positions against the bar it is written
3486
+ // in, which is the bar this call is placing it in.
3487
+ ts
3110
3488
  },
3111
3489
  draw,
3112
3490
  "pattern",
@@ -3117,13 +3495,18 @@ function placeDrumPattern(opts) {
3117
3495
  }
3118
3496
  const barStart = bar2 * barBeats;
3119
3497
  for (const stroke of strokes) {
3120
- const pitch = DRUM_NOTES[stroke.voice];
3498
+ const pitch = drumNoteOf(stroke.voice);
3121
3499
  if (pitch === void 0) {
3122
3500
  continue;
3123
3501
  }
3124
3502
  track.add(
3125
3503
  pitch,
3126
- quantizeSwing(barStart + stroke.step * STEP_BEATS, swing, "sixteenth"),
3504
+ // The bar's own grid is what is swung, and the bar start is added
3505
+ // back after. Swinging the absolute beat takes the offset inside a
3506
+ // quarter note, which is the same thing only while a bar is a whole
3507
+ // number of them: in 7/8 every other bar begins half a beat into one,
3508
+ // so the downbeat was warped and the offbeats came out straight.
3509
+ barStart + quantizeSwing(stroke.step * STEP_BEATS, swing, "sixteenth"),
3127
3510
  stroke.duration ?? STEP_BEATS,
3128
3511
  baseVelocity * stroke.velocity,
3129
3512
  stroke.articulation
@@ -3138,6 +3521,7 @@ function placeDrumPattern(opts) {
3138
3521
  var MAX_HITS_PER_BAR = 128;
3139
3522
  var DEFAULT_BPM3 = 120;
3140
3523
  function generateDrums(opts) {
3524
+ assertRecord(opts, "drum options");
3141
3525
  assertPositiveInt(opts.bars, "drum bars");
3142
3526
  assertGenerationBudget(opts.bars * MAX_HITS_PER_BAR, "drum hits", opts.budget);
3143
3527
  if (opts.euclideanKick) {
@@ -3162,13 +3546,15 @@ function generateDrums(opts) {
3162
3546
  const style = mapping.style;
3163
3547
  const feel = opts.feel === void 0 ? mapping.feel : assertOneOf(opts.feel, DRUM_FEELS, "drum feel");
3164
3548
  const role = opts.role === void 0 ? "full" : assertOneOf(opts.role, DRUM_ROLES, "drum role");
3165
- const ts = opts.ts ?? { numerator: 4, denominator: 4 };
3166
- assertTimeSignature(ts, "drum time signature");
3167
- if (ts.numerator !== 4 || ts.denominator !== 4) {
3168
- throw new InvalidInputError(
3169
- `drum time signature must be 4/4; received ${ts.numerator}/${ts.denominator}`
3170
- );
3549
+ const meters = resolveMeters({ ts: opts.ts }, "ts");
3550
+ for (const change of meters) {
3551
+ if (change.ts.numerator !== 4 || change.ts.denominator !== 4) {
3552
+ throw new InvalidInputError(
3553
+ `drum time signature must be 4/4 throughout; received ${change.ts.numerator}/${change.ts.denominator} at beat ${change.startBeat}`
3554
+ );
3555
+ }
3171
3556
  }
3557
+ const ts = meterAt(0, meters);
3172
3558
  const barBeats = beatsPerBar(ts);
3173
3559
  const playMainVoices = role !== "fxOnly";
3174
3560
  const section = mapSection(publicSection);
@@ -3182,7 +3568,6 @@ function generateDrums(opts) {
3182
3568
  rhythmic,
3183
3569
  ornament: ornamentDial,
3184
3570
  difficulty,
3185
- useGhostNotes: (section === "b" || section === "chorus" || section === "bridge") && style !== "sparse",
3186
3571
  ghostBoost: mapping.ghostBoost,
3187
3572
  useRide: shouldUseRideForSection(section, style),
3188
3573
  useFootHh: shouldUseFootHiHat(section, role),
@@ -3253,7 +3638,14 @@ function generateDrums(opts) {
3253
3638
  if (playMainVoices) {
3254
3639
  const before = track.hits.length;
3255
3640
  if (currentFill) {
3256
- generateFill(track, beatTick, beat, currentFill, fillVelocity);
3641
+ generateFill(
3642
+ track,
3643
+ beatTick,
3644
+ beat,
3645
+ currentFill,
3646
+ fillVelocity,
3647
+ (tick) => swing16(tick, sec, swingAmount)
3648
+ );
3257
3649
  }
3258
3650
  if (track.hits.length > before) {
3259
3651
  continue;
@@ -3297,7 +3689,7 @@ function generateDrums(opts) {
3297
3689
  generateKickForBeat(ctx, sec, kick);
3298
3690
  }
3299
3691
  generateSnareForBeat(ctx, sec, section === "intro" && bar2 === 0);
3300
- if (sec.useGhostNotes && !inLift) {
3692
+ if (!inLift) {
3301
3693
  generateGhostNotesForBeat(ctx, sec);
3302
3694
  }
3303
3695
  }
@@ -3360,7 +3752,7 @@ function playableOn(kit, pitch) {
3360
3752
  }
3361
3753
 
3362
3754
  // src/generate/groove/index.ts
3363
- var DEFAULT_TS4 = { numerator: 4, denominator: 4 };
3755
+ var DEFAULT_TS3 = { numerator: 4, denominator: 4 };
3364
3756
  var DEFAULT_PART = "humanize";
3365
3757
  var DEFAULT_GROOVE_SUBDIVISION = 4;
3366
3758
  var DEFAULT_TIMING = 0.02;
@@ -3371,21 +3763,21 @@ var MAX_METRIC_WEIGHT = 3;
3371
3763
  var MIN_VELOCITY = 1;
3372
3764
  var MAX_VELOCITY = 127;
3373
3765
  function humanize(events, opts = {}) {
3374
- const ts = opts.ts ?? DEFAULT_TS4;
3375
- assertTimeSignature(ts);
3766
+ const asked = assertOptions(opts, "opts");
3767
+ const ts = meterAt(0, toMeterData(asked.ts ?? DEFAULT_TS3, "ts"));
3376
3768
  assertNoteEvents(events, "humanize events", { allowNonPositiveDuration: true });
3377
- const sounding = dropSilentNotes(events);
3378
- const timing = opts.timing ?? DEFAULT_TIMING;
3379
- const velocityJitter = opts.velocity ?? DEFAULT_VELOCITY_JITTER;
3380
- const accent2 = opts.accent ?? DEFAULT_ACCENT;
3381
- const baseVelocity = opts.baseVelocity ?? DEFAULT_BASE_VELOCITY;
3769
+ const sounding2 = dropSilentNotes(events);
3770
+ const timing = asked.timing ?? DEFAULT_TIMING;
3771
+ const velocityJitter = asked.velocity ?? DEFAULT_VELOCITY_JITTER;
3772
+ const accent2 = asked.accent ?? DEFAULT_ACCENT;
3773
+ const baseVelocity = asked.baseVelocity ?? DEFAULT_BASE_VELOCITY;
3382
3774
  assertRange(timing, 0, Number.MAX_SAFE_INTEGER, "humanize timing");
3383
3775
  assertRange(velocityJitter, 0, 127, "humanize velocity jitter");
3384
3776
  assertRange(accent2, 0, 127, "humanize accent");
3385
3777
  assertRange(baseVelocity, 0, 127, "humanize base velocity");
3386
- const ctx = resolveContext(opts.ctx);
3387
- const draw = ctx.part(opts.part ?? DEFAULT_PART);
3388
- return sounding.map((event) => {
3778
+ const ctx = resolveContext(asked.ctx);
3779
+ const draw = ctx.part(asked.part ?? DEFAULT_PART);
3780
+ return sounding2.map((event) => {
3389
3781
  const timingOffset = draw.float(-timing, timing, "timing", event.startBeat, event.pitch);
3390
3782
  const startBeat = event.startBeat + timingOffset;
3391
3783
  const weight = metricWeight(event.startBeat, ts);
@@ -3399,12 +3791,7 @@ function humanize(events, opts = {}) {
3399
3791
  );
3400
3792
  const rawVelocity = (event.velocity ?? baseVelocity) + accentBoost + velocityOffset;
3401
3793
  const velocity = Math.min(MAX_VELOCITY, Math.max(MIN_VELOCITY, Math.round(rawVelocity)));
3402
- return {
3403
- pitch: event.pitch,
3404
- startBeat,
3405
- durationBeat: event.durationBeat,
3406
- velocity
3407
- };
3794
+ return { ...event, startBeat, velocity };
3408
3795
  });
3409
3796
  }
3410
3797
  function quantizeToGrid(beat, barBeats, subdivision, slotsPerBar) {
@@ -3422,10 +3809,10 @@ function quantizeToGrid(beat, barBeats, subdivision, slotsPerBar) {
3422
3809
  };
3423
3810
  }
3424
3811
  function extractGrooveTemplate(events, ts, subdivision = DEFAULT_GROOVE_SUBDIVISION) {
3425
- assertTimeSignature(ts);
3812
+ const meter = meterAt(0, toMeterData(ts, "ts"));
3426
3813
  assertNoteEvents(events, "groove source events", { allowNonPositiveDuration: true });
3427
3814
  assertPositiveInt(subdivision, "groove subdivision");
3428
- const barBeats = beatsPerBar(ts);
3815
+ const barBeats = beatsPerBar(meter);
3429
3816
  const slotsPerBar = Math.ceil(barBeats * subdivision);
3430
3817
  assertPositiveInt(slotsPerBar, "groove template slots");
3431
3818
  assertGenerationBudget(slotsPerBar, "groove template slots");
@@ -3460,10 +3847,10 @@ function extractGrooveTemplate(events, ts, subdivision = DEFAULT_GROOVE_SUBDIVIS
3460
3847
  velocity: velocityCount > 0 ? (velocitySums[i] ?? 0) / velocityCount : null
3461
3848
  });
3462
3849
  }
3463
- return { subdivision, slotsPerBar, slots, ts };
3850
+ return { subdivision, slotsPerBar, slots, ts: meter };
3464
3851
  }
3465
3852
  function applyGrooveTemplate(events, template, ts) {
3466
- assertTimeSignature(ts);
3853
+ const meter = meterAt(0, toMeterData(ts, "ts"));
3467
3854
  assertNoteEvents(events, "groove target events", { allowNonPositiveDuration: true });
3468
3855
  assertPositiveInt(template.subdivision, "template subdivision");
3469
3856
  assertPositiveInt(template.slotsPerBar, "template slotsPerBar");
@@ -3471,12 +3858,28 @@ function applyGrooveTemplate(events, template, ts) {
3471
3858
  if (template.slots.length !== template.slotsPerBar) {
3472
3859
  throw new InvalidInputError("template slots length must equal slotsPerBar");
3473
3860
  }
3474
- if (template.ts && (template.ts.numerator !== ts.numerator || template.ts.denominator !== ts.denominator)) {
3861
+ if (template.ts && (template.ts.numerator !== meter.numerator || template.ts.denominator !== meter.denominator)) {
3475
3862
  throw new InvalidInputError(
3476
- `Groove template meter ${formatTimeSignature(template.ts)} does not match apply-time meter ${formatTimeSignature(ts)}`
3863
+ `Groove template meter ${formatTimeSignature(template.ts)} does not match apply-time meter ${formatTimeSignature(meter)}`
3477
3864
  );
3478
3865
  }
3479
- const barBeats = beatsPerBar(ts);
3866
+ const barBeats = beatsPerBar(meter);
3867
+ const expectedSlots = Math.ceil(barBeats * template.subdivision);
3868
+ if (template.slotsPerBar !== expectedSlots) {
3869
+ throw new InvalidInputError(
3870
+ `Groove template slotsPerBar ${template.slotsPerBar} does not match ${expectedSlots} slots per bar of ${formatTimeSignature(meter)} at subdivision ${template.subdivision}`
3871
+ );
3872
+ }
3873
+ for (let index = 0; index < template.slots.length; index += 1) {
3874
+ const slot = template.slots[index];
3875
+ if (slot === void 0) {
3876
+ continue;
3877
+ }
3878
+ assertFiniteNumber(slot.timingOffset, `template slots[${index}].timingOffset`);
3879
+ if (slot.velocity !== null && slot.velocity !== void 0) {
3880
+ assertRange(slot.velocity, 0, 127, `template slots[${index}].velocity`);
3881
+ }
3882
+ }
3480
3883
  return dropSilentNotes(events).map((event) => {
3481
3884
  const { quantizedBeat, slotIndex } = quantizeToGrid(
3482
3885
  event.startBeat,
@@ -3487,20 +3890,14 @@ function applyGrooveTemplate(events, template, ts) {
3487
3890
  const slot = template.slots[slotIndex];
3488
3891
  const startBeat = quantizedBeat + (slot?.timingOffset ?? 0);
3489
3892
  const velocity = slot && slot.velocity !== null && slot.velocity !== void 0 ? Math.round(slot.velocity) : event.velocity;
3490
- return {
3491
- pitch: event.pitch,
3492
- startBeat,
3493
- durationBeat: event.durationBeat,
3494
- velocity
3495
- };
3893
+ return { ...event, startBeat, velocity };
3496
3894
  });
3497
3895
  }
3498
3896
 
3499
3897
  // src/generate/harmonize/nct.ts
3500
- var ADJACENCY_TOLERANCE = 0.05;
3501
3898
  function snapToPulse(beat, pulse2) {
3502
3899
  const snapped = Math.round(beat / pulse2) * pulse2;
3503
- return Math.abs(snapped - beat) <= ADJACENCY_TOLERANCE ? snapped : beat;
3900
+ return Math.abs(snapped - beat) <= HUMANIZE_ADJACENCY ? snapped : beat;
3504
3901
  }
3505
3902
  var STEP_SEMITONES = 2;
3506
3903
  function isStep(a, b) {
@@ -3514,11 +3911,11 @@ function sameDirection(prev, note2, next) {
3514
3911
  return prev !== note2 && next !== note2 && note2 - prev > 0 === next - note2 > 0;
3515
3912
  }
3516
3913
  function classifyMelodyTones(melody, ts) {
3914
+ const meter = meterAt(0, toMeterData(ts, "ts"));
3517
3915
  assertNoteEvents(melody, "melody notes", { allowNonPositiveDuration: true });
3518
- assertTimeSignature(ts);
3519
- const pulse2 = pulseBeats(ts);
3916
+ const pulse2 = pulseBeats(meter);
3520
3917
  return melody.map((_, index) => {
3521
- const role = roleOfNote(melody, index, ts, pulse2);
3918
+ const role = roleOfNote(melody, index, meter, pulse2);
3522
3919
  return { noteIndex: index, role, ornamental: role !== "structural" };
3523
3920
  });
3524
3921
  }
@@ -3531,7 +3928,7 @@ function roleOfNote(melody, index, ts, pulse2) {
3531
3928
  }
3532
3929
  const intoNote = Math.abs(prev.startBeat + prev.durationBeat - note2.startBeat);
3533
3930
  const outOfNote = Math.abs(note2.startBeat + note2.durationBeat - next.startBeat);
3534
- if (intoNote > ADJACENCY_TOLERANCE || outOfNote > ADJACENCY_TOLERANCE) {
3931
+ if (intoNote > HUMANIZE_ADJACENCY || outOfNote > HUMANIZE_ADJACENCY) {
3535
3932
  return "structural";
3536
3933
  }
3537
3934
  const weight = metricWeight(snapToPulse(note2.startBeat, pulse2), ts);
@@ -3562,7 +3959,7 @@ function roleOfNote(melody, index, ts, pulse2) {
3562
3959
  return "structural";
3563
3960
  }
3564
3961
 
3565
- // src/generate/harmonize/index.ts
3962
+ // src/generate/harmonize/internal.ts
3566
3963
  var FALLBACK = {
3567
3964
  rootPc: 0,
3568
3965
  quality: "maj",
@@ -3575,36 +3972,17 @@ function defaultHarmonicRhythm(ts) {
3575
3972
  const half = bar2 / 2;
3576
3973
  return half % pulseBeats(ts) === 0 ? half : bar2;
3577
3974
  }
3578
- var DEFAULT_PLACEMENT = { transposeSearch: false, octaveSearch: false };
3975
+ var DEFAULT_PLACEMENT = {
3976
+ transposeSearch: false,
3977
+ octaveSearch: false
3978
+ };
3579
3979
  var DEFAULT_METER = { numerator: 4, denominator: 4 };
3580
- var COMFORT_LOW = 55;
3581
- var COMFORT_HIGH = 79;
3582
- var TESSITURA_WEIGHT = 1e-3;
3583
- var NON_CHORD_TONE_STRONG = 4;
3584
- var NON_CHORD_TONE_WEAK = 1;
3585
- var NON_SCALE_TONE = 0.5;
3586
- var DEGREE_BASE = [0, 0.3, 0.45, 0.15, 0.1, 0.3, 0.9];
3587
- var SECONDARY_DOMINANT_BASE = 0.5;
3588
- var BORROWED_BASE = 0.5;
3589
- var CHORD_CHANGE = 0.8;
3590
- var DESCENDING_FIFTH = -0.4;
3591
- var ASCENDING_FIFTH = 0;
3592
- var DOMINANT_TO_TONIC = -0.3;
3593
- var SECONDARY_UNRESOLVED = 1;
3594
- var SECONDARY_AFTER_TARGET = 0.8;
3595
- var ROOT_MOTION_COST = [0, 0.15, 0.15, 0.2, 0.2, 0, 0.4];
3596
- var PHRASE_TONIC = -7;
3597
- var PHRASE_AUTHENTIC = -0.5;
3598
- var OUT_OF_KEY_PER_BEAT = 3;
3599
3980
  var TIE_BREAK_JITTER2 = 1e-6;
3600
- function isMinorKey(key) {
3601
- return (key.modeMask12 >> 3 & 1) === 1 && (key.modeMask12 >> 4 & 1) === 0;
3602
- }
3603
- function leadingTonePc(key) {
3604
- return (pitchClassOf(key.rootPc) + 11) % 12;
3981
+ function cadencesThroughRaisedSeventh(key) {
3982
+ return isMinorKey(key) && scaleSystemOf(key) === "common-practice";
3605
3983
  }
3606
3984
  function isKeyTone(pitch, key) {
3607
- return isScaleTone(pitch, key) || isMinorKey(key) && pitchClassOf(pitch) === leadingTonePc(key);
3985
+ return isScaleTone(pitch, key) || cadencesThroughRaisedSeventh(key) && pitchClassOf(pitch) === leadingTonePcOf(key);
3608
3986
  }
3609
3987
  function inferKey(melody) {
3610
3988
  let best = majorKey(0);
@@ -3633,93 +4011,21 @@ function inferKey(melody) {
3633
4011
  }
3634
4012
  return best;
3635
4013
  }
3636
- var SECONDARY_DOMINANT_TARGETS = [2, 4, 5, 6];
3637
- var SECONDARY_DOMINANT_ENTRY_ORDER = [5, 6, 2, 4];
3638
- var SECONDARY_DOMINANT_BAND = 0.5;
3639
- var REHARMONIZE_DIAL = {
3640
- diatonic: 0,
3641
- secondaryDominant: SECONDARY_DOMINANT_BAND,
3642
- borrowed: 1
3643
- };
3644
- function admittedCount(fraction, size) {
3645
- return Math.max(0, Math.min(size, Math.ceil(fraction * size)));
3646
- }
3647
- function buildCandidates(key, harmonic) {
3648
- const tones = scaleTonesInDegreeOrder(key);
3649
- const candidates = tones.map((rootPc, index) => {
3650
- const degree = index + 1;
3651
- const quality = diatonicTriad(degree, key).quality;
3652
- return {
3653
- rootPc,
3654
- quality,
3655
- degree,
3656
- secondaryDominant: false,
3657
- base: DEGREE_BASE[index] ?? 0.6,
3658
- pcs: chordPitchClasses(makeChord(rootPc, quality))
3659
- };
3660
- });
3661
- if (isMinorKey(key)) {
3662
- const rootPc = (pitchClassOf(key.rootPc) + 7) % 12;
3663
- candidates.push({
3664
- rootPc,
3665
- quality: "maj",
3666
- degree: 5,
3667
- secondaryDominant: false,
3668
- base: DEGREE_BASE[4] ?? 0.6,
3669
- pcs: chordPitchClasses(makeChord(rootPc, "maj"))
3670
- });
3671
- }
3672
- const secondaryOpen = admittedCount(
3673
- harmonic / SECONDARY_DOMINANT_BAND,
3674
- SECONDARY_DOMINANT_TARGETS.length
3675
- );
3676
- for (const target of SECONDARY_DOMINANT_TARGETS) {
3677
- if (SECONDARY_DOMINANT_ENTRY_ORDER.indexOf(target) >= secondaryOpen) {
3678
- continue;
3679
- }
3680
- const targetRoot = tones[target - 1] ?? 0;
3681
- const rootPc = (targetRoot + 7) % 12;
3682
- candidates.push({
3683
- rootPc,
3684
- quality: "dom7",
3685
- secondaryDominant: true,
3686
- targetDegree: target,
3687
- base: SECONDARY_DOMINANT_BASE,
3688
- pcs: chordPitchClasses(makeChord(rootPc, "dom7"))
3689
- });
3690
- }
3691
- const parallel = parallelKey(key);
3692
- const parallelTones = scaleTonesInDegreeOrder(parallel);
3693
- const borrowed = [];
3694
- for (let degree = 1; degree <= parallelTones.length; degree += 1) {
3695
- const chord = diatonicTriad(degree, parallel);
3696
- if (!isDiatonic(chord, key)) {
3697
- borrowed.push(chord);
3698
- }
3699
- }
3700
- const borrowedOpen = admittedCount(
3701
- (harmonic - SECONDARY_DOMINANT_BAND) / (1 - SECONDARY_DOMINANT_BAND),
3702
- borrowed.length
3703
- );
3704
- for (const chord of borrowed.slice(0, borrowedOpen)) {
3705
- candidates.push({
3706
- rootPc: chord.rootPc,
3707
- quality: chord.quality,
3708
- secondaryDominant: false,
3709
- base: BORROWED_BASE,
3710
- pcs: chordPitchClasses(chord)
3711
- });
3712
- }
3713
- const seen = /* @__PURE__ */ new Set();
3714
- return candidates.filter((candidate) => {
3715
- const id = `${candidate.rootPc}:${candidate.quality}`;
3716
- if (seen.has(id)) {
3717
- return false;
3718
- }
3719
- seen.add(id);
3720
- return true;
3721
- });
3722
- }
4014
+
4015
+ // src/generate/harmonize/cost.ts
4016
+ var NON_CHORD_TONE_STRONG = 4;
4017
+ var NON_CHORD_TONE_WEAK = 1;
4018
+ var NON_SCALE_TONE = 0.5;
4019
+ var CHORD_CHANGE = 0.8;
4020
+ var DESCENDING_FIFTH = -0.4;
4021
+ var ASCENDING_FIFTH = 0;
4022
+ var DOMINANT_TO_TONIC = -0.3;
4023
+ var SECONDARY_UNRESOLVED = 1;
4024
+ var SECONDARY_AFTER_TARGET = 0.8;
4025
+ var ROOT_MOTION_COST = [0, 0.15, 0.15, 0.2, 0.2, 0, 0.4];
4026
+ var PHRASE_TONIC = -7;
4027
+ var PHRASE_AUTHENTIC = -0.5;
4028
+ var OUT_OF_KEY_PER_BEAT = 3;
3723
4029
  function emissionCost(seg, cand, melody, key) {
3724
4030
  const pcs = cand.pcs;
3725
4031
  let cost = cand.base * seg.weight;
@@ -3735,40 +4041,9 @@ function emissionCost(seg, cand, melody, key) {
3735
4041
  }
3736
4042
  return cost;
3737
4043
  }
3738
- function tessituraCost(melody) {
3739
- let cost = 0;
3740
- for (const n of melody) {
3741
- if (n.pitch < COMFORT_LOW) {
3742
- cost += (COMFORT_LOW - n.pitch) * TESSITURA_WEIGHT;
3743
- } else if (n.pitch > COMFORT_HIGH) {
3744
- cost += (n.pitch - COMFORT_HIGH) * TESSITURA_WEIGHT;
3745
- }
3746
- }
3747
- return cost;
3748
- }
3749
4044
  function isDominantOf(cand, tonicPc) {
3750
4045
  return cand.rootPc === (tonicPc + 7) % 12 && (cand.quality === "maj" || cand.quality === "dom7");
3751
4046
  }
3752
- function keyFitCost(melody, key) {
3753
- let cost = 0;
3754
- for (const n of melody) {
3755
- if (!isKeyTone(n.pitch, key)) {
3756
- cost += Math.max(0.25, n.durationBeat) * OUT_OF_KEY_PER_BEAT;
3757
- }
3758
- }
3759
- return cost;
3760
- }
3761
- function mergeRepeats(spans) {
3762
- const merged = [];
3763
- for (const span of spans) {
3764
- const prev = merged.at(-1);
3765
- if (prev && prev.rootPc === span.rootPc && prev.quality === span.quality && prev.degree === span.degree && prev.secondaryDominant === span.secondaryDominant) {
3766
- continue;
3767
- }
3768
- merged.push(span);
3769
- }
3770
- return merged;
3771
- }
3772
4047
  function transitionCost(prev, cur, tonicPc) {
3773
4048
  let cost = prev.rootPc === cur.rootPc && prev.quality === cur.quality ? 0 : CHORD_CHANGE;
3774
4049
  const down = (prev.rootPc - cur.rootPc + 12) % 12;
@@ -3801,7 +4076,92 @@ function phraseEndBonus(prev, cur, tonicPc, seg, melody) {
3801
4076
  const weight = Math.max(seg.weight, seg.beats);
3802
4077
  return (PHRASE_TONIC + (prev !== null && isDominantOf(prev, tonicPc) ? PHRASE_AUTHENTIC : 0)) * weight;
3803
4078
  }
3804
- var BEAT_EPS = 1e-9;
4079
+
4080
+ // src/generate/harmonize/key-fit.ts
4081
+ var COMFORT_LOW = 55;
4082
+ var COMFORT_HIGH = 79;
4083
+ var TESSITURA_WEIGHT = 1e-3;
4084
+ function tessituraCost(melody) {
4085
+ let cost = 0;
4086
+ for (const n of melody) {
4087
+ if (n.pitch < COMFORT_LOW) {
4088
+ cost += (COMFORT_LOW - n.pitch) * TESSITURA_WEIGHT;
4089
+ } else if (n.pitch > COMFORT_HIGH) {
4090
+ cost += (n.pitch - COMFORT_HIGH) * TESSITURA_WEIGHT;
4091
+ }
4092
+ }
4093
+ return cost;
4094
+ }
4095
+ function keyFitCost(melody, key) {
4096
+ let cost = 0;
4097
+ for (const n of melody) {
4098
+ if (!isKeyTone(n.pitch, key)) {
4099
+ cost += Math.max(0.25, n.durationBeat) * OUT_OF_KEY_PER_BEAT;
4100
+ }
4101
+ }
4102
+ return cost;
4103
+ }
4104
+
4105
+ // src/generate/harmonize/search.ts
4106
+ function sameHarmony(a, b) {
4107
+ return a.rootPc === b.rootPc && a.quality === b.quality;
4108
+ }
4109
+ function harmonizeOnce(melody, key, candidates, segments, jitter, phraseEnds, articulated) {
4110
+ const tonicPc = pitchClassOf(key.rootPc);
4111
+ const n = candidates.length;
4112
+ const candAt = (i) => candidates[i] ?? FALLBACK;
4113
+ const seg0 = segments[0];
4114
+ let dp = candidates.map(
4115
+ (c, ci) => (seg0 ? emissionCost(seg0, c, melody, key) : 0) + (seg0 && phraseEnds.has(0) ? phraseEndBonus(null, c, tonicPc, seg0, melody) : 0) + (jitter[ci] ?? 0)
4116
+ );
4117
+ const back = [];
4118
+ for (let s = 1; s < segments.length; s += 1) {
4119
+ const seg = segments[s];
4120
+ if (!seg) {
4121
+ continue;
4122
+ }
4123
+ const closes = phraseEnds.has(s);
4124
+ const articulates = articulated.has(s);
4125
+ const next = [];
4126
+ const ptr = [];
4127
+ for (let c = 0; c < n; c += 1) {
4128
+ let best = Number.POSITIVE_INFINITY;
4129
+ let bestPrev = 0;
4130
+ for (let p = 0; p < n; p += 1) {
4131
+ if (articulates && sameHarmony(candAt(p), candAt(c))) {
4132
+ continue;
4133
+ }
4134
+ const cost = (dp[p] ?? Number.POSITIVE_INFINITY) + transitionCost(candAt(p), candAt(c), tonicPc) * seg.beats + (closes ? phraseEndBonus(candAt(p), candAt(c), tonicPc, seg, melody) : 0);
4135
+ if (cost < best) {
4136
+ best = cost;
4137
+ bestPrev = p;
4138
+ }
4139
+ }
4140
+ next[c] = best + emissionCost(seg, candAt(c), melody, key) + (jitter[c] ?? 0);
4141
+ ptr[c] = bestPrev;
4142
+ }
4143
+ dp = next;
4144
+ back.push(ptr);
4145
+ }
4146
+ let bestCost = Number.POSITIVE_INFINITY;
4147
+ let bestEnd = 0;
4148
+ for (let c = 0; c < n; c += 1) {
4149
+ const total = dp[c] ?? Number.POSITIVE_INFINITY;
4150
+ if (total < bestCost) {
4151
+ bestCost = total;
4152
+ bestEnd = c;
4153
+ }
4154
+ }
4155
+ const path = new Array(segments.length).fill(bestEnd);
4156
+ for (let s = segments.length - 2; s >= 0; s -= 1) {
4157
+ const ptr = back[s];
4158
+ const nextIdx = path[s + 1] ?? bestEnd;
4159
+ path[s] = ptr ? ptr[nextIdx] ?? bestEnd : bestEnd;
4160
+ }
4161
+ return { cost: bestCost, path };
4162
+ }
4163
+
4164
+ // src/generate/harmonize/segments.ts
3805
4165
  function gridBounds(segmentStart, hr, gridCount, ends) {
3806
4166
  const bounds = [];
3807
4167
  for (let s = 0; s <= gridCount; s += 1) {
@@ -3850,8 +4210,16 @@ function segmentIndexAt(bounds, beat) {
3850
4210
  }
3851
4211
  return found;
3852
4212
  }
3853
- function sameHarmony(a, b) {
3854
- return a.rootPc === b.rootPc && a.quality === b.quality;
4213
+ function mergeRepeats(spans) {
4214
+ const merged = [];
4215
+ for (const span of spans) {
4216
+ const prev = merged.at(-1);
4217
+ if (prev && prev.rootPc === span.rootPc && prev.quality === span.quality && prev.degree === span.degree && prev.secondaryDominant === span.secondaryDominant) {
4218
+ continue;
4219
+ }
4220
+ merged.push(span);
4221
+ }
4222
+ return merged;
3855
4223
  }
3856
4224
  function phraseEndSegments(segments, ends) {
3857
4225
  const closing = new Set(segments.length > 0 ? [segments.length - 1] : []);
@@ -3870,103 +4238,19 @@ function phraseEndSegments(segments, ends) {
3870
4238
  }
3871
4239
  return { closing, named };
3872
4240
  }
3873
- function harmonizeOnce(melody, key, candidates, segments, jitter, phraseEnds, articulated) {
3874
- const tonicPc = pitchClassOf(key.rootPc);
3875
- const n = candidates.length;
3876
- const candAt = (i) => candidates[i] ?? FALLBACK;
3877
- const seg0 = segments[0];
3878
- let dp = candidates.map(
3879
- (c, ci) => (seg0 ? emissionCost(seg0, c, melody, key) : 0) + (seg0 && phraseEnds.has(0) ? phraseEndBonus(null, c, tonicPc, seg0, melody) : 0) + (jitter[ci] ?? 0)
3880
- );
3881
- const back = [];
3882
- for (let s = 1; s < segments.length; s += 1) {
3883
- const seg = segments[s];
3884
- if (!seg) {
3885
- continue;
3886
- }
3887
- const closes = phraseEnds.has(s);
3888
- const articulates = articulated.has(s);
3889
- const next = [];
3890
- const ptr = [];
3891
- for (let c = 0; c < n; c += 1) {
3892
- let best = Number.POSITIVE_INFINITY;
3893
- let bestPrev = 0;
3894
- for (let p = 0; p < n; p += 1) {
3895
- if (articulates && sameHarmony(candAt(p), candAt(c))) {
3896
- continue;
3897
- }
3898
- const cost = (dp[p] ?? Number.POSITIVE_INFINITY) + transitionCost(candAt(p), candAt(c), tonicPc) * seg.beats + (closes ? phraseEndBonus(candAt(p), candAt(c), tonicPc, seg, melody) : 0);
3899
- if (cost < best) {
3900
- best = cost;
3901
- bestPrev = p;
3902
- }
3903
- }
3904
- next[c] = best + emissionCost(seg, candAt(c), melody, key) + (jitter[c] ?? 0);
3905
- ptr[c] = bestPrev;
3906
- }
3907
- dp = next;
3908
- back.push(ptr);
3909
- }
3910
- let bestCost = Number.POSITIVE_INFINITY;
3911
- let bestEnd = 0;
3912
- for (let c = 0; c < n; c += 1) {
3913
- const total = dp[c] ?? Number.POSITIVE_INFINITY;
3914
- if (total < bestCost) {
3915
- bestCost = total;
3916
- bestEnd = c;
3917
- }
3918
- }
3919
- const path = new Array(segments.length).fill(bestEnd);
3920
- for (let s = segments.length - 2; s >= 0; s -= 1) {
3921
- const ptr = back[s];
3922
- const nextIdx = path[s + 1] ?? bestEnd;
3923
- path[s] = ptr ? ptr[nextIdx] ?? bestEnd : bestEnd;
3924
- }
3925
- return { cost: bestCost, path };
3926
- }
3927
- function harmonizeMelody(opts) {
3928
- assertNoteEvents(opts.melody, "harmonize melody", { allowNonPositiveDuration: true });
3929
- const phraseEnds = opts.phraseEnds ?? [];
3930
- for (const end of phraseEnds) {
3931
- assertFiniteNumber(end, "harmonize phrase end");
3932
- }
3933
- const soundingMelody = soundingNotesOnly(opts.melody);
3934
- const ts = opts.ts ?? DEFAULT_METER;
3935
- assertTimeSignature(ts);
3936
- const requestedKey = opts.key ?? "infer";
3937
- const key = requestedKey === "infer" ? inferKey(soundingMelody) : toKeyScale(requestedKey);
3938
- const placement = opts.placement ?? DEFAULT_PLACEMENT;
3939
- const ctx = resolveContext(opts.ctx);
3940
- const harmonic = ctx.harmonic ?? REHARMONIZE_DIAL[opts.reharmonize ?? "diatonic"];
3941
- if (soundingMelody.length === 0) {
3942
- return { transposeSemitones: 0, key, chords: [], melodyRoles: [] };
3943
- }
3944
- const candidates = buildCandidates(key, harmonic);
3945
- const candAt = (i) => candidates[i] ?? FALLBACK;
3946
- const draw = ctx.part("harmony");
3947
- const jitter = candidates.map((_, index) => draw.at("jitter", index) * TIE_BREAK_JITTER2);
3948
- const pulse2 = pulseBeats(ts);
3949
- const spans = soundingMelody.map((note2) => ({
3950
- startBeat: snapToPulse(note2.startBeat, pulse2),
3951
- endBeat: snapToPulse(note2.startBeat + note2.durationBeat, pulse2)
3952
- }));
3953
- const melodyStart = spans.reduce(
3954
- (start, span) => Math.min(start, span.startBeat),
3955
- Number.POSITIVE_INFINITY
3956
- );
3957
- const melodyEnd = spans.reduce((end, span) => Math.max(end, span.endBeat), 0);
3958
- const hr = assertRange(
3959
- opts.harmonicRhythm ?? defaultHarmonicRhythm(ts),
3960
- Number.MIN_VALUE,
3961
- Number.MAX_SAFE_INTEGER,
3962
- "harmonic rhythm"
3963
- );
4241
+ function buildSegments(spans, melody, ts, grid, phraseEnds, budget) {
4242
+ const melodyStart = grid.start;
4243
+ const melodyEnd = grid.end;
4244
+ const hr = grid.harmonicRhythm;
3964
4245
  const segmentStart = Math.floor(melodyStart / hr) * hr;
3965
4246
  const gridCount = Math.max(1, Math.ceil((melodyEnd - segmentStart) / hr));
3966
- assertGenerationBudget(gridCount, "harmonic segments");
3967
- const bounds = gridBounds(segmentStart, hr, gridCount, phraseEnds);
4247
+ assertGenerationBudget(gridCount, "harmonic segments", budget);
4248
+ const innerEnds = phraseEnds.filter(
4249
+ (end) => end > segmentStart + BEAT_EPS && end < melodyEnd - BEAT_EPS
4250
+ );
4251
+ const bounds = gridBounds(segmentStart, hr, gridCount, innerEnds);
3968
4252
  const segCount = bounds.length - 1;
3969
- assertGenerationBudget(segCount, "harmonic segments");
4253
+ assertGenerationBudget(segCount, "harmonic segments", budget);
3970
4254
  const segments = Array.from({ length: segCount }, (_, s) => {
3971
4255
  const startBeat = bounds[s] ?? segmentStart;
3972
4256
  const endBeat = bounds[s + 1] ?? startBeat + hr;
@@ -3987,13 +4271,13 @@ function harmonizeMelody(opts) {
3987
4271
  lastExclusive += 1;
3988
4272
  }
3989
4273
  memberships += Math.max(0, lastExclusive - first);
3990
- assertGenerationBudget(memberships, "note-to-segment memberships");
4274
+ assertGenerationBudget(memberships, "note-to-segment memberships", budget);
3991
4275
  for (let segment = first; segment < lastExclusive; segment += 1) {
3992
4276
  segments[segment]?.noteIndices.push(index);
3993
4277
  }
3994
4278
  }
3995
- const tones = classifyMelodyTones(soundingMelody, ts);
3996
- const phraseClosingNotes = closingNoteIndices(spans, phraseEnds);
4279
+ const tones = classifyMelodyTones(melody, ts);
4280
+ const phraseClosingNotes = closingNoteIndices(spans, innerEnds);
3997
4281
  for (const segment of segments) {
3998
4282
  const structural = segment.noteIndices.filter(
3999
4283
  (idx) => tones[idx]?.ornamental !== true || phraseClosingNotes.has(idx)
@@ -4019,9 +4303,200 @@ function harmonizeMelody(opts) {
4019
4303
  }
4020
4304
  }
4021
4305
  }
4306
+ return { segments, innerEnds, bounds, segmentStart };
4307
+ }
4308
+
4309
+ // src/generate/harmonize/candidates.ts
4310
+ var ROOT_WEIGHT_BY_SEMITONE = [0, 0.9, 0.3, 0.45, 0.45, 0.15, 0.9, 0.1, 0.3, 0.3, 0.3, 0.9];
4311
+ var UNSTABLE_TRIAD = 0.9;
4312
+ var UNSTABLE_QUALITIES = /* @__PURE__ */ new Set([
4313
+ "dim",
4314
+ "aug",
4315
+ "dim7",
4316
+ "m7b5",
4317
+ "aug7"
4318
+ ]);
4319
+ function vocabularyBase(rootPc, quality, tonicPc) {
4320
+ const above = (pitchClassOf(rootPc) - tonicPc + 12) % 12;
4321
+ const root = ROOT_WEIGHT_BY_SEMITONE[above] ?? UNSTABLE_TRIAD;
4322
+ return UNSTABLE_QUALITIES.has(quality) ? Math.max(root, UNSTABLE_TRIAD) : root;
4323
+ }
4324
+ var SECONDARY_DOMINANT_BASE = 0.5;
4325
+ var BORROWED_BASE = 0.5;
4326
+ var SECONDARY_DOMINANT_TARGETS = [2, 4, 5, 6];
4327
+ function secondaryDominantTargets(key) {
4328
+ const tonicizable = new Map(
4329
+ tonicizableDegrees(key).map((degree) => [degree.degreeNumber, degree])
4330
+ );
4331
+ return SECONDARY_DOMINANT_TARGETS.map((degree) => tonicizable.get(degree)).filter(
4332
+ (degree) => degree !== void 0
4333
+ );
4334
+ }
4335
+ var SECONDARY_DOMINANT_ENTRY_ORDER = [5, 6, 2, 4];
4336
+ var BORROWED_ENTRY_ORDER = [5, 10, 8, 3, 7, 2, 0];
4337
+ var SECONDARY_DOMINANT_BAND = 0.5;
4338
+ var REHARMONIZE_STRENGTHS = ["diatonic", "secondaryDominant", "borrowed"];
4339
+ var REHARMONIZE_DIAL = {
4340
+ diatonic: 0,
4341
+ secondaryDominant: SECONDARY_DOMINANT_BAND,
4342
+ borrowed: 1
4343
+ };
4344
+ function admittedCount(fraction, size) {
4345
+ return Math.max(0, Math.min(size, Math.ceil(fraction * size)));
4346
+ }
4347
+ function buildCandidates(key, harmonic) {
4348
+ const tonicPc = pitchClassOf(key.rootPc);
4349
+ const frame = heptatonicFrameOf(key);
4350
+ const tones = scaleTonesInDegreeOrder(frame);
4351
+ const candidates = tones.map((rootPc, index) => {
4352
+ const degree = index + 1;
4353
+ const quality = diatonicTriad(degree, frame).quality;
4354
+ return {
4355
+ rootPc,
4356
+ quality,
4357
+ degree,
4358
+ secondaryDominant: false,
4359
+ base: vocabularyBase(rootPc, quality, tonicPc),
4360
+ pcs: chordPitchClasses(makeChord(rootPc, quality))
4361
+ };
4362
+ });
4363
+ if (cadencesThroughRaisedSeventh(key)) {
4364
+ const rootPc = (tonicPc + 7) % 12;
4365
+ candidates.push({
4366
+ rootPc,
4367
+ quality: "maj",
4368
+ degree: 5,
4369
+ secondaryDominant: false,
4370
+ base: vocabularyBase(rootPc, "maj", tonicPc),
4371
+ pcs: chordPitchClasses(makeChord(rootPc, "maj"))
4372
+ });
4373
+ }
4374
+ const targets = secondaryDominantTargets(key);
4375
+ const entryOrder = SECONDARY_DOMINANT_ENTRY_ORDER.filter(
4376
+ (degree) => targets.some((target) => target.degreeNumber === degree)
4377
+ );
4378
+ const secondaryOpen = admittedCount(harmonic / SECONDARY_DOMINANT_BAND, targets.length);
4379
+ for (const target of targets) {
4380
+ if (entryOrder.indexOf(target.degreeNumber) >= secondaryOpen) {
4381
+ continue;
4382
+ }
4383
+ const rootPc = (target.rootPc + 7) % 12;
4384
+ candidates.push({
4385
+ rootPc,
4386
+ quality: "dom7",
4387
+ secondaryDominant: true,
4388
+ targetDegree: target.degreeNumber,
4389
+ base: SECONDARY_DOMINANT_BASE,
4390
+ pcs: chordPitchClasses(makeChord(rootPc, "dom7"))
4391
+ });
4392
+ }
4393
+ const parallel = parallelKey(key);
4394
+ const parallelTones = scaleTonesInDegreeOrder(parallel);
4395
+ const borrowed = [];
4396
+ for (let degree = 1; degree <= parallelTones.length; degree += 1) {
4397
+ const chord = diatonicTriad(degree, parallel);
4398
+ if (!isDiatonic(chord, key)) {
4399
+ borrowed.push(chord);
4400
+ }
4401
+ }
4402
+ const borrowedRank = (chord) => {
4403
+ const offset = BORROWED_ENTRY_ORDER.indexOf(pitchClassOf(chord.rootPc - tonicPc));
4404
+ return offset < 0 ? BORROWED_ENTRY_ORDER.length : offset;
4405
+ };
4406
+ borrowed.sort((a, b) => borrowedRank(a) - borrowedRank(b));
4407
+ const borrowedOpen = admittedCount(
4408
+ (harmonic - SECONDARY_DOMINANT_BAND) / (1 - SECONDARY_DOMINANT_BAND),
4409
+ borrowed.length
4410
+ );
4411
+ for (const chord of borrowed.slice(0, borrowedOpen)) {
4412
+ candidates.push({
4413
+ rootPc: chord.rootPc,
4414
+ quality: chord.quality,
4415
+ secondaryDominant: false,
4416
+ // A diminished or augmented triad is charged for what it is wherever it
4417
+ // comes from: priced at the flat borrowing rate, the ii dim borrowed from
4418
+ // the parallel minor came out cheaper than the key's own vii dim, and
4419
+ // which of the two a phrase took was decided by the tie-break jitter
4420
+ // rather than by the melody.
4421
+ base: UNSTABLE_QUALITIES.has(chord.quality) ? Math.max(BORROWED_BASE, UNSTABLE_TRIAD) : BORROWED_BASE,
4422
+ pcs: chordPitchClasses(chord)
4423
+ });
4424
+ }
4425
+ const seen = /* @__PURE__ */ new Set();
4426
+ return candidates.filter((candidate) => {
4427
+ const id = `${candidate.rootPc}:${candidate.quality}`;
4428
+ if (seen.has(id)) {
4429
+ return false;
4430
+ }
4431
+ seen.add(id);
4432
+ return true;
4433
+ });
4434
+ }
4435
+
4436
+ // src/generate/harmonize/index.ts
4437
+ function harmonizeMelody(opts) {
4438
+ assertRecord(opts, "harmonize options");
4439
+ assertNoteEvents(opts.melody, "harmonize melody", {
4440
+ allowNonPositiveDuration: true,
4441
+ budget: opts.budget
4442
+ });
4443
+ const phraseEnds = opts.phraseEnds ?? [];
4444
+ for (const end of phraseEnds) {
4445
+ assertFiniteNumber(end, "harmonize phrase end");
4446
+ }
4447
+ const soundingMelody = soundingNotesOnly(opts.melody);
4448
+ const ts = meterAt(0, toMeterData(opts.ts ?? DEFAULT_METER, "ts"));
4449
+ assertTimeSignature(ts);
4450
+ const requestedKey = opts.key ?? "infer";
4451
+ const resolvedKey = resolveKey(
4452
+ requestedKey === "infer" ? inferKey(soundingMelody) : requestedKey
4453
+ );
4454
+ const key = resolvedKey.scale;
4455
+ const placement = opts.placement ?? DEFAULT_PLACEMENT;
4456
+ const ctx = resolveContext(opts.ctx);
4457
+ const reharmonize = assertOneOf(
4458
+ opts.reharmonize ?? "diatonic",
4459
+ REHARMONIZE_STRENGTHS,
4460
+ "reharmonize"
4461
+ );
4462
+ const harmonic = ctx.harmonic ?? REHARMONIZE_DIAL[reharmonize];
4463
+ if (soundingMelody.length === 0) {
4464
+ return { transposeSemitones: 0, key: resolvedKey, chords: [], melodyRoles: [] };
4465
+ }
4466
+ const candidates = buildCandidates(key, harmonic);
4467
+ const candAt = (i) => candidates[i] ?? FALLBACK;
4468
+ const draw = ctx.part("harmony");
4469
+ const jitter = candidates.map((_, index) => draw.at("jitter", index) * TIE_BREAK_JITTER2);
4470
+ const pulse2 = pulseBeats(ts);
4471
+ const spans = soundingMelody.map((note2) => ({
4472
+ startBeat: snapToPulse(note2.startBeat, pulse2),
4473
+ endBeat: snapToPulse(note2.startBeat + note2.durationBeat, pulse2)
4474
+ }));
4475
+ const melodyStart = spans.reduce(
4476
+ (start, span) => Math.min(start, span.startBeat),
4477
+ Number.POSITIVE_INFINITY
4478
+ );
4479
+ const melodyEnd = spans.reduce(
4480
+ (end, span) => Math.max(end, span.endBeat),
4481
+ Number.NEGATIVE_INFINITY
4482
+ );
4483
+ const hr = assertRange(
4484
+ opts.harmonicRhythm ?? defaultHarmonicRhythm(ts),
4485
+ Number.MIN_VALUE,
4486
+ Number.MAX_SAFE_INTEGER,
4487
+ "harmonic rhythm"
4488
+ );
4489
+ const { segments, innerEnds, bounds, segmentStart } = buildSegments(
4490
+ spans,
4491
+ soundingMelody,
4492
+ ts,
4493
+ { start: melodyStart, end: melodyEnd, harmonicRhythm: hr },
4494
+ phraseEnds,
4495
+ opts.budget
4496
+ );
4022
4497
  const { closing: closingSegments, named: namedEndSegments } = phraseEndSegments(
4023
4498
  segments,
4024
- phraseEnds
4499
+ innerEnds
4025
4500
  );
4026
4501
  const articulatedSegments = new Set(candidates.map((c) => `${c.rootPc}:${c.quality}`)).size > 1 ? namedEndSegments : /* @__PURE__ */ new Set();
4027
4502
  const semitoneShifts = [0];
@@ -4035,8 +4510,9 @@ function harmonizeMelody(opts) {
4035
4510
  const octaveShifts = placement.octaveSearch ? [0, -12, 12] : [0];
4036
4511
  const transposes = semitoneShifts.flatMap((s) => octaveShifts.map((o) => s + o));
4037
4512
  assertGenerationBudget(
4038
- transposes.length * segCount * candidates.length * candidates.length,
4039
- "harmonization placement search"
4513
+ transposes.length * segments.length * candidates.length * candidates.length,
4514
+ "harmonization placement search",
4515
+ opts.budget
4040
4516
  );
4041
4517
  let bestCost = Number.POSITIVE_INFINITY;
4042
4518
  let bestTs = 0;
@@ -4077,21 +4553,29 @@ function harmonizeMelody(opts) {
4077
4553
  })
4078
4554
  );
4079
4555
  const melodyRoles = opts.melody.map((note2, noteIndex) => {
4080
- const segIdx = segmentIndexAt(bounds, note2.startBeat);
4556
+ const segIdx = segmentIndexAt(bounds, snapToPulse(note2.startBeat, pulse2));
4081
4557
  const cand = candAt(bestPath[segIdx] ?? 0);
4082
4558
  const chord = makeChord(cand.rootPc, cand.quality);
4083
4559
  return { noteIndex, role: roleOf(note2.pitch + bestTs, chord).role };
4084
4560
  });
4085
- return { transposeSemitones: bestTs, key, chords, melodyRoles };
4561
+ return { transposeSemitones: bestTs, key: resolvedKey, chords, melodyRoles };
4086
4562
  }
4087
4563
 
4088
4564
  // src/generate/motif/index.ts
4565
+ var MOTIF_TRANSFORMS = Object.freeze([
4566
+ "transposeDiatonic",
4567
+ "transposeChromatic",
4568
+ "invert",
4569
+ "retrograde",
4570
+ "augment",
4571
+ "diminish",
4572
+ "sequence"
4573
+ ]);
4089
4574
  var MIN_TILE_SPAN = 1 / 256;
4090
- var DEFAULT_TS5 = { numerator: 4, denominator: 4 };
4091
- var EPS4 = 1e-9;
4575
+ var DEFAULT_TS4 = { numerator: 4, denominator: 4 };
4092
4576
  function isDownbeat(startBeat, barBeats) {
4093
4577
  const bar2 = startBeat / barBeats;
4094
- return Math.abs(bar2 - Math.round(bar2)) < EPS4;
4578
+ return Math.abs(bar2 - Math.round(bar2)) < BEAT_EPS;
4095
4579
  }
4096
4580
  function nearestChordTone(pitch, chord) {
4097
4581
  const pcs = chordPitchClasses(chord);
@@ -4146,6 +4630,18 @@ function cellSpan(cell) {
4146
4630
  function clone(cell) {
4147
4631
  return { notes: cell.notes.map((n) => ({ ...n })) };
4148
4632
  }
4633
+ function inTimeOrder(cell) {
4634
+ return { notes: [...cell.notes].sort((a, b) => a.startBeat - b.startBeat) };
4635
+ }
4636
+ function pivotPitch(notes) {
4637
+ let pivot;
4638
+ for (const note2 of notes) {
4639
+ if (pivot === void 0 || note2.startBeat < pivot.startBeat) {
4640
+ pivot = note2;
4641
+ }
4642
+ }
4643
+ return pivot?.pitch ?? 0;
4644
+ }
4149
4645
  function contourOffsets(contour, count) {
4150
4646
  const offsets = [];
4151
4647
  for (let i = 0; i < count; i += 1) {
@@ -4167,9 +4663,10 @@ function contourOffsets(contour, count) {
4167
4663
  return offsets;
4168
4664
  }
4169
4665
  function generateMotif(opts) {
4666
+ assertRecord(opts, "motif options");
4170
4667
  const contour = opts.contour ?? "arch";
4171
4668
  const bars = assertPositiveInt(opts.bars, "motif bars");
4172
- const barBeats = beatsPerBar(opts.ts ?? DEFAULT_TS5);
4669
+ const barBeats = beatsPerBar(meterAt(0, toMeterData(opts.ts ?? DEFAULT_TS4, "ts")));
4173
4670
  const totalBeats = bars * barBeats;
4174
4671
  const noteCount = Math.max(3, bars * 2);
4175
4672
  assertGenerationBudget(noteCount, "motif notes");
@@ -4201,13 +4698,18 @@ function generateMotif(opts) {
4201
4698
  return { notes };
4202
4699
  }
4203
4700
  function motifToNoteEvents(cell) {
4204
- return cell.notes.map((note2) => ({ ...note2 }));
4701
+ return assertCellNotes(cell, "motif cell notes").map((note2) => ({ ...note2 }));
4702
+ }
4703
+ function assertCellNotes(cell, name) {
4704
+ const read = assertRecord(cell, "motif cell");
4705
+ assertNoteEvents(read.notes, name);
4706
+ return read.notes;
4205
4707
  }
4206
4708
  function transformMotif(cell, t, amount, key) {
4207
4709
  return transformCell(cell, t, amount, key === void 0 ? void 0 : toKeyScale(key));
4208
4710
  }
4209
4711
  function transformCell(cell, t, amount, key) {
4210
- return assertInRange(transformUnchecked(cell, t, amount, key));
4712
+ return assertInRange(inTimeOrder(transformUnchecked(cell, t, amount, key)));
4211
4713
  }
4212
4714
  function assertInRange(cell) {
4213
4715
  for (const note2 of cell.notes) {
@@ -4220,7 +4722,8 @@ function assertInRange(cell) {
4220
4722
  return cell;
4221
4723
  }
4222
4724
  function transformUnchecked(cell, t, amount, key) {
4223
- assertNoteEvents(cell.notes, "motif notes");
4725
+ const notes = assertCellNotes(cell, "motif notes");
4726
+ assertOneOf(t, MOTIF_TRANSFORMS, "motif transform");
4224
4727
  if (amount !== void 0) {
4225
4728
  assertFiniteNumber(amount, "motif transform amount");
4226
4729
  }
@@ -4233,7 +4736,6 @@ function transformUnchecked(cell, t, amount, key) {
4233
4736
  if ((t === "augment" || t === "diminish") && amount !== void 0 && amount <= 0) {
4234
4737
  throw new InvalidInputError("time transform amount must be positive");
4235
4738
  }
4236
- const notes = cell.notes;
4237
4739
  switch (t) {
4238
4740
  case "transposeChromatic": {
4239
4741
  const semis = amount ?? 0;
@@ -4255,7 +4757,7 @@ function transformUnchecked(cell, t, amount, key) {
4255
4757
  };
4256
4758
  }
4257
4759
  case "invert": {
4258
- const pivot = notes[0]?.pitch ?? 0;
4760
+ const pivot = pivotPitch(notes);
4259
4761
  return { notes: notes.map((n) => ({ ...n, pitch: 2 * pivot - n.pitch })) };
4260
4762
  }
4261
4763
  case "retrograde": {
@@ -4295,26 +4797,28 @@ function scaleTime(cell, factor) {
4295
4797
  }))
4296
4798
  };
4297
4799
  }
4298
- function developMotif(cell, timeline, key, bars, ts = DEFAULT_TS5) {
4800
+ function developMotif(cell, timeline, key, bars, ts) {
4801
+ const meter = meterAt(0, toMeterData(ts, "ts"));
4802
+ assertChordTimeline(timeline);
4299
4803
  assertPositiveInt(bars, "development bars");
4300
- assertNoteEvents(cell.notes, "motif notes");
4804
+ const cellNotes = assertCellNotes(cell, "motif notes");
4301
4805
  const scale = toKeyScale(key);
4302
4806
  const span = cellSpan(cell);
4303
- const barBeats = beatsPerBar(ts);
4807
+ const barBeats = beatsPerBar(meter);
4304
4808
  const totalBeats = bars * barBeats;
4305
- const origin = cellOrigin(cell.notes);
4809
+ const origin = cellOrigin(cellNotes);
4306
4810
  const out = [];
4307
4811
  if (span <= 0) {
4308
- return clone(cell);
4812
+ return inTimeOrder(cell);
4309
4813
  }
4310
4814
  const tileSpan = Math.max(span, MIN_TILE_SPAN);
4311
4815
  const tileCount = Math.ceil(totalBeats / tileSpan);
4312
- assertGenerationBudget(tileCount * cell.notes.length, "developed motif notes");
4816
+ assertGenerationBudget(tileCount * cellNotes.length, "developed motif notes");
4313
4817
  for (let k = 0; k < tileCount; k += 1) {
4314
4818
  const offset = k * tileSpan;
4315
4819
  const taken = /* @__PURE__ */ new Map();
4316
4820
  let previous;
4317
- for (const n of cell.notes) {
4821
+ for (const n of cellNotes) {
4318
4822
  const startBeat = offset + (n.startBeat - origin);
4319
4823
  if (startBeat >= totalBeats) {
4320
4824
  continue;
@@ -4330,12 +4834,12 @@ function developMotif(cell, timeline, key, bars, ts = DEFAULT_TS5) {
4330
4834
  out.push({ pitch, startBeat, durationBeat: n.durationBeat });
4331
4835
  }
4332
4836
  }
4333
- return { notes: out };
4837
+ return inTimeOrder({ notes: out });
4334
4838
  }
4335
4839
 
4336
4840
  // src/generate/ornament/index.ts
4337
4841
  var ORNAMENT_STYLES = Object.freeze(["ghost", "flam", "drag", "slide", "accent"]);
4338
- var DEFAULT_TS6 = { numerator: 4, denominator: 4 };
4842
+ var DEFAULT_TS5 = { numerator: 4, denominator: 4 };
4339
4843
  var DEFAULT_STYLE2 = "ghost";
4340
4844
  var DEFAULT_AMOUNT = 0.5;
4341
4845
  var GHOST_VELOCITY_SCALE = 0.45;
@@ -4343,7 +4847,7 @@ var ACCENT_VELOCITY_BOOST = 12;
4343
4847
  var DEFAULT_VELOCITY2 = 80;
4344
4848
  var MIN_VELOCITY2 = 1;
4345
4849
  var MAX_VELOCITY2 = 127;
4346
- var SLIDE_MIN_SEMITONES = 2;
4850
+ var SLIDE_MIN_SEMITONES = 1;
4347
4851
  function eligible(style, note2, previous, next, ts) {
4348
4852
  const strong = isStrongBeat(note2.startBeat, ts);
4349
4853
  switch (style) {
@@ -4378,28 +4882,23 @@ function clampVelocity(velocity) {
4378
4882
  return Math.min(MAX_VELOCITY2, Math.max(MIN_VELOCITY2, Math.round(velocity)));
4379
4883
  }
4380
4884
  function ornament(notes, opts = {}) {
4885
+ const asked = assertOptions(opts, "opts");
4381
4886
  assertNoteEvents(notes, "ornament notes", { allowNonPositiveDuration: true });
4382
- const style = assertOneOf(opts.style ?? DEFAULT_STYLE2, ORNAMENT_STYLES, "ornament style");
4383
- const ts = opts.ts ?? DEFAULT_TS6;
4887
+ const style = assertOneOf(asked.style ?? DEFAULT_STYLE2, ORNAMENT_STYLES, "ornament style");
4888
+ const ts = meterAt(0, toMeterData(asked.ts ?? DEFAULT_TS5, "ts"));
4384
4889
  assertTimeSignature(ts);
4385
- if (opts.amount !== void 0) {
4386
- assertRange(opts.amount, 0, 1, "ornament amount");
4890
+ if (asked.amount !== void 0) {
4891
+ assertRange(asked.amount, 0, 1, "ornament amount");
4387
4892
  }
4388
- const ctx = resolveContextWith(opts.ctx, { ornament: opts.amount });
4893
+ const ctx = resolveContextWith(asked.ctx, { ornament: asked.amount });
4389
4894
  const amount = ctx.ornament ?? DEFAULT_AMOUNT;
4390
4895
  const draw = ctx.part("ornament");
4391
- const sounding = dropSilentNotes(notes);
4392
- const ordered = [...sounding].sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);
4393
- const previousOf = /* @__PURE__ */ new Map();
4394
- for (let index = 0; index < ordered.length; index += 1) {
4395
- const note2 = ordered[index];
4396
- if (note2) {
4397
- previousOf.set(note2, index === 0 ? void 0 : ordered[index - 1]);
4398
- }
4399
- }
4896
+ const sounding2 = dropSilentNotes(notes);
4897
+ const ordered = [...sounding2].sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);
4898
+ const previousOf = previousOnsets(ordered);
4400
4899
  const nextOf = nextOnsets(ordered);
4401
4900
  const gaps = strokeGaps(ordered);
4402
- return sounding.map((note2) => {
4901
+ return sounding2.map((note2) => {
4403
4902
  const decorated = { ...note2 };
4404
4903
  if (note2.articulation !== void 0 || !eligible(style, note2, previousOf.get(note2), nextOf.get(note2), ts)) {
4405
4904
  return decorated;
@@ -4418,6 +4917,19 @@ function ornament(notes, opts = {}) {
4418
4917
  return decorated;
4419
4918
  });
4420
4919
  }
4920
+ function previousOnsets(ordered) {
4921
+ const previousOf = /* @__PURE__ */ new Map();
4922
+ let earlier;
4923
+ for (const note2 of ordered) {
4924
+ if (earlier !== void 0 && earlier.startBeat === note2.startBeat) {
4925
+ previousOf.set(note2, previousOf.get(earlier));
4926
+ } else {
4927
+ previousOf.set(note2, earlier);
4928
+ }
4929
+ earlier = note2;
4930
+ }
4931
+ return previousOf;
4932
+ }
4421
4933
  function nextOnsets(ordered) {
4422
4934
  const nextOf = /* @__PURE__ */ new Map();
4423
4935
  let later;
@@ -4450,6 +4962,8 @@ function strokeGaps(ordered) {
4450
4962
  }
4451
4963
 
4452
4964
  // src/generate/progression/index.ts
4965
+ var DEFAULT_TS6 = { numerator: 4, denominator: 4 };
4966
+ var PROG_STYLES = Object.freeze(["minimal", "dance", "idol", "rock"]);
4453
4967
  var BORROWED_DEGREES = Object.freeze({
4454
4968
  /** Flat submediant: bVI. */
4455
4969
  bVI: 8,
@@ -4633,23 +5147,23 @@ function progressions() {
4633
5147
  return PRESETS.map((p) => ({ ...p, degrees: [...p.degrees], styles: [...p.styles] }));
4634
5148
  }
4635
5149
  function progressionsByStyle(style) {
4636
- return progressions().filter((p) => p.styles.includes(style));
5150
+ const named = assertOneOf(style, PROG_STYLES, "style");
5151
+ return progressions().filter((p) => p.styles.includes(named));
4637
5152
  }
4638
5153
  function degreeToRootPc(degree, key) {
4639
5154
  if (degree >= 1 && degree <= 7) {
4640
- const tones = scaleTonesInDegreeOrder(key);
4641
- return tones.length > 0 ? tones[(degree - 1) % tones.length] ?? key.rootPc % 12 : key.rootPc % 12;
5155
+ const tones = scaleTonesInDegreeOrder(heptatonicFrameOf(key));
5156
+ return tones[degree - 1] ?? key.rootPc % 12;
4642
5157
  }
4643
5158
  const offset = BORROWED_OFFSET[degree] ?? 0;
4644
5159
  return ((key.rootPc % 12 + offset) % 12 + 12) % 12;
4645
5160
  }
4646
5161
  function autoQuality(degree, key, harmonicDominant) {
4647
5162
  if (degree >= 1 && degree <= 7) {
4648
- const isMinor = (key.modeMask12 >> 3 & 1) === 1 && (key.modeMask12 >> 4 & 1) === 0;
4649
- if (harmonicDominant && isMinor && degree === 5) {
5163
+ if (harmonicDominant && isMinorKey(key) && degree === 5) {
4650
5164
  return "maj";
4651
5165
  }
4652
- return diatonicTriad(degree, key).quality;
5166
+ return diatonicTriad(degree, heptatonicFrameOf(key)).quality;
4653
5167
  }
4654
5168
  if (degree === 14) {
4655
5169
  return "dim";
@@ -4660,46 +5174,44 @@ function autoQuality(degree, key, harmonicDominant) {
4660
5174
  return "maj";
4661
5175
  }
4662
5176
  function resolveCycle(degrees, key, ext, harmonicDominant) {
4663
- const steps = [];
4664
- for (const degree of degrees) {
5177
+ const steps = degrees.map((degree) => {
4665
5178
  const step = {
4666
- source: degree,
4667
5179
  rootPc: degreeToRootPc(degree, key),
4668
5180
  quality: ext !== void 0 && ext !== "auto" ? ext : autoQuality(degree, key, harmonicDominant)
4669
5181
  };
4670
5182
  if (degree >= 1 && degree <= 7) {
4671
5183
  step.degree = degree;
4672
5184
  }
4673
- const previous = steps[steps.length - 1];
4674
- if (previous !== void 0 && previous.source !== step.source && sameStep(previous, step)) {
4675
- continue;
4676
- }
4677
- steps.push(step);
4678
- }
4679
- const first = steps[0];
4680
- const last = steps[steps.length - 1];
4681
- if (steps.length > 1 && first !== void 0 && last !== void 0 && first.source !== last.source && sameStep(first, last)) {
4682
- steps.pop();
4683
- }
4684
- return steps.length > 0 ? steps : [{ source: 0, degree: 0, rootPc: degreeToRootPc(0, key), quality: "maj" }];
5185
+ return step;
5186
+ });
5187
+ return steps.length > 0 ? steps : [{ rootPc: degreeToRootPc(1, key), quality: "maj" }];
4685
5188
  }
4686
- function sameStep(a, b) {
4687
- return a.rootPc === b.rootPc && a.quality === b.quality;
5189
+ function pickProgressionPreset(style, ctx = 0) {
5190
+ return presetFor(style, resolveContext(ctx));
4688
5191
  }
4689
- function pickProgressionPreset(style, seed = 0) {
5192
+ function presetFor(style, ctx) {
4690
5193
  const pool = PRESETS.filter((preset) => preset.styles.includes(style));
4691
5194
  if (pool.length === 0) {
4692
5195
  throw new InvalidInputError(`Unknown progression style: ${style}`);
4693
5196
  }
4694
- const index = resolveContext(seed).part("progression").range(0, pool.length - 1, "preset");
5197
+ const index = ctx.part("progression").range(0, pool.length - 1, "preset");
4695
5198
  return pool[index] ?? PRESETS[0];
4696
5199
  }
5200
+ function tonicizableRootPcs(key) {
5201
+ return new Set(tonicizableDegrees(key).map((degree) => degree.rootPc));
5202
+ }
5203
+ function canBeTonicized(span, tonicizable) {
5204
+ if (!tonicizable.has(span.rootPc)) {
5205
+ return false;
5206
+ }
5207
+ return fifthPcOf(makeChord(span.rootPc, span.quality)) === (span.rootPc + 7) % 12;
5208
+ }
4697
5209
  function generateProgression(opts) {
5210
+ assertRecord(opts, "progression options");
4698
5211
  assertPositiveInt(opts.bars, "progression bars");
4699
5212
  assertGenerationBudget(opts.bars, "progression chords");
4700
5213
  const key = toKeyScale(opts.key);
4701
5214
  const ctx = resolveContext(opts.ctx);
4702
- const seed = ctx.seed;
4703
5215
  let preset;
4704
5216
  if (opts.preset !== void 0) {
4705
5217
  if (opts.preset.degrees.length === 0) {
@@ -4728,7 +5240,7 @@ function generateProgression(opts) {
4728
5240
  throw new InvalidInputError(`Unknown progression preset: ${opts.presetId}`);
4729
5241
  }
4730
5242
  }
4731
- preset ??= pickProgressionPreset(opts.style, seed);
5243
+ preset ??= presetFor(opts.style, ctx);
4732
5244
  const ext = opts.ext === void 0 ? "auto" : assertOneOf(opts.ext, ["auto", ...chordQualities()], "progression extension");
4733
5245
  const cycle = resolveCycle(
4734
5246
  preset?.degrees ?? [0],
@@ -4736,13 +5248,14 @@ function generateProgression(opts) {
4736
5248
  ext,
4737
5249
  preset?.functional === "cadenceStrong"
4738
5250
  );
5251
+ const barBeats = beatsPerBar(opts.ts ?? DEFAULT_TS6);
4739
5252
  const chords = [];
4740
5253
  for (let bar2 = 0; bar2 < opts.bars; bar2 += 1) {
4741
5254
  const step = cycle[bar2 % cycle.length];
4742
5255
  const chord = {
4743
5256
  rootPc: step?.rootPc ?? 0,
4744
5257
  quality: step?.quality ?? "maj",
4745
- startBeat: bar2 * 4
5258
+ startBeat: bar2 * barBeats
4746
5259
  };
4747
5260
  if (step?.degree !== void 0) {
4748
5261
  chord.degree = step.degree;
@@ -4752,6 +5265,7 @@ function generateProgression(opts) {
4752
5265
  const harmonic = ctx.harmonic ?? DEFAULT_HARMONIC;
4753
5266
  if (harmonic > 0) {
4754
5267
  const tonicPc = (key.rootPc % 12 + 12) % 12;
5268
+ const tonicizable = tonicizableRootPcs(key);
4755
5269
  const draw = ctx.part("progression");
4756
5270
  let tonicStatements = chords.filter((chord) => chord.rootPc === tonicPc).length;
4757
5271
  for (let i = 0; i < chords.length - 1; i += 1) {
@@ -4765,7 +5279,8 @@ function generateProgression(opts) {
4765
5279
  const isLastTonicStatement = cur.rootPc === tonicPc && tonicStatements <= 1;
4766
5280
  const alreadySecondary = cur.rootPc === domRoot && cur.quality === "dom7";
4767
5281
  const isResolutionTarget = chords[i - 1]?.secondaryDominant === true;
4768
- if (targetsTonic || isLastTonicStatement || alreadySecondary || isResolutionTarget) {
5282
+ const takesNoDominant = !canBeTonicized(next, tonicizable);
5283
+ if (targetsTonic || isLastTonicStatement || alreadySecondary || isResolutionTarget || takesNoDominant) {
4769
5284
  continue;
4770
5285
  }
4771
5286
  if (draw.prob(harmonic, "reharmonize", i)) {
@@ -4785,12 +5300,6 @@ function generateProgression(opts) {
4785
5300
  }
4786
5301
 
4787
5302
  // src/generate/reharmony/index.ts
4788
- function hasInterval(chord, semitone) {
4789
- return chord.intervals.some((iv) => pitchClassOf(iv) === semitone);
4790
- }
4791
- function isDominantType(chord) {
4792
- return hasInterval(chord, 4) && hasInterval(chord, 10);
4793
- }
4794
5303
  function commonToneCount(a, b) {
4795
5304
  const set = new Set(b);
4796
5305
  return a.filter((pc) => set.has(pc)).length;
@@ -4821,23 +5330,10 @@ function sameChord(a, b) {
4821
5330
  return pitchClassOf(a.rootPc) === pitchClassOf(b.rootPc) && a.quality === b.quality;
4822
5331
  }
4823
5332
  var THIRD_OFFSETS = [3, 4, 8, 9];
4824
- var MIDDLE_C = 60;
4825
5333
  var MAX_HINT_ALTER = 2;
4826
5334
  function bareSpelling(note2) {
4827
5335
  return { letter: note2.letter, alter: note2.alter };
4828
5336
  }
4829
- function accidentalLoad(tonic, key) {
4830
- return spellScale(tonic, key).reduce((sum, note2) => sum + Math.abs(note2.alter), 0);
4831
- }
4832
- function keyTonicOf(key) {
4833
- const rootPc = pitchClassOf(key.rootPc);
4834
- const sharp = bareSpelling(midiToNote(MIDDLE_C + rootPc, "sharp"));
4835
- const flat = bareSpelling(midiToNote(MIDDLE_C + rootPc, "flat"));
4836
- if (sharp.letter === flat.letter) {
4837
- return sharp;
4838
- }
4839
- return accidentalLoad(flat, key) <= accidentalLoad(sharp, key) ? flat : sharp;
4840
- }
4841
5337
  function spelledOnRoot(chord, root, tonic, key) {
4842
5338
  const usable = noteToPitchClass(root) === pitchClassOf(chord.rootPc) && Math.abs(root.alter) <= MAX_HINT_ALTER;
4843
5339
  const spelled = {
@@ -4861,12 +5357,12 @@ function tritoneSubstituteRoot(dominant, tonic, key) {
4861
5357
  return transposeNote(resolution, 1);
4862
5358
  }
4863
5359
  function substituteChord(chord, key, opts) {
5360
+ assertOptions(opts, "opts");
4864
5361
  const source = toChordData(chord);
4865
- const scale = toKeyScale(key);
5362
+ const { scale, tonic } = resolveKey(key);
4866
5363
  const originalTriad = chordPitchClasses(makeChord(source.rootPc, triadQualityOf(source)));
4867
5364
  const candidates = [];
4868
- const tonic = keyTonicOf(scale);
4869
- if (isDominantType(source)) {
5365
+ if (soundsDominantSeventh(source)) {
4870
5366
  const substitute = makeChord(pitchClassOf(source.rootPc + 6), "dom7");
4871
5367
  candidates.push({
4872
5368
  chord: spelledOnRoot(substitute, tritoneSubstituteRoot(source, tonic, scale), tonic, scale),
@@ -4922,7 +5418,7 @@ function substituteChord(chord, key, opts) {
4922
5418
  return results;
4923
5419
  }
4924
5420
  function modalInterchangePalette(key) {
4925
- const scale = toKeyScale(key);
5421
+ const { scale, tonic } = resolveKey(key);
4926
5422
  const palette = [];
4927
5423
  const seen = /* @__PURE__ */ new Set();
4928
5424
  const add = (chord) => {
@@ -4937,7 +5433,6 @@ function modalInterchangePalette(key) {
4937
5433
  source: borrowedSource(chord, scale)
4938
5434
  });
4939
5435
  };
4940
- const tonic = keyTonicOf(scale);
4941
5436
  const parallel = parallelKey(scale);
4942
5437
  for (const triad of diatonicTriadsOf(parallel)) {
4943
5438
  if (!isDiatonic(triad, scale) && borrowedSource(triad, scale) !== null) {
@@ -4952,13 +5447,12 @@ function modalInterchangePalette(key) {
4952
5447
  }
4953
5448
  function negativeHarmonyMirror(chord, key) {
4954
5449
  const source = toChordData(chord);
4955
- const scale = toKeyScale(key);
5450
+ const { scale, tonic } = resolveKey(key);
4956
5451
  const tonicPc = pitchClassOf(scale.rootPc);
4957
- const tonic = keyTonicOf(scale);
4958
5452
  const mirror = (p) => pitchClassOf(2 * tonicPc + 7 - p);
4959
5453
  const pcs = chordPitchClasses(source).map(mirror);
4960
5454
  const bassPc = source.bassPc !== void 0 ? mirror(source.bassPc) : void 0;
4961
- const pitches = bassPc !== void 0 ? [bassPc - 12, ...pcs] : pcs;
5455
+ const pitches = bassPc !== void 0 ? [bassPc, ...pcs.map((pc) => pc + 12)] : pcs;
4962
5456
  const detected = detectChordBest(pitches);
4963
5457
  if (detected) {
4964
5458
  if (bassPc === void 0 && detected.bassPc !== void 0) {
@@ -4973,9 +5467,9 @@ function negativeHarmonyMirror(chord, key) {
4973
5467
  // src/generate/rhythm/index.ts
4974
5468
  var DEFAULT_RHYTHM_VELOCITY = 96;
4975
5469
  function rhythmToNoteEvents(events, pitch, velocity = DEFAULT_RHYTHM_VELOCITY) {
4976
- assertFiniteNumber(pitch, "pitch");
4977
- assertRange(velocity, 0, 127, "velocity");
4978
- return events.map((event) => ({
5470
+ assertMidiPitch(pitch, "pitch");
5471
+ assertInteger(velocity, "velocity", 0, 127);
5472
+ return assertArray(events, "events").map((event) => ({
4979
5473
  pitch,
4980
5474
  startBeat: event.position,
4981
5475
  durationBeat: event.duration,
@@ -4986,6 +5480,7 @@ var DEFAULT_SUBDIVISION = 2;
4986
5480
  var DEFAULT_DENSITY = 0.5;
4987
5481
  var DEFAULT_BARS = 1;
4988
5482
  function onsetWeightCurve(weight) {
5483
+ assertInteger(weight, "weight", 0, 3);
4989
5484
  switch (weight) {
4990
5485
  case 3:
4991
5486
  return 1;
@@ -4998,13 +5493,15 @@ function onsetWeightCurve(weight) {
4998
5493
  }
4999
5494
  }
5000
5495
  function generateRhythm(ts, opts = {}) {
5001
- const bars = opts.bars ?? DEFAULT_BARS;
5002
- const subdivision = opts.subdivision ?? DEFAULT_SUBDIVISION;
5003
- const ctx = resolveContext(opts.ctx);
5496
+ const asked = assertOptions(opts, "opts");
5497
+ const meter = meterAt(0, toMeterData(ts, "ts"));
5498
+ const bars = asked.bars ?? DEFAULT_BARS;
5499
+ const subdivision = asked.subdivision ?? DEFAULT_SUBDIVISION;
5500
+ const ctx = resolveContext(asked.ctx);
5004
5501
  const density = ctx.rhythmic ?? DEFAULT_DENSITY;
5005
5502
  assertPositiveInt(bars, "rhythm bars");
5006
5503
  assertPositiveInt(subdivision, "rhythm subdivision");
5007
- const barBeats = beatsPerBar(ts);
5504
+ const barBeats = beatsPerBar(meter);
5008
5505
  const spanBeats = barBeats * bars;
5009
5506
  const step = 1 / subdivision;
5010
5507
  const slotsPerBar = Math.ceil(barBeats * subdivision);
@@ -5024,7 +5521,7 @@ function generateRhythm(ts, opts = {}) {
5024
5521
  positions.push(position);
5025
5522
  continue;
5026
5523
  }
5027
- const weight = metricWeight(position, ts);
5524
+ const weight = metricWeight(position, meter);
5028
5525
  const probability = onsetWeightCurve(weight) * density;
5029
5526
  if (draw.prob(probability, "onset", bar2, slot)) {
5030
5527
  positions.push(position);
@@ -5040,12 +5537,14 @@ function generateRhythm(ts, opts = {}) {
5040
5537
  return events;
5041
5538
  }
5042
5539
  function rhythmDensity(events, ts) {
5540
+ assertArray(events, "events");
5541
+ const barBeats = beatsPerBar(ts);
5043
5542
  if (events.length === 0) {
5044
5543
  return 0;
5045
5544
  }
5046
5545
  const last = events[events.length - 1];
5047
5546
  const spanBeats = (last?.position ?? 0) + (last?.duration ?? 0);
5048
- const bars = spanBeats / beatsPerBar(ts);
5547
+ const bars = spanBeats / barBeats;
5049
5548
  return bars > 0 ? events.length / bars : 0;
5050
5549
  }
5051
5550
 
@@ -5099,6 +5598,7 @@ export {
5099
5598
  applyGrooveTemplate,
5100
5599
  classifyMelodyTones,
5101
5600
  harmonizeMelody,
5601
+ cellSpan,
5102
5602
  generateMotif,
5103
5603
  motifToNoteEvents,
5104
5604
  transformMotif,
@@ -5118,4 +5618,4 @@ export {
5118
5618
  generateRhythm,
5119
5619
  rhythmDensity
5120
5620
  };
5121
- //# sourceMappingURL=chunk-VOHZB45S.js.map
5621
+ //# sourceMappingURL=chunk-R5VUXQMP.js.map