@libraz/libcantus 0.9.3 → 0.9.5
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 +29 -20
- package/README_ja.md +268 -0
- package/dist/analyze/index.cjs +39 -1947
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +161 -91
- package/dist/analyze/index.d.ts +161 -91
- package/dist/analyze/index.js +19 -8
- package/dist/chunk-2A2QMA74.cjs +261 -0
- package/dist/chunk-2A2QMA74.cjs.map +1 -0
- package/dist/chunk-2PTFGVD7.js +261 -0
- package/dist/chunk-2PTFGVD7.js.map +1 -0
- package/dist/chunk-357ZDPVU.cjs +162 -0
- package/dist/chunk-357ZDPVU.cjs.map +1 -0
- package/dist/chunk-3KX76RLG.js +1548 -0
- package/dist/chunk-3KX76RLG.js.map +1 -0
- package/dist/chunk-4VZ5MZAM.cjs +889 -0
- package/dist/chunk-4VZ5MZAM.cjs.map +1 -0
- package/dist/{chunk-C5XYKUVG.js → chunk-7EONYVI5.js} +471 -171
- package/dist/chunk-7EONYVI5.js.map +1 -0
- package/dist/{chunk-IT2O4XFE.js → chunk-7JEP3MFY.js} +42 -34
- package/dist/chunk-7JEP3MFY.js.map +1 -0
- package/dist/{chunk-4MHA432G.js → chunk-AID75RN3.js} +572 -250
- package/dist/chunk-AID75RN3.js.map +1 -0
- package/dist/chunk-BSLTLGM3.cjs +497 -0
- package/dist/chunk-BSLTLGM3.cjs.map +1 -0
- package/dist/chunk-CU6VG2YM.cjs +109 -0
- package/dist/chunk-CU6VG2YM.cjs.map +1 -0
- package/dist/{chunk-EH6EM37U.js → chunk-DZMHIGI4.js} +81 -59
- package/dist/chunk-DZMHIGI4.js.map +1 -0
- package/dist/{chunk-AKLYPWIN.js → chunk-EDOTEABK.js} +45 -19
- package/dist/chunk-EDOTEABK.js.map +1 -0
- package/dist/{chunk-2ZWMSJH6.js → chunk-HN2OEUN2.js} +7 -4
- package/dist/chunk-HN2OEUN2.js.map +1 -0
- package/dist/{chunk-MIWO425M.js → chunk-KBTTEX7Q.js} +128 -43
- package/dist/chunk-KBTTEX7Q.js.map +1 -0
- package/dist/chunk-KQN2ZYND.cjs +724 -0
- package/dist/chunk-KQN2ZYND.cjs.map +1 -0
- package/dist/chunk-MPTVJIVG.cjs +3176 -0
- package/dist/chunk-MPTVJIVG.cjs.map +1 -0
- package/dist/chunk-MTMZTMWM.cjs +53 -0
- package/dist/chunk-MTMZTMWM.cjs.map +1 -0
- package/dist/{chunk-CHMXELDS.js → chunk-NNB67XA2.js} +372 -197
- package/dist/chunk-NNB67XA2.js.map +1 -0
- package/dist/chunk-OL3IHHSJ.cjs +1204 -0
- package/dist/chunk-OL3IHHSJ.cjs.map +1 -0
- package/dist/chunk-PI3UXRWK.cjs +528 -0
- package/dist/chunk-PI3UXRWK.cjs.map +1 -0
- package/dist/chunk-R2VVFAIN.js +528 -0
- package/dist/chunk-R2VVFAIN.js.map +1 -0
- package/dist/chunk-T64C7ZD7.cjs +1 -0
- package/dist/chunk-T64C7ZD7.cjs.map +1 -0
- package/dist/chunk-WFDXOLUB.cjs +337 -0
- package/dist/chunk-WFDXOLUB.cjs.map +1 -0
- package/dist/chunk-WTAN4ACG.cjs +1548 -0
- package/dist/chunk-WTAN4ACG.cjs.map +1 -0
- package/dist/chunk-YFJXS3AU.cjs +33 -0
- package/dist/chunk-YFJXS3AU.cjs.map +1 -0
- package/dist/{chunk-GOAW4VYL.js → chunk-YUE6TJ4M.js} +315 -145
- package/dist/chunk-YUE6TJ4M.js.map +1 -0
- package/dist/{chunk-62QZKCJQ.js → chunk-ZGLJM4A4.js} +10 -6
- package/dist/chunk-ZGLJM4A4.js.map +1 -0
- package/dist/core/index.cjs +140 -647
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +313 -41
- package/dist/core/index.d.ts +313 -41
- package/dist/core/index.js +64 -16
- package/dist/{function-DVH4Eusr.d.ts → function-CxixTB02.d.cts} +122 -10
- package/dist/{function-DYt4iKsn.d.cts → function-DTCt675z.d.ts} +122 -10
- package/dist/generate/index.cjs +28 -4085
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +374 -66
- package/dist/generate/index.d.ts +374 -66
- package/dist/generate/index.js +23 -9
- package/dist/{index-Jc0Ryi79.d.ts → index-B2Ulxxpo.d.cts} +21 -5
- package/dist/{index-CpOsP1y9.d.cts → index-BkYJ0xMq.d.cts} +18 -17
- package/dist/{index-CxqBQtH1.d.cts → index-BuaB-ypt.d.ts} +21 -5
- package/dist/index-CAN7v-kw.d.ts +199 -0
- package/dist/{index-Bt4tOUD9.d.ts → index-COw1Ky9g.d.ts} +93 -8
- package/dist/{index-Dikst44v.d.cts → index-Cfx8xgH0.d.ts} +14 -11
- package/dist/{index-DxvARIP2.d.ts → index-DRtIhlJj.d.ts} +18 -17
- package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
- package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
- package/dist/index-alrRninU.d.cts +199 -0
- package/dist/{index-BNlGP86A.d.cts → index-oTQbYYIs.d.cts} +93 -8
- package/dist/index-tR9jTn26.d.cts +275 -0
- package/dist/index-tR9jTn26.d.ts +275 -0
- package/dist/{index-Bg5dYMmZ.d.ts → index-x2vcsduE.d.cts} +14 -11
- package/dist/index.cjs +429 -7383
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -15
- package/dist/index.d.ts +15 -15
- package/dist/index.js +121 -27
- package/dist/model/index.cjs +8 -2562
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +471 -81
- package/dist/model/index.d.ts +471 -81
- package/dist/model/index.js +6 -7
- package/dist/styled-Bl0ZK28h.d.ts +543 -0
- package/dist/styled-Crv6SA4S.d.cts +543 -0
- package/dist/theory/index.cjs +172 -1822
- package/dist/theory/index.cjs.map +1 -1
- package/dist/theory/index.d.cts +151 -233
- package/dist/theory/index.d.ts +151 -233
- package/dist/theory/index.js +30 -7
- package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
- package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
- package/package.json +34 -13
- package/dist/chunk-2ZWMSJH6.js.map +0 -1
- package/dist/chunk-4MHA432G.js.map +0 -1
- package/dist/chunk-62QZKCJQ.js.map +0 -1
- package/dist/chunk-AKLYPWIN.js.map +0 -1
- package/dist/chunk-C5XYKUVG.js.map +0 -1
- package/dist/chunk-CHMXELDS.js.map +0 -1
- package/dist/chunk-DYWFNC4A.js +0 -152
- package/dist/chunk-DYWFNC4A.js.map +0 -1
- package/dist/chunk-EH6EM37U.js.map +0 -1
- package/dist/chunk-FBQK3ZQN.js +0 -131
- package/dist/chunk-FBQK3ZQN.js.map +0 -1
- package/dist/chunk-GOAW4VYL.js.map +0 -1
- package/dist/chunk-I5R7ZBJS.js +0 -202
- package/dist/chunk-I5R7ZBJS.js.map +0 -1
- package/dist/chunk-IT2O4XFE.js.map +0 -1
- package/dist/chunk-MIWO425M.js.map +0 -1
- package/dist/chunk-SGU7SPLK.js +0 -924
- package/dist/chunk-SGU7SPLK.js.map +0 -1
- package/dist/index-CGMZNCx6.d.cts +0 -140
- package/dist/index-CGMZNCx6.d.ts +0 -140
- package/dist/index-CnZCo29u.d.cts +0 -108
- package/dist/index-JnF4v9bx.d.ts +0 -108
- package/dist/styled-DZ7GjShb.d.ts +0 -282
- package/dist/styled-ZTbdaVFM.d.cts +0 -282
- package/src/analyze/arrange/index.ts +0 -21
- package/src/analyze/arrange/internal.ts +0 -127
- package/src/analyze/arrange/tension.ts +0 -199
- package/src/analyze/arrange/tracks.ts +0 -312
- package/src/analyze/detect/index.ts +0 -272
- package/src/analyze/functional/borrowed.ts +0 -84
- package/src/analyze/functional/cadence.ts +0 -66
- package/src/analyze/functional/function.ts +0 -184
- package/src/analyze/functional/index.ts +0 -23
- package/src/analyze/functional/internal.ts +0 -29
- package/src/analyze/functional/roman.ts +0 -402
- package/src/analyze/index.ts +0 -49
- package/src/analyze/timeline/index.ts +0 -451
- package/src/analyze/voice/index.ts +0 -339
- package/src/core/event-index/index.ts +0 -104
- package/src/core/index.ts +0 -61
- package/src/core/interval/index.ts +0 -81
- package/src/core/meter/index.ts +0 -302
- package/src/core/pitch/index.ts +0 -311
- package/src/core/random/index.ts +0 -76
- package/src/core/tuning/index.ts +0 -186
- package/src/core/types.ts +0 -29
- package/src/core/validation/index.ts +0 -119
- package/src/generate/bass/index.ts +0 -373
- package/src/generate/countermelody/index.ts +0 -470
- package/src/generate/drums/beat.ts +0 -266
- package/src/generate/drums/euclid.ts +0 -65
- package/src/generate/drums/fills.ts +0 -356
- package/src/generate/drums/ghost.ts +0 -119
- package/src/generate/drums/hihat.ts +0 -285
- package/src/generate/drums/hit.ts +0 -31
- package/src/generate/drums/index.ts +0 -361
- package/src/generate/drums/internal.ts +0 -257
- package/src/generate/drums/kick.ts +0 -160
- package/src/generate/drums/percussion.ts +0 -121
- package/src/generate/drums/rng.ts +0 -7
- package/src/generate/drums/swing.ts +0 -71
- package/src/generate/groove/index.ts +0 -351
- package/src/generate/harmonize/index.ts +0 -451
- package/src/generate/index.ts +0 -58
- package/src/generate/motif/index.ts +0 -422
- package/src/generate/progression/index.ts +0 -362
- package/src/generate/reharmony/index.ts +0 -323
- package/src/generate/rhythm/index.ts +0 -175
- package/src/index.ts +0 -13
- package/src/model/chord.ts +0 -442
- package/src/model/index.ts +0 -15
- package/src/model/interval.ts +0 -82
- package/src/model/key.ts +0 -266
- package/src/model/note.ts +0 -176
- package/src/model/progression.ts +0 -161
- package/src/model/shared.ts +0 -12
- package/src/theory/chord/index.ts +0 -375
- package/src/theory/chordscale/index.ts +0 -374
- package/src/theory/counterpoint/index.ts +0 -230
- package/src/theory/harmony/index.ts +0 -77
- package/src/theory/index.ts +0 -112
- package/src/theory/safety/index.ts +0 -488
- package/src/theory/scale/degrees.ts +0 -123
- package/src/theory/scale/index.ts +0 -28
- package/src/theory/scale/key.ts +0 -56
- package/src/theory/scale/masks.ts +0 -153
- package/src/theory/spelling/index.ts +0 -249
- package/src/theory/symbol/index.ts +0 -268
- package/src/theory/voicing/index.ts +0 -5
- package/src/theory/voicing/internal.ts +0 -168
- package/src/theory/voicing/leading.ts +0 -112
- package/src/theory/voicing/satb.ts +0 -216
- package/src/theory/voicing/styled.ts +0 -199
- package/tsconfig.json +0 -23
|
@@ -4,33 +4,36 @@ import {
|
|
|
4
4
|
isStrongBeat,
|
|
5
5
|
metricWeight,
|
|
6
6
|
parseTimeSignature
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-2PTFGVD7.js";
|
|
8
8
|
import {
|
|
9
9
|
detectCadence,
|
|
10
10
|
detectChord,
|
|
11
|
-
|
|
12
|
-
detectKey,
|
|
11
|
+
detectKeyFromNotes,
|
|
13
12
|
functionOf
|
|
14
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-NNB67XA2.js";
|
|
15
14
|
import {
|
|
16
15
|
evaluateSafety
|
|
17
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-DZMHIGI4.js";
|
|
18
17
|
import {
|
|
19
18
|
chordPitchClasses,
|
|
20
19
|
chordToneRole,
|
|
21
20
|
createsVerticalDissonance,
|
|
21
|
+
intervalAboveRoot,
|
|
22
|
+
isChordMember,
|
|
22
23
|
isLeadingToneResolution,
|
|
23
24
|
isScaleTone,
|
|
24
25
|
majorKey,
|
|
25
26
|
makeChord
|
|
26
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-KBTTEX7Q.js";
|
|
27
28
|
import {
|
|
29
|
+
InvalidInputError,
|
|
28
30
|
assertFiniteNumber,
|
|
29
31
|
assertGenerationBudget,
|
|
30
32
|
assertNoteEvents,
|
|
31
33
|
assertRange,
|
|
32
|
-
assertTimeSignature
|
|
33
|
-
|
|
34
|
+
assertTimeSignature,
|
|
35
|
+
pitchClassOf
|
|
36
|
+
} from "./chunk-R2VVFAIN.js";
|
|
34
37
|
|
|
35
38
|
// src/analyze/timeline/index.ts
|
|
36
39
|
function chordTimelineFromChords(chords, totalBeats) {
|
|
@@ -60,25 +63,27 @@ function chordTimelineFromChords(chords, totalBeats) {
|
|
|
60
63
|
function segmentLookup(segments) {
|
|
61
64
|
return (beat) => {
|
|
62
65
|
assertFiniteNumber(beat, "timeline query beat");
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
let low = 0;
|
|
67
|
+
let high = segments.length;
|
|
68
|
+
while (low < high) {
|
|
69
|
+
const middle = Math.floor((low + high) / 2);
|
|
70
|
+
if ((segments[middle]?.startBeat ?? Number.POSITIVE_INFINITY) <= beat) {
|
|
71
|
+
low = middle + 1;
|
|
72
|
+
} else {
|
|
73
|
+
high = middle;
|
|
66
74
|
}
|
|
67
75
|
}
|
|
68
|
-
|
|
76
|
+
const candidate = segments[low - 1];
|
|
77
|
+
return candidate !== void 0 && beat >= candidate.startBeat && beat < candidate.endBeat ? candidate.chord : null;
|
|
69
78
|
};
|
|
70
79
|
}
|
|
71
80
|
var EPS = 1e-9;
|
|
72
81
|
var NOISE_THRESHOLD_RATIO = 0.2;
|
|
73
82
|
var MAX_DETECTION_PCS = 6;
|
|
74
|
-
var FALLBACK_PCS = 3;
|
|
75
83
|
var INEXACT_CONFIDENCE_FACTOR = 0.85;
|
|
76
84
|
var DIATONIC_BONUS = 0.5;
|
|
77
85
|
var EXACT_BONUS = 0.5;
|
|
78
86
|
var MISMATCH_PENALTY = 0.3;
|
|
79
|
-
function pitchClass(value) {
|
|
80
|
-
return (Math.trunc(value) % 12 + 12) % 12;
|
|
81
|
-
}
|
|
82
87
|
function scoreMatch(match, weights, maxWeight, key) {
|
|
83
88
|
const rootWeight = maxWeight > 0 ? (weights[match.rootPc] ?? 0) / maxWeight : 0;
|
|
84
89
|
const tones = chordPitchClasses(makeChord(match.rootPc, match.quality));
|
|
@@ -116,7 +121,7 @@ function analyzeWindow(notes, windowStart, windowEnd, ts, key) {
|
|
|
116
121
|
const velocityFactor = note.velocity !== void 0 ? note.velocity / 127 : 1;
|
|
117
122
|
const onsetInWindow = note.startBeat >= windowStart - EPS && note.startBeat < windowEnd - EPS;
|
|
118
123
|
const accent = onsetInWindow ? 1 + metricWeight(note.startBeat, ts) / 3 : 1;
|
|
119
|
-
const pc =
|
|
124
|
+
const pc = pitchClassOf(note.pitch);
|
|
120
125
|
weights[pc] = (weights[pc] ?? 0) + overlap * velocityFactor * accent;
|
|
121
126
|
if (note.pitch < lowestPitch) {
|
|
122
127
|
lowestPitch = note.pitch;
|
|
@@ -133,10 +138,10 @@ function analyzeWindow(notes, windowStart, windowEnd, ts, key) {
|
|
|
133
138
|
}
|
|
134
139
|
const ranked = weights.map((weight, pc) => ({ pc, weight })).filter(({ weight }) => weight > 0).sort((a, b) => b.weight - a.weight || a.pc - b.pc);
|
|
135
140
|
const selected = ranked.filter(({ weight }) => weight >= maxWeight * NOISE_THRESHOLD_RATIO).slice(0, MAX_DETECTION_PCS).map(({ pc }) => pc);
|
|
136
|
-
const bassPc =
|
|
137
|
-
const
|
|
138
|
-
const detectionPitches = selected.map((pc) => pc ===
|
|
139
|
-
const matches = detectChord(detectionPitches);
|
|
141
|
+
const bassPc = pitchClassOf(lowestPitch);
|
|
142
|
+
const hasBass = selected.includes(bassPc);
|
|
143
|
+
const detectionPitches = hasBass ? selected.map((pc) => pc === bassPc ? pc : pc + 12) : selected;
|
|
144
|
+
const matches = detectChord(detectionPitches, { input: hasBass ? "midi" : "pitchClass" });
|
|
140
145
|
let bestMatch = null;
|
|
141
146
|
let bestScore = Number.NEGATIVE_INFINITY;
|
|
142
147
|
for (const match of matches) {
|
|
@@ -150,10 +155,6 @@ function analyzeWindow(notes, windowStart, windowEnd, ts, key) {
|
|
|
150
155
|
const chord = makeChord(bestMatch.rootPc, bestMatch.quality, bestMatch.bassPc);
|
|
151
156
|
return { chord, confidence: chordConfidence(chord, weights, totalWeight, bestMatch.exact) };
|
|
152
157
|
}
|
|
153
|
-
const fallback = detectChordBest(selected.slice(0, FALLBACK_PCS));
|
|
154
|
-
if (fallback) {
|
|
155
|
-
return { chord: fallback, confidence: chordConfidence(fallback, weights, totalWeight, false) };
|
|
156
|
-
}
|
|
157
158
|
return null;
|
|
158
159
|
}
|
|
159
160
|
function sameChord(a, b) {
|
|
@@ -164,24 +165,28 @@ function chordTimelineFromNotes(notes, opts = {}) {
|
|
|
164
165
|
assertTimeSignature(ts);
|
|
165
166
|
const harmonicRhythm = opts.harmonicRhythm ?? beatsPerBar(ts);
|
|
166
167
|
assertRange(harmonicRhythm, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "harmonic rhythm");
|
|
167
|
-
|
|
168
|
-
|
|
168
|
+
const budget = opts.budget;
|
|
169
|
+
assertNoteEvents(notes, "timeline notes", { allowNonPositiveDuration: true, budget });
|
|
170
|
+
const soundingIndex = createNoteEventIndex(
|
|
171
|
+
notes.filter((note) => note.durationBeat > 0),
|
|
172
|
+
{ name: "timeline notes", budget }
|
|
173
|
+
);
|
|
169
174
|
const sounding = soundingIndex.notes.map(({ note }) => note);
|
|
170
175
|
const lastNoteEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
|
|
171
176
|
const totalBeats = opts.totalBeats ?? lastNoteEnd;
|
|
172
177
|
assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, "timeline totalBeats");
|
|
173
|
-
const key = opts.key ??
|
|
178
|
+
const key = opts.key ?? detectKeyFromNotes(sounding, { budget })[0]?.key ?? majorKey(0);
|
|
174
179
|
const segments = [];
|
|
175
180
|
const segmentConfidence = [];
|
|
176
181
|
const windowCount = Math.max(0, Math.ceil(totalBeats / harmonicRhythm - EPS));
|
|
177
|
-
assertGenerationBudget(windowCount, "timeline windows");
|
|
182
|
+
assertGenerationBudget(windowCount, "timeline windows", budget);
|
|
178
183
|
const windowNotes = Array.from({ length: windowCount }, () => []);
|
|
179
184
|
let memberships = 0;
|
|
180
185
|
for (const indexed of soundingIndex.notes) {
|
|
181
186
|
const first = Math.max(0, Math.floor(indexed.note.startBeat / harmonicRhythm));
|
|
182
187
|
const lastExclusive = Math.min(windowCount, Math.ceil(indexed.endBeat / harmonicRhythm));
|
|
183
188
|
memberships += Math.max(0, lastExclusive - first);
|
|
184
|
-
assertGenerationBudget(memberships, "timeline note-to-window memberships");
|
|
189
|
+
assertGenerationBudget(memberships, "timeline note-to-window memberships", budget);
|
|
185
190
|
for (let window = first; window < lastExclusive; window += 1) {
|
|
186
191
|
windowNotes[window]?.push(indexed.note);
|
|
187
192
|
}
|
|
@@ -232,12 +237,23 @@ function detectCadences(timeline, key) {
|
|
|
232
237
|
|
|
233
238
|
// src/analyze/arrange/internal.ts
|
|
234
239
|
var EPS2 = 1e-9;
|
|
240
|
+
var MAX_LANE_LEAP = 24;
|
|
235
241
|
function covers(note, beat) {
|
|
236
242
|
return beat >= note.startBeat - EPS2 && beat < note.endBeat - EPS2;
|
|
237
243
|
}
|
|
238
|
-
function
|
|
244
|
+
function isPercussion(role) {
|
|
245
|
+
return role === "drums";
|
|
246
|
+
}
|
|
247
|
+
function poolNotes(tracks, only) {
|
|
239
248
|
const notes = [];
|
|
240
|
-
for (
|
|
249
|
+
for (let index = 0; index < tracks.length; index += 1) {
|
|
250
|
+
const track = tracks[index];
|
|
251
|
+
if (track === void 0 || isPercussion(track.role)) {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (only !== void 0 && !only.has(index)) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
241
257
|
for (const note of track.notes) {
|
|
242
258
|
if (note.durationBeat > 0) {
|
|
243
259
|
notes.push(note);
|
|
@@ -248,26 +264,89 @@ function poolNotes(tracks) {
|
|
|
248
264
|
}
|
|
249
265
|
function splitIntoSubVoices(ordered) {
|
|
250
266
|
const lanes = [];
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
267
|
+
const attachNearest = (note, excluded = /* @__PURE__ */ new Set()) => {
|
|
268
|
+
let best = -1;
|
|
269
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
270
|
+
let bestEnd = Number.NEGATIVE_INFINITY;
|
|
271
|
+
for (let lane = 0; lane < lanes.length; lane += 1) {
|
|
272
|
+
if (excluded.has(lane)) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const last = lanes[lane]?.[(lanes[lane]?.length ?? 0) - 1];
|
|
276
|
+
if (last === void 0) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const end = last.startBeat + last.durationBeat;
|
|
280
|
+
if (end > note.startBeat + EPS2) {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
const distance = Math.abs(last.pitch - note.pitch);
|
|
284
|
+
if (distance > MAX_LANE_LEAP) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (distance < bestDistance || distance === bestDistance && end > bestEnd) {
|
|
288
|
+
best = lane;
|
|
289
|
+
bestDistance = distance;
|
|
290
|
+
bestEnd = end;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (best >= 0) {
|
|
294
|
+
lanes[best]?.push(note);
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
return false;
|
|
298
|
+
};
|
|
299
|
+
for (let start = 0; start < ordered.length; ) {
|
|
300
|
+
const onset = ordered[start]?.startBeat ?? 0;
|
|
301
|
+
let end = start + 1;
|
|
302
|
+
while (end < ordered.length && Math.abs((ordered[end]?.startBeat ?? 0) - onset) <= EPS2) {
|
|
303
|
+
end += 1;
|
|
304
|
+
}
|
|
305
|
+
const block = ordered.slice(start, end);
|
|
306
|
+
if (block.length > 1) {
|
|
307
|
+
const freeLanes = lanes.map((lane, index) => ({ index, last: lane[lane.length - 1] })).filter(
|
|
308
|
+
(entry) => entry.last !== void 0 && entry.last.startBeat + entry.last.durationBeat <= onset + EPS2
|
|
309
|
+
).sort((a, b) => a.last.pitch - b.last.pitch);
|
|
310
|
+
const paired = /* @__PURE__ */ new Set();
|
|
311
|
+
for (let index = 0; index < block.length; index += 1) {
|
|
312
|
+
const note = block[index];
|
|
313
|
+
const lane = freeLanes[index];
|
|
314
|
+
if (note !== void 0 && lane !== void 0 && Math.abs(lane.last.pitch - note.pitch) <= MAX_LANE_LEAP) {
|
|
315
|
+
lanes[lane.index]?.push(note);
|
|
316
|
+
paired.add(lane.index);
|
|
317
|
+
} else if (note !== void 0 && !attachNearest(note, paired)) {
|
|
318
|
+
lanes.push([note]);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
258
321
|
} else {
|
|
259
|
-
|
|
322
|
+
const note = block[0];
|
|
323
|
+
if (note !== void 0 && !attachNearest(note)) {
|
|
324
|
+
lanes.push([note]);
|
|
325
|
+
}
|
|
260
326
|
}
|
|
327
|
+
start = end;
|
|
261
328
|
}
|
|
262
329
|
return lanes;
|
|
263
330
|
}
|
|
331
|
+
function lastStartingAtOrBefore(sounding, beat) {
|
|
332
|
+
let low = 0;
|
|
333
|
+
let high = sounding.length;
|
|
334
|
+
while (low < high) {
|
|
335
|
+
const middle = Math.floor((low + high) / 2);
|
|
336
|
+
if ((sounding[middle]?.startBeat ?? Number.POSITIVE_INFINITY) <= beat + EPS2) {
|
|
337
|
+
low = middle + 1;
|
|
338
|
+
} else {
|
|
339
|
+
high = middle;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return low - 1;
|
|
343
|
+
}
|
|
264
344
|
function prepareTracks(tracks) {
|
|
265
345
|
let nextId = 0;
|
|
266
346
|
return tracks.map((track, index) => {
|
|
267
|
-
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
|
|
268
|
-
const voices = splitIntoSubVoices(ordered).map((voice) =>
|
|
269
|
-
voice,
|
|
270
|
-
sounding: voice.map((note, i) => {
|
|
347
|
+
const ordered = track.notes.map((note, originalIndex) => ({ note, originalIndex })).filter(({ note }) => note.durationBeat > 0).sort((a, b) => a.note.startBeat - b.note.startBeat || a.note.pitch - b.note.pitch).map(({ note, originalIndex }) => ({ ...note, id: nextId++, originalIndex }));
|
|
348
|
+
const voices = splitIntoSubVoices(ordered).map((voice) => {
|
|
349
|
+
const sounding = voice.map((note, i) => {
|
|
271
350
|
const prev = i > 0 ? voice[i - 1] : void 0;
|
|
272
351
|
const prevEnd = prev === void 0 ? void 0 : prev.startBeat + prev.durationBeat;
|
|
273
352
|
const contiguous = prevEnd !== void 0 && Math.abs(prevEnd - note.startBeat) <= EPS2;
|
|
@@ -277,11 +356,20 @@ function prepareTracks(tracks) {
|
|
|
277
356
|
startBeat: note.startBeat,
|
|
278
357
|
endBeat: note.startBeat + note.durationBeat
|
|
279
358
|
};
|
|
280
|
-
})
|
|
281
|
-
|
|
359
|
+
});
|
|
360
|
+
return {
|
|
361
|
+
voice,
|
|
362
|
+
sounding,
|
|
363
|
+
at: (beat) => {
|
|
364
|
+
const candidate = sounding[lastStartingAtOrBefore(sounding, beat)];
|
|
365
|
+
return candidate !== void 0 && covers(candidate, beat) ? candidate : void 0;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
});
|
|
282
369
|
return {
|
|
283
370
|
name: track.name ?? `track ${index + 1}`,
|
|
284
371
|
role: track.role ?? "other",
|
|
372
|
+
trackIndex: index,
|
|
285
373
|
voices
|
|
286
374
|
};
|
|
287
375
|
});
|
|
@@ -297,53 +385,68 @@ var FUNCTION_WEIGHT = 0.5;
|
|
|
297
385
|
var DISSONANCE_WEIGHT = 0.35;
|
|
298
386
|
var SPAN_WEIGHT = 0.15;
|
|
299
387
|
var SPAN_SATURATION = 24;
|
|
300
|
-
function pitchClass2(pitch) {
|
|
301
|
-
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
302
|
-
}
|
|
303
388
|
function clamp01(value) {
|
|
304
389
|
return Math.min(1, Math.max(0, value));
|
|
305
390
|
}
|
|
306
391
|
function tensionCurve(tracks, opts = {}) {
|
|
307
392
|
const ts = opts.ts ?? parseTimeSignature("4/4");
|
|
308
393
|
assertTimeSignature(ts);
|
|
309
|
-
|
|
394
|
+
const budget = opts.budget;
|
|
395
|
+
assertGenerationBudget(tracks.length, "arrangement tracks", budget);
|
|
310
396
|
for (let index = 0; index < tracks.length; index += 1) {
|
|
311
397
|
assertNoteEvents(tracks[index]?.notes ?? [], `tracks[${index}].notes`, {
|
|
312
|
-
allowNonPositiveDuration: true
|
|
398
|
+
allowNonPositiveDuration: true,
|
|
399
|
+
budget
|
|
313
400
|
});
|
|
314
401
|
}
|
|
315
402
|
const profile = opts.profile ?? "pop";
|
|
316
403
|
const step = opts.step ?? 1;
|
|
317
404
|
assertRange(step, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "tension sampling step");
|
|
318
|
-
const
|
|
319
|
-
const
|
|
320
|
-
const
|
|
405
|
+
const harmonyTracks = opts.harmonyTracks === void 0 ? void 0 : new Set(opts.harmonyTracks.map(Math.trunc));
|
|
406
|
+
const pooled = poolNotes(tracks, harmonyTracks);
|
|
407
|
+
const totalBeats = poolNotes(tracks).reduce(
|
|
408
|
+
(end, n) => Math.max(end, n.startBeat + n.durationBeat),
|
|
409
|
+
0
|
|
410
|
+
);
|
|
411
|
+
const { timeline, key } = opts.timeline === void 0 ? chordTimelineFromNotes(pooled, {
|
|
321
412
|
key: opts.key,
|
|
322
413
|
ts,
|
|
323
|
-
harmonicRhythm: opts.harmonicRhythm
|
|
324
|
-
|
|
414
|
+
harmonicRhythm: opts.harmonicRhythm,
|
|
415
|
+
budget
|
|
416
|
+
}) : {
|
|
417
|
+
timeline: opts.timeline,
|
|
418
|
+
key: opts.key ?? detectKeyFromNotes(pooled, { budget })[0]?.key ?? majorKey(0)
|
|
419
|
+
};
|
|
325
420
|
const prepared = prepareTracks(tracks);
|
|
326
421
|
const points = [];
|
|
327
422
|
const sampleCount = Math.max(0, Math.ceil(totalBeats / step - EPS2));
|
|
328
|
-
assertGenerationBudget(sampleCount, "tension samples");
|
|
423
|
+
assertGenerationBudget(sampleCount, "tension samples", budget);
|
|
424
|
+
const voiceCount = prepared.reduce((sum, track) => sum + track.voices.length, 0);
|
|
425
|
+
assertGenerationBudget(
|
|
426
|
+
sampleCount * Math.min(128, voiceCount) * voiceCount,
|
|
427
|
+
"tension sample-voice comparisons",
|
|
428
|
+
budget
|
|
429
|
+
);
|
|
329
430
|
for (let i = 0; i < sampleCount; i += 1) {
|
|
330
431
|
const beat = i * step;
|
|
331
432
|
points.push({ beat, tension: sampleTension(prepared, timeline, key, ts, profile, beat) });
|
|
332
433
|
}
|
|
333
434
|
return points;
|
|
334
435
|
}
|
|
436
|
+
function tensionCurveFrom(tracks, analysis, opts = {}) {
|
|
437
|
+
return tensionCurve(tracks, { ...opts, timeline: analysis.timeline, key: analysis.key });
|
|
438
|
+
}
|
|
335
439
|
function allSounding(prepared, beat) {
|
|
336
440
|
const out = [];
|
|
337
441
|
for (let t = 0; t < prepared.length; t += 1) {
|
|
338
442
|
const track = prepared[t];
|
|
339
|
-
if (!track) {
|
|
443
|
+
if (!track || isPercussion(track.role)) {
|
|
340
444
|
continue;
|
|
341
445
|
}
|
|
342
446
|
for (const subVoice of track.voices) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
447
|
+
const note = subVoice.at(beat);
|
|
448
|
+
if (note !== void 0) {
|
|
449
|
+
out.push({ pitch: note.pitch, track: t });
|
|
347
450
|
}
|
|
348
451
|
}
|
|
349
452
|
}
|
|
@@ -360,25 +463,32 @@ function sampleTension(prepared, timeline, key, ts, profile, beat) {
|
|
|
360
463
|
const strongBeat = isStrongBeat(beat, ts);
|
|
361
464
|
let nonChord = 0;
|
|
362
465
|
let dissonant = 0;
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
466
|
+
const pitches = /* @__PURE__ */ new Map();
|
|
467
|
+
for (const voice of sounding) {
|
|
468
|
+
pitches.set(voice.pitch, (pitches.get(voice.pitch) ?? 0) + 1);
|
|
469
|
+
}
|
|
470
|
+
for (const [pitch, count] of pitches) {
|
|
471
|
+
if (chordPcs && !chordPcs.has(pitchClassOf(pitch))) {
|
|
472
|
+
nonChord += count;
|
|
367
473
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
474
|
+
const others = [...pitches].flatMap(
|
|
475
|
+
([otherPitch, otherCount]) => Array.from({ length: otherCount - (otherPitch === pitch ? 1 : 0) }, () => ({
|
|
476
|
+
pitch: otherPitch
|
|
477
|
+
}))
|
|
478
|
+
);
|
|
479
|
+
const result = evaluateSafety(
|
|
480
|
+
{
|
|
481
|
+
profile,
|
|
482
|
+
candidatePitch: pitch,
|
|
483
|
+
chord,
|
|
484
|
+
key,
|
|
485
|
+
otherVoices: others,
|
|
486
|
+
strongBeat
|
|
487
|
+
},
|
|
488
|
+
{ suggestions: false }
|
|
489
|
+
);
|
|
380
490
|
if (result.safety === 2 /* Dissonant */) {
|
|
381
|
-
dissonant +=
|
|
491
|
+
dissonant += count;
|
|
382
492
|
}
|
|
383
493
|
}
|
|
384
494
|
const dissonanceScore = Math.max(nonChord, dissonant) / sounding.length;
|
|
@@ -395,13 +505,10 @@ function sampleTension(prepared, timeline, key, ts, profile, beat) {
|
|
|
395
505
|
}
|
|
396
506
|
|
|
397
507
|
// src/analyze/voice/index.ts
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
401
|
-
}
|
|
402
|
-
function intervalAboveRoot(pitch, chord) {
|
|
403
|
-
return ((pitchClass3(pitch) - pitchClass3(chord.rootPc)) % 12 + 12) % 12;
|
|
508
|
+
function toVoiceNotes(events) {
|
|
509
|
+
return events.map((event, index) => ({ ...event, id: index, originalIndex: index }));
|
|
404
510
|
}
|
|
511
|
+
var HUMANIZE_ADJACENCY = 0.05;
|
|
405
512
|
function intervalAboveBass(pitch, others, chord) {
|
|
406
513
|
let bass = Number.POSITIVE_INFINITY;
|
|
407
514
|
for (const ov of others) {
|
|
@@ -411,7 +518,7 @@ function intervalAboveBass(pitch, others, chord) {
|
|
|
411
518
|
return ((pitch - bass) % 12 + 12) % 12;
|
|
412
519
|
}
|
|
413
520
|
const ref = chord.bassPc ?? chord.rootPc;
|
|
414
|
-
return ((
|
|
521
|
+
return ((pitchClassOf(pitch) - pitchClassOf(ref)) % 12 + 12) % 12;
|
|
415
522
|
}
|
|
416
523
|
function tensionDegree(ic) {
|
|
417
524
|
if (ic === 1 || ic === 2) {
|
|
@@ -422,25 +529,27 @@ function tensionDegree(ic) {
|
|
|
422
529
|
}
|
|
423
530
|
return 13;
|
|
424
531
|
}
|
|
425
|
-
function isChordMember(pitch, chord) {
|
|
426
|
-
return chord ? chordPitchClasses(chord).includes(pitchClass3(pitch)) : false;
|
|
427
|
-
}
|
|
428
532
|
function isStep(a, b) {
|
|
429
533
|
const d = Math.abs(a - b);
|
|
430
534
|
return d === 1 || d === 2;
|
|
431
535
|
}
|
|
432
536
|
function suspensionType(ic, delta) {
|
|
433
|
-
let type;
|
|
434
537
|
if (delta > 0) {
|
|
435
|
-
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
type = "sus7-6";
|
|
440
|
-
} else {
|
|
441
|
-
type = "sus4-3";
|
|
538
|
+
return "sus2-3";
|
|
539
|
+
}
|
|
540
|
+
if (ic === 2) {
|
|
541
|
+
return "sus9-8";
|
|
442
542
|
}
|
|
443
|
-
|
|
543
|
+
if (ic === 10 || ic === 11) {
|
|
544
|
+
return "sus7-6";
|
|
545
|
+
}
|
|
546
|
+
if (ic === 5 || ic === 6) {
|
|
547
|
+
return "sus4-3";
|
|
548
|
+
}
|
|
549
|
+
if (ic === 8 || ic === 9) {
|
|
550
|
+
return "sus6-5";
|
|
551
|
+
}
|
|
552
|
+
return null;
|
|
444
553
|
}
|
|
445
554
|
function stepResolution(pitch, chord) {
|
|
446
555
|
for (let delta = 1; delta <= 2; delta += 1) {
|
|
@@ -450,7 +559,7 @@ function stepResolution(pitch, chord) {
|
|
|
450
559
|
}
|
|
451
560
|
return void 0;
|
|
452
561
|
}
|
|
453
|
-
function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat) {
|
|
562
|
+
function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat = () => []) {
|
|
454
563
|
assertNoteEvents(voice, "voice notes", { allowNonPositiveDuration: true });
|
|
455
564
|
const result = [];
|
|
456
565
|
for (let i = 0; i < voice.length; i += 1) {
|
|
@@ -462,8 +571,8 @@ function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat) {
|
|
|
462
571
|
const nextNote = i + 1 < voice.length ? voice[i + 1] : void 0;
|
|
463
572
|
const prevEnd = prevNote === void 0 ? Number.NEGATIVE_INFINITY : prevNote.startBeat + prevNote.durationBeat;
|
|
464
573
|
const noteEnd = note.startBeat + note.durationBeat;
|
|
465
|
-
const prev = prevNote !== void 0 && Math.abs(prevEnd - note.startBeat) <=
|
|
466
|
-
const next = nextNote !== void 0 && Math.abs(noteEnd - nextNote.startBeat) <=
|
|
574
|
+
const prev = prevNote !== void 0 && Math.abs(prevEnd - note.startBeat) <= HUMANIZE_ADJACENCY ? prevNote : void 0;
|
|
575
|
+
const next = nextNote !== void 0 && Math.abs(noteEnd - nextNote.startBeat) <= HUMANIZE_ADJACENCY ? nextNote : void 0;
|
|
467
576
|
const chord = chordAtBeat(note.startBeat);
|
|
468
577
|
const labels = [];
|
|
469
578
|
const member = isChordMember(note.pitch, chord);
|
|
@@ -488,10 +597,11 @@ function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat) {
|
|
|
488
597
|
);
|
|
489
598
|
if (prepared && verticallyDissonant && resolves) {
|
|
490
599
|
const ic = intervalAboveBass(note.pitch, others, chord);
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
600
|
+
const type = suspensionType(ic, next.pitch - note.pitch);
|
|
601
|
+
if (type !== null) {
|
|
602
|
+
labels.push({ kind: "suspension", type, resolveTo: next.pitch });
|
|
603
|
+
handled = true;
|
|
604
|
+
}
|
|
495
605
|
}
|
|
496
606
|
}
|
|
497
607
|
if (!handled && chord && prev && next) {
|
|
@@ -549,7 +659,18 @@ function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat) {
|
|
|
549
659
|
if (next && isLeadingToneResolution(note.pitch, next.pitch, key)) {
|
|
550
660
|
labels.push({ kind: "leadingTone", resolveTo: next.pitch });
|
|
551
661
|
}
|
|
552
|
-
|
|
662
|
+
const analyzed = {
|
|
663
|
+
noteId: note.id ?? i,
|
|
664
|
+
pitch: note.pitch,
|
|
665
|
+
startBeat: note.startBeat,
|
|
666
|
+
durationBeat: note.durationBeat,
|
|
667
|
+
labels,
|
|
668
|
+
rationale: describe(labels)
|
|
669
|
+
};
|
|
670
|
+
if (note.originalIndex !== void 0) {
|
|
671
|
+
analyzed.originalIndex = note.originalIndex;
|
|
672
|
+
}
|
|
673
|
+
result.push(analyzed);
|
|
553
674
|
}
|
|
554
675
|
return result;
|
|
555
676
|
}
|
|
@@ -583,64 +704,99 @@ function describe(labels) {
|
|
|
583
704
|
}
|
|
584
705
|
|
|
585
706
|
// src/analyze/arrange/tracks.ts
|
|
586
|
-
function
|
|
707
|
+
function soundingVoicesAt(prepared, beat) {
|
|
587
708
|
const out = [];
|
|
588
709
|
for (let t = 0; t < prepared.length; t += 1) {
|
|
589
710
|
const track = prepared[t];
|
|
590
|
-
if (!track) {
|
|
711
|
+
if (!track || isPercussion(track.role)) {
|
|
591
712
|
continue;
|
|
592
713
|
}
|
|
593
714
|
for (let v = 0; v < track.voices.length; v += 1) {
|
|
594
|
-
|
|
715
|
+
const note = track.voices[v]?.at(beat);
|
|
716
|
+
if (note === void 0) {
|
|
595
717
|
continue;
|
|
596
718
|
}
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
for (const note of subVoice.sounding) {
|
|
602
|
-
if (!covers(note, beat)) {
|
|
603
|
-
continue;
|
|
604
|
-
}
|
|
605
|
-
const snap = { pitch: note.pitch };
|
|
606
|
-
const previous = Math.abs(note.startBeat - beat) <= EPS2 ? note.prevPitch : note.pitch;
|
|
607
|
-
if (previous !== void 0) {
|
|
608
|
-
snap.prevPitch = previous;
|
|
609
|
-
}
|
|
610
|
-
out.push(snap);
|
|
719
|
+
const snap = { pitch: note.pitch };
|
|
720
|
+
const previous = Math.abs(note.startBeat - beat) <= EPS2 ? note.prevPitch : note.pitch;
|
|
721
|
+
if (previous !== void 0) {
|
|
722
|
+
snap.prevPitch = previous;
|
|
611
723
|
}
|
|
724
|
+
out.push({ track: t, voice: v, snapshot: snap });
|
|
612
725
|
}
|
|
613
726
|
}
|
|
614
727
|
return out;
|
|
615
728
|
}
|
|
729
|
+
function otherVoicesSounding(prepared, excludeTrack, excludeVoice, beat, cache) {
|
|
730
|
+
const sounding = cache.get(beat) ?? soundingVoicesAt(prepared, beat);
|
|
731
|
+
cache.set(beat, sounding);
|
|
732
|
+
return sounding.filter(({ track, voice }) => track !== excludeTrack || voice !== excludeVoice).map(({ snapshot }) => snapshot);
|
|
733
|
+
}
|
|
734
|
+
function firstSegmentAfter(timeline, beat) {
|
|
735
|
+
let low = 0;
|
|
736
|
+
let high = timeline.segments.length;
|
|
737
|
+
while (low < high) {
|
|
738
|
+
const middle = Math.floor((low + high) / 2);
|
|
739
|
+
if ((timeline.segments[middle]?.startBeat ?? Number.POSITIVE_INFINITY) <= beat + EPS2) {
|
|
740
|
+
low = middle + 1;
|
|
741
|
+
} else {
|
|
742
|
+
high = middle;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return low;
|
|
746
|
+
}
|
|
616
747
|
function evaluationBeats(note, timeline) {
|
|
617
748
|
const noteEnd = note.startBeat + note.durationBeat;
|
|
618
749
|
const beats = [note.startBeat];
|
|
619
|
-
for (
|
|
620
|
-
|
|
621
|
-
|
|
750
|
+
for (let i = firstSegmentAfter(timeline, note.startBeat); i < timeline.segments.length; i += 1) {
|
|
751
|
+
const start = timeline.segments[i]?.startBeat;
|
|
752
|
+
if (start === void 0 || start >= noteEnd - EPS2) {
|
|
753
|
+
break;
|
|
622
754
|
}
|
|
755
|
+
beats.push(start);
|
|
623
756
|
}
|
|
624
757
|
return beats;
|
|
625
758
|
}
|
|
626
759
|
function analyzeArrangement(tracks, opts = {}) {
|
|
627
760
|
const ts = opts.ts ?? parseTimeSignature("4/4");
|
|
628
761
|
assertTimeSignature(ts);
|
|
629
|
-
|
|
762
|
+
const budget = opts.budget;
|
|
763
|
+
assertGenerationBudget(tracks.length, "arrangement tracks", budget);
|
|
764
|
+
let noteCount = 0;
|
|
630
765
|
for (let index = 0; index < tracks.length; index += 1) {
|
|
631
|
-
|
|
632
|
-
|
|
766
|
+
const notes = tracks[index]?.notes;
|
|
767
|
+
if (!Array.isArray(notes)) {
|
|
768
|
+
throw new InvalidInputError(
|
|
769
|
+
`tracks[${index}].notes must be an array; received ${typeof notes}`
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
assertNoteEvents(notes, `tracks[${index}].notes`, {
|
|
773
|
+
allowNonPositiveDuration: true,
|
|
774
|
+
budget
|
|
633
775
|
});
|
|
776
|
+
noteCount += notes.length;
|
|
634
777
|
}
|
|
778
|
+
assertGenerationBudget(noteCount, "arrangement notes", budget);
|
|
635
779
|
const profile = opts.profile ?? "pop";
|
|
636
|
-
const
|
|
637
|
-
const
|
|
780
|
+
const minSeverity = opts.minSeverity ?? 1 /* Warning */;
|
|
781
|
+
const harmonyTracks = opts.harmonyTracks === void 0 ? void 0 : new Set(opts.harmonyTracks.map(Math.trunc));
|
|
782
|
+
const pooled = poolNotes(tracks, harmonyTracks);
|
|
783
|
+
const inferred = opts.timeline === void 0 ? chordTimelineFromNotes(pooled, {
|
|
638
784
|
key: opts.key,
|
|
639
785
|
ts,
|
|
640
|
-
harmonicRhythm: opts.harmonicRhythm
|
|
641
|
-
|
|
786
|
+
harmonicRhythm: opts.harmonicRhythm,
|
|
787
|
+
budget
|
|
788
|
+
}) : {
|
|
789
|
+
timeline: opts.timeline,
|
|
790
|
+
key: opts.key ?? detectKeyFromNotes(pooled, { budget })[0]?.key ?? majorKey(0),
|
|
791
|
+
// A caller-provided timeline was not inferred from these notes, so
|
|
792
|
+
// it has no measured confidence. Reporting 1 would turn confidence
|
|
793
|
+
// gates into unconditional passes.
|
|
794
|
+
segmentConfidence: opts.timeline.segments.map(() => 0)
|
|
795
|
+
};
|
|
796
|
+
const { timeline, key, segmentConfidence } = inferred;
|
|
642
797
|
const cadences = detectCadences(timeline, key);
|
|
643
798
|
const prepared = prepareTracks(tracks);
|
|
799
|
+
const soundingCache = /* @__PURE__ */ new Map();
|
|
644
800
|
const trackAnalyses = [];
|
|
645
801
|
const conflicts = [];
|
|
646
802
|
for (let t = 0; t < prepared.length; t += 1) {
|
|
@@ -648,20 +804,24 @@ function analyzeArrangement(tracks, opts = {}) {
|
|
|
648
804
|
if (!track) {
|
|
649
805
|
continue;
|
|
650
806
|
}
|
|
807
|
+
if (isPercussion(track.role)) {
|
|
808
|
+
trackAnalyses.push({ name: track.name, role: track.role, notes: [] });
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
651
811
|
const notes = [];
|
|
652
812
|
for (let v = 0; v < track.voices.length; v += 1) {
|
|
653
813
|
const subVoice = track.voices[v];
|
|
654
814
|
if (!subVoice) {
|
|
655
815
|
continue;
|
|
656
816
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
(beat) => otherVoicesSounding(prepared, t, v, beat)
|
|
663
|
-
)
|
|
817
|
+
const analyzed = analyzeVoice(
|
|
818
|
+
subVoice.voice,
|
|
819
|
+
timeline.at,
|
|
820
|
+
key,
|
|
821
|
+
(beat) => otherVoicesSounding(prepared, t, v, beat, soundingCache)
|
|
664
822
|
);
|
|
823
|
+
notes.push(...analyzed.map((note) => ({ ...note, trackIndex: track.trackIndex })));
|
|
824
|
+
const labelsById = new Map(analyzed.map((note) => [note.noteId, note.labels]));
|
|
665
825
|
for (let noteIndex = 0; noteIndex < subVoice.voice.length; noteIndex += 1) {
|
|
666
826
|
const note = subVoice.voice[noteIndex];
|
|
667
827
|
const preparedNote = subVoice.sounding[noteIndex];
|
|
@@ -670,23 +830,31 @@ function analyzeArrangement(tracks, opts = {}) {
|
|
|
670
830
|
}
|
|
671
831
|
for (const beat of evaluationBeats(note, timeline)) {
|
|
672
832
|
const atOnset = Math.abs(beat - note.startBeat) <= EPS2;
|
|
673
|
-
const
|
|
833
|
+
const safetyQuery = {
|
|
674
834
|
profile,
|
|
675
835
|
candidatePitch: note.pitch,
|
|
676
836
|
prevPitch: atOnset ? preparedNote.prevPitch : note.pitch,
|
|
677
837
|
chord: timeline.at(beat),
|
|
678
838
|
key,
|
|
679
|
-
otherVoices: otherVoicesSounding(prepared, t, v, beat),
|
|
839
|
+
otherVoices: otherVoicesSounding(prepared, t, v, beat, soundingCache),
|
|
680
840
|
strongBeat: isStrongBeat(beat, ts)
|
|
681
|
-
}
|
|
682
|
-
|
|
841
|
+
};
|
|
842
|
+
let result = evaluateSafety(safetyQuery, { suggestions: false });
|
|
843
|
+
if (result.safety !== 0 /* Safe */ && result.safety >= minSeverity) {
|
|
844
|
+
result = evaluateSafety(safetyQuery);
|
|
683
845
|
const conflict = {
|
|
684
846
|
beat,
|
|
685
847
|
trackName: track.name,
|
|
848
|
+
trackIndex: track.trackIndex,
|
|
849
|
+
noteId: note.id,
|
|
686
850
|
pitch: note.pitch,
|
|
851
|
+
labels: labelsById.get(note.id) ?? [],
|
|
687
852
|
safety: result.safety,
|
|
688
853
|
reasons: result.reasons
|
|
689
854
|
};
|
|
855
|
+
if (note.originalIndex !== void 0) {
|
|
856
|
+
conflict.originalIndex = note.originalIndex;
|
|
857
|
+
}
|
|
690
858
|
if (result.rationale !== void 0) {
|
|
691
859
|
conflict.rationale = result.rationale;
|
|
692
860
|
}
|
|
@@ -713,7 +881,9 @@ export {
|
|
|
713
881
|
chordTimelineFromNotes,
|
|
714
882
|
detectCadences,
|
|
715
883
|
tensionCurve,
|
|
884
|
+
tensionCurveFrom,
|
|
885
|
+
toVoiceNotes,
|
|
716
886
|
analyzeVoice,
|
|
717
887
|
analyzeArrangement
|
|
718
888
|
};
|
|
719
|
-
//# sourceMappingURL=chunk-
|
|
889
|
+
//# sourceMappingURL=chunk-YUE6TJ4M.js.map
|