@libraz/libcantus 0.9.3 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/analyze/index.cjs +39 -1947
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +197 -37
- package/dist/analyze/index.d.ts +197 -37
- package/dist/analyze/index.js +19 -8
- package/dist/chunk-36N73KZ3.cjs +246 -0
- package/dist/chunk-36N73KZ3.cjs.map +1 -0
- package/dist/chunk-4HLLZPXT.cjs +3077 -0
- package/dist/chunk-4HLLZPXT.cjs.map +1 -0
- package/dist/chunk-7MGZ7D62.cjs +498 -0
- package/dist/chunk-7MGZ7D62.cjs.map +1 -0
- package/dist/{chunk-4MHA432G.js → chunk-7NKDPF74.js} +403 -180
- package/dist/chunk-7NKDPF74.js.map +1 -0
- package/dist/chunk-CAN76NCU.cjs +679 -0
- package/dist/chunk-CAN76NCU.cjs.map +1 -0
- package/dist/{chunk-MIWO425M.js → chunk-D2FAJBYL.js} +101 -34
- package/dist/chunk-D2FAJBYL.js.map +1 -0
- package/dist/{chunk-SGU7SPLK.js → chunk-EHVEBWM5.js} +1056 -531
- package/dist/chunk-EHVEBWM5.js.map +1 -0
- package/dist/chunk-FCANXBAT.cjs +30 -0
- package/dist/chunk-FCANXBAT.cjs.map +1 -0
- package/dist/chunk-FNDWXRRI.cjs +479 -0
- package/dist/chunk-FNDWXRRI.cjs.map +1 -0
- package/dist/{chunk-IT2O4XFE.js → chunk-GQSGS2N4.js} +43 -24
- package/dist/chunk-GQSGS2N4.js.map +1 -0
- package/dist/{chunk-62QZKCJQ.js → chunk-HKGYCQ34.js} +10 -6
- package/dist/chunk-HKGYCQ34.js.map +1 -0
- package/dist/{chunk-AKLYPWIN.js → chunk-I24MV65U.js} +29 -6
- package/dist/chunk-I24MV65U.js.map +1 -0
- package/dist/{chunk-CHMXELDS.js → chunk-KMHOICIY.js} +311 -181
- package/dist/chunk-KMHOICIY.js.map +1 -0
- package/dist/chunk-LW53AHQU.cjs +1117 -0
- package/dist/chunk-LW53AHQU.cjs.map +1 -0
- package/dist/{chunk-2ZWMSJH6.js → chunk-NKIBQ6PT.js} +2 -2
- package/dist/chunk-NKIBQ6PT.js.map +1 -0
- package/dist/chunk-NP4NA2FB.cjs +106 -0
- package/dist/chunk-NP4NA2FB.cjs.map +1 -0
- package/dist/chunk-NVE34YGZ.cjs +1449 -0
- package/dist/chunk-NVE34YGZ.cjs.map +1 -0
- package/dist/{chunk-EH6EM37U.js → chunk-NYYWMV2C.js} +61 -49
- package/dist/chunk-NYYWMV2C.js.map +1 -0
- package/dist/{chunk-GOAW4VYL.js → chunk-Q2XUP227.js} +243 -117
- package/dist/chunk-Q2XUP227.js.map +1 -0
- package/dist/chunk-Q6OP4U43.cjs +327 -0
- package/dist/chunk-Q6OP4U43.cjs.map +1 -0
- package/dist/chunk-RC6GBMCD.cjs +845 -0
- package/dist/chunk-RC6GBMCD.cjs.map +1 -0
- package/dist/{chunk-C5XYKUVG.js → chunk-RK35RIN7.js} +370 -157
- package/dist/chunk-RK35RIN7.js.map +1 -0
- package/dist/chunk-RUMU3UA6.cjs +53 -0
- package/dist/chunk-RUMU3UA6.cjs.map +1 -0
- package/dist/chunk-T64C7ZD7.cjs +1 -0
- package/dist/chunk-T64C7ZD7.cjs.map +1 -0
- package/dist/chunk-XEBG3G53.js +498 -0
- package/dist/chunk-XEBG3G53.js.map +1 -0
- package/dist/{chunk-I5R7ZBJS.js → chunk-YLEMGEFH.js} +103 -59
- package/dist/chunk-YLEMGEFH.js.map +1 -0
- package/dist/chunk-ZEJR4TKL.cjs +173 -0
- package/dist/chunk-ZEJR4TKL.cjs.map +1 -0
- package/dist/core/index.cjs +130 -647
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +301 -39
- package/dist/core/index.d.ts +301 -39
- package/dist/core/index.js +54 -16
- package/dist/{function-DYt4iKsn.d.cts → function-BtiZb9i9.d.ts} +28 -7
- package/dist/{function-DVH4Eusr.d.ts → function-DNugAOrt.d.cts} +28 -7
- package/dist/generate/index.cjs +28 -4085
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +370 -62
- package/dist/generate/index.d.ts +370 -62
- package/dist/generate/index.js +23 -9
- package/dist/{index-CxqBQtH1.d.cts → index-6VR8-kit.d.ts} +19 -4
- package/dist/index-70p168VM.d.ts +192 -0
- package/dist/{index-CpOsP1y9.d.cts → index-Be74mcon.d.cts} +18 -17
- package/dist/index-BtXT6UQ0.d.cts +192 -0
- package/dist/{index-DxvARIP2.d.ts → index-C5UBbQ4G.d.ts} +18 -17
- package/dist/{index-Bt4tOUD9.d.ts → index-C83mZI1P.d.ts} +93 -8
- package/dist/{index-Bg5dYMmZ.d.ts → index-D6b6na69.d.cts} +14 -11
- package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
- package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
- package/dist/index-OG65ocOr.d.cts +272 -0
- package/dist/index-OG65ocOr.d.ts +272 -0
- package/dist/{index-Jc0Ryi79.d.ts → index-tJhkmQgE.d.cts} +19 -4
- package/dist/{index-Dikst44v.d.cts → index-tSQh-xwY.d.ts} +14 -11
- package/dist/{index-BNlGP86A.d.cts → index-y_ak9_Hi.d.cts} +93 -8
- package/dist/index.cjs +419 -7383
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -14
- package/dist/index.d.ts +14 -14
- package/dist/index.js +111 -27
- package/dist/model/index.cjs +8 -2562
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +433 -75
- package/dist/model/index.d.ts +433 -75
- package/dist/model/index.js +6 -7
- package/dist/styled-DIRkk9Uz.d.ts +506 -0
- package/dist/styled-DJbkJQfj.d.cts +506 -0
- package/dist/theory/index.cjs +172 -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 +24 -6
- 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.map +0 -1
- package/dist/chunk-IT2O4XFE.js.map +0 -1
- package/dist/chunk-MIWO425M.js.map +0 -1
- 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
|
@@ -1,451 +0,0 @@
|
|
|
1
|
-
import { isDiatonic, parallelKey } from '../../analyze/functional/index.js';
|
|
2
|
-
import { createNoteEventIndex } from '../../core/event-index/index.js';
|
|
3
|
-
import type { TimeSignature } from '../../core/meter/index.js';
|
|
4
|
-
import { isStrongBeat } from '../../core/meter/index.js';
|
|
5
|
-
import { createRng } from '../../core/random/index.js';
|
|
6
|
-
import type { KeyScale, NoteEvent } from '../../core/types.js';
|
|
7
|
-
import { assertGenerationBudget, assertRange } from '../../core/validation/index.js';
|
|
8
|
-
import type { Chord, ChordQuality } from '../../theory/chord/index.js';
|
|
9
|
-
import { chordPitchClasses, diatonicTriad, makeChord } from '../../theory/chord/index.js';
|
|
10
|
-
import type { HarmonyRole } from '../../theory/harmony/index.js';
|
|
11
|
-
import { roleOf } from '../../theory/harmony/index.js';
|
|
12
|
-
import {
|
|
13
|
-
isScaleTone,
|
|
14
|
-
majorKey,
|
|
15
|
-
minorKey,
|
|
16
|
-
NATURAL_MINOR_MASK,
|
|
17
|
-
scaleTonesInDegreeOrder,
|
|
18
|
-
} from '../../theory/scale/index.js';
|
|
19
|
-
import type { ChordSpan } from '../progression/index.js';
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* A melody note supplied to the harmonizer: a {@link NoteEvent}.
|
|
23
|
-
*
|
|
24
|
-
* @category Reharmonization
|
|
25
|
-
*/
|
|
26
|
-
export type MelodyNote = NoteEvent;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Options controlling {@link harmonizeMelody}.
|
|
30
|
-
*
|
|
31
|
-
* @category Reharmonization
|
|
32
|
-
*/
|
|
33
|
-
export type HarmonizeOptions = {
|
|
34
|
-
melody: MelodyNote[];
|
|
35
|
-
key: KeyScale | 'infer';
|
|
36
|
-
harmonicRhythm: number;
|
|
37
|
-
reharmonize: 'diatonic' | 'secondaryDominant' | 'borrowed';
|
|
38
|
-
placement: { transposeSearch: boolean; octaveSearch: boolean };
|
|
39
|
-
/**
|
|
40
|
-
* Seed for the deterministic tie-break perturbation.
|
|
41
|
-
*
|
|
42
|
-
* @defaultValue 0
|
|
43
|
-
*/
|
|
44
|
-
seed?: number;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* The chosen transpose, key, chord path, and per-note roles.
|
|
49
|
-
*
|
|
50
|
-
* @category Reharmonization
|
|
51
|
-
*/
|
|
52
|
-
export type HarmonizeResult = {
|
|
53
|
-
transposeSemitones: number;
|
|
54
|
-
key: KeyScale;
|
|
55
|
-
chords: ChordSpan[];
|
|
56
|
-
melodyRoles: { noteIndex: number; role: HarmonyRole }[];
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
type Candidate = {
|
|
60
|
-
rootPc: number;
|
|
61
|
-
quality: ChordQuality;
|
|
62
|
-
degree?: number;
|
|
63
|
-
secondaryDominant: boolean;
|
|
64
|
-
targetDegree?: number;
|
|
65
|
-
base: number;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
type Segment = { startBeat: number; endBeat: number; noteIndices: number[] };
|
|
69
|
-
|
|
70
|
-
const FALLBACK: Candidate = { rootPc: 0, quality: 'maj', secondaryDominant: false, base: 0 };
|
|
71
|
-
|
|
72
|
-
/** Default meter used to weight metric accents when none is supplied. */
|
|
73
|
-
const DEFAULT_METER: TimeSignature = { numerator: 4, denominator: 4 };
|
|
74
|
-
|
|
75
|
-
/** Comfortable melodic range (MIDI) and the per-semitone cost of leaving it. */
|
|
76
|
-
const COMFORT_LOW = 55;
|
|
77
|
-
const COMFORT_HIGH = 79;
|
|
78
|
-
const TESSITURA_WEIGHT = 0.001;
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Magnitude of the seed-driven per-candidate perturbation. Kept far below the
|
|
82
|
-
* smallest real cost difference (transition/emission terms are on the order of
|
|
83
|
-
* 0.3 and up), so it can only decide between candidates or paths whose costs are
|
|
84
|
-
* otherwise exactly equal. The seed therefore breaks ties deterministically and
|
|
85
|
-
* never overrides melody fit or functional flow.
|
|
86
|
-
*/
|
|
87
|
-
const TIE_BREAK_JITTER = 1e-6;
|
|
88
|
-
|
|
89
|
-
function pitchClass(pitch: number): number {
|
|
90
|
-
return ((Math.trunc(pitch) % 12) + 12) % 12;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Estimate the best-fit key from a melody's pitch-class weighting.
|
|
95
|
-
*
|
|
96
|
-
* Both major and natural-minor tonics are scored, so a minor melody is
|
|
97
|
-
* recognized as its own minor key rather than being folded into the relative
|
|
98
|
-
* major (which shares identical pitch-class content). Each in-scale note adds
|
|
99
|
-
* its duration weight; notes matching the candidate tonic add a further
|
|
100
|
-
* half-weight, and that tonic emphasis is what separates a minor key from its
|
|
101
|
-
* relative major. For minor candidates the raised leading tone (the
|
|
102
|
-
* harmonic-minor seventh) also counts as in-scale, so cadential leading tones do
|
|
103
|
-
* not penalize the true minor key. Ties are broken toward the earlier candidate,
|
|
104
|
-
* and major is scored before minor at each tonic.
|
|
105
|
-
*/
|
|
106
|
-
function inferKey(melody: MelodyNote[]): KeyScale {
|
|
107
|
-
let best: KeyScale = majorKey(0);
|
|
108
|
-
let bestScore = Number.NEGATIVE_INFINITY;
|
|
109
|
-
const candidates: KeyScale[] = [];
|
|
110
|
-
for (let tonic = 0; tonic < 12; tonic += 1) {
|
|
111
|
-
candidates.push(majorKey(tonic), minorKey(tonic));
|
|
112
|
-
}
|
|
113
|
-
for (const key of candidates) {
|
|
114
|
-
const tonic = pitchClass(key.rootPc);
|
|
115
|
-
const isMinor = key.modeMask12 === NATURAL_MINOR_MASK;
|
|
116
|
-
const leadingTone = (tonic + 11) % 12;
|
|
117
|
-
let score = 0;
|
|
118
|
-
for (const n of melody) {
|
|
119
|
-
const w = Math.max(0.25, n.durationBeat);
|
|
120
|
-
const pc = pitchClass(n.pitch);
|
|
121
|
-
if (isScaleTone(n.pitch, key) || (isMinor && pc === leadingTone)) {
|
|
122
|
-
score += w;
|
|
123
|
-
}
|
|
124
|
-
if (pc === tonic) {
|
|
125
|
-
score += 0.5 * w;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
if (score > bestScore) {
|
|
129
|
-
bestScore = score;
|
|
130
|
-
best = key;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return best;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/** Enumerate candidate chords for the key, gated by reharmonization strength. */
|
|
137
|
-
function buildCandidates(key: KeyScale, reharmonize: HarmonizeOptions['reharmonize']): Candidate[] {
|
|
138
|
-
const tones = scaleTonesInDegreeOrder(key);
|
|
139
|
-
const candidates: Candidate[] = tones.map((rootPc, degree) => ({
|
|
140
|
-
rootPc,
|
|
141
|
-
quality: diatonicTriad(degree, key).quality,
|
|
142
|
-
degree,
|
|
143
|
-
secondaryDominant: false,
|
|
144
|
-
base: 0,
|
|
145
|
-
}));
|
|
146
|
-
|
|
147
|
-
if (reharmonize !== 'diatonic') {
|
|
148
|
-
for (const target of [1, 3, 4, 5]) {
|
|
149
|
-
const targetRoot = tones[target] ?? 0;
|
|
150
|
-
candidates.push({
|
|
151
|
-
rootPc: (targetRoot + 7) % 12,
|
|
152
|
-
quality: 'dom7',
|
|
153
|
-
secondaryDominant: true,
|
|
154
|
-
targetDegree: target,
|
|
155
|
-
base: 1.5,
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (reharmonize === 'borrowed') {
|
|
161
|
-
const parallel = parallelKey(key);
|
|
162
|
-
const parallelTones = scaleTonesInDegreeOrder(parallel);
|
|
163
|
-
for (let degree = 0; degree < parallelTones.length; degree += 1) {
|
|
164
|
-
const chord = diatonicTriad(degree, parallel);
|
|
165
|
-
if (!isDiatonic(chord, key)) {
|
|
166
|
-
candidates.push({
|
|
167
|
-
rootPc: chord.rootPc,
|
|
168
|
-
quality: chord.quality,
|
|
169
|
-
secondaryDominant: false,
|
|
170
|
-
base: 1.25,
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const seen = new Set<string>();
|
|
177
|
-
return candidates.filter((candidate) => {
|
|
178
|
-
const id = `${candidate.rootPc}:${candidate.quality}`;
|
|
179
|
-
if (seen.has(id)) {
|
|
180
|
-
return false;
|
|
181
|
-
}
|
|
182
|
-
seen.add(id);
|
|
183
|
-
return true;
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Melody-fit cost of a candidate over a segment's notes.
|
|
189
|
-
*
|
|
190
|
-
* Each note is weighted by the portion of its duration that overlaps the
|
|
191
|
-
* segment, so a note sustained across a boundary contributes to every segment
|
|
192
|
-
* it sounds in rather than only the one it starts in.
|
|
193
|
-
*/
|
|
194
|
-
function emissionCost(seg: Segment, cand: Candidate, melody: MelodyNote[], key: KeyScale): number {
|
|
195
|
-
const pcs = chordPitchClasses(makeChord(cand.rootPc, cand.quality));
|
|
196
|
-
let cost = cand.base;
|
|
197
|
-
for (const idx of seg.noteIndices) {
|
|
198
|
-
const note = melody[idx];
|
|
199
|
-
if (!note) {
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
const overlapStart = Math.max(note.startBeat, seg.startBeat);
|
|
203
|
-
const overlap = Math.min(note.startBeat + note.durationBeat, seg.endBeat) - overlapStart;
|
|
204
|
-
if (overlap <= 0) {
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
const w = Math.max(0.25, overlap);
|
|
208
|
-
if (pcs.includes(pitchClass(note.pitch))) {
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
const strong = isStrongBeat(overlapStart, DEFAULT_METER);
|
|
212
|
-
cost += (strong ? 10 : 2) * w;
|
|
213
|
-
if (!isScaleTone(note.pitch, key)) {
|
|
214
|
-
cost += w;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
return cost;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Register cost of a placed melody: a small penalty for notes pushed outside a
|
|
222
|
-
* comfortable range. Kept far below emission/transition costs so it only breaks
|
|
223
|
-
* ties between otherwise-equal octave placements.
|
|
224
|
-
*/
|
|
225
|
-
function tessituraCost(melody: MelodyNote[]): number {
|
|
226
|
-
let cost = 0;
|
|
227
|
-
for (const n of melody) {
|
|
228
|
-
if (n.pitch < COMFORT_LOW) {
|
|
229
|
-
cost += (COMFORT_LOW - n.pitch) * TESSITURA_WEIGHT;
|
|
230
|
-
} else if (n.pitch > COMFORT_HIGH) {
|
|
231
|
-
cost += (n.pitch - COMFORT_HIGH) * TESSITURA_WEIGHT;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
return cost;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/** Functional-flow cost of moving from one candidate chord to the next. */
|
|
238
|
-
function transitionCost(prev: Candidate, cur: Candidate, tonicPc: number): number {
|
|
239
|
-
let cost = 0;
|
|
240
|
-
if ((prev.rootPc - cur.rootPc + 12) % 12 === 7) {
|
|
241
|
-
cost -= 2; // descending-fifth root motion
|
|
242
|
-
}
|
|
243
|
-
if (
|
|
244
|
-
prev.rootPc === (tonicPc + 7) % 12 &&
|
|
245
|
-
(prev.quality === 'maj' || prev.quality === 'dom7') &&
|
|
246
|
-
cur.rootPc === tonicPc
|
|
247
|
-
) {
|
|
248
|
-
cost -= 3; // V -> I
|
|
249
|
-
}
|
|
250
|
-
if (prev.secondaryDominant) {
|
|
251
|
-
cost += cur.degree === prev.targetDegree ? -3 : 2;
|
|
252
|
-
}
|
|
253
|
-
const dist = Math.min((prev.rootPc - cur.rootPc + 12) % 12, (cur.rootPc - prev.rootPc + 12) % 12);
|
|
254
|
-
cost += dist * 0.3;
|
|
255
|
-
return cost;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/** Run one Viterbi harmonization pass over a fixed melody and key. */
|
|
259
|
-
function harmonizeOnce(
|
|
260
|
-
melody: MelodyNote[],
|
|
261
|
-
key: KeyScale,
|
|
262
|
-
candidates: Candidate[],
|
|
263
|
-
segments: Segment[],
|
|
264
|
-
jitter: number[],
|
|
265
|
-
): { cost: number; path: number[] } {
|
|
266
|
-
const tonicPc = pitchClass(key.rootPc);
|
|
267
|
-
const n = candidates.length;
|
|
268
|
-
const candAt = (i: number): Candidate => candidates[i] ?? FALLBACK;
|
|
269
|
-
const seg0 = segments[0];
|
|
270
|
-
|
|
271
|
-
let dp = candidates.map(
|
|
272
|
-
(c, ci) => (seg0 ? emissionCost(seg0, c, melody, key) : 0) + (jitter[ci] ?? 0),
|
|
273
|
-
);
|
|
274
|
-
const back: number[][] = [];
|
|
275
|
-
|
|
276
|
-
for (let s = 1; s < segments.length; s += 1) {
|
|
277
|
-
const seg = segments[s];
|
|
278
|
-
if (!seg) {
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
const next: number[] = [];
|
|
282
|
-
const ptr: number[] = [];
|
|
283
|
-
for (let c = 0; c < n; c += 1) {
|
|
284
|
-
let best = Number.POSITIVE_INFINITY;
|
|
285
|
-
let bestPrev = 0;
|
|
286
|
-
for (let p = 0; p < n; p += 1) {
|
|
287
|
-
const cost =
|
|
288
|
-
(dp[p] ?? Number.POSITIVE_INFINITY) + transitionCost(candAt(p), candAt(c), tonicPc);
|
|
289
|
-
if (cost < best) {
|
|
290
|
-
best = cost;
|
|
291
|
-
bestPrev = p;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
next[c] = best + emissionCost(seg, candAt(c), melody, key) + (jitter[c] ?? 0);
|
|
295
|
-
ptr[c] = bestPrev;
|
|
296
|
-
}
|
|
297
|
-
dp = next;
|
|
298
|
-
back.push(ptr);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
let bestCost = Number.POSITIVE_INFINITY;
|
|
302
|
-
let bestEnd = 0;
|
|
303
|
-
for (let c = 0; c < n; c += 1) {
|
|
304
|
-
const cadence = candAt(c).rootPc === tonicPc ? -2 : 0;
|
|
305
|
-
const total = (dp[c] ?? Number.POSITIVE_INFINITY) + cadence;
|
|
306
|
-
if (total < bestCost) {
|
|
307
|
-
bestCost = total;
|
|
308
|
-
bestEnd = c;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const path: number[] = new Array<number>(segments.length).fill(bestEnd);
|
|
313
|
-
for (let s = segments.length - 2; s >= 0; s -= 1) {
|
|
314
|
-
const ptr = back[s];
|
|
315
|
-
const nextIdx = path[s + 1] ?? bestEnd;
|
|
316
|
-
path[s] = ptr ? (ptr[nextIdx] ?? bestEnd) : bestEnd;
|
|
317
|
-
}
|
|
318
|
-
return { cost: bestCost, path };
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Harmonize a melody with a min-cost chord progression.
|
|
323
|
-
*
|
|
324
|
-
* The melody is segmented by `harmonicRhythm`; each segment is scored against
|
|
325
|
-
* diatonic (and, per `reharmonize`, secondary-dominant and borrowed) candidate
|
|
326
|
-
* chords by melody fit, and a Viterbi search picks the lowest-cost path using a
|
|
327
|
-
* functional-flow transition cost. When `placement.transposeSearch`/`octaveSearch`
|
|
328
|
-
* is set, the whole melody is transposed across a range and the best
|
|
329
|
-
* `(transpose, progression)` pair is returned; a small tessitura cost breaks
|
|
330
|
-
* ties toward placements that keep the melody in a comfortable register.
|
|
331
|
-
*
|
|
332
|
-
* The `seed` drives a deterministic tie-break only: it perturbs candidates by a
|
|
333
|
-
* magnitude far below any real cost difference (see {@link TIE_BREAK_JITTER}),
|
|
334
|
-
* so it can decide between chords or paths of otherwise-equal cost but never
|
|
335
|
-
* overrides melody fit or functional flow. For a well-determined melody the
|
|
336
|
-
* result is identical across seeds; the same seed always yields the same result.
|
|
337
|
-
*
|
|
338
|
-
* @param opts Melody, key (or `'infer'`), harmonic rhythm, reharmonization
|
|
339
|
-
* strength, height-search flags, and seed.
|
|
340
|
-
* @returns The chosen transpose, key, chord path, and per-note roles.
|
|
341
|
-
* @example
|
|
342
|
-
* ```ts
|
|
343
|
-
* import { harmonizeMelody, majorKey } from '@libraz/libcantus';
|
|
344
|
-
* const result = harmonizeMelody({
|
|
345
|
-
* melody: [
|
|
346
|
-
* { pitch: 60, startBeat: 0, durationBeat: 1 },
|
|
347
|
-
* { pitch: 64, startBeat: 1, durationBeat: 1 },
|
|
348
|
-
* ],
|
|
349
|
-
* key: majorKey(0),
|
|
350
|
-
* harmonicRhythm: 1,
|
|
351
|
-
* reharmonize: 'diatonic',
|
|
352
|
-
* placement: { transposeSearch: false, octaveSearch: false },
|
|
353
|
-
* });
|
|
354
|
-
* result.chords; // one ChordSpan per harmonic-rhythm segment
|
|
355
|
-
* ```
|
|
356
|
-
* @category Reharmonization
|
|
357
|
-
*/
|
|
358
|
-
export function harmonizeMelody(opts: HarmonizeOptions): HarmonizeResult {
|
|
359
|
-
const noteIndex = createNoteEventIndex(opts.melody);
|
|
360
|
-
const key = opts.key === 'infer' ? inferKey(opts.melody) : opts.key;
|
|
361
|
-
const candidates = buildCandidates(key, opts.reharmonize);
|
|
362
|
-
const candAt = (i: number): Candidate => candidates[i] ?? FALLBACK;
|
|
363
|
-
const rng = createRng(opts.seed ?? 0);
|
|
364
|
-
const jitter = candidates.map(() => rng.next() * TIE_BREAK_JITTER);
|
|
365
|
-
|
|
366
|
-
const melodyEnd = noteIndex.notes.reduce((m, n) => Math.max(m, n.endBeat), 0);
|
|
367
|
-
const requestedHr = assertRange(
|
|
368
|
-
opts.harmonicRhythm,
|
|
369
|
-
Number.MIN_VALUE,
|
|
370
|
-
Number.MAX_SAFE_INTEGER,
|
|
371
|
-
'harmonic rhythm',
|
|
372
|
-
);
|
|
373
|
-
const hr = Math.max(0.25, requestedHr);
|
|
374
|
-
const segCount = Math.max(1, Math.ceil(melodyEnd / hr));
|
|
375
|
-
assertGenerationBudget(segCount, 'harmonic segments');
|
|
376
|
-
assertGenerationBudget(segCount * candidates.length * candidates.length, 'harmonization search');
|
|
377
|
-
const segments: Segment[] = Array.from({ length: segCount }, (_, s) => ({
|
|
378
|
-
startBeat: s * hr,
|
|
379
|
-
endBeat: (s + 1) * hr,
|
|
380
|
-
noteIndices: [],
|
|
381
|
-
}));
|
|
382
|
-
// Associate each note only with the windows it actually spans. This replaces
|
|
383
|
-
// the former per-window full melody scan and creates no temporary note objects.
|
|
384
|
-
let memberships = 0;
|
|
385
|
-
for (const indexed of noteIndex.notes) {
|
|
386
|
-
const first = Math.max(0, Math.floor(indexed.note.startBeat / hr));
|
|
387
|
-
const lastExclusive = Math.min(segCount, Math.ceil(indexed.endBeat / hr - Number.EPSILON));
|
|
388
|
-
memberships += Math.max(0, lastExclusive - first);
|
|
389
|
-
assertGenerationBudget(memberships, 'note-to-segment memberships');
|
|
390
|
-
for (let segment = first; segment < lastExclusive; segment += 1) {
|
|
391
|
-
segments[segment]?.noteIndices.push(indexed.originalIndex);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const transposes: number[] = [0];
|
|
396
|
-
if (opts.placement.transposeSearch) {
|
|
397
|
-
for (let s = -6; s <= 6; s += 1) {
|
|
398
|
-
if (s !== 0) {
|
|
399
|
-
transposes.push(s);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
if (opts.placement.octaveSearch) {
|
|
404
|
-
for (const o of [-12, 12]) {
|
|
405
|
-
if (!transposes.includes(o)) {
|
|
406
|
-
transposes.push(o);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
let bestCost = Number.POSITIVE_INFINITY;
|
|
412
|
-
let bestTs = 0;
|
|
413
|
-
let bestPath: number[] = [];
|
|
414
|
-
let bestMelody = opts.melody;
|
|
415
|
-
for (const ts of transposes) {
|
|
416
|
-
const shifted = opts.melody.map((n) => ({ ...n, pitch: n.pitch + ts }));
|
|
417
|
-
const { cost, path } = harmonizeOnce(shifted, key, candidates, segments, jitter);
|
|
418
|
-
const total = cost + tessituraCost(shifted);
|
|
419
|
-
if (total < bestCost) {
|
|
420
|
-
bestCost = total;
|
|
421
|
-
bestTs = ts;
|
|
422
|
-
bestPath = path;
|
|
423
|
-
bestMelody = shifted;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
const chords: ChordSpan[] = bestPath.map((ci, s) => {
|
|
428
|
-
const cand = candAt(ci);
|
|
429
|
-
const chord: ChordSpan = {
|
|
430
|
-
rootPc: cand.rootPc,
|
|
431
|
-
quality: cand.quality,
|
|
432
|
-
startBeat: segments[s]?.startBeat ?? s * hr,
|
|
433
|
-
};
|
|
434
|
-
if (cand.degree !== undefined) {
|
|
435
|
-
chord.degree = cand.degree;
|
|
436
|
-
}
|
|
437
|
-
if (cand.secondaryDominant) {
|
|
438
|
-
chord.secondaryDominant = true;
|
|
439
|
-
}
|
|
440
|
-
return chord;
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
const melodyRoles = bestMelody.map((note, noteIndex) => {
|
|
444
|
-
const segIdx = Math.min(segments.length - 1, Math.max(0, Math.floor(note.startBeat / hr)));
|
|
445
|
-
const cand = candAt(bestPath[segIdx] ?? 0);
|
|
446
|
-
const chord: Chord = makeChord(cand.rootPc, cand.quality);
|
|
447
|
-
return { noteIndex, role: roleOf(note.pitch, chord).role };
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
return { transposeSemitones: bestTs, key, chords, melodyRoles };
|
|
451
|
-
}
|
package/src/generate/index.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Public API of the generate layer. Re-exports every generate-layer module;
|
|
3
|
-
* also available from the package root.
|
|
4
|
-
*/
|
|
5
|
-
export type { BassLineOptions, BassSegment, BassStyle } from './bass/index.js';
|
|
6
|
-
export { generateBassLine } from './bass/index.js';
|
|
7
|
-
export type { CounterMelodyOptions } from './countermelody/index.js';
|
|
8
|
-
export { generateCounterMelody } from './countermelody/index.js';
|
|
9
|
-
export type {
|
|
10
|
-
DrumHit,
|
|
11
|
-
DrumRole,
|
|
12
|
-
DrumsOptions,
|
|
13
|
-
EuclideanKick,
|
|
14
|
-
GrooveFeel,
|
|
15
|
-
GrooveStyle,
|
|
16
|
-
Section,
|
|
17
|
-
} from './drums/index.js';
|
|
18
|
-
export { generateDrums } from './drums/index.js';
|
|
19
|
-
export type { GrooveSlot, GrooveTemplate, HumanizeOptions } from './groove/index.js';
|
|
20
|
-
export { applyGrooveTemplate, extractGrooveTemplate, humanize } from './groove/index.js';
|
|
21
|
-
export type {
|
|
22
|
-
HarmonizeOptions,
|
|
23
|
-
HarmonizeResult,
|
|
24
|
-
MelodyNote,
|
|
25
|
-
} from './harmonize/index.js';
|
|
26
|
-
export { harmonizeMelody } from './harmonize/index.js';
|
|
27
|
-
export type {
|
|
28
|
-
MotifCell,
|
|
29
|
-
MotifContour,
|
|
30
|
-
MotifNote,
|
|
31
|
-
MotifOptions,
|
|
32
|
-
MotifTransform,
|
|
33
|
-
} from './motif/index.js';
|
|
34
|
-
export { developMotif, generateMotif, transformMotif } from './motif/index.js';
|
|
35
|
-
export type {
|
|
36
|
-
ProgFunction,
|
|
37
|
-
ProgressionOptions,
|
|
38
|
-
ProgressionPreset,
|
|
39
|
-
ProgStyle,
|
|
40
|
-
} from './progression/index.js';
|
|
41
|
-
export {
|
|
42
|
-
generateProgression,
|
|
43
|
-
progressions,
|
|
44
|
-
progressionsByStyle,
|
|
45
|
-
} from './progression/index.js';
|
|
46
|
-
export type {
|
|
47
|
-
BorrowedChord,
|
|
48
|
-
SubstituteOptions,
|
|
49
|
-
Substitution,
|
|
50
|
-
SubstitutionType,
|
|
51
|
-
} from './reharmony/index.js';
|
|
52
|
-
export {
|
|
53
|
-
modalInterchangePalette,
|
|
54
|
-
negativeHarmonyMirror,
|
|
55
|
-
substituteChord,
|
|
56
|
-
} from './reharmony/index.js';
|
|
57
|
-
export type { RhythmEvent, RhythmOptions } from './rhythm/index.js';
|
|
58
|
-
export { generateRhythm, onsetWeightCurve, rhythmDensity } from './rhythm/index.js';
|