@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.
Files changed (198) hide show
  1. package/README.md +2 -2
  2. package/dist/analyze/index.cjs +39 -1947
  3. package/dist/analyze/index.cjs.map +1 -1
  4. package/dist/analyze/index.d.cts +197 -37
  5. package/dist/analyze/index.d.ts +197 -37
  6. package/dist/analyze/index.js +19 -8
  7. package/dist/chunk-36N73KZ3.cjs +246 -0
  8. package/dist/chunk-36N73KZ3.cjs.map +1 -0
  9. package/dist/chunk-4HLLZPXT.cjs +3077 -0
  10. package/dist/chunk-4HLLZPXT.cjs.map +1 -0
  11. package/dist/chunk-7MGZ7D62.cjs +498 -0
  12. package/dist/chunk-7MGZ7D62.cjs.map +1 -0
  13. package/dist/{chunk-4MHA432G.js → chunk-7NKDPF74.js} +403 -180
  14. package/dist/chunk-7NKDPF74.js.map +1 -0
  15. package/dist/chunk-CAN76NCU.cjs +679 -0
  16. package/dist/chunk-CAN76NCU.cjs.map +1 -0
  17. package/dist/{chunk-MIWO425M.js → chunk-D2FAJBYL.js} +101 -34
  18. package/dist/chunk-D2FAJBYL.js.map +1 -0
  19. package/dist/{chunk-SGU7SPLK.js → chunk-EHVEBWM5.js} +1056 -531
  20. package/dist/chunk-EHVEBWM5.js.map +1 -0
  21. package/dist/chunk-FCANXBAT.cjs +30 -0
  22. package/dist/chunk-FCANXBAT.cjs.map +1 -0
  23. package/dist/chunk-FNDWXRRI.cjs +479 -0
  24. package/dist/chunk-FNDWXRRI.cjs.map +1 -0
  25. package/dist/{chunk-IT2O4XFE.js → chunk-GQSGS2N4.js} +43 -24
  26. package/dist/chunk-GQSGS2N4.js.map +1 -0
  27. package/dist/{chunk-62QZKCJQ.js → chunk-HKGYCQ34.js} +10 -6
  28. package/dist/chunk-HKGYCQ34.js.map +1 -0
  29. package/dist/{chunk-AKLYPWIN.js → chunk-I24MV65U.js} +29 -6
  30. package/dist/chunk-I24MV65U.js.map +1 -0
  31. package/dist/{chunk-CHMXELDS.js → chunk-KMHOICIY.js} +311 -181
  32. package/dist/chunk-KMHOICIY.js.map +1 -0
  33. package/dist/chunk-LW53AHQU.cjs +1117 -0
  34. package/dist/chunk-LW53AHQU.cjs.map +1 -0
  35. package/dist/{chunk-2ZWMSJH6.js → chunk-NKIBQ6PT.js} +2 -2
  36. package/dist/chunk-NKIBQ6PT.js.map +1 -0
  37. package/dist/chunk-NP4NA2FB.cjs +106 -0
  38. package/dist/chunk-NP4NA2FB.cjs.map +1 -0
  39. package/dist/chunk-NVE34YGZ.cjs +1449 -0
  40. package/dist/chunk-NVE34YGZ.cjs.map +1 -0
  41. package/dist/{chunk-EH6EM37U.js → chunk-NYYWMV2C.js} +61 -49
  42. package/dist/chunk-NYYWMV2C.js.map +1 -0
  43. package/dist/{chunk-GOAW4VYL.js → chunk-Q2XUP227.js} +243 -117
  44. package/dist/chunk-Q2XUP227.js.map +1 -0
  45. package/dist/chunk-Q6OP4U43.cjs +327 -0
  46. package/dist/chunk-Q6OP4U43.cjs.map +1 -0
  47. package/dist/chunk-RC6GBMCD.cjs +845 -0
  48. package/dist/chunk-RC6GBMCD.cjs.map +1 -0
  49. package/dist/{chunk-C5XYKUVG.js → chunk-RK35RIN7.js} +370 -157
  50. package/dist/chunk-RK35RIN7.js.map +1 -0
  51. package/dist/chunk-RUMU3UA6.cjs +53 -0
  52. package/dist/chunk-RUMU3UA6.cjs.map +1 -0
  53. package/dist/chunk-T64C7ZD7.cjs +1 -0
  54. package/dist/chunk-T64C7ZD7.cjs.map +1 -0
  55. package/dist/chunk-XEBG3G53.js +498 -0
  56. package/dist/chunk-XEBG3G53.js.map +1 -0
  57. package/dist/{chunk-I5R7ZBJS.js → chunk-YLEMGEFH.js} +103 -59
  58. package/dist/chunk-YLEMGEFH.js.map +1 -0
  59. package/dist/chunk-ZEJR4TKL.cjs +173 -0
  60. package/dist/chunk-ZEJR4TKL.cjs.map +1 -0
  61. package/dist/core/index.cjs +130 -647
  62. package/dist/core/index.cjs.map +1 -1
  63. package/dist/core/index.d.cts +301 -39
  64. package/dist/core/index.d.ts +301 -39
  65. package/dist/core/index.js +54 -16
  66. package/dist/{function-DYt4iKsn.d.cts → function-BtiZb9i9.d.ts} +28 -7
  67. package/dist/{function-DVH4Eusr.d.ts → function-DNugAOrt.d.cts} +28 -7
  68. package/dist/generate/index.cjs +28 -4085
  69. package/dist/generate/index.cjs.map +1 -1
  70. package/dist/generate/index.d.cts +370 -62
  71. package/dist/generate/index.d.ts +370 -62
  72. package/dist/generate/index.js +23 -9
  73. package/dist/{index-CxqBQtH1.d.cts → index-6VR8-kit.d.ts} +19 -4
  74. package/dist/index-70p168VM.d.ts +192 -0
  75. package/dist/{index-CpOsP1y9.d.cts → index-Be74mcon.d.cts} +18 -17
  76. package/dist/index-BtXT6UQ0.d.cts +192 -0
  77. package/dist/{index-DxvARIP2.d.ts → index-C5UBbQ4G.d.ts} +18 -17
  78. package/dist/{index-Bt4tOUD9.d.ts → index-C83mZI1P.d.ts} +93 -8
  79. package/dist/{index-Bg5dYMmZ.d.ts → index-D6b6na69.d.cts} +14 -11
  80. package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
  81. package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
  82. package/dist/index-OG65ocOr.d.cts +272 -0
  83. package/dist/index-OG65ocOr.d.ts +272 -0
  84. package/dist/{index-Jc0Ryi79.d.ts → index-tJhkmQgE.d.cts} +19 -4
  85. package/dist/{index-Dikst44v.d.cts → index-tSQh-xwY.d.ts} +14 -11
  86. package/dist/{index-BNlGP86A.d.cts → index-y_ak9_Hi.d.cts} +93 -8
  87. package/dist/index.cjs +419 -7383
  88. package/dist/index.cjs.map +1 -1
  89. package/dist/index.d.cts +14 -14
  90. package/dist/index.d.ts +14 -14
  91. package/dist/index.js +111 -27
  92. package/dist/model/index.cjs +8 -2562
  93. package/dist/model/index.cjs.map +1 -1
  94. package/dist/model/index.d.cts +433 -75
  95. package/dist/model/index.d.ts +433 -75
  96. package/dist/model/index.js +6 -7
  97. package/dist/styled-DIRkk9Uz.d.ts +506 -0
  98. package/dist/styled-DJbkJQfj.d.cts +506 -0
  99. package/dist/theory/index.cjs +172 -1822
  100. package/dist/theory/index.cjs.map +1 -1
  101. package/dist/theory/index.d.cts +151 -233
  102. package/dist/theory/index.d.ts +151 -233
  103. package/dist/theory/index.js +30 -7
  104. package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
  105. package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
  106. package/package.json +24 -6
  107. package/dist/chunk-2ZWMSJH6.js.map +0 -1
  108. package/dist/chunk-4MHA432G.js.map +0 -1
  109. package/dist/chunk-62QZKCJQ.js.map +0 -1
  110. package/dist/chunk-AKLYPWIN.js.map +0 -1
  111. package/dist/chunk-C5XYKUVG.js.map +0 -1
  112. package/dist/chunk-CHMXELDS.js.map +0 -1
  113. package/dist/chunk-DYWFNC4A.js +0 -152
  114. package/dist/chunk-DYWFNC4A.js.map +0 -1
  115. package/dist/chunk-EH6EM37U.js.map +0 -1
  116. package/dist/chunk-FBQK3ZQN.js +0 -131
  117. package/dist/chunk-FBQK3ZQN.js.map +0 -1
  118. package/dist/chunk-GOAW4VYL.js.map +0 -1
  119. package/dist/chunk-I5R7ZBJS.js.map +0 -1
  120. package/dist/chunk-IT2O4XFE.js.map +0 -1
  121. package/dist/chunk-MIWO425M.js.map +0 -1
  122. package/dist/chunk-SGU7SPLK.js.map +0 -1
  123. package/dist/index-CGMZNCx6.d.cts +0 -140
  124. package/dist/index-CGMZNCx6.d.ts +0 -140
  125. package/dist/index-CnZCo29u.d.cts +0 -108
  126. package/dist/index-JnF4v9bx.d.ts +0 -108
  127. package/dist/styled-DZ7GjShb.d.ts +0 -282
  128. package/dist/styled-ZTbdaVFM.d.cts +0 -282
  129. package/src/analyze/arrange/index.ts +0 -21
  130. package/src/analyze/arrange/internal.ts +0 -127
  131. package/src/analyze/arrange/tension.ts +0 -199
  132. package/src/analyze/arrange/tracks.ts +0 -312
  133. package/src/analyze/detect/index.ts +0 -272
  134. package/src/analyze/functional/borrowed.ts +0 -84
  135. package/src/analyze/functional/cadence.ts +0 -66
  136. package/src/analyze/functional/function.ts +0 -184
  137. package/src/analyze/functional/index.ts +0 -23
  138. package/src/analyze/functional/internal.ts +0 -29
  139. package/src/analyze/functional/roman.ts +0 -402
  140. package/src/analyze/index.ts +0 -49
  141. package/src/analyze/timeline/index.ts +0 -451
  142. package/src/analyze/voice/index.ts +0 -339
  143. package/src/core/event-index/index.ts +0 -104
  144. package/src/core/index.ts +0 -61
  145. package/src/core/interval/index.ts +0 -81
  146. package/src/core/meter/index.ts +0 -302
  147. package/src/core/pitch/index.ts +0 -311
  148. package/src/core/random/index.ts +0 -76
  149. package/src/core/tuning/index.ts +0 -186
  150. package/src/core/types.ts +0 -29
  151. package/src/core/validation/index.ts +0 -119
  152. package/src/generate/bass/index.ts +0 -373
  153. package/src/generate/countermelody/index.ts +0 -470
  154. package/src/generate/drums/beat.ts +0 -266
  155. package/src/generate/drums/euclid.ts +0 -65
  156. package/src/generate/drums/fills.ts +0 -356
  157. package/src/generate/drums/ghost.ts +0 -119
  158. package/src/generate/drums/hihat.ts +0 -285
  159. package/src/generate/drums/hit.ts +0 -31
  160. package/src/generate/drums/index.ts +0 -361
  161. package/src/generate/drums/internal.ts +0 -257
  162. package/src/generate/drums/kick.ts +0 -160
  163. package/src/generate/drums/percussion.ts +0 -121
  164. package/src/generate/drums/rng.ts +0 -7
  165. package/src/generate/drums/swing.ts +0 -71
  166. package/src/generate/groove/index.ts +0 -351
  167. package/src/generate/harmonize/index.ts +0 -451
  168. package/src/generate/index.ts +0 -58
  169. package/src/generate/motif/index.ts +0 -422
  170. package/src/generate/progression/index.ts +0 -362
  171. package/src/generate/reharmony/index.ts +0 -323
  172. package/src/generate/rhythm/index.ts +0 -175
  173. package/src/index.ts +0 -13
  174. package/src/model/chord.ts +0 -442
  175. package/src/model/index.ts +0 -15
  176. package/src/model/interval.ts +0 -82
  177. package/src/model/key.ts +0 -266
  178. package/src/model/note.ts +0 -176
  179. package/src/model/progression.ts +0 -161
  180. package/src/model/shared.ts +0 -12
  181. package/src/theory/chord/index.ts +0 -375
  182. package/src/theory/chordscale/index.ts +0 -374
  183. package/src/theory/counterpoint/index.ts +0 -230
  184. package/src/theory/harmony/index.ts +0 -77
  185. package/src/theory/index.ts +0 -112
  186. package/src/theory/safety/index.ts +0 -488
  187. package/src/theory/scale/degrees.ts +0 -123
  188. package/src/theory/scale/index.ts +0 -28
  189. package/src/theory/scale/key.ts +0 -56
  190. package/src/theory/scale/masks.ts +0 -153
  191. package/src/theory/spelling/index.ts +0 -249
  192. package/src/theory/symbol/index.ts +0 -268
  193. package/src/theory/voicing/index.ts +0 -5
  194. package/src/theory/voicing/internal.ts +0 -168
  195. package/src/theory/voicing/leading.ts +0 -112
  196. package/src/theory/voicing/satb.ts +0 -216
  197. package/src/theory/voicing/styled.ts +0 -199
  198. 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
- }
@@ -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';