@libraz/libcantus 0.9.2 → 0.9.4

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 (200) hide show
  1. package/README.md +6 -5
  2. package/dist/analyze/index.cjs +41 -1676
  3. package/dist/analyze/index.cjs.map +1 -1
  4. package/dist/analyze/index.d.cts +194 -122
  5. package/dist/analyze/index.d.ts +194 -122
  6. package/dist/analyze/index.js +19 -8
  7. package/dist/chunk-36N73KZ3.cjs +246 -0
  8. package/dist/chunk-36N73KZ3.cjs.map +1 -0
  9. package/dist/chunk-4HLLZPXT.cjs +3077 -0
  10. package/dist/chunk-4HLLZPXT.cjs.map +1 -0
  11. package/dist/chunk-7MGZ7D62.cjs +498 -0
  12. package/dist/chunk-7MGZ7D62.cjs.map +1 -0
  13. package/dist/{chunk-R3S2LUBH.js → chunk-7NKDPF74.js} +660 -305
  14. package/dist/chunk-7NKDPF74.js.map +1 -0
  15. package/dist/chunk-CAN76NCU.cjs +679 -0
  16. package/dist/chunk-CAN76NCU.cjs.map +1 -0
  17. package/dist/{chunk-7Y7WF64D.js → chunk-D2FAJBYL.js} +104 -35
  18. package/dist/chunk-D2FAJBYL.js.map +1 -0
  19. package/dist/{chunk-COKTX2G5.js → chunk-EHVEBWM5.js} +1052 -510
  20. package/dist/chunk-EHVEBWM5.js.map +1 -0
  21. package/dist/chunk-FCANXBAT.cjs +30 -0
  22. package/dist/chunk-FCANXBAT.cjs.map +1 -0
  23. package/dist/chunk-FNDWXRRI.cjs +479 -0
  24. package/dist/chunk-FNDWXRRI.cjs.map +1 -0
  25. package/dist/{chunk-GQKN5KDT.js → chunk-GQSGS2N4.js} +43 -29
  26. package/dist/chunk-GQSGS2N4.js.map +1 -0
  27. package/dist/chunk-HKGYCQ34.js +53 -0
  28. package/dist/chunk-HKGYCQ34.js.map +1 -0
  29. package/dist/chunk-I24MV65U.js +106 -0
  30. package/dist/chunk-I24MV65U.js.map +1 -0
  31. package/dist/{chunk-LN7ZZMD2.js → chunk-KMHOICIY.js} +322 -173
  32. package/dist/chunk-KMHOICIY.js.map +1 -0
  33. package/dist/chunk-LW53AHQU.cjs +1117 -0
  34. package/dist/chunk-LW53AHQU.cjs.map +1 -0
  35. package/dist/chunk-N6IHQUOX.js +1 -0
  36. package/dist/chunk-N6IHQUOX.js.map +1 -0
  37. package/dist/chunk-NKIBQ6PT.js +30 -0
  38. package/dist/chunk-NKIBQ6PT.js.map +1 -0
  39. package/dist/chunk-NP4NA2FB.cjs +106 -0
  40. package/dist/chunk-NP4NA2FB.cjs.map +1 -0
  41. package/dist/chunk-NVE34YGZ.cjs +1449 -0
  42. package/dist/chunk-NVE34YGZ.cjs.map +1 -0
  43. package/dist/{chunk-A4532ZFR.js → chunk-NYYWMV2C.js} +115 -47
  44. package/dist/chunk-NYYWMV2C.js.map +1 -0
  45. package/dist/{chunk-5HTNMQW4.js → chunk-Q2XUP227.js} +310 -118
  46. package/dist/chunk-Q2XUP227.js.map +1 -0
  47. package/dist/chunk-Q6OP4U43.cjs +327 -0
  48. package/dist/chunk-Q6OP4U43.cjs.map +1 -0
  49. package/dist/chunk-RC6GBMCD.cjs +845 -0
  50. package/dist/chunk-RC6GBMCD.cjs.map +1 -0
  51. package/dist/{chunk-XTC3X6MS.js → chunk-RK35RIN7.js} +453 -152
  52. package/dist/chunk-RK35RIN7.js.map +1 -0
  53. package/dist/chunk-RUMU3UA6.cjs +53 -0
  54. package/dist/chunk-RUMU3UA6.cjs.map +1 -0
  55. package/dist/chunk-T64C7ZD7.cjs +1 -0
  56. package/dist/chunk-T64C7ZD7.cjs.map +1 -0
  57. package/dist/chunk-XEBG3G53.js +498 -0
  58. package/dist/chunk-XEBG3G53.js.map +1 -0
  59. package/dist/chunk-YLEMGEFH.js +246 -0
  60. package/dist/chunk-YLEMGEFH.js.map +1 -0
  61. package/dist/chunk-ZEJR4TKL.cjs +173 -0
  62. package/dist/chunk-ZEJR4TKL.cjs.map +1 -0
  63. package/dist/core/index.cjs +130 -431
  64. package/dist/core/index.cjs.map +1 -1
  65. package/dist/core/index.d.cts +331 -17
  66. package/dist/core/index.d.ts +331 -17
  67. package/dist/core/index.js +71 -13
  68. package/dist/{function-2bJW1Zp_.d.ts → function-BtiZb9i9.d.ts} +28 -7
  69. package/dist/{function-r2sh-jOO.d.cts → function-DNugAOrt.d.cts} +28 -7
  70. package/dist/generate/index.cjs +30 -3751
  71. package/dist/generate/index.cjs.map +1 -1
  72. package/dist/generate/index.d.cts +373 -59
  73. package/dist/generate/index.d.ts +373 -59
  74. package/dist/generate/index.js +23 -9
  75. package/dist/{index-DZ1M21do.d.cts → index-6VR8-kit.d.ts} +20 -4
  76. package/dist/index-70p168VM.d.ts +192 -0
  77. package/dist/{index-D_ZTUNKp.d.cts → index-Be74mcon.d.cts} +18 -17
  78. package/dist/index-BtXT6UQ0.d.cts +192 -0
  79. package/dist/{index-BMPxwjwV.d.ts → index-C5UBbQ4G.d.ts} +18 -17
  80. package/dist/{index-BOBXBNwP.d.cts → index-C83mZI1P.d.ts} +93 -8
  81. package/dist/{index-BdeQ1r_p.d.cts → index-D6b6na69.d.cts} +14 -11
  82. package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
  83. package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
  84. package/dist/index-OG65ocOr.d.cts +272 -0
  85. package/dist/index-OG65ocOr.d.ts +272 -0
  86. package/dist/{index-BreoJRw0.d.ts → index-tJhkmQgE.d.cts} +20 -4
  87. package/dist/{index-CP3wAfEH.d.ts → index-tSQh-xwY.d.ts} +14 -11
  88. package/dist/{index-CM5W4rcm.d.ts → index-y_ak9_Hi.d.cts} +93 -8
  89. package/dist/index.cjs +419 -6848
  90. package/dist/index.cjs.map +1 -1
  91. package/dist/index.d.cts +14 -13
  92. package/dist/index.d.ts +14 -13
  93. package/dist/index.js +132 -27
  94. package/dist/model/index.cjs +8 -2423
  95. package/dist/model/index.cjs.map +1 -1
  96. package/dist/model/index.d.cts +433 -74
  97. package/dist/model/index.d.ts +433 -74
  98. package/dist/model/index.js +6 -7
  99. package/dist/styled-DIRkk9Uz.d.ts +506 -0
  100. package/dist/styled-DJbkJQfj.d.cts +506 -0
  101. package/dist/theory/index.cjs +172 -1676
  102. package/dist/theory/index.cjs.map +1 -1
  103. package/dist/theory/index.d.cts +151 -233
  104. package/dist/theory/index.d.ts +151 -233
  105. package/dist/theory/index.js +31 -7
  106. package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
  107. package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
  108. package/package.json +73 -24
  109. package/dist/chunk-5HTNMQW4.js.map +0 -1
  110. package/dist/chunk-7Y7WF64D.js.map +0 -1
  111. package/dist/chunk-A4532ZFR.js.map +0 -1
  112. package/dist/chunk-BHUT2S5G.js +0 -22
  113. package/dist/chunk-BHUT2S5G.js.map +0 -1
  114. package/dist/chunk-CCBLVAPK.js +0 -42
  115. package/dist/chunk-CCBLVAPK.js.map +0 -1
  116. package/dist/chunk-COKTX2G5.js.map +0 -1
  117. package/dist/chunk-DYWFNC4A.js +0 -152
  118. package/dist/chunk-DYWFNC4A.js.map +0 -1
  119. package/dist/chunk-GQKN5KDT.js.map +0 -1
  120. package/dist/chunk-LN7ZZMD2.js.map +0 -1
  121. package/dist/chunk-MSWFZF7J.js +0 -118
  122. package/dist/chunk-MSWFZF7J.js.map +0 -1
  123. package/dist/chunk-R3S2LUBH.js.map +0 -1
  124. package/dist/chunk-SDM3VJJ5.js +0 -41
  125. package/dist/chunk-SDM3VJJ5.js.map +0 -1
  126. package/dist/chunk-XTC3X6MS.js.map +0 -1
  127. package/dist/chunk-ZXW5YIWK.js +0 -55
  128. package/dist/chunk-ZXW5YIWK.js.map +0 -1
  129. package/dist/index-CGMZNCx6.d.cts +0 -140
  130. package/dist/index-CGMZNCx6.d.ts +0 -140
  131. package/dist/styled-BjXtOFNk.d.cts +0 -282
  132. package/dist/styled-D7BAx3S3.d.ts +0 -282
  133. package/src/analyze/arrange/index.ts +0 -21
  134. package/src/analyze/arrange/internal.ts +0 -125
  135. package/src/analyze/arrange/tension.ts +0 -187
  136. package/src/analyze/arrange/tracks.ts +0 -279
  137. package/src/analyze/detect/index.ts +0 -235
  138. package/src/analyze/functional/borrowed.ts +0 -84
  139. package/src/analyze/functional/cadence.ts +0 -66
  140. package/src/analyze/functional/function.ts +0 -184
  141. package/src/analyze/functional/index.ts +0 -23
  142. package/src/analyze/functional/internal.ts +0 -29
  143. package/src/analyze/functional/roman.ts +0 -393
  144. package/src/analyze/index.ts +0 -49
  145. package/src/analyze/timeline/index.ts +0 -415
  146. package/src/analyze/voice/index.ts +0 -330
  147. package/src/core/index.ts +0 -47
  148. package/src/core/interval/index.ts +0 -81
  149. package/src/core/meter/index.ts +0 -282
  150. package/src/core/pitch/index.ts +0 -311
  151. package/src/core/random/index.ts +0 -53
  152. package/src/core/tuning/index.ts +0 -156
  153. package/src/core/types.ts +0 -29
  154. package/src/generate/bass/index.ts +0 -350
  155. package/src/generate/countermelody/index.ts +0 -379
  156. package/src/generate/drums/beat.ts +0 -266
  157. package/src/generate/drums/euclid.ts +0 -58
  158. package/src/generate/drums/fills.ts +0 -356
  159. package/src/generate/drums/ghost.ts +0 -119
  160. package/src/generate/drums/hihat.ts +0 -285
  161. package/src/generate/drums/hit.ts +0 -31
  162. package/src/generate/drums/index.ts +0 -329
  163. package/src/generate/drums/internal.ts +0 -257
  164. package/src/generate/drums/kick.ts +0 -160
  165. package/src/generate/drums/percussion.ts +0 -121
  166. package/src/generate/drums/rng.ts +0 -7
  167. package/src/generate/drums/swing.ts +0 -71
  168. package/src/generate/groove/index.ts +0 -326
  169. package/src/generate/harmonize/index.ts +0 -440
  170. package/src/generate/index.ts +0 -59
  171. package/src/generate/motif/index.ts +0 -398
  172. package/src/generate/progression/index.ts +0 -370
  173. package/src/generate/reharmony/index.ts +0 -330
  174. package/src/generate/rhythm/index.ts +0 -165
  175. package/src/index.ts +0 -247
  176. package/src/model/chord.ts +0 -423
  177. package/src/model/index.ts +0 -15
  178. package/src/model/interval.ts +0 -82
  179. package/src/model/key.ts +0 -266
  180. package/src/model/note.ts +0 -176
  181. package/src/model/progression.ts +0 -161
  182. package/src/model/shared.ts +0 -12
  183. package/src/theory/chord/index.ts +0 -373
  184. package/src/theory/chordscale/index.ts +0 -374
  185. package/src/theory/counterpoint/index.ts +0 -230
  186. package/src/theory/harmony/index.ts +0 -92
  187. package/src/theory/index.ts +0 -107
  188. package/src/theory/safety/index.ts +0 -419
  189. package/src/theory/scale/degrees.ts +0 -123
  190. package/src/theory/scale/index.ts +0 -28
  191. package/src/theory/scale/key.ts +0 -56
  192. package/src/theory/scale/masks.ts +0 -153
  193. package/src/theory/spelling/index.ts +0 -182
  194. package/src/theory/symbol/index.ts +0 -268
  195. package/src/theory/voicing/index.ts +0 -5
  196. package/src/theory/voicing/internal.ts +0 -168
  197. package/src/theory/voicing/leading.ts +0 -112
  198. package/src/theory/voicing/satb.ts +0 -195
  199. package/src/theory/voicing/styled.ts +0 -176
  200. package/tsconfig.json +0 -23
