@libraz/libcantus 0.9.4 → 0.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +27 -18
  2. package/README_ja.md +268 -0
  3. package/dist/analyze/index.cjs +7 -7
  4. package/dist/analyze/index.d.cts +9 -99
  5. package/dist/analyze/index.d.ts +9 -99
  6. package/dist/analyze/index.js +6 -6
  7. package/dist/{chunk-36N73KZ3.cjs → chunk-2A2QMA74.cjs} +80 -65
  8. package/dist/chunk-2A2QMA74.cjs.map +1 -0
  9. package/dist/{chunk-YLEMGEFH.js → chunk-2PTFGVD7.js} +50 -35
  10. package/dist/chunk-2PTFGVD7.js.map +1 -0
  11. package/dist/{chunk-ZEJR4TKL.cjs → chunk-357ZDPVU.cjs} +36 -47
  12. package/dist/chunk-357ZDPVU.cjs.map +1 -0
  13. package/dist/{chunk-EHVEBWM5.js → chunk-3KX76RLG.js} +136 -37
  14. package/dist/chunk-3KX76RLG.js.map +1 -0
  15. package/dist/{chunk-RC6GBMCD.cjs → chunk-4VZ5MZAM.cjs} +155 -111
  16. package/dist/chunk-4VZ5MZAM.cjs.map +1 -0
  17. package/dist/{chunk-RK35RIN7.js → chunk-7EONYVI5.js} +115 -28
  18. package/dist/chunk-7EONYVI5.js.map +1 -0
  19. package/dist/{chunk-GQSGS2N4.js → chunk-7JEP3MFY.js} +5 -16
  20. package/dist/chunk-7JEP3MFY.js.map +1 -0
  21. package/dist/{chunk-7NKDPF74.js → chunk-AID75RN3.js} +203 -104
  22. package/dist/chunk-AID75RN3.js.map +1 -0
  23. package/dist/{chunk-FNDWXRRI.cjs → chunk-BSLTLGM3.cjs} +55 -37
  24. package/dist/chunk-BSLTLGM3.cjs.map +1 -0
  25. package/dist/{chunk-NP4NA2FB.cjs → chunk-CU6VG2YM.cjs} +39 -36
  26. package/dist/chunk-CU6VG2YM.cjs.map +1 -0
  27. package/dist/{chunk-NYYWMV2C.js → chunk-DZMHIGI4.js} +27 -17
  28. package/dist/chunk-DZMHIGI4.js.map +1 -0
  29. package/dist/{chunk-I24MV65U.js → chunk-EDOTEABK.js} +18 -15
  30. package/dist/{chunk-I24MV65U.js.map → chunk-EDOTEABK.js.map} +1 -1
  31. package/dist/{chunk-NKIBQ6PT.js → chunk-HN2OEUN2.js} +7 -4
  32. package/dist/chunk-HN2OEUN2.js.map +1 -0
  33. package/dist/{chunk-D2FAJBYL.js → chunk-KBTTEX7Q.js} +30 -12
  34. package/dist/chunk-KBTTEX7Q.js.map +1 -0
  35. package/dist/{chunk-CAN76NCU.cjs → chunk-KQN2ZYND.cjs} +147 -102
  36. package/dist/chunk-KQN2ZYND.cjs.map +1 -0
  37. package/dist/{chunk-4HLLZPXT.cjs → chunk-MPTVJIVG.cjs} +350 -251
  38. package/dist/chunk-MPTVJIVG.cjs.map +1 -0
  39. package/dist/{chunk-RUMU3UA6.cjs → chunk-MTMZTMWM.cjs} +12 -12
  40. package/dist/{chunk-RUMU3UA6.cjs.map → chunk-MTMZTMWM.cjs.map} +1 -1
  41. package/dist/{chunk-KMHOICIY.js → chunk-NNB67XA2.js} +90 -45
  42. package/dist/chunk-NNB67XA2.js.map +1 -0
  43. package/dist/{chunk-LW53AHQU.cjs → chunk-OL3IHHSJ.cjs} +228 -141
  44. package/dist/chunk-OL3IHHSJ.cjs.map +1 -0
  45. package/dist/{chunk-7MGZ7D62.cjs → chunk-PI3UXRWK.cjs} +43 -13
  46. package/dist/chunk-PI3UXRWK.cjs.map +1 -0
  47. package/dist/{chunk-XEBG3G53.js → chunk-R2VVFAIN.js} +42 -12
  48. package/dist/chunk-R2VVFAIN.js.map +1 -0
  49. package/dist/{chunk-Q6OP4U43.cjs → chunk-WFDXOLUB.cjs} +50 -40
  50. package/dist/chunk-WFDXOLUB.cjs.map +1 -0
  51. package/dist/{chunk-NVE34YGZ.cjs → chunk-WTAN4ACG.cjs} +212 -113
  52. package/dist/chunk-WTAN4ACG.cjs.map +1 -0
  53. package/dist/{chunk-FCANXBAT.cjs → chunk-YFJXS3AU.cjs} +8 -5
  54. package/dist/chunk-YFJXS3AU.cjs.map +1 -0
  55. package/dist/{chunk-Q2XUP227.js → chunk-YUE6TJ4M.js} +89 -45
  56. package/dist/chunk-YUE6TJ4M.js.map +1 -0
  57. package/dist/{chunk-HKGYCQ34.js → chunk-ZGLJM4A4.js} +2 -2
  58. package/dist/core/index.cjs +15 -5
  59. package/dist/core/index.cjs.map +1 -1
  60. package/dist/core/index.d.cts +14 -4
  61. package/dist/core/index.d.ts +14 -4
  62. package/dist/core/index.js +14 -4
  63. package/dist/{function-DNugAOrt.d.cts → function-CxixTB02.d.cts} +98 -7
  64. package/dist/{function-BtiZb9i9.d.ts → function-DTCt675z.d.ts} +98 -7
  65. package/dist/generate/index.cjs +10 -10
  66. package/dist/generate/index.d.cts +21 -21
  67. package/dist/generate/index.d.ts +21 -21
  68. package/dist/generate/index.js +9 -9
  69. package/dist/{index-tJhkmQgE.d.cts → index-B2Ulxxpo.d.cts} +3 -2
  70. package/dist/{index-Be74mcon.d.cts → index-BkYJ0xMq.d.cts} +2 -2
  71. package/dist/{index-6VR8-kit.d.ts → index-BuaB-ypt.d.ts} +3 -2
  72. package/dist/{index-70p168VM.d.ts → index-CAN7v-kw.d.ts} +9 -2
  73. package/dist/{index-C83mZI1P.d.ts → index-COw1Ky9g.d.ts} +1 -1
  74. package/dist/{index-tSQh-xwY.d.ts → index-Cfx8xgH0.d.ts} +1 -1
  75. package/dist/{index-C5UBbQ4G.d.ts → index-DRtIhlJj.d.ts} +2 -2
  76. package/dist/{index-BtXT6UQ0.d.cts → index-alrRninU.d.cts} +9 -2
  77. package/dist/{index-y_ak9_Hi.d.cts → index-oTQbYYIs.d.cts} +1 -1
  78. package/dist/{index-OG65ocOr.d.ts → index-tR9jTn26.d.cts} +4 -1
  79. package/dist/{index-OG65ocOr.d.cts → index-tR9jTn26.d.ts} +4 -1
  80. package/dist/{index-D6b6na69.d.cts → index-x2vcsduE.d.cts} +1 -1
  81. package/dist/index.cjs +24 -14
  82. package/dist/index.cjs.map +1 -1
  83. package/dist/index.d.cts +11 -11
  84. package/dist/index.d.ts +11 -11
  85. package/dist/index.js +23 -13
  86. package/dist/model/index.cjs +7 -7
  87. package/dist/model/index.d.cts +48 -16
  88. package/dist/model/index.d.ts +48 -16
  89. package/dist/model/index.js +6 -6
  90. package/dist/{styled-DIRkk9Uz.d.ts → styled-Bl0ZK28h.d.ts} +40 -3
  91. package/dist/{styled-DJbkJQfj.d.cts → styled-Crv6SA4S.d.cts} +40 -3
  92. package/dist/theory/index.cjs +6 -6
  93. package/dist/theory/index.d.cts +9 -9
  94. package/dist/theory/index.d.ts +9 -9
  95. package/dist/theory/index.js +5 -5
  96. package/package.json +12 -9
  97. package/dist/chunk-36N73KZ3.cjs.map +0 -1
  98. package/dist/chunk-4HLLZPXT.cjs.map +0 -1
  99. package/dist/chunk-7MGZ7D62.cjs.map +0 -1
  100. package/dist/chunk-7NKDPF74.js.map +0 -1
  101. package/dist/chunk-CAN76NCU.cjs.map +0 -1
  102. package/dist/chunk-D2FAJBYL.js.map +0 -1
  103. package/dist/chunk-EHVEBWM5.js.map +0 -1
  104. package/dist/chunk-FCANXBAT.cjs.map +0 -1
  105. package/dist/chunk-FNDWXRRI.cjs.map +0 -1
  106. package/dist/chunk-GQSGS2N4.js.map +0 -1
  107. package/dist/chunk-KMHOICIY.js.map +0 -1
  108. package/dist/chunk-LW53AHQU.cjs.map +0 -1
  109. package/dist/chunk-NKIBQ6PT.js.map +0 -1
  110. package/dist/chunk-NP4NA2FB.cjs.map +0 -1
  111. package/dist/chunk-NVE34YGZ.cjs.map +0 -1
  112. package/dist/chunk-NYYWMV2C.js.map +0 -1
  113. package/dist/chunk-Q2XUP227.js.map +0 -1
  114. package/dist/chunk-Q6OP4U43.cjs.map +0 -1
  115. package/dist/chunk-RC6GBMCD.cjs.map +0 -1
  116. package/dist/chunk-RK35RIN7.js.map +0 -1
  117. package/dist/chunk-XEBG3G53.js.map +0 -1
  118. package/dist/chunk-YLEMGEFH.js.map +0 -1
  119. package/dist/chunk-ZEJR4TKL.cjs.map +0 -1
  120. /package/dist/{chunk-HKGYCQ34.js.map → chunk-ZGLJM4A4.js.map} +0 -0
