@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.
Files changed (126) hide show
  1. package/README.md +4 -3
  2. package/dist/analyze/index.cjs +315 -42
  3. package/dist/analyze/index.cjs.map +1 -1
  4. package/dist/analyze/index.d.cts +11 -99
  5. package/dist/analyze/index.d.ts +11 -99
  6. package/dist/analyze/index.js +6 -6
  7. package/dist/chunk-2ZWMSJH6.js +30 -0
  8. package/dist/chunk-2ZWMSJH6.js.map +1 -0
  9. package/dist/{chunk-R3S2LUBH.js → chunk-4MHA432G.js} +316 -184
  10. package/dist/chunk-4MHA432G.js.map +1 -0
  11. package/dist/chunk-62QZKCJQ.js +49 -0
  12. package/dist/chunk-62QZKCJQ.js.map +1 -0
  13. package/dist/chunk-AKLYPWIN.js +83 -0
  14. package/dist/chunk-AKLYPWIN.js.map +1 -0
  15. package/dist/{chunk-XTC3X6MS.js → chunk-C5XYKUVG.js} +100 -12
  16. package/dist/chunk-C5XYKUVG.js.map +1 -0
  17. package/dist/{chunk-LN7ZZMD2.js → chunk-CHMXELDS.js} +33 -14
  18. package/dist/chunk-CHMXELDS.js.map +1 -0
  19. package/dist/{chunk-A4532ZFR.js → chunk-EH6EM37U.js} +60 -4
  20. package/dist/chunk-EH6EM37U.js.map +1 -0
  21. package/dist/chunk-FBQK3ZQN.js +131 -0
  22. package/dist/chunk-FBQK3ZQN.js.map +1 -0
  23. package/dist/{chunk-5HTNMQW4.js → chunk-GOAW4VYL.js} +87 -21
  24. package/dist/chunk-GOAW4VYL.js.map +1 -0
  25. package/dist/{chunk-MSWFZF7J.js → chunk-I5R7ZBJS.js} +90 -6
  26. package/dist/chunk-I5R7ZBJS.js.map +1 -0
  27. package/dist/{chunk-GQKN5KDT.js → chunk-IT2O4XFE.js} +4 -9
  28. package/dist/chunk-IT2O4XFE.js.map +1 -0
  29. package/dist/{chunk-7Y7WF64D.js → chunk-MIWO425M.js} +6 -4
  30. package/dist/chunk-MIWO425M.js.map +1 -0
  31. package/dist/chunk-N6IHQUOX.js +1 -0
  32. package/dist/chunk-N6IHQUOX.js.map +1 -0
  33. package/dist/{chunk-COKTX2G5.js → chunk-SGU7SPLK.js} +29 -12
  34. package/dist/chunk-SGU7SPLK.js.map +1 -0
  35. package/dist/core/index.cjs +230 -14
  36. package/dist/core/index.cjs.map +1 -1
  37. package/dist/core/index.d.cts +56 -4
  38. package/dist/core/index.d.ts +56 -4
  39. package/dist/core/index.js +24 -4
  40. package/dist/{function-2bJW1Zp_.d.ts → function-DVH4Eusr.d.ts} +1 -1
  41. package/dist/{function-r2sh-jOO.d.cts → function-DYt4iKsn.d.cts} +1 -1
  42. package/dist/generate/index.cjs +830 -494
  43. package/dist/generate/index.cjs.map +1 -1
  44. package/dist/generate/index.d.cts +11 -5
  45. package/dist/generate/index.d.ts +11 -5
  46. package/dist/generate/index.js +9 -9
  47. package/dist/{index-BOBXBNwP.d.cts → index-BNlGP86A.d.cts} +1 -1
  48. package/dist/{index-CP3wAfEH.d.ts → index-Bg5dYMmZ.d.ts} +1 -1
  49. package/dist/{index-CM5W4rcm.d.ts → index-Bt4tOUD9.d.ts} +1 -1
  50. package/dist/index-CnZCo29u.d.cts +108 -0
  51. package/dist/{index-D_ZTUNKp.d.cts → index-CpOsP1y9.d.cts} +2 -2
  52. package/dist/{index-DZ1M21do.d.cts → index-CxqBQtH1.d.cts} +2 -1
  53. package/dist/{index-BdeQ1r_p.d.cts → index-Dikst44v.d.cts} +1 -1
  54. package/dist/{index-BMPxwjwV.d.ts → index-DxvARIP2.d.ts} +2 -2
  55. package/dist/{index-BreoJRw0.d.ts → index-Jc0Ryi79.d.ts} +2 -1
  56. package/dist/index-JnF4v9bx.d.ts +108 -0
  57. package/dist/index.cjs +818 -283
  58. package/dist/index.cjs.map +1 -1
  59. package/dist/index.d.cts +9 -8
  60. package/dist/index.d.ts +9 -8
  61. package/dist/index.js +34 -13
  62. package/dist/model/index.cjs +170 -31
  63. package/dist/model/index.cjs.map +1 -1
  64. package/dist/model/index.d.cts +6 -5
  65. package/dist/model/index.d.ts +6 -5
  66. package/dist/model/index.js +6 -6
  67. package/dist/{styled-D7BAx3S3.d.ts → styled-DZ7GjShb.d.ts} +1 -1
  68. package/dist/{styled-BjXtOFNk.d.cts → styled-ZTbdaVFM.d.cts} +1 -1
  69. package/dist/theory/index.cjs +180 -34
  70. package/dist/theory/index.cjs.map +1 -1
  71. package/dist/theory/index.d.cts +6 -6
  72. package/dist/theory/index.d.ts +6 -6
  73. package/dist/theory/index.js +6 -5
  74. package/package.json +50 -19
  75. package/src/analyze/arrange/internal.ts +3 -1
  76. package/src/analyze/arrange/tension.ts +15 -3
  77. package/src/analyze/arrange/tracks.ts +36 -3
  78. package/src/analyze/detect/index.ts +46 -9
  79. package/src/analyze/functional/roman.ts +14 -5
  80. package/src/analyze/index.ts +1 -1
  81. package/src/analyze/timeline/index.ts +43 -7
  82. package/src/analyze/voice/index.ts +11 -2
  83. package/src/core/event-index/index.ts +104 -0
  84. package/src/core/index.ts +14 -0
  85. package/src/core/meter/index.ts +24 -4
  86. package/src/core/random/index.ts +26 -3
  87. package/src/core/tuning/index.ts +36 -6
  88. package/src/core/validation/index.ts +119 -0
  89. package/src/generate/bass/index.ts +23 -0
  90. package/src/generate/countermelody/index.ts +167 -76
  91. package/src/generate/drums/euclid.ts +10 -3
  92. package/src/generate/drums/index.ts +56 -24
  93. package/src/generate/drums/percussion.ts +1 -1
  94. package/src/generate/groove/index.ts +25 -0
  95. package/src/generate/harmonize/index.ts +55 -44
  96. package/src/generate/index.ts +0 -1
  97. package/src/generate/motif/index.ts +45 -21
  98. package/src/generate/progression/index.ts +8 -16
  99. package/src/generate/reharmony/index.ts +1 -8
  100. package/src/generate/rhythm/index.ts +11 -1
  101. package/src/index.ts +13 -247
  102. package/src/model/chord.ts +29 -10
  103. package/src/theory/chord/index.ts +4 -2
  104. package/src/theory/harmony/index.ts +6 -21
  105. package/src/theory/index.ts +6 -1
  106. package/src/theory/safety/index.ts +71 -2
  107. package/src/theory/spelling/index.ts +70 -3
  108. package/src/theory/voicing/satb.ts +25 -4
  109. package/src/theory/voicing/styled.ts +26 -3
  110. package/dist/chunk-5HTNMQW4.js.map +0 -1
  111. package/dist/chunk-7Y7WF64D.js.map +0 -1
  112. package/dist/chunk-A4532ZFR.js.map +0 -1
  113. package/dist/chunk-BHUT2S5G.js +0 -22
  114. package/dist/chunk-BHUT2S5G.js.map +0 -1
  115. package/dist/chunk-CCBLVAPK.js +0 -42
  116. package/dist/chunk-CCBLVAPK.js.map +0 -1
  117. package/dist/chunk-COKTX2G5.js.map +0 -1
  118. package/dist/chunk-GQKN5KDT.js.map +0 -1
  119. package/dist/chunk-LN7ZZMD2.js.map +0 -1
  120. package/dist/chunk-MSWFZF7J.js.map +0 -1
  121. package/dist/chunk-R3S2LUBH.js.map +0 -1
  122. package/dist/chunk-SDM3VJJ5.js +0 -41
  123. package/dist/chunk-SDM3VJJ5.js.map +0 -1
  124. package/dist/chunk-XTC3X6MS.js.map +0 -1
  125. package/dist/chunk-ZXW5YIWK.js +0 -55
  126. package/dist/chunk-ZXW5YIWK.js.map +0 -1
