@libraz/libcantus 0.9.1 → 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 (160) hide show
  1. package/README.md +19 -3
  2. package/dist/analyze/index.cjs +1970 -0
  3. package/dist/analyze/index.cjs.map +1 -0
  4. package/dist/analyze/index.d.cts +341 -0
  5. package/dist/analyze/index.d.ts +341 -0
  6. package/dist/analyze/index.js +51 -0
  7. package/dist/analyze/index.js.map +1 -0
  8. package/dist/chunk-2ZWMSJH6.js +30 -0
  9. package/dist/chunk-2ZWMSJH6.js.map +1 -0
  10. package/dist/chunk-4MHA432G.js +2854 -0
  11. package/dist/chunk-4MHA432G.js.map +1 -0
  12. package/dist/chunk-62QZKCJQ.js +49 -0
  13. package/dist/chunk-62QZKCJQ.js.map +1 -0
  14. package/dist/chunk-AKLYPWIN.js +83 -0
  15. package/dist/chunk-AKLYPWIN.js.map +1 -0
  16. package/dist/chunk-C5XYKUVG.js +904 -0
  17. package/dist/chunk-C5XYKUVG.js.map +1 -0
  18. package/dist/chunk-CHMXELDS.js +549 -0
  19. package/dist/chunk-CHMXELDS.js.map +1 -0
  20. package/dist/chunk-DYWFNC4A.js +152 -0
  21. package/dist/chunk-DYWFNC4A.js.map +1 -0
  22. package/dist/chunk-EH6EM37U.js +315 -0
  23. package/dist/chunk-EH6EM37U.js.map +1 -0
  24. package/dist/chunk-FBQK3ZQN.js +131 -0
  25. package/dist/chunk-FBQK3ZQN.js.map +1 -0
  26. package/dist/chunk-GOAW4VYL.js +719 -0
  27. package/dist/chunk-GOAW4VYL.js.map +1 -0
  28. package/dist/chunk-I5R7ZBJS.js +202 -0
  29. package/dist/chunk-I5R7ZBJS.js.map +1 -0
  30. package/dist/chunk-IT2O4XFE.js +154 -0
  31. package/dist/chunk-IT2O4XFE.js.map +1 -0
  32. package/dist/chunk-MIWO425M.js +412 -0
  33. package/dist/chunk-MIWO425M.js.map +1 -0
  34. package/dist/chunk-N6IHQUOX.js +1 -0
  35. package/dist/chunk-N6IHQUOX.js.map +1 -0
  36. package/dist/chunk-SGU7SPLK.js +924 -0
  37. package/dist/chunk-SGU7SPLK.js.map +1 -0
  38. package/dist/core/index.cjs +648 -0
  39. package/dist/core/index.cjs.map +1 -0
  40. package/dist/core/index.d.cts +262 -0
  41. package/dist/core/index.d.ts +262 -0
  42. package/dist/core/index.js +93 -0
  43. package/dist/core/index.js.map +1 -0
  44. package/dist/function-DVH4Eusr.d.ts +187 -0
  45. package/dist/function-DYt4iKsn.d.cts +187 -0
  46. package/dist/generate/index.cjs +4123 -0
  47. package/dist/generate/index.cjs.map +1 -0
  48. package/dist/generate/index.d.cts +986 -0
  49. package/dist/generate/index.d.ts +986 -0
  50. package/dist/generate/index.js +52 -0
  51. package/dist/generate/index.js.map +1 -0
  52. package/dist/index-BNlGP86A.d.cts +157 -0
  53. package/dist/index-Bg5dYMmZ.d.ts +56 -0
  54. package/dist/index-Bt4tOUD9.d.ts +157 -0
  55. package/dist/index-CGMZNCx6.d.cts +140 -0
  56. package/dist/index-CGMZNCx6.d.ts +140 -0
  57. package/dist/index-Cl7mhOFM.d.cts +164 -0
  58. package/dist/index-Cl7mhOFM.d.ts +164 -0
  59. package/dist/index-CnZCo29u.d.cts +108 -0
  60. package/dist/index-CpOsP1y9.d.cts +151 -0
  61. package/dist/index-CxqBQtH1.d.cts +128 -0
  62. package/dist/index-Dikst44v.d.cts +56 -0
  63. package/dist/index-DxvARIP2.d.ts +151 -0
  64. package/dist/index-Jc0Ryi79.d.ts +128 -0
  65. package/dist/index-JnF4v9bx.d.ts +108 -0
  66. package/dist/index.cjs +3710 -3143
  67. package/dist/index.cjs.map +1 -1
  68. package/dist/index.d.cts +15 -3982
  69. package/dist/index.d.ts +15 -3982
  70. package/dist/index.js +191 -6504
  71. package/dist/index.js.map +1 -1
  72. package/dist/model/index.cjs +2574 -0
  73. package/dist/model/index.cjs.map +1 -0
  74. package/dist/model/index.d.cts +631 -0
  75. package/dist/model/index.d.ts +631 -0
  76. package/dist/model/index.js +21 -0
  77. package/dist/model/index.js.map +1 -0
  78. package/dist/styled-DZ7GjShb.d.ts +282 -0
  79. package/dist/styled-ZTbdaVFM.d.cts +282 -0
  80. package/dist/theory/index.cjs +1823 -0
  81. package/dist/theory/index.cjs.map +1 -0
  82. package/dist/theory/index.d.cts +535 -0
  83. package/dist/theory/index.d.ts +535 -0
  84. package/dist/theory/index.js +150 -0
  85. package/dist/theory/index.js.map +1 -0
  86. package/dist/types-MGP29r3t.d.cts +30 -0
  87. package/dist/types-MGP29r3t.d.ts +30 -0
  88. package/package.json +61 -5
  89. package/src/analyze/arrange/index.ts +21 -0
  90. package/src/analyze/arrange/internal.ts +127 -0
  91. package/src/analyze/arrange/tension.ts +199 -0
  92. package/src/analyze/arrange/tracks.ts +312 -0
  93. package/src/{detect → analyze/detect}/index.ts +50 -13
  94. package/src/analyze/functional/borrowed.ts +84 -0
  95. package/src/analyze/functional/cadence.ts +66 -0
  96. package/src/analyze/functional/function.ts +184 -0
  97. package/src/analyze/functional/index.ts +23 -0
  98. package/src/analyze/functional/internal.ts +29 -0
  99. package/src/{functional/index.ts → analyze/functional/roman.ts} +23 -332
  100. package/src/analyze/index.ts +49 -0
  101. package/src/{timeline → analyze/timeline}/index.ts +50 -18
  102. package/src/{analysis → analyze/voice}/index.ts +19 -7
  103. package/src/core/event-index/index.ts +104 -0
  104. package/src/core/index.ts +61 -0
  105. package/src/{meter → core/meter}/index.ts +24 -4
  106. package/src/{random → core/random}/index.ts +26 -3
  107. package/src/{tuning → core/tuning}/index.ts +36 -6
  108. package/src/core/validation/index.ts +119 -0
  109. package/src/{bass → generate/bass}/index.ts +28 -5
  110. package/src/{countermelody → generate/countermelody}/index.ts +176 -85
  111. package/src/{drums → generate/drums}/euclid.ts +10 -3
  112. package/src/{drums → generate/drums}/index.ts +58 -26
  113. package/src/{drums → generate/drums}/percussion.ts +1 -1
  114. package/src/{drums → generate/drums}/rng.ts +2 -2
  115. package/src/{groove → generate/groove}/index.ts +28 -3
  116. package/src/{harmonize → generate/harmonize}/index.ts +68 -57
  117. package/src/generate/index.ts +58 -0
  118. package/src/{motif → generate/motif}/index.ts +52 -28
  119. package/src/{progression → generate/progression}/index.ts +19 -41
  120. package/src/{reharmony → generate/reharmony}/index.ts +12 -19
  121. package/src/{rhythm → generate/rhythm}/index.ts +13 -3
  122. package/src/index.ts +13 -234
  123. package/src/model/chord.ts +442 -0
  124. package/src/model/index.ts +5 -1096
  125. package/src/model/interval.ts +82 -0
  126. package/src/model/key.ts +266 -0
  127. package/src/model/note.ts +176 -0
  128. package/src/model/progression.ts +161 -0
  129. package/src/model/shared.ts +12 -0
  130. package/src/{chord → theory/chord}/index.ts +23 -3
  131. package/src/{counterpoint → theory/counterpoint}/index.ts +2 -2
  132. package/src/{harmony → theory/harmony}/index.ts +6 -21
  133. package/src/theory/index.ts +112 -0
  134. package/src/{safety → theory/safety}/index.ts +72 -3
  135. package/src/theory/scale/degrees.ts +123 -0
  136. package/src/theory/scale/index.ts +28 -0
  137. package/src/theory/scale/key.ts +56 -0
  138. package/src/theory/scale/masks.ts +153 -0
  139. package/src/{spelling → theory/spelling}/index.ts +73 -6
  140. package/src/{symbol → theory/symbol}/index.ts +8 -1
  141. package/src/theory/voicing/index.ts +5 -0
  142. package/src/theory/voicing/internal.ts +168 -0
  143. package/src/theory/voicing/leading.ts +112 -0
  144. package/src/theory/voicing/satb.ts +216 -0
  145. package/src/theory/voicing/styled.ts +199 -0
  146. package/src/arrange/index.ts +0 -571
  147. package/src/scale/index.ts +0 -331
  148. package/src/voicing/index.ts +0 -623
  149. /package/src/{interval → core/interval}/index.ts +0 -0
  150. /package/src/{pitch → core/pitch}/index.ts +0 -0
  151. /package/src/{types.ts → core/types.ts} +0 -0
  152. /package/src/{drums → generate/drums}/beat.ts +0 -0
  153. /package/src/{drums → generate/drums}/fills.ts +0 -0
  154. /package/src/{drums → generate/drums}/ghost.ts +0 -0
  155. /package/src/{drums → generate/drums}/hihat.ts +0 -0
  156. /package/src/{drums → generate/drums}/hit.ts +0 -0
  157. /package/src/{drums → generate/drums}/internal.ts +0 -0
  158. /package/src/{drums → generate/drums}/kick.ts +0 -0
  159. /package/src/{drums → generate/drums}/swing.ts +0 -0
  160. /package/src/{chordscale → theory/chordscale}/index.ts +0 -0
