@libraz/libcantus 1.1.0 → 1.2.0

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 (93) hide show
  1. package/dist/analyze/index.cjs +7 -7
  2. package/dist/analyze/index.d.cts +8 -8
  3. package/dist/analyze/index.d.ts +8 -8
  4. package/dist/analyze/index.js +6 -6
  5. package/dist/{chunk-TT6PKMPR.cjs → chunk-35NBV2FT.cjs} +16 -16
  6. package/dist/{chunk-TT6PKMPR.cjs.map → chunk-35NBV2FT.cjs.map} +1 -1
  7. package/dist/{chunk-YPB3A3EY.cjs → chunk-3R3QVKY5.cjs} +314 -285
  8. package/dist/chunk-3R3QVKY5.cjs.map +1 -0
  9. package/dist/{chunk-RVS4G4MR.cjs → chunk-76ZBTRG2.cjs} +198 -198
  10. package/dist/{chunk-RVS4G4MR.cjs.map → chunk-76ZBTRG2.cjs.map} +1 -1
  11. package/dist/{chunk-X6RJAHII.js → chunk-7KG3RTYS.js} +2 -2
  12. package/dist/{chunk-PQL7E5F6.js → chunk-AGOVW7U6.js} +1 -1
  13. package/dist/{chunk-PQL7E5F6.js.map → chunk-AGOVW7U6.js.map} +1 -1
  14. package/dist/{chunk-J772DL3B.cjs → chunk-CGBADIO5.cjs} +53 -53
  15. package/dist/{chunk-J772DL3B.cjs.map → chunk-CGBADIO5.cjs.map} +1 -1
  16. package/dist/{chunk-GDKE6RQW.cjs → chunk-DSMJRXCZ.cjs} +402 -401
  17. package/dist/chunk-DSMJRXCZ.cjs.map +1 -0
  18. package/dist/{chunk-DKICR7AN.js → chunk-FBH75DAY.js} +13 -12
  19. package/dist/{chunk-DKICR7AN.js.map → chunk-FBH75DAY.js.map} +1 -1
  20. package/dist/{chunk-VLE5R7PF.js → chunk-H2KJHBW4.js} +4 -4
  21. package/dist/{chunk-XVMJO3QF.cjs → chunk-HPVSH7SF.cjs} +237 -215
  22. package/dist/chunk-HPVSH7SF.cjs.map +1 -0
  23. package/dist/{chunk-QCXYKSZS.cjs → chunk-HWHGOVO6.cjs} +19 -19
  24. package/dist/{chunk-QCXYKSZS.cjs.map → chunk-HWHGOVO6.cjs.map} +1 -1
  25. package/dist/{chunk-HSY2VQXZ.js → chunk-HYJJ2V2I.js} +32 -10
  26. package/dist/chunk-HYJJ2V2I.js.map +1 -0
  27. package/dist/{chunk-5KW7DNCJ.cjs → chunk-J7FHVZII.cjs} +445 -445
  28. package/dist/{chunk-5KW7DNCJ.cjs.map → chunk-J7FHVZII.cjs.map} +1 -1
  29. package/dist/{chunk-AWEMD747.js → chunk-M5RYMAFI.js} +3 -3
  30. package/dist/{chunk-H66ARFU5.js → chunk-NB25P3EH.js} +3 -3
  31. package/dist/{chunk-57WB2EDF.js → chunk-NFJ4P6ZZ.js} +3 -3
  32. package/dist/{chunk-5JJ6EHEZ.cjs → chunk-NM7UAMC3.cjs} +62 -62
  33. package/dist/{chunk-5JJ6EHEZ.cjs.map → chunk-NM7UAMC3.cjs.map} +1 -1
  34. package/dist/{chunk-R7B5N2JK.cjs → chunk-PM4XJE2Y.cjs} +47 -47
  35. package/dist/{chunk-R7B5N2JK.cjs.map → chunk-PM4XJE2Y.cjs.map} +1 -1
  36. package/dist/{chunk-7GOKQJTQ.js → chunk-PUKJP2Z5.js} +2 -2
  37. package/dist/{chunk-R5VUXQMP.js → chunk-Q6LFSAKV.js} +7 -7
  38. package/dist/{chunk-FPBZFNCD.js → chunk-RFIBF2RR.js} +47 -18
  39. package/dist/{chunk-FPBZFNCD.js.map → chunk-RFIBF2RR.js.map} +1 -1
  40. package/dist/{chunk-XKSO2V5I.cjs → chunk-S5BKZZGM.cjs} +1 -1
  41. package/dist/{chunk-XKSO2V5I.cjs.map → chunk-S5BKZZGM.cjs.map} +1 -1
  42. package/dist/{chunk-Q6R3QAZD.cjs → chunk-SGLEQVJH.cjs} +332 -332
  43. package/dist/{chunk-Q6R3QAZD.cjs.map → chunk-SGLEQVJH.cjs.map} +1 -1
  44. package/dist/{chunk-HOHFYGKW.js → chunk-UMWZ4ZET.js} +2 -2
  45. package/dist/{coerce-wwdELNYS.d.ts → coerce-DP0vYu-x.d.ts} +77 -1
  46. package/dist/{coerce-CJlgULrQ.d.cts → coerce-DRERzdve.d.cts} +77 -1
  47. package/dist/core/index.cjs +5 -5
  48. package/dist/core/index.d.cts +2 -2
  49. package/dist/core/index.d.ts +2 -2
  50. package/dist/core/index.js +4 -4
  51. package/dist/generate/index.cjs +8 -8
  52. package/dist/generate/index.d.cts +8 -8
  53. package/dist/generate/index.d.ts +8 -8
  54. package/dist/generate/index.js +7 -7
  55. package/dist/{index-BaoKVVhE.d.cts → index-B9QLziDe.d.cts} +2 -2
  56. package/dist/{index-B3WCj_Hm.d.cts → index-BBWNZS0f.d.cts} +2 -2
  57. package/dist/{index-CnHQgqNh.d.ts → index-BHIUyjhZ.d.ts} +2 -2
  58. package/dist/{index-DpC-mDlV.d.ts → index-BJgP97N1.d.ts} +4 -4
  59. package/dist/{index-CBCP84rv.d.cts → index-BL5N2Oyz.d.cts} +1 -1
  60. package/dist/{index-D_hzxD8w.d.cts → index-BMweUlm5.d.cts} +3 -0
  61. package/dist/{index-BzLNkmA7.d.cts → index-BNSM-DSk.d.cts} +4 -4
  62. package/dist/{index-BUtIf2go.d.ts → index-BRMbPkHE.d.ts} +25 -5
  63. package/dist/{index-BpWOhkkE.d.ts → index-CJBBiAmU.d.ts} +2 -2
  64. package/dist/{index-Ci3xyows.d.ts → index-CL4b4gUQ.d.ts} +3 -0
  65. package/dist/{index-DTsKaPaC.d.cts → index-DGCNKAUr.d.cts} +25 -5
  66. package/dist/{index-CnIdP4EZ.d.ts → index-Dw2DP7vm.d.ts} +1 -1
  67. package/dist/index.cjs +13 -13
  68. package/dist/index.d.cts +8 -8
  69. package/dist/index.d.ts +8 -8
  70. package/dist/index.js +12 -12
  71. package/dist/{masks-O73Rhj5d.d.ts → masks-Cc7-MzzY.d.ts} +1 -1
  72. package/dist/{masks-C8TWLFIi.d.cts → masks-pADfTOiD.d.cts} +1 -1
  73. package/dist/model/index.cjs +12 -12
  74. package/dist/model/index.d.cts +16 -15
  75. package/dist/model/index.d.ts +16 -15
  76. package/dist/model/index.js +11 -11
  77. package/dist/theory/index.cjs +5 -5
  78. package/dist/theory/index.d.cts +7 -7
  79. package/dist/theory/index.d.ts +7 -7
  80. package/dist/theory/index.js +4 -4
  81. package/package.json +6 -5
  82. package/dist/chunk-GDKE6RQW.cjs.map +0 -1
  83. package/dist/chunk-HSY2VQXZ.js.map +0 -1
  84. package/dist/chunk-XVMJO3QF.cjs.map +0 -1
  85. package/dist/chunk-YPB3A3EY.cjs.map +0 -1
  86. /package/dist/{chunk-X6RJAHII.js.map → chunk-7KG3RTYS.js.map} +0 -0
  87. /package/dist/{chunk-VLE5R7PF.js.map → chunk-H2KJHBW4.js.map} +0 -0
  88. /package/dist/{chunk-AWEMD747.js.map → chunk-M5RYMAFI.js.map} +0 -0
  89. /package/dist/{chunk-H66ARFU5.js.map → chunk-NB25P3EH.js.map} +0 -0
  90. /package/dist/{chunk-57WB2EDF.js.map → chunk-NFJ4P6ZZ.js.map} +0 -0
  91. /package/dist/{chunk-7GOKQJTQ.js.map → chunk-PUKJP2Z5.js.map} +0 -0
  92. /package/dist/{chunk-R5VUXQMP.js.map → chunk-Q6LFSAKV.js.map} +0 -0
  93. /package/dist/{chunk-HOHFYGKW.js.map → chunk-UMWZ4ZET.js.map} +0 -0
@@ -9,7 +9,7 @@
9
9
 
10
10
 
11
11
 
12
- var _chunkYPB3A3EYcjs = require('./chunk-YPB3A3EY.cjs');
12
+ var _chunk3R3QVKY5cjs = require('./chunk-3R3QVKY5.cjs');
13
13
 
14
14
 
15
15
 
@@ -19,17 +19,17 @@ var _chunkYPB3A3EYcjs = require('./chunk-YPB3A3EY.cjs');
19
19
 
20
20
 
21
21
 
22
- var _chunkXKSO2V5Icjs = require('./chunk-XKSO2V5I.cjs');
22
+ var _chunkS5BKZZGMcjs = require('./chunk-S5BKZZGM.cjs');
23
23
 
24
24
  // src/theory/chordscale/index.ts
