@libraz/libcantus 0.9.3 → 0.9.4

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 (198) hide show
  1. package/README.md +2 -2
  2. package/dist/analyze/index.cjs +39 -1947
  3. package/dist/analyze/index.cjs.map +1 -1
  4. package/dist/analyze/index.d.cts +197 -37
  5. package/dist/analyze/index.d.ts +197 -37
  6. package/dist/analyze/index.js +19 -8
  7. package/dist/chunk-36N73KZ3.cjs +246 -0
  8. package/dist/chunk-36N73KZ3.cjs.map +1 -0
  9. package/dist/chunk-4HLLZPXT.cjs +3077 -0
  10. package/dist/chunk-4HLLZPXT.cjs.map +1 -0
  11. package/dist/chunk-7MGZ7D62.cjs +498 -0
  12. package/dist/chunk-7MGZ7D62.cjs.map +1 -0
  13. package/dist/{chunk-4MHA432G.js → chunk-7NKDPF74.js} +403 -180
  14. package/dist/chunk-7NKDPF74.js.map +1 -0
  15. package/dist/chunk-CAN76NCU.cjs +679 -0
  16. package/dist/chunk-CAN76NCU.cjs.map +1 -0
  17. package/dist/{chunk-MIWO425M.js → chunk-D2FAJBYL.js} +101 -34
  18. package/dist/chunk-D2FAJBYL.js.map +1 -0
  19. package/dist/{chunk-SGU7SPLK.js → chunk-EHVEBWM5.js} +1056 -531
  20. package/dist/chunk-EHVEBWM5.js.map +1 -0
  21. package/dist/chunk-FCANXBAT.cjs +30 -0
  22. package/dist/chunk-FCANXBAT.cjs.map +1 -0
  23. package/dist/chunk-FNDWXRRI.cjs +479 -0
  24. package/dist/chunk-FNDWXRRI.cjs.map +1 -0
  25. package/dist/{chunk-IT2O4XFE.js → chunk-GQSGS2N4.js} +43 -24
  26. package/dist/chunk-GQSGS2N4.js.map +1 -0
  27. package/dist/{chunk-62QZKCJQ.js → chunk-HKGYCQ34.js} +10 -6
  28. package/dist/chunk-HKGYCQ34.js.map +1 -0
  29. package/dist/{chunk-AKLYPWIN.js → chunk-I24MV65U.js} +29 -6
  30. package/dist/chunk-I24MV65U.js.map +1 -0
  31. package/dist/{chunk-CHMXELDS.js → chunk-KMHOICIY.js} +311 -181
  32. package/dist/chunk-KMHOICIY.js.map +1 -0
  33. package/dist/chunk-LW53AHQU.cjs +1117 -0
  34. package/dist/chunk-LW53AHQU.cjs.map +1 -0
  35. package/dist/{chunk-2ZWMSJH6.js → chunk-NKIBQ6PT.js} +2 -2
  36. package/dist/chunk-NKIBQ6PT.js.map +1 -0
  37. package/dist/chunk-NP4NA2FB.cjs +106 -0
  38. package/dist/chunk-NP4NA2FB.cjs.map +1 -0
  39. package/dist/chunk-NVE34YGZ.cjs +1449 -0
  40. package/dist/chunk-NVE34YGZ.cjs.map +1 -0
  41. package/dist/{chunk-EH6EM37U.js → chunk-NYYWMV2C.js} +61 -49
  42. package/dist/chunk-NYYWMV2C.js.map +1 -0
  43. package/dist/{chunk-GOAW4VYL.js → chunk-Q2XUP227.js} +243 -117
  44. package/dist/chunk-Q2XUP227.js.map +1 -0
  45. package/dist/chunk-Q6OP4U43.cjs +327 -0
  46. package/dist/chunk-Q6OP4U43.cjs.map +1 -0
  47. package/dist/chunk-RC6GBMCD.cjs +845 -0
  48. package/dist/chunk-RC6GBMCD.cjs.map +1 -0
  49. package/dist/{chunk-C5XYKUVG.js → chunk-RK35RIN7.js} +370 -157
  50. package/dist/chunk-RK35RIN7.js.map +1 -0
  51. package/dist/chunk-RUMU3UA6.cjs +53 -0
  52. package/dist/chunk-RUMU3UA6.cjs.map +1 -0
  53. package/dist/chunk-T64C7ZD7.cjs +1 -0
  54. package/dist/chunk-T64C7ZD7.cjs.map +1 -0
  55. package/dist/chunk-XEBG3G53.js +498 -0
  56. package/dist/chunk-XEBG3G53.js.map +1 -0
  57. package/dist/{chunk-I5R7ZBJS.js → chunk-YLEMGEFH.js} +103 -59
  58. package/dist/chunk-YLEMGEFH.js.map +1 -0
  59. package/dist/chunk-ZEJR4TKL.cjs +173 -0
  60. package/dist/chunk-ZEJR4TKL.cjs.map +1 -0
  61. package/dist/core/index.cjs +130 -647
  62. package/dist/core/index.cjs.map +1 -1
  63. package/dist/core/index.d.cts +301 -39
  64. package/dist/core/index.d.ts +301 -39
  65. package/dist/core/index.js +54 -16
  66. package/dist/{function-DYt4iKsn.d.cts → function-BtiZb9i9.d.ts} +28 -7
  67. package/dist/{function-DVH4Eusr.d.ts → function-DNugAOrt.d.cts} +28 -7
  68. package/dist/generate/index.cjs +28 -4085
  69. package/dist/generate/index.cjs.map +1 -1
  70. package/dist/generate/index.d.cts +370 -62
  71. package/dist/generate/index.d.ts +370 -62
  72. package/dist/generate/index.js +23 -9
  73. package/dist/{index-CxqBQtH1.d.cts → index-6VR8-kit.d.ts} +19 -4
  74. package/dist/index-70p168VM.d.ts +192 -0
  75. package/dist/{index-CpOsP1y9.d.cts → index-Be74mcon.d.cts} +18 -17
  76. package/dist/index-BtXT6UQ0.d.cts +192 -0
  77. package/dist/{index-DxvARIP2.d.ts → index-C5UBbQ4G.d.ts} +18 -17
  78. package/dist/{index-Bt4tOUD9.d.ts → index-C83mZI1P.d.ts} +93 -8
  79. package/dist/{index-Bg5dYMmZ.d.ts → index-D6b6na69.d.cts} +14 -11
  80. package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
  81. package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
  82. package/dist/index-OG65ocOr.d.cts +272 -0
  83. package/dist/index-OG65ocOr.d.ts +272 -0
  84. package/dist/{index-Jc0Ryi79.d.ts → index-tJhkmQgE.d.cts} +19 -4
  85. package/dist/{index-Dikst44v.d.cts → index-tSQh-xwY.d.ts} +14 -11
  86. package/dist/{index-BNlGP86A.d.cts → index-y_ak9_Hi.d.cts} +93 -8
  87. package/dist/index.cjs +419 -7383
  88. package/dist/index.cjs.map +1 -1
  89. package/dist/index.d.cts +14 -14
  90. package/dist/index.d.ts +14 -14
  91. package/dist/index.js +111 -27
  92. package/dist/model/index.cjs +8 -2562
  93. package/dist/model/index.cjs.map +1 -1
  94. package/dist/model/index.d.cts +433 -75
  95. package/dist/model/index.d.ts +433 -75
  96. package/dist/model/index.js +6 -7
  97. package/dist/styled-DIRkk9Uz.d.ts +506 -0
  98. package/dist/styled-DJbkJQfj.d.cts +506 -0
  99. package/dist/theory/index.cjs +172 -1822
  100. package/dist/theory/index.cjs.map +1 -1
  101. package/dist/theory/index.d.cts +151 -233
  102. package/dist/theory/index.d.ts +151 -233
  103. package/dist/theory/index.js +30 -7
  104. package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
  105. package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
  106. package/package.json +24 -6
  107. package/dist/chunk-2ZWMSJH6.js.map +0 -1
  108. package/dist/chunk-4MHA432G.js.map +0 -1
  109. package/dist/chunk-62QZKCJQ.js.map +0 -1
  110. package/dist/chunk-AKLYPWIN.js.map +0 -1
  111. package/dist/chunk-C5XYKUVG.js.map +0 -1
  112. package/dist/chunk-CHMXELDS.js.map +0 -1
  113. package/dist/chunk-DYWFNC4A.js +0 -152
  114. package/dist/chunk-DYWFNC4A.js.map +0 -1
  115. package/dist/chunk-EH6EM37U.js.map +0 -1
  116. package/dist/chunk-FBQK3ZQN.js +0 -131
  117. package/dist/chunk-FBQK3ZQN.js.map +0 -1
  118. package/dist/chunk-GOAW4VYL.js.map +0 -1
  119. package/dist/chunk-I5R7ZBJS.js.map +0 -1
  120. package/dist/chunk-IT2O4XFE.js.map +0 -1
  121. package/dist/chunk-MIWO425M.js.map +0 -1
  122. package/dist/chunk-SGU7SPLK.js.map +0 -1
  123. package/dist/index-CGMZNCx6.d.cts +0 -140
  124. package/dist/index-CGMZNCx6.d.ts +0 -140
  125. package/dist/index-CnZCo29u.d.cts +0 -108
  126. package/dist/index-JnF4v9bx.d.ts +0 -108
  127. package/dist/styled-DZ7GjShb.d.ts +0 -282
  128. package/dist/styled-ZTbdaVFM.d.cts +0 -282
  129. package/src/analyze/arrange/index.ts +0 -21
  130. package/src/analyze/arrange/internal.ts +0 -127
  131. package/src/analyze/arrange/tension.ts +0 -199
  132. package/src/analyze/arrange/tracks.ts +0 -312
  133. package/src/analyze/detect/index.ts +0 -272
  134. package/src/analyze/functional/borrowed.ts +0 -84
  135. package/src/analyze/functional/cadence.ts +0 -66
  136. package/src/analyze/functional/function.ts +0 -184
  137. package/src/analyze/functional/index.ts +0 -23
  138. package/src/analyze/functional/internal.ts +0 -29
  139. package/src/analyze/functional/roman.ts +0 -402
  140. package/src/analyze/index.ts +0 -49
  141. package/src/analyze/timeline/index.ts +0 -451
  142. package/src/analyze/voice/index.ts +0 -339
  143. package/src/core/event-index/index.ts +0 -104
  144. package/src/core/index.ts +0 -61
  145. package/src/core/interval/index.ts +0 -81
  146. package/src/core/meter/index.ts +0 -302
  147. package/src/core/pitch/index.ts +0 -311
  148. package/src/core/random/index.ts +0 -76
  149. package/src/core/tuning/index.ts +0 -186
  150. package/src/core/types.ts +0 -29
  151. package/src/core/validation/index.ts +0 -119
  152. package/src/generate/bass/index.ts +0 -373
  153. package/src/generate/countermelody/index.ts +0 -470
  154. package/src/generate/drums/beat.ts +0 -266
  155. package/src/generate/drums/euclid.ts +0 -65
  156. package/src/generate/drums/fills.ts +0 -356
  157. package/src/generate/drums/ghost.ts +0 -119
  158. package/src/generate/drums/hihat.ts +0 -285
  159. package/src/generate/drums/hit.ts +0 -31
  160. package/src/generate/drums/index.ts +0 -361
  161. package/src/generate/drums/internal.ts +0 -257
  162. package/src/generate/drums/kick.ts +0 -160
  163. package/src/generate/drums/percussion.ts +0 -121
  164. package/src/generate/drums/rng.ts +0 -7
  165. package/src/generate/drums/swing.ts +0 -71
  166. package/src/generate/groove/index.ts +0 -351
  167. package/src/generate/harmonize/index.ts +0 -451
  168. package/src/generate/index.ts +0 -58
  169. package/src/generate/motif/index.ts +0 -422
  170. package/src/generate/progression/index.ts +0 -362
  171. package/src/generate/reharmony/index.ts +0 -323
  172. package/src/generate/rhythm/index.ts +0 -175
  173. package/src/index.ts +0 -13
  174. package/src/model/chord.ts +0 -442
  175. package/src/model/index.ts +0 -15
  176. package/src/model/interval.ts +0 -82
  177. package/src/model/key.ts +0 -266
  178. package/src/model/note.ts +0 -176
  179. package/src/model/progression.ts +0 -161
  180. package/src/model/shared.ts +0 -12
  181. package/src/theory/chord/index.ts +0 -375
  182. package/src/theory/chordscale/index.ts +0 -374
  183. package/src/theory/counterpoint/index.ts +0 -230
  184. package/src/theory/harmony/index.ts +0 -77
  185. package/src/theory/index.ts +0 -112
  186. package/src/theory/safety/index.ts +0 -488
  187. package/src/theory/scale/degrees.ts +0 -123
  188. package/src/theory/scale/index.ts +0 -28
  189. package/src/theory/scale/key.ts +0 -56
  190. package/src/theory/scale/masks.ts +0 -153
  191. package/src/theory/spelling/index.ts +0 -249
  192. package/src/theory/symbol/index.ts +0 -268
  193. package/src/theory/voicing/index.ts +0 -5
  194. package/src/theory/voicing/internal.ts +0 -168
  195. package/src/theory/voicing/leading.ts +0 -112
  196. package/src/theory/voicing/satb.ts +0 -216
  197. package/src/theory/voicing/styled.ts +0 -199
  198. package/tsconfig.json +0 -23