@@ -1,18 +1,22 @@
1
- import type { Chord, ChordQuality } from '../chord/index.js';
2
- import { chordPitchClasses, diatonicTriad, makeChord } from '../chord/index.js';
3
- import type { HarmonyRole } from '../harmony/index.js';
4
- import { roleOf } from '../harmony/index.js';
5
- import type { TimeSignature } from '../meter/index.js';
6
- import { isStrongBeat } from '../meter/index.js';
7
- import type { GeneratedChord } from '../progression/index.js';
1
+ import { isDiatonic, parallelKey } from '../../analyze/functional/index.js';
2
+ import { createNoteEventIndex } from '../../core/event-index/index.js';
3
+ import type { TimeSignature } from '../../core/meter/index.js';
4
+ import { isStrongBeat } from '../../core/meter/index.js';
5
+ import { createRng } from '../../core/random/index.js';
6
+ import type { KeyScale, NoteEvent } from '../../core/types.js';
7
+ import { assertGenerationBudget, assertRange } from '../../core/validation/index.js';
8
+ import type { Chord, ChordQuality } from '../../theory/chord/index.js';
9
+ import { chordPitchClasses, diatonicTriad, makeChord } from '../../theory/chord/index.js';
10
+ import type { HarmonyRole } from '../../theory/harmony/index.js';
11
+ import { roleOf } from '../../theory/harmony/index.js';
8
12
  import {
9
13
  isScaleTone,
10
14
  majorKey,
11
15
  minorKey,
12
16
  NATURAL_MINOR_MASK,
13
17
  scaleTonesInDegreeOrder,
14
- } from '../scale/index.js';
15
- import type { KeyScale, NoteEvent } from '../types.js';
18
+ } from '../../theory/scale/index.js';
19
+ import type { ChordSpan } from '../progression/index.js';
16
20
 
