@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,498 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3;// src/core/errors/index.ts
2
+ var InvalidInputError = (_class = class extends RangeError {constructor(...args) { super(...args); _class.prototype.__init.call(this);_class.prototype.__init2.call(this); }
3
+ __init() {this.code = "INVALID_INPUT"}
4
+ __init2() {this.name = "InvalidInputError"}
5
+ }, _class);
6
+ var NoSolutionError = (_class2 = class extends Error {
7
+ __init3() {this.code = "NO_SOLUTION"}
8
+ __init4() {this.name = "NoSolutionError"}
9
+ /**
10
+ * Where in a sequence the failure occurred, when the operation was working
11
+ * through one — the chord index of a progression, say.
12
+ */
13
+
14
+ constructor(message, options = {}) {
15
+ super(message);_class2.prototype.__init3.call(this);_class2.prototype.__init4.call(this);;
16
+ this.at = options.at;
17
+ }
18
+ }, _class2);
19
+ var BudgetExceededError = (_class3 = class extends RangeError {constructor(...args2) { super(...args2); _class3.prototype.__init5.call(this);_class3.prototype.__init6.call(this); }
20
+ __init5() {this.code = "BUDGET_EXCEEDED"}
21
+ __init6() {this.name = "BudgetExceededError"}
22
+ }, _class3);
23
+ function isLibcantusError(value) {
24
+ return value instanceof InvalidInputError || value instanceof NoSolutionError || value instanceof BudgetExceededError;
25
+ }
26
+
27
+ // src/core/meter/internal.ts
28
+ function unitBeatsOf(ts) {
29
+ return 4 / ts.denominator;
30
+ }
31
+ function isCompoundNumerator(numerator) {
32
+ return numerator % 3 === 0 && numerator > 3;
33
+ }
34
+ function groupingSumOf(ts) {
35
+ const grouping = ts.grouping;
36
+ if (grouping === void 0) {
37
+ return void 0;
38
+ }
39
+ let sum = 0;
40
+ for (const entry of grouping) {
41
+ sum += entry;
42
+ }
43
+ return sum;
44
+ }
45
+ function isAdditiveReading(ts) {
46
+ const sum = groupingSumOf(ts);
47
+ return sum !== void 0 && isCompoundNumerator(ts.numerator) && sum === ts.numerator && sum !== 0;
48
+ }
49
+ function pulseCountOf(ts) {
50
+ if (isAdditiveReading(ts)) {
51
+ return ts.numerator;
52
+ }
53
+ return isCompoundNumerator(ts.numerator) ? ts.numerator / 3 : ts.numerator;
54
+ }
55
+ function pulseBeatsOf(ts) {
56
+ return ts.numerator * unitBeatsOf(ts) / pulseCountOf(ts);
57
+ }
58
+ function barBeatsOf(ts) {
59
+ return ts.numerator * unitBeatsOf(ts);
60
+ }
61
+
62
+ // src/core/validation/index.ts
63
+ var DEFAULT_GENERATION_BUDGET = 1e6;
64
+ function assertFiniteNumber(value, name) {
65
+ if (!Number.isFinite(value)) {
66
+ throw new InvalidInputError(`${name} must be finite; received ${value}`);
67
+ }
68
+ return value;
69
+ }
70
+ function assertInteger(value, name, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) {
71
+ assertFiniteNumber(value, name);
72
+ if (!Number.isSafeInteger(value) || value < min || value > max) {
73
+ throw new InvalidInputError(
74
+ `${name} must be an integer in [${min}, ${max}]; received ${value}`
75
+ );
76
+ }
77
+ return value;
78
+ }
79
+ function dropSilentNotes(events) {
80
+ return events.filter((event) => event !== void 0 && event.durationBeat > 0);
81
+ }
82
+ function assertOneOf(value, allowed, name) {
83
+ if (typeof value !== "string" || !allowed.includes(value)) {
84
+ throw new InvalidInputError(
85
+ `${name} must be one of ${allowed.join(", ")}; received ${JSON.stringify(value)}`
86
+ );
87
+ }
88
+ return value;
89
+ }
90
+ function assertPositiveInt(value, name, max = DEFAULT_GENERATION_BUDGET) {
91
+ return assertInteger(value, name, 1, max);
92
+ }
93
+ function assertRange(value, min, max, name) {
94
+ assertFiniteNumber(value, name);
95
+ if (value < min || value > max) {
96
+ throw new InvalidInputError(`${name} must be in [${min}, ${max}]; received ${value}`);
97
+ }
98
+ return value;
99
+ }
100
+ function assertGenerationBudget(estimated, name, limit = DEFAULT_GENERATION_BUDGET) {
101
+ const cap = _nullishCoalesce(limit, () => ( DEFAULT_GENERATION_BUDGET));
102
+ assertFiniteNumber(estimated, name);
103
+ assertPositiveInt(cap, `${name} limit`, Number.MAX_SAFE_INTEGER);
104
+ if (estimated < 0) {
105
+ throw new InvalidInputError(`${name} must not be negative; received ${estimated}`);
106
+ }
107
+ if (estimated > cap) {
108
+ throw new BudgetExceededError(
109
+ `${name} exceeds the generation budget ${cap}; received ${estimated}`
110
+ );
111
+ }
112
+ return estimated;
113
+ }
114
+ function assertTimeSignature(ts, name = "time signature") {
115
+ assertPositiveInt(ts.numerator, `${name}.numerator`);
116
+ assertPositiveInt(ts.denominator, `${name}.denominator`);
117
+ if (ts.grouping !== void 0) {
118
+ if (ts.grouping.length === 0) {
119
+ throw new InvalidInputError(`${name}.grouping must not be empty`);
120
+ }
121
+ let sum = 0;
122
+ for (let index = 0; index < ts.grouping.length; index += 1) {
123
+ sum += assertPositiveInt(_nullishCoalesce(ts.grouping[index], () => ( Number.NaN)), `${name}.grouping[${index}]`);
124
+ }
125
+ const pulses = isCompoundNumerator(ts.numerator) ? ts.numerator / 3 : ts.numerator;
126
+ if (sum !== pulses && sum !== ts.numerator) {
127
+ const accepted = pulses === ts.numerator ? `${pulses}` : `${pulses} (pulses) or ${ts.numerator} (units)`;
128
+ throw new InvalidInputError(`${name}.grouping must sum to ${accepted}; received ${sum}`);
129
+ }
130
+ }
131
+ return ts;
132
+ }
133
+ function assertNoteEvent(event, name = "note event", options = {}) {
134
+ assertInteger(event.pitch, `${name}.pitch`, 0, 127);
135
+ assertRange(event.startBeat, 0, Number.MAX_SAFE_INTEGER, `${name}.startBeat`);
136
+ assertFiniteNumber(event.durationBeat, `${name}.durationBeat`);
137
+ if (!options.allowNonPositiveDuration && event.durationBeat <= 0) {
138
+ throw new InvalidInputError(
139
+ `${name}.durationBeat must be positive; received ${event.durationBeat}`
140
+ );
141
+ }
142
+ assertRange(
143
+ event.durationBeat,
144
+ -Number.MAX_SAFE_INTEGER,
145
+ Number.MAX_SAFE_INTEGER,
146
+ `${name}.durationBeat`
147
+ );
148
+ if (event.velocity !== void 0) {
149
+ assertRange(event.velocity, 0, 127, `${name}.velocity`);
150
+ }
151
+ return event;
152
+ }
153
+ function assertNoteEvents(events, name = "note events", options = {}) {
154
+ if (!Array.isArray(events)) {
155
+ throw new TypeError(`${name} must be an array; received ${typeof events}`);
156
+ }
157
+ assertGenerationBudget(events.length, `${name} count`, options.budget);
158
+ for (let index = 0; index < events.length; index += 1) {
159
+ const event = events[index];
160
+ if (event === void 0) {
161
+ throw new InvalidInputError(`${name}[${index}] must be a note event; received undefined`);
162
+ }
163
+ assertNoteEvent(event, `${name}[${index}]`, options);
164
+ }
165
+ return events;
166
+ }
167
+
168
+ // src/core/pitch/index.ts
169
+ var LETTER_SEMITONES = [0, 2, 4, 5, 7, 9, 11];
170
+ var MAX_ALTER = 6;
171
+ function assertNote(note, name) {
172
+ assertInteger(note.letter, `${name}.letter`, -1e3, 1e3);
173
+ assertInteger(note.alter, `${name}.alter`, -MAX_ALTER, MAX_ALTER);
174
+ if (note.octave !== void 0) {
175
+ assertInteger(note.octave, `${name}.octave`, -100, 100);
176
+ }
177
+ return note;
178
+ }
179
+ var LETTER_NAMES = ["C", "D", "E", "F", "G", "A", "B"];
180
+ function pitchClassOf(value) {
181
+ assertFiniteNumber(value, "pitch");
182
+ return (Math.round(value) % 12 + 12) % 12;
183
+ }
184
+ var mod12 = pitchClassOf;
185
+ function diatonicLetterOf(value) {
186
+ return (Math.round(value) % 7 + 7) % 7;
187
+ }
188
+ function naturalPitchClassOf(letter) {
189
+ return _nullishCoalesce(LETTER_SEMITONES[diatonicLetterOf(letter)], () => ( 0));
190
+ }
191
+ var mod7 = diatonicLetterOf;
192
+ function parseNote(text) {
193
+ const match = /^([A-Ga-g])([#x]*|b*)(-?\d+)?$/.exec(text.trim());
194
+ if (!match) {
195
+ throw new InvalidInputError(`Invalid note: ${text}`);
196
+ }
197
+ const letterGlyph = (_nullishCoalesce(match[1], () => ( ""))).toUpperCase();
198
+ const letter = LETTER_NAMES.indexOf(letterGlyph);
199
+ let alter = 0;
200
+ for (const ch of _nullishCoalesce(match[2], () => ( ""))) {
201
+ if (ch === "#") {
202
+ alter += 1;
203
+ } else if (ch === "x") {
204
+ alter += 2;
205
+ } else if (ch === "b") {
206
+ alter -= 1;
207
+ }
208
+ }
209
+ const note = { letter, alter };
210
+ if (match[3] !== void 0) {
211
+ note.octave = Number.parseInt(match[3], 10);
212
+ }
213
+ return assertNote(note, `note ${text}`);
214
+ }
215
+ function formatAlter(alter) {
216
+ if (alter > 0) {
217
+ return "#".repeat(alter);
218
+ }
219
+ if (alter < 0) {
220
+ return "b".repeat(-alter);
221
+ }
222
+ return "";
223
+ }
224
+ function formatNote(note) {
225
+ assertNote(note, "note");
226
+ const glyph = _nullishCoalesce(LETTER_NAMES[mod7(note.letter)], () => ( "C"));
227
+ const octave = note.octave === void 0 ? "" : String(note.octave);
228
+ return `${glyph}${formatAlter(note.alter)}${octave}`;
229
+ }
230
+ function noteToPitchClass(note) {
231
+ assertNote(note, "note");
232
+ const natural = _nullishCoalesce(LETTER_SEMITONES[mod7(note.letter)], () => ( 0));
233
+ return mod12(natural + note.alter);
234
+ }
235
+ function noteToMidi(note) {
236
+ assertNote(note, "note");
237
+ if (note.octave === void 0) {
238
+ throw new InvalidInputError("noteToMidi requires an octave");
239
+ }
240
+ const natural = _nullishCoalesce(LETTER_SEMITONES[mod7(note.letter)], () => ( 0));
241
+ return (note.octave + 1) * 12 + natural + note.alter;
242
+ }
243
+ var SHARP_SPELLING = [
244
+ [0, 0],
245
+ [0, 1],
246
+ [1, 0],
247
+ [1, 1],
248
+ [2, 0],
249
+ [3, 0],
250
+ [3, 1],
251
+ [4, 0],
252
+ [4, 1],
253
+ [5, 0],
254
+ [5, 1],
255
+ [6, 0]
256
+ ];
257
+ var FLAT_SPELLING = [
258
+ [0, 0],
259
+ [1, -1],
260
+ [1, 0],
261
+ [2, -1],
262
+ [2, 0],
263
+ [3, 0],
264
+ [4, -1],
265
+ [4, 0],
266
+ [5, -1],
267
+ [5, 0],
268
+ [6, -1],
269
+ [6, 0]
270
+ ];
271
+ function midiToNote(midi, spelling = "sharp") {
272
+ assertFiniteNumber(midi, "midi");
273
+ const rounded = Math.round(midi);
274
+ const pc = mod12(rounded);
275
+ const octave = Math.floor(rounded / 12) - 1;
276
+ const table = spelling === "sharp" ? SHARP_SPELLING : FLAT_SPELLING;
277
+ const entry = _nullishCoalesce(table[pc], () => ( [0, 0]));
278
+ return { letter: entry[0], alter: entry[1], octave };
279
+ }
280
+ var LETTER_STEPS_BY_SEMITONE = [0, 1, 1, 2, 2, 3, 3, 4, 5, 5, 6, 6];
281
+ function transposeNote(note, semitones, opts) {
282
+ if (!Number.isFinite(semitones)) {
283
+ throw new InvalidInputError(`semitones must be a finite number; received ${semitones}`);
284
+ }
285
+ const steps = Math.round(semitones);
286
+ if (_optionalChain([opts, 'optionalAccess', _ => _.spelling]) !== void 0) {
287
+ return note.octave === void 0 ? bareOf(midiToNote(60 + mod12(noteToPitchClass(note) + steps), opts.spelling)) : midiToNote(noteToMidi(note) + steps, opts.spelling);
288
+ }
289
+ const octaves = Math.floor(steps / 12);
290
+ const letterSteps = (_nullishCoalesce(LETTER_STEPS_BY_SEMITONE[steps - 12 * octaves], () => ( 0))) + 7 * octaves;
291
+ const absoluteLetter = mod7(note.letter) + letterSteps;
292
+ const letter = mod7(absoluteLetter);
293
+ const natural = _nullishCoalesce(LETTER_SEMITONES[letter], () => ( 0));
294
+ if (note.octave === void 0) {
295
+ return { letter, alter: alterFor(natural, mod12(noteToPitchClass(note) + steps)) };
296
+ }
297
+ const octave = note.octave + Math.floor(absoluteLetter / 7);
298
+ return { letter, alter: noteToMidi(note) + steps - ((octave + 1) * 12 + natural), octave };
299
+ }
300
+ function bareOf(note) {
301
+ return { letter: note.letter, alter: note.alter };
302
+ }
303
+ function alterFor(natural, pc) {
304
+ const d = mod12(pc - natural);
305
+ return d > 6 ? d - 12 : d;
306
+ }
307
+ var SIMPLE_REFERENCE = [0, 0, 2, 4, 5, 7, 9, 11];
308
+ function isPerfectNumber(simpleNumber) {
309
+ return simpleNumber === 1 || simpleNumber === 4 || simpleNumber === 5;
310
+ }
311
+ function qualityFromSpan(numberValue, semitones) {
312
+ const octaves = Math.floor((numberValue - 1) / 7);
313
+ const simple = numberValue - 7 * octaves;
314
+ const reference = (_nullishCoalesce(SIMPLE_REFERENCE[simple], () => ( 0))) + 12 * octaves;
315
+ const delta = semitones - reference;
316
+ const repeat = (glyph, count) => `${glyph}${glyph.repeat(Math.min(count, MAX_ALTER * 2) - 1)}`;
317
+ if (isPerfectNumber(simple)) {
318
+ if (delta === 0) {
319
+ return "P";
320
+ }
321
+ return delta > 0 ? repeat("A", delta) : repeat("d", -delta);
322
+ }
323
+ if (delta === 0) {
324
+ return "M";
325
+ }
326
+ if (delta === -1) {
327
+ return "m";
328
+ }
329
+ return delta > 0 ? repeat("A", delta) : repeat("d", -delta - 1);
330
+ }
331
+ function intervalSemitones(numberValue, quality) {
332
+ assertInteger(numberValue, "interval number", 1, 64);
333
+ const octaves = Math.floor((numberValue - 1) / 7);
334
+ const simple = numberValue - 7 * octaves;
335
+ const reference = (_nullishCoalesce(SIMPLE_REFERENCE[simple], () => ( 0))) + 12 * octaves;
336
+ const perfect = isPerfectNumber(simple);
337
+ if (quality === "P" || quality === "M") {
338
+ if (perfect !== (quality === "P")) {
339
+ throw new InvalidInputError(
340
+ `a ${numberValue === 1 ? "unison" : `${numberValue}th`} cannot be ${quality === "P" ? "perfect" : "major"}`
341
+ );
342
+ }
343
+ return reference;
344
+ }
345
+ if (quality === "m") {
346
+ if (perfect) {
347
+ throw new InvalidInputError(
348
+ `a perfect-class interval cannot be minor; received ${numberValue}`
349
+ );
350
+ }
351
+ return reference - 1;
352
+ }
353
+ if (/^A+$/.test(quality)) {
354
+ return reference + quality.length;
355
+ }
356
+ if (/^d+$/.test(quality)) {
357
+ return reference - quality.length - (perfect ? 0 : 1);
358
+ }
359
+ throw new InvalidInputError(`unknown interval quality ${JSON.stringify(quality)}`);
360
+ }
361
+ function parseInterval(name) {
362
+ const match = /^(P|M|m|A+|d+)(\d+)$/.exec(name);
363
+ const quality = _optionalChain([match, 'optionalAccess', _2 => _2[1]]);
364
+ const numberValue = Number(_optionalChain([match, 'optionalAccess', _3 => _3[2]]));
365
+ if (quality === void 0 || !Number.isFinite(numberValue)) {
366
+ throw new InvalidInputError(
367
+ `interval must be a quality followed by a number, such as 'P5'; received ${JSON.stringify(name)}`
368
+ );
369
+ }
370
+ return { number: numberValue, quality, semitones: intervalSemitones(numberValue, quality) };
371
+ }
372
+ function transposeByInterval(note, interval) {
373
+ assertNote(note, "note");
374
+ assertInteger(interval.number, "interval.number", 1, 64);
375
+ assertFiniteNumber(interval.semitones, "interval.semitones");
376
+ const descending = interval.semitones < 0;
377
+ const letterSteps = (interval.number - 1) * (descending ? -1 : 1);
378
+ const absoluteLetter = mod7(note.letter) + letterSteps;
379
+ const letter = mod7(absoluteLetter);
380
+ const natural = _nullishCoalesce(LETTER_SEMITONES[letter], () => ( 0));
381
+ const steps = Math.round(interval.semitones);
382
+ if (note.octave === void 0) {
383
+ return { letter, alter: alterFor(natural, mod12(noteToPitchClass(note) + steps)) };
384
+ }
385
+ const octave = note.octave + Math.floor(absoluteLetter / 7);
386
+ return { letter, alter: noteToMidi(note) + steps - ((octave + 1) * 12 + natural), octave };
387
+ }
388
+ function diatonicIndex(note) {
389
+ return mod7(note.letter) + 7 * (_nullishCoalesce(note.octave, () => ( 0)));
390
+ }
391
+ function spelledInterval(a, b) {
392
+ assertNote(a, "a");
393
+ assertNote(b, "b");
394
+ if (a.octave === void 0 !== (b.octave === void 0)) {
395
+ throw new InvalidInputError(
396
+ "spelledInterval needs both notes to carry an octave or neither to; received one of each"
397
+ );
398
+ }
399
+ const octaved = a.octave !== void 0 && b.octave !== void 0;
400
+ let letterSteps;
401
+ let semitones;
402
+ if (octaved) {
403
+ letterSteps = diatonicIndex(b) - diatonicIndex(a);
404
+ semitones = noteToMidi(b) - noteToMidi(a);
405
+ } else {
406
+ letterSteps = mod7(b.letter - a.letter);
407
+ let rawSemis = mod12(noteToPitchClass(b) - noteToPitchClass(a));
408
+ const reference = _nullishCoalesce(SIMPLE_REFERENCE[letterSteps + 1], () => ( 0));
409
+ if (reference - rawSemis > 6) {
410
+ rawSemis += 12;
411
+ } else if (rawSemis - reference > 6) {
412
+ rawSemis -= 12;
413
+ }
414
+ semitones = rawSemis;
415
+ }
416
+ const absSteps = Math.abs(letterSteps);
417
+ const number = absSteps + 1;
418
+ const directedSpan = letterSteps === 0 ? Math.abs(semitones) : letterSteps > 0 ? semitones : -semitones;
419
+ const quality = qualityFromSpan(number, directedSpan);
420
+ return { number, quality, semitones };
421
+ }
422
+
423
+ // src/core/interval/index.ts
424
+ var ConsonanceClass = /* @__PURE__ */ ((ConsonanceClass2) => {
425
+ ConsonanceClass2[ConsonanceClass2["PerfectConsonance"] = 0] = "PerfectConsonance";
426
+ ConsonanceClass2[ConsonanceClass2["ImperfectConsonance"] = 1] = "ImperfectConsonance";
427
+ ConsonanceClass2[ConsonanceClass2["Dissonance"] = 2] = "Dissonance";
428
+ return ConsonanceClass2;
429
+ })(ConsonanceClass || {});
430
+ function simpleInterval(semitones) {
431
+ assertFiniteNumber(semitones, "semitones");
432
+ return Math.abs(Math.round(semitones)) % 12;
433
+ }
434
+ function classifyInterval(semitones, twoVoice = true) {
435
+ const pc = simpleInterval(semitones);
436
+ if (pc === 0 || pc === 7) {
437
+ return 0 /* PerfectConsonance */;
438
+ }
439
+ if (pc === 3 || pc === 4 || pc === 8 || pc === 9) {
440
+ return 1 /* ImperfectConsonance */;
441
+ }
442
+ if (pc === 5) {
443
+ return twoVoice ? 2 /* Dissonance */ : 1 /* ImperfectConsonance */;
444
+ }
445
+ return 2 /* Dissonance */;
446
+ }
447
+ function isPerfectInterval(semitones) {
448
+ const pc = simpleInterval(semitones);
449
+ return pc === 0 || pc === 7;
450
+ }
451
+ function isConsonantInterval(semitones, twoVoice = true) {
452
+ const pc = simpleInterval(semitones);
453
+ if (pc === 5) {
454
+ return !twoVoice;
455
+ }
456
+ return pc === 0 || pc === 3 || pc === 4 || pc === 7 || pc === 8 || pc === 9;
457
+ }
458
+
459
+
460
+
461
+
462
+
463
+
464
+
465
+
466
+
467
+
468
+
469
+
470
+
471
+
472
+
473
+
474
+
475
+
476
+
477
+
478
+
479
+
480
+
481
+
482
+
483
+
484
+
485
+
486
+
487
+
488
+
489
+
490
+
491
+
492
+
493
+
494
+
495
+
496
+
497
+ exports.InvalidInputError = InvalidInputError; exports.NoSolutionError = NoSolutionError; exports.BudgetExceededError = BudgetExceededError; exports.isLibcantusError = isLibcantusError; exports.isCompoundNumerator = isCompoundNumerator; exports.isAdditiveReading = isAdditiveReading; exports.pulseCountOf = pulseCountOf; exports.pulseBeatsOf = pulseBeatsOf; exports.barBeatsOf = barBeatsOf; exports.DEFAULT_GENERATION_BUDGET = DEFAULT_GENERATION_BUDGET; exports.assertFiniteNumber = assertFiniteNumber; exports.assertInteger = assertInteger; exports.dropSilentNotes = dropSilentNotes; exports.assertOneOf = assertOneOf; exports.assertPositiveInt = assertPositiveInt; exports.assertRange = assertRange; exports.assertGenerationBudget = assertGenerationBudget; exports.assertTimeSignature = assertTimeSignature; exports.assertNoteEvent = assertNoteEvent; exports.assertNoteEvents = assertNoteEvents; exports.pitchClassOf = pitchClassOf; exports.diatonicLetterOf = diatonicLetterOf; exports.naturalPitchClassOf = naturalPitchClassOf; exports.parseNote = parseNote; exports.formatNote = formatNote; exports.noteToPitchClass = noteToPitchClass; exports.noteToMidi = noteToMidi; exports.midiToNote = midiToNote; exports.transposeNote = transposeNote; exports.intervalSemitones = intervalSemitones; exports.parseInterval = parseInterval; exports.transposeByInterval = transposeByInterval; exports.spelledInterval = spelledInterval; exports.ConsonanceClass = ConsonanceClass; exports.classifyInterval = classifyInterval; exports.isPerfectInterval = isPerfectInterval; exports.isConsonantInterval = isConsonantInterval;
498
+ //# sourceMappingURL=chunk-7MGZ7D62.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/libcantus/libcantus/dist/chunk-7MGZ7D62.cjs","../src/core/errors/index.ts","../src/core/meter/internal.ts","../src/core/validation/index.ts","../src/core/pitch/index.ts","../src/core/interval/index.ts"],"names":["ConsonanceClass"],"mappings":"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AACA,EAAA;AACE,EAAA;AACF,EAAA;AACR,EAAA;AACJ,EAAA;AACS,EAAA;AACM,EAAA;AACL,IAAA;AAC1B,EAAA;AAC6B,EAAA;AACL,EAAA;AAC1B;AAG2C;AACd,EAAA;AAC7B;AAoBmE;AAChD,EAAA;AACA,EAAA;AAKmB,EAAA;AACxB,IAAA;AACR,MAAA;AACF,IAAA;AACF,EAAA;AAC6B,EAAA;AACzB,EAAA;AACA,EAAA;AACS,EAAA;AACkB,IAAA;AAED,IAAA;AACvB,EAAA;AACgB,IAAA;AACA,IAAA;AAQH,IAAA;AACS,IAAA;AACb,MAAA;AACQ,IAAA;AACR,MAAA;AACd,IAAA;AACY,IAAA;AACd,EAAA;AAC0B,EAAA;AACA,EAAA;AASxB,EAAA;AACc,EAAA;AACU,EAAA;AAC5B;AJ3KiC;AACA;AKzZ1B;AACLA,EAAAA;AACAA,EAAAA;AACAA,EAAAA;AAHUA,EAAAA;AAAA;AAYY;AACQ,EAAA;AACH,EAAA;AAC7B;AAsBiC;AACL,EAAA;AACA,EAAA;AACjB,IAAA;AACT,EAAA;AAC4B,EAAA;AACnB,IAAA;AACT,EAAA;AACc,EAAA;AACM,IAAA;AACpB,EAAA;AACO,EAAA;AACT;AAekC;AACN,EAAA;AACA,EAAA;AAC5B;AAcoC;AACR,EAAA;AACZ,EAAA;AACJ,IAAA;AACV,EAAA;AAC+B,EAAA;AACjC;ALqWiC;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-7MGZ7D62.cjs","sourcesContent":[null,"/**\n * The library's error taxonomy.\n *\n * Every failure this library raises falls into one of three kinds, and the kind\n * decides what a caller can do about it. Batch-voicing a lead sheet, for\n * instance, has to tell \"fix the input\" apart from \"loosen the constraints\",\n * and a message string is not a contract.\n *\n * The classes extend the built-in error types a caller would otherwise catch —\n * a rejected argument is still a `RangeError` — so the `code` narrows an\n * existing category rather than replacing it.\n */\n\n/**\n * What kind of failure an error reports.\n *\n * - `INVALID_INPUT`: an argument is malformed or out of range. The caller's\n * input must change.\n * - `NO_SOLUTION`: the input is valid but nothing satisfies the constraints.\n * The constraints must change, or the failure must be accepted.\n * - `BUDGET_EXCEEDED`: the requested work is larger than the synchronous\n * generation budget. The request must be smaller, or the budget larger.\n *\n * @category Core\n */\nexport type LibcantusErrorCode = 'INVALID_INPUT' | 'NO_SOLUTION' | 'BUDGET_EXCEEDED';\n\n/**\n * A rejected argument: malformed, out of range, or naming something unknown.\n *\n * @category Core\n */\nexport class InvalidInputError extends RangeError {\n readonly code = 'INVALID_INPUT' as const;\n override readonly name = 'InvalidInputError';\n}\n\n/**\n * A valid request with no answer: no voicing fits the given ranges, no chord\n * matches the pitches, no preset claims the style.\n *\n * @category Core\n */\nexport class NoSolutionError extends Error {\n readonly code = 'NO_SOLUTION' as const;\n override readonly name = 'NoSolutionError';\n\n /**\n * Where in a sequence the failure occurred, when the operation was working\n * through one — the chord index of a progression, say.\n */\n readonly at: number | undefined;\n\n constructor(message: string, options: { at?: number } = {}) {\n super(message);\n this.at = options.at;\n }\n}\n\n/**\n * Work that would exceed the synchronous generation budget.\n *\n * @category Core\n */\nexport class BudgetExceededError extends RangeError {\n readonly code = 'BUDGET_EXCEEDED' as const;\n override readonly name = 'BudgetExceededError';\n}\n\n/**\n * Whether a caught value is one of this library's coded errors.\n *\n * @param value The caught value.\n * @returns True when it carries a {@link LibcantusErrorCode}.\n * @example\n * ```ts\n * import { isLibcantusError, voiceChord, parseChordSymbol } from '@libraz/libcantus';\n * try {\n * voiceChord(parseChordSymbol('C'), { ranges: [] });\n * } catch (error) {\n * if (isLibcantusError(error) && error.code === 'NO_SOLUTION') {\n * // loosen the constraints rather than blaming the input\n * }\n * }\n * ```\n * @category Core\n */\nexport function isLibcantusError(\n value: unknown,\n): value is InvalidInputError | NoSolutionError | BudgetExceededError {\n return (\n value instanceof InvalidInputError ||\n value instanceof NoSolutionError ||\n value instanceof BudgetExceededError\n );\n}\n","/**\n * Meter arithmetic shared by the meter module and the validation guards.\n *\n * These helpers never validate: they are the single definition of how a time\n * signature is read, so the guard that rejects a malformed signature and the\n * functions that use a validated one cannot drift apart. Everything here is\n * integer arithmetic on the numerator, so a bar's pulse count is exact rather\n * than the result of dividing two floats.\n */\n\nimport type { TimeSignature } from './index.js';\n\n/** Length of one denominator unit in quarter-note beats. */\nexport function unitBeatsOf(ts: TimeSignature): number {\n return 4 / ts.denominator;\n}\n\n/**\n * Whether a numerator reads as compound: a multiple of three greater than\n * three, so its main pulses each divide into three.\n */\nexport function isCompoundNumerator(numerator: number): boolean {\n return numerator % 3 === 0 && numerator > 3;\n}\n\n/** Sum of an additive grouping, or undefined when there is none. */\nexport function groupingSumOf(ts: TimeSignature): number | undefined {\n const grouping = ts.grouping;\n if (grouping === undefined) {\n return undefined;\n }\n let sum = 0;\n for (const entry of grouping) {\n sum += entry;\n }\n return sum;\n}\n\n/**\n * Whether the signature's grouping counts denominator units rather than\n * compound pulses.\n *\n * A compound numerator normally groups into pulses of three — 9/8 is three\n * dotted-quarter pulses — but the same signature is also how additive metres\n * are written, where 9/8 means 2+2+2+3 quavers. A grouping that sums to the\n * numerator selects that additive reading.\n */\nexport function isAdditiveReading(ts: TimeSignature): boolean {\n const sum = groupingSumOf(ts);\n return (\n sum !== undefined && isCompoundNumerator(ts.numerator) && sum === ts.numerator && sum !== 0\n );\n}\n\n/** Number of main pulses per bar under the signature's effective reading. */\nexport function pulseCountOf(ts: TimeSignature): number {\n if (isAdditiveReading(ts)) {\n return ts.numerator;\n }\n return isCompoundNumerator(ts.numerator) ? ts.numerator / 3 : ts.numerator;\n}\n\n/** Length of one main pulse in quarter-note beats, under the same reading. */\nexport function pulseBeatsOf(ts: TimeSignature): number {\n return (ts.numerator * unitBeatsOf(ts)) / pulseCountOf(ts);\n}\n\n/** Length of a bar in quarter-note beats. */\nexport function barBeatsOf(ts: TimeSignature): number {\n return ts.numerator * unitBeatsOf(ts);\n}\n","import { BudgetExceededError, InvalidInputError } from '../errors/index.js';\nimport type { TimeSignature } from '../meter/index.js';\nimport { isCompoundNumerator } from '../meter/internal.js';\nimport type { NoteEvent } from '../types.js';\n\n/**\n * Default upper bound for synchronous event/window/candidate generation.\n *\n * @category Core\n */\nexport const DEFAULT_GENERATION_BUDGET = 1_000_000;\n\n/**\n * Require a finite JavaScript number and return it unchanged.\n *\n * @category Core\n */\nexport function assertFiniteNumber(value: number, name: string): number {\n if (!Number.isFinite(value)) {\n throw new InvalidInputError(`${name} must be finite; received ${value}`);\n }\n return value;\n}\n\n/**\n * Require an integer in the inclusive range `[min, max]`.\n *\n * @category Core\n */\nexport function assertInteger(\n value: number,\n name: string,\n min = Number.MIN_SAFE_INTEGER,\n max = Number.MAX_SAFE_INTEGER,\n): number {\n assertFiniteNumber(value, name);\n if (!Number.isSafeInteger(value) || value < min || value > max) {\n throw new InvalidInputError(\n `${name} must be an integer in [${min}, ${max}]; received ${value}`,\n );\n }\n return value;\n}\n\n/**\n * Drop the notes that never sound.\n *\n * A MIDI or DAW import routinely carries zero-length artefacts. Every entry\n * point in this library ignores them, but a caller holding such an array still\n * has to decide what to do with it before comparing note counts or indices;\n * this is the one filter to apply.\n *\n * @param events The note events to filter.\n * @returns A new array containing only the notes with a positive duration.\n * @example\n * ```ts\n * import { dropSilentNotes } from '@libraz/libcantus';\n * dropSilentNotes([{ pitch: 60, startBeat: 0, durationBeat: 0 }]); // []\n * ```\n * @category Core\n */\nexport function dropSilentNotes(events: readonly NoteEvent[]): NoteEvent[] {\n return events.filter((event) => event !== undefined && event.durationBeat > 0);\n}\n\n/**\n * Require a value to be one of a fixed set of names.\n *\n * TypeScript checks string-union options at compile time only, so a value that\n * arrives from JSON, a config file, or a JavaScript caller reaches the engine\n * unchecked and is then read against a table that has no entry for it —\n * producing a silent `undefined`, a NaN velocity, or a crash far from the call.\n *\n * @param value The value to check.\n * @param allowed Every accepted name.\n * @param name What the value is, for the error message.\n * @returns The value, narrowed to the allowed union.\n * @throws If the value is not one of `allowed`.\n *\n * @category Core\n */\nexport function assertOneOf<const T extends string>(\n value: unknown,\n allowed: readonly T[],\n name: string,\n): T {\n if (typeof value !== 'string' || !(allowed as readonly string[]).includes(value)) {\n throw new InvalidInputError(\n `${name} must be one of ${allowed.join(', ')}; received ${JSON.stringify(value)}`,\n );\n }\n return value as T;\n}\n\n/**\n * Require a positive safe integer, optionally capped by `max`.\n *\n * @category Core\n */\nexport function assertPositiveInt(\n value: number,\n name: string,\n max = DEFAULT_GENERATION_BUDGET,\n): number {\n return assertInteger(value, name, 1, max);\n}\n\n/**\n * Require a finite number in the inclusive range `[min, max]`.\n *\n * @category Core\n */\nexport function assertRange(value: number, min: number, max: number, name: string): number {\n assertFiniteNumber(value, name);\n if (value < min || value > max) {\n throw new InvalidInputError(`${name} must be in [${min}, ${max}]; received ${value}`);\n }\n return value;\n}\n\n/**\n * Reject work estimates that would exceed a synchronous generation budget.\n *\n * @category Core\n */\nexport function assertGenerationBudget(\n estimated: number,\n name: string,\n limit: number | undefined = DEFAULT_GENERATION_BUDGET,\n): number {\n const cap = limit ?? DEFAULT_GENERATION_BUDGET;\n assertFiniteNumber(estimated, name);\n assertPositiveInt(cap, `${name} limit`, Number.MAX_SAFE_INTEGER);\n if (estimated < 0) {\n throw new InvalidInputError(`${name} must not be negative; received ${estimated}`);\n }\n if (estimated > cap) {\n throw new BudgetExceededError(\n `${name} exceeds the generation budget ${cap}; received ${estimated}`,\n );\n }\n return estimated;\n}\n\n/**\n * Validate a time signature, including additive grouping, before any early return.\n *\n * @category Core\n */\nexport function assertTimeSignature(ts: TimeSignature, name = 'time signature'): TimeSignature {\n assertPositiveInt(ts.numerator, `${name}.numerator`);\n assertPositiveInt(ts.denominator, `${name}.denominator`);\n if (ts.grouping !== undefined) {\n if (ts.grouping.length === 0) {\n throw new InvalidInputError(`${name}.grouping must not be empty`);\n }\n let sum = 0;\n for (let index = 0; index < ts.grouping.length; index += 1) {\n sum += assertPositiveInt(ts.grouping[index] ?? Number.NaN, `${name}.grouping[${index}]`);\n }\n // A compound numerator accepts either reading: grouping its dotted pulses\n // (9/8 as [1, 1, 1]) or grouping its denominator units additively (9/8 as\n // [2, 2, 2, 3]), which is how aksak and other additive metres are written.\n const pulses = isCompoundNumerator(ts.numerator) ? ts.numerator / 3 : ts.numerator;\n if (sum !== pulses && sum !== ts.numerator) {\n const accepted =\n pulses === ts.numerator ? `${pulses}` : `${pulses} (pulses) or ${ts.numerator} (units)`;\n throw new InvalidInputError(`${name}.grouping must sum to ${accepted}; received ${sum}`);\n }\n }\n return ts;\n}\n\n/**\n * How strictly {@link assertNoteEvent} and {@link assertNoteEvents} read an\n * event array.\n *\n * @category Core\n */\nexport type NoteEventAssertOptions = {\n /**\n * Accept notes that never sound. The analysis side does, so an array from a\n * MIDI import can be validated before the zero-length artefacts are dropped.\n */\n allowNonPositiveDuration?: boolean;\n /** Upper bound on the event count; defaults to the generation budget. */\n budget?: number;\n};\n\n/**\n * Validate the finite fields of one timeline note event.\n *\n * @category Core\n */\nexport function assertNoteEvent(\n event: NoteEvent,\n name = 'note event',\n options: NoteEventAssertOptions = {},\n): NoteEvent {\n assertInteger(event.pitch, `${name}.pitch`, 0, 127);\n assertRange(event.startBeat, 0, Number.MAX_SAFE_INTEGER, `${name}.startBeat`);\n // The positivity check comes first so the common failure — a zero-length note\n // from a MIDI import — reads as such instead of naming a denormal lower bound.\n assertFiniteNumber(event.durationBeat, `${name}.durationBeat`);\n if (!options.allowNonPositiveDuration && event.durationBeat <= 0) {\n throw new InvalidInputError(\n `${name}.durationBeat must be positive; received ${event.durationBeat}`,\n );\n }\n assertRange(\n event.durationBeat,\n -Number.MAX_SAFE_INTEGER,\n Number.MAX_SAFE_INTEGER,\n `${name}.durationBeat`,\n );\n if (event.velocity !== undefined) {\n assertRange(event.velocity, 0, 127, `${name}.velocity`);\n }\n return event;\n}\n\n/**\n * Validate an event array and its allocation budget without copying it.\n *\n * @category Core\n */\nexport function assertNoteEvents(\n events: readonly NoteEvent[],\n name = 'note events',\n options: NoteEventAssertOptions = {},\n): NoteEvent[] {\n if (!Array.isArray(events)) {\n throw new TypeError(`${name} must be an array; received ${typeof events}`);\n }\n assertGenerationBudget(events.length, `${name} count`, options.budget);\n for (let index = 0; index < events.length; index += 1) {\n const event = events[index];\n // A hole in a sparse array and an explicit undefined are both rejected\n // here: letting either through only moves the failure to a later\n // TypeError, or drops the note silently.\n if (event === undefined) {\n throw new InvalidInputError(`${name}[${index}] must be a note event; received undefined`);\n }\n assertNoteEvent(event, `${name}[${index}]`, options);\n }\n return events;\n}\n","/**\n * Pitch spelling: letter-name notes, enharmonic-aware conversion, and spelled\n * intervals.\n *\n * The rest of the library works in pitch classes (0..11), which cannot tell a\n * G# from an Ab or an augmented fourth from a diminished fifth. This module adds\n * an explicit spelling layer — a note is a diatonic letter plus a chromatic\n * alteration — so interval quality (P/M/m/A/d) and enharmonic identity are\n * preserved.\n */\n\nimport { InvalidInputError } from '../errors/index.js';\nimport { assertFiniteNumber, assertInteger } from '../validation/index.js';\n\n/** Semitone offset of each natural letter above C: C D E F G A B. */\nconst LETTER_SEMITONES = [0, 2, 4, 5, 7, 9, 11] as const;\n\n/**\n * Widest alteration a note may carry.\n *\n * Six accidentals already exceeds anything common practice writes, and the\n * bound is what keeps an unvalidated number from reaching `String.repeat`.\n */\nconst MAX_ALTER = 6;\n\n/** Validate the fields of a spelled note once, at a public entry point. */\nfunction assertNote(note: Note, name: string): Note {\n assertInteger(note.letter, `${name}.letter`, -1000, 1000);\n assertInteger(note.alter, `${name}.alter`, -MAX_ALTER, MAX_ALTER);\n if (note.octave !== undefined) {\n assertInteger(note.octave, `${name}.octave`, -100, 100);\n }\n return note;\n}\n\n/** Letter glyphs indexed by letter number (0 = C .. 6 = B). */\nconst LETTER_NAMES = ['C', 'D', 'E', 'F', 'G', 'A', 'B'] as const;\n\n/**\n * A spelled note: a diatonic letter, a chromatic alteration, and an optional\n * octave.\n *\n * - `letter` is 0..6 for C..B.\n * - `alter` is the number of semitones of alteration: -2 double-flat, -1 flat,\n * 0 natural, +1 sharp, +2 double-sharp.\n * - `octave` follows scientific pitch notation (middle C = C4 = MIDI 60). When\n * absent, the note denotes a bare pitch class.\n *\n * @category Pitch & Intervals\n */\nexport type Note = {\n letter: number;\n alter: number;\n octave?: number;\n};\n\n/**\n * The quality label of a spelled interval: perfect, major, minor, or one or\n * more augmentations or diminutions.\n *\n * This names how an interval is *spelled*. It is a different notion from\n * {@link ConsonanceClass}, which classifies how an interval *sounds* against\n * another voice.\n *\n * @see {@link ConsonanceClass}\n * @category Pitch & Intervals\n */\nexport type IntervalQualityLabel = 'P' | 'M' | 'm' | `A${string}` | `d${string}`;\n\n/**\n * A spelled interval: a diatonic number, a quality label, and its semitone span.\n *\n * @category Pitch & Intervals\n */\nexport type SpelledInterval = {\n /** Diatonic size: 1 = unison, 2 = second, ... 8 = octave, and beyond. */\n number: number;\n /** Quality label: 'P', 'M', 'm', or repeated 'A'/'d' for (multiply) aug/dim. */\n quality: IntervalQualityLabel;\n /** Signed semitone distance from the first note to the second. */\n semitones: number;\n};\n\n/**\n * Reduce a MIDI pitch, a bare pitch class, or any signed offset to a pitch\n * class in [0, 11].\n *\n * This is the library's single definition of pitch-class arithmetic: every\n * layer routes through it so a fractional pitch reduces the same way\n * everywhere. A non-integral input is rounded to the nearest semitone first, so\n * a microtonal 60.6 reads as C# rather than C.\n *\n * @param value A MIDI pitch, pitch class, or signed semitone offset.\n * @returns The pitch class in [0, 11].\n * @example\n * ```ts\n * import { pitchClassOf } from '@libraz/libcantus';\n * pitchClassOf(61); // 1\n * pitchClassOf(-1); // 11\n * pitchClassOf(60.6); // 1\n * ```\n * @category Pitch & Intervals\n */\nexport function pitchClassOf(value: number): number {\n assertFiniteNumber(value, 'pitch');\n return ((Math.round(value) % 12) + 12) % 12;\n}\n\n/** Reduce any integer to a pitch class in [0, 11]. */\nconst mod12 = pitchClassOf;\n\n/**\n * Reduce a letter number or letter offset to a diatonic letter in [0, 6]\n * (0 = C .. 6 = B).\n *\n * @param value A letter number or signed letter offset.\n * @returns The diatonic letter in [0, 6].\n * @category Pitch & Intervals\n */\nexport function diatonicLetterOf(value: number): number {\n return ((Math.round(value) % 7) + 7) % 7;\n}\n\n/**\n * The pitch class of a diatonic letter with no accidental (C = 0, D = 2, ...).\n *\n * @param letter A letter number or signed letter offset; reduced to [0, 6].\n * @returns The natural pitch class of that letter.\n * @category Pitch & Intervals\n */\nexport function naturalPitchClassOf(letter: number): number {\n return LETTER_SEMITONES[diatonicLetterOf(letter)] ?? 0;\n}\n\n/** Reduce any integer to [0, 7). */\nconst mod7 = diatonicLetterOf;\n\n/**\n * Parse scientific pitch notation into a {@link Note}.\n *\n * Accepts a letter (case-insensitive) followed by any number of same-direction\n * accidentals and an optional octave, e.g. `\"C\"`, `\"C#4\"`, `\"Bb\"`, `\"F##3\"`,\n * `\"Ebb2\"`. Both `#` and `x` (double-sharp) are accepted for sharps; `b` for\n * flats. Mixing sharps and flats (e.g. `\"C#b\"`) is rejected.\n *\n * @param text The note text.\n * @returns The parsed note.\n * @throws If the text is not a valid note.\n * @example\n * ```ts\n * import { parseNote, noteToMidi } from '@libraz/libcantus';\n * const n = parseNote('C#4');\n * noteToMidi(n); // 61\n * ```\n * @category Pitch & Intervals\n */\nexport function parseNote(text: string): Note {\n const match = /^([A-Ga-g])([#x]*|b*)(-?\\d+)?$/.exec(text.trim());\n if (!match) {\n throw new InvalidInputError(`Invalid note: ${text}`);\n }\n const letterGlyph = (match[1] ?? '').toUpperCase();\n const letter = LETTER_NAMES.indexOf(letterGlyph as (typeof LETTER_NAMES)[number]);\n let alter = 0;\n for (const ch of match[2] ?? '') {\n if (ch === '#') {\n alter += 1;\n } else if (ch === 'x') {\n alter += 2;\n } else if (ch === 'b') {\n alter -= 1;\n }\n }\n const note: Note = { letter, alter };\n if (match[3] !== undefined) {\n note.octave = Number.parseInt(match[3], 10);\n }\n return assertNote(note, `note ${text}`);\n}\n\n/** Render an alteration as accidental glyphs (`##`, `b`, empty for natural). */\nfunction formatAlter(alter: number): string {\n if (alter > 0) {\n return '#'.repeat(alter);\n }\n if (alter < 0) {\n return 'b'.repeat(-alter);\n }\n return '';\n}\n\n/**\n * Render a {@link Note} as scientific pitch notation.\n *\n * @param note The note to format.\n * @returns The note text, including the octave when present.\n * @example\n * ```ts\n * import { formatNote, parseNote } from '@libraz/libcantus';\n * formatNote(parseNote('C#4')); // 'C#4'\n * ```\n * @category Pitch & Intervals\n */\nexport function formatNote(note: Note): string {\n assertNote(note, 'note');\n const glyph = LETTER_NAMES[mod7(note.letter)] ?? 'C';\n const octave = note.octave === undefined ? '' : String(note.octave);\n return `${glyph}${formatAlter(note.alter)}${octave}`;\n}\n\n/**\n * Pitch class (0..11) of a note, ignoring octave.\n *\n * @param note The note.\n * @returns The pitch class.\n * @example\n * ```ts\n * import { noteToPitchClass, parseNote } from '@libraz/libcantus';\n * noteToPitchClass(parseNote('Db')); // 1\n * ```\n * @category Pitch & Intervals\n */\nexport function noteToPitchClass(note: Note): number {\n assertNote(note, 'note');\n const natural = LETTER_SEMITONES[mod7(note.letter)] ?? 0;\n return mod12(natural + note.alter);\n}\n\n/**\n * MIDI number of a note (middle C = C4 = 60).\n *\n * @param note The note; must carry an octave.\n * @returns The MIDI number.\n * @throws If the note has no octave.\n * @example\n * ```ts\n * import { noteToMidi, parseNote } from '@libraz/libcantus';\n * noteToMidi(parseNote('A4')); // 69\n * ```\n * @category Pitch & Intervals\n */\nexport function noteToMidi(note: Note): number {\n assertNote(note, 'note');\n if (note.octave === undefined) {\n throw new InvalidInputError('noteToMidi requires an octave');\n }\n const natural = LETTER_SEMITONES[mod7(note.letter)] ?? 0;\n return (note.octave + 1) * 12 + natural + note.alter;\n}\n\n/** Preferred spelling when naming a black key from a bare MIDI number. */\nconst SHARP_SPELLING: readonly [number, number][] = [\n [0, 0],\n [0, 1],\n [1, 0],\n [1, 1],\n [2, 0],\n [3, 0],\n [3, 1],\n [4, 0],\n [4, 1],\n [5, 0],\n [5, 1],\n [6, 0],\n];\n\nconst FLAT_SPELLING: readonly [number, number][] = [\n [0, 0],\n [1, -1],\n [1, 0],\n [2, -1],\n [2, 0],\n [3, 0],\n [4, -1],\n [4, 0],\n [5, -1],\n [5, 0],\n [6, -1],\n [6, 0],\n];\n\n/**\n * Name a MIDI number as a {@link Note}, choosing sharp or flat spelling.\n *\n * The input is rounded to the nearest integer but is not clamped to the MIDI\n * range 0..127: out-of-range values extrapolate linearly (e.g. `-1` -> `B-2`,\n * `128` -> `G#9`) and remain an exact inverse of {@link noteToMidi}.\n *\n * @param midi The MIDI number.\n * @param spelling Whether to prefer sharps or flats for black keys.\n * @returns The spelled note, with octave.\n * @example\n * ```ts\n * import { midiToNote, formatNote } from '@libraz/libcantus';\n * formatNote(midiToNote(61)); // 'C#4'\n * formatNote(midiToNote(61, 'flat')); // 'Db4'\n * ```\n * @category Pitch & Intervals\n */\nexport function midiToNote(midi: number, spelling: 'sharp' | 'flat' = 'sharp'): Note {\n assertFiniteNumber(midi, 'midi');\n const rounded = Math.round(midi);\n const pc = mod12(rounded);\n const octave = Math.floor(rounded / 12) - 1;\n const table = spelling === 'sharp' ? SHARP_SPELLING : FLAT_SPELLING;\n const entry = table[pc] ?? [0, 0];\n return { letter: entry[0], alter: entry[1], octave };\n}\n\n/**\n * Diatonic letter distance of the conventional ascending interval for each\n * semitone step within an octave: P1, m2, M2, m3, M3, P4, A4, P5, m6, M6, m7, M7.\n */\nconst LETTER_STEPS_BY_SEMITONE = [0, 1, 1, 2, 2, 3, 3, 4, 5, 5, 6, 6] as const;\n\n/**\n * Transpose a note by a signed number of semitones, keeping its spelling.\n *\n * The letter moves by the diatonic distance of the conventional interval for\n * that many semitones and the accidental is recomputed for the new letter, so\n * enharmonic identity survives: `Ab4` up a major second is `Bb4`, not `A#4`, and\n * the operation is an exact inverse of itself. An octave-less note stays\n * octave-less.\n *\n * A bare semitone count cannot distinguish an augmented fourth from a diminished\n * fifth; pass `spelling` to override the letter distance when the enharmonic\n * choice matters.\n *\n * @param note The note to transpose.\n * @param semitones The signed semitone offset.\n * @param opts `spelling` forces the result onto the sharp or flat side instead\n * of following the source note's letter.\n * @returns The transposed note.\n * @example\n * ```ts\n * import { transposeNote, formatNote, parseNote } from '@libraz/libcantus';\n * formatNote(transposeNote(parseNote('Ab4'), 2)); // 'Bb4'\n * formatNote(transposeNote(parseNote('Ab4'), 2, { spelling: 'sharp' })); // 'A#4'\n * ```\n * @category Pitch & Intervals\n */\nexport function transposeNote(\n note: Note,\n semitones: number,\n opts?: { spelling?: 'sharp' | 'flat' },\n): Note {\n if (!Number.isFinite(semitones)) {\n throw new InvalidInputError(`semitones must be a finite number; received ${semitones}`);\n }\n const steps = Math.round(semitones);\n if (opts?.spelling !== undefined) {\n return note.octave === undefined\n ? bareOf(midiToNote(60 + mod12(noteToPitchClass(note) + steps), opts.spelling))\n : midiToNote(noteToMidi(note) + steps, opts.spelling);\n }\n const octaves = Math.floor(steps / 12);\n const letterSteps = (LETTER_STEPS_BY_SEMITONE[steps - 12 * octaves] ?? 0) + 7 * octaves;\n const absoluteLetter = mod7(note.letter) + letterSteps;\n const letter = mod7(absoluteLetter);\n const natural = LETTER_SEMITONES[letter] ?? 0;\n if (note.octave === undefined) {\n return { letter, alter: alterFor(natural, mod12(noteToPitchClass(note) + steps)) };\n }\n const octave = note.octave + Math.floor(absoluteLetter / 7);\n return { letter, alter: noteToMidi(note) + steps - ((octave + 1) * 12 + natural), octave };\n}\n\n/** Drop the octave from a spelled note. */\nfunction bareOf(note: Note): Note {\n return { letter: note.letter, alter: note.alter };\n}\n\n/** Shortest signed alteration taking a letter's natural pitch class to `pc`. */\nfunction alterFor(natural: number, pc: number): number {\n const d = mod12(pc - natural);\n return d > 6 ? d - 12 : d;\n}\n\n/** Reference semitone span of a perfect/major simple interval by diatonic number. */\nconst SIMPLE_REFERENCE = [0, 0, 2, 4, 5, 7, 9, 11] as const;\n\n/** Whether a simple diatonic number (1..7 within an octave) is a perfect kind. */\nfunction isPerfectNumber(simpleNumber: number): boolean {\n return simpleNumber === 1 || simpleNumber === 4 || simpleNumber === 5;\n}\n\n/** Quality label from a diatonic number and its actual semitone span. */\nfunction qualityFromSpan(numberValue: number, semitones: number): IntervalQualityLabel {\n const octaves = Math.floor((numberValue - 1) / 7);\n const simple = numberValue - 7 * octaves;\n const reference = (SIMPLE_REFERENCE[simple] ?? 0) + 12 * octaves;\n const delta = semitones - reference;\n const repeat = (glyph: 'A' | 'd', count: number): IntervalQualityLabel =>\n `${glyph}${glyph.repeat(Math.min(count, MAX_ALTER * 2) - 1)}`;\n if (isPerfectNumber(simple)) {\n if (delta === 0) {\n return 'P';\n }\n return delta > 0 ? repeat('A', delta) : repeat('d', -delta);\n }\n if (delta === 0) {\n return 'M';\n }\n if (delta === -1) {\n return 'm';\n }\n return delta > 0 ? repeat('A', delta) : repeat('d', -delta - 1);\n}\n\n/**\n * The semitone span of the interval named by a diatonic number and a quality.\n *\n * The inverse of the quality derivation in {@link spelledInterval}: it turns a\n * name back into a distance, which is what transposing by a named interval or\n * parsing one from text needs.\n *\n * @param numberValue Diatonic size: 1 = unison, 2 = second, ... 8 = octave.\n * @param quality Quality label: `'P'`, `'M'`, `'m'`, or repeated `'A'`/`'d'`.\n * @returns The unsigned semitone span.\n * @throws If the quality cannot apply to the number, such as a major fifth.\n * @example\n * ```ts\n * import { intervalSemitones } from '@libraz/libcantus';\n * intervalSemitones(5, 'P'); // 7\n * ```\n * @category Pitch & Intervals\n */\nexport function intervalSemitones(numberValue: number, quality: IntervalQualityLabel): number {\n assertInteger(numberValue, 'interval number', 1, 64);\n const octaves = Math.floor((numberValue - 1) / 7);\n const simple = numberValue - 7 * octaves;\n const reference = (SIMPLE_REFERENCE[simple] ?? 0) + 12 * octaves;\n const perfect = isPerfectNumber(simple);\n if (quality === 'P' || quality === 'M') {\n if (perfect !== (quality === 'P')) {\n throw new InvalidInputError(\n `a ${numberValue === 1 ? 'unison' : `${numberValue}th`} cannot be ${quality === 'P' ? 'perfect' : 'major'}`,\n );\n }\n return reference;\n }\n if (quality === 'm') {\n if (perfect) {\n throw new InvalidInputError(\n `a perfect-class interval cannot be minor; received ${numberValue}`,\n );\n }\n return reference - 1;\n }\n if (/^A+$/.test(quality)) {\n return reference + quality.length;\n }\n if (/^d+$/.test(quality)) {\n return reference - quality.length - (perfect ? 0 : 1);\n }\n throw new InvalidInputError(`unknown interval quality ${JSON.stringify(quality)}`);\n}\n\n/**\n * Parse an interval name such as `'P5'`, `'m3'`, or `'AA4'`.\n *\n * @param name The interval name: a quality label followed by a diatonic number.\n * @returns The spelled interval, with an ascending (positive) span.\n * @throws If the name is not a quality label followed by a number, or the two\n * cannot describe the same interval.\n * @example\n * ```ts\n * import { parseInterval } from '@libraz/libcantus';\n * parseInterval('m3'); // { number: 3, quality: 'm', semitones: 3 }\n * ```\n * @category Pitch & Intervals\n */\nexport function parseInterval(name: string): SpelledInterval {\n const match = /^(P|M|m|A+|d+)(\\d+)$/.exec(name);\n const quality = match?.[1] as IntervalQualityLabel | undefined;\n const numberValue = Number(match?.[2]);\n if (quality === undefined || !Number.isFinite(numberValue)) {\n throw new InvalidInputError(\n `interval must be a quality followed by a number, such as 'P5'; received ${JSON.stringify(name)}`,\n );\n }\n return { number: numberValue, quality, semitones: intervalSemitones(numberValue, quality) };\n}\n\n/**\n * Transpose a note by a spelled interval, keeping the spelling the interval\n * names.\n *\n * Unlike {@link transposeNote}, which picks a letter from the semitone count,\n * the diatonic number decides the letter here: transposing C by an augmented\n * second gives D#, not Eb.\n *\n * @param note The note to transpose.\n * @param interval The interval to apply; a negative span transposes downward.\n * @returns The transposed note, octave-less if the source was.\n * @example\n * ```ts\n * import { formatNote, parseInterval, parseNote, transposeByInterval } from '@libraz/libcantus';\n * formatNote(transposeByInterval(parseNote('C4'), parseInterval('A2'))); // 'D#4'\n * ```\n * @category Pitch & Intervals\n */\nexport function transposeByInterval(note: Note, interval: SpelledInterval): Note {\n assertNote(note, 'note');\n assertInteger(interval.number, 'interval.number', 1, 64);\n assertFiniteNumber(interval.semitones, 'interval.semitones');\n const descending = interval.semitones < 0;\n const letterSteps = (interval.number - 1) * (descending ? -1 : 1);\n const absoluteLetter = mod7(note.letter) + letterSteps;\n const letter = mod7(absoluteLetter);\n const natural = LETTER_SEMITONES[letter] ?? 0;\n const steps = Math.round(interval.semitones);\n if (note.octave === undefined) {\n return { letter, alter: alterFor(natural, mod12(noteToPitchClass(note) + steps)) };\n }\n const octave = note.octave + Math.floor(absoluteLetter / 7);\n return { letter, alter: noteToMidi(note) + steps - ((octave + 1) * 12 + natural), octave };\n}\n\n/** Diatonic ladder index of a note (letter + 7 * octave when octave-bearing). */\nfunction diatonicIndex(note: Note): number {\n return mod7(note.letter) + 7 * (note.octave ?? 0);\n}\n\n/**\n * The spelled interval from one note to another.\n *\n * When both notes carry octaves, the interval reflects their full signed\n * distance (so `C4 -> C5` is a perfect octave). When either lacks an octave the\n * interval is measured within a single ascending octave from `a` to `b`.\n *\n * @param a The lower/first note.\n * @param b The upper/second note.\n * @returns The diatonic number, quality, and semitone span.\n * @example\n * ```ts\n * import { spelledInterval, parseNote } from '@libraz/libcantus';\n * spelledInterval(parseNote('C4'), parseNote('G4'));\n * // { number: 5, quality: 'P', semitones: 7 }\n * ```\n * @category Pitch & Intervals\n */\nexport function spelledInterval(a: Note, b: Note): SpelledInterval {\n assertNote(a, 'a');\n assertNote(b, 'b');\n // Two octave-bearing notes measure their full signed distance; two\n // octave-less ones measure a single ascending octave. One of each has no\n // defined answer, and silently picking either reading gives a number the\n // caller cannot interpret.\n if ((a.octave === undefined) !== (b.octave === undefined)) {\n throw new InvalidInputError(\n 'spelledInterval needs both notes to carry an octave or neither to; received one of each',\n );\n }\n const octaved = a.octave !== undefined && b.octave !== undefined;\n let letterSteps: number;\n let semitones: number;\n if (octaved) {\n letterSteps = diatonicIndex(b) - diatonicIndex(a);\n // Signed pitch distance from a to b, so a note below a yields a negative span.\n semitones = noteToMidi(b) - noteToMidi(a);\n } else {\n letterSteps = mod7(b.letter - a.letter);\n let rawSemis = mod12(noteToPitchClass(b) - noteToPitchClass(a));\n // Lift the chromatic span into the octave nearest the diatonic reference so\n // wraparound intervals stay consistent with the diatonic number instead of\n // collapsing modulo 12. The correction is bidirectional: an upward wrap\n // (e.g. Ab -> G# = augmented seventh) adds an octave, while a same-letter or\n // downward chromatic step (e.g. E -> Eb, F# -> F, C -> Cb = descending\n // diminished/augmented unison) subtracts one so the sign follows pitch\n // direction instead of returning a garbage stack of augmentations.\n const reference = SIMPLE_REFERENCE[letterSteps + 1] ?? 0;\n if (reference - rawSemis > 6) {\n rawSemis += 12;\n } else if (rawSemis - reference > 6) {\n rawSemis -= 12;\n }\n semitones = rawSemis;\n }\n const absSteps = Math.abs(letterSteps);\n const number = absSteps + 1;\n // The span is measured in the direction the letters move, not as a magnitude:\n // C# up to Dbb steps up one letter but down one semitone, which is a doubly\n // diminished second, not a minor one. `semitones` keeps its own sign.\n //\n // A unison is the exception: the letters do not move, so there is no\n // direction to measure against and the quality names the chromatic\n // alteration alone — C down to Cb is a descending augmented unison.\n const directedSpan =\n letterSteps === 0 ? Math.abs(semitones) : letterSteps > 0 ? semitones : -semitones;\n const quality = qualityFromSpan(number, directedSpan);\n return { number, quality, semitones };\n}\n","import { assertFiniteNumber } from '../validation/index.js';\n\n/**\n * Counterpoint classification of a harmonic interval by consonance.\n *\n * Distinct from a spelled interval's quality label — see\n * {@link IntervalQualityLabel}, which names the `'P'` / `'M'` / `'m'` / `'A'` /\n * `'d'` spelling of an interval rather than how it sounds against another\n * voice.\n *\n * @see {@link IntervalQualityLabel}\n * @category Pitch & Intervals\n */\nexport enum ConsonanceClass {\n PerfectConsonance = 0,\n ImperfectConsonance = 1,\n Dissonance = 2,\n}\n\n/**\n * Reduce an interval to a simple interval class in the range [0, 11].\n *\n * Every public predicate in this module funnels through here, so a non-finite\n * interval is rejected once rather than silently classifying as a dissonance.\n */\nfunction simpleInterval(semitones: number): number {\n assertFiniteNumber(semitones, 'semitones');\n return Math.abs(Math.round(semitones)) % 12;\n}\n\n/**\n * Classify an interval for counterpoint evaluation.\n *\n * The interval is reduced modulo 12, so compound intervals classify as their\n * simple equivalents. The perfect fourth (5) is context-dependent: with\n * `twoVoice` (the default) it classifies as a dissonance, matching\n * {@link isConsonantInterval}; otherwise it is an imperfect consonance.\n *\n * @param semitones Interval size in semitones (may be negative or compound).\n * @param twoVoice When true, the perfect fourth is treated as dissonant.\n * @returns The counterpoint quality of the interval.\n * @example\n * ```ts\n * import { classifyInterval, ConsonanceClass } from '@libraz/libcantus';\n * classifyInterval(7); // ConsonanceClass.PerfectConsonance\n * classifyInterval(5); // ConsonanceClass.Dissonance (two-voice)\n * classifyInterval(5, false); // ConsonanceClass.ImperfectConsonance\n * ```\n * @category Pitch & Intervals\n */\nexport function classifyInterval(semitones: number, twoVoice = true): ConsonanceClass {\n const pc = simpleInterval(semitones);\n if (pc === 0 || pc === 7) {\n return ConsonanceClass.PerfectConsonance;\n }\n if (pc === 3 || pc === 4 || pc === 8 || pc === 9) {\n return ConsonanceClass.ImperfectConsonance;\n }\n if (pc === 5) {\n return twoVoice ? ConsonanceClass.Dissonance : ConsonanceClass.ImperfectConsonance;\n }\n return ConsonanceClass.Dissonance;\n}\n\n/**\n * Test whether an interval is a perfect consonance: the unison/octave or the\n * fifth.\n *\n * The perfect fourth is deliberately excluded. This is the predicate the\n * parallel-motion rules are written against, where the fourth behaves as a\n * dissonance between two voices; {@link classifyInterval} is the general\n * classifier, and it treats the fourth as context-dependent.\n *\n * @param semitones Interval size in semitones (may be negative or compound).\n * @returns True for the unison/octave (0) and the perfect fifth (7) mod 12.\n * @category Pitch & Intervals\n */\nexport function isPerfectInterval(semitones: number): boolean {\n const pc = simpleInterval(semitones);\n return pc === 0 || pc === 7;\n}\n\n/**\n * Test whether an interval is consonant.\n *\n * The consonant set is {0, 3, 4, 5, 7, 8, 9} mod 12. In two-voice counterpoint\n * the perfect fourth (5) is treated as a dissonance, so `twoVoice` set to true\n * excludes it.\n *\n * @param semitones Interval size in semitones (may be negative or compound).\n * @param twoVoice When true, the perfect fourth is treated as dissonant.\n * @returns True if the interval is consonant in the given context.\n * @category Pitch & Intervals\n */\nexport function isConsonantInterval(semitones: number, twoVoice = true): boolean {\n const pc = simpleInterval(semitones);\n if (pc === 5) {\n return !twoVoice;\n }\n return pc === 0 || pc === 3 || pc === 4 || pc === 7 || pc === 8 || pc === 9;\n}\n"]}