@libraz/libcantus 0.9.3 → 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 (201) hide show
  1. package/README.md +29 -20
  2. package/README_ja.md +268 -0
  3. package/dist/analyze/index.cjs +39 -1947
  4. package/dist/analyze/index.cjs.map +1 -1
  5. package/dist/analyze/index.d.cts +161 -91
  6. package/dist/analyze/index.d.ts +161 -91
  7. package/dist/analyze/index.js +19 -8
  8. package/dist/chunk-2A2QMA74.cjs +261 -0
  9. package/dist/chunk-2A2QMA74.cjs.map +1 -0
  10. package/dist/chunk-2PTFGVD7.js +261 -0
  11. package/dist/chunk-2PTFGVD7.js.map +1 -0
  12. package/dist/chunk-357ZDPVU.cjs +162 -0
  13. package/dist/chunk-357ZDPVU.cjs.map +1 -0
  14. package/dist/chunk-3KX76RLG.js +1548 -0
  15. package/dist/chunk-3KX76RLG.js.map +1 -0
  16. package/dist/chunk-4VZ5MZAM.cjs +889 -0
  17. package/dist/chunk-4VZ5MZAM.cjs.map +1 -0
  18. package/dist/{chunk-C5XYKUVG.js → chunk-7EONYVI5.js} +471 -171
  19. package/dist/chunk-7EONYVI5.js.map +1 -0
  20. package/dist/{chunk-IT2O4XFE.js → chunk-7JEP3MFY.js} +42 -34
  21. package/dist/chunk-7JEP3MFY.js.map +1 -0
  22. package/dist/{chunk-4MHA432G.js → chunk-AID75RN3.js} +572 -250
  23. package/dist/chunk-AID75RN3.js.map +1 -0
  24. package/dist/chunk-BSLTLGM3.cjs +497 -0
  25. package/dist/chunk-BSLTLGM3.cjs.map +1 -0
  26. package/dist/chunk-CU6VG2YM.cjs +109 -0
  27. package/dist/chunk-CU6VG2YM.cjs.map +1 -0
  28. package/dist/{chunk-EH6EM37U.js → chunk-DZMHIGI4.js} +81 -59
  29. package/dist/chunk-DZMHIGI4.js.map +1 -0
  30. package/dist/{chunk-AKLYPWIN.js → chunk-EDOTEABK.js} +45 -19
  31. package/dist/chunk-EDOTEABK.js.map +1 -0
  32. package/dist/{chunk-2ZWMSJH6.js → chunk-HN2OEUN2.js} +7 -4
  33. package/dist/chunk-HN2OEUN2.js.map +1 -0
  34. package/dist/{chunk-MIWO425M.js → chunk-KBTTEX7Q.js} +128 -43
  35. package/dist/chunk-KBTTEX7Q.js.map +1 -0
  36. package/dist/chunk-KQN2ZYND.cjs +724 -0
  37. package/dist/chunk-KQN2ZYND.cjs.map +1 -0
  38. package/dist/chunk-MPTVJIVG.cjs +3176 -0
  39. package/dist/chunk-MPTVJIVG.cjs.map +1 -0
  40. package/dist/chunk-MTMZTMWM.cjs +53 -0
  41. package/dist/chunk-MTMZTMWM.cjs.map +1 -0
  42. package/dist/{chunk-CHMXELDS.js → chunk-NNB67XA2.js} +372 -197
  43. package/dist/chunk-NNB67XA2.js.map +1 -0
  44. package/dist/chunk-OL3IHHSJ.cjs +1204 -0
  45. package/dist/chunk-OL3IHHSJ.cjs.map +1 -0
  46. package/dist/chunk-PI3UXRWK.cjs +528 -0
  47. package/dist/chunk-PI3UXRWK.cjs.map +1 -0
  48. package/dist/chunk-R2VVFAIN.js +528 -0
  49. package/dist/chunk-R2VVFAIN.js.map +1 -0
  50. package/dist/chunk-T64C7ZD7.cjs +1 -0
  51. package/dist/chunk-T64C7ZD7.cjs.map +1 -0
  52. package/dist/chunk-WFDXOLUB.cjs +337 -0
  53. package/dist/chunk-WFDXOLUB.cjs.map +1 -0
  54. package/dist/chunk-WTAN4ACG.cjs +1548 -0
  55. package/dist/chunk-WTAN4ACG.cjs.map +1 -0
  56. package/dist/chunk-YFJXS3AU.cjs +33 -0
  57. package/dist/chunk-YFJXS3AU.cjs.map +1 -0
  58. package/dist/{chunk-GOAW4VYL.js → chunk-YUE6TJ4M.js} +315 -145
  59. package/dist/chunk-YUE6TJ4M.js.map +1 -0
  60. package/dist/{chunk-62QZKCJQ.js → chunk-ZGLJM4A4.js} +10 -6
  61. package/dist/chunk-ZGLJM4A4.js.map +1 -0
  62. package/dist/core/index.cjs +140 -647
  63. package/dist/core/index.cjs.map +1 -1
  64. package/dist/core/index.d.cts +313 -41
  65. package/dist/core/index.d.ts +313 -41
  66. package/dist/core/index.js +64 -16
  67. package/dist/{function-DVH4Eusr.d.ts → function-CxixTB02.d.cts} +122 -10
  68. package/dist/{function-DYt4iKsn.d.cts → function-DTCt675z.d.ts} +122 -10
  69. package/dist/generate/index.cjs +28 -4085
  70. package/dist/generate/index.cjs.map +1 -1
  71. package/dist/generate/index.d.cts +374 -66
  72. package/dist/generate/index.d.ts +374 -66
  73. package/dist/generate/index.js +23 -9
  74. package/dist/{index-Jc0Ryi79.d.ts → index-B2Ulxxpo.d.cts} +21 -5
  75. package/dist/{index-CpOsP1y9.d.cts → index-BkYJ0xMq.d.cts} +18 -17
  76. package/dist/{index-CxqBQtH1.d.cts → index-BuaB-ypt.d.ts} +21 -5
  77. package/dist/index-CAN7v-kw.d.ts +199 -0
  78. package/dist/{index-Bt4tOUD9.d.ts → index-COw1Ky9g.d.ts} +93 -8
  79. package/dist/{index-Dikst44v.d.cts → index-Cfx8xgH0.d.ts} +14 -11
  80. package/dist/{index-DxvARIP2.d.ts → index-DRtIhlJj.d.ts} +18 -17
  81. package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
  82. package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
  83. package/dist/index-alrRninU.d.cts +199 -0
  84. package/dist/{index-BNlGP86A.d.cts → index-oTQbYYIs.d.cts} +93 -8
  85. package/dist/index-tR9jTn26.d.cts +275 -0
  86. package/dist/index-tR9jTn26.d.ts +275 -0
  87. package/dist/{index-Bg5dYMmZ.d.ts → index-x2vcsduE.d.cts} +14 -11
  88. package/dist/index.cjs +429 -7383
  89. package/dist/index.cjs.map +1 -1
  90. package/dist/index.d.cts +15 -15
  91. package/dist/index.d.ts +15 -15
  92. package/dist/index.js +121 -27
  93. package/dist/model/index.cjs +8 -2562
  94. package/dist/model/index.cjs.map +1 -1
  95. package/dist/model/index.d.cts +471 -81
  96. package/dist/model/index.d.ts +471 -81
  97. package/dist/model/index.js +6 -7
  98. package/dist/styled-Bl0ZK28h.d.ts +543 -0
  99. package/dist/styled-Crv6SA4S.d.cts +543 -0
  100. package/dist/theory/index.cjs +172 -1822
  101. package/dist/theory/index.cjs.map +1 -1
  102. package/dist/theory/index.d.cts +151 -233
  103. package/dist/theory/index.d.ts +151 -233
  104. package/dist/theory/index.js +30 -7
  105. package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
  106. package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
  107. package/package.json +34 -13
  108. package/dist/chunk-2ZWMSJH6.js.map +0 -1
  109. package/dist/chunk-4MHA432G.js.map +0 -1
  110. package/dist/chunk-62QZKCJQ.js.map +0 -1
  111. package/dist/chunk-AKLYPWIN.js.map +0 -1
  112. package/dist/chunk-C5XYKUVG.js.map +0 -1
  113. package/dist/chunk-CHMXELDS.js.map +0 -1
  114. package/dist/chunk-DYWFNC4A.js +0 -152
  115. package/dist/chunk-DYWFNC4A.js.map +0 -1
  116. package/dist/chunk-EH6EM37U.js.map +0 -1
  117. package/dist/chunk-FBQK3ZQN.js +0 -131
  118. package/dist/chunk-FBQK3ZQN.js.map +0 -1
  119. package/dist/chunk-GOAW4VYL.js.map +0 -1
  120. package/dist/chunk-I5R7ZBJS.js +0 -202
  121. package/dist/chunk-I5R7ZBJS.js.map +0 -1
  122. package/dist/chunk-IT2O4XFE.js.map +0 -1
  123. package/dist/chunk-MIWO425M.js.map +0 -1
  124. package/dist/chunk-SGU7SPLK.js +0 -924
  125. package/dist/chunk-SGU7SPLK.js.map +0 -1
  126. package/dist/index-CGMZNCx6.d.cts +0 -140
  127. package/dist/index-CGMZNCx6.d.ts +0 -140
  128. package/dist/index-CnZCo29u.d.cts +0 -108
  129. package/dist/index-JnF4v9bx.d.ts +0 -108
  130. package/dist/styled-DZ7GjShb.d.ts +0 -282
  131. package/dist/styled-ZTbdaVFM.d.cts +0 -282
  132. package/src/analyze/arrange/index.ts +0 -21
  133. package/src/analyze/arrange/internal.ts +0 -127
  134. package/src/analyze/arrange/tension.ts +0 -199
  135. package/src/analyze/arrange/tracks.ts +0 -312
  136. package/src/analyze/detect/index.ts +0 -272
  137. package/src/analyze/functional/borrowed.ts +0 -84
  138. package/src/analyze/functional/cadence.ts +0 -66
  139. package/src/analyze/functional/function.ts +0 -184
  140. package/src/analyze/functional/index.ts +0 -23
  141. package/src/analyze/functional/internal.ts +0 -29
  142. package/src/analyze/functional/roman.ts +0 -402
  143. package/src/analyze/index.ts +0 -49
  144. package/src/analyze/timeline/index.ts +0 -451
  145. package/src/analyze/voice/index.ts +0 -339
  146. package/src/core/event-index/index.ts +0 -104
  147. package/src/core/index.ts +0 -61
  148. package/src/core/interval/index.ts +0 -81
  149. package/src/core/meter/index.ts +0 -302
  150. package/src/core/pitch/index.ts +0 -311
  151. package/src/core/random/index.ts +0 -76
  152. package/src/core/tuning/index.ts +0 -186
  153. package/src/core/types.ts +0 -29
  154. package/src/core/validation/index.ts +0 -119
  155. package/src/generate/bass/index.ts +0 -373
  156. package/src/generate/countermelody/index.ts +0 -470
  157. package/src/generate/drums/beat.ts +0 -266
  158. package/src/generate/drums/euclid.ts +0 -65
  159. package/src/generate/drums/fills.ts +0 -356
  160. package/src/generate/drums/ghost.ts +0 -119
  161. package/src/generate/drums/hihat.ts +0 -285
  162. package/src/generate/drums/hit.ts +0 -31
  163. package/src/generate/drums/index.ts +0 -361
  164. package/src/generate/drums/internal.ts +0 -257
  165. package/src/generate/drums/kick.ts +0 -160
  166. package/src/generate/drums/percussion.ts +0 -121
  167. package/src/generate/drums/rng.ts +0 -7
  168. package/src/generate/drums/swing.ts +0 -71
  169. package/src/generate/groove/index.ts +0 -351
  170. package/src/generate/harmonize/index.ts +0 -451
  171. package/src/generate/index.ts +0 -58
  172. package/src/generate/motif/index.ts +0 -422
  173. package/src/generate/progression/index.ts +0 -362
  174. package/src/generate/reharmony/index.ts +0 -323
  175. package/src/generate/rhythm/index.ts +0 -175
  176. package/src/index.ts +0 -13
  177. package/src/model/chord.ts +0 -442
  178. package/src/model/index.ts +0 -15
  179. package/src/model/interval.ts +0 -82
  180. package/src/model/key.ts +0 -266
  181. package/src/model/note.ts +0 -176
  182. package/src/model/progression.ts +0 -161
  183. package/src/model/shared.ts +0 -12
  184. package/src/theory/chord/index.ts +0 -375
  185. package/src/theory/chordscale/index.ts +0 -374
  186. package/src/theory/counterpoint/index.ts +0 -230
  187. package/src/theory/harmony/index.ts +0 -77
  188. package/src/theory/index.ts +0 -112
  189. package/src/theory/safety/index.ts +0 -488
  190. package/src/theory/scale/degrees.ts +0 -123
  191. package/src/theory/scale/index.ts +0 -28
  192. package/src/theory/scale/key.ts +0 -56
  193. package/src/theory/scale/masks.ts +0 -153
  194. package/src/theory/spelling/index.ts +0 -249
  195. package/src/theory/symbol/index.ts +0 -268
  196. package/src/theory/voicing/index.ts +0 -5
  197. package/src/theory/voicing/internal.ts +0 -168
  198. package/src/theory/voicing/leading.ts +0 -112
  199. package/src/theory/voicing/satb.ts +0 -216
  200. package/src/theory/voicing/styled.ts +0 -199
  201. package/tsconfig.json +0 -23
