@libraz/libcantus 0.9.3 → 0.9.5

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 (201) hide show
  1. package/README.md +29 -20
  2. package/README_ja.md +268 -0
  3. package/dist/analyze/index.cjs +39 -1947
  4. package/dist/analyze/index.cjs.map +1 -1
  5. package/dist/analyze/index.d.cts +161 -91
  6. package/dist/analyze/index.d.ts +161 -91
  7. package/dist/analyze/index.js +19 -8
  8. package/dist/chunk-2A2QMA74.cjs +261 -0
  9. package/dist/chunk-2A2QMA74.cjs.map +1 -0
  10. package/dist/chunk-2PTFGVD7.js +261 -0
  11. package/dist/chunk-2PTFGVD7.js.map +1 -0
  12. package/dist/chunk-357ZDPVU.cjs +162 -0
  13. package/dist/chunk-357ZDPVU.cjs.map +1 -0
  14. package/dist/chunk-3KX76RLG.js +1548 -0
  15. package/dist/chunk-3KX76RLG.js.map +1 -0
  16. package/dist/chunk-4VZ5MZAM.cjs +889 -0
  17. package/dist/chunk-4VZ5MZAM.cjs.map +1 -0
  18. package/dist/{chunk-C5XYKUVG.js → chunk-7EONYVI5.js} +471 -171
  19. package/dist/chunk-7EONYVI5.js.map +1 -0
  20. package/dist/{chunk-IT2O4XFE.js → chunk-7JEP3MFY.js} +42 -34
  21. package/dist/chunk-7JEP3MFY.js.map +1 -0
  22. package/dist/{chunk-4MHA432G.js → chunk-AID75RN3.js} +572 -250
  23. package/dist/chunk-AID75RN3.js.map +1 -0
  24. package/dist/chunk-BSLTLGM3.cjs +497 -0
  25. package/dist/chunk-BSLTLGM3.cjs.map +1 -0
  26. package/dist/chunk-CU6VG2YM.cjs +109 -0
  27. package/dist/chunk-CU6VG2YM.cjs.map +1 -0
  28. package/dist/{chunk-EH6EM37U.js → chunk-DZMHIGI4.js} +81 -59
  29. package/dist/chunk-DZMHIGI4.js.map +1 -0
  30. package/dist/{chunk-AKLYPWIN.js → chunk-EDOTEABK.js} +45 -19
  31. package/dist/chunk-EDOTEABK.js.map +1 -0
  32. package/dist/{chunk-2ZWMSJH6.js → chunk-HN2OEUN2.js} +7 -4
  33. package/dist/chunk-HN2OEUN2.js.map +1 -0
  34. package/dist/{chunk-MIWO425M.js → chunk-KBTTEX7Q.js} +128 -43
  35. package/dist/chunk-KBTTEX7Q.js.map +1 -0
  36. package/dist/chunk-KQN2ZYND.cjs +724 -0
  37. package/dist/chunk-KQN2ZYND.cjs.map +1 -0
  38. package/dist/chunk-MPTVJIVG.cjs +3176 -0
  39. package/dist/chunk-MPTVJIVG.cjs.map +1 -0
  40. package/dist/chunk-MTMZTMWM.cjs +53 -0
  41. package/dist/chunk-MTMZTMWM.cjs.map +1 -0
  42. package/dist/{chunk-CHMXELDS.js → chunk-NNB67XA2.js} +372 -197
  43. package/dist/chunk-NNB67XA2.js.map +1 -0
  44. package/dist/chunk-OL3IHHSJ.cjs +1204 -0
  45. package/dist/chunk-OL3IHHSJ.cjs.map +1 -0
  46. package/dist/chunk-PI3UXRWK.cjs +528 -0
  47. package/dist/chunk-PI3UXRWK.cjs.map +1 -0
  48. package/dist/chunk-R2VVFAIN.js +528 -0
  49. package/dist/chunk-R2VVFAIN.js.map +1 -0
  50. package/dist/chunk-T64C7ZD7.cjs +1 -0
  51. package/dist/chunk-T64C7ZD7.cjs.map +1 -0
  52. package/dist/chunk-WFDXOLUB.cjs +337 -0
  53. package/dist/chunk-WFDXOLUB.cjs.map +1 -0
  54. package/dist/chunk-WTAN4ACG.cjs +1548 -0
  55. package/dist/chunk-WTAN4ACG.cjs.map +1 -0
  56. package/dist/chunk-YFJXS3AU.cjs +33 -0
  57. package/dist/chunk-YFJXS3AU.cjs.map +1 -0
  58. package/dist/{chunk-GOAW4VYL.js → chunk-YUE6TJ4M.js} +315 -145
  59. package/dist/chunk-YUE6TJ4M.js.map +1 -0
  60. package/dist/{chunk-62QZKCJQ.js → chunk-ZGLJM4A4.js} +10 -6
  61. package/dist/chunk-ZGLJM4A4.js.map +1 -0
  62. package/dist/core/index.cjs +140 -647
  63. package/dist/core/index.cjs.map +1 -1
  64. package/dist/core/index.d.cts +313 -41
  65. package/dist/core/index.d.ts +313 -41
  66. package/dist/core/index.js +64 -16
  67. package/dist/{function-DVH4Eusr.d.ts → function-CxixTB02.d.cts} +122 -10
  68. package/dist/{function-DYt4iKsn.d.cts → function-DTCt675z.d.ts} +122 -10
  69. package/dist/generate/index.cjs +28 -4085
  70. package/dist/generate/index.cjs.map +1 -1
  71. package/dist/generate/index.d.cts +374 -66
  72. package/dist/generate/index.d.ts +374 -66
  73. package/dist/generate/index.js +23 -9
  74. package/dist/{index-Jc0Ryi79.d.ts → index-B2Ulxxpo.d.cts} +21 -5
  75. package/dist/{index-CpOsP1y9.d.cts → index-BkYJ0xMq.d.cts} +18 -17
  76. package/dist/{index-CxqBQtH1.d.cts → index-BuaB-ypt.d.ts} +21 -5
  77. package/dist/index-CAN7v-kw.d.ts +199 -0
  78. package/dist/{index-Bt4tOUD9.d.ts → index-COw1Ky9g.d.ts} +93 -8
  79. package/dist/{index-Dikst44v.d.cts → index-Cfx8xgH0.d.ts} +14 -11
  80. package/dist/{index-DxvARIP2.d.ts → index-DRtIhlJj.d.ts} +18 -17
  81. package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
  82. package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
  83. package/dist/index-alrRninU.d.cts +199 -0
  84. package/dist/{index-BNlGP86A.d.cts → index-oTQbYYIs.d.cts} +93 -8
  85. package/dist/index-tR9jTn26.d.cts +275 -0
  86. package/dist/index-tR9jTn26.d.ts +275 -0
  87. package/dist/{index-Bg5dYMmZ.d.ts → index-x2vcsduE.d.cts} +14 -11
  88. package/dist/index.cjs +429 -7383
  89. package/dist/index.cjs.map +1 -1
  90. package/dist/index.d.cts +15 -15
  91. package/dist/index.d.ts +15 -15
  92. package/dist/index.js +121 -27
  93. package/dist/model/index.cjs +8 -2562
  94. package/dist/model/index.cjs.map +1 -1
  95. package/dist/model/index.d.cts +471 -81
  96. package/dist/model/index.d.ts +471 -81
  97. package/dist/model/index.js +6 -7
  98. package/dist/styled-Bl0ZK28h.d.ts +543 -0
  99. package/dist/styled-Crv6SA4S.d.cts +543 -0
  100. package/dist/theory/index.cjs +172 -1822
  101. package/dist/theory/index.cjs.map +1 -1
  102. package/dist/theory/index.d.cts +151 -233
  103. package/dist/theory/index.d.ts +151 -233
  104. package/dist/theory/index.js +30 -7
  105. package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
  106. package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
  107. package/package.json +34 -13
  108. package/dist/chunk-2ZWMSJH6.js.map +0 -1
  109. package/dist/chunk-4MHA432G.js.map +0 -1
  110. package/dist/chunk-62QZKCJQ.js.map +0 -1
  111. package/dist/chunk-AKLYPWIN.js.map +0 -1
  112. package/dist/chunk-C5XYKUVG.js.map +0 -1
  113. package/dist/chunk-CHMXELDS.js.map +0 -1
  114. package/dist/chunk-DYWFNC4A.js +0 -152
  115. package/dist/chunk-DYWFNC4A.js.map +0 -1
  116. package/dist/chunk-EH6EM37U.js.map +0 -1
  117. package/dist/chunk-FBQK3ZQN.js +0 -131
  118. package/dist/chunk-FBQK3ZQN.js.map +0 -1
  119. package/dist/chunk-GOAW4VYL.js.map +0 -1
  120. package/dist/chunk-I5R7ZBJS.js +0 -202
  121. package/dist/chunk-I5R7ZBJS.js.map +0 -1
  122. package/dist/chunk-IT2O4XFE.js.map +0 -1
  123. package/dist/chunk-MIWO425M.js.map +0 -1
  124. package/dist/chunk-SGU7SPLK.js +0 -924
  125. package/dist/chunk-SGU7SPLK.js.map +0 -1
  126. package/dist/index-CGMZNCx6.d.cts +0 -140
  127. package/dist/index-CGMZNCx6.d.ts +0 -140
  128. package/dist/index-CnZCo29u.d.cts +0 -108
  129. package/dist/index-JnF4v9bx.d.ts +0 -108
  130. package/dist/styled-DZ7GjShb.d.ts +0 -282
  131. package/dist/styled-ZTbdaVFM.d.cts +0 -282
  132. package/src/analyze/arrange/index.ts +0 -21
  133. package/src/analyze/arrange/internal.ts +0 -127
  134. package/src/analyze/arrange/tension.ts +0 -199
  135. package/src/analyze/arrange/tracks.ts +0 -312
  136. package/src/analyze/detect/index.ts +0 -272
  137. package/src/analyze/functional/borrowed.ts +0 -84
  138. package/src/analyze/functional/cadence.ts +0 -66
  139. package/src/analyze/functional/function.ts +0 -184
  140. package/src/analyze/functional/index.ts +0 -23
  141. package/src/analyze/functional/internal.ts +0 -29
  142. package/src/analyze/functional/roman.ts +0 -402
  143. package/src/analyze/index.ts +0 -49
  144. package/src/analyze/timeline/index.ts +0 -451
  145. package/src/analyze/voice/index.ts +0 -339
  146. package/src/core/event-index/index.ts +0 -104
  147. package/src/core/index.ts +0 -61
  148. package/src/core/interval/index.ts +0 -81
  149. package/src/core/meter/index.ts +0 -302
  150. package/src/core/pitch/index.ts +0 -311
  151. package/src/core/random/index.ts +0 -76
  152. package/src/core/tuning/index.ts +0 -186
  153. package/src/core/types.ts +0 -29
  154. package/src/core/validation/index.ts +0 -119
  155. package/src/generate/bass/index.ts +0 -373
  156. package/src/generate/countermelody/index.ts +0 -470
  157. package/src/generate/drums/beat.ts +0 -266
  158. package/src/generate/drums/euclid.ts +0 -65
  159. package/src/generate/drums/fills.ts +0 -356
  160. package/src/generate/drums/ghost.ts +0 -119
  161. package/src/generate/drums/hihat.ts +0 -285
  162. package/src/generate/drums/hit.ts +0 -31
  163. package/src/generate/drums/index.ts +0 -361
  164. package/src/generate/drums/internal.ts +0 -257
  165. package/src/generate/drums/kick.ts +0 -160
  166. package/src/generate/drums/percussion.ts +0 -121
  167. package/src/generate/drums/rng.ts +0 -7
  168. package/src/generate/drums/swing.ts +0 -71
  169. package/src/generate/groove/index.ts +0 -351
  170. package/src/generate/harmonize/index.ts +0 -451
  171. package/src/generate/index.ts +0 -58
  172. package/src/generate/motif/index.ts +0 -422
  173. package/src/generate/progression/index.ts +0 -362
  174. package/src/generate/reharmony/index.ts +0 -323
  175. package/src/generate/rhythm/index.ts +0 -175
  176. package/src/index.ts +0 -13
  177. package/src/model/chord.ts +0 -442
  178. package/src/model/index.ts +0 -15
  179. package/src/model/interval.ts +0 -82
  180. package/src/model/key.ts +0 -266
  181. package/src/model/note.ts +0 -176
  182. package/src/model/progression.ts +0 -161
  183. package/src/model/shared.ts +0 -12
  184. package/src/theory/chord/index.ts +0 -375
  185. package/src/theory/chordscale/index.ts +0 -374
  186. package/src/theory/counterpoint/index.ts +0 -230
  187. package/src/theory/harmony/index.ts +0 -77
  188. package/src/theory/index.ts +0 -112
  189. package/src/theory/safety/index.ts +0 -488
  190. package/src/theory/scale/degrees.ts +0 -123
  191. package/src/theory/scale/index.ts +0 -28
  192. package/src/theory/scale/key.ts +0 -56
  193. package/src/theory/scale/masks.ts +0 -153
  194. package/src/theory/spelling/index.ts +0 -249
  195. package/src/theory/symbol/index.ts +0 -268
  196. package/src/theory/voicing/index.ts +0 -5
  197. package/src/theory/voicing/internal.ts +0 -168
  198. package/src/theory/voicing/leading.ts +0 -112
  199. package/src/theory/voicing/satb.ts +0 -216
  200. package/src/theory/voicing/styled.ts +0 -199
  201. package/tsconfig.json +0 -23
