@libraz/libcantus 0.9.1 → 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 +15 -0
- 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 +3016 -2984
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -3982
- package/dist/index.d.ts +14 -3982
- package/dist/index.js +170 -6504
- 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 +27 -2
- 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 +4 -4
- 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/{functional/index.ts → analyze/functional/roman.ts} +9 -327
- package/src/analyze/index.ts +49 -0
- package/src/{timeline → analyze/timeline}/index.ts +7 -11
- package/src/{analysis → analyze/voice}/index.ts +8 -5
- package/src/core/index.ts +47 -0
- package/src/{bass → generate/bass}/index.ts +5 -5
- package/src/{countermelody → generate/countermelody}/index.ts +11 -11
- package/src/{drums → generate/drums}/index.ts +2 -2
- package/src/{drums → generate/drums}/rng.ts +2 -2
- package/src/{groove → generate/groove}/index.ts +3 -3
- package/src/{harmonize → generate/harmonize}/index.ts +13 -13
- package/src/generate/index.ts +59 -0
- package/src/{motif → generate/motif}/index.ts +7 -7
- package/src/{progression → generate/progression}/index.ts +11 -25
- package/src/{reharmony → generate/reharmony}/index.ts +11 -11
- package/src/{rhythm → generate/rhythm}/index.ts +2 -2
- package/src/index.ts +130 -117
- package/src/model/chord.ts +423 -0
- package/src/model/index.ts +5 -1096
- 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 +19 -1
- package/src/{counterpoint → theory/counterpoint}/index.ts +2 -2
- package/src/theory/index.ts +107 -0
- package/src/{safety → theory/safety}/index.ts +1 -1
- 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 +3 -3
- package/src/{symbol → theory/symbol}/index.ts +8 -1
- 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 -571
- package/src/scale/index.ts +0 -331
- package/src/voicing/index.ts +0 -623
- /package/src/{interval → core/interval}/index.ts +0 -0
- /package/src/{meter → core/meter}/index.ts +0 -0
- /package/src/{pitch → core/pitch}/index.ts +0 -0
- /package/src/{random → core/random}/index.ts +0 -0
- /package/src/{tuning → core/tuning}/index.ts +0 -0
- /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}/fills.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}/internal.ts +0 -0
- /package/src/{drums → generate/drums}/kick.ts +0 -0
- /package/src/{drums → generate/drums}/percussion.ts +0 -0
- /package/src/{drums → generate/drums}/swing.ts +0 -0
- /package/src/{chordscale → theory/chordscale}/index.ts +0 -0
- /package/src/{harmony → theory/harmony}/index.ts +0 -0
|
@@ -0,0 +1,1677 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/theory/index.ts
|
|
21
|
+
var theory_exports = {};
|
|
22
|
+
__export(theory_exports, {
|
|
23
|
+
BLUES_MASK: () => BLUES_MASK,
|
|
24
|
+
CHROMATIC_MASK: () => CHROMATIC_MASK,
|
|
25
|
+
DORIAN_MASK: () => DORIAN_MASK,
|
|
26
|
+
HARMONIC_MINOR_MASK: () => HARMONIC_MINOR_MASK,
|
|
27
|
+
LOCRIAN_MASK: () => LOCRIAN_MASK,
|
|
28
|
+
LYDIAN_MASK: () => LYDIAN_MASK,
|
|
29
|
+
MAJOR_MASK: () => MAJOR_MASK,
|
|
30
|
+
MAJOR_PENTATONIC_MASK: () => MAJOR_PENTATONIC_MASK,
|
|
31
|
+
MELODIC_MINOR_MASK: () => MELODIC_MINOR_MASK,
|
|
32
|
+
MINOR_PENTATONIC_MASK: () => MINOR_PENTATONIC_MASK,
|
|
33
|
+
MIXOLYDIAN_MASK: () => MIXOLYDIAN_MASK,
|
|
34
|
+
NAMED_SCALES: () => NAMED_SCALES,
|
|
35
|
+
NATURAL_MINOR_MASK: () => NATURAL_MINOR_MASK,
|
|
36
|
+
NoteSafety: () => NoteSafety,
|
|
37
|
+
OCTATONIC_HALF_WHOLE_MASK: () => OCTATONIC_HALF_WHOLE_MASK,
|
|
38
|
+
OCTATONIC_WHOLE_HALF_MASK: () => OCTATONIC_WHOLE_HALF_MASK,
|
|
39
|
+
PHRYGIAN_MASK: () => PHRYGIAN_MASK,
|
|
40
|
+
ReasonFlag: () => ReasonFlag,
|
|
41
|
+
SATB_RANGES: () => SATB_RANGES,
|
|
42
|
+
WHOLE_TONE_MASK: () => WHOLE_TONE_MASK,
|
|
43
|
+
availableTensions: () => availableTensions,
|
|
44
|
+
avoidNotes: () => avoidNotes,
|
|
45
|
+
chordFromDegree: () => chordFromDegree,
|
|
46
|
+
chordPitchClasses: () => chordPitchClasses,
|
|
47
|
+
chordQualities: () => chordQualities,
|
|
48
|
+
chordScaleReport: () => chordScaleReport,
|
|
49
|
+
chordScales: () => chordScales,
|
|
50
|
+
chordToneRole: () => chordToneRole,
|
|
51
|
+
createsHiddenParallelPerfect: () => createsHiddenParallelPerfect,
|
|
52
|
+
createsParallelOctave: () => createsParallelOctave,
|
|
53
|
+
createsParallelPerfect: () => createsParallelPerfect,
|
|
54
|
+
createsParallelUnison: () => createsParallelUnison,
|
|
55
|
+
createsVerticalDissonance: () => createsVerticalDissonance,
|
|
56
|
+
createsVoiceCrossing: () => createsVoiceCrossing,
|
|
57
|
+
createsVoiceOverlap: () => createsVoiceOverlap,
|
|
58
|
+
diatonicPitchClasses: () => diatonicPitchClasses,
|
|
59
|
+
diatonicSeventh: () => diatonicSeventh,
|
|
60
|
+
diatonicTriad: () => diatonicTriad,
|
|
61
|
+
enumerateSafePitches: () => enumerateSafePitches,
|
|
62
|
+
evaluateSafety: () => evaluateSafety,
|
|
63
|
+
exceedsSpacing: () => exceedsSpacing,
|
|
64
|
+
formatChordSymbol: () => formatChordSymbol,
|
|
65
|
+
isForbiddenMelodicLeap: () => isForbiddenMelodicLeap,
|
|
66
|
+
isLeadingToneResolution: () => isLeadingToneResolution,
|
|
67
|
+
isScaleTone: () => isScaleTone,
|
|
68
|
+
majorKey: () => majorKey,
|
|
69
|
+
makeChord: () => makeChord,
|
|
70
|
+
maskFromOffsets: () => maskFromOffsets,
|
|
71
|
+
minorKey: () => minorKey,
|
|
72
|
+
nearestScaleTone: () => nearestScaleTone,
|
|
73
|
+
nextVoicing: () => nextVoicing,
|
|
74
|
+
noteNames: () => noteNames,
|
|
75
|
+
parseChordSymbol: () => parseChordSymbol,
|
|
76
|
+
pitchToScaleDegree: () => pitchToScaleDegree,
|
|
77
|
+
roleOf: () => roleOf,
|
|
78
|
+
scaleByName: () => scaleByName,
|
|
79
|
+
scaleMatchesChord: () => scaleMatchesChord,
|
|
80
|
+
scaleTonesInDegreeOrder: () => scaleTonesInDegreeOrder,
|
|
81
|
+
scalesForChanges: () => scalesForChanges,
|
|
82
|
+
spellChord: () => spellChord,
|
|
83
|
+
spellPitchClass: () => spellPitchClass,
|
|
84
|
+
spellPitchClasses: () => spellPitchClasses,
|
|
85
|
+
spellScale: () => spellScale,
|
|
86
|
+
transposeChordSymbol: () => transposeChordSymbol,
|
|
87
|
+
voiceChord: () => voiceChord,
|
|
88
|
+
voiceChordStyled: () => voiceChordStyled,
|
|
89
|
+
voiceLeadingCost: () => voiceLeadingCost,
|
|
90
|
+
voiceProgression: () => voiceProgression
|
|
91
|
+
});
|
|
92
|
+
module.exports = __toCommonJS(theory_exports);
|
|
93
|
+
|
|
94
|
+
// src/theory/scale/degrees.ts
|
|
95
|
+
function pitchClass(pitch) {
|
|
96
|
+
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
97
|
+
}
|
|
98
|
+
function scaleOffset(pitch, key) {
|
|
99
|
+
return (pitchClass(pitch) - pitchClass(key.rootPc) + 12) % 12;
|
|
100
|
+
}
|
|
101
|
+
function isScaleTone(pitch, key) {
|
|
102
|
+
return (key.modeMask12 >> scaleOffset(pitch, key) & 1) === 1;
|
|
103
|
+
}
|
|
104
|
+
function nearestScaleTone(pitch, key) {
|
|
105
|
+
const base = Math.round(pitch);
|
|
106
|
+
for (let distance = 0; distance < 12; distance += 1) {
|
|
107
|
+
const lower = base - distance;
|
|
108
|
+
if (isScaleTone(lower, key)) {
|
|
109
|
+
return lower;
|
|
110
|
+
}
|
|
111
|
+
const higher = base + distance;
|
|
112
|
+
if (isScaleTone(higher, key)) {
|
|
113
|
+
return higher;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return base;
|
|
117
|
+
}
|
|
118
|
+
function pitchToScaleDegree(pitch, key) {
|
|
119
|
+
const offset = scaleOffset(pitch, key);
|
|
120
|
+
if ((key.modeMask12 >> offset & 1) === 0) {
|
|
121
|
+
return -1;
|
|
122
|
+
}
|
|
123
|
+
let degree = 0;
|
|
124
|
+
for (let n = 0; n < offset; n += 1) {
|
|
125
|
+
if ((key.modeMask12 >> n & 1) === 1) {
|
|
126
|
+
degree += 1;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return degree;
|
|
130
|
+
}
|
|
131
|
+
function diatonicPitchClasses(key) {
|
|
132
|
+
const root = pitchClass(key.rootPc);
|
|
133
|
+
const pcs = [];
|
|
134
|
+
for (let n = 0; n < 12; n += 1) {
|
|
135
|
+
if ((key.modeMask12 >> n & 1) === 1) {
|
|
136
|
+
pcs.push((root + n) % 12);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return pcs.sort((a, b) => a - b);
|
|
140
|
+
}
|
|
141
|
+
function scaleTonesInDegreeOrder(key) {
|
|
142
|
+
const root = pitchClass(key.rootPc);
|
|
143
|
+
const pcs = [];
|
|
144
|
+
for (let n = 0; n < 12; n += 1) {
|
|
145
|
+
if ((key.modeMask12 >> n & 1) === 1) {
|
|
146
|
+
pcs.push((root + n) % 12);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return pcs;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/theory/scale/masks.ts
|
|
153
|
+
function maskFromOffsets(offsets) {
|
|
154
|
+
let mask = 1;
|
|
155
|
+
for (const offset of offsets) {
|
|
156
|
+
mask |= 1 << (offset % 12 + 12) % 12;
|
|
157
|
+
}
|
|
158
|
+
return mask;
|
|
159
|
+
}
|
|
160
|
+
var MAJOR_MASK = 2741;
|
|
161
|
+
var NATURAL_MINOR_MASK = 1453;
|
|
162
|
+
var HARMONIC_MINOR_MASK = maskFromOffsets([0, 2, 3, 5, 7, 8, 11]);
|
|
163
|
+
var MELODIC_MINOR_MASK = maskFromOffsets([0, 2, 3, 5, 7, 9, 11]);
|
|
164
|
+
var DORIAN_MASK = maskFromOffsets([0, 2, 3, 5, 7, 9, 10]);
|
|
165
|
+
var PHRYGIAN_MASK = maskFromOffsets([0, 1, 3, 5, 7, 8, 10]);
|
|
166
|
+
var LYDIAN_MASK = maskFromOffsets([0, 2, 4, 6, 7, 9, 11]);
|
|
167
|
+
var MIXOLYDIAN_MASK = maskFromOffsets([0, 2, 4, 5, 7, 9, 10]);
|
|
168
|
+
var LOCRIAN_MASK = maskFromOffsets([0, 1, 3, 5, 6, 8, 10]);
|
|
169
|
+
var MAJOR_PENTATONIC_MASK = maskFromOffsets([0, 2, 4, 7, 9]);
|
|
170
|
+
var MINOR_PENTATONIC_MASK = maskFromOffsets([0, 3, 5, 7, 10]);
|
|
171
|
+
var BLUES_MASK = maskFromOffsets([0, 3, 5, 6, 7, 10]);
|
|
172
|
+
var WHOLE_TONE_MASK = maskFromOffsets([0, 2, 4, 6, 8, 10]);
|
|
173
|
+
var OCTATONIC_HALF_WHOLE_MASK = maskFromOffsets([0, 1, 3, 4, 6, 7, 9, 10]);
|
|
174
|
+
var OCTATONIC_WHOLE_HALF_MASK = maskFromOffsets([0, 2, 3, 5, 6, 8, 9, 11]);
|
|
175
|
+
var CHROMATIC_MASK = 4095;
|
|
176
|
+
var NAMED_SCALES = {
|
|
177
|
+
major: MAJOR_MASK,
|
|
178
|
+
ionian: MAJOR_MASK,
|
|
179
|
+
naturalMinor: NATURAL_MINOR_MASK,
|
|
180
|
+
aeolian: NATURAL_MINOR_MASK,
|
|
181
|
+
harmonicMinor: HARMONIC_MINOR_MASK,
|
|
182
|
+
melodicMinor: MELODIC_MINOR_MASK,
|
|
183
|
+
dorian: DORIAN_MASK,
|
|
184
|
+
phrygian: PHRYGIAN_MASK,
|
|
185
|
+
lydian: LYDIAN_MASK,
|
|
186
|
+
mixolydian: MIXOLYDIAN_MASK,
|
|
187
|
+
locrian: LOCRIAN_MASK,
|
|
188
|
+
majorPentatonic: MAJOR_PENTATONIC_MASK,
|
|
189
|
+
minorPentatonic: MINOR_PENTATONIC_MASK,
|
|
190
|
+
blues: BLUES_MASK,
|
|
191
|
+
wholeTone: WHOLE_TONE_MASK,
|
|
192
|
+
octatonicHalfWhole: OCTATONIC_HALF_WHOLE_MASK,
|
|
193
|
+
octatonicWholeHalf: OCTATONIC_WHOLE_HALF_MASK,
|
|
194
|
+
chromatic: CHROMATIC_MASK
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// src/theory/scale/key.ts
|
|
198
|
+
function majorKey(rootPc) {
|
|
199
|
+
return { rootPc: (rootPc % 12 + 12) % 12, modeMask12: MAJOR_MASK };
|
|
200
|
+
}
|
|
201
|
+
function minorKey(rootPc) {
|
|
202
|
+
return { rootPc: (rootPc % 12 + 12) % 12, modeMask12: NATURAL_MINOR_MASK };
|
|
203
|
+
}
|
|
204
|
+
function scaleByName(name, rootPc) {
|
|
205
|
+
const mask = NAMED_SCALES[name];
|
|
206
|
+
if (mask === void 0) {
|
|
207
|
+
throw new Error(`Unknown scale: ${name}`);
|
|
208
|
+
}
|
|
209
|
+
return { rootPc: (rootPc % 12 + 12) % 12, modeMask12: mask };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/theory/chord/index.ts
|
|
213
|
+
var QUALITY_INTERVALS = {
|
|
214
|
+
maj: [0, 4, 7],
|
|
215
|
+
min: [0, 3, 7],
|
|
216
|
+
dim: [0, 3, 6],
|
|
217
|
+
aug: [0, 4, 8],
|
|
218
|
+
majb5: [0, 4, 6],
|
|
219
|
+
maj7: [0, 4, 7, 11],
|
|
220
|
+
min7: [0, 3, 7, 10],
|
|
221
|
+
dom7: [0, 4, 7, 10],
|
|
222
|
+
dim7: [0, 3, 6, 9],
|
|
223
|
+
m7b5: [0, 3, 6, 10],
|
|
224
|
+
minMaj7: [0, 3, 7, 11],
|
|
225
|
+
aug7: [0, 4, 8, 10],
|
|
226
|
+
augMaj7: [0, 4, 8, 11],
|
|
227
|
+
"6": [0, 4, 7, 9],
|
|
228
|
+
min6: [0, 3, 7, 9],
|
|
229
|
+
"6/9": [0, 4, 7, 9, 14],
|
|
230
|
+
sus2: [0, 2, 7],
|
|
231
|
+
sus4: [0, 5, 7],
|
|
232
|
+
add9: [0, 4, 7, 14],
|
|
233
|
+
add11: [0, 4, 7, 17],
|
|
234
|
+
maj9: [0, 4, 7, 11, 14],
|
|
235
|
+
min9: [0, 3, 7, 10, 14],
|
|
236
|
+
dom9: [0, 4, 7, 10, 14],
|
|
237
|
+
"7b9": [0, 4, 7, 10, 13],
|
|
238
|
+
"7#9": [0, 4, 7, 10, 15],
|
|
239
|
+
"7#11": [0, 4, 7, 10, 18],
|
|
240
|
+
"7b13": [0, 4, 7, 10, 20],
|
|
241
|
+
"11": [0, 7, 10, 14, 17],
|
|
242
|
+
"13": [0, 4, 7, 10, 14, 21],
|
|
243
|
+
"5": [0, 7]
|
|
244
|
+
};
|
|
245
|
+
function pitchClass2(value) {
|
|
246
|
+
return (Math.trunc(value) % 12 + 12) % 12;
|
|
247
|
+
}
|
|
248
|
+
function chordQualities() {
|
|
249
|
+
return Object.keys(QUALITY_INTERVALS);
|
|
250
|
+
}
|
|
251
|
+
function chordFromDegree(degree, ext, key) {
|
|
252
|
+
const tones = scaleTonesInDegreeOrder(key);
|
|
253
|
+
const length = tones.length;
|
|
254
|
+
const index = length > 0 ? (degree % length + length) % length : 0;
|
|
255
|
+
const rootPc = tones[index] ?? pitchClass2(key.rootPc);
|
|
256
|
+
return { rootPc, quality: ext, intervals: [...QUALITY_INTERVALS[ext]] };
|
|
257
|
+
}
|
|
258
|
+
function makeChord(rootPc, quality, bassPc) {
|
|
259
|
+
const chord = {
|
|
260
|
+
rootPc: pitchClass2(rootPc),
|
|
261
|
+
quality,
|
|
262
|
+
intervals: [...QUALITY_INTERVALS[quality]]
|
|
263
|
+
};
|
|
264
|
+
if (bassPc !== void 0) {
|
|
265
|
+
chord.bassPc = pitchClass2(bassPc);
|
|
266
|
+
}
|
|
267
|
+
return chord;
|
|
268
|
+
}
|
|
269
|
+
function chordPitchClasses(chord) {
|
|
270
|
+
const set = /* @__PURE__ */ new Set();
|
|
271
|
+
for (const interval of chord.intervals) {
|
|
272
|
+
set.add(pitchClass2(chord.rootPc + interval));
|
|
273
|
+
}
|
|
274
|
+
return [...set].sort((a, b) => a - b);
|
|
275
|
+
}
|
|
276
|
+
function chordToneOffsets(chord) {
|
|
277
|
+
return new Set(chord.intervals.map((i) => (i % 12 + 12) % 12));
|
|
278
|
+
}
|
|
279
|
+
function chordToneRole(pitch, chord) {
|
|
280
|
+
const interval = (pitchClass2(pitch) - pitchClass2(chord.rootPc) + 12) % 12;
|
|
281
|
+
const tones = chordToneOffsets(chord);
|
|
282
|
+
if (interval === 0) {
|
|
283
|
+
return "root";
|
|
284
|
+
}
|
|
285
|
+
if (interval === 3 || interval === 4) {
|
|
286
|
+
return tones.has(interval) ? "third" : null;
|
|
287
|
+
}
|
|
288
|
+
if (interval === 7) {
|
|
289
|
+
return tones.has(7) ? "fifth" : null;
|
|
290
|
+
}
|
|
291
|
+
if (interval === 6) {
|
|
292
|
+
return tones.has(6) && !tones.has(7) ? "fifth" : null;
|
|
293
|
+
}
|
|
294
|
+
if (interval === 8) {
|
|
295
|
+
return tones.has(8) && !tones.has(7) ? "fifth" : null;
|
|
296
|
+
}
|
|
297
|
+
if (interval === 9) {
|
|
298
|
+
const hasHigherSeventh = tones.has(10) || tones.has(11);
|
|
299
|
+
const isDiminishedSeventh = tones.has(3) && tones.has(6) && !hasHigherSeventh;
|
|
300
|
+
if (isDiminishedSeventh) {
|
|
301
|
+
return "seventh";
|
|
302
|
+
}
|
|
303
|
+
if (tones.has(9) && !hasHigherSeventh) {
|
|
304
|
+
return "sixth";
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
if (interval === 10 || interval === 11) {
|
|
309
|
+
return tones.has(interval) ? "seventh" : null;
|
|
310
|
+
}
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
function classifyTriad(thirdIc, fifthIc) {
|
|
314
|
+
if (thirdIc === 4 && fifthIc === 8) {
|
|
315
|
+
return "aug";
|
|
316
|
+
}
|
|
317
|
+
if (thirdIc === 4 && fifthIc === 6) {
|
|
318
|
+
return "majb5";
|
|
319
|
+
}
|
|
320
|
+
if (thirdIc === 3 && fifthIc === 6) {
|
|
321
|
+
return "dim";
|
|
322
|
+
}
|
|
323
|
+
if (thirdIc === 3) {
|
|
324
|
+
return "min";
|
|
325
|
+
}
|
|
326
|
+
return "maj";
|
|
327
|
+
}
|
|
328
|
+
function classifySeventh(thirdIc, fifthIc, seventhIc) {
|
|
329
|
+
if (thirdIc === 3 && fifthIc === 6 && seventhIc === 9) {
|
|
330
|
+
return "dim7";
|
|
331
|
+
}
|
|
332
|
+
if (thirdIc === 3 && fifthIc === 6 && seventhIc === 10) {
|
|
333
|
+
return "m7b5";
|
|
334
|
+
}
|
|
335
|
+
if (thirdIc === 3 && seventhIc === 11) {
|
|
336
|
+
return "minMaj7";
|
|
337
|
+
}
|
|
338
|
+
if (thirdIc === 3) {
|
|
339
|
+
return "min7";
|
|
340
|
+
}
|
|
341
|
+
if (fifthIc === 8 && seventhIc === 10) {
|
|
342
|
+
return "aug7";
|
|
343
|
+
}
|
|
344
|
+
if (fifthIc === 8 && seventhIc === 11) {
|
|
345
|
+
return "augMaj7";
|
|
346
|
+
}
|
|
347
|
+
if (seventhIc === 11) {
|
|
348
|
+
return "maj7";
|
|
349
|
+
}
|
|
350
|
+
return "dom7";
|
|
351
|
+
}
|
|
352
|
+
function stackThirds(degree, key, size) {
|
|
353
|
+
const tones = scaleTonesInDegreeOrder(key);
|
|
354
|
+
const length = tones.length;
|
|
355
|
+
if (length === 0) {
|
|
356
|
+
return { rootPc: pitchClass2(key.rootPc), quality: "maj", intervals: [0, 4, 7] };
|
|
357
|
+
}
|
|
358
|
+
const idx = (step) => ((degree + step) % length + length) % length;
|
|
359
|
+
const rootPc = tones[idx(0)] ?? pitchClass2(key.rootPc);
|
|
360
|
+
const offsets = [0, 2, 4, 6].slice(0, size).map((step) => {
|
|
361
|
+
const pc = tones[idx(step)] ?? rootPc;
|
|
362
|
+
return ((pc - rootPc) % 12 + 12) % 12;
|
|
363
|
+
});
|
|
364
|
+
const thirdIc = offsets[1] ?? 4;
|
|
365
|
+
const fifthIc = offsets[2] ?? 7;
|
|
366
|
+
const quality = size === 3 ? classifyTriad(thirdIc, fifthIc) : classifySeventh(thirdIc, fifthIc, offsets[3] ?? 11);
|
|
367
|
+
return { rootPc, quality, intervals: offsets };
|
|
368
|
+
}
|
|
369
|
+
function diatonicTriad(degree, key) {
|
|
370
|
+
return stackThirds(degree, key, 3);
|
|
371
|
+
}
|
|
372
|
+
function diatonicSeventh(degree, key) {
|
|
373
|
+
return stackThirds(degree, key, 4);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/theory/chordscale/index.ts
|
|
377
|
+
function pitchClass3(value) {
|
|
378
|
+
return (Math.trunc(value) % 12 + 12) % 12;
|
|
379
|
+
}
|
|
380
|
+
function popcount12(mask) {
|
|
381
|
+
let count = 0;
|
|
382
|
+
for (let n = 0; n < 12; n += 1) {
|
|
383
|
+
count += mask >> n & 1;
|
|
384
|
+
}
|
|
385
|
+
return count;
|
|
386
|
+
}
|
|
387
|
+
function maskHasPitchClass(mask, scaleRootPc, pc) {
|
|
388
|
+
const offset = pitchClass3(pc - scaleRootPc);
|
|
389
|
+
return (mask >> offset & 1) === 1;
|
|
390
|
+
}
|
|
391
|
+
function scaleMatchesChord(chordPcs, scaleMask, scaleRootPc) {
|
|
392
|
+
for (const pc of chordPcs) {
|
|
393
|
+
if (!maskHasPitchClass(scaleMask, scaleRootPc, pc)) {
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
var ALIASED_SCALE_NAMES = /* @__PURE__ */ new Set(["major", "naturalMinor"]);
|
|
400
|
+
var HEPTATONIC_SIZE = 7;
|
|
401
|
+
function chordScales(chord) {
|
|
402
|
+
const chordPcs = chordPitchClasses(chord);
|
|
403
|
+
const rootPc = pitchClass3(chord.rootPc);
|
|
404
|
+
const ranked = [];
|
|
405
|
+
const seenMasks = /* @__PURE__ */ new Set();
|
|
406
|
+
for (const name of Object.keys(NAMED_SCALES)) {
|
|
407
|
+
if (name === "chromatic" || ALIASED_SCALE_NAMES.has(name)) {
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const mask = NAMED_SCALES[name];
|
|
411
|
+
if (mask === void 0 || seenMasks.has(mask)) {
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
seenMasks.add(mask);
|
|
415
|
+
if (scaleMatchesChord(chordPcs, mask, rootPc)) {
|
|
416
|
+
const size = popcount12(mask);
|
|
417
|
+
ranked.push({ name, extra: size - chordPcs.length, size });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (ranked.length === 0) {
|
|
421
|
+
if (NAMED_SCALES.chromatic !== void 0) {
|
|
422
|
+
return [{ name: "chromatic", rootPc }];
|
|
423
|
+
}
|
|
424
|
+
return [];
|
|
425
|
+
}
|
|
426
|
+
const preferHeptatonic = chordPcs.length <= 3;
|
|
427
|
+
ranked.sort((a, b) => {
|
|
428
|
+
if (preferHeptatonic && a.size === HEPTATONIC_SIZE !== (b.size === HEPTATONIC_SIZE)) {
|
|
429
|
+
return a.size === HEPTATONIC_SIZE ? -1 : 1;
|
|
430
|
+
}
|
|
431
|
+
if (a.extra !== b.extra) {
|
|
432
|
+
return a.extra - b.extra;
|
|
433
|
+
}
|
|
434
|
+
if (a.size !== b.size) {
|
|
435
|
+
return a.size - b.size;
|
|
436
|
+
}
|
|
437
|
+
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
|
|
438
|
+
});
|
|
439
|
+
return ranked.map((entry) => ({ name: entry.name, rootPc }));
|
|
440
|
+
}
|
|
441
|
+
function avoidNotes(chord, scaleName) {
|
|
442
|
+
const mask = NAMED_SCALES[scaleName];
|
|
443
|
+
if (mask === void 0) {
|
|
444
|
+
return [];
|
|
445
|
+
}
|
|
446
|
+
const rootPc = pitchClass3(chord.rootPc);
|
|
447
|
+
const chordPcs = chordPitchClasses(chord);
|
|
448
|
+
if (!scaleMatchesChord(chordPcs, mask, rootPc)) {
|
|
449
|
+
return [];
|
|
450
|
+
}
|
|
451
|
+
const chordSet = new Set(chordPcs);
|
|
452
|
+
const avoid = [];
|
|
453
|
+
for (let pc = 0; pc < 12; pc += 1) {
|
|
454
|
+
if (!maskHasPitchClass(mask, rootPc, pc) || chordSet.has(pc)) {
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
if (chordSet.has(pitchClass3(pc - 1))) {
|
|
458
|
+
avoid.push(pc);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return avoid.sort((a, b) => a - b);
|
|
462
|
+
}
|
|
463
|
+
function availableTensions(chord, scaleName) {
|
|
464
|
+
const mask = NAMED_SCALES[scaleName];
|
|
465
|
+
if (mask === void 0) {
|
|
466
|
+
return [];
|
|
467
|
+
}
|
|
468
|
+
const rootPc = pitchClass3(chord.rootPc);
|
|
469
|
+
const chordPcs = chordPitchClasses(chord);
|
|
470
|
+
if (!scaleMatchesChord(chordPcs, mask, rootPc)) {
|
|
471
|
+
return [];
|
|
472
|
+
}
|
|
473
|
+
const chordSet = new Set(chordPcs);
|
|
474
|
+
const avoidSet = new Set(avoidNotes(chord, scaleName));
|
|
475
|
+
const tensions = [];
|
|
476
|
+
for (let pc = 0; pc < 12; pc += 1) {
|
|
477
|
+
if (!maskHasPitchClass(mask, rootPc, pc) || chordSet.has(pc) || avoidSet.has(pc)) {
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
tensions.push(pc);
|
|
481
|
+
}
|
|
482
|
+
return tensions.sort((a, b) => a - b);
|
|
483
|
+
}
|
|
484
|
+
function chordScaleReport(chord, limit) {
|
|
485
|
+
const matches = chordScales(chord);
|
|
486
|
+
const chosen = limit === void 0 ? matches : matches.slice(0, Math.max(0, limit));
|
|
487
|
+
return chosen.map((match) => ({
|
|
488
|
+
name: match.name,
|
|
489
|
+
rootPc: match.rootPc,
|
|
490
|
+
avoid: avoidNotes(chord, match.name),
|
|
491
|
+
tensions: availableTensions(chord, match.name)
|
|
492
|
+
}));
|
|
493
|
+
}
|
|
494
|
+
var RANK_PENALTY = 0.01;
|
|
495
|
+
function scalePitchClassSet(name, rootPc) {
|
|
496
|
+
const mask = NAMED_SCALES[name];
|
|
497
|
+
const pcs = /* @__PURE__ */ new Set();
|
|
498
|
+
if (mask === void 0) {
|
|
499
|
+
return pcs;
|
|
500
|
+
}
|
|
501
|
+
for (let n = 0; n < 12; n += 1) {
|
|
502
|
+
if ((mask >> n & 1) === 1) {
|
|
503
|
+
pcs.add(pitchClass3(rootPc + n));
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return pcs;
|
|
507
|
+
}
|
|
508
|
+
function symmetricDifferenceSize(a, b) {
|
|
509
|
+
let count = 0;
|
|
510
|
+
for (const pc of a) {
|
|
511
|
+
if (!b.has(pc)) {
|
|
512
|
+
count += 1;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
for (const pc of b) {
|
|
516
|
+
if (!a.has(pc)) {
|
|
517
|
+
count += 1;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return count;
|
|
521
|
+
}
|
|
522
|
+
function scalesForChanges(chords) {
|
|
523
|
+
if (chords.length === 0) {
|
|
524
|
+
return [];
|
|
525
|
+
}
|
|
526
|
+
const candidateLists = chords.map((chord) => chordScales(chord));
|
|
527
|
+
const pcSets = candidateLists.map(
|
|
528
|
+
(candidates) => candidates.map((match) => scalePitchClassSet(match.name, match.rootPc))
|
|
529
|
+
);
|
|
530
|
+
const first = candidateLists[0] ?? [];
|
|
531
|
+
const dp = [first.map((_, j) => j * RANK_PENALTY)];
|
|
532
|
+
const back = [first.map(() => -1)];
|
|
533
|
+
for (let i = 1; i < candidateLists.length; i += 1) {
|
|
534
|
+
const candidates = candidateLists[i] ?? [];
|
|
535
|
+
const prevCosts = dp[i - 1] ?? [];
|
|
536
|
+
const prevSets = pcSets[i - 1] ?? [];
|
|
537
|
+
const curSets = pcSets[i] ?? [];
|
|
538
|
+
const stageCosts = [];
|
|
539
|
+
const stageBack = [];
|
|
540
|
+
for (let j = 0; j < candidates.length; j += 1) {
|
|
541
|
+
let bestCost = Number.POSITIVE_INFINITY;
|
|
542
|
+
let bestPrev = -1;
|
|
543
|
+
for (let k = 0; k < prevCosts.length; k += 1) {
|
|
544
|
+
const transition = symmetricDifferenceSize(
|
|
545
|
+
prevSets[k] ?? /* @__PURE__ */ new Set(),
|
|
546
|
+
curSets[j] ?? /* @__PURE__ */ new Set()
|
|
547
|
+
);
|
|
548
|
+
const cost = (prevCosts[k] ?? 0) + transition;
|
|
549
|
+
if (cost < bestCost) {
|
|
550
|
+
bestCost = cost;
|
|
551
|
+
bestPrev = k;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
stageCosts.push(bestCost + j * RANK_PENALTY);
|
|
555
|
+
stageBack.push(bestPrev);
|
|
556
|
+
}
|
|
557
|
+
dp.push(stageCosts);
|
|
558
|
+
back.push(stageBack);
|
|
559
|
+
}
|
|
560
|
+
const lastCosts = dp[dp.length - 1] ?? [];
|
|
561
|
+
let bestFinal = 0;
|
|
562
|
+
for (let j = 1; j < lastCosts.length; j += 1) {
|
|
563
|
+
if ((lastCosts[j] ?? Number.POSITIVE_INFINITY) < (lastCosts[bestFinal] ?? 0)) {
|
|
564
|
+
bestFinal = j;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
const chosenIndices = new Array(candidateLists.length);
|
|
568
|
+
let current = bestFinal;
|
|
569
|
+
for (let i = candidateLists.length - 1; i >= 0; i -= 1) {
|
|
570
|
+
chosenIndices[i] = current;
|
|
571
|
+
current = back[i]?.[current] ?? -1;
|
|
572
|
+
}
|
|
573
|
+
return chords.map((chord, i) => {
|
|
574
|
+
const candidates = candidateLists[i] ?? [];
|
|
575
|
+
const scale = candidates[chosenIndices[i] ?? 0] ?? { name: "chromatic", rootPc: chord.rootPc };
|
|
576
|
+
return { chord, scale };
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// src/core/interval/index.ts
|
|
581
|
+
function simpleInterval(semitones) {
|
|
582
|
+
return Math.abs(semitones) % 12;
|
|
583
|
+
}
|
|
584
|
+
function isConsonantInterval(semitones, twoVoice = true) {
|
|
585
|
+
const pc = simpleInterval(semitones);
|
|
586
|
+
if (pc === 5) {
|
|
587
|
+
return !twoVoice;
|
|
588
|
+
}
|
|
589
|
+
return pc === 0 || pc === 3 || pc === 4 || pc === 7 || pc === 8 || pc === 9;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// src/theory/counterpoint/index.ts
|
|
593
|
+
function pitchClass4(pitch) {
|
|
594
|
+
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
595
|
+
}
|
|
596
|
+
function createsVoiceCrossing(upperPitch, lowerPitch) {
|
|
597
|
+
return upperPitch < lowerPitch;
|
|
598
|
+
}
|
|
599
|
+
function createsVerticalDissonance(a, b, twoVoice) {
|
|
600
|
+
return !isConsonantInterval(a - b, twoVoice);
|
|
601
|
+
}
|
|
602
|
+
function isForbiddenMelodicLeap(prev, cur) {
|
|
603
|
+
const semis = Math.abs(cur - prev);
|
|
604
|
+
return semis === 6 || semis === 10 || semis === 11 || semis > 12;
|
|
605
|
+
}
|
|
606
|
+
function simpleClass(semitones) {
|
|
607
|
+
return Math.abs(semitones) % 12;
|
|
608
|
+
}
|
|
609
|
+
function isPerfectClass(cls) {
|
|
610
|
+
return cls === 0 || cls === 7;
|
|
611
|
+
}
|
|
612
|
+
function similarMotion(aMove, bMove) {
|
|
613
|
+
return aMove !== 0 && bMove !== 0 && aMove > 0 === bMove > 0;
|
|
614
|
+
}
|
|
615
|
+
function bothVoicesMove(aMove, bMove) {
|
|
616
|
+
return aMove !== 0 && bMove !== 0;
|
|
617
|
+
}
|
|
618
|
+
function createsParallelPerfect(aPrev, aCur, bPrev, bCur) {
|
|
619
|
+
if (!bothVoicesMove(aCur - aPrev, bCur - bPrev)) {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
const nowClass = simpleClass(aCur - bCur);
|
|
623
|
+
const prevClass = simpleClass(aPrev - bPrev);
|
|
624
|
+
return isPerfectClass(nowClass) && nowClass === prevClass;
|
|
625
|
+
}
|
|
626
|
+
function createsParallelOctave(aPrev, aCur, bPrev, bCur) {
|
|
627
|
+
if (!similarMotion(aCur - aPrev, bCur - bPrev)) {
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
return simpleClass(aCur - bCur) === 0 && simpleClass(aPrev - bPrev) === 0;
|
|
631
|
+
}
|
|
632
|
+
function createsParallelUnison(aPrev, aCur, bPrev, bCur) {
|
|
633
|
+
if (aCur === aPrev || bCur === bPrev) {
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
return aCur === bCur && aPrev === bPrev;
|
|
637
|
+
}
|
|
638
|
+
function createsHiddenParallelPerfect(aPrev, aCur, bPrev, bCur) {
|
|
639
|
+
const aMove = aCur - aPrev;
|
|
640
|
+
const bMove = bCur - bPrev;
|
|
641
|
+
if (!similarMotion(aMove, bMove)) {
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
644
|
+
const nowClass = simpleClass(aCur - bCur);
|
|
645
|
+
const prevClass = simpleClass(aPrev - bPrev);
|
|
646
|
+
if (!isPerfectClass(nowClass) || prevClass === nowClass) {
|
|
647
|
+
return false;
|
|
648
|
+
}
|
|
649
|
+
const upperMove = aCur >= bCur ? aMove : bMove;
|
|
650
|
+
if (Math.abs(upperMove) <= 2) {
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
return true;
|
|
654
|
+
}
|
|
655
|
+
function createsVoiceOverlap(upperPrev, upperCur, lowerPrev, lowerCur) {
|
|
656
|
+
return upperCur < lowerPrev || lowerCur > upperPrev;
|
|
657
|
+
}
|
|
658
|
+
function exceedsSpacing(upperPitch, lowerPitch, maxSemitones = 12) {
|
|
659
|
+
return Math.abs(upperPitch - lowerPitch) > maxSemitones;
|
|
660
|
+
}
|
|
661
|
+
function isLeadingToneResolution(prev, cur, key) {
|
|
662
|
+
const tonic = pitchClass4(key.rootPc);
|
|
663
|
+
const leading = (tonic + 11) % 12;
|
|
664
|
+
if (pitchClass4(prev) !== leading || pitchClass4(cur) !== tonic) {
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
return cur > prev && cur - prev <= 2;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// src/theory/harmony/index.ts
|
|
671
|
+
function roleOf(pitch, chord, chordId = 0) {
|
|
672
|
+
const interval = ((Math.trunc(pitch) - chord.rootPc) % 12 + 12) % 12;
|
|
673
|
+
const tones = new Set(chord.intervals.map((i) => (i % 12 + 12) % 12));
|
|
674
|
+
const hasHigherSeventh = tones.has(10) || tones.has(11);
|
|
675
|
+
const hasThird = tones.has(3) || tones.has(4);
|
|
676
|
+
const isSuspendedTone = !hasThird && (interval === 5 && tones.has(5) || interval === 2 && tones.has(2));
|
|
677
|
+
let role;
|
|
678
|
+
let lock;
|
|
679
|
+
if (interval === 0) {
|
|
680
|
+
role = "root";
|
|
681
|
+
lock = "identity";
|
|
682
|
+
} else if (isSuspendedTone) {
|
|
683
|
+
role = "third";
|
|
684
|
+
lock = "quality";
|
|
685
|
+
} else if (interval === 3 || interval === 4) {
|
|
686
|
+
role = "third";
|
|
687
|
+
lock = "quality";
|
|
688
|
+
} else if (interval === 6 || interval === 7 || interval === 8) {
|
|
689
|
+
role = "fifth";
|
|
690
|
+
lock = "voicing";
|
|
691
|
+
} else if (interval === 9 && tones.has(3) && tones.has(6) && !hasHigherSeventh) {
|
|
692
|
+
role = "seventh";
|
|
693
|
+
lock = "voicing";
|
|
694
|
+
} else if (interval === 9 && tones.has(9) && !hasHigherSeventh) {
|
|
695
|
+
role = "sixth";
|
|
696
|
+
lock = "voicing";
|
|
697
|
+
} else if (interval === 10 || interval === 11) {
|
|
698
|
+
role = "seventh";
|
|
699
|
+
lock = "voicing";
|
|
700
|
+
} else {
|
|
701
|
+
role = "tension";
|
|
702
|
+
lock = "voicing";
|
|
703
|
+
}
|
|
704
|
+
return { role, lock, belongsToChordId: chordId };
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/theory/safety/index.ts
|
|
708
|
+
var NoteSafety = /* @__PURE__ */ ((NoteSafety2) => {
|
|
709
|
+
NoteSafety2[NoteSafety2["Safe"] = 0] = "Safe";
|
|
710
|
+
NoteSafety2[NoteSafety2["Warning"] = 1] = "Warning";
|
|
711
|
+
NoteSafety2[NoteSafety2["Dissonant"] = 2] = "Dissonant";
|
|
712
|
+
return NoteSafety2;
|
|
713
|
+
})(NoteSafety || {});
|
|
714
|
+
var ReasonFlag = /* @__PURE__ */ ((ReasonFlag2) => {
|
|
715
|
+
ReasonFlag2[ReasonFlag2["ChordTone"] = 1] = "ChordTone";
|
|
716
|
+
ReasonFlag2[ReasonFlag2["Tension"] = 2] = "Tension";
|
|
717
|
+
ReasonFlag2[ReasonFlag2["AvoidNote"] = 4] = "AvoidNote";
|
|
718
|
+
ReasonFlag2[ReasonFlag2["ScaleTone"] = 8] = "ScaleTone";
|
|
719
|
+
ReasonFlag2[ReasonFlag2["NonScale"] = 16] = "NonScale";
|
|
720
|
+
ReasonFlag2[ReasonFlag2["OutOfRange"] = 32] = "OutOfRange";
|
|
721
|
+
ReasonFlag2[ReasonFlag2["Tritone"] = 64] = "Tritone";
|
|
722
|
+
ReasonFlag2[ReasonFlag2["LargeLeap"] = 128] = "LargeLeap";
|
|
723
|
+
ReasonFlag2[ReasonFlag2["MinorSecond"] = 256] = "MinorSecond";
|
|
724
|
+
ReasonFlag2[ReasonFlag2["MajorSeventh"] = 512] = "MajorSeventh";
|
|
725
|
+
ReasonFlag2[ReasonFlag2["VerticalDissonance"] = 1024] = "VerticalDissonance";
|
|
726
|
+
ReasonFlag2[ReasonFlag2["ParallelPerfect"] = 2048] = "ParallelPerfect";
|
|
727
|
+
ReasonFlag2[ReasonFlag2["HiddenParallel"] = 4096] = "HiddenParallel";
|
|
728
|
+
ReasonFlag2[ReasonFlag2["VoiceCrossing"] = 8192] = "VoiceCrossing";
|
|
729
|
+
ReasonFlag2[ReasonFlag2["Suspension"] = 16384] = "Suspension";
|
|
730
|
+
ReasonFlag2[ReasonFlag2["NeedsResolution"] = 32768] = "NeedsResolution";
|
|
731
|
+
return ReasonFlag2;
|
|
732
|
+
})(ReasonFlag || {});
|
|
733
|
+
var SUGGESTION_WINDOW = 12;
|
|
734
|
+
var MAX_SUGGESTIONS = 3;
|
|
735
|
+
function pitchClass5(pitch) {
|
|
736
|
+
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
737
|
+
}
|
|
738
|
+
function intervalAboveRoot(pitch, chord) {
|
|
739
|
+
return ((pitchClass5(pitch) - pitchClass5(chord.rootPc)) % 12 + 12) % 12;
|
|
740
|
+
}
|
|
741
|
+
function isChordMember(pitch, chord) {
|
|
742
|
+
return chordPitchClasses(chord).includes(pitchClass5(pitch));
|
|
743
|
+
}
|
|
744
|
+
function avoidsFourth(chord) {
|
|
745
|
+
return chord.intervals.includes(4) && !chord.intervals.includes(5);
|
|
746
|
+
}
|
|
747
|
+
function avoidsMajorSeventh(chord) {
|
|
748
|
+
return chord.intervals.includes(4) && chord.intervals.includes(10);
|
|
749
|
+
}
|
|
750
|
+
function stepResolution(pitch, chord) {
|
|
751
|
+
for (let delta = 1; delta <= 2; delta += 1) {
|
|
752
|
+
if (isChordMember(pitch - delta, chord)) {
|
|
753
|
+
return pitch - delta;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return void 0;
|
|
757
|
+
}
|
|
758
|
+
function evaluateSafety(q) {
|
|
759
|
+
return evaluateInternal(q, true);
|
|
760
|
+
}
|
|
761
|
+
function evaluateInternal(q, collectSuggestions) {
|
|
762
|
+
let reasons = 0;
|
|
763
|
+
let level = 0 /* Safe */;
|
|
764
|
+
let resolveTo;
|
|
765
|
+
const raise = (l) => {
|
|
766
|
+
if (l > level) {
|
|
767
|
+
level = l;
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
const avoidSeverity = q.profile === "strict" ? 2 /* Dissonant */ : 1 /* Warning */;
|
|
771
|
+
const parallelSeverity = avoidSeverity;
|
|
772
|
+
const pitch = q.candidatePitch;
|
|
773
|
+
if (q.vocalLow !== void 0 && pitch < q.vocalLow || q.vocalHigh !== void 0 && pitch > q.vocalHigh) {
|
|
774
|
+
reasons |= 32 /* OutOfRange */;
|
|
775
|
+
raise(1 /* Warning */);
|
|
776
|
+
}
|
|
777
|
+
const inScale = isScaleTone(pitch, q.key);
|
|
778
|
+
const chord = q.chord;
|
|
779
|
+
if (chord && isChordMember(pitch, chord)) {
|
|
780
|
+
reasons |= 1 /* ChordTone */;
|
|
781
|
+
} else if (chord) {
|
|
782
|
+
const ic = intervalAboveRoot(pitch, chord);
|
|
783
|
+
const isTension = ic === 2 || ic === 5 || ic === 9;
|
|
784
|
+
const avoid = ic === 5 && avoidsFourth(chord) || ic === 11 && avoidsMajorSeventh(chord);
|
|
785
|
+
if (avoid) {
|
|
786
|
+
reasons |= 4 /* AvoidNote */;
|
|
787
|
+
if (isTension) {
|
|
788
|
+
reasons |= 2 /* Tension */;
|
|
789
|
+
}
|
|
790
|
+
reasons |= 32768 /* NeedsResolution */;
|
|
791
|
+
resolveTo = stepResolution(pitch, chord);
|
|
792
|
+
raise(avoidSeverity);
|
|
793
|
+
} else if (isTension && inScale) {
|
|
794
|
+
reasons |= 2 /* Tension */;
|
|
795
|
+
raise(1 /* Warning */);
|
|
796
|
+
} else if (inScale) {
|
|
797
|
+
reasons |= 8 /* ScaleTone */;
|
|
798
|
+
raise(1 /* Warning */);
|
|
799
|
+
} else {
|
|
800
|
+
reasons |= 16 /* NonScale */;
|
|
801
|
+
raise(2 /* Dissonant */);
|
|
802
|
+
}
|
|
803
|
+
} else if (inScale) {
|
|
804
|
+
reasons |= 8 /* ScaleTone */;
|
|
805
|
+
raise(1 /* Warning */);
|
|
806
|
+
} else {
|
|
807
|
+
reasons |= 16 /* NonScale */;
|
|
808
|
+
raise(2 /* Dissonant */);
|
|
809
|
+
}
|
|
810
|
+
if (chord && !(chord.intervals.includes(4) && chord.intervals.includes(10))) {
|
|
811
|
+
const ic = intervalAboveRoot(pitch, chord);
|
|
812
|
+
const toneOffsets = chord.intervals.map((i) => (i % 12 + 12) % 12);
|
|
813
|
+
if (toneOffsets.some((t) => Math.abs(ic - t) === 6)) {
|
|
814
|
+
reasons |= 64 /* Tritone */;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (q.prevPitch !== void 0) {
|
|
818
|
+
const pc = Math.abs(pitch - q.prevPitch) % 12;
|
|
819
|
+
if (pc === 6) {
|
|
820
|
+
reasons |= 64 /* Tritone */;
|
|
821
|
+
}
|
|
822
|
+
if (isForbiddenMelodicLeap(q.prevPitch, pitch)) {
|
|
823
|
+
reasons |= 128 /* LargeLeap */;
|
|
824
|
+
}
|
|
825
|
+
if (pc === 1) {
|
|
826
|
+
reasons |= 256 /* MinorSecond */;
|
|
827
|
+
}
|
|
828
|
+
if (pc === 11) {
|
|
829
|
+
reasons |= 512 /* MajorSeventh */;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (q.strongBeat) {
|
|
833
|
+
const twoVoice = q.otherVoices.length === 1;
|
|
834
|
+
const prev = q.prevPitch;
|
|
835
|
+
for (const ov of q.otherVoices) {
|
|
836
|
+
if (!createsVerticalDissonance(pitch, ov.pitch, twoVoice)) {
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
reasons |= 1024 /* VerticalDissonance */;
|
|
840
|
+
raise(2 /* Dissonant */);
|
|
841
|
+
if (prev !== void 0 && pitch === prev && ov.prevPitch !== void 0 && !createsVerticalDissonance(prev, ov.prevPitch, twoVoice)) {
|
|
842
|
+
reasons |= 16384 /* Suspension */;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
if (q.prevPitch !== void 0) {
|
|
847
|
+
const prev = q.prevPitch;
|
|
848
|
+
for (const ov of q.otherVoices) {
|
|
849
|
+
if (ov.prevPitch === void 0) {
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
if (createsParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {
|
|
853
|
+
reasons |= 2048 /* ParallelPerfect */;
|
|
854
|
+
raise(parallelSeverity);
|
|
855
|
+
}
|
|
856
|
+
if (createsHiddenParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {
|
|
857
|
+
reasons |= 4096 /* HiddenParallel */;
|
|
858
|
+
raise(parallelSeverity);
|
|
859
|
+
}
|
|
860
|
+
const crossedNow = pitch - ov.pitch;
|
|
861
|
+
const crossedPrev = prev - ov.prevPitch;
|
|
862
|
+
if (crossedNow !== 0 && crossedPrev !== 0 && crossedNow > 0 !== crossedPrev > 0) {
|
|
863
|
+
reasons |= 8192 /* VoiceCrossing */;
|
|
864
|
+
raise(parallelSeverity);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
const result = { safety: level, reasons, rationale: describe(reasons, q) };
|
|
869
|
+
if (resolveTo !== void 0) {
|
|
870
|
+
result.resolveTo = resolveTo;
|
|
871
|
+
}
|
|
872
|
+
if (collectSuggestions && level !== 0 /* Safe */) {
|
|
873
|
+
const suggestions = findSafeNearby(q, pitch);
|
|
874
|
+
if (suggestions.length > 0) {
|
|
875
|
+
result.suggestions = suggestions;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
return result;
|
|
879
|
+
}
|
|
880
|
+
function findSafeNearby(q, candidate) {
|
|
881
|
+
const out = [];
|
|
882
|
+
for (let d = 1; d <= SUGGESTION_WINDOW; d += 1) {
|
|
883
|
+
for (const p of [candidate - d, candidate + d]) {
|
|
884
|
+
if (evaluateInternal({ ...q, candidatePitch: p }, false).safety === 0 /* Safe */) {
|
|
885
|
+
out.push(p);
|
|
886
|
+
if (out.length >= MAX_SUGGESTIONS) {
|
|
887
|
+
return out;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return out;
|
|
893
|
+
}
|
|
894
|
+
function describe(reasons, q) {
|
|
895
|
+
if (reasons & 32 /* OutOfRange */) {
|
|
896
|
+
return "Outside the target vocal range";
|
|
897
|
+
}
|
|
898
|
+
if (reasons & 16384 /* Suspension */) {
|
|
899
|
+
return "Prepared suspension \u2014 a held consonance now dissonant, awaiting resolution";
|
|
900
|
+
}
|
|
901
|
+
if (reasons & 1024 /* VerticalDissonance */) {
|
|
902
|
+
return "Dissonant against a sounding voice on a strong beat";
|
|
903
|
+
}
|
|
904
|
+
if (reasons & 4 /* AvoidNote */) {
|
|
905
|
+
return "Avoid note \u2014 clashes with a chord tone a semitone away";
|
|
906
|
+
}
|
|
907
|
+
if (reasons & 2048 /* ParallelPerfect */) {
|
|
908
|
+
return "Parallel perfect interval with another voice";
|
|
909
|
+
}
|
|
910
|
+
if (reasons & 4096 /* HiddenParallel */) {
|
|
911
|
+
return "Hidden perfect interval by similar motion";
|
|
912
|
+
}
|
|
913
|
+
if (reasons & 8192 /* VoiceCrossing */) {
|
|
914
|
+
return "Crosses another voice";
|
|
915
|
+
}
|
|
916
|
+
if (reasons & 1 /* ChordTone */) {
|
|
917
|
+
return "Chord tone";
|
|
918
|
+
}
|
|
919
|
+
if (reasons & 2 /* Tension */) {
|
|
920
|
+
return "Chord tension";
|
|
921
|
+
}
|
|
922
|
+
if (reasons & 16 /* NonScale */) {
|
|
923
|
+
return "Outside the key";
|
|
924
|
+
}
|
|
925
|
+
if (reasons & 8 /* ScaleTone */) {
|
|
926
|
+
return "Scale tone, not in the chord";
|
|
927
|
+
}
|
|
928
|
+
return q.chord ? "Placeable" : "No chord context";
|
|
929
|
+
}
|
|
930
|
+
function enumerateSafePitches(q, pitchLow, pitchHigh) {
|
|
931
|
+
const chordTones = [];
|
|
932
|
+
const others = [];
|
|
933
|
+
if (!Number.isFinite(pitchLow) || !Number.isFinite(pitchHigh)) {
|
|
934
|
+
return [];
|
|
935
|
+
}
|
|
936
|
+
for (let pitch = pitchHigh; pitch >= pitchLow; pitch -= 1) {
|
|
937
|
+
const result = evaluateInternal({ ...q, candidatePitch: pitch }, false);
|
|
938
|
+
if (result.safety === 2 /* Dissonant */) {
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
if (result.reasons & 1 /* ChordTone */) {
|
|
942
|
+
chordTones.push(pitch);
|
|
943
|
+
} else {
|
|
944
|
+
others.push(pitch);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return [...chordTones, ...others];
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// src/core/pitch/index.ts
|
|
951
|
+
var LETTER_SEMITONES = [0, 2, 4, 5, 7, 9, 11];
|
|
952
|
+
var LETTER_NAMES = ["C", "D", "E", "F", "G", "A", "B"];
|
|
953
|
+
function mod12(n) {
|
|
954
|
+
return (n % 12 + 12) % 12;
|
|
955
|
+
}
|
|
956
|
+
function mod7(n) {
|
|
957
|
+
return (n % 7 + 7) % 7;
|
|
958
|
+
}
|
|
959
|
+
function parseNote(text) {
|
|
960
|
+
const match = /^([A-Ga-g])([#x]*|b*)(-?\d+)?$/.exec(text.trim());
|
|
961
|
+
if (!match) {
|
|
962
|
+
throw new Error(`Invalid note: ${text}`);
|
|
963
|
+
}
|
|
964
|
+
const letterGlyph = (match[1] ?? "").toUpperCase();
|
|
965
|
+
const letter = LETTER_NAMES.indexOf(letterGlyph);
|
|
966
|
+
let alter = 0;
|
|
967
|
+
for (const ch of match[2] ?? "") {
|
|
968
|
+
if (ch === "#") {
|
|
969
|
+
alter += 1;
|
|
970
|
+
} else if (ch === "x") {
|
|
971
|
+
alter += 2;
|
|
972
|
+
} else if (ch === "b") {
|
|
973
|
+
alter -= 1;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
const note = { letter, alter };
|
|
977
|
+
if (match[3] !== void 0) {
|
|
978
|
+
note.octave = Number.parseInt(match[3], 10);
|
|
979
|
+
}
|
|
980
|
+
return note;
|
|
981
|
+
}
|
|
982
|
+
function formatAlter(alter) {
|
|
983
|
+
if (alter > 0) {
|
|
984
|
+
return "#".repeat(alter);
|
|
985
|
+
}
|
|
986
|
+
if (alter < 0) {
|
|
987
|
+
return "b".repeat(-alter);
|
|
988
|
+
}
|
|
989
|
+
return "";
|
|
990
|
+
}
|
|
991
|
+
function formatNote(note) {
|
|
992
|
+
const glyph = LETTER_NAMES[mod7(note.letter)] ?? "C";
|
|
993
|
+
const octave = note.octave === void 0 ? "" : String(note.octave);
|
|
994
|
+
return `${glyph}${formatAlter(note.alter)}${octave}`;
|
|
995
|
+
}
|
|
996
|
+
function noteToPitchClass(note) {
|
|
997
|
+
const natural = LETTER_SEMITONES[mod7(note.letter)] ?? 0;
|
|
998
|
+
return mod12(natural + note.alter);
|
|
999
|
+
}
|
|
1000
|
+
var SHARP_SPELLING = [
|
|
1001
|
+
[0, 0],
|
|
1002
|
+
[0, 1],
|
|
1003
|
+
[1, 0],
|
|
1004
|
+
[1, 1],
|
|
1005
|
+
[2, 0],
|
|
1006
|
+
[3, 0],
|
|
1007
|
+
[3, 1],
|
|
1008
|
+
[4, 0],
|
|
1009
|
+
[4, 1],
|
|
1010
|
+
[5, 0],
|
|
1011
|
+
[5, 1],
|
|
1012
|
+
[6, 0]
|
|
1013
|
+
];
|
|
1014
|
+
var FLAT_SPELLING = [
|
|
1015
|
+
[0, 0],
|
|
1016
|
+
[1, -1],
|
|
1017
|
+
[1, 0],
|
|
1018
|
+
[2, -1],
|
|
1019
|
+
[2, 0],
|
|
1020
|
+
[3, 0],
|
|
1021
|
+
[4, -1],
|
|
1022
|
+
[4, 0],
|
|
1023
|
+
[5, -1],
|
|
1024
|
+
[5, 0],
|
|
1025
|
+
[6, -1],
|
|
1026
|
+
[6, 0]
|
|
1027
|
+
];
|
|
1028
|
+
function midiToNote(midi, spelling = "sharp") {
|
|
1029
|
+
const rounded = Math.round(midi);
|
|
1030
|
+
const pc = mod12(rounded);
|
|
1031
|
+
const octave = Math.floor(rounded / 12) - 1;
|
|
1032
|
+
const table = spelling === "sharp" ? SHARP_SPELLING : FLAT_SPELLING;
|
|
1033
|
+
const entry = table[pc] ?? [0, 0];
|
|
1034
|
+
return { letter: entry[0], alter: entry[1], octave };
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// src/theory/spelling/index.ts
|
|
1038
|
+
var LETTER_SEMITONES2 = [0, 2, 4, 5, 7, 9, 11];
|
|
1039
|
+
var CHROMATIC_SPELLING = {
|
|
1040
|
+
1: { degreeOffset: 1, alter: -1 },
|
|
1041
|
+
// b2
|
|
1042
|
+
3: { degreeOffset: 2, alter: -1 },
|
|
1043
|
+
// b3
|
|
1044
|
+
6: { degreeOffset: 3, alter: 1 },
|
|
1045
|
+
// #4
|
|
1046
|
+
8: { degreeOffset: 5, alter: -1 },
|
|
1047
|
+
// b6
|
|
1048
|
+
9: { degreeOffset: 5, alter: 1 },
|
|
1049
|
+
// #6 (raised sixth of a minor key)
|
|
1050
|
+
10: { degreeOffset: 6, alter: -1 },
|
|
1051
|
+
// b7
|
|
1052
|
+
11: { degreeOffset: 6, alter: 1 }
|
|
1053
|
+
// #7 (raised leading tone of a minor key)
|
|
1054
|
+
};
|
|
1055
|
+
function mod122(n) {
|
|
1056
|
+
return (n % 12 + 12) % 12;
|
|
1057
|
+
}
|
|
1058
|
+
function mod72(n) {
|
|
1059
|
+
return (n % 7 + 7) % 7;
|
|
1060
|
+
}
|
|
1061
|
+
function naturalPc(letter) {
|
|
1062
|
+
return LETTER_SEMITONES2[mod72(letter)] ?? 0;
|
|
1063
|
+
}
|
|
1064
|
+
function alterFor(letter, pc) {
|
|
1065
|
+
let d = mod122(pc - naturalPc(letter));
|
|
1066
|
+
if (d > 6) {
|
|
1067
|
+
d -= 12;
|
|
1068
|
+
}
|
|
1069
|
+
return d;
|
|
1070
|
+
}
|
|
1071
|
+
function isHeptatonic(key) {
|
|
1072
|
+
return scaleTonesInDegreeOrder(key).length === 7;
|
|
1073
|
+
}
|
|
1074
|
+
function spellPitchClass(pc, tonic, key) {
|
|
1075
|
+
const tonicPc = mod122(naturalPc(tonic.letter) + tonic.alter);
|
|
1076
|
+
const offset = mod122(pc - tonicPc);
|
|
1077
|
+
const tones = scaleTonesInDegreeOrder(key);
|
|
1078
|
+
if (isHeptatonic(key)) {
|
|
1079
|
+
const degree = tones.indexOf(mod122(pc));
|
|
1080
|
+
if (degree >= 0) {
|
|
1081
|
+
const letter = mod72(tonic.letter + degree);
|
|
1082
|
+
return { letter, alter: alterFor(letter, pc) };
|
|
1083
|
+
}
|
|
1084
|
+
const chromatic = CHROMATIC_SPELLING[offset];
|
|
1085
|
+
if (chromatic) {
|
|
1086
|
+
const letter = mod72(tonic.letter + chromatic.degreeOffset);
|
|
1087
|
+
return { letter, alter: alterFor(letter, pc) };
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
for (let letter = 0; letter < 7; letter += 1) {
|
|
1091
|
+
if (naturalPc(letter) === mod122(pc)) {
|
|
1092
|
+
return { letter, alter: 0 };
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
const belowLetter = mod72([0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6][mod122(pc)] ?? 0);
|
|
1096
|
+
return { letter: belowLetter, alter: alterFor(belowLetter, pc) };
|
|
1097
|
+
}
|
|
1098
|
+
function spellScale(tonic, key) {
|
|
1099
|
+
return scaleTonesInDegreeOrder(key).map((pc) => spellPitchClass(pc, tonic, key));
|
|
1100
|
+
}
|
|
1101
|
+
function spellPitchClasses(pcs, tonic, key) {
|
|
1102
|
+
return pcs.map((pc) => spellPitchClass(pc, tonic, key));
|
|
1103
|
+
}
|
|
1104
|
+
function spellChord(chord, tonic, key) {
|
|
1105
|
+
return chord.intervals.map(
|
|
1106
|
+
(interval) => spellPitchClass(mod122(chord.rootPc + interval), tonic, key)
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
function noteNames(notes) {
|
|
1110
|
+
return notes.map(formatNote);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// src/theory/symbol/index.ts
|
|
1114
|
+
var ROOT_RE = /^[A-Ga-g](?:#{1,2}|b{1,2})?/;
|
|
1115
|
+
var BASS_RE = /^[A-Ga-g](?:#{1,2}|b{1,2})?$/;
|
|
1116
|
+
var QUALITY_MAP = {
|
|
1117
|
+
"": "maj",
|
|
1118
|
+
m: "min",
|
|
1119
|
+
min: "min",
|
|
1120
|
+
"-": "min",
|
|
1121
|
+
dim: "dim",
|
|
1122
|
+
"\xB0": "dim",
|
|
1123
|
+
aug: "aug",
|
|
1124
|
+
"+": "aug",
|
|
1125
|
+
maj7: "maj7",
|
|
1126
|
+
M7: "maj7",
|
|
1127
|
+
\u0394: "maj7",
|
|
1128
|
+
\u03947: "maj7",
|
|
1129
|
+
m7: "min7",
|
|
1130
|
+
min7: "min7",
|
|
1131
|
+
"-7": "min7",
|
|
1132
|
+
"7": "dom7",
|
|
1133
|
+
dim7: "dim7",
|
|
1134
|
+
"\xB07": "dim7",
|
|
1135
|
+
m7b5: "m7b5",
|
|
1136
|
+
\u00F8: "m7b5",
|
|
1137
|
+
\u00F87: "m7b5",
|
|
1138
|
+
min7b5: "m7b5",
|
|
1139
|
+
mMaj7: "minMaj7",
|
|
1140
|
+
mM7: "minMaj7",
|
|
1141
|
+
minMaj7: "minMaj7",
|
|
1142
|
+
aug7: "aug7",
|
|
1143
|
+
"+7": "aug7",
|
|
1144
|
+
"7#5": "aug7",
|
|
1145
|
+
augMaj7: "augMaj7",
|
|
1146
|
+
augM7: "augMaj7",
|
|
1147
|
+
"+maj7": "augMaj7",
|
|
1148
|
+
"+M7": "augMaj7",
|
|
1149
|
+
majb5: "majb5",
|
|
1150
|
+
"maj(b5)": "majb5",
|
|
1151
|
+
"(b5)": "majb5",
|
|
1152
|
+
"6": "6",
|
|
1153
|
+
m6: "min6",
|
|
1154
|
+
min6: "min6",
|
|
1155
|
+
"6/9": "6/9",
|
|
1156
|
+
"69": "6/9",
|
|
1157
|
+
sus2: "sus2",
|
|
1158
|
+
sus: "sus4",
|
|
1159
|
+
sus4: "sus4",
|
|
1160
|
+
add9: "add9",
|
|
1161
|
+
add11: "add11",
|
|
1162
|
+
maj9: "maj9",
|
|
1163
|
+
M9: "maj9",
|
|
1164
|
+
m9: "min9",
|
|
1165
|
+
min9: "min9",
|
|
1166
|
+
"9": "dom9",
|
|
1167
|
+
"7b9": "7b9",
|
|
1168
|
+
"7#9": "7#9",
|
|
1169
|
+
"7#11": "7#11",
|
|
1170
|
+
"7b13": "7b13",
|
|
1171
|
+
"11": "11",
|
|
1172
|
+
"13": "13",
|
|
1173
|
+
"5": "5"
|
|
1174
|
+
};
|
|
1175
|
+
var CANONICAL_SUFFIX = {
|
|
1176
|
+
maj: "",
|
|
1177
|
+
min: "m",
|
|
1178
|
+
dim: "dim",
|
|
1179
|
+
aug: "aug",
|
|
1180
|
+
maj7: "maj7",
|
|
1181
|
+
min7: "m7",
|
|
1182
|
+
dom7: "7",
|
|
1183
|
+
dim7: "dim7",
|
|
1184
|
+
m7b5: "m7b5",
|
|
1185
|
+
minMaj7: "mMaj7",
|
|
1186
|
+
aug7: "aug7",
|
|
1187
|
+
augMaj7: "augMaj7",
|
|
1188
|
+
majb5: "maj(b5)",
|
|
1189
|
+
"6": "6",
|
|
1190
|
+
min6: "m6",
|
|
1191
|
+
"6/9": "6/9",
|
|
1192
|
+
sus2: "sus2",
|
|
1193
|
+
sus4: "sus4",
|
|
1194
|
+
add9: "add9",
|
|
1195
|
+
add11: "add11",
|
|
1196
|
+
maj9: "maj9",
|
|
1197
|
+
min9: "m9",
|
|
1198
|
+
dom9: "9",
|
|
1199
|
+
"7b9": "7b9",
|
|
1200
|
+
"7#9": "7#9",
|
|
1201
|
+
"7#11": "7#11",
|
|
1202
|
+
"7b13": "7b13",
|
|
1203
|
+
"11": "11",
|
|
1204
|
+
"13": "13",
|
|
1205
|
+
"5": "5"
|
|
1206
|
+
};
|
|
1207
|
+
function pitchClass6(value) {
|
|
1208
|
+
return (Math.trunc(value) % 12 + 12) % 12;
|
|
1209
|
+
}
|
|
1210
|
+
function makeSpelledChord(root, quality, bass) {
|
|
1211
|
+
const chord = makeChord(
|
|
1212
|
+
noteToPitchClass(root),
|
|
1213
|
+
quality,
|
|
1214
|
+
bass === void 0 ? void 0 : noteToPitchClass(bass)
|
|
1215
|
+
);
|
|
1216
|
+
chord.rootSpelling = { letter: root.letter, alter: root.alter };
|
|
1217
|
+
if (bass !== void 0) {
|
|
1218
|
+
chord.bassSpelling = { letter: bass.letter, alter: bass.alter };
|
|
1219
|
+
}
|
|
1220
|
+
return chord;
|
|
1221
|
+
}
|
|
1222
|
+
function parseChordSymbol(text) {
|
|
1223
|
+
const trimmed = text.trim();
|
|
1224
|
+
const rootMatch = ROOT_RE.exec(trimmed);
|
|
1225
|
+
if (!rootMatch) {
|
|
1226
|
+
throw new Error(`Invalid chord symbol: ${text}`);
|
|
1227
|
+
}
|
|
1228
|
+
const rootToken = rootMatch[0];
|
|
1229
|
+
const rootNote = parseNote(rootToken);
|
|
1230
|
+
const rest = trimmed.slice(rootToken.length);
|
|
1231
|
+
const slashIndex = rest.lastIndexOf("/");
|
|
1232
|
+
if (slashIndex !== -1) {
|
|
1233
|
+
const before = rest.slice(0, slashIndex);
|
|
1234
|
+
const after = rest.slice(slashIndex + 1);
|
|
1235
|
+
const beforeQuality = QUALITY_MAP[before];
|
|
1236
|
+
if (beforeQuality !== void 0 && BASS_RE.test(after)) {
|
|
1237
|
+
return makeSpelledChord(rootNote, beforeQuality, parseNote(after));
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
const quality = QUALITY_MAP[rest];
|
|
1241
|
+
if (quality === void 0) {
|
|
1242
|
+
throw new Error(`Unrecognized chord quality: ${text}`);
|
|
1243
|
+
}
|
|
1244
|
+
return makeSpelledChord(rootNote, quality);
|
|
1245
|
+
}
|
|
1246
|
+
function pitchClassName(pc, hint, flats) {
|
|
1247
|
+
if (flats === void 0 && hint !== void 0 && noteToPitchClass(hint) === pc) {
|
|
1248
|
+
return formatNote(hint);
|
|
1249
|
+
}
|
|
1250
|
+
const note = midiToNote(60 + pc, flats ? "flat" : "sharp");
|
|
1251
|
+
return formatNote({ letter: note.letter, alter: note.alter });
|
|
1252
|
+
}
|
|
1253
|
+
function formatChordSymbol(chord, opts) {
|
|
1254
|
+
const rootPc = pitchClass6(chord.rootPc);
|
|
1255
|
+
const rootName = pitchClassName(rootPc, chord.rootSpelling, opts?.flats);
|
|
1256
|
+
const suffix = CANONICAL_SUFFIX[chord.quality];
|
|
1257
|
+
let symbol = `${rootName}${suffix}`;
|
|
1258
|
+
if (chord.bassPc !== void 0 && pitchClass6(chord.bassPc) !== rootPc) {
|
|
1259
|
+
symbol += `/${pitchClassName(pitchClass6(chord.bassPc), chord.bassSpelling, opts?.flats)}`;
|
|
1260
|
+
}
|
|
1261
|
+
return symbol;
|
|
1262
|
+
}
|
|
1263
|
+
function transposeChordSymbol(text, semitones, opts) {
|
|
1264
|
+
const chord = parseChordSymbol(text);
|
|
1265
|
+
const transposed = { ...chord, rootPc: pitchClass6(chord.rootPc + semitones) };
|
|
1266
|
+
if (chord.bassPc !== void 0) {
|
|
1267
|
+
transposed.bassPc = pitchClass6(chord.bassPc + semitones);
|
|
1268
|
+
}
|
|
1269
|
+
return formatChordSymbol(transposed, opts);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// src/theory/voicing/internal.ts
|
|
1273
|
+
var DEFAULT_MAX_SPACING = 12;
|
|
1274
|
+
var VIOLATION_PENALTY = 1e3;
|
|
1275
|
+
var MISSING_TONE_PENALTY = 500;
|
|
1276
|
+
var POOR_DOUBLING_PENALTY = 4;
|
|
1277
|
+
var MAX_CANDIDATES = 4e3;
|
|
1278
|
+
function pitchClass7(value) {
|
|
1279
|
+
return (Math.trunc(value) % 12 + 12) % 12;
|
|
1280
|
+
}
|
|
1281
|
+
function pitchesForPc(pc, range) {
|
|
1282
|
+
const result = [];
|
|
1283
|
+
for (let midi = Math.ceil(range.min); midi <= range.max; midi += 1) {
|
|
1284
|
+
if (pitchClass7(midi) === pc) {
|
|
1285
|
+
result.push(midi);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
const center = (range.min + range.max) / 2;
|
|
1289
|
+
return result.sort((a, b) => {
|
|
1290
|
+
const da = Math.abs(a - center);
|
|
1291
|
+
const db = Math.abs(b - center);
|
|
1292
|
+
return da === db ? a - b : da - db;
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
function enumerateVoicings(chord, ranges, maxSpacing) {
|
|
1296
|
+
const chordPcs = chordPitchClasses(chord);
|
|
1297
|
+
const bassPc = pitchClass7(chord.bassPc ?? chord.rootPc);
|
|
1298
|
+
const results = [];
|
|
1299
|
+
const current = [];
|
|
1300
|
+
const build = (voice) => {
|
|
1301
|
+
if (results.length >= MAX_CANDIDATES) {
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
if (voice === ranges.length) {
|
|
1305
|
+
results.push([...current]);
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
const range = ranges[voice];
|
|
1309
|
+
if (range === void 0) {
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
const pcs = voice === 0 ? [bassPc] : chordPcs;
|
|
1313
|
+
const prev = current[voice - 1];
|
|
1314
|
+
for (const pc of pcs) {
|
|
1315
|
+
for (const pitch of pitchesForPc(pc, range)) {
|
|
1316
|
+
if (prev !== void 0) {
|
|
1317
|
+
if (pitch < prev) {
|
|
1318
|
+
continue;
|
|
1319
|
+
}
|
|
1320
|
+
const spacingLimit = voice === 1 ? maxSpacing + 12 : maxSpacing;
|
|
1321
|
+
if (pitch - prev > spacingLimit) {
|
|
1322
|
+
continue;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
current.push(pitch);
|
|
1326
|
+
build(voice + 1);
|
|
1327
|
+
current.pop();
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
build(0);
|
|
1332
|
+
return results;
|
|
1333
|
+
}
|
|
1334
|
+
function structuralPenalty(pitches, chord) {
|
|
1335
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1336
|
+
for (const pitch of pitches) {
|
|
1337
|
+
const pc = pitchClass7(pitch);
|
|
1338
|
+
counts.set(pc, (counts.get(pc) ?? 0) + 1);
|
|
1339
|
+
}
|
|
1340
|
+
let penalty = 0;
|
|
1341
|
+
for (const pc of chordPitchClasses(chord)) {
|
|
1342
|
+
if (!counts.has(pc)) {
|
|
1343
|
+
penalty += MISSING_TONE_PENALTY;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
const rootPc = pitchClass7(chord.rootPc);
|
|
1347
|
+
const fifthPc = pitchClass7(chord.rootPc + 7);
|
|
1348
|
+
for (const [pc, count] of counts) {
|
|
1349
|
+
if (count > 1 && pc !== rootPc && pc !== fifthPc) {
|
|
1350
|
+
penalty += (count - 1) * POOR_DOUBLING_PENALTY;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
return penalty;
|
|
1354
|
+
}
|
|
1355
|
+
function violationCount(prev, cur, maxSpacing) {
|
|
1356
|
+
let count = 0;
|
|
1357
|
+
for (let lower = 0; lower < cur.length; lower += 1) {
|
|
1358
|
+
for (let upper = lower + 1; upper < cur.length; upper += 1) {
|
|
1359
|
+
const prevLower = prev[lower];
|
|
1360
|
+
const prevUpper = prev[upper];
|
|
1361
|
+
const curLower = cur[lower];
|
|
1362
|
+
const curUpper = cur[upper];
|
|
1363
|
+
if (prevLower === void 0 || prevUpper === void 0 || curLower === void 0 || curUpper === void 0) {
|
|
1364
|
+
continue;
|
|
1365
|
+
}
|
|
1366
|
+
if (createsParallelPerfect(prevUpper, curUpper, prevLower, curLower)) {
|
|
1367
|
+
count += 1;
|
|
1368
|
+
}
|
|
1369
|
+
if (createsVoiceCrossing(curUpper, curLower)) {
|
|
1370
|
+
count += 1;
|
|
1371
|
+
}
|
|
1372
|
+
if (upper === lower + 1) {
|
|
1373
|
+
if (createsVoiceOverlap(prevUpper, curUpper, prevLower, curLower)) {
|
|
1374
|
+
count += 1;
|
|
1375
|
+
}
|
|
1376
|
+
if (lower > 0 && exceedsSpacing(curUpper, curLower, maxSpacing)) {
|
|
1377
|
+
count += 1;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
return count;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// src/theory/voicing/satb.ts
|
|
1386
|
+
var SATB_RANGES = [
|
|
1387
|
+
{ min: 40, max: 60 },
|
|
1388
|
+
{ min: 48, max: 67 },
|
|
1389
|
+
{ min: 55, max: 74 },
|
|
1390
|
+
{ min: 60, max: 79 }
|
|
1391
|
+
];
|
|
1392
|
+
var DERIVED_LOW = 40;
|
|
1393
|
+
var DERIVED_HIGH = 79;
|
|
1394
|
+
var DERIVED_SPAN = 19;
|
|
1395
|
+
function resolveRanges(opts) {
|
|
1396
|
+
if (opts?.ranges !== void 0) {
|
|
1397
|
+
if (opts.ranges.length === 0) {
|
|
1398
|
+
throw new Error("ranges must contain at least one voice range");
|
|
1399
|
+
}
|
|
1400
|
+
return opts.ranges.map((range) => ({ ...range }));
|
|
1401
|
+
}
|
|
1402
|
+
const voices = opts?.voices ?? 4;
|
|
1403
|
+
if (voices < 1) {
|
|
1404
|
+
throw new Error("voices must be at least 1");
|
|
1405
|
+
}
|
|
1406
|
+
if (voices === 4) {
|
|
1407
|
+
return SATB_RANGES.map((range) => ({ ...range }));
|
|
1408
|
+
}
|
|
1409
|
+
if (voices === 1) {
|
|
1410
|
+
return [{ min: DERIVED_LOW, max: DERIVED_HIGH }];
|
|
1411
|
+
}
|
|
1412
|
+
const ranges = [];
|
|
1413
|
+
for (let i = 0; i < voices; i += 1) {
|
|
1414
|
+
const min = Math.round(
|
|
1415
|
+
DERIVED_LOW + i * (DERIVED_HIGH - DERIVED_SPAN - DERIVED_LOW) / (voices - 1)
|
|
1416
|
+
);
|
|
1417
|
+
ranges.push({ min, max: min + DERIVED_SPAN });
|
|
1418
|
+
}
|
|
1419
|
+
return ranges;
|
|
1420
|
+
}
|
|
1421
|
+
function voiceChord(chord, opts) {
|
|
1422
|
+
const ranges = resolveRanges(opts);
|
|
1423
|
+
const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;
|
|
1424
|
+
const candidates = enumerateVoicings(chord, ranges, maxSpacing);
|
|
1425
|
+
let best;
|
|
1426
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
1427
|
+
for (const candidate of candidates) {
|
|
1428
|
+
let score = structuralPenalty(candidate, chord);
|
|
1429
|
+
for (let i = 0; i < candidate.length; i += 1) {
|
|
1430
|
+
const pitch = candidate[i];
|
|
1431
|
+
const range = ranges[i];
|
|
1432
|
+
if (pitch === void 0 || range === void 0) {
|
|
1433
|
+
continue;
|
|
1434
|
+
}
|
|
1435
|
+
score += Math.abs(pitch - (range.min + range.max) / 2);
|
|
1436
|
+
}
|
|
1437
|
+
if (score < bestScore) {
|
|
1438
|
+
bestScore = score;
|
|
1439
|
+
best = candidate;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
if (best === void 0) {
|
|
1443
|
+
throw new Error("no voicing satisfies the given ranges");
|
|
1444
|
+
}
|
|
1445
|
+
return best;
|
|
1446
|
+
}
|
|
1447
|
+
function voiceProgression(chords, opts) {
|
|
1448
|
+
const ranges = resolveRanges(opts);
|
|
1449
|
+
const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;
|
|
1450
|
+
const result = [];
|
|
1451
|
+
let prev;
|
|
1452
|
+
for (const chord of chords) {
|
|
1453
|
+
if (prev === void 0) {
|
|
1454
|
+
prev = voiceChord(chord, opts);
|
|
1455
|
+
result.push(prev);
|
|
1456
|
+
continue;
|
|
1457
|
+
}
|
|
1458
|
+
const candidates = enumerateVoicings(chord, ranges, maxSpacing);
|
|
1459
|
+
let best;
|
|
1460
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
1461
|
+
for (const candidate of candidates) {
|
|
1462
|
+
const score = structuralPenalty(candidate, chord) + voiceLeadingCost(prev, candidate) + VIOLATION_PENALTY * violationCount(prev, candidate, maxSpacing);
|
|
1463
|
+
if (score < bestScore) {
|
|
1464
|
+
bestScore = score;
|
|
1465
|
+
best = candidate;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
if (best === void 0) {
|
|
1469
|
+
throw new Error("no voicing satisfies the given ranges");
|
|
1470
|
+
}
|
|
1471
|
+
result.push(best);
|
|
1472
|
+
prev = best;
|
|
1473
|
+
}
|
|
1474
|
+
return result;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// src/theory/voicing/leading.ts
|
|
1478
|
+
var HIDDEN_PERFECT_PENALTY = 6;
|
|
1479
|
+
function voiceLeadingCost(from, to) {
|
|
1480
|
+
if (from.length !== to.length) {
|
|
1481
|
+
return Number.POSITIVE_INFINITY;
|
|
1482
|
+
}
|
|
1483
|
+
let total = 0;
|
|
1484
|
+
for (let i = 0; i < from.length; i += 1) {
|
|
1485
|
+
const a = from[i];
|
|
1486
|
+
const b = to[i];
|
|
1487
|
+
if (a === void 0 || b === void 0) {
|
|
1488
|
+
continue;
|
|
1489
|
+
}
|
|
1490
|
+
total += Math.abs(b - a);
|
|
1491
|
+
}
|
|
1492
|
+
if (from.length >= 2) {
|
|
1493
|
+
const bassPrev = from[0];
|
|
1494
|
+
const bassCur = to[0];
|
|
1495
|
+
const sopPrev = from[from.length - 1];
|
|
1496
|
+
const sopCur = to[to.length - 1];
|
|
1497
|
+
if (bassPrev !== void 0 && bassCur !== void 0 && sopPrev !== void 0 && sopCur !== void 0 && createsHiddenParallelPerfect(bassPrev, bassCur, sopPrev, sopCur)) {
|
|
1498
|
+
total += HIDDEN_PERFECT_PENALTY;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
return total;
|
|
1502
|
+
}
|
|
1503
|
+
function nextVoicing(current, chord, opts) {
|
|
1504
|
+
const ranges = opts?.ranges !== void 0 || opts?.voices !== void 0 ? resolveRanges(opts) : current.map((pitch) => {
|
|
1505
|
+
const centre = Math.min(127, Math.max(0, pitch));
|
|
1506
|
+
return { min: Math.max(0, centre - 12), max: Math.min(127, centre + 12) };
|
|
1507
|
+
});
|
|
1508
|
+
const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;
|
|
1509
|
+
const candidates = enumerateVoicings(chord, ranges, maxSpacing);
|
|
1510
|
+
let best;
|
|
1511
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
1512
|
+
for (const candidate of candidates) {
|
|
1513
|
+
const score = structuralPenalty(candidate, chord) + voiceLeadingCost(current, candidate) + VIOLATION_PENALTY * violationCount(current, candidate, maxSpacing);
|
|
1514
|
+
if (score < bestScore) {
|
|
1515
|
+
bestScore = score;
|
|
1516
|
+
best = candidate;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
if (best === void 0) {
|
|
1520
|
+
throw new Error("no voicing satisfies the given ranges");
|
|
1521
|
+
}
|
|
1522
|
+
return best;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// src/theory/voicing/styled.ts
|
|
1526
|
+
var DEFAULT_STYLE_OCTAVE = 4;
|
|
1527
|
+
function dedupePcs(pcs) {
|
|
1528
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1529
|
+
const result = [];
|
|
1530
|
+
for (const pc of pcs) {
|
|
1531
|
+
if (!seen.has(pc)) {
|
|
1532
|
+
seen.add(pc);
|
|
1533
|
+
result.push(pc);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
return result;
|
|
1537
|
+
}
|
|
1538
|
+
function lowestPitchAtOrAbove(pc, floor) {
|
|
1539
|
+
return floor + ((pc - pitchClass7(floor)) % 12 + 12) % 12;
|
|
1540
|
+
}
|
|
1541
|
+
function nearestPc(target, pcs) {
|
|
1542
|
+
let best = pcs[0] ?? target;
|
|
1543
|
+
let bestDist = 12;
|
|
1544
|
+
for (const pc of pcs) {
|
|
1545
|
+
const dist = Math.min(((pc - target) % 12 + 12) % 12, ((target - pc) % 12 + 12) % 12);
|
|
1546
|
+
if (dist < bestDist) {
|
|
1547
|
+
bestDist = dist;
|
|
1548
|
+
best = pc;
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
return best;
|
|
1552
|
+
}
|
|
1553
|
+
function voiceChordStyled(chord, opts) {
|
|
1554
|
+
const style = opts?.style ?? "close";
|
|
1555
|
+
const base = 12 * ((opts?.octave ?? DEFAULT_STYLE_OCTAVE) + 1);
|
|
1556
|
+
const rootPc = pitchClass7(chord.rootPc);
|
|
1557
|
+
const bassPc = pitchClass7(chord.bassPc ?? chord.rootPc);
|
|
1558
|
+
const omitRoot = style === "rootless" || opts?.rootless === true;
|
|
1559
|
+
let order = dedupePcs(chord.intervals.map((interval) => pitchClass7(chord.rootPc + interval)));
|
|
1560
|
+
const bassIndex = order.indexOf(bassPc);
|
|
1561
|
+
if (bassIndex > 0) {
|
|
1562
|
+
order = [...order.slice(bassIndex), ...order.slice(0, bassIndex)];
|
|
1563
|
+
} else if (bassIndex < 0) {
|
|
1564
|
+
order = [bassPc, ...order];
|
|
1565
|
+
}
|
|
1566
|
+
if (style === "shell") {
|
|
1567
|
+
const roles = order.map((pc) => chordToneRole(pc, chord));
|
|
1568
|
+
const guide = roles.includes("seventh") ? "seventh" : roles.includes("sixth") ? "sixth" : "fifth";
|
|
1569
|
+
order = order.filter((_pc, i) => {
|
|
1570
|
+
const role = roles[i];
|
|
1571
|
+
return role === "root" || role === "third" || role === guide;
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
if (omitRoot) {
|
|
1575
|
+
order = order.filter((pc) => pc !== rootPc);
|
|
1576
|
+
}
|
|
1577
|
+
if (order.length === 0) {
|
|
1578
|
+
return [];
|
|
1579
|
+
}
|
|
1580
|
+
if (opts?.topNote !== void 0) {
|
|
1581
|
+
const target = order.includes(pitchClass7(opts.topNote)) ? pitchClass7(opts.topNote) : nearestPc(pitchClass7(opts.topNote), order);
|
|
1582
|
+
const targetIndex = order.indexOf(target);
|
|
1583
|
+
order = [...order.slice(targetIndex + 1), ...order.slice(0, targetIndex + 1)];
|
|
1584
|
+
}
|
|
1585
|
+
const stack = [];
|
|
1586
|
+
let prev;
|
|
1587
|
+
for (const pc of order) {
|
|
1588
|
+
if (prev === void 0) {
|
|
1589
|
+
prev = lowestPitchAtOrAbove(pc, base);
|
|
1590
|
+
} else {
|
|
1591
|
+
let delta = ((pc - pitchClass7(prev)) % 12 + 12) % 12;
|
|
1592
|
+
if (delta === 0) {
|
|
1593
|
+
delta = 12;
|
|
1594
|
+
}
|
|
1595
|
+
prev += delta;
|
|
1596
|
+
}
|
|
1597
|
+
stack.push(prev);
|
|
1598
|
+
}
|
|
1599
|
+
if (style === "drop2" && stack.length >= 2) {
|
|
1600
|
+
stack[stack.length - 2] = (stack[stack.length - 2] ?? 0) - 12;
|
|
1601
|
+
} else if (style === "drop3" && stack.length >= 3) {
|
|
1602
|
+
stack[stack.length - 3] = (stack[stack.length - 3] ?? 0) - 12;
|
|
1603
|
+
}
|
|
1604
|
+
return stack.sort((a, b) => a - b);
|
|
1605
|
+
}
|
|
1606
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1607
|
+
0 && (module.exports = {
|
|
1608
|
+
BLUES_MASK,
|
|
1609
|
+
CHROMATIC_MASK,
|
|
1610
|
+
DORIAN_MASK,
|
|
1611
|
+
HARMONIC_MINOR_MASK,
|
|
1612
|
+
LOCRIAN_MASK,
|
|
1613
|
+
LYDIAN_MASK,
|
|
1614
|
+
MAJOR_MASK,
|
|
1615
|
+
MAJOR_PENTATONIC_MASK,
|
|
1616
|
+
MELODIC_MINOR_MASK,
|
|
1617
|
+
MINOR_PENTATONIC_MASK,
|
|
1618
|
+
MIXOLYDIAN_MASK,
|
|
1619
|
+
NAMED_SCALES,
|
|
1620
|
+
NATURAL_MINOR_MASK,
|
|
1621
|
+
NoteSafety,
|
|
1622
|
+
OCTATONIC_HALF_WHOLE_MASK,
|
|
1623
|
+
OCTATONIC_WHOLE_HALF_MASK,
|
|
1624
|
+
PHRYGIAN_MASK,
|
|
1625
|
+
ReasonFlag,
|
|
1626
|
+
SATB_RANGES,
|
|
1627
|
+
WHOLE_TONE_MASK,
|
|
1628
|
+
availableTensions,
|
|
1629
|
+
avoidNotes,
|
|
1630
|
+
chordFromDegree,
|
|
1631
|
+
chordPitchClasses,
|
|
1632
|
+
chordQualities,
|
|
1633
|
+
chordScaleReport,
|
|
1634
|
+
chordScales,
|
|
1635
|
+
chordToneRole,
|
|
1636
|
+
createsHiddenParallelPerfect,
|
|
1637
|
+
createsParallelOctave,
|
|
1638
|
+
createsParallelPerfect,
|
|
1639
|
+
createsParallelUnison,
|
|
1640
|
+
createsVerticalDissonance,
|
|
1641
|
+
createsVoiceCrossing,
|
|
1642
|
+
createsVoiceOverlap,
|
|
1643
|
+
diatonicPitchClasses,
|
|
1644
|
+
diatonicSeventh,
|
|
1645
|
+
diatonicTriad,
|
|
1646
|
+
enumerateSafePitches,
|
|
1647
|
+
evaluateSafety,
|
|
1648
|
+
exceedsSpacing,
|
|
1649
|
+
formatChordSymbol,
|
|
1650
|
+
isForbiddenMelodicLeap,
|
|
1651
|
+
isLeadingToneResolution,
|
|
1652
|
+
isScaleTone,
|
|
1653
|
+
majorKey,
|
|
1654
|
+
makeChord,
|
|
1655
|
+
maskFromOffsets,
|
|
1656
|
+
minorKey,
|
|
1657
|
+
nearestScaleTone,
|
|
1658
|
+
nextVoicing,
|
|
1659
|
+
noteNames,
|
|
1660
|
+
parseChordSymbol,
|
|
1661
|
+
pitchToScaleDegree,
|
|
1662
|
+
roleOf,
|
|
1663
|
+
scaleByName,
|
|
1664
|
+
scaleMatchesChord,
|
|
1665
|
+
scaleTonesInDegreeOrder,
|
|
1666
|
+
scalesForChanges,
|
|
1667
|
+
spellChord,
|
|
1668
|
+
spellPitchClass,
|
|
1669
|
+
spellPitchClasses,
|
|
1670
|
+
spellScale,
|
|
1671
|
+
transposeChordSymbol,
|
|
1672
|
+
voiceChord,
|
|
1673
|
+
voiceChordStyled,
|
|
1674
|
+
voiceLeadingCost,
|
|
1675
|
+
voiceProgression
|
|
1676
|
+
});
|
|
1677
|
+
//# sourceMappingURL=index.cjs.map
|