@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,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
- }