@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.
- package/README.md +25 -2
- package/dist/analyze/index.cjs +1697 -0
- package/dist/analyze/index.cjs.map +1 -0
- package/dist/analyze/index.d.cts +429 -0
- package/dist/analyze/index.d.ts +429 -0
- package/dist/analyze/index.js +51 -0
- package/dist/analyze/index.js.map +1 -0
- package/dist/chunk-5HTNMQW4.js +653 -0
- package/dist/chunk-5HTNMQW4.js.map +1 -0
- package/dist/chunk-7Y7WF64D.js +410 -0
- package/dist/chunk-7Y7WF64D.js.map +1 -0
- package/dist/chunk-A4532ZFR.js +259 -0
- package/dist/chunk-A4532ZFR.js.map +1 -0
- package/dist/chunk-BHUT2S5G.js +22 -0
- package/dist/chunk-BHUT2S5G.js.map +1 -0
- package/dist/chunk-CCBLVAPK.js +42 -0
- package/dist/chunk-CCBLVAPK.js.map +1 -0
- package/dist/chunk-COKTX2G5.js +907 -0
- package/dist/chunk-COKTX2G5.js.map +1 -0
- package/dist/chunk-DYWFNC4A.js +152 -0
- package/dist/chunk-DYWFNC4A.js.map +1 -0
- package/dist/chunk-GQKN5KDT.js +159 -0
- package/dist/chunk-GQKN5KDT.js.map +1 -0
- package/dist/chunk-LN7ZZMD2.js +530 -0
- package/dist/chunk-LN7ZZMD2.js.map +1 -0
- package/dist/chunk-MSWFZF7J.js +118 -0
- package/dist/chunk-MSWFZF7J.js.map +1 -0
- package/dist/chunk-R3S2LUBH.js +2722 -0
- package/dist/chunk-R3S2LUBH.js.map +1 -0
- package/dist/chunk-SDM3VJJ5.js +41 -0
- package/dist/chunk-SDM3VJJ5.js.map +1 -0
- package/dist/chunk-XTC3X6MS.js +816 -0
- package/dist/chunk-XTC3X6MS.js.map +1 -0
- package/dist/chunk-ZXW5YIWK.js +55 -0
- package/dist/chunk-ZXW5YIWK.js.map +1 -0
- package/dist/core/index.cjs +432 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +210 -0
- package/dist/core/index.d.ts +210 -0
- package/dist/core/index.js +73 -0
- package/dist/core/index.js.map +1 -0
- package/dist/function-2bJW1Zp_.d.ts +187 -0
- package/dist/function-r2sh-jOO.d.cts +187 -0
- package/dist/generate/index.cjs +3787 -0
- package/dist/generate/index.cjs.map +1 -0
- package/dist/generate/index.d.cts +980 -0
- package/dist/generate/index.d.ts +980 -0
- package/dist/generate/index.js +52 -0
- package/dist/generate/index.js.map +1 -0
- package/dist/index-BMPxwjwV.d.ts +151 -0
- package/dist/index-BOBXBNwP.d.cts +157 -0
- package/dist/index-BdeQ1r_p.d.cts +56 -0
- package/dist/index-BreoJRw0.d.ts +127 -0
- package/dist/index-CGMZNCx6.d.cts +140 -0
- package/dist/index-CGMZNCx6.d.ts +140 -0
- package/dist/index-CM5W4rcm.d.ts +157 -0
- package/dist/index-CP3wAfEH.d.ts +56 -0
- package/dist/index-Cl7mhOFM.d.cts +164 -0
- package/dist/index-Cl7mhOFM.d.ts +164 -0
- package/dist/index-DZ1M21do.d.cts +127 -0
- package/dist/index-D_ZTUNKp.d.cts +151 -0
- package/dist/index.cjs +5277 -4893
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -2621
- package/dist/index.d.ts +14 -2621
- package/dist/index.js +170 -6152
- package/dist/index.js.map +1 -1
- package/dist/model/index.cjs +2435 -0
- package/dist/model/index.cjs.map +1 -0
- package/dist/model/index.d.cts +630 -0
- package/dist/model/index.d.ts +630 -0
- package/dist/model/index.js +21 -0
- package/dist/model/index.js.map +1 -0
- package/dist/styled-BjXtOFNk.d.cts +282 -0
- package/dist/styled-D7BAx3S3.d.ts +282 -0
- package/dist/theory/index.cjs +1677 -0
- package/dist/theory/index.cjs.map +1 -0
- package/dist/theory/index.d.cts +535 -0
- package/dist/theory/index.d.ts +535 -0
- package/dist/theory/index.js +149 -0
- package/dist/theory/index.js.map +1 -0
- package/dist/types-MGP29r3t.d.cts +30 -0
- package/dist/types-MGP29r3t.d.ts +30 -0
- package/package.json +31 -1
- package/src/analyze/arrange/index.ts +21 -0
- package/src/analyze/arrange/internal.ts +125 -0
- package/src/analyze/arrange/tension.ts +187 -0
- package/src/analyze/arrange/tracks.ts +279 -0
- package/src/{detect → analyze/detect}/index.ts +82 -23
- package/src/analyze/functional/borrowed.ts +84 -0
- package/src/analyze/functional/cadence.ts +66 -0
- package/src/analyze/functional/function.ts +184 -0
- package/src/analyze/functional/index.ts +23 -0
- package/src/analyze/functional/internal.ts +29 -0
- package/src/analyze/functional/roman.ts +393 -0
- package/src/analyze/index.ts +49 -0
- package/src/{timeline → analyze/timeline}/index.ts +87 -24
- package/src/{analysis → analyze/voice}/index.ts +81 -12
- package/src/core/index.ts +47 -0
- package/src/{interval → core/interval}/index.ts +15 -1
- package/src/{meter → core/meter}/index.ts +108 -5
- package/src/{pitch → core/pitch}/index.ts +58 -3
- package/src/{random → core/random}/index.ts +13 -1
- package/src/{tuning → core/tuning}/index.ts +28 -1
- package/src/{bass → generate/bass}/index.ts +66 -17
- package/src/{countermelody → generate/countermelody}/index.ts +54 -24
- package/src/{drums → generate/drums}/fills.ts +31 -20
- package/src/{drums → generate/drums}/index.ts +127 -9
- package/src/{drums → generate/drums}/internal.ts +7 -1
- package/src/{drums → generate/drums}/rng.ts +2 -2
- package/src/{drums → generate/drums}/swing.ts +7 -2
- package/src/{groove → generate/groove}/index.ts +118 -22
- package/src/{harmonize → generate/harmonize}/index.ts +96 -23
- package/src/generate/index.ts +59 -0
- package/src/{motif → generate/motif}/index.ts +94 -17
- package/src/{progression → generate/progression}/index.ts +61 -27
- package/src/{reharmony → generate/reharmony}/index.ts +46 -15
- package/src/{rhythm → generate/rhythm}/index.ts +47 -13
- package/src/index.ts +130 -116
- package/src/model/chord.ts +423 -0
- package/src/model/index.ts +5 -947
- package/src/model/interval.ts +82 -0
- package/src/model/key.ts +266 -0
- package/src/model/note.ts +176 -0
- package/src/model/progression.ts +161 -0
- package/src/model/shared.ts +12 -0
- package/src/{chord → theory/chord}/index.ts +85 -4
- package/src/{chordscale → theory/chordscale}/index.ts +67 -9
- package/src/{counterpoint → theory/counterpoint}/index.ts +23 -2
- package/src/{harmony → theory/harmony}/index.ts +19 -2
- package/src/theory/index.ts +107 -0
- package/src/{safety → theory/safety}/index.ts +59 -12
- package/src/theory/scale/degrees.ts +123 -0
- package/src/theory/scale/index.ts +28 -0
- package/src/theory/scale/key.ts +56 -0
- package/src/theory/scale/masks.ts +153 -0
- package/src/{spelling → theory/spelling}/index.ts +21 -3
- package/src/{symbol → theory/symbol}/index.ts +91 -36
- package/src/theory/voicing/index.ts +5 -0
- package/src/theory/voicing/internal.ts +168 -0
- package/src/theory/voicing/leading.ts +112 -0
- package/src/theory/voicing/satb.ts +195 -0
- package/src/theory/voicing/styled.ts +176 -0
- package/src/arrange/index.ts +0 -409
- package/src/functional/index.ts +0 -602
- package/src/scale/index.ts +0 -215
- package/src/voicing/index.ts +0 -523
- /package/src/{types.ts → core/types.ts} +0 -0
- /package/src/{drums → generate/drums}/beat.ts +0 -0
- /package/src/{drums → generate/drums}/euclid.ts +0 -0
- /package/src/{drums → generate/drums}/ghost.ts +0 -0
- /package/src/{drums → generate/drums}/hihat.ts +0 -0
- /package/src/{drums → generate/drums}/hit.ts +0 -0
- /package/src/{drums → generate/drums}/kick.ts +0 -0
- /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
|
-
/**
|
|
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
|
|
244
|
-
//
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Time signature; used for metric accents.
|
|
51
|
+
*
|
|
52
|
+
* @defaultValue `{ numerator: 4, denominator: 4 }`
|
|
53
|
+
*/
|
|
38
54
|
ts?: TimeSignature;
|
|
39
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Bass-line idiom.
|
|
57
|
+
*
|
|
58
|
+
* @defaultValue `'root'`
|
|
59
|
+
*/
|
|
40
60
|
style?: BassStyle;
|
|
41
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
104
|
-
|
|
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
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from '
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Meter used for strong-beat decisions.
|
|
27
|
+
*
|
|
28
|
+
* @defaultValue 4/4
|
|
29
|
+
*/
|
|
26
30
|
ts?: TimeSignature;
|
|
27
|
-
/**
|
|
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'`
|
|
31
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
313
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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 (
|
|
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
|
}
|