@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.
Files changed (201) hide show
  1. package/README.md +29 -20
  2. package/README_ja.md +268 -0
  3. package/dist/analyze/index.cjs +39 -1947
  4. package/dist/analyze/index.cjs.map +1 -1
  5. package/dist/analyze/index.d.cts +161 -91
  6. package/dist/analyze/index.d.ts +161 -91
  7. package/dist/analyze/index.js +19 -8
  8. package/dist/chunk-2A2QMA74.cjs +261 -0
  9. package/dist/chunk-2A2QMA74.cjs.map +1 -0
  10. package/dist/chunk-2PTFGVD7.js +261 -0
  11. package/dist/chunk-2PTFGVD7.js.map +1 -0
  12. package/dist/chunk-357ZDPVU.cjs +162 -0
  13. package/dist/chunk-357ZDPVU.cjs.map +1 -0
  14. package/dist/chunk-3KX76RLG.js +1548 -0
  15. package/dist/chunk-3KX76RLG.js.map +1 -0
  16. package/dist/chunk-4VZ5MZAM.cjs +889 -0
  17. package/dist/chunk-4VZ5MZAM.cjs.map +1 -0
  18. package/dist/{chunk-C5XYKUVG.js → chunk-7EONYVI5.js} +471 -171
  19. package/dist/chunk-7EONYVI5.js.map +1 -0
  20. package/dist/{chunk-IT2O4XFE.js → chunk-7JEP3MFY.js} +42 -34
  21. package/dist/chunk-7JEP3MFY.js.map +1 -0
  22. package/dist/{chunk-4MHA432G.js → chunk-AID75RN3.js} +572 -250
  23. package/dist/chunk-AID75RN3.js.map +1 -0
  24. package/dist/chunk-BSLTLGM3.cjs +497 -0
  25. package/dist/chunk-BSLTLGM3.cjs.map +1 -0
  26. package/dist/chunk-CU6VG2YM.cjs +109 -0
  27. package/dist/chunk-CU6VG2YM.cjs.map +1 -0
  28. package/dist/{chunk-EH6EM37U.js → chunk-DZMHIGI4.js} +81 -59
  29. package/dist/chunk-DZMHIGI4.js.map +1 -0
  30. package/dist/{chunk-AKLYPWIN.js → chunk-EDOTEABK.js} +45 -19
  31. package/dist/chunk-EDOTEABK.js.map +1 -0
  32. package/dist/{chunk-2ZWMSJH6.js → chunk-HN2OEUN2.js} +7 -4
  33. package/dist/chunk-HN2OEUN2.js.map +1 -0
  34. package/dist/{chunk-MIWO425M.js → chunk-KBTTEX7Q.js} +128 -43
  35. package/dist/chunk-KBTTEX7Q.js.map +1 -0
  36. package/dist/chunk-KQN2ZYND.cjs +724 -0
  37. package/dist/chunk-KQN2ZYND.cjs.map +1 -0
  38. package/dist/chunk-MPTVJIVG.cjs +3176 -0
  39. package/dist/chunk-MPTVJIVG.cjs.map +1 -0
  40. package/dist/chunk-MTMZTMWM.cjs +53 -0
  41. package/dist/chunk-MTMZTMWM.cjs.map +1 -0
  42. package/dist/{chunk-CHMXELDS.js → chunk-NNB67XA2.js} +372 -197
  43. package/dist/chunk-NNB67XA2.js.map +1 -0
  44. package/dist/chunk-OL3IHHSJ.cjs +1204 -0
  45. package/dist/chunk-OL3IHHSJ.cjs.map +1 -0
  46. package/dist/chunk-PI3UXRWK.cjs +528 -0
  47. package/dist/chunk-PI3UXRWK.cjs.map +1 -0
  48. package/dist/chunk-R2VVFAIN.js +528 -0
  49. package/dist/chunk-R2VVFAIN.js.map +1 -0
  50. package/dist/chunk-T64C7ZD7.cjs +1 -0
  51. package/dist/chunk-T64C7ZD7.cjs.map +1 -0
  52. package/dist/chunk-WFDXOLUB.cjs +337 -0
  53. package/dist/chunk-WFDXOLUB.cjs.map +1 -0
  54. package/dist/chunk-WTAN4ACG.cjs +1548 -0
  55. package/dist/chunk-WTAN4ACG.cjs.map +1 -0
  56. package/dist/chunk-YFJXS3AU.cjs +33 -0
  57. package/dist/chunk-YFJXS3AU.cjs.map +1 -0
  58. package/dist/{chunk-GOAW4VYL.js → chunk-YUE6TJ4M.js} +315 -145
  59. package/dist/chunk-YUE6TJ4M.js.map +1 -0
  60. package/dist/{chunk-62QZKCJQ.js → chunk-ZGLJM4A4.js} +10 -6
  61. package/dist/chunk-ZGLJM4A4.js.map +1 -0
  62. package/dist/core/index.cjs +140 -647
  63. package/dist/core/index.cjs.map +1 -1
  64. package/dist/core/index.d.cts +313 -41
  65. package/dist/core/index.d.ts +313 -41
  66. package/dist/core/index.js +64 -16
  67. package/dist/{function-DVH4Eusr.d.ts → function-CxixTB02.d.cts} +122 -10
  68. package/dist/{function-DYt4iKsn.d.cts → function-DTCt675z.d.ts} +122 -10
  69. package/dist/generate/index.cjs +28 -4085
  70. package/dist/generate/index.cjs.map +1 -1
  71. package/dist/generate/index.d.cts +374 -66
  72. package/dist/generate/index.d.ts +374 -66
  73. package/dist/generate/index.js +23 -9
  74. package/dist/{index-Jc0Ryi79.d.ts → index-B2Ulxxpo.d.cts} +21 -5
  75. package/dist/{index-CpOsP1y9.d.cts → index-BkYJ0xMq.d.cts} +18 -17
  76. package/dist/{index-CxqBQtH1.d.cts → index-BuaB-ypt.d.ts} +21 -5
  77. package/dist/index-CAN7v-kw.d.ts +199 -0
  78. package/dist/{index-Bt4tOUD9.d.ts → index-COw1Ky9g.d.ts} +93 -8
  79. package/dist/{index-Dikst44v.d.cts → index-Cfx8xgH0.d.ts} +14 -11
  80. package/dist/{index-DxvARIP2.d.ts → index-DRtIhlJj.d.ts} +18 -17
  81. package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
  82. package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
  83. package/dist/index-alrRninU.d.cts +199 -0
  84. package/dist/{index-BNlGP86A.d.cts → index-oTQbYYIs.d.cts} +93 -8
  85. package/dist/index-tR9jTn26.d.cts +275 -0
  86. package/dist/index-tR9jTn26.d.ts +275 -0
  87. package/dist/{index-Bg5dYMmZ.d.ts → index-x2vcsduE.d.cts} +14 -11
  88. package/dist/index.cjs +429 -7383
  89. package/dist/index.cjs.map +1 -1
  90. package/dist/index.d.cts +15 -15
  91. package/dist/index.d.ts +15 -15
  92. package/dist/index.js +121 -27
  93. package/dist/model/index.cjs +8 -2562
  94. package/dist/model/index.cjs.map +1 -1
  95. package/dist/model/index.d.cts +471 -81
  96. package/dist/model/index.d.ts +471 -81
  97. package/dist/model/index.js +6 -7
  98. package/dist/styled-Bl0ZK28h.d.ts +543 -0
  99. package/dist/styled-Crv6SA4S.d.cts +543 -0
  100. package/dist/theory/index.cjs +172 -1822
  101. package/dist/theory/index.cjs.map +1 -1
  102. package/dist/theory/index.d.cts +151 -233
  103. package/dist/theory/index.d.ts +151 -233
  104. package/dist/theory/index.js +30 -7
  105. package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
  106. package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
  107. package/package.json +34 -13
  108. package/dist/chunk-2ZWMSJH6.js.map +0 -1
  109. package/dist/chunk-4MHA432G.js.map +0 -1
  110. package/dist/chunk-62QZKCJQ.js.map +0 -1
  111. package/dist/chunk-AKLYPWIN.js.map +0 -1
  112. package/dist/chunk-C5XYKUVG.js.map +0 -1
  113. package/dist/chunk-CHMXELDS.js.map +0 -1
  114. package/dist/chunk-DYWFNC4A.js +0 -152
  115. package/dist/chunk-DYWFNC4A.js.map +0 -1
  116. package/dist/chunk-EH6EM37U.js.map +0 -1
  117. package/dist/chunk-FBQK3ZQN.js +0 -131
  118. package/dist/chunk-FBQK3ZQN.js.map +0 -1
  119. package/dist/chunk-GOAW4VYL.js.map +0 -1
  120. package/dist/chunk-I5R7ZBJS.js +0 -202
  121. package/dist/chunk-I5R7ZBJS.js.map +0 -1
  122. package/dist/chunk-IT2O4XFE.js.map +0 -1
  123. package/dist/chunk-MIWO425M.js.map +0 -1
  124. package/dist/chunk-SGU7SPLK.js +0 -924
  125. package/dist/chunk-SGU7SPLK.js.map +0 -1
  126. package/dist/index-CGMZNCx6.d.cts +0 -140
  127. package/dist/index-CGMZNCx6.d.ts +0 -140
  128. package/dist/index-CnZCo29u.d.cts +0 -108
  129. package/dist/index-JnF4v9bx.d.ts +0 -108
  130. package/dist/styled-DZ7GjShb.d.ts +0 -282
  131. package/dist/styled-ZTbdaVFM.d.cts +0 -282
  132. package/src/analyze/arrange/index.ts +0 -21
  133. package/src/analyze/arrange/internal.ts +0 -127
  134. package/src/analyze/arrange/tension.ts +0 -199
  135. package/src/analyze/arrange/tracks.ts +0 -312
  136. package/src/analyze/detect/index.ts +0 -272
  137. package/src/analyze/functional/borrowed.ts +0 -84
  138. package/src/analyze/functional/cadence.ts +0 -66
  139. package/src/analyze/functional/function.ts +0 -184
  140. package/src/analyze/functional/index.ts +0 -23
  141. package/src/analyze/functional/internal.ts +0 -29
  142. package/src/analyze/functional/roman.ts +0 -402
  143. package/src/analyze/index.ts +0 -49
  144. package/src/analyze/timeline/index.ts +0 -451
  145. package/src/analyze/voice/index.ts +0 -339
  146. package/src/core/event-index/index.ts +0 -104
  147. package/src/core/index.ts +0 -61
  148. package/src/core/interval/index.ts +0 -81
  149. package/src/core/meter/index.ts +0 -302
  150. package/src/core/pitch/index.ts +0 -311
  151. package/src/core/random/index.ts +0 -76
  152. package/src/core/tuning/index.ts +0 -186
  153. package/src/core/types.ts +0 -29
  154. package/src/core/validation/index.ts +0 -119
  155. package/src/generate/bass/index.ts +0 -373
  156. package/src/generate/countermelody/index.ts +0 -470
  157. package/src/generate/drums/beat.ts +0 -266
  158. package/src/generate/drums/euclid.ts +0 -65
  159. package/src/generate/drums/fills.ts +0 -356
  160. package/src/generate/drums/ghost.ts +0 -119
  161. package/src/generate/drums/hihat.ts +0 -285
  162. package/src/generate/drums/hit.ts +0 -31
  163. package/src/generate/drums/index.ts +0 -361
  164. package/src/generate/drums/internal.ts +0 -257
  165. package/src/generate/drums/kick.ts +0 -160
  166. package/src/generate/drums/percussion.ts +0 -121
  167. package/src/generate/drums/rng.ts +0 -7
  168. package/src/generate/drums/swing.ts +0 -71
  169. package/src/generate/groove/index.ts +0 -351
  170. package/src/generate/harmonize/index.ts +0 -451
  171. package/src/generate/index.ts +0 -58
  172. package/src/generate/motif/index.ts +0 -422
  173. package/src/generate/progression/index.ts +0 -362
  174. package/src/generate/reharmony/index.ts +0 -323
  175. package/src/generate/rhythm/index.ts +0 -175
  176. package/src/index.ts +0 -13
  177. package/src/model/chord.ts +0 -442
  178. package/src/model/index.ts +0 -15
  179. package/src/model/interval.ts +0 -82
  180. package/src/model/key.ts +0 -266
  181. package/src/model/note.ts +0 -176
  182. package/src/model/progression.ts +0 -161
  183. package/src/model/shared.ts +0 -12
  184. package/src/theory/chord/index.ts +0 -375
  185. package/src/theory/chordscale/index.ts +0 -374
  186. package/src/theory/counterpoint/index.ts +0 -230
  187. package/src/theory/harmony/index.ts +0 -77
  188. package/src/theory/index.ts +0 -112
  189. package/src/theory/safety/index.ts +0 -488
  190. package/src/theory/scale/degrees.ts +0 -123
  191. package/src/theory/scale/index.ts +0 -28
  192. package/src/theory/scale/key.ts +0 -56
  193. package/src/theory/scale/masks.ts +0 -153
  194. package/src/theory/spelling/index.ts +0 -249
  195. package/src/theory/symbol/index.ts +0 -268
  196. package/src/theory/voicing/index.ts +0 -5
  197. package/src/theory/voicing/internal.ts +0 -168
  198. package/src/theory/voicing/leading.ts +0 -112
  199. package/src/theory/voicing/satb.ts +0 -216
  200. package/src/theory/voicing/styled.ts +0 -199
  201. 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';