@@ -1,186 +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
- import {
12
- assertFiniteNumber,
13
- assertInteger,
14
- assertPositiveInt,
15
- assertRange,
16
- } from '../validation/index.js';
17
-
18
- /**
19
- * An equal-tempered tuning: a reference pitch and a number of equal divisions of
20
- * the octave. With `divisions === 12` a step index is an ordinary MIDI number.
21
- *
22
- * @category Pitch & Intervals
23
- */
24
- export type Tuning = {
25
- /** Step index (MIDI number when `divisions` is 12) whose frequency is `refFreq`. */
26
- refStep: number;
27
- /** Frequency in Hz of `refStep`. */
28
- refFreq: number;
29
- /** Equal divisions of the octave (12 standard; e.g. 19 or 31 for microtonal). */
30
- divisions: number;
31
- };
32
-
33
- /**
34
- * Standard twelve-tone equal temperament, A4 (MIDI 69) = 440 Hz.
35
- *
36
- * @category Pitch & Intervals
37
- */
38
- export const TWELVE_TET: Tuning = { refStep: 69, refFreq: 440, divisions: 12 };
39
-
40
- function assertTuning(tuning: Tuning): Tuning {
41
- assertFiniteNumber(tuning.refStep, 'tuning.refStep');
42
- assertRange(tuning.refFreq, Number.MIN_VALUE, Number.MAX_VALUE, 'tuning.refFreq');
43
- assertPositiveInt(tuning.divisions, 'tuning.divisions');
44
- return tuning;
45
- }
46
-
47
- /**
48
- * Build an equal temperament with `n` divisions of the octave.
49
- *
50
- * @param n Divisions of the octave (e.g. 19, 24, 31).
51
- * @param refFreq Reference frequency in Hz (default 440).
52
- * @param refStep Step index of the reference (default 69).
53
- * @returns The tuning.
54
- * @example
55
- * ```ts
56
- * import { edo, frequencyOf } from '@libraz/libcantus';
57
- * const et19 = edo(19); // { refStep: 69, refFreq: 440, divisions: 19 }
58
- * frequencyOf(70, et19); // one 19-EDO step above A4
59
- * ```
60
- * @category Pitch & Intervals
61
- */
62
- export function edo(n: number, refFreq = 440, refStep = 69): Tuning {
63
- assertPositiveInt(n, 'EDO divisions');
64
- assertRange(refFreq, Number.MIN_VALUE, Number.MAX_VALUE, 'reference frequency');
65
- assertFiniteNumber(refStep, 'reference step');
66
- return { refStep, refFreq, divisions: n };
67
- }
68
-
69
- /**
70
- * Frequency in Hz of a step index under a tuning.
71
- *
72
- * @param step Step index (a MIDI number under 12-EDO).
73
- * @param tuning The tuning (default 12-TET).
74
- * @returns The frequency in Hz.
75
- * @example
76
- * ```ts
77
- * import { frequencyOf } from '@libraz/libcantus';
78
- * frequencyOf(69); // 440 (A4 in 12-TET)
79
- * frequencyOf(60); // middle C in Hz
80
- * ```
81
- * @category Pitch & Intervals
82
- */
83
- export function frequencyOf(step: number, tuning: Tuning = TWELVE_TET): number {
84
- assertFiniteNumber(step, 'step');
85
- assertTuning(tuning);
86
- const result = tuning.refFreq * 2 ** ((step - tuning.refStep) / tuning.divisions);
87
- return assertRange(result, Number.MIN_VALUE, Number.MAX_VALUE, 'frequency result');
88
- }
89
-
90
- /**
91
- * Nearest step index to a frequency under a tuning (the inverse of
92
- * {@link frequencyOf}, rounded).
93
- *
94
- * @param freq Frequency in Hz.
95
- * @param tuning The tuning (default 12-TET).
96
- * @returns The nearest step index.
97
- * @category Pitch & Intervals
98
- */
99
- export function nearestStep(freq: number, tuning: Tuning = TWELVE_TET): number {
100
- assertRange(freq, Number.MIN_VALUE, Number.MAX_VALUE, 'frequency');
101
- assertTuning(tuning);
102
- return Math.round(tuning.refStep + tuning.divisions * Math.log2(freq / tuning.refFreq));
103
- }
104
-
105
- /**
106
- * Interval in cents between two frequencies.
107
- *
108
- * @param a Lower/first frequency in Hz.
109
- * @param b Upper/second frequency in Hz.
110
- * @returns Cents from `a` to `b` (negative if `b` is lower).
111
- * @category Pitch & Intervals
112
- */
113
- export function centsBetweenFreq(a: number, b: number): number {
114
- assertRange(a, Number.MIN_VALUE, Number.MAX_VALUE, 'first frequency');
115
- assertRange(b, Number.MIN_VALUE, Number.MAX_VALUE, 'second frequency');
116
- return 1200 * (Math.log2(b) - Math.log2(a));
117
- }
118
-
119
- /**
120
- * Cents spanned by a number of equal-temperament steps.
121
- *
122
- * @param steps Number of steps.
123
- * @param tuning The tuning (default 12-TET).
124
- * @returns The cents.
125
- * @category Pitch & Intervals
126
- */
127
- export function centsOfSteps(steps: number, tuning: Tuning = TWELVE_TET): number {
128
- assertFiniteNumber(steps, 'steps');
129
- assertTuning(tuning);
130
- return assertFiniteNumber((steps * 1200) / tuning.divisions, 'cents result');
131
- }
132
-
133
- /**
134
- * Cents of a frequency ratio, e.g. `ratioToCents(3, 2)` ≈ 701.955 for the just
135
- * perfect fifth.
136
- *
137
- * @param numerator Ratio numerator.
138
- * @param denominator Ratio denominator.
139
- * @returns The interval in cents.
140
- * @category Pitch & Intervals
141
- */
142
- export function ratioToCents(numerator: number, denominator: number): number {
143
- assertRange(numerator, Number.MIN_VALUE, Number.MAX_VALUE, 'ratio numerator');
144
- assertRange(denominator, Number.MIN_VALUE, Number.MAX_VALUE, 'ratio denominator');
145
- return 1200 * (Math.log2(numerator) - Math.log2(denominator));
146
- }
147
-
148
- /**
149
- * Five-limit just-intonation ratios for the twelve interval classes above a
150
- * unison, indexed by semitone class (0..12).
151
- *
152
- * @category Pitch & Intervals
153
- */
154
- export const JUST_RATIOS: Record<number, [number, number]> = {
155
- 0: [1, 1],
156
- 1: [16, 15],
157
- 2: [9, 8],
158
- 3: [6, 5],
159
- 4: [5, 4],
160
- 5: [4, 3],
161
- 6: [45, 32],
162
- 7: [3, 2],
163
- 8: [8, 5],
164
- 9: [5, 3],
165
- 10: [9, 5],
166
- 11: [15, 8],
167
- 12: [2, 1],
168
- };
169
-
170
- /**
171
- * Cents by which a five-limit just interval departs from its 12-TET tempering
172
- * (positive means the just interval is wider).
173
- *
174
- * @param semitoneClass Semitone class in [0, 12].
175
- * @returns The deviation in cents.
176
- * @throws If `semitoneClass` is not an integer in [0, 12].
177
- * @category Pitch & Intervals
178
- */
179
- export function justDeviationCents(semitoneClass: number): number {
180
- assertInteger(semitoneClass, 'semitone class', 0, 12);
181
- const ratio = JUST_RATIOS[semitoneClass];
182
- if (!ratio) {
183
- throw new RangeError(`semitone class has no just ratio: ${semitoneClass}`);
184
- }
185
- return ratioToCents(ratio[0], ratio[1]) - semitoneClass * 100;
186
- }
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,119 +0,0 @@
1
- import type { TimeSignature } from '../meter/index.js';
2
- import type { NoteEvent } from '../types.js';
3
-
4
- /** Default upper bound for synchronous event/window/candidate generation. */
5
- export const DEFAULT_GENERATION_BUDGET = 1_000_000;
6
-
7
- /** Require a finite JavaScript number and return it unchanged. */
8
- export function assertFiniteNumber(value: number, name: string): number {
9
- if (!Number.isFinite(value)) {
10
- throw new RangeError(`${name} must be finite; received ${value}`);
11
- }
12
- return value;
13
- }
14
-
15
- /** Require an integer in the inclusive range `[min, max]`. */
16
- export function assertInteger(
17
- value: number,
18
- name: string,
19
- min = Number.MIN_SAFE_INTEGER,
20
- max = Number.MAX_SAFE_INTEGER,
21
- ): number {
22
- assertFiniteNumber(value, name);
23
- if (!Number.isSafeInteger(value) || value < min || value > max) {
24
- throw new RangeError(`${name} must be an integer in [${min}, ${max}]; received ${value}`);
25
- }
26
- return value;
27
- }
28
-
29
- /** Require a positive safe integer, optionally capped by `max`. */
30
- export function assertPositiveInt(
31
- value: number,
32
- name: string,
33
- max = DEFAULT_GENERATION_BUDGET,
34
- ): number {
35
- return assertInteger(value, name, 1, max);
36
- }
37
-
38
- /** Require a finite number in the inclusive range `[min, max]`. */
39
- export function assertRange(value: number, min: number, max: number, name: string): number {
40
- assertFiniteNumber(value, name);
41
- if (value < min || value > max) {
42
- throw new RangeError(`${name} must be in [${min}, ${max}]; received ${value}`);
43
- }
44
- return value;
45
- }
46
-
47
- /** Reject work estimates that would exceed a synchronous generation budget. */
48
- export function assertGenerationBudget(
49
- estimated: number,
50
- name: string,
51
- limit = DEFAULT_GENERATION_BUDGET,
52
- ): number {
53
- assertFiniteNumber(estimated, name);
54
- assertPositiveInt(limit, `${name} limit`, Number.MAX_SAFE_INTEGER);
55
- if (estimated < 0 || estimated > limit) {
56
- throw new RangeError(`${name} exceeds the generation budget ${limit}; received ${estimated}`);
57
- }
58
- return estimated;
59
- }
60
-
61
- /** Validate a time signature, including additive grouping, before any early return. */
62
- export function assertTimeSignature(ts: TimeSignature, name = 'time signature'): TimeSignature {
63
- assertPositiveInt(ts.numerator, `${name}.numerator`);
64
- assertPositiveInt(ts.denominator, `${name}.denominator`);
65
- if (ts.grouping !== undefined) {
66
- if (ts.grouping.length === 0) {
67
- throw new RangeError(`${name}.grouping must not be empty`);
68
- }
69
- let sum = 0;
70
- for (let index = 0; index < ts.grouping.length; index += 1) {
71
- sum += assertPositiveInt(ts.grouping[index] ?? Number.NaN, `${name}.grouping[${index}]`);
72
- }
73
- const compound = ts.numerator % 3 === 0 && ts.numerator > 3;
74
- const pulses = compound ? ts.numerator / 3 : ts.numerator;
75
- if (sum !== pulses) {
76
- throw new RangeError(`${name}.grouping must sum to ${pulses}; received ${sum}`);
77
- }
78
- }
79
- return ts;
80
- }
81
-
82
- /** Validate the finite fields of one timeline note event. */
83
- export function assertNoteEvent(
84
- event: NoteEvent,
85
- name = 'note event',
86
- options: { allowNonPositiveDuration?: boolean } = {},
87
- ): NoteEvent {
88
- assertFiniteNumber(event.pitch, `${name}.pitch`);
89
- assertRange(event.startBeat, 0, Number.MAX_SAFE_INTEGER, `${name}.startBeat`);
90
- assertRange(
91
- event.durationBeat,
92
- options.allowNonPositiveDuration ? -Number.MAX_SAFE_INTEGER : Number.MIN_VALUE,
93
- Number.MAX_SAFE_INTEGER,
94
- `${name}.durationBeat`,
95
- );
96
- if (!options.allowNonPositiveDuration && event.durationBeat <= 0) {
97
- throw new RangeError(`${name}.durationBeat must be positive; received ${event.durationBeat}`);
98
- }
99
- if (event.velocity !== undefined) {
100
- assertRange(event.velocity, 0, 127, `${name}.velocity`);
101
- }
102
- return event;
103
- }
104
-
105
- /** Validate an event array and its allocation budget without copying it. */
106
- export function assertNoteEvents(
107
- events: NoteEvent[],
108
- name = 'note events',
109
- options: { allowNonPositiveDuration?: boolean; budget?: number } = {},
110
- ): NoteEvent[] {
111
- assertGenerationBudget(events.length, `${name} count`, options.budget);
112
- for (let index = 0; index < events.length; index += 1) {
113
- const event = events[index];
114
- if (event !== undefined) {
115
- assertNoteEvent(event, `${name}[${index}]`, options);
116
- }
117
- }
118
- return events;
119
- }
@@ -1,373 +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 {
19
- assertGenerationBudget,
20
- assertInteger,
21
- assertRange,
22
- assertTimeSignature,
23
- } from '../../core/validation/index.js';
24
- import type { Chord } from '../../theory/chord/index.js';
25
- import { nearestScaleTone } from '../../theory/scale/index.js';
26
-
27
- /**
28
- * A chord sounding over a half-open beat span `[startBeat, endBeat)`.
29
- *
30
- * @category Composition
31
- */
32
- export type BassSegment = {
33
- startBeat: number;
34
- endBeat: number;
35
- chord: Chord;
36
- };
37
-
38
- /**
39
- * The bass-line idiom to generate.
40
- *
41
- * @category Composition
42
- */
43
- export type BassStyle = 'root' | 'rootFifth' | 'pop' | 'walking' | 'arpeggio';
44
-
45
- /**
46
- * Options controlling {@link generateBassLine}.
47
- *
48
- * @category Composition
49
- */
50
- export type BassLineOptions = {
51
- /** Chord placement to follow; need not be pre-sorted. */
52
- segments: BassSegment[];
53
- /** Key/scale context, used for diatonic approach tones in `walking`. */
54
- key: KeyScale;
55
- /**
56
- * Time signature; used for metric accents.
57
- *
58
- * @defaultValue `{ numerator: 4, denominator: 4 }`
59
- */
60
- ts?: TimeSignature;
61
- /**
62
- * Bass-line idiom.
63
- *
64
- * @defaultValue `'root'`
65
- */
66
- style?: BassStyle;
67
- /**
68
- * Target register as a base MIDI octave; roots land around `octave*12+12`.
69
- *
70
- * @defaultValue 2
71
- */
72
- octave?: number;
73
- /**
74
- * Seed for the deterministic PRNG.
75
- *
76
- * @defaultValue 0
77
- */
78
- seed?: number;
79
- };
80
-
81
- const DEFAULT_TS: TimeSignature = { numerator: 4, denominator: 4 };
82
- const DEFAULT_STYLE: BassStyle = 'root';
83
- const DEFAULT_OCTAVE = 2;
84
-
85
- /** Velocity for notes on metrically strong positions. */
86
- const STRONG_VELOCITY = 100;
87
- /** Velocity for notes on weak positions. */
88
- const WEAK_VELOCITY = 80;
89
- /** Probability of a weak-beat pickup in the `pop` style. */
90
- const PICKUP_PROB = 0.35;
91
-
92
- const EPS = 1e-9;
93
-
94
- /** Reduce a value to a pitch class in [0, 11]. */
95
- function pitchClass(value: number): number {
96
- return ((Math.trunc(value) % 12) + 12) % 12;
97
- }
98
-
99
- /**
100
- * Place a pitch class as a MIDI note near an anchor, clamped to the bass band.
101
- *
102
- * The octave nearest the anchor is chosen first (so motion from the previous
103
- * note is minimal), then the result is shifted by whole octaves into the band
104
- * `[low, low + 12]` if it falls outside.
105
- */
106
- function placePc(pc: number, anchor: number, low: number): number {
107
- const high = low + 12;
108
- let midi = pc + 12 * Math.round((anchor - pc) / 12);
109
- while (midi < low) {
110
- midi += 12;
111
- }
112
- while (midi > high) {
113
- midi -= 12;
114
- }
115
- return midi;
116
- }
117
-
118
- /** Chord-tone pitch classes in stacked-thirds order, deduplicated. */
119
- function chordTonePcs(chord: Chord): number[] {
120
- const seen = new Set<number>();
121
- const pcs: number[] = [];
122
- for (const interval of chord.intervals) {
123
- const pc = pitchClass(chord.rootPc + interval);
124
- if (!seen.has(pc)) {
125
- seen.add(pc);
126
- pcs.push(pc);
127
- }
128
- }
129
- if (pcs.length === 0) {
130
- pcs.push(pitchClass(chord.rootPc));
131
- }
132
- return pcs;
133
- }
134
-
135
- /**
136
- * The chord's fifth pitch class.
137
- *
138
- * Uses the perfect fifth when present, otherwise the chord's actual altered
139
- * fifth (diminished = 6, augmented = 8 semitones above the root), so dim/aug/
140
- * m7b5 chords sound their real fifth rather than a repeated root. Falls back to
141
- * the root only when the chord has no fifth degree at all.
142
- */
143
- function fifthPcOf(chord: Chord): number {
144
- const fifths = new Set(chord.intervals.map((i) => pitchClass(i)));
145
- const chosen = fifths.has(7) ? 7 : fifths.has(6) ? 6 : fifths.has(8) ? 8 : undefined;
146
- return chosen === undefined ? pitchClass(chord.rootPc) : pitchClass(chord.rootPc + chosen);
147
- }
148
-
149
- /** The sounding bass pitch class of a chord: its slash bass, else its root. */
150
- function bassPcOf(chord: Chord): number {
151
- return pitchClass(chord.bassPc ?? chord.rootPc);
152
- }
153
-
154
- /** Integer quarter-note beat positions within `[start, end)`, at least one. */
155
- function beatPositions(start: number, end: number): number[] {
156
- const positions: number[] = [];
157
- for (let p = start; p < end - EPS; p += 1) {
158
- positions.push(p);
159
- }
160
- if (positions.length === 0) {
161
- positions.push(start);
162
- }
163
- return positions;
164
- }
165
-
166
- type RawNote = { startBeat: number; pitch: number; velocity: number };
167
-
168
- /** Working state threaded through the per-segment builders. */
169
- type BuildContext = {
170
- ts: TimeSignature;
171
- low: number;
172
- key: KeyScale;
173
- rng: ReturnType<typeof createRng>;
174
- notes: RawNote[];
175
- prevMidi: number;
176
- };
177
-
178
- /** Append a note for pitch class `pc` at `pos`, placed near the running anchor. */
179
- function emit(ctx: BuildContext, pos: number, pc: number, midiOverride?: number): number {
180
- const midi = midiOverride ?? placePc(pc, ctx.prevMidi, ctx.low);
181
- const velocity = isStrongBeat(pos, ctx.ts) ? STRONG_VELOCITY : WEAK_VELOCITY;
182
- ctx.notes.push({ startBeat: pos, pitch: midi, velocity });
183
- return midi;
184
- }
185
-
186
- /** One root note per segment. */
187
- function buildRoot(ctx: BuildContext, seg: BassSegment): void {
188
- ctx.prevMidi = emit(ctx, seg.startBeat, bassPcOf(seg.chord));
189
- }
190
-
191
- /** Root on the downbeat, fifth on the segment's midpoint. */
192
- function buildRootFifth(ctx: BuildContext, seg: BassSegment): void {
193
- ctx.prevMidi = emit(ctx, seg.startBeat, bassPcOf(seg.chord));
194
- const mid = (seg.startBeat + seg.endBeat) / 2;
195
- if (mid > seg.startBeat + EPS && mid < seg.endBeat - EPS) {
196
- ctx.prevMidi = emit(ctx, mid, fifthPcOf(seg.chord));
197
- }
198
- }
199
-
200
- /** Root on every strong beat, with occasional octave/fifth pickups on weak beats. */
201
- function buildPop(ctx: BuildContext, seg: BassSegment): void {
202
- const positions = beatPositions(seg.startBeat, seg.endBeat);
203
- const rootPc = bassPcOf(seg.chord);
204
- let emitted = false;
205
- for (const pos of positions) {
206
- if (isStrongBeat(pos, ctx.ts)) {
207
- ctx.prevMidi = emit(ctx, pos, rootPc);
208
- emitted = true;
209
- } else if (ctx.rng.prob(PICKUP_PROB)) {
210
- if (ctx.rng.prob(0.5)) {
211
- // Octave pickup: the root dropped toward the bottom of the register,
212
- // clamped so it never falls below the declared band.
213
- const base = placePc(rootPc, ctx.prevMidi, ctx.low);
214
- emit(ctx, pos, rootPc, Math.max(ctx.low, base - 12));
215
- } else {
216
- ctx.prevMidi = emit(ctx, pos, fifthPcOf(seg.chord));
217
- }
218
- }
219
- }
220
- if (!emitted) {
221
- ctx.prevMidi = emit(ctx, seg.startBeat, rootPc);
222
- }
223
- }
224
-
225
- /** Cycle root -> third -> fifth (-> seventh) across the segment's beats. */
226
- function buildArpeggio(ctx: BuildContext, seg: BassSegment): void {
227
- const tones = chordTonePcs(seg.chord);
228
- const positions = beatPositions(seg.startBeat, seg.endBeat);
229
- positions.forEach((pos, i) => {
230
- const pc = tones[i % tones.length] ?? bassPcOf(seg.chord);
231
- ctx.prevMidi = emit(ctx, pos, pc);
232
- });
233
- }
234
-
235
- /** A diatonic or chromatic neighbor of `target`, a step toward `from`. */
236
- function approachNote(ctx: BuildContext, target: number, from: number): number {
237
- const dir = from <= target ? -1 : 1;
238
- if (ctx.rng.prob(0.5)) {
239
- return target + dir; // chromatic semitone
240
- }
241
- const cand = nearestScaleTone(target + dir * 2, ctx.key);
242
- const step = Math.abs(cand - target);
243
- return step >= 1 && step <= 2 ? cand : target + dir;
244
- }
245
-
246
- /** A quarter-note line of chord tones that leads by step into each chord change. */
247
- function buildWalking(ctx: BuildContext, seg: BassSegment, next: BassSegment | undefined): void {
248
- const tones = chordTonePcs(seg.chord);
249
- const positions = beatPositions(seg.startBeat, seg.endBeat);
250
- const count = positions.length;
251
- positions.forEach((pos, i) => {
252
- if (i === 0) {
253
- ctx.prevMidi = emit(ctx, pos, bassPcOf(seg.chord));
254
- return;
255
- }
256
- if (next && count > 1 && i === count - 1) {
257
- const nextBass = placePc(bassPcOf(next.chord), ctx.prevMidi, ctx.low);
258
- const midi = approachNote(ctx, nextBass, ctx.prevMidi);
259
- ctx.prevMidi = emit(ctx, pos, pitchClass(midi), midi);
260
- return;
261
- }
262
- const pc = tones[i % tones.length] ?? bassPcOf(seg.chord);
263
- ctx.prevMidi = emit(ctx, pos, pc);
264
- });
265
- }
266
-
267
- /**
268
- * Generate a bass line following a chord placement.
269
- *
270
- * Each segment contributes notes in the chosen bass register per the selected
271
- * style; consecutive notes are kept within roughly a fifth. Every note's
272
- * duration extends to the next onset (the final note extends to the last
273
- * segment's end). Given a seed the output is fully reproducible.
274
- *
275
- * @param opts Segments, key, and generation options.
276
- * @returns Bass notes sorted by onset, non-overlapping.
277
- *
278
- * @example
279
- * ```ts
280
- * import { generateBassLine, makeChord, majorKey } from '@libraz/libcantus';
281
- * const segments = [
282
- * { startBeat: 0, endBeat: 4, chord: makeChord(0, 'maj') },
283
- * { startBeat: 4, endBeat: 8, chord: makeChord(7, 'maj') },
284
- * ];
285
- * const notes = generateBassLine({ segments, key: majorKey(0), style: 'walking' });
286
- * ```
287
- *
288
- * @category Composition
289
- */
290
- export function generateBassLine(opts: BassLineOptions): NoteEvent[] {
291
- assertGenerationBudget(opts.segments.length, 'bass segments');
292
- for (let index = 0; index < opts.segments.length; index += 1) {
293
- const segment = opts.segments[index];
294
- if (!segment) continue;
295
- assertRange(segment.startBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].startBeat`);
296
- assertRange(segment.endBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].endBeat`);
297
- if (segment.endBeat <= segment.startBeat) {
298
- throw new RangeError(`segments[${index}] must have a positive duration`);
299
- }
300
- }
301
- const segments = [...opts.segments].sort((a, b) => a.startBeat - b.startBeat);
302
- if (segments.length === 0) {
303
- return [];
304
- }
305
-
306
- const ts = opts.ts ?? DEFAULT_TS;
307
- assertTimeSignature(ts);
308
- const style = opts.style ?? DEFAULT_STYLE;
309
- const octave = opts.octave ?? DEFAULT_OCTAVE;
310
- assertInteger(octave, 'bass octave', -1, 9);
311
- const estimatedNotes = segments.reduce(
312
- (count, segment) => count + Math.max(1, Math.ceil(segment.endBeat - segment.startBeat)),
313
- 0,
314
- );
315
- assertGenerationBudget(estimatedNotes, 'bass notes');
316
- const low = octave * 12 + 12;
317
-
318
- const ctx: BuildContext = {
319
- ts,
320
- low,
321
- key: opts.key,
322
- rng: createRng(opts.seed ?? 0),
323
- notes: [],
324
- prevMidi: low,
325
- };
326
-
327
- for (let s = 0; s < segments.length; s += 1) {
328
- const seg = segments[s];
329
- if (!seg) {
330
- continue;
331
- }
332
- switch (style) {
333
- case 'rootFifth':
334
- buildRootFifth(ctx, seg);
335
- break;
336
- case 'pop':
337
- buildPop(ctx, seg);
338
- break;
339
- case 'walking':
340
- buildWalking(ctx, seg, segments[s + 1]);
341
- break;
342
- case 'arpeggio':
343
- buildArpeggio(ctx, seg);
344
- break;
345
- default:
346
- buildRoot(ctx, seg);
347
- break;
348
- }
349
- }
350
-
351
- ctx.notes.sort((a, b) => a.startBeat - b.startBeat);
352
- const lastEnd = segments.reduce((m, seg) => Math.max(m, seg.endBeat), Number.NEGATIVE_INFINITY);
353
-
354
- const out: NoteEvent[] = [];
355
- for (let i = 0; i < ctx.notes.length; i += 1) {
356
- const note = ctx.notes[i];
357
- if (!note) {
358
- continue;
359
- }
360
- const nextStart = ctx.notes[i + 1]?.startBeat ?? lastEnd;
361
- const durationBeat = nextStart - note.startBeat;
362
- if (durationBeat <= EPS) {
363
- continue;
364
- }
365
- out.push({
366
- pitch: note.pitch,
367
- startBeat: note.startBeat,
368
- durationBeat,
369
- velocity: note.velocity,
370
- });
371
- }
372
- return out;
373
- }