@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
package/dist/generate/index.cjs
CHANGED
|
@@ -42,6 +42,86 @@ __export(generate_exports, {
|
|
|
42
42
|
});
|
|
43
43
|
module.exports = __toCommonJS(generate_exports);
|
|
44
44
|
|
|
45
|
+
// src/core/validation/index.ts
|
|
46
|
+
var DEFAULT_GENERATION_BUDGET = 1e6;
|
|
47
|
+
function assertFiniteNumber(value, name) {
|
|
48
|
+
if (!Number.isFinite(value)) {
|
|
49
|
+
throw new RangeError(`${name} must be finite; received ${value}`);
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
function assertInteger(value, name, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) {
|
|
54
|
+
assertFiniteNumber(value, name);
|
|
55
|
+
if (!Number.isSafeInteger(value) || value < min || value > max) {
|
|
56
|
+
throw new RangeError(`${name} must be an integer in [${min}, ${max}]; received ${value}`);
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
function assertPositiveInt(value, name, max = DEFAULT_GENERATION_BUDGET) {
|
|
61
|
+
return assertInteger(value, name, 1, max);
|
|
62
|
+
}
|
|
63
|
+
function assertRange(value, min, max, name) {
|
|
64
|
+
assertFiniteNumber(value, name);
|
|
65
|
+
if (value < min || value > max) {
|
|
66
|
+
throw new RangeError(`${name} must be in [${min}, ${max}]; received ${value}`);
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
function assertGenerationBudget(estimated, name, limit = DEFAULT_GENERATION_BUDGET) {
|
|
71
|
+
assertFiniteNumber(estimated, name);
|
|
72
|
+
assertPositiveInt(limit, `${name} limit`, Number.MAX_SAFE_INTEGER);
|
|
73
|
+
if (estimated < 0 || estimated > limit) {
|
|
74
|
+
throw new RangeError(`${name} exceeds the generation budget ${limit}; received ${estimated}`);
|
|
75
|
+
}
|
|
76
|
+
return estimated;
|
|
77
|
+
}
|
|
78
|
+
function assertTimeSignature(ts, name = "time signature") {
|
|
79
|
+
assertPositiveInt(ts.numerator, `${name}.numerator`);
|
|
80
|
+
assertPositiveInt(ts.denominator, `${name}.denominator`);
|
|
81
|
+
if (ts.grouping !== void 0) {
|
|
82
|
+
if (ts.grouping.length === 0) {
|
|
83
|
+
throw new RangeError(`${name}.grouping must not be empty`);
|
|
84
|
+
}
|
|
85
|
+
let sum = 0;
|
|
86
|
+
for (let index = 0; index < ts.grouping.length; index += 1) {
|
|
87
|
+
sum += assertPositiveInt(ts.grouping[index] ?? Number.NaN, `${name}.grouping[${index}]`);
|
|
88
|
+
}
|
|
89
|
+
const compound = ts.numerator % 3 === 0 && ts.numerator > 3;
|
|
90
|
+
const pulses = compound ? ts.numerator / 3 : ts.numerator;
|
|
91
|
+
if (sum !== pulses) {
|
|
92
|
+
throw new RangeError(`${name}.grouping must sum to ${pulses}; received ${sum}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return ts;
|
|
96
|
+
}
|
|
97
|
+
function assertNoteEvent(event, name = "note event", options = {}) {
|
|
98
|
+
assertFiniteNumber(event.pitch, `${name}.pitch`);
|
|
99
|
+
assertRange(event.startBeat, 0, Number.MAX_SAFE_INTEGER, `${name}.startBeat`);
|
|
100
|
+
assertRange(
|
|
101
|
+
event.durationBeat,
|
|
102
|
+
options.allowNonPositiveDuration ? -Number.MAX_SAFE_INTEGER : Number.MIN_VALUE,
|
|
103
|
+
Number.MAX_SAFE_INTEGER,
|
|
104
|
+
`${name}.durationBeat`
|
|
105
|
+
);
|
|
106
|
+
if (!options.allowNonPositiveDuration && event.durationBeat <= 0) {
|
|
107
|
+
throw new RangeError(`${name}.durationBeat must be positive; received ${event.durationBeat}`);
|
|
108
|
+
}
|
|
109
|
+
if (event.velocity !== void 0) {
|
|
110
|
+
assertRange(event.velocity, 0, 127, `${name}.velocity`);
|
|
111
|
+
}
|
|
112
|
+
return event;
|
|
113
|
+
}
|
|
114
|
+
function assertNoteEvents(events, name = "note events", options = {}) {
|
|
115
|
+
assertGenerationBudget(events.length, `${name} count`, options.budget);
|
|
116
|
+
for (let index = 0; index < events.length; index += 1) {
|
|
117
|
+
const event = events[index];
|
|
118
|
+
if (event !== void 0) {
|
|
119
|
+
assertNoteEvent(event, `${name}[${index}]`, options);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return events;
|
|
123
|
+
}
|
|
124
|
+
|
|
45
125
|
// src/core/meter/index.ts
|
|
46
126
|
var EPS = 1e-9;
|
|
47
127
|
function isMultiple(value, unit) {
|
|
@@ -52,15 +132,18 @@ function isMultiple(value, unit) {
|
|
|
52
132
|
return Math.abs(ratio - Math.round(ratio)) < EPS;
|
|
53
133
|
}
|
|
54
134
|
function formatTimeSignature(ts) {
|
|
135
|
+
assertTimeSignature(ts);
|
|
55
136
|
return `${ts.numerator}/${ts.denominator}`;
|
|
56
137
|
}
|
|
57
138
|
function isCompound(ts) {
|
|
139
|
+
assertTimeSignature(ts);
|
|
58
140
|
return ts.numerator % 3 === 0 && ts.numerator > 3;
|
|
59
141
|
}
|
|
60
142
|
function unitBeats(ts) {
|
|
61
143
|
return 4 / ts.denominator;
|
|
62
144
|
}
|
|
63
145
|
function beatsPerBar(ts) {
|
|
146
|
+
assertTimeSignature(ts);
|
|
64
147
|
return ts.numerator * unitBeats(ts);
|
|
65
148
|
}
|
|
66
149
|
function pulseBeats(ts) {
|
|
@@ -93,11 +176,14 @@ function isGroupHead(grouping, pulseIndex) {
|
|
|
93
176
|
return false;
|
|
94
177
|
}
|
|
95
178
|
function beatToBarPosition(beatInQuarters, ts) {
|
|
179
|
+
assertFiniteNumber(beatInQuarters, "beat");
|
|
96
180
|
const barLen = beatsPerBar(ts);
|
|
97
181
|
const bar = Math.floor(beatInQuarters / barLen);
|
|
98
182
|
return { bar, beat: beatInQuarters - bar * barLen };
|
|
99
183
|
}
|
|
100
184
|
function metricWeight(beatInQuarters, ts) {
|
|
185
|
+
assertFiniteNumber(beatInQuarters, "beat");
|
|
186
|
+
assertTimeSignature(ts);
|
|
101
187
|
const { beat } = beatToBarPosition(beatInQuarters, ts);
|
|
102
188
|
const pulse = pulseBeats(ts);
|
|
103
189
|
if (!isMultiple(beat, pulse)) {
|
|
@@ -128,6 +214,7 @@ function isStrongBeat(beatInQuarters, ts) {
|
|
|
128
214
|
|
|
129
215
|
// src/core/random/index.ts
|
|
130
216
|
function createRng(seed) {
|
|
217
|
+
assertFiniteNumber(seed, "seed");
|
|
131
218
|
let state = seed >>> 0;
|
|
132
219
|
const next = () => {
|
|
133
220
|
state = state + 1831565813 >>> 0;
|
|
@@ -138,9 +225,29 @@ function createRng(seed) {
|
|
|
138
225
|
};
|
|
139
226
|
return {
|
|
140
227
|
next,
|
|
141
|
-
prob: (p) => next() < p,
|
|
142
|
-
range: (lo, hi) =>
|
|
143
|
-
|
|
228
|
+
prob: (p) => next() < assertRange(p, 0, 1, "probability"),
|
|
229
|
+
range: (lo, hi) => {
|
|
230
|
+
assertInteger(lo, "range lower bound");
|
|
231
|
+
assertInteger(hi, "range upper bound");
|
|
232
|
+
if (lo > hi) {
|
|
233
|
+
throw new RangeError(
|
|
234
|
+
`range lower bound must not exceed upper bound; received ${lo} > ${hi}`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
assertInteger(hi - lo, "range span", 0);
|
|
238
|
+
return lo + Math.floor(next() * (hi - lo + 1));
|
|
239
|
+
},
|
|
240
|
+
float: (lo, hi) => {
|
|
241
|
+
assertFiniteNumber(lo, "float lower bound");
|
|
242
|
+
assertFiniteNumber(hi, "float upper bound");
|
|
243
|
+
if (lo > hi) {
|
|
244
|
+
throw new RangeError(
|
|
245
|
+
`float lower bound must not exceed upper bound; received ${lo} > ${hi}`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
assertFiniteNumber(hi - lo, "float span");
|
|
249
|
+
return lo + next() * (hi - lo);
|
|
250
|
+
}
|
|
144
251
|
};
|
|
145
252
|
}
|
|
146
253
|
|
|
@@ -340,13 +447,30 @@ function buildWalking(ctx, seg, next) {
|
|
|
340
447
|
});
|
|
341
448
|
}
|
|
342
449
|
function generateBassLine(opts) {
|
|
450
|
+
assertGenerationBudget(opts.segments.length, "bass segments");
|
|
451
|
+
for (let index = 0; index < opts.segments.length; index += 1) {
|
|
452
|
+
const segment = opts.segments[index];
|
|
453
|
+
if (!segment) continue;
|
|
454
|
+
assertRange(segment.startBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].startBeat`);
|
|
455
|
+
assertRange(segment.endBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].endBeat`);
|
|
456
|
+
if (segment.endBeat <= segment.startBeat) {
|
|
457
|
+
throw new RangeError(`segments[${index}] must have a positive duration`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
343
460
|
const segments = [...opts.segments].sort((a, b) => a.startBeat - b.startBeat);
|
|
344
461
|
if (segments.length === 0) {
|
|
345
462
|
return [];
|
|
346
463
|
}
|
|
347
464
|
const ts = opts.ts ?? DEFAULT_TS;
|
|
465
|
+
assertTimeSignature(ts);
|
|
348
466
|
const style = opts.style ?? DEFAULT_STYLE;
|
|
349
467
|
const octave = opts.octave ?? DEFAULT_OCTAVE;
|
|
468
|
+
assertInteger(octave, "bass octave", -1, 9);
|
|
469
|
+
const estimatedNotes = segments.reduce(
|
|
470
|
+
(count, segment) => count + Math.max(1, Math.ceil(segment.endBeat - segment.startBeat)),
|
|
471
|
+
0
|
|
472
|
+
);
|
|
473
|
+
assertGenerationBudget(estimatedNotes, "bass notes");
|
|
350
474
|
const low = octave * 12 + 12;
|
|
351
475
|
const ctx = {
|
|
352
476
|
ts,
|
|
@@ -402,6 +526,73 @@ function generateBassLine(opts) {
|
|
|
402
526
|
return out;
|
|
403
527
|
}
|
|
404
528
|
|
|
529
|
+
// src/core/event-index/index.ts
|
|
530
|
+
var EPS3 = 1e-9;
|
|
531
|
+
function upperBound(values, beat) {
|
|
532
|
+
let low = 0;
|
|
533
|
+
let high = values.length;
|
|
534
|
+
while (low < high) {
|
|
535
|
+
const middle = Math.floor((low + high) / 2);
|
|
536
|
+
if ((values[middle]?.note.startBeat ?? Number.POSITIVE_INFINITY) <= beat) {
|
|
537
|
+
low = middle + 1;
|
|
538
|
+
} else {
|
|
539
|
+
high = middle;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return low;
|
|
543
|
+
}
|
|
544
|
+
function createNoteEventIndex(events, options = {}) {
|
|
545
|
+
assertNoteEvents(events, "note events", options);
|
|
546
|
+
const notes = events.map((note, originalIndex) => ({
|
|
547
|
+
note,
|
|
548
|
+
originalIndex,
|
|
549
|
+
endBeat: note.startBeat + note.durationBeat
|
|
550
|
+
})).sort((a, b) => a.note.startBeat - b.note.startBeat || a.originalIndex - b.originalIndex);
|
|
551
|
+
const prefixMaxEnd = [];
|
|
552
|
+
let maxEnd = Number.NEGATIVE_INFINITY;
|
|
553
|
+
for (const indexed of notes) {
|
|
554
|
+
maxEnd = Math.max(maxEnd, indexed.endBeat);
|
|
555
|
+
prefixMaxEnd.push(maxEnd);
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
notes,
|
|
559
|
+
at(beat) {
|
|
560
|
+
let index = upperBound(notes, beat + EPS3) - 1;
|
|
561
|
+
while (index >= 0) {
|
|
562
|
+
if ((prefixMaxEnd[index] ?? Number.NEGATIVE_INFINITY) <= beat + EPS3) {
|
|
563
|
+
return void 0;
|
|
564
|
+
}
|
|
565
|
+
const indexed = notes[index];
|
|
566
|
+
if (indexed && indexed.note.durationBeat > 0 && indexed.note.startBeat - EPS3 <= beat && beat < indexed.endBeat - EPS3) {
|
|
567
|
+
return indexed;
|
|
568
|
+
}
|
|
569
|
+
index -= 1;
|
|
570
|
+
}
|
|
571
|
+
return void 0;
|
|
572
|
+
},
|
|
573
|
+
attacksAt(beat) {
|
|
574
|
+
const firstAfter = upperBound(notes, beat + EPS3);
|
|
575
|
+
const candidate = notes[firstAfter - 1];
|
|
576
|
+
return candidate !== void 0 && Math.abs(candidate.note.startBeat - beat) < EPS3;
|
|
577
|
+
},
|
|
578
|
+
onsetsBetween(startBeat, endBeat) {
|
|
579
|
+
const result = [];
|
|
580
|
+
let index = upperBound(notes, startBeat + EPS3);
|
|
581
|
+
while (index < notes.length) {
|
|
582
|
+
const onset = notes[index]?.note.startBeat;
|
|
583
|
+
if (onset === void 0 || onset >= endBeat - EPS3) {
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
if (result[result.length - 1] !== onset) {
|
|
587
|
+
result.push(onset);
|
|
588
|
+
}
|
|
589
|
+
index += 1;
|
|
590
|
+
}
|
|
591
|
+
return result;
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
405
596
|
// src/theory/chord/index.ts
|
|
406
597
|
var QUALITY_INTERVALS = {
|
|
407
598
|
maj: [0, 4, 7],
|
|
@@ -538,8 +729,10 @@ function classifySeventh(thirdIc, fifthIc, seventhIc) {
|
|
|
538
729
|
function stackThirds(degree, key, size) {
|
|
539
730
|
const tones = scaleTonesInDegreeOrder(key);
|
|
540
731
|
const length = tones.length;
|
|
541
|
-
if (length
|
|
542
|
-
|
|
732
|
+
if (length !== 7) {
|
|
733
|
+
throw new Error(
|
|
734
|
+
`diatonic chord stacking requires a heptatonic scale (received ${length} tones)`
|
|
735
|
+
);
|
|
543
736
|
}
|
|
544
737
|
const idx = (step) => ((degree + step) % length + length) % length;
|
|
545
738
|
const rootPc = tones[idx(0)] ?? pitchClass3(key.rootPc);
|
|
@@ -617,6 +810,31 @@ function createsHiddenParallelPerfect(aPrev, aCur, bPrev, bCur) {
|
|
|
617
810
|
// src/theory/safety/index.ts
|
|
618
811
|
var SUGGESTION_WINDOW = 12;
|
|
619
812
|
var MAX_SUGGESTIONS = 3;
|
|
813
|
+
var REASON_SEVERITY = [
|
|
814
|
+
[2 /* Tension */, 1 /* Warning */, 1 /* Warning */],
|
|
815
|
+
[4 /* AvoidNote */, 1 /* Warning */, 2 /* Dissonant */],
|
|
816
|
+
[8 /* ScaleTone */, 1 /* Warning */, 1 /* Warning */],
|
|
817
|
+
[16 /* NonScale */, 2 /* Dissonant */, 2 /* Dissonant */],
|
|
818
|
+
[32 /* OutOfRange */, 1 /* Warning */, 1 /* Warning */],
|
|
819
|
+
[64 /* Tritone */, 2 /* Dissonant */, 2 /* Dissonant */],
|
|
820
|
+
[128 /* LargeLeap */, 1 /* Warning */, 2 /* Dissonant */],
|
|
821
|
+
[1024 /* VerticalDissonance */, 2 /* Dissonant */, 2 /* Dissonant */],
|
|
822
|
+
[2048 /* ParallelPerfect */, 1 /* Warning */, 2 /* Dissonant */],
|
|
823
|
+
[4096 /* HiddenParallel */, 1 /* Warning */, 2 /* Dissonant */],
|
|
824
|
+
[8192 /* VoiceCrossing */, 1 /* Warning */, 2 /* Dissonant */],
|
|
825
|
+
[16384 /* Suspension */, 2 /* Dissonant */, 2 /* Dissonant */],
|
|
826
|
+
[32768 /* NeedsResolution */, 1 /* Warning */, 2 /* Dissonant */]
|
|
827
|
+
];
|
|
828
|
+
function minimumSafetyForReasons(reasons, profile) {
|
|
829
|
+
let minimum = 0 /* Safe */;
|
|
830
|
+
const severityIndex = profile === "strict" ? 2 : 1;
|
|
831
|
+
for (const entry of REASON_SEVERITY) {
|
|
832
|
+
if (reasons & entry[0]) {
|
|
833
|
+
minimum = Math.max(minimum, entry[severityIndex]);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return minimum;
|
|
837
|
+
}
|
|
620
838
|
function pitchClass4(pitch) {
|
|
621
839
|
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
622
840
|
}
|
|
@@ -640,6 +858,28 @@ function stepResolution(pitch, chord) {
|
|
|
640
858
|
}
|
|
641
859
|
return void 0;
|
|
642
860
|
}
|
|
861
|
+
function evaluateSafety(q) {
|
|
862
|
+
assertSafetyContext(q);
|
|
863
|
+
assertFiniteNumber(q.candidatePitch, "candidatePitch");
|
|
864
|
+
return evaluateInternal(q, true);
|
|
865
|
+
}
|
|
866
|
+
function assertSafetyContext(q) {
|
|
867
|
+
if (q.prevPitch !== void 0) assertFiniteNumber(q.prevPitch, "prevPitch");
|
|
868
|
+
if (q.vocalLow !== void 0) assertFiniteNumber(q.vocalLow, "vocalLow");
|
|
869
|
+
if (q.vocalHigh !== void 0) assertFiniteNumber(q.vocalHigh, "vocalHigh");
|
|
870
|
+
if (q.vocalLow !== void 0 && q.vocalHigh !== void 0 && q.vocalLow > q.vocalHigh) {
|
|
871
|
+
throw new RangeError("vocalLow must not exceed vocalHigh");
|
|
872
|
+
}
|
|
873
|
+
assertGenerationBudget(q.otherVoices.length, "other voices");
|
|
874
|
+
for (let index = 0; index < q.otherVoices.length; index += 1) {
|
|
875
|
+
const voice = q.otherVoices[index];
|
|
876
|
+
if (!voice) continue;
|
|
877
|
+
assertFiniteNumber(voice.pitch, `otherVoices[${index}].pitch`);
|
|
878
|
+
if (voice.prevPitch !== void 0) {
|
|
879
|
+
assertFiniteNumber(voice.prevPitch, `otherVoices[${index}].prevPitch`);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
643
883
|
function evaluateInternal(q, collectSuggestions) {
|
|
644
884
|
let reasons = 0;
|
|
645
885
|
let level = 0 /* Safe */;
|
|
@@ -700,9 +940,11 @@ function evaluateInternal(q, collectSuggestions) {
|
|
|
700
940
|
const pc = Math.abs(pitch - q.prevPitch) % 12;
|
|
701
941
|
if (pc === 6) {
|
|
702
942
|
reasons |= 64 /* Tritone */;
|
|
943
|
+
raise(2 /* Dissonant */);
|
|
703
944
|
}
|
|
704
945
|
if (isForbiddenMelodicLeap(q.prevPitch, pitch)) {
|
|
705
946
|
reasons |= 128 /* LargeLeap */;
|
|
947
|
+
raise(parallelSeverity);
|
|
706
948
|
}
|
|
707
949
|
if (pc === 1) {
|
|
708
950
|
reasons |= 256 /* MinorSecond */;
|
|
@@ -747,6 +989,7 @@ function evaluateInternal(q, collectSuggestions) {
|
|
|
747
989
|
}
|
|
748
990
|
}
|
|
749
991
|
}
|
|
992
|
+
raise(minimumSafetyForReasons(reasons, q.profile));
|
|
750
993
|
const result = { safety: level, reasons, rationale: describe(reasons, q) };
|
|
751
994
|
if (resolveTo !== void 0) {
|
|
752
995
|
result.resolveTo = resolveTo;
|
|
@@ -810,11 +1053,15 @@ function describe(reasons, q) {
|
|
|
810
1053
|
return q.chord ? "Placeable" : "No chord context";
|
|
811
1054
|
}
|
|
812
1055
|
function enumerateSafePitches(q, pitchLow, pitchHigh) {
|
|
1056
|
+
assertSafetyContext(q);
|
|
813
1057
|
const chordTones = [];
|
|
814
1058
|
const others = [];
|
|
815
|
-
|
|
816
|
-
|
|
1059
|
+
assertInteger(pitchLow, "pitchLow");
|
|
1060
|
+
assertInteger(pitchHigh, "pitchHigh");
|
|
1061
|
+
if (pitchLow > pitchHigh) {
|
|
1062
|
+
throw new RangeError(`pitchLow must not exceed pitchHigh; received ${pitchLow} > ${pitchHigh}`);
|
|
817
1063
|
}
|
|
1064
|
+
assertGenerationBudget(pitchHigh - pitchLow + 1, "safe pitch candidates");
|
|
818
1065
|
for (let pitch = pitchHigh; pitch >= pitchLow; pitch -= 1) {
|
|
819
1066
|
const result = evaluateInternal({ ...q, candidatePitch: pitch }, false);
|
|
820
1067
|
if (result.safety === 2 /* Dissonant */) {
|
|
@@ -831,7 +1078,7 @@ function enumerateSafePitches(q, pitchLow, pitchHigh) {
|
|
|
831
1078
|
|
|
832
1079
|
// src/generate/countermelody/index.ts
|
|
833
1080
|
var DEFAULT_TS2 = { numerator: 4, denominator: 4 };
|
|
834
|
-
var
|
|
1081
|
+
var EPS4 = 1e-9;
|
|
835
1082
|
var GRID_STEP = 0.5;
|
|
836
1083
|
var MIN_ONSET_GAP = 1;
|
|
837
1084
|
var REGISTER_OFFSET = 12;
|
|
@@ -842,30 +1089,35 @@ var REINFORCE_PROB = 0.35;
|
|
|
842
1089
|
var DEFAULT_MELODY_VELOCITY = 96;
|
|
843
1090
|
var VELOCITY_DROP = 16;
|
|
844
1091
|
function isWholeBeat(beat) {
|
|
845
|
-
return Math.abs(beat - Math.round(beat)) <
|
|
1092
|
+
return Math.abs(beat - Math.round(beat)) < EPS4;
|
|
846
1093
|
}
|
|
847
1094
|
function melodyNoteAt(melody, beat) {
|
|
848
|
-
|
|
849
|
-
for (const note of melody) {
|
|
850
|
-
if (note.startBeat - EPS3 <= beat && beat < note.startBeat + note.durationBeat - EPS3) {
|
|
851
|
-
if (!found || note.startBeat > found.startBeat) {
|
|
852
|
-
found = note;
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
return found;
|
|
1095
|
+
return melody.at(beat)?.note;
|
|
857
1096
|
}
|
|
858
1097
|
function melodyAttacksAt(melody, beat) {
|
|
859
|
-
return melody.
|
|
1098
|
+
return melody.attacksAt(beat);
|
|
1099
|
+
}
|
|
1100
|
+
function melodySnapshotAt(melody, beat) {
|
|
1101
|
+
const current = melodyNoteAt(melody, beat);
|
|
1102
|
+
if (current === void 0) {
|
|
1103
|
+
return void 0;
|
|
1104
|
+
}
|
|
1105
|
+
const attacked = Math.abs(current.startBeat - beat) < EPS4;
|
|
1106
|
+
const previous = attacked ? melodyNoteAt(melody, beat - EPS4 * 2) : current;
|
|
1107
|
+
const snapshot = { pitch: current.pitch };
|
|
1108
|
+
if (previous !== void 0) {
|
|
1109
|
+
snapshot.prevPitch = previous.pitch;
|
|
1110
|
+
}
|
|
1111
|
+
return snapshot;
|
|
860
1112
|
}
|
|
861
1113
|
function followOnsets(melody) {
|
|
862
|
-
return [...new Set(melody.map((note) => note.startBeat))]
|
|
1114
|
+
return [...new Set(melody.notes.map(({ note }) => note.startBeat))];
|
|
863
1115
|
}
|
|
864
1116
|
function complementOnsets(melody, ts, spanStart, spanEnd, rng) {
|
|
865
1117
|
const onsets = [];
|
|
866
1118
|
const gridStart = Math.floor(spanStart / GRID_STEP) * GRID_STEP;
|
|
867
1119
|
let last = Number.NEGATIVE_INFINITY;
|
|
868
|
-
for (let beat = gridStart; beat < spanEnd -
|
|
1120
|
+
for (let beat = gridStart; beat < spanEnd - EPS4; beat += GRID_STEP) {
|
|
869
1121
|
const attacked = melodyAttacksAt(melody, beat);
|
|
870
1122
|
const sounding = melodyNoteAt(melody, beat) !== void 0;
|
|
871
1123
|
let place = false;
|
|
@@ -876,7 +1128,7 @@ function complementOnsets(melody, ts, spanStart, spanEnd, rng) {
|
|
|
876
1128
|
} else if (isStrongBeat(beat, ts)) {
|
|
877
1129
|
place = rng.prob(REINFORCE_PROB);
|
|
878
1130
|
}
|
|
879
|
-
if (place && beat - last >= MIN_ONSET_GAP -
|
|
1131
|
+
if (place && beat - last >= MIN_ONSET_GAP - EPS4) {
|
|
880
1132
|
onsets.push(beat);
|
|
881
1133
|
last = beat;
|
|
882
1134
|
}
|
|
@@ -922,54 +1174,102 @@ function scoreCandidate(pitch, melPitch, melPrev, prevPitch, chord, center) {
|
|
|
922
1174
|
score -= Math.abs(pitch - center) * 0.02;
|
|
923
1175
|
return score;
|
|
924
1176
|
}
|
|
925
|
-
function
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1177
|
+
function heldNoteBoundaries(melody, startBeat, endBeat, chordChangeBeats) {
|
|
1178
|
+
const boundaries = /* @__PURE__ */ new Set([startBeat]);
|
|
1179
|
+
for (const onset of melody.onsetsBetween(startBeat, endBeat)) {
|
|
1180
|
+
boundaries.add(onset);
|
|
1181
|
+
}
|
|
1182
|
+
for (const beat of chordChangeBeats) {
|
|
1183
|
+
if (beat > startBeat + EPS4 && beat < endBeat - EPS4) {
|
|
1184
|
+
boundaries.add(beat);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
const firstGrid = Math.floor(startBeat / GRID_STEP + 1) * GRID_STEP;
|
|
1188
|
+
for (let beat = firstGrid; beat < endBeat - EPS4; beat += GRID_STEP) {
|
|
1189
|
+
boundaries.add(beat);
|
|
1190
|
+
}
|
|
1191
|
+
return [...boundaries].sort((a, b) => a - b);
|
|
1192
|
+
}
|
|
1193
|
+
function heldPitchSafety(pitch, startBeat, endBeat, prevPitch, melody, opts, profile, register, low, high, ts) {
|
|
1194
|
+
let worst = 0 /* Safe */;
|
|
1195
|
+
const boundaries = heldNoteBoundaries(melody, startBeat, endBeat, opts.chordChangeBeats ?? []);
|
|
1196
|
+
for (const boundary of boundaries) {
|
|
1197
|
+
const melodyVoice = melodySnapshotAt(melody, boundary);
|
|
1198
|
+
if (melodyVoice !== void 0 && (register === "below" && pitch >= melodyVoice.pitch || register === "above" && pitch <= melodyVoice.pitch)) {
|
|
1199
|
+
return null;
|
|
1200
|
+
}
|
|
1201
|
+
const atCounterOnset = Math.abs(boundary - startBeat) < EPS4;
|
|
1202
|
+
const result = evaluateSafety({
|
|
1203
|
+
profile,
|
|
1204
|
+
candidatePitch: pitch,
|
|
1205
|
+
prevPitch: atCounterOnset ? prevPitch : pitch,
|
|
1206
|
+
chord: opts.chordAt(boundary),
|
|
1207
|
+
key: opts.key,
|
|
1208
|
+
otherVoices: melodyVoice ? [melodyVoice] : [],
|
|
1209
|
+
strongBeat: isStrongBeat(boundary, ts),
|
|
1210
|
+
vocalLow: low,
|
|
1211
|
+
vocalHigh: high
|
|
1212
|
+
});
|
|
1213
|
+
worst = Math.max(worst, result.safety);
|
|
1214
|
+
if (worst === 2 /* Dissonant */) {
|
|
1215
|
+
return worst;
|
|
942
1216
|
}
|
|
943
1217
|
}
|
|
944
|
-
return
|
|
1218
|
+
return worst;
|
|
945
1219
|
}
|
|
946
1220
|
function generateCounterMelody(opts) {
|
|
947
|
-
const melody =
|
|
948
|
-
if (melody.length === 0) {
|
|
1221
|
+
const melody = createNoteEventIndex(opts.melody);
|
|
1222
|
+
if (melody.notes.length === 0) {
|
|
949
1223
|
return [];
|
|
950
1224
|
}
|
|
951
1225
|
const ts = opts.ts ?? DEFAULT_TS2;
|
|
1226
|
+
assertTimeSignature(ts);
|
|
952
1227
|
const register = opts.register ?? "below";
|
|
953
1228
|
const rhythm = opts.rhythm ?? "complement";
|
|
954
1229
|
const profile = opts.profile ?? "pop";
|
|
955
1230
|
const rng = createRng(opts.seed ?? 0);
|
|
956
|
-
const spanStart = melody[0]?.startBeat ?? 0;
|
|
957
|
-
const spanEnd = melody.reduce((end, n) => Math.max(end, n.
|
|
958
|
-
|
|
1231
|
+
const spanStart = melody.notes[0]?.note.startBeat ?? 0;
|
|
1232
|
+
const spanEnd = melody.notes.reduce((end, n) => Math.max(end, n.endBeat), spanStart);
|
|
1233
|
+
assertGenerationBudget(Math.ceil((spanEnd - spanStart) / GRID_STEP), "countermelody grid");
|
|
1234
|
+
const meanPitch = melody.notes.reduce((sum, n) => sum + n.note.pitch, 0) / melody.notes.length;
|
|
959
1235
|
const defaultCenter = register === "below" ? meanPitch - REGISTER_OFFSET : meanPitch + REGISTER_OFFSET;
|
|
960
1236
|
const low = opts.pitchLow ?? Math.round(defaultCenter) - DEFAULT_HALF_RANGE;
|
|
961
1237
|
const high = opts.pitchHigh ?? Math.round(defaultCenter) + DEFAULT_HALF_RANGE;
|
|
1238
|
+
assertInteger(low, "countermelody pitchLow");
|
|
1239
|
+
assertInteger(high, "countermelody pitchHigh");
|
|
1240
|
+
if (low > high) {
|
|
1241
|
+
throw new RangeError(
|
|
1242
|
+
`countermelody pitchLow must not exceed pitchHigh; received ${low} > ${high}`
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
assertGenerationBudget(high - low + 1, "countermelody pitch candidates");
|
|
1246
|
+
assertGenerationBudget(opts.chordChangeBeats?.length ?? 0, "chord change beats");
|
|
1247
|
+
for (let index = 0; index < (opts.chordChangeBeats?.length ?? 0); index += 1) {
|
|
1248
|
+
assertRange(
|
|
1249
|
+
opts.chordChangeBeats?.[index] ?? Number.NaN,
|
|
1250
|
+
0,
|
|
1251
|
+
Number.MAX_SAFE_INTEGER,
|
|
1252
|
+
`chordChangeBeats[${index}]`
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
962
1255
|
const center = (low + high) / 2;
|
|
963
1256
|
const onsets = rhythm === "follow" ? followOnsets(melody) : complementOnsets(melody, ts, spanStart, spanEnd, rng);
|
|
1257
|
+
assertGenerationBudget(onsets.length * (high - low + 1), "countermelody search");
|
|
964
1258
|
const out = [];
|
|
965
1259
|
let prevPitch;
|
|
966
|
-
let
|
|
967
|
-
|
|
1260
|
+
for (let onsetIndex = 0; onsetIndex < onsets.length; onsetIndex += 1) {
|
|
1261
|
+
const beat = onsets[onsetIndex];
|
|
1262
|
+
if (beat === void 0) {
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
const nextOnset = onsets[onsetIndex + 1];
|
|
1266
|
+
const endBeat = nextOnset ?? Math.max(spanEnd, beat + GRID_STEP);
|
|
968
1267
|
const melNote = melodyNoteAt(melody, beat);
|
|
969
1268
|
const melPitch = melNote?.pitch;
|
|
970
|
-
const
|
|
1269
|
+
const melodyVoice = melodySnapshotAt(melody, beat);
|
|
1270
|
+
const melPrev = melodyVoice?.prevPitch;
|
|
971
1271
|
const chord = opts.chordAt(beat);
|
|
972
|
-
const otherVoices =
|
|
1272
|
+
const otherVoices = melodyVoice !== void 0 ? [melodyVoice] : [];
|
|
973
1273
|
const candidates = enumerateSafePitches(
|
|
974
1274
|
{
|
|
975
1275
|
profile,
|
|
@@ -985,15 +1285,26 @@ function generateCounterMelody(opts) {
|
|
|
985
1285
|
high
|
|
986
1286
|
);
|
|
987
1287
|
let bestPitch;
|
|
1288
|
+
let bestWorstSafety = 2 /* Dissonant */;
|
|
988
1289
|
let bestScore = Number.NEGATIVE_INFINITY;
|
|
989
1290
|
for (const candidate of candidates) {
|
|
1291
|
+
const worstSafety = heldPitchSafety(
|
|
1292
|
+
candidate,
|
|
1293
|
+
beat,
|
|
1294
|
+
endBeat,
|
|
1295
|
+
prevPitch,
|
|
1296
|
+
melody,
|
|
1297
|
+
opts,
|
|
1298
|
+
profile,
|
|
1299
|
+
register,
|
|
1300
|
+
low,
|
|
1301
|
+
high,
|
|
1302
|
+
ts
|
|
1303
|
+
);
|
|
1304
|
+
if (worstSafety === null || worstSafety === 2 /* Dissonant */) {
|
|
1305
|
+
continue;
|
|
1306
|
+
}
|
|
990
1307
|
if (melPitch !== void 0) {
|
|
991
|
-
if (register === "below" && candidate >= melPitch) {
|
|
992
|
-
continue;
|
|
993
|
-
}
|
|
994
|
-
if (register === "above" && candidate <= melPitch) {
|
|
995
|
-
continue;
|
|
996
|
-
}
|
|
997
1308
|
if (prevPitch !== void 0 && melPrev !== void 0 && // Parallel octaves are the perfect-class-zero case of this predicate,
|
|
998
1309
|
// so a single check covers both parallel fifths and octaves.
|
|
999
1310
|
createsParallelPerfect(prevPitch, candidate, melPrev, melPitch)) {
|
|
@@ -1001,29 +1312,24 @@ function generateCounterMelody(opts) {
|
|
|
1001
1312
|
}
|
|
1002
1313
|
}
|
|
1003
1314
|
const score = scoreCandidate(candidate, melPitch, melPrev, prevPitch, chord, center);
|
|
1004
|
-
const tie = Math.abs(score - bestScore) <=
|
|
1005
|
-
if (score > bestScore +
|
|
1315
|
+
const tie = Math.abs(score - bestScore) <= EPS4 && candidate < (bestPitch ?? Number.POSITIVE_INFINITY);
|
|
1316
|
+
if (worstSafety < bestWorstSafety || worstSafety === bestWorstSafety && (score > bestScore + EPS4 || tie)) {
|
|
1317
|
+
bestWorstSafety = worstSafety;
|
|
1006
1318
|
bestScore = score;
|
|
1007
1319
|
bestPitch = candidate;
|
|
1008
1320
|
}
|
|
1009
1321
|
}
|
|
1010
|
-
|
|
1322
|
+
if (bestPitch === void 0) {
|
|
1323
|
+
prevPitch = void 0;
|
|
1324
|
+
continue;
|
|
1325
|
+
}
|
|
1326
|
+
const pitch = bestPitch;
|
|
1011
1327
|
const velocity = Math.max(
|
|
1012
1328
|
1,
|
|
1013
1329
|
Math.min(127, Math.round((melNote?.velocity ?? DEFAULT_MELODY_VELOCITY) - VELOCITY_DROP))
|
|
1014
1330
|
);
|
|
1015
|
-
out.push({ pitch, startBeat: beat, durationBeat:
|
|
1331
|
+
out.push({ pitch, startBeat: beat, durationBeat: endBeat - beat, velocity });
|
|
1016
1332
|
prevPitch = pitch;
|
|
1017
|
-
prevOnset = beat;
|
|
1018
|
-
}
|
|
1019
|
-
for (let i = 0; i < out.length; i += 1) {
|
|
1020
|
-
const note = out[i];
|
|
1021
|
-
if (!note) {
|
|
1022
|
-
continue;
|
|
1023
|
-
}
|
|
1024
|
-
const next = out[i + 1];
|
|
1025
|
-
const end = next ? next.startBeat : Math.max(spanEnd, note.startBeat + GRID_STEP);
|
|
1026
|
-
note.durationBeat = end - note.startBeat;
|
|
1027
1333
|
}
|
|
1028
1334
|
return out;
|
|
1029
1335
|
}
|
|
@@ -1743,9 +2049,10 @@ function generateHiHatForBeat(ctx, sec) {
|
|
|
1743
2049
|
|
|
1744
2050
|
// src/generate/drums/euclid.ts
|
|
1745
2051
|
function euclideanRhythm(pulses, steps, rotation = 0) {
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
2052
|
+
assertPositiveInt(steps, "euclidean steps");
|
|
2053
|
+
assertInteger(pulses, "euclidean pulses");
|
|
2054
|
+
assertInteger(rotation, "euclidean rotation");
|
|
2055
|
+
assertGenerationBudget(steps, "euclidean pattern steps");
|
|
1749
2056
|
const p = Math.max(0, Math.min(pulses, steps));
|
|
1750
2057
|
const base = bjorklund(p, steps);
|
|
1751
2058
|
const shift = (rotation % steps + steps) % steps;
|
|
@@ -1754,18 +2061,6 @@ function euclideanRhythm(pulses, steps, rotation = 0) {
|
|
|
1754
2061
|
}
|
|
1755
2062
|
return base.map((_, i) => base[(i - shift + steps) % steps] ?? false);
|
|
1756
2063
|
}
|
|
1757
|
-
function hasHit(mask, step) {
|
|
1758
|
-
return (mask >> step & 1) === 1;
|
|
1759
|
-
}
|
|
1760
|
-
function patternToMask(pattern) {
|
|
1761
|
-
let mask = 0;
|
|
1762
|
-
for (let i = 0; i < pattern.length; i += 1) {
|
|
1763
|
-
if (pattern[i]) {
|
|
1764
|
-
mask |= 1 << i;
|
|
1765
|
-
}
|
|
1766
|
-
}
|
|
1767
|
-
return mask;
|
|
1768
|
-
}
|
|
1769
2064
|
function bjorklund(pulses, steps) {
|
|
1770
2065
|
if (pulses === 0) {
|
|
1771
2066
|
return Array.from({ length: steps }, () => false);
|
|
@@ -2097,19 +2392,6 @@ function isInPreChorusLift(section, bar, sectionBars, nextIsChorus) {
|
|
|
2097
2392
|
}
|
|
2098
2393
|
return bar >= sectionBars - 2;
|
|
2099
2394
|
}
|
|
2100
|
-
function euclideanToKickPattern(mask) {
|
|
2101
|
-
const slot = (step) => hasHit(mask, step) || hasHit(mask, (step + 15) % 16);
|
|
2102
|
-
return {
|
|
2103
|
-
beat1: slot(0),
|
|
2104
|
-
beat1and: slot(2),
|
|
2105
|
-
beat2: slot(4),
|
|
2106
|
-
beat2and: slot(6),
|
|
2107
|
-
beat3: slot(8),
|
|
2108
|
-
beat3and: slot(10),
|
|
2109
|
-
beat4: slot(12),
|
|
2110
|
-
beat4and: slot(14)
|
|
2111
|
-
};
|
|
2112
|
-
}
|
|
2113
2395
|
function getKickPattern(section, style, bar, rng) {
|
|
2114
2396
|
const p = { ...EMPTY };
|
|
2115
2397
|
if (section === "outro") {
|
|
@@ -2231,7 +2513,7 @@ function getPercussionConfig(mood, section) {
|
|
|
2231
2513
|
};
|
|
2232
2514
|
}
|
|
2233
2515
|
function generateAuxPercussionForBar(track, barStart, config, role, densityMult, rng, bpm) {
|
|
2234
|
-
if (role === "minimal"
|
|
2516
|
+
if (role === "minimal") {
|
|
2235
2517
|
return;
|
|
2236
2518
|
}
|
|
2237
2519
|
if (config.tambourine) {
|
|
@@ -2280,6 +2562,16 @@ function generateAuxPercussionForBar(track, barStart, config, role, densityMult,
|
|
|
2280
2562
|
|
|
2281
2563
|
// src/generate/drums/index.ts
|
|
2282
2564
|
function generateDrums(opts) {
|
|
2565
|
+
assertPositiveInt(opts.bars, "drum bars");
|
|
2566
|
+
assertRange(opts.bpm, Number.MIN_VALUE, 1e3, "drum bpm");
|
|
2567
|
+
assertRange(opts.density, 0, 1, "drum density");
|
|
2568
|
+
assertGenerationBudget(opts.bars * 128, "drum hits");
|
|
2569
|
+
if (opts.euclideanKick) {
|
|
2570
|
+
const steps = opts.euclideanKick.steps ?? 16;
|
|
2571
|
+
assertInteger(steps, "euclidean steps", 1, 16);
|
|
2572
|
+
assertInteger(opts.euclideanKick.pulses, "euclidean pulses");
|
|
2573
|
+
assertInteger(opts.euclideanKick.rotation ?? 0, "euclidean rotation");
|
|
2574
|
+
}
|
|
2283
2575
|
const track = new HitList();
|
|
2284
2576
|
const rng = createRng(opts.seed ?? 0);
|
|
2285
2577
|
const mapping = mapStyle(opts.style);
|
|
@@ -2316,15 +2608,8 @@ function generateDrums(opts) {
|
|
|
2316
2608
|
const percMood = percMoodCategory(style);
|
|
2317
2609
|
const nextSection = opts.nextSection ? mapSection(opts.nextSection) : section;
|
|
2318
2610
|
const nextEnergy = sectionEnergy(nextSection);
|
|
2319
|
-
const
|
|
2320
|
-
|
|
2321
|
-
euclideanRhythm(
|
|
2322
|
-
opts.euclideanKick.pulses,
|
|
2323
|
-
opts.euclideanKick.steps ?? 16,
|
|
2324
|
-
opts.euclideanKick.rotation ?? 0
|
|
2325
|
-
)
|
|
2326
|
-
)
|
|
2327
|
-
) : void 0;
|
|
2611
|
+
const euclidSteps = opts.euclideanKick?.steps ?? 16;
|
|
2612
|
+
const euclidKick = opts.euclideanKick ? euclideanRhythm(opts.euclideanKick.pulses, euclidSteps, opts.euclideanKick.rotation ?? 0) : void 0;
|
|
2328
2613
|
const reuseSectionKick = (section === "b" || section === "chorus") && style !== "sparse";
|
|
2329
2614
|
let sectionKick;
|
|
2330
2615
|
for (let bar = 0; bar < opts.bars; bar += 1) {
|
|
@@ -2340,12 +2625,10 @@ function generateDrums(opts) {
|
|
|
2340
2625
|
barHasOpenHh = !track.hasCrashNear(barStart + openHhBeatIndex);
|
|
2341
2626
|
}
|
|
2342
2627
|
let kick;
|
|
2343
|
-
if (euclidKick) {
|
|
2344
|
-
kick = euclidKick;
|
|
2345
|
-
} else if (reuseSectionKick) {
|
|
2628
|
+
if (!euclidKick && reuseSectionKick) {
|
|
2346
2629
|
sectionKick ??= getKickPattern(section, style, 0, rng);
|
|
2347
2630
|
kick = sectionKick;
|
|
2348
|
-
} else {
|
|
2631
|
+
} else if (!euclidKick) {
|
|
2349
2632
|
kick = getKickPattern(section, style, bar, rng);
|
|
2350
2633
|
}
|
|
2351
2634
|
const inLift = isInPreChorusLift(section, bar, opts.bars, section === "b");
|
|
@@ -2386,7 +2669,23 @@ function generateDrums(opts) {
|
|
|
2386
2669
|
if (inLift) {
|
|
2387
2670
|
generatePreChorusBuildup(ctx, sec, isLastBar);
|
|
2388
2671
|
}
|
|
2389
|
-
|
|
2672
|
+
if (euclidKick && !inLift) {
|
|
2673
|
+
const stepLength = 4 / euclidSteps;
|
|
2674
|
+
for (let step = 0; step < euclidKick.length; step += 1) {
|
|
2675
|
+
const onset = step * stepLength;
|
|
2676
|
+
if (euclidKick[step] && Math.floor(onset) === beat) {
|
|
2677
|
+
const wholeBeat = Math.abs(onset - Math.round(onset)) < 1e-9;
|
|
2678
|
+
track.add(
|
|
2679
|
+
GM.BD,
|
|
2680
|
+
barStart + onset,
|
|
2681
|
+
Math.min(0.5, stepLength),
|
|
2682
|
+
velocity * (wholeBeat ? 1 : 0.85)
|
|
2683
|
+
);
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
} else if (kick) {
|
|
2687
|
+
generateKickForBeat(ctx, sec, kick);
|
|
2688
|
+
}
|
|
2390
2689
|
generateSnareForBeat(ctx, sec, section === "intro" && bar === 0);
|
|
2391
2690
|
if (sec.useGhostNotes && !inLift) {
|
|
2392
2691
|
generateGhostNotesForBeat(ctx, sec);
|
|
@@ -2403,10 +2702,19 @@ function generateDrums(opts) {
|
|
|
2403
2702
|
}
|
|
2404
2703
|
}
|
|
2405
2704
|
}
|
|
2705
|
+
let percussionConfig = getPercussionConfig(percMood, section);
|
|
2706
|
+
if (role === "fxOnly" && !percussionConfig.tambourine && !percussionConfig.shaker && !percussionConfig.handclap) {
|
|
2707
|
+
percussionConfig = {
|
|
2708
|
+
tambourine: false,
|
|
2709
|
+
shaker: true,
|
|
2710
|
+
handclap: false,
|
|
2711
|
+
shaker16th: false
|
|
2712
|
+
};
|
|
2713
|
+
}
|
|
2406
2714
|
generateAuxPercussionForBar(
|
|
2407
2715
|
track,
|
|
2408
2716
|
barStart,
|
|
2409
|
-
|
|
2717
|
+
percussionConfig,
|
|
2410
2718
|
role,
|
|
2411
2719
|
densityMult,
|
|
2412
2720
|
rng,
|
|
@@ -2427,10 +2735,16 @@ var MIN_VELOCITY = 1;
|
|
|
2427
2735
|
var MAX_VELOCITY = 127;
|
|
2428
2736
|
function humanize(events, opts = {}) {
|
|
2429
2737
|
const ts = opts.ts ?? DEFAULT_TS3;
|
|
2738
|
+
assertTimeSignature(ts);
|
|
2739
|
+
assertNoteEvents(events, "humanize events");
|
|
2430
2740
|
const timing = opts.timing ?? DEFAULT_TIMING;
|
|
2431
2741
|
const velocityJitter = opts.velocity ?? DEFAULT_VELOCITY_JITTER;
|
|
2432
2742
|
const accent = opts.accent ?? DEFAULT_ACCENT;
|
|
2433
2743
|
const baseVelocity = opts.baseVelocity ?? DEFAULT_BASE_VELOCITY;
|
|
2744
|
+
assertRange(timing, 0, Number.MAX_SAFE_INTEGER, "humanize timing");
|
|
2745
|
+
assertRange(velocityJitter, 0, 127, "humanize velocity jitter");
|
|
2746
|
+
assertRange(accent, 0, 127, "humanize accent");
|
|
2747
|
+
assertRange(baseVelocity, 0, 127, "humanize base velocity");
|
|
2434
2748
|
const rng = createRng(opts.seed ?? 0);
|
|
2435
2749
|
return events.map((event) => {
|
|
2436
2750
|
const timingOffset = rng.float(-timing, timing);
|
|
@@ -2463,8 +2777,12 @@ function quantizeToGrid(beat, barBeats, subdivision, slotsPerBar) {
|
|
|
2463
2777
|
};
|
|
2464
2778
|
}
|
|
2465
2779
|
function extractGrooveTemplate(events, ts, subdivision) {
|
|
2780
|
+
assertTimeSignature(ts);
|
|
2781
|
+
assertNoteEvents(events, "groove source events");
|
|
2782
|
+
assertPositiveInt(subdivision, "groove subdivision");
|
|
2466
2783
|
const barBeats = beatsPerBar(ts);
|
|
2467
2784
|
const slotsPerBar = Math.round(barBeats * subdivision);
|
|
2785
|
+
assertGenerationBudget(slotsPerBar, "groove template slots");
|
|
2468
2786
|
const offsetSums = new Array(slotsPerBar).fill(0);
|
|
2469
2787
|
const velocitySums = new Array(slotsPerBar).fill(0);
|
|
2470
2788
|
const counts = new Array(slotsPerBar).fill(0);
|
|
@@ -2499,6 +2817,14 @@ function extractGrooveTemplate(events, ts, subdivision) {
|
|
|
2499
2817
|
return { subdivision, slotsPerBar, slots, ts };
|
|
2500
2818
|
}
|
|
2501
2819
|
function applyGrooveTemplate(events, template, ts) {
|
|
2820
|
+
assertTimeSignature(ts);
|
|
2821
|
+
assertNoteEvents(events, "groove target events");
|
|
2822
|
+
assertPositiveInt(template.subdivision, "template subdivision");
|
|
2823
|
+
assertPositiveInt(template.slotsPerBar, "template slotsPerBar");
|
|
2824
|
+
assertGenerationBudget(template.slots.length, "template slots");
|
|
2825
|
+
if (template.slots.length !== template.slotsPerBar) {
|
|
2826
|
+
throw new RangeError("template slots length must equal slotsPerBar");
|
|
2827
|
+
}
|
|
2502
2828
|
if (template.ts && (template.ts.numerator !== ts.numerator || template.ts.denominator !== ts.denominator)) {
|
|
2503
2829
|
throw new Error(
|
|
2504
2830
|
`Groove template meter ${formatTimeSignature(template.ts)} does not match apply-time meter ${formatTimeSignature(ts)}`
|
|
@@ -2524,107 +2850,291 @@ function applyGrooveTemplate(events, template, ts) {
|
|
|
2524
2850
|
});
|
|
2525
2851
|
}
|
|
2526
2852
|
|
|
2527
|
-
// src/
|
|
2528
|
-
function
|
|
2529
|
-
|
|
2530
|
-
const tones = new Set(chord.intervals.map((i) => (i % 12 + 12) % 12));
|
|
2531
|
-
const hasHigherSeventh = tones.has(10) || tones.has(11);
|
|
2532
|
-
const hasThird = tones.has(3) || tones.has(4);
|
|
2533
|
-
const isSuspendedTone = !hasThird && (interval === 5 && tones.has(5) || interval === 2 && tones.has(2));
|
|
2534
|
-
let role;
|
|
2535
|
-
let lock;
|
|
2536
|
-
if (interval === 0) {
|
|
2537
|
-
role = "root";
|
|
2538
|
-
lock = "identity";
|
|
2539
|
-
} else if (isSuspendedTone) {
|
|
2540
|
-
role = "third";
|
|
2541
|
-
lock = "quality";
|
|
2542
|
-
} else if (interval === 3 || interval === 4) {
|
|
2543
|
-
role = "third";
|
|
2544
|
-
lock = "quality";
|
|
2545
|
-
} else if (interval === 6 || interval === 7 || interval === 8) {
|
|
2546
|
-
role = "fifth";
|
|
2547
|
-
lock = "voicing";
|
|
2548
|
-
} else if (interval === 9 && tones.has(3) && tones.has(6) && !hasHigherSeventh) {
|
|
2549
|
-
role = "seventh";
|
|
2550
|
-
lock = "voicing";
|
|
2551
|
-
} else if (interval === 9 && tones.has(9) && !hasHigherSeventh) {
|
|
2552
|
-
role = "sixth";
|
|
2553
|
-
lock = "voicing";
|
|
2554
|
-
} else if (interval === 10 || interval === 11) {
|
|
2555
|
-
role = "seventh";
|
|
2556
|
-
lock = "voicing";
|
|
2557
|
-
} else {
|
|
2558
|
-
role = "tension";
|
|
2559
|
-
lock = "voicing";
|
|
2560
|
-
}
|
|
2561
|
-
return { role, lock, belongsToChordId: chordId };
|
|
2853
|
+
// src/analyze/functional/internal.ts
|
|
2854
|
+
function mod12(n) {
|
|
2855
|
+
return (n % 12 + 12) % 12;
|
|
2562
2856
|
}
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
var FALLBACK = { rootPc: 0, quality: "maj", secondaryDominant: false, base: 0 };
|
|
2566
|
-
var DEFAULT_METER = { numerator: 4, denominator: 4 };
|
|
2567
|
-
var COMFORT_LOW = 55;
|
|
2568
|
-
var COMFORT_HIGH = 79;
|
|
2569
|
-
var TESSITURA_WEIGHT = 1e-3;
|
|
2570
|
-
var TIE_BREAK_JITTER = 1e-6;
|
|
2571
|
-
function pitchClass5(pitch) {
|
|
2572
|
-
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
2857
|
+
function isNeapolitan(chord, key) {
|
|
2858
|
+
return mod12(chord.rootPc - key.rootPc) === 1 && chord.quality === "maj";
|
|
2573
2859
|
}
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2860
|
+
|
|
2861
|
+
// src/analyze/functional/roman.ts
|
|
2862
|
+
var ROMAN = ["I", "II", "III", "IV", "V", "VI", "VII"];
|
|
2863
|
+
var OFFSET_SPELLING = [
|
|
2864
|
+
[1, ""],
|
|
2865
|
+
[2, "b"],
|
|
2866
|
+
[2, ""],
|
|
2867
|
+
[3, "b"],
|
|
2868
|
+
[3, ""],
|
|
2869
|
+
[4, ""],
|
|
2870
|
+
[4, "#"],
|
|
2871
|
+
[5, ""],
|
|
2872
|
+
[6, "b"],
|
|
2873
|
+
[6, ""],
|
|
2874
|
+
[7, "b"],
|
|
2875
|
+
[7, ""]
|
|
2876
|
+
];
|
|
2877
|
+
var ROMAN_STYLE = {
|
|
2878
|
+
maj: { lower: false, suffix: "" },
|
|
2879
|
+
min: { lower: true, suffix: "" },
|
|
2880
|
+
dim: { lower: true, suffix: "o" },
|
|
2881
|
+
aug: { lower: false, suffix: "+" },
|
|
2882
|
+
maj7: { lower: false, suffix: "maj7" },
|
|
2883
|
+
min7: { lower: true, suffix: "7" },
|
|
2884
|
+
dom7: { lower: false, suffix: "7" },
|
|
2885
|
+
dim7: { lower: true, suffix: "o7" },
|
|
2886
|
+
m7b5: { lower: true, suffix: "\xF87" },
|
|
2887
|
+
minMaj7: { lower: true, suffix: "maj7" },
|
|
2888
|
+
aug7: { lower: false, suffix: "+7" },
|
|
2889
|
+
augMaj7: { lower: false, suffix: "+maj7" },
|
|
2890
|
+
majb5: { lower: false, suffix: "b5" },
|
|
2891
|
+
"6": { lower: false, suffix: "add6" },
|
|
2892
|
+
min6: { lower: true, suffix: "add6" },
|
|
2893
|
+
"6/9": { lower: false, suffix: "69" },
|
|
2894
|
+
sus2: { lower: false, suffix: "sus2" },
|
|
2895
|
+
sus4: { lower: false, suffix: "sus4" },
|
|
2896
|
+
add9: { lower: false, suffix: "add9" },
|
|
2897
|
+
add11: { lower: false, suffix: "add11" },
|
|
2898
|
+
maj9: { lower: false, suffix: "maj9" },
|
|
2899
|
+
min9: { lower: true, suffix: "9" },
|
|
2900
|
+
dom9: { lower: false, suffix: "9" },
|
|
2901
|
+
"7b9": { lower: false, suffix: "7b9" },
|
|
2902
|
+
"7#9": { lower: false, suffix: "7#9" },
|
|
2903
|
+
"7#11": { lower: false, suffix: "7#11" },
|
|
2904
|
+
"7b13": { lower: false, suffix: "7b13" },
|
|
2905
|
+
"11": { lower: false, suffix: "11" },
|
|
2906
|
+
"13": { lower: false, suffix: "13" },
|
|
2907
|
+
"5": { lower: false, suffix: "5" }
|
|
2908
|
+
};
|
|
2909
|
+
var SUFFIX_QUALITY = new Map(
|
|
2910
|
+
Object.entries(ROMAN_STYLE).map(
|
|
2911
|
+
([quality, style]) => [`${style.lower ? "l" : "u"}:${style.suffix}`, quality]
|
|
2912
|
+
)
|
|
2913
|
+
);
|
|
2914
|
+
function romanStyle(quality) {
|
|
2915
|
+
return ROMAN_STYLE[quality];
|
|
2583
2916
|
}
|
|
2584
|
-
function
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
candidates.push(majorKey(tonic), minorKey(tonic));
|
|
2917
|
+
function romanSpelling(rootPc, key) {
|
|
2918
|
+
const tones = scaleTonesInDegreeOrder(key);
|
|
2919
|
+
const diatonic = tones.indexOf(mod12(rootPc));
|
|
2920
|
+
if (diatonic >= 0) {
|
|
2921
|
+
return { degreeNumber: diatonic + 1, accidental: "" };
|
|
2590
2922
|
}
|
|
2591
|
-
for (
|
|
2592
|
-
const
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
let score = 0;
|
|
2596
|
-
for (const n of melody) {
|
|
2597
|
-
const w = Math.max(0.25, n.durationBeat);
|
|
2598
|
-
const pc = pitchClass5(n.pitch);
|
|
2599
|
-
if (isScaleTone(n.pitch, key) || isMinor && pc === leadingTone) {
|
|
2600
|
-
score += w;
|
|
2601
|
-
}
|
|
2602
|
-
if (pc === tonic) {
|
|
2603
|
-
score += 0.5 * w;
|
|
2604
|
-
}
|
|
2923
|
+
for (let i = 0; i < tones.length; i += 1) {
|
|
2924
|
+
const degreeNumber2 = i + 1;
|
|
2925
|
+
if (degreeNumber2 === 1 || degreeNumber2 === 5) {
|
|
2926
|
+
continue;
|
|
2605
2927
|
}
|
|
2606
|
-
if (
|
|
2607
|
-
|
|
2608
|
-
best = key;
|
|
2928
|
+
if (mod12((tones[i] ?? 0) - 1) === mod12(rootPc)) {
|
|
2929
|
+
return { degreeNumber: degreeNumber2, accidental: "b" };
|
|
2609
2930
|
}
|
|
2610
2931
|
}
|
|
2611
|
-
|
|
2932
|
+
for (let i = 0; i < tones.length; i += 1) {
|
|
2933
|
+
if (mod12((tones[i] ?? 0) + 1) === mod12(rootPc)) {
|
|
2934
|
+
return { degreeNumber: i + 1, accidental: "#" };
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
const [degreeNumber, accidental] = OFFSET_SPELLING[mod12(rootPc - key.rootPc)] ?? [1, ""];
|
|
2938
|
+
return { degreeNumber, accidental };
|
|
2612
2939
|
}
|
|
2613
|
-
function
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
if (
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2940
|
+
function baseMarker(quality) {
|
|
2941
|
+
if (quality === "dim" || quality === "dim7") {
|
|
2942
|
+
return "o";
|
|
2943
|
+
}
|
|
2944
|
+
if (quality === "m7b5") {
|
|
2945
|
+
return "\xF8";
|
|
2946
|
+
}
|
|
2947
|
+
if (quality === "aug" || quality === "aug7") {
|
|
2948
|
+
return "+";
|
|
2949
|
+
}
|
|
2950
|
+
if (quality === "augMaj7") {
|
|
2951
|
+
return "+maj7";
|
|
2952
|
+
}
|
|
2953
|
+
if (quality === "maj7" || quality === "minMaj7") {
|
|
2954
|
+
return "maj7";
|
|
2955
|
+
}
|
|
2956
|
+
return "";
|
|
2957
|
+
}
|
|
2958
|
+
var TRIAD_FIGURES = { 1: "6", 2: "64" };
|
|
2959
|
+
var SEVENTH_FIGURES = { 1: "65", 2: "43", 3: "42" };
|
|
2960
|
+
var TRIAD_FIGURE_QUALITIES = /* @__PURE__ */ new Set(["maj", "min", "dim", "aug"]);
|
|
2961
|
+
var SEVENTH_FIGURE_QUALITIES = /* @__PURE__ */ new Set([
|
|
2962
|
+
"maj7",
|
|
2963
|
+
"min7",
|
|
2964
|
+
"dom7",
|
|
2965
|
+
"dim7",
|
|
2966
|
+
"m7b5",
|
|
2967
|
+
"minMaj7",
|
|
2968
|
+
"aug7",
|
|
2969
|
+
"augMaj7"
|
|
2970
|
+
]);
|
|
2971
|
+
function chordToRoman(chord, key) {
|
|
2972
|
+
const { degreeNumber, accidental } = romanSpelling(chord.rootPc, key);
|
|
2973
|
+
const { lower, suffix } = romanStyle(chord.quality);
|
|
2974
|
+
const numeral = ROMAN[degreeNumber - 1] ?? "I";
|
|
2975
|
+
const cased = lower ? numeral.toLowerCase() : numeral;
|
|
2976
|
+
let inversion = 0;
|
|
2977
|
+
if (chord.bassPc !== void 0) {
|
|
2978
|
+
const idx = chord.intervals.findIndex((iv) => mod12(chord.rootPc + iv) === chord.bassPc);
|
|
2979
|
+
if (idx > 0) {
|
|
2980
|
+
inversion = idx;
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
if (inversion > 0) {
|
|
2984
|
+
const figure = SEVENTH_FIGURE_QUALITIES.has(chord.quality) ? SEVENTH_FIGURES[inversion] : TRIAD_FIGURE_QUALITIES.has(chord.quality) ? TRIAD_FIGURES[inversion] : void 0;
|
|
2985
|
+
if (figure !== void 0) {
|
|
2986
|
+
return `${accidental}${cased}${baseMarker(chord.quality)}${figure}`;
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
return `${accidental}${cased}${suffix}`;
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
// src/analyze/functional/function.ts
|
|
2993
|
+
var FUNCTION_BY_OFFSET = [
|
|
2994
|
+
"tonic",
|
|
2995
|
+
// 0 I
|
|
2996
|
+
"subdominant",
|
|
2997
|
+
// 1 bII (Neapolitan)
|
|
2998
|
+
"subdominant",
|
|
2999
|
+
// 2 ii
|
|
3000
|
+
"tonic",
|
|
3001
|
+
// 3 bIII
|
|
3002
|
+
"tonic",
|
|
3003
|
+
// 4 iii
|
|
3004
|
+
"subdominant",
|
|
3005
|
+
// 5 IV
|
|
3006
|
+
"dominant",
|
|
3007
|
+
// 6 #IV / bV
|
|
3008
|
+
"dominant",
|
|
3009
|
+
// 7 V
|
|
3010
|
+
"subdominant",
|
|
3011
|
+
// 8 bVI
|
|
3012
|
+
"tonic",
|
|
3013
|
+
// 9 vi
|
|
3014
|
+
"subdominant",
|
|
3015
|
+
// 10 bVII
|
|
3016
|
+
"dominant"
|
|
3017
|
+
// 11 vii
|
|
3018
|
+
];
|
|
3019
|
+
function isMinorKey(key) {
|
|
3020
|
+
const hasMinorThird = key.modeMask12 >> 3 & 1;
|
|
3021
|
+
const hasMajorThird = key.modeMask12 >> 4 & 1;
|
|
3022
|
+
return Boolean(hasMinorThird) && !hasMajorThird;
|
|
3023
|
+
}
|
|
3024
|
+
function functionOf(chord, key) {
|
|
3025
|
+
return FUNCTION_BY_OFFSET[mod12(chord.rootPc - key.rootPc)] ?? "tonic";
|
|
3026
|
+
}
|
|
3027
|
+
function isDiatonic(chord, key) {
|
|
3028
|
+
return chordPitchClasses(chord).every((pc) => isScaleTone(pc, key));
|
|
3029
|
+
}
|
|
3030
|
+
function parallelKey(key) {
|
|
3031
|
+
return {
|
|
3032
|
+
rootPc: mod12(key.rootPc),
|
|
3033
|
+
modeMask12: isMinorKey(key) ? MAJOR_MASK : NATURAL_MINOR_MASK
|
|
3034
|
+
};
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
// src/analyze/functional/borrowed.ts
|
|
3038
|
+
function isHarmonicMinorAlteration(chord, key) {
|
|
3039
|
+
return isMinorKey(key) && isDiatonic(chord, { rootPc: mod12(key.rootPc), modeMask12: HARMONIC_MINOR_MASK });
|
|
3040
|
+
}
|
|
3041
|
+
function isBorrowedChord(chord, key) {
|
|
3042
|
+
if (isDiatonic(chord, key)) {
|
|
3043
|
+
return false;
|
|
3044
|
+
}
|
|
3045
|
+
if (isHarmonicMinorAlteration(chord, key)) {
|
|
3046
|
+
return false;
|
|
3047
|
+
}
|
|
3048
|
+
return isDiatonic(chord, parallelKey(key));
|
|
3049
|
+
}
|
|
3050
|
+
function borrowedSource(chord, key) {
|
|
3051
|
+
if (isDiatonic(chord, key)) {
|
|
3052
|
+
return null;
|
|
3053
|
+
}
|
|
3054
|
+
if (isNeapolitan(chord, key)) {
|
|
3055
|
+
return "neapolitan";
|
|
3056
|
+
}
|
|
3057
|
+
if (isBorrowedChord(chord, key)) {
|
|
3058
|
+
return isMinorKey(key) ? "parallel-major" : "parallel-minor";
|
|
3059
|
+
}
|
|
3060
|
+
return null;
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
// src/theory/harmony/index.ts
|
|
3064
|
+
function roleOf(pitch, chord, chordId = 0) {
|
|
3065
|
+
const interval = ((Math.trunc(pitch) - chord.rootPc) % 12 + 12) % 12;
|
|
3066
|
+
const tones = new Set(chord.intervals.map((i) => (i % 12 + 12) % 12));
|
|
3067
|
+
const hasThird = tones.has(3) || tones.has(4);
|
|
3068
|
+
const isSuspendedTone = !hasThird && (interval === 5 && tones.has(5) || interval === 2 && tones.has(2));
|
|
3069
|
+
const chordRole = chordToneRole(pitch, chord);
|
|
3070
|
+
let role;
|
|
3071
|
+
let lock;
|
|
3072
|
+
if (isSuspendedTone) {
|
|
3073
|
+
role = "third";
|
|
3074
|
+
lock = "quality";
|
|
3075
|
+
} else if (chordRole !== null) {
|
|
3076
|
+
role = chordRole;
|
|
3077
|
+
lock = chordRole === "root" ? "identity" : chordRole === "third" ? "quality" : "voicing";
|
|
3078
|
+
} else {
|
|
3079
|
+
role = "tension";
|
|
3080
|
+
lock = "voicing";
|
|
3081
|
+
}
|
|
3082
|
+
return { role, lock, belongsToChordId: chordId };
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
// src/generate/harmonize/index.ts
|
|
3086
|
+
var FALLBACK = { rootPc: 0, quality: "maj", secondaryDominant: false, base: 0 };
|
|
3087
|
+
var DEFAULT_METER = { numerator: 4, denominator: 4 };
|
|
3088
|
+
var COMFORT_LOW = 55;
|
|
3089
|
+
var COMFORT_HIGH = 79;
|
|
3090
|
+
var TESSITURA_WEIGHT = 1e-3;
|
|
3091
|
+
var TIE_BREAK_JITTER = 1e-6;
|
|
3092
|
+
function pitchClass5(pitch) {
|
|
3093
|
+
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
3094
|
+
}
|
|
3095
|
+
function inferKey(melody) {
|
|
3096
|
+
let best = majorKey(0);
|
|
3097
|
+
let bestScore = Number.NEGATIVE_INFINITY;
|
|
3098
|
+
const candidates = [];
|
|
3099
|
+
for (let tonic = 0; tonic < 12; tonic += 1) {
|
|
3100
|
+
candidates.push(majorKey(tonic), minorKey(tonic));
|
|
3101
|
+
}
|
|
3102
|
+
for (const key of candidates) {
|
|
3103
|
+
const tonic = pitchClass5(key.rootPc);
|
|
3104
|
+
const isMinor = key.modeMask12 === NATURAL_MINOR_MASK;
|
|
3105
|
+
const leadingTone = (tonic + 11) % 12;
|
|
3106
|
+
let score = 0;
|
|
3107
|
+
for (const n of melody) {
|
|
3108
|
+
const w = Math.max(0.25, n.durationBeat);
|
|
3109
|
+
const pc = pitchClass5(n.pitch);
|
|
3110
|
+
if (isScaleTone(n.pitch, key) || isMinor && pc === leadingTone) {
|
|
3111
|
+
score += w;
|
|
3112
|
+
}
|
|
3113
|
+
if (pc === tonic) {
|
|
3114
|
+
score += 0.5 * w;
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
if (score > bestScore) {
|
|
3118
|
+
bestScore = score;
|
|
3119
|
+
best = key;
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
return best;
|
|
3123
|
+
}
|
|
3124
|
+
function buildCandidates(key, reharmonize) {
|
|
3125
|
+
const tones = scaleTonesInDegreeOrder(key);
|
|
3126
|
+
const candidates = tones.map((rootPc, degree) => ({
|
|
3127
|
+
rootPc,
|
|
3128
|
+
quality: diatonicTriad(degree, key).quality,
|
|
3129
|
+
degree,
|
|
3130
|
+
secondaryDominant: false,
|
|
3131
|
+
base: 0
|
|
3132
|
+
}));
|
|
3133
|
+
if (reharmonize !== "diatonic") {
|
|
3134
|
+
for (const target of [1, 3, 4, 5]) {
|
|
3135
|
+
const targetRoot = tones[target] ?? 0;
|
|
3136
|
+
candidates.push({
|
|
3137
|
+
rootPc: (targetRoot + 7) % 12,
|
|
2628
3138
|
quality: "dom7",
|
|
2629
3139
|
secondaryDominant: true,
|
|
2630
3140
|
targetDegree: target,
|
|
@@ -2633,26 +3143,29 @@ function buildCandidates(key, reharmonize) {
|
|
|
2633
3143
|
}
|
|
2634
3144
|
}
|
|
2635
3145
|
if (reharmonize === "borrowed") {
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
rootPc: (tonic + 5) % 12,
|
|
2650
|
-
quality: "min",
|
|
2651
|
-
secondaryDominant: false,
|
|
2652
|
-
base: 2
|
|
2653
|
-
});
|
|
3146
|
+
const parallel = parallelKey(key);
|
|
3147
|
+
const parallelTones = scaleTonesInDegreeOrder(parallel);
|
|
3148
|
+
for (let degree = 0; degree < parallelTones.length; degree += 1) {
|
|
3149
|
+
const chord = diatonicTriad(degree, parallel);
|
|
3150
|
+
if (!isDiatonic(chord, key)) {
|
|
3151
|
+
candidates.push({
|
|
3152
|
+
rootPc: chord.rootPc,
|
|
3153
|
+
quality: chord.quality,
|
|
3154
|
+
secondaryDominant: false,
|
|
3155
|
+
base: 1.25
|
|
3156
|
+
});
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
2654
3159
|
}
|
|
2655
|
-
|
|
3160
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3161
|
+
return candidates.filter((candidate) => {
|
|
3162
|
+
const id = `${candidate.rootPc}:${candidate.quality}`;
|
|
3163
|
+
if (seen.has(id)) {
|
|
3164
|
+
return false;
|
|
3165
|
+
}
|
|
3166
|
+
seen.add(id);
|
|
3167
|
+
return true;
|
|
3168
|
+
});
|
|
2656
3169
|
}
|
|
2657
3170
|
function emissionCost(seg, cand, melody, key) {
|
|
2658
3171
|
const pcs = chordPitchClasses(makeChord(cand.rootPc, cand.quality));
|
|
@@ -2756,20 +3269,37 @@ function harmonizeOnce(melody, key, candidates, segments, jitter) {
|
|
|
2756
3269
|
return { cost: bestCost, path };
|
|
2757
3270
|
}
|
|
2758
3271
|
function harmonizeMelody(opts) {
|
|
3272
|
+
const noteIndex = createNoteEventIndex(opts.melody);
|
|
2759
3273
|
const key = opts.key === "infer" ? inferKey(opts.melody) : opts.key;
|
|
2760
3274
|
const candidates = buildCandidates(key, opts.reharmonize);
|
|
2761
3275
|
const candAt = (i) => candidates[i] ?? FALLBACK;
|
|
2762
|
-
const rng =
|
|
2763
|
-
const jitter = candidates.map(() => rng() * TIE_BREAK_JITTER);
|
|
2764
|
-
const melodyEnd =
|
|
2765
|
-
const
|
|
3276
|
+
const rng = createRng(opts.seed ?? 0);
|
|
3277
|
+
const jitter = candidates.map(() => rng.next() * TIE_BREAK_JITTER);
|
|
3278
|
+
const melodyEnd = noteIndex.notes.reduce((m, n) => Math.max(m, n.endBeat), 0);
|
|
3279
|
+
const requestedHr = assertRange(
|
|
3280
|
+
opts.harmonicRhythm,
|
|
3281
|
+
Number.MIN_VALUE,
|
|
3282
|
+
Number.MAX_SAFE_INTEGER,
|
|
3283
|
+
"harmonic rhythm"
|
|
3284
|
+
);
|
|
3285
|
+
const hr = Math.max(0.25, requestedHr);
|
|
2766
3286
|
const segCount = Math.max(1, Math.ceil(melodyEnd / hr));
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
3287
|
+
assertGenerationBudget(segCount, "harmonic segments");
|
|
3288
|
+
assertGenerationBudget(segCount * candidates.length * candidates.length, "harmonization search");
|
|
3289
|
+
const segments = Array.from({ length: segCount }, (_, s) => ({
|
|
3290
|
+
startBeat: s * hr,
|
|
3291
|
+
endBeat: (s + 1) * hr,
|
|
3292
|
+
noteIndices: []
|
|
3293
|
+
}));
|
|
3294
|
+
let memberships = 0;
|
|
3295
|
+
for (const indexed of noteIndex.notes) {
|
|
3296
|
+
const first = Math.max(0, Math.floor(indexed.note.startBeat / hr));
|
|
3297
|
+
const lastExclusive = Math.min(segCount, Math.ceil(indexed.endBeat / hr - Number.EPSILON));
|
|
3298
|
+
memberships += Math.max(0, lastExclusive - first);
|
|
3299
|
+
assertGenerationBudget(memberships, "note-to-segment memberships");
|
|
3300
|
+
for (let segment = first; segment < lastExclusive; segment += 1) {
|
|
3301
|
+
segments[segment]?.noteIndices.push(indexed.originalIndex);
|
|
3302
|
+
}
|
|
2773
3303
|
}
|
|
2774
3304
|
const transposes = [0];
|
|
2775
3305
|
if (opts.placement.transposeSearch) {
|
|
@@ -2816,11 +3346,11 @@ function harmonizeMelody(opts) {
|
|
|
2816
3346
|
}
|
|
2817
3347
|
return chord;
|
|
2818
3348
|
});
|
|
2819
|
-
const melodyRoles = bestMelody.map((note,
|
|
3349
|
+
const melodyRoles = bestMelody.map((note, noteIndex2) => {
|
|
2820
3350
|
const segIdx = Math.min(segments.length - 1, Math.max(0, Math.floor(note.startBeat / hr)));
|
|
2821
3351
|
const cand = candAt(bestPath[segIdx] ?? 0);
|
|
2822
3352
|
const chord = makeChord(cand.rootPc, cand.quality);
|
|
2823
|
-
return { noteIndex, role: roleOf(note.pitch, chord).role };
|
|
3353
|
+
return { noteIndex: noteIndex2, role: roleOf(note.pitch, chord).role };
|
|
2824
3354
|
});
|
|
2825
3355
|
return { transposeSemitones: bestTs, key, chords, melodyRoles };
|
|
2826
3356
|
}
|
|
@@ -2867,29 +3397,30 @@ function nearestChordTone(pitch, chord) {
|
|
|
2867
3397
|
return pitch;
|
|
2868
3398
|
}
|
|
2869
3399
|
function cellOrigin(notes) {
|
|
2870
|
-
|
|
3400
|
+
let origin = Number.POSITIVE_INFINITY;
|
|
3401
|
+
for (const note of notes) {
|
|
3402
|
+
origin = Math.min(origin, note.startBeat);
|
|
3403
|
+
}
|
|
3404
|
+
return Number.isFinite(origin) ? origin : 0;
|
|
3405
|
+
}
|
|
3406
|
+
function cellEnd(notes) {
|
|
3407
|
+
let end = Number.NEGATIVE_INFINITY;
|
|
3408
|
+
for (const note of notes) {
|
|
3409
|
+
end = Math.max(end, note.startBeat + note.durationBeat);
|
|
3410
|
+
}
|
|
3411
|
+
return Number.isFinite(end) ? end : 0;
|
|
2871
3412
|
}
|
|
2872
3413
|
function cellSpan(cell) {
|
|
2873
3414
|
if (cell.notes.length === 0) {
|
|
2874
3415
|
return 0;
|
|
2875
3416
|
}
|
|
2876
3417
|
const start = cellOrigin(cell.notes);
|
|
2877
|
-
const end =
|
|
3418
|
+
const end = cellEnd(cell.notes);
|
|
2878
3419
|
return end - start;
|
|
2879
3420
|
}
|
|
2880
3421
|
function clone(cell) {
|
|
2881
3422
|
return { notes: cell.notes.map((n) => ({ ...n })) };
|
|
2882
3423
|
}
|
|
2883
|
-
function mulberry322(seed) {
|
|
2884
|
-
let state = seed >>> 0;
|
|
2885
|
-
return () => {
|
|
2886
|
-
state = state + 1831565813 >>> 0;
|
|
2887
|
-
let t = state;
|
|
2888
|
-
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
2889
|
-
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
2890
|
-
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
2891
|
-
};
|
|
2892
|
-
}
|
|
2893
3424
|
function contourOffsets(contour, count) {
|
|
2894
3425
|
const offsets = [];
|
|
2895
3426
|
for (let i = 0; i < count; i += 1) {
|
|
@@ -2912,19 +3443,21 @@ function contourOffsets(contour, count) {
|
|
|
2912
3443
|
}
|
|
2913
3444
|
function generateMotif(opts) {
|
|
2914
3445
|
const contour = opts.contour ?? "arch";
|
|
2915
|
-
const bars =
|
|
3446
|
+
const bars = assertPositiveInt(opts.bars, "motif bars");
|
|
2916
3447
|
const totalBeats = bars * 4;
|
|
2917
3448
|
const noteCount = Math.max(3, bars * 2);
|
|
3449
|
+
assertGenerationBudget(noteCount, "motif notes");
|
|
2918
3450
|
const beatsPerNote = totalBeats / noteCount;
|
|
2919
|
-
const rng =
|
|
2920
|
-
const
|
|
3451
|
+
const rng = createRng(opts.seed ?? 0);
|
|
3452
|
+
const requestedJitter = assertFiniteNumber(opts.jitter ?? 0, "motif jitter");
|
|
3453
|
+
const jitterProb = Math.min(1, Math.max(0, requestedJitter));
|
|
2921
3454
|
const tonic = pitchClass6(opts.key.rootPc) + 60;
|
|
2922
3455
|
const offsets = contourOffsets(contour, noteCount);
|
|
2923
3456
|
const notes = [];
|
|
2924
3457
|
for (let i = 0; i < noteCount; i += 1) {
|
|
2925
3458
|
let jitter = 0;
|
|
2926
|
-
if (jitterProb > 0 && rng() < jitterProb) {
|
|
2927
|
-
jitter = rng() < 0.5 ? 1 : -1;
|
|
3459
|
+
if (jitterProb > 0 && rng.next() < jitterProb) {
|
|
3460
|
+
jitter = rng.next() < 0.5 ? 1 : -1;
|
|
2928
3461
|
}
|
|
2929
3462
|
let pitch = stepDiatonic(tonic, (offsets[i] ?? 0) + jitter, opts.key);
|
|
2930
3463
|
const startBeat = i * beatsPerNote;
|
|
@@ -2936,6 +3469,16 @@ function generateMotif(opts) {
|
|
|
2936
3469
|
return { notes };
|
|
2937
3470
|
}
|
|
2938
3471
|
function transformMotif(cell, t, amount, key) {
|
|
3472
|
+
assertNoteEvents(cell.notes, "motif notes");
|
|
3473
|
+
if (amount !== void 0) {
|
|
3474
|
+
assertFiniteNumber(amount, "motif transform amount");
|
|
3475
|
+
}
|
|
3476
|
+
if ((t === "transposeDiatonic" || t === "sequence") && amount !== void 0) {
|
|
3477
|
+
assertInteger(amount, "diatonic transform amount");
|
|
3478
|
+
}
|
|
3479
|
+
if ((t === "augment" || t === "diminish") && amount !== void 0 && amount <= 0) {
|
|
3480
|
+
throw new RangeError("time transform amount must be positive");
|
|
3481
|
+
}
|
|
2939
3482
|
const notes = cell.notes;
|
|
2940
3483
|
switch (t) {
|
|
2941
3484
|
case "transposeChromatic": {
|
|
@@ -2962,7 +3505,7 @@ function transformMotif(cell, t, amount, key) {
|
|
|
2962
3505
|
return { notes: [] };
|
|
2963
3506
|
}
|
|
2964
3507
|
const start = cellOrigin(notes);
|
|
2965
|
-
const end =
|
|
3508
|
+
const end = cellEnd(notes);
|
|
2966
3509
|
return {
|
|
2967
3510
|
notes: notes.map((n) => ({
|
|
2968
3511
|
pitch: n.pitch,
|
|
@@ -2995,8 +3538,10 @@ function scaleTime(cell, factor) {
|
|
|
2995
3538
|
};
|
|
2996
3539
|
}
|
|
2997
3540
|
function developMotif(cell, timeline, key, bars) {
|
|
3541
|
+
assertPositiveInt(bars, "development bars");
|
|
3542
|
+
assertNoteEvents(cell.notes, "motif notes");
|
|
2998
3543
|
const span = cellSpan(cell);
|
|
2999
|
-
const totalBeats =
|
|
3544
|
+
const totalBeats = bars * 4;
|
|
3000
3545
|
const origin = cellOrigin(cell.notes);
|
|
3001
3546
|
const out = [];
|
|
3002
3547
|
if (span <= 0) {
|
|
@@ -3004,6 +3549,7 @@ function developMotif(cell, timeline, key, bars) {
|
|
|
3004
3549
|
}
|
|
3005
3550
|
const tileSpan = Math.max(span, MIN_TILE_SPAN);
|
|
3006
3551
|
const tileCount = Math.ceil(totalBeats / tileSpan);
|
|
3552
|
+
assertGenerationBudget(tileCount * cell.notes.length, "developed motif notes");
|
|
3007
3553
|
for (let k = 0; k < tileCount; k += 1) {
|
|
3008
3554
|
const offset = k * tileSpan;
|
|
3009
3555
|
for (const n of cell.notes) {
|
|
@@ -3202,16 +3748,6 @@ function progressions() {
|
|
|
3202
3748
|
function progressionsByStyle(style) {
|
|
3203
3749
|
return progressions().filter((p) => p.styles.includes(style));
|
|
3204
3750
|
}
|
|
3205
|
-
function mulberry323(seed) {
|
|
3206
|
-
let state = seed >>> 0;
|
|
3207
|
-
return () => {
|
|
3208
|
-
state = state + 1831565813 >>> 0;
|
|
3209
|
-
let t = state;
|
|
3210
|
-
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
3211
|
-
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
3212
|
-
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
3213
|
-
};
|
|
3214
|
-
}
|
|
3215
3751
|
function degreeToRootPc(degree, key) {
|
|
3216
3752
|
if (degree >= 0 && degree <= 6) {
|
|
3217
3753
|
const tones = scaleTonesInDegreeOrder(key);
|
|
@@ -3233,6 +3769,8 @@ function autoQuality(degree, key) {
|
|
|
3233
3769
|
return "maj";
|
|
3234
3770
|
}
|
|
3235
3771
|
function generateProgression(opts) {
|
|
3772
|
+
assertPositiveInt(opts.bars, "progression bars");
|
|
3773
|
+
assertGenerationBudget(opts.bars, "progression chords");
|
|
3236
3774
|
const seed = opts.seed ?? 0;
|
|
3237
3775
|
let preset;
|
|
3238
3776
|
if (opts.presetId !== void 0) {
|
|
@@ -3244,8 +3782,8 @@ function generateProgression(opts) {
|
|
|
3244
3782
|
if (preset === void 0) {
|
|
3245
3783
|
const pool = PRESETS.filter((p) => p.styles.includes(opts.style));
|
|
3246
3784
|
const candidates = pool.length > 0 ? pool : PRESETS;
|
|
3247
|
-
const rng =
|
|
3248
|
-
const index = Math.floor(rng() * candidates.length) % candidates.length;
|
|
3785
|
+
const rng = createRng(seed);
|
|
3786
|
+
const index = Math.floor(rng.next() * candidates.length) % candidates.length;
|
|
3249
3787
|
preset = candidates[index] ?? PRESETS[0];
|
|
3250
3788
|
}
|
|
3251
3789
|
const degrees = preset?.degrees ?? [0];
|
|
@@ -3263,7 +3801,7 @@ function generateProgression(opts) {
|
|
|
3263
3801
|
}
|
|
3264
3802
|
if (opts.reharmonize) {
|
|
3265
3803
|
const tonicPc = (opts.key.rootPc % 12 + 12) % 12;
|
|
3266
|
-
const rrng =
|
|
3804
|
+
const rrng = createRng((seed ^ 2654435769) >>> 0);
|
|
3267
3805
|
for (let i = 0; i < chords.length - 1; i += 1) {
|
|
3268
3806
|
const cur = chords[i];
|
|
3269
3807
|
const next = chords[i + 1];
|
|
@@ -3277,7 +3815,7 @@ function generateProgression(opts) {
|
|
|
3277
3815
|
if (targetsTonic || alreadySecondary || isResolutionTarget) {
|
|
3278
3816
|
continue;
|
|
3279
3817
|
}
|
|
3280
|
-
if (rrng() < 0.5) {
|
|
3818
|
+
if (rrng.next() < 0.5) {
|
|
3281
3819
|
chords[i] = {
|
|
3282
3820
|
rootPc: domRoot,
|
|
3283
3821
|
quality: "dom7",
|
|
@@ -3297,12 +3835,21 @@ function pitchClass7(value) {
|
|
|
3297
3835
|
function uniquePitchClasses(pitches) {
|
|
3298
3836
|
return [...new Set(pitches.map(pitchClass7))].sort((a, b) => a - b);
|
|
3299
3837
|
}
|
|
3300
|
-
function
|
|
3838
|
+
function assertPitches(pitches) {
|
|
3839
|
+
assertGenerationBudget(pitches.length, "detection pitches");
|
|
3840
|
+
for (let index = 0; index < pitches.length; index += 1) {
|
|
3841
|
+
assertFiniteNumber(pitches[index] ?? Number.NaN, `pitches[${index}]`);
|
|
3842
|
+
}
|
|
3843
|
+
}
|
|
3844
|
+
function detectChord(pitches, opts = {}) {
|
|
3845
|
+
assertPitches(pitches);
|
|
3846
|
+
if (opts.bassPc !== void 0) assertFiniteNumber(opts.bassPc, "bassPc");
|
|
3301
3847
|
const input = uniquePitchClasses(pitches);
|
|
3302
3848
|
if (input.length === 0) {
|
|
3303
3849
|
return [];
|
|
3304
3850
|
}
|
|
3305
|
-
const
|
|
3851
|
+
const inputKind = opts.input === void 0 || opts.input === "auto" ? pitches.every((pitch) => Number.isInteger(pitch) && pitch >= 0 && pitch <= 11) ? "pitchClass" : "midi" : opts.input;
|
|
3852
|
+
const bassPc = opts.bassPc !== void 0 ? pitchClass7(opts.bassPc) : inputKind === "midi" ? pitchClass7(pitches.reduce((lowest, pitch) => Math.min(lowest, pitch), Infinity)) : void 0;
|
|
3306
3853
|
const inputSet = new Set(input);
|
|
3307
3854
|
const matches = [];
|
|
3308
3855
|
const qualities = chordQualities();
|
|
@@ -3317,8 +3864,8 @@ function detectChord(pitches) {
|
|
|
3317
3864
|
continue;
|
|
3318
3865
|
}
|
|
3319
3866
|
const extraPcs = input.filter((pc) => !toneSet.has(pc));
|
|
3320
|
-
const bassIndex = chord.intervals.findIndex((iv) => pitchClass7(rootPc + iv) === bassPc);
|
|
3321
|
-
const inversion = bassIndex > 0 ? bassIndex : 0;
|
|
3867
|
+
const bassIndex = bassPc === void 0 ? -1 : chord.intervals.findIndex((iv) => pitchClass7(rootPc + iv) === bassPc);
|
|
3868
|
+
const inversion = bassPc === void 0 ? null : bassIndex > 0 ? bassIndex : 0;
|
|
3322
3869
|
const match = {
|
|
3323
3870
|
rootPc,
|
|
3324
3871
|
quality,
|
|
@@ -3327,7 +3874,7 @@ function detectChord(pitches) {
|
|
|
3327
3874
|
exact: extraPcs.length === 0 && missingPcs.length === 0,
|
|
3328
3875
|
inversion
|
|
3329
3876
|
};
|
|
3330
|
-
if (
|
|
3877
|
+
if (bassPc !== void 0 && bassPc !== rootPc) {
|
|
3331
3878
|
match.bassPc = bassPc;
|
|
3332
3879
|
}
|
|
3333
3880
|
matches.push(match);
|
|
@@ -3349,224 +3896,14 @@ function detectChord(pitches) {
|
|
|
3349
3896
|
});
|
|
3350
3897
|
return matches;
|
|
3351
3898
|
}
|
|
3352
|
-
function detectChordBest(pitches) {
|
|
3353
|
-
const best = detectChord(pitches)[0];
|
|
3899
|
+
function detectChordBest(pitches, opts = {}) {
|
|
3900
|
+
const best = detectChord(pitches, opts)[0];
|
|
3354
3901
|
if (!best) {
|
|
3355
3902
|
return null;
|
|
3356
3903
|
}
|
|
3357
3904
|
return makeChord(best.rootPc, best.quality, best.bassPc);
|
|
3358
3905
|
}
|
|
3359
3906
|
|
|
3360
|
-
// src/analyze/functional/internal.ts
|
|
3361
|
-
function mod12(n) {
|
|
3362
|
-
return (n % 12 + 12) % 12;
|
|
3363
|
-
}
|
|
3364
|
-
function isNeapolitan(chord, key) {
|
|
3365
|
-
return mod12(chord.rootPc - key.rootPc) === 1 && chord.quality === "maj";
|
|
3366
|
-
}
|
|
3367
|
-
|
|
3368
|
-
// src/analyze/functional/roman.ts
|
|
3369
|
-
var ROMAN = ["I", "II", "III", "IV", "V", "VI", "VII"];
|
|
3370
|
-
var OFFSET_SPELLING = [
|
|
3371
|
-
[1, ""],
|
|
3372
|
-
[2, "b"],
|
|
3373
|
-
[2, ""],
|
|
3374
|
-
[3, "b"],
|
|
3375
|
-
[3, ""],
|
|
3376
|
-
[4, ""],
|
|
3377
|
-
[4, "#"],
|
|
3378
|
-
[5, ""],
|
|
3379
|
-
[6, "b"],
|
|
3380
|
-
[6, ""],
|
|
3381
|
-
[7, "b"],
|
|
3382
|
-
[7, ""]
|
|
3383
|
-
];
|
|
3384
|
-
var ROMAN_STYLE = {
|
|
3385
|
-
maj: { lower: false, suffix: "" },
|
|
3386
|
-
min: { lower: true, suffix: "" },
|
|
3387
|
-
dim: { lower: true, suffix: "o" },
|
|
3388
|
-
aug: { lower: false, suffix: "+" },
|
|
3389
|
-
maj7: { lower: false, suffix: "maj7" },
|
|
3390
|
-
min7: { lower: true, suffix: "7" },
|
|
3391
|
-
dom7: { lower: false, suffix: "7" },
|
|
3392
|
-
dim7: { lower: true, suffix: "o7" },
|
|
3393
|
-
m7b5: { lower: true, suffix: "\xF87" },
|
|
3394
|
-
minMaj7: { lower: true, suffix: "maj7" },
|
|
3395
|
-
aug7: { lower: false, suffix: "+7" },
|
|
3396
|
-
augMaj7: { lower: false, suffix: "+maj7" },
|
|
3397
|
-
majb5: { lower: false, suffix: "b5" },
|
|
3398
|
-
"6": { lower: false, suffix: "add6" },
|
|
3399
|
-
min6: { lower: true, suffix: "add6" },
|
|
3400
|
-
"6/9": { lower: false, suffix: "69" },
|
|
3401
|
-
sus2: { lower: false, suffix: "sus2" },
|
|
3402
|
-
sus4: { lower: false, suffix: "sus4" },
|
|
3403
|
-
add9: { lower: false, suffix: "add9" },
|
|
3404
|
-
add11: { lower: false, suffix: "add11" },
|
|
3405
|
-
maj9: { lower: false, suffix: "maj9" },
|
|
3406
|
-
min9: { lower: true, suffix: "9" },
|
|
3407
|
-
dom9: { lower: false, suffix: "9" },
|
|
3408
|
-
"7b9": { lower: false, suffix: "7b9" },
|
|
3409
|
-
"7#9": { lower: false, suffix: "7#9" },
|
|
3410
|
-
"7#11": { lower: false, suffix: "7#11" },
|
|
3411
|
-
"7b13": { lower: false, suffix: "7b13" },
|
|
3412
|
-
"11": { lower: false, suffix: "11" },
|
|
3413
|
-
"13": { lower: false, suffix: "13" },
|
|
3414
|
-
"5": { lower: false, suffix: "5" }
|
|
3415
|
-
};
|
|
3416
|
-
var SUFFIX_QUALITY = new Map(
|
|
3417
|
-
Object.entries(ROMAN_STYLE).map(
|
|
3418
|
-
([quality, style]) => [`${style.lower ? "l" : "u"}:${style.suffix}`, quality]
|
|
3419
|
-
)
|
|
3420
|
-
);
|
|
3421
|
-
function romanStyle(quality) {
|
|
3422
|
-
return ROMAN_STYLE[quality];
|
|
3423
|
-
}
|
|
3424
|
-
function romanSpelling(rootPc, key) {
|
|
3425
|
-
const tones = scaleTonesInDegreeOrder(key);
|
|
3426
|
-
const diatonic = tones.indexOf(mod12(rootPc));
|
|
3427
|
-
if (diatonic >= 0) {
|
|
3428
|
-
return { degreeNumber: diatonic + 1, accidental: "" };
|
|
3429
|
-
}
|
|
3430
|
-
for (let i = 0; i < tones.length; i += 1) {
|
|
3431
|
-
const degreeNumber2 = i + 1;
|
|
3432
|
-
if (degreeNumber2 === 1 || degreeNumber2 === 5) {
|
|
3433
|
-
continue;
|
|
3434
|
-
}
|
|
3435
|
-
if (mod12((tones[i] ?? 0) - 1) === mod12(rootPc)) {
|
|
3436
|
-
return { degreeNumber: degreeNumber2, accidental: "b" };
|
|
3437
|
-
}
|
|
3438
|
-
}
|
|
3439
|
-
for (let i = 0; i < tones.length; i += 1) {
|
|
3440
|
-
if (mod12((tones[i] ?? 0) + 1) === mod12(rootPc)) {
|
|
3441
|
-
return { degreeNumber: i + 1, accidental: "#" };
|
|
3442
|
-
}
|
|
3443
|
-
}
|
|
3444
|
-
const [degreeNumber, accidental] = OFFSET_SPELLING[mod12(rootPc - key.rootPc)] ?? [1, ""];
|
|
3445
|
-
return { degreeNumber, accidental };
|
|
3446
|
-
}
|
|
3447
|
-
function baseMarker(quality) {
|
|
3448
|
-
if (quality === "dim" || quality === "dim7") {
|
|
3449
|
-
return "o";
|
|
3450
|
-
}
|
|
3451
|
-
if (quality === "m7b5") {
|
|
3452
|
-
return "\xF8";
|
|
3453
|
-
}
|
|
3454
|
-
if (quality === "aug" || quality === "aug7") {
|
|
3455
|
-
return "+";
|
|
3456
|
-
}
|
|
3457
|
-
if (quality === "augMaj7") {
|
|
3458
|
-
return "+maj7";
|
|
3459
|
-
}
|
|
3460
|
-
if (quality === "maj7" || quality === "minMaj7") {
|
|
3461
|
-
return "maj7";
|
|
3462
|
-
}
|
|
3463
|
-
return "";
|
|
3464
|
-
}
|
|
3465
|
-
var TRIAD_FIGURES = { 1: "6", 2: "64" };
|
|
3466
|
-
var SEVENTH_FIGURES = { 1: "65", 2: "43", 3: "42" };
|
|
3467
|
-
var TRIAD_FIGURE_QUALITIES = /* @__PURE__ */ new Set(["maj", "min", "dim", "aug"]);
|
|
3468
|
-
var SEVENTH_FIGURE_QUALITIES = /* @__PURE__ */ new Set([
|
|
3469
|
-
"maj7",
|
|
3470
|
-
"min7",
|
|
3471
|
-
"dom7",
|
|
3472
|
-
"dim7",
|
|
3473
|
-
"m7b5",
|
|
3474
|
-
"minMaj7",
|
|
3475
|
-
"aug7",
|
|
3476
|
-
"augMaj7"
|
|
3477
|
-
]);
|
|
3478
|
-
function chordToRoman(chord, key) {
|
|
3479
|
-
const { degreeNumber, accidental } = romanSpelling(chord.rootPc, key);
|
|
3480
|
-
const { lower, suffix } = romanStyle(chord.quality);
|
|
3481
|
-
const numeral = ROMAN[degreeNumber - 1] ?? "I";
|
|
3482
|
-
const cased = lower ? numeral.toLowerCase() : numeral;
|
|
3483
|
-
let inversion = 0;
|
|
3484
|
-
if (chord.bassPc !== void 0) {
|
|
3485
|
-
const idx = chord.intervals.findIndex((iv) => mod12(chord.rootPc + iv) === chord.bassPc);
|
|
3486
|
-
if (idx > 0) {
|
|
3487
|
-
inversion = idx;
|
|
3488
|
-
}
|
|
3489
|
-
}
|
|
3490
|
-
if (inversion > 0) {
|
|
3491
|
-
const figure = SEVENTH_FIGURE_QUALITIES.has(chord.quality) ? SEVENTH_FIGURES[inversion] : TRIAD_FIGURE_QUALITIES.has(chord.quality) ? TRIAD_FIGURES[inversion] : void 0;
|
|
3492
|
-
if (figure !== void 0) {
|
|
3493
|
-
return `${accidental}${cased}${baseMarker(chord.quality)}${figure}`;
|
|
3494
|
-
}
|
|
3495
|
-
}
|
|
3496
|
-
return `${accidental}${cased}${suffix}`;
|
|
3497
|
-
}
|
|
3498
|
-
|
|
3499
|
-
// src/analyze/functional/function.ts
|
|
3500
|
-
var FUNCTION_BY_OFFSET = [
|
|
3501
|
-
"tonic",
|
|
3502
|
-
// 0 I
|
|
3503
|
-
"subdominant",
|
|
3504
|
-
// 1 bII (Neapolitan)
|
|
3505
|
-
"subdominant",
|
|
3506
|
-
// 2 ii
|
|
3507
|
-
"tonic",
|
|
3508
|
-
// 3 bIII
|
|
3509
|
-
"tonic",
|
|
3510
|
-
// 4 iii
|
|
3511
|
-
"subdominant",
|
|
3512
|
-
// 5 IV
|
|
3513
|
-
"dominant",
|
|
3514
|
-
// 6 #IV / bV
|
|
3515
|
-
"dominant",
|
|
3516
|
-
// 7 V
|
|
3517
|
-
"subdominant",
|
|
3518
|
-
// 8 bVI
|
|
3519
|
-
"tonic",
|
|
3520
|
-
// 9 vi
|
|
3521
|
-
"subdominant",
|
|
3522
|
-
// 10 bVII
|
|
3523
|
-
"dominant"
|
|
3524
|
-
// 11 vii
|
|
3525
|
-
];
|
|
3526
|
-
function isMinorKey(key) {
|
|
3527
|
-
const hasMinorThird = key.modeMask12 >> 3 & 1;
|
|
3528
|
-
const hasMajorThird = key.modeMask12 >> 4 & 1;
|
|
3529
|
-
return Boolean(hasMinorThird) && !hasMajorThird;
|
|
3530
|
-
}
|
|
3531
|
-
function functionOf(chord, key) {
|
|
3532
|
-
return FUNCTION_BY_OFFSET[mod12(chord.rootPc - key.rootPc)] ?? "tonic";
|
|
3533
|
-
}
|
|
3534
|
-
function isDiatonic(chord, key) {
|
|
3535
|
-
return chordPitchClasses(chord).every((pc) => isScaleTone(pc, key));
|
|
3536
|
-
}
|
|
3537
|
-
function parallelKey(key) {
|
|
3538
|
-
return {
|
|
3539
|
-
rootPc: mod12(key.rootPc),
|
|
3540
|
-
modeMask12: isMinorKey(key) ? MAJOR_MASK : NATURAL_MINOR_MASK
|
|
3541
|
-
};
|
|
3542
|
-
}
|
|
3543
|
-
|
|
3544
|
-
// src/analyze/functional/borrowed.ts
|
|
3545
|
-
function isHarmonicMinorAlteration(chord, key) {
|
|
3546
|
-
return isMinorKey(key) && isDiatonic(chord, { rootPc: mod12(key.rootPc), modeMask12: HARMONIC_MINOR_MASK });
|
|
3547
|
-
}
|
|
3548
|
-
function isBorrowedChord(chord, key) {
|
|
3549
|
-
if (isDiatonic(chord, key)) {
|
|
3550
|
-
return false;
|
|
3551
|
-
}
|
|
3552
|
-
if (isHarmonicMinorAlteration(chord, key)) {
|
|
3553
|
-
return false;
|
|
3554
|
-
}
|
|
3555
|
-
return isDiatonic(chord, parallelKey(key));
|
|
3556
|
-
}
|
|
3557
|
-
function borrowedSource(chord, key) {
|
|
3558
|
-
if (isDiatonic(chord, key)) {
|
|
3559
|
-
return null;
|
|
3560
|
-
}
|
|
3561
|
-
if (isNeapolitan(chord, key)) {
|
|
3562
|
-
return "neapolitan";
|
|
3563
|
-
}
|
|
3564
|
-
if (isBorrowedChord(chord, key)) {
|
|
3565
|
-
return isMinorKey(key) ? "parallel-major" : "parallel-minor";
|
|
3566
|
-
}
|
|
3567
|
-
return null;
|
|
3568
|
-
}
|
|
3569
|
-
|
|
3570
3907
|
// src/generate/reharmony/index.ts
|
|
3571
3908
|
function mod122(n) {
|
|
3572
3909
|
return (n % 12 + 12) % 12;
|
|
@@ -3582,11 +3919,6 @@ function isStableTarget(chord) {
|
|
|
3582
3919
|
const hasThird = hasInterval(chord, 3) || hasInterval(chord, 4);
|
|
3583
3920
|
return hasFifth && hasThird && !isDominantType(chord);
|
|
3584
3921
|
}
|
|
3585
|
-
function triadPitchClasses(chord) {
|
|
3586
|
-
const third = hasInterval(chord, 4) ? 4 : hasInterval(chord, 3) ? 3 : 4;
|
|
3587
|
-
const fifth = hasInterval(chord, 8) ? 8 : hasInterval(chord, 6) ? 6 : 7;
|
|
3588
|
-
return [mod122(chord.rootPc), mod122(chord.rootPc + third), mod122(chord.rootPc + fifth)];
|
|
3589
|
-
}
|
|
3590
3922
|
function commonToneCount(a, b) {
|
|
3591
3923
|
const set = new Set(b);
|
|
3592
3924
|
return a.filter((pc) => set.has(pc)).length;
|
|
@@ -3604,7 +3936,7 @@ function sameChord(a, b) {
|
|
|
3604
3936
|
}
|
|
3605
3937
|
var THIRD_OFFSETS = [3, 4, 8, 9];
|
|
3606
3938
|
function substituteChord(chord, key, opts) {
|
|
3607
|
-
const original =
|
|
3939
|
+
const original = chordPitchClasses(chord);
|
|
3608
3940
|
const candidates = [];
|
|
3609
3941
|
if (isDominantType(chord)) {
|
|
3610
3942
|
candidates.push({ chord: makeChord(mod122(chord.rootPc + 6), "dom7"), type: "tritone" });
|
|
@@ -3725,11 +4057,15 @@ function generateRhythm(ts, opts = {}) {
|
|
|
3725
4057
|
const seed = opts.seed ?? 0;
|
|
3726
4058
|
const bars = opts.bars ?? DEFAULT_BARS;
|
|
3727
4059
|
const subdivision = opts.subdivision ?? DEFAULT_SUBDIVISION;
|
|
3728
|
-
const
|
|
4060
|
+
const requestedDensity = assertFiniteNumber(opts.density ?? DEFAULT_DENSITY, "rhythm density");
|
|
4061
|
+
const density = Math.min(1, Math.max(0, requestedDensity));
|
|
4062
|
+
assertPositiveInt(bars, "rhythm bars");
|
|
4063
|
+
assertPositiveInt(subdivision, "rhythm subdivision");
|
|
3729
4064
|
const barBeats = beatsPerBar(ts);
|
|
3730
4065
|
const spanBeats = barBeats * bars;
|
|
3731
4066
|
const step = 1 / subdivision;
|
|
3732
4067
|
const slotCount = Math.round(spanBeats * subdivision);
|
|
4068
|
+
assertGenerationBudget(slotCount, "rhythm grid slots");
|
|
3733
4069
|
const slotsPerBar = Math.round(barBeats * subdivision);
|
|
3734
4070
|
const rng = createRng(seed);
|
|
3735
4071
|
const positions = [];
|