@libraz/libcantus 0.9.4 → 0.9.5

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 (120) hide show
  1. package/README.md +27 -18
  2. package/README_ja.md +268 -0
  3. package/dist/analyze/index.cjs +7 -7
  4. package/dist/analyze/index.d.cts +9 -99
  5. package/dist/analyze/index.d.ts +9 -99
  6. package/dist/analyze/index.js +6 -6
  7. package/dist/{chunk-36N73KZ3.cjs → chunk-2A2QMA74.cjs} +80 -65
  8. package/dist/chunk-2A2QMA74.cjs.map +1 -0
  9. package/dist/{chunk-YLEMGEFH.js → chunk-2PTFGVD7.js} +50 -35
  10. package/dist/chunk-2PTFGVD7.js.map +1 -0
  11. package/dist/{chunk-ZEJR4TKL.cjs → chunk-357ZDPVU.cjs} +36 -47
  12. package/dist/chunk-357ZDPVU.cjs.map +1 -0
  13. package/dist/{chunk-EHVEBWM5.js → chunk-3KX76RLG.js} +136 -37
  14. package/dist/chunk-3KX76RLG.js.map +1 -0
  15. package/dist/{chunk-RC6GBMCD.cjs → chunk-4VZ5MZAM.cjs} +155 -111
  16. package/dist/chunk-4VZ5MZAM.cjs.map +1 -0
  17. package/dist/{chunk-RK35RIN7.js → chunk-7EONYVI5.js} +115 -28
  18. package/dist/chunk-7EONYVI5.js.map +1 -0
  19. package/dist/{chunk-GQSGS2N4.js → chunk-7JEP3MFY.js} +5 -16
  20. package/dist/chunk-7JEP3MFY.js.map +1 -0
  21. package/dist/{chunk-7NKDPF74.js → chunk-AID75RN3.js} +203 -104
  22. package/dist/chunk-AID75RN3.js.map +1 -0
  23. package/dist/{chunk-FNDWXRRI.cjs → chunk-BSLTLGM3.cjs} +55 -37
  24. package/dist/chunk-BSLTLGM3.cjs.map +1 -0
  25. package/dist/{chunk-NP4NA2FB.cjs → chunk-CU6VG2YM.cjs} +39 -36
  26. package/dist/chunk-CU6VG2YM.cjs.map +1 -0
  27. package/dist/{chunk-NYYWMV2C.js → chunk-DZMHIGI4.js} +27 -17
  28. package/dist/chunk-DZMHIGI4.js.map +1 -0
  29. package/dist/{chunk-I24MV65U.js → chunk-EDOTEABK.js} +18 -15
  30. package/dist/{chunk-I24MV65U.js.map → chunk-EDOTEABK.js.map} +1 -1
  31. package/dist/{chunk-NKIBQ6PT.js → chunk-HN2OEUN2.js} +7 -4
  32. package/dist/chunk-HN2OEUN2.js.map +1 -0
  33. package/dist/{chunk-D2FAJBYL.js → chunk-KBTTEX7Q.js} +30 -12
  34. package/dist/chunk-KBTTEX7Q.js.map +1 -0
  35. package/dist/{chunk-CAN76NCU.cjs → chunk-KQN2ZYND.cjs} +147 -102
  36. package/dist/chunk-KQN2ZYND.cjs.map +1 -0
  37. package/dist/{chunk-4HLLZPXT.cjs → chunk-MPTVJIVG.cjs} +350 -251
  38. package/dist/chunk-MPTVJIVG.cjs.map +1 -0
  39. package/dist/{chunk-RUMU3UA6.cjs → chunk-MTMZTMWM.cjs} +12 -12
  40. package/dist/{chunk-RUMU3UA6.cjs.map → chunk-MTMZTMWM.cjs.map} +1 -1
  41. package/dist/{chunk-KMHOICIY.js → chunk-NNB67XA2.js} +90 -45
  42. package/dist/chunk-NNB67XA2.js.map +1 -0
  43. package/dist/{chunk-LW53AHQU.cjs → chunk-OL3IHHSJ.cjs} +228 -141
  44. package/dist/chunk-OL3IHHSJ.cjs.map +1 -0
  45. package/dist/{chunk-7MGZ7D62.cjs → chunk-PI3UXRWK.cjs} +43 -13
  46. package/dist/chunk-PI3UXRWK.cjs.map +1 -0
  47. package/dist/{chunk-XEBG3G53.js → chunk-R2VVFAIN.js} +42 -12
  48. package/dist/chunk-R2VVFAIN.js.map +1 -0
  49. package/dist/{chunk-Q6OP4U43.cjs → chunk-WFDXOLUB.cjs} +50 -40
  50. package/dist/chunk-WFDXOLUB.cjs.map +1 -0
  51. package/dist/{chunk-NVE34YGZ.cjs → chunk-WTAN4ACG.cjs} +212 -113
  52. package/dist/chunk-WTAN4ACG.cjs.map +1 -0
  53. package/dist/{chunk-FCANXBAT.cjs → chunk-YFJXS3AU.cjs} +8 -5
  54. package/dist/chunk-YFJXS3AU.cjs.map +1 -0
  55. package/dist/{chunk-Q2XUP227.js → chunk-YUE6TJ4M.js} +89 -45
  56. package/dist/chunk-YUE6TJ4M.js.map +1 -0
  57. package/dist/{chunk-HKGYCQ34.js → chunk-ZGLJM4A4.js} +2 -2
  58. package/dist/core/index.cjs +15 -5
  59. package/dist/core/index.cjs.map +1 -1
  60. package/dist/core/index.d.cts +14 -4
  61. package/dist/core/index.d.ts +14 -4
  62. package/dist/core/index.js +14 -4
  63. package/dist/{function-DNugAOrt.d.cts → function-CxixTB02.d.cts} +98 -7
  64. package/dist/{function-BtiZb9i9.d.ts → function-DTCt675z.d.ts} +98 -7
  65. package/dist/generate/index.cjs +10 -10
  66. package/dist/generate/index.d.cts +21 -21
  67. package/dist/generate/index.d.ts +21 -21
  68. package/dist/generate/index.js +9 -9
  69. package/dist/{index-tJhkmQgE.d.cts → index-B2Ulxxpo.d.cts} +3 -2
  70. package/dist/{index-Be74mcon.d.cts → index-BkYJ0xMq.d.cts} +2 -2
  71. package/dist/{index-6VR8-kit.d.ts → index-BuaB-ypt.d.ts} +3 -2
  72. package/dist/{index-70p168VM.d.ts → index-CAN7v-kw.d.ts} +9 -2
  73. package/dist/{index-C83mZI1P.d.ts → index-COw1Ky9g.d.ts} +1 -1
  74. package/dist/{index-tSQh-xwY.d.ts → index-Cfx8xgH0.d.ts} +1 -1
  75. package/dist/{index-C5UBbQ4G.d.ts → index-DRtIhlJj.d.ts} +2 -2
  76. package/dist/{index-BtXT6UQ0.d.cts → index-alrRninU.d.cts} +9 -2
  77. package/dist/{index-y_ak9_Hi.d.cts → index-oTQbYYIs.d.cts} +1 -1
  78. package/dist/{index-OG65ocOr.d.ts → index-tR9jTn26.d.cts} +4 -1
  79. package/dist/{index-OG65ocOr.d.cts → index-tR9jTn26.d.ts} +4 -1
  80. package/dist/{index-D6b6na69.d.cts → index-x2vcsduE.d.cts} +1 -1
  81. package/dist/index.cjs +24 -14
  82. package/dist/index.cjs.map +1 -1
  83. package/dist/index.d.cts +11 -11
  84. package/dist/index.d.ts +11 -11
  85. package/dist/index.js +23 -13
  86. package/dist/model/index.cjs +7 -7
  87. package/dist/model/index.d.cts +48 -16
  88. package/dist/model/index.d.ts +48 -16
  89. package/dist/model/index.js +6 -6
  90. package/dist/{styled-DIRkk9Uz.d.ts → styled-Bl0ZK28h.d.ts} +40 -3
  91. package/dist/{styled-DJbkJQfj.d.cts → styled-Crv6SA4S.d.cts} +40 -3
  92. package/dist/theory/index.cjs +6 -6
  93. package/dist/theory/index.d.cts +9 -9
  94. package/dist/theory/index.d.ts +9 -9
  95. package/dist/theory/index.js +5 -5
  96. package/package.json +12 -9
  97. package/dist/chunk-36N73KZ3.cjs.map +0 -1
  98. package/dist/chunk-4HLLZPXT.cjs.map +0 -1
  99. package/dist/chunk-7MGZ7D62.cjs.map +0 -1
  100. package/dist/chunk-7NKDPF74.js.map +0 -1
  101. package/dist/chunk-CAN76NCU.cjs.map +0 -1
  102. package/dist/chunk-D2FAJBYL.js.map +0 -1
  103. package/dist/chunk-EHVEBWM5.js.map +0 -1
  104. package/dist/chunk-FCANXBAT.cjs.map +0 -1
  105. package/dist/chunk-FNDWXRRI.cjs.map +0 -1
  106. package/dist/chunk-GQSGS2N4.js.map +0 -1
  107. package/dist/chunk-KMHOICIY.js.map +0 -1
  108. package/dist/chunk-LW53AHQU.cjs.map +0 -1
  109. package/dist/chunk-NKIBQ6PT.js.map +0 -1
  110. package/dist/chunk-NP4NA2FB.cjs.map +0 -1
  111. package/dist/chunk-NVE34YGZ.cjs.map +0 -1
  112. package/dist/chunk-NYYWMV2C.js.map +0 -1
  113. package/dist/chunk-Q2XUP227.js.map +0 -1
  114. package/dist/chunk-Q6OP4U43.cjs.map +0 -1
  115. package/dist/chunk-RC6GBMCD.cjs.map +0 -1
  116. package/dist/chunk-RK35RIN7.js.map +0 -1
  117. package/dist/chunk-XEBG3G53.js.map +0 -1
  118. package/dist/chunk-YLEMGEFH.js.map +0 -1
  119. package/dist/chunk-ZEJR4TKL.cjs.map +0 -1
  120. /package/dist/{chunk-HKGYCQ34.js.map → chunk-ZGLJM4A4.js.map} +0 -0