17
21
  /**
18
22
  * A melody note supplied to the harmonizer: a {@link NoteEvent}.
@@ -48,7 +52,7 @@ export type HarmonizeOptions = {
48
52
  export type HarmonizeResult = {
49
53
  transposeSemitones: number;
50
54
  key: KeyScale;
51
- chords: GeneratedChord[];
55
+ chords: ChordSpan[];
52
56
  melodyRoles: { noteIndex: number; role: HarmonyRole }[];
53
57
  };
54
58
 
@@ -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
  /**
@@ -356,29 +351,45 @@ function harmonizeOnce(
356
351
  * reharmonize: 'diatonic',
357
352
  * placement: { transposeSearch: false, octaveSearch: false },
358
353
  * });
359
- * result.chords; // one GeneratedChord per harmonic-rhythm segment
354
+ * result.chords; // one ChordSpan per harmonic-rhythm segment
360
355
  * ```
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];
@@ -413,9 +424,9 @@ export function harmonizeMelody(opts: HarmonizeOptions): HarmonizeResult {
413
424
  }
414
425
  }
415
426
 
416
- const chords: GeneratedChord[] = bestPath.map((ci, s) => {
427
+ const chords: ChordSpan[] = bestPath.map((ci, s) => {
417
428
  const cand = candAt(ci);
418
- const chord: GeneratedChord = {
429
+ const chord: ChordSpan = {
419
430
  rootPc: cand.rootPc,
420
431
  quality: cand.quality,
421
432
  startBeat: segments[s]?.startBeat ?? s * hr,
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Public API of the generate layer. Re-exports every generate-layer module;
3
+ * also available from the package root.
4
+ */
5
+ export type { BassLineOptions, BassSegment, BassStyle } from './bass/index.js';
6
+ export { generateBassLine } from './bass/index.js';
7
+ export type { CounterMelodyOptions } from './countermelody/index.js';
8
+ export { generateCounterMelody } from './countermelody/index.js';
9
+ export type {
10
+ DrumHit,
11
+ DrumRole,
12
+ DrumsOptions,
13
+ EuclideanKick,
14
+ GrooveFeel,
15
+ GrooveStyle,
16
+ Section,
17
+ } from './drums/index.js';
18
+ export { generateDrums } from './drums/index.js';
19
+ export type { GrooveSlot, GrooveTemplate, HumanizeOptions } from './groove/index.js';
20
+ export { applyGrooveTemplate, extractGrooveTemplate, humanize } from './groove/index.js';
21
+ export type {
22
+ HarmonizeOptions,
23
+ HarmonizeResult,
24
+ MelodyNote,
25
+ } from './harmonize/index.js';
26
+ export { harmonizeMelody } from './harmonize/index.js';
27
+ export type {
28
+ MotifCell,
29
+ MotifContour,
30
+ MotifNote,
31
+ MotifOptions,
32
+ MotifTransform,
33
+ } from './motif/index.js';
34
+ export { developMotif, generateMotif, transformMotif } from './motif/index.js';
35
+ export type {
36
+ ProgFunction,
37
+ ProgressionOptions,
38
+ ProgressionPreset,
39
+ ProgStyle,
40
+ } from './progression/index.js';
41
+ export {
42
+ generateProgression,
43
+ progressions,
44
+ progressionsByStyle,
45
+ } from './progression/index.js';
46
+ export type {
47
+ BorrowedChord,
48
+ SubstituteOptions,
49
+ Substitution,
50
+ SubstitutionType,
51
+ } from './reharmony/index.js';
52
+ export {
53
+ modalInterchangePalette,
54
+ negativeHarmonyMirror,
55
+ substituteChord,
56
+ } from './reharmony/index.js';
57
+ export type { RhythmEvent, RhythmOptions } from './rhythm/index.js';
58
+ export { generateRhythm, onsetWeightCurve, rhythmDensity } from './rhythm/index.js';
@@ -1,8 +1,16 @@
1
- import type { Chord } from '../chord/index.js';
2
- import { chordPitchClasses } from '../chord/index.js';
3
- import { isScaleTone, nearestScaleTone } from '../scale/index.js';
4
- import type { ChordTimeline } from '../timeline/index.js';
5
- import type { KeyScale } from '../types.js';
1
+ import type { ChordTimeline } from '../../analyze/timeline/index.js';
2
+ import { createRng } from '../../core/random/index.js';
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';
11
+ import type { Chord } from '../../theory/chord/index.js';
12
+ import { chordPitchClasses } from '../../theory/chord/index.js';
13
+ import { isScaleTone, nearestScaleTone } from '../../theory/scale/index.js';
6
14
 
