@libraz/libcantus 0.9.0 → 0.9.2

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 (155) hide show
  1. package/README.md +25 -2
  2. package/dist/analyze/index.cjs +1697 -0
  3. package/dist/analyze/index.cjs.map +1 -0
  4. package/dist/analyze/index.d.cts +429 -0
  5. package/dist/analyze/index.d.ts +429 -0
  6. package/dist/analyze/index.js +51 -0
  7. package/dist/analyze/index.js.map +1 -0
  8. package/dist/chunk-5HTNMQW4.js +653 -0
  9. package/dist/chunk-5HTNMQW4.js.map +1 -0
  10. package/dist/chunk-7Y7WF64D.js +410 -0
  11. package/dist/chunk-7Y7WF64D.js.map +1 -0
  12. package/dist/chunk-A4532ZFR.js +259 -0
  13. package/dist/chunk-A4532ZFR.js.map +1 -0
  14. package/dist/chunk-BHUT2S5G.js +22 -0
  15. package/dist/chunk-BHUT2S5G.js.map +1 -0
  16. package/dist/chunk-CCBLVAPK.js +42 -0
  17. package/dist/chunk-CCBLVAPK.js.map +1 -0
  18. package/dist/chunk-COKTX2G5.js +907 -0
  19. package/dist/chunk-COKTX2G5.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-GQKN5KDT.js +159 -0
  23. package/dist/chunk-GQKN5KDT.js.map +1 -0
  24. package/dist/chunk-LN7ZZMD2.js +530 -0
  25. package/dist/chunk-LN7ZZMD2.js.map +1 -0
  26. package/dist/chunk-MSWFZF7J.js +118 -0
  27. package/dist/chunk-MSWFZF7J.js.map +1 -0
  28. package/dist/chunk-R3S2LUBH.js +2722 -0
  29. package/dist/chunk-R3S2LUBH.js.map +1 -0
  30. package/dist/chunk-SDM3VJJ5.js +41 -0
  31. package/dist/chunk-SDM3VJJ5.js.map +1 -0
  32. package/dist/chunk-XTC3X6MS.js +816 -0
  33. package/dist/chunk-XTC3X6MS.js.map +1 -0
  34. package/dist/chunk-ZXW5YIWK.js +55 -0
  35. package/dist/chunk-ZXW5YIWK.js.map +1 -0
  36. package/dist/core/index.cjs +432 -0
  37. package/dist/core/index.cjs.map +1 -0
  38. package/dist/core/index.d.cts +210 -0
  39. package/dist/core/index.d.ts +210 -0
  40. package/dist/core/index.js +73 -0
  41. package/dist/core/index.js.map +1 -0
  42. package/dist/function-2bJW1Zp_.d.ts +187 -0
  43. package/dist/function-r2sh-jOO.d.cts +187 -0
  44. package/dist/generate/index.cjs +3787 -0
  45. package/dist/generate/index.cjs.map +1 -0
  46. package/dist/generate/index.d.cts +980 -0
  47. package/dist/generate/index.d.ts +980 -0
  48. package/dist/generate/index.js +52 -0
  49. package/dist/generate/index.js.map +1 -0
  50. package/dist/index-BMPxwjwV.d.ts +151 -0
  51. package/dist/index-BOBXBNwP.d.cts +157 -0
  52. package/dist/index-BdeQ1r_p.d.cts +56 -0
  53. package/dist/index-BreoJRw0.d.ts +127 -0
  54. package/dist/index-CGMZNCx6.d.cts +140 -0
  55. package/dist/index-CGMZNCx6.d.ts +140 -0
  56. package/dist/index-CM5W4rcm.d.ts +157 -0
  57. package/dist/index-CP3wAfEH.d.ts +56 -0
  58. package/dist/index-Cl7mhOFM.d.cts +164 -0
  59. package/dist/index-Cl7mhOFM.d.ts +164 -0
  60. package/dist/index-DZ1M21do.d.cts +127 -0
  61. package/dist/index-D_ZTUNKp.d.cts +151 -0
  62. package/dist/index.cjs +5277 -4893
  63. package/dist/index.cjs.map +1 -1
  64. package/dist/index.d.cts +14 -2621
  65. package/dist/index.d.ts +14 -2621
  66. package/dist/index.js +170 -6152
  67. package/dist/index.js.map +1 -1
  68. package/dist/model/index.cjs +2435 -0
  69. package/dist/model/index.cjs.map +1 -0
  70. package/dist/model/index.d.cts +630 -0
  71. package/dist/model/index.d.ts +630 -0
  72. package/dist/model/index.js +21 -0
  73. package/dist/model/index.js.map +1 -0
  74. package/dist/styled-BjXtOFNk.d.cts +282 -0
  75. package/dist/styled-D7BAx3S3.d.ts +282 -0
  76. package/dist/theory/index.cjs +1677 -0
  77. package/dist/theory/index.cjs.map +1 -0
  78. package/dist/theory/index.d.cts +535 -0
  79. package/dist/theory/index.d.ts +535 -0
  80. package/dist/theory/index.js +149 -0
  81. package/dist/theory/index.js.map +1 -0
  82. package/dist/types-MGP29r3t.d.cts +30 -0
  83. package/dist/types-MGP29r3t.d.ts +30 -0
  84. package/package.json +31 -1
  85. package/src/analyze/arrange/index.ts +21 -0
  86. package/src/analyze/arrange/internal.ts +125 -0
  87. package/src/analyze/arrange/tension.ts +187 -0
  88. package/src/analyze/arrange/tracks.ts +279 -0
  89. package/src/{detect → analyze/detect}/index.ts +82 -23
  90. package/src/analyze/functional/borrowed.ts +84 -0
  91. package/src/analyze/functional/cadence.ts +66 -0
  92. package/src/analyze/functional/function.ts +184 -0
  93. package/src/analyze/functional/index.ts +23 -0
  94. package/src/analyze/functional/internal.ts +29 -0
  95. package/src/analyze/functional/roman.ts +393 -0
  96. package/src/analyze/index.ts +49 -0
  97. package/src/{timeline → analyze/timeline}/index.ts +87 -24
  98. package/src/{analysis → analyze/voice}/index.ts +81 -12
  99. package/src/core/index.ts +47 -0
  100. package/src/{interval → core/interval}/index.ts +15 -1
  101. package/src/{meter → core/meter}/index.ts +108 -5
  102. package/src/{pitch → core/pitch}/index.ts +58 -3
  103. package/src/{random → core/random}/index.ts +13 -1
  104. package/src/{tuning → core/tuning}/index.ts +28 -1
  105. package/src/{bass → generate/bass}/index.ts +66 -17
  106. package/src/{countermelody → generate/countermelody}/index.ts +54 -24
  107. package/src/{drums → generate/drums}/fills.ts +31 -20
  108. package/src/{drums → generate/drums}/index.ts +127 -9
  109. package/src/{drums → generate/drums}/internal.ts +7 -1
  110. package/src/{drums → generate/drums}/rng.ts +2 -2
  111. package/src/{drums → generate/drums}/swing.ts +7 -2
  112. package/src/{groove → generate/groove}/index.ts +118 -22
  113. package/src/{harmonize → generate/harmonize}/index.ts +96 -23
  114. package/src/generate/index.ts +59 -0
  115. package/src/{motif → generate/motif}/index.ts +94 -17
  116. package/src/{progression → generate/progression}/index.ts +61 -27
  117. package/src/{reharmony → generate/reharmony}/index.ts +46 -15
  118. package/src/{rhythm → generate/rhythm}/index.ts +47 -13
  119. package/src/index.ts +130 -116
  120. package/src/model/chord.ts +423 -0
  121. package/src/model/index.ts +5 -947
  122. package/src/model/interval.ts +82 -0
  123. package/src/model/key.ts +266 -0
  124. package/src/model/note.ts +176 -0
  125. package/src/model/progression.ts +161 -0
  126. package/src/model/shared.ts +12 -0
  127. package/src/{chord → theory/chord}/index.ts +85 -4
  128. package/src/{chordscale → theory/chordscale}/index.ts +67 -9
  129. package/src/{counterpoint → theory/counterpoint}/index.ts +23 -2
  130. package/src/{harmony → theory/harmony}/index.ts +19 -2
  131. package/src/theory/index.ts +107 -0
  132. package/src/{safety → theory/safety}/index.ts +59 -12
  133. package/src/theory/scale/degrees.ts +123 -0
  134. package/src/theory/scale/index.ts +28 -0
  135. package/src/theory/scale/key.ts +56 -0
  136. package/src/theory/scale/masks.ts +153 -0
  137. package/src/{spelling → theory/spelling}/index.ts +21 -3
  138. package/src/{symbol → theory/symbol}/index.ts +91 -36
  139. package/src/theory/voicing/index.ts +5 -0
  140. package/src/theory/voicing/internal.ts +168 -0
  141. package/src/theory/voicing/leading.ts +112 -0
  142. package/src/theory/voicing/satb.ts +195 -0
  143. package/src/theory/voicing/styled.ts +176 -0
  144. package/src/arrange/index.ts +0 -409
  145. package/src/functional/index.ts +0 -602
  146. package/src/scale/index.ts +0 -215
  147. package/src/voicing/index.ts +0 -523
  148. /package/src/{types.ts → core/types.ts} +0 -0
  149. /package/src/{drums → generate/drums}/beat.ts +0 -0
  150. /package/src/{drums → generate/drums}/euclid.ts +0 -0
  151. /package/src/{drums → generate/drums}/ghost.ts +0 -0
  152. /package/src/{drums → generate/drums}/hihat.ts +0 -0
  153. /package/src/{drums → generate/drums}/hit.ts +0 -0
  154. /package/src/{drums → generate/drums}/kick.ts +0 -0
  155. /package/src/{drums → generate/drums}/percussion.ts +0 -0
