@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.
- package/README.md +2 -2
- package/dist/analyze/index.cjs +39 -1947
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +197 -37
- package/dist/analyze/index.d.ts +197 -37
- 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-4MHA432G.js → chunk-7NKDPF74.js} +403 -180
- 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-MIWO425M.js → chunk-D2FAJBYL.js} +101 -34
- package/dist/chunk-D2FAJBYL.js.map +1 -0
- package/dist/{chunk-SGU7SPLK.js → chunk-EHVEBWM5.js} +1056 -531
- 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-IT2O4XFE.js → chunk-GQSGS2N4.js} +43 -24
- package/dist/chunk-GQSGS2N4.js.map +1 -0
- package/dist/{chunk-62QZKCJQ.js → chunk-HKGYCQ34.js} +10 -6
- package/dist/chunk-HKGYCQ34.js.map +1 -0
- package/dist/{chunk-AKLYPWIN.js → chunk-I24MV65U.js} +29 -6
- package/dist/chunk-I24MV65U.js.map +1 -0
- package/dist/{chunk-CHMXELDS.js → chunk-KMHOICIY.js} +311 -181
- 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-2ZWMSJH6.js → chunk-NKIBQ6PT.js} +2 -2
- 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-EH6EM37U.js → chunk-NYYWMV2C.js} +61 -49
- package/dist/chunk-NYYWMV2C.js.map +1 -0
- package/dist/{chunk-GOAW4VYL.js → chunk-Q2XUP227.js} +243 -117
- 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-C5XYKUVG.js → chunk-RK35RIN7.js} +370 -157
- 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-I5R7ZBJS.js → chunk-YLEMGEFH.js} +103 -59
- 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 -647
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +301 -39
- package/dist/core/index.d.ts +301 -39
- package/dist/core/index.js +54 -16
- package/dist/{function-DYt4iKsn.d.cts → function-BtiZb9i9.d.ts} +28 -7
- package/dist/{function-DVH4Eusr.d.ts → function-DNugAOrt.d.cts} +28 -7
- package/dist/generate/index.cjs +28 -4085
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +370 -62
- package/dist/generate/index.d.ts +370 -62
- package/dist/generate/index.js +23 -9
- package/dist/{index-CxqBQtH1.d.cts → index-6VR8-kit.d.ts} +19 -4
- package/dist/index-70p168VM.d.ts +192 -0
- package/dist/{index-CpOsP1y9.d.cts → index-Be74mcon.d.cts} +18 -17
- package/dist/index-BtXT6UQ0.d.cts +192 -0
- package/dist/{index-DxvARIP2.d.ts → index-C5UBbQ4G.d.ts} +18 -17
- package/dist/{index-Bt4tOUD9.d.ts → index-C83mZI1P.d.ts} +93 -8
- package/dist/{index-Bg5dYMmZ.d.ts → 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-Jc0Ryi79.d.ts → index-tJhkmQgE.d.cts} +19 -4
- package/dist/{index-Dikst44v.d.cts → index-tSQh-xwY.d.ts} +14 -11
- package/dist/{index-BNlGP86A.d.cts → index-y_ak9_Hi.d.cts} +93 -8
- package/dist/index.cjs +419 -7383
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -14
- package/dist/index.d.ts +14 -14
- package/dist/index.js +111 -27
- package/dist/model/index.cjs +8 -2562
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +433 -75
- package/dist/model/index.d.ts +433 -75
- 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 -1822
- 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 +30 -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 +24 -6
- package/dist/chunk-2ZWMSJH6.js.map +0 -1
- package/dist/chunk-4MHA432G.js.map +0 -1
- package/dist/chunk-62QZKCJQ.js.map +0 -1
- package/dist/chunk-AKLYPWIN.js.map +0 -1
- package/dist/chunk-C5XYKUVG.js.map +0 -1
- package/dist/chunk-CHMXELDS.js.map +0 -1
- package/dist/chunk-DYWFNC4A.js +0 -152
- package/dist/chunk-DYWFNC4A.js.map +0 -1
- package/dist/chunk-EH6EM37U.js.map +0 -1
- package/dist/chunk-FBQK3ZQN.js +0 -131
- package/dist/chunk-FBQK3ZQN.js.map +0 -1
- package/dist/chunk-GOAW4VYL.js.map +0 -1
- package/dist/chunk-I5R7ZBJS.js.map +0 -1
- package/dist/chunk-IT2O4XFE.js.map +0 -1
- package/dist/chunk-MIWO425M.js.map +0 -1
- package/dist/chunk-SGU7SPLK.js.map +0 -1
- package/dist/index-CGMZNCx6.d.cts +0 -140
- package/dist/index-CGMZNCx6.d.ts +0 -140
- package/dist/index-CnZCo29u.d.cts +0 -108
- package/dist/index-JnF4v9bx.d.ts +0 -108
- package/dist/styled-DZ7GjShb.d.ts +0 -282
- package/dist/styled-ZTbdaVFM.d.cts +0 -282
- package/src/analyze/arrange/index.ts +0 -21
- package/src/analyze/arrange/internal.ts +0 -127
- package/src/analyze/arrange/tension.ts +0 -199
- package/src/analyze/arrange/tracks.ts +0 -312
- package/src/analyze/detect/index.ts +0 -272
- 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 -402
- package/src/analyze/index.ts +0 -49
- package/src/analyze/timeline/index.ts +0 -451
- package/src/analyze/voice/index.ts +0 -339
- package/src/core/event-index/index.ts +0 -104
- package/src/core/index.ts +0 -61
- package/src/core/interval/index.ts +0 -81
- package/src/core/meter/index.ts +0 -302
- package/src/core/pitch/index.ts +0 -311
- package/src/core/random/index.ts +0 -76
- package/src/core/tuning/index.ts +0 -186
- package/src/core/types.ts +0 -29
- package/src/core/validation/index.ts +0 -119
- package/src/generate/bass/index.ts +0 -373
- package/src/generate/countermelody/index.ts +0 -470
- package/src/generate/drums/beat.ts +0 -266
- package/src/generate/drums/euclid.ts +0 -65
- 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 -361
- 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 -351
- package/src/generate/harmonize/index.ts +0 -451
- package/src/generate/index.ts +0 -58
- package/src/generate/motif/index.ts +0 -422
- package/src/generate/progression/index.ts +0 -362
- package/src/generate/reharmony/index.ts +0 -323
- package/src/generate/rhythm/index.ts +0 -175
- package/src/index.ts +0 -13
- package/src/model/chord.ts +0 -442
- 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 -375
- package/src/theory/chordscale/index.ts +0 -374
- package/src/theory/counterpoint/index.ts +0 -230
- package/src/theory/harmony/index.ts +0 -77
- package/src/theory/index.ts +0 -112
- package/src/theory/safety/index.ts +0 -488
- 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 -249
- 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 -216
- package/src/theory/voicing/styled.ts +0 -199
- package/tsconfig.json +0 -23
package/src/core/tuning/index.ts
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tuning and microtonality: frequency conversion, cents, arbitrary equal
|
|
3
|
-
* temperaments (EDO), and just-intonation ratios.
|
|
4
|
-
*
|
|
5
|
-
* The rest of the library reasons in twelve-tone pitch classes, which is a
|
|
6
|
-
* deliberate scope choice for tonal theory. This module is the escape hatch for
|
|
7
|
-
* anyone who needs actual frequencies, cents, non-12 equal temperaments, or the
|
|
8
|
-
* acoustic (just) tuning behind the tempered intervals.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
assertFiniteNumber,
|
|
13
|
-
assertInteger,
|
|
14
|
-
assertPositiveInt,
|
|
15
|
-
assertRange,
|
|
16
|
-
} from '../validation/index.js';
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* An equal-tempered tuning: a reference pitch and a number of equal divisions of
|
|
20
|
-
* the octave. With `divisions === 12` a step index is an ordinary MIDI number.
|
|
21
|
-
*
|
|
22
|
-
* @category Pitch & Intervals
|
|
23
|
-
*/
|
|
24
|
-
export type Tuning = {
|
|
25
|
-
/** Step index (MIDI number when `divisions` is 12) whose frequency is `refFreq`. */
|
|
26
|
-
refStep: number;
|
|
27
|
-
/** Frequency in Hz of `refStep`. */
|
|
28
|
-
refFreq: number;
|
|
29
|
-
/** Equal divisions of the octave (12 standard; e.g. 19 or 31 for microtonal). */
|
|
30
|
-
divisions: number;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Standard twelve-tone equal temperament, A4 (MIDI 69) = 440 Hz.
|
|
35
|
-
*
|
|
36
|
-
* @category Pitch & Intervals
|
|
37
|
-
*/
|
|
38
|
-
export const TWELVE_TET: Tuning = { refStep: 69, refFreq: 440, divisions: 12 };
|
|
39
|
-
|
|
40
|
-
function assertTuning(tuning: Tuning): Tuning {
|
|
41
|
-
assertFiniteNumber(tuning.refStep, 'tuning.refStep');
|
|
42
|
-
assertRange(tuning.refFreq, Number.MIN_VALUE, Number.MAX_VALUE, 'tuning.refFreq');
|
|
43
|
-
assertPositiveInt(tuning.divisions, 'tuning.divisions');
|
|
44
|
-
return tuning;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Build an equal temperament with `n` divisions of the octave.
|
|
49
|
-
*
|
|
50
|
-
* @param n Divisions of the octave (e.g. 19, 24, 31).
|
|
51
|
-
* @param refFreq Reference frequency in Hz (default 440).
|
|
52
|
-
* @param refStep Step index of the reference (default 69).
|
|
53
|
-
* @returns The tuning.
|
|
54
|
-
* @example
|
|
55
|
-
* ```ts
|
|
56
|
-
* import { edo, frequencyOf } from '@libraz/libcantus';
|
|
57
|
-
* const et19 = edo(19); // { refStep: 69, refFreq: 440, divisions: 19 }
|
|
58
|
-
* frequencyOf(70, et19); // one 19-EDO step above A4
|
|
59
|
-
* ```
|
|
60
|
-
* @category Pitch & Intervals
|
|
61
|
-
*/
|
|
62
|
-
export function edo(n: number, refFreq = 440, refStep = 69): Tuning {
|
|
63
|
-
assertPositiveInt(n, 'EDO divisions');
|
|
64
|
-
assertRange(refFreq, Number.MIN_VALUE, Number.MAX_VALUE, 'reference frequency');
|
|
65
|
-
assertFiniteNumber(refStep, 'reference step');
|
|
66
|
-
return { refStep, refFreq, divisions: n };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Frequency in Hz of a step index under a tuning.
|
|
71
|
-
*
|
|
72
|
-
* @param step Step index (a MIDI number under 12-EDO).
|
|
73
|
-
* @param tuning The tuning (default 12-TET).
|
|
74
|
-
* @returns The frequency in Hz.
|
|
75
|
-
* @example
|
|
76
|
-
* ```ts
|
|
77
|
-
* import { frequencyOf } from '@libraz/libcantus';
|
|
78
|
-
* frequencyOf(69); // 440 (A4 in 12-TET)
|
|
79
|
-
* frequencyOf(60); // middle C in Hz
|
|
80
|
-
* ```
|
|
81
|
-
* @category Pitch & Intervals
|
|
82
|
-
*/
|
|
83
|
-
export function frequencyOf(step: number, tuning: Tuning = TWELVE_TET): number {
|
|
84
|
-
assertFiniteNumber(step, 'step');
|
|
85
|
-
assertTuning(tuning);
|
|
86
|
-
const result = tuning.refFreq * 2 ** ((step - tuning.refStep) / tuning.divisions);
|
|
87
|
-
return assertRange(result, Number.MIN_VALUE, Number.MAX_VALUE, 'frequency result');
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Nearest step index to a frequency under a tuning (the inverse of
|
|
92
|
-
* {@link frequencyOf}, rounded).
|
|
93
|
-
*
|
|
94
|
-
* @param freq Frequency in Hz.
|
|
95
|
-
* @param tuning The tuning (default 12-TET).
|
|
96
|
-
* @returns The nearest step index.
|
|
97
|
-
* @category Pitch & Intervals
|
|
98
|
-
*/
|
|
99
|
-
export function nearestStep(freq: number, tuning: Tuning = TWELVE_TET): number {
|
|
100
|
-
assertRange(freq, Number.MIN_VALUE, Number.MAX_VALUE, 'frequency');
|
|
101
|
-
assertTuning(tuning);
|
|
102
|
-
return Math.round(tuning.refStep + tuning.divisions * Math.log2(freq / tuning.refFreq));
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Interval in cents between two frequencies.
|
|
107
|
-
*
|
|
108
|
-
* @param a Lower/first frequency in Hz.
|
|
109
|
-
* @param b Upper/second frequency in Hz.
|
|
110
|
-
* @returns Cents from `a` to `b` (negative if `b` is lower).
|
|
111
|
-
* @category Pitch & Intervals
|
|
112
|
-
*/
|
|
113
|
-
export function centsBetweenFreq(a: number, b: number): number {
|
|
114
|
-
assertRange(a, Number.MIN_VALUE, Number.MAX_VALUE, 'first frequency');
|
|
115
|
-
assertRange(b, Number.MIN_VALUE, Number.MAX_VALUE, 'second frequency');
|
|
116
|
-
return 1200 * (Math.log2(b) - Math.log2(a));
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Cents spanned by a number of equal-temperament steps.
|
|
121
|
-
*
|
|
122
|
-
* @param steps Number of steps.
|
|
123
|
-
* @param tuning The tuning (default 12-TET).
|
|
124
|
-
* @returns The cents.
|
|
125
|
-
* @category Pitch & Intervals
|
|
126
|
-
*/
|
|
127
|
-
export function centsOfSteps(steps: number, tuning: Tuning = TWELVE_TET): number {
|
|
128
|
-
assertFiniteNumber(steps, 'steps');
|
|
129
|
-
assertTuning(tuning);
|
|
130
|
-
return assertFiniteNumber((steps * 1200) / tuning.divisions, 'cents result');
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Cents of a frequency ratio, e.g. `ratioToCents(3, 2)` ≈ 701.955 for the just
|
|
135
|
-
* perfect fifth.
|
|
136
|
-
*
|
|
137
|
-
* @param numerator Ratio numerator.
|
|
138
|
-
* @param denominator Ratio denominator.
|
|
139
|
-
* @returns The interval in cents.
|
|
140
|
-
* @category Pitch & Intervals
|
|
141
|
-
*/
|
|
142
|
-
export function ratioToCents(numerator: number, denominator: number): number {
|
|
143
|
-
assertRange(numerator, Number.MIN_VALUE, Number.MAX_VALUE, 'ratio numerator');
|
|
144
|
-
assertRange(denominator, Number.MIN_VALUE, Number.MAX_VALUE, 'ratio denominator');
|
|
145
|
-
return 1200 * (Math.log2(numerator) - Math.log2(denominator));
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Five-limit just-intonation ratios for the twelve interval classes above a
|
|
150
|
-
* unison, indexed by semitone class (0..12).
|
|
151
|
-
*
|
|
152
|
-
* @category Pitch & Intervals
|
|
153
|
-
*/
|
|
154
|
-
export const JUST_RATIOS: Record<number, [number, number]> = {
|
|
155
|
-
0: [1, 1],
|
|
156
|
-
1: [16, 15],
|
|
157
|
-
2: [9, 8],
|
|
158
|
-
3: [6, 5],
|
|
159
|
-
4: [5, 4],
|
|
160
|
-
5: [4, 3],
|
|
161
|
-
6: [45, 32],
|
|
162
|
-
7: [3, 2],
|
|
163
|
-
8: [8, 5],
|
|
164
|
-
9: [5, 3],
|
|
165
|
-
10: [9, 5],
|
|
166
|
-
11: [15, 8],
|
|
167
|
-
12: [2, 1],
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Cents by which a five-limit just interval departs from its 12-TET tempering
|
|
172
|
-
* (positive means the just interval is wider).
|
|
173
|
-
*
|
|
174
|
-
* @param semitoneClass Semitone class in [0, 12].
|
|
175
|
-
* @returns The deviation in cents.
|
|
176
|
-
* @throws If `semitoneClass` is not an integer in [0, 12].
|
|
177
|
-
* @category Pitch & Intervals
|
|
178
|
-
*/
|
|
179
|
-
export function justDeviationCents(semitoneClass: number): number {
|
|
180
|
-
assertInteger(semitoneClass, 'semitone class', 0, 12);
|
|
181
|
-
const ratio = JUST_RATIOS[semitoneClass];
|
|
182
|
-
if (!ratio) {
|
|
183
|
-
throw new RangeError(`semitone class has no just ratio: ${semitoneClass}`);
|
|
184
|
-
}
|
|
185
|
-
return ratioToCents(ratio[0], ratio[1]) - semitoneClass * 100;
|
|
186
|
-
}
|
package/src/core/types.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A key/scale definition anchored on a root pitch class.
|
|
3
|
-
*
|
|
4
|
-
* `modeMask12` is a 12-bit mask where bit `n` set means the pitch class
|
|
5
|
-
* `(rootPc + n) % 12` belongs to the scale. Bit 0 (the root) is always set;
|
|
6
|
-
* {@link maskFromOffsets} enforces this so the root is always a scale tone.
|
|
7
|
-
*/
|
|
8
|
-
export type KeyScale = {
|
|
9
|
-
rootPc: number;
|
|
10
|
-
modeMask12: number;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* A single sounding note on a beat-indexed timeline: an absolute MIDI pitch, an
|
|
15
|
-
* onset and duration measured in quarter-note beats, and an optional MIDI
|
|
16
|
-
* velocity. This is the library-wide interchange shape for note events read
|
|
17
|
-
* from or written to a DAW/MIDI track; the harmonizer's `MelodyNote` and the
|
|
18
|
-
* analysis `VoiceNote` are specializations of it.
|
|
19
|
-
*/
|
|
20
|
-
export type NoteEvent = {
|
|
21
|
-
/** MIDI pitch (middle C = 60). */
|
|
22
|
-
pitch: number;
|
|
23
|
-
/** Onset in quarter-note beats, absolute from the start of the timeline. */
|
|
24
|
-
startBeat: number;
|
|
25
|
-
/** Duration in quarter-note beats. */
|
|
26
|
-
durationBeat: number;
|
|
27
|
-
/** MIDI velocity in [0, 127], when known. */
|
|
28
|
-
velocity?: number;
|
|
29
|
-
};
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import type { TimeSignature } from '../meter/index.js';
|
|
2
|
-
import type { NoteEvent } from '../types.js';
|
|
3
|
-
|
|
4
|
-
/** Default upper bound for synchronous event/window/candidate generation. */
|
|
5
|
-
export const DEFAULT_GENERATION_BUDGET = 1_000_000;
|
|
6
|
-
|
|
7
|
-
/** Require a finite JavaScript number and return it unchanged. */
|
|
8
|
-
export function assertFiniteNumber(value: number, name: string): number {
|
|
9
|
-
if (!Number.isFinite(value)) {
|
|
10
|
-
throw new RangeError(`${name} must be finite; received ${value}`);
|
|
11
|
-
}
|
|
12
|
-
return value;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Require an integer in the inclusive range `[min, max]`. */
|
|
16
|
-
export function assertInteger(
|
|
17
|
-
value: number,
|
|
18
|
-
name: string,
|
|
19
|
-
min = Number.MIN_SAFE_INTEGER,
|
|
20
|
-
max = Number.MAX_SAFE_INTEGER,
|
|
21
|
-
): number {
|
|
22
|
-
assertFiniteNumber(value, name);
|
|
23
|
-
if (!Number.isSafeInteger(value) || value < min || value > max) {
|
|
24
|
-
throw new RangeError(`${name} must be an integer in [${min}, ${max}]; received ${value}`);
|
|
25
|
-
}
|
|
26
|
-
return value;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Require a positive safe integer, optionally capped by `max`. */
|
|
30
|
-
export function assertPositiveInt(
|
|
31
|
-
value: number,
|
|
32
|
-
name: string,
|
|
33
|
-
max = DEFAULT_GENERATION_BUDGET,
|
|
34
|
-
): number {
|
|
35
|
-
return assertInteger(value, name, 1, max);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Require a finite number in the inclusive range `[min, max]`. */
|
|
39
|
-
export function assertRange(value: number, min: number, max: number, name: string): number {
|
|
40
|
-
assertFiniteNumber(value, name);
|
|
41
|
-
if (value < min || value > max) {
|
|
42
|
-
throw new RangeError(`${name} must be in [${min}, ${max}]; received ${value}`);
|
|
43
|
-
}
|
|
44
|
-
return value;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Reject work estimates that would exceed a synchronous generation budget. */
|
|
48
|
-
export function assertGenerationBudget(
|
|
49
|
-
estimated: number,
|
|
50
|
-
name: string,
|
|
51
|
-
limit = DEFAULT_GENERATION_BUDGET,
|
|
52
|
-
): number {
|
|
53
|
-
assertFiniteNumber(estimated, name);
|
|
54
|
-
assertPositiveInt(limit, `${name} limit`, Number.MAX_SAFE_INTEGER);
|
|
55
|
-
if (estimated < 0 || estimated > limit) {
|
|
56
|
-
throw new RangeError(`${name} exceeds the generation budget ${limit}; received ${estimated}`);
|
|
57
|
-
}
|
|
58
|
-
return estimated;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Validate a time signature, including additive grouping, before any early return. */
|
|
62
|
-
export function assertTimeSignature(ts: TimeSignature, name = 'time signature'): TimeSignature {
|
|
63
|
-
assertPositiveInt(ts.numerator, `${name}.numerator`);
|
|
64
|
-
assertPositiveInt(ts.denominator, `${name}.denominator`);
|
|
65
|
-
if (ts.grouping !== undefined) {
|
|
66
|
-
if (ts.grouping.length === 0) {
|
|
67
|
-
throw new RangeError(`${name}.grouping must not be empty`);
|
|
68
|
-
}
|
|
69
|
-
let sum = 0;
|
|
70
|
-
for (let index = 0; index < ts.grouping.length; index += 1) {
|
|
71
|
-
sum += assertPositiveInt(ts.grouping[index] ?? Number.NaN, `${name}.grouping[${index}]`);
|
|
72
|
-
}
|
|
73
|
-
const compound = ts.numerator % 3 === 0 && ts.numerator > 3;
|
|
74
|
-
const pulses = compound ? ts.numerator / 3 : ts.numerator;
|
|
75
|
-
if (sum !== pulses) {
|
|
76
|
-
throw new RangeError(`${name}.grouping must sum to ${pulses}; received ${sum}`);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return ts;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Validate the finite fields of one timeline note event. */
|
|
83
|
-
export function assertNoteEvent(
|
|
84
|
-
event: NoteEvent,
|
|
85
|
-
name = 'note event',
|
|
86
|
-
options: { allowNonPositiveDuration?: boolean } = {},
|
|
87
|
-
): NoteEvent {
|
|
88
|
-
assertFiniteNumber(event.pitch, `${name}.pitch`);
|
|
89
|
-
assertRange(event.startBeat, 0, Number.MAX_SAFE_INTEGER, `${name}.startBeat`);
|
|
90
|
-
assertRange(
|
|
91
|
-
event.durationBeat,
|
|
92
|
-
options.allowNonPositiveDuration ? -Number.MAX_SAFE_INTEGER : Number.MIN_VALUE,
|
|
93
|
-
Number.MAX_SAFE_INTEGER,
|
|
94
|
-
`${name}.durationBeat`,
|
|
95
|
-
);
|
|
96
|
-
if (!options.allowNonPositiveDuration && event.durationBeat <= 0) {
|
|
97
|
-
throw new RangeError(`${name}.durationBeat must be positive; received ${event.durationBeat}`);
|
|
98
|
-
}
|
|
99
|
-
if (event.velocity !== undefined) {
|
|
100
|
-
assertRange(event.velocity, 0, 127, `${name}.velocity`);
|
|
101
|
-
}
|
|
102
|
-
return event;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** Validate an event array and its allocation budget without copying it. */
|
|
106
|
-
export function assertNoteEvents(
|
|
107
|
-
events: NoteEvent[],
|
|
108
|
-
name = 'note events',
|
|
109
|
-
options: { allowNonPositiveDuration?: boolean; budget?: number } = {},
|
|
110
|
-
): NoteEvent[] {
|
|
111
|
-
assertGenerationBudget(events.length, `${name} count`, options.budget);
|
|
112
|
-
for (let index = 0; index < events.length; index += 1) {
|
|
113
|
-
const event = events[index];
|
|
114
|
-
if (event !== undefined) {
|
|
115
|
-
assertNoteEvent(event, `${name}[${index}]`, options);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return events;
|
|
119
|
-
}
|
|
@@ -1,373 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Deterministic bass-line generation over a chord placement.
|
|
3
|
-
*
|
|
4
|
-
* Given a sequence of chord segments and a key, this builds a monophonic bass
|
|
5
|
-
* line in a chosen low register. Several idiomatic styles are supported, from a
|
|
6
|
-
* single held root per segment to a quarter-note walking line that leads by
|
|
7
|
-
* step into each chord change. All pitches are kept in a narrow bass band and
|
|
8
|
-
* consecutive notes move by small intervals; given a seed the output is fully
|
|
9
|
-
* reproducible.
|
|
10
|
-
*
|
|
11
|
-
* Positions and durations are measured in quarter-note beats, matching the
|
|
12
|
-
* library-wide convention.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { isStrongBeat, type TimeSignature } from '../../core/meter/index.js';
|
|
16
|
-
import { createRng } from '../../core/random/index.js';
|
|
17
|
-
import type { KeyScale, NoteEvent } from '../../core/types.js';
|
|
18
|
-
import {
|
|
19
|
-
assertGenerationBudget,
|
|
20
|
-
assertInteger,
|
|
21
|
-
assertRange,
|
|
22
|
-
assertTimeSignature,
|
|
23
|
-
} from '../../core/validation/index.js';
|
|
24
|
-
import type { Chord } from '../../theory/chord/index.js';
|
|
25
|
-
import { nearestScaleTone } from '../../theory/scale/index.js';
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* A chord sounding over a half-open beat span `[startBeat, endBeat)`.
|
|
29
|
-
*
|
|
30
|
-
* @category Composition
|
|
31
|
-
*/
|
|
32
|
-
export type BassSegment = {
|
|
33
|
-
startBeat: number;
|
|
34
|
-
endBeat: number;
|
|
35
|
-
chord: Chord;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* The bass-line idiom to generate.
|
|
40
|
-
*
|
|
41
|
-
* @category Composition
|
|
42
|
-
*/
|
|
43
|
-
export type BassStyle = 'root' | 'rootFifth' | 'pop' | 'walking' | 'arpeggio';
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Options controlling {@link generateBassLine}.
|
|
47
|
-
*
|
|
48
|
-
* @category Composition
|
|
49
|
-
*/
|
|
50
|
-
export type BassLineOptions = {
|
|
51
|
-
/** Chord placement to follow; need not be pre-sorted. */
|
|
52
|
-
segments: BassSegment[];
|
|
53
|
-
/** Key/scale context, used for diatonic approach tones in `walking`. */
|
|
54
|
-
key: KeyScale;
|
|
55
|
-
/**
|
|
56
|
-
* Time signature; used for metric accents.
|
|
57
|
-
*
|
|
58
|
-
* @defaultValue `{ numerator: 4, denominator: 4 }`
|
|
59
|
-
*/
|
|
60
|
-
ts?: TimeSignature;
|
|
61
|
-
/**
|
|
62
|
-
* Bass-line idiom.
|
|
63
|
-
*
|
|
64
|
-
* @defaultValue `'root'`
|
|
65
|
-
*/
|
|
66
|
-
style?: BassStyle;
|
|
67
|
-
/**
|
|
68
|
-
* Target register as a base MIDI octave; roots land around `octave*12+12`.
|
|
69
|
-
*
|
|
70
|
-
* @defaultValue 2
|
|
71
|
-
*/
|
|
72
|
-
octave?: number;
|
|
73
|
-
/**
|
|
74
|
-
* Seed for the deterministic PRNG.
|
|
75
|
-
*
|
|
76
|
-
* @defaultValue 0
|
|
77
|
-
*/
|
|
78
|
-
seed?: number;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const DEFAULT_TS: TimeSignature = { numerator: 4, denominator: 4 };
|
|
82
|
-
const DEFAULT_STYLE: BassStyle = 'root';
|
|
83
|
-
const DEFAULT_OCTAVE = 2;
|
|
84
|
-
|
|
85
|
-
/** Velocity for notes on metrically strong positions. */
|
|
86
|
-
const STRONG_VELOCITY = 100;
|
|
87
|
-
/** Velocity for notes on weak positions. */
|
|
88
|
-
const WEAK_VELOCITY = 80;
|
|
89
|
-
/** Probability of a weak-beat pickup in the `pop` style. */
|
|
90
|
-
const PICKUP_PROB = 0.35;
|
|
91
|
-
|
|
92
|
-
const EPS = 1e-9;
|
|
93
|
-
|
|
94
|
-
/** Reduce a value to a pitch class in [0, 11]. */
|
|
95
|
-
function pitchClass(value: number): number {
|
|
96
|
-
return ((Math.trunc(value) % 12) + 12) % 12;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Place a pitch class as a MIDI note near an anchor, clamped to the bass band.
|
|
101
|
-
*
|
|
102
|
-
* The octave nearest the anchor is chosen first (so motion from the previous
|
|
103
|
-
* note is minimal), then the result is shifted by whole octaves into the band
|
|
104
|
-
* `[low, low + 12]` if it falls outside.
|
|
105
|
-
*/
|
|
106
|
-
function placePc(pc: number, anchor: number, low: number): number {
|
|
107
|
-
const high = low + 12;
|
|
108
|
-
let midi = pc + 12 * Math.round((anchor - pc) / 12);
|
|
109
|
-
while (midi < low) {
|
|
110
|
-
midi += 12;
|
|
111
|
-
}
|
|
112
|
-
while (midi > high) {
|
|
113
|
-
midi -= 12;
|
|
114
|
-
}
|
|
115
|
-
return midi;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** Chord-tone pitch classes in stacked-thirds order, deduplicated. */
|
|
119
|
-
function chordTonePcs(chord: Chord): number[] {
|
|
120
|
-
const seen = new Set<number>();
|
|
121
|
-
const pcs: number[] = [];
|
|
122
|
-
for (const interval of chord.intervals) {
|
|
123
|
-
const pc = pitchClass(chord.rootPc + interval);
|
|
124
|
-
if (!seen.has(pc)) {
|
|
125
|
-
seen.add(pc);
|
|
126
|
-
pcs.push(pc);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
if (pcs.length === 0) {
|
|
130
|
-
pcs.push(pitchClass(chord.rootPc));
|
|
131
|
-
}
|
|
132
|
-
return pcs;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* The chord's fifth pitch class.
|
|
137
|
-
*
|
|
138
|
-
* Uses the perfect fifth when present, otherwise the chord's actual altered
|
|
139
|
-
* fifth (diminished = 6, augmented = 8 semitones above the root), so dim/aug/
|
|
140
|
-
* m7b5 chords sound their real fifth rather than a repeated root. Falls back to
|
|
141
|
-
* the root only when the chord has no fifth degree at all.
|
|
142
|
-
*/
|
|
143
|
-
function fifthPcOf(chord: Chord): number {
|
|
144
|
-
const fifths = new Set(chord.intervals.map((i) => pitchClass(i)));
|
|
145
|
-
const chosen = fifths.has(7) ? 7 : fifths.has(6) ? 6 : fifths.has(8) ? 8 : undefined;
|
|
146
|
-
return chosen === undefined ? pitchClass(chord.rootPc) : pitchClass(chord.rootPc + chosen);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** The sounding bass pitch class of a chord: its slash bass, else its root. */
|
|
150
|
-
function bassPcOf(chord: Chord): number {
|
|
151
|
-
return pitchClass(chord.bassPc ?? chord.rootPc);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/** Integer quarter-note beat positions within `[start, end)`, at least one. */
|
|
155
|
-
function beatPositions(start: number, end: number): number[] {
|
|
156
|
-
const positions: number[] = [];
|
|
157
|
-
for (let p = start; p < end - EPS; p += 1) {
|
|
158
|
-
positions.push(p);
|
|
159
|
-
}
|
|
160
|
-
if (positions.length === 0) {
|
|
161
|
-
positions.push(start);
|
|
162
|
-
}
|
|
163
|
-
return positions;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
type RawNote = { startBeat: number; pitch: number; velocity: number };
|
|
167
|
-
|
|
168
|
-
/** Working state threaded through the per-segment builders. */
|
|
169
|
-
type BuildContext = {
|
|
170
|
-
ts: TimeSignature;
|
|
171
|
-
low: number;
|
|
172
|
-
key: KeyScale;
|
|
173
|
-
rng: ReturnType<typeof createRng>;
|
|
174
|
-
notes: RawNote[];
|
|
175
|
-
prevMidi: number;
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
/** Append a note for pitch class `pc` at `pos`, placed near the running anchor. */
|
|
179
|
-
function emit(ctx: BuildContext, pos: number, pc: number, midiOverride?: number): number {
|
|
180
|
-
const midi = midiOverride ?? placePc(pc, ctx.prevMidi, ctx.low);
|
|
181
|
-
const velocity = isStrongBeat(pos, ctx.ts) ? STRONG_VELOCITY : WEAK_VELOCITY;
|
|
182
|
-
ctx.notes.push({ startBeat: pos, pitch: midi, velocity });
|
|
183
|
-
return midi;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/** One root note per segment. */
|
|
187
|
-
function buildRoot(ctx: BuildContext, seg: BassSegment): void {
|
|
188
|
-
ctx.prevMidi = emit(ctx, seg.startBeat, bassPcOf(seg.chord));
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/** Root on the downbeat, fifth on the segment's midpoint. */
|
|
192
|
-
function buildRootFifth(ctx: BuildContext, seg: BassSegment): void {
|
|
193
|
-
ctx.prevMidi = emit(ctx, seg.startBeat, bassPcOf(seg.chord));
|
|
194
|
-
const mid = (seg.startBeat + seg.endBeat) / 2;
|
|
195
|
-
if (mid > seg.startBeat + EPS && mid < seg.endBeat - EPS) {
|
|
196
|
-
ctx.prevMidi = emit(ctx, mid, fifthPcOf(seg.chord));
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/** Root on every strong beat, with occasional octave/fifth pickups on weak beats. */
|
|
201
|
-
function buildPop(ctx: BuildContext, seg: BassSegment): void {
|
|
202
|
-
const positions = beatPositions(seg.startBeat, seg.endBeat);
|
|
203
|
-
const rootPc = bassPcOf(seg.chord);
|
|
204
|
-
let emitted = false;
|
|
205
|
-
for (const pos of positions) {
|
|
206
|
-
if (isStrongBeat(pos, ctx.ts)) {
|
|
207
|
-
ctx.prevMidi = emit(ctx, pos, rootPc);
|
|
208
|
-
emitted = true;
|
|
209
|
-
} else if (ctx.rng.prob(PICKUP_PROB)) {
|
|
210
|
-
if (ctx.rng.prob(0.5)) {
|
|
211
|
-
// Octave pickup: the root dropped toward the bottom of the register,
|
|
212
|
-
// clamped so it never falls below the declared band.
|
|
213
|
-
const base = placePc(rootPc, ctx.prevMidi, ctx.low);
|
|
214
|
-
emit(ctx, pos, rootPc, Math.max(ctx.low, base - 12));
|
|
215
|
-
} else {
|
|
216
|
-
ctx.prevMidi = emit(ctx, pos, fifthPcOf(seg.chord));
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
if (!emitted) {
|
|
221
|
-
ctx.prevMidi = emit(ctx, seg.startBeat, rootPc);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/** Cycle root -> third -> fifth (-> seventh) across the segment's beats. */
|
|
226
|
-
function buildArpeggio(ctx: BuildContext, seg: BassSegment): void {
|
|
227
|
-
const tones = chordTonePcs(seg.chord);
|
|
228
|
-
const positions = beatPositions(seg.startBeat, seg.endBeat);
|
|
229
|
-
positions.forEach((pos, i) => {
|
|
230
|
-
const pc = tones[i % tones.length] ?? bassPcOf(seg.chord);
|
|
231
|
-
ctx.prevMidi = emit(ctx, pos, pc);
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/** A diatonic or chromatic neighbor of `target`, a step toward `from`. */
|
|
236
|
-
function approachNote(ctx: BuildContext, target: number, from: number): number {
|
|
237
|
-
const dir = from <= target ? -1 : 1;
|
|
238
|
-
if (ctx.rng.prob(0.5)) {
|
|
239
|
-
return target + dir; // chromatic semitone
|
|
240
|
-
}
|
|
241
|
-
const cand = nearestScaleTone(target + dir * 2, ctx.key);
|
|
242
|
-
const step = Math.abs(cand - target);
|
|
243
|
-
return step >= 1 && step <= 2 ? cand : target + dir;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/** A quarter-note line of chord tones that leads by step into each chord change. */
|
|
247
|
-
function buildWalking(ctx: BuildContext, seg: BassSegment, next: BassSegment | undefined): void {
|
|
248
|
-
const tones = chordTonePcs(seg.chord);
|
|
249
|
-
const positions = beatPositions(seg.startBeat, seg.endBeat);
|
|
250
|
-
const count = positions.length;
|
|
251
|
-
positions.forEach((pos, i) => {
|
|
252
|
-
if (i === 0) {
|
|
253
|
-
ctx.prevMidi = emit(ctx, pos, bassPcOf(seg.chord));
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
if (next && count > 1 && i === count - 1) {
|
|
257
|
-
const nextBass = placePc(bassPcOf(next.chord), ctx.prevMidi, ctx.low);
|
|
258
|
-
const midi = approachNote(ctx, nextBass, ctx.prevMidi);
|
|
259
|
-
ctx.prevMidi = emit(ctx, pos, pitchClass(midi), midi);
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
const pc = tones[i % tones.length] ?? bassPcOf(seg.chord);
|
|
263
|
-
ctx.prevMidi = emit(ctx, pos, pc);
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Generate a bass line following a chord placement.
|
|
269
|
-
*
|
|
270
|
-
* Each segment contributes notes in the chosen bass register per the selected
|
|
271
|
-
* style; consecutive notes are kept within roughly a fifth. Every note's
|
|
272
|
-
* duration extends to the next onset (the final note extends to the last
|
|
273
|
-
* segment's end). Given a seed the output is fully reproducible.
|
|
274
|
-
*
|
|
275
|
-
* @param opts Segments, key, and generation options.
|
|
276
|
-
* @returns Bass notes sorted by onset, non-overlapping.
|
|
277
|
-
*
|
|
278
|
-
* @example
|
|
279
|
-
* ```ts
|
|
280
|
-
* import { generateBassLine, makeChord, majorKey } from '@libraz/libcantus';
|
|
281
|
-
* const segments = [
|
|
282
|
-
* { startBeat: 0, endBeat: 4, chord: makeChord(0, 'maj') },
|
|
283
|
-
* { startBeat: 4, endBeat: 8, chord: makeChord(7, 'maj') },
|
|
284
|
-
* ];
|
|
285
|
-
* const notes = generateBassLine({ segments, key: majorKey(0), style: 'walking' });
|
|
286
|
-
* ```
|
|
287
|
-
*
|
|
288
|
-
* @category Composition
|
|
289
|
-
*/
|
|
290
|
-
export function generateBassLine(opts: BassLineOptions): NoteEvent[] {
|
|
291
|
-
assertGenerationBudget(opts.segments.length, 'bass segments');
|
|
292
|
-
for (let index = 0; index < opts.segments.length; index += 1) {
|
|
293
|
-
const segment = opts.segments[index];
|
|
294
|
-
if (!segment) continue;
|
|
295
|
-
assertRange(segment.startBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].startBeat`);
|
|
296
|
-
assertRange(segment.endBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].endBeat`);
|
|
297
|
-
if (segment.endBeat <= segment.startBeat) {
|
|
298
|
-
throw new RangeError(`segments[${index}] must have a positive duration`);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
const segments = [...opts.segments].sort((a, b) => a.startBeat - b.startBeat);
|
|
302
|
-
if (segments.length === 0) {
|
|
303
|
-
return [];
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const ts = opts.ts ?? DEFAULT_TS;
|
|
307
|
-
assertTimeSignature(ts);
|
|
308
|
-
const style = opts.style ?? DEFAULT_STYLE;
|
|
309
|
-
const octave = opts.octave ?? DEFAULT_OCTAVE;
|
|
310
|
-
assertInteger(octave, 'bass octave', -1, 9);
|
|
311
|
-
const estimatedNotes = segments.reduce(
|
|
312
|
-
(count, segment) => count + Math.max(1, Math.ceil(segment.endBeat - segment.startBeat)),
|
|
313
|
-
0,
|
|
314
|
-
);
|
|
315
|
-
assertGenerationBudget(estimatedNotes, 'bass notes');
|
|
316
|
-
const low = octave * 12 + 12;
|
|
317
|
-
|
|
318
|
-
const ctx: BuildContext = {
|
|
319
|
-
ts,
|
|
320
|
-
low,
|
|
321
|
-
key: opts.key,
|
|
322
|
-
rng: createRng(opts.seed ?? 0),
|
|
323
|
-
notes: [],
|
|
324
|
-
prevMidi: low,
|
|
325
|
-
};
|
|
326
|
-
|
|
327
|
-
for (let s = 0; s < segments.length; s += 1) {
|
|
328
|
-
const seg = segments[s];
|
|
329
|
-
if (!seg) {
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
332
|
-
switch (style) {
|
|
333
|
-
case 'rootFifth':
|
|
334
|
-
buildRootFifth(ctx, seg);
|
|
335
|
-
break;
|
|
336
|
-
case 'pop':
|
|
337
|
-
buildPop(ctx, seg);
|
|
338
|
-
break;
|
|
339
|
-
case 'walking':
|
|
340
|
-
buildWalking(ctx, seg, segments[s + 1]);
|
|
341
|
-
break;
|
|
342
|
-
case 'arpeggio':
|
|
343
|
-
buildArpeggio(ctx, seg);
|
|
344
|
-
break;
|
|
345
|
-
default:
|
|
346
|
-
buildRoot(ctx, seg);
|
|
347
|
-
break;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
ctx.notes.sort((a, b) => a.startBeat - b.startBeat);
|
|
352
|
-
const lastEnd = segments.reduce((m, seg) => Math.max(m, seg.endBeat), Number.NEGATIVE_INFINITY);
|
|
353
|
-
|
|
354
|
-
const out: NoteEvent[] = [];
|
|
355
|
-
for (let i = 0; i < ctx.notes.length; i += 1) {
|
|
356
|
-
const note = ctx.notes[i];
|
|
357
|
-
if (!note) {
|
|
358
|
-
continue;
|
|
359
|
-
}
|
|
360
|
-
const nextStart = ctx.notes[i + 1]?.startBeat ?? lastEnd;
|
|
361
|
-
const durationBeat = nextStart - note.startBeat;
|
|
362
|
-
if (durationBeat <= EPS) {
|
|
363
|
-
continue;
|
|
364
|
-
}
|
|
365
|
-
out.push({
|
|
366
|
-
pitch: note.pitch,
|
|
367
|
-
startBeat: note.startBeat,
|
|
368
|
-
durationBeat,
|
|
369
|
-
velocity: note.velocity,
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
|
-
return out;
|
|
373
|
-
}
|