@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,398 +0,0 @@
1
- import type { ChordTimeline } from '../../analyze/timeline/index.js';
2
- import type { KeyScale } from '../../core/types.js';
3
- import type { Chord } from '../../theory/chord/index.js';
4
- import { chordPitchClasses } from '../../theory/chord/index.js';
5
- import { isScaleTone, nearestScaleTone } from '../../theory/scale/index.js';
6
-
7
- /**
8
- * A transformation applicable to a motif cell.
9
- *
10
- * @category Composition
11
- */
12
- export type MotifTransform =
13
- | 'transposeDiatonic'
14
- | 'transposeChromatic'
15
- | 'invert'
16
- | 'retrograde'
17
- | 'augment'
18
- | 'diminish'
19
- | 'sequence';
20
-
21
- /**
22
- * A single note within a motif cell.
23
- *
24
- * @category Composition
25
- */
26
- export type MotifNote = {
27
- pitch: number;
28
- startBeat: number;
29
- durationBeat: number;
30
- };
31
-
32
- /**
33
- * A short melodic cell.
34
- *
35
- * @category Composition
36
- */
37
- export type MotifCell = {
38
- notes: MotifNote[];
39
- };
40
-
41
- /**
42
- * Melodic contour shape for {@link generateMotif}.
43
- *
44
- * @category Composition
45
- */
46
- export type MotifContour = 'arch' | 'ascending' | 'descending' | 'wave';
47
-
48
- /**
49
- * Options controlling {@link generateMotif}.
50
- *
51
- * @category Composition
52
- */
53
- export type MotifOptions = {
54
- key: KeyScale;
55
- chord?: Chord | null;
56
- bars: number;
57
- /**
58
- * Melodic contour shape the line follows.
59
- *
60
- * @defaultValue `'arch'`
61
- */
62
- contour?: MotifContour;
63
- /**
64
- * Probability in [0, 1] that a note is nudged by a single diatonic step. The
65
- * nudge direction is balanced (up or down with equal odds), so it adds
66
- * variety without biasing the line off its contour. Default 0, which
67
- * reproduces the requested contour exactly (no drift, tails return to tonic).
68
- *
69
- * @defaultValue 0
70
- */
71
- jitter?: number;
72
- /**
73
- * Seed for the deterministic PRNG.
74
- *
75
- * @defaultValue 0
76
- */
77
- seed?: number;
78
- };
79
-
80
- function pitchClass(pitch: number): number {
81
- return ((Math.trunc(pitch) % 12) + 12) % 12;
82
- }
83
-
84
- /**
85
- * Smallest span, in beats, treated as a distinct tile step. A positive but
86
- * near-zero cell span is clamped to this so tiling cannot stall.
87
- */
88
- const MIN_TILE_SPAN = 1 / 256;
89
-
90
- /** Nearest in-scale pitch strictly above `pitch`. */
91
- function upScaleTone(pitch: number, key: KeyScale): number {
92
- for (let d = 1; d <= 12; d += 1) {
93
- if (isScaleTone(pitch + d, key)) {
94
- return pitch + d;
95
- }
96
- }
97
- return pitch + 12;
98
- }
99
-
100
- /** Nearest in-scale pitch strictly below `pitch`. */
101
- function downScaleTone(pitch: number, key: KeyScale): number {
102
- for (let d = 1; d <= 12; d += 1) {
103
- if (isScaleTone(pitch - d, key)) {
104
- return pitch - d;
105
- }
106
- }
107
- return pitch - 12;
108
- }
109
-
110
- /** Shift a pitch by a number of diatonic scale degrees. */
111
- function stepDiatonic(pitch: number, degrees: number, key: KeyScale): number {
112
- let p = nearestScaleTone(pitch, key);
113
- const steps = Math.abs(Math.trunc(degrees));
114
- for (let i = 0; i < steps; i += 1) {
115
- p = degrees >= 0 ? upScaleTone(p, key) : downScaleTone(p, key);
116
- }
117
- return p;
118
- }
119
-
120
- /** Nearest chord tone to `pitch`, preferring the lower pitch on a tie. */
121
- function nearestChordTone(pitch: number, chord: Chord): number {
122
- const pcs = chordPitchClasses(chord);
123
- for (let d = 0; d <= 12; d += 1) {
124
- if (pcs.includes(pitchClass(pitch - d))) {
125
- return pitch - d;
126
- }
127
- if (pcs.includes(pitchClass(pitch + d))) {
128
- return pitch + d;
129
- }
130
- }
131
- return pitch;
132
- }
133
-
134
- /** Earliest onset in a cell; the time origin for timing transforms. */
135
- function cellOrigin(notes: MotifNote[]): number {
136
- return notes.length === 0 ? 0 : Math.min(...notes.map((n) => n.startBeat));
137
- }
138
-
139
- /** Total beat span covered by a cell (from first onset to last offset). */
140
- function cellSpan(cell: MotifCell): number {
141
- if (cell.notes.length === 0) {
142
- return 0;
143
- }
144
- const start = cellOrigin(cell.notes);
145
- const end = Math.max(...cell.notes.map((n) => n.startBeat + n.durationBeat));
146
- return end - start;
147
- }
148
-
149
- function clone(cell: MotifCell): MotifCell {
150
- return { notes: cell.notes.map((n) => ({ ...n })) };
151
- }
152
-
153
- /** Deterministic 32-bit PRNG in [0, 1). */
154
- function mulberry32(seed: number): () => number {
155
- let state = seed >>> 0;
156
- return () => {
157
- state = (state + 0x6d2b79f5) >>> 0;
158
- let t = state;
159
- t = Math.imul(t ^ (t >>> 15), t | 1);
160
- t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
161
- return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
162
- };
163
- }
164
-
165
- /** Diatonic scale-degree offsets shaping a contour of `count` notes. */
166
- function contourOffsets(contour: MotifContour, count: number): number[] {
167
- const offsets: number[] = [];
168
- for (let i = 0; i < count; i += 1) {
169
- switch (contour) {
170
- case 'ascending':
171
- offsets.push(i);
172
- break;
173
- case 'descending':
174
- offsets.push(-i);
175
- break;
176
- case 'arch':
177
- // Symmetric rise-and-fall: a palindrome that starts and ends on the
178
- // tonic (offset 0) for both odd and even lengths.
179
- offsets.push(Math.min(i, count - 1 - i));
180
- break;
181
- case 'wave':
182
- offsets.push([0, 1, 0, -1][i % 4] ?? 0);
183
- break;
184
- }
185
- }
186
- return offsets;
187
- }
188
-
189
- /**
190
- * Generate a short seed motif over `bars`.
191
- *
192
- * The melody follows the requested contour in diatonic steps from the tonic,
193
- * snapped to the key. When a chord is supplied, notes landing on bar downbeats
194
- * are pulled to the nearest chord tone. An optional `jitter` adds balanced
195
- * per-note variation without biasing the contour; it is off by default, so the
196
- * contour is reproduced exactly. Output is deterministic for a given seed.
197
- *
198
- * @param opts Key, optional chord, length, contour, jitter, and seed.
199
- * @returns The generated motif cell.
200
- *
201
- * @example
202
- * ```ts
203
- * import { generateMotif, majorKey } from '@libraz/libcantus';
204
- * const cell = generateMotif({ key: majorKey(0), bars: 2, contour: 'arch' });
205
- * // cell.notes is a deterministic MotifCell (seed defaults to 0)
206
- * ```
207
- *
208
- * @category Composition
209
- */
210
- export function generateMotif(opts: MotifOptions): MotifCell {
211
- const contour = opts.contour ?? 'arch';
212
- const bars = Math.max(1, Math.trunc(opts.bars));
213
- const totalBeats = bars * 4;
214
- const noteCount = Math.max(3, bars * 2);
215
- const beatsPerNote = totalBeats / noteCount;
216
- const rng = mulberry32(opts.seed ?? 0);
217
- const jitterProb = Math.min(1, Math.max(0, opts.jitter ?? 0));
218
- const tonic = pitchClass(opts.key.rootPc) + 60;
219
- const offsets = contourOffsets(contour, noteCount);
220
-
221
- const notes: MotifNote[] = [];
222
- for (let i = 0; i < noteCount; i += 1) {
223
- // Opt-in, direction-balanced jitter: disabled by default so the contour is
224
- // preserved exactly. When enabled it nudges up or down with equal odds,
225
- // avoiding the upward bias that used to drift arch/wave tails off the tonic.
226
- let jitter = 0;
227
- if (jitterProb > 0 && rng() < jitterProb) {
228
- jitter = rng() < 0.5 ? 1 : -1;
229
- }
230
- let pitch = stepDiatonic(tonic, (offsets[i] ?? 0) + jitter, opts.key);
231
- const startBeat = i * beatsPerNote;
232
- if (opts.chord && startBeat % 4 === 0) {
233
- pitch = nearestChordTone(pitch, opts.chord);
234
- }
235
- notes.push({ pitch, startBeat, durationBeat: beatsPerNote });
236
- }
237
- return { notes };
238
- }
239
-
240
- /**
241
- * Apply a transformation to a motif cell.
242
- *
243
- * `invert` reflects pitches about the first note (chromatic, self-inverse);
244
- * `retrograde` mirrors onsets about the cell span, preserving rests (self-inverse);
245
- * `augment`/`diminish` scale time by `amount ?? 2` and its reciprocal;
246
- * `transposeChromatic` adds `amount` semitones; `transposeDiatonic` shifts by
247
- * `amount` scale degrees when a `key` is given, or — with no `key` — falls back
248
- * to a chromatic shift of `amount` semitones; `sequence` appends a shifted copy
249
- * (by `amount` diatonic degrees with a `key`, or by `amount` semitones without
250
- * one, since it delegates to `transposeDiatonic`).
251
- *
252
- * @param cell The cell to transform.
253
- * @param t The transformation.
254
- * @param amount Optional parameter (semitones, degrees, or time factor).
255
- * @param key Key context for the diatonic transforms; without it,
256
- * `transposeDiatonic` and `sequence` shift chromatically by semitones.
257
- * @returns The transformed cell.
258
- *
259
- * @example
260
- * ```ts
261
- * import { generateMotif, transformMotif, majorKey } from '@libraz/libcantus';
262
- * const cell = generateMotif({ key: majorKey(0), bars: 1 });
263
- * const inverted = transformMotif(cell, 'invert');
264
- * ```
265
- *
266
- * @category Composition
267
- */
268
- export function transformMotif(
269
- cell: MotifCell,
270
- t: MotifTransform,
271
- amount?: number,
272
- key?: KeyScale,
273
- ): MotifCell {
274
- const notes = cell.notes;
275
- switch (t) {
276
- case 'transposeChromatic': {
277
- const semis = amount ?? 0;
278
- return { notes: notes.map((n) => ({ ...n, pitch: n.pitch + semis })) };
279
- }
280
- case 'transposeDiatonic': {
281
- const degrees = amount ?? 1;
282
- return {
283
- notes: notes.map((n) => ({
284
- ...n,
285
- // With a key, shift by scale degrees; without one, fall back to a
286
- // chromatic shift of `degrees` semitones.
287
- pitch: key ? stepDiatonic(n.pitch, degrees, key) : n.pitch + degrees,
288
- })),
289
- };
290
- }
291
- case 'invert': {
292
- const pivot = notes[0]?.pitch ?? 0;
293
- return { notes: notes.map((n) => ({ ...n, pitch: 2 * pivot - n.pitch })) };
294
- }
295
- case 'retrograde': {
296
- if (notes.length === 0) {
297
- return { notes: [] };
298
- }
299
- const start = cellOrigin(notes);
300
- const end = Math.max(...notes.map((n) => n.startBeat + n.durationBeat));
301
- return {
302
- notes: notes.map((n) => ({
303
- pitch: n.pitch,
304
- startBeat: start + (end - (n.startBeat + n.durationBeat)),
305
- durationBeat: n.durationBeat,
306
- })),
307
- };
308
- }
309
- case 'augment':
310
- return scaleTime(cell, amount ?? 2);
311
- case 'diminish':
312
- return scaleTime(cell, 1 / (amount ?? 2));
313
- case 'sequence': {
314
- const degrees = amount ?? 2;
315
- const span = cellSpan(cell);
316
- // Delegates to transposeDiatonic: a diatonic shift when `key` is given,
317
- // otherwise a chromatic shift of `degrees` semitones.
318
- const copy = transformMotif(cell, 'transposeDiatonic', degrees, key);
319
- const shifted = copy.notes.map((n) => ({ ...n, startBeat: n.startBeat + span }));
320
- return { notes: [...clone(cell).notes, ...shifted] };
321
- }
322
- }
323
- }
324
-
325
- /** Scale a cell's timing about its origin by `factor`. */
326
- function scaleTime(cell: MotifCell, factor: number): MotifCell {
327
- const origin = cellOrigin(cell.notes);
328
- return {
329
- notes: cell.notes.map((n) => ({
330
- pitch: n.pitch,
331
- startBeat: origin + (n.startBeat - origin) * factor,
332
- durationBeat: n.durationBeat * factor,
333
- })),
334
- };
335
- }
336
-
337
- /**
338
- * Lay a motif across `bars` and snap it to a chord timeline.
339
- *
340
- * The cell is tiled back-to-back to fill the requested span; each note is then
341
- * pulled to the nearest chord tone of the segment sounding at its onset, so the
342
- * developed line spells the underlying harmony.
343
- *
344
- * @param cell The source motif.
345
- * @param timeline Chord segments to snap against.
346
- * @param key Key context (used to keep snapped pitches sensible).
347
- * @param bars Number of four-beat bars to fill.
348
- * @returns The developed, harmony-aware cell.
349
- *
350
- * @example
351
- * ```ts
352
- * import { chordTimelineFromChords, developMotif, generateMotif, majorKey } from '@libraz/libcantus';
353
- * const key = majorKey(0);
354
- * const timeline = chordTimelineFromChords([{ rootPc: 0, quality: 'maj', startBeat: 0 }], 8);
355
- * const developed = developMotif(generateMotif({ key, bars: 1 }), timeline, key, 2);
356
- * ```
357
- *
358
- * @category Composition
359
- */
360
- export function developMotif(
361
- cell: MotifCell,
362
- timeline: ChordTimeline,
363
- key: KeyScale,
364
- bars: number,
365
- ): MotifCell {
366
- const span = cellSpan(cell);
367
- const totalBeats = Math.max(1, Math.trunc(bars)) * 4;
368
- const origin = cellOrigin(cell.notes);
369
- const out: MotifNote[] = [];
370
-
371
- if (span <= 0) {
372
- return clone(cell);
373
- }
374
-
375
- // Clamp a degenerate near-zero span so the tile count stays finite, and step
376
- // the offset multiplicatively so rounding error cannot accumulate over tiles.
377
- const tileSpan = Math.max(span, MIN_TILE_SPAN);
378
- const tileCount = Math.ceil(totalBeats / tileSpan);
379
- for (let k = 0; k < tileCount; k += 1) {
380
- const offset = k * tileSpan;
381
- for (const n of cell.notes) {
382
- const startBeat = offset + (n.startBeat - origin);
383
- if (startBeat >= totalBeats) {
384
- continue;
385
- }
386
- const chord = timeline.at(startBeat);
387
- let pitch = n.pitch;
388
- if (chord) {
389
- const isMember = chordPitchClasses(chord).includes(pitchClass(pitch));
390
- pitch = isMember ? pitch : nearestChordTone(pitch, chord);
391
- } else {
392
- pitch = nearestScaleTone(pitch, key);
393
- }
394
- out.push({ pitch, startBeat, durationBeat: n.durationBeat });
395
- }
396
- }
397
- return { notes: out };
398
- }