@libraz/libcantus 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/NOTICE +63 -0
  2. package/README.md +7 -1
  3. package/README_ja.md +17 -11
  4. package/dist/analyze/index.cjs +31 -7
  5. package/dist/analyze/index.cjs.map +1 -1
  6. package/dist/analyze/index.d.cts +161 -13
  7. package/dist/analyze/index.d.ts +161 -13
  8. package/dist/analyze/index.js +37 -13
  9. package/dist/{chunk-NB25P3EH.js → chunk-2G5MCGYU.js} +33 -11
  10. package/dist/chunk-2G5MCGYU.js.map +1 -0
  11. package/dist/{chunk-NM7UAMC3.cjs → chunk-3FDBMIPR.cjs} +64 -64
  12. package/dist/chunk-3FDBMIPR.cjs.map +1 -0
  13. package/dist/{chunk-HPVSH7SF.cjs → chunk-3MDOEFIQ.cjs} +618 -954
  14. package/dist/chunk-3MDOEFIQ.cjs.map +1 -0
  15. package/dist/{chunk-FBH75DAY.js → chunk-5MWRBYAT.js} +560 -159
  16. package/dist/chunk-5MWRBYAT.js.map +1 -0
  17. package/dist/{chunk-J7FHVZII.cjs → chunk-6TTBEON3.cjs} +3074 -740
  18. package/dist/chunk-6TTBEON3.cjs.map +1 -0
  19. package/dist/{chunk-UMWZ4ZET.js → chunk-7NQ6AL3H.js} +4 -4
  20. package/dist/chunk-7NQ6AL3H.js.map +1 -0
  21. package/dist/{chunk-HWHGOVO6.cjs → chunk-E3QO3UET.cjs} +31 -23
  22. package/dist/chunk-E3QO3UET.cjs.map +1 -0
  23. package/dist/{chunk-H2KJHBW4.js → chunk-FJFULJZE.js} +2375 -191
  24. package/dist/chunk-FJFULJZE.js.map +1 -0
  25. package/dist/{chunk-Q6LFSAKV.js → chunk-FK3DGAIZ.js} +2657 -323
  26. package/dist/chunk-FK3DGAIZ.js.map +1 -0
  27. package/dist/chunk-GEDTJHAC.cjs.map +1 -1
  28. package/dist/{chunk-RFIBF2RR.js → chunk-H7FVMBUC.js} +138 -65
  29. package/dist/chunk-H7FVMBUC.js.map +1 -0
  30. package/dist/{chunk-DSMJRXCZ.cjs → chunk-HYSVSFNI.cjs} +931 -530
  31. package/dist/chunk-HYSVSFNI.cjs.map +1 -0
  32. package/dist/{chunk-35NBV2FT.cjs → chunk-KMYGRFZ6.cjs} +47 -24
  33. package/dist/chunk-KMYGRFZ6.cjs.map +1 -0
  34. package/dist/chunk-LQZQDBCD.cjs +5513 -0
  35. package/dist/chunk-LQZQDBCD.cjs.map +1 -0
  36. package/dist/{chunk-PM4XJE2Y.cjs → chunk-P3CPXAJ5.cjs} +76 -54
  37. package/dist/chunk-P3CPXAJ5.cjs.map +1 -0
  38. package/dist/{chunk-S5BKZZGM.cjs → chunk-S3QELJUE.cjs} +72 -56
  39. package/dist/chunk-S3QELJUE.cjs.map +1 -0
  40. package/dist/{chunk-PUKJP2Z5.js → chunk-TXFWSU5D.js} +31 -8
  41. package/dist/chunk-TXFWSU5D.js.map +1 -0
  42. package/dist/{chunk-76ZBTRG2.cjs → chunk-U4EQTDGN.cjs} +258 -218
  43. package/dist/chunk-U4EQTDGN.cjs.map +1 -0
  44. package/dist/chunk-UGLFDG7L.cjs.map +1 -1
  45. package/dist/{chunk-HYJJ2V2I.js → chunk-UTE5A36R.js} +483 -819
  46. package/dist/chunk-UTE5A36R.js.map +1 -0
  47. package/dist/{chunk-NFJ4P6ZZ.js → chunk-UZKQISRF.js} +17 -9
  48. package/dist/chunk-UZKQISRF.js.map +1 -0
  49. package/dist/{chunk-7KG3RTYS.js → chunk-VL5NXVCN.js} +281 -155
  50. package/dist/chunk-VL5NXVCN.js.map +1 -0
  51. package/dist/{chunk-CGBADIO5.cjs → chunk-WOMCXN2W.cjs} +328 -202
  52. package/dist/chunk-WOMCXN2W.cjs.map +1 -0
  53. package/dist/{chunk-3R3QVKY5.cjs → chunk-YDGMWH43.cjs} +397 -324
  54. package/dist/chunk-YDGMWH43.cjs.map +1 -0
  55. package/dist/{chunk-AGOVW7U6.js → chunk-YYVXRV3V.js} +72 -56
  56. package/dist/chunk-YYVXRV3V.js.map +1 -0
  57. package/dist/{chunk-M5RYMAFI.js → chunk-ZLNSH4UL.js} +71 -31
  58. package/dist/chunk-ZLNSH4UL.js.map +1 -0
  59. package/dist/{coerce-DRERzdve.d.cts → coerce-CUhHc3Tu.d.ts} +16 -8
  60. package/dist/{coerce-DP0vYu-x.d.ts → coerce-Ytt2Eiq9.d.cts} +16 -8
  61. package/dist/core/index.cjs +5 -5
  62. package/dist/core/index.cjs.map +1 -1
  63. package/dist/core/index.d.cts +9 -8
  64. package/dist/core/index.d.ts +9 -8
  65. package/dist/core/index.js +4 -4
  66. package/dist/generate/index.cjs +20 -8
  67. package/dist/generate/index.cjs.map +1 -1
  68. package/dist/generate/index.d.cts +105 -17
  69. package/dist/generate/index.d.ts +105 -17
  70. package/dist/generate/index.js +19 -7
  71. package/dist/{index-CL4b4gUQ.d.ts → index-5VJgKz_Q.d.cts} +6 -2
  72. package/dist/{index-B9QLziDe.d.cts → index-BLuDesVG.d.ts} +20 -13
  73. package/dist/{index-BHIUyjhZ.d.ts → index-BV6APVa6.d.cts} +20 -13
  74. package/dist/{index-BMweUlm5.d.cts → index-Bf0Sv8a9.d.ts} +6 -2
  75. package/dist/{index-Dw2DP7vm.d.ts → index-BkT21EMv.d.cts} +3 -2
  76. package/dist/{index-BL5N2Oyz.d.cts → index-C7KCaTRz.d.ts} +3 -2
  77. package/dist/{index-Cy15oTvh.d.ts → index-C9MYbuKS.d.cts} +7 -3
  78. package/dist/{index-Cy15oTvh.d.cts → index-C9MYbuKS.d.ts} +7 -3
  79. package/dist/{index-BNSM-DSk.d.cts → index-DVInzCDS.d.cts} +533 -28
  80. package/dist/{index-9eoQs2C1.d.ts → index-Dg-SAfhz.d.ts} +6 -4
  81. package/dist/{index-BJgP97N1.d.ts → index-jgeug9EA.d.ts} +533 -28
  82. package/dist/{index-cHvbYgJI.d.cts → index-pQihL4z7.d.cts} +6 -4
  83. package/dist/index.cjs +49 -13
  84. package/dist/index.cjs.map +1 -1
  85. package/dist/index.d.cts +13 -13
  86. package/dist/index.d.ts +13 -13
  87. package/dist/index.js +55 -19
  88. package/dist/{masks-Cc7-MzzY.d.ts → masks-BnzXnBj0.d.cts} +3 -2
  89. package/dist/{masks-pADfTOiD.d.cts → masks-tycmkrgs.d.ts} +3 -2
  90. package/dist/model/index.cjs +12 -12
  91. package/dist/model/index.cjs.map +1 -1
  92. package/dist/model/index.d.cts +259 -48
  93. package/dist/model/index.d.ts +259 -48
  94. package/dist/model/index.js +11 -11
  95. package/dist/{positional-DJgs6ltD.d.ts → positional-CscpxtlE.d.cts} +3 -2
  96. package/dist/{positional-DMZer-FM.d.cts → positional-DLnfUgqG.d.ts} +3 -2
  97. package/dist/{index-BRMbPkHE.d.ts → profile-B4WqcM4H.d.ts} +85 -610
  98. package/dist/{index-DGCNKAUr.d.cts → profile-BJd1XkPB.d.cts} +85 -610
  99. package/dist/theory/index.cjs +5 -5
  100. package/dist/theory/index.cjs.map +1 -1
  101. package/dist/theory/index.d.cts +18 -14
  102. package/dist/theory/index.d.ts +18 -14
  103. package/dist/theory/index.js +4 -4
  104. package/dist/{index-CJBBiAmU.d.ts → types-7mGFwvMm.d.ts} +1210 -31
  105. package/dist/{index-BBWNZS0f.d.cts → types-_ofpb2be.d.cts} +1210 -31
  106. package/package.json +13 -13
  107. package/dist/chunk-35NBV2FT.cjs.map +0 -1
  108. package/dist/chunk-3R3QVKY5.cjs.map +0 -1
  109. package/dist/chunk-76ZBTRG2.cjs.map +0 -1
  110. package/dist/chunk-7KG3RTYS.js.map +0 -1
  111. package/dist/chunk-AGOVW7U6.js.map +0 -1
  112. package/dist/chunk-CGBADIO5.cjs.map +0 -1
  113. package/dist/chunk-DSMJRXCZ.cjs.map +0 -1
  114. package/dist/chunk-FBH75DAY.js.map +0 -1
  115. package/dist/chunk-H2KJHBW4.js.map +0 -1
  116. package/dist/chunk-HPVSH7SF.cjs.map +0 -1
  117. package/dist/chunk-HWHGOVO6.cjs.map +0 -1
  118. package/dist/chunk-HYJJ2V2I.js.map +0 -1
  119. package/dist/chunk-J7FHVZII.cjs.map +0 -1
  120. package/dist/chunk-M5RYMAFI.js.map +0 -1
  121. package/dist/chunk-NB25P3EH.js.map +0 -1
  122. package/dist/chunk-NFJ4P6ZZ.js.map +0 -1
  123. package/dist/chunk-NM7UAMC3.cjs.map +0 -1
  124. package/dist/chunk-PM4XJE2Y.cjs.map +0 -1
  125. package/dist/chunk-PUKJP2Z5.js.map +0 -1
  126. package/dist/chunk-Q6LFSAKV.js.map +0 -1
  127. package/dist/chunk-RFIBF2RR.js.map +0 -1
  128. package/dist/chunk-S5BKZZGM.cjs.map +0 -1
  129. package/dist/chunk-SGLEQVJH.cjs +0 -3329
  130. package/dist/chunk-SGLEQVJH.cjs.map +0 -1
  131. package/dist/chunk-UMWZ4ZET.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/libcantus/libcantus/ts/dist/chunk-YDGMWH43.cjs","../src/theory/scale/masks.ts","../src/theory/scale/scales.ts","../src/theory/scale/name.ts","../src/theory/scale/coerce.ts","../src/theory/scale/degrees.ts","../src/theory/scale/signature.ts","../src/theory/scale/relations.ts","../src/theory/scale/identity.ts","../src/theory/scale/key.ts","../src/theory/scale/system.ts","../src/theory/chord/spec.ts","../src/theory/chord/index.ts","../src/theory/spelling/index.ts","../src/theory/counterpoint/internal.ts","../src/theory/counterpoint/independence.ts","../src/theory/counterpoint/index.ts","../src/theory/safety/index.ts","../src/theory/symbol/grammar.ts","../src/theory/symbol/suffix.ts","../src/theory/symbol/spelling.ts","../src/theory/symbol/index.ts","../src/theory/symbol/coerce.ts","../src/theory/tendency/index.ts"],"names":["spelled","keySpelling","NoteSafety","ReasonFlag"],"mappings":"AAAA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,wDAA6B;AAC7B;AACA;AClBO,SAAS,eAAA,CAAgB,OAAA,EAAoC;AAClE,EAAA,2CAAA,OAAY,EAAS,SAAS,CAAA;AAC9B,EAAA,IAAI,KAAA,EAAO,CAAA;AACX,EAAA,IAAA,CAAA,MAAW,CAAC,KAAA,EAAO,MAAM,EAAA,GAAK,OAAA,CAAQ,OAAA,CAAQ,CAAA,EAAG;AAC/C,IAAA,6CAAA,MAAc,EAAQ,CAAA,QAAA,EAAW,KAAK,CAAA,CAAA,CAAG,CAAA;AACzC,IAAA,KAAA,GAAQ,EAAA,GAAA,CAAQ,OAAA,EAAS,GAAA,EAAM,EAAA,EAAA,EAAM,EAAA;AAAA,EACvC;AACA,EAAA,OAAO,IAAA;AACT;AAOO,IAAM,WAAA,EAAa,IAAA;AAOnB,IAAM,mBAAA,EAAqB,IAAA;AAO3B,IAAM,oBAAA,EAAsB,eAAA,CAAgB,CAAC,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,EAAE,CAAC,CAAA;AAOlE,IAAM,mBAAA,EAAqB,eAAA,CAAgB,CAAC,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,EAAE,CAAC,CAAA;AAOjE,IAAM,YAAA,EAAc,eAAA,CAAgB,CAAC,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,EAAE,CAAC,CAAA;AAO1D,IAAM,cAAA,EAAgB,eAAA,CAAgB,CAAC,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,EAAE,CAAC,CAAA;AAO5D,IAAM,YAAA,EAAc,eAAA,CAAgB,CAAC,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,EAAE,CAAC,CAAA;AAO1D,IAAM,gBAAA,EAAkB,eAAA,CAAgB,CAAC,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,EAAE,CAAC,CAAA;AAO9D,IAAM,aAAA,EAAe,eAAA,CAAgB,CAAC,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,EAAE,CAAC,CAAA;AAO3D,IAAM,sBAAA,EAAwB,eAAA,CAAgB,CAAC,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAC,CAAC,CAAA;AAO7D,IAAM,sBAAA,EAAwB,eAAA,CAAgB,CAAC,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,EAAE,CAAC,CAAA;AAO9D,IAAM,WAAA,EAAa,eAAA,CAAgB,CAAC,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,EAAE,CAAC,CAAA;AAOtD,IAAM,gBAAA,EAAkB,eAAA,CAAgB,CAAC,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,EAAE,CAAC,CAAA;AAO3D,IAAM,0BAAA,EAA4B,eAAA,CAAgB,CAAC,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,EAAE,CAAC,CAAA;AAO3E,IAAM,0BAAA,EAA4B,eAAA,CAAgB,CAAC,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,EAAE,CAAC,CAAA;AAO3E,IAAM,eAAA,EAAiB,IAAA;AAuBvB,SAAS,cAAA,CAAe,UAAA,EAAoB,KAAA,EAAO,YAAA,EAAsB;AAC9E,EAAA,OAAO,6CAAA,UAAc,EAAY,IAAA,EAAM,CAAA,EAAG,cAAc,CAAA;AAC1D;AAeO,SAAS,MAAA,CAAO,KAAA,EAAiB,KAAA,EAAO,KAAA,EAAe;AAC5D,EAAA,OAAO,cAAA,CAAe,4CAAA,KAAuB,EAAO,IAAI,CAAA,CAAE,UAAA,EAAY,CAAA,EAAA;AACxE;AASuE;AAUC;AASA;AAUN;AAUI;AAiBN;AAaP;AAWE;AAYY;AAQR;AAWC;AAQA;AAatB;AACjC,EAAA;AACC,EAAA;AACM,EAAA;AACL,EAAA;AACM,EAAA;AACD,EAAA;AACN,EAAA;AACE,EAAA;AACF,EAAA;AACI,EAAA;AACH,EAAA;AACO,EAAA;AACD,EAAA;AACE,EAAA;AACR,EAAA;AACS,EAAA;AACD,EAAA;AACA,EAAA;AACV,EAAA;AACI,EAAA;AACS,EAAA;AACA,EAAA;AACT,EAAA;AACZ;AA4CyC;AAAA;AAE3B,EAAA;AAAA;AAEN,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAMA,EAAA;AAAA;AAEC,EAAA;AAAA;AAEF,EAAA;AAAA;AAAA;AAAA;AAAA;AAKI,EAAA;AAAA;AAEJ,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAMC,EAAA;AAAA;AAEG,EAAA;AAAA;AAED,EAAA;AAAA;AAED,EAAA;AAAA;AAEF,EAAA;AAAA;AAEG,EAAA;AAAA;AAEC,EAAA;AAAA;AAED,EAAA;AAAA;AAED,EAAA;AAAA;AAED,EAAA;AAAA;AAEA,EAAA;AAAA;AAED,EAAA;AACP;AAmB0C;AAAA;AAE7B,EAAA;AAAA;AAEF,EAAA;AAAA;AAEH,EAAA;AAAA;AAES,EAAA;AAAA;AAEP,EAAA;AAAA;AAEF,EAAA;AAAA;AAEC,EAAA;AAC+D;AAmCsB;AAI/D,EAAA;AACA,IAAA;AAC9B,EAAA;AACuC,EAAA;AAC9B,IAAA;AACT,EAAA;AACuC,EAAA;AAC9B,IAAA;AACT,EAAA;AAC0D,EAAA;AAC5D;AAiByE;AAChC,EAAA;AACV,EAAA;AACpB,IAAA;AACT,EAAA;AAEiB,EAAA;AAEnB;AAegF;AAC9C,EAAA;AACR,EAAA;AACkC,IAAA;AAC1D,EAAA;AACO,EAAA;AACT;AAGqF;AAC5E,EAAA;AACE,EAAA;AACC,EAAA;AACD,EAAA;AACX;AAiByD;AACM,EAAA;AAC/D;AAS8D;AACnC,EAAA;AACoC,EAAA;AAClC,IAAA;AAChB,MAAA;AACT,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAGmD;AACpB,EAAA;AAC7B,EAAA;AACF;AAgBgF;AAClC,EAAA;AAGnB,EAAA;AAC0C,EAAA;AAE5B,EAAA;AACzB,EAAA;AACF,IAAA;AAC4C,MAAA;AAEtD,IAAA;AACF,EAAA;AACF;ADja+D;AACA;AEpNV;AACA,EAAA;AACrD;AAGqD;AACA,EAAA;AACrD;AAG2E;AACtB,EAAA;AACrD;AFkN+D;AACA;AGxNnB;AACS,EAAA;AACrD;AAGuF;AACzD,EAAA;AACA,EAAA;AACC,EAAA;AAC/B;AAS4F;AAC7B,EAAA;AAC/D;AAkBsF;AACvB,EAAA;AAC1B,IAAA;AACxB,MAAA;AACT,IAAA;AACF,EAAA;AACkE,EAAA;AAC/B,IAAA;AACxB,MAAA;AACT,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAGqC;AACa,EAAA;AAClD;AAW4D;AAChC,EAAA;AACW,EAAA;AAClB,EAAA;AACV,IAAA;AACT,EAAA;AAC0D,EAAA;AAClC,EAAA;AACf,IAAA;AACT,EAAA;AACsD,EAAA;AACvC,EAAA;AACN,IAAA;AACT,EAAA;AAC4D,EAAA;AACJ,EAAA;AAC1D;AAwBkG;AACxD,EAAA;AACuB,EAAA;AAC1B,IAAA;AACf,IAAA;AACc,MAAA;AAClC,IAAA;AACF,EAAA;AAC0C,EAAA;AAC1B,EAAA;AACP,IAAA;AACT,EAAA;AAC0C,EAAA;AACL,EAAA;AAEZ,EAAA;AAE3B;AAoBkF;AAChC,EAAA;AAClD;AHuI+D;AACA;AIpNR;AACJ,EAAA;AACf,EAAA;AAItB,IAAA;AACgD,MAAA;AAC1D,IAAA;AACF,EAAA;AACwC,EAAA;AACiB,EAAA;AAC3D;AA+BqD;AACpB,EAAA;AACA,IAAA;AAC/B,EAAA;AACiD,EAAA;AAKA,IAAA;AAKA,IAAA;AACH,MAAA;AAC5C,IAAA;AACF,EAAA;AACU,EAAA;AAC2C,IAAA;AACrD,EAAA;AACF;AJ6K+D;AACA;AKzUJ;AACJ,EAAA;AACvD;AAG0D;AACE,EAAA;AAC5D;AAoBkE;AAC3B,EAAA;AACvC;AAcsE;AACxC,EAAA;AAGK,EAAA;AACkB,EAAA;AACE,EAAA;AAC9B,IAAA;AACoB,IAAA;AAChC,MAAA;AACT,IAAA;AACsB,IAAA;AACuB,IAAA;AACpC,MAAA;AACT,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAG+C;AAClB,EAAA;AACK,EAAA;AACS,IAAA;AACvB,MAAA;AAChB,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAkCsF;AACxC,EAAA;AAC9C;AAG2E;AAIxC,EAAA;AACD,EAAA;AACX,EAAA;AACL,EAAA;AACc,IAAA;AAC9B,EAAA;AAC8C,EAAA;AACP,EAAA;AACJ,EAAA;AACtB,EAAA;AACqB,EAAA;AACC,IAAA;AACtB,MAAA;AACX,IAAA;AACF,EAAA;AACyD,EAAA;AAC3D;AAeqE;AAC3B,EAAA;AAC1C;AAG0D;AACzB,EAAA;AACC,EAAA;AACX,EAAA;AACL,EAAA;AACc,IAAA;AAC9B,EAAA;AACqC,EAAA;AACN,EAAA;AACwB,EAAA;AACzD;AAoB0F;AAC5D,EAAA;AACyB,EAAA;AACpC,EAAA;AACR,IAAA;AACT,EAAA;AACoD,EAAA;AACV,EAAA;AAC5C;AAgBwE;AAC1C,EAAA;AACW,EAAA;AACO,EAAA;AACrC,IAAA;AACT,EAAA;AACa,EAAA;AACuB,EAAA;AACO,IAAA;AAC7B,MAAA;AACZ,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAU6D;AAKH,EAAA;AAC1D;AAagE;AAClC,EAAA;AACQ,EAAA;AACb,EAAA;AACS,EAAA;AACW,IAAA;AACf,MAAA;AAC1B,IAAA;AACF,EAAA;AACO,EAAA;AACT;ALoL+D;AACA;AMrZb;AAGA;AAGb;AAGJ;AAGP;AAGmC;AACnB,EAAA;AACY,EAAA;AACtD;AASoF;AACrD,EAAA;AAAA;AACf,EAAA;AAAA;AACgB,EAAA;AAAA;AACA,EAAA;AAAA;AACe,EAAA;AAAA;AACf,EAAA;AAAA;AACC,EAAA;AAAA;AACjC;AAGsD;AAUX;AACgB,EAAA;AAC7B,EAAA;AACnB,IAAA;AACT,EAAA;AACyD,EAAA;AAC3D;AAgBuD;AAC9B,EAAA;AAEuB,EAAA;AAEhD;AAgC0E;AAC3C,EAAA;AACD,EAAA;AACe,EAAA;AACJ,EAAA;AAC4B,EAAA;AAC7B,EAAA;AACxC;AAuBoF;AAC3B,EAAA;AACG,EAAA;AAGT,EAAA;AACO,EAAA;AACtB,EAAA;AACiB,EAAA;AACI,EAAA;AAChD,EAAA;AACkB,IAAA;AACvB,IAAA;AACuC,IAAA;AACzC,EAAA;AACF;ANyT+D;AACA;AO9c9B;AAGM;AAGL;AAWI;AACyB;AAQvB;AACM,EAAA;AAC9C;AAG8C;AACR,EAAA;AACtC;AAWuD;AAGnD,EAAA;AAEJ;AAGgD;AACW,EAAA;AAC3D;AA+C0D;AAC1C,EAAA;AACI,EAAA;AACyB,EAAA;AACR,IAAA;AAClB,IAAA;AACoB,IAAA;AACtB,MAAA;AACb,IAAA;AACF,EAAA;AACgD,EAAA;AAClD;AAakF;AAEtD,EAAA;AAEA,EAAA;AACC,IAAA;AACA,MAAA;AACzB,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAkDyD;AACR,EAAA;AACP,EAAA;AACC,EAAA;AAGN,EAAA;AAC/B,EAAA;AAI+C,EAAA;AACT,IAAA;AACI,IAAA;AAC1C,MAAA;AACF,IAAA;AACuC,IAAA;AACY,IAAA;AACzC,MAAA;AACD,MAAA;AACT,IAAA;AACF,EAAA;AACuD,EAAA;AACzD;AAiC0E;AACrC,EAAA;AAMwB,EAAA;AAC7D;AAuB0E;AAC3C,EAAA;AACoB,EAAA;AAGb,EAAA;AACkB,EAAA;AAC/C,EAAA;AACkD,IAAA;AACvD,IAAA;AACuC,IAAA;AACzC,EAAA;AACF;AAG4C;AACmB,EAAA;AAC/D;AAYyD;AACL,EAAA;AACpD;AAgBoE;AAC9B,EAAA;AACX,EAAA;AAUZ,IAAA;AAEb,EAAA;AAcc,EAAA;AAGhB;AAuB0E;AACrC,EAAA;AACyB,EAAA;AAC9D;AAoB6E;AACxC,EAAA;AACyB,EAAA;AAC9D;AAsCmF;AACpD,EAAA;AACD,EAAA;AAChB,EAAA;AACW,EAAA;AACC,IAAA;AACA,IAAA;AACrB,EAAA;AAKoC,IAAA;AACnC,MAAA;AACF,IAAA;AACwC,IAAA;AACH,IAAA;AACnC,MAAA;AACF,IAAA;AACO,IAAA;AACEA,MAAAA;AAAA;AAAA;AAGiD,MAAA;AACjB,MAAA;AACzC,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAG0E;AAC/B,EAAA;AACU,EAAA;AACrD;AAG6E;AAC5B,EAAA;AACU,EAAA;AAC3D;AASM;AACsC,EAAA;AACA,EAAA;AACA,EAAA;AACM,EAAA;AACV,EAAA;AACG,EAAA;AAC3C;AAuC+C;AAChB,EAAA;AACD,EAAA;AAC8B,EAAA;AAC5D;AA+BuF;AACxC,EAAA;AACF,EAAA;AACf,EAAA;AACF,EAAA;AACY,EAAA;AACgB,IAAA;AACtD,EAAA;AACgD,EAAA;AACW,IAAA;AAChD,MAAA;AACT,IAAA;AACF,EAAA;AACO,EAAA;AACT;APoC+D;AACA;AQtnB2B;AAC1C,EAAA;AAChD;AAa0D;AACV,EAAA;AAClC,IAAA;AACkB,MAAA;AAE5B,IAAA;AACF,EAAA;AACkD,EAAA;AACpD;AAG0E;AAC9B,EAAA;AACnC,EAAA;AACT;AAsCwD;AACvB,EAAA;AACF,IAAA;AAC7B,EAAA;AAC8B,EAAA;AAEU,EAAA;AAIQ,EAAA;AAIV,EAAA;AAGP,EAAA;AACjC;AAaoD;AACE,EAAA;AAC3B,EAAA;AAClB,EAAA;AACT;AR+iB+D;AACA;ASpqBZ;AACJ,EAAA;AACX,EAAA;AACpC;AAa0D;AACvB,EAAA;AACnC;AAa0D;AACvB,EAAA;AACnC;AAkBmF;AAC1C,EAAA;AACzC;AT6nB+D;AACA;AUvpB/C;AACL,EAAA;AACC,EAAA;AACM,EAAA;AACL,EAAA;AACM,EAAA;AACD,EAAA;AACN,EAAA;AACE,EAAA;AACF,EAAA;AACI,EAAA;AACH,EAAA;AACO,EAAA;AACD,EAAA;AACE,EAAA;AACR,EAAA;AACS,EAAA;AACD,EAAA;AACA,EAAA;AACV,EAAA;AACI,EAAA;AACS,EAAA;AACA,EAAA;AACT,EAAA;AACE,EAAA;AACN,EAAA;AACA,EAAA;AACC,EAAA;AACF,EAAA;AACI,EAAA;AACJ,EAAA;AACC,EAAA;AACG,EAAA;AACD,EAAA;AACD,EAAA;AACF,EAAA;AACG,EAAA;AACC,EAAA;AACD,EAAA;AACD,EAAA;AACD,EAAA;AACA,EAAA;AACD,EAAA;AACP;AAW6D;AAClB,EAAA;AACE,EAAA;AAChB,IAAA;AACA,IAAA;AAC9B,EAAA;AAC0B,EAAA;AAEpB,IAAA;AAEmB,IAAA;AACe,MAAA;AACtC,IAAA;AACF,EAAA;AACO,EAAA;AACN;AAyBsF;AACxD,EAAA;AACW,IAAA;AACX,IAAA;AAC0B,MAAA;AACvD,IAAA;AAC8B,IAAA;AAChC,EAAA;AACgD,EAAA;AAClD;AAgCqF;AACnD,EAAA;AAClC;AVslB+D;AACA;AWtoBsB;AAC9E,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACP,EAAA;AACK,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACP,EAAA;AACK,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACP,EAAA;AACK,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACP,EAAA;AACM,EAAA;AACC,IAAA;AACA,IAAA;AACA,IAAA;AACP,EAAA;AACM,EAAA;AACC,IAAA;AACA,IAAA;AACA,IAAA;AACP,EAAA;AACO,EAAA;AACA,IAAA;AACA,IAAA;AACP,EAAA;AACF;AAG6F;AAGP;AAGzD;AAGX;AAGuB;AAGoB;AACtD,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACC,EAAA;AACA,EAAA;AACC,EAAA;AACT;AAWiE;AACtC,EAAA;AACf,IAAA;AACR,IAAA;AAC0D,IAAA;AAC3B,IAAA;AACC,IAAA;AAClC,EAAA;AACgC,EAAA;AACT,IAAA;AACvB,EAAA;AACO,EAAA;AACT;AAY8E;AACxD,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACsB,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACG,EAAA;AACe,EAAA;AAC5D,EAAA;AACE,IAAA;AACa,IAAA;AACF,MAAA;AACJ,MAAA;AACE,QAAA;AACC,QAAA;AACR,MAAA;AACD,IAAA;AACH,EAAA;AACA,EAAA;AACE,IAAA;AACa,IAAA;AACF,MAAA;AACJ,MAAA;AACE,QAAA;AACC,QAAA;AACR,MAAA;AACD,IAAA;AACH,EAAA;AAC0C,EAAA;AACG,EAAA;AACb,EAAA;AACG,EAAA;AACE,EAAA;AACf,EAAA;AACA,EAAA;AACa,EAAA;AACE,EAAA;AACoB,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACD,EAAA;AACE,EAAA;AACC,EAAA;AAAA;AAAA;AAG3D,EAAA;AACE,IAAA;AACa,IAAA;AACF,MAAA;AACJ,MAAA;AACE,QAAA;AACC,QAAA;AACR,MAAA;AACQ,MAAA;AACT,IAAA;AACH,EAAA;AACA,EAAA;AACE,IAAA;AACa,IAAA;AACF,MAAA;AACJ,MAAA;AACE,QAAA;AACC,QAAA;AACR,MAAA;AACD,IAAA;AACH,EAAA;AACoB,EAAA;AACwB,EAAA;AACa,EAAA;AAAA;AAAA;AAAA;AAIA,EAAA;AACzD,EAAA;AACE,IAAA;AACa,IAAA;AACF,MAAA;AACJ,MAAA;AACG,QAAA;AACA,QAAA;AACR,MAAA;AACD,IAAA;AACH,EAAA;AACA,EAAA;AACE,IAAA;AACa,IAAA;AACF,MAAA;AACJ,MAAA;AACE,QAAA;AACC,QAAA;AACR,MAAA;AACD,IAAA;AACH,EAAA;AAC6D,EAAA;AAC7D,EAAA;AACE,IAAA;AACa,IAAA;AACF,MAAA;AACJ,MAAA;AACE,QAAA;AACC,QAAA;AACR,MAAA;AACD,IAAA;AACH,EAAA;AACA,EAAA;AACE,IAAA;AACa,IAAA;AACF,MAAA;AACJ,MAAA;AACE,QAAA;AACC,QAAA;AACR,MAAA;AACD,IAAA;AACH,EAAA;AACsC,EAAA;AACE,EAAA;AAC1C;AAS8F;AAGX;AAiBpB;AAC7C,EAAA;AAMgC,EAAA;AACC,EAAA;AACrB,IAAA;AACR,IAAA;AACyB,MAAA;AACY,MAAA;AACnD,QAAA;AACF,MAAA;AAC4C,MAAA;AAC5C,MAAA;AACF,IAAA;AACuD,IAAA;AACzD,EAAA;AAC8C,EAAA;AACA,EAAA;AACJ,EAAA;AACjB,EAAA;AAGH,IAAA;AACX,MAAA;AACkB,IAAA;AAClB,MAAA;AACT,IAAA;AACiB,IAAA;AACa,MAAA;AACY,IAAA;AACjC,MAAA;AACqB,MAAA;AACa,IAAA;AAClC,MAAA;AACqB,MAAA;AAC9B,IAAA;AACF,EAAA;AACgC,EAAA;AAG2B,IAAA;AACtB,MAAA;AACT,QAAA;AACF,QAAA;AACtB,MAAA;AACF,IAAA;AACF,EAAA;AAC8B,EAAA;AACf,IAAA;AACb,IAAA;AACwD,IAAA;AACV,IAAA;AACA,IAAA;AAChD,EAAA;AACgC,EAAA;AACJ,IAAA;AAC5B,EAAA;AAC+B,EAAA;AACJ,IAAA;AAC3B,EAAA;AACO,EAAA;AACT;AAawD;AACW,EAAA;AACT,EAAA;AACpB,IAAA;AACE,MAAA;AACpC,IAAA;AACF,EAAA;AAC2C,EAAA;AACtB,IAAA;AACrB,EAAA;AACsC,EAAA;AACA,EAAA;AACa,IAAA;AACnD,EAAA;AACwD,EAAA;AACpC,IAAA;AACmC,MAAA;AACrD,IAAA;AACF,EAAA;AAC2C,EAAA;AACA,IAAA;AAC3C,EAAA;AACwC,EAAA;AAC1C;AA8B8D;AAClB,EAAA;AAC5C;AAGwE;AACrB,EAAA;AACnD;AAG0C;AACE,EAAA;AACnC,EAAA;AACM,IAAA;AACW,qBAAA;AACoC,IAAA;AAC7B,IAAA;AACA,IAAA;AACrB,EAAA;AACZ;AAG8D;AACL,EAAA;AACzD;AAiBa;AAC8B,EAAA;AAChB,EAAA;AAC+B,IAAA;AACxD,EAAA;AACqC,EAAA;AACvB,EAAA;AACY,EAAA;AACV,IAAA;AAChB,EAAA;AACO,EAAA;AACT;AAYiF;AACxC,EAAA;AACzC;AA4BgE;AAC1B,EAAA;AACO,EAAA;AAClB,EAAA;AAChB,IAAA;AACT,EAAA;AAKoD,EAAA;AAChD,EAAA;AACO,EAAA;AAC0B,EAAA;AACO,IAAA;AACe,IAAA;AAC7C,MAAA;AACM,MAAA;AAClB,IAAA;AACF,EAAA;AAC2C,EAAA;AAC7C;AAG6E;AAC9C,EAAA;AACD,EAAA;AACA,EAAA;AACE,EAAA;AACD,EAAA;AACA,EAAA;AACC,EAAA;AACD,EAAA;AACA,EAAA;AAC/B;AAGwF;AAGd;AACb,EAAA;AAC7D;AAgB4F;AAC9B,EAAA;AACI,EAAA;AAC7B,EAAA;AACN,EAAA;AACA,EAAA;AAChB,EAAA;AACK,IAAA;AAClB,EAAA;AAC0D,EAAA;AACL,EAAA;AAC/B,EAAA;AACL,EAAA;AACR,IAAA;AACe,EAAA;AACf,IAAA;AACe,EAAA;AACf,IAAA;AACuB,EAAA;AAGqB,IAAA;AAC1C,MAAA;AACF,IAAA;AACW,MAAA;AAClB,IAAA;AACF,EAAA;AACyB,EAAA;AACP,IAAA;AACM,EAAA;AACa,IAAA;AAC1B,MAAA;AACiC,IAAA;AACjC,MAAA;AACF,IAAA;AACmD,MAAA;AAC1D,IAAA;AACF,EAAA;AAC8E,EAAA;AAClE,EAAA;AAGmC,IAAA;AACjC,MAAA;AACL,IAAA;AACW,MAAA;AAClB,IAAA;AACF,EAAA;AAC8B,EAAA;AACV,IAAA;AAChB,MAAA;AACF,IAAA;AACkD,IAAA;AACrB,IAAA;AACpB,MAAA;AACT,IAAA;AAC0B,IAAA;AAC5B,EAAA;AACmE,EAAA;AACxC,EAAA;AACV,IAAA;AACjB,EAAA;AAC0C,EAAA;AACe,EAAA;AAC3D;AAGiE;AAGL;AAGG;AAGyB;AACxD,EAAA;AAClB,IAAA;AACoC,MAAA;AAC9C,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAGyF;AACzD,EAAA;AAClB,IAAA;AACoC,MAAA;AAC9C,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAOQ;AACqB,EAAA;AACU,IAAA;AACrC,EAAA;AAC4B,EAAA;AACU,IAAA;AACtC,EAAA;AACF;AAmB4D;AACX,EAAA;AACjB,IAAA;AAC9B,EAAA;AACmD,EAAA;AACpB,EAAA;AACsB,IAAA;AACrD,EAAA;AAC8D,EAAA;AAC9B,EAAA;AAC9B,IAAA;AACO,MAAA;AACwB,MAAA;AAC7B,MAAA;AACF,IAAA;AACF,EAAA;AACsC,EAAA;AACR,IAAA;AAC9B,EAAA;AAC2C,EAAA;AACa,IAAA;AACH,IAAA;AACrD,EAAA;AACkD,EAAA;AACE,EAAA;AACtB,EAAA;AAChC;AXga+D;AACA;AY/jC7B;AAYe;AACvB,EAAA;AAC1B;AAewE;AACpB,EAAA;AACM,IAAA;AACxD,EAAA;AACO,EAAA;AACT;AAoBiE;AACnB,EAAA;AACI,EAAA;AACjB,EAAA;AACxB,EAAA;AACT;AAoB+F;AAG1D,EAAA;AACb,EAAA;AACS,EAAA;AACqB,EAAA;AAC/B,EAAA;AAC8B,EAAA;AACM,EAAA;AACJ,EAAA;AACvD;AAmByF;AACpC,EAAA;AACrD;AAmCS;AAIkC,EAAA;AACT,EAAA;AACiB,IAAA;AACjD,EAAA;AAC0B,EAAA;AACL,EAAA;AACM,IAAA;AACzB,IAAA;AACiD,IAAA;AACnD,EAAA;AACgC,EAAA;AACQ,IAAA;AACxC,EAAA;AACsC,EAAA;AACsB,IAAA;AAC5D,EAAA;AACsC,EAAA;AACsB,IAAA;AACI,EAAA;AAChC,IAAA;AAC0B,MAAA;AACxD,IAAA;AAC0D,IAAA;AAClC,IAAA;AACkC,MAAA;AAC1D,IAAA;AACF,EAAA;AACuC,EAAA;AACoB,IAAA;AAC3D,EAAA;AACO,EAAA;AACT;AAGyE;AACV,EAAA;AACtC,EAAA;AACzB;AA+BsD;AAChB,EAAA;AACf,EAAA;AACc,IAAA;AACA,IAAA;AACI,IAAA;AACvC,EAAA;AACkC,EAAA;AACQ,IAAA;AAC1C,EAAA;AACO,EAAA;AACT;AAwBqD;AACjB,EAAA;AACa,EAAA;AACT,EAAA;AACkB,EAAA;AAGlB,EAAA;AACY,EAAA;AAC1B,EAAA;AACkC,IAAA;AAC1D,EAAA;AACc,EAAA;AACY,EAAA;AACV,IAAA;AAChB,EAAA;AACO,EAAA;AACT;AAUmE;AACpD,EAAA;AACkB,EAAA;AACnB,IAAA;AAC+C,MAAA;AACzD,IAAA;AACF,EAAA;AAC4B,EAAA;AACS,IAAA;AACrC,EAAA;AACwB,EAAA;AACkC,EAAA;AACxB,IAAA;AACJ,IAAA;AAChB,MAAA;AACQ,QAAA;AAClB,MAAA;AACF,IAAA;AACK,IAAA;AAC2C,MAAA;AAChD,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAGmD;AACI,EAAA;AACvD;AA4BsD;AACV,EAAA;AACkB,EAAA;AAC1B,EAAA;AACkB,IAAA;AACpD,EAAA;AACO,EAAA;AACT;AAuB0E;AACpB,EAAA;AACnC,EAAA;AACuD,EAAA;AACxC,EAAA;AACV,IAAA;AACtB,EAAA;AAC+B,EAAA;AACO,IAAA;AACtC,EAAA;AACO,EAAA;AACT;AAwBuE;AACpD,EAAA;AACwB,EAAA;AACP,EAAA;AACb,EAAA;AACoB,IAAA;AACxB,IAAA;AACe,IAAA;AAChC,EAAA;AACgC,EAAA;AACgB,IAAA;AAChD,EAAA;AACO,EAAA;AACT;AAe8E;AACxD,EAAA;AACwC,EAAA;AACnD,IAAA;AACT,EAAA;AACiC,EAAA;AACqB,EAAA;AAC5B,IAAA;AACc,IAAA;AACY,IAAA;AACzC,MAAA;AACT,IAAA;AACyD,IAAA;AAChD,MAAA;AACT,IAAA;AACqD,IAAA;AACvD,EAAA;AACO,EAAA;AACT;AAkB2E;AACnC,EAAA;AACc,EAAA;AACD,EAAA;AAC1C,IAAA;AACT,EAAA;AAC0D,EAAA;AAC5D;AAUoE;AAC/D,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACL;AAGwF;AAC1C,EAAA;AACjB,EAAA;AAClB,IAAA;AACT,EAAA;AAC8B,EAAA;AAC0B,IAAA;AACxD,EAAA;AAC4B,EAAA;AACqC,EAAA;AACnE;AAewF;AACpD,EAAA;AACY,EAAA;AACnC,EAAA;AAC6B,EAAA;AACS,IAAA;AACjD,EAAA;AAC8D,EAAA;AACxB,IAAA;AACtC,EAAA;AACO,EAAA;AACT;AAG0D;AAC1B,EAAA;AAChC;AAQiD;AACpC,EAAA;AAC6B,EAAA;AACC,IAAA;AACzC,EAAA;AACO,EAAA;AACT;AAsBgG;AACnC,EAAA;AACpC,EAAA;AACY,EAAA;AACJ,IAAA;AAChB,MAAA;AACb,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAauE;AACX,EAAA;AAC5D;AAU2E;AACf,EAAA;AAC5D;AAoBiF;AAC7C,EAAA;AACU,EAAA;AACjB,EAAA;AAClBA,IAAAA;AACT,EAAA;AACuD,EAAA;AAClB,EAAA;AAC0B,EAAA;AAC3C,EAAA;AAGU,IAAA;AAC9B,EAAA;AACsC,EAAA;AACmB,IAAA;AACzD,EAAA;AACoB,EAAA;AACW,IAAA;AAC/B,EAAA;AACoB,EAAA;AAGyB,IAAA;AAC7C,EAAA;AACoB,EAAA;AAGyB,IAAA;AAC7C,EAAA;AACoB,EAAA;AAC8B,IAAA;AACO,IAAA;AACjB,IAAA;AAC7B,MAAA;AACT,IAAA;AACoC,IAAA;AAC3B,MAAA;AACT,IAAA;AACO,IAAA;AACT,EAAA;AACwC,EAAA;AACA,IAAA;AACxC,EAAA;AACO,EAAA;AACT;AAGuE;AAC/D,EAAA;AACC,EAAA;AACA,EAAA;AACA,EAAA;AACE,EAAA;AACX;AAWoF;AAYC;AAC9B,EAAA;AAClC,EAAA;AACS,IAAA;AAC5B,EAAA;AACiD,EAAA;AACnD;AAsCqD;AACd,EAAA;AACR,EAAA;AACS,IAAA;AACtC,EAAA;AAC6B,EAAA;AACS,IAAA;AACtC,EAAA;AAC8B,EAAA;AACJ,EAAA;AAC+B,IAAA;AACzD,EAAA;AAGwD,EAAA;AACC,IAAA;AACzD,EAAA;AACsB,EAAA;AACxB;AAawD;AAEjD,EAAA;AAEP;AAGuE;AACjC,EAAA;AAC3B,IAAA;AACT,EAAA;AACoC,EAAA;AAC3B,IAAA;AACT,EAAA;AACoC,EAAA;AAC3B,IAAA;AACT,EAAA;AACmB,EAAA;AACV,IAAA;AACT,EAAA;AACO,EAAA;AACT;AAG4F;AACnC,EAAA;AAC9C,IAAA;AACT,EAAA;AACwD,EAAA;AAC/C,IAAA;AACT,EAAA;AACuC,EAAA;AAC9B,IAAA;AACT,EAAA;AACmB,EAAA;AACV,IAAA;AACT,EAAA;AACuC,EAAA;AAC9B,IAAA;AACT,EAAA;AACuC,EAAA;AAC9B,IAAA;AACT,EAAA;AACuC,EAAA;AAC9B,IAAA;AACT,EAAA;AACsB,EAAA;AACb,IAAA;AACT,EAAA;AACO,EAAA;AACT;AAG+F;AACnD,EAAA;AAG/B,EAAA;AAEb;AAMgF;AAG3C,EAAA;AACd,EAAA;AACoB,EAAA;AACpB,EAAA;AACH,EAAA;AACN,IAAA;AACR,MAAA;AACF,IAAA;AACF,EAAA;AACoD,EAAA;AACC,EAAA;AACK,EAAA;AACzB,IAAA;AACM,IAAA;AACtC,EAAA;AAC6B,EAAA;AACA,EAAA;AAGM,EAAA;AAEY,EAAA;AACpC,IAAA;AACwB,MAAA;AAClC,IAAA;AACF,EAAA;AACyD,EAAA;AAC3D;AA0B0E;AAC3B,EAAA;AAC/C;AAe4E;AAC7B,EAAA;AAC/C;AZmjB+D;AACA;AariDL;AACrD,EAAA;AAAA;AACA,EAAA;AAAA;AACA,EAAA;AAAA;AACA,EAAA;AAAA;AACA,EAAA;AAAA;AACA,EAAA;AAAA;AACA,EAAA;AAAA;AACA,EAAA;AAAA;AACA,EAAA;AAAA;AACC,EAAA;AAAA;AACA,EAAA;AAAA;AACN;AAS+D;AAC1D,EAAA;AACA,EAAA;AAAA;AACL;AAMgE;AAC3D,EAAA;AAAA;AACA,EAAA;AAAA;AACA,EAAA;AAAA;AACA,EAAA;AAAA;AACA,EAAA;AAAA;AACA,EAAA;AAAA;AACA,EAAA;AAAA;AACA,EAAA;AAAA;AACA,EAAA;AAAA;AACC,EAAA;AAAA;AACA,EAAA;AAAA;AACN;AAG4C;AACC,EAAA;AAC7C;AAoBgF;AAC1C,EAAA;AACL,EAAA;AACP,EAAA;AACuB,IAAA;AACnC,IAAA;AAC8C,MAAA;AAExD,IAAA;AACF,EAAA;AACF;AAGsD;AAChB,EAAA;AACzB,EAAA;AACJ,IAAA;AACP,EAAA;AACO,EAAA;AACT;AAGgG;AAC9D,EAAA;AACvB,IAAA;AACT,EAAA;AAC+C,EAAA;AACF,EAAA;AAC/C;AAOoB;AAC6B,EAAA;AAC9B,IAAA;AACjB,EAAA;AACoD,EAAA;AACI,IAAA;AACxD,EAAA;AAC2B,EAAA;AAC7B;AAOuF;AACrB,EAAA;AAGlE;AASqC;AAMuB;AAGR;AAGe;AAGZ;AAWU;AAC5B,EAAA;AACrC;AAauD;AACP,EAAA;AACF,EAAA;AACI,EAAA;AAC7B,EAAA;AACJ,IAAA;AACf,EAAA;AAIyC,EAAA;AAGhB,EAAA;AAC3B;AAG8C;AACG,EAAA;AACjD;AAG+C;AACJ,EAAA;AACZ,EAAA;AACoB,EAAA;AACnD;AAGwD;AACX,EAAA;AAC7C;AAWgG;AAClC,EAAA;AACJ,EAAA;AACF,EAAA;AACzB,EAAA;AACnB,IAAA;AACV,EAAA;AACiD,EAAA;AACU,EAAA;AAG7D;AAmBgG;AAC1D,EAAA;AAChC,EAAA;AACY,EAAA;AACC,EAAA;AAC4B,EAAA;AACpB,IAAA;AACX,IAAA;AACC,IAAA;AACS,IAAA;AAC8B,MAAA;AACK,MAAA;AAC/B,MAAA;AACtB,QAAA;AACF,MAAA;AACe,MAAA;AACa,MAAA;AACb,MAAA;AACjB,IAAA;AACiC,IAAA;AAC/B,MAAA;AACF,IAAA;AAEuC,IAAA;AACF,IAAA;AACF,IAAA;AAC1B,MAAA;AACK,MAAA;AACC,MAAA;AACf,IAAA;AACF,EAAA;AACO,EAAA;AACT;AA6BsB;AACmB,EAAA;AACnC,EAAA;AACa,EAAA;AACD,EAAA;AACK,EAAA;AACiC,EAAA;AAC5B,IAAA;AACV,IAAA;AACF,IAAA;AACC,IAAA;AACD,IAAA;AACK,IAAA;AACoC,IAAA;AACxB,MAAA;AACoB,MAAA;AACvB,MAAA;AACtB,QAAA;AACF,MAAA;AACiD,MAAA;AACG,MAAA;AAClD,QAAA;AACF,MAAA;AACqB,MAAA;AACA,MAAA;AACa,MAAA;AACN,MAAA;AACW,MAAA;AACvB,MAAA;AAClB,IAAA;AACyC,IAAA;AACvC,MAAA;AACF,IAAA;AAGM,IAAA;AAI4B,IAAA;AACzB,MAAA;AACM,MAAA;AACD,MAAA;AACK,MAAA;AACnB,IAAA;AACF,EAAA;AACO,EAAA;AACT;AA6BqF;AAOnC,EAAA;AACL,EAAA;AACjB,EAAA;AACV,IAAA;AAChB,EAAA;AACyC,EAAA;AAIU,EAAA;AAC3B,IAAA;AACxB,EAAA;AACmD,EAAA;AAC5C,EAAA;AACT;AAS8B;AAGwD;AAGF;AACzC,EAAA;AACA,EAAA;AAChC,IAAA;AACT,EAAA;AAC4C,EAAA;AACU,EAAA;AACf,EAAA;AACa,EAAA;AACQ,EAAA;AAEb,EAAA;AAAA;AAI7C,EAAA;AACwB,EAAA;AACjB,IAAA;AACT,EAAA;AACmD,EAAA;AACT,EAAA;AACf,IAAA;AACD,IAAA;AACO,MAAA;AAC/B,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAakE;AACpB,EAAA;AACX,EAAA;AAIf,EAAA;AAC4B,IAAA;AAC9C,EAAA;AAEmC,EAAA;AACnB,EAAA;AACsC,IAAA;AACnC,IAAA;AAC0B,MAAA;AACI,MAAA;AAC/C,IAAA;AAC8C,IAAA;AACK,IAAA;AACtB,IAAA;AAC2B,MAAA;AACE,MAAA;AAKlD,MAAA;AAI+C,MAAA;AACvD,IAAA;AACK,EAAA;AAGqB,IAAA;AAC2B,MAAA;AACvB,MAAA;AAGwB,QAAA;AACpD,MAAA;AACF,IAAA;AAMwD,IAAA;AAC3B,IAAA;AACpB,MAAA;AACT,IAAA;AACF,EAAA;AAIyB,EAAA;AACoC,EAAA;AAC/D;AAG0F;AAgD3B;AAC1D,EAAA;AAAA;AACA,EAAA;AAAA;AACA,EAAA;AAAA;AACA,EAAA;AAAA;AACC,EAAA;AAAA;AACA,EAAA;AAAA;AACN;AAUkE;AAC1D,EAAA;AACC,EAAA;AACA,EAAA;AACA,EAAA;AACE,EAAA;AACX;AAGiC;AAuBX;AACK,EAAA;AACqB,IAAA;AACd,IAAA;AACqB,MAAA;AACnD,IAAA;AAC8B,IAAA;AACJ,IAAA;AACjB,MAAA;AACT,IAAA;AACoC,IAAA;AACjB,IAAA;AACc,MAAA;AACjC,IAAA;AACF,EAAA;AACiD,EAAA;AACnD;AAcoB;AACA,EAAA;AACyB,EAAA;AACd,EAAA;AACpB,IAAA;AACT,EAAA;AAGuB,EAAA;AAEmC,EAAA;AAC5D;AAkBE;AAGyD,EAAA;AAChD,IAAA;AACT,EAAA;AACqD,EAAA;AACvD;AAgBEC;AAE4C,EAAA;AACO,EAAA;AAC1CA,IAAAA;AACT,EAAA;AAC2B,EAAA;AACR,EAAA;AAAA;AAEqB,IAAA;AAAA;AAEO,IAAA;AAAA;AAAA;AAGI,IAAA;AAAA;AAAA;AAGJ,IAAA;AAAA;AAAA;AAGG,IAAA;AAClD,EAAA;AACoC,EAAA;AAC0B,IAAA;AACnD,MAAA;AACT,IAAA;AACF,EAAA;AACOA,EAAAA;AACT;AA8DQ;AACuB,EAAA;AACD,EAAA;AACgB,EAAA;AACZ,EAAA;AACiB,EAAA;AACnD;AAQQ;AACuC,EAAA;AAGzB,EAAA;AACtB;AA0BkE;AACnC,EAAA;AACD,EAAA;AACW,EAAA;AACW,EAAA;AACpD;AAawF;AACxC,EAAA;AACjB,EAAA;AACD,EAAA;AACkB,EAAA;AAC/B,EAAA;AAC0B,IAAA;AACzC,EAAA;AACF;AAG2F;AAkBxB;AAC/C,EAAA;AACX,IAAA;AACA,IAAA;AACI,MAAA;AACJ,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACI,MAAA;AACJ,IAAA;AACA,IAAA;AACI,MAAA;AACJ,IAAA;AAGI,MAAA;AACJ,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACI,MAAA;AACJ,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACI,MAAA;AACJ,IAAA;AACmC,MAAA;AACnC,IAAA;AACA,IAAA;AACA,IAAA;AACI,MAAA;AACJ,IAAA;AACA,IAAA;AACA,IAAA;AACI,MAAA;AACA,IAAA;AAG8C,MAAA;AACxB,MAAA;AACyB,MAAA;AACjC,MAAA;AACvB,IAAA;AACF,EAAA;AACF;AAiByE;AACV,EAAA;AAC/D;AAG8D;AACpB,EAAA;AACV,IAAA;AAC9B,EAAA;AAC2C,EAAA;AACK,EAAA;AACF,IAAA;AACJ,IAAA;AACK,IAAA;AAC9C,EAAA;AACH;AA+BgF;AAC7D,EAAA;AACY,EAAA;AACD,EAAA;AACW,EAAA;AACmB,EAAA;AAC5D;AAW2E;AACtD,EAAA;AAC4B,EAAA;AACF,IAAA;AAC7C,EAAA;AACiD,EAAA;AACnD;AAiCQ;AACuB,EAAA;AACD,EAAA;AACW,EAAA;AACP,EAAA;AAC6B,EAAA;AACX,EAAA;AACA,EAAA;AACK,EAAA;AACzD;AAUgE;AACpC,EAAA;AAG6B,EAAA;AACzD;Abw9B+D;AACA;Ac7hEkB;AACpB,EAAA;AAC/C,IAAA;AACR,MAAA;AACF,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAGsD;AACO,EAAA;AAC7D;AAGkE;AACrB,EAAA;AAC7C;AA0BmB;AAIc,EAAA;AACgC,EAAA;AACT,EAAA;AACP,EAAA;AAC1B,EAAA;AACe,IAAA;AAChC,MAAA;AACF,IAAA;AACkB,IAAA;AACT,MAAA;AACT,IAAA;AACA,IAAA;AACF,EAAA;AAC6D,EAAA;AAC3D,IAAA;AACF,EAAA;AACA,EAAA;AACF;Ad+/D+D;AACA;Ae3/Dc;AAC7B,EAAA;AAChD;AAOW;AACgB,EAAA;AACC,IAAA;AAC1B,EAAA;AAC2B,EAAA;AACoB,EAAA;AACtC,IAAA;AACT,EAAA;AACgD,EAAA;AACC,EAAA;AACxC,IAAA;AACT,EAAA;AAGW,EAAA;AAGb;AAM0D;AACH,EAAA;AACE,EAAA;AACd,EAAA;AAChC,IAAA;AACT,EAAA;AACyC,EAAA;AAChC,IAAA;AACT,EAAA;AACsC,EAAA;AAC7B,IAAA;AACT,EAAA;AACyD,EAAA;AACH,EAAA;AACf,EAAA;AACzC;AAoC2B;AACC,EAAA;AACyB,EAAA;AACM,EAAA;AACtB,EAAA;AACvB,IAAA;AACR,MAAA;AACF,IAAA;AACF,EAAA;AAI4B,EAAA;AACO,IAAA;AACM,IAAA;AAC7B,EAAA;AACgD,IAAA;AAC9C,MAAA;AACuB,QAAA;AACjC,MAAA;AACF,IAAA;AACF,EAAA;AACqD,EAAA;AACf,IAAA;AAC7B,IAAA;AACC,MAAA;AACG,MAAA;AAC6C,MAAA;AACF,MAAA;AACtD,IAAA;AACD,EAAA;AAEqD,EAAA;AAC1C,EAAA;AAC0C,EAAA;AACxB,IAAA;AACN,IAAA;AACuC,IAAA;AAC3D,MAAA;AACF,IAAA;AACqC,IAAA;AAChB,IAAA;AACnB,MAAA;AACF,IAAA;AACkB,IAAA;AACT,IAAA;AACX,EAAA;AAEa,EAAA;AACF,EAAA;AAC2C,EAAA;AACO,IAAA;AAC3C,IAAA;AACd,MAAA;AACF,IAAA;AACQ,IAAA;AAC6C,IAAA;AACzC,MAAA;AACZ,IAAA;AACF,EAAA;AAEe,EAAA;AACA,EAAA;AACK,EAAA;AACL,EAAA;AACE,EAAA;AACO,EAAA;AACE,EAAA;AACmC,IAAA;AAC5C,MAAA;AACb,MAAA;AACF,IAAA;AACY,IAAA;AACsB,IAAA;AACV,IAAA;AACe,IAAA;AACd,IAAA;AAC+C,IAAA;AAEtC,IAAA;AAIM,IAAA;AACkB,IAAA;AAC5D,EAAA;AAK8B,EAAA;AACd,EAAA;AACU,EAAA;AACmC,IAAA;AACzD,MAAA;AACF,IAAA;AACkC,IAAA;AACM,IAAA;AACzB,MAAA;AACf,IAAA;AACF,EAAA;AAEO,EAAA;AACG,IAAA;AACwC,MAAA;AACF,MAAA;AACA,MAAA;AACE,MAAA;AAChD,IAAA;AACmD,IAAA;AACvC,IAAA;AAC4B,MAAA;AACZ,MAAA;AAC5B,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;Af47D+D;AACA;AgBrsEI;AAmBuB;AACnD,EAAA;AACvC;AA4BW;AAC2C,EAAA;AAGI,IAAA;AACxD,EAAA;AAC6D,EAAA;AAC/D;AA2ByF;AAC9B,EAAA;AACb,IAAA;AACiB,IAAA;AAClD,MAAA;AACT,IAAA;AACiD,IAAA;AACxC,MAAA;AACT,IAAA;AACiC,IAAA;AACnC,EAAA;AACmD,EAAA;AACG,EAAA;AACxD;AAG+D;AAGD,EAAA;AAC9D;AA6B6F;AAClC,EAAA;AAChD,IAAA;AACT,EAAA;AAC0C,EAAA;AACiB,EAAA;AAC7D;AAGgD;AACT,EAAA;AACvC;AAG8C;AAChB,EAAA;AAC9B;AAG8D;AACD,EAAA;AAC7D;AAG+D;AAC7B,EAAA;AAClC;AAiCW;AACe,EAAA;AACD,EAAA;AACC,EAAA;AACD,EAAA;AACgB,EAAA;AAC9B,IAAA;AACT,EAAA;AACoC,EAAA;AACC,EAAA;AACW,EAAA;AAClD;AA2BW;AACe,EAAA;AACD,EAAA;AACC,EAAA;AACD,EAAA;AACe,EAAA;AAC7B,IAAA;AACT,EAAA;AACwD,EAAA;AAC1D;AAoBW;AACe,EAAA;AACD,EAAA;AACC,EAAA;AACD,EAAA;AACK,EAAA;AACnB,IAAA;AACT,EAAA;AAC2B,EAAA;AAC7B;AAwCE;AAMkD,EAAA;AAC1B,EAAA;AACD,EAAA;AACC,EAAA;AACD,EAAA;AACJ,EAAA;AACA,EAAA;AACe,EAAA;AACzB,IAAA;AACT,EAAA;AACoC,EAAA;AACC,EAAA;AAIoB,EAAA;AAChD,IAAA;AACT,EAAA;AACqC,EAAA;AACsB,EAAA;AAClD,IAAA;AACT,EAAA;AACO,EAAA;AACT;AAuBW;AACe,EAAA;AACD,EAAA;AACC,EAAA;AACD,EAAA;AACJ,EAAA;AACA,EAAA;AACuC,EAAA;AACjD,IAAA;AACT,EAAA;AACoC,EAAA;AAChB,EAAA;AACX,IAAA;AACT,EAAA;AACqC,EAAA;AACT,EAAA;AACnB,IAAA;AACT,EAAA;AACqC,EAAA;AAClB,EAAA;AACrB;AA6BE;AAE4B,EAAA;AACD,EAAA;AACC,EAAA;AACD,EAAA;AACJ,EAAA;AACzB;AAkBW;AAGsC,EAAA;AACI,EAAA;AACrD;AAiBW;AACqB,EAAA;AACF,EAAA;AACmB,EAAA;AAChB,EAAA;AACqB,EAAA;AAC3C,IAAA;AACT,EAAA;AACuD,EAAA;AACzD;AhBo4D+D;AACA;AiBlyE/C;AACU,EAAA;AACC,IAAA;AACF,IAAA;AACP,IAAA;AACD,IAAA;AACK,IAAA;AACD,IAAA;AACA,IAAA;AACI,IAAA;AACT,IAAA;AACO,IAAA;AACF,IAAA;AACA,IAAA;AAChB,EAAA;AACkB,EAAA;AACI,IAAA;AACF,IAAA;AACP,IAAA;AACD,IAAA;AACK,IAAA;AACD,IAAA;AACA,IAAA;AACI,IAAA;AACT,IAAA;AACO,IAAA;AACF,IAAA;AACA,IAAA;AAChB,EAAA;AACF;AAMgD;AAyBjC;AACkC,EAAA;AACrB,EAAA;AACV,IAAA;AACnB,EAAA;AAC4D,EAAA;AAClB,EAAA;AACc,EAAA;AAC7B,IAAA;AACvB,MAAA;AACF,IAAA;AACkF,IAAA;AACpF,EAAA;AACO,EAAA;AACT;AAOO;AACL,EAAA;AACA,EAAA;AACA,EAAA;AAHUC,EAAAA;AAAA;AASY;AAcjB;AAEL,EAAA;AAKA,EAAA;AAMA,EAAA;AAEA,EAAA;AAEA,EAAA;AAEA,EAAA;AAEA,EAAA;AAQA,EAAA;AAEA,EAAA;AAEA,EAAA;AAOA,EAAA;AAEA,EAAA;AAKA,EAAA;AAEA,EAAA;AAKA,EAAA;AAEA,EAAA;AAEA,EAAA;AA1DUC,EAAAA;AAAA;AA6DY;AAgES;AAEF;AAK3B;AACyD,EAAA;AACI,EAAA;AACF,EAAA;AACG,EAAA;AACF,EAAA;AACC,EAAA;AACf,EAAA;AACe,EAAA;AACT,EAAA;AACL,EAAA;AACD,EAAA;AACmB,EAAA;AACD,EAAA;AACjB,EAAA;AACnD;AAEsF;AACtE,EAAA;AACmC,EAAA;AACZ,EAAA;AACX,IAAA;AAC0B,MAAA;AAClD,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAG6C;AACY,EAAA;AACzD;AAGmD;AACK,EAAA;AACxD;AAWkF;AACrC,EAAA;AACD,EAAA;AACA,EAAA;AAEH,EAAA;AAEH,EAAA;AAC3B,IAAA;AACT,EAAA;AACwD,EAAA;AAC1D;AAGyE;AAC3B,EAAA;AACD,IAAA;AACxB,MAAA;AACjB,IAAA;AACF,EAAA;AACO,EAAA;AACT;AA6B+F;AACxE,EAAA;AAC6B,EAAA;AACR,EAAA;AACc,EAAA;AAC1D;AAkC2E;AAC3C,EAAA;AAC0B,EAAA;AACI,EAAA;AACF,EAAA;AACE,EAAA;AACG,EAAA;AACjC,IAAA;AAC9B,EAAA;AACmC,EAAA;AACvB,IAAA;AACkC,MAAA;AAC5C,IAAA;AACF,EAAA;AAC2D,EAAA;AACA,EAAA;AACO,IAAA;AACN,IAAA;AACvB,IAAA;AACoB,MAAA;AACvD,IAAA;AACF,EAAA;AAEsB,EAAA;AACS,IAAA;AAC/B,EAAA;AACF;AAcE;AAEc,EAAA;AACF,EAAA;AACR,EAAA;AAC6B,EAAA;AAChB,IAAA;AACL,MAAA;AACV,IAAA;AACF,EAAA;AAC+C,EAAA;AACtB,EAAA;AACT,EAAA;AAIX,EAAA;AAEQ,IAAA;AACa,IAAA;AAC1B,EAAA;AAEwC,EAAA;AACxB,EAAA;AAKyC,EAAA;AAC5C,IAAA;AACe,IAAA;AAC5B,EAAA;AAE0C,EAAA;AAC7B,IAAA;AACK,EAAA;AACyB,IAAA;AACQ,IAAA;AACU,IAAA;AAChD,IAAA;AACE,MAAA;AACI,MAAA;AACF,QAAA;AACb,MAAA;AACW,MAAA;AAC4B,MAAA;AACpB,MAAA;AACY,IAAA;AACpB,MAAA;AACa,MAAA;AACN,IAAA;AACP,MAAA;AACa,MAAA;AAC1B,IAAA;AACkB,EAAA;AACP,IAAA;AACa,IAAA;AAC1B,EAAA;AAO2C,EAAA;AACA,IAAA;AACkB,IAAA;AACN,IAAA;AACxC,MAAA;AACb,IAAA;AACF,EAAA;AAE+B,EAAA;AAKc,IAAA;AAC7B,IAAA;AACD,MAAA;AAC2C,MAAA;AACxD,IAAA;AACgD,IAAA;AACnC,MAAA;AACW,MAAA;AACxB,IAAA;AACc,IAAA;AACD,MAAA;AACb,IAAA;AACe,IAAA;AACF,MAAA;AACb,IAAA;AACF,EAAA;AAIW,EAAA;AACD,EAAA;AACsB,EAAA;AACA,IAAA;AACF,IAAA;AAC9B,EAAA;AAGgB,EAAA;AAEsC,EAAA;AACzC,IAAA;AACoB,IAAA;AACoB,MAAA;AACnD,IAAA;AACF,EAAA;AAEkB,EAAA;AACD,IAAA;AACoC,IAAA;AAEE,MAAA;AACH,MAAA;AAC9C,QAAA;AACF,MAAA;AACmD,MAAA;AACjD,QAAA;AACF,MAAA;AAMkB,MAAA;AAIhB,QAAA;AACF,MAAA;AAC6C,MAAA;AACnB,MAAA;AAMrB,MAAA;AAGkC,QAAA;AACvC,MAAA;AACF,IAAA;AACF,EAAA;AAE+B,EAAA;AACd,IAAA;AACoC,IAAA;AACjB,MAAA;AAC9B,QAAA;AACF,MAAA;AAGyD,MAAA;AACb,QAAA;AACpB,QAAA;AACxB,MAAA;AAI2C,MAAA;AACS,MAAA;AACT,QAAA;AACnB,QAAA;AACxB,MAAA;AAC8B,MAAA;AACA,MAAA;AACe,MAAA;AACH,QAAA;AAClB,QAAA;AACxB,MAAA;AACF,IAAA;AACF,EAAA;AAMiD,EAAA;AAE0B,EAAA;AAC9C,EAAA;AACR,IAAA;AACrB,EAAA;AACqD,EAAA;AACR,IAAA;AACf,IAAA;AACL,MAAA;AACvB,IAAA;AACF,EAAA;AAC+B,EAAA;AACM,IAAA;AACE,IAAA;AACnB,MAAA;AACwB,QAAA;AACxC,MAAA;AACD,IAAA;AACwB,IAAA;AACL,MAAA;AACpB,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAaqE;AAC5C,EAAA;AACyB,EAAA;AACE,IAAA;AACxB,MAAA;AACpB,QAAA;AACF,MAAA;AACyD,MAAA;AAC7C,QAAA;AACyB,QAAA;AAC1B,UAAA;AACT,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAUyE;AACf,EAAA;AACxD,EAAA;AACE,IAAA;AACA,IAAA;AACF,EAAA;AACgC,EAAA;AAC2B,EAAA;AACT,EAAA;AAC3B,EAAA;AACgB,EAAA;AACV,EAAA;AACD,EAAA;AACsB,EAAA;AAC3B,EAAA;AACM,EAAA;AACO,EAAA;AACiB,EAAA;AACV,EAAA;AACM,EAAA;AACd,EAAA;AACrC;AAGgF;AAC1B,EAAA;AACa,EAAA;AACnE;AAU2D;AACrD,EAAA;AACe,EAAA;AACqB,EAAA;AACf,IAAA;AACrB,MAAA;AACF,IAAA;AACiD,IAAA;AACpB,IAAA;AACZ,MAAA;AACR,MAAA;AACT,IAAA;AACF,EAAA;AACwB,EAAA;AACf,IAAA;AACT,EAAA;AAC+B,EAAA;AACjC;AA2BY;AACW,EAAA;AACS,EAAA;AACJ,EAAA;AAIU,EAAA;AACE,EAAA;AACZ,EAAA;AACd,IAAA;AACgD,MAAA;AAC1D,IAAA;AACF,EAAA;AACiD,EAAA;AACI,EAAA;AACI,EAAA;AACR,EAAA;AACS,IAAA;AACZ,IAAA;AAC1C,MAAA;AACF,IAAA;AAC2C,IAAA;AACpB,MAAA;AAChB,IAAA;AACY,MAAA;AACnB,IAAA;AACF,EAAA;AACgC,EAAA;AAClC;AjBg7D+D;AACA;AkBpuF/C;AAGA;AAQ8B;AAGG;AAGQ;AACnD,EAAA;AACC,EAAA;AACF,EAAA;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACC,EAAA;AACF,EAAA;AACD,EAAA;AACC,EAAA;AACA,EAAA;AACE,EAAA;AACA,EAAA;AACD,EAAA;AACC,EAAA;AACA,EAAA;AACA,EAAA;AACH,EAAA;AACC,EAAA;AACI,EAAA;AACD,EAAA;AACF,EAAA;AACI,EAAA;AACF,EAAA;AACF,EAAA;AACI,EAAA;AACD,EAAA;AACF,EAAA;AACI,EAAA;AACF,EAAA;AACF,EAAA;AACI,EAAA;AACJ,EAAA;AACA,EAAA;AACC,EAAA;AACE,EAAA;AACF,EAAA;AACE,EAAA;AACF,EAAA;AACA,EAAA;AACI,EAAA;AACH,EAAA;AACH,EAAA;AACD,EAAA;AACE,EAAA;AACC,EAAA;AACD,EAAA;AACA,EAAA;AACD,EAAA;AACC,EAAA;AACA,EAAA;AACC,EAAA;AACD,EAAA;AACF,EAAA;AACA,EAAA;AACE,EAAA;AACD,EAAA;AACE,EAAA;AACA,EAAA;AACC,EAAA;AACA,EAAA;AACF,EAAA;AACA,EAAA;AACD,EAAA;AACI,EAAA;AACD,EAAA;AACC,EAAA;AACD,EAAA;AACD,EAAA;AACC,EAAA;AACH,EAAA;AACG,EAAA;AACD,EAAA;AACF,EAAA;AACM,EAAA;AACF,EAAA;AACJ,EAAA;AACE,EAAA;AACF,EAAA;AACE,EAAA;AACA,EAAA;AACM,EAAA;AACF,EAAA;AACF,EAAA;AACD,EAAA;AACH,EAAA;AACK,EAAA;AAAA;AAAA;AAGP,EAAA;AACC,EAAA;AACD,EAAA;AACC,EAAA;AACN;AASgE;AACN,EAAA;AAC1D;AAGgF;AAC/D,EAAA;AACA,EAAA;AACD,EAAA;AACD,EAAA;AACA,EAAA;AACA,EAAA;AACE,EAAA;AACA,EAAA;AACJ,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACf;AAaO;AACiC,EAAA;AACA,EAAA;AACxC;AAS+E;AAC/D,EAAA;AACA,EAAA;AACF,EAAA;AACC,EAAA;AACA,EAAA;AACf;AAGiD;AAGI;AAC9C,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACC,EAAA;AACA,EAAA;AACR;AAG6F;AAGnB;AAGE;AAC3D,EAAA;AACA,EAAA;AACD,EAAA;AAChB;AAGuE;AAC3D,EAAA;AACF,EAAA;AACV;AAGyE;AAC/D,EAAA;AACD,EAAA;AACT;AA2ByE;AAC9C,EAAA;AAC3B;AAU2F;AACvD,EAAA;AACiB,IAAA;AACnD,EAAA;AACsD,EAAA;AACxD;AAGwE;AAC9C,EAAA;AACkC,EAAA;AAC5D;AAGwE;AACjB,EAAA;AACjB,IAAA;AACzB,MAAA;AACT,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAO0C;AACpC,EAAA;AACgC,EAAA;AACwB,IAAA;AACf,MAAA;AAC3C,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAG4F;AACnD,EAAA;AACX,IAAA;AACM,IAAA;AACU,MAAA;AAC1C,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAoBsD;AACrC,EAAA;AACQ,IAAA;AACrB,IAAA;AACF,EAAA;AAGW,EAAA;AACG,EAAA;AACY,IAAA;AAC1B,EAAA;AACgB,EAAA;AACW,IAAA;AACiB,IAAA;AACnB,MAAA;AACvB,IAAA;AACF,EAAA;AACgB,EAAA;AACW,IAAA;AAC3B,EAAA;AACF;AASqF;AAChC,EAAA;AAC3B,EAAA;AACJ,IAAA;AACN,IAAA;AACd,EAAA;AACgC,EAAA;AAGjB,IAAA;AACG,IAAA;AACW,IAAA;AACf,IAAA;AACd,EAAA;AACgC,EAAA;AACgB,IAAA;AACpB,IAAA;AACjB,MAAA;AACT,IAAA;AACgC,IAAA;AAClB,IAAA;AAChB,EAAA;AACoD,EAAA;AACzB,EAAA;AAC2B,IAAA;AAC1B,IAAA;AACjB,MAAA;AACT,IAAA;AACgC,IAAA;AAClB,IAAA;AAChB,EAAA;AACyD,EAAA;AACxB,EAAA;AACoB,EAAA;AAC3B,EAAA;AACjB,IAAA;AACT,EAAA;AAOuD,EAAA;AAC9C,IAAA;AACT,EAAA;AAC2D,EAAA;AAC7C,EAAA;AAChB;AASgD;AACS,EAAA;AACzD;AAO0C;AACgB,EAAA;AACrB,EAAA;AACrC;AA0ByD;AAC7B,EAAA;AACoB,EAAA;AAChB,IAAA;AAC2B,MAAA;AACvD,IAAA;AACF,EAAA;AAC0C,EAAA;AACX,IAAA;AAC3B,MAAA;AACF,IAAA;AACM,IAAA;AAEqC,MAAA;AAE3C,IAAA;AACF,EAAA;AACwC,EAAA;AAGb,EAAA;AAC7B;AAc6D;AAChD,EAAA;AACe,EAAA;AACb,EAAA;AACA,EAAA;AACY,EAAA;AACL,IAAA;AACJ,MAAA;AACH,QAAA;AACT,MAAA;AACS,MAAA;AACA,MAAA;AACc,IAAA;AACV,MAAA;AACJ,QAAA;AACT,MAAA;AACS,MAAA;AACS,MAAA;AACD,IAAA;AACP,MAAA;AACL,IAAA;AACG,MAAA;AACV,IAAA;AACF,EAAA;AACY,EAAA;AACH,IAAA;AACT,EAAA;AACsC,EAAA;AACO,IAAA;AACnB,IAAA;AACf,MAAA;AACT,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAeyB;AACE,EAAA;AACjB,IAAA;AACe,IAAA;AACF,IAAA;AACA,IAAA;AACF,IAAA;AACV,IAAA;AACT,EAAA;AACS,EAAA;AACS,EAAA;AACI,EAAA;AACK,EAAA;AACZ,IAAA;AACe,IAAA;AACZ,MAAA;AACkB,MAAA;AACK,IAAA;AAChB,MAAA;AACc,MAAA;AACT,QAAA;AACQ,QAAA;AAClC,MAAA;AACF,IAAA;AACF,EAAA;AACkB,EAAA;AACiC,IAAA;AACvB,IAAA;AACV,MAAA;AACiB,MAAA;AACD,MAAA;AAClB,MAAA;AACd,IAAA;AACF,EAAA;AACmC,EAAA;AACT,EAAA;AACZ,IAAA;AACe,IAAA;AAC+B,EAAA;AAG1C,IAAA;AAClB,EAAA;AACyB,EAAA;AAKiC,IAAA;AAChD,MAAA;AACN,MAAA;AACF,IAAA;AACwC,IAAA;AACD,IAAA;AAC9B,MAAA;AACT,IAAA;AACK,IAAA;AACP,EAAA;AAC4B,EAAA;AACmB,IAAA;AAClB,MAAA;AACvB,QAAA;AACF,MAAA;AACyC,MAAA;AACb,MAAA;AACnB,QAAA;AACT,MAAA;AACF,IAAA;AACF,EAAA;AACO,EAAA;AACG,IAAA;AACI,IAAA;AAC4C,IAAA;AACb,IAAA;AACb,IAAA;AACA,IAAA;AAChC,EAAA;AACF;AAQ2D;AACtB,EAAA;AACqB,EAAA;AAC1D;AAS0D;AACrB,EAAA;AACrC;AAS0E;AAC9B,EAAA;AAC5B,EAAA;AACL,IAAA;AACT,EAAA;AACsD,EAAA;AACxD;AAoB8E;AAClD,EAAA;AACQ,IAAA;AACT,IAAA;AAGS,IAAA;AAClC,EAAA;AACyB,EAAA;AACI,EAAA;AAC2B,EAAA;AACb,EAAA;AACO,IAAA;AACxB,IAAA;AACqB,MAAA;AAC7C,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAG0E;AAC9C,EAAA;AACkC,IAAA;AAC5D,EAAA;AAC4B,EAAA;AAC9B;AAG+E;AACT,EAAA;AAC5C,EAAA;AACe,IAAA;AACvC,EAAA;AACkC,EAAA;AACsB,EAAA;AAChC,EAAA;AACkC,IAAA;AAC1D,EAAA;AACO,EAAA;AACT;AAUqB;AAGoB,EAAA;AAChB,EAAA;AACiC,IAAA;AACW,IAAA;AACd,IAAA;AACA,MAAA;AACnD,IAAA;AACF,EAAA;AAC4B,EAAA;AACuC,EAAA;AACrE;AlBk9E+D;AACA;AmBtrGD;AACvD,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACC,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACG,EAAA;AACA,EAAA;AACC,EAAA;AACA,EAAA;AACJ,EAAA;AACG,EAAA;AACF,EAAA;AACF,EAAA;AACC,EAAA;AACC,EAAA;AACD,EAAA;AACA,EAAA;AACA,EAAA;AACC,EAAA;AACD,EAAA;AACA,EAAA;AACA,EAAA;AACC,EAAA;AACA,EAAA;AACC,EAAA;AACA,EAAA;AACF,EAAA;AACA,EAAA;AACD,EAAA;AACI,EAAA;AACF,EAAA;AACC,EAAA;AACA,EAAA;AACD,EAAA;AACI,EAAA;AACJ,EAAA;AACA,EAAA;AACE,EAAA;AACC,EAAA;AACZ;AAYqB;AAIkD;AAC7C,EAAA;AACqC,IAAA;AAC7D,EAAA;AACF;AAUgE;AAC1D,EAAA;AACQ,EAAA;AACwC,EAAA;AACI,IAAA;AAIpD,IAAA;AAKA,MAAA;AACF,IAAA;AACO,IAAA;AACC,IAAA;AACV,EAAA;AACO,EAAA;AACT;AAGsD;AAC/C,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACC,EAAA;AACA,EAAA;AACC,EAAA;AACT;AAG4D;AACpD,EAAA;AACA,EAAA;AACA,EAAA;AACR;AAOiC;AAWiB;AACd,EAAA;AACF,EAAA;AACvB,IAAA;AACT,EAAA;AAC2C,EAAA;AACO,EAAA;AACxB,IAAA;AAC1B,EAAA;AACyB,EAAA;AAChB,IAAA;AACT,EAAA;AAC2C,EAAA;AAC7C;AAS4E;AACrB,EAAA;AACV,EAAA;AACmB,IAAA;AAC1D,MAAA;AACF,IAAA;AAC0B,IAAA;AACgC,IAAA;AAC5D,EAAA;AACqC,EAAA;AACY,IAAA;AACA,MAAA;AAC/C,IAAA;AACF,EAAA;AAC0D,EAAA;AAC5D;AAYoD;AACV,EAAA;AACf,EAAA;AACM,IAAA;AAC/B,EAAA;AAC6B,EAAA;AACqB,EAAA;AACQ,EAAA;AACjB,EAAA;AACoB,EAAA;AAC/D;AnBmnG+D;AACA;AoB9yGP;AACN,EAAA;AAClD;AAG0E;AACd,EAAA;AAC5D;AAWuE;AAC7B,EAAA;AACqB,EAAA;AAC/D;AAUoE;AAClD,EAAA;AACgB,IAAA;AACA,IAAA;AAChC,EAAA;AACqD,EAAA;AACvD;AAiBU;AAC0D,EAAA;AAClC,IAAA;AAChC,EAAA;AAC6D,EAAA;AAC/D;AAQ+F;AAC9B,EAAA;AACjE;AAaiB;AACmC,EAAA;AACpD;AAS+F;AACxC,EAAA;AACA,EAAA;AACW,EAAA;AAClE;AAmCE;AAE6C,EAAA;AACa,EAAA;AAG5D;AAGgG;AACjC,EAAA;AAC/D;AAYiB;AAEX,EAAA;AAEN;ApB4rG+D;AACA;AqBrwGe;AAC1B,EAAA;AACpD;AAuB8F;AAGpD,EAAA;AACpC,EAAA;AAC4B,IAAA;AACA,MAAA;AAC9B,IAAA;AAC+B,IAAA;AAI5B,IAAA;AAKsC,IAAA;AAChB,IAAA;AAC8B,MAAA;AACvD,IAAA;AACqC,IAAA;AACY,MAAA;AACtB,MAAA;AACS,QAAA;AAClC,MAAA;AACF,IAAA;AAC2D,IAAA;AAC7C,EAAA;AACW,IAAA;AAC3B,EAAA;AACF;AAyDuF;AAIvD,EAAA;AACyB,EAAA;AACC,IAAA;AACxD,EAAA;AACwC,EAAA;AACT,EAAA;AACM,EAAA;AACA,EAAA;AACa,EAAA;AACU,EAAA;AACjB,EAAA;AACc,EAAA;AACI,EAAA;AACtB,IAAA;AACa,IAAA;AAMA,IAAA;AAE9C,IAAA;AACE,MAAA;AACmD,MAAA;AACnD,MAAA;AAEF,IAAA;AAWkC,IAAA;AAE9B,MAAA;AACE,QAAA;AACA,QAAA;AACA,QAAA;AAC8B,yBAAA;AAGnB,QAAA;AAEb,MAAA;AACA,MAAA;AAE8C,IAAA;AAChC,IAAA;AACxB,EAAA;AACO,EAAA;AACT;AAGsE;AACF,EAAA;AACpE;AA8CkB;AAEW,EAAA;AACsB,EAAA;AACA,EAAA;AACW,EAAA;AACC,IAAA;AAC7D,EAAA;AACyD,EAAA;AACZ,EAAA;AAE7B,EAAA;AAEA,EAAA;AACO,EAAA;AACA,EAAA;AAKoC,EAAA;AACM,IAAA;AACtD,MAAA;AACT,IAAA;AACgD,IAAA;AACM,IAAA;AACxD,EAAA;AAC+B,EAAA;AACF,EAAA;AAO0B,IAAA;AAClC,IAAA;AACiC,MAAA;AAClB,MAAA;AACL,MAAA;AACM,MAAA;AACnC,IAAA;AACqD,IAAA;AACvD,EAAA;AAM+C,EAAA;AAI7C,EAAA;AAC4D,IAAA;AAC5D,EAAA;AACsB,EAAA;AACoB,IAAA;AAC5C,EAAA;AAC2C,EAAA;AACe,IAAA;AAC1D,EAAA;AACO,EAAA;AACT;AA+BU;AACuB,EAAA;AACU,EAAA;AACsB,EAAA;AAC/B,EAAA;AACyB,IAAA;AACzD,EAAA;AAC4D,EAAA;AAClB,EAAA;AACJ,IAAA;AACtC,EAAA;AAC0C,EAAA;AACJ,IAAA;AACtC,EAAA;AAC2C,EAAA;AACJ,IAAA;AACvC,EAAA;AACyC,EAAA;AAC3C;ArBqjG+D;AACA;AsBn9GlB;AACP,EAAA;AACN,IAAA;AAC9B,EAAA;AACoB,EAAA;AACiC,IAAA;AACrC,IAAA;AAAA;AAAA;AAAA;AAIY,IAAA;AACK,MAAA;AAC/B,IAAA;AACF,EAAA;AAC+B,EAAA;AAC4B,IAAA;AAC3D,EAAA;AAIqC,EAAA;AACV,IAAA;AAC3B,EAAA;AACqC,EAAA;AACV,IAAA;AAC3B,EAAA;AACsC,EAAA;AACV,IAAA;AAC5B,EAAA;AACO,EAAA;AACT;AAyBqD;AACpB,EAAA;AACA,IAAA;AAC/B,EAAA;AACiD,EAAA;AAIA,IAAA;AAKX,IAAA;AACtC,EAAA;AACU,EAAA;AACgD,IAAA;AAC1D,EAAA;AACF;AtBm7G+D;AACA;AuB//GA;AACN,EAAA;AACzD;AASsE;AACV,EAAA;AAC5D;AASgF;AACzB,EAAA;AACvD;AASqD;AACxB,EAAA;AAC7B;AAW6D;AACP,EAAA;AACtD;AAYwD;AACK,EAAA;AAC7D;AAU2D;AACI,EAAA;AAC/D;AAewE;AACnB,EAAA;AAC1C,IAAA;AACT,EAAA;AAC0B,EAAA;AACjB,IAAA;AACT,EAAA;AACyD,EAAA;AAChB,EAAA;AAC3C;AAa0E;AAGtE,EAAA;AAGJ;AAa+B;AAU4B;AACA,EAAA;AAC3D;AAGgG;AACjE,EAAA;AACtB,EAAA;AACqD,IAAA;AACA,IAAA;AAC5D,EAAA;AACF;AAkBuE;AACT,EAAA;AACpB,EAAA;AACN,EAAA;AACoB,EAAA;AAC1B,IAAA;AACA,IAAA;AACxB,MAAA;AACF,IAAA;AACsC,IAAA;AACf,IAAA;AACrB,MAAA;AACF,IAAA;AAK8C,IAAA;AACW,IAAA;AAC3D,EAAA;AACO,EAAA;AACT;AAiB+F;AAC3D,EAAA;AAER,EAAA;AACqC,IAAA;AACxD,EAAA;AAET;AAaqE;AACN,EAAA;AAC/D;AAQuD;AACnB,EAAA;AACpC;AAGyF;AASpB;AACnE,EAAA;AACF;AAO0F;AAC9C,EAAA;AACmB,EAAA;AACpD,IAAA;AACT,EAAA;AAC4D,EAAA;AAC9D;AAa6D;AACD,EAAA;AAC5D;AAS2E;AAChB,EAAA;AAC3D;AAY+E;AACvB,EAAA;AAChC,EAAA;AACmC,IAAA;AACzD,EAAA;AACqB,EAAA;AACe,IAAA;AACpC,EAAA;AACO,EAAA;AACT;AAa8D;AACpB,EAAA;AACS,IAAA;AACH,IAAA;AACnC,MAAA;AACT,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAG4D;AAClB,EAAA;AACS,IAAA;AACL,IAAA;AACjC,MAAA;AACT,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAeqE;AACX,EAAA;AAC1D;AAGuD;AAqBrD;AAEoD,EAAA;AAC3C,IAAA;AACT,EAAA;AACmC,EAAA;AACuB,EAAA;AAC5D;AvB0vG+D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/libcantus/libcantus/ts/dist/chunk-YDGMWH43.cjs","sourcesContent":[null,"import { InvalidInputError } from '../../core/errors/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport {\n assertArray,\n assertInteger,\n assertOneOf,\n assertRecord,\n describeRejected,\n} from '../../core/validation/index.js';\nimport type { KeyVariant } from './kinds.js';\n\n/**\n * Build a 12-bit mode mask from a list of semitone offsets above the root.\n *\n * Bit 0 (the root) is always set, enforcing the `KeyScale` invariant that the\n * root is a scale tone even when the offset list omits 0.\n *\n * @category Scales\n */\nexport function maskFromOffsets(offsets: readonly number[]): number {\n assertArray(offsets, 'offsets');\n let mask = 1;\n for (const [index, offset] of offsets.entries()) {\n assertInteger(offset, `offsets[${index}]`);\n mask |= 1 << (((offset % 12) + 12) % 12);\n }\n return mask;\n}\n\n/**\n * Mode mask for the major (Ionian) scale: offsets {0, 2, 4, 5, 7, 9, 11}.\n *\n * @category Scales\n */\nexport const MAJOR_MASK = 0b101010110101;\n\n/**\n * Mode mask for the natural minor (Aeolian) scale: offsets {0, 2, 3, 5, 7, 8, 10}.\n *\n * @category Scales\n */\nexport const NATURAL_MINOR_MASK = 0b010110101101;\n\n/**\n * Harmonic minor: natural minor with a raised seventh — offsets {0,2,3,5,7,8,11}.\n *\n * @category Scales\n */\nexport const HARMONIC_MINOR_MASK = maskFromOffsets([0, 2, 3, 5, 7, 8, 11]);\n\n/**\n * Ascending melodic minor: offsets {0,2,3,5,7,9,11}.\n *\n * @category Scales\n */\nexport const MELODIC_MINOR_MASK = maskFromOffsets([0, 2, 3, 5, 7, 9, 11]);\n\n/**\n * Dorian mode: offsets {0,2,3,5,7,9,10}.\n *\n * @category Scales\n */\nexport const DORIAN_MASK = maskFromOffsets([0, 2, 3, 5, 7, 9, 10]);\n\n/**\n * Phrygian mode: offsets {0,1,3,5,7,8,10}.\n *\n * @category Scales\n */\nexport const PHRYGIAN_MASK = maskFromOffsets([0, 1, 3, 5, 7, 8, 10]);\n\n/**\n * Lydian mode: offsets {0,2,4,6,7,9,11}.\n *\n * @category Scales\n */\nexport const LYDIAN_MASK = maskFromOffsets([0, 2, 4, 6, 7, 9, 11]);\n\n/**\n * Mixolydian mode: offsets {0,2,4,5,7,9,10}.\n *\n * @category Scales\n */\nexport const MIXOLYDIAN_MASK = maskFromOffsets([0, 2, 4, 5, 7, 9, 10]);\n\n/**\n * Locrian mode: offsets {0,1,3,5,6,8,10}.\n *\n * @category Scales\n */\nexport const LOCRIAN_MASK = maskFromOffsets([0, 1, 3, 5, 6, 8, 10]);\n\n/**\n * Major pentatonic: offsets {0,2,4,7,9}.\n *\n * @category Scales\n */\nexport const MAJOR_PENTATONIC_MASK = maskFromOffsets([0, 2, 4, 7, 9]);\n\n/**\n * Minor pentatonic: offsets {0,3,5,7,10}.\n *\n * @category Scales\n */\nexport const MINOR_PENTATONIC_MASK = maskFromOffsets([0, 3, 5, 7, 10]);\n\n/**\n * Hexatonic blues scale: minor pentatonic plus the flat-fifth — {0,3,5,6,7,10}.\n *\n * @category Scales\n */\nexport const BLUES_MASK = maskFromOffsets([0, 3, 5, 6, 7, 10]);\n\n/**\n * Whole-tone scale: offsets {0,2,4,6,8,10}.\n *\n * @category Scales\n */\nexport const WHOLE_TONE_MASK = maskFromOffsets([0, 2, 4, 6, 8, 10]);\n\n/**\n * Octatonic (half-step first): offsets {0,1,3,4,6,7,9,10}.\n *\n * @category Scales\n */\nexport const OCTATONIC_HALF_WHOLE_MASK = maskFromOffsets([0, 1, 3, 4, 6, 7, 9, 10]);\n\n/**\n * Octatonic (whole-step first): offsets {0,2,3,5,6,8,9,11}.\n *\n * @category Scales\n */\nexport const OCTATONIC_WHOLE_HALF_MASK = maskFromOffsets([0, 2, 3, 5, 6, 8, 9, 11]);\n\n/**\n * Chromatic scale: all twelve pitch classes.\n *\n * @category Scales\n */\nexport const CHROMATIC_MASK = 0b111111111111;\n\n/**\n * Require a twelve-bit mode mask, and return it.\n *\n * A mask is read with bit operations, and those coerce: a `null` reads as the\n * empty mask, an object as zero. Every answer taken from a mask is a musical\n * claim — which form a key stands in, whether its signature is its own, which\n * system it belongs to — so a value that is not a mask has to be refused rather\n * than answered, and it is refused in one place so the answers cannot disagree\n * about what a mask is.\n *\n * @param modeMask12 The value to check.\n * @param name What the value is, for the error message.\n * @returns The mask.\n * @throws If it is not an integer in [1, {@link CHROMATIC_MASK}].\n * @example\n * ```ts\n * import { assertModeMask, MAJOR_MASK } from '@libraz/libcantus';\n * assertModeMask(MAJOR_MASK); // 2741\n * ```\n * @category Scales\n */\nexport function assertModeMask(modeMask12: number, name = 'modeMask12'): number {\n return assertInteger(modeMask12, name, 1, CHROMATIC_MASK);\n}\n\n/**\n * The checked mode mask of a key or scale.\n *\n * The companion of {@link assertModeMask} for the entrances that are handed the\n * record rather than the number: the record is read as one before the field is,\n * so a key restored without its scale is refused instead of answering from a\n * mask that was never there.\n *\n * @param scale The key or scale to read.\n * @param name What the value is, for the error message.\n * @returns Its mode mask.\n * @throws If it is not a record carrying a mode mask.\n */\nexport function maskOf(scale: KeyScale, name = 'key'): number {\n return assertModeMask(assertRecord<KeyScale>(scale, name).modeMask12, `${name}.modeMask12`);\n}\n\n/**\n * Lydian dominant (melodic minor's fourth mode): offsets {0,2,4,6,7,9,10}.\n *\n * The scale over an unaltered dominant with a raised eleventh.\n *\n * @category Scales\n */\nexport const LYDIAN_DOMINANT_MASK = maskFromOffsets([0, 2, 4, 6, 7, 9, 10]);\n\n/**\n * Mixolydian b13, the aeolian dominant (melodic minor's fifth mode): offsets\n * {0,2,4,5,7,8,10}.\n *\n * The scale over a dominant with a flat thirteenth.\n *\n * @category Scales\n */\nexport const MIXOLYDIAN_B13_MASK = maskFromOffsets([0, 2, 4, 5, 7, 8, 10]);\n\n/**\n * Locrian natural 2 (melodic minor's sixth mode): offsets {0,2,3,5,6,8,10}.\n *\n * The scale over a half-diminished chord used as a minor ii.\n *\n * @category Scales\n */\nexport const LOCRIAN_NATURAL2_MASK = maskFromOffsets([0, 2, 3, 5, 6, 8, 10]);\n\n/**\n * The altered scale, or superlocrian (melodic minor's seventh mode): offsets\n * {0,1,3,4,6,8,10}.\n *\n * The scale over an altered dominant: every tension raised or lowered.\n *\n * @category Scales\n */\nexport const ALTERED_MASK = maskFromOffsets([0, 1, 3, 4, 6, 8, 10]);\n\n/**\n * Phrygian dominant (harmonic minor's fifth mode): offsets {0,1,4,5,7,8,10}.\n *\n * The scale over the dominant of a minor key, and the characteristic sound of\n * the flamenco and maqam repertoires.\n *\n * @category Scales\n */\nexport const PHRYGIAN_DOMINANT_MASK = maskFromOffsets([0, 1, 4, 5, 7, 8, 10]);\n\n/**\n * Miyako-bushi (都節), the hemitonic pentatonic of Japanese urban art music —\n * shamisen and koto song: offsets {0,1,5,7,8}.\n *\n * Two miyako-bushi tetrachords a fourth apart, in the terms of Koizumi's\n * tetrachord theory: between the two nuclear tones of each tetrachord sits a\n * note a semitone above the lower one.\n *\n * The mask fixes the pitch classes and nothing else: the repertoire's\n * intonation is not twelve-tone equal temperament, and the scale is a pair of\n * tetrachords around nuclear tones rather than a ladder of degrees above a\n * tonic. Cents and non-12 temperaments are the tuning module's business.\n *\n * @category Scales\n */\nexport const MIYAKO_BUSHI_MASK = maskFromOffsets([0, 1, 5, 7, 8]);\n\n/**\n * Ritsu (律), the anhemitonic pentatonic of Japanese court music and Buddhist\n * chant: offsets {0,2,5,7,9}.\n *\n * Two ritsu tetrachords a fourth apart: the middle note sits a whole tone above\n * the lower nuclear tone. The result is the same five pitch classes as the\n * major pentatonic a fourth above, which is why the mask alone cannot say which\n * tradition is meant.\n *\n * @category Scales\n */\nexport const RITSU_MASK = maskFromOffsets([0, 2, 5, 7, 9]);\n\n/**\n * Ryūkyū (琉球), the pentatonic of Okinawan music: offsets {0,4,5,7,11}.\n *\n * Two ryūkyū tetrachords a fourth apart: the middle note sits a major third\n * above the lower nuclear tone, which is what puts the two semitone steps under\n * the fourth and the octave.\n *\n * @category Scales\n */\nexport const RYUKYU_MASK = maskFromOffsets([0, 4, 5, 7, 11]);\n\n/**\n * The heptatonic set with two augmented seconds: offsets {0,1,4,5,7,8,11}.\n *\n * Maqam Hijazkar in Arabic practice, thaat Bhairav in Hindustani practice, and\n * the double harmonic major scale in English-language scale lists — one\n * pitch-class set that three traditions arrived at separately and treat\n * differently.\n *\n * @category Scales\n */\nexport const DOUBLE_HARMONIC_MASK = maskFromOffsets([0, 1, 4, 5, 7, 8, 11]);\n\n/**\n * Thaat Todi of Hindustani classical music — S r g M' P d N: offsets\n * {0,1,3,6,7,8,11}.\n *\n * @category Scales\n */\nexport const TODI_MASK = maskFromOffsets([0, 1, 3, 6, 7, 8, 11]);\n\n/**\n * Thaat Marwa of Hindustani classical music — S r G M' P D N: offsets\n * {0,1,4,6,7,9,11}.\n *\n * The thaat carries the fifth; raga Marwa itself leaves it out, which is the\n * kind of distinction a pitch-class mask cannot make.\n *\n * @category Scales\n */\nexport const MARWA_MASK = maskFromOffsets([0, 1, 4, 6, 7, 9, 11]);\n\n/**\n * Thaat Purvi of Hindustani classical music — S r G M' P d N: offsets\n * {0,1,4,6,7,8,11}.\n *\n * @category Scales\n */\nexport const PURVI_MASK = maskFromOffsets([0, 1, 4, 6, 7, 8, 11]);\n\n/**\n * Named scale masks addressable by {@link scaleByName}.\n *\n * These are the scales of Western common practice and of jazz, the vocabulary\n * chord-scale theory is stated in. Scales belonging to traditions that chord\n * function does not organise live in {@link WORLD_SCALES}, which\n * {@link scaleByName} resolves just as readily; {@link scaleSystemOf} says\n * which kind a name is.\n *\n * @category Scales\n */\nexport const NAMED_SCALES = Object.freeze({\n major: MAJOR_MASK,\n ionian: MAJOR_MASK,\n naturalMinor: NATURAL_MINOR_MASK,\n aeolian: NATURAL_MINOR_MASK,\n harmonicMinor: HARMONIC_MINOR_MASK,\n melodicMinor: MELODIC_MINOR_MASK,\n dorian: DORIAN_MASK,\n phrygian: PHRYGIAN_MASK,\n lydian: LYDIAN_MASK,\n mixolydian: MIXOLYDIAN_MASK,\n locrian: LOCRIAN_MASK,\n lydianDominant: LYDIAN_DOMINANT_MASK,\n mixolydianB13: MIXOLYDIAN_B13_MASK,\n locrianNatural2: LOCRIAN_NATURAL2_MASK,\n altered: ALTERED_MASK,\n phrygianDominant: PHRYGIAN_DOMINANT_MASK,\n majorPentatonic: MAJOR_PENTATONIC_MASK,\n minorPentatonic: MINOR_PENTATONIC_MASK,\n blues: BLUES_MASK,\n wholeTone: WHOLE_TONE_MASK,\n octatonicHalfWhole: OCTATONIC_HALF_WHOLE_MASK,\n octatonicWholeHalf: OCTATONIC_WHOLE_HALF_MASK,\n chromatic: CHROMATIC_MASK,\n});\n\n/**\n * The name of a built-in scale, as accepted by {@link scaleByName},\n * {@link avoidNotes}, and {@link availableTensions}.\n *\n * @category Scales\n */\nexport type ScaleName = keyof typeof NAMED_SCALES;\n\n/**\n * Scale masks from traditions outside Western common practice, addressable by\n * {@link scaleByName} exactly as {@link NAMED_SCALES} is.\n *\n * They are kept apart from {@link NAMED_SCALES} because chord-scale theory is a\n * Western practice: a raga offered as the scale over an augmented major seventh\n * would read as an answer while being a category error. Nothing else separates\n * them — a `KeyScale` built from one of these masks behaves like any other.\n *\n * Three limits are worth stating plainly rather than leaving a caller to\n * discover:\n *\n * - A twelve-bit mask can only hold what twelve-tone equal temperament can\n * express. The maqamat that turn on half-flat degrees — Rast, Bayati, Saba,\n * Sikah — are therefore absent rather than approximated, and the entries that\n * are here are not tuned the way their traditions tune them. Cents, arbitrary\n * equal divisions and just ratios live in the tuning module.\n * - A thaat is the pitch material a raga draws on, not the raga: ascent and\n * descent, the notes a phrase leans on, and the ornaments belong to the raga\n * and have no place in a mask.\n * - Several of these sets are the same pitch classes as a Western mode. The\n * name is what carries the tradition, so pass the name, not the mask,\n * wherever the tradition matters ({@link scaleSystemOf} reads both).\n *\n * The second limit is the general case: an entry is the pitch material a\n * tradition names, never the modal system built on it. A maqam is assembled\n * from ajnas that are transposed and exchanged as a phrase moves, and Koizumi's\n * tetrachord theory describes the Japanese scales by the nuclear tones framing\n * each tetrachord, so `miyakoBushi` and `minyo` differ by tetrachord type — a\n * difference their pitch classes record without explaining. Reach for these\n * entries when a pitch set is what is wanted.\n *\n * @category Scales\n */\nexport const WORLD_SCALES = Object.freeze({\n /** Miyako-bushi (都節), the hemitonic pentatonic of Japanese art music. */\n miyakoBushi: MIYAKO_BUSHI_MASK,\n /** Ritsu (律), the anhemitonic pentatonic of Japanese court music and chant. */\n ritsu: RITSU_MASK,\n /**\n * Min'yō (民謡), the pentatonic of Japanese folk song, called inaka-bushi\n * (田舎節) opposite miyako-bushi. The same five pitch classes as the minor\n * pentatonic.\n */\n minyo: MINOR_PENTATONIC_MASK,\n /** Ryūkyū (琉球), the pentatonic of Okinawan music. */\n ryukyu: RYUKYU_MASK,\n /** Maqam Ajam of Arabic practice; the same pitch classes as the major scale. */\n ajam: MAJOR_MASK,\n /**\n * Maqam Nahawand of Arabic practice; the same pitch classes as the natural\n * minor, though practice raises the seventh degree at a cadence.\n */\n nahawand: NATURAL_MINOR_MASK,\n /** Maqam Kurd of Arabic practice; the same pitch classes as the phrygian mode. */\n kurd: PHRYGIAN_MASK,\n /**\n * Maqam Hijaz of Arabic practice; the same pitch classes as the phrygian\n * dominant. Played intonation stretches the augmented second beyond its\n * tempered size, which is the sound the mask cannot carry.\n */\n hijaz: PHRYGIAN_DOMINANT_MASK,\n /** Maqam Hijazkar of Arabic practice: Hijaz over Hijaz, with two augmented seconds. */\n hijazkar: DOUBLE_HARMONIC_MASK,\n /** Thaat Bilaval of Hindustani classical music; the same pitch classes as the major scale. */\n bilaval: MAJOR_MASK,\n /** Thaat Khamaj; the same pitch classes as the mixolydian mode. */\n khamaj: MIXOLYDIAN_MASK,\n /** Thaat Kafi; the same pitch classes as the dorian mode. */\n kafi: DORIAN_MASK,\n /** Thaat Asavari; the same pitch classes as the natural minor. */\n asavari: NATURAL_MINOR_MASK,\n /** Thaat Bhairavi; the same pitch classes as the phrygian mode. */\n bhairavi: PHRYGIAN_MASK,\n /** Thaat Bhairav, with komal Re and komal Dha against a natural third and seventh. */\n bhairav: DOUBLE_HARMONIC_MASK,\n /** Thaat Kalyan; the same pitch classes as the lydian mode. */\n kalyan: LYDIAN_MASK,\n /** Thaat Marwa, with komal Re and tivra Ma. */\n marwa: MARWA_MASK,\n /** Thaat Purvi, Marwa's komal Dha counterpart. */\n purvi: PURVI_MASK,\n /** Thaat Todi, with komal Re, komal Ga, komal Dha and tivra Ma. */\n todi: TODI_MASK,\n});\n\n/**\n * The name of a scale in {@link WORLD_SCALES}.\n *\n * @category Scales\n */\nexport type WorldScaleName = keyof typeof WORLD_SCALES;\n\n/**\n * Alternative names that resolve to a canonical scale name.\n *\n * A scale carrying several names gets one canonical entry and its other names\n * here, so that two spellings of one tradition's scale cannot drift into two\n * entries with two masks. Transliterations vary, and one pitch-class set is\n * often named differently in different repertoires.\n *\n * @category Scales\n */\nexport const SCALE_ALIASES = Object.freeze({\n /** Inaka-bushi (田舎節), the name the in/yō pairing gives the min'yō scale. */\n inakaBushi: 'minyo',\n /** The Okinawan scale, the usual English name for the ryūkyū scale. */\n okinawan: 'ryukyu',\n /** Turkish spelling of Hijaz. */\n hicaz: 'hijaz',\n /** The English-language scale-list name for the Hijazkar/Bhairav set. */\n doubleHarmonic: 'hijazkar',\n /** Transliteration variant of Bilaval. */\n bilawal: 'bilaval',\n /** Transliteration variant of Marwa. */\n marva: 'marwa',\n /** Transliteration variant of Purvi. */\n poorvi: 'purvi',\n} as const satisfies Readonly<Record<string, ScaleName | WorldScaleName>>);\n\n/**\n * An alternative scale name that {@link resolveScaleName} maps to a canonical one.\n *\n * @category Scales\n */\nexport type ScaleAliasName = keyof typeof SCALE_ALIASES;\n\n/**\n * A scale name that completes to the built-in names but still accepts any\n * string, so a caller with a name from configuration is not forced to cast.\n *\n * @category Scales\n */\nexport type ScaleNameInput = ScaleName | (string & {});\n\n/**\n * The canonical name a scale name stands for, or undefined when it names no\n * built-in scale.\n *\n * A canonical name resolves to itself; an entry of {@link SCALE_ALIASES}\n * resolves to the name it is an alias of. The lookup goes through\n * `Object.hasOwn` for the same reason {@link namedScaleMask} does.\n *\n * @param name The scale name or alias.\n * @returns The canonical name, or undefined for an unknown one.\n * @example\n * ```ts\n * import { resolveScaleName } from '@libraz/libcantus';\n * resolveScaleName('okinawan'); // 'ryukyu'\n * resolveScaleName('dorian'); // 'dorian'\n * ```\n * @category Scales\n */\nexport function resolveScaleName(name: ScaleNameInput): ScaleName | WorldScaleName | undefined {\n // Undefined means \"no scale goes by this name\", which is a statement about a\n // name; a value that is no name at all is refused instead, so a caller cannot\n // read the two as the same answer.\n if (typeof name !== 'string') {\n throw new InvalidInputError(`scale name must be a string; received ${describeRejected(name)}`);\n }\n if (Object.hasOwn(NAMED_SCALES, name)) {\n return name as ScaleName;\n }\n if (Object.hasOwn(WORLD_SCALES, name)) {\n return name as WorldScaleName;\n }\n return Object.hasOwn(SCALE_ALIASES, name) ? SCALE_ALIASES[name as ScaleAliasName] : undefined;\n}\n\n/**\n * The mask of a named scale, or undefined when the name is not one.\n *\n * Names of {@link NAMED_SCALES}, of {@link WORLD_SCALES} and of\n * {@link SCALE_ALIASES} all answer here, so a caller never has to know which\n * register a scale is kept in.\n *\n * Looking the name up through this helper is what keeps `'constructor'` or\n * `'toString'` from resolving to something inherited from `Object.prototype`\n * and producing a scale whose mask is a function.\n *\n * @param name The scale name or alias.\n * @returns The 12-bit mask, or undefined for an unknown name.\n * @category Scales\n */\nexport function namedScaleMask(name: ScaleNameInput): number | undefined {\n const canonical = resolveScaleName(name);\n if (canonical === undefined) {\n return undefined;\n }\n return Object.hasOwn(NAMED_SCALES, canonical)\n ? NAMED_SCALES[canonical as ScaleName]\n : WORLD_SCALES[canonical as WorldScaleName];\n}\n\n/**\n * The mask of a named scale, rejecting an unknown name.\n *\n * Every public entry point that takes a scale name funnels through here, so a\n * typo is reported the same way everywhere instead of being answered with an\n * empty result that also means \"this scale has none\".\n *\n * @param name The scale name.\n * @param label What the name is, for the error message.\n * @returns The 12-bit mask.\n * @throws If the name is not a built-in scale.\n * @category Scales\n */\nexport function requireScaleMask(name: ScaleNameInput, label = 'scale'): number {\n const mask = namedScaleMask(name);\n if (mask === undefined) {\n throw new InvalidInputError(`Unknown ${label}: ${String(name)}`);\n }\n return mask;\n}\n\n/** The mask each named form stands for, and the only place that pairing is written. */\nexport const VARIANT_MASKS: Readonly<Record<Exclude<KeyVariant, 'modal'>, number>> = {\n major: MAJOR_MASK,\n natural: NATURAL_MINOR_MASK,\n harmonic: HARMONIC_MINOR_MASK,\n melodic: MELODIC_MINOR_MASK,\n};\n\n/**\n * Whether a mode mask is a minor one: it has a minor third and no major third.\n *\n * The third is what the question rests on, so every scale that lowers it — the\n * natural, harmonic and melodic minors, dorian, phrygian, locrian — answers\n * true, and a scale carrying both thirds answers false because the major one\n * is the one the ear takes. Written here, below the layers that ask it: the key\n * signature, the key relations, harmonic function and the generators all branch\n * on it, and an answer that differed between two of them would put a chord in a\n * key whose own signature disagreed about its mode.\n *\n * @param modeMask12 The twelve-bit mode mask to read.\n * @returns True when the mask leans minor.\n * @category Scales\n */\nexport function isMinorMask(modeMask12: number): boolean {\n return ((modeMask12 >> 3) & 1) === 1 && ((modeMask12 >> 4) & 1) === 0;\n}\n\n/**\n * The form a mask stands in, when it stands in one.\n *\n * A bare scale still names its form — a major mask is a major key whoever built\n * it — so the form is read from the mask rather than defaulted away. Only a\n * mask matching none of the four is modal.\n */\nexport function variantOfMask(modeMask12: number): KeyVariant {\n assertModeMask(modeMask12);\n for (const [variant, mask] of Object.entries(VARIANT_MASKS)) {\n if (mask === modeMask12) {\n return variant as KeyVariant;\n }\n }\n return 'modal';\n}\n\n/** Every value a key's variant may hold, in a fixed order. */\nexport const KEY_VARIANTS: readonly KeyVariant[] = [\n ...(Object.keys(VARIANT_MASKS) as Exclude<KeyVariant, 'modal'>[]),\n 'modal',\n];\n\n/**\n * Refuse a scale form the key's own mask does not hold.\n *\n * A key whose variant says `'harmonic'` over a major mask prints as a harmonic\n * minor while comparing equal to plain C major, so a project file that carried\n * the two apart is refused where the mismatch is still an argument. Kept beside\n * the type it validates: the pairing of form and mask is written once, and\n * every construction path reads that one.\n *\n * @param variant The form claimed for the key.\n * @param modeMask12 The mask the key actually holds.\n * @throws If the form is not one a key may stand in, or the mask does not hold it.\n * @category Scales\n */\nexport function assertKeyVariant(variant: KeyVariant, modeMask12: number): void {\n assertOneOf(variant, KEY_VARIANTS, 'variant');\n // Read as a mask before it is compared with one: `modal` is the form that\n // matches no named mask, so a value that is no mask at all would agree with it.\n assertModeMask(modeMask12);\n const named = VARIANT_MASKS[variant as Exclude<KeyVariant, 'modal'>];\n const matches =\n named === undefined ? !Object.values(VARIANT_MASKS).includes(modeMask12) : named === modeMask12;\n if (!matches) {\n throw new InvalidInputError(\n `variant ${variant} does not match the scale mask ${modeMask12}; ` +\n 'pass the scale that variant names, or omit the variant',\n );\n }\n}\n","import { pitchClassOf } from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport type { ScaleNameInput } from './masks.js';\nimport { MAJOR_MASK, NATURAL_MINOR_MASK, requireScaleMask } from './masks.js';\n\n/**\n * The scales themselves, with no spelling on them.\n *\n * Below the reading that decides how a key is written, because that reading is\n * built out of these: it walks the circle of fifths asking what each candidate\n * tonic would cost, and every candidate it weighs is a scale. A builder that\n * spelled itself would have to ask the reading, and the reading would ask the\n * builder back.\n *\n * So the public builders in `key.ts` are these plus that reading, and these stay\n * unexported: a caller reaching for a scale with no spelling on it is reaching\n * for the shape the spelling-sensitive entry points refuse, and the way to say\n * that deliberately is to spell it.\n */\n\n/** The major scale on a root pitch class. */\nexport function majorScale(rootPc: number): KeyScale {\n return { rootPc: pitchClassOf(rootPc), modeMask12: MAJOR_MASK };\n}\n\n/** The natural-minor scale on a root pitch class. */\nexport function minorScale(rootPc: number): KeyScale {\n return { rootPc: pitchClassOf(rootPc), modeMask12: NATURAL_MINOR_MASK };\n}\n\n/** A named scale on a root pitch class. */\nexport function namedScale(name: ScaleNameInput, rootPc: number): KeyScale {\n return { rootPc: pitchClassOf(rootPc), modeMask12: requireScaleMask(name) };\n}\n","import { type ParseResult, unwrapParse } from '../../core/errors/index.js';\nimport type { Note, NoteNameOptions } from '../../core/pitch/index.js';\nimport { noteToPitchClass, tryParseKeyName, tryParseNote } from '../../core/pitch/index.js';\nimport { assertOptions } from '../../core/validation/index.js';\nimport type { ResolvedKey } from './kinds.js';\nimport {\n NAMED_SCALES,\n SCALE_ALIASES,\n type ScaleAliasName,\n type ScaleName,\n variantOfMask,\n WORLD_SCALES,\n type WorldScaleName,\n} from './masks.js';\nimport { majorScale, minorScale, namedScale } from './scales.js';\n\n/**\n * Reading a key from the name it is written under.\n *\n * One reader, because a name carries the two facts that are lost when the same\n * text is read twice: which letter the tonic is spelled on, and which scale form\n * the word names. A second reader that answers only the first is how `'Ab\n * minor'` came back spelled `'G# minor'` — the name said Ab and the reading\n * discarded it, so the spelling had to be guessed again from the pitch classes,\n * and the guess reads the flat side of the circle as sharps.\n */\n\n/** A scale word with its spacing and case dropped, so two spellings compare. */\nfunction scaleWordKey(text: string): string {\n return text.replace(/[^a-z0-9]/gi, '').toLowerCase();\n}\n\n/** Every name a built-in scale answers to, canonical names before aliases. */\nconst BUILT_IN_SCALE_NAMES: readonly (ScaleName | WorldScaleName | ScaleAliasName)[] = [\n ...(Object.keys(NAMED_SCALES) as ScaleName[]),\n ...(Object.keys(WORLD_SCALES) as WorldScaleName[]),\n ...(Object.keys(SCALE_ALIASES) as ScaleAliasName[]),\n];\n\n/**\n * The built-in scale each scale word names, indexed the way names compare.\n *\n * Every register a scale can be built from is here, because a name\n * {@link scaleByName} accepts is a name a key can be written under, and a word\n * this map does not hold is a key that cannot be read back from its own text.\n */\nconst SCALE_BY_WORD: ReadonlyMap<string, ScaleName | WorldScaleName | ScaleAliasName> = new Map(\n BUILT_IN_SCALE_NAMES.map((name) => [scaleWordKey(name), name]),\n);\n\n/**\n * The scale word a mask is written with, or undefined when no built-in scale\n * has that mask.\n *\n * The inverse of {@link SCALE_BY_WORD}, and the reason a key on a scale outside\n * the Western vocabulary prints as itself: the miyako-bushi scale names the five\n * pitch classes it actually holds rather than the major key its tonic sits in.\n *\n * The first name declared with the mask answers, so a mask several traditions\n * name reads under the Western name where there is one — the min'yō scale prints\n * as the minor pentatonic it shares its pitch classes with — and the register\n * order is what settles it rather than a table of exceptions.\n *\n * @param mask The 12-bit mode mask.\n * @returns The scale name, or undefined for a mask no built-in scale has.\n */\nexport function scaleNameOfMask(mask: number): ScaleName | WorldScaleName | undefined {\n for (const name of Object.keys(NAMED_SCALES) as ScaleName[]) {\n if (NAMED_SCALES[name] === mask) {\n return name;\n }\n }\n for (const name of Object.keys(WORLD_SCALES) as WorldScaleName[]) {\n if (WORLD_SCALES[name] === mask) {\n return name;\n }\n }\n return undefined;\n}\n\n/** The tonic without its register: a key has none, whatever the name carried. */\nfunction bareTonic(note: Note): Note {\n return { letter: note.letter, alter: note.alter };\n}\n\n/**\n * Read a key named by a tonic and a scale word — `'D harmonic minor'`,\n * `'D dorian'` — or answer null when the text names no built-in scale.\n *\n * This is the inverse of what a key writes itself as, so a key that is neither a\n * plain major nor a plain minor survives being written down and read back. A\n * plain mode word is left to the key-name parser, which reads it in every\n * notation system rather than in English alone.\n */\nfunction tryNamedScaleKey(text: string): ResolvedKey | null {\n const trimmed = text.trim();\n const separator = trimmed.search(/\\s/);\n if (separator < 0) {\n return null;\n }\n const name = SCALE_BY_WORD.get(scaleWordKey(trimmed.slice(separator)));\n if (name === undefined) {\n return null;\n }\n const tonic = tryParseNote(trimmed.slice(0, separator));\n if (!tonic.ok) {\n return null;\n }\n const scale = namedScale(name, noteToPitchClass(tonic.value));\n return { scale, tonic: bareTonic(tonic.value), variant: variantOfMask(scale.modeMask12) };\n}\n\n/**\n * Read a key name, reporting failure instead of throwing it.\n *\n * The scale-word form is tried first, because a name ending in a mode word —\n * `'D harmonic minor'` — would otherwise be read as the plain minor key that\n * word names and lose the scale it was written with. It is English-only, so a\n * name asked for in another notation system goes straight to the key-name\n * parser, which reads that system.\n *\n * @param text The key name.\n * @param opts `system` reads the name in that notation system instead of\n * detecting it.\n * @returns The key the name denotes, or the error explaining why the text is\n * not one.\n * @example\n * ```ts\n * import { tryResolveKeyName } from '@libraz/libcantus';\n * const result = tryResolveKeyName('D dorian');\n * result.ok ? result.value.variant : result.error.message; // 'modal'\n * ```\n * @category Scales\n */\nexport function tryResolveKeyName(text: string, opts?: NoteNameOptions): ParseResult<ResolvedKey> {\n const asked = assertOptions(opts, 'opts');\n if (typeof text === 'string' && (asked.system === undefined || asked.system === 'english')) {\n const named = tryNamedScaleKey(text);\n if (named !== null) {\n return { ok: true, value: named };\n }\n }\n const parsed = tryParseKeyName(text, asked);\n if (!parsed.ok) {\n return parsed;\n }\n const tonic = bareTonic(parsed.value.tonic);\n const rootPc = noteToPitchClass(tonic);\n return parsed.value.mode === 'major'\n ? { ok: true, value: { scale: majorScale(rootPc), tonic, variant: 'major' } }\n : { ok: true, value: { scale: minorScale(rootPc), tonic, variant: 'natural' } };\n}\n\n/**\n * Read a key name to the whole key it denotes.\n *\n * The tonic is the one the name spells, not one derived back from the pitch\n * classes: `'Ab minor'` is an A flat minor here and stays one wherever the\n * result travels.\n *\n * Not published: for a caller `resolveKey` is this function, since it reads a\n * name by calling it, and a second public name for one reading is the split\n * this module exists to prevent. A notation system other than English is asked\n * for through `Key.parse`, which carries the option.\n *\n * @param text The key name.\n * @param opts `system` reads the name in that notation system instead of\n * detecting it.\n * @returns The key, whole.\n * @throws If the text names no key.\n */\nexport function resolveKeyName(text: string, opts?: NoteNameOptions): ResolvedKey {\n return unwrapParse(tryResolveKeyName(text, opts));\n}\n","import { InvalidInputError } from '../../core/errors/index.js';\nimport type { Note } from '../../core/pitch/index.js';\nimport { pitchClassOf } from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport { assertInteger, describeRejected } from '../../core/validation/index.js';\nimport type { KeyVariant } from './kinds.js';\nimport { assertModeMask } from './masks.js';\nimport { resolveKeyName } from './name.js';\n\nexport type { KeyVariant };\n\n/**\n * Anything that names a key: a key name, a plain {@link KeyScale}, the plain\n * data a key serializes to, or a value that serializes to one such as the `Key`\n * class.\n *\n * The plain-data form is here because a project file holds a key that way, and\n * reading one back has to be the same call as passing the class it came from.\n *\n * Deliberately wider than what a resolver hands back: a caller may hand in a\n * bare scale, or data written before the spelling travelled with it, and the\n * resolver fills in what is missing rather than refusing it. Narrowing this to\n * the resolved shape would make the acceptance of every entry point shrink the\n * day the resolved shape gained a field.\n *\n * A scale is accepted with the spelling and the form written beside it as well\n * as without them, because a key region carries its key that way — asked\n * positional questions wherever a plain scale would be. The resolver reads them\n * when they are there, and this says so: a shape a resolver reads and a type\n * denies is one refactoring away from being dropped silently.\n *\n * @category Scales\n */\nexport type KeyLike =\n | string\n | (KeyScale & {\n /** The spelled tonic the key is written with, when it travels beside it. */\n tonic?: Note;\n /** The scale form the key stands in, when it travels beside it. */\n variant?: KeyVariant;\n })\n | {\n /** The scale this key data denotes. */\n scale: KeyScale;\n /** The spelled tonic the key is written with, when the caller named one. */\n tonic?: Note;\n /** The scale form the key was read under, when it was read under one. */\n variant?: KeyVariant;\n }\n | {\n /** The key data this value stands for, carrying everything it knows. */\n toJSON(): { scale: KeyScale; tonic?: Note; variant?: KeyVariant };\n };\n\n/**\n * A key that says how it is written, for the entry points whose answer depends\n * on it.\n *\n * The shapes {@link KeyLike} accepts, minus the one that cannot carry a\n * spelling: a bare {@link KeyScale}. An A flat minor and a G sharp minor are one\n * set of pitch classes and two keys, so an entry point whose answer differs\n * between them cannot be handed the shape that does not distinguish them and\n * still be right. Declaring this makes that a compile error rather than a\n * defect found later — and `Key`, `ResolvedKey`, a key name and stored key data\n * all satisfy it, so the narrowing costs a caller nothing it was doing\n * correctly.\n *\n * Which entry points declare this is measured rather than chosen: an entry point\n * belongs here exactly when varying its key argument alone — between the two\n * spellings of one sound — changes its answer. A scale a caller genuinely wants\n * to hand in is spelled first, at the place it happens, which is the same rule\n * the reduction in the other direction already follows.\n *\n * @category Scales\n */\nexport type SpelledKeyLike =\n | string\n | (KeyScale & {\n /** The spelled tonic the key is written with. Required: it is the point. */\n tonic: Note;\n /** The scale form the key stands in, when it travels beside it. */\n variant?: KeyVariant;\n })\n | {\n /** The scale this key data denotes. */\n scale: KeyScale;\n /** The spelled tonic the key is written with, when the caller named one. */\n tonic?: Note;\n /** The scale form the key was read under, when it was read under one. */\n variant?: KeyVariant;\n }\n | {\n /** The key data this value stands for, carrying everything it knows. */\n toJSON(): { scale: KeyScale; tonic?: Note; variant?: KeyVariant };\n };\n\n/** Validate a plain key/scale and return it in the canonical shape. */\nfunction normalizedKeyScale(scale: KeyScale): KeyScale {\n assertModeMask(scale.modeMask12, 'key.modeMask12');\n if ((scale.modeMask12 & 1) === 0) {\n // Every scale contains its own root, and the rest of the library reads\n // bit 0 as given. A mask without it names no key and would degrade into\n // wrong answers far from here rather than failing at the boundary.\n throw new InvalidInputError(\n `key.modeMask12 must include its root; received ${scale.modeMask12}`,\n );\n }\n assertInteger(scale.rootPc, 'key.rootPc');\n return { rootPc: pitchClassOf(scale.rootPc), modeMask12: scale.modeMask12 };\n}\n\n/**\n * Resolve any key-shaped value to a plain {@link KeyScale}.\n *\n * The counterpart of {@link toSpelledInterval} for keys: an entry point takes\n * whatever form the caller has — the name a key field holds, the scale the\n * theory module returns, or a `Key` instance — and gets one shape back. An\n * instance is accepted through its `toJSON` method rather than by its type, so\n * the layers below the model can read a class without importing it.\n *\n * A name is read by the one reader {@link resolveKey} and `Key.parse` also use,\n * so every entry point that takes a key accepts exactly the same names: a mode\n * word in every notation system that reader knows, and a built-in scale's own\n * word — `'D dorian'`, `'D harmonic minor'` — in English.\n * The spelling the name carries is dropped here along with the scale form,\n * which is what {@link resolveKey} is for.\n *\n * @param value A key name, a plain key/scale, or a value whose `toJSON`\n * returns data carrying one.\n * @returns The validated key/scale, with its root reduced to a pitch class.\n * @throws If the value names no key, or its mode mask is out of range or\n * excludes its own root.\n * @example\n * ```ts\n * import { toKeyScale } from '@libraz/libcantus';\n * toKeyScale('C major').rootPc; // 0\n * toKeyScale('A minor').rootPc; // 9\n * ```\n * @category Scales\n */\nexport function toKeyScale(value: KeyLike): KeyScale {\n if (typeof value === 'string') {\n return resolveKeyName(value).scale;\n }\n if (typeof value === 'object' && value !== null) {\n // A key reaches here in three shapes: the class, the plain data it\n // serializes to, and the bare scale. The first two carry the spelled tonic\n // that layers above this one read; here only the scale is wanted.\n const data =\n 'toJSON' in value && typeof value.toJSON === 'function'\n ? value.toJSON().scale\n : 'scale' in value\n ? value.scale\n : value;\n if (typeof data === 'object' && data !== null) {\n return normalizedKeyScale(data as KeyScale);\n }\n }\n throw new InvalidInputError(\n `key must be a key name or a key/scale; received ${describeRejected(value)}`,\n );\n}\n","import { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport { assertFiniteNumber, clampToMidi } from '../../core/validation/index.js';\nimport { type KeyLike, toKeyScale } from './coerce.js';\n\n/** Scale offset of a pitch relative to the key root, in [0, 11]. */\nfunction scaleOffset(pitch: number, key: KeyScale): number {\n return (pitchClass(pitch) - pitchClass(key.rootPc) + 12) % 12;\n}\n\n/** Whether a pitch belongs to an already-resolved scale. */\nfunction inScale(pitch: number, scale: KeyScale): boolean {\n return ((scale.modeMask12 >> scaleOffset(pitch, scale)) & 1) === 1;\n}\n\n/**\n * Test whether a pitch belongs to the scale.\n *\n * @param pitch MIDI pitch or bare pitch class.\n * @param key The key/scale to test against, as a key name, a key/scale, or a\n * `Key`.\n * @returns True if the pitch class is a member of the scale.\n *\n * @example\n * ```ts\n * import { majorKey, isScaleTone } from '@libraz/libcantus';\n * const key = majorKey(0); // C major\n * isScaleTone(64, key); // true — E is in C major\n * isScaleTone(61, key); // false — C# is not\n * ```\n *\n * @category Scales\n */\nexport function isScaleTone(pitch: number, key: KeyLike): boolean {\n return inScale(pitch, toKeyScale(key));\n}\n\n/**\n * Find the nearest MIDI pitch whose pitch class is in the scale.\n *\n * The search expands symmetrically outward from `pitch`. On a tie (equal\n * distance above and below) the lower pitch is returned.\n *\n * @param pitch MIDI pitch to snap.\n * @param key The key/scale to snap to, as a key name, a key/scale, or a `Key`.\n * @returns The nearest in-scale MIDI pitch.\n *\n * @category Scales\n */\nexport function nearestScaleTone(pitch: number, key: KeyLike): number {\n const scale = toKeyScale(key);\n // Before the rounding: `Math.round` reads a `null` as zero, which the clamp\n // then accepts as the lowest MIDI pitch.\n assertFiniteNumber(pitch, 'pitch');\n const base = clampToMidi(Math.round(pitch), 'pitch');\n for (let distance = 0; distance < 12; distance += 1) {\n const lower = base - distance;\n if (lower >= 0 && inScale(lower, scale)) {\n return lower;\n }\n const higher = base + distance;\n if (higher <= 127 && inScale(higher, scale)) {\n return higher;\n }\n }\n return base;\n}\n\n/** The key's scale offsets above its root, ascending within one octave. */\nfunction scaleOffsets(key: KeyScale): number[] {\n const offsets: number[] = [];\n for (let n = 0; n < 12; n += 1) {\n if (((key.modeMask12 >> n) & 1) === 1) {\n offsets.push(n);\n }\n }\n return offsets;\n}\n\n/**\n * Where a pitch sits on the key's ladder of scale tones.\n *\n * `rung` counts scale tones from the tonic and continues across octaves, so\n * adding to it moves by scale degrees; it is negative below the tonic of octave\n * 0. `offset` is how many semitones the pitch sounds above that rung, which is 0\n * for a scale tone and positive for one between two of them.\n */\nexport type ScaleLadderPosition = {\n rung: number;\n offset: number;\n};\n\n/**\n * Locate a pitch on the key's ladder of scale tones.\n *\n * Anything counting in scale degrees has to put a pitch on this ladder and back\n * again afterwards. Keeping the chromatic offset apart from the rung is what\n * lets a note between two scale tones survive the arithmetic as the note it was:\n * without it a shift by any number of degrees — including none — would first\n * flatten every chromatic pitch onto the scale.\n *\n * In C major, C# sits on the tonic's rung with an offset of one semitone.\n *\n * Layer-internal: the generators count degrees with it, and callers reach the\n * same arithmetic through the transforms that use it.\n *\n * @param pitch MIDI pitch.\n * @param key The key/scale to count along, as a key name, a key/scale, or a\n * `Key`.\n * @returns The rung the pitch sits on and its semitone offset above it.\n */\nexport function scaleLadderPosition(pitch: number, key: KeyLike): ScaleLadderPosition {\n return ladderPosition(pitch, toKeyScale(key));\n}\n\n/** {@link scaleLadderPosition} over an already-resolved scale. */\nfunction ladderPosition(pitch: number, key: KeyScale): ScaleLadderPosition {\n // Both directions of the ladder do arithmetic on the number they are given\n // and both are reached from a public entrance, so each checks its own: a\n // `null` pitch would place itself on a rung of a real key.\n assertFiniteNumber(pitch, 'pitch');\n const offsets = scaleOffsets(key);\n const size = offsets.length;\n if (size === 0) {\n return { rung: 0, offset: 0 };\n }\n const relative = pitch - pitchClass(key.rootPc);\n const octave = Math.floor(relative / 12);\n const within = relative - octave * 12;\n let degree = 0;\n for (let n = 0; n < size; n += 1) {\n if ((offsets[n] ?? 0) <= within) {\n degree = n;\n }\n }\n return { rung: octave * size + degree, offset: within - (offsets[degree] ?? 0) };\n}\n\n/**\n * The pitch a rung of the key's ladder of scale tones stands for.\n *\n * The inverse of {@link scaleLadderPosition}'s `rung`: adding the position's\n * `offset` back to the result returns the pitch it came from.\n *\n * Layer-internal, like {@link scaleLadderPosition}.\n *\n * @param rung Scale tones counted from the tonic, negative below it.\n * @param key The key/scale to count along, as a key name, a key/scale, or a\n * `Key`.\n * @returns The MIDI pitch of that scale tone.\n */\nexport function scaleLadderPitch(rung: number, key: KeyLike): number {\n return ladderPitch(rung, toKeyScale(key));\n}\n\n/** {@link scaleLadderPitch} over an already-resolved scale. */\nfunction ladderPitch(rung: number, key: KeyScale): number {\n assertFiniteNumber(rung, 'rung');\n const offsets = scaleOffsets(key);\n const size = offsets.length;\n if (size === 0) {\n return pitchClass(key.rootPc);\n }\n const octave = Math.floor(rung / size);\n const degree = rung - octave * size;\n return pitchClass(key.rootPc) + octave * 12 + (offsets[degree] ?? 0);\n}\n\n/**\n * Shift a pitch by scale degrees along the key's ladder of scale tones.\n *\n * A pitch between two scale tones keeps its distance above the one below it, so\n * a chromatic passing note is still a chromatic passing note after the shift,\n * and a shift of no degrees returns the pitch untouched: a step up the C-major\n * ladder answers C# with D#, not with D.\n *\n * Layer-internal. It is what `transposeDiatonic` and a tonal imitation both\n * count along, so the two answer the same shift the same way; a caller reaches\n * it through either of them.\n *\n * @param pitch MIDI pitch to shift.\n * @param degrees Scale degrees to move by; negative moves down.\n * @param key The key/scale to count along, as a key name, a key/scale, or a\n * `Key`.\n * @returns The shifted pitch.\n */\nexport function shiftByScaleDegrees(pitch: number, degrees: number, key: KeyLike): number {\n const scale = toKeyScale(key);\n const steps = Math.trunc(assertFiniteNumber(degrees, 'degrees'));\n if (steps === 0) {\n return pitch;\n }\n const { rung, offset } = ladderPosition(pitch, scale);\n return ladderPitch(rung + steps, scale) + offset;\n}\n\n/**\n * Get the scale degree of a pitch, counted from 1: the tonic is degree 1.\n *\n * A pitch outside the scale answers -1 rather than 0, so a caller testing the\n * result for a degree cannot mistake the \"not in the scale\" answer for the\n * tonic.\n *\n * @param pitch MIDI pitch or bare pitch class.\n * @param key The key/scale to measure against, as a key name, a key/scale, or a\n * `Key`.\n * @returns The 1-based degree, or -1 if the pitch is not in the scale.\n *\n * @category Scales\n */\nexport function pitchToScaleDegree(pitch: number, key: KeyLike): number {\n const scale = toKeyScale(key);\n const offset = scaleOffset(pitch, scale);\n if (((scale.modeMask12 >> offset) & 1) === 0) {\n return -1;\n }\n let degree = 1;\n for (let n = 0; n < offset; n += 1) {\n if (((scale.modeMask12 >> n) & 1) === 1) {\n degree += 1;\n }\n }\n return degree;\n}\n\n/**\n * List the pitch classes of a key's scale.\n *\n * @param key The key/scale to enumerate, as a key name, a key/scale, or a `Key`.\n * @returns The member pitch classes, sorted ascending in [0, 11].\n *\n * @category Scales\n */\nexport function diatonicPitchClasses(key: KeyLike): number[] {\n // The same walk of the mask, sorted: two copies of it would read alike today\n // and drift the moment one of them was corrected, which is a difference\n // nothing would report. The array comes back fresh, so sorting it in place\n // touches nothing the caller holds.\n return scaleTonesInDegreeOrder(key).sort((a, b) => a - b);\n}\n\n/**\n * List the pitch classes of a key's scale in ascending scale-degree order.\n *\n * The first entry is the root (degree 1); degrees follow the mask bits in\n * offset order rather than sorted pitch-class order.\n *\n * @param key The key/scale to enumerate, as a key name, a key/scale, or a `Key`.\n * @returns The member pitch classes ordered by scale degree.\n *\n * @category Scales\n */\nexport function scaleTonesInDegreeOrder(key: KeyLike): number[] {\n const scale = toKeyScale(key);\n const root = pitchClass(scale.rootPc);\n const pcs: number[] = [];\n for (let n = 0; n < 12; n += 1) {\n if (((scale.modeMask12 >> n) & 1) === 1) {\n pcs.push((root + n) % 12);\n }\n }\n return pcs;\n}\n","import type { NoteLike } from '../../core/pitch/index.js';\nimport {\n diatonicLetterOf,\n pitchClassOf as mod12,\n naturalPitchClassOf,\n toNoteData,\n} from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport { assertInteger, assertOneOf } from '../../core/validation/index.js';\nimport { type KeyLike, toKeyScale } from './coerce.js';\nimport type { ResolvedKey } from './identity.js';\nimport {\n HARMONIC_MINOR_MASK,\n isMinorMask,\n MAJOR_MASK,\n MELODIC_MINOR_MASK,\n maskOf,\n variantOfMask,\n} from './masks.js';\nimport { majorScale, minorScale } from './scales.js';\n\n/**\n * Which of the two modes a key signature is read in.\n *\n * A signature names two keys — three sharps is both A major and F# minor — so\n * every entry point that turns a signature into a key, or that names the mode\n * of a key relation, selects between them with this.\n *\n * @example\n * ```ts\n * import type { KeyMode } from '@libraz/libcantus';\n * const mode: KeyMode = 'minor';\n * ```\n * @category Scales\n */\nexport type KeyMode = 'major' | 'minor';\n\n/** Position of each natural letter on the circle of fifths: F=-1, C=0, ... B=5. */\nexport const LETTER_FIFTHS = [0, 2, 4, -1, 1, 3, 5] as const;\n\n/** The letters of the circle of fifths from F upward, as letter numbers. */\nexport const FIFTHS_LETTERS = [3, 0, 4, 1, 5, 2, 6] as const;\n\n/** How many fifths a sharp or a flat on the tonic moves its signature. */\nexport const FIFTHS_PER_ALTERATION = 7;\n\n/** Signature offset of the natural-minor mode: three fifths below its parallel major. */\nexport const MINOR_MODE_OFFSET = -3;\n\n/** Widest signature this module builds a key for, including theoretical ones. */\nexport const MAX_FIFTHS = 12;\n\n/** Rotate a 12-bit mode mask down by `semitones`, wrapping at the octave. */\nfunction rotateMask(mask: number, semitones: number): number {\n const steps = ((semitones % 12) + 12) % 12;\n return ((mask >> steps) | (mask << (12 - steps))) & 0b111111111111;\n}\n\n/**\n * Signature offset of each diatonic mode, keyed by its mode mask.\n *\n * The offset is how many fifths the mode's signature sits from the major key on\n * the same tonic, which is exactly the mode's own position in the circle: D\n * dorian is two fifths flatter than D major, and so carries no accidentals.\n */\nexport const DIATONIC_MODE_OFFSET_ENTRIES: readonly (readonly [number, number])[] = [\n [rotateMask(MAJOR_MASK, 5), 1], // lydian\n [MAJOR_MASK, 0], // ionian / major\n [rotateMask(MAJOR_MASK, 7), -1], // mixolydian\n [rotateMask(MAJOR_MASK, 2), -2], // dorian\n [rotateMask(MAJOR_MASK, 9), MINOR_MODE_OFFSET], // aeolian / natural minor\n [rotateMask(MAJOR_MASK, 4), -4], // phrygian\n [rotateMask(MAJOR_MASK, 11), -5], // locrian\n];\n\n/** {@link DIATONIC_MODE_OFFSET_ENTRIES} by mask. */\nconst DIATONIC_MODE_OFFSETS = new Map<number, number>(DIATONIC_MODE_OFFSET_ENTRIES);\n\n/**\n * How many fifths a key's signature sits from the major key on the same tonic.\n *\n * A diatonic mode has its own answer. Anything else — harmonic and melodic\n * minor, and every scale that is not heptatonic — takes the signature of its\n * parallel major or minor instead, since its remaining tones are written as\n * accidentals rather than in the signature.\n */\nfunction modeOffset(key: KeyScale): number {\n const diatonic = DIATONIC_MODE_OFFSETS.get(key.modeMask12);\n if (diatonic !== undefined) {\n return diatonic;\n }\n return isMinorMask(key.modeMask12) ? MINOR_MODE_OFFSET : 0;\n}\n\n/**\n * Whether a key is one that is actually written with a key signature.\n *\n * Each of the seven diatonic modes has a signature of its own, and the harmonic\n * and melodic minor are the minor key with a degree or two raised: G# harmonic\n * minor is written with G# minor's five sharps and an F## in front of the note,\n * which is why it is spelled G# and not Ab. Every other scale only borrows the\n * signature of its parallel major or minor as an approximation, so nothing\n * about how it is written follows from that signature — and how such a scale is\n * spelled has to be decided from the scale itself.\n *\n * @param key The key/scale.\n * @returns True when the signature is the key's own rather than a stand-in.\n */\nexport function isSignatureKey(key: KeyScale): boolean {\n const mask = maskOf(key);\n return (\n DIATONIC_MODE_OFFSETS.has(mask) || mask === HARMONIC_MINOR_MASK || mask === MELODIC_MINOR_MASK\n );\n}\n\n/**\n * The key signature of a key, as a signed count of sharps (positive) or flats\n * (negative) — the `fifths` value a notation format such as MusicXML writes.\n *\n * The tonic's letter and accidental place the key on the circle of fifths, and\n * the mode moves it from there: lydian +1, major 0, mixolydian -1, dorian -2,\n * natural minor -3, phrygian -4, locrian -5.\n *\n * A scale that is not a diatonic mode has no signature of its own, so it takes\n * the signature of its parallel major or minor: a scale with a minor third and\n * no major third counts as minor (-3), everything else as major (0). This is\n * why A harmonic minor reports 0 — its G# is written as an accidental, not in\n * the signature — and why A melodic minor reports 0 as well.\n *\n * The result is not clamped: a theoretical key such as Fb major reports -8.\n * Clamp to ±7 if the consuming format cannot express more.\n *\n * @param tonic The spelled tonic, as a note name, note data, or a `Note`; its\n * accidental is worth seven fifths.\n * @param key The key/scale. This one stays a plain key/scale: it reads any\n * 12-bit mask, including the tonic-less ones a key name cannot describe.\n * @returns The signed number of sharps (positive) or flats (negative).\n * @example\n * ```ts\n * import { keySignatureFifths, majorKey, minorKey, parseNote } from '@libraz/libcantus';\n * keySignatureFifths(parseNote('Bb'), majorKey(10)); // -2\n * keySignatureFifths('A', minorKey(9)); // 0\n * ```\n * @category Scales\n */\nexport function keySignatureFifths(tonic: NoteLike, key: KeyLike): number {\n const note = toNoteData(tonic);\n const scale = toKeyScale(key);\n const letter = diatonicLetterOf(note.letter);\n assertInteger(note.alter, 'tonic.alter');\n assertInteger(scale.modeMask12, 'key.modeMask12', 1, 0b111111111111);\n return (LETTER_FIFTHS[letter] ?? 0) + FIFTHS_PER_ALTERATION * note.alter + modeOffset(scale);\n}\n\n/**\n * The key a signature denotes: the inverse of {@link keySignatureFifths} for\n * the two modes a signature is conventionally read in.\n *\n * A signature names two keys — three sharps is both A major and F# minor — so\n * the mode decides which one is built. The tonic is spelled the way that key is\n * written, including the theoretical keys past ±7 (nine sharps is D# major).\n *\n * @param fifths The signed number of sharps (positive) or flats (negative).\n * @param mode Which of the signature's two keys to build; defaults to major.\n * @returns The spelled tonic and the matching key/scale.\n * @throws If `fifths` is not an integer in [-12, 12], or `mode` is not one of\n * `'major'` or `'minor'`.\n * @example\n * ```ts\n * import { keyFromFifths, formatNote } from '@libraz/libcantus';\n * formatNote(keyFromFifths(-2).tonic); // 'Bb'\n * formatNote(keyFromFifths(-2, 'minor').tonic); // 'G'\n * ```\n * @category Scales\n */\nexport function keyFromFifths(fifths: number, mode: KeyMode = 'major'): ResolvedKey {\n assertInteger(fifths, 'fifths', -MAX_FIFTHS, MAX_FIFTHS);\n const which = assertOneOf(mode, ['major', 'minor'], 'mode');\n // The circle of fifths starts at F, so the major tonic of `fifths` sharps is\n // one step along it; a minor key is the relative minor, three fifths further.\n const index = fifths + 1 + (which === 'minor' ? -MINOR_MODE_OFFSET : 0);\n const letter = FIFTHS_LETTERS[((index % 7) + 7) % 7] ?? 0;\n const alter = Math.floor(index / 7);\n const rootPc = mod12(naturalPitchClassOf(letter) + alter);\n const scale = which === 'minor' ? minorScale(rootPc) : majorScale(rootPc);\n return {\n tonic: { letter, alter },\n scale,\n variant: variantOfMask(scale.modeMask12),\n };\n}\n","/**\n * How keys stand to one another: the relative, parallel, dominant and\n * subdominant keys, the enharmonic respelling of a key, and the relation that\n * holds between two given keys.\n *\n * Every relation but {@link parallelKeyOf} is computed in fifths space, on top\n * of {@link keySignatureFifths} and {@link keyFromFifths}. Stepping around the\n * circle of fifths and reading the tonic back off it is what keeps the spelling\n * right: the relative of Db major comes out as Bb minor, not A# minor.\n *\n * A relation always returns a plain major or minor key, because those are the\n * only keys these relations name; {@link enharmonicKeyOf} is the exception that\n * proves it, since a respelling is the same key rather than another one and so\n * keeps the mask it was handed. A key that is not a plain major or minor —\n * a church mode, harmonic minor, a pentatonic, an octatonic — is read through\n * the mode its third names, and the fifths are counted from where its tonic\n * stands on the circle rather than from the key's own signature: a mode's\n * signature carries its own offset (D dorian is two fifths flatter than D\n * major), which {@link keyFromFifths} does not know how to give back.\n * {@link parallelKeyOf} travels no fifths at all, so it keeps the tonic\n * spelling it was given.\n */\n\nimport type { Note, NoteLike } from '../../core/pitch/index.js';\nimport {\n diatonicLetterOf,\n noteToPitchClass,\n pitchClassOf,\n toNoteData,\n} from '../../core/pitch/index.js';\nimport { MAX_NAME_ACCIDENTALS } from '../../core/pitch/naming.js';\nimport type { KeyScale } from '../../core/types.js';\nimport { assertRecord } from '../../core/validation/index.js';\nimport { spellScale } from '../spelling/index.js';\nimport { type KeyLike, toKeyScale } from './coerce.js';\nimport type { ResolvedKey } from './identity.js';\nimport { isMinorMask, maskOf, variantOfMask } from './masks.js';\nimport { majorScale, minorScale } from './scales.js';\nimport type { KeyMode } from './signature.js';\nimport { isSignatureKey, keyFromFifths, keySignatureFifths, MAX_FIFTHS } from './signature.js';\n\n/** How many fifths apart the two spellings of one sounding key sit. */\nexport const ENHARMONIC_FIFTHS = 12;\n\n/** Widest signature a key is actually written with. */\nexport const MAX_CONVENTIONAL_FIFTHS = 7;\n\n/** How many fifths a minor key's tonic stands above its signature: A minor is +3. */\nexport const MINOR_TONIC_FIFTHS = 3;\n\n/**\n * The flattest and sharpest points on the circle a tonic is ever written at.\n *\n * A key is written with at most seven accidentals, which puts its tonic between\n * Cb — the tonic of the seven-flat major key — and A#, the tonic of the\n * seven-sharp minor one. Every pitch class is named once or twice inside that\n * span, and those names are the only ones a key is spelled on: F# and Gb are\n * both there, E# and Fb are not.\n */\nexport const FLATTEST_TONIC_FIFTHS = -MAX_CONVENTIONAL_FIFTHS;\nexport const SHARPEST_TONIC_FIFTHS = MAX_CONVENTIONAL_FIFTHS + MINOR_TONIC_FIFTHS;\n\n/**\n * Which of the two modes a key leans to.\n *\n * This is the same test the key-signature module applies, so a relation and a\n * signature never disagree about a key's mode.\n */\nfunction modeOf(key: KeyScale): KeyMode {\n return isMinorMask(maskOf(key)) ? 'minor' : 'major';\n}\n\n/** The other of the two modes a signature is read in. */\nfunction oppositeMode(mode: KeyMode): KeyMode {\n return mode === 'major' ? 'minor' : 'major';\n}\n\n/**\n * Whether two keys name the same key: same root pitch class, same mode.\n *\n * Both keys are read the way every relation here reads them — through the mode\n * their third names — rather than by comparing masks. A relation answers with a\n * plain major or minor key, so a key that arrives as A harmonic minor has to be\n * met on the same terms as the A minor it is a form of; matching masks would\n * make the relation hold in one direction and not in the other.\n */\nfunction soundsLike(a: KeyScale, b: KeyScale): boolean {\n return (\n pitchClassOf(a.rootPc) === pitchClassOf(b.rootPc) &&\n isMinorMask(a.modeMask12) === isMinorMask(b.modeMask12)\n );\n}\n\n/** Whether two tonics are written alike, octave aside. */\nfunction spelledLike(a: Note, b: Note): boolean {\n return diatonicLetterOf(a.letter) === diatonicLetterOf(b.letter) && a.alter === b.alter;\n}\n\n/**\n * A key paired with the spelled tonic that anchors its letter names.\n *\n * The tonic carries no octave: a key is rooted on a pitch class, and its\n * spelling is what decides whether the key is written Db major or C# major.\n *\n * @example\n * ```ts\n * import type { ResolvedKey } from '@libraz/libcantus';\n * import { formatNote, majorKey, spelledKeyOf } from '@libraz/libcantus';\n * const dbMajor: ResolvedKey = spelledKeyOf(majorKey(1));\n * formatNote(dbMajor.tonic); // 'Db'\n * ```\n * @category Scales\n */\n\n/**\n * How one key stands to another, as reported by {@link keyRelationBetween} and\n * tagged onto every entry of {@link relatedKeysOf}.\n *\n * @example\n * ```ts\n * import type { KeyRelation } from '@libraz/libcantus';\n * const relation: KeyRelation = 'relative'; // C major -> A minor\n * ```\n * @category Scales\n */\nexport type KeyRelation =\n | 'same'\n | 'enharmonic'\n | 'relative'\n | 'parallel'\n | 'dominant'\n | 'subdominant'\n | 'relativeOfDominant'\n | 'relativeOfSubdominant';\n\n/**\n * What a candidate tonic costs the scale it is asked to spell: the double\n * accidentals it forces, the accidentals it needs altogether, and how far round\n * the circle of fifths the key it names is written.\n */\ntype TonicCost = { doubles: number; accidentals: number; fifths: number };\n\n/** How the scale reads when spelled from `tonic`. */\nfunction tonicCost(tonic: Note, key: KeyScale): TonicCost {\n let doubles = 0;\n let accidentals = 0;\n for (const note of spellScale(tonic, key)) {\n const alter = Math.abs(note.alter);\n accidentals += alter;\n if (alter >= MAX_NAME_ACCIDENTALS) {\n doubles += 1;\n }\n }\n return { doubles, accidentals, fifths: Math.abs(keySignatureFifths(tonic, key)) };\n}\n\n/**\n * Whether the first tonic spells the key better than the second; ties keep the\n * first, which is the flatter one because the scan runs from the flat end.\n *\n * A key that is written with a signature is spelled on the tonic that signature\n * is written on — pitch class 8 in minor is G# with five sharps, not Ab with\n * seven, and the F## its harmonic form writes is an accidental over that\n * signature rather than a reason to respell the key. A scale that only borrows\n * a signature has nothing to go on but how it reads, so there the double\n * accidentals it forces come first and the borrowed signature only breaks ties.\n */\nfunction spellsBetter(a: TonicCost, b: TonicCost, signatureKey: boolean): boolean {\n const order: (keyof TonicCost)[] = signatureKey\n ? ['fifths', 'doubles', 'accidentals']\n : ['doubles', 'accidentals', 'fifths'];\n for (const term of order) {\n if (a[term] !== b[term]) {\n return a[term] < b[term];\n }\n }\n return false;\n}\n\n/**\n * Spell a key's tonic the conventional way: the letter and accidental the key\n * is written with, paired with the key exactly as given.\n *\n * The candidates are the tonics keys are actually written on — everything from\n * Cb to A#, which names each pitch class once or twice (C, but Db and C#) — and\n * which of them names the key depends on whether the key has a signature of its\n * own. A major or minor key, a church mode, and the harmonic and melodic minor\n * are spelled on the tonic their signature is written on: pitch class 1 in\n * major is Db, five flats, rather than C#, seven sharps; pitch class 8 in minor\n * is G#, five sharps, rather than Ab, seven flats; and G# harmonic minor writes\n * its raised seventh F## as an accidental over that signature rather than\n * respelling the key. Where two signatures are equally long — pitch class 6 in\n * major is F# at +6 and Gb at -6 — the flat side is taken.\n *\n * Every other scale only borrows the signature of its parallel major or minor,\n * so nothing follows from it and the scale is spelled on the tonic it reads\n * best from: the fewest double accidentals first, then the fewest accidentals\n * altogether, and only then the shorter signature. That is why pitch class 1\n * altered is C# — C# D E F G A B — rather than Db, which would have to write\n * four double flats to say the same thing, and why pitch class 8 minor\n * pentatonic is G# B C# D# F# rather than Ab B Db Eb Gb, which reads its third\n * as an augmented second.\n *\n * The mask's third picks the mode the signature is measured in — a minor third\n * and no major third reads as minor, anything else as major — and the key is\n * returned unchanged, so a scale keeps its own mask and only borrows a tonic\n * spelling. A caller that has a tonic spelling of its own always keeps it: this\n * only answers for a key that arrives as a bare pitch class.\n *\n * Every 12-bit mask and every integer root is answered — a root outside [0, 11]\n * is reduced first, and a mask with no third at all counts as major.\n *\n * The key is paired with the answer exactly as it was handed in, root and mask\n * untouched, so this takes a plain key/scale rather than the wider forms the\n * relations below accept: a coerced key could not be given back verbatim.\n *\n * @param key The key/scale to spell.\n * @returns The conventional tonic spelling, paired with `key` verbatim.\n * @example\n * ```ts\n * import { formatNote, majorKey, scaleByName, spelledKeyOf } from '@libraz/libcantus';\n * formatNote(spelledKeyOf(majorKey(1)).tonic); // 'Db'\n * formatNote(spelledKeyOf(scaleByName('harmonicMinor', 8)).tonic); // 'G#'\n * formatNote(spelledKeyOf(scaleByName('altered', 1)).tonic); // 'C#'\n * ```\n * @category Scales\n */\nexport function spelledKeyOf(key: KeyScale): ResolvedKey {\n const scale = assertRecord<KeyScale>(key, 'key');\n const rootPc = pitchClassOf(scale.rootPc);\n const signatureKey = isSignatureKey(scale);\n // Every pitch class is named somewhere in the span, so the starting tonic here\n // only stands in until the scan makes its first match.\n let tonic: Note = keyFromFifths(0).tonic;\n let best: TonicCost | undefined;\n // Scanning from the flat end and keeping only a strictly better candidate\n // settles an even tie on the flat side: pitch class 6 in major is Gb, and\n // pitch class 3 in minor is Eb.\n for (let fifths = FLATTEST_TONIC_FIFTHS; fifths <= SHARPEST_TONIC_FIFTHS; fifths += 1) {\n const candidate = keyFromFifths(fifths).tonic;\n if (noteToPitchClass(candidate) !== rootPc) {\n continue;\n }\n const cost = tonicCost(candidate, scale);\n if (best === undefined || spellsBetter(cost, best, signatureKey)) {\n tonic = candidate;\n best = cost;\n }\n }\n return { tonic, scale: key, variant: variantOfMask(key.modeMask12) };\n}\n\n/**\n * The relative key: for a plain major or minor key, the same key signature read\n * in the other mode.\n *\n * C major and A minor share a signature, and so are each other's relative; the\n * relation is its own inverse for a plain major or minor key. Because the new\n * tonic is read off the circle of fifths rather than transposed by semitones,\n * the relative of Db major is Bb minor and not its enharmonic A# minor.\n *\n * A key that is not a plain major or minor is read through the mode its third\n * names and stepped from where its tonic stands on the circle, the origin every\n * relation here shares: the relative of D dorian is F major, the relative of the\n * D minor its third names. The shared-signature reading does not carry over to\n * those — D dorian is written with no accidentals and F major with one — and the\n * modes have no relative of their own for it to be measured against.\n *\n * @param tonic The spelled tonic of the key, as a note name, note data, or a\n * `Note`.\n * @param key The key/scale, as a key name, a key/scale, or a `Key`; its mask\n * decides which mode the relative is in.\n * @returns The relative key and the tonic spelling it is written with.\n * @throws If the tonic or the mask is malformed, or if the resulting signature\n * falls outside [-12, 12] fifths.\n * @example\n * ```ts\n * import { formatNote, majorKey, parseNote, relativeKeyOf } from '@libraz/libcantus';\n * formatNote(relativeKeyOf(parseNote('C'), majorKey(0)).tonic); // 'A'\n * formatNote(relativeKeyOf('Db', majorKey(1)).tonic); // 'Bb'\n * ```\n * @category Scales\n */\nexport function relativeKeyOf(tonic: NoteLike, key: KeyLike): ResolvedKey {\n const mode = modeOf(toKeyScale(key));\n // Counted from where the tonic itself stands on the circle, as every other\n // relation here counts: a mode's own signature carries an offset that\n // `keyFromFifths` cannot give back, so stepping from it would answer C major\n // for D dorian — the key one of its own neighbours already names — instead of\n // the F major a fifth flatter.\n return keyFromFifths(tonicFifths(toNoteData(tonic), mode), oppositeMode(mode));\n}\n\n/**\n * The parallel key: the other mode on the same tonic.\n *\n * The tonic keeps the spelling it was given — the parallel minor of C major is\n * C minor, never B# minor — which is why this is the one relation here that\n * does not travel the circle of fifths.\n *\n * @param tonic The spelled tonic of the key, as a note name, note data, or a\n * `Note`; its letter and alteration are kept.\n * @param key The key/scale, as a key name, a key/scale, or a `Key`; its mask\n * decides which mode the parallel is in.\n * @returns The parallel key on the same tonic.\n * @throws If the tonic or the mask is malformed.\n * @example\n * ```ts\n * import { formatNote, majorKey, parallelKeyOf, parseNote } from '@libraz/libcantus';\n * const parallel = parallelKeyOf(parseNote('C'), majorKey(0));\n * formatNote(parallel.tonic); // 'C'\n * ```\n * @category Scales\n */\nexport function parallelKeyOf(tonic: NoteLike, key: KeyLike): ResolvedKey {\n const note = toNoteData(tonic);\n const mode = oppositeMode(modeOf(toKeyScale(key)));\n // The spelled tonic decides the root, so the returned key and its tonic agree\n // even when the caller's `rootPc` was left on the other mode's root.\n const rootPc = noteToPitchClass(note);\n const scale = mode === 'minor' ? minorScale(rootPc) : majorScale(rootPc);\n return {\n tonic: { letter: diatonicLetterOf(note.letter), alter: note.alter },\n scale,\n variant: variantOfMask(scale.modeMask12),\n };\n}\n\n/** Where a spelled tonic itself stands on the circle of fifths: C at 0, F# at +6. */\nfunction tonicPosition(tonic: Note): number {\n return keySignatureFifths(tonic, majorScale(noteToPitchClass(tonic)));\n}\n\n/**\n * The signature the tonic's own plain major or minor key is written with.\n *\n * This, and not the key's signature, is what a relation that travels the circle\n * steps from. The two agree for a plain major or minor key and part company\n * everywhere else: G mixolydian has the signature of C major, so stepping from\n * that signature and reading the tonic back with {@link keyFromFifths} — which\n * knows only the major and minor offsets — would answer G major, a unison\n * above, instead of D, a fifth above.\n */\nfunction tonicFifths(tonic: Note, mode: KeyMode): number {\n return tonicPosition(tonic) - (mode === 'minor' ? MINOR_TONIC_FIFTHS : 0);\n}\n\n/**\n * Whether a key is actually written on this tonic — the same question\n * {@link spelledKeyOf} asks, and answered the same way.\n *\n * The one answer to it. Every path that reaches the question — the enharmonic\n * relation here, and the class layer's `Key.transpose`, `Key.keyOnDegree` and\n * `Key.forInstrument` through the respelling they end on — comes here, because\n * a predicate written twice drifts the moment one copy is corrected.\n *\n * A key with a signature of its own is written wherever that signature is: up\n * to seven sharps or flats, which is what makes Ab minor and G# minor two\n * spellings of one key and D# major none at all. A scale that only borrows a\n * signature is written wherever it reads without a double accidental.\n */\nexport function isWrittenTonic(tonic: Note, key: KeyScale): boolean {\n const position = tonicPosition(tonic);\n if (isSignatureKey(key)) {\n // A key with a signature of its own is written wherever that signature is,\n // and the signature says so on its own. Bounding the position by where a\n // major or a minor key puts a tonic would answer for it and answer wrong:\n // those two modes sit at offsets 0 and -3, so a window drawn from them cuts\n // off the ends of the others — a lydian is written as far flatward as F\n // flat and a locrian as far sharpward as E sharp, and both fall outside it.\n // What is left here is only the reader's own range.\n return (\n Math.abs(position) <= MAX_FIFTHS &&\n Math.abs(keySignatureFifths(tonic, key)) <= MAX_CONVENTIONAL_FIFTHS\n );\n }\n // A scale that only borrows a signature has no such test to defer to, so the\n // window stays: without it the double-accidental reading, which is not\n // symmetric between two spellings of one sound, would make the relation\n // one-way for scales that have no written spelling at either end.\n //\n // The reading is taken of the scale as this tonic roots it, which is how the\n // branch above already answers: `keySignatureFifths` reads the mask and the\n // tonic and never the root the caller's scale carries. Spelling the caller's\n // root from a tonic that does not sound it is a question with no answer, and\n // asking it made the same enharmonic lookup that a key with a signature\n // answers throw for a pentatonic.\n return (\n position >= FLATTEST_TONIC_FIFTHS &&\n position <= SHARPEST_TONIC_FIFTHS &&\n tonicCost(tonic, { rootPc: noteToPitchClass(tonic), modeMask12: key.modeMask12 }).doubles === 0\n );\n}\n\n/**\n * The dominant key: the key a fifth above, in the mode this key's third names.\n *\n * The fifth is measured from the tonic, so a key that is not a plain major or\n * minor answers with the same move: the dominant of G mixolydian is D major and\n * the dominant of D dorian is A minor.\n *\n * @param tonic The spelled tonic of the key, as a note name, note data, or a\n * `Note`.\n * @param key The key/scale, as a key name, a key/scale, or a `Key`; its mask\n * decides the mode of the result.\n * @returns The key a fifth above, in the mode this key's third names.\n * @throws If the tonic or the mask is malformed, or if the resulting signature\n * falls outside [-12, 12] fifths — the dominant of a key already at +12.\n * @example\n * ```ts\n * import { dominantKeyOf, formatNote, majorKey, parseNote } from '@libraz/libcantus';\n * formatNote(dominantKeyOf(parseNote('C'), majorKey(0)).tonic); // 'G'\n * ```\n * @category Scales\n */\nexport function dominantKeyOf(tonic: NoteLike, key: KeyLike): ResolvedKey {\n const mode = modeOf(toKeyScale(key));\n return keyFromFifths(tonicFifths(toNoteData(tonic), mode) + 1, mode);\n}\n\n/**\n * The subdominant key: the key a fifth below, in the mode this key's third\n * names, and the exact inverse of {@link dominantKeyOf} on the tonic spelling.\n *\n * @param tonic The spelled tonic of the key, as a note name, note data, or a\n * `Note`.\n * @param key The key/scale, as a key name, a key/scale, or a `Key`; its mask\n * decides the mode of the result.\n * @returns The key a fifth below, in the mode this key's third names.\n * @throws If the tonic or the mask is malformed, or if the resulting signature\n * falls outside [-12, 12] fifths — the subdominant of a key already at -12.\n * @example\n * ```ts\n * import { formatNote, majorKey, parseNote, subdominantKeyOf } from '@libraz/libcantus';\n * formatNote(subdominantKeyOf(parseNote('C'), majorKey(0)).tonic); // 'F'\n * ```\n * @category Scales\n */\nexport function subdominantKeyOf(tonic: NoteLike, key: KeyLike): ResolvedKey {\n const mode = modeOf(toKeyScale(key));\n return keyFromFifths(tonicFifths(toNoteData(tonic), mode) - 1, mode);\n}\n\n/**\n * The same sounding key written the other way round the circle of fifths, or\n * null when it has no second spelling.\n *\n * Twelve fifths is one enharmonic turn of the circle, so a tonic's alternative\n * spelling sits at its own position ±12: Db (-5) is also C# (+7). It is offered\n * only when the key is actually written there, by the same test\n * {@link spelledKeyOf} spells with. That is why C major is alone while Db major\n * and F# major each answer with their twin: Eb major's twin would be D# major\n * and its nine sharps, and pitch class 3 altered is written D# but never Eb,\n * which would need two double flats to say the same thing.\n *\n * The answer is therefore always a spelling the key is written on, which makes\n * the relation its own inverse for such a key. A key spelled some other way —\n * a tonic a transposition drove out to Ebb, say — still names its written twin,\n * so this is also the way back from a spelling no one writes.\n *\n * The relation respells rather than moves, so the mask is kept: the other\n * spelling of C# lydian is Db lydian, and its root pitch class is the one it\n * was handed.\n *\n * @param tonic The spelled tonic of the key, as a note name, note data, or a\n * `Note`.\n * @param key The key/scale, as a key name, a key/scale, or a `Key`; its mask\n * decides the mode the tonic is read in, and is carried into the result.\n * @returns The other spelling of the key, or null when it is not written.\n * @throws If the tonic or the mask is malformed.\n * @example\n * ```ts\n * import { enharmonicKeyOf, formatNote, majorKey, parseNote } from '@libraz/libcantus';\n * const other = enharmonicKeyOf(parseNote('Db'), majorKey(1));\n * other === null ? 'none' : formatNote(other.tonic); // 'C#'\n * enharmonicKeyOf('C', majorKey(0)); // null\n * ```\n * @category Scales\n */\nexport function enharmonicKeyOf(tonic: NoteLike, key: KeyLike): ResolvedKey | null {\n const note = toNoteData(tonic);\n const scale = toKeyScale(key);\n maskOf(scale);\n for (const position of [\n tonicPosition(note) - ENHARMONIC_FIFTHS,\n tonicPosition(note) + ENHARMONIC_FIFTHS,\n ]) {\n // Bounded by what the reader can spell, and no further: narrowing it to the\n // major/minor window here would refuse the far spelling of a modal key\n // while the same call from that spelling still found this one, and the\n // relation is documented to be its own inverse.\n if (Math.abs(position) > MAX_FIFTHS) {\n continue;\n }\n const spelled = keyFromFifths(position).tonic;\n if (!isWrittenTonic(spelled, scale)) {\n continue;\n }\n return {\n tonic: spelled,\n // The mask is the caller's; only the root is re-read, so a tonic that did\n // not spell the mask's own root still comes back consistent.\n scale: { rootPc: noteToPitchClass(spelled), modeMask12: scale.modeMask12 },\n variant: variantOfMask(scale.modeMask12),\n };\n }\n return null;\n}\n\n/** The relative of the dominant key: two moves along the circle, then across. */\nfunction relativeOfDominantKeyOf(tonic: Note, key: KeyScale): ResolvedKey {\n const dominant = dominantKeyOf(tonic, key);\n return relativeKeyOf(dominant.tonic, dominant.scale);\n}\n\n/** The relative of the subdominant key. */\nfunction relativeOfSubdominantKeyOf(tonic: Note, key: KeyScale): ResolvedKey {\n const subdominant = subdominantKeyOf(tonic, key);\n return relativeKeyOf(subdominant.tonic, subdominant.scale);\n}\n\n/**\n * The closely related keys, in the order {@link relatedKeysOf} reports them and\n * {@link keyRelationBetween} tests them.\n */\nconst CLOSELY_RELATED: readonly {\n relation: KeyRelation;\n of: (tonic: Note, key: KeyScale) => ResolvedKey;\n}[] = [\n { relation: 'relative', of: relativeKeyOf },\n { relation: 'parallel', of: parallelKeyOf },\n { relation: 'dominant', of: dominantKeyOf },\n { relation: 'subdominant', of: subdominantKeyOf },\n { relation: 'relativeOfDominant', of: relativeOfDominantKeyOf },\n { relation: 'relativeOfSubdominant', of: relativeOfSubdominantKeyOf },\n];\n\n/**\n * The six closely related keys of a key, each tagged with its relation.\n *\n * These are the keys the German and Japanese teaching tradition counts as a\n * key's near relations: the relative and parallel keys, the dominant and the\n * subdominant, and the relatives of those two. For C major they are A minor,\n * C minor, G major, F major, E minor and D minor, in that order. For a plain\n * major or minor key five of them are written within one accidental of the key's\n * own signature — the relative shares it exactly and the other four stand one\n * away; the parallel key does not — C minor is three flats away from C major —\n * and belongs to the set for the tonic it shares rather than for the signature\n * it carries.\n *\n * A key that is not a plain major or minor is read through the mode its third\n * names, so its relations are walked from where its tonic stands on the circle\n * rather than from its own signature, and the accidental counts above do not\n * describe them: the relative of D dorian is F major, one flat against dorian's\n * none.\n *\n * @param tonic The spelled tonic of the key, as a note name, note data, or a\n * `Note`.\n * @param key The key/scale, as a key name, a key/scale, or a `Key`.\n * @returns The six related keys, in relation order, each with its tonic\n * spelling and its `relation` tag.\n * @throws If the tonic or the mask is malformed, or if a neighbouring signature\n * falls outside [-12, 12] fifths.\n * @example\n * ```ts\n * import { majorKey, parseNote, relatedKeysOf } from '@libraz/libcantus';\n * relatedKeysOf(parseNote('C'), majorKey(0)).map((related) => related.relation);\n * // ['relative', 'parallel', 'dominant', 'subdominant', 'relativeOfDominant', 'relativeOfSubdominant']\n * ```\n * @category Scales\n */\nexport function relatedKeysOf(\n tonic: NoteLike,\n key: KeyLike,\n): (ResolvedKey & { relation: KeyRelation })[] {\n const note = toNoteData(tonic);\n const scale = toKeyScale(key);\n return CLOSELY_RELATED.map(({ relation, of }) => ({ ...of(note, scale), relation }));\n}\n\n/**\n * How the second key stands to the first, or null when they are not related.\n *\n * The relations are tested in a fixed order — identity, enharmonic spelling,\n * then the six closely related keys in the order {@link relatedKeysOf} lists\n * them — and the first match is reported. Only the identity test looks at the\n * tonic spelling; every other test compares root pitch class and mode, so\n * C# minor and Db minor both read as the relative of E major.\n *\n * Both keys are read through the mode their third names, exactly as\n * {@link relatedKeysOf} reads the key it is given, so A harmonic minor stands\n * to C major as A minor does whichever of the two is asked about. That is what\n * makes the answer symmetric: if one key's related keys name the other, the\n * relation between them is reported from either side.\n *\n * @param a The key the relation is measured from.\n * @param b The key the relation is measured to.\n * @returns The relation, or null when `b` is none of `a`'s related keys.\n * @throws If either key is malformed, or if a neighbouring signature of `a`\n * falls outside [-12, 12] fifths.\n * @example\n * ```ts\n * import { keyRelationBetween, majorKey, minorKey, spelledKeyOf } from '@libraz/libcantus';\n * const cMajor = spelledKeyOf(majorKey(0));\n * keyRelationBetween(cMajor, spelledKeyOf(minorKey(9))); // 'relative'\n * keyRelationBetween(cMajor, spelledKeyOf(minorKey(3))); // null\n * ```\n * @category Scales\n */\nexport function keyRelationBetween(a: ResolvedKey, b: ResolvedKey): KeyRelation | null {\n const from = assertRecord<ResolvedKey>(a, 'a');\n const to = assertRecord<ResolvedKey>(b, 'b');\n maskOf(from.scale, 'a.scale');\n maskOf(to.scale, 'b.scale');\n if (soundsLike(from.scale, to.scale)) {\n return spelledLike(from.tonic, to.tonic) ? 'same' : 'enharmonic';\n }\n for (const { relation, of } of CLOSELY_RELATED) {\n if (soundsLike(of(from.tonic, from.scale).scale, to.scale)) {\n return relation;\n }\n }\n return null;\n}\n","import { InvalidInputError } from '../../core/errors/index.js';\nimport type { Note } from '../../core/pitch/index.js';\nimport { formatNote, noteToPitchClass } from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport { assertRecord } from '../../core/validation/index.js';\nimport { type KeyLike, toKeyScale } from './coerce.js';\nimport type { KeyVariant, ResolvedKey } from './kinds.js';\nimport { assertKeyVariant, maskOf, variantOfMask } from './masks.js';\nimport { resolveKeyName } from './name.js';\nimport { spelledKeyOf } from './relations.js';\n\n/**\n * A key's whole identity, and the one resolver that produces it.\n *\n * A key is three facts, and the library used to keep them in three layers: the\n * pitch classes in the core, the spelled tonic in the theory, the scale form in\n * the analysis. Only the topmost layer could hold all three, so a key handed\n * down from there arrived as pitch classes and came back spelled from whichever\n * side of the circle read best — an Ab minor went down and a G# minor came up.\n *\n * The three are one value here, low enough that every layer that reads a key\n * can carry it whole. Reducing a key to its pitch classes is still a thing a\n * caller may need, but it is now something written down at the place it\n * happens, rather than what happens by default on the way past.\n */\n\nexport type { KeyVariant, ResolvedKey };\nexport { assertKeyVariant, variantOfMask };\n\n/** Whether a value carries the fields a resolved key is read through. */\nfunction carriesIdentity(value: unknown): value is { tonic?: Note; variant?: KeyVariant } {\n return typeof value === 'object' && value !== null;\n}\n\n/**\n * A carried tonic, refused when it spells a pitch class the scale is not rooted\n * on.\n *\n * A key whose tonic and scale disagree is two answers to what the key is, and\n * nothing downstream can tell which was meant: the chord builders read the\n * pitch classes and the spellers read the letters, so an augmented sixth comes\n * back naming one chord by its pitches and another by its letters. The words\n * are `Key.of`'s, because a resolver that accepted what the class refuses would\n * make the two APIs answer the same input differently.\n */\nfunction carriedTonic(tonic: Note, scale: KeyScale): Note {\n if (noteToPitchClass(tonic) !== scale.rootPc) {\n throw new InvalidInputError(\n `tonic ${formatNote(tonic)} does not match the scale root pitch class ${scale.rootPc}; ` +\n 'pass a tonic that spells the scale root, or omit it to have one chosen',\n );\n }\n return { letter: tonic.letter, alter: tonic.alter };\n}\n\n/** A carried scale form, refused when the mask it arrived with does not hold it. */\nfunction carriedVariant(variant: KeyVariant, scale: KeyScale): KeyVariant {\n assertKeyVariant(variant, scale.modeMask12);\n return variant;\n}\n\n/**\n * Resolve any key-shaped value to the whole key it names.\n *\n * The counterpart of {@link toKeyScale}, and the one to prefer: both read the\n * same shapes, but this one keeps what it read. A caller that genuinely wants\n * only the pitch classes asks for them with {@link scaleOf}, which says so at\n * the place it happens.\n *\n * What the value does not carry is derived rather than dropped: a bare scale is\n * spelled by {@link spelledKeyOf}, the same reading the rest of the library\n * uses, and its form is read from its mask. A key that carries a spelled tonic\n * keeps that one — the caller's spelling outranks any the library would pick.\n * A name carries one too, and it is kept: `'Ab minor'` resolves to an A flat\n * minor rather than being reduced to pitch classes and spelled back as the G#\n * minor those read best as.\n *\n * What it carries is checked rather than trusted. Accepting a wide range of\n * shapes is not the same as accepting whatever those shapes hold, and a key\n * whose tonic or form contradicts its own scale is refused here in the words\n * `Key.of` refuses it — the two APIs answer the same input alike, and the\n * refusal reaches every entry point built on this one.\n *\n * @param value A key name, a plain key/scale, a resolved key, or a value whose\n * `toJSON` returns one of those.\n * @returns The key, whole.\n * @throws If the value names no key, its mask is out of range or excludes its\n * own root, its tonic spells a pitch class its scale is not rooted on, or its\n * form is not one that mask holds.\n * @example\n * ```ts\n * import { formatNote, resolveKey } from '@libraz/libcantus';\n * formatNote(resolveKey('Ab minor').tonic); // 'Ab'\n * resolveKey('C major').tonic.letter; // 0\n * ```\n * @category Scales\n */\nexport function resolveKey(value: KeyLike): ResolvedKey {\n if (typeof value === 'string') {\n return resolveKeyName(value);\n }\n const scale = toKeyScale(value);\n const carried =\n carriesIdentity(value) && 'toJSON' in value && typeof value.toJSON === 'function'\n ? (value.toJSON() as unknown)\n : value;\n const tonic =\n carriesIdentity(carried) && carried.tonic !== undefined\n ? carriedTonic(carried.tonic, scale)\n : spelledKeyOf(scale).tonic;\n const variant =\n carriesIdentity(carried) && carried.variant !== undefined\n ? carriedVariant(carried.variant, scale)\n : variantOfMask(scale.modeMask12);\n return { scale, tonic, variant };\n}\n\n/**\n * The pitch classes of a key, with its spelling and form deliberately dropped.\n *\n * Named so that dropping them is something a reader can see. A layer that\n * reduces a key silently is where an Ab minor becomes a G# minor, and the only\n * way to keep finding those is to make the reduction say its own name.\n *\n * @param key The key to read.\n * @returns Its pitch classes alone.\n * @category Scales\n */\nexport function scaleOf(key: ResolvedKey): KeyScale {\n const scale = assertRecord<ResolvedKey>(key, 'key').scale;\n maskOf(scale, 'key.scale');\n return scale;\n}\n","import type { KeyScale } from '../../core/types.js';\nimport type { SpelledKeyScale } from './kinds.js';\nimport type { ScaleNameInput } from './masks.js';\nimport { spelledKeyOf } from './relations.js';\nimport { majorScale, minorScale, namedScale } from './scales.js';\n\n/**\n * A built key, with the spelling it is conventionally written in beside it.\n *\n * The tonic is read by the one reader the rest of the library reads keys with,\n * so a built key is spelled exactly as the same pitch classes would be spelled\n * anywhere else — nothing about the answer changes by carrying it. What changes\n * is that the key can now be handed to an entry point whose answer depends on\n * how the key is written, which a bare scale cannot be: a bare scale reaching\n * one of those has had a spelling and lost it.\n */\nfunction spelled(scale: KeyScale): SpelledKeyScale {\n const { tonic, variant } = spelledKeyOf(scale);\n return { ...scale, tonic, variant };\n}\n\n/**\n * Build a `KeyScale` for a major key on the given root pitch class.\n *\n * @example\n * ```ts\n * import { majorKey } from '@libraz/libcantus';\n * const cMajor = majorKey(0); // C major: { rootPc: 0, modeMask12: MAJOR_MASK }\n * ```\n *\n * @category Scales\n */\nexport function majorKey(rootPc: number): SpelledKeyScale {\n return spelled(majorScale(rootPc));\n}\n\n/**\n * Build a `KeyScale` for a natural-minor key on the given root pitch class.\n *\n * @example\n * ```ts\n * import { minorKey } from '@libraz/libcantus';\n * const aMinor = minorKey(9); // A natural minor: { rootPc: 9, modeMask12: NATURAL_MINOR_MASK }\n * ```\n *\n * @category Scales\n */\nexport function minorKey(rootPc: number): SpelledKeyScale {\n return spelled(minorScale(rootPc));\n}\n\n/**\n * Build a `KeyScale` from a named scale (see {@link NAMED_SCALES}).\n *\n * @param name The scale name, e.g. `'dorian'` or `'harmonicMinor'`.\n * @param rootPc The root pitch class.\n * @returns The key/scale.\n * @throws If the name is not a known scale.\n *\n * @example\n * ```ts\n * import { scaleByName } from '@libraz/libcantus';\n * const dDorian = scaleByName('dorian', 2); // D Dorian, rootPc 2\n * ```\n *\n * @category Scales\n */\nexport function scaleByName(name: ScaleNameInput, rootPc: number): SpelledKeyScale {\n return spelled(namedScale(name, rootPc));\n}\n","/**\n * Scale systems: which pitch organisations functional harmony was built for,\n * and which ones it would only be imposed on.\n *\n * Roman numerals, tonic/subdominant/dominant function and cadence detection all\n * presuppose Western common practice. A `KeyScale` is a root pitch class and a\n * twelve-bit mask, which says nothing about the tradition the scale came from,\n * so analysis code that wants to branch instead of assuming needs this\n * classification to branch on.\n */\n\nimport { InvalidInputError } from '../../core/errors/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport type { ScaleName, ScaleNameInput, WorldScaleName } from './masks.js';\nimport { maskOf, NAMED_SCALES, resolveScaleName, WORLD_SCALES } from './masks.js';\n\n/**\n * The kind of pitch organisation a scale belongs to.\n *\n * - `'common-practice'`: the scale functional harmony is defined on — a tonic,\n * a dominant carrying a leading tone, and a triad available on every degree.\n * - `'modal'`: a seven-note rotation or alteration of that material. Degrees\n * and Roman numerals still describe it, but a dominant-to-tonic cadence is\n * not what holds the music together.\n * - `'non-functional'`: the collection carries no chord function of its own. A\n * gapped or symmetric set takes its harmony from whatever key it is used\n * over, and a scale from a tradition organised melodically rather than by\n * chords has no functional reading at all.\n *\n * @category Scales\n */\nexport type ScaleSystem = 'common-practice' | 'modal' | 'non-functional';\n\n/**\n * The system each built-in scale belongs to.\n *\n * Every name in {@link NAMED_SCALES} and {@link WORLD_SCALES} appears here, so\n * a scale added without a classification fails to compile rather than falling\n * silently into a default. Aliases are classified through the name they resolve\n * to; use {@link scaleSystemOf} to look one up.\n *\n * @category Scales\n */\nexport const SCALE_SYSTEMS: Readonly<Record<ScaleName | WorldScaleName, ScaleSystem>> =\n Object.freeze({\n major: 'common-practice',\n ionian: 'common-practice',\n naturalMinor: 'common-practice',\n aeolian: 'common-practice',\n harmonicMinor: 'common-practice',\n melodicMinor: 'common-practice',\n dorian: 'modal',\n phrygian: 'modal',\n lydian: 'modal',\n mixolydian: 'modal',\n locrian: 'modal',\n lydianDominant: 'modal',\n mixolydianB13: 'modal',\n locrianNatural2: 'modal',\n altered: 'modal',\n phrygianDominant: 'modal',\n majorPentatonic: 'non-functional',\n minorPentatonic: 'non-functional',\n blues: 'non-functional',\n wholeTone: 'non-functional',\n octatonicHalfWhole: 'non-functional',\n octatonicWholeHalf: 'non-functional',\n chromatic: 'non-functional',\n miyakoBushi: 'non-functional',\n ritsu: 'non-functional',\n minyo: 'non-functional',\n ryukyu: 'non-functional',\n ajam: 'non-functional',\n nahawand: 'non-functional',\n kurd: 'non-functional',\n hijaz: 'non-functional',\n hijazkar: 'non-functional',\n bilaval: 'non-functional',\n khamaj: 'non-functional',\n kafi: 'non-functional',\n asavari: 'non-functional',\n bhairavi: 'non-functional',\n bhairav: 'non-functional',\n kalyan: 'non-functional',\n marwa: 'non-functional',\n purvi: 'non-functional',\n todi: 'non-functional',\n });\n\n/**\n * System of each built-in mask, Western reading first.\n *\n * Maqam Nahawand and the natural minor are the same twelve pitch classes, so a\n * mask cannot tell them apart. Building the table in name order — the Western\n * register before the world one — makes the mask answer with the Western\n * reading, which is the reading a bare `KeyScale` from key detection or from a\n * caller's own data actually carries.\n */\nconst SYSTEM_BY_MASK: ReadonlyMap<number, ScaleSystem> = (() => {\n const byMask = new Map<number, ScaleSystem>();\n const names: (ScaleName | WorldScaleName)[] = [\n ...(Object.keys(NAMED_SCALES) as ScaleName[]),\n ...(Object.keys(WORLD_SCALES) as WorldScaleName[]),\n ];\n for (const name of names) {\n const mask = Object.hasOwn(NAMED_SCALES, name)\n ? NAMED_SCALES[name as ScaleName]\n : WORLD_SCALES[name as WorldScaleName];\n if (!byMask.has(mask)) {\n byMask.set(mask, SCALE_SYSTEMS[name]);\n }\n }\n return byMask;\n})();\n\n/**\n * The system a scale belongs to.\n *\n * A name — canonical or an alias — is answered exactly: `'nahawand'` is a maqam\n * even though its mask is the natural minor's. A `KeyScale` carries no\n * tradition, so it is answered by its mask, under the Western reading of that\n * mask; a mask that is no built-in scale answers undefined.\n *\n * @param scale The scale name, or a key/scale to read the mask of.\n * @returns The system, or undefined for a mask that names no built-in scale.\n * @throws If `scale` is a string naming no built-in scale. An unknown name is a\n * typo, and answering it the way an unrecognised mask is answered would hide\n * that.\n * @example\n * ```ts\n * import { scaleSystemOf, majorKey } from '@libraz/libcantus';\n * scaleSystemOf('harmonicMinor'); // 'common-practice'\n * scaleSystemOf('dorian'); // 'modal'\n * scaleSystemOf('hijaz'); // 'non-functional'\n * scaleSystemOf(majorKey(0)); // 'common-practice'\n * ```\n * @category Scales\n */\nexport function scaleSystemOf(scale: KeyScale | ScaleNameInput): ScaleSystem | undefined {\n if (typeof scale === 'string') {\n const canonical = resolveScaleName(scale);\n if (canonical === undefined) {\n throw new InvalidInputError(`Unknown scale: ${scale}`);\n }\n return SCALE_SYSTEMS[canonical];\n }\n return SYSTEM_BY_MASK.get(maskOf(scale, 'scale'));\n}\n\n/**\n * Whether functional (Roman-numeral) analysis applies to a scale.\n *\n * True for common-practice and modal scales, false for everything classed\n * `'non-functional'` — analysis that would otherwise read a raga as a chord\n * progression can ask here first.\n *\n * The two ways of naming a scale are answered differently, exactly as\n * {@link scaleSystemOf} answers them. A name carries its tradition, so\n * `'hijaz'` is false. A `KeyScale` carries only a mask, and a mask shared with\n * a Western scale is read the Western way — maqam Hijaz and the phrygian\n * dominant are the same seven pitch classes, so that mask is modal and answers\n * true. A mask that names no built-in scale answers true as well, which keeps a\n * caller's own mask being analysed the way it always has been. Pass the name\n * whenever the tradition is known and the answer should follow it.\n *\n * @param scale The scale name, or a key/scale to read the mask of.\n * @returns True when functional harmony describes the scale.\n * @throws If `scale` is a string naming no built-in scale.\n * @example\n * ```ts\n * import { supportsFunctionalHarmony, scaleByName } from '@libraz/libcantus';\n * supportsFunctionalHarmony('harmonicMinor'); // true\n * supportsFunctionalHarmony('bhairav'); // false\n * supportsFunctionalHarmony('hijaz'); // false — named, so read as a maqam\n * supportsFunctionalHarmony(scaleByName('hijaz', 0)); // true — its mask is the phrygian dominant\n * supportsFunctionalHarmony(scaleByName('ryukyu', 0)); // false — a mask no Western scale shares\n * ```\n * @category Scales\n */\nexport function supportsFunctionalHarmony(scale: KeyScale | ScaleNameInput): boolean {\n return scaleSystemOf(scale) !== 'non-functional';\n}\n","/**\n * The structured reading of a chord: what it is built from, not what it is\n * called.\n *\n * A quality name is a closed vocabulary and a lead sheet's harmony is not. A\n * chart writes a *set* of alterations over a base — `maj7#11`, `7b9#11`,\n * `sus4(add9)` — so an enumeration of names can never be complete, and every\n * name added to one is a name missing from its neighbours. A {@link ChordSpec}\n * names the parts instead: a base triad, an optional seventh, altered, added\n * and omitted degrees. A combination nothing names still has exact intervals,\n * an exact pitch-class set, and a symbol that reads back as itself.\n *\n * Each {@link ChordQuality} is an alias for one spec, so the two directions\n * cannot drift: a named chord's interval template is derived from its spec\n * rather than listed beside it.\n */\n\nimport { InvalidInputError } from '../../core/errors/index.js';\nimport { assertFiniteNumber, describeRejected } from '../../core/validation/index.js';\n\n/**\n * Chord quality identifiers understood by the chord builder.\n *\n * Each name is an alias for one {@link ChordSpec}. The union is closed, so it\n * names the chords a chart writes most often rather than every chord that can\n * be written; build the rest from a spec.\n *\n * @category Chords\n */\nexport type ChordQuality =\n | 'maj'\n | 'min'\n | 'dim'\n | 'aug'\n | 'maj7'\n | 'min7'\n | 'dom7'\n | 'dim7'\n | 'm7b5'\n | 'minMaj7'\n | 'minMaj9'\n | 'minMaj11'\n | 'minMaj13'\n | 'aug7'\n | 'augMaj7'\n | 'majb5'\n | '6'\n | 'min6'\n | '6/9'\n | 'sus2'\n | 'sus4'\n | 'add9'\n | 'add11'\n | 'maj9'\n | 'min9'\n | 'dom9'\n | '7b9'\n | '7#9'\n | '7#11'\n | '7b13'\n | '11'\n | '13'\n | '5'\n | '7sus4'\n | '7b5'\n | '7alt'\n | '13b9'\n | 'maj13'\n | 'maj7#11'\n | 'min11'\n | 'min13'\n | 'minAdd9'\n | 'min6/9';\n\n/**\n * The triad (or dyad) a chord is built on, before any seventh or tension.\n *\n * `sus2` and `sus4` replace the third rather than sitting beside it, and\n * `power` is the bare root and fifth, so the base says which tones the chord\n * has before anything is added to them.\n *\n * @category Chords\n */\nexport type ChordBase = 'maj' | 'min' | 'dim' | 'aug' | 'sus2' | 'sus4' | 'power';\n\n/**\n * The seventh stacked over a base: major, minor, or diminished.\n *\n * @category Chords\n */\nexport type ChordSeventh = 'maj7' | 'min7' | 'dim7';\n\n/**\n * A chord degree that can be raised or lowered: the fifth and the tensions.\n *\n * @category Chords\n */\nexport type AlteredDegree = 5 | 9 | 11 | 13;\n\n/**\n * One altered degree: which one, and by how much.\n *\n * `alter: 0` is not redundant — it says the degree is *present* and unaltered,\n * which is what the natural ninth of a thirteenth chord is.\n *\n * @category Chords\n */\nexport type Alteration = {\n degree: AlteredDegree;\n alter: -1 | 0 | 1;\n};\n\n/**\n * A chord as its parts: base, seventh, alterations, additions and omissions.\n *\n * The library's structural chord model. `alterations` are tensions over a\n * seventh chord; `additions` are degrees added without one (`add9`, and the\n * sixth of a sixth chord); `omissions` are degrees the chord leaves out (the\n * third of an eleventh chord, the fifth of a shell voicing) named by chord\n * degree, so `omissions: [5]` is a fifth-less chord.\n *\n * @category Chords\n */\nexport type ChordSpec = {\n rootPc: number;\n base: ChordBase;\n seventh?: ChordSeventh;\n alterations: Alteration[];\n additions: number[];\n omissions: number[];\n bassPc?: number;\n};\n\n/** The tones each base names, as `[chord degree, semitones above the root]`. */\nexport const BASE_TONES: Record<ChordBase, readonly (readonly [number, number])[]> = {\n maj: [\n [1, 0],\n [3, 4],\n [5, 7],\n ],\n min: [\n [1, 0],\n [3, 3],\n [5, 7],\n ],\n dim: [\n [1, 0],\n [3, 3],\n [5, 6],\n ],\n aug: [\n [1, 0],\n [3, 4],\n [5, 8],\n ],\n sus2: [\n [1, 0],\n [3, 2],\n [5, 7],\n ],\n sus4: [\n [1, 0],\n [3, 5],\n [5, 7],\n ],\n power: [\n [1, 0],\n [5, 7],\n ],\n};\n\n/** Semitones above the root of each seventh. */\nexport const SEVENTH_SEMITONES: Record<ChordSeventh, number> = { maj7: 11, min7: 10, dim7: 9 };\n\n/** Semitones above the root of each addable/alterable degree, unaltered. */\nexport const DEGREE_SEMITONES: Record<number, number> = { 6: 9, 9: 14, 11: 17, 13: 21 };\n\n/** Semitones above the root of an unaltered fifth, the tone alterations move. */\nexport const PERFECT_FIFTH = 7;\n\n/** The identity every alteration of the fifth shares, since a chord has one. */\nconst FIFTH_KEY = '5';\n\n/** Chord degrees a chord may omit: root, third, fifth. */\nexport const OMITTABLE_DEGREES = [1, 3, 5];\n\n/** The quality naming each base on its own, when nothing closer fits. */\nexport const BASE_QUALITY: Record<ChordBase, ChordQuality> = {\n maj: 'maj',\n min: 'min',\n dim: 'dim',\n aug: 'aug',\n sus2: 'sus2',\n sus4: 'sus4',\n power: '5',\n};\n\n/** The parts of a quality's spec, in the order a chord is built from them. */\ntype SpecParts = {\n seventh?: ChordSeventh;\n alt?: readonly (readonly [AlteredDegree, -1 | 0 | 1])[];\n add?: readonly number[];\n omit?: readonly number[];\n};\n\n/** Build a rootless spec for the alias table, so it reads as the chord does. */\nfunction parts(base: ChordBase, spec: SpecParts = {}): ChordSpec {\n const built: ChordSpec = {\n rootPc: 0,\n base,\n alterations: (spec.alt ?? []).map(([degree, alter]) => ({ degree, alter })),\n additions: [...(spec.add ?? [])],\n omissions: [...(spec.omit ?? [])],\n };\n if (spec.seventh !== undefined) {\n built.seventh = spec.seventh;\n }\n return built;\n}\n\n/**\n * The spec each quality name stands for, in the order the names are declared.\n *\n * A list, not an object: `5`, `6`, `11` and `13` are integer-like property\n * names, which JavaScript hoists to the front of any object's key order, so an\n * object literal could not hold the order this file writes. The order is stable\n * but carries no meaning beyond that: {@link chordQualities} publishes it and\n * chord detection uses it as a final tie-break, so it is part of the observable\n * behaviour and does not get reshuffled casually.\n */\nexport const QUALITY_SPECS: readonly (readonly [ChordQuality, ChordSpec])[] = [\n ['maj', parts('maj')],\n ['min', parts('min')],\n ['dim', parts('dim')],\n ['aug', parts('aug')],\n ['majb5', parts('maj', { alt: [[5, -1]] })],\n ['maj7', parts('maj', { seventh: 'maj7' })],\n ['min7', parts('min', { seventh: 'min7' })],\n ['dom7', parts('maj', { seventh: 'min7' })],\n ['dim7', parts('dim', { seventh: 'dim7' })],\n ['m7b5', parts('dim', { seventh: 'min7' })],\n ['minMaj7', parts('min', { seventh: 'maj7' })],\n ['minMaj9', parts('min', { seventh: 'maj7', alt: [[9, 0]] })],\n [\n 'minMaj11',\n parts('min', {\n seventh: 'maj7',\n alt: [\n [9, 0],\n [11, 0],\n ],\n }),\n ],\n [\n 'minMaj13',\n parts('min', {\n seventh: 'maj7',\n alt: [\n [9, 0],\n [13, 0],\n ],\n }),\n ],\n ['aug7', parts('aug', { seventh: 'min7' })],\n ['augMaj7', parts('aug', { seventh: 'maj7' })],\n ['6', parts('maj', { add: [6] })],\n ['min6', parts('min', { add: [6] })],\n ['6/9', parts('maj', { add: [6, 9] })],\n ['sus2', parts('sus2')],\n ['sus4', parts('sus4')],\n ['add9', parts('maj', { add: [9] })],\n ['add11', parts('maj', { add: [11] })],\n ['maj9', parts('maj', { seventh: 'maj7', alt: [[9, 0]] })],\n ['min9', parts('min', { seventh: 'min7', alt: [[9, 0]] })],\n ['dom9', parts('maj', { seventh: 'min7', alt: [[9, 0]] })],\n ['7b9', parts('maj', { seventh: 'min7', alt: [[9, -1]] })],\n ['7#9', parts('maj', { seventh: 'min7', alt: [[9, 1]] })],\n ['7#11', parts('maj', { seventh: 'min7', alt: [[11, 1]] })],\n ['7b13', parts('maj', { seventh: 'min7', alt: [[13, -1]] })],\n // The eleventh chord as a chart voices it: the third gives way to the\n // eleventh a semitone above it rather than sounding against it.\n [\n '11',\n parts('maj', {\n seventh: 'min7',\n alt: [\n [9, 0],\n [11, 0],\n ],\n omit: [3],\n }),\n ],\n [\n '13',\n parts('maj', {\n seventh: 'min7',\n alt: [\n [9, 0],\n [13, 0],\n ],\n }),\n ],\n ['5', parts('power')],\n ['7sus4', parts('sus4', { seventh: 'min7' })],\n ['7b5', parts('maj', { seventh: 'min7', alt: [[5, -1]] })],\n // The altered dominant as it is voiced from a lead sheet: a dominant seventh\n // with a raised fifth and a raised ninth. The full seven-note altered stack\n // has no seven-letter spelling, so it is not what this symbol denotes.\n ['7alt', parts('aug', { seventh: 'min7', alt: [[9, 1]] })],\n [\n '13b9',\n parts('maj', {\n seventh: 'min7',\n alt: [\n [9, -1],\n [13, 0],\n ],\n }),\n ],\n [\n 'maj13',\n parts('maj', {\n seventh: 'maj7',\n alt: [\n [9, 0],\n [13, 0],\n ],\n }),\n ],\n ['maj7#11', parts('maj', { seventh: 'maj7', alt: [[11, 1]] })],\n [\n 'min11',\n parts('min', {\n seventh: 'min7',\n alt: [\n [9, 0],\n [11, 0],\n ],\n }),\n ],\n [\n 'min13',\n parts('min', {\n seventh: 'min7',\n alt: [\n [9, 0],\n [13, 0],\n ],\n }),\n ],\n ['minAdd9', parts('min', { add: [9] })],\n ['min6/9', parts('min', { add: [6, 9] })],\n];\n\n/**\n * The one canonical order of the quality names.\n *\n * Every enumeration of qualities — the published list, the nearest-name scan,\n * the symbol formatter's core candidates, detection's final tie-break — reads\n * this array, so none of them can drift onto a key order the engine chose.\n */\nexport const QUALITY_ORDER: readonly ChordQuality[] = QUALITY_SPECS.map(([quality]) => quality);\n\n/** The spec behind each quality name, for lookup by name. */\nconst SPEC_BY_QUALITY: ReadonlyMap<ChordQuality, ChordSpec> = new Map(QUALITY_SPECS);\n\n/**\n * Reduce a spec to the one form that stands for its harmony.\n *\n * Two specs that name the same chord must compare equal, or a name lookup would\n * miss the chord it holds: an augmented fifth over a major triad *is* the\n * augmented triad, and a natural ninth over a chord with no seventh *is* an\n * added ninth. Normalizing is what lets `C7(#5)` and `C7#5` reach one spec, and\n * `C(9)` reach the same spec as `Cadd9`.\n *\n * Not part of the package's public surface: every spec the library hands out is\n * already normalized.\n *\n * @param spec The spec to reduce.\n * @returns An equivalent spec in canonical form.\n */\nexport function normalizeChordSpec(spec: ChordSpec): ChordSpec {\n let base = spec.base;\n // Keyed by degree *and* accidental: a flat ninth and a raised ninth are two\n // tones a chart writes side by side, so they are two alterations and not two\n // spellings of one. Only the fifth is single-valued, since a chord sounds one\n // fifth; contradictory fifths settle on the lowest, which is the same answer\n // whichever order they were written in.\n const alterations = new Map<string, Alteration>();\n for (const alteration of spec.alterations ?? []) {\n const { degree, alter } = alteration;\n if (degree === 5) {\n const current = alterations.get(FIFTH_KEY);\n if (current !== undefined && current.alter <= alter) {\n continue;\n }\n alterations.set(FIFTH_KEY, { degree, alter });\n continue;\n }\n alterations.set(`${degree}:${alter}`, { degree, alter });\n }\n const additions = new Set(spec.additions ?? []);\n const omissions = new Set(spec.omissions ?? []);\n const fifth = alterations.get(FIFTH_KEY)?.alter;\n if (fifth !== undefined) {\n // The alteration is the statement about the fifth, so a base that already\n // alters it steps back to its plain form before the two are combined.\n if (base === 'aug') {\n base = 'maj';\n } else if (base === 'dim') {\n base = 'min';\n }\n if (fifth === 0) {\n alterations.delete(FIFTH_KEY);\n } else if (base === 'maj' && fifth === 1) {\n base = 'aug';\n alterations.delete(FIFTH_KEY);\n } else if (base === 'min' && fifth === -1) {\n base = 'dim';\n alterations.delete(FIFTH_KEY);\n }\n }\n if (spec.seventh === undefined) {\n // Without a seventh under it, an unaltered upper degree is an added tone\n // rather than a tension: C(9) is the chord Cadd9 names.\n for (const [key, { degree, alter }] of [...alterations]) {\n if (degree !== 5 && alter === 0) {\n alterations.delete(key);\n additions.add(degree);\n }\n }\n }\n const normalized: ChordSpec = {\n rootPc: spec.rootPc,\n base,\n alterations: [...alterations.values()].sort((a, b) => a.degree - b.degree || a.alter - b.alter),\n additions: [...additions].sort((a, b) => a - b),\n omissions: [...omissions].sort((a, b) => a - b),\n };\n if (spec.seventh !== undefined) {\n normalized.seventh = spec.seventh;\n }\n if (spec.bassPc !== undefined) {\n normalized.bassPc = spec.bassPc;\n }\n return normalized;\n}\n\n/**\n * Derive the tones of a spec that is already validated and normalized.\n *\n * Not part of the package's public surface, and the one derivation that skips\n * {@link assertChordSpec}: the alias table below is built from it before the\n * checker's own tables are complete, and every other caller reaches it through\n * a public entry point that has already checked its spec.\n *\n * @param normalized A normalized spec whose parts the model defines.\n * @returns Semitone offsets above the root, ascending and deduplicated.\n */\nfunction specIntervals(normalized: ChordSpec): number[] {\n const tones = new Map<number, number>(BASE_TONES[normalized.base]);\n for (const { degree, alter } of normalized.alterations) {\n if (degree === 5 && tones.has(5)) {\n tones.set(5, PERFECT_FIFTH + alter);\n }\n }\n for (const degree of normalized.omissions) {\n tones.delete(degree);\n }\n const offsets = new Set(tones.values());\n if (normalized.seventh !== undefined) {\n offsets.add(SEVENTH_SEMITONES[normalized.seventh]);\n }\n for (const { degree, alter } of normalized.alterations) {\n if (degree !== 5) {\n offsets.add((DEGREE_SEMITONES[degree] ?? 0) + alter);\n }\n }\n for (const degree of normalized.additions) {\n offsets.add(DEGREE_SEMITONES[degree] ?? 0);\n }\n return [...offsets].sort((a, b) => a - b);\n}\n\n/**\n * The semitone offsets above the root a spec sounds, ascending.\n *\n * The library's single derivation of chord tones: a name contributes nothing\n * here, so a combination of alterations no name covers yields its intervals the\n * same way a `maj7` does.\n *\n * @param spec The chord spec.\n * @returns Semitone offsets above the root, ascending and deduplicated.\n * @throws If any part of the spec is not one the model defines\n * ({@link InvalidInputError}), exactly as {@link chordFromSpec} rejects it.\n *\n * @example\n * ```ts\n * import { chordSpecIntervals } from '@libraz/libcantus';\n * chordSpecIntervals({\n * rootPc: 0,\n * base: 'maj',\n * seventh: 'min7',\n * alterations: [{ degree: 9, alter: -1 }, { degree: 11, alter: 1 }],\n * additions: [],\n * omissions: [],\n * });\n * // [0, 4, 7, 10, 13, 18] — the C7(b9,#11) no quality name covers\n * ```\n *\n * @category Chords\n */\nexport function chordSpecIntervals(spec: ChordSpec): number[] {\n return specIntervals(assertChordSpec(spec));\n}\n\n/** Semitone offsets from the root for each supported chord quality. */\nexport const QUALITY_INTERVALS: Record<ChordQuality, number[]> = Object.fromEntries(\n QUALITY_SPECS.map(([quality, spec]) => [quality, specIntervals(normalizeChordSpec(spec))]),\n) as Record<ChordQuality, number[]>;\n\n/** A spec's identity as text, so two readings of one chord compare equal. */\nfunction specKey(spec: ChordSpec): string {\n const normalized = normalizeChordSpec(spec);\n return [\n normalized.base,\n normalized.seventh ?? '',\n normalized.alterations.map(({ degree, alter }) => `${alter}:${degree}`).join('.'),\n normalized.additions.join('.'),\n normalized.omissions.join('.'),\n ].join('|');\n}\n\n/** The quality naming each spec exactly, keyed by the spec's own identity. */\nconst QUALITY_BY_KEY: ReadonlyMap<string, ChordQuality> = new Map(\n QUALITY_SPECS.map(([quality, spec]) => [specKey(spec), quality]),\n);\n\n/**\n * A copy of the spec a quality name stands for, rooted on `rootPc`.\n *\n * @param quality The quality name.\n * @param rootPc Root pitch class to root the spec on.\n * @param bassPc Optional slash-chord bass pitch class.\n * @returns The spec, freshly built so the alias table is never shared.\n * @throws If the quality is not a known name.\n *\n * @category Chords\n */\nexport function chordSpecForQuality(\n quality: ChordQuality,\n rootPc: number,\n bassPc?: number,\n): ChordSpec {\n const named = SPEC_BY_QUALITY.get(quality);\n if (named === undefined) {\n throw new InvalidInputError(`Unknown chord quality: ${String(quality)}`);\n }\n const spec = normalizeChordSpec(named);\n spec.rootPc = rootPc;\n if (bassPc !== undefined) {\n spec.bassPc = bassPc;\n }\n return spec;\n}\n\n/**\n * The quality naming a spec exactly, or undefined when none does.\n *\n * Not part of the package's public surface: {@link chordSpecQuality} is the\n * total answer callers want, and this is the half of it that admits there is no\n * name.\n *\n * @param spec The chord spec.\n * @returns The quality name, or undefined.\n */\nexport function exactChordSpecQuality(spec: ChordSpec): ChordQuality | undefined {\n return QUALITY_BY_KEY.get(specKey(spec));\n}\n\n/**\n * The quality name a spec is reported under.\n *\n * A spec no name covers still has to answer, because `quality` is a required\n * field of every {@link Chord} and of the records that travel with one. The\n * answer is the *nearest* name: the largest named chord whose every tone this\n * spec also sounds, and failing that the name of its base. Nothing is lost by\n * it — the spec and the interval template still carry the alterations, and a\n * symbol is written from those rather than from this name.\n *\n * @param spec The chord spec.\n * @returns The name that fits the spec exactly, or the nearest one that fits\n * inside it.\n * @throws If any part of the spec is not one the model defines\n * ({@link InvalidInputError}), exactly as {@link chordFromSpec} rejects it.\n *\n * @example\n * ```ts\n * import { chordSpecQuality, parseChordSymbol } from '@libraz/libcantus';\n * parseChordSymbol('C7(b9,#11)').quality; // '7b9' — the nearest name\n * chordSpecQuality({ rootPc: 0, base: 'maj', seventh: 'min7', alterations: [],\n * additions: [], omissions: [] }); // 'dom7'\n * ```\n *\n * @category Chords\n */\nexport function chordSpecQuality(spec: ChordSpec): ChordQuality {\n const checked = assertChordSpec(spec);\n const exact = exactChordSpecQuality(checked);\n if (exact !== undefined) {\n return exact;\n }\n // An omitted degree is a tone the chord does not sound, not evidence against\n // a name: a fifth-less dominant is still a dominant seventh, and a template\n // that names the omitted tone must not lose to one that does not. Putting the\n // omitted degrees back drops them from both sides of the comparison at once.\n const offsets = new Set(specIntervals({ ...checked, omissions: [] }));\n let nearest: ChordQuality | undefined;\n let size = 0;\n for (const quality of QUALITY_ORDER) {\n const template = QUALITY_INTERVALS[quality];\n if (template.length > size && template.every((offset) => offsets.has(offset))) {\n nearest = quality;\n size = template.length;\n }\n }\n return nearest ?? BASE_QUALITY[checked.base];\n}\n\n/** The degree and alteration each compound semitone offset names. */\nexport const EXTENSION_ENTRIES: readonly (readonly [number, Alteration])[] = [\n [13, { degree: 9, alter: -1 }],\n [14, { degree: 9, alter: 0 }],\n [15, { degree: 9, alter: 1 }],\n [16, { degree: 11, alter: -1 }],\n [17, { degree: 11, alter: 0 }],\n [18, { degree: 11, alter: 1 }],\n [20, { degree: 13, alter: -1 }],\n [21, { degree: 13, alter: 0 }],\n [22, { degree: 13, alter: 1 }],\n];\n\n/** {@link EXTENSION_ENTRIES} by semitone offset. */\nconst EXTENSION_BY_SEMITONE: ReadonlyMap<number, Alteration> = new Map(EXTENSION_ENTRIES);\n\n/** Whether two ascending offset lists name the same tones. */\nfunction sameOffsets(a: readonly number[], b: readonly number[]): boolean {\n return a.length === b.length && a.every((offset, index) => offset === b[index]);\n}\n\n/**\n * Read an interval template structurally, or undefined when it has no reading.\n *\n * The inverse of {@link chordSpecIntervals}, and checked against it: a template\n * whose reading does not rebuild the template exactly has no spec, which is how\n * an arbitrary set of offsets — a cluster, a voicing recorded as a chord — is\n * kept from being reported as a chord it is not.\n *\n * Not part of the package's public surface: `chordSpecOf` is the entry point,\n * and it has a chord's quality to fall back on when this finds no reading.\n *\n * @param intervals Semitone offsets above the root.\n * @returns The spec (rooted on pitch class 0), or undefined.\n */\nexport function chordSpecFromIntervals(intervals: readonly number[]): ChordSpec | undefined {\n const offsets = [...new Set(intervals)].sort((a, b) => a - b);\n const has = (offset: number): boolean => offsets.includes(offset);\n const alterations: Alteration[] = [];\n const additions: number[] = [];\n const omissions: number[] = [];\n if (!has(0)) {\n omissions.push(1);\n }\n const third = has(4) ? 4 : has(3) ? 3 : has(5) ? 5 : has(2) ? 2 : undefined;\n const fifth = has(7) ? 7 : has(6) ? 6 : has(8) ? 8 : undefined;\n let base: ChordBase = 'maj';\n if (third === 3) {\n base = 'min';\n } else if (third === 5) {\n base = 'sus4';\n } else if (third === 2) {\n base = 'sus2';\n } else if (third === undefined) {\n // A bare root and fifth is the power chord itself; anything more is a chord\n // that left its third out.\n if (fifth === 7 && offsets.length === 2 && has(0)) {\n base = 'power';\n } else {\n omissions.push(3);\n }\n }\n if (fifth === undefined) {\n omissions.push(5);\n } else if (fifth !== 7) {\n if (base === 'maj' && fifth === 8) {\n base = 'aug';\n } else if (base === 'min' && fifth === 6) {\n base = 'dim';\n } else {\n alterations.push({ degree: 5, alter: fifth === 6 ? -1 : 1 });\n }\n }\n let seventh: ChordSeventh | undefined = has(11) ? 'maj7' : has(10) ? 'min7' : undefined;\n if (has(9)) {\n // Nine semitones is the diminished seventh only over a diminished triad\n // with no seventh of its own; anywhere else it is the added sixth.\n if (seventh === undefined && base === 'dim') {\n seventh = 'dim7';\n } else {\n additions.push(6);\n }\n }\n for (const offset of offsets) {\n if (offset <= 12) {\n continue;\n }\n const extension = EXTENSION_BY_SEMITONE.get(offset);\n if (extension === undefined) {\n return undefined;\n }\n alterations.push(extension);\n }\n const spec: ChordSpec = { rootPc: 0, base, alterations, additions, omissions };\n if (seventh !== undefined) {\n spec.seventh = seventh;\n }\n const normalized = normalizeChordSpec(spec);\n return sameOffsets(specIntervals(normalized), offsets) ? normalized : undefined;\n}\n\n/** The degrees an alteration may name. */\nexport const ALTERABLE_DEGREES: readonly number[] = [5, 9, 11, 13];\n\n/** The steps an alteration may move its degree by. */\nexport const ALTERATION_STEPS: readonly number[] = [-1, 0, 1];\n\n/** The degrees a spec may add without a seventh. */\nexport const ADDABLE_DEGREES: readonly number[] = [6, 9, 11, 13];\n\n/** Reject a value that is not one of a fixed set of spec parts. */\nfunction assertPart<T extends string>(value: T, allowed: readonly T[], name: string): T {\n if (!allowed.includes(value)) {\n throw new InvalidInputError(\n `${name} must be one of ${allowed.join(', ')}; received ${describeRejected(value)}`,\n );\n }\n return value;\n}\n\n/** Reject a degree a spec cannot alter, add, or omit. */\nfunction assertDegreeIn(value: number, allowed: readonly number[], name: string): number {\n if (!allowed.includes(value)) {\n throw new InvalidInputError(\n `${name} must be one of ${allowed.join(', ')}; received ${String(value)}`,\n );\n }\n return value;\n}\n\n/** Reject a list of degrees that is not a list, before it is iterated. */\nfunction assertDegreeList(\n value: readonly number[],\n allowed: readonly number[],\n name: string,\n): void {\n if (!Array.isArray(value)) {\n throw new InvalidInputError(`${name} must be an array; received ${describeRejected(value)}`);\n }\n for (const degree of value) {\n assertDegreeIn(degree, allowed, name);\n }\n}\n\n/**\n * Validate a spec and return it in canonical form.\n *\n * A spec arrives from a caller as plain data, so every part is checked before\n * any of it reaches the interval derivation: an unknown base has no tones, an\n * alteration of a degree the chord cannot alter would silently contribute\n * nothing, and a list of degrees that is not a list would fail as a `TypeError`\n * from inside the derivation rather than as this library's own error.\n *\n * @param spec The spec to check.\n * @returns The normalized spec.\n * @throws If the spec is not an object, if `alterations`, `additions` or\n * `omissions` is not an array, or if any part of the spec is not one the\n * model defines ({@link InvalidInputError}).\n *\n * @category Chords\n */\nexport function assertChordSpec(spec: ChordSpec): ChordSpec {\n if (spec === null || typeof spec !== 'object') {\n throw new InvalidInputError(`chord spec must be an object; received ${describeRejected(spec)}`);\n }\n assertFiniteNumber(spec.rootPc, 'chord spec rootPc');\n if (spec.bassPc !== undefined) {\n assertFiniteNumber(spec.bassPc, 'chord spec bassPc');\n }\n assertPart(spec.base, Object.keys(BASE_TONES) as ChordBase[], 'chord spec base');\n if (spec.seventh !== undefined) {\n assertPart(\n spec.seventh,\n Object.keys(SEVENTH_SEMITONES) as ChordSeventh[],\n 'chord spec seventh',\n );\n }\n if (!Array.isArray(spec.alterations)) {\n throw new InvalidInputError('chord spec alterations must be an array');\n }\n for (const alteration of spec.alterations) {\n assertDegreeIn(alteration?.degree, ALTERABLE_DEGREES, 'chord spec alteration degree');\n assertDegreeIn(alteration.alter, ALTERATION_STEPS, 'chord spec alteration');\n }\n assertDegreeList(spec.additions, ADDABLE_DEGREES, 'chord spec additions');\n assertDegreeList(spec.omissions, OMITTABLE_DEGREES, 'chord spec omissions');\n return normalizeChordSpec(spec);\n}\n","import { InvalidInputError } from '../../core/errors/index.js';\nimport {\n diatonicLetterOf,\n noteToPitchClass,\n pitchClassOf as pitchClass,\n} from '../../core/pitch/index.js';\nimport {\n assertArray,\n assertDegree,\n assertFiniteNumber,\n assertInteger,\n assertOptions,\n assertRecord,\n describeRejected,\n} from '../../core/validation/index.js';\nimport {\n type ResolvedKey,\n resolveKey,\n type SpelledKeyLike,\n scaleTonesInDegreeOrder,\n} from '../scale/index.js';\nimport { spellChordFromRoot, spellPitchClass } from '../spelling/index.js';\nimport type { ChordQuality, ChordSpec } from './spec.js';\nimport {\n assertChordSpec,\n chordSpecForQuality,\n chordSpecFromIntervals,\n chordSpecIntervals,\n chordSpecQuality,\n QUALITY_INTERVALS,\n QUALITY_ORDER,\n} from './spec.js';\n\nexport type {\n Alteration,\n AlteredDegree,\n ChordBase,\n ChordQuality,\n ChordSeventh,\n ChordSpec,\n} from './spec.js';\nexport { chordSpecIntervals, chordSpecQuality } from './spec.js';\n\n/**\n * The basic role a chord tone plays: the degrees a chord's own template names.\n *\n * The shared vocabulary behind {@link chordToneRole}, the harmony layer's\n * `HarmonyRole`, and the voice analyser's labels, so the three cannot drift.\n *\n * @category Chords\n */\nexport type ChordToneRole = 'root' | 'third' | 'fifth' | 'sixth' | 'seventh';\n\n/**\n * A bare spelled pitch used as an enharmonic hint: a diatonic letter\n * (0..6 = C..B) plus a chromatic alteration (-2 double-flat .. +2 double-sharp).\n * Mirrors the letter/alter half of the pitch module's `Note` without an octave.\n *\n * @category Chords\n */\nexport type PitchSpelling = {\n letter: number;\n alter: number;\n};\n\n/**\n * A chord expressed as a root pitch class plus semitone offsets.\n *\n * `intervals` is what the chord sounds and `quality` is what it is called. The\n * two are not equally expressive: a chart writes any set of alterations over a\n * base, while {@link ChordQuality} is a closed list of names, so `quality` is a\n * *derived* label — the name that fits the chord exactly, or else the nearest\n * name inside it (see {@link chordSpecQuality}). Read the structure with\n * {@link chordSpecOf} rather than the name when the alterations matter; that is\n * also what {@link formatChordSymbol} does, so a chord no name covers still\n * writes itself out in full.\n *\n * `rootSpelling`/`bassSpelling` are optional enharmonic hints recorded by\n * parsers (e.g. `parseChordSymbol('Bb7')`) so formatters can reproduce the\n * original spelling instead of defaulting to sharps. Consumers may ignore\n * them; a hint is only trusted when its pitch class still matches the\n * corresponding `rootPc`/`bassPc`.\n *\n * `toneSpellings` is the same kind of hint for the tones themselves, one\n * spelling per entry in `intervals`. It exists for the chords whose letters do\n * not follow from stacking thirds: an augmented sixth is ten semitones like a\n * minor seventh but is five letters above its root rather than six, and no\n * {@link ChordQuality} can say so. Builders that know better attach it, and\n * {@link spellChordFromRoot} reads the letter distances back out of it; a hint\n * is trusted as a whole or not at all, and only while every spelling still\n * names the pitch class its interval names.\n *\n * @category Chords\n */\nexport type Chord = {\n rootPc: number;\n quality: ChordQuality;\n intervals: number[];\n bassPc?: number;\n rootSpelling?: PitchSpelling;\n bassSpelling?: PitchSpelling;\n toneSpellings?: PitchSpelling[];\n};\n\n/**\n * A chord positioned in time: a root pitch class and quality placed at a beat,\n * with optional harmonic-analysis hints. Serves as the neutral exchange record\n * between progression generators and timeline analysers.\n *\n * @category Composition\n */\nexport type ChordSpan = {\n rootPc: number;\n quality: ChordQuality;\n startBeat: number;\n bassPc?: number;\n /**\n * Semitone offsets from the root, for a chord the {@link ChordQuality} union\n * cannot name exactly. Omitted for a standard chord: the template is then\n * derived from `quality`, which is what every producer and consumer did\n * before the field existed. Read a span with {@link chordFromSpan} so either\n * case yields the same chord.\n */\n intervals?: number[];\n /** 1-based scale degree of the chord root, when known. */\n degree?: number;\n /** True when the chord is a secondary dominant tonicizing another degree. */\n secondaryDominant?: boolean;\n};\n\n/**\n * A chord occupying a half-open beat span `[startBeat, endBeat)`.\n *\n * The exchange record between anything that lays chords out in time — a\n * timeline analysis, a bass-line plan, an accompaniment generator — so those\n * surfaces share one definition instead of agreeing by coincidence.\n *\n * @category Composition\n */\nexport type ChordSegment = {\n startBeat: number;\n endBeat: number;\n chord: Chord;\n};\n\n/** Widest semitone offset a custom interval template may name: one MIDI range. */\nexport const MAX_CHORD_INTERVAL = 127;\n\n/**\n * All chord qualities the builder understands, in declaration order.\n *\n * The order is stable but carries no meaning: it is neither alphabetical nor\n * sorted by template size. {@link detectChord} uses it as its final tie-break,\n * so it is part of the observable behaviour and does not get reshuffled\n * casually — but do not read it as a ranking.\n *\n * @category Chords\n */\nexport function chordQualities(): ChordQuality[] {\n return [...QUALITY_ORDER];\n}\n\n/**\n * Reject a quality the chord tables do not name.\n *\n * The single check every way of building a chord runs, so a quality that no\n * table has a row for is refused where the chord is made rather than surfacing\n * as an undefined lookup somewhere downstream.\n *\n * Not part of the package's public surface.\n *\n * @param quality The quality name to check.\n * @returns The quality, unchanged.\n * @throws If the quality is not a known name ({@link InvalidInputError}).\n */\nexport function assertChordQuality(quality: ChordQuality): ChordQuality {\n if (typeof quality !== 'string' || !Object.hasOwn(QUALITY_INTERVALS, quality)) {\n throw new InvalidInputError(`Unknown chord quality: ${String(quality)}`);\n }\n return quality;\n}\n\n/**\n * Read a plain chord as one, before any field of it is.\n *\n * The entrances that take a {@link ChordLike} go through {@link toChordData},\n * which reads whatever form the caller held; the ones that take a plain\n * {@link Chord} skip that, and used to read a root pitch class straight off\n * whatever they were handed. The two fields every such entrance reads are\n * checked here, so a chord restored from a session without them is refused\n * rather than answered — a root of `null` reduces to pitch class 0, which is a\n * chord on C.\n *\n * Not part of the package's public surface.\n *\n * @param chord The chord to check.\n * @param name What the chord is, for the error message.\n * @returns The chord, unchanged.\n * @throws If it is not a record carrying a root pitch class and a known quality.\n */\nexport function assertChord(chord: Chord, name = 'chord'): Chord {\n const read = assertRecord<Chord>(chord, name);\n assertFiniteNumber(read.rootPc, `${name}.rootPc`);\n assertChordQuality(read.quality);\n return read;\n}\n\n/**\n * Build a chord rooted on a diatonic scale degree.\n *\n * The root pitch class is the degree's diatonic pitch class in `key`, spelled\n * the way the key writes it (a flat key names its degrees with flats); the\n * quality's interval template is attached unchanged. Degrees beyond the scale\n * length wrap around, so degree 8 of a heptatonic scale is its degree 1.\n *\n * @param degree 1-based scale degree of the chord root: 1 is the tonic.\n * @param ext Chord quality to apply.\n * @param key Key context supplying the diatonic root, as a key name, a\n * key/scale, or a `Key`.\n * @returns The constructed chord.\n * @throws If `degree` is not a positive integer, or `ext` is not a known\n * quality.\n *\n * @category Chords\n */\nexport function chordFromDegree(degree: number, ext: ChordQuality, key: SpelledKeyLike): Chord {\n // Degree 0 is rejected rather than wrapped onto the last degree: a caller\n // still counting from zero gets an error instead of a plausible chord.\n assertDegree(degree, 'chord degree');\n assertChordQuality(ext);\n const resolved = resolveKey(key);\n const tones = scaleTonesInDegreeOrder(resolved.scale);\n const length = tones.length;\n const index = length > 0 ? (degree - 1) % length : 0;\n const rootPc = tones[index] ?? pitchClass(resolved.scale.rootPc);\n return makeSpelledChord(rootPc, ext, { rootSpelling: keySpelling(rootPc, resolved) });\n}\n\n/**\n * Build a chord from an explicit root pitch class and quality.\n *\n * @param rootPc Root pitch class (0..11).\n * @param quality Chord quality supplying the interval template.\n * @param bassPc Optional slash-chord bass pitch class.\n * @returns The constructed chord.\n *\n * @example\n * ```ts\n * import { makeChord } from '@libraz/libcantus';\n * const cmaj7 = makeChord(0, 'maj7');\n * // { rootPc: 0, quality: 'maj7', intervals: [0, 4, 7, 11] }\n * ```\n *\n * @category Chords\n */\nexport function makeChord(rootPc: number, quality: ChordQuality, bassPc?: number): Chord {\n return makeSpelledChord(rootPc, quality, { bassPc });\n}\n\n/**\n * The slash bass and spellings a chord builder attaches beside the pitch\n * classes.\n *\n * `bassSpelling` may be left out when the bass is one of the chord's own tones:\n * it is then that tone's spelling over the given root spelling.\n */\nexport type SpelledChordParts = {\n bassPc?: number;\n rootSpelling?: PitchSpelling;\n bassSpelling?: PitchSpelling;\n toneSpellings?: PitchSpelling[];\n};\n\n/**\n * The one constructor behind every chord built from a quality.\n *\n * {@link makeChord} is this without spellings. A builder that knows how its\n * chord is written, from a key or a numeral, passes the letters here: a chord\n * built from a bare root pitch class forgets them, and a formatter then writes\n * the default name (Bb7 as A#7, the Neapolitan of Ab minor as A major).\n *\n * Not part of the package's public surface.\n *\n * @param rootPc Root pitch class.\n * @param quality Chord quality supplying the interval template.\n * @param parts The slash bass and the spellings to attach; each is copied.\n * @returns The constructed chord.\n */\nexport function makeSpelledChord(\n rootPc: number,\n quality: ChordQuality,\n parts: SpelledChordParts = {},\n): Chord {\n // A NaN root formats as 'C' and an unknown quality as 'Cundefined', so both\n // travel on into voicing, detection, and serialization as a chord that looks\n // real. They are rejected where the chord is built instead.\n assertFiniteNumber(rootPc, 'chord rootPc');\n if (parts.bassPc !== undefined) {\n assertFiniteNumber(parts.bassPc, 'chord bassPc');\n }\n assertChordQuality(quality);\n const chord: Chord = {\n rootPc: pitchClass(rootPc),\n quality,\n intervals: [...(QUALITY_INTERVALS[quality] ?? [])],\n };\n if (parts.bassPc !== undefined) {\n chord.bassPc = pitchClass(parts.bassPc);\n }\n if (parts.rootSpelling !== undefined) {\n chord.rootSpelling = { letter: parts.rootSpelling.letter, alter: parts.rootSpelling.alter };\n }\n if (parts.bassSpelling !== undefined) {\n chord.bassSpelling = { letter: parts.bassSpelling.letter, alter: parts.bassSpelling.alter };\n } else if (chord.rootSpelling !== undefined && chord.bassPc !== undefined) {\n const index = chord.intervals.findIndex(\n (interval) => pitchClass(chord.rootPc + interval) === chord.bassPc,\n );\n const tone = index >= 0 ? spellChordFromRoot(chord, chord.rootSpelling)[index] : undefined;\n if (tone !== undefined) {\n chord.bassSpelling = { letter: tone.letter, alter: tone.alter };\n }\n }\n if (parts.toneSpellings !== undefined) {\n chord.toneSpellings = parts.toneSpellings.map(({ letter, alter }) => ({ letter, alter }));\n }\n return chord;\n}\n\n/** How a key writes a pitch class, as a bare spelling. */\nexport function keySpelling(pc: number, key: ResolvedKey): PitchSpelling {\n const { letter, alter } = spellPitchClass(pc, key.tonic, key.scale);\n return { letter, alter };\n}\n\n/**\n * Build a chord from a {@link ChordSpec}.\n *\n * The structural way in, for the chords a quality name cannot reach: the tones\n * come from the spec, and `quality` is the name that fits it, or the nearest\n * name when none does. A chord built this way is an ordinary {@link Chord} —\n * every consumer reads its `intervals` as before, and\n * {@link formatChordSymbol} writes the alterations back out.\n *\n * @param spec The chord spec; its `rootPc`/`bassPc` become the chord's.\n * @returns The constructed chord.\n * @throws If any part of the spec is not one the model defines.\n *\n * @example\n * ```ts\n * import { chordFromSpec } from '@libraz/libcantus';\n * chordFromSpec({\n * rootPc: 0,\n * base: 'maj',\n * seventh: 'maj7',\n * alterations: [{ degree: 11, alter: 1 }],\n * additions: [],\n * omissions: [],\n * });\n * // { rootPc: 0, quality: 'maj7#11', intervals: [0, 4, 7, 11, 18] }\n * ```\n *\n * @category Chords\n */\nexport function chordFromSpec(spec: ChordSpec): Chord {\n const checked = assertChordSpec(spec);\n const chord: Chord = {\n rootPc: pitchClass(checked.rootPc),\n quality: chordSpecQuality(checked),\n intervals: chordSpecIntervals(checked),\n };\n if (checked.bassPc !== undefined) {\n chord.bassPc = pitchClass(checked.bassPc);\n }\n return chord;\n}\n\n/**\n * Read a chord structurally: which base, seventh, alterations and omissions it\n * is made of.\n *\n * The reading comes from the tones themselves, so it describes a chord whatever\n * built it — a parsed symbol, a detected pitch set, a span carrying a template\n * no name covers. Only a template with no structural reading at all falls back\n * to the one its `quality` names.\n *\n * @param chord The chord to read.\n * @returns The spec, carrying the chord's own root and slash bass.\n * @throws If the chord's template has no reading and its quality is unknown.\n *\n * @example\n * ```ts\n * import { chordSpecOf, parseChordSymbol } from '@libraz/libcantus';\n * chordSpecOf(parseChordSymbol('C7(b9,#11)')).alterations;\n * // [{ degree: 9, alter: -1 }, { degree: 11, alter: 1 }]\n * ```\n *\n * @category Chords\n */\nexport function chordSpecOf(chord: Chord): ChordSpec {\n assertRecord<Chord>(chord, 'chord');\n assertFiniteNumber(chord.rootPc, 'chord rootPc');\n const rootPc = pitchClass(chord.rootPc);\n const bassPc = chord.bassPc === undefined ? undefined : pitchClass(chord.bassPc);\n // A chord with no tones at all says nothing about its own structure, so the\n // name it carries is the only thing left to read it by.\n const intervals = chord.intervals ?? [];\n const read = intervals.length === 0 ? undefined : chordSpecFromIntervals(intervals);\n if (read === undefined) {\n return chordSpecForQuality(chord.quality, rootPc, bassPc);\n }\n read.rootPc = rootPc;\n if (bassPc !== undefined) {\n read.bassPc = bassPc;\n }\n return read;\n}\n\n/**\n * Validate a span's custom interval template and return a defensive copy.\n *\n * Such a template reaches the library without passing any builder, so it is\n * checked here: a fractional offset would place a chord tone between pitch\n * classes, and a hole in the array would read as an undefined chord tone in\n * every consumer downstream.\n */\nfunction copySpanIntervals(intervals: readonly number[]): number[] {\n const name = 'chord span intervals';\n if (!Array.isArray(intervals)) {\n throw new InvalidInputError(\n `${name} must be an array; received ${describeRejected(intervals)}`,\n );\n }\n if (intervals.length === 0) {\n throw new InvalidInputError(`${name} must name at least one interval`);\n }\n const copy: number[] = [];\n for (let index = 0; index < intervals.length; index += 1) {\n const interval = intervals[index];\n if (interval === undefined) {\n throw new InvalidInputError(\n `${name}[${index}] must be a semitone offset; received undefined`,\n );\n }\n copy.push(\n assertInteger(interval, `${name}[${index}]`, -MAX_CHORD_INTERVAL, MAX_CHORD_INTERVAL),\n );\n }\n return copy;\n}\n\n/** Whether a chord's template departs from the one its quality names. */\nfunction hasCustomIntervals(chord: Chord): boolean {\n return !matchesQualityIntervals(chord.quality, chord.intervals);\n}\n\n/**\n * Read a {@link ChordSpan} as a chord.\n *\n * The canonical way to cross from a span to a chord. A span carrying no\n * `intervals` yields exactly the chord {@link makeChord} builds from its root,\n * quality and bass; a span carrying an explicit template keeps it, so a color\n * tone the {@link ChordQuality} union cannot name survives the trip through a\n * progression, a timeline, or a generator. The template is copied, so the\n * chord and the span never share an array.\n *\n * @param span The span to read.\n * @returns The chord the span describes.\n * @throws If the span's root, bass or quality is invalid, or its `intervals`\n * are not a non-empty array of integer semitone offsets.\n *\n * @example\n * ```ts\n * import { chordFromSpan } from '@libraz/libcantus';\n * chordFromSpan({ rootPc: 0, quality: 'maj7', startBeat: 0 });\n * // { rootPc: 0, quality: 'maj7', intervals: [0, 4, 7, 11] }\n * chordFromSpan({ rootPc: 0, quality: 'maj7', startBeat: 0, intervals: [0, 4, 11, 18] });\n * // the same span with a fifth-less, #11 voicing the quality cannot name\n * ```\n *\n * @category Composition\n */\nexport function chordFromSpan(span: ChordSpan): Chord {\n assertRecord<ChordSpan>(span, 'chord span');\n const chord = makeSpelledChord(span.rootPc, span.quality, { bassPc: span.bassPc });\n if (span.intervals !== undefined) {\n chord.intervals = copySpanIntervals(span.intervals);\n }\n return chord;\n}\n\n/**\n * Place a chord at a beat as a {@link ChordSpan}.\n *\n * The interval template is recorded only when it departs from the one the\n * chord's quality names, so a span built from a standard chord carries the\n * same fields it always did while a custom template is not lost.\n *\n * @param chord The chord to place.\n * @param startBeat Beat the chord starts on.\n * @returns The span describing that chord at that beat.\n * @throws If `startBeat` is not finite, or the chord's quality is unknown.\n *\n * @example\n * ```ts\n * import { makeChord, spanFromChord } from '@libraz/libcantus';\n * spanFromChord(makeChord(0, 'maj7'), 4);\n * // { rootPc: 0, quality: 'maj7', startBeat: 4 } — no intervals for a standard chord\n * ```\n *\n * @category Composition\n */\nexport function spanFromChord(chord: Chord, startBeat: number): ChordSpan {\n assertFiniteNumber(startBeat, 'chord span startBeat');\n assertChord(chord);\n const span: ChordSpan = { rootPc: chord.rootPc, quality: chord.quality, startBeat };\n if (chord.bassPc !== undefined) {\n span.bassPc = chord.bassPc;\n }\n if (hasCustomIntervals(chord)) {\n span.intervals = [...chord.intervals];\n }\n return span;\n}\n\n/**\n * Transpose a chord by a number of semitones.\n *\n * The interval template is carried over untouched, so a chord whose intervals\n * were customised keeps them, and a slash bass moves with the root. Enharmonic\n * spelling hints are dropped rather than shifted: the letter a transposed root\n * should take depends on the destination key, which a semitone count does not\n * carry. Pass the result through {@link spellChord} to respell it.\n *\n * @param chord The chord to transpose.\n * @param semitones The signed semitone offset.\n * @returns The transposed chord.\n * @throws If `semitones` is not finite.\n *\n * @example\n * ```ts\n * import { formatChordSymbol, parseChordSymbol, transposeChord } from '@libraz/libcantus';\n * formatChordSymbol(transposeChord(parseChordSymbol('C/G'), 2)); // 'D/A'\n * ```\n *\n * @category Chords\n */\nexport function transposeChord(chord: Chord, semitones: number): Chord {\n assertChord(chord);\n assertFiniteNumber(semitones, 'semitones');\n const steps = Math.round(semitones);\n const moved: Chord = {\n rootPc: pitchClass(chord.rootPc + steps),\n quality: chord.quality,\n intervals: [...chord.intervals],\n };\n if (chord.bassPc !== undefined) {\n moved.bassPc = pitchClass(chord.bassPc + steps);\n }\n return moved;\n}\n\n/**\n * A chord's per-tone spelling hints, when they still describe its own tones.\n *\n * The hint is trusted as a whole or not at all: it must name one spelling per\n * interval, and each spelling must still sound the pitch class its interval\n * names. A hint that has fallen out of step with the template — one left behind\n * by a transposition that moved the intervals and not the letters — is dropped\n * rather than half-applied, the same discipline a stale `rootSpelling` gets.\n *\n * Not part of the package's public surface: the readers of the hint\n * ({@link spellChordFromRoot} and the class API) share this check so they\n * cannot disagree about which hints are usable.\n */\nexport function chordToneSpellings(chord: Chord): PitchSpelling[] | undefined {\n const hints = chord.toneSpellings;\n if (hints === undefined || hints.length !== chord.intervals.length) {\n return undefined;\n }\n const usable: PitchSpelling[] = [];\n for (let index = 0; index < hints.length; index += 1) {\n const hint = hints[index];\n const interval = chord.intervals[index];\n if (hint === undefined || interval === undefined) {\n return undefined;\n }\n if (noteToPitchClass(hint) !== pitchClass(chord.rootPc + interval)) {\n return undefined;\n }\n usable.push({ letter: diatonicLetterOf(hint.letter), alter: hint.alter });\n }\n return usable;\n}\n\n/**\n * The letter distance above the chord root each of a chord's own tone spellings\n * implies, or undefined when the chord names none this function can trust.\n *\n * This is the one route by which a chord can name a tone a stack of thirds\n * cannot: the augmented sixth over the lowered submediant is ten semitones like\n * a minor seventh, but five letters above its root rather than six, and no\n * {@link ChordQuality} can say so. Distances are read out of the hint rather\n * than its letters, and they are measured from the hint's own root tone, so the\n * tones follow whichever spelling of the root is in force: the German sixth\n * spells Ab C Eb F# from an Ab root and G# B# D# E## from a G# one.\n *\n * Not part of the package's public surface: it is the single owner of \"which\n * degree does this chord write this tone as\", so the speller, the tone-role\n * reader and the part-writing rules cannot disagree about it.\n */\nexport function chordToneLetterOffsets(chord: Chord): number[] | undefined {\n const hints = chordToneSpellings(chord);\n const rootHint = hints?.[chord.intervals.findIndex((interval) => pitchClass(interval) === 0)];\n if (hints === undefined || rootHint === undefined) {\n return undefined;\n }\n return hints.map((hint) => diatonicLetterOf(hint.letter - rootHint.letter));\n}\n\n/**\n * The role a letter distance above the root names, for the chords that carry\n * their own spelling.\n *\n * Only the distances a basic role is defined for appear: a tone written two\n * letters above the root is its third and one written five its sixth, whatever\n * the semitones would say on their own.\n */\nexport const ROLE_BY_LETTER_OFFSET: Record<number, ChordToneRole> = {\n 0: 'root',\n 2: 'third',\n 4: 'fifth',\n 5: 'sixth',\n 6: 'seventh',\n};\n\n/** The role a chord's own spelling gives a pitch class, when it names one. */\nfunction spelledToneRole(pitch: number, chord: Chord): ChordToneRole | null | undefined {\n const offsets = chordToneLetterOffsets(chord);\n if (offsets === undefined) {\n return undefined;\n }\n const index = chord.intervals.findIndex(\n (interval) => pitchClass(chord.rootPc + interval) === pitchClass(pitch),\n );\n const offset = offsets[index];\n return offset === undefined ? undefined : (ROLE_BY_LETTER_OFFSET[offset] ?? null);\n}\n\n/**\n * A chord's pitch classes as a twelve-bit mask, bit `n` for pitch class `n`.\n *\n * The same representation a scale is held in, and for the same reason: asking\n * whether a chord sounds a pitch is one shift and one and, on a number the\n * caller never has to allocate. The analysers ask it per note per beat, so the\n * set and the sorted array the enumeration hands out are built only where a\n * caller actually wants to walk the tones.\n *\n * @param chord The chord to read.\n * @param opts Set `includeBass: false` to omit a slash bass.\n * @returns The mask, in [0, 4095].\n */\nexport function chordPcMask(chord: Chord, opts: { includeBass?: boolean } = {}): number {\n assertRecord<Chord>(chord, 'chord');\n assertArray(chord.intervals, 'chord intervals');\n let mask = 0;\n for (const interval of chord.intervals) {\n mask |= 1 << pitchClass(chord.rootPc + interval);\n }\n if (opts.includeBass !== false && chord.bassPc !== undefined) {\n mask |= 1 << pitchClass(chord.bassPc);\n }\n return mask;\n}\n\n/** Whether a twelve-bit pitch-class mask carries a pitch class. */\nfunction hasPitchClass(mask: number, pc: number): boolean {\n return ((mask >> pc) & 1) === 1;\n}\n\n/**\n * A chord's intervals as a twelve-bit mask of interval classes above its root.\n *\n * The root's own frame rather than the sounding one: what a chord is made of is\n * a question about its template, so a slash bass is never part of the answer.\n */\nfunction chordIntervalMask(chord: Chord): number {\n let mask = 0;\n for (const interval of chord.intervals) {\n mask |= 1 << (((interval % 12) + 12) % 12);\n }\n return mask;\n}\n\n/**\n * Get the sorted, deduplicated pitch classes of a chord.\n *\n * A slash bass is one of them: `F/G` sounds a G, the voicers put it in the\n * bass, and a set that omitted it would not re-detect as the chord it came\n * from. Pass `includeBass: false` to enumerate the interval template alone.\n *\n * @param chord The chord to enumerate.\n * @param opts Set `includeBass: false` to omit a slash bass.\n * @returns The chord's pitch classes, sorted ascending in [0, 11].\n *\n * @example\n * ```ts\n * import { makeChord, chordPitchClasses } from '@libraz/libcantus';\n * chordPitchClasses(makeChord(0, 'maj7')); // [0, 4, 7, 11]\n * chordPitchClasses(makeChord(5, 'maj', 7)); // [0, 5, 7, 9] — F/G, G included\n * ```\n *\n * @category Chords\n */\nexport function chordPitchClasses(chord: Chord, opts: { includeBass?: boolean } = {}): number[] {\n const mask = chordPcMask(assertChord(chord), assertOptions(opts, 'opts'));\n const pcs: number[] = [];\n for (let pc = 0; pc < 12; pc += 1) {\n if (hasPitchClass(mask, pc)) {\n pcs.push(pc);\n }\n }\n return pcs;\n}\n\n/**\n * A pitch's interval class above the chord root, in [0, 11].\n *\n * The shared reading of \"where does this pitch sit in the chord\", used by the\n * safety evaluator and the voice analyser alike so they cannot disagree.\n *\n * @param pitch MIDI pitch or bare pitch class.\n * @param chord The chord providing the root reference.\n * @returns The interval class above the root.\n * @category Chords\n */\nexport function intervalAboveRoot(pitch: number, chord: Chord): number {\n return (((pitchClass(pitch) - pitchClass(assertChord(chord).rootPc)) % 12) + 12) % 12;\n}\n\n/**\n * Whether a pitch is one of a chord's tones, ignoring octave.\n *\n * @param pitch MIDI pitch or bare pitch class.\n * @param chord The chord, or null for no sounding harmony.\n * @returns True when the pitch class belongs to the chord.\n * @category Chords\n */\nexport function isChordMember(pitch: number, chord: Chord | null): boolean {\n return chord === null ? false : hasPitchClass(chordPcMask(chord), pitchClass(pitch));\n}\n\n/**\n * Determine a pitch's harmonic role within a chord.\n *\n * A chord carrying its own tone spellings is read by them, since the degree it\n * writes a tone as is the degree that tone plays: the ten semitones above the\n * root of a German sixth are written five letters up, so they are its sixth and\n * not a seventh owing a step downward. Everywhere else the role is derived from\n * the pitch's interval above the chord root, reduced modulo 12. A major sixth\n * (9) reads as a `sixth` for sixth chords but as a `seventh` for a\n * diminished-seventh chord; ninths and other tensions have no basic role and\n * return null.\n *\n * @param pitch MIDI pitch or bare pitch class.\n * @param chord The chord providing the root reference.\n * @returns The chord-tone role, or null if the pitch has no basic role.\n *\n * @category Chords\n */\nexport function chordToneRole(pitch: number, chord: Chord): ChordToneRole | null {\n assertRecord<Chord>(chord, 'chord');\n const spelled = spelledToneRole(pitch, chord);\n if (spelled !== undefined) {\n return spelled;\n }\n const interval = (pitchClass(pitch) - pitchClass(chord.rootPc) + 12) % 12;\n const tones = chordIntervalMask(chord);\n const sounds = (ic: number): boolean => hasPitchClass(tones, ic);\n if (interval === 0) {\n // A chord that leaves its root out does not sound one, so the pitch has no\n // role in it: every other branch below asks the same of its own degree.\n return sounds(0) ? 'root' : null;\n }\n if (interval === 3 || interval === 4) {\n return sounds(interval) && (interval !== 3 || !sounds(4)) ? 'third' : null;\n }\n if (interval === 7) {\n return sounds(7) ? 'fifth' : null;\n }\n if (interval === 6) {\n // A diminished fifth is the chord's fifth only when no perfect fifth is\n // present; alongside a perfect fifth it is a #11 tension, not a fifth.\n return sounds(6) && !sounds(7) ? 'fifth' : null;\n }\n if (interval === 8) {\n // An augmented fifth is the chord's fifth only without a perfect fifth;\n // alongside a perfect fifth it is a b13 tension, not a fifth.\n return sounds(8) && !sounds(7) ? 'fifth' : null;\n }\n if (interval === 9) {\n const hasHigherSeventh = sounds(10) || sounds(11);\n const isDiminishedSeventh = sounds(3) && sounds(6) && !hasHigherSeventh;\n if (sounds(9) && isDiminishedSeventh) {\n return 'seventh';\n }\n if (sounds(9) && !hasHigherSeventh) {\n return 'sixth';\n }\n return null;\n }\n if (interval === 10 || interval === 11) {\n return sounds(interval) ? 'seventh' : null;\n }\n return null;\n}\n\n/** The degree a chord tone names, once the chord has said which tone it is. */\nexport const DEGREE_BY_ROLE: Readonly<Record<ChordToneRole, number>> = {\n root: 1,\n third: 3,\n fifth: 5,\n sixth: 6,\n seventh: 7,\n};\n\n/**\n * The degree an interval class names when the chord's own template does not\n * claim it, indexed by interval class.\n *\n * A tone the chord does not read as one of its degrees is a tension, and the\n * degree a tension names is the one its letter would be written on: the six and\n * the eight beside a perfect fifth are the sharp eleventh and the flat\n * thirteenth, not a second fifth and a sixth.\n */\nexport const TENSION_DEGREE: readonly number[] = [1, 2, 2, 2, 3, 4, 4, 5, 6, 6, 7, 7];\n\n/**\n * Which chord degree one of a chord's own intervals spells.\n *\n * The chord decides, through the same reading every other layer asks — a flat\n * thirteenth over a chord that already sounds its fifth is a thirteenth, and a\n * flattened fifth over a chord that does not is the fifth. Answering from the\n * interval class alone folded 6, 7 and 8 onto the fifth, so a figure asking a\n * `7(b13)` for its thirteenth matched nothing, fell through to the key, and\n * played a tone the chord had already contradicted.\n */\nexport function degreeOfInterval(interval: number, chord: Chord): number | undefined {\n const role = chordToneRole(pitchClass(chord.rootPc + interval), chord);\n if (role !== null) {\n return DEGREE_BY_ROLE[role];\n }\n return TENSION_DEGREE[((interval % 12) + 12) % 12];\n}\n\n/**\n * What occupies a chord's third slot: the degree that says what the chord is.\n *\n * A discriminated union rather than a nullable interval, because the three\n * cases are not degrees of one thing. A chord states a third, or a suspension\n * stands in its place, or the slot is empty — and a reader that has to handle\n * the suspension differently from the third (the avoid-note rule, the voicing\n * lock) says so in the shape of the answer rather than in a comment beside it.\n */\nexport type ThirdSlot =\n /** The chord states its own third: 4 for a major, 3 for a minor. */\n | { readonly kind: 'third'; readonly interval: 3 | 4 }\n /** A suspension stands in the third's place: the fourth, or the second. */\n | { readonly kind: 'suspension'; readonly interval: 2 | 5 }\n /** The chord names no third and suspends nothing into its place. */\n | { readonly kind: 'none' };\n\n/**\n * Read what stands in a chord's third slot.\n *\n * One reading for every layer that needs it — the avoid notes a suspension\n * creates, the voicing lock that says a tone identifies the chord, the cadence\n * reader asking whether a chord carries a leading tone. Three of them had their\n * own, and the loosest of the three answered a suspension by whichever of the\n * fourth and the second happened to sound, which is not a question folded pitch\n * classes can answer: an eleventh chord's tensions fold onto 2 and 5 alike. So\n * the spelling decides — the suspension a `sus4` or a `sus2` names is its\n * fourth or its second, and an eleventh chord's is the eleventh its omitted\n * third gave way to.\n *\n * At most one interval class is ever reported, so a consumer reading the answer\n * as \"this is the tone that identifies the chord\" gets one tone rather than two.\n *\n * @param chord The chord to read.\n * @returns What occupies the slot.\n */\nexport function thirdSlotOf(chord: Chord): ThirdSlot {\n const tones = chordIntervalMask(chord);\n if (hasPitchClass(tones, 4)) {\n return { kind: 'third', interval: 4 };\n }\n if (hasPitchClass(tones, 3)) {\n return { kind: 'third', interval: 3 };\n }\n const spec = chordSpecOf(chord);\n if (spec.base === 'sus2') {\n return hasPitchClass(tones, 2) ? { kind: 'suspension', interval: 2 } : { kind: 'none' };\n }\n // An eleventh chord states its suspension by omitting the third rather than\n // by naming one, so it is read the same way a `sus4` is.\n if (spec.base === 'sus4' || spec.omissions.includes(3)) {\n return hasPitchClass(tones, 5) ? { kind: 'suspension', interval: 5 } : { kind: 'none' };\n }\n return { kind: 'none' };\n}\n\n/**\n * The thirds a suspension is standing in for, as pitch classes.\n *\n * Sounding either of them against the chord resolves the suspension and the\n * chord stops being the chord that was written: over a `Csus2` an E makes a\n * `Cadd9`. Both are named because the suspension does not say which third it\n * displaced — that is what suspending is — so both are what it must not meet.\n *\n * @param chord The chord to read.\n * @returns The two thirds, or an empty array when the chord states its own.\n */\nexport function displacedThirds(chord: Chord): number[] {\n return thirdSlotOf(chord).kind === 'suspension'\n ? [pitchClass(chord.rootPc + 3), pitchClass(chord.rootPc + 4)]\n : [];\n}\n\n/** Classify a stacked-thirds triad into a chord quality from its interval set. */\nfunction classifyTriad(thirdIc: number, fifthIc: number): ChordQuality {\n if (thirdIc === 4 && fifthIc === 8) {\n return 'aug';\n }\n if (thirdIc === 4 && fifthIc === 6) {\n return 'majb5';\n }\n if (thirdIc === 3 && fifthIc === 6) {\n return 'dim';\n }\n if (thirdIc === 3) {\n return 'min';\n }\n return 'maj';\n}\n\n/** Classify a stacked-thirds seventh chord into a chord quality. */\nfunction classifySeventh(thirdIc: number, fifthIc: number, seventhIc: number): ChordQuality {\n if (thirdIc === 3 && fifthIc === 6 && seventhIc === 9) {\n return 'dim7';\n }\n if (thirdIc === 3 && fifthIc === 6 && seventhIc === 10) {\n return 'm7b5';\n }\n if (thirdIc === 3 && seventhIc === 11) {\n return 'minMaj7';\n }\n if (thirdIc === 3) {\n return 'min7';\n }\n if (fifthIc === 8 && seventhIc === 10) {\n return 'aug7';\n }\n if (fifthIc === 8 && seventhIc === 11) {\n return 'augMaj7';\n }\n if (fifthIc === 6 && seventhIc === 10) {\n return '7b5';\n }\n if (seventhIc === 11) {\n return 'maj7';\n }\n return 'dom7';\n}\n\n/** Whether a classified quality describes precisely the stacked pitch classes. */\nfunction matchesQualityIntervals(quality: ChordQuality, intervals: readonly number[]): boolean {\n const template = QUALITY_INTERVALS[quality];\n return (\n template.length === intervals.length &&\n template.every((interval, index) => interval === intervals[index])\n );\n}\n\n/**\n * Stack scale thirds from a 1-based scale degree into a chord of `size` notes.\n * Degrees beyond the scale length wrap around.\n */\nfunction stackThirds(degree: number, resolved: ResolvedKey, size: 3 | 4): Chord {\n // Degree 0 is rejected rather than wrapped onto the last degree: a caller\n // still counting from zero gets an error instead of a plausible chord.\n assertDegree(degree, 'chord degree');\n const key = resolved.scale;\n const tones = scaleTonesInDegreeOrder(key);\n const length = tones.length;\n if (length !== 7) {\n throw new InvalidInputError(\n `diatonic chord stacking requires a heptatonic scale (received ${length} tones)`,\n );\n }\n const idx = (step: number) => (degree - 1 + step) % length;\n const rootPc = tones[idx(0)] ?? pitchClass(key.rootPc);\n const offsets = [0, 2, 4, 6].slice(0, size).map((step) => {\n const pc = tones[idx(step)] ?? rootPc;\n return (((pc - rootPc) % 12) + 12) % 12;\n });\n const thirdIc = offsets[1] ?? 4;\n const fifthIc = offsets[2] ?? 7;\n const quality =\n size === 3\n ? classifyTriad(thirdIc, fifthIc)\n : classifySeventh(thirdIc, fifthIc, offsets[3] ?? 11);\n if (!matchesQualityIntervals(quality, offsets)) {\n throw new InvalidInputError(\n `scale thirds at degree ${degree} produce unsupported intervals [${offsets.join(', ')}]`,\n );\n }\n return makeSpelledChord(rootPc, quality, { rootSpelling: keySpelling(rootPc, resolved) });\n}\n\n/**\n * Build the diatonic triad rooted on a scale degree by stacking scale thirds.\n *\n * Unlike {@link chordFromDegree}, the chord quality is derived from the scale\n * rather than supplied, so degrees yield their scale-correct triads (e.g. a\n * diminished triad on the leading tone of a major key).\n * The root is spelled the way the key writes it.\n *\n * @param degree 1-based scale degree of the chord root: 1 is the tonic.\n * @param key Key/scale context, as a key name, a key/scale, or a `Key`.\n * @returns The diatonic triad.\n * @throws If the scale is not heptatonic. Stacking thirds means skipping every\n * other degree, which only lands on a triad when there are seven of them, so\n * the pentatonic, blues, whole-tone, octatonic and chromatic scales have no\n * diatonic triad; use {@link chordFromDegree} with an explicit quality there.\n *\n * @example\n * ```ts\n * import { majorKey, diatonicTriad } from '@libraz/libcantus';\n * const tonic = diatonicTriad(1, majorKey(0)); // C major triad (degree 1 of C major)\n * ```\n *\n * @category Chords\n */\nexport function diatonicTriad(degree: number, key: SpelledKeyLike): Chord {\n return stackThirds(degree, resolveKey(key), 3);\n}\n\n/**\n * Build the diatonic seventh chord rooted on a scale degree.\n *\n * The root is spelled the way the key writes it.\n *\n * @param degree 1-based scale degree of the chord root: 1 is the tonic.\n * @param key Key/scale context, as a key name, a key/scale, or a `Key`.\n * @returns The diatonic seventh chord.\n * @throws If the scale is not heptatonic, for the reason given on\n * {@link diatonicTriad}.\n *\n * @category Chords\n */\nexport function diatonicSeventh(degree: number, key: SpelledKeyLike): Chord {\n return stackThirds(degree, resolveKey(key), 4);\n}\n","/**\n * Spelling: derive letter-name notes for scales and chords from a spelled tonic.\n *\n * The theory core works in pitch classes, which cannot choose between (say) a\n * G# and an Ab. Given a spelled tonic, this module assigns diatonic letters to\n * scale degrees and chord tones so a C major scale spells as C D E F G A B and\n * A harmonic minor spells its seventh as G#.\n *\n * Every entry point takes the spelled tonic *of the key it spells*: a tonic\n * sounding a pitch class other than `key.rootPc` is rejected rather than\n * spelled, because the letters counted from it name no reading of the key.\n */\n\nimport { InvalidInputError } from '../../core/errors/index.js';\nimport type { Note, NoteLike } from '../../core/pitch/index.js';\nimport {\n formatNote,\n diatonicLetterOf as mod7,\n pitchClassOf as mod12,\n naturalPitchClassOf as naturalPc,\n toNoteData,\n} from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport {\n assertArray,\n assertFiniteNumber,\n assertOptions,\n assertPitchClass,\n} from '../../core/validation/index.js';\nimport {\n assertChord,\n type Chord,\n type ChordToneRole,\n chordToneLetterOffsets,\n chordToneRole,\n} from '../chord/index.js';\nimport { isScaleTone, type KeyLike, scaleTonesInDegreeOrder, toKeyScale } from '../scale/index.js';\n\n/**\n * Conventional degree assignment for each chromatic offset above the tonic.\n *\n * The letter is derived from the tonic's letter plus `degreeOffset` (so the\n * spelling follows the key's diatonic letters), and the accidental is recomputed\n * for that letter. The table covers the whole octave, so a pitch class that is\n * not a degree of the scale — a chromatic tone in a heptatonic key, or any tone\n * outside a pentatonic or blues scale — still spells from the tonic's letter\n * rather than falling back to a fixed sharp table.\n *\n * Offsets follow the conventional ascending chromatic spelling: b2, M2, b3, M3,\n * P4, #4, P5, b6, #6, b7, #7. The raised sixth and leading tone keep sharp-side\n * minor keys spelling their raised degrees with the key-correct letter (e.g. F#\n * minor spells its leading tone E#, not F).\n */\nexport const CHROMATIC_SPELLING: Record<number, number> = {\n 1: 1, // b2\n 2: 1, // M2\n 3: 2, // b3\n 4: 2, // M3\n 5: 3, // P4\n 6: 3, // #4\n 7: 4, // P5\n 8: 5, // b6\n 9: 5, // #6 (raised sixth of a minor key)\n 10: 6, // b7\n 11: 6, // #7 (raised leading tone of a minor key)\n};\n\n/**\n * Flat-side table for scales that are not heptatonic.\n *\n * Identical to {@link CHROMATIC_SPELLING} except at the tritone, which a\n * flat-side scale spells as a diminished fifth rather than the augmented fourth\n * a lydian mode wants: C blues spells C Eb F Gb G Bb.\n */\nexport const CHROMATIC_SPELLING_FLAT: Record<number, number> = {\n ...CHROMATIC_SPELLING,\n 6: 4, // b5\n};\n\n/**\n * Sharp-side counterpart, used for scales that are not heptatonic and lean\n * sharp: C whole-tone spells C D E F# G# A#.\n */\nexport const CHROMATIC_SPELLING_SHARP: Record<number, number> = {\n 1: 0, // #1\n 2: 1, // M2\n 3: 1, // #2\n 4: 2, // M3\n 5: 3, // P4\n 6: 3, // #4\n 7: 4, // P5\n 8: 4, // #5\n 9: 5, // M6\n 10: 5, // #6\n 11: 6, // M7\n};\n\n/** The pitch class a spelled note sounds. */\nfunction notePitchClass(note: Note): number {\n return mod12(naturalPc(note.letter) + note.alter);\n}\n\n/**\n * Reject a tonic that does not spell the key's own root.\n *\n * Every letter these functions assign is counted from the tonic's letter, so a\n * tonic naming a different pitch class than `key.rootPc` does not produce\n * another spelling of the same scale: it produces letters no reading of the key\n * supports, and a heptatonic scale that names one letter twice. The class API\n * rejects the pair in its `Key` constructor, so the functions that take the two\n * apart check it here and both surfaces answer the same way.\n *\n * Not part of the package's public surface: it guards the entry points rather\n * than being one.\n *\n * @param tonic The spelled tonic.\n * @param key The key/scale the tonic is meant to spell.\n * @param source The name of the function that received the pair.\n * @throws If the tonic sounds a pitch class other than the key's root.\n */\nexport function assertTonicOf(tonic: Note, key: KeyScale, source: string): void {\n const tonicPc = notePitchClass(tonic);\n const rootPc = mod12(key.rootPc);\n if (tonicPc !== rootPc) {\n const name = formatNote({ letter: mod7(tonic.letter), alter: tonic.alter });\n throw new InvalidInputError(\n `${source}: tonic ${name} sounds pitch class ${tonicPc}, not the key root ${rootPc}; ` +\n 'pass a tonic that spells the key root, or take one from spelledKeyOf',\n );\n }\n}\n\n/** Shortest signed alteration (in [-6, 6]) taking a letter's natural pc to `pc`. */\nfunction alterFor(letter: number, pc: number): number {\n let d = mod12(pc - naturalPc(letter));\n if (d > 6) {\n d -= 12;\n }\n return d;\n}\n\n/** Spell `pc` on the letter `degreeOffset` steps above the tonic's letter. */\nfunction letterFor(tonic: Note, degreeOffset: number | undefined, pc: number): Note | undefined {\n if (degreeOffset === undefined) {\n return undefined;\n }\n const letter = mod7(tonic.letter + degreeOffset);\n return { letter, alter: alterFor(letter, pc) };\n}\n\n/** The lighter-accidental of two candidate spellings; ties go to `preferFlat`. */\nfunction pickSpelling(\n flat: Note | undefined,\n sharp: Note | undefined,\n preferFlat: boolean,\n): Note | undefined {\n if (flat === undefined || sharp === undefined) {\n return flat ?? sharp;\n }\n if (Math.abs(flat.alter) !== Math.abs(sharp.alter)) {\n return Math.abs(flat.alter) < Math.abs(sharp.alter) ? flat : sharp;\n }\n return preferFlat ? flat : sharp;\n}\n\n/**\n * Keep a conventional chromatic degree unless its key-letter spelling costs\n * two or more accidentals than the enharmonic alternative. This preserves Db\n * for C-major bII while avoiding Bbb in Ab major, where A is more readable.\n */\nfunction capChromaticAccidentals(preferred: Note, alternative: Note | undefined): Note {\n return alternative !== undefined && Math.abs(preferred.alter) - Math.abs(alternative.alter) >= 2\n ? alternative\n : preferred;\n}\n\n/**\n * How many letters a spelling has to share out: one per diatonic letter name.\n *\n * A scale with more tones than this has to write one letter twice, and a scale\n * with more tones than one letter over — the chromatic set — is spelled tone by\n * tone instead.\n */\nexport const DIATONIC_LETTER_COUNT = 7;\n\n/**\n * The most tones a scale may have and still be spelled jointly: one letter over\n * the seven, which the eight-note scales write twice.\n */\nexport const MAX_JOINT_TONE_COUNT = DIATONIC_LETTER_COUNT + 1;\n\n/** Semitones above its tonic each degree of the major scale sounds. */\nexport const MAJOR_SEMITONES = [0, 2, 4, 5, 7, 9, 11];\n\n/** The degrees whose plain readings are major or minor rather than perfect. */\nexport const IMPERFECT_DEGREE_LIST: readonly number[] = [1, 2, 5, 6];\n\n/** {@link IMPERFECT_DEGREE_LIST} as a set. */\nconst IMPERFECT_DEGREES = new Set(IMPERFECT_DEGREE_LIST);\n\n/**\n * The offsets above the tonic of every diatonic mode, mode by mode and in\n * degree order: rotating the major scale onto each of its own degrees.\n *\n * The readings a gapped scale inherits when its tones all fit one of them. A\n * pentatonic has no key signature of its own, but a minor pentatonic is a\n * subset of the dorian, phrygian and aeolian modes, and all three agree that\n * the tone three semitones above the tonic is its third.\n */\nexport const DIATONIC_MODE_OFFSETS: number[][] = MAJOR_SEMITONES.map((tonic, mode) =>\n MAJOR_SEMITONES.map((_, degree) => mod12((MAJOR_SEMITONES[(mode + degree) % 7] ?? 0) - tonic)),\n);\n\n/**\n * How far a tone's reading strays from a plain degree above the tonic: zero for\n * a major, minor or perfect interval, one for an augmented or diminished one,\n * two for anything wider.\n *\n * The measure of a reading that a scale of eight tones is judged by. Such a\n * scale cannot spell plainly throughout — some letter carries two of its tones\n * — and this is what decides where that pressure lands: the seventh of a\n * dominant-sounding octatonic or bebop scale stays a seventh (Bb over C) rather\n * than becoming the augmented sixth (A#) that leaves the B unused.\n */\nfunction degreeStrain(tonic: Note, note: Note): number {\n const letterOffset = mod7(note.letter - tonic.letter);\n const offset = mod12(notePitchClass(note) - notePitchClass(tonic));\n let deviation = mod12(offset - (MAJOR_SEMITONES[letterOffset] ?? 0));\n if (deviation > 6) {\n deviation -= 12;\n }\n // A major or minor reading is plain for the degrees that take them; the\n // perfect degrees are plain only when they are perfect.\n const strain =\n IMPERFECT_DEGREES.has(letterOffset) && deviation < 0\n ? Math.abs(deviation + 1)\n : Math.abs(deviation);\n return Math.min(strain, 2);\n}\n\n/** Whether the key's scale is a seven-note (heptatonic) scale. */\nfunction isHeptatonic(key: KeyScale): boolean {\n return scaleTonesInDegreeOrder(key).length === DIATONIC_LETTER_COUNT;\n}\n\n/** Whether the scale has a minor third and no major third — it leans flat. */\nfunction hasMinorThird(key: KeyScale): boolean {\n const tones = scaleTonesInDegreeOrder(key);\n const root = mod12(key.rootPc);\n return tones.includes(mod12(root + 3)) && !tones.includes(mod12(root + 4));\n}\n\n/** Which way a scale leans: a flat tonic and a minor third both mean flats. */\nfunction leansFlat(tonic: Note, key: KeyScale): boolean {\n return tonic.alter < 0 || hasMinorThird(key);\n}\n\n/**\n * The conventional spellings of the tone `offset` semitones above the tonic,\n * the side the scale leans towards first.\n *\n * The two chromatic tables name at most two letters for an offset — the flat\n * side's and the sharp side's — and an offset both tables agree on has only the\n * one. Nothing outside them is offered, so a tone is never pushed onto a letter\n * no reading of the interval supports.\n */\nfunction gappedCandidates(tonic: Note, offset: number, pc: number, preferFlat: boolean): Note[] {\n const flat = letterFor(tonic, CHROMATIC_SPELLING_FLAT[offset], pc);\n const sharp = letterFor(tonic, CHROMATIC_SPELLING_SHARP[offset], pc);\n const preferred = pickSpelling(flat, sharp, preferFlat);\n if (preferred === undefined) {\n return [];\n }\n const alternative = preferred === flat ? sharp : flat;\n return alternative === undefined || alternative.letter === preferred.letter\n ? [preferred]\n : [preferred, alternative];\n}\n\n/**\n * Spell the tones of a scale that is a subset of a diatonic mode, by the\n * degrees that mode gives them, or undefined when no mode holds them all.\n *\n * A gapped scale carries no signature of its own, but one whose tones all sit\n * inside a mode inherits that mode's letters, and inherits them whatever they\n * cost: the minor third of Ab minor pentatonic is the Cb its parent modes write,\n * never the B that would read as an augmented second. Accidental count only\n * chooses between modes that disagree — the tritone is a raised fourth in one\n * and a lowered fifth in another — and never between a degree and a reading\n * that is no degree at all.\n *\n * @param tonic The spelled tonic, whose letter the degrees are counted from.\n * @param pcs The pitch classes to spell, tonic excluded.\n * @param preferFlat Which side breaks a tie between two equally light modes.\n * @returns One note per entry of `pcs`, or undefined when no mode contains them.\n */\nfunction spellByParentMode(tonic: Note, pcs: number[], preferFlat: boolean): Note[] | undefined {\n const tonicPc = notePitchClass(tonic);\n let best: Note[] | undefined;\n let bestAlter = 0;\n let bestSigned = 0;\n for (const offsets of DIATONIC_MODE_OFFSETS) {\n const notes: Note[] = [];\n let alter = 0;\n let signed = 0;\n for (const pc of pcs) {\n const degree = offsets.indexOf(mod12(pc - tonicPc));\n const note = letterFor(tonic, degree < 0 ? undefined : degree, pc);\n if (note === undefined) {\n break;\n }\n notes.push(note);\n alter += Math.abs(note.alter);\n signed += note.alter;\n }\n if (notes.length !== pcs.length) {\n continue;\n }\n const evenlyLight =\n alter === bestAlter && (preferFlat ? signed < bestSigned : signed > bestSigned);\n const lighter = alter < bestAlter || evenlyLight;\n if (best === undefined || lighter) {\n best = notes;\n bestAlter = alter;\n bestSigned = signed;\n }\n }\n return best;\n}\n\n/**\n * Choose one candidate per tone so that the letters run out no faster than the\n * scale allows, or undefined when no such choice exists.\n *\n * `reserved` is the letter the tonic has already taken, and it is never written\n * twice: the tonic is fixed by the caller, so a scale that has to double a\n * letter doubles one of its own. `duplicates` is how many other letters may\n * carry two tones — none for a scale that fits the seven letters, one for a\n * scale of eight tones, which cannot fit however it is read.\n *\n * Every tone offers at most two candidates, so the whole space is small enough\n * to walk. A scale that fits is read as lightly as it can be: fewest accidentals\n * overall, and on a tie the one that keeps most tones on the side the scale\n * leans. A scale that does not fit is read by how its tones sound as degrees\n * first ({@link degreeStrain}), since the doubling has to fall somewhere and it\n * belongs where the scale is genuinely chromatic rather than on a degree that\n * reads plainly.\n *\n * Because each tone's leading candidate is already its lightest, the\n * tone-by-tone spelling is itself the winner whenever its letters happen to be\n * distinct, which is what keeps a scale of whole tones or wider spelled exactly\n * as it always reads.\n */\nfunction assignDistinctLetters(\n candidates: Note[][],\n tonic: Note,\n duplicates: number,\n): Note[] | undefined {\n const reserved = 1 << mod7(tonic.letter);\n let best: Note[] | undefined;\n let bestStrain = 0;\n let bestAlter = 0;\n let bestDeviations = 0;\n for (let choice = 0; choice < 1 << candidates.length; choice += 1) {\n const picked: Note[] = [];\n let letters = reserved;\n let spare = duplicates;\n let strain = 0;\n let alter = 0;\n let deviations = 0;\n for (const [index, options] of candidates.entries()) {\n const preferred = options[0];\n const note = options[(choice >> index) & 1] ?? preferred;\n if (note === undefined) {\n break;\n }\n const taken = (letters & (1 << note.letter)) !== 0;\n if (taken && (spare === 0 || (reserved & (1 << note.letter)) !== 0)) {\n break;\n }\n spare -= taken ? 1 : 0;\n letters |= 1 << note.letter;\n strain += degreeStrain(tonic, note);\n alter += Math.abs(note.alter);\n deviations += note === preferred ? 0 : 1;\n picked.push(note);\n }\n if (picked.length !== candidates.length) {\n continue;\n }\n const better =\n duplicates > 0 && strain !== bestStrain\n ? strain < bestStrain\n : alter !== bestAlter\n ? alter < bestAlter\n : deviations < bestDeviations;\n if (best === undefined || better) {\n best = picked;\n bestStrain = strain;\n bestAlter = alter;\n bestDeviations = deviations;\n }\n }\n return best;\n}\n\n/**\n * Spell every tone of a scale that is not heptatonic, one letter per tone.\n *\n * A heptatonic key spells by degree, which hands every tone its own letter for\n * free. A gapped scale has no such rule, so its tones are spelled together\n * rather than one at a time: the semitone above the tonic of a hemitonic\n * pentatonic is a flattened second (C miyako-bushi spells C Db F G Ab) because\n * the tonic already holds the C, not because a table said so.\n *\n * A scale whose tones all fit a diatonic mode is read by that mode's degrees,\n * which is the reading the scale would have if it were written in a key: Ab\n * minor pentatonic spells its third Cb, as Ab minor does, rather than taking the\n * lighter B that would read as an augmented second. Only a scale no mode holds\n * — the whole-tone, blues, octatonic and chromatic sets, which sound intervals\n * no degree reading covers — is left to the conventional tables.\n *\n * The answer is undefined — and the caller falls back to spelling tone by tone\n * — for the chromatic set, which has more tones than one letter over the seven,\n * and when the tones cannot be told apart by letter at all: the blues scale\n * sounds both the fifth and the flattened fifth against a fourth that fixes the\n * letter below them, so one letter is bound to be used twice however the scale\n * is read. A scale of eight tones is spelled here even so, with the one letter\n * it has to write twice placed where the reading strains least.\n *\n * @returns A spelling per scale pitch class, or undefined if there is no\n * reading within the letters the scale is allowed.\n */\nfunction spellGappedScale(tonic: Note, key: KeyScale): Map<number, Note> | undefined {\n // The joint reading is a property of the scale, and it is asked for once per\n // pitch class of that scale — by `spellScale`, by a key's note names, by\n // every line spelled in it. Walking the whole candidate space again for each\n // of them costs the scale over and over to answer the same question, so the\n // reading a scale gets is remembered under the tonic and the scale it was\n // read for, which is everything it depends on.\n const cacheKey = `${mod7(tonic.letter)}:${tonic.alter}:${key.rootPc}:${key.modeMask12}`;\n const cached = JOINT_SPELLINGS.get(cacheKey);\n if (cached !== undefined) {\n return cached.spellings;\n }\n const answer = readGappedScale(tonic, key);\n // A caller may name any mask, so the store is capped rather than left to grow\n // with the scales one process happens to spell. Dropping it whole is what a\n // cap costs here: nothing below depends on an entry surviving.\n if (JOINT_SPELLINGS.size >= MAX_REMEMBERED_SCALES) {\n JOINT_SPELLINGS.clear();\n }\n JOINT_SPELLINGS.set(cacheKey, { spellings: answer });\n return answer;\n}\n\n/**\n * How many joint scale readings are remembered at once.\n *\n * Twelve tonics against the scales a piece is written in, several times over —\n * far more than any one analysis asks for, and small enough that a process\n * spelling arbitrary masks never holds much.\n */\nconst MAX_REMEMBERED_SCALES = 512;\n\n/** Joint readings already worked out, by tonic and scale. */\nconst JOINT_SPELLINGS = new Map<string, { spellings: Map<number, Note> | undefined }>();\n\n/** Work out the joint reading of a scale, without consulting the store. */\nfunction readGappedScale(tonic: Note, key: KeyScale): Map<number, Note> | undefined {\n const tones = scaleTonesInDegreeOrder(key);\n if (tones.length > MAX_JOINT_TONE_COUNT) {\n return undefined;\n }\n const tonicPc = mod12(naturalPc(tonic.letter) + tonic.alter);\n const tonicNote: Note = { letter: mod7(tonic.letter), alter: tonic.alter };\n const preferFlat = leansFlat(tonic, key);\n const rest = tones.filter((pc) => mod12(pc - tonicPc) !== 0);\n const candidates = rest.map((pc) => gappedCandidates(tonic, mod12(pc - tonicPc), pc, preferFlat));\n const chosen =\n spellByParentMode(tonicNote, rest, preferFlat) ??\n // A scale that can hold a letter per tone does; only one that cannot writes\n // a letter twice, and then where the doubling falls is what is chosen.\n assignDistinctLetters(candidates, tonicNote, 0) ??\n assignDistinctLetters(candidates, tonicNote, 1);\n if (chosen === undefined) {\n return undefined;\n }\n const spellings = new Map<number, Note>([[tonicPc, tonicNote]]);\n for (const [index, pc] of rest.entries()) {\n const note = chosen[index];\n if (note !== undefined) {\n spellings.set(mod12(pc), note);\n }\n }\n return spellings;\n}\n\n/**\n * Spell a single pitch class from the key alone, with no surrounding context.\n *\n * The tonic always spells as itself. In a heptatonic key each scale degree takes\n * the next letter above the tonic, so the scale spells with one letter per\n * degree. A gapped scale reaches the same one-letter-per-tone spelling through\n * {@link spellGappedScale} wherever its pitch set allows one. Every other pitch\n * class — a chromatic tone, or a tone of a scale whose letters cannot all\n * differ — takes the conventional interval spelling above the tonic's letter,\n * so a flat-side key keeps flat-side names.\n */\nfunction spellByKey(pc: number, tonic: Note, key: KeyScale): Note {\n const tonicPc = mod12(naturalPc(tonic.letter) + tonic.alter);\n const offset = mod12(pc - tonicPc);\n\n // The tonic spells as itself, whatever the scale: a caller that asked for Eb\n // major pentatonic must not be handed back a D#.\n if (offset === 0) {\n return { letter: mod7(tonic.letter), alter: tonic.alter };\n }\n\n const heptatonic = isHeptatonic(key);\n if (heptatonic) {\n const degree = scaleTonesInDegreeOrder(key).indexOf(mod12(pc));\n if (degree >= 0) {\n const letter = mod7(tonic.letter + degree);\n return { letter, alter: alterFor(letter, pc) };\n }\n const degreeOffset = CHROMATIC_SPELLING[offset];\n const preferred = letterFor(tonic, degreeOffset, pc);\n if (preferred !== undefined) {\n const flat = letterFor(tonic, CHROMATIC_SPELLING_FLAT[offset], pc);\n const sharp = letterFor(tonic, CHROMATIC_SPELLING_SHARP[offset], pc);\n // The conventional heptatonic degree is usually the flat-side candidate;\n // use the other side only when it avoids a double (or worse) accidental.\n const alternative =\n flat?.letter === preferred.letter\n ? sharp\n : sharp?.letter === preferred.letter\n ? flat\n : undefined;\n return capChromaticAccidentals(preferred, alternative);\n }\n } else {\n // A scale that is not heptatonic has no letter-per-degree spelling to\n // follow, so its own tones are spelled together, one letter each.\n if (isScaleTone(pc, key)) {\n const assigned = spellGappedScale(tonic, key)?.get(mod12(pc));\n if (assigned !== undefined) {\n // A copy: the reading is remembered for the scale, and every caller of\n // this hands back a note of its own to write to.\n return { letter: assigned.letter, alter: assigned.alter };\n }\n }\n // Anything that joint spelling does not reach — a chromatic tone, or a\n // scale whose tones cannot each hold a letter — takes whichever\n // conventional letter needs the smaller accidental. Ties go to the side the\n // scale itself leans: a flat tonic or a minor third both mean flats (C\n // blues spells Gb and Bb, not F# and A#).\n const [preferred] = gappedCandidates(tonic, offset, pc, leansFlat(tonic, key));\n if (preferred !== undefined) {\n return preferred;\n }\n }\n\n // Unreachable for a pitch class, since both tables cover offsets 1..11 and\n // offset 0 returns above; kept so a non-integral input still names something.\n const belowLetter = mod7(LETTER_BELOW_PITCH_CLASS[mod12(pc)] ?? 0);\n return { letter: belowLetter, alter: alterFor(belowLetter, pc) };\n}\n\n/** The natural letter at or below each pitch class, the fallback of {@link spellByKey}. */\nexport const LETTER_BELOW_PITCH_CLASS: readonly number[] = [0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6];\n\n/**\n * The musical evidence around a pitch, used to refine an enharmonic choice the\n * key alone cannot make.\n *\n * Every field is optional and every field is only ever advisory: a context that\n * says nothing about the pitch leaves its key spelling untouched.\n *\n * @category Pitch & Intervals\n */\nexport type SpellingContext = {\n /**\n * The chord sounding under the pitch, when the caller holds it.\n *\n * A chord names its own tones better than its root alone can: it carries the\n * degree it writes each tone as, so a pitch that is one of its tones is\n * spelled exactly as {@link spellChord} spells it. Supersedes `chordRoot`,\n * which stays for callers holding nothing but a root pitch class.\n */\n chord?: Chord;\n /** Root pitch class of the chord sounding under the pitch, when known. */\n chordRoot?: number;\n /** The pitch immediately before this one (MIDI pitch or bare pitch class). */\n previous?: number;\n /** The pitch immediately after this one (MIDI pitch or bare pitch class). */\n next?: number;\n};\n\n/**\n * Letter distance above a chord root for the chord-tone intervals that spell\n * unambiguously: root, minor third, major third, perfect fifth, minor seventh,\n * major seventh.\n *\n * Both thirds sit two letters above the root and both sevenths six, which is\n * what makes the third of a D7 in C major an F# rather than a Gb. The intervals\n * left out are the ones a chord tone does not fix — the tritone, the augmented\n * fifth and the tensions all have two defensible spellings, so they are left to\n * the melodic rules and, failing those, to the key.\n *\n * A distance on its own cannot say which degree a chord writes a tone as, which\n * is why the ambiguous distances are absent rather than guessed: nine semitones\n * are the sixth of a sixth chord and the diminished seventh of a\n * diminished-seventh chord, and the table sees the same number in both. So this\n * is the last resort of {@link chordToneLetterStep} — reached for a caller\n * holding nothing but a root pitch class, and for a tone the sounding chord\n * gives no degree of its own.\n */\nexport const CHORD_TONE_LETTER_STEPS: Record<number, number> = {\n 0: 0, // root\n 3: 2, // minor third\n 4: 2, // major third\n 7: 4, // perfect fifth\n 10: 6, // minor seventh\n 11: 6, // major seventh\n};\n\n/**\n * The letter distance above the root each chord-tone role is written at.\n *\n * The inverse of the reading {@link chordToneRole} performs: a tone the chord\n * calls its seventh is written six letters above the root whether it spans ten\n * semitones or nine, so a diminished seventh over C is a B double-flat and never\n * the A its nine semitones would suggest on their own.\n */\nexport const LETTER_STEP_BY_ROLE: Record<ChordToneRole, number> = {\n root: 0,\n third: 2,\n fifth: 4,\n sixth: 5,\n seventh: 6,\n};\n\n/** Widest alteration a context-derived spelling may carry: a double accidental. */\nexport const MAX_CONTEXT_ALTER = 2;\n\n/**\n * The letter distance above its root a sounding chord gives a pitch class, or\n * undefined when it gives none.\n *\n * The chord is asked twice before any distance is. Its own spelling answers\n * first and answers alone where it answers at all, so the tone that is a sixth\n * to the chord is a sixth here too: the German sixth of C major writes F#, five\n * letters above its Ab, and never the Gb the ten semitones would otherwise\n * imply. A chord that names no spelling of its own is asked for the degree its\n * structure gives the tone, which is the reading that separates the two chords\n * that sound nine semitones above their root — the added sixth is a sixth and\n * the diminished seventh a seventh, so a Cdim7 writes Bbb and a C6 writes A.\n *\n * Only a tone no chord names a degree for falls through to\n * {@link CHORD_TONE_LETTER_STEPS}, which is where a caller holding a bare root\n * pitch class starts and where a tension over a chord ends up.\n */\nfunction chordToneLetterStep(\n pc: number,\n rootPc: number,\n chord: Chord | undefined,\n): number | undefined {\n if (chord !== undefined) {\n const offsets = chordToneLetterOffsets(chord);\n const index = chord.intervals.findIndex(\n (interval) => mod12(chord.rootPc + interval) === mod12(pc),\n );\n const offset = offsets?.[index];\n if (offset !== undefined) {\n return offset;\n }\n const role = chordToneRole(pc, chord);\n if (role !== null) {\n return LETTER_STEP_BY_ROLE[role];\n }\n }\n return CHORD_TONE_LETTER_STEPS[mod12(pc - rootPc)];\n}\n\n/**\n * Spell `pc` as a chord tone, on the letter its degree above the root implies.\n *\n * Distinct from `chordToneSpellings`, which validates the spellings a chord\n * carries: this writes one pitch, reading those spellings when the chord has\n * them and falling back to the degree table when it does not.\n */\nfunction spellChordTone(\n pc: number,\n context: SpellingContext,\n tonic: Note,\n key: KeyScale,\n): Note | undefined {\n const { chord } = context;\n const chordRoot = chord?.rootPc ?? context.chordRoot;\n if (chordRoot === undefined) {\n return undefined;\n }\n const root =\n chord === undefined\n ? spellByKey(mod12(chordRoot), tonic, key)\n : chordRootSpelling(chord, tonic, key);\n return letterFor(root, chordToneLetterStep(pc, chordRoot, chord), pc);\n}\n\n/**\n * Spell `pc` relative to a neighbouring pitch, when that neighbour lies `gap`\n * semitones away from it.\n *\n * The neighbour is spelled by the key alone — the pitch being spelled is the\n * ambiguous one, so letting it influence its own evidence would be circular —\n * and `pc` then takes the letter `letterStep` steps from it. A `letterStep` of\n * zero keeps the neighbour's own letter, which spells `pc` as that note\n * inflected by an accidental. Neighbours compare as pitch classes, so a MIDI\n * pitch and a bare pitch class both work.\n */\nfunction neighbourSpelling(\n neighbour: number | undefined,\n gap: number,\n letterStep: number,\n pc: number,\n tonic: Note,\n key: KeyScale,\n): Note | undefined {\n if (neighbour === undefined || mod12(neighbour - pc) !== mod12(gap)) {\n return undefined;\n }\n return letterFor(spellByKey(mod12(neighbour), tonic, key), letterStep, pc);\n}\n\n/**\n * Refine a key spelling with the surrounding evidence, in rule priority order.\n *\n * A scale tone and the tonic itself are never touched: the key already spells\n * them, and a context must not turn the third of the scale into something else.\n * For anything chromatic the rules are tried in order and the first spelling\n * that stays within a double accidental wins; when none does, the key spelling\n * stands.\n */\nfunction refineByContext(\n pc: number,\n tonic: Note,\n key: KeyScale,\n context: SpellingContext,\n keySpelling: Note,\n): Note {\n const tonicPc = mod12(naturalPc(tonic.letter) + tonic.alter);\n if (mod12(pc) === tonicPc || isScaleTone(pc, key)) {\n return keySpelling;\n }\n const { previous, next } = context;\n const candidates = [\n // The sounding chord names the pitch by the degree it writes it as.\n spellChordTone(pc, context, tonic, key),\n // A semitone below what follows: the pitch is its leading tone, a letter down.\n neighbourSpelling(next, 1, -1, pc, tonic, key),\n // A semitone above what preceded: the pitch is that note inflected upward,\n // so it keeps its letter and a chromatic ascent spells with sharps.\n neighbourSpelling(previous, -1, 0, pc, tonic, key),\n // A semitone above what follows: the pitch leans on it from a letter up,\n // so a falling line spells with flats.\n neighbourSpelling(next, -1, 1, pc, tonic, key),\n // Failing that, a semitone below what preceded: that note inflected downward,\n // again on its own letter.\n neighbourSpelling(previous, 1, 0, pc, tonic, key),\n ];\n for (const candidate of candidates) {\n if (candidate !== undefined && Math.abs(candidate.alter) <= MAX_CONTEXT_ALTER) {\n return candidate;\n }\n }\n return keySpelling;\n}\n\n/**\n * Spell a single pitch class relative to a spelled tonic and key, optionally\n * refined by the music around it.\n *\n * Without a context the key decides alone: the tonic spells as itself, each\n * degree of a heptatonic key takes the next letter above the tonic, a gapped\n * scale gives each of its own tones a letter wherever its pitch set allows one,\n * and every other pitch class takes the conventional interval spelling above\n * the tonic's letter, so a flat-side key keeps flat-side names.\n *\n * A context only ever refines a pitch class the key leaves ambiguous — one that\n * is not a tone of `key`. Scale tones, and the tonic, keep their key spelling\n * whatever the context says.\n *\n * The melodic rules read the two neighbours differently, because a chromatic\n * note is an inflection of the note it came from but a leading tone into the\n * note it goes to: `next` moves the letter, `previous` keeps it. For a chromatic\n * pitch class the evidence is weighed in this order, and the first spelling\n * within a double accidental wins:\n *\n * 1. The sounding chord, when `chordRoot` is given and the pitch is an\n * unambiguous chord tone above it (root, third, fifth or seventh) — the third\n * of a secondary dominant D7 in C major spells F#, not Gb.\n * 2. Resolution: `next` a semitone above makes the pitch a leading tone into it,\n * one letter below — the C# rising to D in D minor.\n * 3. Approach: `previous` a semitone below inflects that note upward, keeping\n * its letter, so a chromatic ascent spells with sharps — F F# G in C major.\n * 4. Descent: `next` a semitone below puts the pitch one letter above it, so a\n * falling chromatic line spells with flats — A Ab G in C major. Failing that,\n * `previous` a semitone above inflects that note downward on its own letter,\n * which spells the same line from the note before instead.\n *\n * If no rule applies, or every candidate would need more than a double\n * accidental, the key spelling stands.\n *\n * @param pc The pitch class to spell.\n * @param tonic The spelled tonic (its letter anchors the spelling), as a note\n * name, a MIDI number, note data, or a `Note`.\n * @param key The key/scale, as a key name, a key/scale, or a `Key`.\n * @param context Optional surrounding evidence; an absent or empty context\n * spells exactly as the key alone does.\n * @returns The spelled note (without octave).\n * @throws If `tonic` does not sound the key's root pitch class.\n * @example\n * ```ts\n * import { spellPitchClass, formatNote, parseNote, majorKey } from '@libraz/libcantus';\n * const c = parseNote('C');\n * formatNote(spellPitchClass(6, c, majorKey(0))); // 'F#'\n * // The third of D7, the secondary dominant of G.\n * formatNote(spellPitchClass(6, 'C', 'C major', { chordRoot: 2 })); // 'F#'\n * // A chromatic passing tone falling from G to F.\n * formatNote(spellPitchClass(6, c, majorKey(0), { previous: 7, next: 5 })); // 'Gb'\n * ```\n * @category Pitch & Intervals\n */\nexport function spellPitchClass(\n pc: number,\n tonic: NoteLike,\n key: KeyLike,\n context?: SpellingContext,\n): Note {\n const note = toNoteData(tonic);\n const scale = toKeyScale(key);\n assertTonicOf(note, scale, 'spellPitchClass');\n assertOptions(context, 'context');\n return spelledPitchClass(pc, note, scale, context);\n}\n\n/** Spell a pitch class from an already-resolved tonic and key. */\nfunction spelledPitchClass(\n pc: number,\n tonic: Note,\n key: KeyScale,\n context?: SpellingContext,\n): Note {\n const keySpelling = spellByKey(pc, tonic, key);\n return context === undefined\n ? keySpelling\n : refineByContext(pc, tonic, key, context, keySpelling);\n}\n\n/**\n * Spell every pitch class of a scale, in ascending scale-degree order.\n *\n * Correct for heptatonic scales (each degree gets the next letter). A gapped\n * scale gives each of its tones a letter of its own wherever its pitch set\n * allows one — C miyako-bushi spells C Db F G Ab — and is spelled tone by tone,\n * on the accidental side the scale leans towards, where it does not: the blues\n * scale has to name one letter twice however it is read.\n *\n * @param tonic The spelled tonic, as a note name, a MIDI number, note data, or a\n * `Note`.\n * @param key The key/scale, as a key name, a key/scale, or a `Key` — the same\n * forms its sibling spellers take.\n * @returns Spelled notes, one per scale degree.\n * @throws If `key` names no key, or if `tonic` does not sound the key's root\n * pitch class.\n * @example\n * ```ts\n * import { spellScale, noteNames, parseNote, majorKey } from '@libraz/libcantus';\n * noteNames(spellScale(parseNote('C'), majorKey(0)));\n * // ['C', 'D', 'E', 'F', 'G', 'A', 'B']\n * ```\n * @category Pitch & Intervals\n */\nexport function spellScale(tonic: NoteLike, key: KeyLike): Note[] {\n const note = toNoteData(tonic);\n const scale = toKeyScale(key);\n assertTonicOf(note, scale, 'spellScale');\n return scaleTonesInDegreeOrder(scale).map((pc) => spelledPitchClass(pc, note, scale));\n}\n\n/**\n * Spell an arbitrary list of pitch classes relative to a key.\n *\n * @param pcs The pitch classes.\n * @param tonic The spelled tonic, as a note name, a MIDI number, note data, or a\n * `Note`.\n * @param key The key/scale, as a key name, a key/scale, or a `Key`.\n * @returns Spelled notes, in input order.\n * @throws If `tonic` does not sound the key's root pitch class.\n * @category Pitch & Intervals\n */\nexport function spellPitchClasses(pcs: number[], tonic: NoteLike, key: KeyLike): Note[] {\n const classes = assertArray<number>(pcs, 'pcs');\n const note = toNoteData(tonic);\n const scale = toKeyScale(key);\n assertTonicOf(note, scale, 'spellPitchClasses');\n return classes.map((pc, index) =>\n spelledPitchClass(assertPitchClass(pc, `pcs[${index}]`), note, scale),\n );\n}\n\n/** Letter distance above the root of each simple interval, for interval data no case names. */\nexport const SIMPLE_LETTER_OFFSETS: readonly number[] = [0, 1, 1, 2, 2, 3, 4, 4, 4, 5, 6, 6];\n\n/**\n * Diatonic letter distance implied by a chord interval.\n *\n * Chord intervals are not merely pitch-class distances: 6 is a diminished\n * fifth in the basic chord templates while 18 is an augmented eleventh, and 9\n * is a diminished seventh in `dim7` but a sixth elsewhere. Keeping that\n * distinction is what prevents functional chord tones from being respelled by\n * their nearest key pitch (for example G# becoming Ab in E7).\n *\n * An extension is placed by the degree it alters rather than by where its\n * semitones land: a ninth is one letter above the root, an eleventh three and a\n * thirteenth five, however the alteration moves it. So a lowered eleventh is a\n * fourth-class letter even though it sounds a major third above the root — C7b11\n * spells Fb, not a second E — and a raised thirteenth a sixth-class one even\n * though it sounds a minor seventh.\n */\nfunction chordLetterOffset(interval: number, chord: Chord): number {\n switch (interval) {\n case 0:\n case 12:\n return 0;\n case 1:\n case 2:\n case 13:\n case 14:\n return 1;\n case 3:\n case 4:\n return 2;\n case 15:\n // The spec reads 15 as a raised ninth whatever else the chord holds, so it\n // is one letter above the root: C7#9 and C7sus4(#9) both spell D#.\n return 1;\n case 5:\n case 16:\n case 17:\n case 18:\n return 3;\n case 6:\n case 7:\n case 8:\n case 19:\n return 4;\n case 9:\n return chord.quality === 'dim7' ? 6 : 5;\n case 20:\n case 21:\n case 22:\n return 5;\n case 10:\n case 11:\n case 23:\n return 6;\n default: {\n // Unknown/custom interval data gets the nearest conventional diatonic\n // distance. Public builders use the explicit cases above.\n const octaves = Math.floor(Math.max(0, interval) / 12);\n const simple = mod12(interval);\n const simpleOffset = SIMPLE_LETTER_OFFSETS[simple] ?? 0;\n return octaves * 7 + simpleOffset;\n }\n }\n}\n\n/**\n * Spell chord tones from an already chosen root spelling.\n *\n * This is public for callers that already know the desired root spelling;\n * {@link spellChord} instead derives that spelling from the chord and key.\n *\n * Letters follow the chord's own tone spellings when it carries a usable set,\n * and otherwise the diatonic distance each interval implies in a stack of\n * thirds. Accidentals are always recomputed from the sounding pitch class, so\n * the tones name exactly the chord's own pitch classes either way.\n *\n * @param chord The chord whose tones are spelled.\n * @param root The chosen root spelling, as a note name, a MIDI number, note\n * data, or a `Note`.\n */\nexport function spellChordFromRoot(chord: Chord, root: NoteLike): Note[] {\n return chordTonesFromRoot(assertChord(chord), toNoteData(root));\n}\n\n/** Spell chord tones from an already-resolved root spelling. */\nfunction chordTonesFromRoot(chord: Chord, root: Note): Note[] {\n if (mod12(naturalPc(root.letter) + root.alter) !== mod12(chord.rootPc)) {\n throw new InvalidInputError('chord root spelling must match chord.rootPc');\n }\n const hinted = chordToneLetterOffsets(chord);\n return chord.intervals.map((interval, index) => {\n const letter = mod7(root.letter + (hinted?.[index] ?? chordLetterOffset(interval, chord)));\n const pc = mod12(chord.rootPc + interval);\n return { letter, alter: alterFor(letter, pc) };\n });\n}\n\n/**\n * Spell a chord's tones, in the chord's own (tertian) order, relative to a key.\n *\n * Diatonic chords spell exactly (e.g. G7 in C major -> G B D F). Chromatic chord\n * tones take their conventional spelling; enharmonically ambiguous altered\n * tensions may be spelled by the general convention rather than by chord\n * function.\n *\n * A chord carrying its own tone spellings is spelled by them, since a chord\n * whose letters do not follow from stacking thirds can say so no other way:\n * that is what keeps the German sixth of C major on its F#, the note that\n * resolves outward to the dominant, rather than the Gb a dominant seventh\n * would put there.\n *\n * @param chord The chord.\n * @param tonic The spelled tonic of the key, as a note name, a MIDI number,\n * note data, or a `Note`.\n * @param key The key/scale, as a key name, a key/scale, or a `Key`.\n * @returns Spelled chord tones, root first.\n * @throws If `tonic` does not sound the key's root pitch class.\n * @example\n * ```ts\n * import { spellChord, noteNames, parseNote, majorKey, makeChord } from '@libraz/libcantus';\n * // G7 in C major\n * noteNames(spellChord(makeChord(7, 'dom7'), parseNote('C'), majorKey(0)));\n * // ['G', 'B', 'D', 'F']\n * ```\n * @category Pitch & Intervals\n */\nexport function spellChord(chord: Chord, tonic: NoteLike, key: KeyLike): Note[] {\n assertChord(chord);\n const note = toNoteData(tonic);\n const scale = toKeyScale(key);\n assertTonicOf(note, scale, 'spellChord');\n return chordTonesFromRoot(chord, chordRootSpelling(chord, note, scale));\n}\n\n/**\n * The root spelling to use for a chord: the chord's own hint when it still\n * matches the root pitch class, otherwise the key's spelling of that pitch\n * class.\n *\n * Honouring the hint is what keeps a chord's spelled tones agreeing with the\n * symbol it renders as — `parseChordSymbol('C#7')` must spell C# E# G# B even\n * when analysed in a flat-side key.\n */\nfunction chordRootSpelling(chord: Chord, tonic: Note, key: KeyScale): Note {\n const hint = chord.rootSpelling;\n if (hint !== undefined && mod12(naturalPc(hint.letter) + hint.alter) === mod12(chord.rootPc)) {\n return { letter: mod7(hint.letter), alter: hint.alter };\n }\n return spelledPitchClass(chord.rootPc, tonic, key);\n}\n\n/**\n * Spell a MIDI pitch relative to a key, keeping its octave.\n *\n * The octave-less sibling of this function, {@link spellPitchClass}, is enough\n * for chord and scale spelling; this one is for naming actual sounding pitches\n * (a generated line, an imported track) without losing the register.\n *\n * The octave is the one that reproduces `pitch` from the chosen spelling, so a\n * context that changes the letter changes the octave with it: MIDI 60 spelled\n * B# is B#3, not B#4.\n *\n * @param pitch The MIDI pitch; rounded to the nearest integer.\n * @param tonic The spelled tonic of the key, as a note name, a MIDI number,\n * note data, or a `Note`.\n * @param key The key/scale, as a key name, a key/scale, or a `Key`.\n * @param context Optional surrounding evidence, weighed exactly as\n * {@link spellPitchClass} weighs it.\n * @returns The spelled note, carrying the octave that reproduces `pitch`.\n * @throws If `tonic` does not sound the key's root pitch class.\n * @example\n * ```ts\n * import { spellPitch, formatNote, parseNote, majorKey } from '@libraz/libcantus';\n * formatNote(spellPitch(70, parseNote('Eb'), majorKey(3))); // 'Bb4'\n * ```\n * @category Pitch & Intervals\n */\nexport function spellPitch(\n pitch: number,\n tonic: NoteLike,\n key: KeyLike,\n context?: SpellingContext,\n): Note {\n const note = toNoteData(tonic);\n const scale = toKeyScale(key);\n assertTonicOf(note, scale, 'spellPitch');\n assertOptions(context, 'context');\n const rounded = Math.round(assertFiniteNumber(pitch, 'pitch'));\n const spelled = spelledPitchClass(mod12(rounded), note, scale, context);\n const octave = (rounded - naturalPc(spelled.letter) - spelled.alter) / 12 - 1;\n return { letter: spelled.letter, alter: spelled.alter, octave };\n}\n\n/**\n * Convenience: render spelled notes as letter-name strings.\n *\n * @param notes The notes, each a note name, a MIDI number, note data, or a\n * `Note`.\n * @returns Their formatted names.\n * @category Pitch & Intervals\n */\nexport function noteNames(notes: readonly NoteLike[]): string[] {\n assertArray(notes, 'notes');\n // Wrapped rather than passed by reference: `map` supplies an index that\n // `formatNote`'s options parameter would read as a naming system.\n return notes.map((note) => formatNote(toNoteData(note)));\n}\n","/**\n * Shared interval reading for the counterpoint rules.\n *\n * The predicates, the species checker and the independence metric all need the\n * same two conversions — a pitch argument to a sounding pitch, and a spelled\n * interval to its consonance class — and importing them from one another would\n * make the module graph circular.\n */\n\nimport { InvalidInputError } from '../../core/errors/index.js';\nimport type { ConsonanceClass } from '../../core/interval/index.js';\nimport { ConsonanceClass as Consonance } from '../../core/interval/index.js';\nimport type { IntervalQualityLabel, Note, SpelledInterval } from '../../core/pitch/index.js';\nimport { noteToMidi } from '../../core/pitch/index.js';\nimport {\n assertDegree,\n assertFlag,\n assertRecord,\n describeRejected,\n} from '../../core/validation/index.js';\n\n/**\n * Require an interval quality label.\n *\n * The label is open at both ends — an augmented interval is `'A'` repeated and a\n * diminished one `'d'` repeated, so there is no list to check against — but its\n * shape is closed: one of the three plain qualities, or a run of one glyph.\n */\nfunction assertQualityLabel(quality: IntervalQualityLabel): IntervalQualityLabel {\n if (typeof quality !== 'string' || !/^(P|M|m|A+|d+)$/.test(quality)) {\n throw new InvalidInputError(\n `interval.quality must be an interval quality; received ${describeRejected(quality)}`,\n );\n }\n return quality;\n}\n\n/** The sounding pitch of a predicate argument, whichever form it arrived in. */\nexport function pitchOf(value: number | Note): number {\n return typeof value === 'number' ? value : noteToMidi(value);\n}\n\n/** Reduce a compound diatonic number to its simple form in [1, 7]. */\nexport function simpleIntervalNumber(numberValue: number): number {\n return ((Math.abs(numberValue) - 1) % 7) + 1;\n}\n\n/**\n * Classify a spelled interval by consonance, the way a counterpoint exercise\n * reads it.\n *\n * The spelling decides, not the semitone count: a diminished fourth is a\n * dissonance though it sounds like a major third, and an augmented fifth is a\n * dissonance though it sounds like a minor sixth. Only the perfect unison, fifth\n * and octave and the major and minor thirds and sixths are consonant. The\n * perfect fourth is context-dependent exactly as in {@link classifyInterval}.\n *\n * @param interval The spelled interval between the two voices.\n * @param twoVoice When true, the perfect fourth counts as a dissonance.\n * @returns The counterpoint quality of the interval.\n * @example\n * ```ts\n * import { classifySpelledInterval, ConsonanceClass, parseInterval } from '@libraz/libcantus';\n * classifySpelledInterval(parseInterval('d4')); // ConsonanceClass.Dissonance\n * classifySpelledInterval(parseInterval('M3')); // ConsonanceClass.ImperfectConsonance\n * ```\n * @category Voicing & Counterpoint\n */\nexport function classifySpelledInterval(\n interval: SpelledInterval,\n twoVoice = true,\n): ConsonanceClass {\n // The record check alone leaves the two fields the answer is read from\n // unread, and an interval carrying neither would classify as a dissonance —\n // the same verdict a real tritone gets, and one a counterpoint report acts on.\n assertFlag(twoVoice, 'twoVoice');\n const read = assertRecord<SpelledInterval>(interval, 'interval');\n const simple = simpleIntervalNumber(assertDegree(read.number, 'interval.number'));\n const quality = assertQualityLabel(read.quality);\n if (quality === 'P') {\n if (simple === 1 || simple === 5) {\n return Consonance.PerfectConsonance;\n }\n if (simple === 4) {\n return twoVoice ? Consonance.Dissonance : Consonance.ImperfectConsonance;\n }\n return Consonance.Dissonance;\n }\n if ((quality === 'M' || quality === 'm') && (simple === 3 || simple === 6)) {\n return Consonance.ImperfectConsonance;\n }\n return Consonance.Dissonance;\n}\n","/**\n * Voice independence as a measurement rather than a verdict.\n *\n * The species rules answer \"is this allowed\", which is the wrong question of a\n * pop arrangement: a harmony line in parallel thirds, a power-chord riff and a\n * pedal point all break them and all are the intended sound. This module\n * reports how independent two lines actually are and leaves the judgement to\n * the caller.\n */\n\nimport { InvalidInputError } from '../../core/errors/index.js';\nimport { ConsonanceClass } from '../../core/interval/index.js';\nimport type { Note } from '../../core/pitch/index.js';\nimport { spelledInterval } from '../../core/pitch/index.js';\nimport { assertArray, assertOptions } from '../../core/validation/index.js';\nimport { classifySpelledInterval, pitchOf, simpleIntervalNumber } from './internal.js';\n\n/**\n * How two lines move against each other, and how far apart they sit.\n *\n * @category Voicing & Counterpoint\n */\nexport type VoiceIndependenceReport = {\n /**\n * Share of the moves that fall into each motion type, summing to 1. A slot\n * pair in which neither voice moves is not a move and is left out entirely,\n * so an accompaniment that mostly sits still can still report mostly contrary\n * motion. All four are 0 when nothing moves.\n */\n motion: {\n /** The voices move in opposite directions. */\n contrary: number;\n /** One voice holds while the other moves. */\n oblique: number;\n /** Both rise or both fall, and the interval between them changes. */\n similar: number;\n /** Both rise or both fall, keeping the same interval. */\n parallel: number;\n };\n /**\n * Share of the slots where the lead holds or rests on which the counter\n * attacks: 1 when the counter fills every gap the lead leaves, 0 when it moves\n * only where the lead does. It is 0 when the lead never holds or rests, since\n * there was nothing to complement.\n */\n rhythmicComplementarity: number;\n /** Distance between the two lines, in semitones, over the slots where both sound. */\n separation: {\n /** Mean distance; 0 when the two lines never sound together. */\n mean: number;\n /** Closest approach; 0 when the two lines never sound together. */\n min: number;\n };\n /**\n * Slots at which the two lines sit in the opposite order to the one they keep\n * overall. A unison is not a crossing.\n */\n crossings: number;\n /** Longest run of consecutive slots sounding a perfect consonance. */\n longestPerfectRun: number;\n /** Slots at which both lines sound, the denominator of `separation`. */\n sounding: number;\n};\n\n/**\n * Options controlling {@link voiceIndependence}.\n *\n * @category Voicing & Counterpoint\n */\nexport type VoiceIndependenceOptions = {\n /**\n * Which slots are fresh attacks in the lead. Without it a slot counts as an\n * attack when its note differs from the previous slot's, which reads a\n * deliberately repeated note as a sustain.\n */\n leadAttacks?: readonly boolean[];\n /** Which slots are fresh attacks in the counter; read as `leadAttacks` is. */\n counterAttacks?: readonly boolean[];\n /**\n * Count the perfect fourth among the perfect consonances when measuring\n * `longestPerfectRun`. Useful for quartal writing, where the fourth is the\n * interval being sustained.\n *\n * @defaultValue false\n */\n countFourths?: boolean;\n};\n\n/** One slot of both lines, resolved to what the metrics read off it. */\ntype Slot = {\n lead: Note | null;\n counter: Note | null;\n leadPitch: number | null;\n counterPitch: number | null;\n};\n\n/** Whether a slot has both voices sounding. */\nfunction bothSound(slot: Slot): slot is Slot & { lead: Note; counter: Note } {\n return slot.lead !== null && slot.counter !== null;\n}\n\n/** Whether a voice attacks at a slot, by the caller's flags or by note change. */\nfunction attacksAt(\n index: number,\n notes: readonly (Note | null)[],\n flags: readonly boolean[] | undefined,\n): boolean {\n if (flags !== undefined) {\n return flags[index] === true;\n }\n const current = notes[index];\n if (current === undefined || current === null) {\n return false;\n }\n const previous = index > 0 ? notes[index - 1] : undefined;\n if (previous === undefined || previous === null) {\n return true;\n }\n return (\n previous.letter !== current.letter ||\n previous.alter !== current.alter ||\n previous.octave !== current.octave\n );\n}\n\n/** The motion the two voices make between two adjacent sounding slots. */\nfunction motionBetween(\n from: Slot & { lead: Note; counter: Note },\n to: Slot & { lead: Note; counter: Note },\n): 'contrary' | 'oblique' | 'similar' | 'parallel' | null {\n const leadMove = pitchOf(to.lead) - pitchOf(from.lead);\n const counterMove = pitchOf(to.counter) - pitchOf(from.counter);\n if (leadMove === 0 && counterMove === 0) {\n return null;\n }\n if (leadMove === 0 || counterMove === 0) {\n return 'oblique';\n }\n if (leadMove > 0 !== counterMove > 0) {\n return 'contrary';\n }\n const before = simpleIntervalNumber(spelledInterval(from.counter, from.lead).number);\n const after = simpleIntervalNumber(spelledInterval(to.counter, to.lead).number);\n return before === after ? 'parallel' : 'similar';\n}\n\n/**\n * Measure how independent a counter line is from the lead it accompanies.\n *\n * Both lines are index-aligned samples: one slot per beat (or whatever grid the\n * caller works on), `null` for a rest, and a repeated note for a sustain. That\n * is what makes the rhythmic reading possible without a duration model, and\n * what makes a re-attacked repeated note look like a sustain unless\n * `leadAttacks` / `counterAttacks` say otherwise. Notes must carry octaves,\n * since separation and motion are measured between sounding pitches.\n *\n * Nothing here is a verdict. Parallel thirds report as parallel motion with a\n * small separation, a pedal point as oblique motion, a power-chord line as a\n * long perfect run — all facts about the texture, not faults in it.\n *\n * @param lead The leading line, one entry per slot.\n * @param counter The accompanying line, aligned slot for slot with `lead`.\n * @param opts Attack flags and the treatment of the fourth.\n * @returns The motion breakdown, rhythmic complementarity, separation,\n * crossings, and the longest perfect-consonance run.\n * @throws If the two lines differ in length, if an attack array is supplied at\n * another length than the lines, or if a note carries no octave.\n * @example\n * ```ts\n * import { parseNote, voiceIndependence } from '@libraz/libcantus';\n * const lead = ['C5', 'D5', 'E5'].map((n) => parseNote(n));\n * const counter = ['E4', 'F4', 'G4'].map((n) => parseNote(n));\n * voiceIndependence(lead, counter).motion.parallel; // 1 — a harmony line in sixths\n * ```\n * @category Voicing & Counterpoint\n */\nexport function voiceIndependence(\n lead: readonly (Note | null)[],\n counter: readonly (Note | null)[],\n opts?: VoiceIndependenceOptions,\n): VoiceIndependenceReport {\n assertOptions(opts, 'opts');\n const upper = assertArray<Note | null>(lead, 'lead');\n const lower = assertArray<Note | null>(counter, 'counter');\n if (upper.length !== lower.length) {\n throw new InvalidInputError(\n `voiceIndependence needs the two lines aligned slot for slot; received ${upper.length} and ${lower.length}`,\n );\n }\n // The attack arrays are read slot by slot alongside the lines, so one of the\n // wrong length is not a shorter reading of the same texture but a silently\n // wrong one: every slot past its end would count as a sustain.\n for (const [name, flags] of [\n ['leadAttacks', opts?.leadAttacks],\n ['counterAttacks', opts?.counterAttacks],\n ] as const) {\n if (flags !== undefined && flags.length !== upper.length) {\n throw new InvalidInputError(\n `voiceIndependence needs ${name} aligned slot for slot with the lines; received ${flags.length} and ${upper.length}`,\n );\n }\n }\n const slots: Slot[] = upper.map((leadNote, index) => {\n const counterNote = lower[index] ?? null;\n return {\n lead: leadNote,\n counter: counterNote,\n leadPitch: leadNote === null ? null : pitchOf(leadNote),\n counterPitch: counterNote === null ? null : pitchOf(counterNote),\n };\n });\n\n const counts = { contrary: 0, oblique: 0, similar: 0, parallel: 0 };\n let moves = 0;\n for (let index = 1; index < slots.length; index += 1) {\n const from = slots[index - 1];\n const to = slots[index];\n if (from === undefined || to === undefined || !bothSound(from) || !bothSound(to)) {\n continue;\n }\n const motion = motionBetween(from, to);\n if (motion === null) {\n continue;\n }\n counts[motion] += 1;\n moves += 1;\n }\n\n let filled = 0;\n let gaps = 0;\n for (let index = 0; index < slots.length; index += 1) {\n const leadHolds = !attacksAt(index, lead, opts?.leadAttacks);\n if (!leadHolds) {\n continue;\n }\n gaps += 1;\n if (attacksAt(index, counter, opts?.counterAttacks)) {\n filled += 1;\n }\n }\n\n let sounding = 0;\n let totalGap = 0;\n let minGap = Number.POSITIVE_INFINITY;\n let orderSum = 0;\n let perfectRun = 0;\n let longestPerfectRun = 0;\n for (const slot of slots) {\n if (slot.leadPitch === null || slot.counterPitch === null) {\n perfectRun = 0;\n continue;\n }\n sounding += 1;\n const gap = slot.leadPitch - slot.counterPitch;\n totalGap += Math.abs(gap);\n minGap = Math.min(minGap, Math.abs(gap));\n orderSum += Math.sign(gap);\n const interval = spelledInterval(slot.counter as Note, slot.lead as Note);\n const perfect =\n classifySpelledInterval(interval) === ConsonanceClass.PerfectConsonance ||\n ((opts?.countFourths ?? false) &&\n interval.quality === 'P' &&\n simpleIntervalNumber(interval.number) === 4);\n perfectRun = perfect ? perfectRun + 1 : 0;\n longestPerfectRun = Math.max(longestPerfectRun, perfectRun);\n }\n\n // The prevailing order is whichever way round the two lines sit most of the\n // time; a line written above the lead is as ordinary as one written below, and\n // only the departures from its own habit count as crossings.\n const leadAbove = orderSum >= 0;\n let crossings = 0;\n for (const slot of slots) {\n if (slot.leadPitch === null || slot.counterPitch === null) {\n continue;\n }\n const gap = slot.leadPitch - slot.counterPitch;\n if (gap !== 0 && gap > 0 !== leadAbove) {\n crossings += 1;\n }\n }\n\n return {\n motion: {\n contrary: moves === 0 ? 0 : counts.contrary / moves,\n oblique: moves === 0 ? 0 : counts.oblique / moves,\n similar: moves === 0 ? 0 : counts.similar / moves,\n parallel: moves === 0 ? 0 : counts.parallel / moves,\n },\n rhythmicComplementarity: gaps === 0 ? 0 : filled / gaps,\n separation: {\n mean: sounding === 0 ? 0 : totalGap / sounding,\n min: sounding === 0 ? 0 : minGap,\n },\n crossings,\n longestPerfectRun,\n sounding,\n };\n}\n","/**\n * Counterpoint predicates: each judges one pair of voices at one moment.\n *\n * Every predicate reads spelled notes, because spelling is what decides two of\n * these rules. An augmented second and a minor third span the same three\n * semitones, and a diminished fourth sounds like a major third; the first is a\n * forbidden melodic leap and the second a vertical dissonance, and neither can\n * be seen in a MIDI integer. {@link createsVerticalDissonance} and\n * {@link isForbiddenMelodicLeap} therefore answer the spelled question when\n * given spelled notes, and the sounding question — all a bare pitch can support\n * — when given integers. {@link isAugmentedMelodicInterval} asks a question that\n * has no sounding half at all, so its integer form answers false rather than\n * refusing the call: every predicate here takes either form, and the ones that\n * need spelling say so by degrading.\n *\n * The remaining rules compare registers or perfect classes, which spelling\n * cannot change: a parallel fifth is a parallel fifth however the two voices are\n * written. Those keep their integer form as an equal partner.\n */\n\nimport { ConsonanceClass, isConsonantInterval } from '../../core/interval/index.js';\nimport type { Note, SpelledInterval } from '../../core/pitch/index.js';\nimport { pitchClassOf as pitchClass, spelledInterval } from '../../core/pitch/index.js';\nimport { assertFiniteNumber, assertOneOf } from '../../core/validation/index.js';\nimport { type KeyLike, toKeyScale } from '../scale/index.js';\nimport { classifySpelledInterval, pitchOf, simpleIntervalNumber } from './internal.js';\n\n/**\n * The readings {@link createsHiddenParallelPerfect} judges an approach by.\n *\n * Named once so the three places the choice is spelled — the two overloads and\n * the implementation — cannot fall out of step with the check that a caller's\n * value is one of them.\n */\nexport const HIDDEN_PARALLEL_READINGS = Object.freeze(['fourPart', 'twoVoice'] as const);\n\n/**\n * Which reading of hidden parallels an approach is judged by.\n *\n * @category Voicing & Counterpoint\n */\nexport type HiddenParallelReading = (typeof HIDDEN_PARALLEL_READINGS)[number];\n\n/**\n * Whether an upper voice has crossed below a lower voice.\n *\n * @param upper The nominally higher voice.\n * @param lower The nominally lower voice.\n * @returns True when the upper voice sits below the lower voice.\n * @category Voicing & Counterpoint\n */\nexport function createsVoiceCrossing(upper: Note, lower: Note): boolean;\nexport function createsVoiceCrossing(upper: number, lower: number): boolean;\nexport function createsVoiceCrossing(upper: number | Note, lower: number | Note): boolean {\n return pitchOf(upper) < pitchOf(lower);\n}\n\n/**\n * Whether two simultaneous voices form a dissonance.\n *\n * Given spelled notes the spelling decides, so a diminished fourth reads as the\n * dissonance it is. Given MIDI integers only the sounding interval is available,\n * which cannot tell that fourth from a major third; use the spelled form\n * wherever the exercise is written in notes.\n *\n * @param a First voice.\n * @param b Second voice.\n * @param twoVoice When true, the perfect fourth counts as dissonant.\n * @returns True when the vertical interval is dissonant.\n * @example\n * ```ts\n * import { createsVerticalDissonance, parseNote } from '@libraz/libcantus';\n * createsVerticalDissonance(parseNote('Fb4'), parseNote('C4'), true); // true — d4\n * createsVerticalDissonance(64, 60, true); // false — the same pitches read as a major third\n * ```\n * @category Voicing & Counterpoint\n */\nexport function createsVerticalDissonance(a: Note, b: Note, twoVoice: boolean): boolean;\nexport function createsVerticalDissonance(a: number, b: number, twoVoice: boolean): boolean;\nexport function createsVerticalDissonance(\n a: number | Note,\n b: number | Note,\n twoVoice: boolean,\n): boolean {\n if (typeof a !== 'number' && typeof b !== 'number') {\n // The interval number and quality are the same read either way round, so the\n // argument order only decides a sign the classification never looks at.\n return classifySpelledInterval(spelledInterval(a, b), twoVoice) === ConsonanceClass.Dissonance;\n }\n return !isConsonantInterval(pitchOf(a) - pitchOf(b), twoVoice);\n}\n\n/**\n * Whether a melodic move is a forbidden leap.\n *\n * Forbidden are the seventh in either quality, any leap wider than an octave,\n * and — where the spelling is known — every augmented and diminished interval,\n * the augmented second of the harmonic minor among them. Given MIDI integers the\n * tritone is still caught, since it is forbidden under either spelling, but an\n * augmented second is indistinguishable from a minor third and passes.\n *\n * The raw distance is used for the octave check, so a compound leap is not\n * reduced to its simple class first.\n *\n * @param prev Starting note.\n * @param cur Ending note.\n * @returns True when the leap is forbidden in strict counterpoint.\n * @example\n * ```ts\n * import { isForbiddenMelodicLeap, parseNote } from '@libraz/libcantus';\n * isForbiddenMelodicLeap(parseNote('Ab4'), parseNote('B4')); // true — augmented second\n * isForbiddenMelodicLeap(68, 71); // false — the same pitches read as a minor third\n * ```\n * @category Voicing & Counterpoint\n */\nexport function isForbiddenMelodicLeap(prev: Note, cur: Note): boolean;\nexport function isForbiddenMelodicLeap(prev: number, cur: number): boolean;\nexport function isForbiddenMelodicLeap(prev: number | Note, cur: number | Note): boolean {\n if (typeof prev !== 'number' && typeof cur !== 'number') {\n const interval = spelledInterval(prev, cur);\n if (Math.abs(interval.semitones) > 12 || interval.number > 8) {\n return true;\n }\n if (simpleIntervalNumber(interval.number) === 7) {\n return true;\n }\n return isAlteredInterval(interval);\n }\n const semis = Math.abs(pitchOf(cur) - pitchOf(prev));\n return semis === 6 || semis === 10 || semis === 11 || semis > 12;\n}\n\n/** Whether a spelled interval is augmented or diminished rather than plain. */\nfunction isAlteredInterval(interval: SpelledInterval): boolean {\n // The altered unison is left out: a chromatic inflection of the note a voice\n // already holds is ordinary voice leading, not a leap at all.\n return interval.number >= 2 && (interval.quality.startsWith('A') || interval.quality[0] === 'd');\n}\n\n/**\n * Whether a voice moves by an augmented interval — the melodic step no\n * sixteenth-century line takes, and the one a MIDI integer cannot show.\n *\n * The augmented unison is excluded: raising or lowering the note a voice already\n * holds is a chromatic inflection, not a leap.\n *\n * Every augmented interval sounds like a plain one — the augmented second like a\n * minor third, the augmented fourth like a diminished fifth — so MIDI integers\n * carry nothing this rule can read, and the numeric form always answers false.\n * It exists so a host can run the whole predicate set over a bare pitch pair;\n * take the spelled form wherever the exercise is written in notes.\n *\n * @param prev Starting note.\n * @param cur Ending note.\n * @returns True when the move spans an augmented second or wider.\n * @example\n * ```ts\n * import { isAugmentedMelodicInterval, parseNote } from '@libraz/libcantus';\n * isAugmentedMelodicInterval(parseNote('Ab4'), parseNote('B4')); // true — A2 in C minor\n * isAugmentedMelodicInterval(parseNote('A4'), parseNote('C5')); // false — m3\n * isAugmentedMelodicInterval(68, 71); // false — the same pitches carry no spelling\n * ```\n * @category Voicing & Counterpoint\n */\nexport function isAugmentedMelodicInterval(prev: Note, cur: Note): boolean;\nexport function isAugmentedMelodicInterval(prev: number, cur: number): boolean;\nexport function isAugmentedMelodicInterval(prev: number | Note, cur: number | Note): boolean {\n if (typeof prev === 'number' || typeof cur === 'number') {\n return false;\n }\n const interval = spelledInterval(prev, cur);\n return interval.number >= 2 && interval.quality.startsWith('A');\n}\n\n/** Reduce an interval to its simple class in [0, 11]. */\nfunction simpleClass(semitones: number): number {\n return pitchClass(Math.abs(semitones));\n}\n\n/** Whether a simple interval class is a perfect kind (unison/octave or fifth). */\nfunction isPerfectClass(cls: number): boolean {\n return cls === 0 || cls === 7;\n}\n\n/** Whether two voices move in the same direction (similar or parallel motion). */\nfunction similarMotion(aMove: number, bMove: number): boolean {\n return aMove !== 0 && bMove !== 0 && aMove > 0 === bMove > 0;\n}\n\n/** Whether both voices actually move (neither is stationary — excludes oblique motion). */\nfunction bothVoicesMove(aMove: number, bMove: number): boolean {\n return aMove !== 0 && bMove !== 0;\n}\n\n/**\n * Whether two voices move into consecutive perfect intervals of the same kind\n * (fifth-to-fifth, octave-to-octave, unison-to-unison).\n *\n * Both true parallels (similar motion) and anti-parallels — the same perfect\n * class reached by contrary motion, e.g. octave to octave with the voices moving\n * in opposite directions — are flagged, as both are forbidden in strict two-voice\n * counterpoint. The rule requires that both voices actually move: oblique motion\n * (either voice stationary) and the no-change case (identical pitches) are excluded.\n *\n * A fifth expanding to a twelfth counts (same perfect class); a fifth moving to\n * an octave does not (different perfect kinds — the direct/hidden case owned by\n * {@link createsHiddenParallelPerfect}).\n *\n * The perfect classes are what they sound like under any spelling, so spelled\n * notes and MIDI integers give the same answer here.\n *\n * @category Voicing & Counterpoint\n */\nexport function createsParallelPerfect(aPrev: Note, aCur: Note, bPrev: Note, bCur: Note): boolean;\nexport function createsParallelPerfect(\n aPrev: number,\n aCur: number,\n bPrev: number,\n bCur: number,\n): boolean;\nexport function createsParallelPerfect(\n aPrev: number | Note,\n aCur: number | Note,\n bPrev: number | Note,\n bCur: number | Note,\n): boolean {\n const a0 = pitchOf(aPrev);\n const a1 = pitchOf(aCur);\n const b0 = pitchOf(bPrev);\n const b1 = pitchOf(bCur);\n if (!bothVoicesMove(a1 - a0, b1 - b0)) {\n return false;\n }\n const nowClass = simpleClass(a1 - b1);\n const prevClass = simpleClass(a0 - b0);\n return isPerfectClass(nowClass) && nowClass === prevClass;\n}\n\n/**\n * Whether two voices move in consecutive parallel octaves (or unisons) by\n * similar motion.\n *\n * This is a strict subset of {@link createsParallelPerfect}: a similar-motion\n * octave-to-octave is the perfect-class-zero case that predicate already flags\n * (and it additionally catches the contrary-motion anti-parallel). Callers that\n * tally parallel violations should therefore use {@link createsParallelPerfect}\n * alone to avoid double counting; this predicate remains for callers wanting a\n * dedicated similar-motion octave test.\n *\n * @category Voicing & Counterpoint\n */\nexport function createsParallelOctave(aPrev: Note, aCur: Note, bPrev: Note, bCur: Note): boolean;\nexport function createsParallelOctave(\n aPrev: number,\n aCur: number,\n bPrev: number,\n bCur: number,\n): boolean;\nexport function createsParallelOctave(\n aPrev: number | Note,\n aCur: number | Note,\n bPrev: number | Note,\n bCur: number | Note,\n): boolean {\n const a0 = pitchOf(aPrev);\n const a1 = pitchOf(aCur);\n const b0 = pitchOf(bPrev);\n const b1 = pitchOf(bCur);\n if (!similarMotion(a1 - a0, b1 - b0)) {\n return false;\n }\n return simpleClass(a1 - b1) === 0 && simpleClass(a0 - b0) === 0;\n}\n\n/**\n * Whether two voices move in consecutive parallel unisons — both landing on the\n * same pitch, having shared a pitch on the previous move.\n *\n * @category Voicing & Counterpoint\n */\nexport function createsParallelUnison(aPrev: Note, aCur: Note, bPrev: Note, bCur: Note): boolean;\nexport function createsParallelUnison(\n aPrev: number,\n aCur: number,\n bPrev: number,\n bCur: number,\n): boolean;\nexport function createsParallelUnison(\n aPrev: number | Note,\n aCur: number | Note,\n bPrev: number | Note,\n bCur: number | Note,\n): boolean {\n const a0 = pitchOf(aPrev);\n const a1 = pitchOf(aCur);\n const b0 = pitchOf(bPrev);\n const b1 = pitchOf(bCur);\n if (a1 === a0 || b1 === b0) {\n return false;\n }\n return a1 === b1 && a0 === b0;\n}\n\n/**\n * Whether two voices reach a perfect interval by similar motion from an\n * imperfect one (a hidden/direct fifth or octave).\n *\n * How strictly the approach is judged is the caller's to say, because the two\n * textures do not agree about it:\n *\n * - `fourPart` (the default) applies the step exception of the chorale — the\n * approach is allowed when the upper of the two voices moves by step, so only\n * leaps into the perfect interval are flagged. That exception is written for\n * the outer voices of a four-part texture, where the other two cover the\n * arrival.\n * - `twoVoice` exempts no approach. With nothing between them, two voices\n * moving the same way into a perfect fifth or octave expose it however the\n * upper one got there, and strict sixteenth-century writing forbids it\n * outright.\n *\n * @param strictness Which reading to judge the approach by.\n * @category Voicing & Counterpoint\n */\nexport function createsHiddenParallelPerfect(\n aPrev: Note,\n aCur: Note,\n bPrev: Note,\n bCur: Note,\n strictness?: HiddenParallelReading,\n): boolean;\nexport function createsHiddenParallelPerfect(\n aPrev: number,\n aCur: number,\n bPrev: number,\n bCur: number,\n strictness?: HiddenParallelReading,\n): boolean;\nexport function createsHiddenParallelPerfect(\n aPrev: number | Note,\n aCur: number | Note,\n bPrev: number | Note,\n bCur: number | Note,\n strictness: HiddenParallelReading = 'fourPart',\n): boolean {\n // A reading outside the two named ones would silently take the four-part\n // exemption, which is the lenient half of the rule: an approach the caller\n // asked to have judged strictly would come back clean.\n assertOneOf(strictness, HIDDEN_PARALLEL_READINGS, 'strictness');\n const a0 = pitchOf(aPrev);\n const a1 = pitchOf(aCur);\n const b0 = pitchOf(bPrev);\n const b1 = pitchOf(bCur);\n const aMove = a1 - a0;\n const bMove = b1 - b0;\n if (!similarMotion(aMove, bMove)) {\n return false;\n }\n const nowClass = simpleClass(a1 - b1);\n const prevClass = simpleClass(a0 - b0);\n // Approaching the same perfect class (e.g. fifth to fifth) is a true parallel\n // owned by createsParallelPerfect; approaching a different perfect interval\n // (fifth to octave, or vice versa) is the hidden/direct case flagged here.\n if (!isPerfectClass(nowClass) || prevClass === nowClass) {\n return false;\n }\n const upperMove = a1 >= b1 ? aMove : bMove;\n if (strictness === 'fourPart' && Math.abs(upperMove) <= 2) {\n return false; // upper voice moves by step — direct interval is acceptable\n }\n return true;\n}\n\n/**\n * Whether two voices arrive at a perfect octave or unison by contrary motion\n * with the upper voice leaping down — the *ottava battuta* the sixteenth-century\n * theorists forbid.\n *\n * The stepwise arrival is allowed, as is the same octave reached with the upper\n * voice rising, so only the downward leap into the perfect class is flagged.\n * Arriving at a perfect class the voices were already sitting on (e.g. octave\n * to octave by contrary motion) is the anti-parallel case owned by\n * {@link createsParallelPerfect}; battuta is reserved for a perfect class that\n * was not already there.\n *\n * @category Voicing & Counterpoint\n */\nexport function createsBattuta(aPrev: Note, aCur: Note, bPrev: Note, bCur: Note): boolean;\nexport function createsBattuta(aPrev: number, aCur: number, bPrev: number, bCur: number): boolean;\nexport function createsBattuta(\n aPrev: number | Note,\n aCur: number | Note,\n bPrev: number | Note,\n bCur: number | Note,\n): boolean {\n const a0 = pitchOf(aPrev);\n const a1 = pitchOf(aCur);\n const b0 = pitchOf(bPrev);\n const b1 = pitchOf(bCur);\n const aMove = a1 - a0;\n const bMove = b1 - b0;\n if (!bothVoicesMove(aMove, bMove) || similarMotion(aMove, bMove)) {\n return false;\n }\n const nowClass = simpleClass(a1 - b1);\n if (nowClass !== 0) {\n return false;\n }\n const prevClass = simpleClass(a0 - b0);\n if (prevClass === nowClass) {\n return false; // anti-parallel octave, owned by createsParallelPerfect\n }\n const upperMove = a1 >= b1 ? aMove : bMove;\n return upperMove < -2;\n}\n\n/**\n * Whether two voices overlap: the upper voice descends below where the lower\n * voice just was, or the lower voice rises above where the upper voice just was.\n * Distinct from a simultaneous voice crossing.\n *\n * @param upperPrev Previous note of the upper voice.\n * @param upperCur Current note of the upper voice.\n * @param lowerPrev Previous note of the lower voice.\n * @param lowerCur Current note of the lower voice.\n * @category Voicing & Counterpoint\n */\nexport function createsVoiceOverlap(\n upperPrev: Note,\n upperCur: Note,\n lowerPrev: Note,\n lowerCur: Note,\n): boolean;\nexport function createsVoiceOverlap(\n upperPrev: number,\n upperCur: number,\n lowerPrev: number,\n lowerCur: number,\n): boolean;\nexport function createsVoiceOverlap(\n upperPrev: number | Note,\n upperCur: number | Note,\n lowerPrev: number | Note,\n lowerCur: number | Note,\n): boolean {\n const u0 = pitchOf(upperPrev);\n const u1 = pitchOf(upperCur);\n const l0 = pitchOf(lowerPrev);\n const l1 = pitchOf(lowerCur);\n return u1 < l0 || l1 > u0;\n}\n\n/**\n * Whether two adjacent upper voices are spaced more than a maximum apart\n * (commonly an octave). Bass-to-tenor spacing is conventionally exempt, so this\n * is meant for the upper voice pairs.\n *\n * @param upper The higher voice.\n * @param lower The lower voice.\n * @param maxSemitones Maximum allowed spacing in semitones (default an octave).\n * @category Voicing & Counterpoint\n */\nexport function exceedsSpacing(upper: Note, lower: Note, maxSemitones?: number): boolean;\nexport function exceedsSpacing(upper: number, lower: number, maxSemitones?: number): boolean;\nexport function exceedsSpacing(\n upper: number | Note,\n lower: number | Note,\n maxSemitones = 12,\n): boolean {\n // A limit that is not a number compares false against every distance, so the\n // spacing rule answers \"within the limit\" for a texture nobody could play.\n assertFiniteNumber(maxSemitones, 'maxSemitones');\n return Math.abs(pitchOf(upper) - pitchOf(lower)) > maxSemitones;\n}\n\n/**\n * Whether a leading tone resolves correctly upward to the tonic.\n *\n * @param prev The leading-tone note.\n * @param cur The following note.\n * @param key Key context supplying the tonic.\n * @returns True when `prev` is the leading tone and `cur` is the tonic a step above.\n * @category Voicing & Counterpoint\n */\nexport function isLeadingToneResolution(prev: Note, cur: Note, key: KeyLike): boolean;\nexport function isLeadingToneResolution(prev: number, cur: number, key: KeyLike): boolean;\nexport function isLeadingToneResolution(\n prev: number | Note,\n cur: number | Note,\n key: KeyLike,\n): boolean {\n const prevPitch = pitchOf(prev);\n const curPitch = pitchOf(cur);\n const tonic = pitchClass(toKeyScale(key).rootPc);\n const leading = (tonic + 11) % 12;\n if (pitchClass(prevPitch) !== leading || pitchClass(curPitch) !== tonic) {\n return false;\n }\n return curPitch > prevPitch && curPitch - prevPitch <= 2;\n}\n\nexport type { VoiceIndependenceOptions, VoiceIndependenceReport } from './independence.js';\nexport { voiceIndependence } from './independence.js';\nexport { classifySpelledInterval } from './internal.js';\n","import { InvalidInputError } from '../../core/errors/index.js';\nimport { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport {\n assertFiniteNumber,\n assertGenerationBudget,\n assertMidiPitch,\n assertOneOf,\n assertOptions,\n assertRecord,\n describeRejected,\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 * Style policy: `strict` counterpoint vs. `pop` voice leading.\n *\n * A profile says two things. It sets the severity of each reason\n * {@link evaluateSafety} finds, which decides what is rejected outright, and it\n * carries the {@link ProfileWeights} that rank whatever survives — because most\n * of what separates the two styles is preference rather than prohibition.\n *\n * @category Arrangement & Analysis\n */\nexport type SafetyProfile = 'strict' | 'pop';\n\n/**\n * How a profile ranks the candidates its severity table has let through.\n *\n * Each field is a score contribution, added when the situation it names occurs;\n * a negative value is a penalty. The scale is arbitrary but shared, so only the\n * relative sizes matter. Nothing here can make an unsafe pitch usable: rejection\n * is the severity table's business, and these weights only order what is left.\n *\n * @category Arrangement & Analysis\n */\nexport type ProfileWeights = {\n /** A third or a sixth against the other voice. */\n imperfectConsonance: number;\n /** A unison, fifth or octave against the other voice. */\n perfectConsonance: number;\n /** A second, seventh or tritone against the other voice. */\n dissonance: number;\n /** The candidate belongs to the sounding chord. */\n chordTone: number;\n /** The two voices move in opposite directions. */\n contraryMotion: number;\n /** One voice holds while the other moves. */\n obliqueMotion: number;\n /** Both voices move the same way into a different interval. */\n similarMotion: number;\n /** Both voices move the same way, keeping the same imperfect consonance. */\n parallelImperfect: number;\n /** The candidate is a step from the previous note of its own line. */\n stepwise: number;\n /** Per semitone of melodic movement, so a leap costs more than a step. */\n melodicDistance: number;\n /** The move is a leap strict counterpoint forbids outright. */\n forbiddenLeap: number;\n /** Per semitone away from the centre of the line's register. */\n registerDrift: number;\n};\n\n/**\n * The weight table each profile ranks candidates by.\n *\n * `strict` states the sixteenth-century preferences: contrary motion is worth\n * seeking, similar motion is worth avoiding, imperfect consonances carry the\n * line, and a run of parallel thirds earns nothing. `pop` inverts two of those.\n * Parallel thirds and sixths are the harmony line an arranger writes on purpose,\n * a pedal or a held unison under a moving lead is a texture rather than a fault,\n * and similar motion is simply how two parts often move together — while a bass\n * moving against the lead is still worth preferring where it is available.\n *\n * @category Arrangement & Analysis\n */\nexport const PROFILE_WEIGHTS: Readonly<Record<SafetyProfile, Readonly<ProfileWeights>>> =\n Object.freeze({\n strict: Object.freeze({\n imperfectConsonance: 2,\n perfectConsonance: 0.5,\n dissonance: -2,\n chordTone: 1,\n contraryMotion: 2,\n obliqueMotion: 1,\n similarMotion: -1,\n parallelImperfect: 0,\n stepwise: 1,\n melodicDistance: -0.3,\n forbiddenLeap: -6,\n registerDrift: -0.02,\n }),\n pop: Object.freeze({\n imperfectConsonance: 2.5,\n perfectConsonance: 1,\n dissonance: -2,\n chordTone: 1.5,\n contraryMotion: 1.5,\n obliqueMotion: 1.25,\n similarMotion: 0,\n parallelImperfect: 2,\n stepwise: 0.75,\n melodicDistance: -0.2,\n forbiddenLeap: -6,\n registerDrift: -0.02,\n }),\n });\n\n/**\n * Every profile name, read from the weight table so an entrance check and the\n * table it guards cannot drift apart.\n */\nconst SAFETY_PROFILES = Object.keys(PROFILE_WEIGHTS) as SafetyProfile[];\n\n/**\n * Resolve a profile to the weights that rank its candidates, with per-field\n * overrides applied on top.\n *\n * @param profile The profile whose table to start from.\n * @param overrides Fields to replace; anything absent keeps the profile's value.\n * A field written as an explicit `undefined` is absent, not a replacement.\n * @returns The complete weight table to score with.\n * @throws If `profile` is not one of the profile names, or an override is not a\n * finite number ({@link InvalidInputError}). A misspelled profile scored under\n * a default table would rank candidates by a style the caller did not ask for,\n * and a single non-finite weight poisons every candidate's score, which turns\n * the ranking into the order the candidates happened to be enumerated in.\n * @example\n * ```ts\n * import { profileWeights } from '@libraz/libcantus';\n * profileWeights('pop', { parallelImperfect: 4 }).contraryMotion; // 1.5 — the rest is untouched\n * ```\n * @category Arrangement & Analysis\n */\nexport function profileWeights(\n profile: SafetyProfile,\n overrides?: Partial<ProfileWeights>,\n): ProfileWeights {\n const base = PROFILE_WEIGHTS[assertOneOf(profile, SAFETY_PROFILES, 'safety profile')];\n if (overrides === undefined) {\n return { ...base };\n }\n assertRecord<Partial<ProfileWeights>>(overrides, 'overrides');\n const weights: ProfileWeights = { ...base };\n for (const [field, value] of Object.entries(overrides)) {\n if (value === undefined) {\n continue;\n }\n weights[field as keyof ProfileWeights] = assertFiniteNumber(value, `weight ${field}`);\n }\n return weights;\n}\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// An enum is emitted as a plain object carrying both the name-to-value and the\n// value-to-name mapping, so without this it is a public table a consumer could\n// write to — and a second consumer in the same process would read the edit.\nObject.freeze(NoteSafety);\n\n/**\n * Bit flags describing why a pitch received its verdict.\n *\n * Several flags can describe one pitch, so test the combination rather than a\n * flag alone: an avoid eleventh carries `Tension` as well as `AvoidNote`, and a\n * usable tension is `Tension` without `AvoidNote`. `ChordTone`, `MinorSecond`\n * and `MajorSeventh` are informational and never lower the verdict on their\n * own; every other flag sets at least a warning, and the profile decides how\n * far past that it goes.\n *\n * @category Arrangement & Analysis\n */\nexport enum ReasonFlag {\n /** The candidate is a member of the chord. Informational. */\n ChordTone = 1 << 0,\n /**\n * The candidate is a ninth, eleventh or thirteenth above the root, not a chord\n * tone. Also set alongside `AvoidNote` when the avoid note is the eleventh.\n */\n Tension = 1 << 1,\n /**\n * The eleventh over a chord with a major third and no fourth, or the major\n * seventh over a dominant. Always set with `NeedsResolution`, and with\n * `Tension` for the eleventh.\n */\n AvoidNote = 1 << 2,\n /** The candidate is in the key but neither a chord tone nor a tension. */\n ScaleTone = 1 << 3,\n /** The candidate is outside the key and not a chord tone. */\n NonScale = 1 << 4,\n /** The candidate lies outside `vocalLow`..`vocalHigh`. */\n OutOfRange = 1 << 5,\n /** The candidate, not itself a chord tone, is a tritone from one. */\n Tritone = 1 << 6,\n /**\n * The move from `prevPitch` is a forbidden melodic leap as\n * {@link isForbiddenMelodicLeap} defines it: a tritone, either seventh, or\n * anything wider than an octave. An octave itself is not. The profile sets\n * the severity, not the set of leaps. Set with `MelodicTritone` on a tritone\n * and with `MajorSeventh` on a major seventh.\n */\n LargeLeap = 1 << 7,\n /** The move from `prevPitch` reduces to a semitone, the minor ninth included. Informational. */\n MinorSecond = 1 << 8,\n /** The move from `prevPitch` reduces to a major seventh; always with `LargeLeap`. Informational. */\n MajorSeventh = 1 << 9,\n /**\n * On a strong beat, the candidate is dissonant against at least one of\n * `otherVoices`, apart from the tritone or seventh two chord tones spell. A\n * perfect fourth counts only when one of the two is the lowest sounding voice,\n * and under `pop` not when both are chord tones — an inverted chord.\n */\n VerticalDissonance = 1 << 10,\n /** The candidate moves in parallel perfect intervals with at least one of `otherVoices`. */\n ParallelPerfect = 1 << 11,\n /**\n * The candidate and another voice, the two outer voices of the texture, reach\n * a perfect interval by similar motion with the upper one leaping.\n */\n HiddenParallel = 1 << 12,\n /** The candidate crosses at least one of `otherVoices` since the previous step. */\n VoiceCrossing = 1 << 13,\n /**\n * A held pitch, consonant with a voice on the previous step, now dissonant\n * against it. Always with `VerticalDissonance`.\n */\n Suspension = 1 << 14,\n /** The candidate should move by step to `resolveTo`. Set with `AvoidNote`. */\n NeedsResolution = 1 << 15,\n /** The move from `prevPitch` reduces to a tritone; always with `LargeLeap`. */\n MelodicTritone = 1 << 16,\n}\n\nObject.freeze(ReasonFlag);\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 reasons one of `otherVoices` contributes to a verdict.\n *\n * @category Arrangement & Analysis\n */\nexport type SafetyPartner = {\n /** The voice's index in {@link SafetyQuery.otherVoices}. */\n index: number;\n /**\n * The pairwise flags this voice raises: `VerticalDissonance`, `Suspension`,\n * `ParallelPerfect`, `HiddenParallel` and `VoiceCrossing`.\n */\n reasons: 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 /**\n * The voices behind the pairwise flags in `reasons`, in `otherVoices` order,\n * when {@link EvaluateSafetyOptions.partners} asked for them and any voice\n * raised one. Their `reasons` together are exactly the pairwise flags set.\n */\n partners?: SafetyPartner[];\n rationale?: string;\n};\n\n/** Semitone radius searched around a rejected candidate for safe alternatives. */\nexport const SUGGESTION_WINDOW = 12;\n/** Maximum number of alternative pitches returned in `SafetyResult.suggestions`. */\nexport const MAX_SUGGESTIONS = 3;\n\n/** Canonical minimum verdict contributed by each non-informational reason. */\nexport const REASON_SEVERITY: ReadonlyArray<\n readonly [flag: ReasonFlag, pop: NoteSafety, strict: NoteSafety]\n> = [\n [ReasonFlag.Tension, NoteSafety.Warning, NoteSafety.Warning],\n [ReasonFlag.AvoidNote, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.ScaleTone, NoteSafety.Warning, NoteSafety.Warning],\n [ReasonFlag.NonScale, NoteSafety.Dissonant, NoteSafety.Dissonant],\n [ReasonFlag.OutOfRange, NoteSafety.Warning, NoteSafety.Warning],\n [ReasonFlag.Tritone, NoteSafety.Dissonant, NoteSafety.Dissonant],\n [ReasonFlag.MelodicTritone, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.LargeLeap, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.VerticalDissonance, NoteSafety.Dissonant, NoteSafety.Dissonant],\n [ReasonFlag.ParallelPerfect, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.HiddenParallel, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.VoiceCrossing, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.Suspension, NoteSafety.Dissonant, NoteSafety.Dissonant],\n [ReasonFlag.NeedsResolution, NoteSafety.Warning, NoteSafety.Dissonant],\n];\n\nfunction minimumSafetyForReasons(reasons: number, profile: SafetyProfile): NoteSafety {\n let minimum = NoteSafety.Safe;\n const severityIndex = profile === 'strict' ? 2 : 1;\n for (const entry of REASON_SEVERITY) {\n if (reasons & entry[0]) {\n minimum = Math.max(minimum, entry[severityIndex]) as NoteSafety;\n }\n }\n return minimum;\n}\n\n/** Whether the perfect fourth is an avoid note over this chord. */\nfunction avoidsFourth(chord: Chord): boolean {\n return chord.intervals.includes(4) && !chord.intervals.includes(5);\n}\n\n/** Whether the major seventh is an avoid note over this chord (dominant). */\nfunction avoidsMajorSeventh(chord: Chord): boolean {\n return chord.intervals.includes(4) && chord.intervals.includes(10);\n}\n\n/**\n * Whether a dissonance between two chord tones is one the chord is built from.\n *\n * A seventh chord states a seventh against its root and a dominant states the\n * tritone between its third and seventh; those intervals are the chord's\n * identity, not a clash to be prepared and resolved. A second between two chord\n * tones (a ninth voiced against the root, say) is a spacing problem rather than\n * a property of the harmony, so it stays reportable.\n */\nfunction isStructuralChordDissonance(a: number, b: number, chord: Chord): boolean {\n const rawClass = pitchClass(Math.abs(a - b));\n const aOffset = intervalAboveRoot(a, chord);\n const bOffset = intervalAboveRoot(b, chord);\n const rootSeventh =\n (aOffset === 0 && (bOffset === 10 || bOffset === 11)) ||\n (bOffset === 0 && (aOffset === 10 || aOffset === 11));\n if (rawClass !== 6 && !rootSeventh) {\n return false;\n }\n return isChordMember(a, chord) && isChordMember(b, chord);\n}\n\n/** Nearest chord tone at or below the candidate, within two semitones. */\nfunction stepResolution(pitch: number, chord: Chord): number | undefined {\n for (let delta = 1; delta <= 2; delta += 1) {\n if (isChordMember(pitch - delta, chord)) {\n return pitch - delta;\n }\n }\n return undefined;\n}\n\n/**\n * Evaluate the safety of placing a candidate pitch in a voice.\n *\n * Reasons are merged from harmonic context (chord tone / tension / avoid / scale)\n * and counterpoint (vertical dissonance, parallels, voice crossing). The `strict`\n * profile treats avoid notes and parallels as dissonant; `pop` treats them as\n * warnings. The final verdict is the worst severity among the reasons.\n *\n * @param q The candidate and its harmonic/voice-leading context.\n * @returns The verdict, reason bitmask, and optional resolution guidance.\n * @throws If the profile is not one of the profile names, or if any pitch in\n * the query is not a MIDI pitch in 0..127 ({@link InvalidInputError}).\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 assertMidiPitch(q.candidatePitch, 'candidatePitch');\n const options = assertOptions(opts, 'opts');\n return evaluateInternal(q, options.suggestions ?? true, options.partners ?? false);\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 * Whether to report which of `otherVoices` raised each pairwise flag, as\n * {@link SafetyResult.partners}. The bitmask alone says a parallel fifth is\n * there; this says against which voice.\n *\n * @defaultValue false\n */\n partners?: boolean;\n};\n\n/**\n * Validate the profile and every pitch a public safety entry point reads.\n *\n * Each field named here is a MIDI pitch, and the suggestion search already\n * refuses to leave 0..127, so the same bound is the entrance contract: a pitch\n * outside it cannot be written to a note event, and a profile outside the union\n * would be scored by whichever table a lookup happened to fall back on.\n */\nfunction assertSafetyContext(q: Omit<SafetyQuery, 'candidatePitch'>): void {\n assertRecord(q, 'safety query');\n assertOneOf(q.profile, SAFETY_PROFILES, 'safety profile');\n if (q.prevPitch !== undefined) assertMidiPitch(q.prevPitch, 'prevPitch');\n if (q.vocalLow !== undefined) assertMidiPitch(q.vocalLow, 'vocalLow');\n if (q.vocalHigh !== undefined) assertMidiPitch(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 if (!Array.isArray(q.otherVoices)) {\n throw new InvalidInputError(\n `otherVoices must be an array; received ${describeRejected(q.otherVoices)}`,\n );\n }\n assertGenerationBudget(q.otherVoices.length, 'other voices');\n for (let index = 0; index < q.otherVoices.length; index += 1) {\n const voice = assertRecord<VoiceSnapshot>(q.otherVoices[index], `otherVoices[${index}]`);\n assertMidiPitch(voice.pitch, `otherVoices[${index}].pitch`);\n if (voice.prevPitch !== undefined) {\n assertMidiPitch(voice.prevPitch, `otherVoices[${index}].prevPitch`);\n }\n }\n // No harmony is `null`; anything else is read as a chord wherever the verdict asks of it.\n if (q.chord !== null) {\n assertRecord(q.chord, 'chord');\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 * @param collectPartners Whether to record the pairwise flags per other voice.\n * @returns The verdict, reason bitmask, and optional guidance.\n */\nfunction evaluateInternal(\n q: SafetyQuery,\n collectSuggestions: boolean,\n collectPartners: boolean,\n): SafetyResult {\n let reasons = 0;\n let level = NoteSafety.Safe;\n let resolveTo: number | undefined;\n const raise = (l: NoteSafety) => {\n if (l > level) {\n level = l;\n }\n };\n const avoidSeverity = q.profile === 'strict' ? NoteSafety.Dissonant : NoteSafety.Warning;\n const parallelSeverity = avoidSeverity;\n const pitch = q.candidatePitch;\n\n if (\n (q.vocalLow !== undefined && pitch < q.vocalLow) ||\n (q.vocalHigh !== undefined && pitch > q.vocalHigh)\n ) {\n reasons |= ReasonFlag.OutOfRange;\n raise(NoteSafety.Warning);\n }\n\n const inScale = isScaleTone(pitch, q.key);\n const chord = q.chord;\n\n // A chromatic avoid note remains chromatic. The avoid-note branch below\n // refines the resolution guidance, but must not erase the key relationship\n // or downgrade a non-scale pitch to a merely stylistic warning.\n if (!inScale && (chord === null || !isChordMember(pitch, chord))) {\n reasons |= ReasonFlag.NonScale;\n raise(NoteSafety.Dissonant);\n }\n\n if (chord && isChordMember(pitch, chord)) {\n reasons |= ReasonFlag.ChordTone;\n } else if (chord) {\n const ic = intervalAboveRoot(pitch, chord);\n const isTension = ic === 2 || ic === 5 || ic === 9;\n const avoid = (ic === 5 && avoidsFourth(chord)) || (ic === 11 && avoidsMajorSeventh(chord));\n if (avoid) {\n reasons |= ReasonFlag.AvoidNote;\n if (isTension) {\n reasons |= ReasonFlag.Tension;\n }\n reasons |= ReasonFlag.NeedsResolution;\n resolveTo = stepResolution(pitch, chord);\n raise(avoidSeverity);\n } else if (isTension && inScale) {\n reasons |= ReasonFlag.Tension;\n raise(NoteSafety.Warning);\n } else if (inScale) {\n reasons |= ReasonFlag.ScaleTone;\n raise(NoteSafety.Warning);\n }\n } else if (inScale) {\n reasons |= ReasonFlag.ScaleTone;\n raise(NoteSafety.Warning);\n }\n\n // Harmonic tritone against a chord tone. A chord tone is never flagged for a\n // tritone its own chord spells: the interval is the chord's identity, not a\n // clash with it. This covers dominant sevenths (third against seventh) and\n // equally the diminished, half-diminished, diminished-seventh and minor-sixth\n // families, where the tritone falls between two chord tones as well.\n if (chord && !isChordMember(pitch, chord)) {\n const ic = intervalAboveRoot(pitch, chord);\n const toneOffsets = chord.intervals.map((i) => ((i % 12) + 12) % 12);\n if (toneOffsets.some((t) => Math.abs(ic - t) === 6)) {\n reasons |= ReasonFlag.Tritone;\n }\n }\n\n if (q.prevPitch !== undefined) {\n // Quality flags reduce the melodic interval to its pitch-class interval so\n // compound intervals are treated uniformly with their simple form. The\n // forbidden-leap flag defers to the counterpoint rule, which preserves the\n // octave boundary (an octave leap is allowed, wider leaps are not).\n const pc = Math.abs(pitch - q.prevPitch) % 12;\n if (pc === 6) {\n reasons |= ReasonFlag.MelodicTritone;\n raise(q.profile === 'strict' ? NoteSafety.Dissonant : NoteSafety.Warning);\n }\n if (isForbiddenMelodicLeap(q.prevPitch, pitch)) {\n reasons |= ReasonFlag.LargeLeap;\n raise(parallelSeverity);\n }\n if (pc === 1) {\n reasons |= ReasonFlag.MinorSecond;\n }\n if (pc === 11) {\n reasons |= ReasonFlag.MajorSeventh;\n }\n }\n\n // The texture's outer voices, as sounding now. With one other voice both\n // voices are outer, which is the two-voice reading.\n let bass = pitch;\n let top = pitch;\n for (const ov of q.otherVoices) {\n bass = Math.min(bass, ov.pitch);\n top = Math.max(top, ov.pitch);\n }\n\n const pairReasons: number[] | undefined = collectPartners\n ? new Array(q.otherVoices.length).fill(0)\n : undefined;\n const flagPair = (index: number, flag: ReasonFlag) => {\n reasons |= flag;\n if (pairReasons !== undefined) {\n pairReasons[index] = (pairReasons[index] ?? 0) | flag;\n }\n };\n\n if (q.strongBeat) {\n const prev = q.prevPitch;\n for (const [index, ov] of q.otherVoices.entries()) {\n // A fourth is dissonant against the bass and consonant between upper voices.\n const againstBass = pitch === bass || ov.pitch === bass;\n if (!createsVerticalDissonance(pitch, ov.pitch, againstBass)) {\n continue;\n }\n if (chord && isStructuralChordDissonance(pitch, ov.pitch, chord)) {\n continue;\n }\n // Pop reads a fourth between two chord tones as the chord in inversion;\n // strict keeps the six-four a dissonance.\n if (\n chord &&\n q.profile === 'pop' &&\n pitchClass(Math.abs(pitch - ov.pitch)) === 5 &&\n isChordMember(pitch, chord) &&\n isChordMember(ov.pitch, chord)\n ) {\n continue;\n }\n flagPair(index, 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, againstBass)\n ) {\n flagPair(index, ReasonFlag.Suspension);\n }\n }\n }\n\n if (q.prevPitch !== undefined) {\n const prev = q.prevPitch;\n for (const [index, ov] of q.otherVoices.entries()) {\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 flagPair(index, ReasonFlag.ParallelPerfect);\n raise(parallelSeverity);\n }\n // A hidden perfect is exposed only between the outer voices; between\n // inner ones the others cover it, as the part-writing check reads it.\n const outerPair =\n (pitch === top && ov.pitch === bass) || (pitch === bass && ov.pitch === top);\n if (outerPair && createsHiddenParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {\n flagPair(index, 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 flagPair(index, 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 if (pairReasons !== undefined) {\n const partners: SafetyPartner[] = [];\n pairReasons.forEach((pair, index) => {\n if (pair !== 0) {\n partners.push({ index, reasons: pair });\n }\n });\n if (partners.length > 0) {\n result.partners = partners;\n }\n }\n return result;\n}\n\n/**\n * Find safe alternative pitches near a rejected candidate.\n *\n * Searches outward from the candidate within `SUGGESTION_WINDOW` semitones,\n * probing the pitch below before the one above at each distance so the result\n * is deterministic and ordered by nearness.\n *\n * @param q The safety context of the rejected candidate.\n * @param candidate The candidate pitch that was not Safe.\n * @returns Up to `MAX_SUGGESTIONS` safe pitches, nearest first.\n */\nfunction findSafeNearby(q: SafetyQuery, candidate: number): number[] {\n const out: number[] = [];\n for (let d = 1; d <= SUGGESTION_WINDOW; d += 1) {\n for (const p of [candidate - d, candidate + d]) {\n if (p < 0 || p > 127) {\n continue;\n }\n if (evaluateInternal({ ...q, candidatePitch: p }, false, 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 */\nexport const REASON_TEXT: ReadonlyArray<readonly [ReasonFlag, string]> = [\n [ReasonFlag.OutOfRange, 'Outside the target vocal range'],\n [\n ReasonFlag.Suspension,\n 'Prepared suspension — a held consonance now dissonant, awaiting resolution',\n ],\n [ReasonFlag.VerticalDissonance, 'Dissonant against a sounding voice on a strong beat'],\n [ReasonFlag.Tritone, 'Tritone against the sounding harmony'],\n [ReasonFlag.MelodicTritone, 'Melodic tritone leap'],\n [ReasonFlag.AvoidNote, 'Avoid note — clashes with a chord tone a semitone away'],\n [ReasonFlag.NonScale, 'Outside the key'],\n [ReasonFlag.ParallelPerfect, 'Parallel perfect interval with another voice'],\n [ReasonFlag.HiddenParallel, 'Hidden perfect interval by similar motion'],\n [ReasonFlag.VoiceCrossing, 'Crosses another voice'],\n [ReasonFlag.LargeLeap, 'Forbidden melodic leap — a seventh or wider than an octave'],\n [ReasonFlag.NeedsResolution, 'Dissonance awaiting a stepwise resolution'],\n [ReasonFlag.Tension, 'Chord tension'],\n [ReasonFlag.ScaleTone, 'Scale tone, not in the chord'],\n [ReasonFlag.MinorSecond, 'Melodic semitone'],\n [ReasonFlag.MajorSeventh, 'Melodic major seventh'],\n [ReasonFlag.ChordTone, 'Chord tone'],\n];\n\n/** The severity a single flag carries under a profile; Safe when purely informational. */\nfunction severityOfReason(flag: ReasonFlag, profile: SafetyProfile): NoteSafety {\n const entry = REASON_SEVERITY.find(([candidate]) => candidate === flag);\n return entry === undefined ? NoteSafety.Safe : entry[profile === 'strict' ? 2 : 1];\n}\n\n/**\n * Build a short human explanation from the reason flags.\n *\n * The flag driving the verdict wins: the one whose severity under this profile\n * is highest, ties going to the more specific wording. Explaining a rejected\n * pitch by an informational flag it also carries would tell the caller nothing\n * about what to fix.\n */\nfunction describe(reasons: number, q: SafetyQuery): string {\n let best: string | undefined;\n let bestSeverity = -1;\n for (const [flag, text] of REASON_TEXT) {\n if (!(reasons & flag)) {\n continue;\n }\n const severity = severityOfReason(flag, q.profile);\n if (severity > bestSeverity) {\n bestSeverity = severity;\n best = text;\n }\n }\n if (best !== undefined) {\n return best;\n }\n return q.chord ? 'Placeable' : 'No chord context';\n}\n\n/**\n * Enumerate placeable pitches in a range, chord tones first, descending.\n *\n * @param q The safety context minus the candidate pitch.\n * @param pitchLow Lowest MIDI pitch to consider (inclusive).\n * @param pitchHigh Highest MIDI pitch to consider (inclusive).\n * @returns Placeable pitches (non-dissonant), chord tones before others, each group descending.\n * @throws If either bound is not a MIDI pitch in 0..127, reversed, or exceeds\n * the generation budget, or if the context names an unknown profile.\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 // The bounds are MIDI pitches, and so is everything enumerated between them:\n // a pitch outside 0..127 cannot be written to a note event, so it is refused\n // here rather than handed to an arranger that has nowhere to put it.\n assertMidiPitch(pitchLow, 'pitchLow');\n assertMidiPitch(pitchHigh, 'pitchHigh');\n if (pitchLow > pitchHigh) {\n throw new InvalidInputError(\n `pitchLow must not exceed pitchHigh; received ${pitchLow} > ${pitchHigh}`,\n );\n }\n assertGenerationBudget(pitchHigh - pitchLow + 1, 'safe pitch candidates');\n const low = Math.max(pitchLow, q.vocalLow ?? pitchLow);\n const high = Math.min(pitchHigh, q.vocalHigh ?? pitchHigh);\n for (let pitch = high; pitch >= low; pitch -= 1) {\n const result = evaluateInternal({ ...q, candidatePitch: pitch }, false, 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","import {\n formatNote,\n type Note,\n type NoteNameSystem,\n noteToPitchClass,\n parseNote,\n tryParseNote,\n} from '../../core/pitch/index.js';\nimport { maxNoteNameLength } from '../../core/pitch/naming.js';\nimport type {\n Alteration,\n AlteredDegree,\n Chord,\n ChordBase,\n ChordQuality,\n ChordSeventh,\n ChordSpec,\n} from '../chord/index.js';\nimport { chordFromSpec } from '../chord/index.js';\nimport { chordSpecForQuality } from '../chord/spec.js';\n\n/**\n * What a chord symbol is allowed to say, and how it is read.\n *\n * The reading side of the unit, kept apart from the writing side because the\n * two are not mirror images: a symbol is read far more loosely than it is\n * written. `-7`, `m7` and `min7` all read as a minor seventh; exactly one of\n * them is written, and that choice lives in `suffix.ts`.\n */\n\n/**\n * Matches an English chord root: a letter A-G (case-insensitive, like\n * {@link parseNote}) with an optional single-direction accidental.\n */\nconst ROOT_RE = /^[A-Ga-g](?:#{1,2}|b{1,2}|x)?/;\n\n/** Matches a bare English note usable as a slash-chord bass (no octave). */\nconst BASS_RE = /^[A-Ga-g](?:#{1,2}|b{1,2}|x)?$/;\n\n/**\n * The system a symbol is read and written in when the caller names none.\n *\n * A German reading is something a caller asks for, never something inferred:\n * see this module's own documentation for why chord symbols do not detect.\n */\nexport const DEFAULT_SYSTEM: NoteNameSystem = 'english';\n\n/** A note every system can write, used only to validate a requested system. */\nconst SYSTEM_PROBE: Note = { letter: 0, alter: 0 };\n\n/** Recognized quality suffixes, keyed by their exact lead-sheet spelling. */\nexport const QUALITY_MAP: Record<string, ChordQuality> = {\n '': 'maj',\n maj: 'maj',\n m: 'min',\n min: 'min',\n '-': 'min',\n dim: 'dim',\n '°': 'dim',\n aug: 'aug',\n '+': 'aug',\n maj7: 'maj7',\n M7: 'maj7',\n Δ: 'maj7',\n Δ7: 'maj7',\n m7: 'min7',\n min7: 'min7',\n '-7': 'min7',\n '7': 'dom7',\n dim7: 'dim7',\n '°7': 'dim7',\n m7b5: 'm7b5',\n ø: 'm7b5',\n ø7: 'm7b5',\n min7b5: 'm7b5',\n mMaj7: 'minMaj7',\n mM7: 'minMaj7',\n minMaj7: 'minMaj7',\n mMaj9: 'minMaj9',\n mM9: 'minMaj9',\n minMaj9: 'minMaj9',\n mMaj11: 'minMaj11',\n mM11: 'minMaj11',\n minMaj11: 'minMaj11',\n mMaj13: 'minMaj13',\n mM13: 'minMaj13',\n minMaj13: 'minMaj13',\n aug7: 'aug7',\n '+7': 'aug7',\n '7#5': 'aug7',\n augMaj7: 'augMaj7',\n augM7: 'augMaj7',\n '+maj7': 'augMaj7',\n '+M7': 'augMaj7',\n majb5: 'majb5',\n 'maj(b5)': 'majb5',\n '(b5)': 'majb5',\n '6': '6',\n m6: 'min6',\n min6: 'min6',\n '6/9': '6/9',\n '69': '6/9',\n sus2: 'sus2',\n sus: 'sus4',\n sus4: 'sus4',\n add9: 'add9',\n add11: 'add11',\n maj9: 'maj9',\n M9: 'maj9',\n m9: 'min9',\n min9: 'min9',\n '9': 'dom9',\n '7b9': '7b9',\n '7#9': '7#9',\n '7#11': '7#11',\n '7b13': '7b13',\n '11': '11',\n '13': '13',\n '5': '5',\n '7sus4': '7sus4',\n '7sus': '7sus4',\n '9sus4': '11',\n '9sus': '11',\n '7b5': '7b5',\n '7alt': '7alt',\n alt: '7alt',\n '13b9': '13b9',\n maj13: 'maj13',\n M13: 'maj13',\n 'maj7#11': 'maj7#11',\n 'M7#11': 'maj7#11',\n m11: 'min11',\n min11: 'min11',\n m13: 'min13',\n min13: 'min13',\n madd9: 'minAdd9',\n 'min(add9)': 'minAdd9',\n 'm(add9)': 'minAdd9',\n '6add9': '6/9',\n 'm6/9': 'min6/9',\n m69: 'min6/9',\n 'min6/9': 'min6/9',\n // ASCII stand-ins for the degree and half-diminished glyphs, which most\n // lead sheets and chord-chart text files use instead of the typographic ones.\n o: 'dim',\n o7: 'dim7',\n h: 'm7b5',\n h7: 'm7b5',\n};\n\n/**\n * The quality a suffix names outright, or undefined when it names none.\n *\n * The own-property check is what stops `'constructor'` and `'toString'` from\n * resolving through `Object.prototype` and reaching the chord builder with a\n * quality it has no interval template for.\n */\nfunction knownQuality(suffix: string): ChordQuality | undefined {\n return Object.hasOwn(QUALITY_MAP, suffix) ? QUALITY_MAP[suffix] : undefined;\n}\n\n/** How a base is written where a symbol opens with one. */\nexport const BASE_TOKENS: readonly (readonly [string, ChordBase | 'major'])[] = [\n ['sus2', 'sus2'],\n ['sus4', 'sus4'],\n ['sus', 'sus4'],\n ['min', 'min'],\n ['dim', 'dim'],\n ['aug', 'aug'],\n ['maj', 'major'],\n ['Maj', 'major'],\n ['m', 'min'],\n ['-', 'min'],\n ['o', 'dim'],\n ['°', 'dim'],\n ['+', 'aug'],\n ['M', 'major'],\n ['Δ', 'major'],\n ['^', 'major'],\n];\n\n/**\n * The glyphs that name a base and the seventh over it in one character.\n *\n * The half-diminished glyph is a minor seventh over a diminished triad, not a\n * diminished seventh, so it is read as the pair it stands for: reading it as a\n * plain diminished base would let `Cø9` stack the diminished seventh the base\n * implies and print a chord the glyph does not name.\n */\nexport const COMPOUND_TOKENS: readonly (readonly [\n string,\n { base: ChordBase; seventh: ChordSeventh },\n])[] = [\n ['ø', { base: 'dim', seventh: 'min7' }],\n ['h', { base: 'dim', seventh: 'min7' }],\n];\n\n/**\n * The markers that say a seventh is major.\n *\n * The glyphs say it on their own — `CΔ` is a major seventh — while the words do\n * not: `Cmaj` is the plain triad, and `CmMaj` only reads as a major seventh\n * because the base already took the letter before it.\n */\nexport const MAJOR_MARKERS: readonly (readonly [string, 'word' | 'glyph'])[] = [\n ['maj', 'word'],\n ['Maj', 'word'],\n ['M', 'word'],\n ['Δ', 'glyph'],\n ['^', 'glyph'],\n];\n\n/** The extension numbers a symbol stacks a chord up to. */\nexport const EXTENSION_NUMBERS = [13, 11, 9, 7, 6] as const;\n\n/** The degree each `add` figure names, so `add2` and `add9` are one tone. */\nexport const ADDED_DEGREES: Record<string, number> = {\n '2': 9,\n '4': 11,\n '6': 6,\n '9': 9,\n '11': 11,\n '13': 13,\n};\n\n/** The degrees a symbol can write an accidental against. */\nexport const ALTERABLE: Record<string, AlteredDegree> = { '5': 5, '9': 9, '11': 11, '13': 13 };\n\n/** The degrees a symbol can ask a chord to leave out. */\nexport const OMITTABLE: Record<string, number> = { '1': 1, '3': 3, '5': 5 };\n\n/** How a suspension is written as a figure. */\nexport const SUSPENSION_TOKENS: readonly (readonly [string, ChordBase])[] = [\n ['sus2', 'sus2'],\n ['sus4', 'sus4'],\n ['sus', 'sus4'],\n];\n\n/** The words that leave a degree out, with the length of each. */\nexport const OMISSION_TOKENS: readonly (readonly [string, number])[] = [\n ['omit', 4],\n ['no', 2],\n];\n\n/** The accidentals a figure is written with, and the alteration each names. */\nexport const ACCIDENTAL_TOKENS: readonly (readonly [string, -1 | 1])[] = [\n ['b', -1],\n ['#', 1],\n];\n\n/** A chord read token by token, before it settles into a {@link ChordSpec}. */\ntype SpecDraft = {\n base: ChordBase;\n seventh?: ChordSeventh;\n /**\n * The alterations read so far, keyed by degree *and* accidental so that the\n * flat ninth and the raised ninth of an altered dominant are two tones rather\n * than two readings of one.\n */\n alterations: Map<string, Alteration>;\n additions: Set<number>;\n omissions: Set<number>;\n /**\n * The seventh the opening glyph names over its base, where it names one. An\n * extension number stacks up to this seventh instead of the one the base\n * implies, so the minor seventh of a half-diminished chord survives `ø9`.\n */\n seventhOverBase?: ChordSeventh;\n /** Degrees the extension number implied, which a written figure replaces. */\n implied: Set<AlteredDegree>;\n /** Whether a `maj`/`M`/`Δ` marker has already claimed the seventh. */\n major: boolean;\n};\n\n/** The identity of one alteration: which degree, and with which accidental. */\nfunction alterationKey(degree: AlteredDegree, alter: -1 | 0 | 1): string {\n return `${degree}:${alter}`;\n}\n\n/**\n * Record an alteration the symbol writes out.\n *\n * A written figure states the degree outright, so it replaces the unaltered\n * tone the extension number implied — `C11b9` flattens the ninth of the stack\n * rather than sounding both — while two written figures against one degree both\n * stand, since that is what a chart asking for them means.\n */\nfunction writeAlteration(draft: SpecDraft, degree: AlteredDegree, alter: -1 | 0 | 1): void {\n if (draft.implied.delete(degree)) {\n draft.alterations.delete(alterationKey(degree, 0));\n }\n draft.alterations.set(alterationKey(degree, alter), { degree, alter });\n}\n\n/** Record an unaltered degree the extension number stacks up to. */\nfunction implyAlteration(draft: SpecDraft, degree: AlteredDegree): void {\n draft.implied.add(degree);\n draft.alterations.set(alterationKey(degree, 0), { degree, alter: 0 });\n}\n\n/** Whether the draft already carries an alteration of `degree`. */\nfunction altersDegree(draft: SpecDraft, degree: AlteredDegree): boolean {\n for (const alteration of draft.alterations.values()) {\n if (alteration.degree === degree) {\n return true;\n }\n }\n return false;\n}\n\n/** The longest token in `table` that `text` carries at `at`. */\nfunction matchToken<T>(\n text: string,\n at: number,\n table: readonly (readonly [string, T])[],\n): { value: T; next: number } | undefined {\n let found: { value: T; next: number } | undefined;\n for (const [token, value] of table) {\n if (text.startsWith(token, at) && (found === undefined || at + token.length > found.next)) {\n found = { value, next: at + token.length };\n }\n }\n return found;\n}\n\n/** The number token at `at`, longest first so 11 and 13 are not read as 1. */\nfunction matchNumber(text: string, at: number): { value: number; next: number } | undefined {\n for (const value of EXTENSION_NUMBERS) {\n const token = String(value);\n if (text.startsWith(token, at)) {\n return { value, next: at + token.length };\n }\n }\n return undefined;\n}\n\n/**\n * Stack the draft up to an extension number.\n *\n * A number names the top of a stack of thirds, not a single tone: `13` is a\n * seventh with a ninth and a thirteenth over it. The eleventh is left out of a\n * thirteenth chord, which is how a chart voices one.\n *\n * The seventh stacked under the number is the major seventh a marker asks for,\n * else the one the opening already named, else the one the base implies.\n *\n * The one place the eleventh's quarrel with the third is settled: over a\n * dominant seventh the eleventh sits a semitone above a major third, so the\n * third gives way to it, as a chart voices an eleventh chord. A minor third is\n * a whole tone below the eleventh and does not clash, and a major seventh puts\n * the chord in the family the dictionaries write out in full, so neither loses\n * its third. The rule is the number's alone, so it does not turn on whichever\n * further figures a symbol writes after it.\n */\nfunction stackTo(draft: SpecDraft, top: number): void {\n if (top === 6) {\n draft.additions.add(6);\n return;\n }\n draft.seventh = draft.major\n ? 'maj7'\n : (draft.seventhOverBase ?? (draft.base === 'dim' ? 'dim7' : 'min7'));\n if (top >= 9) {\n implyAlteration(draft, 9);\n }\n if (top === 11) {\n implyAlteration(draft, 11);\n if (!draft.major && draft.base === 'maj') {\n draft.omissions.add(3);\n }\n }\n if (top === 13) {\n implyAlteration(draft, 13);\n }\n}\n\n/**\n * Apply one figure written after the chord's core, or report that it is none.\n *\n * The same grammar reads a figure written bare (`7b9`) and one written inside\n * brackets (`7(b9,#11)`), so the two spellings of a chart's tensions cannot\n * drift apart.\n */\nfunction applyFigure(draft: SpecDraft, text: string, at: number): number | undefined {\n const base = matchToken(text, at, SUSPENSION_TOKENS);\n if (base !== undefined) {\n draft.base = base.value;\n return base.next;\n }\n if (text.startsWith('alt', at)) {\n // The altered dominant, whichever way it is reached: a raised fifth and a\n // raised ninth over a dominant seventh.\n draft.base = 'aug';\n draft.seventh = 'min7';\n writeAlteration(draft, 9, 1);\n return at + 3;\n }\n if (text.startsWith('add', at)) {\n const degree = matchNumberToken(text, at + 3, ADDED_DEGREES);\n if (degree === undefined) {\n return undefined;\n }\n draft.additions.add(degree.value);\n return degree.next;\n }\n const omitted = matchToken(text, at, OMISSION_TOKENS);\n if (omitted !== undefined) {\n const degree = matchNumberToken(text, omitted.next, OMITTABLE);\n if (degree === undefined) {\n return undefined;\n }\n draft.omissions.add(degree.value);\n return degree.next;\n }\n const accidental = matchToken(text, at, ACCIDENTAL_TOKENS);\n const from = accidental?.next ?? at;\n const degree = matchNumberToken(text, from, ALTERABLE);\n if (degree === undefined) {\n return undefined;\n }\n // A symbol writes its fifth once. Read bare, a `5` would carry no accidental\n // of its own and so would state a perfect fifth over a chord whose fifth the\n // text has already altered — `dim`, `aug`, or a `b5`/`#5` figure before it —\n // silently returning a chord with a fifth the symbol does not spell. It is\n // the suffix that is wrong, not the fifth, so the whole symbol fails rather\n // than one reading of it being chosen.\n if (degree.value === 5 && (accidental === undefined || writesFifth(draft))) {\n return undefined;\n }\n writeAlteration(draft, degree.value, accidental?.value ?? 0);\n return degree.next;\n}\n\n/**\n * Whether the draft's fifth has already been written by the symbol.\n *\n * `dim` and `aug` name a fifth as part of the base and a `b5`/`#5` figure names\n * one outright, so a further fifth figure over either is a second, contradictory\n * spelling of the same tone rather than an addition to the chord.\n */\nfunction writesFifth(draft: SpecDraft): boolean {\n return draft.base === 'dim' || draft.base === 'aug' || altersDegree(draft, 5);\n}\n\n/** The longest degree number in `table` that `text` carries at `at`. */\nfunction matchNumberToken<T>(\n text: string,\n at: number,\n table: Record<string, T>,\n): { value: T; next: number } | undefined {\n const entries = Object.entries(table).map(([token, value]) => [token, value] as const);\n return matchToken(text, at, entries);\n}\n\n/** One way a suffix's opening characters can be read. */\ntype Opening = {\n /** How many characters of the core the opening spends. */\n length: number;\n /** The base it names, where it names one. */\n base?: ChordBase;\n /** The seventh it names over that base, as the half-diminished glyph does. */\n seventh?: ChordSeventh;\n /** Whether it is a major-seventh marker rather than a base. */\n major?: boolean;\n /** Whether that marker is a glyph, which names the seventh on its own. */\n glyph?: boolean;\n};\n\n/**\n * The ways a suffix can open, in the order they are worth trying.\n *\n * Longest first, as elsewhere in this grammar, but the shorter readings are\n * kept rather than discarded: a character that opens a base can equally be the\n * first character of a figure, and only reading the rest of the suffix says\n * which it was. The `o` of `omit3` names a diminished triad in `Co9`, so the\n * base reading is tried first and the bare figure is what is left when it\n * leaves `mit3` unread.\n */\nfunction openings(core: string): (Opening | undefined)[] {\n const found: Opening[] = [];\n for (const [token, value] of COMPOUND_TOKENS) {\n if (core.startsWith(token)) {\n found.push({ length: token.length, base: value.base, seventh: value.seventh });\n }\n }\n for (const [token, value] of BASE_TOKENS) {\n if (!core.startsWith(token)) {\n continue;\n }\n found.push(\n value === 'major'\n ? { length: token.length, major: true, glyph: token === 'Δ' || token === '^' }\n : { length: token.length, base: value },\n );\n }\n found.sort((a, b) => b.length - a.length);\n // Reading no base at all is the last resort, so a suffix that is figures from\n // its first character is still read as the figures it is.\n return [...found, undefined];\n}\n\n/**\n * Read a quality suffix as a chord, or undefined when it is not one.\n *\n * The suffix is read structurally — a base, a marker, an extension number, then\n * any number of figures written bare or in brackets — so a combination of\n * tensions no quality name covers is read as readily as one that has a name.\n * Returning undefined rather than throwing is what lets the caller keep trying\n * shorter roots when a system writes its accidental as an affix.\n *\n * Where the opening is ambiguous every reading of it is tried, so the suffix is\n * refused only when none of them accounts for all of its characters.\n */\nfunction structuralSpec(text: string): ChordSpec | undefined {\n let core = '';\n const groups: string[] = [];\n let inside = false;\n let buffer = '';\n for (const char of text) {\n if (char === '(') {\n if (inside) {\n return undefined;\n }\n inside = true;\n buffer = '';\n } else if (char === ')') {\n if (!inside) {\n return undefined;\n }\n inside = false;\n groups.push(buffer);\n } else if (inside) {\n buffer += char;\n } else {\n core += char;\n }\n }\n if (inside) {\n return undefined;\n }\n for (const opening of openings(core)) {\n const spec = readCore(core, groups, opening);\n if (spec !== undefined) {\n return spec;\n }\n }\n return undefined;\n}\n\n/**\n * Read a suffix's core and bracketed groups under one reading of its opening,\n * or undefined when that reading leaves anything unread.\n *\n * @param core The suffix outside any brackets.\n * @param groups The contents of the bracketed groups, in order.\n * @param opening The reading of the opening characters, or undefined to read\n * the core as figures from its first character.\n */\nfunction readCore(\n core: string,\n groups: readonly string[],\n opening: Opening | undefined,\n): ChordSpec | undefined {\n const draft: SpecDraft = {\n base: 'maj',\n alterations: new Map(),\n additions: new Set(),\n omissions: new Set(),\n implied: new Set(),\n major: false,\n };\n let at = 0;\n let markerGlyph = false;\n let markerAfterBase = false;\n if (opening !== undefined) {\n at = opening.length;\n if (opening.major === true) {\n draft.major = true;\n markerGlyph = opening.glyph === true;\n } else if (opening.base !== undefined) {\n draft.base = opening.base;\n if (opening.seventh !== undefined) {\n draft.seventh = opening.seventh;\n draft.seventhOverBase = opening.seventh;\n }\n }\n }\n if (!draft.major) {\n const marker = matchToken(core, at, MAJOR_MARKERS);\n if (marker !== undefined) {\n draft.major = true;\n markerGlyph = marker.value === 'glyph';\n markerAfterBase = opening !== undefined;\n at = marker.next;\n }\n }\n const number = matchNumber(core, at);\n if (number !== undefined) {\n at = number.next;\n stackTo(draft, number.value);\n } else if (draft.major && (markerGlyph || markerAfterBase)) {\n // A marker with no number after it names the seventh itself, but only where\n // it is not the whole suffix: `Cmaj` is the triad and `CΔ` the seventh.\n draft.seventh = 'maj7';\n }\n while (at < core.length) {\n // Between two figures a '/' separates them, as it already does inside\n // brackets: the added sixth and ninth of a `6/9` are written that way, and\n // so are the tensions a chart writes after one. A leading or trailing slash\n // separates nothing, so it is left to fail as the stray character it is.\n if (core[at] === '/' && at > 0 && at + 1 < core.length) {\n at += 1;\n continue;\n }\n const next = applyFigure(draft, core, at);\n if (next === undefined || next === at) {\n return undefined;\n }\n at = next;\n }\n for (const group of groups) {\n for (const figure of group.split(/[,/\\s]+/)) {\n if (figure.length === 0) {\n continue;\n }\n const next = applyFigure(draft, figure, 0);\n if (next !== figure.length) {\n return undefined;\n }\n }\n }\n return {\n rootPc: 0,\n base: draft.base,\n ...(draft.seventh === undefined ? {} : { seventh: draft.seventh }),\n alterations: [...draft.alterations.values()],\n additions: [...draft.additions],\n omissions: [...draft.omissions],\n };\n}\n\n/**\n * The chord a quality suffix names, or undefined when it names none.\n *\n * A name in the alias table wins, so every spelling a chart already uses keeps\n * the exact reading it had; anything else is read structurally.\n */\nfunction suffixSpec(suffix: string): ChordSpec | undefined {\n const quality = knownQuality(suffix);\n return quality === undefined ? structuralSpec(suffix) : chordSpecForQuality(quality, 0);\n}\n\n/**\n * Reject a notation system the naming layer has no table for.\n *\n * A root is found by trying to read one, and a name a system cannot read is\n * simply not a root, so without this an unknown `system` would be reported as\n * an invalid chord symbol rather than as the invalid option it is.\n */\nfunction assertNamingSystem(system: NoteNameSystem): void {\n formatNote(SYSTEM_PROBE, { system });\n}\n\n/**\n * Read `text` as an octave-less note in `system`, or undefined when it is none.\n *\n * A chord root carries no register: `H7` is a dominant seventh, not the German\n * H in octave 7, so a name that swallowed the quality's digits is refused here\n * instead of being taken for a root.\n */\nfunction bareNote(text: string, system: NoteNameSystem): Note | undefined {\n const read = tryParseNote(text, { system });\n if (!read.ok) {\n return undefined;\n }\n return read.value.octave === undefined ? read.value : undefined;\n}\n\n/** A chord root read off the front of a symbol, and the text left after it. */\ntype RootSplit = { root: Note; rest: string };\n\n/**\n * Every way the front of `text` reads as a chord root, longest root first.\n *\n * An English root is a letter and an optional accidental, which the pattern\n * settles in one step. The other systems write the accidental as an affix that\n * can also open a quality suffix — German `As` is the A flat, but `Asus4` is an\n * A chord — so every prefix a name could fill is offered and the caller keeps\n * the longest one whose remainder it recognizes as a quality.\n *\n * How far those prefixes reach is a property of the system, not of the text: a\n * root is a bare note name, and no system writes one longer than a letter and\n * two accidentals. Offering every prefix of the text instead would make the\n * same symbol cost more to read in one system than in another, and would read a\n * long line of text as many candidate roots when it can hold at most one.\n */\nexport function rootSplits(text: string, system: NoteNameSystem): RootSplit[] {\n if (system === 'english') {\n const match = text.match(ROOT_RE);\n const token = match?.[0];\n return token === undefined\n ? []\n : [{ root: parseNote(token, { system }), rest: text.slice(token.length) }];\n }\n assertNamingSystem(system);\n const splits: RootSplit[] = [];\n const longest = Math.min(text.length, maxNoteNameLength(system));\n for (let end = longest; end > 0; end -= 1) {\n const root = bareNote(text.slice(0, end), system);\n if (root !== undefined) {\n splits.push({ root, rest: text.slice(end) });\n }\n }\n return splits;\n}\n\n/** Read a slash-chord bass, which is a bare note in the symbol's own system. */\nfunction bassNote(text: string, system: NoteNameSystem): Note | undefined {\n if (system === 'english') {\n return BASS_RE.test(text) ? parseNote(text, { system }) : undefined;\n }\n return bareNote(text, system);\n}\n\n/** Build a chord and record the parsed spellings as enharmonic hints. */\nfunction chordFromSymbolParts(root: Note, spec: ChordSpec, bass?: Note): Chord {\n const rooted: ChordSpec = { ...spec, rootPc: noteToPitchClass(root) };\n if (bass !== undefined) {\n rooted.bassPc = noteToPitchClass(bass);\n }\n const chord = chordFromSpec(rooted);\n chord.rootSpelling = { letter: root.letter, alter: root.alter };\n if (bass !== undefined) {\n chord.bassSpelling = { letter: bass.letter, alter: bass.alter };\n }\n return chord;\n}\n\n/**\n * The chord a root and the text after it name, or undefined when that text\n * names no chord this module can read.\n */\nexport function chordAfterRoot(\n root: Note,\n rest: string,\n system: NoteNameSystem,\n): Chord | undefined {\n // Split at the last '/' so the '6/9' quality's own slash never masks a\n // trailing slash bass (e.g. 'C6/9/E').\n const slashIndex = rest.lastIndexOf('/');\n if (slashIndex !== -1) {\n const slashSpec = suffixSpec(rest.slice(0, slashIndex));\n const bass = slashSpec === undefined ? undefined : bassNote(rest.slice(slashIndex + 1), system);\n if (slashSpec !== undefined && bass !== undefined) {\n return chordFromSymbolParts(root, slashSpec, bass);\n }\n }\n const spec = suffixSpec(rest);\n return spec === undefined ? undefined : chordFromSymbolParts(root, spec);\n}\n","import type {\n Alteration,\n ChordBase,\n ChordQuality,\n ChordSeventh,\n ChordSpec,\n} from '../chord/index.js';\nimport { chordQualities } from '../chord/index.js';\nimport { chordSpecForQuality, exactChordSpecQuality } from '../chord/spec.js';\n\n/**\n * The suffix a chord symbol writes after its root.\n *\n * The other direction from the grammar: the grammar reads a suffix into a spec,\n * this writes a spec back out as one. They are kept apart because a symbol is\n * read far more loosely than it is written — `-7`, `m7` and `min7` all read as\n * a minor seventh, and exactly one of them is written.\n */\n\n/** Canonical lead-sheet suffix emitted for each chord quality when formatting. */\nexport const CANONICAL_SUFFIX: Record<ChordQuality, string> = {\n maj: '',\n min: 'm',\n dim: 'dim',\n aug: 'aug',\n maj7: 'maj7',\n min7: 'm7',\n dom7: '7',\n dim7: 'dim7',\n m7b5: 'm7b5',\n minMaj7: 'mMaj7',\n minMaj9: 'mMaj9',\n minMaj11: 'mMaj11',\n minMaj13: 'mMaj13',\n aug7: 'aug7',\n augMaj7: 'augMaj7',\n majb5: 'maj(b5)',\n '6': '6',\n min6: 'm6',\n '6/9': '6/9',\n sus2: 'sus2',\n sus4: 'sus4',\n add9: 'add9',\n add11: 'add11',\n maj9: 'maj9',\n min9: 'm9',\n dom9: '9',\n '7b9': '7b9',\n '7#9': '7#9',\n '7#11': '7#11',\n '7b13': '7b13',\n '11': '11',\n '13': '13',\n '5': '5',\n '7sus4': '7sus4',\n '7b5': '7b5',\n '7alt': '7alt',\n '13b9': '13b9',\n maj13: 'maj13',\n 'maj7#11': 'maj7#11',\n min11: 'm11',\n min13: 'm13',\n minAdd9: 'madd9',\n 'min6/9': 'm6/9',\n};\n\n/**\n * The named chords a symbol can write as its core, with the spec each stands\n * for.\n *\n * A core carries no tension of its own beyond the ones a chart writes into the\n * name itself — the unaltered ninth of an `m9`, the flattened fifth of a `7b5`\n * — so what is left over is written as figures rather than swallowed by a name\n * that does not say it.\n */\nconst CORE_CANDIDATES: readonly (readonly [ChordQuality, ChordSpec])[] = chordQualities()\n .map((quality) => [quality, chordSpecForQuality(quality, 0)] as const)\n .filter(([, spec]) => spec.alterations.every(({ degree, alter }) => degree === 5 || alter === 0));\n\n/** Whether `part` is one of the alterations `spec` carries. */\nfunction carriesAlteration(spec: ChordSpec, part: Alteration): boolean {\n return spec.alterations.some(\n ({ degree, alter }) => degree === part.degree && alter === part.alter,\n );\n}\n\n/**\n * The largest named chord a spec can be written as before its figures.\n *\n * Every part of the name must be a part of the chord: same base and seventh,\n * and no alteration, addition or omission the chord does not have. The largest\n * such name is the core, which is what makes a minor ninth with a raised\n * eleventh `Cm9(#11)` rather than `Cm7(9,#11)`.\n */\nfunction coreQuality(spec: ChordSpec): ChordQuality | undefined {\n let core: ChordQuality | undefined;\n let named = -1;\n for (const [quality, candidate] of CORE_CANDIDATES) {\n const size = candidate.alterations.length + candidate.additions.length;\n if (\n size <= named ||\n candidate.base !== spec.base ||\n candidate.seventh !== spec.seventh ||\n !candidate.alterations.every((part) => carriesAlteration(spec, part)) ||\n !candidate.additions.every((degree) => spec.additions.includes(degree)) ||\n !candidate.omissions.every((degree) => spec.omissions.includes(degree))\n ) {\n continue;\n }\n core = quality;\n named = size;\n }\n return core;\n}\n\n/** How each base opens a suffix that no quality name covers. */\nexport const BASE_SUFFIX: Record<ChordBase, string> = {\n maj: '',\n min: 'm',\n dim: 'dim',\n aug: 'aug',\n sus2: 'sus2',\n sus4: 'sus4',\n power: '5',\n};\n\n/** How each seventh is written where it opens a suffix of its own. */\nexport const SEVENTH_SUFFIX: Record<ChordSeventh, string> = {\n maj7: 'maj7',\n min7: '7',\n dim7: 'dim7',\n};\n\n/**\n * The major seventh written after a base letter, which has taken the word's\n * place: `mMaj7` and `augMaj7` capitalize where a bare `maj7` cannot, since the\n * letter before it would otherwise read as the start of the word.\n */\nconst MAJOR_SEVENTH_AFTER_BASE = 'Maj7';\n\n/**\n * A core no quality name covers, written from its parts.\n *\n * A suspension carries its seventh in front of it the way a chart writes\n * `7sus4`, and a major base writes the seventh alone, since `maj` says nothing\n * a bare root does not. Both of those put the seventh at the front of the\n * suffix, where it is the lowercase word a chart writes — `maj7sus4`, not a\n * capitalized form that reads as a base letter.\n */\nfunction builtCoreSuffix(spec: ChordSpec): string {\n const base = BASE_SUFFIX[spec.base];\n if (spec.seventh === undefined) {\n return base;\n }\n const seventh = SEVENTH_SUFFIX[spec.seventh];\n if (spec.base === 'sus2' || spec.base === 'sus4') {\n return `${seventh}${base}`;\n }\n if (spec.base === 'maj') {\n return seventh;\n }\n return `${base}${spec.seventh === 'maj7' ? MAJOR_SEVENTH_AFTER_BASE : seventh}`;\n}\n\n/**\n * The figures a symbol writes after its core, in degree order.\n *\n * Whatever the core already names is left out of them, and so is an omitted\n * tone: a symbol names the harmony a player reads, and one that spelled out its\n * own omissions would not read as a chart's.\n */\nfunction figureText(spec: ChordSpec, core: ChordSpec | undefined): string[] {\n const figures: { degree: number; text: string }[] = [];\n for (const alteration of spec.alterations) {\n if (core !== undefined && carriesAlteration(core, alteration)) {\n continue;\n }\n const { degree, alter } = alteration;\n figures.push({ degree, text: `${alter < 0 ? 'b' : alter > 0 ? '#' : ''}${degree}` });\n }\n for (const degree of spec.additions) {\n if (core?.additions.includes(degree) !== true) {\n figures.push({ degree, text: `add${degree}` });\n }\n }\n return figures.sort((a, b) => a.degree - b.degree).map(({ text }) => text);\n}\n\n/**\n * The suffix a chord's structure is written as.\n *\n * A spec a quality name covers is written under that name, so every symbol a\n * chart already uses is unchanged. Anything else is written as the largest name\n * that fits inside it followed by the figures it does not cover — the way a\n * lead sheet writes tensions — so a combination no name covers still writes\n * itself out, and reads back as the same tones bar the ones it omits, which\n * {@link figureText} leaves unwritten.\n */\nexport function specSuffix(spec: ChordSpec): string {\n const named = exactChordSpecQuality(spec);\n if (named !== undefined) {\n return CANONICAL_SUFFIX[named];\n }\n const core = coreQuality(spec);\n const coreSpec = core === undefined ? undefined : chordSpecForQuality(core, 0);\n const text = core === undefined ? builtCoreSuffix(spec) : CANONICAL_SUFFIX[core];\n const figures = figureText(spec, coreSpec);\n return figures.length === 0 ? text : `${text}(${figures.join(',')})`;\n}\n","import {\n formatNote,\n midiToNote,\n type Note,\n type NoteNameSystem,\n noteToPitchClass,\n spelledInterval,\n transposeByInterval,\n} from '../../core/pitch/index.js';\nimport type { PitchSpelling } from '../chord/index.js';\n\n/**\n * How a chord symbol writes a pitch, and which spellings a chart will take.\n *\n * One module because it is one judgement. Formatting a symbol and transposing\n * one both have to answer \"can this be written down\", and both used to answer\n * it for themselves: transposition checked, formatting did not, and a slash\n * bass carried by the root's own letters came out as `Db7/Ebb`. What decides it\n * lives here, and both read it.\n */\n\n/** Drop the octave from a note, leaving the spelling alone. */\nexport function bareSpelling(note: Note): PitchSpelling {\n return { letter: note.letter, alter: note.alter };\n}\n\n/** A pitch class named from the table, on the side asked for. */\nexport function plainSpelling(pc: number, asFlat: boolean): PitchSpelling {\n return bareSpelling(midiToNote(60 + pc, asFlat ? 'flat' : 'sharp'));\n}\n\n/**\n * A root or bass written the way a chord symbol writes it.\n *\n * German note names are lowercase, but a German chord symbol capitalizes its\n * root — `Es`, `Fis`, `H7` — and leaves the quality suffix to say major or\n * minor. Since case means nothing here, reading ignores it and writing settles\n * on the capitalized form charts use. Every other system writes its name as the\n * naming layer does.\n */\nexport function symbolName(note: Note, system: NoteNameSystem): string {\n const name = formatNote(note, { system });\n return system === 'german' ? `${name.slice(0, 1).toUpperCase()}${name.slice(1)}` : name;\n}\n\n/**\n * Whether a spelling is one a chart writes for a root or a slash bass.\n *\n * The vocabulary is exactly what {@link pitchClassName} draws from with no hint\n * to follow: a natural or a single accidental, and never the accidental that\n * merely renames a natural letter — no C flat, F flat, B sharp or E sharp root,\n * and no double accidental at all.\n */\nexport function isWrittenSpelling(spelling: PitchSpelling): boolean {\n const written = midiToNote(\n 60 + noteToPitchClass(spelling),\n spelling.alter < 0 ? 'flat' : 'sharp',\n );\n return written.letter === spelling.letter && written.alter === spelling.alter;\n}\n\n/**\n * Name a pitch class, preferring a spelling hint when it is still valid.\n *\n * A hint is used only when no explicit sharp/flat preference was given and the\n * hint still resolves to `pc` (a stale hint left over after transposition is\n * ignored). Otherwise the pitch class is respelled from the requested table,\n * falling back to `inheritFlats` so an unhinted slash bass follows the side its\n * root was spelled on rather than flipping to sharps inside one symbol.\n */\nexport function pitchClassName(\n pc: number,\n hint: PitchSpelling | undefined,\n system: NoteNameSystem,\n flats?: boolean,\n inheritFlats?: boolean,\n): string {\n if (flats === undefined && hint !== undefined && noteToPitchClass(hint) === pc) {\n return symbolName(hint, system);\n }\n return symbolName(plainSpelling(pc, flats ?? inheritFlats ?? false), system);\n}\n\n/**\n * Whether a spelling is written on the side of the staff the caller asked for.\n *\n * A natural sits on both sides, so it never forces a respelling; with no\n * preference given every spelling qualifies.\n */\nexport function onAccidentalSide(spelling: PitchSpelling, flats: boolean | undefined): boolean {\n return flats === undefined || spelling.alter === 0 || spelling.alter < 0 === flats;\n}\n\n/**\n * Move a spelling by the step that carries `from` to `to`.\n *\n * This is the one step a whole symbol moves by, whether it is being transposed\n * or respelled onto the other side of the staff: taking the bass and the tones\n * through it is what keeps them inside the chord the root names.\n */\nexport function bySameStep(\n spelling: PitchSpelling,\n from: PitchSpelling,\n to: PitchSpelling,\n): PitchSpelling {\n return bareSpelling(transposeByInterval(spelling, spelledInterval(from, to)));\n}\n\n/**\n * The spelling `letters` letters above `root` that sounds `pc`.\n *\n * How a chord writes one of its own tones: the degree decides the letter and\n * the pitch class decides the accidental, which is what makes the augmented\n * fifth of a G sharp a D double sharp rather than an E.\n */\nexport function spellingAbove(root: PitchSpelling, letters: number, pc: number): PitchSpelling {\n const letter = (((root.letter + letters) % 7) + 7) % 7;\n const natural = noteToPitchClass({ letter, alter: 0 });\n return { letter, alter: ((((pc - natural) % 12) + 18) % 12) - 6 };\n}\n\n/**\n * A slash bass carried by the root's own step, kept to what a chart writes.\n *\n * The step is what keeps the bass inside the chord the root names, so it is\n * taken first; but carried far enough it lands on a spelling no chart writes —\n * a `C#7/D` asked for on the flat side becomes `Db7` over a bass spelled E\n * double flat. Where that happens the bass falls back to the plain name of its\n * pitch class on the side already in force, which is the reading a chart would\n * have written in the first place.\n *\n * A bass carried onto the spelling the chord itself gives that tone is left\n * alone whatever it spells. The augmented fifth of a G sharp is a D double\n * sharp, and writing the first inversion of that chord over an E would name a\n * note the chord does not contain: the odd spelling is what the chord is, not a\n * failure to write it down. Being a chord tone is not enough on its own — the\n * F under a `C#` is the chord's third, and carried onto the flat side it lands\n * on a G double flat, which is neither what a chart writes nor how a D flat\n * chord spells its third.\n *\n * @param spelling The bass as it is written now.\n * @param from The root as it is written now.\n * @param to The root as it is being written.\n * @param asFlat Which side to fall back to when the carried spelling is not one\n * a chart writes.\n * @param isOwnSpelling Whether a spelling is how the chord writes that tone.\n * @returns The carried bass: the chord's own spelling of one of its tones,\n * otherwise a spelling {@link isWrittenSpelling} takes.\n */\nexport function carriedBass(\n spelling: PitchSpelling,\n from: PitchSpelling,\n to: PitchSpelling,\n asFlat: boolean,\n isOwnSpelling: (spelling: PitchSpelling) => boolean,\n): PitchSpelling {\n const carried = bySameStep(spelling, from, to);\n return isOwnSpelling(carried) || isWrittenSpelling(carried)\n ? carried\n : plainSpelling(noteToPitchClass(carried), asFlat);\n}\n\n/** A spelling hint, or undefined when it no longer names `pc`. */\nexport function hintFor(hint: PitchSpelling | undefined, pc: number): PitchSpelling | undefined {\n return hint !== undefined && noteToPitchClass(hint) === pc ? hint : undefined;\n}\n\n/**\n * The spelling a symbol writes its root in.\n *\n * A hint is kept when it still names the root and already sits on the side the\n * caller asked for; otherwise the pitch class is named from that side's table.\n */\nexport function rootSpellingFor(\n pc: number,\n hint: PitchSpelling | undefined,\n flats: boolean | undefined,\n): PitchSpelling {\n return hint !== undefined && onAccidentalSide(hint, flats)\n ? hint\n : plainSpelling(pc, flats ?? false);\n}\n","/**\n * Lead-sheet chord symbol parsing and formatting.\n *\n * Bridges the informal `<root><quality>[/<bass>]` notation musicians write\n * (`Cmaj7`, `F#m7b5`, `Bb7`, `C/G`) and the library's structured {@link Chord}\n * type, so callers can accept/emit plain text at the edges of the API.\n *\n * The root and the slash bass are note names, so they are read and written in\n * whichever notation system the caller names — a German chart's `H7`, `As7` and\n * `Ges/B` included. Unlike a note name, a chord symbol is never attributed to a\n * system on its own: German `B` is a B flat and English `B` a B natural, and a\n * symbol carries no `dur`/`moll` word to break the tie, so the most common\n * symbol in the vocabulary would otherwise change chord with the source it came\n * from. With no system given a symbol is English.\n */\n\nimport {\n InvalidInputError,\n type ParseResult,\n parseFailure,\n unwrapParse,\n} from '../../core/errors/index.js';\nimport {\n type NoteNameOptions,\n noteToPitchClass,\n parseNote,\n pitchClassOf as pitchClass,\n spelledInterval,\n transposeByInterval,\n transposeNote,\n} from '../../core/pitch/index.js';\nimport {\n assertFiniteSemitones,\n assertFlag,\n assertOptions,\n describeRejected,\n} from '../../core/validation/index.js';\nimport type { Chord, PitchSpelling } from '../chord/index.js';\nimport { chordPitchClasses, chordSpecOf, degreeOfInterval } from '../chord/index.js';\nimport { type ChordLike, toChordData } from './coerce.js';\nimport { chordAfterRoot, DEFAULT_SYSTEM, rootSplits } from './grammar.js';\nimport { CANONICAL_SUFFIX, specSuffix } from './suffix.js';\n\n// The unit's public surface is this file, so what the neighbouring modules\n// define is re-exported from here rather than reached for directly.\nexport { type ChordLike, toChordData } from './coerce.js';\n\nimport {\n bareSpelling,\n bySameStep,\n carriedBass,\n hintFor,\n isWrittenSpelling,\n onAccidentalSide,\n pitchClassName,\n plainSpelling,\n rootSpellingFor,\n spellingAbove,\n symbolName,\n} from './spelling.js';\n\n/**\n * Parse a lead-sheet chord symbol into a {@link Chord}.\n *\n * Accepts `<root><quality>[/<bass>]`, e.g. `Cmaj7`, `F#m7b5`, `Bb7`, `C/G`,\n * `C6/9`, `C6/9/E`. Root and bass letters are case-insensitive, matching\n * {@link parseNote}. A `/` is ambiguous between a slash bass and the `6/9`\n * quality; the text splits at the last `/` whose right-hand side is a valid\n * bare note preceded by a recognized quality, so `6/9` (with or without a\n * further slash bass) parses as a quality.\n *\n * The root and bass are read in `system`, so a German chart's `H7`, `As7` and\n * `Ges/B` are read by asking for `'german'`. The system is never inferred: with\n * none given the symbol is English, where `B` is the B natural and `H` no note\n * at all. Case carries nothing in a chord symbol — unlike a German key name,\n * where the case of the tonic is the mode — so the quality suffix alone says\n * major or minor. Where a German root's flat suffix could equally open the\n * quality, the longest root that leaves a known quality behind wins: `Asus4` is\n * an A suspended chord and `Assus4` the A flat one.\n *\n * The quality suffix is read structurally when no name covers it, so a chart's\n * bracketed tensions parse as readily as the names do: `Cmaj7(#11)`,\n * `C7(b9,#11)`, `C7(13)`, `Csus4(add9)`, `C-Δ9`, `C6/9(#11)`. A `/` between two\n * figures separates them as it does inside brackets, which is what the sixth\n * and ninth of a `6/9` are. Such a chord carries the tones it names and reports\n * the nearest quality name (see {@link chordSpecQuality}); read\n * {@link chordSpecOf} for its structure.\n *\n * The parsed chord carries the root/bass spellings as enharmonic hints\n * (`rootSpelling`/`bassSpelling`) so {@link formatChordSymbol} can reproduce\n * flat spellings such as `Bbmaj7` instead of respelling them with sharps.\n *\n * @param text The chord symbol text.\n * @param opts `system` reads the root and bass in that notation system instead\n * of English.\n * @returns The parsed chord.\n * @throws If the root or quality is not recognized in that system. Use\n * {@link tryParseChordSymbol} where failure is ordinary, such as a chord\n * field read on every keystroke.\n * @example\n * ```ts\n * import { parseChordSymbol, formatChordSymbol } from '@libraz/libcantus';\n * const c = parseChordSymbol('Bbmaj7');\n * formatChordSymbol(c); // 'Bbmaj7' — flat spelling is preserved on round-trip\n * parseChordSymbol('H7', { system: 'german' }).rootPc; // 11 — the B natural\n * parseChordSymbol('B', { system: 'german' }).rootPc; // 10 — the German B flat\n * ```\n * @category Chords\n */\nexport function parseChordSymbol(text: string, opts?: NoteNameOptions): Chord {\n return unwrapParse(tryParseChordSymbol(text, opts));\n}\n\n/**\n * Parse a lead-sheet chord symbol, reporting failure instead of throwing it.\n *\n * The same reading as {@link parseChordSymbol} — that function is this one with\n * its error thrown — for the callers where a symbol that does not parse yet is\n * the normal state of the input rather than a fault: a chord field says what is\n * wrong with what has been typed so far, and does not need a `try` around every\n * keystroke to do it.\n *\n * @param text The chord symbol text.\n * @param opts `system` reads the root and bass in that notation system instead\n * of English.\n * @returns The chord, or the error explaining why the text is not one.\n * @example\n * ```ts\n * import { tryParseChordSymbol } from '@libraz/libcantus';\n * const result = tryParseChordSymbol('Cmaj7(#11)');\n * result.ok ? result.value.intervals : result.error.message;\n * ```\n * @category Chords\n */\nexport function tryParseChordSymbol(text: string, opts?: NoteNameOptions): ParseResult<Chord> {\n // Read before the try: the options are the caller's own configuration, so a\n // malformed bag is a fault to raise rather than a symbol that failed to parse.\n const asked = assertOptions(opts, 'opts');\n try {\n if (typeof text !== 'string') {\n throw new InvalidInputError(`chord symbol must be a string; received ${typeof text}`);\n }\n const system = asked.system ?? DEFAULT_SYSTEM;\n const trimmed = text\n .trim()\n .replaceAll('♯', '#')\n .replaceAll('♭', 'b')\n .replaceAll('𝄪', 'x')\n .replaceAll('𝄫', 'bb')\n // Both delta glyphs in circulation stand for the same major seventh.\n .replaceAll('∆', 'Δ');\n const splits = rootSplits(trimmed, system);\n if (splits.length === 0) {\n throw new InvalidInputError(`Invalid chord symbol: ${describeRejected(text)}`);\n }\n for (const { root, rest } of splits) {\n const chord = chordAfterRoot(root, rest, system);\n if (chord !== undefined) {\n return { ok: true, value: chord };\n }\n }\n throw new InvalidInputError(`Unrecognized chord quality: ${describeRejected(text)}`);\n } catch (error) {\n return parseFailure(error);\n }\n}\n\n/**\n * How a chord symbol is written: in which notation system, and on which side of\n * the enharmonic fence.\n *\n * @category Chords\n */\nexport type ChordSymbolOptions = NoteNameOptions & {\n /**\n * Which side of the staff the whole symbol is written on: `true` writes it in\n * flats, `false` in sharps. The root, the slash bass and the chord's tones are\n * spelled together, so a symbol never pairs a flat root with a sharp bass, and\n * a spelling hint already on the requested side is kept rather than renamed.\n * Where that side cannot spell the chord at all, the spelling a chart writes\n * wins over an unwritable one.\n */\n flats?: boolean;\n};\n\n/**\n * Format a {@link Chord} as a lead-sheet chord symbol.\n *\n * The inverse of {@link parseChordSymbol} in every notation system: each\n * quality maps to one canonical suffix spelling, the root and bass are written\n * in `system`, and a slash bass is appended only when it differs from the root.\n * Every symbol written here reads back through {@link parseChordSymbol} as the\n * same root, sounding bass and tones apart from the degrees the chord omits —\n * whatever combination of tensions it carries, and whether or not a quality\n * name covers it. Those omissions are the one difference the round trip has: a\n * chord that leaves a degree out is written without saying so, so reading the\n * symbol back puts the degree in.\n * When the chord carries `rootSpelling`/`bassSpelling` hints (as chords from\n * {@link parseChordSymbol} do) and no explicit `flats` preference is given, the\n * hints are reused so flat symbols round-trip unchanged.\n *\n * The suffix is written from the chord's own tones (see {@link chordSpecOf}),\n * not from its quality name, so a chord carrying alterations no name covers is\n * written out in full — `C7(b9,#11)` — rather than under the nearest name. A\n * tone the chord leaves out is not written: the symbol names the harmony, so a\n * fifth-less dominant is still `C7`.\n *\n * @param chord The chord to format, as a chord symbol, chord data, or a\n * `Chord`.\n * @param opts `system` writes the root and bass in that notation system instead\n * of English; `flats` chooses the side of the staff the whole symbol is\n * written on, as {@link ChordSymbolOptions} describes.\n * @returns The chord symbol text.\n * @example\n * ```ts\n * import { makeChord, formatChordSymbol, parseChordSymbol } from '@libraz/libcantus';\n * formatChordSymbol(makeChord(0, 'min7')); // 'Cm7'\n * formatChordSymbol(parseChordSymbol('Bb7'), { system: 'german' }); // 'B7'\n * formatChordSymbol(parseChordSymbol('B7'), { system: 'german' }); // 'H7'\n * ```\n * @category Chords\n */\nexport function formatChordSymbol(chord: ChordLike, opts?: ChordSymbolOptions): string {\n // A chord rebuilt from JSON or arrived at through arithmetic can carry a\n // non-finite root or a quality this module has no suffix for; formatting\n // those produces 'C' and 'Cundefined', which read as real symbols.\n const data = toChordData(chord);\n if (typeof data.quality !== 'string' || !Object.hasOwn(CANONICAL_SUFFIX, data.quality)) {\n throw new InvalidInputError(`Unknown chord quality: ${String(data.quality)}`);\n }\n const asked = assertOptions(opts, 'opts');\n const system = asked.system ?? DEFAULT_SYSTEM;\n const flats = symbolFlats(asked.flats);\n const rootPc = pitchClass(data.rootPc);\n const rootHint = hintFor(data.rootSpelling, rootPc);\n const rootSpelling = rootSpellingFor(rootPc, rootHint, flats);\n const suffix = specSuffix(chordSpecOf(data));\n let symbol = `${symbolName(rootSpelling, system)}${suffix}`;\n if (data.bassPc !== undefined && pitchClass(data.bassPc) !== rootPc) {\n const bassPc = pitchClass(data.bassPc);\n const bassHint = hintFor(data.bassSpelling, bassPc);\n // How the chord itself writes the tone standing in the bass, when the bass\n // is one of its tones at all: the degree decides the letter, so the\n // augmented fifth of a G sharp is a D double sharp and the third of a D\n // flat is an F. The point of the test is to tell a bass the chord spells\n // that way from one that merely sounds the same pitch class.\n const tonePcs = new Set(chordPitchClasses(data, { includeBass: false }));\n const ownSpelling = tonePcs.has(bassPc)\n ? spellingAbove(\n rootSpelling,\n (degreeOfInterval(pitchClass(bassPc - rootPc), data) ?? 1) - 1,\n bassPc,\n )\n : undefined;\n // Respelling the root respells the bass with it: the two are one symbol, so\n // a preference that moves the root to the flat side takes the bass there\n // too rather than leaving a sharp bass under a flat root. The step the root\n // moved by is what keeps the bass inside the chord the root names, but it\n // is not a licence to write a name no chart writes, so a carried bass that\n // lands outside the written vocabulary falls back to the plain name of its\n // pitch class — the same policy transposing the symbol applies. With no\n // side asked for the root keeps its spelling, so a bass hint that still names\n // the bass is written as given, as the root is: `Gb7/Cb` stays `Gb7/Cb`.\n const bassName =\n flats !== undefined && rootHint !== undefined && bassHint !== undefined\n ? symbolName(\n carriedBass(\n bassHint,\n rootHint,\n rootSpelling,\n flats ?? rootSpelling.alter < 0,\n (spelling) =>\n ownSpelling !== undefined &&\n spelling.letter === ownSpelling.letter &&\n spelling.alter === ownSpelling.alter,\n ),\n system,\n )\n : pitchClassName(bassPc, bassHint, system, flats, rootSpelling.alter < 0);\n symbol += `/${bassName}`;\n }\n return symbol;\n}\n\n/** The `flats` preference of a symbol's options: absent, or a boolean. */\nfunction symbolFlats(flats: boolean | undefined): boolean | undefined {\n return flats === undefined ? undefined : assertFlag(flats, 'flats');\n}\n\n/**\n * The spellings a caller attaches to a chord, which transposition carries with\n * it: the root, the slash bass, and one per chord tone.\n */\nexport type ChordSpellings = {\n /** How the root is written. */\n rootSpelling?: PitchSpelling;\n /** How the slash bass is written. */\n bassSpelling?: PitchSpelling;\n /** How each chord tone is written. */\n toneSpellings?: PitchSpelling[];\n};\n\n/**\n * Move a chord's spellings by a semitone count, keeping them writable.\n *\n * Moving a spelling by letter is what keeps B flat and A sharp apart, but\n * carried far enough it lands on a root no chart writes: B flat up a semitone\n * spells C flat, D flat up one spells E double flat. Where that happens the\n * spelling falls back to the plain name of the pitch class, taken on the\n * accidental side already in force — an explicit preference first, then the\n * side the moved root reads on, then the root's own, then the bass's — so one\n * symbol never pairs a sharp root with a flat bass. The same fallback catches a\n * root whose own letters would leave the bass unwritable, so `Cmaj7/B` up a\n * tritone is `Gbmaj7/F` rather than an F# chord over a bass spelled E#. The\n * bass and the tones then\n * follow the root's own step rather than the semitone count, so respelling the\n * root respells the chord with it and the letter distances inside the chord\n * survive: `Bb7/D` up a semitone is `B7/D#`, the third of the chord it names,\n * and never `B7/Eb`, a note B7 does not contain.\n *\n * A transposition by whole octaves returns every letter to itself and is left\n * alone: a chart that writes C flat keeps it.\n *\n * @param spellings The chord's spellings.\n * @param semitones The signed semitone offset.\n * @param flats Writes the whole symbol on the flat or the sharp side: the root\n * is chosen so that the bass carried with it lands there too.\n * @returns The moved spellings, carrying only the ones that were given.\n */\nexport function transposeChordSpellings(\n spellings: ChordSpellings,\n semitones: number,\n flats?: boolean,\n): ChordSpellings {\n const byLetter = (spelling: PitchSpelling | undefined): PitchSpelling | undefined =>\n spelling === undefined ? undefined : bareSpelling(transposeNote(spelling, semitones));\n const movedRoot = byLetter(spellings.rootSpelling);\n const movedBass = byLetter(spellings.bassSpelling);\n const side = [movedRoot, spellings.rootSpelling, movedBass, spellings.bassSpelling].find(\n (spelling) => spelling !== undefined && spelling.alter !== 0,\n );\n const preferFlats = flats ?? (side !== undefined && side.alter < 0);\n const octaveOnly = pitchClass(semitones) === 0;\n const plainName = (spelling: PitchSpelling, asFlat: boolean): PitchSpelling =>\n plainSpelling(noteToPitchClass(spelling), asFlat);\n const writable = (spelling: PitchSpelling): PitchSpelling =>\n octaveOnly || isWrittenSpelling(spelling) ? spelling : plainName(spelling, preferFlats);\n const root = spellings.rootSpelling;\n const bass = spellings.bassSpelling;\n // Whether a candidate root leaves a bass the chart can still write, on the\n // side the caller asked for. A bass that already reads as an odd name is no\n // test of the root, but one that reads plainly must stay that way, so a chord\n // whose sharp root would need E# under it is written on the flat side.\n const carriesBass = (candidate: PitchSpelling): boolean => {\n if (root === undefined || bass === undefined || octaveOnly || !isWrittenSpelling(bass)) {\n return true;\n }\n const carried = bySameStep(bass, root, candidate);\n return isWrittenSpelling(carried) && onAccidentalSide(carried, flats);\n };\n const moved: ChordSpellings = {};\n if (movedRoot !== undefined) {\n // The root is chosen for the symbol as a whole: the letter it moves to\n // first, then the plain name of its pitch class on the side in force, then\n // the other side. A caller who named a side is offered one more reading\n // before the symbol turns around — the letters' own spelling, odd as it may\n // be — because `Cb/Gb` is the flat side of that chord and `B/Gb` is not a\n // chord at all. Without a named side the written vocabulary wins.\n const asked = flats !== undefined && !octaveOnly && !isWrittenSpelling(movedRoot);\n const candidates = [\n ...(octaveOnly || isWrittenSpelling(movedRoot) ? [movedRoot] : []),\n plainName(movedRoot, preferFlats),\n ...(asked ? [movedRoot] : []),\n plainName(movedRoot, !preferFlats),\n ];\n moved.rootSpelling = candidates.find(carriesBass) ?? plainName(movedRoot, preferFlats);\n }\n // The one step the whole chord moves by: from the root as written to the root\n // as it comes out, fallback included. Everything below it takes this same step\n // instead of the semitone count, which is what keeps the bass and the tones\n // inside the chord the root names.\n const step =\n root !== undefined && moved.rootSpelling !== undefined\n ? spelledInterval(root, moved.rootSpelling)\n : undefined;\n const byStep = (spelling: PitchSpelling): PitchSpelling =>\n bareSpelling(\n step === undefined ? transposeNote(spelling, semitones) : transposeByInterval(spelling, step),\n );\n if (bass !== undefined) {\n moved.bassSpelling = writable(byStep(bass));\n }\n if (spellings.toneSpellings !== undefined) {\n moved.toneSpellings = spellings.toneSpellings.map(byStep);\n }\n return moved;\n}\n\n/**\n * Transpose a chord symbol by a number of semitones.\n *\n * The symbol is read in the same system it is written back in, so a German\n * chart stays German across the transposition.\n *\n * The root and the bass are respelled where moving them by letter would leave\n * a name no chart writes, so raising a flat chart a semitone gives `Bmaj7`\n * rather than `Cbmaj7`. Pass `flats` to write the whole symbol on one side of\n * the staff, root and bass together.\n *\n * @param text The chord symbol text.\n * @param semitones Signed semitone offset to apply to the root and bass.\n * @param opts Options for reading and writing the symbol; see\n * {@link ChordSymbolOptions}.\n * @returns The transposed chord symbol text.\n * @throws If `text` does not parse as a chord symbol.\n * @example\n * ```ts\n * import { transposeChordSymbol } from '@libraz/libcantus';\n * transposeChordSymbol('C/G', 2); // 'D/A'\n * transposeChordSymbol('H7', 1, { system: 'german' }); // 'C7'\n * ```\n * @category Chords\n */\nexport function transposeChordSymbol(\n text: string,\n semitones: number,\n opts?: ChordSymbolOptions,\n): string {\n assertFiniteSemitones(semitones);\n const chord = parseChordSymbol(text, opts);\n const transposed: Chord = { ...chord, rootPc: pitchClass(chord.rootPc + semitones) };\n if (chord.bassPc !== undefined) {\n transposed.bassPc = pitchClass(chord.bassPc + semitones);\n }\n const spellings = transposeChordSpellings(chord, semitones, symbolFlats(opts?.flats));\n if (spellings.rootSpelling !== undefined) {\n transposed.rootSpelling = spellings.rootSpelling;\n }\n if (spellings.bassSpelling !== undefined) {\n transposed.bassSpelling = spellings.bassSpelling;\n }\n if (spellings.toneSpellings !== undefined) {\n transposed.toneSpellings = spellings.toneSpellings;\n }\n return formatChordSymbol(transposed, opts);\n}\n","import { InvalidInputError } from '../../core/errors/index.js';\nimport { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\nimport { assertFiniteNumber, describeRejected } from '../../core/validation/index.js';\nimport type { Chord } from '../chord/index.js';\nimport { parseChordSymbol } from './index.js';\n\n/**\n * Reading a caller's chord into the one shape the library works in.\n *\n * The widening counterpart of the other `to*Data` coercers, kept beside the\n * grammar it falls back on rather than inside it: what a chord symbol means is\n * the parser's question, and what shapes stand for a chord is this one's.\n */\n\n/**\n * Anything that names a chord: a chord symbol, plain chord data, or a value\n * that serializes to chord data such as the `Chord` class.\n *\n * A value that also holds its chord as `data`, as the `Chord` class does, is\n * read through that: the chord as it prints, spelled by the key it carries.\n *\n * @category Chords\n */\nexport type ChordLike =\n | string\n | Chord\n | {\n /** The chord data this value stands for. */\n toJSON(): Chord;\n };\n\n/** Validate plain chord data and return it in the canonical shape. */\nfunction normalizedChord(data: Chord): Chord {\n if (!Array.isArray(data.intervals)) {\n throw new InvalidInputError('chord.intervals must be an array of semitone offsets');\n }\n const copy: Chord = {\n rootPc: pitchClass(assertFiniteNumber(data.rootPc, 'chord.rootPc')),\n quality: data.quality,\n // Each offset is checked rather than copied blind: a chord holding a NaN\n // interval voices, spells and formats as a chord that looks real, and the\n // failure surfaces wherever the number is finally used.\n intervals: data.intervals.map((interval, index) =>\n assertFiniteNumber(interval, `chord.intervals[${index}]`),\n ),\n };\n if (data.bassPc !== undefined) {\n copy.bassPc = pitchClass(assertFiniteNumber(data.bassPc, 'chord.bassPc'));\n }\n // Spelling hints are carried through untouched. They are the caller's record\n // of how the chord is written, and deriving or discarding one here would\n // overrule a decision made where the key was known.\n if (data.rootSpelling !== undefined) {\n copy.rootSpelling = data.rootSpelling;\n }\n if (data.bassSpelling !== undefined) {\n copy.bassSpelling = data.bassSpelling;\n }\n if (data.toneSpellings !== undefined) {\n copy.toneSpellings = data.toneSpellings;\n }\n return copy;\n}\n\n/**\n * Resolve any chord-shaped value to plain {@link Chord} data.\n *\n * The counterpart of {@link toSpelledInterval} for chords: an entry point takes\n * whatever form the caller has — the symbol a chart holds, the data the chord\n * module returns, or a `Chord` instance — and gets one shape back. An instance\n * is accepted through its `toJSON` method rather than by its type, so the\n * layers below the model can read a class without importing it.\n *\n * @param value A chord symbol, plain chord data, or a value whose `data` or\n * `toJSON` returns chord data.\n * @returns The validated chord data, with root and bass reduced to pitch\n * classes and any spelling hint carried through.\n * @throws If the value names no chord, or an interval, root, or bass is not a\n * finite number.\n * @example\n * ```ts\n * import { toChordData } from '@libraz/libcantus';\n * toChordData('Cmaj7').intervals; // [0, 4, 7, 11]\n * toChordData({ rootPc: 0, quality: 'maj', intervals: [0, 4, 7] }).rootPc; // 0\n * ```\n * @category Chords\n */\nexport function toChordData(value: ChordLike): Chord {\n if (typeof value === 'string') {\n return parseChordSymbol(value);\n }\n if (typeof value === 'object' && value !== null) {\n // `toJSON` keeps only the caller's spellings, for a later key to re-spell;\n // `data` is the chord as it prints, which is what a reader is handed.\n const data =\n 'toJSON' in value && typeof value.toJSON === 'function'\n ? 'data' in value && typeof value.data === 'object' && value.data !== null\n ? value.data\n : value.toJSON()\n : value;\n return normalizedChord(data as Chord);\n }\n throw new InvalidInputError(\n `chord must be a chord symbol or chord data; received ${describeRejected(value)}`,\n );\n}\n","import type { SpelledInterval } from '../../core/pitch/index.js';\nimport { pitchClassOf } from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport type { Chord, ChordQuality } from '../chord/index.js';\nimport { chordPitchClasses, chordToneRole, makeChord } from '../chord/index.js';\nimport { isScaleTone, majorKey, scaleSystemOf, scaleTonesInDegreeOrder } from '../scale/index.js';\nimport { isMinorMask } from '../scale/masks.js';\n\n/**\n * What a chord is to a key, written once: the leading tone, the dominant's own\n * sonority, the two readings — diatonic and Neapolitan — that decide whether a\n * chord belongs to the key at all, and which of the key's degrees can be made a\n * local tonic.\n *\n * Four modules ask about the leading tone and each had grown its own answer:\n * the voicing search, the part-writing checker, the cadence reader and the\n * harmonizer. The answers agreed until one of them was corrected, and then a\n * voicing the generator chose was reported as faulty by the checker that had\n * not been. The same happened to \"does this sound like a dominant\", which had\n * four answers — one of them a match on a quality name, so that `G13` was not a\n * dominant while `G7` was.\n *\n * The facts live below all of them rather than in any one: the voicing module\n * reads the counterpoint module, so a shared fact cannot live in either without\n * one importing the other back, and the analysis layer sits above both. Nothing\n * here reaches past the chord and the key, which is what makes that placement\n * possible.\n */\n\n/** Whether a chord's template carries an interval, reduced to within an octave. */\nfunction hasInterval(chord: Chord, semitones: number): boolean {\n return chord.intervals.some((interval) => pitchClassOf(interval) === semitones);\n}\n\n/**\n * Whether every pitch class of a chord belongs to the key's scale.\n *\n * The test is strict against the key's own mode mask: in a natural-minor key\n * the harmonic-minor dominant is not diatonic, since the raised leading tone\n * lies outside the mask.\n */\nexport function isDiatonicChord(chord: Chord, key: KeyScale): boolean {\n return chordPitchClasses(chord).every((pc) => isScaleTone(pc, key));\n}\n\n/**\n * Whether the dominant seventh on a root is a chord the key already contains.\n *\n * Such a chord is native to the key rather than applied to one of its degrees\n * (I7 of mixolydian, II7 of lydian), so it is never marked as a secondary\n * dominant, whichever generator places it.\n */\nexport function isNativeDominantSeventh(rootPc: number, key: KeyScale): boolean {\n return isDiatonicChord(makeChord(rootPc, 'dom7'), key);\n}\n\n/**\n * Whether a chord's interval template carries a major third above its root.\n *\n * The interval is reduced first, so a third voiced as a tenth still counts: it\n * is the same chord tone, and a dominant spread across two octaves is heard\n * through the same leading tone as a close one.\n */\nexport function hasMajorThird(chord: Chord): boolean {\n return hasInterval(chord, 4);\n}\n\n/**\n * Whether a chord sounds a dominant seventh: a major third with a minor\n * seventh, whatever quality it is labelled with.\n *\n * The tritone between those two is the whole of it, so every extension and\n * alteration built over them — the ninth, the thirteenth, the altered dominant\n * — sounds the same dominant as the plain seventh and is read as one. Asking\n * the quality name instead is what let a lead sheet's `G13` stop naming its key.\n */\nexport function soundsDominantSeventh(chord: Chord): boolean {\n return hasMajorThird(chord) && hasInterval(chord, 10);\n}\n\n/**\n * Whether a chord sounds a major triad: a major third with no seventh over it.\n *\n * Read from the tones rather than from the quality name, because the name is a\n * label for the nearest template and the sonority is not. A `Vadd9` and a `V6`\n * are the major triad every pop chart writes the dominant as, and asking for\n * the name `maj` stopped calling them one the moment the ninth or the sixth was\n * added. What ends the triad is a seventh, which makes it a different sonority\n * with different obligations — not a colour tone above it.\n */\nexport function soundsMajorTriad(chord: Chord): boolean {\n return hasMajorThird(chord) && !hasInterval(chord, 10) && !hasInterval(chord, 11);\n}\n\n/**\n * Whether a chord has the dominant's sonority: a bare major triad, or any chord\n * sounding a dominant seventh.\n *\n * The triad belongs here because it can still tonicize by falling a fifth; what\n * it cannot do is resolve a tritone it does not have, which is why the callers\n * that need the tritone ask {@link soundsDominantSeventh} instead.\n */\nexport function hasDominantSonority(chord: Chord): boolean {\n return soundsMajorTriad(chord) || soundsDominantSeventh(chord);\n}\n\n/**\n * Whether a chord is the key's own dominant: it stands a perfect fifth above\n * the tonic and sounds the third that dominant is heard through.\n *\n * That third is the major one, which carries the leading tone, or the\n * suspension standing in its place over a seventh — a suspended chord replaces\n * the third rather than lowering it, and `V7sus4` is how gospel, pop and modal\n * jazz routinely write the dominant. The seventh is what settles the reading:\n * without it a bare `sus4` is a suspension over the fifth degree that has yet\n * to say what it is. A minor third is a different chord again: the bare `v` of\n * a natural-minor key carries no leading tone and frames nothing the tonic is\n * approached from.\n */\nexport function isDominantChordOf(chord: Chord, key: KeyScale): boolean {\n if (pitchClassOf(chord.rootPc - key.rootPc) !== 7) {\n return false;\n }\n if (hasMajorThird(chord)) {\n return true;\n }\n const suspended = !hasInterval(chord, 3) && (hasInterval(chord, 5) || hasInterval(chord, 2));\n return suspended && hasInterval(chord, 10);\n}\n\n/**\n * Whether a chord is the Neapolitan: a major triad on the flat second degree of\n * a key that does not have that degree already.\n *\n * The Neapolitan is an altered predominant, so it has to be an alteration. A\n * mode carrying a lowered second of its own — phrygian, locrian, and the\n * phrygian-dominant scales of flamenco and modal jazz — sounds that triad as a\n * native chord of the key, and calling it chromatic there would make the key's\n * own II a borrowing every time it appeared, exempt from the chromatic rules\n * every other diatonic chord answers to.\n */\nexport function isNeapolitanChordOf(chord: Chord, key: KeyScale): boolean {\n return (\n pitchClassOf(chord.rootPc - key.rootPc) === 1 &&\n soundsMajorTriad(chord) &&\n !isDiatonicChord(chord, key)\n );\n}\n\n/** A degree a chord can tonicize, as every layer that names one needs it. */\nexport type TonicizableDegree = {\n /** 1-based degree number in the key's heptatonic frame. */\n degreeNumber: number;\n /** Pitch class of the degree's root. */\n rootPc: number;\n /** Whether the degree's own triad is minor, which cases its numeral. */\n lower: boolean;\n};\n\n/** The degree whose triad the leading-tone convention makes major in a minor key. */\nexport const DOMINANT_DEGREE = 5;\n\n/**\n * The seven-degree frame a key's degrees are counted in.\n *\n * A degree is one of seven, so a scale with any other number of tones has no\n * degree-for-degree frame of its own and is read against its parallel major:\n * such a key still tonicizes the degrees that major gives it rather than none\n * at all.\n */\nexport function heptatonicFrameOf(key: KeyScale): KeyScale {\n return scaleTonesInDegreeOrder(key).length === 7 ? key : majorKey(pitchClassOf(key.rootPc));\n}\n\n/** The third and fifth above a scale degree, measured within its own scale. */\nfunction degreeTriad(tones: readonly number[], index: number): { third: number; fifth: number } {\n const root = tones[index] ?? 0;\n return {\n third: pitchClassOf((tones[(index + 2) % tones.length] ?? 0) - root),\n fifth: pitchClassOf((tones[(index + 4) % tones.length] ?? 0) - root),\n };\n}\n\n/**\n * The degrees of a key that can be made a local tonic, in degree order.\n *\n * A tonicized degree has to be a major or minor triad to be a tonic at all, so\n * a degree whose diatonic triad spans no perfect fifth is no target: nothing\n * tonicizes the diminished triad on the seventh degree of a major key, which is\n * why a chord standing over it is named against the home key instead (`F#7` in\n * C major is `#IV7`, not a numeral applied to `vii`).\n *\n * The tonic is among them, since it is a tonic already; what may point at it is\n * the narrower question {@link appliedDominantTarget} answers.\n *\n * Degrees are read in the key's heptatonic frame, the same frame the numerals\n * are measured against, so a key with some other number of tones tonicizes the\n * degrees of its parallel major rather than none at all.\n */\nexport function tonicizableDegrees(key: KeyScale): TonicizableDegree[] {\n const tones = scaleTonesInDegreeOrder(heptatonicFrameOf(key));\n const minor = isMinorMask(key.modeMask12);\n const out: TonicizableDegree[] = [];\n for (let index = 0; index < tones.length; index += 1) {\n const rootPc = tones[index];\n if (rootPc === undefined) {\n continue;\n }\n const triad = degreeTriad(tones, index);\n if (triad.fifth !== 7) {\n continue;\n }\n // The case follows the numeral the target degree takes in this key, not the\n // bare scale triad: the numeral layer writes the fifth degree of a minor key\n // as a major V, since the key cadences through its raised seventh, so the\n // dominant of that degree is `V7/V` and never `V7/v`.\n const dominantDegree = minor && index + 1 === DOMINANT_DEGREE;\n out.push({ degreeNumber: index + 1, rootPc, lower: triad.third === 3 && !dominantDegree });\n }\n return out;\n}\n\n/**\n * The degree of a key an applied dominant may tonicize at a given pitch class,\n * or null where that pitch class is no target.\n *\n * The caller works out where its chord points — a dominant sonority a fifth\n * below itself, a diminished one a semitone above itself — and asks here what\n * lies there. The tonic is never a target: a chord pointing at it is the key's\n * own dominant, and `V/I` is a numeral no harmony text writes.\n *\n * One question, one answer, because the layers above ask it for different\n * reasons and must not part company over it: the analysis layer names the\n * degree a chord tonicizes, and the part-writing checker licenses the chromatic\n * tone that tonicizing brings with it. A chord exempted from the cross-relation\n * rule is one the analysis can write an applied numeral for.\n */\nexport function appliedDominantTarget(rootPc: number, key: KeyScale): TonicizableDegree | null {\n const target = pitchClassOf(rootPc);\n return (\n tonicizableDegrees(key).find(\n (degree) => degree.degreeNumber !== 1 && degree.rootPc === target,\n ) ?? null\n );\n}\n\n/**\n * Whether a key cadences through the raised seventh of the harmonic minor.\n *\n * A minor key does, and it is the only key that raises a seventh its own\n * signature lowers. A mode with a lowered third does not: dorian and phrygian\n * are held together by the sevenths they have, and locrian has no perfect fifth\n * to stand that dominant on at all, so raising theirs would overwrite the very\n * degree that names them. The scale system is what separates the two — a\n * natural-minor mask and an aeolian one are the same twelve bits, and the\n * difference is which tradition the key is being read in.\n */\nexport function cadencesThroughRaisedSeventh(key: KeyScale): boolean {\n return isMinorMask(key.modeMask12) && scaleSystemOf(key) === 'common-practice';\n}\n\n/**\n * The pitch class a semitone below the tonic.\n *\n * The raised seventh whatever the key's own signature writes: a minor key\n * cadences through it, and the numeral that stands on it is spelled from it.\n */\nexport function leadingTonePcOf(key: KeyScale): number {\n return pitchClassOf(key.rootPc - 1);\n}\n\n/** The qualities of {@link LEADING_TONE_QUALITIES}, in declaration order. */\nexport const LEADING_TONE_QUALITY_NAMES: readonly ChordQuality[] = ['dim', 'dim7', 'm7b5'];\n\n/**\n * The qualities a leading-tone chord takes.\n *\n * The diminished triad and both diminished sevenths: the fully diminished and\n * the half-diminished stand on the same raised leading tone and differ only in\n * the colour of the sixth degree above it.\n */\nexport const LEADING_TONE_QUALITIES: ReadonlySet<ChordQuality> = new Set(\n LEADING_TONE_QUALITY_NAMES,\n);\n\n/**\n * The degree a chord points at as an applied chord, or null where it points at\n * none: a dominant sonority a fifth below itself, a leading-tone chord a\n * semitone above itself, each limited to {@link appliedDominantTarget}.\n */\nexport function appliedChordTarget(chord: Chord, key: KeyScale): TonicizableDegree | null {\n const dominant = hasDominantSonority(chord);\n if (!dominant && !LEADING_TONE_QUALITIES.has(chord.quality)) {\n return null;\n }\n return appliedDominantTarget(chord.rootPc + (dominant ? 5 : 1), key);\n}\n\n/**\n * Whether a chord sounds the diminished triad a leading-tone chord is built on.\n *\n * The tones rather than the name, for the reason {@link soundsMajorTriad} gives:\n * `quality` is a label for the nearest template in a closed list, so a\n * diminished chord carrying a colour tone the list cannot name stopped being\n * one. What ends the sonority is a perfect fifth, which is a different chord —\n * not an addition above the diminished one. The names remain the vocabulary a\n * numeral is written from, which is a question about spelling rather than\n * sound, and {@link LEADING_TONE_QUALITIES} still answers that one.\n */\nexport function soundsDiminishedTriad(chord: Chord): boolean {\n return hasInterval(chord, 3) && hasInterval(chord, 6) && !hasInterval(chord, 7);\n}\n\n/**\n * Whether a chord is the key's leading-tone chord: one of the diminished\n * qualities, standing on the raised seventh degree.\n *\n * The dominant's function without the dominant's root, which is why the cadence\n * reader and the voicing search both have to agree about it.\n */\nexport function isLeadingToneChordOf(chord: Chord, key: KeyScale): boolean {\n return pitchClassOf(chord.rootPc) === leadingTonePcOf(key) && soundsDiminishedTriad(chord);\n}\n\n/**\n * Whether the key's leading tone is functioning as one in a chord.\n *\n * The tendency is the dominant's, not the pitch class's: the leading tone must\n * rise where it is the third of a chord built on the dominant degree, or the\n * root of a leading-tone chord, which are the two places it carries dominant\n * function. The same pitch class is an ordinary chord tone elsewhere — the\n * fifth of iii, or the seventh of Imaj7 — and is free to move as the line asks,\n * which for a seventh means falling by step.\n */\nexport function isFunctioningLeadingTone(chord: Chord, key: KeyScale): boolean {\n const role = chordToneRole(leadingTonePcOf(key), chord);\n if (role === 'third') {\n return pitchClassOf(chord.rootPc) === pitchClassOf(key.rootPc + 7);\n }\n if (role === 'root') {\n return soundsDiminishedTriad(chord);\n }\n return false;\n}\n\n/**\n * The chord's own seventh as a pitch class, or undefined when it has none.\n *\n * \"Seventh\" means the tone the chord itself writes as one, which is what\n * {@link chordToneRole} answers: a chord carrying its own spelling is read by\n * that spelling, so the augmented sixth of an Italian or German sixth — ten\n * semitones above the root but five letters up — is a sixth resolving outward\n * and owes nothing to the seventh's downward rule. The French sixth is rooted\n * on the supertonic, where its ten semitones really are a chordal seventh, and\n * keeps that obligation.\n */\nexport function seventhPcOf(chord: Chord): number | undefined {\n for (const interval of chord.intervals) {\n const pc = pitchClassOf(chord.rootPc + interval);\n if (chordToneRole(pc, chord) === 'seventh') {\n return pc;\n }\n }\n return undefined;\n}\n\n/** The chord's own fifth as a pitch class, or undefined when it has none. */\nexport function fifthPcOf(chord: Chord): number | undefined {\n for (const interval of chord.intervals) {\n const pc = pitchClassOf(chord.rootPc + interval);\n if (chordToneRole(pc, chord) === 'fifth') {\n return pc;\n }\n }\n return undefined;\n}\n\n/**\n * Whether a written melodic interval falls by a diatonic step, which is the\n * only way a chordal seventh may be left.\n *\n * The question is about letters rather than semitones, and the two answers part\n * company exactly where the rule bites: Gb down to E spans two semitones but\n * writes a diminished third, and the seventh has not resolved. A search that\n * counted semitones would hand out a voicing its own checker rejects, so both\n * layers ask here.\n *\n * @param interval The written interval from the note left to the note reached.\n * @returns True for a descending minor or major second.\n */\nexport function isDescendingStep(interval: SpelledInterval): boolean {\n return interval.number === 2 && (interval.semitones === -1 || interval.semitones === -2);\n}\n\n/** The two thirds a frustrated leading tone may fall by, in semitones. */\nexport const THIRD_SEMITONES: readonly number[] = [3, 4];\n\n/**\n * Whether a leading tone that does not rise is nevertheless left the way the\n * classical norm allows: the frustrated leading tone.\n *\n * An inner voice may drop a third from the leading tone onto the fifth of the\n * arriving chord, which is what completes a triad the rising resolution would\n * leave without one — the standard answer to a complete dominant seventh\n * moving to a complete tonic. The exemption is the inner voices' alone: in the\n * bass or the top voice the leading tone is exposed and must rise, so callers\n * ask this only about a voice that is neither.\n *\n * @param fromPitch The sounding leading tone.\n * @param toPitch Where the voice went.\n * @param nextChord The chord arrived on, supplying the fifth.\n * @returns True when the voice falls a third onto that chord's fifth.\n */\nexport function isFrustratedLeadingTone(\n fromPitch: number,\n toPitch: number,\n nextChord: Chord,\n): boolean {\n if (!THIRD_SEMITONES.includes(fromPitch - toPitch)) {\n return false;\n }\n const fifthPc = fifthPcOf(nextChord);\n return fifthPc !== undefined && pitchClassOf(toPitch) === fifthPc;\n}\n"]}