@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
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import type { KeyScale } from '../../core/types.js';
|
|
1
2
|
import { scaleTonesInDegreeOrder } from '../scale/index.js';
|
|
2
|
-
import type { KeyScale } from '../types.js';
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Chord quality identifiers understood by the chord builder.
|
|
6
|
+
*
|
|
7
|
+
* @category Chords
|
|
8
|
+
*/
|
|
5
9
|
export type ChordQuality =
|
|
6
10
|
| 'maj'
|
|
7
11
|
| 'min'
|
|
@@ -34,12 +38,54 @@ export type ChordQuality =
|
|
|
34
38
|
| '13'
|
|
35
39
|
| '5';
|
|
36
40
|
|
|
37
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* A bare spelled pitch used as an enharmonic hint: a diatonic letter
|
|
43
|
+
* (0..6 = C..B) plus a chromatic alteration (-2 double-flat .. +2 double-sharp).
|
|
44
|
+
* Mirrors the letter/alter half of the pitch module's `Note` without an octave.
|
|
45
|
+
*
|
|
46
|
+
* @category Chords
|
|
47
|
+
*/
|
|
48
|
+
export type PitchSpelling = {
|
|
49
|
+
letter: number;
|
|
50
|
+
alter: number;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A chord expressed as a root pitch class plus semitone offsets.
|
|
55
|
+
*
|
|
56
|
+
* `rootSpelling`/`bassSpelling` are optional enharmonic hints recorded by
|
|
57
|
+
* parsers (e.g. `parseChordSymbol('Bb7')`) so formatters can reproduce the
|
|
58
|
+
* original spelling instead of defaulting to sharps. Consumers may ignore
|
|
59
|
+
* them; a hint is only trusted when its pitch class still matches the
|
|
60
|
+
* corresponding `rootPc`/`bassPc`.
|
|
61
|
+
*
|
|
62
|
+
* @category Chords
|
|
63
|
+
*/
|
|
38
64
|
export type Chord = {
|
|
39
65
|
rootPc: number;
|
|
40
66
|
quality: ChordQuality;
|
|
41
67
|
intervals: number[];
|
|
42
68
|
bassPc?: number;
|
|
69
|
+
rootSpelling?: PitchSpelling;
|
|
70
|
+
bassSpelling?: PitchSpelling;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A chord positioned in time: a root pitch class and quality placed at a beat,
|
|
75
|
+
* with optional harmonic-analysis hints. Serves as the neutral exchange record
|
|
76
|
+
* between progression generators and timeline analysers.
|
|
77
|
+
*
|
|
78
|
+
* @category Composition
|
|
79
|
+
*/
|
|
80
|
+
export type ChordSpan = {
|
|
81
|
+
rootPc: number;
|
|
82
|
+
quality: ChordQuality;
|
|
83
|
+
startBeat: number;
|
|
84
|
+
bassPc?: number;
|
|
85
|
+
/** 0-based scale degree of the chord root, when known. */
|
|
86
|
+
degree?: number;
|
|
87
|
+
/** True when the chord is a secondary dominant tonicizing another degree. */
|
|
88
|
+
secondaryDominant?: boolean;
|
|
43
89
|
};
|
|
44
90
|
|
|
45
91
|
/** Semitone offsets from the root for each supported chord quality. */
|
|
@@ -81,7 +127,11 @@ function pitchClass(value: number): number {
|
|
|
81
127
|
return ((Math.trunc(value) % 12) + 12) % 12;
|
|
82
128
|
}
|
|
83
129
|
|
|
84
|
-
/**
|
|
130
|
+
/**
|
|
131
|
+
* All chord qualities the builder understands, longest templates last.
|
|
132
|
+
*
|
|
133
|
+
* @category Chords
|
|
134
|
+
*/
|
|
85
135
|
export function chordQualities(): ChordQuality[] {
|
|
86
136
|
return Object.keys(QUALITY_INTERVALS) as ChordQuality[];
|
|
87
137
|
}
|
|
@@ -97,6 +147,8 @@ export function chordQualities(): ChordQuality[] {
|
|
|
97
147
|
* @param ext Chord quality to apply.
|
|
98
148
|
* @param key Key context supplying the diatonic root.
|
|
99
149
|
* @returns The constructed chord.
|
|
150
|
+
*
|
|
151
|
+
* @category Chords
|
|
100
152
|
*/
|
|
101
153
|
export function chordFromDegree(degree: number, ext: ChordQuality, key: KeyScale): Chord {
|
|
102
154
|
const tones = scaleTonesInDegreeOrder(key);
|
|
@@ -113,6 +165,15 @@ export function chordFromDegree(degree: number, ext: ChordQuality, key: KeyScale
|
|
|
113
165
|
* @param quality Chord quality supplying the interval template.
|
|
114
166
|
* @param bassPc Optional slash-chord bass pitch class.
|
|
115
167
|
* @returns The constructed chord.
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* ```ts
|
|
171
|
+
* import { makeChord } from '@libraz/libcantus';
|
|
172
|
+
* const cmaj7 = makeChord(0, 'maj7');
|
|
173
|
+
* // { rootPc: 0, quality: 'maj7', intervals: [0, 4, 7, 11] }
|
|
174
|
+
* ```
|
|
175
|
+
*
|
|
176
|
+
* @category Chords
|
|
116
177
|
*/
|
|
117
178
|
export function makeChord(rootPc: number, quality: ChordQuality, bassPc?: number): Chord {
|
|
118
179
|
const chord: Chord = {
|
|
@@ -131,6 +192,14 @@ export function makeChord(rootPc: number, quality: ChordQuality, bassPc?: number
|
|
|
131
192
|
*
|
|
132
193
|
* @param chord The chord to enumerate.
|
|
133
194
|
* @returns The chord's pitch classes, sorted ascending in [0, 11].
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* ```ts
|
|
198
|
+
* import { makeChord, chordPitchClasses } from '@libraz/libcantus';
|
|
199
|
+
* chordPitchClasses(makeChord(0, 'maj7')); // [0, 4, 7, 11]
|
|
200
|
+
* ```
|
|
201
|
+
*
|
|
202
|
+
* @category Chords
|
|
134
203
|
*/
|
|
135
204
|
export function chordPitchClasses(chord: Chord): number[] {
|
|
136
205
|
const set = new Set<number>();
|
|
@@ -156,6 +225,8 @@ function chordToneOffsets(chord: Chord): Set<number> {
|
|
|
156
225
|
* @param pitch MIDI pitch or bare pitch class.
|
|
157
226
|
* @param chord The chord providing the root reference.
|
|
158
227
|
* @returns The chord-tone role, or null if the pitch has no basic role.
|
|
228
|
+
*
|
|
229
|
+
* @category Chords
|
|
159
230
|
*/
|
|
160
231
|
export function chordToneRole(
|
|
161
232
|
pitch: number,
|
|
@@ -275,6 +346,14 @@ function stackThirds(degree: number, key: KeyScale, size: 3 | 4): Chord {
|
|
|
275
346
|
* @param degree 0-based scale degree of the chord root.
|
|
276
347
|
* @param key Key/scale context.
|
|
277
348
|
* @returns The diatonic triad.
|
|
349
|
+
*
|
|
350
|
+
* @example
|
|
351
|
+
* ```ts
|
|
352
|
+
* import { majorKey, diatonicTriad } from '@libraz/libcantus';
|
|
353
|
+
* const tonic = diatonicTriad(0, majorKey(0)); // C major triad (degree 0 of C major)
|
|
354
|
+
* ```
|
|
355
|
+
*
|
|
356
|
+
* @category Chords
|
|
278
357
|
*/
|
|
279
358
|
export function diatonicTriad(degree: number, key: KeyScale): Chord {
|
|
280
359
|
return stackThirds(degree, key, 3);
|
|
@@ -286,6 +365,8 @@ export function diatonicTriad(degree: number, key: KeyScale): Chord {
|
|
|
286
365
|
* @param degree 0-based scale degree of the chord root.
|
|
287
366
|
* @param key Key/scale context.
|
|
288
367
|
* @returns The diatonic seventh chord.
|
|
368
|
+
*
|
|
369
|
+
* @category Chords
|
|
289
370
|
*/
|
|
290
371
|
export function diatonicSeventh(degree: number, key: KeyScale): Chord {
|
|
291
372
|
return stackThirds(degree, key, 4);
|
|
@@ -29,6 +29,8 @@ function maskHasPitchClass(mask: number, scaleRootPc: number, pc: number): boole
|
|
|
29
29
|
* @param scaleMask The scale's 12-bit mode mask.
|
|
30
30
|
* @param scaleRootPc The pitch class the mask is rooted on.
|
|
31
31
|
* @returns True if the scale is a superset of the chord.
|
|
32
|
+
*
|
|
33
|
+
* @category Scales
|
|
32
34
|
*/
|
|
33
35
|
export function scaleMatchesChord(
|
|
34
36
|
chordPcs: number[],
|
|
@@ -43,37 +45,67 @@ export function scaleMatchesChord(
|
|
|
43
45
|
return true;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* A named scale rooted on a pitch class that fits over a chord.
|
|
50
|
+
*
|
|
51
|
+
* @category Scales
|
|
52
|
+
*/
|
|
47
53
|
export type ChordScaleMatch = {
|
|
48
54
|
name: string;
|
|
49
55
|
rootPc: number;
|
|
50
56
|
};
|
|
51
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Scale names whose mask duplicates another {@link NAMED_SCALES} entry
|
|
60
|
+
* (major/ionian, naturalMinor/aeolian). Chord-scale analysis conventionally
|
|
61
|
+
* speaks in mode names, so these aliases are skipped and only the modal name
|
|
62
|
+
* is reported.
|
|
63
|
+
*/
|
|
64
|
+
const ALIASED_SCALE_NAMES = new Set(['major', 'naturalMinor']);
|
|
65
|
+
|
|
66
|
+
/** Number of tones in a heptatonic (seven-note) scale. */
|
|
67
|
+
const HEPTATONIC_SIZE = 7;
|
|
68
|
+
|
|
52
69
|
/**
|
|
53
70
|
* List the scales that fit over a chord, best fit first.
|
|
54
71
|
*
|
|
55
72
|
* Only the chord root is considered as the scale root, matching the
|
|
56
73
|
* conventional chord-scale relationship. Every entry of {@link NAMED_SCALES}
|
|
57
|
-
* whose pitch-class set is a superset of the chord's is returned
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
74
|
+
* whose pitch-class set is a superset of the chord's is returned once per
|
|
75
|
+
* distinct pitch-class set (aliased masks such as major/ionian report only the
|
|
76
|
+
* modal name), ranked by fewest extra scale tones beyond the chord, then by
|
|
77
|
+
* scale size (heptatonic before larger scales), then by scale name. For bare
|
|
78
|
+
* triads and smaller chords, heptatonic scales rank before pentatonics and
|
|
79
|
+
* other sizes: a pentatonic adds no modal color over a triad, so the seven-note
|
|
80
|
+
* modes are the more useful answer. The chromatic scale is only returned as a
|
|
81
|
+
* fallback when no other scale contains the chord.
|
|
61
82
|
*
|
|
62
83
|
* @param chord The chord to fit scales over.
|
|
63
84
|
* @returns The matching scales rooted on the chord root, best fit first.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* import { makeChord, chordScales } from '@libraz/libcantus';
|
|
89
|
+
* const scales = chordScales(makeChord(0, 'maj7'));
|
|
90
|
+
* // scales for Cmaj7, best fit first; each is { name, rootPc: 0 }
|
|
91
|
+
* ```
|
|
92
|
+
*
|
|
93
|
+
* @category Scales
|
|
64
94
|
*/
|
|
65
95
|
export function chordScales(chord: Chord): ChordScaleMatch[] {
|
|
66
96
|
const chordPcs = chordPitchClasses(chord);
|
|
67
97
|
const rootPc = pitchClass(chord.rootPc);
|
|
68
98
|
const ranked: { name: string; extra: number; size: number }[] = [];
|
|
99
|
+
const seenMasks = new Set<number>();
|
|
69
100
|
for (const name of Object.keys(NAMED_SCALES)) {
|
|
70
|
-
if (name === 'chromatic') {
|
|
101
|
+
if (name === 'chromatic' || ALIASED_SCALE_NAMES.has(name)) {
|
|
71
102
|
continue;
|
|
72
103
|
}
|
|
73
104
|
const mask = NAMED_SCALES[name];
|
|
74
|
-
if (mask === undefined) {
|
|
105
|
+
if (mask === undefined || seenMasks.has(mask)) {
|
|
75
106
|
continue;
|
|
76
107
|
}
|
|
108
|
+
seenMasks.add(mask);
|
|
77
109
|
if (scaleMatchesChord(chordPcs, mask, rootPc)) {
|
|
78
110
|
const size = popcount12(mask);
|
|
79
111
|
ranked.push({ name, extra: size - chordPcs.length, size });
|
|
@@ -87,7 +119,11 @@ export function chordScales(chord: Chord): ChordScaleMatch[] {
|
|
|
87
119
|
}
|
|
88
120
|
return [];
|
|
89
121
|
}
|
|
122
|
+
const preferHeptatonic = chordPcs.length <= 3;
|
|
90
123
|
ranked.sort((a, b) => {
|
|
124
|
+
if (preferHeptatonic && (a.size === HEPTATONIC_SIZE) !== (b.size === HEPTATONIC_SIZE)) {
|
|
125
|
+
return a.size === HEPTATONIC_SIZE ? -1 : 1;
|
|
126
|
+
}
|
|
91
127
|
if (a.extra !== b.extra) {
|
|
92
128
|
return a.extra - b.extra;
|
|
93
129
|
}
|
|
@@ -109,6 +145,8 @@ export function chordScales(chord: Chord): ChordScaleMatch[] {
|
|
|
109
145
|
* @param chord The chord providing the chord tones.
|
|
110
146
|
* @param scaleName A key of {@link NAMED_SCALES}.
|
|
111
147
|
* @returns The avoid-note pitch classes, sorted ascending in [0, 11].
|
|
148
|
+
*
|
|
149
|
+
* @category Scales
|
|
112
150
|
*/
|
|
113
151
|
export function avoidNotes(chord: Chord, scaleName: string): number[] {
|
|
114
152
|
const mask = NAMED_SCALES[scaleName];
|
|
@@ -144,6 +182,14 @@ export function avoidNotes(chord: Chord, scaleName: string): number[] {
|
|
|
144
182
|
* @param chord The chord providing the chord tones.
|
|
145
183
|
* @param scaleName A key of {@link NAMED_SCALES}.
|
|
146
184
|
* @returns The available-tension pitch classes, sorted ascending in [0, 11].
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```ts
|
|
188
|
+
* import { makeChord, availableTensions } from '@libraz/libcantus';
|
|
189
|
+
* availableTensions(makeChord(0, 'maj7'), 'ionian'); // [2, 9] — color tones over Cmaj7
|
|
190
|
+
* ```
|
|
191
|
+
*
|
|
192
|
+
* @category Scales
|
|
147
193
|
*/
|
|
148
194
|
export function availableTensions(chord: Chord, scaleName: string): number[] {
|
|
149
195
|
const mask = NAMED_SCALES[scaleName];
|
|
@@ -167,7 +213,11 @@ export function availableTensions(chord: Chord, scaleName: string): number[] {
|
|
|
167
213
|
return tensions.sort((a, b) => a - b);
|
|
168
214
|
}
|
|
169
215
|
|
|
170
|
-
/**
|
|
216
|
+
/**
|
|
217
|
+
* A scale fit over a chord together with its avoid notes and tensions.
|
|
218
|
+
*
|
|
219
|
+
* @category Scales
|
|
220
|
+
*/
|
|
171
221
|
export type ChordScaleReportEntry = {
|
|
172
222
|
name: string;
|
|
173
223
|
rootPc: number;
|
|
@@ -184,6 +234,8 @@ export type ChordScaleReportEntry = {
|
|
|
184
234
|
* @param chord The chord to analyze.
|
|
185
235
|
* @param limit Optional maximum number of scales to report; all by default.
|
|
186
236
|
* @returns One entry per reported scale, best fit first.
|
|
237
|
+
*
|
|
238
|
+
* @category Scales
|
|
187
239
|
*/
|
|
188
240
|
export function chordScaleReport(chord: Chord, limit?: number): ChordScaleReportEntry[] {
|
|
189
241
|
const matches = chordScales(chord);
|
|
@@ -196,7 +248,11 @@ export function chordScaleReport(chord: Chord, limit?: number): ChordScaleReport
|
|
|
196
248
|
}));
|
|
197
249
|
}
|
|
198
250
|
|
|
199
|
-
/**
|
|
251
|
+
/**
|
|
252
|
+
* A chord paired with the scale chosen for it by {@link scalesForChanges}.
|
|
253
|
+
*
|
|
254
|
+
* @category Scales
|
|
255
|
+
*/
|
|
200
256
|
export type ScaleChoice = {
|
|
201
257
|
chord: Chord;
|
|
202
258
|
scale: ChordScaleMatch;
|
|
@@ -251,6 +307,8 @@ function symmetricDifferenceSize(a: Set<number>, b: Set<number>): number {
|
|
|
251
307
|
*
|
|
252
308
|
* @param chords The chord sequence to choose scales for.
|
|
253
309
|
* @returns One scale choice per chord, in input order.
|
|
310
|
+
*
|
|
311
|
+
* @category Scales
|
|
254
312
|
*/
|
|
255
313
|
export function scalesForChanges(chords: Chord[]): ScaleChoice[] {
|
|
256
314
|
if (chords.length === 0) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { isConsonantInterval } from '
|
|
2
|
-
import type { KeyScale } from '
|
|
1
|
+
import { isConsonantInterval } from '../../core/interval/index.js';
|
|
2
|
+
import type { KeyScale } from '../../core/types.js';
|
|
3
3
|
|
|
4
4
|
/** Reduce a MIDI pitch (or pitch class) to a pitch class in [0, 11]. */
|
|
5
5
|
function pitchClass(pitch: number): number {
|
|
@@ -12,6 +12,7 @@ function pitchClass(pitch: number): number {
|
|
|
12
12
|
* @param upperPitch Pitch of the nominally higher voice.
|
|
13
13
|
* @param lowerPitch Pitch of the nominally lower voice.
|
|
14
14
|
* @returns True when the upper voice sits below the lower voice.
|
|
15
|
+
* @category Voicing & Counterpoint
|
|
15
16
|
*/
|
|
16
17
|
export function createsVoiceCrossing(upperPitch: number, lowerPitch: number): boolean {
|
|
17
18
|
return upperPitch < lowerPitch;
|
|
@@ -24,6 +25,7 @@ export function createsVoiceCrossing(upperPitch: number, lowerPitch: number): bo
|
|
|
24
25
|
* @param b Second pitch.
|
|
25
26
|
* @param twoVoice When true, the perfect fourth counts as dissonant.
|
|
26
27
|
* @returns True when the vertical interval is dissonant.
|
|
28
|
+
* @category Voicing & Counterpoint
|
|
27
29
|
*/
|
|
28
30
|
export function createsVerticalDissonance(a: number, b: number, twoVoice: boolean): boolean {
|
|
29
31
|
return !isConsonantInterval(a - b, twoVoice);
|
|
@@ -39,6 +41,7 @@ export function createsVerticalDissonance(a: number, b: number, twoVoice: boolea
|
|
|
39
41
|
* @param prev Starting pitch.
|
|
40
42
|
* @param cur Ending pitch.
|
|
41
43
|
* @returns True when the leap is forbidden in strict counterpoint.
|
|
44
|
+
* @category Voicing & Counterpoint
|
|
42
45
|
*/
|
|
43
46
|
export function isForbiddenMelodicLeap(prev: number, cur: number): boolean {
|
|
44
47
|
const semis = Math.abs(cur - prev);
|
|
@@ -78,6 +81,8 @@ function bothVoicesMove(aMove: number, bMove: number): boolean {
|
|
|
78
81
|
* A fifth expanding to a twelfth counts (same perfect class); a fifth moving to
|
|
79
82
|
* an octave does not (different perfect kinds — the direct/hidden case owned by
|
|
80
83
|
* {@link createsHiddenParallelPerfect}).
|
|
84
|
+
*
|
|
85
|
+
* @category Voicing & Counterpoint
|
|
81
86
|
*/
|
|
82
87
|
export function createsParallelPerfect(
|
|
83
88
|
aPrev: number,
|
|
@@ -96,6 +101,15 @@ export function createsParallelPerfect(
|
|
|
96
101
|
/**
|
|
97
102
|
* Whether two voices move in consecutive parallel octaves (or unisons) by
|
|
98
103
|
* similar motion.
|
|
104
|
+
*
|
|
105
|
+
* This is a strict subset of {@link createsParallelPerfect}: a similar-motion
|
|
106
|
+
* octave-to-octave is the perfect-class-zero case that predicate already flags
|
|
107
|
+
* (and it additionally catches the contrary-motion anti-parallel). Callers that
|
|
108
|
+
* tally parallel violations should therefore use {@link createsParallelPerfect}
|
|
109
|
+
* alone to avoid double counting; this predicate remains for callers wanting a
|
|
110
|
+
* dedicated similar-motion octave test.
|
|
111
|
+
*
|
|
112
|
+
* @category Voicing & Counterpoint
|
|
99
113
|
*/
|
|
100
114
|
export function createsParallelOctave(
|
|
101
115
|
aPrev: number,
|
|
@@ -112,6 +126,8 @@ export function createsParallelOctave(
|
|
|
112
126
|
/**
|
|
113
127
|
* Whether two voices move in consecutive parallel unisons — both landing on the
|
|
114
128
|
* same pitch, having shared a pitch on the previous move.
|
|
129
|
+
*
|
|
130
|
+
* @category Voicing & Counterpoint
|
|
115
131
|
*/
|
|
116
132
|
export function createsParallelUnison(
|
|
117
133
|
aPrev: number,
|
|
@@ -132,6 +148,8 @@ export function createsParallelUnison(
|
|
|
132
148
|
* The traditional step exception is applied: the approach is allowed when the
|
|
133
149
|
* upper of the two voices moves by step, so only leaps into the perfect
|
|
134
150
|
* interval are flagged.
|
|
151
|
+
*
|
|
152
|
+
* @category Voicing & Counterpoint
|
|
135
153
|
*/
|
|
136
154
|
export function createsHiddenParallelPerfect(
|
|
137
155
|
aPrev: number,
|
|
@@ -168,6 +186,7 @@ export function createsHiddenParallelPerfect(
|
|
|
168
186
|
* @param upperCur Current pitch of the upper voice.
|
|
169
187
|
* @param lowerPrev Previous pitch of the lower voice.
|
|
170
188
|
* @param lowerCur Current pitch of the lower voice.
|
|
189
|
+
* @category Voicing & Counterpoint
|
|
171
190
|
*/
|
|
172
191
|
export function createsVoiceOverlap(
|
|
173
192
|
upperPrev: number,
|
|
@@ -186,6 +205,7 @@ export function createsVoiceOverlap(
|
|
|
186
205
|
* @param upperPitch Pitch of the higher voice.
|
|
187
206
|
* @param lowerPitch Pitch of the lower voice.
|
|
188
207
|
* @param maxSemitones Maximum allowed spacing in semitones (default an octave).
|
|
208
|
+
* @category Voicing & Counterpoint
|
|
189
209
|
*/
|
|
190
210
|
export function exceedsSpacing(upperPitch: number, lowerPitch: number, maxSemitones = 12): boolean {
|
|
191
211
|
return Math.abs(upperPitch - lowerPitch) > maxSemitones;
|
|
@@ -198,6 +218,7 @@ export function exceedsSpacing(upperPitch: number, lowerPitch: number, maxSemito
|
|
|
198
218
|
* @param cur The following pitch.
|
|
199
219
|
* @param key Key context supplying the tonic.
|
|
200
220
|
* @returns True when `prev` is the leading tone and `cur` is the tonic a step above.
|
|
221
|
+
* @category Voicing & Counterpoint
|
|
201
222
|
*/
|
|
202
223
|
export function isLeadingToneResolution(prev: number, cur: number, key: KeyScale): boolean {
|
|
203
224
|
const tonic = pitchClass(key.rootPc);
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { Chord } from '../chord/index.js';
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Harmonic role a pitch plays within a chord.
|
|
5
|
+
*
|
|
6
|
+
* @category Functional Harmony
|
|
7
|
+
*/
|
|
4
8
|
export type HarmonyRole = 'root' | 'third' | 'fifth' | 'sixth' | 'seventh' | 'tension' | 'doubling';
|
|
5
9
|
|
|
6
10
|
/**
|
|
@@ -9,10 +13,16 @@ export type HarmonyRole = 'root' | 'third' | 'fifth' | 'sixth' | 'seventh' | 'te
|
|
|
9
13
|
* - `identity`: moving it produces a different chord (the root).
|
|
10
14
|
* - `quality`: moving it flips the chord quality (the third).
|
|
11
15
|
* - `voicing`: it can move freely without changing chord identity or quality.
|
|
16
|
+
*
|
|
17
|
+
* @category Functional Harmony
|
|
12
18
|
*/
|
|
13
19
|
export type LockLevel = 'identity' | 'quality' | 'voicing';
|
|
14
20
|
|
|
15
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* A pitch's role, lock level, and owning chord.
|
|
23
|
+
*
|
|
24
|
+
* @category Functional Harmony
|
|
25
|
+
*/
|
|
16
26
|
export type VoicedRole = {
|
|
17
27
|
role: HarmonyRole;
|
|
18
28
|
lock: LockLevel;
|
|
@@ -36,6 +46,13 @@ export type VoicedRole = {
|
|
|
36
46
|
* @param chord The chord providing the root reference.
|
|
37
47
|
* @param chordId Identifier stored on the result (defaults to 0).
|
|
38
48
|
* @returns The pitch's role, lock level, and owning chord id.
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* import { roleOf, makeChord } from '@libraz/libcantus';
|
|
52
|
+
* roleOf(64, makeChord(0, 'maj')); // E over C major
|
|
53
|
+
* // { role: 'third', lock: 'quality', belongsToChordId: 0 }
|
|
54
|
+
* ```
|
|
55
|
+
* @category Functional Harmony
|
|
39
56
|
*/
|
|
40
57
|
export function roleOf(pitch: number, chord: Chord, chordId = 0): VoicedRole {
|
|
41
58
|
const interval = (((Math.trunc(pitch) - chord.rootPc) % 12) + 12) % 12;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public API of the theory layer. Re-exports every theory-layer module;
|
|
3
|
+
* also available from the package root.
|
|
4
|
+
*/
|
|
5
|
+
export type { Chord as ChordData, ChordQuality } from './chord/index.js';
|
|
6
|
+
export {
|
|
7
|
+
chordFromDegree,
|
|
8
|
+
chordPitchClasses,
|
|
9
|
+
chordQualities,
|
|
10
|
+
chordToneRole,
|
|
11
|
+
diatonicSeventh,
|
|
12
|
+
diatonicTriad,
|
|
13
|
+
makeChord,
|
|
14
|
+
} from './chord/index.js';
|
|
15
|
+
export type {
|
|
16
|
+
ChordScaleMatch,
|
|
17
|
+
ChordScaleReportEntry,
|
|
18
|
+
ScaleChoice,
|
|
19
|
+
} from './chordscale/index.js';
|
|
20
|
+
export {
|
|
21
|
+
availableTensions,
|
|
22
|
+
avoidNotes,
|
|
23
|
+
chordScaleReport,
|
|
24
|
+
chordScales,
|
|
25
|
+
scaleMatchesChord,
|
|
26
|
+
scalesForChanges,
|
|
27
|
+
} from './chordscale/index.js';
|
|
28
|
+
export {
|
|
29
|
+
createsHiddenParallelPerfect,
|
|
30
|
+
createsParallelOctave,
|
|
31
|
+
createsParallelPerfect,
|
|
32
|
+
createsParallelUnison,
|
|
33
|
+
createsVerticalDissonance,
|
|
34
|
+
createsVoiceCrossing,
|
|
35
|
+
createsVoiceOverlap,
|
|
36
|
+
exceedsSpacing,
|
|
37
|
+
isForbiddenMelodicLeap,
|
|
38
|
+
isLeadingToneResolution,
|
|
39
|
+
} from './counterpoint/index.js';
|
|
40
|
+
export type { HarmonyRole, LockLevel, VoicedRole } from './harmony/index.js';
|
|
41
|
+
export { roleOf } from './harmony/index.js';
|
|
42
|
+
export type {
|
|
43
|
+
SafetyProfile,
|
|
44
|
+
SafetyQuery,
|
|
45
|
+
SafetyResult,
|
|
46
|
+
VoiceSnapshot,
|
|
47
|
+
} from './safety/index.js';
|
|
48
|
+
export {
|
|
49
|
+
enumerateSafePitches,
|
|
50
|
+
evaluateSafety,
|
|
51
|
+
NoteSafety,
|
|
52
|
+
ReasonFlag,
|
|
53
|
+
} from './safety/index.js';
|
|
54
|
+
export {
|
|
55
|
+
BLUES_MASK,
|
|
56
|
+
CHROMATIC_MASK,
|
|
57
|
+
DORIAN_MASK,
|
|
58
|
+
diatonicPitchClasses,
|
|
59
|
+
HARMONIC_MINOR_MASK,
|
|
60
|
+
isScaleTone,
|
|
61
|
+
LOCRIAN_MASK,
|
|
62
|
+
LYDIAN_MASK,
|
|
63
|
+
MAJOR_MASK,
|
|
64
|
+
MAJOR_PENTATONIC_MASK,
|
|
65
|
+
MELODIC_MINOR_MASK,
|
|
66
|
+
MINOR_PENTATONIC_MASK,
|
|
67
|
+
MIXOLYDIAN_MASK,
|
|
68
|
+
majorKey,
|
|
69
|
+
maskFromOffsets,
|
|
70
|
+
minorKey,
|
|
71
|
+
NAMED_SCALES,
|
|
72
|
+
NATURAL_MINOR_MASK,
|
|
73
|
+
nearestScaleTone,
|
|
74
|
+
OCTATONIC_HALF_WHOLE_MASK,
|
|
75
|
+
OCTATONIC_WHOLE_HALF_MASK,
|
|
76
|
+
PHRYGIAN_MASK,
|
|
77
|
+
pitchToScaleDegree,
|
|
78
|
+
scaleByName,
|
|
79
|
+
scaleTonesInDegreeOrder,
|
|
80
|
+
WHOLE_TONE_MASK,
|
|
81
|
+
} from './scale/index.js';
|
|
82
|
+
export {
|
|
83
|
+
noteNames,
|
|
84
|
+
spellChord,
|
|
85
|
+
spellPitchClass,
|
|
86
|
+
spellPitchClasses,
|
|
87
|
+
spellScale,
|
|
88
|
+
} from './spelling/index.js';
|
|
89
|
+
export {
|
|
90
|
+
formatChordSymbol,
|
|
91
|
+
parseChordSymbol,
|
|
92
|
+
transposeChordSymbol,
|
|
93
|
+
} from './symbol/index.js';
|
|
94
|
+
export type {
|
|
95
|
+
StyledVoicingOptions,
|
|
96
|
+
VoiceRange,
|
|
97
|
+
VoicingOptions,
|
|
98
|
+
VoicingStyle,
|
|
99
|
+
} from './voicing/index.js';
|
|
100
|
+
export {
|
|
101
|
+
nextVoicing,
|
|
102
|
+
SATB_RANGES,
|
|
103
|
+
voiceChord,
|
|
104
|
+
voiceChordStyled,
|
|
105
|
+
voiceLeadingCost,
|
|
106
|
+
voiceProgression,
|
|
107
|
+
} from './voicing/index.js';
|
|
@@ -1,26 +1,37 @@
|
|
|
1
|
+
import type { KeyScale } from '../../core/types.js';
|
|
1
2
|
import type { Chord } from '../chord/index.js';
|
|
2
3
|
import { chordPitchClasses } from '../chord/index.js';
|
|
3
4
|
import {
|
|
4
5
|
createsHiddenParallelPerfect,
|
|
5
|
-
createsParallelOctave,
|
|
6
6
|
createsParallelPerfect,
|
|
7
7
|
createsVerticalDissonance,
|
|
8
8
|
isForbiddenMelodicLeap,
|
|
9
9
|
} from '../counterpoint/index.js';
|
|
10
10
|
import { isScaleTone } from '../scale/index.js';
|
|
11
|
-
import type { KeyScale } from '../types.js';
|
|
12
11
|
|
|
13
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Severity policy: `strict` counterpoint vs. lenient `pop` voice-leading.
|
|
14
|
+
*
|
|
15
|
+
* @category Arrangement & Analysis
|
|
16
|
+
*/
|
|
14
17
|
export type SafetyProfile = 'strict' | 'pop';
|
|
15
18
|
|
|
16
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Overall placeability verdict for a candidate pitch.
|
|
21
|
+
*
|
|
22
|
+
* @category Arrangement & Analysis
|
|
23
|
+
*/
|
|
17
24
|
export enum NoteSafety {
|
|
18
25
|
Safe = 0,
|
|
19
26
|
Warning = 1,
|
|
20
27
|
Dissonant = 2,
|
|
21
28
|
}
|
|
22
29
|
|
|
23
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Bit flags describing why a pitch received its verdict.
|
|
32
|
+
*
|
|
33
|
+
* @category Arrangement & Analysis
|
|
34
|
+
*/
|
|
24
35
|
export enum ReasonFlag {
|
|
25
36
|
ChordTone = 1 << 0,
|
|
26
37
|
Tension = 1 << 1,
|
|
@@ -40,13 +51,21 @@ export enum ReasonFlag {
|
|
|
40
51
|
NeedsResolution = 1 << 15,
|
|
41
52
|
}
|
|
42
53
|
|
|
43
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* A neighbouring voice's current and previous pitch.
|
|
56
|
+
*
|
|
57
|
+
* @category Arrangement & Analysis
|
|
58
|
+
*/
|
|
44
59
|
export type VoiceSnapshot = {
|
|
45
60
|
pitch: number;
|
|
46
61
|
prevPitch?: number;
|
|
47
62
|
};
|
|
48
63
|
|
|
49
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Everything needed to evaluate one candidate pitch.
|
|
66
|
+
*
|
|
67
|
+
* @category Arrangement & Analysis
|
|
68
|
+
*/
|
|
50
69
|
export type SafetyQuery = {
|
|
51
70
|
profile: SafetyProfile;
|
|
52
71
|
candidatePitch: number;
|
|
@@ -59,7 +78,11 @@ export type SafetyQuery = {
|
|
|
59
78
|
vocalHigh?: number;
|
|
60
79
|
};
|
|
61
80
|
|
|
62
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* The verdict, reason flags, and optional resolution guidance.
|
|
83
|
+
*
|
|
84
|
+
* @category Arrangement & Analysis
|
|
85
|
+
*/
|
|
63
86
|
export type SafetyResult = {
|
|
64
87
|
safety: NoteSafety;
|
|
65
88
|
reasons: number;
|
|
@@ -115,6 +138,20 @@ function stepResolution(pitch: number, chord: Chord): number | undefined {
|
|
|
115
138
|
*
|
|
116
139
|
* @param q The candidate and its harmonic/voice-leading context.
|
|
117
140
|
* @returns The verdict, reason bitmask, and optional resolution guidance.
|
|
141
|
+
* @example
|
|
142
|
+
* ```ts
|
|
143
|
+
* import { evaluateSafety, makeChord, majorKey, NoteSafety } from '@libraz/libcantus';
|
|
144
|
+
* const result = evaluateSafety({
|
|
145
|
+
* profile: 'pop',
|
|
146
|
+
* candidatePitch: 60, // C over a C major chord
|
|
147
|
+
* chord: makeChord(0, 'maj'),
|
|
148
|
+
* key: majorKey(0),
|
|
149
|
+
* otherVoices: [],
|
|
150
|
+
* strongBeat: true,
|
|
151
|
+
* });
|
|
152
|
+
* result.safety === NoteSafety.Safe; // true — C is a chord tone
|
|
153
|
+
* ```
|
|
154
|
+
* @category Arrangement & Analysis
|
|
118
155
|
*/
|
|
119
156
|
export function evaluateSafety(q: SafetyQuery): SafetyResult {
|
|
120
157
|
return evaluateInternal(q, true);
|
|
@@ -242,10 +279,9 @@ function evaluateInternal(q: SafetyQuery, collectSuggestions: boolean): SafetyRe
|
|
|
242
279
|
if (ov.prevPitch === undefined) {
|
|
243
280
|
continue;
|
|
244
281
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
) {
|
|
282
|
+
// createsParallelPerfect covers every perfect class (unison/octave and
|
|
283
|
+
// fifth), so no separate octave check is needed.
|
|
284
|
+
if (createsParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {
|
|
249
285
|
reasons |= ReasonFlag.ParallelPerfect;
|
|
250
286
|
raise(parallelSeverity);
|
|
251
287
|
}
|
|
@@ -346,6 +382,17 @@ function describe(reasons: number, q: SafetyQuery): string {
|
|
|
346
382
|
* @param pitchLow Lowest MIDI pitch to consider (inclusive).
|
|
347
383
|
* @param pitchHigh Highest MIDI pitch to consider (inclusive).
|
|
348
384
|
* @returns Placeable pitches (non-dissonant), chord tones before others, each group descending.
|
|
385
|
+
* @example
|
|
386
|
+
* ```ts
|
|
387
|
+
* import { enumerateSafePitches, makeChord, majorKey } from '@libraz/libcantus';
|
|
388
|
+
* const pitches = enumerateSafePitches(
|
|
389
|
+
* { profile: 'pop', chord: makeChord(0, 'maj'), key: majorKey(0), otherVoices: [], strongBeat: true },
|
|
390
|
+
* 60,
|
|
391
|
+
* 72,
|
|
392
|
+
* );
|
|
393
|
+
* pitches; // placeable pitches in [60, 72], chord tones first, each group descending
|
|
394
|
+
* ```
|
|
395
|
+
* @category Arrangement & Analysis
|
|
349
396
|
*/
|
|
350
397
|
export function enumerateSafePitches(
|
|
351
398
|
q: Omit<SafetyQuery, 'candidatePitch'>,
|