@libraz/libcantus 0.9.4 → 0.9.5

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 (120) hide show
  1. package/README.md +27 -18
  2. package/README_ja.md +268 -0
  3. package/dist/analyze/index.cjs +7 -7
  4. package/dist/analyze/index.d.cts +9 -99
  5. package/dist/analyze/index.d.ts +9 -99
  6. package/dist/analyze/index.js +6 -6
  7. package/dist/{chunk-36N73KZ3.cjs → chunk-2A2QMA74.cjs} +80 -65
  8. package/dist/chunk-2A2QMA74.cjs.map +1 -0
  9. package/dist/{chunk-YLEMGEFH.js → chunk-2PTFGVD7.js} +50 -35
  10. package/dist/chunk-2PTFGVD7.js.map +1 -0
  11. package/dist/{chunk-ZEJR4TKL.cjs → chunk-357ZDPVU.cjs} +36 -47
  12. package/dist/chunk-357ZDPVU.cjs.map +1 -0
  13. package/dist/{chunk-EHVEBWM5.js → chunk-3KX76RLG.js} +136 -37
  14. package/dist/chunk-3KX76RLG.js.map +1 -0
  15. package/dist/{chunk-RC6GBMCD.cjs → chunk-4VZ5MZAM.cjs} +155 -111
  16. package/dist/chunk-4VZ5MZAM.cjs.map +1 -0
  17. package/dist/{chunk-RK35RIN7.js → chunk-7EONYVI5.js} +115 -28
  18. package/dist/chunk-7EONYVI5.js.map +1 -0
  19. package/dist/{chunk-GQSGS2N4.js → chunk-7JEP3MFY.js} +5 -16
  20. package/dist/chunk-7JEP3MFY.js.map +1 -0
  21. package/dist/{chunk-7NKDPF74.js → chunk-AID75RN3.js} +203 -104
  22. package/dist/chunk-AID75RN3.js.map +1 -0
  23. package/dist/{chunk-FNDWXRRI.cjs → chunk-BSLTLGM3.cjs} +55 -37
  24. package/dist/chunk-BSLTLGM3.cjs.map +1 -0
  25. package/dist/{chunk-NP4NA2FB.cjs → chunk-CU6VG2YM.cjs} +39 -36
  26. package/dist/chunk-CU6VG2YM.cjs.map +1 -0
  27. package/dist/{chunk-NYYWMV2C.js → chunk-DZMHIGI4.js} +27 -17
  28. package/dist/chunk-DZMHIGI4.js.map +1 -0
  29. package/dist/{chunk-I24MV65U.js → chunk-EDOTEABK.js} +18 -15
  30. package/dist/{chunk-I24MV65U.js.map → chunk-EDOTEABK.js.map} +1 -1
  31. package/dist/{chunk-NKIBQ6PT.js → chunk-HN2OEUN2.js} +7 -4
  32. package/dist/chunk-HN2OEUN2.js.map +1 -0
  33. package/dist/{chunk-D2FAJBYL.js → chunk-KBTTEX7Q.js} +30 -12
  34. package/dist/chunk-KBTTEX7Q.js.map +1 -0
  35. package/dist/{chunk-CAN76NCU.cjs → chunk-KQN2ZYND.cjs} +147 -102
  36. package/dist/chunk-KQN2ZYND.cjs.map +1 -0
  37. package/dist/{chunk-4HLLZPXT.cjs → chunk-MPTVJIVG.cjs} +350 -251
  38. package/dist/chunk-MPTVJIVG.cjs.map +1 -0
  39. package/dist/{chunk-RUMU3UA6.cjs → chunk-MTMZTMWM.cjs} +12 -12
  40. package/dist/{chunk-RUMU3UA6.cjs.map → chunk-MTMZTMWM.cjs.map} +1 -1
  41. package/dist/{chunk-KMHOICIY.js → chunk-NNB67XA2.js} +90 -45
  42. package/dist/chunk-NNB67XA2.js.map +1 -0
  43. package/dist/{chunk-LW53AHQU.cjs → chunk-OL3IHHSJ.cjs} +228 -141
  44. package/dist/chunk-OL3IHHSJ.cjs.map +1 -0
  45. package/dist/{chunk-7MGZ7D62.cjs → chunk-PI3UXRWK.cjs} +43 -13
  46. package/dist/chunk-PI3UXRWK.cjs.map +1 -0
  47. package/dist/{chunk-XEBG3G53.js → chunk-R2VVFAIN.js} +42 -12
  48. package/dist/chunk-R2VVFAIN.js.map +1 -0
  49. package/dist/{chunk-Q6OP4U43.cjs → chunk-WFDXOLUB.cjs} +50 -40
  50. package/dist/chunk-WFDXOLUB.cjs.map +1 -0
  51. package/dist/{chunk-NVE34YGZ.cjs → chunk-WTAN4ACG.cjs} +212 -113
  52. package/dist/chunk-WTAN4ACG.cjs.map +1 -0
  53. package/dist/{chunk-FCANXBAT.cjs → chunk-YFJXS3AU.cjs} +8 -5
  54. package/dist/chunk-YFJXS3AU.cjs.map +1 -0
  55. package/dist/{chunk-Q2XUP227.js → chunk-YUE6TJ4M.js} +89 -45
  56. package/dist/chunk-YUE6TJ4M.js.map +1 -0
  57. package/dist/{chunk-HKGYCQ34.js → chunk-ZGLJM4A4.js} +2 -2
  58. package/dist/core/index.cjs +15 -5
  59. package/dist/core/index.cjs.map +1 -1
  60. package/dist/core/index.d.cts +14 -4
  61. package/dist/core/index.d.ts +14 -4
  62. package/dist/core/index.js +14 -4
  63. package/dist/{function-DNugAOrt.d.cts → function-CxixTB02.d.cts} +98 -7
  64. package/dist/{function-BtiZb9i9.d.ts → function-DTCt675z.d.ts} +98 -7
  65. package/dist/generate/index.cjs +10 -10
  66. package/dist/generate/index.d.cts +21 -21
  67. package/dist/generate/index.d.ts +21 -21
  68. package/dist/generate/index.js +9 -9
  69. package/dist/{index-tJhkmQgE.d.cts → index-B2Ulxxpo.d.cts} +3 -2
  70. package/dist/{index-Be74mcon.d.cts → index-BkYJ0xMq.d.cts} +2 -2
  71. package/dist/{index-6VR8-kit.d.ts → index-BuaB-ypt.d.ts} +3 -2
  72. package/dist/{index-70p168VM.d.ts → index-CAN7v-kw.d.ts} +9 -2
  73. package/dist/{index-C83mZI1P.d.ts → index-COw1Ky9g.d.ts} +1 -1
  74. package/dist/{index-tSQh-xwY.d.ts → index-Cfx8xgH0.d.ts} +1 -1
  75. package/dist/{index-C5UBbQ4G.d.ts → index-DRtIhlJj.d.ts} +2 -2
  76. package/dist/{index-BtXT6UQ0.d.cts → index-alrRninU.d.cts} +9 -2
  77. package/dist/{index-y_ak9_Hi.d.cts → index-oTQbYYIs.d.cts} +1 -1
  78. package/dist/{index-OG65ocOr.d.ts → index-tR9jTn26.d.cts} +4 -1
  79. package/dist/{index-OG65ocOr.d.cts → index-tR9jTn26.d.ts} +4 -1
  80. package/dist/{index-D6b6na69.d.cts → index-x2vcsduE.d.cts} +1 -1
  81. package/dist/index.cjs +24 -14
  82. package/dist/index.cjs.map +1 -1
  83. package/dist/index.d.cts +11 -11
  84. package/dist/index.d.ts +11 -11
  85. package/dist/index.js +23 -13
  86. package/dist/model/index.cjs +7 -7
  87. package/dist/model/index.d.cts +48 -16
  88. package/dist/model/index.d.ts +48 -16
  89. package/dist/model/index.js +6 -6
  90. package/dist/{styled-DIRkk9Uz.d.ts → styled-Bl0ZK28h.d.ts} +40 -3
  91. package/dist/{styled-DJbkJQfj.d.cts → styled-Crv6SA4S.d.cts} +40 -3
  92. package/dist/theory/index.cjs +6 -6
  93. package/dist/theory/index.d.cts +9 -9
  94. package/dist/theory/index.d.ts +9 -9
  95. package/dist/theory/index.js +5 -5
  96. package/package.json +12 -9
  97. package/dist/chunk-36N73KZ3.cjs.map +0 -1
  98. package/dist/chunk-4HLLZPXT.cjs.map +0 -1
  99. package/dist/chunk-7MGZ7D62.cjs.map +0 -1
  100. package/dist/chunk-7NKDPF74.js.map +0 -1
  101. package/dist/chunk-CAN76NCU.cjs.map +0 -1
  102. package/dist/chunk-D2FAJBYL.js.map +0 -1
  103. package/dist/chunk-EHVEBWM5.js.map +0 -1
  104. package/dist/chunk-FCANXBAT.cjs.map +0 -1
  105. package/dist/chunk-FNDWXRRI.cjs.map +0 -1
  106. package/dist/chunk-GQSGS2N4.js.map +0 -1
  107. package/dist/chunk-KMHOICIY.js.map +0 -1
  108. package/dist/chunk-LW53AHQU.cjs.map +0 -1
  109. package/dist/chunk-NKIBQ6PT.js.map +0 -1
  110. package/dist/chunk-NP4NA2FB.cjs.map +0 -1
  111. package/dist/chunk-NVE34YGZ.cjs.map +0 -1
  112. package/dist/chunk-NYYWMV2C.js.map +0 -1
  113. package/dist/chunk-Q2XUP227.js.map +0 -1
  114. package/dist/chunk-Q6OP4U43.cjs.map +0 -1
  115. package/dist/chunk-RC6GBMCD.cjs.map +0 -1
  116. package/dist/chunk-RK35RIN7.js.map +0 -1
  117. package/dist/chunk-XEBG3G53.js.map +0 -1
  118. package/dist/chunk-YLEMGEFH.js.map +0 -1
  119. package/dist/chunk-ZEJR4TKL.cjs.map +0 -1
  120. /package/dist/{chunk-HKGYCQ34.js.map → chunk-ZGLJM4A4.js.map} +0 -0
@@ -6,13 +6,14 @@ import {
6
6
  isChordMember,
7
7
  isForbiddenMelodicLeap,
8
8
  isScaleTone
9
- } from "./chunk-D2FAJBYL.js";
9
+ } from "./chunk-KBTTEX7Q.js";
10
10
  import {
11
11
  InvalidInputError,
12
12
  assertFiniteNumber,
13
13
  assertGenerationBudget,
14
- assertInteger
15
- } from "./chunk-XEBG3G53.js";
14
+ assertInteger,
15
+ pitchClassOf
16
+ } from "./chunk-R2VVFAIN.js";
16
17
 
17
18
  // src/theory/safety/index.ts
