@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/index.cjs
CHANGED
|
@@ -23,6 +23,7 @@ __export(index_exports, {
|
|
|
23
23
|
BLUES_MASK: () => BLUES_MASK,
|
|
24
24
|
CHROMATIC_MASK: () => CHROMATIC_MASK,
|
|
25
25
|
Chord: () => Chord,
|
|
26
|
+
DEFAULT_GENERATION_BUDGET: () => DEFAULT_GENERATION_BUDGET,
|
|
26
27
|
DORIAN_MASK: () => DORIAN_MASK,
|
|
27
28
|
HARMONIC_MINOR_MASK: () => HARMONIC_MINOR_MASK,
|
|
28
29
|
Interval: () => Interval,
|
|
@@ -52,6 +53,14 @@ __export(index_exports, {
|
|
|
52
53
|
analyzeChord: () => analyzeChord,
|
|
53
54
|
analyzeVoice: () => analyzeVoice,
|
|
54
55
|
applyGrooveTemplate: () => applyGrooveTemplate,
|
|
56
|
+
assertFiniteNumber: () => assertFiniteNumber,
|
|
57
|
+
assertGenerationBudget: () => assertGenerationBudget,
|
|
58
|
+
assertInteger: () => assertInteger,
|
|
59
|
+
assertNoteEvent: () => assertNoteEvent,
|
|
60
|
+
assertNoteEvents: () => assertNoteEvents,
|
|
61
|
+
assertPositiveInt: () => assertPositiveInt,
|
|
62
|
+
assertRange: () => assertRange,
|
|
63
|
+
assertTimeSignature: () => assertTimeSignature,
|
|
55
64
|
availableTensions: () => availableTensions,
|
|
56
65
|
avoidNotes: () => avoidNotes,
|
|
57
66
|
barPositionToBeat: () => barPositionToBeat,
|
|
@@ -70,6 +79,7 @@ __export(index_exports, {
|
|
|
70
79
|
chordToRoman: () => chordToRoman,
|
|
71
80
|
chordToneRole: () => chordToneRole,
|
|
72
81
|
classifyInterval: () => classifyInterval,
|
|
82
|
+
createNoteEventIndex: () => createNoteEventIndex,
|
|
73
83
|
createRng: () => createRng,
|
|
74
84
|
createsHiddenParallelPerfect: () => createsHiddenParallelPerfect,
|
|
75
85
|
createsParallelOctave: () => createsParallelOctave,
|
|
@@ -165,6 +175,86 @@ __export(index_exports, {
|
|
|
165
175
|
});
|
|
166
176
|
module.exports = __toCommonJS(index_exports);
|
|
167
177
|
|
|
178
|
+
// src/core/validation/index.ts
|
|
179
|
+
var DEFAULT_GENERATION_BUDGET = 1e6;
|
|
180
|
+
function assertFiniteNumber(value, name) {
|
|
181
|
+
if (!Number.isFinite(value)) {
|
|
182
|
+
throw new RangeError(`${name} must be finite; received ${value}`);
|
|
183
|
+
}
|
|
184
|
+
return value;
|
|
185
|
+
}
|
|
186
|
+
function assertInteger(value, name, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) {
|
|
187
|
+
assertFiniteNumber(value, name);
|
|
188
|
+
if (!Number.isSafeInteger(value) || value < min || value > max) {
|
|
189
|
+
throw new RangeError(`${name} must be an integer in [${min}, ${max}]; received ${value}`);
|
|
190
|
+
}
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
193
|
+
function assertPositiveInt(value, name, max = DEFAULT_GENERATION_BUDGET) {
|
|
194
|
+
return assertInteger(value, name, 1, max);
|
|
195
|
+
}
|
|
196
|
+
function assertRange(value, min, max, name) {
|
|
197
|
+
assertFiniteNumber(value, name);
|
|
198
|
+
if (value < min || value > max) {
|
|
199
|
+
throw new RangeError(`${name} must be in [${min}, ${max}]; received ${value}`);
|
|
200
|
+
}
|
|
201
|
+
return value;
|
|
202
|
+
}
|
|
203
|
+
function assertGenerationBudget(estimated, name, limit = DEFAULT_GENERATION_BUDGET) {
|
|
204
|
+
assertFiniteNumber(estimated, name);
|
|
205
|
+
assertPositiveInt(limit, `${name} limit`, Number.MAX_SAFE_INTEGER);
|
|
206
|
+
if (estimated < 0 || estimated > limit) {
|
|
207
|
+
throw new RangeError(`${name} exceeds the generation budget ${limit}; received ${estimated}`);
|
|
208
|
+
}
|
|
209
|
+
return estimated;
|
|
210
|
+
}
|
|
211
|
+
function assertTimeSignature(ts, name = "time signature") {
|
|
212
|
+
assertPositiveInt(ts.numerator, `${name}.numerator`);
|
|
213
|
+
assertPositiveInt(ts.denominator, `${name}.denominator`);
|
|
214
|
+
if (ts.grouping !== void 0) {
|
|
215
|
+
if (ts.grouping.length === 0) {
|
|
216
|
+
throw new RangeError(`${name}.grouping must not be empty`);
|
|
217
|
+
}
|
|
218
|
+
let sum = 0;
|
|
219
|
+
for (let index = 0; index < ts.grouping.length; index += 1) {
|
|
220
|
+
sum += assertPositiveInt(ts.grouping[index] ?? Number.NaN, `${name}.grouping[${index}]`);
|
|
221
|
+
}
|
|
222
|
+
const compound = ts.numerator % 3 === 0 && ts.numerator > 3;
|
|
223
|
+
const pulses = compound ? ts.numerator / 3 : ts.numerator;
|
|
224
|
+
if (sum !== pulses) {
|
|
225
|
+
throw new RangeError(`${name}.grouping must sum to ${pulses}; received ${sum}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return ts;
|
|
229
|
+
}
|
|
230
|
+
function assertNoteEvent(event, name = "note event", options = {}) {
|
|
231
|
+
assertFiniteNumber(event.pitch, `${name}.pitch`);
|
|
232
|
+
assertRange(event.startBeat, 0, Number.MAX_SAFE_INTEGER, `${name}.startBeat`);
|
|
233
|
+
assertRange(
|
|
234
|
+
event.durationBeat,
|
|
235
|
+
options.allowNonPositiveDuration ? -Number.MAX_SAFE_INTEGER : Number.MIN_VALUE,
|
|
236
|
+
Number.MAX_SAFE_INTEGER,
|
|
237
|
+
`${name}.durationBeat`
|
|
238
|
+
);
|
|
239
|
+
if (!options.allowNonPositiveDuration && event.durationBeat <= 0) {
|
|
240
|
+
throw new RangeError(`${name}.durationBeat must be positive; received ${event.durationBeat}`);
|
|
241
|
+
}
|
|
242
|
+
if (event.velocity !== void 0) {
|
|
243
|
+
assertRange(event.velocity, 0, 127, `${name}.velocity`);
|
|
244
|
+
}
|
|
245
|
+
return event;
|
|
246
|
+
}
|
|
247
|
+
function assertNoteEvents(events, name = "note events", options = {}) {
|
|
248
|
+
assertGenerationBudget(events.length, `${name} count`, options.budget);
|
|
249
|
+
for (let index = 0; index < events.length; index += 1) {
|
|
250
|
+
const event = events[index];
|
|
251
|
+
if (event !== void 0) {
|
|
252
|
+
assertNoteEvent(event, `${name}[${index}]`, options);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return events;
|
|
256
|
+
}
|
|
257
|
+
|
|
168
258
|
// src/core/meter/index.ts
|
|
169
259
|
var EPS = 1e-9;
|
|
170
260
|
function isMultiple(value, unit) {
|
|
@@ -181,21 +271,24 @@ function parseTimeSignature(text) {
|
|
|
181
271
|
}
|
|
182
272
|
const numerator = Number.parseInt(match[1] ?? "", 10);
|
|
183
273
|
const denominator = Number.parseInt(match[2] ?? "", 10);
|
|
184
|
-
if (numerator <= 0 || denominator <= 0) {
|
|
274
|
+
if (!Number.isSafeInteger(numerator) || !Number.isSafeInteger(denominator) || numerator <= 0 || denominator <= 0) {
|
|
185
275
|
throw new Error(`Invalid time signature: ${text}`);
|
|
186
276
|
}
|
|
187
277
|
return { numerator, denominator };
|
|
188
278
|
}
|
|
189
279
|
function formatTimeSignature(ts) {
|
|
280
|
+
assertTimeSignature(ts);
|
|
190
281
|
return `${ts.numerator}/${ts.denominator}`;
|
|
191
282
|
}
|
|
192
283
|
function isCompound(ts) {
|
|
284
|
+
assertTimeSignature(ts);
|
|
193
285
|
return ts.numerator % 3 === 0 && ts.numerator > 3;
|
|
194
286
|
}
|
|
195
287
|
function unitBeats(ts) {
|
|
196
288
|
return 4 / ts.denominator;
|
|
197
289
|
}
|
|
198
290
|
function beatsPerBar(ts) {
|
|
291
|
+
assertTimeSignature(ts);
|
|
199
292
|
return ts.numerator * unitBeats(ts);
|
|
200
293
|
}
|
|
201
294
|
function pulseBeats(ts) {
|
|
@@ -228,14 +321,19 @@ function isGroupHead(grouping, pulseIndex) {
|
|
|
228
321
|
return false;
|
|
229
322
|
}
|
|
230
323
|
function beatToBarPosition(beatInQuarters, ts) {
|
|
324
|
+
assertFiniteNumber(beatInQuarters, "beat");
|
|
231
325
|
const barLen = beatsPerBar(ts);
|
|
232
326
|
const bar = Math.floor(beatInQuarters / barLen);
|
|
233
327
|
return { bar, beat: beatInQuarters - bar * barLen };
|
|
234
328
|
}
|
|
235
329
|
function barPositionToBeat(pos, ts) {
|
|
330
|
+
assertInteger(pos.bar, "bar position bar");
|
|
331
|
+
assertFiniteNumber(pos.beat, "bar position beat");
|
|
236
332
|
return pos.bar * beatsPerBar(ts) + pos.beat;
|
|
237
333
|
}
|
|
238
334
|
function metricWeight(beatInQuarters, ts) {
|
|
335
|
+
assertFiniteNumber(beatInQuarters, "beat");
|
|
336
|
+
assertTimeSignature(ts);
|
|
239
337
|
const { beat } = beatToBarPosition(beatInQuarters, ts);
|
|
240
338
|
const pulse = pulseBeats(ts);
|
|
241
339
|
if (!isMultiple(beat, pulse)) {
|
|
@@ -264,9 +362,8 @@ function isStrongBeat(beatInQuarters, ts) {
|
|
|
264
362
|
return metricWeight(beatInQuarters, ts) >= 2;
|
|
265
363
|
}
|
|
266
364
|
function tuplet(totalBeats, count) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
365
|
+
assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, "tuplet total beats");
|
|
366
|
+
assertPositiveInt(count, "tuplet count");
|
|
270
367
|
return new Array(count).fill(totalBeats / count);
|
|
271
368
|
}
|
|
272
369
|
|
|
@@ -531,8 +628,10 @@ function classifySeventh(thirdIc, fifthIc, seventhIc) {
|
|
|
531
628
|
function stackThirds(degree, key, size) {
|
|
532
629
|
const tones = scaleTonesInDegreeOrder(key);
|
|
533
630
|
const length = tones.length;
|
|
534
|
-
if (length
|
|
535
|
-
|
|
631
|
+
if (length !== 7) {
|
|
632
|
+
throw new Error(
|
|
633
|
+
`diatonic chord stacking requires a heptatonic scale (received ${length} tones)`
|
|
634
|
+
);
|
|
536
635
|
}
|
|
537
636
|
const idx = (step) => ((degree + step) % length + length) % length;
|
|
538
637
|
const rootPc = tones[idx(0)] ?? pitchClass2(key.rootPc);
|
|
@@ -693,6 +792,31 @@ var ReasonFlag = /* @__PURE__ */ ((ReasonFlag2) => {
|
|
|
693
792
|
})(ReasonFlag || {});
|
|
694
793
|
var SUGGESTION_WINDOW = 12;
|
|
695
794
|
var MAX_SUGGESTIONS = 3;
|
|
795
|
+
var REASON_SEVERITY = [
|
|
796
|
+
[2 /* Tension */, 1 /* Warning */, 1 /* Warning */],
|
|
797
|
+
[4 /* AvoidNote */, 1 /* Warning */, 2 /* Dissonant */],
|
|
798
|
+
[8 /* ScaleTone */, 1 /* Warning */, 1 /* Warning */],
|
|
799
|
+
[16 /* NonScale */, 2 /* Dissonant */, 2 /* Dissonant */],
|
|
800
|
+
[32 /* OutOfRange */, 1 /* Warning */, 1 /* Warning */],
|
|
801
|
+
[64 /* Tritone */, 2 /* Dissonant */, 2 /* Dissonant */],
|
|
802
|
+
[128 /* LargeLeap */, 1 /* Warning */, 2 /* Dissonant */],
|
|
803
|
+
[1024 /* VerticalDissonance */, 2 /* Dissonant */, 2 /* Dissonant */],
|
|
804
|
+
[2048 /* ParallelPerfect */, 1 /* Warning */, 2 /* Dissonant */],
|
|
805
|
+
[4096 /* HiddenParallel */, 1 /* Warning */, 2 /* Dissonant */],
|
|
806
|
+
[8192 /* VoiceCrossing */, 1 /* Warning */, 2 /* Dissonant */],
|
|
807
|
+
[16384 /* Suspension */, 2 /* Dissonant */, 2 /* Dissonant */],
|
|
808
|
+
[32768 /* NeedsResolution */, 1 /* Warning */, 2 /* Dissonant */]
|
|
809
|
+
];
|
|
810
|
+
function minimumSafetyForReasons(reasons, profile) {
|
|
811
|
+
let minimum = 0 /* Safe */;
|
|
812
|
+
const severityIndex = profile === "strict" ? 2 : 1;
|
|
813
|
+
for (const entry of REASON_SEVERITY) {
|
|
814
|
+
if (reasons & entry[0]) {
|
|
815
|
+
minimum = Math.max(minimum, entry[severityIndex]);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
return minimum;
|
|
819
|
+
}
|
|
696
820
|
function pitchClass4(pitch) {
|
|
697
821
|
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
698
822
|
}
|
|
@@ -717,8 +841,27 @@ function stepResolution(pitch, chord) {
|
|
|
717
841
|
return void 0;
|
|
718
842
|
}
|
|
719
843
|
function evaluateSafety(q) {
|
|
844
|
+
assertSafetyContext(q);
|
|
845
|
+
assertFiniteNumber(q.candidatePitch, "candidatePitch");
|
|
720
846
|
return evaluateInternal(q, true);
|
|
721
847
|
}
|
|
848
|
+
function assertSafetyContext(q) {
|
|
849
|
+
if (q.prevPitch !== void 0) assertFiniteNumber(q.prevPitch, "prevPitch");
|
|
850
|
+
if (q.vocalLow !== void 0) assertFiniteNumber(q.vocalLow, "vocalLow");
|
|
851
|
+
if (q.vocalHigh !== void 0) assertFiniteNumber(q.vocalHigh, "vocalHigh");
|
|
852
|
+
if (q.vocalLow !== void 0 && q.vocalHigh !== void 0 && q.vocalLow > q.vocalHigh) {
|
|
853
|
+
throw new RangeError("vocalLow must not exceed vocalHigh");
|
|
854
|
+
}
|
|
855
|
+
assertGenerationBudget(q.otherVoices.length, "other voices");
|
|
856
|
+
for (let index = 0; index < q.otherVoices.length; index += 1) {
|
|
857
|
+
const voice = q.otherVoices[index];
|
|
858
|
+
if (!voice) continue;
|
|
859
|
+
assertFiniteNumber(voice.pitch, `otherVoices[${index}].pitch`);
|
|
860
|
+
if (voice.prevPitch !== void 0) {
|
|
861
|
+
assertFiniteNumber(voice.prevPitch, `otherVoices[${index}].prevPitch`);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
722
865
|
function evaluateInternal(q, collectSuggestions) {
|
|
723
866
|
let reasons = 0;
|
|
724
867
|
let level = 0 /* Safe */;
|
|
@@ -779,9 +922,11 @@ function evaluateInternal(q, collectSuggestions) {
|
|
|
779
922
|
const pc = Math.abs(pitch - q.prevPitch) % 12;
|
|
780
923
|
if (pc === 6) {
|
|
781
924
|
reasons |= 64 /* Tritone */;
|
|
925
|
+
raise(2 /* Dissonant */);
|
|
782
926
|
}
|
|
783
927
|
if (isForbiddenMelodicLeap(q.prevPitch, pitch)) {
|
|
784
928
|
reasons |= 128 /* LargeLeap */;
|
|
929
|
+
raise(parallelSeverity);
|
|
785
930
|
}
|
|
786
931
|
if (pc === 1) {
|
|
787
932
|
reasons |= 256 /* MinorSecond */;
|
|
@@ -826,6 +971,7 @@ function evaluateInternal(q, collectSuggestions) {
|
|
|
826
971
|
}
|
|
827
972
|
}
|
|
828
973
|
}
|
|
974
|
+
raise(minimumSafetyForReasons(reasons, q.profile));
|
|
829
975
|
const result = { safety: level, reasons, rationale: describe(reasons, q) };
|
|
830
976
|
if (resolveTo !== void 0) {
|
|
831
977
|
result.resolveTo = resolveTo;
|
|
@@ -889,11 +1035,15 @@ function describe(reasons, q) {
|
|
|
889
1035
|
return q.chord ? "Placeable" : "No chord context";
|
|
890
1036
|
}
|
|
891
1037
|
function enumerateSafePitches(q, pitchLow, pitchHigh) {
|
|
1038
|
+
assertSafetyContext(q);
|
|
892
1039
|
const chordTones = [];
|
|
893
1040
|
const others = [];
|
|
894
|
-
|
|
895
|
-
|
|
1041
|
+
assertInteger(pitchLow, "pitchLow");
|
|
1042
|
+
assertInteger(pitchHigh, "pitchHigh");
|
|
1043
|
+
if (pitchLow > pitchHigh) {
|
|
1044
|
+
throw new RangeError(`pitchLow must not exceed pitchHigh; received ${pitchLow} > ${pitchHigh}`);
|
|
896
1045
|
}
|
|
1046
|
+
assertGenerationBudget(pitchHigh - pitchLow + 1, "safe pitch candidates");
|
|
897
1047
|
for (let pitch = pitchHigh; pitch >= pitchLow; pitch -= 1) {
|
|
898
1048
|
const result = evaluateInternal({ ...q, candidatePitch: pitch }, false);
|
|
899
1049
|
if (result.safety === 2 /* Dissonant */) {
|
|
@@ -1037,11 +1187,16 @@ function parseSimpleRoman(text, key) {
|
|
|
1037
1187
|
if (canonical !== void 0) {
|
|
1038
1188
|
return { rootPc, quality: canonical, inversion: 0 };
|
|
1039
1189
|
}
|
|
1040
|
-
const
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
const
|
|
1190
|
+
const parsedSuffix = /^(o|°|dim|ø|\+|aug)?(maj7|M7)?(65|64|43|42|7|6|2)?$/.exec(suffix);
|
|
1191
|
+
if (!parsedSuffix) {
|
|
1192
|
+
throw new Error(`Unsupported suffix "${suffix}" in Roman numeral: ${text}`);
|
|
1193
|
+
}
|
|
1194
|
+
const qualityMarker = parsedSuffix[1] ?? "";
|
|
1195
|
+
const isDim = qualityMarker === "o" || qualityMarker === "\xB0" || qualityMarker === "dim";
|
|
1196
|
+
const isAug = qualityMarker === "+" || qualityMarker === "aug";
|
|
1197
|
+
const isHalfDim = qualityMarker === "\xF8";
|
|
1198
|
+
const explicitMaj7 = parsedSuffix[2] !== void 0;
|
|
1199
|
+
const figures = parsedSuffix[3] ?? "";
|
|
1045
1200
|
if (figures !== "" && !INVERSION_FIGURES.has(figures)) {
|
|
1046
1201
|
throw new Error(`Unsupported figured-bass or extension "${figures}" in Roman numeral: ${text}`);
|
|
1047
1202
|
}
|
|
@@ -1290,6 +1445,73 @@ function detectCadence(from, to, key) {
|
|
|
1290
1445
|
return null;
|
|
1291
1446
|
}
|
|
1292
1447
|
|
|
1448
|
+
// src/core/event-index/index.ts
|
|
1449
|
+
var EPS2 = 1e-9;
|
|
1450
|
+
function upperBound(values, beat) {
|
|
1451
|
+
let low = 0;
|
|
1452
|
+
let high = values.length;
|
|
1453
|
+
while (low < high) {
|
|
1454
|
+
const middle = Math.floor((low + high) / 2);
|
|
1455
|
+
if ((values[middle]?.note.startBeat ?? Number.POSITIVE_INFINITY) <= beat) {
|
|
1456
|
+
low = middle + 1;
|
|
1457
|
+
} else {
|
|
1458
|
+
high = middle;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
return low;
|
|
1462
|
+
}
|
|
1463
|
+
function createNoteEventIndex(events, options = {}) {
|
|
1464
|
+
assertNoteEvents(events, "note events", options);
|
|
1465
|
+
const notes = events.map((note, originalIndex) => ({
|
|
1466
|
+
note,
|
|
1467
|
+
originalIndex,
|
|
1468
|
+
endBeat: note.startBeat + note.durationBeat
|
|
1469
|
+
})).sort((a, b) => a.note.startBeat - b.note.startBeat || a.originalIndex - b.originalIndex);
|
|
1470
|
+
const prefixMaxEnd = [];
|
|
1471
|
+
let maxEnd = Number.NEGATIVE_INFINITY;
|
|
1472
|
+
for (const indexed of notes) {
|
|
1473
|
+
maxEnd = Math.max(maxEnd, indexed.endBeat);
|
|
1474
|
+
prefixMaxEnd.push(maxEnd);
|
|
1475
|
+
}
|
|
1476
|
+
return {
|
|
1477
|
+
notes,
|
|
1478
|
+
at(beat) {
|
|
1479
|
+
let index = upperBound(notes, beat + EPS2) - 1;
|
|
1480
|
+
while (index >= 0) {
|
|
1481
|
+
if ((prefixMaxEnd[index] ?? Number.NEGATIVE_INFINITY) <= beat + EPS2) {
|
|
1482
|
+
return void 0;
|
|
1483
|
+
}
|
|
1484
|
+
const indexed = notes[index];
|
|
1485
|
+
if (indexed && indexed.note.durationBeat > 0 && indexed.note.startBeat - EPS2 <= beat && beat < indexed.endBeat - EPS2) {
|
|
1486
|
+
return indexed;
|
|
1487
|
+
}
|
|
1488
|
+
index -= 1;
|
|
1489
|
+
}
|
|
1490
|
+
return void 0;
|
|
1491
|
+
},
|
|
1492
|
+
attacksAt(beat) {
|
|
1493
|
+
const firstAfter = upperBound(notes, beat + EPS2);
|
|
1494
|
+
const candidate = notes[firstAfter - 1];
|
|
1495
|
+
return candidate !== void 0 && Math.abs(candidate.note.startBeat - beat) < EPS2;
|
|
1496
|
+
},
|
|
1497
|
+
onsetsBetween(startBeat, endBeat) {
|
|
1498
|
+
const result = [];
|
|
1499
|
+
let index = upperBound(notes, startBeat + EPS2);
|
|
1500
|
+
while (index < notes.length) {
|
|
1501
|
+
const onset = notes[index]?.note.startBeat;
|
|
1502
|
+
if (onset === void 0 || onset >= endBeat - EPS2) {
|
|
1503
|
+
break;
|
|
1504
|
+
}
|
|
1505
|
+
if (result[result.length - 1] !== onset) {
|
|
1506
|
+
result.push(onset);
|
|
1507
|
+
}
|
|
1508
|
+
index += 1;
|
|
1509
|
+
}
|
|
1510
|
+
return result;
|
|
1511
|
+
}
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1293
1515
|
// src/analyze/detect/index.ts
|
|
1294
1516
|
function pitchClass5(value) {
|
|
1295
1517
|
return (Math.trunc(value) % 12 + 12) % 12;
|
|
@@ -1297,12 +1519,21 @@ function pitchClass5(value) {
|
|
|
1297
1519
|
function uniquePitchClasses(pitches) {
|
|
1298
1520
|
return [...new Set(pitches.map(pitchClass5))].sort((a, b) => a - b);
|
|
1299
1521
|
}
|
|
1300
|
-
function
|
|
1522
|
+
function assertPitches(pitches) {
|
|
1523
|
+
assertGenerationBudget(pitches.length, "detection pitches");
|
|
1524
|
+
for (let index = 0; index < pitches.length; index += 1) {
|
|
1525
|
+
assertFiniteNumber(pitches[index] ?? Number.NaN, `pitches[${index}]`);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
function detectChord(pitches, opts = {}) {
|
|
1529
|
+
assertPitches(pitches);
|
|
1530
|
+
if (opts.bassPc !== void 0) assertFiniteNumber(opts.bassPc, "bassPc");
|
|
1301
1531
|
const input = uniquePitchClasses(pitches);
|
|
1302
1532
|
if (input.length === 0) {
|
|
1303
1533
|
return [];
|
|
1304
1534
|
}
|
|
1305
|
-
const
|
|
1535
|
+
const inputKind = opts.input === void 0 || opts.input === "auto" ? pitches.every((pitch) => Number.isInteger(pitch) && pitch >= 0 && pitch <= 11) ? "pitchClass" : "midi" : opts.input;
|
|
1536
|
+
const bassPc = opts.bassPc !== void 0 ? pitchClass5(opts.bassPc) : inputKind === "midi" ? pitchClass5(pitches.reduce((lowest, pitch) => Math.min(lowest, pitch), Infinity)) : void 0;
|
|
1306
1537
|
const inputSet = new Set(input);
|
|
1307
1538
|
const matches = [];
|
|
1308
1539
|
const qualities = chordQualities();
|
|
@@ -1317,8 +1548,8 @@ function detectChord(pitches) {
|
|
|
1317
1548
|
continue;
|
|
1318
1549
|
}
|
|
1319
1550
|
const extraPcs = input.filter((pc) => !toneSet.has(pc));
|
|
1320
|
-
const bassIndex = chord.intervals.findIndex((iv) => pitchClass5(rootPc + iv) === bassPc);
|
|
1321
|
-
const inversion = bassIndex > 0 ? bassIndex : 0;
|
|
1551
|
+
const bassIndex = bassPc === void 0 ? -1 : chord.intervals.findIndex((iv) => pitchClass5(rootPc + iv) === bassPc);
|
|
1552
|
+
const inversion = bassPc === void 0 ? null : bassIndex > 0 ? bassIndex : 0;
|
|
1322
1553
|
const match = {
|
|
1323
1554
|
rootPc,
|
|
1324
1555
|
quality,
|
|
@@ -1327,7 +1558,7 @@ function detectChord(pitches) {
|
|
|
1327
1558
|
exact: extraPcs.length === 0 && missingPcs.length === 0,
|
|
1328
1559
|
inversion
|
|
1329
1560
|
};
|
|
1330
|
-
if (
|
|
1561
|
+
if (bassPc !== void 0 && bassPc !== rootPc) {
|
|
1331
1562
|
match.bassPc = bassPc;
|
|
1332
1563
|
}
|
|
1333
1564
|
matches.push(match);
|
|
@@ -1349,8 +1580,8 @@ function detectChord(pitches) {
|
|
|
1349
1580
|
});
|
|
1350
1581
|
return matches;
|
|
1351
1582
|
}
|
|
1352
|
-
function detectChordBest(pitches) {
|
|
1353
|
-
const best = detectChord(pitches)[0];
|
|
1583
|
+
function detectChordBest(pitches, opts = {}) {
|
|
1584
|
+
const best = detectChord(pitches, opts)[0];
|
|
1354
1585
|
if (!best) {
|
|
1355
1586
|
return null;
|
|
1356
1587
|
}
|
|
@@ -1358,6 +1589,7 @@ function detectChordBest(pitches) {
|
|
|
1358
1589
|
}
|
|
1359
1590
|
var MINOR_MASK_VARIANTS = [NATURAL_MINOR_MASK, HARMONIC_MINOR_MASK, MELODIC_MINOR_MASK];
|
|
1360
1591
|
function detectKey(pitches) {
|
|
1592
|
+
assertPitches(pitches);
|
|
1361
1593
|
const input = uniquePitchClasses(pitches);
|
|
1362
1594
|
if (input.length === 0) {
|
|
1363
1595
|
return [];
|
|
@@ -1409,10 +1641,21 @@ function detectKey(pitches) {
|
|
|
1409
1641
|
|
|
1410
1642
|
// src/analyze/timeline/index.ts
|
|
1411
1643
|
function chordTimelineFromChords(chords, totalBeats) {
|
|
1412
|
-
|
|
1644
|
+
assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, "timeline totalBeats");
|
|
1645
|
+
assertGenerationBudget(chords.length, "timeline chords");
|
|
1646
|
+
for (let index = 0; index < chords.length; index += 1) {
|
|
1647
|
+
assertRange(
|
|
1648
|
+
chords[index]?.startBeat ?? Number.NaN,
|
|
1649
|
+
0,
|
|
1650
|
+
Number.MAX_SAFE_INTEGER,
|
|
1651
|
+
`chords[${index}].startBeat`
|
|
1652
|
+
);
|
|
1653
|
+
}
|
|
1654
|
+
const sorted = [...chords].filter((chord) => chord.startBeat < totalBeats).sort((a, b) => a.startBeat - b.startBeat);
|
|
1413
1655
|
const segments = sorted.map((gc, i) => {
|
|
1414
1656
|
const next = sorted[i + 1];
|
|
1415
|
-
const
|
|
1657
|
+
const nextBeat = next ? next.startBeat : totalBeats;
|
|
1658
|
+
const endBeat = Math.min(totalBeats, Math.max(gc.startBeat, nextBeat));
|
|
1416
1659
|
return {
|
|
1417
1660
|
startBeat: gc.startBeat,
|
|
1418
1661
|
endBeat,
|
|
@@ -1423,6 +1666,7 @@ function chordTimelineFromChords(chords, totalBeats) {
|
|
|
1423
1666
|
}
|
|
1424
1667
|
function segmentLookup(segments) {
|
|
1425
1668
|
return (beat) => {
|
|
1669
|
+
assertFiniteNumber(beat, "timeline query beat");
|
|
1426
1670
|
for (const seg of segments) {
|
|
1427
1671
|
if (beat >= seg.startBeat && beat < seg.endBeat) {
|
|
1428
1672
|
return seg.chord;
|
|
@@ -1431,7 +1675,7 @@ function segmentLookup(segments) {
|
|
|
1431
1675
|
return null;
|
|
1432
1676
|
};
|
|
1433
1677
|
}
|
|
1434
|
-
var
|
|
1678
|
+
var EPS3 = 1e-9;
|
|
1435
1679
|
var NOISE_THRESHOLD_RATIO = 0.2;
|
|
1436
1680
|
var MAX_DETECTION_PCS = 6;
|
|
1437
1681
|
var FALLBACK_PCS = 3;
|
|
@@ -1473,11 +1717,11 @@ function analyzeWindow(notes, windowStart, windowEnd, ts, key) {
|
|
|
1473
1717
|
for (const note of notes) {
|
|
1474
1718
|
const noteEnd = note.startBeat + note.durationBeat;
|
|
1475
1719
|
const overlap = Math.min(noteEnd, windowEnd) - Math.max(note.startBeat, windowStart);
|
|
1476
|
-
if (overlap <=
|
|
1720
|
+
if (overlap <= EPS3) {
|
|
1477
1721
|
continue;
|
|
1478
1722
|
}
|
|
1479
1723
|
const velocityFactor = note.velocity !== void 0 ? note.velocity / 127 : 1;
|
|
1480
|
-
const onsetInWindow = note.startBeat >= windowStart -
|
|
1724
|
+
const onsetInWindow = note.startBeat >= windowStart - EPS3 && note.startBeat < windowEnd - EPS3;
|
|
1481
1725
|
const accent = onsetInWindow ? 1 + metricWeight(note.startBeat, ts) / 3 : 1;
|
|
1482
1726
|
const pc = pitchClass6(note.pitch);
|
|
1483
1727
|
weights[pc] = (weights[pc] ?? 0) + overlap * velocityFactor * accent;
|
|
@@ -1491,7 +1735,7 @@ function analyzeWindow(notes, windowStart, windowEnd, ts, key) {
|
|
|
1491
1735
|
totalWeight += w;
|
|
1492
1736
|
maxWeight = Math.max(maxWeight, w);
|
|
1493
1737
|
}
|
|
1494
|
-
if (totalWeight <=
|
|
1738
|
+
if (totalWeight <= EPS3) {
|
|
1495
1739
|
return null;
|
|
1496
1740
|
}
|
|
1497
1741
|
const ranked = weights.map((weight, pc) => ({ pc, weight })).filter(({ weight }) => weight > 0).sort((a, b) => b.weight - a.weight || a.pc - b.pc);
|
|
@@ -1524,26 +1768,40 @@ function sameChord(a, b) {
|
|
|
1524
1768
|
}
|
|
1525
1769
|
function chordTimelineFromNotes(notes, opts = {}) {
|
|
1526
1770
|
const ts = opts.ts ?? parseTimeSignature("4/4");
|
|
1771
|
+
assertTimeSignature(ts);
|
|
1527
1772
|
const harmonicRhythm = opts.harmonicRhythm ?? beatsPerBar(ts);
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
const sounding = notes.
|
|
1773
|
+
assertRange(harmonicRhythm, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "harmonic rhythm");
|
|
1774
|
+
assertNoteEvents(notes, "timeline notes", { allowNonPositiveDuration: true });
|
|
1775
|
+
const soundingIndex = createNoteEventIndex(notes.filter((note) => note.durationBeat > 0));
|
|
1776
|
+
const sounding = soundingIndex.notes.map(({ note }) => note);
|
|
1532
1777
|
const lastNoteEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
|
|
1533
1778
|
const totalBeats = opts.totalBeats ?? lastNoteEnd;
|
|
1779
|
+
assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, "timeline totalBeats");
|
|
1534
1780
|
const key = opts.key ?? detectKey(sounding.map((n) => n.pitch))[0]?.key ?? majorKey(0);
|
|
1535
1781
|
const segments = [];
|
|
1536
1782
|
const segmentConfidence = [];
|
|
1537
|
-
const windowCount = Math.max(0, Math.ceil(totalBeats / harmonicRhythm -
|
|
1783
|
+
const windowCount = Math.max(0, Math.ceil(totalBeats / harmonicRhythm - EPS3));
|
|
1784
|
+
assertGenerationBudget(windowCount, "timeline windows");
|
|
1785
|
+
const windowNotes = Array.from({ length: windowCount }, () => []);
|
|
1786
|
+
let memberships = 0;
|
|
1787
|
+
for (const indexed of soundingIndex.notes) {
|
|
1788
|
+
const first = Math.max(0, Math.floor(indexed.note.startBeat / harmonicRhythm));
|
|
1789
|
+
const lastExclusive = Math.min(windowCount, Math.ceil(indexed.endBeat / harmonicRhythm));
|
|
1790
|
+
memberships += Math.max(0, lastExclusive - first);
|
|
1791
|
+
assertGenerationBudget(memberships, "timeline note-to-window memberships");
|
|
1792
|
+
for (let window = first; window < lastExclusive; window += 1) {
|
|
1793
|
+
windowNotes[window]?.push(indexed.note);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1538
1796
|
for (let i = 0; i < windowCount; i += 1) {
|
|
1539
1797
|
const start = i * harmonicRhythm;
|
|
1540
1798
|
const end = Math.min(start + harmonicRhythm, totalBeats);
|
|
1541
|
-
const inferred = analyzeWindow(
|
|
1799
|
+
const inferred = analyzeWindow(windowNotes[i] ?? [], start, end, ts, key);
|
|
1542
1800
|
if (!inferred) {
|
|
1543
1801
|
continue;
|
|
1544
1802
|
}
|
|
1545
1803
|
const last = segments[segments.length - 1];
|
|
1546
|
-
if (last && sameChord(last.chord, inferred.chord) && Math.abs(last.endBeat - start) <
|
|
1804
|
+
if (last && sameChord(last.chord, inferred.chord) && Math.abs(last.endBeat - start) < EPS3) {
|
|
1547
1805
|
const lastLength = last.endBeat - last.startBeat;
|
|
1548
1806
|
const length = end - start;
|
|
1549
1807
|
const lastConfidence = segmentConfidence[segmentConfidence.length - 1] ?? 0;
|
|
@@ -1568,7 +1826,7 @@ function detectCadences(timeline, key) {
|
|
|
1568
1826
|
if (!prev || !cur) {
|
|
1569
1827
|
continue;
|
|
1570
1828
|
}
|
|
1571
|
-
if (Math.abs(cur.startBeat - prev.endBeat) >
|
|
1829
|
+
if (Math.abs(cur.startBeat - prev.endBeat) > EPS3) {
|
|
1572
1830
|
continue;
|
|
1573
1831
|
}
|
|
1574
1832
|
const type = detectCadence(prev.chord, cur.chord, key);
|
|
@@ -1580,9 +1838,9 @@ function detectCadences(timeline, key) {
|
|
|
1580
1838
|
}
|
|
1581
1839
|
|
|
1582
1840
|
// src/analyze/arrange/internal.ts
|
|
1583
|
-
var
|
|
1841
|
+
var EPS4 = 1e-9;
|
|
1584
1842
|
function covers(note, beat) {
|
|
1585
|
-
return beat >= note.startBeat -
|
|
1843
|
+
return beat >= note.startBeat - EPS4 && beat < note.endBeat - EPS4;
|
|
1586
1844
|
}
|
|
1587
1845
|
function poolNotes(tracks) {
|
|
1588
1846
|
const notes = [];
|
|
@@ -1600,7 +1858,7 @@ function splitIntoSubVoices(ordered) {
|
|
|
1600
1858
|
for (const note of ordered) {
|
|
1601
1859
|
const lane = lanes.find((candidate) => {
|
|
1602
1860
|
const last = candidate[candidate.length - 1];
|
|
1603
|
-
return last !== void 0 && last.startBeat + last.durationBeat <= note.startBeat +
|
|
1861
|
+
return last !== void 0 && last.startBeat + last.durationBeat <= note.startBeat + EPS4;
|
|
1604
1862
|
});
|
|
1605
1863
|
if (lane) {
|
|
1606
1864
|
lane.push(note);
|
|
@@ -1618,9 +1876,11 @@ function prepareTracks(tracks) {
|
|
|
1618
1876
|
voice,
|
|
1619
1877
|
sounding: voice.map((note, i) => {
|
|
1620
1878
|
const prev = i > 0 ? voice[i - 1] : void 0;
|
|
1879
|
+
const prevEnd = prev === void 0 ? void 0 : prev.startBeat + prev.durationBeat;
|
|
1880
|
+
const contiguous = prevEnd !== void 0 && Math.abs(prevEnd - note.startBeat) <= EPS4;
|
|
1621
1881
|
return {
|
|
1622
1882
|
pitch: note.pitch,
|
|
1623
|
-
prevPitch: prev?.pitch,
|
|
1883
|
+
prevPitch: contiguous ? prev?.pitch : void 0,
|
|
1624
1884
|
startBeat: note.startBeat,
|
|
1625
1885
|
endBeat: note.startBeat + note.durationBeat
|
|
1626
1886
|
};
|
|
@@ -1652,11 +1912,16 @@ function clamp01(value) {
|
|
|
1652
1912
|
}
|
|
1653
1913
|
function tensionCurve(tracks, opts = {}) {
|
|
1654
1914
|
const ts = opts.ts ?? parseTimeSignature("4/4");
|
|
1915
|
+
assertTimeSignature(ts);
|
|
1916
|
+
assertGenerationBudget(tracks.length, "arrangement tracks");
|
|
1917
|
+
for (let index = 0; index < tracks.length; index += 1) {
|
|
1918
|
+
assertNoteEvents(tracks[index]?.notes ?? [], `tracks[${index}].notes`, {
|
|
1919
|
+
allowNonPositiveDuration: true
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1655
1922
|
const profile = opts.profile ?? "pop";
|
|
1656
1923
|
const step = opts.step ?? 1;
|
|
1657
|
-
|
|
1658
|
-
throw new Error(`Invalid tension sampling step: ${step}`);
|
|
1659
|
-
}
|
|
1924
|
+
assertRange(step, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "tension sampling step");
|
|
1660
1925
|
const pooled = poolNotes(tracks);
|
|
1661
1926
|
const totalBeats = pooled.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
|
|
1662
1927
|
const { timeline, key } = chordTimelineFromNotes(pooled, {
|
|
@@ -1666,7 +1931,8 @@ function tensionCurve(tracks, opts = {}) {
|
|
|
1666
1931
|
});
|
|
1667
1932
|
const prepared = prepareTracks(tracks);
|
|
1668
1933
|
const points = [];
|
|
1669
|
-
const sampleCount = Math.max(0, Math.ceil(totalBeats / step -
|
|
1934
|
+
const sampleCount = Math.max(0, Math.ceil(totalBeats / step - EPS4));
|
|
1935
|
+
assertGenerationBudget(sampleCount, "tension samples");
|
|
1670
1936
|
for (let i = 0; i < sampleCount; i += 1) {
|
|
1671
1937
|
const beat = i * step;
|
|
1672
1938
|
points.push({ beat, tension: sampleTension(prepared, timeline, key, ts, profile, beat) });
|
|
@@ -1736,7 +2002,7 @@ function sampleTension(prepared, timeline, key, ts, profile, beat) {
|
|
|
1736
2002
|
}
|
|
1737
2003
|
|
|
1738
2004
|
// src/analyze/voice/index.ts
|
|
1739
|
-
var
|
|
2005
|
+
var EPS5 = 1e-9;
|
|
1740
2006
|
function pitchClass8(pitch) {
|
|
1741
2007
|
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
1742
2008
|
}
|
|
@@ -1792,6 +2058,7 @@ function stepResolution2(pitch, chord) {
|
|
|
1792
2058
|
return void 0;
|
|
1793
2059
|
}
|
|
1794
2060
|
function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat) {
|
|
2061
|
+
assertNoteEvents(voice, "voice notes", { allowNonPositiveDuration: true });
|
|
1795
2062
|
const result = [];
|
|
1796
2063
|
for (let i = 0; i < voice.length; i += 1) {
|
|
1797
2064
|
const note = voice[i];
|
|
@@ -1800,8 +2067,10 @@ function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat) {
|
|
|
1800
2067
|
}
|
|
1801
2068
|
const prevNote = i > 0 ? voice[i - 1] : void 0;
|
|
1802
2069
|
const nextNote = i + 1 < voice.length ? voice[i + 1] : void 0;
|
|
1803
|
-
const
|
|
1804
|
-
const
|
|
2070
|
+
const prevEnd = prevNote === void 0 ? Number.NEGATIVE_INFINITY : prevNote.startBeat + prevNote.durationBeat;
|
|
2071
|
+
const noteEnd = note.startBeat + note.durationBeat;
|
|
2072
|
+
const prev = prevNote !== void 0 && Math.abs(prevEnd - note.startBeat) <= EPS5 ? prevNote : void 0;
|
|
2073
|
+
const next = nextNote !== void 0 && Math.abs(noteEnd - nextNote.startBeat) <= EPS5 ? nextNote : void 0;
|
|
1805
2074
|
const chord = chordAtBeat(note.startBeat);
|
|
1806
2075
|
const labels = [];
|
|
1807
2076
|
const member = isChordMember2(note.pitch, chord);
|
|
@@ -1941,8 +2210,9 @@ function otherVoicesSounding(prepared, excludeTrack, excludeVoice, beat) {
|
|
|
1941
2210
|
continue;
|
|
1942
2211
|
}
|
|
1943
2212
|
const snap = { pitch: note.pitch };
|
|
1944
|
-
|
|
1945
|
-
|
|
2213
|
+
const previous = Math.abs(note.startBeat - beat) <= EPS4 ? note.prevPitch : note.pitch;
|
|
2214
|
+
if (previous !== void 0) {
|
|
2215
|
+
snap.prevPitch = previous;
|
|
1946
2216
|
}
|
|
1947
2217
|
out.push(snap);
|
|
1948
2218
|
}
|
|
@@ -1954,7 +2224,7 @@ function evaluationBeats(note, timeline) {
|
|
|
1954
2224
|
const noteEnd = note.startBeat + note.durationBeat;
|
|
1955
2225
|
const beats = [note.startBeat];
|
|
1956
2226
|
for (const segment of timeline.segments) {
|
|
1957
|
-
if (segment.startBeat > note.startBeat +
|
|
2227
|
+
if (segment.startBeat > note.startBeat + EPS4 && segment.startBeat < noteEnd - EPS4) {
|
|
1958
2228
|
beats.push(segment.startBeat);
|
|
1959
2229
|
}
|
|
1960
2230
|
}
|
|
@@ -1962,6 +2232,13 @@ function evaluationBeats(note, timeline) {
|
|
|
1962
2232
|
}
|
|
1963
2233
|
function analyzeArrangement(tracks, opts = {}) {
|
|
1964
2234
|
const ts = opts.ts ?? parseTimeSignature("4/4");
|
|
2235
|
+
assertTimeSignature(ts);
|
|
2236
|
+
assertGenerationBudget(tracks.length, "arrangement tracks");
|
|
2237
|
+
for (let index = 0; index < tracks.length; index += 1) {
|
|
2238
|
+
assertNoteEvents(tracks[index]?.notes ?? [], `tracks[${index}].notes`, {
|
|
2239
|
+
allowNonPositiveDuration: true
|
|
2240
|
+
});
|
|
2241
|
+
}
|
|
1965
2242
|
const profile = opts.profile ?? "pop";
|
|
1966
2243
|
const pooled = poolNotes(tracks);
|
|
1967
2244
|
const { timeline, key, segmentConfidence } = chordTimelineFromNotes(pooled, {
|
|
@@ -1992,11 +2269,18 @@ function analyzeArrangement(tracks, opts = {}) {
|
|
|
1992
2269
|
(beat) => otherVoicesSounding(prepared, t, v, beat)
|
|
1993
2270
|
)
|
|
1994
2271
|
);
|
|
1995
|
-
for (
|
|
2272
|
+
for (let noteIndex = 0; noteIndex < subVoice.voice.length; noteIndex += 1) {
|
|
2273
|
+
const note = subVoice.voice[noteIndex];
|
|
2274
|
+
const preparedNote = subVoice.sounding[noteIndex];
|
|
2275
|
+
if (note === void 0 || preparedNote === void 0) {
|
|
2276
|
+
continue;
|
|
2277
|
+
}
|
|
1996
2278
|
for (const beat of evaluationBeats(note, timeline)) {
|
|
2279
|
+
const atOnset = Math.abs(beat - note.startBeat) <= EPS4;
|
|
1997
2280
|
const result = evaluateSafety({
|
|
1998
2281
|
profile,
|
|
1999
2282
|
candidatePitch: note.pitch,
|
|
2283
|
+
prevPitch: atOnset ? preparedNote.prevPitch : note.pitch,
|
|
2000
2284
|
chord: timeline.at(beat),
|
|
2001
2285
|
key,
|
|
2002
2286
|
otherVoices: otherVoicesSounding(prepared, t, v, beat),
|
|
@@ -2013,6 +2297,12 @@ function analyzeArrangement(tracks, opts = {}) {
|
|
|
2013
2297
|
if (result.rationale !== void 0) {
|
|
2014
2298
|
conflict.rationale = result.rationale;
|
|
2015
2299
|
}
|
|
2300
|
+
if (result.resolveTo !== void 0) {
|
|
2301
|
+
conflict.resolveTo = result.resolveTo;
|
|
2302
|
+
}
|
|
2303
|
+
if (result.suggestions !== void 0) {
|
|
2304
|
+
conflict.suggestions = [...result.suggestions];
|
|
2305
|
+
}
|
|
2016
2306
|
conflicts.push(conflict);
|
|
2017
2307
|
}
|
|
2018
2308
|
}
|
|
@@ -2170,6 +2460,7 @@ function spelledInterval(a, b) {
|
|
|
2170
2460
|
|
|
2171
2461
|
// src/core/random/index.ts
|
|
2172
2462
|
function createRng(seed) {
|
|
2463
|
+
assertFiniteNumber(seed, "seed");
|
|
2173
2464
|
let state = seed >>> 0;
|
|
2174
2465
|
const next = () => {
|
|
2175
2466
|
state = state + 1831565813 >>> 0;
|
|
@@ -2180,31 +2471,71 @@ function createRng(seed) {
|
|
|
2180
2471
|
};
|
|
2181
2472
|
return {
|
|
2182
2473
|
next,
|
|
2183
|
-
prob: (p) => next() < p,
|
|
2184
|
-
range: (lo, hi) =>
|
|
2185
|
-
|
|
2474
|
+
prob: (p) => next() < assertRange(p, 0, 1, "probability"),
|
|
2475
|
+
range: (lo, hi) => {
|
|
2476
|
+
assertInteger(lo, "range lower bound");
|
|
2477
|
+
assertInteger(hi, "range upper bound");
|
|
2478
|
+
if (lo > hi) {
|
|
2479
|
+
throw new RangeError(
|
|
2480
|
+
`range lower bound must not exceed upper bound; received ${lo} > ${hi}`
|
|
2481
|
+
);
|
|
2482
|
+
}
|
|
2483
|
+
assertInteger(hi - lo, "range span", 0);
|
|
2484
|
+
return lo + Math.floor(next() * (hi - lo + 1));
|
|
2485
|
+
},
|
|
2486
|
+
float: (lo, hi) => {
|
|
2487
|
+
assertFiniteNumber(lo, "float lower bound");
|
|
2488
|
+
assertFiniteNumber(hi, "float upper bound");
|
|
2489
|
+
if (lo > hi) {
|
|
2490
|
+
throw new RangeError(
|
|
2491
|
+
`float lower bound must not exceed upper bound; received ${lo} > ${hi}`
|
|
2492
|
+
);
|
|
2493
|
+
}
|
|
2494
|
+
assertFiniteNumber(hi - lo, "float span");
|
|
2495
|
+
return lo + next() * (hi - lo);
|
|
2496
|
+
}
|
|
2186
2497
|
};
|
|
2187
2498
|
}
|
|
2188
2499
|
|
|
2189
2500
|
// src/core/tuning/index.ts
|
|
2190
2501
|
var TWELVE_TET = { refStep: 69, refFreq: 440, divisions: 12 };
|
|
2502
|
+
function assertTuning(tuning) {
|
|
2503
|
+
assertFiniteNumber(tuning.refStep, "tuning.refStep");
|
|
2504
|
+
assertRange(tuning.refFreq, Number.MIN_VALUE, Number.MAX_VALUE, "tuning.refFreq");
|
|
2505
|
+
assertPositiveInt(tuning.divisions, "tuning.divisions");
|
|
2506
|
+
return tuning;
|
|
2507
|
+
}
|
|
2191
2508
|
function edo(n, refFreq = 440, refStep = 69) {
|
|
2509
|
+
assertPositiveInt(n, "EDO divisions");
|
|
2510
|
+
assertRange(refFreq, Number.MIN_VALUE, Number.MAX_VALUE, "reference frequency");
|
|
2511
|
+
assertFiniteNumber(refStep, "reference step");
|
|
2192
2512
|
return { refStep, refFreq, divisions: n };
|
|
2193
2513
|
}
|
|
2194
2514
|
function frequencyOf(step, tuning = TWELVE_TET) {
|
|
2195
|
-
|
|
2515
|
+
assertFiniteNumber(step, "step");
|
|
2516
|
+
assertTuning(tuning);
|
|
2517
|
+
const result = tuning.refFreq * 2 ** ((step - tuning.refStep) / tuning.divisions);
|
|
2518
|
+
return assertRange(result, Number.MIN_VALUE, Number.MAX_VALUE, "frequency result");
|
|
2196
2519
|
}
|
|
2197
2520
|
function nearestStep(freq, tuning = TWELVE_TET) {
|
|
2521
|
+
assertRange(freq, Number.MIN_VALUE, Number.MAX_VALUE, "frequency");
|
|
2522
|
+
assertTuning(tuning);
|
|
2198
2523
|
return Math.round(tuning.refStep + tuning.divisions * Math.log2(freq / tuning.refFreq));
|
|
2199
2524
|
}
|
|
2200
2525
|
function centsBetweenFreq(a, b) {
|
|
2201
|
-
|
|
2526
|
+
assertRange(a, Number.MIN_VALUE, Number.MAX_VALUE, "first frequency");
|
|
2527
|
+
assertRange(b, Number.MIN_VALUE, Number.MAX_VALUE, "second frequency");
|
|
2528
|
+
return 1200 * (Math.log2(b) - Math.log2(a));
|
|
2202
2529
|
}
|
|
2203
2530
|
function centsOfSteps(steps, tuning = TWELVE_TET) {
|
|
2204
|
-
|
|
2531
|
+
assertFiniteNumber(steps, "steps");
|
|
2532
|
+
assertTuning(tuning);
|
|
2533
|
+
return assertFiniteNumber(steps * 1200 / tuning.divisions, "cents result");
|
|
2205
2534
|
}
|
|
2206
2535
|
function ratioToCents(numerator, denominator) {
|
|
2207
|
-
|
|
2536
|
+
assertRange(numerator, Number.MIN_VALUE, Number.MAX_VALUE, "ratio numerator");
|
|
2537
|
+
assertRange(denominator, Number.MIN_VALUE, Number.MAX_VALUE, "ratio denominator");
|
|
2538
|
+
return 1200 * (Math.log2(numerator) - Math.log2(denominator));
|
|
2208
2539
|
}
|
|
2209
2540
|
var JUST_RATIOS = {
|
|
2210
2541
|
0: [1, 1],
|
|
@@ -2222,9 +2553,10 @@ var JUST_RATIOS = {
|
|
|
2222
2553
|
12: [2, 1]
|
|
2223
2554
|
};
|
|
2224
2555
|
function justDeviationCents(semitoneClass) {
|
|
2556
|
+
assertInteger(semitoneClass, "semitone class", 0, 12);
|
|
2225
2557
|
const ratio = JUST_RATIOS[semitoneClass];
|
|
2226
2558
|
if (!ratio) {
|
|
2227
|
-
|
|
2559
|
+
throw new RangeError(`semitone class has no just ratio: ${semitoneClass}`);
|
|
2228
2560
|
}
|
|
2229
2561
|
return ratioToCents(ratio[0], ratio[1]) - semitoneClass * 100;
|
|
2230
2562
|
}
|
|
@@ -2236,7 +2568,7 @@ var DEFAULT_OCTAVE = 2;
|
|
|
2236
2568
|
var STRONG_VELOCITY = 100;
|
|
2237
2569
|
var WEAK_VELOCITY = 80;
|
|
2238
2570
|
var PICKUP_PROB = 0.35;
|
|
2239
|
-
var
|
|
2571
|
+
var EPS6 = 1e-9;
|
|
2240
2572
|
function pitchClass9(value) {
|
|
2241
2573
|
return (Math.trunc(value) % 12 + 12) % 12;
|
|
2242
2574
|
}
|
|
@@ -2276,7 +2608,7 @@ function bassPcOf(chord) {
|
|
|
2276
2608
|
}
|
|
2277
2609
|
function beatPositions(start, end) {
|
|
2278
2610
|
const positions = [];
|
|
2279
|
-
for (let p = start; p < end -
|
|
2611
|
+
for (let p = start; p < end - EPS6; p += 1) {
|
|
2280
2612
|
positions.push(p);
|
|
2281
2613
|
}
|
|
2282
2614
|
if (positions.length === 0) {
|
|
@@ -2296,7 +2628,7 @@ function buildRoot(ctx, seg) {
|
|
|
2296
2628
|
function buildRootFifth(ctx, seg) {
|
|
2297
2629
|
ctx.prevMidi = emit(ctx, seg.startBeat, bassPcOf(seg.chord));
|
|
2298
2630
|
const mid = (seg.startBeat + seg.endBeat) / 2;
|
|
2299
|
-
if (mid > seg.startBeat +
|
|
2631
|
+
if (mid > seg.startBeat + EPS6 && mid < seg.endBeat - EPS6) {
|
|
2300
2632
|
ctx.prevMidi = emit(ctx, mid, fifthPcOf(seg.chord));
|
|
2301
2633
|
}
|
|
2302
2634
|
}
|
|
@@ -2358,13 +2690,30 @@ function buildWalking(ctx, seg, next) {
|
|
|
2358
2690
|
});
|
|
2359
2691
|
}
|
|
2360
2692
|
function generateBassLine(opts) {
|
|
2693
|
+
assertGenerationBudget(opts.segments.length, "bass segments");
|
|
2694
|
+
for (let index = 0; index < opts.segments.length; index += 1) {
|
|
2695
|
+
const segment = opts.segments[index];
|
|
2696
|
+
if (!segment) continue;
|
|
2697
|
+
assertRange(segment.startBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].startBeat`);
|
|
2698
|
+
assertRange(segment.endBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].endBeat`);
|
|
2699
|
+
if (segment.endBeat <= segment.startBeat) {
|
|
2700
|
+
throw new RangeError(`segments[${index}] must have a positive duration`);
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2361
2703
|
const segments = [...opts.segments].sort((a, b) => a.startBeat - b.startBeat);
|
|
2362
2704
|
if (segments.length === 0) {
|
|
2363
2705
|
return [];
|
|
2364
2706
|
}
|
|
2365
2707
|
const ts = opts.ts ?? DEFAULT_TS;
|
|
2708
|
+
assertTimeSignature(ts);
|
|
2366
2709
|
const style = opts.style ?? DEFAULT_STYLE;
|
|
2367
2710
|
const octave = opts.octave ?? DEFAULT_OCTAVE;
|
|
2711
|
+
assertInteger(octave, "bass octave", -1, 9);
|
|
2712
|
+
const estimatedNotes = segments.reduce(
|
|
2713
|
+
(count, segment) => count + Math.max(1, Math.ceil(segment.endBeat - segment.startBeat)),
|
|
2714
|
+
0
|
|
2715
|
+
);
|
|
2716
|
+
assertGenerationBudget(estimatedNotes, "bass notes");
|
|
2368
2717
|
const low = octave * 12 + 12;
|
|
2369
2718
|
const ctx = {
|
|
2370
2719
|
ts,
|
|
@@ -2407,7 +2756,7 @@ function generateBassLine(opts) {
|
|
|
2407
2756
|
}
|
|
2408
2757
|
const nextStart = ctx.notes[i + 1]?.startBeat ?? lastEnd;
|
|
2409
2758
|
const durationBeat = nextStart - note.startBeat;
|
|
2410
|
-
if (durationBeat <=
|
|
2759
|
+
if (durationBeat <= EPS6) {
|
|
2411
2760
|
continue;
|
|
2412
2761
|
}
|
|
2413
2762
|
out.push({
|
|
@@ -2422,7 +2771,7 @@ function generateBassLine(opts) {
|
|
|
2422
2771
|
|
|
2423
2772
|
// src/generate/countermelody/index.ts
|
|
2424
2773
|
var DEFAULT_TS2 = { numerator: 4, denominator: 4 };
|
|
2425
|
-
var
|
|
2774
|
+
var EPS7 = 1e-9;
|
|
2426
2775
|
var GRID_STEP = 0.5;
|
|
2427
2776
|
var MIN_ONSET_GAP = 1;
|
|
2428
2777
|
var REGISTER_OFFSET = 12;
|
|
@@ -2433,30 +2782,35 @@ var REINFORCE_PROB = 0.35;
|
|
|
2433
2782
|
var DEFAULT_MELODY_VELOCITY = 96;
|
|
2434
2783
|
var VELOCITY_DROP = 16;
|
|
2435
2784
|
function isWholeBeat(beat) {
|
|
2436
|
-
return Math.abs(beat - Math.round(beat)) <
|
|
2785
|
+
return Math.abs(beat - Math.round(beat)) < EPS7;
|
|
2437
2786
|
}
|
|
2438
2787
|
function melodyNoteAt(melody, beat) {
|
|
2439
|
-
|
|
2440
|
-
for (const note of melody) {
|
|
2441
|
-
if (note.startBeat - EPS6 <= beat && beat < note.startBeat + note.durationBeat - EPS6) {
|
|
2442
|
-
if (!found || note.startBeat > found.startBeat) {
|
|
2443
|
-
found = note;
|
|
2444
|
-
}
|
|
2445
|
-
}
|
|
2446
|
-
}
|
|
2447
|
-
return found;
|
|
2788
|
+
return melody.at(beat)?.note;
|
|
2448
2789
|
}
|
|
2449
2790
|
function melodyAttacksAt(melody, beat) {
|
|
2450
|
-
return melody.
|
|
2791
|
+
return melody.attacksAt(beat);
|
|
2792
|
+
}
|
|
2793
|
+
function melodySnapshotAt(melody, beat) {
|
|
2794
|
+
const current = melodyNoteAt(melody, beat);
|
|
2795
|
+
if (current === void 0) {
|
|
2796
|
+
return void 0;
|
|
2797
|
+
}
|
|
2798
|
+
const attacked = Math.abs(current.startBeat - beat) < EPS7;
|
|
2799
|
+
const previous = attacked ? melodyNoteAt(melody, beat - EPS7 * 2) : current;
|
|
2800
|
+
const snapshot = { pitch: current.pitch };
|
|
2801
|
+
if (previous !== void 0) {
|
|
2802
|
+
snapshot.prevPitch = previous.pitch;
|
|
2803
|
+
}
|
|
2804
|
+
return snapshot;
|
|
2451
2805
|
}
|
|
2452
2806
|
function followOnsets(melody) {
|
|
2453
|
-
return [...new Set(melody.map((note) => note.startBeat))]
|
|
2807
|
+
return [...new Set(melody.notes.map(({ note }) => note.startBeat))];
|
|
2454
2808
|
}
|
|
2455
2809
|
function complementOnsets(melody, ts, spanStart, spanEnd, rng) {
|
|
2456
2810
|
const onsets = [];
|
|
2457
2811
|
const gridStart = Math.floor(spanStart / GRID_STEP) * GRID_STEP;
|
|
2458
2812
|
let last = Number.NEGATIVE_INFINITY;
|
|
2459
|
-
for (let beat = gridStart; beat < spanEnd -
|
|
2813
|
+
for (let beat = gridStart; beat < spanEnd - EPS7; beat += GRID_STEP) {
|
|
2460
2814
|
const attacked = melodyAttacksAt(melody, beat);
|
|
2461
2815
|
const sounding = melodyNoteAt(melody, beat) !== void 0;
|
|
2462
2816
|
let place = false;
|
|
@@ -2467,7 +2821,7 @@ function complementOnsets(melody, ts, spanStart, spanEnd, rng) {
|
|
|
2467
2821
|
} else if (isStrongBeat(beat, ts)) {
|
|
2468
2822
|
place = rng.prob(REINFORCE_PROB);
|
|
2469
2823
|
}
|
|
2470
|
-
if (place && beat - last >= MIN_ONSET_GAP -
|
|
2824
|
+
if (place && beat - last >= MIN_ONSET_GAP - EPS7) {
|
|
2471
2825
|
onsets.push(beat);
|
|
2472
2826
|
last = beat;
|
|
2473
2827
|
}
|
|
@@ -2513,54 +2867,102 @@ function scoreCandidate(pitch, melPitch, melPrev, prevPitch, chord, center) {
|
|
|
2513
2867
|
score -= Math.abs(pitch - center) * 0.02;
|
|
2514
2868
|
return score;
|
|
2515
2869
|
}
|
|
2516
|
-
function
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2870
|
+
function heldNoteBoundaries(melody, startBeat, endBeat, chordChangeBeats) {
|
|
2871
|
+
const boundaries = /* @__PURE__ */ new Set([startBeat]);
|
|
2872
|
+
for (const onset of melody.onsetsBetween(startBeat, endBeat)) {
|
|
2873
|
+
boundaries.add(onset);
|
|
2874
|
+
}
|
|
2875
|
+
for (const beat of chordChangeBeats) {
|
|
2876
|
+
if (beat > startBeat + EPS7 && beat < endBeat - EPS7) {
|
|
2877
|
+
boundaries.add(beat);
|
|
2523
2878
|
}
|
|
2524
2879
|
}
|
|
2525
|
-
|
|
2526
|
-
let
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2880
|
+
const firstGrid = Math.floor(startBeat / GRID_STEP + 1) * GRID_STEP;
|
|
2881
|
+
for (let beat = firstGrid; beat < endBeat - EPS7; beat += GRID_STEP) {
|
|
2882
|
+
boundaries.add(beat);
|
|
2883
|
+
}
|
|
2884
|
+
return [...boundaries].sort((a, b) => a - b);
|
|
2885
|
+
}
|
|
2886
|
+
function heldPitchSafety(pitch, startBeat, endBeat, prevPitch, melody, opts, profile, register, low, high, ts) {
|
|
2887
|
+
let worst = 0 /* Safe */;
|
|
2888
|
+
const boundaries = heldNoteBoundaries(melody, startBeat, endBeat, opts.chordChangeBeats ?? []);
|
|
2889
|
+
for (const boundary of boundaries) {
|
|
2890
|
+
const melodyVoice = melodySnapshotAt(melody, boundary);
|
|
2891
|
+
if (melodyVoice !== void 0 && (register === "below" && pitch >= melodyVoice.pitch || register === "above" && pitch <= melodyVoice.pitch)) {
|
|
2892
|
+
return null;
|
|
2530
2893
|
}
|
|
2531
|
-
|
|
2532
|
-
|
|
2894
|
+
const atCounterOnset = Math.abs(boundary - startBeat) < EPS7;
|
|
2895
|
+
const result = evaluateSafety({
|
|
2896
|
+
profile,
|
|
2897
|
+
candidatePitch: pitch,
|
|
2898
|
+
prevPitch: atCounterOnset ? prevPitch : pitch,
|
|
2899
|
+
chord: opts.chordAt(boundary),
|
|
2900
|
+
key: opts.key,
|
|
2901
|
+
otherVoices: melodyVoice ? [melodyVoice] : [],
|
|
2902
|
+
strongBeat: isStrongBeat(boundary, ts),
|
|
2903
|
+
vocalLow: low,
|
|
2904
|
+
vocalHigh: high
|
|
2905
|
+
});
|
|
2906
|
+
worst = Math.max(worst, result.safety);
|
|
2907
|
+
if (worst === 2 /* Dissonant */) {
|
|
2908
|
+
return worst;
|
|
2533
2909
|
}
|
|
2534
2910
|
}
|
|
2535
|
-
return
|
|
2911
|
+
return worst;
|
|
2536
2912
|
}
|
|
2537
2913
|
function generateCounterMelody(opts) {
|
|
2538
|
-
const melody =
|
|
2539
|
-
if (melody.length === 0) {
|
|
2914
|
+
const melody = createNoteEventIndex(opts.melody);
|
|
2915
|
+
if (melody.notes.length === 0) {
|
|
2540
2916
|
return [];
|
|
2541
2917
|
}
|
|
2542
2918
|
const ts = opts.ts ?? DEFAULT_TS2;
|
|
2919
|
+
assertTimeSignature(ts);
|
|
2543
2920
|
const register = opts.register ?? "below";
|
|
2544
2921
|
const rhythm = opts.rhythm ?? "complement";
|
|
2545
2922
|
const profile = opts.profile ?? "pop";
|
|
2546
2923
|
const rng = createRng(opts.seed ?? 0);
|
|
2547
|
-
const spanStart = melody[0]?.startBeat ?? 0;
|
|
2548
|
-
const spanEnd = melody.reduce((end, n) => Math.max(end, n.
|
|
2549
|
-
|
|
2924
|
+
const spanStart = melody.notes[0]?.note.startBeat ?? 0;
|
|
2925
|
+
const spanEnd = melody.notes.reduce((end, n) => Math.max(end, n.endBeat), spanStart);
|
|
2926
|
+
assertGenerationBudget(Math.ceil((spanEnd - spanStart) / GRID_STEP), "countermelody grid");
|
|
2927
|
+
const meanPitch = melody.notes.reduce((sum, n) => sum + n.note.pitch, 0) / melody.notes.length;
|
|
2550
2928
|
const defaultCenter = register === "below" ? meanPitch - REGISTER_OFFSET : meanPitch + REGISTER_OFFSET;
|
|
2551
2929
|
const low = opts.pitchLow ?? Math.round(defaultCenter) - DEFAULT_HALF_RANGE;
|
|
2552
2930
|
const high = opts.pitchHigh ?? Math.round(defaultCenter) + DEFAULT_HALF_RANGE;
|
|
2931
|
+
assertInteger(low, "countermelody pitchLow");
|
|
2932
|
+
assertInteger(high, "countermelody pitchHigh");
|
|
2933
|
+
if (low > high) {
|
|
2934
|
+
throw new RangeError(
|
|
2935
|
+
`countermelody pitchLow must not exceed pitchHigh; received ${low} > ${high}`
|
|
2936
|
+
);
|
|
2937
|
+
}
|
|
2938
|
+
assertGenerationBudget(high - low + 1, "countermelody pitch candidates");
|
|
2939
|
+
assertGenerationBudget(opts.chordChangeBeats?.length ?? 0, "chord change beats");
|
|
2940
|
+
for (let index = 0; index < (opts.chordChangeBeats?.length ?? 0); index += 1) {
|
|
2941
|
+
assertRange(
|
|
2942
|
+
opts.chordChangeBeats?.[index] ?? Number.NaN,
|
|
2943
|
+
0,
|
|
2944
|
+
Number.MAX_SAFE_INTEGER,
|
|
2945
|
+
`chordChangeBeats[${index}]`
|
|
2946
|
+
);
|
|
2947
|
+
}
|
|
2553
2948
|
const center = (low + high) / 2;
|
|
2554
2949
|
const onsets = rhythm === "follow" ? followOnsets(melody) : complementOnsets(melody, ts, spanStart, spanEnd, rng);
|
|
2950
|
+
assertGenerationBudget(onsets.length * (high - low + 1), "countermelody search");
|
|
2555
2951
|
const out = [];
|
|
2556
2952
|
let prevPitch;
|
|
2557
|
-
let
|
|
2558
|
-
|
|
2953
|
+
for (let onsetIndex = 0; onsetIndex < onsets.length; onsetIndex += 1) {
|
|
2954
|
+
const beat = onsets[onsetIndex];
|
|
2955
|
+
if (beat === void 0) {
|
|
2956
|
+
continue;
|
|
2957
|
+
}
|
|
2958
|
+
const nextOnset = onsets[onsetIndex + 1];
|
|
2959
|
+
const endBeat = nextOnset ?? Math.max(spanEnd, beat + GRID_STEP);
|
|
2559
2960
|
const melNote = melodyNoteAt(melody, beat);
|
|
2560
2961
|
const melPitch = melNote?.pitch;
|
|
2561
|
-
const
|
|
2962
|
+
const melodyVoice = melodySnapshotAt(melody, beat);
|
|
2963
|
+
const melPrev = melodyVoice?.prevPitch;
|
|
2562
2964
|
const chord = opts.chordAt(beat);
|
|
2563
|
-
const otherVoices =
|
|
2965
|
+
const otherVoices = melodyVoice !== void 0 ? [melodyVoice] : [];
|
|
2564
2966
|
const candidates = enumerateSafePitches(
|
|
2565
2967
|
{
|
|
2566
2968
|
profile,
|
|
@@ -2576,15 +2978,26 @@ function generateCounterMelody(opts) {
|
|
|
2576
2978
|
high
|
|
2577
2979
|
);
|
|
2578
2980
|
let bestPitch;
|
|
2981
|
+
let bestWorstSafety = 2 /* Dissonant */;
|
|
2579
2982
|
let bestScore = Number.NEGATIVE_INFINITY;
|
|
2580
2983
|
for (const candidate of candidates) {
|
|
2984
|
+
const worstSafety = heldPitchSafety(
|
|
2985
|
+
candidate,
|
|
2986
|
+
beat,
|
|
2987
|
+
endBeat,
|
|
2988
|
+
prevPitch,
|
|
2989
|
+
melody,
|
|
2990
|
+
opts,
|
|
2991
|
+
profile,
|
|
2992
|
+
register,
|
|
2993
|
+
low,
|
|
2994
|
+
high,
|
|
2995
|
+
ts
|
|
2996
|
+
);
|
|
2997
|
+
if (worstSafety === null || worstSafety === 2 /* Dissonant */) {
|
|
2998
|
+
continue;
|
|
2999
|
+
}
|
|
2581
3000
|
if (melPitch !== void 0) {
|
|
2582
|
-
if (register === "below" && candidate >= melPitch) {
|
|
2583
|
-
continue;
|
|
2584
|
-
}
|
|
2585
|
-
if (register === "above" && candidate <= melPitch) {
|
|
2586
|
-
continue;
|
|
2587
|
-
}
|
|
2588
3001
|
if (prevPitch !== void 0 && melPrev !== void 0 && // Parallel octaves are the perfect-class-zero case of this predicate,
|
|
2589
3002
|
// so a single check covers both parallel fifths and octaves.
|
|
2590
3003
|
createsParallelPerfect(prevPitch, candidate, melPrev, melPitch)) {
|
|
@@ -2592,29 +3005,24 @@ function generateCounterMelody(opts) {
|
|
|
2592
3005
|
}
|
|
2593
3006
|
}
|
|
2594
3007
|
const score = scoreCandidate(candidate, melPitch, melPrev, prevPitch, chord, center);
|
|
2595
|
-
const tie = Math.abs(score - bestScore) <=
|
|
2596
|
-
if (score > bestScore +
|
|
3008
|
+
const tie = Math.abs(score - bestScore) <= EPS7 && candidate < (bestPitch ?? Number.POSITIVE_INFINITY);
|
|
3009
|
+
if (worstSafety < bestWorstSafety || worstSafety === bestWorstSafety && (score > bestScore + EPS7 || tie)) {
|
|
3010
|
+
bestWorstSafety = worstSafety;
|
|
2597
3011
|
bestScore = score;
|
|
2598
3012
|
bestPitch = candidate;
|
|
2599
3013
|
}
|
|
2600
3014
|
}
|
|
2601
|
-
|
|
3015
|
+
if (bestPitch === void 0) {
|
|
3016
|
+
prevPitch = void 0;
|
|
3017
|
+
continue;
|
|
3018
|
+
}
|
|
3019
|
+
const pitch = bestPitch;
|
|
2602
3020
|
const velocity = Math.max(
|
|
2603
3021
|
1,
|
|
2604
3022
|
Math.min(127, Math.round((melNote?.velocity ?? DEFAULT_MELODY_VELOCITY) - VELOCITY_DROP))
|
|
2605
3023
|
);
|
|
2606
|
-
out.push({ pitch, startBeat: beat, durationBeat:
|
|
3024
|
+
out.push({ pitch, startBeat: beat, durationBeat: endBeat - beat, velocity });
|
|
2607
3025
|
prevPitch = pitch;
|
|
2608
|
-
prevOnset = beat;
|
|
2609
|
-
}
|
|
2610
|
-
for (let i = 0; i < out.length; i += 1) {
|
|
2611
|
-
const note = out[i];
|
|
2612
|
-
if (!note) {
|
|
2613
|
-
continue;
|
|
2614
|
-
}
|
|
2615
|
-
const next = out[i + 1];
|
|
2616
|
-
const end = next ? next.startBeat : Math.max(spanEnd, note.startBeat + GRID_STEP);
|
|
2617
|
-
note.durationBeat = end - note.startBeat;
|
|
2618
3026
|
}
|
|
2619
3027
|
return out;
|
|
2620
3028
|
}
|
|
@@ -3334,9 +3742,10 @@ function generateHiHatForBeat(ctx, sec) {
|
|
|
3334
3742
|
|
|
3335
3743
|
// src/generate/drums/euclid.ts
|
|
3336
3744
|
function euclideanRhythm(pulses, steps, rotation = 0) {
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3745
|
+
assertPositiveInt(steps, "euclidean steps");
|
|
3746
|
+
assertInteger(pulses, "euclidean pulses");
|
|
3747
|
+
assertInteger(rotation, "euclidean rotation");
|
|
3748
|
+
assertGenerationBudget(steps, "euclidean pattern steps");
|
|
3340
3749
|
const p = Math.max(0, Math.min(pulses, steps));
|
|
3341
3750
|
const base = bjorklund(p, steps);
|
|
3342
3751
|
const shift = (rotation % steps + steps) % steps;
|
|
@@ -3345,18 +3754,6 @@ function euclideanRhythm(pulses, steps, rotation = 0) {
|
|
|
3345
3754
|
}
|
|
3346
3755
|
return base.map((_, i) => base[(i - shift + steps) % steps] ?? false);
|
|
3347
3756
|
}
|
|
3348
|
-
function hasHit(mask, step) {
|
|
3349
|
-
return (mask >> step & 1) === 1;
|
|
3350
|
-
}
|
|
3351
|
-
function patternToMask(pattern) {
|
|
3352
|
-
let mask = 0;
|
|
3353
|
-
for (let i = 0; i < pattern.length; i += 1) {
|
|
3354
|
-
if (pattern[i]) {
|
|
3355
|
-
mask |= 1 << i;
|
|
3356
|
-
}
|
|
3357
|
-
}
|
|
3358
|
-
return mask;
|
|
3359
|
-
}
|
|
3360
3757
|
function bjorklund(pulses, steps) {
|
|
3361
3758
|
if (pulses === 0) {
|
|
3362
3759
|
return Array.from({ length: steps }, () => false);
|
|
@@ -3688,19 +4085,6 @@ function isInPreChorusLift(section, bar, sectionBars, nextIsChorus) {
|
|
|
3688
4085
|
}
|
|
3689
4086
|
return bar >= sectionBars - 2;
|
|
3690
4087
|
}
|
|
3691
|
-
function euclideanToKickPattern(mask) {
|
|
3692
|
-
const slot = (step) => hasHit(mask, step) || hasHit(mask, (step + 15) % 16);
|
|
3693
|
-
return {
|
|
3694
|
-
beat1: slot(0),
|
|
3695
|
-
beat1and: slot(2),
|
|
3696
|
-
beat2: slot(4),
|
|
3697
|
-
beat2and: slot(6),
|
|
3698
|
-
beat3: slot(8),
|
|
3699
|
-
beat3and: slot(10),
|
|
3700
|
-
beat4: slot(12),
|
|
3701
|
-
beat4and: slot(14)
|
|
3702
|
-
};
|
|
3703
|
-
}
|
|
3704
4088
|
function getKickPattern(section, style, bar, rng) {
|
|
3705
4089
|
const p = { ...EMPTY };
|
|
3706
4090
|
if (section === "outro") {
|
|
@@ -3822,7 +4206,7 @@ function getPercussionConfig(mood, section) {
|
|
|
3822
4206
|
};
|
|
3823
4207
|
}
|
|
3824
4208
|
function generateAuxPercussionForBar(track, barStart, config, role, densityMult, rng, bpm) {
|
|
3825
|
-
if (role === "minimal"
|
|
4209
|
+
if (role === "minimal") {
|
|
3826
4210
|
return;
|
|
3827
4211
|
}
|
|
3828
4212
|
if (config.tambourine) {
|
|
@@ -3871,6 +4255,16 @@ function generateAuxPercussionForBar(track, barStart, config, role, densityMult,
|
|
|
3871
4255
|
|
|
3872
4256
|
// src/generate/drums/index.ts
|
|
3873
4257
|
function generateDrums(opts) {
|
|
4258
|
+
assertPositiveInt(opts.bars, "drum bars");
|
|
4259
|
+
assertRange(opts.bpm, Number.MIN_VALUE, 1e3, "drum bpm");
|
|
4260
|
+
assertRange(opts.density, 0, 1, "drum density");
|
|
4261
|
+
assertGenerationBudget(opts.bars * 128, "drum hits");
|
|
4262
|
+
if (opts.euclideanKick) {
|
|
4263
|
+
const steps = opts.euclideanKick.steps ?? 16;
|
|
4264
|
+
assertInteger(steps, "euclidean steps", 1, 16);
|
|
4265
|
+
assertInteger(opts.euclideanKick.pulses, "euclidean pulses");
|
|
4266
|
+
assertInteger(opts.euclideanKick.rotation ?? 0, "euclidean rotation");
|
|
4267
|
+
}
|
|
3874
4268
|
const track = new HitList();
|
|
3875
4269
|
const rng = createRng(opts.seed ?? 0);
|
|
3876
4270
|
const mapping = mapStyle(opts.style);
|
|
@@ -3907,15 +4301,8 @@ function generateDrums(opts) {
|
|
|
3907
4301
|
const percMood = percMoodCategory(style);
|
|
3908
4302
|
const nextSection = opts.nextSection ? mapSection(opts.nextSection) : section;
|
|
3909
4303
|
const nextEnergy = sectionEnergy(nextSection);
|
|
3910
|
-
const
|
|
3911
|
-
|
|
3912
|
-
euclideanRhythm(
|
|
3913
|
-
opts.euclideanKick.pulses,
|
|
3914
|
-
opts.euclideanKick.steps ?? 16,
|
|
3915
|
-
opts.euclideanKick.rotation ?? 0
|
|
3916
|
-
)
|
|
3917
|
-
)
|
|
3918
|
-
) : void 0;
|
|
4304
|
+
const euclidSteps = opts.euclideanKick?.steps ?? 16;
|
|
4305
|
+
const euclidKick = opts.euclideanKick ? euclideanRhythm(opts.euclideanKick.pulses, euclidSteps, opts.euclideanKick.rotation ?? 0) : void 0;
|
|
3919
4306
|
const reuseSectionKick = (section === "b" || section === "chorus") && style !== "sparse";
|
|
3920
4307
|
let sectionKick;
|
|
3921
4308
|
for (let bar = 0; bar < opts.bars; bar += 1) {
|
|
@@ -3931,12 +4318,10 @@ function generateDrums(opts) {
|
|
|
3931
4318
|
barHasOpenHh = !track.hasCrashNear(barStart + openHhBeatIndex);
|
|
3932
4319
|
}
|
|
3933
4320
|
let kick;
|
|
3934
|
-
if (euclidKick) {
|
|
3935
|
-
kick = euclidKick;
|
|
3936
|
-
} else if (reuseSectionKick) {
|
|
4321
|
+
if (!euclidKick && reuseSectionKick) {
|
|
3937
4322
|
sectionKick ??= getKickPattern(section, style, 0, rng);
|
|
3938
4323
|
kick = sectionKick;
|
|
3939
|
-
} else {
|
|
4324
|
+
} else if (!euclidKick) {
|
|
3940
4325
|
kick = getKickPattern(section, style, bar, rng);
|
|
3941
4326
|
}
|
|
3942
4327
|
const inLift = isInPreChorusLift(section, bar, opts.bars, section === "b");
|
|
@@ -3977,7 +4362,23 @@ function generateDrums(opts) {
|
|
|
3977
4362
|
if (inLift) {
|
|
3978
4363
|
generatePreChorusBuildup(ctx, sec, isLastBar);
|
|
3979
4364
|
}
|
|
3980
|
-
|
|
4365
|
+
if (euclidKick && !inLift) {
|
|
4366
|
+
const stepLength = 4 / euclidSteps;
|
|
4367
|
+
for (let step = 0; step < euclidKick.length; step += 1) {
|
|
4368
|
+
const onset = step * stepLength;
|
|
4369
|
+
if (euclidKick[step] && Math.floor(onset) === beat) {
|
|
4370
|
+
const wholeBeat = Math.abs(onset - Math.round(onset)) < 1e-9;
|
|
4371
|
+
track.add(
|
|
4372
|
+
GM.BD,
|
|
4373
|
+
barStart + onset,
|
|
4374
|
+
Math.min(0.5, stepLength),
|
|
4375
|
+
velocity * (wholeBeat ? 1 : 0.85)
|
|
4376
|
+
);
|
|
4377
|
+
}
|
|
4378
|
+
}
|
|
4379
|
+
} else if (kick) {
|
|
4380
|
+
generateKickForBeat(ctx, sec, kick);
|
|
4381
|
+
}
|
|
3981
4382
|
generateSnareForBeat(ctx, sec, section === "intro" && bar === 0);
|
|
3982
4383
|
if (sec.useGhostNotes && !inLift) {
|
|
3983
4384
|
generateGhostNotesForBeat(ctx, sec);
|
|
@@ -3994,10 +4395,19 @@ function generateDrums(opts) {
|
|
|
3994
4395
|
}
|
|
3995
4396
|
}
|
|
3996
4397
|
}
|
|
4398
|
+
let percussionConfig = getPercussionConfig(percMood, section);
|
|
4399
|
+
if (role === "fxOnly" && !percussionConfig.tambourine && !percussionConfig.shaker && !percussionConfig.handclap) {
|
|
4400
|
+
percussionConfig = {
|
|
4401
|
+
tambourine: false,
|
|
4402
|
+
shaker: true,
|
|
4403
|
+
handclap: false,
|
|
4404
|
+
shaker16th: false
|
|
4405
|
+
};
|
|
4406
|
+
}
|
|
3997
4407
|
generateAuxPercussionForBar(
|
|
3998
4408
|
track,
|
|
3999
4409
|
barStart,
|
|
4000
|
-
|
|
4410
|
+
percussionConfig,
|
|
4001
4411
|
role,
|
|
4002
4412
|
densityMult,
|
|
4003
4413
|
rng,
|
|
@@ -4018,10 +4428,16 @@ var MIN_VELOCITY = 1;
|
|
|
4018
4428
|
var MAX_VELOCITY = 127;
|
|
4019
4429
|
function humanize(events, opts = {}) {
|
|
4020
4430
|
const ts = opts.ts ?? DEFAULT_TS3;
|
|
4431
|
+
assertTimeSignature(ts);
|
|
4432
|
+
assertNoteEvents(events, "humanize events");
|
|
4021
4433
|
const timing = opts.timing ?? DEFAULT_TIMING;
|
|
4022
4434
|
const velocityJitter = opts.velocity ?? DEFAULT_VELOCITY_JITTER;
|
|
4023
4435
|
const accent = opts.accent ?? DEFAULT_ACCENT;
|
|
4024
4436
|
const baseVelocity = opts.baseVelocity ?? DEFAULT_BASE_VELOCITY;
|
|
4437
|
+
assertRange(timing, 0, Number.MAX_SAFE_INTEGER, "humanize timing");
|
|
4438
|
+
assertRange(velocityJitter, 0, 127, "humanize velocity jitter");
|
|
4439
|
+
assertRange(accent, 0, 127, "humanize accent");
|
|
4440
|
+
assertRange(baseVelocity, 0, 127, "humanize base velocity");
|
|
4025
4441
|
const rng = createRng(opts.seed ?? 0);
|
|
4026
4442
|
return events.map((event) => {
|
|
4027
4443
|
const timingOffset = rng.float(-timing, timing);
|
|
@@ -4054,8 +4470,12 @@ function quantizeToGrid(beat, barBeats, subdivision, slotsPerBar) {
|
|
|
4054
4470
|
};
|
|
4055
4471
|
}
|
|
4056
4472
|
function extractGrooveTemplate(events, ts, subdivision) {
|
|
4473
|
+
assertTimeSignature(ts);
|
|
4474
|
+
assertNoteEvents(events, "groove source events");
|
|
4475
|
+
assertPositiveInt(subdivision, "groove subdivision");
|
|
4057
4476
|
const barBeats = beatsPerBar(ts);
|
|
4058
4477
|
const slotsPerBar = Math.round(barBeats * subdivision);
|
|
4478
|
+
assertGenerationBudget(slotsPerBar, "groove template slots");
|
|
4059
4479
|
const offsetSums = new Array(slotsPerBar).fill(0);
|
|
4060
4480
|
const velocitySums = new Array(slotsPerBar).fill(0);
|
|
4061
4481
|
const counts = new Array(slotsPerBar).fill(0);
|
|
@@ -4090,6 +4510,14 @@ function extractGrooveTemplate(events, ts, subdivision) {
|
|
|
4090
4510
|
return { subdivision, slotsPerBar, slots, ts };
|
|
4091
4511
|
}
|
|
4092
4512
|
function applyGrooveTemplate(events, template, ts) {
|
|
4513
|
+
assertTimeSignature(ts);
|
|
4514
|
+
assertNoteEvents(events, "groove target events");
|
|
4515
|
+
assertPositiveInt(template.subdivision, "template subdivision");
|
|
4516
|
+
assertPositiveInt(template.slotsPerBar, "template slotsPerBar");
|
|
4517
|
+
assertGenerationBudget(template.slots.length, "template slots");
|
|
4518
|
+
if (template.slots.length !== template.slotsPerBar) {
|
|
4519
|
+
throw new RangeError("template slots length must equal slotsPerBar");
|
|
4520
|
+
}
|
|
4093
4521
|
if (template.ts && (template.ts.numerator !== ts.numerator || template.ts.denominator !== ts.denominator)) {
|
|
4094
4522
|
throw new Error(
|
|
4095
4523
|
`Groove template meter ${formatTimeSignature(template.ts)} does not match apply-time meter ${formatTimeSignature(ts)}`
|
|
@@ -4119,32 +4547,17 @@ function applyGrooveTemplate(events, template, ts) {
|
|
|
4119
4547
|
function roleOf(pitch, chord, chordId = 0) {
|
|
4120
4548
|
const interval = ((Math.trunc(pitch) - chord.rootPc) % 12 + 12) % 12;
|
|
4121
4549
|
const tones = new Set(chord.intervals.map((i) => (i % 12 + 12) % 12));
|
|
4122
|
-
const hasHigherSeventh = tones.has(10) || tones.has(11);
|
|
4123
4550
|
const hasThird = tones.has(3) || tones.has(4);
|
|
4124
4551
|
const isSuspendedTone = !hasThird && (interval === 5 && tones.has(5) || interval === 2 && tones.has(2));
|
|
4552
|
+
const chordRole = chordToneRole(pitch, chord);
|
|
4125
4553
|
let role;
|
|
4126
4554
|
let lock;
|
|
4127
|
-
if (
|
|
4128
|
-
role = "root";
|
|
4129
|
-
lock = "identity";
|
|
4130
|
-
} else if (isSuspendedTone) {
|
|
4131
|
-
role = "third";
|
|
4132
|
-
lock = "quality";
|
|
4133
|
-
} else if (interval === 3 || interval === 4) {
|
|
4555
|
+
if (isSuspendedTone) {
|
|
4134
4556
|
role = "third";
|
|
4135
4557
|
lock = "quality";
|
|
4136
|
-
} else if (
|
|
4137
|
-
role =
|
|
4138
|
-
lock = "voicing";
|
|
4139
|
-
} else if (interval === 9 && tones.has(3) && tones.has(6) && !hasHigherSeventh) {
|
|
4140
|
-
role = "seventh";
|
|
4141
|
-
lock = "voicing";
|
|
4142
|
-
} else if (interval === 9 && tones.has(9) && !hasHigherSeventh) {
|
|
4143
|
-
role = "sixth";
|
|
4144
|
-
lock = "voicing";
|
|
4145
|
-
} else if (interval === 10 || interval === 11) {
|
|
4146
|
-
role = "seventh";
|
|
4147
|
-
lock = "voicing";
|
|
4558
|
+
} else if (chordRole !== null) {
|
|
4559
|
+
role = chordRole;
|
|
4560
|
+
lock = chordRole === "root" ? "identity" : chordRole === "third" ? "quality" : "voicing";
|
|
4148
4561
|
} else {
|
|
4149
4562
|
role = "tension";
|
|
4150
4563
|
lock = "voicing";
|
|
@@ -4162,16 +4575,6 @@ var TIE_BREAK_JITTER = 1e-6;
|
|
|
4162
4575
|
function pitchClass10(pitch) {
|
|
4163
4576
|
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
4164
4577
|
}
|
|
4165
|
-
function mulberry32(seed) {
|
|
4166
|
-
let state = seed >>> 0;
|
|
4167
|
-
return () => {
|
|
4168
|
-
state = state + 1831565813 >>> 0;
|
|
4169
|
-
let t = state;
|
|
4170
|
-
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
4171
|
-
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
4172
|
-
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
4173
|
-
};
|
|
4174
|
-
}
|
|
4175
4578
|
function inferKey(melody) {
|
|
4176
4579
|
let best = majorKey(0);
|
|
4177
4580
|
let bestScore = Number.NEGATIVE_INFINITY;
|
|
@@ -4210,7 +4613,6 @@ function buildCandidates(key, reharmonize) {
|
|
|
4210
4613
|
secondaryDominant: false,
|
|
4211
4614
|
base: 0
|
|
4212
4615
|
}));
|
|
4213
|
-
const tonic = tones[0] ?? 0;
|
|
4214
4616
|
if (reharmonize !== "diatonic") {
|
|
4215
4617
|
for (const target of [1, 3, 4, 5]) {
|
|
4216
4618
|
const targetRoot = tones[target] ?? 0;
|
|
@@ -4224,26 +4626,29 @@ function buildCandidates(key, reharmonize) {
|
|
|
4224
4626
|
}
|
|
4225
4627
|
}
|
|
4226
4628
|
if (reharmonize === "borrowed") {
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
rootPc: (tonic + 5) % 12,
|
|
4241
|
-
quality: "min",
|
|
4242
|
-
secondaryDominant: false,
|
|
4243
|
-
base: 2
|
|
4244
|
-
});
|
|
4629
|
+
const parallel = parallelKey(key);
|
|
4630
|
+
const parallelTones = scaleTonesInDegreeOrder(parallel);
|
|
4631
|
+
for (let degree = 0; degree < parallelTones.length; degree += 1) {
|
|
4632
|
+
const chord = diatonicTriad(degree, parallel);
|
|
4633
|
+
if (!isDiatonic(chord, key)) {
|
|
4634
|
+
candidates.push({
|
|
4635
|
+
rootPc: chord.rootPc,
|
|
4636
|
+
quality: chord.quality,
|
|
4637
|
+
secondaryDominant: false,
|
|
4638
|
+
base: 1.25
|
|
4639
|
+
});
|
|
4640
|
+
}
|
|
4641
|
+
}
|
|
4245
4642
|
}
|
|
4246
|
-
|
|
4643
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4644
|
+
return candidates.filter((candidate) => {
|
|
4645
|
+
const id = `${candidate.rootPc}:${candidate.quality}`;
|
|
4646
|
+
if (seen.has(id)) {
|
|
4647
|
+
return false;
|
|
4648
|
+
}
|
|
4649
|
+
seen.add(id);
|
|
4650
|
+
return true;
|
|
4651
|
+
});
|
|
4247
4652
|
}
|
|
4248
4653
|
function emissionCost(seg, cand, melody, key) {
|
|
4249
4654
|
const pcs = chordPitchClasses(makeChord(cand.rootPc, cand.quality));
|
|
@@ -4347,20 +4752,37 @@ function harmonizeOnce(melody, key, candidates, segments, jitter) {
|
|
|
4347
4752
|
return { cost: bestCost, path };
|
|
4348
4753
|
}
|
|
4349
4754
|
function harmonizeMelody(opts) {
|
|
4755
|
+
const noteIndex = createNoteEventIndex(opts.melody);
|
|
4350
4756
|
const key = opts.key === "infer" ? inferKey(opts.melody) : opts.key;
|
|
4351
4757
|
const candidates = buildCandidates(key, opts.reharmonize);
|
|
4352
4758
|
const candAt = (i) => candidates[i] ?? FALLBACK;
|
|
4353
|
-
const rng =
|
|
4354
|
-
const jitter = candidates.map(() => rng() * TIE_BREAK_JITTER);
|
|
4355
|
-
const melodyEnd =
|
|
4356
|
-
const
|
|
4759
|
+
const rng = createRng(opts.seed ?? 0);
|
|
4760
|
+
const jitter = candidates.map(() => rng.next() * TIE_BREAK_JITTER);
|
|
4761
|
+
const melodyEnd = noteIndex.notes.reduce((m, n) => Math.max(m, n.endBeat), 0);
|
|
4762
|
+
const requestedHr = assertRange(
|
|
4763
|
+
opts.harmonicRhythm,
|
|
4764
|
+
Number.MIN_VALUE,
|
|
4765
|
+
Number.MAX_SAFE_INTEGER,
|
|
4766
|
+
"harmonic rhythm"
|
|
4767
|
+
);
|
|
4768
|
+
const hr = Math.max(0.25, requestedHr);
|
|
4357
4769
|
const segCount = Math.max(1, Math.ceil(melodyEnd / hr));
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4770
|
+
assertGenerationBudget(segCount, "harmonic segments");
|
|
4771
|
+
assertGenerationBudget(segCount * candidates.length * candidates.length, "harmonization search");
|
|
4772
|
+
const segments = Array.from({ length: segCount }, (_, s) => ({
|
|
4773
|
+
startBeat: s * hr,
|
|
4774
|
+
endBeat: (s + 1) * hr,
|
|
4775
|
+
noteIndices: []
|
|
4776
|
+
}));
|
|
4777
|
+
let memberships = 0;
|
|
4778
|
+
for (const indexed of noteIndex.notes) {
|
|
4779
|
+
const first = Math.max(0, Math.floor(indexed.note.startBeat / hr));
|
|
4780
|
+
const lastExclusive = Math.min(segCount, Math.ceil(indexed.endBeat / hr - Number.EPSILON));
|
|
4781
|
+
memberships += Math.max(0, lastExclusive - first);
|
|
4782
|
+
assertGenerationBudget(memberships, "note-to-segment memberships");
|
|
4783
|
+
for (let segment = first; segment < lastExclusive; segment += 1) {
|
|
4784
|
+
segments[segment]?.noteIndices.push(indexed.originalIndex);
|
|
4785
|
+
}
|
|
4364
4786
|
}
|
|
4365
4787
|
const transposes = [0];
|
|
4366
4788
|
if (opts.placement.transposeSearch) {
|
|
@@ -4407,11 +4829,11 @@ function harmonizeMelody(opts) {
|
|
|
4407
4829
|
}
|
|
4408
4830
|
return chord;
|
|
4409
4831
|
});
|
|
4410
|
-
const melodyRoles = bestMelody.map((note,
|
|
4832
|
+
const melodyRoles = bestMelody.map((note, noteIndex2) => {
|
|
4411
4833
|
const segIdx = Math.min(segments.length - 1, Math.max(0, Math.floor(note.startBeat / hr)));
|
|
4412
4834
|
const cand = candAt(bestPath[segIdx] ?? 0);
|
|
4413
4835
|
const chord = makeChord(cand.rootPc, cand.quality);
|
|
4414
|
-
return { noteIndex, role: roleOf(note.pitch, chord).role };
|
|
4836
|
+
return { noteIndex: noteIndex2, role: roleOf(note.pitch, chord).role };
|
|
4415
4837
|
});
|
|
4416
4838
|
return { transposeSemitones: bestTs, key, chords, melodyRoles };
|
|
4417
4839
|
}
|
|
@@ -4458,29 +4880,30 @@ function nearestChordTone(pitch, chord) {
|
|
|
4458
4880
|
return pitch;
|
|
4459
4881
|
}
|
|
4460
4882
|
function cellOrigin(notes) {
|
|
4461
|
-
|
|
4883
|
+
let origin = Number.POSITIVE_INFINITY;
|
|
4884
|
+
for (const note of notes) {
|
|
4885
|
+
origin = Math.min(origin, note.startBeat);
|
|
4886
|
+
}
|
|
4887
|
+
return Number.isFinite(origin) ? origin : 0;
|
|
4888
|
+
}
|
|
4889
|
+
function cellEnd(notes) {
|
|
4890
|
+
let end = Number.NEGATIVE_INFINITY;
|
|
4891
|
+
for (const note of notes) {
|
|
4892
|
+
end = Math.max(end, note.startBeat + note.durationBeat);
|
|
4893
|
+
}
|
|
4894
|
+
return Number.isFinite(end) ? end : 0;
|
|
4462
4895
|
}
|
|
4463
4896
|
function cellSpan(cell) {
|
|
4464
4897
|
if (cell.notes.length === 0) {
|
|
4465
4898
|
return 0;
|
|
4466
4899
|
}
|
|
4467
4900
|
const start = cellOrigin(cell.notes);
|
|
4468
|
-
const end =
|
|
4901
|
+
const end = cellEnd(cell.notes);
|
|
4469
4902
|
return end - start;
|
|
4470
4903
|
}
|
|
4471
4904
|
function clone(cell) {
|
|
4472
4905
|
return { notes: cell.notes.map((n) => ({ ...n })) };
|
|
4473
4906
|
}
|
|
4474
|
-
function mulberry322(seed) {
|
|
4475
|
-
let state = seed >>> 0;
|
|
4476
|
-
return () => {
|
|
4477
|
-
state = state + 1831565813 >>> 0;
|
|
4478
|
-
let t = state;
|
|
4479
|
-
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
4480
|
-
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
4481
|
-
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
4482
|
-
};
|
|
4483
|
-
}
|
|
4484
4907
|
function contourOffsets(contour, count) {
|
|
4485
4908
|
const offsets = [];
|
|
4486
4909
|
for (let i = 0; i < count; i += 1) {
|
|
@@ -4503,19 +4926,21 @@ function contourOffsets(contour, count) {
|
|
|
4503
4926
|
}
|
|
4504
4927
|
function generateMotif(opts) {
|
|
4505
4928
|
const contour = opts.contour ?? "arch";
|
|
4506
|
-
const bars =
|
|
4929
|
+
const bars = assertPositiveInt(opts.bars, "motif bars");
|
|
4507
4930
|
const totalBeats = bars * 4;
|
|
4508
4931
|
const noteCount = Math.max(3, bars * 2);
|
|
4932
|
+
assertGenerationBudget(noteCount, "motif notes");
|
|
4509
4933
|
const beatsPerNote = totalBeats / noteCount;
|
|
4510
|
-
const rng =
|
|
4511
|
-
const
|
|
4934
|
+
const rng = createRng(opts.seed ?? 0);
|
|
4935
|
+
const requestedJitter = assertFiniteNumber(opts.jitter ?? 0, "motif jitter");
|
|
4936
|
+
const jitterProb = Math.min(1, Math.max(0, requestedJitter));
|
|
4512
4937
|
const tonic = pitchClass11(opts.key.rootPc) + 60;
|
|
4513
4938
|
const offsets = contourOffsets(contour, noteCount);
|
|
4514
4939
|
const notes = [];
|
|
4515
4940
|
for (let i = 0; i < noteCount; i += 1) {
|
|
4516
4941
|
let jitter = 0;
|
|
4517
|
-
if (jitterProb > 0 && rng() < jitterProb) {
|
|
4518
|
-
jitter = rng() < 0.5 ? 1 : -1;
|
|
4942
|
+
if (jitterProb > 0 && rng.next() < jitterProb) {
|
|
4943
|
+
jitter = rng.next() < 0.5 ? 1 : -1;
|
|
4519
4944
|
}
|
|
4520
4945
|
let pitch = stepDiatonic(tonic, (offsets[i] ?? 0) + jitter, opts.key);
|
|
4521
4946
|
const startBeat = i * beatsPerNote;
|
|
@@ -4527,6 +4952,16 @@ function generateMotif(opts) {
|
|
|
4527
4952
|
return { notes };
|
|
4528
4953
|
}
|
|
4529
4954
|
function transformMotif(cell, t, amount, key) {
|
|
4955
|
+
assertNoteEvents(cell.notes, "motif notes");
|
|
4956
|
+
if (amount !== void 0) {
|
|
4957
|
+
assertFiniteNumber(amount, "motif transform amount");
|
|
4958
|
+
}
|
|
4959
|
+
if ((t === "transposeDiatonic" || t === "sequence") && amount !== void 0) {
|
|
4960
|
+
assertInteger(amount, "diatonic transform amount");
|
|
4961
|
+
}
|
|
4962
|
+
if ((t === "augment" || t === "diminish") && amount !== void 0 && amount <= 0) {
|
|
4963
|
+
throw new RangeError("time transform amount must be positive");
|
|
4964
|
+
}
|
|
4530
4965
|
const notes = cell.notes;
|
|
4531
4966
|
switch (t) {
|
|
4532
4967
|
case "transposeChromatic": {
|
|
@@ -4553,7 +4988,7 @@ function transformMotif(cell, t, amount, key) {
|
|
|
4553
4988
|
return { notes: [] };
|
|
4554
4989
|
}
|
|
4555
4990
|
const start = cellOrigin(notes);
|
|
4556
|
-
const end =
|
|
4991
|
+
const end = cellEnd(notes);
|
|
4557
4992
|
return {
|
|
4558
4993
|
notes: notes.map((n) => ({
|
|
4559
4994
|
pitch: n.pitch,
|
|
@@ -4586,8 +5021,10 @@ function scaleTime(cell, factor) {
|
|
|
4586
5021
|
};
|
|
4587
5022
|
}
|
|
4588
5023
|
function developMotif(cell, timeline, key, bars) {
|
|
5024
|
+
assertPositiveInt(bars, "development bars");
|
|
5025
|
+
assertNoteEvents(cell.notes, "motif notes");
|
|
4589
5026
|
const span = cellSpan(cell);
|
|
4590
|
-
const totalBeats =
|
|
5027
|
+
const totalBeats = bars * 4;
|
|
4591
5028
|
const origin = cellOrigin(cell.notes);
|
|
4592
5029
|
const out = [];
|
|
4593
5030
|
if (span <= 0) {
|
|
@@ -4595,6 +5032,7 @@ function developMotif(cell, timeline, key, bars) {
|
|
|
4595
5032
|
}
|
|
4596
5033
|
const tileSpan = Math.max(span, MIN_TILE_SPAN);
|
|
4597
5034
|
const tileCount = Math.ceil(totalBeats / tileSpan);
|
|
5035
|
+
assertGenerationBudget(tileCount * cell.notes.length, "developed motif notes");
|
|
4598
5036
|
for (let k = 0; k < tileCount; k += 1) {
|
|
4599
5037
|
const offset = k * tileSpan;
|
|
4600
5038
|
for (const n of cell.notes) {
|
|
@@ -4793,16 +5231,6 @@ function progressions() {
|
|
|
4793
5231
|
function progressionsByStyle(style) {
|
|
4794
5232
|
return progressions().filter((p) => p.styles.includes(style));
|
|
4795
5233
|
}
|
|
4796
|
-
function mulberry323(seed) {
|
|
4797
|
-
let state = seed >>> 0;
|
|
4798
|
-
return () => {
|
|
4799
|
-
state = state + 1831565813 >>> 0;
|
|
4800
|
-
let t = state;
|
|
4801
|
-
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
4802
|
-
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
4803
|
-
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
4804
|
-
};
|
|
4805
|
-
}
|
|
4806
5234
|
function degreeToRootPc(degree, key) {
|
|
4807
5235
|
if (degree >= 0 && degree <= 6) {
|
|
4808
5236
|
const tones = scaleTonesInDegreeOrder(key);
|
|
@@ -4824,6 +5252,8 @@ function autoQuality(degree, key) {
|
|
|
4824
5252
|
return "maj";
|
|
4825
5253
|
}
|
|
4826
5254
|
function generateProgression(opts) {
|
|
5255
|
+
assertPositiveInt(opts.bars, "progression bars");
|
|
5256
|
+
assertGenerationBudget(opts.bars, "progression chords");
|
|
4827
5257
|
const seed = opts.seed ?? 0;
|
|
4828
5258
|
let preset;
|
|
4829
5259
|
if (opts.presetId !== void 0) {
|
|
@@ -4835,8 +5265,8 @@ function generateProgression(opts) {
|
|
|
4835
5265
|
if (preset === void 0) {
|
|
4836
5266
|
const pool = PRESETS.filter((p) => p.styles.includes(opts.style));
|
|
4837
5267
|
const candidates = pool.length > 0 ? pool : PRESETS;
|
|
4838
|
-
const rng =
|
|
4839
|
-
const index = Math.floor(rng() * candidates.length) % candidates.length;
|
|
5268
|
+
const rng = createRng(seed);
|
|
5269
|
+
const index = Math.floor(rng.next() * candidates.length) % candidates.length;
|
|
4840
5270
|
preset = candidates[index] ?? PRESETS[0];
|
|
4841
5271
|
}
|
|
4842
5272
|
const degrees = preset?.degrees ?? [0];
|
|
@@ -4854,7 +5284,7 @@ function generateProgression(opts) {
|
|
|
4854
5284
|
}
|
|
4855
5285
|
if (opts.reharmonize) {
|
|
4856
5286
|
const tonicPc = (opts.key.rootPc % 12 + 12) % 12;
|
|
4857
|
-
const rrng =
|
|
5287
|
+
const rrng = createRng((seed ^ 2654435769) >>> 0);
|
|
4858
5288
|
for (let i = 0; i < chords.length - 1; i += 1) {
|
|
4859
5289
|
const cur = chords[i];
|
|
4860
5290
|
const next = chords[i + 1];
|
|
@@ -4868,7 +5298,7 @@ function generateProgression(opts) {
|
|
|
4868
5298
|
if (targetsTonic || alreadySecondary || isResolutionTarget) {
|
|
4869
5299
|
continue;
|
|
4870
5300
|
}
|
|
4871
|
-
if (rrng() < 0.5) {
|
|
5301
|
+
if (rrng.next() < 0.5) {
|
|
4872
5302
|
chords[i] = {
|
|
4873
5303
|
rootPc: domRoot,
|
|
4874
5304
|
quality: "dom7",
|
|
@@ -4896,11 +5326,6 @@ function isStableTarget(chord) {
|
|
|
4896
5326
|
const hasThird = hasInterval(chord, 3) || hasInterval(chord, 4);
|
|
4897
5327
|
return hasFifth && hasThird && !isDominantType(chord);
|
|
4898
5328
|
}
|
|
4899
|
-
function triadPitchClasses(chord) {
|
|
4900
|
-
const third = hasInterval(chord, 4) ? 4 : hasInterval(chord, 3) ? 3 : 4;
|
|
4901
|
-
const fifth = hasInterval(chord, 8) ? 8 : hasInterval(chord, 6) ? 6 : 7;
|
|
4902
|
-
return [mod123(chord.rootPc), mod123(chord.rootPc + third), mod123(chord.rootPc + fifth)];
|
|
4903
|
-
}
|
|
4904
5329
|
function commonToneCount(a, b) {
|
|
4905
5330
|
const set = new Set(b);
|
|
4906
5331
|
return a.filter((pc) => set.has(pc)).length;
|
|
@@ -4918,7 +5343,7 @@ function sameChord2(a, b) {
|
|
|
4918
5343
|
}
|
|
4919
5344
|
var THIRD_OFFSETS = [3, 4, 8, 9];
|
|
4920
5345
|
function substituteChord(chord, key, opts) {
|
|
4921
|
-
const original =
|
|
5346
|
+
const original = chordPitchClasses(chord);
|
|
4922
5347
|
const candidates = [];
|
|
4923
5348
|
if (isDominantType(chord)) {
|
|
4924
5349
|
candidates.push({ chord: makeChord(mod123(chord.rootPc + 6), "dom7"), type: "tritone" });
|
|
@@ -5039,11 +5464,15 @@ function generateRhythm(ts, opts = {}) {
|
|
|
5039
5464
|
const seed = opts.seed ?? 0;
|
|
5040
5465
|
const bars = opts.bars ?? DEFAULT_BARS;
|
|
5041
5466
|
const subdivision = opts.subdivision ?? DEFAULT_SUBDIVISION;
|
|
5042
|
-
const
|
|
5467
|
+
const requestedDensity = assertFiniteNumber(opts.density ?? DEFAULT_DENSITY, "rhythm density");
|
|
5468
|
+
const density = Math.min(1, Math.max(0, requestedDensity));
|
|
5469
|
+
assertPositiveInt(bars, "rhythm bars");
|
|
5470
|
+
assertPositiveInt(subdivision, "rhythm subdivision");
|
|
5043
5471
|
const barBeats = beatsPerBar(ts);
|
|
5044
5472
|
const spanBeats = barBeats * bars;
|
|
5045
5473
|
const step = 1 / subdivision;
|
|
5046
5474
|
const slotCount = Math.round(spanBeats * subdivision);
|
|
5475
|
+
assertGenerationBudget(slotCount, "rhythm grid slots");
|
|
5047
5476
|
const slotsPerBar = Math.round(barBeats * subdivision);
|
|
5048
5477
|
const rng = createRng(seed);
|
|
5049
5478
|
const positions = [];
|
|
@@ -5348,10 +5777,58 @@ function spellScale(tonic, key) {
|
|
|
5348
5777
|
function spellPitchClasses(pcs, tonic, key) {
|
|
5349
5778
|
return pcs.map((pc) => spellPitchClass(pc, tonic, key));
|
|
5350
5779
|
}
|
|
5780
|
+
function chordLetterOffset(interval, chord) {
|
|
5781
|
+
switch (interval) {
|
|
5782
|
+
case 0:
|
|
5783
|
+
case 12:
|
|
5784
|
+
return 0;
|
|
5785
|
+
case 1:
|
|
5786
|
+
case 2:
|
|
5787
|
+
case 13:
|
|
5788
|
+
case 14:
|
|
5789
|
+
return 1;
|
|
5790
|
+
case 3:
|
|
5791
|
+
case 4:
|
|
5792
|
+
case 15:
|
|
5793
|
+
case 16:
|
|
5794
|
+
return 2;
|
|
5795
|
+
case 5:
|
|
5796
|
+
case 17:
|
|
5797
|
+
case 18:
|
|
5798
|
+
return 3;
|
|
5799
|
+
case 6:
|
|
5800
|
+
case 7:
|
|
5801
|
+
case 8:
|
|
5802
|
+
case 19:
|
|
5803
|
+
return 4;
|
|
5804
|
+
case 9:
|
|
5805
|
+
return chord.quality === "dim7" ? 6 : 5;
|
|
5806
|
+
case 20:
|
|
5807
|
+
case 21:
|
|
5808
|
+
return 5;
|
|
5809
|
+
case 10:
|
|
5810
|
+
case 11:
|
|
5811
|
+
case 22:
|
|
5812
|
+
case 23:
|
|
5813
|
+
return 6;
|
|
5814
|
+
default: {
|
|
5815
|
+
const octaves = Math.floor(Math.max(0, interval) / 12);
|
|
5816
|
+
const simple = mod124(interval);
|
|
5817
|
+
const simpleOffset = [0, 1, 1, 2, 2, 3, 4, 4, 4, 5, 6, 6][simple] ?? 0;
|
|
5818
|
+
return octaves * 7 + simpleOffset;
|
|
5819
|
+
}
|
|
5820
|
+
}
|
|
5821
|
+
}
|
|
5822
|
+
function spellChordFromRoot(chord, root) {
|
|
5823
|
+
return chord.intervals.map((interval) => {
|
|
5824
|
+
const letter = mod72(root.letter + chordLetterOffset(interval, chord));
|
|
5825
|
+
const pc = mod124(chord.rootPc + interval);
|
|
5826
|
+
return { letter, alter: alterFor(letter, pc) };
|
|
5827
|
+
});
|
|
5828
|
+
}
|
|
5351
5829
|
function spellChord(chord, tonic, key) {
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
);
|
|
5830
|
+
const root = spellPitchClass(chord.rootPc, tonic, key);
|
|
5831
|
+
return spellChordFromRoot(chord, root);
|
|
5355
5832
|
}
|
|
5356
5833
|
function noteNames(notes) {
|
|
5357
5834
|
return notes.map(formatNote);
|
|
@@ -5644,12 +6121,23 @@ function resolveRanges(opts) {
|
|
|
5644
6121
|
if (opts.ranges.length === 0) {
|
|
5645
6122
|
throw new Error("ranges must contain at least one voice range");
|
|
5646
6123
|
}
|
|
5647
|
-
|
|
6124
|
+
assertGenerationBudget(opts.ranges.length, "voice ranges", 128);
|
|
6125
|
+
return opts.ranges.map((range, index) => {
|
|
6126
|
+
assertFiniteNumber(range.min, `ranges[${index}].min`);
|
|
6127
|
+
assertFiniteNumber(range.max, `ranges[${index}].max`);
|
|
6128
|
+
if (range.min > range.max) {
|
|
6129
|
+
throw new RangeError(`ranges[${index}].min must not exceed max`);
|
|
6130
|
+
}
|
|
6131
|
+
assertGenerationBudget(
|
|
6132
|
+
Math.floor(range.max) - Math.ceil(range.min) + 1,
|
|
6133
|
+
`ranges[${index}] span`,
|
|
6134
|
+
4096
|
|
6135
|
+
);
|
|
6136
|
+
return { ...range };
|
|
6137
|
+
});
|
|
5648
6138
|
}
|
|
5649
6139
|
const voices = opts?.voices ?? 4;
|
|
5650
|
-
|
|
5651
|
-
throw new Error("voices must be at least 1");
|
|
5652
|
-
}
|
|
6140
|
+
assertPositiveInt(voices, "voices", 128);
|
|
5653
6141
|
if (voices === 4) {
|
|
5654
6142
|
return SATB_RANGES.map((range) => ({ ...range }));
|
|
5655
6143
|
}
|
|
@@ -5668,6 +6156,8 @@ function resolveRanges(opts) {
|
|
|
5668
6156
|
function voiceChord(chord, opts) {
|
|
5669
6157
|
const ranges = resolveRanges(opts);
|
|
5670
6158
|
const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;
|
|
6159
|
+
assertFiniteNumber(maxSpacing, "maxSpacing");
|
|
6160
|
+
if (maxSpacing < 0) throw new RangeError("maxSpacing must be non-negative");
|
|
5671
6161
|
const candidates = enumerateVoicings(chord, ranges, maxSpacing);
|
|
5672
6162
|
let best;
|
|
5673
6163
|
let bestScore = Number.POSITIVE_INFINITY;
|
|
@@ -5692,8 +6182,11 @@ function voiceChord(chord, opts) {
|
|
|
5692
6182
|
return best;
|
|
5693
6183
|
}
|
|
5694
6184
|
function voiceProgression(chords, opts) {
|
|
6185
|
+
assertGenerationBudget(chords.length * 4e3, "voiced progression search");
|
|
5695
6186
|
const ranges = resolveRanges(opts);
|
|
5696
6187
|
const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;
|
|
6188
|
+
assertFiniteNumber(maxSpacing, "maxSpacing");
|
|
6189
|
+
if (maxSpacing < 0) throw new RangeError("maxSpacing must be non-negative");
|
|
5697
6190
|
const result = [];
|
|
5698
6191
|
let prev;
|
|
5699
6192
|
for (const chord of chords) {
|
|
@@ -5799,7 +6292,12 @@ function nearestPc(target, pcs) {
|
|
|
5799
6292
|
}
|
|
5800
6293
|
function voiceChordStyled(chord, opts) {
|
|
5801
6294
|
const style = opts?.style ?? "close";
|
|
5802
|
-
const
|
|
6295
|
+
const octave = opts?.octave ?? DEFAULT_STYLE_OCTAVE;
|
|
6296
|
+
assertInteger(octave, "styled voicing octave", -10, 10);
|
|
6297
|
+
if (opts?.topNote !== void 0) {
|
|
6298
|
+
assertFiniteNumber(opts.topNote, "styled voicing topNote");
|
|
6299
|
+
}
|
|
6300
|
+
const base = 12 * (octave + 1);
|
|
5803
6301
|
const rootPc = pitchClass14(chord.rootPc);
|
|
5804
6302
|
const bassPc = pitchClass14(chord.bassPc ?? chord.rootPc);
|
|
5805
6303
|
const omitRoot = style === "rootless" || opts?.rootless === true;
|
|
@@ -5815,11 +6313,11 @@ function voiceChordStyled(chord, opts) {
|
|
|
5815
6313
|
const guide = roles.includes("seventh") ? "seventh" : roles.includes("sixth") ? "sixth" : "fifth";
|
|
5816
6314
|
order = order.filter((_pc, i) => {
|
|
5817
6315
|
const role = roles[i];
|
|
5818
|
-
return role === "root" || role === "third" || role === guide;
|
|
6316
|
+
return _pc === bassPc || role === "root" || role === "third" || role === guide;
|
|
5819
6317
|
});
|
|
5820
6318
|
}
|
|
5821
6319
|
if (omitRoot) {
|
|
5822
|
-
order = order.filter((pc) => pc !== rootPc);
|
|
6320
|
+
order = order.filter((pc) => pc !== rootPc || chord.bassPc !== void 0 && pc === bassPc);
|
|
5823
6321
|
}
|
|
5824
6322
|
if (order.length === 0) {
|
|
5825
6323
|
return [];
|
|
@@ -5848,6 +6346,18 @@ function voiceChordStyled(chord, opts) {
|
|
|
5848
6346
|
} else if (style === "drop3" && stack.length >= 3) {
|
|
5849
6347
|
stack[stack.length - 3] = (stack[stack.length - 3] ?? 0) - 12;
|
|
5850
6348
|
}
|
|
6349
|
+
if (chord.bassPc !== void 0) {
|
|
6350
|
+
const bassIndexInStack = stack.findIndex((pitch) => pitchClass14(pitch) === bassPc);
|
|
6351
|
+
if (bassIndexInStack >= 0) {
|
|
6352
|
+
const otherLowest = Math.min(
|
|
6353
|
+
...stack.filter((_pitch, index) => index !== bassIndexInStack),
|
|
6354
|
+
Number.POSITIVE_INFINITY
|
|
6355
|
+
);
|
|
6356
|
+
while ((stack[bassIndexInStack] ?? 0) >= otherLowest) {
|
|
6357
|
+
stack[bassIndexInStack] = (stack[bassIndexInStack] ?? 0) - 12;
|
|
6358
|
+
}
|
|
6359
|
+
}
|
|
6360
|
+
}
|
|
5851
6361
|
return stack.sort((a, b) => a - b);
|
|
5852
6362
|
}
|
|
5853
6363
|
|
|
@@ -6143,7 +6653,11 @@ var Chord = class _Chord {
|
|
|
6143
6653
|
* @param key Optional key context for analysis methods.
|
|
6144
6654
|
*/
|
|
6145
6655
|
constructor(data, key) {
|
|
6146
|
-
|
|
6656
|
+
const copy = copyChord(data);
|
|
6657
|
+
if (copy.rootSpelling === void 0 && key !== void 0) {
|
|
6658
|
+
copy.rootSpelling = spellPitchClass(copy.rootPc, key.tonic.data, key.scale);
|
|
6659
|
+
}
|
|
6660
|
+
this.#data = copy;
|
|
6147
6661
|
this.#key = key;
|
|
6148
6662
|
}
|
|
6149
6663
|
/**
|
|
@@ -6155,8 +6669,13 @@ var Chord = class _Chord {
|
|
|
6155
6669
|
* @returns The chord (without key context).
|
|
6156
6670
|
*/
|
|
6157
6671
|
static of(root, quality, bass) {
|
|
6158
|
-
|
|
6159
|
-
|
|
6672
|
+
if (typeof root === "string") {
|
|
6673
|
+
const rootNote = Note.of(root);
|
|
6674
|
+
const data = makeChord(rootNote.pitchClass, quality, bass);
|
|
6675
|
+
data.rootSpelling = { letter: rootNote.letter, alter: rootNote.alter };
|
|
6676
|
+
return new _Chord(data);
|
|
6677
|
+
}
|
|
6678
|
+
return new _Chord(makeChord(root, quality, bass));
|
|
6160
6679
|
}
|
|
6161
6680
|
/**
|
|
6162
6681
|
* Wrap an existing plain chord object.
|
|
@@ -6183,8 +6702,8 @@ var Chord = class _Chord {
|
|
|
6183
6702
|
* @param pitches MIDI pitches or bare pitch classes.
|
|
6184
6703
|
* @returns Ranked chord interpretations (may be empty).
|
|
6185
6704
|
*/
|
|
6186
|
-
static detect(pitches) {
|
|
6187
|
-
return detectChord(pitches).map(
|
|
6705
|
+
static detect(pitches, opts) {
|
|
6706
|
+
return detectChord(pitches, opts).map(
|
|
6188
6707
|
(match) => new _Chord(makeChord(match.rootPc, match.quality, match.bassPc))
|
|
6189
6708
|
);
|
|
6190
6709
|
}
|
|
@@ -6194,8 +6713,8 @@ var Chord = class _Chord {
|
|
|
6194
6713
|
* @param pitches MIDI pitches or bare pitch classes.
|
|
6195
6714
|
* @returns The top-ranked chord, or null when nothing matches.
|
|
6196
6715
|
*/
|
|
6197
|
-
static detectBest(pitches) {
|
|
6198
|
-
const best = detectChordBest(pitches);
|
|
6716
|
+
static detectBest(pitches, opts) {
|
|
6717
|
+
const best = detectChordBest(pitches, opts);
|
|
6199
6718
|
return best === null ? null : new _Chord(best);
|
|
6200
6719
|
}
|
|
6201
6720
|
/** The root pitch class (0..11). */
|
|
@@ -6353,8 +6872,14 @@ var Chord = class _Chord {
|
|
|
6353
6872
|
const data = copyChord(this.#data);
|
|
6354
6873
|
if (index === 0) {
|
|
6355
6874
|
delete data.bassPc;
|
|
6875
|
+
delete data.bassSpelling;
|
|
6356
6876
|
} else {
|
|
6357
6877
|
data.bassPc = mod125(this.#data.rootPc + (intervals[index] ?? 0));
|
|
6878
|
+
const rootSpelling = data.rootSpelling ?? (this.#key ? spellPitchClass(data.rootPc, this.#key.tonic.data, this.#key.scale) : spellPitchClassBare(data.rootPc, "sharp"));
|
|
6879
|
+
const bassSpelling = spellChordFromRoot(data, rootSpelling)[index];
|
|
6880
|
+
if (bassSpelling !== void 0) {
|
|
6881
|
+
data.bassSpelling = bassSpelling;
|
|
6882
|
+
}
|
|
6358
6883
|
}
|
|
6359
6884
|
return new _Chord(data, this.#key);
|
|
6360
6885
|
}
|
|
@@ -6706,6 +7231,7 @@ var Key = class _Key {
|
|
|
6706
7231
|
BLUES_MASK,
|
|
6707
7232
|
CHROMATIC_MASK,
|
|
6708
7233
|
Chord,
|
|
7234
|
+
DEFAULT_GENERATION_BUDGET,
|
|
6709
7235
|
DORIAN_MASK,
|
|
6710
7236
|
HARMONIC_MINOR_MASK,
|
|
6711
7237
|
Interval,
|
|
@@ -6735,6 +7261,14 @@ var Key = class _Key {
|
|
|
6735
7261
|
analyzeChord,
|
|
6736
7262
|
analyzeVoice,
|
|
6737
7263
|
applyGrooveTemplate,
|
|
7264
|
+
assertFiniteNumber,
|
|
7265
|
+
assertGenerationBudget,
|
|
7266
|
+
assertInteger,
|
|
7267
|
+
assertNoteEvent,
|
|
7268
|
+
assertNoteEvents,
|
|
7269
|
+
assertPositiveInt,
|
|
7270
|
+
assertRange,
|
|
7271
|
+
assertTimeSignature,
|
|
6738
7272
|
availableTensions,
|
|
6739
7273
|
avoidNotes,
|
|
6740
7274
|
barPositionToBeat,
|
|
@@ -6753,6 +7287,7 @@ var Key = class _Key {
|
|
|
6753
7287
|
chordToRoman,
|
|
6754
7288
|
chordToneRole,
|
|
6755
7289
|
classifyInterval,
|
|
7290
|
+
createNoteEventIndex,
|
|
6756
7291
|
createRng,
|
|
6757
7292
|
createsHiddenParallelPerfect,
|
|
6758
7293
|
createsParallelOctave,
|