7
15
  /**
8
16
  * A transformation applicable to a motif cell.
@@ -50,7 +58,7 @@ export type MotifContour = 'arch' | 'ascending' | 'descending' | 'wave';
50
58
  *
51
59
  * @category Composition
52
60
  */
53
- export type GenerateMotifOptions = {
61
+ export type MotifOptions = {
54
62
  key: KeyScale;
55
63
  chord?: Chord | null;
56
64
  bars: number;
@@ -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[] = [];
@@ -207,14 +216,16 @@ function contourOffsets(contour: MotifContour, count: number): number[] {
207
216
  *
208
217
  * @category Composition
209
218
  */
210
- export function generateMotif(opts: GenerateMotifOptions): MotifCell {
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: GenerateMotifOptions): 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,7 +1,11 @@
1
- import type { ChordQuality } from '../chord/index.js';
2
- import { diatonicTriad } from '../chord/index.js';
3
- import { scaleTonesInDegreeOrder } from '../scale/index.js';
4
- import type { KeyScale } from '../types.js';
1
+ import { createRng } from '../../core/random/index.js';
2
+ import type { KeyScale } from '../../core/types.js';
3
+ import { assertGenerationBudget, assertPositiveInt } from '../../core/validation/index.js';
4
+ import type { ChordQuality, ChordSpan } from '../../theory/chord/index.js';
5
+ import { diatonicTriad } from '../../theory/chord/index.js';
6
+ import { scaleTonesInDegreeOrder } from '../../theory/scale/index.js';
7
+
8
+ export type { ChordSpan } from '../../theory/chord/index.js';
5
9
 
6
10
  /**
7
11
  * Broad production style a progression preset suits.
@@ -30,28 +34,12 @@ export type ProgressionPreset = {
30
34
  styles: ProgStyle[];
31
35
  };
32
36
 
33
- /**
34
- * A chord placed on the timeline by {@link generateProgression}.
35
- *
36
- * @category Composition
37
- */
38
- export type GeneratedChord = {
39
- rootPc: number;
40
- quality: ChordQuality;
41
- startBeat: number;
42
- bassPc?: number;
43
- /** 0-based scale degree of the chord root, when known. */
44
- degree?: number;
45
- /** True when the chord is a secondary dominant tonicizing another degree. */
46
- secondaryDominant?: boolean;
47
- };
48
-
49
37
  /**
50
38
  * Options controlling {@link generateProgression}.
51
39
  *
52
40
  * @category Composition
53
41
  */
54
- export type GenerateProgressionOptions = {
42
+ export type ProgressionOptions = {
55
43
  key: KeyScale;
56
44
  style: ProgStyle;
57
45
  bars: number;
@@ -251,18 +239,6 @@ export function progressionsByStyle(style: ProgStyle): ProgressionPreset[] {
251
239
  return progressions().filter((p) => p.styles.includes(style));
252
240
  }
253
241
 
254
- /** Deterministic 32-bit PRNG producing a float in [0, 1). */
255
- function mulberry32(seed: number): () => number {
256
- let state = seed >>> 0;
257
- return () => {
258
- state = (state + 0x6d2b79f5) >>> 0;
259
- let t = state;
260
- t = Math.imul(t ^ (t >>> 15), t | 1);
261
- t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
262
- return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
263
- };
264
- }
265
-
266
242
  /** Root pitch class of a scale degree in the given key, including borrowed degrees. */
267
243
  function degreeToRootPc(degree: number, key: KeyScale): number {
268
244
  if (degree >= 0 && degree <= 6) {
@@ -315,12 +291,14 @@ function autoQuality(degree: number, key: KeyScale): ChordQuality {
315
291
  * ```ts
316
292
  * import { generateProgression, majorKey } from '@libraz/libcantus';
317
293
  * const chords = generateProgression({ key: majorKey(0), style: 'dance', bars: 4 });
318
- * // Deterministic for a given seed (defaults to 0); one GeneratedChord per bar.
294
+ * // Deterministic for a given seed (defaults to 0); one ChordSpan per bar.
319
295
  * ```
320
296
  *
321
297
  * @category Composition
322
298
  */
323
- export function generateProgression(opts: GenerateProgressionOptions): GeneratedChord[] {
299
+ export function generateProgression(opts: ProgressionOptions): ChordSpan[] {
300
+ assertPositiveInt(opts.bars, 'progression bars');
301
+ assertGenerationBudget(opts.bars, 'progression chords');
324
302
  const seed = opts.seed ?? 0;
325
303
  let preset: ProgressionPreset | undefined;
326
304
  if (opts.presetId !== undefined) {
@@ -332,17 +310,17 @@ export function generateProgression(opts: GenerateProgressionOptions): Generated
332
310
  if (preset === undefined) {
333
311
  const pool = PRESETS.filter((p) => p.styles.includes(opts.style));
334
312
  const candidates = pool.length > 0 ? pool : PRESETS;
335
- const rng = mulberry32(seed);
336
- 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;
337
315
  preset = candidates[index] ?? PRESETS[0];
338
316
  }
339
317
  const degrees = preset?.degrees ?? [0];
340
- const chords: GeneratedChord[] = [];
318
+ const chords: ChordSpan[] = [];
341
319
  for (let bar = 0; bar < opts.bars; bar += 1) {
342
320
  const degree = degrees[bar % degrees.length] ?? 0;
343
321
  const quality =
344
322
  opts.ext !== undefined && opts.ext !== 'auto' ? opts.ext : autoQuality(degree, opts.key);
345
- const chord: GeneratedChord = {
323
+ const chord: ChordSpan = {
346
324
  rootPc: degreeToRootPc(degree, opts.key),
347
325
  quality,
348
326
  startBeat: bar * 4,
@@ -353,7 +331,7 @@ export function generateProgression(opts: GenerateProgressionOptions): Generated
353
331
 
354
332
  if (opts.reharmonize) {
355
333
  const tonicPc = (((opts.key.rootPc % 12) + 12) % 12) as number;
356
- const rrng = mulberry32((seed ^ 0x9e3779b9) >>> 0);
334
+ const rrng = createRng((seed ^ 0x9e3779b9) >>> 0);
357
335
  for (let i = 0; i < chords.length - 1; i += 1) {
358
336
  const cur = chords[i];
359
337
  const next = chords[i + 1];
@@ -369,7 +347,7 @@ export function generateProgression(opts: GenerateProgressionOptions): Generated
369
347
  if (targetsTonic || alreadySecondary || isResolutionTarget) {
370
348
  continue;
371
349
  }
372
- if (rrng() < 0.5) {
350
+ if (rrng.next() < 0.5) {
373
351
  chords[i] = {
374
352
  rootPc: domRoot,
375
353
  quality: 'dom7',
@@ -7,14 +7,7 @@
7
7
  * Roots are pitch classes (0..11); no spelled key signature is required.
8
8
  */
9
9
 
10
- import {
11
- type Chord,
12
- type ChordQuality,
13
- chordPitchClasses,
14
- diatonicTriad,
15
- makeChord,
16
- } from '../chord/index.js';
17
- import { detectChordBest } from '../detect/index.js';
10
+ import { detectChordBest } from '../../analyze/detect/index.js';
18
11
  import {
19
12
  type BorrowedSource,
20
13
  borrowedSource,
@@ -23,9 +16,16 @@ import {
23
16
  type HarmonicFunction,
24
17
  isDiatonic,
25
18
  parallelKey,
26
- } from '../functional/index.js';
27
- import { scaleTonesInDegreeOrder } from '../scale/index.js';
28
- import type { KeyScale } from '../types.js';
19
+ } from '../../analyze/functional/index.js';
20
+ import type { KeyScale } from '../../core/types.js';
21
+ import {
22
+ type Chord,
23
+ type ChordQuality,
24
+ chordPitchClasses,
25
+ diatonicTriad,
26
+ makeChord,
27
+ } from '../../theory/chord/index.js';
28
+ import { scaleTonesInDegreeOrder } from '../../theory/scale/index.js';
29
29
 
30
30
  /** Reduce a value to a pitch class in [0, 11]. */
31
31
  function mod12(n: number): number {
@@ -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.
@@ -11,8 +11,13 @@
11
11
  * library-wide convention.
12
12
  */
13
13
 
14
- import { beatsPerBar, metricWeight, type TimeSignature } from '../meter/index.js';
15
- import { createRng } from '../random/index.js';
14
+ import { beatsPerBar, metricWeight, type TimeSignature } from '../../core/meter/index.js';
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