@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.
- package/README.md +6 -5
- package/dist/analyze/index.cjs +41 -1676
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +194 -122
- package/dist/analyze/index.d.ts +194 -122
- package/dist/analyze/index.js +19 -8
- package/dist/chunk-36N73KZ3.cjs +246 -0
- package/dist/chunk-36N73KZ3.cjs.map +1 -0
- package/dist/chunk-4HLLZPXT.cjs +3077 -0
- package/dist/chunk-4HLLZPXT.cjs.map +1 -0
- package/dist/chunk-7MGZ7D62.cjs +498 -0
- package/dist/chunk-7MGZ7D62.cjs.map +1 -0
- package/dist/{chunk-R3S2LUBH.js → chunk-7NKDPF74.js} +660 -305
- package/dist/chunk-7NKDPF74.js.map +1 -0
- package/dist/chunk-CAN76NCU.cjs +679 -0
- package/dist/chunk-CAN76NCU.cjs.map +1 -0
- package/dist/{chunk-7Y7WF64D.js → chunk-D2FAJBYL.js} +104 -35
- package/dist/chunk-D2FAJBYL.js.map +1 -0
- package/dist/{chunk-COKTX2G5.js → chunk-EHVEBWM5.js} +1052 -510
- package/dist/chunk-EHVEBWM5.js.map +1 -0
- package/dist/chunk-FCANXBAT.cjs +30 -0
- package/dist/chunk-FCANXBAT.cjs.map +1 -0
- package/dist/chunk-FNDWXRRI.cjs +479 -0
- package/dist/chunk-FNDWXRRI.cjs.map +1 -0
- package/dist/{chunk-GQKN5KDT.js → chunk-GQSGS2N4.js} +43 -29
- package/dist/chunk-GQSGS2N4.js.map +1 -0
- package/dist/chunk-HKGYCQ34.js +53 -0
- package/dist/chunk-HKGYCQ34.js.map +1 -0
- package/dist/chunk-I24MV65U.js +106 -0
- package/dist/chunk-I24MV65U.js.map +1 -0
- package/dist/{chunk-LN7ZZMD2.js → chunk-KMHOICIY.js} +322 -173
- package/dist/chunk-KMHOICIY.js.map +1 -0
- package/dist/chunk-LW53AHQU.cjs +1117 -0
- package/dist/chunk-LW53AHQU.cjs.map +1 -0
- package/dist/chunk-N6IHQUOX.js +1 -0
- package/dist/chunk-N6IHQUOX.js.map +1 -0
- package/dist/chunk-NKIBQ6PT.js +30 -0
- package/dist/chunk-NKIBQ6PT.js.map +1 -0
- package/dist/chunk-NP4NA2FB.cjs +106 -0
- package/dist/chunk-NP4NA2FB.cjs.map +1 -0
- package/dist/chunk-NVE34YGZ.cjs +1449 -0
- package/dist/chunk-NVE34YGZ.cjs.map +1 -0
- package/dist/{chunk-A4532ZFR.js → chunk-NYYWMV2C.js} +115 -47
- package/dist/chunk-NYYWMV2C.js.map +1 -0
- package/dist/{chunk-5HTNMQW4.js → chunk-Q2XUP227.js} +310 -118
- package/dist/chunk-Q2XUP227.js.map +1 -0
- package/dist/chunk-Q6OP4U43.cjs +327 -0
- package/dist/chunk-Q6OP4U43.cjs.map +1 -0
- package/dist/chunk-RC6GBMCD.cjs +845 -0
- package/dist/chunk-RC6GBMCD.cjs.map +1 -0
- package/dist/{chunk-XTC3X6MS.js → chunk-RK35RIN7.js} +453 -152
- package/dist/chunk-RK35RIN7.js.map +1 -0
- package/dist/chunk-RUMU3UA6.cjs +53 -0
- package/dist/chunk-RUMU3UA6.cjs.map +1 -0
- package/dist/chunk-T64C7ZD7.cjs +1 -0
- package/dist/chunk-T64C7ZD7.cjs.map +1 -0
- package/dist/chunk-XEBG3G53.js +498 -0
- package/dist/chunk-XEBG3G53.js.map +1 -0
- package/dist/chunk-YLEMGEFH.js +246 -0
- package/dist/chunk-YLEMGEFH.js.map +1 -0
- package/dist/chunk-ZEJR4TKL.cjs +173 -0
- package/dist/chunk-ZEJR4TKL.cjs.map +1 -0
- package/dist/core/index.cjs +130 -431
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +331 -17
- package/dist/core/index.d.ts +331 -17
- package/dist/core/index.js +71 -13
- package/dist/{function-2bJW1Zp_.d.ts → function-BtiZb9i9.d.ts} +28 -7
- package/dist/{function-r2sh-jOO.d.cts → function-DNugAOrt.d.cts} +28 -7
- package/dist/generate/index.cjs +30 -3751
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +373 -59
- package/dist/generate/index.d.ts +373 -59
- package/dist/generate/index.js +23 -9
- package/dist/{index-DZ1M21do.d.cts → index-6VR8-kit.d.ts} +20 -4
- package/dist/index-70p168VM.d.ts +192 -0
- package/dist/{index-D_ZTUNKp.d.cts → index-Be74mcon.d.cts} +18 -17
- package/dist/index-BtXT6UQ0.d.cts +192 -0
- package/dist/{index-BMPxwjwV.d.ts → index-C5UBbQ4G.d.ts} +18 -17
- package/dist/{index-BOBXBNwP.d.cts → index-C83mZI1P.d.ts} +93 -8
- package/dist/{index-BdeQ1r_p.d.cts → index-D6b6na69.d.cts} +14 -11
- package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
- package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
- package/dist/index-OG65ocOr.d.cts +272 -0
- package/dist/index-OG65ocOr.d.ts +272 -0
- package/dist/{index-BreoJRw0.d.ts → index-tJhkmQgE.d.cts} +20 -4
- package/dist/{index-CP3wAfEH.d.ts → index-tSQh-xwY.d.ts} +14 -11
- package/dist/{index-CM5W4rcm.d.ts → index-y_ak9_Hi.d.cts} +93 -8
- package/dist/index.cjs +419 -6848
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -13
- package/dist/index.d.ts +14 -13
- package/dist/index.js +132 -27
- package/dist/model/index.cjs +8 -2423
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +433 -74
- package/dist/model/index.d.ts +433 -74
- package/dist/model/index.js +6 -7
- package/dist/styled-DIRkk9Uz.d.ts +506 -0
- package/dist/styled-DJbkJQfj.d.cts +506 -0
- package/dist/theory/index.cjs +172 -1676
- package/dist/theory/index.cjs.map +1 -1
- package/dist/theory/index.d.cts +151 -233
- package/dist/theory/index.d.ts +151 -233
- package/dist/theory/index.js +31 -7
- package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
- package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
- package/package.json +73 -24
- package/dist/chunk-5HTNMQW4.js.map +0 -1
- package/dist/chunk-7Y7WF64D.js.map +0 -1
- package/dist/chunk-A4532ZFR.js.map +0 -1
- package/dist/chunk-BHUT2S5G.js +0 -22
- package/dist/chunk-BHUT2S5G.js.map +0 -1
- package/dist/chunk-CCBLVAPK.js +0 -42
- package/dist/chunk-CCBLVAPK.js.map +0 -1
- package/dist/chunk-COKTX2G5.js.map +0 -1
- package/dist/chunk-DYWFNC4A.js +0 -152
- package/dist/chunk-DYWFNC4A.js.map +0 -1
- package/dist/chunk-GQKN5KDT.js.map +0 -1
- package/dist/chunk-LN7ZZMD2.js.map +0 -1
- package/dist/chunk-MSWFZF7J.js +0 -118
- package/dist/chunk-MSWFZF7J.js.map +0 -1
- package/dist/chunk-R3S2LUBH.js.map +0 -1
- package/dist/chunk-SDM3VJJ5.js +0 -41
- package/dist/chunk-SDM3VJJ5.js.map +0 -1
- package/dist/chunk-XTC3X6MS.js.map +0 -1
- package/dist/chunk-ZXW5YIWK.js +0 -55
- package/dist/chunk-ZXW5YIWK.js.map +0 -1
- package/dist/index-CGMZNCx6.d.cts +0 -140
- package/dist/index-CGMZNCx6.d.ts +0 -140
- package/dist/styled-BjXtOFNk.d.cts +0 -282
- package/dist/styled-D7BAx3S3.d.ts +0 -282
- package/src/analyze/arrange/index.ts +0 -21
- package/src/analyze/arrange/internal.ts +0 -125
- package/src/analyze/arrange/tension.ts +0 -187
- package/src/analyze/arrange/tracks.ts +0 -279
- package/src/analyze/detect/index.ts +0 -235
- package/src/analyze/functional/borrowed.ts +0 -84
- package/src/analyze/functional/cadence.ts +0 -66
- package/src/analyze/functional/function.ts +0 -184
- package/src/analyze/functional/index.ts +0 -23
- package/src/analyze/functional/internal.ts +0 -29
- package/src/analyze/functional/roman.ts +0 -393
- package/src/analyze/index.ts +0 -49
- package/src/analyze/timeline/index.ts +0 -415
- package/src/analyze/voice/index.ts +0 -330
- package/src/core/index.ts +0 -47
- package/src/core/interval/index.ts +0 -81
- package/src/core/meter/index.ts +0 -282
- package/src/core/pitch/index.ts +0 -311
- package/src/core/random/index.ts +0 -53
- package/src/core/tuning/index.ts +0 -156
- package/src/core/types.ts +0 -29
- package/src/generate/bass/index.ts +0 -350
- package/src/generate/countermelody/index.ts +0 -379
- package/src/generate/drums/beat.ts +0 -266
- package/src/generate/drums/euclid.ts +0 -58
- package/src/generate/drums/fills.ts +0 -356
- package/src/generate/drums/ghost.ts +0 -119
- package/src/generate/drums/hihat.ts +0 -285
- package/src/generate/drums/hit.ts +0 -31
- package/src/generate/drums/index.ts +0 -329
- package/src/generate/drums/internal.ts +0 -257
- package/src/generate/drums/kick.ts +0 -160
- package/src/generate/drums/percussion.ts +0 -121
- package/src/generate/drums/rng.ts +0 -7
- package/src/generate/drums/swing.ts +0 -71
- package/src/generate/groove/index.ts +0 -326
- package/src/generate/harmonize/index.ts +0 -440
- package/src/generate/index.ts +0 -59
- package/src/generate/motif/index.ts +0 -398
- package/src/generate/progression/index.ts +0 -370
- package/src/generate/reharmony/index.ts +0 -330
- package/src/generate/rhythm/index.ts +0 -165
- package/src/index.ts +0 -247
- package/src/model/chord.ts +0 -423
- package/src/model/index.ts +0 -15
- package/src/model/interval.ts +0 -82
- package/src/model/key.ts +0 -266
- package/src/model/note.ts +0 -176
- package/src/model/progression.ts +0 -161
- package/src/model/shared.ts +0 -12
- package/src/theory/chord/index.ts +0 -373
- package/src/theory/chordscale/index.ts +0 -374
- package/src/theory/counterpoint/index.ts +0 -230
- package/src/theory/harmony/index.ts +0 -92
- package/src/theory/index.ts +0 -107
- package/src/theory/safety/index.ts +0 -419
- package/src/theory/scale/degrees.ts +0 -123
- package/src/theory/scale/index.ts +0 -28
- package/src/theory/scale/key.ts +0 -56
- package/src/theory/scale/masks.ts +0 -153
- package/src/theory/spelling/index.ts +0 -182
- package/src/theory/symbol/index.ts +0 -268
- package/src/theory/voicing/index.ts +0 -5
- package/src/theory/voicing/internal.ts +0 -168
- package/src/theory/voicing/leading.ts +0 -112
- package/src/theory/voicing/satb.ts +0 -195
- package/src/theory/voicing/styled.ts +0 -176
- 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
|
-
}
|