@libraz/libcantus 0.9.4 → 0.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +27 -18
  2. package/README_ja.md +268 -0
  3. package/dist/analyze/index.cjs +7 -7
  4. package/dist/analyze/index.d.cts +9 -99
  5. package/dist/analyze/index.d.ts +9 -99
  6. package/dist/analyze/index.js +6 -6
  7. package/dist/{chunk-36N73KZ3.cjs → chunk-2A2QMA74.cjs} +80 -65
  8. package/dist/chunk-2A2QMA74.cjs.map +1 -0
  9. package/dist/{chunk-YLEMGEFH.js → chunk-2PTFGVD7.js} +50 -35
  10. package/dist/chunk-2PTFGVD7.js.map +1 -0
  11. package/dist/{chunk-ZEJR4TKL.cjs → chunk-357ZDPVU.cjs} +36 -47
  12. package/dist/chunk-357ZDPVU.cjs.map +1 -0
  13. package/dist/{chunk-EHVEBWM5.js → chunk-3KX76RLG.js} +136 -37
  14. package/dist/chunk-3KX76RLG.js.map +1 -0
  15. package/dist/{chunk-RC6GBMCD.cjs → chunk-4VZ5MZAM.cjs} +155 -111
  16. package/dist/chunk-4VZ5MZAM.cjs.map +1 -0
  17. package/dist/{chunk-RK35RIN7.js → chunk-7EONYVI5.js} +115 -28
  18. package/dist/chunk-7EONYVI5.js.map +1 -0
  19. package/dist/{chunk-GQSGS2N4.js → chunk-7JEP3MFY.js} +5 -16
  20. package/dist/chunk-7JEP3MFY.js.map +1 -0
  21. package/dist/{chunk-7NKDPF74.js → chunk-AID75RN3.js} +203 -104
  22. package/dist/chunk-AID75RN3.js.map +1 -0
  23. package/dist/{chunk-FNDWXRRI.cjs → chunk-BSLTLGM3.cjs} +55 -37
  24. package/dist/chunk-BSLTLGM3.cjs.map +1 -0
  25. package/dist/{chunk-NP4NA2FB.cjs → chunk-CU6VG2YM.cjs} +39 -36
  26. package/dist/chunk-CU6VG2YM.cjs.map +1 -0
  27. package/dist/{chunk-NYYWMV2C.js → chunk-DZMHIGI4.js} +27 -17
  28. package/dist/chunk-DZMHIGI4.js.map +1 -0
  29. package/dist/{chunk-I24MV65U.js → chunk-EDOTEABK.js} +18 -15
  30. package/dist/{chunk-I24MV65U.js.map → chunk-EDOTEABK.js.map} +1 -1
  31. package/dist/{chunk-NKIBQ6PT.js → chunk-HN2OEUN2.js} +7 -4
  32. package/dist/chunk-HN2OEUN2.js.map +1 -0
  33. package/dist/{chunk-D2FAJBYL.js → chunk-KBTTEX7Q.js} +30 -12
  34. package/dist/chunk-KBTTEX7Q.js.map +1 -0
  35. package/dist/{chunk-CAN76NCU.cjs → chunk-KQN2ZYND.cjs} +147 -102
  36. package/dist/chunk-KQN2ZYND.cjs.map +1 -0
  37. package/dist/{chunk-4HLLZPXT.cjs → chunk-MPTVJIVG.cjs} +350 -251
  38. package/dist/chunk-MPTVJIVG.cjs.map +1 -0
  39. package/dist/{chunk-RUMU3UA6.cjs → chunk-MTMZTMWM.cjs} +12 -12
  40. package/dist/{chunk-RUMU3UA6.cjs.map → chunk-MTMZTMWM.cjs.map} +1 -1
  41. package/dist/{chunk-KMHOICIY.js → chunk-NNB67XA2.js} +90 -45
  42. package/dist/chunk-NNB67XA2.js.map +1 -0
  43. package/dist/{chunk-LW53AHQU.cjs → chunk-OL3IHHSJ.cjs} +228 -141
  44. package/dist/chunk-OL3IHHSJ.cjs.map +1 -0
  45. package/dist/{chunk-7MGZ7D62.cjs → chunk-PI3UXRWK.cjs} +43 -13
  46. package/dist/chunk-PI3UXRWK.cjs.map +1 -0
  47. package/dist/{chunk-XEBG3G53.js → chunk-R2VVFAIN.js} +42 -12
  48. package/dist/chunk-R2VVFAIN.js.map +1 -0
  49. package/dist/{chunk-Q6OP4U43.cjs → chunk-WFDXOLUB.cjs} +50 -40
  50. package/dist/chunk-WFDXOLUB.cjs.map +1 -0
  51. package/dist/{chunk-NVE34YGZ.cjs → chunk-WTAN4ACG.cjs} +212 -113
  52. package/dist/chunk-WTAN4ACG.cjs.map +1 -0
  53. package/dist/{chunk-FCANXBAT.cjs → chunk-YFJXS3AU.cjs} +8 -5
  54. package/dist/chunk-YFJXS3AU.cjs.map +1 -0
  55. package/dist/{chunk-Q2XUP227.js → chunk-YUE6TJ4M.js} +89 -45
  56. package/dist/chunk-YUE6TJ4M.js.map +1 -0
  57. package/dist/{chunk-HKGYCQ34.js → chunk-ZGLJM4A4.js} +2 -2
  58. package/dist/core/index.cjs +15 -5
  59. package/dist/core/index.cjs.map +1 -1
  60. package/dist/core/index.d.cts +14 -4
  61. package/dist/core/index.d.ts +14 -4
  62. package/dist/core/index.js +14 -4
  63. package/dist/{function-DNugAOrt.d.cts → function-CxixTB02.d.cts} +98 -7
  64. package/dist/{function-BtiZb9i9.d.ts → function-DTCt675z.d.ts} +98 -7
  65. package/dist/generate/index.cjs +10 -10
  66. package/dist/generate/index.d.cts +21 -21
  67. package/dist/generate/index.d.ts +21 -21
  68. package/dist/generate/index.js +9 -9
  69. package/dist/{index-tJhkmQgE.d.cts → index-B2Ulxxpo.d.cts} +3 -2
  70. package/dist/{index-Be74mcon.d.cts → index-BkYJ0xMq.d.cts} +2 -2
  71. package/dist/{index-6VR8-kit.d.ts → index-BuaB-ypt.d.ts} +3 -2
  72. package/dist/{index-70p168VM.d.ts → index-CAN7v-kw.d.ts} +9 -2
  73. package/dist/{index-C83mZI1P.d.ts → index-COw1Ky9g.d.ts} +1 -1
  74. package/dist/{index-tSQh-xwY.d.ts → index-Cfx8xgH0.d.ts} +1 -1
  75. package/dist/{index-C5UBbQ4G.d.ts → index-DRtIhlJj.d.ts} +2 -2
  76. package/dist/{index-BtXT6UQ0.d.cts → index-alrRninU.d.cts} +9 -2
  77. package/dist/{index-y_ak9_Hi.d.cts → index-oTQbYYIs.d.cts} +1 -1
  78. package/dist/{index-OG65ocOr.d.ts → index-tR9jTn26.d.cts} +4 -1
  79. package/dist/{index-OG65ocOr.d.cts → index-tR9jTn26.d.ts} +4 -1
  80. package/dist/{index-D6b6na69.d.cts → index-x2vcsduE.d.cts} +1 -1
  81. package/dist/index.cjs +24 -14
  82. package/dist/index.cjs.map +1 -1
  83. package/dist/index.d.cts +11 -11
  84. package/dist/index.d.ts +11 -11
  85. package/dist/index.js +23 -13
  86. package/dist/model/index.cjs +7 -7
  87. package/dist/model/index.d.cts +48 -16
  88. package/dist/model/index.d.ts +48 -16
  89. package/dist/model/index.js +6 -6
  90. package/dist/{styled-DIRkk9Uz.d.ts → styled-Bl0ZK28h.d.ts} +40 -3
  91. package/dist/{styled-DJbkJQfj.d.cts → styled-Crv6SA4S.d.cts} +40 -3
  92. package/dist/theory/index.cjs +6 -6
  93. package/dist/theory/index.d.cts +9 -9
  94. package/dist/theory/index.d.ts +9 -9
  95. package/dist/theory/index.js +5 -5
  96. package/package.json +12 -9
  97. package/dist/chunk-36N73KZ3.cjs.map +0 -1
  98. package/dist/chunk-4HLLZPXT.cjs.map +0 -1
  99. package/dist/chunk-7MGZ7D62.cjs.map +0 -1
  100. package/dist/chunk-7NKDPF74.js.map +0 -1
  101. package/dist/chunk-CAN76NCU.cjs.map +0 -1
  102. package/dist/chunk-D2FAJBYL.js.map +0 -1
  103. package/dist/chunk-EHVEBWM5.js.map +0 -1
  104. package/dist/chunk-FCANXBAT.cjs.map +0 -1
  105. package/dist/chunk-FNDWXRRI.cjs.map +0 -1
  106. package/dist/chunk-GQSGS2N4.js.map +0 -1
  107. package/dist/chunk-KMHOICIY.js.map +0 -1
  108. package/dist/chunk-LW53AHQU.cjs.map +0 -1
  109. package/dist/chunk-NKIBQ6PT.js.map +0 -1
  110. package/dist/chunk-NP4NA2FB.cjs.map +0 -1
  111. package/dist/chunk-NVE34YGZ.cjs.map +0 -1
  112. package/dist/chunk-NYYWMV2C.js.map +0 -1
  113. package/dist/chunk-Q2XUP227.js.map +0 -1
  114. package/dist/chunk-Q6OP4U43.cjs.map +0 -1
  115. package/dist/chunk-RC6GBMCD.cjs.map +0 -1
  116. package/dist/chunk-RK35RIN7.js.map +0 -1
  117. package/dist/chunk-XEBG3G53.js.map +0 -1
  118. package/dist/chunk-YLEMGEFH.js.map +0 -1
  119. package/dist/chunk-ZEJR4TKL.cjs.map +0 -1
  120. /package/dist/{chunk-HKGYCQ34.js.map → chunk-ZGLJM4A4.js.map} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"sources":["/home/runner/work/libcantus/libcantus/dist/chunk-NP4NA2FB.cjs","../src/core/tuning/index.ts"],"names":[],"mappings":"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","file":"/home/runner/work/libcantus/libcantus/dist/chunk-NP4NA2FB.cjs","sourcesContent":[null,"/**\n * Tuning and microtonality: frequency conversion, cents, arbitrary equal\n * temperaments (EDO), and just-intonation ratios.\n *\n * The rest of the library reasons in twelve-tone pitch classes, which is a\n * deliberate scope choice for tonal theory. This module is the escape hatch for\n * anyone who needs actual frequencies, cents, non-12 equal temperaments, or the\n * acoustic (just) tuning behind the tempered intervals.\n */\n\nimport { InvalidInputError } from '../errors/index.js';\nimport {\n assertFiniteNumber,\n assertInteger,\n assertPositiveInt,\n assertRange,\n} from '../validation/index.js';\n\n/**\n * An equal-tempered tuning: a reference pitch and a number of equal divisions of\n * the octave. With `divisions === 12` a step index is an ordinary MIDI number.\n *\n * @category Pitch & Intervals\n */\nexport type Tuning = {\n /** Step index (MIDI number when `divisions` is 12) whose frequency is `refFreq`. */\n readonly refStep: number;\n /** Frequency in Hz of `refStep`. */\n readonly refFreq: number;\n /** Equal divisions of the octave (12 standard; e.g. 19 or 31 for microtonal). */\n readonly divisions: number;\n};\n\n/**\n * Standard twelve-tone equal temperament, A4 (MIDI 69) = 440 Hz.\n *\n * @category Pitch & Intervals\n */\nexport const TWELVE_TET: Tuning = Object.freeze({ refStep: 69, refFreq: 440, divisions: 12 });\n\nfunction assertTuning(tuning: Tuning): Tuning {\n assertFiniteNumber(tuning.refStep, 'tuning.refStep');\n assertRange(tuning.refFreq, Number.MIN_VALUE, Number.MAX_VALUE, 'tuning.refFreq');\n assertPositiveInt(tuning.divisions, 'tuning.divisions');\n return tuning;\n}\n\n/**\n * Build an equal temperament with `n` divisions of the octave.\n *\n * @param n Divisions of the octave (e.g. 19, 24, 31).\n * @param refFreq Reference frequency in Hz (default 440).\n * @param refStep Step index of the reference (default 69).\n * @returns The tuning.\n * @example\n * ```ts\n * import { edo, frequencyOf } from '@libraz/libcantus';\n * const et19 = edo(19); // { refStep: 69, refFreq: 440, divisions: 19 }\n * frequencyOf(70, et19); // one 19-EDO step above A4\n * ```\n * @category Pitch & Intervals\n */\nexport function edo(n: number, refFreq = 440, refStep = 69): Tuning {\n assertPositiveInt(n, 'EDO divisions');\n assertRange(refFreq, Number.MIN_VALUE, Number.MAX_VALUE, 'reference frequency');\n assertFiniteNumber(refStep, 'reference step');\n return { refStep, refFreq, divisions: n };\n}\n\n/**\n * Frequency in Hz of a step index under a tuning.\n *\n * @param step Step index (a MIDI number under 12-EDO).\n * @param tuning The tuning (default 12-TET).\n * @returns The frequency in Hz.\n * @example\n * ```ts\n * import { frequencyOf } from '@libraz/libcantus';\n * frequencyOf(69); // 440 (A4 in 12-TET)\n * frequencyOf(60); // middle C in Hz\n * ```\n * @category Pitch & Intervals\n */\nexport function frequencyOf(step: number, tuning: Tuning = TWELVE_TET): number {\n assertFiniteNumber(step, 'step');\n assertTuning(tuning);\n const result = tuning.refFreq * 2 ** ((step - tuning.refStep) / tuning.divisions);\n return assertRange(result, Number.MIN_VALUE, Number.MAX_VALUE, 'frequency result');\n}\n\n/**\n * Nearest step index to a frequency under a tuning (the inverse of\n * {@link frequencyOf}, rounded).\n *\n * @param freq Frequency in Hz.\n * @param tuning The tuning (default 12-TET).\n * @returns The nearest step index.\n * @category Pitch & Intervals\n */\nexport function nearestStep(freq: number, tuning: Tuning = TWELVE_TET): number {\n assertRange(freq, Number.MIN_VALUE, Number.MAX_VALUE, 'frequency');\n assertTuning(tuning);\n return Math.round(tuning.refStep + tuning.divisions * Math.log2(freq / tuning.refFreq));\n}\n\n/**\n * Step index of a frequency under a tuning, unrounded.\n *\n * The exact inverse of {@link frequencyOf}. {@link nearestStep} rounds it,\n * which discards how far off the pitch actually is — the figure a tuner\n * display, a pitch-bend amount, or an intonation check is built on.\n *\n * @param freq Frequency in Hz.\n * @param tuning The tuning (default 12-TET).\n * @returns The fractional step index.\n * @example\n * ```ts\n * import { stepOf } from '@libraz/libcantus';\n * stepOf(442); // slightly above 69, the step for A440\n * ```\n * @category Pitch & Intervals\n */\nexport function stepOf(freq: number, tuning: Tuning = TWELVE_TET): number {\n assertRange(freq, Number.MIN_VALUE, Number.MAX_VALUE, 'frequency');\n assertTuning(tuning);\n return tuning.refStep + tuning.divisions * Math.log2(freq / tuning.refFreq);\n}\n\n/**\n * How far a frequency sits from its nearest step, in cents.\n *\n * Positive means sharp of the step, negative flat.\n *\n * @param freq Frequency in Hz.\n * @param tuning The tuning (default 12-TET).\n * @returns The signed deviation in cents.\n * @example\n * ```ts\n * import { centsFromNearestStep } from '@libraz/libcantus';\n * Math.round(centsFromNearestStep(442)); // 8\n * ```\n * @category Pitch & Intervals\n */\nexport function centsFromNearestStep(freq: number, tuning: Tuning = TWELVE_TET): number {\n const exact = stepOf(freq, tuning);\n return centsOfSteps(exact - Math.round(exact), tuning);\n}\n\n/**\n * Steps spanned by an interval in cents, unrounded — the inverse of\n * {@link centsOfSteps}.\n *\n * @param cents The interval in cents.\n * @param tuning The tuning (default 12-TET).\n * @returns The fractional step count.\n * @category Pitch & Intervals\n */\nexport function stepsOfCents(cents: number, tuning: Tuning = TWELVE_TET): number {\n assertFiniteNumber(cents, 'cents');\n assertTuning(tuning);\n return assertFiniteNumber((cents * tuning.divisions) / 1200, 'steps result');\n}\n\n/**\n * The frequency ratio an interval in cents spans — the inverse of\n * {@link ratioToCents}.\n *\n * Multiply a frequency by this to move it by that many cents, which is how a\n * cents offset (a pitch-bend spec, a tuning table entry) is applied.\n *\n * @param cents The interval in cents.\n * @returns The frequency ratio.\n * @example\n * ```ts\n * import { centsToRatio } from '@libraz/libcantus';\n * 440 * centsToRatio(14); // A440 raised by 14 cents\n * ```\n * @category Pitch & Intervals\n */\nexport function centsToRatio(cents: number): number {\n assertFiniteNumber(cents, 'cents');\n return 2 ** (cents / 1200);\n}\n\n/**\n * Interval in cents between two frequencies.\n *\n * @param a Lower/first frequency in Hz.\n * @param b Upper/second frequency in Hz.\n * @returns Cents from `a` to `b` (negative if `b` is lower).\n * @category Pitch & Intervals\n */\nexport function centsBetweenFreq(a: number, b: number): number {\n assertRange(a, Number.MIN_VALUE, Number.MAX_VALUE, 'first frequency');\n assertRange(b, Number.MIN_VALUE, Number.MAX_VALUE, 'second frequency');\n return 1200 * (Math.log2(b) - Math.log2(a));\n}\n\n/**\n * Cents spanned by a number of equal-temperament steps.\n *\n * @param steps Number of steps.\n * @param tuning The tuning (default 12-TET).\n * @returns The cents.\n * @category Pitch & Intervals\n */\nexport function centsOfSteps(steps: number, tuning: Tuning = TWELVE_TET): number {\n assertFiniteNumber(steps, 'steps');\n assertTuning(tuning);\n return assertFiniteNumber((steps * 1200) / tuning.divisions, 'cents result');\n}\n\n/**\n * Cents of a frequency ratio, e.g. `ratioToCents(3, 2)` ≈ 701.955 for the just\n * perfect fifth.\n *\n * @param numerator Ratio numerator.\n * @param denominator Ratio denominator.\n * @returns The interval in cents.\n * @category Pitch & Intervals\n */\nexport function ratioToCents(numerator: number, denominator: number): number {\n assertRange(numerator, Number.MIN_VALUE, Number.MAX_VALUE, 'ratio numerator');\n assertRange(denominator, Number.MIN_VALUE, Number.MAX_VALUE, 'ratio denominator');\n return 1200 * (Math.log2(numerator) - Math.log2(denominator));\n}\n\n/**\n * Five-limit just-intonation ratios for the twelve interval classes above a\n * unison, indexed by semitone class (0..12).\n *\n * @category Pitch & Intervals\n */\nexport const JUST_RATIOS: Readonly<Record<number, readonly [number, number]>> = Object.freeze({\n 0: [1, 1],\n 1: [16, 15],\n 2: [9, 8],\n 3: [6, 5],\n 4: [5, 4],\n 5: [4, 3],\n 6: [45, 32],\n 7: [3, 2],\n 8: [8, 5],\n 9: [5, 3],\n 10: [9, 5],\n 11: [15, 8],\n 12: [2, 1],\n});\n\n/**\n * Cents by which a five-limit just interval departs from its 12-TET tempering\n * (positive means the just interval is wider).\n *\n * @param semitoneClass Semitone class in [0, 12].\n * @returns The deviation in cents.\n * @throws If `semitoneClass` is not an integer in [0, 12].\n * @category Pitch & Intervals\n */\nexport function justDeviationCents(semitoneClass: number): number {\n assertInteger(semitoneClass, 'semitone class', 0, 12);\n const ratio = JUST_RATIOS[semitoneClass];\n if (!ratio) {\n throw new InvalidInputError(`semitone class has no just ratio: ${semitoneClass}`);\n }\n return ratioToCents(ratio[0], ratio[1]) - semitoneClass * 100;\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["/home/runner/work/libcantus/libcantus/dist/chunk-NVE34YGZ.cjs","../src/model/interval.ts","../src/model/note.ts","../src/model/shared.ts","../src/model/key.ts","../src/model/progression.ts","../src/model/chord.ts"],"names":[],"mappings":"AAAA;AACE;AACF,wDAA6B;AAC7B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,wDAA6B;AAC7B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,wDAA6B;AAC7B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,wDAA6B;AAC7B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,wDAA6B;AAC7B;AACA;ACtDA,IAAM,cAAA,EAAgB,CAAA;AAGtB,SAAS,aAAA,CAAc,OAAA,EAAqD;AAC1E,EAAA,GAAA,CAAI,QAAA,IAAY,GAAA,EAAK;AACnB,IAAA,OAAO,GAAA;AAAA,EACT;AACA,EAAA,GAAA,CAAI,QAAA,IAAY,GAAA,EAAK;AACnB,IAAA,OAAO,GAAA;AAAA,EACT;AACA,EAAA,GAAA,CAAI,QAAA,IAAY,GAAA,EAAK;AACnB,IAAA,OAAO,GAAA;AAAA,EACT;AACA,EAAA,OAAO,OAAA,CAAQ,UAAA,CAAW,GAAG,EAAA,EACxB,CAAA,CAAA,EAAI,GAAA,CAAI,MAAA,CAAO,OAAA,CAAQ,OAAA,EAAS,CAAC,CAAC,CAAA,EAAA;AAEzC;AAcsB;AACX,EAAA;AACA,EAAA;AACA,EAAA;AAEkF,EAAA;AAC1E,IAAA;AACC,IAAA;AACE,IAAA;AACpB,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAS2C,EAAA;AACpB,IAAA;AACvB,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAa2F,EAAA;AAClC,IAAA;AACjB,IAAA;AAC1B,MAAA;AACkC,QAAA;AAC5C,MAAA;AACF,IAAA;AACmD,IAAA;AACrD,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQiD,EAAA;AACc,IAAA;AAC/D,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQiD,EAAA;AAClB,IAAA;AAC/B,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASqC,EAAA;AACS,IAAA;AAC9C,EAAA;AAAA;AAGqB,EAAA;AACP,IAAA;AACd,EAAA;AAAA;AAGoC,EAAA;AACtB,IAAA;AACd,EAAA;AAAA;AAGwB,EAAA;AACV,IAAA;AACd,EAAA;AAAA;AAGmB,EAAA;AACqB,IAAA;AACxC,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWmB,EAAA;AACyB,IAAA;AACN,IAAA;AACO,IAAA;AACgB,IAAA;AAC7D,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASsC,EAAA;AACgB,IAAA;AACtD,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWiC,EAAA;AAGL,IAAA;AAG5B,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAU0B,EAAA;AAC0C,IAAA;AACpE,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQmB,EAAA;AACL,IAAA;AACd,EAAA;AACF;ADoBsE;AACA;AEpM1B;AACoB,EAAA;AAClD,IAAA;AACmD,MAAA;AAC7D,IAAA;AACF,EAAA;AACgE,EAAA;AACjC,EAAA;AACV,IAAA;AACrB,EAAA;AACO,EAAA;AACT;AAckB;AACP,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQmB,EAAA;AACA,IAAA;AAC5B,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAS8B,EAAA;AACG,IAAA;AACjC,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAS0E,EAAA;AAC9B,IAAA;AAC5C,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASsC,EAAA;AAChB,IAAA;AACtB,EAAA;AAAA;AAGmB,EAAA;AACW,IAAA;AAC9B,EAAA;AAAA;AAGyB,EAAA;AACW,IAAA;AACpC,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOmB,EAAA;AACoB,IAAA;AACU,MAAA;AAC/C,IAAA;AAC4B,IAAA;AAC9B,EAAA;AAAA;AAGqB,EAAA;AACD,IAAA;AACpB,EAAA;AAAA;AAGoB,EAAA;AACA,IAAA;AACpB,EAAA;AAAA;AAGiC,EAAA;AACb,IAAA;AACpB,EAAA;AAAA;AAGqB,EAAA;AACO,IAAA;AAC5B,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAsB2E,EAAA;AACpB,IAAA;AACzB,MAAA;AAC5B,IAAA;AAC0D,IAAA;AAC5D,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQkC,EAAA;AAKgC,IAAA;AAClE,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiBsC,EAAA;AAC6B,IAAA;AACnE,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASsC,EAAA;AAChB,IAAA;AACtB,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQ6B,EAAA;AACX,IAAA;AAGS,IAAA;AAG3B,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUmB,EAAA;AACL,IAAA;AACd,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOmB,EAAA;AACL,IAAA;AACd,EAAA;AACF;AF+JsE;AACA;AG3YgB;AACjC,EAAA;AACG,EAAA;AACxD;AH6YsE;AACA;AI5XJ;AACC,EAAA;AACnE;AAOkE;AACf,EAAA;AACF,EAAA;AACiB,EAAA;AACzC,IAAA;AACvB,EAAA;AACiE,EAAA;AAGnE;AAciB;AACN,EAAA;AACA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQiC,EAAA;AACP,IAAA;AACA,IAAA;AACrB,MAAA;AACW,QAAA;AAErB,MAAA;AACF,IAAA;AACqD,IAAA;AACvC,IAAA;AAChB,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUyC,EAAA;AACT,IAAA;AACF,MAAA;AACsB,MAAA;AAClD,IAAA;AAC2B,IAAA;AACyB,IAAA;AACtD,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUyC,EAAA;AACT,IAAA;AACF,MAAA;AACsB,MAAA;AAClD,IAAA;AAC2B,IAAA;AACyB,IAAA;AACtD,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAYuD,EAAA;AACvB,IAAA;AACF,MAAA;AAC+B,MAAA;AAC3D,IAAA;AAC2C,IAAA;AACe,IAAA;AAC5D,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgB8C,EAAA;AACW,IAAA;AACzD,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQiE,EAAA;AAChB,IAAA;AACjD,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAW0E,EAAA;AACR,IAAA;AAClE,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAcmF,EAAA;AACzC,IAAA;AACK,IAAA;AAC/C,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAW4B,EAAA;AACN,IAAA;AACsC,IAAA;AAC5D,EAAA;AAAA;AAGsB,EAAA;AACyC,IAAA;AAC/D,EAAA;AAAA;AAGkB,EAAA;AACJ,IAAA;AACd,EAAA;AAAA;AAGqB,EAAA;AACA,IAAA;AACrB,EAAA;AAAA;AAGuB,EAAA;AACQ,IAAA;AAC/B,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOyB,EAAA;AACmB,IAAA;AAC5C,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiBkC,EAAA;AACS,IAAA;AACY,IAAA;AACO,IAAA;AAC9D,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOgB,EAAA;AACiD,IAAA;AACjE,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOgB,EAAA;AACI,IAAA;AACpB,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOsB,EAAA;AACuB,IAAA;AAC7C,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiBqD,EAAA;AAGlB,IAAA;AAEN,IAAA;AAC7B,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUqC,EAAA;AACsB,IAAA;AAC3D,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUuC,EAAA;AACsB,IAAA;AAC7D,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAU2B,EAAA;AAC6B,IAAA;AACxD,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQoC,EAAA;AACgC,IAAA;AACpE,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAY+C,EAAA;AACO,IAAA;AACtD,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASmB,EAAA;AAC6C,IAAA;AAChE,EAAA;AACF;AJ4UsE;AACA;AKtqB7C;AACd,EAAA;AACA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQwC,EAAA;AACP,IAAA;AAC5B,IAAA;AACd,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqBsE,EAAA;AACjC,IAAA;AACkC,MAAA;AACf,MAAA;AACrD,IAAA;AACwD,IAAA;AAC3D,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWgB,EAAA;AAC6D,IAAA;AACP,IAAA;AACX,IAAA;AAC3D,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQ+B,EAAA;AACjB,IAAA;AACd,EAAA;AAAA;AAG2B,EAAA;AACb,IAAA;AACd,EAAA;AAAA;AAGqB,EAAA;AACC,IAAA;AACtB,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQqC,EAAA;AACP,IAAA;AAC9B,EAAA;AAAA;AAG6C,EAAA;AACN,IAAA;AACvC,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWoC,EAAA;AACQ,IAAA;AACjC,MAAA;AACT,IAAA;AAC4C,IAAA;AACb,MAAA;AACqB,MAAA;AACnD,IAAA;AACH,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQ+B,EAAA;AAC6B,IAAA;AAC5D,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQ+B,EAAA;AACW,IAAA;AAC1C,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASyC,EAAA;AAChC,IAAA;AACiC,MAAA;AACtC,MAAA;AACF,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAS2B,EAAA;AACY,IAAA;AACmB,IAAA;AAC1D,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASyC,EAAA;AACF,IAAA;AACsB,IAAA;AAC7D,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAYkE,EAAA;AAC3B,IAAA;AAC6B,IAAA;AACjB,IAAA;AACF,IAAA;AAG3B,IAAA;AAEK,IAAA;AAC3B,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAyB0C,EAAA;AACE,IAAA;AACC,IAAA;AACF,MAAA;AAI6B,MAAA;AACrE,IAAA;AACwD,IAAA;AAC3D,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQmB,EAAA;AAC0C,IAAA;AAC7D,EAAA;AAEyF,EAAA;AAChF,IAAA;AACyC,MAAA;AACvB,MAAA;AACzB,IAAA;AACF,EAAA;AAAA;AAG4B,EAAA;AACG,IAAA;AACD,IAAA;AAChB,MAAA;AACR,QAAA;AACF,MAAA;AACF,IAAA;AACO,IAAA;AACT,EAAA;AACF;AL4oBsE;AACA;AM33BiB;AAC9B,EAAA;AACzD;AAW+C;AACrB,EAAA;AACT,IAAA;AACC,IAAA;AACe,IAAA;AAC/B,EAAA;AAC+B,EAAA;AACV,IAAA;AACrB,EAAA;AACwD,EAAA;AACS,IAAA;AACjE,EAAA;AACwD,EAAA;AACS,IAAA;AACjE,EAAA;AACO,EAAA;AACT;AAS+F;AACxE,EAAA;AACK,EAAA;AACjB,IAAA;AACT,EAAA;AACmB,EAAA;AACK,EAAA;AACqC,IAAA;AACI,IAAA;AACvC,IAAA;AAC0B,MAAA;AAClD,IAAA;AACF,EAAA;AACuB,EAAA;AACd,IAAA;AACT,EAAA;AACiE,EAAA;AACX,EAAA;AACxD;AAemB;AAAA;AAER,EAAA;AACA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQ+B,EAAA;AACV,IAAA;AAChB,IAAA;AACd,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASuB,EAAA;AACY,IAAA;AAChB,IAAA;AACwC,IAAA;AACQ,MAAA;AACjE,IAAA;AACoC,IAAA;AACM,MAAA;AAChB,MAAA;AACH,QAAA;AACrB,MAAA;AACF,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWuF,EAAA;AACzB,IAAA;AACG,IAAA;AACH,IAAA;AAC/C,IAAA;AACqC,MAAA;AAChD,MAAA;AACA,MAAA;AACF,IAAA;AAC4B,IAAA;AACqC,MAAA;AACjE,IAAA;AAC4B,IAAA;AACqC,MAAA;AACjE,IAAA;AACqB,IAAA;AACvB,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQoC,EAAA;AACb,IAAA;AACvB,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASwC,EAAA;AACjB,IAAA;AACvB,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWwC,EAAA;AACjB,IAAA;AACvB,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASoC,EAAA;AACO,IAAA;AAC3C,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAS8E,EAAA;AAC1C,IAAA;AAC4B,MAAA;AAC9D,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuByC,EAAA;AACW,IAAA;AACc,MAAA;AAC9D,MAAA;AACA,IAAA;AACJ,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASuF,EAAA;AAC3C,IAAA;AACE,IAAA;AAC9C,EAAA;AAAA;AAGqB,EAAA;AACD,IAAA;AACpB,EAAA;AAAA;AAG4B,EAAA;AACR,IAAA;AACpB,EAAA;AAAA;AAG0B,EAAA;AACO,IAAA;AACjC,EAAA;AAAA;AAGiC,EAAA;AACb,IAAA;AACpB,EAAA;AAAA;AAAA;AAAA;AAAA;AAMsB,EAAA;AACR,IAAA;AACd,EAAA;AAAA;AAG2B,EAAA;AACb,IAAA;AACd,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAcyB,EAAA;AACU,IAAA;AACnC,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOyB,EAAA;AACY,IAAA;AACrC,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASyB,EAAA;AACoC,IAAA;AAC7D,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASsC,EAAA;AACqB,IAAA;AAC3D,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASkC,EAAA;AAC2B,IAAA;AAC7D,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAS+B,EAAA;AACiC,IAAA;AAChE,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAS0C,EAAA;AACqB,IAAA;AAC/D,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQ2C,EAAA;AACA,IAAA;AAC3C,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAYuC,EAAA;AACD,IAAA;AACgC,IAAA;AACtE,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASqD,EAAA;AACX,IAAA;AAC1C,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWkC,EAAA;AACK,IAAA;AAG4B,IAAA;AACnE,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAcyB,EAAA;AACW,IAAA;AACX,IAAA;AACE,IAAA;AACP,IAAA;AACY,MAAA;AAC9B,IAAA;AACwC,IAAA;AAG5B,IAAA;AACK,IAAA;AACH,MAAA;AACP,IAAA;AACoD,MAAA;AAC3D,IAAA;AACgC,IAAA;AAClC,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkBoC,EAAA;AACiB,IAAA;AACI,IAAA;AACzD,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQ4B,EAAA;AACG,IAAA;AAC/B,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAU8C,EAAA;AACE,IAAA;AAChD,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUgD,EAAA;AACP,IAAA;AACzC,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqByB,EAAA;AACL,IAAA;AACW,IAAA;AACzB,IAAA;AACwB,IAAA;AACR,MAAA;AACM,MAAA;AACZ,QAAA;AACR,UAAA;AACF,QAAA;AACF,MAAA;AACqC,MAAA;AAChC,IAAA;AACuD,MAAA;AAC9D,IAAA;AACoB,IAAA;AACF,IAAA;AAIM,IAAA;AAEuC,MAAA;AAC/D,IAAA;AACyC,IAAA;AAC3C,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAS+C,EAAA;AACM,IAAA;AACrD,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAS8B,EAAA;AACb,IAAA;AACC,IAAA;AAIZ,IAAA;AAIN,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOoB,EAAA;AACN,IAAA;AACd,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOmB,EAAA;AACE,IAAA;AACrB,EAAA;AAAA;AAG4B,EAAA;AACG,IAAA;AACD,IAAA;AAChB,MAAA;AACR,QAAA;AACF,MAAA;AACF,IAAA;AACO,IAAA;AACT,EAAA;AACF;ANsyBsE;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/libcantus/libcantus/dist/chunk-NVE34YGZ.cjs","sourcesContent":[null,"import { InvalidInputError } from '../core/errors/index.js';\nimport { isConsonantInterval } from '../core/interval/index.js';\nimport type { IntervalQualityLabel, SpelledInterval } from '../core/pitch/index.js';\nimport { intervalSemitones, parseInterval } from '../core/pitch/index.js';\nimport type { Note } from './note.js';\n\n/** Number of diatonic degrees an interval and its inversion span together. */\nconst INVERSION_SUM = 9;\n\n/** The quality an interval's inversion carries. */\nfunction invertQuality(quality: IntervalQualityLabel): IntervalQualityLabel {\n if (quality === 'P') {\n return 'P';\n }\n if (quality === 'M') {\n return 'm';\n }\n if (quality === 'm') {\n return 'M';\n }\n return quality.startsWith('A')\n ? (`d${'d'.repeat(quality.length - 1)}` as IntervalQualityLabel)\n : (`A${'A'.repeat(quality.length - 1)}` as IntervalQualityLabel);\n}\n\n/**\n * An immutable spelled interval value: a diatonic number, a quality label, and\n * a signed semitone span. A thin convenience wrapper over the pitch module's\n * plain interval result.\n *\n * @category Class API\n * @example\n * ```ts\n * import { Interval, Note } from '@libraz/libcantus';\n * Interval.between(Note.of('C4'), Note.of('G4')).name; // 'P5'\n * ```\n */\nexport class Interval {\n readonly #number: number;\n readonly #quality: IntervalQualityLabel;\n readonly #semitones: number;\n\n private constructor(numberValue: number, quality: IntervalQualityLabel, semitones: number) {\n this.#number = numberValue;\n this.#quality = quality;\n this.#semitones = semitones;\n }\n\n /**\n * The spelled interval between two notes.\n *\n * @param a The first note.\n * @param b The second note.\n * @returns The interval from `a` to `b`.\n */\n static between(a: Note, b: Note): Interval {\n return a.intervalTo(b);\n }\n\n /**\n * Build an interval from explicit components.\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 * @param semitones Signed semitone span; its magnitude must be the span the\n * number and quality describe.\n * @returns The interval.\n * @throws If the three components do not describe the same interval — a\n * `P5` spanning 8 semitones is not a value any other method can produce.\n */\n static of(numberValue: number, quality: IntervalQualityLabel, semitones: number): Interval {\n const expected = intervalSemitones(numberValue, quality);\n if (Math.abs(semitones) !== expected) {\n throw new InvalidInputError(\n `${quality}${numberValue} spans ${expected} semitones; received ${semitones}`,\n );\n }\n return new Interval(numberValue, quality, semitones);\n }\n\n /**\n * Wrap a plain spelled interval, as returned by the pitch module.\n *\n * @param data The plain interval.\n * @returns The wrapped interval.\n */\n static fromData(data: SpelledInterval): Interval {\n return new Interval(data.number, data.quality, data.semitones);\n }\n\n /**\n * Rebuild an interval from its {@link Interval.toJSON} output.\n *\n * @param data The serialized interval.\n * @returns The wrapped interval.\n */\n static fromJSON(data: SpelledInterval): Interval {\n return Interval.fromData(data);\n }\n\n /**\n * Parse an interval name such as `'P5'`, `'m3'`, or `'AA4'`.\n *\n * @param name The interval name.\n * @returns The ascending interval of that name.\n * @throws If the name is not a quality label followed by a diatonic number.\n */\n static parse(name: string): Interval {\n return Interval.fromData(parseInterval(name));\n }\n\n /** Diatonic size: 1 = unison, 2 = second, ... 8 = octave, and beyond. */\n get number(): number {\n return this.#number;\n }\n\n /** Quality label: `'P'`, `'M'`, `'m'`, or repeated `'A'`/`'d'`. */\n get quality(): IntervalQualityLabel {\n return this.#quality;\n }\n\n /** Signed semitone span. */\n get semitones(): number {\n return this.#semitones;\n }\n\n /** A readable label composed of quality and number, e.g. `'P5'` or `'M3'`. */\n get name(): string {\n return `${this.#quality}${this.#number}`;\n }\n\n /**\n * The interval's inversion: the complement that completes the octave.\n *\n * A compound interval is reduced to its simple form first, and the result is\n * always ascending — an inversion answers \"what is left of the octave\",\n * which has no direction of its own.\n *\n * @returns The inverted interval, e.g. `M3` becomes `m6`.\n */\n invert(): Interval {\n const simple = ((this.#number - 1) % 7) + 1;\n const numberValue = INVERSION_SUM - simple;\n const quality = invertQuality(this.#quality);\n return Interval.of(numberValue, quality, intervalSemitones(numberValue, quality));\n }\n\n /**\n * Whether the interval is consonant.\n *\n * @param twoVoice When true, the perfect fourth counts as a dissonance,\n * matching two-voice counterpoint.\n * @returns True if the interval is consonant in that context.\n */\n isConsonant(twoVoice = true): boolean {\n return isConsonantInterval(this.#semitones, twoVoice);\n }\n\n /**\n * Whether another interval is spelled identically.\n *\n * Enharmonic equivalents are not equal: an augmented second and a minor\n * third span the same distance but are different intervals.\n *\n * @param other The interval to compare with.\n * @returns True when number, quality, and span all match.\n */\n equals(other: Interval): boolean {\n return (\n this.#number === other.number &&\n this.#quality === other.quality &&\n this.#semitones === other.semitones\n );\n }\n\n /**\n * The plain interval data, for JSON serialization.\n *\n * Private class fields do not serialize, so an explicit `toJSON` keeps\n * `JSON.stringify(interval)` from collapsing to `{}`.\n *\n * @returns The diatonic number, quality, and semitone span.\n */\n toJSON(): SpelledInterval {\n return { number: this.#number, quality: this.#quality, semitones: this.#semitones };\n }\n\n /**\n * The interval's name, so a template literal or a log line reads as the\n * interval.\n *\n * @returns The name, e.g. `'M3'`.\n */\n toString(): string {\n return this.name;\n }\n}\n","import { InvalidInputError } from '../core/errors/index.js';\nimport {\n formatNote,\n midiToNote,\n type Note as NoteData,\n noteToMidi,\n noteToPitchClass,\n parseNote,\n spelledInterval,\n transposeByInterval,\n transposeNote,\n} from '../core/pitch/index.js';\nimport { Interval } from './interval.js';\n\n/**\n * Defensive copy of a plain note, with the letter checked.\n *\n * Formatting and pitch-class arithmetic reduce the letter modulo 7, but\n * `equals` compares it directly: an unreduced letter produces two notes that\n * print the same name and report the same pitch class yet compare unequal.\n */\nfunction copyNote(data: NoteData): NoteData {\n if (!Number.isInteger(data.letter) || data.letter < 0 || data.letter > 6) {\n throw new InvalidInputError(\n `note letter must be an integer in [0, 6]; received ${data.letter}`,\n );\n }\n const copy: NoteData = { letter: data.letter, alter: data.alter };\n if (data.octave !== undefined) {\n copy.octave = data.octave;\n }\n return copy;\n}\n\n/**\n * An immutable spelled note: a diatonic letter plus a chromatic alteration and\n * an optional octave. Wraps the plain note object and delegates to the pitch\n * module; every transformation returns a new instance.\n *\n * @category Class API\n * @example\n * ```ts\n * import { Note } from '@libraz/libcantus';\n * Note.of('C4').transpose(7).name; // 'G4'\n * ```\n */\nexport class Note {\n readonly #data: NoteData;\n\n /**\n * Wrap a plain note object.\n *\n * @param data The spelled note; it is copied, never retained or mutated.\n * @throws If `letter` is not an integer in 0..6.\n */\n constructor(data: NoteData) {\n this.#data = copyNote(data);\n }\n\n /**\n * Parse scientific pitch notation (e.g. `'C#4'`, `'Bb'`, `'F##3'`).\n *\n * @param name The note text.\n * @returns The parsed note.\n * @throws If the text is not a valid note.\n */\n static of(name: string): Note {\n return new Note(parseNote(name));\n }\n\n /**\n * Name a MIDI number (middle C = C4 = 60) as a note.\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 */\n static fromMidi(midi: number, spelling: 'sharp' | 'flat' = 'sharp'): Note {\n return new Note(midiToNote(midi, spelling));\n }\n\n /**\n * Wrap an existing plain note object.\n *\n * @param data The plain note.\n * @returns The wrapped note.\n * @throws If `letter` is not an integer in 0..6.\n */\n static fromData(data: NoteData): Note {\n return new Note(data);\n }\n\n /** The note rendered as scientific pitch notation, e.g. `'G4'` or `'Bb'`. */\n get name(): string {\n return formatNote(this.#data);\n }\n\n /** The pitch class (0..11), ignoring octave. */\n get pitchClass(): number {\n return noteToPitchClass(this.#data);\n }\n\n /**\n * The MIDI number (middle C = C4 = 60).\n *\n * @throws If the note has no octave and therefore no fixed pitch.\n */\n get midi(): number {\n if (this.#data.octave === undefined) {\n throw new InvalidInputError(`note ${this.name} has no octave, so it has no MIDI number`);\n }\n return noteToMidi(this.#data);\n }\n\n /** The diatonic letter number: 0..6 for C..B. */\n get letter(): number {\n return this.#data.letter;\n }\n\n /** The chromatic alteration in semitones: -1 flat, 0 natural, +1 sharp, ... */\n get alter(): number {\n return this.#data.alter;\n }\n\n /** The octave (scientific pitch notation), or undefined for a bare pitch class. */\n get octave(): number | undefined {\n return this.#data.octave;\n }\n\n /** A copy of the underlying plain note object. */\n get data(): NoteData {\n return copyNote(this.#data);\n }\n\n /**\n * Transpose by a signed number of semitones, keeping the spelling.\n *\n * The letter moves by the diatonic distance of the conventional interval for\n * that many semitones, so `Ab4` up a major second is `Bb4` rather than `A#4`\n * and a flat key stays on the flat side. An octave-less note stays\n * octave-less: only its pitch class is moved. Transposing by zero is the\n * identity: the original spelling is preserved (no enharmonic respelling).\n *\n * @param semitones The signed semitone offset.\n * @param opts `spelling` forces the result onto the sharp or flat side\n * instead of following this note's letter.\n * @returns The transposed note.\n * @example\n * ```ts\n * import { Note } from '@libraz/libcantus';\n * Note.of('Ab4').transpose(2).name; // 'Bb4'\n * Note.of('Ab4').transpose(2, { spelling: 'sharp' }).name; // 'A#4'\n * ```\n */\n transpose(semitones: number, opts?: { spelling?: 'sharp' | 'flat' }): Note {\n if (semitones === 0 && opts?.spelling === undefined) {\n return new Note(this.#data);\n }\n return new Note(transposeNote(this.#data, semitones, opts));\n }\n\n /**\n * The spelled interval from this note to another.\n *\n * @param other The second note.\n * @returns The interval, which {@link Interval.toJSON} unwraps to plain data.\n */\n intervalTo(other: Note): Interval {\n // Read the other note through its public accessor rather than its private\n // field: a bundler that emits two copies of this class — as a CommonJS\n // build without shared chunks does for the root and /model entries — would\n // otherwise throw on the brand check.\n return Interval.fromData(spelledInterval(this.#data, other.data));\n }\n\n /**\n * Transpose by a spelled interval, keeping the spelling the interval names.\n *\n * Unlike {@link Note.transpose}, which picks a letter from the semitone\n * count, the interval's diatonic number decides the letter: C up an\n * augmented second is D#, not Eb.\n *\n * @param interval The interval to apply; a descending interval moves down.\n * @returns The transposed note.\n * @example\n * ```ts\n * import { Interval, Note } from '@libraz/libcantus';\n * Note.of('C4').transposeBy(Interval.parse('A2')).name; // 'D#4'\n * ```\n */\n transposeBy(interval: Interval): Note {\n return new Note(transposeByInterval(this.#data, interval.toJSON()));\n }\n\n /**\n * Rebuild a note from its {@link Note.toJSON} output.\n *\n * @param data The serialized note.\n * @returns The wrapped note.\n * @throws If `letter` is not an integer in 0..6.\n */\n static fromJSON(data: NoteData): Note {\n return new Note(data);\n }\n\n /**\n * Whether another note has the same letter, alteration, and octave.\n *\n * @param other The note to compare.\n * @returns True if the spellings are identical.\n */\n equals(other: Note): boolean {\n const b = other.data;\n return (\n this.#data.letter === b.letter &&\n this.#data.alter === b.alter &&\n this.#data.octave === b.octave\n );\n }\n\n /**\n * The plain note data, for JSON serialization.\n *\n * Private class fields do not serialize, so an explicit `toJSON` keeps\n * `JSON.stringify(note)` from collapsing to `{}`.\n *\n * @returns A copy of the underlying plain note object.\n */\n toJSON(): NoteData {\n return this.data;\n }\n\n /**\n * The note's name, so a template literal or a log line reads as the note.\n *\n * @returns The spelled name, e.g. `'Bb3'`.\n */\n toString(): string {\n return this.name;\n }\n}\n","import { midiToNote, pitchClassOf as mod12, type Note as NoteData } from '../core/pitch/index.js';\n\nexport { pitchClassOf as mod12 } from '../core/pitch/index.js';\n\n/** Spell a bare pitch class as an octave-less note with a sharp/flat preference. */\nexport function spellPitchClassBare(pc: number, spelling: 'sharp' | 'flat'): NoteData {\n const spelled = midiToNote(60 + mod12(pc), spelling);\n return { letter: spelled.letter, alter: spelled.alter };\n}\n","import type { DetectKeyOptions } from '../analyze/detect/index.js';\nimport { detectKey, detectKeyBest } from '../analyze/detect/index.js';\nimport { isMinorKey, romanToChord } from '../analyze/functional/index.js';\nimport { InvalidInputError } from '../core/errors/index.js';\nimport type { Note as NoteData } from '../core/pitch/index.js';\nimport type { KeyScale } from '../core/types.js';\nimport { assertFiniteNumber } from '../core/validation/index.js';\nimport {\n type ChordQuality,\n chordFromDegree,\n diatonicSeventh,\n diatonicTriad,\n} from '../theory/chord/index.js';\nimport {\n isScaleTone,\n majorKey,\n minorKey,\n scaleByName,\n scaleTonesInDegreeOrder,\n} from '../theory/scale/index.js';\nimport { spellScale } from '../theory/spelling/index.js';\nimport { Chord } from './chord.js';\nimport { Note } from './note.js';\nimport { mod12, spellPitchClassBare } from './shared.js';\n\n/** Total accidentals a spelled tonic produces across a key's whole scale. */\nfunction accidentalLoad(tonic: NoteData, scale: KeyScale): number {\n return spellScale(tonic, scale).reduce((sum, note) => sum + Math.abs(note.alter), 0);\n}\n\n/**\n * Choose the tonic spelling (sharp- or flat-side) that spells `scale` with the\n * fewest accidentals, so a numeric root never yields a double-flat/double-sharp\n * scale (e.g. pitch class 6 minor spells as F# minor, not Gb minor with Bbb).\n */\nfunction bestTonicForScale(rootPc: number, scale: KeyScale): Note {\n const sharp = spellPitchClassBare(rootPc, 'sharp');\n const flat = spellPitchClassBare(rootPc, 'flat');\n if (sharp.letter === flat.letter && sharp.alter === flat.alter) {\n return new Note(sharp);\n }\n return accidentalLoad(flat, scale) <= accidentalLoad(sharp, scale)\n ? new Note(flat)\n : new Note(sharp);\n}\n\n/**\n * An immutable key/scale: a `KeyScale` (root pitch class plus mode mask) paired\n * with a spelled tonic that anchors letter-name spelling. Acts as the factory\n * for key-aware chords.\n *\n * @category Class API\n * @example\n * ```ts\n * import { Key } from '@libraz/libcantus';\n * Key.major('C').chord(4).symbol(); // 'G' (the diatonic triad on scale degree 4)\n * ```\n */\nexport class Key {\n readonly #scale: KeyScale;\n readonly #tonic: Note;\n\n /**\n * Wrap a key/scale and its spelled tonic.\n *\n * @param scale The key/scale; its root is normalized to a pitch class.\n * @param tonic The spelled tonic anchoring letter-name spelling.\n */\n constructor(scale: KeyScale, tonic: Note) {\n const rootPc = mod12(scale.rootPc);\n if (tonic.pitchClass !== rootPc) {\n throw new InvalidInputError(\n `tonic ${tonic.name} does not match the scale root pitch class ${rootPc}; ` +\n 'pass a tonic that spells the scale root, or omit it to have one chosen',\n );\n }\n this.#scale = { rootPc, modeMask12: scale.modeMask12 };\n this.#tonic = tonic;\n }\n\n /**\n * A major key.\n *\n * @param root Tonic as a note name (e.g. `'Eb'`) or a pitch class; a numeric\n * root is spelled with whichever accidental side yields the fewest\n * accidentals across the scale.\n * @returns The major key.\n */\n static major(root: string | number): Key {\n if (typeof root === 'string') {\n const tonic = Note.of(root);\n return new Key(majorKey(tonic.pitchClass), tonic);\n }\n const scale = majorKey(root);\n return new Key(scale, bestTonicForScale(root, scale));\n }\n\n /**\n * A natural-minor key.\n *\n * @param root Tonic as a note name or a pitch class; a numeric root is\n * spelled with whichever accidental side yields the fewest accidentals\n * across the scale.\n * @returns The minor key.\n */\n static minor(root: string | number): Key {\n if (typeof root === 'string') {\n const tonic = Note.of(root);\n return new Key(minorKey(tonic.pitchClass), tonic);\n }\n const scale = minorKey(root);\n return new Key(scale, bestTonicForScale(root, scale));\n }\n\n /**\n * A key on a named scale (e.g. `'dorian'`, `'harmonicMinor'`).\n *\n * @param name The scale name, a key of the scale module's named-scale table.\n * @param root Tonic as a note name or a pitch class; a numeric root is\n * spelled with whichever accidental side yields the fewest accidentals\n * across the scale, exactly as {@link Key.major} does.\n * @returns The key.\n * @throws If the name is not a known scale.\n */\n static named(name: string, root: string | number): Key {\n if (typeof root === 'string') {\n const tonic = Note.of(root);\n return new Key(scaleByName(name, tonic.pitchClass), tonic);\n }\n const scale = scaleByName(name, mod12(root));\n return new Key(scale, bestTonicForScale(mod12(root), scale));\n }\n\n /**\n * Wrap an existing `KeyScale`, synthesizing a spelled tonic when none is\n * given.\n *\n * The synthesized tonic is chosen the same way as for a numeric root\n * elsewhere: whichever accidental side spells the scale with the fewest\n * accidentals. This is what keeps `Key.of(detectKey(...).scale)` from handing\n * every downstream chord a double-sharp spelling.\n *\n * @param scale The key/scale to wrap.\n * @param tonic Optional spelled tonic; must spell the scale's root pitch class.\n * @returns The key.\n * @throws If the given tonic is not the scale's root pitch class.\n */\n static of(scale: KeyScale, tonic?: Note): Key {\n return new Key(scale, tonic ?? bestTonicForScale(mod12(scale.rootPc), scale));\n }\n\n /**\n * Rebuild a key from its {@link Key.toJSON} output.\n *\n * @param data The serialized key and tonic.\n * @returns The key.\n */\n static fromJSON(data: { scale: KeyScale; tonic: NoteData }): Key {\n return new Key(data.scale, new Note(data.tonic));\n }\n\n /**\n * Identify the keys a set of pitches fits, best interpretation first.\n *\n * The counterpart of {@link Chord.detect}.\n *\n * @param pitches MIDI pitches or bare pitch classes.\n * @param opts How to weigh the input; see {@link DetectKeyOptions}.\n * @returns Ranked keys (may be empty).\n */\n static detect(pitches: readonly number[], opts?: DetectKeyOptions): Key[] {\n return detectKey(pitches, opts).map((match) => Key.of(match.key));\n }\n\n /**\n * The single best key interpretation of a pitch set.\n *\n * @param pitches MIDI pitches or bare pitch classes.\n * @param opts How to weigh the input; see {@link DetectKeyOptions}.\n * @returns The top-ranked key, or null when nothing matches.\n * @example\n * ```ts\n * import { Key } from '@libraz/libcantus';\n * Key.detectBest([0, 2, 4, 5, 7, 9, 11])?.toString(); // 'C major'\n * ```\n */\n static detectBest(pitches: readonly number[], opts?: DetectKeyOptions): Key | null {\n const best = detectKeyBest(pitches, opts);\n return best === null ? null : Key.of(best.key);\n }\n\n /**\n * Whether another key has the same tonic pitch class and mode.\n *\n * The spelled tonic is not compared: C# major and Db major are the same key\n * written two ways.\n *\n * @param other The key to compare.\n * @returns True when tonic and mode mask both match.\n */\n equals(other: Key): boolean {\n const scale = other.scale;\n return this.#scale.rootPc === scale.rootPc && this.#scale.modeMask12 === scale.modeMask12;\n }\n\n /** A copy of the underlying plain `KeyScale`. */\n get scale(): KeyScale {\n return { rootPc: this.#scale.rootPc, modeMask12: this.#scale.modeMask12 };\n }\n\n /** The spelled tonic. */\n get tonic(): Note {\n return this.#tonic;\n }\n\n /** The tonic pitch class (0..11). */\n get rootPc(): number {\n return this.#scale.rootPc;\n }\n\n /** Whether the scale has a minor third and no major third. */\n get isMinor(): boolean {\n return isMinorKey(this.#scale);\n }\n\n /**\n * The scale's pitch classes in ascending scale-degree order (degree 0 first).\n *\n * @returns One pitch class per scale degree.\n */\n pitchClasses(): number[] {\n return scaleTonesInDegreeOrder(this.#scale);\n }\n\n /**\n * Transpose the key by a number of semitones.\n *\n * The mode mask is unchanged, so the scale keeps its shape; only the tonic\n * moves. The new tonic is spelled the way that key is conventionally written\n * — D major, not C## major.\n *\n * @param semitones The signed semitone offset.\n * @returns The transposed key.\n * @example\n * ```ts\n * import { Key } from '@libraz/libcantus';\n * Key.major('C').transpose(2).toString(); // 'D major'\n * ```\n */\n transpose(semitones: number): Key {\n assertFiniteNumber(semitones, 'semitones');\n const rootPc = mod12(this.#scale.rootPc + Math.round(semitones));\n return Key.of({ rootPc, modeMask12: this.#scale.modeMask12 });\n }\n\n /**\n * The spelled scale, one note per degree (e.g. C D E F G A B for C major).\n *\n * @returns Spelled octave-less notes in scale-degree order.\n */\n notes(): Note[] {\n return spellScale(this.#tonic.data, this.#scale).map((note) => new Note(note));\n }\n\n /**\n * Alias of {@link Key.notes}.\n *\n * @returns Spelled octave-less notes in scale-degree order.\n */\n spell(): Note[] {\n return this.notes();\n }\n\n /**\n * The spelled scale as letter-name strings.\n *\n * @returns One name per scale degree.\n */\n noteNames(): string[] {\n return this.notes().map((note) => note.name);\n }\n\n /**\n * Build a chord on a scale degree, carrying this key as context.\n *\n * With an explicit quality the quality's interval template is attached to the\n * degree's diatonic root; without one the scale-correct diatonic triad is\n * stacked (e.g. a diminished triad on the leading tone of a major key).\n *\n * @param degree 0-based scale degree of the chord root.\n * @param quality Optional chord quality.\n * @returns The chord, with this key attached.\n * @throws Without a `quality`, if this key's scale is not heptatonic —\n * stacking thirds needs seven degrees, so the pentatonic, blues,\n * whole-tone, octatonic and chromatic scales have no diatonic triad. Pass\n * an explicit `quality` for those.\n */\n chord(degree: number, quality?: ChordQuality): Chord {\n const data =\n quality === undefined\n ? diatonicTriad(degree, this.#scale)\n : chordFromDegree(degree, quality, this.#scale);\n return new Chord(data, this);\n }\n\n /**\n * The diatonic triad on a scale degree, carrying this key as context.\n *\n * @param degree 0-based scale degree of the chord root.\n * @returns The triad, with this key attached.\n * @throws If this key's scale is not heptatonic; stacking thirds needs seven\n * degrees. Use {@link Key.chord} with an explicit quality instead.\n */\n diatonicTriad(degree: number): Chord {\n return new Chord(diatonicTriad(degree, this.#scale), this);\n }\n\n /**\n * The diatonic seventh chord on a scale degree, carrying this key as context.\n *\n * @param degree 0-based scale degree of the chord root.\n * @returns The seventh chord, with this key attached.\n * @throws If this key's scale is not heptatonic; stacking thirds needs seven\n * degrees. Use {@link Key.chord} with an explicit quality instead.\n */\n diatonicSeventh(degree: number): Chord {\n return new Chord(diatonicSeventh(degree, this.#scale), this);\n }\n\n /**\n * Build the chord denoted by a Roman numeral in this key (including applied\n * chords such as `'V7/V'`), carrying this key as context.\n *\n * @param text The Roman numeral.\n * @returns The chord, with this key attached.\n * @throws If the numeral is not valid.\n */\n roman(text: string): Chord {\n return new Chord(romanToChord(text, this.#scale), this);\n }\n\n /**\n * Whether a pitch belongs to the scale.\n *\n * @param x A MIDI pitch, bare pitch class, or note.\n * @returns True if the pitch class is a scale tone.\n */\n contains(x: number | Note): boolean {\n return isScaleTone(typeof x === 'number' ? x : x.pitchClass, this.#scale);\n }\n\n /**\n * The plain key data, for JSON serialization.\n *\n * Private class fields do not serialize, so an explicit `toJSON` keeps\n * `JSON.stringify(key)` from collapsing to `{}`. The result pairs the\n * `KeyScale` with the spelled tonic, enough to reconstruct the key via\n * {@link Key.of}.\n *\n * @returns The key/scale and its spelled tonic.\n */\n toJSON(): { scale: KeyScale; tonic: NoteData } {\n return { scale: this.scale, tonic: this.#tonic.data };\n }\n\n /**\n * The key's tonic and mode, so a template literal or a log line reads as the\n * key. Only the major/minor distinction is named, since a mask does not carry\n * a scale name.\n *\n * @returns The name, e.g. `'C major'` or `'A minor'`.\n */\n toString(): string {\n return `${this.#tonic.name} ${this.isMinor ? 'minor' : 'major'}`;\n }\n}\n","import {\n type Cadence,\n type ChordAnalysis,\n detectCadence,\n type HarmonicFunction,\n} from '../analyze/functional/index.js';\nimport { InvalidInputError } from '../core/errors/index.js';\nimport type { Note as NoteData } from '../core/pitch/index.js';\nimport type { KeyScale } from '../core/types.js';\nimport type { Chord as ChordData, ChordSpan } from '../theory/chord/index.js';\nimport { makeChord } from '../theory/chord/index.js';\nimport { type VoicingOptions, voiceProgression } from '../theory/voicing/index.js';\nimport type { Chord } from './chord.js';\nimport { Chord as ChordClass } from './chord.js';\nimport type { Key } from './key.js';\nimport { Key as KeyClass } from './key.js';\n\n/**\n * An immutable ordered sequence of chords, optionally carrying a {@link Key}\n * context shared by its analysis methods.\n *\n * @category Class API\n * @example\n * ```ts\n * import { Key } from '@libraz/libcantus';\n * const key = Key.major('C');\n * key.chord(1).progressionTo(key.chord(4), key.chord(0)).roman();\n * // ['ii', 'V', 'I']\n * ```\n */\nexport class Progression {\n readonly #chords: readonly Chord[];\n readonly #key: Key | undefined;\n\n /**\n * Wrap a chord sequence.\n *\n * @param chords The chords in order; the array is copied.\n * @param key Optional key context for analysis methods.\n */\n constructor(chords: readonly Chord[], key?: Key) {\n this.#chords = Object.freeze([...chords]);\n this.#key = key;\n }\n\n /**\n * Build a progression from the {@link ChordSpan} records the generators\n * return, keeping their order.\n *\n * The spans' `startBeat`, `degree`, and `secondaryDominant` are analysis\n * annotations that a chord sequence does not carry; only the harmony crosses\n * over. Keep the spans themselves if the timing matters.\n *\n * @param spans The chord spans, in order.\n * @param key Optional key context for the analysis methods.\n * @returns The progression.\n * @example\n * ```ts\n * import { generateProgression, Key, Progression } from '@libraz/libcantus';\n * const key = Key.major('C');\n * const spans = generateProgression({ key: key.scale, style: 'dance', bars: 4 });\n * Progression.fromSpans(spans, key).roman();\n * ```\n */\n static fromSpans(spans: readonly ChordSpan[], key?: Key): Progression {\n const chords = spans.map((span) => {\n const chord = ChordClass.from(makeChord(span.rootPc, span.quality, span.bassPc));\n return key === undefined ? chord : chord.withKey(key);\n });\n return key === undefined ? new Progression(chords) : new Progression(chords, key);\n }\n\n /**\n * Rebuild a progression from its {@link Progression.toJSON} output.\n *\n * @param data The serialized chords and key.\n * @returns The progression.\n */\n static fromJSON(data: {\n chords: ChordData[];\n key?: { scale: KeyScale; tonic: NoteData } | undefined;\n }): Progression {\n const key = data.key === undefined ? undefined : KeyClass.fromJSON(data.key);\n const chords = data.chords.map((chord) => ChordClass.fromJSON(chord));\n return key === undefined ? new Progression(chords) : new Progression(chords, key);\n }\n\n /**\n * The chord sequence.\n *\n * The array is the progression's own and is frozen rather than copied, so\n * reading it in a loop stays linear.\n */\n get chords(): readonly Chord[] {\n return this.#chords;\n }\n\n /** The carried key context, if any. */\n get key(): Key | undefined {\n return this.#key;\n }\n\n /** The number of chords. */\n get length(): number {\n return this.#chords.length;\n }\n\n /**\n * The chord at an index.\n *\n * @param index 0-based position; a negative index counts from the end.\n * @returns The chord, or undefined when the index is out of range.\n */\n at(index: number): Chord | undefined {\n return this.#chords.at(index);\n }\n\n /** Iterate the chords in order, so a progression works with `for...of`. */\n [Symbol.iterator](): IterableIterator<Chord> {\n return this.#chords[Symbol.iterator]();\n }\n\n /**\n * Whether another progression holds the same chords in the same order.\n *\n * The key context is not compared: it is an analysis lens, not part of the\n * harmony. Chord equality follows {@link Chord.equals}.\n *\n * @param other The progression to compare.\n * @returns True when the chord sequences match.\n */\n equals(other: Progression): boolean {\n if (this.#chords.length !== other.length) {\n return false;\n }\n return this.#chords.every((chord, index) => {\n const theirs = other.at(index);\n return theirs !== undefined && chord.equals(theirs);\n });\n }\n\n /**\n * A copy of this progression with a chord appended.\n *\n * @param chord The chord to append.\n * @returns The new progression.\n */\n add(chord: Chord): Progression {\n return new Progression([...this.#chords, chord], this.#key);\n }\n\n /**\n * A copy of this progression carrying the given key context.\n *\n * @param key The key context to attach.\n * @returns The new progression.\n */\n withKey(key: Key): Progression {\n return new Progression(this.#chords, key);\n }\n\n /**\n * Voice the progression with smooth voice leading.\n *\n * @param opts Voicing options; defaults to four SATB voices.\n * @returns One ascending voicing (MIDI pitches) per chord.\n * @throws If any chord admits no voicing within the given ranges.\n */\n voice(opts?: VoicingOptions): number[][] {\n return voiceProgression(\n this.#chords.map((chord) => chord.data),\n opts,\n );\n }\n\n /**\n * The Roman numeral of each chord in a key.\n *\n * @param key Key to analyze in; falls back to the carried context.\n * @returns One numeral per chord.\n * @throws If no key is given and none is carried.\n */\n roman(key?: Key): string[] {\n const resolved = this.#resolveKey(key);\n return this.#chords.map((chord) => chord.roman(resolved));\n }\n\n /**\n * The harmonic function of each chord in a key.\n *\n * @param key Key to analyze in; falls back to the carried context.\n * @returns One function per chord.\n * @throws If no key is given and none is carried.\n */\n functions(key?: Key): HarmonicFunction[] {\n const resolved = this.#resolveKey(key);\n return this.#chords.map((chord) => chord.function(resolved));\n }\n\n /**\n * Analyze every chord and classify the closing cadence.\n *\n * The cadence is detected on the final chord pair and is null when the\n * progression has fewer than two chords.\n *\n * @param key Key to analyze in; falls back to the carried context.\n * @returns Per-chord analyses and the closing cadence.\n * @throws If no key is given and none is carried.\n */\n analyze(key?: Key): { chords: ChordAnalysis[]; cadence: Cadence } {\n const resolved = this.#resolveKey(key);\n const chords = this.#chords.map((chord) => chord.analyze(resolved));\n const from = this.#chords[this.#chords.length - 2];\n const to = this.#chords[this.#chords.length - 1];\n const cadence =\n from !== undefined && to !== undefined\n ? detectCadence(from.data, to.data, resolved.scale)\n : null;\n return { chords, cadence };\n }\n\n /**\n * The plain progression data, for JSON serialization.\n *\n * Private class fields do not serialize, so an explicit `toJSON` keeps\n * `JSON.stringify(progression)` from collapsing to `{}`. The chords are\n * emitted as plain data and the carried key, when present, as its own data.\n *\n * @returns The chord data sequence and the carried key, if any.\n */\n /**\n * Transpose every chord by a number of semitones.\n *\n * A carried key moves with the chords, so the progression keeps its degrees\n * and functions in the new key.\n *\n * @param semitones The signed semitone offset.\n * @returns The transposed progression.\n * @example\n * ```ts\n * import { Chord, Progression } from '@libraz/libcantus';\n * new Progression([Chord.parse('C'), Chord.parse('G')]).transpose(2).toString(); // 'D A'\n * ```\n */\n transpose(semitones: number): Progression {\n const key = this.#key?.transpose(semitones);\n const chords = this.#chords.map((chord) => {\n const moved = chord.transpose(semitones);\n // A chord that carried no key of its own is spelled by this progression's\n // key, so it has to receive the transposed one or it would fall back to\n // sharps in a flat key.\n return key !== undefined && moved.key === undefined ? moved.withKey(key) : moved;\n });\n return key === undefined ? new Progression(chords) : new Progression(chords, key);\n }\n\n /**\n * The chord symbols separated by spaces, so a template literal or a log line\n * reads as the progression.\n *\n * @returns The symbols in order, e.g. `'C Am F G'`.\n */\n toString(): string {\n return this.#chords.map((chord) => chord.symbol()).join(' ');\n }\n\n toJSON(): { chords: ChordData[]; key: { scale: KeyScale; tonic: NoteData } | undefined } {\n return {\n chords: this.#chords.map((chord) => chord.data),\n key: this.#key?.toJSON(),\n };\n }\n\n /** Resolve the key for an analysis method: explicit first, then carried. */\n #resolveKey(key?: Key): Key {\n const resolved = key ?? this.#key;\n if (resolved === undefined) {\n throw new InvalidInputError(\n 'progression has no key context; pass a Key or attach one with withKey()',\n );\n }\n return resolved;\n }\n}\n","import type { ChordMatch, DetectChordOptions } from '../analyze/detect/index.js';\nimport { detectChord, detectChordBest } from '../analyze/detect/index.js';\nimport {\n analyzeChord,\n type BorrowedSource,\n borrowedSource,\n type ChordAnalysis,\n chordToRoman,\n functionOf,\n type HarmonicFunction,\n isBorrowedChord,\n} from '../analyze/functional/index.js';\nimport { InvalidInputError } from '../core/errors/index.js';\nimport type { Note as NoteData } from '../core/pitch/index.js';\nimport { noteToPitchClass } from '../core/pitch/index.js';\nimport { negativeHarmonyMirror } from '../generate/reharmony/index.js';\nimport {\n type Chord as ChordData,\n type ChordQuality,\n chordPitchClasses,\n makeChord,\n type PitchSpelling,\n transposeChord,\n} from '../theory/chord/index.js';\nimport {\n availableTensions,\n avoidNotes,\n type ChordScaleMatch,\n chordScales,\n} from '../theory/chordscale/index.js';\nimport type { ScaleNameInput } from '../theory/scale/index.js';\nimport { spellChord, spellChordFromRoot, spellPitchClass } from '../theory/spelling/index.js';\nimport { formatChordSymbol, parseChordSymbol } from '../theory/symbol/index.js';\nimport {\n type StyledVoicingOptions,\n type VoicingOptions,\n voiceChord,\n voiceChordStyled,\n} from '../theory/voicing/index.js';\nimport type { Key } from './key.js';\nimport { Note } from './note.js';\nimport { Progression } from './progression.js';\nimport { mod12 } from './shared.js';\n\n/** Whether a spelling hint still names the pitch class it is attached to. */\nfunction hintMatches(hint: PitchSpelling | undefined, pc: number | undefined): boolean {\n return hint !== undefined && pc !== undefined && mod12(noteToPitchClass(hint)) === mod12(pc);\n}\n\n/**\n * Defensive copy of a plain chord.\n *\n * Enharmonic spelling hints (`rootSpelling`/`bassSpelling`, populated by\n * `parseChordSymbol`) are carried through so a flat-named chord round-trips\n * through the class API — but only while they still name their own pitch\n * class. A stale hint is dropped here rather than passed on, so that everything\n * downstream, including the derived bass spelling, reads a hint it can trust.\n */\nfunction copyChord(data: ChordData): ChordData {\n const copy: ChordData = {\n rootPc: data.rootPc,\n quality: data.quality,\n intervals: [...data.intervals],\n };\n if (data.bassPc !== undefined) {\n copy.bassPc = data.bassPc;\n }\n if (hintMatches(data.rootSpelling, data.rootPc) && data.rootSpelling !== undefined) {\n copy.rootSpelling = { letter: data.rootSpelling.letter, alter: data.rootSpelling.alter };\n }\n if (hintMatches(data.bassSpelling, data.bassPc) && data.bassSpelling !== undefined) {\n copy.bassSpelling = { letter: data.bassSpelling.letter, alter: data.bassSpelling.alter };\n }\n return copy;\n}\n\n/**\n * Spell the slash bass of a chord whose root spelling is already settled.\n *\n * A bass that is one of the chord's own tones takes that tone's spelling, so\n * `Eb/Bb` never renders as `Eb/A#`; a bass outside the chord is spelled by the\n * key when one is available.\n */\nfunction deriveBassSpelling(chord: ChordData, key: Key | undefined): PitchSpelling | undefined {\n const bassPc = chord.bassPc;\n if (bassPc === undefined) {\n return undefined;\n }\n const root = chord.rootSpelling;\n if (root !== undefined) {\n const index = chord.intervals.findIndex((i) => mod12(chord.rootPc + i) === bassPc);\n const tone = index >= 0 ? spellChordFromRoot(chord, root)[index] : undefined;\n if (tone !== undefined) {\n return { letter: tone.letter, alter: tone.alter };\n }\n }\n if (key === undefined) {\n return undefined;\n }\n const spelled = spellPitchClass(bassPc, key.tonic.data, key.scale);\n return { letter: spelled.letter, alter: spelled.alter };\n}\n\n/**\n * An immutable chord: a root pitch class, quality, interval template, and\n * optional slash bass, optionally carrying a {@link Key} context. Analysis\n * methods (`roman`, `function`, `analyze`, ...) use an explicitly passed key\n * first and fall back to the carried context.\n *\n * @category Class API\n * @example\n * ```ts\n * import { Chord } from '@libraz/libcantus';\n * Chord.parse('Cmaj7').invert(1).symbol(); // 'Cmaj7/E' (third in the bass)\n * ```\n */\nexport class Chord {\n /** Exactly what the caller supplied: any spelling here is the caller's own. */\n readonly #given: ChordData;\n readonly #key: Key | undefined;\n\n /**\n * Wrap a plain chord object.\n *\n * @param data The chord; it is copied, never retained or mutated.\n * @param key Optional key context for analysis methods.\n */\n constructor(data: ChordData, key?: Key) {\n this.#given = copyChord(data);\n this.#key = key;\n }\n\n /**\n * The chord data with any missing spelling hint filled in from the key.\n *\n * Derived spellings are computed on read rather than baked in at construction,\n * so re-attaching a different key re-spells the chord instead of carrying the\n * first key's letters forever. A spelling the caller supplied always wins.\n */\n get #data(): ChordData {\n const out = copyChord(this.#given);\n const key = this.#key;\n if (out.rootSpelling === undefined && key !== undefined) {\n out.rootSpelling = spellPitchClass(out.rootPc, key.tonic.data, key.scale);\n }\n if (out.bassSpelling === undefined) {\n const bass = deriveBassSpelling(out, key);\n if (bass !== undefined) {\n out.bassSpelling = bass;\n }\n }\n return out;\n }\n\n /**\n * Build a chord from a root and quality.\n *\n * @param root Root as a note name (e.g. `'Eb'`) or a pitch class.\n * @param quality The chord quality.\n * @param bass Optional slash-chord bass, as a note name (e.g. `'Bb'`) or a\n * pitch class. A named bass keeps its own spelling.\n * @returns The chord (without key context).\n */\n static of(root: string | number, quality: ChordQuality, bass?: string | number): Chord {\n const bassNote = typeof bass === 'string' ? Note.of(bass) : undefined;\n const bassPc = bassNote !== undefined ? bassNote.pitchClass : (bass as number | undefined);\n const rootNote = typeof root === 'string' ? Note.of(root) : undefined;\n const data = makeChord(\n rootNote !== undefined ? rootNote.pitchClass : (root as number),\n quality,\n bassPc,\n );\n if (rootNote !== undefined) {\n data.rootSpelling = { letter: rootNote.letter, alter: rootNote.alter };\n }\n if (bassNote !== undefined) {\n data.bassSpelling = { letter: bassNote.letter, alter: bassNote.alter };\n }\n return new Chord(data);\n }\n\n /**\n * Wrap an existing plain chord object.\n *\n * @param data The plain chord.\n * @returns The wrapped chord (without key context).\n */\n static from(data: ChordData): Chord {\n return new Chord(data);\n }\n\n /**\n * Alias of {@link Chord.from}, matching the `fromData` factory on the other\n * classes.\n *\n * @param data The plain chord.\n * @returns The wrapped chord (without key context).\n */\n static fromData(data: ChordData): Chord {\n return new Chord(data);\n }\n\n /**\n * Rebuild a chord from its {@link Chord.toJSON} output.\n *\n * The key context is not serialized, so the result carries none; re-attach\n * one with {@link Chord.withKey}.\n *\n * @param data The serialized chord.\n * @returns The wrapped chord.\n */\n static fromJSON(data: ChordData): Chord {\n return new Chord(data);\n }\n\n /**\n * Parse a lead-sheet chord symbol (e.g. `'Cmaj7'`, `'F#m7b5'`, `'C/G'`).\n *\n * @param symbol The chord symbol.\n * @returns The chord (without key context).\n * @throws If the root or quality is not recognized.\n */\n static parse(symbol: string): Chord {\n return new Chord(parseChordSymbol(symbol));\n }\n\n /**\n * Identify the chords matching a set of pitches, best interpretation first.\n *\n * @param pitches MIDI pitches or bare pitch classes.\n * @param opts How to interpret the input; see {@link DetectChordOptions}.\n * @returns Ranked chord interpretations (may be empty).\n */\n static detect(pitches: readonly number[], opts?: DetectChordOptions): Chord[] {\n return detectChord(pitches, opts).map(\n (match) => new Chord(makeChord(match.rootPc, match.quality, match.bassPc)),\n );\n }\n\n /**\n * Identify the chords matching a set of pitches, keeping each match's\n * recognition metadata beside the chord.\n *\n * {@link Chord.detect} discards the confidence signals a recognition UI needs\n * — whether the set matched exactly, which chord tones were missing, which\n * input notes were foreign, and which inversion the bass implies.\n *\n * @param pitches MIDI pitches or bare pitch classes.\n * @param opts How to interpret the input; see {@link DetectChordOptions}.\n * @returns Ranked interpretations, each with its chord and its match record.\n * @example\n * ```ts\n * import { Chord } from '@libraz/libcantus';\n * const [best] = Chord.detectMatches([60, 64, 67]);\n * best?.match.exact; // true\n * ```\n */\n static detectMatches(\n pitches: readonly number[],\n opts?: DetectChordOptions,\n ): { chord: Chord; match: ChordMatch }[] {\n return detectChord(pitches, opts).map((match) => ({\n chord: new Chord(makeChord(match.rootPc, match.quality, match.bassPc)),\n match,\n }));\n }\n\n /**\n * The single best chord interpretation of a pitch set.\n *\n * @param pitches MIDI pitches or bare pitch classes.\n * @param opts How to interpret the input; see {@link DetectChordOptions}.\n * @returns The top-ranked chord, or null when nothing matches.\n */\n static detectBest(pitches: readonly number[], opts?: DetectChordOptions): Chord | null {\n const best = detectChordBest(pitches, opts);\n return best === null ? null : new Chord(best);\n }\n\n /** The root pitch class (0..11). */\n get rootPc(): number {\n return this.#data.rootPc;\n }\n\n /** The chord quality. */\n get quality(): ChordQuality {\n return this.#data.quality;\n }\n\n /** A copy of the semitone offsets above the root. */\n get intervals(): number[] {\n return [...this.#data.intervals];\n }\n\n /** The slash-chord bass pitch class, or undefined in root position. */\n get bassPc(): number | undefined {\n return this.#data.bassPc;\n }\n\n /**\n * A copy of the underlying plain chord object, including the spellings\n * derived from any attached key.\n */\n get data(): ChordData {\n return this.#data;\n }\n\n /** The carried key context, if any. */\n get key(): Key | undefined {\n return this.#key;\n }\n\n /**\n * A copy of this chord carrying the given key context.\n *\n * Only a spelling the caller supplied (via `Chord.parse`, `Chord.of` with a\n * named root, or plain data carrying a hint) survives; a spelling that came\n * from a previously attached key is re-derived, so re-keying a progression\n * during a modulation does not keep the old key's letters and the order of\n * `withKey` calls does not affect the result.\n *\n * @param key The key context to attach.\n * @returns The new chord.\n */\n withKey(key: Key): Chord {\n return new Chord(this.#given, key);\n }\n\n /**\n * The chord's sorted, deduplicated pitch classes.\n *\n * @returns Pitch classes ascending in [0, 11].\n */\n pitchClasses(): number[] {\n return chordPitchClasses(this.#data);\n }\n\n /**\n * The chord's Roman numeral in a key.\n *\n * @param key Key to analyze in; falls back to the carried context.\n * @returns The Roman numeral string.\n * @throws If no key is given and none is carried.\n */\n roman(key?: Key): string {\n return chordToRoman(this.#data, this.#resolveKey(key).scale);\n }\n\n /**\n * The chord's harmonic function (tonic / subdominant / dominant) in a key.\n *\n * @param key Key to analyze in; falls back to the carried context.\n * @returns The harmonic function.\n * @throws If no key is given and none is carried.\n */\n function(key?: Key): HarmonicFunction {\n return functionOf(this.#data, this.#resolveKey(key).scale);\n }\n\n /**\n * Full functional analysis: function, borrowing, and Roman numeral.\n *\n * @param key Key to analyze in; falls back to the carried context.\n * @returns The chord analysis.\n * @throws If no key is given and none is carried.\n */\n analyze(key?: Key): ChordAnalysis {\n return analyzeChord(this.#data, this.#resolveKey(key).scale);\n }\n\n /**\n * Whether the chord is borrowed from the parallel mode (modal interchange).\n *\n * @param key Key to analyze in; falls back to the carried context.\n * @returns True if the chord is borrowed.\n * @throws If no key is given and none is carried.\n */\n isBorrowed(key?: Key): boolean {\n return isBorrowedChord(this.#data, this.#resolveKey(key).scale);\n }\n\n /**\n * The origin of a non-diatonic chord (parallel mode or Neapolitan), or null.\n *\n * @param key Key to analyze in; falls back to the carried context.\n * @returns The borrowing source, or null.\n * @throws If no key is given and none is carried.\n */\n borrowedSource(key?: Key): BorrowedSource {\n return borrowedSource(this.#data, this.#resolveKey(key).scale);\n }\n\n /**\n * The chord rendered as a lead-sheet symbol (e.g. `'Cmaj7'`, `'F#m7'`, `'C/G'`).\n *\n * @param opts Set `flats: true` to spell the root/bass with flats.\n * @returns The chord symbol.\n */\n symbol(opts?: { flats?: boolean }): string {\n return formatChordSymbol(this.#data, opts);\n }\n\n /**\n * Realize the chord as one MIDI pitch per voice, ascending.\n *\n * A chord that carries a key passes it to the voicer, so the leading tone is\n * not doubled; an explicit `opts.key` overrides it.\n *\n * @param opts Voicing options; defaults to four SATB voices.\n * @returns MIDI pitches, ascending, one per voice.\n * @throws If no voicing fits the given ranges.\n */\n voice(opts?: VoicingOptions): number[] {\n const key = opts?.key ?? this.#key?.scale;\n return voiceChord(this.#data, key === undefined ? opts : { ...opts, key });\n }\n\n /**\n * Realize the chord as a single styled voicing (`close`, `drop2`, `drop3`,\n * `shell`, or `rootless`), optionally constraining the top voice.\n *\n * @param opts Styled-voicing options; defaults to a close-position voicing.\n * @returns MIDI pitches, ascending.\n */\n styledVoicing(opts?: StyledVoicingOptions): number[] {\n return voiceChordStyled(this.#data, opts);\n }\n\n /**\n * The negative-harmony mirror of the chord about the key's tonic–dominant\n * axis (major becomes minor and vice versa).\n *\n * @param key Key providing the reflection axis; falls back to the carried\n * context.\n * @returns The mirrored chord, keeping any key context.\n * @throws If no key is given and none is carried.\n */\n negativeHarmony(key?: Key): Chord {\n const resolved = this.#resolveKey(key);\n // Retain the key that anchored the reflection (explicit first, then carried)\n // so a later no-arg analysis method still has a key context.\n return new Chord(negativeHarmonyMirror(this.#data, resolved.scale), key ?? this.#key);\n }\n\n /**\n * The n-th inversion: a copy whose bass is the chord tone `n` steps above the\n * root in the interval template (`invert(1)` puts the third in the bass).\n * `n` wraps around the template length; negative values count backwards.\n *\n * `invert(0)` (and any `n` that wraps to it) is root position, so it carries\n * no slash bass and equals the original chord.\n *\n * @param n The inversion number.\n * @returns The inverted chord, keeping any key context.\n * @throws If the chord has no intervals.\n */\n invert(n: number): Chord {\n const data = copyChord(this.#given);\n const intervals = data.intervals;\n const length = intervals.length;\n if (length === 0) {\n throw new InvalidInputError('cannot invert a chord with no intervals');\n }\n const index = ((n % length) + length) % length;\n // The bass spelling follows from the root spelling in force at read time, so\n // it is derived rather than frozen in here.\n delete data.bassSpelling;\n if (index === 0) {\n delete data.bassPc;\n } else {\n data.bassPc = mod12(data.rootPc + (intervals[index] ?? 0));\n }\n return new Chord(data, this.#key);\n }\n\n /**\n * Transpose the chord by a number of semitones.\n *\n * The quality and interval template are carried over, so a chord that a\n * symbol round-trip could not express — a custom interval set, an inversion —\n * survives. A carried key moves with the chord, so the transposed chord keeps\n * the same degree and function inside the transposed key.\n *\n * @param semitones The signed semitone offset.\n * @returns The transposed chord, in the transposed key when one is carried.\n * @example\n * ```ts\n * import { Chord } from '@libraz/libcantus';\n * Chord.parse('C/G').transpose(2).symbol(); // 'D/A'\n * ```\n */\n transpose(semitones: number): Chord {\n const moved = transposeChord(this.#given, semitones);\n return new Chord(moved, this.#key?.transpose(semitones));\n }\n\n /**\n * The named scales that fit over this chord, best fit first, rooted on the\n * chord root.\n *\n * @returns The matching scales.\n */\n scales(): ChordScaleMatch[] {\n return chordScales(this.#data);\n }\n\n /**\n * The available tensions (usable non-chord, non-avoid scale tones) of a\n * scale over this chord.\n *\n * @param scaleName A named scale, rooted on the chord root.\n * @returns Tension pitch classes, ascending in [0, 11].\n * @throws If `scaleName` is not a built-in scale.\n */\n tensions(scaleName: ScaleNameInput): number[] {\n return availableTensions(this.#data, scaleName);\n }\n\n /**\n * The avoid notes (scale tones a semitone above a chord tone) of a scale\n * over this chord.\n *\n * @param scaleName A named scale, rooted on the chord root.\n * @returns Avoid-note pitch classes, ascending in [0, 11].\n * @throws If `scaleName` is not a built-in scale.\n */\n avoidNotes(scaleName: ScaleNameInput): number[] {\n return avoidNotes(this.#data, scaleName);\n }\n\n /**\n * Spell the chord tones with letter names, root first, in the key's spelling.\n *\n * A chord that already knows how its root is spelled — one from\n * `Chord.parse`, or `Chord.of` with a named root — can spell itself with no\n * key at all; a key is only needed to choose a spelling for a bare pitch\n * class.\n *\n * A slash bass outside the chord tones is appended, so the spelled notes\n * are the notes the chord sounds — the same set {@link Chord.pitchClasses}\n * reports.\n *\n * @param key Key providing the spelled tonic; falls back to the carried\n * context, then to the chord's own root spelling.\n * @returns Spelled octave-less notes in the chord's own (tertian) order,\n * with any foreign slash bass last.\n * @throws If no key is given, none is carried, and the chord has no root\n * spelling of its own.\n */\n spell(key?: Key): Note[] {\n const data = this.#data;\n const resolved = key ?? this.#key;\n let tones: NoteData[];\n if (resolved === undefined) {\n const root = data.rootSpelling;\n if (root === undefined) {\n throw new InvalidInputError(\n 'chord has no key context and no root spelling; pass a Key, attach one with withKey(), or build the chord from a symbol',\n );\n }\n tones = spellChordFromRoot(data, root);\n } else {\n tones = spellChord(data, resolved.tonic.data, resolved.scale);\n }\n const bassPc = data.bassPc;\n const bass = data.bassSpelling;\n if (\n bassPc !== undefined &&\n bass !== undefined &&\n !tones.some((tone) => mod12(noteToPitchClass(tone)) === mod12(bassPc))\n ) {\n tones = [...tones, { letter: bass.letter, alter: bass.alter }];\n }\n return tones.map((note) => new Note(note));\n }\n\n /**\n * Start a progression with this chord followed by others, carrying this\n * chord's key context (if any).\n *\n * @param others The chords following this one.\n * @returns The progression.\n */\n progressionTo(...others: Chord[]): Progression {\n return new Progression([this, ...others], this.#key);\n }\n\n /**\n * Whether another chord has the same root, quality, intervals, and bass.\n * Key context is not compared.\n *\n * @param other The chord to compare.\n * @returns True if the chord data is identical.\n */\n equals(other: Chord): boolean {\n const a = this.#data;\n const b = other.data;\n return (\n a.rootPc === b.rootPc &&\n a.quality === b.quality &&\n a.bassPc === b.bassPc &&\n a.intervals.length === b.intervals.length &&\n a.intervals.every((interval, i) => interval === b.intervals[i])\n );\n }\n\n /**\n * The plain chord data, for JSON serialization.\n *\n * @returns A copy of the underlying plain chord object.\n */\n toJSON(): ChordData {\n return this.data;\n }\n\n /**\n * The chord symbol, so a template literal or a log line reads as the chord.\n *\n * @returns The symbol, e.g. `'Cmaj7'`.\n */\n toString(): string {\n return this.symbol();\n }\n\n /** Resolve the key for an analysis method: explicit first, then carried. */\n #resolveKey(key?: Key): Key {\n const resolved = key ?? this.#key;\n if (resolved === undefined) {\n throw new InvalidInputError(\n 'chord has no key context; pass a Key or attach one with withKey()',\n );\n }\n return resolved;\n }\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/theory/safety/index.ts"],"sourcesContent":["import { InvalidInputError } from '../../core/errors/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport {\n assertFiniteNumber,\n assertGenerationBudget,\n assertInteger,\n} from '../../core/validation/index.js';\nimport type { Chord } from '../chord/index.js';\nimport { intervalAboveRoot, isChordMember } from '../chord/index.js';\nimport {\n createsHiddenParallelPerfect,\n createsParallelPerfect,\n createsVerticalDissonance,\n isForbiddenMelodicLeap,\n} from '../counterpoint/index.js';\nimport { isScaleTone } from '../scale/index.js';\n\n/**\n * Severity policy: `strict` counterpoint vs. lenient `pop` voice-leading.\n *\n * @category Arrangement & Analysis\n */\nexport type SafetyProfile = 'strict' | 'pop';\n\n/**\n * Overall placeability verdict for a candidate pitch.\n *\n * @category Arrangement & Analysis\n */\nexport enum NoteSafety {\n Safe = 0,\n Warning = 1,\n Dissonant = 2,\n}\n\n/**\n * Bit flags describing why a pitch received its verdict.\n *\n * @category Arrangement & Analysis\n */\nexport enum ReasonFlag {\n ChordTone = 1 << 0,\n Tension = 1 << 1,\n AvoidNote = 1 << 2,\n ScaleTone = 1 << 3,\n NonScale = 1 << 4,\n OutOfRange = 1 << 5,\n Tritone = 1 << 6,\n LargeLeap = 1 << 7,\n MinorSecond = 1 << 8,\n MajorSeventh = 1 << 9,\n VerticalDissonance = 1 << 10,\n ParallelPerfect = 1 << 11,\n HiddenParallel = 1 << 12,\n VoiceCrossing = 1 << 13,\n Suspension = 1 << 14,\n NeedsResolution = 1 << 15,\n}\n\n/**\n * A neighbouring voice's current and previous pitch.\n *\n * @category Arrangement & Analysis\n */\nexport type VoiceSnapshot = {\n pitch: number;\n prevPitch?: number;\n};\n\n/**\n * Everything needed to evaluate one candidate pitch.\n *\n * @category Arrangement & Analysis\n */\nexport type SafetyQuery = {\n profile: SafetyProfile;\n candidatePitch: number;\n prevPitch?: number;\n chord: Chord | null;\n key: KeyScale;\n otherVoices: VoiceSnapshot[];\n strongBeat: boolean;\n vocalLow?: number;\n vocalHigh?: number;\n};\n\n/**\n * The verdict, reason flags, and optional resolution guidance.\n *\n * @category Arrangement & Analysis\n */\nexport type SafetyResult = {\n safety: NoteSafety;\n reasons: number;\n resolveTo?: number;\n suggestions?: number[];\n rationale?: string;\n};\n\n/** Semitone radius searched around a rejected candidate for safe alternatives. */\nconst SUGGESTION_WINDOW = 12;\n/** Maximum number of alternative pitches returned in `SafetyResult.suggestions`. */\nconst MAX_SUGGESTIONS = 3;\n\n/** Canonical minimum verdict contributed by each non-informational reason. */\nconst REASON_SEVERITY: ReadonlyArray<\n readonly [flag: ReasonFlag, pop: NoteSafety, strict: NoteSafety]\n> = [\n [ReasonFlag.Tension, NoteSafety.Warning, NoteSafety.Warning],\n [ReasonFlag.AvoidNote, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.ScaleTone, NoteSafety.Warning, NoteSafety.Warning],\n [ReasonFlag.NonScale, NoteSafety.Dissonant, NoteSafety.Dissonant],\n [ReasonFlag.OutOfRange, NoteSafety.Warning, NoteSafety.Warning],\n [ReasonFlag.Tritone, NoteSafety.Dissonant, NoteSafety.Dissonant],\n [ReasonFlag.LargeLeap, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.VerticalDissonance, NoteSafety.Dissonant, NoteSafety.Dissonant],\n [ReasonFlag.ParallelPerfect, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.HiddenParallel, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.VoiceCrossing, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.Suspension, NoteSafety.Dissonant, NoteSafety.Dissonant],\n [ReasonFlag.NeedsResolution, NoteSafety.Warning, NoteSafety.Dissonant],\n];\n\nfunction minimumSafetyForReasons(reasons: number, profile: SafetyProfile): NoteSafety {\n let minimum = NoteSafety.Safe;\n const severityIndex = profile === 'strict' ? 2 : 1;\n for (const entry of REASON_SEVERITY) {\n if (reasons & entry[0]) {\n minimum = Math.max(minimum, entry[severityIndex]) as NoteSafety;\n }\n }\n return minimum;\n}\n\n/** Whether the perfect fourth is an avoid note over this chord. */\nfunction avoidsFourth(chord: Chord): boolean {\n return chord.intervals.includes(4) && !chord.intervals.includes(5);\n}\n\n/** Whether the major seventh is an avoid note over this chord (dominant). */\nfunction avoidsMajorSeventh(chord: Chord): boolean {\n return chord.intervals.includes(4) && chord.intervals.includes(10);\n}\n\n/**\n * Whether a dissonance between two chord tones is one the chord is built from.\n *\n * A seventh chord states a seventh against its root and a dominant states the\n * tritone between its third and seventh; those intervals are the chord's\n * identity, not a clash to be prepared and resolved. A second between two chord\n * tones (a ninth voiced against the root, say) is a spacing problem rather than\n * a property of the harmony, so it stays reportable.\n */\nfunction isStructuralChordDissonance(a: number, b: number, chord: Chord): boolean {\n const cls = Math.abs(Math.trunc(a) - Math.trunc(b)) % 12;\n if (cls !== 6 && cls !== 10 && cls !== 11) {\n return false;\n }\n return isChordMember(a, chord) && isChordMember(b, chord);\n}\n\n/** Nearest chord tone at or below the candidate, within two semitones. */\nfunction stepResolution(pitch: number, chord: Chord): number | undefined {\n for (let delta = 1; delta <= 2; delta += 1) {\n if (isChordMember(pitch - delta, chord)) {\n return pitch - delta;\n }\n }\n return undefined;\n}\n\n/**\n * Evaluate the safety of placing a candidate pitch in a voice.\n *\n * Reasons are merged from harmonic context (chord tone / tension / avoid / scale)\n * and counterpoint (vertical dissonance, parallels, voice crossing). The `strict`\n * profile treats avoid notes and parallels as dissonant; `pop` treats them as\n * warnings. The final verdict is the worst severity among the reasons.\n *\n * @param q The candidate and its harmonic/voice-leading context.\n * @returns The verdict, reason bitmask, and optional resolution guidance.\n * @example\n * ```ts\n * import { evaluateSafety, makeChord, majorKey, NoteSafety } from '@libraz/libcantus';\n * const result = evaluateSafety({\n * profile: 'pop',\n * candidatePitch: 60, // C over a C major chord\n * chord: makeChord(0, 'maj'),\n * key: majorKey(0),\n * otherVoices: [],\n * strongBeat: true,\n * });\n * result.safety === NoteSafety.Safe; // true — C is a chord tone\n * ```\n * @category Arrangement & Analysis\n */\nexport function evaluateSafety(q: SafetyQuery, opts: EvaluateSafetyOptions = {}): SafetyResult {\n assertSafetyContext(q);\n assertFiniteNumber(q.candidatePitch, 'candidatePitch');\n return evaluateInternal(q, opts.suggestions ?? true);\n}\n\n/**\n * Options for {@link evaluateSafety}.\n *\n * @category Arrangement & Analysis\n */\nexport type EvaluateSafetyOptions = {\n /**\n * Whether to search for safe replacement pitches when the verdict is not\n * safe. Turning it off skips up to two dozen extra evaluations per rejected\n * pitch, which is worth doing when only the verdict is wanted.\n *\n * @defaultValue true\n */\n suggestions?: boolean;\n};\n\n/** Validate all numeric context fields once at a public safety entry point. */\nfunction assertSafetyContext(q: Omit<SafetyQuery, 'candidatePitch'>): void {\n if (q.prevPitch !== undefined) assertFiniteNumber(q.prevPitch, 'prevPitch');\n if (q.vocalLow !== undefined) assertFiniteNumber(q.vocalLow, 'vocalLow');\n if (q.vocalHigh !== undefined) assertFiniteNumber(q.vocalHigh, 'vocalHigh');\n if (q.vocalLow !== undefined && q.vocalHigh !== undefined && q.vocalLow > q.vocalHigh) {\n throw new InvalidInputError('vocalLow must not exceed vocalHigh');\n }\n assertGenerationBudget(q.otherVoices.length, 'other voices');\n for (let index = 0; index < q.otherVoices.length; index += 1) {\n const voice = q.otherVoices[index];\n if (!voice) continue;\n assertFiniteNumber(voice.pitch, `otherVoices[${index}].pitch`);\n if (voice.prevPitch !== undefined) {\n assertFiniteNumber(voice.prevPitch, `otherVoices[${index}].prevPitch`);\n }\n }\n}\n\n/**\n * Core safety evaluation.\n *\n * @param q The candidate and its harmonic/voice-leading context.\n * @param collectSuggestions Whether to search for safe alternatives on a\n * non-Safe verdict. Set false during that search itself to bound recursion.\n * @returns The verdict, reason bitmask, and optional guidance.\n */\nfunction evaluateInternal(q: SafetyQuery, collectSuggestions: boolean): SafetyResult {\n let reasons = 0;\n let level = NoteSafety.Safe;\n let resolveTo: number | undefined;\n const raise = (l: NoteSafety) => {\n if (l > level) {\n level = l;\n }\n };\n const avoidSeverity = q.profile === 'strict' ? NoteSafety.Dissonant : NoteSafety.Warning;\n const parallelSeverity = avoidSeverity;\n const pitch = q.candidatePitch;\n\n if (\n (q.vocalLow !== undefined && pitch < q.vocalLow) ||\n (q.vocalHigh !== undefined && pitch > q.vocalHigh)\n ) {\n reasons |= ReasonFlag.OutOfRange;\n raise(NoteSafety.Warning);\n }\n\n const inScale = isScaleTone(pitch, q.key);\n const chord = q.chord;\n\n if (chord && isChordMember(pitch, chord)) {\n reasons |= ReasonFlag.ChordTone;\n } else if (chord) {\n const ic = intervalAboveRoot(pitch, chord);\n const isTension = ic === 2 || ic === 5 || ic === 9;\n const avoid = (ic === 5 && avoidsFourth(chord)) || (ic === 11 && avoidsMajorSeventh(chord));\n if (avoid) {\n reasons |= ReasonFlag.AvoidNote;\n if (isTension) {\n reasons |= ReasonFlag.Tension;\n }\n reasons |= ReasonFlag.NeedsResolution;\n resolveTo = stepResolution(pitch, chord);\n raise(avoidSeverity);\n } else if (isTension && inScale) {\n reasons |= ReasonFlag.Tension;\n raise(NoteSafety.Warning);\n } else if (inScale) {\n reasons |= ReasonFlag.ScaleTone;\n raise(NoteSafety.Warning);\n } else {\n reasons |= ReasonFlag.NonScale;\n raise(NoteSafety.Dissonant);\n }\n } else if (inScale) {\n reasons |= ReasonFlag.ScaleTone;\n raise(NoteSafety.Warning);\n } else {\n reasons |= ReasonFlag.NonScale;\n raise(NoteSafety.Dissonant);\n }\n\n // Harmonic tritone against a chord tone. A chord tone is never flagged for a\n // tritone its own chord spells: the interval is the chord's identity, not a\n // clash with it. This covers dominant sevenths (third against seventh) and\n // equally the diminished, half-diminished, diminished-seventh and minor-sixth\n // families, where the tritone falls between two chord tones as well.\n if (chord && !isChordMember(pitch, chord)) {\n const ic = intervalAboveRoot(pitch, chord);\n const toneOffsets = chord.intervals.map((i) => ((i % 12) + 12) % 12);\n if (toneOffsets.some((t) => Math.abs(ic - t) === 6)) {\n reasons |= ReasonFlag.Tritone;\n }\n }\n\n if (q.prevPitch !== undefined) {\n // Quality flags reduce the melodic interval to its pitch-class interval so\n // compound intervals are treated uniformly with their simple form. The\n // forbidden-leap flag defers to the counterpoint rule, which preserves the\n // octave boundary (an octave leap is allowed, wider leaps are not).\n const pc = Math.abs(pitch - q.prevPitch) % 12;\n if (pc === 6) {\n reasons |= ReasonFlag.Tritone;\n raise(NoteSafety.Dissonant);\n }\n if (isForbiddenMelodicLeap(q.prevPitch, pitch)) {\n reasons |= ReasonFlag.LargeLeap;\n raise(parallelSeverity);\n }\n if (pc === 1) {\n reasons |= ReasonFlag.MinorSecond;\n }\n if (pc === 11) {\n reasons |= ReasonFlag.MajorSeventh;\n }\n }\n\n if (q.strongBeat) {\n const twoVoice = q.otherVoices.length === 1;\n const prev = q.prevPitch;\n for (const ov of q.otherVoices) {\n if (!createsVerticalDissonance(pitch, ov.pitch, twoVoice)) {\n continue;\n }\n if (chord && isStructuralChordDissonance(pitch, ov.pitch, chord)) {\n continue;\n }\n reasons |= ReasonFlag.VerticalDissonance;\n raise(NoteSafety.Dissonant);\n // A held pitch that was consonant on the previous step and is now\n // dissonant against the same voice is a prepared suspension.\n if (\n prev !== undefined &&\n pitch === prev &&\n ov.prevPitch !== undefined &&\n !createsVerticalDissonance(prev, ov.prevPitch, twoVoice)\n ) {\n reasons |= ReasonFlag.Suspension;\n }\n }\n }\n\n if (q.prevPitch !== undefined) {\n const prev = q.prevPitch;\n for (const ov of q.otherVoices) {\n if (ov.prevPitch === undefined) {\n continue;\n }\n // createsParallelPerfect covers every perfect class (unison/octave and\n // fifth), so no separate octave check is needed.\n if (createsParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {\n reasons |= ReasonFlag.ParallelPerfect;\n raise(parallelSeverity);\n }\n if (createsHiddenParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {\n reasons |= ReasonFlag.HiddenParallel;\n raise(parallelSeverity);\n }\n const crossedNow = pitch - ov.pitch;\n const crossedPrev = prev - ov.prevPitch;\n if (crossedNow !== 0 && crossedPrev !== 0 && crossedNow > 0 !== crossedPrev > 0) {\n reasons |= ReasonFlag.VoiceCrossing;\n raise(parallelSeverity);\n }\n }\n }\n\n // Keep reason production and verdict policy as one invariant even when a new\n // call path skips an earlier local `raise`: informational flags (ChordTone,\n // MinorSecond, MajorSeventh) may coexist with Safe; every policy flag above\n // contributes at least its table severity.\n raise(minimumSafetyForReasons(reasons, q.profile));\n\n const result: SafetyResult = { safety: level, reasons, rationale: describe(reasons, q) };\n if (resolveTo !== undefined) {\n result.resolveTo = resolveTo;\n }\n if (collectSuggestions && level !== NoteSafety.Safe) {\n const suggestions = findSafeNearby(q, pitch);\n if (suggestions.length > 0) {\n result.suggestions = suggestions;\n }\n }\n return result;\n}\n\n/**\n * Find safe alternative pitches near a rejected candidate.\n *\n * Searches outward from the candidate within `SUGGESTION_WINDOW` semitones,\n * probing the pitch below before the one above at each distance so the result\n * is deterministic and ordered by nearness.\n *\n * @param q The safety context of the rejected candidate.\n * @param candidate The candidate pitch that was not Safe.\n * @returns Up to `MAX_SUGGESTIONS` safe pitches, nearest first.\n */\nfunction findSafeNearby(q: SafetyQuery, candidate: number): number[] {\n const out: number[] = [];\n for (let d = 1; d <= SUGGESTION_WINDOW; d += 1) {\n for (const p of [candidate - d, candidate + d]) {\n if (evaluateInternal({ ...q, candidatePitch: p }, false).safety === NoteSafety.Safe) {\n out.push(p);\n if (out.length >= MAX_SUGGESTIONS) {\n return out;\n }\n }\n }\n }\n return out;\n}\n\n/**\n * Wording for every reason flag, most to least specific.\n *\n * The list is exhaustive by construction: {@link describe} picks from it by\n * severity, so a flag with no wording here would let a rejected pitch be\n * explained by a milder reason — the seventh of a dominant reading as a plain\n * \"Chord tone\" while its verdict says dissonant.\n */\nconst REASON_TEXT: ReadonlyArray<readonly [ReasonFlag, string]> = [\n [ReasonFlag.OutOfRange, 'Outside the target vocal range'],\n [\n ReasonFlag.Suspension,\n 'Prepared suspension — a held consonance now dissonant, awaiting resolution',\n ],\n [ReasonFlag.VerticalDissonance, 'Dissonant against a sounding voice on a strong beat'],\n [ReasonFlag.Tritone, 'Tritone against the sounding harmony'],\n [ReasonFlag.AvoidNote, 'Avoid note — clashes with a chord tone a semitone away'],\n [ReasonFlag.NonScale, 'Outside the key'],\n [ReasonFlag.ParallelPerfect, 'Parallel perfect interval with another voice'],\n [ReasonFlag.HiddenParallel, 'Hidden perfect interval by similar motion'],\n [ReasonFlag.VoiceCrossing, 'Crosses another voice'],\n [ReasonFlag.LargeLeap, 'Leap wider than the profile allows'],\n [ReasonFlag.NeedsResolution, 'Dissonance awaiting a stepwise resolution'],\n [ReasonFlag.Tension, 'Chord tension'],\n [ReasonFlag.ScaleTone, 'Scale tone, not in the chord'],\n [ReasonFlag.MinorSecond, 'A semitone from a sounding voice'],\n [ReasonFlag.MajorSeventh, 'A major seventh from a sounding voice'],\n [ReasonFlag.ChordTone, 'Chord tone'],\n];\n\n/** The severity a single flag carries under a profile; Safe when purely informational. */\nfunction severityOfReason(flag: ReasonFlag, profile: SafetyProfile): NoteSafety {\n const entry = REASON_SEVERITY.find(([candidate]) => candidate === flag);\n return entry === undefined ? NoteSafety.Safe : entry[profile === 'strict' ? 2 : 1];\n}\n\n/**\n * Build a short human explanation from the reason flags.\n *\n * The flag driving the verdict wins: the one whose severity under this profile\n * is highest, ties going to the more specific wording. Explaining a rejected\n * pitch by an informational flag it also carries would tell the caller nothing\n * about what to fix.\n */\nfunction describe(reasons: number, q: SafetyQuery): string {\n let best: string | undefined;\n let bestSeverity = -1;\n for (const [flag, text] of REASON_TEXT) {\n if (!(reasons & flag)) {\n continue;\n }\n const severity = severityOfReason(flag, q.profile);\n if (severity > bestSeverity) {\n bestSeverity = severity;\n best = text;\n }\n }\n if (best !== undefined) {\n return best;\n }\n return q.chord ? 'Placeable' : 'No chord context';\n}\n\n/**\n * Enumerate placeable pitches in a range, chord tones first, descending.\n *\n * @param q The safety context minus the candidate pitch.\n * @param pitchLow Lowest MIDI pitch to consider (inclusive).\n * @param pitchHigh Highest MIDI pitch to consider (inclusive).\n * @returns Placeable pitches (non-dissonant), chord tones before others, each group descending.\n * @throws If either bound is non-finite/non-integral, reversed, or exceeds the generation budget.\n * @example\n * ```ts\n * import { enumerateSafePitches, makeChord, majorKey } from '@libraz/libcantus';\n * const pitches = enumerateSafePitches(\n * { profile: 'pop', chord: makeChord(0, 'maj'), key: majorKey(0), otherVoices: [], strongBeat: true },\n * 60,\n * 72,\n * );\n * pitches; // placeable pitches in [60, 72], chord tones first, each group descending\n * ```\n * @category Arrangement & Analysis\n */\nexport function enumerateSafePitches(\n q: Omit<SafetyQuery, 'candidatePitch'>,\n pitchLow: number,\n pitchHigh: number,\n): number[] {\n assertSafetyContext(q);\n const chordTones: number[] = [];\n const others: number[] = [];\n assertInteger(pitchLow, 'pitchLow');\n assertInteger(pitchHigh, 'pitchHigh');\n if (pitchLow > pitchHigh) {\n throw new InvalidInputError(\n `pitchLow must not exceed pitchHigh; received ${pitchLow} > ${pitchHigh}`,\n );\n }\n assertGenerationBudget(pitchHigh - pitchLow + 1, 'safe pitch candidates');\n for (let pitch = pitchHigh; pitch >= pitchLow; pitch -= 1) {\n const result = evaluateInternal({ ...q, candidatePitch: pitch }, false);\n if (result.safety === NoteSafety.Dissonant) {\n continue;\n }\n if (result.reasons & ReasonFlag.ChordTone) {\n chordTones.push(pitch);\n } else {\n others.push(pitch);\n }\n }\n return [...chordTones, ...others];\n}\n"],"mappings":"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","names":["NoteSafety","ReasonFlag"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/analyze/timeline/index.ts","../src/analyze/arrange/internal.ts","../src/analyze/arrange/tension.ts","../src/analyze/voice/index.ts","../src/analyze/arrange/tracks.ts"],"sourcesContent":["import { createNoteEventIndex } from '../../core/event-index/index.js';\nimport type { TimeSignature } from '../../core/meter/index.js';\nimport { beatsPerBar, metricWeight, parseTimeSignature } from '../../core/meter/index.js';\nimport { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\nimport type { KeyScale, NoteEvent } from '../../core/types.js';\nimport {\n assertFiniteNumber,\n assertGenerationBudget,\n assertNoteEvents,\n assertRange,\n assertTimeSignature,\n} from '../../core/validation/index.js';\nimport type { Chord, ChordSpan } from '../../theory/chord/index.js';\nimport { chordPitchClasses, makeChord } from '../../theory/chord/index.js';\nimport { isScaleTone, majorKey } from '../../theory/scale/index.js';\nimport type { ChordMatch } from '../detect/index.js';\nimport { detectChord, detectChordBest, detectKeyFromNotes } from '../detect/index.js';\nimport type { Cadence } from '../functional/index.js';\nimport { detectCadence } from '../functional/index.js';\n\nexport type { ChordSegment } from '../../theory/chord/index.js';\n\nimport type { ChordSegment } from '../../theory/chord/index.js';\n\n/**\n * A beat-indexed sequence of chord segments.\n *\n * @category Arrangement & Analysis\n */\nexport type ChordTimeline = {\n /** The chord sounding at a beat, or null when no segment covers it. */\n at: (beat: number) => Chord | null;\n segments: ChordSegment[];\n};\n\n/**\n * Build a chord timeline from placed chords.\n *\n * Each chord spans from its `startBeat` to the next chord's `startBeat`; the last\n * chord runs to `totalBeats`. Segments with no positive length (from duplicate\n * onsets or a last chord at or past `totalBeats`) are dropped. `at(beat)` returns\n * the covering segment's chord, or null when the beat lies outside every segment.\n *\n * @param chords Placed chords in time order.\n * @param totalBeats End of the timeline in beats.\n * @returns A queryable chord timeline.\n * @category Arrangement & Analysis\n */\nexport function chordTimelineFromChords(\n chords: readonly ChordSpan[],\n totalBeats: number,\n): ChordTimeline {\n assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, 'timeline totalBeats');\n assertGenerationBudget(chords.length, 'timeline chords');\n for (let index = 0; index < chords.length; index += 1) {\n assertRange(\n chords[index]?.startBeat ?? Number.NaN,\n 0,\n Number.MAX_SAFE_INTEGER,\n `chords[${index}].startBeat`,\n );\n }\n const sorted = [...chords]\n .filter((chord) => chord.startBeat < totalBeats)\n .sort((a, b) => a.startBeat - b.startBeat);\n const segments: ChordSegment[] = sorted\n .map((gc, i) => {\n const next = sorted[i + 1];\n const nextBeat = next ? next.startBeat : totalBeats;\n const endBeat = Math.min(totalBeats, Math.max(gc.startBeat, nextBeat));\n return {\n startBeat: gc.startBeat,\n endBeat,\n chord: makeChord(gc.rootPc, gc.quality, gc.bassPc),\n };\n })\n .filter((seg) => seg.endBeat > seg.startBeat);\n\n return { at: segmentLookup(segments), segments };\n}\n\n/**\n * Build the `at(beat)` lookup over a segment list.\n *\n * Segments are disjoint and in beat order, so the covering one is found by\n * binary search. A linear scan here is the inner loop of arrangement analysis,\n * which queries it once per note per chord change.\n */\nfunction segmentLookup(segments: ChordSegment[]): (beat: number) => Chord | null {\n return (beat) => {\n assertFiniteNumber(beat, 'timeline query beat');\n let low = 0;\n let high = segments.length;\n while (low < high) {\n const middle = Math.floor((low + high) / 2);\n if ((segments[middle]?.startBeat ?? Number.POSITIVE_INFINITY) <= beat) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n const candidate = segments[low - 1];\n return candidate !== undefined && beat >= candidate.startBeat && beat < candidate.endBeat\n ? candidate.chord\n : null;\n };\n}\n\nconst EPS = 1e-9;\n\n/** Fraction of the strongest pitch-class weight below which a pc is noise. */\nconst NOISE_THRESHOLD_RATIO = 0.2;\n\n/** Maximum number of pitch classes fed to chord detection per window. */\nconst MAX_DETECTION_PCS = 6;\n\n/** Number of top-weighted pitch classes tried in the detection fallback. */\nconst FALLBACK_PCS = 3;\n\n/** Confidence multiplier applied when the chosen chord match is inexact. */\nconst INEXACT_CONFIDENCE_FACTOR = 0.85;\n\n/** Score bonus for a match whose tones are all in the key. */\nconst DIATONIC_BONUS = 0.5;\n\n/** Score bonus for an exact match (no extra and no missing tones). */\nconst EXACT_BONUS = 0.5;\n\n/** Score penalty per extra or missing tone in a match. */\nconst MISMATCH_PENALTY = 0.3;\n\n/**\n * Options controlling {@link chordTimelineFromNotes}.\n *\n * @category Arrangement & Analysis\n */\nexport type ChordTimelineOptions = {\n /** Key context; inferred from the notes with {@link detectKeyFromNotes} when omitted. */\n key?: KeyScale;\n /**\n * Time signature used for metric accents; defaults to 4/4.\n *\n * @defaultValue `4/4`\n */\n ts?: TimeSignature;\n /**\n * Window length in beats per chord slot; defaults to one bar of `ts`.\n *\n * @defaultValue one bar of `ts`\n */\n harmonicRhythm?: number;\n /**\n * End of the analyzed span in beats; defaults to the end of the last note.\n *\n * @defaultValue the end of the last note\n */\n totalBeats?: number;\n /**\n * Upper bound on the work this call may do — note counts, windows, and\n * candidate counts are each checked against it before anything is allocated.\n *\n * Raise it to analyse a piece larger than the default allows; the default is\n * {@link DEFAULT_GENERATION_BUDGET}, chosen so a runaway input fails fast\n * rather than blocking the thread.\n *\n * @defaultValue {@link DEFAULT_GENERATION_BUDGET}\n */\n budget?: number;\n};\n\n/**\n * The result of {@link chordTimelineFromNotes}.\n *\n * @category Arrangement & Analysis\n */\nexport type ChordTimelineResult = {\n /** The inferred timeline, with adjacent identical chords merged. */\n timeline: ChordTimeline;\n /** The key used for the analysis (given or inferred). */\n key: KeyScale;\n /** One confidence value in [0, 1] per segment, in segment order. */\n segmentConfidence: number[];\n};\n\n/** A window's inferred chord and its confidence, or null for an empty window. */\ntype WindowChord = {\n chord: Chord;\n confidence: number;\n};\n\n/**\n * Score a chord match against a window's pitch-class weights: a heavily\n * weighted root, in-key tones, and exactness all raise the score; extra or\n * missing tones lower it.\n */\nfunction scoreMatch(\n match: ChordMatch,\n weights: number[],\n maxWeight: number,\n key: KeyScale,\n): number {\n const rootWeight = maxWeight > 0 ? (weights[match.rootPc] ?? 0) / maxWeight : 0;\n const tones = chordPitchClasses(makeChord(match.rootPc, match.quality));\n let score = rootWeight;\n if (tones.every((pc) => isScaleTone(pc, key))) {\n score += DIATONIC_BONUS;\n }\n if (match.exact) {\n score += EXACT_BONUS;\n }\n score -= MISMATCH_PENALTY * (match.extraPcs.length + match.missingPcs.length);\n return score;\n}\n\n/** Confidence of a chord for a window: chord-tone weight over total weight. */\nfunction chordConfidence(\n chord: Chord,\n weights: number[],\n totalWeight: number,\n exact: boolean,\n): number {\n const tones = new Set(chordPitchClasses(chord));\n let chordWeight = 0;\n for (let pc = 0; pc < 12; pc += 1) {\n if (tones.has(pc)) {\n chordWeight += weights[pc] ?? 0;\n }\n }\n const raw = totalWeight > 0 ? chordWeight / totalWeight : 0;\n const confidence = Math.min(1, Math.max(0, raw));\n return exact ? confidence : confidence * INEXACT_CONFIDENCE_FACTOR;\n}\n\n/**\n * Infer the chord sounding in one window from the notes overlapping it.\n *\n * Builds a pitch-class weight histogram (overlap duration x velocity x\n * metric-accent bonus for onsets inside the window), keeps the significantly\n * weighted pitch classes, and picks the best {@link detectChord} match by\n * root weight, key membership, and exactness.\n */\nfunction analyzeWindow(\n notes: readonly NoteEvent[],\n windowStart: number,\n windowEnd: number,\n ts: TimeSignature,\n key: KeyScale,\n): WindowChord | null {\n const weights = new Array<number>(12).fill(0);\n let lowestPitch = Number.POSITIVE_INFINITY;\n for (const note of notes) {\n const noteEnd = note.startBeat + note.durationBeat;\n const overlap = Math.min(noteEnd, windowEnd) - Math.max(note.startBeat, windowStart);\n if (overlap <= EPS) {\n continue;\n }\n const velocityFactor = note.velocity !== undefined ? note.velocity / 127 : 1;\n const onsetInWindow = note.startBeat >= windowStart - EPS && note.startBeat < windowEnd - EPS;\n const accent = onsetInWindow ? 1 + metricWeight(note.startBeat, ts) / 3 : 1;\n const pc = pitchClass(note.pitch);\n weights[pc] = (weights[pc] ?? 0) + overlap * velocityFactor * accent;\n if (note.pitch < lowestPitch) {\n lowestPitch = note.pitch;\n }\n }\n let totalWeight = 0;\n let maxWeight = 0;\n for (const w of weights) {\n totalWeight += w;\n maxWeight = Math.max(maxWeight, w);\n }\n if (totalWeight <= EPS) {\n return null;\n }\n\n // Keep pitch classes with meaningful weight, strongest first.\n const ranked = weights\n .map((weight, pc) => ({ pc, weight }))\n .filter(({ weight }) => weight > 0)\n .sort((a, b) => b.weight - a.weight || a.pc - b.pc);\n const selected = ranked\n .filter(({ weight }) => weight >= maxWeight * NOISE_THRESHOLD_RATIO)\n .slice(0, MAX_DETECTION_PCS)\n .map(({ pc }) => pc);\n\n // Feed detection with the window's true bass as the lowest pitch so inversion\n // detection works; when the bass pc was filtered out as noise, anchor on the\n // heaviest pc instead so no spurious inversion is reported.\n const bassPc = pitchClass(lowestPitch);\n const anchorPc = selected.includes(bassPc) ? bassPc : (selected[0] ?? bassPc);\n const detectionPitches = selected.map((pc) => (pc === anchorPc ? pc : pc + 12));\n\n const matches = detectChord(detectionPitches);\n let bestMatch: ChordMatch | null = null;\n let bestScore = Number.NEGATIVE_INFINITY;\n for (const match of matches) {\n const score = scoreMatch(match, weights, maxWeight, key);\n if (score > bestScore) {\n bestScore = score;\n bestMatch = match;\n }\n }\n if (bestMatch) {\n const chord = makeChord(bestMatch.rootPc, bestMatch.quality, bestMatch.bassPc);\n return { chord, confidence: chordConfidence(chord, weights, totalWeight, bestMatch.exact) };\n }\n\n // Fallback: retry on only the top-weighted pitch classes.\n const fallback = detectChordBest(selected.slice(0, FALLBACK_PCS));\n if (fallback) {\n return { chord: fallback, confidence: chordConfidence(fallback, weights, totalWeight, false) };\n }\n return null;\n}\n\n/** Whether two chords are the same root, quality, and bass. */\nfunction sameChord(a: Chord, b: Chord): boolean {\n return a.rootPc === b.rootPc && a.quality === b.quality && a.bassPc === b.bassPc;\n}\n\n/**\n * Infer a chord timeline from raw multi-track notes.\n *\n * The span `[0, totalBeats)` is sliced into `harmonicRhythm`-beat windows; each\n * window's chord is inferred from a pitch-class weight histogram of the notes\n * overlapping it (weight = overlap duration x velocity x metric-accent bonus\n * for onsets in the window). Adjacent windows carrying the identical chord are\n * merged into one segment; windows with no notes produce no segment, so\n * `at(beat)` returns null there. Each segment carries a confidence in [0, 1]:\n * the fraction of the window weight explained by chord tones, reduced when the\n * match is inexact, and duration-weighted across merged windows.\n *\n * Notes with a zero or negative duration never sound, so they are dropped at\n * ingest: they contribute to neither the key inference, the span, nor any\n * window's histogram.\n *\n * @param notes The notes to analyze (any number of tracks, flattened).\n * @param opts Analysis options; see {@link ChordTimelineOptions}.\n * @returns The inferred timeline, the key used, and per-segment confidences.\n * @throws If `harmonicRhythm` is not positive.\n * @example\n * ```ts\n * import { chordTimelineFromNotes } from '@libraz/libcantus';\n * const notes = [\n * { pitch: 60, startBeat: 0, durationBeat: 2 }, // C\n * { pitch: 64, startBeat: 0, durationBeat: 2 }, // E\n * { pitch: 67, startBeat: 0, durationBeat: 2 }, // G\n * ];\n * const { timeline, key } = chordTimelineFromNotes(notes);\n * timeline.at(0); // the chord inferred over beat 0, or null\n * ```\n * @category Arrangement & Analysis\n */\nexport function chordTimelineFromNotes(\n notes: NoteEvent[],\n opts: ChordTimelineOptions = {},\n): ChordTimelineResult {\n const ts = opts.ts ?? parseTimeSignature('4/4');\n assertTimeSignature(ts);\n const harmonicRhythm = opts.harmonicRhythm ?? beatsPerBar(ts);\n assertRange(harmonicRhythm, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, 'harmonic rhythm');\n const budget = opts.budget;\n assertNoteEvents(notes, 'timeline notes', { allowNonPositiveDuration: true, budget });\n // Zero/negative-length notes never sound; drop them before any inference.\n const soundingIndex = createNoteEventIndex(\n notes.filter((note) => note.durationBeat > 0),\n { name: 'timeline notes', budget },\n );\n const sounding = soundingIndex.notes.map(({ note }) => note);\n const lastNoteEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);\n const totalBeats = opts.totalBeats ?? lastNoteEnd;\n assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, 'timeline totalBeats');\n // Weighted by duration and velocity, exactly as the per-window chord\n // histogram below is: a busy ornamental figure must not outvote the sustained\n // harmony that establishes the key.\n const key = opts.key ?? detectKeyFromNotes(sounding)[0]?.key ?? majorKey(0);\n\n const segments: ChordSegment[] = [];\n const segmentConfidence: number[] = [];\n const windowCount = Math.max(0, Math.ceil(totalBeats / harmonicRhythm - EPS));\n assertGenerationBudget(windowCount, 'timeline windows', budget);\n const windowNotes: NoteEvent[][] = Array.from({ length: windowCount }, () => []);\n let memberships = 0;\n for (const indexed of soundingIndex.notes) {\n const first = Math.max(0, Math.floor(indexed.note.startBeat / harmonicRhythm));\n const lastExclusive = Math.min(windowCount, Math.ceil(indexed.endBeat / harmonicRhythm));\n memberships += Math.max(0, lastExclusive - first);\n assertGenerationBudget(memberships, 'timeline note-to-window memberships', budget);\n for (let window = first; window < lastExclusive; window += 1) {\n windowNotes[window]?.push(indexed.note);\n }\n }\n for (let i = 0; i < windowCount; i += 1) {\n const start = i * harmonicRhythm;\n const end = Math.min(start + harmonicRhythm, totalBeats);\n const inferred = analyzeWindow(windowNotes[i] ?? [], start, end, ts, key);\n if (!inferred) {\n continue;\n }\n const last = segments[segments.length - 1];\n if (last && sameChord(last.chord, inferred.chord) && Math.abs(last.endBeat - start) < EPS) {\n // Merge into the previous segment, blending confidence by duration.\n const lastLength = last.endBeat - last.startBeat;\n const length = end - start;\n const lastConfidence = segmentConfidence[segmentConfidence.length - 1] ?? 0;\n segmentConfidence[segmentConfidence.length - 1] =\n (lastConfidence * lastLength + inferred.confidence * length) / (lastLength + length);\n last.endBeat = end;\n } else {\n segments.push({ startBeat: start, endBeat: end, chord: inferred.chord });\n segmentConfidence.push(inferred.confidence);\n }\n }\n\n return {\n timeline: { at: segmentLookup(segments), segments },\n key,\n segmentConfidence,\n };\n}\n\n/**\n * A cadence found between two consecutive timeline segments.\n *\n * @category Arrangement & Analysis\n */\nexport type CadenceHit = {\n /** The beat where the cadence arrives (the second chord's onset). */\n atBeat: number;\n type: Exclude<Cadence, null>;\n from: Chord;\n to: Chord;\n};\n\n/**\n * Find cadences between consecutive segments of a chord timeline.\n *\n * Each temporally adjacent segment pair is classified with\n * {@link detectCadence}; pairs forming no cadence are skipped. Segments\n * separated by a gap (a rest in the timeline) are not a chord-to-chord\n * progression, so they are never paired.\n *\n * @param timeline The chord timeline to scan.\n * @param key The prevailing key.\n * @returns The cadences found, in time order.\n * @example\n * ```ts\n * import { chordTimelineFromNotes, detectCadences } from '@libraz/libcantus';\n * const notes = [\n * { pitch: 67, startBeat: 0, durationBeat: 4 }, // G, a dominant\n * { pitch: 60, startBeat: 4, durationBeat: 4 }, // C, the tonic\n * ];\n * const { timeline, key } = chordTimelineFromNotes(notes);\n * const cadences = detectCadences(timeline, key);\n * ```\n * @category Arrangement & Analysis\n */\nexport function detectCadences(timeline: ChordTimeline, key: KeyScale): CadenceHit[] {\n const hits: CadenceHit[] = [];\n for (let i = 1; i < timeline.segments.length; i += 1) {\n const prev = timeline.segments[i - 1];\n const cur = timeline.segments[i];\n if (!prev || !cur) {\n continue;\n }\n if (Math.abs(cur.startBeat - prev.endBeat) > EPS) {\n continue; // A rest separates the chords; no cadential motion across it.\n }\n const type = detectCadence(prev.chord, cur.chord, key);\n if (type !== null) {\n hits.push({ atBeat: cur.startBeat, type, from: prev.chord, to: cur.chord });\n }\n }\n return hits;\n}\n","/**\n * Shared helpers for the arrangement-analysis modules: pooling every track's\n * sounding notes and partitioning a track into monophonic sub-voices, plus the\n * small geometric primitives both the track-role and tension passes rely on.\n */\n\nimport type { NoteEvent } from '../../core/types.js';\nimport type { IdentifiedVoiceNote } from '../voice/index.js';\nimport type { ArrangementTrack, TrackRole } from './tracks.js';\n\n/** Float tolerance for beat boundary comparisons. */\nexport const EPS = 1e-9;\n\n/**\n * Largest interval, in semitones, a sub-voice will span between consecutive\n * notes. Beyond two octaves the connection is not a melodic step or leap but an\n * artefact of lane packing, so a new sub-voice is opened instead.\n */\nconst MAX_LANE_LEAP = 24;\n\n/** A note with the pitch that immediately preceded it in the same sub-voice. */\nexport type PreparedNote = {\n pitch: number;\n prevPitch?: number;\n startBeat: number;\n endBeat: number;\n};\n\n/** One monophonic sub-voice of a track: ordered notes plus sounding spans. */\nexport type PreparedVoice = {\n /** The sub-voice's notes in time order, each with a stable id. */\n voice: IdentifiedVoiceNote[];\n /** The same notes as sounding spans, each carrying its predecessor's pitch. */\n sounding: PreparedNote[];\n /**\n * The span sounding at a beat, or undefined. A sub-voice is monophonic by\n * construction, so at most one span covers any beat and the lookup is a\n * single binary search rather than a scan.\n */\n at: (beat: number) => PreparedNote | undefined;\n};\n\n/** A track prepared for analysis: resolved metadata plus monophonic sub-voices. */\nexport type PreparedTrack = {\n name: string;\n role: TrackRole;\n /** The track's index in the caller's input array. */\n trackIndex: number;\n /**\n * The track's notes partitioned into monophonic sub-voices. Voice analysis\n * assumes one note at a time, so a polyphonic track (e.g. a 'harmony' track\n * playing block chords) is split here; a monophonic track yields exactly one\n * sub-voice.\n */\n voices: PreparedVoice[];\n};\n\n/** Whether a sounding span `[start, end)` covers a beat. */\nexport function covers(note: PreparedNote, beat: number): boolean {\n return beat >= note.startBeat - EPS && beat < note.endBeat - EPS;\n}\n\n/** Whether a track's role means it carries no harmonic content. */\nexport function isPercussion(role: TrackRole | undefined): boolean {\n return role === 'drums';\n}\n\n/**\n * Every sounding note of every pitched track pooled into one flat list. Zero-\n * and negative-length notes never sound, so they are dropped here (matching the\n * chord-inference ingest in the timeline module), and percussion tracks are\n * skipped entirely: their pitches are instrument selections, not harmony.\n */\nexport function poolNotes(tracks: ArrangementTrack[], only?: ReadonlySet<number>): NoteEvent[] {\n const notes: NoteEvent[] = [];\n for (let index = 0; index < tracks.length; index += 1) {\n const track = tracks[index];\n if (track === undefined || isPercussion(track.role)) {\n continue;\n }\n if (only !== undefined && !only.has(index)) {\n continue;\n }\n for (const note of track.notes) {\n if (note.durationBeat > 0) {\n notes.push(note);\n }\n }\n }\n return notes;\n}\n\n/**\n * Split a track's ordered notes into monophonic sub-voices.\n *\n * Each note joins the free sub-voice — one whose last note has ended — whose\n * last pitch is nearest its own, so a track whose voices cross keeps each voice\n * on its own lane instead of handing a low note to whichever lane happened to\n * be created first. Ties go to the lane that ended latest, which is the one the\n * note actually follows. A connection wider than {@link MAX_LANE_LEAP} is not a\n * melodic one, so it opens a new sub-voice instead.\n *\n * The predecessor pitch this produces feeds leap and parallel detection, so a\n * mis-assigned lane invents voice-leading faults that are not in the music.\n */\nfunction splitIntoSubVoices(ordered: IdentifiedVoiceNote[]): IdentifiedVoiceNote[][] {\n const lanes: IdentifiedVoiceNote[][] = [];\n for (const note of ordered) {\n let best = -1;\n let bestDistance = Number.POSITIVE_INFINITY;\n let bestEnd = Number.NEGATIVE_INFINITY;\n for (let lane = 0; lane < lanes.length; lane += 1) {\n const last = lanes[lane]?.[(lanes[lane]?.length ?? 0) - 1];\n if (last === undefined) {\n continue;\n }\n const end = last.startBeat + last.durationBeat;\n if (end > note.startBeat + EPS) {\n continue; // still sounding, so the lane is not free\n }\n const distance = Math.abs(last.pitch - note.pitch);\n if (distance > MAX_LANE_LEAP) {\n continue;\n }\n if (distance < bestDistance || (distance === bestDistance && end > bestEnd)) {\n best = lane;\n bestDistance = distance;\n bestEnd = end;\n }\n }\n if (best >= 0) {\n lanes[best]?.push(note);\n } else {\n lanes.push([note]);\n }\n }\n return lanes;\n}\n\n/** Index of the last span starting at or before a beat, or -1. */\nfunction lastStartingAtOrBefore(sounding: PreparedNote[], beat: number): number {\n let low = 0;\n let high = sounding.length;\n while (low < high) {\n const middle = Math.floor((low + high) / 2);\n if ((sounding[middle]?.startBeat ?? Number.POSITIVE_INFINITY) <= beat + EPS) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n return low - 1;\n}\n\n/**\n * Resolve each track's metadata and derive its monophonic sub-voices.\n *\n * Notes are sorted by onset, then pitch, so ordering is deterministic, and ids\n * are assigned in that order; each note keeps the index it had in the caller's\n * array so annotations can be mapped back. Zero- and negative-length notes never\n * sound and are dropped at ingest. Each track is then partitioned into\n * monophonic sub-voices (see {@link splitIntoSubVoices}); the predecessor pitch\n * used for suspension and parallel detection is the previous note of the same\n * sub-voice, never a simultaneous chord member.\n */\nexport function prepareTracks(tracks: ArrangementTrack[]): PreparedTrack[] {\n let nextId = 0;\n return tracks.map((track, index) => {\n const ordered: IdentifiedVoiceNote[] = track.notes\n .map((note, originalIndex) => ({ note, originalIndex }))\n .filter(({ note }) => note.durationBeat > 0)\n .sort((a, b) => a.note.startBeat - b.note.startBeat || a.note.pitch - b.note.pitch)\n .map(({ note, originalIndex }) => ({ ...note, id: nextId++, originalIndex }));\n const voices: PreparedVoice[] = splitIntoSubVoices(ordered).map((voice) => {\n const sounding: PreparedNote[] = voice.map((note, i) => {\n const prev = i > 0 ? voice[i - 1] : undefined;\n const prevEnd = prev === undefined ? undefined : prev.startBeat + prev.durationBeat;\n const contiguous = prevEnd !== undefined && Math.abs(prevEnd - note.startBeat) <= EPS;\n return {\n pitch: note.pitch,\n prevPitch: contiguous ? prev?.pitch : undefined,\n startBeat: note.startBeat,\n endBeat: note.startBeat + note.durationBeat,\n };\n });\n return {\n voice,\n sounding,\n at: (beat: number) => {\n const candidate = sounding[lastStartingAtOrBefore(sounding, beat)];\n return candidate !== undefined && covers(candidate, beat) ? candidate : undefined;\n },\n };\n });\n return {\n name: track.name ?? `track ${index + 1}`,\n role: track.role ?? 'other',\n trackIndex: index,\n voices,\n };\n });\n}\n","/**\n * Arrangement tension analysis: a coarse harmonic-tension curve sampled at\n * regular beats across a whole multi-track piece, combining the sounding\n * chord's harmonic function, the dissonance of the sounding notes, and their\n * registral span into a single normalized reading.\n */\n\nimport { isStrongBeat, parseTimeSignature, type TimeSignature } from '../../core/meter/index.js';\nimport { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport {\n assertGenerationBudget,\n assertNoteEvents,\n assertRange,\n assertTimeSignature,\n} from '../../core/validation/index.js';\nimport { chordPitchClasses } from '../../theory/chord/index.js';\nimport { evaluateSafety, NoteSafety, type SafetyProfile } from '../../theory/safety/index.js';\nimport { majorKey } from '../../theory/scale/index.js';\nimport { detectKeyFromNotes } from '../detect/index.js';\nimport { functionOf } from '../functional/index.js';\nimport { type ChordTimeline, chordTimelineFromNotes } from '../timeline/index.js';\nimport { EPS, isPercussion, type PreparedTrack, poolNotes, prepareTracks } from './internal.js';\nimport type { ArrangementAnalysis, ArrangementOptions, ArrangementTrack } from './tracks.js';\n\n/**\n * A tension reading sampled at a beat.\n *\n * @category Arrangement & Analysis\n */\nexport type TensionPoint = {\n beat: number;\n /** Combined tension in [0, 1]. */\n tension: number;\n};\n\n/** Tension contributed by the sounding chord's harmonic function. */\nconst FUNCTION_TENSION: Record<'tonic' | 'subdominant' | 'dominant', number> = {\n tonic: 0,\n subdominant: 0.5,\n dominant: 1,\n};\n\n/** Weight of the harmonic-function term in the combined tension score. */\nconst FUNCTION_WEIGHT = 0.5;\n/** Weight of the non-chord-tone / dissonance term. */\nconst DISSONANCE_WEIGHT = 0.35;\n/** Weight of the registral-span term. */\nconst SPAN_WEIGHT = 0.15;\n/** Pitch span, in semitones, that saturates the span term. */\nconst SPAN_SATURATION = 24;\n\n/** Clamp a value into [0, 1]. */\nfunction clamp01(value: number): number {\n return Math.min(1, Math.max(0, value));\n}\n\n/**\n * Sample the harmonic tension of an arrangement at regular beats.\n *\n * At each sample beat the tension combines three normalized terms:\n * - the sounding chord's harmonic function (dominant `1`, subdominant `0.5`,\n * tonic or no chord `0`), weighted `FUNCTION_WEIGHT`;\n * - the dissonance of the sounding notes — the larger of the share of sounding\n * pitches that are not chord tones and the share that {@link evaluateSafety}\n * rates {@link NoteSafety.Dissonant} — weighted `DISSONANCE_WEIGHT`;\n * - the registral span of the sounding notes, saturating at\n * `SPAN_SATURATION` semitones, weighted `SPAN_WEIGHT`.\n *\n * The weighted sum is clamped to [0, 1]. The harmony is inferred from all tracks\n * pooled together, so the result is deterministic and self-contained.\n *\n * @param tracks The tracks to sample.\n * @param opts Analysis options plus an optional `step` (default one beat).\n * @returns One {@link TensionPoint} per sampled beat, in beat order.\n * @throws If `step` is not positive.\n * @example\n * ```ts\n * import { tensionCurve } from '@libraz/libcantus';\n * const notes = [{ pitch: 60, startBeat: 0, durationBeat: 4 }];\n * const curve = tensionCurve([{ notes }], { step: 1 });\n * curve; // one { beat, tension } sample per beat, in beat order\n * ```\n * @category Arrangement & Analysis\n */\nexport function tensionCurve(\n tracks: ArrangementTrack[],\n opts: ArrangementOptions & { step?: number } = {},\n): TensionPoint[] {\n const ts = opts.ts ?? parseTimeSignature('4/4');\n assertTimeSignature(ts);\n const budget = opts.budget;\n assertGenerationBudget(tracks.length, 'arrangement tracks', budget);\n for (let index = 0; index < tracks.length; index += 1) {\n assertNoteEvents(tracks[index]?.notes ?? [], `tracks[${index}].notes`, {\n allowNonPositiveDuration: true,\n budget,\n });\n }\n const profile: SafetyProfile = opts.profile ?? 'pop';\n const step = opts.step ?? 1;\n assertRange(step, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, 'tension sampling step');\n\n const harmonyTracks =\n opts.harmonyTracks === undefined ? undefined : new Set(opts.harmonyTracks.map(Math.trunc));\n const pooled = poolNotes(tracks, harmonyTracks);\n const totalBeats = poolNotes(tracks).reduce(\n (end, n) => Math.max(end, n.startBeat + n.durationBeat),\n 0,\n );\n const { timeline, key } =\n opts.timeline === undefined\n ? chordTimelineFromNotes(pooled, {\n key: opts.key,\n ts,\n harmonicRhythm: opts.harmonicRhythm,\n budget,\n })\n : {\n timeline: opts.timeline,\n key: opts.key ?? detectKeyFromNotes(pooled)[0]?.key ?? majorKey(0),\n };\n\n const prepared = prepareTracks(tracks);\n const points: TensionPoint[] = [];\n const sampleCount = Math.max(0, Math.ceil(totalBeats / step - EPS));\n assertGenerationBudget(sampleCount, 'tension samples', budget);\n // The per-sample cost is one lookup per sub-voice, so it is their product —\n // not either dimension alone — that has to stay inside the budget.\n const voiceCount = prepared.reduce((sum, track) => sum + track.voices.length, 0);\n assertGenerationBudget(sampleCount * voiceCount, 'tension sample-voice lookups', budget);\n for (let i = 0; i < sampleCount; i += 1) {\n const beat = i * step;\n points.push({ beat, tension: sampleTension(prepared, timeline, key, ts, profile, beat) });\n }\n return points;\n}\n\n/**\n * Sample the tension of an arrangement already analysed by\n * {@link analyzeArrangement}, reusing its harmony instead of inferring it again.\n *\n * `analyzeArrangement` followed by `tensionCurve` runs chord and key inference\n * twice over the same notes. This takes the analysis it already produced, so\n * the curve and the annotations describe the same harmony by construction.\n *\n * @param tracks The same tracks that were analysed.\n * @param analysis The result of {@link analyzeArrangement} for those tracks.\n * @param opts Analysis options plus an optional `step` (default one beat).\n * @returns One {@link TensionPoint} per sampled beat, in beat order.\n * @example\n * ```ts\n * import { analyzeArrangement, tensionCurveFrom } from '@libraz/libcantus';\n * const tracks = [{ notes: [{ pitch: 60, startBeat: 0, durationBeat: 4 }] }];\n * const analysis = analyzeArrangement(tracks);\n * tensionCurveFrom(tracks, analysis, { step: 1 });\n * ```\n * @category Arrangement & Analysis\n */\nexport function tensionCurveFrom(\n tracks: ArrangementTrack[],\n analysis: ArrangementAnalysis,\n opts: ArrangementOptions & { step?: number } = {},\n): TensionPoint[] {\n return tensionCurve(tracks, { ...opts, timeline: analysis.timeline, key: analysis.key });\n}\n\n/**\n * All sounding pitched notes across every track at a beat, tagged with their\n * track. Percussion carries no harmony, so it is skipped; each sub-voice is\n * monophonic, so its contribution is one binary search rather than a scan.\n */\nfunction allSounding(prepared: PreparedTrack[], beat: number): { pitch: number; track: number }[] {\n const out: { pitch: number; track: number }[] = [];\n for (let t = 0; t < prepared.length; t += 1) {\n const track = prepared[t];\n if (!track || isPercussion(track.role)) {\n continue;\n }\n for (const subVoice of track.voices) {\n const note = subVoice.at(beat);\n if (note !== undefined) {\n out.push({ pitch: note.pitch, track: t });\n }\n }\n }\n return out;\n}\n\n/** Combine the harmonic, dissonance, and span terms at one sample beat. */\nfunction sampleTension(\n prepared: PreparedTrack[],\n timeline: ChordTimeline,\n key: KeyScale,\n ts: TimeSignature,\n profile: SafetyProfile,\n beat: number,\n): number {\n const sounding = allSounding(prepared, beat);\n if (sounding.length === 0) {\n return 0;\n }\n const chord = timeline.at(beat);\n const functionScore = chord ? FUNCTION_TENSION[functionOf(chord, key)] : 0;\n\n const chordPcs = chord ? new Set(chordPitchClasses(chord)) : null;\n const strongBeat = isStrongBeat(beat, ts);\n let nonChord = 0;\n let dissonant = 0;\n for (let i = 0; i < sounding.length; i += 1) {\n const voice = sounding[i];\n if (!voice) {\n continue;\n }\n // Non-chord-tone share only applies when a chord is sounding; at a timeline\n // gap there is no reference harmony, so vertical dissonance alone drives the\n // dissonance term (the chord function term is already 0 there).\n if (chordPcs && !chordPcs.has(pitchClass(voice.pitch))) {\n nonChord += 1;\n }\n const others = sounding.filter((_, j) => j !== i).map((s) => ({ pitch: s.pitch }));\n // Only the verdict is read, so the replacement-pitch search is skipped.\n const result = evaluateSafety(\n {\n profile,\n candidatePitch: voice.pitch,\n chord,\n key,\n otherVoices: others,\n strongBeat,\n },\n { suggestions: false },\n );\n if (result.safety === NoteSafety.Dissonant) {\n dissonant += 1;\n }\n }\n const dissonanceScore = Math.max(nonChord, dissonant) / sounding.length;\n\n let low = Number.POSITIVE_INFINITY;\n let high = Number.NEGATIVE_INFINITY;\n for (const voice of sounding) {\n low = Math.min(low, voice.pitch);\n high = Math.max(high, voice.pitch);\n }\n const spanScore = Math.min(1, (high - low) / SPAN_SATURATION);\n\n return clamp01(\n FUNCTION_WEIGHT * functionScore + DISSONANCE_WEIGHT * dissonanceScore + SPAN_WEIGHT * spanScore,\n );\n}\n","import { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\nimport type { KeyScale, NoteEvent } from '../../core/types.js';\nimport { assertNoteEvents } from '../../core/validation/index.js';\nimport type { Chord, ChordToneRole } from '../../theory/chord/index.js';\nimport { chordToneRole, intervalAboveRoot, isChordMember } from '../../theory/chord/index.js';\nimport {\n createsVerticalDissonance,\n isLeadingToneResolution,\n} from '../../theory/counterpoint/index.js';\nimport type { VoiceSnapshot } from '../../theory/safety/index.js';\n\n/**\n * A suspension figure, named by the interval above the bass and the interval it\n * resolves to.\n *\n * @category Arrangement & Analysis\n */\nexport type SuspensionFigure = 'sus4-3' | 'sus6-5' | 'sus7-6' | 'sus9-8' | 'sus2-3';\n\n/**\n * A theory annotation attached to a note.\n *\n * @category Arrangement & Analysis\n */\nexport type TheoryLabel =\n | { kind: 'chordTone'; role: ChordToneRole }\n | { kind: 'tension'; degree: 9 | 11 | 13 }\n | { kind: 'avoid' }\n | { kind: 'passing' }\n | { kind: 'neighbor' }\n | { kind: 'suspension'; type: SuspensionFigure; resolveTo: number }\n | { kind: 'anticipation' }\n | { kind: 'escape' }\n | { kind: 'needsResolution'; resolveTo: number }\n | { kind: 'leadingTone'; resolveTo: number };\n\n/**\n * A note with its theory labels and a short rationale.\n *\n * The labels describe the note against the chord sounding at its **onset**;\n * they are not re-derived for the later chords a long note sustains across.\n * `analyzeArrangement` does re-check every crossed chord change, and reports\n * what it finds as conflicts, so a note labelled a chord tone here can still\n * appear in `conflicts` at a later beat.\n *\n * The identity fields make an annotation mappable back to the caller's input:\n * `originalIndex` is the note's position in the array that was passed in, and\n * `trackIndex` is set by {@link analyzeArrangement}.\n *\n * @category Arrangement & Analysis\n */\nexport type AnalyzedNote = {\n noteId: number;\n /** The note's index in the caller's own array, when the caller supplied one. */\n originalIndex?: number;\n /** The note's track, set by `analyzeArrangement`. */\n trackIndex?: number;\n pitch: number;\n startBeat: number;\n durationBeat: number;\n labels: TheoryLabel[];\n rationale?: string;\n};\n\n/**\n * A single note in a monophonic voice: a {@link NoteEvent} with a stable id and,\n * optionally, its index in the caller's original array.\n *\n * @category Arrangement & Analysis\n */\nexport type VoiceNote = NoteEvent & { id?: number; originalIndex?: number };\n\n/**\n * A voice note whose id has been assigned, as {@link toVoiceNotes} returns and\n * {@link analyzeArrangement} works in.\n *\n * @category Arrangement & Analysis\n */\nexport type IdentifiedVoiceNote = VoiceNote & { id: number };\n\n/**\n * Give plain note events the ids {@link analyzeVoice} reports back.\n *\n * The id is only an identity handle, so the array position serves; this exists\n * so the mapping is a named call rather than a `map` every caller writes.\n *\n * @param events The note events, in time order.\n * @returns The same notes as voice notes, each carrying its index as its id.\n * @category Arrangement & Analysis\n */\nexport function toVoiceNotes(events: readonly NoteEvent[]): IdentifiedVoiceNote[] {\n return events.map((event, index) => ({ ...event, id: index, originalIndex: index }));\n}\n\n/** Float tolerance for beat comparisons. */\nconst EPS = 1e-9;\n\n/**\n * Interval class of a pitch above the actual sounding bass.\n *\n * Suspension figures (4-3, 7-6, 9-8) are named for the interval above the bass,\n * not the chord root, so this prefers the lowest pitch among the other sounding\n * voices when one lies below the note. Without a lower sounding voice it falls\n * back to the chord's bass pitch class (or root in root position).\n */\nfunction intervalAboveBass(pitch: number, others: VoiceSnapshot[], chord: Chord): number {\n let bass = Number.POSITIVE_INFINITY;\n for (const ov of others) {\n bass = Math.min(bass, ov.pitch);\n }\n if (Number.isFinite(bass) && bass < pitch) {\n return (((pitch - bass) % 12) + 12) % 12;\n }\n const ref = chord.bassPc ?? chord.rootPc;\n return (((pitchClass(pitch) - pitchClass(ref)) % 12) + 12) % 12;\n}\n\n/**\n * Map an interval class above the root to its extended-tension scale degree:\n * a flat/natural ninth (1 or 2) to 9, an eleventh (5 or 6) to 11, and a\n * thirteenth (8 or 9) to 13.\n */\nfunction tensionDegree(ic: number): 9 | 11 | 13 {\n if (ic === 1 || ic === 2) {\n return 9;\n }\n if (ic === 5 || ic === 6) {\n return 11;\n }\n return 13;\n}\n\nfunction isStep(a: number, b: number): boolean {\n const d = Math.abs(a - b);\n return d === 1 || d === 2;\n}\n\n/**\n * Classify a suspension figure from the interval class above the sounding bass\n * (`ic`) and the resolution direction (`delta`, positive when resolving upward).\n */\nfunction suspensionType(ic: number, delta: number): SuspensionFigure | null {\n if (delta > 0) {\n return 'sus2-3';\n }\n if (ic === 2) {\n return 'sus9-8';\n }\n if (ic === 10 || ic === 11) {\n return 'sus7-6';\n }\n if (ic === 5 || ic === 6) {\n return 'sus4-3';\n }\n if (ic === 8 || ic === 9) {\n return 'sus6-5';\n }\n // Any other interval above the bass is a downward-resolving dissonance with\n // no standard figure; a catch-all `sus4-3` would name it after an interval it\n // does not form.\n return null;\n}\n\nfunction stepResolution(pitch: number, chord: Chord): number | undefined {\n for (let delta = 1; delta <= 2; delta += 1) {\n if (isChordMember(pitch - delta, chord)) {\n return pitch - delta;\n }\n }\n return undefined;\n}\n\n/**\n * Label every note of a voice with its theory roles.\n *\n * Each note is classified against the chord sounding at its beat. Chord tones\n * get a role label; non-chord tones are matched, in order, as suspensions\n * (prepared by an identical consonant pitch and resolving by step), passing\n * tones, neighbors, anticipations, and escape tones, then fall back to tension,\n * avoid, or an unresolved-dissonance label. Leading-tone resolutions are noted\n * additionally.\n *\n * The voice is expected to be monophonic (one note at a time). Notes sharing an\n * onset are treated as simultaneous cluster members, not melodic neighbors, so\n * they receive no melodic labels (suspension, passing, neighbor, anticipation,\n * escape) — only harmonic ones. Callers with truly polyphonic material should\n * split it into monophonic sub-voices first, as `analyzeArrangement` does.\n *\n * @param voice The monophonic voice, in time order. A note without an `id` is\n * identified by its position, so plain note events can be passed straight in.\n * @param chordAtBeat Chord sounding at a given beat, or null.\n * @param key Key context for leading-tone detection.\n * @param otherVoicesAtBeat Other sounding voices at a given beat; defaults to\n * none, which is the whole story for a solo line.\n * @returns One annotation per input note.\n * @example\n * ```ts\n * import { analyzeVoice, makeChord, majorKey } from '@libraz/libcantus';\n * const voice = [\n * { pitch: 60, startBeat: 0, durationBeat: 1 },\n * { pitch: 62, startBeat: 1, durationBeat: 1 },\n * { pitch: 64, startBeat: 2, durationBeat: 1 },\n * ];\n * const cMajor = makeChord(0, 'maj');\n * const labels = analyzeVoice(voice, () => cMajor, majorKey(0));\n * labels; // one AnalyzedNote per input note, in the same order\n * ```\n * @category Arrangement & Analysis\n */\nexport function analyzeVoice(\n voice: readonly VoiceNote[],\n chordAtBeat: (beat: number) => Chord | null,\n key: KeyScale,\n otherVoicesAtBeat: (beat: number) => VoiceSnapshot[] = () => [],\n): AnalyzedNote[] {\n assertNoteEvents(voice, 'voice notes', { allowNonPositiveDuration: true });\n const result: AnalyzedNote[] = [];\n\n for (let i = 0; i < voice.length; i += 1) {\n const note = voice[i];\n if (!note) {\n continue;\n }\n const prevNote = i > 0 ? voice[i - 1] : undefined;\n const nextNote = i + 1 < voice.length ? voice[i + 1] : undefined;\n // A neighboring note sharing this note's onset is a simultaneous cluster\n // member, not a melodic predecessor/successor; treating it as one would\n // fabricate passing/suspension figures, so it is dropped here.\n const prevEnd =\n prevNote === undefined\n ? Number.NEGATIVE_INFINITY\n : prevNote.startBeat + prevNote.durationBeat;\n const noteEnd = note.startBeat + note.durationBeat;\n const prev =\n prevNote !== undefined && Math.abs(prevEnd - note.startBeat) <= EPS ? prevNote : undefined;\n const next =\n nextNote !== undefined && Math.abs(noteEnd - nextNote.startBeat) <= EPS\n ? nextNote\n : undefined;\n const chord = chordAtBeat(note.startBeat);\n const labels: TheoryLabel[] = [];\n const member = isChordMember(note.pitch, chord);\n let handled = false;\n\n if (chord && member) {\n const role = chordToneRole(note.pitch, chord);\n if (role) {\n labels.push({ kind: 'chordTone', role });\n } else {\n const ic = intervalAboveRoot(note.pitch, chord);\n labels.push({ kind: 'tension', degree: tensionDegree(ic) });\n }\n handled = true;\n }\n\n if (!handled && chord && prev && next) {\n const prevChord = chordAtBeat(prev.startBeat);\n const prepared = prev.pitch === note.pitch && isChordMember(prev.pitch, prevChord);\n const resolves = isStep(next.pitch, note.pitch);\n const others = otherVoicesAtBeat(note.startBeat);\n const verticallyDissonant = others.some((ov) =>\n createsVerticalDissonance(note.pitch, ov.pitch, true),\n );\n if (prepared && verticallyDissonant && resolves) {\n const ic = intervalAboveBass(note.pitch, others, chord);\n const type = suspensionType(ic, next.pitch - note.pitch);\n if (type !== null) {\n labels.push({ kind: 'suspension', type, resolveTo: next.pitch });\n handled = true;\n }\n }\n }\n\n if (!handled && chord && prev && next) {\n const prevMember = isChordMember(prev.pitch, chordAtBeat(prev.startBeat));\n const nextMember = isChordMember(next.pitch, chordAtBeat(next.startBeat));\n const up = note.pitch - prev.pitch > 0;\n const contInto = next.pitch - note.pitch > 0;\n const sameDir = note.pitch !== prev.pitch && next.pitch !== note.pitch && up === contInto;\n if (\n prevMember &&\n nextMember &&\n !member &&\n isStep(note.pitch, prev.pitch) &&\n isStep(next.pitch, note.pitch) &&\n sameDir\n ) {\n labels.push({ kind: 'passing' });\n handled = true;\n }\n }\n\n if (\n !handled &&\n chord &&\n prev &&\n next &&\n !member &&\n prev.pitch === next.pitch &&\n isChordMember(prev.pitch, chordAtBeat(prev.startBeat)) &&\n isStep(note.pitch, prev.pitch)\n ) {\n labels.push({ kind: 'neighbor' });\n handled = true;\n }\n\n if (!handled && chord && next && !member && note.pitch === next.pitch) {\n const nextChord = chordAtBeat(next.startBeat);\n if (isChordMember(next.pitch, nextChord)) {\n labels.push({ kind: 'anticipation' });\n handled = true;\n }\n }\n\n if (!handled && chord && prev && next && !member) {\n const prevMember = isChordMember(prev.pitch, chordAtBeat(prev.startBeat));\n const nextMember = isChordMember(next.pitch, chordAtBeat(next.startBeat));\n const stepFromPrev = isStep(note.pitch, prev.pitch);\n const leapToNext = Math.abs(next.pitch - note.pitch) >= 3;\n const opposite = note.pitch - prev.pitch > 0 !== next.pitch - note.pitch > 0;\n if (prevMember && nextMember && stepFromPrev && leapToNext && opposite) {\n labels.push({ kind: 'escape' });\n handled = true;\n }\n }\n\n if (!handled && chord && !member) {\n const ic = intervalAboveRoot(note.pitch, chord);\n const isTension = ic === 2 || ic === 5 || ic === 9;\n const avoid =\n (ic === 5 && chord.intervals.includes(4) && !chord.intervals.includes(5)) ||\n (ic === 11 && chord.intervals.includes(4) && chord.intervals.includes(10));\n if (avoid) {\n labels.push({ kind: 'avoid' });\n const resolveTo = stepResolution(note.pitch, chord);\n if (resolveTo !== undefined) {\n labels.push({ kind: 'needsResolution', resolveTo });\n }\n } else if (isTension) {\n labels.push({ kind: 'tension', degree: tensionDegree(ic) });\n } else {\n const resolveTo = stepResolution(note.pitch, chord);\n if (resolveTo !== undefined) {\n labels.push({ kind: 'needsResolution', resolveTo });\n }\n }\n }\n\n if (next && isLeadingToneResolution(note.pitch, next.pitch, key)) {\n labels.push({ kind: 'leadingTone', resolveTo: next.pitch });\n }\n\n const analyzed: AnalyzedNote = {\n noteId: note.id ?? i,\n pitch: note.pitch,\n startBeat: note.startBeat,\n durationBeat: note.durationBeat,\n labels,\n rationale: describe(labels),\n };\n if (note.originalIndex !== undefined) {\n analyzed.originalIndex = note.originalIndex;\n }\n result.push(analyzed);\n }\n\n return result;\n}\n\n/** Build a short rationale from a note's primary label. */\nfunction describe(labels: TheoryLabel[]): string {\n const primary = labels[0];\n if (!primary) {\n return 'Unclassified note';\n }\n switch (primary.kind) {\n case 'chordTone':\n return `Chord ${primary.role}`;\n case 'tension':\n return `${primary.degree}th tension`;\n case 'avoid':\n return 'Avoid note';\n case 'passing':\n return 'Passing tone between chord tones';\n case 'neighbor':\n return 'Neighbor tone';\n case 'suspension':\n return `Suspension (${primary.type})`;\n case 'anticipation':\n return 'Anticipation of the next chord';\n case 'escape':\n return 'Escape tone';\n case 'needsResolution':\n return 'Unresolved dissonance';\n case 'leadingTone':\n return 'Leading tone resolving to the tonic';\n }\n}\n","/**\n * Arrangement track-role analysis: a single harmony is inferred from every\n * track's notes pooled together, each track is then analysed against that shared\n * harmony, and notes that clash with the sounding chord are collected as\n * conflicts.\n */\n\nimport { InvalidInputError } from '../../core/errors/index.js';\nimport { isStrongBeat, parseTimeSignature, type TimeSignature } from '../../core/meter/index.js';\nimport type { KeyScale, NoteEvent } from '../../core/types.js';\nimport {\n assertGenerationBudget,\n assertNoteEvents,\n assertTimeSignature,\n} from '../../core/validation/index.js';\nimport {\n evaluateSafety,\n NoteSafety,\n type SafetyProfile,\n type VoiceSnapshot,\n} from '../../theory/safety/index.js';\nimport { majorKey } from '../../theory/scale/index.js';\nimport { detectKeyFromNotes } from '../detect/index.js';\nimport {\n type CadenceHit,\n type ChordTimeline,\n chordTimelineFromNotes,\n detectCadences,\n} from '../timeline/index.js';\nimport {\n type AnalyzedNote,\n analyzeVoice,\n type TheoryLabel,\n type VoiceNote,\n} from '../voice/index.js';\nimport { EPS, isPercussion, type PreparedTrack, poolNotes, prepareTracks } from './internal.js';\n\n/**\n * The musical role a track plays in the arrangement.\n *\n * Only `drums` changes the analysis: its pitches select instruments rather than\n * naming harmony, so such a track is excluded from chord and key inference and\n * from every voice-leading comparison, and is reported back with no\n * annotations. `harmony` is meaningful through\n * {@link ArrangementOptions.harmonyTracks}, which names the tracks the chords\n * are inferred from. The rest are labels carried through to\n * {@link TrackAnalysis.role} for the caller's own use; nothing is inferred, so\n * a track left unlabelled reports `'other'` rather than a guess.\n *\n * @category Arrangement & Analysis\n */\nexport type TrackRole = 'melody' | 'harmony' | 'bass' | 'drums' | 'other';\n\n/**\n * One input track: its notes plus optional name and role.\n *\n * @category Arrangement & Analysis\n */\nexport type ArrangementTrack = {\n name?: string;\n role?: TrackRole;\n notes: readonly NoteEvent[];\n};\n\n/**\n * A track after analysis: its resolved name, role, and per-note annotations.\n *\n * @category Arrangement & Analysis\n */\nexport type TrackAnalysis = {\n name: string;\n role: TrackRole;\n /** One {@link AnalyzedNote} per note, labelled against the inferred chords. */\n notes: AnalyzedNote[];\n};\n\n/**\n * A note that clashes with the harmony sounding beneath it.\n *\n * @category Arrangement & Analysis\n */\nexport type Conflict = {\n beat: number;\n trackName: string;\n /** The clashing track's index in the caller's input array. */\n trackIndex: number;\n /** The note's index in that track's own note array. */\n originalIndex?: number;\n /** The id the note carries in {@link TrackAnalysis.notes}. */\n noteId: number;\n pitch: number;\n /**\n * The note's theory labels at its onset, so a caller can tell a genuine clash\n * from a passing tone, a neighbour, or a prepared suspension — all of which\n * are ordinary non-chord tones that still evaluate as unsafe.\n */\n labels: TheoryLabel[];\n /** The clash severity: {@link NoteSafety.Warning} or {@link NoteSafety.Dissonant}. */\n safety: NoteSafety;\n /** {@link SafetyResult.reasons} bitmask. */\n reasons: number;\n /** Preferred stepwise resolution, when the safety evaluator provides one. */\n resolveTo?: number;\n /** Nearby fully safe replacement pitches, nearest first. */\n suggestions?: number[];\n rationale?: string;\n};\n\n/**\n * The full result of {@link analyzeArrangement}.\n *\n * @category Arrangement & Analysis\n */\nexport type ArrangementAnalysis = {\n key: KeyScale;\n timeline: ChordTimeline;\n segmentConfidence: number[];\n cadences: CadenceHit[];\n tracks: TrackAnalysis[];\n /** Notes clashing with the sounding harmony, worst severity first. */\n conflicts: Conflict[];\n};\n\n/**\n * Options controlling {@link analyzeArrangement} and {@link tensionCurve}.\n *\n * @category Arrangement & Analysis\n */\nexport type ArrangementOptions = {\n /** Key context; inferred from the pooled notes when omitted. */\n key?: KeyScale;\n /**\n * A chord timeline to analyse against, instead of inferring one from the\n * notes. Supply it to analyse a hand-written progression, or to reuse the\n * timeline of an earlier call rather than re-running chord inference.\n */\n timeline?: ChordTimeline;\n /**\n * Indices of the tracks the harmony is inferred from. Defaults to every\n * pitched track. Percussion tracks are excluded either way.\n */\n harmonyTracks?: number[];\n /**\n * Lowest severity reported in `conflicts`.\n *\n * @defaultValue {@link NoteSafety.Warning}\n */\n minSeverity?: NoteSafety;\n /**\n * Time signature; defaults to 4/4.\n *\n * @defaultValue `4/4`\n */\n ts?: TimeSignature;\n /**\n * Chord-slot length in beats; defaults to one bar of `ts`.\n *\n * @defaultValue one bar of `ts`\n */\n harmonicRhythm?: number;\n /**\n * Safety profile used for conflict detection; defaults to `pop`.\n *\n * @defaultValue `'pop'`\n */\n profile?: SafetyProfile;\n /**\n * Upper bound on the work this call may do — note counts, windows, and\n * candidate counts are each checked against it before anything is allocated.\n *\n * Raise it to analyse a piece larger than the default allows; the default is\n * {@link DEFAULT_GENERATION_BUDGET}, chosen so a runaway input fails fast\n * rather than blocking the thread.\n *\n * @defaultValue {@link DEFAULT_GENERATION_BUDGET}\n */\n budget?: number;\n};\n\n/**\n * Sounding pitches of every sub-voice at a beat, excluding one sub-voice.\n *\n * Only the sub-voice under analysis is excluded — sibling sub-voices of the\n * same track are included, so dissonant clusters inside a single polyphonic\n * track are still detected.\n */\nfunction otherVoicesSounding(\n prepared: PreparedTrack[],\n excludeTrack: number,\n excludeVoice: number,\n beat: number,\n): VoiceSnapshot[] {\n const out: VoiceSnapshot[] = [];\n for (let t = 0; t < prepared.length; t += 1) {\n const track = prepared[t];\n if (!track || isPercussion(track.role)) {\n continue;\n }\n for (let v = 0; v < track.voices.length; v += 1) {\n if (t === excludeTrack && v === excludeVoice) {\n continue;\n }\n // Each sub-voice is monophonic, so this is one binary search per voice\n // rather than a scan of every note in the arrangement.\n const note = track.voices[v]?.at(beat);\n if (note === undefined) {\n continue;\n }\n const snap: VoiceSnapshot = { pitch: note.pitch };\n // Motion reasons compare one real transition shared by both voices.\n // A voice attacking exactly here contributes its adjacent predecessor;\n // a sustained voice contributes the same pitch (oblique motion).\n const previous = Math.abs(note.startBeat - beat) <= EPS ? note.prevPitch : note.pitch;\n if (previous !== undefined) {\n snap.prevPitch = previous;\n }\n out.push(snap);\n }\n }\n return out;\n}\n\n/** Index of the first segment starting strictly after a beat. */\nfunction firstSegmentAfter(timeline: ChordTimeline, beat: number): number {\n let low = 0;\n let high = timeline.segments.length;\n while (low < high) {\n const middle = Math.floor((low + high) / 2);\n if ((timeline.segments[middle]?.startBeat ?? Number.POSITIVE_INFINITY) <= beat + EPS) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n return low;\n}\n\n/**\n * The beats at which a note must be checked against the harmony: its onset,\n * plus the onset of every later chord segment the note sustains across. Each\n * crossed segment is visited exactly once (at its own start), so a boundary\n * beat is never evaluated twice for the same note.\n *\n * Segments are in beat order, so the crossed ones are a contiguous run found by\n * binary search rather than by scanning the whole timeline for every note.\n */\nfunction evaluationBeats(note: VoiceNote, timeline: ChordTimeline): number[] {\n const noteEnd = note.startBeat + note.durationBeat;\n const beats = [note.startBeat];\n for (let i = firstSegmentAfter(timeline, note.startBeat); i < timeline.segments.length; i += 1) {\n const start = timeline.segments[i]?.startBeat;\n if (start === undefined || start >= noteEnd - EPS) {\n break;\n }\n beats.push(start);\n }\n return beats;\n}\n\n/**\n * Analyse a whole arrangement against a single inferred harmony.\n *\n * The chord timeline and key are inferred from the pooled notes of every\n * pitched track (see {@link chordTimelineFromNotes}); pooling all voices is\n * robust even when roles are absent or a track doubles the harmony, so it is\n * preferred over deriving the harmony from a subset. A track marked\n * `role: 'drums'` is excluded — its pitches name instruments, not harmony, and\n * pooling them would corrupt the key, the timeline and every reading built on\n * them. Pass `harmonyTracks` to restrict inference further, or `timeline` to\n * analyse against a progression you already have. Zero- and negative-length\n * notes never sound, so they are dropped at ingest and appear in neither the\n * annotations nor the conflicts.\n *\n * Voice analysis assumes one monophonic voice at a time, so each track is\n * first partitioned into monophonic sub-voices (a polyphonic block-chord track\n * fans out into one sub-voice per simultaneous note; a monophonic track is\n * unaffected). Each sub-voice is labelled note-by-note against the harmony\n * with {@link analyzeVoice}, seeing every other sub-voice — including siblings\n * within the same track — as its accompaniment. Every note is then re-checked\n * with {@link evaluateSafety} at its onset and again at each chord change it\n * sustains across, so a held note that clashes with a later chord is caught:\n * evaluations that are not {@link NoteSafety.Safe} become conflicts, one per\n * clashing beat, sorted worst severity first and then by beat.\n *\n * Every ordinary non-chord tone — a passing note, a neighbour, a prepared\n * suspension — is by definition unsafe against the chord under it, so the\n * conflict list is a report, not a fault list. Each conflict carries the note's\n * `labels` and its `trackIndex`/`originalIndex`, so a caller can tell those\n * apart and map a conflict back to the note it passed in; `minSeverity` narrows\n * the list to outright dissonance.\n *\n * @param tracks The tracks to analyse.\n * @param opts Analysis options; see {@link ArrangementOptions}.\n * @returns The inferred harmony, per-track annotations, cadences, and conflicts.\n * @example\n * ```ts\n * import { analyzeArrangement } from '@libraz/libcantus';\n * const melody = [\n * { pitch: 60, startBeat: 0, durationBeat: 2 },\n * { pitch: 67, startBeat: 2, durationBeat: 2 },\n * ];\n * const { key, conflicts } = analyzeArrangement([{ role: 'melody', notes: melody }]);\n * conflicts; // notes clashing with the inferred harmony, worst severity first\n * ```\n * @category Arrangement & Analysis\n */\nexport function analyzeArrangement(\n tracks: ArrangementTrack[],\n opts: ArrangementOptions = {},\n): ArrangementAnalysis {\n const ts = opts.ts ?? parseTimeSignature('4/4');\n assertTimeSignature(ts);\n const budget = opts.budget;\n assertGenerationBudget(tracks.length, 'arrangement tracks', budget);\n let noteCount = 0;\n for (let index = 0; index < tracks.length; index += 1) {\n const notes = tracks[index]?.notes;\n // A missing array is rejected by name here rather than becoming a raw\n // TypeError further down, where the message no longer says which track.\n if (!Array.isArray(notes)) {\n throw new InvalidInputError(\n `tracks[${index}].notes must be an array; received ${typeof notes}`,\n );\n }\n assertNoteEvents(notes, `tracks[${index}].notes`, {\n allowNonPositiveDuration: true,\n budget,\n });\n noteCount += notes.length;\n }\n // The pooled total is what the downstream timeline analysis actually sizes\n // its work by, so it is checked here, under this layer's own name.\n assertGenerationBudget(noteCount, 'arrangement notes', budget);\n const profile: SafetyProfile = opts.profile ?? 'pop';\n const minSeverity = opts.minSeverity ?? NoteSafety.Warning;\n const harmonyTracks =\n opts.harmonyTracks === undefined ? undefined : new Set(opts.harmonyTracks.map(Math.trunc));\n const pooled = poolNotes(tracks, harmonyTracks);\n\n const inferred =\n opts.timeline === undefined\n ? chordTimelineFromNotes(pooled, {\n key: opts.key,\n ts,\n harmonicRhythm: opts.harmonicRhythm,\n })\n : {\n timeline: opts.timeline,\n key: opts.key ?? detectKeyFromNotes(pooled)[0]?.key ?? majorKey(0),\n segmentConfidence: opts.timeline.segments.map(() => 1),\n };\n const { timeline, key, segmentConfidence } = inferred;\n const cadences = detectCadences(timeline, key);\n const prepared = prepareTracks(tracks);\n\n const trackAnalyses: TrackAnalysis[] = [];\n const conflicts: Conflict[] = [];\n\n for (let t = 0; t < prepared.length; t += 1) {\n const track = prepared[t];\n if (!track) {\n continue;\n }\n if (isPercussion(track.role)) {\n // A drum hit has no harmonic reading, so labelling it against the chord\n // would produce noise rather than analysis.\n trackAnalyses.push({ name: track.name, role: track.role, notes: [] });\n continue;\n }\n const notes: AnalyzedNote[] = [];\n for (let v = 0; v < track.voices.length; v += 1) {\n const subVoice = track.voices[v];\n if (!subVoice) {\n continue;\n }\n const analyzed = analyzeVoice(subVoice.voice, timeline.at, key, (beat) =>\n otherVoicesSounding(prepared, t, v, beat),\n );\n notes.push(...analyzed.map((note) => ({ ...note, trackIndex: track.trackIndex })));\n const labelsById = new Map(analyzed.map((note) => [note.noteId, note.labels]));\n\n for (let noteIndex = 0; noteIndex < subVoice.voice.length; noteIndex += 1) {\n const note = subVoice.voice[noteIndex];\n const preparedNote = subVoice.sounding[noteIndex];\n if (note === undefined || preparedNote === undefined) {\n continue;\n }\n for (const beat of evaluationBeats(note, timeline)) {\n const atOnset = Math.abs(beat - note.startBeat) <= EPS;\n const result = evaluateSafety({\n profile,\n candidatePitch: note.pitch,\n prevPitch: atOnset ? preparedNote.prevPitch : note.pitch,\n chord: timeline.at(beat),\n key,\n otherVoices: otherVoicesSounding(prepared, t, v, beat),\n strongBeat: isStrongBeat(beat, ts),\n });\n if (result.safety !== NoteSafety.Safe && result.safety >= minSeverity) {\n const conflict: Conflict = {\n beat,\n trackName: track.name,\n trackIndex: track.trackIndex,\n noteId: note.id,\n pitch: note.pitch,\n labels: labelsById.get(note.id) ?? [],\n safety: result.safety,\n reasons: result.reasons,\n };\n if (note.originalIndex !== undefined) {\n conflict.originalIndex = note.originalIndex;\n }\n if (result.rationale !== undefined) {\n conflict.rationale = result.rationale;\n }\n if (result.resolveTo !== undefined) {\n conflict.resolveTo = result.resolveTo;\n }\n if (result.suggestions !== undefined) {\n conflict.suggestions = [...result.suggestions];\n }\n conflicts.push(conflict);\n }\n }\n }\n }\n // Sub-voice results interleave; restore the track's onset-then-pitch order.\n notes.sort((a, b) => a.noteId - b.noteId);\n trackAnalyses.push({ name: track.name, role: track.role, notes });\n }\n\n conflicts.sort((a, b) => b.safety - a.safety || a.beat - b.beat);\n\n return { key, timeline, segmentConfidence, cadences, tracks: trackAnalyses, conflicts };\n}\n"],"mappings":"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","names":["EPS","EPS","EPS","EPS"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["/home/runner/work/libcantus/libcantus/dist/chunk-Q6OP4U43.cjs","../src/theory/safety/index.ts"],"names":["NoteSafety","ReasonFlag"],"mappings":"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","file":"/home/runner/work/libcantus/libcantus/dist/chunk-Q6OP4U43.cjs","sourcesContent":[null,"import { InvalidInputError } from '../../core/errors/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport {\n assertFiniteNumber,\n assertGenerationBudget,\n assertInteger,\n} from '../../core/validation/index.js';\nimport type { Chord } from '../chord/index.js';\nimport { intervalAboveRoot, isChordMember } from '../chord/index.js';\nimport {\n createsHiddenParallelPerfect,\n createsParallelPerfect,\n createsVerticalDissonance,\n isForbiddenMelodicLeap,\n} from '../counterpoint/index.js';\nimport { isScaleTone } from '../scale/index.js';\n\n/**\n * Severity policy: `strict` counterpoint vs. lenient `pop` voice-leading.\n *\n * @category Arrangement & Analysis\n */\nexport type SafetyProfile = 'strict' | 'pop';\n\n/**\n * Overall placeability verdict for a candidate pitch.\n *\n * @category Arrangement & Analysis\n */\nexport enum NoteSafety {\n Safe = 0,\n Warning = 1,\n Dissonant = 2,\n}\n\n/**\n * Bit flags describing why a pitch received its verdict.\n *\n * @category Arrangement & Analysis\n */\nexport enum ReasonFlag {\n ChordTone = 1 << 0,\n Tension = 1 << 1,\n AvoidNote = 1 << 2,\n ScaleTone = 1 << 3,\n NonScale = 1 << 4,\n OutOfRange = 1 << 5,\n Tritone = 1 << 6,\n LargeLeap = 1 << 7,\n MinorSecond = 1 << 8,\n MajorSeventh = 1 << 9,\n VerticalDissonance = 1 << 10,\n ParallelPerfect = 1 << 11,\n HiddenParallel = 1 << 12,\n VoiceCrossing = 1 << 13,\n Suspension = 1 << 14,\n NeedsResolution = 1 << 15,\n}\n\n/**\n * A neighbouring voice's current and previous pitch.\n *\n * @category Arrangement & Analysis\n */\nexport type VoiceSnapshot = {\n pitch: number;\n prevPitch?: number;\n};\n\n/**\n * Everything needed to evaluate one candidate pitch.\n *\n * @category Arrangement & Analysis\n */\nexport type SafetyQuery = {\n profile: SafetyProfile;\n candidatePitch: number;\n prevPitch?: number;\n chord: Chord | null;\n key: KeyScale;\n otherVoices: VoiceSnapshot[];\n strongBeat: boolean;\n vocalLow?: number;\n vocalHigh?: number;\n};\n\n/**\n * The verdict, reason flags, and optional resolution guidance.\n *\n * @category Arrangement & Analysis\n */\nexport type SafetyResult = {\n safety: NoteSafety;\n reasons: number;\n resolveTo?: number;\n suggestions?: number[];\n rationale?: string;\n};\n\n/** Semitone radius searched around a rejected candidate for safe alternatives. */\nconst SUGGESTION_WINDOW = 12;\n/** Maximum number of alternative pitches returned in `SafetyResult.suggestions`. */\nconst MAX_SUGGESTIONS = 3;\n\n/** Canonical minimum verdict contributed by each non-informational reason. */\nconst REASON_SEVERITY: ReadonlyArray<\n readonly [flag: ReasonFlag, pop: NoteSafety, strict: NoteSafety]\n> = [\n [ReasonFlag.Tension, NoteSafety.Warning, NoteSafety.Warning],\n [ReasonFlag.AvoidNote, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.ScaleTone, NoteSafety.Warning, NoteSafety.Warning],\n [ReasonFlag.NonScale, NoteSafety.Dissonant, NoteSafety.Dissonant],\n [ReasonFlag.OutOfRange, NoteSafety.Warning, NoteSafety.Warning],\n [ReasonFlag.Tritone, NoteSafety.Dissonant, NoteSafety.Dissonant],\n [ReasonFlag.LargeLeap, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.VerticalDissonance, NoteSafety.Dissonant, NoteSafety.Dissonant],\n [ReasonFlag.ParallelPerfect, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.HiddenParallel, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.VoiceCrossing, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.Suspension, NoteSafety.Dissonant, NoteSafety.Dissonant],\n [ReasonFlag.NeedsResolution, NoteSafety.Warning, NoteSafety.Dissonant],\n];\n\nfunction minimumSafetyForReasons(reasons: number, profile: SafetyProfile): NoteSafety {\n let minimum = NoteSafety.Safe;\n const severityIndex = profile === 'strict' ? 2 : 1;\n for (const entry of REASON_SEVERITY) {\n if (reasons & entry[0]) {\n minimum = Math.max(minimum, entry[severityIndex]) as NoteSafety;\n }\n }\n return minimum;\n}\n\n/** Whether the perfect fourth is an avoid note over this chord. */\nfunction avoidsFourth(chord: Chord): boolean {\n return chord.intervals.includes(4) && !chord.intervals.includes(5);\n}\n\n/** Whether the major seventh is an avoid note over this chord (dominant). */\nfunction avoidsMajorSeventh(chord: Chord): boolean {\n return chord.intervals.includes(4) && chord.intervals.includes(10);\n}\n\n/**\n * Whether a dissonance between two chord tones is one the chord is built from.\n *\n * A seventh chord states a seventh against its root and a dominant states the\n * tritone between its third and seventh; those intervals are the chord's\n * identity, not a clash to be prepared and resolved. A second between two chord\n * tones (a ninth voiced against the root, say) is a spacing problem rather than\n * a property of the harmony, so it stays reportable.\n */\nfunction isStructuralChordDissonance(a: number, b: number, chord: Chord): boolean {\n const cls = Math.abs(Math.trunc(a) - Math.trunc(b)) % 12;\n if (cls !== 6 && cls !== 10 && cls !== 11) {\n return false;\n }\n return isChordMember(a, chord) && isChordMember(b, chord);\n}\n\n/** Nearest chord tone at or below the candidate, within two semitones. */\nfunction stepResolution(pitch: number, chord: Chord): number | undefined {\n for (let delta = 1; delta <= 2; delta += 1) {\n if (isChordMember(pitch - delta, chord)) {\n return pitch - delta;\n }\n }\n return undefined;\n}\n\n/**\n * Evaluate the safety of placing a candidate pitch in a voice.\n *\n * Reasons are merged from harmonic context (chord tone / tension / avoid / scale)\n * and counterpoint (vertical dissonance, parallels, voice crossing). The `strict`\n * profile treats avoid notes and parallels as dissonant; `pop` treats them as\n * warnings. The final verdict is the worst severity among the reasons.\n *\n * @param q The candidate and its harmonic/voice-leading context.\n * @returns The verdict, reason bitmask, and optional resolution guidance.\n * @example\n * ```ts\n * import { evaluateSafety, makeChord, majorKey, NoteSafety } from '@libraz/libcantus';\n * const result = evaluateSafety({\n * profile: 'pop',\n * candidatePitch: 60, // C over a C major chord\n * chord: makeChord(0, 'maj'),\n * key: majorKey(0),\n * otherVoices: [],\n * strongBeat: true,\n * });\n * result.safety === NoteSafety.Safe; // true — C is a chord tone\n * ```\n * @category Arrangement & Analysis\n */\nexport function evaluateSafety(q: SafetyQuery, opts: EvaluateSafetyOptions = {}): SafetyResult {\n assertSafetyContext(q);\n assertFiniteNumber(q.candidatePitch, 'candidatePitch');\n return evaluateInternal(q, opts.suggestions ?? true);\n}\n\n/**\n * Options for {@link evaluateSafety}.\n *\n * @category Arrangement & Analysis\n */\nexport type EvaluateSafetyOptions = {\n /**\n * Whether to search for safe replacement pitches when the verdict is not\n * safe. Turning it off skips up to two dozen extra evaluations per rejected\n * pitch, which is worth doing when only the verdict is wanted.\n *\n * @defaultValue true\n */\n suggestions?: boolean;\n};\n\n/** Validate all numeric context fields once at a public safety entry point. */\nfunction assertSafetyContext(q: Omit<SafetyQuery, 'candidatePitch'>): void {\n if (q.prevPitch !== undefined) assertFiniteNumber(q.prevPitch, 'prevPitch');\n if (q.vocalLow !== undefined) assertFiniteNumber(q.vocalLow, 'vocalLow');\n if (q.vocalHigh !== undefined) assertFiniteNumber(q.vocalHigh, 'vocalHigh');\n if (q.vocalLow !== undefined && q.vocalHigh !== undefined && q.vocalLow > q.vocalHigh) {\n throw new InvalidInputError('vocalLow must not exceed vocalHigh');\n }\n assertGenerationBudget(q.otherVoices.length, 'other voices');\n for (let index = 0; index < q.otherVoices.length; index += 1) {\n const voice = q.otherVoices[index];\n if (!voice) continue;\n assertFiniteNumber(voice.pitch, `otherVoices[${index}].pitch`);\n if (voice.prevPitch !== undefined) {\n assertFiniteNumber(voice.prevPitch, `otherVoices[${index}].prevPitch`);\n }\n }\n}\n\n/**\n * Core safety evaluation.\n *\n * @param q The candidate and its harmonic/voice-leading context.\n * @param collectSuggestions Whether to search for safe alternatives on a\n * non-Safe verdict. Set false during that search itself to bound recursion.\n * @returns The verdict, reason bitmask, and optional guidance.\n */\nfunction evaluateInternal(q: SafetyQuery, collectSuggestions: boolean): SafetyResult {\n let reasons = 0;\n let level = NoteSafety.Safe;\n let resolveTo: number | undefined;\n const raise = (l: NoteSafety) => {\n if (l > level) {\n level = l;\n }\n };\n const avoidSeverity = q.profile === 'strict' ? NoteSafety.Dissonant : NoteSafety.Warning;\n const parallelSeverity = avoidSeverity;\n const pitch = q.candidatePitch;\n\n if (\n (q.vocalLow !== undefined && pitch < q.vocalLow) ||\n (q.vocalHigh !== undefined && pitch > q.vocalHigh)\n ) {\n reasons |= ReasonFlag.OutOfRange;\n raise(NoteSafety.Warning);\n }\n\n const inScale = isScaleTone(pitch, q.key);\n const chord = q.chord;\n\n if (chord && isChordMember(pitch, chord)) {\n reasons |= ReasonFlag.ChordTone;\n } else if (chord) {\n const ic = intervalAboveRoot(pitch, chord);\n const isTension = ic === 2 || ic === 5 || ic === 9;\n const avoid = (ic === 5 && avoidsFourth(chord)) || (ic === 11 && avoidsMajorSeventh(chord));\n if (avoid) {\n reasons |= ReasonFlag.AvoidNote;\n if (isTension) {\n reasons |= ReasonFlag.Tension;\n }\n reasons |= ReasonFlag.NeedsResolution;\n resolveTo = stepResolution(pitch, chord);\n raise(avoidSeverity);\n } else if (isTension && inScale) {\n reasons |= ReasonFlag.Tension;\n raise(NoteSafety.Warning);\n } else if (inScale) {\n reasons |= ReasonFlag.ScaleTone;\n raise(NoteSafety.Warning);\n } else {\n reasons |= ReasonFlag.NonScale;\n raise(NoteSafety.Dissonant);\n }\n } else if (inScale) {\n reasons |= ReasonFlag.ScaleTone;\n raise(NoteSafety.Warning);\n } else {\n reasons |= ReasonFlag.NonScale;\n raise(NoteSafety.Dissonant);\n }\n\n // Harmonic tritone against a chord tone. A chord tone is never flagged for a\n // tritone its own chord spells: the interval is the chord's identity, not a\n // clash with it. This covers dominant sevenths (third against seventh) and\n // equally the diminished, half-diminished, diminished-seventh and minor-sixth\n // families, where the tritone falls between two chord tones as well.\n if (chord && !isChordMember(pitch, chord)) {\n const ic = intervalAboveRoot(pitch, chord);\n const toneOffsets = chord.intervals.map((i) => ((i % 12) + 12) % 12);\n if (toneOffsets.some((t) => Math.abs(ic - t) === 6)) {\n reasons |= ReasonFlag.Tritone;\n }\n }\n\n if (q.prevPitch !== undefined) {\n // Quality flags reduce the melodic interval to its pitch-class interval so\n // compound intervals are treated uniformly with their simple form. The\n // forbidden-leap flag defers to the counterpoint rule, which preserves the\n // octave boundary (an octave leap is allowed, wider leaps are not).\n const pc = Math.abs(pitch - q.prevPitch) % 12;\n if (pc === 6) {\n reasons |= ReasonFlag.Tritone;\n raise(NoteSafety.Dissonant);\n }\n if (isForbiddenMelodicLeap(q.prevPitch, pitch)) {\n reasons |= ReasonFlag.LargeLeap;\n raise(parallelSeverity);\n }\n if (pc === 1) {\n reasons |= ReasonFlag.MinorSecond;\n }\n if (pc === 11) {\n reasons |= ReasonFlag.MajorSeventh;\n }\n }\n\n if (q.strongBeat) {\n const twoVoice = q.otherVoices.length === 1;\n const prev = q.prevPitch;\n for (const ov of q.otherVoices) {\n if (!createsVerticalDissonance(pitch, ov.pitch, twoVoice)) {\n continue;\n }\n if (chord && isStructuralChordDissonance(pitch, ov.pitch, chord)) {\n continue;\n }\n reasons |= ReasonFlag.VerticalDissonance;\n raise(NoteSafety.Dissonant);\n // A held pitch that was consonant on the previous step and is now\n // dissonant against the same voice is a prepared suspension.\n if (\n prev !== undefined &&\n pitch === prev &&\n ov.prevPitch !== undefined &&\n !createsVerticalDissonance(prev, ov.prevPitch, twoVoice)\n ) {\n reasons |= ReasonFlag.Suspension;\n }\n }\n }\n\n if (q.prevPitch !== undefined) {\n const prev = q.prevPitch;\n for (const ov of q.otherVoices) {\n if (ov.prevPitch === undefined) {\n continue;\n }\n // createsParallelPerfect covers every perfect class (unison/octave and\n // fifth), so no separate octave check is needed.\n if (createsParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {\n reasons |= ReasonFlag.ParallelPerfect;\n raise(parallelSeverity);\n }\n if (createsHiddenParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {\n reasons |= ReasonFlag.HiddenParallel;\n raise(parallelSeverity);\n }\n const crossedNow = pitch - ov.pitch;\n const crossedPrev = prev - ov.prevPitch;\n if (crossedNow !== 0 && crossedPrev !== 0 && crossedNow > 0 !== crossedPrev > 0) {\n reasons |= ReasonFlag.VoiceCrossing;\n raise(parallelSeverity);\n }\n }\n }\n\n // Keep reason production and verdict policy as one invariant even when a new\n // call path skips an earlier local `raise`: informational flags (ChordTone,\n // MinorSecond, MajorSeventh) may coexist with Safe; every policy flag above\n // contributes at least its table severity.\n raise(minimumSafetyForReasons(reasons, q.profile));\n\n const result: SafetyResult = { safety: level, reasons, rationale: describe(reasons, q) };\n if (resolveTo !== undefined) {\n result.resolveTo = resolveTo;\n }\n if (collectSuggestions && level !== NoteSafety.Safe) {\n const suggestions = findSafeNearby(q, pitch);\n if (suggestions.length > 0) {\n result.suggestions = suggestions;\n }\n }\n return result;\n}\n\n/**\n * Find safe alternative pitches near a rejected candidate.\n *\n * Searches outward from the candidate within `SUGGESTION_WINDOW` semitones,\n * probing the pitch below before the one above at each distance so the result\n * is deterministic and ordered by nearness.\n *\n * @param q The safety context of the rejected candidate.\n * @param candidate The candidate pitch that was not Safe.\n * @returns Up to `MAX_SUGGESTIONS` safe pitches, nearest first.\n */\nfunction findSafeNearby(q: SafetyQuery, candidate: number): number[] {\n const out: number[] = [];\n for (let d = 1; d <= SUGGESTION_WINDOW; d += 1) {\n for (const p of [candidate - d, candidate + d]) {\n if (evaluateInternal({ ...q, candidatePitch: p }, false).safety === NoteSafety.Safe) {\n out.push(p);\n if (out.length >= MAX_SUGGESTIONS) {\n return out;\n }\n }\n }\n }\n return out;\n}\n\n/**\n * Wording for every reason flag, most to least specific.\n *\n * The list is exhaustive by construction: {@link describe} picks from it by\n * severity, so a flag with no wording here would let a rejected pitch be\n * explained by a milder reason — the seventh of a dominant reading as a plain\n * \"Chord tone\" while its verdict says dissonant.\n */\nconst REASON_TEXT: ReadonlyArray<readonly [ReasonFlag, string]> = [\n [ReasonFlag.OutOfRange, 'Outside the target vocal range'],\n [\n ReasonFlag.Suspension,\n 'Prepared suspension — a held consonance now dissonant, awaiting resolution',\n ],\n [ReasonFlag.VerticalDissonance, 'Dissonant against a sounding voice on a strong beat'],\n [ReasonFlag.Tritone, 'Tritone against the sounding harmony'],\n [ReasonFlag.AvoidNote, 'Avoid note — clashes with a chord tone a semitone away'],\n [ReasonFlag.NonScale, 'Outside the key'],\n [ReasonFlag.ParallelPerfect, 'Parallel perfect interval with another voice'],\n [ReasonFlag.HiddenParallel, 'Hidden perfect interval by similar motion'],\n [ReasonFlag.VoiceCrossing, 'Crosses another voice'],\n [ReasonFlag.LargeLeap, 'Leap wider than the profile allows'],\n [ReasonFlag.NeedsResolution, 'Dissonance awaiting a stepwise resolution'],\n [ReasonFlag.Tension, 'Chord tension'],\n [ReasonFlag.ScaleTone, 'Scale tone, not in the chord'],\n [ReasonFlag.MinorSecond, 'A semitone from a sounding voice'],\n [ReasonFlag.MajorSeventh, 'A major seventh from a sounding voice'],\n [ReasonFlag.ChordTone, 'Chord tone'],\n];\n\n/** The severity a single flag carries under a profile; Safe when purely informational. */\nfunction severityOfReason(flag: ReasonFlag, profile: SafetyProfile): NoteSafety {\n const entry = REASON_SEVERITY.find(([candidate]) => candidate === flag);\n return entry === undefined ? NoteSafety.Safe : entry[profile === 'strict' ? 2 : 1];\n}\n\n/**\n * Build a short human explanation from the reason flags.\n *\n * The flag driving the verdict wins: the one whose severity under this profile\n * is highest, ties going to the more specific wording. Explaining a rejected\n * pitch by an informational flag it also carries would tell the caller nothing\n * about what to fix.\n */\nfunction describe(reasons: number, q: SafetyQuery): string {\n let best: string | undefined;\n let bestSeverity = -1;\n for (const [flag, text] of REASON_TEXT) {\n if (!(reasons & flag)) {\n continue;\n }\n const severity = severityOfReason(flag, q.profile);\n if (severity > bestSeverity) {\n bestSeverity = severity;\n best = text;\n }\n }\n if (best !== undefined) {\n return best;\n }\n return q.chord ? 'Placeable' : 'No chord context';\n}\n\n/**\n * Enumerate placeable pitches in a range, chord tones first, descending.\n *\n * @param q The safety context minus the candidate pitch.\n * @param pitchLow Lowest MIDI pitch to consider (inclusive).\n * @param pitchHigh Highest MIDI pitch to consider (inclusive).\n * @returns Placeable pitches (non-dissonant), chord tones before others, each group descending.\n * @throws If either bound is non-finite/non-integral, reversed, or exceeds the generation budget.\n * @example\n * ```ts\n * import { enumerateSafePitches, makeChord, majorKey } from '@libraz/libcantus';\n * const pitches = enumerateSafePitches(\n * { profile: 'pop', chord: makeChord(0, 'maj'), key: majorKey(0), otherVoices: [], strongBeat: true },\n * 60,\n * 72,\n * );\n * pitches; // placeable pitches in [60, 72], chord tones first, each group descending\n * ```\n * @category Arrangement & Analysis\n */\nexport function enumerateSafePitches(\n q: Omit<SafetyQuery, 'candidatePitch'>,\n pitchLow: number,\n pitchHigh: number,\n): number[] {\n assertSafetyContext(q);\n const chordTones: number[] = [];\n const others: number[] = [];\n assertInteger(pitchLow, 'pitchLow');\n assertInteger(pitchHigh, 'pitchHigh');\n if (pitchLow > pitchHigh) {\n throw new InvalidInputError(\n `pitchLow must not exceed pitchHigh; received ${pitchLow} > ${pitchHigh}`,\n );\n }\n assertGenerationBudget(pitchHigh - pitchLow + 1, 'safe pitch candidates');\n for (let pitch = pitchHigh; pitch >= pitchLow; pitch -= 1) {\n const result = evaluateInternal({ ...q, candidatePitch: pitch }, false);\n if (result.safety === NoteSafety.Dissonant) {\n continue;\n }\n if (result.reasons & ReasonFlag.ChordTone) {\n chordTones.push(pitch);\n } else {\n others.push(pitch);\n }\n }\n return [...chordTones, ...others];\n}\n"]}