@@ -1,27 +1,29 @@
1
1
  import {
2
2
  createRng
3
- } from "./chunk-HKGYCQ34.js";
3
+ } from "./chunk-ZGLJM4A4.js";
4
4
  import {
5
5
  beatsPerBar,
6
6
  createNoteEventIndex,
7
7
  formatTimeSignature,
8
8
  isStrongBeat,
9
- metricWeight
10
- } from "./chunk-YLEMGEFH.js";
9
+ metricWeight,
10
+ pulseBeats
11
+ } from "./chunk-2PTFGVD7.js";
11
12
  import {
12
13
  isDiatonic,
13
14
  parallelKey
14
- } from "./chunk-KMHOICIY.js";
15
+ } from "./chunk-NNB67XA2.js";
15
16
  import {
16
17
  roleOf
17
- } from "./chunk-NKIBQ6PT.js";
18
+ } from "./chunk-HN2OEUN2.js";
18
19
  import {
19
20
  enumerateSafePitches,
20
21
  evaluateSafety
21
- } from "./chunk-NYYWMV2C.js";
22
+ } from "./chunk-DZMHIGI4.js";
22
23
  import {
23
24
  NATURAL_MINOR_MASK,
24
25
  chordPitchClasses,
26
+ chordQualities,
25
27
  chordToneRole,
26
28
  createsParallelPerfect,
27
29
  diatonicTriad,
@@ -32,9 +34,10 @@ import {
32
34
  minorKey,
33
35
  nearestScaleTone,
34
36
  scaleTonesInDegreeOrder
35
- } from "./chunk-D2FAJBYL.js";
37
+ } from "./chunk-KBTTEX7Q.js";
36
38
  import {
37
39
  InvalidInputError,
40
+ assertDegree,
38
41
  assertFiniteNumber,
39
42
  assertGenerationBudget,
40
43
  assertInteger,
@@ -43,12 +46,20 @@ import {
43
46
  assertPositiveInt,
44
47
  assertRange,
45
48
  assertTimeSignature,
49
+ clampToMidi,
46
50
  dropSilentNotes,
47
- pitchClassOf
48
- } from "./chunk-XEBG3G53.js";
51
+ pitchClassOf,
52
+ soundingNotesOnly
53
+ } from "./chunk-R2VVFAIN.js";
49
54
 
50
55
  // src/generate/bass/index.ts
51
- var BASS_STYLES = ["root", "rootFifth", "pop", "walking", "arpeggio"];
56
+ var BASS_STYLES = Object.freeze([
57
+ "root",
58
+ "rootFifth",
59
+ "pop",
60
+ "walking",
61
+ "arpeggio"
62
+ ]);
52
63
  var DEFAULT_TS = { numerator: 4, denominator: 4 };
53
64
  var DEFAULT_STYLE = "root";
54
65
  var DEFAULT_OCTAVE = 2;
