@libraz/libcantus 0.9.2 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +4 -3
  2. package/dist/analyze/index.cjs +315 -42
  3. package/dist/analyze/index.cjs.map +1 -1
  4. package/dist/analyze/index.d.cts +11 -99
  5. package/dist/analyze/index.d.ts +11 -99
  6. package/dist/analyze/index.js +6 -6
  7. package/dist/chunk-2ZWMSJH6.js +30 -0
  8. package/dist/chunk-2ZWMSJH6.js.map +1 -0
  9. package/dist/{chunk-R3S2LUBH.js → chunk-4MHA432G.js} +316 -184
  10. package/dist/chunk-4MHA432G.js.map +1 -0
  11. package/dist/chunk-62QZKCJQ.js +49 -0
  12. package/dist/chunk-62QZKCJQ.js.map +1 -0
  13. package/dist/chunk-AKLYPWIN.js +83 -0
  14. package/dist/chunk-AKLYPWIN.js.map +1 -0
  15. package/dist/{chunk-XTC3X6MS.js → chunk-C5XYKUVG.js} +100 -12
  16. package/dist/chunk-C5XYKUVG.js.map +1 -0
  17. package/dist/{chunk-LN7ZZMD2.js → chunk-CHMXELDS.js} +33 -14
  18. package/dist/chunk-CHMXELDS.js.map +1 -0
  19. package/dist/{chunk-A4532ZFR.js → chunk-EH6EM37U.js} +60 -4
  20. package/dist/chunk-EH6EM37U.js.map +1 -0
  21. package/dist/chunk-FBQK3ZQN.js +131 -0
  22. package/dist/chunk-FBQK3ZQN.js.map +1 -0
  23. package/dist/{chunk-5HTNMQW4.js → chunk-GOAW4VYL.js} +87 -21
  24. package/dist/chunk-GOAW4VYL.js.map +1 -0
  25. package/dist/{chunk-MSWFZF7J.js → chunk-I5R7ZBJS.js} +90 -6
  26. package/dist/chunk-I5R7ZBJS.js.map +1 -0
  27. package/dist/{chunk-GQKN5KDT.js → chunk-IT2O4XFE.js} +4 -9
  28. package/dist/chunk-IT2O4XFE.js.map +1 -0
  29. package/dist/{chunk-7Y7WF64D.js → chunk-MIWO425M.js} +6 -4
  30. package/dist/chunk-MIWO425M.js.map +1 -0
  31. package/dist/chunk-N6IHQUOX.js +1 -0
  32. package/dist/chunk-N6IHQUOX.js.map +1 -0
  33. package/dist/{chunk-COKTX2G5.js → chunk-SGU7SPLK.js} +29 -12
  34. package/dist/chunk-SGU7SPLK.js.map +1 -0
  35. package/dist/core/index.cjs +230 -14
  36. package/dist/core/index.cjs.map +1 -1
  37. package/dist/core/index.d.cts +56 -4
  38. package/dist/core/index.d.ts +56 -4
  39. package/dist/core/index.js +24 -4
  40. package/dist/{function-2bJW1Zp_.d.ts → function-DVH4Eusr.d.ts} +1 -1
  41. package/dist/{function-r2sh-jOO.d.cts → function-DYt4iKsn.d.cts} +1 -1
  42. package/dist/generate/index.cjs +830 -494
  43. package/dist/generate/index.cjs.map +1 -1
  44. package/dist/generate/index.d.cts +11 -5
  45. package/dist/generate/index.d.ts +11 -5
  46. package/dist/generate/index.js +9 -9
  47. package/dist/{index-BOBXBNwP.d.cts → index-BNlGP86A.d.cts} +1 -1
  48. package/dist/{index-CP3wAfEH.d.ts → index-Bg5dYMmZ.d.ts} +1 -1
  49. package/dist/{index-CM5W4rcm.d.ts → index-Bt4tOUD9.d.ts} +1 -1
  50. package/dist/index-CnZCo29u.d.cts +108 -0
  51. package/dist/{index-D_ZTUNKp.d.cts → index-CpOsP1y9.d.cts} +2 -2
  52. package/dist/{index-DZ1M21do.d.cts → index-CxqBQtH1.d.cts} +2 -1
  53. package/dist/{index-BdeQ1r_p.d.cts → index-Dikst44v.d.cts} +1 -1
  54. package/dist/{index-BMPxwjwV.d.ts → index-DxvARIP2.d.ts} +2 -2
  55. package/dist/{index-BreoJRw0.d.ts → index-Jc0Ryi79.d.ts} +2 -1
  56. package/dist/index-JnF4v9bx.d.ts +108 -0
  57. package/dist/index.cjs +818 -283
  58. package/dist/index.cjs.map +1 -1
  59. package/dist/index.d.cts +9 -8
  60. package/dist/index.d.ts +9 -8
  61. package/dist/index.js +34 -13
  62. package/dist/model/index.cjs +170 -31
  63. package/dist/model/index.cjs.map +1 -1
  64. package/dist/model/index.d.cts +6 -5
  65. package/dist/model/index.d.ts +6 -5
  66. package/dist/model/index.js +6 -6
  67. package/dist/{styled-D7BAx3S3.d.ts → styled-DZ7GjShb.d.ts} +1 -1
  68. package/dist/{styled-BjXtOFNk.d.cts → styled-ZTbdaVFM.d.cts} +1 -1
  69. package/dist/theory/index.cjs +180 -34
  70. package/dist/theory/index.cjs.map +1 -1
  71. package/dist/theory/index.d.cts +6 -6
  72. package/dist/theory/index.d.ts +6 -6
  73. package/dist/theory/index.js +6 -5
  74. package/package.json +50 -19
  75. package/src/analyze/arrange/internal.ts +3 -1
  76. package/src/analyze/arrange/tension.ts +15 -3
  77. package/src/analyze/arrange/tracks.ts +36 -3
  78. package/src/analyze/detect/index.ts +46 -9
  79. package/src/analyze/functional/roman.ts +14 -5
  80. package/src/analyze/index.ts +1 -1
  81. package/src/analyze/timeline/index.ts +43 -7
  82. package/src/analyze/voice/index.ts +11 -2
  83. package/src/core/event-index/index.ts +104 -0
  84. package/src/core/index.ts +14 -0
  85. package/src/core/meter/index.ts +24 -4
  86. package/src/core/random/index.ts +26 -3
  87. package/src/core/tuning/index.ts +36 -6
  88. package/src/core/validation/index.ts +119 -0
  89. package/src/generate/bass/index.ts +23 -0
  90. package/src/generate/countermelody/index.ts +167 -76
  91. package/src/generate/drums/euclid.ts +10 -3
  92. package/src/generate/drums/index.ts +56 -24
  93. package/src/generate/drums/percussion.ts +1 -1
  94. package/src/generate/groove/index.ts +25 -0
  95. package/src/generate/harmonize/index.ts +55 -44
  96. package/src/generate/index.ts +0 -1
  97. package/src/generate/motif/index.ts +45 -21
  98. package/src/generate/progression/index.ts +8 -16
  99. package/src/generate/reharmony/index.ts +1 -8
  100. package/src/generate/rhythm/index.ts +11 -1
  101. package/src/index.ts +13 -247
  102. package/src/model/chord.ts +29 -10
  103. package/src/theory/chord/index.ts +4 -2
  104. package/src/theory/harmony/index.ts +6 -21
  105. package/src/theory/index.ts +6 -1
  106. package/src/theory/safety/index.ts +71 -2
  107. package/src/theory/spelling/index.ts +70 -3
  108. package/src/theory/voicing/satb.ts +25 -4
  109. package/src/theory/voicing/styled.ts +26 -3
  110. package/dist/chunk-5HTNMQW4.js.map +0 -1
  111. package/dist/chunk-7Y7WF64D.js.map +0 -1
  112. package/dist/chunk-A4532ZFR.js.map +0 -1
  113. package/dist/chunk-BHUT2S5G.js +0 -22
  114. package/dist/chunk-BHUT2S5G.js.map +0 -1
  115. package/dist/chunk-CCBLVAPK.js +0 -42
  116. package/dist/chunk-CCBLVAPK.js.map +0 -1
  117. package/dist/chunk-COKTX2G5.js.map +0 -1
  118. package/dist/chunk-GQKN5KDT.js.map +0 -1
  119. package/dist/chunk-LN7ZZMD2.js.map +0 -1
  120. package/dist/chunk-MSWFZF7J.js.map +0 -1
  121. package/dist/chunk-R3S2LUBH.js.map +0 -1
  122. package/dist/chunk-SDM3VJJ5.js +0 -41
  123. package/dist/chunk-SDM3VJJ5.js.map +0 -1
  124. package/dist/chunk-XTC3X6MS.js.map +0 -1
  125. package/dist/chunk-ZXW5YIWK.js +0 -55
  126. package/dist/chunk-ZXW5YIWK.js.map +0 -1