@@ -21,7 +21,11 @@ var BudgetExceededError = class extends RangeError {
21
21
  name = "BudgetExceededError";
22
22
  };
23
23
  function isLibcantusError(value) {
24
- return value instanceof InvalidInputError || value instanceof NoSolutionError || value instanceof BudgetExceededError;
24
+ if (typeof value !== "object" || value === null || !("code" in value)) {
25
+ return false;
26
+ }
27
+ const code = value.code;
28
+ return code === "INVALID_INPUT" || code === "NO_SOLUTION" || code === "BUDGET_EXCEEDED";
25
29
  }
26
30
 
27
31
  // src/core/meter/internal.ts
@@ -76,9 +80,23 @@ function assertInteger(value, name, min = Number.MIN_SAFE_INTEGER, max = Number.
76
80
  }
77
81
  return value;
78
82
  }
83
+ function assertMidiPitch(value, name = "MIDI pitch") {
84
+ return assertInteger(value, name, 0, 127);
85
+ }
86
+ function clampToMidi(value, name = "MIDI pitch") {
87
+ assertInteger(value, name);
88
+ return Math.min(127, Math.max(0, value));
89
+ }
90
+ function assertDegree(value, name = "degree") {
91
+ return assertInteger(value, name, -1e3, 1e3);
92
+ }
93
+ function assertFiniteSemitones(value, name = "semitones") {
94
+ return assertFiniteNumber(value, name);
95
+ }
79
96
  function dropSilentNotes(events) {
80
97
  return events.filter((event) => event !== void 0 && event.durationBeat > 0);
81
98
  }
