@libraz/libcantus 0.9.1 → 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 +19 -3
- package/dist/analyze/index.cjs +1970 -0
- package/dist/analyze/index.cjs.map +1 -0
- package/dist/analyze/index.d.cts +341 -0
- package/dist/analyze/index.d.ts +341 -0
- package/dist/analyze/index.js +51 -0
- package/dist/analyze/index.js.map +1 -0
- package/dist/chunk-2ZWMSJH6.js +30 -0
- package/dist/chunk-2ZWMSJH6.js.map +1 -0
- package/dist/chunk-4MHA432G.js +2854 -0
- 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-C5XYKUVG.js +904 -0
- package/dist/chunk-C5XYKUVG.js.map +1 -0
- package/dist/chunk-CHMXELDS.js +549 -0
- package/dist/chunk-CHMXELDS.js.map +1 -0
- package/dist/chunk-DYWFNC4A.js +152 -0
- package/dist/chunk-DYWFNC4A.js.map +1 -0
- package/dist/chunk-EH6EM37U.js +315 -0
- 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-GOAW4VYL.js +719 -0
- package/dist/chunk-GOAW4VYL.js.map +1 -0
- package/dist/chunk-I5R7ZBJS.js +202 -0
- package/dist/chunk-I5R7ZBJS.js.map +1 -0
- package/dist/chunk-IT2O4XFE.js +154 -0
- package/dist/chunk-IT2O4XFE.js.map +1 -0
- package/dist/chunk-MIWO425M.js +412 -0
- 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-SGU7SPLK.js +924 -0
- package/dist/chunk-SGU7SPLK.js.map +1 -0
- package/dist/core/index.cjs +648 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +262 -0
- package/dist/core/index.d.ts +262 -0
- package/dist/core/index.js +93 -0
- package/dist/core/index.js.map +1 -0
- package/dist/function-DVH4Eusr.d.ts +187 -0
- package/dist/function-DYt4iKsn.d.cts +187 -0
- package/dist/generate/index.cjs +4123 -0
- package/dist/generate/index.cjs.map +1 -0
- package/dist/generate/index.d.cts +986 -0
- package/dist/generate/index.d.ts +986 -0
- package/dist/generate/index.js +52 -0
- package/dist/generate/index.js.map +1 -0
- package/dist/index-BNlGP86A.d.cts +157 -0
- package/dist/index-Bg5dYMmZ.d.ts +56 -0
- package/dist/index-Bt4tOUD9.d.ts +157 -0
- package/dist/index-CGMZNCx6.d.cts +140 -0
- package/dist/index-CGMZNCx6.d.ts +140 -0
- package/dist/index-Cl7mhOFM.d.cts +164 -0
- package/dist/index-Cl7mhOFM.d.ts +164 -0
- package/dist/index-CnZCo29u.d.cts +108 -0
- package/dist/index-CpOsP1y9.d.cts +151 -0
- package/dist/index-CxqBQtH1.d.cts +128 -0
- package/dist/index-Dikst44v.d.cts +56 -0
- package/dist/index-DxvARIP2.d.ts +151 -0
- package/dist/index-Jc0Ryi79.d.ts +128 -0
- package/dist/index-JnF4v9bx.d.ts +108 -0
- package/dist/index.cjs +3710 -3143
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -3982
- package/dist/index.d.ts +15 -3982
- package/dist/index.js +191 -6504
- package/dist/index.js.map +1 -1
- package/dist/model/index.cjs +2574 -0
- package/dist/model/index.cjs.map +1 -0
- package/dist/model/index.d.cts +631 -0
- package/dist/model/index.d.ts +631 -0
- package/dist/model/index.js +21 -0
- package/dist/model/index.js.map +1 -0
- package/dist/styled-DZ7GjShb.d.ts +282 -0
- package/dist/styled-ZTbdaVFM.d.cts +282 -0
- package/dist/theory/index.cjs +1823 -0
- package/dist/theory/index.cjs.map +1 -0
- package/dist/theory/index.d.cts +535 -0
- package/dist/theory/index.d.ts +535 -0
- package/dist/theory/index.js +150 -0
- package/dist/theory/index.js.map +1 -0
- package/dist/types-MGP29r3t.d.cts +30 -0
- package/dist/types-MGP29r3t.d.ts +30 -0
- package/package.json +61 -5
- package/src/analyze/arrange/index.ts +21 -0
- package/src/analyze/arrange/internal.ts +127 -0
- package/src/analyze/arrange/tension.ts +199 -0
- package/src/analyze/arrange/tracks.ts +312 -0
- package/src/{detect → analyze/detect}/index.ts +50 -13
- package/src/analyze/functional/borrowed.ts +84 -0
- package/src/analyze/functional/cadence.ts +66 -0
- package/src/analyze/functional/function.ts +184 -0
- package/src/analyze/functional/index.ts +23 -0
- package/src/analyze/functional/internal.ts +29 -0
- package/src/{functional/index.ts → analyze/functional/roman.ts} +23 -332
- package/src/analyze/index.ts +49 -0
- package/src/{timeline → analyze/timeline}/index.ts +50 -18
- package/src/{analysis → analyze/voice}/index.ts +19 -7
- package/src/core/event-index/index.ts +104 -0
- package/src/core/index.ts +61 -0
- package/src/{meter → core/meter}/index.ts +24 -4
- package/src/{random → core/random}/index.ts +26 -3
- package/src/{tuning → core/tuning}/index.ts +36 -6
- package/src/core/validation/index.ts +119 -0
- package/src/{bass → generate/bass}/index.ts +28 -5
- package/src/{countermelody → generate/countermelody}/index.ts +176 -85
- package/src/{drums → generate/drums}/euclid.ts +10 -3
- package/src/{drums → generate/drums}/index.ts +58 -26
- package/src/{drums → generate/drums}/percussion.ts +1 -1
- package/src/{drums → generate/drums}/rng.ts +2 -2
- package/src/{groove → generate/groove}/index.ts +28 -3
- package/src/{harmonize → generate/harmonize}/index.ts +68 -57
- package/src/generate/index.ts +58 -0
- package/src/{motif → generate/motif}/index.ts +52 -28
- package/src/{progression → generate/progression}/index.ts +19 -41
- package/src/{reharmony → generate/reharmony}/index.ts +12 -19
- package/src/{rhythm → generate/rhythm}/index.ts +13 -3
- package/src/index.ts +13 -234
- package/src/model/chord.ts +442 -0
- package/src/model/index.ts +5 -1096
- package/src/model/interval.ts +82 -0
- package/src/model/key.ts +266 -0
- package/src/model/note.ts +176 -0
- package/src/model/progression.ts +161 -0
- package/src/model/shared.ts +12 -0
- package/src/{chord → theory/chord}/index.ts +23 -3
- package/src/{counterpoint → theory/counterpoint}/index.ts +2 -2
- package/src/{harmony → theory/harmony}/index.ts +6 -21
- package/src/theory/index.ts +112 -0
- package/src/{safety → theory/safety}/index.ts +72 -3
- package/src/theory/scale/degrees.ts +123 -0
- package/src/theory/scale/index.ts +28 -0
- package/src/theory/scale/key.ts +56 -0
- package/src/theory/scale/masks.ts +153 -0
- package/src/{spelling → theory/spelling}/index.ts +73 -6
- package/src/{symbol → theory/symbol}/index.ts +8 -1
- package/src/theory/voicing/index.ts +5 -0
- package/src/theory/voicing/internal.ts +168 -0
- package/src/theory/voicing/leading.ts +112 -0
- package/src/theory/voicing/satb.ts +216 -0
- package/src/theory/voicing/styled.ts +199 -0
- package/src/arrange/index.ts +0 -571
- package/src/scale/index.ts +0 -331
- package/src/voicing/index.ts +0 -623
- /package/src/{interval → core/interval}/index.ts +0 -0
- /package/src/{pitch → core/pitch}/index.ts +0 -0
- /package/src/{types.ts → core/types.ts} +0 -0
- /package/src/{drums → generate/drums}/beat.ts +0 -0
- /package/src/{drums → generate/drums}/fills.ts +0 -0
- /package/src/{drums → generate/drums}/ghost.ts +0 -0
- /package/src/{drums → generate/drums}/hihat.ts +0 -0
- /package/src/{drums → generate/drums}/hit.ts +0 -0
- /package/src/{drums → generate/drums}/internal.ts +0 -0
- /package/src/{drums → generate/drums}/kick.ts +0 -0
- /package/src/{drums → generate/drums}/swing.ts +0 -0
- /package/src/{chordscale → theory/chordscale}/index.ts +0 -0
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { createNoteEventIndex } from '../../core/event-index/index.js';
|
|
2
|
+
import type { TimeSignature } from '../../core/meter/index.js';
|
|
3
|
+
import { beatsPerBar, metricWeight, parseTimeSignature } from '../../core/meter/index.js';
|
|
4
|
+
import type { KeyScale, NoteEvent } from '../../core/types.js';
|
|
5
|
+
import {
|
|
6
|
+
assertFiniteNumber,
|
|
7
|
+
assertGenerationBudget,
|
|
8
|
+
assertNoteEvents,
|
|
9
|
+
assertRange,
|
|
10
|
+
assertTimeSignature,
|
|
11
|
+
} from '../../core/validation/index.js';
|
|
12
|
+
import type { Chord, ChordSpan } from '../../theory/chord/index.js';
|
|
13
|
+
import { chordPitchClasses, makeChord } from '../../theory/chord/index.js';
|
|
14
|
+
import { isScaleTone, majorKey } from '../../theory/scale/index.js';
|
|
3
15
|
import type { ChordMatch } from '../detect/index.js';
|
|
4
16
|
import { detectChord, detectChordBest, detectKey } from '../detect/index.js';
|
|
5
17
|
import type { Cadence } from '../functional/index.js';
|
|
6
18
|
import { detectCadence } from '../functional/index.js';
|
|
7
|
-
import type { TimeSignature } from '../meter/index.js';
|
|
8
|
-
import { beatsPerBar, metricWeight, parseTimeSignature } from '../meter/index.js';
|
|
9
|
-
import type { GeneratedChord } from '../progression/index.js';
|
|
10
|
-
import { isScaleTone, majorKey } from '../scale/index.js';
|
|
11
|
-
import type { KeyScale, NoteEvent } from '../types.js';
|
|
12
19
|
|
|
13
20
|
/**
|
|
14
21
|
* A chord occupying a half-open beat span.
|
|
@@ -45,15 +52,25 @@ export type ChordTimeline = {
|
|
|
45
52
|
* @returns A queryable chord timeline.
|
|
46
53
|
* @category Arrangement & Analysis
|
|
47
54
|
*/
|
|
48
|
-
export function chordTimelineFromChords(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
)
|
|
52
|
-
|
|
55
|
+
export function chordTimelineFromChords(chords: ChordSpan[], totalBeats: number): ChordTimeline {
|
|
56
|
+
assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, 'timeline totalBeats');
|
|
57
|
+
assertGenerationBudget(chords.length, 'timeline chords');
|
|
58
|
+
for (let index = 0; index < chords.length; index += 1) {
|
|
59
|
+
assertRange(
|
|
60
|
+
chords[index]?.startBeat ?? Number.NaN,
|
|
61
|
+
0,
|
|
62
|
+
Number.MAX_SAFE_INTEGER,
|
|
63
|
+
`chords[${index}].startBeat`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
const sorted = [...chords]
|
|
67
|
+
.filter((chord) => chord.startBeat < totalBeats)
|
|
68
|
+
.sort((a, b) => a.startBeat - b.startBeat);
|
|
53
69
|
const segments: ChordSegment[] = sorted
|
|
54
70
|
.map((gc, i) => {
|
|
55
71
|
const next = sorted[i + 1];
|
|
56
|
-
const
|
|
72
|
+
const nextBeat = next ? next.startBeat : totalBeats;
|
|
73
|
+
const endBeat = Math.min(totalBeats, Math.max(gc.startBeat, nextBeat));
|
|
57
74
|
return {
|
|
58
75
|
startBeat: gc.startBeat,
|
|
59
76
|
endBeat,
|
|
@@ -68,6 +85,7 @@ export function chordTimelineFromChords(
|
|
|
68
85
|
/** Build the `at(beat)` lookup over a segment list. */
|
|
69
86
|
function segmentLookup(segments: ChordSegment[]): (beat: number) => Chord | null {
|
|
70
87
|
return (beat) => {
|
|
88
|
+
assertFiniteNumber(beat, 'timeline query beat');
|
|
71
89
|
for (const seg of segments) {
|
|
72
90
|
if (beat >= seg.startBeat && beat < seg.endBeat) {
|
|
73
91
|
return seg.chord;
|
|
@@ -321,23 +339,37 @@ export function chordTimelineFromNotes(
|
|
|
321
339
|
opts: ChordTimelineOptions = {},
|
|
322
340
|
): ChordTimelineResult {
|
|
323
341
|
const ts = opts.ts ?? parseTimeSignature('4/4');
|
|
342
|
+
assertTimeSignature(ts);
|
|
324
343
|
const harmonicRhythm = opts.harmonicRhythm ?? beatsPerBar(ts);
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
344
|
+
assertRange(harmonicRhythm, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, 'harmonic rhythm');
|
|
345
|
+
assertNoteEvents(notes, 'timeline notes', { allowNonPositiveDuration: true });
|
|
328
346
|
// Zero/negative-length notes never sound; drop them before any inference.
|
|
329
|
-
const
|
|
347
|
+
const soundingIndex = createNoteEventIndex(notes.filter((note) => note.durationBeat > 0));
|
|
348
|
+
const sounding = soundingIndex.notes.map(({ note }) => note);
|
|
330
349
|
const lastNoteEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
|
|
331
350
|
const totalBeats = opts.totalBeats ?? lastNoteEnd;
|
|
351
|
+
assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, 'timeline totalBeats');
|
|
332
352
|
const key = opts.key ?? detectKey(sounding.map((n) => n.pitch))[0]?.key ?? majorKey(0);
|
|
333
353
|
|
|
334
354
|
const segments: ChordSegment[] = [];
|
|
335
355
|
const segmentConfidence: number[] = [];
|
|
336
356
|
const windowCount = Math.max(0, Math.ceil(totalBeats / harmonicRhythm - EPS));
|
|
357
|
+
assertGenerationBudget(windowCount, 'timeline windows');
|
|
358
|
+
const windowNotes: NoteEvent[][] = Array.from({ length: windowCount }, () => []);
|
|
359
|
+
let memberships = 0;
|
|
360
|
+
for (const indexed of soundingIndex.notes) {
|
|
361
|
+
const first = Math.max(0, Math.floor(indexed.note.startBeat / harmonicRhythm));
|
|
362
|
+
const lastExclusive = Math.min(windowCount, Math.ceil(indexed.endBeat / harmonicRhythm));
|
|
363
|
+
memberships += Math.max(0, lastExclusive - first);
|
|
364
|
+
assertGenerationBudget(memberships, 'timeline note-to-window memberships');
|
|
365
|
+
for (let window = first; window < lastExclusive; window += 1) {
|
|
366
|
+
windowNotes[window]?.push(indexed.note);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
337
369
|
for (let i = 0; i < windowCount; i += 1) {
|
|
338
370
|
const start = i * harmonicRhythm;
|
|
339
371
|
const end = Math.min(start + harmonicRhythm, totalBeats);
|
|
340
|
-
const inferred = analyzeWindow(
|
|
372
|
+
const inferred = analyzeWindow(windowNotes[i] ?? [], start, end, ts, key);
|
|
341
373
|
if (!inferred) {
|
|
342
374
|
continue;
|
|
343
375
|
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import
|
|
1
|
+
import type { KeyScale, NoteEvent } from '../../core/types.js';
|
|
2
|
+
import { assertNoteEvents } from '../../core/validation/index.js';
|
|
3
|
+
import type { Chord } from '../../theory/chord/index.js';
|
|
4
|
+
import { chordPitchClasses, chordToneRole } from '../../theory/chord/index.js';
|
|
5
|
+
import {
|
|
6
|
+
createsVerticalDissonance,
|
|
7
|
+
isLeadingToneResolution,
|
|
8
|
+
} from '../../theory/counterpoint/index.js';
|
|
9
|
+
import type { VoiceSnapshot } from '../../theory/safety/index.js';
|
|
6
10
|
|
|
7
11
|
/**
|
|
8
12
|
* A theory annotation attached to a note.
|
|
@@ -162,6 +166,7 @@ export function analyzeVoice(
|
|
|
162
166
|
key: KeyScale,
|
|
163
167
|
otherVoicesAtBeat: (beat: number) => VoiceSnapshot[],
|
|
164
168
|
): AnalyzedNote[] {
|
|
169
|
+
assertNoteEvents(voice, 'voice notes', { allowNonPositiveDuration: true });
|
|
165
170
|
const result: AnalyzedNote[] = [];
|
|
166
171
|
|
|
167
172
|
for (let i = 0; i < voice.length; i += 1) {
|
|
@@ -174,10 +179,17 @@ export function analyzeVoice(
|
|
|
174
179
|
// A neighboring note sharing this note's onset is a simultaneous cluster
|
|
175
180
|
// member, not a melodic predecessor/successor; treating it as one would
|
|
176
181
|
// fabricate passing/suspension figures, so it is dropped here.
|
|
182
|
+
const prevEnd =
|
|
183
|
+
prevNote === undefined
|
|
184
|
+
? Number.NEGATIVE_INFINITY
|
|
185
|
+
: prevNote.startBeat + prevNote.durationBeat;
|
|
186
|
+
const noteEnd = note.startBeat + note.durationBeat;
|
|
177
187
|
const prev =
|
|
178
|
-
prevNote !== undefined &&
|
|
188
|
+
prevNote !== undefined && Math.abs(prevEnd - note.startBeat) <= EPS ? prevNote : undefined;
|
|
179
189
|
const next =
|
|
180
|
-
nextNote !== undefined &&
|
|
190
|
+
nextNote !== undefined && Math.abs(noteEnd - nextNote.startBeat) <= EPS
|
|
191
|
+
? nextNote
|
|
192
|
+
: undefined;
|
|
181
193
|
const chord = chordAtBeat(note.startBeat);
|
|
182
194
|
const labels: TheoryLabel[] = [];
|
|
183
195
|
const member = isChordMember(note.pitch, chord);
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public API of the core layer. Re-exports every core-layer module;
|
|
3
|
+
* also available from the package root.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type { IndexedNoteEvent, NoteEventIndex } from './event-index/index.js';
|
|
7
|
+
export { createNoteEventIndex } from './event-index/index.js';
|
|
8
|
+
export {
|
|
9
|
+
classifyInterval,
|
|
10
|
+
IntervalQuality,
|
|
11
|
+
isConsonantInterval,
|
|
12
|
+
isPerfectInterval,
|
|
13
|
+
} from './interval/index.js';
|
|
14
|
+
export type { BarPosition, TimeSignature } from './meter/index.js';
|
|
15
|
+
export {
|
|
16
|
+
barPositionToBeat,
|
|
17
|
+
beatsPerBar,
|
|
18
|
+
beatToBarPosition,
|
|
19
|
+
formatTimeSignature,
|
|
20
|
+
isCompound,
|
|
21
|
+
isStrongBeat,
|
|
22
|
+
metricWeight,
|
|
23
|
+
parseTimeSignature,
|
|
24
|
+
pulsesPerBar,
|
|
25
|
+
tuplet,
|
|
26
|
+
} from './meter/index.js';
|
|
27
|
+
export type { Note as NoteData, SpelledInterval } from './pitch/index.js';
|
|
28
|
+
export {
|
|
29
|
+
formatNote,
|
|
30
|
+
midiToNote,
|
|
31
|
+
noteToMidi,
|
|
32
|
+
noteToPitchClass,
|
|
33
|
+
parseNote,
|
|
34
|
+
spelledInterval,
|
|
35
|
+
} from './pitch/index.js';
|
|
36
|
+
export type { Rng } from './random/index.js';
|
|
37
|
+
export { createRng } from './random/index.js';
|
|
38
|
+
export type { Tuning } from './tuning/index.js';
|
|
39
|
+
export {
|
|
40
|
+
centsBetweenFreq,
|
|
41
|
+
centsOfSteps,
|
|
42
|
+
edo,
|
|
43
|
+
frequencyOf,
|
|
44
|
+
JUST_RATIOS,
|
|
45
|
+
justDeviationCents,
|
|
46
|
+
nearestStep,
|
|
47
|
+
ratioToCents,
|
|
48
|
+
TWELVE_TET,
|
|
49
|
+
} from './tuning/index.js';
|
|
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';
|
|
@@ -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
|
}
|
|
@@ -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
|
}
|
|
@@ -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
|
+
}
|