@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,18 +1,22 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
1
|
+
import { isDiatonic, parallelKey } from '../../analyze/functional/index.js';
|
|
2
|
+
import { createNoteEventIndex } from '../../core/event-index/index.js';
|
|
3
|
+
import type { TimeSignature } from '../../core/meter/index.js';
|
|
4
|
+
import { isStrongBeat } from '../../core/meter/index.js';
|
|
5
|
+
import { createRng } from '../../core/random/index.js';
|
|
6
|
+
import type { KeyScale, NoteEvent } from '../../core/types.js';
|
|
7
|
+
import { assertGenerationBudget, assertRange } from '../../core/validation/index.js';
|
|
8
|
+
import type { Chord, ChordQuality } from '../../theory/chord/index.js';
|
|
9
|
+
import { chordPitchClasses, diatonicTriad, makeChord } from '../../theory/chord/index.js';
|
|
10
|
+
import type { HarmonyRole } from '../../theory/harmony/index.js';
|
|
11
|
+
import { roleOf } from '../../theory/harmony/index.js';
|
|
8
12
|
import {
|
|
9
13
|
isScaleTone,
|
|
10
14
|
majorKey,
|
|
11
15
|
minorKey,
|
|
12
16
|
NATURAL_MINOR_MASK,
|
|
13
17
|
scaleTonesInDegreeOrder,
|
|
14
|
-
} from '
|
|
15
|
-
import type {
|
|
18
|
+
} from '../../theory/scale/index.js';
|
|
19
|
+
import type { ChordSpan } from '../progression/index.js';
|
|
16
20
|
|
|
17
21
|
/**
|
|
18
22
|
* A melody note supplied to the harmonizer: a {@link NoteEvent}.
|
|
@@ -48,7 +52,7 @@ export type HarmonizeOptions = {
|
|
|
48
52
|
export type HarmonizeResult = {
|
|
49
53
|
transposeSemitones: number;
|
|
50
54
|
key: KeyScale;
|
|
51
|
-
chords:
|
|
55
|
+
chords: ChordSpan[];
|
|
52
56
|
melodyRoles: { noteIndex: number; role: HarmonyRole }[];
|
|
53
57
|
};
|
|
54
58
|
|
|
@@ -86,17 +90,6 @@ function pitchClass(pitch: number): number {
|
|
|
86
90
|
return ((Math.trunc(pitch) % 12) + 12) % 12;
|
|
87
91
|
}
|
|
88
92
|
|
|
89
|
-
function mulberry32(seed: number): () => number {
|
|
90
|
-
let state = seed >>> 0;
|
|
91
|
-
return () => {
|
|
92
|
-
state = (state + 0x6d2b79f5) >>> 0;
|
|
93
|
-
let t = state;
|
|
94
|
-
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
95
|
-
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
96
|
-
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
93
|
/**
|
|
101
94
|
* Estimate the best-fit key from a melody's pitch-class weighting.
|
|
102
95
|
*
|
|
@@ -151,7 +144,6 @@ function buildCandidates(key: KeyScale, reharmonize: HarmonizeOptions['reharmoni
|
|
|
151
144
|
base: 0,
|
|
152
145
|
}));
|
|
153
146
|
|
|
154
|
-
const tonic = tones[0] ?? 0;
|
|
155
147
|
if (reharmonize !== 'diatonic') {
|
|
156
148
|
for (const target of [1, 3, 4, 5]) {
|
|
157
149
|
const targetRoot = tones[target] ?? 0;
|
|
@@ -166,27 +158,30 @@ function buildCandidates(key: KeyScale, reharmonize: HarmonizeOptions['reharmoni
|
|
|
166
158
|
}
|
|
167
159
|
|
|
168
160
|
if (reharmonize === 'borrowed') {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
rootPc: (tonic + 5) % 12,
|
|
183
|
-
quality: 'min',
|
|
184
|
-
secondaryDominant: false,
|
|
185
|
-
base: 2,
|
|
186
|
-
}); // iv
|
|
161
|
+
const parallel = parallelKey(key);
|
|
162
|
+
const parallelTones = scaleTonesInDegreeOrder(parallel);
|
|
163
|
+
for (let degree = 0; degree < parallelTones.length; degree += 1) {
|
|
164
|
+
const chord = diatonicTriad(degree, parallel);
|
|
165
|
+
if (!isDiatonic(chord, key)) {
|
|
166
|
+
candidates.push({
|
|
167
|
+
rootPc: chord.rootPc,
|
|
168
|
+
quality: chord.quality,
|
|
169
|
+
secondaryDominant: false,
|
|
170
|
+
base: 1.25,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
187
174
|
}
|
|
188
175
|
|
|
189
|
-
|
|
176
|
+
const seen = new Set<string>();
|
|
177
|
+
return candidates.filter((candidate) => {
|
|
178
|
+
const id = `${candidate.rootPc}:${candidate.quality}`;
|
|
179
|
+
if (seen.has(id)) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
seen.add(id);
|
|
183
|
+
return true;
|
|
184
|
+
});
|
|
190
185
|
}
|
|
191
186
|
|
|
192
187
|
/**
|
|
@@ -356,29 +351,45 @@ function harmonizeOnce(
|
|
|
356
351
|
* reharmonize: 'diatonic',
|
|
357
352
|
* placement: { transposeSearch: false, octaveSearch: false },
|
|
358
353
|
* });
|
|
359
|
-
* result.chords; // one
|
|
354
|
+
* result.chords; // one ChordSpan per harmonic-rhythm segment
|
|
360
355
|
* ```
|
|
361
356
|
* @category Reharmonization
|
|
362
357
|
*/
|
|
363
358
|
export function harmonizeMelody(opts: HarmonizeOptions): HarmonizeResult {
|
|
359
|
+
const noteIndex = createNoteEventIndex(opts.melody);
|
|
364
360
|
const key = opts.key === 'infer' ? inferKey(opts.melody) : opts.key;
|
|
365
361
|
const candidates = buildCandidates(key, opts.reharmonize);
|
|
366
362
|
const candAt = (i: number): Candidate => candidates[i] ?? FALLBACK;
|
|
367
|
-
const rng =
|
|
368
|
-
const jitter = candidates.map(() => rng() * TIE_BREAK_JITTER);
|
|
363
|
+
const rng = createRng(opts.seed ?? 0);
|
|
364
|
+
const jitter = candidates.map(() => rng.next() * TIE_BREAK_JITTER);
|
|
369
365
|
|
|
370
|
-
const melodyEnd =
|
|
371
|
-
const
|
|
366
|
+
const melodyEnd = noteIndex.notes.reduce((m, n) => Math.max(m, n.endBeat), 0);
|
|
367
|
+
const requestedHr = assertRange(
|
|
368
|
+
opts.harmonicRhythm,
|
|
369
|
+
Number.MIN_VALUE,
|
|
370
|
+
Number.MAX_SAFE_INTEGER,
|
|
371
|
+
'harmonic rhythm',
|
|
372
|
+
);
|
|
373
|
+
const hr = Math.max(0.25, requestedHr);
|
|
372
374
|
const segCount = Math.max(1, Math.ceil(melodyEnd / hr));
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
375
|
+
assertGenerationBudget(segCount, 'harmonic segments');
|
|
376
|
+
assertGenerationBudget(segCount * candidates.length * candidates.length, 'harmonization search');
|
|
377
|
+
const segments: Segment[] = Array.from({ length: segCount }, (_, s) => ({
|
|
378
|
+
startBeat: s * hr,
|
|
379
|
+
endBeat: (s + 1) * hr,
|
|
380
|
+
noteIndices: [],
|
|
381
|
+
}));
|
|
382
|
+
// Associate each note only with the windows it actually spans. This replaces
|
|
383
|
+
// the former per-window full melody scan and creates no temporary note objects.
|
|
384
|
+
let memberships = 0;
|
|
385
|
+
for (const indexed of noteIndex.notes) {
|
|
386
|
+
const first = Math.max(0, Math.floor(indexed.note.startBeat / hr));
|
|
387
|
+
const lastExclusive = Math.min(segCount, Math.ceil(indexed.endBeat / hr - Number.EPSILON));
|
|
388
|
+
memberships += Math.max(0, lastExclusive - first);
|
|
389
|
+
assertGenerationBudget(memberships, 'note-to-segment memberships');
|
|
390
|
+
for (let segment = first; segment < lastExclusive; segment += 1) {
|
|
391
|
+
segments[segment]?.noteIndices.push(indexed.originalIndex);
|
|
392
|
+
}
|
|
382
393
|
}
|
|
383
394
|
|
|
384
395
|
const transposes: number[] = [0];
|
|
@@ -413,9 +424,9 @@ export function harmonizeMelody(opts: HarmonizeOptions): HarmonizeResult {
|
|
|
413
424
|
}
|
|
414
425
|
}
|
|
415
426
|
|
|
416
|
-
const chords:
|
|
427
|
+
const chords: ChordSpan[] = bestPath.map((ci, s) => {
|
|
417
428
|
const cand = candAt(ci);
|
|
418
|
-
const chord:
|
|
429
|
+
const chord: ChordSpan = {
|
|
419
430
|
rootPc: cand.rootPc,
|
|
420
431
|
quality: cand.quality,
|
|
421
432
|
startBeat: segments[s]?.startBeat ?? s * hr,
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public API of the generate layer. Re-exports every generate-layer module;
|
|
3
|
+
* also available from the package root.
|
|
4
|
+
*/
|
|
5
|
+
export type { BassLineOptions, BassSegment, BassStyle } from './bass/index.js';
|
|
6
|
+
export { generateBassLine } from './bass/index.js';
|
|
7
|
+
export type { CounterMelodyOptions } from './countermelody/index.js';
|
|
8
|
+
export { generateCounterMelody } from './countermelody/index.js';
|
|
9
|
+
export type {
|
|
10
|
+
DrumHit,
|
|
11
|
+
DrumRole,
|
|
12
|
+
DrumsOptions,
|
|
13
|
+
EuclideanKick,
|
|
14
|
+
GrooveFeel,
|
|
15
|
+
GrooveStyle,
|
|
16
|
+
Section,
|
|
17
|
+
} from './drums/index.js';
|
|
18
|
+
export { generateDrums } from './drums/index.js';
|
|
19
|
+
export type { GrooveSlot, GrooveTemplate, HumanizeOptions } from './groove/index.js';
|
|
20
|
+
export { applyGrooveTemplate, extractGrooveTemplate, humanize } from './groove/index.js';
|
|
21
|
+
export type {
|
|
22
|
+
HarmonizeOptions,
|
|
23
|
+
HarmonizeResult,
|
|
24
|
+
MelodyNote,
|
|
25
|
+
} from './harmonize/index.js';
|
|
26
|
+
export { harmonizeMelody } from './harmonize/index.js';
|
|
27
|
+
export type {
|
|
28
|
+
MotifCell,
|
|
29
|
+
MotifContour,
|
|
30
|
+
MotifNote,
|
|
31
|
+
MotifOptions,
|
|
32
|
+
MotifTransform,
|
|
33
|
+
} from './motif/index.js';
|
|
34
|
+
export { developMotif, generateMotif, transformMotif } from './motif/index.js';
|
|
35
|
+
export type {
|
|
36
|
+
ProgFunction,
|
|
37
|
+
ProgressionOptions,
|
|
38
|
+
ProgressionPreset,
|
|
39
|
+
ProgStyle,
|
|
40
|
+
} from './progression/index.js';
|
|
41
|
+
export {
|
|
42
|
+
generateProgression,
|
|
43
|
+
progressions,
|
|
44
|
+
progressionsByStyle,
|
|
45
|
+
} from './progression/index.js';
|
|
46
|
+
export type {
|
|
47
|
+
BorrowedChord,
|
|
48
|
+
SubstituteOptions,
|
|
49
|
+
Substitution,
|
|
50
|
+
SubstitutionType,
|
|
51
|
+
} from './reharmony/index.js';
|
|
52
|
+
export {
|
|
53
|
+
modalInterchangePalette,
|
|
54
|
+
negativeHarmonyMirror,
|
|
55
|
+
substituteChord,
|
|
56
|
+
} from './reharmony/index.js';
|
|
57
|
+
export type { RhythmEvent, RhythmOptions } from './rhythm/index.js';
|
|
58
|
+
export { generateRhythm, onsetWeightCurve, rhythmDensity } from './rhythm/index.js';
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
|
|
1
|
+
import type { ChordTimeline } from '../../analyze/timeline/index.js';
|
|
2
|
+
import { createRng } from '../../core/random/index.js';
|
|
3
|
+
import type { KeyScale } from '../../core/types.js';
|
|
4
|
+
import {
|
|
5
|
+
assertFiniteNumber,
|
|
6
|
+
assertGenerationBudget,
|
|
7
|
+
assertInteger,
|
|
8
|
+
assertNoteEvents,
|
|
9
|
+
assertPositiveInt,
|
|
10
|
+
} from '../../core/validation/index.js';
|
|
11
|
+
import type { Chord } from '../../theory/chord/index.js';
|
|
12
|
+
import { chordPitchClasses } from '../../theory/chord/index.js';
|
|
13
|
+
import { isScaleTone, nearestScaleTone } from '../../theory/scale/index.js';
|
|
6
14
|
|
|
7
15
|
/**
|
|
8
16
|
* A transformation applicable to a motif cell.
|
|
@@ -50,7 +58,7 @@ export type MotifContour = 'arch' | 'ascending' | 'descending' | 'wave';
|
|
|
50
58
|
*
|
|
51
59
|
* @category Composition
|
|
52
60
|
*/
|
|
53
|
-
export type
|
|
61
|
+
export type MotifOptions = {
|
|
54
62
|
key: KeyScale;
|
|
55
63
|
chord?: Chord | null;
|
|
56
64
|
bars: number;
|
|
@@ -133,7 +141,20 @@ function nearestChordTone(pitch: number, chord: Chord): number {
|
|
|
133
141
|
|
|
134
142
|
/** Earliest onset in a cell; the time origin for timing transforms. */
|
|
135
143
|
function cellOrigin(notes: MotifNote[]): number {
|
|
136
|
-
|
|
144
|
+
let origin = Number.POSITIVE_INFINITY;
|
|
145
|
+
for (const note of notes) {
|
|
146
|
+
origin = Math.min(origin, note.startBeat);
|
|
147
|
+
}
|
|
148
|
+
return Number.isFinite(origin) ? origin : 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Latest offset in a cell, without spreading an unbounded caller array. */
|
|
152
|
+
function cellEnd(notes: MotifNote[]): number {
|
|
153
|
+
let end = Number.NEGATIVE_INFINITY;
|
|
154
|
+
for (const note of notes) {
|
|
155
|
+
end = Math.max(end, note.startBeat + note.durationBeat);
|
|
156
|
+
}
|
|
157
|
+
return Number.isFinite(end) ? end : 0;
|
|
137
158
|
}
|
|
138
159
|
|
|
139
160
|
/** Total beat span covered by a cell (from first onset to last offset). */
|
|
@@ -142,7 +163,7 @@ function cellSpan(cell: MotifCell): number {
|
|
|
142
163
|
return 0;
|
|
143
164
|
}
|
|
144
165
|
const start = cellOrigin(cell.notes);
|
|
145
|
-
const end =
|
|
166
|
+
const end = cellEnd(cell.notes);
|
|
146
167
|
return end - start;
|
|
147
168
|
}
|
|
148
169
|
|
|
@@ -150,18 +171,6 @@ function clone(cell: MotifCell): MotifCell {
|
|
|
150
171
|
return { notes: cell.notes.map((n) => ({ ...n })) };
|
|
151
172
|
}
|
|
152
173
|
|
|
153
|
-
/** Deterministic 32-bit PRNG in [0, 1). */
|
|
154
|
-
function mulberry32(seed: number): () => number {
|
|
155
|
-
let state = seed >>> 0;
|
|
156
|
-
return () => {
|
|
157
|
-
state = (state + 0x6d2b79f5) >>> 0;
|
|
158
|
-
let t = state;
|
|
159
|
-
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
160
|
-
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
161
|
-
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
|
|
165
174
|
/** Diatonic scale-degree offsets shaping a contour of `count` notes. */
|
|
166
175
|
function contourOffsets(contour: MotifContour, count: number): number[] {
|
|
167
176
|
const offsets: number[] = [];
|
|
@@ -207,14 +216,16 @@ function contourOffsets(contour: MotifContour, count: number): number[] {
|
|
|
207
216
|
*
|
|
208
217
|
* @category Composition
|
|
209
218
|
*/
|
|
210
|
-
export function generateMotif(opts:
|
|
219
|
+
export function generateMotif(opts: MotifOptions): MotifCell {
|
|
211
220
|
const contour = opts.contour ?? 'arch';
|
|
212
|
-
const bars =
|
|
221
|
+
const bars = assertPositiveInt(opts.bars, 'motif bars');
|
|
213
222
|
const totalBeats = bars * 4;
|
|
214
223
|
const noteCount = Math.max(3, bars * 2);
|
|
224
|
+
assertGenerationBudget(noteCount, 'motif notes');
|
|
215
225
|
const beatsPerNote = totalBeats / noteCount;
|
|
216
|
-
const rng =
|
|
217
|
-
const
|
|
226
|
+
const rng = createRng(opts.seed ?? 0);
|
|
227
|
+
const requestedJitter = assertFiniteNumber(opts.jitter ?? 0, 'motif jitter');
|
|
228
|
+
const jitterProb = Math.min(1, Math.max(0, requestedJitter));
|
|
218
229
|
const tonic = pitchClass(opts.key.rootPc) + 60;
|
|
219
230
|
const offsets = contourOffsets(contour, noteCount);
|
|
220
231
|
|
|
@@ -224,8 +235,8 @@ export function generateMotif(opts: GenerateMotifOptions): MotifCell {
|
|
|
224
235
|
// preserved exactly. When enabled it nudges up or down with equal odds,
|
|
225
236
|
// avoiding the upward bias that used to drift arch/wave tails off the tonic.
|
|
226
237
|
let jitter = 0;
|
|
227
|
-
if (jitterProb > 0 && rng() < jitterProb) {
|
|
228
|
-
jitter = rng() < 0.5 ? 1 : -1;
|
|
238
|
+
if (jitterProb > 0 && rng.next() < jitterProb) {
|
|
239
|
+
jitter = rng.next() < 0.5 ? 1 : -1;
|
|
229
240
|
}
|
|
230
241
|
let pitch = stepDiatonic(tonic, (offsets[i] ?? 0) + jitter, opts.key);
|
|
231
242
|
const startBeat = i * beatsPerNote;
|
|
@@ -271,6 +282,16 @@ export function transformMotif(
|
|
|
271
282
|
amount?: number,
|
|
272
283
|
key?: KeyScale,
|
|
273
284
|
): MotifCell {
|
|
285
|
+
assertNoteEvents(cell.notes, 'motif notes');
|
|
286
|
+
if (amount !== undefined) {
|
|
287
|
+
assertFiniteNumber(amount, 'motif transform amount');
|
|
288
|
+
}
|
|
289
|
+
if ((t === 'transposeDiatonic' || t === 'sequence') && amount !== undefined) {
|
|
290
|
+
assertInteger(amount, 'diatonic transform amount');
|
|
291
|
+
}
|
|
292
|
+
if ((t === 'augment' || t === 'diminish') && amount !== undefined && amount <= 0) {
|
|
293
|
+
throw new RangeError('time transform amount must be positive');
|
|
294
|
+
}
|
|
274
295
|
const notes = cell.notes;
|
|
275
296
|
switch (t) {
|
|
276
297
|
case 'transposeChromatic': {
|
|
@@ -297,7 +318,7 @@ export function transformMotif(
|
|
|
297
318
|
return { notes: [] };
|
|
298
319
|
}
|
|
299
320
|
const start = cellOrigin(notes);
|
|
300
|
-
const end =
|
|
321
|
+
const end = cellEnd(notes);
|
|
301
322
|
return {
|
|
302
323
|
notes: notes.map((n) => ({
|
|
303
324
|
pitch: n.pitch,
|
|
@@ -363,8 +384,10 @@ export function developMotif(
|
|
|
363
384
|
key: KeyScale,
|
|
364
385
|
bars: number,
|
|
365
386
|
): MotifCell {
|
|
387
|
+
assertPositiveInt(bars, 'development bars');
|
|
388
|
+
assertNoteEvents(cell.notes, 'motif notes');
|
|
366
389
|
const span = cellSpan(cell);
|
|
367
|
-
const totalBeats =
|
|
390
|
+
const totalBeats = bars * 4;
|
|
368
391
|
const origin = cellOrigin(cell.notes);
|
|
369
392
|
const out: MotifNote[] = [];
|
|
370
393
|
|
|
@@ -376,6 +399,7 @@ export function developMotif(
|
|
|
376
399
|
// the offset multiplicatively so rounding error cannot accumulate over tiles.
|
|
377
400
|
const tileSpan = Math.max(span, MIN_TILE_SPAN);
|
|
378
401
|
const tileCount = Math.ceil(totalBeats / tileSpan);
|
|
402
|
+
assertGenerationBudget(tileCount * cell.notes.length, 'developed motif notes');
|
|
379
403
|
for (let k = 0; k < tileCount; k += 1) {
|
|
380
404
|
const offset = k * tileSpan;
|
|
381
405
|
for (const n of cell.notes) {
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import type {
|
|
1
|
+
import { createRng } from '../../core/random/index.js';
|
|
2
|
+
import type { KeyScale } from '../../core/types.js';
|
|
3
|
+
import { assertGenerationBudget, assertPositiveInt } from '../../core/validation/index.js';
|
|
4
|
+
import type { ChordQuality, ChordSpan } from '../../theory/chord/index.js';
|
|
5
|
+
import { diatonicTriad } from '../../theory/chord/index.js';
|
|
6
|
+
import { scaleTonesInDegreeOrder } from '../../theory/scale/index.js';
|
|
7
|
+
|
|
8
|
+
export type { ChordSpan } from '../../theory/chord/index.js';
|
|
5
9
|
|
|
6
10
|
/**
|
|
7
11
|
* Broad production style a progression preset suits.
|
|
@@ -30,28 +34,12 @@ export type ProgressionPreset = {
|
|
|
30
34
|
styles: ProgStyle[];
|
|
31
35
|
};
|
|
32
36
|
|
|
33
|
-
/**
|
|
34
|
-
* A chord placed on the timeline by {@link generateProgression}.
|
|
35
|
-
*
|
|
36
|
-
* @category Composition
|
|
37
|
-
*/
|
|
38
|
-
export type GeneratedChord = {
|
|
39
|
-
rootPc: number;
|
|
40
|
-
quality: ChordQuality;
|
|
41
|
-
startBeat: number;
|
|
42
|
-
bassPc?: number;
|
|
43
|
-
/** 0-based scale degree of the chord root, when known. */
|
|
44
|
-
degree?: number;
|
|
45
|
-
/** True when the chord is a secondary dominant tonicizing another degree. */
|
|
46
|
-
secondaryDominant?: boolean;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
37
|
/**
|
|
50
38
|
* Options controlling {@link generateProgression}.
|
|
51
39
|
*
|
|
52
40
|
* @category Composition
|
|
53
41
|
*/
|
|
54
|
-
export type
|
|
42
|
+
export type ProgressionOptions = {
|
|
55
43
|
key: KeyScale;
|
|
56
44
|
style: ProgStyle;
|
|
57
45
|
bars: number;
|
|
@@ -251,18 +239,6 @@ export function progressionsByStyle(style: ProgStyle): ProgressionPreset[] {
|
|
|
251
239
|
return progressions().filter((p) => p.styles.includes(style));
|
|
252
240
|
}
|
|
253
241
|
|
|
254
|
-
/** Deterministic 32-bit PRNG producing a float in [0, 1). */
|
|
255
|
-
function mulberry32(seed: number): () => number {
|
|
256
|
-
let state = seed >>> 0;
|
|
257
|
-
return () => {
|
|
258
|
-
state = (state + 0x6d2b79f5) >>> 0;
|
|
259
|
-
let t = state;
|
|
260
|
-
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
261
|
-
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
262
|
-
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
|
|
266
242
|
/** Root pitch class of a scale degree in the given key, including borrowed degrees. */
|
|
267
243
|
function degreeToRootPc(degree: number, key: KeyScale): number {
|
|
268
244
|
if (degree >= 0 && degree <= 6) {
|
|
@@ -315,12 +291,14 @@ function autoQuality(degree: number, key: KeyScale): ChordQuality {
|
|
|
315
291
|
* ```ts
|
|
316
292
|
* import { generateProgression, majorKey } from '@libraz/libcantus';
|
|
317
293
|
* const chords = generateProgression({ key: majorKey(0), style: 'dance', bars: 4 });
|
|
318
|
-
* // Deterministic for a given seed (defaults to 0); one
|
|
294
|
+
* // Deterministic for a given seed (defaults to 0); one ChordSpan per bar.
|
|
319
295
|
* ```
|
|
320
296
|
*
|
|
321
297
|
* @category Composition
|
|
322
298
|
*/
|
|
323
|
-
export function generateProgression(opts:
|
|
299
|
+
export function generateProgression(opts: ProgressionOptions): ChordSpan[] {
|
|
300
|
+
assertPositiveInt(opts.bars, 'progression bars');
|
|
301
|
+
assertGenerationBudget(opts.bars, 'progression chords');
|
|
324
302
|
const seed = opts.seed ?? 0;
|
|
325
303
|
let preset: ProgressionPreset | undefined;
|
|
326
304
|
if (opts.presetId !== undefined) {
|
|
@@ -332,17 +310,17 @@ export function generateProgression(opts: GenerateProgressionOptions): Generated
|
|
|
332
310
|
if (preset === undefined) {
|
|
333
311
|
const pool = PRESETS.filter((p) => p.styles.includes(opts.style));
|
|
334
312
|
const candidates = pool.length > 0 ? pool : PRESETS;
|
|
335
|
-
const rng =
|
|
336
|
-
const index = Math.floor(rng() * candidates.length) % candidates.length;
|
|
313
|
+
const rng = createRng(seed);
|
|
314
|
+
const index = Math.floor(rng.next() * candidates.length) % candidates.length;
|
|
337
315
|
preset = candidates[index] ?? PRESETS[0];
|
|
338
316
|
}
|
|
339
317
|
const degrees = preset?.degrees ?? [0];
|
|
340
|
-
const chords:
|
|
318
|
+
const chords: ChordSpan[] = [];
|
|
341
319
|
for (let bar = 0; bar < opts.bars; bar += 1) {
|
|
342
320
|
const degree = degrees[bar % degrees.length] ?? 0;
|
|
343
321
|
const quality =
|
|
344
322
|
opts.ext !== undefined && opts.ext !== 'auto' ? opts.ext : autoQuality(degree, opts.key);
|
|
345
|
-
const chord:
|
|
323
|
+
const chord: ChordSpan = {
|
|
346
324
|
rootPc: degreeToRootPc(degree, opts.key),
|
|
347
325
|
quality,
|
|
348
326
|
startBeat: bar * 4,
|
|
@@ -353,7 +331,7 @@ export function generateProgression(opts: GenerateProgressionOptions): Generated
|
|
|
353
331
|
|
|
354
332
|
if (opts.reharmonize) {
|
|
355
333
|
const tonicPc = (((opts.key.rootPc % 12) + 12) % 12) as number;
|
|
356
|
-
const rrng =
|
|
334
|
+
const rrng = createRng((seed ^ 0x9e3779b9) >>> 0);
|
|
357
335
|
for (let i = 0; i < chords.length - 1; i += 1) {
|
|
358
336
|
const cur = chords[i];
|
|
359
337
|
const next = chords[i + 1];
|
|
@@ -369,7 +347,7 @@ export function generateProgression(opts: GenerateProgressionOptions): Generated
|
|
|
369
347
|
if (targetsTonic || alreadySecondary || isResolutionTarget) {
|
|
370
348
|
continue;
|
|
371
349
|
}
|
|
372
|
-
if (rrng() < 0.5) {
|
|
350
|
+
if (rrng.next() < 0.5) {
|
|
373
351
|
chords[i] = {
|
|
374
352
|
rootPc: domRoot,
|
|
375
353
|
quality: 'dom7',
|
|
@@ -7,14 +7,7 @@
|
|
|
7
7
|
* Roots are pitch classes (0..11); no spelled key signature is required.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
type Chord,
|
|
12
|
-
type ChordQuality,
|
|
13
|
-
chordPitchClasses,
|
|
14
|
-
diatonicTriad,
|
|
15
|
-
makeChord,
|
|
16
|
-
} from '../chord/index.js';
|
|
17
|
-
import { detectChordBest } from '../detect/index.js';
|
|
10
|
+
import { detectChordBest } from '../../analyze/detect/index.js';
|
|
18
11
|
import {
|
|
19
12
|
type BorrowedSource,
|
|
20
13
|
borrowedSource,
|
|
@@ -23,9 +16,16 @@ import {
|
|
|
23
16
|
type HarmonicFunction,
|
|
24
17
|
isDiatonic,
|
|
25
18
|
parallelKey,
|
|
26
|
-
} from '
|
|
27
|
-
import {
|
|
28
|
-
import
|
|
19
|
+
} from '../../analyze/functional/index.js';
|
|
20
|
+
import type { KeyScale } from '../../core/types.js';
|
|
21
|
+
import {
|
|
22
|
+
type Chord,
|
|
23
|
+
type ChordQuality,
|
|
24
|
+
chordPitchClasses,
|
|
25
|
+
diatonicTriad,
|
|
26
|
+
makeChord,
|
|
27
|
+
} from '../../theory/chord/index.js';
|
|
28
|
+
import { scaleTonesInDegreeOrder } from '../../theory/scale/index.js';
|
|
29
29
|
|
|
30
30
|
/** Reduce a value to a pitch class in [0, 11]. */
|
|
31
31
|
function mod12(n: number): number {
|
|
@@ -91,13 +91,6 @@ function isStableTarget(chord: Chord): boolean {
|
|
|
91
91
|
return hasFifth && hasThird && !isDominantType(chord);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
/** The triad pitch classes (root, third, fifth) implied by a chord's template. */
|
|
95
|
-
function triadPitchClasses(chord: Chord): number[] {
|
|
96
|
-
const third = hasInterval(chord, 4) ? 4 : hasInterval(chord, 3) ? 3 : 4;
|
|
97
|
-
const fifth = hasInterval(chord, 8) ? 8 : hasInterval(chord, 6) ? 6 : 7;
|
|
98
|
-
return [mod12(chord.rootPc), mod12(chord.rootPc + third), mod12(chord.rootPc + fifth)];
|
|
99
|
-
}
|
|
100
|
-
|
|
101
94
|
/** Count the pitch classes shared between two pitch-class sets. */
|
|
102
95
|
function commonToneCount(a: number[], b: number[]): number {
|
|
103
96
|
const set = new Set(b);
|
|
@@ -157,7 +150,7 @@ export function substituteChord(
|
|
|
157
150
|
key: KeyScale,
|
|
158
151
|
opts?: SubstituteOptions,
|
|
159
152
|
): Substitution[] {
|
|
160
|
-
const original =
|
|
153
|
+
const original = chordPitchClasses(chord);
|
|
161
154
|
const candidates: { chord: Chord; type: SubstitutionType }[] = [];
|
|
162
155
|
|
|
163
156
|
// Tritone substitution: only for dominant-type chords.
|
|
@@ -11,8 +11,13 @@
|
|
|
11
11
|
* library-wide convention.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { beatsPerBar, metricWeight, type TimeSignature } from '
|
|
15
|
-
import { createRng } from '
|
|
14
|
+
import { beatsPerBar, metricWeight, type TimeSignature } from '../../core/meter/index.js';
|
|
15
|
+
import { createRng } from '../../core/random/index.js';
|
|
16
|
+
import {
|
|
17
|
+
assertFiniteNumber,
|
|
18
|
+
assertGenerationBudget,
|
|
19
|
+
assertPositiveInt,
|
|
20
|
+
} from '../../core/validation/index.js';
|
|
16
21
|
|
|
17
22
|
/**
|
|
18
23
|
* A single rhythmic note: an onset position and how long it sounds.
|
|
@@ -113,12 +118,17 @@ export function generateRhythm(ts: TimeSignature, opts: RhythmOptions = {}): Rhy
|
|
|
113
118
|
const seed = opts.seed ?? 0;
|
|
114
119
|
const bars = opts.bars ?? DEFAULT_BARS;
|
|
115
120
|
const subdivision = opts.subdivision ?? DEFAULT_SUBDIVISION;
|
|
116
|
-
const
|
|
121
|
+
const requestedDensity = assertFiniteNumber(opts.density ?? DEFAULT_DENSITY, 'rhythm density');
|
|
122
|
+
const density = Math.min(1, Math.max(0, requestedDensity));
|
|
123
|
+
|
|
124
|
+
assertPositiveInt(bars, 'rhythm bars');
|
|
125
|
+
assertPositiveInt(subdivision, 'rhythm subdivision');
|
|
117
126
|
|
|
118
127
|
const barBeats = beatsPerBar(ts);
|
|
119
128
|
const spanBeats = barBeats * bars;
|
|
120
129
|
const step = 1 / subdivision;
|
|
121
130
|
const slotCount = Math.round(spanBeats * subdivision);
|
|
131
|
+
assertGenerationBudget(slotCount, 'rhythm grid slots');
|
|
122
132
|
const slotsPerBar = Math.round(barBeats * subdivision);
|
|
123
133
|
const rng = createRng(seed);
|
|
124
134
|
|