@libraz/libcantus 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +27 -65
  2. package/README_ja.md +28 -66
  3. package/dist/analyze/index.cjs +11 -7
  4. package/dist/analyze/index.cjs.map +1 -1
  5. package/dist/analyze/index.d.cts +79 -144
  6. package/dist/analyze/index.d.ts +79 -144
  7. package/dist/analyze/index.js +20 -16
  8. package/dist/chunk-35NBV2FT.cjs +145 -0
  9. package/dist/chunk-35NBV2FT.cjs.map +1 -0
  10. package/dist/{chunk-UADYAYAT.cjs → chunk-3R3QVKY5.cjs} +1353 -580
  11. package/dist/chunk-3R3QVKY5.cjs.map +1 -0
  12. package/dist/{chunk-WWURE4XT.cjs → chunk-76ZBTRG2.cjs} +624 -729
  13. package/dist/chunk-76ZBTRG2.cjs.map +1 -0
  14. package/dist/{chunk-FVSI36UV.js → chunk-7KG3RTYS.js} +197 -110
  15. package/dist/chunk-7KG3RTYS.js.map +1 -0
  16. package/dist/{chunk-VTI6FOLB.js → chunk-AGOVW7U6.js} +709 -133
  17. package/dist/chunk-AGOVW7U6.js.map +1 -0
  18. package/dist/{chunk-E3F4AHAH.cjs → chunk-CGBADIO5.cjs} +221 -134
  19. package/dist/chunk-CGBADIO5.cjs.map +1 -0
  20. package/dist/{chunk-6XH6QZYQ.cjs → chunk-DSMJRXCZ.cjs} +1455 -849
  21. package/dist/chunk-DSMJRXCZ.cjs.map +1 -0
  22. package/dist/{chunk-PKFNUHDY.js → chunk-FBH75DAY.js} +1218 -612
  23. package/dist/chunk-FBH75DAY.js.map +1 -0
  24. package/dist/chunk-H2KJHBW4.js +3329 -0
  25. package/dist/chunk-H2KJHBW4.js.map +1 -0
  26. package/dist/chunk-HPVSH7SF.cjs +2294 -0
  27. package/dist/chunk-HPVSH7SF.cjs.map +1 -0
  28. package/dist/{chunk-MXO72PLY.cjs → chunk-HWHGOVO6.cjs} +21 -19
  29. package/dist/chunk-HWHGOVO6.cjs.map +1 -0
  30. package/dist/chunk-HYJJ2V2I.js +2294 -0
  31. package/dist/chunk-HYJJ2V2I.js.map +1 -0
  32. package/dist/{chunk-AFM2ULJH.cjs → chunk-J7FHVZII.cjs} +1567 -1067
  33. package/dist/chunk-J7FHVZII.cjs.map +1 -0
  34. package/dist/chunk-K6OZXIR6.js +1 -0
  35. package/dist/{chunk-H3VORETI.js → chunk-M5RYMAFI.js} +487 -592
  36. package/dist/chunk-M5RYMAFI.js.map +1 -0
  37. package/dist/chunk-NB25P3EH.js +363 -0
  38. package/dist/chunk-NB25P3EH.js.map +1 -0
  39. package/dist/{chunk-TTCLJX6D.js → chunk-NFJ4P6ZZ.js} +10 -8
  40. package/dist/chunk-NFJ4P6ZZ.js.map +1 -0
  41. package/dist/chunk-NM7UAMC3.cjs +457 -0
  42. package/dist/chunk-NM7UAMC3.cjs.map +1 -0
  43. package/dist/chunk-PM4XJE2Y.cjs +363 -0
  44. package/dist/chunk-PM4XJE2Y.cjs.map +1 -0
  45. package/dist/chunk-PUKJP2Z5.js +145 -0
  46. package/dist/chunk-PUKJP2Z5.js.map +1 -0
  47. package/dist/{chunk-VOHZB45S.js → chunk-Q6LFSAKV.js} +1321 -821
  48. package/dist/chunk-Q6LFSAKV.js.map +1 -0
  49. package/dist/{chunk-INHVXP6Z.js → chunk-RFIBF2RR.js} +1203 -430
  50. package/dist/chunk-RFIBF2RR.js.map +1 -0
  51. package/dist/{chunk-Q2WGKJNN.cjs → chunk-S5BKZZGM.cjs} +699 -123
  52. package/dist/chunk-S5BKZZGM.cjs.map +1 -0
  53. package/dist/chunk-SGLEQVJH.cjs +3329 -0
  54. package/dist/chunk-SGLEQVJH.cjs.map +1 -0
  55. package/dist/chunk-UGLFDG7L.cjs +1 -0
  56. package/dist/chunk-UGLFDG7L.cjs.map +1 -0
  57. package/dist/{chunk-YH3BIKHH.js → chunk-UMWZ4ZET.js} +146 -49
  58. package/dist/chunk-UMWZ4ZET.js.map +1 -0
  59. package/dist/{index-szBv3496.d.ts → coerce-DP0vYu-x.d.ts} +286 -199
  60. package/dist/{index-C_8o--VE.d.cts → coerce-DRERzdve.d.cts} +286 -199
  61. package/dist/core/index.cjs +9 -5
  62. package/dist/core/index.cjs.map +1 -1
  63. package/dist/core/index.d.cts +122 -108
  64. package/dist/core/index.d.ts +122 -108
  65. package/dist/core/index.js +32 -28
  66. package/dist/generate/index.cjs +8 -8
  67. package/dist/generate/index.d.cts +168 -84
  68. package/dist/generate/index.d.ts +168 -84
  69. package/dist/generate/index.js +7 -7
  70. package/dist/{index-CGpRv5sl.d.cts → index-9eoQs2C1.d.ts} +34 -19
  71. package/dist/{index-CCNJRG-P.d.cts → index-B9QLziDe.d.cts} +293 -56
  72. package/dist/{index-Dzr13q3d.d.cts → index-BBWNZS0f.d.cts} +135 -58
  73. package/dist/{index-9wcMhG73.d.ts → index-BHIUyjhZ.d.ts} +293 -56
  74. package/dist/{index-BGFW-8P2.d.cts → index-BJgP97N1.d.ts} +196 -114
  75. package/dist/{index-CunYKLiu.d.ts → index-BL5N2Oyz.d.cts} +1 -1
  76. package/dist/{index-Cj49F892.d.ts → index-BMweUlm5.d.cts} +63 -23
  77. package/dist/{index-CgHqxdps.d.ts → index-BNSM-DSk.d.cts} +196 -114
  78. package/dist/{index-D57QRbVn.d.ts → index-BRMbPkHE.d.ts} +480 -254
  79. package/dist/{index-Chbst3Jx.d.ts → index-CJBBiAmU.d.ts} +135 -58
  80. package/dist/{index-BkGmlyI3.d.cts → index-CL4b4gUQ.d.ts} +63 -23
  81. package/dist/{index-B9MNZboQ.d.ts → index-Cy15oTvh.d.cts} +52 -22
  82. package/dist/{index-B9MNZboQ.d.cts → index-Cy15oTvh.d.ts} +52 -22
  83. package/dist/{index-BO6xE9YU.d.cts → index-DGCNKAUr.d.cts} +480 -254
  84. package/dist/{index-B4o55NDr.d.cts → index-Dw2DP7vm.d.ts} +1 -1
  85. package/dist/{index-G1uwGGWQ.d.ts → index-cHvbYgJI.d.cts} +34 -19
  86. package/dist/index.cjs +42 -14
  87. package/dist/index.cjs.map +1 -1
  88. package/dist/index.d.cts +14 -14
  89. package/dist/index.d.ts +14 -14
  90. package/dist/index.js +82 -54
  91. package/dist/{masks-DYbE485J.d.cts → masks-Cc7-MzzY.d.ts} +49 -2
  92. package/dist/{masks-DYbE485J.d.ts → masks-pADfTOiD.d.cts} +49 -2
  93. package/dist/model/index.cjs +19 -12
  94. package/dist/model/index.cjs.map +1 -1
  95. package/dist/model/index.d.cts +504 -182
  96. package/dist/model/index.d.ts +504 -182
  97. package/dist/model/index.js +18 -11
  98. package/dist/{positional-Cptlj-xs.d.ts → positional-DJgs6ltD.d.ts} +85 -8
  99. package/dist/{positional-DvH_sd8u.d.cts → positional-DMZer-FM.d.cts} +85 -8
  100. package/dist/theory/index.cjs +35 -6
  101. package/dist/theory/index.cjs.map +1 -1
  102. package/dist/theory/index.d.cts +257 -62
  103. package/dist/theory/index.d.ts +257 -62
  104. package/dist/theory/index.js +41 -12
  105. package/package.json +7 -5
  106. package/dist/chunk-2LCPM3EG.js +0 -3224
  107. package/dist/chunk-2LCPM3EG.js.map +0 -1
  108. package/dist/chunk-6XH6QZYQ.cjs.map +0 -1
  109. package/dist/chunk-6XPTBE7Y.cjs +0 -49
  110. package/dist/chunk-6XPTBE7Y.cjs.map +0 -1
  111. package/dist/chunk-AFM2ULJH.cjs.map +0 -1
  112. package/dist/chunk-B6XNKWZV.cjs +0 -3224
  113. package/dist/chunk-B6XNKWZV.cjs.map +0 -1
  114. package/dist/chunk-E3F4AHAH.cjs.map +0 -1
  115. package/dist/chunk-ELCTZUJJ.js +0 -379
  116. package/dist/chunk-ELCTZUJJ.js.map +0 -1
  117. package/dist/chunk-FVSI36UV.js.map +0 -1
  118. package/dist/chunk-H3VORETI.js.map +0 -1
  119. package/dist/chunk-H42UHPYW.js +0 -1577
  120. package/dist/chunk-H42UHPYW.js.map +0 -1
  121. package/dist/chunk-HFPS7JFD.js +0 -1
  122. package/dist/chunk-INHVXP6Z.js.map +0 -1
  123. package/dist/chunk-K4ZJUGF4.cjs +0 -379
  124. package/dist/chunk-K4ZJUGF4.cjs.map +0 -1
  125. package/dist/chunk-KNJEKLKV.js +0 -49
  126. package/dist/chunk-KNJEKLKV.js.map +0 -1
  127. package/dist/chunk-MXO72PLY.cjs.map +0 -1
  128. package/dist/chunk-NLEU4QSE.cjs +0 -360
  129. package/dist/chunk-NLEU4QSE.cjs.map +0 -1
  130. package/dist/chunk-OE6WK2Y2.cjs +0 -1577
  131. package/dist/chunk-OE6WK2Y2.cjs.map +0 -1
  132. package/dist/chunk-PKFNUHDY.js.map +0 -1
  133. package/dist/chunk-Q2WGKJNN.cjs.map +0 -1
  134. package/dist/chunk-RHIJ3S4I.cjs +0 -1
  135. package/dist/chunk-RHIJ3S4I.cjs.map +0 -1
  136. package/dist/chunk-TTCLJX6D.js.map +0 -1
  137. package/dist/chunk-UADYAYAT.cjs.map +0 -1
  138. package/dist/chunk-VOHZB45S.js.map +0 -1
  139. package/dist/chunk-VTI6FOLB.js.map +0 -1
  140. package/dist/chunk-WWURE4XT.cjs.map +0 -1
  141. package/dist/chunk-YH3BIKHH.js.map +0 -1
  142. /package/dist/{chunk-HFPS7JFD.js.map → chunk-K6OZXIR6.js.map} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"sources":["/home/runner/work/libcantus/libcantus/dist/chunk-Q2WGKJNN.cjs","../src/core/errors/index.ts","../src/core/meter/internal.ts","../src/core/validation/index.ts","../src/core/pitch/naming.ts","../src/core/pitch/index.ts","../src/core/interval/index.ts"],"names":["ConsonanceClass"],"mappings":"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2C;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;AACgC,EAAA;AAChC,IAAA;AACyB,IAAA;AACzB,IAAA;AACF,EAAA;AAIwB,EAAA;AACA,IAAA;AACxB,EAAA;AACO,EAAA;AACT;ALrBiC;AACA;AMx7B1B;AACLA,EAAAA;AACAA,EAAAA;AACAA,EAAAA;AAHUA,EAAAA;AAAA;AAYY;AACO,EAAA;AAC/B;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;ANo4BiC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/libcantus/libcantus/dist/chunk-Q2WGKJNN.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 * Any error this library raises, as a union of its three classes.\n *\n * The type a non-throwing entry point reports its failure as: a caller that\n * holds one of these already knows it carries a {@link LibcantusErrorCode} and\n * a message worth showing.\n *\n * @category Core\n */\nexport type LibcantusError = InvalidInputError | NoSolutionError | BudgetExceededError;\n\n/**\n * The outcome of a parse that reports failure instead of throwing it.\n *\n * A text field is parsed on every keystroke, and most of those keystrokes are\n * halfway through a valid symbol, so failure is the normal case rather than an\n * exceptional one. The error travels in the result — not as `null` — because an\n * input field has to say what is wrong with what was typed.\n *\n * @typeParam T The value a successful parse produces.\n * @category Core\n * @example\n * ```ts\n * import { tryParseChordSymbol } from '@libraz/libcantus';\n * const result = tryParseChordSymbol('Cmaj');\n * const label = result.ok ? result.value.quality : result.error.message;\n * ```\n */\nexport type ParseResult<T> = { ok: true; value: T } | { ok: false; error: LibcantusError };\n\n/**\n * Report a caught value as a failed parse, or re-throw what is not ours.\n *\n * Not part of the package's public surface: the non-throwing parsers share it\n * so that a bug inside one of them surfaces as the crash it is instead of being\n * reported as invalid input.\n *\n * @param error The caught value.\n * @returns The failed parse result.\n */\nexport function parseFailure(error: unknown): { ok: false; error: LibcantusError } {\n if (isLibcantusError(error)) {\n return { ok: false, error };\n }\n throw error;\n}\n\n/**\n * Read a parse result, throwing its error when it failed.\n *\n * Not part of the package's public surface: it is how each throwing parser is\n * written on top of its non-throwing sibling, so the two cannot disagree about\n * what is valid.\n *\n * @param result The result to read.\n * @returns The parsed value.\n * @throws The result's own error when the parse failed.\n */\nexport function unwrapParse<T>(result: ParseResult<T>): T {\n if (result.ok) {\n return result.value;\n }\n throw result.error;\n}\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 { MeterMap, TimeSignature } from './index.js';\n\n/**\n * A meter in the plain data the library reads it as: one signature, or the map\n * of a piece that changes meter.\n *\n * What {@link MeterLike} resolves to, and the type every function below the\n * entry points works in — the text form is read once, at the boundary.\n *\n * @category Rhythm & Meter\n */\nexport type MeterData = TimeSignature | MeterMap;\n\n/**\n * Tolerance for the divisions below.\n *\n * A bar boundary reached by accumulating tuplet durations lands an epsilon\n * short of it, and a beat an epsilon short of a bar line belongs to the bar it\n * is arriving at, not to the one it is leaving.\n */\nconst EPS = 1e-9;\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 is written in units and selects that additive reading, with one\n * exception: groups of nothing but threes spell the compound division itself\n * (9/8 as [3, 3, 3], 6/8 as [3, 3]), so they keep the compound reading rather\n * than flattening the bar into equal quaver pulses.\n */\nexport function isAdditiveReading(ts: TimeSignature): boolean {\n const grouping = ts.grouping;\n const sum = groupingSumOf(ts);\n if (grouping === undefined || sum === undefined || sum === 0) {\n return false;\n }\n if (!isCompoundNumerator(ts.numerator) || sum !== ts.numerator) {\n return false;\n }\n return !grouping.every((entry) => entry === 3);\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/**\n * The signature's grouping measured in main pulses, or undefined when it has\n * none.\n *\n * A grouping is written either in main pulses (9/8 as [1, 1, 1], 7/8 as\n * [2, 2, 3]) or in denominator units (9/8 as [3, 3, 3] or [2, 2, 2, 3]), and\n * the two coincide wherever a pulse is one unit. Group heads are found by\n * counting pulses, so the unit spelling of a compound bar is divided down to\n * the pulses it describes.\n */\nexport function pulseGroupingOf(ts: TimeSignature): number[] | undefined {\n const grouping = ts.grouping;\n if (grouping === undefined) {\n return undefined;\n }\n const pulses = pulseCountOf(ts);\n if (groupingSumOf(ts) === pulses) {\n return grouping;\n }\n // The only accepted grouping that does not count pulses is the all-threes\n // spelling of a compound bar, whose groups are three units to the pulse.\n return grouping.map((entry) => entry / 3);\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\n/** Whether a meter argument is a map of changes rather than one signature. */\nexport function isMeterMap(meter: MeterData): meter is MeterMap {\n return Array.isArray(meter);\n}\n\n/**\n * Index of the map entry in force at a beat.\n *\n * The first entry also covers everything before it, so a pickup written at\n * negative beats is read in the signature the piece opens in.\n */\nexport function entryIndexOf(map: MeterMap, beat: number): number {\n let index = 0;\n for (let i = 1; i < map.length; i += 1) {\n if ((map[i]?.startBeat ?? Number.POSITIVE_INFINITY) > beat + EPS) {\n break;\n }\n index = i;\n }\n return index;\n}\n\n/**\n * Bars completed before the entry at `index`.\n *\n * A meter change starts a new bar, so a span that does not divide evenly into\n * its own bars still contributes a whole final bar — an incomplete bar before a\n * change is a bar all the same.\n */\nfunction barsBeforeEntry(map: MeterMap, index: number): number {\n let bars = 0;\n for (let i = 0; i < index; i += 1) {\n const entry = map[i];\n const next = map[i + 1];\n if (entry === undefined || next === undefined) {\n break;\n }\n bars += Math.max(1, Math.ceil((next.startBeat - entry.startBeat) / barBeatsOf(entry.ts) - EPS));\n }\n return bars;\n}\n\n/** Absolute beat at which the bar containing `beat` begins. */\nexport function barStartOf(map: MeterMap, beat: number): number {\n const entry = map[entryIndexOf(map, beat)];\n if (entry === undefined) {\n return 0;\n }\n const barLen = barBeatsOf(entry.ts);\n return entry.startBeat + Math.floor((beat - entry.startBeat) / barLen + EPS) * barLen;\n}\n\n/**\n * Length in quarter-note beats of the bar containing `beat`.\n *\n * A meter change starts a new bar, so the bar it interrupts is shorter than its\n * own signature says: this is that bar's real length, which is what a position\n * display has to count felt beats against.\n */\nexport function barLengthOf(map: MeterMap, beat: number): number {\n const index = entryIndexOf(map, beat);\n const entry = map[index];\n if (entry === undefined) {\n return 0;\n }\n const full = barBeatsOf(entry.ts);\n const next = map[index + 1]?.startBeat;\n return next === undefined ? full : Math.min(full, next - barStartOf(map, beat));\n}\n\n/**\n * Bar index of a beat, counting the map's first bar as 0.\n *\n * Beats before that bar count backwards, which is what numbers a pickup bar as\n * bar -1 rather than folding it into the first full bar.\n */\nexport function barIndexOf(map: MeterMap, beat: number): number {\n const index = entryIndexOf(map, beat);\n const entry = map[index];\n if (entry === undefined) {\n return 0;\n }\n const barLen = barBeatsOf(entry.ts);\n return barsBeforeEntry(map, index) + Math.floor((beat - entry.startBeat) / barLen + EPS);\n}\n\n/** Absolute beat at which bar `barIndex` begins. */\nexport function beatOfBarIndex(map: MeterMap, barIndex: number): number {\n let base = 0;\n for (let i = 0; i < map.length; i += 1) {\n const entry = map[i];\n if (entry === undefined) {\n continue;\n }\n const barLen = barBeatsOf(entry.ts);\n const next = map[i + 1];\n if (next === undefined) {\n return entry.startBeat + (barIndex - base) * barLen;\n }\n const bars = Math.max(1, Math.ceil((next.startBeat - entry.startBeat) / barLen - EPS));\n if (barIndex < base + bars) {\n return entry.startBeat + (barIndex - base) * barLen;\n }\n base += bars;\n }\n return 0;\n}\n","import { BudgetExceededError, InvalidInputError } from '../errors/index.js';\nimport type { MeterMap, 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/**\n * Require a bounded scale/chord degree, counted from 1 the way musicians count\n * degrees: 1 is the tonic, 5 the dominant. Zero and negative degrees name no\n * degree at all and are rejected.\n */\nexport function assertDegree(value: number, name = 'degree'): number {\n return assertInteger(value, name, 1, 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 * Validate a meter map: a non-empty run of time signatures, each with the beat\n * it takes effect at, in strictly increasing beat order.\n *\n * The first entry also governs everything before it, so a map need not start at\n * beat 0 — that is what lets a pickup at a negative beat be read in the\n * signature the piece opens in.\n *\n * @category Core\n */\nexport function assertMeterMap(meters: MeterMap, name = 'meters'): MeterMap {\n if (!Array.isArray(meters)) {\n throw new InvalidInputError(`${name} must be an array; received ${typeof meters}`);\n }\n if (meters.length === 0) {\n throw new InvalidInputError(`${name} must not be empty`);\n }\n let previous = Number.NEGATIVE_INFINITY;\n for (let index = 0; index < meters.length; index += 1) {\n const entry = meters[index];\n if (entry === undefined) {\n throw new InvalidInputError(`${name}[${index}] must be a meter change; received undefined`);\n }\n assertFiniteNumber(entry.startBeat, `${name}[${index}].startBeat`);\n if (index > 0 && entry.startBeat <= previous) {\n throw new InvalidInputError(\n `${name}[${index}].startBeat must be greater than ${previous}; received ${entry.startBeat}`,\n );\n }\n previous = entry.startBeat;\n assertTimeSignature(entry.ts, `${name}[${index}].ts`);\n }\n return meters;\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 * Earliest accepted onset. Onsets are unbounded below by default, because a\n * pickup sounds before the downbeat and the downbeat is beat 0; pass\n * `-pickupBeats` to reject a note starting earlier than the pickup a caller\n * has declared.\n *\n * @defaultValue `-Number.MAX_SAFE_INTEGER`\n */\n minStartBeat?: 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 // An onset is not bounded below: the downbeat is beat 0, so a pickup sounds\n // at a negative beat. A nonsensical onset is still rejected — the bound is\n // finite, and a caller that knows its pickup narrows it further.\n const minStartBeat = options.minStartBeat ?? -Number.MAX_SAFE_INTEGER;\n assertFiniteNumber(minStartBeat, 'minStartBeat');\n assertRange(event.startBeat, minStartBeat, 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 * Note-name notation systems.\n *\n * A pitch has one identity but many written names: the note an English score\n * calls `G#` is `gis` in German, `嬰ト` in Japanese and `sol diesis` in Italian.\n * This module holds each system's table — letters, accidental affixes and mode\n * words — so a name can be read and written in the system a source actually\n * uses, and so a name of unknown origin can be attributed to one.\n *\n * The tables live together because attribution needs them together: `B` is a B\n * natural in English but a B flat in German, and only a table that knows both\n * can say so.\n */\n\nimport { InvalidInputError } from '../errors/index.js';\nimport { assertOneOf } from '../validation/index.js';\nimport type { Note } from './index.js';\n\n/**\n * A system of written note names.\n *\n * - `english`: C D E F G A B, `#` sharp and `b` flat (`C#`, `Bb`, `F##`).\n * - `german`: C D E F G A H, `-is` sharp and `-es` flat (`cis`, `des`), with B\n * for B flat and H for B natural.\n * - `japanese`: the katakana 音名 ハ ニ ホ ヘ ト イ ロ, prefixed 嬰 sharp and\n * 変 flat.\n * - `italian`: the solfège syllables do re mi fa sol la si, followed by\n * `diesis` or `bemolle`.\n * - `fixedDo`: the same syllables read as fixed-do note names; identical to\n * `italian`, and named separately because callers ask for it by that name.\n *\n * @category Pitch & Intervals\n */\nexport type NoteNameSystem = 'english' | 'german' | 'japanese' | 'italian' | 'fixedDo';\n\n/** Every system name, for validating a caller-supplied one. */\nconst NOTE_NAME_SYSTEMS = [\n 'english',\n 'german',\n 'japanese',\n 'italian',\n 'fixedDo',\n] as const satisfies readonly NoteNameSystem[];\n\n/**\n * Which notation system to read or write a name in.\n *\n * @category Pitch & Intervals\n */\nexport type NoteNameOptions = {\n /**\n * The system. Omitted on parsing, the system is detected from the name\n * itself; omitted on formatting, the name is written in English.\n */\n system?: NoteNameSystem;\n};\n\n/**\n * A key named the way a score names it: a spelled tonic and a mode.\n *\n * This is what a key *name* carries. It is deliberately not a key/scale: a name\n * fixes the tonic's spelling and says major or minor, and nothing more.\n *\n * @category Scales\n */\nexport type KeyName = {\n /** The spelled tonic, without an octave. */\n tonic: Note;\n /** Which of the two modes the name gives. */\n mode: 'major' | 'minor';\n};\n\n/**\n * The systems auto-detection chooses between, in precedence order.\n *\n * English comes first because it is the only pair with a genuine clash: `B` is\n * a B natural in English and a B flat in German, so a name that either system\n * could read is read as English unless it carries a German-only cue.\n */\nconst DETECTION_ORDER = ['english', 'german', 'japanese', 'italian'] as const;\n\n/** A system with a table of its own; `fixedDo` shares the Italian one. */\ntype NamingTable = (typeof DETECTION_ORDER)[number];\n\n/** The table a system reads from: fixed-do names are the Italian syllables. */\nfunction tableOf(system: NoteNameSystem): NamingTable {\n return system === 'fixedDo' ? 'italian' : system;\n}\n\n/** English letter glyphs, indexed by letter number (0 = C .. 6 = B). */\nconst ENGLISH_LETTERS = ['C', 'D', 'E', 'F', 'G', 'A', 'B'] as const;\n\n/** German letter glyphs; the seventh letter is H, since B is the B flat. */\nconst GERMAN_LETTERS = ['c', 'd', 'e', 'f', 'g', 'a', 'h'] as const;\n\n/** Japanese 音名, written in katakana in the イロハ order. */\nconst JAPANESE_LETTERS = ['ハ', 'ニ', 'ホ', 'ヘ', 'ト', 'イ', 'ロ'] as const;\n\n/** Italian and fixed-do solfège syllables. */\nconst ITALIAN_LETTERS = ['do', 're', 'mi', 'fa', 'sol', 'la', 'si'] as const;\n\n/** Letter numbers whose German flat elides to a bare `s`: E -> es, A -> as. */\nconst GERMAN_ELIDING_LETTERS = [2, 5];\n\n/** Letter number of A, the letter that also writes its double flat as `asas`. */\nconst A_LETTER = 5;\n\n/** Letter number of B, the one letter German and English disagree about. */\nconst B_LETTER = 6;\n\n/** The mode words each system writes, keyed by mode. */\nconst MODE_WORDS: Record<NamingTable, Record<'major' | 'minor', string>> = {\n english: { major: 'major', minor: 'minor' },\n german: { major: 'dur', minor: 'moll' },\n japanese: { major: '長調', minor: '短調' },\n italian: { major: 'maggiore', minor: 'minore' },\n};\n\n/** Japanese accidental marks: 嬰 raises, 変 lowers, 重 doubles the mark it precedes. */\nconst JAPANESE_SHARP = '嬰';\nconst JAPANESE_FLAT = '変';\nconst JAPANESE_DOUBLE = '重';\n\n/** Italian accidental words, and the word that doubles the one after it. */\nconst ITALIAN_SHARP = 'diesis';\nconst ITALIAN_FLAT = 'bemolle';\nconst ITALIAN_DOUBLE = 'doppio';\n\n/** Trim and collapse internal whitespace, so `'do diesis'` reads as one name. */\nfunction normalize(text: string): string {\n return text.trim().replace(/\\s+/g, ' ');\n}\n\n/** Reject a system this module has no table for. */\nfunction assertSystem(system: NoteNameSystem): NoteNameSystem {\n return assertOneOf(system, NOTE_NAME_SYSTEMS, 'system');\n}\n\n/**\n * Scientific pitch notation: a letter, any number of same-direction\n * accidentals, an optional octave. `x` is a double sharp; mixing `#` and `b` is\n * refused by the alternation rather than silently cancelling out.\n */\nconst ENGLISH_PATTERN = /^([A-Ga-g])([#x]*|b*)(-?\\d+)?$/;\n\n/** Read an English name, or null when the text is not one. */\nfunction parseEnglish(text: string): Note | null {\n const match = text.match(ENGLISH_PATTERN);\n if (match === null) {\n return null;\n }\n const glyph = (match[1] ?? '').toUpperCase();\n const letter = ENGLISH_LETTERS.indexOf(glyph as (typeof ENGLISH_LETTERS)[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 note;\n}\n\n/** Write an English name, without its octave. */\nfunction formatEnglish(note: Note): string {\n const glyph = ENGLISH_LETTERS[note.letter] ?? 'C';\n if (note.alter > 0) {\n return `${glyph}${'#'.repeat(note.alter)}`;\n }\n return note.alter < 0 ? `${glyph}${'b'.repeat(-note.alter)}` : glyph;\n}\n\n/**\n * How many flats a German flat suffix carries, or null when it is not one.\n *\n * The first mark elides to a bare `s` after E and A (`es`, `as`) and is a full\n * `es` elsewhere (`ces`, `ges`); every further `es` adds another flat, so\n * `eses` is a double flat. After A the doubled form `asas` is read too, since\n * both it and `ases` are written for the A double flat.\n */\nfunction germanFlats(letter: number, suffix: string): number | null {\n let rest = suffix;\n if (GERMAN_ELIDING_LETTERS.includes(letter) && rest.startsWith('s')) {\n rest = rest.slice(1);\n } else if (rest.startsWith('es')) {\n rest = rest.slice(2);\n } else {\n return null;\n }\n let flats = 1;\n while (rest.length > 0) {\n if (rest.startsWith('es') || (letter === A_LETTER && rest.startsWith('as'))) {\n rest = rest.slice(2);\n flats += 1;\n } else {\n return null;\n }\n }\n return flats;\n}\n\n/** Read a German name, or null when the text is not one. */\nfunction parseGerman(text: string): Note | null {\n const lower = text.toLowerCase();\n // B alone is the B flat; every other name on that letter is written on H, so\n // `bis` and `bes` are not German names at all.\n if (lower === 'b') {\n return { letter: B_LETTER, alter: -1 };\n }\n const letter = GERMAN_LETTERS.indexOf(lower.slice(0, 1) as (typeof GERMAN_LETTERS)[number]);\n if (letter < 0) {\n return null;\n }\n const suffix = lower.slice(1);\n if (suffix === '') {\n return { letter, alter: 0 };\n }\n if (/^(?:is)+$/.test(suffix)) {\n return { letter, alter: suffix.length / 2 };\n }\n const flats = germanFlats(letter, suffix);\n return flats === null ? null : { letter, alter: -flats };\n}\n\n/** Write a German name, without its octave. */\nfunction formatGerman(note: Note): string {\n const glyph = GERMAN_LETTERS[note.letter] ?? 'c';\n if (note.alter >= 0) {\n return `${glyph}${'is'.repeat(note.alter)}`;\n }\n const flats = -note.alter;\n if (note.letter === B_LETTER) {\n // The B flat is B; anything flatter is written on H, as heses.\n return flats === 1 ? 'b' : `h${'es'.repeat(flats)}`;\n }\n const first = GERMAN_ELIDING_LETTERS.includes(note.letter) ? `${glyph}s` : `${glyph}es`;\n return `${first}${'es'.repeat(flats - 1)}`;\n}\n\n/** Read a Japanese name, or null when the text is not one. */\nfunction parseJapanese(text: string): Note | null {\n let rest = text;\n let alter = 0;\n let direction = 0;\n while (rest.length > 0) {\n const doubled = rest.startsWith(JAPANESE_DOUBLE);\n const mark = doubled ? rest.slice(1, 2) : rest.slice(0, 1);\n const step = mark === JAPANESE_SHARP ? 1 : mark === JAPANESE_FLAT ? -1 : 0;\n if (step === 0) {\n break;\n }\n // 嬰 and 変 cancelling each other out is not a spelling anyone writes.\n if (direction !== 0 && direction !== step) {\n return null;\n }\n direction = step;\n alter += doubled ? step * 2 : step;\n rest = rest.slice(doubled ? 2 : 1);\n }\n const letter = JAPANESE_LETTERS.indexOf(rest as (typeof JAPANESE_LETTERS)[number]);\n return letter < 0 ? null : { letter, alter };\n}\n\n/** Write a Japanese name, without its octave. */\nfunction formatJapanese(note: Note): string {\n const glyph = JAPANESE_LETTERS[note.letter] ?? 'ハ';\n const marks = Math.abs(note.alter);\n if (marks === 0) {\n return glyph;\n }\n const mark = note.alter > 0 ? JAPANESE_SHARP : JAPANESE_FLAT;\n // 重 is the written form of a double accidental; past that there is no\n // convention to follow, so the mark simply repeats.\n return `${marks === 2 ? `${JAPANESE_DOUBLE}${mark}` : mark.repeat(marks)}${glyph}`;\n}\n\n/** Read an Italian (fixed-do) name, or null when the text is not one. */\nfunction parseItalian(text: string): Note | null {\n const words = text.toLowerCase().split(' ');\n const letter = ITALIAN_LETTERS.indexOf(words[0] as (typeof ITALIAN_LETTERS)[number]);\n if (letter < 0) {\n return null;\n }\n let alter = 0;\n let direction = 0;\n let doubled = false;\n for (const word of words.slice(1)) {\n if (word === ITALIAN_DOUBLE) {\n if (doubled) {\n return null;\n }\n doubled = true;\n continue;\n }\n const step = word === ITALIAN_SHARP ? 1 : word === ITALIAN_FLAT ? -1 : 0;\n if (step === 0 || (direction !== 0 && direction !== step)) {\n return null;\n }\n direction = step;\n alter += doubled ? step * 2 : step;\n doubled = false;\n }\n return doubled ? null : { letter, alter };\n}\n\n/** Write an Italian (fixed-do) name, without its octave. */\nfunction formatItalian(note: Note): string {\n const glyph = ITALIAN_LETTERS[note.letter] ?? 'do';\n const marks = Math.abs(note.alter);\n if (marks === 0) {\n return glyph;\n }\n const word = note.alter > 0 ? ITALIAN_SHARP : ITALIAN_FLAT;\n const accidental =\n marks === 2 ? `${ITALIAN_DOUBLE} ${word}` : Array.from({ length: marks }, () => word).join(' ');\n return `${glyph} ${accidental}`;\n}\n\n/** Split a trailing scientific octave off a name written in words or glyphs. */\nfunction splitOctave(text: string): { name: string; octave: number | undefined } {\n const match = text.match(/^(.*?)(-?\\d+)$/);\n if (match === null) {\n return { name: text, octave: undefined };\n }\n return { name: match[1] ?? '', octave: Number.parseInt(match[2] ?? '0', 10) };\n}\n\n/** Read a name in one system, or null when that system does not accept it. */\nfunction parseIn(text: string, system: NoteNameSystem): Note | null {\n const table = tableOf(system);\n if (table === 'english') {\n return parseEnglish(text);\n }\n const { name, octave } = splitOctave(text);\n const parser =\n table === 'german' ? parseGerman : table === 'japanese' ? parseJapanese : parseItalian;\n const note = parser(name);\n if (note === null) {\n return null;\n }\n return octave === undefined ? note : { ...note, octave };\n}\n\n/** A key name split into its tonic text and the mode its mode word gives. */\ntype ModeSplit = {\n tonic: string;\n mode: 'major' | 'minor' | undefined;\n system: NamingTable | undefined;\n};\n\n/**\n * Split a trailing mode word off a key name.\n *\n * Every system's words are tried and the longest match wins, so the mode word\n * also says which system the name is written in. German writes the word against\n * the tonic with a hyphen as often as with a space (`gis-Moll`), so both\n * separators go with it; Japanese writes no separator at all.\n */\nfunction splitModeWord(text: string): ModeSplit {\n const lower = text.toLowerCase();\n let best: { word: string; mode: 'major' | 'minor'; system: NamingTable } | undefined;\n for (const system of DETECTION_ORDER) {\n for (const mode of ['major', 'minor'] as const) {\n const word = MODE_WORDS[system][mode];\n if (lower.endsWith(word) && (best === undefined || word.length > best.word.length)) {\n best = { word, mode, system };\n }\n }\n }\n if (best === undefined) {\n return { tonic: text, mode: undefined, system: undefined };\n }\n return {\n tonic: text.slice(0, text.length - best.word.length).replace(/[\\s-]+$/, ''),\n mode: best.mode,\n system: best.system,\n };\n}\n\n/**\n * Which system a name is written in, or null when no system reads it.\n *\n * A name several systems accept goes to the first of `DETECTION_ORDER`, which\n * is what keeps a bare `B` an English B natural rather than a German B flat. A\n * mode word overrides that, because `dur` and `moll` are cues no other system\n * has — so `B dur` is B flat major. When the mode word belongs to a system that\n * cannot read the tonic, the name mixes two systems and there is nothing to\n * prefer, so it is rejected.\n */\nfunction resolveSystem(text: string): NoteNameSystem | null {\n const { tonic, system: wordSystem } = splitModeWord(text);\n const accepted = DETECTION_ORDER.filter((system) => parseIn(tonic, system) !== null);\n const first = accepted[0];\n if (first === undefined) {\n return null;\n }\n if (wordSystem === undefined) {\n return first;\n }\n if (!accepted.includes(wordSystem)) {\n throw new InvalidInputError(\n `${JSON.stringify(text)} mixes note-name systems: ${JSON.stringify(tonic)} is ` +\n `${accepted.join(' or ')} but its mode word is ${wordSystem}`,\n );\n }\n return wordSystem;\n}\n\n/**\n * The notation system a note name or key name is written in.\n *\n * A name several systems could read is attributed to the first of English,\n * German, Japanese, Italian that reads it — so `B` is the English B natural,\n * and German is chosen only for a name carrying a German-only cue (`H`, an\n * `-is`/`-es` ending, or `dur`/`moll`). Because the fixed-do syllables are the\n * Italian ones, detection reports `'italian'` for them; `'fixedDo'` is only ever\n * a system a caller asks for by name.\n *\n * @param text The note name or key name.\n * @returns The system that reads it.\n * @throws If no system reads the name, or the name mixes two systems — a German\n * tonic under an English mode word, say.\n * @example\n * ```ts\n * import { detectNoteNameSystem } from '@libraz/libcantus';\n * detectNoteNameSystem('B'); // 'english' — never the German B flat\n * detectNoteNameSystem('gis moll'); // 'german'\n * detectNoteNameSystem('嬰ト短調'); // 'japanese'\n * ```\n * @category Pitch & Intervals\n */\nexport function detectNoteNameSystem(text: string): NoteNameSystem {\n if (typeof text !== 'string') {\n throw new InvalidInputError(`name must be a string; received ${typeof text}`);\n }\n const system = resolveSystem(normalize(text));\n if (system === null) {\n throw new InvalidInputError(`no note-name system reads ${JSON.stringify(text)}`);\n }\n return system;\n}\n\n/** Read a note name, in the given system or in the one the name is written in. */\nexport function readNoteName(text: string, opts?: NoteNameOptions): Note {\n const name = normalize(text);\n const system = opts?.system === undefined ? resolveSystem(name) : assertSystem(opts.system);\n const note = system === null ? null : parseIn(name, system);\n if (note === null) {\n throw new InvalidInputError(`Invalid note: ${text}`);\n }\n return note;\n}\n\n/**\n * Write a note name in the given system, English by default.\n *\n * The letter must already be reduced to 0..6; the public entry point does that.\n */\nexport function writeNoteName(note: Note, opts?: NoteNameOptions): string {\n const system = opts?.system === undefined ? 'english' : assertSystem(opts.system);\n const table = tableOf(system);\n const name =\n table === 'german'\n ? formatGerman(note)\n : table === 'japanese'\n ? formatJapanese(note)\n : table === 'italian'\n ? formatItalian(note)\n : formatEnglish(note);\n return note.octave === undefined ? name : `${name}${note.octave}`;\n}\n\n/**\n * The mode a key name with no mode word carries.\n *\n * German writes the mode in the case of the tonic — `C` is C major and `c` is C\n * minor — which is the only convention here that a bare tonic decides.\n * Everywhere else a tonic alone names a major key.\n */\nfunction impliedMode(system: NoteNameSystem, tonic: string): 'major' | 'minor' {\n return tableOf(system) === 'german' && /^[a-h]/.test(tonic) ? 'minor' : 'major';\n}\n\n/** Read a key name, in the given system or in the one the name is written in. */\nexport function readKeyName(text: string, opts?: NoteNameOptions): KeyName {\n const name = normalize(text);\n const split = splitModeWord(name);\n const system = opts?.system === undefined ? resolveSystem(name) : assertSystem(opts.system);\n if (system !== null && split.system !== undefined && split.system !== tableOf(system)) {\n throw new InvalidInputError(\n `${JSON.stringify(text)} is not a ${system} key name: its mode word is ${split.system}`,\n );\n }\n const tonic = system === null ? null : parseIn(split.tonic, system);\n if (tonic === null || system === null) {\n throw new InvalidInputError(`Invalid key name: ${text}`);\n }\n if (tonic.octave !== undefined) {\n throw new InvalidInputError(`a key name carries no octave; received ${JSON.stringify(text)}`);\n }\n // An explicit mode word outranks the German case convention, so `C moll` is C\n // minor: the word is what the writer said, the case only what they typed.\n return { tonic, mode: split.mode ?? impliedMode(system, split.tonic) };\n}\n\n/**\n * Write a key name in the given system, English by default.\n *\n * The tonic's letter must already be reduced to 0..6 and its octave dropped;\n * the public entry point does both.\n */\nexport function writeKeyName(key: KeyName, opts?: NoteNameOptions): string {\n const system = opts?.system === undefined ? 'english' : assertSystem(opts.system);\n const table = tableOf(system);\n const word = MODE_WORDS[table][key.mode];\n const tonic = writeNoteName(key.tonic, { system });\n if (table === 'japanese') {\n return `${tonic}${word}`;\n }\n if (table === 'german') {\n // German carries the mode in the case of the tonic as well as in the word.\n const cased =\n key.mode === 'major' ? `${tonic.slice(0, 1).toUpperCase()}${tonic.slice(1)}` : tonic;\n return `${cased} ${word}`;\n }\n return `${tonic} ${word}`;\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, type ParseResult, parseFailure, unwrapParse } from '../errors/index.js';\nimport { assertFiniteNumber, assertInteger, assertOneOf } from '../validation/index.js';\nimport type { KeyName, NoteNameOptions } from './naming.js';\nimport { readKeyName, readNoteName, writeKeyName, writeNoteName } from './naming.js';\n\nexport type { KeyName, NoteNameOptions, NoteNameSystem } from './naming.js';\nexport { detectNoteNameSystem } from './naming.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/**\n * Validate the fields of a spelled note once, at a public entry point.\n *\n * The letter is required in its own range rather than reduced on the way in:\n * a caller who built a note by letter arithmetic (`base.letter + 2`) would\n * otherwise hold a value that prints like a library-made note but compares\n * unequal to it, so the unreduced letter is refused where it is written rather\n * than where it later fails to match.\n */\nfunction assertNote(note: Note, name: string): Note {\n assertInteger(note.letter, `${name}.letter`, 0, 6);\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/**\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 * True when the diatonic letters move down, and absent otherwise — an\n * ascending interval leaves the key out rather than carrying `false`, so\n * every producer of this type returns the same object for the same interval.\n * The flag is not redundant with the sign of the span: a descending unison\n * spans zero semitones, and a letter can rise while the pitch falls.\n */\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 a note name into a {@link Note}, in any supported notation system.\n *\n * English (the default when a name could be read in more than one system)\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 * Without a `system` the name is attributed by {@link detectNoteNameSystem},\n * which reads a name any two systems could claim as English: `\"B\"` is the B\n * natural, never the German B flat. German is chosen only for a German-only\n * name such as `\"H\"`, `\"fis\"` or `\"es\"`.\n *\n * A scientific octave may follow the name in every system (`\"gis4\"`,\n * `\"嬰ト4\"`), which is what makes formatting and parsing exact inverses for an\n * octave-bearing note.\n *\n * @param text The note name.\n * @param opts `system` reads the name in that notation system instead of\n * detecting it.\n * @returns The parsed note.\n * @throws If the text is not a valid note in the given (or detected) system.\n * Use {@link tryParseNote} where failure is ordinary, such as a note field\n * read on every keystroke.\n * @example\n * ```ts\n * import { parseNote, noteToMidi, noteToPitchClass } from '@libraz/libcantus';\n * noteToMidi(parseNote('C#4')); // 61\n * noteToPitchClass(parseNote('gis')); // 8 — the German G sharp\n * noteToPitchClass(parseNote('B', { system: 'german' })); // 10 — the German B flat\n * ```\n * @category Pitch & Intervals\n */\nexport function parseNote(text: string, opts?: NoteNameOptions): Note {\n return unwrapParse(tryParseNote(text, opts));\n}\n\n/**\n * Parse a note name, reporting failure instead of throwing it.\n *\n * The same reading as {@link parseNote} — that function is this one with its\n * error thrown — for the callers where a name that does not parse yet is the\n * normal state of the input rather than a fault.\n *\n * @param text The note name.\n * @param opts `system` reads the name in that notation system instead of\n * detecting it.\n * @returns The note, or the error explaining why the text is not one.\n * @example\n * ```ts\n * import { tryParseNote } from '@libraz/libcantus';\n * const result = tryParseNote('C#4');\n * result.ok ? result.value.letter : result.error.message;\n * ```\n * @category Pitch & Intervals\n */\nexport function tryParseNote(text: string, opts?: NoteNameOptions): ParseResult<Note> {\n try {\n if (typeof text !== 'string') {\n throw new InvalidInputError(`note must be a string; received ${typeof text}`);\n }\n return { ok: true, value: assertNote(readNoteName(text, opts), `note ${text}`) };\n } catch (error) {\n return parseFailure(error);\n }\n}\n\n/**\n * Render a {@link Note} as a note name, in any supported notation system.\n *\n * The inverse of {@link parseNote} when both are given the same `system`,\n * including double accidentals and octaves. Without one, {@link parseNote}\n * detects the system and reads a bare `B` as the English B natural, so a German\n * B flat written as `'b'` does not survive a round trip that names no system.\n *\n * @param note The note to format.\n * @param opts `system` writes the name in that notation system instead of\n * English.\n * @returns The note name, including the octave when present.\n * @example\n * ```ts\n * import { formatNote, parseNote } from '@libraz/libcantus';\n * formatNote(parseNote('C#4')); // 'C#4'\n * formatNote(parseNote('G#'), { system: 'german' }); // 'gis'\n * formatNote(parseNote('G#'), { system: 'japanese' }); // '嬰ト'\n * ```\n * @category Pitch & Intervals\n */\nexport function formatNote(note: Note, opts?: NoteNameOptions): string {\n assertNote(note, 'note');\n return writeNoteName({ letter: mod7(note.letter), alter: note.alter, octave: note.octave }, opts);\n}\n\n/**\n * Parse a key name such as `'C major'`, `'gis moll'` or `'嬰ト短調'`.\n *\n * The name is a tonic followed by a mode word, in any of the supported\n * notation systems; German also accepts its hyphenated form (`'gis-Moll'`).\n * Without a `system` the name is attributed by {@link detectNoteNameSystem},\n * so a mode word is itself enough to pick one: `'B dur'` is B flat major while\n * `'B major'` is B major.\n *\n * German carries the mode twice, in the word and in the case of the tonic\n * (`C dur`, `c moll`). When the two disagree the word wins — `'C moll'` is C\n * minor — because the word is what the writer said and the case only what they\n * typed. A German name with no mode word is decided by its case alone\n * (`'gis'` is G sharp minor, `'Gis'` G sharp major); a bare tonic in any other\n * system names a major key.\n *\n * @param text The key name.\n * @param opts `system` reads the name in that notation system instead of\n * detecting it.\n * @returns The spelled tonic and the mode.\n * @throws If the text is not a key name in the given (or detected) system, if\n * it mixes two systems, or if the tonic carries an octave. Use\n * {@link tryParseKeyName} where failure is ordinary, such as a key field read\n * on every keystroke.\n * @example\n * ```ts\n * import { formatNote, parseKeyName } from '@libraz/libcantus';\n * const key = parseKeyName('gis moll');\n * formatNote(key.tonic); // 'G#'\n * key.mode; // 'minor'\n * ```\n * @category Scales\n */\nexport function parseKeyName(text: string, opts?: NoteNameOptions): KeyName {\n return unwrapParse(tryParseKeyName(text, opts));\n}\n\n/**\n * Parse a key name, reporting failure instead of throwing it.\n *\n * The same reading as {@link parseKeyName} — that function is this one with its\n * error thrown — for the callers where a name that does not parse yet is the\n * normal state of the input rather than a fault.\n *\n * @param text The key name.\n * @param opts `system` reads the name in that notation system instead of\n * detecting it.\n * @returns The key, or the error explaining why the text is not one.\n * @example\n * ```ts\n * import { tryParseKeyName } from '@libraz/libcantus';\n * const result = tryParseKeyName('gis moll');\n * result.ok ? result.value.mode : result.error.message; // 'minor'\n * ```\n * @category Scales\n */\nexport function tryParseKeyName(text: string, opts?: NoteNameOptions): ParseResult<KeyName> {\n try {\n if (typeof text !== 'string') {\n throw new InvalidInputError(`key name must be a string; received ${typeof text}`);\n }\n const key = readKeyName(text, opts);\n assertNote(key.tonic, `key ${text}`);\n return { ok: true, value: key };\n } catch (error) {\n return parseFailure(error);\n }\n}\n\n/**\n * Render a spelled tonic and a mode as a key name.\n *\n * The inverse of {@link parseKeyName}. German is written the way it is read,\n * with the mode in the case of the tonic as well as in the word.\n *\n * @param key The spelled tonic and mode; any octave on the tonic is dropped,\n * since a key has no register.\n * @param opts `system` writes the name in that notation system instead of\n * English.\n * @returns The key name.\n * @throws If the tonic or the mode is not valid.\n * @example\n * ```ts\n * import { formatKeyName, parseNote } from '@libraz/libcantus';\n * formatKeyName({ tonic: parseNote('G#'), mode: 'minor' }); // 'G# minor'\n * formatKeyName({ tonic: parseNote('G#'), mode: 'minor' }, { system: 'german' }); // 'gis moll'\n * ```\n * @category Scales\n */\nexport function formatKeyName(key: KeyName, opts?: NoteNameOptions): string {\n assertNote(key.tonic, 'key.tonic');\n const mode = assertOneOf(key.mode, ['major', 'minor'], 'key.mode');\n return writeKeyName(\n { tonic: { letter: mod7(key.tonic.letter), alter: key.tonic.alter }, mode },\n opts,\n );\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/**\n * Letter and alteration of each pitch class, by the side a black key is named\n * from when a bare MIDI number carries no spelling of its own.\n */\nconst SPELLINGS = {\n sharp: [\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 flat: [\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} as const satisfies Record<'sharp' | 'flat', readonly (readonly [number, number])[]>;\n\n/** The spellings a caller may ask for, read off the table rather than repeated. */\nconst SPELLING_NAMES = Object.keys(SPELLINGS) as (keyof typeof SPELLINGS)[];\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 * @throws If `spelling` is neither `'sharp'` nor `'flat'`; a name the table does\n * not carry would otherwise be spelled as one of them without saying which.\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 side = assertOneOf(spelling, SPELLING_NAMES, 'midi spelling');\n const rounded = Math.round(midi);\n const pc = mod12(rounded);\n const octave = Math.floor(rounded / 12) - 1;\n const entry = SPELLINGS[side][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 * Widest diatonic interval number the library measures.\n *\n * It is the widest {@link spelledInterval} can produce over the MIDI range:\n * C-1 to G9 climbs 74 letters, which is a 75th. Rejecting anything narrower\n * would make a value one public function returns impossible to hand to another.\n */\nconst MAX_INTERVAL_NUMBER = 75;\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 or a\n * diminished unison.\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, MAX_INTERVAL_NUMBER);\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 `an interval of ${numberValue} 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 if (numberValue === 1) {\n // The letters do not move in a unison, so narrowing one takes it past\n // zero and back up the other side: `spelledInterval` reads C down to Cb\n // as a descending augmented unison, and no measurement in the library\n // produces a diminished one. Accepting the name would return a span\n // indistinguishable from the augmented unison's.\n throw new InvalidInputError(\n `a unison cannot be diminished; a unison narrowed by a semitone descends, so name it '-${'A'.repeat(quality.length)}1'`,\n );\n }\n return Math.abs(reference - quality.length - (perfect ? 0 : 1));\n }\n throw new InvalidInputError(`unknown interval quality ${JSON.stringify(quality)}`);\n}\n\n/** Interval name grammar: an optional descent marker, a quality, a number. */\nconst INTERVAL_NAME_PATTERN = /^(-?)(P|M|m|A+|d+)(\\d+)$/;\n\n/**\n * Parse an interval name such as `'P5'`, `'m3'`, `'AA4'`, or `'-m3'`.\n *\n * A leading `'-'` names the same interval taken downward: the span is negated\n * and `descending` is set. The flag is not redundant with the sign — a\n * descending unison spans zero semitones, so only the flag records that the\n * letters move down.\n *\n * @param name The interval name: an optional `'-'`, a quality label, and a\n * diatonic number. Surrounding whitespace is ignored.\n * @returns The spelled interval. An ascending name carries a non-negative span\n * and no `descending` flag.\n * @throws If the name is not a quality label followed by a number, or the two\n * cannot describe the same interval. Use {@link tryParseInterval} where\n * failure is ordinary, such as an interval field read on every keystroke.\n * @example\n * ```ts\n * import { parseInterval } from '@libraz/libcantus';\n * parseInterval('m3'); // { number: 3, quality: 'm', semitones: 3 }\n * parseInterval('-m3'); // { number: 3, quality: 'm', semitones: -3, descending: true }\n * ```\n * @category Pitch & Intervals\n */\nexport function parseInterval(name: string): SpelledInterval {\n return unwrapParse(tryParseInterval(name));\n}\n\n/**\n * Parse an interval name, reporting failure instead of throwing it.\n *\n * The same reading as {@link parseInterval} — that function is this one with\n * its error thrown — for the callers where a name that does not parse yet is\n * the normal state of the input rather than a fault.\n *\n * @param name The interval name.\n * @returns The interval, or the error explaining why the text is not one.\n * @example\n * ```ts\n * import { tryParseInterval } from '@libraz/libcantus';\n * const result = tryParseInterval('M5');\n * result.ok ? result.value.semitones : result.error.message; // a fifth cannot be major\n * ```\n * @category Pitch & Intervals\n */\nexport function tryParseInterval(name: string): ParseResult<SpelledInterval> {\n try {\n const match = typeof name === 'string' ? name.trim().match(INTERVAL_NAME_PATTERN) : null;\n const quality = match?.[2] as IntervalQualityLabel | undefined;\n const numberValue = Number(match?.[3]);\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 const span = intervalSemitones(numberValue, quality);\n if (match?.[1] !== '-') {\n return { ok: true, value: { number: numberValue, quality, semitones: span } };\n }\n // Negating a zero span yields -0, which compares unequal to 0 under\n // Object.is and would leak into every equality check downstream.\n return {\n ok: true,\n value: { number: numberValue, quality, semitones: span === 0 ? 0 : -span, descending: true },\n };\n } catch (error) {\n return parseFailure(error);\n }\n}\n\n/**\n * Anything that names a spelled interval: an interval name, plain interval\n * data, or a value that serializes to interval data such as the `Interval`\n * class.\n *\n * @category Pitch & Intervals\n */\nexport type IntervalLike =\n | string\n | SpelledInterval\n | {\n /** The interval data this value stands for. */\n toJSON(): SpelledInterval;\n };\n\n/**\n * Whether an interval's letters move down.\n *\n * Data that carries the flag decides it there. Without one the span's sign\n * answers it, except where the quality says otherwise: an interval can climb a\n * letter while losing a semitone — C# up to Dbb is a doubly diminished second —\n * and reading such a span as a descent would spell the result on the wrong\n * letter. The exception is taken only when the ascending reading names the very\n * quality the interval carries, so plain data such as `{ P5, -7 }` still reads\n * as the descending fifth it is.\n */\nfunction isDescendingInterval(interval: SpelledInterval): boolean {\n if (interval.descending !== undefined) {\n return interval.descending;\n }\n const span = Math.round(interval.semitones);\n if (span < 0 && qualityFromSpan(interval.number, span) === interval.quality) {\n return false;\n }\n return span < 0;\n}\n\n/** Validate plain interval data and return it in the canonical shape. */\nfunction normalizedInterval(data: SpelledInterval): SpelledInterval {\n assertInteger(data.number, 'interval.number', 1);\n assertFiniteNumber(data.semitones, 'interval.semitones');\n const expected = intervalSemitones(data.number, data.quality);\n if (Math.abs(data.semitones) !== expected) {\n throw new InvalidInputError(\n `${data.quality}${data.number} spans ${expected} semitones; received ${data.semitones}`,\n );\n }\n const normalized: SpelledInterval = {\n number: data.number,\n quality: data.quality,\n semitones: data.semitones,\n };\n // An ascending interval leaves the flag out entirely, so a parsed name, this\n // function, and `spelledInterval` all produce the same object.\n if (isDescendingInterval(data)) {\n normalized.descending = true;\n }\n return normalized;\n}\n\n/**\n * Resolve any interval-shaped value to plain {@link SpelledInterval} data.\n *\n * This is what lets an entry point accept an interval name, the plain data the\n * pitch module returns, or an `Interval` instance without every caller\n * branching on the form. An instance is accepted through its `toJSON` method\n * rather than by its type, because the core layer cannot import the model layer\n * that defines the class.\n *\n * @param value An interval name, plain interval data, or a value whose `toJSON`\n * returns interval data.\n * @returns The validated interval data, carrying `descending` only when the\n * interval descends.\n * @throws If the value is not interval-shaped, or its number, quality, and span\n * do not describe the same interval.\n * @example\n * ```ts\n * import { toSpelledInterval } from '@libraz/libcantus';\n * toSpelledInterval('P5'); // { number: 5, quality: 'P', semitones: 7 }\n * toSpelledInterval({ number: 5, quality: 'P', semitones: -7 });\n * // { number: 5, quality: 'P', semitones: -7, descending: true }\n * ```\n * @category Pitch & Intervals\n */\nexport function toSpelledInterval(value: IntervalLike): SpelledInterval {\n if (typeof value === 'string') {\n return parseInterval(value);\n }\n if (typeof value === 'object' && value !== null) {\n const data = 'toJSON' in value && typeof value.toJSON === 'function' ? value.toJSON() : value;\n return normalizedInterval(data as SpelledInterval);\n }\n throw new InvalidInputError(\n `interval must be a name or spelled interval data; received ${typeof value}`,\n );\n}\n\n/**\n * Anything that names a spelled note: a note name, plain note data, a MIDI\n * number, or a value that serializes to note data such as the `Note` class.\n *\n * @category Pitch & Intervals\n */\nexport type NoteLike =\n | string\n | number\n | Note\n | {\n /** The note data this value stands for. */\n toJSON(): Note;\n };\n\n/**\n * Resolve any note-shaped value to plain {@link Note} data.\n *\n * The counterpart of {@link toSpelledInterval} for notes: an entry point takes\n * whatever form the caller has — the name a text field holds, the MIDI number a\n * device sends, the data the pitch module returns, or a `Note` instance — and\n * gets one shape back. An instance is accepted through its `toJSON` method\n * rather than by its type, because the core layer cannot import the model layer\n * that defines the class.\n *\n * A number is read as MIDI and spelled with sharps, the same reading\n * {@link midiToNote} takes by default. Where a key is known the caller spells\n * it there instead — `spellPitch` reads the key — because no single default\n * suits both a G# in E major and an Ab in Eb major.\n *\n * @param value A note name, a MIDI number, plain note data, or a value whose\n * `toJSON` returns note data.\n * @returns The validated note data, carrying `octave` only when the value has\n * one.\n * @throws If the value is not note-shaped, or its letter, alteration, or octave\n * fall outside the range a spelled note holds.\n * @example\n * ```ts\n * import { toNoteData } from '@libraz/libcantus';\n * toNoteData('Eb4'); // { letter: 2, alter: -1, octave: 4 }\n * toNoteData(60); // { letter: 0, alter: 0, octave: 4 }\n * ```\n * @category Pitch & Intervals\n */\nexport function toNoteData(value: NoteLike): Note {\n if (typeof value === 'string') {\n return parseNote(value);\n }\n if (typeof value === 'number') {\n return midiToNote(value);\n }\n if (typeof value === 'object' && value !== null) {\n const data = 'toJSON' in value && typeof value.toJSON === 'function' ? value.toJSON() : value;\n const note = assertNote(data as Note, 'note');\n // A fresh object, so a caller's own note cannot become library state and\n // the result carries `octave` only when the note has one.\n return note.octave === undefined\n ? { letter: note.letter, alter: note.alter }\n : { letter: note.letter, alter: note.alter, octave: note.octave };\n }\n throw new InvalidInputError(\n `note must be a name, a MIDI number, or spelled note data; received ${typeof value}`,\n );\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 letterSteps = (interval.number - 1) * (isDescendingInterval(interval) ? -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 interval: SpelledInterval = {\n number,\n quality: qualityFromSpan(number, directedSpan),\n semitones,\n };\n // An ascending interval leaves the flag out entirely, so a parsed name,\n // `normalizedInterval`, and this function all produce the same object for the\n // same interval.\n if (letterSteps < 0 || (letterSteps === 0 && semitones < 0)) {\n interval.descending = true;\n }\n return interval;\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"]}
