@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/model/index.d.cts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as
|
|
3
|
-
import {
|
|
1
|
+
import { D as DetectChordOptions } from '../index-CnZCo29u.cjs';
|
|
2
|
+
import { H as HarmonicFunction, a as ChordAnalysis, C as Cadence, B as BorrowedSource } from '../function-DYt4iKsn.cjs';
|
|
3
|
+
import { a as ChordQuality, C as Chord$1 } from '../index-BNlGP86A.cjs';
|
|
4
|
+
import { d as VoicingOptions, c as StyledVoicingOptions, C as ChordScaleMatch } from '../styled-ZTbdaVFM.cjs';
|
|
4
5
|
import { N as Note$1, S as SpelledInterval } from '../index-CGMZNCx6.cjs';
|
|
5
6
|
import { K as KeyScale } from '../types-MGP29r3t.cjs';
|
|
6
7
|
|
|
@@ -399,14 +400,14 @@ declare class Chord {
|
|
|
399
400
|
* @param pitches MIDI pitches or bare pitch classes.
|
|
400
401
|
* @returns Ranked chord interpretations (may be empty).
|
|
401
402
|
*/
|
|
402
|
-
static detect(pitches: number[]): Chord[];
|
|
403
|
+
static detect(pitches: number[], opts?: DetectChordOptions): Chord[];
|
|
403
404
|
/**
|
|
404
405
|
* The single best chord interpretation of a pitch set.
|
|
405
406
|
*
|
|
406
407
|
* @param pitches MIDI pitches or bare pitch classes.
|
|
407
408
|
* @returns The top-ranked chord, or null when nothing matches.
|
|
408
409
|
*/
|
|
409
|
-
static detectBest(pitches: number[]): Chord | null;
|
|
410
|
+
static detectBest(pitches: number[], opts?: DetectChordOptions): Chord | null;
|
|
410
411
|
/** The root pitch class (0..11). */
|
|
411
412
|
get rootPc(): number;
|
|
412
413
|
/** The chord quality. */
|
package/dist/model/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as
|
|
3
|
-
import {
|
|
1
|
+
import { D as DetectChordOptions } from '../index-JnF4v9bx.js';
|
|
2
|
+
import { H as HarmonicFunction, a as ChordAnalysis, C as Cadence, B as BorrowedSource } from '../function-DVH4Eusr.js';
|
|
3
|
+
import { a as ChordQuality, C as Chord$1 } from '../index-Bt4tOUD9.js';
|
|
4
|
+
import { d as VoicingOptions, c as StyledVoicingOptions, C as ChordScaleMatch } from '../styled-DZ7GjShb.js';
|
|
4
5
|
import { N as Note$1, S as SpelledInterval } from '../index-CGMZNCx6.js';
|
|
5
6
|
import { K as KeyScale } from '../types-MGP29r3t.js';
|
|
6
7
|
|
|
@@ -399,14 +400,14 @@ declare class Chord {
|
|
|
399
400
|
* @param pitches MIDI pitches or bare pitch classes.
|
|
400
401
|
* @returns Ranked chord interpretations (may be empty).
|
|
401
402
|
*/
|
|
402
|
-
static detect(pitches: number[]): Chord[];
|
|
403
|
+
static detect(pitches: number[], opts?: DetectChordOptions): Chord[];
|
|
403
404
|
/**
|
|
404
405
|
* The single best chord interpretation of a pitch set.
|
|
405
406
|
*
|
|
406
407
|
* @param pitches MIDI pitches or bare pitch classes.
|
|
407
408
|
* @returns The top-ranked chord, or null when nothing matches.
|
|
408
409
|
*/
|
|
409
|
-
static detectBest(pitches: number[]): Chord | null;
|
|
410
|
+
static detectBest(pitches: number[], opts?: DetectChordOptions): Chord | null;
|
|
410
411
|
/** The root pitch class (0..11). */
|
|
411
412
|
get rootPc(): number;
|
|
412
413
|
/** The chord quality. */
|
package/dist/model/index.js
CHANGED
|
@@ -4,13 +4,13 @@ import {
|
|
|
4
4
|
Key,
|
|
5
5
|
Note,
|
|
6
6
|
Progression
|
|
7
|
-
} from "../chunk-
|
|
8
|
-
import "../chunk-
|
|
9
|
-
import "../chunk-
|
|
10
|
-
import "../chunk-
|
|
7
|
+
} from "../chunk-SGU7SPLK.js";
|
|
8
|
+
import "../chunk-IT2O4XFE.js";
|
|
9
|
+
import "../chunk-CHMXELDS.js";
|
|
10
|
+
import "../chunk-C5XYKUVG.js";
|
|
11
11
|
import "../chunk-DYWFNC4A.js";
|
|
12
|
-
import "../chunk-
|
|
13
|
-
import "../chunk-
|
|
12
|
+
import "../chunk-MIWO425M.js";
|
|
13
|
+
import "../chunk-FBQK3ZQN.js";
|
|
14
14
|
export {
|
|
15
15
|
Chord,
|
|
16
16
|
Interval,
|
package/dist/theory/index.cjs
CHANGED
|
@@ -352,8 +352,10 @@ function classifySeventh(thirdIc, fifthIc, seventhIc) {
|
|
|
352
352
|
function stackThirds(degree, key, size) {
|
|
353
353
|
const tones = scaleTonesInDegreeOrder(key);
|
|
354
354
|
const length = tones.length;
|
|
355
|
-
if (length
|
|
356
|
-
|
|
355
|
+
if (length !== 7) {
|
|
356
|
+
throw new Error(
|
|
357
|
+
`diatonic chord stacking requires a heptatonic scale (received ${length} tones)`
|
|
358
|
+
);
|
|
357
359
|
}
|
|
358
360
|
const idx = (step) => ((degree + step) % length + length) % length;
|
|
359
361
|
const rootPc = tones[idx(0)] ?? pitchClass2(key.rootPc);
|
|
@@ -671,32 +673,17 @@ function isLeadingToneResolution(prev, cur, key) {
|
|
|
671
673
|
function roleOf(pitch, chord, chordId = 0) {
|
|
672
674
|
const interval = ((Math.trunc(pitch) - chord.rootPc) % 12 + 12) % 12;
|
|
673
675
|
const tones = new Set(chord.intervals.map((i) => (i % 12 + 12) % 12));
|
|
674
|
-
const hasHigherSeventh = tones.has(10) || tones.has(11);
|
|
675
676
|
const hasThird = tones.has(3) || tones.has(4);
|
|
676
677
|
const isSuspendedTone = !hasThird && (interval === 5 && tones.has(5) || interval === 2 && tones.has(2));
|
|
678
|
+
const chordRole = chordToneRole(pitch, chord);
|
|
677
679
|
let role;
|
|
678
680
|
let lock;
|
|
679
|
-
if (
|
|
680
|
-
role = "root";
|
|
681
|
-
lock = "identity";
|
|
682
|
-
} else if (isSuspendedTone) {
|
|
681
|
+
if (isSuspendedTone) {
|
|
683
682
|
role = "third";
|
|
684
683
|
lock = "quality";
|
|
685
|
-
} else if (
|
|
686
|
-
role =
|
|
687
|
-
lock = "quality";
|
|
688
|
-
} else if (interval === 6 || interval === 7 || interval === 8) {
|
|
689
|
-
role = "fifth";
|
|
690
|
-
lock = "voicing";
|
|
691
|
-
} else if (interval === 9 && tones.has(3) && tones.has(6) && !hasHigherSeventh) {
|
|
692
|
-
role = "seventh";
|
|
693
|
-
lock = "voicing";
|
|
694
|
-
} else if (interval === 9 && tones.has(9) && !hasHigherSeventh) {
|
|
695
|
-
role = "sixth";
|
|
696
|
-
lock = "voicing";
|
|
697
|
-
} else if (interval === 10 || interval === 11) {
|
|
698
|
-
role = "seventh";
|
|
699
|
-
lock = "voicing";
|
|
684
|
+
} else if (chordRole !== null) {
|
|
685
|
+
role = chordRole;
|
|
686
|
+
lock = chordRole === "root" ? "identity" : chordRole === "third" ? "quality" : "voicing";
|
|
700
687
|
} else {
|
|
701
688
|
role = "tension";
|
|
702
689
|
lock = "voicing";
|
|
@@ -704,6 +691,33 @@ function roleOf(pitch, chord, chordId = 0) {
|
|
|
704
691
|
return { role, lock, belongsToChordId: chordId };
|
|
705
692
|
}
|
|
706
693
|
|
|
694
|
+
// src/core/validation/index.ts
|
|
695
|
+
var DEFAULT_GENERATION_BUDGET = 1e6;
|
|
696
|
+
function assertFiniteNumber(value, name) {
|
|
697
|
+
if (!Number.isFinite(value)) {
|
|
698
|
+
throw new RangeError(`${name} must be finite; received ${value}`);
|
|
699
|
+
}
|
|
700
|
+
return value;
|
|
701
|
+
}
|
|
702
|
+
function assertInteger(value, name, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) {
|
|
703
|
+
assertFiniteNumber(value, name);
|
|
704
|
+
if (!Number.isSafeInteger(value) || value < min || value > max) {
|
|
705
|
+
throw new RangeError(`${name} must be an integer in [${min}, ${max}]; received ${value}`);
|
|
706
|
+
}
|
|
707
|
+
return value;
|
|
708
|
+
}
|
|
709
|
+
function assertPositiveInt(value, name, max = DEFAULT_GENERATION_BUDGET) {
|
|
710
|
+
return assertInteger(value, name, 1, max);
|
|
711
|
+
}
|
|
712
|
+
function assertGenerationBudget(estimated, name, limit = DEFAULT_GENERATION_BUDGET) {
|
|
713
|
+
assertFiniteNumber(estimated, name);
|
|
714
|
+
assertPositiveInt(limit, `${name} limit`, Number.MAX_SAFE_INTEGER);
|
|
715
|
+
if (estimated < 0 || estimated > limit) {
|
|
716
|
+
throw new RangeError(`${name} exceeds the generation budget ${limit}; received ${estimated}`);
|
|
717
|
+
}
|
|
718
|
+
return estimated;
|
|
719
|
+
}
|
|
720
|
+
|
|
707
721
|
// src/theory/safety/index.ts
|
|
708
722
|
var NoteSafety = /* @__PURE__ */ ((NoteSafety2) => {
|
|
709
723
|
NoteSafety2[NoteSafety2["Safe"] = 0] = "Safe";
|
|
@@ -732,6 +746,31 @@ var ReasonFlag = /* @__PURE__ */ ((ReasonFlag2) => {
|
|
|
732
746
|
})(ReasonFlag || {});
|
|
733
747
|
var SUGGESTION_WINDOW = 12;
|
|
734
748
|
var MAX_SUGGESTIONS = 3;
|
|
749
|
+
var REASON_SEVERITY = [
|
|
750
|
+
[2 /* Tension */, 1 /* Warning */, 1 /* Warning */],
|
|
751
|
+
[4 /* AvoidNote */, 1 /* Warning */, 2 /* Dissonant */],
|
|
752
|
+
[8 /* ScaleTone */, 1 /* Warning */, 1 /* Warning */],
|
|
753
|
+
[16 /* NonScale */, 2 /* Dissonant */, 2 /* Dissonant */],
|
|
754
|
+
[32 /* OutOfRange */, 1 /* Warning */, 1 /* Warning */],
|
|
755
|
+
[64 /* Tritone */, 2 /* Dissonant */, 2 /* Dissonant */],
|
|
756
|
+
[128 /* LargeLeap */, 1 /* Warning */, 2 /* Dissonant */],
|
|
757
|
+
[1024 /* VerticalDissonance */, 2 /* Dissonant */, 2 /* Dissonant */],
|
|
758
|
+
[2048 /* ParallelPerfect */, 1 /* Warning */, 2 /* Dissonant */],
|
|
759
|
+
[4096 /* HiddenParallel */, 1 /* Warning */, 2 /* Dissonant */],
|
|
760
|
+
[8192 /* VoiceCrossing */, 1 /* Warning */, 2 /* Dissonant */],
|
|
761
|
+
[16384 /* Suspension */, 2 /* Dissonant */, 2 /* Dissonant */],
|
|
762
|
+
[32768 /* NeedsResolution */, 1 /* Warning */, 2 /* Dissonant */]
|
|
763
|
+
];
|
|
764
|
+
function minimumSafetyForReasons(reasons, profile) {
|
|
765
|
+
let minimum = 0 /* Safe */;
|
|
766
|
+
const severityIndex = profile === "strict" ? 2 : 1;
|
|
767
|
+
for (const entry of REASON_SEVERITY) {
|
|
768
|
+
if (reasons & entry[0]) {
|
|
769
|
+
minimum = Math.max(minimum, entry[severityIndex]);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return minimum;
|
|
773
|
+
}
|
|
735
774
|
function pitchClass5(pitch) {
|
|
736
775
|
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
737
776
|
}
|
|
@@ -756,8 +795,27 @@ function stepResolution(pitch, chord) {
|
|
|
756
795
|
return void 0;
|
|
757
796
|
}
|
|
758
797
|
function evaluateSafety(q) {
|
|
798
|
+
assertSafetyContext(q);
|
|
799
|
+
assertFiniteNumber(q.candidatePitch, "candidatePitch");
|
|
759
800
|
return evaluateInternal(q, true);
|
|
760
801
|
}
|
|
802
|
+
function assertSafetyContext(q) {
|
|
803
|
+
if (q.prevPitch !== void 0) assertFiniteNumber(q.prevPitch, "prevPitch");
|
|
804
|
+
if (q.vocalLow !== void 0) assertFiniteNumber(q.vocalLow, "vocalLow");
|
|
805
|
+
if (q.vocalHigh !== void 0) assertFiniteNumber(q.vocalHigh, "vocalHigh");
|
|
806
|
+
if (q.vocalLow !== void 0 && q.vocalHigh !== void 0 && q.vocalLow > q.vocalHigh) {
|
|
807
|
+
throw new RangeError("vocalLow must not exceed vocalHigh");
|
|
808
|
+
}
|
|
809
|
+
assertGenerationBudget(q.otherVoices.length, "other voices");
|
|
810
|
+
for (let index = 0; index < q.otherVoices.length; index += 1) {
|
|
811
|
+
const voice = q.otherVoices[index];
|
|
812
|
+
if (!voice) continue;
|
|
813
|
+
assertFiniteNumber(voice.pitch, `otherVoices[${index}].pitch`);
|
|
814
|
+
if (voice.prevPitch !== void 0) {
|
|
815
|
+
assertFiniteNumber(voice.prevPitch, `otherVoices[${index}].prevPitch`);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
761
819
|
function evaluateInternal(q, collectSuggestions) {
|
|
762
820
|
let reasons = 0;
|
|
763
821
|
let level = 0 /* Safe */;
|
|
@@ -818,9 +876,11 @@ function evaluateInternal(q, collectSuggestions) {
|
|
|
818
876
|
const pc = Math.abs(pitch - q.prevPitch) % 12;
|
|
819
877
|
if (pc === 6) {
|
|
820
878
|
reasons |= 64 /* Tritone */;
|
|
879
|
+
raise(2 /* Dissonant */);
|
|
821
880
|
}
|
|
822
881
|
if (isForbiddenMelodicLeap(q.prevPitch, pitch)) {
|
|
823
882
|
reasons |= 128 /* LargeLeap */;
|
|
883
|
+
raise(parallelSeverity);
|
|
824
884
|
}
|
|
825
885
|
if (pc === 1) {
|
|
826
886
|
reasons |= 256 /* MinorSecond */;
|
|
@@ -865,6 +925,7 @@ function evaluateInternal(q, collectSuggestions) {
|
|
|
865
925
|
}
|
|
866
926
|
}
|
|
867
927
|
}
|
|
928
|
+
raise(minimumSafetyForReasons(reasons, q.profile));
|
|
868
929
|
const result = { safety: level, reasons, rationale: describe(reasons, q) };
|
|
869
930
|
if (resolveTo !== void 0) {
|
|
870
931
|
result.resolveTo = resolveTo;
|
|
@@ -928,11 +989,15 @@ function describe(reasons, q) {
|
|
|
928
989
|
return q.chord ? "Placeable" : "No chord context";
|
|
929
990
|
}
|
|
930
991
|
function enumerateSafePitches(q, pitchLow, pitchHigh) {
|
|
992
|
+
assertSafetyContext(q);
|
|
931
993
|
const chordTones = [];
|
|
932
994
|
const others = [];
|
|
933
|
-
|
|
934
|
-
|
|
995
|
+
assertInteger(pitchLow, "pitchLow");
|
|
996
|
+
assertInteger(pitchHigh, "pitchHigh");
|
|
997
|
+
if (pitchLow > pitchHigh) {
|
|
998
|
+
throw new RangeError(`pitchLow must not exceed pitchHigh; received ${pitchLow} > ${pitchHigh}`);
|
|
935
999
|
}
|
|
1000
|
+
assertGenerationBudget(pitchHigh - pitchLow + 1, "safe pitch candidates");
|
|
936
1001
|
for (let pitch = pitchHigh; pitch >= pitchLow; pitch -= 1) {
|
|
937
1002
|
const result = evaluateInternal({ ...q, candidatePitch: pitch }, false);
|
|
938
1003
|
if (result.safety === 2 /* Dissonant */) {
|
|
@@ -1101,10 +1166,58 @@ function spellScale(tonic, key) {
|
|
|
1101
1166
|
function spellPitchClasses(pcs, tonic, key) {
|
|
1102
1167
|
return pcs.map((pc) => spellPitchClass(pc, tonic, key));
|
|
1103
1168
|
}
|
|
1169
|
+
function chordLetterOffset(interval, chord) {
|
|
1170
|
+
switch (interval) {
|
|
1171
|
+
case 0:
|
|
1172
|
+
case 12:
|
|
1173
|
+
return 0;
|
|
1174
|
+
case 1:
|
|
1175
|
+
case 2:
|
|
1176
|
+
case 13:
|
|
1177
|
+
case 14:
|
|
1178
|
+
return 1;
|
|
1179
|
+
case 3:
|
|
1180
|
+
case 4:
|
|
1181
|
+
case 15:
|
|
1182
|
+
case 16:
|
|
1183
|
+
return 2;
|
|
1184
|
+
case 5:
|
|
1185
|
+
case 17:
|
|
1186
|
+
case 18:
|
|
1187
|
+
return 3;
|
|
1188
|
+
case 6:
|
|
1189
|
+
case 7:
|
|
1190
|
+
case 8:
|
|
1191
|
+
case 19:
|
|
1192
|
+
return 4;
|
|
1193
|
+
case 9:
|
|
1194
|
+
return chord.quality === "dim7" ? 6 : 5;
|
|
1195
|
+
case 20:
|
|
1196
|
+
case 21:
|
|
1197
|
+
return 5;
|
|
1198
|
+
case 10:
|
|
1199
|
+
case 11:
|
|
1200
|
+
case 22:
|
|
1201
|
+
case 23:
|
|
1202
|
+
return 6;
|
|
1203
|
+
default: {
|
|
1204
|
+
const octaves = Math.floor(Math.max(0, interval) / 12);
|
|
1205
|
+
const simple = mod122(interval);
|
|
1206
|
+
const simpleOffset = [0, 1, 1, 2, 2, 3, 4, 4, 4, 5, 6, 6][simple] ?? 0;
|
|
1207
|
+
return octaves * 7 + simpleOffset;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
function spellChordFromRoot(chord, root) {
|
|
1212
|
+
return chord.intervals.map((interval) => {
|
|
1213
|
+
const letter = mod72(root.letter + chordLetterOffset(interval, chord));
|
|
1214
|
+
const pc = mod122(chord.rootPc + interval);
|
|
1215
|
+
return { letter, alter: alterFor(letter, pc) };
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1104
1218
|
function spellChord(chord, tonic, key) {
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
);
|
|
1219
|
+
const root = spellPitchClass(chord.rootPc, tonic, key);
|
|
1220
|
+
return spellChordFromRoot(chord, root);
|
|
1108
1221
|
}
|
|
1109
1222
|
function noteNames(notes) {
|
|
1110
1223
|
return notes.map(formatNote);
|
|
@@ -1397,12 +1510,23 @@ function resolveRanges(opts) {
|
|
|
1397
1510
|
if (opts.ranges.length === 0) {
|
|
1398
1511
|
throw new Error("ranges must contain at least one voice range");
|
|
1399
1512
|
}
|
|
1400
|
-
|
|
1513
|
+
assertGenerationBudget(opts.ranges.length, "voice ranges", 128);
|
|
1514
|
+
return opts.ranges.map((range, index) => {
|
|
1515
|
+
assertFiniteNumber(range.min, `ranges[${index}].min`);
|
|
1516
|
+
assertFiniteNumber(range.max, `ranges[${index}].max`);
|
|
1517
|
+
if (range.min > range.max) {
|
|
1518
|
+
throw new RangeError(`ranges[${index}].min must not exceed max`);
|
|
1519
|
+
}
|
|
1520
|
+
assertGenerationBudget(
|
|
1521
|
+
Math.floor(range.max) - Math.ceil(range.min) + 1,
|
|
1522
|
+
`ranges[${index}] span`,
|
|
1523
|
+
4096
|
|
1524
|
+
);
|
|
1525
|
+
return { ...range };
|
|
1526
|
+
});
|
|
1401
1527
|
}
|
|
1402
1528
|
const voices = opts?.voices ?? 4;
|
|
1403
|
-
|
|
1404
|
-
throw new Error("voices must be at least 1");
|
|
1405
|
-
}
|
|
1529
|
+
assertPositiveInt(voices, "voices", 128);
|
|
1406
1530
|
if (voices === 4) {
|
|
1407
1531
|
return SATB_RANGES.map((range) => ({ ...range }));
|
|
1408
1532
|
}
|
|
@@ -1421,6 +1545,8 @@ function resolveRanges(opts) {
|
|
|
1421
1545
|
function voiceChord(chord, opts) {
|
|
1422
1546
|
const ranges = resolveRanges(opts);
|
|
1423
1547
|
const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;
|
|
1548
|
+
assertFiniteNumber(maxSpacing, "maxSpacing");
|
|
1549
|
+
if (maxSpacing < 0) throw new RangeError("maxSpacing must be non-negative");
|
|
1424
1550
|
const candidates = enumerateVoicings(chord, ranges, maxSpacing);
|
|
1425
1551
|
let best;
|
|
1426
1552
|
let bestScore = Number.POSITIVE_INFINITY;
|
|
@@ -1445,8 +1571,11 @@ function voiceChord(chord, opts) {
|
|
|
1445
1571
|
return best;
|
|
1446
1572
|
}
|
|
1447
1573
|
function voiceProgression(chords, opts) {
|
|
1574
|
+
assertGenerationBudget(chords.length * 4e3, "voiced progression search");
|
|
1448
1575
|
const ranges = resolveRanges(opts);
|
|
1449
1576
|
const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;
|
|
1577
|
+
assertFiniteNumber(maxSpacing, "maxSpacing");
|
|
1578
|
+
if (maxSpacing < 0) throw new RangeError("maxSpacing must be non-negative");
|
|
1450
1579
|
const result = [];
|
|
1451
1580
|
let prev;
|
|
1452
1581
|
for (const chord of chords) {
|
|
@@ -1552,7 +1681,12 @@ function nearestPc(target, pcs) {
|
|
|
1552
1681
|
}
|
|
1553
1682
|
function voiceChordStyled(chord, opts) {
|
|
1554
1683
|
const style = opts?.style ?? "close";
|
|
1555
|
-
const
|
|
1684
|
+
const octave = opts?.octave ?? DEFAULT_STYLE_OCTAVE;
|
|
1685
|
+
assertInteger(octave, "styled voicing octave", -10, 10);
|
|
1686
|
+
if (opts?.topNote !== void 0) {
|
|
1687
|
+
assertFiniteNumber(opts.topNote, "styled voicing topNote");
|
|
1688
|
+
}
|
|
1689
|
+
const base = 12 * (octave + 1);
|
|
1556
1690
|
const rootPc = pitchClass7(chord.rootPc);
|
|
1557
1691
|
const bassPc = pitchClass7(chord.bassPc ?? chord.rootPc);
|
|
1558
1692
|
const omitRoot = style === "rootless" || opts?.rootless === true;
|
|
@@ -1568,11 +1702,11 @@ function voiceChordStyled(chord, opts) {
|
|
|
1568
1702
|
const guide = roles.includes("seventh") ? "seventh" : roles.includes("sixth") ? "sixth" : "fifth";
|
|
1569
1703
|
order = order.filter((_pc, i) => {
|
|
1570
1704
|
const role = roles[i];
|
|
1571
|
-
return role === "root" || role === "third" || role === guide;
|
|
1705
|
+
return _pc === bassPc || role === "root" || role === "third" || role === guide;
|
|
1572
1706
|
});
|
|
1573
1707
|
}
|
|
1574
1708
|
if (omitRoot) {
|
|
1575
|
-
order = order.filter((pc) => pc !== rootPc);
|
|
1709
|
+
order = order.filter((pc) => pc !== rootPc || chord.bassPc !== void 0 && pc === bassPc);
|
|
1576
1710
|
}
|
|
1577
1711
|
if (order.length === 0) {
|
|
1578
1712
|
return [];
|
|
@@ -1601,6 +1735,18 @@ function voiceChordStyled(chord, opts) {
|
|
|
1601
1735
|
} else if (style === "drop3" && stack.length >= 3) {
|
|
1602
1736
|
stack[stack.length - 3] = (stack[stack.length - 3] ?? 0) - 12;
|
|
1603
1737
|
}
|
|
1738
|
+
if (chord.bassPc !== void 0) {
|
|
1739
|
+
const bassIndexInStack = stack.findIndex((pitch) => pitchClass7(pitch) === bassPc);
|
|
1740
|
+
if (bassIndexInStack >= 0) {
|
|
1741
|
+
const otherLowest = Math.min(
|
|
1742
|
+
...stack.filter((_pitch, index) => index !== bassIndexInStack),
|
|
1743
|
+
Number.POSITIVE_INFINITY
|
|
1744
|
+
);
|
|
1745
|
+
while ((stack[bassIndexInStack] ?? 0) >= otherLowest) {
|
|
1746
|
+
stack[bassIndexInStack] = (stack[bassIndexInStack] ?? 0) - 12;
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1604
1750
|
return stack.sort((a, b) => a - b);
|
|
1605
1751
|
}
|
|
1606
1752
|
// Annotate the CommonJS export names for ESM import in node:
|