@@ -0,0 +1,479 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }
2
+
3
+
4
+
5
+
6
+
7
+ var _chunk7MGZ7D62cjs = require('./chunk-7MGZ7D62.cjs');
8
+
9
+ // src/theory/scale/degrees.ts
10
+ function scaleOffset(pitch, key) {
11
+ return (_chunk7MGZ7D62cjs.pitchClassOf.call(void 0, pitch) - _chunk7MGZ7D62cjs.pitchClassOf.call(void 0, key.rootPc) + 12) % 12;
12
+ }
13
+ function isScaleTone(pitch, key) {
14
+ return (key.modeMask12 >> scaleOffset(pitch, key) & 1) === 1;
15
+ }
16
+ function nearestScaleTone(pitch, key) {
17
+ const base = Math.round(pitch);
18
+ for (let distance = 0; distance < 12; distance += 1) {
19
+ const lower = base - distance;
20
+ if (isScaleTone(lower, key)) {
21
+ return lower;
22
+ }
23
+ const higher = base + distance;
24
+ if (isScaleTone(higher, key)) {
25
+ return higher;
26
+ }
27
+ }
28
+ return base;
29
+ }
30
+ function pitchToScaleDegree(pitch, key) {
31
+ const offset = scaleOffset(pitch, key);
32
+ if ((key.modeMask12 >> offset & 1) === 0) {
33
+ return -1;
34
+ }
35
+ let degree = 0;
36
+ for (let n = 0; n < offset; n += 1) {
37
+ if ((key.modeMask12 >> n & 1) === 1) {
38
+ degree += 1;
39
+ }
40
+ }
41
+ return degree;
42
+ }
43
+ function diatonicPitchClasses(key) {
44
+ const root = _chunk7MGZ7D62cjs.pitchClassOf.call(void 0, key.rootPc);
45
+ const pcs = [];
46
+ for (let n = 0; n < 12; n += 1) {
47
+ if ((key.modeMask12 >> n & 1) === 1) {
48
+ pcs.push((root + n) % 12);
49
+ }
50
+ }
51
+ return pcs.sort((a, b) => a - b);
52
+ }
53
+ function scaleTonesInDegreeOrder(key) {
54
+ const root = _chunk7MGZ7D62cjs.pitchClassOf.call(void 0, key.rootPc);
55
+ const pcs = [];
56
+ for (let n = 0; n < 12; n += 1) {
57
+ if ((key.modeMask12 >> n & 1) === 1) {
58
+ pcs.push((root + n) % 12);
59
+ }
60
+ }
61
+ return pcs;
62
+ }
63
+
64
+ // src/theory/scale/masks.ts
65
+ function maskFromOffsets(offsets) {
66
+ let mask = 1;
67
+ for (const offset of offsets) {
68
+ mask |= 1 << (offset % 12 + 12) % 12;
69
+ }
70
+ return mask;
71
+ }
72
+ var MAJOR_MASK = 2741;
73
+ var NATURAL_MINOR_MASK = 1453;
74
+ var HARMONIC_MINOR_MASK = maskFromOffsets([0, 2, 3, 5, 7, 8, 11]);
75
+ var MELODIC_MINOR_MASK = maskFromOffsets([0, 2, 3, 5, 7, 9, 11]);
76
+ var DORIAN_MASK = maskFromOffsets([0, 2, 3, 5, 7, 9, 10]);
77
+ var PHRYGIAN_MASK = maskFromOffsets([0, 1, 3, 5, 7, 8, 10]);
78
+ var LYDIAN_MASK = maskFromOffsets([0, 2, 4, 6, 7, 9, 11]);
79
+ var MIXOLYDIAN_MASK = maskFromOffsets([0, 2, 4, 5, 7, 9, 10]);
80
+ var LOCRIAN_MASK = maskFromOffsets([0, 1, 3, 5, 6, 8, 10]);
81
+ var MAJOR_PENTATONIC_MASK = maskFromOffsets([0, 2, 4, 7, 9]);
82
+ var MINOR_PENTATONIC_MASK = maskFromOffsets([0, 3, 5, 7, 10]);
83
+ var BLUES_MASK = maskFromOffsets([0, 3, 5, 6, 7, 10]);
84
+ var WHOLE_TONE_MASK = maskFromOffsets([0, 2, 4, 6, 8, 10]);
85
+ var OCTATONIC_HALF_WHOLE_MASK = maskFromOffsets([0, 1, 3, 4, 6, 7, 9, 10]);
86
+ var OCTATONIC_WHOLE_HALF_MASK = maskFromOffsets([0, 2, 3, 5, 6, 8, 9, 11]);
87
+ var CHROMATIC_MASK = 4095;
88
+ var LYDIAN_DOMINANT_MASK = maskFromOffsets([0, 2, 4, 6, 7, 9, 10]);
89
+ var MIXOLYDIAN_B13_MASK = maskFromOffsets([0, 2, 4, 5, 7, 8, 10]);
90
+ var LOCRIAN_NATURAL2_MASK = maskFromOffsets([0, 2, 3, 5, 6, 8, 10]);
91
+ var ALTERED_MASK = maskFromOffsets([0, 1, 3, 4, 6, 8, 10]);
92
+ var PHRYGIAN_DOMINANT_MASK = maskFromOffsets([0, 1, 4, 5, 7, 8, 10]);
93
+ var NAMED_SCALES = Object.freeze({
94
+ major: MAJOR_MASK,
95
+ ionian: MAJOR_MASK,
96
+ naturalMinor: NATURAL_MINOR_MASK,
97
+ aeolian: NATURAL_MINOR_MASK,
98
+ harmonicMinor: HARMONIC_MINOR_MASK,
99
+ melodicMinor: MELODIC_MINOR_MASK,
100
+ dorian: DORIAN_MASK,
101
+ phrygian: PHRYGIAN_MASK,
102
+ lydian: LYDIAN_MASK,
103
+ mixolydian: MIXOLYDIAN_MASK,
104
+ locrian: LOCRIAN_MASK,
105
+ lydianDominant: LYDIAN_DOMINANT_MASK,
106
+ mixolydianB13: MIXOLYDIAN_B13_MASK,
107
+ locrianNatural2: LOCRIAN_NATURAL2_MASK,
108
+ altered: ALTERED_MASK,
109
+ phrygianDominant: PHRYGIAN_DOMINANT_MASK,
110
+ majorPentatonic: MAJOR_PENTATONIC_MASK,
111
+ minorPentatonic: MINOR_PENTATONIC_MASK,
112
+ blues: BLUES_MASK,
113
+ wholeTone: WHOLE_TONE_MASK,
114
+ octatonicHalfWhole: OCTATONIC_HALF_WHOLE_MASK,
115
+ octatonicWholeHalf: OCTATONIC_WHOLE_HALF_MASK,
116
+ chromatic: CHROMATIC_MASK
117
+ });
118
+ function namedScaleMask(name) {
119
+ return Object.hasOwn(NAMED_SCALES, name) ? NAMED_SCALES[name] : void 0;
120
+ }
121
+ function requireScaleMask(name, label = "scale") {
122
+ const mask = namedScaleMask(name);
123
+ if (mask === void 0) {
124
+ throw new (0, _chunk7MGZ7D62cjs.InvalidInputError)(`Unknown ${label}: ${String(name)}`);
125
+ }
126
+ return mask;
127
+ }
128
+
129
+ // src/theory/scale/key.ts
130
+ function majorKey(rootPc) {
131
+ return { rootPc: (rootPc % 12 + 12) % 12, modeMask12: MAJOR_MASK };
132
+ }
133
+ function minorKey(rootPc) {
134
+ return { rootPc: (rootPc % 12 + 12) % 12, modeMask12: NATURAL_MINOR_MASK };
135
+ }
136
+ function scaleByName(name, rootPc) {
137
+ return { rootPc: (rootPc % 12 + 12) % 12, modeMask12: requireScaleMask(name) };
138
+ }
139
+
140
+ // src/theory/chord/index.ts
141
+ var QUALITY_INTERVALS = {
142
+ maj: [0, 4, 7],
143
+ min: [0, 3, 7],
144
+ dim: [0, 3, 6],
145
+ aug: [0, 4, 8],
146
+ majb5: [0, 4, 6],
147
+ maj7: [0, 4, 7, 11],
148
+ min7: [0, 3, 7, 10],
149
+ dom7: [0, 4, 7, 10],
150
+ dim7: [0, 3, 6, 9],
151
+ m7b5: [0, 3, 6, 10],
152
+ minMaj7: [0, 3, 7, 11],
153
+ aug7: [0, 4, 8, 10],
154
+ augMaj7: [0, 4, 8, 11],
155
+ "6": [0, 4, 7, 9],
156
+ min6: [0, 3, 7, 9],
157
+ "6/9": [0, 4, 7, 9, 14],
158
+ sus2: [0, 2, 7],
159
+ sus4: [0, 5, 7],
160
+ add9: [0, 4, 7, 14],
161
+ add11: [0, 4, 7, 17],
162
+ maj9: [0, 4, 7, 11, 14],
163
+ min9: [0, 3, 7, 10, 14],
164
+ dom9: [0, 4, 7, 10, 14],
165
+ "7b9": [0, 4, 7, 10, 13],
166
+ "7#9": [0, 4, 7, 10, 15],
167
+ "7#11": [0, 4, 7, 10, 18],
168
+ "7b13": [0, 4, 7, 10, 20],
169
+ "11": [0, 7, 10, 14, 17],
170
+ "13": [0, 4, 7, 10, 14, 21],
171
+ "5": [0, 7],
172
+ "7sus4": [0, 5, 7, 10],
173
+ "7b5": [0, 4, 6, 10],
174
+ // The altered dominant as it is voiced from a lead sheet: a dominant seventh
175
+ // with a raised fifth and a raised ninth. The full seven-note altered stack
176
+ // has no seven-letter spelling, so it is not what this symbol denotes.
177
+ "7alt": [0, 4, 8, 10, 15],
178
+ "13b9": [0, 4, 7, 10, 13, 21],
179
+ maj13: [0, 4, 7, 11, 14, 21],
180
+ "maj7#11": [0, 4, 7, 11, 18],
181
+ min11: [0, 3, 7, 10, 14, 17],
182
+ min13: [0, 3, 7, 10, 14, 21],
183
+ minAdd9: [0, 3, 7, 14],
184
+ "min6/9": [0, 3, 7, 9, 14]
185
+ };
186
+ function chordQualities() {
187
+ return Object.keys(QUALITY_INTERVALS);
188
+ }
189
+ function chordFromDegree(degree, ext, key) {
190
+ _chunk7MGZ7D62cjs.assertInteger.call(void 0, degree, "chord degree", -1e3, 1e3);
191
+ if (!Object.hasOwn(QUALITY_INTERVALS, ext)) {
192
+ throw new (0, _chunk7MGZ7D62cjs.InvalidInputError)(`Unknown chord quality: ${String(ext)}`);
193
+ }
194
+ const tones = scaleTonesInDegreeOrder(key);
195
+ const length = tones.length;
196
+ const index = length > 0 ? (degree % length + length) % length : 0;
197
+ const rootPc = _nullishCoalesce(tones[index], () => ( _chunk7MGZ7D62cjs.pitchClassOf.call(void 0, key.rootPc)));
198
+ return { rootPc, quality: ext, intervals: [...QUALITY_INTERVALS[ext]] };
199
+ }
200
+ function makeChord(rootPc, quality, bassPc) {
201
+ _chunk7MGZ7D62cjs.assertFiniteNumber.call(void 0, rootPc, "chord rootPc");
202
+ if (bassPc !== void 0) {
203
+ _chunk7MGZ7D62cjs.assertFiniteNumber.call(void 0, bassPc, "chord bassPc");
204
+ }
205
+ if (!Object.hasOwn(QUALITY_INTERVALS, quality)) {
206
+ throw new (0, _chunk7MGZ7D62cjs.InvalidInputError)(`Unknown chord quality: ${String(quality)}`);
207
+ }
208
+ const chord = {
209
+ rootPc: _chunk7MGZ7D62cjs.pitchClassOf.call(void 0, rootPc),
210
+ quality,
211
+ intervals: [..._nullishCoalesce(QUALITY_INTERVALS[quality], () => ( []))]
212
+ };
213
+ if (bassPc !== void 0) {
214
+ chord.bassPc = _chunk7MGZ7D62cjs.pitchClassOf.call(void 0, bassPc);
215
+ }
216
+ return chord;
217
+ }
218
+ function transposeChord(chord, semitones) {
219
+ _chunk7MGZ7D62cjs.assertFiniteNumber.call(void 0, semitones, "semitones");
220
+ const steps = Math.round(semitones);
221
+ const moved = {
222
+ rootPc: _chunk7MGZ7D62cjs.pitchClassOf.call(void 0, chord.rootPc + steps),
223
+ quality: chord.quality,
224
+ intervals: [...chord.intervals]
225
+ };
226
+ if (chord.bassPc !== void 0) {
227
+ moved.bassPc = _chunk7MGZ7D62cjs.pitchClassOf.call(void 0, chord.bassPc + steps);
228
+ }
229
+ return moved;
230
+ }
231
+ function chordPitchClasses(chord, opts = {}) {
232
+ const set = /* @__PURE__ */ new Set();
233
+ for (const interval of chord.intervals) {
234
+ set.add(_chunk7MGZ7D62cjs.pitchClassOf.call(void 0, chord.rootPc + interval));
235
+ }
236
+ if (opts.includeBass !== false && chord.bassPc !== void 0) {
237
+ set.add(_chunk7MGZ7D62cjs.pitchClassOf.call(void 0, chord.bassPc));
238
+ }
239
+ return [...set].sort((a, b) => a - b);
240
+ }
241
+ function intervalAboveRoot(pitch, chord) {
242
+ return ((_chunk7MGZ7D62cjs.pitchClassOf.call(void 0, pitch) - _chunk7MGZ7D62cjs.pitchClassOf.call(void 0, chord.rootPc)) % 12 + 12) % 12;
243
+ }
244
+ function isChordMember(pitch, chord) {
245
+ return chord === null ? false : chordPitchClasses(chord).includes(_chunk7MGZ7D62cjs.pitchClassOf.call(void 0, pitch));
246
+ }
247
+ function chordToneOffsets(chord) {
248
+ return new Set(chord.intervals.map((i) => (i % 12 + 12) % 12));
249
+ }
250
+ function chordToneRole(pitch, chord) {
251
+ const interval = (_chunk7MGZ7D62cjs.pitchClassOf.call(void 0, pitch) - _chunk7MGZ7D62cjs.pitchClassOf.call(void 0, chord.rootPc) + 12) % 12;
252
+ const tones = chordToneOffsets(chord);
253
+ if (interval === 0) {
254
+ return "root";
255
+ }
256
+ if (interval === 3 || interval === 4) {
257
+ return tones.has(interval) ? "third" : null;
258
+ }
259
+ if (interval === 7) {
260
+ return tones.has(7) ? "fifth" : null;
261
+ }
262
+ if (interval === 6) {
263
+ return tones.has(6) && !tones.has(7) ? "fifth" : null;
264
+ }
265
+ if (interval === 8) {
266
+ return tones.has(8) && !tones.has(7) ? "fifth" : null;
267
+ }
268
+ if (interval === 9) {
269
+ const hasHigherSeventh = tones.has(10) || tones.has(11);
270
+ const isDiminishedSeventh = tones.has(3) && tones.has(6) && !hasHigherSeventh;
271
+ if (isDiminishedSeventh) {
272
+ return "seventh";
273
+ }
274
+ if (tones.has(9) && !hasHigherSeventh) {
275
+ return "sixth";
276
+ }
277
+ return null;
278
+ }
279
+ if (interval === 10 || interval === 11) {
280
+ return tones.has(interval) ? "seventh" : null;
281
+ }
282
+ return null;
283
+ }
284
+ function classifyTriad(thirdIc, fifthIc) {
285
+ if (thirdIc === 4 && fifthIc === 8) {
286
+ return "aug";
287
+ }
288
+ if (thirdIc === 4 && fifthIc === 6) {
289
+ return "majb5";
290
+ }
291
+ if (thirdIc === 3 && fifthIc === 6) {
292
+ return "dim";
293
+ }
294
+ if (thirdIc === 3) {
295
+ return "min";
296
+ }
297
+ return "maj";
298
+ }
299
+ function classifySeventh(thirdIc, fifthIc, seventhIc) {
300
+ if (thirdIc === 3 && fifthIc === 6 && seventhIc === 9) {
301
+ return "dim7";
302
+ }
303
+ if (thirdIc === 3 && fifthIc === 6 && seventhIc === 10) {
304
+ return "m7b5";
305
+ }
306
+ if (thirdIc === 3 && seventhIc === 11) {
307
+ return "minMaj7";
308
+ }
309
+ if (thirdIc === 3) {
310
+ return "min7";
311
+ }
312
+ if (fifthIc === 8 && seventhIc === 10) {
313
+ return "aug7";
314
+ }
315
+ if (fifthIc === 8 && seventhIc === 11) {
316
+ return "augMaj7";
317
+ }
318
+ if (seventhIc === 11) {
319
+ return "maj7";
320
+ }
321
+ return "dom7";
322
+ }
323
+ function stackThirds(degree, key, size) {
324
+ const tones = scaleTonesInDegreeOrder(key);
325
+ const length = tones.length;
326
+ if (length !== 7) {
327
+ throw new (0, _chunk7MGZ7D62cjs.InvalidInputError)(
328
+ `diatonic chord stacking requires a heptatonic scale (received ${length} tones)`
329
+ );
330
+ }
331
+ const idx = (step) => ((degree + step) % length + length) % length;
332
+ const rootPc = _nullishCoalesce(tones[idx(0)], () => ( _chunk7MGZ7D62cjs.pitchClassOf.call(void 0, key.rootPc)));
333
+ const offsets = [0, 2, 4, 6].slice(0, size).map((step) => {
334
+ const pc = _nullishCoalesce(tones[idx(step)], () => ( rootPc));
335
+ return ((pc - rootPc) % 12 + 12) % 12;
336
+ });
337
+ const thirdIc = _nullishCoalesce(offsets[1], () => ( 4));
338
+ const fifthIc = _nullishCoalesce(offsets[2], () => ( 7));
339
+ const quality = size === 3 ? classifyTriad(thirdIc, fifthIc) : classifySeventh(thirdIc, fifthIc, _nullishCoalesce(offsets[3], () => ( 11)));
340
+ return { rootPc, quality, intervals: offsets };
341
+ }
342
+ function diatonicTriad(degree, key) {
343
+ return stackThirds(degree, key, 3);
344
+ }
345
+ function diatonicSeventh(degree, key) {
346
+ return stackThirds(degree, key, 4);
347
+ }
348
+
349
+ // src/theory/counterpoint/index.ts
350
+ function createsVoiceCrossing(upperPitch, lowerPitch) {
351
+ return upperPitch < lowerPitch;
352
+ }
353
+ function createsVerticalDissonance(a, b, twoVoice) {
354
+ return !_chunk7MGZ7D62cjs.isConsonantInterval.call(void 0, a - b, twoVoice);
355
+ }
356
+ function isForbiddenMelodicLeap(prev, cur) {
357
+ const semis = Math.abs(cur - prev);
358
+ return semis === 6 || semis === 10 || semis === 11 || semis > 12;
359
+ }
360
+ function simpleClass(semitones) {
361
+ return Math.abs(semitones) % 12;
362
+ }
363
+ function isPerfectClass(cls) {
364
+ return cls === 0 || cls === 7;
365
+ }
366
+ function similarMotion(aMove, bMove) {
367
+ return aMove !== 0 && bMove !== 0 && aMove > 0 === bMove > 0;
368
+ }
369
+ function bothVoicesMove(aMove, bMove) {
370
+ return aMove !== 0 && bMove !== 0;
371
+ }
372
+ function createsParallelPerfect(aPrev, aCur, bPrev, bCur) {
373
+ if (!bothVoicesMove(aCur - aPrev, bCur - bPrev)) {
374
+ return false;
375
+ }
376
+ const nowClass = simpleClass(aCur - bCur);
377
+ const prevClass = simpleClass(aPrev - bPrev);
378
+ return isPerfectClass(nowClass) && nowClass === prevClass;
379
+ }
380
+ function createsParallelOctave(aPrev, aCur, bPrev, bCur) {
381
+ if (!similarMotion(aCur - aPrev, bCur - bPrev)) {
382
+ return false;
383
+ }
384
+ return simpleClass(aCur - bCur) === 0 && simpleClass(aPrev - bPrev) === 0;
385
+ }
386
+ function createsParallelUnison(aPrev, aCur, bPrev, bCur) {
387
+ if (aCur === aPrev || bCur === bPrev) {
388
+ return false;
389
+ }
390
+ return aCur === bCur && aPrev === bPrev;
391
+ }
392
+ function createsHiddenParallelPerfect(aPrev, aCur, bPrev, bCur) {
393
+ const aMove = aCur - aPrev;
394
+ const bMove = bCur - bPrev;
395
+ if (!similarMotion(aMove, bMove)) {
396
+ return false;
397
+ }
398
+ const nowClass = simpleClass(aCur - bCur);
399
+ const prevClass = simpleClass(aPrev - bPrev);
400
+ if (!isPerfectClass(nowClass) || prevClass === nowClass) {
401
+ return false;
402
+ }
403
+ const upperMove = aCur >= bCur ? aMove : bMove;
404
+ if (Math.abs(upperMove) <= 2) {
405
+ return false;
406
+ }
407
+ return true;
408
+ }
409
+ function createsVoiceOverlap(upperPrev, upperCur, lowerPrev, lowerCur) {
410
+ return upperCur < lowerPrev || lowerCur > upperPrev;
411
+ }
412
+ function exceedsSpacing(upperPitch, lowerPitch, maxSemitones = 12) {
413
+ return Math.abs(upperPitch - lowerPitch) > maxSemitones;
414
+ }
415
+ function isLeadingToneResolution(prev, cur, key) {
416
+ const tonic = _chunk7MGZ7D62cjs.pitchClassOf.call(void 0, key.rootPc);
417
+ const leading = (tonic + 11) % 12;
418
+ if (_chunk7MGZ7D62cjs.pitchClassOf.call(void 0, prev) !== leading || _chunk7MGZ7D62cjs.pitchClassOf.call(void 0, cur) !== tonic) {
419
+ return false;
420
+ }
421
+ return cur > prev && cur - prev <= 2;
422
+ }
423
+
424
+
425
+
426
+
427
+
428
+
429
+
430
+
431
+
432
+
433
+
434
+
435
+
436
+
437
+
438
+
439
+
440
+
441
+
442
+
443
+
444
+
445
+
446
+
447
+
448
+
449
+
450
+
451
+
452
+
453
+
454
+
455
+
456
+
457
+
458
+
459
+
460
+
461
+
462
+
463
+
464
+
465
+
466
+
467
+
468
+
469
+
470
+
471
+
472
+
473
+
474
+
475
+
476
+
477
+
478
+ exports.isScaleTone = isScaleTone; exports.nearestScaleTone = nearestScaleTone; exports.pitchToScaleDegree = pitchToScaleDegree; exports.diatonicPitchClasses = diatonicPitchClasses; exports.scaleTonesInDegreeOrder = scaleTonesInDegreeOrder; exports.maskFromOffsets = maskFromOffsets; exports.MAJOR_MASK = MAJOR_MASK; exports.NATURAL_MINOR_MASK = NATURAL_MINOR_MASK; exports.HARMONIC_MINOR_MASK = HARMONIC_MINOR_MASK; exports.MELODIC_MINOR_MASK = MELODIC_MINOR_MASK; exports.DORIAN_MASK = DORIAN_MASK; exports.PHRYGIAN_MASK = PHRYGIAN_MASK; exports.LYDIAN_MASK = LYDIAN_MASK; exports.MIXOLYDIAN_MASK = MIXOLYDIAN_MASK; exports.LOCRIAN_MASK = LOCRIAN_MASK; exports.MAJOR_PENTATONIC_MASK = MAJOR_PENTATONIC_MASK; exports.MINOR_PENTATONIC_MASK = MINOR_PENTATONIC_MASK; exports.BLUES_MASK = BLUES_MASK; exports.WHOLE_TONE_MASK = WHOLE_TONE_MASK; exports.OCTATONIC_HALF_WHOLE_MASK = OCTATONIC_HALF_WHOLE_MASK; exports.OCTATONIC_WHOLE_HALF_MASK = OCTATONIC_WHOLE_HALF_MASK; exports.CHROMATIC_MASK = CHROMATIC_MASK; exports.LYDIAN_DOMINANT_MASK = LYDIAN_DOMINANT_MASK; exports.MIXOLYDIAN_B13_MASK = MIXOLYDIAN_B13_MASK; exports.LOCRIAN_NATURAL2_MASK = LOCRIAN_NATURAL2_MASK; exports.ALTERED_MASK = ALTERED_MASK; exports.PHRYGIAN_DOMINANT_MASK = PHRYGIAN_DOMINANT_MASK; exports.NAMED_SCALES = NAMED_SCALES; exports.namedScaleMask = namedScaleMask; exports.requireScaleMask = requireScaleMask; exports.majorKey = majorKey; exports.minorKey = minorKey; exports.scaleByName = scaleByName; exports.chordQualities = chordQualities; exports.chordFromDegree = chordFromDegree; exports.makeChord = makeChord; exports.transposeChord = transposeChord; exports.chordPitchClasses = chordPitchClasses; exports.intervalAboveRoot = intervalAboveRoot; exports.isChordMember = isChordMember; exports.chordToneRole = chordToneRole; exports.diatonicTriad = diatonicTriad; exports.diatonicSeventh = diatonicSeventh; exports.createsVoiceCrossing = createsVoiceCrossing; exports.createsVerticalDissonance = createsVerticalDissonance; exports.isForbiddenMelodicLeap = isForbiddenMelodicLeap; exports.createsParallelPerfect = createsParallelPerfect; exports.createsParallelOctave = createsParallelOctave; exports.createsParallelUnison = createsParallelUnison; exports.createsHiddenParallelPerfect = createsHiddenParallelPerfect; exports.createsVoiceOverlap = createsVoiceOverlap; exports.exceedsSpacing = exceedsSpacing; exports.isLeadingToneResolution = isLeadingToneResolution;
479
+ //# sourceMappingURL=chunk-FNDWXRRI.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/libcantus/libcantus/dist/chunk-FNDWXRRI.cjs","../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"],"names":[],"mappings":"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,IAAA;AACT,EAAA;AACwD,EAAA;AAC/C,IAAA;AACT,EAAA;AACuC,EAAA;AAC9B,IAAA;AACT,EAAA;AACmB,EAAA;AACV,IAAA;AACT,EAAA;AACuC,EAAA;AAC9B,IAAA;AACT,EAAA;AACuC,EAAA;AAC9B,IAAA;AACT,EAAA;AACsB,EAAA;AACb,IAAA;AACT,EAAA;AACO,EAAA;AACT;AAGwE;AAC7B,EAAA;AACpB,EAAA;AACH,EAAA;AACN,IAAA;AACR,MAAA;AACF,IAAA;AACF,EAAA;AACsE,EAAA;AACjB,EAAA;AACK,EAAA;AACzB,IAAA;AACM,IAAA;AACtC,EAAA;AAC6B,EAAA;AACA,EAAA;AAIxB,EAAA;AACuC,EAAA;AAC/C;AAyBoE;AACjC,EAAA;AACnC;AAasE;AACnC,EAAA;AACnC;AJtKkE;AACA;AKhVoB;AAChE,EAAA;AACtB;AAW4F;AAC/C,EAAA;AAC7C;AAc2E;AACxC,EAAA;AAC6B,EAAA;AAChE;AAGgD;AACjB,EAAA;AAC/B;AAG8C;AAChB,EAAA;AAC9B;AAG8D;AACD,EAAA;AAC7D;AAG+D;AAC7B,EAAA;AAClC;AAuBW;AACwC,EAAA;AACxC,IAAA;AACT,EAAA;AACwC,EAAA;AACG,EAAA;AACK,EAAA;AAClD;AAoBW;AACuC,EAAA;AACvC,IAAA;AACT,EAAA;AAC6D,EAAA;AAC/D;AAaW;AAC6B,EAAA;AAC7B,IAAA;AACT,EAAA;AACkC,EAAA;AACpC;AAiBW;AACY,EAAA;AACA,EAAA;AACa,EAAA;AACzB,IAAA;AACT,EAAA;AACwC,EAAA;AACG,EAAA;AAIc,EAAA;AAChD,IAAA;AACT,EAAA;AACyC,EAAA;AACX,EAAA;AACrB,IAAA;AACT,EAAA;AACO,EAAA;AACT;AAiBE;AAE0C,EAAA;AAC5C;AAYsF;AACzC,EAAA;AAC7C;AAW2F;AACtD,EAAA;AACJ,EAAA;AACyB,EAAA;AAC/C,IAAA;AACT,EAAA;AACmC,EAAA;AACrC;ALqMkE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/libcantus/libcantus/dist/chunk-FNDWXRRI.cjs","sourcesContent":[null,"import { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.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 = Math.round(pitch);\n for (let distance = 0; distance < 12; distance += 1) {\n const lower = base - distance;\n if (isScaleTone(lower, key)) {\n return lower;\n }\n const higher = base + distance;\n if (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","/**\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 */\nimport { InvalidInputError } from '../../core/errors/index.js';\nexport function maskFromOffsets(offsets: readonly number[]): number {\n let mask = 1;\n for (const offset of offsets) {\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 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: ((rootPc % 12) + 12) % 12, 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: ((rootPc % 12) + 12) % 12, 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: ((rootPc % 12) + 12) % 12, 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 | '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 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) ? '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 (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 (seventhIc === 11) {\n return 'maj7';\n }\n return 'dom7';\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 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 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 Math.abs(semitones) % 12;\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"]}