@@ -1 +0,0 @@
1
- "use strict";//# sourceMappingURL=chunk-RHIJ3S4I.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["/home/runner/work/libcantus/libcantus/dist/chunk-RHIJ3S4I.cjs"],"names":[],"mappings":"AAAA","file":"/home/runner/work/libcantus/libcantus/dist/chunk-RHIJ3S4I.cjs"}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/analyze/spelling/index.ts"],"sourcesContent":["/**\n * Line spelling: name every note of a melodic line in one pass.\n *\n * {@link spellPitch} decides one note from the key, the root of the chord under\n * it and its two immediate neighbours. That is enough for a single note, but a\n * line spelled note by note can zig-zag between the two readings of one idea —\n * a rising whole-tone figure comes out F# Ab Bb, with a diminished third in the\n * middle of it.\n *\n * This module solves the whole line at once. The theory layer's own judgment\n * becomes the cost of emitting a spelling, the melodic rules become the cost of\n * moving from one spelling to the next, and a dynamic program takes the\n * cheapest path across the line. Nothing here re-decides what\n * {@link spellPitchClass} and {@link spellChord} already decide: it only weighs\n * their answers against the shape of the line they sit in.\n */\n\nimport type { Note, NoteLike } from '../../core/pitch/index.js';\nimport {\n diatonicLetterOf as mod7,\n pitchClassOf as mod12,\n naturalPitchClassOf as naturalPc,\n toNoteData,\n} from '../../core/pitch/index.js';\nimport type { KeyScale, NoteEvent } from '../../core/types.js';\nimport { assertGenerationBudget, assertNoteEvents } from '../../core/validation/index.js';\nimport type { Chord } from '../../theory/chord/index.js';\nimport { isScaleTone, type KeyLike, spelledKeyOf, toKeyScale } from '../../theory/scale/index.js';\nimport { assertTonicOf, spellChord, spellPitchClass } from '../../theory/spelling/index.js';\nimport type { ChordTimeline } from '../timeline/index.js';\n\n/**\n * The four rules, and what happens where they pull against each other.\n *\n * The costs below are spread far enough apart that a rule can never be\n * outvoted by any number of the rules under it, which fixes the precedence:\n *\n * 1. **The sounding chord.** A note the chord names takes the chord's own\n * spelling of that tone. This is the one place the line departs from\n * {@link spellPitchClass}, which never respells a tone of the key: over a\n * Db7 the seventh has to read Cb even in C major, where B is a scale tone,\n * because a dominant seventh spelled as a sixth is not the chord that was\n * played.\n * 2. **The key's scale.** Anything else the key itself spells — a scale tone,\n * and the tonic — keeps that spelling, exactly as the note-by-note path\n * keeps it. No amount of melodic pressure moves it.\n * 3. **The shape of the line, then its direction.** A chromatic note is spelled\n * so the letters of the intervals around it match the intervals actually\n * sounding, and a stepwise line then prefers sharps rising and flats\n * falling. Shape outranks direction because a rising line spelled with\n * sharps is still wrong if it spells a whole tone as a diminished third.\n * 4. **The key's chromatic convention, then the fewest accidentals.** What the\n * key alone would have said, as the tiebreak it is: the direction rule beats\n * it, which is why a chromatic ascent in C major spells C# D# G# A# rather\n * than the flats the key prefers, but one-sided evidence — a chromatic note\n * with only one stepwise neighbour — does not.\n */\n\n/** Emission cost for a spelling the chord sounding under the note does not name. */\nconst CHORD_MISMATCH = 64;\n\n/** Emission cost for respelling a tone the key's own scale already names. */\nconst SCALE_TONE_MISMATCH = 32;\n\n/** Transition cost per diatonic step an interval's letters miss its written size by. */\nconst LETTER_STEP_DEVIATION = 4;\n\n/**\n * How many steps of deviation are counted before the cost stops growing.\n *\n * Two letters out is already an interval no reading supports, and capping there\n * keeps the whole melodic term below {@link SCALE_TONE_MISMATCH}, so a badly\n * shaped line can never argue a scale tone out of its key spelling.\n */\nconst MAX_COUNTED_DEVIATION = 2;\n\n/** Transition cost for an accidental leaning against the direction of a stepwise line. */\nconst AGAINST_DIRECTION = 3;\n\n/** Emission cost for a chromatic spelling the key alone would not choose. */\nconst KEY_MISMATCH = 4;\n\n/** Emission cost per semitone of alteration: what minimises accidentals overall. */\nconst ACCIDENTAL = 1;\n\n/** Widest melodic interval still counted as a step, where the direction rule applies. */\nconst MAX_STEP = 2;\n\n/** Widest alteration a spelling may carry: a double accidental. */\nconst MAX_ALTER = 2;\n\n/**\n * Spellings each pitch class has within a double accidental.\n *\n * Always exactly three — a natural or its accidental, one letter above, one\n * letter below (C is C, B# and Dbb) — which is what bounds the work the dynamic\n * program is allowed to do.\n */\nconst SPELLINGS_PER_PITCH = 3;\n\n/**\n * The diatonic letter distances a melodic interval of a given size is\n * conventionally written with, indexed by its semitone class.\n *\n * Most sizes read only one way: a whole tone is a second, a minor third is a\n * third, so spelling one as anything else costs. Two sizes read two ways and\n * both are free — a semitone is either a chromatic inflection of one letter\n * (F to F#) or a diatonic step (E to F), and the tritone is either an augmented\n * fourth or a diminished fifth. Direction chooses between those.\n */\nconst CONVENTIONAL_LETTER_STEPS: readonly (readonly number[])[] = [\n [0], // unison\n [0, 1], // chromatic semitone or diatonic semitone\n [1], // second\n [2], // minor third\n [2], // major third\n [3], // fourth\n [3, 4], // augmented fourth or diminished fifth\n [4], // fifth\n [5], // minor sixth\n [5], // major sixth\n [6], // minor seventh\n [6], // major seventh\n];\n\n/**\n * Options for {@link spellLine}.\n *\n * @category Arrangement & Analysis\n */\nexport type SpellLineOptions = {\n /**\n * Tonic spelling to anchor the key, for a caller that already knows how the\n * piece is written. It must sound the key's own root pitch class; anything\n * else is rejected rather than spelled. Defaults to the conventional\n * spelling `spelledKeyOf` gives the key. Accepted as a note name, a MIDI\n * number, note data, or a `Note`.\n */\n tonic?: NoteLike;\n /** Upper bound on the work the line may cost; defaults to the generation budget. */\n budget?: number;\n};\n\n/** Shortest signed alteration (in [-6, 6]) taking a letter's natural pc to `pc`. */\nfunction alterationFor(letter: number, pc: number): number {\n const distance = mod12(pc - naturalPc(letter));\n return distance > 6 ? distance - 12 : distance;\n}\n\n/** The octave that reproduces `pitch` from a letter and its alteration. */\nfunction octaveFor(pitch: number, letter: number, alter: number): number {\n return (pitch - naturalPc(letter) - alter) / 12 - 1;\n}\n\n/** Whether two spellings name the same letter with the same alteration. */\nfunction sameSpelling(a: Note, b: Note): boolean {\n return a.letter === b.letter && a.alter === b.alter;\n}\n\n/**\n * Every spelling of `pitch` within a double accidental, `preferred` first.\n *\n * The order is what settles a tie: two readings that cost the same are equally\n * defensible, and the one the theory layer would have chosen alone is then the\n * one that stands.\n */\nfunction candidatesFor(pitch: number, preferred: Note): Note[] {\n const pc = mod12(pitch);\n const rest: Note[] = [];\n for (let letter = 0; letter < 7; letter += 1) {\n const alter = alterationFor(letter, pc);\n if (Math.abs(alter) > MAX_ALTER || letter === preferred.letter) {\n continue;\n }\n rest.push({ letter, alter, octave: octaveFor(pitch, letter, alter) });\n }\n rest.sort((a, b) => Math.abs(a.alter) - Math.abs(b.alter) || a.letter - b.letter);\n return [\n {\n letter: preferred.letter,\n alter: preferred.alter,\n octave: octaveFor(pitch, preferred.letter, preferred.alter),\n },\n ...rest,\n ];\n}\n\n/**\n * The chord's own spelling of a pitch class, or undefined when the chord does\n * not sound it.\n *\n * The tones come from {@link spellChord}, so a chord carrying its own spellings\n * — a parsed symbol, an augmented sixth — is read by them; a slash bass outside\n * the chord's tones is spelled from its hint when it has one.\n */\nfunction chordSpellingOf(pc: number, chord: Chord, tonic: Note, key: KeyScale): Note | undefined {\n for (const tone of spellChord(chord, tonic, key)) {\n if (mod12(naturalPc(tone.letter) + tone.alter) === pc) {\n return tone;\n }\n }\n const bass = chord.bassPc;\n if (bass === undefined || mod12(bass) !== pc) {\n return undefined;\n }\n const hint = chord.bassSpelling;\n return hint !== undefined && mod12(naturalPc(hint.letter) + hint.alter) === pc\n ? { letter: mod7(hint.letter), alter: hint.alter }\n : spellPitchClass(pc, tonic, key);\n}\n\n/** One note of the line, with the spellings open to it and what each costs. */\ntype LineState = {\n pitch: number;\n /** True when the key does not already spell this pitch, so direction may decide. */\n chromatic: boolean;\n candidates: Note[];\n emissions: number[];\n /** The spelling the note-by-note path would have chosen, used as the fallback. */\n reference: Note;\n};\n\n/**\n * Weigh every spelling open to one note against what the chord and the key say\n * about it.\n *\n * The reference spelling is the theory layer's own answer — the chord's, when\n * the chord names the note, and the key's otherwise — and the cost of departing\n * from it is what ranks the three rules that do not depend on the neighbours.\n */\nfunction stateFor(pitch: number, chord: Chord | null, tonic: Note, key: KeyScale): LineState {\n const pc = mod12(pitch);\n const fromChord = chord === null ? undefined : chordSpellingOf(pc, chord, tonic, key);\n const scaleTone = isScaleTone(pc, key);\n const reference = fromChord ?? spellPitchClass(pc, tonic, key);\n const mismatch =\n fromChord !== undefined ? CHORD_MISMATCH : scaleTone ? SCALE_TONE_MISMATCH : KEY_MISMATCH;\n const candidates = candidatesFor(pitch, reference);\n return {\n pitch,\n chromatic: !scaleTone,\n candidates,\n emissions: candidates.map(\n (candidate) =>\n (sameSpelling(candidate, reference) ? 0 : mismatch) +\n ACCIDENTAL * Math.abs(candidate.alter),\n ),\n reference,\n };\n}\n\n/** A spelling's position on the diatonic ladder, octave included. */\nfunction diatonicIndex(note: Note): number {\n return note.letter + 7 * (note.octave ?? 0);\n}\n\n/**\n * What the letters of a melodic interval cost when they do not match the\n * interval that actually sounds.\n *\n * Both notes are already spelled, so the sounding interval and the written one\n * are both known: a whole tone written F# to Ab is two letters apart where a\n * second is one, and pays for the difference.\n */\nfunction letterStepCost(from: Note, to: Note, semitones: number): number {\n const written = diatonicIndex(to) - diatonicIndex(from);\n const size = Math.abs(semitones);\n const octaves = Math.floor(size / 12);\n const direction = semitones < 0 ? -1 : 1;\n let deviation = Number.POSITIVE_INFINITY;\n for (const steps of CONVENTIONAL_LETTER_STEPS[size % 12] ?? []) {\n deviation = Math.min(deviation, Math.abs(written - direction * (octaves * 7 + steps)));\n }\n if (!Number.isFinite(deviation)) {\n return 0;\n }\n return LETTER_STEP_DEVIATION * Math.min(deviation, MAX_COUNTED_DEVIATION);\n}\n\n/**\n * What an accidental costs for leaning the wrong way in a stepwise line: a\n * rising line is written with sharps and a falling one with flats.\n *\n * Only steps are judged. The rule describes chromatic motion, and a leap onto a\n * flat says nothing about the flat — the key and the chord have the whole story\n * there.\n */\nfunction directionCost(note: Note, chromatic: boolean, semitones: number): number {\n if (!chromatic || semitones === 0 || Math.abs(semitones) > MAX_STEP) {\n return 0;\n }\n const rising = semitones > 0;\n return (rising && note.alter < 0) || (!rising && note.alter > 0) ? AGAINST_DIRECTION : 0;\n}\n\n/**\n * The cost of following one spelling with another.\n *\n * Direction is charged to both notes: a chromatic note is judged by the step it\n * arrives on and by the step it leaves on, so a note inside a run collects the\n * rule twice and a note at the end of one collects it once.\n */\nfunction transitionCost(from: LineState, source: Note, to: LineState, target: Note): number {\n const semitones = to.pitch - from.pitch;\n return (\n letterStepCost(source, target, semitones) +\n directionCost(target, to.chromatic, semitones) +\n directionCost(source, from.chromatic, semitones)\n );\n}\n\n/**\n * Spell a whole melodic line at once, so its notes agree with each other.\n *\n * Every note is spelled together with the rest of the line rather than one at a\n * time: each pitch offers the three spellings a double accidental allows, the\n * chord and the key price them, the intervals between neighbours price the\n * pairs, and the cheapest reading of the whole line wins. Ties fall to the\n * spelling {@link spellPitchClass} would have chosen alone, so the answer never\n * drifts from the theory layer without a reason.\n *\n * Four rules decide, in this order:\n *\n * 1. A note the sounding chord names takes that chord's spelling of it, even\n * when the key spells the pitch otherwise — the seventh of a Db7 reads Cb in\n * C major, not B.\n * 2. Anything else the key's scale spells keeps its key spelling, exactly as\n * the note-by-note path keeps it.\n * 3. A chromatic note is spelled so the written intervals around it match the\n * sounding ones, and then so a rising step carries a sharp and a falling\n * step a flat.\n * 4. Failing all of that, the key's own chromatic spelling stands, and the\n * fewest accidentals win.\n *\n * Without a timeline — pass `null` — rules 2 to 4 still apply in full, so a\n * diatonic line and a chromatic run come out the same. What is lost is rule 1:\n * a chromatic chord can no longer name its own tones, so the tones of a\n * borrowed or altered chord fall back to the key's convention (that Cb becomes\n * B), and an enharmonically ambiguous tone under a chord that would have fixed\n * it is decided by the line's direction instead.\n *\n * @param notes The line, monophonic and in time order. Notes sharing an onset\n * are read as consecutive, so split polyphonic material into voices first.\n * @param timeline The chords sounding under the line, or null when only the key\n * is known. Each note is judged against the chord at its own onset.\n * @param key The key the line is written in, as a key name, a key/scale, or a\n * `Key`.\n * @param opts Optional tonic spelling and work budget.\n * @returns One spelled note per input note, in input order, each carrying the\n * octave that reproduces its pitch.\n * @throws If `opts.tonic` does not sound the key's root pitch class, if a note\n * event is malformed, or if the line is longer than the budget allows.\n * @example\n * ```ts\n * import { chordTimelineFromChords, majorKey, noteNames, spellLine } from '@libraz/libcantus';\n * // A chromatic ascent spells with sharps, whatever the key would say alone.\n * const rising = [60, 61, 62, 63, 64].map((pitch, index) => ({\n * pitch,\n * startBeat: index,\n * durationBeat: 1,\n * }));\n * noteNames(spellLine(rising, null, majorKey(0)));\n * // ['C4', 'C#4', 'D4', 'D#4', 'E4']\n * // Under a Db7 the same B sounds as the chord's seventh.\n * const timeline = chordTimelineFromChords([{ rootPc: 1, quality: 'dom7', startBeat: 0 }], 4);\n * noteNames(spellLine([{ pitch: 71, startBeat: 0, durationBeat: 1 }], timeline, majorKey(0)));\n * // ['Cb5']\n * ```\n * @category Arrangement & Analysis\n */\nexport function spellLine(\n notes: readonly NoteEvent[],\n timeline: ChordTimeline | null,\n key: KeyLike,\n opts: SpellLineOptions = {},\n): Note[] {\n const scale = toKeyScale(key);\n const given = opts.tonic === undefined ? undefined : toNoteData(opts.tonic);\n assertNoteEvents(notes, 'line notes', {\n allowNonPositiveDuration: true,\n budget: opts.budget,\n });\n // The dynamic program compares every spelling of a note with every spelling\n // of the one before it, so the work is bounded before any of it is done.\n assertGenerationBudget(\n notes.length * SPELLINGS_PER_PITCH * SPELLINGS_PER_PITCH,\n 'line spelling states',\n opts.budget,\n );\n if (given !== undefined) {\n assertTonicOf(given, scale, 'spellLine');\n }\n if (notes.length === 0) {\n return [];\n }\n const tonic = given ?? spelledKeyOf(scale).tonic;\n const states = notes.map((note) =>\n stateFor(note.pitch, timeline?.at(note.startBeat) ?? null, tonic, scale),\n );\n\n // Viterbi across the line: `costs` holds the cheapest path reaching each\n // spelling of the current note, and `back` remembers which spelling of the\n // previous note that path came through.\n let costs: number[] = [];\n let previous: LineState | undefined;\n const back: number[][] = [];\n for (const current of states) {\n if (previous === undefined) {\n costs = [...current.emissions];\n back.push(current.candidates.map(() => 0));\n previous = current;\n continue;\n }\n const reached: number[] = [];\n const chosen: number[] = [];\n for (const [target, candidate] of current.candidates.entries()) {\n let best = Number.POSITIVE_INFINITY;\n let bestSource = 0;\n for (const [source, sourceNote] of previous.candidates.entries()) {\n const total =\n (costs[source] ?? Number.POSITIVE_INFINITY) +\n transitionCost(previous, sourceNote, current, candidate);\n if (total < best) {\n best = total;\n bestSource = source;\n }\n }\n reached.push(best + (current.emissions[target] ?? 0));\n chosen.push(bestSource);\n }\n costs = reached;\n back.push(chosen);\n previous = current;\n }\n\n let cursor = 0;\n for (const [index, cost] of costs.entries()) {\n if (cost < (costs[cursor] ?? Number.POSITIVE_INFINITY)) {\n cursor = index;\n }\n }\n const picked: number[] = new Array(states.length).fill(0);\n for (let index = states.length - 1; index >= 0; index -= 1) {\n picked[index] = cursor;\n cursor = back[index]?.[cursor] ?? 0;\n }\n return states.map((state, index) => state.candidates[picked[index] ?? 0] ?? state.reference);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA2DA,IAAM,iBAAiB;AAGvB,IAAM,sBAAsB;AAG5B,IAAM,wBAAwB;AAS9B,IAAM,wBAAwB;AAG9B,IAAM,oBAAoB;AAG1B,IAAM,eAAe;AAGrB,IAAM,aAAa;AAGnB,IAAM,WAAW;AAGjB,IAAM,YAAY;AASlB,IAAM,sBAAsB;AAY5B,IAAM,4BAA4D;AAAA,EAChE,CAAC,CAAC;AAAA;AAAA,EACF,CAAC,GAAG,CAAC;AAAA;AAAA,EACL,CAAC,CAAC;AAAA;AAAA,EACF,CAAC,CAAC;AAAA;AAAA,EACF,CAAC,CAAC;AAAA;AAAA,EACF,CAAC,CAAC;AAAA;AAAA,EACF,CAAC,GAAG,CAAC;AAAA;AAAA,EACL,CAAC,CAAC;AAAA;AAAA,EACF,CAAC,CAAC;AAAA;AAAA,EACF,CAAC,CAAC;AAAA;AAAA,EACF,CAAC,CAAC;AAAA;AAAA,EACF,CAAC,CAAC;AAAA;AACJ;AAqBA,SAAS,cAAc,QAAgB,IAAoB;AACzD,QAAM,WAAW,aAAM,KAAK,oBAAU,MAAM,CAAC;AAC7C,SAAO,WAAW,IAAI,WAAW,KAAK;AACxC;AAGA,SAAS,UAAU,OAAe,QAAgB,OAAuB;AACvE,UAAQ,QAAQ,oBAAU,MAAM,IAAI,SAAS,KAAK;AACpD;AAGA,SAAS,aAAa,GAAS,GAAkB;AAC/C,SAAO,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE;AAChD;AASA,SAAS,cAAc,OAAe,WAAyB;AAC7D,QAAM,KAAK,aAAM,KAAK;AACtB,QAAM,OAAe,CAAC;AACtB,WAAS,SAAS,GAAG,SAAS,GAAG,UAAU,GAAG;AAC5C,UAAM,QAAQ,cAAc,QAAQ,EAAE;AACtC,QAAI,KAAK,IAAI,KAAK,IAAI,aAAa,WAAW,UAAU,QAAQ;AAC9D;AAAA,IACF;AACA,SAAK,KAAK,EAAE,QAAQ,OAAO,QAAQ,UAAU,OAAO,QAAQ,KAAK,EAAE,CAAC;AAAA,EACtE;AACA,OAAK,KAAK,CAAC,GAAG,MAAM,KAAK,IAAI,EAAE,KAAK,IAAI,KAAK,IAAI,EAAE,KAAK,KAAK,EAAE,SAAS,EAAE,MAAM;AAChF,SAAO;AAAA,IACL;AAAA,MACE,QAAQ,UAAU;AAAA,MAClB,OAAO,UAAU;AAAA,MACjB,QAAQ,UAAU,OAAO,UAAU,QAAQ,UAAU,KAAK;AAAA,IAC5D;AAAA,IACA,GAAG;AAAA,EACL;AACF;AAUA,SAAS,gBAAgB,IAAY,OAAc,OAAa,KAAiC;AAC/F,aAAW,QAAQ,WAAW,OAAO,OAAO,GAAG,GAAG;AAChD,QAAI,aAAM,oBAAU,KAAK,MAAM,IAAI,KAAK,KAAK,MAAM,IAAI;AACrD,aAAO;AAAA,IACT;AAAA,EACF;AACA,QAAM,OAAO,MAAM;AACnB,MAAI,SAAS,UAAa,aAAM,IAAI,MAAM,IAAI;AAC5C,WAAO;AAAA,EACT;AACA,QAAM,OAAO,MAAM;AACnB,SAAO,SAAS,UAAa,aAAM,oBAAU,KAAK,MAAM,IAAI,KAAK,KAAK,MAAM,KACxE,EAAE,QAAQ,iBAAK,KAAK,MAAM,GAAG,OAAO,KAAK,MAAM,IAC/C,gBAAgB,IAAI,OAAO,GAAG;AACpC;AAqBA,SAAS,SAAS,OAAe,OAAqB,OAAa,KAA0B;AAC3F,QAAM,KAAK,aAAM,KAAK;AACtB,QAAM,YAAY,UAAU,OAAO,SAAY,gBAAgB,IAAI,OAAO,OAAO,GAAG;AACpF,QAAM,YAAY,YAAY,IAAI,GAAG;AACrC,QAAM,YAAY,aAAa,gBAAgB,IAAI,OAAO,GAAG;AAC7D,QAAM,WACJ,cAAc,SAAY,iBAAiB,YAAY,sBAAsB;AAC/E,QAAM,aAAa,cAAc,OAAO,SAAS;AACjD,SAAO;AAAA,IACL;AAAA,IACA,WAAW,CAAC;AAAA,IACZ;AAAA,IACA,WAAW,WAAW;AAAA,MACpB,CAAC,eACE,aAAa,WAAW,SAAS,IAAI,IAAI,YAC1C,aAAa,KAAK,IAAI,UAAU,KAAK;AAAA,IACzC;AAAA,IACA;AAAA,EACF;AACF;AAGA,SAAS,cAAc,MAAoB;AACzC,SAAO,KAAK,SAAS,KAAK,KAAK,UAAU;AAC3C;AAUA,SAAS,eAAe,MAAY,IAAU,WAA2B;AACvE,QAAM,UAAU,cAAc,EAAE,IAAI,cAAc,IAAI;AACtD,QAAM,OAAO,KAAK,IAAI,SAAS;AAC/B,QAAM,UAAU,KAAK,MAAM,OAAO,EAAE;AACpC,QAAM,YAAY,YAAY,IAAI,KAAK;AACvC,MAAI,YAAY,OAAO;AACvB,aAAW,SAAS,0BAA0B,OAAO,EAAE,KAAK,CAAC,GAAG;AAC9D,gBAAY,KAAK,IAAI,WAAW,KAAK,IAAI,UAAU,aAAa,UAAU,IAAI,MAAM,CAAC;AAAA,EACvF;AACA,MAAI,CAAC,OAAO,SAAS,SAAS,GAAG;AAC/B,WAAO;AAAA,EACT;AACA,SAAO,wBAAwB,KAAK,IAAI,WAAW,qBAAqB;AAC1E;AAUA,SAAS,cAAc,MAAY,WAAoB,WAA2B;AAChF,MAAI,CAAC,aAAa,cAAc,KAAK,KAAK,IAAI,SAAS,IAAI,UAAU;AACnE,WAAO;AAAA,EACT;AACA,QAAM,SAAS,YAAY;AAC3B,SAAQ,UAAU,KAAK,QAAQ,KAAO,CAAC,UAAU,KAAK,QAAQ,IAAK,oBAAoB;AACzF;AASA,SAAS,eAAe,MAAiB,QAAc,IAAe,QAAsB;AAC1F,QAAM,YAAY,GAAG,QAAQ,KAAK;AAClC,SACE,eAAe,QAAQ,QAAQ,SAAS,IACxC,cAAc,QAAQ,GAAG,WAAW,SAAS,IAC7C,cAAc,QAAQ,KAAK,WAAW,SAAS;AAEnD;AA6DO,SAAS,UACd,OACA,UACA,KACA,OAAyB,CAAC,GAClB;AACR,QAAM,QAAQ,WAAW,GAAG;AAC5B,QAAM,QAAQ,KAAK,UAAU,SAAY,SAAY,WAAW,KAAK,KAAK;AAC1E,mBAAiB,OAAO,cAAc;AAAA,IACpC,0BAA0B;AAAA,IAC1B,QAAQ,KAAK;AAAA,EACf,CAAC;AAGD;AAAA,IACE,MAAM,SAAS,sBAAsB;AAAA,IACrC;AAAA,IACA,KAAK;AAAA,EACP;AACA,MAAI,UAAU,QAAW;AACvB,kBAAc,OAAO,OAAO,WAAW;AAAA,EACzC;AACA,MAAI,MAAM,WAAW,GAAG;AACtB,WAAO,CAAC;AAAA,EACV;AACA,QAAM,QAAQ,SAAS,aAAa,KAAK,EAAE;AAC3C,QAAM,SAAS,MAAM;AAAA,IAAI,CAAC,SACxB,SAAS,KAAK,OAAO,UAAU,GAAG,KAAK,SAAS,KAAK,MAAM,OAAO,KAAK;AAAA,EACzE;AAKA,MAAI,QAAkB,CAAC;AACvB,MAAI;AACJ,QAAM,OAAmB,CAAC;AAC1B,aAAW,WAAW,QAAQ;AAC5B,QAAI,aAAa,QAAW;AAC1B,cAAQ,CAAC,GAAG,QAAQ,SAAS;AAC7B,WAAK,KAAK,QAAQ,WAAW,IAAI,MAAM,CAAC,CAAC;AACzC,iBAAW;AACX;AAAA,IACF;AACA,UAAM,UAAoB,CAAC;AAC3B,UAAM,SAAmB,CAAC;AAC1B,eAAW,CAAC,QAAQ,SAAS,KAAK,QAAQ,WAAW,QAAQ,GAAG;AAC9D,UAAI,OAAO,OAAO;AAClB,UAAI,aAAa;AACjB,iBAAW,CAAC,QAAQ,UAAU,KAAK,SAAS,WAAW,QAAQ,GAAG;AAChE,cAAM,SACH,MAAM,MAAM,KAAK,OAAO,qBACzB,eAAe,UAAU,YAAY,SAAS,SAAS;AACzD,YAAI,QAAQ,MAAM;AAChB,iBAAO;AACP,uBAAa;AAAA,QACf;AAAA,MACF;AACA,cAAQ,KAAK,QAAQ,QAAQ,UAAU,MAAM,KAAK,EAAE;AACpD,aAAO,KAAK,UAAU;AAAA,IACxB;AACA,YAAQ;AACR,SAAK,KAAK,MAAM;AAChB,eAAW;AAAA,EACb;AAEA,MAAI,SAAS;AACb,aAAW,CAAC,OAAO,IAAI,KAAK,MAAM,QAAQ,GAAG;AAC3C,QAAI,QAAQ,MAAM,MAAM,KAAK,OAAO,oBAAoB;AACtD,eAAS;AAAA,IACX;AAAA,EACF;AACA,QAAM,SAAmB,IAAI,MAAM,OAAO,MAAM,EAAE,KAAK,CAAC;AACxD,WAAS,QAAQ,OAAO,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG;AAC1D,WAAO,KAAK,IAAI;AAChB,aAAS,KAAK,KAAK,IAAI,MAAM,KAAK;AAAA,EACpC;AACA,SAAO,OAAO,IAAI,CAAC,OAAO,UAAU,MAAM,WAAW,OAAO,KAAK,KAAK,CAAC,KAAK,MAAM,SAAS;AAC7F;","names":[]}