@libraz/libcantus 0.9.2 → 0.9.4
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 +6 -5
- package/dist/analyze/index.cjs +41 -1676
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +194 -122
- package/dist/analyze/index.d.ts +194 -122
- package/dist/analyze/index.js +19 -8
- package/dist/chunk-36N73KZ3.cjs +246 -0
- package/dist/chunk-36N73KZ3.cjs.map +1 -0
- package/dist/chunk-4HLLZPXT.cjs +3077 -0
- package/dist/chunk-4HLLZPXT.cjs.map +1 -0
- package/dist/chunk-7MGZ7D62.cjs +498 -0
- package/dist/chunk-7MGZ7D62.cjs.map +1 -0
- package/dist/{chunk-R3S2LUBH.js → chunk-7NKDPF74.js} +660 -305
- package/dist/chunk-7NKDPF74.js.map +1 -0
- package/dist/chunk-CAN76NCU.cjs +679 -0
- package/dist/chunk-CAN76NCU.cjs.map +1 -0
- package/dist/{chunk-7Y7WF64D.js → chunk-D2FAJBYL.js} +104 -35
- package/dist/chunk-D2FAJBYL.js.map +1 -0
- package/dist/{chunk-COKTX2G5.js → chunk-EHVEBWM5.js} +1052 -510
- package/dist/chunk-EHVEBWM5.js.map +1 -0
- package/dist/chunk-FCANXBAT.cjs +30 -0
- package/dist/chunk-FCANXBAT.cjs.map +1 -0
- package/dist/chunk-FNDWXRRI.cjs +479 -0
- package/dist/chunk-FNDWXRRI.cjs.map +1 -0
- package/dist/{chunk-GQKN5KDT.js → chunk-GQSGS2N4.js} +43 -29
- package/dist/chunk-GQSGS2N4.js.map +1 -0
- package/dist/chunk-HKGYCQ34.js +53 -0
- package/dist/chunk-HKGYCQ34.js.map +1 -0
- package/dist/chunk-I24MV65U.js +106 -0
- package/dist/chunk-I24MV65U.js.map +1 -0
- package/dist/{chunk-LN7ZZMD2.js → chunk-KMHOICIY.js} +322 -173
- package/dist/chunk-KMHOICIY.js.map +1 -0
- package/dist/chunk-LW53AHQU.cjs +1117 -0
- package/dist/chunk-LW53AHQU.cjs.map +1 -0
- package/dist/chunk-N6IHQUOX.js +1 -0
- package/dist/chunk-N6IHQUOX.js.map +1 -0
- package/dist/chunk-NKIBQ6PT.js +30 -0
- package/dist/chunk-NKIBQ6PT.js.map +1 -0
- package/dist/chunk-NP4NA2FB.cjs +106 -0
- package/dist/chunk-NP4NA2FB.cjs.map +1 -0
- package/dist/chunk-NVE34YGZ.cjs +1449 -0
- package/dist/chunk-NVE34YGZ.cjs.map +1 -0
- package/dist/{chunk-A4532ZFR.js → chunk-NYYWMV2C.js} +115 -47
- package/dist/chunk-NYYWMV2C.js.map +1 -0
- package/dist/{chunk-5HTNMQW4.js → chunk-Q2XUP227.js} +310 -118
- package/dist/chunk-Q2XUP227.js.map +1 -0
- package/dist/chunk-Q6OP4U43.cjs +327 -0
- package/dist/chunk-Q6OP4U43.cjs.map +1 -0
- package/dist/chunk-RC6GBMCD.cjs +845 -0
- package/dist/chunk-RC6GBMCD.cjs.map +1 -0
- package/dist/{chunk-XTC3X6MS.js → chunk-RK35RIN7.js} +453 -152
- package/dist/chunk-RK35RIN7.js.map +1 -0
- package/dist/chunk-RUMU3UA6.cjs +53 -0
- package/dist/chunk-RUMU3UA6.cjs.map +1 -0
- package/dist/chunk-T64C7ZD7.cjs +1 -0
- package/dist/chunk-T64C7ZD7.cjs.map +1 -0
- package/dist/chunk-XEBG3G53.js +498 -0
- package/dist/chunk-XEBG3G53.js.map +1 -0
- package/dist/chunk-YLEMGEFH.js +246 -0
- package/dist/chunk-YLEMGEFH.js.map +1 -0
- package/dist/chunk-ZEJR4TKL.cjs +173 -0
- package/dist/chunk-ZEJR4TKL.cjs.map +1 -0
- package/dist/core/index.cjs +130 -431
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +331 -17
- package/dist/core/index.d.ts +331 -17
- package/dist/core/index.js +71 -13
- package/dist/{function-2bJW1Zp_.d.ts → function-BtiZb9i9.d.ts} +28 -7
- package/dist/{function-r2sh-jOO.d.cts → function-DNugAOrt.d.cts} +28 -7
- package/dist/generate/index.cjs +30 -3751
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +373 -59
- package/dist/generate/index.d.ts +373 -59
- package/dist/generate/index.js +23 -9
- package/dist/{index-DZ1M21do.d.cts → index-6VR8-kit.d.ts} +20 -4
- package/dist/index-70p168VM.d.ts +192 -0
- package/dist/{index-D_ZTUNKp.d.cts → index-Be74mcon.d.cts} +18 -17
- package/dist/index-BtXT6UQ0.d.cts +192 -0
- package/dist/{index-BMPxwjwV.d.ts → index-C5UBbQ4G.d.ts} +18 -17
- package/dist/{index-BOBXBNwP.d.cts → index-C83mZI1P.d.ts} +93 -8
- package/dist/{index-BdeQ1r_p.d.cts → index-D6b6na69.d.cts} +14 -11
- package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
- package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
- package/dist/index-OG65ocOr.d.cts +272 -0
- package/dist/index-OG65ocOr.d.ts +272 -0
- package/dist/{index-BreoJRw0.d.ts → index-tJhkmQgE.d.cts} +20 -4
- package/dist/{index-CP3wAfEH.d.ts → index-tSQh-xwY.d.ts} +14 -11
- package/dist/{index-CM5W4rcm.d.ts → index-y_ak9_Hi.d.cts} +93 -8
- package/dist/index.cjs +419 -6848
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -13
- package/dist/index.d.ts +14 -13
- package/dist/index.js +132 -27
- package/dist/model/index.cjs +8 -2423
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +433 -74
- package/dist/model/index.d.ts +433 -74
- package/dist/model/index.js +6 -7
- package/dist/styled-DIRkk9Uz.d.ts +506 -0
- package/dist/styled-DJbkJQfj.d.cts +506 -0
- package/dist/theory/index.cjs +172 -1676
- package/dist/theory/index.cjs.map +1 -1
- package/dist/theory/index.d.cts +151 -233
- package/dist/theory/index.d.ts +151 -233
- package/dist/theory/index.js +31 -7
- package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
- package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
- package/package.json +73 -24
- package/dist/chunk-5HTNMQW4.js.map +0 -1
- package/dist/chunk-7Y7WF64D.js.map +0 -1
- package/dist/chunk-A4532ZFR.js.map +0 -1
- package/dist/chunk-BHUT2S5G.js +0 -22
- package/dist/chunk-BHUT2S5G.js.map +0 -1
- package/dist/chunk-CCBLVAPK.js +0 -42
- package/dist/chunk-CCBLVAPK.js.map +0 -1
- package/dist/chunk-COKTX2G5.js.map +0 -1
- package/dist/chunk-DYWFNC4A.js +0 -152
- package/dist/chunk-DYWFNC4A.js.map +0 -1
- package/dist/chunk-GQKN5KDT.js.map +0 -1
- package/dist/chunk-LN7ZZMD2.js.map +0 -1
- package/dist/chunk-MSWFZF7J.js +0 -118
- package/dist/chunk-MSWFZF7J.js.map +0 -1
- package/dist/chunk-R3S2LUBH.js.map +0 -1
- package/dist/chunk-SDM3VJJ5.js +0 -41
- package/dist/chunk-SDM3VJJ5.js.map +0 -1
- package/dist/chunk-XTC3X6MS.js.map +0 -1
- package/dist/chunk-ZXW5YIWK.js +0 -55
- package/dist/chunk-ZXW5YIWK.js.map +0 -1
- package/dist/index-CGMZNCx6.d.cts +0 -140
- package/dist/index-CGMZNCx6.d.ts +0 -140
- package/dist/styled-BjXtOFNk.d.cts +0 -282
- package/dist/styled-D7BAx3S3.d.ts +0 -282
- package/src/analyze/arrange/index.ts +0 -21
- package/src/analyze/arrange/internal.ts +0 -125
- package/src/analyze/arrange/tension.ts +0 -187
- package/src/analyze/arrange/tracks.ts +0 -279
- package/src/analyze/detect/index.ts +0 -235
- package/src/analyze/functional/borrowed.ts +0 -84
- package/src/analyze/functional/cadence.ts +0 -66
- package/src/analyze/functional/function.ts +0 -184
- package/src/analyze/functional/index.ts +0 -23
- package/src/analyze/functional/internal.ts +0 -29
- package/src/analyze/functional/roman.ts +0 -393
- package/src/analyze/index.ts +0 -49
- package/src/analyze/timeline/index.ts +0 -415
- package/src/analyze/voice/index.ts +0 -330
- package/src/core/index.ts +0 -47
- package/src/core/interval/index.ts +0 -81
- package/src/core/meter/index.ts +0 -282
- package/src/core/pitch/index.ts +0 -311
- package/src/core/random/index.ts +0 -53
- package/src/core/tuning/index.ts +0 -156
- package/src/core/types.ts +0 -29
- package/src/generate/bass/index.ts +0 -350
- package/src/generate/countermelody/index.ts +0 -379
- package/src/generate/drums/beat.ts +0 -266
- package/src/generate/drums/euclid.ts +0 -58
- package/src/generate/drums/fills.ts +0 -356
- package/src/generate/drums/ghost.ts +0 -119
- package/src/generate/drums/hihat.ts +0 -285
- package/src/generate/drums/hit.ts +0 -31
- package/src/generate/drums/index.ts +0 -329
- package/src/generate/drums/internal.ts +0 -257
- package/src/generate/drums/kick.ts +0 -160
- package/src/generate/drums/percussion.ts +0 -121
- package/src/generate/drums/rng.ts +0 -7
- package/src/generate/drums/swing.ts +0 -71
- package/src/generate/groove/index.ts +0 -326
- package/src/generate/harmonize/index.ts +0 -440
- package/src/generate/index.ts +0 -59
- package/src/generate/motif/index.ts +0 -398
- package/src/generate/progression/index.ts +0 -370
- package/src/generate/reharmony/index.ts +0 -330
- package/src/generate/rhythm/index.ts +0 -165
- package/src/index.ts +0 -247
- package/src/model/chord.ts +0 -423
- package/src/model/index.ts +0 -15
- package/src/model/interval.ts +0 -82
- package/src/model/key.ts +0 -266
- package/src/model/note.ts +0 -176
- package/src/model/progression.ts +0 -161
- package/src/model/shared.ts +0 -12
- package/src/theory/chord/index.ts +0 -373
- package/src/theory/chordscale/index.ts +0 -374
- package/src/theory/counterpoint/index.ts +0 -230
- package/src/theory/harmony/index.ts +0 -92
- package/src/theory/index.ts +0 -107
- package/src/theory/safety/index.ts +0 -419
- package/src/theory/scale/degrees.ts +0 -123
- package/src/theory/scale/index.ts +0 -28
- package/src/theory/scale/key.ts +0 -56
- package/src/theory/scale/masks.ts +0 -153
- package/src/theory/spelling/index.ts +0 -182
- package/src/theory/symbol/index.ts +0 -268
- package/src/theory/voicing/index.ts +0 -5
- package/src/theory/voicing/internal.ts +0 -168
- package/src/theory/voicing/leading.ts +0 -112
- package/src/theory/voicing/satb.ts +0 -195
- package/src/theory/voicing/styled.ts +0 -176
- package/tsconfig.json +0 -23
|
@@ -1,370 +0,0 @@
|
|
|
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
|
-
|
|
6
|
-
export type { ChordSpan } from '../../theory/chord/index.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Broad production style a progression preset suits.
|
|
10
|
-
*
|
|
11
|
-
* @category Composition
|
|
12
|
-
*/
|
|
13
|
-
export type ProgStyle = 'minimal' | 'dance' | 'idol' | 'rock';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Harmonic-function role of a progression as a whole.
|
|
17
|
-
*
|
|
18
|
-
* @category Composition
|
|
19
|
-
*/
|
|
20
|
-
export type ProgFunction = 'loop' | 'tensionBuild' | 'cadenceStrong' | 'stable';
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* A named chord-progression preset expressed in scale degrees.
|
|
24
|
-
*
|
|
25
|
-
* @category Composition
|
|
26
|
-
*/
|
|
27
|
-
export type ProgressionPreset = {
|
|
28
|
-
id: string;
|
|
29
|
-
name: string;
|
|
30
|
-
degrees: number[];
|
|
31
|
-
functional: ProgFunction;
|
|
32
|
-
styles: ProgStyle[];
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Options controlling {@link generateProgression}.
|
|
37
|
-
*
|
|
38
|
-
* @category Composition
|
|
39
|
-
*/
|
|
40
|
-
export type ProgressionOptions = {
|
|
41
|
-
key: KeyScale;
|
|
42
|
-
style: ProgStyle;
|
|
43
|
-
bars: number;
|
|
44
|
-
presetId?: string;
|
|
45
|
-
ext?: ChordQuality | 'auto';
|
|
46
|
-
reharmonize?: boolean;
|
|
47
|
-
/**
|
|
48
|
-
* Seed for the deterministic preset choice and reharmonization.
|
|
49
|
-
*
|
|
50
|
-
* @defaultValue 0
|
|
51
|
-
*/
|
|
52
|
-
seed?: number;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const PRESETS: ProgressionPreset[] = [
|
|
56
|
-
{
|
|
57
|
-
id: 'fourChordPop',
|
|
58
|
-
name: 'Four Chord Pop',
|
|
59
|
-
degrees: [0, 4, 5, 3],
|
|
60
|
-
functional: 'loop',
|
|
61
|
-
styles: ['minimal', 'dance', 'idol'],
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
id: 'pop1',
|
|
65
|
-
name: 'Pop 1',
|
|
66
|
-
degrees: [0, 5, 3, 4],
|
|
67
|
-
functional: 'loop',
|
|
68
|
-
styles: ['minimal', 'dance', 'idol'],
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
id: 'axis',
|
|
72
|
-
name: 'Axis',
|
|
73
|
-
degrees: [5, 3, 0, 4],
|
|
74
|
-
functional: 'loop',
|
|
75
|
-
styles: ['minimal', 'dance', 'idol', 'rock'],
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
id: 'pop2',
|
|
79
|
-
name: 'Pop 2',
|
|
80
|
-
degrees: [3, 0, 4, 5],
|
|
81
|
-
functional: 'loop',
|
|
82
|
-
styles: ['minimal', 'dance', 'idol'],
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
id: 'classic',
|
|
86
|
-
name: 'Classic',
|
|
87
|
-
degrees: [0, 3, 4, 0],
|
|
88
|
-
functional: 'cadenceStrong',
|
|
89
|
-
styles: ['dance', 'idol', 'rock'],
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
id: 'pop3',
|
|
93
|
-
name: 'Pop 3',
|
|
94
|
-
degrees: [0, 3, 5, 4],
|
|
95
|
-
functional: 'loop',
|
|
96
|
-
styles: ['minimal', 'dance', 'idol'],
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
id: 'royalRoad',
|
|
100
|
-
name: 'Royal Road',
|
|
101
|
-
degrees: [3, 4, 2, 5],
|
|
102
|
-
functional: 'tensionBuild',
|
|
103
|
-
styles: ['dance', 'idol'],
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
id: 'minor1',
|
|
107
|
-
name: 'Minor 1',
|
|
108
|
-
degrees: [5, 4, 3, 4],
|
|
109
|
-
functional: 'tensionBuild',
|
|
110
|
-
styles: ['idol', 'rock'],
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
id: 'minor2',
|
|
114
|
-
name: 'Minor 2',
|
|
115
|
-
degrees: [5, 3, 4, 0],
|
|
116
|
-
functional: 'tensionBuild',
|
|
117
|
-
styles: ['idol', 'rock'],
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
id: 'pop4',
|
|
121
|
-
name: 'Pop 4',
|
|
122
|
-
degrees: [0, 4, 2, 3],
|
|
123
|
-
functional: 'loop',
|
|
124
|
-
styles: ['minimal', 'dance', 'idol'],
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
id: 'pop5',
|
|
128
|
-
name: 'Pop 5',
|
|
129
|
-
degrees: [0, 2, 3, 4],
|
|
130
|
-
functional: 'stable',
|
|
131
|
-
styles: ['minimal', 'dance', 'idol'],
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
id: 'rock1',
|
|
135
|
-
name: 'Rock 1',
|
|
136
|
-
degrees: [0, 10, 3, 0],
|
|
137
|
-
functional: 'tensionBuild',
|
|
138
|
-
styles: ['rock'],
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
id: 'rock2',
|
|
142
|
-
name: 'Rock 2',
|
|
143
|
-
degrees: [0, 3, 10, 0],
|
|
144
|
-
functional: 'tensionBuild',
|
|
145
|
-
styles: ['rock'],
|
|
146
|
-
},
|
|
147
|
-
{
|
|
148
|
-
id: 'extended4',
|
|
149
|
-
name: 'Extended 4',
|
|
150
|
-
degrees: [0, 4, 5, 2],
|
|
151
|
-
functional: 'stable',
|
|
152
|
-
styles: ['minimal', 'dance'],
|
|
153
|
-
},
|
|
154
|
-
{
|
|
155
|
-
id: 'minor3',
|
|
156
|
-
name: 'Minor 3',
|
|
157
|
-
degrees: [5, 0, 4, 3],
|
|
158
|
-
functional: 'loop',
|
|
159
|
-
styles: ['dance', 'idol'],
|
|
160
|
-
},
|
|
161
|
-
{
|
|
162
|
-
id: 'aeolianPop',
|
|
163
|
-
name: 'Aeolian Pop',
|
|
164
|
-
degrees: [5, 8, 10, 0],
|
|
165
|
-
functional: 'tensionBuild',
|
|
166
|
-
styles: ['minimal', 'dance', 'idol', 'rock'],
|
|
167
|
-
},
|
|
168
|
-
{
|
|
169
|
-
id: 'animeHighEnergy1',
|
|
170
|
-
name: 'Anime High Energy 1',
|
|
171
|
-
degrees: [5, 2, 3, 0],
|
|
172
|
-
functional: 'loop',
|
|
173
|
-
styles: ['dance', 'idol'],
|
|
174
|
-
},
|
|
175
|
-
{
|
|
176
|
-
id: 'jazzPop',
|
|
177
|
-
name: 'Jazz Pop',
|
|
178
|
-
degrees: [1, 4, 0, 5],
|
|
179
|
-
functional: 'cadenceStrong',
|
|
180
|
-
styles: ['minimal', 'dance'],
|
|
181
|
-
},
|
|
182
|
-
{
|
|
183
|
-
id: 'animeHighEnergy2',
|
|
184
|
-
name: 'Anime High Energy 2',
|
|
185
|
-
degrees: [5, 1, 4, 0],
|
|
186
|
-
functional: 'cadenceStrong',
|
|
187
|
-
styles: ['dance', 'idol'],
|
|
188
|
-
},
|
|
189
|
-
{
|
|
190
|
-
id: 'cityPop',
|
|
191
|
-
name: 'City Pop',
|
|
192
|
-
degrees: [0, 5, 1, 4],
|
|
193
|
-
functional: 'stable',
|
|
194
|
-
styles: ['minimal', 'dance'],
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
id: 'extended5',
|
|
198
|
-
name: 'Extended 5',
|
|
199
|
-
degrees: [0, 4, 5, 2, 3],
|
|
200
|
-
functional: 'loop',
|
|
201
|
-
styles: ['minimal', 'dance', 'idol'],
|
|
202
|
-
},
|
|
203
|
-
{
|
|
204
|
-
id: 'neapolitanPop',
|
|
205
|
-
name: 'Neapolitan Pop',
|
|
206
|
-
degrees: [5, 12, 13, 4, 0],
|
|
207
|
-
functional: 'cadenceStrong',
|
|
208
|
-
styles: ['minimal', 'dance', 'idol'],
|
|
209
|
-
},
|
|
210
|
-
];
|
|
211
|
-
|
|
212
|
-
/** Chromatic semitone offset from the tonic for borrowed (non-diatonic) degrees. */
|
|
213
|
-
const BORROWED_OFFSET: Record<number, number> = {
|
|
214
|
-
8: 8, // bVI
|
|
215
|
-
10: 10, // bVII
|
|
216
|
-
11: 3, // bIII
|
|
217
|
-
12: 5, // iv
|
|
218
|
-
13: 1, // bII
|
|
219
|
-
14: 6, // #IV
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* All built-in progression presets.
|
|
224
|
-
*
|
|
225
|
-
* @category Composition
|
|
226
|
-
*/
|
|
227
|
-
export function progressions(): ProgressionPreset[] {
|
|
228
|
-
return PRESETS.map((p) => ({ ...p, degrees: [...p.degrees], styles: [...p.styles] }));
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Presets whose style list includes the given style.
|
|
233
|
-
*
|
|
234
|
-
* @category Composition
|
|
235
|
-
*/
|
|
236
|
-
export function progressionsByStyle(style: ProgStyle): ProgressionPreset[] {
|
|
237
|
-
return progressions().filter((p) => p.styles.includes(style));
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/** Deterministic 32-bit PRNG producing a float in [0, 1). */
|
|
241
|
-
function mulberry32(seed: number): () => number {
|
|
242
|
-
let state = seed >>> 0;
|
|
243
|
-
return () => {
|
|
244
|
-
state = (state + 0x6d2b79f5) >>> 0;
|
|
245
|
-
let t = state;
|
|
246
|
-
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
247
|
-
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
248
|
-
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/** Root pitch class of a scale degree in the given key, including borrowed degrees. */
|
|
253
|
-
function degreeToRootPc(degree: number, key: KeyScale): number {
|
|
254
|
-
if (degree >= 0 && degree <= 6) {
|
|
255
|
-
const tones = scaleTonesInDegreeOrder(key);
|
|
256
|
-
return tones.length > 0 ? (tones[degree % tones.length] ?? key.rootPc % 12) : key.rootPc % 12;
|
|
257
|
-
}
|
|
258
|
-
const offset = BORROWED_OFFSET[degree] ?? 0;
|
|
259
|
-
return ((((key.rootPc % 12) + offset) % 12) + 12) % 12;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Diatonic (or borrowed) triad quality of a degree in the given key.
|
|
264
|
-
*
|
|
265
|
-
* Diatonic degrees (0-6) take their scale-correct triad quality, so non-major
|
|
266
|
-
* keys yield diatonic chords. Borrowed degrees keep their fixed chromatic
|
|
267
|
-
* qualities (`#IV` diminished, `iv` minor, the rest major).
|
|
268
|
-
*/
|
|
269
|
-
function autoQuality(degree: number, key: KeyScale): ChordQuality {
|
|
270
|
-
if (degree >= 0 && degree <= 6) {
|
|
271
|
-
return diatonicTriad(degree, key).quality;
|
|
272
|
-
}
|
|
273
|
-
if (degree === 14) {
|
|
274
|
-
return 'dim';
|
|
275
|
-
}
|
|
276
|
-
if (degree === 12) {
|
|
277
|
-
return 'min';
|
|
278
|
-
}
|
|
279
|
-
return 'maj';
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Generate a chord progression laid out one chord per bar.
|
|
284
|
-
*
|
|
285
|
-
* A preset is chosen by `presetId` when given, otherwise deterministically from
|
|
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
|
|
289
|
-
* fill `bars`; each bar is four beats, so `startBeat` is `barIndex * 4`. Chord
|
|
290
|
-
* roots come from the key's diatonic scale-degree mapping. When `ext` is
|
|
291
|
-
* omitted or `'auto'`, each chord takes its diatonic triad quality; otherwise
|
|
292
|
-
* `ext` is forced on every chord. When `reharmonize` is set, some chords are
|
|
293
|
-
* deterministically replaced with the secondary dominant (V7) of the following
|
|
294
|
-
* chord, flagged with `secondaryDominant`.
|
|
295
|
-
*
|
|
296
|
-
* @param opts Generation options.
|
|
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
|
|
308
|
-
*/
|
|
309
|
-
export function generateProgression(opts: ProgressionOptions): ChordSpan[] {
|
|
310
|
-
const seed = opts.seed ?? 0;
|
|
311
|
-
let preset: ProgressionPreset | undefined;
|
|
312
|
-
if (opts.presetId !== undefined) {
|
|
313
|
-
preset = PRESETS.find((p) => p.id === opts.presetId);
|
|
314
|
-
if (preset === undefined) {
|
|
315
|
-
throw new Error(`Unknown progression preset: ${opts.presetId}`);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
if (preset === undefined) {
|
|
319
|
-
const pool = PRESETS.filter((p) => p.styles.includes(opts.style));
|
|
320
|
-
const candidates = pool.length > 0 ? pool : PRESETS;
|
|
321
|
-
const rng = mulberry32(seed);
|
|
322
|
-
const index = Math.floor(rng() * candidates.length) % candidates.length;
|
|
323
|
-
preset = candidates[index] ?? PRESETS[0];
|
|
324
|
-
}
|
|
325
|
-
const degrees = preset?.degrees ?? [0];
|
|
326
|
-
const chords: ChordSpan[] = [];
|
|
327
|
-
for (let bar = 0; bar < opts.bars; bar += 1) {
|
|
328
|
-
const degree = degrees[bar % degrees.length] ?? 0;
|
|
329
|
-
const quality =
|
|
330
|
-
opts.ext !== undefined && opts.ext !== 'auto' ? opts.ext : autoQuality(degree, opts.key);
|
|
331
|
-
const chord: ChordSpan = {
|
|
332
|
-
rootPc: degreeToRootPc(degree, opts.key),
|
|
333
|
-
quality,
|
|
334
|
-
startBeat: bar * 4,
|
|
335
|
-
degree: degree >= 0 && degree <= 6 ? degree : undefined,
|
|
336
|
-
};
|
|
337
|
-
chords.push(chord);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
if (opts.reharmonize) {
|
|
341
|
-
const tonicPc = (((opts.key.rootPc % 12) + 12) % 12) as number;
|
|
342
|
-
const rrng = mulberry32((seed ^ 0x9e3779b9) >>> 0);
|
|
343
|
-
for (let i = 0; i < chords.length - 1; i += 1) {
|
|
344
|
-
const cur = chords[i];
|
|
345
|
-
const next = chords[i + 1];
|
|
346
|
-
if (!cur || !next) {
|
|
347
|
-
continue;
|
|
348
|
-
}
|
|
349
|
-
const domRoot = (next.rootPc + 7) % 12;
|
|
350
|
-
const targetsTonic = (next.rootPc - tonicPc + 12) % 12 === 0;
|
|
351
|
-
const alreadySecondary = cur.rootPc === domRoot && cur.quality === 'dom7';
|
|
352
|
-
// A secondary dominant inserted at i-1 resolves onto this chord; replacing
|
|
353
|
-
// it here would orphan that dominant.
|
|
354
|
-
const isResolutionTarget = chords[i - 1]?.secondaryDominant === true;
|
|
355
|
-
if (targetsTonic || alreadySecondary || isResolutionTarget) {
|
|
356
|
-
continue;
|
|
357
|
-
}
|
|
358
|
-
if (rrng() < 0.5) {
|
|
359
|
-
chords[i] = {
|
|
360
|
-
rootPc: domRoot,
|
|
361
|
-
quality: 'dom7',
|
|
362
|
-
startBeat: cur.startBeat,
|
|
363
|
-
secondaryDominant: true,
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
return chords;
|
|
370
|
-
}
|
|
@@ -1,330 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reharmonization: chord substitution, modal-interchange palettes, and negative
|
|
3
|
-
* harmony. These transforms take an existing chord (or a key) and propose
|
|
4
|
-
* alternative harmonies that preserve a chosen relationship — a shared function,
|
|
5
|
-
* a common tone, a tonicizing dominant, or a reflection across the key axis.
|
|
6
|
-
*
|
|
7
|
-
* Roots are pitch classes (0..11); no spelled key signature is required.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { detectChordBest } from '../../analyze/detect/index.js';
|
|
11
|
-
import {
|
|
12
|
-
type BorrowedSource,
|
|
13
|
-
borrowedSource,
|
|
14
|
-
chordToRoman,
|
|
15
|
-
functionOf,
|
|
16
|
-
type HarmonicFunction,
|
|
17
|
-
isDiatonic,
|
|
18
|
-
parallelKey,
|
|
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
|
-
|
|
30
|
-
/** Reduce a value to a pitch class in [0, 11]. */
|
|
31
|
-
function mod12(n: number): number {
|
|
32
|
-
return ((n % 12) + 12) % 12;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* The kind of substitution relationship a candidate realizes.
|
|
37
|
-
*
|
|
38
|
-
* @category Reharmonization
|
|
39
|
-
*/
|
|
40
|
-
export type SubstitutionType =
|
|
41
|
-
| 'tritone'
|
|
42
|
-
| 'relative'
|
|
43
|
-
| 'borrowed'
|
|
44
|
-
| 'chromatic-mediant'
|
|
45
|
-
| 'secondary-dominant';
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* A proposed chord substitution with its relationship, numeral, and function.
|
|
49
|
-
*
|
|
50
|
-
* @category Reharmonization
|
|
51
|
-
*/
|
|
52
|
-
export type Substitution = {
|
|
53
|
-
chord: Chord;
|
|
54
|
-
type: SubstitutionType;
|
|
55
|
-
roman: string;
|
|
56
|
-
function: HarmonicFunction;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Options controlling which substitutions {@link substituteChord} returns.
|
|
61
|
-
*
|
|
62
|
-
* @category Reharmonization
|
|
63
|
-
*/
|
|
64
|
-
export type SubstituteOptions = {
|
|
65
|
-
/**
|
|
66
|
-
* Melody pitch classes that must remain chord tones. When given, only
|
|
67
|
-
* substitutions whose pitch classes contain every listed pitch class are
|
|
68
|
-
* kept, so the melody stays consonant against the new harmony.
|
|
69
|
-
*/
|
|
70
|
-
melodyPcs?: number[];
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
/** Whether the chord's template carries a given interval (mod 12). */
|
|
74
|
-
function hasInterval(chord: Chord, semitone: number): boolean {
|
|
75
|
-
return chord.intervals.some((iv) => mod12(iv) === semitone);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Whether a chord is a dominant type: a major third with a minor seventh. */
|
|
79
|
-
function isDominantType(chord: Chord): boolean {
|
|
80
|
-
return hasInterval(chord, 4) && hasInterval(chord, 10);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* A chord that can be tonicized: it has a perfect fifth and a third but is not
|
|
85
|
-
* itself a dominant seventh (which is an unstable, tonicizing sonority rather
|
|
86
|
-
* than a stable target).
|
|
87
|
-
*/
|
|
88
|
-
function isStableTarget(chord: Chord): boolean {
|
|
89
|
-
const hasFifth = hasInterval(chord, 7);
|
|
90
|
-
const hasThird = hasInterval(chord, 3) || hasInterval(chord, 4);
|
|
91
|
-
return hasFifth && hasThird && !isDominantType(chord);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** The triad pitch classes (root, third, fifth) implied by a chord's template. */
|
|
95
|
-
function triadPitchClasses(chord: Chord): number[] {
|
|
96
|
-
const third = hasInterval(chord, 4) ? 4 : hasInterval(chord, 3) ? 3 : 4;
|
|
97
|
-
const fifth = hasInterval(chord, 8) ? 8 : hasInterval(chord, 6) ? 6 : 7;
|
|
98
|
-
return [mod12(chord.rootPc), mod12(chord.rootPc + third), mod12(chord.rootPc + fifth)];
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** Count the pitch classes shared between two pitch-class sets. */
|
|
102
|
-
function commonToneCount(a: number[], b: number[]): number {
|
|
103
|
-
const set = new Set(b);
|
|
104
|
-
return a.filter((pc) => set.has(pc)).length;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** The diatonic triads of a key, one per scale degree. */
|
|
108
|
-
function diatonicTriadsOf(key: KeyScale): Chord[] {
|
|
109
|
-
const degrees = scaleTonesInDegreeOrder(key).length;
|
|
110
|
-
const triads: Chord[] = [];
|
|
111
|
-
for (let degree = 0; degree < degrees; degree += 1) {
|
|
112
|
-
triads.push(diatonicTriad(degree, key));
|
|
113
|
-
}
|
|
114
|
-
return triads;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** Whether two chords share the same root pitch class and quality. */
|
|
118
|
-
function sameChord(a: Chord, b: Chord): boolean {
|
|
119
|
-
return mod12(a.rootPc) === mod12(b.rootPc) && a.quality === b.quality;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** Root offsets (semitones) that form a third above or below a root. */
|
|
123
|
-
const THIRD_OFFSETS = [3, 4, 8, 9] as const;
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Propose substitutions for a chord within a key.
|
|
127
|
-
*
|
|
128
|
-
* Candidates are generated by several relationships and deduplicated against the
|
|
129
|
-
* original chord and each other:
|
|
130
|
-
*
|
|
131
|
-
* - `tritone`: for a dominant-type chord, the dominant seventh a tritone away.
|
|
132
|
-
* - `secondary-dominant`: for a stable target, the dominant seventh a fifth
|
|
133
|
-
* above its root (tonicizing it).
|
|
134
|
-
* - `relative`: a diatonic triad a third away that shares two common tones.
|
|
135
|
-
* - `borrowed`: a triad from the parallel mode with the same harmonic function.
|
|
136
|
-
* - `chromatic-mediant`: a major/minor triad a third away sharing one common
|
|
137
|
-
* tone.
|
|
138
|
-
*
|
|
139
|
-
* Each result carries its Roman numeral and harmonic function in `key`. When
|
|
140
|
-
* `opts.melodyPcs` is given, only substitutions whose pitch classes contain all
|
|
141
|
-
* of those pitch classes are returned.
|
|
142
|
-
*
|
|
143
|
-
* @param chord The chord to reharmonize.
|
|
144
|
-
* @param key The prevailing key.
|
|
145
|
-
* @param opts Optional melody-preservation constraint.
|
|
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
|
|
154
|
-
*/
|
|
155
|
-
export function substituteChord(
|
|
156
|
-
chord: Chord,
|
|
157
|
-
key: KeyScale,
|
|
158
|
-
opts?: SubstituteOptions,
|
|
159
|
-
): Substitution[] {
|
|
160
|
-
const original = triadPitchClasses(chord);
|
|
161
|
-
const candidates: { chord: Chord; type: SubstitutionType }[] = [];
|
|
162
|
-
|
|
163
|
-
// Tritone substitution: only for dominant-type chords.
|
|
164
|
-
if (isDominantType(chord)) {
|
|
165
|
-
candidates.push({ chord: makeChord(mod12(chord.rootPc + 6), 'dom7'), type: 'tritone' });
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Secondary dominant: the V7 tonicizing a stable target.
|
|
169
|
-
if (isStableTarget(chord)) {
|
|
170
|
-
candidates.push({
|
|
171
|
-
chord: makeChord(mod12(chord.rootPc + 7), 'dom7'),
|
|
172
|
-
type: 'secondary-dominant',
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Relative: a diatonic triad a third away sharing two common tones.
|
|
177
|
-
for (const triad of diatonicTriadsOf(key)) {
|
|
178
|
-
const offset = mod12(triad.rootPc - chord.rootPc);
|
|
179
|
-
if (
|
|
180
|
-
(THIRD_OFFSETS as readonly number[]).includes(offset) &&
|
|
181
|
-
commonToneCount(original, chordPitchClasses(triad)) === 2
|
|
182
|
-
) {
|
|
183
|
-
candidates.push({ chord: triad, type: 'relative' });
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Borrowed: a parallel-mode triad sharing the original's harmonic function.
|
|
188
|
-
const targetFunction = functionOf(chord, key);
|
|
189
|
-
for (const triad of diatonicTriadsOf(parallelKey(key))) {
|
|
190
|
-
if (functionOf(triad, key) === targetFunction && !isDiatonic(triad, key)) {
|
|
191
|
-
candidates.push({ chord: triad, type: 'borrowed' });
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Chromatic mediants: major/minor triads a third away sharing one common tone.
|
|
196
|
-
for (const offset of THIRD_OFFSETS) {
|
|
197
|
-
for (const quality of ['maj', 'min'] as const) {
|
|
198
|
-
const mediant = makeChord(mod12(chord.rootPc + offset), quality);
|
|
199
|
-
if (
|
|
200
|
-
commonToneCount(original, chordPitchClasses(mediant)) === 1 &&
|
|
201
|
-
!isDiatonic(mediant, key)
|
|
202
|
-
) {
|
|
203
|
-
candidates.push({ chord: mediant, type: 'chromatic-mediant' });
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const melodyPcs = opts?.melodyPcs?.map(mod12);
|
|
209
|
-
const seen = new Set<string>();
|
|
210
|
-
const results: Substitution[] = [];
|
|
211
|
-
for (const candidate of candidates) {
|
|
212
|
-
if (sameChord(candidate.chord, chord)) {
|
|
213
|
-
continue;
|
|
214
|
-
}
|
|
215
|
-
const dedupeKey = `${mod12(candidate.chord.rootPc)}:${candidate.chord.quality}`;
|
|
216
|
-
if (seen.has(dedupeKey)) {
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
seen.add(dedupeKey);
|
|
220
|
-
if (melodyPcs !== undefined) {
|
|
221
|
-
const pcs = new Set(chordPitchClasses(candidate.chord));
|
|
222
|
-
if (!melodyPcs.every((pc) => pcs.has(pc))) {
|
|
223
|
-
continue;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
results.push({
|
|
227
|
-
chord: candidate.chord,
|
|
228
|
-
type: candidate.type,
|
|
229
|
-
roman: chordToRoman(candidate.chord, key),
|
|
230
|
-
function: functionOf(candidate.chord, key),
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
return results;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* A borrowed chord in the modal-interchange palette of a key.
|
|
238
|
-
*
|
|
239
|
-
* @category Reharmonization
|
|
240
|
-
*/
|
|
241
|
-
export type BorrowedChord = {
|
|
242
|
-
chord: Chord;
|
|
243
|
-
roman: string;
|
|
244
|
-
source: BorrowedSource;
|
|
245
|
-
};
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* The modal-interchange palette of a key.
|
|
249
|
-
*
|
|
250
|
-
* Lists the diatonic triads of the {@link parallelKey} that are not diatonic to
|
|
251
|
-
* `key` (the classic borrowed chords such as `iv`, `bVI`, and `bVII` in a major
|
|
252
|
-
* key), plus the Neapolitan (a major triad on the flat second degree). Each
|
|
253
|
-
* carries its Roman numeral and {@link borrowedSource} relative to `key`.
|
|
254
|
-
*
|
|
255
|
-
* @param key The prevailing key.
|
|
256
|
-
* @returns The borrowed-chord palette.
|
|
257
|
-
* @category Reharmonization
|
|
258
|
-
*/
|
|
259
|
-
export function modalInterchangePalette(key: KeyScale): BorrowedChord[] {
|
|
260
|
-
const palette: BorrowedChord[] = [];
|
|
261
|
-
const seen = new Set<string>();
|
|
262
|
-
const add = (chord: Chord) => {
|
|
263
|
-
const dedupeKey = `${mod12(chord.rootPc)}:${chord.quality}`;
|
|
264
|
-
if (seen.has(dedupeKey)) {
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
seen.add(dedupeKey);
|
|
268
|
-
palette.push({
|
|
269
|
-
chord,
|
|
270
|
-
roman: chordToRoman(chord, key),
|
|
271
|
-
source: borrowedSource(chord, key),
|
|
272
|
-
});
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
for (const triad of diatonicTriadsOf(parallelKey(key))) {
|
|
276
|
-
if (!isDiatonic(triad, key)) {
|
|
277
|
-
add(triad);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
// The Neapolitan (major triad on b2) sits outside both parallel modes, so it
|
|
281
|
-
// is added explicitly — unless b2 is already diatonic (e.g. in a Phrygian key,
|
|
282
|
-
// where it is a native chord rather than a borrowing).
|
|
283
|
-
const neapolitan = makeChord(mod12(key.rootPc + 1), 'maj');
|
|
284
|
-
if (!isDiatonic(neapolitan, key)) {
|
|
285
|
-
add(neapolitan);
|
|
286
|
-
}
|
|
287
|
-
return palette;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Reflect a chord across the tonic–dominant axis of a key (negative harmony).
|
|
292
|
-
*
|
|
293
|
-
* Every pitch class `p` maps to `(2*T + 7 - p) mod 12`, where `T` is the key
|
|
294
|
-
* tonic — the reflection that swaps the tonic with the dominant. The mirrored
|
|
295
|
-
* pitch classes (including the bass, if set) are reassembled into a chord by
|
|
296
|
-
* {@link detectChordBest}; when no chord is recognized, the mirrored root and
|
|
297
|
-
* the original quality are used as a fallback.
|
|
298
|
-
*
|
|
299
|
-
* @param chord The chord to reflect.
|
|
300
|
-
* @param key The prevailing key.
|
|
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
|
|
309
|
-
*/
|
|
310
|
-
export function negativeHarmonyMirror(chord: Chord, key: KeyScale): Chord {
|
|
311
|
-
const tonic = mod12(key.rootPc);
|
|
312
|
-
const mirror = (p: number) => mod12(2 * tonic + 7 - p);
|
|
313
|
-
const pcs = chordPitchClasses(chord).map(mirror);
|
|
314
|
-
const bassPc = chord.bassPc !== undefined ? mirror(chord.bassPc) : undefined;
|
|
315
|
-
// Place the mirrored bass an octave below so it is recognized as the lowest
|
|
316
|
-
// note and preserved through detection.
|
|
317
|
-
const pitches = bassPc !== undefined ? [bassPc - 12, ...pcs] : pcs;
|
|
318
|
-
const detected = detectChordBest(pitches);
|
|
319
|
-
if (detected) {
|
|
320
|
-
// Without an input bass the mirrored tones are octave-less, so detection may
|
|
321
|
-
// read the lowest pitch class as a slash bass and report a spurious
|
|
322
|
-
// inversion. Restore the recognized chord to root position in that case.
|
|
323
|
-
if (bassPc === undefined && detected.bassPc !== undefined) {
|
|
324
|
-
return makeChord(detected.rootPc, detected.quality);
|
|
325
|
-
}
|
|
326
|
-
return detected;
|
|
327
|
-
}
|
|
328
|
-
const quality: ChordQuality = chord.quality;
|
|
329
|
-
return makeChord(mirror(chord.rootPc), quality, bassPc);
|
|
330
|
-
}
|