@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
@@ -24,6 +24,8 @@ const LETTER_NAMES = ['C', 'D', 'E', 'F', 'G', 'A', 'B'] as const;
24
24
  * 0 natural, +1 sharp, +2 double-sharp.
25
25
  * - `octave` follows scientific pitch notation (middle C = C4 = MIDI 60). When
26
26
  * absent, the note denotes a bare pitch class.
27
+ *
28
+ * @category Pitch & Intervals
27
29
  */
28
30
  export type Note = {
29
31
  letter: number;
@@ -31,7 +33,11 @@ export type Note = {
31
33
  octave?: number;
32
34
  };
33
35
 
34
- /** A spelled interval: a diatonic number, a quality label, and its semitone span. */
36
+ /**
37
+ * A spelled interval: a diatonic number, a quality label, and its semitone span.
38
+ *
39
+ * @category Pitch & Intervals
40
+ */
35
41
  export type SpelledInterval = {
36
42
  /** Diatonic size: 1 = unison, 2 = second, ... 8 = octave, and beyond. */
37
43
  number: number;
@@ -62,6 +68,13 @@ function mod7(n: number): number {
62
68
  * @param text The note text.
63
69
  * @returns The parsed note.
64
70
  * @throws If the text is not a valid note.
71
+ * @example
72
+ * ```ts
73
+ * import { parseNote, noteToMidi } from '@libraz/libcantus';
74
+ * const n = parseNote('C#4');
75
+ * noteToMidi(n); // 61
76
+ * ```
77
+ * @category Pitch & Intervals
65
78
  */
66
79
  export function parseNote(text: string): Note {
67
80
  const match = /^([A-Ga-g])([#x]*|b*)(-?\d+)?$/.exec(text.trim());
@@ -103,6 +116,12 @@ function formatAlter(alter: number): string {
103
116
  *
104
117
  * @param note The note to format.
105
118
  * @returns The note text, including the octave when present.
119
+ * @example
120
+ * ```ts
121
+ * import { formatNote, parseNote } from '@libraz/libcantus';
122
+ * formatNote(parseNote('C#4')); // 'C#4'
123
+ * ```
124
+ * @category Pitch & Intervals
106
125
  */
107
126
  export function formatNote(note: Note): string {
108
127
  const glyph = LETTER_NAMES[mod7(note.letter)] ?? 'C';
@@ -115,6 +134,12 @@ export function formatNote(note: Note): string {
115
134
  *
116
135
  * @param note The note.
117
136
  * @returns The pitch class.
137
+ * @example
138
+ * ```ts
139
+ * import { noteToPitchClass, parseNote } from '@libraz/libcantus';
140
+ * noteToPitchClass(parseNote('Db')); // 1
141
+ * ```
142
+ * @category Pitch & Intervals
118
143
  */
119
144
  export function noteToPitchClass(note: Note): number {
120
145
  const natural = LETTER_SEMITONES[mod7(note.letter)] ?? 0;
@@ -127,6 +152,12 @@ export function noteToPitchClass(note: Note): number {
127
152
  * @param note The note; must carry an octave.
128
153
  * @returns The MIDI number.
129
154
  * @throws If the note has no octave.
155
+ * @example
156
+ * ```ts
157
+ * import { noteToMidi, parseNote } from '@libraz/libcantus';
158
+ * noteToMidi(parseNote('A4')); // 69
159
+ * ```
160
+ * @category Pitch & Intervals
130
161
  */
131
162
  export function noteToMidi(note: Note): number {
132
163
  if (note.octave === undefined) {
@@ -170,9 +201,20 @@ const FLAT_SPELLING: readonly [number, number][] = [
170
201
  /**
171
202
  * Name a MIDI number as a {@link Note}, choosing sharp or flat spelling.
172
203
  *
204
+ * The input is rounded to the nearest integer but is not clamped to the MIDI
205
+ * range 0..127: out-of-range values extrapolate linearly (e.g. `-1` -> `B-2`,
206
+ * `128` -> `G#9`) and remain an exact inverse of {@link noteToMidi}.
207
+ *
173
208
  * @param midi The MIDI number.
174
209
  * @param spelling Whether to prefer sharps or flats for black keys.
175
210
  * @returns The spelled note, with octave.
211
+ * @example
212
+ * ```ts
213
+ * import { midiToNote, formatNote } from '@libraz/libcantus';
214
+ * formatNote(midiToNote(61)); // 'C#4'
215
+ * formatNote(midiToNote(61, 'flat')); // 'Db4'
216
+ * ```
217
+ * @category Pitch & Intervals
176
218
  */
177
219
  export function midiToNote(midi: number, spelling: 'sharp' | 'flat' = 'sharp'): Note {
178
220
  const rounded = Math.round(midi);
@@ -227,6 +269,13 @@ function diatonicIndex(note: Note): number {
227
269
  * @param a The lower/first note.
228
270
  * @param b The upper/second note.
229
271
  * @returns The diatonic number, quality, and semitone span.
272
+ * @example
273
+ * ```ts
274
+ * import { spelledInterval, parseNote } from '@libraz/libcantus';
275
+ * spelledInterval(parseNote('C4'), parseNote('G4'));
276
+ * // { number: 5, quality: 'P', semitones: 7 }
277
+ * ```
278
+ * @category Pitch & Intervals
230
279
  */
231
280
  export function spelledInterval(a: Note, b: Note): SpelledInterval {
232
281
  const octaved = a.octave !== undefined && b.octave !== undefined;
@@ -240,11 +289,17 @@ export function spelledInterval(a: Note, b: Note): SpelledInterval {
240
289
  letterSteps = mod7(b.letter - a.letter);
241
290
  let rawSemis = mod12(noteToPitchClass(b) - noteToPitchClass(a));
242
291
  // Lift the chromatic span into the octave nearest the diatonic reference so
243
- // wraparound intervals (e.g. Ab -> G# = augmented seventh) stay consistent
244
- // with the ascending diatonic number instead of collapsing modulo 12.
292
+ // wraparound intervals stay consistent with the diatonic number instead of
293
+ // collapsing modulo 12. The correction is bidirectional: an upward wrap
294
+ // (e.g. Ab -> G# = augmented seventh) adds an octave, while a same-letter or
295
+ // downward chromatic step (e.g. E -> Eb, F# -> F, C -> Cb = descending
296
+ // diminished/augmented unison) subtracts one so the sign follows pitch
297
+ // direction instead of returning a garbage stack of augmentations.
245
298
  const reference = SIMPLE_REFERENCE[letterSteps + 1] ?? 0;
246
299
  if (reference - rawSemis > 6) {
247
300
  rawSemis += 12;
301
+ } else if (rawSemis - reference > 6) {
302
+ rawSemis -= 12;
248
303
  }
249
304
  semitones = rawSemis;
250
305
  }
@@ -5,7 +5,11 @@
5
5
  * seed the stream is fully reproducible.
6
6
  */
7
7
 
8
- /** A deterministic PRNG with the sampling helpers the generators need. */
8
+ /**
9
+ * A deterministic PRNG with the sampling helpers the generators need.
10
+ *
11
+ * @category Utilities
12
+ */
9
13
  export type Rng = {
10
14
  /** Next float in [0, 1). */
11
15
  next: () => number;
@@ -22,6 +26,14 @@ export type Rng = {
22
26
  *
23
27
  * @param seed The 32-bit seed; the same seed always yields the same stream.
24
28
  * @returns The seeded generator.
29
+ * @example
30
+ * ```ts
31
+ * import { createRng } from '@libraz/libcantus';
32
+ * const rng = createRng(42);
33
+ * rng.next(); // deterministic float in [0, 1)
34
+ * rng.range(1, 6); // deterministic integer in [1, 6]
35
+ * ```
36
+ * @category Utilities
25
37
  */
26
38
  export function createRng(seed: number): Rng {
27
39
  let state = seed >>> 0;
@@ -11,6 +11,8 @@
11
11
  /**
12
12
  * An equal-tempered tuning: a reference pitch and a number of equal divisions of
13
13
  * the octave. With `divisions === 12` a step index is an ordinary MIDI number.
14
+ *
15
+ * @category Pitch & Intervals
14
16
  */
15
17
  export type Tuning = {
16
18
  /** Step index (MIDI number when `divisions` is 12) whose frequency is `refFreq`. */
@@ -21,7 +23,11 @@ export type Tuning = {
21
23
  divisions: number;
22
24
  };
23
25
 
24
- /** Standard twelve-tone equal temperament, A4 (MIDI 69) = 440 Hz. */
26
+ /**
27
+ * Standard twelve-tone equal temperament, A4 (MIDI 69) = 440 Hz.
28
+ *
29
+ * @category Pitch & Intervals
30
+ */
25
31
  export const TWELVE_TET: Tuning = { refStep: 69, refFreq: 440, divisions: 12 };
26
32
 
27
33
  /**
@@ -31,6 +37,13 @@ export const TWELVE_TET: Tuning = { refStep: 69, refFreq: 440, divisions: 12 };
31
37
  * @param refFreq Reference frequency in Hz (default 440).
32
38
  * @param refStep Step index of the reference (default 69).
33
39
  * @returns The tuning.
40
+ * @example
41
+ * ```ts
42
+ * import { edo, frequencyOf } from '@libraz/libcantus';
43
+ * const et19 = edo(19); // { refStep: 69, refFreq: 440, divisions: 19 }
44
+ * frequencyOf(70, et19); // one 19-EDO step above A4
45
+ * ```
46
+ * @category Pitch & Intervals
34
47
  */
35
48
  export function edo(n: number, refFreq = 440, refStep = 69): Tuning {
36
49
  return { refStep, refFreq, divisions: n };
@@ -42,6 +55,13 @@ export function edo(n: number, refFreq = 440, refStep = 69): Tuning {
42
55
  * @param step Step index (a MIDI number under 12-EDO).
43
56
  * @param tuning The tuning (default 12-TET).
44
57
  * @returns The frequency in Hz.
58
+ * @example
59
+ * ```ts
60
+ * import { frequencyOf } from '@libraz/libcantus';
61
+ * frequencyOf(69); // 440 (A4 in 12-TET)
62
+ * frequencyOf(60); // middle C in Hz
63
+ * ```
64
+ * @category Pitch & Intervals
45
65
  */
46
66
  export function frequencyOf(step: number, tuning: Tuning = TWELVE_TET): number {
47
67
  return tuning.refFreq * 2 ** ((step - tuning.refStep) / tuning.divisions);
@@ -54,6 +74,7 @@ export function frequencyOf(step: number, tuning: Tuning = TWELVE_TET): number {
54
74
  * @param freq Frequency in Hz.
55
75
  * @param tuning The tuning (default 12-TET).
56
76
  * @returns The nearest step index.
77
+ * @category Pitch & Intervals
57
78
  */
58
79
  export function nearestStep(freq: number, tuning: Tuning = TWELVE_TET): number {
59
80
  return Math.round(tuning.refStep + tuning.divisions * Math.log2(freq / tuning.refFreq));
@@ -65,6 +86,7 @@ export function nearestStep(freq: number, tuning: Tuning = TWELVE_TET): number {
65
86
  * @param a Lower/first frequency in Hz.
66
87
  * @param b Upper/second frequency in Hz.
67
88
  * @returns Cents from `a` to `b` (negative if `b` is lower).
89
+ * @category Pitch & Intervals
68
90
  */
69
91
  export function centsBetweenFreq(a: number, b: number): number {
70
92
  return 1200 * Math.log2(b / a);
@@ -76,6 +98,7 @@ export function centsBetweenFreq(a: number, b: number): number {
76
98
  * @param steps Number of steps.
77
99
  * @param tuning The tuning (default 12-TET).
78
100
  * @returns The cents.
101
+ * @category Pitch & Intervals
79
102
  */
80
103
  export function centsOfSteps(steps: number, tuning: Tuning = TWELVE_TET): number {
81
104
  return (steps * 1200) / tuning.divisions;
@@ -88,6 +111,7 @@ export function centsOfSteps(steps: number, tuning: Tuning = TWELVE_TET): number
88
111
  * @param numerator Ratio numerator.
89
112
  * @param denominator Ratio denominator.
90
113
  * @returns The interval in cents.
114
+ * @category Pitch & Intervals
91
115
  */
92
116
  export function ratioToCents(numerator: number, denominator: number): number {
93
117
  return 1200 * Math.log2(numerator / denominator);
@@ -96,6 +120,8 @@ export function ratioToCents(numerator: number, denominator: number): number {
96
120
  /**
97
121
  * Five-limit just-intonation ratios for the twelve interval classes above a
98
122
  * unison, indexed by semitone class (0..12).
123
+ *
124
+ * @category Pitch & Intervals
99
125
  */
100
126
  export const JUST_RATIOS: Record<number, [number, number]> = {
101
127
  0: [1, 1],
@@ -119,6 +145,7 @@ export const JUST_RATIOS: Record<number, [number, number]> = {
119
145
  *
120
146
  * @param semitoneClass Semitone class in [0, 12].
121
147
  * @returns The deviation in cents, or NaN if the class has no listed ratio.
148
+ * @category Pitch & Intervals
122
149
  */
123
150
  export function justDeviationCents(semitoneClass: number): number {
124
151
  const ratio = JUST_RATIOS[semitoneClass];
@@ -12,35 +12,63 @@
12
12
  * library-wide convention.
13
13
  */
14
14
 
15
- import type { Chord } from '../chord/index.js';
16
- import { isStrongBeat, type TimeSignature } from '../meter/index.js';
17
- import { createRng } from '../random/index.js';
18
- import { nearestScaleTone } from '../scale/index.js';
19
- import type { KeyScale, NoteEvent } from '../types.js';
15
+ import { isStrongBeat, type TimeSignature } from '../../core/meter/index.js';
16
+ import { createRng } from '../../core/random/index.js';
17
+ import type { KeyScale, NoteEvent } from '../../core/types.js';
18
+ import type { Chord } from '../../theory/chord/index.js';
19
+ import { nearestScaleTone } from '../../theory/scale/index.js';
20
20
 
21
- /** A chord sounding over a half-open beat span `[startBeat, endBeat)`. */
21
+ /**
22
+ * A chord sounding over a half-open beat span `[startBeat, endBeat)`.
23
+ *
24
+ * @category Composition
25
+ */
22
26
  export type BassSegment = {
23
27
  startBeat: number;
24
28
  endBeat: number;
25
29
  chord: Chord;
26
30
  };
27
31
 
28
- /** The bass-line idiom to generate. */
32
+ /**
33
+ * The bass-line idiom to generate.
34
+ *
35
+ * @category Composition
36
+ */
29
37
  export type BassStyle = 'root' | 'rootFifth' | 'pop' | 'walking' | 'arpeggio';
30
38
 
31
- /** Options controlling {@link generateBassLine}. */
39
+ /**
40
+ * Options controlling {@link generateBassLine}.
41
+ *
42
+ * @category Composition
43
+ */
32
44
  export type BassLineOptions = {
33
45
  /** Chord placement to follow; need not be pre-sorted. */
34
46
  segments: BassSegment[];
35
47
  /** Key/scale context, used for diatonic approach tones in `walking`. */
36
48
  key: KeyScale;
37
- /** Time signature; used for metric accents. Default 4/4. */
49
+ /**
50
+ * Time signature; used for metric accents.
51
+ *
52
+ * @defaultValue `{ numerator: 4, denominator: 4 }`
53
+ */
38
54
  ts?: TimeSignature;
39
- /** Bass-line idiom. Default `'root'`. */
55
+ /**
56
+ * Bass-line idiom.
57
+ *
58
+ * @defaultValue `'root'`
59
+ */
40
60
  style?: BassStyle;
41
- /** Target register as a base MIDI octave; roots land around `octave*12+12`. Default 2. */
61
+ /**
62
+ * Target register as a base MIDI octave; roots land around `octave*12+12`.
63
+ *
64
+ * @defaultValue 2
65
+ */
42
66
  octave?: number;
43
- /** Seed for the deterministic PRNG. Default 0. */
67
+ /**
68
+ * Seed for the deterministic PRNG.
69
+ *
70
+ * @defaultValue 0
71
+ */
44
72
  seed?: number;
45
73
  };
46
74
 
@@ -98,10 +126,18 @@ function chordTonePcs(chord: Chord): number[] {
98
126
  return pcs;
99
127
  }
100
128
 
101
- /** The chord's perfect-fifth pitch class, falling back to the root when absent. */
129
+ /**
130
+ * The chord's fifth pitch class.
131
+ *
132
+ * Uses the perfect fifth when present, otherwise the chord's actual altered
133
+ * fifth (diminished = 6, augmented = 8 semitones above the root), so dim/aug/
134
+ * m7b5 chords sound their real fifth rather than a repeated root. Falls back to
135
+ * the root only when the chord has no fifth degree at all.
136
+ */
102
137
  function fifthPcOf(chord: Chord): number {
103
- const hasFifth = chord.intervals.some((i) => pitchClass(i) === 7);
104
- return hasFifth ? pitchClass(chord.rootPc + 7) : pitchClass(chord.rootPc);
138
+ const fifths = new Set(chord.intervals.map((i) => pitchClass(i)));
139
+ const chosen = fifths.has(7) ? 7 : fifths.has(6) ? 6 : fifths.has(8) ? 8 : undefined;
140
+ return chosen === undefined ? pitchClass(chord.rootPc) : pitchClass(chord.rootPc + chosen);
105
141
  }
106
142
 
107
143
  /** The sounding bass pitch class of a chord: its slash bass, else its root. */
@@ -166,9 +202,10 @@ function buildPop(ctx: BuildContext, seg: BassSegment): void {
166
202
  emitted = true;
167
203
  } else if (ctx.rng.prob(PICKUP_PROB)) {
168
204
  if (ctx.rng.prob(0.5)) {
169
- // Octave pickup: the root an octave below the band anchor.
205
+ // Octave pickup: the root dropped toward the bottom of the register,
206
+ // clamped so it never falls below the declared band.
170
207
  const base = placePc(rootPc, ctx.prevMidi, ctx.low);
171
- emit(ctx, pos, rootPc, base - 12);
208
+ emit(ctx, pos, rootPc, Math.max(ctx.low, base - 12));
172
209
  } else {
173
210
  ctx.prevMidi = emit(ctx, pos, fifthPcOf(seg.chord));
174
211
  }
@@ -231,6 +268,18 @@ function buildWalking(ctx: BuildContext, seg: BassSegment, next: BassSegment | u
231
268
  *
232
269
  * @param opts Segments, key, and generation options.
233
270
  * @returns Bass notes sorted by onset, non-overlapping.
271
+ *
272
+ * @example
273
+ * ```ts
274
+ * import { generateBassLine, makeChord, majorKey } from '@libraz/libcantus';
275
+ * const segments = [
276
+ * { startBeat: 0, endBeat: 4, chord: makeChord(0, 'maj') },
277
+ * { startBeat: 4, endBeat: 8, chord: makeChord(7, 'maj') },
278
+ * ];
279
+ * const notes = generateBassLine({ segments, key: majorKey(0), style: 'walking' });
280
+ * ```
281
+ *
282
+ * @category Composition
234
283
  */
235
284
  export function generateBassLine(opts: BassLineOptions): NoteEvent[] {
236
285
  const segments = [...opts.segments].sort((a, b) => a.startBeat - b.startBeat);
@@ -1,20 +1,20 @@
1
- import type { Chord } from '../chord/index.js';
2
- import { chordToneRole } from '../chord/index.js';
3
- import {
4
- createsParallelOctave,
5
- createsParallelPerfect,
6
- isForbiddenMelodicLeap,
7
- } from '../counterpoint/index.js';
8
- import type { TimeSignature } from '../meter/index.js';
9
- import { isStrongBeat } from '../meter/index.js';
10
- import type { Rng } from '../random/index.js';
11
- import { createRng } from '../random/index.js';
12
- import type { VoiceSnapshot } from '../safety/index.js';
13
- import { enumerateSafePitches } from '../safety/index.js';
14
- import { nearestScaleTone } from '../scale/index.js';
15
- import type { KeyScale, NoteEvent } from '../types.js';
1
+ import type { TimeSignature } from '../../core/meter/index.js';
2
+ import { isStrongBeat } from '../../core/meter/index.js';
3
+ import type { Rng } from '../../core/random/index.js';
4
+ import { createRng } from '../../core/random/index.js';
5
+ import type { KeyScale, NoteEvent } from '../../core/types.js';
6
+ import type { Chord } from '../../theory/chord/index.js';
7
+ import { chordToneRole } from '../../theory/chord/index.js';
8
+ import { createsParallelPerfect, isForbiddenMelodicLeap } from '../../theory/counterpoint/index.js';
9
+ import type { VoiceSnapshot } from '../../theory/safety/index.js';
10
+ import { enumerateSafePitches } from '../../theory/safety/index.js';
11
+ import { nearestScaleTone } from '../../theory/scale/index.js';
16
12
 
17
- /** Options controlling {@link generateCounterMelody}. */
13
+ /**
14
+ * Options controlling {@link generateCounterMelody}.
15
+ *
16
+ * @category Voicing & Counterpoint
17
+ */
18
18
  export type CounterMelodyOptions = {
19
19
  /** The lead line to write against, in ascending onset order. */
20
20
  melody: NoteEvent[];
@@ -22,22 +22,40 @@ export type CounterMelodyOptions = {
22
22
  chordAt: (beat: number) => Chord | null;
23
23
  /** Key/scale context for scale-tone decisions. */
24
24
  key: KeyScale;
25
- /** Meter used for strong-beat decisions (default 4/4). */
25
+ /**
26
+ * Meter used for strong-beat decisions.
27
+ *
28
+ * @defaultValue 4/4
29
+ */
26
30
  ts?: TimeSignature;
27
- /** Which side of the melody the counter line occupies (default `'below'`). */
31
+ /**
32
+ * Which side of the melody the counter line occupies.
33
+ *
34
+ * @defaultValue 'below'
35
+ */
28
36
  register?: 'above' | 'below';
29
37
  /**
30
- * Onset strategy: `'complement'` (default) moves where the melody holds or
31
- * rests and reinforces some strong beats; `'follow'` mirrors melody onsets.
38
+ * Onset strategy: `'complement'` moves where the melody holds or rests and
39
+ * reinforces some strong beats; `'follow'` mirrors melody onsets.
40
+ *
41
+ * @defaultValue 'complement'
32
42
  */
33
43
  rhythm?: 'complement' | 'follow';
34
- /** Safety profile applied to candidate pitches (default `'pop'`). */
44
+ /**
45
+ * Safety profile applied to candidate pitches.
46
+ *
47
+ * @defaultValue 'pop'
48
+ */
35
49
  profile?: 'strict' | 'pop';
36
50
  /** Lowest MIDI pitch the counter line may use (default derived from `register`). */
37
51
  pitchLow?: number;
38
52
  /** Highest MIDI pitch the counter line may use (default derived from `register`). */
39
53
  pitchHigh?: number;
40
- /** PRNG seed; the same seed always yields the same line (default 0). */
54
+ /**
55
+ * PRNG seed; the same seed always yields the same line.
56
+ *
57
+ * @defaultValue 0
58
+ */
41
59
  seed?: number;
42
60
  };
43
61
 
@@ -244,6 +262,17 @@ function fallbackPitch(
244
262
  *
245
263
  * @param opts The lead line, chord context callback, key, and generation knobs.
246
264
  * @returns The counter line as note events sorted by onset; `[]` for an empty melody.
265
+ * @example
266
+ * ```ts
267
+ * import { generateCounterMelody, majorKey, parseChordSymbol } from '@libraz/libcantus';
268
+ * const melody = [{ pitch: 72, startBeat: 0, durationBeat: 2 }];
269
+ * const counter = generateCounterMelody({
270
+ * melody,
271
+ * chordAt: () => parseChordSymbol('C'),
272
+ * key: majorKey(0),
273
+ * }); // note events below the melody, in onset order
274
+ * ```
275
+ * @category Voicing & Counterpoint
247
276
  */
248
277
  export function generateCounterMelody(opts: CounterMelodyOptions): NoteEvent[] {
249
278
  const melody = [...opts.melody].sort((a, b) => a.startBeat - b.startBeat);
@@ -309,8 +338,9 @@ export function generateCounterMelody(opts: CounterMelodyOptions): NoteEvent[] {
309
338
  if (
310
339
  prevPitch !== undefined &&
311
340
  melPrev !== undefined &&
312
- (createsParallelPerfect(prevPitch, candidate, melPrev, melPitch) ||
313
- createsParallelOctave(prevPitch, candidate, melPrev, melPitch))
341
+ // Parallel octaves are the perfect-class-zero case of this predicate,
342
+ // so a single check covers both parallel fifths and octaves.
343
+ createsParallelPerfect(prevPitch, candidate, melPrev, melPitch)
314
344
  ) {
315
345
  continue;
316
346
  }
@@ -32,7 +32,19 @@ export function getFillStartBeat(energy: SectionEnergy): number {
32
32
  }
33
33
  }
34
34
 
35
- /** Pick a fill archetype from the transition context. */
35
+ /**
36
+ * Pick a fill archetype for a section transition.
37
+ *
38
+ * The transition-specific archetypes (into a chorus, out of an intro) are
39
+ * checked before the generic energy fills so every variation stays reachable;
40
+ * with `from === to` the caller effectively asks for a within-section fill.
41
+ *
42
+ * @param from Section the fill leaves.
43
+ * @param to Section the fill leads into.
44
+ * @param style Internal drum style.
45
+ * @param nextEnergy Energy of the section the fill leads into.
46
+ * @param rng Deterministic PRNG.
47
+ */
36
48
  export function selectFillType(
37
49
  from: SectionType,
38
50
  to: SectionType,
@@ -44,6 +56,11 @@ export function selectFillType(
44
56
  return rng.range(0, 1) === 0 ? 'simpleCrash' : 'breakdownFill';
45
57
  }
46
58
 
59
+ const toChorus = to === 'chorus';
60
+ const fromIntro = from === 'intro';
61
+ const highEnergy = style === 'rock' || style === 'fourOnFloor';
62
+
63
+ // Dropping into a low-energy section: keep the phrase end gentle.
47
64
  if (nextEnergy === 'low') {
48
65
  switch (rng.range(0, 2)) {
49
66
  case 0:
@@ -55,25 +72,9 @@ export function selectFillType(
55
72
  }
56
73
  }
57
74
 
58
- if (nextEnergy === 'peak') {
59
- switch (rng.range(0, 3)) {
60
- case 0:
61
- return 'tomDescend';
62
- case 1:
63
- return 'snareRoll';
64
- case 2:
65
- return 'linearFill';
66
- default:
67
- return 'flamsAndDrags';
68
- }
69
- }
70
-
71
- const toChorus = to === 'chorus';
72
- const fromIntro = from === 'intro';
73
- const highEnergy = style === 'rock' || style === 'fourOnFloor';
74
- const choice = rng.range(0, 7);
75
-
75
+ // Big lead-ins to a chorus take precedence over the generic energy fills.
76
76
  if (toChorus) {
77
+ const choice = rng.range(0, 7);
77
78
  if (highEnergy) {
78
79
  switch (choice) {
79
80
  case 0:
@@ -110,8 +111,9 @@ export function selectFillType(
110
111
  }
111
112
  }
112
113
 
114
+ // Leaving the intro: lighter, building character.
113
115
  if (fromIntro) {
114
- switch (choice) {
116
+ switch (rng.range(0, 5)) {
115
117
  case 0:
116
118
  case 1:
117
119
  return 'snareRoll';
@@ -126,6 +128,8 @@ export function selectFillType(
126
128
  }
127
129
  }
128
130
 
131
+ // Generic medium/high/peak fills, split by style energy.
132
+ const choice = rng.range(0, 7);
129
133
  if (highEnergy) {
130
134
  switch (choice) {
131
135
  case 0:
@@ -339,6 +343,13 @@ export function generateFill(
339
343
  if (beat === 2) {
340
344
  track.add(GM.SD, beatTick, 1, accentVel);
341
345
  track.add(GM.BD, beatTick, 1, fillVel);
346
+ } else if (beat === 3) {
347
+ // At low energy the fill spans only beat 3, so this branch keeps the
348
+ // phrase end from going silent: a broad half-time backbeat snare with a
349
+ // light pickup into the next section.
350
+ track.add(GM.SD, beatTick, EIGHTH, accentVel);
351
+ track.add(GM.SD, beatTick + E, S, fillVel * 0.6);
352
+ track.add(GM.SD, beatTick + E + S, S, accentVel);
342
353
  }
343
354
  break;
344
355
  }