@@ -1,6 +1,10 @@
1
+ import { isDiatonic, parallelKey } from '../../analyze/functional/index.js';
2
+ import { createNoteEventIndex } from '../../core/event-index/index.js';
1
3
  import type { TimeSignature } from '../../core/meter/index.js';
2
4
  import { isStrongBeat } from '../../core/meter/index.js';
5
+ import { createRng } from '../../core/random/index.js';
3
6
  import type { KeyScale, NoteEvent } from '../../core/types.js';
7
+ import { assertGenerationBudget, assertRange } from '../../core/validation/index.js';
4
8
  import type { Chord, ChordQuality } from '../../theory/chord/index.js';
5
9
  import { chordPitchClasses, diatonicTriad, makeChord } from '../../theory/chord/index.js';
6
10
  import type { HarmonyRole } from '../../theory/harmony/index.js';
@@ -86,17 +90,6 @@ function pitchClass(pitch: number): number {
86
90
  return ((Math.trunc(pitch) % 12) + 12) % 12;
87
91
  }
88
92
 
89
- function mulberry32(seed: number): () => number {
90
- let state = seed >>> 0;
91
- return () => {
92
- state = (state + 0x6d2b79f5) >>> 0;
93
- let t = state;
94
- t = Math.imul(t ^ (t >>> 15), t | 1);
95
- t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
96
- return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
97
- };
98
- }
99
-
100
93
  /**
101
94
  * Estimate the best-fit key from a melody's pitch-class weighting.
102
95
  *
@@ -151,7 +144,6 @@ function buildCandidates(key: KeyScale, reharmonize: HarmonizeOptions['reharmoni
151
144
  base: 0,
152
145
  }));
153
146
 
154
- const tonic = tones[0] ?? 0;
155
147
  if (reharmonize !== 'diatonic') {
156
148
  for (const target of [1, 3, 4, 5]) {
157
149
  const targetRoot = tones[target] ?? 0;
@@ -166,27 +158,30 @@ function buildCandidates(key: KeyScale, reharmonize: HarmonizeOptions['reharmoni
166
158
  }
167
159
 
168
160
  if (reharmonize === 'borrowed') {
169
- candidates.push({
170
- rootPc: (tonic + 10) % 12,
171
- quality: 'maj',
172
- secondaryDominant: false,
173
- base: 2,
174
- }); // bVII
175
- candidates.push({
176
- rootPc: (tonic + 8) % 12,
177
- quality: 'maj',
178
- secondaryDominant: false,
179
- base: 2,
180
- }); // bVI
181
- candidates.push({
182
- rootPc: (tonic + 5) % 12,
183
- quality: 'min',
184
- secondaryDominant: false,
185
- base: 2,
186
- }); // iv
161
+ const parallel = parallelKey(key);
162
+ const parallelTones = scaleTonesInDegreeOrder(parallel);
163
+ for (let degree = 0; degree < parallelTones.length; degree += 1) {
164
+ const chord = diatonicTriad(degree, parallel);
165
+ if (!isDiatonic(chord, key)) {
166
+ candidates.push({
167
+ rootPc: chord.rootPc,
168
+ quality: chord.quality,
169
+ secondaryDominant: false,
170
+ base: 1.25,
171
+ });
172
+ }
173
+ }
187
174
  }
188
175
 
189
- return candidates;
176
+ const seen = new Set<string>();
177
+ return candidates.filter((candidate) => {
178
+ const id = `${candidate.rootPc}:${candidate.quality}`;
179
+ if (seen.has(id)) {
180
+ return false;
181
+ }
182
+ seen.add(id);
183
+ return true;
184
+ });
190
185
  }
191
186
 
192
187
  /**
@@ -361,24 +356,40 @@ function harmonizeOnce(
361
356
  * @category Reharmonization
362
357
  */
