@libraz/libcantus 0.9.2 → 0.9.3
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 +4 -3
- package/dist/analyze/index.cjs +315 -42
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +11 -99
- package/dist/analyze/index.d.ts +11 -99
- package/dist/analyze/index.js +6 -6
- package/dist/chunk-2ZWMSJH6.js +30 -0
- package/dist/chunk-2ZWMSJH6.js.map +1 -0
- package/dist/{chunk-R3S2LUBH.js → chunk-4MHA432G.js} +316 -184
- package/dist/chunk-4MHA432G.js.map +1 -0
- package/dist/chunk-62QZKCJQ.js +49 -0
- package/dist/chunk-62QZKCJQ.js.map +1 -0
- package/dist/chunk-AKLYPWIN.js +83 -0
- package/dist/chunk-AKLYPWIN.js.map +1 -0
- package/dist/{chunk-XTC3X6MS.js → chunk-C5XYKUVG.js} +100 -12
- package/dist/chunk-C5XYKUVG.js.map +1 -0
- package/dist/{chunk-LN7ZZMD2.js → chunk-CHMXELDS.js} +33 -14
- package/dist/chunk-CHMXELDS.js.map +1 -0
- package/dist/{chunk-A4532ZFR.js → chunk-EH6EM37U.js} +60 -4
- package/dist/chunk-EH6EM37U.js.map +1 -0
- package/dist/chunk-FBQK3ZQN.js +131 -0
- package/dist/chunk-FBQK3ZQN.js.map +1 -0
- package/dist/{chunk-5HTNMQW4.js → chunk-GOAW4VYL.js} +87 -21
- package/dist/chunk-GOAW4VYL.js.map +1 -0
- package/dist/{chunk-MSWFZF7J.js → chunk-I5R7ZBJS.js} +90 -6
- package/dist/chunk-I5R7ZBJS.js.map +1 -0
- package/dist/{chunk-GQKN5KDT.js → chunk-IT2O4XFE.js} +4 -9
- package/dist/chunk-IT2O4XFE.js.map +1 -0
- package/dist/{chunk-7Y7WF64D.js → chunk-MIWO425M.js} +6 -4
- package/dist/chunk-MIWO425M.js.map +1 -0
- package/dist/chunk-N6IHQUOX.js +1 -0
- package/dist/chunk-N6IHQUOX.js.map +1 -0
- package/dist/{chunk-COKTX2G5.js → chunk-SGU7SPLK.js} +29 -12
- package/dist/chunk-SGU7SPLK.js.map +1 -0
- package/dist/core/index.cjs +230 -14
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +56 -4
- package/dist/core/index.d.ts +56 -4
- package/dist/core/index.js +24 -4
- package/dist/{function-2bJW1Zp_.d.ts → function-DVH4Eusr.d.ts} +1 -1
- package/dist/{function-r2sh-jOO.d.cts → function-DYt4iKsn.d.cts} +1 -1
- package/dist/generate/index.cjs +830 -494
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +11 -5
- package/dist/generate/index.d.ts +11 -5
- package/dist/generate/index.js +9 -9
- package/dist/{index-BOBXBNwP.d.cts → index-BNlGP86A.d.cts} +1 -1
- package/dist/{index-CP3wAfEH.d.ts → index-Bg5dYMmZ.d.ts} +1 -1
- package/dist/{index-CM5W4rcm.d.ts → index-Bt4tOUD9.d.ts} +1 -1
- package/dist/index-CnZCo29u.d.cts +108 -0
- package/dist/{index-D_ZTUNKp.d.cts → index-CpOsP1y9.d.cts} +2 -2
- package/dist/{index-DZ1M21do.d.cts → index-CxqBQtH1.d.cts} +2 -1
- package/dist/{index-BdeQ1r_p.d.cts → index-Dikst44v.d.cts} +1 -1
- package/dist/{index-BMPxwjwV.d.ts → index-DxvARIP2.d.ts} +2 -2
- package/dist/{index-BreoJRw0.d.ts → index-Jc0Ryi79.d.ts} +2 -1
- package/dist/index-JnF4v9bx.d.ts +108 -0
- package/dist/index.cjs +818 -283
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -8
- package/dist/index.d.ts +9 -8
- package/dist/index.js +34 -13
- package/dist/model/index.cjs +170 -31
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +6 -5
- package/dist/model/index.d.ts +6 -5
- package/dist/model/index.js +6 -6
- package/dist/{styled-D7BAx3S3.d.ts → styled-DZ7GjShb.d.ts} +1 -1
- package/dist/{styled-BjXtOFNk.d.cts → styled-ZTbdaVFM.d.cts} +1 -1
- package/dist/theory/index.cjs +180 -34
- package/dist/theory/index.cjs.map +1 -1
- package/dist/theory/index.d.cts +6 -6
- package/dist/theory/index.d.ts +6 -6
- package/dist/theory/index.js +6 -5
- package/package.json +50 -19
- package/src/analyze/arrange/internal.ts +3 -1
- package/src/analyze/arrange/tension.ts +15 -3
- package/src/analyze/arrange/tracks.ts +36 -3
- package/src/analyze/detect/index.ts +46 -9
- package/src/analyze/functional/roman.ts +14 -5
- package/src/analyze/index.ts +1 -1
- package/src/analyze/timeline/index.ts +43 -7
- package/src/analyze/voice/index.ts +11 -2
- package/src/core/event-index/index.ts +104 -0
- package/src/core/index.ts +14 -0
- package/src/core/meter/index.ts +24 -4
- package/src/core/random/index.ts +26 -3
- package/src/core/tuning/index.ts +36 -6
- package/src/core/validation/index.ts +119 -0
- package/src/generate/bass/index.ts +23 -0
- package/src/generate/countermelody/index.ts +167 -76
- package/src/generate/drums/euclid.ts +10 -3
- package/src/generate/drums/index.ts +56 -24
- package/src/generate/drums/percussion.ts +1 -1
- package/src/generate/groove/index.ts +25 -0
- package/src/generate/harmonize/index.ts +55 -44
- package/src/generate/index.ts +0 -1
- package/src/generate/motif/index.ts +45 -21
- package/src/generate/progression/index.ts +8 -16
- package/src/generate/reharmony/index.ts +1 -8
- package/src/generate/rhythm/index.ts +11 -1
- package/src/index.ts +13 -247
- package/src/model/chord.ts +29 -10
- package/src/theory/chord/index.ts +4 -2
- package/src/theory/harmony/index.ts +6 -21
- package/src/theory/index.ts +6 -1
- package/src/theory/safety/index.ts +71 -2
- package/src/theory/spelling/index.ts +70 -3
- package/src/theory/voicing/satb.ts +25 -4
- package/src/theory/voicing/styled.ts +26 -3
- 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-GQKN5KDT.js.map +0 -1
- package/dist/chunk-LN7ZZMD2.js.map +0 -1
- 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
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { NoteEvent } from '../types.js';
|
|
2
|
+
import { assertNoteEvents } from '../validation/index.js';
|
|
3
|
+
|
|
4
|
+
/** A validated note retaining its position in the caller's original array. */
|
|
5
|
+
export type IndexedNoteEvent = {
|
|
6
|
+
note: NoteEvent;
|
|
7
|
+
originalIndex: number;
|
|
8
|
+
endBeat: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** Binary-searchable, stable-sorted index over note onsets and active spans. */
|
|
12
|
+
export type NoteEventIndex = {
|
|
13
|
+
notes: IndexedNoteEvent[];
|
|
14
|
+
/** Latest-onset note sounding at `beat`, with later input order winning ties. */
|
|
15
|
+
at: (beat: number) => IndexedNoteEvent | undefined;
|
|
16
|
+
/** Whether one or more notes attack at `beat`. */
|
|
17
|
+
attacksAt: (beat: number) => boolean;
|
|
18
|
+
/** Unique attack beats strictly inside `(startBeat, endBeat)`. */
|
|
19
|
+
onsetsBetween: (startBeat: number, endBeat: number) => number[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const EPS = 1e-9;
|
|
23
|
+
|
|
24
|
+
function upperBound(values: IndexedNoteEvent[], beat: number): number {
|
|
25
|
+
let low = 0;
|
|
26
|
+
let high = values.length;
|
|
27
|
+
while (low < high) {
|
|
28
|
+
const middle = Math.floor((low + high) / 2);
|
|
29
|
+
if ((values[middle]?.note.startBeat ?? Number.POSITIVE_INFINITY) <= beat) {
|
|
30
|
+
low = middle + 1;
|
|
31
|
+
} else {
|
|
32
|
+
high = middle;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return low;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Validate and stable-sort note events once, then expose logarithmic onset and
|
|
40
|
+
* active-note lookups. Non-positive-duration notes may be retained for callers
|
|
41
|
+
* that intentionally filter them later, but never count as sounding.
|
|
42
|
+
*/
|
|
43
|
+
export function createNoteEventIndex(
|
|
44
|
+
events: NoteEvent[],
|
|
45
|
+
options: { allowNonPositiveDuration?: boolean } = {},
|
|
46
|
+
): NoteEventIndex {
|
|
47
|
+
assertNoteEvents(events, 'note events', options);
|
|
48
|
+
const notes = events
|
|
49
|
+
.map((note, originalIndex) => ({
|
|
50
|
+
note,
|
|
51
|
+
originalIndex,
|
|
52
|
+
endBeat: note.startBeat + note.durationBeat,
|
|
53
|
+
}))
|
|
54
|
+
.sort((a, b) => a.note.startBeat - b.note.startBeat || a.originalIndex - b.originalIndex);
|
|
55
|
+
const prefixMaxEnd: number[] = [];
|
|
56
|
+
let maxEnd = Number.NEGATIVE_INFINITY;
|
|
57
|
+
for (const indexed of notes) {
|
|
58
|
+
maxEnd = Math.max(maxEnd, indexed.endBeat);
|
|
59
|
+
prefixMaxEnd.push(maxEnd);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
notes,
|
|
64
|
+
at(beat) {
|
|
65
|
+
let index = upperBound(notes, beat + EPS) - 1;
|
|
66
|
+
while (index >= 0) {
|
|
67
|
+
if ((prefixMaxEnd[index] ?? Number.NEGATIVE_INFINITY) <= beat + EPS) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
const indexed = notes[index];
|
|
71
|
+
if (
|
|
72
|
+
indexed &&
|
|
73
|
+
indexed.note.durationBeat > 0 &&
|
|
74
|
+
indexed.note.startBeat - EPS <= beat &&
|
|
75
|
+
beat < indexed.endBeat - EPS
|
|
76
|
+
) {
|
|
77
|
+
return indexed;
|
|
78
|
+
}
|
|
79
|
+
index -= 1;
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
},
|
|
83
|
+
attacksAt(beat) {
|
|
84
|
+
const firstAfter = upperBound(notes, beat + EPS);
|
|
85
|
+
const candidate = notes[firstAfter - 1];
|
|
86
|
+
return candidate !== undefined && Math.abs(candidate.note.startBeat - beat) < EPS;
|
|
87
|
+
},
|
|
88
|
+
onsetsBetween(startBeat, endBeat) {
|
|
89
|
+
const result: number[] = [];
|
|
90
|
+
let index = upperBound(notes, startBeat + EPS);
|
|
91
|
+
while (index < notes.length) {
|
|
92
|
+
const onset = notes[index]?.note.startBeat;
|
|
93
|
+
if (onset === undefined || onset >= endBeat - EPS) {
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
if (result[result.length - 1] !== onset) {
|
|
97
|
+
result.push(onset);
|
|
98
|
+
}
|
|
99
|
+
index += 1;
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
package/src/core/index.ts
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* Public API of the core layer. Re-exports every core-layer module;
|
|
3
3
|
* also available from the package root.
|
|
4
4
|
*/
|
|
5
|
+
|
|
6
|
+
export type { IndexedNoteEvent, NoteEventIndex } from './event-index/index.js';
|
|
7
|
+
export { createNoteEventIndex } from './event-index/index.js';
|
|
5
8
|
export {
|
|
6
9
|
classifyInterval,
|
|
7
10
|
IntervalQuality,
|
|
@@ -45,3 +48,14 @@ export {
|
|
|
45
48
|
TWELVE_TET,
|
|
46
49
|
} from './tuning/index.js';
|
|
47
50
|
export type { KeyScale, NoteEvent } from './types.js';
|
|
51
|
+
export {
|
|
52
|
+
assertFiniteNumber,
|
|
53
|
+
assertGenerationBudget,
|
|
54
|
+
assertInteger,
|
|
55
|
+
assertNoteEvent,
|
|
56
|
+
assertNoteEvents,
|
|
57
|
+
assertPositiveInt,
|
|
58
|
+
assertRange,
|
|
59
|
+
assertTimeSignature,
|
|
60
|
+
DEFAULT_GENERATION_BUDGET,
|
|
61
|
+
} from './validation/index.js';
|
package/src/core/meter/index.ts
CHANGED
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
* of the library's beat convention (four quarter-note beats per bar in 4/4).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import {
|
|
10
|
+
assertFiniteNumber,
|
|
11
|
+
assertInteger,
|
|
12
|
+
assertPositiveInt,
|
|
13
|
+
assertRange,
|
|
14
|
+
assertTimeSignature,
|
|
15
|
+
} from '../validation/index.js';
|
|
16
|
+
|
|
9
17
|
/**
|
|
10
18
|
* A time signature as a numerator over a note-value denominator.
|
|
11
19
|
*
|
|
@@ -68,7 +76,12 @@ export function parseTimeSignature(text: string): TimeSignature {
|
|
|
68
76
|
}
|
|
69
77
|
const numerator = Number.parseInt(match[1] ?? '', 10);
|
|
70
78
|
const denominator = Number.parseInt(match[2] ?? '', 10);
|
|
71
|
-
if (
|
|
79
|
+
if (
|
|
80
|
+
!Number.isSafeInteger(numerator) ||
|
|
81
|
+
!Number.isSafeInteger(denominator) ||
|
|
82
|
+
numerator <= 0 ||
|
|
83
|
+
denominator <= 0
|
|
84
|
+
) {
|
|
72
85
|
throw new Error(`Invalid time signature: ${text}`);
|
|
73
86
|
}
|
|
74
87
|
return { numerator, denominator };
|
|
@@ -80,6 +93,7 @@ export function parseTimeSignature(text: string): TimeSignature {
|
|
|
80
93
|
* @category Rhythm & Meter
|
|
81
94
|
*/
|
|
82
95
|
export function formatTimeSignature(ts: TimeSignature): string {
|
|
96
|
+
assertTimeSignature(ts);
|
|
83
97
|
return `${ts.numerator}/${ts.denominator}`;
|
|
84
98
|
}
|
|
85
99
|
|
|
@@ -94,6 +108,7 @@ export function formatTimeSignature(ts: TimeSignature): string {
|
|
|
94
108
|
* @category Rhythm & Meter
|
|
95
109
|
*/
|
|
96
110
|
export function isCompound(ts: TimeSignature): boolean {
|
|
111
|
+
assertTimeSignature(ts);
|
|
97
112
|
return ts.numerator % 3 === 0 && ts.numerator > 3;
|
|
98
113
|
}
|
|
99
114
|
|
|
@@ -110,6 +125,7 @@ function unitBeats(ts: TimeSignature): number {
|
|
|
110
125
|
* @category Rhythm & Meter
|
|
111
126
|
*/
|
|
112
127
|
export function beatsPerBar(ts: TimeSignature): number {
|
|
128
|
+
assertTimeSignature(ts);
|
|
113
129
|
return ts.numerator * unitBeats(ts);
|
|
114
130
|
}
|
|
115
131
|
|
|
@@ -181,6 +197,7 @@ function isGroupHead(grouping: number[], pulseIndex: number): boolean {
|
|
|
181
197
|
* @category Rhythm & Meter
|
|
182
198
|
*/
|
|
183
199
|
export function beatToBarPosition(beatInQuarters: number, ts: TimeSignature): BarPosition {
|
|
200
|
+
assertFiniteNumber(beatInQuarters, 'beat');
|
|
184
201
|
const barLen = beatsPerBar(ts);
|
|
185
202
|
const bar = Math.floor(beatInQuarters / barLen);
|
|
186
203
|
return { bar, beat: beatInQuarters - bar * barLen };
|
|
@@ -196,6 +213,8 @@ export function beatToBarPosition(beatInQuarters: number, ts: TimeSignature): Ba
|
|
|
196
213
|
* @category Rhythm & Meter
|
|
197
214
|
*/
|
|
198
215
|
export function barPositionToBeat(pos: BarPosition, ts: TimeSignature): number {
|
|
216
|
+
assertInteger(pos.bar, 'bar position bar');
|
|
217
|
+
assertFiniteNumber(pos.beat, 'bar position beat');
|
|
199
218
|
return pos.bar * beatsPerBar(ts) + pos.beat;
|
|
200
219
|
}
|
|
201
220
|
|
|
@@ -225,6 +244,8 @@ export function barPositionToBeat(pos: BarPosition, ts: TimeSignature): number {
|
|
|
225
244
|
* @category Rhythm & Meter
|
|
226
245
|
*/
|
|
227
246
|
export function metricWeight(beatInQuarters: number, ts: TimeSignature): number {
|
|
247
|
+
assertFiniteNumber(beatInQuarters, 'beat');
|
|
248
|
+
assertTimeSignature(ts);
|
|
228
249
|
const { beat } = beatToBarPosition(beatInQuarters, ts);
|
|
229
250
|
const pulse = pulseBeats(ts);
|
|
230
251
|
if (!isMultiple(beat, pulse)) {
|
|
@@ -275,8 +296,7 @@ export function isStrongBeat(beatInQuarters: number, ts: TimeSignature): boolean
|
|
|
275
296
|
* @category Rhythm & Meter
|
|
276
297
|
*/
|
|
277
298
|
export function tuplet(totalBeats: number, count: number): number[] {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
299
|
+
assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, 'tuplet total beats');
|
|
300
|
+
assertPositiveInt(count, 'tuplet count');
|
|
281
301
|
return new Array<number>(count).fill(totalBeats / count);
|
|
282
302
|
}
|
package/src/core/random/index.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* seed the stream is fully reproducible.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { assertFiniteNumber, assertInteger, assertRange } from '../validation/index.js';
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* A deterministic PRNG with the sampling helpers the generators need.
|
|
10
12
|
*
|
|
@@ -36,6 +38,7 @@ export type Rng = {
|
|
|
36
38
|
* @category Utilities
|
|
37
39
|
*/
|
|
38
40
|
export function createRng(seed: number): Rng {
|
|
41
|
+
assertFiniteNumber(seed, 'seed');
|
|
39
42
|
let state = seed >>> 0;
|
|
40
43
|
const next = () => {
|
|
41
44
|
state = (state + 0x6d2b79f5) >>> 0;
|
|
@@ -46,8 +49,28 @@ export function createRng(seed: number): Rng {
|
|
|
46
49
|
};
|
|
47
50
|
return {
|
|
48
51
|
next,
|
|
49
|
-
prob: (p) => next() < p,
|
|
50
|
-
range: (lo, hi) =>
|
|
51
|
-
|
|
52
|
+
prob: (p) => next() < assertRange(p, 0, 1, 'probability'),
|
|
53
|
+
range: (lo, hi) => {
|
|
54
|
+
assertInteger(lo, 'range lower bound');
|
|
55
|
+
assertInteger(hi, 'range upper bound');
|
|
56
|
+
if (lo > hi) {
|
|
57
|
+
throw new RangeError(
|
|
58
|
+
`range lower bound must not exceed upper bound; received ${lo} > ${hi}`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
assertInteger(hi - lo, 'range span', 0);
|
|
62
|
+
return lo + Math.floor(next() * (hi - lo + 1));
|
|
63
|
+
},
|
|
64
|
+
float: (lo, hi) => {
|
|
65
|
+
assertFiniteNumber(lo, 'float lower bound');
|
|
66
|
+
assertFiniteNumber(hi, 'float upper bound');
|
|
67
|
+
if (lo > hi) {
|
|
68
|
+
throw new RangeError(
|
|
69
|
+
`float lower bound must not exceed upper bound; received ${lo} > ${hi}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
assertFiniteNumber(hi - lo, 'float span');
|
|
73
|
+
return lo + next() * (hi - lo);
|
|
74
|
+
},
|
|
52
75
|
};
|
|
53
76
|
}
|
package/src/core/tuning/index.ts
CHANGED
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
* acoustic (just) tuning behind the tempered intervals.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import {
|
|
12
|
+
assertFiniteNumber,
|
|
13
|
+
assertInteger,
|
|
14
|
+
assertPositiveInt,
|
|
15
|
+
assertRange,
|
|
16
|
+
} from '../validation/index.js';
|
|
17
|
+
|
|
11
18
|
/**
|
|
12
19
|
* An equal-tempered tuning: a reference pitch and a number of equal divisions of
|
|
13
20
|
* the octave. With `divisions === 12` a step index is an ordinary MIDI number.
|
|
@@ -30,6 +37,13 @@ export type Tuning = {
|
|
|
30
37
|
*/
|
|
31
38
|
export const TWELVE_TET: Tuning = { refStep: 69, refFreq: 440, divisions: 12 };
|
|
32
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
|
+
|
|
33
47
|
/**
|
|
34
48
|
* Build an equal temperament with `n` divisions of the octave.
|
|
35
49
|
*
|
|
@@ -46,6 +60,9 @@ export const TWELVE_TET: Tuning = { refStep: 69, refFreq: 440, divisions: 12 };
|
|
|
46
60
|
* @category Pitch & Intervals
|
|
47
61
|
*/
|
|
48
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');
|
|
49
66
|
return { refStep, refFreq, divisions: n };
|
|
50
67
|
}
|
|
51
68
|
|
|
@@ -64,7 +81,10 @@ export function edo(n: number, refFreq = 440, refStep = 69): Tuning {
|
|
|
64
81
|
* @category Pitch & Intervals
|
|
65
82
|
*/
|
|
66
83
|
export function frequencyOf(step: number, tuning: Tuning = TWELVE_TET): number {
|
|
67
|
-
|
|
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');
|
|
68
88
|
}
|
|
69
89
|
|
|
70
90
|
/**
|
|
@@ -77,6 +97,8 @@ export function frequencyOf(step: number, tuning: Tuning = TWELVE_TET): number {
|
|
|
77
97
|
* @category Pitch & Intervals
|
|
78
98
|
*/
|
|
79
99
|
export function nearestStep(freq: number, tuning: Tuning = TWELVE_TET): number {
|
|
100
|
+
assertRange(freq, Number.MIN_VALUE, Number.MAX_VALUE, 'frequency');
|
|
101
|
+
assertTuning(tuning);
|
|
80
102
|
return Math.round(tuning.refStep + tuning.divisions * Math.log2(freq / tuning.refFreq));
|
|
81
103
|
}
|
|
82
104
|
|
|
@@ -89,7 +111,9 @@ export function nearestStep(freq: number, tuning: Tuning = TWELVE_TET): number {
|
|
|
89
111
|
* @category Pitch & Intervals
|
|
90
112
|
*/
|
|
91
113
|
export function centsBetweenFreq(a: number, b: number): number {
|
|
92
|
-
|
|
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));
|
|
93
117
|
}
|
|
94
118
|
|
|
95
119
|
/**
|
|
@@ -101,7 +125,9 @@ export function centsBetweenFreq(a: number, b: number): number {
|
|
|
101
125
|
* @category Pitch & Intervals
|
|
102
126
|
*/
|
|
103
127
|
export function centsOfSteps(steps: number, tuning: Tuning = TWELVE_TET): number {
|
|
104
|
-
|
|
128
|
+
assertFiniteNumber(steps, 'steps');
|
|
129
|
+
assertTuning(tuning);
|
|
130
|
+
return assertFiniteNumber((steps * 1200) / tuning.divisions, 'cents result');
|
|
105
131
|
}
|
|
106
132
|
|
|
107
133
|
/**
|
|
@@ -114,7 +140,9 @@ export function centsOfSteps(steps: number, tuning: Tuning = TWELVE_TET): number
|
|
|
114
140
|
* @category Pitch & Intervals
|
|
115
141
|
*/
|
|
116
142
|
export function ratioToCents(numerator: number, denominator: number): number {
|
|
117
|
-
|
|
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));
|
|
118
146
|
}
|
|
119
147
|
|
|
120
148
|
/**
|
|
@@ -144,13 +172,15 @@ export const JUST_RATIOS: Record<number, [number, number]> = {
|
|
|
144
172
|
* (positive means the just interval is wider).
|
|
145
173
|
*
|
|
146
174
|
* @param semitoneClass Semitone class in [0, 12].
|
|
147
|
-
* @returns The deviation in cents
|
|
175
|
+
* @returns The deviation in cents.
|
|
176
|
+
* @throws If `semitoneClass` is not an integer in [0, 12].
|
|
148
177
|
* @category Pitch & Intervals
|
|
149
178
|
*/
|
|
150
179
|
export function justDeviationCents(semitoneClass: number): number {
|
|
180
|
+
assertInteger(semitoneClass, 'semitone class', 0, 12);
|
|
151
181
|
const ratio = JUST_RATIOS[semitoneClass];
|
|
152
182
|
if (!ratio) {
|
|
153
|
-
|
|
183
|
+
throw new RangeError(`semitone class has no just ratio: ${semitoneClass}`);
|
|
154
184
|
}
|
|
155
185
|
return ratioToCents(ratio[0], ratio[1]) - semitoneClass * 100;
|
|
156
186
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
}
|
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
import { isStrongBeat, type TimeSignature } from '../../core/meter/index.js';
|
|
16
16
|
import { createRng } from '../../core/random/index.js';
|
|
17
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';
|
|
18
24
|
import type { Chord } from '../../theory/chord/index.js';
|
|
19
25
|
import { nearestScaleTone } from '../../theory/scale/index.js';
|
|
20
26
|
|
|
@@ -282,14 +288,31 @@ function buildWalking(ctx: BuildContext, seg: BassSegment, next: BassSegment | u
|
|
|
282
288
|
* @category Composition
|
|
283
289
|
*/
|
|
284
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
|
+
}
|
|
285
301
|
const segments = [...opts.segments].sort((a, b) => a.startBeat - b.startBeat);
|
|
286
302
|
if (segments.length === 0) {
|
|
287
303
|
return [];
|
|
288
304
|
}
|
|
289
305
|
|
|
290
306
|
const ts = opts.ts ?? DEFAULT_TS;
|
|
307
|
+
assertTimeSignature(ts);
|
|
291
308
|
const style = opts.style ?? DEFAULT_STYLE;
|
|
292
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');
|
|
293
316
|
const low = octave * 12 + 12;
|
|
294
317
|
|
|
295
318
|
const ctx: BuildContext = {
|