@@ -1,27 +1,29 @@
1
1
  import {
2
2
  createRng
3
- } from "./chunk-62QZKCJQ.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-I5R7ZBJS.js";
9
+ metricWeight,
10
+ pulseBeats
11
+ } from "./chunk-2PTFGVD7.js";
11
12
  import {
12
13
  isDiatonic,
13
14
  parallelKey
14
- } from "./chunk-CHMXELDS.js";
15
+ } from "./chunk-NNB67XA2.js";
15
16
  import {
16
17
  roleOf
17
- } from "./chunk-2ZWMSJH6.js";
18
+ } from "./chunk-HN2OEUN2.js";
18
19
  import {
19
20
  enumerateSafePitches,
20
21
  evaluateSafety
21
- } from "./chunk-EH6EM37U.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,18 +34,32 @@ import {
32
34
  minorKey,
33
35
  nearestScaleTone,
34
36
  scaleTonesInDegreeOrder
35
- } from "./chunk-MIWO425M.js";
37
+ } from "./chunk-KBTTEX7Q.js";
36
38
  import {
39
+ InvalidInputError,
40
+ assertDegree,
37
41
  assertFiniteNumber,
38
42
  assertGenerationBudget,
39
43
  assertInteger,
40
44
  assertNoteEvents,
45
+ assertOneOf,
41
46
  assertPositiveInt,
42
47
  assertRange,
43
- assertTimeSignature
44
- } from "./chunk-FBQK3ZQN.js";
48
+ assertTimeSignature,
49
+ clampToMidi,
50
+ dropSilentNotes,
51
+ pitchClassOf,
52
+ soundingNotesOnly
53
+ } from "./chunk-R2VVFAIN.js";
45
54
 
46
55
  // src/generate/bass/index.ts
56
+ var BASS_STYLES = Object.freeze([
57
+ "root",
58
+ "rootFifth",
59
+ "pop",
60
+ "walking",
61
+ "arpeggio"
62
+ ]);
47
63
  var DEFAULT_TS = { numerator: 4, denominator: 4 };
48
64
  var DEFAULT_STYLE = "root";
49
65
  var DEFAULT_OCTAVE = 2;
@@ -51,9 +67,6 @@ var STRONG_VELOCITY = 100;
51
67
  var WEAK_VELOCITY = 80;
52
68
  var PICKUP_PROB = 0.35;
53
69
  var EPS = 1e-9;
