@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,123 @@
|
|
|
1
|
+
import type { KeyScale } from '../../core/types.js';
|
|
2
|
+
|
|
3
|
+
/** Reduce a pitch (or pitch class) to a pitch class in [0, 11]. */
|
|
4
|
+
function pitchClass(pitch: number): number {
|
|
5
|
+
return ((Math.trunc(pitch) % 12) + 12) % 12;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Scale offset of a pitch relative to the key root, in [0, 11]. */
|
|
9
|
+
function scaleOffset(pitch: number, key: KeyScale): number {
|
|
10
|
+
return (pitchClass(pitch) - pitchClass(key.rootPc) + 12) % 12;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Test whether a pitch belongs to the scale.
|
|
15
|
+
*
|
|
16
|
+
* @param pitch MIDI pitch or bare pitch class.
|
|
17
|
+
* @param key The key/scale to test against.
|
|
18
|
+
* @returns True if the pitch class is a member of the scale.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* import { majorKey, isScaleTone } from '@libraz/libcantus';
|
|
23
|
+
* const key = majorKey(0); // C major
|
|
24
|
+
* isScaleTone(64, key); // true — E is in C major
|
|
25
|
+
* isScaleTone(61, key); // false — C# is not
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @category Scales
|
|
29
|
+
*/
|
|
30
|
+
export function isScaleTone(pitch: number, key: KeyScale): boolean {
|
|
31
|
+
return ((key.modeMask12 >> scaleOffset(pitch, key)) & 1) === 1;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Find the nearest MIDI pitch whose pitch class is in the scale.
|
|
36
|
+
*
|
|
37
|
+
* The search expands symmetrically outward from `pitch`. On a tie (equal
|
|
38
|
+
* distance above and below) the lower pitch is returned.
|
|
39
|
+
*
|
|
40
|
+
* @param pitch MIDI pitch to snap.
|
|
41
|
+
* @param key The key/scale to snap to.
|
|
42
|
+
* @returns The nearest in-scale MIDI pitch.
|
|
43
|
+
*
|
|
44
|
+
* @category Scales
|
|
45
|
+
*/
|
|
46
|
+
export function nearestScaleTone(pitch: number, key: KeyScale): number {
|
|
47
|
+
const base = Math.round(pitch);
|
|
48
|
+
for (let distance = 0; distance < 12; distance += 1) {
|
|
49
|
+
const lower = base - distance;
|
|
50
|
+
if (isScaleTone(lower, key)) {
|
|
51
|
+
return lower;
|
|
52
|
+
}
|
|
53
|
+
const higher = base + distance;
|
|
54
|
+
if (isScaleTone(higher, key)) {
|
|
55
|
+
return higher;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return base;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get the 0-based scale degree of a pitch.
|
|
63
|
+
*
|
|
64
|
+
* @param pitch MIDI pitch or bare pitch class.
|
|
65
|
+
* @param key The key/scale to measure against.
|
|
66
|
+
* @returns The 0-based degree, or -1 if the pitch is not in the scale.
|
|
67
|
+
*
|
|
68
|
+
* @category Scales
|
|
69
|
+
*/
|
|
70
|
+
export function pitchToScaleDegree(pitch: number, key: KeyScale): number {
|
|
71
|
+
const offset = scaleOffset(pitch, key);
|
|
72
|
+
if (((key.modeMask12 >> offset) & 1) === 0) {
|
|
73
|
+
return -1;
|
|
74
|
+
}
|
|
75
|
+
let degree = 0;
|
|
76
|
+
for (let n = 0; n < offset; n += 1) {
|
|
77
|
+
if (((key.modeMask12 >> n) & 1) === 1) {
|
|
78
|
+
degree += 1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return degree;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* List the pitch classes of a key's scale.
|
|
86
|
+
*
|
|
87
|
+
* @param key The key/scale to enumerate.
|
|
88
|
+
* @returns The member pitch classes, sorted ascending in [0, 11].
|
|
89
|
+
*
|
|
90
|
+
* @category Scales
|
|
91
|
+
*/
|
|
92
|
+
export function diatonicPitchClasses(key: KeyScale): number[] {
|
|
93
|
+
const root = pitchClass(key.rootPc);
|
|
94
|
+
const pcs: number[] = [];
|
|
95
|
+
for (let n = 0; n < 12; n += 1) {
|
|
96
|
+
if (((key.modeMask12 >> n) & 1) === 1) {
|
|
97
|
+
pcs.push((root + n) % 12);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return pcs.sort((a, b) => a - b);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* List the pitch classes of a key's scale in ascending scale-degree order.
|
|
105
|
+
*
|
|
106
|
+
* Degree 0 is the root; degrees follow the mask bits in offset order rather
|
|
107
|
+
* than sorted pitch-class order.
|
|
108
|
+
*
|
|
109
|
+
* @param key The key/scale to enumerate.
|
|
110
|
+
* @returns The member pitch classes ordered by scale degree.
|
|
111
|
+
*
|
|
112
|
+
* @category Scales
|
|
113
|
+
*/
|
|
114
|
+
export function scaleTonesInDegreeOrder(key: KeyScale): number[] {
|
|
115
|
+
const root = pitchClass(key.rootPc);
|
|
116
|
+
const pcs: number[] = [];
|
|
117
|
+
for (let n = 0; n < 12; n += 1) {
|
|
118
|
+
if (((key.modeMask12 >> n) & 1) === 1) {
|
|
119
|
+
pcs.push((root + n) % 12);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return pcs;
|
|
123
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export {
|
|
2
|
+
diatonicPitchClasses,
|
|
3
|
+
isScaleTone,
|
|
4
|
+
nearestScaleTone,
|
|
5
|
+
pitchToScaleDegree,
|
|
6
|
+
scaleTonesInDegreeOrder,
|
|
7
|
+
} from './degrees.js';
|
|
8
|
+
export { majorKey, minorKey, scaleByName } from './key.js';
|
|
9
|
+
export {
|
|
10
|
+
BLUES_MASK,
|
|
11
|
+
CHROMATIC_MASK,
|
|
12
|
+
DORIAN_MASK,
|
|
13
|
+
HARMONIC_MINOR_MASK,
|
|
14
|
+
LOCRIAN_MASK,
|
|
15
|
+
LYDIAN_MASK,
|
|
16
|
+
MAJOR_MASK,
|
|
17
|
+
MAJOR_PENTATONIC_MASK,
|
|
18
|
+
MELODIC_MINOR_MASK,
|
|
19
|
+
MINOR_PENTATONIC_MASK,
|
|
20
|
+
MIXOLYDIAN_MASK,
|
|
21
|
+
maskFromOffsets,
|
|
22
|
+
NAMED_SCALES,
|
|
23
|
+
NATURAL_MINOR_MASK,
|
|
24
|
+
OCTATONIC_HALF_WHOLE_MASK,
|
|
25
|
+
OCTATONIC_WHOLE_HALF_MASK,
|
|
26
|
+
PHRYGIAN_MASK,
|
|
27
|
+
WHOLE_TONE_MASK,
|
|
28
|
+
} from './masks.js';
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { KeyScale } from '../../core/types.js';
|
|
2
|
+
import { MAJOR_MASK, NAMED_SCALES, NATURAL_MINOR_MASK } from './masks.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build a `KeyScale` for a major key on the given root pitch class.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { majorKey } from '@libraz/libcantus';
|
|
10
|
+
* const cMajor = majorKey(0); // C major: { rootPc: 0, modeMask12: MAJOR_MASK }
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* @category Scales
|
|
14
|
+
*/
|
|
15
|
+
export function majorKey(rootPc: number): KeyScale {
|
|
16
|
+
return { rootPc: ((rootPc % 12) + 12) % 12, modeMask12: MAJOR_MASK };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build a `KeyScale` for a natural-minor key on the given root pitch class.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* import { minorKey } from '@libraz/libcantus';
|
|
25
|
+
* const aMinor = minorKey(9); // A natural minor: { rootPc: 9, modeMask12: NATURAL_MINOR_MASK }
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @category Scales
|
|
29
|
+
*/
|
|
30
|
+
export function minorKey(rootPc: number): KeyScale {
|
|
31
|
+
return { rootPc: ((rootPc % 12) + 12) % 12, modeMask12: NATURAL_MINOR_MASK };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build a `KeyScale` from a named scale (see {@link NAMED_SCALES}).
|
|
36
|
+
*
|
|
37
|
+
* @param name The scale name, e.g. `'dorian'` or `'harmonicMinor'`.
|
|
38
|
+
* @param rootPc The root pitch class.
|
|
39
|
+
* @returns The key/scale.
|
|
40
|
+
* @throws If the name is not a known scale.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* import { scaleByName } from '@libraz/libcantus';
|
|
45
|
+
* const dDorian = scaleByName('dorian', 2); // D Dorian, rootPc 2
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* @category Scales
|
|
49
|
+
*/
|
|
50
|
+
export function scaleByName(name: string, rootPc: number): KeyScale {
|
|
51
|
+
const mask = NAMED_SCALES[name];
|
|
52
|
+
if (mask === undefined) {
|
|
53
|
+
throw new Error(`Unknown scale: ${name}`);
|
|
54
|
+
}
|
|
55
|
+
return { rootPc: ((rootPc % 12) + 12) % 12, modeMask12: mask };
|
|
56
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a 12-bit mode mask from a list of semitone offsets above the root.
|
|
3
|
+
*
|
|
4
|
+
* Bit 0 (the root) is always set, enforcing the `KeyScale` invariant that the
|
|
5
|
+
* root is a scale tone even when the offset list omits 0.
|
|
6
|
+
*
|
|
7
|
+
* @category Scales
|
|
8
|
+
*/
|
|
9
|
+
export function maskFromOffsets(offsets: readonly number[]): number {
|
|
10
|
+
let mask = 1;
|
|
11
|
+
for (const offset of offsets) {
|
|
12
|
+
mask |= 1 << (((offset % 12) + 12) % 12);
|
|
13
|
+
}
|
|
14
|
+
return mask;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Mode mask for the major (Ionian) scale: offsets {0, 2, 4, 5, 7, 9, 11}.
|
|
19
|
+
*
|
|
20
|
+
* @category Scales
|
|
21
|
+
*/
|
|
22
|
+
export const MAJOR_MASK = 0b101010110101;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Mode mask for the natural minor (Aeolian) scale: offsets {0, 2, 3, 5, 7, 8, 10}.
|
|
26
|
+
*
|
|
27
|
+
* @category Scales
|
|
28
|
+
*/
|
|
29
|
+
export const NATURAL_MINOR_MASK = 0b010110101101;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Harmonic minor: natural minor with a raised seventh — offsets {0,2,3,5,7,8,11}.
|
|
33
|
+
*
|
|
34
|
+
* @category Scales
|
|
35
|
+
*/
|
|
36
|
+
export const HARMONIC_MINOR_MASK = maskFromOffsets([0, 2, 3, 5, 7, 8, 11]);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Ascending melodic minor: offsets {0,2,3,5,7,9,11}.
|
|
40
|
+
*
|
|
41
|
+
* @category Scales
|
|
42
|
+
*/
|
|
43
|
+
export const MELODIC_MINOR_MASK = maskFromOffsets([0, 2, 3, 5, 7, 9, 11]);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Dorian mode: offsets {0,2,3,5,7,9,10}.
|
|
47
|
+
*
|
|
48
|
+
* @category Scales
|
|
49
|
+
*/
|
|
50
|
+
export const DORIAN_MASK = maskFromOffsets([0, 2, 3, 5, 7, 9, 10]);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Phrygian mode: offsets {0,1,3,5,7,8,10}.
|
|
54
|
+
*
|
|
55
|
+
* @category Scales
|
|
56
|
+
*/
|
|
57
|
+
export const PHRYGIAN_MASK = maskFromOffsets([0, 1, 3, 5, 7, 8, 10]);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Lydian mode: offsets {0,2,4,6,7,9,11}.
|
|
61
|
+
*
|
|
62
|
+
* @category Scales
|
|
63
|
+
*/
|
|
64
|
+
export const LYDIAN_MASK = maskFromOffsets([0, 2, 4, 6, 7, 9, 11]);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Mixolydian mode: offsets {0,2,4,5,7,9,10}.
|
|
68
|
+
*
|
|
69
|
+
* @category Scales
|
|
70
|
+
*/
|
|
71
|
+
export const MIXOLYDIAN_MASK = maskFromOffsets([0, 2, 4, 5, 7, 9, 10]);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Locrian mode: offsets {0,1,3,5,6,8,10}.
|
|
75
|
+
*
|
|
76
|
+
* @category Scales
|
|
77
|
+
*/
|
|
78
|
+
export const LOCRIAN_MASK = maskFromOffsets([0, 1, 3, 5, 6, 8, 10]);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Major pentatonic: offsets {0,2,4,7,9}.
|
|
82
|
+
*
|
|
83
|
+
* @category Scales
|
|
84
|
+
*/
|
|
85
|
+
export const MAJOR_PENTATONIC_MASK = maskFromOffsets([0, 2, 4, 7, 9]);
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Minor pentatonic: offsets {0,3,5,7,10}.
|
|
89
|
+
*
|
|
90
|
+
* @category Scales
|
|
91
|
+
*/
|
|
92
|
+
export const MINOR_PENTATONIC_MASK = maskFromOffsets([0, 3, 5, 7, 10]);
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Hexatonic blues scale: minor pentatonic plus the flat-fifth — {0,3,5,6,7,10}.
|
|
96
|
+
*
|
|
97
|
+
* @category Scales
|
|
98
|
+
*/
|
|
99
|
+
export const BLUES_MASK = maskFromOffsets([0, 3, 5, 6, 7, 10]);
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Whole-tone scale: offsets {0,2,4,6,8,10}.
|
|
103
|
+
*
|
|
104
|
+
* @category Scales
|
|
105
|
+
*/
|
|
106
|
+
export const WHOLE_TONE_MASK = maskFromOffsets([0, 2, 4, 6, 8, 10]);
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Octatonic (half-step first): offsets {0,1,3,4,6,7,9,10}.
|
|
110
|
+
*
|
|
111
|
+
* @category Scales
|
|
112
|
+
*/
|
|
113
|
+
export const OCTATONIC_HALF_WHOLE_MASK = maskFromOffsets([0, 1, 3, 4, 6, 7, 9, 10]);
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Octatonic (whole-step first): offsets {0,2,3,5,6,8,9,11}.
|
|
117
|
+
*
|
|
118
|
+
* @category Scales
|
|
119
|
+
*/
|
|
120
|
+
export const OCTATONIC_WHOLE_HALF_MASK = maskFromOffsets([0, 2, 3, 5, 6, 8, 9, 11]);
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Chromatic scale: all twelve pitch classes.
|
|
124
|
+
*
|
|
125
|
+
* @category Scales
|
|
126
|
+
*/
|
|
127
|
+
export const CHROMATIC_MASK = 0b111111111111;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Named scale masks addressable by {@link scaleByName}.
|
|
131
|
+
*
|
|
132
|
+
* @category Scales
|
|
133
|
+
*/
|
|
134
|
+
export const NAMED_SCALES: Record<string, number> = {
|
|
135
|
+
major: MAJOR_MASK,
|
|
136
|
+
ionian: MAJOR_MASK,
|
|
137
|
+
naturalMinor: NATURAL_MINOR_MASK,
|
|
138
|
+
aeolian: NATURAL_MINOR_MASK,
|
|
139
|
+
harmonicMinor: HARMONIC_MINOR_MASK,
|
|
140
|
+
melodicMinor: MELODIC_MINOR_MASK,
|
|
141
|
+
dorian: DORIAN_MASK,
|
|
142
|
+
phrygian: PHRYGIAN_MASK,
|
|
143
|
+
lydian: LYDIAN_MASK,
|
|
144
|
+
mixolydian: MIXOLYDIAN_MASK,
|
|
145
|
+
locrian: LOCRIAN_MASK,
|
|
146
|
+
majorPentatonic: MAJOR_PENTATONIC_MASK,
|
|
147
|
+
minorPentatonic: MINOR_PENTATONIC_MASK,
|
|
148
|
+
blues: BLUES_MASK,
|
|
149
|
+
wholeTone: WHOLE_TONE_MASK,
|
|
150
|
+
octatonicHalfWhole: OCTATONIC_HALF_WHOLE_MASK,
|
|
151
|
+
octatonicWholeHalf: OCTATONIC_WHOLE_HALF_MASK,
|
|
152
|
+
chromatic: CHROMATIC_MASK,
|
|
153
|
+
};
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
* A harmonic minor spells its seventh as G#.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import type { Note } from '../../core/pitch/index.js';
|
|
11
|
+
import { formatNote } from '../../core/pitch/index.js';
|
|
12
|
+
import type { KeyScale } from '../../core/types.js';
|
|
10
13
|
import type { Chord } from '../chord/index.js';
|
|
11
|
-
import type { Note } from '../pitch/index.js';
|
|
12
|
-
import { formatNote } from '../pitch/index.js';
|
|
13
14
|
import { scaleTonesInDegreeOrder } from '../scale/index.js';
|
|
14
|
-
import type { KeyScale } from '../types.js';
|
|
15
15
|
|
|
16
16
|
/** Semitone offset of each natural letter above C: C D E F G A B. */
|
|
17
17
|
const LETTER_SEMITONES = [0, 2, 4, 5, 7, 9, 11] as const;
|
|
@@ -79,6 +79,7 @@ function isHeptatonic(key: KeyScale): boolean {
|
|
|
79
79
|
* @param tonic The spelled tonic (its letter anchors the spelling).
|
|
80
80
|
* @param key The key/scale.
|
|
81
81
|
* @returns The spelled note (without octave).
|
|
82
|
+
* @category Pitch & Intervals
|
|
82
83
|
*/
|
|
83
84
|
export function spellPitchClass(pc: number, tonic: Note, key: KeyScale): Note {
|
|
84
85
|
const tonicPc = mod12(naturalPc(tonic.letter) + tonic.alter);
|
|
@@ -117,6 +118,13 @@ export function spellPitchClass(pc: number, tonic: Note, key: KeyScale): Note {
|
|
|
117
118
|
* @param tonic The spelled tonic.
|
|
118
119
|
* @param key The key/scale.
|
|
119
120
|
* @returns Spelled notes, one per scale degree.
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
123
|
+
* import { spellScale, noteNames, parseNote, majorKey } from '@libraz/libcantus';
|
|
124
|
+
* noteNames(spellScale(parseNote('C'), majorKey(0)));
|
|
125
|
+
* // ['C', 'D', 'E', 'F', 'G', 'A', 'B']
|
|
126
|
+
* ```
|
|
127
|
+
* @category Pitch & Intervals
|
|
120
128
|
*/
|
|
121
129
|
export function spellScale(tonic: Note, key: KeyScale): Note[] {
|
|
122
130
|
return scaleTonesInDegreeOrder(key).map((pc) => spellPitchClass(pc, tonic, key));
|
|
@@ -129,6 +137,7 @@ export function spellScale(tonic: Note, key: KeyScale): Note[] {
|
|
|
129
137
|
* @param tonic The spelled tonic.
|
|
130
138
|
* @param key The key/scale.
|
|
131
139
|
* @returns Spelled notes, in input order.
|
|
140
|
+
* @category Pitch & Intervals
|
|
132
141
|
*/
|
|
133
142
|
export function spellPitchClasses(pcs: number[], tonic: Note, key: KeyScale): Note[] {
|
|
134
143
|
return pcs.map((pc) => spellPitchClass(pc, tonic, key));
|
|
@@ -146,6 +155,14 @@ export function spellPitchClasses(pcs: number[], tonic: Note, key: KeyScale): No
|
|
|
146
155
|
* @param tonic The spelled tonic of the key.
|
|
147
156
|
* @param key The key/scale.
|
|
148
157
|
* @returns Spelled chord tones, root first.
|
|
158
|
+
* @example
|
|
159
|
+
* ```ts
|
|
160
|
+
* import { spellChord, noteNames, parseNote, majorKey, makeChord } from '@libraz/libcantus';
|
|
161
|
+
* // G7 in C major
|
|
162
|
+
* noteNames(spellChord(makeChord(7, 'dom7'), parseNote('C'), majorKey(0)));
|
|
163
|
+
* // ['G', 'B', 'D', 'F']
|
|
164
|
+
* ```
|
|
165
|
+
* @category Pitch & Intervals
|
|
149
166
|
*/
|
|
150
167
|
export function spellChord(chord: Chord, tonic: Note, key: KeyScale): Note[] {
|
|
151
168
|
return chord.intervals.map((interval) =>
|
|
@@ -158,6 +175,7 @@ export function spellChord(chord: Chord, tonic: Note, key: KeyScale): Note[] {
|
|
|
158
175
|
*
|
|
159
176
|
* @param notes The notes.
|
|
160
177
|
* @returns Their formatted names.
|
|
178
|
+
* @category Pitch & Intervals
|
|
161
179
|
*/
|
|
162
180
|
export function noteNames(notes: Note[]): string[] {
|
|
163
181
|
return notes.map(formatNote);
|
|
@@ -5,14 +5,24 @@
|
|
|
5
5
|
* (`Cmaj7`, `F#m7b5`, `Bb7`, `C/G`) and the library's structured {@link Chord}
|
|
6
6
|
* type, so callers can accept/emit plain text at the edges of the API.
|
|
7
7
|
*/
|
|
8
|
-
import { type Chord, type ChordQuality, makeChord } from '../chord/index.js';
|
|
9
|
-
import { formatNote, midiToNote, noteToPitchClass, parseNote } from '../pitch/index.js';
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
import {
|
|
10
|
+
formatNote,
|
|
11
|
+
midiToNote,
|
|
12
|
+
type Note,
|
|
13
|
+
noteToPitchClass,
|
|
14
|
+
parseNote,
|
|
15
|
+
} from '../../core/pitch/index.js';
|
|
16
|
+
import { type Chord, type ChordQuality, makeChord, type PitchSpelling } from '../chord/index.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Matches a chord root: a letter A-G (case-insensitive, like {@link parseNote})
|
|
20
|
+
* with an optional single-direction accidental.
|
|
21
|
+
*/
|
|
22
|
+
const ROOT_RE = /^[A-Ga-g](?:#{1,2}|b{1,2})?/;
|
|
13
23
|
|
|
14
24
|
/** Matches a bare note usable as a slash-chord bass (no octave). */
|
|
15
|
-
const BASS_RE = /^[A-
|
|
25
|
+
const BASS_RE = /^[A-Ga-g](?:#{1,2}|b{1,2})?$/;
|
|
16
26
|
|
|
17
27
|
/** Recognized quality suffixes, keyed by their exact lead-sheet spelling. */
|
|
18
28
|
const QUALITY_MAP: Record<string, ChordQuality> = {
|
|
@@ -114,22 +124,44 @@ function pitchClass(value: number): number {
|
|
|
114
124
|
return ((Math.trunc(value) % 12) + 12) % 12;
|
|
115
125
|
}
|
|
116
126
|
|
|
117
|
-
/**
|
|
118
|
-
function
|
|
119
|
-
|
|
127
|
+
/** Build a chord and record the parsed spellings as enharmonic hints. */
|
|
128
|
+
function makeSpelledChord(root: Note, quality: ChordQuality, bass?: Note): Chord {
|
|
129
|
+
const chord = makeChord(
|
|
130
|
+
noteToPitchClass(root),
|
|
131
|
+
quality,
|
|
132
|
+
bass === undefined ? undefined : noteToPitchClass(bass),
|
|
133
|
+
);
|
|
134
|
+
chord.rootSpelling = { letter: root.letter, alter: root.alter };
|
|
135
|
+
if (bass !== undefined) {
|
|
136
|
+
chord.bassSpelling = { letter: bass.letter, alter: bass.alter };
|
|
137
|
+
}
|
|
138
|
+
return chord;
|
|
120
139
|
}
|
|
121
140
|
|
|
122
141
|
/**
|
|
123
142
|
* Parse a lead-sheet chord symbol into a {@link Chord}.
|
|
124
143
|
*
|
|
125
144
|
* Accepts `<root><quality>[/<bass>]`, e.g. `Cmaj7`, `F#m7b5`, `Bb7`, `C/G`,
|
|
126
|
-
* `C6/9`.
|
|
127
|
-
*
|
|
128
|
-
*
|
|
145
|
+
* `C6/9`, `C6/9/E`. Root and bass letters are case-insensitive, matching
|
|
146
|
+
* {@link parseNote}. A `/` is ambiguous between a slash bass and the `6/9`
|
|
147
|
+
* quality; the text splits at the last `/` whose right-hand side is a valid
|
|
148
|
+
* bare note preceded by a recognized quality, so `6/9` (with or without a
|
|
149
|
+
* further slash bass) parses as a quality.
|
|
150
|
+
*
|
|
151
|
+
* The parsed chord carries the root/bass spellings as enharmonic hints
|
|
152
|
+
* (`rootSpelling`/`bassSpelling`) so {@link formatChordSymbol} can reproduce
|
|
153
|
+
* flat spellings such as `Bbmaj7` instead of respelling them with sharps.
|
|
129
154
|
*
|
|
130
155
|
* @param text The chord symbol text.
|
|
131
156
|
* @returns The parsed chord.
|
|
132
157
|
* @throws If the root or quality is not recognized.
|
|
158
|
+
* @example
|
|
159
|
+
* ```ts
|
|
160
|
+
* import { parseChordSymbol, formatChordSymbol } from '@libraz/libcantus';
|
|
161
|
+
* const c = parseChordSymbol('Bbmaj7');
|
|
162
|
+
* formatChordSymbol(c); // 'Bbmaj7' — flat spelling is preserved on round-trip
|
|
163
|
+
* ```
|
|
164
|
+
* @category Chords
|
|
133
165
|
*/
|
|
134
166
|
export function parseChordSymbol(text: string): Chord {
|
|
135
167
|
const trimmed = text.trim();
|
|
@@ -138,31 +170,41 @@ export function parseChordSymbol(text: string): Chord {
|
|
|
138
170
|
throw new Error(`Invalid chord symbol: ${text}`);
|
|
139
171
|
}
|
|
140
172
|
const rootToken = rootMatch[0];
|
|
141
|
-
const
|
|
173
|
+
const rootNote = parseNote(rootToken);
|
|
142
174
|
const rest = trimmed.slice(rootToken.length);
|
|
143
175
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
176
|
+
// Split at the last '/' so the '6/9' quality's own slash never masks a
|
|
177
|
+
// trailing slash bass (e.g. 'C6/9/E').
|
|
178
|
+
const slashIndex = rest.lastIndexOf('/');
|
|
179
|
+
if (slashIndex !== -1) {
|
|
180
|
+
const before = rest.slice(0, slashIndex);
|
|
181
|
+
const after = rest.slice(slashIndex + 1);
|
|
182
|
+
const beforeQuality = QUALITY_MAP[before];
|
|
183
|
+
if (beforeQuality !== undefined && BASS_RE.test(after)) {
|
|
184
|
+
return makeSpelledChord(rootNote, beforeQuality, parseNote(after));
|
|
149
185
|
}
|
|
150
|
-
return makeChord(rootPc, quality);
|
|
151
186
|
}
|
|
152
187
|
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (beforeQuality !== undefined && BASS_RE.test(after)) {
|
|
157
|
-
return makeChord(rootPc, beforeQuality, bassPitchClass(after));
|
|
188
|
+
const quality = QUALITY_MAP[rest];
|
|
189
|
+
if (quality === undefined) {
|
|
190
|
+
throw new Error(`Unrecognized chord quality: ${text}`);
|
|
158
191
|
}
|
|
192
|
+
return makeSpelledChord(rootNote, quality);
|
|
193
|
+
}
|
|
159
194
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
195
|
+
/**
|
|
196
|
+
* Name a pitch class, preferring a spelling hint when it is still valid.
|
|
197
|
+
*
|
|
198
|
+
* A hint is used only when no explicit sharp/flat preference was given and the
|
|
199
|
+
* hint still resolves to `pc` (a stale hint left over after transposition is
|
|
200
|
+
* ignored). Otherwise the pitch class is respelled from the requested table.
|
|
201
|
+
*/
|
|
202
|
+
function pitchClassName(pc: number, hint: PitchSpelling | undefined, flats?: boolean): string {
|
|
203
|
+
if (flats === undefined && hint !== undefined && noteToPitchClass(hint) === pc) {
|
|
204
|
+
return formatNote(hint);
|
|
163
205
|
}
|
|
164
|
-
|
|
165
|
-
|
|
206
|
+
const note = midiToNote(60 + pc, flats ? 'flat' : 'sharp');
|
|
207
|
+
return formatNote({ letter: note.letter, alter: note.alter });
|
|
166
208
|
}
|
|
167
209
|
|
|
168
210
|
/**
|
|
@@ -170,22 +212,29 @@ export function parseChordSymbol(text: string): Chord {
|
|
|
170
212
|
*
|
|
171
213
|
* The inverse of {@link parseChordSymbol}: each quality maps to one canonical
|
|
172
214
|
* suffix spelling, and a slash bass is appended only when it differs from the
|
|
173
|
-
* root.
|
|
215
|
+
* root. When the chord carries `rootSpelling`/`bassSpelling` hints (as chords
|
|
216
|
+
* from {@link parseChordSymbol} do) and no explicit `flats` preference is
|
|
217
|
+
* given, the hints are reused so flat symbols round-trip unchanged.
|
|
174
218
|
*
|
|
175
219
|
* @param chord The chord to format.
|
|
176
220
|
* @param opts Formatting options.
|
|
177
|
-
* @param opts.flats Prefer flat spellings over sharps for altered roots/basses
|
|
221
|
+
* @param opts.flats Prefer flat spellings over sharps for altered roots/basses;
|
|
222
|
+
* passing either `true` or `false` overrides any spelling hints.
|
|
178
223
|
* @returns The chord symbol text.
|
|
224
|
+
* @example
|
|
225
|
+
* ```ts
|
|
226
|
+
* import { makeChord, formatChordSymbol } from '@libraz/libcantus';
|
|
227
|
+
* formatChordSymbol(makeChord(0, 'min7')); // 'Cm7'
|
|
228
|
+
* ```
|
|
229
|
+
* @category Chords
|
|
179
230
|
*/
|
|
180
231
|
export function formatChordSymbol(chord: Chord, opts?: { flats?: boolean }): string {
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
const rootName = formatNote({ letter: rootNote.letter, alter: rootNote.alter });
|
|
232
|
+
const rootPc = pitchClass(chord.rootPc);
|
|
233
|
+
const rootName = pitchClassName(rootPc, chord.rootSpelling, opts?.flats);
|
|
184
234
|
const suffix = CANONICAL_SUFFIX[chord.quality];
|
|
185
235
|
let symbol = `${rootName}${suffix}`;
|
|
186
|
-
if (chord.bassPc !== undefined && pitchClass(chord.bassPc) !==
|
|
187
|
-
|
|
188
|
-
symbol += `/${formatNote({ letter: bassNote.letter, alter: bassNote.alter })}`;
|
|
236
|
+
if (chord.bassPc !== undefined && pitchClass(chord.bassPc) !== rootPc) {
|
|
237
|
+
symbol += `/${pitchClassName(pitchClass(chord.bassPc), chord.bassSpelling, opts?.flats)}`;
|
|
189
238
|
}
|
|
190
239
|
return symbol;
|
|
191
240
|
}
|
|
@@ -198,6 +247,12 @@ export function formatChordSymbol(chord: Chord, opts?: { flats?: boolean }): str
|
|
|
198
247
|
* @param opts Formatting options for the result; see {@link formatChordSymbol}.
|
|
199
248
|
* @returns The transposed chord symbol text.
|
|
200
249
|
* @throws If `text` does not parse as a chord symbol.
|
|
250
|
+
* @example
|
|
251
|
+
* ```ts
|
|
252
|
+
* import { transposeChordSymbol } from '@libraz/libcantus';
|
|
253
|
+
* transposeChordSymbol('C/G', 2); // 'D/A'
|
|
254
|
+
* ```
|
|
255
|
+
* @category Chords
|
|
201
256
|
*/
|
|
202
257
|
export function transposeChordSymbol(
|
|
203
258
|
text: string,
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { nextVoicing, voiceLeadingCost } from './leading.js';
|
|
2
|
+
export type { VoiceRange, VoicingOptions } from './satb.js';
|
|
3
|
+
export { SATB_RANGES, voiceChord, voiceProgression } from './satb.js';
|
|
4
|
+
export type { StyledVoicingOptions, VoicingStyle } from './styled.js';
|
|
5
|
+
export { voiceChordStyled } from './styled.js';
|