@@ -1,156 +0,0 @@
1
- /**
2
- * Tuning and microtonality: frequency conversion, cents, arbitrary equal
3
- * temperaments (EDO), and just-intonation ratios.
4
- *
5
- * The rest of the library reasons in twelve-tone pitch classes, which is a
6
- * deliberate scope choice for tonal theory. This module is the escape hatch for
7
- * anyone who needs actual frequencies, cents, non-12 equal temperaments, or the
8
- * acoustic (just) tuning behind the tempered intervals.
9
- */
10
-
11
- /**
12
- * An equal-tempered tuning: a reference pitch and a number of equal divisions of
13
- * the octave. With `divisions === 12` a step index is an ordinary MIDI number.
14
- *
15
- * @category Pitch & Intervals
16
- */
17
- export type Tuning = {
18
- /** Step index (MIDI number when `divisions` is 12) whose frequency is `refFreq`. */
19
- refStep: number;
20
- /** Frequency in Hz of `refStep`. */
21
- refFreq: number;
22
- /** Equal divisions of the octave (12 standard; e.g. 19 or 31 for microtonal). */
23
- divisions: number;
24
- };
25
-
26
- /**
27
- * Standard twelve-tone equal temperament, A4 (MIDI 69) = 440 Hz.
28
- *
29
- * @category Pitch & Intervals
30
- */
31
- export const TWELVE_TET: Tuning = { refStep: 69, refFreq: 440, divisions: 12 };
32
-
33
- /**
34
- * Build an equal temperament with `n` divisions of the octave.
35
- *
36
- * @param n Divisions of the octave (e.g. 19, 24, 31).
37
- * @param refFreq Reference frequency in Hz (default 440).
38
- * @param refStep Step index of the reference (default 69).
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
47
- */
48
- export function edo(n: number, refFreq = 440, refStep = 69): Tuning {
49
- return { refStep, refFreq, divisions: n };
50
- }
51
-
52
- /**
53
- * Frequency in Hz of a step index under a tuning.
54
- *
55
- * @param step Step index (a MIDI number under 12-EDO).
56
- * @param tuning The tuning (default 12-TET).
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
65
- */
66
- export function frequencyOf(step: number, tuning: Tuning = TWELVE_TET): number {
67
- return tuning.refFreq * 2 ** ((step - tuning.refStep) / tuning.divisions);
68
- }
69
-
70
- /**
71
- * Nearest step index to a frequency under a tuning (the inverse of
72
- * {@link frequencyOf}, rounded).
73
- *
74
- * @param freq Frequency in Hz.
75
- * @param tuning The tuning (default 12-TET).
76
- * @returns The nearest step index.
77
- * @category Pitch & Intervals
78
- */
79
- export function nearestStep(freq: number, tuning: Tuning = TWELVE_TET): number {
80
- return Math.round(tuning.refStep + tuning.divisions * Math.log2(freq / tuning.refFreq));
81
- }
82
-
83
- /**
84
- * Interval in cents between two frequencies.
85
- *
86
- * @param a Lower/first frequency in Hz.
87
- * @param b Upper/second frequency in Hz.
88
- * @returns Cents from `a` to `b` (negative if `b` is lower).
89
- * @category Pitch & Intervals
90
- */
91
- export function centsBetweenFreq(a: number, b: number): number {
92
- return 1200 * Math.log2(b / a);
93
- }
94
-
95
- /**
96
- * Cents spanned by a number of equal-temperament steps.
97
- *
98
- * @param steps Number of steps.
99
- * @param tuning The tuning (default 12-TET).
100
- * @returns The cents.
101
- * @category Pitch & Intervals
102
- */
103
- export function centsOfSteps(steps: number, tuning: Tuning = TWELVE_TET): number {
104
- return (steps * 1200) / tuning.divisions;
105
- }
106
-
107
- /**
108
- * Cents of a frequency ratio, e.g. `ratioToCents(3, 2)` ≈ 701.955 for the just
109
- * perfect fifth.
110
- *
111
- * @param numerator Ratio numerator.
112
- * @param denominator Ratio denominator.
113
- * @returns The interval in cents.
114
- * @category Pitch & Intervals
115
- */
116
- export function ratioToCents(numerator: number, denominator: number): number {
117
- return 1200 * Math.log2(numerator / denominator);
118
- }
119
-
120
- /**
121
- * Five-limit just-intonation ratios for the twelve interval classes above a
122
- * unison, indexed by semitone class (0..12).
123
- *
124
- * @category Pitch & Intervals
125
- */
126
- export const JUST_RATIOS: Record<number, [number, number]> = {
127
- 0: [1, 1],
128
- 1: [16, 15],
129
- 2: [9, 8],
130
- 3: [6, 5],
131
- 4: [5, 4],
132
- 5: [4, 3],
133
- 6: [45, 32],
134
- 7: [3, 2],
135
- 8: [8, 5],
136
- 9: [5, 3],
137
- 10: [9, 5],
138
- 11: [15, 8],
139
- 12: [2, 1],
140
- };
141
-
142
- /**
143
- * Cents by which a five-limit just interval departs from its 12-TET tempering
144
- * (positive means the just interval is wider).
145
- *
146
- * @param semitoneClass Semitone class in [0, 12].
147
- * @returns The deviation in cents, or NaN if the class has no listed ratio.
148
- * @category Pitch & Intervals
149
- */
150
- export function justDeviationCents(semitoneClass: number): number {
151
- const ratio = JUST_RATIOS[semitoneClass];
152
- if (!ratio) {
153
- return Number.NaN;
154
- }
155
- return ratioToCents(ratio[0], ratio[1]) - semitoneClass * 100;
156
- }
package/src/core/types.ts DELETED
@@ -1,29 +0,0 @@
1
- /**
2
- * A key/scale definition anchored on a root pitch class.
3
- *
4
- * `modeMask12` is a 12-bit mask where bit `n` set means the pitch class
5
- * `(rootPc + n) % 12` belongs to the scale. Bit 0 (the root) is always set;
6
- * {@link maskFromOffsets} enforces this so the root is always a scale tone.
7
- */
8
- export type KeyScale = {
9
- rootPc: number;
10
- modeMask12: number;
11
- };
12
-
13
- /**
14
- * A single sounding note on a beat-indexed timeline: an absolute MIDI pitch, an
15
- * onset and duration measured in quarter-note beats, and an optional MIDI
16
- * velocity. This is the library-wide interchange shape for note events read
17
- * from or written to a DAW/MIDI track; the harmonizer's `MelodyNote` and the
18
- * analysis `VoiceNote` are specializations of it.
19
- */
20
- export type NoteEvent = {
21
- /** MIDI pitch (middle C = 60). */
22
- pitch: number;
23
- /** Onset in quarter-note beats, absolute from the start of the timeline. */
24
- startBeat: number;
25
- /** Duration in quarter-note beats. */
26
- durationBeat: number;
27
- /** MIDI velocity in [0, 127], when known. */
28
- velocity?: number;
29
- };
@@ -1,350 +0,0 @@
1
- /**
2
- * Deterministic bass-line generation over a chord placement.
3
- *
4
- * Given a sequence of chord segments and a key, this builds a monophonic bass
5
- * line in a chosen low register. Several idiomatic styles are supported, from a
6
- * single held root per segment to a quarter-note walking line that leads by
7
- * step into each chord change. All pitches are kept in a narrow bass band and
8
- * consecutive notes move by small intervals; given a seed the output is fully
9
- * reproducible.
10
- *
11
- * Positions and durations are measured in quarter-note beats, matching the
12
- * library-wide convention.
13
- */
14
-
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
-
21
- /**
22
- * A chord sounding over a half-open beat span `[startBeat, endBeat)`.
23
- *
24
- * @category Composition
25
- */
26
- export type BassSegment = {
27
- startBeat: number;
28
- endBeat: number;
29
- chord: Chord;
30
- };
31
-
32
- /**
33
- * The bass-line idiom to generate.
34
- *
35
- * @category Composition
36
- */
37
- export type BassStyle = 'root' | 'rootFifth' | 'pop' | 'walking' | 'arpeggio';
38
-
39
- /**
40
- * Options controlling {@link generateBassLine}.
41
- *
42
- * @category Composition
43
- */
44
- export type BassLineOptions = {
45
- /** Chord placement to follow; need not be pre-sorted. */
46
- segments: BassSegment[];
47
- /** Key/scale context, used for diatonic approach tones in `walking`. */
48
- key: KeyScale;
49
- /**
50
- * Time signature; used for metric accents.
51
- *
52
- * @defaultValue `{ numerator: 4, denominator: 4 }`
53
- */
54
- ts?: TimeSignature;
55
- /**
56
- * Bass-line idiom.
57
- *
58
- * @defaultValue `'root'`
59
- */
60
- style?: BassStyle;
61
- /**
62
- * Target register as a base MIDI octave; roots land around `octave*12+12`.
63
- *
64
- * @defaultValue 2
65
- */
66
- octave?: number;
67
- /**
68
- * Seed for the deterministic PRNG.
69
- *
70
- * @defaultValue 0
71
- */
72
- seed?: number;
73
- };
74
-
75
- const DEFAULT_TS: TimeSignature = { numerator: 4, denominator: 4 };
76
- const DEFAULT_STYLE: BassStyle = 'root';
77
- const DEFAULT_OCTAVE = 2;
78
-
79
- /** Velocity for notes on metrically strong positions. */
80
- const STRONG_VELOCITY = 100;
81
- /** Velocity for notes on weak positions. */
82
- const WEAK_VELOCITY = 80;
83
- /** Probability of a weak-beat pickup in the `pop` style. */
84
- const PICKUP_PROB = 0.35;
85
-
86
- const EPS = 1e-9;
87
-
88
- /** Reduce a value to a pitch class in [0, 11]. */
89
- function pitchClass(value: number): number {
90
- return ((Math.trunc(value) % 12) + 12) % 12;
91
- }
92
-
93
- /**
94
- * Place a pitch class as a MIDI note near an anchor, clamped to the bass band.
95
- *
96
- * The octave nearest the anchor is chosen first (so motion from the previous
97
- * note is minimal), then the result is shifted by whole octaves into the band
98
- * `[low, low + 12]` if it falls outside.
99
- */
100
- function placePc(pc: number, anchor: number, low: number): number {
101
- const high = low + 12;
102
- let midi = pc + 12 * Math.round((anchor - pc) / 12);
103
- while (midi < low) {
104
- midi += 12;
105
- }
106
- while (midi > high) {
107
- midi -= 12;
108
- }
109
- return midi;
110
- }
111
-
112
- /** Chord-tone pitch classes in stacked-thirds order, deduplicated. */
113
- function chordTonePcs(chord: Chord): number[] {
114
- const seen = new Set<number>();
115
- const pcs: number[] = [];
116
- for (const interval of chord.intervals) {
117
- const pc = pitchClass(chord.rootPc + interval);
118
- if (!seen.has(pc)) {
119
- seen.add(pc);
120
- pcs.push(pc);
121
- }
122
- }
123
- if (pcs.length === 0) {
124
- pcs.push(pitchClass(chord.rootPc));
125
- }
126
- return pcs;
127
- }
128
-
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
- */
137
- function fifthPcOf(chord: Chord): number {
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);
141
- }
142
-
143
- /** The sounding bass pitch class of a chord: its slash bass, else its root. */
144
- function bassPcOf(chord: Chord): number {
145
- return pitchClass(chord.bassPc ?? chord.rootPc);
146
- }
147
-
148
- /** Integer quarter-note beat positions within `[start, end)`, at least one. */
149
- function beatPositions(start: number, end: number): number[] {
150
- const positions: number[] = [];
151
- for (let p = start; p < end - EPS; p += 1) {
152
- positions.push(p);
153
- }
154
- if (positions.length === 0) {
155
- positions.push(start);
156
- }
157
- return positions;
158
- }
159
-
160
- type RawNote = { startBeat: number; pitch: number; velocity: number };
161
-
162
- /** Working state threaded through the per-segment builders. */
163
- type BuildContext = {
164
- ts: TimeSignature;
165
- low: number;
166
- key: KeyScale;
167
- rng: ReturnType<typeof createRng>;
168
- notes: RawNote[];
169
- prevMidi: number;
170
- };
171
-
172
- /** Append a note for pitch class `pc` at `pos`, placed near the running anchor. */
173
- function emit(ctx: BuildContext, pos: number, pc: number, midiOverride?: number): number {
174
- const midi = midiOverride ?? placePc(pc, ctx.prevMidi, ctx.low);
175
- const velocity = isStrongBeat(pos, ctx.ts) ? STRONG_VELOCITY : WEAK_VELOCITY;
176
- ctx.notes.push({ startBeat: pos, pitch: midi, velocity });
177
- return midi;
178
- }
179
-
180
- /** One root note per segment. */
181
- function buildRoot(ctx: BuildContext, seg: BassSegment): void {
182
- ctx.prevMidi = emit(ctx, seg.startBeat, bassPcOf(seg.chord));
183
- }
184
-
185
- /** Root on the downbeat, fifth on the segment's midpoint. */
186
- function buildRootFifth(ctx: BuildContext, seg: BassSegment): void {
187
- ctx.prevMidi = emit(ctx, seg.startBeat, bassPcOf(seg.chord));
188
- const mid = (seg.startBeat + seg.endBeat) / 2;
189
- if (mid > seg.startBeat + EPS && mid < seg.endBeat - EPS) {
190
- ctx.prevMidi = emit(ctx, mid, fifthPcOf(seg.chord));
191
- }
192
- }
193
-
194
- /** Root on every strong beat, with occasional octave/fifth pickups on weak beats. */
195
- function buildPop(ctx: BuildContext, seg: BassSegment): void {
196
- const positions = beatPositions(seg.startBeat, seg.endBeat);
197
- const rootPc = bassPcOf(seg.chord);
198
- let emitted = false;
199
- for (const pos of positions) {
200
- if (isStrongBeat(pos, ctx.ts)) {
201
- ctx.prevMidi = emit(ctx, pos, rootPc);
202
- emitted = true;
203
- } else if (ctx.rng.prob(PICKUP_PROB)) {
204
- if (ctx.rng.prob(0.5)) {
205
- // Octave pickup: the root dropped toward the bottom of the register,
206
- // clamped so it never falls below the declared band.
207
- const base = placePc(rootPc, ctx.prevMidi, ctx.low);
208
- emit(ctx, pos, rootPc, Math.max(ctx.low, base - 12));
209
- } else {
210
- ctx.prevMidi = emit(ctx, pos, fifthPcOf(seg.chord));
211
- }
212
- }
213
- }
214
- if (!emitted) {
215
- ctx.prevMidi = emit(ctx, seg.startBeat, rootPc);
216
- }
217
- }
218
-
219
- /** Cycle root -> third -> fifth (-> seventh) across the segment's beats. */
220
- function buildArpeggio(ctx: BuildContext, seg: BassSegment): void {
221
- const tones = chordTonePcs(seg.chord);
222
- const positions = beatPositions(seg.startBeat, seg.endBeat);
223
- positions.forEach((pos, i) => {
224
- const pc = tones[i % tones.length] ?? bassPcOf(seg.chord);
225
- ctx.prevMidi = emit(ctx, pos, pc);
226
- });
227
- }
228
-
229
- /** A diatonic or chromatic neighbor of `target`, a step toward `from`. */
230
- function approachNote(ctx: BuildContext, target: number, from: number): number {
231
- const dir = from <= target ? -1 : 1;
232
- if (ctx.rng.prob(0.5)) {
233
- return target + dir; // chromatic semitone
234
- }
235
- const cand = nearestScaleTone(target + dir * 2, ctx.key);
236
- const step = Math.abs(cand - target);
237
- return step >= 1 && step <= 2 ? cand : target + dir;
238
- }
239
-
240
- /** A quarter-note line of chord tones that leads by step into each chord change. */
241
- function buildWalking(ctx: BuildContext, seg: BassSegment, next: BassSegment | undefined): void {
242
- const tones = chordTonePcs(seg.chord);
243
- const positions = beatPositions(seg.startBeat, seg.endBeat);
244
- const count = positions.length;
245
- positions.forEach((pos, i) => {
246
- if (i === 0) {
247
- ctx.prevMidi = emit(ctx, pos, bassPcOf(seg.chord));
248
- return;
249
- }
250
- if (next && count > 1 && i === count - 1) {
251
- const nextBass = placePc(bassPcOf(next.chord), ctx.prevMidi, ctx.low);
252
- const midi = approachNote(ctx, nextBass, ctx.prevMidi);
253
- ctx.prevMidi = emit(ctx, pos, pitchClass(midi), midi);
254
- return;
255
- }
256
- const pc = tones[i % tones.length] ?? bassPcOf(seg.chord);
257
- ctx.prevMidi = emit(ctx, pos, pc);
258
- });
259
- }
260
-
261
- /**
262
- * Generate a bass line following a chord placement.
263
- *
264
- * Each segment contributes notes in the chosen bass register per the selected
265
- * style; consecutive notes are kept within roughly a fifth. Every note's
266
- * duration extends to the next onset (the final note extends to the last
267
- * segment's end). Given a seed the output is fully reproducible.
268
- *
269
- * @param opts Segments, key, and generation options.
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
283
- */
284
- export function generateBassLine(opts: BassLineOptions): NoteEvent[] {
285
- const segments = [...opts.segments].sort((a, b) => a.startBeat - b.startBeat);
286
- if (segments.length === 0) {
287
- return [];
288
- }
289
-
290
- const ts = opts.ts ?? DEFAULT_TS;
291
- const style = opts.style ?? DEFAULT_STYLE;
292
- const octave = opts.octave ?? DEFAULT_OCTAVE;
293
- const low = octave * 12 + 12;
294
-
295
- const ctx: BuildContext = {
296
- ts,
297
- low,
298
- key: opts.key,
299
- rng: createRng(opts.seed ?? 0),
300
- notes: [],
301
- prevMidi: low,
302
- };
303
-
304
- for (let s = 0; s < segments.length; s += 1) {
305
- const seg = segments[s];
306
- if (!seg) {
307
- continue;
308
- }
309
- switch (style) {
310
- case 'rootFifth':
311
- buildRootFifth(ctx, seg);
312
- break;
313
- case 'pop':
314
- buildPop(ctx, seg);
315
- break;
316
- case 'walking':
317
- buildWalking(ctx, seg, segments[s + 1]);
318
- break;
319
- case 'arpeggio':
320
- buildArpeggio(ctx, seg);
321
- break;
322
- default:
323
- buildRoot(ctx, seg);
324
- break;
325
- }
326
- }
327
-
328
- ctx.notes.sort((a, b) => a.startBeat - b.startBeat);
329
- const lastEnd = segments.reduce((m, seg) => Math.max(m, seg.endBeat), Number.NEGATIVE_INFINITY);
330
-
331
- const out: NoteEvent[] = [];
332
- for (let i = 0; i < ctx.notes.length; i += 1) {
333
- const note = ctx.notes[i];
334
- if (!note) {
335
- continue;
336
- }
337
- const nextStart = ctx.notes[i + 1]?.startBeat ?? lastEnd;
338
- const durationBeat = nextStart - note.startBeat;
339
- if (durationBeat <= EPS) {
340
- continue;
341
- }
342
- out.push({
343
- pitch: note.pitch,
344
- startBeat: note.startBeat,
345
- durationBeat,
346
- velocity: note.velocity,
347
- });
348
- }
349
- return out;
350
- }