@@ -0,0 +1,59 @@
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
+ ChordSpan,
37
+ ProgFunction,
38
+ ProgressionOptions,
39
+ ProgressionPreset,
40
+ ProgStyle,
41
+ } from './progression/index.js';
42
+ export {
43
+ generateProgression,
44
+ progressions,
45
+ progressionsByStyle,
46
+ } from './progression/index.js';
47
+ export type {
48
+ BorrowedChord,
49
+ SubstituteOptions,
50
+ Substitution,
51
+ SubstitutionType,
52
+ } from './reharmony/index.js';
53
+ export {
54
+ modalInterchangePalette,
55
+ negativeHarmonyMirror,
56
+ substituteChord,
57
+ } from './reharmony/index.js';
58
+ export type { RhythmEvent, RhythmOptions } from './rhythm/index.js';
59
+ export { generateRhythm, onsetWeightCurve, rhythmDensity } from './rhythm/index.js';
@@ -1,10 +1,14 @@
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 type { KeyScale } from '../../core/types.js';
3
+ import type { Chord } from '../../theory/chord/index.js';
4
+ import { chordPitchClasses } from '../../theory/chord/index.js';
5
+ import { isScaleTone, nearestScaleTone } from '../../theory/scale/index.js';
6
6
 
7
- /** A transformation applicable to a motif cell. */
7
+ /**
8
+ * A transformation applicable to a motif cell.
9
+ *
10
+ * @category Composition
11
+ */
8
12
  export type MotifTransform =