99
+ var soundingNotesOnly = dropSilentNotes;
82
100
  function assertOneOf(value, allowed, name) {
83
101
  if (typeof value !== "string" || !allowed.includes(value)) {
84
102
  throw new InvalidInputError(
@@ -131,7 +149,7 @@ function assertTimeSignature(ts, name = "time signature") {
131
149
  return ts;
132
150
  }
133
151
  function assertNoteEvent(event, name = "note event", options = {}) {
134
- assertInteger(event.pitch, `${name}.pitch`, 0, 127);
152
+ assertMidiPitch(event.pitch, `${name}.pitch`);
135
153
  assertRange(event.startBeat, 0, Number.MAX_SAFE_INTEGER, `${name}.startBeat`);
136
154
  assertFiniteNumber(event.durationBeat, `${name}.durationBeat`);
137
155
  if (!options.allowNonPositiveDuration && event.durationBeat <= 0) {
@@ -152,7 +170,7 @@ function assertNoteEvent(event, name = "note event", options = {}) {
152
170
  }
153
171
  function assertNoteEvents(events, name = "note events", options = {}) {
154
172
  if (!Array.isArray(events)) {
155
- throw new TypeError(`${name} must be an array; received ${typeof events}`);
173
+ throw new InvalidInputError(`${name} must be an array; received ${typeof events}`);
156
174
  }
157
175
  assertGenerationBudget(events.length, `${name} count`, options.budget);
158
176
  for (let index = 0; index < events.length; index += 1) {
@@ -183,6 +201,7 @@ function pitchClassOf(value) {
183
201
  }
184
202
  var mod12 = pitchClassOf;
185
203
  function diatonicLetterOf(value) {
204
+ assertFiniteNumber(value, "diatonic letter");
186
205
  return (Math.round(value) % 7 + 7) % 7;
187
206
  }
188
207
  function naturalPitchClassOf(letter) {
@@ -190,6 +209,9 @@ function naturalPitchClassOf(letter) {
190
209
  }
191
210
  var mod7 = diatonicLetterOf;
192
211
  function parseNote(text) {
212
+ if (typeof text !== "string") {
213
+ throw new InvalidInputError(`note must be a string; received ${typeof text}`);
214
+ }
193
215
  const match = /^([A-Ga-g])([#x]*|b*)(-?\d+)?$/.exec(text.trim());
194
216
  if (!match) {
195
217
  throw new InvalidInputError(`Invalid note: ${text}`);
@@ -286,8 +308,11 @@ function transposeNote(note, semitones, opts) {
286
308
  if (opts?.spelling !== void 0) {
287
309
  return note.octave === void 0 ? bareOf(midiToNote(60 + mod12(noteToPitchClass(note) + steps), opts.spelling)) : midiToNote(noteToMidi(note) + steps, opts.spelling);
288
310
  }
289
- const octaves = Math.floor(steps / 12);
290
- const letterSteps = (LETTER_STEPS_BY_SEMITONE[steps - 12 * octaves] ?? 0) + 7 * octaves;
311
+ const direction = steps < 0 ? -1 : 1;
312
+ const magnitude = Math.abs(steps);
313
+ const octaves = Math.floor(magnitude / 12);
314
+ const withinOctave = magnitude % 12;
315
+ const letterSteps = direction * ((LETTER_STEPS_BY_SEMITONE[withinOctave] ?? 0) + 7 * octaves);
291
316
  const absoluteLetter = mod7(note.letter) + letterSteps;
292
317
  const letter = mod7(absoluteLetter);
293
318
  const natural = LETTER_SEMITONES[letter] ?? 0;
@@ -354,7 +379,7 @@ function intervalSemitones(numberValue, quality) {
354
379
  return reference + quality.length;
355
380
  }
356
381
  if (/^d+$/.test(quality)) {
357
- return reference - quality.length - (perfect ? 0 : 1);
382
+ return Math.abs(reference - quality.length - (perfect ? 0 : 1));
358
383
  }
359
384
  throw new InvalidInputError(`unknown interval quality ${JSON.stringify(quality)}`);
360
385
  }
@@ -371,9 +396,9 @@ function parseInterval(name) {
371
396
  }
372
397
  function transposeByInterval(note, interval) {
373
398
  assertNote(note, "note");
374
- assertInteger(interval.number, "interval.number", 1, 64);
399
+ assertInteger(interval.number, "interval.number", 1);
375
400
  assertFiniteNumber(interval.semitones, "interval.semitones");
376
- const descending = interval.semitones < 0;
401
+ const descending = interval.descending ?? interval.semitones < 0;
377
402
  const letterSteps = (interval.number - 1) * (descending ? -1 : 1);
378
403
  const absoluteLetter = mod7(note.letter) + letterSteps;
379
404
  const letter = mod7(absoluteLetter);
@@ -417,7 +442,8 @@ function spelledInterval(a, b) {
417
442
  const number = absSteps + 1;
418
443
  const directedSpan = letterSteps === 0 ? Math.abs(semitones) : letterSteps > 0 ? semitones : -semitones;
419
444
  const quality = qualityFromSpan(number, directedSpan);
420
- return { number, quality, semitones };
445
+ const descending = letterSteps < 0 || letterSteps === 0 && semitones < 0;
446
+ return { number, quality, semitones, descending };
421
447
  }
422
448
 
423
449
  // src/core/interval/index.ts
@@ -428,8 +454,7 @@ var ConsonanceClass = /* @__PURE__ */ ((ConsonanceClass2) => {
428
454
  return ConsonanceClass2;
429
455
  })(ConsonanceClass || {});
430
456
  function simpleInterval(semitones) {
431
- assertFiniteNumber(semitones, "semitones");
432
- return Math.abs(Math.round(semitones)) % 12;
457
+ return pitchClassOf(Math.abs(semitones));
433
458
  }
434
459
  function classifyInterval(semitones, twoVoice = true) {
435
460
  const pc = simpleInterval(semitones);
@@ -469,7 +494,12 @@ export {
469
494
  DEFAULT_GENERATION_BUDGET,
470
495
  assertFiniteNumber,
471
496
  assertInteger,
497
+ assertMidiPitch,
498
+ clampToMidi,
499
+ assertDegree,
500
+ assertFiniteSemitones,
472
501
  dropSilentNotes,
502
+ soundingNotesOnly,
473
503
  assertOneOf,
474
504
  assertPositiveInt,
475
505
  assertRange,
@@ -495,4 +525,4 @@ export {
495
525
  isPerfectInterval,
496
526
  isConsonantInterval
497
527
  };
498
- //# sourceMappingURL=chunk-XEBG3G53.js.map
528
+ //# sourceMappingURL=chunk-R2VVFAIN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../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"],"sourcesContent":["/**\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 if (typeof value !== 'object' || value === null || !('code' in value)) {\n return false;\n }\n const code = value.code;\n return code === 'INVALID_INPUT' || code === 'NO_SOLUTION' || code === 'BUDGET_EXCEEDED';\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/** Require an integer MIDI pitch in the inclusive range 0..127. */\nexport function assertMidiPitch(value: number, name = 'MIDI pitch'): number {\n return assertInteger(value, name, 0, 127);\n}\n\n/** Clamp an integer MIDI pitch to the inclusive range 0..127. */\nexport function clampToMidi(value: number, name = 'MIDI pitch'): number {\n assertInteger(value, name);\n return Math.min(127, Math.max(0, value));\n}\n\n/** Require a bounded integer scale/chord degree. */\nexport function assertDegree(value: number, name = 'degree'): number {\n return assertInteger(value, name, -1000, 1000);\n}\n\n/** Require a finite semitone offset before pitch-class reduction. */\nexport function assertFiniteSemitones(value: number, name = 'semitones'): number {\n return assertFiniteNumber(value, name);\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/** Alias that makes the shared positive-duration policy explicit at API entrances. */\nexport const soundingNotesOnly = dropSilentNotes;\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 assertMidiPitch(event.pitch, `${name}.pitch`);\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 InvalidInputError(`${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 /** True when the diatonic letter movement is descending despite a zero span. */\n descending?: boolean;\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 assertFiniteNumber(value, 'diatonic letter');\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 if (typeof text !== 'string') {\n throw new InvalidInputError(`note must be a string; received ${typeof text}`);\n }\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 direction = steps < 0 ? -1 : 1;\n const magnitude = Math.abs(steps);\n const octaves = Math.floor(magnitude / 12);\n const withinOctave = magnitude % 12;\n // Descending motion reverses the conventional ascending letter distance.\n // Deriving it from floor(negative / 12) treated -6 as a descending fourth,\n // so transpose(+n) followed by transpose(-n) changed the letter spelling.\n const letterSteps = direction * ((LETTER_STEPS_BY_SEMITONE[withinOctave] ?? 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 Math.abs(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. Diminished unisons have a descending\n * (negative) semitone span; all other supported names are ascending.\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 // `spelledInterval` accepts the full supported octave range, so accepting\n // only 64 here made a value produced by that sibling public function\n // impossible to apply back to its source note.\n assertInteger(interval.number, 'interval.number', 1);\n assertFiniteNumber(interval.semitones, 'interval.semitones');\n const descending = interval.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 const descending = letterSteps < 0 || (letterSteps === 0 && semitones < 0);\n return { number, quality, semitones, descending };\n}\n","import { pitchClassOf } from '../pitch/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 return pitchClassOf(Math.abs(semitones));\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"],"mappings":"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","names":["ConsonanceClass"]}
@@ -6,13 +6,14 @@
6
6
 
7
7
 
8
8
 
9
- var _chunkFNDWXRRIcjs = require('./chunk-FNDWXRRI.cjs');
9
+ var _chunkBSLTLGM3cjs = require('./chunk-BSLTLGM3.cjs');
10
10
 
11
11
 
12
12
 
13
13
 
14
14
 
15
- var _chunk7MGZ7D62cjs = require('./chunk-7MGZ7D62.cjs');
15
+
16
+ var _chunkPI3UXRWKcjs = require('./chunk-PI3UXRWK.cjs');
16
17
 
17
18
  // src/theory/safety/index.ts
18
19
  var NoteSafety = /* @__PURE__ */ ((NoteSafety2) => {
@@ -38,6 +39,7 @@ var ReasonFlag = /* @__PURE__ */ ((ReasonFlag2) => {
38
39
  ReasonFlag2[ReasonFlag2["VoiceCrossing"] = 8192] = "VoiceCrossing";
39
40
  ReasonFlag2[ReasonFlag2["Suspension"] = 16384] = "Suspension";
40
41
  ReasonFlag2[ReasonFlag2["NeedsResolution"] = 32768] = "NeedsResolution";
42
+ ReasonFlag2[ReasonFlag2["MelodicTritone"] = 65536] = "MelodicTritone";
41
43
  return ReasonFlag2;
42
44
  })(ReasonFlag || {});
43
45
  var SUGGESTION_WINDOW = 12;
@@ -49,6 +51,7 @@ var REASON_SEVERITY = [
49
51
  [16 /* NonScale */, 2 /* Dissonant */, 2 /* Dissonant */],
50
52
  [32 /* OutOfRange */, 1 /* Warning */, 1 /* Warning */],
51
53
  [64 /* Tritone */, 2 /* Dissonant */, 2 /* Dissonant */],
54
+ [65536 /* MelodicTritone */, 1 /* Warning */, 2 /* Dissonant */],
52
55
  [128 /* LargeLeap */, 1 /* Warning */, 2 /* Dissonant */],
53
56
  [1024 /* VerticalDissonance */, 2 /* Dissonant */, 2 /* Dissonant */],
54
57
  [2048 /* ParallelPerfect */, 1 /* Warning */, 2 /* Dissonant */],
@@ -74,15 +77,18 @@ function avoidsMajorSeventh(chord) {
74
77
  return chord.intervals.includes(4) && chord.intervals.includes(10);
75
78
  }
76
79
  function isStructuralChordDissonance(a, b, chord) {
77
- const cls = Math.abs(Math.trunc(a) - Math.trunc(b)) % 12;
78
- if (cls !== 6 && cls !== 10 && cls !== 11) {
80
+ const rawClass = _chunkPI3UXRWKcjs.pitchClassOf.call(void 0, Math.abs(a - b));
81
+ const aOffset = _chunkBSLTLGM3cjs.intervalAboveRoot.call(void 0, a, chord);
82
+ const bOffset = _chunkBSLTLGM3cjs.intervalAboveRoot.call(void 0, b, chord);
83
+ const rootSeventh = aOffset === 0 && (bOffset === 10 || bOffset === 11) || bOffset === 0 && (aOffset === 10 || aOffset === 11);
84
+ if (rawClass !== 6 && !rootSeventh) {
79
85
  return false;
80
86
  }
81
- return _chunkFNDWXRRIcjs.isChordMember.call(void 0, a, chord) && _chunkFNDWXRRIcjs.isChordMember.call(void 0, b, chord);
87
+ return _chunkBSLTLGM3cjs.isChordMember.call(void 0, a, chord) && _chunkBSLTLGM3cjs.isChordMember.call(void 0, b, chord);
82
88
  }
83
89
  function stepResolution(pitch, chord) {
84
90
  for (let delta = 1; delta <= 2; delta += 1) {
85
- if (_chunkFNDWXRRIcjs.isChordMember.call(void 0, pitch - delta, chord)) {
91
+ if (_chunkBSLTLGM3cjs.isChordMember.call(void 0, pitch - delta, chord)) {
86
92
  return pitch - delta;
87
93
  }
88
94
  }
@@ -90,23 +96,23 @@ function stepResolution(pitch, chord) {
90
96
  }
91
97
  function evaluateSafety(q, opts = {}) {
92
98
  assertSafetyContext(q);
93
- _chunk7MGZ7D62cjs.assertFiniteNumber.call(void 0, q.candidatePitch, "candidatePitch");
99
+ _chunkPI3UXRWKcjs.assertFiniteNumber.call(void 0, q.candidatePitch, "candidatePitch");
94
100
  return evaluateInternal(q, _nullishCoalesce(opts.suggestions, () => ( true)));
95
101
  }
96
102
  function assertSafetyContext(q) {
97
- if (q.prevPitch !== void 0) _chunk7MGZ7D62cjs.assertFiniteNumber.call(void 0, q.prevPitch, "prevPitch");
98
- if (q.vocalLow !== void 0) _chunk7MGZ7D62cjs.assertFiniteNumber.call(void 0, q.vocalLow, "vocalLow");
99
- if (q.vocalHigh !== void 0) _chunk7MGZ7D62cjs.assertFiniteNumber.call(void 0, q.vocalHigh, "vocalHigh");
103
+ if (q.prevPitch !== void 0) _chunkPI3UXRWKcjs.assertFiniteNumber.call(void 0, q.prevPitch, "prevPitch");
104
+ if (q.vocalLow !== void 0) _chunkPI3UXRWKcjs.assertFiniteNumber.call(void 0, q.vocalLow, "vocalLow");
105
+ if (q.vocalHigh !== void 0) _chunkPI3UXRWKcjs.assertFiniteNumber.call(void 0, q.vocalHigh, "vocalHigh");
100
106
  if (q.vocalLow !== void 0 && q.vocalHigh !== void 0 && q.vocalLow > q.vocalHigh) {
101
- throw new (0, _chunk7MGZ7D62cjs.InvalidInputError)("vocalLow must not exceed vocalHigh");
107
+ throw new (0, _chunkPI3UXRWKcjs.InvalidInputError)("vocalLow must not exceed vocalHigh");
102
108
  }
103
- _chunk7MGZ7D62cjs.assertGenerationBudget.call(void 0, q.otherVoices.length, "other voices");
109
+ _chunkPI3UXRWKcjs.assertGenerationBudget.call(void 0, q.otherVoices.length, "other voices");
104
110
  for (let index = 0; index < q.otherVoices.length; index += 1) {
105
111
  const voice = q.otherVoices[index];
106
112
  if (!voice) continue;
107
- _chunk7MGZ7D62cjs.assertFiniteNumber.call(void 0, voice.pitch, `otherVoices[${index}].pitch`);
113
+ _chunkPI3UXRWKcjs.assertFiniteNumber.call(void 0, voice.pitch, `otherVoices[${index}].pitch`);
108
114
  if (voice.prevPitch !== void 0) {
109
- _chunk7MGZ7D62cjs.assertFiniteNumber.call(void 0, voice.prevPitch, `otherVoices[${index}].prevPitch`);
115
+ _chunkPI3UXRWKcjs.assertFiniteNumber.call(void 0, voice.prevPitch, `otherVoices[${index}].prevPitch`);
110
116
  }
111
117
  }
112
118
  }
@@ -126,12 +132,16 @@ function evaluateInternal(q, collectSuggestions) {
126
132
  reasons |= 32 /* OutOfRange */;
127
133
  raise(1 /* Warning */);
128
134
  }
129
- const inScale = _chunkFNDWXRRIcjs.isScaleTone.call(void 0, pitch, q.key);
135
+ const inScale = _chunkBSLTLGM3cjs.isScaleTone.call(void 0, pitch, q.key);
130
136
  const chord = q.chord;
131
- if (chord && _chunkFNDWXRRIcjs.isChordMember.call(void 0, pitch, chord)) {
137
+ if (!inScale && (chord === null || !_chunkBSLTLGM3cjs.isChordMember.call(void 0, pitch, chord))) {
138
+ reasons |= 16 /* NonScale */;
139
+ raise(2 /* Dissonant */);
140
+ }
141
+ if (chord && _chunkBSLTLGM3cjs.isChordMember.call(void 0, pitch, chord)) {
132
142
  reasons |= 1 /* ChordTone */;
133
143
  } else if (chord) {
134
- const ic = _chunkFNDWXRRIcjs.intervalAboveRoot.call(void 0, pitch, chord);
144
+ const ic = _chunkBSLTLGM3cjs.intervalAboveRoot.call(void 0, pitch, chord);
135
145
  const isTension = ic === 2 || ic === 5 || ic === 9;
136
146
  const avoid = ic === 5 && avoidsFourth(chord) || ic === 11 && avoidsMajorSeventh(chord);
137
147
  if (avoid) {
@@ -148,19 +158,13 @@ function evaluateInternal(q, collectSuggestions) {
148
158
  } else if (inScale) {
149
159
  reasons |= 8 /* ScaleTone */;
150
160
  raise(1 /* Warning */);
151
- } else {
152
- reasons |= 16 /* NonScale */;
153
- raise(2 /* Dissonant */);
154
161
  }
155
162
  } else if (inScale) {
156
163
  reasons |= 8 /* ScaleTone */;
157
164
  raise(1 /* Warning */);
158
- } else {
159
- reasons |= 16 /* NonScale */;
160
- raise(2 /* Dissonant */);
161
165
  }
162
- if (chord && !_chunkFNDWXRRIcjs.isChordMember.call(void 0, pitch, chord)) {
163
- const ic = _chunkFNDWXRRIcjs.intervalAboveRoot.call(void 0, pitch, chord);
166
+ if (chord && !_chunkBSLTLGM3cjs.isChordMember.call(void 0, pitch, chord)) {
167
+ const ic = _chunkBSLTLGM3cjs.intervalAboveRoot.call(void 0, pitch, chord);
164
168
  const toneOffsets = chord.intervals.map((i) => (i % 12 + 12) % 12);
165
169
  if (toneOffsets.some((t) => Math.abs(ic - t) === 6)) {
166
170
  reasons |= 64 /* Tritone */;
@@ -169,10 +173,10 @@ function evaluateInternal(q, collectSuggestions) {
169
173
  if (q.prevPitch !== void 0) {
170
174
  const pc = Math.abs(pitch - q.prevPitch) % 12;
171
175
  if (pc === 6) {
172
- reasons |= 64 /* Tritone */;
173
- raise(2 /* Dissonant */);
176
+ reasons |= 65536 /* MelodicTritone */;
177
+ raise(q.profile === "strict" ? 2 /* Dissonant */ : 1 /* Warning */);
174
178
  }
175
- if (_chunkFNDWXRRIcjs.isForbiddenMelodicLeap.call(void 0, q.prevPitch, pitch)) {
179
+ if (_chunkBSLTLGM3cjs.isForbiddenMelodicLeap.call(void 0, q.prevPitch, pitch)) {
176
180
  reasons |= 128 /* LargeLeap */;
177
181
  raise(parallelSeverity);
178
182
  }
@@ -187,7 +191,7 @@ function evaluateInternal(q, collectSuggestions) {
187
191
  const twoVoice = q.otherVoices.length === 1;
188
192
  const prev = q.prevPitch;
189
193
  for (const ov of q.otherVoices) {
190
- if (!_chunkFNDWXRRIcjs.createsVerticalDissonance.call(void 0, pitch, ov.pitch, twoVoice)) {
194
+ if (!_chunkBSLTLGM3cjs.createsVerticalDissonance.call(void 0, pitch, ov.pitch, twoVoice)) {
191
195
  continue;
192
196
  }
193
197
  if (chord && isStructuralChordDissonance(pitch, ov.pitch, chord)) {
@@ -195,7 +199,7 @@ function evaluateInternal(q, collectSuggestions) {
195
199
  }
196
200
  reasons |= 1024 /* VerticalDissonance */;
197
201
  raise(2 /* Dissonant */);
198
- if (prev !== void 0 && pitch === prev && ov.prevPitch !== void 0 && !_chunkFNDWXRRIcjs.createsVerticalDissonance.call(void 0, prev, ov.prevPitch, twoVoice)) {
202
+ if (prev !== void 0 && pitch === prev && ov.prevPitch !== void 0 && !_chunkBSLTLGM3cjs.createsVerticalDissonance.call(void 0, prev, ov.prevPitch, twoVoice)) {
199
203
  reasons |= 16384 /* Suspension */;
200
204
  }
201
205
  }
@@ -206,11 +210,11 @@ function evaluateInternal(q, collectSuggestions) {
206
210
  if (ov.prevPitch === void 0) {
207
211
  continue;
208
212
  }
209
- if (_chunkFNDWXRRIcjs.createsParallelPerfect.call(void 0, prev, pitch, ov.prevPitch, ov.pitch)) {
213
+ if (_chunkBSLTLGM3cjs.createsParallelPerfect.call(void 0, prev, pitch, ov.prevPitch, ov.pitch)) {
210
214
  reasons |= 2048 /* ParallelPerfect */;
211
215
  raise(parallelSeverity);
212
216
  }
213
- if (_chunkFNDWXRRIcjs.createsHiddenParallelPerfect.call(void 0, prev, pitch, ov.prevPitch, ov.pitch)) {
217
+ if (_chunkBSLTLGM3cjs.createsHiddenParallelPerfect.call(void 0, prev, pitch, ov.prevPitch, ov.pitch)) {
214
218
  reasons |= 4096 /* HiddenParallel */;
215
219
  raise(parallelSeverity);
216
220
  }
@@ -239,6 +243,9 @@ function findSafeNearby(q, candidate) {
239
243
  const out = [];
240
244
  for (let d = 1; d <= SUGGESTION_WINDOW; d += 1) {
241
245
  for (const p of [candidate - d, candidate + d]) {
246
+ if (p < 0 || p > 127) {
247
+ continue;
248
+ }
242
249
  if (evaluateInternal({ ...q, candidatePitch: p }, false).safety === 0 /* Safe */) {
243
250
  out.push(p);
244
251
  if (out.length >= MAX_SUGGESTIONS) {
@@ -257,6 +264,7 @@ var REASON_TEXT = [
257
264
  ],
258
265
  [1024 /* VerticalDissonance */, "Dissonant against a sounding voice on a strong beat"],
259
266
  [64 /* Tritone */, "Tritone against the sounding harmony"],
267
+ [65536 /* MelodicTritone */, "Melodic tritone leap"],
260
268
  [4 /* AvoidNote */, "Avoid note \u2014 clashes with a chord tone a semitone away"],
261
269
  [16 /* NonScale */, "Outside the key"],
262
270
  [2048 /* ParallelPerfect */, "Parallel perfect interval with another voice"],
@@ -266,8 +274,8 @@ var REASON_TEXT = [
266
274
  [32768 /* NeedsResolution */, "Dissonance awaiting a stepwise resolution"],
267
275
  [2 /* Tension */, "Chord tension"],
268
276
  [8 /* ScaleTone */, "Scale tone, not in the chord"],
269
- [256 /* MinorSecond */, "A semitone from a sounding voice"],
270
- [512 /* MajorSeventh */, "A major seventh from a sounding voice"],
277
+ [256 /* MinorSecond */, "Melodic semitone"],
278
+ [512 /* MajorSeventh */, "Melodic major seventh"],
271
279
  [1 /* ChordTone */, "Chord tone"]
272
280
  ];
273
281
  function severityOfReason(flag, profile) {
@@ -296,15 +304,17 @@ function enumerateSafePitches(q, pitchLow, pitchHigh) {
296
304
  assertSafetyContext(q);
297
305
  const chordTones = [];
298
306
  const others = [];
299
- _chunk7MGZ7D62cjs.assertInteger.call(void 0, pitchLow, "pitchLow");
300
- _chunk7MGZ7D62cjs.assertInteger.call(void 0, pitchHigh, "pitchHigh");
307
+ _chunkPI3UXRWKcjs.assertInteger.call(void 0, pitchLow, "pitchLow");
308
+ _chunkPI3UXRWKcjs.assertInteger.call(void 0, pitchHigh, "pitchHigh");
301
309
  if (pitchLow > pitchHigh) {
302
- throw new (0, _chunk7MGZ7D62cjs.InvalidInputError)(
310
+ throw new (0, _chunkPI3UXRWKcjs.InvalidInputError)(
303
311
  `pitchLow must not exceed pitchHigh; received ${pitchLow} > ${pitchHigh}`
304
312
  );
305
313
  }
306
- _chunk7MGZ7D62cjs.assertGenerationBudget.call(void 0, pitchHigh - pitchLow + 1, "safe pitch candidates");
307
- for (let pitch = pitchHigh; pitch >= pitchLow; pitch -= 1) {
314
+ _chunkPI3UXRWKcjs.assertGenerationBudget.call(void 0, pitchHigh - pitchLow + 1, "safe pitch candidates");
315
+ const low = Math.max(pitchLow, _nullishCoalesce(q.vocalLow, () => ( pitchLow)));
316
+ const high = Math.min(pitchHigh, _nullishCoalesce(q.vocalHigh, () => ( pitchHigh)));
317
+ for (let pitch = high; pitch >= low; pitch -= 1) {
308
318
  const result = evaluateInternal({ ...q, candidatePitch: pitch }, false);
309
319
  if (result.safety === 2 /* Dissonant */) {
310
320
  continue;
@@ -324,4 +334,4 @@ function enumerateSafePitches(q, pitchLow, pitchHigh) {
324
334
 
325
335
 
326
336
  exports.NoteSafety = NoteSafety; exports.ReasonFlag = ReasonFlag; exports.evaluateSafety = evaluateSafety; exports.enumerateSafePitches = enumerateSafePitches;
327
- //# sourceMappingURL=chunk-Q6OP4U43.cjs.map
337
+ //# sourceMappingURL=chunk-WFDXOLUB.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/libcantus/libcantus/dist/chunk-WFDXOLUB.cjs","../src/theory/safety/index.ts"],"names":["NoteSafety","ReasonFlag"],"mappings":"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","file":"/home/runner/work/libcantus/libcantus/dist/chunk-WFDXOLUB.cjs","sourcesContent":[null,"import { InvalidInputError } from '../../core/errors/index.js';\nimport { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport {\n assertFiniteNumber,\n assertGenerationBudget,\n assertInteger,\n} from '../../core/validation/index.js';\nimport type { Chord } from '../chord/index.js';\nimport { intervalAboveRoot, isChordMember } from '../chord/index.js';\nimport {\n createsHiddenParallelPerfect,\n createsParallelPerfect,\n createsVerticalDissonance,\n isForbiddenMelodicLeap,\n} from '../counterpoint/index.js';\nimport { isScaleTone } from '../scale/index.js';\n\n/**\n * Severity policy: `strict` counterpoint vs. lenient `pop` voice-leading.\n *\n * @category Arrangement & Analysis\n */\nexport type SafetyProfile = 'strict' | 'pop';\n\n/**\n * Overall placeability verdict for a candidate pitch.\n *\n * @category Arrangement & Analysis\n */\nexport enum NoteSafety {\n Safe = 0,\n Warning = 1,\n Dissonant = 2,\n}\n\n/**\n * Bit flags describing why a pitch received its verdict.\n *\n * @category Arrangement & Analysis\n */\nexport enum ReasonFlag {\n ChordTone = 1 << 0,\n Tension = 1 << 1,\n AvoidNote = 1 << 2,\n ScaleTone = 1 << 3,\n NonScale = 1 << 4,\n OutOfRange = 1 << 5,\n Tritone = 1 << 6,\n LargeLeap = 1 << 7,\n MinorSecond = 1 << 8,\n MajorSeventh = 1 << 9,\n VerticalDissonance = 1 << 10,\n ParallelPerfect = 1 << 11,\n HiddenParallel = 1 << 12,\n VoiceCrossing = 1 << 13,\n Suspension = 1 << 14,\n NeedsResolution = 1 << 15,\n MelodicTritone = 1 << 16,\n}\n\n/**\n * A neighbouring voice's current and previous pitch.\n *\n * @category Arrangement & Analysis\n */\nexport type VoiceSnapshot = {\n pitch: number;\n prevPitch?: number;\n};\n\n/**\n * Everything needed to evaluate one candidate pitch.\n *\n * @category Arrangement & Analysis\n */\nexport type SafetyQuery = {\n profile: SafetyProfile;\n candidatePitch: number;\n prevPitch?: number;\n chord: Chord | null;\n key: KeyScale;\n otherVoices: VoiceSnapshot[];\n strongBeat: boolean;\n vocalLow?: number;\n vocalHigh?: number;\n};\n\n/**\n * The verdict, reason flags, and optional resolution guidance.\n *\n * @category Arrangement & Analysis\n */\nexport type SafetyResult = {\n safety: NoteSafety;\n reasons: number;\n resolveTo?: number;\n suggestions?: number[];\n rationale?: string;\n};\n\n/** Semitone radius searched around a rejected candidate for safe alternatives. */\nconst SUGGESTION_WINDOW = 12;\n/** Maximum number of alternative pitches returned in `SafetyResult.suggestions`. */\nconst MAX_SUGGESTIONS = 3;\n\n/** Canonical minimum verdict contributed by each non-informational reason. */\nconst REASON_SEVERITY: ReadonlyArray<\n readonly [flag: ReasonFlag, pop: NoteSafety, strict: NoteSafety]\n> = [\n [ReasonFlag.Tension, NoteSafety.Warning, NoteSafety.Warning],\n [ReasonFlag.AvoidNote, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.ScaleTone, NoteSafety.Warning, NoteSafety.Warning],\n [ReasonFlag.NonScale, NoteSafety.Dissonant, NoteSafety.Dissonant],\n [ReasonFlag.OutOfRange, NoteSafety.Warning, NoteSafety.Warning],\n [ReasonFlag.Tritone, NoteSafety.Dissonant, NoteSafety.Dissonant],\n [ReasonFlag.MelodicTritone, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.LargeLeap, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.VerticalDissonance, NoteSafety.Dissonant, NoteSafety.Dissonant],\n [ReasonFlag.ParallelPerfect, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.HiddenParallel, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.VoiceCrossing, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.Suspension, NoteSafety.Dissonant, NoteSafety.Dissonant],\n [ReasonFlag.NeedsResolution, NoteSafety.Warning, NoteSafety.Dissonant],\n];\n\nfunction minimumSafetyForReasons(reasons: number, profile: SafetyProfile): NoteSafety {\n let minimum = NoteSafety.Safe;\n const severityIndex = profile === 'strict' ? 2 : 1;\n for (const entry of REASON_SEVERITY) {\n if (reasons & entry[0]) {\n minimum = Math.max(minimum, entry[severityIndex]) as NoteSafety;\n }\n }\n return minimum;\n}\n\n/** Whether the perfect fourth is an avoid note over this chord. */\nfunction avoidsFourth(chord: Chord): boolean {\n return chord.intervals.includes(4) && !chord.intervals.includes(5);\n}\n\n/** Whether the major seventh is an avoid note over this chord (dominant). */\nfunction avoidsMajorSeventh(chord: Chord): boolean {\n return chord.intervals.includes(4) && chord.intervals.includes(10);\n}\n\n/**\n * Whether a dissonance between two chord tones is one the chord is built from.\n *\n * A seventh chord states a seventh against its root and a dominant states the\n * tritone between its third and seventh; those intervals are the chord's\n * identity, not a clash to be prepared and resolved. A second between two chord\n * tones (a ninth voiced against the root, say) is a spacing problem rather than\n * a property of the harmony, so it stays reportable.\n */\nfunction isStructuralChordDissonance(a: number, b: number, chord: Chord): boolean {\n const rawClass = pitchClass(Math.abs(a - b));\n const aOffset = intervalAboveRoot(a, chord);\n const bOffset = intervalAboveRoot(b, chord);\n const rootSeventh =\n (aOffset === 0 && (bOffset === 10 || bOffset === 11)) ||\n (bOffset === 0 && (aOffset === 10 || aOffset === 11));\n if (rawClass !== 6 && !rootSeventh) {\n return false;\n }\n return isChordMember(a, chord) && isChordMember(b, chord);\n}\n\n/** Nearest chord tone at or below the candidate, within two semitones. */\nfunction stepResolution(pitch: number, chord: Chord): number | undefined {\n for (let delta = 1; delta <= 2; delta += 1) {\n if (isChordMember(pitch - delta, chord)) {\n return pitch - delta;\n }\n }\n return undefined;\n}\n\n/**\n * Evaluate the safety of placing a candidate pitch in a voice.\n *\n * Reasons are merged from harmonic context (chord tone / tension / avoid / scale)\n * and counterpoint (vertical dissonance, parallels, voice crossing). The `strict`\n * profile treats avoid notes and parallels as dissonant; `pop` treats them as\n * warnings. The final verdict is the worst severity among the reasons.\n *\n * @param q The candidate and its harmonic/voice-leading context.\n * @returns The verdict, reason bitmask, and optional resolution guidance.\n * @example\n * ```ts\n * import { evaluateSafety, makeChord, majorKey, NoteSafety } from '@libraz/libcantus';\n * const result = evaluateSafety({\n * profile: 'pop',\n * candidatePitch: 60, // C over a C major chord\n * chord: makeChord(0, 'maj'),\n * key: majorKey(0),\n * otherVoices: [],\n * strongBeat: true,\n * });\n * result.safety === NoteSafety.Safe; // true — C is a chord tone\n * ```\n * @category Arrangement & Analysis\n */\nexport function evaluateSafety(q: SafetyQuery, opts: EvaluateSafetyOptions = {}): SafetyResult {\n assertSafetyContext(q);\n assertFiniteNumber(q.candidatePitch, 'candidatePitch');\n return evaluateInternal(q, opts.suggestions ?? true);\n}\n\n/**\n * Options for {@link evaluateSafety}.\n *\n * @category Arrangement & Analysis\n */\nexport type EvaluateSafetyOptions = {\n /**\n * Whether to search for safe replacement pitches when the verdict is not\n * safe. Turning it off skips up to two dozen extra evaluations per rejected\n * pitch, which is worth doing when only the verdict is wanted.\n *\n * @defaultValue true\n */\n suggestions?: boolean;\n};\n\n/** Validate all numeric context fields once at a public safety entry point. */\nfunction assertSafetyContext(q: Omit<SafetyQuery, 'candidatePitch'>): void {\n if (q.prevPitch !== undefined) assertFiniteNumber(q.prevPitch, 'prevPitch');\n if (q.vocalLow !== undefined) assertFiniteNumber(q.vocalLow, 'vocalLow');\n if (q.vocalHigh !== undefined) assertFiniteNumber(q.vocalHigh, 'vocalHigh');\n if (q.vocalLow !== undefined && q.vocalHigh !== undefined && q.vocalLow > q.vocalHigh) {\n throw new InvalidInputError('vocalLow must not exceed vocalHigh');\n }\n assertGenerationBudget(q.otherVoices.length, 'other voices');\n for (let index = 0; index < q.otherVoices.length; index += 1) {\n const voice = q.otherVoices[index];\n if (!voice) continue;\n assertFiniteNumber(voice.pitch, `otherVoices[${index}].pitch`);\n if (voice.prevPitch !== undefined) {\n assertFiniteNumber(voice.prevPitch, `otherVoices[${index}].prevPitch`);\n }\n }\n}\n\n/**\n * Core safety evaluation.\n *\n * @param q The candidate and its harmonic/voice-leading context.\n * @param collectSuggestions Whether to search for safe alternatives on a\n * non-Safe verdict. Set false during that search itself to bound recursion.\n * @returns The verdict, reason bitmask, and optional guidance.\n */\nfunction evaluateInternal(q: SafetyQuery, collectSuggestions: boolean): SafetyResult {\n let reasons = 0;\n let level = NoteSafety.Safe;\n let resolveTo: number | undefined;\n const raise = (l: NoteSafety) => {\n if (l > level) {\n level = l;\n }\n };\n const avoidSeverity = q.profile === 'strict' ? NoteSafety.Dissonant : NoteSafety.Warning;\n const parallelSeverity = avoidSeverity;\n const pitch = q.candidatePitch;\n\n if (\n (q.vocalLow !== undefined && pitch < q.vocalLow) ||\n (q.vocalHigh !== undefined && pitch > q.vocalHigh)\n ) {\n reasons |= ReasonFlag.OutOfRange;\n raise(NoteSafety.Warning);\n }\n\n const inScale = isScaleTone(pitch, q.key);\n const chord = q.chord;\n\n // A chromatic avoid note remains chromatic. The avoid-note branch below\n // refines the resolution guidance, but must not erase the key relationship\n // or downgrade a non-scale pitch to a merely stylistic warning.\n if (!inScale && (chord === null || !isChordMember(pitch, chord))) {\n reasons |= ReasonFlag.NonScale;\n raise(NoteSafety.Dissonant);\n }\n\n if (chord && isChordMember(pitch, chord)) {\n reasons |= ReasonFlag.ChordTone;\n } else if (chord) {\n const ic = intervalAboveRoot(pitch, chord);\n const isTension = ic === 2 || ic === 5 || ic === 9;\n const avoid = (ic === 5 && avoidsFourth(chord)) || (ic === 11 && avoidsMajorSeventh(chord));\n if (avoid) {\n reasons |= ReasonFlag.AvoidNote;\n if (isTension) {\n reasons |= ReasonFlag.Tension;\n }\n reasons |= ReasonFlag.NeedsResolution;\n resolveTo = stepResolution(pitch, chord);\n raise(avoidSeverity);\n } else if (isTension && inScale) {\n reasons |= ReasonFlag.Tension;\n raise(NoteSafety.Warning);\n } else if (inScale) {\n reasons |= ReasonFlag.ScaleTone;\n raise(NoteSafety.Warning);\n }\n } else if (inScale) {\n reasons |= ReasonFlag.ScaleTone;\n raise(NoteSafety.Warning);\n }\n\n // Harmonic tritone against a chord tone. A chord tone is never flagged for a\n // tritone its own chord spells: the interval is the chord's identity, not a\n // clash with it. This covers dominant sevenths (third against seventh) and\n // equally the diminished, half-diminished, diminished-seventh and minor-sixth\n // families, where the tritone falls between two chord tones as well.\n if (chord && !isChordMember(pitch, chord)) {\n const ic = intervalAboveRoot(pitch, chord);\n const toneOffsets = chord.intervals.map((i) => ((i % 12) + 12) % 12);\n if (toneOffsets.some((t) => Math.abs(ic - t) === 6)) {\n reasons |= ReasonFlag.Tritone;\n }\n }\n\n if (q.prevPitch !== undefined) {\n // Quality flags reduce the melodic interval to its pitch-class interval so\n // compound intervals are treated uniformly with their simple form. The\n // forbidden-leap flag defers to the counterpoint rule, which preserves the\n // octave boundary (an octave leap is allowed, wider leaps are not).\n const pc = Math.abs(pitch - q.prevPitch) % 12;\n if (pc === 6) {\n reasons |= ReasonFlag.MelodicTritone;\n raise(q.profile === 'strict' ? NoteSafety.Dissonant : NoteSafety.Warning);\n }\n if (isForbiddenMelodicLeap(q.prevPitch, pitch)) {\n reasons |= ReasonFlag.LargeLeap;\n raise(parallelSeverity);\n }\n if (pc === 1) {\n reasons |= ReasonFlag.MinorSecond;\n }\n if (pc === 11) {\n reasons |= ReasonFlag.MajorSeventh;\n }\n }\n\n if (q.strongBeat) {\n const twoVoice = q.otherVoices.length === 1;\n const prev = q.prevPitch;\n for (const ov of q.otherVoices) {\n if (!createsVerticalDissonance(pitch, ov.pitch, twoVoice)) {\n continue;\n }\n if (chord && isStructuralChordDissonance(pitch, ov.pitch, chord)) {\n continue;\n }\n reasons |= ReasonFlag.VerticalDissonance;\n raise(NoteSafety.Dissonant);\n // A held pitch that was consonant on the previous step and is now\n // dissonant against the same voice is a prepared suspension.\n if (\n prev !== undefined &&\n pitch === prev &&\n ov.prevPitch !== undefined &&\n !createsVerticalDissonance(prev, ov.prevPitch, twoVoice)\n ) {\n reasons |= ReasonFlag.Suspension;\n }\n }\n }\n\n if (q.prevPitch !== undefined) {\n const prev = q.prevPitch;\n for (const ov of q.otherVoices) {\n if (ov.prevPitch === undefined) {\n continue;\n }\n // createsParallelPerfect covers every perfect class (unison/octave and\n // fifth), so no separate octave check is needed.\n if (createsParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {\n reasons |= ReasonFlag.ParallelPerfect;\n raise(parallelSeverity);\n }\n if (createsHiddenParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {\n reasons |= ReasonFlag.HiddenParallel;\n raise(parallelSeverity);\n }\n const crossedNow = pitch - ov.pitch;\n const crossedPrev = prev - ov.prevPitch;\n if (crossedNow !== 0 && crossedPrev !== 0 && crossedNow > 0 !== crossedPrev > 0) {\n reasons |= ReasonFlag.VoiceCrossing;\n raise(parallelSeverity);\n }\n }\n }\n\n // Keep reason production and verdict policy as one invariant even when a new\n // call path skips an earlier local `raise`: informational flags (ChordTone,\n // MinorSecond, MajorSeventh) may coexist with Safe; every policy flag above\n // contributes at least its table severity.\n raise(minimumSafetyForReasons(reasons, q.profile));\n\n const result: SafetyResult = { safety: level, reasons, rationale: describe(reasons, q) };\n if (resolveTo !== undefined) {\n result.resolveTo = resolveTo;\n }\n if (collectSuggestions && level !== NoteSafety.Safe) {\n const suggestions = findSafeNearby(q, pitch);\n if (suggestions.length > 0) {\n result.suggestions = suggestions;\n }\n }\n return result;\n}\n\n/**\n * Find safe alternative pitches near a rejected candidate.\n *\n * Searches outward from the candidate within `SUGGESTION_WINDOW` semitones,\n * probing the pitch below before the one above at each distance so the result\n * is deterministic and ordered by nearness.\n *\n * @param q The safety context of the rejected candidate.\n * @param candidate The candidate pitch that was not Safe.\n * @returns Up to `MAX_SUGGESTIONS` safe pitches, nearest first.\n */\nfunction findSafeNearby(q: SafetyQuery, candidate: number): number[] {\n const out: number[] = [];\n for (let d = 1; d <= SUGGESTION_WINDOW; d += 1) {\n for (const p of [candidate - d, candidate + d]) {\n if (p < 0 || p > 127) {\n continue;\n }\n if (evaluateInternal({ ...q, candidatePitch: p }, false).safety === NoteSafety.Safe) {\n out.push(p);\n if (out.length >= MAX_SUGGESTIONS) {\n return out;\n }\n }\n }\n }\n return out;\n}\n\n/**\n * Wording for every reason flag, most to least specific.\n *\n * The list is exhaustive by construction: {@link describe} picks from it by\n * severity, so a flag with no wording here would let a rejected pitch be\n * explained by a milder reason — the seventh of a dominant reading as a plain\n * \"Chord tone\" while its verdict says dissonant.\n */\nconst REASON_TEXT: ReadonlyArray<readonly [ReasonFlag, string]> = [\n [ReasonFlag.OutOfRange, 'Outside the target vocal range'],\n [\n ReasonFlag.Suspension,\n 'Prepared suspension — a held consonance now dissonant, awaiting resolution',\n ],\n [ReasonFlag.VerticalDissonance, 'Dissonant against a sounding voice on a strong beat'],\n [ReasonFlag.Tritone, 'Tritone against the sounding harmony'],\n [ReasonFlag.MelodicTritone, 'Melodic tritone leap'],\n [ReasonFlag.AvoidNote, 'Avoid note — clashes with a chord tone a semitone away'],\n [ReasonFlag.NonScale, 'Outside the key'],\n [ReasonFlag.ParallelPerfect, 'Parallel perfect interval with another voice'],\n [ReasonFlag.HiddenParallel, 'Hidden perfect interval by similar motion'],\n [ReasonFlag.VoiceCrossing, 'Crosses another voice'],\n [ReasonFlag.LargeLeap, 'Leap wider than the profile allows'],\n [ReasonFlag.NeedsResolution, 'Dissonance awaiting a stepwise resolution'],\n [ReasonFlag.Tension, 'Chord tension'],\n [ReasonFlag.ScaleTone, 'Scale tone, not in the chord'],\n [ReasonFlag.MinorSecond, 'Melodic semitone'],\n [ReasonFlag.MajorSeventh, 'Melodic major seventh'],\n [ReasonFlag.ChordTone, 'Chord tone'],\n];\n\n/** The severity a single flag carries under a profile; Safe when purely informational. */\nfunction severityOfReason(flag: ReasonFlag, profile: SafetyProfile): NoteSafety {\n const entry = REASON_SEVERITY.find(([candidate]) => candidate === flag);\n return entry === undefined ? NoteSafety.Safe : entry[profile === 'strict' ? 2 : 1];\n}\n\n/**\n * Build a short human explanation from the reason flags.\n *\n * The flag driving the verdict wins: the one whose severity under this profile\n * is highest, ties going to the more specific wording. Explaining a rejected\n * pitch by an informational flag it also carries would tell the caller nothing\n * about what to fix.\n */\nfunction describe(reasons: number, q: SafetyQuery): string {\n let best: string | undefined;\n let bestSeverity = -1;\n for (const [flag, text] of REASON_TEXT) {\n if (!(reasons & flag)) {\n continue;\n }\n const severity = severityOfReason(flag, q.profile);\n if (severity > bestSeverity) {\n bestSeverity = severity;\n best = text;\n }\n }\n if (best !== undefined) {\n return best;\n }\n return q.chord ? 'Placeable' : 'No chord context';\n}\n\n/**\n * Enumerate placeable pitches in a range, chord tones first, descending.\n *\n * @param q The safety context minus the candidate pitch.\n * @param pitchLow Lowest MIDI pitch to consider (inclusive).\n * @param pitchHigh Highest MIDI pitch to consider (inclusive).\n * @returns Placeable pitches (non-dissonant), chord tones before others, each group descending.\n * @throws If either bound is non-finite/non-integral, reversed, or exceeds the generation budget.\n * @example\n * ```ts\n * import { enumerateSafePitches, makeChord, majorKey } from '@libraz/libcantus';\n * const pitches = enumerateSafePitches(\n * { profile: 'pop', chord: makeChord(0, 'maj'), key: majorKey(0), otherVoices: [], strongBeat: true },\n * 60,\n * 72,\n * );\n * pitches; // placeable pitches in [60, 72], chord tones first, each group descending\n * ```\n * @category Arrangement & Analysis\n */\nexport function enumerateSafePitches(\n q: Omit<SafetyQuery, 'candidatePitch'>,\n pitchLow: number,\n pitchHigh: number,\n): number[] {\n assertSafetyContext(q);\n const chordTones: number[] = [];\n const others: number[] = [];\n assertInteger(pitchLow, 'pitchLow');\n assertInteger(pitchHigh, 'pitchHigh');\n if (pitchLow > pitchHigh) {\n throw new InvalidInputError(\n `pitchLow must not exceed pitchHigh; received ${pitchLow} > ${pitchHigh}`,\n );\n }\n assertGenerationBudget(pitchHigh - pitchLow + 1, 'safe pitch candidates');\n const low = Math.max(pitchLow, q.vocalLow ?? pitchLow);\n const high = Math.min(pitchHigh, q.vocalHigh ?? pitchHigh);\n for (let pitch = high; pitch >= low; pitch -= 1) {\n const result = evaluateInternal({ ...q, candidatePitch: pitch }, false);\n if (result.safety === NoteSafety.Dissonant) {\n continue;\n }\n if (result.reasons & ReasonFlag.ChordTone) {\n chordTones.push(pitch);\n } else {\n others.push(pitch);\n }\n }\n return [...chordTones, ...others];\n}\n"]}