25
25
  function requireChordScaleMask(name, label = "scale") {
26
- const canonical = _chunkYPB3A3EYcjs.resolveScaleName.call(void 0, name);
27
- if (canonical !== void 0 && !Object.hasOwn(_chunkYPB3A3EYcjs.NAMED_SCALES, canonical)) {
28
- throw new (0, _chunkXKSO2V5Icjs.InvalidInputError)(
26
+ const canonical = _chunk3R3QVKY5cjs.resolveScaleName.call(void 0, name);
27
+ if (canonical !== void 0 && !Object.hasOwn(_chunk3R3QVKY5cjs.NAMED_SCALES, canonical)) {
28
+ throw new (0, _chunkS5BKZZGMcjs.InvalidInputError)(
29
29
  `${label} must name a scale chord-scale theory ranks; ${String(name)} is a world scale`
30
30
  );
31
31
  }
32
- return _chunkYPB3A3EYcjs.requireScaleMask.call(void 0, name, label);
32
+ return _chunk3R3QVKY5cjs.requireScaleMask.call(void 0, name, label);
33
33
  }
34
34
  function popcount12(mask) {
35
35
  let count = 0;
@@ -39,15 +39,15 @@ function popcount12(mask) {
39
39
  return count;
40
40
  }
41
41
  function maskHasPitchClass(mask, scaleRootPc, pc) {
42
- const offset = _chunkXKSO2V5Icjs.pitchClassOf.call(void 0, pc - scaleRootPc);
42
+ const offset = _chunkS5BKZZGMcjs.pitchClassOf.call(void 0, pc - scaleRootPc);
43
43
  return (mask >> offset & 1) === 1;
44
44
  }
45
45
  function scaleMatchesChord(chordPcs, scaleMask, scaleRootPc) {
46
- _chunkYPB3A3EYcjs.assertModeMask.call(void 0, scaleMask, "scaleMask");
47
- _chunkXKSO2V5Icjs.assertPitchClass.call(void 0, scaleRootPc, "scaleRootPc");
48
- const pcs = _chunkXKSO2V5Icjs.assertArray.call(void 0, chordPcs, "chord pitch classes");
46
+ _chunk3R3QVKY5cjs.assertModeMask.call(void 0, scaleMask, "scaleMask");
47
+ _chunkS5BKZZGMcjs.assertPitchClass.call(void 0, scaleRootPc, "scaleRootPc");
48
+ const pcs = _chunkS5BKZZGMcjs.assertArray.call(void 0, chordPcs, "chord pitch classes");
49
49
  for (let index = 0; index < pcs.length; index += 1) {
50
- const pc = _chunkXKSO2V5Icjs.assertPitchClass.call(void 0, _nullishCoalesce(pcs[index], () => ( Number.NaN)), `chord pitch classes[${index}]`);
50
+ const pc = _chunkS5BKZZGMcjs.assertPitchClass.call(void 0, _nullishCoalesce(pcs[index], () => ( Number.NaN)), `chord pitch classes[${index}]`);
51
51
  if (!maskHasPitchClass(scaleMask, scaleRootPc, pc)) {
52
52
  return false;
53
53
  }
@@ -93,16 +93,16 @@ function isAlteredDominant(chord, scaleName) {
93
93
  return chord.quality === "dom7" && scaleName === "altered";
94
94
  }
95
95
  function chordScales(chord) {
96
- const data = _chunkYPB3A3EYcjs.toChordData.call(void 0, chord);
97
- const chordPcs = _chunkYPB3A3EYcjs.chordPitchClasses.call(void 0, data);
98
- const rootPc = _chunkXKSO2V5Icjs.pitchClassOf.call(void 0, data.rootPc);
96
+ const data = _chunk3R3QVKY5cjs.toChordData.call(void 0, chord);
97
+ const chordPcs = _chunk3R3QVKY5cjs.chordPitchClasses.call(void 0, data);
98
+ const rootPc = _chunkS5BKZZGMcjs.pitchClassOf.call(void 0, data.rootPc);
99
99
  const ranked = [];
100
100
  const seenMasks = /* @__PURE__ */ new Set();
101
- for (const name of Object.keys(_chunkYPB3A3EYcjs.NAMED_SCALES)) {
101
+ for (const name of Object.keys(_chunk3R3QVKY5cjs.NAMED_SCALES)) {
102
102
  if (name === "chromatic" || ALIASED_SCALE_NAMES.has(name)) {
103
103
  continue;
104
104
  }
105
- const mask = _chunkYPB3A3EYcjs.namedScaleMask.call(void 0, name);
105
+ const mask = _chunk3R3QVKY5cjs.namedScaleMask.call(void 0, name);
106
106
  if (mask === void 0 || seenMasks.has(mask)) {
107
107
  continue;
108
108
  }
@@ -118,7 +118,7 @@ function chordScales(chord) {
118
118
  }
119
119
  }
120
120
  if (ranked.length === 0) {
121
- if (_chunkYPB3A3EYcjs.NAMED_SCALES.chromatic !== void 0) {
121
+ if (_chunk3R3QVKY5cjs.NAMED_SCALES.chromatic !== void 0) {
122
122
  return [{ name: "chromatic", rootPc }];
123
123
  }
124
124
  return [];
@@ -149,19 +149,19 @@ function chordScales(chord) {
149
149
  return ranked.map((entry) => ({ name: entry.name, rootPc }));
150
150
  }
151
151
  function avoidNotes(chord, scaleName, opts = {}) {
152
- const asked = _chunkXKSO2V5Icjs.assertOptions.call(void 0, opts, "opts");
152
+ const asked = _chunkS5BKZZGMcjs.assertOptions.call(void 0, opts, "opts");
153
153
  const mask = requireChordScaleMask(scaleName);
154
- const data = _chunkYPB3A3EYcjs.toChordData.call(void 0, chord);
155
- const rootPc = _chunkXKSO2V5Icjs.pitchClassOf.call(void 0, data.rootPc);
156
- const chordPcs = _chunkYPB3A3EYcjs.chordPitchClasses.call(void 0, data);
154
+ const data = _chunk3R3QVKY5cjs.toChordData.call(void 0, chord);
155
+ const rootPc = _chunkS5BKZZGMcjs.pitchClassOf.call(void 0, data.rootPc);
156
+ const chordPcs = _chunk3R3QVKY5cjs.chordPitchClasses.call(void 0, data);
157
157
  const alteredDominant = isAlteredDominant(data, scaleName);
158
158
  if (!scaleMatchesChord(chordPcs, mask, rootPc) && !alteredDominant) {
159
159
  return [];
160
160
  }
161
161
  const chordSet = new Set(chordPcs);
162
- const displaced = new Set(_chunkYPB3A3EYcjs.displacedThirds.call(void 0, data));
162
+ const displaced = new Set(_chunk3R3QVKY5cjs.displacedThirds.call(void 0, data));
163
163
  const melodic = asked.use === "melodic";
164
- const semitoneAboveRoot = _chunkXKSO2V5Icjs.pitchClassOf.call(void 0, rootPc + 1);
164
+ const semitoneAboveRoot = _chunkS5BKZZGMcjs.pitchClassOf.call(void 0, rootPc + 1);
165
165
  const avoid = [];
166
166
  for (let pc = 0; pc < 12; pc += 1) {
167
167
  if (!maskHasPitchClass(mask, rootPc, pc) || chordSet.has(pc)) {
@@ -170,7 +170,7 @@ function avoidNotes(chord, scaleName, opts = {}) {
170
170
  if (melodic && pc !== semitoneAboveRoot) {
171
171
  continue;
172
172
  }
173
- if (!alteredDominant && (chordSet.has(_chunkXKSO2V5Icjs.pitchClassOf.call(void 0, pc - 1)) || displaced.has(pc))) {
173
+ if (!alteredDominant && (chordSet.has(_chunkS5BKZZGMcjs.pitchClassOf.call(void 0, pc - 1)) || displaced.has(pc))) {
174
174
  avoid.push(pc);
175
175
  }
176
176
  }
@@ -181,23 +181,23 @@ function resolvesToMinorTonic(chord, chordPcs, target) {
181
181
  if (target === void 0) {
182
182
  return false;
183
183
  }
184
- const rootPc = _chunkXKSO2V5Icjs.pitchClassOf.call(void 0, chord.rootPc);
185
- const sounds = (semitones) => chordPcs.includes(_chunkXKSO2V5Icjs.pitchClassOf.call(void 0, rootPc + semitones));
184
+ const rootPc = _chunkS5BKZZGMcjs.pitchClassOf.call(void 0, chord.rootPc);
185
+ const sounds = (semitones) => chordPcs.includes(_chunkS5BKZZGMcjs.pitchClassOf.call(void 0, rootPc + semitones));
186
186
  if (!sounds(4) || !sounds(10)) {
187
187
  return false;
188
188
  }
189
- const targetRootPc = _chunkXKSO2V5Icjs.pitchClassOf.call(void 0, target.rootPc);
190
- if (targetRootPc !== _chunkXKSO2V5Icjs.pitchClassOf.call(void 0, rootPc + 5)) {
189
+ const targetRootPc = _chunkS5BKZZGMcjs.pitchClassOf.call(void 0, target.rootPc);
190
+ if (targetRootPc !== _chunkS5BKZZGMcjs.pitchClassOf.call(void 0, rootPc + 5)) {
191
191
  return false;
192
192
  }
193
- return _chunkYPB3A3EYcjs.chordPitchClasses.call(void 0, target).includes(_chunkXKSO2V5Icjs.pitchClassOf.call(void 0, targetRootPc + 3));
193
+ return _chunk3R3QVKY5cjs.chordPitchClasses.call(void 0, target).includes(_chunkS5BKZZGMcjs.pitchClassOf.call(void 0, targetRootPc + 3));
194
194
  }
195
195
  function availableTensions(chord, scaleName, opts = {}) {
196
- const asked = _chunkXKSO2V5Icjs.assertOptions.call(void 0, opts, "opts");
196
+ const asked = _chunkS5BKZZGMcjs.assertOptions.call(void 0, opts, "opts");
197
197
  const mask = requireChordScaleMask(scaleName);
198
- const data = _chunkYPB3A3EYcjs.toChordData.call(void 0, chord);
199
- const rootPc = _chunkXKSO2V5Icjs.pitchClassOf.call(void 0, data.rootPc);
200
- const chordPcs = _chunkYPB3A3EYcjs.chordPitchClasses.call(void 0, data);
198
+ const data = _chunk3R3QVKY5cjs.toChordData.call(void 0, chord);
199
+ const rootPc = _chunkS5BKZZGMcjs.pitchClassOf.call(void 0, data.rootPc);
200
+ const chordPcs = _chunk3R3QVKY5cjs.chordPitchClasses.call(void 0, data);
201
201
  const alteredDominant = isAlteredDominant(data, scaleName);
202
202
  if (!scaleMatchesChord(chordPcs, mask, rootPc) && !alteredDominant) {
203
203
  return [];
@@ -207,8 +207,8 @@ function availableTensions(chord, scaleName, opts = {}) {
207
207
  const functional = resolvesToMinorTonic(
208
208
  data,
209
209
  chordPcs,
210
- asked.resolvesTo === void 0 ? void 0 : _chunkYPB3A3EYcjs.toChordData.call(void 0, asked.resolvesTo)
211
- ) ? new Set(MINOR_RESOLUTION_TENSIONS.map((semitones) => _chunkXKSO2V5Icjs.pitchClassOf.call(void 0, rootPc + semitones))) : void 0;
210
+ asked.resolvesTo === void 0 ? void 0 : _chunk3R3QVKY5cjs.toChordData.call(void 0, asked.resolvesTo)
211
+ ) ? new Set(MINOR_RESOLUTION_TENSIONS.map((semitones) => _chunkS5BKZZGMcjs.pitchClassOf.call(void 0, rootPc + semitones))) : void 0;
212
212
  const tensions = [];
213
213
  for (let pc = 0; pc < 12; pc += 1) {
214
214
  if (!maskHasPitchClass(mask, rootPc, pc) || chordSet.has(pc)) {
@@ -223,9 +223,9 @@ function availableTensions(chord, scaleName, opts = {}) {
223
223
  }
224
224
  function chordScaleReport(chord, limit) {
225
225
  if (limit !== void 0) {
226
- _chunkXKSO2V5Icjs.assertPositiveInt.call(void 0, limit, "chord-scale report limit");
226
+ _chunkS5BKZZGMcjs.assertPositiveInt.call(void 0, limit, "chord-scale report limit");
227
227
  }
228
- const data = _chunkYPB3A3EYcjs.toChordData.call(void 0, chord);
228
+ const data = _chunk3R3QVKY5cjs.toChordData.call(void 0, chord);
229
229
  const matches = chordScales(data);
230
230
  const chosen = limit === void 0 ? matches : matches.slice(0, limit);
231
231
  return chosen.map((match) => {
@@ -242,14 +242,14 @@ function chordScaleReport(chord, limit) {
242
242
  }
243
243
  var RANK_PENALTY = 0.01;
244
244
  function scalePitchClassSet(name, rootPc) {
245
- const mask = _chunkYPB3A3EYcjs.namedScaleMask.call(void 0, name);
245
+ const mask = _chunk3R3QVKY5cjs.namedScaleMask.call(void 0, name);
246
246
  const pcs = /* @__PURE__ */ new Set();
247
247
  if (mask === void 0) {
248
248
  return pcs;
249
249
  }
250
250
  for (let n = 0; n < 12; n += 1) {
251
251
  if ((mask >> n & 1) === 1) {
252
- pcs.add(_chunkXKSO2V5Icjs.pitchClassOf.call(void 0, rootPc + n));
252
+ pcs.add(_chunkS5BKZZGMcjs.pitchClassOf.call(void 0, rootPc + n));
253
253
  }
254
254
  }
255
255
  return pcs;
@@ -269,8 +269,8 @@ function symmetricDifferenceSize(a, b) {
269
269
  return count;
270
270
  }
271
271
  function scalesForChanges(chords) {
272
- _chunkXKSO2V5Icjs.assertArray.call(void 0, chords, "chord-scale changes");
273
- _chunkXKSO2V5Icjs.assertGenerationBudget.call(void 0, chords.length, "chord-scale changes");
272
+ _chunkS5BKZZGMcjs.assertArray.call(void 0, chords, "chord-scale changes");
273
+ _chunkS5BKZZGMcjs.assertGenerationBudget.call(void 0, chords.length, "chord-scale changes");
274
274
  if (chords.length === 0) {
275
275
  return [];
276
276
  }
@@ -330,12 +330,12 @@ function scalesForChanges(chords) {
330
330
 
331
331
  // src/theory/harmony/index.ts
332
332
  function roleOf(pitch, chord, chordId = 0) {
333
- _chunkXKSO2V5Icjs.assertFiniteNumber.call(void 0, chordId, "chordId");
334
- const data = _chunkYPB3A3EYcjs.toChordData.call(void 0, chord);
335
- const interval = (_chunkXKSO2V5Icjs.pitchClassOf.call(void 0, pitch) - _chunkXKSO2V5Icjs.pitchClassOf.call(void 0, data.rootPc) + 12) % 12;
336
- const slot = _chunkYPB3A3EYcjs.thirdSlotOf.call(void 0, data);
333
+ _chunkS5BKZZGMcjs.assertFiniteNumber.call(void 0, chordId, "chordId");
334
+ const data = _chunk3R3QVKY5cjs.toChordData.call(void 0, chord);
335
+ const interval = (_chunkS5BKZZGMcjs.pitchClassOf.call(void 0, pitch) - _chunkS5BKZZGMcjs.pitchClassOf.call(void 0, data.rootPc) + 12) % 12;
336
+ const slot = _chunk3R3QVKY5cjs.thirdSlotOf.call(void 0, data);
337
337
  const isSuspendedTone = slot.kind === "suspension" && interval === slot.interval;
338
- const chordRole = _chunkYPB3A3EYcjs.chordToneRole.call(void 0, pitch, data);
338
+ const chordRole = _chunk3R3QVKY5cjs.chordToneRole.call(void 0, pitch, data);
339
339
  let role;
340
340
  let lock;
341
341
  if (isSuspendedTone) {
@@ -360,4 +360,4 @@ function roleOf(pitch, chord, chordId = 0) {
360
360
 
361
361
 
362
362
  exports.scaleMatchesChord = scaleMatchesChord; exports.chordScales = chordScales; exports.avoidNotes = avoidNotes; exports.availableTensions = availableTensions; exports.chordScaleReport = chordScaleReport; exports.scalesForChanges = scalesForChanges; exports.roleOf = roleOf;
363
- //# sourceMappingURL=chunk-R7B5N2JK.cjs.map
363
+ //# sourceMappingURL=chunk-PM4XJE2Y.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/runner/work/libcantus/libcantus/dist/chunk-R7B5N2JK.cjs","../src/theory/chordscale/index.ts","../src/theory/harmony/index.ts"],"names":[],"mappings":"AAAA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,wDAA6B;AAC7B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,wDAA6B;AAC7B;AACA;ACaA,SAAS,qBAAA,CAAsB,IAAA,EAAsB,MAAA,EAAQ,OAAA,EAAiB;AAC5E,EAAA,MAAM,UAAA,EAAY,gDAAA,IAAqB,CAAA;AACvC,EAAA,GAAA,CAAI,UAAA,IAAc,KAAA,EAAA,GAAa,CAAC,MAAA,CAAO,MAAA,CAAO,8BAAA,EAAc,SAAS,CAAA,EAAG;AACtE,IAAA,MAAM,IAAI,wCAAA;AAAA,MACR,CAAA,EAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAGA;AACM,EAAA;AACJ,EAAA;AACE,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAGA;AACE,EAAA;AACA,EAAA;AACF;AAYO;AAOL,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAkBM;AAGA;AASA;AACJ,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAGM;AACC,EAAA;AACL,EAAA;AAAgB;AAAA;AAAA;AAAA;AAKhB,EAAA;AACA,EAAA;AACF;AAGA;AACE,EAAA;AACA,EAAA;AACF;AAGA;AACE,EAAA;AACI,EAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAOA;AACE,EAAA;AACF;AA6CO;AACL,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AAAY,QAAA;AACV,QAAA;AACuB,QAAA;AACvB,QAAA;AAED,MAAA;AACH,IAAA;AACF,EAAA;AACI,EAAA;AAGF,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AAKA,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACD,EAAA;AACD,EAAA;AACF;AAwDO;AAKL,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACK,EAAA;AACH,IAAA;AACF,EAAA;AACA,EAAA;AAKA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAWM;AAGN;AAKM,EAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACK,EAAA;AACH,IAAA;AACF,EAAA;AACA,EAAA;AACI,EAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACF;AA+CO;AAKL,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACK,EAAA;AACH,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AAEE,EAAA;AAEJ,EAAA;AACA,EAAA;AACE,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAqCO;AACD,EAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACD,EAAA;AACH;AAgBM;AAGN;AACE,EAAA;AACA,EAAA;AACI,EAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACE,IAAA;AACE,MAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAGA;AACM,EAAA;AACJ,EAAA;AACE,IAAA;AACE,MAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACE,IAAA;AACE,MAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAqBO;AACL,EAAA;AACA,EAAA;AACI,EAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AAAmC,IAAA;AAEnC,EAAA;AAEA,EAAA;AACA,EAAA;AACA,EAAA;AAEA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACE,QAAA;AAAmB,2BAAA;AACM,2BAAA;AACD,QAAA;AAExB,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AAAW,QAAA;AAEf,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACG,IAAA;AACH,IAAA;AACF,EAAA;AAEA,EAAA;AACI,EAAA;AACJ,EAAA;AACE,IAAA;AACE,MAAA;AACF,IAAA;AACF,EAAA;AAEA,EAAA;AACI,EAAA;AACJ,EAAA;AACE,IAAA;AACA,IAAA;AACF,EAAA;AAEA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACD,EAAA;AACH;AD5UO;AACA;AExQA;AAGL,EAAA;AACA,EAAA;AACA,EAAA;AAIA,EAAA;AACA,EAAA;AACA,EAAA;AACI,EAAA;AACA,EAAA;AACA,EAAA;AACF,IAAA;AACA,IAAA;AACF,EAAA;AACE,IAAA;AACA,IAAA;AACF,EAAA;AACE,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACF;AFqQO;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/libcantus/libcantus/dist/chunk-R7B5N2JK.cjs","sourcesContent":[null,"import { InvalidInputError } from '../../core/errors/index.js';\nimport { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\nimport {\n assertArray,\n assertGenerationBudget,\n assertOptions,\n assertPitchClass,\n assertPositiveInt,\n} from '../../core/validation/index.js';\nimport type { Chord } from '../chord/index.js';\nimport { chordPitchClasses, displacedThirds } from '../chord/index.js';\nimport type { ScaleNameInput } from '../scale/index.js';\nimport {\n assertModeMask,\n NAMED_SCALES,\n namedScaleMask,\n requireScaleMask,\n resolveScaleName,\n} from '../scale/index.js';\nimport { type ChordLike, toChordData } from '../symbol/index.js';\n\n/**\n * The mask of a scale a chord-scale question may be asked about.\n *\n * Chord-scale theory is a Western practice, so the register of world scales is\n * not a vocabulary these functions answer from: a raga offered as the scale\n * over an altered dominant would read as an answer while being a category\n * error. {@link chordScales} proposes {@link NAMED_SCALES} and nothing else,\n * and the questions asked about one chosen scale accept exactly what it can\n * propose — an alias of a Western scale included, a world scale not.\n *\n * @param name The scale name or alias.\n * @param label What the name is, for the error message.\n * @returns The 12-bit mask.\n * @throws If the name is not a built-in scale, or names a world scale.\n */\nfunction requireChordScaleMask(name: ScaleNameInput, label = 'scale'): number {\n const canonical = resolveScaleName(name);\n if (canonical !== undefined && !Object.hasOwn(NAMED_SCALES, canonical)) {\n throw new InvalidInputError(\n `${label} must name a scale chord-scale theory ranks; ${String(name)} is a world scale`,\n );\n }\n return requireScaleMask(name, label);\n}\n\n/** Count the set bits (scale tones) in a 12-bit mode mask. */\nfunction popcount12(mask: number): number {\n let count = 0;\n for (let n = 0; n < 12; n += 1) {\n count += (mask >> n) & 1;\n }\n return count;\n}\n\n/** Test whether a pitch class belongs to a mode mask rooted on `scaleRootPc`. */\nfunction maskHasPitchClass(mask: number, scaleRootPc: number, pc: number): boolean {\n const offset = pitchClass(pc - scaleRootPc);\n return ((mask >> offset) & 1) === 1;\n}\n\n/**\n * Test whether every chord pitch class is contained in a scale.\n *\n * @param chordPcs The chord's pitch classes (0..11).\n * @param scaleMask The scale's 12-bit mode mask.\n * @param scaleRootPc The pitch class the mask is rooted on.\n * @returns True if the scale is a superset of the chord.\n *\n * @category Scales\n */\nexport function scaleMatchesChord(\n chordPcs: number[],\n scaleMask: number,\n scaleRootPc: number,\n): boolean {\n // The mask is read with bit operations and a pitch class shifts it, so both\n // coerce: an entry that is not a pitch class would silently be read as one.\n assertModeMask(scaleMask, 'scaleMask');\n assertPitchClass(scaleRootPc, 'scaleRootPc');\n const pcs = assertArray<number>(chordPcs, 'chord pitch classes');\n for (let index = 0; index < pcs.length; index += 1) {\n const pc = assertPitchClass(pcs[index] ?? Number.NaN, `chord pitch classes[${index}]`);\n if (!maskHasPitchClass(scaleMask, scaleRootPc, pc)) {\n return false;\n }\n }\n return true;\n}\n\n/**\n * A named scale rooted on a pitch class that fits over a chord.\n *\n * @category Scales\n */\nexport type ChordScaleMatch = {\n name: string;\n rootPc: number;\n};\n\n/**\n * Scale names whose mask duplicates another {@link NAMED_SCALES} entry\n * (major/ionian, naturalMinor/aeolian). Chord-scale analysis conventionally\n * speaks in mode names, so these aliases are skipped and only the modal name\n * is reported.\n */\nconst ALIASED_SCALE_NAMES = new Set(['major', 'naturalMinor']);\n\n/** Number of tones in a heptatonic (seven-note) scale. */\nconst HEPTATONIC_SIZE = 7;\n\n/**\n * Conventional preference among equally-fitting scales, brightest first.\n *\n * Several modes fit a chord that states no third or sixth — a power chord, a\n * suspension — equally well. Alphabetical order made `aeolian` the answer for\n * those, naming a minor mode as the best fit for a mode-neutral chord.\n */\nconst SCALE_PREFERENCE = [\n 'ionian',\n 'major',\n 'mixolydian',\n 'dorian',\n 'lydian',\n 'aeolian',\n 'naturalMinor',\n 'phrygian',\n 'locrian',\n] as const;\n\n/** Conventional first choices that pure set-fit ranking cannot express. */\nconst IDIOMATIC_SCALES: Partial<Record<Chord['quality'], readonly string[]>> = {\n aug: ['wholeTone'],\n m7b5: ['locrian'],\n // The diminished scale played over a diminished seventh is the whole-half\n // octatonic. Its half-whole rotation belongs to the dominant it colours: over\n // a diminished seventh every one of its non-chord tones lands a semitone\n // above a chord tone, so it offers no tension at all.\n dim7: ['octatonicWholeHalf'],\n minMaj7: ['melodicMinor'],\n};\n\n/** Position of a scale in {@link SCALE_PREFERENCE}, or last when unlisted. */\nfunction conventionalRank(name: string): number {\n const index = (SCALE_PREFERENCE as readonly string[]).indexOf(name);\n return index < 0 ? SCALE_PREFERENCE.length : index;\n}\n\n/** Whether this scale is an idiomatic color choice for the chord quality. */\nfunction idiomaticRank(chord: Chord, name: string): number {\n const choices = IDIOMATIC_SCALES[chord.quality];\n if (choices === undefined) {\n return 0;\n }\n const index = choices.indexOf(name);\n return index < 0 ? choices.length + 1 : index;\n}\n\n/**\n * An ordinary dominant seventh may use altered colors while retaining its\n * perfect fifth in the voicing; the `7alt` quality makes that alteration\n * explicit, but lead sheets commonly write the shorter `7` here.\n */\nfunction isAlteredDominant(chord: Chord, scaleName: string): boolean {\n return chord.quality === 'dom7' && scaleName === 'altered';\n}\n\n/**\n * List the scales that fit over a chord, best fit first.\n *\n * Only the chord root is considered as the scale root, matching the\n * conventional chord-scale relationship. Every entry of {@link NAMED_SCALES}\n * whose pitch-class set is a superset of the chord's is returned once per\n * distinct pitch-class set (aliased masks such as major/ionian report only the\n * modal name). The chromatic scale is only returned as a fallback when no other\n * scale contains the chord.\n *\n * The ranking applies these keys in order:\n *\n * 1. The idiomatic scales for the chord quality, in the order they are named\n * there, ahead of every scale that is not one of them.\n * 2. For bare triads and smaller chords, heptatonic scales ahead of pentatonics\n * and other sizes: a pentatonic adds no modal color over a triad, so the\n * seven-note modes are the more useful answer.\n * 3. Fewest extra scale tones beyond the chord.\n * 4. Conventional modal preference.\n * 5. Fewest avoid notes, which separates the symmetric and melodic-minor\n * scales the modal preference does not name.\n * 6. Alphabetical order, so the list is stable.\n *\n * The idiomatic key comes first, so the head of the list is the scale a player\n * would name rather than the tightest fit: over `Cm7b5` the locrian mode leads\n * even though the blues scale adds fewer tones, because locrian is what a\n * half-diminished chord is read as. Read further down the list, or pick by name,\n * when the closest fit is what is wanted; {@link scalesForChanges} makes the\n * choice for a whole progression.\n *\n * @param chord The chord to fit scales over, as a chord symbol, chord data, or\n * a `Chord`.\n * @returns The matching scales rooted on the chord root, best fit first.\n *\n * @example\n * ```ts\n * import { makeChord, chordScales } from '@libraz/libcantus';\n * const scales = chordScales(makeChord(0, 'maj7'));\n * // scales for Cmaj7, best fit first; each is { name, rootPc: 0 }\n * ```\n *\n * @category Scales\n */\nexport function chordScales(chord: ChordLike): ChordScaleMatch[] {\n const data = toChordData(chord);\n const chordPcs = chordPitchClasses(data);\n const rootPc = pitchClass(data.rootPc);\n const ranked: { name: string; extra: number; size: number; avoid: number }[] = [];\n const seenMasks = new Set<number>();\n for (const name of Object.keys(NAMED_SCALES)) {\n if (name === 'chromatic' || ALIASED_SCALE_NAMES.has(name)) {\n continue;\n }\n const mask = namedScaleMask(name);\n if (mask === undefined || seenMasks.has(mask)) {\n continue;\n }\n seenMasks.add(mask);\n if (scaleMatchesChord(chordPcs, mask, rootPc) || isAlteredDominant(data, name)) {\n const size = popcount12(mask);\n ranked.push({\n name,\n extra: size - chordPcs.length,\n size,\n avoid: avoidNotes(data, name).length,\n });\n }\n }\n if (ranked.length === 0) {\n // No diatonic-style scale contains the chord; fall back to the chromatic\n // scale, which trivially contains every pitch class.\n if (NAMED_SCALES.chromatic !== undefined) {\n return [{ name: 'chromatic', rootPc }];\n }\n return [];\n }\n const preferHeptatonic = chordPcs.length <= 3;\n ranked.sort((a, b) => {\n const idiomaticA = idiomaticRank(data, a.name);\n const idiomaticB = idiomaticRank(data, b.name);\n if (idiomaticA !== idiomaticB) {\n return idiomaticA - idiomaticB;\n }\n if (preferHeptatonic && (a.size === HEPTATONIC_SIZE) !== (b.size === HEPTATONIC_SIZE)) {\n return a.size === HEPTATONIC_SIZE ? -1 : 1;\n }\n if (a.extra !== b.extra) {\n return a.extra - b.extra;\n }\n const rankA = conventionalRank(a.name);\n const rankB = conventionalRank(b.name);\n if (rankA !== rankB) {\n return rankA - rankB;\n }\n // Among scales the modal preference does not name — the symmetric and\n // melodic-minor scales — the one that clashes with the chord least is the\n // one a player reaches for. Alphabetical order answered with the rotation\n // that leaves nothing playable.\n if (a.avoid !== b.avoid) {\n return a.avoid - b.avoid;\n }\n return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;\n });\n return ranked.map((entry) => ({ name: entry.name, rootPc }));\n}\n\n/**\n * Which use an avoid note is judged for: sounding it, or passing through it.\n *\n * `'harmonic'` is the Berklee-style rule in full — a tone that must not be\n * sounded against the chord. `'melodic'` keeps only what a line cannot pass\n * through either, which is the semitone above the root; every other clash\n * resolves as the line moves on.\n *\n * @category Scales\n */\nexport type AvoidNoteUse = 'harmonic' | 'melodic';\n\n/**\n * Options for {@link avoidNotes}.\n *\n * @category Scales\n */\nexport type AvoidNotesOptions = {\n use?: AvoidNoteUse;\n};\n\n/**\n * List a scale's avoid notes over a chord.\n *\n * An avoid note is a non-chord scale tone that lies a semitone directly above a\n * chord tone, or the third a suspension displaced; sounding it against the\n * chord clashes. The scale is rooted on the chord root. If the scale does not\n * contain the chord, no avoid notes exist.\n *\n * The rule states which tones must not be *sounded* against the chord, which is\n * not the same question as which tones a line may not touch. Pass\n * `{ use: 'melodic' }` for the melodic reading: only the semitone above the\n * root survives it, so the result is always a subset of the harmonic one.\n *\n * @param chord The chord providing the chord tones, as a chord symbol, chord\n * data, or a `Chord`.\n * @param scaleName A key of {@link NAMED_SCALES}, or an alias of one. A world\n * scale is not one: chord-scale theory is a Western practice, so a raga named\n * here is rejected rather than answered with a number.\n * @param opts Set `use: 'melodic'` to judge a line rather than a voicing.\n * @returns The avoid-note pitch classes, sorted ascending in [0, 11].\n * @throws If `scaleName` is not a built-in scale. An empty result already\n * means \"this scale has no avoid notes over this chord\"; answering a typo\n * the same way would make the two indistinguishable.\n *\n * @example\n * ```ts\n * import { makeChord, avoidNotes } from '@libraz/libcantus';\n * avoidNotes(makeChord(0, 'maj7'), 'ionian'); // [5] — F clashes with the third\n * avoidNotes(makeChord(0, 'maj7'), 'ionian', { use: 'melodic' }); // [] — a line may pass through it\n * ```\n *\n * @category Scales\n */\nexport function avoidNotes(\n chord: ChordLike,\n scaleName: ScaleNameInput,\n opts: AvoidNotesOptions = {},\n): number[] {\n const asked = assertOptions(opts, 'opts');\n const mask = requireChordScaleMask(scaleName);\n const data = toChordData(chord);\n const rootPc = pitchClass(data.rootPc);\n const chordPcs = chordPitchClasses(data);\n const alteredDominant = isAlteredDominant(data, scaleName);\n if (!scaleMatchesChord(chordPcs, mask, rootPc) && !alteredDominant) {\n return [];\n }\n const chordSet = new Set(chordPcs);\n // Sounding the third a suspension stands in for is what undoes the\n // suspension, so it is an avoid note however consonant it is against the\n // scale. Both thirds are named: a suspension does not say which one it\n // displaced — that is what suspending is — so meeting either one ends it.\n const displaced = new Set(displacedThirds(data));\n const melodic = asked.use === 'melodic';\n const semitoneAboveRoot = pitchClass(rootPc + 1);\n const avoid: number[] = [];\n for (let pc = 0; pc < 12; pc += 1) {\n if (!maskHasPitchClass(mask, rootPc, pc) || chordSet.has(pc)) {\n continue;\n }\n if (melodic && pc !== semitoneAboveRoot) {\n continue;\n }\n if (!alteredDominant && (chordSet.has(pitchClass(pc - 1)) || displaced.has(pc))) {\n avoid.push(pc);\n }\n }\n return avoid.sort((a, b) => a - b);\n}\n\n/**\n * Semitones above the root of the colors a dominant takes when it resolves to a\n * minor tonic: the flat ninth, the sharp ninth, and the flat thirteenth.\n *\n * Over a dominant read on its own, each of them sits a semitone above a chord\n * tone and is an avoid note. Heard as the dominant of a minor key they are the\n * key's own tones, and standard practice, so the function the chord serves is\n * what decides.\n */\nconst MINOR_RESOLUTION_TENSIONS = [1, 3, 8] as const;\n\n/** Whether this chord is a dominant seventh resolving down a fifth to minor. */\nfunction resolvesToMinorTonic(\n chord: Chord,\n chordPcs: number[],\n target: Chord | undefined,\n): boolean {\n if (target === undefined) {\n return false;\n }\n const rootPc = pitchClass(chord.rootPc);\n const sounds = (semitones: number): boolean => chordPcs.includes(pitchClass(rootPc + semitones));\n if (!sounds(4) || !sounds(10)) {\n return false;\n }\n const targetRootPc = pitchClass(target.rootPc);\n if (targetRootPc !== pitchClass(rootPc + 5)) {\n return false;\n }\n return chordPitchClasses(target).includes(pitchClass(targetRootPc + 3));\n}\n\n/**\n * Options for {@link availableTensions}.\n *\n * @category Scales\n */\nexport type AvailableTensionsOptions = {\n /**\n * The chord this one resolves to, as a chord symbol, chord data, or a\n * `Chord`.\n */\n resolvesTo?: ChordLike;\n};\n\n/**\n * List a scale's available tensions over a chord.\n *\n * Available tensions are scale tones that are neither chord tones nor avoid\n * notes, i.e. the usable color tones (typically the 9/11/13 region). The scale\n * is rooted on the chord root. If the scale does not contain the chord, there\n * are no available tensions.\n *\n * A tension's availability also depends on what the chord is doing. Pass\n * `resolvesTo` to say which chord this one resolves to: a dominant seventh\n * resolving down a fifth to a minor tonic takes its flat ninth, sharp ninth and\n * flat thirteenth, which read on their own would each be an avoid note. Only\n * scale tones are ever added, so the result stays within the scale.\n *\n * @param chord The chord providing the chord tones, as a chord symbol, chord\n * data, or a `Chord`.\n * @param scaleName A key of {@link NAMED_SCALES}, or an alias of one; a world\n * scale is outside chord-scale theory, as in {@link avoidNotes}.\n * @param opts Set `resolvesTo` to the chord this one resolves to.\n * @returns The available-tension pitch classes, sorted ascending in [0, 11].\n * @throws If `scaleName` is not a built-in scale, as {@link avoidNotes} does.\n *\n * @example\n * ```ts\n * import { makeChord, availableTensions } from '@libraz/libcantus';\n * availableTensions(makeChord(0, 'maj7'), 'ionian'); // [2, 9] — color tones over Cmaj7\n * availableTensions(makeChord(7, 'dom7'), 'phrygianDominant', { resolvesTo: makeChord(0, 'min') });\n * // [3, 8] — the b13 and b9 the resolution to C minor makes available\n * ```\n *\n * @category Scales\n */\nexport function availableTensions(\n chord: ChordLike,\n scaleName: ScaleNameInput,\n opts: AvailableTensionsOptions = {},\n): number[] {\n const asked = assertOptions(opts, 'opts');\n const mask = requireChordScaleMask(scaleName);\n const data = toChordData(chord);\n const rootPc = pitchClass(data.rootPc);\n const chordPcs = chordPitchClasses(data);\n const alteredDominant = isAlteredDominant(data, scaleName);\n if (!scaleMatchesChord(chordPcs, mask, rootPc) && !alteredDominant) {\n return [];\n }\n const chordSet = new Set(chordPcs);\n const avoidSet = alteredDominant ? new Set<number>() : new Set(avoidNotes(data, scaleName));\n const functional = resolvesToMinorTonic(\n data,\n chordPcs,\n asked.resolvesTo === undefined ? undefined : toChordData(asked.resolvesTo),\n )\n ? new Set(MINOR_RESOLUTION_TENSIONS.map((semitones) => pitchClass(rootPc + semitones)))\n : undefined;\n const tensions: number[] = [];\n for (let pc = 0; pc < 12; pc += 1) {\n if (!maskHasPitchClass(mask, rootPc, pc) || chordSet.has(pc)) {\n continue;\n }\n if (avoidSet.has(pc) && functional?.has(pc) !== true) {\n continue;\n }\n tensions.push(pc);\n }\n return tensions.sort((a, b) => a - b);\n}\n\n/**\n * A scale fit over a chord, with its non-chord tones sorted by what may be done\n * with them.\n *\n * `avoid`, `passing` and `tensions` partition the scale tones the chord does\n * not state: `avoid` may not be played at all, `passing` may be passed through\n * melodically but not sounded against the chord, and `tensions` may be added\n * freely as color. `avoid` is {@link avoidNotes} under `use: 'melodic'`, and\n * `avoid` together with `passing` is the harmonic reading.\n *\n * @category Scales\n */\nexport type ChordScaleReportEntry = {\n name: string;\n rootPc: number;\n avoid: number[];\n passing: number[];\n tensions: number[];\n};\n\n/**\n * Report the best-fitting scales for a chord with their avoid notes and tensions.\n *\n * Combines {@link chordScales}, {@link avoidNotes}, and {@link availableTensions}\n * into a single ergonomic result, ordered best fit first. The report is read as\n * playing advice, so the tones a line may pass through are reported apart from\n * the ones it may not touch; see {@link ChordScaleReportEntry}.\n *\n * @param chord The chord to analyze, as a chord symbol, chord data, or a\n * `Chord`.\n * @param limit Optional maximum number of scales to report; all by default.\n * @returns One entry per reported scale, best fit first.\n *\n * @category Scales\n */\nexport function chordScaleReport(chord: ChordLike, limit?: number): ChordScaleReportEntry[] {\n if (limit !== undefined) {\n assertPositiveInt(limit, 'chord-scale report limit');\n }\n const data = toChordData(chord);\n const matches = chordScales(data);\n const chosen = limit === undefined ? matches : matches.slice(0, limit);\n return chosen.map((match) => {\n const harmonic = avoidNotes(data, match.name);\n const melodic = new Set(avoidNotes(data, match.name, { use: 'melodic' }));\n return {\n name: match.name,\n rootPc: match.rootPc,\n avoid: harmonic.filter((pc) => melodic.has(pc)),\n passing: harmonic.filter((pc) => !melodic.has(pc)),\n tensions: availableTensions(data, match.name),\n };\n });\n}\n\n/**\n * A chord paired with the scale chosen for it by {@link scalesForChanges}.\n *\n * @category Scales\n */\nexport type ScaleChoice = {\n chord: Chord;\n scale: ChordScaleMatch;\n};\n\n/**\n * Small per-candidate penalty added in {@link scalesForChanges} so that, among\n * choices of otherwise-equal transition cost, the tighter best-fit scale wins.\n */\nconst RANK_PENALTY = 0.01;\n\n/** Build the pitch-class set of a named scale rooted on `rootPc`. */\nfunction scalePitchClassSet(name: string, rootPc: number): Set<number> {\n const mask = namedScaleMask(name);\n const pcs = new Set<number>();\n if (mask === undefined) {\n return pcs;\n }\n for (let n = 0; n < 12; n += 1) {\n if (((mask >> n) & 1) === 1) {\n pcs.add(pitchClass(rootPc + n));\n }\n }\n return pcs;\n}\n\n/** Count pitch classes that belong to exactly one of two sets. */\nfunction symmetricDifferenceSize(a: Set<number>, b: Set<number>): number {\n let count = 0;\n for (const pc of a) {\n if (!b.has(pc)) {\n count += 1;\n }\n }\n for (const pc of b) {\n if (!a.has(pc)) {\n count += 1;\n }\n }\n return count;\n}\n\n/**\n * Choose one scale per chord across a progression, favoring continuity.\n *\n * Each chord's {@link chordScales} candidates form a stage in a Viterbi-style\n * dynamic program. The transition cost between adjacent choices is the number\n * of pitch classes that differ between their pitch-class sets (the symmetric\n * difference), plus a small penalty for straying from a chord's best-fit scale\n * so that ties break toward the tighter fit. The minimum-total-cost path is\n * returned, one {@link ScaleChoice} per input chord in the original order.\n *\n * Each choice pairs its scale with the chord exactly as it was handed in, so\n * this takes plain chord data rather than the wider forms the entry points\n * above accept.\n *\n * @param chords The chord sequence to choose scales for.\n * @returns One scale choice per chord, in input order.\n *\n * @category Scales\n */\nexport function scalesForChanges(chords: readonly Chord[]): ScaleChoice[] {\n assertArray(chords, 'chord-scale changes');\n assertGenerationBudget(chords.length, 'chord-scale changes');\n if (chords.length === 0) {\n return [];\n }\n const candidateLists = chords.map((chord) => chordScales(chord));\n const pcSets = candidateLists.map((candidates) =>\n candidates.map((match) => scalePitchClassSet(match.name, match.rootPc)),\n );\n\n const first = candidateLists[0] ?? [];\n const dp: number[][] = [first.map((_, j) => j * RANK_PENALTY)];\n const back: number[][] = [first.map(() => -1)];\n\n for (let i = 1; i < candidateLists.length; i += 1) {\n const candidates = candidateLists[i] ?? [];\n const prevCosts = dp[i - 1] ?? [];\n const prevSets = pcSets[i - 1] ?? [];\n const curSets = pcSets[i] ?? [];\n const stageCosts: number[] = [];\n const stageBack: number[] = [];\n for (let j = 0; j < candidates.length; j += 1) {\n let bestCost = Number.POSITIVE_INFINITY;\n let bestPrev = -1;\n for (let k = 0; k < prevCosts.length; k += 1) {\n const transition = symmetricDifferenceSize(\n prevSets[k] ?? new Set(),\n curSets[j] ?? new Set(),\n );\n const cost = (prevCosts[k] ?? 0) + transition;\n if (cost < bestCost) {\n bestCost = cost;\n bestPrev = k;\n }\n }\n stageCosts.push(bestCost + j * RANK_PENALTY);\n stageBack.push(bestPrev);\n }\n dp.push(stageCosts);\n back.push(stageBack);\n }\n\n const lastCosts = dp[dp.length - 1] ?? [];\n let bestFinal = 0;\n for (let j = 1; j < lastCosts.length; j += 1) {\n if ((lastCosts[j] ?? Number.POSITIVE_INFINITY) < (lastCosts[bestFinal] ?? 0)) {\n bestFinal = j;\n }\n }\n\n const chosenIndices: number[] = new Array(candidateLists.length);\n let current = bestFinal;\n for (let i = candidateLists.length - 1; i >= 0; i -= 1) {\n chosenIndices[i] = current;\n current = back[i]?.[current] ?? -1;\n }\n\n return chords.map((chord, i) => {\n const candidates = candidateLists[i] ?? [];\n const scale = candidates[chosenIndices[i] ?? 0] ?? { name: 'chromatic', rootPc: chord.rootPc };\n return { chord, scale };\n });\n}\n","import { pitchClassOf } from '../../core/pitch/index.js';\nimport { assertFiniteNumber } from '../../core/validation/index.js';\nimport { type ChordToneRole, chordToneRole, thirdSlotOf } from '../chord/index.js';\nimport { type ChordLike, toChordData } from '../symbol/index.js';\n\n/**\n * Harmonic role a pitch plays within a chord.\n *\n * @category Functional Harmony\n */\nexport type HarmonyRole = ChordToneRole | 'tension' | 'doubling';\n\n/**\n * How firmly a pitch is locked to the chord's identity.\n *\n * - `identity`: moving it produces a different chord (the root).\n * - `quality`: moving it flips the chord quality (the third).\n * - `voicing`: it can move freely without changing chord identity or quality.\n *\n * @category Functional Harmony\n */\nexport type LockLevel = 'identity' | 'quality' | 'voicing';\n\n/**\n * A pitch's role, lock level, and owning chord.\n *\n * @category Functional Harmony\n */\nexport type VoicedRole = {\n role: HarmonyRole;\n lock: LockLevel;\n belongsToChordId: number;\n};\n\n/**\n * Classify a pitch's harmonic role and lock level within a chord.\n *\n * The role comes from the pitch's interval class above the chord root, read\n * against the chord's own template: root (0), third (3/4), fifth (6/7/8),\n * sixth or seventh (9/10/11). Every pitch the template does not claim is\n * `'tension'` — including one that is simply foreign to the chord, which this\n * single-pitch query cannot tell from a colour tone. The root locks the chord\n * identity, the third locks its quality, and everything else is free voicing.\n * Where a chord has no third, the tone standing in its place — the fourth of a\n * `sus4`, the second of a `sus2`, the eleventh of an eleventh chord — takes the\n * third's slot and is locked to `quality`, since moving it changes the chord.\n * At most one interval class of a chord is ever reported as its third, so a\n * consumer reading `lock: 'quality'` as \"this tone identifies the chord\" gets\n * one answer rather than two. Detecting an octave doubling requires the\n * surrounding voicing, which this query does not carry, so `'doubling'` is part\n * of the type — the shared vocabulary of harmonic roles — but is never returned\n * here.\n *\n * @param pitch MIDI pitch or bare pitch class.\n * @param chord The chord providing the root reference, as a chord symbol, chord\n * data, or a `Chord`.\n * @param chordId Identifier stored on the result (defaults to 0).\n * @returns The pitch's role, lock level, and owning chord id.\n * @example\n * ```ts\n * import { roleOf, makeChord } from '@libraz/libcantus';\n * roleOf(64, makeChord(0, 'maj')); // E over C major\n * // { role: 'third', lock: 'quality', belongsToChordId: 0 }\n * ```\n * @category Functional Harmony\n */\nexport function roleOf(pitch: number, chord: ChordLike, chordId = 0): VoicedRole {\n // The id is carried into the result, so a value that is not one would be\n // reported back as the chord this role belongs to.\n assertFiniteNumber(chordId, 'chordId');\n const data = toChordData(chord);\n const interval = (pitchClassOf(pitch) - pitchClassOf(data.rootPc) + 12) % 12;\n // What stands in the third's slot is read where the chord is read, not here:\n // the tone that identifies a chord and the tone the avoid-note rule refuses\n // against it are two consequences of one fact about the chord.\n const slot = thirdSlotOf(data);\n const isSuspendedTone = slot.kind === 'suspension' && interval === slot.interval;\n const chordRole = chordToneRole(pitch, data);\n let role: HarmonyRole;\n let lock: LockLevel;\n if (isSuspendedTone) {\n role = 'third'; // suspended tone occupies the third's quality-defining slot\n lock = 'quality';\n } else if (chordRole !== null) {\n role = chordRole;\n lock = chordRole === 'root' ? 'identity' : chordRole === 'third' ? 'quality' : 'voicing';\n } else {\n role = 'tension';\n lock = 'voicing';\n }\n return { role, lock, belongsToChordId: chordId };\n}\n"]}
1
+ {"version":3,"sources":["/home/runner/work/libcantus/libcantus/dist/chunk-PM4XJE2Y.cjs","../src/theory/chordscale/index.ts","../src/theory/harmony/index.ts"],"names":[],"mappings":"AAAA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,wDAA6B;AAC7B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,wDAA6B;AAC7B;AACA;ACaA,SAAS,qBAAA,CAAsB,IAAA,EAAsB,MAAA,EAAQ,OAAA,EAAiB;AAC5E,EAAA,MAAM,UAAA,EAAY,gDAAA,IAAqB,CAAA;AACvC,EAAA,GAAA,CAAI,UAAA,IAAc,KAAA,EAAA,GAAa,CAAC,MAAA,CAAO,MAAA,CAAO,8BAAA,EAAc,SAAS,CAAA,EAAG;AACtE,IAAA,MAAM,IAAI,wCAAA;AAAA,MACR,CAAA,EAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAGA;AACM,EAAA;AACJ,EAAA;AACE,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAGA;AACE,EAAA;AACA,EAAA;AACF;AAYO;AAOL,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAkBM;AAGA;AASA;AACJ,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAGM;AACC,EAAA;AACL,EAAA;AAAgB;AAAA;AAAA;AAAA;AAKhB,EAAA;AACA,EAAA;AACF;AAGA;AACE,EAAA;AACA,EAAA;AACF;AAGA;AACE,EAAA;AACI,EAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAOA;AACE,EAAA;AACF;AA6CO;AACL,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AAAY,QAAA;AACV,QAAA;AACuB,QAAA;AACvB,QAAA;AAED,MAAA;AACH,IAAA;AACF,EAAA;AACI,EAAA;AAGF,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AAKA,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACD,EAAA;AACD,EAAA;AACF;AAwDO;AAKL,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACK,EAAA;AACH,IAAA;AACF,EAAA;AACA,EAAA;AAKA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAWM;AAGN;AAKM,EAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACK,EAAA;AACH,IAAA;AACF,EAAA;AACA,EAAA;AACI,EAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACF;AA+CO;AAKL,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACK,EAAA;AACH,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AAEE,EAAA;AAEJ,EAAA;AACA,EAAA;AACE,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAqCO;AACD,EAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACD,EAAA;AACH;AAgBM;AAGN;AACE,EAAA;AACA,EAAA;AACI,EAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACE,IAAA;AACE,MAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAGA;AACM,EAAA;AACJ,EAAA;AACE,IAAA;AACE,MAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACE,IAAA;AACE,MAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAqBO;AACL,EAAA;AACA,EAAA;AACI,EAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AAAmC,IAAA;AAEnC,EAAA;AAEA,EAAA;AACA,EAAA;AACA,EAAA;AAEA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACE,QAAA;AAAmB,2BAAA;AACM,2BAAA;AACD,QAAA;AAExB,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AAAW,QAAA;AAEf,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACG,IAAA;AACH,IAAA;AACF,EAAA;AAEA,EAAA;AACI,EAAA;AACJ,EAAA;AACE,IAAA;AACE,MAAA;AACF,IAAA;AACF,EAAA;AAEA,EAAA;AACI,EAAA;AACJ,EAAA;AACE,IAAA;AACA,IAAA;AACF,EAAA;AAEA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACD,EAAA;AACH;AD5UO;AACA;AExQA;AAGL,EAAA;AACA,EAAA;AACA,EAAA;AAIA,EAAA;AACA,EAAA;AACA,EAAA;AACI,EAAA;AACA,EAAA;AACA,EAAA;AACF,IAAA;AACA,IAAA;AACF,EAAA;AACE,IAAA;AACA,IAAA;AACF,EAAA;AACE,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACF;AFqQO;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/libcantus/libcantus/dist/chunk-PM4XJE2Y.cjs","sourcesContent":[null,"import { InvalidInputError } from '../../core/errors/index.js';\nimport { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\nimport {\n assertArray,\n assertGenerationBudget,\n assertOptions,\n assertPitchClass,\n assertPositiveInt,\n} from '../../core/validation/index.js';\nimport type { Chord } from '../chord/index.js';\nimport { chordPitchClasses, displacedThirds } from '../chord/index.js';\nimport type { ScaleNameInput } from '../scale/index.js';\nimport {\n assertModeMask,\n NAMED_SCALES,\n namedScaleMask,\n requireScaleMask,\n resolveScaleName,\n} from '../scale/index.js';\nimport { type ChordLike, toChordData } from '../symbol/index.js';\n\n/**\n * The mask of a scale a chord-scale question may be asked about.\n *\n * Chord-scale theory is a Western practice, so the register of world scales is\n * not a vocabulary these functions answer from: a raga offered as the scale\n * over an altered dominant would read as an answer while being a category\n * error. {@link chordScales} proposes {@link NAMED_SCALES} and nothing else,\n * and the questions asked about one chosen scale accept exactly what it can\n * propose — an alias of a Western scale included, a world scale not.\n *\n * @param name The scale name or alias.\n * @param label What the name is, for the error message.\n * @returns The 12-bit mask.\n * @throws If the name is not a built-in scale, or names a world scale.\n */\nfunction requireChordScaleMask(name: ScaleNameInput, label = 'scale'): number {\n const canonical = resolveScaleName(name);\n if (canonical !== undefined && !Object.hasOwn(NAMED_SCALES, canonical)) {\n throw new InvalidInputError(\n `${label} must name a scale chord-scale theory ranks; ${String(name)} is a world scale`,\n );\n }\n return requireScaleMask(name, label);\n}\n\n/** Count the set bits (scale tones) in a 12-bit mode mask. */\nfunction popcount12(mask: number): number {\n let count = 0;\n for (let n = 0; n < 12; n += 1) {\n count += (mask >> n) & 1;\n }\n return count;\n}\n\n/** Test whether a pitch class belongs to a mode mask rooted on `scaleRootPc`. */\nfunction maskHasPitchClass(mask: number, scaleRootPc: number, pc: number): boolean {\n const offset = pitchClass(pc - scaleRootPc);\n return ((mask >> offset) & 1) === 1;\n}\n\n/**\n * Test whether every chord pitch class is contained in a scale.\n *\n * @param chordPcs The chord's pitch classes (0..11).\n * @param scaleMask The scale's 12-bit mode mask.\n * @param scaleRootPc The pitch class the mask is rooted on.\n * @returns True if the scale is a superset of the chord.\n *\n * @category Scales\n */\nexport function scaleMatchesChord(\n chordPcs: number[],\n scaleMask: number,\n scaleRootPc: number,\n): boolean {\n // The mask is read with bit operations and a pitch class shifts it, so both\n // coerce: an entry that is not a pitch class would silently be read as one.\n assertModeMask(scaleMask, 'scaleMask');\n assertPitchClass(scaleRootPc, 'scaleRootPc');\n const pcs = assertArray<number>(chordPcs, 'chord pitch classes');\n for (let index = 0; index < pcs.length; index += 1) {\n const pc = assertPitchClass(pcs[index] ?? Number.NaN, `chord pitch classes[${index}]`);\n if (!maskHasPitchClass(scaleMask, scaleRootPc, pc)) {\n return false;\n }\n }\n return true;\n}\n\n/**\n * A named scale rooted on a pitch class that fits over a chord.\n *\n * @category Scales\n */\nexport type ChordScaleMatch = {\n name: string;\n rootPc: number;\n};\n\n/**\n * Scale names whose mask duplicates another {@link NAMED_SCALES} entry\n * (major/ionian, naturalMinor/aeolian). Chord-scale analysis conventionally\n * speaks in mode names, so these aliases are skipped and only the modal name\n * is reported.\n */\nconst ALIASED_SCALE_NAMES = new Set(['major', 'naturalMinor']);\n\n/** Number of tones in a heptatonic (seven-note) scale. */\nconst HEPTATONIC_SIZE = 7;\n\n/**\n * Conventional preference among equally-fitting scales, brightest first.\n *\n * Several modes fit a chord that states no third or sixth — a power chord, a\n * suspension — equally well. Alphabetical order made `aeolian` the answer for\n * those, naming a minor mode as the best fit for a mode-neutral chord.\n */\nconst SCALE_PREFERENCE = [\n 'ionian',\n 'major',\n 'mixolydian',\n 'dorian',\n 'lydian',\n 'aeolian',\n 'naturalMinor',\n 'phrygian',\n 'locrian',\n] as const;\n\n/** Conventional first choices that pure set-fit ranking cannot express. */\nconst IDIOMATIC_SCALES: Partial<Record<Chord['quality'], readonly string[]>> = {\n aug: ['wholeTone'],\n m7b5: ['locrian'],\n // The diminished scale played over a diminished seventh is the whole-half\n // octatonic. Its half-whole rotation belongs to the dominant it colours: over\n // a diminished seventh every one of its non-chord tones lands a semitone\n // above a chord tone, so it offers no tension at all.\n dim7: ['octatonicWholeHalf'],\n minMaj7: ['melodicMinor'],\n};\n\n/** Position of a scale in {@link SCALE_PREFERENCE}, or last when unlisted. */\nfunction conventionalRank(name: string): number {\n const index = (SCALE_PREFERENCE as readonly string[]).indexOf(name);\n return index < 0 ? SCALE_PREFERENCE.length : index;\n}\n\n/** Whether this scale is an idiomatic color choice for the chord quality. */\nfunction idiomaticRank(chord: Chord, name: string): number {\n const choices = IDIOMATIC_SCALES[chord.quality];\n if (choices === undefined) {\n return 0;\n }\n const index = choices.indexOf(name);\n return index < 0 ? choices.length + 1 : index;\n}\n\n/**\n * An ordinary dominant seventh may use altered colors while retaining its\n * perfect fifth in the voicing; the `7alt` quality makes that alteration\n * explicit, but lead sheets commonly write the shorter `7` here.\n */\nfunction isAlteredDominant(chord: Chord, scaleName: string): boolean {\n return chord.quality === 'dom7' && scaleName === 'altered';\n}\n\n/**\n * List the scales that fit over a chord, best fit first.\n *\n * Only the chord root is considered as the scale root, matching the\n * conventional chord-scale relationship. Every entry of {@link NAMED_SCALES}\n * whose pitch-class set is a superset of the chord's is returned once per\n * distinct pitch-class set (aliased masks such as major/ionian report only the\n * modal name). The chromatic scale is only returned as a fallback when no other\n * scale contains the chord.\n *\n * The ranking applies these keys in order:\n *\n * 1. The idiomatic scales for the chord quality, in the order they are named\n * there, ahead of every scale that is not one of them.\n * 2. For bare triads and smaller chords, heptatonic scales ahead of pentatonics\n * and other sizes: a pentatonic adds no modal color over a triad, so the\n * seven-note modes are the more useful answer.\n * 3. Fewest extra scale tones beyond the chord.\n * 4. Conventional modal preference.\n * 5. Fewest avoid notes, which separates the symmetric and melodic-minor\n * scales the modal preference does not name.\n * 6. Alphabetical order, so the list is stable.\n *\n * The idiomatic key comes first, so the head of the list is the scale a player\n * would name rather than the tightest fit: over `Cm7b5` the locrian mode leads\n * even though the blues scale adds fewer tones, because locrian is what a\n * half-diminished chord is read as. Read further down the list, or pick by name,\n * when the closest fit is what is wanted; {@link scalesForChanges} makes the\n * choice for a whole progression.\n *\n * @param chord The chord to fit scales over, as a chord symbol, chord data, or\n * a `Chord`.\n * @returns The matching scales rooted on the chord root, best fit first.\n *\n * @example\n * ```ts\n * import { makeChord, chordScales } from '@libraz/libcantus';\n * const scales = chordScales(makeChord(0, 'maj7'));\n * // scales for Cmaj7, best fit first; each is { name, rootPc: 0 }\n * ```\n *\n * @category Scales\n */\nexport function chordScales(chord: ChordLike): ChordScaleMatch[] {\n const data = toChordData(chord);\n const chordPcs = chordPitchClasses(data);\n const rootPc = pitchClass(data.rootPc);\n const ranked: { name: string; extra: number; size: number; avoid: number }[] = [];\n const seenMasks = new Set<number>();\n for (const name of Object.keys(NAMED_SCALES)) {\n if (name === 'chromatic' || ALIASED_SCALE_NAMES.has(name)) {\n continue;\n }\n const mask = namedScaleMask(name);\n if (mask === undefined || seenMasks.has(mask)) {\n continue;\n }\n seenMasks.add(mask);\n if (scaleMatchesChord(chordPcs, mask, rootPc) || isAlteredDominant(data, name)) {\n const size = popcount12(mask);\n ranked.push({\n name,\n extra: size - chordPcs.length,\n size,\n avoid: avoidNotes(data, name).length,\n });\n }\n }\n if (ranked.length === 0) {\n // No diatonic-style scale contains the chord; fall back to the chromatic\n // scale, which trivially contains every pitch class.\n if (NAMED_SCALES.chromatic !== undefined) {\n return [{ name: 'chromatic', rootPc }];\n }\n return [];\n }\n const preferHeptatonic = chordPcs.length <= 3;\n ranked.sort((a, b) => {\n const idiomaticA = idiomaticRank(data, a.name);\n const idiomaticB = idiomaticRank(data, b.name);\n if (idiomaticA !== idiomaticB) {\n return idiomaticA - idiomaticB;\n }\n if (preferHeptatonic && (a.size === HEPTATONIC_SIZE) !== (b.size === HEPTATONIC_SIZE)) {\n return a.size === HEPTATONIC_SIZE ? -1 : 1;\n }\n if (a.extra !== b.extra) {\n return a.extra - b.extra;\n }\n const rankA = conventionalRank(a.name);\n const rankB = conventionalRank(b.name);\n if (rankA !== rankB) {\n return rankA - rankB;\n }\n // Among scales the modal preference does not name — the symmetric and\n // melodic-minor scales — the one that clashes with the chord least is the\n // one a player reaches for. Alphabetical order answered with the rotation\n // that leaves nothing playable.\n if (a.avoid !== b.avoid) {\n return a.avoid - b.avoid;\n }\n return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;\n });\n return ranked.map((entry) => ({ name: entry.name, rootPc }));\n}\n\n/**\n * Which use an avoid note is judged for: sounding it, or passing through it.\n *\n * `'harmonic'` is the Berklee-style rule in full — a tone that must not be\n * sounded against the chord. `'melodic'` keeps only what a line cannot pass\n * through either, which is the semitone above the root; every other clash\n * resolves as the line moves on.\n *\n * @category Scales\n */\nexport type AvoidNoteUse = 'harmonic' | 'melodic';\n\n/**\n * Options for {@link avoidNotes}.\n *\n * @category Scales\n */\nexport type AvoidNotesOptions = {\n use?: AvoidNoteUse;\n};\n\n/**\n * List a scale's avoid notes over a chord.\n *\n * An avoid note is a non-chord scale tone that lies a semitone directly above a\n * chord tone, or the third a suspension displaced; sounding it against the\n * chord clashes. The scale is rooted on the chord root. If the scale does not\n * contain the chord, no avoid notes exist.\n *\n * The rule states which tones must not be *sounded* against the chord, which is\n * not the same question as which tones a line may not touch. Pass\n * `{ use: 'melodic' }` for the melodic reading: only the semitone above the\n * root survives it, so the result is always a subset of the harmonic one.\n *\n * @param chord The chord providing the chord tones, as a chord symbol, chord\n * data, or a `Chord`.\n * @param scaleName A key of {@link NAMED_SCALES}, or an alias of one. A world\n * scale is not one: chord-scale theory is a Western practice, so a raga named\n * here is rejected rather than answered with a number.\n * @param opts Set `use: 'melodic'` to judge a line rather than a voicing.\n * @returns The avoid-note pitch classes, sorted ascending in [0, 11].\n * @throws If `scaleName` is not a built-in scale. An empty result already\n * means \"this scale has no avoid notes over this chord\"; answering a typo\n * the same way would make the two indistinguishable.\n *\n * @example\n * ```ts\n * import { makeChord, avoidNotes } from '@libraz/libcantus';\n * avoidNotes(makeChord(0, 'maj7'), 'ionian'); // [5] — F clashes with the third\n * avoidNotes(makeChord(0, 'maj7'), 'ionian', { use: 'melodic' }); // [] — a line may pass through it\n * ```\n *\n * @category Scales\n */\nexport function avoidNotes(\n chord: ChordLike,\n scaleName: ScaleNameInput,\n opts: AvoidNotesOptions = {},\n): number[] {\n const asked = assertOptions(opts, 'opts');\n const mask = requireChordScaleMask(scaleName);\n const data = toChordData(chord);\n const rootPc = pitchClass(data.rootPc);\n const chordPcs = chordPitchClasses(data);\n const alteredDominant = isAlteredDominant(data, scaleName);\n if (!scaleMatchesChord(chordPcs, mask, rootPc) && !alteredDominant) {\n return [];\n }\n const chordSet = new Set(chordPcs);\n // Sounding the third a suspension stands in for is what undoes the\n // suspension, so it is an avoid note however consonant it is against the\n // scale. Both thirds are named: a suspension does not say which one it\n // displaced — that is what suspending is — so meeting either one ends it.\n const displaced = new Set(displacedThirds(data));\n const melodic = asked.use === 'melodic';\n const semitoneAboveRoot = pitchClass(rootPc + 1);\n const avoid: number[] = [];\n for (let pc = 0; pc < 12; pc += 1) {\n if (!maskHasPitchClass(mask, rootPc, pc) || chordSet.has(pc)) {\n continue;\n }\n if (melodic && pc !== semitoneAboveRoot) {\n continue;\n }\n if (!alteredDominant && (chordSet.has(pitchClass(pc - 1)) || displaced.has(pc))) {\n avoid.push(pc);\n }\n }\n return avoid.sort((a, b) => a - b);\n}\n\n/**\n * Semitones above the root of the colors a dominant takes when it resolves to a\n * minor tonic: the flat ninth, the sharp ninth, and the flat thirteenth.\n *\n * Over a dominant read on its own, each of them sits a semitone above a chord\n * tone and is an avoid note. Heard as the dominant of a minor key they are the\n * key's own tones, and standard practice, so the function the chord serves is\n * what decides.\n */\nconst MINOR_RESOLUTION_TENSIONS = [1, 3, 8] as const;\n\n/** Whether this chord is a dominant seventh resolving down a fifth to minor. */\nfunction resolvesToMinorTonic(\n chord: Chord,\n chordPcs: number[],\n target: Chord | undefined,\n): boolean {\n if (target === undefined) {\n return false;\n }\n const rootPc = pitchClass(chord.rootPc);\n const sounds = (semitones: number): boolean => chordPcs.includes(pitchClass(rootPc + semitones));\n if (!sounds(4) || !sounds(10)) {\n return false;\n }\n const targetRootPc = pitchClass(target.rootPc);\n if (targetRootPc !== pitchClass(rootPc + 5)) {\n return false;\n }\n return chordPitchClasses(target).includes(pitchClass(targetRootPc + 3));\n}\n\n/**\n * Options for {@link availableTensions}.\n *\n * @category Scales\n */\nexport type AvailableTensionsOptions = {\n /**\n * The chord this one resolves to, as a chord symbol, chord data, or a\n * `Chord`.\n */\n resolvesTo?: ChordLike;\n};\n\n/**\n * List a scale's available tensions over a chord.\n *\n * Available tensions are scale tones that are neither chord tones nor avoid\n * notes, i.e. the usable color tones (typically the 9/11/13 region). The scale\n * is rooted on the chord root. If the scale does not contain the chord, there\n * are no available tensions.\n *\n * A tension's availability also depends on what the chord is doing. Pass\n * `resolvesTo` to say which chord this one resolves to: a dominant seventh\n * resolving down a fifth to a minor tonic takes its flat ninth, sharp ninth and\n * flat thirteenth, which read on their own would each be an avoid note. Only\n * scale tones are ever added, so the result stays within the scale.\n *\n * @param chord The chord providing the chord tones, as a chord symbol, chord\n * data, or a `Chord`.\n * @param scaleName A key of {@link NAMED_SCALES}, or an alias of one; a world\n * scale is outside chord-scale theory, as in {@link avoidNotes}.\n * @param opts Set `resolvesTo` to the chord this one resolves to.\n * @returns The available-tension pitch classes, sorted ascending in [0, 11].\n * @throws If `scaleName` is not a built-in scale, as {@link avoidNotes} does.\n *\n * @example\n * ```ts\n * import { makeChord, availableTensions } from '@libraz/libcantus';\n * availableTensions(makeChord(0, 'maj7'), 'ionian'); // [2, 9] — color tones over Cmaj7\n * availableTensions(makeChord(7, 'dom7'), 'phrygianDominant', { resolvesTo: makeChord(0, 'min') });\n * // [3, 8] — the b13 and b9 the resolution to C minor makes available\n * ```\n *\n * @category Scales\n */\nexport function availableTensions(\n chord: ChordLike,\n scaleName: ScaleNameInput,\n opts: AvailableTensionsOptions = {},\n): number[] {\n const asked = assertOptions(opts, 'opts');\n const mask = requireChordScaleMask(scaleName);\n const data = toChordData(chord);\n const rootPc = pitchClass(data.rootPc);\n const chordPcs = chordPitchClasses(data);\n const alteredDominant = isAlteredDominant(data, scaleName);\n if (!scaleMatchesChord(chordPcs, mask, rootPc) && !alteredDominant) {\n return [];\n }\n const chordSet = new Set(chordPcs);\n const avoidSet = alteredDominant ? new Set<number>() : new Set(avoidNotes(data, scaleName));\n const functional = resolvesToMinorTonic(\n data,\n chordPcs,\n asked.resolvesTo === undefined ? undefined : toChordData(asked.resolvesTo),\n )\n ? new Set(MINOR_RESOLUTION_TENSIONS.map((semitones) => pitchClass(rootPc + semitones)))\n : undefined;\n const tensions: number[] = [];\n for (let pc = 0; pc < 12; pc += 1) {\n if (!maskHasPitchClass(mask, rootPc, pc) || chordSet.has(pc)) {\n continue;\n }\n if (avoidSet.has(pc) && functional?.has(pc) !== true) {\n continue;\n }\n tensions.push(pc);\n }\n return tensions.sort((a, b) => a - b);\n}\n\n/**\n * A scale fit over a chord, with its non-chord tones sorted by what may be done\n * with them.\n *\n * `avoid`, `passing` and `tensions` partition the scale tones the chord does\n * not state: `avoid` may not be played at all, `passing` may be passed through\n * melodically but not sounded against the chord, and `tensions` may be added\n * freely as color. `avoid` is {@link avoidNotes} under `use: 'melodic'`, and\n * `avoid` together with `passing` is the harmonic reading.\n *\n * @category Scales\n */\nexport type ChordScaleReportEntry = {\n name: string;\n rootPc: number;\n avoid: number[];\n passing: number[];\n tensions: number[];\n};\n\n/**\n * Report the best-fitting scales for a chord with their avoid notes and tensions.\n *\n * Combines {@link chordScales}, {@link avoidNotes}, and {@link availableTensions}\n * into a single ergonomic result, ordered best fit first. The report is read as\n * playing advice, so the tones a line may pass through are reported apart from\n * the ones it may not touch; see {@link ChordScaleReportEntry}.\n *\n * @param chord The chord to analyze, as a chord symbol, chord data, or a\n * `Chord`.\n * @param limit Optional maximum number of scales to report; all by default.\n * @returns One entry per reported scale, best fit first.\n *\n * @category Scales\n */\nexport function chordScaleReport(chord: ChordLike, limit?: number): ChordScaleReportEntry[] {\n if (limit !== undefined) {\n assertPositiveInt(limit, 'chord-scale report limit');\n }\n const data = toChordData(chord);\n const matches = chordScales(data);\n const chosen = limit === undefined ? matches : matches.slice(0, limit);\n return chosen.map((match) => {\n const harmonic = avoidNotes(data, match.name);\n const melodic = new Set(avoidNotes(data, match.name, { use: 'melodic' }));\n return {\n name: match.name,\n rootPc: match.rootPc,\n avoid: harmonic.filter((pc) => melodic.has(pc)),\n passing: harmonic.filter((pc) => !melodic.has(pc)),\n tensions: availableTensions(data, match.name),\n };\n });\n}\n\n/**\n * A chord paired with the scale chosen for it by {@link scalesForChanges}.\n *\n * @category Scales\n */\nexport type ScaleChoice = {\n chord: Chord;\n scale: ChordScaleMatch;\n};\n\n/**\n * Small per-candidate penalty added in {@link scalesForChanges} so that, among\n * choices of otherwise-equal transition cost, the tighter best-fit scale wins.\n */\nconst RANK_PENALTY = 0.01;\n\n/** Build the pitch-class set of a named scale rooted on `rootPc`. */\nfunction scalePitchClassSet(name: string, rootPc: number): Set<number> {\n const mask = namedScaleMask(name);\n const pcs = new Set<number>();\n if (mask === undefined) {\n return pcs;\n }\n for (let n = 0; n < 12; n += 1) {\n if (((mask >> n) & 1) === 1) {\n pcs.add(pitchClass(rootPc + n));\n }\n }\n return pcs;\n}\n\n/** Count pitch classes that belong to exactly one of two sets. */\nfunction symmetricDifferenceSize(a: Set<number>, b: Set<number>): number {\n let count = 0;\n for (const pc of a) {\n if (!b.has(pc)) {\n count += 1;\n }\n }\n for (const pc of b) {\n if (!a.has(pc)) {\n count += 1;\n }\n }\n return count;\n}\n\n/**\n * Choose one scale per chord across a progression, favoring continuity.\n *\n * Each chord's {@link chordScales} candidates form a stage in a Viterbi-style\n * dynamic program. The transition cost between adjacent choices is the number\n * of pitch classes that differ between their pitch-class sets (the symmetric\n * difference), plus a small penalty for straying from a chord's best-fit scale\n * so that ties break toward the tighter fit. The minimum-total-cost path is\n * returned, one {@link ScaleChoice} per input chord in the original order.\n *\n * Each choice pairs its scale with the chord exactly as it was handed in, so\n * this takes plain chord data rather than the wider forms the entry points\n * above accept.\n *\n * @param chords The chord sequence to choose scales for.\n * @returns One scale choice per chord, in input order.\n *\n * @category Scales\n */\nexport function scalesForChanges(chords: readonly Chord[]): ScaleChoice[] {\n assertArray(chords, 'chord-scale changes');\n assertGenerationBudget(chords.length, 'chord-scale changes');\n if (chords.length === 0) {\n return [];\n }\n const candidateLists = chords.map((chord) => chordScales(chord));\n const pcSets = candidateLists.map((candidates) =>\n candidates.map((match) => scalePitchClassSet(match.name, match.rootPc)),\n );\n\n const first = candidateLists[0] ?? [];\n const dp: number[][] = [first.map((_, j) => j * RANK_PENALTY)];\n const back: number[][] = [first.map(() => -1)];\n\n for (let i = 1; i < candidateLists.length; i += 1) {\n const candidates = candidateLists[i] ?? [];\n const prevCosts = dp[i - 1] ?? [];\n const prevSets = pcSets[i - 1] ?? [];\n const curSets = pcSets[i] ?? [];\n const stageCosts: number[] = [];\n const stageBack: number[] = [];\n for (let j = 0; j < candidates.length; j += 1) {\n let bestCost = Number.POSITIVE_INFINITY;\n let bestPrev = -1;\n for (let k = 0; k < prevCosts.length; k += 1) {\n const transition = symmetricDifferenceSize(\n prevSets[k] ?? new Set(),\n curSets[j] ?? new Set(),\n );\n const cost = (prevCosts[k] ?? 0) + transition;\n if (cost < bestCost) {\n bestCost = cost;\n bestPrev = k;\n }\n }\n stageCosts.push(bestCost + j * RANK_PENALTY);\n stageBack.push(bestPrev);\n }\n dp.push(stageCosts);\n back.push(stageBack);\n }\n\n const lastCosts = dp[dp.length - 1] ?? [];\n let bestFinal = 0;\n for (let j = 1; j < lastCosts.length; j += 1) {\n if ((lastCosts[j] ?? Number.POSITIVE_INFINITY) < (lastCosts[bestFinal] ?? 0)) {\n bestFinal = j;\n }\n }\n\n const chosenIndices: number[] = new Array(candidateLists.length);\n let current = bestFinal;\n for (let i = candidateLists.length - 1; i >= 0; i -= 1) {\n chosenIndices[i] = current;\n current = back[i]?.[current] ?? -1;\n }\n\n return chords.map((chord, i) => {\n const candidates = candidateLists[i] ?? [];\n const scale = candidates[chosenIndices[i] ?? 0] ?? { name: 'chromatic', rootPc: chord.rootPc };\n return { chord, scale };\n });\n}\n","import { pitchClassOf } from '../../core/pitch/index.js';\nimport { assertFiniteNumber } from '../../core/validation/index.js';\nimport { type ChordToneRole, chordToneRole, thirdSlotOf } from '../chord/index.js';\nimport { type ChordLike, toChordData } from '../symbol/index.js';\n\n/**\n * Harmonic role a pitch plays within a chord.\n *\n * @category Functional Harmony\n */\nexport type HarmonyRole = ChordToneRole | 'tension' | 'doubling';\n\n/**\n * How firmly a pitch is locked to the chord's identity.\n *\n * - `identity`: moving it produces a different chord (the root).\n * - `quality`: moving it flips the chord quality (the third).\n * - `voicing`: it can move freely without changing chord identity or quality.\n *\n * @category Functional Harmony\n */\nexport type LockLevel = 'identity' | 'quality' | 'voicing';\n\n/**\n * A pitch's role, lock level, and owning chord.\n *\n * @category Functional Harmony\n */\nexport type VoicedRole = {\n role: HarmonyRole;\n lock: LockLevel;\n belongsToChordId: number;\n};\n\n/**\n * Classify a pitch's harmonic role and lock level within a chord.\n *\n * The role comes from the pitch's interval class above the chord root, read\n * against the chord's own template: root (0), third (3/4), fifth (6/7/8),\n * sixth or seventh (9/10/11). Every pitch the template does not claim is\n * `'tension'` — including one that is simply foreign to the chord, which this\n * single-pitch query cannot tell from a colour tone. The root locks the chord\n * identity, the third locks its quality, and everything else is free voicing.\n * Where a chord has no third, the tone standing in its place — the fourth of a\n * `sus4`, the second of a `sus2`, the eleventh of an eleventh chord — takes the\n * third's slot and is locked to `quality`, since moving it changes the chord.\n * At most one interval class of a chord is ever reported as its third, so a\n * consumer reading `lock: 'quality'` as \"this tone identifies the chord\" gets\n * one answer rather than two. Detecting an octave doubling requires the\n * surrounding voicing, which this query does not carry, so `'doubling'` is part\n * of the type — the shared vocabulary of harmonic roles — but is never returned\n * here.\n *\n * @param pitch MIDI pitch or bare pitch class.\n * @param chord The chord providing the root reference, as a chord symbol, chord\n * data, or a `Chord`.\n * @param chordId Identifier stored on the result (defaults to 0).\n * @returns The pitch's role, lock level, and owning chord id.\n * @example\n * ```ts\n * import { roleOf, makeChord } from '@libraz/libcantus';\n * roleOf(64, makeChord(0, 'maj')); // E over C major\n * // { role: 'third', lock: 'quality', belongsToChordId: 0 }\n * ```\n * @category Functional Harmony\n */\nexport function roleOf(pitch: number, chord: ChordLike, chordId = 0): VoicedRole {\n // The id is carried into the result, so a value that is not one would be\n // reported back as the chord this role belongs to.\n assertFiniteNumber(chordId, 'chordId');\n const data = toChordData(chord);\n const interval = (pitchClassOf(pitch) - pitchClassOf(data.rootPc) + 12) % 12;\n // What stands in the third's slot is read where the chord is read, not here:\n // the tone that identifies a chord and the tone the avoid-note rule refuses\n // against it are two consequences of one fact about the chord.\n const slot = thirdSlotOf(data);\n const isSuspendedTone = slot.kind === 'suspension' && interval === slot.interval;\n const chordRole = chordToneRole(pitch, data);\n let role: HarmonyRole;\n let lock: LockLevel;\n if (isSuspendedTone) {\n role = 'third'; // suspended tone occupies the third's quality-defining slot\n lock = 'quality';\n } else if (chordRole !== null) {\n role = chordRole;\n lock = chordRole === 'root' ? 'identity' : chordRole === 'third' ? 'quality' : 'voicing';\n } else {\n role = 'tension';\n lock = 'voicing';\n }\n return { role, lock, belongsToChordId: chordId };\n}\n"]}
@@ -3,7 +3,7 @@ import {
3
3
  assertFiniteNumber,
4
4
  assertNoteEvents,
5
5
  assertOptions
6
- } from "./chunk-PQL7E5F6.js";
6
+ } from "./chunk-AGOVW7U6.js";
7
7
 
8
8
  // src/core/event-index/index.ts
9
9
  function upperBound(values, beat) {
@@ -142,4 +142,4 @@ export {
142
142
  sortedNoteEvents,
143
143
  createNoteEventIndex
144
144
  };
145
- //# sourceMappingURL=chunk-7GOKQJTQ.js.map
145
+ //# sourceMappingURL=chunk-PUKJP2Z5.js.map
@@ -8,7 +8,7 @@ import {
8
8
  isDiatonic,
9
9
  isMinorKey,
10
10
  parallelKey
11
- } from "./chunk-VLE5R7PF.js";
11
+ } from "./chunk-H2KJHBW4.js";
12
12
  import {
13
13
  LIMBS,
14
14
  canSound,
@@ -19,14 +19,14 @@ import {
19
19
  instrumentRange,
20
20
  resolveAlgorithmVersion,
21
21
  toStringedProfile
22
- } from "./chunk-X6RJAHII.js";
22
+ } from "./chunk-7KG3RTYS.js";
23
23
  import {
24
24
  createNoteEventIndex
25
- } from "./chunk-7GOKQJTQ.js";
25
+ } from "./chunk-PUKJP2Z5.js";
26
26
  import {
27
27
  chordScales,
28
28
  roleOf
29
- } from "./chunk-H66ARFU5.js";
29
+ } from "./chunk-NB25P3EH.js";
30
30
  import {
31
31
  chordPitchClasses,
32
32
  chordQualities,
@@ -58,7 +58,7 @@ import {
58
58
  toChordData,
59
59
  toKeyScale,
60
60
  tonicizableDegrees
61
- } from "./chunk-FPBZFNCD.js";
61
+ } from "./chunk-RFIBF2RR.js";
62
62
  import {
63
63
  ARTICULATIONS,
64
64
  BEAT_EPS,
@@ -95,7 +95,7 @@ import {
95
95
  toMeterData,
96
96
  toSpelledInterval,
97
97
  transposeNote
98
- } from "./chunk-PQL7E5F6.js";
98
+ } from "./chunk-AGOVW7U6.js";
99
99
 
100
100
  // src/generate/context/draw.ts
101
101
  var DRAW_METHODS = ["at", "prob", "range", "float"];
@@ -5618,4 +5618,4 @@ export {
5618
5618
  generateRhythm,
5619
5619
  rhythmDensity
5620
5620
  };
5621
- //# sourceMappingURL=chunk-R5VUXQMP.js.map
5621
+ //# sourceMappingURL=chunk-Q6LFSAKV.js.map
@@ -33,7 +33,7 @@ import {
33
33
  tryParseKeyName,
34
34
  tryParseNote,
35
35
  unwrapParse
36
- } from "./chunk-PQL7E5F6.js";
36
+ } from "./chunk-AGOVW7U6.js";
37
37
 
38
38
  // src/theory/scale/masks.ts
39
39
  function maskFromOffsets(offsets) {
@@ -2514,7 +2514,8 @@ function stepResolution(pitch, chord) {
2514
2514
  function evaluateSafety(q, opts = {}) {
2515
2515
  assertSafetyContext(q);
2516
2516
  assertMidiPitch(q.candidatePitch, "candidatePitch");
2517
- return evaluateInternal(q, assertOptions(opts, "opts").suggestions ?? true);
2517
+ const options = assertOptions(opts, "opts");
2518
+ return evaluateInternal(q, options.suggestions ?? true, options.partners ?? false);
2518
2519
  }
2519
2520
  function assertSafetyContext(q) {
2520
2521
  assertRecord(q, "safety query");
@@ -2538,7 +2539,7 @@ function assertSafetyContext(q) {
2538
2539
  }
2539
2540
  }
2540
2541
  }
2541
- function evaluateInternal(q, collectSuggestions) {
2542
+ function evaluateInternal(q, collectSuggestions, collectPartners) {
2542
2543
  let reasons = 0;
2543
2544
  let level = 0 /* Safe */;
2544
2545
  let resolveTo;
@@ -2609,41 +2610,58 @@ function evaluateInternal(q, collectSuggestions) {
2609
2610
  reasons |= 512 /* MajorSeventh */;
2610
2611
  }
2611
2612
  }
2613
+ let bass = pitch;
2614
+ let top = pitch;
2615
+ for (const ov of q.otherVoices) {
2616
+ bass = Math.min(bass, ov.pitch);
2617
+ top = Math.max(top, ov.pitch);
2618
+ }
2619
+ const pairReasons = collectPartners ? new Array(q.otherVoices.length).fill(0) : void 0;
2620
+ const flagPair = (index, flag) => {
2621
+ reasons |= flag;
2622
+ if (pairReasons !== void 0) {
2623
+ pairReasons[index] = (pairReasons[index] ?? 0) | flag;
2624
+ }
2625
+ };
2612
2626
  if (q.strongBeat) {
2613
- const twoVoice = q.otherVoices.length === 1;
2614
2627
  const prev = q.prevPitch;
2615
- for (const ov of q.otherVoices) {
2616
- if (!createsVerticalDissonance(pitch, ov.pitch, twoVoice)) {
2628
+ for (const [index, ov] of q.otherVoices.entries()) {
2629
+ const againstBass = pitch === bass || ov.pitch === bass;
2630
+ if (!createsVerticalDissonance(pitch, ov.pitch, againstBass)) {
2617
2631
  continue;
2618
2632
  }
2619
2633
  if (chord && isStructuralChordDissonance(pitch, ov.pitch, chord)) {
2620
2634
  continue;
2621
2635
  }
2622
- reasons |= 1024 /* VerticalDissonance */;
2636
+ if (chord && q.profile === "pop" && pitchClassOf(Math.abs(pitch - ov.pitch)) === 5 && isChordMember(pitch, chord) && isChordMember(ov.pitch, chord)) {
2637
+ continue;
2638
+ }
2639
+ flagPair(index, 1024 /* VerticalDissonance */);
2623
2640
  raise(2 /* Dissonant */);
2624
- if (prev !== void 0 && pitch === prev && ov.prevPitch !== void 0 && !createsVerticalDissonance(prev, ov.prevPitch, twoVoice)) {
2625
- reasons |= 16384 /* Suspension */;
2641
+ if (prev !== void 0 && pitch === prev && ov.prevPitch !== void 0 && !createsVerticalDissonance(prev, ov.prevPitch, againstBass)) {
2642
+ flagPair(index, 16384 /* Suspension */);
2626
2643
  }
2627
2644
  }
2628
2645
  }
2629
2646
  if (q.prevPitch !== void 0) {
2630
2647
  const prev = q.prevPitch;
2631
- for (const ov of q.otherVoices) {
2648
+ for (const [index, ov] of q.otherVoices.entries()) {
2632
2649
  if (ov.prevPitch === void 0) {
2633
2650
  continue;
2634
2651
  }
2635
2652
  if (createsParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {
2636
- reasons |= 2048 /* ParallelPerfect */;
2653
+ flagPair(index, 2048 /* ParallelPerfect */);
2637
2654
  raise(parallelSeverity);
2638
2655
  }
2639
- if (createsHiddenParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {
2640
- reasons |= 4096 /* HiddenParallel */;
2656
+ const outerPair = pitch === top && ov.pitch === bass || pitch === bass && ov.pitch === top;
2657
+ if (outerPair && createsHiddenParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {
2658
+ flagPair(index, 4096 /* HiddenParallel */);
2641
2659
  raise(parallelSeverity);
2642
2660
  }
2643
2661
  const crossedNow = pitch - ov.pitch;
2644
2662
  const crossedPrev = prev - ov.prevPitch;
2645
2663
  if (crossedNow !== 0 && crossedPrev !== 0 && crossedNow > 0 !== crossedPrev > 0) {
2646
- reasons |= 8192 /* VoiceCrossing */;
2664
+ flagPair(index, 8192 /* VoiceCrossing */);
2647
2665
  raise(parallelSeverity);
2648
2666
  }
2649
2667
  }
@@ -2659,6 +2677,17 @@ function evaluateInternal(q, collectSuggestions) {
2659
2677
  result.suggestions = suggestions;
2660
2678
  }
2661
2679
  }
2680
+ if (pairReasons !== void 0) {
2681
+ const partners = [];
2682
+ pairReasons.forEach((pair, index) => {
2683
+ if (pair !== 0) {
2684
+ partners.push({ index, reasons: pair });
2685
+ }
2686
+ });
2687
+ if (partners.length > 0) {
2688
+ result.partners = partners;
2689
+ }
2690
+ }
2662
2691
  return result;
2663
2692
  }
2664
2693
  function findSafeNearby(q, candidate) {
@@ -2668,7 +2697,7 @@ function findSafeNearby(q, candidate) {
2668
2697
  if (p < 0 || p > 127) {
2669
2698
  continue;
2670
2699
  }
2671
- if (evaluateInternal({ ...q, candidatePitch: p }, false).safety === 0 /* Safe */) {
2700
+ if (evaluateInternal({ ...q, candidatePitch: p }, false, false).safety === 0 /* Safe */) {
2672
2701
  out.push(p);
2673
2702
  if (out.length >= MAX_SUGGESTIONS) {
2674
2703
  return out;
@@ -2692,7 +2721,7 @@ var REASON_TEXT = [
2692
2721
  [2048 /* ParallelPerfect */, "Parallel perfect interval with another voice"],
2693
2722
  [4096 /* HiddenParallel */, "Hidden perfect interval by similar motion"],
2694
2723
  [8192 /* VoiceCrossing */, "Crosses another voice"],
2695
- [128 /* LargeLeap */, "Leap wider than the profile allows"],
2724
+ [128 /* LargeLeap */, "Forbidden melodic leap \u2014 a seventh or wider than an octave"],
2696
2725
  [32768 /* NeedsResolution */, "Dissonance awaiting a stepwise resolution"],
2697
2726
  [2 /* Tension */, "Chord tension"],
2698
2727
  [8 /* ScaleTone */, "Scale tone, not in the chord"],
@@ -2737,7 +2766,7 @@ function enumerateSafePitches(q, pitchLow, pitchHigh) {
2737
2766
  const low = Math.max(pitchLow, q.vocalLow ?? pitchLow);
2738
2767
  const high = Math.min(pitchHigh, q.vocalHigh ?? pitchHigh);
2739
2768
  for (let pitch = high; pitch >= low; pitch -= 1) {
2740
- const result = evaluateInternal({ ...q, candidatePitch: pitch }, false);
2769
+ const result = evaluateInternal({ ...q, candidatePitch: pitch }, false, false);
2741
2770
  if (result.safety === 2 /* Dissonant */) {
2742
2771
  continue;
2743
2772
  }
@@ -3794,4 +3823,4 @@ export {
3794
3823
  isDescendingStep,
3795
3824
  isFrustratedLeadingTone
3796
3825
  };
3797
- //# sourceMappingURL=chunk-FPBZFNCD.js.map
3826
+ //# sourceMappingURL=chunk-RFIBF2RR.js.map