54
- function pitchClass(value) {
55
- return (Math.trunc(value) % 12 + 12) % 12;
56
- }
57
70
  function placePc(pc, anchor, low) {
58
71
  const high = low + 12;
59
72
  let midi = pc + 12 * Math.round((anchor - pc) / 12);
@@ -69,29 +82,43 @@ function chordTonePcs(chord) {
69
82
  const seen = /* @__PURE__ */ new Set();
70
83
  const pcs = [];
71
84
  for (const interval of chord.intervals) {
72
- const pc = pitchClass(chord.rootPc + interval);
85
+ const pc = pitchClassOf(chord.rootPc + interval);
73
86
  if (!seen.has(pc)) {
74
87
  seen.add(pc);
75
88
  pcs.push(pc);
76
89
  }
77
90
  }
78
91
  if (pcs.length === 0) {
79
- pcs.push(pitchClass(chord.rootPc));
92
+ pcs.push(pitchClassOf(chord.rootPc));
80
93
  }
81
94
  return pcs;
82
95
  }
83
96
  function fifthPcOf(chord) {
84
- const fifths = new Set(chord.intervals.map((i) => pitchClass(i)));
97
+ const fifths = new Set(chord.intervals.map((i) => pitchClassOf(i)));
85
98
  const chosen = fifths.has(7) ? 7 : fifths.has(6) ? 6 : fifths.has(8) ? 8 : void 0;
86
- return chosen === void 0 ? pitchClass(chord.rootPc) : pitchClass(chord.rootPc + chosen);
99
+ return chosen === void 0 ? pitchClassOf(chord.rootPc) : pitchClassOf(chord.rootPc + chosen);
87
100
  }
88
101
  function bassPcOf(chord) {
89
- return pitchClass(chord.bassPc ?? chord.rootPc);
102
+ return pitchClassOf(chord.bassPc ?? chord.rootPc);
90
103
  }
91
- function beatPositions(start, end) {
104
+ function beatPositions(start, end, ts) {
92
105
  const positions = [];
93
- for (let p = start; p < end - EPS; p += 1) {
94
- 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
+ }
95
122
  }
96
123
  if (positions.length === 0) {
97
124
  positions.push(start);
@@ -99,7 +126,10 @@ function beatPositions(start, end) {
99
126
  return positions;
100
127
  }
101
128
  function emit(ctx, pos, pc, midiOverride) {
102
- 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
+ );
103
133
  const velocity = isStrongBeat(pos, ctx.ts) ? STRONG_VELOCITY : WEAK_VELOCITY;
104
134
  ctx.notes.push({ startBeat: pos, pitch: midi, velocity });
105
135
  return midi;
@@ -115,7 +145,7 @@ function buildRootFifth(ctx, seg) {
115
145
  }
116
146
  }
117
147
  function buildPop(ctx, seg) {
118
- const positions = beatPositions(seg.startBeat, seg.endBeat);
148
+ const positions = beatPositions(seg.startBeat, seg.endBeat, ctx.ts);
119
149
  const rootPc = bassPcOf(seg.chord);
120
150
  let emitted = false;
121
151
  for (const pos of positions) {
@@ -125,7 +155,8 @@ function buildPop(ctx, seg) {
125
155
  } else if (ctx.rng.prob(PICKUP_PROB)) {
126
156
  if (ctx.rng.prob(0.5)) {
127
157
  const base = placePc(rootPc, ctx.prevMidi, ctx.low);
128
- emit(ctx, pos, rootPc, Math.max(ctx.low, base - 12));
158
+ const dropped = base - 12;
159
+ emit(ctx, pos, rootPc, dropped >= 0 ? dropped : base);
129
160
  } else {
130
161
  ctx.prevMidi = emit(ctx, pos, fifthPcOf(seg.chord));
131
162
  }
@@ -137,7 +168,7 @@ function buildPop(ctx, seg) {
137
168
  }
138
169
  function buildArpeggio(ctx, seg) {
139
170
  const tones = chordTonePcs(seg.chord);
140
- const positions = beatPositions(seg.startBeat, seg.endBeat);
171
+ const positions = beatPositions(seg.startBeat, seg.endBeat, ctx.ts);
141
172
  positions.forEach((pos, i) => {
142
173
  const pc = tones[i % tones.length] ?? bassPcOf(seg.chord);
143
174
  ctx.prevMidi = emit(ctx, pos, pc);
@@ -145,16 +176,27 @@ function buildArpeggio(ctx, seg) {
145
176
  }
146
177
  function approachNote(ctx, target, from) {
147
178
  const dir = from <= target ? -1 : 1;
179
+ const chromatic = target + dir;
148
180
  if (ctx.rng.prob(0.5)) {
149
- return target + dir;
181
+ return foldIntoBand(chromatic, ctx.low);
150
182
  }
151
183
  const cand = nearestScaleTone(target + dir * 2, ctx.key);
152
184
  const step = Math.abs(cand - target);
153
- return step >= 1 && step <= 2 ? cand : target + dir;
185
+ return foldIntoBand(step >= 1 && step <= 2 ? cand : chromatic, ctx.low);
186
+ }
187
+ function foldIntoBand(midi, low) {
188
+ let result = midi;
189
+ while (result < low) {
190
+ result += 12;
191
+ }
192
+ while (result > low + 12) {
193
+ result -= 12;
194
+ }
195
+ return result;
154
196
  }
155
197
  function buildWalking(ctx, seg, next) {
156
198
  const tones = chordTonePcs(seg.chord);
157
- const positions = beatPositions(seg.startBeat, seg.endBeat);
199
+ const positions = beatPositions(seg.startBeat, seg.endBeat, ctx.ts);
158
200
  const count = positions.length;
159
201
  positions.forEach((pos, i) => {
160
202
  if (i === 0) {
@@ -164,7 +206,7 @@ function buildWalking(ctx, seg, next) {
164
206
  if (next && count > 1 && i === count - 1) {
165
207
  const nextBass = placePc(bassPcOf(next.chord), ctx.prevMidi, ctx.low);
166
208
  const midi = approachNote(ctx, nextBass, ctx.prevMidi);
167
- ctx.prevMidi = emit(ctx, pos, pitchClass(midi), midi);
209
+ ctx.prevMidi = emit(ctx, pos, pitchClassOf(midi), midi);
168
210
  return;
169
211
  }
170
212
  const pc = tones[i % tones.length] ?? bassPcOf(seg.chord);
@@ -179,18 +221,27 @@ function generateBassLine(opts) {
179
221
  assertRange(segment.startBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].startBeat`);
180
222
  assertRange(segment.endBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].endBeat`);
181
223
  if (segment.endBeat <= segment.startBeat) {
182
- throw new RangeError(`segments[${index}] must have a positive duration`);
224
+ throw new InvalidInputError(`segments[${index}] must have a positive duration`);
183
225
  }
184
226
  }
185
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
+ }
186
237
  if (segments.length === 0) {
187
238
  return [];
188
239
  }
189
240
  const ts = opts.ts ?? DEFAULT_TS;
190
241
  assertTimeSignature(ts);
191
- const style = opts.style ?? DEFAULT_STYLE;
242
+ const style = assertOneOf(opts.style ?? DEFAULT_STYLE, BASS_STYLES, "bass style");
192
243
  const octave = opts.octave ?? DEFAULT_OCTAVE;
193
- assertInteger(octave, "bass octave", -1, 9);
244
+ assertInteger(octave, "bass octave", -1, 8);
194
245
  const estimatedNotes = segments.reduce(
195
246
  (count, segment) => count + Math.max(1, Math.ceil(segment.endBeat - segment.startBeat)),
196
247
  0
@@ -242,7 +293,7 @@ function generateBassLine(opts) {
242
293
  continue;
243
294
  }
244
295
  out.push({
245
- pitch: note.pitch,
296
+ pitch: clampToMidi(note.pitch, "generated bass pitch"),
246
297
  startBeat: note.startBeat,
247
298
  durationBeat,
248
299
  velocity: note.velocity
@@ -263,6 +314,7 @@ var HOLD_OFF_BEAT_PROB = 0.2;
263
314
  var REINFORCE_PROB = 0.35;
264
315
  var DEFAULT_MELODY_VELOCITY = 96;
265
316
  var VELOCITY_DROP = 16;
317
+ var TIE_BREAK_JITTER = 1e-6;
266
318
  function isWholeBeat(beat) {
267
319
  return Math.abs(beat - Math.round(beat)) < EPS2;
268
320
  }
@@ -365,26 +417,40 @@ function heldNoteBoundaries(melody, startBeat, endBeat, chordChangeBeats) {
365
417
  }
366
418
  return [...boundaries].sort((a, b) => a - b);
367
419
  }
368
- function heldPitchSafety(pitch, startBeat, endBeat, prevPitch, melody, opts, profile, register, low, high, ts) {
369
- let worst = 0 /* Safe */;
370
- const boundaries = heldNoteBoundaries(melody, startBeat, endBeat, opts.chordChangeBeats ?? []);
371
- for (const boundary of boundaries) {
420
+ function boundaryContexts(melody, harmony, startBeat, endBeat, ts) {
421
+ const boundaries = heldNoteBoundaries(melody, startBeat, endBeat, harmony.chordChangeBeats);
422
+ return boundaries.map((boundary) => {
372
423
  const melodyVoice = melodySnapshotAt(melody, boundary);
424
+ return {
425
+ melodyVoice,
426
+ otherVoices: melodyVoice === void 0 ? [] : [melodyVoice],
427
+ chord: harmony.chordAt(boundary),
428
+ strongBeat: isStrongBeat(boundary, ts),
429
+ atCounterOnset: Math.abs(boundary - startBeat) < EPS2
430
+ };
431
+ });
432
+ }
433
+ function heldPitchSafety(pitch, contexts, prevPitch, key, profile, register, low, high) {
434
+ let worst = 0 /* Safe */;
435
+ for (const context of contexts) {
436
+ const melodyVoice = context.melodyVoice;
373
437
  if (melodyVoice !== void 0 && (register === "below" && pitch >= melodyVoice.pitch || register === "above" && pitch <= melodyVoice.pitch)) {
374
438
  return null;
375
439
  }
376
- const atCounterOnset = Math.abs(boundary - startBeat) < EPS2;
377
- const result = evaluateSafety({
378
- profile,
379
- candidatePitch: pitch,
380
- prevPitch: atCounterOnset ? prevPitch : pitch,
381
- chord: opts.chordAt(boundary),
382
- key: opts.key,
383
- otherVoices: melodyVoice ? [melodyVoice] : [],
384
- strongBeat: isStrongBeat(boundary, ts),
385
- vocalLow: low,
386
- vocalHigh: high
387
- });
440
+ const result = evaluateSafety(
441
+ {
442
+ profile,
443
+ candidatePitch: pitch,
444
+ prevPitch: context.atCounterOnset ? prevPitch : pitch,
445
+ chord: context.chord,
446
+ key,
447
+ otherVoices: context.otherVoices,
448
+ strongBeat: context.strongBeat,
449
+ vocalLow: low,
450
+ vocalHigh: high
451
+ },
452
+ { suggestions: false }
453
+ );
388
454
  worst = Math.max(worst, result.safety);
389
455
  if (worst === 2 /* Dissonant */) {
390
456
  return worst;
@@ -393,35 +459,48 @@ function heldPitchSafety(pitch, startBeat, endBeat, prevPitch, melody, opts, pro
393
459
  return worst;
394
460
  }
395
461
  function generateCounterMelody(opts) {
396
- const melody = createNoteEventIndex(opts.melody);
397
- if (melody.notes.length === 0) {
398
- return [];
462
+ const timeline = opts.timeline;
463
+ const chordAt = timeline === void 0 ? opts.chordAt : (beat) => timeline.at(beat);
464
+ if (chordAt === void 0) {
465
+ throw new TypeError("countermelody needs a timeline or a chordAt callback");
399
466
  }
467
+ const chordChangeBeats = timeline === void 0 ? opts.chordChangeBeats ?? [] : timeline.segments.flatMap((segment) => [segment.startBeat, segment.endBeat]);
468
+ const register = opts.register ?? "below";
469
+ assertNoteEvents(opts.melody, "countermelody melody", { allowNonPositiveDuration: true });
470
+ const melody = createNoteEventIndex(soundingNotesOnly(opts.melody), {
471
+ tieBreak: register === "below" ? "highest" : "lowest"
472
+ });
400
473
  const ts = opts.ts ?? DEFAULT_TS2;
401
474
  assertTimeSignature(ts);
402
- const register = opts.register ?? "below";
403
- const rhythm = opts.rhythm ?? "complement";
404
- const profile = opts.profile ?? "pop";
475
+ const rhythm = assertOneOf(opts.rhythm ?? "complement", ["complement", "follow"], "rhythm");
476
+ const profile = assertOneOf(opts.profile ?? "pop", ["strict", "pop"], "safety profile");
405
477
  const rng = createRng(opts.seed ?? 0);
478
+ if (melody.notes.length === 0) {
479
+ return [];
480
+ }
406
481
  const spanStart = melody.notes[0]?.note.startBeat ?? 0;
407
482
  const spanEnd = melody.notes.reduce((end, n) => Math.max(end, n.endBeat), spanStart);
408
483
  assertGenerationBudget(Math.ceil((spanEnd - spanStart) / GRID_STEP), "countermelody grid");
409
484
  const meanPitch = melody.notes.reduce((sum, n) => sum + n.note.pitch, 0) / melody.notes.length;
410
485
  const defaultCenter = register === "below" ? meanPitch - REGISTER_OFFSET : meanPitch + REGISTER_OFFSET;
411
- const low = opts.pitchLow ?? Math.round(defaultCenter) - DEFAULT_HALF_RANGE;
412
- const high = opts.pitchHigh ?? Math.round(defaultCenter) + DEFAULT_HALF_RANGE;
413
- assertInteger(low, "countermelody pitchLow");
414
- 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
+ );
415
494
  if (low > high) {
416
- throw new RangeError(
495
+ throw new InvalidInputError(
417
496
  `countermelody pitchLow must not exceed pitchHigh; received ${low} > ${high}`
418
497
  );
419
498
  }
420
499
  assertGenerationBudget(high - low + 1, "countermelody pitch candidates");
421
- assertGenerationBudget(opts.chordChangeBeats?.length ?? 0, "chord change beats");
422
- for (let index = 0; index < (opts.chordChangeBeats?.length ?? 0); index += 1) {
500
+ assertGenerationBudget(chordChangeBeats.length, "chord change beats");
501
+ for (let index = 0; index < chordChangeBeats.length; index += 1) {
423
502
  assertRange(
424
- opts.chordChangeBeats?.[index] ?? Number.NaN,
503
+ chordChangeBeats[index] ?? Number.NaN,
425
504
  0,
426
505
  Number.MAX_SAFE_INTEGER,
427
506
  `chordChangeBeats[${index}]`
@@ -429,7 +508,9 @@ function generateCounterMelody(opts) {
429
508
  }
430
509
  const center = (low + high) / 2;
431
510
  const onsets = rhythm === "follow" ? followOnsets(melody) : complementOnsets(melody, ts, spanStart, spanEnd, rng);
432
- assertGenerationBudget(onsets.length * (high - low + 1), "countermelody search");
511
+ const boundaryEstimate = onsets.length + Math.ceil((spanEnd - spanStart) / GRID_STEP) + melody.notes.length + chordChangeBeats.length;
512
+ assertGenerationBudget(boundaryEstimate * (high - low + 1), "countermelody search");
513
+ const jitter = Array.from({ length: high - low + 1 }, () => rng.next() * TIE_BREAK_JITTER);
433
514
  const out = [];
434
515
  let prevPitch;
435
516
  for (let onsetIndex = 0; onsetIndex < onsets.length; onsetIndex += 1) {
@@ -443,7 +524,8 @@ function generateCounterMelody(opts) {
443
524
  const melPitch = melNote?.pitch;
444
525
  const melodyVoice = melodySnapshotAt(melody, beat);
445
526
  const melPrev = melodyVoice?.prevPitch;
446
- const chord = opts.chordAt(beat);
527
+ const contexts = boundaryContexts(melody, { chordAt, chordChangeBeats }, beat, endBeat, ts);
528
+ const chord = contexts[0]?.chord ?? chordAt(beat);
447
529
  const otherVoices = melodyVoice !== void 0 ? [melodyVoice] : [];
448
530
  const candidates = enumerateSafePitches(
449
531
  {
@@ -465,16 +547,13 @@ function generateCounterMelody(opts) {
465
547
  for (const candidate of candidates) {
466
548
  const worstSafety = heldPitchSafety(
467
549
  candidate,
468
- beat,
469
- endBeat,
550
+ contexts,
470
551
  prevPitch,
471
- melody,
472
- opts,
552
+ opts.key,
473
553
  profile,
474
554
  register,
475
555
  low,
476
- high,
477
- ts
556
+ high
478
557
  );
479
558
  if (worstSafety === null || worstSafety === 2 /* Dissonant */) {
480
559
  continue;
@@ -486,7 +565,7 @@ function generateCounterMelody(opts) {
486
565
  continue;
487
566
  }
488
567
  }
489
- const score = scoreCandidate(candidate, melPitch, melPrev, prevPitch, chord, center);
568
+ const score = scoreCandidate(candidate, melPitch, melPrev, prevPitch, chord, center) + (jitter[candidate - low] ?? 0);
490
569
  const tie = Math.abs(score - bestScore) <= EPS2 && candidate < (bestPitch ?? Number.POSITIVE_INFINITY);
491
570
  if (worstSafety < bestWorstSafety || worstSafety === bestWorstSafety && (score > bestScore + EPS2 || tie)) {
492
571
  bestWorstSafety = worstSafety;
@@ -503,7 +582,12 @@ function generateCounterMelody(opts) {
503
582
  1,
504
583
  Math.min(127, Math.round((melNote?.velocity ?? DEFAULT_MELODY_VELOCITY) - VELOCITY_DROP))
505
584
  );
506
- 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
+ });
507
591
  prevPitch = pitch;
508
592
  }
509
593
  return out;
@@ -528,6 +612,27 @@ var GM = {
528
612
  };
529
613
  var EIGHTH = 0.5;
530
614
  var SIXTEENTH = 0.25;
615
+ var DRUM_FEELS = ["straight", "swing", "shuffle"];
616
+ var GROOVE_STYLES = [
617
+ "standard",
618
+ "funk",
619
+ "shuffle",
620
+ "bossa",
621
+ "trap",
622
+ "halftime",
623
+ "breakbeat",
624
+ "house",
625
+ "synthpop"
626
+ ];
627
+ var DRUM_ROLES = ["full", "ambient", "minimal", "fxOnly"];
628
+ var PUBLIC_SECTIONS = [
629
+ "intro",
630
+ "verse",
631
+ "prechorus",
632
+ "chorus",
633
+ "bridge",
634
+ "outro"
635
+ ];
531
636
  var STYLE_MAP = {
532
637
  // Standard pop pulse: kick on beats 1 and 3, backbeat snare on 2 and 4.
533
638
  standard: { style: "standard", feel: "straight", ghostBoost: false, snareBeat3: false },
@@ -727,7 +832,7 @@ function getGhostDensity(mood, section, backingDensity, bpm) {
727
832
  }
728
833
  return levelToProbability(level);
729
834
  }
730
- function getGhostVelocity(section, beatPosition, isAfterSnare) {
835
+ function getGhostVelocity(section, beatPosition) {
731
836
  let base = 0.4;
732
837
  const even = beatPosition % 2 === 0 ? 0.05 : 0;
733
838
  switch (section) {
@@ -748,22 +853,16 @@ function getGhostVelocity(section, beatPosition, isAfterSnare) {
748
853
  base = 0.38;
749
854
  break;
750
855
  }
751
- if (isAfterSnare) {
752
- base += 0.1;
753
- }
754
856
  return Math.max(0.25, Math.min(0.65, base));
755
857
  }
756
858
  function getGhostProbabilityAtPosition(beat, sixteenthInBeat) {
757
- const nearSnare = beat === 0 && sixteenthInBeat === 3 || beat === 1 && sixteenthInBeat === 1 || beat === 2 && sixteenthInBeat === 3 || beat === 3 && sixteenthInBeat === 1;
758
- return nearSnare ? 0.6 : 0.25;
859
+ const leadsIntoBackbeat = (beat === 0 || beat === 2) && sixteenthInBeat === 3;
860
+ return leadsIntoBackbeat ? 0.6 : 0.25;
759
861
  }
760
- function selectGhostPositions(mood, rng) {
862
+ function selectGhostPositions(mood) {
761
863
  if (mood === 2 /* Energetic */) {
762
864
  return ["e", "a"];
763
865
  }
764
- if (mood === 0 /* Calm */) {
765
- return rng.prob(0.5) ? ["e"] : [];
766
- }
767
866
  return ["e"];
768
867
  }
769
868
 
@@ -779,6 +878,9 @@ function roleHiHatInstrument(role, useRide) {
779
878
  if (role === "ambient") {
780
879
  return GM.RIDE;
781
880
  }
881
+ if (role === "minimal") {
882
+ return GM.FHH;
883
+ }
782
884
  return useRide ? GM.RIDE : GM.CHH;
783
885
  }
784
886
  function sparser(level) {
@@ -1038,10 +1140,7 @@ function quantizeSwing(startBeat, swing, resolution) {
1038
1140
  }
1039
1141
 
1040
1142
  // src/generate/drums/beat.ts
1041
- function swing16(tick, feel, swingAmount) {
1042
- return quantizeSwing(tick, effectiveSwing(feel, swingAmount), "sixteenth");
1043
- }
1044
- function hatSwingFactor(style) {
1143
+ function styleSwingFactor(style) {
1045
1144
  if (style === "trap") {
1046
1145
  return 0;
1047
1146
  }
@@ -1050,6 +1149,12 @@ function hatSwingFactor(style) {
1050
1149
  }
1051
1150
  return 0.5;
1052
1151
  }
1152
+ function sectionSwing(sec, swingAmount) {
1153
+ return effectiveSwing(sec.feel, swingAmount) * styleSwingFactor(sec.style);
1154
+ }
1155
+ function swing16(tick, sec, swingAmount) {
1156
+ return quantizeSwing(tick, sectionSwing(sec, swingAmount), "sixteenth");
1157
+ }
1053
1158
  function generateKickForBeat(ctx, sec, kick) {
1054
1159
  if (ctx.inPrechorusLift) {
1055
1160
  return;
@@ -1060,7 +1165,7 @@ function generateKickForBeat(ctx, sec, kick) {
1060
1165
  ctx.track.add(GM.BD, ctx.beatTick, EIGHTH, ctx.velocity);
1061
1166
  }
1062
1167
  if (and) {
1063
- const andTick = swing16(ctx.beatTick + EIGHTH, sec.feel, ctx.swingAmount);
1168
+ const andTick = swing16(ctx.beatTick + EIGHTH, sec, ctx.swingAmount);
1064
1169
  ctx.track.add(GM.BD, andTick, EIGHTH, ctx.velocity * 0.85);
1065
1170
  }
1066
1171
  }
@@ -1077,12 +1182,17 @@ function generateSnareForBeat(ctx, sec, isIntroFirst) {
1077
1182
  }
1078
1183
  const backbeatVel = Math.min(127, ctx.velocity + 16);
1079
1184
  const promoteSparseChorus = sec.style === "sparse" && ctx.section === "chorus" && sec.role === "full";
1185
+ if (sec.role === "fxOnly") {
1186
+ return;
1187
+ }
1188
+ if (sec.role === "minimal") {
1189
+ ctx.track.add(GM.SIDESTICK, ctx.beatTick, EIGHTH, ctx.velocity * 0.65);
1190
+ return;
1191
+ }
1080
1192
  if (promoteSparseChorus) {
1081
1193
  ctx.track.add(GM.SD, ctx.beatTick, EIGHTH, backbeatVel);
1082
1194
  } else if (sec.style === "sparse" || sec.role === "ambient") {
1083
- if (sec.role !== "fxOnly" && sec.role !== "minimal") {
1084
- ctx.track.add(GM.SIDESTICK, ctx.beatTick, EIGHTH, ctx.velocity * 0.8);
1085
- }
1195
+ ctx.track.add(GM.SIDESTICK, ctx.beatTick, EIGHTH, ctx.velocity * 0.8);
1086
1196
  } else {
1087
1197
  ctx.track.add(GM.SD, ctx.beatTick, EIGHTH, backbeatVel);
1088
1198
  }
@@ -1091,7 +1201,7 @@ function generateGhostNotesForBeat(ctx, sec) {
1091
1201
  if (ctx.beat !== 0 && ctx.beat !== 2) {
1092
1202
  return;
1093
1203
  }
1094
- const positions = selectGhostPositions(sec.ghostMood, ctx.rng);
1204
+ const positions = selectGhostPositions(sec.ghostMood);
1095
1205
  let ghostProb = getGhostDensity(sec.ghostMood, ctx.section, sec.backingDensity, ctx.bpm);
1096
1206
  if (sec.ghostBoost) {
1097
1207
  ghostProb = Math.min(1, ghostProb * 1.4);
@@ -1103,13 +1213,13 @@ function generateGhostNotesForBeat(ctx, sec) {
1103
1213
  continue;
1104
1214
  }
1105
1215
  const variation = ctx.rng.float(0.85, 1.15);
1106
- const ghostBase = getGhostVelocity(ctx.section, ctx.beat / 2, false);
1216
+ const ghostBase = getGhostVelocity(ctx.section, ctx.beat / 2);
1107
1217
  let ghostVel = ctx.velocity * ghostBase * variation;
1108
1218
  if (pos === "a") {
1109
1219
  ghostVel *= 0.9;
1110
1220
  }
1111
1221
  const offset = pos === "e" ? SIXTEENTH : 3 * SIXTEENTH;
1112
- const tick = swing16(ctx.beatTick + offset, sec.feel, ctx.swingAmount);
1222
+ const tick = swing16(ctx.beatTick + offset, sec, ctx.swingAmount);
1113
1223
  ctx.track.add(GM.SD, tick, SIXTEENTH, ghostVel);
1114
1224
  }
1115
1225
  }
@@ -1126,16 +1236,18 @@ function generatePreChorusBuildup(ctx, sec, isSectionLastBar) {
1126
1236
  const progress = (barInLift * 4 + ctx.beat) / (barsInLift * 4);
1127
1237
  const buildupVel = ctx.velocity * (0.5 + 0.5 * progress);
1128
1238
  ctx.track.add(GM.SD, ctx.beatTick, EIGHTH, buildupVel);
1129
- 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
+ );
1130
1245
  if (isSectionLastBar && ctx.beat === 3) {
1131
1246
  ctx.track.add(GM.CRASH, ctx.beatTick + EIGHTH + SIXTEENTH, SIXTEENTH, ctx.velocity * 1.1);
1132
1247
  }
1133
1248
  }
1134
1249
  function generateHiHatForBeat(ctx, sec) {
1135
1250
  if (!shouldPlayHiHat(sec.role)) {
1136
- if (sec.useFootHh && (ctx.beat === 0 || ctx.beat === 2)) {
1137
- ctx.track.add(GM.FHH, ctx.beatTick, EIGHTH, footHiHatVelocity(ctx.rng));
1138
- }
1139
1251
  return;
1140
1252
  }
1141
1253
  const crossStick = sec.useRide && shouldUseBridgeCrossStick(ctx.section, ctx.beat);
@@ -1143,11 +1255,12 @@ function generateHiHatForBeat(ctx, sec) {
1143
1255
  const hhType = sectionHiHatType(ctx.section, sec.role);
1144
1256
  const typeMult = hiHatTypeVelocityMultiplier(hhType);
1145
1257
  const isDynamicOpen = ctx.barHasOpenHh && ctx.beat === ctx.openHhBeat;
1258
+ const allowsOpenHiHat = sec.role !== "ambient" && sec.role !== "minimal";
1146
1259
  const dm = sec.densityMult;
1147
1260
  if (sec.hhLevel === "quarter") {
1148
1261
  const introRest = ctx.section === "intro" && ctx.beat !== 0;
1149
1262
  if (!introRest) {
1150
- if (isDynamicOpen) {
1263
+ if (isDynamicOpen && allowsOpenHiHat) {
1151
1264
  ctx.track.add(
1152
1265
  GM.OHH,
1153
1266
  ctx.beatTick,
@@ -1171,7 +1284,7 @@ function generateHiHatForBeat(ctx, sec) {
1171
1284
  for (let eighth = 0; eighth < 2; eighth += 1) {
1172
1285
  let hhTick = ctx.beatTick + eighth * EIGHTH;
1173
1286
  if (eighth === 1) {
1174
- hhTick = swing16(hhTick, sec.feel, ctx.swingAmount);
1287
+ hhTick = swing16(hhTick, sec, ctx.swingAmount);
1175
1288
  }
1176
1289
  if (ctx.section === "intro" && eighth === 1) {
1177
1290
  if (sec.useFootHh && ctx.beat % 2 === 0) {
@@ -1180,7 +1293,7 @@ function generateHiHatForBeat(ctx, sec) {
1180
1293
  continue;
1181
1294
  }
1182
1295
  const hhVel = Math.max(20, ctx.velocity * dm * typeMult * (eighth === 0 ? 0.9 : 0.65));
1183
- if (isDynamicOpen && eighth === 0) {
1296
+ if (isDynamicOpen && allowsOpenHiHat && eighth === 0) {
1184
1297
  ctx.track.add(GM.OHH, hhTick, EIGHTH, hhVel + OHH_VEL_BOOST);
1185
1298
  continue;
1186
1299
  }
@@ -1191,7 +1304,7 @@ function generateHiHatForBeat(ctx, sec) {
1191
1304
  } else if (eighth === 0) {
1192
1305
  useOpen = shouldAddOpenHHAccent(ctx.section, ctx.beat, ctx.bar, ctx.rng);
1193
1306
  }
1194
- if (useOpen) {
1307
+ if (useOpen && allowsOpenHiHat) {
1195
1308
  ctx.track.add(hiHatNote("open"), hhTick, EIGHTH, Math.max(20, hhVel * 1.1));
1196
1309
  } else {
1197
1310
  ctx.track.add(hhInstrument, hhTick, EIGHTH / 2, hhVel);
@@ -1201,17 +1314,16 @@ function generateHiHatForBeat(ctx, sec) {
1201
1314
  }
1202
1315
  for (let sixteenth = 0; sixteenth < 4; sixteenth += 1) {
1203
1316
  let hhTick = ctx.beatTick + sixteenth * SIXTEENTH;
1204
- if (sixteenth === 1 || sixteenth === 3) {
1205
- const swung = effectiveSwing(sec.feel, ctx.swingAmount) * hatSwingFactor(sec.style);
1206
- hhTick = quantizeSwing(hhTick, swung, "sixteenth");
1317
+ if (sixteenth !== 0) {
1318
+ hhTick = swing16(hhTick, sec, ctx.swingAmount);
1207
1319
  }
1208
1320
  const metricVel = hiHatVelocityMultiplier(sixteenth, ctx.rng);
1209
1321
  const hhVel = Math.max(20, ctx.velocity * dm * typeMult * metricVel);
1210
- if (isDynamicOpen && sixteenth === 0) {
1322
+ if (isDynamicOpen && allowsOpenHiHat && sixteenth === 0) {
1211
1323
  ctx.track.add(GM.OHH, hhTick, SIXTEENTH, hhVel + OHH_VEL_BOOST);
1212
1324
  continue;
1213
1325
  }
1214
- if (ctx.beat === 3 && sixteenth === 3) {
1326
+ if (allowsOpenHiHat && ctx.beat === 3 && sixteenth === 3) {
1215
1327
  const openProb = Math.max(0.1, Math.min(0.4, 30 / ctx.bpm));
1216
1328
  if (ctx.rng.prob(openProb)) {
1217
1329
  ctx.track.add(GM.OHH, hhTick, SIXTEENTH, Math.max(20, hhVel * 1.2));
@@ -1266,7 +1378,7 @@ function getFillStartBeat(energy) {
1266
1378
  return 2;
1267
1379
  case "high":
1268
1380
  case "peak":
1269
- return 0;
1381
+ return 2;
1270
1382
  }
1271
1383
  }
1272
1384
  function selectFillType(from, to, style, nextEnergy, rng) {
@@ -1531,24 +1643,62 @@ function generateFill(track, beatTick, beat, fillType, velocity) {
1531
1643
  }
1532
1644
 
1533
1645
  // src/generate/drums/hit.ts
1646
+ var DRUM_NOTES = Object.freeze({
1647
+ kick: GM.BD,
1648
+ snare: GM.SD,
1649
+ sideStick: GM.SIDESTICK,
1650
+ handClap: GM.HANDCLAP,
1651
+ closedHiHat: GM.CHH,
1652
+ pedalHiHat: GM.FHH,
1653
+ openHiHat: GM.OHH,
1654
+ crash: GM.CRASH,
1655
+ ride: GM.RIDE,
1656
+ tambourine: GM.TAMBOURINE,
1657
+ highTom: GM.TOM_H,
1658
+ midTom: GM.TOM_M,
1659
+ lowTom: GM.TOM_L,
1660
+ shaker: GM.SHAKER
1661
+ });
1662
+ var VOICE_BY_NOTE = new Map(
1663
+ Object.entries(DRUM_NOTES).map(([voice, note]) => [note, voice])
1664
+ );
1665
+ function drumVoiceOf(pitch) {
1666
+ return VOICE_BY_NOTE.get(pitch);
1667
+ }
1668
+ var ONSET_KEY_SCALE = 128;
1534
1669
  var HitList = class {
1535
1670
  hits = [];
1671
+ /** Onset keys already occupied, as `pitch:quantizedBeat`. */
1672
+ #onsets = /* @__PURE__ */ new Set();
1536
1673
  /** Append a hit, clamping velocity and dropping non-positive positions. */
1537
1674
  add(pitch, startBeat, durationBeat, velocity) {
1675
+ const beat = Math.max(0, startBeat);
1538
1676
  this.hits.push({
1539
1677
  pitch,
1540
- startBeat: Math.max(0, startBeat),
1678
+ startBeat: beat,
1541
1679
  durationBeat,
1542
1680
  velocity: clampVel(velocity)
1543
1681
  });
1682
+ this.#onsets.add(onsetKey(pitch, beat));
1683
+ }
1684
+ /** Whether a hit of this pitch already starts exactly at `startBeat`. */
1685
+ hasOnset(pitch, startBeat) {
1686
+ return this.#onsets.has(onsetKey(pitch, startBeat));
1544
1687
  }
1545
1688
  /** True when a crash already sits within a 16th of `startBeat`. */
1546
1689
  hasCrashNear(startBeat) {
1547
- return this.hits.some(
1548
- (h) => h.pitch === GM.CRASH && h.startBeat >= startBeat && h.startBeat < startBeat + 0.25
1549
- );
1690
+ const steps = Math.round(0.25 * ONSET_KEY_SCALE);
1691
+ for (let step = 0; step < steps; step += 1) {
1692
+ if (this.#onsets.has(onsetKey(GM.CRASH, startBeat + step / ONSET_KEY_SCALE))) {
1693
+ return true;
1694
+ }
1695
+ }
1696
+ return false;
1550
1697
  }
1551
1698
  };
1699
+ function onsetKey(pitch, startBeat) {
1700
+ return `${pitch}:${Math.round(startBeat * ONSET_KEY_SCALE)}`;
1701
+ }
1552
1702
 
1553
1703
  // src/generate/drums/kick.ts
1554
1704
  var EMPTY = {
@@ -1687,12 +1837,12 @@ function getPercussionConfig(mood, section) {
1687
1837
  shaker16th: act[1]
1688
1838
  };
1689
1839
  }
1690
- function generateAuxPercussionForBar(track, barStart, config, role, densityMult, rng, bpm) {
1840
+ function generateAuxPercussionForBar(track, barStart, config, role, densityMult, rng, bpm, sec, swingAmount, barBeats = 4) {
1691
1841
  if (role === "minimal") {
1692
1842
  return;
1693
1843
  }
1694
1844
  if (config.tambourine) {
1695
- for (let beat = 1; beat <= 3; beat += 2) {
1845
+ for (let beat = 1; beat < barBeats; beat += 2) {
1696
1846
  const raw = 70 * densityMult * rng.float(0.9, 1.1);
1697
1847
  track.add(GM.TAMBOURINE, barStart + beat, EIGHTH, Math.max(40, Math.min(90, raw)));
1698
1848
  }
@@ -1701,12 +1851,12 @@ function generateAuxPercussionForBar(track, barStart, config, role, densityMult,
1701
1851
  const use16th = config.shaker16th && bpm < HH_16TH_BPM_THRESHOLD;
1702
1852
  if (use16th) {
1703
1853
  const velCurve = [0.75, 0.45, 0.6, 0.45];
1704
- for (let beat = 0; beat < 4; beat += 1) {
1854
+ for (let beat = 0; beat < barBeats; beat += 1) {
1705
1855
  for (let sub = 0; sub < 4; sub += 1) {
1706
1856
  const raw = 80 * (velCurve[sub] ?? 0.5) * densityMult * rng.float(0.9, 1.1);
1707
1857
  track.add(
1708
1858
  GM.SHAKER,
1709
- barStart + beat + sub * SIXTEENTH,
1859
+ swing16(barStart + beat + sub * SIXTEENTH, sec, swingAmount),
1710
1860
  SIXTEENTH,
1711
1861
  Math.max(25, Math.min(85, raw))
1712
1862
  );
@@ -1714,12 +1864,12 @@ function generateAuxPercussionForBar(track, barStart, config, role, densityMult,
1714
1864
  }
1715
1865
  } else {
1716
1866
  const velCurve = [0.75, 0.55];
1717
- for (let beat = 0; beat < 4; beat += 1) {
1867
+ for (let beat = 0; beat < barBeats; beat += 1) {
1718
1868
  for (let sub = 0; sub < 2; sub += 1) {
1719
1869
  const raw = 80 * (velCurve[sub] ?? 0.6) * densityMult * rng.float(0.9, 1.1);
1720
1870
  track.add(
1721
1871
  GM.SHAKER,
1722
- barStart + beat + sub * EIGHTH,
1872
+ swing16(barStart + beat + sub * EIGHTH, sec, swingAmount),
1723
1873
  EIGHTH,
1724
1874
  Math.max(25, Math.min(85, raw))
1725
1875
  );
@@ -1728,7 +1878,7 @@ function generateAuxPercussionForBar(track, barStart, config, role, densityMult,
1728
1878
  }
1729
1879
  }
1730
1880
  if (config.handclap) {
1731
- for (let beat = 1; beat <= 3; beat += 2) {
1881
+ for (let beat = 1; beat < barBeats; beat += 2) {
1732
1882
  const raw = 85 * densityMult * rng.float(0.9, 1.1);
1733
1883
  track.add(GM.HANDCLAP, barStart + beat, EIGHTH, Math.max(50, Math.min(100, raw)));
1734
1884
  }
@@ -1736,25 +1886,34 @@ function generateAuxPercussionForBar(track, barStart, config, role, densityMult,
1736
1886
  }
1737
1887
 
1738
1888
  // src/generate/drums/index.ts
1889
+ var MAX_HITS_PER_BAR = 128;
1739
1890
  function generateDrums(opts) {
1740
1891
  assertPositiveInt(opts.bars, "drum bars");
1741
1892
  assertRange(opts.bpm, Number.MIN_VALUE, 1e3, "drum bpm");
1742
1893
  assertRange(opts.density, 0, 1, "drum density");
1743
- assertGenerationBudget(opts.bars * 128, "drum hits");
1894
+ assertGenerationBudget(opts.bars * MAX_HITS_PER_BAR, "drum hits");
1744
1895
  if (opts.euclideanKick) {
1745
1896
  const steps = opts.euclideanKick.steps ?? 16;
1746
1897
  assertInteger(steps, "euclidean steps", 1, 16);
1747
1898
  assertInteger(opts.euclideanKick.pulses, "euclidean pulses");
1748
1899
  assertInteger(opts.euclideanKick.rotation ?? 0, "euclidean rotation");
1749
1900
  }
1901
+ const publicStyle = assertOneOf(opts.style, GROOVE_STYLES, "drum style");
1902
+ const publicSection = assertOneOf(opts.section, PUBLIC_SECTIONS, "drum section");
1903
+ if (opts.nextSection !== void 0) {
1904
+ assertOneOf(opts.nextSection, PUBLIC_SECTIONS, "drum nextSection");
1905
+ }
1750
1906
  const track = new HitList();
1751
1907
  const rng = createRng(opts.seed ?? 0);
1752
- const mapping = mapStyle(opts.style);
1908
+ const mapping = mapStyle(publicStyle);
1753
1909
  const style = mapping.style;
1754
- const feel = opts.feel ?? mapping.feel;
1755
- const role = opts.role ?? "full";
1910
+ const feel = opts.feel === void 0 ? mapping.feel : assertOneOf(opts.feel, DRUM_FEELS, "drum feel");
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);
1756
1915
  const playMainVoices = role !== "fxOnly";
1757
- const section = mapSection(opts.section);
1916
+ const section = mapSection(publicSection);
1758
1917
  const backingDensity = mapDensity(opts.density);
1759
1918
  const swingAmount = feelSwingAmount(feel);
1760
1919
  let densityMult = sectionDensityMultiplier(section);
@@ -1788,7 +1947,7 @@ function generateDrums(opts) {
1788
1947
  const reuseSectionKick = (section === "b" || section === "chorus") && style !== "sparse";
1789
1948
  let sectionKick;
1790
1949
  for (let bar = 0; bar < opts.bars; bar += 1) {
1791
- const barStart = bar * 4;
1950
+ const barStart = bar * barBeats;
1792
1951
  const isLastBar = bar === opts.bars - 1;
1793
1952
  if (bar === 0 && section === "chorus") {
1794
1953
  track.add(GM.CRASH, barStart, 0.5, 100 * densityMult);
@@ -1806,9 +1965,9 @@ function generateDrums(opts) {
1806
1965
  } else if (!euclidKick) {
1807
1966
  kick = getKickPattern(section, style, bar, rng);
1808
1967
  }
1809
- const inLift = isInPreChorusLift(section, bar, opts.bars, section === "b");
1968
+ const inLift = isInPreChorusLift(section, bar, opts.bars, nextSection === "chorus");
1810
1969
  let currentFill = "snareRoll";
1811
- for (let beat = 0; beat < 4; beat += 1) {
1970
+ for (let beat = 0; beat < barBeats; beat += 1) {
1812
1971
  const beatTick = barStart + beat;
1813
1972
  const velocity = calculateVelocity(section, beat);
1814
1973
  if (opts.fills && isLastBar && !inLift && beat >= fillStartBeat) {
@@ -1845,14 +2004,14 @@ function generateDrums(opts) {
1845
2004
  generatePreChorusBuildup(ctx, sec, isLastBar);
1846
2005
  }
1847
2006
  if (euclidKick && !inLift) {
1848
- const stepLength = 4 / euclidSteps;
2007
+ const stepLength = barBeats / euclidSteps;
1849
2008
  for (let step = 0; step < euclidKick.length; step += 1) {
1850
2009
  const onset = step * stepLength;
1851
2010
  if (euclidKick[step] && Math.floor(onset) === beat) {
1852
2011
  const wholeBeat = Math.abs(onset - Math.round(onset)) < 1e-9;
1853
2012
  track.add(
1854
2013
  GM.BD,
1855
- barStart + onset,
2014
+ swing16(barStart + onset, sec, swingAmount),
1856
2015
  Math.min(0.5, stepLength),
1857
2016
  velocity * (wholeBeat ? 1 : 0.85)
1858
2017
  );
@@ -1869,10 +2028,9 @@ function generateDrums(opts) {
1869
2028
  generateHiHatForBeat(ctx, sec);
1870
2029
  }
1871
2030
  if (sec.useFootHh && shouldPlayHiHat(role)) {
1872
- for (let fhhBeat = 0; fhhBeat < 4; fhhBeat += 2) {
2031
+ for (let fhhBeat = 0; fhhBeat < barBeats; fhhBeat += 2) {
1873
2032
  const fhhTick = barStart + fhhBeat;
1874
- const occupied = track.hits.some((h) => h.pitch === GM.FHH && h.startBeat === fhhTick);
1875
- if (!occupied) {
2033
+ if (!track.hasOnset(GM.FHH, fhhTick)) {
1876
2034
  track.add(GM.FHH, fhhTick, 0.5, footHiHatVelocity(rng));
1877
2035
  }
1878
2036
  }
@@ -1893,14 +2051,19 @@ function generateDrums(opts) {
1893
2051
  role,
1894
2052
  densityMult,
1895
2053
  rng,
1896
- opts.bpm
2054
+ opts.bpm,
2055
+ sec,
2056
+ swingAmount,
2057
+ barBeats
1897
2058
  );
1898
2059
  }
1899
- return track.hits;
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);
1900
2062
  }
1901
2063
 
1902
2064
  // src/generate/groove/index.ts
1903
2065
  var DEFAULT_TS3 = { numerator: 4, denominator: 4 };
2066
+ var DEFAULT_GROOVE_SUBDIVISION = 4;
1904
2067
  var DEFAULT_TIMING = 0.02;
1905
2068
  var DEFAULT_VELOCITY_JITTER = 8;
1906
2069
  var DEFAULT_ACCENT = 12;
@@ -1911,7 +2074,8 @@ var MAX_VELOCITY = 127;
1911
2074
  function humanize(events, opts = {}) {
1912
2075
  const ts = opts.ts ?? DEFAULT_TS3;
1913
2076
  assertTimeSignature(ts);
1914
- assertNoteEvents(events, "humanize events");
2077
+ assertNoteEvents(events, "humanize events", { allowNonPositiveDuration: true });
2078
+ const sounding = dropSilentNotes(events);
1915
2079
  const timing = opts.timing ?? DEFAULT_TIMING;
1916
2080
  const velocityJitter = opts.velocity ?? DEFAULT_VELOCITY_JITTER;
1917
2081
  const accent = opts.accent ?? DEFAULT_ACCENT;
@@ -1921,7 +2085,7 @@ function humanize(events, opts = {}) {
1921
2085
  assertRange(accent, 0, 127, "humanize accent");
1922
2086
  assertRange(baseVelocity, 0, 127, "humanize base velocity");
1923
2087
  const rng = createRng(opts.seed ?? 0);
1924
- return events.map((event) => {
2088
+ return sounding.map((event) => {
1925
2089
  const timingOffset = rng.float(-timing, timing);
1926
2090
  const startBeat = Math.max(0, event.startBeat + timingOffset);
1927
2091
  const weight = metricWeight(event.startBeat, ts);
@@ -1951,18 +2115,19 @@ function quantizeToGrid(beat, barBeats, subdivision, slotsPerBar) {
1951
2115
  slotIndex
1952
2116
  };
1953
2117
  }
1954
- function extractGrooveTemplate(events, ts, subdivision) {
2118
+ function extractGrooveTemplate(events, ts, subdivision = DEFAULT_GROOVE_SUBDIVISION) {
1955
2119
  assertTimeSignature(ts);
1956
- assertNoteEvents(events, "groove source events");
2120
+ assertNoteEvents(events, "groove source events", { allowNonPositiveDuration: true });
1957
2121
  assertPositiveInt(subdivision, "groove subdivision");
1958
2122
  const barBeats = beatsPerBar(ts);
1959
- const slotsPerBar = Math.round(barBeats * subdivision);
2123
+ const slotsPerBar = Math.ceil(barBeats * subdivision);
2124
+ assertPositiveInt(slotsPerBar, "groove template slots");
1960
2125
  assertGenerationBudget(slotsPerBar, "groove template slots");
1961
2126
  const offsetSums = new Array(slotsPerBar).fill(0);
1962
2127
  const velocitySums = new Array(slotsPerBar).fill(0);
1963
2128
  const counts = new Array(slotsPerBar).fill(0);
1964
2129
  const velocityCounts = new Array(slotsPerBar).fill(0);
1965
- for (const event of events) {
2130
+ for (const event of dropSilentNotes(events)) {
1966
2131
  const { quantizedBeat, slotIndex } = quantizeToGrid(
1967
2132
  event.startBeat,
1968
2133
  barBeats,
@@ -1993,20 +2158,20 @@ function extractGrooveTemplate(events, ts, subdivision) {
1993
2158
  }
1994
2159
  function applyGrooveTemplate(events, template, ts) {
1995
2160
  assertTimeSignature(ts);
1996
- assertNoteEvents(events, "groove target events");
2161
+ assertNoteEvents(events, "groove target events", { allowNonPositiveDuration: true });
1997
2162
  assertPositiveInt(template.subdivision, "template subdivision");
1998
2163
  assertPositiveInt(template.slotsPerBar, "template slotsPerBar");
1999
2164
  assertGenerationBudget(template.slots.length, "template slots");
2000
2165
  if (template.slots.length !== template.slotsPerBar) {
2001
- throw new RangeError("template slots length must equal slotsPerBar");
2166
+ throw new InvalidInputError("template slots length must equal slotsPerBar");
2002
2167
  }
2003
2168
  if (template.ts && (template.ts.numerator !== ts.numerator || template.ts.denominator !== ts.denominator)) {
2004
- throw new Error(
2169
+ throw new InvalidInputError(
2005
2170
  `Groove template meter ${formatTimeSignature(template.ts)} does not match apply-time meter ${formatTimeSignature(ts)}`
2006
2171
  );
2007
2172
  }
2008
2173
  const barBeats = beatsPerBar(ts);
2009
- return events.map((event) => {
2174
+ return dropSilentNotes(events).map((event) => {
2010
2175
  const { quantizedBeat, slotIndex } = quantizeToGrid(
2011
2176
  event.startBeat,
2012
2177
  barBeats,
@@ -2014,7 +2179,7 @@ function applyGrooveTemplate(events, template, ts) {
2014
2179
  template.slotsPerBar
2015
2180
  );
2016
2181
  const slot = template.slots[slotIndex];
2017
- const startBeat = quantizedBeat + (slot?.timingOffset ?? 0);
2182
+ const startBeat = Math.max(0, quantizedBeat + (slot?.timingOffset ?? 0));
2018
2183
  const velocity = slot && slot.velocity !== null && slot.velocity !== void 0 ? Math.round(slot.velocity) : event.velocity;
2019
2184
  return {
2020
2185
  pitch: event.pitch,
@@ -2026,15 +2191,20 @@ function applyGrooveTemplate(events, template, ts) {
2026
2191
  }
2027
2192
 
2028
2193
  // src/generate/harmonize/index.ts
2029
- var FALLBACK = { rootPc: 0, quality: "maj", secondaryDominant: false, base: 0 };
2194
+ var FALLBACK = {
2195
+ rootPc: 0,
2196
+ quality: "maj",
2197
+ secondaryDominant: false,
2198
+ base: 0,
2199
+ pcs: chordPitchClasses(makeChord(0, "maj"))
2200
+ };
2201
+ var DEFAULT_HARMONIC_RHYTHM = 2;
2202
+ var DEFAULT_PLACEMENT = { transposeSearch: false, octaveSearch: false };
2030
2203
  var DEFAULT_METER = { numerator: 4, denominator: 4 };
2031
2204
  var COMFORT_LOW = 55;
2032
2205
  var COMFORT_HIGH = 79;
2033
2206
  var TESSITURA_WEIGHT = 1e-3;
2034
- var TIE_BREAK_JITTER = 1e-6;
2035
- function pitchClass2(pitch) {
2036
- return (Math.trunc(pitch) % 12 + 12) % 12;
2037
- }
2207
+ var TIE_BREAK_JITTER2 = 1e-6;
2038
2208
  function inferKey(melody) {
2039
2209
  let best = majorKey(0);
2040
2210
  let bestScore = Number.NEGATIVE_INFINITY;
@@ -2043,13 +2213,13 @@ function inferKey(melody) {
2043
2213
  candidates.push(majorKey(tonic), minorKey(tonic));
2044
2214
  }
2045
2215
  for (const key of candidates) {
2046
- const tonic = pitchClass2(key.rootPc);
2216
+ const tonic = pitchClassOf(key.rootPc);
2047
2217
  const isMinor = key.modeMask12 === NATURAL_MINOR_MASK;
2048
2218
  const leadingTone = (tonic + 11) % 12;
2049
2219
  let score = 0;
2050
2220
  for (const n of melody) {
2051
2221
  const w = Math.max(0.25, n.durationBeat);
2052
- const pc = pitchClass2(n.pitch);
2222
+ const pc = pitchClassOf(n.pitch);
2053
2223
  if (isScaleTone(n.pitch, key) || isMinor && pc === leadingTone) {
2054
2224
  score += w;
2055
2225
  }
@@ -2066,22 +2236,28 @@ function inferKey(melody) {
2066
2236
  }
2067
2237
  function buildCandidates(key, reharmonize) {
2068
2238
  const tones = scaleTonesInDegreeOrder(key);
2069
- const candidates = tones.map((rootPc, degree) => ({
2070
- rootPc,
2071
- quality: diatonicTriad(degree, key).quality,
2072
- degree,
2073
- secondaryDominant: false,
2074
- base: 0
2075
- }));
2239
+ const candidates = tones.map((rootPc, degree) => {
2240
+ const quality = diatonicTriad(degree, key).quality;
2241
+ return {
2242
+ rootPc,
2243
+ quality,
2244
+ degree,
2245
+ secondaryDominant: false,
2246
+ base: 0,
2247
+ pcs: chordPitchClasses(makeChord(rootPc, quality))
2248
+ };
2249
+ });
2076
2250
  if (reharmonize !== "diatonic") {
2077
2251
  for (const target of [1, 3, 4, 5]) {
2078
2252
  const targetRoot = tones[target] ?? 0;
2253
+ const rootPc = (targetRoot + 7) % 12;
2079
2254
  candidates.push({
2080
- rootPc: (targetRoot + 7) % 12,
2255
+ rootPc,
2081
2256
  quality: "dom7",
2082
2257
  secondaryDominant: true,
2083
2258
  targetDegree: target,
2084
- base: 1.5
2259
+ base: 1.5,
2260
+ pcs: chordPitchClasses(makeChord(rootPc, "dom7"))
2085
2261
  });
2086
2262
  }
2087
2263
  }
@@ -2095,7 +2271,8 @@ function buildCandidates(key, reharmonize) {
2095
2271
  rootPc: chord.rootPc,
2096
2272
  quality: chord.quality,
2097
2273
  secondaryDominant: false,
2098
- base: 1.25
2274
+ base: 1.25,
2275
+ pcs: chordPitchClasses(chord)
2099
2276
  });
2100
2277
  }
2101
2278
  }
@@ -2110,8 +2287,8 @@ function buildCandidates(key, reharmonize) {
2110
2287
  return true;
2111
2288
  });
2112
2289
  }
2113
- function emissionCost(seg, cand, melody, key) {
2114
- const pcs = chordPitchClasses(makeChord(cand.rootPc, cand.quality));
2290
+ function emissionCost(seg, cand, melody, key, ts) {
2291
+ const pcs = cand.pcs;
2115
2292
  let cost = cand.base;
2116
2293
  for (const idx of seg.noteIndices) {
2117
2294
  const note = melody[idx];
@@ -2124,10 +2301,10 @@ function emissionCost(seg, cand, melody, key) {
2124
2301
  continue;
2125
2302
  }
2126
2303
  const w = Math.max(0.25, overlap);
2127
- if (pcs.includes(pitchClass2(note.pitch))) {
2304
+ if (pcs.includes(pitchClassOf(note.pitch))) {
2128
2305
  continue;
2129
2306
  }
2130
- const strong = isStrongBeat(overlapStart, DEFAULT_METER);
2307
+ const strong = isStrongBeat(overlapStart, ts);
2131
2308
  cost += (strong ? 10 : 2) * w;
2132
2309
  if (!isScaleTone(note.pitch, key)) {
2133
2310
  cost += w;
@@ -2161,13 +2338,13 @@ function transitionCost(prev, cur, tonicPc) {
2161
2338
  cost += dist * 0.3;
2162
2339
  return cost;
2163
2340
  }
2164
- function harmonizeOnce(melody, key, candidates, segments, jitter) {
2165
- const tonicPc = pitchClass2(key.rootPc);
2341
+ function harmonizeOnce(melody, key, candidates, segments, jitter, ts) {
2342
+ const tonicPc = pitchClassOf(key.rootPc);
2166
2343
  const n = candidates.length;
2167
2344
  const candAt = (i) => candidates[i] ?? FALLBACK;
2168
2345
  const seg0 = segments[0];
2169
2346
  let dp = candidates.map(
2170
- (c, ci) => (seg0 ? emissionCost(seg0, c, melody, key) : 0) + (jitter[ci] ?? 0)
2347
+ (c, ci) => (seg0 ? emissionCost(seg0, c, melody, key, ts) : 0) + (jitter[ci] ?? 0)
2171
2348
  );
2172
2349
  const back = [];
2173
2350
  for (let s = 1; s < segments.length; s += 1) {
@@ -2187,7 +2364,7 @@ function harmonizeOnce(melody, key, candidates, segments, jitter) {
2187
2364
  bestPrev = p;
2188
2365
  }
2189
2366
  }
2190
- next[c] = best + emissionCost(seg, candAt(c), melody, key) + (jitter[c] ?? 0);
2367
+ next[c] = best + emissionCost(seg, candAt(c), melody, key, ts) + (jitter[c] ?? 0);
2191
2368
  ptr[c] = bestPrev;
2192
2369
  }
2193
2370
  dp = next;
@@ -2212,32 +2389,47 @@ function harmonizeOnce(melody, key, candidates, segments, jitter) {
2212
2389
  return { cost: bestCost, path };
2213
2390
  }
2214
2391
  function harmonizeMelody(opts) {
2215
- const noteIndex = createNoteEventIndex(opts.melody);
2216
- const key = opts.key === "infer" ? inferKey(opts.melody) : opts.key;
2217
- const candidates = buildCandidates(key, opts.reharmonize);
2392
+ assertNoteEvents(opts.melody, "harmonize melody", { allowNonPositiveDuration: true });
2393
+ const soundingMelody = soundingNotesOnly(opts.melody);
2394
+ const noteIndex = createNoteEventIndex(soundingMelody);
2395
+ const ts = opts.ts ?? DEFAULT_METER;
2396
+ assertTimeSignature(ts);
2397
+ const requestedKey = opts.key ?? "infer";
2398
+ const key = requestedKey === "infer" ? inferKey(soundingMelody) : requestedKey;
2399
+ const placement = opts.placement ?? DEFAULT_PLACEMENT;
2400
+ if (noteIndex.notes.length === 0) {
2401
+ return { transposeSemitones: 0, key, chords: [], melodyRoles: [] };
2402
+ }
2403
+ const candidates = buildCandidates(key, opts.reharmonize ?? "diatonic");
2218
2404
  const candAt = (i) => candidates[i] ?? FALLBACK;
2219
2405
  const rng = createRng(opts.seed ?? 0);
2220
- const jitter = candidates.map(() => rng.next() * TIE_BREAK_JITTER);
2221
- const melodyEnd = noteIndex.notes.reduce((m, n) => Math.max(m, n.endBeat), 0);
2222
- const requestedHr = assertRange(
2223
- opts.harmonicRhythm,
2406
+ const jitter = candidates.map(() => rng.next() * TIE_BREAK_JITTER2);
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);
2412
+ const hr = assertRange(
2413
+ opts.harmonicRhythm ?? DEFAULT_HARMONIC_RHYTHM,
2224
2414
  Number.MIN_VALUE,
2225
2415
  Number.MAX_SAFE_INTEGER,
2226
2416
  "harmonic rhythm"
2227
2417
  );
2228
- const hr = Math.max(0.25, requestedHr);
2229
- 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));
2230
2420
  assertGenerationBudget(segCount, "harmonic segments");
2231
- assertGenerationBudget(segCount * candidates.length * candidates.length, "harmonization search");
2232
2421
  const segments = Array.from({ length: segCount }, (_, s) => ({
2233
- startBeat: s * hr,
2234
- endBeat: (s + 1) * hr,
2422
+ startBeat: segmentStart + s * hr,
2423
+ endBeat: segmentStart + (s + 1) * hr,
2235
2424
  noteIndices: []
2236
2425
  }));
2237
2426
  let memberships = 0;
2238
2427
  for (const indexed of noteIndex.notes) {
2239
- const first = Math.max(0, Math.floor(indexed.note.startBeat / hr));
2240
- const lastExclusive = Math.min(segCount, Math.ceil(indexed.endBeat / hr - Number.EPSILON));
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
+ );
2241
2433
  memberships += Math.max(0, lastExclusive - first);
2242
2434
  assertGenerationBudget(memberships, "note-to-segment memberships");
2243
2435
  for (let segment = first; segment < lastExclusive; segment += 1) {
@@ -2245,31 +2437,35 @@ function harmonizeMelody(opts) {
2245
2437
  }
2246
2438
  }
2247
2439
  const transposes = [0];
2248
- if (opts.placement.transposeSearch) {
2440
+ if (placement.transposeSearch) {
2249
2441
  for (let s = -6; s <= 6; s += 1) {
2250
2442
  if (s !== 0) {
2251
2443
  transposes.push(s);
2252
2444
  }
2253
2445
  }
2254
2446
  }
2255
- if (opts.placement.octaveSearch) {
2447
+ if (placement.octaveSearch) {
2256
2448
  for (const o of [-12, 12]) {
2257
2449
  if (!transposes.includes(o)) {
2258
2450
  transposes.push(o);
2259
2451
  }
2260
2452
  }
2261
2453
  }
2454
+ assertGenerationBudget(
2455
+ transposes.length * segCount * candidates.length * candidates.length,
2456
+ "harmonization placement search"
2457
+ );
2262
2458
  let bestCost = Number.POSITIVE_INFINITY;
2263
2459
  let bestTs = 0;
2264
2460
  let bestPath = [];
2265
2461
  let bestMelody = opts.melody;
2266
- for (const ts of transposes) {
2267
- const shifted = opts.melody.map((n) => ({ ...n, pitch: n.pitch + ts }));
2268
- const { cost, path } = harmonizeOnce(shifted, key, candidates, segments, jitter);
2462
+ for (const shift of transposes) {
2463
+ const shifted = opts.melody.map((n) => ({ ...n, pitch: n.pitch + shift }));
2464
+ const { cost, path } = harmonizeOnce(shifted, key, candidates, segments, jitter, ts);
2269
2465
  const total = cost + tessituraCost(shifted);
2270
2466
  if (total < bestCost) {
2271
2467
  bestCost = total;
2272
- bestTs = ts;
2468
+ bestTs = shift;
2273
2469
  bestPath = path;
2274
2470
  bestMelody = shifted;
2275
2471
  }
@@ -2279,7 +2475,7 @@ function harmonizeMelody(opts) {
2279
2475
  const chord = {
2280
2476
  rootPc: cand.rootPc,
2281
2477
  quality: cand.quality,
2282
- startBeat: segments[s]?.startBeat ?? s * hr
2478
+ startBeat: segments[s]?.startBeat ?? segmentStart + s * hr
2283
2479
  };
2284
2480
  if (cand.degree !== void 0) {
2285
2481
  chord.degree = cand.degree;
@@ -2299,10 +2495,13 @@ function harmonizeMelody(opts) {
2299
2495
  }
2300
2496
 
2301
2497
  // src/generate/motif/index.ts
2302
- function pitchClass3(pitch) {
2303
- return (Math.trunc(pitch) % 12 + 12) % 12;
2304
- }
2305
2498
  var MIN_TILE_SPAN = 1 / 256;
2499
+ var DEFAULT_TS4 = { numerator: 4, denominator: 4 };
2500
+ var EPS3 = 1e-9;
2501
+ function isDownbeat(startBeat, barBeats) {
2502
+ const bar = startBeat / barBeats;
2503
+ return Math.abs(bar - Math.round(bar)) < EPS3;
2504
+ }
2306
2505
  function upScaleTone(pitch, key) {
2307
2506
  for (let d = 1; d <= 12; d += 1) {
2308
2507
  if (isScaleTone(pitch + d, key)) {
@@ -2330,10 +2529,10 @@ function stepDiatonic(pitch, degrees, key) {
2330
2529
  function nearestChordTone(pitch, chord) {
2331
2530
  const pcs = chordPitchClasses(chord);
2332
2531
  for (let d = 0; d <= 12; d += 1) {
2333
- if (pcs.includes(pitchClass3(pitch - d))) {
2532
+ if (pcs.includes(pitchClassOf(pitch - d))) {
2334
2533
  return pitch - d;
2335
2534
  }
2336
- if (pcs.includes(pitchClass3(pitch + d))) {
2535
+ if (pcs.includes(pitchClassOf(pitch + d))) {
2337
2536
  return pitch + d;
2338
2537
  }
2339
2538
  }
@@ -2387,14 +2586,15 @@ function contourOffsets(contour, count) {
2387
2586
  function generateMotif(opts) {
2388
2587
  const contour = opts.contour ?? "arch";
2389
2588
  const bars = assertPositiveInt(opts.bars, "motif bars");
2390
- const totalBeats = bars * 4;
2589
+ const barBeats = beatsPerBar(opts.ts ?? DEFAULT_TS4);
2590
+ const totalBeats = bars * barBeats;
2391
2591
  const noteCount = Math.max(3, bars * 2);
2392
2592
  assertGenerationBudget(noteCount, "motif notes");
2393
2593
  const beatsPerNote = totalBeats / noteCount;
2394
2594
  const rng = createRng(opts.seed ?? 0);
2395
2595
  const requestedJitter = assertFiniteNumber(opts.jitter ?? 0, "motif jitter");
2396
2596
  const jitterProb = Math.min(1, Math.max(0, requestedJitter));
2397
- const tonic = pitchClass3(opts.key.rootPc) + 60;
2597
+ const tonic = pitchClassOf(opts.key.rootPc) + 60;
2398
2598
  const offsets = contourOffsets(contour, noteCount);
2399
2599
  const notes = [];
2400
2600
  for (let i = 0; i < noteCount; i += 1) {
@@ -2404,23 +2604,46 @@ function generateMotif(opts) {
2404
2604
  }
2405
2605
  let pitch = stepDiatonic(tonic, (offsets[i] ?? 0) + jitter, opts.key);
2406
2606
  const startBeat = i * beatsPerNote;
2407
- if (opts.chord && startBeat % 4 === 0) {
2607
+ if (opts.chord && isDownbeat(startBeat, barBeats)) {
2408
2608
  pitch = nearestChordTone(pitch, opts.chord);
2409
2609
  }
2410
- notes.push({ pitch, startBeat, durationBeat: beatsPerNote });
2610
+ notes.push({
2611
+ pitch: clampToMidi(pitch, "generated motif pitch"),
2612
+ startBeat,
2613
+ durationBeat: beatsPerNote
2614
+ });
2411
2615
  }
2412
2616
  return { notes };
2413
2617
  }
2618
+ function motifToNoteEvents(cell) {
2619
+ return cell.notes.map((note) => ({ ...note }));
2620
+ }
2414
2621
  function transformMotif(cell, t, amount, key) {
2622
+ return assertInRange(transformUnchecked(cell, t, amount, key));
2623
+ }
2624
+ function assertInRange(cell) {
2625
+ for (const note of cell.notes) {
2626
+ if (note.pitch < 0 || note.pitch > 127) {
2627
+ throw new InvalidInputError(
2628
+ `transformed motif pitch ${note.pitch} is outside the MIDI range 0..127`
2629
+ );
2630
+ }
2631
+ }
2632
+ return cell;
2633
+ }
2634
+ function transformUnchecked(cell, t, amount, key) {
2415
2635
  assertNoteEvents(cell.notes, "motif notes");
2416
2636
  if (amount !== void 0) {
2417
2637
  assertFiniteNumber(amount, "motif transform amount");
2418
2638
  }
2419
- if ((t === "transposeDiatonic" || t === "sequence") && amount !== void 0) {
2420
- 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
+ );
2421
2644
  }
2422
2645
  if ((t === "augment" || t === "diminish") && amount !== void 0 && amount <= 0) {
2423
- throw new RangeError("time transform amount must be positive");
2646
+ throw new InvalidInputError("time transform amount must be positive");
2424
2647
  }
2425
2648
  const notes = cell.notes;
2426
2649
  switch (t) {
@@ -2480,11 +2703,11 @@ function scaleTime(cell, factor) {
2480
2703
  }))
2481
2704
  };
2482
2705
  }
2483
- function developMotif(cell, timeline, key, bars) {
2706
+ function developMotif(cell, timeline, key, bars, ts = DEFAULT_TS4) {
2484
2707
  assertPositiveInt(bars, "development bars");
2485
2708
  assertNoteEvents(cell.notes, "motif notes");
2486
2709
  const span = cellSpan(cell);
2487
- const totalBeats = bars * 4;
2710
+ const totalBeats = bars * beatsPerBar(ts);
2488
2711
  const origin = cellOrigin(cell.notes);
2489
2712
  const out = [];
2490
2713
  if (span <= 0) {
@@ -2503,7 +2726,7 @@ function developMotif(cell, timeline, key, bars) {
2503
2726
  const chord = timeline.at(startBeat);
2504
2727
  let pitch = n.pitch;
2505
2728
  if (chord) {
2506
- const isMember = chordPitchClasses(chord).includes(pitchClass3(pitch));
2729
+ const isMember = chordPitchClasses(chord).includes(pitchClassOf(pitch));
2507
2730
  pitch = isMember ? pitch : nearestChordTone(pitch, chord);
2508
2731
  } else {
2509
2732
  pitch = nearestScaleTone(pitch, key);
@@ -2515,6 +2738,20 @@ function developMotif(cell, timeline, key, bars) {
2515
2738
  }
2516
2739
 
2517
2740
  // src/generate/progression/index.ts
2741
+ var BORROWED_DEGREES = Object.freeze({
2742
+ /** Flat submediant: bVI. */
2743
+ bVI: 8,
2744
+ /** Flat subtonic: bVII. */
2745
+ bVII: 10,
2746
+ /** Flat mediant: bIII. */
2747
+ bIII: 11,
2748
+ /** Minor subdominant borrowed from the parallel minor: iv. */
2749
+ iv: 12,
2750
+ /** Neapolitan: bII. */
2751
+ bII: 13,
2752
+ /** Sharp subdominant, the diminished #IV. */
2753
+ sharpIV: 14
2754
+ });
2518
2755
  var PRESETS = [
2519
2756
  {
2520
2757
  id: "fourChordPop",
@@ -2672,18 +2909,12 @@ var PRESETS = [
2672
2909
  }
2673
2910
  ];
2674
2911
  var BORROWED_OFFSET = {
2675
- 8: 8,
2676
- // bVI
2677
- 10: 10,
2678
- // bVII
2679
- 11: 3,
2680
- // bIII
2681
- 12: 5,
2682
- // iv
2683
- 13: 1,
2684
- // bII
2685
- 14: 6
2686
- // #IV
2912
+ [BORROWED_DEGREES.bVI]: 8,
2913
+ [BORROWED_DEGREES.bVII]: 10,
2914
+ [BORROWED_DEGREES.bIII]: 3,
2915
+ [BORROWED_DEGREES.iv]: 5,
2916
+ [BORROWED_DEGREES.bII]: 1,
2917
+ [BORROWED_DEGREES.sharpIV]: 6
2687
2918
  };
2688
2919
  function progressions() {
2689
2920
  return PRESETS.map((p) => ({ ...p, degrees: [...p.degrees], styles: [...p.styles] }));
@@ -2699,8 +2930,12 @@ function degreeToRootPc(degree, key) {
2699
2930
  const offset = BORROWED_OFFSET[degree] ?? 0;
2700
2931
  return ((key.rootPc % 12 + offset) % 12 + 12) % 12;
2701
2932
  }
2702
- function autoQuality(degree, key) {
2933
+ function autoQuality(degree, key, harmonicDominant) {
2703
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
+ }
2704
2939
  return diatonicTriad(degree, key).quality;
2705
2940
  }
2706
2941
  if (degree === 14) {
@@ -2711,40 +2946,99 @@ function autoQuality(degree, key) {
2711
2946
  }
2712
2947
  return "maj";
2713
2948
  }
2949
+ function resolveCycle(degrees, key, ext, harmonicDominant) {
2950
+ const steps = [];
2951
+ for (const degree of degrees) {
2952
+ const step = {
2953
+ source: degree,
2954
+ rootPc: degreeToRootPc(degree, key),
2955
+ quality: ext !== void 0 && ext !== "auto" ? ext : autoQuality(degree, key, harmonicDominant)
2956
+ };
2957
+ if (degree >= 0 && degree <= 6) {
2958
+ step.degree = degree;
2959
+ }
2960
+ const previous = steps[steps.length - 1];
2961
+ if (previous !== void 0 && previous.source !== step.source && sameStep(previous, step)) {
2962
+ continue;
2963
+ }
2964
+ steps.push(step);
2965
+ }
2966
+ const first = steps[0];
2967
+ const last = steps[steps.length - 1];
2968
+ if (steps.length > 1 && first !== void 0 && last !== void 0 && first.source !== last.source && sameStep(first, last)) {
2969
+ steps.pop();
2970
+ }
2971
+ return steps.length > 0 ? steps : [{ source: 0, degree: 0, rootPc: degreeToRootPc(0, key), quality: "maj" }];
2972
+ }
2973
+ function sameStep(a, b) {
2974
+ return a.rootPc === b.rootPc && a.quality === b.quality;
2975
+ }
2976
+ function pickProgressionPreset(style, seed = 0) {
2977
+ const pool = PRESETS.filter((preset) => preset.styles.includes(style));
2978
+ if (pool.length === 0) {
2979
+ throw new InvalidInputError(`Unknown progression style: ${style}`);
2980
+ }
2981
+ const rng = createRng(seed);
2982
+ const index = Math.floor(rng.next() * pool.length) % pool.length;
2983
+ return pool[index] ?? PRESETS[0];
2984
+ }
2714
2985
  function generateProgression(opts) {
2715
2986
  assertPositiveInt(opts.bars, "progression bars");
2716
2987
  assertGenerationBudget(opts.bars, "progression chords");
2717
2988
  const seed = opts.seed ?? 0;
2718
2989
  let preset;
2719
- if (opts.presetId !== void 0) {
2990
+ if (opts.preset !== void 0) {
2991
+ if (opts.preset.degrees.length === 0) {
2992
+ throw new InvalidInputError("progression preset must name at least one degree");
2993
+ }
2994
+ preset = {
2995
+ id: opts.preset.id ?? "custom",
2996
+ name: opts.preset.name ?? "Custom",
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
+ }),
3007
+ functional: opts.preset.functional ?? "loop",
3008
+ styles: opts.preset.styles ?? [opts.style]
3009
+ };
3010
+ }
3011
+ if (preset === void 0 && opts.presetId !== void 0) {
2720
3012
  preset = PRESETS.find((p) => p.id === opts.presetId);
2721
3013
  if (preset === void 0) {
2722
- throw new Error(`Unknown progression preset: ${opts.presetId}`);
3014
+ throw new InvalidInputError(`Unknown progression preset: ${opts.presetId}`);
2723
3015
  }
2724
3016
  }
2725
- if (preset === void 0) {
2726
- const pool = PRESETS.filter((p) => p.styles.includes(opts.style));
2727
- const candidates = pool.length > 0 ? pool : PRESETS;
2728
- const rng = createRng(seed);
2729
- const index = Math.floor(rng.next() * candidates.length) % candidates.length;
2730
- preset = candidates[index] ?? PRESETS[0];
2731
- }
2732
- const degrees = preset?.degrees ?? [0];
3017
+ preset ??= pickProgressionPreset(opts.style, seed);
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
+ );
2733
3025
  const chords = [];
2734
3026
  for (let bar = 0; bar < opts.bars; bar += 1) {
2735
- const degree = degrees[bar % degrees.length] ?? 0;
2736
- const quality = opts.ext !== void 0 && opts.ext !== "auto" ? opts.ext : autoQuality(degree, opts.key);
3027
+ const step = cycle[bar % cycle.length];
2737
3028
  const chord = {
2738
- rootPc: degreeToRootPc(degree, opts.key),
2739
- quality,
2740
- startBeat: bar * 4,
2741
- degree: degree >= 0 && degree <= 6 ? degree : void 0
3029
+ rootPc: step?.rootPc ?? 0,
3030
+ quality: step?.quality ?? "maj",
3031
+ startBeat: bar * 4
2742
3032
  };
3033
+ if (step?.degree !== void 0) {
3034
+ chord.degree = step.degree;
3035
+ }
2743
3036
  chords.push(chord);
2744
3037
  }
2745
3038
  if (opts.reharmonize) {
2746
3039
  const tonicPc = (opts.key.rootPc % 12 + 12) % 12;
2747
3040
  const rrng = createRng((seed ^ 2654435769) >>> 0);
3041
+ let tonicStatements = chords.filter((chord) => chord.rootPc === tonicPc).length;
2748
3042
  for (let i = 0; i < chords.length - 1; i += 1) {
2749
3043
  const cur = chords[i];
2750
3044
  const next = chords[i + 1];
@@ -2753,12 +3047,16 @@ function generateProgression(opts) {
2753
3047
  }
2754
3048
  const domRoot = (next.rootPc + 7) % 12;
2755
3049
  const targetsTonic = (next.rootPc - tonicPc + 12) % 12 === 0;
3050
+ const isLastTonicStatement = cur.rootPc === tonicPc && tonicStatements <= 1;
2756
3051
  const alreadySecondary = cur.rootPc === domRoot && cur.quality === "dom7";
2757
3052
  const isResolutionTarget = chords[i - 1]?.secondaryDominant === true;
2758
- if (targetsTonic || alreadySecondary || isResolutionTarget) {
3053
+ if (targetsTonic || isLastTonicStatement || alreadySecondary || isResolutionTarget) {
2759
3054
  continue;
2760
3055
  }
2761
3056
  if (rrng.next() < 0.5) {
3057
+ if (cur.rootPc === tonicPc) {
3058
+ tonicStatements -= 1;
3059
+ }
2762
3060
  chords[i] = {
2763
3061
  rootPc: domRoot,
2764
3062
  quality: "dom7",
@@ -2772,6 +3070,17 @@ function generateProgression(opts) {
2772
3070
  }
2773
3071
 
2774
3072
  // src/generate/rhythm/index.ts
3073
+ var DEFAULT_RHYTHM_VELOCITY = 96;
3074
+ function rhythmToNoteEvents(events, pitch, velocity = DEFAULT_RHYTHM_VELOCITY) {
3075
+ assertFiniteNumber(pitch, "pitch");
3076
+ assertRange(velocity, 0, 127, "velocity");
3077
+ return events.map((event) => ({
3078
+ pitch,
3079
+ startBeat: event.position,
3080
+ durationBeat: event.duration,
3081
+ velocity
3082
+ }));
3083
+ }
2775
3084
  var DEFAULT_SUBDIVISION = 2;
2776
3085
  var DEFAULT_DENSITY = 0.5;
2777
3086
  var DEFAULT_BARS = 1;
@@ -2791,28 +3100,34 @@ function generateRhythm(ts, opts = {}) {
2791
3100
  const seed = opts.seed ?? 0;
2792
3101
  const bars = opts.bars ?? DEFAULT_BARS;
2793
3102
  const subdivision = opts.subdivision ?? DEFAULT_SUBDIVISION;
2794
- const requestedDensity = assertFiniteNumber(opts.density ?? DEFAULT_DENSITY, "rhythm density");
2795
- const density = Math.min(1, Math.max(0, requestedDensity));
3103
+ const density = assertRange(opts.density ?? DEFAULT_DENSITY, 0, 1, "rhythm density");
2796
3104
  assertPositiveInt(bars, "rhythm bars");
2797
3105
  assertPositiveInt(subdivision, "rhythm subdivision");
2798
3106
  const barBeats = beatsPerBar(ts);
2799
3107
  const spanBeats = barBeats * bars;
2800
3108
  const step = 1 / subdivision;
2801
- const slotCount = Math.round(spanBeats * subdivision);
3109
+ const slotsPerBar = Math.ceil(barBeats * subdivision);
3110
+ const slotCount = slotsPerBar * bars;
2802
3111
  assertGenerationBudget(slotCount, "rhythm grid slots");
2803
- const slotsPerBar = Math.round(barBeats * subdivision);
2804
3112
  const rng = createRng(seed);
2805
3113
  const positions = [];
2806
- for (let i = 0; i < slotCount; i += 1) {
2807
- const position = i * step;
2808
- if (i % slotsPerBar === 0) {
2809
- positions.push(position);
2810
- continue;
2811
- }
2812
- const weight = metricWeight(position, ts);
2813
- const probability = onsetWeightCurve(weight) * density;
2814
- if (rng.prob(probability)) {
2815
- 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
+ }
2816
3131
  }
2817
3132
  }
2818
3133
  const events = [];
@@ -2834,21 +3149,28 @@ function rhythmDensity(events, ts) {
2834
3149
  }
2835
3150
 
2836
3151
  export {
3152
+ BASS_STYLES,
2837
3153
  generateBassLine,
2838
3154
  generateCounterMelody,
3155
+ DRUM_NOTES,
3156
+ drumVoiceOf,
2839
3157
  generateDrums,
2840
3158
  humanize,
2841
3159
  extractGrooveTemplate,
2842
3160
  applyGrooveTemplate,
2843
3161
  harmonizeMelody,
2844
3162
  generateMotif,
3163
+ motifToNoteEvents,
2845
3164
  transformMotif,
2846
3165
  developMotif,
3166
+ BORROWED_DEGREES,
2847
3167
  progressions,
2848
3168
  progressionsByStyle,
3169
+ pickProgressionPreset,
2849
3170
  generateProgression,
3171
+ rhythmToNoteEvents,
2850
3172
  onsetWeightCurve,
2851
3173
  generateRhythm,
2852
3174
  rhythmDensity
2853
3175
  };
2854
- //# sourceMappingURL=chunk-4MHA432G.js.map
3176
+ //# sourceMappingURL=chunk-AID75RN3.js.map