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