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