@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,653 @@
|
|
|
1
|
+
import {
|
|
2
|
+
beatsPerBar,
|
|
3
|
+
isStrongBeat,
|
|
4
|
+
metricWeight,
|
|
5
|
+
parseTimeSignature
|
|
6
|
+
} from "./chunk-MSWFZF7J.js";
|
|
7
|
+
import {
|
|
8
|
+
detectCadence,
|
|
9
|
+
detectChord,
|
|
10
|
+
detectChordBest,
|
|
11
|
+
detectKey,
|
|
12
|
+
functionOf
|
|
13
|
+
} from "./chunk-LN7ZZMD2.js";
|
|
14
|
+
import {
|
|
15
|
+
evaluateSafety
|
|
16
|
+
} from "./chunk-A4532ZFR.js";
|
|
17
|
+
import {
|
|
18
|
+
chordPitchClasses,
|
|
19
|
+
chordToneRole,
|
|
20
|
+
createsVerticalDissonance,
|
|
21
|
+
isLeadingToneResolution,
|
|
22
|
+
isScaleTone,
|
|
23
|
+
majorKey,
|
|
24
|
+
makeChord
|
|
25
|
+
} from "./chunk-7Y7WF64D.js";
|
|
26
|
+
|
|
27
|
+
// src/analyze/timeline/index.ts
|
|
28
|
+
function chordTimelineFromChords(chords, totalBeats) {
|
|
29
|
+
const sorted = [...chords].sort((a, b) => a.startBeat - b.startBeat);
|
|
30
|
+
const segments = sorted.map((gc, i) => {
|
|
31
|
+
const next = sorted[i + 1];
|
|
32
|
+
const endBeat = Math.max(gc.startBeat, next ? next.startBeat : totalBeats);
|
|
33
|
+
return {
|
|
34
|
+
startBeat: gc.startBeat,
|
|
35
|
+
endBeat,
|
|
36
|
+
chord: makeChord(gc.rootPc, gc.quality, gc.bassPc)
|
|
37
|
+
};
|
|
38
|
+
}).filter((seg) => seg.endBeat > seg.startBeat);
|
|
39
|
+
return { at: segmentLookup(segments), segments };
|
|
40
|
+
}
|
|
41
|
+
function segmentLookup(segments) {
|
|
42
|
+
return (beat) => {
|
|
43
|
+
for (const seg of segments) {
|
|
44
|
+
if (beat >= seg.startBeat && beat < seg.endBeat) {
|
|
45
|
+
return seg.chord;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
var EPS = 1e-9;
|
|
52
|
+
var NOISE_THRESHOLD_RATIO = 0.2;
|
|
53
|
+
var MAX_DETECTION_PCS = 6;
|
|
54
|
+
var FALLBACK_PCS = 3;
|
|
55
|
+
var INEXACT_CONFIDENCE_FACTOR = 0.85;
|
|
56
|
+
var DIATONIC_BONUS = 0.5;
|
|
57
|
+
var EXACT_BONUS = 0.5;
|
|
58
|
+
var MISMATCH_PENALTY = 0.3;
|
|
59
|
+
function pitchClass(value) {
|
|
60
|
+
return (Math.trunc(value) % 12 + 12) % 12;
|
|
61
|
+
}
|
|
62
|
+
function scoreMatch(match, weights, maxWeight, key) {
|
|
63
|
+
const rootWeight = maxWeight > 0 ? (weights[match.rootPc] ?? 0) / maxWeight : 0;
|
|
64
|
+
const tones = chordPitchClasses(makeChord(match.rootPc, match.quality));
|
|
65
|
+
let score = rootWeight;
|
|
66
|
+
if (tones.every((pc) => isScaleTone(pc, key))) {
|
|
67
|
+
score += DIATONIC_BONUS;
|
|
68
|
+
}
|
|
69
|
+
if (match.exact) {
|
|
70
|
+
score += EXACT_BONUS;
|
|
71
|
+
}
|
|
72
|
+
score -= MISMATCH_PENALTY * (match.extraPcs.length + match.missingPcs.length);
|
|
73
|
+
return score;
|
|
74
|
+
}
|
|
75
|
+
function chordConfidence(chord, weights, totalWeight, exact) {
|
|
76
|
+
const tones = new Set(chordPitchClasses(chord));
|
|
77
|
+
let chordWeight = 0;
|
|
78
|
+
for (let pc = 0; pc < 12; pc += 1) {
|
|
79
|
+
if (tones.has(pc)) {
|
|
80
|
+
chordWeight += weights[pc] ?? 0;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const raw = totalWeight > 0 ? chordWeight / totalWeight : 0;
|
|
84
|
+
const confidence = Math.min(1, Math.max(0, raw));
|
|
85
|
+
return exact ? confidence : confidence * INEXACT_CONFIDENCE_FACTOR;
|
|
86
|
+
}
|
|
87
|
+
function analyzeWindow(notes, windowStart, windowEnd, ts, key) {
|
|
88
|
+
const weights = new Array(12).fill(0);
|
|
89
|
+
let lowestPitch = Number.POSITIVE_INFINITY;
|
|
90
|
+
for (const note of notes) {
|
|
91
|
+
const noteEnd = note.startBeat + note.durationBeat;
|
|
92
|
+
const overlap = Math.min(noteEnd, windowEnd) - Math.max(note.startBeat, windowStart);
|
|
93
|
+
if (overlap <= EPS) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const velocityFactor = note.velocity !== void 0 ? note.velocity / 127 : 1;
|
|
97
|
+
const onsetInWindow = note.startBeat >= windowStart - EPS && note.startBeat < windowEnd - EPS;
|
|
98
|
+
const accent = onsetInWindow ? 1 + metricWeight(note.startBeat, ts) / 3 : 1;
|
|
99
|
+
const pc = pitchClass(note.pitch);
|
|
100
|
+
weights[pc] = (weights[pc] ?? 0) + overlap * velocityFactor * accent;
|
|
101
|
+
if (note.pitch < lowestPitch) {
|
|
102
|
+
lowestPitch = note.pitch;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
let totalWeight = 0;
|
|
106
|
+
let maxWeight = 0;
|
|
107
|
+
for (const w of weights) {
|
|
108
|
+
totalWeight += w;
|
|
109
|
+
maxWeight = Math.max(maxWeight, w);
|
|
110
|
+
}
|
|
111
|
+
if (totalWeight <= EPS) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const ranked = weights.map((weight, pc) => ({ pc, weight })).filter(({ weight }) => weight > 0).sort((a, b) => b.weight - a.weight || a.pc - b.pc);
|
|
115
|
+
const selected = ranked.filter(({ weight }) => weight >= maxWeight * NOISE_THRESHOLD_RATIO).slice(0, MAX_DETECTION_PCS).map(({ pc }) => pc);
|
|
116
|
+
const bassPc = pitchClass(lowestPitch);
|
|
117
|
+
const anchorPc = selected.includes(bassPc) ? bassPc : selected[0] ?? bassPc;
|
|
118
|
+
const detectionPitches = selected.map((pc) => pc === anchorPc ? pc : pc + 12);
|
|
119
|
+
const matches = detectChord(detectionPitches);
|
|
120
|
+
let bestMatch = null;
|
|
121
|
+
let bestScore = Number.NEGATIVE_INFINITY;
|
|
122
|
+
for (const match of matches) {
|
|
123
|
+
const score = scoreMatch(match, weights, maxWeight, key);
|
|
124
|
+
if (score > bestScore) {
|
|
125
|
+
bestScore = score;
|
|
126
|
+
bestMatch = match;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (bestMatch) {
|
|
130
|
+
const chord = makeChord(bestMatch.rootPc, bestMatch.quality, bestMatch.bassPc);
|
|
131
|
+
return { chord, confidence: chordConfidence(chord, weights, totalWeight, bestMatch.exact) };
|
|
132
|
+
}
|
|
133
|
+
const fallback = detectChordBest(selected.slice(0, FALLBACK_PCS));
|
|
134
|
+
if (fallback) {
|
|
135
|
+
return { chord: fallback, confidence: chordConfidence(fallback, weights, totalWeight, false) };
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
function sameChord(a, b) {
|
|
140
|
+
return a.rootPc === b.rootPc && a.quality === b.quality && a.bassPc === b.bassPc;
|
|
141
|
+
}
|
|
142
|
+
function chordTimelineFromNotes(notes, opts = {}) {
|
|
143
|
+
const ts = opts.ts ?? parseTimeSignature("4/4");
|
|
144
|
+
const harmonicRhythm = opts.harmonicRhythm ?? beatsPerBar(ts);
|
|
145
|
+
if (!(harmonicRhythm > 0)) {
|
|
146
|
+
throw new Error(`Invalid harmonic rhythm: ${harmonicRhythm}`);
|
|
147
|
+
}
|
|
148
|
+
const sounding = notes.filter((note) => note.durationBeat > 0);
|
|
149
|
+
const lastNoteEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
|
|
150
|
+
const totalBeats = opts.totalBeats ?? lastNoteEnd;
|
|
151
|
+
const key = opts.key ?? detectKey(sounding.map((n) => n.pitch))[0]?.key ?? majorKey(0);
|
|
152
|
+
const segments = [];
|
|
153
|
+
const segmentConfidence = [];
|
|
154
|
+
const windowCount = Math.max(0, Math.ceil(totalBeats / harmonicRhythm - EPS));
|
|
155
|
+
for (let i = 0; i < windowCount; i += 1) {
|
|
156
|
+
const start = i * harmonicRhythm;
|
|
157
|
+
const end = Math.min(start + harmonicRhythm, totalBeats);
|
|
158
|
+
const inferred = analyzeWindow(sounding, start, end, ts, key);
|
|
159
|
+
if (!inferred) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const last = segments[segments.length - 1];
|
|
163
|
+
if (last && sameChord(last.chord, inferred.chord) && Math.abs(last.endBeat - start) < EPS) {
|
|
164
|
+
const lastLength = last.endBeat - last.startBeat;
|
|
165
|
+
const length = end - start;
|
|
166
|
+
const lastConfidence = segmentConfidence[segmentConfidence.length - 1] ?? 0;
|
|
167
|
+
segmentConfidence[segmentConfidence.length - 1] = (lastConfidence * lastLength + inferred.confidence * length) / (lastLength + length);
|
|
168
|
+
last.endBeat = end;
|
|
169
|
+
} else {
|
|
170
|
+
segments.push({ startBeat: start, endBeat: end, chord: inferred.chord });
|
|
171
|
+
segmentConfidence.push(inferred.confidence);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
timeline: { at: segmentLookup(segments), segments },
|
|
176
|
+
key,
|
|
177
|
+
segmentConfidence
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function detectCadences(timeline, key) {
|
|
181
|
+
const hits = [];
|
|
182
|
+
for (let i = 1; i < timeline.segments.length; i += 1) {
|
|
183
|
+
const prev = timeline.segments[i - 1];
|
|
184
|
+
const cur = timeline.segments[i];
|
|
185
|
+
if (!prev || !cur) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (Math.abs(cur.startBeat - prev.endBeat) > EPS) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const type = detectCadence(prev.chord, cur.chord, key);
|
|
192
|
+
if (type !== null) {
|
|
193
|
+
hits.push({ atBeat: cur.startBeat, type, from: prev.chord, to: cur.chord });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return hits;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/analyze/arrange/internal.ts
|
|
200
|
+
var EPS2 = 1e-9;
|
|
201
|
+
function covers(note, beat) {
|
|
202
|
+
return beat >= note.startBeat - EPS2 && beat < note.endBeat - EPS2;
|
|
203
|
+
}
|
|
204
|
+
function poolNotes(tracks) {
|
|
205
|
+
const notes = [];
|
|
206
|
+
for (const track of tracks) {
|
|
207
|
+
for (const note of track.notes) {
|
|
208
|
+
if (note.durationBeat > 0) {
|
|
209
|
+
notes.push(note);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return notes;
|
|
214
|
+
}
|
|
215
|
+
function splitIntoSubVoices(ordered) {
|
|
216
|
+
const lanes = [];
|
|
217
|
+
for (const note of ordered) {
|
|
218
|
+
const lane = lanes.find((candidate) => {
|
|
219
|
+
const last = candidate[candidate.length - 1];
|
|
220
|
+
return last !== void 0 && last.startBeat + last.durationBeat <= note.startBeat + EPS2;
|
|
221
|
+
});
|
|
222
|
+
if (lane) {
|
|
223
|
+
lane.push(note);
|
|
224
|
+
} else {
|
|
225
|
+
lanes.push([note]);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return lanes;
|
|
229
|
+
}
|
|
230
|
+
function prepareTracks(tracks) {
|
|
231
|
+
let nextId = 0;
|
|
232
|
+
return tracks.map((track, index) => {
|
|
233
|
+
const ordered = track.notes.filter((note) => note.durationBeat > 0).sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch).map((note) => ({ ...note, id: nextId++ }));
|
|
234
|
+
const voices = splitIntoSubVoices(ordered).map((voice) => ({
|
|
235
|
+
voice,
|
|
236
|
+
sounding: voice.map((note, i) => {
|
|
237
|
+
const prev = i > 0 ? voice[i - 1] : void 0;
|
|
238
|
+
return {
|
|
239
|
+
pitch: note.pitch,
|
|
240
|
+
prevPitch: prev?.pitch,
|
|
241
|
+
startBeat: note.startBeat,
|
|
242
|
+
endBeat: note.startBeat + note.durationBeat
|
|
243
|
+
};
|
|
244
|
+
})
|
|
245
|
+
}));
|
|
246
|
+
return {
|
|
247
|
+
name: track.name ?? `track ${index + 1}`,
|
|
248
|
+
role: track.role ?? "other",
|
|
249
|
+
voices
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/analyze/arrange/tension.ts
|
|
255
|
+
var FUNCTION_TENSION = {
|
|
256
|
+
tonic: 0,
|
|
257
|
+
subdominant: 0.5,
|
|
258
|
+
dominant: 1
|
|
259
|
+
};
|
|
260
|
+
var FUNCTION_WEIGHT = 0.5;
|
|
261
|
+
var DISSONANCE_WEIGHT = 0.35;
|
|
262
|
+
var SPAN_WEIGHT = 0.15;
|
|
263
|
+
var SPAN_SATURATION = 24;
|
|
264
|
+
function pitchClass2(pitch) {
|
|
265
|
+
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
266
|
+
}
|
|
267
|
+
function clamp01(value) {
|
|
268
|
+
return Math.min(1, Math.max(0, value));
|
|
269
|
+
}
|
|
270
|
+
function tensionCurve(tracks, opts = {}) {
|
|
271
|
+
const ts = opts.ts ?? parseTimeSignature("4/4");
|
|
272
|
+
const profile = opts.profile ?? "pop";
|
|
273
|
+
const step = opts.step ?? 1;
|
|
274
|
+
if (!(step > 0)) {
|
|
275
|
+
throw new Error(`Invalid tension sampling step: ${step}`);
|
|
276
|
+
}
|
|
277
|
+
const pooled = poolNotes(tracks);
|
|
278
|
+
const totalBeats = pooled.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
|
|
279
|
+
const { timeline, key } = chordTimelineFromNotes(pooled, {
|
|
280
|
+
key: opts.key,
|
|
281
|
+
ts,
|
|
282
|
+
harmonicRhythm: opts.harmonicRhythm
|
|
283
|
+
});
|
|
284
|
+
const prepared = prepareTracks(tracks);
|
|
285
|
+
const points = [];
|
|
286
|
+
const sampleCount = Math.max(0, Math.ceil(totalBeats / step - EPS2));
|
|
287
|
+
for (let i = 0; i < sampleCount; i += 1) {
|
|
288
|
+
const beat = i * step;
|
|
289
|
+
points.push({ beat, tension: sampleTension(prepared, timeline, key, ts, profile, beat) });
|
|
290
|
+
}
|
|
291
|
+
return points;
|
|
292
|
+
}
|
|
293
|
+
function allSounding(prepared, beat) {
|
|
294
|
+
const out = [];
|
|
295
|
+
for (let t = 0; t < prepared.length; t += 1) {
|
|
296
|
+
const track = prepared[t];
|
|
297
|
+
if (!track) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
for (const subVoice of track.voices) {
|
|
301
|
+
for (const note of subVoice.sounding) {
|
|
302
|
+
if (covers(note, beat)) {
|
|
303
|
+
out.push({ pitch: note.pitch, track: t });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return out;
|
|
309
|
+
}
|
|
310
|
+
function sampleTension(prepared, timeline, key, ts, profile, beat) {
|
|
311
|
+
const sounding = allSounding(prepared, beat);
|
|
312
|
+
if (sounding.length === 0) {
|
|
313
|
+
return 0;
|
|
314
|
+
}
|
|
315
|
+
const chord = timeline.at(beat);
|
|
316
|
+
const functionScore = chord ? FUNCTION_TENSION[functionOf(chord, key)] : 0;
|
|
317
|
+
const chordPcs = chord ? new Set(chordPitchClasses(chord)) : null;
|
|
318
|
+
const strongBeat = isStrongBeat(beat, ts);
|
|
319
|
+
let nonChord = 0;
|
|
320
|
+
let dissonant = 0;
|
|
321
|
+
for (let i = 0; i < sounding.length; i += 1) {
|
|
322
|
+
const voice = sounding[i];
|
|
323
|
+
if (!voice) {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (chordPcs && !chordPcs.has(pitchClass2(voice.pitch))) {
|
|
327
|
+
nonChord += 1;
|
|
328
|
+
}
|
|
329
|
+
const others = sounding.filter((_, j) => j !== i).map((s) => ({ pitch: s.pitch }));
|
|
330
|
+
const result = evaluateSafety({
|
|
331
|
+
profile,
|
|
332
|
+
candidatePitch: voice.pitch,
|
|
333
|
+
chord,
|
|
334
|
+
key,
|
|
335
|
+
otherVoices: others,
|
|
336
|
+
strongBeat
|
|
337
|
+
});
|
|
338
|
+
if (result.safety === 2 /* Dissonant */) {
|
|
339
|
+
dissonant += 1;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const dissonanceScore = Math.max(nonChord, dissonant) / sounding.length;
|
|
343
|
+
let low = Number.POSITIVE_INFINITY;
|
|
344
|
+
let high = Number.NEGATIVE_INFINITY;
|
|
345
|
+
for (const voice of sounding) {
|
|
346
|
+
low = Math.min(low, voice.pitch);
|
|
347
|
+
high = Math.max(high, voice.pitch);
|
|
348
|
+
}
|
|
349
|
+
const spanScore = Math.min(1, (high - low) / SPAN_SATURATION);
|
|
350
|
+
return clamp01(
|
|
351
|
+
FUNCTION_WEIGHT * functionScore + DISSONANCE_WEIGHT * dissonanceScore + SPAN_WEIGHT * spanScore
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/analyze/voice/index.ts
|
|
356
|
+
var EPS3 = 1e-9;
|
|
357
|
+
function pitchClass3(pitch) {
|
|
358
|
+
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
359
|
+
}
|
|
360
|
+
function intervalAboveRoot(pitch, chord) {
|
|
361
|
+
return ((pitchClass3(pitch) - pitchClass3(chord.rootPc)) % 12 + 12) % 12;
|
|
362
|
+
}
|
|
363
|
+
function intervalAboveBass(pitch, others, chord) {
|
|
364
|
+
let bass = Number.POSITIVE_INFINITY;
|
|
365
|
+
for (const ov of others) {
|
|
366
|
+
bass = Math.min(bass, ov.pitch);
|
|
367
|
+
}
|
|
368
|
+
if (Number.isFinite(bass) && bass < pitch) {
|
|
369
|
+
return ((pitch - bass) % 12 + 12) % 12;
|
|
370
|
+
}
|
|
371
|
+
const ref = chord.bassPc ?? chord.rootPc;
|
|
372
|
+
return ((pitchClass3(pitch) - pitchClass3(ref)) % 12 + 12) % 12;
|
|
373
|
+
}
|
|
374
|
+
function tensionDegree(ic) {
|
|
375
|
+
if (ic === 1 || ic === 2) {
|
|
376
|
+
return 9;
|
|
377
|
+
}
|
|
378
|
+
if (ic === 5 || ic === 6) {
|
|
379
|
+
return 11;
|
|
380
|
+
}
|
|
381
|
+
return 13;
|
|
382
|
+
}
|
|
383
|
+
function isChordMember(pitch, chord) {
|
|
384
|
+
return chord ? chordPitchClasses(chord).includes(pitchClass3(pitch)) : false;
|
|
385
|
+
}
|
|
386
|
+
function isStep(a, b) {
|
|
387
|
+
const d = Math.abs(a - b);
|
|
388
|
+
return d === 1 || d === 2;
|
|
389
|
+
}
|
|
390
|
+
function suspensionType(ic, delta) {
|
|
391
|
+
let type;
|
|
392
|
+
if (delta > 0) {
|
|
393
|
+
type = "sus2-3";
|
|
394
|
+
} else if (ic === 2) {
|
|
395
|
+
type = "sus9-8";
|
|
396
|
+
} else if (ic === 10 || ic === 11) {
|
|
397
|
+
type = "sus7-6";
|
|
398
|
+
} else {
|
|
399
|
+
type = "sus4-3";
|
|
400
|
+
}
|
|
401
|
+
return { kind: "suspension", type, resolveTo: 0 };
|
|
402
|
+
}
|
|
403
|
+
function stepResolution(pitch, chord) {
|
|
404
|
+
for (let delta = 1; delta <= 2; delta += 1) {
|
|
405
|
+
if (isChordMember(pitch - delta, chord)) {
|
|
406
|
+
return pitch - delta;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return void 0;
|
|
410
|
+
}
|
|
411
|
+
function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat) {
|
|
412
|
+
const result = [];
|
|
413
|
+
for (let i = 0; i < voice.length; i += 1) {
|
|
414
|
+
const note = voice[i];
|
|
415
|
+
if (!note) {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
const prevNote = i > 0 ? voice[i - 1] : void 0;
|
|
419
|
+
const nextNote = i + 1 < voice.length ? voice[i + 1] : void 0;
|
|
420
|
+
const prev = prevNote !== void 0 && prevNote.startBeat < note.startBeat - EPS3 ? prevNote : void 0;
|
|
421
|
+
const next = nextNote !== void 0 && nextNote.startBeat > note.startBeat + EPS3 ? nextNote : void 0;
|
|
422
|
+
const chord = chordAtBeat(note.startBeat);
|
|
423
|
+
const labels = [];
|
|
424
|
+
const member = isChordMember(note.pitch, chord);
|
|
425
|
+
let handled = false;
|
|
426
|
+
if (chord && member) {
|
|
427
|
+
const role = chordToneRole(note.pitch, chord);
|
|
428
|
+
if (role) {
|
|
429
|
+
labels.push({ kind: "chordTone", role });
|
|
430
|
+
} else {
|
|
431
|
+
const ic = intervalAboveRoot(note.pitch, chord);
|
|
432
|
+
labels.push({ kind: "tension", degree: tensionDegree(ic) });
|
|
433
|
+
}
|
|
434
|
+
handled = true;
|
|
435
|
+
}
|
|
436
|
+
if (!handled && chord && prev && next) {
|
|
437
|
+
const prevChord = chordAtBeat(prev.startBeat);
|
|
438
|
+
const prepared = prev.pitch === note.pitch && isChordMember(prev.pitch, prevChord);
|
|
439
|
+
const resolves = isStep(next.pitch, note.pitch);
|
|
440
|
+
const others = otherVoicesAtBeat(note.startBeat);
|
|
441
|
+
const verticallyDissonant = others.some(
|
|
442
|
+
(ov) => createsVerticalDissonance(note.pitch, ov.pitch, true)
|
|
443
|
+
);
|
|
444
|
+
if (prepared && verticallyDissonant && resolves) {
|
|
445
|
+
const ic = intervalAboveBass(note.pitch, others, chord);
|
|
446
|
+
const sus = suspensionType(ic, next.pitch - note.pitch);
|
|
447
|
+
sus.resolveTo = next.pitch;
|
|
448
|
+
labels.push(sus);
|
|
449
|
+
handled = true;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (!handled && chord && prev && next) {
|
|
453
|
+
const prevMember = isChordMember(prev.pitch, chordAtBeat(prev.startBeat));
|
|
454
|
+
const nextMember = isChordMember(next.pitch, chordAtBeat(next.startBeat));
|
|
455
|
+
const up = note.pitch - prev.pitch > 0;
|
|
456
|
+
const contInto = next.pitch - note.pitch > 0;
|
|
457
|
+
const sameDir = note.pitch !== prev.pitch && next.pitch !== note.pitch && up === contInto;
|
|
458
|
+
if (prevMember && nextMember && !member && isStep(note.pitch, prev.pitch) && isStep(next.pitch, note.pitch) && sameDir) {
|
|
459
|
+
labels.push({ kind: "passing" });
|
|
460
|
+
handled = true;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (!handled && chord && prev && next && !member && prev.pitch === next.pitch && isChordMember(prev.pitch, chordAtBeat(prev.startBeat)) && isStep(note.pitch, prev.pitch)) {
|
|
464
|
+
labels.push({ kind: "neighbor" });
|
|
465
|
+
handled = true;
|
|
466
|
+
}
|
|
467
|
+
if (!handled && chord && next && !member && note.pitch === next.pitch) {
|
|
468
|
+
const nextChord = chordAtBeat(next.startBeat);
|
|
469
|
+
if (isChordMember(next.pitch, nextChord)) {
|
|
470
|
+
labels.push({ kind: "anticipation" });
|
|
471
|
+
handled = true;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (!handled && chord && prev && next && !member) {
|
|
475
|
+
const prevMember = isChordMember(prev.pitch, chordAtBeat(prev.startBeat));
|
|
476
|
+
const nextMember = isChordMember(next.pitch, chordAtBeat(next.startBeat));
|
|
477
|
+
const stepFromPrev = isStep(note.pitch, prev.pitch);
|
|
478
|
+
const leapToNext = Math.abs(next.pitch - note.pitch) >= 3;
|
|
479
|
+
const opposite = note.pitch - prev.pitch > 0 !== next.pitch - note.pitch > 0;
|
|
480
|
+
if (prevMember && nextMember && stepFromPrev && leapToNext && opposite) {
|
|
481
|
+
labels.push({ kind: "escape" });
|
|
482
|
+
handled = true;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (!handled && chord && !member) {
|
|
486
|
+
const ic = intervalAboveRoot(note.pitch, chord);
|
|
487
|
+
const isTension = ic === 2 || ic === 5 || ic === 9;
|
|
488
|
+
const avoid = ic === 5 && chord.intervals.includes(4) && !chord.intervals.includes(5) || ic === 11 && chord.intervals.includes(4) && chord.intervals.includes(10);
|
|
489
|
+
if (avoid) {
|
|
490
|
+
labels.push({ kind: "avoid" });
|
|
491
|
+
const resolveTo = stepResolution(note.pitch, chord);
|
|
492
|
+
if (resolveTo !== void 0) {
|
|
493
|
+
labels.push({ kind: "needsResolution", resolveTo });
|
|
494
|
+
}
|
|
495
|
+
} else if (isTension) {
|
|
496
|
+
labels.push({ kind: "tension", degree: tensionDegree(ic) });
|
|
497
|
+
} else {
|
|
498
|
+
const resolveTo = stepResolution(note.pitch, chord);
|
|
499
|
+
if (resolveTo !== void 0) {
|
|
500
|
+
labels.push({ kind: "needsResolution", resolveTo });
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (next && isLeadingToneResolution(note.pitch, next.pitch, key)) {
|
|
505
|
+
labels.push({ kind: "leadingTone", resolveTo: next.pitch });
|
|
506
|
+
}
|
|
507
|
+
result.push({ noteId: note.id, labels, rationale: describe(labels) });
|
|
508
|
+
}
|
|
509
|
+
return result;
|
|
510
|
+
}
|
|
511
|
+
function describe(labels) {
|
|
512
|
+
const primary = labels[0];
|
|
513
|
+
if (!primary) {
|
|
514
|
+
return "Unclassified note";
|
|
515
|
+
}
|
|
516
|
+
switch (primary.kind) {
|
|
517
|
+
case "chordTone":
|
|
518
|
+
return `Chord ${primary.role}`;
|
|
519
|
+
case "tension":
|
|
520
|
+
return `${primary.degree}th tension`;
|
|
521
|
+
case "avoid":
|
|
522
|
+
return "Avoid note";
|
|
523
|
+
case "passing":
|
|
524
|
+
return "Passing tone between chord tones";
|
|
525
|
+
case "neighbor":
|
|
526
|
+
return "Neighbor tone";
|
|
527
|
+
case "suspension":
|
|
528
|
+
return `Suspension (${primary.type})`;
|
|
529
|
+
case "anticipation":
|
|
530
|
+
return "Anticipation of the next chord";
|
|
531
|
+
case "escape":
|
|
532
|
+
return "Escape tone";
|
|
533
|
+
case "needsResolution":
|
|
534
|
+
return "Unresolved dissonance";
|
|
535
|
+
case "leadingTone":
|
|
536
|
+
return "Leading tone resolving to the tonic";
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// src/analyze/arrange/tracks.ts
|
|
541
|
+
function otherVoicesSounding(prepared, excludeTrack, excludeVoice, beat) {
|
|
542
|
+
const out = [];
|
|
543
|
+
for (let t = 0; t < prepared.length; t += 1) {
|
|
544
|
+
const track = prepared[t];
|
|
545
|
+
if (!track) {
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
for (let v = 0; v < track.voices.length; v += 1) {
|
|
549
|
+
if (t === excludeTrack && v === excludeVoice) {
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
const subVoice = track.voices[v];
|
|
553
|
+
if (!subVoice) {
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
for (const note of subVoice.sounding) {
|
|
557
|
+
if (!covers(note, beat)) {
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
const snap = { pitch: note.pitch };
|
|
561
|
+
if (note.prevPitch !== void 0) {
|
|
562
|
+
snap.prevPitch = note.prevPitch;
|
|
563
|
+
}
|
|
564
|
+
out.push(snap);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return out;
|
|
569
|
+
}
|
|
570
|
+
function evaluationBeats(note, timeline) {
|
|
571
|
+
const noteEnd = note.startBeat + note.durationBeat;
|
|
572
|
+
const beats = [note.startBeat];
|
|
573
|
+
for (const segment of timeline.segments) {
|
|
574
|
+
if (segment.startBeat > note.startBeat + EPS2 && segment.startBeat < noteEnd - EPS2) {
|
|
575
|
+
beats.push(segment.startBeat);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return beats;
|
|
579
|
+
}
|
|
580
|
+
function analyzeArrangement(tracks, opts = {}) {
|
|
581
|
+
const ts = opts.ts ?? parseTimeSignature("4/4");
|
|
582
|
+
const profile = opts.profile ?? "pop";
|
|
583
|
+
const pooled = poolNotes(tracks);
|
|
584
|
+
const { timeline, key, segmentConfidence } = chordTimelineFromNotes(pooled, {
|
|
585
|
+
key: opts.key,
|
|
586
|
+
ts,
|
|
587
|
+
harmonicRhythm: opts.harmonicRhythm
|
|
588
|
+
});
|
|
589
|
+
const cadences = detectCadences(timeline, key);
|
|
590
|
+
const prepared = prepareTracks(tracks);
|
|
591
|
+
const trackAnalyses = [];
|
|
592
|
+
const conflicts = [];
|
|
593
|
+
for (let t = 0; t < prepared.length; t += 1) {
|
|
594
|
+
const track = prepared[t];
|
|
595
|
+
if (!track) {
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
const notes = [];
|
|
599
|
+
for (let v = 0; v < track.voices.length; v += 1) {
|
|
600
|
+
const subVoice = track.voices[v];
|
|
601
|
+
if (!subVoice) {
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
notes.push(
|
|
605
|
+
...analyzeVoice(
|
|
606
|
+
subVoice.voice,
|
|
607
|
+
timeline.at,
|
|
608
|
+
key,
|
|
609
|
+
(beat) => otherVoicesSounding(prepared, t, v, beat)
|
|
610
|
+
)
|
|
611
|
+
);
|
|
612
|
+
for (const note of subVoice.voice) {
|
|
613
|
+
for (const beat of evaluationBeats(note, timeline)) {
|
|
614
|
+
const result = evaluateSafety({
|
|
615
|
+
profile,
|
|
616
|
+
candidatePitch: note.pitch,
|
|
617
|
+
chord: timeline.at(beat),
|
|
618
|
+
key,
|
|
619
|
+
otherVoices: otherVoicesSounding(prepared, t, v, beat),
|
|
620
|
+
strongBeat: isStrongBeat(beat, ts)
|
|
621
|
+
});
|
|
622
|
+
if (result.safety !== 0 /* Safe */) {
|
|
623
|
+
const conflict = {
|
|
624
|
+
beat,
|
|
625
|
+
trackName: track.name,
|
|
626
|
+
pitch: note.pitch,
|
|
627
|
+
safety: result.safety,
|
|
628
|
+
reasons: result.reasons
|
|
629
|
+
};
|
|
630
|
+
if (result.rationale !== void 0) {
|
|
631
|
+
conflict.rationale = result.rationale;
|
|
632
|
+
}
|
|
633
|
+
conflicts.push(conflict);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
notes.sort((a, b) => a.noteId - b.noteId);
|
|
639
|
+
trackAnalyses.push({ name: track.name, role: track.role, notes });
|
|
640
|
+
}
|
|
641
|
+
conflicts.sort((a, b) => b.safety - a.safety || a.beat - b.beat);
|
|
642
|
+
return { key, timeline, segmentConfidence, cadences, tracks: trackAnalyses, conflicts };
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
export {
|
|
646
|
+
chordTimelineFromChords,
|
|
647
|
+
chordTimelineFromNotes,
|
|
648
|
+
detectCadences,
|
|
649
|
+
tensionCurve,
|
|
650
|
+
analyzeVoice,
|
|
651
|
+
analyzeArrangement
|
|
652
|
+
};
|
|
653
|
+
//# sourceMappingURL=chunk-5HTNMQW4.js.map
|