@libraz/libcantus 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -2
- package/dist/analyze/index.cjs +1697 -0
- package/dist/analyze/index.cjs.map +1 -0
- package/dist/analyze/index.d.cts +429 -0
- package/dist/analyze/index.d.ts +429 -0
- package/dist/analyze/index.js +51 -0
- package/dist/analyze/index.js.map +1 -0
- package/dist/chunk-5HTNMQW4.js +653 -0
- package/dist/chunk-5HTNMQW4.js.map +1 -0
- package/dist/chunk-7Y7WF64D.js +410 -0
- package/dist/chunk-7Y7WF64D.js.map +1 -0
- package/dist/chunk-A4532ZFR.js +259 -0
- package/dist/chunk-A4532ZFR.js.map +1 -0
- package/dist/chunk-BHUT2S5G.js +22 -0
- package/dist/chunk-BHUT2S5G.js.map +1 -0
- package/dist/chunk-CCBLVAPK.js +42 -0
- package/dist/chunk-CCBLVAPK.js.map +1 -0
- package/dist/chunk-COKTX2G5.js +907 -0
- package/dist/chunk-COKTX2G5.js.map +1 -0
- package/dist/chunk-DYWFNC4A.js +152 -0
- package/dist/chunk-DYWFNC4A.js.map +1 -0
- package/dist/chunk-GQKN5KDT.js +159 -0
- package/dist/chunk-GQKN5KDT.js.map +1 -0
- package/dist/chunk-LN7ZZMD2.js +530 -0
- package/dist/chunk-LN7ZZMD2.js.map +1 -0
- package/dist/chunk-MSWFZF7J.js +118 -0
- package/dist/chunk-MSWFZF7J.js.map +1 -0
- package/dist/chunk-R3S2LUBH.js +2722 -0
- package/dist/chunk-R3S2LUBH.js.map +1 -0
- package/dist/chunk-SDM3VJJ5.js +41 -0
- package/dist/chunk-SDM3VJJ5.js.map +1 -0
- package/dist/chunk-XTC3X6MS.js +816 -0
- package/dist/chunk-XTC3X6MS.js.map +1 -0
- package/dist/chunk-ZXW5YIWK.js +55 -0
- package/dist/chunk-ZXW5YIWK.js.map +1 -0
- package/dist/core/index.cjs +432 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +210 -0
- package/dist/core/index.d.ts +210 -0
- package/dist/core/index.js +73 -0
- package/dist/core/index.js.map +1 -0
- package/dist/function-2bJW1Zp_.d.ts +187 -0
- package/dist/function-r2sh-jOO.d.cts +187 -0
- package/dist/generate/index.cjs +3787 -0
- package/dist/generate/index.cjs.map +1 -0
- package/dist/generate/index.d.cts +980 -0
- package/dist/generate/index.d.ts +980 -0
- package/dist/generate/index.js +52 -0
- package/dist/generate/index.js.map +1 -0
- package/dist/index-BMPxwjwV.d.ts +151 -0
- package/dist/index-BOBXBNwP.d.cts +157 -0
- package/dist/index-BdeQ1r_p.d.cts +56 -0
- package/dist/index-BreoJRw0.d.ts +127 -0
- package/dist/index-CGMZNCx6.d.cts +140 -0
- package/dist/index-CGMZNCx6.d.ts +140 -0
- package/dist/index-CM5W4rcm.d.ts +157 -0
- package/dist/index-CP3wAfEH.d.ts +56 -0
- package/dist/index-Cl7mhOFM.d.cts +164 -0
- package/dist/index-Cl7mhOFM.d.ts +164 -0
- package/dist/index-DZ1M21do.d.cts +127 -0
- package/dist/index-D_ZTUNKp.d.cts +151 -0
- package/dist/index.cjs +5277 -4893
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -2621
- package/dist/index.d.ts +14 -2621
- package/dist/index.js +170 -6152
- package/dist/index.js.map +1 -1
- package/dist/model/index.cjs +2435 -0
- package/dist/model/index.cjs.map +1 -0
- package/dist/model/index.d.cts +630 -0
- package/dist/model/index.d.ts +630 -0
- package/dist/model/index.js +21 -0
- package/dist/model/index.js.map +1 -0
- package/dist/styled-BjXtOFNk.d.cts +282 -0
- package/dist/styled-D7BAx3S3.d.ts +282 -0
- package/dist/theory/index.cjs +1677 -0
- package/dist/theory/index.cjs.map +1 -0
- package/dist/theory/index.d.cts +535 -0
- package/dist/theory/index.d.ts +535 -0
- package/dist/theory/index.js +149 -0
- package/dist/theory/index.js.map +1 -0
- package/dist/types-MGP29r3t.d.cts +30 -0
- package/dist/types-MGP29r3t.d.ts +30 -0
- package/package.json +31 -1
- package/src/analyze/arrange/index.ts +21 -0
- package/src/analyze/arrange/internal.ts +125 -0
- package/src/analyze/arrange/tension.ts +187 -0
- package/src/analyze/arrange/tracks.ts +279 -0
- package/src/{detect → analyze/detect}/index.ts +82 -23
- package/src/analyze/functional/borrowed.ts +84 -0
- package/src/analyze/functional/cadence.ts +66 -0
- package/src/analyze/functional/function.ts +184 -0
- package/src/analyze/functional/index.ts +23 -0
- package/src/analyze/functional/internal.ts +29 -0
- package/src/analyze/functional/roman.ts +393 -0
- package/src/analyze/index.ts +49 -0
- package/src/{timeline → analyze/timeline}/index.ts +87 -24
- package/src/{analysis → analyze/voice}/index.ts +81 -12
- package/src/core/index.ts +47 -0
- package/src/{interval → core/interval}/index.ts +15 -1
- package/src/{meter → core/meter}/index.ts +108 -5
- package/src/{pitch → core/pitch}/index.ts +58 -3
- package/src/{random → core/random}/index.ts +13 -1
- package/src/{tuning → core/tuning}/index.ts +28 -1
- package/src/{bass → generate/bass}/index.ts +66 -17
- package/src/{countermelody → generate/countermelody}/index.ts +54 -24
- package/src/{drums → generate/drums}/fills.ts +31 -20
- package/src/{drums → generate/drums}/index.ts +127 -9
- package/src/{drums → generate/drums}/internal.ts +7 -1
- package/src/{drums → generate/drums}/rng.ts +2 -2
- package/src/{drums → generate/drums}/swing.ts +7 -2
- package/src/{groove → generate/groove}/index.ts +118 -22
- package/src/{harmonize → generate/harmonize}/index.ts +96 -23
- package/src/generate/index.ts +59 -0
- package/src/{motif → generate/motif}/index.ts +94 -17
- package/src/{progression → generate/progression}/index.ts +61 -27
- package/src/{reharmony → generate/reharmony}/index.ts +46 -15
- package/src/{rhythm → generate/rhythm}/index.ts +47 -13
- package/src/index.ts +130 -116
- package/src/model/chord.ts +423 -0
- package/src/model/index.ts +5 -947
- package/src/model/interval.ts +82 -0
- package/src/model/key.ts +266 -0
- package/src/model/note.ts +176 -0
- package/src/model/progression.ts +161 -0
- package/src/model/shared.ts +12 -0
- package/src/{chord → theory/chord}/index.ts +85 -4
- package/src/{chordscale → theory/chordscale}/index.ts +67 -9
- package/src/{counterpoint → theory/counterpoint}/index.ts +23 -2
- package/src/{harmony → theory/harmony}/index.ts +19 -2
- package/src/theory/index.ts +107 -0
- package/src/{safety → theory/safety}/index.ts +59 -12
- package/src/theory/scale/degrees.ts +123 -0
- package/src/theory/scale/index.ts +28 -0
- package/src/theory/scale/key.ts +56 -0
- package/src/theory/scale/masks.ts +153 -0
- package/src/{spelling → theory/spelling}/index.ts +21 -3
- package/src/{symbol → theory/symbol}/index.ts +91 -36
- package/src/theory/voicing/index.ts +5 -0
- package/src/theory/voicing/internal.ts +168 -0
- package/src/theory/voicing/leading.ts +112 -0
- package/src/theory/voicing/satb.ts +195 -0
- package/src/theory/voicing/styled.ts +176 -0
- package/src/arrange/index.ts +0 -409
- package/src/functional/index.ts +0 -602
- package/src/scale/index.ts +0 -215
- package/src/voicing/index.ts +0 -523
- /package/src/{types.ts → core/types.ts} +0 -0
- /package/src/{drums → generate/drums}/beat.ts +0 -0
- /package/src/{drums → generate/drums}/euclid.ts +0 -0
- /package/src/{drums → generate/drums}/ghost.ts +0 -0
- /package/src/{drums → generate/drums}/hihat.ts +0 -0
- /package/src/{drums → generate/drums}/hit.ts +0 -0
- /package/src/{drums → generate/drums}/kick.ts +0 -0
- /package/src/{drums → generate/drums}/percussion.ts +0 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chordPitchClasses,
|
|
3
|
+
createsHiddenParallelPerfect,
|
|
4
|
+
createsParallelPerfect,
|
|
5
|
+
createsVerticalDissonance,
|
|
6
|
+
isForbiddenMelodicLeap,
|
|
7
|
+
isScaleTone
|
|
8
|
+
} from "./chunk-7Y7WF64D.js";
|
|
9
|
+
|
|
10
|
+
// src/theory/safety/index.ts
|
|
11
|
+
var NoteSafety = /* @__PURE__ */ ((NoteSafety2) => {
|
|
12
|
+
NoteSafety2[NoteSafety2["Safe"] = 0] = "Safe";
|
|
13
|
+
NoteSafety2[NoteSafety2["Warning"] = 1] = "Warning";
|
|
14
|
+
NoteSafety2[NoteSafety2["Dissonant"] = 2] = "Dissonant";
|
|
15
|
+
return NoteSafety2;
|
|
16
|
+
})(NoteSafety || {});
|
|
17
|
+
var ReasonFlag = /* @__PURE__ */ ((ReasonFlag2) => {
|
|
18
|
+
ReasonFlag2[ReasonFlag2["ChordTone"] = 1] = "ChordTone";
|
|
19
|
+
ReasonFlag2[ReasonFlag2["Tension"] = 2] = "Tension";
|
|
20
|
+
ReasonFlag2[ReasonFlag2["AvoidNote"] = 4] = "AvoidNote";
|
|
21
|
+
ReasonFlag2[ReasonFlag2["ScaleTone"] = 8] = "ScaleTone";
|
|
22
|
+
ReasonFlag2[ReasonFlag2["NonScale"] = 16] = "NonScale";
|
|
23
|
+
ReasonFlag2[ReasonFlag2["OutOfRange"] = 32] = "OutOfRange";
|
|
24
|
+
ReasonFlag2[ReasonFlag2["Tritone"] = 64] = "Tritone";
|
|
25
|
+
ReasonFlag2[ReasonFlag2["LargeLeap"] = 128] = "LargeLeap";
|
|
26
|
+
ReasonFlag2[ReasonFlag2["MinorSecond"] = 256] = "MinorSecond";
|
|
27
|
+
ReasonFlag2[ReasonFlag2["MajorSeventh"] = 512] = "MajorSeventh";
|
|
28
|
+
ReasonFlag2[ReasonFlag2["VerticalDissonance"] = 1024] = "VerticalDissonance";
|
|
29
|
+
ReasonFlag2[ReasonFlag2["ParallelPerfect"] = 2048] = "ParallelPerfect";
|
|
30
|
+
ReasonFlag2[ReasonFlag2["HiddenParallel"] = 4096] = "HiddenParallel";
|
|
31
|
+
ReasonFlag2[ReasonFlag2["VoiceCrossing"] = 8192] = "VoiceCrossing";
|
|
32
|
+
ReasonFlag2[ReasonFlag2["Suspension"] = 16384] = "Suspension";
|
|
33
|
+
ReasonFlag2[ReasonFlag2["NeedsResolution"] = 32768] = "NeedsResolution";
|
|
34
|
+
return ReasonFlag2;
|
|
35
|
+
})(ReasonFlag || {});
|
|
36
|
+
var SUGGESTION_WINDOW = 12;
|
|
37
|
+
var MAX_SUGGESTIONS = 3;
|
|
38
|
+
function pitchClass(pitch) {
|
|
39
|
+
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
40
|
+
}
|
|
41
|
+
function intervalAboveRoot(pitch, chord) {
|
|
42
|
+
return ((pitchClass(pitch) - pitchClass(chord.rootPc)) % 12 + 12) % 12;
|
|
43
|
+
}
|
|
44
|
+
function isChordMember(pitch, chord) {
|
|
45
|
+
return chordPitchClasses(chord).includes(pitchClass(pitch));
|
|
46
|
+
}
|
|
47
|
+
function avoidsFourth(chord) {
|
|
48
|
+
return chord.intervals.includes(4) && !chord.intervals.includes(5);
|
|
49
|
+
}
|
|
50
|
+
function avoidsMajorSeventh(chord) {
|
|
51
|
+
return chord.intervals.includes(4) && chord.intervals.includes(10);
|
|
52
|
+
}
|
|
53
|
+
function stepResolution(pitch, chord) {
|
|
54
|
+
for (let delta = 1; delta <= 2; delta += 1) {
|
|
55
|
+
if (isChordMember(pitch - delta, chord)) {
|
|
56
|
+
return pitch - delta;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return void 0;
|
|
60
|
+
}
|
|
61
|
+
function evaluateSafety(q) {
|
|
62
|
+
return evaluateInternal(q, true);
|
|
63
|
+
}
|
|
64
|
+
function evaluateInternal(q, collectSuggestions) {
|
|
65
|
+
let reasons = 0;
|
|
66
|
+
let level = 0 /* Safe */;
|
|
67
|
+
let resolveTo;
|
|
68
|
+
const raise = (l) => {
|
|
69
|
+
if (l > level) {
|
|
70
|
+
level = l;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const avoidSeverity = q.profile === "strict" ? 2 /* Dissonant */ : 1 /* Warning */;
|
|
74
|
+
const parallelSeverity = avoidSeverity;
|
|
75
|
+
const pitch = q.candidatePitch;
|
|
76
|
+
if (q.vocalLow !== void 0 && pitch < q.vocalLow || q.vocalHigh !== void 0 && pitch > q.vocalHigh) {
|
|
77
|
+
reasons |= 32 /* OutOfRange */;
|
|
78
|
+
raise(1 /* Warning */);
|
|
79
|
+
}
|
|
80
|
+
const inScale = isScaleTone(pitch, q.key);
|
|
81
|
+
const chord = q.chord;
|
|
82
|
+
if (chord && isChordMember(pitch, chord)) {
|
|
83
|
+
reasons |= 1 /* ChordTone */;
|
|
84
|
+
} else if (chord) {
|
|
85
|
+
const ic = intervalAboveRoot(pitch, chord);
|
|
86
|
+
const isTension = ic === 2 || ic === 5 || ic === 9;
|
|
87
|
+
const avoid = ic === 5 && avoidsFourth(chord) || ic === 11 && avoidsMajorSeventh(chord);
|
|
88
|
+
if (avoid) {
|
|
89
|
+
reasons |= 4 /* AvoidNote */;
|
|
90
|
+
if (isTension) {
|
|
91
|
+
reasons |= 2 /* Tension */;
|
|
92
|
+
}
|
|
93
|
+
reasons |= 32768 /* NeedsResolution */;
|
|
94
|
+
resolveTo = stepResolution(pitch, chord);
|
|
95
|
+
raise(avoidSeverity);
|
|
96
|
+
} else if (isTension && inScale) {
|
|
97
|
+
reasons |= 2 /* Tension */;
|
|
98
|
+
raise(1 /* Warning */);
|
|
99
|
+
} else if (inScale) {
|
|
100
|
+
reasons |= 8 /* ScaleTone */;
|
|
101
|
+
raise(1 /* Warning */);
|
|
102
|
+
} else {
|
|
103
|
+
reasons |= 16 /* NonScale */;
|
|
104
|
+
raise(2 /* Dissonant */);
|
|
105
|
+
}
|
|
106
|
+
} else if (inScale) {
|
|
107
|
+
reasons |= 8 /* ScaleTone */;
|
|
108
|
+
raise(1 /* Warning */);
|
|
109
|
+
} else {
|
|
110
|
+
reasons |= 16 /* NonScale */;
|
|
111
|
+
raise(2 /* Dissonant */);
|
|
112
|
+
}
|
|
113
|
+
if (chord && !(chord.intervals.includes(4) && chord.intervals.includes(10))) {
|
|
114
|
+
const ic = intervalAboveRoot(pitch, chord);
|
|
115
|
+
const toneOffsets = chord.intervals.map((i) => (i % 12 + 12) % 12);
|
|
116
|
+
if (toneOffsets.some((t) => Math.abs(ic - t) === 6)) {
|
|
117
|
+
reasons |= 64 /* Tritone */;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (q.prevPitch !== void 0) {
|
|
121
|
+
const pc = Math.abs(pitch - q.prevPitch) % 12;
|
|
122
|
+
if (pc === 6) {
|
|
123
|
+
reasons |= 64 /* Tritone */;
|
|
124
|
+
}
|
|
125
|
+
if (isForbiddenMelodicLeap(q.prevPitch, pitch)) {
|
|
126
|
+
reasons |= 128 /* LargeLeap */;
|
|
127
|
+
}
|
|
128
|
+
if (pc === 1) {
|
|
129
|
+
reasons |= 256 /* MinorSecond */;
|
|
130
|
+
}
|
|
131
|
+
if (pc === 11) {
|
|
132
|
+
reasons |= 512 /* MajorSeventh */;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (q.strongBeat) {
|
|
136
|
+
const twoVoice = q.otherVoices.length === 1;
|
|
137
|
+
const prev = q.prevPitch;
|
|
138
|
+
for (const ov of q.otherVoices) {
|
|
139
|
+
if (!createsVerticalDissonance(pitch, ov.pitch, twoVoice)) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
reasons |= 1024 /* VerticalDissonance */;
|
|
143
|
+
raise(2 /* Dissonant */);
|
|
144
|
+
if (prev !== void 0 && pitch === prev && ov.prevPitch !== void 0 && !createsVerticalDissonance(prev, ov.prevPitch, twoVoice)) {
|
|
145
|
+
reasons |= 16384 /* Suspension */;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (q.prevPitch !== void 0) {
|
|
150
|
+
const prev = q.prevPitch;
|
|
151
|
+
for (const ov of q.otherVoices) {
|
|
152
|
+
if (ov.prevPitch === void 0) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (createsParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {
|
|
156
|
+
reasons |= 2048 /* ParallelPerfect */;
|
|
157
|
+
raise(parallelSeverity);
|
|
158
|
+
}
|
|
159
|
+
if (createsHiddenParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {
|
|
160
|
+
reasons |= 4096 /* HiddenParallel */;
|
|
161
|
+
raise(parallelSeverity);
|
|
162
|
+
}
|
|
163
|
+
const crossedNow = pitch - ov.pitch;
|
|
164
|
+
const crossedPrev = prev - ov.prevPitch;
|
|
165
|
+
if (crossedNow !== 0 && crossedPrev !== 0 && crossedNow > 0 !== crossedPrev > 0) {
|
|
166
|
+
reasons |= 8192 /* VoiceCrossing */;
|
|
167
|
+
raise(parallelSeverity);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const result = { safety: level, reasons, rationale: describe(reasons, q) };
|
|
172
|
+
if (resolveTo !== void 0) {
|
|
173
|
+
result.resolveTo = resolveTo;
|
|
174
|
+
}
|
|
175
|
+
if (collectSuggestions && level !== 0 /* Safe */) {
|
|
176
|
+
const suggestions = findSafeNearby(q, pitch);
|
|
177
|
+
if (suggestions.length > 0) {
|
|
178
|
+
result.suggestions = suggestions;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
function findSafeNearby(q, candidate) {
|
|
184
|
+
const out = [];
|
|
185
|
+
for (let d = 1; d <= SUGGESTION_WINDOW; d += 1) {
|
|
186
|
+
for (const p of [candidate - d, candidate + d]) {
|
|
187
|
+
if (evaluateInternal({ ...q, candidatePitch: p }, false).safety === 0 /* Safe */) {
|
|
188
|
+
out.push(p);
|
|
189
|
+
if (out.length >= MAX_SUGGESTIONS) {
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
function describe(reasons, q) {
|
|
198
|
+
if (reasons & 32 /* OutOfRange */) {
|
|
199
|
+
return "Outside the target vocal range";
|
|
200
|
+
}
|
|
201
|
+
if (reasons & 16384 /* Suspension */) {
|
|
202
|
+
return "Prepared suspension \u2014 a held consonance now dissonant, awaiting resolution";
|
|
203
|
+
}
|
|
204
|
+
if (reasons & 1024 /* VerticalDissonance */) {
|
|
205
|
+
return "Dissonant against a sounding voice on a strong beat";
|
|
206
|
+
}
|
|
207
|
+
if (reasons & 4 /* AvoidNote */) {
|
|
208
|
+
return "Avoid note \u2014 clashes with a chord tone a semitone away";
|
|
209
|
+
}
|
|
210
|
+
if (reasons & 2048 /* ParallelPerfect */) {
|
|
211
|
+
return "Parallel perfect interval with another voice";
|
|
212
|
+
}
|
|
213
|
+
if (reasons & 4096 /* HiddenParallel */) {
|
|
214
|
+
return "Hidden perfect interval by similar motion";
|
|
215
|
+
}
|
|
216
|
+
if (reasons & 8192 /* VoiceCrossing */) {
|
|
217
|
+
return "Crosses another voice";
|
|
218
|
+
}
|
|
219
|
+
if (reasons & 1 /* ChordTone */) {
|
|
220
|
+
return "Chord tone";
|
|
221
|
+
}
|
|
222
|
+
if (reasons & 2 /* Tension */) {
|
|
223
|
+
return "Chord tension";
|
|
224
|
+
}
|
|
225
|
+
if (reasons & 16 /* NonScale */) {
|
|
226
|
+
return "Outside the key";
|
|
227
|
+
}
|
|
228
|
+
if (reasons & 8 /* ScaleTone */) {
|
|
229
|
+
return "Scale tone, not in the chord";
|
|
230
|
+
}
|
|
231
|
+
return q.chord ? "Placeable" : "No chord context";
|
|
232
|
+
}
|
|
233
|
+
function enumerateSafePitches(q, pitchLow, pitchHigh) {
|
|
234
|
+
const chordTones = [];
|
|
235
|
+
const others = [];
|
|
236
|
+
if (!Number.isFinite(pitchLow) || !Number.isFinite(pitchHigh)) {
|
|
237
|
+
return [];
|
|
238
|
+
}
|
|
239
|
+
for (let pitch = pitchHigh; pitch >= pitchLow; pitch -= 1) {
|
|
240
|
+
const result = evaluateInternal({ ...q, candidatePitch: pitch }, false);
|
|
241
|
+
if (result.safety === 2 /* Dissonant */) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (result.reasons & 1 /* ChordTone */) {
|
|
245
|
+
chordTones.push(pitch);
|
|
246
|
+
} else {
|
|
247
|
+
others.push(pitch);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return [...chordTones, ...others];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export {
|
|
254
|
+
NoteSafety,
|
|
255
|
+
ReasonFlag,
|
|
256
|
+
evaluateSafety,
|
|
257
|
+
enumerateSafePitches
|
|
258
|
+
};
|
|
259
|
+
//# sourceMappingURL=chunk-A4532ZFR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/theory/safety/index.ts"],"sourcesContent":["import type { KeyScale } from '../../core/types.js';\nimport type { Chord } from '../chord/index.js';\nimport { chordPitchClasses } from '../chord/index.js';\nimport {\n createsHiddenParallelPerfect,\n createsParallelPerfect,\n createsVerticalDissonance,\n isForbiddenMelodicLeap,\n} from '../counterpoint/index.js';\nimport { isScaleTone } from '../scale/index.js';\n\n/**\n * Severity policy: `strict` counterpoint vs. lenient `pop` voice-leading.\n *\n * @category Arrangement & Analysis\n */\nexport type SafetyProfile = 'strict' | 'pop';\n\n/**\n * Overall placeability verdict for a candidate pitch.\n *\n * @category Arrangement & Analysis\n */\nexport enum NoteSafety {\n Safe = 0,\n Warning = 1,\n Dissonant = 2,\n}\n\n/**\n * Bit flags describing why a pitch received its verdict.\n *\n * @category Arrangement & Analysis\n */\nexport enum ReasonFlag {\n ChordTone = 1 << 0,\n Tension = 1 << 1,\n AvoidNote = 1 << 2,\n ScaleTone = 1 << 3,\n NonScale = 1 << 4,\n OutOfRange = 1 << 5,\n Tritone = 1 << 6,\n LargeLeap = 1 << 7,\n MinorSecond = 1 << 8,\n MajorSeventh = 1 << 9,\n VerticalDissonance = 1 << 10,\n ParallelPerfect = 1 << 11,\n HiddenParallel = 1 << 12,\n VoiceCrossing = 1 << 13,\n Suspension = 1 << 14,\n NeedsResolution = 1 << 15,\n}\n\n/**\n * A neighbouring voice's current and previous pitch.\n *\n * @category Arrangement & Analysis\n */\nexport type VoiceSnapshot = {\n pitch: number;\n prevPitch?: number;\n};\n\n/**\n * Everything needed to evaluate one candidate pitch.\n *\n * @category Arrangement & Analysis\n */\nexport type SafetyQuery = {\n profile: SafetyProfile;\n candidatePitch: number;\n prevPitch?: number;\n chord: Chord | null;\n key: KeyScale;\n otherVoices: VoiceSnapshot[];\n strongBeat: boolean;\n vocalLow?: number;\n vocalHigh?: number;\n};\n\n/**\n * The verdict, reason flags, and optional resolution guidance.\n *\n * @category Arrangement & Analysis\n */\nexport type SafetyResult = {\n safety: NoteSafety;\n reasons: number;\n resolveTo?: number;\n suggestions?: number[];\n rationale?: string;\n};\n\n/** Semitone radius searched around a rejected candidate for safe alternatives. */\nconst SUGGESTION_WINDOW = 12;\n/** Maximum number of alternative pitches returned in `SafetyResult.suggestions`. */\nconst MAX_SUGGESTIONS = 3;\n\nfunction pitchClass(pitch: number): number {\n return ((Math.trunc(pitch) % 12) + 12) % 12;\n}\n\nfunction intervalAboveRoot(pitch: number, chord: Chord): number {\n return (((pitchClass(pitch) - pitchClass(chord.rootPc)) % 12) + 12) % 12;\n}\n\nfunction isChordMember(pitch: number, chord: Chord): boolean {\n return chordPitchClasses(chord).includes(pitchClass(pitch));\n}\n\n/** Whether the perfect fourth is an avoid note over this chord. */\nfunction avoidsFourth(chord: Chord): boolean {\n return chord.intervals.includes(4) && !chord.intervals.includes(5);\n}\n\n/** Whether the major seventh is an avoid note over this chord (dominant). */\nfunction avoidsMajorSeventh(chord: Chord): boolean {\n return chord.intervals.includes(4) && chord.intervals.includes(10);\n}\n\n/** Nearest chord tone at or below the candidate, within two semitones. */\nfunction stepResolution(pitch: number, chord: Chord): number | undefined {\n for (let delta = 1; delta <= 2; delta += 1) {\n if (isChordMember(pitch - delta, chord)) {\n return pitch - delta;\n }\n }\n return undefined;\n}\n\n/**\n * Evaluate the safety of placing a candidate pitch in a voice.\n *\n * Reasons are merged from harmonic context (chord tone / tension / avoid / scale)\n * and counterpoint (vertical dissonance, parallels, voice crossing). The `strict`\n * profile treats avoid notes and parallels as dissonant; `pop` treats them as\n * warnings. The final verdict is the worst severity among the reasons.\n *\n * @param q The candidate and its harmonic/voice-leading context.\n * @returns The verdict, reason bitmask, and optional resolution guidance.\n * @example\n * ```ts\n * import { evaluateSafety, makeChord, majorKey, NoteSafety } from '@libraz/libcantus';\n * const result = evaluateSafety({\n * profile: 'pop',\n * candidatePitch: 60, // C over a C major chord\n * chord: makeChord(0, 'maj'),\n * key: majorKey(0),\n * otherVoices: [],\n * strongBeat: true,\n * });\n * result.safety === NoteSafety.Safe; // true — C is a chord tone\n * ```\n * @category Arrangement & Analysis\n */\nexport function evaluateSafety(q: SafetyQuery): SafetyResult {\n return evaluateInternal(q, true);\n}\n\n/**\n * Core safety evaluation.\n *\n * @param q The candidate and its harmonic/voice-leading context.\n * @param collectSuggestions Whether to search for safe alternatives on a\n * non-Safe verdict. Set false during that search itself to bound recursion.\n * @returns The verdict, reason bitmask, and optional guidance.\n */\nfunction evaluateInternal(q: SafetyQuery, collectSuggestions: boolean): SafetyResult {\n let reasons = 0;\n let level = NoteSafety.Safe;\n let resolveTo: number | undefined;\n const raise = (l: NoteSafety) => {\n if (l > level) {\n level = l;\n }\n };\n const avoidSeverity = q.profile === 'strict' ? NoteSafety.Dissonant : NoteSafety.Warning;\n const parallelSeverity = avoidSeverity;\n const pitch = q.candidatePitch;\n\n if (\n (q.vocalLow !== undefined && pitch < q.vocalLow) ||\n (q.vocalHigh !== undefined && pitch > q.vocalHigh)\n ) {\n reasons |= ReasonFlag.OutOfRange;\n raise(NoteSafety.Warning);\n }\n\n const inScale = isScaleTone(pitch, q.key);\n const chord = q.chord;\n\n if (chord && isChordMember(pitch, chord)) {\n reasons |= ReasonFlag.ChordTone;\n } else if (chord) {\n const ic = intervalAboveRoot(pitch, chord);\n const isTension = ic === 2 || ic === 5 || ic === 9;\n const avoid = (ic === 5 && avoidsFourth(chord)) || (ic === 11 && avoidsMajorSeventh(chord));\n if (avoid) {\n reasons |= ReasonFlag.AvoidNote;\n if (isTension) {\n reasons |= ReasonFlag.Tension;\n }\n reasons |= ReasonFlag.NeedsResolution;\n resolveTo = stepResolution(pitch, chord);\n raise(avoidSeverity);\n } else if (isTension && inScale) {\n reasons |= ReasonFlag.Tension;\n raise(NoteSafety.Warning);\n } else if (inScale) {\n reasons |= ReasonFlag.ScaleTone;\n raise(NoteSafety.Warning);\n } else {\n reasons |= ReasonFlag.NonScale;\n raise(NoteSafety.Dissonant);\n }\n } else if (inScale) {\n reasons |= ReasonFlag.ScaleTone;\n raise(NoteSafety.Warning);\n } else {\n reasons |= ReasonFlag.NonScale;\n raise(NoteSafety.Dissonant);\n }\n\n // Harmonic tritone against a chord tone (except in dominant chords, where the\n // tritone is a defining, stable colour).\n if (chord && !(chord.intervals.includes(4) && chord.intervals.includes(10))) {\n const ic = intervalAboveRoot(pitch, chord);\n const toneOffsets = chord.intervals.map((i) => ((i % 12) + 12) % 12);\n if (toneOffsets.some((t) => Math.abs(ic - t) === 6)) {\n reasons |= ReasonFlag.Tritone;\n }\n }\n\n if (q.prevPitch !== undefined) {\n // Quality flags reduce the melodic interval to its pitch-class interval so\n // compound intervals are treated uniformly with their simple form. The\n // forbidden-leap flag defers to the counterpoint rule, which preserves the\n // octave boundary (an octave leap is allowed, wider leaps are not).\n const pc = Math.abs(pitch - q.prevPitch) % 12;\n if (pc === 6) {\n reasons |= ReasonFlag.Tritone;\n }\n if (isForbiddenMelodicLeap(q.prevPitch, pitch)) {\n reasons |= ReasonFlag.LargeLeap;\n }\n if (pc === 1) {\n reasons |= ReasonFlag.MinorSecond;\n }\n if (pc === 11) {\n reasons |= ReasonFlag.MajorSeventh;\n }\n }\n\n if (q.strongBeat) {\n const twoVoice = q.otherVoices.length === 1;\n const prev = q.prevPitch;\n for (const ov of q.otherVoices) {\n if (!createsVerticalDissonance(pitch, ov.pitch, twoVoice)) {\n continue;\n }\n reasons |= ReasonFlag.VerticalDissonance;\n raise(NoteSafety.Dissonant);\n // A held pitch that was consonant on the previous step and is now\n // dissonant against the same voice is a prepared suspension.\n if (\n prev !== undefined &&\n pitch === prev &&\n ov.prevPitch !== undefined &&\n !createsVerticalDissonance(prev, ov.prevPitch, twoVoice)\n ) {\n reasons |= ReasonFlag.Suspension;\n }\n }\n }\n\n if (q.prevPitch !== undefined) {\n const prev = q.prevPitch;\n for (const ov of q.otherVoices) {\n if (ov.prevPitch === undefined) {\n continue;\n }\n // createsParallelPerfect covers every perfect class (unison/octave and\n // fifth), so no separate octave check is needed.\n if (createsParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {\n reasons |= ReasonFlag.ParallelPerfect;\n raise(parallelSeverity);\n }\n if (createsHiddenParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {\n reasons |= ReasonFlag.HiddenParallel;\n raise(parallelSeverity);\n }\n const crossedNow = pitch - ov.pitch;\n const crossedPrev = prev - ov.prevPitch;\n if (crossedNow !== 0 && crossedPrev !== 0 && crossedNow > 0 !== crossedPrev > 0) {\n reasons |= ReasonFlag.VoiceCrossing;\n raise(parallelSeverity);\n }\n }\n }\n\n const result: SafetyResult = { safety: level, reasons, rationale: describe(reasons, q) };\n if (resolveTo !== undefined) {\n result.resolveTo = resolveTo;\n }\n if (collectSuggestions && level !== NoteSafety.Safe) {\n const suggestions = findSafeNearby(q, pitch);\n if (suggestions.length > 0) {\n result.suggestions = suggestions;\n }\n }\n return result;\n}\n\n/**\n * Find safe alternative pitches near a rejected candidate.\n *\n * Searches outward from the candidate within `SUGGESTION_WINDOW` semitones,\n * probing the pitch below before the one above at each distance so the result\n * is deterministic and ordered by nearness.\n *\n * @param q The safety context of the rejected candidate.\n * @param candidate The candidate pitch that was not Safe.\n * @returns Up to `MAX_SUGGESTIONS` safe pitches, nearest first.\n */\nfunction findSafeNearby(q: SafetyQuery, candidate: number): number[] {\n const out: number[] = [];\n for (let d = 1; d <= SUGGESTION_WINDOW; d += 1) {\n for (const p of [candidate - d, candidate + d]) {\n if (evaluateInternal({ ...q, candidatePitch: p }, false).safety === NoteSafety.Safe) {\n out.push(p);\n if (out.length >= MAX_SUGGESTIONS) {\n return out;\n }\n }\n }\n }\n return out;\n}\n\n/** Build a short human explanation from the reason flags. */\nfunction describe(reasons: number, q: SafetyQuery): string {\n if (reasons & ReasonFlag.OutOfRange) {\n return 'Outside the target vocal range';\n }\n if (reasons & ReasonFlag.Suspension) {\n return 'Prepared suspension — a held consonance now dissonant, awaiting resolution';\n }\n if (reasons & ReasonFlag.VerticalDissonance) {\n return 'Dissonant against a sounding voice on a strong beat';\n }\n if (reasons & ReasonFlag.AvoidNote) {\n return 'Avoid note — clashes with a chord tone a semitone away';\n }\n if (reasons & ReasonFlag.ParallelPerfect) {\n return 'Parallel perfect interval with another voice';\n }\n if (reasons & ReasonFlag.HiddenParallel) {\n return 'Hidden perfect interval by similar motion';\n }\n if (reasons & ReasonFlag.VoiceCrossing) {\n return 'Crosses another voice';\n }\n if (reasons & ReasonFlag.ChordTone) {\n return 'Chord tone';\n }\n if (reasons & ReasonFlag.Tension) {\n return 'Chord tension';\n }\n if (reasons & ReasonFlag.NonScale) {\n return 'Outside the key';\n }\n if (reasons & ReasonFlag.ScaleTone) {\n return 'Scale tone, not in the chord';\n }\n return q.chord ? 'Placeable' : 'No chord context';\n}\n\n/**\n * Enumerate placeable pitches in a range, chord tones first, descending.\n *\n * @param q The safety context minus the candidate pitch.\n * @param pitchLow Lowest MIDI pitch to consider (inclusive).\n * @param pitchHigh Highest MIDI pitch to consider (inclusive).\n * @returns Placeable pitches (non-dissonant), chord tones before others, each group descending.\n * @example\n * ```ts\n * import { enumerateSafePitches, makeChord, majorKey } from '@libraz/libcantus';\n * const pitches = enumerateSafePitches(\n * { profile: 'pop', chord: makeChord(0, 'maj'), key: majorKey(0), otherVoices: [], strongBeat: true },\n * 60,\n * 72,\n * );\n * pitches; // placeable pitches in [60, 72], chord tones first, each group descending\n * ```\n * @category Arrangement & Analysis\n */\nexport function enumerateSafePitches(\n q: Omit<SafetyQuery, 'candidatePitch'>,\n pitchLow: number,\n pitchHigh: number,\n): number[] {\n const chordTones: number[] = [];\n const others: number[] = [];\n if (!Number.isFinite(pitchLow) || !Number.isFinite(pitchHigh)) {\n return [];\n }\n for (let pitch = pitchHigh; pitch >= pitchLow; pitch -= 1) {\n const result = evaluateInternal({ ...q, candidatePitch: pitch }, false);\n if (result.safety === NoteSafety.Dissonant) {\n continue;\n }\n if (result.reasons & ReasonFlag.ChordTone) {\n chordTones.push(pitch);\n } else {\n others.push(pitch);\n }\n }\n return [...chordTones, ...others];\n}\n"],"mappings":"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","names":["NoteSafety","ReasonFlag"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// src/core/random/index.ts
|
|
2
|
+
function createRng(seed) {
|
|
3
|
+
let state = seed >>> 0;
|
|
4
|
+
const next = () => {
|
|
5
|
+
state = state + 1831565813 >>> 0;
|
|
6
|
+
let t = state;
|
|
7
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
8
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
9
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
10
|
+
};
|
|
11
|
+
return {
|
|
12
|
+
next,
|
|
13
|
+
prob: (p) => next() < p,
|
|
14
|
+
range: (lo, hi) => lo + Math.floor(next() * (hi - lo + 1)),
|
|
15
|
+
float: (lo, hi) => lo + next() * (hi - lo)
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
createRng
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=chunk-BHUT2S5G.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/random/index.ts"],"sourcesContent":["/**\n * Deterministic seeded pseudo-random number generation shared across the\n * generative modules (rhythm, drums, bass, groove, counter-melody). A single\n * mulberry32 core exposes the small set of samplers those modules need; given a\n * seed the stream is fully reproducible.\n */\n\n/**\n * A deterministic PRNG with the sampling helpers the generators need.\n *\n * @category Utilities\n */\nexport type Rng = {\n /** Next float in [0, 1). */\n next: () => number;\n /** True with probability `p`. */\n prob: (p: number) => boolean;\n /** Integer in the inclusive range [lo, hi]. */\n range: (lo: number, hi: number) => number;\n /** Float in [lo, hi). */\n float: (lo: number, hi: number) => number;\n};\n\n/**\n * Create a seeded PRNG (mulberry32) exposing the shared samplers.\n *\n * @param seed The 32-bit seed; the same seed always yields the same stream.\n * @returns The seeded generator.\n * @example\n * ```ts\n * import { createRng } from '@libraz/libcantus';\n * const rng = createRng(42);\n * rng.next(); // deterministic float in [0, 1)\n * rng.range(1, 6); // deterministic integer in [1, 6]\n * ```\n * @category Utilities\n */\nexport function createRng(seed: number): Rng {\n let state = seed >>> 0;\n const next = () => {\n state = (state + 0x6d2b79f5) >>> 0;\n let t = state;\n t = Math.imul(t ^ (t >>> 15), t | 1);\n t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n };\n return {\n next,\n prob: (p) => next() < p,\n range: (lo, hi) => lo + Math.floor(next() * (hi - lo + 1)),\n float: (lo, hi) => lo + next() * (hi - lo),\n };\n}\n"],"mappings":";AAqCO,SAAS,UAAU,MAAmB;AAC3C,MAAI,QAAQ,SAAS;AACrB,QAAM,OAAO,MAAM;AACjB,YAAS,QAAQ,eAAgB;AACjC,QAAI,IAAI;AACR,QAAI,KAAK,KAAK,IAAK,MAAM,IAAK,IAAI,CAAC;AACnC,SAAK,IAAI,KAAK,KAAK,IAAK,MAAM,GAAI,IAAI,EAAE;AACxC,aAAS,IAAK,MAAM,QAAS,KAAK;AAAA,EACpC;AACA,SAAO;AAAA,IACL;AAAA,IACA,MAAM,CAAC,MAAM,KAAK,IAAI;AAAA,IACtB,OAAO,CAAC,IAAI,OAAO,KAAK,KAAK,MAAM,KAAK,KAAK,KAAK,KAAK,EAAE;AAAA,IACzD,OAAO,CAAC,IAAI,OAAO,KAAK,KAAK,KAAK,KAAK;AAAA,EACzC;AACF;","names":[]}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// src/core/interval/index.ts
|
|
2
|
+
var IntervalQuality = /* @__PURE__ */ ((IntervalQuality2) => {
|
|
3
|
+
IntervalQuality2[IntervalQuality2["PerfectConsonance"] = 0] = "PerfectConsonance";
|
|
4
|
+
IntervalQuality2[IntervalQuality2["ImperfectConsonance"] = 1] = "ImperfectConsonance";
|
|
5
|
+
IntervalQuality2[IntervalQuality2["Dissonance"] = 2] = "Dissonance";
|
|
6
|
+
return IntervalQuality2;
|
|
7
|
+
})(IntervalQuality || {});
|
|
8
|
+
function simpleInterval(semitones) {
|
|
9
|
+
return Math.abs(semitones) % 12;
|
|
10
|
+
}
|
|
11
|
+
function classifyInterval(semitones, twoVoice = true) {
|
|
12
|
+
const pc = simpleInterval(semitones);
|
|
13
|
+
if (pc === 0 || pc === 7) {
|
|
14
|
+
return 0 /* PerfectConsonance */;
|
|
15
|
+
}
|
|
16
|
+
if (pc === 3 || pc === 4 || pc === 8 || pc === 9) {
|
|
17
|
+
return 1 /* ImperfectConsonance */;
|
|
18
|
+
}
|
|
19
|
+
if (pc === 5) {
|
|
20
|
+
return twoVoice ? 2 /* Dissonance */ : 1 /* ImperfectConsonance */;
|
|
21
|
+
}
|
|
22
|
+
return 2 /* Dissonance */;
|
|
23
|
+
}
|
|
24
|
+
function isPerfectInterval(semitones) {
|
|
25
|
+
const pc = simpleInterval(semitones);
|
|
26
|
+
return pc === 0 || pc === 7;
|
|
27
|
+
}
|
|
28
|
+
function isConsonantInterval(semitones, twoVoice = true) {
|
|
29
|
+
const pc = simpleInterval(semitones);
|
|
30
|
+
if (pc === 5) {
|
|
31
|
+
return !twoVoice;
|
|
32
|
+
}
|
|
33
|
+
return pc === 0 || pc === 3 || pc === 4 || pc === 7 || pc === 8 || pc === 9;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
IntervalQuality,
|
|
38
|
+
classifyInterval,
|
|
39
|
+
isPerfectInterval,
|
|
40
|
+
isConsonantInterval
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=chunk-CCBLVAPK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/interval/index.ts"],"sourcesContent":["/**\n * Counterpoint classification of a harmonic interval.\n *\n * @category Pitch & Intervals\n */\nexport enum IntervalQuality {\n PerfectConsonance = 0,\n ImperfectConsonance = 1,\n Dissonance = 2,\n}\n\n/** Reduce an interval to a simple interval class in the range [0, 11]. */\nfunction simpleInterval(semitones: number): number {\n return Math.abs(semitones) % 12;\n}\n\n/**\n * Classify an interval for counterpoint evaluation.\n *\n * The interval is reduced modulo 12, so compound intervals classify as their\n * simple equivalents. The perfect fourth (5) is context-dependent: with\n * `twoVoice` (the default) it classifies as a dissonance, matching\n * {@link isConsonantInterval}; otherwise it is an imperfect consonance.\n *\n * @param semitones Interval size in semitones (may be negative or compound).\n * @param twoVoice When true, the perfect fourth is treated as dissonant.\n * @returns The counterpoint quality of the interval.\n * @example\n * ```ts\n * import { classifyInterval, IntervalQuality } from '@libraz/libcantus';\n * classifyInterval(7); // IntervalQuality.PerfectConsonance\n * classifyInterval(5); // IntervalQuality.Dissonance (two-voice)\n * classifyInterval(5, false); // IntervalQuality.ImperfectConsonance\n * ```\n * @category Pitch & Intervals\n */\nexport function classifyInterval(semitones: number, twoVoice = true): IntervalQuality {\n const pc = simpleInterval(semitones);\n if (pc === 0 || pc === 7) {\n return IntervalQuality.PerfectConsonance;\n }\n if (pc === 3 || pc === 4 || pc === 8 || pc === 9) {\n return IntervalQuality.ImperfectConsonance;\n }\n if (pc === 5) {\n return twoVoice ? IntervalQuality.Dissonance : IntervalQuality.ImperfectConsonance;\n }\n return IntervalQuality.Dissonance;\n}\n\n/**\n * Test whether an interval is a perfect interval (unison/octave or fifth).\n *\n * @param semitones Interval size in semitones (may be negative or compound).\n * @returns True for the unison/octave (0) and the perfect fifth (7) mod 12.\n * @category Pitch & Intervals\n */\nexport function isPerfectInterval(semitones: number): boolean {\n const pc = simpleInterval(semitones);\n return pc === 0 || pc === 7;\n}\n\n/**\n * Test whether an interval is consonant.\n *\n * The consonant set is {0, 3, 4, 5, 7, 8, 9} mod 12. In two-voice counterpoint\n * the perfect fourth (5) is treated as a dissonance, so `twoVoice` set to true\n * excludes it.\n *\n * @param semitones Interval size in semitones (may be negative or compound).\n * @param twoVoice When true, the perfect fourth is treated as dissonant.\n * @returns True if the interval is consonant in the given context.\n * @category Pitch & Intervals\n */\nexport function isConsonantInterval(semitones: number, twoVoice = true): boolean {\n const pc = simpleInterval(semitones);\n if (pc === 5) {\n return !twoVoice;\n }\n return pc === 0 || pc === 3 || pc === 4 || pc === 7 || pc === 8 || pc === 9;\n}\n"],"mappings":";AAKO,IAAK,kBAAL,kBAAKA,qBAAL;AACL,EAAAA,kCAAA,uBAAoB,KAApB;AACA,EAAAA,kCAAA,yBAAsB,KAAtB;AACA,EAAAA,kCAAA,gBAAa,KAAb;AAHU,SAAAA;AAAA,GAAA;AAOZ,SAAS,eAAe,WAA2B;AACjD,SAAO,KAAK,IAAI,SAAS,IAAI;AAC/B;AAsBO,SAAS,iBAAiB,WAAmB,WAAW,MAAuB;AACpF,QAAM,KAAK,eAAe,SAAS;AACnC,MAAI,OAAO,KAAK,OAAO,GAAG;AACxB,WAAO;AAAA,EACT;AACA,MAAI,OAAO,KAAK,OAAO,KAAK,OAAO,KAAK,OAAO,GAAG;AAChD,WAAO;AAAA,EACT;AACA,MAAI,OAAO,GAAG;AACZ,WAAO,WAAW,qBAA6B;AAAA,EACjD;AACA,SAAO;AACT;AASO,SAAS,kBAAkB,WAA4B;AAC5D,QAAM,KAAK,eAAe,SAAS;AACnC,SAAO,OAAO,KAAK,OAAO;AAC5B;AAcO,SAAS,oBAAoB,WAAmB,WAAW,MAAe;AAC/E,QAAM,KAAK,eAAe,SAAS;AACnC,MAAI,OAAO,GAAG;AACZ,WAAO,CAAC;AAAA,EACV;AACA,SAAO,OAAO,KAAK,OAAO,KAAK,OAAO,KAAK,OAAO,KAAK,OAAO,KAAK,OAAO;AAC5E;","names":["IntervalQuality"]}
|