@@ -90,10 +101,24 @@ function fifthPcOf(chord) {
90
101
  function bassPcOf(chord) {
91
102
  return pitchClassOf(chord.bassPc ?? chord.rootPc);
92
103
  }
93
- function beatPositions(start, end) {
104
+ function beatPositions(start, end, ts) {
94
105
  const positions = [];
95
- for (let p = start; p < end - EPS; p += 1) {
96
- positions.push(p);
106
+ const barBeats = beatsPerBar(ts);
107
+ const pulse = pulseBeats(ts);
108
+ const firstBar = Math.floor(start / barBeats);
109
+ const lastBar = Math.floor((end - EPS) / barBeats);
110
+ for (let bar = firstBar; bar <= lastBar; bar += 1) {
111
+ const barStart = bar * barBeats;
112
+ const firstPulse = Math.max(0, Math.ceil((start - barStart - EPS) / pulse));
113
+ for (let index = firstPulse; ; index += 1) {
114
+ const position = barStart + index * pulse;
115
+ if (position >= barStart + barBeats - EPS || position >= end - EPS) {
116
+ break;
117
+ }
118
+ if (position >= start - EPS) {
119
+ positions.push(position);
120
+ }
121
+ }
97
122
  }
98
123
  if (positions.length === 0) {
99
124
  positions.push(start);
@@ -101,7 +126,10 @@ function beatPositions(start, end) {
101
126
  return positions;
102
127
  }
103
128
  function emit(ctx, pos, pc, midiOverride) {
104
- const midi = midiOverride ?? placePc(pc, ctx.prevMidi, ctx.low);
129
+ const midi = clampToMidi(
130
+ midiOverride ?? placePc(pc, ctx.prevMidi, ctx.low),
131
+ "generated bass pitch"
132
+ );
105
133
  const velocity = isStrongBeat(pos, ctx.ts) ? STRONG_VELOCITY : WEAK_VELOCITY;
106
134
  ctx.notes.push({ startBeat: pos, pitch: midi, velocity });
107
135
  return midi;
@@ -117,7 +145,7 @@ function buildRootFifth(ctx, seg) {
117
145
  }
118
146
  }
119
147
  function buildPop(ctx, seg) {
120
- const positions = beatPositions(seg.startBeat, seg.endBeat);
148
+ const positions = beatPositions(seg.startBeat, seg.endBeat, ctx.ts);
121
149
  const rootPc = bassPcOf(seg.chord);
122
150
  let emitted = false;
123
151
  for (const pos of positions) {
@@ -140,7 +168,7 @@ function buildPop(ctx, seg) {
140
168
  }
141
169
  function buildArpeggio(ctx, seg) {
142
170
  const tones = chordTonePcs(seg.chord);
143
- const positions = beatPositions(seg.startBeat, seg.endBeat);
171
+ const positions = beatPositions(seg.startBeat, seg.endBeat, ctx.ts);
144
172
  positions.forEach((pos, i) => {
145
173
  const pc = tones[i % tones.length] ?? bassPcOf(seg.chord);
146
174
  ctx.prevMidi = emit(ctx, pos, pc);
@@ -168,7 +196,7 @@ function foldIntoBand(midi, low) {
168
196
  }
169
197
  function buildWalking(ctx, seg, next) {
170
198
  const tones = chordTonePcs(seg.chord);
171
- const positions = beatPositions(seg.startBeat, seg.endBeat);
199
+ const positions = beatPositions(seg.startBeat, seg.endBeat, ctx.ts);
172
200
  const count = positions.length;
173
201
  positions.forEach((pos, i) => {
174
202
  if (i === 0) {
@@ -197,6 +225,15 @@ function generateBassLine(opts) {
197
225
  }
198
226
  }
199
227
  const segments = [...opts.segments].sort((a, b) => a.startBeat - b.startBeat);
228
+ for (let index = 1; index < segments.length; index += 1) {
229
+ const previous = segments[index - 1];
230
+ const current = segments[index];
231
+ if (previous !== void 0 && current !== void 0 && current.startBeat < previous.endBeat) {
232
+ throw new InvalidInputError(
233
+ `bass segments must not overlap: segments ${index - 1} and ${index} overlap`
234
+ );
235
+ }
236
+ }
200
237
  if (segments.length === 0) {
201
238
  return [];
202
239
  }
@@ -204,7 +241,7 @@ function generateBassLine(opts) {
204
241
  assertTimeSignature(ts);
205
242
  const style = assertOneOf(opts.style ?? DEFAULT_STYLE, BASS_STYLES, "bass style");
206
243
  const octave = opts.octave ?? DEFAULT_OCTAVE;
207
- assertInteger(octave, "bass octave", -1, 9);
244
+ assertInteger(octave, "bass octave", -1, 8);
208
245
  const estimatedNotes = segments.reduce(
209
246
  (count, segment) => count + Math.max(1, Math.ceil(segment.endBeat - segment.startBeat)),
210
247
  0
@@ -256,7 +293,7 @@ function generateBassLine(opts) {
256
293
  continue;
257
294
  }
258
295
  out.push({
259
- pitch: note.pitch,
296
+ pitch: clampToMidi(note.pitch, "generated bass pitch"),
260
297
  startBeat: note.startBeat,
261
298
  durationBeat,
262
299
  velocity: note.velocity
@@ -429,8 +466,8 @@ function generateCounterMelody(opts) {
429
466
  }
430
467
  const chordChangeBeats = timeline === void 0 ? opts.chordChangeBeats ?? [] : timeline.segments.flatMap((segment) => [segment.startBeat, segment.endBeat]);
431
468
  const register = opts.register ?? "below";
432
- const melody = createNoteEventIndex(opts.melody, {
433
- allowNonPositiveDuration: true,
469
+ assertNoteEvents(opts.melody, "countermelody melody", { allowNonPositiveDuration: true });
470
+ const melody = createNoteEventIndex(soundingNotesOnly(opts.melody), {
434
471
  tieBreak: register === "below" ? "highest" : "lowest"
435
472
  });
436
473
  const ts = opts.ts ?? DEFAULT_TS2;
@@ -438,13 +475,7 @@ function generateCounterMelody(opts) {
438
475
  const rhythm = assertOneOf(opts.rhythm ?? "complement", ["complement", "follow"], "rhythm");
439
476
  const profile = assertOneOf(opts.profile ?? "pop", ["strict", "pop"], "safety profile");
440
477
  const rng = createRng(opts.seed ?? 0);
441
- if (opts.pitchLow !== void 0) {
442
- assertInteger(opts.pitchLow, "countermelody pitchLow");
443
- }
444
- if (opts.pitchHigh !== void 0) {
445
- assertInteger(opts.pitchHigh, "countermelody pitchHigh");
446
- }
447
- if (melody.notes.every((indexed) => indexed.note.durationBeat <= 0)) {
478
+ if (melody.notes.length === 0) {
448
479
  return [];
449
480
  }
450
481
  const spanStart = melody.notes[0]?.note.startBeat ?? 0;
@@ -452,10 +483,14 @@ function generateCounterMelody(opts) {
452
483
  assertGenerationBudget(Math.ceil((spanEnd - spanStart) / GRID_STEP), "countermelody grid");
453
484
  const meanPitch = melody.notes.reduce((sum, n) => sum + n.note.pitch, 0) / melody.notes.length;
454
485
  const defaultCenter = register === "below" ? meanPitch - REGISTER_OFFSET : meanPitch + REGISTER_OFFSET;
455
- const low = opts.pitchLow ?? Math.round(defaultCenter) - DEFAULT_HALF_RANGE;
456
- const high = opts.pitchHigh ?? Math.round(defaultCenter) + DEFAULT_HALF_RANGE;
457
- assertInteger(low, "countermelody pitchLow");
458
- assertInteger(high, "countermelody pitchHigh");
486
+ const low = clampToMidi(
487
+ opts.pitchLow ?? Math.round(defaultCenter) - DEFAULT_HALF_RANGE,
488
+ "countermelody pitchLow"
489
+ );
490
+ const high = clampToMidi(
491
+ opts.pitchHigh ?? Math.round(defaultCenter) + DEFAULT_HALF_RANGE,
492
+ "countermelody pitchHigh"
493
+ );
459
494
  if (low > high) {
460
495
  throw new InvalidInputError(
461
496
  `countermelody pitchLow must not exceed pitchHigh; received ${low} > ${high}`
@@ -547,7 +582,12 @@ function generateCounterMelody(opts) {
547
582
  1,
548
583
  Math.min(127, Math.round((melNote?.velocity ?? DEFAULT_MELODY_VELOCITY) - VELOCITY_DROP))
549
584
  );
550
- out.push({ pitch, startBeat: beat, durationBeat: endBeat - beat, velocity });
585
+ out.push({
586
+ pitch: clampToMidi(pitch),
587
+ startBeat: beat,
588
+ durationBeat: endBeat - beat,
589
+ velocity
590
+ });
551
591
  prevPitch = pitch;
552
592
  }
553
593
  return out;
@@ -819,13 +859,10 @@ function getGhostProbabilityAtPosition(beat, sixteenthInBeat) {
819
859
  const leadsIntoBackbeat = (beat === 0 || beat === 2) && sixteenthInBeat === 3;
820
860
  return leadsIntoBackbeat ? 0.6 : 0.25;
821
861
  }
822
- function selectGhostPositions(mood, rng) {
862
+ function selectGhostPositions(mood) {
823
863
  if (mood === 2 /* Energetic */) {
824
864
  return ["e", "a"];
825
865
  }
826
- if (mood === 0 /* Calm */) {
827
- return rng.prob(0.5) ? ["e"] : [];
828
- }
829
866
  return ["e"];
830
867
  }
831
868
 
@@ -841,6 +878,9 @@ function roleHiHatInstrument(role, useRide) {
841
878
  if (role === "ambient") {
842
879
  return GM.RIDE;
843
880
  }
881
+ if (role === "minimal") {
882
+ return GM.FHH;
883
+ }
844
884
  return useRide ? GM.RIDE : GM.CHH;
845
885
  }
846
886
  function sparser(level) {
@@ -1161,7 +1201,7 @@ function generateGhostNotesForBeat(ctx, sec) {
1161
1201
  if (ctx.beat !== 0 && ctx.beat !== 2) {
1162
1202
  return;
1163
1203
  }
1164
- const positions = selectGhostPositions(sec.ghostMood, ctx.rng);
1204
+ const positions = selectGhostPositions(sec.ghostMood);
1165
1205
  let ghostProb = getGhostDensity(sec.ghostMood, ctx.section, sec.backingDensity, ctx.bpm);
1166
1206
  if (sec.ghostBoost) {
1167
1207
  ghostProb = Math.min(1, ghostProb * 1.4);
@@ -1196,16 +1236,18 @@ function generatePreChorusBuildup(ctx, sec, isSectionLastBar) {
1196
1236
  const progress = (barInLift * 4 + ctx.beat) / (barsInLift * 4);
1197
1237
  const buildupVel = ctx.velocity * (0.5 + 0.5 * progress);
1198
1238
  ctx.track.add(GM.SD, ctx.beatTick, EIGHTH, buildupVel);
1199
- ctx.track.add(GM.SD, ctx.beatTick + EIGHTH, EIGHTH, buildupVel * 0.85);
1239
+ ctx.track.add(
1240
+ GM.SD,
1241
+ swing16(ctx.beatTick + EIGHTH, sec, ctx.swingAmount),
1242
+ EIGHTH,
1243
+ buildupVel * 0.85
1244
+ );
1200
1245
  if (isSectionLastBar && ctx.beat === 3) {
1201
1246
  ctx.track.add(GM.CRASH, ctx.beatTick + EIGHTH + SIXTEENTH, SIXTEENTH, ctx.velocity * 1.1);
1202
1247
  }
1203
1248
  }
1204
1249
  function generateHiHatForBeat(ctx, sec) {
1205
1250
  if (!shouldPlayHiHat(sec.role)) {
1206
- if (sec.useFootHh && (ctx.beat === 0 || ctx.beat === 2)) {
1207
- ctx.track.add(GM.FHH, ctx.beatTick, EIGHTH, footHiHatVelocity(ctx.rng));
1208
- }
1209
1251
  return;
1210
1252
  }
1211
1253
  const crossStick = sec.useRide && shouldUseBridgeCrossStick(ctx.section, ctx.beat);
@@ -1213,11 +1255,12 @@ function generateHiHatForBeat(ctx, sec) {
1213
1255
  const hhType = sectionHiHatType(ctx.section, sec.role);
1214
1256
  const typeMult = hiHatTypeVelocityMultiplier(hhType);
1215
1257
  const isDynamicOpen = ctx.barHasOpenHh && ctx.beat === ctx.openHhBeat;
1258
+ const allowsOpenHiHat = sec.role !== "ambient" && sec.role !== "minimal";
1216
1259
  const dm = sec.densityMult;
1217
1260
  if (sec.hhLevel === "quarter") {
1218
1261
  const introRest = ctx.section === "intro" && ctx.beat !== 0;
1219
1262
  if (!introRest) {
1220
- if (isDynamicOpen) {
1263
+ if (isDynamicOpen && allowsOpenHiHat) {
1221
1264
  ctx.track.add(
1222
1265
  GM.OHH,
1223
1266
  ctx.beatTick,
@@ -1250,7 +1293,7 @@ function generateHiHatForBeat(ctx, sec) {
1250
1293
  continue;
1251
1294
  }
1252
1295
  const hhVel = Math.max(20, ctx.velocity * dm * typeMult * (eighth === 0 ? 0.9 : 0.65));
1253
- if (isDynamicOpen && eighth === 0) {
1296
+ if (isDynamicOpen && allowsOpenHiHat && eighth === 0) {
1254
1297
  ctx.track.add(GM.OHH, hhTick, EIGHTH, hhVel + OHH_VEL_BOOST);
1255
1298
  continue;
1256
1299
  }
@@ -1261,7 +1304,7 @@ function generateHiHatForBeat(ctx, sec) {
1261
1304
  } else if (eighth === 0) {
1262
1305
  useOpen = shouldAddOpenHHAccent(ctx.section, ctx.beat, ctx.bar, ctx.rng);
1263
1306
  }
1264
- if (useOpen) {
1307
+ if (useOpen && allowsOpenHiHat) {
1265
1308
  ctx.track.add(hiHatNote("open"), hhTick, EIGHTH, Math.max(20, hhVel * 1.1));
1266
1309
  } else {
1267
1310
  ctx.track.add(hhInstrument, hhTick, EIGHTH / 2, hhVel);
@@ -1276,11 +1319,11 @@ function generateHiHatForBeat(ctx, sec) {
1276
1319
  }
1277
1320
  const metricVel = hiHatVelocityMultiplier(sixteenth, ctx.rng);
1278
1321
  const hhVel = Math.max(20, ctx.velocity * dm * typeMult * metricVel);
1279
- if (isDynamicOpen && sixteenth === 0) {
1322
+ if (isDynamicOpen && allowsOpenHiHat && sixteenth === 0) {
1280
1323
  ctx.track.add(GM.OHH, hhTick, SIXTEENTH, hhVel + OHH_VEL_BOOST);
1281
1324
  continue;
1282
1325
  }
1283
- if (ctx.beat === 3 && sixteenth === 3) {
1326
+ if (allowsOpenHiHat && ctx.beat === 3 && sixteenth === 3) {
1284
1327
  const openProb = Math.max(0.1, Math.min(0.4, 30 / ctx.bpm));
1285
1328
  if (ctx.rng.prob(openProb)) {
1286
1329
  ctx.track.add(GM.OHH, hhTick, SIXTEENTH, Math.max(20, hhVel * 1.2));
@@ -1335,7 +1378,7 @@ function getFillStartBeat(energy) {
1335
1378
  return 2;
1336
1379
  case "high":
1337
1380
  case "peak":
1338
- return 0;
1381
+ return 2;
1339
1382
  }
1340
1383
  }
1341
1384
  function selectFillType(from, to, style, nextEnergy, rng) {
@@ -1600,7 +1643,7 @@ function generateFill(track, beatTick, beat, fillType, velocity) {
1600
1643
  }
1601
1644
 
1602
1645
  // src/generate/drums/hit.ts
1603
- var DRUM_NOTES = {
1646
+ var DRUM_NOTES = Object.freeze({
1604
1647
  kick: GM.BD,
1605
1648
  snare: GM.SD,
1606
1649
  sideStick: GM.SIDESTICK,
@@ -1615,7 +1658,7 @@ var DRUM_NOTES = {
1615
1658
  midTom: GM.TOM_M,
1616
1659
  lowTom: GM.TOM_L,
1617
1660
  shaker: GM.SHAKER
1618
- };
1661
+ });
1619
1662
  var VOICE_BY_NOTE = new Map(
1620
1663
  Object.entries(DRUM_NOTES).map(([voice, note]) => [note, voice])
1621
1664
  );
@@ -1794,12 +1837,12 @@ function getPercussionConfig(mood, section) {
1794
1837
  shaker16th: act[1]
1795
1838
  };
1796
1839
  }
1797
- function generateAuxPercussionForBar(track, barStart, config, role, densityMult, rng, bpm) {
1840
+ function generateAuxPercussionForBar(track, barStart, config, role, densityMult, rng, bpm, sec, swingAmount, barBeats = 4) {
1798
1841
  if (role === "minimal") {
1799
1842
  return;
1800
1843
  }
1801
1844
  if (config.tambourine) {
1802
- for (let beat = 1; beat <= 3; beat += 2) {
1845
+ for (let beat = 1; beat < barBeats; beat += 2) {
1803
1846
  const raw = 70 * densityMult * rng.float(0.9, 1.1);
1804
1847
  track.add(GM.TAMBOURINE, barStart + beat, EIGHTH, Math.max(40, Math.min(90, raw)));
1805
1848
  }
@@ -1808,12 +1851,12 @@ function generateAuxPercussionForBar(track, barStart, config, role, densityMult,
1808
1851
  const use16th = config.shaker16th && bpm < HH_16TH_BPM_THRESHOLD;
1809
1852
  if (use16th) {
1810
1853
  const velCurve = [0.75, 0.45, 0.6, 0.45];
1811
- for (let beat = 0; beat < 4; beat += 1) {
1854
+ for (let beat = 0; beat < barBeats; beat += 1) {
1812
1855
  for (let sub = 0; sub < 4; sub += 1) {
1813
1856
  const raw = 80 * (velCurve[sub] ?? 0.5) * densityMult * rng.float(0.9, 1.1);
1814
1857
  track.add(
1815
1858
  GM.SHAKER,
1816
- barStart + beat + sub * SIXTEENTH,
1859
+ swing16(barStart + beat + sub * SIXTEENTH, sec, swingAmount),
1817
1860
  SIXTEENTH,
1818
1861
  Math.max(25, Math.min(85, raw))
1819
1862
  );
@@ -1821,12 +1864,12 @@ function generateAuxPercussionForBar(track, barStart, config, role, densityMult,
1821
1864
  }
1822
1865
  } else {
1823
1866
  const velCurve = [0.75, 0.55];
1824
- for (let beat = 0; beat < 4; beat += 1) {
1867
+ for (let beat = 0; beat < barBeats; beat += 1) {
1825
1868
  for (let sub = 0; sub < 2; sub += 1) {
1826
1869
  const raw = 80 * (velCurve[sub] ?? 0.6) * densityMult * rng.float(0.9, 1.1);
1827
1870
  track.add(
1828
1871
  GM.SHAKER,
1829
- barStart + beat + sub * EIGHTH,
1872
+ swing16(barStart + beat + sub * EIGHTH, sec, swingAmount),
1830
1873
  EIGHTH,
1831
1874
  Math.max(25, Math.min(85, raw))
1832
1875
  );
@@ -1835,7 +1878,7 @@ function generateAuxPercussionForBar(track, barStart, config, role, densityMult,
1835
1878
  }
1836
1879
  }
1837
1880
  if (config.handclap) {
1838
- for (let beat = 1; beat <= 3; beat += 2) {
1881
+ for (let beat = 1; beat < barBeats; beat += 2) {
1839
1882
  const raw = 85 * densityMult * rng.float(0.9, 1.1);
1840
1883
  track.add(GM.HANDCLAP, barStart + beat, EIGHTH, Math.max(50, Math.min(100, raw)));
1841
1884
  }
@@ -1866,6 +1909,9 @@ function generateDrums(opts) {
1866
1909
  const style = mapping.style;
1867
1910
  const feel = opts.feel === void 0 ? mapping.feel : assertOneOf(opts.feel, DRUM_FEELS, "drum feel");
1868
1911
  const role = opts.role === void 0 ? "full" : assertOneOf(opts.role, DRUM_ROLES, "drum role");
1912
+ const ts = opts.ts ?? { numerator: 4, denominator: 4 };
1913
+ assertTimeSignature(ts, "drum time signature");
1914
+ const barBeats = beatsPerBar(ts);
1869
1915
  const playMainVoices = role !== "fxOnly";
1870
1916
  const section = mapSection(publicSection);
1871
1917
  const backingDensity = mapDensity(opts.density);
@@ -1901,7 +1947,7 @@ function generateDrums(opts) {
1901
1947
  const reuseSectionKick = (section === "b" || section === "chorus") && style !== "sparse";
1902
1948
  let sectionKick;
1903
1949
  for (let bar = 0; bar < opts.bars; bar += 1) {
1904
- const barStart = bar * 4;
1950
+ const barStart = bar * barBeats;
1905
1951
  const isLastBar = bar === opts.bars - 1;
1906
1952
  if (bar === 0 && section === "chorus") {
1907
1953
  track.add(GM.CRASH, barStart, 0.5, 100 * densityMult);
@@ -1921,7 +1967,7 @@ function generateDrums(opts) {
1921
1967
  }
1922
1968
  const inLift = isInPreChorusLift(section, bar, opts.bars, nextSection === "chorus");
1923
1969
  let currentFill = "snareRoll";
1924
- for (let beat = 0; beat < 4; beat += 1) {
1970
+ for (let beat = 0; beat < barBeats; beat += 1) {
1925
1971
  const beatTick = barStart + beat;
1926
1972
  const velocity = calculateVelocity(section, beat);
1927
1973
  if (opts.fills && isLastBar && !inLift && beat >= fillStartBeat) {
@@ -1958,14 +2004,14 @@ function generateDrums(opts) {
1958
2004
  generatePreChorusBuildup(ctx, sec, isLastBar);
1959
2005
  }
1960
2006
  if (euclidKick && !inLift) {
1961
- const stepLength = 4 / euclidSteps;
2007
+ const stepLength = barBeats / euclidSteps;
1962
2008
  for (let step = 0; step < euclidKick.length; step += 1) {
1963
2009
  const onset = step * stepLength;
1964
2010
  if (euclidKick[step] && Math.floor(onset) === beat) {
1965
2011
  const wholeBeat = Math.abs(onset - Math.round(onset)) < 1e-9;
1966
2012
  track.add(
1967
2013
  GM.BD,
1968
- barStart + onset,
2014
+ swing16(barStart + onset, sec, swingAmount),
1969
2015
  Math.min(0.5, stepLength),
1970
2016
  velocity * (wholeBeat ? 1 : 0.85)
1971
2017
  );
@@ -1982,7 +2028,7 @@ function generateDrums(opts) {
1982
2028
  generateHiHatForBeat(ctx, sec);
1983
2029
  }
1984
2030
  if (sec.useFootHh && shouldPlayHiHat(role)) {
1985
- for (let fhhBeat = 0; fhhBeat < 4; fhhBeat += 2) {
2031
+ for (let fhhBeat = 0; fhhBeat < barBeats; fhhBeat += 2) {
1986
2032
  const fhhTick = barStart + fhhBeat;
1987
2033
  if (!track.hasOnset(GM.FHH, fhhTick)) {
1988
2034
  track.add(GM.FHH, fhhTick, 0.5, footHiHatVelocity(rng));
@@ -2005,10 +2051,14 @@ function generateDrums(opts) {
2005
2051
  role,
2006
2052
  densityMult,
2007
2053
  rng,
2008
- opts.bpm
2054
+ opts.bpm,
2055
+ sec,
2056
+ swingAmount,
2057
+ barBeats
2009
2058
  );
2010
2059
  }
2011
- return [...track.hits].sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);
2060
+ const endBeat = opts.bars * barBeats;
2061
+ return track.hits.filter((hit) => hit.startBeat < endBeat).sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);
2012
2062
  }
2013
2063
 
2014
2064
  // src/generate/groove/index.ts
@@ -2070,7 +2120,8 @@ function extractGrooveTemplate(events, ts, subdivision = DEFAULT_GROOVE_SUBDIVIS
2070
2120
  assertNoteEvents(events, "groove source events", { allowNonPositiveDuration: true });
2071
2121
  assertPositiveInt(subdivision, "groove subdivision");
2072
2122
  const barBeats = beatsPerBar(ts);
2073
- const slotsPerBar = Math.round(barBeats * subdivision);
2123
+ const slotsPerBar = Math.ceil(barBeats * subdivision);
2124
+ assertPositiveInt(slotsPerBar, "groove template slots");
2074
2125
  assertGenerationBudget(slotsPerBar, "groove template slots");
2075
2126
  const offsetSums = new Array(slotsPerBar).fill(0);
2076
2127
  const velocitySums = new Array(slotsPerBar).fill(0);
@@ -2128,7 +2179,7 @@ function applyGrooveTemplate(events, template, ts) {
2128
2179
  template.slotsPerBar
2129
2180
  );
2130
2181
  const slot = template.slots[slotIndex];
2131
- const startBeat = quantizedBeat + (slot?.timingOffset ?? 0);
2182
+ const startBeat = Math.max(0, quantizedBeat + (slot?.timingOffset ?? 0));
2132
2183
  const velocity = slot && slot.velocity !== null && slot.velocity !== void 0 ? Math.round(slot.velocity) : event.velocity;
2133
2184
  return {
2134
2185
  pitch: event.pitch,
@@ -2338,41 +2389,47 @@ function harmonizeOnce(melody, key, candidates, segments, jitter, ts) {
2338
2389
  return { cost: bestCost, path };
2339
2390
  }
2340
2391
  function harmonizeMelody(opts) {
2341
- const noteIndex = createNoteEventIndex(opts.melody, { allowNonPositiveDuration: true });
2392
+ assertNoteEvents(opts.melody, "harmonize melody", { allowNonPositiveDuration: true });
2393
+ const soundingMelody = soundingNotesOnly(opts.melody);
2394
+ const noteIndex = createNoteEventIndex(soundingMelody);
2342
2395
  const ts = opts.ts ?? DEFAULT_METER;
2343
2396
  assertTimeSignature(ts);
2344
2397
  const requestedKey = opts.key ?? "infer";
2345
- const key = requestedKey === "infer" ? inferKey(opts.melody) : requestedKey;
2398
+ const key = requestedKey === "infer" ? inferKey(soundingMelody) : requestedKey;
2346
2399
  const placement = opts.placement ?? DEFAULT_PLACEMENT;
2347
- if (noteIndex.notes.every((indexed) => indexed.note.durationBeat <= 0)) {
2400
+ if (noteIndex.notes.length === 0) {
2348
2401
  return { transposeSemitones: 0, key, chords: [], melodyRoles: [] };
2349
2402
  }
2350
2403
  const candidates = buildCandidates(key, opts.reharmonize ?? "diatonic");
2351
2404
  const candAt = (i) => candidates[i] ?? FALLBACK;
2352
2405
  const rng = createRng(opts.seed ?? 0);
2353
2406
  const jitter = candidates.map(() => rng.next() * TIE_BREAK_JITTER2);
2354
- const melodyEnd = noteIndex.notes.reduce((m, n) => Math.max(m, n.endBeat), 0);
2407
+ const melodyStart = noteIndex.notes.reduce(
2408
+ (start, indexed) => Math.min(start, indexed.note.startBeat),
2409
+ Number.POSITIVE_INFINITY
2410
+ );
2411
+ const melodyEnd = noteIndex.notes.reduce((end, indexed) => Math.max(end, indexed.endBeat), 0);
2355
2412
  const hr = assertRange(
2356
2413
  opts.harmonicRhythm ?? DEFAULT_HARMONIC_RHYTHM,
2357
2414
  Number.MIN_VALUE,
2358
2415
  Number.MAX_SAFE_INTEGER,
2359
2416
  "harmonic rhythm"
2360
2417
  );
2361
- const segCount = Math.max(1, Math.ceil(melodyEnd / hr));
2418
+ const segmentStart = Math.floor(melodyStart / hr) * hr;
2419
+ const segCount = Math.max(1, Math.ceil((melodyEnd - segmentStart) / hr));
2362
2420
  assertGenerationBudget(segCount, "harmonic segments");
2363
- assertGenerationBudget(segCount * candidates.length * candidates.length, "harmonization search");
2364
2421
  const segments = Array.from({ length: segCount }, (_, s) => ({
2365
- startBeat: s * hr,
2366
- endBeat: (s + 1) * hr,
2422
+ startBeat: segmentStart + s * hr,
2423
+ endBeat: segmentStart + (s + 1) * hr,
2367
2424
  noteIndices: []
2368
2425
  }));
2369
2426
  let memberships = 0;
2370
2427
  for (const indexed of noteIndex.notes) {
2371
- if (indexed.note.durationBeat <= 0) {
2372
- continue;
2373
- }
2374
- const first = Math.max(0, Math.floor(indexed.note.startBeat / hr));
2375
- const lastExclusive = Math.min(segCount, Math.ceil(indexed.endBeat / hr - Number.EPSILON));
2428
+ const first = Math.max(0, Math.floor((indexed.note.startBeat - segmentStart) / hr));
2429
+ const lastExclusive = Math.min(
2430
+ segCount,
2431
+ Math.ceil((indexed.endBeat - segmentStart) / hr - Number.EPSILON)
2432
+ );
2376
2433
  memberships += Math.max(0, lastExclusive - first);
2377
2434
  assertGenerationBudget(memberships, "note-to-segment memberships");
2378
2435
  for (let segment = first; segment < lastExclusive; segment += 1) {
@@ -2394,6 +2451,10 @@ function harmonizeMelody(opts) {
2394
2451
  }
2395
2452
  }
2396
2453
  }
2454
+ assertGenerationBudget(
2455
+ transposes.length * segCount * candidates.length * candidates.length,
2456
+ "harmonization placement search"
2457
+ );
2397
2458
  let bestCost = Number.POSITIVE_INFINITY;
2398
2459
  let bestTs = 0;
2399
2460
  let bestPath = [];
@@ -2414,7 +2475,7 @@ function harmonizeMelody(opts) {
2414
2475
  const chord = {
2415
2476
  rootPc: cand.rootPc,
2416
2477
  quality: cand.quality,
2417
- startBeat: segments[s]?.startBeat ?? s * hr
2478
+ startBeat: segments[s]?.startBeat ?? segmentStart + s * hr
2418
2479
  };
2419
2480
  if (cand.degree !== void 0) {
2420
2481
  chord.degree = cand.degree;
@@ -2546,7 +2607,11 @@ function generateMotif(opts) {
2546
2607
  if (opts.chord && isDownbeat(startBeat, barBeats)) {
2547
2608
  pitch = nearestChordTone(pitch, opts.chord);
2548
2609
  }
2549
- notes.push({ pitch, startBeat, durationBeat: beatsPerNote });
2610
+ notes.push({
2611
+ pitch: clampToMidi(pitch, "generated motif pitch"),
2612
+ startBeat,
2613
+ durationBeat: beatsPerNote
2614
+ });
2550
2615
  }
2551
2616
  return { notes };
2552
2617
  }
@@ -2571,8 +2636,11 @@ function transformUnchecked(cell, t, amount, key) {
2571
2636
  if (amount !== void 0) {
2572
2637
  assertFiniteNumber(amount, "motif transform amount");
2573
2638
  }
2574
- if ((t === "transposeDiatonic" || t === "sequence") && amount !== void 0) {
2575
- assertInteger(amount, "diatonic transform amount");
2639
+ if ((t === "transposeChromatic" || t === "transposeDiatonic" || t === "sequence") && amount !== void 0) {
2640
+ assertInteger(
2641
+ amount,
2642
+ `${t === "transposeChromatic" ? "chromatic" : "diatonic"} transform amount`
2643
+ );
2576
2644
  }
2577
2645
  if ((t === "augment" || t === "diminish") && amount !== void 0 && amount <= 0) {
2578
2646
  throw new InvalidInputError("time transform amount must be positive");
@@ -2670,7 +2738,7 @@ function developMotif(cell, timeline, key, bars, ts = DEFAULT_TS4) {
2670
2738
  }
2671
2739
 
2672
2740
  // src/generate/progression/index.ts
2673
- var BORROWED_DEGREES = {
2741
+ var BORROWED_DEGREES = Object.freeze({
2674
2742
  /** Flat submediant: bVI. */
2675
2743
  bVI: 8,
2676
2744
  /** Flat subtonic: bVII. */
@@ -2683,7 +2751,7 @@ var BORROWED_DEGREES = {
2683
2751
  bII: 13,
2684
2752
  /** Sharp subdominant, the diminished #IV. */
2685
2753
  sharpIV: 14
2686
- };
2754
+ });
2687
2755
  var PRESETS = [
2688
2756
  {
2689
2757
  id: "fourChordPop",
@@ -2862,8 +2930,12 @@ function degreeToRootPc(degree, key) {
2862
2930
  const offset = BORROWED_OFFSET[degree] ?? 0;
2863
2931
  return ((key.rootPc % 12 + offset) % 12 + 12) % 12;
2864
2932
  }
2865
- function autoQuality(degree, key) {
2933
+ function autoQuality(degree, key, harmonicDominant) {
2866
2934
  if (degree >= 0 && degree <= 6) {
2935
+ const isMinor = (key.modeMask12 >> 3 & 1) === 1 && (key.modeMask12 >> 4 & 1) === 0;
2936
+ if (harmonicDominant && isMinor && degree === 4) {
2937
+ return "maj";
2938
+ }
2867
2939
  return diatonicTriad(degree, key).quality;
2868
2940
  }
2869
2941
  if (degree === 14) {
@@ -2874,13 +2946,13 @@ function autoQuality(degree, key) {
2874
2946
  }
2875
2947
  return "maj";
2876
2948
  }
2877
- function resolveCycle(degrees, key, ext) {
2949
+ function resolveCycle(degrees, key, ext, harmonicDominant) {
2878
2950
  const steps = [];
2879
2951
  for (const degree of degrees) {
2880
2952
  const step = {
2881
2953
  source: degree,
2882
2954
  rootPc: degreeToRootPc(degree, key),
2883
- quality: ext !== void 0 && ext !== "auto" ? ext : autoQuality(degree, key)
2955
+ quality: ext !== void 0 && ext !== "auto" ? ext : autoQuality(degree, key, harmonicDominant)
2884
2956
  };
2885
2957
  if (degree >= 0 && degree <= 6) {
2886
2958
  step.degree = degree;
@@ -2922,7 +2994,16 @@ function generateProgression(opts) {
2922
2994
  preset = {
2923
2995
  id: opts.preset.id ?? "custom",
2924
2996
  name: opts.preset.name ?? "Custom",
2925
- degrees: opts.preset.degrees,
2997
+ degrees: opts.preset.degrees.map((degree, index) => {
2998
+ assertDegree(degree, `progression preset degrees[${index}]`);
2999
+ const supported = degree >= 0 && degree <= 6 || Object.values(BORROWED_DEGREES).includes(degree);
3000
+ if (!supported) {
3001
+ throw new InvalidInputError(
3002
+ `progression preset degrees[${index}] is not a supported progression degree; received ${degree}`
3003
+ );
3004
+ }
3005
+ return degree;
3006
+ }),
2926
3007
  functional: opts.preset.functional ?? "loop",
2927
3008
  styles: opts.preset.styles ?? [opts.style]
2928
3009
  };
@@ -2934,7 +3015,13 @@ function generateProgression(opts) {
2934
3015
  }
2935
3016
  }
2936
3017
  preset ??= pickProgressionPreset(opts.style, seed);
2937
- const cycle = resolveCycle(preset?.degrees ?? [0], opts.key, opts.ext);
3018
+ const ext = opts.ext === void 0 ? "auto" : assertOneOf(opts.ext, ["auto", ...chordQualities()], "progression extension");
3019
+ const cycle = resolveCycle(
3020
+ preset?.degrees ?? [0],
3021
+ opts.key,
3022
+ ext,
3023
+ preset?.functional === "cadenceStrong"
3024
+ );
2938
3025
  const chords = [];
2939
3026
  for (let bar = 0; bar < opts.bars; bar += 1) {
2940
3027
  const step = cycle[bar % cycle.length];
@@ -2951,6 +3038,7 @@ function generateProgression(opts) {
2951
3038
  if (opts.reharmonize) {
2952
3039
  const tonicPc = (opts.key.rootPc % 12 + 12) % 12;
2953
3040
  const rrng = createRng((seed ^ 2654435769) >>> 0);
3041
+ let tonicStatements = chords.filter((chord) => chord.rootPc === tonicPc).length;
2954
3042
  for (let i = 0; i < chords.length - 1; i += 1) {
2955
3043
  const cur = chords[i];
2956
3044
  const next = chords[i + 1];
@@ -2959,12 +3047,16 @@ function generateProgression(opts) {
2959
3047
  }
2960
3048
  const domRoot = (next.rootPc + 7) % 12;
2961
3049
  const targetsTonic = (next.rootPc - tonicPc + 12) % 12 === 0;
3050
+ const isLastTonicStatement = cur.rootPc === tonicPc && tonicStatements <= 1;
2962
3051
  const alreadySecondary = cur.rootPc === domRoot && cur.quality === "dom7";
2963
3052
  const isResolutionTarget = chords[i - 1]?.secondaryDominant === true;
2964
- if (targetsTonic || alreadySecondary || isResolutionTarget) {
3053
+ if (targetsTonic || isLastTonicStatement || alreadySecondary || isResolutionTarget) {
2965
3054
  continue;
2966
3055
  }
2967
3056
  if (rrng.next() < 0.5) {
3057
+ if (cur.rootPc === tonicPc) {
3058
+ tonicStatements -= 1;
3059
+ }
2968
3060
  chords[i] = {
2969
3061
  rootPc: domRoot,
2970
3062
  quality: "dom7",
@@ -3014,21 +3106,28 @@ function generateRhythm(ts, opts = {}) {
3014
3106
  const barBeats = beatsPerBar(ts);
3015
3107
  const spanBeats = barBeats * bars;
3016
3108
  const step = 1 / subdivision;
3017
- const slotCount = Math.round(spanBeats * subdivision);
3109
+ const slotsPerBar = Math.ceil(barBeats * subdivision);
3110
+ const slotCount = slotsPerBar * bars;
3018
3111
  assertGenerationBudget(slotCount, "rhythm grid slots");
3019
- const slotsPerBar = Math.round(barBeats * subdivision);
3020
3112
  const rng = createRng(seed);
3021
3113
  const positions = [];
3022
- for (let i = 0; i < slotCount; i += 1) {
3023
- const position = i * step;
3024
- if (i % slotsPerBar === 0) {
3025
- positions.push(position);
3026
- continue;
3027
- }
3028
- const weight = metricWeight(position, ts);
3029
- const probability = onsetWeightCurve(weight) * density;
3030
- if (rng.prob(probability)) {
3031
- positions.push(position);
3114
+ for (let bar = 0; bar < bars; bar += 1) {
3115
+ const barStart = bar * barBeats;
3116
+ const barEnd = barStart + barBeats;
3117
+ for (let slot = 0; slot < slotsPerBar; slot += 1) {
3118
+ const position = barStart + slot * step;
3119
+ if (position >= barEnd - Number.EPSILON) {
3120
+ break;
3121
+ }
3122
+ if (slot === 0) {
3123
+ positions.push(position);
3124
+ continue;
3125
+ }
3126
+ const weight = metricWeight(position, ts);
3127
+ const probability = onsetWeightCurve(weight) * density;
3128
+ if (rng.prob(probability)) {
3129
+ positions.push(position);
3130
+ }
3032
3131
  }
3033
3132
  }
3034
3133
  const events = [];
@@ -3074,4 +3173,4 @@ export {
3074
3173
  generateRhythm,
3075
3174
  rhythmDensity
3076
3175
  };
3077
- //# sourceMappingURL=chunk-7NKDPF74.js.map
3176
+ //# sourceMappingURL=chunk-AID75RN3.js.map