@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
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { isDiatonic, parallelKey } from '../../analyze/functional/index.js';
|
|
2
|
+
import { createNoteEventIndex } from '../../core/event-index/index.js';
|
|
1
3
|
import type { TimeSignature } from '../../core/meter/index.js';
|
|
2
4
|
import { isStrongBeat } from '../../core/meter/index.js';
|
|
5
|
+
import { createRng } from '../../core/random/index.js';
|
|
3
6
|
import type { KeyScale, NoteEvent } from '../../core/types.js';
|
|
7
|
+
import { assertGenerationBudget, assertRange } from '../../core/validation/index.js';
|
|
4
8
|
import type { Chord, ChordQuality } from '../../theory/chord/index.js';
|
|
5
9
|
import { chordPitchClasses, diatonicTriad, makeChord } from '../../theory/chord/index.js';
|
|
6
10
|
import type { HarmonyRole } from '../../theory/harmony/index.js';
|
|
@@ -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
|
/**
|
|
@@ -361,24 +356,40 @@ function harmonizeOnce(
|
|
|
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];
|
package/src/generate/index.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { ChordTimeline } from '../../analyze/timeline/index.js';
|
|
2
|
+
import { createRng } from '../../core/random/index.js';
|
|
2
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';
|
|
3
11
|
import type { Chord } from '../../theory/chord/index.js';
|
|
4
12
|
import { chordPitchClasses } from '../../theory/chord/index.js';
|
|
5
13
|
import { isScaleTone, nearestScaleTone } from '../../theory/scale/index.js';
|
|
@@ -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[] = [];
|
|
@@ -209,12 +218,14 @@ function contourOffsets(contour: MotifContour, count: number): number[] {
|
|
|
209
218
|
*/
|
|
210
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: MotifOptions): 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,4 +1,6 @@
|
|
|
1
|
+
import { createRng } from '../../core/random/index.js';
|
|
1
2
|
import type { KeyScale } from '../../core/types.js';
|
|
3
|
+
import { assertGenerationBudget, assertPositiveInt } from '../../core/validation/index.js';
|
|
2
4
|
import type { ChordQuality, ChordSpan } from '../../theory/chord/index.js';
|
|
3
5
|
import { diatonicTriad } from '../../theory/chord/index.js';
|
|
4
6
|
import { scaleTonesInDegreeOrder } from '../../theory/scale/index.js';
|
|
@@ -237,18 +239,6 @@ export function progressionsByStyle(style: ProgStyle): ProgressionPreset[] {
|
|
|
237
239
|
return progressions().filter((p) => p.styles.includes(style));
|
|
238
240
|
}
|
|
239
241
|
|
|
240
|
-
/** Deterministic 32-bit PRNG producing a float in [0, 1). */
|
|
241
|
-
function mulberry32(seed: number): () => number {
|
|
242
|
-
let state = seed >>> 0;
|
|
243
|
-
return () => {
|
|
244
|
-
state = (state + 0x6d2b79f5) >>> 0;
|
|
245
|
-
let t = state;
|
|
246
|
-
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
247
|
-
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
248
|
-
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
|
|
252
242
|
/** Root pitch class of a scale degree in the given key, including borrowed degrees. */
|
|
253
243
|
function degreeToRootPc(degree: number, key: KeyScale): number {
|
|
254
244
|
if (degree >= 0 && degree <= 6) {
|
|
@@ -307,6 +297,8 @@ function autoQuality(degree: number, key: KeyScale): ChordQuality {
|
|
|
307
297
|
* @category Composition
|
|
308
298
|
*/
|
|
309
299
|
export function generateProgression(opts: ProgressionOptions): ChordSpan[] {
|
|
300
|
+
assertPositiveInt(opts.bars, 'progression bars');
|
|
301
|
+
assertGenerationBudget(opts.bars, 'progression chords');
|
|
310
302
|
const seed = opts.seed ?? 0;
|
|
311
303
|
let preset: ProgressionPreset | undefined;
|
|
312
304
|
if (opts.presetId !== undefined) {
|
|
@@ -318,8 +310,8 @@ export function generateProgression(opts: ProgressionOptions): ChordSpan[] {
|
|
|
318
310
|
if (preset === undefined) {
|
|
319
311
|
const pool = PRESETS.filter((p) => p.styles.includes(opts.style));
|
|
320
312
|
const candidates = pool.length > 0 ? pool : PRESETS;
|
|
321
|
-
const rng =
|
|
322
|
-
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;
|
|
323
315
|
preset = candidates[index] ?? PRESETS[0];
|
|
324
316
|
}
|
|
325
317
|
const degrees = preset?.degrees ?? [0];
|
|
@@ -339,7 +331,7 @@ export function generateProgression(opts: ProgressionOptions): ChordSpan[] {
|
|
|
339
331
|
|
|
340
332
|
if (opts.reharmonize) {
|
|
341
333
|
const tonicPc = (((opts.key.rootPc % 12) + 12) % 12) as number;
|
|
342
|
-
const rrng =
|
|
334
|
+
const rrng = createRng((seed ^ 0x9e3779b9) >>> 0);
|
|
343
335
|
for (let i = 0; i < chords.length - 1; i += 1) {
|
|
344
336
|
const cur = chords[i];
|
|
345
337
|
const next = chords[i + 1];
|
|
@@ -355,7 +347,7 @@ export function generateProgression(opts: ProgressionOptions): ChordSpan[] {
|
|
|
355
347
|
if (targetsTonic || alreadySecondary || isResolutionTarget) {
|
|
356
348
|
continue;
|
|
357
349
|
}
|
|
358
|
-
if (rrng() < 0.5) {
|
|
350
|
+
if (rrng.next() < 0.5) {
|
|
359
351
|
chords[i] = {
|
|
360
352
|
rootPc: domRoot,
|
|
361
353
|
quality: 'dom7',
|
|
@@ -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.
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
|
|
14
14
|
import { beatsPerBar, metricWeight, type TimeSignature } from '../../core/meter/index.js';
|
|
15
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
|
|
package/src/index.ts
CHANGED
|
@@ -1,247 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export
|
|
11
|
-
export
|
|
12
|
-
export
|
|
13
|
-
export
|
|
14
|
-
BorrowedSource,
|
|
15
|
-
Cadence,
|
|
16
|
-
ChordAnalysis,
|
|
17
|
-
HarmonicFunction,
|
|
18
|
-
} from './analyze/functional/index.js';
|
|
19
|
-
export {
|
|
20
|
-
analyzeChord,
|
|
21
|
-
borrowedSource,
|
|
22
|
-
chordToRoman,
|
|
23
|
-
detectCadence,
|
|
24
|
-
functionOf,
|
|
25
|
-
isBorrowedChord,
|
|
26
|
-
isDiatonic,
|
|
27
|
-
isMinorKey,
|
|
28
|
-
parallelKey,
|
|
29
|
-
romanToChord,
|
|
30
|
-
secondaryDominant,
|
|
31
|
-
} from './analyze/functional/index.js';
|
|
32
|
-
export type {
|
|
33
|
-
CadenceHit,
|
|
34
|
-
ChordSegment,
|
|
35
|
-
ChordTimeline,
|
|
36
|
-
ChordTimelineOptions,
|
|
37
|
-
ChordTimelineResult,
|
|
38
|
-
} from './analyze/timeline/index.js';
|
|
39
|
-
export {
|
|
40
|
-
chordTimelineFromChords,
|
|
41
|
-
chordTimelineFromNotes,
|
|
42
|
-
detectCadences,
|
|
43
|
-
} from './analyze/timeline/index.js';
|
|
44
|
-
export type { AnalyzedNote, TheoryLabel, VoiceNote } from './analyze/voice/index.js';
|
|
45
|
-
export { analyzeVoice } from './analyze/voice/index.js';
|
|
46
|
-
export {
|
|
47
|
-
classifyInterval,
|
|
48
|
-
IntervalQuality,
|
|
49
|
-
isConsonantInterval,
|
|
50
|
-
isPerfectInterval,
|
|
51
|
-
} from './core/interval/index.js';
|
|
52
|
-
export type { BarPosition, TimeSignature } from './core/meter/index.js';
|
|
53
|
-
export {
|
|
54
|
-
barPositionToBeat,
|
|
55
|
-
beatsPerBar,
|
|
56
|
-
beatToBarPosition,
|
|
57
|
-
formatTimeSignature,
|
|
58
|
-
isCompound,
|
|
59
|
-
isStrongBeat,
|
|
60
|
-
metricWeight,
|
|
61
|
-
parseTimeSignature,
|
|
62
|
-
pulsesPerBar,
|
|
63
|
-
tuplet,
|
|
64
|
-
} from './core/meter/index.js';
|
|
65
|
-
export type { Note as NoteData, SpelledInterval } from './core/pitch/index.js';
|
|
66
|
-
export {
|
|
67
|
-
formatNote,
|
|
68
|
-
midiToNote,
|
|
69
|
-
noteToMidi,
|
|
70
|
-
noteToPitchClass,
|
|
71
|
-
parseNote,
|
|
72
|
-
spelledInterval,
|
|
73
|
-
} from './core/pitch/index.js';
|
|
74
|
-
export type { Rng } from './core/random/index.js';
|
|
75
|
-
export { createRng } from './core/random/index.js';
|
|
76
|
-
export type { Tuning } from './core/tuning/index.js';
|
|
77
|
-
export {
|
|
78
|
-
centsBetweenFreq,
|
|
79
|
-
centsOfSteps,
|
|
80
|
-
edo,
|
|
81
|
-
frequencyOf,
|
|
82
|
-
JUST_RATIOS,
|
|
83
|
-
justDeviationCents,
|
|
84
|
-
nearestStep,
|
|
85
|
-
ratioToCents,
|
|
86
|
-
TWELVE_TET,
|
|
87
|
-
} from './core/tuning/index.js';
|
|
88
|
-
export type { KeyScale, NoteEvent } from './core/types.js';
|
|
89
|
-
export type { BassLineOptions, BassSegment, BassStyle } from './generate/bass/index.js';
|
|
90
|
-
export { generateBassLine } from './generate/bass/index.js';
|
|
91
|
-
export type { CounterMelodyOptions } from './generate/countermelody/index.js';
|
|
92
|
-
export { generateCounterMelody } from './generate/countermelody/index.js';
|
|
93
|
-
export type {
|
|
94
|
-
DrumHit,
|
|
95
|
-
DrumRole,
|
|
96
|
-
DrumsOptions,
|
|
97
|
-
EuclideanKick,
|
|
98
|
-
GrooveFeel,
|
|
99
|
-
GrooveStyle,
|
|
100
|
-
Section,
|
|
101
|
-
} from './generate/drums/index.js';
|
|
102
|
-
export { generateDrums } from './generate/drums/index.js';
|
|
103
|
-
export type { GrooveSlot, GrooveTemplate, HumanizeOptions } from './generate/groove/index.js';
|
|
104
|
-
export { applyGrooveTemplate, extractGrooveTemplate, humanize } from './generate/groove/index.js';
|
|
105
|
-
export type {
|
|
106
|
-
HarmonizeOptions,
|
|
107
|
-
HarmonizeResult,
|
|
108
|
-
MelodyNote,
|
|
109
|
-
} from './generate/harmonize/index.js';
|
|
110
|
-
export { harmonizeMelody } from './generate/harmonize/index.js';
|
|
111
|
-
export type {
|
|
112
|
-
MotifCell,
|
|
113
|
-
MotifContour,
|
|
114
|
-
MotifNote,
|
|
115
|
-
MotifOptions,
|
|
116
|
-
MotifTransform,
|
|
117
|
-
} from './generate/motif/index.js';
|
|
118
|
-
export { developMotif, generateMotif, transformMotif } from './generate/motif/index.js';
|
|
119
|
-
export type {
|
|
120
|
-
ChordSpan,
|
|
121
|
-
ProgFunction,
|
|
122
|
-
ProgressionOptions,
|
|
123
|
-
ProgressionPreset,
|
|
124
|
-
ProgStyle,
|
|
125
|
-
} from './generate/progression/index.js';
|
|
126
|
-
export {
|
|
127
|
-
generateProgression,
|
|
128
|
-
progressions,
|
|
129
|
-
progressionsByStyle,
|
|
130
|
-
} from './generate/progression/index.js';
|
|
131
|
-
export type {
|
|
132
|
-
BorrowedChord,
|
|
133
|
-
SubstituteOptions,
|
|
134
|
-
Substitution,
|
|
135
|
-
SubstitutionType,
|
|
136
|
-
} from './generate/reharmony/index.js';
|
|
137
|
-
export {
|
|
138
|
-
modalInterchangePalette,
|
|
139
|
-
negativeHarmonyMirror,
|
|
140
|
-
substituteChord,
|
|
141
|
-
} from './generate/reharmony/index.js';
|
|
142
|
-
export type { RhythmEvent, RhythmOptions } from './generate/rhythm/index.js';
|
|
143
|
-
export { generateRhythm, onsetWeightCurve, rhythmDensity } from './generate/rhythm/index.js';
|
|
144
|
-
export { Chord, Interval, Key, Note, Progression } from './model/index.js';
|
|
145
|
-
export type { Chord as ChordData, ChordQuality } from './theory/chord/index.js';
|
|
146
|
-
export {
|
|
147
|
-
chordFromDegree,
|
|
148
|
-
chordPitchClasses,
|
|
149
|
-
chordQualities,
|
|
150
|
-
chordToneRole,
|
|
151
|
-
diatonicSeventh,
|
|
152
|
-
diatonicTriad,
|
|
153
|
-
makeChord,
|
|
154
|
-
} from './theory/chord/index.js';
|
|
155
|
-
export type {
|
|
156
|
-
ChordScaleMatch,
|
|
157
|
-
ChordScaleReportEntry,
|
|
158
|
-
ScaleChoice,
|
|
159
|
-
} from './theory/chordscale/index.js';
|
|
160
|
-
export {
|
|
161
|
-
availableTensions,
|
|
162
|
-
avoidNotes,
|
|
163
|
-
chordScaleReport,
|
|
164
|
-
chordScales,
|
|
165
|
-
scaleMatchesChord,
|
|
166
|
-
scalesForChanges,
|
|
167
|
-
} from './theory/chordscale/index.js';
|
|
168
|
-
export {
|
|
169
|
-
createsHiddenParallelPerfect,
|
|
170
|
-
createsParallelOctave,
|
|
171
|
-
createsParallelPerfect,
|
|
172
|
-
createsParallelUnison,
|
|
173
|
-
createsVerticalDissonance,
|
|
174
|
-
createsVoiceCrossing,
|
|
175
|
-
createsVoiceOverlap,
|
|
176
|
-
exceedsSpacing,
|
|
177
|
-
isForbiddenMelodicLeap,
|
|
178
|
-
isLeadingToneResolution,
|
|
179
|
-
} from './theory/counterpoint/index.js';
|
|
180
|
-
export type { HarmonyRole, LockLevel, VoicedRole } from './theory/harmony/index.js';
|
|
181
|
-
export { roleOf } from './theory/harmony/index.js';
|
|
182
|
-
export type {
|
|
183
|
-
SafetyProfile,
|
|
184
|
-
SafetyQuery,
|
|
185
|
-
SafetyResult,
|
|
186
|
-
VoiceSnapshot,
|
|
187
|
-
} from './theory/safety/index.js';
|
|
188
|
-
export {
|
|
189
|
-
enumerateSafePitches,
|
|
190
|
-
evaluateSafety,
|
|
191
|
-
NoteSafety,
|
|
192
|
-
ReasonFlag,
|
|
193
|
-
} from './theory/safety/index.js';
|
|
194
|
-
export {
|
|
195
|
-
BLUES_MASK,
|
|
196
|
-
CHROMATIC_MASK,
|
|
197
|
-
DORIAN_MASK,
|
|
198
|
-
diatonicPitchClasses,
|
|
199
|
-
HARMONIC_MINOR_MASK,
|
|
200
|
-
isScaleTone,
|
|
201
|
-
LOCRIAN_MASK,
|
|
202
|
-
LYDIAN_MASK,
|
|
203
|
-
MAJOR_MASK,
|
|
204
|
-
MAJOR_PENTATONIC_MASK,
|
|
205
|
-
MELODIC_MINOR_MASK,
|
|
206
|
-
MINOR_PENTATONIC_MASK,
|
|
207
|
-
MIXOLYDIAN_MASK,
|
|
208
|
-
majorKey,
|
|
209
|
-
maskFromOffsets,
|
|
210
|
-
minorKey,
|
|
211
|
-
NAMED_SCALES,
|
|
212
|
-
NATURAL_MINOR_MASK,
|
|
213
|
-
nearestScaleTone,
|
|
214
|
-
OCTATONIC_HALF_WHOLE_MASK,
|
|
215
|
-
OCTATONIC_WHOLE_HALF_MASK,
|
|
216
|
-
PHRYGIAN_MASK,
|
|
217
|
-
pitchToScaleDegree,
|
|
218
|
-
scaleByName,
|
|
219
|
-
scaleTonesInDegreeOrder,
|
|
220
|
-
WHOLE_TONE_MASK,
|
|
221
|
-
} from './theory/scale/index.js';
|
|
222
|
-
export {
|
|
223
|
-
noteNames,
|
|
224
|
-
spellChord,
|
|
225
|
-
spellPitchClass,
|
|
226
|
-
spellPitchClasses,
|
|
227
|
-
spellScale,
|
|
228
|
-
} from './theory/spelling/index.js';
|
|
229
|
-
export {
|
|
230
|
-
formatChordSymbol,
|
|
231
|
-
parseChordSymbol,
|
|
232
|
-
transposeChordSymbol,
|
|
233
|
-
} from './theory/symbol/index.js';
|
|
234
|
-
export type {
|
|
235
|
-
StyledVoicingOptions,
|
|
236
|
-
VoiceRange,
|
|
237
|
-
VoicingOptions,
|
|
238
|
-
VoicingStyle,
|
|
239
|
-
} from './theory/voicing/index.js';
|
|
240
|
-
export {
|
|
241
|
-
nextVoicing,
|
|
242
|
-
SATB_RANGES,
|
|
243
|
-
voiceChord,
|
|
244
|
-
voiceChordStyled,
|
|
245
|
-
voiceLeadingCost,
|
|
246
|
-
voiceProgression,
|
|
247
|
-
} from './theory/voicing/index.js';
|
|
1
|
+
/**
|
|
2
|
+
* Complete public API.
|
|
3
|
+
*
|
|
4
|
+
* Each layer barrel is the canonical source of that layer's runtime and type
|
|
5
|
+
* exports. The package root composes those barrels so a symbol added to a
|
|
6
|
+
* public layer cannot silently disappear from the root entry point.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export * from './analyze/index.js';
|
|
10
|
+
export * from './core/index.js';
|
|
11
|
+
export * from './generate/index.js';
|
|
12
|
+
export * from './model/index.js';
|
|
13
|
+
export * from './theory/index.js';
|