18
19
  var NoteSafety = /* @__PURE__ */ ((NoteSafety2) => {
@@ -38,6 +39,7 @@ var ReasonFlag = /* @__PURE__ */ ((ReasonFlag2) => {
38
39
  ReasonFlag2[ReasonFlag2["VoiceCrossing"] = 8192] = "VoiceCrossing";
39
40
  ReasonFlag2[ReasonFlag2["Suspension"] = 16384] = "Suspension";
40
41
  ReasonFlag2[ReasonFlag2["NeedsResolution"] = 32768] = "NeedsResolution";
42
+ ReasonFlag2[ReasonFlag2["MelodicTritone"] = 65536] = "MelodicTritone";
41
43
  return ReasonFlag2;
42
44
  })(ReasonFlag || {});
43
45
  var SUGGESTION_WINDOW = 12;
@@ -49,6 +51,7 @@ var REASON_SEVERITY = [
49
51
  [16 /* NonScale */, 2 /* Dissonant */, 2 /* Dissonant */],
50
52
  [32 /* OutOfRange */, 1 /* Warning */, 1 /* Warning */],
51
53
  [64 /* Tritone */, 2 /* Dissonant */, 2 /* Dissonant */],
54
+ [65536 /* MelodicTritone */, 1 /* Warning */, 2 /* Dissonant */],
52
55
  [128 /* LargeLeap */, 1 /* Warning */, 2 /* Dissonant */],
53
56
  [1024 /* VerticalDissonance */, 2 /* Dissonant */, 2 /* Dissonant */],
54
57
  [2048 /* ParallelPerfect */, 1 /* Warning */, 2 /* Dissonant */],
@@ -74,8 +77,11 @@ function avoidsMajorSeventh(chord) {
74
77
  return chord.intervals.includes(4) && chord.intervals.includes(10);
75
78
  }
76
79
  function isStructuralChordDissonance(a, b, chord) {
77
- const cls = Math.abs(Math.trunc(a) - Math.trunc(b)) % 12;
78
- if (cls !== 6 && cls !== 10 && cls !== 11) {
80
+ const rawClass = pitchClassOf(Math.abs(a - b));
81
+ const aOffset = intervalAboveRoot(a, chord);
82
+ const bOffset = intervalAboveRoot(b, chord);
83
+ const rootSeventh = aOffset === 0 && (bOffset === 10 || bOffset === 11) || bOffset === 0 && (aOffset === 10 || aOffset === 11);
84
+ if (rawClass !== 6 && !rootSeventh) {
79
85
  return false;
80
86
  }
81
87
  return isChordMember(a, chord) && isChordMember(b, chord);
@@ -128,6 +134,10 @@ function evaluateInternal(q, collectSuggestions) {
128
134
  }
129
135
  const inScale = isScaleTone(pitch, q.key);
130
136
  const chord = q.chord;
137
+ if (!inScale && (chord === null || !isChordMember(pitch, chord))) {
138
+ reasons |= 16 /* NonScale */;
139
+ raise(2 /* Dissonant */);
140
+ }
131
141
  if (chord && isChordMember(pitch, chord)) {
132
142
  reasons |= 1 /* ChordTone */;
133
143
  } else if (chord) {
@@ -148,16 +158,10 @@ function evaluateInternal(q, collectSuggestions) {
148
158
  } else if (inScale) {
149
159
  reasons |= 8 /* ScaleTone */;
150
160
  raise(1 /* Warning */);
151
- } else {
152
- reasons |= 16 /* NonScale */;
153
- raise(2 /* Dissonant */);
154
161
  }
155
162
  } else if (inScale) {
156
163
  reasons |= 8 /* ScaleTone */;
157
164
  raise(1 /* Warning */);
158
- } else {
159
- reasons |= 16 /* NonScale */;
160
- raise(2 /* Dissonant */);
161
165
  }
162
166
  if (chord && !isChordMember(pitch, chord)) {
163
167
  const ic = intervalAboveRoot(pitch, chord);
@@ -169,8 +173,8 @@ function evaluateInternal(q, collectSuggestions) {
169
173
  if (q.prevPitch !== void 0) {
170
174
  const pc = Math.abs(pitch - q.prevPitch) % 12;
171
175
  if (pc === 6) {
172
- reasons |= 64 /* Tritone */;
173
- raise(2 /* Dissonant */);
176
+ reasons |= 65536 /* MelodicTritone */;
177
+ raise(q.profile === "strict" ? 2 /* Dissonant */ : 1 /* Warning */);
174
178
  }
175
179
  if (isForbiddenMelodicLeap(q.prevPitch, pitch)) {
176
180
  reasons |= 128 /* LargeLeap */;
@@ -239,6 +243,9 @@ function findSafeNearby(q, candidate) {
239
243
  const out = [];
240
244
  for (let d = 1; d <= SUGGESTION_WINDOW; d += 1) {
241
245
  for (const p of [candidate - d, candidate + d]) {
246
+ if (p < 0 || p > 127) {
247
+ continue;
248
+ }
242
249
  if (evaluateInternal({ ...q, candidatePitch: p }, false).safety === 0 /* Safe */) {
243
250
  out.push(p);
244
251
  if (out.length >= MAX_SUGGESTIONS) {
@@ -257,6 +264,7 @@ var REASON_TEXT = [
257
264
  ],
258
265
  [1024 /* VerticalDissonance */, "Dissonant against a sounding voice on a strong beat"],
259
266
  [64 /* Tritone */, "Tritone against the sounding harmony"],
267
+ [65536 /* MelodicTritone */, "Melodic tritone leap"],
260
268
  [4 /* AvoidNote */, "Avoid note \u2014 clashes with a chord tone a semitone away"],
261
269
  [16 /* NonScale */, "Outside the key"],
262
270
  [2048 /* ParallelPerfect */, "Parallel perfect interval with another voice"],
@@ -266,8 +274,8 @@ var REASON_TEXT = [
266
274
  [32768 /* NeedsResolution */, "Dissonance awaiting a stepwise resolution"],
267
275
  [2 /* Tension */, "Chord tension"],
268
276
  [8 /* ScaleTone */, "Scale tone, not in the chord"],
269
- [256 /* MinorSecond */, "A semitone from a sounding voice"],
270
- [512 /* MajorSeventh */, "A major seventh from a sounding voice"],
277
+ [256 /* MinorSecond */, "Melodic semitone"],
278
+ [512 /* MajorSeventh */, "Melodic major seventh"],
271
279
  [1 /* ChordTone */, "Chord tone"]
272
280
  ];
273
281
  function severityOfReason(flag, profile) {
@@ -304,7 +312,9 @@ function enumerateSafePitches(q, pitchLow, pitchHigh) {
304
312
  );
305
313
  }
306
314
  assertGenerationBudget(pitchHigh - pitchLow + 1, "safe pitch candidates");
307
- for (let pitch = pitchHigh; pitch >= pitchLow; pitch -= 1) {
315
+ const low = Math.max(pitchLow, q.vocalLow ?? pitchLow);
316
+ const high = Math.min(pitchHigh, q.vocalHigh ?? pitchHigh);
317
+ for (let pitch = high; pitch >= low; pitch -= 1) {
308
318
  const result = evaluateInternal({ ...q, candidatePitch: pitch }, false);
309
319
  if (result.safety === 2 /* Dissonant */) {
310
320
  continue;
@@ -324,4 +334,4 @@ export {
324
334
  evaluateSafety,
325
335
  enumerateSafePitches
326
336
  };
327
- //# sourceMappingURL=chunk-NYYWMV2C.js.map
337
+ //# sourceMappingURL=chunk-DZMHIGI4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/theory/safety/index.ts"],"sourcesContent":["import { InvalidInputError } from '../../core/errors/index.js';\nimport { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport {\n assertFiniteNumber,\n assertGenerationBudget,\n assertInteger,\n} from '../../core/validation/index.js';\nimport type { Chord } from '../chord/index.js';\nimport { intervalAboveRoot, isChordMember } from '../chord/index.js';\nimport {\n createsHiddenParallelPerfect,\n createsParallelPerfect,\n createsVerticalDissonance,\n isForbiddenMelodicLeap,\n} from '../counterpoint/index.js';\nimport { isScaleTone } from '../scale/index.js';\n\n/**\n * Severity policy: `strict` counterpoint vs. lenient `pop` voice-leading.\n *\n * @category Arrangement & Analysis\n */\nexport type SafetyProfile = 'strict' | 'pop';\n\n/**\n * Overall placeability verdict for a candidate pitch.\n *\n * @category Arrangement & Analysis\n */\nexport enum NoteSafety {\n Safe = 0,\n Warning = 1,\n Dissonant = 2,\n}\n\n/**\n * Bit flags describing why a pitch received its verdict.\n *\n * @category Arrangement & Analysis\n */\nexport enum ReasonFlag {\n ChordTone = 1 << 0,\n Tension = 1 << 1,\n AvoidNote = 1 << 2,\n ScaleTone = 1 << 3,\n NonScale = 1 << 4,\n OutOfRange = 1 << 5,\n Tritone = 1 << 6,\n LargeLeap = 1 << 7,\n MinorSecond = 1 << 8,\n MajorSeventh = 1 << 9,\n VerticalDissonance = 1 << 10,\n ParallelPerfect = 1 << 11,\n HiddenParallel = 1 << 12,\n VoiceCrossing = 1 << 13,\n Suspension = 1 << 14,\n NeedsResolution = 1 << 15,\n MelodicTritone = 1 << 16,\n}\n\n/**\n * A neighbouring voice's current and previous pitch.\n *\n * @category Arrangement & Analysis\n */\nexport type VoiceSnapshot = {\n pitch: number;\n prevPitch?: number;\n};\n\n/**\n * Everything needed to evaluate one candidate pitch.\n *\n * @category Arrangement & Analysis\n */\nexport type SafetyQuery = {\n profile: SafetyProfile;\n candidatePitch: number;\n prevPitch?: number;\n chord: Chord | null;\n key: KeyScale;\n otherVoices: VoiceSnapshot[];\n strongBeat: boolean;\n vocalLow?: number;\n vocalHigh?: number;\n};\n\n/**\n * The verdict, reason flags, and optional resolution guidance.\n *\n * @category Arrangement & Analysis\n */\nexport type SafetyResult = {\n safety: NoteSafety;\n reasons: number;\n resolveTo?: number;\n suggestions?: number[];\n rationale?: string;\n};\n\n/** Semitone radius searched around a rejected candidate for safe alternatives. */\nconst SUGGESTION_WINDOW = 12;\n/** Maximum number of alternative pitches returned in `SafetyResult.suggestions`. */\nconst MAX_SUGGESTIONS = 3;\n\n/** Canonical minimum verdict contributed by each non-informational reason. */\nconst REASON_SEVERITY: ReadonlyArray<\n readonly [flag: ReasonFlag, pop: NoteSafety, strict: NoteSafety]\n> = [\n [ReasonFlag.Tension, NoteSafety.Warning, NoteSafety.Warning],\n [ReasonFlag.AvoidNote, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.ScaleTone, NoteSafety.Warning, NoteSafety.Warning],\n [ReasonFlag.NonScale, NoteSafety.Dissonant, NoteSafety.Dissonant],\n [ReasonFlag.OutOfRange, NoteSafety.Warning, NoteSafety.Warning],\n [ReasonFlag.Tritone, NoteSafety.Dissonant, NoteSafety.Dissonant],\n [ReasonFlag.MelodicTritone, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.LargeLeap, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.VerticalDissonance, NoteSafety.Dissonant, NoteSafety.Dissonant],\n [ReasonFlag.ParallelPerfect, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.HiddenParallel, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.VoiceCrossing, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.Suspension, NoteSafety.Dissonant, NoteSafety.Dissonant],\n [ReasonFlag.NeedsResolution, NoteSafety.Warning, NoteSafety.Dissonant],\n];\n\nfunction minimumSafetyForReasons(reasons: number, profile: SafetyProfile): NoteSafety {\n let minimum = NoteSafety.Safe;\n const severityIndex = profile === 'strict' ? 2 : 1;\n for (const entry of REASON_SEVERITY) {\n if (reasons & entry[0]) {\n minimum = Math.max(minimum, entry[severityIndex]) as NoteSafety;\n }\n }\n return minimum;\n}\n\n/** Whether the perfect fourth is an avoid note over this chord. */\nfunction avoidsFourth(chord: Chord): boolean {\n return chord.intervals.includes(4) && !chord.intervals.includes(5);\n}\n\n/** Whether the major seventh is an avoid note over this chord (dominant). */\nfunction avoidsMajorSeventh(chord: Chord): boolean {\n return chord.intervals.includes(4) && chord.intervals.includes(10);\n}\n\n/**\n * Whether a dissonance between two chord tones is one the chord is built from.\n *\n * A seventh chord states a seventh against its root and a dominant states the\n * tritone between its third and seventh; those intervals are the chord's\n * identity, not a clash to be prepared and resolved. A second between two chord\n * tones (a ninth voiced against the root, say) is a spacing problem rather than\n * a property of the harmony, so it stays reportable.\n */\nfunction isStructuralChordDissonance(a: number, b: number, chord: Chord): boolean {\n const rawClass = pitchClass(Math.abs(a - b));\n const aOffset = intervalAboveRoot(a, chord);\n const bOffset = intervalAboveRoot(b, chord);\n const rootSeventh =\n (aOffset === 0 && (bOffset === 10 || bOffset === 11)) ||\n (bOffset === 0 && (aOffset === 10 || aOffset === 11));\n if (rawClass !== 6 && !rootSeventh) {\n return false;\n }\n return isChordMember(a, chord) && isChordMember(b, chord);\n}\n\n/** Nearest chord tone at or below the candidate, within two semitones. */\nfunction stepResolution(pitch: number, chord: Chord): number | undefined {\n for (let delta = 1; delta <= 2; delta += 1) {\n if (isChordMember(pitch - delta, chord)) {\n return pitch - delta;\n }\n }\n return undefined;\n}\n\n/**\n * Evaluate the safety of placing a candidate pitch in a voice.\n *\n * Reasons are merged from harmonic context (chord tone / tension / avoid / scale)\n * and counterpoint (vertical dissonance, parallels, voice crossing). The `strict`\n * profile treats avoid notes and parallels as dissonant; `pop` treats them as\n * warnings. The final verdict is the worst severity among the reasons.\n *\n * @param q The candidate and its harmonic/voice-leading context.\n * @returns The verdict, reason bitmask, and optional resolution guidance.\n * @example\n * ```ts\n * import { evaluateSafety, makeChord, majorKey, NoteSafety } from '@libraz/libcantus';\n * const result = evaluateSafety({\n * profile: 'pop',\n * candidatePitch: 60, // C over a C major chord\n * chord: makeChord(0, 'maj'),\n * key: majorKey(0),\n * otherVoices: [],\n * strongBeat: true,\n * });\n * result.safety === NoteSafety.Safe; // true — C is a chord tone\n * ```\n * @category Arrangement & Analysis\n */\nexport function evaluateSafety(q: SafetyQuery, opts: EvaluateSafetyOptions = {}): SafetyResult {\n assertSafetyContext(q);\n assertFiniteNumber(q.candidatePitch, 'candidatePitch');\n return evaluateInternal(q, opts.suggestions ?? true);\n}\n\n/**\n * Options for {@link evaluateSafety}.\n *\n * @category Arrangement & Analysis\n */\nexport type EvaluateSafetyOptions = {\n /**\n * Whether to search for safe replacement pitches when the verdict is not\n * safe. Turning it off skips up to two dozen extra evaluations per rejected\n * pitch, which is worth doing when only the verdict is wanted.\n *\n * @defaultValue true\n */\n suggestions?: boolean;\n};\n\n/** Validate all numeric context fields once at a public safety entry point. */\nfunction assertSafetyContext(q: Omit<SafetyQuery, 'candidatePitch'>): void {\n if (q.prevPitch !== undefined) assertFiniteNumber(q.prevPitch, 'prevPitch');\n if (q.vocalLow !== undefined) assertFiniteNumber(q.vocalLow, 'vocalLow');\n if (q.vocalHigh !== undefined) assertFiniteNumber(q.vocalHigh, 'vocalHigh');\n if (q.vocalLow !== undefined && q.vocalHigh !== undefined && q.vocalLow > q.vocalHigh) {\n throw new InvalidInputError('vocalLow must not exceed vocalHigh');\n }\n assertGenerationBudget(q.otherVoices.length, 'other voices');\n for (let index = 0; index < q.otherVoices.length; index += 1) {\n const voice = q.otherVoices[index];\n if (!voice) continue;\n assertFiniteNumber(voice.pitch, `otherVoices[${index}].pitch`);\n if (voice.prevPitch !== undefined) {\n assertFiniteNumber(voice.prevPitch, `otherVoices[${index}].prevPitch`);\n }\n }\n}\n\n/**\n * Core safety evaluation.\n *\n * @param q The candidate and its harmonic/voice-leading context.\n * @param collectSuggestions Whether to search for safe alternatives on a\n * non-Safe verdict. Set false during that search itself to bound recursion.\n * @returns The verdict, reason bitmask, and optional guidance.\n */\nfunction evaluateInternal(q: SafetyQuery, collectSuggestions: boolean): SafetyResult {\n let reasons = 0;\n let level = NoteSafety.Safe;\n let resolveTo: number | undefined;\n const raise = (l: NoteSafety) => {\n if (l > level) {\n level = l;\n }\n };\n const avoidSeverity = q.profile === 'strict' ? NoteSafety.Dissonant : NoteSafety.Warning;\n const parallelSeverity = avoidSeverity;\n const pitch = q.candidatePitch;\n\n if (\n (q.vocalLow !== undefined && pitch < q.vocalLow) ||\n (q.vocalHigh !== undefined && pitch > q.vocalHigh)\n ) {\n reasons |= ReasonFlag.OutOfRange;\n raise(NoteSafety.Warning);\n }\n\n const inScale = isScaleTone(pitch, q.key);\n const chord = q.chord;\n\n // A chromatic avoid note remains chromatic. The avoid-note branch below\n // refines the resolution guidance, but must not erase the key relationship\n // or downgrade a non-scale pitch to a merely stylistic warning.\n if (!inScale && (chord === null || !isChordMember(pitch, chord))) {\n reasons |= ReasonFlag.NonScale;\n raise(NoteSafety.Dissonant);\n }\n\n if (chord && isChordMember(pitch, chord)) {\n reasons |= ReasonFlag.ChordTone;\n } else if (chord) {\n const ic = intervalAboveRoot(pitch, chord);\n const isTension = ic === 2 || ic === 5 || ic === 9;\n const avoid = (ic === 5 && avoidsFourth(chord)) || (ic === 11 && avoidsMajorSeventh(chord));\n if (avoid) {\n reasons |= ReasonFlag.AvoidNote;\n if (isTension) {\n reasons |= ReasonFlag.Tension;\n }\n reasons |= ReasonFlag.NeedsResolution;\n resolveTo = stepResolution(pitch, chord);\n raise(avoidSeverity);\n } else if (isTension && inScale) {\n reasons |= ReasonFlag.Tension;\n raise(NoteSafety.Warning);\n } else if (inScale) {\n reasons |= ReasonFlag.ScaleTone;\n raise(NoteSafety.Warning);\n }\n } else if (inScale) {\n reasons |= ReasonFlag.ScaleTone;\n raise(NoteSafety.Warning);\n }\n\n // Harmonic tritone against a chord tone. A chord tone is never flagged for a\n // tritone its own chord spells: the interval is the chord's identity, not a\n // clash with it. This covers dominant sevenths (third against seventh) and\n // equally the diminished, half-diminished, diminished-seventh and minor-sixth\n // families, where the tritone falls between two chord tones as well.\n if (chord && !isChordMember(pitch, chord)) {\n const ic = intervalAboveRoot(pitch, chord);\n const toneOffsets = chord.intervals.map((i) => ((i % 12) + 12) % 12);\n if (toneOffsets.some((t) => Math.abs(ic - t) === 6)) {\n reasons |= ReasonFlag.Tritone;\n }\n }\n\n if (q.prevPitch !== undefined) {\n // Quality flags reduce the melodic interval to its pitch-class interval so\n // compound intervals are treated uniformly with their simple form. The\n // forbidden-leap flag defers to the counterpoint rule, which preserves the\n // octave boundary (an octave leap is allowed, wider leaps are not).\n const pc = Math.abs(pitch - q.prevPitch) % 12;\n if (pc === 6) {\n reasons |= ReasonFlag.MelodicTritone;\n raise(q.profile === 'strict' ? NoteSafety.Dissonant : NoteSafety.Warning);\n }\n if (isForbiddenMelodicLeap(q.prevPitch, pitch)) {\n reasons |= ReasonFlag.LargeLeap;\n raise(parallelSeverity);\n }\n if (pc === 1) {\n reasons |= ReasonFlag.MinorSecond;\n }\n if (pc === 11) {\n reasons |= ReasonFlag.MajorSeventh;\n }\n }\n\n if (q.strongBeat) {\n const twoVoice = q.otherVoices.length === 1;\n const prev = q.prevPitch;\n for (const ov of q.otherVoices) {\n if (!createsVerticalDissonance(pitch, ov.pitch, twoVoice)) {\n continue;\n }\n if (chord && isStructuralChordDissonance(pitch, ov.pitch, chord)) {\n continue;\n }\n reasons |= ReasonFlag.VerticalDissonance;\n raise(NoteSafety.Dissonant);\n // A held pitch that was consonant on the previous step and is now\n // dissonant against the same voice is a prepared suspension.\n if (\n prev !== undefined &&\n pitch === prev &&\n ov.prevPitch !== undefined &&\n !createsVerticalDissonance(prev, ov.prevPitch, twoVoice)\n ) {\n reasons |= ReasonFlag.Suspension;\n }\n }\n }\n\n if (q.prevPitch !== undefined) {\n const prev = q.prevPitch;\n for (const ov of q.otherVoices) {\n if (ov.prevPitch === undefined) {\n continue;\n }\n // createsParallelPerfect covers every perfect class (unison/octave and\n // fifth), so no separate octave check is needed.\n if (createsParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {\n reasons |= ReasonFlag.ParallelPerfect;\n raise(parallelSeverity);\n }\n if (createsHiddenParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {\n reasons |= ReasonFlag.HiddenParallel;\n raise(parallelSeverity);\n }\n const crossedNow = pitch - ov.pitch;\n const crossedPrev = prev - ov.prevPitch;\n if (crossedNow !== 0 && crossedPrev !== 0 && crossedNow > 0 !== crossedPrev > 0) {\n reasons |= ReasonFlag.VoiceCrossing;\n raise(parallelSeverity);\n }\n }\n }\n\n // Keep reason production and verdict policy as one invariant even when a new\n // call path skips an earlier local `raise`: informational flags (ChordTone,\n // MinorSecond, MajorSeventh) may coexist with Safe; every policy flag above\n // contributes at least its table severity.\n raise(minimumSafetyForReasons(reasons, q.profile));\n\n const result: SafetyResult = { safety: level, reasons, rationale: describe(reasons, q) };\n if (resolveTo !== undefined) {\n result.resolveTo = resolveTo;\n }\n if (collectSuggestions && level !== NoteSafety.Safe) {\n const suggestions = findSafeNearby(q, pitch);\n if (suggestions.length > 0) {\n result.suggestions = suggestions;\n }\n }\n return result;\n}\n\n/**\n * Find safe alternative pitches near a rejected candidate.\n *\n * Searches outward from the candidate within `SUGGESTION_WINDOW` semitones,\n * probing the pitch below before the one above at each distance so the result\n * is deterministic and ordered by nearness.\n *\n * @param q The safety context of the rejected candidate.\n * @param candidate The candidate pitch that was not Safe.\n * @returns Up to `MAX_SUGGESTIONS` safe pitches, nearest first.\n */\nfunction findSafeNearby(q: SafetyQuery, candidate: number): number[] {\n const out: number[] = [];\n for (let d = 1; d <= SUGGESTION_WINDOW; d += 1) {\n for (const p of [candidate - d, candidate + d]) {\n if (p < 0 || p > 127) {\n continue;\n }\n if (evaluateInternal({ ...q, candidatePitch: p }, false).safety === NoteSafety.Safe) {\n out.push(p);\n if (out.length >= MAX_SUGGESTIONS) {\n return out;\n }\n }\n }\n }\n return out;\n}\n\n/**\n * Wording for every reason flag, most to least specific.\n *\n * The list is exhaustive by construction: {@link describe} picks from it by\n * severity, so a flag with no wording here would let a rejected pitch be\n * explained by a milder reason — the seventh of a dominant reading as a plain\n * \"Chord tone\" while its verdict says dissonant.\n */\nconst REASON_TEXT: ReadonlyArray<readonly [ReasonFlag, string]> = [\n [ReasonFlag.OutOfRange, 'Outside the target vocal range'],\n [\n ReasonFlag.Suspension,\n 'Prepared suspension — a held consonance now dissonant, awaiting resolution',\n ],\n [ReasonFlag.VerticalDissonance, 'Dissonant against a sounding voice on a strong beat'],\n [ReasonFlag.Tritone, 'Tritone against the sounding harmony'],\n [ReasonFlag.MelodicTritone, 'Melodic tritone leap'],\n [ReasonFlag.AvoidNote, 'Avoid note — clashes with a chord tone a semitone away'],\n [ReasonFlag.NonScale, 'Outside the key'],\n [ReasonFlag.ParallelPerfect, 'Parallel perfect interval with another voice'],\n [ReasonFlag.HiddenParallel, 'Hidden perfect interval by similar motion'],\n [ReasonFlag.VoiceCrossing, 'Crosses another voice'],\n [ReasonFlag.LargeLeap, 'Leap wider than the profile allows'],\n [ReasonFlag.NeedsResolution, 'Dissonance awaiting a stepwise resolution'],\n [ReasonFlag.Tension, 'Chord tension'],\n [ReasonFlag.ScaleTone, 'Scale tone, not in the chord'],\n [ReasonFlag.MinorSecond, 'Melodic semitone'],\n [ReasonFlag.MajorSeventh, 'Melodic major seventh'],\n [ReasonFlag.ChordTone, 'Chord tone'],\n];\n\n/** The severity a single flag carries under a profile; Safe when purely informational. */\nfunction severityOfReason(flag: ReasonFlag, profile: SafetyProfile): NoteSafety {\n const entry = REASON_SEVERITY.find(([candidate]) => candidate === flag);\n return entry === undefined ? NoteSafety.Safe : entry[profile === 'strict' ? 2 : 1];\n}\n\n/**\n * Build a short human explanation from the reason flags.\n *\n * The flag driving the verdict wins: the one whose severity under this profile\n * is highest, ties going to the more specific wording. Explaining a rejected\n * pitch by an informational flag it also carries would tell the caller nothing\n * about what to fix.\n */\nfunction describe(reasons: number, q: SafetyQuery): string {\n let best: string | undefined;\n let bestSeverity = -1;\n for (const [flag, text] of REASON_TEXT) {\n if (!(reasons & flag)) {\n continue;\n }\n const severity = severityOfReason(flag, q.profile);\n if (severity > bestSeverity) {\n bestSeverity = severity;\n best = text;\n }\n }\n if (best !== undefined) {\n return best;\n }\n return q.chord ? 'Placeable' : 'No chord context';\n}\n\n/**\n * Enumerate placeable pitches in a range, chord tones first, descending.\n *\n * @param q The safety context minus the candidate pitch.\n * @param pitchLow Lowest MIDI pitch to consider (inclusive).\n * @param pitchHigh Highest MIDI pitch to consider (inclusive).\n * @returns Placeable pitches (non-dissonant), chord tones before others, each group descending.\n * @throws If either bound is non-finite/non-integral, reversed, or exceeds the generation budget.\n * @example\n * ```ts\n * import { enumerateSafePitches, makeChord, majorKey } from '@libraz/libcantus';\n * const pitches = enumerateSafePitches(\n * { profile: 'pop', chord: makeChord(0, 'maj'), key: majorKey(0), otherVoices: [], strongBeat: true },\n * 60,\n * 72,\n * );\n * pitches; // placeable pitches in [60, 72], chord tones first, each group descending\n * ```\n * @category Arrangement & Analysis\n */\nexport function enumerateSafePitches(\n q: Omit<SafetyQuery, 'candidatePitch'>,\n pitchLow: number,\n pitchHigh: number,\n): number[] {\n assertSafetyContext(q);\n const chordTones: number[] = [];\n const others: number[] = [];\n assertInteger(pitchLow, 'pitchLow');\n assertInteger(pitchHigh, 'pitchHigh');\n if (pitchLow > pitchHigh) {\n throw new InvalidInputError(\n `pitchLow must not exceed pitchHigh; received ${pitchLow} > ${pitchHigh}`,\n );\n }\n assertGenerationBudget(pitchHigh - pitchLow + 1, 'safe pitch candidates');\n const low = Math.max(pitchLow, q.vocalLow ?? pitchLow);\n const high = Math.min(pitchHigh, q.vocalHigh ?? pitchHigh);\n for (let pitch = high; pitch >= low; pitch -= 1) {\n const result = evaluateInternal({ ...q, candidatePitch: pitch }, false);\n if (result.safety === NoteSafety.Dissonant) {\n continue;\n }\n if (result.reasons & ReasonFlag.ChordTone) {\n chordTones.push(pitch);\n } else {\n others.push(pitch);\n }\n }\n return [...chordTones, ...others];\n}\n"],"mappings":"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","names":["NoteSafety","ReasonFlag"]}
@@ -4,7 +4,7 @@ import {
4
4
  assertInteger,
5
5
  assertPositiveInt,
6
6
  assertRange
7
- } from "./chunk-XEBG3G53.js";
7
+ } from "./chunk-R2VVFAIN.js";
8
8
 
9
9
  // src/core/tuning/index.ts
10
10
  var TWELVE_TET = Object.freeze({ refStep: 69, refFreq: 440, divisions: 12 });
@@ -64,20 +64,23 @@ function ratioToCents(numerator, denominator) {
64
64
  assertRange(denominator, Number.MIN_VALUE, Number.MAX_VALUE, "ratio denominator");
65
65
  return 1200 * (Math.log2(numerator) - Math.log2(denominator));
66
66
  }
67
+ function justRatio(numerator, denominator) {
68
+ return Object.freeze([numerator, denominator]);
69
+ }
67
70
  var JUST_RATIOS = Object.freeze({
68
- 0: [1, 1],
69
- 1: [16, 15],
70
- 2: [9, 8],
71
- 3: [6, 5],
72
- 4: [5, 4],
73
- 5: [4, 3],
74
- 6: [45, 32],
75
- 7: [3, 2],
76
- 8: [8, 5],
77
- 9: [5, 3],
78
- 10: [9, 5],
79
- 11: [15, 8],
80
- 12: [2, 1]
71
+ 0: justRatio(1, 1),
72
+ 1: justRatio(16, 15),
73
+ 2: justRatio(9, 8),
74
+ 3: justRatio(6, 5),
75
+ 4: justRatio(5, 4),
76
+ 5: justRatio(4, 3),
77
+ 6: justRatio(45, 32),
78
+ 7: justRatio(3, 2),
79
+ 8: justRatio(8, 5),
80
+ 9: justRatio(5, 3),
81
+ 10: justRatio(9, 5),
82
+ 11: justRatio(15, 8),
83
+ 12: justRatio(2, 1)
81
84
  });
82
85
  function justDeviationCents(semitoneClass) {
83
86
  assertInteger(semitoneClass, "semitone class", 0, 12);
@@ -103,4 +106,4 @@ export {
103
106
  JUST_RATIOS,
104
107
  justDeviationCents
105
108
  };
106
- //# sourceMappingURL=chunk-I24MV65U.js.map
109
+ //# sourceMappingURL=chunk-EDOTEABK.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core/tuning/index.ts"],"sourcesContent":["/**\n * Tuning and microtonality: frequency conversion, cents, arbitrary equal\n * temperaments (EDO), and just-intonation ratios.\n *\n * The rest of the library reasons in twelve-tone pitch classes, which is a\n * deliberate scope choice for tonal theory. This module is the escape hatch for\n * anyone who needs actual frequencies, cents, non-12 equal temperaments, or the\n * acoustic (just) tuning behind the tempered intervals.\n */\n\nimport { InvalidInputError } from '../errors/index.js';\nimport {\n assertFiniteNumber,\n assertInteger,\n assertPositiveInt,\n assertRange,\n} from '../validation/index.js';\n\n/**\n * An equal-tempered tuning: a reference pitch and a number of equal divisions of\n * the octave. With `divisions === 12` a step index is an ordinary MIDI number.\n *\n * @category Pitch & Intervals\n */\nexport type Tuning = {\n /** Step index (MIDI number when `divisions` is 12) whose frequency is `refFreq`. */\n readonly refStep: number;\n /** Frequency in Hz of `refStep`. */\n readonly refFreq: number;\n /** Equal divisions of the octave (12 standard; e.g. 19 or 31 for microtonal). */\n readonly divisions: number;\n};\n\n/**\n * Standard twelve-tone equal temperament, A4 (MIDI 69) = 440 Hz.\n *\n * @category Pitch & Intervals\n */\nexport const TWELVE_TET: Tuning = Object.freeze({ refStep: 69, refFreq: 440, divisions: 12 });\n\nfunction assertTuning(tuning: Tuning): Tuning {\n assertFiniteNumber(tuning.refStep, 'tuning.refStep');\n assertRange(tuning.refFreq, Number.MIN_VALUE, Number.MAX_VALUE, 'tuning.refFreq');\n assertPositiveInt(tuning.divisions, 'tuning.divisions');\n return tuning;\n}\n\n/**\n * Build an equal temperament with `n` divisions of the octave.\n *\n * @param n Divisions of the octave (e.g. 19, 24, 31).\n * @param refFreq Reference frequency in Hz (default 440).\n * @param refStep Step index of the reference (default 69).\n * @returns The tuning.\n * @example\n * ```ts\n * import { edo, frequencyOf } from '@libraz/libcantus';\n * const et19 = edo(19); // { refStep: 69, refFreq: 440, divisions: 19 }\n * frequencyOf(70, et19); // one 19-EDO step above A4\n * ```\n * @category Pitch & Intervals\n */\nexport function edo(n: number, refFreq = 440, refStep = 69): Tuning {\n assertPositiveInt(n, 'EDO divisions');\n assertRange(refFreq, Number.MIN_VALUE, Number.MAX_VALUE, 'reference frequency');\n assertFiniteNumber(refStep, 'reference step');\n return { refStep, refFreq, divisions: n };\n}\n\n/**\n * Frequency in Hz of a step index under a tuning.\n *\n * @param step Step index (a MIDI number under 12-EDO).\n * @param tuning The tuning (default 12-TET).\n * @returns The frequency in Hz.\n * @example\n * ```ts\n * import { frequencyOf } from '@libraz/libcantus';\n * frequencyOf(69); // 440 (A4 in 12-TET)\n * frequencyOf(60); // middle C in Hz\n * ```\n * @category Pitch & Intervals\n */\nexport function frequencyOf(step: number, tuning: Tuning = TWELVE_TET): number {\n assertFiniteNumber(step, 'step');\n assertTuning(tuning);\n const result = tuning.refFreq * 2 ** ((step - tuning.refStep) / tuning.divisions);\n return assertRange(result, Number.MIN_VALUE, Number.MAX_VALUE, 'frequency result');\n}\n\n/**\n * Nearest step index to a frequency under a tuning (the inverse of\n * {@link frequencyOf}, rounded).\n *\n * @param freq Frequency in Hz.\n * @param tuning The tuning (default 12-TET).\n * @returns The nearest step index.\n * @category Pitch & Intervals\n */\nexport function nearestStep(freq: number, tuning: Tuning = TWELVE_TET): number {\n assertRange(freq, Number.MIN_VALUE, Number.MAX_VALUE, 'frequency');\n assertTuning(tuning);\n return Math.round(tuning.refStep + tuning.divisions * Math.log2(freq / tuning.refFreq));\n}\n\n/**\n * Step index of a frequency under a tuning, unrounded.\n *\n * The exact inverse of {@link frequencyOf}. {@link nearestStep} rounds it,\n * which discards how far off the pitch actually is — the figure a tuner\n * display, a pitch-bend amount, or an intonation check is built on.\n *\n * @param freq Frequency in Hz.\n * @param tuning The tuning (default 12-TET).\n * @returns The fractional step index.\n * @example\n * ```ts\n * import { stepOf } from '@libraz/libcantus';\n * stepOf(442); // slightly above 69, the step for A440\n * ```\n * @category Pitch & Intervals\n */\nexport function stepOf(freq: number, tuning: Tuning = TWELVE_TET): number {\n assertRange(freq, Number.MIN_VALUE, Number.MAX_VALUE, 'frequency');\n assertTuning(tuning);\n return tuning.refStep + tuning.divisions * Math.log2(freq / tuning.refFreq);\n}\n\n/**\n * How far a frequency sits from its nearest step, in cents.\n *\n * Positive means sharp of the step, negative flat.\n *\n * @param freq Frequency in Hz.\n * @param tuning The tuning (default 12-TET).\n * @returns The signed deviation in cents.\n * @example\n * ```ts\n * import { centsFromNearestStep } from '@libraz/libcantus';\n * Math.round(centsFromNearestStep(442)); // 8\n * ```\n * @category Pitch & Intervals\n */\nexport function centsFromNearestStep(freq: number, tuning: Tuning = TWELVE_TET): number {\n const exact = stepOf(freq, tuning);\n return centsOfSteps(exact - Math.round(exact), tuning);\n}\n\n/**\n * Steps spanned by an interval in cents, unrounded — the inverse of\n * {@link centsOfSteps}.\n *\n * @param cents The interval in cents.\n * @param tuning The tuning (default 12-TET).\n * @returns The fractional step count.\n * @category Pitch & Intervals\n */\nexport function stepsOfCents(cents: number, tuning: Tuning = TWELVE_TET): number {\n assertFiniteNumber(cents, 'cents');\n assertTuning(tuning);\n return assertFiniteNumber((cents * tuning.divisions) / 1200, 'steps result');\n}\n\n/**\n * The frequency ratio an interval in cents spans — the inverse of\n * {@link ratioToCents}.\n *\n * Multiply a frequency by this to move it by that many cents, which is how a\n * cents offset (a pitch-bend spec, a tuning table entry) is applied.\n *\n * @param cents The interval in cents.\n * @returns The frequency ratio.\n * @example\n * ```ts\n * import { centsToRatio } from '@libraz/libcantus';\n * 440 * centsToRatio(14); // A440 raised by 14 cents\n * ```\n * @category Pitch & Intervals\n */\nexport function centsToRatio(cents: number): number {\n assertFiniteNumber(cents, 'cents');\n return 2 ** (cents / 1200);\n}\n\n/**\n * Interval in cents between two frequencies.\n *\n * @param a Lower/first frequency in Hz.\n * @param b Upper/second frequency in Hz.\n * @returns Cents from `a` to `b` (negative if `b` is lower).\n * @category Pitch & Intervals\n */\nexport function centsBetweenFreq(a: number, b: number): number {\n assertRange(a, Number.MIN_VALUE, Number.MAX_VALUE, 'first frequency');\n assertRange(b, Number.MIN_VALUE, Number.MAX_VALUE, 'second frequency');\n return 1200 * (Math.log2(b) - Math.log2(a));\n}\n\n/**\n * Cents spanned by a number of equal-temperament steps.\n *\n * @param steps Number of steps.\n * @param tuning The tuning (default 12-TET).\n * @returns The cents.\n * @category Pitch & Intervals\n */\nexport function centsOfSteps(steps: number, tuning: Tuning = TWELVE_TET): number {\n assertFiniteNumber(steps, 'steps');\n assertTuning(tuning);\n return assertFiniteNumber((steps * 1200) / tuning.divisions, 'cents result');\n}\n\n/**\n * Cents of a frequency ratio, e.g. `ratioToCents(3, 2)` ≈ 701.955 for the just\n * perfect fifth.\n *\n * @param numerator Ratio numerator.\n * @param denominator Ratio denominator.\n * @returns The interval in cents.\n * @category Pitch & Intervals\n */\nexport function ratioToCents(numerator: number, denominator: number): number {\n assertRange(numerator, Number.MIN_VALUE, Number.MAX_VALUE, 'ratio numerator');\n assertRange(denominator, Number.MIN_VALUE, Number.MAX_VALUE, 'ratio denominator');\n return 1200 * (Math.log2(numerator) - Math.log2(denominator));\n}\n\n/**\n * Five-limit just-intonation ratios for the twelve interval classes above a\n * unison, indexed by semitone class (0..12).\n *\n * @category Pitch & Intervals\n */\nexport const JUST_RATIOS: Readonly<Record<number, readonly [number, number]>> = Object.freeze({\n 0: [1, 1],\n 1: [16, 15],\n 2: [9, 8],\n 3: [6, 5],\n 4: [5, 4],\n 5: [4, 3],\n 6: [45, 32],\n 7: [3, 2],\n 8: [8, 5],\n 9: [5, 3],\n 10: [9, 5],\n 11: [15, 8],\n 12: [2, 1],\n});\n\n/**\n * Cents by which a five-limit just interval departs from its 12-TET tempering\n * (positive means the just interval is wider).\n *\n * @param semitoneClass Semitone class in [0, 12].\n * @returns The deviation in cents.\n * @throws If `semitoneClass` is not an integer in [0, 12].\n * @category Pitch & Intervals\n */\nexport function justDeviationCents(semitoneClass: number): number {\n assertInteger(semitoneClass, 'semitone class', 0, 12);\n const ratio = JUST_RATIOS[semitoneClass];\n if (!ratio) {\n throw new InvalidInputError(`semitone class has no just ratio: ${semitoneClass}`);\n }\n return ratioToCents(ratio[0], ratio[1]) - semitoneClass * 100;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/core/tuning/index.ts"],"sourcesContent":["/**\n * Tuning and microtonality: frequency conversion, cents, arbitrary equal\n * temperaments (EDO), and just-intonation ratios.\n *\n * The rest of the library reasons in twelve-tone pitch classes, which is a\n * deliberate scope choice for tonal theory. This module is the escape hatch for\n * anyone who needs actual frequencies, cents, non-12 equal temperaments, or the\n * acoustic (just) tuning behind the tempered intervals.\n */\n\nimport { InvalidInputError } from '../errors/index.js';\nimport {\n assertFiniteNumber,\n assertInteger,\n assertPositiveInt,\n assertRange,\n} from '../validation/index.js';\n\n/**\n * An equal-tempered tuning: a reference pitch and a number of equal divisions of\n * the octave. With `divisions === 12` a step index is an ordinary MIDI number.\n *\n * @category Pitch & Intervals\n */\nexport type Tuning = {\n /** Step index (MIDI number when `divisions` is 12) whose frequency is `refFreq`. */\n readonly refStep: number;\n /** Frequency in Hz of `refStep`. */\n readonly refFreq: number;\n /** Equal divisions of the octave (12 standard; e.g. 19 or 31 for microtonal). */\n readonly divisions: number;\n};\n\n/**\n * Standard twelve-tone equal temperament, A4 (MIDI 69) = 440 Hz.\n *\n * @category Pitch & Intervals\n */\nexport const TWELVE_TET: Tuning = Object.freeze({ refStep: 69, refFreq: 440, divisions: 12 });\n\nfunction assertTuning(tuning: Tuning): Tuning {\n assertFiniteNumber(tuning.refStep, 'tuning.refStep');\n assertRange(tuning.refFreq, Number.MIN_VALUE, Number.MAX_VALUE, 'tuning.refFreq');\n assertPositiveInt(tuning.divisions, 'tuning.divisions');\n return tuning;\n}\n\n/**\n * Build an equal temperament with `n` divisions of the octave.\n *\n * @param n Divisions of the octave (e.g. 19, 24, 31).\n * @param refFreq Reference frequency in Hz (default 440).\n * @param refStep Step index of the reference (default 69).\n * @returns The tuning.\n * @example\n * ```ts\n * import { edo, frequencyOf } from '@libraz/libcantus';\n * const et19 = edo(19); // { refStep: 69, refFreq: 440, divisions: 19 }\n * frequencyOf(70, et19); // one 19-EDO step above A4\n * ```\n * @category Pitch & Intervals\n */\nexport function edo(n: number, refFreq = 440, refStep = 69): Tuning {\n assertPositiveInt(n, 'EDO divisions');\n assertRange(refFreq, Number.MIN_VALUE, Number.MAX_VALUE, 'reference frequency');\n assertFiniteNumber(refStep, 'reference step');\n return { refStep, refFreq, divisions: n };\n}\n\n/**\n * Frequency in Hz of a step index under a tuning.\n *\n * @param step Step index (a MIDI number under 12-EDO).\n * @param tuning The tuning (default 12-TET).\n * @returns The frequency in Hz.\n * @example\n * ```ts\n * import { frequencyOf } from '@libraz/libcantus';\n * frequencyOf(69); // 440 (A4 in 12-TET)\n * frequencyOf(60); // middle C in Hz\n * ```\n * @category Pitch & Intervals\n */\nexport function frequencyOf(step: number, tuning: Tuning = TWELVE_TET): number {\n assertFiniteNumber(step, 'step');\n assertTuning(tuning);\n const result = tuning.refFreq * 2 ** ((step - tuning.refStep) / tuning.divisions);\n return assertRange(result, Number.MIN_VALUE, Number.MAX_VALUE, 'frequency result');\n}\n\n/**\n * Nearest step index to a frequency under a tuning (the inverse of\n * {@link frequencyOf}, rounded).\n *\n * @param freq Frequency in Hz.\n * @param tuning The tuning (default 12-TET).\n * @returns The nearest step index.\n * @category Pitch & Intervals\n */\nexport function nearestStep(freq: number, tuning: Tuning = TWELVE_TET): number {\n assertRange(freq, Number.MIN_VALUE, Number.MAX_VALUE, 'frequency');\n assertTuning(tuning);\n return Math.round(tuning.refStep + tuning.divisions * Math.log2(freq / tuning.refFreq));\n}\n\n/**\n * Step index of a frequency under a tuning, unrounded.\n *\n * The exact inverse of {@link frequencyOf}. {@link nearestStep} rounds it,\n * which discards how far off the pitch actually is — the figure a tuner\n * display, a pitch-bend amount, or an intonation check is built on.\n *\n * @param freq Frequency in Hz.\n * @param tuning The tuning (default 12-TET).\n * @returns The fractional step index.\n * @example\n * ```ts\n * import { stepOf } from '@libraz/libcantus';\n * stepOf(442); // slightly above 69, the step for A440\n * ```\n * @category Pitch & Intervals\n */\nexport function stepOf(freq: number, tuning: Tuning = TWELVE_TET): number {\n assertRange(freq, Number.MIN_VALUE, Number.MAX_VALUE, 'frequency');\n assertTuning(tuning);\n return tuning.refStep + tuning.divisions * Math.log2(freq / tuning.refFreq);\n}\n\n/**\n * How far a frequency sits from its nearest step, in cents.\n *\n * Positive means sharp of the step, negative flat.\n *\n * @param freq Frequency in Hz.\n * @param tuning The tuning (default 12-TET).\n * @returns The signed deviation in cents.\n * @example\n * ```ts\n * import { centsFromNearestStep } from '@libraz/libcantus';\n * Math.round(centsFromNearestStep(442)); // 8\n * ```\n * @category Pitch & Intervals\n */\nexport function centsFromNearestStep(freq: number, tuning: Tuning = TWELVE_TET): number {\n const exact = stepOf(freq, tuning);\n return centsOfSteps(exact - Math.round(exact), tuning);\n}\n\n/**\n * Steps spanned by an interval in cents, unrounded — the inverse of\n * {@link centsOfSteps}.\n *\n * @param cents The interval in cents.\n * @param tuning The tuning (default 12-TET).\n * @returns The fractional step count.\n * @category Pitch & Intervals\n */\nexport function stepsOfCents(cents: number, tuning: Tuning = TWELVE_TET): number {\n assertFiniteNumber(cents, 'cents');\n assertTuning(tuning);\n return assertFiniteNumber((cents * tuning.divisions) / 1200, 'steps result');\n}\n\n/**\n * The frequency ratio an interval in cents spans — the inverse of\n * {@link ratioToCents}.\n *\n * Multiply a frequency by this to move it by that many cents, which is how a\n * cents offset (a pitch-bend spec, a tuning table entry) is applied.\n *\n * @param cents The interval in cents.\n * @returns The frequency ratio.\n * @example\n * ```ts\n * import { centsToRatio } from '@libraz/libcantus';\n * 440 * centsToRatio(14); // A440 raised by 14 cents\n * ```\n * @category Pitch & Intervals\n */\nexport function centsToRatio(cents: number): number {\n assertFiniteNumber(cents, 'cents');\n return 2 ** (cents / 1200);\n}\n\n/**\n * Interval in cents between two frequencies.\n *\n * @param a Lower/first frequency in Hz.\n * @param b Upper/second frequency in Hz.\n * @returns Cents from `a` to `b` (negative if `b` is lower).\n * @category Pitch & Intervals\n */\nexport function centsBetweenFreq(a: number, b: number): number {\n assertRange(a, Number.MIN_VALUE, Number.MAX_VALUE, 'first frequency');\n assertRange(b, Number.MIN_VALUE, Number.MAX_VALUE, 'second frequency');\n return 1200 * (Math.log2(b) - Math.log2(a));\n}\n\n/**\n * Cents spanned by a number of equal-temperament steps.\n *\n * @param steps Number of steps.\n * @param tuning The tuning (default 12-TET).\n * @returns The cents.\n * @category Pitch & Intervals\n */\nexport function centsOfSteps(steps: number, tuning: Tuning = TWELVE_TET): number {\n assertFiniteNumber(steps, 'steps');\n assertTuning(tuning);\n return assertFiniteNumber((steps * 1200) / tuning.divisions, 'cents result');\n}\n\n/**\n * Cents of a frequency ratio, e.g. `ratioToCents(3, 2)` ≈ 701.955 for the just\n * perfect fifth.\n *\n * @param numerator Ratio numerator.\n * @param denominator Ratio denominator.\n * @returns The interval in cents.\n * @category Pitch & Intervals\n */\nexport function ratioToCents(numerator: number, denominator: number): number {\n assertRange(numerator, Number.MIN_VALUE, Number.MAX_VALUE, 'ratio numerator');\n assertRange(denominator, Number.MIN_VALUE, Number.MAX_VALUE, 'ratio denominator');\n return 1200 * (Math.log2(numerator) - Math.log2(denominator));\n}\n\n/**\n * Five-limit just-intonation ratios for the twelve interval classes above a\n * unison, indexed by semitone class (0..12).\n *\n * @category Pitch & Intervals\n */\nfunction justRatio(numerator: number, denominator: number): readonly [number, number] {\n return Object.freeze([numerator, denominator] as [number, number]);\n}\n\n/**\n * Five-limit just-intonation ratios for the twelve interval classes above a\n * unison, indexed by semitone class (0..12).\n *\n * @category Pitch & Intervals\n */\nexport const JUST_RATIOS: Readonly<Record<number, readonly [number, number]>> = Object.freeze({\n 0: justRatio(1, 1),\n 1: justRatio(16, 15),\n 2: justRatio(9, 8),\n 3: justRatio(6, 5),\n 4: justRatio(5, 4),\n 5: justRatio(4, 3),\n 6: justRatio(45, 32),\n 7: justRatio(3, 2),\n 8: justRatio(8, 5),\n 9: justRatio(5, 3),\n 10: justRatio(9, 5),\n 11: justRatio(15, 8),\n 12: justRatio(2, 1),\n});\n\n/**\n * Cents by which a five-limit just interval departs from its 12-TET tempering\n * (positive means the just interval is wider).\n *\n * @param semitoneClass Semitone class in [0, 12].\n * @returns The deviation in cents.\n * @throws If `semitoneClass` is not an integer in [0, 12].\n * @category Pitch & Intervals\n */\nexport function justDeviationCents(semitoneClass: number): number {\n assertInteger(semitoneClass, 'semitone class', 0, 12);\n const ratio = JUST_RATIOS[semitoneClass];\n if (!ratio) {\n throw new InvalidInputError(`semitone class has no just ratio: ${semitoneClass}`);\n }\n return ratioToCents(ratio[0], ratio[1]) - semitoneClass * 100;\n}\n"],"mappings":"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","names":[]}
@@ -1,11 +1,14 @@
1
1
  import {
2
2
  chordToneRole
3
- } from "./chunk-D2FAJBYL.js";
3
+ } from "./chunk-KBTTEX7Q.js";
4
+ import {
5
+ pitchClassOf
6
+ } from "./chunk-R2VVFAIN.js";
4
7
 
5
8
  // src/theory/harmony/index.ts
6
9
  function roleOf(pitch, chord, chordId = 0) {
7
- const interval = ((Math.trunc(pitch) - chord.rootPc) % 12 + 12) % 12;
8
- const tones = new Set(chord.intervals.map((i) => (i % 12 + 12) % 12));
10
+ const interval = (pitchClassOf(pitch) - pitchClassOf(chord.rootPc) + 12) % 12;
11
+ const tones = new Set(chord.intervals.map(pitchClassOf));
9
12
  const hasThird = tones.has(3) || tones.has(4);
10
13
  const isSuspendedTone = !hasThird && (interval === 5 && tones.has(5) || interval === 2 && tones.has(2));
11
14
  const chordRole = chordToneRole(pitch, chord);
@@ -27,4 +30,4 @@ function roleOf(pitch, chord, chordId = 0) {
27
30
  export {
28
31
  roleOf
29
32
  };
30
- //# sourceMappingURL=chunk-NKIBQ6PT.js.map
33
+ //# sourceMappingURL=chunk-HN2OEUN2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/theory/harmony/index.ts"],"sourcesContent":["import { pitchClassOf } from '../../core/pitch/index.js';\nimport { type Chord, type ChordToneRole, chordToneRole } from '../chord/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 * In a suspended chord (a chord whose intervals include a 4th (5) or 2nd (2)\n * but no third), the suspended tone takes the third's place as the\n * quality-defining tone and is locked to `quality`, since moving it changes the\n * chord. Detecting an octave doubling requires the surrounding voicing, which\n * this query does not carry, so `'doubling'` is part of the type — the shared\n * vocabulary of harmonic roles — but is never returned here.\n *\n * @param pitch MIDI pitch or bare pitch class.\n * @param chord The chord providing the root reference.\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: Chord, chordId = 0): VoicedRole {\n const interval = (pitchClassOf(pitch) - pitchClassOf(chord.rootPc) + 12) % 12;\n const tones = new Set(chord.intervals.map(pitchClassOf));\n const hasThird = tones.has(3) || tones.has(4);\n const isSuspendedTone =\n !hasThird && ((interval === 5 && tones.has(5)) || (interval === 2 && tones.has(2)));\n const chordRole = chordToneRole(pitch, chord);\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"],"mappings":";;;;;;;;AA4DO,SAAS,OAAO,OAAe,OAAc,UAAU,GAAe;AAC3E,QAAM,YAAY,aAAa,KAAK,IAAI,aAAa,MAAM,MAAM,IAAI,MAAM;AAC3E,QAAM,QAAQ,IAAI,IAAI,MAAM,UAAU,IAAI,YAAY,CAAC;AACvD,QAAM,WAAW,MAAM,IAAI,CAAC,KAAK,MAAM,IAAI,CAAC;AAC5C,QAAM,kBACJ,CAAC,aAAc,aAAa,KAAK,MAAM,IAAI,CAAC,KAAO,aAAa,KAAK,MAAM,IAAI,CAAC;AAClF,QAAM,YAAY,cAAc,OAAO,KAAK;AAC5C,MAAI;AACJ,MAAI;AACJ,MAAI,iBAAiB;AACnB,WAAO;AACP,WAAO;AAAA,EACT,WAAW,cAAc,MAAM;AAC7B,WAAO;AACP,WAAO,cAAc,SAAS,aAAa,cAAc,UAAU,YAAY;AAAA,EACjF,OAAO;AACL,WAAO;AACP,WAAO;AAAA,EACT;AACA,SAAO,EAAE,MAAM,MAAM,kBAAkB,QAAQ;AACjD;","names":[]}
@@ -2,9 +2,10 @@ import {
2
2
  InvalidInputError,
3
3
  assertFiniteNumber,
4
4
  assertInteger,
5
+ clampToMidi,
5
6
  isConsonantInterval,
6
7
  pitchClassOf
7
- } from "./chunk-XEBG3G53.js";
8
+ } from "./chunk-R2VVFAIN.js";
8
9
 
9
10
  // src/theory/scale/degrees.ts
10
11
  function scaleOffset(pitch, key) {
@@ -14,14 +15,14 @@ function isScaleTone(pitch, key) {
14
15
  return (key.modeMask12 >> scaleOffset(pitch, key) & 1) === 1;
15
16
  }
16
17
  function nearestScaleTone(pitch, key) {
17
- const base = Math.round(pitch);
18
+ const base = clampToMidi(Math.round(pitch), "pitch");
18
19
  for (let distance = 0; distance < 12; distance += 1) {
19
20
  const lower = base - distance;
20
- if (isScaleTone(lower, key)) {
21
+ if (lower >= 0 && isScaleTone(lower, key)) {
21
22
  return lower;
22
23
  }
23
24
  const higher = base + distance;
24
- if (isScaleTone(higher, key)) {
25
+ if (higher <= 127 && isScaleTone(higher, key)) {
25
26
  return higher;
26
27
  }
27
28
  }
@@ -64,7 +65,8 @@ function scaleTonesInDegreeOrder(key) {
64
65
  // src/theory/scale/masks.ts
65
66
  function maskFromOffsets(offsets) {
66
67
  let mask = 1;
67
- for (const offset of offsets) {
68
+ for (const [index, offset] of offsets.entries()) {
69
+ assertInteger(offset, `offsets[${index}]`);
68
70
  mask |= 1 << (offset % 12 + 12) % 12;
69
71
  }
70
72
  return mask;
@@ -128,13 +130,13 @@ function requireScaleMask(name, label = "scale") {
128
130
 
129
131
  // src/theory/scale/key.ts
130
132
  function majorKey(rootPc) {
131
- return { rootPc: (rootPc % 12 + 12) % 12, modeMask12: MAJOR_MASK };
133
+ return { rootPc: pitchClassOf(rootPc), modeMask12: MAJOR_MASK };
132
134
  }
133
135
  function minorKey(rootPc) {
134
- return { rootPc: (rootPc % 12 + 12) % 12, modeMask12: NATURAL_MINOR_MASK };
136
+ return { rootPc: pitchClassOf(rootPc), modeMask12: NATURAL_MINOR_MASK };
135
137
  }
136
138
  function scaleByName(name, rootPc) {
137
- return { rootPc: (rootPc % 12 + 12) % 12, modeMask12: requireScaleMask(name) };
139
+ return { rootPc: pitchClassOf(rootPc), modeMask12: requireScaleMask(name) };
138
140
  }
139
141
 
140
142
  // src/theory/chord/index.ts
@@ -150,6 +152,9 @@ var QUALITY_INTERVALS = {
150
152
  dim7: [0, 3, 6, 9],
151
153
  m7b5: [0, 3, 6, 10],
152
154
  minMaj7: [0, 3, 7, 11],
155
+ minMaj9: [0, 3, 7, 11, 14],
156
+ minMaj11: [0, 3, 7, 11, 14, 17],
157
+ minMaj13: [0, 3, 7, 11, 14, 21],
153
158
  aug7: [0, 4, 8, 10],
154
159
  augMaj7: [0, 4, 8, 11],
155
160
  "6": [0, 4, 7, 9],
@@ -254,7 +259,7 @@ function chordToneRole(pitch, chord) {
254
259
  return "root";
255
260
  }
256
261
  if (interval === 3 || interval === 4) {
257
- return tones.has(interval) ? "third" : null;
262
+ return tones.has(interval) && (interval !== 3 || !tones.has(4)) ? "third" : null;
258
263
  }
259
264
  if (interval === 7) {
260
265
  return tones.has(7) ? "fifth" : null;
@@ -268,7 +273,7 @@ function chordToneRole(pitch, chord) {
268
273
  if (interval === 9) {
269
274
  const hasHigherSeventh = tones.has(10) || tones.has(11);
270
275
  const isDiminishedSeventh = tones.has(3) && tones.has(6) && !hasHigherSeventh;
271
- if (isDiminishedSeventh) {
276
+ if (tones.has(9) && isDiminishedSeventh) {
272
277
  return "seventh";
273
278
  }
274
279
  if (tones.has(9) && !hasHigherSeventh) {
@@ -315,12 +320,20 @@ function classifySeventh(thirdIc, fifthIc, seventhIc) {
315
320
  if (fifthIc === 8 && seventhIc === 11) {
316
321
  return "augMaj7";
317
322
  }
323
+ if (fifthIc === 6 && seventhIc === 10) {
324
+ return "7b5";
325
+ }
318
326
  if (seventhIc === 11) {
319
327
  return "maj7";
320
328
  }
321
329
  return "dom7";
322
330
  }
331
+ function matchesQualityIntervals(quality, intervals) {
332
+ const template = QUALITY_INTERVALS[quality];
333
+ return template.length === intervals.length && template.every((interval, index) => interval === intervals[index]);
334
+ }
323
335
  function stackThirds(degree, key, size) {
336
+ assertInteger(degree, "chord degree", -1e3, 1e3);
324
337
  const tones = scaleTonesInDegreeOrder(key);
325
338
  const length = tones.length;
326
339
  if (length !== 7) {
@@ -337,6 +350,11 @@ function stackThirds(degree, key, size) {
337
350
  const thirdIc = offsets[1] ?? 4;
338
351
  const fifthIc = offsets[2] ?? 7;
339
352
  const quality = size === 3 ? classifyTriad(thirdIc, fifthIc) : classifySeventh(thirdIc, fifthIc, offsets[3] ?? 11);
353
+ if (!matchesQualityIntervals(quality, offsets)) {
354
+ throw new InvalidInputError(
355
+ `scale thirds at degree ${degree} produce unsupported intervals [${offsets.join(", ")}]`
356
+ );
357
+ }
340
358
  return { rootPc, quality, intervals: offsets };
341
359
  }
342
360
  function diatonicTriad(degree, key) {
@@ -358,7 +376,7 @@ function isForbiddenMelodicLeap(prev, cur) {
358
376
  return semis === 6 || semis === 10 || semis === 11 || semis > 12;
359
377
  }
360
378
  function simpleClass(semitones) {
361
- return Math.abs(semitones) % 12;
379
+ return pitchClassOf(Math.abs(semitones));
362
380
  }
363
381
  function isPerfectClass(cls) {
364
382
  return cls === 0 || cls === 7;
@@ -476,4 +494,4 @@ export {
476
494
  exceedsSpacing,
477
495
  isLeadingToneResolution
478
496
  };
479
- //# sourceMappingURL=chunk-D2FAJBYL.js.map
497
+ //# sourceMappingURL=chunk-KBTTEX7Q.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/theory/scale/degrees.ts","../src/theory/scale/masks.ts","../src/theory/scale/key.ts","../src/theory/chord/index.ts","../src/theory/counterpoint/index.ts"],"sourcesContent":["import { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport { clampToMidi } from '../../core/validation/index.js';\n\n/** Scale offset of a pitch relative to the key root, in [0, 11]. */\nfunction scaleOffset(pitch: number, key: KeyScale): number {\n return (pitchClass(pitch) - pitchClass(key.rootPc) + 12) % 12;\n}\n\n/**\n * Test whether a pitch belongs to the scale.\n *\n * @param pitch MIDI pitch or bare pitch class.\n * @param key The key/scale to test against.\n * @returns True if the pitch class is a member of the scale.\n *\n * @example\n * ```ts\n * import { majorKey, isScaleTone } from '@libraz/libcantus';\n * const key = majorKey(0); // C major\n * isScaleTone(64, key); // true — E is in C major\n * isScaleTone(61, key); // false — C# is not\n * ```\n *\n * @category Scales\n */\nexport function isScaleTone(pitch: number, key: KeyScale): boolean {\n return ((key.modeMask12 >> scaleOffset(pitch, key)) & 1) === 1;\n}\n\n/**\n * Find the nearest MIDI pitch whose pitch class is in the scale.\n *\n * The search expands symmetrically outward from `pitch`. On a tie (equal\n * distance above and below) the lower pitch is returned.\n *\n * @param pitch MIDI pitch to snap.\n * @param key The key/scale to snap to.\n * @returns The nearest in-scale MIDI pitch.\n *\n * @category Scales\n */\nexport function nearestScaleTone(pitch: number, key: KeyScale): number {\n const base = clampToMidi(Math.round(pitch), 'pitch');\n for (let distance = 0; distance < 12; distance += 1) {\n const lower = base - distance;\n if (lower >= 0 && isScaleTone(lower, key)) {\n return lower;\n }\n const higher = base + distance;\n if (higher <= 127 && isScaleTone(higher, key)) {\n return higher;\n }\n }\n return base;\n}\n\n/**\n * Get the 0-based scale degree of a pitch.\n *\n * @param pitch MIDI pitch or bare pitch class.\n * @param key The key/scale to measure against.\n * @returns The 0-based degree, or -1 if the pitch is not in the scale.\n *\n * @category Scales\n */\nexport function pitchToScaleDegree(pitch: number, key: KeyScale): number {\n const offset = scaleOffset(pitch, key);\n if (((key.modeMask12 >> offset) & 1) === 0) {\n return -1;\n }\n let degree = 0;\n for (let n = 0; n < offset; n += 1) {\n if (((key.modeMask12 >> n) & 1) === 1) {\n degree += 1;\n }\n }\n return degree;\n}\n\n/**\n * List the pitch classes of a key's scale.\n *\n * @param key The key/scale to enumerate.\n * @returns The member pitch classes, sorted ascending in [0, 11].\n *\n * @category Scales\n */\nexport function diatonicPitchClasses(key: KeyScale): number[] {\n const root = pitchClass(key.rootPc);\n const pcs: number[] = [];\n for (let n = 0; n < 12; n += 1) {\n if (((key.modeMask12 >> n) & 1) === 1) {\n pcs.push((root + n) % 12);\n }\n }\n return pcs.sort((a, b) => a - b);\n}\n\n/**\n * List the pitch classes of a key's scale in ascending scale-degree order.\n *\n * Degree 0 is the root; degrees follow the mask bits in offset order rather\n * than sorted pitch-class order.\n *\n * @param key The key/scale to enumerate.\n * @returns The member pitch classes ordered by scale degree.\n *\n * @category Scales\n */\nexport function scaleTonesInDegreeOrder(key: KeyScale): number[] {\n const root = pitchClass(key.rootPc);\n const pcs: number[] = [];\n for (let n = 0; n < 12; n += 1) {\n if (((key.modeMask12 >> n) & 1) === 1) {\n pcs.push((root + n) % 12);\n }\n }\n return pcs;\n}\n","import { InvalidInputError } from '../../core/errors/index.js';\nimport { assertInteger } from '../../core/validation/index.js';\n\n/**\n * Build a 12-bit mode mask from a list of semitone offsets above the root.\n *\n * Bit 0 (the root) is always set, enforcing the `KeyScale` invariant that the\n * root is a scale tone even when the offset list omits 0.\n *\n * @category Scales\n */\nexport function maskFromOffsets(offsets: readonly number[]): number {\n let mask = 1;\n for (const [index, offset] of offsets.entries()) {\n assertInteger(offset, `offsets[${index}]`);\n mask |= 1 << (((offset % 12) + 12) % 12);\n }\n return mask;\n}\n\n/**\n * Mode mask for the major (Ionian) scale: offsets {0, 2, 4, 5, 7, 9, 11}.\n *\n * @category Scales\n */\nexport const MAJOR_MASK = 0b101010110101;\n\n/**\n * Mode mask for the natural minor (Aeolian) scale: offsets {0, 2, 3, 5, 7, 8, 10}.\n *\n * @category Scales\n */\nexport const NATURAL_MINOR_MASK = 0b010110101101;\n\n/**\n * Harmonic minor: natural minor with a raised seventh — offsets {0,2,3,5,7,8,11}.\n *\n * @category Scales\n */\nexport const HARMONIC_MINOR_MASK = maskFromOffsets([0, 2, 3, 5, 7, 8, 11]);\n\n/**\n * Ascending melodic minor: offsets {0,2,3,5,7,9,11}.\n *\n * @category Scales\n */\nexport const MELODIC_MINOR_MASK = maskFromOffsets([0, 2, 3, 5, 7, 9, 11]);\n\n/**\n * Dorian mode: offsets {0,2,3,5,7,9,10}.\n *\n * @category Scales\n */\nexport const DORIAN_MASK = maskFromOffsets([0, 2, 3, 5, 7, 9, 10]);\n\n/**\n * Phrygian mode: offsets {0,1,3,5,7,8,10}.\n *\n * @category Scales\n */\nexport const PHRYGIAN_MASK = maskFromOffsets([0, 1, 3, 5, 7, 8, 10]);\n\n/**\n * Lydian mode: offsets {0,2,4,6,7,9,11}.\n *\n * @category Scales\n */\nexport const LYDIAN_MASK = maskFromOffsets([0, 2, 4, 6, 7, 9, 11]);\n\n/**\n * Mixolydian mode: offsets {0,2,4,5,7,9,10}.\n *\n * @category Scales\n */\nexport const MIXOLYDIAN_MASK = maskFromOffsets([0, 2, 4, 5, 7, 9, 10]);\n\n/**\n * Locrian mode: offsets {0,1,3,5,6,8,10}.\n *\n * @category Scales\n */\nexport const LOCRIAN_MASK = maskFromOffsets([0, 1, 3, 5, 6, 8, 10]);\n\n/**\n * Major pentatonic: offsets {0,2,4,7,9}.\n *\n * @category Scales\n */\nexport const MAJOR_PENTATONIC_MASK = maskFromOffsets([0, 2, 4, 7, 9]);\n\n/**\n * Minor pentatonic: offsets {0,3,5,7,10}.\n *\n * @category Scales\n */\nexport const MINOR_PENTATONIC_MASK = maskFromOffsets([0, 3, 5, 7, 10]);\n\n/**\n * Hexatonic blues scale: minor pentatonic plus the flat-fifth — {0,3,5,6,7,10}.\n *\n * @category Scales\n */\nexport const BLUES_MASK = maskFromOffsets([0, 3, 5, 6, 7, 10]);\n\n/**\n * Whole-tone scale: offsets {0,2,4,6,8,10}.\n *\n * @category Scales\n */\nexport const WHOLE_TONE_MASK = maskFromOffsets([0, 2, 4, 6, 8, 10]);\n\n/**\n * Octatonic (half-step first): offsets {0,1,3,4,6,7,9,10}.\n *\n * @category Scales\n */\nexport const OCTATONIC_HALF_WHOLE_MASK = maskFromOffsets([0, 1, 3, 4, 6, 7, 9, 10]);\n\n/**\n * Octatonic (whole-step first): offsets {0,2,3,5,6,8,9,11}.\n *\n * @category Scales\n */\nexport const OCTATONIC_WHOLE_HALF_MASK = maskFromOffsets([0, 2, 3, 5, 6, 8, 9, 11]);\n\n/**\n * Chromatic scale: all twelve pitch classes.\n *\n * @category Scales\n */\nexport const CHROMATIC_MASK = 0b111111111111;\n\n/**\n * Lydian dominant (melodic minor's fourth mode): offsets {0,2,4,6,7,9,10}.\n *\n * The scale over an unaltered dominant with a raised eleventh.\n *\n * @category Scales\n */\nexport const LYDIAN_DOMINANT_MASK = maskFromOffsets([0, 2, 4, 6, 7, 9, 10]);\n\n/**\n * Mixolydian b13, the aeolian dominant (melodic minor's fifth mode): offsets\n * {0,2,4,5,7,8,10}.\n *\n * The scale over a dominant with a flat thirteenth.\n *\n * @category Scales\n */\nexport const MIXOLYDIAN_B13_MASK = maskFromOffsets([0, 2, 4, 5, 7, 8, 10]);\n\n/**\n * Locrian natural 2 (melodic minor's sixth mode): offsets {0,2,3,5,6,8,10}.\n *\n * The scale over a half-diminished chord used as a minor ii.\n *\n * @category Scales\n */\nexport const LOCRIAN_NATURAL2_MASK = maskFromOffsets([0, 2, 3, 5, 6, 8, 10]);\n\n/**\n * The altered scale, or superlocrian (melodic minor's seventh mode): offsets\n * {0,1,3,4,6,8,10}.\n *\n * The scale over an altered dominant: every tension raised or lowered.\n *\n * @category Scales\n */\nexport const ALTERED_MASK = maskFromOffsets([0, 1, 3, 4, 6, 8, 10]);\n\n/**\n * Phrygian dominant (harmonic minor's fifth mode): offsets {0,1,4,5,7,8,10}.\n *\n * The scale over the dominant of a minor key, and the characteristic sound of\n * the flamenco and maqam repertoires.\n *\n * @category Scales\n */\nexport const PHRYGIAN_DOMINANT_MASK = maskFromOffsets([0, 1, 4, 5, 7, 8, 10]);\n\n/**\n * Named scale masks addressable by {@link scaleByName}.\n *\n * @category Scales\n */\nexport const NAMED_SCALES = Object.freeze({\n major: MAJOR_MASK,\n ionian: MAJOR_MASK,\n naturalMinor: NATURAL_MINOR_MASK,\n aeolian: NATURAL_MINOR_MASK,\n harmonicMinor: HARMONIC_MINOR_MASK,\n melodicMinor: MELODIC_MINOR_MASK,\n dorian: DORIAN_MASK,\n phrygian: PHRYGIAN_MASK,\n lydian: LYDIAN_MASK,\n mixolydian: MIXOLYDIAN_MASK,\n locrian: LOCRIAN_MASK,\n lydianDominant: LYDIAN_DOMINANT_MASK,\n mixolydianB13: MIXOLYDIAN_B13_MASK,\n locrianNatural2: LOCRIAN_NATURAL2_MASK,\n altered: ALTERED_MASK,\n phrygianDominant: PHRYGIAN_DOMINANT_MASK,\n majorPentatonic: MAJOR_PENTATONIC_MASK,\n minorPentatonic: MINOR_PENTATONIC_MASK,\n blues: BLUES_MASK,\n wholeTone: WHOLE_TONE_MASK,\n octatonicHalfWhole: OCTATONIC_HALF_WHOLE_MASK,\n octatonicWholeHalf: OCTATONIC_WHOLE_HALF_MASK,\n chromatic: CHROMATIC_MASK,\n});\n\n/**\n * The name of a built-in scale, as accepted by {@link scaleByName},\n * {@link avoidNotes}, and {@link availableTensions}.\n *\n * @category Scales\n */\nexport type ScaleName = keyof typeof NAMED_SCALES;\n\n/**\n * A scale name that completes to the built-in names but still accepts any\n * string, so a caller with a name from configuration is not forced to cast.\n *\n * @category Scales\n */\nexport type ScaleNameInput = ScaleName | (string & {});\n\n/**\n * The mask of a named scale, or undefined when the name is not one.\n *\n * Looking the name up through this helper is what keeps `'constructor'` or\n * `'toString'` from resolving to something inherited from `Object.prototype`\n * and producing a scale whose mask is a function.\n *\n * @param name The scale name.\n * @returns The 12-bit mask, or undefined for an unknown name.\n * @category Scales\n */\nexport function namedScaleMask(name: ScaleNameInput): number | undefined {\n return Object.hasOwn(NAMED_SCALES, name) ? NAMED_SCALES[name as ScaleName] : undefined;\n}\n\n/**\n * The mask of a named scale, rejecting an unknown name.\n *\n * Every public entry point that takes a scale name funnels through here, so a\n * typo is reported the same way everywhere instead of being answered with an\n * empty result that also means \"this scale has none\".\n *\n * @param name The scale name.\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.\n * @category Scales\n */\nexport function requireScaleMask(name: ScaleNameInput, label = 'scale'): number {\n const mask = namedScaleMask(name);\n if (mask === undefined) {\n throw new InvalidInputError(`Unknown ${label}: ${String(name)}`);\n }\n return mask;\n}\n","import { pitchClassOf } from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport type { ScaleNameInput } from './masks.js';\nimport { MAJOR_MASK, NATURAL_MINOR_MASK, requireScaleMask } from './masks.js';\n\n/**\n * Build a `KeyScale` for a major key on the given root pitch class.\n *\n * @example\n * ```ts\n * import { majorKey } from '@libraz/libcantus';\n * const cMajor = majorKey(0); // C major: { rootPc: 0, modeMask12: MAJOR_MASK }\n * ```\n *\n * @category Scales\n */\nexport function majorKey(rootPc: number): KeyScale {\n return { rootPc: pitchClassOf(rootPc), modeMask12: MAJOR_MASK };\n}\n\n/**\n * Build a `KeyScale` for a natural-minor key on the given root pitch class.\n *\n * @example\n * ```ts\n * import { minorKey } from '@libraz/libcantus';\n * const aMinor = minorKey(9); // A natural minor: { rootPc: 9, modeMask12: NATURAL_MINOR_MASK }\n * ```\n *\n * @category Scales\n */\nexport function minorKey(rootPc: number): KeyScale {\n return { rootPc: pitchClassOf(rootPc), modeMask12: NATURAL_MINOR_MASK };\n}\n\n/**\n * Build a `KeyScale` from a named scale (see {@link NAMED_SCALES}).\n *\n * @param name The scale name, e.g. `'dorian'` or `'harmonicMinor'`.\n * @param rootPc The root pitch class.\n * @returns The key/scale.\n * @throws If the name is not a known scale.\n *\n * @example\n * ```ts\n * import { scaleByName } from '@libraz/libcantus';\n * const dDorian = scaleByName('dorian', 2); // D Dorian, rootPc 2\n * ```\n *\n * @category Scales\n */\nexport function scaleByName(name: ScaleNameInput, rootPc: number): KeyScale {\n return { rootPc: pitchClassOf(rootPc), modeMask12: requireScaleMask(name) };\n}\n","import { InvalidInputError } from '../../core/errors/index.js';\nimport { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport { assertFiniteNumber, assertInteger } from '../../core/validation/index.js';\nimport { scaleTonesInDegreeOrder } from '../scale/index.js';\n\n/**\n * Chord quality identifiers understood by the chord builder.\n *\n * @category Chords\n */\nexport type ChordQuality =\n | 'maj'\n | 'min'\n | 'dim'\n | 'aug'\n | 'maj7'\n | 'min7'\n | 'dom7'\n | 'dim7'\n | 'm7b5'\n | 'minMaj7'\n | 'minMaj9'\n | 'minMaj11'\n | 'minMaj13'\n | 'aug7'\n | 'augMaj7'\n | 'majb5'\n | '6'\n | 'min6'\n | '6/9'\n | 'sus2'\n | 'sus4'\n | 'add9'\n | 'add11'\n | 'maj9'\n | 'min9'\n | 'dom9'\n | '7b9'\n | '7#9'\n | '7#11'\n | '7b13'\n | '11'\n | '13'\n | '5'\n | '7sus4'\n | '7b5'\n | '7alt'\n | '13b9'\n | 'maj13'\n | 'maj7#11'\n | 'min11'\n | 'min13'\n | 'minAdd9'\n | 'min6/9';\n\n/**\n * The basic role a chord tone plays: the degrees a chord's own template names.\n *\n * The shared vocabulary behind {@link chordToneRole}, the harmony layer's\n * `HarmonyRole`, and the voice analyser's labels, so the three cannot drift.\n *\n * @category Chords\n */\nexport type ChordToneRole = 'root' | 'third' | 'fifth' | 'sixth' | 'seventh';\n\n/**\n * A bare spelled pitch used as an enharmonic hint: a diatonic letter\n * (0..6 = C..B) plus a chromatic alteration (-2 double-flat .. +2 double-sharp).\n * Mirrors the letter/alter half of the pitch module's `Note` without an octave.\n *\n * @category Chords\n */\nexport type PitchSpelling = {\n letter: number;\n alter: number;\n};\n\n/**\n * A chord expressed as a root pitch class plus semitone offsets.\n *\n * `rootSpelling`/`bassSpelling` are optional enharmonic hints recorded by\n * parsers (e.g. `parseChordSymbol('Bb7')`) so formatters can reproduce the\n * original spelling instead of defaulting to sharps. Consumers may ignore\n * them; a hint is only trusted when its pitch class still matches the\n * corresponding `rootPc`/`bassPc`.\n *\n * @category Chords\n */\nexport type Chord = {\n rootPc: number;\n quality: ChordQuality;\n intervals: number[];\n bassPc?: number;\n rootSpelling?: PitchSpelling;\n bassSpelling?: PitchSpelling;\n};\n\n/**\n * A chord positioned in time: a root pitch class and quality placed at a beat,\n * with optional harmonic-analysis hints. Serves as the neutral exchange record\n * between progression generators and timeline analysers.\n *\n * @category Composition\n */\nexport type ChordSpan = {\n rootPc: number;\n quality: ChordQuality;\n startBeat: number;\n bassPc?: number;\n /** 0-based scale degree of the chord root, when known. */\n degree?: number;\n /** True when the chord is a secondary dominant tonicizing another degree. */\n secondaryDominant?: boolean;\n};\n\n/**\n * A chord occupying a half-open beat span `[startBeat, endBeat)`.\n *\n * The exchange record between anything that lays chords out in time — a\n * timeline analysis, a bass-line plan, an accompaniment generator — so those\n * surfaces share one definition instead of agreeing by coincidence.\n *\n * @category Composition\n */\nexport type ChordSegment = {\n startBeat: number;\n endBeat: number;\n chord: Chord;\n};\n\n/** Semitone offsets from the root for each supported chord quality. */\nconst QUALITY_INTERVALS: Record<ChordQuality, number[]> = {\n maj: [0, 4, 7],\n min: [0, 3, 7],\n dim: [0, 3, 6],\n aug: [0, 4, 8],\n majb5: [0, 4, 6],\n maj7: [0, 4, 7, 11],\n min7: [0, 3, 7, 10],\n dom7: [0, 4, 7, 10],\n dim7: [0, 3, 6, 9],\n m7b5: [0, 3, 6, 10],\n minMaj7: [0, 3, 7, 11],\n minMaj9: [0, 3, 7, 11, 14],\n minMaj11: [0, 3, 7, 11, 14, 17],\n minMaj13: [0, 3, 7, 11, 14, 21],\n aug7: [0, 4, 8, 10],\n augMaj7: [0, 4, 8, 11],\n '6': [0, 4, 7, 9],\n min6: [0, 3, 7, 9],\n '6/9': [0, 4, 7, 9, 14],\n sus2: [0, 2, 7],\n sus4: [0, 5, 7],\n add9: [0, 4, 7, 14],\n add11: [0, 4, 7, 17],\n maj9: [0, 4, 7, 11, 14],\n min9: [0, 3, 7, 10, 14],\n dom9: [0, 4, 7, 10, 14],\n '7b9': [0, 4, 7, 10, 13],\n '7#9': [0, 4, 7, 10, 15],\n '7#11': [0, 4, 7, 10, 18],\n '7b13': [0, 4, 7, 10, 20],\n '11': [0, 7, 10, 14, 17],\n '13': [0, 4, 7, 10, 14, 21],\n '5': [0, 7],\n '7sus4': [0, 5, 7, 10],\n '7b5': [0, 4, 6, 10],\n // The altered dominant as it is voiced from a lead sheet: a dominant seventh\n // with a raised fifth and a raised ninth. The full seven-note altered stack\n // has no seven-letter spelling, so it is not what this symbol denotes.\n '7alt': [0, 4, 8, 10, 15],\n '13b9': [0, 4, 7, 10, 13, 21],\n maj13: [0, 4, 7, 11, 14, 21],\n 'maj7#11': [0, 4, 7, 11, 18],\n min11: [0, 3, 7, 10, 14, 17],\n min13: [0, 3, 7, 10, 14, 21],\n minAdd9: [0, 3, 7, 14],\n 'min6/9': [0, 3, 7, 9, 14],\n};\n\n/**\n * All chord qualities the builder understands, in declaration order.\n *\n * The order is stable but carries no meaning: it is neither alphabetical nor\n * sorted by template size. {@link detectChord} uses it as its final tie-break,\n * so it is part of the observable behaviour and does not get reshuffled\n * casually — but do not read it as a ranking.\n *\n * @category Chords\n */\nexport function chordQualities(): ChordQuality[] {\n return Object.keys(QUALITY_INTERVALS) as ChordQuality[];\n}\n\n/**\n * Build a chord rooted on a diatonic scale degree.\n *\n * The root pitch class is the degree's diatonic pitch class in `key`; the\n * quality's interval template is attached unchanged. Degrees beyond the scale\n * length wrap around.\n *\n * @param degree 0-based scale degree of the chord root.\n * @param ext Chord quality to apply.\n * @param key Key context supplying the diatonic root.\n * @returns The constructed chord.\n *\n * @category Chords\n */\nexport function chordFromDegree(degree: number, ext: ChordQuality, key: KeyScale): Chord {\n assertInteger(degree, 'chord degree', -1000, 1000);\n if (!Object.hasOwn(QUALITY_INTERVALS, ext)) {\n throw new InvalidInputError(`Unknown chord quality: ${String(ext)}`);\n }\n const tones = scaleTonesInDegreeOrder(key);\n const length = tones.length;\n const index = length > 0 ? ((degree % length) + length) % length : 0;\n const rootPc = tones[index] ?? pitchClass(key.rootPc);\n return { rootPc, quality: ext, intervals: [...QUALITY_INTERVALS[ext]] };\n}\n\n/**\n * Build a chord from an explicit root pitch class and quality.\n *\n * @param rootPc Root pitch class (0..11).\n * @param quality Chord quality supplying the interval template.\n * @param bassPc Optional slash-chord bass pitch class.\n * @returns The constructed chord.\n *\n * @example\n * ```ts\n * import { makeChord } from '@libraz/libcantus';\n * const cmaj7 = makeChord(0, 'maj7');\n * // { rootPc: 0, quality: 'maj7', intervals: [0, 4, 7, 11] }\n * ```\n *\n * @category Chords\n */\nexport function makeChord(rootPc: number, quality: ChordQuality, bassPc?: number): Chord {\n // A NaN root formats as 'C' and an unknown quality as 'Cundefined', so both\n // travel on into voicing, detection, and serialization as a chord that looks\n // real. They are rejected where the chord is built instead.\n assertFiniteNumber(rootPc, 'chord rootPc');\n if (bassPc !== undefined) {\n assertFiniteNumber(bassPc, 'chord bassPc');\n }\n if (!Object.hasOwn(QUALITY_INTERVALS, quality)) {\n throw new InvalidInputError(`Unknown chord quality: ${String(quality)}`);\n }\n const chord: Chord = {\n rootPc: pitchClass(rootPc),\n quality,\n intervals: [...(QUALITY_INTERVALS[quality] ?? [])],\n };\n if (bassPc !== undefined) {\n chord.bassPc = pitchClass(bassPc);\n }\n return chord;\n}\n\n/**\n * Transpose a chord by a number of semitones.\n *\n * The interval template is carried over untouched, so a chord whose intervals\n * were customised keeps them, and a slash bass moves with the root. Enharmonic\n * spelling hints are dropped rather than shifted: the letter a transposed root\n * should take depends on the destination key, which a semitone count does not\n * carry. Pass the result through {@link spellChord} to respell it.\n *\n * @param chord The chord to transpose.\n * @param semitones The signed semitone offset.\n * @returns The transposed chord.\n * @throws If `semitones` is not finite.\n *\n * @example\n * ```ts\n * import { formatChordSymbol, parseChordSymbol, transposeChord } from '@libraz/libcantus';\n * formatChordSymbol(transposeChord(parseChordSymbol('C/G'), 2)); // 'D/A'\n * ```\n *\n * @category Chords\n */\nexport function transposeChord(chord: Chord, semitones: number): Chord {\n assertFiniteNumber(semitones, 'semitones');\n const steps = Math.round(semitones);\n const moved: Chord = {\n rootPc: pitchClass(chord.rootPc + steps),\n quality: chord.quality,\n intervals: [...chord.intervals],\n };\n if (chord.bassPc !== undefined) {\n moved.bassPc = pitchClass(chord.bassPc + steps);\n }\n return moved;\n}\n\n/**\n * Get the sorted, deduplicated pitch classes of a chord.\n *\n * A slash bass is one of them: `F/G` sounds a G, the voicers put it in the\n * bass, and a set that omitted it would not re-detect as the chord it came\n * from. Pass `includeBass: false` to enumerate the interval template alone.\n *\n * @param chord The chord to enumerate.\n * @param opts Set `includeBass: false` to omit a slash bass.\n * @returns The chord's pitch classes, sorted ascending in [0, 11].\n *\n * @example\n * ```ts\n * import { makeChord, chordPitchClasses } from '@libraz/libcantus';\n * chordPitchClasses(makeChord(0, 'maj7')); // [0, 4, 7, 11]\n * chordPitchClasses(makeChord(5, 'maj', 7)); // [0, 5, 7, 9] — F/G, G included\n * ```\n *\n * @category Chords\n */\nexport function chordPitchClasses(chord: Chord, opts: { includeBass?: boolean } = {}): number[] {\n const set = new Set<number>();\n for (const interval of chord.intervals) {\n set.add(pitchClass(chord.rootPc + interval));\n }\n if (opts.includeBass !== false && chord.bassPc !== undefined) {\n set.add(pitchClass(chord.bassPc));\n }\n return [...set].sort((a, b) => a - b);\n}\n\n/**\n * A pitch's interval class above the chord root, in [0, 11].\n *\n * The shared reading of \"where does this pitch sit in the chord\", used by the\n * safety evaluator and the voice analyser alike so they cannot disagree.\n *\n * @param pitch MIDI pitch or bare pitch class.\n * @param chord The chord providing the root reference.\n * @returns The interval class above the root.\n * @category Chords\n */\nexport function intervalAboveRoot(pitch: number, chord: Chord): number {\n return (((pitchClass(pitch) - pitchClass(chord.rootPc)) % 12) + 12) % 12;\n}\n\n/**\n * Whether a pitch is one of a chord's tones, ignoring octave.\n *\n * @param pitch MIDI pitch or bare pitch class.\n * @param chord The chord, or null for no sounding harmony.\n * @returns True when the pitch class belongs to the chord.\n * @category Chords\n */\nexport function isChordMember(pitch: number, chord: Chord | null): boolean {\n return chord === null ? false : chordPitchClasses(chord).includes(pitchClass(pitch));\n}\n\n/** The chord-tone offsets present in a chord, reduced to pitch-class space. */\nfunction chordToneOffsets(chord: Chord): Set<number> {\n return new Set(chord.intervals.map((i) => ((i % 12) + 12) % 12));\n}\n\n/**\n * Determine a pitch's harmonic role within a chord.\n *\n * The role is derived from the pitch's interval above the chord root, reduced\n * modulo 12. A major sixth (9) reads as a `sixth` for sixth chords but as a\n * `seventh` for a diminished-seventh chord; ninths and other tensions have no\n * basic role and return null.\n *\n * @param pitch MIDI pitch or bare pitch class.\n * @param chord The chord providing the root reference.\n * @returns The chord-tone role, or null if the pitch has no basic role.\n *\n * @category Chords\n */\nexport function chordToneRole(pitch: number, chord: Chord): ChordToneRole | null {\n const interval = (pitchClass(pitch) - pitchClass(chord.rootPc) + 12) % 12;\n const tones = chordToneOffsets(chord);\n if (interval === 0) {\n return 'root';\n }\n if (interval === 3 || interval === 4) {\n return tones.has(interval) && (interval !== 3 || !tones.has(4)) ? 'third' : null;\n }\n if (interval === 7) {\n return tones.has(7) ? 'fifth' : null;\n }\n if (interval === 6) {\n // A diminished fifth is the chord's fifth only when no perfect fifth is\n // present; alongside a perfect fifth it is a #11 tension, not a fifth.\n return tones.has(6) && !tones.has(7) ? 'fifth' : null;\n }\n if (interval === 8) {\n // An augmented fifth is the chord's fifth only without a perfect fifth;\n // alongside a perfect fifth it is a b13 tension, not a fifth.\n return tones.has(8) && !tones.has(7) ? 'fifth' : null;\n }\n if (interval === 9) {\n const hasHigherSeventh = tones.has(10) || tones.has(11);\n const isDiminishedSeventh = tones.has(3) && tones.has(6) && !hasHigherSeventh;\n if (tones.has(9) && isDiminishedSeventh) {\n return 'seventh';\n }\n if (tones.has(9) && !hasHigherSeventh) {\n return 'sixth';\n }\n return null;\n }\n if (interval === 10 || interval === 11) {\n return tones.has(interval) ? 'seventh' : null;\n }\n return null;\n}\n\n/** Classify a stacked-thirds triad into a chord quality from its interval set. */\nfunction classifyTriad(thirdIc: number, fifthIc: number): ChordQuality {\n if (thirdIc === 4 && fifthIc === 8) {\n return 'aug';\n }\n if (thirdIc === 4 && fifthIc === 6) {\n return 'majb5';\n }\n if (thirdIc === 3 && fifthIc === 6) {\n return 'dim';\n }\n if (thirdIc === 3) {\n return 'min';\n }\n return 'maj';\n}\n\n/** Classify a stacked-thirds seventh chord into a chord quality. */\nfunction classifySeventh(thirdIc: number, fifthIc: number, seventhIc: number): ChordQuality {\n if (thirdIc === 3 && fifthIc === 6 && seventhIc === 9) {\n return 'dim7';\n }\n if (thirdIc === 3 && fifthIc === 6 && seventhIc === 10) {\n return 'm7b5';\n }\n if (thirdIc === 3 && seventhIc === 11) {\n return 'minMaj7';\n }\n if (thirdIc === 3) {\n return 'min7';\n }\n if (fifthIc === 8 && seventhIc === 10) {\n return 'aug7';\n }\n if (fifthIc === 8 && seventhIc === 11) {\n return 'augMaj7';\n }\n if (fifthIc === 6 && seventhIc === 10) {\n return '7b5';\n }\n if (seventhIc === 11) {\n return 'maj7';\n }\n return 'dom7';\n}\n\n/** Whether a classified quality describes precisely the stacked pitch classes. */\nfunction matchesQualityIntervals(quality: ChordQuality, intervals: readonly number[]): boolean {\n const template = QUALITY_INTERVALS[quality];\n return (\n template.length === intervals.length &&\n template.every((interval, index) => interval === intervals[index])\n );\n}\n\n/** Stack scale thirds from a scale degree into a chord of `size` notes. */\nfunction stackThirds(degree: number, key: KeyScale, size: 3 | 4): Chord {\n assertInteger(degree, 'chord degree', -1000, 1000);\n const tones = scaleTonesInDegreeOrder(key);\n const length = tones.length;\n if (length !== 7) {\n throw new InvalidInputError(\n `diatonic chord stacking requires a heptatonic scale (received ${length} tones)`,\n );\n }\n const idx = (step: number) => (((degree + step) % length) + length) % length;\n const rootPc = tones[idx(0)] ?? pitchClass(key.rootPc);\n const offsets = [0, 2, 4, 6].slice(0, size).map((step) => {\n const pc = tones[idx(step)] ?? rootPc;\n return (((pc - rootPc) % 12) + 12) % 12;\n });\n const thirdIc = offsets[1] ?? 4;\n const fifthIc = offsets[2] ?? 7;\n const quality =\n size === 3\n ? classifyTriad(thirdIc, fifthIc)\n : classifySeventh(thirdIc, fifthIc, offsets[3] ?? 11);\n if (!matchesQualityIntervals(quality, offsets)) {\n throw new InvalidInputError(\n `scale thirds at degree ${degree} produce unsupported intervals [${offsets.join(', ')}]`,\n );\n }\n return { rootPc, quality, intervals: offsets };\n}\n\n/**\n * Build the diatonic triad rooted on a scale degree by stacking scale thirds.\n *\n * Unlike {@link chordFromDegree}, the chord quality is derived from the scale\n * rather than supplied, so degrees yield their scale-correct triads (e.g. a\n * diminished triad on the leading tone of a major key).\n *\n * @param degree 0-based scale degree of the chord root.\n * @param key Key/scale context.\n * @returns The diatonic triad.\n * @throws If the scale is not heptatonic. Stacking thirds means skipping every\n * other degree, which only lands on a triad when there are seven of them, so\n * the pentatonic, blues, whole-tone, octatonic and chromatic scales have no\n * diatonic triad; use {@link chordFromDegree} with an explicit quality there.\n *\n * @example\n * ```ts\n * import { majorKey, diatonicTriad } from '@libraz/libcantus';\n * const tonic = diatonicTriad(0, majorKey(0)); // C major triad (degree 0 of C major)\n * ```\n *\n * @category Chords\n */\nexport function diatonicTriad(degree: number, key: KeyScale): Chord {\n return stackThirds(degree, key, 3);\n}\n\n/**\n * Build the diatonic seventh chord rooted on a scale degree.\n *\n * @param degree 0-based scale degree of the chord root.\n * @param key Key/scale context.\n * @returns The diatonic seventh chord.\n * @throws If the scale is not heptatonic, for the reason given on\n * {@link diatonicTriad}.\n *\n * @category Chords\n */\nexport function diatonicSeventh(degree: number, key: KeyScale): Chord {\n return stackThirds(degree, key, 4);\n}\n","import { isConsonantInterval } from '../../core/interval/index.js';\nimport { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.js';\n\n/**\n * Whether an upper voice has crossed below a lower voice.\n *\n * @param upperPitch Pitch of the nominally higher voice.\n * @param lowerPitch Pitch of the nominally lower voice.\n * @returns True when the upper voice sits below the lower voice.\n * @category Voicing & Counterpoint\n */\nexport function createsVoiceCrossing(upperPitch: number, lowerPitch: number): boolean {\n return upperPitch < lowerPitch;\n}\n\n/**\n * Whether two simultaneous pitches form a dissonance.\n *\n * @param a First pitch.\n * @param b Second pitch.\n * @param twoVoice When true, the perfect fourth counts as dissonant.\n * @returns True when the vertical interval is dissonant.\n * @category Voicing & Counterpoint\n */\nexport function createsVerticalDissonance(a: number, b: number, twoVoice: boolean): boolean {\n return !isConsonantInterval(a - b, twoVoice);\n}\n\n/**\n * Whether a melodic move is a forbidden leap: a tritone, either seventh (minor\n * or major), or any leap wider than an octave.\n *\n * Operates on the raw absolute distance, so compound leaps are not reduced to\n * their simple class before the octave check.\n *\n * @param prev Starting pitch.\n * @param cur Ending pitch.\n * @returns True when the leap is forbidden in strict counterpoint.\n * @category Voicing & Counterpoint\n */\nexport function isForbiddenMelodicLeap(prev: number, cur: number): boolean {\n const semis = Math.abs(cur - prev);\n return semis === 6 || semis === 10 || semis === 11 || semis > 12;\n}\n\n/** Reduce an interval to its simple class in [0, 11]. */\nfunction simpleClass(semitones: number): number {\n return pitchClass(Math.abs(semitones));\n}\n\n/** Whether a simple interval class is a perfect kind (unison/octave or fifth). */\nfunction isPerfectClass(cls: number): boolean {\n return cls === 0 || cls === 7;\n}\n\n/** Whether two voices move in the same direction (similar or parallel motion). */\nfunction similarMotion(aMove: number, bMove: number): boolean {\n return aMove !== 0 && bMove !== 0 && aMove > 0 === bMove > 0;\n}\n\n/** Whether both voices actually move (neither is stationary — excludes oblique motion). */\nfunction bothVoicesMove(aMove: number, bMove: number): boolean {\n return aMove !== 0 && bMove !== 0;\n}\n\n/**\n * Whether two voices move into consecutive perfect intervals of the same kind\n * (fifth-to-fifth, octave-to-octave, unison-to-unison).\n *\n * Both true parallels (similar motion) and anti-parallels — the same perfect\n * class reached by contrary motion, e.g. octave to octave with the voices moving\n * in opposite directions — are flagged, as both are forbidden in strict two-voice\n * counterpoint. The rule requires that both voices actually move: oblique motion\n * (either voice stationary) and the no-change case (identical pitches) are excluded.\n *\n * A fifth expanding to a twelfth counts (same perfect class); a fifth moving to\n * an octave does not (different perfect kinds — the direct/hidden case owned by\n * {@link createsHiddenParallelPerfect}).\n *\n * @category Voicing & Counterpoint\n */\nexport function createsParallelPerfect(\n aPrev: number,\n aCur: number,\n bPrev: number,\n bCur: number,\n): boolean {\n if (!bothVoicesMove(aCur - aPrev, bCur - bPrev)) {\n return false;\n }\n const nowClass = simpleClass(aCur - bCur);\n const prevClass = simpleClass(aPrev - bPrev);\n return isPerfectClass(nowClass) && nowClass === prevClass;\n}\n\n/**\n * Whether two voices move in consecutive parallel octaves (or unisons) by\n * similar motion.\n *\n * This is a strict subset of {@link createsParallelPerfect}: a similar-motion\n * octave-to-octave is the perfect-class-zero case that predicate already flags\n * (and it additionally catches the contrary-motion anti-parallel). Callers that\n * tally parallel violations should therefore use {@link createsParallelPerfect}\n * alone to avoid double counting; this predicate remains for callers wanting a\n * dedicated similar-motion octave test.\n *\n * @category Voicing & Counterpoint\n */\nexport function createsParallelOctave(\n aPrev: number,\n aCur: number,\n bPrev: number,\n bCur: number,\n): boolean {\n if (!similarMotion(aCur - aPrev, bCur - bPrev)) {\n return false;\n }\n return simpleClass(aCur - bCur) === 0 && simpleClass(aPrev - bPrev) === 0;\n}\n\n/**\n * Whether two voices move in consecutive parallel unisons — both landing on the\n * same pitch, having shared a pitch on the previous move.\n *\n * @category Voicing & Counterpoint\n */\nexport function createsParallelUnison(\n aPrev: number,\n aCur: number,\n bPrev: number,\n bCur: number,\n): boolean {\n if (aCur === aPrev || bCur === bPrev) {\n return false;\n }\n return aCur === bCur && aPrev === bPrev;\n}\n\n/**\n * Whether two voices reach a perfect interval by similar motion from an\n * imperfect one (a hidden/direct fifth or octave).\n *\n * The traditional step exception is applied: the approach is allowed when the\n * upper of the two voices moves by step, so only leaps into the perfect\n * interval are flagged.\n *\n * @category Voicing & Counterpoint\n */\nexport function createsHiddenParallelPerfect(\n aPrev: number,\n aCur: number,\n bPrev: number,\n bCur: number,\n): boolean {\n const aMove = aCur - aPrev;\n const bMove = bCur - bPrev;\n if (!similarMotion(aMove, bMove)) {\n return false;\n }\n const nowClass = simpleClass(aCur - bCur);\n const prevClass = simpleClass(aPrev - bPrev);\n // Approaching the same perfect class (e.g. fifth to fifth) is a true parallel\n // owned by createsParallelPerfect; approaching a different perfect interval\n // (fifth to octave, or vice versa) is the hidden/direct case flagged here.\n if (!isPerfectClass(nowClass) || prevClass === nowClass) {\n return false;\n }\n const upperMove = aCur >= bCur ? aMove : bMove;\n if (Math.abs(upperMove) <= 2) {\n return false; // upper voice moves by step — direct interval is acceptable\n }\n return true;\n}\n\n/**\n * Whether two voices overlap: the upper voice descends below where the lower\n * voice just was, or the lower voice rises above where the upper voice just was.\n * Distinct from a simultaneous voice crossing.\n *\n * @param upperPrev Previous pitch of the upper voice.\n * @param upperCur Current pitch of the upper voice.\n * @param lowerPrev Previous pitch of the lower voice.\n * @param lowerCur Current pitch of the lower voice.\n * @category Voicing & Counterpoint\n */\nexport function createsVoiceOverlap(\n upperPrev: number,\n upperCur: number,\n lowerPrev: number,\n lowerCur: number,\n): boolean {\n return upperCur < lowerPrev || lowerCur > upperPrev;\n}\n\n/**\n * Whether two adjacent upper voices are spaced more than a maximum apart\n * (commonly an octave). Bass-to-tenor spacing is conventionally exempt, so this\n * is meant for the upper voice pairs.\n *\n * @param upperPitch Pitch of the higher voice.\n * @param lowerPitch Pitch of the lower voice.\n * @param maxSemitones Maximum allowed spacing in semitones (default an octave).\n * @category Voicing & Counterpoint\n */\nexport function exceedsSpacing(upperPitch: number, lowerPitch: number, maxSemitones = 12): boolean {\n return Math.abs(upperPitch - lowerPitch) > maxSemitones;\n}\n\n/**\n * Whether a leading tone resolves correctly upward to the tonic.\n *\n * @param prev The leading-tone pitch.\n * @param cur The following pitch.\n * @param key Key context supplying the tonic.\n * @returns True when `prev` is the leading tone and `cur` is the tonic a step above.\n * @category Voicing & Counterpoint\n */\nexport function isLeadingToneResolution(prev: number, cur: number, key: KeyScale): boolean {\n const tonic = pitchClass(key.rootPc);\n const leading = (tonic + 11) % 12;\n if (pitchClass(prev) !== leading || pitchClass(cur) !== tonic) {\n return false;\n }\n return cur > prev && cur - prev <= 2;\n}\n"],"mappings":"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","names":[]}