363
358
  export function harmonizeMelody(opts: HarmonizeOptions): HarmonizeResult {
359
+ const noteIndex = createNoteEventIndex(opts.melody);
364
360
  const key = opts.key === 'infer' ? inferKey(opts.melody) : opts.key;
365
361
  const candidates = buildCandidates(key, opts.reharmonize);
366
362
  const candAt = (i: number): Candidate => candidates[i] ?? FALLBACK;
367
- const rng = mulberry32(opts.seed ?? 0);
368
- const jitter = candidates.map(() => rng() * TIE_BREAK_JITTER);
363
+ const rng = createRng(opts.seed ?? 0);
364
+ const jitter = candidates.map(() => rng.next() * TIE_BREAK_JITTER);
369
365
 
370
- const melodyEnd = opts.melody.reduce((m, n) => Math.max(m, n.startBeat + n.durationBeat), 0);
371
- const hr = Math.max(0.25, opts.harmonicRhythm);
366
+ const melodyEnd = noteIndex.notes.reduce((m, n) => Math.max(m, n.endBeat), 0);
367
+ const requestedHr = assertRange(
368
+ opts.harmonicRhythm,
369
+ Number.MIN_VALUE,
370
+ Number.MAX_SAFE_INTEGER,
371
+ 'harmonic rhythm',
372
+ );
373
+ const hr = Math.max(0.25, requestedHr);
372
374
  const segCount = Math.max(1, Math.ceil(melodyEnd / hr));
373
- const segments: Segment[] = [];
374
- for (let s = 0; s < segCount; s += 1) {
375
- const startBeat = s * hr;
376
- const endBeat = startBeat + hr;
377
- const noteIndices = opts.melody
378
- .map((n, i) => ({ n, i }))
379
- .filter(({ n }) => n.startBeat < endBeat && n.startBeat + n.durationBeat > startBeat)
380
- .map(({ i }) => i);
381
- segments.push({ startBeat, endBeat, noteIndices });
375
+ assertGenerationBudget(segCount, 'harmonic segments');
376
+ assertGenerationBudget(segCount * candidates.length * candidates.length, 'harmonization search');
377
+ const segments: Segment[] = Array.from({ length: segCount }, (_, s) => ({
378
+ startBeat: s * hr,
379
+ endBeat: (s + 1) * hr,
380
+ noteIndices: [],
381
+ }));
382
+ // Associate each note only with the windows it actually spans. This replaces
383
+ // the former per-window full melody scan and creates no temporary note objects.
384
+ let memberships = 0;
385
+ for (const indexed of noteIndex.notes) {
386
+ const first = Math.max(0, Math.floor(indexed.note.startBeat / hr));
387
+ const lastExclusive = Math.min(segCount, Math.ceil(indexed.endBeat / hr - Number.EPSILON));
388
+ memberships += Math.max(0, lastExclusive - first);
389
+ assertGenerationBudget(memberships, 'note-to-segment memberships');
390
+ for (let segment = first; segment < lastExclusive; segment += 1) {
391
+ segments[segment]?.noteIndices.push(indexed.originalIndex);
392
+ }
382
393
  }
383
394
 
384
395
  const transposes: number[] = [0];
@@ -33,7 +33,6 @@ export type {
33
33
  } from './motif/index.js';
34
34
  export { developMotif, generateMotif, transformMotif } from './motif/index.js';
35
35
  export type {
36
- ChordSpan,
37
36
  ProgFunction,
38
37
  ProgressionOptions,
39
38
  ProgressionPreset,
@@ -1,5 +1,13 @@
1
1
  import type { ChordTimeline } from '../../analyze/timeline/index.js';
2
+ import { createRng } from '../../core/random/index.js';
2
3
  import type { KeyScale } from '../../core/types.js';
4
+ import {
5
+ assertFiniteNumber,
6
+ assertGenerationBudget,
7
+ assertInteger,
8
+ assertNoteEvents,
9
+ assertPositiveInt,
10
+ } from '../../core/validation/index.js';
3
11
  import type { Chord } from '../../theory/chord/index.js';
4
12
  import { chordPitchClasses } from '../../theory/chord/index.js';
5
13
  import { isScaleTone, nearestScaleTone } from '../../theory/scale/index.js';
@@ -133,7 +141,20 @@ function nearestChordTone(pitch: number, chord: Chord): number {
133
141
 
134
142
  /** Earliest onset in a cell; the time origin for timing transforms. */
135
143
  function cellOrigin(notes: MotifNote[]): number {
136
- return notes.length === 0 ? 0 : Math.min(...notes.map((n) => n.startBeat));
144
+ let origin = Number.POSITIVE_INFINITY;
145
+ for (const note of notes) {
146
+ origin = Math.min(origin, note.startBeat);
147
+ }
148
+ return Number.isFinite(origin) ? origin : 0;
149
+ }
150
+
151
+ /** Latest offset in a cell, without spreading an unbounded caller array. */
152
+ function cellEnd(notes: MotifNote[]): number {
153
+ let end = Number.NEGATIVE_INFINITY;
154
+ for (const note of notes) {
155
+ end = Math.max(end, note.startBeat + note.durationBeat);
156
+ }
157
+ return Number.isFinite(end) ? end : 0;
137
158
  }
138
159
 
139
160
  /** Total beat span covered by a cell (from first onset to last offset). */
@@ -142,7 +163,7 @@ function cellSpan(cell: MotifCell): number {
142
163
  return 0;
143
164
  }
144
165
  const start = cellOrigin(cell.notes);
145
- const end = Math.max(...cell.notes.map((n) => n.startBeat + n.durationBeat));
166
+ const end = cellEnd(cell.notes);
146
167
  return end - start;
147
168
  }
148
169
 
@@ -150,18 +171,6 @@ function clone(cell: MotifCell): MotifCell {
150
171
  return { notes: cell.notes.map((n) => ({ ...n })) };
151
172
  }
152
173
 
153
- /** Deterministic 32-bit PRNG in [0, 1). */
154
- function mulberry32(seed: number): () => number {
155
- let state = seed >>> 0;
156
- return () => {
157
- state = (state + 0x6d2b79f5) >>> 0;
158
- let t = state;
159
- t = Math.imul(t ^ (t >>> 15), t | 1);
160
- t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
161
- return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
162
- };
163
- }
164
-
165
174
  /** Diatonic scale-degree offsets shaping a contour of `count` notes. */
166
175
  function contourOffsets(contour: MotifContour, count: number): number[] {
167
176
  const offsets: number[] = [];
@@ -209,12 +218,14 @@ function contourOffsets(contour: MotifContour, count: number): number[] {
209
218
  */
210
219
  export function generateMotif(opts: MotifOptions): MotifCell {
211
220
  const contour = opts.contour ?? 'arch';
212
- const bars = Math.max(1, Math.trunc(opts.bars));
221
+ const bars = assertPositiveInt(opts.bars, 'motif bars');
213
222
  const totalBeats = bars * 4;
214
223
  const noteCount = Math.max(3, bars * 2);
224
+ assertGenerationBudget(noteCount, 'motif notes');
215
225
  const beatsPerNote = totalBeats / noteCount;
216
- const rng = mulberry32(opts.seed ?? 0);
217
- const jitterProb = Math.min(1, Math.max(0, opts.jitter ?? 0));
226
+ const rng = createRng(opts.seed ?? 0);
227
+ const requestedJitter = assertFiniteNumber(opts.jitter ?? 0, 'motif jitter');
228
+ const jitterProb = Math.min(1, Math.max(0, requestedJitter));
218
229
  const tonic = pitchClass(opts.key.rootPc) + 60;
219
230
  const offsets = contourOffsets(contour, noteCount);
220
231
 
@@ -224,8 +235,8 @@ export function generateMotif(opts: MotifOptions): MotifCell {
224
235
  // preserved exactly. When enabled it nudges up or down with equal odds,
225
236
  // avoiding the upward bias that used to drift arch/wave tails off the tonic.
226
237
  let jitter = 0;
227
- if (jitterProb > 0 && rng() < jitterProb) {
228
- jitter = rng() < 0.5 ? 1 : -1;
238
+ if (jitterProb > 0 && rng.next() < jitterProb) {
239
+ jitter = rng.next() < 0.5 ? 1 : -1;
229
240
  }
230
241
  let pitch = stepDiatonic(tonic, (offsets[i] ?? 0) + jitter, opts.key);
231
242
  const startBeat = i * beatsPerNote;
@@ -271,6 +282,16 @@ export function transformMotif(
271
282
  amount?: number,
272
283
  key?: KeyScale,
273
284
  ): MotifCell {
285
+ assertNoteEvents(cell.notes, 'motif notes');
286
+ if (amount !== undefined) {
287
+ assertFiniteNumber(amount, 'motif transform amount');
288
+ }
289
+ if ((t === 'transposeDiatonic' || t === 'sequence') && amount !== undefined) {
290
+ assertInteger(amount, 'diatonic transform amount');
291
+ }
292
+ if ((t === 'augment' || t === 'diminish') && amount !== undefined && amount <= 0) {
293
+ throw new RangeError('time transform amount must be positive');
294
+ }
274
295
  const notes = cell.notes;
275
296
  switch (t) {
276
297
  case 'transposeChromatic': {
@@ -297,7 +318,7 @@ export function transformMotif(
297
318
  return { notes: [] };
298
319
  }
299
320
  const start = cellOrigin(notes);
300
- const end = Math.max(...notes.map((n) => n.startBeat + n.durationBeat));
321
+ const end = cellEnd(notes);
301
322
  return {
302
323
  notes: notes.map((n) => ({
303
324
  pitch: n.pitch,
@@ -363,8 +384,10 @@ export function developMotif(
363
384
  key: KeyScale,
364
385
  bars: number,
365
386
  ): MotifCell {
387
+ assertPositiveInt(bars, 'development bars');
388
+ assertNoteEvents(cell.notes, 'motif notes');
366
389
  const span = cellSpan(cell);
367
- const totalBeats = Math.max(1, Math.trunc(bars)) * 4;
390
+ const totalBeats = bars * 4;
368
391
  const origin = cellOrigin(cell.notes);
369
392
  const out: MotifNote[] = [];
370
393
 
@@ -376,6 +399,7 @@ export function developMotif(
376
399
  // the offset multiplicatively so rounding error cannot accumulate over tiles.
377
400
  const tileSpan = Math.max(span, MIN_TILE_SPAN);
378
401
  const tileCount = Math.ceil(totalBeats / tileSpan);
402
+ assertGenerationBudget(tileCount * cell.notes.length, 'developed motif notes');
379
403
  for (let k = 0; k < tileCount; k += 1) {
380
404
  const offset = k * tileSpan;
381
405
  for (const n of cell.notes) {
@@ -1,4 +1,6 @@
1
+ import { createRng } from '../../core/random/index.js';
1
2
  import type { KeyScale } from '../../core/types.js';
3
+ import { assertGenerationBudget, assertPositiveInt } from '../../core/validation/index.js';
2
4
  import type { ChordQuality, ChordSpan } from '../../theory/chord/index.js';
3
5
  import { diatonicTriad } from '../../theory/chord/index.js';
4
6
  import { scaleTonesInDegreeOrder } from '../../theory/scale/index.js';
@@ -237,18 +239,6 @@ export function progressionsByStyle(style: ProgStyle): ProgressionPreset[] {
237
239
  return progressions().filter((p) => p.styles.includes(style));
238
240
  }
239
241
 
240
- /** Deterministic 32-bit PRNG producing a float in [0, 1). */
241
- function mulberry32(seed: number): () => number {
242
- let state = seed >>> 0;
243
- return () => {
244
- state = (state + 0x6d2b79f5) >>> 0;
245
- let t = state;
246
- t = Math.imul(t ^ (t >>> 15), t | 1);
247
- t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
248
- return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
249
- };
250
- }
251
-
252
242
  /** Root pitch class of a scale degree in the given key, including borrowed degrees. */
253
243
  function degreeToRootPc(degree: number, key: KeyScale): number {
254
244
  if (degree >= 0 && degree <= 6) {
@@ -307,6 +297,8 @@ function autoQuality(degree: number, key: KeyScale): ChordQuality {
307
297
  * @category Composition
308
298
  */
309
299
  export function generateProgression(opts: ProgressionOptions): ChordSpan[] {
300
+ assertPositiveInt(opts.bars, 'progression bars');
301
+ assertGenerationBudget(opts.bars, 'progression chords');
310
302
  const seed = opts.seed ?? 0;
311
303
  let preset: ProgressionPreset | undefined;
312
304
  if (opts.presetId !== undefined) {
@@ -318,8 +310,8 @@ export function generateProgression(opts: ProgressionOptions): ChordSpan[] {
318
310
  if (preset === undefined) {
319
311
  const pool = PRESETS.filter((p) => p.styles.includes(opts.style));
320
312
  const candidates = pool.length > 0 ? pool : PRESETS;
321
- const rng = mulberry32(seed);
322
- const index = Math.floor(rng() * candidates.length) % candidates.length;
313
+ const rng = createRng(seed);
314
+ const index = Math.floor(rng.next() * candidates.length) % candidates.length;
323
315
  preset = candidates[index] ?? PRESETS[0];
324
316
  }
325
317
  const degrees = preset?.degrees ?? [0];
@@ -339,7 +331,7 @@ export function generateProgression(opts: ProgressionOptions): ChordSpan[] {
339
331
 
340
332
  if (opts.reharmonize) {
341
333
  const tonicPc = (((opts.key.rootPc % 12) + 12) % 12) as number;
342
- const rrng = mulberry32((seed ^ 0x9e3779b9) >>> 0);
334
+ const rrng = createRng((seed ^ 0x9e3779b9) >>> 0);
343
335
  for (let i = 0; i < chords.length - 1; i += 1) {
344
336
  const cur = chords[i];
345
337
  const next = chords[i + 1];
@@ -355,7 +347,7 @@ export function generateProgression(opts: ProgressionOptions): ChordSpan[] {
355
347
  if (targetsTonic || alreadySecondary || isResolutionTarget) {
356
348
  continue;
357
349
  }
358
- if (rrng() < 0.5) {
350
+ if (rrng.next() < 0.5) {
359
351
  chords[i] = {
360
352
  rootPc: domRoot,
361
353
  quality: 'dom7',
@@ -91,13 +91,6 @@ function isStableTarget(chord: Chord): boolean {
91
91
  return hasFifth && hasThird && !isDominantType(chord);
92
92
  }
93
93
 
94
- /** The triad pitch classes (root, third, fifth) implied by a chord's template. */
95
- function triadPitchClasses(chord: Chord): number[] {
96
- const third = hasInterval(chord, 4) ? 4 : hasInterval(chord, 3) ? 3 : 4;
97
- const fifth = hasInterval(chord, 8) ? 8 : hasInterval(chord, 6) ? 6 : 7;
98
- return [mod12(chord.rootPc), mod12(chord.rootPc + third), mod12(chord.rootPc + fifth)];
99
- }
100
-
101
94
  /** Count the pitch classes shared between two pitch-class sets. */
102
95
  function commonToneCount(a: number[], b: number[]): number {
103
96
  const set = new Set(b);
@@ -157,7 +150,7 @@ export function substituteChord(
157
150
  key: KeyScale,
158
151
  opts?: SubstituteOptions,
159
152
  ): Substitution[] {
160
- const original = triadPitchClasses(chord);
153
+ const original = chordPitchClasses(chord);
161
154
  const candidates: { chord: Chord; type: SubstitutionType }[] = [];
162
155
 
163
156
  // Tritone substitution: only for dominant-type chords.
@@ -13,6 +13,11 @@
13
13
 
14
14
  import { beatsPerBar, metricWeight, type TimeSignature } from '../../core/meter/index.js';
15
15
  import { createRng } from '../../core/random/index.js';
16
+ import {
17
+ assertFiniteNumber,
18
+ assertGenerationBudget,
19
+ assertPositiveInt,
20
+ } from '../../core/validation/index.js';
16
21
 
17
22
  /**
18
23
  * A single rhythmic note: an onset position and how long it sounds.
@@ -113,12 +118,17 @@ export function generateRhythm(ts: TimeSignature, opts: RhythmOptions = {}): Rhy
113
118
  const seed = opts.seed ?? 0;
114
119
  const bars = opts.bars ?? DEFAULT_BARS;
115
120
  const subdivision = opts.subdivision ?? DEFAULT_SUBDIVISION;
116
- const density = Math.min(1, Math.max(0, opts.density ?? DEFAULT_DENSITY));
121
+ const requestedDensity = assertFiniteNumber(opts.density ?? DEFAULT_DENSITY, 'rhythm density');
122
+ const density = Math.min(1, Math.max(0, requestedDensity));
123
+
124
+ assertPositiveInt(bars, 'rhythm bars');
125
+ assertPositiveInt(subdivision, 'rhythm subdivision');
117
126
 
118
127
  const barBeats = beatsPerBar(ts);
119
128
  const spanBeats = barBeats * bars;
120
129
  const step = 1 / subdivision;
121
130
  const slotCount = Math.round(spanBeats * subdivision);
131
+ assertGenerationBudget(slotCount, 'rhythm grid slots');
122
132
  const slotsPerBar = Math.round(barBeats * subdivision);
123
133
  const rng = createRng(seed);
124
134
 
package/src/index.ts CHANGED
@@ -1,247 +1,13 @@
1
- export type {
2
- ArrangementAnalysis,
3
- ArrangementOptions,
4
- ArrangementTrack,
5
- Conflict,
6
- TensionPoint,
7
- TrackAnalysis,
8
- TrackRole,
9
- } from './analyze/arrange/index.js';
10
- export { analyzeArrangement, tensionCurve } from './analyze/arrange/index.js';
11
- export type { ChordMatch, KeyMatch } from './analyze/detect/index.js';
12
- export { detectChord, detectChordBest, detectKey } from './analyze/detect/index.js';
13
- export type {
14
- BorrowedSource,
15
- Cadence,
16
- ChordAnalysis,
17
- HarmonicFunction,
18
- } from './analyze/functional/index.js';
19
- export {
20
- analyzeChord,
21
- borrowedSource,
22
- chordToRoman,
23
- detectCadence,
24
- functionOf,
25
- isBorrowedChord,
26
- isDiatonic,
27
- isMinorKey,
28
- parallelKey,
29
- romanToChord,
30
- secondaryDominant,
31
- } from './analyze/functional/index.js';
32
- export type {
33
- CadenceHit,
34
- ChordSegment,
35
- ChordTimeline,
36
- ChordTimelineOptions,
37
- ChordTimelineResult,
38
- } from './analyze/timeline/index.js';
39
- export {
40
- chordTimelineFromChords,
41
- chordTimelineFromNotes,
42
- detectCadences,
43
- } from './analyze/timeline/index.js';
44
- export type { AnalyzedNote, TheoryLabel, VoiceNote } from './analyze/voice/index.js';
45
- export { analyzeVoice } from './analyze/voice/index.js';
46
- export {
47
- classifyInterval,
48
- IntervalQuality,
49
- isConsonantInterval,
50
- isPerfectInterval,
51
- } from './core/interval/index.js';
52
- export type { BarPosition, TimeSignature } from './core/meter/index.js';
53
- export {
54
- barPositionToBeat,
55
- beatsPerBar,
56
- beatToBarPosition,
57
- formatTimeSignature,
58
- isCompound,
59
- isStrongBeat,
60
- metricWeight,
61
- parseTimeSignature,
62
- pulsesPerBar,
63
- tuplet,
64
- } from './core/meter/index.js';
65
- export type { Note as NoteData, SpelledInterval } from './core/pitch/index.js';
66
- export {
67
- formatNote,
68
- midiToNote,
69
- noteToMidi,
70
- noteToPitchClass,
71
- parseNote,
72
- spelledInterval,
73
- } from './core/pitch/index.js';
74
- export type { Rng } from './core/random/index.js';
75
- export { createRng } from './core/random/index.js';
76
- export type { Tuning } from './core/tuning/index.js';
77
- export {
78
- centsBetweenFreq,
79
- centsOfSteps,
80
- edo,
81
- frequencyOf,
82
- JUST_RATIOS,
83
- justDeviationCents,
84
- nearestStep,
85
- ratioToCents,
86
- TWELVE_TET,
87
- } from './core/tuning/index.js';
88
- export type { KeyScale, NoteEvent } from './core/types.js';
89
- export type { BassLineOptions, BassSegment, BassStyle } from './generate/bass/index.js';
90
- export { generateBassLine } from './generate/bass/index.js';
91
- export type { CounterMelodyOptions } from './generate/countermelody/index.js';
92
- export { generateCounterMelody } from './generate/countermelody/index.js';
93
- export type {
94
- DrumHit,
95
- DrumRole,
96
- DrumsOptions,
97
- EuclideanKick,
98
- GrooveFeel,
99
- GrooveStyle,
100
- Section,
101
- } from './generate/drums/index.js';
102
- export { generateDrums } from './generate/drums/index.js';
103
- export type { GrooveSlot, GrooveTemplate, HumanizeOptions } from './generate/groove/index.js';
104
- export { applyGrooveTemplate, extractGrooveTemplate, humanize } from './generate/groove/index.js';
105
- export type {
106
- HarmonizeOptions,
107
- HarmonizeResult,
108
- MelodyNote,
109
- } from './generate/harmonize/index.js';
110
- export { harmonizeMelody } from './generate/harmonize/index.js';
111
- export type {
112
- MotifCell,
113
- MotifContour,
114
- MotifNote,
115
- MotifOptions,
116
- MotifTransform,
117
- } from './generate/motif/index.js';
118
- export { developMotif, generateMotif, transformMotif } from './generate/motif/index.js';
119
- export type {
120
- ChordSpan,
121
- ProgFunction,
122
- ProgressionOptions,
123
- ProgressionPreset,
124
- ProgStyle,
125
- } from './generate/progression/index.js';
126
- export {
127
- generateProgression,
128
- progressions,
129
- progressionsByStyle,
130
- } from './generate/progression/index.js';
131
- export type {
132
- BorrowedChord,
133
- SubstituteOptions,
134
- Substitution,
135
- SubstitutionType,
136
- } from './generate/reharmony/index.js';
137
- export {
138
- modalInterchangePalette,
139
- negativeHarmonyMirror,
140
- substituteChord,
141
- } from './generate/reharmony/index.js';
142
- export type { RhythmEvent, RhythmOptions } from './generate/rhythm/index.js';
143
- export { generateRhythm, onsetWeightCurve, rhythmDensity } from './generate/rhythm/index.js';
144
- export { Chord, Interval, Key, Note, Progression } from './model/index.js';
145
- export type { Chord as ChordData, ChordQuality } from './theory/chord/index.js';
146
- export {
147
- chordFromDegree,
148
- chordPitchClasses,
149
- chordQualities,
150
- chordToneRole,
151
- diatonicSeventh,
152
- diatonicTriad,
153
- makeChord,
154
- } from './theory/chord/index.js';
155
- export type {
156
- ChordScaleMatch,
157
- ChordScaleReportEntry,
158
- ScaleChoice,
159
- } from './theory/chordscale/index.js';
160
- export {
161
- availableTensions,
162
- avoidNotes,
163
- chordScaleReport,
164
- chordScales,
165
- scaleMatchesChord,
166
- scalesForChanges,
167
- } from './theory/chordscale/index.js';
168
- export {
169
- createsHiddenParallelPerfect,
170
- createsParallelOctave,
171
- createsParallelPerfect,
172
- createsParallelUnison,
173
- createsVerticalDissonance,
174
- createsVoiceCrossing,
175
- createsVoiceOverlap,
176
- exceedsSpacing,
177
- isForbiddenMelodicLeap,
178
- isLeadingToneResolution,
179
- } from './theory/counterpoint/index.js';
180
- export type { HarmonyRole, LockLevel, VoicedRole } from './theory/harmony/index.js';
181
- export { roleOf } from './theory/harmony/index.js';
182
- export type {
183
- SafetyProfile,
184
- SafetyQuery,
185
- SafetyResult,
186
- VoiceSnapshot,
187
- } from './theory/safety/index.js';
188
- export {
189
- enumerateSafePitches,
190
- evaluateSafety,
191
- NoteSafety,
192
- ReasonFlag,
193
- } from './theory/safety/index.js';
194
- export {
195
- BLUES_MASK,
196
- CHROMATIC_MASK,
197
- DORIAN_MASK,
198
- diatonicPitchClasses,
199
- HARMONIC_MINOR_MASK,
200
- isScaleTone,
201
- LOCRIAN_MASK,
202
- LYDIAN_MASK,
203
- MAJOR_MASK,
204
- MAJOR_PENTATONIC_MASK,
205
- MELODIC_MINOR_MASK,
206
- MINOR_PENTATONIC_MASK,
207
- MIXOLYDIAN_MASK,
208
- majorKey,
209
- maskFromOffsets,
210
- minorKey,
211
- NAMED_SCALES,
212
- NATURAL_MINOR_MASK,
213
- nearestScaleTone,
214
- OCTATONIC_HALF_WHOLE_MASK,
215
- OCTATONIC_WHOLE_HALF_MASK,
216
- PHRYGIAN_MASK,
217
- pitchToScaleDegree,
218
- scaleByName,
219
- scaleTonesInDegreeOrder,
220
- WHOLE_TONE_MASK,
221
- } from './theory/scale/index.js';
222
- export {
223
- noteNames,
224
- spellChord,
225
- spellPitchClass,
226
- spellPitchClasses,
227
- spellScale,
228
- } from './theory/spelling/index.js';
229
- export {
230
- formatChordSymbol,
231
- parseChordSymbol,
232
- transposeChordSymbol,
233
- } from './theory/symbol/index.js';
234
- export type {
235
- StyledVoicingOptions,
236
- VoiceRange,
237
- VoicingOptions,
238
- VoicingStyle,
239
- } from './theory/voicing/index.js';
240
- export {
241
- nextVoicing,
242
- SATB_RANGES,
243
- voiceChord,
244
- voiceChordStyled,
245
- voiceLeadingCost,
246
- voiceProgression,
247
- } from './theory/voicing/index.js';
1
+ /**
2
+ * Complete public API.
3
+ *
4
+ * Each layer barrel is the canonical source of that layer's runtime and type
5
+ * exports. The package root composes those barrels so a symbol added to a
6
+ * public layer cannot silently disappear from the root entry point.
7
+ */
8
+
9
+ export * from './analyze/index.js';
10
+ export * from './core/index.js';
11
+ export * from './generate/index.js';
12
+ export * from './model/index.js';
13
+ export * from './theory/index.js';