@@ -1,6 +1,7 @@
1
- import { H as HarmonicFunction, a as ChordAnalysis, C as Cadence, B as BorrowedSource } from '../function-r2sh-jOO.cjs';
2
- import { a as ChordQuality, C as Chord$1 } from '../index-BOBXBNwP.cjs';
3
- import { d as VoicingOptions, c as StyledVoicingOptions, C as ChordScaleMatch } from '../styled-BjXtOFNk.cjs';
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. */
@@ -1,6 +1,7 @@
1
- import { H as HarmonicFunction, a as ChordAnalysis, C as Cadence, B as BorrowedSource } from '../function-2bJW1Zp_.js';
2
- import { a as ChordQuality, C as Chord$1 } from '../index-CM5W4rcm.js';
3
- import { d as VoicingOptions, c as StyledVoicingOptions, C as ChordScaleMatch } from '../styled-D7BAx3S3.js';
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. */
@@ -4,13 +4,13 @@ import {
4
4
  Key,
5
5
  Note,
6
6
  Progression
7
- } from "../chunk-COKTX2G5.js";
8
- import "../chunk-GQKN5KDT.js";
9
- import "../chunk-LN7ZZMD2.js";
10
- import "../chunk-XTC3X6MS.js";
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-7Y7WF64D.js";
13
- import "../chunk-CCBLVAPK.js";
12
+ import "../chunk-MIWO425M.js";
13
+ import "../chunk-FBQK3ZQN.js";
14
14
  export {
15
15
  Chord,
16
16
  Interval,
@@ -1,4 +1,4 @@
1
- import { C as Chord } from './index-CM5W4rcm.js';
1
+ import { C as Chord } from './index-Bt4tOUD9.js';
2
2
 
3
3
  /**
4
4
  * Test whether every chord pitch class is contained in a scale.
@@ -1,4 +1,4 @@
1
- import { C as Chord } from './index-BOBXBNwP.cjs';
1
+ import { C as Chord } from './index-BNlGP86A.cjs';
2
2
 
3
3
  /**
4
4
  * Test whether every chord pitch class is contained in a scale.
@@ -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 === 0) {
356
- return { rootPc: pitchClass2(key.rootPc), quality: "maj", intervals: [0, 4, 7] };
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 (interval === 0) {
680
- role = "root";
681
- lock = "identity";
682
- } else if (isSuspendedTone) {
681
+ if (isSuspendedTone) {
683
682
  role = "third";
684
683
  lock = "quality";
685
- } else if (interval === 3 || interval === 4) {
686
- role = "third";
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
- if (!Number.isFinite(pitchLow) || !Number.isFinite(pitchHigh)) {
934
- return [];
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
- return chord.intervals.map(
1106
- (interval) => spellPitchClass(mod122(chord.rootPc + interval), tonic, key)
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
- return opts.ranges.map((range) => ({ ...range }));
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
- if (voices < 1) {
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 base = 12 * ((opts?.octave ?? DEFAULT_STYLE_OCTAVE) + 1);
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: