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