9
13
  | 'transposeDiatonic'
10
14
  | 'transposeChromatic'
@@ -14,27 +18,62 @@ export type MotifTransform =
14
18
  | 'diminish'
15
19
  | 'sequence';
16
20
 
17
- /** A single note within a motif cell. */
21
+ /**
22
+ * A single note within a motif cell.
23
+ *
24
+ * @category Composition
25
+ */
18
26
  export type MotifNote = {
19
27
  pitch: number;
20
28
  startBeat: number;
21
29
  durationBeat: number;
22
30
  };
23
31
 
24
- /** A short melodic cell. */
32
+ /**
33
+ * A short melodic cell.
34
+ *
35
+ * @category Composition
36
+ */
25
37
  export type MotifCell = {
26
38
  notes: MotifNote[];
27
39
  };
28
40
 
29
- /** Melodic contour shape for {@link generateMotif}. */
41
+ /**
42
+ * Melodic contour shape for {@link generateMotif}.
43
+ *
44
+ * @category Composition
45
+ */
30
46
  export type MotifContour = 'arch' | 'ascending' | 'descending' | 'wave';
31
47
 
32
- /** Options controlling {@link generateMotif}. */
33
- export type GenerateMotifOptions = {
48
+ /**
49
+ * Options controlling {@link generateMotif}.
50
+ *
51
+ * @category Composition
52
+ */
53
+ export type MotifOptions = {
34
54
  key: KeyScale;
35
55
  chord?: Chord | null;
36
56
  bars: number;
57
+ /**
58
+ * Melodic contour shape the line follows.
59
+ *
60
+ * @defaultValue `'arch'`
61
+ */
37
62
  contour?: MotifContour;
63
+ /**
64
+ * Probability in [0, 1] that a note is nudged by a single diatonic step. The
65
+ * nudge direction is balanced (up or down with equal odds), so it adds
66
+ * variety without biasing the line off its contour. Default 0, which
67
+ * reproduces the requested contour exactly (no drift, tails return to tonic).
68
+ *
69
+ * @defaultValue 0
70
+ */
71
+ jitter?: number;
72
+ /**
73
+ * Seed for the deterministic PRNG.
74
+ *
75
+ * @defaultValue 0
76
+ */
38
77
  seed?: number;
39
78
  };
40
79
 
@@ -126,7 +165,6 @@ function mulberry32(seed: number): () => number {
126
165
  /** Diatonic scale-degree offsets shaping a contour of `count` notes. */
127
166
  function contourOffsets(contour: MotifContour, count: number): number[] {
128
167
  const offsets: number[] = [];
129
- const half = Math.floor(count / 2);
130
168
  for (let i = 0; i < count; i += 1) {
131
169
  switch (contour) {
132
170
  case 'ascending':
@@ -136,7 +174,9 @@ function contourOffsets(contour: MotifContour, count: number): number[] {
136
174
  offsets.push(-i);
137
175
  break;
138
176
  case 'arch':
139
- offsets.push(i <= half ? i : count - 1 - i);
177
+ // Symmetric rise-and-fall: a palindrome that starts and ends on the
178
+ // tonic (offset 0) for both odd and even lengths.
179
+ offsets.push(Math.min(i, count - 1 - i));
140
180
  break;
141
181
  case 'wave':
142
182
  offsets.push([0, 1, 0, -1][i % 4] ?? 0);
@@ -151,24 +191,42 @@ function contourOffsets(contour: MotifContour, count: number): number[] {
151
191
  *
152
192
  * The melody follows the requested contour in diatonic steps from the tonic,
153
193
  * snapped to the key. When a chord is supplied, notes landing on bar downbeats
154
- * are pulled to the nearest chord tone. Output is deterministic for a given seed.
194
+ * are pulled to the nearest chord tone. An optional `jitter` adds balanced
195
+ * per-note variation without biasing the contour; it is off by default, so the
196
+ * contour is reproduced exactly. Output is deterministic for a given seed.
155
197
  *
156
- * @param opts Key, optional chord, length, contour, and seed.
198
+ * @param opts Key, optional chord, length, contour, jitter, and seed.
157
199
  * @returns The generated motif cell.
200
+ *
201
+ * @example
202
+ * ```ts
203
+ * import { generateMotif, majorKey } from '@libraz/libcantus';
204
+ * const cell = generateMotif({ key: majorKey(0), bars: 2, contour: 'arch' });
205
+ * // cell.notes is a deterministic MotifCell (seed defaults to 0)
206
+ * ```
207
+ *
208
+ * @category Composition
158
209
  */
159
- export function generateMotif(opts: GenerateMotifOptions): MotifCell {
210
+ export function generateMotif(opts: MotifOptions): MotifCell {
160
211
  const contour = opts.contour ?? 'arch';
161
212
  const bars = Math.max(1, Math.trunc(opts.bars));
162
213
  const totalBeats = bars * 4;
163
214
  const noteCount = Math.max(3, bars * 2);
164
215
  const beatsPerNote = totalBeats / noteCount;
165
216
  const rng = mulberry32(opts.seed ?? 0);
217
+ const jitterProb = Math.min(1, Math.max(0, opts.jitter ?? 0));
166
218
  const tonic = pitchClass(opts.key.rootPc) + 60;
167
219
  const offsets = contourOffsets(contour, noteCount);
168
220
 
169
221
  const notes: MotifNote[] = [];
170
222
  for (let i = 0; i < noteCount; i += 1) {
171
- const jitter = rng() < 0.25 ? 1 : 0;
223
+ // Opt-in, direction-balanced jitter: disabled by default so the contour is
224
+ // preserved exactly. When enabled it nudges up or down with equal odds,
225
+ // avoiding the upward bias that used to drift arch/wave tails off the tonic.
226
+ let jitter = 0;
227
+ if (jitterProb > 0 && rng() < jitterProb) {
228
+ jitter = rng() < 0.5 ? 1 : -1;
229
+ }
172
230
  let pitch = stepDiatonic(tonic, (offsets[i] ?? 0) + jitter, opts.key);
173
231
  const startBeat = i * beatsPerNote;
174
232
  if (opts.chord && startBeat % 4 === 0) {
@@ -197,6 +255,15 @@ export function generateMotif(opts: GenerateMotifOptions): MotifCell {
197
255
  * @param key Key context for the diatonic transforms; without it,
198
256
  * `transposeDiatonic` and `sequence` shift chromatically by semitones.
199
257
  * @returns The transformed cell.
258
+ *
259
+ * @example
260
+ * ```ts
261
+ * import { generateMotif, transformMotif, majorKey } from '@libraz/libcantus';
262
+ * const cell = generateMotif({ key: majorKey(0), bars: 1 });
263
+ * const inverted = transformMotif(cell, 'invert');
264
+ * ```
265
+ *
266
+ * @category Composition
200
267
  */
201
268
  export function transformMotif(
202
269
  cell: MotifCell,
@@ -279,6 +346,16 @@ function scaleTime(cell: MotifCell, factor: number): MotifCell {
279
346
  * @param key Key context (used to keep snapped pitches sensible).
280
347
  * @param bars Number of four-beat bars to fill.
281
348
  * @returns The developed, harmony-aware cell.
349
+ *
350
+ * @example
351
+ * ```ts
352
+ * import { chordTimelineFromChords, developMotif, generateMotif, majorKey } from '@libraz/libcantus';
353
+ * const key = majorKey(0);
354
+ * const timeline = chordTimelineFromChords([{ rootPc: 0, quality: 'maj', startBeat: 0 }], 8);
355
+ * const developed = developMotif(generateMotif({ key, bars: 1 }), timeline, key, 2);
356
+ * ```
357
+ *
358
+ * @category Composition
282
359
  */
283
360
  export function developMotif(
284
361
  cell: MotifCell,
@@ -1,15 +1,29 @@
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 type { KeyScale } from '../../core/types.js';
2
+ import type { ChordQuality, ChordSpan } from '../../theory/chord/index.js';
3
+ import { diatonicTriad } from '../../theory/chord/index.js';
4
+ import { scaleTonesInDegreeOrder } from '../../theory/scale/index.js';
5
5
 
6
- /** Broad production style a progression preset suits. */
6
+ export type { ChordSpan } from '../../theory/chord/index.js';
7
+
8
+ /**
9
+ * Broad production style a progression preset suits.
10
+ *
11
+ * @category Composition
12
+ */
7
13
  export type ProgStyle = 'minimal' | 'dance' | 'idol' | 'rock';
8
14
 
9
- /** Harmonic-function role of a progression as a whole. */
15
+ /**
16
+ * Harmonic-function role of a progression as a whole.
17
+ *
18
+ * @category Composition
19
+ */
10
20
  export type ProgFunction = 'loop' | 'tensionBuild' | 'cadenceStrong' | 'stable';
11
21
 
12
- /** A named chord-progression preset expressed in scale degrees. */
22
+ /**
23
+ * A named chord-progression preset expressed in scale degrees.
24
+ *
25
+ * @category Composition
26
+ */
13
27
  export type ProgressionPreset = {
14
28
  id: string;
15
29
  name: string;
@@ -18,26 +32,23 @@ export type ProgressionPreset = {
18
32
  styles: ProgStyle[];
19
33
  };
20
34
 
21
- /** A chord placed on the timeline by {@link generateProgression}. */
22
- export type GeneratedChord = {
23
- rootPc: number;
24
- quality: ChordQuality;
25
- startBeat: number;
26
- bassPc?: number;
27
- /** 0-based scale degree of the chord root, when known. */
28
- degree?: number;
29
- /** True when the chord is a secondary dominant tonicizing another degree. */
30
- secondaryDominant?: boolean;
31
- };
32
-
33
- /** Options controlling {@link generateProgression}. */
34
- export type GenerateProgressionOptions = {
35
+ /**
36
+ * Options controlling {@link generateProgression}.
37
+ *
38
+ * @category Composition
39
+ */
40
+ export type ProgressionOptions = {
35
41
  key: KeyScale;
36
42
  style: ProgStyle;
37
43
  bars: number;
38
44
  presetId?: string;
39
45
  ext?: ChordQuality | 'auto';
40
46
  reharmonize?: boolean;
47
+ /**
48
+ * Seed for the deterministic preset choice and reharmonization.
49
+ *
50
+ * @defaultValue 0
51
+ */
41
52
  seed?: number;
42
53
  };
43
54
 
@@ -208,12 +219,20 @@ const BORROWED_OFFSET: Record<number, number> = {
208
219
  14: 6, // #IV
209
220
  };
210
221
 
211
- /** All built-in progression presets. */
222
+ /**
223
+ * All built-in progression presets.
224
+ *
225
+ * @category Composition
226
+ */
212
227
  export function progressions(): ProgressionPreset[] {
213
228
  return PRESETS.map((p) => ({ ...p, degrees: [...p.degrees], styles: [...p.styles] }));
214
229
  }
215
230
 
216
- /** Presets whose style list includes the given style. */
231
+ /**
232
+ * Presets whose style list includes the given style.
233
+ *
234
+ * @category Composition
235
+ */
217
236
  export function progressionsByStyle(style: ProgStyle): ProgressionPreset[] {
218
237
  return progressions().filter((p) => p.styles.includes(style));
219
238
  }
@@ -264,7 +283,9 @@ function autoQuality(degree: number, key: KeyScale): ChordQuality {
264
283
  * Generate a chord progression laid out one chord per bar.
265
284
  *
266
285
  * A preset is chosen by `presetId` when given, otherwise deterministically from
267
- * the presets matching `style`, seeded by `seed`. The preset's degrees cycle to
286
+ * the presets matching `style`, seeded by `seed`. An unknown `presetId` is a
287
+ * caller error and throws rather than silently falling back to a random preset.
288
+ * The preset's degrees cycle to
268
289
  * fill `bars`; each bar is four beats, so `startBeat` is `barIndex * 4`. Chord
269
290
  * roots come from the key's diatonic scale-degree mapping. When `ext` is
270
291
  * omitted or `'auto'`, each chord takes its diatonic triad quality; otherwise
@@ -274,12 +295,25 @@ function autoQuality(degree: number, key: KeyScale): ChordQuality {
274
295
  *
275
296
  * @param opts Generation options.
276
297
  * @returns One chord per bar in timeline order.
298
+ * @throws If `presetId` is given but does not match any built-in preset.
299
+ *
300
+ * @example
301
+ * ```ts
302
+ * import { generateProgression, majorKey } from '@libraz/libcantus';
303
+ * const chords = generateProgression({ key: majorKey(0), style: 'dance', bars: 4 });
304
+ * // Deterministic for a given seed (defaults to 0); one ChordSpan per bar.
305
+ * ```
306
+ *
307
+ * @category Composition
277
308
  */
278
- export function generateProgression(opts: GenerateProgressionOptions): GeneratedChord[] {
309
+ export function generateProgression(opts: ProgressionOptions): ChordSpan[] {
279
310
  const seed = opts.seed ?? 0;
280
311
  let preset: ProgressionPreset | undefined;
281
312
  if (opts.presetId !== undefined) {
282
313
  preset = PRESETS.find((p) => p.id === opts.presetId);
314
+ if (preset === undefined) {
315
+ throw new Error(`Unknown progression preset: ${opts.presetId}`);
316
+ }
283
317
  }
284
318
  if (preset === undefined) {
285
319
  const pool = PRESETS.filter((p) => p.styles.includes(opts.style));
@@ -289,12 +323,12 @@ export function generateProgression(opts: GenerateProgressionOptions): Generated
289
323
  preset = candidates[index] ?? PRESETS[0];
290
324
  }
291
325
  const degrees = preset?.degrees ?? [0];
292
- const chords: GeneratedChord[] = [];
326
+ const chords: ChordSpan[] = [];
293
327
  for (let bar = 0; bar < opts.bars; bar += 1) {
294
328
  const degree = degrees[bar % degrees.length] ?? 0;
295
329
  const quality =
296
330
  opts.ext !== undefined && opts.ext !== 'auto' ? opts.ext : autoQuality(degree, opts.key);
297
- const chord: GeneratedChord = {
331
+ const chord: ChordSpan = {
298
332
  rootPc: degreeToRootPc(degree, opts.key),
299
333
  quality,
300
334
  startBeat: bar * 4,
@@ -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,16 +16,27 @@ 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 {
32
32
  return ((n % 12) + 12) % 12;
33
33
  }
34
34
 
35
- /** The kind of substitution relationship a candidate realizes. */
35
+ /**
36
+ * The kind of substitution relationship a candidate realizes.
37
+ *
38
+ * @category Reharmonization
39
+ */
36
40
  export type SubstitutionType =
37
41
  | 'tritone'
38
42
  | 'relative'
@@ -40,7 +44,11 @@ export type SubstitutionType =
40
44
  | 'chromatic-mediant'
41
45
  | 'secondary-dominant';
42
46
 
43
- /** A proposed chord substitution with its relationship, numeral, and function. */
47
+ /**
48
+ * A proposed chord substitution with its relationship, numeral, and function.
49
+ *
50
+ * @category Reharmonization
51
+ */
44
52
  export type Substitution = {
45
53
  chord: Chord;
46
54
  type: SubstitutionType;
@@ -48,7 +56,11 @@ export type Substitution = {
48
56
  function: HarmonicFunction;
49
57
  };
50
58
 
51
- /** Options controlling which substitutions {@link substituteChord} returns. */
59
+ /**
60
+ * Options controlling which substitutions {@link substituteChord} returns.
61
+ *
62
+ * @category Reharmonization
63
+ */
52
64
  export type SubstituteOptions = {
53
65
  /**
54
66
  * Melody pitch classes that must remain chord tones. When given, only
@@ -132,6 +144,13 @@ const THIRD_OFFSETS = [3, 4, 8, 9] as const;
132
144
  * @param key The prevailing key.
133
145
  * @param opts Optional melody-preservation constraint.
134
146
  * @returns The deduplicated substitution candidates.
147
+ * @example
148
+ * ```ts
149
+ * import { substituteChord, makeChord, majorKey } from '@libraz/libcantus';
150
+ * const subs = substituteChord(makeChord(7, 'dom7'), majorKey(0));
151
+ * // includes the tritone sub Db7 (rootPc 1, quality 'dom7')
152
+ * ```
153
+ * @category Reharmonization
135
154
  */
136
155
  export function substituteChord(
137
156
  chord: Chord,
@@ -214,7 +233,11 @@ export function substituteChord(
214
233
  return results;
215
234
  }
216
235
 
217
- /** A borrowed chord in the modal-interchange palette of a key. */
236
+ /**
237
+ * A borrowed chord in the modal-interchange palette of a key.
238
+ *
239
+ * @category Reharmonization
240
+ */
218
241
  export type BorrowedChord = {
219
242
  chord: Chord;
220
243
  roman: string;
@@ -231,6 +254,7 @@ export type BorrowedChord = {
231
254
  *
232
255
  * @param key The prevailing key.
233
256
  * @returns The borrowed-chord palette.
257
+ * @category Reharmonization
234
258
  */
235
259
  export function modalInterchangePalette(key: KeyScale): BorrowedChord[] {
236
260
  const palette: BorrowedChord[] = [];
@@ -275,6 +299,13 @@ export function modalInterchangePalette(key: KeyScale): BorrowedChord[] {
275
299
  * @param chord The chord to reflect.
276
300
  * @param key The prevailing key.
277
301
  * @returns The negative-harmony counterpart of the chord.
302
+ * @example
303
+ * ```ts
304
+ * import { negativeHarmonyMirror, makeChord, majorKey } from '@libraz/libcantus';
305
+ * negativeHarmonyMirror(makeChord(7, 'maj'), majorKey(0));
306
+ * // G major reflected across C's axis => F minor (rootPc 5, quality 'min')
307
+ * ```
308
+ * @category Reharmonization
278
309
  */
279
310
  export function negativeHarmonyMirror(chord: Chord, key: KeyScale): Chord {
280
311
  const tonic = mod12(key.rootPc);
@@ -11,10 +11,14 @@
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
16
 
17
- /** A single rhythmic note: an onset position and how long it sounds. */
17
+ /**
18
+ * A single rhythmic note: an onset position and how long it sounds.
19
+ *
20
+ * @category Rhythm & Meter
21
+ */
18
22
  export type RhythmEvent = {
19
23
  /** Onset position in quarter-note beats, absolute from the start of the span. */
20
24
  position: number;
@@ -22,21 +26,38 @@ export type RhythmEvent = {
22
26
  duration: number;
23
27
  };
24
28
 
25
- /** Options controlling {@link generateRhythm}. */
29
+ /**
30
+ * Options controlling {@link generateRhythm}.
31
+ *
32
+ * @category Rhythm & Meter
33
+ */
26
34
  export type RhythmOptions = {
27
- /** Seed for the deterministic PRNG. Default 0. */
35
+ /**
36
+ * Seed for the deterministic PRNG.
37
+ *
38
+ * @defaultValue 0
39
+ */
28
40
  seed?: number;
29
- /** Number of bars to generate. Default 1. */
41
+ /**
42
+ * Number of bars to generate.
43
+ *
44
+ * @defaultValue 1
45
+ */
30
46
  bars?: number;
31
47
  /**
32
48
  * Grid resolution as the number of equal grid steps per quarter-note beat.
33
49
  * The default of 2 yields an eighth-note grid; 4 yields a sixteenth-note
34
50
  * grid, 3 an eighth-note triplet grid, and so on.
51
+ *
52
+ * @defaultValue 2
35
53
  */
36
54
  subdivision?: number;
37
55
  /**
38
56
  * Overall onset density in [0, 1]. Scales the per-slot onset probability, so
39
- * higher values fill more grid slots. Default 0.5.
57
+ * higher values fill more grid slots. Values outside [0, 1] are clamped into
58
+ * range.
59
+ *
60
+ * @defaultValue 0.5
40
61
  */
41
62
  density?: number;
42
63
  };
@@ -53,6 +74,7 @@ const DEFAULT_BARS = 1;
53
74
  *
54
75
  * @param weight The metric weight, 0 (off-pulse) to 3 (downbeat).
55
76
  * @returns A base onset probability in [0, 1].
77
+ * @category Rhythm & Meter
56
78
  */
57
79
  export function onsetWeightCurve(weight: number): number {
58
80
  switch (weight) {
@@ -71,29 +93,40 @@ export function onsetWeightCurve(weight: number): number {
71
93
  * Generate a deterministic rhythmic pattern over `bars` bars of a time
72
94
  * signature. A grid is built at the chosen subdivision; each slot becomes an
73
95
  * onset with a probability derived from its metric weight scaled by `density`
74
- * and sampled from the seeded PRNG. The first slot (the downbeat) is always an
75
- * onset. Each event's duration extends to the next onset, and the last event
76
- * extends to the end of the span.
96
+ * and sampled from the seeded PRNG. Every bar's downbeat is always an onset, so
97
+ * a pattern of any length has a reliable pulse independent of the seed. Each
98
+ * event's duration extends to the next onset, and the last event extends to the
99
+ * end of the span. `density` is clamped to [0, 1].
77
100
  *
78
101
  * @param ts The time signature.
79
102
  * @param opts Generation options.
80
103
  * @returns Onset events sorted by position, non-overlapping, covering the span.
104
+ * @example
105
+ * ```ts
106
+ * import { parseTimeSignature, generateRhythm } from '@libraz/libcantus';
107
+ * const ts = parseTimeSignature('4/4');
108
+ * generateRhythm(ts, { seed: 42, density: 0.6 }); // onset events over one bar
109
+ * ```
110
+ * @category Rhythm & Meter
81
111
  */
82
112
  export function generateRhythm(ts: TimeSignature, opts: RhythmOptions = {}): RhythmEvent[] {
83
113
  const seed = opts.seed ?? 0;
84
114
  const bars = opts.bars ?? DEFAULT_BARS;
85
115
  const subdivision = opts.subdivision ?? DEFAULT_SUBDIVISION;
86
- const density = opts.density ?? DEFAULT_DENSITY;
116
+ const density = Math.min(1, Math.max(0, opts.density ?? DEFAULT_DENSITY));
87
117
 
88
- const spanBeats = beatsPerBar(ts) * bars;
118
+ const barBeats = beatsPerBar(ts);
119
+ const spanBeats = barBeats * bars;
89
120
  const step = 1 / subdivision;
90
121
  const slotCount = Math.round(spanBeats * subdivision);
122
+ const slotsPerBar = Math.round(barBeats * subdivision);
91
123
  const rng = createRng(seed);
92
124
 
93
125
  const positions: number[] = [];
94
126
  for (let i = 0; i < slotCount; i += 1) {
95
127
  const position = i * step;
96
- if (i === 0) {
128
+ // Force every bar's downbeat so the pulse is reliable regardless of seed.
129
+ if (i % slotsPerBar === 0) {
97
130
  positions.push(position);
98
131
  continue;
99
132
  }
@@ -119,6 +152,7 @@ export function generateRhythm(ts: TimeSignature, opts: RhythmOptions = {}): Rhy
119
152
  * @param events Events from {@link generateRhythm}.
120
153
  * @param ts The time signature.
121
154
  * @returns The onset count divided by the number of bars the events span.
155
+ * @category Rhythm & Meter
122
156
  */
123
157
  export function rhythmDensity(events: RhythmEvent[], ts: TimeSignature): number {
124
158
  if (events.length === 0) {