@libraz/libcantus 1.2.0 → 1.3.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 (56) hide show
  1. package/README.md +4 -0
  2. package/README_ja.md +4 -0
  3. package/dist/analyze/index.cjs +28 -4
  4. package/dist/analyze/index.cjs.map +1 -1
  5. package/dist/analyze/index.d.cts +151 -5
  6. package/dist/analyze/index.d.ts +151 -5
  7. package/dist/analyze/index.js +34 -10
  8. package/dist/{chunk-DSMJRXCZ.cjs → chunk-4R2LWV7E.cjs} +384 -107
  9. package/dist/chunk-4R2LWV7E.cjs.map +1 -0
  10. package/dist/{chunk-J7FHVZII.cjs → chunk-B2PW6EHN.cjs} +2000 -30
  11. package/dist/chunk-B2PW6EHN.cjs.map +1 -0
  12. package/dist/{chunk-Q6LFSAKV.js → chunk-CPMB7P75.js} +1973 -3
  13. package/dist/chunk-CPMB7P75.js.map +1 -0
  14. package/dist/{chunk-H2KJHBW4.js → chunk-GNVN4PPJ.js} +1871 -5
  15. package/dist/chunk-GNVN4PPJ.js.map +1 -0
  16. package/dist/{chunk-SGLEQVJH.cjs → chunk-IFZDWIKM.cjs} +1868 -2
  17. package/dist/chunk-IFZDWIKM.cjs.map +1 -0
  18. package/dist/{chunk-FBH75DAY.js → chunk-IPEF6NWB.js} +287 -10
  19. package/dist/chunk-IPEF6NWB.js.map +1 -0
  20. package/dist/{chunk-HYJJ2V2I.js → chunk-MLBPXA4Z.js} +300 -720
  21. package/dist/chunk-MLBPXA4Z.js.map +1 -0
  22. package/dist/{chunk-HWHGOVO6.cjs → chunk-PWC4CRWT.cjs} +1 -1
  23. package/dist/{chunk-HWHGOVO6.cjs.map → chunk-PWC4CRWT.cjs.map} +1 -1
  24. package/dist/{chunk-HPVSH7SF.cjs → chunk-YFA7X2F6.cjs} +350 -770
  25. package/dist/chunk-YFA7X2F6.cjs.map +1 -0
  26. package/dist/{chunk-NFJ4P6ZZ.js → chunk-ZBUROTJQ.js} +1 -1
  27. package/dist/generate/index.cjs +15 -3
  28. package/dist/generate/index.cjs.map +1 -1
  29. package/dist/generate/index.d.cts +84 -6
  30. package/dist/generate/index.d.ts +84 -6
  31. package/dist/generate/index.js +14 -2
  32. package/dist/{index-BNSM-DSk.d.cts → index-CY-3NVfy.d.cts} +484 -3
  33. package/dist/{index-BJgP97N1.d.ts → index-zaZdmnU4.d.ts} +484 -3
  34. package/dist/index.cjs +43 -7
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +5 -5
  37. package/dist/index.d.ts +5 -5
  38. package/dist/index.js +48 -12
  39. package/dist/model/index.cjs +5 -5
  40. package/dist/model/index.d.cts +203 -7
  41. package/dist/model/index.d.ts +203 -7
  42. package/dist/model/index.js +4 -4
  43. package/dist/{index-DGCNKAUr.d.cts → profile-CQMEqdH9.d.cts} +65 -598
  44. package/dist/{index-BRMbPkHE.d.ts → profile-DLdlIpS9.d.ts} +65 -598
  45. package/dist/{index-BBWNZS0f.d.cts → types-D-XgxGvp.d.cts} +1169 -13
  46. package/dist/{index-CJBBiAmU.d.ts → types-Dh7C064C.d.ts} +1169 -13
  47. package/package.json +5 -5
  48. package/dist/chunk-DSMJRXCZ.cjs.map +0 -1
  49. package/dist/chunk-FBH75DAY.js.map +0 -1
  50. package/dist/chunk-H2KJHBW4.js.map +0 -1
  51. package/dist/chunk-HPVSH7SF.cjs.map +0 -1
  52. package/dist/chunk-HYJJ2V2I.js.map +0 -1
  53. package/dist/chunk-J7FHVZII.cjs.map +0 -1
  54. package/dist/chunk-Q6LFSAKV.js.map +0 -1
  55. package/dist/chunk-SGLEQVJH.cjs.map +0 -1
  56. /package/dist/{chunk-NFJ4P6ZZ.js.map → chunk-ZBUROTJQ.js.map} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"sources":["/home/runner/work/libcantus/libcantus/dist/chunk-J7FHVZII.cjs","../src/generate/context/draw.ts","../src/generate/drums/internal.ts","../src/generate/vocabulary/types.ts","../src/generate/context/difficulty.ts","../src/generate/context/index.ts","../src/generate/bass/internal.ts","../src/generate/vocabulary/transform.ts","../src/generate/bass/degrees.ts","../src/generate/vocabulary/freeze.ts","../src/generate/bass/licks-data.ts","../src/generate/bass/licks.ts","../src/generate/bass/index.ts","../src/generate/countermelody/imitation.ts","../src/generate/countermelody/index.ts","../src/generate/drums/ghost.ts","../src/generate/drums/hihat.ts","../src/generate/drums/swing.ts","../src/generate/drums/beat.ts","../src/generate/drums/euclid.ts","../src/generate/drums/fills.ts","../src/generate/drums/hit.ts","../src/generate/drums/kick.ts","../src/generate/drums/percussion.ts","../src/generate/drums/kit.ts","../src/generate/drums/vocabulary.ts","../src/generate/drums/index.ts","../src/generate/groove/index.ts","../src/generate/harmonize/nct.ts","../src/generate/harmonize/internal.ts","../src/generate/harmonize/cost.ts","../src/generate/harmonize/key-fit.ts","../src/generate/harmonize/search.ts","../src/generate/harmonize/segments.ts","../src/generate/harmonize/candidates.ts","../src/generate/harmonize/index.ts","../src/generate/motif/index.ts","../src/generate/ornament/index.ts","../src/generate/progression/index.ts","../src/generate/reharmony/index.ts","../src/generate/rhythm/index.ts"],"names":["at","ornament","note","bar","FOUR_FOUR","DEFAULT_BPM","hit","pulse","DEFAULT_VELOCITY"],"mappings":"AAAA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,wDAA6B;AAC7B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,wDAA6B;AAC7B;AACE;AACF,wDAA6B;AAC7B;AACE;AACA;AACF,wDAA6B;AAC7B;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;AACF,wDAA6B;AAC7B;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;AACA;AACF,wDAA6B;AAC7B;AACA;ACvDO,IAAM,aAAA,EAAe,CAAC,IAAA,EAAM,MAAA,EAAQ,OAAA,EAAS,OAAO,CAAA;AAgBpD,SAAS,UAAA,CAAW,IAAA,EAAY,KAAA,EAAO,MAAA,EAAc;AAC1D,EAAA,MAAM,QAAA,EAAU,4CAAA,IAAmB,EAAM,IAAI,CAAA;AAC7C,EAAA,IAAA,CAAA,MAAW,OAAA,GAAU,YAAA,EAAc;AACjC,IAAA,8CAAA,OAAe,CAAQ,MAAM,CAAA,EAAG,CAAA,EAAA;AAClC,EAAA;AACO,EAAA;AACT;AAmBsD;AACF,EAAA;AAChB,EAAA;AACR,IAAA;AACS,IAAA;AACrB,MAAA;AACQ,QAAA;AAClB,MAAA;AACF,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAW8D;AACjC,EAAA;AACG,EAAA;AACA,IAAA;AAC9B,EAAA;AACO,EAAA;AACT;AAayE;AAC5B,EAAA;AACpC,EAAA;AACLA,IAAAA;AACgC,IAAA;AACJ,IAAA;AACR,MAAA;AACA,MAAA;AACL,MAAA;AACD,QAAA;AACR,UAAA;AACF,QAAA;AACF,MAAA;AAMuB,MAAA;AACM,MAAA;AAC/B,IAAA;AAC4B,IAAA;AACH,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R,IAAA;AACF,EAAA;AACA,EAAA;AACM,IAAA;AACG,IAAA;AACK,IAAA;AACI,IAAA;AACZ,IAAA;AACoB,IAAA;AACJ,IAAA;AACV,IAAA;AACK,MAAA;AACN,MAAA;AACO,QAAA;AACG,QAAA;AACA,QAAA;AACc,QAAA;AACd,QAAA;AACC,QAAA;AACD,QAAA;AACA,QAAA;AACjB,MAAA;AACF,IAAA;AACY,IAAA;AACH,MAAA;AACD,MAAA;AACR,IAAA;AACF,EAAA;AACA,EAAA;AACM,IAAA;AACG,IAAA;AACK,IAAA;AACI,IAAA;AACZ,IAAA;AACiB,IAAA;AACX,IAAA;AACK,MAAA;AACkB,MAAA;AACjC,IAAA;AACY,IAAA;AACH,MAAA;AACD,MAAA;AACR,IAAA;AACF,EAAA;AACA,EAAA;AACM,IAAA;AACG,IAAA;AACK,IAAA;AACI,IAAA;AACZ,IAAA;AACiB,IAAA;AACX,IAAA;AACK,MAAA;AACkB,MAAA;AACjC,IAAA;AACY,IAAA;AACH,MAAA;AACD,MAAA;AACR,IAAA;AACF,EAAA;AACA,EAAA;AACM,IAAA;AACG,IAAA;AACK,IAAA;AACI,IAAA;AACZ,IAAA;AACgB,IAAA;AACV,IAAA;AACK,MAAA;AACN,MAAA;AACO,QAAA;AACE,QAAA;AACA,QAAA;AACA,QAAA;AACC,QAAA;AACC,QAAA;AACA,QAAA;AACD,QAAA;AACjB,MAAA;AACF,IAAA;AACY,IAAA;AACH,MAAA;AACD,MAAA;AACR,IAAA;AACF,EAAA;AACA,EAAA;AACM,IAAA;AACG,IAAA;AACK,IAAA;AACI,IAAA;AACZ,IAAA;AACgB,IAAA;AACV,IAAA;AACK,MAAA;AACkB,MAAA;AACjC,IAAA;AACY,IAAA;AACH,MAAA;AACD,MAAA;AACR,IAAA;AACF,EAAA;AACA,EAAA;AACM,IAAA;AACG,IAAA;AACK,IAAA;AACI,IAAA;AACZ,IAAA;AACgB,IAAA;AACV,IAAA;AACK,MAAA;AACN,MAAA;AACS,QAAA;AACC,QAAA;AACA,QAAA;AACC,QAAA;AACa,QAAA;AAC/B,MAAA;AACF,IAAA;AACY,IAAA;AACH,MAAA;AACD,MAAA;AACR,IAAA;AACF,EAAA;AACD;AVo3BoC;AACA;AWjiCjB;AAGS;AAsC3B;AAEoD,EAAA;AAClC,EAAA;AAChB,IAAA;AACA,IAAA;AACF,EAAA;AACiC,EAAA;AACD,EAAA;AACE,EAAA;AACH,EAAA;AACT,EAAA;AAGM,EAAA;AAEpB,EAAA;AAEmB,EAAA;AACA,EAAA;AACjB,IAAA;AACV,EAAA;AAO+B,EAAA;AACA,EAAA;AAEd,IAAA;AACf,IAAA;AACF,EAAA;AACuB,EAAA;AAES,EAAA;AACC,EAAA;AACL,EAAA;AACH,EAAA;AAMjB,EAAA;AAGoB,EAAA;AACA,EAAA;AAET,EAAA;AACjB,IAAA;AAC0B,IAAA;AAC5B,EAAA;AAIgC,EAAA;AAEJ,EAAA;AAEA,EAAA;AACI,IAAA;AAChB,IAAA;AACZ,MAAA;AACF,IAAA;AACgC,IAAA;AAIL,IAAA;AACX,IAAA;AACK,IAAA;AACjB,IAAA;AACqB,IAAA;AAKM,IAAA;AACC,IAAA;AACC,MAAA;AACL,MAAA;AAKZ,MAAA;AACZ,QAAA;AACkB,QAAA;AAClB,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AAC4B,MAAA;AAExB,MAAA;AACQ,QAAA;AACN,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACY,QAAA;AACZ,QAAA;AACA,QAAA;AACA,QAAA;AAEF,MAAA;AACuB,MAAA;AACA,MAAA;AACA,QAAA;AAC3B,MAAA;AAE2B,MAAA;AACA,MAAA;AACX,MAAA;AACD,QAAA;AACf,MAAA;AAI8B,MAAA;AAEnB,MAAA;AAOC,QAAA;AAGa,QAAA;AACE,UAAA;AAIR,UAAA;AACJ,YAAA;AACS,6BAAA;AACV,YAAA;AACR,YAAA;AACA,YAAA;AACF,UAAA;AAOWD,UAAAA;AAGF,UAAA;AACIA,YAAAA;AACX,YAAA;AACe,YAAA;AACO,cAAA;AACtB,YAAA;AACM,YAAA;AACP,UAAA;AACH,QAAA;AACF,MAAA;AACF,IAAA;AAQ+B,IAAA;AACH,IAAA;AASJ,MAAA;AACH,MAAA;AACW,MAAA;AACD,QAAA;AACC,QAAA;AAQX,QAAA;AACa,QAAA;AACf,UAAA;AACa,UAAA;AACE,UAAA;AACJ,YAAA;AACjB,UAAA;AACY,YAAA;AACnB,UAAA;AACF,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AAEmC,EAAA;AACF,EAAA;AACP,EAAA;AACM,EAAA;AACZ,IAAA;AACP,IAAA;AACT,MAAA;AACF,IAAA;AAC8B,IAAA;AACH,IAAA;AACD,IAAA;AACP,MAAA;AACc,MAAA;AACjC,IAAA;AAC8B,IAAA;AAC5B,MAAA;AACF,IAAA;AAC4B,IAAA;AACH,IAAA;AACI,MAAA;AACX,MAAA;AAChB,MAAA;AAC+B,MAAA;AACjC,IAAA;AAC6B,IAAA;AACI,MAAA;AACjC,IAAA;AACc,IAAA;AAChB,EAAA;AACO,EAAA;AACT;AAiBsF;AACnD,EAAA;AACT,IAAA;AACa,IAAA;AAC1B,MAAA;AACT,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAgBE;AAG0B,EAAA;AACjB,IAAA;AACT,EAAA;AACI,EAAA;AACsB,EAAA;AACME,IAAAA;AACFF,IAAAA;AAC1B,MAAA;AACF,IAAA;AACiC,IAAA;AACnC,EAAA;AACO,EAAA;AACT;AAUkE;AAC/B,EAAA;AACT,IAAA;AACS,IAAA;AACjB,MAAA;AACd,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAa8C;AACxC,EAAA;AACwB,EAAA;AACM,IAAA;AACjB,MAAA;AACf,IAAA;AACF,EAAA;AACmC,EAAA;AAC1B,IAAA;AACT,EAAA;AACO,EAAA;AACT;AAYE;AASiB,EAAA;AACN,IAAA;AACT,IAAA;AAAA;AAAA;AAAA;AAIE,MAAA;AAAA;AAAA;AAAA;AAAA;AAKU,MAAA;AACgB,MAAA;AACiB,MAAA;AACvB,MAAA;AACtB,IAAA;AACA,IAAA;AACA,IAAA;AACG,IAAA;AACL,EAAA;AAG8B,EAAA;AACE,EAAA;AACJ,EAAA;AACnB,IAAA;AACT,EAAA;AACO,EAAA;AACT;AX21BqC;AACA;AYzyCI;AACvC,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACQ;AA2EuB;AAGF;AA2C8B;AACnC,EAAA;AAC1B;AAG0D;AAClC,EAAA;AACxB;AAG0D;AAC7B,EAAA;AACM,EAAA;AACE,EAAA;AACF,EAAA;AAClB,EAAA;AACR,EAAA;AACT;AAG8D;AAC1B,EAAA;AACpC;AAUmE;AACjC,EAAA;AACJ,IAAA;AACM,IAAA;AACF,IAAA;AACA,MAAA;AAC9B,IAAA;AACF,EAAA;AACF;AAG4E;AAC1C,EAAA;AACC,EAAA;AACnB,EAAA;AACe,EAAA;AACI,IAAA;AACR,MAAA;AACX,MAAA;AACiB,IAAA;AACA,MAAA;AAC7B,IAAA;AACF,EAAA;AACc,EAAA;AACmB,IAAA;AACjC,EAAA;AACF;AAmBkE;AACjC,EAAA;AAEV,EAAA;AAIc,IAAA;AAC1B,IAAA;AACT,EAAA;AAC8B,EAAA;AACG,EAAA;AACxB,IAAA;AACT,EAAA;AACsB,EAAA;AACf,EAAA;AACT;AAe6D;AAC5B,EAAA;AACF,EAAA;AACL,IAAA;AACb,MAAA;AACT,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAQuD;AACtB,EAAA;AACA,EAAA;AACC,EAAA;AAClC;AAGkE;AAChC,EAAA;AACA,EAAA;AACF,EAAA;AACK,IAAA;AAChB,IAAA;AAClB,EAAA;AACH;AAQyD;AACvB,EAAA;AAClC;AAME;AAGgC,EAAA;AACA,EAAA;AACR,EAAA;AACM,EAAA;AACf,IAAA;AACiB,MAAA;AAC5B,MAAA;AACF,IAAA;AAC+B,IAAA;AAIF,MAAA;AACD,MAAA;AACA,MAAA;AAC1B,MAAA;AACF,IAAA;AACiC,IAAA;AAChB,IAAA;AAClB,EAAA;AACH;AAiCqE;AAChD,EAAA;AACQ,EAAA;AACC,EAAA;AAIrB,EAAA;AAEoB,EAAA;AACA,EAAA;AACjB,IAAA;AACV,EAAA;AAEkC,EAAA;AACH,EAAA;AACD,EAAA;AAGR,EAAA;AACU,EAAA;AACG,IAAA;AACjC,IAAA;AACF,EAAA;AACuB,EAAA;AACS,EAAA;AACJ,EAAA;AAEF,EAAA;AACxB,IAAA;AACA,IAAA;AACwB,IAAA;AACE,IAAA;AAClB,IAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACV,IAAA;AACwB,IAAA;AACV,IAAA;AACO,IAAA;AACvB,EAAA;AAE6B,EAAA;AACL,IAAA;AACZ,IAAA;AACR,MAAA;AACF,IAAA;AACyB,IAAA;AACA,IAAA;AACV,IAAA;AACR,MAAA;AACoB,QAAA;AACvB,QAAA;AACG,MAAA;AACiB,QAAA;AACpB,QAAA;AACG,MAAA;AACoB,QAAA;AACvB,QAAA;AACG,MAAA;AACmB,QAAA;AACtB,QAAA;AACF,MAAA;AACoB,QAAA;AAClB,QAAA;AACJ,IAAA;AACF,EAAA;AAE2B,EAAA;AACM,EAAA;AAEP,EAAA;AACI,EAAA;AACJ,IAAA;AACb,IAAA;AACT,MAAA;AACF,IAAA;AACiC,IAAA;AACAE,IAAAA;AACH,IAAA;AAC5B,MAAA;AACF,IAAA;AACS,IAAA;AACiB,MAAA;AACR,MAAA;AAChB,MAAA;AACe,MAAA;AAChB,IAAA;AACH,EAAA;AACO,EAAA;AACT;AZ2jCqC;AACA;Aa3hDF;AAiEmB;AACxB,EAAA;AACI,EAAA;AAClC;AAG+C;AACf,EAAA;AACA,IAAA;AAC9B,EAAA;AACO,EAAA;AACT;AAoCyF;AAChE,EAAA;AACoB,EAAA;AAGX,EAAA;AACG,EAAA;AAGJ,EAAA;AACE,EAAA;AACJ,EAAA;AACA,EAAA;AACG,IAAA;AAChC,EAAA;AAC2B,EAAA;AACO,IAAA;AAClC,EAAA;AACe,EAAA;AACe,IAAA;AAC9B,EAAA;AAC2B,EAAA;AACO,EAAA;AAGH,EAAA;AAKC,EAAA;AAGL,EAAA;AACD,EAAA;AACY,EAAA;AAC5B,IAAA;AACV,EAAA;AAC4B,EAAA;AACE,EAAA;AACI,EAAA;AAEL,EAAA;AACV,IAAA;AAKJ,IAAA;AACD,IAAA;AACC,MAAA;AAMI,QAAA;AACgB,QAAA;AACd,QAAA;AACV,MAAA;AACe,QAAA;AACtB,MAAA;AACF,IAAA;AAEI,IAAA;AAKsB,IAAA;AACrBA,MAAAA;AACwB,MAAA;AACC,MAAA;AAC9B,IAAA;AACiC,IAAA;AACD,MAAA;AAChC,IAAA;AACO,IAAA;AACR,EAAA;AACH;Ab65CqC;AACA;Ac9/CnB;AAGI;AAGE;AAGG;AAGD;AAGC;AAGJ;AAGS;AAGV;AAQG;AAGmB;AACR,EAAA;AACpC;AAGmF;AACzD,EAAA;AAC1B;AAGiD;AACnB,EAAA;AAC9B;AAGkD;AACnB,EAAA;AACF,EAAA;AAClB,IAAA;AACT,EAAA;AACkC,EAAA;AACN,EAAA;AACqB,EAAA;AACrB,EAAA;AACI,IAAA;AAChC,EAAA;AACO,EAAA;AACT;AAGwD;AACtB,EAAA;AAClC;AAGkD;AAUhC;AAchB;AAK0B,EAAA;AACG,EAAA;AACX,EAAA;AACgB,EAAA;AACC,IAAA;AACH,IAAA;AAClB,IAAA;AACgB,IAAA;AACF,MAAA;AACJ,IAAA;AACU,MAAA;AACA,IAAA;AACZ,MAAA;AACpB,IAAA;AAC4B,IAAA;AACV,MAAA;AACT,MAAA;AACT,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAG6C;AACZ,EAAA;AACjC;AAUgC;AACE,EAAA;AACvB,IAAA;AACT,EAAA;AAC8B,EAAA;AAChC;AAe+B;AACA,EAAA;AAC3B,IAAA;AACF,EAAA;AACyB,EAAA;AACb,IAAA;AACR,MAAA;AACF,IAAA;AACF,EAAA;AAC6B,EAAA;AACS,IAAA;AACtC,EAAA;AACF;AAWE;AAOY,EAAA;AACwB,EAAA;AACd,EAAA;AACI,IAAA;AACL,MAAA;AACc,IAAA;AACd,MAAA;AACW,IAAA;AAGX,MAAA;AACnB,IAAA;AACF,EAAA;AACkC,EAAA;AACf,IAAA;AACnB,EAAA;AAC6B,EAAA;AACN,IAAA;AACK,IAAA;AACF,IAAA;AACG,IAAA;AACR,MAAA;AACnB,IAAA;AAC2B,IAAA;AACR,MAAA;AACnB,IAAA;AAC8B,IAAA;AACD,MAAA;AACO,MAAA;AACf,QAAA;AACM,MAAA;AACN,QAAA;AACR,MAAA;AACQ,QAAA;AACZ,MAAA;AACY,QAAA;AACnB,MAAA;AACF,IAAA;AACF,EAAA;AACgC,EAAA;AACzB,EAAA;AACT;AAKE;AAImB,EAAA;AACQ,EAAA;AACL,IAAA;AACtB,EAAA;AACmB,EAAA;AACM,IAAA;AACF,MAAA;AACrB,IAAA;AACF,EAAA;AAI6B,EAAA;AACK,EAAA;AACb,IAAA;AACrB,EAAA;AACgC,EAAA;AAClC;AA2BE;AAKmB,EAAA;AACI,EAAA;AACD,IAAA;AACb,IAAA;AACL,MAAA;AAC6B,MAAA;AACE,MAAA;AACN,MAAA;AACA,MAAA;AAC3B,IAAA;AACD,EAAA;AACH;AASE;AAQI,EAAA;AAC4B,EAAA;AACF,IAAA;AAGxB,IAAA;AAGK,MAAA;AACT,IAAA;AAGe,IAAA;AACb,MAAA;AACE,QAAA;AACgB,QAAA;AACG,QAAA;AACJ,QAAA;AACf,QAAA;AACqB,QAAA;AACD,QAAA;AACV,QAAA;AACC,QAAA;AACb,MAAA;AACqB,MAAA;AACvB,IAAA;AAC+B,IAAA;AACK,IAAA;AAC3B,MAAA;AACT,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAqC+E;AAC1D,EAAA;AAIG,EAAA;AACO,EAAA;AACF,EAAA;AACG,IAAA;AAC9B,EAAA;AAEE,EAAA;AAGgC,EAAA;AAIJ,EAAA;AACf,EAAA;AACoB,IAAA;AAClC,EAAA;AACiC,EAAA;AACZ,EAAA;AACU,EAAA;AACC,EAAA;AACC,EAAA;AACH,EAAA;AAGA,EAAA;AACE,EAAA;AAGF,EAAA;AACrB,IAAA;AACV,EAAA;AAEmC,EAAA;AACN,EAAA;AACK,EAAA;AACH,EAAA;AAEhB,EAAA;AACH,EAAA;AACkB,qBAAA;AAC5B,IAAA;AACF,EAAA;AACa,EAAA;AACkB,qBAAA;AAC7B,IAAA;AACF,EAAA;AACgB,EAAA;AACJ,IAAA;AACR,MAAA;AACF,IAAA;AACF,EAAA;AAC8B,EAAA;AACP,EAAA;AACK,EAAA;AAC1B,IAAA;AAC6B,uBAAA;AAC3B,MAAA;AACO,MAAA;AACkB,MAAA;AAC3B,IAAA;AACF,EAAA;AAC8B,EAAA;AAGjB,EAAA;AAOJ,EAAA;AAIc,EAAA;AACF,EAAA;AACM,IAAA;AACO,IAAA;AAClC,EAAA;AAE0B,EAAA;AACtB,EAAA;AACqB,EAAA;AACO,IAAA;AACN,IAAA;AACtB,MAAA;AACF,IAAA;AACyB,IAAA;AACI,IAAA;AACA,IAAA;AACH,IAAA;AACN,IAAA;AACS,IAAA;AACZ,IAAA;AACU,IAAA;AACU,IAAA;AAElB,IAAA;AACjB,MAAA;AACE,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACyB,QAAA;AACf,QAAA;AACC,QAAA;AACb,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AAEI,IAAA;AACA,IAAA;AACmB,IAAA;AACC,IAAA;AACF,MAAA;AAClB,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AAC4B,MAAA;AAC1B,QAAA;AACF,MAAA;AAC4B,MAAA;AAGxB,QAAA;AAAY;AAGW,QAAA;AAEvB,UAAA;AACF,QAAA;AACF,MAAA;AAEiB,MAAA;AAGE,MAAA;AAGH,MAAA;AAGI,QAAA;AACN,QAAA;AACA,QAAA;AACd,MAAA;AACF,IAAA;AAE6B,IAAA;AAIf,MAAA;AACZ,MAAA;AACF,IAAA;AACc,IAAA;AACQ,IAAA;AACpB,MAAA;AAC0B,MAAA;AAC5B,IAAA;AACS,IAAA;AACiB,MAAA;AACb,MAAA;AACa,MAAA;AACxB,MAAA;AACD,IAAA;AACW,IAAA;AACd,EAAA;AACO,EAAA;AACT;Ad2wCqC;AACA;Ae76D5B;AACU,EAAA;AACJ,EAAA;AACA,EAAA;AACK,EAAA;AACA,EAAA;AACD,EAAA;AAChB;AAGoC;AAEuB;AAC7B,EAAA;AACjC;AAEsD;AACrB,EAAA;AACjC;AAEgF;AAC/C,EAAA;AACF,IAAA;AAC7B,EAAA;AAC8B,EAAA;AACD,IAAA;AAC7B,EAAA;AACO,EAAA;AACT;AAQoD;AAClB,EAAA;AACG,EAAA;AACJ,EAAA;AACjC;AAGuD;AAC1C,EAAA;AACuB,EAAA;AACjB,EAAA;AACV,IAAA;AACW,MAAA;AACd,MAAA;AACG,IAAA;AACU,MAAA;AACb,MAAA;AACG,IAAA;AACW,MAAA;AACd,MAAA;AACG,IAAA;AACU,MAAA;AACb,MAAA;AACG,IAAA;AACA,IAAA;AACI,MAAA;AACP,MAAA;AACJ,EAAA;AAC+B,EAAA;AACjC;AAegB;AAKoB,EAAA;AACD,EAAA;AACnC;AAG0E;AACnC,EAAA;AACnB,IAAA;AAClB,EAAA;AACW,EAAA;AACb;Af64DqC;AACA;AgBj/DA;AAEjB;AACA;AAES;AAG4B;AACvC,EAAA;AAClB;AAGoD;AAC1B,EAAA;AACZ,IAAA;AACZ,EAAA;AACwB,EAAA;AACZ,IAAA;AACZ,EAAA;AAC8B,EAAA;AAChC;AAEgD;AACnB,EAAA;AAClB,IAAA;AACT,EAAA;AACwB,EAAA;AACf,IAAA;AACT,EAAA;AACO,EAAA;AACT;AAE+C;AACpB,EAAA;AAChB,IAAA;AACT,EAAA;AACwB,EAAA;AACf,IAAA;AACT,EAAA;AACO,EAAA;AACT;AAGqD;AACxB,EAAA;AAClB,IAAA;AACT,EAAA;AACkC,EAAA;AACpC;AAmBE;AAOwB,EAAA;AACD,EAAA;AACV,EAAA;AAEW,EAAA;AACQ,IAAA;AACX,EAAA;AACa,IAAA;AACvB,IAAA;AACmB,EAAA;AACM,IAAA;AACzB,IAAA;AACkB,EAAA;AACM,IAAA;AACxB,IAAA;AACmB,EAAA;AACI,IAAA;AACvB,IAAA;AACJ,EAAA;AACY,IAAA;AACV,MAAA;AACI,QAAA;AACP,QAAA;AACG,MAAA;AACI,QAAA;AACP,QAAA;AACG,MAAA;AACmB,QAAA;AACtB,QAAA;AACG,MAAA;AACsB,QAAA;AACzB,QAAA;AACG,MAAA;AACsB,QAAA;AACzB,QAAA;AACG,MAAA;AACI,QAAA;AACP,QAAA;AACJ,IAAA;AACF,EAAA;AAEY,EAAA;AACA,EAAA;AAKoB,IAAA;AACP,MAAA;AACvB,IAAA;AAC6B,IAAA;AACP,MAAA;AACtB,IAAA;AACF,EAAA;AAE4B,EAAA;AAClB,IAAA;AACV,EAAA;AAC+B,EAAA;AACR,IAAA;AACvB,EAAA;AACO,EAAA;AACT;AASwC;AACnB,EAAA;AACZ,IAAA;AACW,MAAA;AACX,IAAA;AACW,MAAA;AACX,IAAA;AACW,MAAA;AAChB,IAAA;AACe,MAAA;AACjB,EAAA;AACF;AAGqC;AACX,EAAA;AACQ,IAAA;AAChC,EAAA;AACiB,EAAA;AACV,IAAA;AACc,MAAA;AACd,IAAA;AACc,MAAA;AACd,IAAA;AACI,MAAA;AACJ,IAAA;AACwB,MAAA;AACxB,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACX,EAAA;AACF;AAGoD;AACxB,EAAA;AACQ,IAAA;AAChB,IAAA;AACP,MAAA;AACT,IAAA;AACgB,IAAA;AACP,MAAA;AACT,IAAA;AACO,IAAA;AACT,EAAA;AACO,EAAA;AACT;AAGyD;AAChC,EAAA;AACd,IAAA;AACT,EAAA;AACiB,EAAA;AACV,IAAA;AACA,IAAA;AACA,IAAA;AACI,MAAA;AACT,IAAA;AAC+B,MAAA;AACjC,EAAA;AACF;AAUuD;AAC7B,EAAA;AACf,IAAA;AACT,EAAA;AACwB,EAAA;AACf,IAAA;AACT,EAAA;AACiB,EAAA;AACV,IAAA;AACA,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACX,EAAA;AACF;AAGmD;AACnC,EAAA;AACP,IAAA;AACO,MAAA;AACP,IAAA;AACO,MAAA;AACP,IAAA;AACO,MAAA;AACZ,IAAA;AACY,MAAA;AACd,EAAA;AACF;AAM4C;AAC5B,EAAA;AACP,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACT,IAAA;AACS,MAAA;AACX,EAAA;AACF;AAIE;AAK4B,EAAA;AACnB,IAAA;AACT,EAAA;AAC0B,EAAA;AACM,IAAA;AACN,MAAA;AACxB,IAAA;AACO,IAAA;AACT,EAAA;AACiC,EAAA;AACT,IAAA;AACxB,EAAA;AACO,EAAA;AACT;AAG8CC;AACb,EAAA;AACjC;AAUgE;AAC7C,EAAA;AACnB;AAG4D;AAC3B,EAAA;AACjC;AAGwC;AACd,EAAA;AACf,IAAA;AACT,EAAA;AACwB,EAAA;AACf,IAAA;AACT,EAAA;AACmB,EAAA;AACrB;AhBs5DqC;AACA;AiBvuEnB;AACH;AAEc;AAEU;AAYI;AAChB,EAAA;AAChB,IAAA;AACT,EAAA;AAC+B,EAAA;AACjC;AAagB;AACJ,EAAA;AACD,IAAA;AACW,IAAA;AACX,IAAA;AACP,EAAA;AACW,EAAA;AACJ,IAAA;AACe,IAAA;AACJ,IAAA;AACQ,IAAA;AACnB,IAAA;AACP,EAAA;AACD;AAmBD;AAGiC,EAAA;AACf,EAAA;AACT,IAAA;AACT,EAAA;AACiC,EAAA;AACN,EAAA;AACD,EAAA;AAEQ,EAAA;AACA,IAAA;AACH,IAAA;AACC,IAAA;AAC5B,MAAA;AACF,IAAA;AACkB,IAAA;AACa,IAAA;AACJ,IAAA;AACA,IAAA;AAC7B,EAAA;AACO,EAAA;AACT;AjB2rEqC;AACA;AkBhsEe;AAC5B,EAAA;AACb,IAAA;AACT,EAAA;AACuB,EAAA;AACd,IAAA;AACT,EAAA;AACO,EAAA;AACT;AAUuC;AACJ,EAAA;AACE,EAAA;AACrC;AAGuD;AAC1B,EAAA;AAC7B;AAWkD;AACvB,EAAA;AACvB,IAAA;AACF,EAAA;AACiC,EAAA;AACP,EAAA;AACC,IAAA;AACP,IAAA;AAChB,MAAA;AACF,IAAA;AACgB,IAAA;AACW,MAAA;AACzB,MAAA;AACF,IAAA;AAC6B,IAAA;AACG,IAAA;AAC9B,MAAA;AACF,IAAA;AACyB,IAAA;AACE,IAAA;AAC7B,EAAA;AACF;AASoD;AAC1B,EAAA;AACf,IAAA;AACT,EAAA;AACkC,EAAA;AACpC;AAoBmE;AACtC,EAAA;AAClB,IAAA;AACT,EAAA;AACgC,EAAA;AACE,EAAA;AACpC;AAgB4D;AACzB,EAAA;AACnC;AAegD;AACf,EAAA;AACN,EAAA;AAChB,IAAA;AACT,EAAA;AAEa,EAAA;AAEf;AASwB;AAG2B;AACxB,EAAA;AACvB,IAAA;AACF,EAAA;AACmC,EAAA;AACjC,IAAA;AACF,EAAA;AACmC,EAAA;AACtB,EAAA;AACX,IAAA;AACF,EAAA;AACgC,EAAA;AAClC;AAG0C;AAGX,EAAA;AAC3B,IAAA;AACF,EAAA;AAKgC,EAAA;AACD,EAAA;AACE,EAAA;AAC/B,IAAA;AACF,EAAA;AAGgC,EAAA;AAC9B,IAAA;AACF,EAAA;AACkB,EAAA;AACO,EAAA;AACL,EAAA;AACM,IAAA;AAC1B,EAAA;AAE6B,EAAA;AACK,IAAA;AAChB,IAAA;AACe,IAAA;AAC7B,MAAA;AACF,IAAA;AACiC,IAAA;AACf,IAAA;AACY,IAAA;AACb,IAAA;AACH,MAAA;AACd,IAAA;AAC6B,IAAA;AACJ,IAAA;AACE,IAAA;AAC7B,EAAA;AACF;AAIE;AAI4B,EAAA;AACE,IAAA;AACD,MAAA;AACG,MAAA;AAC9B,IAAA;AACA,IAAA;AACF,EAAA;AACmB,EAAA;AACc,EAAA;AACC,EAAA;AACC,EAAA;AAId,EAAA;AACD,EAAA;AACO,IAAA;AAC3B,EAAA;AACkB,EAAA;AACN,IAAA;AACL,MAAA;AAC4B,MAAA;AAC/B,MAAA;AACa,MAAA;AACf,IAAA;AACF,EAAA;AAC4B,EAAA;AAIhB,IAAA;AACL,MAAA;AACoB,MAAA;AACvB,MAAA;AACe,MAAA;AACjB,IAAA;AACF,EAAA;AACF;AAWiE;AAC7B,EAAA;AACzB,IAAA;AACT,EAAA;AACiC,EAAA;AACnC;AAO8D;AACzB,EAAA;AAC1B,IAAA;AACT,EAAA;AACqB,EAAA;AACvB;AAGmD;AACjB,EAAA;AAC9B,IAAA;AACF,EAAA;AAEqB,EAAA;AACW,EAAA;AACf,EAAA;AACW,EAAA;AAEtB,EAAA;AACS,EAAA;AAEgB,EAAA;AAKE,IAAA;AAC7B,MAAA;AACF,IAAA;AAC+B,IAAA;AACR,IAAA;AACK,MAAA;AACE,IAAA;AACE,MAAA;AACzB,IAAA;AACuB,MAAA;AAC9B,IAAA;AACA,IAAA;AACF,EAAA;AAE8B,EAAA;AACK,IAAA;AACH,MAAA;AACV,MAAA;AACS,QAAA;AAC3B,MAAA;AAC+B,MAAA;AACJ,QAAA;AACD,UAAA;AACxB,QAAA;AACA,QAAA;AACF,MAAA;AAC+B,MAAA;AACV,MAAA;AACG,QAAA;AACtB,QAAA;AACF,MAAA;AAC+B,MAAA;AAChB,MAAA;AACW,QAAA;AACnB,MAAA;AACuB,QAAA;AAC9B,MAAA;AACF,IAAA;AACA,IAAA;AACF,EAAA;AAEwB,EAAA;AACM,IAAA;AAIP,IAAA;AACW,MAAA;AAChC,IAAA;AACkB,IAAA;AAChB,MAAA;AAC2B,MAAA;AAC7B,IAAA;AAC+B,IAAA;AACV,IAAA;AACW,MAAA;AAC9B,MAAA;AACF,IAAA;AAKuB,IAAA;AACS,MAAA;AAC9B,MAAA;AACF,IAAA;AACuB,IAAA;AACS,MAAA;AAC9B,MAAA;AACF,IAAA;AAC2B,IAAA;AACM,MAAA;AACH,MAAA;AACJ,QAAA;AACtB,QAAA;AACF,MAAA;AACF,IAAA;AAC4B,IAAA;AAC9B,EAAA;AACF;AlBgjEqC;AACA;AmBv/EW;AACrB,EAAA;AACH,EAAA;AACE,EAAA;AACM,EAAA;AACC,EAAA;AACA,EAAA;AACK,EAAA;AACnB,EAAA;AACR,IAAA;AACT,EAAA;AACgC,EAAA;AAClC;AAE6D;AACzC,EAAA;AACY,IAAA;AAC9B,EAAA;AACqB,EAAA;AACS,IAAA;AAC9B,EAAA;AAC6C,EAAA;AACH,EAAA;AACb,EAAA;AACC,IAAA;AACD,IAAA;AACK,IAAA;AACC,MAAA;AACjC,IAAA;AAC0B,IAAA;AACnB,IAAA;AACT,EAAA;AAC2B,EAAA;AAC7B;AnBw/EqC;AACA;AoBzhFG;AACtC,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACQ;AAyDA;AACA;AAGmD;AACpD,EAAA;AACC,IAAA;AAC2B,IAAA;AACC,IAAA;AACpC,EAAA;AACF;AAG8C;AAO5C;AAGkC,EAAA;AACpC;AAMqD;AACjB;AACZ,EAAA;AACgB,EAAA;AACxC;AAGmE;AAC/B,EAAA;AACpC;AAGkC;AACE,EAAA;AACpC;AAME;AAEO,EAAA;AACmB,IAAA;AACU,IAAA;AACpC,EAAA;AACF;AAUqF;AAAA;AAAA;AAAA;AAI9D,EAAA;AACK,IAAA;AACE,IAAA;AAC3B,EAAA;AACW,EAAA;AACsB,IAAA;AAChC,IAAA;AAC2B,MAAA;AACD,MAAA;AACA,MAAA;AACK,MAAA;AAC/B,IAAA;AACF,EAAA;AACW,EAAA;AACuB,IAAA;AAChC,IAAA;AAC2B,MAAA;AACD,MAAA;AACA,MAAA;AACK,MAAA;AAC/B,IAAA;AACF,EAAA;AACe,EAAA;AACmB,IAAA;AAChC,IAAA;AAC2B,MAAA;AACS,MAAA;AACV,MAAA;AACE,MAAA;AAC5B,IAAA;AACF,EAAA;AACkC,EAAA;AACtB,EAAA;AACV,IAAA;AACwB,MAAA;AACA,MAAA;AACO,MAAA;AACA,MAAA;AAC/B,IAAA;AACA,IAAA;AAC0B,MAAA;AACU,MAAA;AACT,MAAA;AACC,MAAA;AAC5B,IAAA;AACF,EAAA;AACkC,EAAA;AAChB,EAAA;AAChB,IAAA;AACwB,MAAA;AACA,MAAA;AACG,MAAA;AACA,MAAA;AAC3B,IAAA;AACA,IAAA;AACoC,MAAA;AACA,MAAA;AACR,MAAA;AACA,MAAA;AAC5B,IAAA;AACF,EAAA;AACY,EAAA;AACuB,IAAA;AACG,IAAA;AACtC,EAAA;AACY,EAAA;AACsB,IAAA;AACA,IAAA;AAClC,EAAA;AACiC,EAAA;AAClB,EAAA;AAAA;AAAA;AAAA;AAIkB,IAAA;AACC,IAAA;AAClC,EAAA;AACc,EAAA;AACoB,IAAA;AAAiB;AAAA;AAAA;AAIlB,IAAA;AAC/B,IAAA;AACF,EAAA;AACD;AAS6E;AAEtD,EAAA;AAIxB;AAWE;AAE4B,EAAA;AAC9B;AASgE;AAC9C,EAAA;AACT,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACX,EAAA;AACF;AAYgD;AAGX;AAGG;AACtC,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAGmC;AACjC,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAGoC;AAClC,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAGsC;AACpC,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAGiC;AAC/B,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAYE;AAGwB,EAAA;AACf,IAAA;AACT,EAAA;AAC6B,EAAA;AACH,EAAA;AACjB,IAAA;AACT,EAAA;AACqB,EAAA;AACC,IAAA;AACtB,EAAA;AACsB,EAAA;AACb,IAAA;AACT,EAAA;AACoB,EAAA;AACtB;AAwBE;AAOiC,EAAA;AAKE,EAAA;AACF,EAAA;AACC,EAAA;AACL,EAAA;AACpB,IAAA;AACT,EAAA;AACgC,EAAA;AAClC;AASkF;AAClD,EAAA;AACG,EAAA;AACF,EAAA;AACjC;AAqBE;AAKgC,EAAA;AACvB,IAAA;AACT,EAAA;AACgB,EAAA;AACiB,EAAA;AACV,IAAA;AACW,MAAA;AACf,MAAA;AACgB,QAAA;AACxB,MAAA;AACsB,QAAA;AAC7B,MAAA;AACF,IAAA;AACF,EAAA;AACgC,EAAA;AACA,IAAA;AACA,IAAA;AACC,MAAA;AACb,MAAA;AACP,QAAA;AACT,MAAA;AACF,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAiBE;AAOsB,EAAA;AACV,EAAA;AACV,IAAA;AACF,EAAA;AAC8B,EAAA;AACH,EAAA;AAKE,EAAA;AACC,EAAA;AACC,IAAA;AACE,IAAA;AACzB,IAAA;AACG,MAAA;AACuB,MAAA;AACvB,MAAA;AACmB,MAAA;AACnB,MAAA;AACT,IAAA;AACF,EAAA;AACF;ApBiyEqC;AACA;AqB1yFG;AAC7B,EAAA;AACC,EAAA;AACI,EAAA;AACD,EAAA;AACG,EAAA;AACD,EAAA;AACD,EAAA;AACJ,EAAA;AACD,EAAA;AACM,EAAA;AACH,EAAA;AACD,EAAA;AACA,EAAA;AACA,EAAA;AACH;AAUgB;AACoC,EAAA;AAC9D;AAa8D;AAC3B,EAAA;AACnC;AASkE;AAIvC,EAAA;AAC3B;AAuBwB;AASH;AACS,iBAAA;AAAA;AAEoB,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAa9C,EAAA;AAIyB,IAAA;AACO,IAAA;AAC9B,MAAA;AACF,IAAA;AACqB,IAAA;AACnB,MAAA;AACW,MAAA;AACX,MAAA;AAC2B,MAAA;AAC7B,IAAA;AAGgC,IAAA;AACX,MAAA;AACrB,IAAA;AACkB,IAAA;AACc,IAAA;AACpB,IAAA;AACM,MAAA;AACX,IAAA;AACyB,MAAA;AAChC,IAAA;AACF,EAAA;AAAA;AAGoD,EAAA;AACjB,IAAA;AACnC,EAAA;AAAA;AAAA;AAAA;AAAA;AAMyC,EAAA;AAGP,IAAA;AACJ,IAAA;AACE,MAAA;AACnB,QAAA;AACT,MAAA;AACF,IAAA;AACO,IAAA;AACT,EAAA;AACF;AAG6C;AACb,EAAA;AAChC;AASmE;AACnC,EAAA;AAChC;ArButFqC;AACA;AsBp4FX;AAWc;AACxB,EAAA;AAChB;AA0ByB;AACA;AACA;AACA;AACG;AACA;AAEC;AAWoB;AAGI;AACrC,EAAA;AAEW,IAAA;AACzB,EAAA;AACF;AAGmC;AAClB,EAAA;AACA,EAAA;AACkB,EAAA;AACC,EAAA;AACA,EAAA;AACpC;AASgF;AAC3C,EAAA;AACtB,EAAA;AACI,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACkB,IAAA;AACnC,EAAA;AACQ,EAAA;AACS,IAAA;AACA,IAAA;AACkB,IAAA;AACA,IAAA;AACnC,EAAA;AACM,EAAA;AACW,IAAA;AACA,IAAA;AAAA;AAAA;AAAA;AAAA;AAKD,IAAA;AACC,IAAA;AACkB,IAAA;AACnC,EAAA;AACO,EAAA;AACU,IAAA;AACA,IAAA;AACa,IAAA;AACA,IAAA;AAC9B,EAAA;AACM,EAAA;AACW,IAAA;AACkB,IAAA;AACJ,IAAA;AACI,IAAA;AACnC,EAAA;AACO,EAAA;AACU,IAAA;AACD,IAAA;AACC,IAAA;AACkB,IAAA;AACnC,EAAA;AACU,EAAA;AACX;AAGkC;AACL,EAAA;AAC9B;AAYEA;AAIwB,EAAA;AACf,IAAA;AACT,EAAA;AAC4B,EAAA;AAC9B;AAcE;AAK6B,EAAA;AACF,EAAA;AACC,IAAA;AACxB,MAAA;AACF,IAAA;AAC2B,IAAA;AACzB,MAAA;AACF,IAAA;AACoCA,IAAAA;AAClC,MAAA;AACF,IAAA;AACoC,IAAA;AACb,MAAA;AACrB,MAAA;AACF,IAAA;AAC4B,IAAA;AACL,MAAA;AACvB,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAaE;AAKmC,EAAA;AACR,EAAA;AACM,EAAA;AACnC;AtB0xFqC;AACA;AuB1+FS;AACP;AAAA;AACA,EAAA;AAAA;AACI,EAAA;AAAmD;AACjF,EAAA;AACT,IAAA;AACmB,IAAA;AACA,IAAA;AACF,IAAA;AACjB,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACiB,IAAA;AACnB,EAAA;AAAA;AACW,EAAA;AACT,IAAA;AACmB,IAAA;AACA,IAAA;AACF,IAAA;AACjB,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACiB,IAAA;AACnB,EAAA;AAAA;AAC8C,EAAA;AAChD;AAKE;AAEgC,EAAA;AACD,EAAA;AACxB,EAAA;AACY,IAAA;AACJ,IAAA;AACE,IAAA;AACE,IAAA;AACnB,EAAA;AACF;AAcE;AAWwB,EAAA;AACtB,IAAA;AACF,EAAA;AAKgC,EAAA;AAET,EAAA;AACS,IAAA;AACG,MAAA;AACN,MAAA;AAC3B,IAAA;AACF,EAAA;AAEmB,EAAA;AAER,IAAA;AAGI,IAAA;AACmB,MAAA;AACJ,MAAA;AACG,QAAA;AAGE,UAAA;AACzB,YAAA;AACF,UAAA;AAGY,UAAA;AAGN,UAAA;AACD,YAAA;AACgB,YAAA;AACnB,YAAA;AACsB,YAAA;AACxB,UAAA;AACF,QAAA;AACF,MAAA;AACK,IAAA;AACuB,MAAA;AACF,MAAA;AACG,QAAA;AAGb,UAAA;AAGN,UAAA;AACD,YAAA;AACgB,YAAA;AACnB,YAAA;AACsB,YAAA;AACxB,UAAA;AACF,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AAEqB,EAAA;AACW,IAAA;AACG,MAAA;AACR,MAAA;AACzB,IAAA;AACF,EAAA;AACF;AvBi8FqC;AACA;AwB/lGgC;AACjD,EAAA;AACK,EAAA;AAAA;AAAA;AAGQ,EAAA;AACI,EAAA;AACD,EAAA;AACT,EAAA;AACE,EAAA;AACQ,EAAA;AACL,EAAA;AACC,EAAA;AACE,EAAA;AACD,EAAA;AACA,EAAA;AACA,EAAA;AAClC;AAW2D;AACzD,EAAA;AACA,EAAA;AACA,EAAA;AACQ;AAG8C;AACN,EAAA;AACnB,EAAA;AACA,IAAA;AAC7B,EAAA;AACO,EAAA;AACT;AAG0D;AACxD,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACQ;AA4B+C;AACjD,EAAA;AACA,EAAA;AACC,EAAA;AACwB,EAAA;AACI,EAAA;AACpB,EAAA;AAAA;AAEW,EAAA;AAC3B;AxBwjGoC;AACA;AyB1jGqC;AACtD,EAAA;AAEK,EAAA;AAKzB;AAWkD;AACd,EAAA;AAEZ,IAAA;AAKR,MAAA;AACiB,QAAA;AAE3B,MAAA;AACF,IAAA;AACD,EAAA;AACH;AAOE;AAEmC,EAAA;AACrC;AAGyC;AACL,EAAA;AACpC;AAGgD;AACnB,EAAA;AACE,EAAA;AACD,IAAA;AAC5B,EAAA;AACO,EAAA;AACT;AAGiD;AACZ,EAAA;AACrC;AAEiD;AAOkB;AACjE,EAAA;AACM,IAAA;AACG,IAAA;AACK,IAAA;AACI,IAAA;AACZC,IAAAA;AACgB,IAAA;AACN,IAAA;AACI,MAAA;AACG,MAAA;AACD,MAAA;AACC,MAAA;AACF,MAAA;AACC,MAAA;AACW,MAAA;AAC9B,IAAA;AACW,IAAA;AACH,MAAA;AACD,MAAA;AACR,IAAA;AACF,EAAA;AACA,EAAA;AACM,IAAA;AACG,IAAA;AACK,IAAA;AACW,IAAA;AACnBA,IAAAA;AACgB,IAAA;AACN,IAAA;AACI,MAAA;AACE,MAAA;AACC,MAAA;AACA,MAAA;AACF,MAAA;AACC,MAAA;AACX,MAAA;AACA,MAAA;AACA,MAAA;AACC,MAAA;AACuB,MAAA;AAChC,IAAA;AACW,IAAA;AACH,MAAA;AACD,MAAA;AACR,IAAA;AACF,EAAA;AACA,EAAA;AACM,IAAA;AACG,IAAA;AACK,IAAA;AACW,IAAA;AACnBA,IAAAA;AACgB,IAAA;AACN,IAAA;AACI,MAAA;AACI,MAAA;AACH,MAAA;AACV,MAAA;AACA,MAAA;AACC,MAAA;AACA,MAAA;AACsB,MAAA;AAC/B,IAAA;AACW,IAAA;AACH,MAAA;AACD,MAAA;AACR,IAAA;AACF,EAAA;AACA,EAAA;AACM,IAAA;AACG,IAAA;AACK,IAAA;AACI,IAAA;AACZA,IAAAA;AACiB,IAAA;AACP,IAAA;AACM,MAAA;AACA,MAAA;AACA,MAAA;AACC,MAAA;AACI,MAAA;AACA,MAAA;AACA,MAAA;AACC,MAAA;AACA,MAAA;AACO,MAAA;AAChC,IAAA;AACW,IAAA;AACH,MAAA;AACD,MAAA;AACR,IAAA;AACF,EAAA;AACA,EAAA;AACM,IAAA;AACG,IAAA;AACK,IAAA;AACI,IAAA;AACZA,IAAAA;AACgB,IAAA;AACN,IAAA;AACO,MAAA;AACD,MAAA;AACF,MAAA;AACE,MAAA;AACC,MAAA;AACA,MAAA;AACF,MAAA;AACE,MAAA;AACK,MAAA;AACC,MAAA;AACA,MAAA;AAC1B,IAAA;AACW,IAAA;AACH,MAAA;AACD,MAAA;AACR,IAAA;AACF,EAAA;AACA,EAAA;AACM,IAAA;AACG,IAAA;AACK,IAAA;AACW,IAAA;AACnBA,IAAAA;AACgB,IAAA;AACN,IAAA;AACI,MAAA;AACE,MAAA;AACE,MAAA;AACA,MAAA;AACH,MAAA;AACC,MAAA;AACX,MAAA;AACA,MAAA;AACC,MAAA;AACA,MAAA;AACsB,MAAA;AAC/B,IAAA;AACW,IAAA;AACH,MAAA;AACD,MAAA;AACR,IAAA;AACF,EAAA;AACA,EAAA;AACM,IAAA;AACG,IAAA;AACK,IAAA;AACW,IAAA;AACnBA,IAAAA;AACiB,IAAA;AACP,IAAA;AACI,MAAA;AACG,MAAA;AACF,MAAA;AACI,MAAA;AACH,MAAA;AACX,MAAA;AACC,MAAA;AACuB,MAAA;AAChC,IAAA;AACW,IAAA;AACH,MAAA;AACD,MAAA;AACR,IAAA;AACF,EAAA;AACD;AA8DwB;AAGL;AAGQ;AAyB0C;AACjD,EAAA;AACU,EAAA;AACM,EAAA;AACJ,EAAA;AAEZ,EAAA;AAGe,EAAA;AACD,EAAA;AACD,IAAA;AAChC,EAAA;AAC2B,EAAA;AACK,EAAA;AACJC,EAAAA;AACM,EAAA;AACR,EAAA;AACK,EAAA;AACC,EAAA;AACG,EAAA;AAED,EAAA;AAIC,EAAA;AACT,IAAA;AACzB,EAAA;AACkC,EAAA;AAKA,EAAA;AACvB,IAAA;AACR,MAAA;AAEF,IAAA;AACF,EAAA;AAC0B,EAAA;AAEG,EAAA;AACb,IAAA;AACZ,MAAA;AACA,MAAA;AACE,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACqB,QAAA;AAAA;AAAA;AAAA;AAIU,QAAA;AACjC,MAAA;AACA,MAAA;AACA,MAAA;AACAF,MAAAA;AACF,IAAA;AACY,IAAA;AACV,MAAA;AACF,IAAA;AACgB,IAAA;AACC,MAAA;AACf,MAAA;AAC4B,QAAA;AACA,QAAA;AACD,QAAA;AACN,QAAA;AACO,QAAA;AAAA;AAAA;AAG1B,QAAA;AACF,MAAA;AACA,MAAA;AACA,MAAA;AACAA,MAAAA;AACF,IAAA;AAGiC,IAAA;AAC/B,MAAA;AACF,IAAA;AACuB,IAAA;AACO,IAAA;AACH,MAAA;AACA,MAAA;AACvB,QAAA;AACF,MAAA;AACM,MAAA;AACJ,QAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAMyB,QAAA;AACN,yBAAA;AACG,QAAA;AACf,QAAA;AACT,MAAA;AACF,IAAA;AACF,EAAA;AAI4B,EAAA;AAET,EAAA;AAErB;AzBs6FqC;AACA;A0BnuGZ;AAGL;AAsCyC;AAC1B,EAAA;AACJ,EAAA;AAGM,EAAA;AACX,EAAA;AACW,IAAA;AACZ,IAAA;AACY,IAAA;AACA,IAAA;AACnC,EAAA;AAIgC,EAAA;AACE,EAAA;AACE,EAAA;AACJ,IAAA;AAChC,EAAA;AAC0B,EAAA;AACM,EAAA;AACE,EAAA;AACNE,EAAAA;AACF,EAAA;AAGI,EAAA;AACF,EAAA;AACH,EAAA;AACH,EAAA;AAEN,EAAA;AAEA,EAAA;AASmB,EAAA;AACN,EAAA;AACM,IAAA;AACrB,MAAA;AACR,QAAA;AAEF,MAAA;AACF,IAAA;AACF,EAAA;AAC4B,EAAA;AACG,EAAA;AAGC,EAAA;AACL,EAAA;AACP,EAAA;AACA,EAAA;AAEI,EAAA;AACtB,IAAA;AACA,IAAA;AAC6B,IAAA;AAC7B,IAAA;AACA,IAAA;AACU,IAAA;AACV,IAAA;AACoB,IAAA;AACa,IAAA;AACH,IAAA;AAC9B,IAAA;AAC6B,IAAA;AACT,IAAA;AACtB,EAAA;AAEuB,EAAA;AACM,EAAA;AACP,EAAA;AACD,EAAA;AACa,EAAA;AAIT,EAAA;AACQ,EAAA;AAKP,EAAA;AACE,IAAA;AACK,IAAA;AACjC,EAAA;AAC4B,EAAA;AACN,EAAA;AAEG,EAAA;AACD,EAAA;AAIE,EAAA;AACtB,EAAA;AAGiB,EAAA;AAEQ,EAAA;AACJ,IAAA;AACQ,IAAA;AACD,IAAA;AAED,IAAA;AACG,MAAA;AAChC,IAAA;AAEmB,IAAA;AACG,IAAA;AACU,IAAA;AACZ,MAAA;AACI,MAAA;AACxB,IAAA;AAEI,IAAA;AACe,IAAA;AACc,MAAA;AACxB,MAAA;AACe,IAAA;AACS,MAAA;AACjC,IAAA;AAMiC,IAAA;AACY,IAAA;AAEnB,IAAA;AACI,MAAA;AACX,MAAA;AAEc,MAAA;AACD,QAAA;AAIX,UAAA;AACb,YAAA;AACA,YAAA;AACA,YAAA;AACA,YAAA;AACA,YAAA;AACAF,YAAAA;AACA,YAAA;AACQ,YAAA;AACY,cAAA;AAEF,cAAA;AAGlB,YAAA;AACF,UAAA;AACyB,UAAA;AAC3B,QAAA;AACoB,QAAA;AACQ,UAAA;AACT,UAAA;AAIf,YAAA;AAAa,cAAA;AAAO,cAAA;AAAU,cAAA;AAAM,cAAA;AAAa,cAAA;AACvC,cAAA;AACV,YAAA;AACF,UAAA;AACwB,UAAA;AACtB,YAAA;AACF,UAAA;AAGK,QAAA;AAEL,UAAA;AACF,QAAA;AACF,MAAA;AAEqB,MAAA;AACnB,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACAA,QAAAA;AACkB,QAAA;AACD,QAAA;AACjB,QAAA;AACA,QAAA;AACY,QAAA;AACZ,QAAA;AACA,QAAA;AACF,MAAA;AAEoB,MAAA;AACN,QAAA;AACe,UAAA;AAC3B,QAAA;AAC2B,QAAA;AACN,UAAA;AACO,UAAA;AACH,YAAA;AACG,YAAA;AACC,cAAA;AACF,cAAA;AAGJ,cAAA;AACM,cAAA;AACzB,YAAA;AACF,UAAA;AACe,QAAA;AACU,UAAA;AAC3B,QAAA;AAC0B,QAAA;AAGb,QAAA;AACe,UAAA;AAC5B,QAAA;AACF,MAAA;AAC6B,MAAA;AAC/B,IAAA;AAEqB,IAAA;AACG,MAAA;AACO,QAAA;AACC,QAAA;AACC,UAAA;AAC7B,QAAA;AACF,MAAA;AACF,IAAA;AAEuB,IAAA;AAGpB,IAAA;AAIkB,MAAA;AACL,QAAA;AACJ,QAAA;AACE,QAAA;AACE,QAAA;AACd,MAAA;AACF,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACF,EAAA;AAK4B,EAAA;AACI,EAAA;AAErBG,EAAAA;AAEuB,EAAA;AACpC;AAyBE;AAG8B,EAAA;AACb,IAAA;AACjB,EAAA;AACkB,EAAA;AACO,EAAA;AACD,EAAA;AACSA,IAAAA;AAEhB,IAAA;AAGC,IAAA;AACd,MAAA;AACF,IAAA;AACa,IAAA;AACgB,IAAA;AAC/B,EAAA;AACO,EAAA;AACT;AAUgF;AACzC,EAAA;AACvC;A1BglGqC;AACA;A2BjlHa;AAE7B;AAKc;AACZ;AACS;AACT;AACO;AACJ;AACL;AACA;AAsC2E;AAC5D,EAAA;AAGA,EAAA;AAIT,EAAA;AACQ,EAAA;AACF,EAAA;AACF,EAAA;AACE,EAAA;AACJ,EAAA;AACG,EAAA;AACC,EAAA;AACH,EAAA;AACM,EAAA;AACD,EAAA;AACL,EAAA;AAEG,EAAA;AAGI,IAAA;AAIT,IAAA;AAEI,IAAA;AACE,IAAA;AACF,IAAA;AACzB,MAAA;AACD,MAAA;AACA,MAAA;AACM,MAAA;AACA,MAAA;AACR,IAAA;AAC2B,IAAA;AACD,IAAA;AAKI,IAAA;AAC/B,EAAA;AACH;AAmDE;AAImC,EAAA;AACD,EAAA;AACP,EAAA;AACH,EAAA;AAGM,EAAA;AAChB,IAAA;AACS,IAAA;AACvB,EAAA;AACO,EAAA;AACU,IAAA;AACf,IAAA;AACF,EAAA;AACF;AAmBE;AAIyB,EAAA;AACA,EAAA;AACM,EAAA;AACG,EAAA;AACJ,EAAA;AACC,EAAA;AACR,EAAA;AACc,EAAA;AACE,EAAA;AACN,EAAA;AACQ,EAAA;AAErB,EAAA;AACe,IAAA;AACzB,MAAA;AACN,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACyB,IAAA;AACG,IAAA;AAGM,IAAA;AACL,MAAA;AACE,MAAA;AAC/B,IAAA;AACF,EAAA;AAE6B,EAAA;AACI,EAAA;AACJ,IAAA;AACV,IAAA;AACe,MAAA;AAC9B,MAAA;AACF,IAAA;AACsB,IAAA;AACX,IAAA;AACkB,MAAA;AACI,MAAA;AAChC,IAAA;AACH,EAAA;AAEmC,EAAA;AACrC;AA8CE;AAGyB,EAAA;AACA,EAAA;AACE,EAAA;AACA,EAAA;AACK,EAAA;AACF,EAAA;AACA,IAAA;AAC9B,EAAA;AAGe,EAAA;AAEH,IAAA;AACiB,MAAA;AAE3B,IAAA;AACF,EAAA;AACkC,EAAA;AAQF,EAAA;AACH,EAAA;AACjB,IAAA;AACuB,MAAA;AAGjC,IAAA;AACF,EAAA;AAM4B,EAAA;AACO,IAAA;AACT,IAAA;AACtB,MAAA;AACF,IAAA;AACwB,IAAA;AACM,IAAA;AACE,MAAA;AAChC,IAAA;AACF,EAAA;AACmC,EAAA;AACA,IAAA;AACzB,MAAA;AACN,MAAA;AACS,MAAA;AACA,MAAA;AACX,IAAA;AAC4B,IAAA;AAIV,IAAA;AAEH,IAAA;AAOe,IAAA;AAC/B,EAAA;AACH;A3B63GqC;AACA;A4BtwH4B;AAC7BC,EAAAA;AACC,EAAA;AACrC;AAGuB;AAEwB;AACrB,EAAA;AACF,EAAA;AACxB;AAE+C;AACpB,EAAA;AAC3B;AAGmD;AAChBL,EAAAA;AACnC;AAqCE;AAEyB,EAAA;AACA,EAAA;AACK,EAAA;AACE,EAAA;AACE,IAAA;AACC,IAAA;AAClC,EAAA;AACH;AAME;AAGyB,EAAA;AACI,EAAA;AACA,EAAA;AACA,EAAA;AACpB,IAAA;AACT,EAAA;AAG+B,EAAA;AACC,EAAA;AACjB,EAAA;AACN,IAAA;AACT,EAAA;AAE4B,EAAA;AACI,EAAA;AACA,EAAA;AAIpB,EAAA;AAMa,EAAA;AAIRA,EAAAA;AAIR,IAAA;AACT,EAAA;AAC+B,EAAA;AACCA,IAAAA;AACrB,MAAA;AACT,IAAA;AAC+B,IAAA;AACtB,MAAA;AACT,IAAA;AACF,EAAA;AAGiB,EAAA;AAIR,IAAA;AACT,EAAA;AAGqBA,EAAAA;AAIZ,IAAA;AACT,EAAA;AAGc,EAAA;AAIL,IAAA;AACT,EAAA;AACO,EAAA;AACT;A5BsrHqC;AACA;A6BtoHF;AACzB,EAAA;AACC,EAAA;AACU,EAAA;AACb,EAAA;AAC2B,EAAA;AACnC;AAYiE;AACrC,EAAA;AACP,EAAA;AACc,EAAA;AACnC;AAEqD;AAClC,EAAA;AACH,EAAA;AAChB;AAE4D;AAQ5B;AAYhB;AACY,EAAA;AAC5B;AAYiE;AAG5D,EAAA;AAEL;AAckE;AACjC,EAAA;AACR,EAAA;AACS,EAAA;AACA,EAAA;AACG,IAAA;AACnC,EAAA;AAC8B,EAAA;AACC,IAAA;AACjB,IAAA;AACY,IAAA;AACK,MAAA;AACE,MAAA;AACA,MAAA;AAClB,QAAA;AACX,MAAA;AACkB,MAAA;AACD,QAAA;AACjB,MAAA;AACF,IAAA;AACuB,IAAA;AACT,MAAA;AACL,MAAA;AACT,IAAA;AACF,EAAA;AACO,EAAA;AACT;A7B8kHqC;AACA;A8B52HP;AACF;AACL;AAWF;AACI;AACD;AACE;AAOG;AACE;AAQI;AAWd;AACI;AACU;AAgBjC;AAGiB,EAAA;AACU,EAAA;AACQ,EAAA;AACR,IAAA;AACC,IAAA;AACxB,MAAA;AACF,IAAA;AACkB,IAAA;AACY,IAAA;AACH,MAAA;AAC3B,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAEiE;AAC7B,EAAA;AACpC;AAUgE;AAC/B,EAAA;AACC,EAAA;AAChB,EAAA;AACN,IAAA;AACa,EAAA;AACb,IAAA;AACH,EAAA;AACyB,IAAA;AAChC,EAAA;AACmC,EAAA;AACzB,IAAA;AACV,EAAA;AACkC,EAAA;AACxB,IAAA;AACV,EAAA;AAIkC,EAAA;AACxB,IAAA;AACV,EAAA;AACO,EAAA;AACT;AAsBE;AAIkC,EAAA;AACzB,IAAA;AACT,EAAA;AAC2B,EAAA;AACA,EAAA;AAClB,IAAA;AACT,EAAA;AAC4B,EAAA;AAEA,EAAA;AAE9B;A9BqxHqC;AACA;A+Bh+HjB;AACC;AACI;AAM4C;AACxD,EAAA;AACa,EAAA;AACK,IAAA;AACA,MAAA;AACN,IAAA;AACA,MAAA;AACrB,IAAA;AACF,EAAA;AACO,EAAA;AACT;AASiF;AACpE,EAAA;AACa,EAAA;AACQ,IAAA;AACH,MAAA;AAC3B,IAAA;AACF,EAAA;AACO,EAAA;AACT;A/Bq9HqC;AACA;AgCx/HqB;AACtB,EAAA;AACpC;AAKE;AAM+B,EAAA;AACV,EAAA;AACgC,EAAA;AAC9B,EAAA;AAIH,EAAA;AAEK,IAAA;AAGzB,EAAA;AAC0B,EAAA;AAEG,EAAA;AACL,IAAA;AACZ,IAAA;AACR,MAAA;AACF,IAAA;AAC+B,IAAA;AAMC,IAAA;AACR,IAAA;AACD,IAAA;AACQ,IAAA;AACX,MAAA;AACH,MAAA;AACgB,MAAA;AACV,QAAA;AACjB,UAAA;AACF,QAAA;AAKY,QAAA;AAGK,QAAA;AACR,UAAA;AACI,UAAA;AACb,QAAA;AACF,MAAA;AAC8B,MAAA;AACrB,MAAA;AACX,IAAA;AACK,IAAA;AACQ,IAAA;AACf,EAAA;AAEsB,EAAA;AACR,EAAA;AACiB,EAAA;AACC,IAAA;AACR,IAAA;AACT,MAAA;AACD,MAAA;AACZ,IAAA;AACF,EAAA;AAEkD,EAAA;AAChB,EAAA;AACd,IAAA;AACa,IAAA;AACE,IAAA;AACnC,EAAA;AAC8B,EAAA;AAChC;AhC69HqC;AACA;AiCviInC;AAI0B,EAAA;AACM,EAAA;AACG,IAAA;AACnC,EAAA;AAC4B,EAAA;AACJ,EAAA;AACG,IAAA;AACR,MAAA;AACjB,IAAA;AACF,EAAA;AAC2B,EAAA;AACb,EAAA;AACoB,IAAA;AAClC,EAAA;AACF;AAOE;AAEgB,EAAA;AACQ,EAAA;AACV,IAAA;AACO,IAAA;AACW,IAAA;AACD,MAAA;AACZ,QAAA;AACL,QAAA;AACV,MAAA;AACF,IAAA;AACgB,IAAA;AACG,MAAA;AACnB,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAE0D;AAC9C,EAAA;AACiB,EAAA;AACf,EAAA;AACQ,EAAA;AACU,IAAA;AACI,IAAA;AACtB,MAAA;AACI,MAAA;AACP,IAAA;AACQ,MAAA;AACf,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAQ8D;AAC/B,EAAA;AACH,EAAA;AACC,IAAA;AAGF,IAAA;AAKrB,MAAA;AACF,IAAA;AACgB,IAAA;AAClB,EAAA;AACO,EAAA;AACT;AAiBE;AAEiC,EAAA;AACf,EAAA;AACM,EAAA;AAIV,IAAA;AACiB,IAAA;AACI,MAAA;AACrB,QAAA;AACV,MAAA;AACF,IAAA;AACgB,IAAA;AACG,MAAA;AACF,MAAA;AACjB,IAAA;AACF,EAAA;AACwB,EAAA;AAC1B;AA+BE;AAMyB,EAAA;AACF,EAAA;AACP,EAAA;AACgB,EAAA;AACG,EAAA;AACD,EAAA;AAQL,EAAA;AACG,IAAA;AAChC,EAAA;AAO0B,EAAA;AACO,EAAA;AACA,EAAA;AACQ,EAAA;AACR,IAAA;AACE,IAAA;AAC1B,IAAA;AACL,MAAA;AACA,MAAA;AACiB,MAAA;AACH,MAAA;AACF,MAAA;AACJ,MAAA;AACV,IAAA;AACD,EAAA;AAIiB,EAAA;AACgB,EAAA;AACH,IAAA;AACT,IAAA;AAEF,IAAA;AAGC,MAAA;AACnB,IAAA;AAC2B,IAAA;AACJ,IAAA;AACG,IAAA;AACO,sBAAA;AACjC,IAAA;AACF,EAAA;AAMkC,EAAA;AAQP,EAAA;AACK,EAAA;AACH,IAAA;AACJ,MAAA;AACvB,IAAA;AAC+B,IAAA;AACL,IAAA;AACO,IAAA;AACP,MAAA;AACb,MAAA;AACT,QAAA;AACF,MAAA;AAC8B,MAAA;AACA,MAAA;AACZ,MAAA;AAChB,QAAA;AACF,MAAA;AAC8B,MAAA;AACL,MAAA;AACP,MAAA;AAGI,MAAA;AACA,QAAA;AACG,QAAA;AACzB,MAAA;AACF,IAAA;AACF,EAAA;AAC8B,EAAA;AAChC;AjCy7HqC;AACA;AkCrrID;AAQb;AAE+B;AACpD,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACD;AAYuC;AACN,EAAA;AACnB,EAAA;AACiB,EAAA;AAChC;AASgC;AACV;AAKiB;AAWL;AACR,EAAA;AACO,IAAA;AAC/B,EAAA;AACkC,EAAA;AACoB,IAAA;AACtD,EAAA;AACF;AAQuC;AAWF;AAML;AAEM;AAEwD;AAClF,EAAA;AACS,EAAA;AACT,EAAA;AACZ;AASyC;AACL,EAAA;AACpC;AAO+C;AACd,EAAA;AAOI,EAAA;AACrB,EAAA;AAC6B,EAAA;AAGlB,IAAA;AACO,IAAA;AACvB,IAAA;AACL,MAAA;AACA,MAAA;AACA,MAAA;AACmB,MAAA;AACU,MAAA;AACN,MAAA;AACzB,IAAA;AACD,EAAA;AAegC,EAAA;AACA,IAAA;AACf,IAAA;AACd,MAAA;AACS,MAAA;AACD,MAAA;AACW,MAAA;AACU,MAAA;AACN,MAAA;AACxB,IAAA;AACH,EAAA;AAIgB,EAAA;AACG,EAAA;AACH,IAAA;AAChB,EAAA;AACsB,EAAA;AACQ,EAAA;AACE,IAAA;AAC5B,MAAA;AACF,IAAA;AAIgC,IAAA;AAChB,IAAA;AACd,MAAA;AACS,MAAA;AACU,MAAA;AACE,MAAA;AACf,MAAA;AACiB,MAAA;AACxB,IAAA;AACH,EAAA;AAEgC,EAAA;AACV,EAAA;AACK,EAAA;AACI,EAAA;AACD,IAAA;AACC,IAAA;AACR,MAAA;AACrB,IAAA;AACF,EAAA;AAK+C,EAAA;AAC9B,IAAA;AACK,IAAA;AACtB,EAAA;AACwB,EAAA;AACH,EAAA;AACP,IAAA;AACH,IAAA;AACX,EAAA;AACmC,EAAA;AACjB,IAAA;AACA,MAAA;AACC,MAAA;AACI,MAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAMU,MAAA;AAGD,MAAA;AAC7B,IAAA;AACH,EAAA;AAEiB,EAAA;AACS,EAAA;AACM,IAAA;AACZ,IAAA;AACT,MAAA;AACT,IAAA;AACW,IAAA;AACJ,IAAA;AACR,EAAA;AACH;AlCokIqC;AACA;AmCxvIoC;AACpD,EAAA;AACW,EAAA;AACF,IAAA;AACb,IAAA;AACd,EAAA;AACuB,EAAA;AACM,EAAA;AACJ,IAAA;AAC1B,EAAA;AACuB,EAAA;AACW,EAAA;AACZ,EAAA;AACW,EAAA;AAKb,EAAA;AACS,IAAA;AAC7B,EAAA;AACwB,EAAA;AACD,EAAA;AACY,EAAA;AAOf,EAAA;AACE,qBAAA;AACpB,IAAA;AACA,IAAA;AACF,EAAA;AACiC,EAAA;AAIA,EAAA;AACC,IAAA;AAClC,EAAA;AACmC,EAAA;AACkB,EAAA;AACtB,EAAA;AACG,EAAA;AAOP,EAAA;AACOA,EAAAA;AACJ,IAAA;AACF,IAAA;AAC1B,EAAA;AAEwB,EAAA;AACS,IAAA;AAC1B,IAAA;AACT,EAAA;AAMwB,EAAA;AACO,IAAA;AACtB,IAAA;AACT,EAAA;AAKW,EAAA;AACc,qBAAA;AAChB,IAAA;AACA,IAAA;AACP,IAAA;AACF,EAAA;AAC6B,EAAA;AAC3B,IAAA;AACA,IAAA;AACA,IAAA;AAC2B,IAAA;AAC3B,IAAA;AACK,IAAA;AACP,EAAA;AAEkC,EAAA;AAChC,IAAA;AACA,IAAA;AACF,EAAA;AAIM,EAAA;AAO6B,EAAA;AACJ,EAAA;AACI,IAAA;AAClB,MAAA;AACU,QAAA;AACvB,MAAA;AACF,IAAA;AACF,EAAA;AACyC,EAAA;AACP,EAAA;AAMlC,EAAA;AAC+B,IAAA;AAC7B,IAAA;AACK,IAAA;AACP,EAAA;AAEsB,EAAA;AACT,EAAA;AACa,EAAA;AACM,EAAA;AACC,IAAA;AACR,IAAA;AACrB,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACgC,IAAA;AACV,IAAA;AACT,MAAA;AACF,MAAA;AACE,MAAA;AACb,IAAA;AACF,EAAA;AAEe,EAAA;AACW,IAAA;AACA,MAAA;AACG,MAAA;AACV,QAAA;AACC,QAAA;AACU,QAAA;AAC1B,MAAA;AAC+B,MAAA;AACT,QAAA;AACtB,MAAA;AAC4B,MAAA;AACA,QAAA;AAC5B,MAAA;AACO,MAAA;AACR,IAAA;AACH,EAAA;AAOgC,EAAA;AACA,IAAA;AACD,IAAA;AACO,IAAA;AACHA,IAAAA;AAClC,EAAA;AAE4B,EAAA;AAC/B;AnCqsIqC;AACA;AoCl7IE;AACrC,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACQ;AA+FgB;AAGwB;AAGX;AACb,EAAA;AACSC,EAAAA;AACnC;AAGyC;AACJ,EAAA;AACF,EAAA;AACH,IAAA;AACX,MAAA;AACjB,IAAA;AAC4B,IAAA;AACX,MAAA;AACjB,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAG0B;AAsBxB;AAImC,EAAA;AACd,EAAA;AACW,IAAA;AACP,MAAA;AACnB,QAAA;AACF,MAAA;AACwB,MAAA;AACG,MAAA;AAClB,QAAA;AACT,MAAA;AACF,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAGgD;AAC1B,EAAA;AACM,EAAA;AACO,IAAA;AACjC,EAAA;AACiC,EAAA;AACnC;AAG6C;AAC1B,EAAA;AACS,EAAA;AACC,IAAA;AAC3B,EAAA;AAC8B,EAAA;AAChC;AAUkD;AACnB,EAAA;AACpB,IAAA;AACT,EAAA;AACmC,EAAA;AACL,EAAA;AACjB,EAAA;AACf;AAE2C;AACT,EAAA;AAClC;AAWiD;AACf,EAAA;AAClC;AAGyD;AACnD,EAAA;AACsB,EAAA;AACQ,IAAA;AACtBD,MAAAA;AACV,IAAA;AACF,EAAA;AACuB,EAAA;AACzB;AAG+C;AAClB,EAAA;AACQ,EAAA;AAChB,IAAA;AACV,MAAA;AACW,QAAA;AACd,QAAA;AACG,MAAA;AACY,QAAA;AACf,QAAA;AACG,MAAA;AAGsB,QAAA;AACzB,QAAA;AACG,MAAA;AACwB,QAAA;AAC3B,QAAA;AACJ,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAuB6D;AACzB,EAAA;AACF,EAAA;AACD,EAAA;AAGF,EAAA;AACH,EAAA;AACI,EAAA;AACI,EAAA;AACA,EAAA;AAKH,EAAA;AACI,EAAA;AACN,EAAA;AAGE,EAAA;AACJ,EAAA;AACE,EAAA;AACE,EAAA;AAEH,EAAA;AACG,EAAA;AAMhB,IAAA;AACa,IAAA;AACA,MAAA;AAC1B,IAAA;AACgC,IAAA;AACV,IAAA;AACE,IAAA;AACG,MAAA;AAC3B,IAAA;AACW,IAAA;AACiB,MAAA;AAC1B,MAAA;AACc,MAAA;AACf,IAAA;AACH,EAAA;AACe,EAAA;AACjB;AAkBgE;AACjC,EAAA;AAC/B;AAWqE;AACxB,EAAA;AACV,EAAA;AACrB,EAAA;AACd;AAuCE;AAK8B,EAAA;AAChC;AAME;AAMiC,EAAA;AACnC;AAWmD;AAClB,EAAA;AACF,IAAA;AACf,MAAA;AACmBA,QAAAA;AAC7B,MAAA;AACF,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAME;AAG8B,EAAA;AAKG,EAAA;AACP,EAAA;AACG,IAAA;AAC7B,EAAA;AAEiC,EAAA;AAG/B,IAAA;AACE,MAAA;AACS,MAAA;AACX,IAAA;AACF,EAAA;AAC8B,EAAA;AACA,IAAA;AAC9B,EAAA;AACW,EAAA;AACkB,IAAA;AACD,MAAA;AACG,MAAA;AAC7B,IAAA;AAC0B,IAAA;AACE,MAAA;AACnB,MAAA;AACoB,QAAA;AACpB,UAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOU,UAAA;AACb,QAAA;AACJ,MAAA;AACF,IAAA;AACe,IAAA;AACiB,MAAA;AACH,MAAA;AAC7B,IAAA;AACmB,IAAA;AACO,MAAA;AACH,QAAA;AACrB,MAAA;AAC8B,MAAA;AACL,MAAA;AAClB,MAAA;AACoB,QAAA;AACd,UAAA;AACkB,UAAA;AACX,UAAA;AAChB,QAAA;AACJ,MAAA;AACF,IAAA;AACK,IAAA;AACoB,MAAA;AACpB,IAAA;AACyB,MAAA;AACb,IAAA;AACW,MAAA;AACA,MAAA;AAGC,MAAA;AACI,MAAA;AACD,MAAA;AAChC,IAAA;AACF,EAAA;AACF;AAG+D;AAC9B,EAAA;AACxB,EAAA;AACyB,IAAA;AACnB,MAAA;AACc,MAAA;AACQ,MAAA;AAC/B,IAAA;AACJ,EAAA;AACF;AAoCE;AAKyB,EAAA;AACG,EAAA;AACJ,EAAA;AACU,EAAA;AAGN,EAAA;AACF,EAAA;AACQ,EAAA;AACR,EAAA;AACS,EAAA;AACT,EAAA;AAEX,EAAA;AACU,IAAA;AACzB,EAAA;AAIgC,EAAA;AACJ,EAAA;AACO,EAAA;AACJ,EAAA;AACV,IAAA;AAIL,IAAA;AACV,IAAA;AACuB,IAAA;AACK,MAAA;AACD,MAAA;AAC3B,QAAA;AACF,MAAA;AAC0B,MAAA;AAGL,MAAA;AACO,MAAA;AAEvB,MAAA;AAE0B,MAAA;AACA,MAAA;AACP,MAAA;AACX,MAAA;AACgB,MAAA;AAC/B,IAAA;AACF,EAAA;AACiC,EAAA;AACnC;ApCqlIqC;AACA;AqC7rJS;AAqDI;AACb;AACd;AAEM;AAEC;AAEL;AACJ;AACA;AAOO;AAM1B;AAIiC,EAAA;AAClB,EAAA;AACR,IAAA;AACK,MAAA;AACL,IAAA;AACA,IAAA;AACI,MAAA;AACJ,IAAA;AAGwB,MAAA;AACxB,IAAA;AAC8B,MAAA;AACrC,EAAA;AACF;AAG4D;AACnD,EAAA;AACT;AAGkDA;AACb,EAAA;AAC1B,IAAA;AACT,EAAA;AACkCM,EAAAA;AACX,EAAA;AACW,IAAA;AAClC,EAAA;AACwB,EAAA;AACU,IAAA;AAClC,EAAA;AACO,EAAA;AACT;AAEiD;AACjB,EAAA;AAChC;AAqC+F;AAC3D,EAAA;AACV,EAAA;AACQ,EAAA;AACE,EAAA;AACZ,EAAA;AACU,EAAA;AACE,IAAA;AAClC,EAAA;AAC+B,EAAA;AACA,EAAA;AACC,EAAA;AAIC,EAAA;AACH,EAAA;AACI,EAAA;AACD,EAAA;AACF,EAAA;AAED,EAAA;AACW,IAAA;AAEf,IAAA;AAGf,MAAA;AACT,IAAA;AAG8BN,IAAAA;AACrB,MAAA;AACT,IAAA;AAC8BA,IAAAA;AACrB,MAAA;AACT,IAAA;AACyB,IAAA;AACR,IAAA;AACW,IAAA;AACL,MAAA;AACvB,IAAA;AACO,IAAA;AACR,EAAA;AACH;AAU8F;AACzE,EAAA;AACf,EAAA;AACwB,EAAA;AACG,IAAA;AACN,MAAA;AAChB,IAAA;AACuB,MAAA;AAC9B,IAAA;AACUA,IAAAA;AACZ,EAAA;AACO,EAAA;AACT;AAQ0F;AACrE,EAAA;AACf,EAAA;AAC8B,EAAA;AACN,IAAA;AACf,IAAA;AACsB,IAAA;AACH,MAAA;AACvB,IAAA;AACiB,MAAA;AACxB,IAAA;AACQA,IAAAA;AACV,EAAA;AACO,EAAA;AACT;AAS2E;AAC5C,EAAA;AACX,EAAA;AACU,EAAA;AACM,IAAA;AACH,IAAA;AAER,IAAA;AACY,IAAA;AACnC,EAAA;AACiB,EAAA;AACW,EAAA;AACI,IAAA;AAChC,EAAA;AACO,EAAA;AACT;ArC6iJqC;AACA;AsC30JF;AASA;AAuDW;AAAA;AAEvC,EAAA;AAAA;AAEC,EAAA;AAAA;AAEA,EAAA;AAAA;AAEF,EAAA;AAAA;AAEC,EAAA;AAAA;AAEI,EAAA;AACD;AA2D2B;AACnC,EAAA;AACM,IAAA;AACE,IAAA;AACc,IAAA;AACR,IAAA;AACiB,IAAA;AAC/B,EAAA;AACA,EAAA;AACM,IAAA;AACE,IAAA;AACc,IAAA;AACR,IAAA;AACiB,IAAA;AAC/B,EAAA;AACA,EAAA;AACM,IAAA;AACE,IAAA;AACc,IAAA;AACR,IAAA;AACiB,IAAA;AAC/B,EAAA;AACA,EAAA;AACM,IAAA;AACE,IAAA;AACc,IAAA;AACR,IAAA;AACiB,IAAA;AAC/B,EAAA;AACA,EAAA;AACM,IAAA;AACE,IAAA;AACc,IAAA;AACR,IAAA;AACoB,IAAA;AAClC,EAAA;AACA,EAAA;AACM,IAAA;AACE,IAAA;AACc,IAAA;AACR,IAAA;AACiB,IAAA;AAC/B,EAAA;AACA,EAAA;AACM,IAAA;AACE,IAAA;AACc,IAAA;AACR,IAAA;AACY,IAAA;AAC1B,EAAA;AACA,EAAA;AACM,IAAA;AACE,IAAA;AACc,IAAA;AACR,IAAA;AACW,IAAA;AACzB,EAAA;AACA,EAAA;AACM,IAAA;AACE,IAAA;AACc,IAAA;AACR,IAAA;AACW,IAAA;AACzB,EAAA;AACA,EAAA;AACM,IAAA;AACE,IAAA;AACc,IAAA;AACR,IAAA;AACiB,IAAA;AAC/B,EAAA;AACA,EAAA;AACM,IAAA;AACE,IAAA;AACc,IAAA;AACR,IAAA;AACiB,IAAA;AAC/B,EAAA;AACA,EAAA;AACM,IAAA;AACE,IAAA;AACe,IAAA;AACT,IAAA;AACG,IAAA;AACjB,EAAA;AACA,EAAA;AACM,IAAA;AACE,IAAA;AACe,IAAA;AACT,IAAA;AACG,IAAA;AACjB,EAAA;AACA,EAAA;AACM,IAAA;AACE,IAAA;AACc,IAAA;AACR,IAAA;AACe,IAAA;AAC7B,EAAA;AACA,EAAA;AACM,IAAA;AACE,IAAA;AACc,IAAA;AACR,IAAA;AACY,IAAA;AAC1B,EAAA;AACA,EAAA;AACM,IAAA;AACE,IAAA;AACe,IAAA;AACT,IAAA;AACiB,IAAA;AAC/B,EAAA;AACA,EAAA;AACM,IAAA;AACE,IAAA;AACc,IAAA;AACR,IAAA;AACY,IAAA;AAC1B,EAAA;AACA,EAAA;AACM,IAAA;AACE,IAAA;AACc,IAAA;AACR,IAAA;AACe,IAAA;AAC7B,EAAA;AACA,EAAA;AACM,IAAA;AACE,IAAA;AACc,IAAA;AACR,IAAA;AACY,IAAA;AAC1B,EAAA;AACA,EAAA;AACM,IAAA;AACE,IAAA;AACc,IAAA;AACR,IAAA;AACe,IAAA;AAC7B,EAAA;AACA,EAAA;AACM,IAAA;AACE,IAAA;AACiB,IAAA;AACX,IAAA;AACiB,IAAA;AAC/B,EAAA;AACA,EAAA;AACM,IAAA;AACE,IAAA;AACmB,IAAA;AACb,IAAA;AACiB,IAAA;AAC/B,EAAA;AACF;AAGyB;AAGuB;AACtB,EAAA;AACC,EAAA;AACA,EAAA;AACF,EAAA;AACC,EAAA;AACI,EAAA;AAC9B;AAOoD;AACf,EAAA;AACrC;AAO2E;AAIxC,EAAA;AACH,EAAA;AAChC;AAY+D;AAC7B,EAAA;AAChB,IAAA;AACkB,IAAA;AAClC,EAAA;AAC+B,EAAA;AACD,EAAA;AAChC;AASoD;AAClB,EAAA;AAGN,IAAA;AACf,MAAA;AACT,IAAA;AAG6B,IAAA;AAC/B,EAAA;AACmB,EAAA;AACV,IAAA;AACT,EAAA;AACmB,EAAA;AACV,IAAA;AACT,EAAA;AACO,EAAA;AACT;AAoBE;AAGwC,EAAA;AACd,IAAA;AACS,MAAA;AAER,MAAA;AACzB,IAAA;AACgC,IAAA;AAChB,MAAA;AAChB,IAAA;AACO,IAAA;AACR,EAAA;AAKkC,EAAA;AACrC;AAqBE;AAGwB,EAAA;AAC1B;AAa8E;AAI/C,EAAA;AACN,EAAA;AACO,IAAA;AAC9B,EAAA;AACuB,EAAA;AACS,EAAA;AAClC;AASwD;AACpB,EAAA;AACpC;AAeyC;AACJ,EAAA;AAC1B,IAAA;AACT,EAAA;AACgC,EAAA;AAClC;AAyC2E;AACtD,EAAA;AACU,EAAA;AACK,EAAA;AAGH,EAAA;AACI,EAAA;AAC/B,EAAA;AAC2B,EAAA;AACL,IAAA;AACM,MAAA;AAC9B,IAAA;AACS,IAAA;AACe,MAAA;AACI,MAAA;AACG,MAAA;AACN,QAAA;AAER,QAAA;AAEG,QAAA;AACJ,UAAA;AACR,YAAA;AACF,UAAA;AACF,QAAA;AACO,QAAA;AACR,MAAA;AACuB,MAAA;AACO,MAAA;AACjC,IAAA;AACF,EAAA;AACiC,EAAA;AACA,IAAA;AACL,IAAA;AACI,MAAA;AAC9B,IAAA;AACF,EAAA;AACiC,EAAA;AAG3B,EAAA;AAEQ,EAAA;AACS,qCAAA;AACrB,IAAA;AACA,IAAA;AACuB,oBAAA;AACzB,EAAA;AAGkC,EAAA;AACL,EAAA;AACA,EAAA;AACI,IAAA;AACN,IAAA;AACC,MAAA;AACE,MAAA;AACT,MAAA;AACnB,IAAA;AACgC,IAAA;AACV,MAAA;AACtB,IAAA;AACiB,IAAA;AACnB,EAAA;AAEiC,EAAA;AACf,EAAA;AACgB,IAAA;AACZ,IAAA;AACE,IAAA;AACO,IAAA;AACF,IAAA;AACL,MAAA;AACK,MAAA;AACN,MAAA;AACjB,QAAA;AACF,MAAA;AAC+B,MAAA;AACJ,MAAA;AACE,MAAA;AACA,MAAA;AAGF,MAAA;AACF,MAAA;AAGvB,MAAA;AAKA,QAAA;AACF,MAAA;AACwB,MAAA;AACM,QAAA;AACP,UAAA;AACrB,QAAA;AACY,QAAA;AACF,UAAA;AACC,UAAA;AACM,UAAA;AACI,UAAA;AACrB,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AAEO,EAAA;AACT;AtC2hJqC;AACA;AuC7lKsB;AACpC,EAAA;AACc,EAAA;AACrC;AAGoD;AACtB,EAAA;AACI,EAAA;AACJ,EAAA;AACb,IAAA;AACf,EAAA;AACmC,EAAA;AACL,IAAA;AAC9B,EAAA;AACmC,EAAA;AAC1B,IAAA;AACT,EAAA;AACO,EAAA;AACT;AAGkD;AAChC,EAAA;AACS,EAAA;AACM,EAAA;AACH,IAAA;AAC5B,EAAA;AACO,EAAA;AACT;AAGgD;AACnB,EAAA;AAC7B;AAGiC;AAGV;AAG0B;AACjB,EAAA;AAChC;AAUiD;AAE5B,EAAA;AACI,EAAA;AAClB,IAAA;AACwB,IAAA;AAC7B,EAAA;AACgC,EAAA;AACP,IAAA;AACzB,EAAA;AACO,EAAA;AACT;AAGiD;AACnB,EAAA;AAC9B;AAW+B;AACJ,EAAA;AACS,EAAA;AACT,IAAA;AACzB,EAAA;AACmC,EAAA;AACD,EAAA;AACpC;AA+CE;AAE0B,EAAA;AAIM,EAAA;AACP,EAAA;AAKH,EAAA;AAC0C,EAAA;AAG7B,EAAA;AACJ,IAAA;AACb,IAAA;AACO,MAAA;AACf,MAAA;AACP,IAAA;AACH,EAAA;AAGoB,EAAA;AACG,IAAA;AAEiC,IAAA;AAG3B,MAAA;AAC3B,IAAA;AACF,EAAA;AAKkC,EAAA;AACA,EAAA;AACd,EAAA;AACe,IAAA;AACN,MAAA;AAC3B,IAAA;AACF,EAAA;AAGqB,EAAA;AACW,IAAA;AACF,MAAA;AAER,MAAA;AAGS,QAAA;AAC3B,MAAA;AACF,IAAA;AACF,EAAA;AAEmC,EAAA;AAClB,EAAA;AACgB,EAAA;AACT,EAAA;AACS,IAAA;AAC7B,MAAA;AACF,IAAA;AACqB,IAAA;AACI,IAAA;AACvB,MAAA;AACF,IAAA;AACkB,IAAA;AACW,IAAA;AACP,MAAA;AACS,MAAA;AAC3B,QAAA;AACF,MAAA;AACF,IAAA;AACa,IAAA;AACM,MAAA;AACD,MAAA;AACc,MAAA;AACC,MAAA;AAChC,IAAA;AACH,EAAA;AACO,EAAA;AACT;AAwCwC;AAGb,EAAA;AACS,EAAA;AACjB,EAAA;AACa,EAAA;AACP,IAAA;AACI,IAAA;AACvB,MAAA;AACF,IAAA;AACkB,IAAA;AACL,IAAA;AACX,MAAA;AAC2B,MAAA;AACG,MAAA;AAC/B,IAAA;AACH,EAAA;AAEkC,EAAA;AACd,EAAA;AACe,IAAA;AACA,MAAA;AACjC,IAAA;AACF,EAAA;AAK6B,EAAA;AACI,EAAA;AACD,IAAA;AAChC,EAAA;AACO,EAAA;AACT;AA0BsC;AAGJ,EAAA;AACP,EAAA;AACG,EAAA;AACQ,EAAA;AACN,EAAA;AACG,EAAA;AAMN,EAAA;AACM,EAAA;AACnB,EAAA;AAIgB,IAAA;AACI,MAAA;AAChC,IAAA;AAC8B,IAAA;AAChC,EAAA;AACqC,EAAA;AACP,EAAA;AAChC;AvCq6JqC;AACA;AwC9yKL;AAyB9B;AAQ8B,EAAA;AACN,EAAA;AACwB,EAAA;AAC9C,IAAA;AACiB,IAAA;AACG,IAAA;AACpB,IAAA;AACA,EAAA;AACJ;AAmC4B;AACJ;AACH;AAYoC;AAIpB,EAAA;AACnB,EAAA;AACT,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACT,IAAA;AACS,MAAA;AACX,EAAA;AACF;AAwBqE;AAGjC,EAAA;AACT,EAAA;AACE,EAAA;AACD,EAAA;AACO,EAAA;AACD,EAAA;AAER,EAAA;AACO,EAAA;AAEG,EAAA;AACL,EAAA;AACZ,EAAA;AACa,EAAA;AACE,EAAA;AACE,EAAA;AACJ,EAAA;AAED,EAAA;AACCC,EAAAA;AACL,IAAA;AACG,IAAA;AACA,IAAA;AACI,MAAA;AACH,MAAA;AACvB,QAAA;AACF,MAAA;AAEgB,MAAA;AACS,QAAA;AACvB,QAAA;AACF,MAAA;AAC4B,MAAA;AACR,MAAA;AACO,MAAA;AACF,QAAA;AACzB,MAAA;AACF,IAAA;AACF,EAAA;AAE+B,EAAA;AACD,EAAA;AACK,IAAA;AACF,IAAA;AACP,IAAA;AAC1B,EAAA;AACO,EAAA;AACT;AAU4E;AACjC,EAAA;AAGV,EAAA;AACN,EAAA;AAChB,IAAA;AACT,EAAA;AAC2B,EAAA;AACF,EAAA;AACA,EAAA;AACS,EAAA;AACpC;AxCwrKqC;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/dist/chunk-J7FHVZII.cjs","sourcesContent":[null,"import { InvalidInputError } from '../../core/errors/index.js';\nimport { includeAt, type PositionalRng, type SeedPath } from '../../core/random/index.js';\nimport {\n assertArray,\n assertFiniteNumber,\n assertFunction,\n assertInteger,\n assertRecord,\n describeRejected,\n} from '../../core/validation/index.js';\n\n/**\n * The samplers a generator needs, each addressed by the position it is drawing\n * for rather than by how many draws came before it.\n *\n * Every method is a pure function of the source and the path: asking twice\n * gives the same answer, asking in a different order changes nothing, and a\n * position nobody asks about costs nothing. That is what lets a caller change\n * one parameter — or one bar — without redrawing the rest of the piece.\n *\n * @category Composition\n */\nexport type Draw = {\n /** The raw draw at a position: a float in [0, 1). */\n at: (...path: SeedPath) => number;\n /**\n * Whether an event of probability `p` happens at a position. Because the\n * underlying draw is fixed by the path, raising `p` only ever turns more\n * positions on.\n */\n prob: (p: number, ...path: SeedPath) => boolean;\n /** An integer in the inclusive range [lo, hi] at a position. */\n range: (lo: number, hi: number, ...path: SeedPath) => number;\n /** A float in [lo, hi) at a position. */\n float: (lo: number, hi: number, ...path: SeedPath) => number;\n};\n\n/**\n * The samplers a {@link Draw} carries, in declaration order.\n *\n * Named once so the check below cannot fall behind the type: a sampler added to\n * `Draw` is added here on the same commit, and the check that the two agree is\n * the suite's rather than a reader's.\n */\nexport const DRAW_METHODS = ['at', 'prob', 'range', 'float'] as const;\n\n/**\n * Read a sampler set as one, before a generator draws from it.\n *\n * Every generator takes a `Draw` and calls straight into it, so a caller that\n * built its own — or restored one from a session where the methods did not\n * survive — reaches the draw as a call on something that is not a function, deep\n * inside the generator. Checked here, it is refused by the name of the sampler\n * that is missing.\n *\n * @param draw The sampler set as the caller passed it.\n * @param name What the value is, for the error message.\n * @returns The sampler set.\n * @throws If it is not an object carrying all four samplers.\n */\nexport function assertDraw(draw: Draw, name = 'draw'): Draw {\n const sampler = assertRecord<Draw>(draw, name);\n for (const method of DRAW_METHODS) {\n assertFunction(sampler[method], `${name}.${method}`);\n }\n return sampler;\n}\n\n/**\n * Read the path a draw is addressed by, before it names a position.\n *\n * The path is what makes a draw reproducible: the same path is the same number,\n * for the same source, forever. A caller assembling one from its own ids can\n * put a `null` or an `undefined` in it — a track that has no name yet, an index\n * that was not computed — and nothing downstream objects, because a path\n * segment is only ever concatenated into a seed string. What the caller gets is\n * a different piece of music under a path it thought it had named, and two\n * different requests that seed identically. Reading the path here refuses that\n * by the segment that is wrong.\n *\n * @param path The path segments as the caller passed them.\n * @param name What the path is, for the error message.\n * @returns The same segments.\n * @throws If a segment is neither a string nor a finite number.\n */\nexport function assertSeedPath(path: SeedPath, name = 'draw path'): SeedPath {\n const steps = assertArray<SeedPath[number]>(path, name);\n for (let index = 0; index < steps.length; index += 1) {\n const step = steps[index];\n if (typeof step !== 'string' && !Number.isFinite(step)) {\n throw new InvalidInputError(\n `${name}[${index}] must be a string or a finite number; received ${describeRejected(step)}`,\n );\n }\n }\n return steps;\n}\n\n/**\n * One draw from a positional source, held to the range the source promises.\n *\n * A caller-supplied {@link PositionalRng} is part of the contract, so a source\n * returning 1, a negative value or NaN is rejected here rather than becoming an\n * index past the end of a vocabulary, a NaN pitch, or a bar silently dropped far\n * from the cause. This is the guard `includeAt` already applies to `prob`, on\n * the same predicate.\n */\nfunction drawFrom(rng: PositionalRng, path: SeedPath): number {\n const draw = rng.at(...path);\n if (!(draw >= 0 && draw < 1)) {\n throw new InvalidInputError(`positional draw must be in [0, 1); received ${draw}`);\n }\n return draw;\n}\n\n/**\n * Build the sampler set for one namespace of a positional source.\n *\n * The prefix separates parts that share a source: a caller-supplied\n * {@link PositionalRng} is used by every generator at once, and without a\n * prefix the drums' bar 3 and the bass's bar 3 would draw the same number.\n *\n * @param rng The positional source.\n * @param prefix Path segments prepended to every draw.\n * @returns The samplers, addressed under `prefix`.\n */\nexport function drawsFrom(rng: PositionalRng, ...prefix: SeedPath): Draw {\n const at = (...path: SeedPath) => drawFrom(rng, [...prefix, ...path]);\n return {\n at,\n prob: (p, ...path) => includeAt(rng, p, ...prefix, ...path),\n range: (lo, hi, ...path) => {\n assertInteger(lo, 'range lower bound');\n assertInteger(hi, 'range upper bound');\n if (lo > hi) {\n throw new InvalidInputError(\n `range lower bound must not exceed upper bound; received ${lo} > ${hi}`,\n );\n }\n // The span is what the draw is scaled by, so a pair whose difference is\n // not itself an exact safe integer would be sampled at reduced precision\n // and could land outside [lo, hi]. `Rng.range` rejects such a pair; the\n // two samplers promise the same inclusive range and so reject the same\n // arguments.\n assertInteger(hi - lo, 'range span', 0);\n return lo + Math.floor(at(...path) * (hi - lo + 1));\n },\n float: (lo, hi, ...path) => {\n assertFiniteNumber(lo, 'float lower bound');\n assertFiniteNumber(hi, 'float upper bound');\n if (lo > hi) {\n throw new InvalidInputError(\n `float lower bound must not exceed upper bound; received ${lo} > ${hi}`,\n );\n }\n // The span scales the draw, so a pair whose difference is not finite\n // would sample outside [lo, hi). `Rng.float` rejects such a pair, and the\n // two samplers promise the same range.\n assertFiniteNumber(hi - lo, 'float span');\n return lo + at(...path) * (hi - lo);\n },\n };\n}\n","/** General MIDI percussion note numbers used across the drum engine. */\nexport const GM = {\n BD: 36,\n SD: 38,\n SIDESTICK: 37,\n HANDCLAP: 39,\n CHH: 42,\n FHH: 44,\n OHH: 46,\n CRASH: 49,\n RIDE: 51,\n TAMBOURINE: 54,\n TOM_H: 50,\n TOM_M: 47,\n TOM_L: 45,\n SHAKER: 82,\n} as const;\n\n/** One quarter, one eighth and one sixteenth expressed in quarter-note beats. */\nexport const QUARTER = 1;\nexport const EIGHTH = 0.5;\nexport const SIXTEENTH = 0.25;\n\n/** Internal drum style selected by the public groove style. */\nexport type DrumStyle =\n | 'sparse'\n | 'standard'\n | 'fourOnFloor'\n | 'upbeat'\n | 'rock'\n | 'synth'\n | 'trap'\n | 'latin';\n\n/** Every groove feel, in declaration order. */\nexport const DRUM_FEELS = ['straight', 'swing', 'shuffle'] as const;\n\n/** Groove feel controlling off-beat swing. */\nexport type Feel = (typeof DRUM_FEELS)[number];\n\n/**\n * Groove feel (the swing/straight rhythmic character), under the name the\n * public surface uses.\n *\n * @category Composition\n */\nexport type GrooveFeel = Feel;\n\n/** Reduced section set the engine reasons about. */\nexport type SectionType = 'intro' | 'a' | 'b' | 'chorus' | 'bridge' | 'outro';\n\n/** Rhythmic density assumed when the caller names none. */\nexport const DEFAULT_RHYTHMIC = 0.5;\n\n/** Section energy level, used to shape fills. */\nexport type SectionEnergy = 'low' | 'medium' | 'high' | 'peak';\n\n/** Ghost-note mood category for the density table. */\nexport enum MoodCategory {\n Calm = 0,\n Standard = 1,\n Energetic = 2,\n}\n\n/** Percussion mood category for the activation table. */\nexport enum PercMoodCategory {\n Calm = 0,\n Standard = 1,\n Energetic = 2,\n Idol = 3,\n RockDark = 4,\n}\n\n/**\n * Every public groove style, in declaration order. The type is derived from\n * this list so the runtime guard and the type can never disagree.\n */\nexport const GROOVE_STYLES = [\n 'standard',\n 'funk',\n 'shuffle',\n 'bossa',\n 'trap',\n 'halftime',\n 'breakbeat',\n 'house',\n 'synthpop',\n] as const;\n\n/**\n * Public groove style identifiers.\n *\n * @category Composition\n */\nexport type GrooveStyle = (typeof GROOVE_STYLES)[number];\n\n/** Every public role, in declaration order. */\nexport const DRUM_ROLES = ['full', 'ambient', 'minimal', 'fxOnly'] as const;\n\n/**\n * Public role identifiers gating which voices are present.\n *\n * @category Composition\n */\nexport type DrumRole = (typeof DRUM_ROLES)[number];\n\n/** Every public section, in declaration order. */\nexport const PUBLIC_SECTIONS = [\n 'intro',\n 'verse',\n 'prechorus',\n 'chorus',\n 'bridge',\n 'outro',\n] as const;\n\n/**\n * Public section identifiers for {@link generateDrums}.\n *\n * These are the names a caller writes. The generator maps them onto a\n * narrower internal set the pattern tables are keyed by, which answers to\n * `'a'` and `'b'` and refuses `'verse'` and `'prechorus'`. That set is not\n * published: a caller who reached for it by name would be held to a\n * vocabulary that is not this one.\n *\n * @category Composition\n */\nexport type Section = (typeof PUBLIC_SECTIONS)[number];\n\n/**\n * The beats of a bar that carry the backbeat, zero-based.\n *\n * Every voice that reinforces the backbeat reads this one definition: when a\n * style moves the backbeat to beat 3, the hand-claps and the tambourine move\n * with it instead of staying where a 2-and-4 groove would have put them, which\n * is two voices disagreeing about where the backbeat of the bar is.\n *\n * @param snareBeat3 Whether the style states its backbeat on beat 3 alone.\n * @param barBeats Beats in the bar.\n */\nexport function backbeatBeats(snareBeat3: boolean, barBeats = 4): number[] {\n if (snareBeat3) {\n return barBeats > 2 ? [2] : [];\n }\n const beats: number[] = [];\n for (let beat = 1; beat < barBeats; beat += 2) {\n beats.push(beat);\n }\n return beats;\n}\n\n/** Resolved internal parameters for a public groove style. */\nexport type StyleMapping = {\n style: DrumStyle;\n feel: Feel;\n ghostBoost: boolean;\n snareBeat3: boolean;\n};\n\nconst STYLE_MAP: Record<GrooveStyle, StyleMapping> = {\n // Standard pop pulse: kick on beats 1 and 3, backbeat snare on 2 and 4.\n standard: { style: 'standard', feel: 'straight', ghostBoost: false, snareBeat3: false },\n funk: { style: 'upbeat', feel: 'straight', ghostBoost: true, snareBeat3: false },\n shuffle: { style: 'standard', feel: 'shuffle', ghostBoost: false, snareBeat3: false },\n bossa: { style: 'latin', feel: 'straight', ghostBoost: false, snareBeat3: false },\n trap: { style: 'trap', feel: 'straight', ghostBoost: false, snareBeat3: true },\n halftime: { style: 'sparse', feel: 'straight', ghostBoost: false, snareBeat3: true },\n breakbeat: { style: 'rock', feel: 'straight', ghostBoost: false, snareBeat3: false },\n // Four-on-the-floor dance kick with idol-style auxiliary percussion.\n house: { style: 'fourOnFloor', feel: 'straight', ghostBoost: false, snareBeat3: false },\n // Synth-pop pulse with denser 16th hi-hats and busy kick syncopation.\n synthpop: { style: 'synth', feel: 'straight', ghostBoost: true, snareBeat3: false },\n};\n\n/** Resolve the internal style/feel parameters for a public groove style. */\nexport function mapStyle(style: GrooveStyle): StyleMapping {\n return STYLE_MAP[style];\n}\n\n/** Map a public section to the internal section type. */\nexport function mapSection(section: Section): SectionType {\n switch (section) {\n case 'intro':\n return 'intro';\n case 'verse':\n return 'a';\n case 'prechorus':\n return 'b';\n case 'chorus':\n return 'chorus';\n case 'bridge':\n return 'bridge';\n case 'outro':\n return 'outro';\n }\n}\n\n/**\n * How loudly the backing sits, as a factor on the section's density.\n *\n * The dial is read as the continuous quantity it is, which is what lets it be a\n * slider: bucketing it into three levels would make two thirds of the travel do\n * nothing and the remaining third jump by half.\n */\nexport function backingScale(rhythmic: number): number {\n return BACKING_SCALE_FLOOR + (BACKING_SCALE_CEILING - BACKING_SCALE_FLOOR) * rhythmic;\n}\n\n/** Backing weight at the bottom and the top of the rhythmic dial. */\nconst BACKING_SCALE_FLOOR = 0.75;\nconst BACKING_SCALE_CEILING = 1.15;\n\n/**\n * A probability leaned on by the rhythmic dial: unchanged at the middle\n * setting, halved at the bottom, half again as likely at the top.\n *\n * Comparing the result against a draw fixed by position is what makes the dial\n * monotone — raising it can only turn an onset on, never off.\n */\nexport function leanedBy(probability: number, rhythmic: number): number {\n return Math.max(0, Math.min(1, probability * (0.5 + rhythmic)));\n}\n\n/**\n * Swing amount (0..1) implied by a groove feel, as a share of the full triplet\n * displacement.\n *\n * A shuffle is the triplet itself — that is what the word names — so it is the\n * whole of it; a swing leans two thirds of the way, which is what separates the\n * two as rhythmic characters rather than as two settings of the same one.\n */\nexport function feelSwingAmount(feel: Feel): number {\n if (feel === 'swing') {\n return 2 / 3;\n }\n if (feel === 'shuffle') {\n return 1;\n }\n return 0;\n}\n\n/** Ghost-note mood category derived from the internal style. */\nexport function ghostMoodCategory(style: DrumStyle): MoodCategory {\n if (style === 'sparse') {\n return MoodCategory.Calm;\n }\n if (style === 'trap' || style === 'upbeat') {\n return MoodCategory.Energetic;\n }\n return MoodCategory.Standard;\n}\n\n/** Percussion mood category derived from the internal style. */\nexport function percMoodCategory(style: DrumStyle): PercMoodCategory {\n switch (style) {\n case 'sparse':\n return PercMoodCategory.Calm;\n case 'upbeat':\n case 'latin':\n return PercMoodCategory.Energetic;\n case 'fourOnFloor':\n return PercMoodCategory.Idol;\n case 'trap':\n case 'rock':\n return PercMoodCategory.RockDark;\n default:\n return PercMoodCategory.Standard;\n }\n}\n\n/** Zero-based section index shared by the ghost and percussion tables. */\nexport function sectionIndex(section: SectionType): number {\n switch (section) {\n case 'intro':\n return 0;\n case 'a':\n return 1;\n case 'b':\n return 2;\n case 'chorus':\n return 3;\n case 'bridge':\n return 4;\n case 'outro':\n return 6;\n }\n}\n\n/** Section energy used to size fills. */\nexport function sectionEnergy(section: SectionType): SectionEnergy {\n switch (section) {\n case 'intro':\n case 'outro':\n return 'low';\n case 'a':\n case 'bridge':\n return 'medium';\n case 'b':\n return 'high';\n case 'chorus':\n return 'peak';\n }\n}\n\n/** Section velocity multiplier applied to the base drum velocity. */\nexport function sectionVelocityMultiplier(section: SectionType): number {\n switch (section) {\n case 'intro':\n case 'outro':\n return 0.8;\n case 'a':\n return 0.9;\n case 'b':\n return 1.0;\n case 'chorus':\n return 1.1;\n case 'bridge':\n return 0.85;\n }\n}\n\n/** Section density multiplier before backing-density scaling. */\nexport function sectionDensityMultiplier(section: SectionType): number {\n switch (section) {\n case 'intro':\n return 0.5;\n case 'a':\n return 0.7;\n case 'b':\n return 0.85;\n case 'chorus':\n return 1.0;\n case 'bridge':\n return 0.6;\n case 'outro':\n return 0.6;\n }\n}\n\n/** Base drum velocity for a section beat (downbeats accented). */\nexport function calculateVelocity(section: SectionType, beat: number): number {\n const base = 80;\n const beatAdj = beat === 0 ? 10 : beat === 2 ? 5 : 0;\n const velocity = (base + beatAdj) * sectionVelocityMultiplier(section);\n return clampVel(velocity);\n}\n\n/**\n * How far a backbeat sits above the velocity of the beat it lands on.\n *\n * The fill reads it too: the stroke a fill lands on has to arrive above the\n * backbeats around it, or the phrase end is heard as a dip rather than a lift.\n */\nexport const BACKBEAT_LIFT = 16;\n\n/** How far the stroke a fill lands on sits above a backbeat. */\nexport const FILL_ACCENT_LIFT = 4;\n\n/** Round and clamp a velocity into the valid MIDI range. */\nexport function clampVel(velocity: number): number {\n return Math.max(1, Math.min(127, Math.round(velocity)));\n}\n","/**\n * The shared vocabulary model: a remembered figure, plus the conditions under\n * which a player would reach for it.\n *\n * A rule-based generator can build a correct line and still sound characterless,\n * because a player does not synthesise from scales — they assemble figures they\n * already know. Those figures are not derivable, so they are held as data here:\n * one record per figure, carrying its own applicability conditions, so choosing\n * one is a deterministic lookup rather than a branch in a generator.\n *\n * ## Provenance\n *\n * No entry may reproduce a phrase from a particular recorded song. Only figures\n * that belong to nobody are admitted — the common currency of a genre, the\n * things every player of that music already plays. Every entry records which\n * basis it qualifies under in {@link Vocabulary.provenance}, and the field is\n * required so the judgment cannot be skipped while curating.\n */\n\nimport { InvalidInputError } from '../../core/errors/index.js';\nimport { ARTICULATIONS, type Articulation } from '../../core/instrument/index.js';\nimport {\n type MeterLike,\n meterAt,\n type TimeSignature,\n toMeterData,\n} from '../../core/meter/index.js';\nimport {\n assertArray,\n assertFunction,\n assertOneOf,\n assertRange,\n assertRecord,\n assertTimeSignature,\n describeRejected,\n} from '../../core/validation/index.js';\nimport { type ChordQuality, chordQualities } from '../../theory/chord/index.js';\nimport { assertDraw, assertSeedPath, type Draw } from '../context/draw.js';\nimport { PUBLIC_SECTIONS, type Section } from '../drums/internal.js';\n\n/**\n * Every genre name a vocabulary entry may carry, in declaration order.\n *\n * Each built-in dictionary stocks part of the list, not all of it: the drum\n * patterns carry `motown`, `funk`, `blues`, `bossa`, `samba`, `gospel` and\n * `dnb`; the bass licks carry `motown`, `soul`, `funk`, `blues`, `jazz`,\n * `bossa`, `gospel`, `country` and `reggae`. Asking a dictionary for a genre it\n * has nothing for selects nothing rather than failing.\n *\n * The list is open in spirit even though the type is closed: a caller with a\n * genre of its own — or one the built-ins do not stock — supplies entries\n * through the generation context rather than waiting for this list to grow.\n *\n * @category Composition\n */\nexport const GENRES = Object.freeze([\n 'pop',\n 'rock',\n 'motown',\n 'funk',\n 'soul',\n 'gospel',\n 'jazz',\n 'blues',\n 'bossa',\n 'samba',\n 'reggae',\n 'country',\n 'hiphop',\n 'house',\n 'dnb',\n] as const);\n\n/**\n * The genre a figure belongs to. Genre is what selects material; it never\n * deforms or rejects it.\n *\n * @category Composition\n */\nexport type Genre = (typeof GENRES)[number];\n\n/**\n * Every ground on which a figure may enter a dictionary, in declaration order.\n *\n * - `'idiom'`: a genre-defining figure no one owns, played by every player of\n * that music — a backbeat, a walking approach, a two-and-four side stick.\n * - `'construction'`: derived here from the genre's rhythmic grid rather than\n * remembered from any performance.\n * - `'traditional'`: from a folk or traditional repertoire with no author.\n *\n * @category Composition\n */\nexport const PROVENANCE_BASES = Object.freeze(['idiom', 'construction', 'traditional'] as const);\n\n/**\n * The ground on which a figure qualifies for a dictionary.\n *\n * @category Composition\n */\nexport type ProvenanceBasis = (typeof PROVENANCE_BASES)[number];\n\n/**\n * Why a figure may be published as vocabulary.\n *\n * @category Composition\n */\nexport type Provenance = {\n /** Which ground the figure qualifies under. */\n basis: ProvenanceBasis;\n /**\n * What the figure is, in the terms a player would use — \"the genre's\n * two-and-four side stick\", not the name of a record it appears on.\n */\n note: string;\n};\n\n/**\n * One remembered figure, together with the conditions it fits.\n *\n * The optional conditions are read as \"no restriction\" when absent: an entry\n * with no `sections` fits every section, one with no `tempoRange` fits every\n * tempo. {@link Vocabulary.difficulty} is on the same 1..5 scale as the\n * playability report, and is compared against the difficulty ceiling: an entry\n * harder than the ceiling is rejected, never simplified in place.\n *\n * @typeParam T - The material this kind of figure is made of: chord-relative\n * degrees for a bass lick, voices on a grid for a drum pattern.\n *\n * @category Composition\n */\nexport type Vocabulary<T> = {\n /** Stable identifier; a caller entry with the same id replaces a built-in. */\n id: string;\n genre: Genre;\n /** The figure itself. */\n material: T;\n /**\n * How the figure is played, over and above the material's own strokes.\n *\n * These are requirements, not decoration: where a query names the techniques\n * the instrument can produce, an entry asking for one the instrument does not\n * have is not offered. A kit with no flam is never handed flam material.\n */\n articulations: Articulation[];\n /** Chord qualities the figure works over; absent means any chord. */\n fitsOver?: ChordQuality[];\n /** Sections the figure suits; absent means any section. */\n sections?: Section[];\n /** Inclusive tempo band in BPM; absent means any tempo. */\n tempoRange?: [number, number];\n /** Time signature the figure is written in; absent means any. */\n ts?: TimeSignature;\n /** How hard the figure is to play, from 1 to 5. */\n difficulty: number;\n /** Why this figure may be published; see the module documentation. */\n provenance: Provenance;\n};\n\n/**\n * What a generator knows about the moment it is choosing a figure for.\n *\n * Every field is optional and an absent field asks nothing: a query with no\n * genre matches every genre, exactly as an entry with no `sections` matches\n * every section.\n *\n * @category Composition\n */\nexport type VocabularyQuery = {\n genre?: Genre;\n section?: Section;\n /** Tempo in BPM, matched against each entry's band. */\n bpm?: number;\n /** The meter in force, matched against each entry's own signature. */\n ts?: MeterLike;\n /** The difficulty ceiling: entries above it are rejected. */\n difficulty?: number;\n /** The chord in force, matched against each entry's `fitsOver`. */\n quality?: ChordQuality;\n /**\n * The techniques the instrument this part is written for can produce, from\n * its profile. An entry asking for one that is not here is not offered;\n * absent asks nothing, which is the programmed case.\n */\n articulations?: readonly Articulation[];\n};\n\n/** Whether two time signatures name the same bar. */\nfunction sameTs(a: TimeSignature, b: TimeSignature): boolean {\n return a.numerator === b.numerator && a.denominator === b.denominator;\n}\n\n/**\n * Whether one entry answers a query.\n *\n * @param entry The dictionary entry.\n * @param query What the generator knows about the moment.\n * @returns True when every condition the entry states is met.\n *\n * @category Composition\n */\nexport function fitsQuery<T>(entry: Vocabulary<T>, query: VocabularyQuery): boolean {\n // Both sides are read field by field below, so both are read as records\n // first: a `null` from a JavaScript caller or a restored session would report\n // this library's own `TypeError` from inside the comparison instead.\n const candidate = assertRecord<Vocabulary<T>>(entry, 'vocabulary entry');\n const asked = assertRecord<VocabularyQuery>(query, 'vocabulary query');\n if (asked.genre !== undefined && candidate.genre !== asked.genre) {\n return false;\n }\n if (\n asked.section !== undefined &&\n candidate.sections &&\n !candidate.sections.includes(asked.section)\n ) {\n return false;\n }\n if (asked.bpm !== undefined && candidate.tempoRange) {\n const [low, high] = candidate.tempoRange;\n if (asked.bpm < low || asked.bpm > high) {\n return false;\n }\n }\n if (\n asked.ts !== undefined &&\n candidate.ts &&\n !sameTs(candidate.ts, meterAt(0, toMeterData(asked.ts, 'ts')))\n ) {\n return false;\n }\n if (\n asked.quality !== undefined &&\n candidate.fitsOver &&\n !candidate.fitsOver.includes(asked.quality)\n ) {\n return false;\n }\n if (asked.articulations !== undefined) {\n const available = asked.articulations;\n if (candidate.articulations.some((articulation) => !available.includes(articulation))) {\n return false;\n }\n }\n // The ceiling is the one condition the entry does not state itself: it is the\n // caller's limit, and it only ever takes candidates away.\n return asked.difficulty === undefined || candidate.difficulty <= asked.difficulty;\n}\n\n/**\n * Every entry of a dictionary that answers a query, in dictionary order.\n *\n * Order is preserved so a selection made from the result is reproducible: the\n * same dictionary and the same query always offer the same candidates in the\n * same positions.\n *\n * @param dictionary The entries to search.\n * @param query What the generator knows about the moment.\n * @returns The matching entries, in the order they appear in the dictionary.\n *\n * @example\n * ```ts\n * import { BASS_LICKS, selectVocabulary } from '@libraz/libcantus';\n * const candidates = selectVocabulary(BASS_LICKS, { genre: 'motown', difficulty: 3 });\n * ```\n *\n * @category Composition\n */\nexport function selectVocabulary<T>(\n dictionary: readonly Vocabulary<T>[],\n query: VocabularyQuery,\n): Vocabulary<T>[] {\n return assertArray<Vocabulary<T>>(dictionary, 'dictionary').filter((entry) =>\n fitsQuery(entry, query),\n );\n}\n\n/**\n * Choose one entry from a dictionary at a position.\n *\n * The choice is addressed by the path, not by how many choices came before it,\n * so the figure at bar 12 does not move because the figure at bar 3 changed.\n *\n * @param dictionary The entries to choose from.\n * @param query What the generator knows about the moment.\n * @param draw The position-addressed sampler.\n * @param path Where the choice is being made.\n * @returns The chosen entry, or undefined when nothing fits.\n *\n * @category Composition\n */\nexport function pickVocabulary<T>(\n dictionary: readonly Vocabulary<T>[],\n query: VocabularyQuery,\n draw: Draw,\n ...path: readonly (string | number)[]\n): Vocabulary<T> | undefined {\n // Read before the early return: a dictionary that happens to select nothing\n // must not be the reason a malformed sampler goes unreported.\n const sampler = assertDraw(draw);\n assertSeedPath(path);\n const candidates = selectVocabulary(dictionary, query);\n if (candidates.length === 0) {\n return undefined;\n }\n return candidates[sampler.range(0, candidates.length - 1, ...path)];\n}\n\n/**\n * Merge a caller's dictionary into a built-in one.\n *\n * Caller entries are appended, so every built-in figure stays reachable, except\n * where a caller entry carries the id of a built-in: that one is a replacement\n * and takes the built-in's place, which is how a caller corrects a figure it\n * disagrees with rather than having both offered.\n *\n * @param builtIn The library's own entries.\n * @param supplied The caller's entries, if any.\n * @returns The merged dictionary, built-ins first.\n *\n * @category Composition\n */\nexport function mergeVocabulary<T>(\n builtIn: readonly Vocabulary<T>[],\n supplied: readonly Vocabulary<T>[] | undefined,\n): Vocabulary<T>[] {\n const library = assertArray<Vocabulary<T>>(builtIn, 'builtIn');\n const extra =\n supplied === undefined || supplied === null\n ? []\n : assertArray<Vocabulary<T>>(supplied, 'supplied');\n if (extra.length === 0) {\n return [...library];\n }\n // The id is what decides whether a caller's entry replaces a built-in, so\n // every entry on both sides is read as one before the ids are compared: an\n // entry that is not a record has no id to match on, and reading it as one\n // would report this library's own `TypeError` from inside the merge.\n const idOf = (entry: Vocabulary<T>, index: number, side: string): string =>\n assertRecord<Vocabulary<T>>(entry, `${side}[${index}]`).id;\n const byId = new Map(extra.map((entry, index) => [idOf(entry, index, 'supplied'), entry]));\n const merged = library.map((entry, index) => byId.get(idOf(entry, index, 'builtIn')) ?? entry);\n const replaced = new Set(library.map((entry, index) => idOf(entry, index, 'builtIn')));\n for (const entry of extra) {\n if (!replaced.has(entry.id)) {\n merged.push(entry);\n }\n }\n return merged;\n}\n\n/**\n * Narrow a mixed dictionary to the entries one part understands.\n *\n * A context carries a single list, because a caller thinks in terms of \"the\n * vocabulary of this piece\" rather than of one list per generator. Each\n * generator recognises its own material by shape, so an entry meant for the\n * bass is invisible to the drums instead of being mis-read by it.\n *\n * @param entries The mixed dictionary.\n * @param isMaterial Guard recognising one kind of material.\n * @returns The entries whose material the guard accepts.\n *\n * @category Composition\n */\nexport function vocabularyOfKind<T>(\n entries: readonly Vocabulary<unknown>[],\n isMaterial: (material: unknown) => material is T,\n): Vocabulary<T>[] {\n const out: Vocabulary<T>[] = [];\n const recognises = assertFunction<(material: unknown) => material is T>(isMaterial, 'isMaterial');\n for (const entry of assertArray<Vocabulary<unknown>>(entries, 'entries')) {\n if (recognises(assertRecord<Vocabulary<unknown>>(entry, 'entry').material)) {\n out.push(entry as Vocabulary<T>);\n }\n }\n return out;\n}\n\n/**\n * Check a list of names against the table that defines them, the list included.\n *\n * A required list that is absent is the case this exists for: reading it as a\n * list would report the library's own TypeError, where every other field of an\n * entry from a config file is refused by name.\n */\nfunction assertNames(values: unknown, allowed: readonly string[], label: string): void {\n if (!Array.isArray(values)) {\n throw new InvalidInputError(\n `${label} must be an array of names; received ${describeRejected(values)}`,\n );\n }\n values.forEach((value, index) => {\n assertOneOf(value, allowed, `${label}[${index}]`);\n });\n}\n\n/**\n * Reject a dictionary entry whose declared fields are outside their domains.\n *\n * Applied to caller-supplied entries at the point they enter a generator: an\n * entry with a difficulty of 40 would otherwise pass every ceiling and be\n * chosen for a beginner's part.\n *\n * @param entry The entry to check.\n * @param label Name used in the error message.\n * @returns The entry, unchanged.\n * @throws If the difficulty is outside 1..5 or the tempo band is inverted.\n *\n * @category Composition\n */\nexport function assertVocabulary<T>(\n entry: Vocabulary<T>,\n label = 'vocabulary entry',\n): Vocabulary<T> {\n // The names are checked against the tables that define them rather than\n // against a list written out again here: a genre or an articulation added to\n // the library is admitted by this check on the same commit, and a typo from a\n // JavaScript caller or a config file is a stated error instead of an entry\n // that silently matches nothing.\n if (typeof entry !== 'object' || entry === null) {\n throw new InvalidInputError(\n `${label} must be a dictionary entry; received ${describeRejected(entry)}`,\n );\n }\n assertOneOf(entry.genre, GENRES, `${label} genre`);\n assertOneOf(entry.provenance?.basis, PROVENANCE_BASES, `${label} provenance basis`);\n assertNames(entry.articulations, ARTICULATIONS, `${label} articulations`);\n if (entry.sections !== undefined) {\n assertNames(entry.sections, PUBLIC_SECTIONS, `${label} sections`);\n }\n if (entry.fitsOver !== undefined) {\n assertNames(entry.fitsOver, chordQualities(), `${label} fitsOver`);\n }\n if (entry.ts !== undefined) {\n assertTimeSignature(entry.ts, `${label} ts`);\n }\n assertRange(entry.difficulty, 1, 5, `${label} difficulty`);\n if (entry.tempoRange !== undefined) {\n if (!Array.isArray(entry.tempoRange) || entry.tempoRange.length !== 2) {\n throw new InvalidInputError(\n `${label} tempoRange must be a low and a high tempo; received ${describeRejected(entry.tempoRange)}`,\n );\n }\n const [low, high] = entry.tempoRange;\n assertRange(low, Number.MIN_VALUE, 1000, `${label} tempoRange low`);\n assertRange(high, low, 1000, `${label} tempoRange high`);\n }\n return entry;\n}\n","/**\n * The playing-difficulty ceiling, as the generators apply it.\n *\n * A ceiling is not an intensity: a passage can be harmonically involved and\n * easy to play, or plain and hard. So difficulty never proposes material — it\n * only rejects what the other complexity dials proposed, and only on the\n * timing layer, where speed is what stands in the way. Whether the note exists\n * on the instrument at all, and whether the hand can hold the shape, belong to\n * the instrument profile and hold regardless of the ceiling.\n */\n\nimport {\n assertFiniteNumber,\n assertFiniteSemitones,\n assertRange,\n} from '../../core/validation/index.js';\n\n/** The ceiling's lowest level: what a beginner sustains. */\nexport const MIN_DIFFICULTY = 1;\n\n/** The ceiling's highest level: what a rudimental player sustains. */\nexport const MAX_DIFFICULTY = 5;\n\n/**\n * Strokes per second one limb sustains at each level, from a beginner's plain\n * eighths to the wrist speed a rudimental player keeps up.\n */\nconst STROKES_PER_SECOND = [3, 5, 8, 12, 16] as const;\n\n/**\n * Frets per second the fretting hand travels at each level. A shift is a\n * displacement, so the figure is a rate rather than a count of notes.\n */\nconst FRETS_PER_SECOND = [6, 12, 20, 32, 50] as const;\n\n/** Seconds one quarter-note beat lasts at a tempo. */\nfunction secondsPerBeat(bpm: number): number {\n return 60 / bpm;\n}\n\n/** Read a per-level table at a possibly fractional level, interpolating. */\nfunction atLevel(table: readonly number[], difficulty: number): number {\n const level = Math.min(MAX_DIFFICULTY, Math.max(MIN_DIFFICULTY, difficulty));\n const lower = Math.floor(level);\n const upper = Math.ceil(level);\n const low = table[lower - 1] ?? table[0] ?? 0;\n const high = table[upper - 1] ?? low;\n return low + (high - low) * (level - lower);\n}\n\n/** Reject a ceiling outside the scale the profiles and reports share. */\nexport function assertDifficulty(difficulty: number, label = 'complexity difficulty'): number {\n return assertRange(difficulty, MIN_DIFFICULTY, MAX_DIFFICULTY, label);\n}\n\n/**\n * Whether a player at this ceiling keeps up a stream of strokes on one voice.\n *\n * @param stepBeats Gap between consecutive strokes, in quarter-note beats.\n * @param bpm Tempo, or undefined when the caller gave none.\n * @param difficulty The ceiling, or undefined for no ceiling.\n * @returns True when the stream is inside the ceiling, and whenever either the\n * ceiling or the tempo is unknown — a limit nobody stated constrains nothing.\n */\nexport function sustainsStrokes(\n stepBeats: number,\n bpm: number | undefined,\n difficulty: number | undefined,\n): boolean {\n if (difficulty === undefined || bpm === undefined) {\n return true;\n }\n // A ceiling off the shared scale reads off the end of the rate table and\n // falls back to its first row, so a ceiling of 20 would be judged as the\n // strictest one there is — the opposite of what asking for 20 means.\n assertDifficulty(difficulty);\n assertFiniteNumber(bpm, 'bpm');\n if (assertFiniteNumber(stepBeats, 'stepBeats') <= 0) {\n return true;\n }\n const rate = 1 / (stepBeats * secondsPerBeat(bpm));\n return rate <= atLevel(STROKES_PER_SECOND, difficulty);\n}\n\n/**\n * Whether the fretting hand covers a leap in the time available.\n *\n * @param semitones Distance moved; on a fretted neck one fret is one semitone.\n * @param stepBeats Time to move it in, in quarter-note beats.\n * @param bpm Tempo, or undefined when the caller gave none.\n * @param difficulty The ceiling, or undefined for no ceiling.\n * @returns True when the shift is inside the ceiling, and whenever either the\n * ceiling or the tempo is unknown.\n */\nexport function sustainsShift(\n semitones: number,\n stepBeats: number,\n bpm: number | undefined,\n difficulty: number | undefined,\n): boolean {\n if (difficulty === undefined || bpm === undefined) {\n return true;\n }\n // Read against the same scale as its sibling above, and for the same reason.\n assertDifficulty(difficulty);\n assertFiniteNumber(bpm, 'bpm');\n assertFiniteSemitones(semitones);\n if (assertFiniteNumber(stepBeats, 'stepBeats') <= 0) {\n return true;\n }\n const seconds = stepBeats * secondsPerBeat(bpm);\n return Math.abs(semitones) / seconds <= atLevel(FRETS_PER_SECOND, difficulty);\n}\n","/**\n * The generation context: one seed, one set of dials, one tempo, shared by\n * every generator.\n *\n * Before it, each generator carried its own idea of \"how involved should this\n * be\" — a 0..1 density here, a style name there, a boolean somewhere else — and\n * only the drums knew the tempo. A context names those things once, so a piece\n * can be asked for at a given complexity in one place, and every part derives\n * its own randomness from the same project seed.\n */\n\nimport { InvalidInputError } from '../../core/errors/index.js';\nimport type { InstrumentProfile } from '../../core/instrument/index.js';\nimport {\n createPositionalRng,\n deriveSeed,\n type PositionalRng,\n resolveAlgorithmVersion,\n} from '../../core/random/index.js';\nimport { assertInteger, assertRange, assertRecord } from '../../core/validation/index.js';\nimport { assertVocabulary, type Vocabulary } from '../vocabulary/types.js';\nimport { assertDifficulty } from './difficulty.js';\nimport { type Draw, drawsFrom } from './draw.js';\n\nexport {\n assertDifficulty,\n MAX_DIFFICULTY,\n MIN_DIFFICULTY,\n sustainsShift,\n sustainsStrokes,\n} from './difficulty.js';\nexport type { Draw } from './draw.js';\nexport { drawsFrom } from './draw.js';\n\n/**\n * How involved the generated music should be, on axes that do not imply one\n * another.\n *\n * The first three ask for more of something and are continuous: moving one of\n * them adds material without disturbing what is already sounding.\n * {@link Complexity.difficulty} is not one of those — it is a ceiling, and it\n * only ever takes candidates away.\n *\n * @category Composition\n */\nexport type Complexity = {\n /** Subdivision and syncopation, in [0, 1]. */\n rhythmic?: number;\n /** Tension, substitution and passing chords, in [0, 1]. */\n harmonic?: number;\n /** Ghosts, flams and decoration, in [0, 1]. */\n ornament?: number;\n /**\n * The hardest the result may be to play, from 1 (a beginner's part) to 5 (at\n * the limit of the instrument). It is a ceiling rather than a strength:\n * \"involved but easy\" and \"plain but hard\" are both real requests, so this\n * filters what the other three proposed instead of scaling it. It governs the\n * timing layer alone — whether the note exists on the instrument, and whether\n * the shape can be held, follow from {@link GenerationContext.instruments} and\n * apply whatever the ceiling says.\n */\n difficulty?: number;\n};\n\n/**\n * Everything a generator needs that is not about the notes themselves.\n *\n * @category Composition\n */\nexport type GenerationContext = {\n /**\n * The project seed every part derives its own randomness from. Optional, and\n * zero when left out: a context naming only a tempo is a whole request, and\n * requiring the seed beside it would make the commonest context unwritable\n * now that it is the only place a seed can go.\n */\n seed?: number;\n /** The dials; see {@link Complexity}. */\n complexity?: Complexity;\n /**\n * Draw from this source instead of one derived from `seed`. Each part still\n * addresses its own namespace, so parts do not collide.\n */\n rng?: PositionalRng;\n /**\n * The instruments the parts are written for, keyed by part name — `'bass'`,\n * `'drums'`. Naming one is itself the request that the part be playable on\n * it, so its range and physical limits always apply.\n */\n instruments?: Record<string, InstrumentProfile>;\n /**\n * Tempo in quarter-note beats per minute. Without it no generator can judge\n * whether a passage is reachable in the time available, so a ceiling on\n * difficulty has nothing to measure against.\n */\n bpm?: number;\n /**\n * The algorithm version to generate under; defaults to the version this build\n * produces. See {@link resolveAlgorithmVersion} for what the number promises.\n */\n algorithmVersion?: number;\n /**\n * Figures this piece may draw on, over and above the library's own.\n *\n * Genres are endless, which is the reason the vocabulary is data at all: a\n * caller brings the dictionary for the music it is writing instead of waiting\n * for the library to grow one. Entries for every part travel in one list —\n * each generator recognises its own material by shape — and an entry carrying\n * the id of a built-in replaces it rather than being offered alongside.\n */\n vocabulary?: readonly Vocabulary<unknown>[];\n};\n\n/**\n * A context, or just the seed for one.\n *\n * A bare number is sugar for `{ seed }`: the common case is a caller who wants\n * reproducible output and nothing else.\n *\n * @category Composition\n */\nexport type GenerationContextInput = GenerationContext | number;\n\n/**\n * A context with everything resolved: dials validated, tempo and ceiling\n * settled, and a positional source ready for each part.\n */\nexport type ResolvedContext = {\n seed: number;\n /** Requested rhythmic density, or undefined when the caller named none. */\n rhythmic: number | undefined;\n /** Requested harmonic richness, or undefined when the caller named none. */\n harmonic: number | undefined;\n /** Requested ornament density, or undefined when the caller named none. */\n ornament: number | undefined;\n /** The difficulty ceiling, or undefined for no ceiling. */\n difficulty: number | undefined;\n /** Tempo in BPM, or undefined when the caller named none. */\n bpm: number | undefined;\n /** The resolved algorithm version. */\n algorithmVersion: number;\n /** The profile written for a part, if one was named for it. */\n instrument: (part: string) => InstrumentProfile | undefined;\n /** The figures the caller brought, validated; empty when it brought none. */\n vocabulary: readonly Vocabulary<unknown>[];\n /** The position-addressed samplers for a part. */\n part: (name: string) => Draw;\n};\n\n/** The seed a generator uses when the caller names none. */\nconst DEFAULT_SEED = 0;\n\n/** Path segment separating one algorithm version's draws from another's. */\nconst VERSION_SEGMENT = 'v';\n\n/**\n * Resolve a context (or a bare seed) into the form the generators use.\n *\n * The algorithm version takes part in every derivation, so two versions never\n * share a draw: pinning one is what makes a saved project reopen as the piece\n * it was, and a version this build does not produce is rejected rather than\n * quietly rendered as another.\n *\n * @param input The context, the seed alone, or nothing.\n * @returns The resolved context.\n * @throws If the seed is not a 32-bit integer, a dial is out of range, the\n * tempo is not positive, or the algorithm version is unknown.\n * @example\n * ```ts\n * import { resolveContext } from '@libraz/libcantus';\n * const ctx = resolveContext({ seed: 42, complexity: { rhythmic: 0.8 }, bpm: 96 });\n * ctx.part('drums').prob(0.5, 'ghost', 1, 2); // the same answer however often it is asked\n * ```\n */\nexport function resolveContext(input?: GenerationContextInput): ResolvedContext {\n // Absent is the default context; anything else has to be a seed or a context,\n // so a `null` passed on by a caller that had none is refused rather than\n // silently answered with the defaults it looks nothing like.\n if (input !== null && typeof input !== 'number' && input !== undefined) {\n assertRecord<GenerationContext>(input, 'ctx');\n }\n if (input === null) {\n throw new InvalidInputError('ctx must be a seed or a generation context; received null');\n }\n const ctx: GenerationContext =\n typeof input === 'number' ? { seed: input } : (input ?? { seed: DEFAULT_SEED });\n const seed = assertInteger(ctx.seed ?? DEFAULT_SEED, 'seed', 0, 0xffffffff);\n const complexity = ctx.complexity ?? {};\n const rhythmic =\n complexity.rhythmic === undefined\n ? undefined\n : assertRange(complexity.rhythmic, 0, 1, 'complexity rhythmic');\n const harmonic =\n complexity.harmonic === undefined\n ? undefined\n : assertRange(complexity.harmonic, 0, 1, 'complexity harmonic');\n const ornament =\n complexity.ornament === undefined\n ? undefined\n : assertRange(complexity.ornament, 0, 1, 'complexity ornament');\n const difficulty =\n complexity.difficulty === undefined ? undefined : assertDifficulty(complexity.difficulty);\n const bpm =\n ctx.bpm === undefined ? undefined : assertRange(ctx.bpm, Number.MIN_VALUE, 1000, 'bpm');\n const algorithmVersion = resolveAlgorithmVersion(ctx.algorithmVersion);\n const supplied = ctx.rng;\n const instruments = ctx.instruments;\n // Validated on the way in: an entry naming a difficulty of 40 would otherwise\n // clear every ceiling and be chosen for a beginner's part.\n const vocabulary = (ctx.vocabulary ?? []).map((entry, index) =>\n assertVocabulary(entry, `vocabulary[${index}]`),\n );\n\n return {\n seed,\n rhythmic,\n harmonic,\n ornament,\n difficulty,\n bpm,\n algorithmVersion,\n instrument: (part) => instruments?.[part],\n vocabulary,\n part: (name) =>\n supplied\n ? drawsFrom(supplied, name, VERSION_SEGMENT, algorithmVersion)\n : drawsFrom(createPositionalRng(deriveSeed(seed, name, VERSION_SEGMENT, algorithmVersion))),\n };\n}\n\n/**\n * Merge a generator's own dial into a context.\n *\n * A generator that names one of the dials in its own vocabulary — the motif's\n * `jitter`, the ornament's `amount` — resolves it here, so the dial reaches the\n * generator by one path whichever surface named it. The context wins where both\n * are given, since it is the one thing that speaks for the whole piece.\n *\n * @param input The context or bare seed, if any.\n * @param sugar The generator's own dial.\n * @returns The resolved context, with the dial filled in where the context was\n * silent.\n */\nexport function resolveContextWith(\n input: GenerationContextInput | undefined,\n sugar: { ornament?: number },\n): ResolvedContext {\n const base = resolveContext(input);\n // The generator's own dial is validated whether or not it ends up being used:\n // whether the context also names the dial is not something the caller of the\n // generator can see, so an out-of-range value must be rejected the same way\n // either time. Only leaving it out skips the check.\n const supplied =\n sugar.ornament === undefined\n ? undefined\n : assertRange(sugar.ornament, 0, 1, 'complexity ornament');\n const ornament = base.ornament ?? supplied;\n return { ...base, ornament };\n}\n","/**\n * The placement rules a bass line is built from, shared by the phrase-shape\n * styles and the lick dictionary.\n *\n * They are the part of a bass part that is derivable — where in the register a\n * pitch class lands, which beats a segment offers, how one note leads into the\n * next. What is not derivable is which figure to play, and that is the\n * dictionary's business.\n */\n\nimport { BEAT_EPS } from '../../analyze/adjacency.js';\nimport { InvalidInputError } from '../../core/errors/index.js';\nimport type { InstrumentProfile, InstrumentProfileLike } from '../../core/instrument/index.js';\nimport {\n instrumentRange,\n type StringedProfile,\n toStringedProfile,\n} from '../../core/instrument/index.js';\nimport { beatsPerBar, pulseBeats, type TimeSignature } from '../../core/meter/index.js';\nimport { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport { assertRange } from '../../core/validation/index.js';\nimport type { Chord, ChordSegment } from '../../theory/chord/index.js';\nimport { nearestScaleTone } from '../../theory/scale/index.js';\nimport { fifthPcOf as chordFifthPcOf } from '../../theory/tendency/index.js';\n\n/** Velocity for notes on metrically strong positions. */\nexport const STRONG_VELOCITY = 100;\n\n/** Velocity for notes on weak positions. */\nexport const WEAK_VELOCITY = 80;\n\n/**\n * The meter a bass part is written against when the caller names none.\n *\n * Shared by both entry points because it is one default, not two that happen to\n * agree: a figure placed in one and a phrase shape built in the other have to\n * fall on the same grid for the two to be laid over each other.\n */\nexport const DEFAULT_TS: TimeSignature = { numerator: 4, denominator: 4 };\n\n/** The register band a bass part is written in when the caller names none. */\nexport const DEFAULT_OCTAVE = 2;\n\n/**\n * Place a pitch class as a MIDI note near an anchor, clamped to the bass band.\n *\n * The octave nearest the anchor is chosen first (so motion from the previous\n * note is minimal), then the result is shifted by whole octaves into the band\n * `[low, low + 12]` if it falls outside.\n */\nexport function placePc(pc: number, anchor: number, low: number): number {\n return foldIntoBand(pc + 12 * Math.round((anchor - pc) / 12), low);\n}\n\n/**\n * Place a pitch class above the bass a chord names, rather than in the band.\n *\n * A chord written over a bass is an inversion, and what makes it one is that\n * the named bass is the lowest note sounding. The band is an octave wide, so\n * folding the chord's other tones into it puts them under a bass placed\n * anywhere but the bottom of it: the C of a `C/E` came out below its own E,\n * which is a root-position C, and reading the line back reported the chord the\n * part was not written on.\n */\nexport function placeAboveBass(pc: number, bassMidi: number): number {\n return foldIntoBand(pc + 12 * Math.round((bassMidi - pc) / 12), bassMidi);\n}\n\n/**\n * Place a segment's bass note in the register band.\n *\n * The octave is a function of the pitch class and the band alone rather than of\n * whatever sounded last, so a chord that comes back lands on the note it landed\n * on before however far the line travelled in between: a two-chord vamp holds\n * the register the caller asked for instead of climbing out of it by its second\n * bar, and every degree measured from the root is then fixed by the chord too.\n */\nexport function placeRoot(pc: number, low: number): number {\n return low + pitchClass(pc - low);\n}\n\n/** A bar-long slice of a segment: the span one turn of a figure fills. */\nexport type BarTile = {\n startBeat: number;\n endBeat: number;\n};\n\n/**\n * The bars a segment spans, one tile per bar of it.\n *\n * A figure is a bar long and a chord is not: a ballad or a modal vamp holds one\n * chord for four of them. What a bass part writes over such a chord repeats\n * once per bar rather than being stretched across the whole span, so the bar\n * after the first is played rather than held. The tiles run from the segment's\n * own onset, which is where the harmony changed, and the last one is cut at the\n * next change.\n */\nexport function barTiles(startBeat: number, endBeat: number, ts: TimeSignature): BarTile[] {\n const barBeats = beatsPerBar(ts);\n const tiles: BarTile[] = [];\n for (let index = 0; ; index += 1) {\n const tileStart = startBeat + index * barBeats;\n if (tileStart >= endBeat - BEAT_EPS) {\n break;\n }\n tiles.push({ startBeat: tileStart, endBeat: Math.min(tileStart + barBeats, endBeat) });\n }\n return tiles;\n}\n\n/**\n * The pulse a bar's second half begins on, which is where an alternating bass\n * answers the root.\n *\n * The bar's midpoint need not be a pulse — a three-beat bar has none there — so\n * the pulse at or before it is taken, keeping the answer on the grid the meter\n * states rather than on an invented local one.\n */\nexport function midBarPulse(barStart: number, ts: TimeSignature): number {\n const pulse = pulseBeats(ts);\n return barStart + Math.floor((beatsPerBar(ts) / 2 + BEAT_EPS) / pulse) * pulse;\n}\n\n/**\n * Check a chord placement a bass part can be written over.\n *\n * Both entry points consume the same {@link ChordSegment} contract, so both ask\n * the same questions of it: a segment runs forward in time, and no two of them\n * sound at once. Overlapping segments are rejected rather than laid over each\n * other, because a bass part is monophonic and interleaving two lines produces\n * notes belonging to neither chord.\n *\n * @param segments The placement, sorted by onset.\n * @throws If a segment starts outside the timeline, has a non-positive\n * duration, or overlaps the segment before it.\n */\nexport function assertBassSegments(segments: readonly ChordSegment[]): void {\n for (let index = 0; index < segments.length; index += 1) {\n const segment = segments[index];\n if (segment === undefined) {\n continue;\n }\n assertRange(segment.startBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].startBeat`);\n assertRange(segment.endBeat, 0, Number.MAX_SAFE_INTEGER, `segments[${index}].endBeat`);\n if (segment.endBeat <= segment.startBeat) {\n throw new InvalidInputError(`segments[${index}] must have a positive duration`);\n }\n const previous = segments[index - 1];\n if (previous !== undefined && segment.startBeat < previous.endBeat - BEAT_EPS) {\n throw new InvalidInputError(\n `bass segments must not overlap: segments ${index - 1} and ${index} overlap`,\n );\n }\n }\n}\n\n/** Chord-tone pitch classes in stacked-thirds order, deduplicated. */\nexport function chordTonePcs(chord: Chord): number[] {\n const seen = new Set<number>();\n const pcs: number[] = [];\n for (const interval of chord.intervals) {\n const pc = pitchClass(chord.rootPc + interval);\n if (!seen.has(pc)) {\n seen.add(pc);\n pcs.push(pc);\n }\n }\n if (pcs.length === 0) {\n pcs.push(pitchClass(chord.rootPc));\n }\n return pcs;\n}\n\n/**\n * The chord's fifth pitch class, falling back to the root when it has none.\n *\n * Which tone is the fifth is the theory layer's reading, not a second one here:\n * a diminished fifth is the chord's fifth only where no perfect fifth stands\n * beside it, and beside one it is a sharp eleventh. Answering that here as well\n * gave dim, aug and m7b5 chords the right tone by agreeing with the definition\n * rather than by reading it.\n */\nexport function fifthPcOf(chord: Chord): number {\n return chordFifthPcOf(chord) ?? pitchClass(chord.rootPc);\n}\n\n/** The sounding bass pitch class of a chord: its slash bass, else its root. */\nexport function bassPcOf(chord: Chord): number {\n return pitchClass(chord.bassPc ?? chord.rootPc);\n}\n\n/**\n * The chord tones an arpeggiating line cycles through, starting from the bass.\n *\n * A slash bass says which note sounds under the chord, so it has to be the one\n * the cycle begins on; the chord's own tones then follow in stacked-thirds\n * order from where the bass sits among them. A bass the chord does not contain\n * is prefixed rather than substituted, so no chord tone is lost.\n */\nexport function bassToneCycle(chord: Chord): number[] {\n const bass = bassPcOf(chord);\n const tones = chordTonePcs(chord);\n const at = tones.indexOf(bass);\n return at < 0 ? [bass, ...tones] : [...tones.slice(at), ...tones.slice(0, at)];\n}\n\n/** Main-pulse positions within `[start, end)`, aligned to the global bar grid. */\nexport function beatPositions(start: number, end: number, ts: TimeSignature): number[] {\n const positions: number[] = [];\n const barBeats = beatsPerBar(ts);\n const pulse = pulseBeats(ts);\n const firstBar = Math.floor(start / barBeats);\n const lastBar = Math.floor((end - BEAT_EPS) / barBeats);\n for (let bar = firstBar; bar <= lastBar; bar += 1) {\n const barStart = bar * barBeats;\n const firstPulse = Math.max(0, Math.ceil((start - barStart - BEAT_EPS) / pulse));\n for (let index = firstPulse; ; index += 1) {\n const position = barStart + index * pulse;\n if (position >= barStart + barBeats - BEAT_EPS || position >= end - BEAT_EPS) {\n break;\n }\n if (position >= start - BEAT_EPS) {\n positions.push(position);\n }\n }\n }\n // A segment shorter than a pulse still needs a bass note. Its boundary is\n // necessarily the harmonic placement rather than an invented local grid.\n if (positions.length === 0) {\n positions.push(start);\n }\n return positions;\n}\n\n/**\n * Shift a pitch by whole octaves until it lies in the band `[low, low + 12]`.\n *\n * The octave count is computed rather than stepped, so a pitch arbitrarily far\n * from the band costs the same as one just outside it.\n */\nexport function foldIntoBand(midi: number, low: number): number {\n const raised = midi + 12 * Math.max(0, Math.ceil((low - midi) / 12));\n return raised - 12 * Math.max(0, Math.ceil((raised - low - 12) / 12));\n}\n\n/**\n * The register band's floor, moved by whole octaves until `[low, low + 12]`\n * lies inside the instrument.\n *\n * Moving the band rather than each note is what keeps the line intact: every\n * interval between consecutive notes survives, and only the register — the one\n * thing a player would have chosen differently — moves.\n *\n * Each move is computed as an octave count rather than stepped one octave at a\n * time, so a band far outside the instrument costs the same as one just outside\n * it. The band is dropped under the top fret first and then lifted over the\n * lowest string, so an instrument spanning less than an octave keeps its lowest\n * note reachable.\n */\nexport function bandFloor(low: number, instrument: StringedProfile | undefined): number {\n if (!instrument) {\n return low;\n }\n const range = instrumentRange(instrument);\n const dropped = low - 12 * Math.max(0, Math.ceil((low + 12 - range.high) / 12));\n return dropped + 12 * Math.max(0, Math.ceil((range.low - dropped) / 12));\n}\n\n/**\n * A diatonic or chromatic neighbor of `target`, a step toward `from`.\n *\n * The note stays where the step puts it, a semitone or a scale step from the\n * target, even when that is just outside the register band: leading into the\n * next chord is what this note is for, and folding it by an octave to keep it\n * in the band would turn the step into a leap of eleven semitones whenever the\n * target sat on the band's floor.\n *\n * Where the line already sits on that neighbour — the note it is coming from is\n * a step from the target — the neighbour on the far side of the target is taken\n * instead. The approach then comes from the other direction, which is the one\n * thing that cannot happen on the beat whose whole job is to move: repeating\n * the note just played would stop the line at the chord change. The far side is\n * taken for a neighbour outside the MIDI domain too, which is the only thing\n * that can happen to a target on the lowest accepted octave's floor.\n *\n * @param target The note being led into.\n * @param from Where the line is coming from, which decides the direction.\n * @param key Key context for the diatonic neighbour.\n * @param chromatic Whether to take the semitone rather than the scale step. The\n * choice is the caller's so that each caller addresses its own draw.\n */\nexport function approachNote(\n target: number,\n from: number,\n key: KeyScale,\n chromatic: boolean,\n): number {\n const dir = from <= target ? -1 : 1;\n const near = neighborOnSide(target, dir, key, chromatic);\n const far = neighborOnSide(target, -dir, key, chromatic);\n const playable = (midi: number): boolean => midi >= 0 && midi <= 127;\n if (playable(near) && near !== from) {\n return near;\n }\n return playable(far) ? far : near;\n}\n\n/** The neighbour of `target` on the side `dir` points to. */\nfunction neighborOnSide(target: number, dir: number, key: KeyScale, chromatic: boolean): number {\n const semitone = target + dir;\n if (chromatic) {\n return semitone;\n }\n const cand = nearestScaleTone(target + dir * 2, key);\n const step = Math.abs(cand - target);\n return step >= 1 && step <= 2 ? cand : semitone;\n}\n\n/**\n * The instrument a bass part is written for, and the floor of its register.\n *\n * Both generators ask this the same way and have to: naming an instrument on\n * one and on the other is one request — write this for that bass — and an\n * answer that differed between them would put the two parts of one arrangement\n * in different registers. A stringed instrument the context names is taken as\n * read; anything else has to be named on the call, since a part written for a\n * piano has no neck to fold a note back onto.\n *\n * @param named The instrument the generation context names for the bass, if any.\n * @param given The instrument the caller named, if any.\n * @param octave The register the part is written in.\n * @returns The instrument to write for, and the lowest pitch of the band.\n */\nexport function bassRegister(\n named: InstrumentProfile | undefined,\n given: InstrumentProfileLike | undefined,\n octave: number,\n): { instrument: StringedProfile | undefined; low: number } {\n const instrument =\n given === undefined\n ? named !== undefined && named.kind === 'stringed'\n ? named\n : undefined\n : toStringedProfile(given, 'instrument');\n return { instrument, low: bandFloor(octave * 12 + 12, instrument) };\n}\n","/**\n * The transformation rules that deform a remembered figure into the one this\n * bar needs.\n *\n * The division of labour is the whole point of keeping these separate from the\n * dictionary: **genre selects the material, complexity deforms it, difficulty\n * rejects it.** A transform therefore never consults a genre and never consults\n * a ceiling — it takes the figure it is given and reshapes it, and whether the\n * result is playable is decided afterwards, by {@link withinCeiling}.\n *\n * Every figure is a list of onsets on a sixteenth grid, whatever it is made of,\n * so the rules are written once and used by both the bass and the drums.\n */\n\nimport type { MeterLike, TimeSignature } from '../../core/meter/index.js';\nimport {\n isCompound,\n meterAt,\n metricWeight,\n pulseBeats,\n toMeterData,\n} from '../../core/meter/index.js';\nimport {\n assertArray,\n assertFiniteNumber,\n assertFunction,\n assertPositiveInt,\n assertRange,\n assertRecord,\n} from '../../core/validation/index.js';\nimport { assertDifficulty, sustainsStrokes } from '../context/difficulty.js';\nimport { assertDraw, assertSeedPath, type Draw } from '../context/draw.js';\n\n/** Sixteenth steps in one 4/4 bar; the grid every figure is written on. */\nexport const BAR_STEPS = 16;\n\n/** Sixteenth steps in one quarter-note beat. */\nexport const BEAT_STEPS = 4;\n\n/** One sixteenth expressed in quarter-note beats. */\nexport const STEP_BEATS = 1 / BEAT_STEPS;\n\n/**\n * The figure as a list of onsets, checked before it is walked.\n *\n * Every transform here takes a figure a caller may have assembled itself or\n * read out of a file, and every one of them starts by walking it: the shape is\n * read once, in one place, so a `null` figure is refused by name rather than\n * reported as this module's own `TypeError` from whichever transform was asked.\n */\nfunction assertGridEvents<T extends GridEvent>(events: readonly T[]): readonly T[] {\n assertArray<T>(events, 'figure events');\n events.forEach((event, index) => {\n assertRecord<T>(event, `figure events[${index}]`);\n assertFiniteNumber(event.step, `figure events[${index}].step`);\n });\n return events;\n}\n\n/**\n * The figure's length in steps, checked wherever a transform is bounded by it.\n *\n * Every transform here compares an onset against the span, and a span that is\n * not a number compares false both ways: the figure is silently emptied, or\n * every onset is silently kept. Either way the caller is handed a figure that\n * is not the one the span asked for.\n */\nfunction assertSpanSteps(spanSteps: number): number {\n return assertPositiveInt(spanSteps, 'figure span steps');\n}\n\n/**\n * An onset on the shared sixteenth grid.\n *\n * Both kinds of material are lists of these with their own extra fields, so a\n * transform is generic over the record and only ever reads the fields named\n * here.\n *\n * @category Composition\n */\nexport type GridEvent = {\n /** Sixteenth-note index from the start of the figure. */\n step: number;\n /** Loudness as a factor of the passage's base velocity. */\n velocity: number;\n /**\n * The voice this onset is played on, where the material has more than one.\n * A figure written for a single voice — a bass lick — leaves it out.\n */\n voice?: string;\n /**\n * The limb that plays it, where the material names one. It outranks\n * {@link GridEvent.voice} as the stream an onset belongs to, since what\n * crowds a player is one limb rather than one drum.\n */\n limb?: string;\n};\n\n/**\n * The stream an onset belongs to: its limb, else its voice, else the one\n * stream a single-voice figure has.\n *\n * Transforms and the ceiling both group by this. Two hands are two players, so\n * a ghost snare neither blocks a hi-hat from anticipating nor makes the pair\n * count as one limb repeating.\n */\nfunction streamOf(event: GridEvent): string {\n return event.limb ?? event.voice ?? '';\n}\n\n/** The bar the figures are written on when the caller names no meter. */\nconst FIGURE_TS: TimeSignature = { numerator: 4, denominator: 4 };\n\n/**\n * The first subdivision of a pulse: half of it in a simple meter, a third in a\n * compound one.\n *\n * The note value a player counts between the felt beats — quavers in 4/4 and in\n * 6/8 alike — which is what separates a position the figure is written on from\n * one that falls between the grid's own positions.\n */\nfunction subdivisionBeats(ts: TimeSignature): number {\n const pulse = pulseBeats(ts);\n return isCompound(ts) ? pulse / 3 : pulse / 2;\n}\n\n/**\n * How strongly a position on the sixteenth grid is felt, from 4 (the downbeat)\n * to 0 (an off sixteenth).\n *\n * This is the ranking the thinning rule works down: the notes a player drops\n * first when asked for something easier are the ones carrying the least of the\n * metre. The ranking is the meter's own — {@link metricWeight} says which\n * positions are the downbeat, a secondary strong pulse, and a plain main pulse,\n * and the two ranks below them are the pulse's first subdivision and everything\n * off it. In 4/4 that is the downbeat, the half bar, the other quarters, the\n * quavers and the semiquavers; in 6/8 it is the two dotted-quarter pulses, then\n * the quavers, and the quarter-note positions a 4/4 reading would rank above\n * them fall where they belong, between the beats.\n *\n * @param step Sixteenth index from the start of the figure.\n * @param ts The meter the figure is counted in, in any form that names one.\n * Asked for rather than defaulted: the built-in figures are written on a 4/4\n * grid, but a caller working in 7/8 who leaves it out has no way to hear that\n * the ranking they got was somebody else's bar.\n * @returns The weight, 0 to 4.\n *\n * @category Composition\n */\nexport function gridMetricWeight(step: number, ts: MeterLike): number {\n // Checked before the grid position is turned into beats: multiplying a `null`\n // by the step length gives beat 0, which is the downbeat — the strongest\n // answer this returns, for a position nobody named.\n assertFiniteNumber(step, 'step');\n return stepWeight(step, meterAt(0, toMeterData(ts, 'ts')));\n}\n\n/**\n * {@link gridMetricWeight} against a signature already read.\n *\n * A transform asks this once per onset, and re-reading the meter for each of\n * them would charge a figure for validating the same bar as many times as it\n * has notes.\n */\nfunction stepWeight(step: number, ts: TimeSignature): number {\n const beats = step * STEP_BEATS;\n const weight = metricWeight(beats, ts);\n if (weight > 0) {\n return weight + 1;\n }\n const subdivision = subdivisionBeats(ts);\n const divisions = beats / subdivision;\n return Math.abs(divisions - Math.round(divisions)) < 1e-9 ? 1 : 0;\n}\n\n/**\n * Drop the notes carrying the least of the metre.\n *\n * The easing direction: at 0 nothing is dropped, and as the amount rises whole\n * ranks of positions go, off sixteenths first and the downbeat last. It is a\n * ranking rather than a sampling, so the same amount always removes the same\n * notes and a caller can explain which ones went.\n *\n * @param events The figure.\n * @param amount How much to thin, in [0, 1].\n * @param ts The meter the figure is counted in, in any form that names one.\n * The ranks are the meter's, so a full turn of the dial leaves the downbeats\n * of the bar the figure is actually in — which is why it is asked for rather\n * than assumed.\n * @returns The surviving events, in the order given.\n *\n * @category Composition\n */\nexport function thin<T extends GridEvent>(\n events: readonly T[],\n amount: number,\n ts: MeterLike,\n): T[] {\n const figure = assertGridEvents<T>(events);\n assertRange(amount, 0, 1, 'thin amount');\n const signature = meterAt(0, toMeterData(ts, 'ts'));\n // Five ranks (0..4), so a full turn of the dial reaches the downbeat alone.\n const floor = Math.min(4, Math.floor(amount * 5));\n return figure.filter((event) => stepWeight(event.step, signature) >= floor);\n}\n\n/**\n * Fill in the gaps of a figure, one note per gap.\n *\n * Doubling is what a player does when asked for more of the same rather than\n * for something different: the added note sits halfway between two the figure\n * already has, takes its predecessor's identity, and is played softer.\n *\n * @param events The figure, which need not be sorted.\n * @param spanSteps Length of the figure in sixteenths.\n * @returns The figure with the midpoints filled, sorted by position.\n *\n * @category Composition\n */\nexport function double<T extends GridEvent>(events: readonly T[], spanSteps = BAR_STEPS): T[] {\n assertSpanSteps(spanSteps);\n const sorted = [...assertGridEvents(events)].sort((a, b) => a.step - b.step);\n const out: T[] = [];\n for (let i = 0; i < sorted.length; i += 1) {\n const event = sorted[i];\n if (!event) {\n continue;\n }\n out.push(event);\n const next = sorted[i + 1]?.step ?? spanSteps;\n const mid = event.step + (next - event.step) / 2;\n // Only where the grid has a position for it: a gap of one sixteenth is\n // already as dense as the figure is written.\n if (Number.isInteger(mid) && mid > event.step && mid < spanSteps) {\n out.push({ ...event, step: mid, velocity: event.velocity * ADDED_NOTE_VELOCITY });\n }\n }\n return out;\n}\n\n/** How loud a note added by a transform is, relative to the one it follows. */\nconst ADDED_NOTE_VELOCITY = 0.75;\n\n/**\n * Stretch a figure to twice its length: everything falls half as often.\n *\n * @param events The figure.\n * @param spanSteps Length of the figure in sixteenths; notes stretched past it\n * are dropped, since the bar is the bar however the figure is felt.\n * @returns The stretched figure.\n *\n * @category Composition\n */\nexport function halfTime<T extends GridEvent>(events: readonly T[], spanSteps = BAR_STEPS): T[] {\n assertSpanSteps(spanSteps);\n return assertGridEvents(events)\n .map((event) => ({ ...event, step: event.step * 2 }))\n .filter((event) => event.step < spanSteps);\n}\n\n/**\n * Compress a figure to half its length and play it twice.\n *\n * The repeat is what makes this the mirror of {@link halfTime} rather than a\n * figure with a hole in it: the bar stays full.\n *\n * Halving brings positions together, and two onsets of one stream landing on\n * the same step are one stroke played louder rather than two: a drum struck\n * twice at the same instant is a pair of note-ons a reader turns into a hung or\n * cut note, so the pair is merged and the louder of the two is what sounds.\n * Streams stay apart, since two limbs striking together is a chord and not a\n * repeat.\n *\n * @param events The figure.\n * @param spanSteps Length of the figure in sixteenths.\n * @returns The compressed figure, repeated to fill the span, at most one event\n * per stream and step.\n *\n * @category Composition\n */\nexport function doubleTime<T extends GridEvent>(events: readonly T[], spanSteps = BAR_STEPS): T[] {\n assertSpanSteps(spanSteps);\n assertGridEvents(events);\n const merged = new Map<string, T>();\n const place = (event: T, step: number, velocity: number): void => {\n const key = `${streamOf(event)}@${step}`;\n const held = merged.get(key);\n if (held === undefined || velocity > held.velocity) {\n merged.set(key, { ...event, step, velocity });\n }\n };\n for (const event of events) {\n // An odd step halves onto no grid position, so it is placed on the nearest\n // one. Dropping it instead took the \"a\" of the kick and three of the four\n // ghosts out of the figure that names the genre, and returned the result as\n // though it were the same figure played faster.\n const step = Math.round(event.step / 2);\n place(event, step, event.velocity);\n const repeated = step + spanSteps / 2;\n if (repeated < spanSteps) {\n place(event, repeated, event.velocity * ADDED_NOTE_VELOCITY);\n }\n }\n return [...merged.values()].sort((a, b) => a.step - b.step);\n}\n\n/**\n * How much a figure is syncopated, and the meter it is felt in.\n *\n * @category Composition\n */\nexport type SyncopateOptions = {\n /** How much syncopation, in [0, 1]. */\n amount: number;\n /**\n * The meter the figure is counted in, in any form that names one; 4/4 when\n * none is named.\n */\n ts?: MeterLike;\n};\n\n/**\n * Anticipate beats, one sixteenth early.\n *\n * Anticipation is the direction syncopation runs in every genre this library\n * names: the note arrives before the beat it belongs to. It is written as an\n * addition rather than a displacement — the anticipating stroke is softer, and\n * the note it anticipates stays where it was, the way a player ties into the\n * beat rather than abandoning it — because this is one of the dials that only\n * ever add material. Displacing instead would move onsets around under the\n * finger of anyone who puts the dial on a slider, and a saved project reopened\n * one notch along would not contain the piece it was. Which beats are\n * anticipated is fixed by position, so the bar next door is unaffected either\n * way.\n *\n * A position already taken blocks the anticipation only within the same stream\n * — the same limb, else the same voice. A ghost snare sitting where the hi-hat\n * wants to anticipate is another hand playing, not an obstacle, and whether two\n * limbs can actually strike together is the playability layer's question rather\n * than this one's.\n *\n * @param events The figure.\n * @param amount How much syncopation, in [0, 1], or the amount together with\n * the meter the figure is counted in. The path is variadic, so the meter is\n * named here rather than after it; naming none reads the figure in 4/4, as\n * every built-in figure is written.\n * @param draw The position-addressed sampler.\n * @param path Where the figure sits, so the same bar always syncopates alike.\n * @returns The figure with its anticipations, sorted by position.\n *\n * @category Composition\n */\nexport function syncopate<T extends GridEvent>(\n events: readonly T[],\n amount: number | SyncopateOptions,\n draw: Draw,\n ...path: readonly (string | number)[]\n): T[] {\n const figure = assertGridEvents<T>(events);\n const sampler = assertDraw(draw);\n assertSeedPath(path);\n const asked =\n typeof amount === 'number' ? undefined : assertRecord<SyncopateOptions>(amount, 'amount');\n const dial = asked === undefined ? amount : asked.amount;\n const signature = meterAt(0, toMeterData(asked?.ts ?? FIGURE_TS, 'ts'));\n assertRange(dial as number, 0, 1, 'syncopate amount');\n const occupied = new Set(figure.map((event) => `${streamOf(event)}@${event.step}`));\n const out: T[] = [...figure];\n for (const event of figure) {\n const anticipated = event.step - 1;\n // The downbeat takes none: its anticipation would fall in the bar before\n // the one the figure belongs to.\n if (\n stepWeight(event.step, signature) < 2 ||\n anticipated < 1 ||\n occupied.has(`${streamOf(event)}@${anticipated}`)\n ) {\n continue;\n }\n if (!sampler.prob(dial as number, ...path, 'syncopate', event.step)) {\n continue;\n }\n out.push({ ...event, step: anticipated, velocity: event.velocity * ADDED_NOTE_VELOCITY });\n }\n return out.sort((a, b) => a.step - b.step);\n}\n\n/**\n * How much decoration a figure keeps.\n *\n * A decorated note — a ghost, a flam, anything the figure marks as ornament —\n * survives on its own draw, so raising the dial only ever brings more of them\n * back and never disturbs the ones already sounding. Notes that are not\n * decoration are untouched.\n *\n * @param events The figure.\n * @param isOrnament Which events count as decoration.\n * @param amount The ornament dial, in [0, 1].\n * @param draw The position-addressed sampler.\n * @param path Where the figure sits.\n * @returns The figure with the surviving decoration.\n *\n * @category Composition\n */\nexport function ornamentBy<T extends GridEvent>(\n events: readonly T[],\n isOrnament: (event: T) => boolean,\n amount: number,\n draw: Draw,\n ...path: readonly (string | number)[]\n): T[] {\n const figure = assertGridEvents<T>(events);\n const marks = assertFunction<(event: T) => boolean>(isOrnament, 'isOrnament');\n const sampler = assertDraw(draw);\n assertSeedPath(path);\n assertRange(amount, 0, 1, 'ornament amount');\n return figure.filter(\n (event) => !marks(event) || sampler.prob(amount, ...path, 'ornament', event.step),\n );\n}\n\n/**\n * Everything the complexity dials do to a figure, in one pass.\n *\n * @category Composition\n */\nexport type DeformOptions = {\n /**\n * Subdivision and syncopation, in [0, 1]. Below the neutral middle the figure\n * is thinned; above it, syncopated. The middle leaves it as the dictionary\n * wrote it, which is what makes the dictionary the thing a reader can check.\n */\n rhythmic?: number;\n /** Ghosts and decoration, in [0, 1]; absent keeps every ornament. */\n ornament?: number;\n /** Which events are decoration, for the ornament dial. */\n isOrnament?: (event: GridEvent) => boolean;\n /**\n * Rate: stretch or compress the figure before the dials are applied. It is\n * named for what it does to the note values, since \"feel\" elsewhere in the\n * drum surface names the swing a figure is played with — a different\n * question, asked of the same figure.\n */\n rate?: 'straight' | 'half' | 'double';\n /** Length of the figure in sixteenths. */\n spanSteps?: number;\n /**\n * The meter the figure is counted in, in any form that names one; 4/4 when\n * none is named, which is the bar every built-in figure is written on.\n *\n * The dials rank a figure's positions by how much of the metre each carries,\n * so a figure written in another meter has to name it or it is thinned and\n * syncopated against accents its own bar does not have.\n */\n ts?: MeterLike;\n};\n\n/** The rhythmic setting at which a figure is left as written. */\nconst NEUTRAL_RHYTHMIC = 0.5;\n\n/**\n * Apply the complexity dials to a figure.\n *\n * This is the one place complexity touches vocabulary: genre has already chosen\n * the material, and the ceiling has not been consulted yet.\n *\n * @param events The figure.\n * @param opts What the dials say.\n * @param draw The position-addressed sampler.\n * @param path Where the figure sits.\n * @returns The deformed figure, sorted by position.\n *\n * @example\n * ```ts\n * import { deform, resolveContext } from '@libraz/libcantus';\n * const figure = [\n * { step: 0, velocity: 1, limb: 'kick' },\n * { step: 4, velocity: 0.9, limb: 'snare' },\n * { step: 8, velocity: 1, limb: 'kick' },\n * { step: 12, velocity: 0.9, limb: 'snare' },\n * ];\n * const draw = resolveContext(7).part('drums');\n * const busier = deform(figure, { rhythmic: 0.9 }, draw, 'bar', 0);\n * // Above the neutral middle the figure is syncopated: onsets are added off\n * // the beat, and the result comes back sorted by position.\n * busier.length; // 7\n * busier.map((event) => event.step); // [0, 3, 4, 7, 8, 11, 12]\n * ```\n *\n * @category Composition\n */\nexport function deform<T extends GridEvent>(\n events: readonly T[],\n opts: DeformOptions,\n draw: Draw,\n ...path: readonly (string | number)[]\n): T[] {\n const figure = assertGridEvents<T>(events);\n const asked = assertRecord<DeformOptions>(opts, 'opts');\n assertDraw(draw);\n assertSeedPath(path);\n const spanSteps = assertSpanSteps(asked.spanSteps === undefined ? BAR_STEPS : asked.spanSteps);\n const ts = asked.ts ?? FIGURE_TS;\n let out: T[] =\n asked.rate === 'half'\n ? halfTime(figure, spanSteps)\n : asked.rate === 'double'\n ? doubleTime(figure, spanSteps)\n : [...figure];\n\n if (asked.rhythmic !== undefined) {\n const rhythmic = assertRange(asked.rhythmic, 0, 1, 'complexity rhythmic');\n if (rhythmic < NEUTRAL_RHYTHMIC) {\n // Below the middle the request is for less, and less means the notes that\n // carry the least of the metre — of the figure's own metre, so the bar\n // this figure is written in is the bar its positions are ranked against.\n out = thin(out, (NEUTRAL_RHYTHMIC - rhythmic) / NEUTRAL_RHYTHMIC, ts);\n } else if (rhythmic > NEUTRAL_RHYTHMIC) {\n out = syncopate(\n out,\n { amount: (rhythmic - NEUTRAL_RHYTHMIC) / NEUTRAL_RHYTHMIC, ts },\n draw,\n ...path,\n );\n }\n }\n\n if (asked.ornament !== undefined && asked.isOrnament) {\n const isOrnament = assertFunction<(event: T) => boolean>(asked.isOrnament, 'opts.isOrnament');\n out = ornamentBy(out, (event) => isOrnament(event), asked.ornament, draw, ...path);\n }\n\n return out.sort((a, b) => a.step - b.step);\n}\n\n/**\n * Whether a figure is inside the difficulty ceiling at this tempo.\n *\n * The ceiling is measured against the closest pair of onsets the figure asks\n * one player for, which is the thing that actually stops a hand — so the pairs\n * are counted within a stream (the limb, else the voice) rather than across the\n * whole kit, because a hi-hat and a kick a sixteenth apart are two limbs and not\n * one hand playing twice. A figure that fails is rejected — never quietly\n * simplified, because a simplified figure is a different figure and the caller\n * asked for this one.\n *\n * @param events The figure.\n * @param bpm Tempo, or undefined when the caller gave none.\n * @param difficulty The ceiling, or undefined for no ceiling.\n * @returns True when the figure is playable at the ceiling, and whenever either\n * the ceiling or the tempo is unknown.\n *\n * @category Composition\n */\nexport function withinCeiling(\n events: readonly GridEvent[],\n bpm: number | undefined,\n difficulty: number | undefined,\n): boolean {\n const figure = assertGridEvents(events);\n if (difficulty === undefined || bpm === undefined) {\n return true;\n }\n // Read on the same terms as the ceiling check it delegates to: absent means\n // no constraint, and anything else has to be a tempo and a ceiling.\n assertFiniteNumber(bpm, 'bpm');\n assertDifficulty(difficulty);\n if (figure.length < 2) {\n return true;\n }\n const byStream = new Map<string, Set<number>>();\n for (const event of figure) {\n const stream = streamOf(event);\n const steps = byStream.get(stream);\n if (steps) {\n steps.add(event.step);\n } else {\n byStream.set(stream, new Set([event.step]));\n }\n }\n let closest = Number.POSITIVE_INFINITY;\n for (const stream of byStream.values()) {\n const steps = [...stream].sort((a, b) => a - b);\n for (let i = 1; i < steps.length; i += 1) {\n const gap = (steps[i] ?? 0) - (steps[i - 1] ?? 0);\n if (gap > 0 && gap < closest) {\n closest = gap;\n }\n }\n }\n if (!Number.isFinite(closest)) {\n return true;\n }\n return sustainsStrokes(closest * STEP_BEATS, bpm, difficulty);\n}\n","import { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport type { Chord } from '../../theory/chord/index.js';\nimport { degreeOfInterval } from '../../theory/chord/index.js';\nimport { chordScales } from '../../theory/chordscale/index.js';\nimport { namedScaleMask, nearestScaleTone } from '../../theory/scale/index.js';\n\n/** Semitones above the root for each degree of a major scale, 1-based. */\nconst MAJOR_DEGREE_SEMITONES = [0, 2, 4, 5, 7, 9, 11] as const;\n/** How many tones a scale has one degree apiece. */\nconst HEPTATONIC_SIZE = 7;\n/**\n * The scale a chord's own quality implies, as semitones above its root.\n *\n * A chord that states a seventh has named its mode — the third and the seventh\n * together are what chord-scale theory reads a chord scale from — so the\n * degrees it does not state are still its own rather than the surrounding key's:\n * the sixth over a dominant seventh is the major sixth its mode carries,\n * whatever the key spells in that place. A triad names no mode, and a chord\n * whose set no seven-tone scale contains has no degrees to give, so both leave\n * the answer to the key.\n *\n * @param chord The chord being played over.\n * @returns One semitone offset per degree, 1-based, or undefined when the\n * quality determines no scale.\n */\nexport function impliedScaleTones(chord: Chord): readonly number[] | undefined {\n if (chordDegreeSemitone(7, chord) === undefined) {\n return undefined;\n }\n for (const match of chordScales(chord)) {\n const mask = namedScaleMask(match.name);\n if (mask === undefined) {\n continue;\n }\n const tones: number[] = [];\n for (let offset = 0; offset < 12; offset += 1) {\n if (((mask >> offset) & 1) === 1) {\n tones.push(offset);\n }\n }\n if (tones.length === HEPTATONIC_SIZE) {\n return tones;\n }\n }\n return undefined;\n}\n/**\n * The tone standing in the place of a degree the chord replaced.\n *\n * A suspension does not leave its third unstated; it puts another tone there.\n * Filling the third back in from the key is the one thing a suspension exists\n * to prevent, so a figure written on the third sounds the tone the chord\n * suspended into instead. Read from the tones the chord sounds rather than from\n * its quality name: one suspension is written `sus4`, `7sus4`, `9sus4` and\n * `11`, and the eleventh states it by omitting the third rather than by naming\n * a suspension at all.\n */\nfunction replacedDegreeSemitone(degree: number, chord: Chord): number | undefined {\n if (degree !== 3) {\n return undefined;\n }\n const sounds = (semitones: number): boolean =>\n chord.intervals.some((interval) => pitchClass(interval) === semitones);\n if (sounds(3) || sounds(4)) {\n return undefined;\n }\n return sounds(5) ? 5 : sounds(2) ? 2 : undefined;\n}\n/**\n * Semitones above the chord's root that a degree names.\n *\n * The chord answers for the degrees it actually contains, so a figure written\n * on the third comes out minor over a minor chord without the dictionary having\n * to hold two versions of it. Next comes what the chord's quality settles\n * without stating: the tone a suspension put in the third's place, and the\n * degrees of the chord scale a seventh chord names. Only what is left — a\n * degree the harmony genuinely leaves open, which is where a passing tone lives\n * — is taken from the key, which keeps it inside the music rather than inside a\n * template.\n *\n * An alteration is a displacement from the plain diatonic degree, so it is\n * measured against the major-scale template rather than stacked on top of what\n * the chord or the key already supplies: a flat seventh is the minor seventh\n * over a dominant chord as much as over a major one, which is what keeps the\n * boogie figure's sixth-to-flat-seventh motion from collapsing onto the sixth.\n *\n * The result is a signed offset, not a pitch class: degree 8 is the octave, and\n * reducing it modulo twelve would spell it as the root the figure just played,\n * turning every octave figure into a repeated note.\n */\nexport function degreeSemitone(\n degree: number,\n alter: number,\n chord: Chord,\n key: KeyScale,\n implied: readonly number[] | undefined,\n): number {\n const octaves = Math.floor((degree - 1) / 7);\n const within = ((degree - 1) % 7) + 1;\n const template = MAJOR_DEGREE_SEMITONES[within - 1] ?? 0;\n const semitone =\n alter !== 0\n ? template\n : (chordDegreeSemitone(within, chord) ??\n replacedDegreeSemitone(within, chord) ??\n implied?.[within - 1] ??\n // A degree neither the chord nor its quality settles is a passing tone,\n // so the key decides it.\n nearestScaleTone(chord.rootPc + template, key) - chord.rootPc);\n // A degree names a position inside one octave; the octaves it spans are what\n // `octaves` carries. An extended chord states its ninth as fourteen semitones\n // and the key's answer may land on the octave itself, so both are folded here.\n return pitchClass(semitone) + 12 * octaves + alter;\n}\n/** The semitone a chord gives one of its own degrees, if it has that degree. */\nexport function chordDegreeSemitone(degree: number, chord: Chord): number | undefined {\n for (const interval of chord.intervals) {\n if (degreeOfInterval(interval, chord) === degree) {\n return interval;\n }\n }\n return undefined;\n}\n","/**\n * Deep freezing for the built-in dictionaries.\n *\n * A dictionary is one object for the whole process, read by the generators on\n * every call. Freezing only the outer container leaves an entry — or a stroke\n * inside one — writable by a caller, and that edit is then seen by every later\n * generation rather than by the caller alone.\n */\n\n/** Freeze one object and descend into the values reachable from it. */\nfunction freezeReachable(value: unknown, seen: WeakSet<object>): void {\n if (value === null || typeof value !== 'object') {\n return;\n }\n const object = value as object;\n if (seen.has(object)) {\n return;\n }\n seen.add(object);\n Object.freeze(object);\n for (const child of Object.values(object)) {\n freezeReachable(child, seen);\n }\n}\n\n/**\n * Freeze a value and everything reachable from it.\n *\n * Re-entry is stopped by the set of values already seen rather than by\n * `Object.isFrozen`, so a container that was frozen shallowly before it reached\n * here still has its contents descended into.\n *\n * @param value The value to freeze.\n * @returns The same value, frozen through.\n */\nexport function deepFreeze<T>(value: T): T {\n freezeReachable(value, new WeakSet<object>());\n return value;\n}\n","/**\n * The bass lick dictionary: the figures a bass player reaches for, held as\n * chord-relative degrees and beat positions and deformed to fit the progression\n * actually in front of them.\n *\n * Provenance: no entry reproduces a bass line from a particular song. Each is a\n * figure that belongs to nobody — the descending soul walk-down, the octave\n * slap, the boogie shuffle — of the kind every player of that music already\n * plays, and each records the ground it qualifies under.\n */\n\nimport type { Articulation } from '../../core/instrument/index.js';\nimport type { TimeSignature } from '../../core/meter/index.js';\nimport { deepFreeze } from '../vocabulary/freeze.js';\nimport { BEAT_STEPS, type GridEvent, type Vocabulary } from '../vocabulary/index.js';\n\n/**\n * One note of a lick, relative to the chord it is played over.\n *\n * A degree is 1-based and counts up from the chord's root: 1 is the root, 3 the\n * third, 8 the octave. Degrees the chord itself names — the third, fifth and\n * seventh — are taken from the chord, so the same figure comes out major over a\n * major chord and minor over a minor one. A degree the chord's quality settles\n * without stating it follows the quality: the third of a suspended chord is the\n * tone the suspension put there, and a seventh chord's remaining degrees are\n * its own chord scale's. Only what the harmony leaves open is taken from the\n * key. That is what lets a figure be written once and fit a whole progression.\n *\n * @category Composition\n */\nexport type LickNote = GridEvent & {\n /** Chord degree, 1-based; 8 is the octave above the root. */\n degree: number;\n /** Chromatic alteration in semitones, for a flattened or raised degree. */\n alter?: number;\n /**\n * Sounding length in sixteenths. Absent, the note sounds to the next onset,\n * as a line written by {@link generateBassLine} does: the built-in figures\n * name no length, so a figure is legato unless it says otherwise.\n */\n lengthSteps?: number;\n /** How the note is played, when it is more than a plain note. */\n articulation?: Articulation;\n};\n/**\n * A lick: its notes and how long the whole figure lasts.\n *\n * @category Composition\n */\nexport type LickMaterial = {\n /** Length of the figure in sixteenths. */\n lengthSteps: number;\n notes: LickNote[];\n};\n/**\n * A lick with the conditions it fits.\n *\n * @category Composition\n */\nexport type BassLick = Vocabulary<LickMaterial>;\n/**\n * Whether a caller's material is a bass lick.\n *\n * @category Composition\n */\nexport function isLickMaterial(material: unknown): material is LickMaterial {\n const candidate = material as LickMaterial | null;\n return (\n typeof candidate === 'object' &&\n candidate !== null &&\n typeof candidate.lengthSteps === 'number' &&\n Array.isArray(candidate.notes)\n );\n}\n/** One note of a lick. */\nfunction note(\n degree: number,\n step: number,\n velocity: number,\n extra?: { alter?: number; lengthSteps?: number; articulation?: LickNote['articulation'] },\n): LickNote {\n return { degree, step, velocity, ...extra };\n}\n/** A one-bar figure on the sixteenth grid. */\nexport const BAR: number = 4 * BEAT_STEPS;\n/**\n * The meter every built-in figure is written in.\n *\n * It is declared rather than left out because an absent condition means \"any\",\n * and these are not any: their accents fall where a four-beat bar puts them, so\n * a waltz or a jig would be handed a figure written against a bar it does not\n * have.\n */\nexport const FOUR_FOUR: TimeSignature = { numerator: 4, denominator: 4 };\n/**\n * The built-in bass licks, in declaration order.\n *\n * @category Composition\n */\nexport const BASS_LICKS: readonly BassLick[] = deepFreeze([\n {\n id: 'motownWalkDown',\n genre: 'motown',\n difficulty: 3,\n articulations: [],\n ts: FOUR_FOUR,\n fitsOver: ['maj', 'maj7', '6', 'dom7'],\n tempoRange: [88, 136],\n material: {\n lengthSteps: BAR,\n notes: [\n note(1, 0, 1),\n note(8, 4, 0.85),\n note(7, 8, 0.85),\n note(6, 10, 0.8),\n note(5, 12, 0.9),\n note(3, 14, 0.8),\n ],\n },\n provenance: {\n basis: 'idiom',\n note: 'the stepwise walk down from the octave that every player of the style plays',\n },\n },\n {\n id: 'soulOctavePush',\n genre: 'soul',\n difficulty: 2,\n articulations: [],\n ts: FOUR_FOUR,\n tempoRange: [72, 120],\n material: {\n lengthSteps: BAR,\n notes: [note(1, 0, 1), note(8, 6, 0.8), note(1, 8, 0.9), note(5, 14, 0.8)],\n },\n provenance: {\n basis: 'idiom',\n note: 'root and octave answering each other across the bar, with a push into the next chord',\n },\n },\n {\n id: 'funkSixteenthPop',\n genre: 'funk',\n difficulty: 4,\n articulations: ['mute'],\n ts: FOUR_FOUR,\n tempoRange: [84, 124],\n material: {\n lengthSteps: BAR,\n notes: [\n note(1, 0, 1),\n note(1, 2, 0.4, { articulation: 'mute' }),\n note(8, 3, 0.9),\n note(1, 6, 0.5, { articulation: 'mute' }),\n note(5, 8, 0.85),\n note(1, 11, 0.4, { articulation: 'mute' }),\n note(8, 12, 0.9),\n note(7, 15, 0.7, { alter: -1 }),\n ],\n },\n provenance: {\n basis: 'idiom',\n note: 'the octave-and-ghost sixteenth figure that defines the style, with a flat seventh on the way out',\n },\n },\n {\n id: 'bluesBoogie',\n genre: 'blues',\n difficulty: 2,\n articulations: [],\n ts: FOUR_FOUR,\n fitsOver: ['maj', 'dom7'],\n tempoRange: [76, 168],\n material: {\n lengthSteps: BAR,\n notes: [\n note(1, 0, 1),\n note(5, 2, 0.85),\n note(6, 4, 0.85),\n note(7, 6, 0.85, { alter: -1 }),\n note(6, 8, 0.85),\n note(5, 10, 0.85),\n note(1, 12, 0.9),\n note(5, 14, 0.8),\n ],\n },\n provenance: {\n basis: 'traditional',\n note: 'the boogie figure of the twelve-bar tradition: root, fifth, sixth, flat seventh and back',\n },\n },\n {\n id: 'jazzWalkingApproach',\n genre: 'jazz',\n difficulty: 3,\n articulations: [],\n ts: FOUR_FOUR,\n tempoRange: [100, 240],\n material: {\n lengthSteps: BAR,\n notes: [note(1, 0, 1), note(3, 4, 0.85), note(5, 8, 0.85), note(6, 12, 0.85)],\n },\n provenance: {\n basis: 'idiom',\n note: 'the quarter-note walk through the chord that ends a step from wherever it is going',\n },\n },\n {\n id: 'bossaTwoFeel',\n genre: 'bossa',\n difficulty: 2,\n articulations: [],\n ts: FOUR_FOUR,\n tempoRange: [104, 168],\n material: {\n lengthSteps: BAR,\n notes: [note(1, 0, 1), note(5, 6, 0.85), note(1, 8, 0.95), note(5, 14, 0.8)],\n },\n provenance: {\n basis: 'traditional',\n note: 'the two-beat root-and-fifth of the Brazilian dance form, anticipated across the bar line',\n },\n },\n {\n id: 'gospelPassing',\n genre: 'gospel',\n difficulty: 4,\n articulations: [],\n ts: FOUR_FOUR,\n tempoRange: [60, 108],\n material: {\n lengthSteps: BAR,\n notes: [\n note(1, 0, 1),\n note(2, 3, 0.7),\n note(3, 4, 0.9),\n note(5, 7, 0.8),\n note(6, 8, 0.85),\n note(5, 11, 0.75),\n note(3, 12, 0.85),\n note(2, 15, 0.7),\n ],\n },\n provenance: {\n basis: 'idiom',\n note: 'the busy stepwise motion between chord tones that the church style fills its bars with',\n },\n },\n {\n id: 'countryAlternating',\n genre: 'country',\n difficulty: 1,\n articulations: [],\n ts: FOUR_FOUR,\n tempoRange: [80, 180],\n material: {\n lengthSteps: BAR,\n notes: [note(1, 0, 1), note(5, 4, 0.85), note(1, 8, 0.95), note(5, 12, 0.85)],\n },\n provenance: {\n basis: 'traditional',\n note: 'the alternating root and fifth of the dance-band bass, older than any recording of it',\n },\n },\n {\n id: 'reggaeOffbeatDrop',\n genre: 'reggae',\n difficulty: 3,\n articulations: [],\n ts: FOUR_FOUR,\n tempoRange: [64, 104],\n material: {\n lengthSteps: BAR,\n notes: [\n note(1, 2, 0.9),\n note(3, 4, 0.85),\n note(5, 6, 0.85),\n note(1, 10, 0.95),\n note(7, 14, 0.8, { alter: -1 }),\n ],\n },\n provenance: {\n basis: 'idiom',\n note: 'the figure that leaves the downbeat empty and lands after it',\n },\n },\n]);\n","/**\n * Lay the figures of the dictionary over a chord progression.\n *\n * A segment takes a figure its genre plays, chosen by a draw addressed to that\n * segment, and the figure's degrees are read against the chord sounding under\n * it. What the figures are is `licks-data.ts`; which pitch a degree asks for is\n * `degrees.ts`. This module is what places them: which segment takes one, how\n * it is fitted to the span, and how one figure leads into the next.\n */\n\nimport { BEAT_EPS } from '../../analyze/adjacency.js';\nimport { foldIntoRange, type StringedProfile } from '../../core/instrument/index.js';\nimport {\n beatsPerBar,\n isStrongBeat,\n type MeterLike,\n meterAt,\n type TimeSignature,\n toMeterData,\n} from '../../core/meter/index.js';\nimport { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\nimport type { NoteEvent } from '../../core/types.js';\nimport {\n assertArray,\n assertGenerationBudget,\n assertInteger,\n assertOneOf,\n assertRecord,\n clampToMidi,\n} from '../../core/validation/index.js';\nimport type { ChordSegment } from '../../theory/chord/index.js';\nimport { type KeyLike, toKeyScale } from '../../theory/scale/index.js';\nimport { type ChordLike, toChordData } from '../../theory/symbol/index.js';\nimport { assertDifficulty, type GenerationContextInput, resolveContext } from '../context/index.js';\nimport {\n BEAT_STEPS,\n deform,\n GENRES,\n type Genre,\n mergeVocabulary,\n pickVocabulary,\n STEP_BEATS,\n vocabularyOfKind,\n withinCeiling,\n} from '../vocabulary/index.js';\nimport { degreeSemitone, impliedScaleTones } from './degrees.js';\nimport {\n approachNote,\n assertBassSegments,\n barTiles,\n bassPcOf,\n bassRegister,\n DEFAULT_OCTAVE,\n DEFAULT_TS,\n placeAboveBass,\n placePc,\n placeRoot,\n STRONG_VELOCITY,\n WEAK_VELOCITY,\n} from './internal.js';\nimport type { LickMaterial, LickNote } from './licks-data.js';\nimport { BASS_LICKS, isLickMaterial } from './licks-data.js';\n\n/**\n * Options controlling {@link placeLicks}.\n *\n * @category Composition\n */\nexport type PlaceLicksOptions = {\n /**\n * The genre whose figures may be used. Genre is what selects the material:\n * the dials then deform it, and the ceiling rejects it.\n */\n genre: Genre;\n /**\n * The hardest figure that may be used, 1 to 5. Sugar for\n * `ctx: { complexity: { difficulty } }`; the context wins over both.\n */\n difficulty?: number;\n /** Time signature, in any form that names one; defaults to 4/4. */\n ts?: MeterLike;\n /**\n * Target register as a base MIDI octave; roots land around `octave*12+12`.\n *\n * @defaultValue 2\n */\n octave?: number;\n /** The instrument the line is written for; its range always applies. */\n instrument?: StringedProfile;\n /**\n * The generation context. Its `vocabulary` brings the caller's own figures,\n * its `complexity.ornament` decides how much decoration survives, and its\n * `complexity.difficulty` is the ceiling figures are rejected against.\n *\n * Its `complexity.rhythmic` is how busy the line is, in [0, 1]: it decides\n * both how often a segment takes a figure at all and, above its middle\n * setting, how much the figure is syncopated — one dial, because \"less busy\"\n * means both. Its `bpm` is the tempo the ceiling is measured against, and its\n * `seed` fixes which figures are chosen and where they land.\n *\n * @defaultValue `{ seed: 0, bpm: 120, complexity: { rhythmic: 0.6 } }`\n */\n ctx?: GenerationContextInput;\n /**\n * Maximum number of segments {@link placeLicks} may lay figures over, and of\n * bars those segments span. One figure is placed per bar of a segment, so\n * both are counted: this is the guard against an unbounded caller — a segment\n * running to a beat far past the music — rather than a limit on any search.\n *\n * @defaultValue 1000000\n */\n budget?: number;\n};\n\nconst DEFAULT_BPM = 120;\n\n/** How much of the line is figures when the caller names nothing. */\nconst DEFAULT_LICK_DENSITY = 0.6;\n\n/**\n * Lay licks over a chord placement.\n *\n * Each segment is offered the figures whose conditions it meets — the genre,\n * the chord quality, the tempo, and the difficulty ceiling — and one of them is\n * chosen by a draw addressed to that segment, so changing a chord elsewhere\n * leaves this segment's figure where it was. A segment that takes no figure\n * gets its root, and the last note before a chord change leads into the next\n * root by step, the way a walking line does.\n *\n * @param timeline Chord segments to play over; they need not be pre-sorted, and\n * each segment's chord may be written as a chord symbol.\n * @param key Key context, which answers for the degrees the chords do not; a\n * key name such as `'C major'` is read as that key.\n * @param opts Genre, density, ceiling and seed.\n * @returns Bass notes sorted by onset, non-overlapping.\n * @throws If a segment has a non-positive duration, two segments overlap, or\n * the genre is not one this library names. A bass part is monophonic, so\n * overlapping segments are refused here exactly as {@link generateBassLine}\n * refuses them rather than being laid over each other.\n *\n * @example\n * ```ts\n * import { majorKey, makeChord, placeLicks } from '@libraz/libcantus';\n * const timeline = [\n * { startBeat: 0, endBeat: 4, chord: makeChord(0, 'maj7') },\n * { startBeat: 4, endBeat: 8, chord: makeChord(5, 'maj7') },\n * ];\n * const notes = placeLicks(timeline, majorKey(0), { genre: 'motown', ctx: { seed: 7 } });\n * ```\n *\n * @category Composition\n */\nexport function placeLicks(\n timeline: readonly (Omit<ChordSegment, 'chord'> & { chord: ChordLike })[],\n key: KeyLike,\n opts: PlaceLicksOptions,\n): NoteEvent[] {\n const asked = assertRecord<PlaceLicksOptions>(opts, 'opts');\n const placement = assertArray<Omit<ChordSegment, 'chord'> & { chord: ChordLike }>(\n timeline,\n 'timeline',\n );\n assertGenerationBudget(placement.length, 'lick segments', asked.budget);\n const genre = assertOneOf(asked.genre, GENRES, 'lick genre');\n const ts = meterAt(0, toMeterData(asked.ts ?? DEFAULT_TS, 'ts'));\n const octave = asked.octave ?? DEFAULT_OCTAVE;\n assertInteger(octave, 'lick octave', -1, 8);\n // Key and chords are read into their plain form once, here at the boundary;\n // everything below works on the plain forms alone.\n const scale = toKeyScale(key);\n const segments: ChordSegment[] = placement\n .map((segment) => ({ ...segment, chord: toChordData(segment.chord) }))\n .sort((a, b) => a.startBeat - b.startBeat);\n assertBassSegments(segments);\n if (segments.length === 0) {\n return [];\n }\n // A figure is a bar long, so what is written is one figure per bar of the\n // placement rather than one per segment: a single segment running to a beat\n // the caller computed from a loop length or read out of a project file lays\n // as many bars as that number holds. The count is charged before any of them\n // is built, which is the same estimate the phrase-shape styles make before\n // reaching the same tiling.\n const barBeats = beatsPerBar(ts);\n const estimatedBars = segments.reduce(\n (count, segment) =>\n count + Math.max(1, Math.ceil((segment.endBeat - segment.startBeat) / barBeats)),\n 0,\n );\n assertGenerationBudget(estimatedBars, 'lick bars', asked.budget);\n\n const resolved = resolveContext(asked.ctx);\n const draw = resolved.part('bass');\n const bpm = resolved.bpm ?? DEFAULT_BPM;\n const density = resolved.rhythmic ?? DEFAULT_LICK_DENSITY;\n // The caller's own ceiling is validated whether or not the context also names\n // one: whether the context does is not something the caller of this surface\n // can see, so an out-of-range value is rejected the same way either time.\n // Only leaving it out skips the check.\n const supplied =\n asked.difficulty === undefined\n ? undefined\n : assertDifficulty(asked.difficulty, 'lick difficulty');\n const difficulty = resolved.difficulty ?? supplied;\n const { instrument, low } = bassRegister(resolved.instrument('bass'), asked.instrument, octave);\n\n const dictionary = mergeVocabulary(\n BASS_LICKS,\n vocabularyOfKind(resolved.vocabulary, isLickMaterial),\n );\n // Naming an instrument is the request that the line be playable on it, so a\n // figure calling for a technique it cannot produce — a slide on an instrument\n // with no slide — is not offered rather than written and misread.\n const playable = instrument === undefined ? {} : { articulations: instrument.articulations };\n\n const raw: RawLickNote[] = [];\n\n for (let index = 0; index < segments.length; index += 1) {\n const segment = segments[index];\n if (!segment) {\n continue;\n }\n const rootPc = bassPcOf(segment.chord);\n // Where this chord sounds its bass note, and what its own quality settles\n // about the degrees it does not state: both are properties of the chord, so\n // both are read once for the whole segment.\n const rootMidi = placeRoot(rootPc, low);\n const implied = impliedScaleTones(segment.chord);\n let lastTilePlayed = false;\n let playedTile: PlayedTile | undefined;\n let firstOnset = segment.startBeat;\n\n // Each tile draws its own figure at its own address, so the bar after the\n // first is played rather than held, and a chord elsewhere in the piece\n // changing does not move any of them.\n const tiles = barTiles(segment.startBeat, segment.endBeat, ts);\n for (let tile = 0; tile < tiles.length; tile += 1) {\n const tileStart = tiles[tile]?.startBeat ?? segment.startBeat;\n const remaining = segment.endBeat - tileStart;\n // The figure this tile would play is decided before the density dial is\n // consulted, so where its root sounds does not move as the dial travels:\n // a genre whose figures land after the downbeat keeps its root there\n // whether or not this turn of the dial takes the whole figure.\n const entry = pickVocabulary(\n dictionary,\n { genre, bpm, ts, difficulty, quality: segment.chord.quality, ...playable },\n draw,\n 'lickChoice',\n index,\n tile,\n );\n const takesLick = entry !== undefined && draw.prob(density, 'lick', index, tile);\n const notes = takesLick\n ? fitToSegment(\n entry.material,\n remaining,\n density,\n resolved,\n draw,\n [index, tile],\n bpm,\n difficulty,\n ts,\n )\n : undefined;\n lastTilePlayed = notes !== undefined;\n if (notes !== undefined && entry !== undefined) {\n playedTile = { material: entry.material, tileStart };\n }\n\n const rootStep = entry === undefined ? 0 : anchorStepOf(entry.material, remaining);\n const rootAt = tileStart + rootStep * STEP_BEATS;\n if (tile === 0) {\n firstOnset = rootAt;\n }\n // Where the figure states its own note on this position, that note is the\n // one that sounds: the two share an onset, and the zero-length root is\n // dropped when durations are measured.\n raw.push({ startBeat: rootAt, pitch: rootMidi, velocity: STRONG_VELOCITY });\n\n if (notes) {\n // Over a slash chord the figure is written above the bass the chord\n // names, not folded into the register band: the band is an octave wide,\n // so a root folded into it sounds under a bass placed anywhere but the\n // bottom of it — and a `C/E` whose C sounds under its own E is a\n // root-position C, which is not the chord the part was written on.\n const figureRoot =\n segment.chord.bassPc === undefined\n ? placePc(pitchClass(segment.chord.rootPc), rootMidi, low)\n : placeAboveBass(pitchClass(segment.chord.rootPc), rootMidi);\n for (const lickNote of notes) {\n const at = tileStart + lickNote.step * STEP_BEATS;\n // Placed against the figure's own root rather than folded one note at\n // a time into the register band, which is what an octave figure\n // needs: its octave has to stay an octave.\n const offset = degreeSemitone(\n lickNote.degree,\n lickNote.alter ?? 0,\n segment.chord,\n scale,\n implied,\n );\n // The figure's plain root on the anchor onset is that onset's bass\n // note, so over a slash chord it sounds the written bass: the two\n // notes share the position and only one of them survives, and which\n // one it is must not decide what the chord change sounds like. The\n // rest of the figure stays measured from the chord's own root.\n const pitch =\n Math.abs(at - rootAt) < BEAT_EPS && lickNote.degree === 1 && (lickNote.alter ?? 0) === 0\n ? rootMidi\n : figureRoot + offset;\n raw.push({\n startBeat: at,\n pitch,\n velocity: Math.round(\n (isStrongBeat(at, ts) ? STRONG_VELOCITY : WEAK_VELOCITY) * lickNote.velocity,\n ),\n note: lickNote,\n });\n }\n }\n }\n\n // The connecting tone between one figure and the next is the walking line's\n // approach note, reused: the figures are the vocabulary, and leading into\n // the next chord is grammar that belongs to neither of them. The beat before\n // a chord change therefore always sounds — the figure's own note where it\n // has one there, this one where it does not — so a busier setting never\n // empties a beat a quieter one filled.\n const next = segments[index + 1];\n if (next && lastTilePlayed) {\n // The connecting tone is the figure's own last note wherever the figure\n // states one in the beat before the change. Fixing it to the head of that\n // beat left the figure's real final onset — a sixteenth or two later in\n // seven of the nine genres — sounding its template degree into the next\n // chord, so the note that actually leads into the change was whatever the\n // figure happened to end on. Where the figure sounds nothing in that beat\n // the tone is introduced on the beat, as it always was, which is what\n // keeps the beat before a chord change from ever falling silent.\n const lastBeat = next.startBeat - STEP_BEATS * BEAT_STEPS;\n const approachAt = materialOnsetIn(playedTile, lastBeat, next.startBeat) ?? lastBeat;\n if (approachAt > firstOnset + BEAT_EPS) {\n const nextRoot = placeRoot(bassPcOf(next.chord), low);\n const occupied = soundingAt(raw, approachAt);\n // What matters is that the beat before the change leads into it by\n // step, not that something sounds there: the figure's own fixed degree\n // holds that beat whatever the next chord is, so leaving it alone\n // because it is occupied is what kept a walking line from ever walking\n // into the change. A note already a step away is the approach and\n // stands; any other is replaced by the connecting tone, which keeps the\n // figure's rhythm and takes over its pitch.\n const distance = occupied === undefined ? 0 : Math.abs(occupied.pitch - nextRoot);\n if (occupied === undefined || distance < 1 || distance > 2) {\n const from = lastPitchBefore(raw, approachAt, rootMidi);\n const midi = approachNote(nextRoot, from, scale, draw.prob(0.5, 'lickApproach', index));\n if (occupied === undefined) {\n raw.push({ startBeat: approachAt, pitch: midi, velocity: WEAK_VELOCITY });\n } else {\n occupied.pitch = midi;\n }\n }\n }\n }\n }\n\n raw.sort((a, b) => a.startBeat - b.startBeat);\n const lastEnd = segments.reduce((m, seg) => Math.max(m, seg.endBeat), Number.NEGATIVE_INFINITY);\n const out: NoteEvent[] = [];\n for (let i = 0; i < raw.length; i += 1) {\n const item = raw[i];\n if (!item) {\n continue;\n }\n const nextStart = raw[i + 1]?.startBeat ?? lastEnd;\n const written = item.note?.lengthSteps;\n const durationBeat = Math.min(\n nextStart - item.startBeat,\n written === undefined ? Number.POSITIVE_INFINITY : written * STEP_BEATS,\n );\n if (durationBeat <= BEAT_EPS) {\n continue;\n }\n const placed = instrument ? foldIntoRange(item.pitch, instrument) : item.pitch;\n const event: NoteEvent = {\n pitch: clampToMidi(placed, 'generated bass pitch'),\n startBeat: item.startBeat,\n durationBeat,\n velocity: Math.max(1, Math.min(127, item.velocity)),\n };\n if (item.note?.articulation) {\n event.articulation = item.note.articulation;\n }\n out.push(event);\n }\n return out;\n}\n\n/** A note on its way into the line, before durations are measured from it. */\ntype RawLickNote = {\n startBeat: number;\n pitch: number;\n velocity: number;\n note?: LickNote;\n};\n\n/**\n * The note carrying a position, if one is written there.\n *\n * Where a figure states its own note on a position the segment also sounds its\n * root on, the last one written is the one that survives the duration pass, so\n * that is the note the position carries.\n */\nfunction soundingAt(raw: readonly RawLickNote[], at: number): RawLickNote | undefined {\n for (let index = raw.length - 1; index >= 0; index -= 1) {\n const item = raw[index];\n if (item !== undefined && Math.abs(item.startBeat - at) < BEAT_EPS) {\n return item;\n }\n }\n return undefined;\n}\n\n/** The figure a tile played, and where that tile began. */\ntype PlayedTile = { material: LickMaterial; tileStart: number };\n\n/**\n * The last onset the figure itself states between two positions, if any.\n *\n * Read from the material rather than from the notes the density dial left, as\n * the figure's anchor step is: where a chord change is led into from is a\n * property of the figure, and reading it off the thinned copy would move it as\n * the dial travels — a position that sounded at one setting would fall silent\n * at the next, which is the one thing the dial promises never to do.\n */\nfunction materialOnsetIn(\n played: PlayedTile | undefined,\n from: number,\n to: number,\n): number | undefined {\n if (played === undefined) {\n return undefined;\n }\n let last: number | undefined;\n for (const note of played.material.notes) {\n const at = played.tileStart + note.step * STEP_BEATS;\n if (at < from - BEAT_EPS || at > to - BEAT_EPS) {\n continue;\n }\n last = last === undefined ? at : Math.max(last, at);\n }\n return last;\n}\n\n/**\n * The pitch the line is coming from at a position, which decides which side of\n * the next chord's bass it is approached from.\n *\n * @param raw The notes written so far, in the order they were written.\n * @param at The position being led into.\n * @param fallback The pitch to answer with when nothing precedes it.\n */\nfunction lastPitchBefore(raw: readonly RawLickNote[], at: number, fallback: number): number {\n for (let index = raw.length - 1; index >= 0; index -= 1) {\n const item = raw[index];\n if (item !== undefined && item.startBeat < at - BEAT_EPS) {\n return item.pitch;\n }\n }\n return fallback;\n}\n\n/**\n * The step a figure begins on, which is where the segment sounds its root.\n *\n * Reading it from the material rather than from the deformed figure is what\n * keeps the position fixed as the density dial travels: thinning can take the\n * figure's first note away, and the root would otherwise jump to the downbeat.\n *\n * @param material The figure the segment would play.\n * @param spanBeats Length of the segment.\n * @returns The step, or 0 when the figure starts past the end of the segment.\n */\nfunction anchorStepOf(material: LickMaterial, spanBeats: number): number {\n let first: number | undefined;\n for (const note of material.notes) {\n if (first === undefined || note.step < first) {\n first = note.step;\n }\n }\n if (first === undefined || first < 0 || first * STEP_BEATS >= spanBeats - BEAT_EPS) {\n return 0;\n }\n return first;\n}\n\n/**\n * Deform a figure to the segment it is being played over, and reject it if the\n * result is beyond the ceiling.\n *\n * The three never fight because each acts once and in one direction: the genre\n * has already chosen this figure, the dials reshape it here, and the ceiling\n * only says yes or no to what came out.\n */\nfunction fitToSegment(\n material: LickMaterial,\n spanBeats: number,\n density: number,\n resolved: ReturnType<typeof resolveContext>,\n draw: ReturnType<ReturnType<typeof resolveContext>['part']>,\n path: readonly (string | number)[],\n bpm: number,\n difficulty: number | undefined,\n ts: TimeSignature,\n): LickNote[] | undefined {\n const deformed = deform(\n material.notes,\n {\n // The figure is thinned against the bar it is played in, not against a\n // four-four ladder: a downbeat of the meter in force must outrank an\n // offbeat of some other meter.\n ts,\n // The one density the whole generator reads, defaults included: passing\n // the documented default explicitly and leaving it out have to be the\n // same request, and they were not while the figure was deformed by the\n // raw dial and chosen by the resolved one.\n rhythmic: density,\n ...(resolved.ornament === undefined ? {} : { ornament: resolved.ornament }),\n isOrnament: (event) => (event as LickNote).articulation === 'mute',\n spanSteps: material.lengthSteps,\n },\n draw,\n 'lickShape',\n ...path,\n );\n // A figure longer than the chord it is played over is cut at the chord\n // change: the harmony is the thing the figure exists to serve.\n const spanSteps = spanBeats / STEP_BEATS;\n const inside = deformed.filter((lickNote) => lickNote.step < spanSteps - BEAT_EPS);\n if (inside.length === 0 || !withinCeiling(inside, bpm, difficulty)) {\n return undefined;\n }\n return inside;\n}\n","/**\n * Deterministic bass-line generation over a chord placement.\n *\n * Given a sequence of chord segments and a key, this builds a monophonic bass\n * line in a chosen low register. Several idiomatic styles are supported, from a\n * single held root per segment to a quarter-note walking line that leads by\n * step into each chord change. All pitches are kept in a narrow bass band and\n * consecutive notes move by small intervals; given a seed the output is fully\n * reproducible.\n *\n * Positions and durations are measured in quarter-note beats, matching the\n * library-wide convention.\n */\n\nimport { BEAT_EPS } from '../../analyze/adjacency.js';\nimport { foldIntoRange, type StringedProfile } from '../../core/instrument/index.js';\nimport {\n isStrongBeat,\n type MeterLike,\n meterAt,\n type TimeSignature,\n toMeterData,\n} from '../../core/meter/index.js';\nimport { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\nimport type { KeyScale, NoteEvent } from '../../core/types.js';\nimport {\n assertArray,\n assertGenerationBudget,\n assertInteger,\n assertOneOf,\n assertRecord,\n clampToMidi,\n} from '../../core/validation/index.js';\nimport type { ChordSegment } from '../../theory/chord/index.js';\nimport { type KeyLike, toKeyScale } from '../../theory/scale/index.js';\nimport { type ChordLike, toChordData } from '../../theory/symbol/index.js';\nimport {\n type Draw,\n type GenerationContextInput,\n resolveContext,\n sustainsShift,\n} from '../context/index.js';\nimport {\n assertBassSegments,\n barTiles,\n bassPcOf,\n bassRegister,\n bassToneCycle,\n beatPositions,\n DEFAULT_OCTAVE,\n DEFAULT_TS,\n fifthPcOf,\n midBarPulse,\n approachNote as neighborOf,\n placeAboveBass,\n placePc,\n placeRoot,\n STRONG_VELOCITY,\n WEAK_VELOCITY,\n} from './internal.js';\n\nexport type { PlaceLicksOptions } from './licks.js';\nexport { placeLicks } from './licks.js';\nexport type { BassLick, LickMaterial, LickNote } from './licks-data.js';\nexport { BASS_LICKS, isLickMaterial } from './licks-data.js';\n\n/**\n * A chord sounding over a half-open beat span `[startBeat, endBeat)`.\n *\n * The same record {@link ChordSegment} names, so a timeline segment can be fed\n * straight to the bass generator.\n *\n * @category Composition\n */\nexport type BassSegment = ChordSegment;\n\n/**\n * Every bass-line idiom, in declaration order.\n *\n * @category Composition\n */\nexport const BASS_STYLES = Object.freeze([\n 'root',\n 'rootFifth',\n 'pop',\n 'walking',\n 'arpeggio',\n] as const);\n\n/**\n * The bass-line idiom to generate.\n *\n * @category Composition\n */\nexport type BassStyle = (typeof BASS_STYLES)[number];\n\n/**\n * Options controlling {@link generateBassLine}.\n *\n * @category Composition\n */\nexport type BassLineOptions = {\n /**\n * Chord placement to follow; need not be pre-sorted. Each segment's chord may\n * be written as a chord symbol as well as given as chord data.\n */\n segments: readonly (Omit<BassSegment, 'chord'> & { chord: ChordLike })[];\n /**\n * Key/scale context, used for diatonic approach tones in `walking`. A key\n * name such as `'C major'` is read as that key.\n */\n key: KeyLike;\n /**\n * Time signature; used for metric accents.\n *\n * @defaultValue `{ numerator: 4, denominator: 4 }`\n */\n ts?: MeterLike;\n /**\n * Bass-line idiom.\n *\n * @defaultValue `'root'`\n */\n style?: BassStyle;\n /**\n * Target register as a base MIDI octave; roots land around `octave*12+12`.\n * An octave pickup in the `pop` style may drop a note into the octave below\n * that band, which is where such a pickup belongs, or take the octave above\n * instead on an instrument with nothing below the band to drop into.\n *\n * @defaultValue 2\n */\n octave?: number;\n /**\n * The instrument the line is written for. Giving one is itself the request\n * that the line be playable on it: `octave` then says where in that\n * instrument to aim rather than which absolute band to use, and where the two\n * disagree the instrument wins — a note below the lowest string comes back an\n * octave up, the way a player would take it. Leave it out for a programmed\n * part, which is generated exactly as before.\n */\n instrument?: StringedProfile;\n /**\n * The generation context. Its `complexity.rhythmic` sets how often the `pop`\n * style takes a weak-beat pickup, its `instruments.bass` names the instrument\n * when `instrument` does not, its `bpm` together with\n * `complexity.difficulty` keeps leaps the hand could not make in time out of\n * the line, and its `seed` fixes every deterministic choice the line makes.\n *\n * @defaultValue `{ seed: 0 }`\n */\n ctx?: GenerationContextInput;\n /**\n * Maximum number of segments, and of notes, {@link generateBassLine} may\n * work through. The line is built segment by segment, so this is the guard\n * against an unbounded caller rather than a limit on any search.\n *\n * @defaultValue 1000000\n */\n budget?: number;\n};\n\nconst DEFAULT_STYLE: BassStyle = 'root';\n\n/** How often the `pop` style takes a weak-beat pickup when nothing else says. */\nconst DEFAULT_PICKUP_DENSITY = 0.35;\n\ntype RawNote = { startBeat: number; pitch: number; velocity: number };\n\n/** Working state threaded through the per-segment builders. */\ntype BuildContext = {\n ts: TimeSignature;\n low: number;\n key: KeyScale;\n /** Position-addressed draws: every decision is keyed by where it happens. */\n draw: Draw;\n notes: RawNote[];\n /**\n * Where the chord being written sounds its bass note. Every pitch of the\n * segment is placed against it rather than against the note before, so the\n * same chord degree over the same chord is the same MIDI note every time that\n * chord comes round.\n */\n rootMidi: number;\n /**\n * The chord names a bass of its own, so the segment is an inversion and every\n * tone of it is placed above that bass rather than in the band.\n */\n overBass: boolean;\n /** The last note emitted, which decides which way an approach note leads. */\n prevMidi: number;\n instrument: StringedProfile | undefined;\n /** How often a weak beat takes a pickup, in [0, 1]. */\n pickupDensity: number;\n bpm: number | undefined;\n difficulty: number | undefined;\n};\n\n/**\n * The pitch a placement actually sounds as.\n *\n * The band already sits inside the instrument, so this catches the placements\n * that deliberately leave it — the pop octave pickup and walking approach notes\n * — rather than transposing the line wholesale. It is a function of its own so\n * that a builder choosing between two placements can ask what each of them will\n * sound as: a note is only an octave below the band if the instrument reaches\n * down there, and the choice has to be made about the note that will be heard.\n */\nfunction sounding(ctx: BuildContext, placed: number): number {\n return ctx.instrument ? foldIntoRange(placed, ctx.instrument) : placed;\n}\n\n/** Where a tone of the chord being written goes, band or inversion. */\nfunction placeTone(ctx: BuildContext, pc: number): number {\n return ctx.overBass ? placeAboveBass(pc, ctx.rootMidi) : placePc(pc, ctx.rootMidi, ctx.low);\n}\n\n/** Append a note for pitch class `pc` at `pos`, placed against the chord's bass. */\nfunction emit(ctx: BuildContext, pos: number, pc: number, midiOverride?: number): number {\n const placed = clampToMidi(midiOverride ?? placeTone(ctx, pc), 'generated bass pitch');\n const midi = sounding(ctx, placed);\n const velocity = isStrongBeat(pos, ctx.ts) ? STRONG_VELOCITY : WEAK_VELOCITY;\n ctx.notes.push({ startBeat: pos, pitch: midi, velocity });\n ctx.prevMidi = midi;\n return midi;\n}\n\n/** One root note per segment. */\nfunction buildRoot(ctx: BuildContext, seg: BassSegment): void {\n emit(ctx, seg.startBeat, bassPcOf(seg.chord));\n}\n\n/**\n * Root and fifth alternating, once per bar the segment covers.\n *\n * The alternation is the style's name, so it is written against the bar the\n * meter states rather than against the segment's length: a chord held for four\n * bars is played four times, not stretched into a root and a fifth of eight\n * beats each.\n */\nfunction buildRootFifth(ctx: BuildContext, seg: BassSegment): void {\n for (const tile of barTiles(seg.startBeat, seg.endBeat, ctx.ts)) {\n emit(ctx, tile.startBeat, bassPcOf(seg.chord));\n const answer = midBarPulse(tile.startBeat, ctx.ts);\n if (answer > tile.startBeat + BEAT_EPS && answer < tile.endBeat - BEAT_EPS) {\n emit(ctx, answer, fifthPcOf(seg.chord));\n }\n }\n}\n\n/** Root on every strong beat, with occasional octave/fifth pickups on weak beats. */\nfunction buildPop(ctx: BuildContext, seg: BassSegment, index: number): void {\n const positions = beatPositions(seg.startBeat, seg.endBeat, ctx.ts);\n const rootPc = bassPcOf(seg.chord);\n let emitted = false;\n for (const pos of positions) {\n if (isStrongBeat(pos, ctx.ts)) {\n emit(ctx, pos, rootPc);\n emitted = true;\n } else if (ctx.draw.prob(ctx.pickupDensity, 'pickup', index, pos)) {\n emitted = pickup(ctx, seg, pos, index) || emitted;\n }\n }\n if (!emitted) {\n emit(ctx, seg.startBeat, rootPc);\n }\n}\n\n/**\n * The weak-beat pickup a `pop` line leads into the next strong beat with.\n *\n * The octave is the idiom's own figure, so it is asked for first. It is a leap,\n * so it is also the candidate a difficulty ceiling rejects first: at that tempo\n * the hand does not get there, and the player takes the fifth instead of\n * dropping the pickup. The leap is measured against the note actually before\n * it, which sits anywhere in the band, rather than against the octave the\n * figure is named for.\n *\n * The fifth is taken again when the instrument has no octave to give. Where the\n * chord has no fifth of its own that is the root a second time, and rather than\n * repeat the note the beat is left empty: a pickup is an extra note, so not\n * playing it is always available.\n *\n * @returns Whether a note was written.\n */\nfunction pickup(ctx: BuildContext, seg: BassSegment, pos: number, index: number): boolean {\n const octave = octavePickup(ctx);\n if (\n ctx.draw.prob(0.5, 'pickupKind', index, pos) &&\n octave !== undefined &&\n reachableLeap(ctx, pos, Math.abs(sounding(ctx, octave) - ctx.prevMidi))\n ) {\n emit(ctx, pos, bassPcOf(seg.chord), octave);\n return true;\n }\n const fifthPc = fifthPcOf(seg.chord);\n if (sounding(ctx, placeTone(ctx, fifthPc)) === sounding(ctx, ctx.rootMidi)) {\n return false;\n }\n emit(ctx, pos, fifthPc);\n return true;\n}\n\n/**\n * Where an octave pickup goes, or nothing when the instrument has no octave.\n *\n * Below the band is where such a pickup belongs and it is taken whenever it can\n * be heard. The band is one octave wide, though, so an instrument that does not\n * reach under it folds the drop straight back onto the root already sounding —\n * a four-string bass has nothing below its E — and a pickup that repeats the\n * note under it is the one thing a pickup cannot be. The octave above is the\n * same figure played the other way, which is what a player without the low\n * string does, so it is taken before the figure is given up. Both are judged by\n * what they sound as rather than by where they are written, since the fold is\n * the whole question.\n */\nfunction octavePickup(ctx: BuildContext): number | undefined {\n const root = sounding(ctx, ctx.rootMidi);\n for (const candidate of [ctx.rootMidi - 12, ctx.rootMidi + 12]) {\n if (candidate >= 0 && candidate <= 127 && sounding(ctx, candidate) !== root) {\n return candidate;\n }\n }\n return undefined;\n}\n\n/**\n * Whether the hand covers a leap of `semitones` in the time since the last note.\n *\n * Answers true whenever no ceiling or no tempo was given: a limit nobody stated\n * constrains nothing, which is what keeps the programmed case unchanged.\n */\nfunction reachableLeap(ctx: BuildContext, pos: number, semitones: number): boolean {\n const previous = ctx.notes[ctx.notes.length - 1];\n const available = previous === undefined ? Number.POSITIVE_INFINITY : pos - previous.startBeat;\n return sustainsShift(semitones, available, ctx.bpm, ctx.difficulty);\n}\n\n/** Cycle bass -> third -> fifth (-> seventh) across the segment's beats. */\nfunction buildArpeggio(ctx: BuildContext, seg: BassSegment): void {\n const tones = bassToneCycle(seg.chord);\n const positions = beatPositions(seg.startBeat, seg.endBeat, ctx.ts);\n positions.forEach((pos, i) => {\n const pc = tones[i % tones.length] ?? bassPcOf(seg.chord);\n emit(ctx, pos, pc);\n });\n}\n\n/**\n * The neighbour a walking line leads into the next chord with.\n *\n * Whether it is the semitone or the scale step is drawn per segment, so the\n * choice is fixed by where in the piece it happens.\n */\nfunction approachNote(ctx: BuildContext, target: number, from: number, index: number): number {\n return neighborOf(target, from, ctx.key, ctx.draw.prob(0.5, 'approach', index));\n}\n\n/** A quarter-note line of chord tones that leads by step into each chord change. */\nfunction buildWalking(\n ctx: BuildContext,\n seg: BassSegment,\n next: BassSegment | undefined,\n index: number,\n): void {\n const tones = bassToneCycle(seg.chord);\n const positions = beatPositions(seg.startBeat, seg.endBeat, ctx.ts);\n const count = positions.length;\n positions.forEach((pos, i) => {\n if (i === 0) {\n emit(ctx, pos, bassPcOf(seg.chord));\n return;\n }\n if (next && count > 1 && i === count - 1) {\n // The next chord's bass is already fixed by its own pitch class and the\n // band, so the line leads into the note that will actually sound rather\n // than into a copy of it placed somewhere else.\n const nextBass = placeRoot(bassPcOf(next.chord), ctx.low);\n const midi = approachNote(ctx, nextBass, ctx.prevMidi, index);\n emit(ctx, pos, pitchClass(midi), midi);\n return;\n }\n const pc = tones[i % tones.length] ?? bassPcOf(seg.chord);\n emit(ctx, pos, pc);\n });\n}\n\n/**\n * Generate a bass line following a chord placement.\n *\n * Each segment contributes notes in the chosen bass register per the selected\n * style; consecutive notes are kept within roughly a fifth. Every note's\n * duration extends to the next onset (the final note extends to the last\n * segment's end). Given a seed the output is fully reproducible.\n *\n * The `pop` style's pickups are drawn per position, so raising\n * `complexity.rhythmic` adds pickups without moving the notes already written.\n *\n * With an `instrument` the line is written for that instrument: the register\n * band is moved into its range and any note still outside comes back an octave,\n * so every pitch has a fret. Nothing is dropped or replaced — the rhythm and\n * the note count are the same either way.\n *\n * @param opts Segments, key, and generation options.\n * @returns Bass notes sorted by onset, non-overlapping.\n *\n * @example\n * ```ts\n * import { generateBassLine, makeChord, majorKey } from '@libraz/libcantus';\n * const segments = [\n * { startBeat: 0, endBeat: 4, chord: makeChord(0, 'maj') },\n * { startBeat: 4, endBeat: 8, chord: makeChord(7, 'maj') },\n * ];\n * const notes = generateBassLine({ segments, key: majorKey(0), style: 'walking' });\n * ```\n *\n * @category Composition\n */\nexport function generateBassLine(opts: BassLineOptions): NoteEvent[] {\n assertRecord(opts, 'bass line options');\n assertArray(opts.segments, 'bass segments');\n assertGenerationBudget(opts.segments.length, 'bass segments', opts.budget);\n // The chords are read into their plain form once, here at the boundary; the\n // builders below work on chord data alone.\n const segments: BassSegment[] = opts.segments\n .map((segment) => ({ ...segment, chord: toChordData(segment.chord) }))\n .sort((a, b) => a.startBeat - b.startBeat);\n assertBassSegments(segments);\n if (segments.length === 0) {\n return [];\n }\n\n const ts = meterAt(0, toMeterData(opts.ts ?? DEFAULT_TS, 'ts'));\n const style = assertOneOf(opts.style ?? DEFAULT_STYLE, BASS_STYLES, 'bass style');\n const octave = opts.octave ?? DEFAULT_OCTAVE;\n // The generated bass band spans `low..low + 12`, so octave 8 is the highest\n // base that can keep every emitted pitch in the MIDI domain.\n assertInteger(octave, 'bass octave', -1, 8);\n const estimatedNotes = segments.reduce(\n (count, segment) => count + Math.max(1, Math.ceil(segment.endBeat - segment.startBeat)),\n 0,\n );\n assertGenerationBudget(estimatedNotes, 'bass notes', opts.budget);\n const resolved = resolveContext(opts.ctx);\n const { instrument, low } = bassRegister(resolved.instrument('bass'), opts.instrument, octave);\n\n const ctx: BuildContext = {\n ts,\n low,\n key: toKeyScale(opts.key),\n draw: resolved.part('bass'),\n notes: [],\n rootMidi: low,\n overBass: false,\n prevMidi: low,\n instrument,\n pickupDensity: resolved.rhythmic ?? DEFAULT_PICKUP_DENSITY,\n bpm: resolved.bpm,\n difficulty: resolved.difficulty,\n };\n\n for (let s = 0; s < segments.length; s += 1) {\n const seg = segments[s];\n if (!seg) {\n continue;\n }\n ctx.rootMidi = placeRoot(bassPcOf(seg.chord), ctx.low);\n ctx.overBass = seg.chord.bassPc !== undefined;\n switch (style) {\n case 'rootFifth':\n buildRootFifth(ctx, seg);\n break;\n case 'pop':\n buildPop(ctx, seg, s);\n break;\n case 'walking':\n buildWalking(ctx, seg, segments[s + 1], s);\n break;\n case 'arpeggio':\n buildArpeggio(ctx, seg);\n break;\n default:\n buildRoot(ctx, seg);\n break;\n }\n }\n\n ctx.notes.sort((a, b) => a.startBeat - b.startBeat);\n const lastEnd = segments.reduce((m, seg) => Math.max(m, seg.endBeat), Number.NEGATIVE_INFINITY);\n\n const out: NoteEvent[] = [];\n for (let i = 0; i < ctx.notes.length; i += 1) {\n const note = ctx.notes[i];\n if (!note) {\n continue;\n }\n const nextStart = ctx.notes[i + 1]?.startBeat ?? lastEnd;\n const durationBeat = nextStart - note.startBeat;\n if (durationBeat <= BEAT_EPS) {\n continue;\n }\n out.push({\n pitch: clampToMidi(note.pitch, 'generated bass pitch'),\n startBeat: note.startBeat,\n durationBeat,\n velocity: note.velocity,\n });\n }\n return out;\n}\n","/**\n * Imitative entries: a second voice restating what the first has just sung.\n *\n * A motif transform has no time relationship to anything — it returns a cell,\n * and where that cell goes is the caller's problem. An imitation is defined by\n * that relationship: it enters at a stated beat, at a stated interval, against a\n * voice that is still sounding. That is what a canon is, and what the answering\n * phrase in the second half of a chorus is.\n */\n\nimport { InvalidInputError } from '../../core/errors/index.js';\nimport type { IntervalLike, SpelledInterval } from '../../core/pitch/index.js';\nimport { toSpelledInterval } from '../../core/pitch/index.js';\nimport type { NoteEvent } from '../../core/types.js';\nimport {\n assertFiniteNumber,\n assertNoteEvents,\n assertOneOf,\n assertRecord,\n dropSilentNotes,\n} from '../../core/validation/index.js';\nimport {\n type KeyLike,\n scaleLadderPitch,\n scaleLadderPosition,\n shiftByScaleDegrees,\n toKeyScale,\n} from '../../theory/scale/index.js';\n\n/** Every way an imitation can answer, in declaration order. */\nconst IMITATION_ANSWERS = ['real', 'tonal'] as const;\n\n/**\n * How an imitation answers the voice it follows.\n *\n * A `real` answer transposes literally, keeping every interval of the subject\n * and leaving the key where the interval puts it. A `tonal` answer counts scale\n * degrees instead, so the answer stays in the key: every interval keeps the\n * number of degrees it spanned in the subject, and only its quality changes as\n * the key's own pattern of steps and half steps asks — a major third answered by\n * a minor one.\n *\n * @category Composition\n */\nexport type ImitationAnswer = (typeof IMITATION_ANSWERS)[number];\n\n/**\n * Options controlling {@link imitate}.\n *\n * @category Composition\n */\nexport type ImitationOptions = {\n /**\n * Beat at which the imitation enters; the copied material starts here.\n * Negative like any other onset, for an answer that enters in a pickup.\n */\n atBeat: number;\n /**\n * Interval to imitate at, spelled: `'P5'` for an answer a fifth above,\n * `'-P4'` for one a fourth below.\n */\n interval: IntervalLike;\n /**\n * The key the answer is measured in; what makes a tonal answer tonal. A key\n * name such as `'C major'` is read as that key.\n */\n key: KeyLike;\n /**\n * Whether the answer transposes literally or by scale degree.\n *\n * @defaultValue 'real'\n */\n answer?: ImitationAnswer;\n /**\n * Turn the subject upside down before transposing it, mirroring it about its\n * own first note: by semitone for a real answer, by scale degree for a tonal\n * one.\n *\n * @defaultValue false\n */\n invert?: boolean;\n /** First beat of the lead to imitate; defaults to the start of the line. */\n from?: number;\n /** Beat to imitate up to, exclusive; defaults to the end of the line. */\n to?: number;\n /**\n * Factor applied to the copied velocities, for an answer that sits under the\n * voice it follows.\n *\n * @defaultValue 1\n */\n velocityScale?: number;\n};\n\n/** How many scale degrees a spelled interval spans, signed by its direction. */\nfunction degreesOf(interval: SpelledInterval): number {\n const descending = interval.descending ?? interval.semitones < 0;\n return (interval.number - 1) * (descending ? -1 : 1);\n}\n\n/** Reject a pitch the transposition has pushed off the keyboard. */\nfunction assertPlayable(pitch: number): number {\n if (pitch < 0 || pitch > 127) {\n throw new InvalidInputError(`imitated pitch ${pitch} is outside the MIDI range 0..127`);\n }\n return pitch;\n}\n\n/**\n * Answer a leading voice with an imitation of it.\n *\n * The chosen span of the lead is copied so that its first note falls on\n * `atBeat`, keeping its internal rhythm; only the answer is returned, so the\n * caller decides what to do with the two voices together. Pitches move by\n * `interval`: literally for a real answer, by scale degree for a tonal one, and\n * upside down first when `invert` is set. Inversion mirrors about the first note\n * of the copied span, so the answer starts where the transposition puts that\n * note either way. In a tonal answer a pitch outside the key keeps its distance\n * from the scale tone below it, and inversion mirrors that distance too — a\n * chromatic passing note stays a chromatic passing note, on the other side.\n * Where a mirrored chromatic note falls into a diatonic semitone the key leaves\n * it no room, and it lands on the scale tone there, so a subject saturated with\n * chromatic steps can answer with a pitch repeated.\n *\n * @param lead The voice being imitated, in ascending onset order.\n * @param opts Where the answer enters, at what interval, and in what key.\n * @returns The imitation as note events sorted by onset; `[]` when the chosen\n * span holds no sounding notes. Notes with a zero or negative duration never\n * sound and are not copied, so the answer can be shorter than the span. Every\n * other field of a copied note — its articulation, and its velocity through\n * `velocityScale` — comes across with it.\n * @throws If the span is reversed, the answer is not a known kind, or a\n * transposed pitch leaves the MIDI range.\n * @example\n * ```ts\n * import { imitate, majorKey } from '@libraz/libcantus';\n * const lead = [0, 1, 2].map((beat) => ({ pitch: 60 + beat, startBeat: beat, durationBeat: 1 }));\n * imitate(lead, { atBeat: 2, interval: 'P5', key: majorKey(0) });\n * // the same three notes a fifth up, entering at beat 2\n * ```\n * @category Composition\n */\nexport function imitate(lead: readonly NoteEvent[], opts: ImitationOptions): NoteEvent[] {\n assertNoteEvents(lead, 'imitate lead', { allowNonPositiveDuration: true });\n assertRecord<ImitationOptions>(opts, 'opts');\n // Onsets are unbounded below library-wide, because a pickup sounds before the\n // downbeat and the downbeat is beat 0; an answer to a pickup enters there too.\n assertFiniteNumber(opts.atBeat, 'atBeat');\n const interval = toSpelledInterval(opts.interval);\n // The key is read into its plain form once, here at the boundary; the degree\n // arithmetic below is given the scale it resolved to.\n const key = toKeyScale(opts.key);\n const from = opts.from ?? Number.NEGATIVE_INFINITY;\n const to = opts.to ?? Number.POSITIVE_INFINITY;\n if (opts.from !== undefined) {\n assertFiniteNumber(opts.from, 'from');\n }\n if (opts.to !== undefined) {\n assertFiniteNumber(opts.to, 'to');\n }\n if (from > to) {\n throw new InvalidInputError(`imitate needs from <= to; received ${from} > ${to}`);\n }\n const velocityScale = opts.velocityScale ?? 1;\n assertFiniteNumber(velocityScale, 'velocityScale');\n // Checked before the empty-span exit, so a malformed option is rejected\n // whether or not the chosen span happens to hold a note.\n const tonal = assertOneOf(opts.answer ?? 'real', IMITATION_ANSWERS, 'imitate answer') === 'tonal';\n\n // Zero-length artefacts are routine in MIDI imports and never sound. Dropping\n // them before the copy keeps the answer passable to any entry point in the\n // library, which reject non-positive durations by default.\n const subject = dropSilentNotes(lead)\n .filter((note) => note.startBeat >= from && note.startBeat < to)\n .sort((a, b) => a.startBeat - b.startBeat);\n const origin = subject[0]?.startBeat;\n const pivot = subject[0]?.pitch;\n if (origin === undefined || pivot === undefined) {\n return [];\n }\n const shift = opts.atBeat - origin;\n const invert = opts.invert ?? false;\n const degrees = degreesOf(interval);\n\n return subject.map((note) => {\n let pitch = note.pitch;\n // A tonal answer counts along the key's ladder of scale tones, which is what\n // carries a pitch between two of them through the transposition as the note\n // it was: {@link shiftByScaleDegrees} keeps its distance above the scale\n // tone below it.\n let offset = 0;\n if (invert) {\n if (tonal) {\n // Mirroring reflects the rung, and the offset with it: a note a semitone\n // above its scale tone lands a semitone below the one it is mirrored\n // onto, which is what keeps two adjacent chromatic pitches from being\n // flattened onto the same answer. The mirrored pitch is a scale tone, so\n // the offset is added back after the degree arithmetic.\n const here = scaleLadderPosition(pitch, key);\n pitch = scaleLadderPitch(2 * scaleLadderPosition(pivot, key).rung - here.rung, key);\n offset = -here.offset;\n } else {\n pitch = 2 * pivot - pitch;\n }\n }\n pitch = tonal\n ? shiftByScaleDegrees(pitch, degrees, key) + offset\n : pitch + Math.round(interval.semitones);\n // The pass owns the pitch and the placement; everything else the subject\n // carries — the articulation an ornament pass wrote, above all — belongs to\n // the answer too, so an imitation composes with the passes before it.\n const answer: NoteEvent = {\n ...note,\n pitch: assertPlayable(pitch),\n startBeat: note.startBeat + shift,\n };\n if (note.velocity !== undefined) {\n answer.velocity = Math.max(1, Math.min(127, Math.round(note.velocity * velocityScale)));\n }\n return answer;\n });\n}\n","import { BEAT_EPS } from '../../analyze/adjacency.js';\nimport type { ChordTimeline } from '../../analyze/timeline/index.js';\nimport { InvalidInputError } from '../../core/errors/index.js';\nimport type { NoteEventIndex } from '../../core/event-index/index.js';\nimport { createNoteEventIndex } from '../../core/event-index/index.js';\nimport type { MeterLike, TimeSignature } from '../../core/meter/index.js';\nimport { isStrongBeat, meterAt, toMeterData } from '../../core/meter/index.js';\nimport type { KeyScale, NoteEvent } from '../../core/types.js';\nimport {\n assertFiniteNumber,\n assertGenerationBudget,\n assertNoteEvents,\n assertOneOf,\n assertRange,\n assertRecord,\n assertTimeSignature,\n clampToMidi,\n describeRejected,\n soundingNotesOnly,\n} from '../../core/validation/index.js';\nimport type { Chord } from '../../theory/chord/index.js';\nimport { chordToneRole } from '../../theory/chord/index.js';\nimport { createsParallelPerfect, isForbiddenMelodicLeap } from '../../theory/counterpoint/index.js';\nimport type { ProfileWeights, VoiceSnapshot } from '../../theory/safety/index.js';\nimport {\n enumerateSafePitches,\n evaluateSafety,\n NoteSafety,\n profileWeights,\n} from '../../theory/safety/index.js';\nimport { type KeyLike, toKeyScale } from '../../theory/scale/index.js';\nimport { type Draw, type GenerationContextInput, resolveContext } from '../context/index.js';\n\n/**\n * Options controlling {@link generateCounterMelody}.\n *\n * @category Voicing & Counterpoint\n */\nexport type CounterMelodyOptions = {\n /** The lead line to write against, in ascending onset order. */\n melody: readonly NoteEvent[];\n /**\n * The harmony to write against. A timeline carries its own segment\n * boundaries, so a chord change anywhere — including off the generator's\n * half-beat probe grid — is seen; prefer it over `chordAt`.\n *\n * Exactly one of `timeline` and `chordAt` is required; `timeline` wins when\n * both are given.\n */\n timeline?: ChordTimeline;\n /**\n * Chord sounding at an absolute beat position, or null when none applies.\n *\n * A callback is opaque: the generator cannot see where it changes, so it\n * probes a half-beat grid and otherwise relies on `chordChangeBeats`. A held\n * counter note that spans an unseen change is not checked against the new\n * chord.\n */\n chordAt?: (beat: number) => Chord | null;\n /**\n * Exact chord-change beats the `chordAt` callback does not expose. Ignored\n * when `timeline` is given, since a timeline lists its own boundaries.\n */\n chordChangeBeats?: number[];\n /**\n * Key/scale context for scale-tone decisions. A key name such as `'C major'`\n * is read as that key.\n */\n key: KeyLike;\n /**\n * Meter used for strong-beat decisions, in any form that names one.\n *\n * @defaultValue 4/4\n */\n ts?: MeterLike;\n /**\n * Which side of the melody the counter line occupies.\n *\n * @defaultValue 'below'\n */\n register?: 'above' | 'below';\n /**\n * Onset strategy: `'complement'` moves where the melody holds or rests and\n * reinforces some strong beats; `'follow'` mirrors melody onsets.\n *\n * @defaultValue 'complement'\n */\n rhythm?: 'complement' | 'follow';\n /**\n * Style profile applied to candidate pitches. It sets what is rejected\n * outright and, through {@link PROFILE_WEIGHTS}, what is preferred among the\n * candidates that remain: `'strict'` seeks contrary motion and avoids\n * similar, `'pop'` treats a run of parallel thirds or sixths as the harmony\n * line an arranger would write.\n *\n * @defaultValue 'pop'\n */\n profile?: 'strict' | 'pop';\n /**\n * Per-field overrides of the profile's ranking weights, for a preference the\n * two stock profiles do not express — a line that should leap more freely, or\n * one that should hold a pedal wherever it can. Every field named must be a\n * finite number.\n */\n weights?: Partial<ProfileWeights>;\n /** Lowest MIDI pitch the counter line may use (default derived from `register`). */\n pitchLow?: number;\n /** Highest MIDI pitch the counter line may use (default derived from `register`). */\n pitchHigh?: number;\n /**\n * The generation context, so the counter line is drawn from the same project\n * source as every other part and no two parts collide.\n *\n * Its `seed` places the `'complement'` onsets and breaks ties between equally\n * good candidate pitches; the same seed always yields the same line. Where the\n * constraints leave only one good answer — which is common with\n * `rhythm: 'follow'`, whose onsets are fixed by the melody — changing the seed\n * changes nothing.\n *\n * @defaultValue `{ seed: 0 }`\n */\n ctx?: GenerationContextInput;\n};\n\n/** Grid resolution, in quarter-note beats, scanned for complement onsets. */\nconst GRID_STEP = 0.5;\n\n/** Minimum spacing between counter onsets, keeping the line uncluttered. */\nconst MIN_ONSET_GAP = 1;\n\n/** Semitone offset of the counter register's centre from the melody's mean pitch. */\nconst REGISTER_OFFSET = 12;\n\n/** Half-width, in semitones, of the default counter register. */\nconst DEFAULT_HALF_RANGE = 10;\n\n/** Probability of moving on a whole beat while the melody sustains. */\nconst HOLD_ON_BEAT_PROB = 0.55;\n\n/** Probability of moving on an off-beat subdivision while the melody sustains. */\nconst HOLD_OFF_BEAT_PROB = 0.2;\n\n/** Probability of reinforcing a strong beat that the melody also attacks. */\nconst REINFORCE_PROB = 0.35;\n\n/** Velocity assumed for melody notes that carry none. */\nconst DEFAULT_MELODY_VELOCITY = 96;\n\n/** How far below the concurrent melody velocity the counter line sits. */\nconst VELOCITY_DROP = 16;\n\n/**\n * Magnitude of the seed-driven per-candidate perturbation. Kept far below the\n * smallest real score difference (the register-centre term alone moves in steps\n * of 0.02), so the seed can only decide between candidates whose scores are\n * otherwise exactly equal, and never overrides consonance or motion.\n */\nconst TIE_BREAK_JITTER = 1e-6;\n\n/** Whether a beat position falls on a whole quarter-note beat. */\nfunction isWholeBeat(beat: number): boolean {\n return Math.abs(beat - Math.round(beat)) < BEAT_EPS;\n}\n\n/** The melody note sounding at a beat (latest onset wins on overlaps). */\nfunction melodyNoteAt(melody: NoteEventIndex, beat: number): NoteEvent | undefined {\n return melody.at(beat)?.note;\n}\n\n/** Whether any melody note attacks exactly at a beat. */\nfunction melodyAttacksAt(melody: NoteEventIndex, beat: number): boolean {\n return melody.attacksAt(beat);\n}\n\n/** Melody voice at a transition, including the pitch immediately before it. */\nfunction melodySnapshotAt(melody: NoteEventIndex, beat: number): VoiceSnapshot | undefined {\n const current = melodyNoteAt(melody, beat);\n if (current === undefined) {\n return undefined;\n }\n const attacked = Math.abs(current.startBeat - beat) < BEAT_EPS;\n const previous = attacked ? melodyNoteAt(melody, beat - BEAT_EPS * 2) : current;\n const snapshot: VoiceSnapshot = { pitch: current.pitch };\n if (previous !== undefined) {\n snapshot.prevPitch = previous.pitch;\n }\n return snapshot;\n}\n\n/** Counter onsets mirroring the melody's own onsets, deduplicated and sorted. */\nfunction followOnsets(melody: NoteEventIndex): number[] {\n return [...new Set(melody.notes.map(({ note }) => note.startBeat))];\n}\n\n/** Meter assumed when none is supplied. */\nconst DEFAULT_TS: TimeSignature = { numerator: 4, denominator: 4 };\n\n/**\n * Largest difference between two candidate scores that are the same score.\n *\n * Not a beat tolerance: a score is a sum of weighted preferences, so what this\n * bounds is the residue of adding those weights up in a different order. It is\n * named apart from the beat axis so that a correction to either does not travel\n * to the other.\n */\nconst SCORE_EPS = 1e-9;\n\n/**\n * Counter onsets complementing the melody: whole beats inside rests are always\n * taken, positions where the melody sustains are taken probabilistically, and\n * strong beats the melody also attacks are occasionally reinforced. A minimum\n * gap between accepted onsets keeps the line sparse.\n *\n * Each grid position is drawn for by where it falls rather than by how many\n * positions preceded it, so editing one bar of the melody leaves the onsets of\n * every other bar where they were.\n */\nfunction complementOnsets(\n melody: NoteEventIndex,\n ts: TimeSignature,\n spanStart: number,\n spanEnd: number,\n draw: Draw,\n): number[] {\n const onsets: number[] = [];\n const gridStart = Math.floor(spanStart / GRID_STEP) * GRID_STEP;\n let last = Number.NEGATIVE_INFINITY;\n for (let beat = gridStart; beat < spanEnd - BEAT_EPS; beat += GRID_STEP) {\n const attacked = melodyAttacksAt(melody, beat);\n const sounding = melodyNoteAt(melody, beat) !== undefined;\n let place = false;\n if (!attacked && !sounding) {\n place = isWholeBeat(beat);\n } else if (!attacked) {\n place = draw.prob(isWholeBeat(beat) ? HOLD_ON_BEAT_PROB : HOLD_OFF_BEAT_PROB, 'hold', beat);\n } else if (isStrongBeat(beat, ts)) {\n place = draw.prob(REINFORCE_PROB, 'reinforce', beat);\n }\n if (place && beat - last >= MIN_ONSET_GAP - BEAT_EPS) {\n onsets.push(beat);\n last = beat;\n }\n }\n return onsets;\n}\n\n/** Whether an interval class is a third or a sixth (an imperfect consonance). */\nfunction imperfectClass(ic: number): boolean {\n return ic === 3 || ic === 4 || ic === 8 || ic === 9;\n}\n\n/**\n * Whether two successive intervals are the same imperfect consonance, so the\n * voices are running in parallel thirds or parallel sixths.\n *\n * Thirds and sixths are grouped by size rather than by quality: a minor third\n * followed by a major third is the parallel-third texture an arranger hears,\n * and a bare pitch cannot tell the two qualities apart in any case.\n */\nfunction keepsImperfectInterval(before: number, now: number): boolean {\n if (!imperfectClass(before) || !imperfectClass(now)) {\n return false;\n }\n return before <= 4 === now <= 4;\n}\n\n/**\n * Validate the caller's ranking-weight overrides before anything is scored.\n *\n * Every field of the table is summed into a candidate's score, so a single\n * non-finite override makes every candidate score NaN alike: no comparison ever\n * holds, the search keeps whichever candidate it enumerated first, and the line\n * comes back plausible but unranked with nothing in the output to say so.\n *\n * @param overrides The per-field overrides, if the caller named any.\n * @throws If the overrides are not an object, or any field is not a finite\n * number — including a field written as an explicit `undefined`, which would\n * otherwise overwrite the profile's own value.\n */\nfunction assertWeightOverrides(overrides: Partial<ProfileWeights> | undefined): void {\n if (overrides === undefined) {\n return;\n }\n if (typeof overrides !== 'object' || overrides === null) {\n throw new InvalidInputError(\n `countermelody weights must be an object of weight overrides; received ${describeRejected(overrides)}`,\n );\n }\n for (const [field, value] of Object.entries(overrides)) {\n assertFiniteNumber(value as number, `countermelody weights.${field}`);\n }\n}\n\n/**\n * Preference score for one candidate counter pitch, under the weights the\n * profile carries. Every term names a situation the weight table prices:\n * consonance with the melody, chord-tone membership, the kind of motion the two\n * voices make, the size of the melodic move, and the drift from the register\n * centre. The weights decide which of those the caller's style actually wants.\n */\nfunction scoreCandidate(\n pitch: number,\n melPitch: number | undefined,\n melPrev: number | undefined,\n prevPitch: number | undefined,\n chord: Chord | null,\n center: number,\n weights: ProfileWeights,\n): number {\n let score = 0;\n const ic = melPitch === undefined ? undefined : Math.abs(pitch - melPitch) % 12;\n if (ic !== undefined) {\n if (imperfectClass(ic)) {\n score += weights.imperfectConsonance;\n } else if (ic === 0 || ic === 7) {\n score += weights.perfectConsonance;\n } else if (ic === 1 || ic === 2 || ic === 6 || ic === 10 || ic === 11) {\n // The fourth is left unpriced: whether it is a dissonance depends on what\n // is under it, which the safety verdict has already ruled on.\n score += weights.dissonance;\n }\n }\n if (chord && chordToneRole(pitch, chord) !== null) {\n score += weights.chordTone;\n }\n if (prevPitch !== undefined) {\n const move = pitch - prevPitch;\n const dist = Math.abs(move);\n score += dist * weights.melodicDistance;\n if (dist > 0 && dist <= 2) {\n score += weights.stepwise;\n }\n if (isForbiddenMelodicLeap(prevPitch, pitch)) {\n score += weights.forbiddenLeap;\n }\n if (melPitch !== undefined && melPrev !== undefined && ic !== undefined) {\n const melMove = melPitch - melPrev;\n if ((move > 0 && melMove < 0) || (move < 0 && melMove > 0)) {\n score += weights.contraryMotion;\n } else if (move === 0 || melMove === 0) {\n score += weights.obliqueMotion;\n } else if (keepsImperfectInterval(Math.abs(prevPitch - melPrev) % 12, ic)) {\n score += weights.parallelImperfect;\n } else {\n score += weights.similarMotion;\n }\n }\n }\n score += Math.abs(pitch - center) * weights.registerDrift;\n return score;\n}\n\n/** Event boundaries at which a held counter pitch can acquire a new context. */\nfunction heldNoteBoundaries(\n melody: NoteEventIndex,\n startBeat: number,\n endBeat: number,\n chordChangeBeats: number[],\n): number[] {\n const boundaries = new Set<number>([startBeat]);\n for (const onset of melody.onsetsBetween(startBeat, endBeat)) {\n boundaries.add(onset);\n }\n for (const beat of chordChangeBeats) {\n if (beat > startBeat + BEAT_EPS && beat < endBeat - BEAT_EPS) {\n boundaries.add(beat);\n }\n }\n // `chordAt` is an opaque callback. Probe the same half-beat grid used by the\n // complement rhythm so ordinary beat/bar chord changes are still found even\n // when the caller does not provide `chordChangeBeats`.\n const firstGrid = Math.floor(startBeat / GRID_STEP + 1) * GRID_STEP;\n for (let beat = firstGrid; beat < endBeat - BEAT_EPS; beat += GRID_STEP) {\n boundaries.add(beat);\n }\n return [...boundaries].sort((a, b) => a - b);\n}\n\n/**\n * Everything about one boundary inside a held counter note that does not depend\n * on which pitch is being tried there.\n */\ntype BoundaryContext = {\n /** Melody voice sounding at the boundary, if any. */\n melodyVoice: VoiceSnapshot | undefined;\n /** `melodyVoice` in the array shape the safety query takes. */\n otherVoices: VoiceSnapshot[];\n /** Chord reported by the caller's callback at the boundary. */\n chord: Chord | null;\n /** Whether the boundary is a strong beat of the meter. */\n strongBeat: boolean;\n /** Whether the boundary is the counter note's own onset. */\n atCounterOnset: boolean;\n};\n\n/**\n * Resolve the candidate-independent context at every boundary a counter note\n * spans. This is computed once per onset rather than once per candidate pitch,\n * which is what keeps the caller's `chordAt` callback from being invoked once\n * per pitch in the register.\n */\nfunction boundaryContexts(\n melody: NoteEventIndex,\n harmony: { chordAt: (beat: number) => Chord | null; chordChangeBeats: number[] },\n startBeat: number,\n endBeat: number,\n ts: TimeSignature,\n): BoundaryContext[] {\n const boundaries = heldNoteBoundaries(melody, startBeat, endBeat, harmony.chordChangeBeats);\n return boundaries.map((boundary) => {\n const melodyVoice = melodySnapshotAt(melody, boundary);\n return {\n melodyVoice,\n otherVoices: melodyVoice === undefined ? [] : [melodyVoice],\n chord: harmony.chordAt(boundary),\n strongBeat: isStrongBeat(boundary, ts),\n atCounterOnset: Math.abs(boundary - startBeat) < BEAT_EPS,\n };\n });\n}\n\n/**\n * Worst safety reached while `pitch` is held through its complete interval.\n * A wrong-side crossing is treated as unavailable. Safe candidates are later\n * preferred over Warning candidates; Dissonant candidates are never emitted.\n */\nfunction heldPitchSafety(\n pitch: number,\n contexts: BoundaryContext[],\n prevPitch: number | undefined,\n key: KeyScale,\n profile: 'strict' | 'pop',\n register: 'above' | 'below',\n low: number,\n high: number,\n): NoteSafety | null {\n let worst = NoteSafety.Safe;\n for (const context of contexts) {\n const melodyVoice = context.melodyVoice;\n if (\n melodyVoice !== undefined &&\n ((register === 'below' && pitch >= melodyVoice.pitch) ||\n (register === 'above' && pitch <= melodyVoice.pitch))\n ) {\n return null;\n }\n // Only `safety` is read, so the suggestion search is turned off rather than\n // run once per candidate and discarded.\n const result = evaluateSafety(\n {\n profile,\n candidatePitch: pitch,\n prevPitch: context.atCounterOnset ? prevPitch : pitch,\n chord: context.chord,\n key,\n otherVoices: context.otherVoices,\n strongBeat: context.strongBeat,\n vocalLow: low,\n vocalHigh: high,\n },\n { suggestions: false },\n );\n worst = Math.max(worst, result.safety) as NoteSafety;\n if (worst === NoteSafety.Dissonant) {\n return worst;\n }\n }\n return worst;\n}\n\n/**\n * Generate a counter melody against a lead line.\n *\n * Onsets are chosen per `rhythm`: `'follow'` places one counter note on each\n * melody onset; `'complement'` fills melody rests and sustains and reinforces\n * an occasional strong beat, spaced at least a beat apart. Each onset's pitch\n * is picked from the safe pitches in the counter register (chord tones and\n * consonant tensions against the sounding melody note), rejecting candidates\n * that would form a parallel perfect interval with the melody or sit on the\n * wrong side of it, then ranked by the profile's weights — which is where the\n * two styles part company: `'strict'` prefers contrary motion and penalizes\n * similar, `'pop'` rewards a run of parallel thirds or sixths and is content\n * with a pedal. Ties are broken by a seeded perturbation far\n * below any real score difference (see `TIE_BREAK_JITTER`), so the same\n * seed always yields the same line while a different seed can only reshuffle\n * candidates that were already equally good. Notes extend to the next counter\n * onset (the last to the melody's end) at a velocity slightly under the melody.\n *\n * @param opts The lead line, chord context callback, key, and generation knobs.\n * @returns The counter line as note events sorted by onset; `[]` for an empty melody.\n * @throws If an option names a value outside its range — including a weight\n * override that is not a finite number, which is rejected before any\n * candidate is scored rather than absorbed into the ranking.\n * @example\n * ```ts\n * import { generateCounterMelody, majorKey, parseChordSymbol } from '@libraz/libcantus';\n * const melody = [{ pitch: 72, startBeat: 0, durationBeat: 2 }];\n * const counter = generateCounterMelody({\n * melody,\n * chordAt: () => parseChordSymbol('C'),\n * key: majorKey(0),\n * }); // note events below the melody, in onset order\n * ```\n * @category Voicing & Counterpoint\n */\nexport function generateCounterMelody(opts: CounterMelodyOptions): NoteEvent[] {\n assertRecord(opts, 'countermelody options');\n // Zero-length artefacts are accepted and ignored, matching the analysis\n // layer, so an array that passed through `analyzeArrangement` can be fed\n // straight in here.\n const timeline = opts.timeline;\n const chordAt = timeline === undefined ? opts.chordAt : (beat: number) => timeline.at(beat);\n if (chordAt === undefined) {\n throw new InvalidInputError('countermelody needs a timeline or a chordAt callback');\n }\n const chordChangeBeats =\n timeline === undefined\n ? (opts.chordChangeBeats ?? [])\n : timeline.segments.flatMap((segment) => [segment.startBeat, segment.endBeat]);\n const register = opts.register ?? 'below';\n // The counter line writes against the voice on its own side of the texture,\n // so a chord in the melody track resolves to that voice rather than to\n // whichever note the caller happened to store last.\n assertNoteEvents(opts.melody, 'countermelody melody', { allowNonPositiveDuration: true });\n const melody = createNoteEventIndex(soundingNotesOnly(opts.melody), {\n tieBreak: register === 'below' ? 'highest' : 'lowest',\n });\n const ts = meterAt(0, toMeterData(opts.ts ?? DEFAULT_TS, 'ts'));\n assertTimeSignature(ts);\n const rhythm = assertOneOf(opts.rhythm ?? 'complement', ['complement', 'follow'], 'rhythm');\n const profile = assertOneOf(opts.profile ?? 'pop', ['strict', 'pop'], 'safety profile');\n assertWeightOverrides(opts.weights);\n const weights = profileWeights(profile, opts.weights);\n // The key is read into its plain form once, here at the boundary; the search\n // below is given the scale it resolved to.\n const key = toKeyScale(opts.key);\n const draw = resolveContext(opts.ctx).part('countermelody');\n // The options are checked before the empty-melody exit, so a call with a\n // malformed option is rejected whether or not the melody happens to sound.\n if (melody.notes.length === 0) {\n return [];\n }\n\n const spanStart = melody.notes[0]?.note.startBeat ?? 0;\n const spanEnd = melody.notes.reduce((end, n) => Math.max(end, n.endBeat), spanStart);\n assertGenerationBudget(Math.ceil((spanEnd - spanStart) / GRID_STEP), 'countermelody grid');\n const meanPitch = melody.notes.reduce((sum, n) => sum + n.note.pitch, 0) / melody.notes.length;\n const defaultCenter =\n register === 'below' ? meanPitch - REGISTER_OFFSET : meanPitch + REGISTER_OFFSET;\n const low = clampToMidi(\n opts.pitchLow ?? Math.round(defaultCenter) - DEFAULT_HALF_RANGE,\n 'countermelody pitchLow',\n );\n const high = clampToMidi(\n opts.pitchHigh ?? Math.round(defaultCenter) + DEFAULT_HALF_RANGE,\n 'countermelody pitchHigh',\n );\n if (low > high) {\n throw new InvalidInputError(\n `countermelody pitchLow must not exceed pitchHigh; received ${low} > ${high}`,\n );\n }\n assertGenerationBudget(high - low + 1, 'countermelody pitch candidates');\n assertGenerationBudget(chordChangeBeats.length, 'chord change beats');\n for (let index = 0; index < chordChangeBeats.length; index += 1) {\n assertRange(\n chordChangeBeats[index] ?? Number.NaN,\n 0,\n Number.MAX_SAFE_INTEGER,\n `chordChangeBeats[${index}]`,\n );\n }\n const center = (low + high) / 2;\n\n const onsets =\n rhythm === 'follow'\n ? followOnsets(melody)\n : complementOnsets(melody, ts, spanStart, spanEnd, draw);\n // Each candidate pitch is evaluated at every boundary its note spans, and the\n // boundaries partition the melody's span, so the whole search is bounded by\n // the total boundary count times the register width.\n const boundaryEstimate =\n onsets.length +\n Math.ceil((spanEnd - spanStart) / GRID_STEP) +\n melody.notes.length +\n chordChangeBeats.length;\n assertGenerationBudget(boundaryEstimate * (high - low + 1), 'countermelody search');\n const jitter = Array.from(\n { length: high - low + 1 },\n (_, index) => draw.at('jitter', index) * TIE_BREAK_JITTER,\n );\n\n const out: NoteEvent[] = [];\n let prevPitch: number | undefined;\n for (let onsetIndex = 0; onsetIndex < onsets.length; onsetIndex += 1) {\n const beat = onsets[onsetIndex];\n if (beat === undefined) {\n continue;\n }\n const nextOnset = onsets[onsetIndex + 1];\n const endBeat = nextOnset ?? Math.max(spanEnd, beat + GRID_STEP);\n const melNote = melodyNoteAt(melody, beat);\n const melPitch = melNote?.pitch;\n const melodyVoice = melodySnapshotAt(melody, beat);\n const melPrev = melodyVoice?.prevPitch;\n const contexts = boundaryContexts(melody, { chordAt, chordChangeBeats }, beat, endBeat, ts);\n const chord = contexts[0]?.chord ?? chordAt(beat);\n const otherVoices: VoiceSnapshot[] = melodyVoice !== undefined ? [melodyVoice] : [];\n\n const candidates = enumerateSafePitches(\n {\n profile,\n prevPitch,\n chord,\n key,\n otherVoices,\n strongBeat: isStrongBeat(beat, ts),\n vocalLow: low,\n vocalHigh: high,\n },\n low,\n high,\n );\n\n let bestPitch: number | undefined;\n let bestWorstSafety = NoteSafety.Dissonant;\n let bestScore = Number.NEGATIVE_INFINITY;\n for (const candidate of candidates) {\n const worstSafety = heldPitchSafety(\n candidate,\n contexts,\n prevPitch,\n key,\n profile,\n register,\n low,\n high,\n );\n if (worstSafety === null || worstSafety === NoteSafety.Dissonant) {\n continue;\n }\n if (melPitch !== undefined) {\n if (\n prevPitch !== undefined &&\n melPrev !== undefined &&\n // Parallel octaves are the perfect-class-zero case of this predicate,\n // so a single check covers both parallel fifths and octaves.\n createsParallelPerfect(prevPitch, candidate, melPrev, melPitch)\n ) {\n continue;\n }\n }\n const score =\n scoreCandidate(candidate, melPitch, melPrev, prevPitch, chord, center, weights) +\n (jitter[candidate - low] ?? 0);\n const tie =\n Math.abs(score - bestScore) <= SCORE_EPS &&\n candidate < (bestPitch ?? Number.POSITIVE_INFINITY);\n if (\n worstSafety < bestWorstSafety ||\n (worstSafety === bestWorstSafety && (score > bestScore + SCORE_EPS || tie))\n ) {\n bestWorstSafety = worstSafety;\n bestScore = score;\n bestPitch = candidate;\n }\n }\n\n if (bestPitch === undefined) {\n // Do not emit an unchecked fallback. Ending the previous note at this\n // planned onset leaves an explicit rest when the constraints are\n // unsatisfiable instead of returning a plausibly named unsafe line.\n prevPitch = undefined;\n continue;\n }\n const pitch = bestPitch;\n const velocity = Math.max(\n 1,\n Math.min(127, Math.round((melNote?.velocity ?? DEFAULT_MELODY_VELOCITY) - VELOCITY_DROP)),\n );\n out.push({\n pitch: clampToMidi(pitch),\n startBeat: beat,\n durationBeat: endBeat - beat,\n velocity,\n });\n prevPitch = pitch;\n }\n return out;\n}\n\nexport type { ImitationAnswer, ImitationOptions } from './imitation.js';\nexport { imitate } from './imitation.js';\n","import type { SectionType } from './internal.js';\nimport { MoodCategory } from './internal.js';\n\n/** Ghost-note position within a beat: the \"e\" (1st 16th) or \"a\" (3rd 16th). */\nexport type GhostPosition = 'e' | 'a';\n\n/** Ghost density level ordinal (0 = none, 3 = heavy). */\ntype GhostDensityLevel = 0 | 1 | 2 | 3;\n\n/**\n * How much a groove ghosts, per section and mood.\n *\n * A row is read by mood category, in the order those categories are declared:\n * calm, standard, energetic.\n */\nexport const GHOST_DENSITY_TABLE: Readonly<Record<SectionType, readonly GhostDensityLevel[]>> =\n Object.freeze({\n intro: [0, 1, 1],\n a: [0, 1, 2],\n b: [1, 2, 2],\n chorus: [1, 2, 3],\n bridge: [1, 1, 2],\n outro: [0, 1, 1],\n });\n\n/** Trigger probability at each density level. */\nconst GHOST_LEVEL_PROBABILITY = Object.freeze([0, 0.15, 0.3, 0.45] as const);\n\nfunction levelToProbability(level: GhostDensityLevel): number {\n return GHOST_LEVEL_PROBABILITY[level];\n}\n\nfunction clampLevel(level: number): GhostDensityLevel {\n return Math.max(0, Math.min(3, level)) as GhostDensityLevel;\n}\n\nfunction adjustForBpm(level: GhostDensityLevel, bpm: number): GhostDensityLevel {\n if (bpm >= 160 && level !== 0) {\n return clampLevel(level - 1);\n }\n if (bpm <= 90 && level !== 3) {\n return clampLevel(level + 1);\n }\n return level;\n}\n\n/**\n * Ghost-note trigger probability for a section, mood, and tempo.\n *\n * How much of it survives is the ornament dial's business, applied by the\n * caller: this is the groove's own appetite for ghosts before any dial.\n */\nexport function getGhostDensity(mood: MoodCategory, section: SectionType, bpm: number): number {\n const row = GHOST_DENSITY_TABLE[section] ?? GHOST_DENSITY_TABLE.a;\n const level = adjustForBpm(row[mood] ?? 0, bpm);\n return levelToProbability(level);\n}\n\n/** Ghost-note velocity multiplier for a section and metric position. */\nexport function getGhostVelocity(section: SectionType, beatPosition: number): number {\n let base = 0.4;\n const even = beatPosition % 2 === 0 ? 0.05 : 0;\n switch (section) {\n case 'a':\n base = 0.35 + even;\n break;\n case 'chorus':\n base = 0.5 + even;\n break;\n case 'bridge':\n base = 0.25 + even;\n break;\n case 'b':\n base = 0.4 + even;\n break;\n case 'intro':\n case 'outro':\n base = 0.38;\n break;\n }\n return Math.max(0.25, Math.min(0.65, base));\n}\n\n/**\n * Probability of a ghost at a specific 16th position, higher when the ghost\n * leads straight into the backbeat.\n *\n * Which beats those are is the groove's own answer rather than a fixed pair:\n * a backbeat on the third beat — which is how trap writes one — is led into\n * from the second, and the amplification written for beats 1 and 3 fell on a\n * beat nothing follows and on the backbeat itself.\n *\n * @param beat Beat index inside the bar.\n * @param sixteenthInBeat Which 16th of that beat, 0 to 3.\n * @param leadIns The beats that lead into a backbeat.\n */\nexport function getGhostProbabilityAtPosition(\n beat: number,\n sixteenthInBeat: number,\n leadIns: readonly number[],\n): number {\n const leadsIntoBackbeat = leadIns.includes(beat) && sixteenthInBeat === 3;\n return leadsIntoBackbeat ? 0.6 : 0.25;\n}\n\n/** Choose which ghost positions a groove favours. */\nexport function selectGhostPositions(mood: MoodCategory): GhostPosition[] {\n if (mood === MoodCategory.Energetic) {\n return ['e', 'a'];\n }\n return ['e'];\n}\n","import type { Draw } from '../context/index.js';\nimport { sustainsStrokes } from '../context/index.js';\nimport type { DrumRole, DrumStyle, SectionType } from './internal.js';\nimport { GM } from './internal.js';\n\n/** Hi-hat subdivision level. */\nexport type HiHatLevel = 'quarter' | 'eighth' | 'sixteenth';\n\n/** Hi-hat articulation type. */\nexport type HiHatType = 'closed' | 'pedal' | 'open' | 'halfOpen' | 'ride';\n\n/** BPM at or above which 16th-note hi-hats become 8ths for playability. */\nexport const HH_16TH_BPM_THRESHOLD = 150;\n\nconst FHH_VEL_MIN = 45;\nconst FHH_VEL_MAX = 60;\n/** Velocity boost applied to dynamic open hi-hat accents. */\nexport const OHH_VEL_BOOST = 7;\n\n/** True unless the role suppresses timekeeping cymbals entirely. */\nexport function shouldPlayHiHat(role: DrumRole): boolean {\n return role !== 'fxOnly';\n}\n\n/** Preferred timekeeping instrument for a role. */\nexport function roleHiHatInstrument(role: DrumRole, useRide: boolean): number {\n if (role === 'ambient') {\n return GM.RIDE;\n }\n if (role === 'minimal') {\n return GM.FHH;\n }\n return useRide ? GM.RIDE : GM.CHH;\n}\n\nfunction sparser(level: HiHatLevel): HiHatLevel {\n if (level === 'sixteenth') {\n return 'eighth';\n }\n if (level === 'eighth') {\n return 'quarter';\n }\n return 'quarter';\n}\n\nfunction denser(level: HiHatLevel): HiHatLevel {\n if (level === 'quarter') {\n return 'eighth';\n }\n if (level === 'eighth') {\n return 'sixteenth';\n }\n return 'sixteenth';\n}\n\n/** Beats between consecutive strokes at a hi-hat subdivision. */\nexport function hiHatStep(level: HiHatLevel): number {\n if (level === 'sixteenth') {\n return 0.25;\n }\n return level === 'eighth' ? 0.5 : 1;\n}\n\n/**\n * Choose the hi-hat subdivision for a section, style, rhythmic dial, and tempo.\n *\n * The style and section propose a level; the dial then moves it, and it moves\n * in one direction only. Both of its tests read a draw fixed by position — one\n * for thinning out, one for filling in — so as the dial rises the thinning\n * switches off before the filling switches on, and the level never falls.\n *\n * A difficulty ceiling can thin the level further: with a tempo to measure\n * against, a subdivision the player cannot keep up is a candidate to reject,\n * not a level to write and hope.\n *\n * The two dial draws are addressed by bar, so the dial moves the hats a bar at\n * a time rather than switching a whole section between two subdivisions.\n */\nexport function getHiHatLevel(\n section: SectionType,\n style: DrumStyle,\n rhythmic: number,\n bpm: number,\n draw: Draw,\n difficulty: number | undefined,\n bar: number,\n): HiHatLevel {\n const allow16th = bpm < HH_16TH_BPM_THRESHOLD;\n let base: HiHatLevel = 'eighth';\n let dialed = true;\n\n if (style === 'sparse') {\n base = section === 'chorus' ? 'eighth' : 'quarter';\n } else if (style === 'fourOnFloor') {\n base = allow16th && section === 'chorus' && draw.prob(0.25, 'hhLevel') ? 'sixteenth' : 'eighth';\n dialed = false;\n } else if (style === 'synth') {\n base = !allow16th || (section === 'a' && draw.prob(0.2, 'hhLevel')) ? 'eighth' : 'sixteenth';\n dialed = false;\n } else if (style === 'trap') {\n base = allow16th ? 'sixteenth' : 'eighth';\n dialed = false;\n } else if (style === 'latin') {\n base = allow16th && section === 'chorus' && draw.prob(0.3, 'hhLevel') ? 'sixteenth' : 'eighth';\n dialed = false;\n } else {\n switch (section) {\n case 'intro':\n base = 'quarter';\n break;\n case 'outro':\n base = 'eighth';\n break;\n case 'a':\n base = draw.prob(0.3, 'hhLevel') ? 'quarter' : 'eighth';\n break;\n case 'b':\n base = allow16th && draw.prob(0.25, 'hhLevel') ? 'sixteenth' : 'eighth';\n break;\n case 'chorus':\n base = allow16th && draw.prob(0.35, 'hhLevel') ? 'sixteenth' : 'eighth';\n break;\n case 'bridge':\n base = 'eighth';\n break;\n }\n }\n\n let level = base;\n if (dialed) {\n // The middle of the dial is the style's own level, so the two halves are\n // read separately: the lower half thins the level out, the upper half\n // fills it in, and each transition is spread across its half instead of\n // landing on one bucket boundary.\n if (draw.at('hhThin', bar) >= 2 * rhythmic) {\n level = sparser(level);\n }\n if (draw.at('hhFill', bar) < 2 * rhythmic - 1) {\n level = denser(level);\n }\n }\n\n if (!allow16th && level === 'sixteenth') {\n level = 'eighth';\n }\n while (level !== 'quarter' && !sustainsStrokes(hiHatStep(level), bpm, difficulty)) {\n level = sparser(level);\n }\n return level;\n}\n\n/**\n * Metric velocity multiplier for a 16th position within a beat.\n *\n * @param sixteenth Position within the beat, 0 to 3.\n * @param jitter Humanizing factor around 1, drawn by the caller for that\n * position so the same 16th of the same bar always sounds the same.\n */\nexport function hiHatVelocityMultiplier(sixteenth: number, jitter: number): number {\n switch (sixteenth) {\n case 0:\n return 0.95 * jitter;\n case 2:\n return 0.75 * jitter;\n case 1:\n return 0.55 * jitter;\n default:\n return 0.5 * jitter;\n }\n}\n\n/** Bars between dynamic open hi-hat accents (0 disables them). */\nexport function openHiHatBarInterval(section: SectionType, style: DrumStyle): number {\n if (style === 'sparse') {\n return section === 'chorus' ? 4 : 0;\n }\n switch (section) {\n case 'intro':\n return style === 'fourOnFloor' ? 4 : 0;\n case 'a':\n return style === 'fourOnFloor' || style === 'upbeat' ? 2 : 4;\n case 'b':\n return 2;\n case 'chorus':\n return style === 'rock' || style === 'fourOnFloor' ? 1 : 2;\n case 'bridge':\n return 0;\n case 'outro':\n return 4;\n }\n}\n\n/** Beat that receives the dynamic open hi-hat within a bar. */\nexport function openHiHatBeat(section: SectionType, draw: Draw, bar: number): number {\n if (section === 'chorus') {\n const choice = draw.range(0, 3, 'ohhBeat', bar);\n if (choice < 2) {\n return 3;\n }\n if (choice < 3) {\n return 1;\n }\n return 2;\n }\n return 3;\n}\n\n/** Whether the section uses an independent foot hi-hat pulse. */\nexport function shouldUseFootHiHat(section: SectionType, role: DrumRole): boolean {\n if (role === 'fxOnly') {\n return false;\n }\n switch (section) {\n case 'intro':\n case 'bridge':\n case 'outro':\n return true;\n default:\n return role === 'ambient' || role === 'minimal';\n }\n}\n\n/**\n * The hi-hat articulation a section leans on.\n *\n * This shapes dynamics only: the note a timekeeping hat actually sounds comes\n * from {@link roleHiHatInstrument}, which follows the voicing role, and from\n * the per-onset open-hat decisions. Feeding this into {@link hiHatNote} instead\n * would let the section override the role's own kit choice.\n */\nexport function sectionHiHatType(section: SectionType, role: DrumRole): HiHatType {\n if (role === 'ambient') {\n return 'ride';\n }\n if (role === 'minimal') {\n return 'pedal';\n }\n switch (section) {\n case 'intro':\n case 'a':\n return 'pedal';\n case 'b':\n return 'closed';\n case 'chorus':\n return 'open';\n case 'bridge':\n return 'ride';\n case 'outro':\n return 'halfOpen';\n }\n}\n\n/** GM note for a hi-hat articulation. */\nexport function hiHatNote(type: HiHatType): number {\n switch (type) {\n case 'pedal':\n return GM.FHH;\n case 'open':\n return GM.OHH;\n case 'ride':\n return GM.RIDE;\n default:\n return GM.CHH;\n }\n}\n\n/**\n * Velocity multiplier for a hi-hat articulation — the only effect\n * {@link sectionHiHatType} has on the output.\n */\nexport function hiHatTypeVelocityMultiplier(type: HiHatType): number {\n switch (type) {\n case 'halfOpen':\n return 0.75;\n case 'pedal':\n return 0.65;\n case 'open':\n return 1.0;\n case 'ride':\n return 0.9;\n default:\n return 0.85;\n }\n}\n\n/** Whether to accent with an open hi-hat at a given beat. */\nexport function shouldAddOpenHHAccent(\n section: SectionType,\n beat: number,\n bar: number,\n draw: Draw,\n): boolean {\n if (section !== 'chorus' && section !== 'b') {\n return false;\n }\n if (section === 'chorus') {\n if (beat === 1 || beat === 3) {\n return draw.prob(0.6, 'ohhAccent', bar, beat);\n }\n return false;\n }\n if (beat === 3 && bar % 2 === 1) {\n return draw.prob(0.4, 'ohhAccent', bar, beat);\n }\n return false;\n}\n\n/** Foot hi-hat velocity with slight humanization, drawn for its own position. */\nexport function footHiHatVelocity(draw: Draw, bar: number, beat: number): number {\n return draw.range(FHH_VEL_MIN, FHH_VEL_MAX, 'fhhVelocity', bar, beat);\n}\n\n/**\n * Whether a style writes its open hi-hats off the beat.\n *\n * A four-on-the-floor kick already occupies every downbeat, so the open hat\n * that answers it belongs on the \"and\". Letting the generic accent rule speak\n * for the same beat put two rules on one decision, and the open hat that\n * defines the style then sounded only when both agreed.\n */\nexport function usesOffbeatOpenHiHat(style: DrumStyle): boolean {\n return style === 'fourOnFloor';\n}\n\n/** Chance an off-beat open hi-hat is taken, slower tempos opening more often. */\nexport function offbeatOpenHiHatChance(bpm: number): number {\n return Math.max(0.15, Math.min(0.8, 45 / bpm));\n}\n\n/** Whether the section uses a ride cymbal instead of hi-hats. */\nexport function shouldUseRideForSection(section: SectionType, style: DrumStyle): boolean {\n if (style === 'rock' && section === 'chorus') {\n return true;\n }\n if (style === 'sparse') {\n return false;\n }\n return section === 'bridge';\n}\n","import type { Feel } from './internal.js';\n\nconst SIXTEENTH = 0.25;\nconst EIGHTH = 0.5;\n/** Delay of an off-beat 16th at full swing (triplet minus straight), in beats. */\nconst DELTA_16 = 1 / 3 - 1 / 4; // 0.08333...\n/** Delay of an off-beat 8th at full swing, in beats. */\nconst DELTA_8 = (2 / 3 - 1 / 2) / 2 + (2 / 3 - 1 / 2) / 2; // 0.16667...\n\n/** Grid resolution for swing quantization. */\nexport type SwingResolution = 'eighth' | 'sixteenth';\n\n/**\n * Effective swing after applying the groove feel to a base swing amount.\n *\n * A feel names how far toward the triplet the off-beats sit, and the amount is\n * already that quantity — `'shuffle'` is the full triplet, `'swing'` a lighter\n * lean. Straight is straight whatever amount comes with it.\n */\nexport function effectiveSwing(feel: Feel, swingAmount: number): number {\n if (feel === 'straight') {\n return 0;\n }\n return Math.max(0, Math.min(1, swingAmount));\n}\n\n/**\n * The grid a resolution swings, as `[position, delay at full swing]` pairs\n * within one beat.\n *\n * The \"a\" 16th is the midpoint of the compressed second half of the beat\n * (between the swung \"and\" and the next downbeat), mirroring the \"e\" 16th in\n * the first half. Because DELTA_8 = 2 * DELTA_16, that midpoint reduces to a\n * single DELTA_16 delay off the straight position, so the \"a\" never overshoots\n * the downbeat and needs no clamp.\n */\nconst SWING_NODES: Readonly<Record<SwingResolution, readonly (readonly [number, number])[]>> =\n Object.freeze({\n eighth: [\n [0, 0],\n [EIGHTH, DELTA_8],\n [1, 0],\n ],\n sixteenth: [\n [0, 0],\n [SIXTEENTH, DELTA_16],\n [EIGHTH, DELTA_8],\n [3 * SIXTEENTH, DELTA_16],\n [1, 0],\n ],\n });\n\n/**\n * Push an off-beat position toward its triplet placement.\n *\n * On-beat positions are returned unchanged and off-beat 8ths and 16ths are\n * delayed proportionally to the swing amount. A position between two grid slots\n * — a Euclidean kick on a five-step bar, a pattern from a caller's own grid — is\n * delayed by an interpolation of its neighbours' delays rather than snapped to\n * one of them: swing is a warp of the beat, so the order of what was written\n * survives it and nothing ever arrives earlier than it was written.\n *\n * @param startBeat Absolute position in beats.\n * @param swing Swing amount in [0, 1] (already feel-adjusted).\n * @param resolution Whether to swing the 16th or only the 8th grid.\n * @returns The swung position in beats, never earlier than `startBeat`.\n */\nexport function quantizeSwing(\n startBeat: number,\n swing: number,\n resolution: SwingResolution,\n): number {\n const clamped = Math.max(0, Math.min(1, swing));\n if (clamped <= 0) {\n return startBeat;\n }\n const base = Math.floor(startBeat);\n const offset = startBeat - base;\n const nodes = SWING_NODES[resolution];\n\n for (let i = 1; i < nodes.length; i += 1) {\n const [from, fromDelay] = nodes[i - 1] ?? [0, 0];\n const [to, toDelay] = nodes[i] ?? [1, 0];\n if (offset < from || offset > to) {\n continue;\n }\n const span = to - from;\n const share = span <= 0 ? 0 : (offset - from) / span;\n const delay = fromDelay + (toDelay - fromDelay) * share;\n return startBeat + delay * clamped;\n }\n return startBeat;\n}\n","import type { Draw } from '../context/index.js';\nimport { sustainsStrokes } from '../context/index.js';\nimport {\n getGhostDensity,\n getGhostProbabilityAtPosition,\n getGhostVelocity,\n selectGhostPositions,\n} from './ghost.js';\nimport {\n footHiHatVelocity,\n type HiHatLevel,\n hiHatNote,\n hiHatTypeVelocityMultiplier,\n hiHatVelocityMultiplier,\n OHH_VEL_BOOST,\n offbeatOpenHiHatChance,\n roleHiHatInstrument,\n sectionHiHatType,\n shouldAddOpenHHAccent,\n shouldPlayHiHat,\n usesOffbeatOpenHiHat,\n} from './hihat.js';\nimport type { HitList } from './hit.js';\nimport type { DrumRole, DrumStyle, Feel, SectionType } from './internal.js';\nimport {\n BACKBEAT_LIFT,\n backbeatBeats,\n EIGHTH,\n GM,\n leanedBy,\n type MoodCategory,\n QUARTER,\n SIXTEENTH,\n} from './internal.js';\nimport type { KickPattern } from './kick.js';\nimport { effectiveSwing, quantizeSwing } from './swing.js';\n\n/** Per-section drum state shared across the bars of a section. */\nexport type SectionCtx = {\n style: DrumStyle;\n feel: Feel;\n /**\n * Whether the feel is the caller's own request rather than the one the style\n * implies. A named feel is taken at its word by every style.\n */\n feelRequested: boolean;\n densityMult: number;\n /** Rhythmic dial in [0, 1]: subdivision and syncopation. */\n rhythmic: number;\n /** Ornament dial in [0, 1]: how many ghosts and decorations survive. */\n ornament: number;\n /** Difficulty ceiling, or undefined when the caller set none. */\n difficulty: number | undefined;\n ghostBoost: boolean;\n useRide: boolean;\n useFootHh: boolean;\n role: DrumRole;\n ghostMood: MoodCategory;\n snareBeat3: boolean;\n};\n\n/** Per-beat context passed to every beat processor. */\nexport type BeatCtx = {\n track: HitList;\n beatTick: number;\n beat: number;\n velocity: number;\n section: SectionType;\n bpm: number;\n bar: number;\n sectionBars: number;\n inPrechorusLift: boolean;\n swingAmount: number;\n barHasOpenHh: boolean;\n openHhBeat: number;\n /** Hi-hat subdivision for this bar. */\n hhLevel: HiHatLevel;\n draw: Draw;\n};\n\n/**\n * How much of the feel's swing a style takes when the caller named no feel.\n *\n * These are the characters of the styles themselves: trap sits on a straight\n * (or triplet-hat) grid and latin only leans. They apply to the feel a style\n * implies, never to one the caller asked for — a request that comes back\n * unrecognisable is worse than one that comes back refused.\n */\nfunction styleSwingFactor(style: DrumStyle): number {\n if (style === 'trap') {\n return 0;\n }\n if (style === 'latin') {\n return 0.35;\n }\n return 1;\n}\n\n/**\n * The one swing amount every voice of a section shares.\n *\n * Kick, hi-hat, and ghost notes must land on the same grid: when the kick's\n * \"and\" is fully swung and the hi-hat's is not, the two voices separate by tens\n * of milliseconds at the same notated position, which is audible as flamming\n * rather than as groove.\n */\nfunction sectionSwing(sec: SectionCtx, swingAmount: number): number {\n const swing = effectiveSwing(sec.feel, swingAmount);\n return sec.feelRequested ? swing : swing * styleSwingFactor(sec.style);\n}\n\n/** Place a tick on the section's shared swung 16th grid. */\nexport function swing16(tick: number, sec: SectionCtx, swingAmount: number): number {\n return quantizeSwing(tick, sectionSwing(sec, swingAmount), 'sixteenth');\n}\n\n/**\n * Emit the kick for one beat.\n *\n * The grid is sixteenths. The downbeat of the beat sounds as written; every\n * other step is an off-beat kick, so it goes through the section's shared swung\n * grid and sits a little below the downbeat, as a player would place it. A kick\n * on an odd sixteenth is the one the ceiling can refuse: at that tempo the foot\n * does not get there, and the stroke is dropped rather than moved.\n */\nexport function generateKickForBeat(ctx: BeatCtx, sec: SectionCtx, kick: KickPattern): void {\n if (ctx.inPrechorusLift) {\n return;\n }\n const sixteenths = kick.length / 4;\n for (let step = 0; step < sixteenths; step += 1) {\n const index = ctx.beat * sixteenths + step;\n if (!kick[index]) {\n continue;\n }\n if (step === 0) {\n ctx.track.add(GM.BD, ctx.beatTick, EIGHTH, ctx.velocity);\n continue;\n }\n const offBeat = step % 2 === 1;\n if (offBeat && !sustainsStrokes(SIXTEENTH, ctx.bpm, sec.difficulty)) {\n continue;\n }\n const tick = swing16(ctx.beatTick + step * SIXTEENTH, sec, ctx.swingAmount);\n ctx.track.add(GM.BD, tick, offBeat ? SIXTEENTH : EIGHTH, ctx.velocity * 0.85);\n }\n}\n\n/** One stroke of the backbeat voice: which drum it is and how hard it is hit. */\nexport type BackbeatStroke = {\n pitch: number;\n velocity: number;\n};\n\n/** How loudly a role plays the backbeat, relative to the beat it lands on. */\nfunction roleBackbeatWeight(role: DrumRole): number {\n if (role === 'minimal') {\n return 0.65;\n }\n return role === 'ambient' ? 0.8 : 1;\n}\n\n/**\n * The drum a section states its backbeat on, or undefined where the role writes\n * no backbeat at all.\n *\n * The style names the instrument: a latin groove states its backbeat as a rim\n * click, so it keeps the side-stick whatever the role asks for, and every other\n * style writes the drum itself. The quieter roles cross-stick theirs, and\n * `'fxOnly'` states none. Only the voice is decided here — the backbeat keeps\n * the positions {@link backbeatBeats} names, so a latin groove is the timbre of\n * a clave rather than its rhythm; the pattern dictionary is where a written-out\n * clave lives.\n *\n * Every voice that answers the backbeat reads this one predicate, so a groove\n * cannot decorate a stroke it never plays.\n *\n * @param sec Section context.\n * @returns The note number of the backbeat voice, or undefined for no backbeat.\n */\nexport function backbeatVoice(sec: SectionCtx): number | undefined {\n if (sec.role === 'fxOnly') {\n return undefined;\n }\n const sideStick = sec.style === 'latin' || sec.role === 'ambient' || sec.role === 'minimal';\n return sideStick ? GM.SIDESTICK : GM.SD;\n}\n\n/**\n * Whether a section decorates its backbeat with ghost snares.\n *\n * A ghost is a soft stroke on the snare head leading into the backbeat, so a\n * groove ghosts exactly when {@link backbeatVoice} is the snare drum: a\n * rim-click clave and a cross-sticked backbeat are not the voice being\n * decorated, and a ghost against either answers a stroke that is not there. A\n * sparse groove keeps its silence whatever it states its backbeat on.\n *\n * The section is not part of the question. How many ghosts survive is the\n * ornament dial's business in every section, and the density table\n * ({@link getGhostDensity}) carries a row for each of them, so a section scales\n * the dial rather than switching the voice off.\n */\nexport function writesSnareGhosts(sec: SectionCtx): boolean {\n return backbeatVoice(sec) === GM.SD && sec.style !== 'sparse';\n}\n\n/**\n * The backbeat stroke of a section: the drum {@link backbeatVoice} names, at\n * the weight its role plays it.\n *\n * `'full'` plays the backbeat at full value, `'ambient'` and `'minimal'` grow\n * quieter with the side-stick they step down to, and `'fxOnly'` writes no\n * backbeat. `'minimal'` is one step quieter than `'ambient'` in every style —\n * never the full backbeat and never nothing at all.\n *\n * @param sec Section context.\n * @param velocity Base velocity of the beat the stroke lands on.\n * @returns The stroke to write, or undefined when the role writes no backbeat.\n */\nexport function backbeatStroke(sec: SectionCtx, velocity: number): BackbeatStroke | undefined {\n const pitch = backbeatVoice(sec);\n if (pitch === undefined) {\n return undefined;\n }\n return pitch === GM.SD\n ? { pitch, velocity: Math.min(127, velocity + BACKBEAT_LIFT) }\n : { pitch, velocity: velocity * roleBackbeatWeight(sec.role) };\n}\n\n/**\n * Beats in the bar the beat processors are written against.\n *\n * The same bar {@link backbeatBeats} answers for by default here: this module\n * is called once per beat of a four-four bar, and a groove in another metre is\n * placed by the vocabulary path rather than by these processors.\n */\nconst FOUR_FOUR_BEATS = 4;\n\n/** Emit the backbeat snare (or side-stick) for one beat. */\nexport function generateSnareForBeat(ctx: BeatCtx, sec: SectionCtx, isIntroFirst: boolean): void {\n if (ctx.inPrechorusLift) {\n return;\n }\n if (!backbeatBeats(sec.snareBeat3).includes(ctx.beat) || isIntroFirst) {\n return;\n }\n const stroke = backbeatStroke(sec, ctx.velocity);\n if (!stroke) {\n return;\n }\n ctx.track.add(stroke.pitch, ctx.beatTick, EIGHTH, stroke.velocity);\n}\n\n/** Emit ghost snares at the \"e\"/\"a\" 16ths of the beats that lead into a backbeat. */\nexport function generateGhostNotesForBeat(ctx: BeatCtx, sec: SectionCtx): void {\n // The one gate on the voice, so no exit path can write a snare-head ghost\n // into a groove whose backbeat is not on the snare head.\n if (!writesSnareGhosts(sec)) {\n return;\n }\n // The beat before each backbeat, wherever the groove puts its backbeat: a\n // snare on the third beat — trap's own backbeat — is led into from the\n // second, and the fixed pair wrote the ghosts a beat early and again on the\n // backbeat itself, so trap's backbeat had no lead-in written at all.\n const backbeats = backbeatBeats(sec.snareBeat3);\n const leadIns = backbeats.map((beat) => (beat - 1 + FOUR_FOUR_BEATS) % FOUR_FOUR_BEATS);\n if (!leadIns.includes(ctx.beat)) {\n return;\n }\n // A ghost is decoration, so the ornament dial decides how many survive; the\n // ceiling decides whether 16ths are reachable at this tempo at all.\n if (!sustainsStrokes(SIXTEENTH, ctx.bpm, sec.difficulty)) {\n return;\n }\n const positions = selectGhostPositions(sec.ghostMood);\n let ghostProb = leanedBy(getGhostDensity(sec.ghostMood, ctx.section, ctx.bpm), sec.ornament);\n if (sec.ghostBoost) {\n ghostProb = Math.min(1, ghostProb * 1.4);\n }\n\n for (const pos of positions) {\n const sixteenthInBeat = pos === 'e' ? 1 : 3;\n const posProb = getGhostProbabilityAtPosition(ctx.beat, sixteenthInBeat, leadIns);\n if (!ctx.draw.prob(ghostProb * posProb, 'ghost', ctx.bar, ctx.beat, pos)) {\n continue;\n }\n const variation = ctx.draw.float(0.85, 1.15, 'ghostVelocity', ctx.bar, ctx.beat, pos);\n const ghostBase = getGhostVelocity(ctx.section, ctx.beat / 2);\n let ghostVel = ctx.velocity * ghostBase * variation;\n if (pos === 'a') {\n ghostVel *= 0.9;\n }\n const offset = pos === 'e' ? SIXTEENTH : 3 * SIXTEENTH;\n const tick = swing16(ctx.beatTick + offset, sec, ctx.swingAmount);\n ctx.track.add(GM.SD, tick, SIXTEENTH, ghostVel);\n }\n}\n\n/** Emit the pre-chorus lift buildup for one beat. */\nexport function generatePreChorusBuildup(\n ctx: BeatCtx,\n sec: SectionCtx,\n isSectionLastBar: boolean,\n): void {\n if (sec.style === 'sparse') {\n if (isSectionLastBar && ctx.beat === 3) {\n ctx.track.add(GM.SD, ctx.beatTick, EIGHTH, Math.max(45, ctx.velocity * 0.75));\n ctx.track.add(GM.CRASH, ctx.beatTick + EIGHTH + SIXTEENTH, SIXTEENTH, ctx.velocity * 0.9);\n }\n return;\n }\n const barsInLift = 2;\n const barInLift = ctx.bar - (ctx.sectionBars - barsInLift);\n const progress = (barInLift * 4 + ctx.beat) / (barsInLift * 4);\n const buildupVel = ctx.velocity * (0.5 + 0.5 * progress);\n // The lift is a stream on one voice, so the ceiling decides how fine it may\n // be written: the offbeat goes first, and where even the quarter is out of\n // reach at this tempo the crash carries the phrase end alone.\n const takesEighths = sustainsStrokes(EIGHTH, ctx.bpm, sec.difficulty);\n if (takesEighths || sustainsStrokes(QUARTER, ctx.bpm, sec.difficulty)) {\n ctx.track.add(GM.SD, ctx.beatTick, EIGHTH, buildupVel);\n }\n if (takesEighths) {\n ctx.track.add(\n GM.SD,\n swing16(ctx.beatTick + EIGHTH, sec, ctx.swingAmount),\n EIGHTH,\n buildupVel * 0.85,\n );\n }\n if (isSectionLastBar && ctx.beat === 3) {\n // Through the section's own grid, as the snare above it is: written\n // straight while the rest of the bar swings, the crash lands ahead of the\n // stroke it is meant to land with and reads as a flam.\n ctx.track.add(\n GM.CRASH,\n swing16(ctx.beatTick + EIGHTH + SIXTEENTH, sec, ctx.swingAmount),\n SIXTEENTH,\n ctx.velocity * 1.1,\n );\n }\n}\n\n/**\n * Whether the downbeat of this beat carries an open hi-hat accent.\n *\n * The decision belongs to the beat rather than to the subdivision it is written\n * on, so a finer grid reached by raising the rhythmic dial keeps the open hat\n * the coarser one had: the dial fills strokes in between the ones already\n * sounding instead of changing what they are. A style that opens off the beat\n * takes no part here, so that one beat never carries two open-hat rules.\n */\nfunction opensOnDownbeat(ctx: BeatCtx, sec: SectionCtx): boolean {\n if (usesOffbeatOpenHiHat(sec.style)) {\n return false;\n }\n return shouldAddOpenHHAccent(ctx.section, ctx.beat, ctx.bar, ctx.draw);\n}\n\n/**\n * Whether the \"and\" of this beat carries an open hi-hat, for the styles that\n * write their open hats there. The draw is addressed by the eighth it lands on,\n * so the same \"and\" opens whichever grid the bar is written on.\n */\nfunction opensOffbeat(ctx: BeatCtx, sec: SectionCtx): boolean {\n if (!usesOffbeatOpenHiHat(sec.style) || (ctx.beat !== 1 && ctx.beat !== 3)) {\n return false;\n }\n return ctx.draw.prob(offbeatOpenHiHatChance(ctx.bpm), 'openHat', ctx.bar, ctx.beat, 1);\n}\n\n/** Emit the timekeeping hi-hat (or ride/foot) for one beat. */\nexport function generateHiHatForBeat(ctx: BeatCtx, sec: SectionCtx): void {\n if (!shouldPlayHiHat(sec.role)) {\n return;\n }\n\n const hhInstrument = roleHiHatInstrument(sec.role, sec.useRide);\n const hhType = sectionHiHatType(ctx.section, sec.role);\n const typeMult = hiHatTypeVelocityMultiplier(hhType);\n const allowsOpenHiHat = sec.role !== 'ambient' && sec.role !== 'minimal';\n const isDynamicOpen =\n ctx.barHasOpenHh && ctx.beat === ctx.openHhBeat && !usesOffbeatOpenHiHat(sec.style);\n const dm = sec.densityMult;\n\n if (ctx.hhLevel === 'quarter') {\n // The intro's quarter grid rests between its downbeats. The foot pulse is\n // written for the bar rather than for these beats: a pedal hat here is a\n // stroke the denser grids replace with a stick hat, and the dial would then\n // take a sound away as it was raised.\n if (ctx.section === 'intro' && ctx.beat !== 0) {\n return;\n }\n const hhVel = Math.max(20, ctx.velocity * dm * 0.75 * typeMult);\n if (allowsOpenHiHat && isDynamicOpen) {\n ctx.track.add(GM.OHH, ctx.beatTick, EIGHTH, hhVel + OHH_VEL_BOOST);\n } else if (allowsOpenHiHat && opensOnDownbeat(ctx, sec)) {\n ctx.track.add(hiHatNote('open'), ctx.beatTick, EIGHTH, Math.max(20, hhVel * 1.1));\n } else {\n ctx.track.add(hhInstrument, ctx.beatTick, EIGHTH, hhVel);\n }\n return;\n }\n\n if (ctx.hhLevel === 'eighth') {\n for (let eighth = 0; eighth < 2; eighth += 1) {\n let hhTick = ctx.beatTick + eighth * EIGHTH;\n if (eighth === 1) {\n hhTick = swing16(hhTick, sec, ctx.swingAmount);\n }\n if (ctx.section === 'intro' && eighth === 1) {\n if (sec.useFootHh && ctx.beat % 2 === 0) {\n ctx.track.add(GM.FHH, hhTick, EIGHTH, footHiHatVelocity(ctx.draw, ctx.bar, ctx.beat));\n }\n continue;\n }\n const hhVel = Math.max(20, ctx.velocity * dm * typeMult * (eighth === 0 ? 0.9 : 0.65));\n if (isDynamicOpen && allowsOpenHiHat && eighth === 0) {\n ctx.track.add(GM.OHH, hhTick, EIGHTH, hhVel + OHH_VEL_BOOST);\n continue;\n }\n const useOpen = eighth === 0 ? opensOnDownbeat(ctx, sec) : opensOffbeat(ctx, sec);\n if (useOpen && allowsOpenHiHat) {\n ctx.track.add(hiHatNote('open'), hhTick, EIGHTH, Math.max(20, hhVel * 1.1));\n } else {\n ctx.track.add(hhInstrument, hhTick, EIGHTH / 2, hhVel);\n }\n }\n return;\n }\n\n for (let sixteenth = 0; sixteenth < 4; sixteenth += 1) {\n let hhTick = ctx.beatTick + sixteenth * SIXTEENTH;\n // Every off-beat 16th goes through the shared grid, the \"and\" included:\n // leaving position 2 straight put the hat and the kick's \"and\" on different\n // ticks in the same bar.\n if (sixteenth !== 0) {\n hhTick = swing16(hhTick, sec, ctx.swingAmount);\n }\n const metricVel = hiHatVelocityMultiplier(\n sixteenth,\n ctx.draw.float(0.95, 1.05, 'hatVelocity', ctx.bar, ctx.beat, sixteenth),\n );\n const hhVel = Math.max(20, ctx.velocity * dm * typeMult * metricVel);\n if (isDynamicOpen && allowsOpenHiHat && sixteenth === 0) {\n ctx.track.add(GM.OHH, hhTick, SIXTEENTH, hhVel + OHH_VEL_BOOST);\n continue;\n }\n // The accents of the coarser grids are taken here as well, at the positions\n // those grids write them on: the beat itself, and the \"and\" for the styles\n // that open there. Deciding them only on the eighth grid meant reaching the\n // sixteenths turned an open hat back into a closed one.\n if (allowsOpenHiHat && sixteenth === 0 && opensOnDownbeat(ctx, sec)) {\n ctx.track.add(hiHatNote('open'), hhTick, SIXTEENTH, Math.max(20, hhVel * 1.1));\n continue;\n }\n if (allowsOpenHiHat && sixteenth === 2 && opensOffbeat(ctx, sec)) {\n ctx.track.add(hiHatNote('open'), hhTick, SIXTEENTH, Math.max(20, hhVel * 1.1));\n continue;\n }\n if (allowsOpenHiHat && ctx.beat === 3 && sixteenth === 3) {\n const openProb = Math.max(0.1, Math.min(0.4, 30 / ctx.bpm));\n if (ctx.draw.prob(openProb, 'openHat', ctx.bar, ctx.beat, sixteenth)) {\n ctx.track.add(GM.OHH, hhTick, SIXTEENTH, Math.max(20, hhVel * 1.2));\n continue;\n }\n }\n ctx.track.add(hhInstrument, hhTick, SIXTEENTH / 2, hhVel);\n }\n}\n","/**\n * Generate a euclidean onset pattern (Bjorklund's algorithm), distributing\n * `pulses` as evenly as possible across `steps`, then rotating.\n *\n * @param pulses Number of onsets, clamped to [0, steps].\n * @param steps Total steps (>= 1).\n * @param rotation Steps to rotate onsets toward later positions.\n * @returns One boolean per step; true marks an onset.\n */\nimport {\n assertGenerationBudget,\n assertInteger,\n assertPositiveInt,\n} from '../../core/validation/index.js';\n\nexport function euclideanRhythm(pulses: number, steps: number, rotation = 0): boolean[] {\n assertPositiveInt(steps, 'euclidean steps');\n assertInteger(pulses, 'euclidean pulses');\n assertInteger(rotation, 'euclidean rotation');\n assertGenerationBudget(steps, 'euclidean pattern steps');\n const p = Math.max(0, Math.min(pulses, steps));\n const base = bjorklund(p, steps);\n const shift = ((rotation % steps) + steps) % steps;\n if (shift === 0) {\n return base;\n }\n return base.map((_, i) => base[(i - shift + steps) % steps] ?? false);\n}\n\nfunction bjorklund(pulses: number, steps: number): boolean[] {\n if (pulses === 0) {\n return Array.from({ length: steps }, () => false);\n }\n if (pulses >= steps) {\n return Array.from({ length: steps }, () => true);\n }\n let head: boolean[][] = Array.from({ length: pulses }, () => [true]);\n let remainder: boolean[][] = Array.from({ length: steps - pulses }, () => [false]);\n while (remainder.length > 1) {\n const count = Math.min(head.length, remainder.length);\n const next: boolean[][] = [];\n for (let i = 0; i < count; i += 1) {\n next.push([...(head[i] ?? []), ...(remainder[i] ?? [])]);\n }\n remainder = head.length > count ? head.slice(count) : remainder.slice(count);\n head = next;\n }\n return [...head.flat(), ...remainder.flat()];\n}\n","/**\n * The fill vocabulary, as data.\n *\n * Every archetype is a list of strokes on the bar's grid and every selection is\n * a table indexed by a draw, so a fill can be added, replaced, or supplied by a\n * caller without touching a generator. The generator's part is to look one up.\n */\n\nimport type { Articulation } from '../../core/instrument/index.js';\nimport { type Draw, sustainsStrokes } from '../context/index.js';\nimport { deepFreeze } from '../vocabulary/freeze.js';\nimport type { HitList } from './hit.js';\nimport type { DrumStyle, SectionEnergy, SectionType } from './internal.js';\nimport { BACKBEAT_LIFT, EIGHTH, FILL_ACCENT_LIFT, GM, SIXTEENTH } from './internal.js';\n\n/** The thirteen fill archetypes for section transitions, in declaration order. */\nexport const FILL_TYPES = Object.freeze([\n 'snareRoll',\n 'tomDescend',\n 'tomAscend',\n 'snareTomCombo',\n 'simpleCrash',\n 'linearFill',\n 'ghostToAccent',\n 'bdSnareAlternate',\n 'hiHatChoke',\n 'tomShuffle',\n 'breakdownFill',\n 'flamsAndDrags',\n 'halfTimeFill',\n] as const);\n\n/** The thirteen fill archetypes for section transitions. */\nexport type FillType = (typeof FILL_TYPES)[number];\n\n/**\n * How loud one stroke of a fill is.\n *\n * A fill's dynamics are written against the fill's own base velocity rather\n * than in absolute terms, so the same archetype sounds right in a verse and a\n * chorus: `fill` is the body of the fill, `accent` the stroke that lands. One\n * base for the whole gesture, not one per beat — a crescendo that restarts its\n * arithmetic at every beat line falls back in the middle of itself.\n */\nexport type FillVelocity = {\n base: 'fill' | 'accent';\n /** Factor on the base, for a crescendo or a ghosted stroke. */\n scale?: number;\n /** Fixed offset in MIDI velocity, for a stroke a little above or below. */\n offset?: number;\n};\n\n/** One stroke of a fill, positioned within the beat it belongs to. */\nexport type FillStroke = {\n /** General MIDI note number of the voice struck. */\n voice: number;\n /** Offset from the start of the beat, in quarter-note beats. */\n offset: number;\n /** Sounding length, in quarter-note beats. */\n duration: number;\n velocity: FillVelocity;\n /** How the stroke is played, when it is more than a plain hit. */\n articulation?: Articulation;\n};\n\n/**\n * A fill archetype: what each beat of the fill bar plays.\n *\n * Beats are indexed from the start of the bar. A beat with no entry is silent,\n * which is how the sparse archetypes leave the earlier beats to the groove.\n */\nexport type FillArchetype = {\n /** Strokes per beat of the bar, indexed 0..3. */\n atBeat: readonly (readonly FillStroke[])[];\n /**\n * Whether the archetype stops where its own strokes stop, rather than holding\n * its last beat over the beats of the bar it does not write.\n *\n * It answers for an archetype shorter than the bar it is placed in, so the\n * built-in archetypes — written beat by beat against the four-beat bar\n * {@link generateDrums} accepts — are unaffected by it; a caller's archetype\n * that writes fewer beats is where it decides anything. Most figures read\n * naturally when held, but a half-time gesture is bar-long and stops there.\n */\n boundToBar?: boolean;\n};\n\nconst S = SIXTEENTH;\nconst E = EIGHTH;\n\n/** Body velocity with an optional factor and offset. */\nfunction fill(scale?: number, offset?: number): FillVelocity {\n return {\n base: 'fill',\n ...(scale === undefined ? {} : { scale }),\n ...(offset === undefined ? {} : { offset }),\n };\n}\n\n/** Accent velocity: the stroke the fill lands on. */\nconst accent: FillVelocity = { base: 'accent' };\n\n/** One stroke. */\nfunction at(\n voice: number,\n offset: number,\n duration: number,\n velocity: FillVelocity,\n articulation?: Articulation,\n): FillStroke {\n return { voice, offset, duration, velocity, ...(articulation ? { articulation } : {}) };\n}\n\n/**\n * The pickup beats every archetype shares: a fill that starts at beat 2 is\n * approached the same way whichever archetype follows.\n */\nconst PICKUP_BEAT_0: FillStroke[] = [at(GM.BD, 0, E, fill()), at(GM.SD, E, E, fill(undefined, -5))];\nconst PICKUP_BEAT_1: FillStroke[] = [\n at(GM.SD, 0, E, fill()),\n at(GM.TOM_H, E, E, fill(undefined, -3)),\n];\n\n/** A crescendo of `count` snare sixteenths starting from `from`. */\nfunction snareCrescendo(count: number, from: number): FillStroke[] {\n return Array.from({ length: count }, (_, i) => at(GM.SD, i * S, S, fill(from + 0.1 * i)));\n}\n\n/** An accelerating ghost-to-accent run of four snare sixteenths. */\nfunction ghostRun(): FillStroke[] {\n return Array.from({ length: 4 }, (_, i) => at(GM.SD, i * S, S, fill(0.4, i * 10)));\n}\n\n/** Build an archetype from its beat-2 and beat-3 strokes. */\nfunction archetype(\n beat2: readonly FillStroke[],\n beat3: readonly FillStroke[],\n boundToBar?: boolean,\n): FillArchetype {\n return {\n atBeat: [PICKUP_BEAT_0, PICKUP_BEAT_1, beat2, beat3],\n ...(boundToBar ? { boundToBar } : {}),\n };\n}\n\n/**\n * The fill dictionary.\n *\n * Provenance: every archetype here is a rudiment or a stock phrase-end gesture\n * — a snare crescendo, a descending tom run, a crash on the last off-beat — of\n * the kind every drummer plays and nobody owns. None reproduces a fill from a\n * particular recording.\n */\nexport const FILL_ARCHETYPES: Readonly<Record<FillType, FillArchetype>> = deepFreeze({\n // One crescendo of seven strokes across the two beats, not two of four and\n // three: the roll is a single gesture and its second half continues where its\n // first half left off.\n snareRoll: archetype(snareCrescendo(4, 0.6), [\n ...snareCrescendo(3, 1.0),\n at(GM.SD, 3 * S, S, accent),\n ]),\n tomDescend: archetype(\n [at(GM.SD, 0, E, fill()), at(GM.TOM_H, E, E, fill(undefined, -5))],\n [\n at(GM.TOM_H, 0, S, fill()),\n at(GM.TOM_M, S, S, fill(undefined, -3)),\n at(GM.TOM_M, E, S, fill(undefined, -5)),\n at(GM.TOM_L, E + S, S, accent),\n ],\n ),\n tomAscend: archetype(\n [at(GM.SD, 0, E, fill()), at(GM.TOM_L, E, E, fill(undefined, -5))],\n [\n at(GM.TOM_L, 0, S, fill()),\n at(GM.TOM_M, S, S, fill(undefined, 3)),\n at(GM.TOM_M, E, S, fill(undefined, 5)),\n at(GM.TOM_H, E + S, S, accent),\n ],\n ),\n snareTomCombo: archetype(\n [at(GM.SD, 0, E, fill()), at(GM.SD, E, S, fill(undefined, -5)), at(GM.TOM_H, E + S, S, fill())],\n [\n at(GM.TOM_M, 0, S, fill()),\n at(GM.SD, S, S, fill(undefined, -3)),\n at(GM.TOM_L, E, S, fill(undefined, 2)),\n at(GM.BD, E + S, S, accent),\n ],\n ),\n simpleCrash: archetype([], [at(GM.BD, E + S, S, accent), at(GM.CRASH, E + S, E, accent)]),\n linearFill: archetype(\n [\n at(GM.BD, 0, S, fill()),\n at(GM.SD, S, S, fill()),\n at(GM.TOM_H, 2 * S, S, fill()),\n at(GM.TOM_M, 3 * S, S, fill()),\n ],\n [\n at(GM.TOM_L, 0, S, fill(undefined, 3)),\n at(GM.SD, S, S, fill(undefined, 5)),\n at(GM.BD, 2 * S, S, fill(undefined, 7)),\n at(GM.SD, 3 * S, S, accent),\n ],\n ),\n ghostToAccent: archetype(ghostRun(), [at(GM.SD, 0, E, fill()), at(GM.SD, E, E, accent)]),\n bdSnareAlternate: archetype(\n [\n at(GM.BD, 0, S, fill()),\n at(GM.SD, S, S, fill()),\n at(GM.BD, 2 * S, S, fill(undefined, 3)),\n at(GM.SD, 3 * S, S, fill(undefined, 3)),\n ],\n [\n at(GM.BD, 0, S, fill(undefined, 5)),\n at(GM.SD, S, S, fill(undefined, 5)),\n at(GM.BD, 2 * S, S, accent),\n at(GM.SD, 3 * S, S, accent),\n ],\n ),\n hiHatChoke: archetype(\n [at(GM.OHH, 0, E, fill()), at(GM.OHH, E, E, fill(undefined, 5))],\n [at(GM.OHH, 0, S, fill(undefined, 8)), at(GM.CHH, S, S, accent), at(GM.SD, E, E, accent)],\n ),\n tomShuffle: archetype(\n [at(GM.TOM_H, 0, E, fill()), at(GM.TOM_M, E + S / 2, S, fill(undefined, -5))],\n [at(GM.TOM_M, 0, E, fill()), at(GM.TOM_L, E + S / 2, S, fill(undefined, 5))],\n ),\n breakdownFill: archetype([], [at(GM.SD, E, S, accent)]),\n flamsAndDrags: archetype(\n // The ornaments are attributes of the principal stroke, not extra onsets:\n // one flammed note rather than a grace note written a 64th early, one\n // dragged note rather than a pair of soft strokes before it.\n [at(GM.SD, 0, E, fill(), 'flam'), at(GM.SD, E + S, E, fill(), 'drag')],\n [at(GM.SD, 0, 1, accent, 'flam')],\n ),\n halfTimeFill: archetype(\n [at(GM.SD, 0, 1, accent), at(GM.BD, 0, 1, fill())],\n // At low energy the fill spans only beat 3, so this beat keeps the phrase\n // end from going silent: a broad half-time backbeat snare with a light\n // pickup into the next section.\n [at(GM.SD, 0, EIGHTH, accent), at(GM.SD, E, S, fill(0.6)), at(GM.SD, E + S, S, accent)],\n true,\n ),\n});\n\n/**\n * Whether a caller's material is a fill.\n *\n * A context carries one dictionary for the whole piece, so each generator has\n * to recognise its own material; a bass lick reaching the fill selector would\n * otherwise be rendered as silence.\n */\nexport function isFillArchetype(material: unknown): material is FillArchetype {\n return (\n typeof material === 'object' &&\n material !== null &&\n Array.isArray((material as FillArchetype).atBeat)\n );\n}\n\n/**\n * The archetype an id names.\n *\n * @param id Archetype name, built-in or supplied.\n * @param supplied The caller's archetypes, keyed by id.\n * @returns The archetype, or undefined when nothing carries that name.\n */\nexport function fillArchetypeFor(\n id: string,\n supplied?: ReadonlyMap<string, FillArchetype>,\n): FillArchetype | undefined {\n return supplied?.get(id) ?? FILL_ARCHETYPES[id as FillType];\n}\n\n/**\n * Beat at which a fill begins, widening with section energy.\n *\n * One beat per step of energy, so every beat an archetype writes is played by\n * some transition: a peak takes the bar from its downbeat, pickup included, and\n * a low-energy phrase end is the last beat alone.\n */\nexport function getFillStartBeat(energy: SectionEnergy): number {\n switch (energy) {\n case 'low':\n return 3;\n case 'medium':\n return 2;\n case 'high':\n return 1;\n case 'peak':\n return 0;\n }\n}\n\n/**\n * The archetype offered at each draw, per transition context.\n *\n * A table is read by index, so an archetype named twice is simply twice as\n * likely; that is the whole weighting mechanism, and it is visible in the data\n * rather than buried in the branches of a switch.\n */\ntype FillTable = readonly FillType[];\n\n/** Sparse styles keep the phrase end small whatever the transition. */\nconst SPARSE_FILLS: FillTable = ['simpleCrash', 'breakdownFill'];\n\n/** Dropping into a low-energy section: keep the phrase end gentle. */\nconst LOW_ENERGY_FILLS: FillTable = ['simpleCrash', 'breakdownFill', 'halfTimeFill'];\n\n/** Big lead-ins to a chorus, from a high-energy style. */\nconst TO_CHORUS_HIGH_FILLS: FillTable = [\n 'tomDescend',\n 'tomDescend',\n 'snareRoll',\n 'linearFill',\n 'bdSnareAlternate',\n 'flamsAndDrags',\n 'tomShuffle',\n 'ghostToAccent',\n];\n\n/** Lead-ins to a chorus from the remaining styles. */\nconst TO_CHORUS_FILLS: FillTable = [\n 'snareTomCombo',\n 'snareTomCombo',\n 'tomDescend',\n 'ghostToAccent',\n 'hiHatChoke',\n 'linearFill',\n 'snareRoll',\n 'snareRoll',\n];\n\n/** Leaving the intro: lighter, building character. */\nconst FROM_INTRO_FILLS: FillTable = [\n 'snareRoll',\n 'snareRoll',\n 'simpleCrash',\n 'ghostToAccent',\n 'breakdownFill',\n 'halfTimeFill',\n];\n\n/** Generic medium/high/peak fills for a high-energy style. */\nconst GENERIC_HIGH_FILLS: FillTable = [\n 'tomDescend',\n 'snareRoll',\n 'tomAscend',\n 'snareTomCombo',\n 'linearFill',\n 'bdSnareAlternate',\n 'flamsAndDrags',\n 'tomShuffle',\n];\n\n/** Generic medium/high/peak fills for the remaining styles. */\nconst GENERIC_FILLS: FillTable = [\n 'snareRoll',\n 'snareRoll',\n 'snareTomCombo',\n 'ghostToAccent',\n 'hiHatChoke',\n 'halfTimeFill',\n 'breakdownFill',\n 'breakdownFill',\n];\n\n/**\n * The table a transition reads from.\n *\n * The transition-specific tables (into a chorus, out of an intro) are checked\n * before the generic energy tables so every archetype stays reachable; with\n * `from === to` the caller effectively asks for a within-section fill.\n */\nfunction fillTableFor(\n from: SectionType,\n to: SectionType,\n style: DrumStyle,\n nextEnergy: SectionEnergy,\n): FillTable {\n if (style === 'sparse') {\n return SPARSE_FILLS;\n }\n const highEnergy = style === 'rock' || style === 'fourOnFloor';\n if (nextEnergy === 'low') {\n return LOW_ENERGY_FILLS;\n }\n if (to === 'chorus') {\n return highEnergy ? TO_CHORUS_HIGH_FILLS : TO_CHORUS_FILLS;\n }\n if (from === 'intro') {\n return FROM_INTRO_FILLS;\n }\n return highEnergy ? GENERIC_HIGH_FILLS : GENERIC_FILLS;\n}\n\n/**\n * Pick a fill archetype for a section transition.\n *\n * @param from Section the fill leaves.\n * @param to Section the fill leads into.\n * @param style Internal drum style.\n * @param nextEnergy Energy of the section the fill leads into.\n * @param draw Position-addressed draws for the fill.\n * @param bar Bar the fill occupies; the choice is addressed by it, so a fill\n * elsewhere in the piece is unaffected by this one.\n * @param extra Ids a caller's dictionary contributes for this transition. They\n * are offered after the built-in table, so the built-in choices keep the draws\n * they had and a caller adds to the vocabulary rather than displacing it.\n * @param accepts Which archetypes may be drawn at all. A fill beyond the\n * difficulty ceiling is taken out of the table and the draw runs over what\n * remains, rather than being simplified into a fill nobody wrote.\n * @returns The id drawn, or undefined when the table has nothing this\n * transition accepts.\n */\nexport function selectFillType(\n from: SectionType,\n to: SectionType,\n style: DrumStyle,\n nextEnergy: SectionEnergy,\n draw: Draw,\n bar: number,\n extra: readonly string[] = [],\n accepts: (id: string) => boolean = () => true,\n): string | undefined {\n const table = fillTableFor(from, to, style, nextEnergy);\n // An id a built-in already carries is a replacement, not an addition: the\n // material is resolved through the caller's map wherever the table names it,\n // so appending it as well would give that one fill two ways of being drawn\n // and a weight no other entry has.\n const added = extra.filter((id) => FILL_ARCHETYPES[id as FillType] === undefined);\n const offered = added.length === 0 ? table : [...table, ...added];\n const candidates = offered.filter(accepts);\n if (candidates.length === 0) {\n return undefined;\n }\n return candidates[draw.range(0, candidates.length - 1, 'fill', bar)] ?? candidates[0];\n}\n\n/**\n * The strokes a fill plays on one beat of the fill bar.\n *\n * @param shape The archetype.\n * @param beat Beat index within the bar.\n * @returns The strokes, empty where the archetype leaves the beat to the groove.\n */\nfunction strokesAtBeat(shape: FillArchetype, beat: number): readonly FillStroke[] {\n const lastBeat = shape.atBeat.length - 1;\n const index = beat > lastBeat ? (shape.boundToBar ? -1 : lastBeat) : beat;\n return shape.atBeat[index] ?? [];\n}\n\n/**\n * Whether a fill is inside the difficulty ceiling at this tempo.\n *\n * The fill is measured as it will actually be played — from the beat it starts\n * on to the end of the bar — and voice by voice, since what stops a player is\n * one limb repeating faster than it can rather than the bar's total count. The\n * phrase end is the most exposed bar there is, so a ceiling that governs the\n * groove and not the fill governs nothing a listener would notice.\n *\n * @param shape The archetype.\n * @param fromBeat Beat the fill starts on.\n * @param barBeats Beats in the bar.\n * @param bpm Tempo, or undefined when the caller gave none.\n * @param difficulty The ceiling, or undefined for no ceiling.\n * @returns True when the fill is playable at the ceiling, and whenever either\n * the ceiling or the tempo is unknown.\n */\nexport function fillWithinCeiling(\n shape: FillArchetype,\n fromBeat: number,\n barBeats: number,\n bpm: number | undefined,\n difficulty: number | undefined,\n): boolean {\n if (difficulty === undefined || bpm === undefined) {\n return true;\n }\n const byVoice = new Map<number, number[]>();\n for (let beat = fromBeat; beat < barBeats; beat += 1) {\n for (const stroke of strokesAtBeat(shape, beat)) {\n const positions = byVoice.get(stroke.voice);\n if (positions) {\n positions.push(beat + stroke.offset);\n } else {\n byVoice.set(stroke.voice, [beat + stroke.offset]);\n }\n }\n }\n for (const positions of byVoice.values()) {\n positions.sort((a, b) => a - b);\n for (let i = 1; i < positions.length; i += 1) {\n const gap = (positions[i] ?? 0) - (positions[i - 1] ?? 0);\n if (gap > 0 && !sustainsStrokes(gap, bpm, difficulty)) {\n return false;\n }\n }\n }\n return true;\n}\n\n/**\n * Emit a fill for a single beat of the fill bar.\n *\n * @param track Hit accumulator.\n * @param beatTick Absolute beat position of this beat.\n * @param beat Beat index within the bar (0-3).\n * @param fillType Selected archetype, by name or as material of its own.\n * @param velocity Base velocity for the beat.\n * @param place Where a position inside the beat is written, which is the\n * section's own grid: a fill written straight through a bar the rest of the\n * kit swings puts its offbeats ahead of every other voice, and two voices a\n * sixteenth's swing apart in one bar are heard as a flam.\n */\nexport function generateFill(\n track: HitList,\n beatTick: number,\n beat: number,\n fillType: FillType | FillArchetype,\n velocity: number,\n place: (tick: number) => number = (tick) => tick,\n): void {\n const shape =\n typeof fillType === 'string' ? FILL_ARCHETYPES[fillType] : (fillType as FillArchetype);\n if (!shape) {\n return;\n }\n const strokes = strokesAtBeat(shape, beat);\n const fillVel = velocity * 0.9;\n // The stroke a fill lands on is the loudest thing in the bar. Reading it as a\n // fraction of the beat velocity put it below the strokes leading into it and\n // some twenty units under the backbeat next to it, which is a phrase end\n // heard as a hole.\n const accentVel = velocity + BACKBEAT_LIFT + FILL_ACCENT_LIFT;\n for (const stroke of strokes) {\n const base = stroke.velocity.base === 'accent' ? accentVel : fillVel;\n const scaled = stroke.velocity.scale === undefined ? base : base * stroke.velocity.scale;\n track.add(\n stroke.voice,\n place(beatTick + stroke.offset),\n stroke.duration,\n scaled + (stroke.velocity.offset ?? 0),\n stroke.articulation,\n );\n }\n}\n","import type { Articulation } from '../../core/instrument/index.js';\nimport { assertMidiPitch } from '../../core/validation/index.js';\nimport { clampVel, GM } from './internal.js';\n\n/**\n * The percussion voices the generator emits, as General MIDI note numbers.\n *\n * A {@link DrumHit} carries only its pitch, so splitting a kit across tracks —\n * kick to one, snare to another — means comparing against these numbers. They\n * are published so that comparison is a named constant covered by semver\n * rather than a hard-coded 36.\n *\n * @example\n * ```ts\n * import { DRUM_NOTES, generateDrums } from '@libraz/libcantus';\n * const hits = generateDrums({\n * bars: 1,\n * style: 'standard',\n * section: 'verse',\n * ctx: { bpm: 120, complexity: { rhythmic: 0.5 } },\n * });\n * const kick = hits.filter((hit) => hit.pitch === DRUM_NOTES.kick);\n * ```\n * @category Composition\n */\nexport const DRUM_NOTES = Object.freeze({\n kick: GM.BD,\n snare: GM.SD,\n sideStick: GM.SIDESTICK,\n handClap: GM.HANDCLAP,\n closedHiHat: GM.CHH,\n pedalHiHat: GM.FHH,\n openHiHat: GM.OHH,\n crash: GM.CRASH,\n ride: GM.RIDE,\n tambourine: GM.TAMBOURINE,\n highTom: GM.TOM_H,\n midTom: GM.TOM_M,\n lowTom: GM.TOM_L,\n shaker: GM.SHAKER,\n} as const);\n\n/**\n * A percussion voice name, one key of {@link DRUM_NOTES}.\n *\n * @category Composition\n */\nexport type DrumVoice = keyof typeof DRUM_NOTES;\n\n/** Voice name for each emitted note number, for {@link drumVoiceOf}. */\nconst VOICE_BY_NOTE = new Map<number, DrumVoice>(\n (Object.entries(DRUM_NOTES) as [DrumVoice, number][]).map(([voice, note]) => [note, voice]),\n);\n\n/**\n * The note number a voice name stands for.\n *\n * Read through `Object.hasOwn` rather than by indexing the table: an inherited\n * name — `'constructor'`, `'toString'` — indexes to a function, and a figure\n * naming one of them put that function where a hit's pitch goes.\n *\n * @param voice The voice name, from a figure that may be a caller's own.\n * @returns The General MIDI note number, or undefined for a name this\n * generator has no voice for.\n */\nexport function drumNoteOf(voice: string): number | undefined {\n return Object.hasOwn(DRUM_NOTES, voice) ? DRUM_NOTES[voice as DrumVoice] : undefined;\n}\n\n/**\n * The voice a percussion note number names.\n *\n * @param pitch The General MIDI note number.\n * @returns The voice, or undefined for a note this generator never emits.\n * @category Composition\n */\nexport function drumVoiceOf(pitch: number): DrumVoice | undefined {\n // Undefined means \"a note this generator never emits\", which is a statement\n // about a note; a value that is no note at all is refused instead, so the two\n // do not come back as the same answer.\n return VOICE_BY_NOTE.get(assertMidiPitch(pitch));\n}\n\n/**\n * A single percussion onset.\n *\n * @category Composition\n */\nexport type DrumHit = {\n /** General MIDI note number; {@link drumVoiceOf} names it. */\n pitch: number;\n startBeat: number;\n durationBeat: number;\n velocity: number;\n /**\n * How the stroke is played, when it is more than a plain hit. A flam is one\n * onset carrying `'flam'`, not a pair of notes a fraction of a beat apart, so\n * the grace stroke is the reader's to place — as a second note, a key switch,\n * or a sample layer — and any pattern can carry one.\n */\n articulation?: Articulation;\n};\n\n/** Rounding used to key onsets by beat: a 128th note, finer than any grid used. */\nconst ONSET_KEY_SCALE = 128;\n\n/**\n * Accumulator for drum onsets with velocity clamping and simple lookups.\n *\n * Onsets are indexed by (pitch, quantized beat) as they are added. The lookups\n * run inside the per-bar loop, so scanning the accumulated hits made the whole\n * generator quadratic in bar count — a few thousand bars took seconds.\n */\nexport class HitList {\n readonly hits: DrumHit[] = [];\n /** Voices already struck at each quantized beat. */\n readonly #onsets = new Map<number, Set<number>>();\n\n /**\n * Append a hit, clamping velocity and dropping non-positive positions.\n *\n * A voice already struck at this instant is left as it stands: one drum\n * sounds once however many voices ask for it, and a second entry at the same\n * pitch and position is a note-on a reader answers with no note-off, heard as\n * a hung or cut stroke rather than as a louder one.\n */\n add(\n pitch: number,\n startBeat: number,\n durationBeat: number,\n velocity: number,\n articulation?: Articulation,\n ): void {\n const beat = Math.max(0, startBeat);\n if (this.hasOnset(pitch, beat)) {\n return;\n }\n const hit: DrumHit = {\n pitch,\n startBeat: beat,\n durationBeat,\n velocity: clampVel(velocity),\n };\n // Left off entirely rather than set to undefined, so a plain stroke stays\n // deep-equal to the same stroke written before articulations existed.\n if (articulation !== undefined) {\n hit.articulation = articulation;\n }\n this.hits.push(hit);\n const struck = this.#onsets.get(beatSlot(beat));\n if (struck) {\n struck.add(pitch);\n } else {\n this.#onsets.set(beatSlot(beat), new Set([pitch]));\n }\n }\n\n /** Whether a hit of this pitch already starts exactly at `startBeat`. */\n hasOnset(pitch: number, startBeat: number): boolean {\n return this.#onsets.get(beatSlot(startBeat))?.has(pitch) ?? false;\n }\n\n /**\n * True when a crash already sits less than a 16th from `startBeat`, on either\n * side of it.\n */\n hasCrashNear(startBeat: number): boolean {\n // The window is a 16th either way, and every grid in use is at least a\n // 128th, so it is enough to probe the quantized positions inside it.\n const steps = Math.round(0.25 * ONSET_KEY_SCALE);\n for (let step = -steps + 1; step < steps; step += 1) {\n if (this.hasOnset(GM.CRASH, startBeat + step / ONSET_KEY_SCALE)) {\n return true;\n }\n }\n return false;\n }\n}\n\n/** The quantized instant an onset belongs to. */\nfunction beatSlot(startBeat: number): number {\n return Math.round(startBeat * ONSET_KEY_SCALE);\n}\n\n/**\n * Key an onset by pitch and quantized beat.\n *\n * Shared so a generator can name onsets of its own — the strokes a caller asked\n * for exactly, which the difficulty ceiling leaves alone — in the same terms the\n * accumulator indexes them by.\n */\nexport function onsetKey(pitch: number, startBeat: number): string {\n return `${pitch}:${Math.round(startBeat * ONSET_KEY_SCALE)}`;\n}\n","/**\n * The kick vocabulary, as data on a sixteenth grid.\n *\n * The grid is sixteenths rather than eighths because most of what belongs in a\n * kick dictionary cannot be written on eighths at all: the funk and modern-pop\n * basics live on the sixteenths between the beats. The eighth-note patterns map\n * onto the even steps of the same grid, so they read exactly as before.\n */\n\nimport type { Draw } from '../context/index.js';\nimport { deepFreeze } from '../vocabulary/freeze.js';\nimport { BAR_STEPS, BEAT_STEPS } from '../vocabulary/transform.js';\nimport {\n type DrumStyle,\n leanedBy,\n mapSection,\n type Section,\n type SectionType,\n} from './internal.js';\n\n/** Sixteenth steps in one bar of 4/4; the grid a kick pattern is written on. */\nexport const KICK_STEPS = BAR_STEPS;\n\n/**\n * Kick presence flags across the sixteen 16th-note slots of a bar.\n *\n * Index `n` is the `n`-th sixteenth from the downbeat, so beat 3 is index 8 and\n * the \"and\" of 2 is index 6.\n */\nexport type KickPattern = readonly boolean[];\n\n/** The sixteenth index of a beat's downbeat. */\nfunction beatStep(beat: number): number {\n return beat * BEAT_STEPS;\n}\n\n/**\n * One onset of a kick figure.\n *\n * A slot with no probability is part of the figure itself and always sounds.\n * One with a probability is where the rhythmic dial reaches the kick: the draw\n * is fixed by position and leaned on by the dial, so raising the dial fills\n * slots in without emptying any that were already there.\n */\nexport type KickSlot = {\n /** Sixteenth index within the bar, 0..15. */\n step: number;\n /** Chance the slot is taken before the dial leans on it. */\n probability?: number;\n /** Draw address for the slot; slots that may sound must name one. */\n slot?: string;\n /** Restrict the slot to bars of one parity, for a two-bar figure. */\n barParity?: 0 | 1;\n /** Sections the slot is added in; absent means every section. */\n sections?: Section[];\n};\n\n/** A style's kick figure: the slots it always plays, plus the ones it may. */\nexport type KickFigure = readonly KickSlot[];\n\nconst BEAT_1 = beatStep(0);\nconst BEAT_2 = beatStep(1);\nconst BEAT_3 = beatStep(2);\nconst BEAT_4 = beatStep(3);\nconst AND_2 = beatStep(1) + 2;\nconst AND_4 = beatStep(3) + 2;\n/** The last sixteenth of beat three, which pushes into the fourth. */\nconst A_OF_3 = beatStep(2) + 3;\n\n/**\n * The kick under every phrase end: two half notes, so the outro stops pushing.\n *\n * A style's own unconditional onsets are played over it — a four-on-the-floor\n * dance kick and a clave-leaning latin figure are those grooves, and an outro\n * that dropped them would end the piece in a style it was never in — while the\n * slots the dials may fill are left out, which is what keeps the phrase end\n * from building.\n */\nconst OUTRO_FIGURE: KickFigure = [{ step: BEAT_1 }, { step: BEAT_3 }];\n\n/** The onsets a figure states outright, with none of the slots a dial fills. */\nfunction statedSlots(figure: KickFigure): KickSlot[] {\n return figure.filter(\n (slot) =>\n slot.probability === undefined && slot.sections === undefined && slot.barParity === undefined,\n );\n}\n\n/** The figure a style with no entry of its own plays. */\nconst DEFAULT_FIGURE: KickFigure = [\n { step: BEAT_1 },\n { step: BEAT_3 },\n { step: AND_2, probability: 0.5, slot: 'beat2and', sections: ['prechorus'] },\n { step: AND_2, probability: 0.55, slot: 'beat2and', sections: ['chorus'] },\n { step: AND_4, probability: 0.35, slot: 'beat4and', sections: ['chorus'] },\n];\n\n/**\n * The kick dictionary, keyed by internal style.\n *\n * Provenance: each figure is the plain pulse of its style — the four-on-the-\n * floor dance kick, the backbeat-answering \"and\" of two, the clave-leaning\n * latin figure — none of them traceable to a particular record.\n */\nexport const KICK_FIGURES: Readonly<Record<DrumStyle, KickFigure>> = deepFreeze({\n sparse: [{ step: BEAT_1 }, { step: BEAT_3, sections: ['chorus'], barParity: 1 }],\n fourOnFloor: [\n { step: BEAT_1 },\n { step: BEAT_2 },\n { step: BEAT_3 },\n { step: BEAT_4 },\n { step: AND_2, probability: 0.2, slot: 'beat2and', sections: ['chorus'] },\n ],\n upbeat: [\n { step: BEAT_1 },\n { step: BEAT_3 },\n { step: AND_2, probability: 0.7, slot: 'beat2and', sections: ['prechorus', 'chorus'] },\n { step: AND_4, probability: 0.6, slot: 'beat4and', sections: ['chorus'] },\n ],\n rock: [\n { step: BEAT_1 },\n { step: BEAT_3 },\n // The broken beat: the \"and\" of two answers the snare and the \"a\" of three\n // pushes into the fourth, in every section. That displacement is the figure\n // rather than a slot a dial may or may not fill in a chorus, which is what\n // separates it from the plain pop pulse wherever it is played.\n { step: AND_2 },\n { step: A_OF_3 },\n { step: AND_4, probability: 0.4, slot: 'beat4and', sections: ['chorus'] },\n ],\n synth: [\n { step: BEAT_1 },\n { step: BEAT_3 },\n { step: AND_2, probability: 0.75, slot: 'beat2and', sections: ['prechorus', 'chorus'] },\n { step: AND_4, probability: 0.65, slot: 'beat4and', sections: ['chorus'] },\n ],\n trap: [\n { step: BEAT_1 },\n { step: AND_2, probability: 0.8, slot: 'beat2and' },\n { step: BEAT_3, probability: 0.3, slot: 'beat3' },\n { step: AND_4, probability: 0.7, slot: 'beat4and' },\n ],\n latin: [\n { step: BEAT_1 },\n { step: AND_2 },\n { step: BEAT_3 },\n { step: AND_4, probability: 0.5, slot: 'beat4and' },\n ],\n standard: DEFAULT_FIGURE,\n});\n\n/** An empty bar of the grid. */\nfunction emptyPattern(): boolean[] {\n return Array.from({ length: KICK_STEPS }, () => false);\n}\n\n/**\n * Whether a pre-chorus bar sits in the two-bar lift into the chorus.\n *\n * @param section Current section.\n * @param bar Bar index within the section.\n * @param sectionBars Total bars in the section.\n * @param nextIsChorus Whether the following section is a chorus.\n */\nexport function isInPreChorusLift(\n section: SectionType,\n bar: number,\n sectionBars: number,\n nextIsChorus: boolean,\n): boolean {\n if (section !== 'b' || !nextIsChorus || sectionBars < 3) {\n return false;\n }\n return bar >= sectionBars - 2;\n}\n\n/**\n * Realise a kick figure for one bar.\n *\n * @param figure The figure to play.\n * @param section Current section, matched against each slot's own sections.\n * @param bar Bar index, which addresses the draws and decides slot parity.\n * @param draw Position-addressed draws for the kick.\n * @param rhythmic Rhythmic dial in [0, 1].\n * @returns The bar's onsets on the sixteenth grid.\n */\nexport function realiseKickFigure(\n figure: KickFigure,\n section: SectionType,\n bar: number,\n draw: Draw,\n rhythmic: number,\n): KickPattern {\n const pattern = emptyPattern();\n for (const slot of figure) {\n if (slot.step < 0 || slot.step >= KICK_STEPS) {\n continue;\n }\n if (slot.sections && !slot.sections.some((name) => mapSection(name) === section)) {\n continue;\n }\n if (slot.barParity !== undefined && bar % 2 !== slot.barParity) {\n continue;\n }\n if (slot.probability === undefined) {\n pattern[slot.step] = true;\n continue;\n }\n if (draw.prob(leanedBy(slot.probability, rhythmic), 'kick', bar, slot.slot ?? slot.step)) {\n pattern[slot.step] = true;\n }\n }\n return pattern;\n}\n\n/**\n * Build a section- and style-aware kick pattern.\n *\n * @param section Current section.\n * @param style Internal drum style.\n * @param bar Bar index (drives variation for a few styles).\n * @param draw Position-addressed draws for the kick.\n * @param rhythmic Rhythmic dial in [0, 1].\n */\nexport function getKickPattern(\n section: SectionType,\n style: DrumStyle,\n bar: number,\n draw: Draw,\n rhythmic: number,\n): KickPattern {\n const own = KICK_FIGURES[style] ?? DEFAULT_FIGURE;\n const figure = section === 'outro' ? [...OUTRO_FIGURE, ...statedSlots(own)] : own;\n return realiseKickFigure(figure, section, bar, draw, rhythmic);\n}\n","import type { Draw } from '../context/index.js';\nimport { sustainsStrokes } from '../context/index.js';\nimport { type SectionCtx, swing16 } from './beat.js';\nimport { HH_16TH_BPM_THRESHOLD } from './hihat.js';\nimport type { HitList } from './hit.js';\nimport type { DrumRole, SectionType } from './internal.js';\nimport {\n backbeatBeats,\n EIGHTH,\n GM,\n PercMoodCategory,\n SIXTEENTH,\n sectionIndex,\n} from './internal.js';\n\n/** Enabled auxiliary percussion voices for a section. */\nexport type PercussionConfig = {\n tambourine: boolean;\n shaker: boolean;\n handclap: boolean;\n shaker16th: boolean;\n};\n\ntype PercActivation = [tambourine: boolean, shaker: boolean, handclap: boolean];\n\n// Percussion activation per [mood category][section index]. Columns:\n// intro, a, b, chorus, bridge, interlude, outro, chant, mixbreak.\nconst F: PercActivation = [false, false, false];\nconst PERC_TABLE: PercActivation[][] = [\n /* calm */ [F, F, F, F, F, F, F, F, F],\n /* std */ [F, F, [false, true, false], [true, false, true], F, F, F, F, [true, false, true]],\n /* ener */ [\n F,\n [false, true, false],\n [false, true, false],\n [true, true, true],\n F,\n F,\n F,\n F,\n [true, true, true],\n ],\n /* idol */ [\n F,\n [false, true, false],\n [false, true, false],\n [true, true, true],\n F,\n F,\n F,\n F,\n [true, true, true],\n ],\n /* rock */ [F, F, F, [false, false, true], F, F, F, F, [false, false, true]],\n];\n\n/** Resolve which auxiliary percussion voices play for a section. */\nexport function getPercussionConfig(\n mood: PercMoodCategory,\n section: SectionType,\n): PercussionConfig {\n const row = PERC_TABLE[mood] ?? PERC_TABLE[PercMoodCategory.Standard];\n const act = row?.[sectionIndex(section)] ?? F;\n return {\n tambourine: act[0],\n shaker: act[1],\n handclap: act[2],\n shaker16th: act[1],\n };\n}\n\n/**\n * Emit one bar of auxiliary percussion (tambourine/shaker/handclap).\n *\n * The shaker's off-16ths are where the rhythmic dial reaches this voice: each\n * has its own draw, so turning the dial up fills the subdivision in one\n * position at a time rather than switching the whole bar between two patterns.\n *\n * These voices are an overdub layer rather than strokes the kit player has a\n * hand free for: a tambourine and a hand-clap on the backbeat sound over a\n * snare and a hi-hat that already take both hands.\n */\nexport function generateAuxPercussionForBar(\n track: HitList,\n barStart: number,\n config: PercussionConfig,\n role: DrumRole,\n densityMult: number,\n draw: Draw,\n bpm: number,\n sec: SectionCtx,\n swingAmount: number,\n barBeats = 4,\n): void {\n if (role === 'minimal') {\n return;\n }\n\n // Tambourine and hand-claps reinforce the backbeat, so they land where the\n // section says the backbeat is: a style that moves it to beat 3 would\n // otherwise have two voices naming two different backbeats in one bar.\n const backbeats = backbeatBeats(sec.snareBeat3, barBeats);\n\n if (config.tambourine) {\n for (const beat of backbeats) {\n const raw = 70 * densityMult * draw.float(0.9, 1.1, 'tambourine', barStart, beat);\n track.add(GM.TAMBOURINE, barStart + beat, EIGHTH, Math.max(40, Math.min(90, raw)));\n }\n }\n\n if (config.shaker) {\n const use16th =\n config.shaker16th &&\n bpm < HH_16TH_BPM_THRESHOLD &&\n sustainsStrokes(SIXTEENTH, bpm, sec.difficulty);\n if (use16th) {\n const velCurve = [0.75, 0.45, 0.6, 0.45];\n for (let beat = 0; beat < barBeats; beat += 1) {\n for (let sub = 0; sub < 4; sub += 1) {\n // The on-beat and mid-beat shakes carry the pattern; the two between\n // them are the dial's to add.\n if (sub % 2 === 1 && !draw.prob(sec.rhythmic, 'shaker16', barStart, beat, sub)) {\n continue;\n }\n const raw =\n 80 *\n (velCurve[sub] ?? 0.5) *\n densityMult *\n draw.float(0.9, 1.1, 'shaker', barStart, beat, sub);\n track.add(\n GM.SHAKER,\n swing16(barStart + beat + sub * SIXTEENTH, sec, swingAmount),\n SIXTEENTH,\n Math.max(25, Math.min(85, raw)),\n );\n }\n }\n } else {\n const velCurve = [0.75, 0.55];\n for (let beat = 0; beat < barBeats; beat += 1) {\n for (let sub = 0; sub < 2; sub += 1) {\n const raw =\n 80 *\n (velCurve[sub] ?? 0.6) *\n densityMult *\n draw.float(0.9, 1.1, 'shaker', barStart, beat, sub);\n track.add(\n GM.SHAKER,\n swing16(barStart + beat + sub * EIGHTH, sec, swingAmount),\n EIGHTH,\n Math.max(25, Math.min(85, raw)),\n );\n }\n }\n }\n }\n\n if (config.handclap) {\n for (const beat of backbeats) {\n const raw = 85 * densityMult * draw.float(0.9, 1.1, 'handclap', barStart, beat);\n track.add(GM.HANDCLAP, barStart + beat, EIGHTH, Math.max(50, Math.min(100, raw)));\n }\n }\n}\n","import type { Articulation, Limb, PercussionProfile } from '../../core/instrument/index.js';\nimport { LIMBS } from '../../core/instrument/index.js';\nimport { deepFreeze } from '../vocabulary/freeze.js';\nimport { DRUM_NOTES, type DrumVoice } from './hit.js';\n\n/** Which limbs reach each voice, in the order a player would take them. */\nconst REACH_BY_VOICE: Readonly<Record<DrumVoice, readonly Limb[]>> = {\n kick: ['rightFoot'],\n pedalHiHat: ['leftFoot'],\n // The left hand rests on the snare and the right on the timekeeping voices;\n // either can cross over, which is what the second entry allows.\n snare: ['leftHand', 'rightHand'],\n sideStick: ['leftHand', 'rightHand'],\n handClap: ['leftHand', 'rightHand'],\n tambourine: ['leftHand', 'rightHand'],\n closedHiHat: ['rightHand', 'leftHand'],\n openHiHat: ['rightHand', 'leftHand'],\n ride: ['rightHand', 'leftHand'],\n crash: ['rightHand', 'leftHand'],\n highTom: ['rightHand', 'leftHand'],\n midTom: ['rightHand', 'leftHand'],\n lowTom: ['rightHand', 'leftHand'],\n shaker: ['rightHand', 'leftHand'],\n};\n\n/**\n * Voices a drum track lays over the kit on a take of their own.\n *\n * The groove commits both hands wherever it puts a hi-hat under a snare, so a\n * tambourine, a hand-clap or a shaker riding over it is a second pass rather\n * than a stroke the player has a hand free for — which is how these parts reach\n * a record, and why keeping them means overdubbing rather than thinning the\n * groove that carries them.\n */\nconst OVERDUB_VOICES: readonly DrumVoice[] = Object.freeze([\n 'handClap',\n 'tambourine',\n 'shaker',\n] as const);\n\n/** The reach table keyed the way a hit is: by General MIDI note number. */\nfunction reachByNote(): Record<number, readonly Limb[]> {\n const reach: Record<number, readonly Limb[]> = {};\n for (const [voice, limbs] of Object.entries(REACH_BY_VOICE) as [DrumVoice, readonly Limb[]][]) {\n reach[DRUM_NOTES[voice]] = limbs;\n }\n return reach;\n}\n\n/** Ornaments a kit produces; the string techniques are not among them. */\nconst KIT_ARTICULATIONS: readonly Articulation[] = Object.freeze([\n 'accent',\n 'ghost',\n 'staccato',\n 'mute',\n 'flam',\n 'drag',\n 'roll',\n 'choke',\n 'open',\n] as const);\n\n/**\n * A drum kit played with four limbs, covering every voice\n * {@link generateDrums} emits.\n *\n * A kit is bounded by limbs rather than by range: two voices wanting the same\n * hand at the same instant is what makes a pattern unplayable, and a voice no\n * limb reaches is not on the kit at all.\n *\n * The auxiliary percussion — hand-claps, tambourine, shaker — is dubbed over\n * the kit on a pass of its own, so it sounds through a backbeat whose snare and\n * hi-hat already take both hands, and the four limbs stay the bound on the\n * groove itself.\n *\n * @example\n * ```ts\n * import { DRUM_KIT, generateDrums, playability } from '@libraz/libcantus';\n * const hits = generateDrums({\n * bars: 1,\n * style: 'funk',\n * section: 'chorus',\n * ctx: { bpm: 120, complexity: { rhythmic: 0.8 } },\n * });\n * const report = playability(hits, DRUM_KIT, 120);\n * ```\n * @category Composition\n */\nexport const DRUM_KIT: PercussionProfile = Object.freeze({\n kind: 'percussion',\n name: 'drum kit',\n limbs: LIMBS,\n reach: deepFreeze(reachByNote()),\n overdub: deepFreeze(OVERDUB_VOICES.map((voice) => DRUM_NOTES[voice])),\n articulations: KIT_ARTICULATIONS,\n // One stroke per limb, and one more per voice dubbed over them.\n polyphony: LIMBS.length + OVERDUB_VOICES.length,\n});\n","/**\n * The drum pattern dictionary: the bar-long figures that make a groove belong\n * to a genre rather than merely being correct.\n *\n * Provenance: every pattern here is the plain, unattributable pulse of its\n * genre — the figure taught as \"this is how that music goes\" — written out on\n * the sixteenth grid from that description. None reproduces the drum part of a\n * particular recording, and each entry says which ground it qualifies under.\n */\n\nimport { InvalidInputError } from '../../core/errors/index.js';\nimport { type Articulation, canSound, type Limb } from '../../core/instrument/index.js';\nimport {\n beatsPerBar,\n formatTimeSignature,\n type MeterLike,\n meterAt,\n type TimeSignature,\n toMeterData,\n} from '../../core/meter/index.js';\nimport {\n assertGenerationBudget,\n assertOneOf,\n assertPositiveInt,\n assertRange,\n assertRecord,\n describeRejected,\n} from '../../core/validation/index.js';\nimport { type GenerationContextInput, resolveContext } from '../context/index.js';\nimport { deepFreeze } from '../vocabulary/freeze.js';\nimport {\n BAR_STEPS,\n deform,\n GENRES,\n type Genre,\n type GridEvent,\n mergeVocabulary,\n pickVocabulary,\n STEP_BEATS,\n selectVocabulary,\n type Vocabulary,\n vocabularyOfKind,\n withinCeiling,\n} from '../vocabulary/index.js';\nimport { type DrumHit, type DrumVoice, drumNoteOf, HitList } from './hit.js';\nimport {\n DRUM_FEELS,\n feelSwingAmount,\n type GrooveFeel,\n PUBLIC_SECTIONS,\n type Section,\n} from './internal.js';\nimport { effectiveSwing, quantizeSwing } from './swing.js';\n\n/**\n * One stroke of a pattern, on the bar's sixteenth grid.\n *\n * Velocity is a factor of the passage's base velocity rather than a MIDI\n * number, so the same figure sits right in a verse and in a chorus.\n */\nexport type DrumStroke = GridEvent & {\n voice: DrumVoice;\n /** Sounding length in quarter-note beats; one sixteenth when absent. */\n duration?: number;\n articulation?: Articulation;\n /** Which limb plays the stroke, where the figure depends on it. */\n limb?: Limb;\n};\n\n/**\n * A bar-long drum figure.\n *\n * @category Composition\n */\nexport type DrumPattern = {\n /** Sixteenth steps in the figure; a 4/4 bar is sixteen. */\n steps: number;\n strokes: DrumStroke[];\n};\n\n/**\n * A drum figure with the conditions it fits.\n *\n * @category Composition\n */\nexport type DrumVocabulary = Vocabulary<DrumPattern>;\n\n/**\n * Whether a caller's material is a drum pattern.\n *\n * A context carries one dictionary for the whole piece, so each generator\n * recognises its own material: an entry meant for the bass is invisible here\n * rather than mis-read.\n *\n * @category Composition\n */\nexport function isDrumPattern(material: unknown): material is DrumPattern {\n const candidate = material as DrumPattern | null;\n return (\n typeof candidate === 'object' &&\n candidate !== null &&\n typeof candidate.steps === 'number' &&\n Array.isArray(candidate.strokes)\n );\n}\n\n/**\n * Reject a figure that strikes a voice this generator has no note for.\n *\n * A figure is recognised by its shape and then read for its voices, which is\n * why the two are separate: an entry meant for another generator is invisible\n * here, but one written for this one and naming `'hihat'` — a plausible name\n * this kit does not use — is a mistake in the dictionary rather than a figure\n * that quietly plays nothing.\n */\nfunction assertDrumPattern(material: DrumPattern, label: string): void {\n material.strokes.forEach((stroke, index) => {\n if (\n typeof stroke !== 'object' ||\n stroke === null ||\n typeof stroke.voice !== 'string' ||\n drumNoteOf(stroke.voice) === undefined\n ) {\n throw new InvalidInputError(\n `${label} strokes[${index}].voice must name a drum voice; received ` +\n `${describeRejected((stroke as DrumStroke | null)?.voice)}`,\n );\n }\n });\n}\n\n/** One stroke. */\nfunction hit(\n voice: DrumVoice,\n step: number,\n velocity: number,\n extra?: { duration?: number; articulation?: Articulation; limb?: Limb },\n): DrumStroke {\n return { voice, step, velocity, ...extra };\n}\n\n/** A ghosted snare: decoration, so the ornament dial decides whether it stays. */\nfunction ghost(step: number): DrumStroke {\n return hit('snare', step, 0.32, { articulation: 'ghost' });\n}\n\n/** A run of strokes on one voice at a fixed spacing. */\nfunction pulse(voice: DrumVoice, every: number, velocity: number, from = 0): DrumStroke[] {\n const out: DrumStroke[] = [];\n for (let step = from; step < BAR_STEPS; step += every) {\n out.push(hit(voice, step, velocity));\n }\n return out;\n}\n\n/** A four-beat pattern on the sixteenth grid. */\nfunction bar(strokes: DrumStroke[]): DrumPattern {\n return { steps: BAR_STEPS, strokes };\n}\n\nconst FOUR_FOUR: TimeSignature = { numerator: 4, denominator: 4 };\n\n/**\n * The built-in drum patterns, in declaration order.\n *\n * @category Composition\n */\nexport const DRUM_PATTERNS: readonly DrumVocabulary[] = deepFreeze([\n {\n id: 'motownBackbeat',\n genre: 'motown',\n difficulty: 2,\n articulations: [],\n ts: FOUR_FOUR,\n tempoRange: [96, 136],\n material: bar([\n hit('kick', 0, 1),\n hit('kick', 6, 0.85),\n hit('kick', 8, 0.9),\n hit('kick', 14, 0.8),\n hit('snare', 4, 1),\n hit('snare', 12, 1),\n ...pulse('tambourine', 2, 0.5),\n ]),\n provenance: {\n basis: 'idiom',\n note: 'the label sound of a four-square backbeat under a tambourine on every eighth',\n },\n },\n {\n id: 'funkSixteenth',\n genre: 'funk',\n difficulty: 4,\n articulations: ['ghost'],\n ts: FOUR_FOUR,\n tempoRange: [84, 120],\n material: bar([\n hit('kick', 0, 1),\n hit('kick', 3, 0.8),\n hit('kick', 6, 0.85),\n hit('kick', 10, 0.8),\n hit('snare', 4, 1),\n hit('snare', 12, 1),\n ghost(2),\n ghost(7),\n ghost(9),\n ghost(15),\n ...pulse('closedHiHat', 1, 0.55),\n ]),\n provenance: {\n basis: 'idiom',\n note: 'the genre-defining sixteenth-note hi-hat with ghosted snares around the backbeat',\n },\n },\n {\n id: 'halfTimeShuffle',\n genre: 'blues',\n difficulty: 5,\n articulations: ['ghost'],\n ts: FOUR_FOUR,\n tempoRange: [72, 104],\n material: bar([\n hit('kick', 0, 1),\n hit('kick', 10, 0.85),\n hit('snare', 8, 1),\n ghost(3),\n ghost(7),\n ghost(11),\n ghost(15),\n ...pulse('closedHiHat', 2, 0.5),\n ]),\n provenance: {\n basis: 'idiom',\n note: 'the shuffle with its backbeat displaced to beat three; the triplet feel is the groove feel applied at render, not written into the grid',\n },\n },\n {\n id: 'bossaNova',\n genre: 'bossa',\n difficulty: 3,\n articulations: [],\n ts: FOUR_FOUR,\n tempoRange: [110, 160],\n material: bar([\n hit('kick', 0, 0.9),\n hit('kick', 6, 0.8),\n hit('kick', 8, 0.9),\n hit('kick', 14, 0.8),\n hit('sideStick', 0, 0.7),\n hit('sideStick', 3, 0.7),\n hit('sideStick', 6, 0.7),\n hit('sideStick', 10, 0.7),\n hit('sideStick', 12, 0.7),\n ...pulse('closedHiHat', 2, 0.45),\n ]),\n provenance: {\n basis: 'traditional',\n note: 'the Brazilian dance pattern: a rim-click clave over a two-beat bass figure',\n },\n },\n {\n id: 'samba',\n genre: 'samba',\n difficulty: 4,\n articulations: [],\n ts: FOUR_FOUR,\n tempoRange: [92, 140],\n material: bar([\n hit('kick', 0, 0.85),\n hit('kick', 3, 0.7),\n hit('kick', 4, 1),\n hit('kick', 7, 0.7),\n hit('kick', 8, 0.85),\n hit('kick', 11, 0.7),\n hit('kick', 12, 1),\n hit('kick', 15, 0.7),\n hit('sideStick', 4, 0.75),\n hit('sideStick', 12, 0.75),\n ...pulse('shaker', 1, 0.4),\n ]),\n provenance: {\n basis: 'traditional',\n note: 'the surdo accent on the second beat of each pair, under a continuous shaker',\n },\n },\n {\n id: 'gospelPocket',\n genre: 'gospel',\n difficulty: 4,\n articulations: ['ghost'],\n ts: FOUR_FOUR,\n tempoRange: [64, 104],\n material: bar([\n hit('kick', 0, 1),\n hit('kick', 6, 0.8),\n hit('kick', 10, 0.85),\n hit('kick', 14, 0.75),\n hit('snare', 4, 1),\n hit('snare', 12, 1),\n ghost(2),\n ghost(6),\n ghost(11),\n ghost(14),\n ...pulse('closedHiHat', 1, 0.5),\n ]),\n provenance: {\n basis: 'idiom',\n note: 'the deep pocket of the church backbeat, ghosted between the accents',\n },\n },\n {\n id: 'drumAndBassBreak',\n genre: 'dnb',\n difficulty: 4,\n articulations: ['ghost'],\n ts: FOUR_FOUR,\n tempoRange: [160, 180],\n material: bar([\n hit('kick', 0, 1),\n hit('kick', 10, 0.9),\n hit('snare', 4, 1),\n hit('snare', 11, 0.85),\n hit('snare', 12, 1),\n ghost(7),\n ghost(15),\n ...pulse('closedHiHat', 2, 0.45),\n ]),\n provenance: {\n basis: 'construction',\n note: 'the two-bar break skeleton of the genre, written out from its rhythmic grid',\n },\n },\n]);\n\n/**\n * Options controlling {@link placeDrumPattern}.\n *\n * @category Composition\n */\nexport type DrumPatternOptions = {\n /** Number of bars to generate. */\n bars: number;\n /**\n * The genre whose figures may be used. Genre is what selects the material;\n * the complexity dials then deform it and the difficulty ceiling rejects it.\n */\n genre: Genre;\n /** Section the pattern plays in, matched against each entry's own sections. */\n section?: Section;\n /** Time signature, in any form that names one; defaults to 4/4. */\n ts?: MeterLike;\n /**\n * The generation context. `bpm` is the tempo, `complexity.rhythmic` thins the\n * figure below its middle setting and syncopates it above,\n * `complexity.ornament` decides how many ghosts survive,\n * `complexity.difficulty` rejects a figure the player could not keep up at\n * this tempo, `vocabulary` brings figures of the caller's own, and `seed`\n * fixes which figure is chosen for each bar.\n *\n * @defaultValue `{ seed: 0, bpm: 120 }`\n */\n ctx?: GenerationContextInput;\n /**\n * Play the figure at half or double its written rate. This is a deformation\n * like the dials, not a different figure: the dictionary entry is the same.\n *\n * It is named for the note values it changes because `feel` names the swing\n * the figure is played with, here and in {@link generateDrums} alike.\n *\n * @defaultValue `'straight'`\n */\n rate?: 'straight' | 'half' | 'double';\n /**\n * The swing the figure is played with. Some entries are written on the\n * straight grid and are only themselves once a triplet feel is applied at\n * render — the half-time shuffle is the plain case — so this is the surface\n * those entries name when they say the feel comes from the performance.\n *\n * @defaultValue `'straight'`\n */\n feel?: GrooveFeel;\n /** Base velocity the figure's own factors are read against. */\n velocity?: number;\n /**\n * Maximum number of onsets {@link placeDrumPattern} may write. One figure is\n * placed per bar, so this is the guard against an unbounded caller rather\n * than a limit on any search.\n *\n * @defaultValue 1000000\n */\n budget?: number;\n};\n\n/** Base velocity a pattern's factors are read against when none is given. */\nconst DEFAULT_VELOCITY = 100;\n\n/** Tempo assumed when neither the context nor the options name one. */\nconst DEFAULT_BPM = 120;\n\n/** Generous upper bound on onsets one bar of a pattern emits. */\nconst MAX_STROKES_PER_BAR = 64;\n\n/**\n * Generate a genre-characteristic groove from the pattern dictionary.\n *\n * The three dials never fight, because each owns a different step: the genre\n * chooses which figures are candidates, the complexity dials deform the chosen\n * figure, and the difficulty ceiling rejects a figure whose closest pair of\n * strokes is faster than a player at that ceiling sustains. A rejected figure\n * is dropped from the candidates rather than simplified, since a simplified\n * figure is a different figure.\n *\n * @param opts Generation options.\n * @returns Percussion onsets in onset order, ties broken by pitch.\n * @throws If the bar count, tempo, or time signature is invalid, or the genre\n * is not one this library names.\n *\n * @example\n * ```ts\n * import { placeDrumPattern } from '@libraz/libcantus';\n * const hits = placeDrumPattern({ bars: 2, genre: 'bossa', ctx: { seed: 7, bpm: 130 } });\n * ```\n *\n * @category Composition\n */\nexport function placeDrumPattern(opts: DrumPatternOptions): DrumHit[] {\n assertRecord(opts, 'drum pattern options');\n assertPositiveInt(opts.bars, 'drum pattern bars');\n assertGenerationBudget(opts.bars * MAX_STROKES_PER_BAR, 'drum pattern hits', opts.budget);\n const genre = assertOneOf(opts.genre, GENRES, 'drum pattern genre');\n const section =\n opts.section === undefined\n ? undefined\n : assertOneOf(opts.section, PUBLIC_SECTIONS, 'drum pattern section');\n const ts = meterAt(0, toMeterData(opts.ts ?? FOUR_FOUR, 'ts'));\n if (opts.velocity !== undefined) {\n assertRange(opts.velocity, 1, 127, 'drum pattern velocity');\n }\n const feel = opts.feel === undefined ? 'straight' : assertOneOf(opts.feel, DRUM_FEELS, 'feel');\n const resolved = resolveContext(opts.ctx);\n const bpm = resolved.bpm ?? DEFAULT_BPM;\n const draw = resolved.part('drums');\n const baseVelocity = opts.velocity ?? DEFAULT_VELOCITY;\n const barBeats = beatsPerBar(ts);\n const kit = resolved.instrument('drums');\n const swing = effectiveSwing(feel, feelSwingAmount(feel));\n\n const supplied = vocabularyOfKind(resolved.vocabulary, isDrumPattern);\n // Read for its voices here rather than where a stroke is placed: an entry the\n // draw happens not to reach this time is as wrong as one it does, and a\n // dictionary is worth hearing about before a bar of it is written.\n supplied.forEach((entry, index) => {\n assertDrumPattern(entry.material, `drum pattern vocabulary[${index}]`);\n });\n const dictionary = mergeVocabulary(DRUM_PATTERNS, supplied);\n // A meter no figure is written in is refused rather than answered with an\n // empty track: the built-in dictionary is written in 4/4 throughout, and a\n // silent result there is indistinguishable from one where the genre, the\n // section or the difficulty ceiling took every candidate away.\n if (selectVocabulary(dictionary, { ts }).length === 0) {\n throw new InvalidInputError(\n `drum pattern ts names a meter no figure is written in: ${formatTimeSignature(ts)}. ` +\n 'The built-in figures are written in 4/4; supply vocabulary in another meter through the context.',\n );\n }\n const track = new HitList();\n\n for (let bar = 0; bar < opts.bars; bar += 1) {\n const entry = pickVocabulary(\n dictionary,\n {\n genre,\n section,\n bpm,\n ts,\n difficulty: resolved.difficulty,\n // Naming a kit is the request that the part be playable on it, so a\n // figure calling for a technique the kit has no way to produce is not\n // offered at all rather than written and silently misread.\n ...(kit === undefined ? {} : { articulations: kit.articulations }),\n },\n draw,\n 'pattern',\n bar,\n );\n if (!entry) {\n continue;\n }\n const strokes = deform(\n entry.material.strokes,\n {\n ...(resolved.rhythmic === undefined ? {} : { rhythmic: resolved.rhythmic }),\n ...(resolved.ornament === undefined ? {} : { ornament: resolved.ornament }),\n isOrnament: (stroke) => (stroke as DrumStroke).articulation === 'ghost',\n rate: opts.rate ?? 'straight',\n spanSteps: entry.material.steps,\n // The dials rank the figure's positions against the bar it is written\n // in, which is the bar this call is placing it in.\n ts,\n },\n draw,\n 'pattern',\n bar,\n );\n // The ceiling judges what the dials actually produced, not what the\n // dictionary wrote: syncopating a figure can bring two strokes together.\n if (!withinCeiling(strokes, bpm, resolved.difficulty)) {\n continue;\n }\n const barStart = bar * barBeats;\n for (const stroke of strokes) {\n const pitch = drumNoteOf(stroke.voice);\n if (pitch === undefined) {\n continue;\n }\n track.add(\n pitch,\n // The bar's own grid is what is swung, and the bar start is added\n // back after. Swinging the absolute beat takes the offset inside a\n // quarter note, which is the same thing only while a bar is a whole\n // number of them: in 7/8 every other bar begins half a beat into one,\n // so the downbeat was warped and the offbeats came out straight.\n barStart + quantizeSwing(stroke.step * STEP_BEATS, swing, 'sixteenth'),\n stroke.duration ?? STEP_BEATS,\n baseVelocity * stroke.velocity,\n stroke.articulation,\n );\n }\n }\n\n // Naming a kit is the request that the part be playable on it, so a voice the\n // kit does not have is an absent stroke rather than a hard one.\n const endBeat = opts.bars * barBeats;\n return track.hits\n .filter((h) => h.startBeat < endBeat && (kit === undefined || canSound(kit, h.pitch)))\n .sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);\n}\n","import { InvalidInputError } from '../../core/errors/index.js';\nimport { canSound, type InstrumentProfile } from '../../core/instrument/index.js';\nimport { beatsPerBar, type MeterLike, meterAt, resolveMeters } from '../../core/meter/index.js';\nimport {\n assertGenerationBudget,\n assertInteger,\n assertOneOf,\n assertPositiveInt,\n assertRecord,\n} from '../../core/validation/index.js';\nimport { type GenerationContextInput, resolveContext, sustainsStrokes } from '../context/index.js';\nimport { selectVocabulary, vocabularyOfKind } from '../vocabulary/index.js';\nimport {\n type BeatCtx,\n generateGhostNotesForBeat,\n generateHiHatForBeat,\n generateKickForBeat,\n generatePreChorusBuildup,\n generateSnareForBeat,\n type SectionCtx,\n swing16,\n} from './beat.js';\nimport { euclideanRhythm } from './euclid.js';\nimport {\n FILL_ARCHETYPES,\n type FillArchetype,\n fillArchetypeFor,\n fillWithinCeiling,\n generateFill,\n getFillStartBeat,\n isFillArchetype,\n selectFillType,\n} from './fills.js';\nimport {\n footHiHatVelocity,\n getHiHatLevel,\n openHiHatBarInterval,\n openHiHatBeat,\n shouldPlayHiHat,\n shouldUseFootHiHat,\n shouldUseRideForSection,\n} from './hihat.js';\nimport { type DrumHit, HitList, onsetKey } from './hit.js';\nimport {\n backingScale,\n calculateVelocity,\n DEFAULT_RHYTHMIC,\n DRUM_FEELS,\n DRUM_ROLES,\n type DrumRole,\n type Feel,\n feelSwingAmount,\n GM,\n GROOVE_STYLES,\n type GrooveFeel,\n type GrooveStyle,\n ghostMoodCategory,\n mapSection,\n mapStyle,\n PUBLIC_SECTIONS,\n percMoodCategory,\n type Section,\n sectionDensityMultiplier,\n sectionEnergy,\n} from './internal.js';\nimport { getKickPattern, isInPreChorusLift, type KickPattern } from './kick.js';\nimport { generateAuxPercussionForBar, getPercussionConfig } from './percussion.js';\n\nexport type { FillArchetype, FillStroke, FillType, FillVelocity } from './fills.js';\nexport { FILL_ARCHETYPES, FILL_TYPES, isFillArchetype } from './fills.js';\n/**\n * A single drum onset emitted by {@link generateDrums}.\n *\n * @category Composition\n */\nexport type { DrumHit, DrumVoice } from './hit.js';\nexport { DRUM_NOTES, drumVoiceOf } from './hit.js';\n/**\n * Groove feel (the swing/straight rhythmic character) for {@link generateDrums}\n * and {@link placeDrumPattern}.\n *\n * @category Composition\n */\n/**\n * Drum voicing role and groove style identifiers for {@link generateDrums}.\n *\n * @category Composition\n */\nexport type { DrumRole, DrumStyle, GrooveFeel, GrooveStyle, Section } from './internal.js';\nexport type { KickFigure, KickPattern, KickSlot } from './kick.js';\nexport { KICK_FIGURES, KICK_STEPS } from './kick.js';\nexport { DRUM_KIT } from './kit.js';\nexport type {\n DrumPattern,\n DrumPatternOptions,\n DrumStroke,\n DrumVocabulary,\n} from './vocabulary.js';\nexport { DRUM_PATTERNS, isDrumPattern, placeDrumPattern } from './vocabulary.js';\n\n/**\n * A Euclidean (Bjorklund) kick pattern for {@link generateDrums}.\n *\n * @category Composition\n */\nexport type EuclideanKick = {\n /** Number of kick onsets, clamped to `[0, steps]`. */\n pulses: number;\n /**\n * Total steps in the bar (1..16).\n *\n * @defaultValue 16\n */\n steps?: number;\n /**\n * Steps to rotate the onsets toward later positions.\n *\n * @defaultValue 0\n */\n rotation?: number;\n};\n\n/**\n * Options controlling {@link generateDrums}.\n *\n * @category Composition\n */\nexport type DrumsOptions = {\n /**\n * Number of bars to generate.\n */\n bars: number;\n style: GrooveStyle;\n section: Section;\n /**\n * Time signature.\n *\n * Only 4/4 is accepted: every shape this generator writes — the backbeat, the\n * hi-hat subdivisions, the open-hat and crash beats, the beat a fill starts on\n * — is written against a four-beat bar, so another meter would come back with\n * 4/4 accents in a bar of the wrong length and no sign that anything was\n * wrong. A meter it cannot place is refused rather than mis-placed, and a\n * meter map is read whole: one that opens in 4/4 and changes later is refused\n * for the change, not accepted for the opening. Use {@link generateRhythm} or\n * {@link placeDrumPattern} for other meters.\n *\n * @defaultValue `{ numerator: 4, denominator: 4 }`\n */\n ts?: MeterLike;\n /**\n * Replace the final bar with a fill.\n *\n * A pre-chorus leading into a chorus builds instead: the two-bar lift takes\n * precedence over the fill, since the buildup already marks the phrase end.\n * Set `nextSection` to anything but `'chorus'` to get the fill.\n *\n * @defaultValue false\n */\n fills?: boolean;\n /**\n * The swing the groove is played with. Naming one is taken at its word by\n * every style, including the ones whose own character is straight: a feel the\n * caller asked for and did not get is worse than one it has to ask for twice.\n * Left out, the style's own feel applies.\n */\n feel?: GrooveFeel;\n /**\n * Voicing role, from busiest to sparsest:\n *\n * - `'full'`: every voice, backbeat snare at full weight.\n * - `'ambient'`: side-stick backbeat, ride instead of hi-hat.\n * - `'minimal'`: side-stick backbeat quieter still, pedal hi-hat, no\n * auxiliary percussion.\n * - `'fxOnly'`: no kick, snare, ghost notes, timekeeping hi-hat, or fills —\n * only the fx and auxiliary voices.\n *\n * Roles express orchestration priority and peak dynamics, not a strict hit\n * count: style-specific ride, foot-hi-hat, and auxiliary patterns may make a\n * sparser role emit more individual onsets in a particular bar.\n *\n * @defaultValue `'full'`\n */\n role?: DrumRole;\n /**\n * The generation context: the tempo, the complexity dials, and the kit this\n * part is written for.\n *\n * `bpm` is the tempo in quarter-note beats per minute. `complexity.rhythmic`\n * is how busy the backing is, in [0, 1]; the dial is continuous, so every\n * value moves the result and raising it only adds onsets — the ones already\n * sounding stay where they were. `complexity.ornament` sets how many ghost\n * notes survive, `complexity.difficulty` caps how fast the generator writes,\n * `instruments.drums` — a percussion profile — restricts the output to voices\n * that kit actually has, and `seed` fixes every deterministic choice.\n *\n * @defaultValue `{ seed: 0, bpm: 120, complexity: { rhythmic: 0.5 } }`\n */\n ctx?: GenerationContextInput;\n /**\n * Section the final-bar fill leads into. Shapes which fill archetype is\n * chosen (into-chorus and out-of-intro fills differ from generic ones).\n * Defaults to `section`, i.e. a within-section fill.\n */\n nextSection?: Section;\n /**\n * When set, the kick follows this Euclidean rhythm instead of the\n * style/section pattern, giving direct access to evenly spread onsets.\n */\n euclideanKick?: EuclideanKick;\n /**\n * Maximum number of onsets {@link generateDrums} may write. Generation is\n * linear in bar count, so this is the guard against an unbounded caller\n * rather than a limit on any search.\n *\n * @defaultValue 1000000\n */\n budget?: number;\n};\n\n/** Generous upper bound on onsets emitted for a single bar, used for budgeting. */\nconst MAX_HITS_PER_BAR = 128;\n\n/** Tempo assumed when neither the context nor the options name one. */\nconst DEFAULT_BPM = 120;\n\n/**\n * Generate a drum performance as a flat list of onsets.\n *\n * Every voice (kick, snare, ghost snares, closed/open/foot hi-hats, ride, toms,\n * crash, and auxiliary percussion) is emitted as a {@link DrumHit} distinguished\n * by its General MIDI pitch. Groove style selects an internal style and feel;\n * `complexity.rhythmic` sets the backing-density level; `section` shapes kick,\n * hi-hat, ghost, and percussion density. 16th-note hi-hats drop to 8ths at or\n * above 150 BPM. When `fills` is true the final bar is replaced with a fill whose\n * archetype is shaped by `nextSection`; a fill that would emit nothing on its\n * beat falls back to the normal groove so the phrase end is never silent.\n * `euclideanKick` overrides the kick with an evenly-spread Euclidean pattern.\n * Output is fully determined by the options plus the context's seed.\n *\n * The patterns are 4/4 only — backbeats, hi-hat subdivisions, and fills are all\n * written against a four-beat bar — so beat positions are quarter notes and bar\n * `n` starts at beat `4n`.\n *\n * @param opts Generation options.\n * @returns Percussion onsets in onset order, ties broken by pitch.\n *\n * @example\n * ```ts\n * import { generateDrums } from '@libraz/libcantus';\n * const hits = generateDrums({\n * bars: 4,\n * style: 'standard',\n * section: 'chorus',\n * fills: true,\n * ctx: { seed: 0, bpm: 120, complexity: { rhythmic: 0.6 } },\n * });\n * // Fully determined by the options plus the context's seed (defaults to 0).\n * ```\n *\n * @category Composition\n */\nexport function generateDrums(opts: DrumsOptions): DrumHit[] {\n assertRecord(opts, 'drum options');\n assertPositiveInt(opts.bars, 'drum bars');\n // Generation is linear in bar count — every lookup inside the bar loop is\n // indexed — so the estimate is the hit count itself.\n assertGenerationBudget(opts.bars * MAX_HITS_PER_BAR, 'drum hits', opts.budget);\n if (opts.euclideanKick) {\n const steps = opts.euclideanKick.steps ?? 16;\n assertInteger(steps, 'euclidean steps', 1, 16);\n assertInteger(opts.euclideanKick.pulses, 'euclidean pulses');\n assertInteger(opts.euclideanKick.rotation ?? 0, 'euclidean rotation');\n }\n // The string options are checked at runtime as well as at compile time: a\n // name from a config file or a JavaScript caller would otherwise be read\n // against a table with no entry for it and yield NaN velocities.\n const publicStyle = assertOneOf(opts.style, GROOVE_STYLES, 'drum style');\n const publicSection = assertOneOf(opts.section, PUBLIC_SECTIONS, 'drum section');\n if (opts.nextSection !== undefined) {\n assertOneOf(opts.nextSection, PUBLIC_SECTIONS, 'drum nextSection');\n }\n const track = new HitList();\n const resolved = resolveContext(opts.ctx);\n const draw = resolved.part('drums');\n const bpm = resolved.bpm ?? DEFAULT_BPM;\n const rhythmic = resolved.rhythmic ?? DEFAULT_RHYTHMIC;\n // Ghosts follow the ornament dial; with none named the rhythmic dial stands\n // in, so a caller who only ever moves `density` still gets one knob.\n const ornamentDial = resolved.ornament ?? rhythmic;\n const difficulty = resolved.difficulty;\n const mapping = mapStyle(publicStyle);\n const style = mapping.style;\n const feel: Feel =\n opts.feel === undefined ? mapping.feel : assertOneOf(opts.feel, DRUM_FEELS, 'drum feel');\n const role: DrumRole =\n opts.role === undefined ? 'full' : assertOneOf(opts.role, DRUM_ROLES, 'drum role');\n // The groove is written against a four-beat bar throughout. Accepting another\n // meter placed 4/4 accents inside a bar of a different length and reported\n // nothing, which is the one outcome a caller cannot detect.\n //\n // The whole meter is read rather than the signature at beat 0: a map that\n // opens in 4/4 and changes later has every bar after the change in a length\n // these patterns cannot be laid out on, and reading only the opening bar let\n // exactly that through while a bare '3/4' was refused.\n const meters = resolveMeters({ ts: opts.ts }, 'ts');\n for (const change of meters) {\n if (change.ts.numerator !== 4 || change.ts.denominator !== 4) {\n throw new InvalidInputError(\n `drum time signature must be 4/4 throughout; received ` +\n `${change.ts.numerator}/${change.ts.denominator} at beat ${change.startBeat}`,\n );\n }\n }\n const ts = meterAt(0, meters);\n const barBeats = beatsPerBar(ts);\n // fxOnly leaves only fx/aux voices: the main kick, snare, ghost, and fill\n // voices are suppressed just as timekeeping hi-hats already are.\n const playMainVoices = role !== 'fxOnly';\n const section = mapSection(publicSection);\n const swingAmount = feelSwingAmount(feel);\n const densityMult = sectionDensityMultiplier(section) * backingScale(rhythmic);\n\n const sec: SectionCtx = {\n style,\n feel,\n feelRequested: opts.feel !== undefined,\n densityMult,\n rhythmic,\n ornament: ornamentDial,\n difficulty,\n ghostBoost: mapping.ghostBoost,\n useRide: shouldUseRideForSection(section, style),\n useFootHh: shouldUseFootHiHat(section, role),\n role,\n ghostMood: ghostMoodCategory(style),\n snareBeat3: mapping.snareBeat3,\n };\n\n const ohhBarInterval = openHiHatBarInterval(section, style);\n const energy = sectionEnergy(section);\n const fillStartBeat = getFillStartBeat(energy);\n const fillVelocity = calculateVelocity(section, fillStartBeat);\n const percMood = percMoodCategory(style);\n\n // The final-bar fill is shaped by the section it leads into. When no next\n // section is given the fill is treated as within-section (from === to).\n const nextSection = opts.nextSection ? mapSection(opts.nextSection) : section;\n const nextEnergy = sectionEnergy(nextSection);\n\n // Fills a caller brought for this piece. They are filtered by the conditions\n // they state — section, tempo, and the difficulty ceiling — and then offered\n // after the built-in table, so the built-in choices keep the draws they had.\n const callerFillEntries = selectVocabulary(\n vocabularyOfKind(resolved.vocabulary, isFillArchetype),\n { section: publicSection, bpm, difficulty },\n );\n const callerFills = new Map(callerFillEntries.map((entry) => [entry.id, entry.material]));\n const callerFillIds = callerFillEntries.map((entry) => entry.id);\n\n const euclidSteps = opts.euclideanKick?.steps ?? 16;\n const euclidKick = opts.euclideanKick\n ? euclideanRhythm(opts.euclideanKick.pulses, euclidSteps, opts.euclideanKick.rotation ?? 0)\n : undefined;\n\n const reuseSectionKick = (section === 'b' || section === 'chorus') && style !== 'sparse';\n let sectionKick: KickPattern | undefined;\n\n /** Onsets written from a pattern the caller named, which the ceiling spares. */\n const exemptOnsets = new Set<string>();\n\n for (let bar = 0; bar < opts.bars; bar += 1) {\n const barStart = bar * barBeats;\n const isLastBar = bar === opts.bars - 1;\n const hhLevel = getHiHatLevel(section, style, rhythmic, bpm, draw, difficulty, bar);\n\n if (bar === 0 && section === 'chorus') {\n track.add(GM.CRASH, barStart, 0.5, 100 * densityMult);\n }\n\n let barHasOpenHh = false;\n let openHhBeatIndex = 3;\n if (ohhBarInterval > 0 && bar % ohhBarInterval === ohhBarInterval - 1) {\n openHhBeatIndex = openHiHatBeat(section, draw, bar);\n barHasOpenHh = !track.hasCrashNear(barStart + openHhBeatIndex);\n }\n\n let kick: KickPattern | undefined;\n if (!euclidKick && reuseSectionKick) {\n sectionKick ??= getKickPattern(section, style, 0, draw, rhythmic);\n kick = sectionKick;\n } else if (!euclidKick) {\n kick = getKickPattern(section, style, bar, draw, rhythmic);\n }\n\n // The lift is a build into a chorus, so what follows decides it. Passing\n // `section === 'b'` made the predicate test the section against itself,\n // which is always true inside a pre-chorus: the lift then fired whatever\n // `nextSection` said, and suppressed the fill the caller asked for.\n const inLift = isInPreChorusLift(section, bar, opts.bars, nextSection === 'chorus');\n let currentFill: FillArchetype | undefined = FILL_ARCHETYPES.snareRoll;\n\n for (let beat = 0; beat < barBeats; beat += 1) {\n const beatTick = barStart + beat;\n const velocity = calculateVelocity(section, beat);\n\n if (opts.fills && isLastBar && !inLift && beat >= fillStartBeat) {\n if (beat === fillStartBeat) {\n // The ceiling applies to the phrase end as it does to the groove: an\n // archetype whose strokes run faster than a player at this ceiling\n // sustains is taken out of the table, and the draw runs over the rest.\n const chosen = selectFillType(\n section,\n nextSection,\n style,\n nextEnergy,\n draw,\n bar,\n callerFillIds,\n (id) => {\n const candidate = fillArchetypeFor(id, callerFills);\n return (\n candidate !== undefined &&\n fillWithinCeiling(candidate, fillStartBeat, barBeats, bpm, difficulty)\n );\n },\n );\n currentFill = chosen === undefined ? undefined : fillArchetypeFor(chosen, callerFills);\n }\n if (playMainVoices) {\n const before = track.hits.length;\n if (currentFill) {\n // One base velocity for the whole fill, read at the beat it starts\n // on: a crescendo written across two beats is one gesture, and\n // rereading the beat velocity partway through drops it.\n generateFill(track, beatTick, beat, currentFill, fillVelocity, (tick) =>\n swing16(tick, sec, swingAmount),\n );\n }\n if (track.hits.length > before) {\n continue;\n }\n // Safety net: an archetype that contributes nothing on this beat would\n // leave a silent phrase end, so fall through to the normal groove.\n } else {\n // fxOnly deliberately emits no main voices on the fill beats.\n continue;\n }\n }\n\n const ctx: BeatCtx = {\n track,\n beatTick,\n beat,\n velocity,\n section,\n bpm,\n bar,\n sectionBars: opts.bars,\n inPrechorusLift: inLift,\n swingAmount,\n barHasOpenHh,\n openHhBeat: openHhBeatIndex,\n hhLevel,\n draw,\n };\n\n if (playMainVoices) {\n if (inLift) {\n generatePreChorusBuildup(ctx, sec, isLastBar);\n }\n if (euclidKick && !inLift) {\n const stepLength = barBeats / euclidSteps;\n for (let step = 0; step < euclidKick.length; step += 1) {\n const onset = step * stepLength;\n if (euclidKick[step] && Math.floor(onset) === beat) {\n const wholeBeat = Math.abs(onset - Math.round(onset)) < 1e-9;\n const tick = swing16(barStart + onset, sec, swingAmount);\n // Naming the pattern is the request that it be written as given,\n // so these strokes are the ones the ceiling does not touch.\n exemptOnsets.add(onsetKey(GM.BD, tick));\n track.add(GM.BD, tick, Math.min(0.5, stepLength), velocity * (wholeBeat ? 1 : 0.85));\n }\n }\n } else if (kick) {\n generateKickForBeat(ctx, sec, kick);\n }\n generateSnareForBeat(ctx, sec, section === 'intro' && bar === 0);\n // Which grooves ghost is the ghost voice's own question; the lift is the\n // one thing outside it, since a buildup replaces the groove it decorates.\n if (!inLift) {\n generateGhostNotesForBeat(ctx, sec);\n }\n }\n generateHiHatForBeat(ctx, sec);\n }\n\n if (sec.useFootHh && shouldPlayHiHat(role)) {\n for (let fhhBeat = 0; fhhBeat < barBeats; fhhBeat += 2) {\n const fhhTick = barStart + fhhBeat;\n if (!track.hasOnset(GM.FHH, fhhTick)) {\n track.add(GM.FHH, fhhTick, 0.5, footHiHatVelocity(draw, bar, fhhBeat));\n }\n }\n }\n\n let percussionConfig = getPercussionConfig(percMood, section);\n if (\n role === 'fxOnly' &&\n !percussionConfig.tambourine &&\n !percussionConfig.shaker &&\n !percussionConfig.handclap\n ) {\n percussionConfig = {\n tambourine: false,\n shaker: true,\n handclap: false,\n shaker16th: false,\n };\n }\n generateAuxPercussionForBar(\n track,\n barStart,\n percussionConfig,\n role,\n densityMult,\n draw,\n bpm,\n sec,\n swingAmount,\n barBeats,\n );\n }\n\n // Foot hi-hats and auxiliary percussion are appended after the beat loop, so\n // the accumulated list is not monotonic within a bar. A consumer writing MIDI\n // reads these in order and would emit a negative delta time.\n const endBeat = opts.bars * barBeats;\n const kit = resolved.instrument('drums');\n const written = track.hits\n .filter((hit) => hit.startBeat < endBeat && playableOn(kit, hit.pitch))\n .sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);\n return underCeiling(written, bpm, difficulty, exemptOnsets);\n}\n\n/**\n * Drop the strokes a player at this ceiling could not reach in time.\n *\n * The ceiling is a property of the finished part rather than of any one voice\n * that proposed a stroke: the groove, the fill, the lift and the auxiliary\n * voices each judge their own material, and the bar where a fill meets the\n * groove before it belongs to neither of them. Applying it once at the end is\n * what makes \"no voice repeats faster than the ceiling sustains\" true of the\n * output instead of true of each generator separately.\n *\n * The earlier of two strokes too close together is the one kept, since it is\n * the one the passage was already committed to, and strokes at the same instant\n * are left alone — two voices layered on one onset are not a stream.\n *\n * @param hits The finished part, in onset order.\n * @param bpm Tempo.\n * @param difficulty The ceiling, or undefined for no ceiling.\n * @param exempt Onset keys the ceiling does not touch.\n * @returns The strokes that survive, in the order given.\n */\nfunction underCeiling(\n hits: readonly DrumHit[],\n bpm: number,\n difficulty: number | undefined,\n exempt: ReadonlySet<string>,\n): DrumHit[] {\n if (difficulty === undefined) {\n return [...hits];\n }\n const lastOnset = new Map<number, number>();\n const kept: DrumHit[] = [];\n for (const hit of hits) {\n const previous = lastOnset.get(hit.pitch);\n const reachable =\n previous === undefined ||\n exempt.has(onsetKey(hit.pitch, hit.startBeat)) ||\n sustainsStrokes(hit.startBeat - previous, bpm, difficulty);\n if (!reachable) {\n continue;\n }\n kept.push(hit);\n lastOnset.set(hit.pitch, hit.startBeat);\n }\n return kept;\n}\n\n/**\n * Whether a kit has the voice a hit asks for.\n *\n * Naming a kit is the request that the part be playable on it, so this holds\n * whatever the difficulty ceiling says: a voice the kit does not have is not a\n * hard stroke but an absent one. Without a kit there is no such constraint, and\n * the programmed case is unchanged.\n */\nfunction playableOn(kit: InstrumentProfile | undefined, pitch: number): boolean {\n return kit === undefined || canSound(kit, pitch);\n}\n","/**\n * Groove and humanization: nudging quantized note events toward a more human\n * feel, and capturing/replaying the timing-and-velocity \"feel\" of a\n * performance as a reusable groove template.\n *\n * Positions and durations are measured in quarter-note beats, matching the\n * library-wide convention. Randomness, where used, always goes through the\n * seeded PRNG from the `random` module so results are fully reproducible.\n */\n\nimport { InvalidInputError } from '../../core/errors/index.js';\nimport {\n beatsPerBar,\n formatTimeSignature,\n type MeterLike,\n meterAt,\n metricWeight,\n type TimeSignature,\n toMeterData,\n} from '../../core/meter/index.js';\nimport type { NoteEvent } from '../../core/types.js';\nimport {\n assertFiniteNumber,\n assertGenerationBudget,\n assertNoteEvents,\n assertOptions,\n assertPositiveInt,\n assertRange,\n dropSilentNotes,\n} from '../../core/validation/index.js';\nimport { type GenerationContextInput, resolveContext } from '../context/index.js';\n\n/**\n * Options controlling {@link humanize}.\n *\n * @category Rhythm & Meter\n */\nexport type HumanizeOptions = {\n /**\n * Time signature used to derive metric accents, in any form that names one.\n *\n * @defaultValue 4/4\n */\n ts?: MeterLike;\n /**\n * Maximum timing jitter in quarter-note beats, applied as ±this value.\n *\n * @defaultValue 0.02\n */\n timing?: number;\n /**\n * Maximum velocity jitter (MIDI units), applied as ±this value.\n *\n * @defaultValue 8\n */\n velocity?: number;\n /**\n * How much louder strong beats get relative to weak ones, in MIDI velocity units.\n *\n * @defaultValue 12\n */\n accent?: number;\n /**\n * Velocity assumed for events with no `velocity` of their own.\n *\n * @defaultValue 80\n */\n baseVelocity?: number;\n /**\n * The generation context, so one project seed covers this call as well as the\n * generators that wrote the notes. Its `seed` fixes the jitter, so the same\n * seed always humanizes the same way.\n *\n * @defaultValue `{ seed: 0 }`\n */\n ctx?: GenerationContextInput;\n /**\n * The name this call draws under. Humanizing two parts from one context with\n * the same name gives notes at the same position and pitch the same jitter,\n * which is what a doubled line does not want; naming each part keeps them\n * independent.\n *\n * @defaultValue `'humanize'`\n */\n part?: string;\n};\n\n/** Default time signature for {@link humanize} when none is given. */\nconst DEFAULT_TS: TimeSignature = { numerator: 4, denominator: 4 };\n/** The namespace {@link humanize} draws under when the caller names none. */\nconst DEFAULT_PART = 'humanize';\n/**\n * Grid resolution assumed by {@link extractGrooveTemplate}. A sixteenth-note\n * grid is where the deviations a template records are audible.\n */\nconst DEFAULT_GROOVE_SUBDIVISION = 4;\nconst DEFAULT_TIMING = 0.02;\nconst DEFAULT_VELOCITY_JITTER = 8;\nconst DEFAULT_ACCENT = 12;\nconst DEFAULT_BASE_VELOCITY = 80;\nconst MAX_METRIC_WEIGHT = 3;\nconst MIN_VELOCITY = 1;\nconst MAX_VELOCITY = 127;\n\n/**\n * Humanize a sequence of note events by nudging their timing and velocity\n * with small, deterministic (seeded) randomness plus a metric-accent shape:\n * events on strong beats (see {@link metricWeight}) come out louder on\n * average than events on weak ones.\n *\n * Each returned event is a copy; `startBeat` is jittered within\n * `[-timing, +timing]` beats — not floored at 0, because a pickup sounds before\n * the downbeat and flooring it there would play two upbeats as one — and\n * `velocity` is the event's own velocity (or `baseVelocity` if it has none) plus an\n * accent term scaled by the event's metric weight plus a jitter within\n * `[-velocity, +velocity]`, clamped to `[1, 127]` and rounded. The metric\n * weight is computed from each event's original (pre-jitter) `startBeat`.\n * `pitch` and `durationBeat` pass through unchanged, and the output keeps the\n * input order.\n *\n * The jitter is drawn from the generation context, addressed by the event's own\n * onset and pitch rather than by its position in the array: an event added or\n * removed leaves every other event's jitter exactly where it was, and a part\n * humanized under its own name is independent of the others sharing the seed.\n *\n * @param events The events to humanize.\n * @param opts Humanization options.\n * @returns Humanized copies of `events`, in input order.\n * @example\n * ```ts\n * import { humanize } from '@libraz/libcantus';\n * const events = [{ pitch: 60, startBeat: 0, durationBeat: 1, velocity: 80 }];\n * humanize(events, { ctx: 1, timing: 0.03 }); // copies with jittered timing and accented velocity\n * humanize(events, { ctx: { seed: 42 }, part: 'lead' }); // under a project context\n * ```\n * Notes with a zero or negative duration never sound and are dropped, so the\n * result can be shorter than the input.\n *\n * @category Rhythm & Meter\n */\nexport function humanize(events: readonly NoteEvent[], opts: HumanizeOptions = {}): NoteEvent[] {\n const asked = assertOptions(opts, 'opts');\n // The meter is read once, here, and every position below is weighed against\n // the signature it names — the one the piece opens in, given a whole map.\n const ts = meterAt(0, toMeterData(asked.ts ?? DEFAULT_TS, 'ts'));\n // Zero-length artefacts are routine in MIDI imports and never sound, so they\n // are accepted and dropped here, exactly as the analysis layer does — the two\n // sides of a pipeline must not disagree about the same array.\n assertNoteEvents(events, 'humanize events', { allowNonPositiveDuration: true });\n const sounding = dropSilentNotes(events);\n const timing = asked.timing ?? DEFAULT_TIMING;\n const velocityJitter = asked.velocity ?? DEFAULT_VELOCITY_JITTER;\n const accent = asked.accent ?? DEFAULT_ACCENT;\n const baseVelocity = asked.baseVelocity ?? DEFAULT_BASE_VELOCITY;\n assertRange(timing, 0, Number.MAX_SAFE_INTEGER, 'humanize timing');\n assertRange(velocityJitter, 0, 127, 'humanize velocity jitter');\n assertRange(accent, 0, 127, 'humanize accent');\n assertRange(baseVelocity, 0, 127, 'humanize base velocity');\n const ctx = resolveContext(asked.ctx);\n const draw = ctx.part(asked.part ?? DEFAULT_PART);\n\n return sounding.map((event) => {\n // Addressed by the event rather than by how many events came before it, so\n // inserting a note leaves every other note's jitter where it was.\n const timingOffset = draw.float(-timing, timing, 'timing', event.startBeat, event.pitch);\n // Not clamped at 0: beat 0 is the first downbeat, so a pickup is written\n // before it, and flooring the jitter there would push every upbeat onto the\n // downbeat — two notes the input told apart, played as one.\n const startBeat = event.startBeat + timingOffset;\n\n const weight = metricWeight(event.startBeat, ts);\n const accentBoost = (weight / MAX_METRIC_WEIGHT) * accent;\n const velocityOffset = draw.float(\n -velocityJitter,\n velocityJitter,\n 'velocity',\n event.startBeat,\n event.pitch,\n );\n const rawVelocity = (event.velocity ?? baseVelocity) + accentBoost + velocityOffset;\n const velocity = Math.min(MAX_VELOCITY, Math.max(MIN_VELOCITY, Math.round(rawVelocity)));\n\n // The note is copied whole and only the fields this pass owns are written\n // over it: an articulation, or anything else a caller has put on a note,\n // belongs to the note rather than to the timing it is played with.\n return { ...event, startBeat, velocity };\n });\n}\n\n/**\n * The recorded feel at a single grid slot: an average timing deviation and velocity.\n *\n * @category Rhythm & Meter\n */\nexport type GrooveSlot = {\n /** Average `actualStartBeat - quantizedBeat` (in quarter-note beats) for events landing on this slot. */\n timingOffset: number;\n /**\n * Average velocity of events landing on this slot, or `null` if no event\n * with a velocity did. `null` (not 0) is the \"unrecorded\" sentinel so that a\n * genuine velocity of 0 is preserved and applied like any other value.\n */\n velocity: number | null;\n};\n\n/**\n * A per-bar grid of timing and velocity deviations captured from a performance.\n *\n * @category Rhythm & Meter\n */\nexport type GrooveTemplate = {\n /** Grid resolution: equal grid steps per quarter-note beat. */\n subdivision: number;\n /** Number of grid slots per bar (`beatsPerBar(ts) * subdivision`). */\n slotsPerBar: number;\n /** One entry per grid slot, in slot-index order (slot 0 is the downbeat). */\n slots: GrooveSlot[];\n /**\n * The time signature the template was extracted under. {@link applyGrooveTemplate}\n * requires the apply-time meter to match this so the per-bar grid lines up\n * instead of silently drifting. Optional for backward compatibility with\n * templates constructed without it (in which case no check is performed).\n */\n ts?: TimeSignature;\n};\n\n/**\n * Quantize a beat position to the nearest grid slot at the given subdivision,\n * within the bar that contains it.\n *\n * @param beat Absolute position in quarter-note beats.\n * @param barBeats Bar length in quarter-note beats.\n * @param subdivision Grid steps per quarter-note beat.\n * @param slotsPerBar Number of grid slots per bar.\n * @returns The quantized absolute beat and its 0-based slot index within the bar.\n */\nfunction quantizeToGrid(\n beat: number,\n barBeats: number,\n subdivision: number,\n slotsPerBar: number,\n): { quantizedBeat: number; slotIndex: number } {\n const barIndex = Math.floor(beat / barBeats);\n const modBeat = beat - barIndex * barBeats;\n let slotIndex = Math.round(modBeat * subdivision);\n let effectiveBarIndex = barIndex;\n // Rounding the last slot in a bar up to the grid step count means it\n // actually lands on the next bar's downbeat.\n if (slotIndex >= slotsPerBar) {\n slotIndex = 0;\n effectiveBarIndex += 1;\n }\n return {\n quantizedBeat: effectiveBarIndex * barBeats + slotIndex / subdivision,\n slotIndex,\n };\n}\n\n/**\n * Extract a groove template from a set of note events: for each grid slot,\n * the average timing deviation from the quantized grid and the average\n * velocity of events landing on it. Slots with no velocity-carrying event\n * record `velocity: null` (the \"unrecorded\" sentinel); empty slots default to\n * `{ timingOffset: 0, velocity: null }`. The time signature is stored on the\n * template so {@link applyGrooveTemplate} can verify the meter matches.\n *\n * @param events The (typically \"groovy\", human-played) events to analyze.\n * @param ts The time signature, used to compute the bar length.\n * @param subdivision Grid resolution: equal grid steps per quarter-note beat.\n * Defaults to a sixteenth-note grid, which is where the timing deviations a\n * template records — swing, push, drag — are audible.\n * @returns The extracted groove template.\n * @category Rhythm & Meter\n */\nexport function extractGrooveTemplate(\n events: readonly NoteEvent[],\n ts: MeterLike,\n subdivision = DEFAULT_GROOVE_SUBDIVISION,\n): GrooveTemplate {\n const meter = meterAt(0, toMeterData(ts, 'ts'));\n assertNoteEvents(events, 'groove source events', { allowNonPositiveDuration: true });\n assertPositiveInt(subdivision, 'groove subdivision');\n const barBeats = beatsPerBar(meter);\n const slotsPerBar = Math.ceil(barBeats * subdivision);\n assertPositiveInt(slotsPerBar, 'groove template slots');\n assertGenerationBudget(slotsPerBar, 'groove template slots');\n const offsetSums = new Array<number>(slotsPerBar).fill(0);\n const velocitySums = new Array<number>(slotsPerBar).fill(0);\n const counts = new Array<number>(slotsPerBar).fill(0);\n const velocityCounts = new Array<number>(slotsPerBar).fill(0);\n\n for (const event of dropSilentNotes(events)) {\n const { quantizedBeat, slotIndex } = quantizeToGrid(\n event.startBeat,\n barBeats,\n subdivision,\n slotsPerBar,\n );\n offsetSums[slotIndex] = (offsetSums[slotIndex] ?? 0) + (event.startBeat - quantizedBeat);\n counts[slotIndex] = (counts[slotIndex] ?? 0) + 1;\n // Only events that carry a velocity contribute to the velocity average, so a\n // velocity-less event does not pull the slot's recorded velocity toward zero.\n if (event.velocity !== undefined) {\n velocitySums[slotIndex] = (velocitySums[slotIndex] ?? 0) + event.velocity;\n velocityCounts[slotIndex] = (velocityCounts[slotIndex] ?? 0) + 1;\n }\n }\n\n const slots: GrooveSlot[] = [];\n for (let i = 0; i < slotsPerBar; i += 1) {\n const count = counts[i] ?? 0;\n if (count === 0) {\n slots.push({ timingOffset: 0, velocity: null });\n continue;\n }\n const velocityCount = velocityCounts[i] ?? 0;\n slots.push({\n timingOffset: (offsetSums[i] ?? 0) / count,\n velocity: velocityCount > 0 ? (velocitySums[i] ?? 0) / velocityCount : null,\n });\n }\n\n return { subdivision, slotsPerBar, slots, ts: meter };\n}\n\n/**\n * Apply a groove template to a set of (typically quantized) note events: each\n * event is snapped to its grid slot and then offset by that slot's recorded\n * timing deviation, pushing stiff timing toward the template's feel.\n * Velocity is set to the slot's recorded average when it recorded one (a\n * non-`null` velocity, including a genuine 0); otherwise the event's own\n * velocity is left untouched.\n * Non-positive-duration input artefacts are omitted, so output positions do\n * not retain a one-to-one correspondence with the input array.\n *\n * If the template carries a time signature (see {@link GrooveTemplate.ts}) it\n * must match `ts`, since a differing bar length would misalign the per-bar\n * grid and drift the feel; a mismatch throws rather than corrupting timing\n * silently. `slotsPerBar` is held to the same standard, since it is what the\n * grid is read through: a template built by hand whose count does not match the\n * bar would send a note past the end of the grid and land it on the next bar's\n * downbeat. What the slots carry is checked too, so nothing this returns is a\n * note event the rest of the library would refuse.\n *\n * @param events The events to reshape.\n * @param template The groove template, from {@link extractGrooveTemplate}.\n * @param ts The time signature, used to compute the bar length.\n * @returns Reshaped copies of `events`, in input order.\n * @throws If `template.ts` is set and does not match `ts`, if `slotsPerBar` is\n * not the number of slots a bar of `ts` holds at the template's subdivision,\n * or if a slot carries a non-finite offset or a velocity outside [0, 127].\n * @example\n * ```ts\n * import { extractGrooveTemplate, applyGrooveTemplate, parseTimeSignature } from '@libraz/libcantus';\n * const ts = parseTimeSignature('4/4');\n * // Learn the feel from a loosely played performance...\n * const groovy = [\n * { pitch: 36, startBeat: 0.02, durationBeat: 1, velocity: 100 },\n * { pitch: 38, startBeat: 1.06, durationBeat: 1, velocity: 70 },\n * ];\n * const template = extractGrooveTemplate(groovy, ts, 4);\n * // ...then impose it on stiff, quantized events.\n * const quantized = [{ pitch: 36, startBeat: 0, durationBeat: 1, velocity: 80 }];\n * applyGrooveTemplate(quantized, template, ts); // events nudged toward the captured feel\n * ```\n * @category Rhythm & Meter\n */\nexport function applyGrooveTemplate(\n events: readonly NoteEvent[],\n template: GrooveTemplate,\n ts: MeterLike,\n): NoteEvent[] {\n const meter = meterAt(0, toMeterData(ts, 'ts'));\n assertNoteEvents(events, 'groove target events', { allowNonPositiveDuration: true });\n assertPositiveInt(template.subdivision, 'template subdivision');\n assertPositiveInt(template.slotsPerBar, 'template slotsPerBar');\n assertGenerationBudget(template.slots.length, 'template slots');\n if (template.slots.length !== template.slotsPerBar) {\n throw new InvalidInputError('template slots length must equal slotsPerBar');\n }\n if (\n template.ts &&\n (template.ts.numerator !== meter.numerator || template.ts.denominator !== meter.denominator)\n ) {\n throw new InvalidInputError(\n `Groove template meter ${formatTimeSignature(template.ts)} does not match ` +\n `apply-time meter ${formatTimeSignature(meter)}`,\n );\n }\n const barBeats = beatsPerBar(meter);\n // `slotsPerBar` is what the grid is read through, so it has to be the number\n // of slots this bar actually holds — the definition the field is documented\n // with. Neither check above reaches it: `slots.length` says the array matches\n // the count, and `template.ts` is optional and says nothing about the\n // subdivision. A count too small quantizes a note past the end of the grid and\n // the wrap lands it on the next bar's downbeat, which is the silent\n // corruption the meter check is there to prevent, arriving by another door.\n const expectedSlots = Math.ceil(barBeats * template.subdivision);\n if (template.slotsPerBar !== expectedSlots) {\n throw new InvalidInputError(\n `Groove template slotsPerBar ${template.slotsPerBar} does not match ` +\n `${expectedSlots} slots per bar of ${formatTimeSignature(meter)} ` +\n `at subdivision ${template.subdivision}`,\n );\n }\n // The slots are read for what goes onto a note, so what they carry is checked\n // before any of it does. A `timingOffset` of NaN moves a note nowhere the\n // caller can find it, and a velocity outside the MIDI byte is a note no\n // format can hold: both are rejected here rather than written out and read\n // back as this library's own output.\n for (let index = 0; index < template.slots.length; index += 1) {\n const slot = template.slots[index];\n if (slot === undefined) {\n continue;\n }\n assertFiniteNumber(slot.timingOffset, `template slots[${index}].timingOffset`);\n if (slot.velocity !== null && slot.velocity !== undefined) {\n assertRange(slot.velocity, 0, 127, `template slots[${index}].velocity`);\n }\n }\n return dropSilentNotes(events).map((event) => {\n const { quantizedBeat, slotIndex } = quantizeToGrid(\n event.startBeat,\n barBeats,\n template.subdivision,\n template.slotsPerBar,\n );\n const slot = template.slots[slotIndex];\n // A slot that lays back or pushes ahead of the beat moves the note it\n // lands on, downbeat included. Clamping at 0 would take the pushed feel off\n // the first note only, and would flatten a pickup onto the downbeat.\n const startBeat = quantizedBeat + (slot?.timingOffset ?? 0);\n const velocity =\n slot && slot.velocity !== null && slot.velocity !== undefined\n ? Math.round(slot.velocity)\n : event.velocity;\n\n // The note is copied whole and only the fields this pass owns are written\n // over it: an articulation, or anything else a caller has put on a note,\n // belongs to the note rather than to the timing it is played with.\n return { ...event, startBeat, velocity };\n });\n}\n","import { HUMANIZE_ADJACENCY } from '../../analyze/adjacency.js';\nimport type { MeterLike, TimeSignature } from '../../core/meter/index.js';\nimport { meterAt, metricWeight, pulseBeats, toMeterData } from '../../core/meter/index.js';\nimport type { NoteEvent } from '../../core/types.js';\nimport { assertNoteEvents } from '../../core/validation/index.js';\n\n/**\n * What a melody note contributes to the harmony: either a structural tone the\n * chord has to account for, or the ornament figure that explains it away.\n *\n * @category Reharmonization\n */\nexport type MelodyToneRole =\n | 'structural'\n | 'passing'\n | 'neighbor'\n | 'appoggiatura'\n | 'suspension'\n | 'anticipation'\n | 'escape';\n\n/**\n * One melody note's classification: its index in the classified melody, the\n * figure it forms, and whether that figure makes it ornamental.\n *\n * @category Reharmonization\n */\nexport type ClassifiedMelodyTone = {\n noteIndex: number;\n role: MelodyToneRole;\n /** True for every role but `'structural'`. */\n ornamental: boolean;\n};\n\n/**\n * Read an onset as the metric position it is playing, snapping it to the pulse\n * grid when it lies within the tolerance a performance stays inside.\n *\n * `metricWeight` answers for an exact grid position and reports no accent at\n * all a fraction of a beat away from one, while a {@link NoteEvent} is the\n * exchange format with a DAW or MIDI track — its onsets carry the timing of a\n * performance. Without this the same phrase would form one set of figures as\n * notated and another once played, and the chords chosen under it would change\n * with a few milliseconds of jitter. The tolerance is the one the adjacency test\n * already reads onsets with, so both sides of the classification agree about\n * what counts as the same beat.\n *\n * @param beat The onset in quarter-note beats.\n * @param pulse The metre's pulse length in quarter-note beats.\n */\nexport function snapToPulse(beat: number, pulse: number): number {\n const snapped = Math.round(beat / pulse) * pulse;\n return Math.abs(snapped - beat) <= HUMANIZE_ADJACENCY ? snapped : beat;\n}\n\n/** Largest interval still heard as a step; anything wider is a leap. */\nconst STEP_SEMITONES = 2;\n\nfunction isStep(a: number, b: number): boolean {\n const d = Math.abs(a - b);\n return d >= 1 && d <= STEP_SEMITONES;\n}\n\nfunction isLeap(a: number, b: number): boolean {\n return Math.abs(a - b) > STEP_SEMITONES;\n}\n\n/** Whether both notes move the same way, neither of them repeating a pitch. */\nfunction sameDirection(prev: number, note: number, next: number): boolean {\n return prev !== note && next !== note && note - prev > 0 === next - note > 0;\n}\n\n/**\n * Classify a melody's ornaments from the melody and the metre alone.\n *\n * This is deliberately chord-free: a harmonizer needs to know which notes the\n * chords must explain *before* it can choose them, so nothing here may consult a\n * harmony. What remains is the melodic and metric evidence — stepwise approach\n * and departure, repeated pitches, leaps, and the relative metric weight and\n * length of a note against its neighbours.\n *\n * The figures are recognized in the order suspension, passing, neighbor,\n * anticipation, appoggiatura, escape. A note is judged ornamental only when it\n * is metrically and rhythmically subordinate to the neighbours that frame it\n * (or, for the accented figures, when it leans on a stronger beat than its\n * approach and gives way to a resolution). A note without an adjacent\n * predecessor and successor has no figure to belong to and stays structural, so\n * the first and last notes of a phrase are always structural.\n *\n * @param melody The melody, in ascending onset order.\n * @param ts Time signature used to weight metric accents.\n * @returns One classification per input note, in input order.\n * @example\n * ```ts\n * import { classifyMelodyTones } from '@libraz/libcantus';\n * const melody = [60, 62, 64, 60].map((pitch, i) => ({\n * pitch,\n * startBeat: i,\n * durationBeat: 1,\n * }));\n * classifyMelodyTones(melody, { numerator: 4, denominator: 4 })[1];\n * // { noteIndex: 1, role: 'passing', ornamental: true }\n * ```\n * @category Reharmonization\n */\nexport function classifyMelodyTones(\n melody: readonly NoteEvent[],\n ts: MeterLike,\n): ClassifiedMelodyTone[] {\n const meter = meterAt(0, toMeterData(ts, 'ts'));\n assertNoteEvents(melody, 'melody notes', { allowNonPositiveDuration: true });\n const pulse = pulseBeats(meter);\n return melody.map((_, index) => {\n const role = roleOfNote(melody, index, meter, pulse);\n return { noteIndex: index, role, ornamental: role !== 'structural' };\n });\n}\n\n/** Classify one note against the neighbours that immediately frame it. */\nfunction roleOfNote(\n melody: readonly NoteEvent[],\n index: number,\n ts: TimeSignature,\n pulse: number,\n): MelodyToneRole {\n const note = melody[index];\n const prev = melody[index - 1];\n const next = melody[index + 1];\n if (!note || !prev || !next) {\n return 'structural';\n }\n // A note separated from its neighbour by a rest is not led into or out of it,\n // and simultaneous onsets are cluster members rather than melodic neighbours.\n const intoNote = Math.abs(prev.startBeat + prev.durationBeat - note.startBeat);\n const outOfNote = Math.abs(note.startBeat + note.durationBeat - next.startBeat);\n if (intoNote > HUMANIZE_ADJACENCY || outOfNote > HUMANIZE_ADJACENCY) {\n return 'structural';\n }\n\n const weight = metricWeight(snapToPulse(note.startBeat, pulse), ts);\n const weightPrev = metricWeight(snapToPulse(prev.startBeat, pulse), ts);\n const weightNext = metricWeight(snapToPulse(next.startBeat, pulse), ts);\n // Subordinate: no stronger metrically and no longer than either neighbour, so\n // the ear hears the neighbours as the frame and this note as decoration.\n const subordinate =\n weight <= weightPrev &&\n weight <= weightNext &&\n note.durationBeat <= prev.durationBeat &&\n note.durationBeat <= next.durationBeat;\n // Leaning: an accented dissonance arrives on a stronger beat than the note it\n // comes from and hands over to a weaker resolution.\n const leaning = weight > weightPrev && weight >= weightNext;\n\n if (\n leaning &&\n prev.pitch === note.pitch &&\n note.pitch - next.pitch >= 1 &&\n isStep(note.pitch, next.pitch)\n ) {\n return 'suspension';\n }\n if (subordinate && isStep(prev.pitch, note.pitch) && isStep(note.pitch, next.pitch)) {\n if (sameDirection(prev.pitch, note.pitch, next.pitch)) {\n return 'passing';\n }\n if (prev.pitch === next.pitch) {\n return 'neighbor';\n }\n }\n if (\n subordinate &&\n note.pitch === next.pitch &&\n note.pitch !== prev.pitch &&\n weight < weightNext\n ) {\n return 'anticipation';\n }\n if (\n leaning &&\n isLeap(prev.pitch, note.pitch) &&\n isStep(note.pitch, next.pitch) &&\n !sameDirection(prev.pitch, note.pitch, next.pitch)\n ) {\n return 'appoggiatura';\n }\n if (\n subordinate &&\n isStep(prev.pitch, note.pitch) &&\n isLeap(note.pitch, next.pitch) &&\n !sameDirection(prev.pitch, note.pitch, next.pitch)\n ) {\n return 'escape';\n }\n return 'structural';\n}\n","/**\n * What the stages of the harmonizer share: the shapes they pass between them,\n * the defaults they fall back on, and the two questions about a key that more\n * than one of them asks.\n */\n\nimport { isMinorKey } from '../../analyze/functional/index.js';\nimport type { MeterLike, TimeSignature } from '../../core/meter/index.js';\nimport { beatsPerBar, pulseBeats } from '../../core/meter/index.js';\nimport { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\nimport type { KeyScale, NoteEvent } from '../../core/types.js';\nimport type { ChordQuality } from '../../theory/chord/index.js';\nimport { chordPitchClasses, makeChord } from '../../theory/chord/index.js';\nimport type { HarmonyRole } from '../../theory/harmony/index.js';\nimport type { KeyLike, ResolvedKey } from '../../theory/scale/index.js';\nimport { isScaleTone, majorKey, minorKey, scaleSystemOf } from '../../theory/scale/index.js';\nimport { leadingTonePcOf } from '../../theory/tendency/index.js';\nimport type { GenerationContextInput } from '../context/index.js';\nimport type { ChordSpan } from '../progression/index.js';\n\n/**\n * A melody note supplied to the harmonizer: a {@link NoteEvent}.\n *\n * @category Reharmonization\n */\nexport type MelodyNote = NoteEvent;\n/**\n * Options controlling {@link harmonizeMelody}.\n *\n * @category Reharmonization\n */\nexport type HarmonizePlacement = {\n /**\n * Move the melody into the key it is harmonized in. The search reports the\n * shift it chose in `transposeSemitones`; transposing the melody by that many\n * semitones puts it in the returned `key`, so melody and harmony always agree.\n *\n * Use it for a melody notated in one key that has to be sung or played in\n * another. It never changes register on its own — that is `octaveSearch`.\n *\n * @defaultValue false\n */\n transposeSearch: boolean;\n /**\n * Search octave placements as well, so a melody written too high or too low\n * is moved into a comfortable register. Octaves leave every pitch class where\n * it was, so this changes neither the key nor the chords.\n *\n * @defaultValue false\n */\n octaveSearch: boolean;\n};\n/**\n * Options controlling {@link harmonizeMelody}.\n *\n * Only `melody` is required; every other knob has the default a first call\n * wants.\n *\n * @category Reharmonization\n */\nexport type HarmonizeOptions = {\n /** The melody to harmonize, in ascending onset order. */\n melody: readonly MelodyNote[];\n /**\n * The key to harmonize in, or `'infer'` to estimate it from the melody's\n * pitch-class weighting — which is what a caller who has only a melody\n * wants, and so the default. A key name such as `'C major'` is read as that\n * key; `'infer'` names no key and always asks for the estimate.\n *\n * @defaultValue `'infer'`\n */\n key?: KeyLike | 'infer';\n /**\n * Length of each chord slot in beats. Harmonization places chords on a fixed\n * grid of this length — unlike {@link chordTimelineFromNotes}, which searches\n * for the boundaries an existing piece already implies. Any positive value is\n * honoured; a value fine enough to make the search explode is rejected by the\n * generation budget rather than rounded up.\n *\n * Left out, it is read from `ts`: half a bar where the half falls on a pulse,\n * and the whole bar where it does not, so a waltz changes chord on its\n * downbeats rather than across them. In 4/4 that is one chord per half bar.\n *\n * @defaultValue half a bar of `ts` — 2 in 4/4\n */\n harmonicRhythm?: number;\n /**\n * Time signature the melody is barred in. It weights the metric accents the\n * chords are chosen against and sets the default chord grid, so a waltz or a\n * jig is harmonized on its own beats rather than on a 4/4 reading of them.\n *\n * @defaultValue `4/4`\n */\n ts?: MeterLike;\n /**\n * How far beyond the key's own triads the chord vocabulary reaches:\n * `'diatonic'` uses only them, `'secondaryDominant'` adds the dominants that\n * tonicize each degree, and `'borrowed'` adds the parallel mode's chords too.\n *\n * Sugar for `ctx: { complexity: { harmonic } }` at the three dial positions\n * these names have always stood for; a context names how far the vocabulary\n * reaches instead of which of three steps it stops at, and wins where both\n * are given.\n *\n * @defaultValue `'diatonic'`\n */\n reharmonize?: 'diatonic' | 'secondaryDominant' | 'borrowed';\n /**\n * Whether to search transpositions and octave placements for the melody.\n * Both are off by default, so the melody is harmonized where it was written.\n *\n * @defaultValue both false\n */\n placement?: HarmonizePlacement;\n /**\n * Beats at which the melody's phrases end, so a longer line closes at each of\n * them instead of only at its end.\n *\n * Naming a beat asks for a close there, and three things follow from it. The\n * beat divides the chord grid, so the slot the phrase closes in ends where the\n * phrase does rather than running on into the next one. The harmony moves into\n * that slot — the chord under a named close is never the chord that was\n * already sounding, which is what makes the close audible as one. And the note\n * the phrase comes to rest on is read as a structural tone rather than as an\n * ornament of the next phrase's first note, so a phrase resting on the tonic\n * is harmonized by the tonic.\n *\n * Which cadence that close forms is still the melody's to decide: a phrase\n * coming to rest on the tonic over an approach that can carry the dominant\n * cadences authentically, and one whose approach cannot is harmonized by what\n * it sounds.\n *\n * The melody always closes where it ends, whatever this says; these are the\n * closes *inside* it. `phrasesFromTimeline` finds the phrases of a line\n * already labelled with chords, and its `endBeat` values are what this\n * expects, so a caller harmonizes a whole piece in one call rather than\n * harmonizing each phrase and joining the results.\n *\n * A boundary landing exactly on a chord-slot boundary closes the slot before\n * it — the beat a phrase ends on is where the next phrase begins.\n *\n * @defaultValue none — one close, at the end\n */\n phraseEnds?: readonly number[];\n /**\n * The generation context. Its `complexity.harmonic` is how far the chord\n * vocabulary reaches beyond the key's own triads — 0 uses them alone, 0.5 has\n * every secondary dominant, 1 the parallel mode's chords as well — and its\n * `seed` drives the deterministic tie-break perturbation.\n *\n * @defaultValue `{ seed: 0 }`\n */\n ctx?: GenerationContextInput;\n /**\n * Upper bound on the work this call may do. The search is one pass per\n * placement over every pair of candidate chords in every slot, so a long line\n * harmonized with `placement.transposeSearch` on is what needs this raised.\n *\n * @defaultValue {@link DEFAULT_GENERATION_BUDGET}\n */\n budget?: number;\n};\n/**\n * The chosen transpose, key, chord path, and per-note roles.\n *\n * @category Reharmonization\n */\nexport type HarmonizeResult = {\n /**\n * How far the melody was moved, in semitones. Transposing the melody by this\n * much puts it in `key`, which is the key the chords are in.\n */\n transposeSemitones: number;\n /**\n * The key the chords are in, whole.\n *\n * A key/scale alone says nothing about how it is written, so a result\n * carrying one would report whichever spelling its pitch classes read best\n * from, and an Ab minor handed in would come back out a G# minor. It matters\n * most under `key: 'infer'`, where this is the only account of which key was\n * chosen and a caller has nothing else to spell the chord spans from.\n */\n key: ResolvedKey;\n /** One span per chord change, each starting on a harmonic-rhythm boundary. */\n chords: ChordSpan[];\n /** Each melody note's role in the chord sounding under it, once chosen. */\n melodyRoles: { noteIndex: number; role: HarmonyRole }[];\n};\n\n/** One chord the search may place in a slot, with everything it is scored by. */\nexport type Candidate = {\n rootPc: number;\n quality: ChordQuality;\n degree?: number;\n secondaryDominant: boolean;\n targetDegree?: number;\n base: number;\n /**\n * The candidate's pitch classes, resolved once. The emission cost is\n * evaluated for every transpose, segment, and candidate, and the chord's\n * pitch classes depend on none of those.\n */\n pcs: number[];\n};\n/**\n * One note as a segment's emission term sees it: how much of the segment it\n * sounds in and whether it lands on an accent. Neither depends on the candidate\n * chord or on the transposition being tried, so both are found once.\n */\nexport type CostNote = {\n /** Index in the sounding melody. */\n index: number;\n /** Beats of the segment the note sounds in, floored so a short note still counts. */\n weight: number;\n /** Whether the note enters the segment on a metric accent. */\n strong: boolean;\n};\nexport type Segment = {\n startBeat: number;\n endBeat: number;\n /**\n * The segment's own length in beats. Every term of the cost table is a rate per\n * beat, and a phrase end divides the grid where the caller named it, so a slot\n * cut short is scored by what it is worth rather than by what the harmonic\n * rhythm would have made it worth.\n */\n beats: number;\n /** Every sounding note overlapping the segment, by index in the sounding melody. */\n noteIndices: number[];\n /**\n * The notes that drive the segment's emission cost: its structural tones,\n * falling back to all of its notes when ornaments are all it has. A chord slot\n * with no note left to explain would be chosen by chord flow alone.\n */\n costNotes: CostNote[];\n /** Total emission weight of `costNotes`: what covering the segment is worth. */\n weight: number;\n /**\n * The last of `costNotes`, which is the tone a phrase closes on. Held as an\n * index because the pitch it carries moves with the transposition being tried.\n */\n closingIndex?: number;\n};\nexport const FALLBACK: Candidate = {\n rootPc: 0,\n quality: 'maj',\n secondaryDominant: false,\n base: 0,\n pcs: chordPitchClasses(makeChord(0, 'maj')),\n};\n/**\n * Chord slot length assumed when the caller names none: half a bar of the metre\n * being harmonized in, or the whole bar where half of it does not fall on a\n * pulse.\n *\n * A grid is anchored at the melody's first slot boundary and repeats, so a slot\n * that is not a whole number of pulses long puts chord changes inside the bar\n * and drifts further from the barline with every repeat. In 3/4 half a bar is a\n * beat and a half; taking the bar instead is what makes a waltz change chord on\n * its downbeats.\n */\nexport function defaultHarmonicRhythm(ts: TimeSignature): number {\n const bar = beatsPerBar(ts);\n const half = bar / 2;\n return half % pulseBeats(ts) === 0 ? half : bar;\n}\n/** Placement assumed when the caller names none: harmonize the melody as written. */\nexport const DEFAULT_PLACEMENT: HarmonizePlacement = {\n transposeSearch: false,\n octaveSearch: false,\n};\n/** Default meter used to weight metric accents when none is supplied. */\nexport const DEFAULT_METER: TimeSignature = { numerator: 4, denominator: 4 };\n/**\n * Magnitude of the seed-driven per-candidate perturbation. Kept far below the\n * smallest real cost difference (transition/emission terms are on the order of\n * 0.1 and up), so it can only decide between candidates or paths whose costs are\n * otherwise exactly equal. The seed therefore breaks ties deterministically and\n * never overrides melody fit or functional flow.\n */\nexport const TIE_BREAK_JITTER = 1e-6;\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 isMinorKey(key) && scaleSystemOf(key) === 'common-practice';\n}\n/**\n * Whether a pitch belongs to the key, counting the raised seventh of a minor\n * key.\n *\n * A minor key cadences through its harmonic-minor dominant, so its leading tone\n * is evidence for the key rather than a note foreign to it. Every term that asks\n * whether a note is in the key reads it this way — key inference, the emission\n * term's out-of-key surcharge, and the transposition search — so the dominant a\n * minor phrase closes with is not paid for twice. A modal key gets no such\n * allowance: its seventh is a degree, not an accidental waiting to be raised.\n */\nexport function isKeyTone(pitch: number, key: KeyScale): boolean {\n return (\n isScaleTone(pitch, key) ||\n (cadencesThroughRaisedSeventh(key) && pitchClass(pitch) === leadingTonePcOf(key))\n );\n}\n/**\n * Estimate the best-fit key from a melody's pitch-class weighting.\n *\n * Both major and natural-minor tonics are scored, so a minor melody is\n * recognized as its own minor key rather than being folded into the relative\n * major (which shares identical pitch-class content). Each in-scale note adds\n * its duration weight; notes matching the candidate tonic add a further\n * half-weight, and that tonic emphasis is what separates a minor key from its\n * relative major. For minor candidates the raised leading tone (the\n * harmonic-minor seventh) also counts as in-scale, so cadential leading tones do\n * not penalize the true minor key. Ties are broken toward the earlier candidate,\n * and major is scored before minor at each tonic.\n */\nexport function inferKey(melody: readonly MelodyNote[]): KeyScale {\n let best: KeyScale = majorKey(0);\n let bestScore = Number.NEGATIVE_INFINITY;\n const candidates: KeyScale[] = [];\n for (let tonic = 0; tonic < 12; tonic += 1) {\n candidates.push(majorKey(tonic), minorKey(tonic));\n }\n for (const key of candidates) {\n const tonic = pitchClass(key.rootPc);\n let score = 0;\n for (const n of melody) {\n const w = Math.max(0.25, n.durationBeat);\n const pc = pitchClass(n.pitch);\n if (isKeyTone(n.pitch, key)) {\n score += w;\n }\n if (pc === tonic) {\n score += 0.5 * w;\n }\n }\n if (score > bestScore) {\n bestScore = score;\n best = key;\n }\n }\n return best;\n}\n","/**\n * What a reading of the melody costs.\n *\n * Three terms, one per kind of evidence: what the melody's own notes say about\n * the chord under them, what the move from the chord before says about the\n * pair, and what a phrase ending asks of the chord that closes it. The search\n * adds them up; nothing here knows how the search runs.\n */\n\nimport { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport type { Candidate, MelodyNote, Segment } from './internal.js';\nimport { isKeyTone } from './internal.js';\nimport { classifyMelodyTones } from './nct.js';\n\n/**\n * The cost table, in one place because only the *ratios* between these terms\n * decide anything. Every term is on the same order of magnitude, so covering one\n * more melody note can never outweigh a functional progression: a chord that\n * leaves an accented structural tone unexplained pays about what a cadence or a\n * dominant resolution is worth, not several times more.\n *\n * **Every term is a rate per beat of music**, not per chord or per slot. The\n * emission term is charged per structural note by the beats it sounds; the\n * vocabulary term by the beats the slot's structural notes are worth; the\n * transition and phrase-end terms by the length of the slot they apply to. That\n * is what keeps the balance between melody fit and functional flow the same\n * whichever harmonic rhythm the caller asks for: with per-chord constants,\n * halving the slot length would double the total root-motion reward paid over\n * the same melody while its emission cost stayed put, and a fine harmonic rhythm\n * would spend chords the melody never asked for.\n *\n * ```text\n * emission per structural note, weighted by the beats it sounds in the segment\n * non-chord tone on a strong beat +4\n * non-chord tone on a weak beat +1\n * note outside the key, on top of the above +0.5\n * vocabulary per beat the slot's structural notes are worth\n * root above the tonic, by tonal weight: tonic 0,\n * dominant 0.1, subdominant 0.15, supertonic,\n * submediant and subtonic 0.3, mediant 0.45,\n * a remote root 0.9\n * diminished or augmented triad, wherever it sits 0.9\n * secondary dominant +0.5\n * borrowed chord +0.5\n * transition per beat of the slot it enters\n * changing chord at all +0.8\n * descending-fifth root motion -0.4\n * ascending-fifth root motion 0\n * root motion by step or third +0.15..0.2\n * root motion by tritone +0.4\n * V -> I -0.3\n * secondary dominant going anywhere but its target +1.0\n * secondary dominant of the chord before it +0.8\n * phrase end per beat of the phrase-final slot, when the melody's own\n * closing tone is the tonic\n * lands on the tonic -7\n * approached from V, on top of the above -0.5\n * placement per candidate placement of the melody\n * note outside the target key, per beat +3.0\n * semitone outside the comfortable range +0.001\n * ```\n *\n * Ornamental tones are removed before the emission term is computed (see\n * {@link classifyMelodyTones}), so these numbers weigh structural tones only.\n */\nconst NON_CHORD_TONE_STRONG = 4;\nconst NON_CHORD_TONE_WEAK = 1;\nconst NON_SCALE_TONE = 0.5;\n/**\n * What changing chord costs at all, before the motion is judged. It is more than\n * the deepest discount any motion earns — a dominant resolving to its tonic —\n * so no chord change is ever cheaper than holding the chord already sounding.\n * Without it the discounts form a cycle a search can ride for free: I - IV - V -\n * I pays less than staying on I, and the melody has no say in it, so a harmonic\n * rhythm fine enough to fit the cycle in fills the whole melody with it.\n * Functional flow still orders the changes the melody does ask for, which is all\n * it is there to do.\n */\nconst CHORD_CHANGE = 0.8;\nconst DESCENDING_FIFTH = -0.4;\nconst ASCENDING_FIFTH = 0;\nconst DOMINANT_TO_TONIC = -0.3;\n/**\n * What a secondary dominant pays for not reaching the degree it tonicizes.\n * Reaching it earns no separate discount: the descending fifth an applied\n * dominant makes into its target is already priced, and paying it twice made\n * borrowing a chromatic chord and resolving it cheaper than staying in the key.\n */\nconst SECONDARY_UNRESOLVED = 1;\nconst SECONDARY_AFTER_TARGET = 0.8;\n/**\n * What other root motion costs, by the shorter distance in semitones between\n * the two roots: a third or a step asks a little, a tritone asks a lot. Motion\n * by a fifth is not priced here — it is the motion tonal harmony is built from\n * and is judged by direction instead, the descending one being the one a\n * progression is driven by.\n */\nconst ROOT_MOTION_COST = [0, 0.15, 0.15, 0.2, 0.2, 0, 0.4];\n/**\n * What closing a phrase on the tonic is worth, per beat of the closing slot. It\n * is worth more than everything that slot can otherwise decide — covering every\n * one of its structural tones, the widest vocabulary and root-motion difference\n * between two candidates — because a phrase whose melody has come to rest on the\n * tonic is closed, and a chord that covers one more note on a stronger beat does\n * not reopen it. The bonus is paid only where the melody's own closing tone is\n * the tonic, so a phrase ending anywhere else is still harmonized by what it\n * sounds.\n */\nconst PHRASE_TONIC = -7;\nconst PHRASE_AUTHENTIC = -0.5;\nexport const OUT_OF_KEY_PER_BEAT = 3;\n/**\n * Melody-fit cost of a candidate over a segment's structural notes.\n *\n * Only the segment's `costNotes` are charged: an ornament is explained by the\n * melodic figure it forms, not by the chord under it, so making the chord cover\n * it would be paying twice. Each note is weighted by the portion of its duration\n * that overlaps the segment, so a note sustained across a boundary contributes\n * to every segment it sounds in rather than only the one it starts in. The\n * candidate's own vocabulary cost is charged by the same total weight, so what a\n * chord costs to use and what it costs to leave a note unexplained are measured\n * against the same amount of music.\n */\nexport function emissionCost(\n seg: Segment,\n cand: Candidate,\n melody: readonly MelodyNote[],\n key: KeyScale,\n): number {\n const pcs = cand.pcs;\n let cost = cand.base * seg.weight;\n for (const { index, weight, strong } of seg.costNotes) {\n const note = melody[index];\n if (!note || pcs.includes(pitchClass(note.pitch))) {\n continue;\n }\n cost += (strong ? NON_CHORD_TONE_STRONG : NON_CHORD_TONE_WEAK) * weight;\n if (!isKeyTone(note.pitch, key)) {\n cost += NON_SCALE_TONE * weight;\n }\n }\n return cost;\n}\n/** Whether a candidate is the key's dominant, in a quality that can act as one. */\nfunction isDominantOf(cand: Candidate, tonicPc: number): boolean {\n return cand.rootPc === (tonicPc + 7) % 12 && (cand.quality === 'maj' || cand.quality === 'dom7');\n}\n/**\n * Functional-flow cost of moving from one candidate chord to the next, per beat\n * of the slot the move enters.\n *\n * The caller scales it by that slot's length: root motion is worth what the\n * music it spans is worth, not what one grid boundary is worth, so dividing the\n * same melody into finer slots cannot buy more flow reward than the melody's\n * emission cost can answer for.\n */\nexport function transitionCost(prev: Candidate, cur: Candidate, tonicPc: number): number {\n let cost = prev.rootPc === cur.rootPc && prev.quality === cur.quality ? 0 : CHORD_CHANGE;\n const down = (prev.rootPc - cur.rootPc + 12) % 12;\n if (down === 7) {\n cost += DESCENDING_FIFTH;\n } else if (down === 5) {\n cost += ASCENDING_FIFTH;\n } else {\n cost += ROOT_MOTION_COST[Math.min(down, 12 - down)] ?? 0;\n }\n if (isDominantOf(prev, tonicPc) && cur.rootPc === tonicPc) {\n cost += DOMINANT_TO_TONIC;\n }\n if (prev.secondaryDominant && cur.degree !== prev.targetDegree) {\n cost += SECONDARY_UNRESOLVED;\n }\n // Reaching a chord's own dominant from that chord leads straight back where it\n // came from. Once it is cheaper to tonicize than to stay put, a search with no\n // memory will otherwise rock between the two forever.\n if (cur.secondaryDominant && prev.degree !== undefined && prev.degree === cur.targetDegree) {\n cost += SECONDARY_AFTER_TARGET;\n }\n return cost;\n}\n/**\n * Bonus for closing a phrase on the tonic, over the whole of what that slot is\n * worth.\n *\n * A melody that has come to rest on the tonic has closed, so the chord under it\n * is the tonic — whatever the rest of the slot sounds, and whatever chord the\n * slot before it settled on. The bonus is therefore weighed against everything\n * that slot can otherwise decide, which is why it is scaled by the slot's own\n * emission weight rather than being a constant one accented note can outbid.\n *\n * It is paid only where the melody's own closing tone is the tonic: a phrase\n * that comes to rest anywhere else — on the third, on the leading tone, on a\n * degree the tonic chord cannot support — has not closed, and is harmonized by\n * what it sounds.\n *\n * `prev` is the chord approaching the close, or null when the phrase is a single\n * chord long and there is nothing to approach it from.\n */\nexport function phraseEndBonus(\n prev: Candidate | null,\n cur: Candidate,\n tonicPc: number,\n seg: Segment,\n melody: readonly MelodyNote[],\n): number {\n if (cur.rootPc !== tonicPc || seg.closingIndex === undefined) {\n return 0;\n }\n const closing = melody[seg.closingIndex];\n if (!closing || pitchClass(closing.pitch) !== tonicPc) {\n return 0;\n }\n const weight = Math.max(seg.weight, seg.beats);\n return (\n (PHRASE_TONIC + (prev !== null && isDominantOf(prev, tonicPc) ? PHRASE_AUTHENTIC : 0)) * weight\n );\n}\n","/**\n * How well a key suits the melody it is being asked to harmonize.\n *\n * Two readings, and they answer different questions: whether the line lies\n * where it can be sung or played, and whether its notes belong to the key at\n * all. A transposition search weighs both, since moving a melody changes where\n * it sits as much as it changes what it is written in.\n */\n\nimport type { KeyScale } from '../../core/types.js';\nimport { OUT_OF_KEY_PER_BEAT } from './cost.js';\nimport type { MelodyNote } from './internal.js';\nimport { isKeyTone } from './internal.js';\n\n/** Comfortable melodic range (MIDI) and the per-semitone cost of leaving it. */\nconst COMFORT_LOW = 55;\nconst COMFORT_HIGH = 79;\nconst TESSITURA_WEIGHT = 0.001;\n/**\n * Register cost of a placed melody: a small penalty for notes pushed outside a\n * comfortable range. Kept far below emission/transition costs so it only breaks\n * ties between otherwise-equal octave placements.\n */\nexport function tessituraCost(melody: readonly MelodyNote[]): number {\n let cost = 0;\n for (const n of melody) {\n if (n.pitch < COMFORT_LOW) {\n cost += (COMFORT_LOW - n.pitch) * TESSITURA_WEIGHT;\n } else if (n.pitch > COMFORT_HIGH) {\n cost += (n.pitch - COMFORT_HIGH) * TESSITURA_WEIGHT;\n }\n }\n return cost;\n}\n/**\n * How badly a placed melody sits in the key it is about to be harmonized in.\n *\n * This is what makes a transposition search mean \"move the melody into this\n * key\": a shift is judged by the melody's own key membership, not by how many\n * chord tones a chord path can be talked into covering. The winning shift\n * therefore always agrees with the key the result reports.\n */\nexport function keyFitCost(melody: readonly MelodyNote[], key: KeyScale): number {\n let cost = 0;\n for (const n of melody) {\n if (!isKeyTone(n.pitch, key)) {\n cost += Math.max(0.25, n.durationBeat) * OUT_OF_KEY_PER_BEAT;\n }\n }\n return cost;\n}\n","/**\n * The search itself: one pass of dynamic programming over the segments.\n *\n * Every segment is scored against every candidate chord, and every pair of\n * consecutive segments against the move between them, so the cheapest reading\n * of the whole line is the cheapest path through that table. What the terms\n * mean is `cost.ts`; this module only adds them up and remembers the path.\n */\n\nimport { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport { emissionCost, phraseEndBonus, transitionCost } from './cost.js';\nimport type { Candidate, MelodyNote, Segment } from './internal.js';\nimport { FALLBACK } from './internal.js';\n\n/** Whether two candidates are the same harmony, which is what holding a chord means. */\nfunction sameHarmony(a: Candidate, b: Candidate): boolean {\n return a.rootPc === b.rootPc && a.quality === b.quality;\n}\n/** Run one Viterbi harmonization pass over a fixed melody and key. */\nexport function harmonizeOnce(\n melody: readonly MelodyNote[],\n key: KeyScale,\n candidates: Candidate[],\n segments: Segment[],\n jitter: number[],\n phraseEnds: ReadonlySet<number>,\n articulated: ReadonlySet<number>,\n): { cost: number; path: number[] } {\n const tonicPc = pitchClass(key.rootPc);\n const n = candidates.length;\n const candAt = (i: number): Candidate => candidates[i] ?? FALLBACK;\n const seg0 = segments[0];\n\n // A phrase one segment long has no approach chord, so its close is judged on\n // the chord alone.\n let dp = candidates.map(\n (c, ci) =>\n (seg0 ? emissionCost(seg0, c, melody, key) : 0) +\n (seg0 && phraseEnds.has(0) ? phraseEndBonus(null, c, tonicPc, seg0, melody) : 0) +\n (jitter[ci] ?? 0),\n );\n const back: number[][] = [];\n\n for (let s = 1; s < segments.length; s += 1) {\n const seg = segments[s];\n if (!seg) {\n continue;\n }\n const closes = phraseEnds.has(s);\n // A beat the caller named as a phrase end is a cadence point, and a cadence\n // is harmonic motion into the close: the chord under it is not the chord\n // that was already sounding. The melody's own close is not held to this —\n // naming a beat is what asks for it — so a call that names none is scored\n // exactly as it was.\n const articulates = articulated.has(s);\n const next: number[] = [];\n const ptr: number[] = [];\n for (let c = 0; c < n; c += 1) {\n let best = Number.POSITIVE_INFINITY;\n let bestPrev = 0;\n for (let p = 0; p < n; p += 1) {\n if (articulates && sameHarmony(candAt(p), candAt(c))) {\n continue;\n }\n // The cadence bonus is part of the step into a phrase-final segment, not\n // an afterthought added to the finished path, so the approach chord is\n // chosen for the cadence it makes.\n const cost =\n (dp[p] ?? Number.POSITIVE_INFINITY) +\n transitionCost(candAt(p), candAt(c), tonicPc) * seg.beats +\n (closes ? phraseEndBonus(candAt(p), candAt(c), tonicPc, seg, melody) : 0);\n if (cost < best) {\n best = cost;\n bestPrev = p;\n }\n }\n next[c] = best + emissionCost(seg, candAt(c), melody, key) + (jitter[c] ?? 0);\n ptr[c] = bestPrev;\n }\n dp = next;\n back.push(ptr);\n }\n\n let bestCost = Number.POSITIVE_INFINITY;\n let bestEnd = 0;\n for (let c = 0; c < n; c += 1) {\n const total = dp[c] ?? Number.POSITIVE_INFINITY;\n if (total < bestCost) {\n bestCost = total;\n bestEnd = c;\n }\n }\n\n const path: number[] = new Array<number>(segments.length).fill(bestEnd);\n for (let s = segments.length - 2; s >= 0; s -= 1) {\n const ptr = back[s];\n const nextIdx = path[s + 1] ?? bestEnd;\n path[s] = ptr ? (ptr[nextIdx] ?? bestEnd) : bestEnd;\n }\n return { cost: bestCost, path };\n}\n","/**\n * The spans the harmony may change on, and where the melody's notes fall in\n * them.\n *\n * A chord change lands on the grid the harmonic rhythm names, cut again at\n * every beat the caller marked as a phrase end. Everything here is about that\n * grid: where its boundaries are, which notes close on them, and which of them\n * a phrase ends in.\n */\n\nimport { BEAT_EPS } from '../../analyze/adjacency.js';\nimport type { MeterLike } from '../../core/meter/index.js';\nimport { isStrongBeat } from '../../core/meter/index.js';\nimport { assertGenerationBudget } from '../../core/validation/index.js';\nimport type { ChordSpan } from '../progression/index.js';\nimport type { MelodyNote, Segment } from './internal.js';\nimport { classifyMelodyTones } from './nct.js';\n\n/**\n * The beats the chord grid may change on: the harmonic rhythm's own boundaries,\n * plus every named phrase end that falls inside the melody.\n *\n * A named end lands on a boundary that is already there for most callers, and\n * naming one that is not adds it rather than moving the grid, so the slots after\n * a phrase end stay where the harmonic rhythm put them.\n */\nexport function gridBounds(\n segmentStart: number,\n hr: number,\n gridCount: number,\n ends: readonly number[],\n): number[] {\n const bounds: number[] = [];\n for (let s = 0; s <= gridCount; s += 1) {\n bounds.push(segmentStart + s * hr);\n }\n const last = segmentStart + gridCount * hr;\n for (const end of ends) {\n if (end > segmentStart + BEAT_EPS && end < last - BEAT_EPS) {\n bounds.push(end);\n }\n }\n bounds.sort((a, b) => a - b);\n return bounds.filter(\n (beat, index) => index === 0 || beat - (bounds[index - 1] ?? beat) > BEAT_EPS,\n );\n}\n/**\n * The note each named phrase closes on: the last one to begin before the beat\n * the phrase ends at.\n */\nexport function closingNoteIndices(\n spans: readonly { startBeat: number; endBeat: number }[],\n ends: readonly number[],\n): Set<number> {\n const closing = new Set<number>();\n for (const end of ends) {\n let index = -1;\n let start = Number.NEGATIVE_INFINITY;\n for (const [i, span] of spans.entries()) {\n if (span.startBeat < end - BEAT_EPS && span.startBeat >= start) {\n start = span.startBeat;\n index = i;\n }\n }\n if (index >= 0) {\n closing.add(index);\n }\n }\n return closing;\n}\n/** The index of the segment a beat sounds in: the last boundary at or before it. */\nexport function segmentIndexAt(bounds: readonly number[], beat: number): number {\n let low = 0;\n let high = bounds.length - 2;\n let found = 0;\n while (low <= high) {\n const mid = (low + high) >> 1;\n if ((bounds[mid] ?? 0) <= beat) {\n found = mid;\n low = mid + 1;\n } else {\n high = mid - 1;\n }\n }\n return found;\n}\n/**\n * Collapse runs of the same chord into one span.\n *\n * The harmonic rhythm sets the grid the search may change chords on, not a\n * requirement to change on every slot; repeating a slot's chord is the same\n * harmony held longer, and reporting it once is what a chart shows.\n */\nexport function mergeRepeats(spans: ChordSpan[]): ChordSpan[] {\n const merged: ChordSpan[] = [];\n for (const span of spans) {\n const prev = merged.at(-1);\n if (\n prev &&\n prev.rootPc === span.rootPc &&\n prev.quality === span.quality &&\n prev.degree === span.degree &&\n prev.secondaryDominant === span.secondaryDominant\n ) {\n continue;\n }\n merged.push(span);\n }\n return merged;\n}\n/**\n * Segments that close and therefore have to cadence.\n *\n * The melody handed to the harmonizer closes at its end, so its last segment\n * always cadences. A caller who knows where the phrases of a longer line fall —\n * `phrasesFromTimeline` finds them — names their ends as beats, and the segment\n * each end falls in cadences too, so one call harmonizes the whole line instead\n * of the caller harmonizing each phrase and joining the results. Every cadence\n * term reads the boundaries from here.\n *\n * The named ends are returned apart from the melody's own close because they ask\n * for more than it does: naming a beat asks for a cadence there, and a cadence\n * is a chord change into the close.\n */\nexport function phraseEndSegments(\n segments: readonly Segment[],\n ends: readonly number[],\n): { closing: Set<number>; named: Set<number> } {\n const closing = new Set(segments.length > 0 ? [segments.length - 1] : []);\n const named = new Set<number>();\n for (const end of ends) {\n // The phrase closes in the segment its last beat sounds in, which is the\n // last segment beginning before that beat — a boundary landing exactly on a\n // segment start belongs to the phrase that ended, not the one starting.\n let index = -1;\n for (let s = 0; s < segments.length; s += 1) {\n if ((segments[s]?.startBeat ?? 0) < end - BEAT_EPS) {\n index = s;\n }\n }\n if (index >= 0) {\n closing.add(index);\n named.add(index);\n }\n }\n return { closing, named };\n}\n\n/** A melody note as the search reads it: snapped to the pulse it plays. */\nexport type MelodySpan = {\n startBeat: number;\n endBeat: number;\n};\n\n/**\n * Cut the melody into the spans a chord may change on, and fill each with what\n * sounds in it.\n *\n * Three steps that only make sense together: the grid the harmonic rhythm and\n * the caller's phrase ends name, the notes each of its slots holds, and which\n * of those notes a chord is actually answerable for — the structural ones,\n * since a passing note is an ornament of the harmony rather than a statement\n * of it. Written out inside the entry point, none of the three could be put to\n * a question of its own.\n *\n * @param spans The melody's notes, snapped to the pulse.\n * @param melody The same notes, for the ornament classifier.\n * @param ts The meter the melody is read in, in any form a caller writes one.\n * @param grid Where the melody starts and ends, and how often the chord may\n * change.\n * @param phraseEnds Beats the caller named as phrase closes.\n * @param budget Upper bound on the work this may do.\n * @returns The segments, the grid boundaries they were cut on, the beat that\n * grid is anchored at, and the phrase ends falling inside the melody.\n */\nexport function buildSegments(\n spans: readonly MelodySpan[],\n melody: readonly MelodyNote[],\n ts: MeterLike,\n grid: { start: number; end: number; harmonicRhythm: number },\n phraseEnds: readonly number[],\n budget?: number,\n): { segments: Segment[]; innerEnds: number[]; bounds: number[]; segmentStart: number } {\n const melodyStart = grid.start;\n const melodyEnd = grid.end;\n const hr = grid.harmonicRhythm;\n const segmentStart = Math.floor(melodyStart / hr) * hr;\n const gridCount = Math.max(1, Math.ceil((melodyEnd - segmentStart) / hr));\n assertGenerationBudget(gridCount, 'harmonic segments', budget);\n // Only a beat inside the melody names a close inside it. A beat at or beyond\n // the melody's end names the close the melody already has, and one at or\n // before the grid's first boundary names no slot at all, so neither may reach\n // the grid, the cadence points, or the tones read as structural: passing the\n // end of the last phrase along with the ends of the others — which is what\n // `phrasesFromTimeline` returns — harmonizes the line exactly as naming the\n // inner closes alone does.\n const innerEnds = phraseEnds.filter(\n (end) => end > segmentStart + BEAT_EPS && end < melodyEnd - BEAT_EPS,\n );\n // The grid the search may change chords on: the harmonic rhythm, anchored at\n // the melody's first slot boundary, divided again at every beat the caller\n // named as a phrase end. A named end falling inside a slot cuts it, so the\n // slot a phrase closes in ends where the phrase does instead of running on\n // into the next one, and the grid then resumes on its own boundaries — which\n // is what keeps the chord changes after the boundary on the barline.\n const bounds = gridBounds(segmentStart, hr, gridCount, innerEnds);\n const segCount = bounds.length - 1;\n assertGenerationBudget(segCount, 'harmonic segments', budget);\n const segments: Segment[] = Array.from({ length: segCount }, (_, s) => {\n const startBeat = bounds[s] ?? segmentStart;\n const endBeat = bounds[s + 1] ?? startBeat + hr;\n return {\n startBeat,\n endBeat,\n beats: endBeat - startBeat,\n noteIndices: [],\n costNotes: [],\n weight: 0,\n };\n });\n // Associate each note only with the windows it actually spans: each note is\n // visited once per window it covers, and no temporary note objects are\n // allocated.\n let memberships = 0;\n for (const [index, span] of spans.entries()) {\n const first = segmentIndexAt(bounds, span.startBeat);\n let lastExclusive = first;\n while (\n lastExclusive < segCount &&\n (bounds[lastExclusive] ?? 0) < span.endBeat - Number.EPSILON\n ) {\n lastExclusive += 1;\n }\n memberships += Math.max(0, lastExclusive - first);\n assertGenerationBudget(memberships, 'note-to-segment memberships', budget);\n for (let segment = first; segment < lastExclusive; segment += 1) {\n segments[segment]?.noteIndices.push(index);\n }\n }\n\n // Classify the ornaments before any chord exists, and keep only the structural\n // tones in each slot's emission term. Transposing a melody moves every note\n // alike, so the figures are the same at every placement and are found once —\n // and so is the metric weight of each note in each slot it sounds in.\n const tones = classifyMelodyTones(melody, ts);\n // The classifier reads a note against the notes on either side of it, and\n // knows nothing of phrases: the melody's own last note is structural because\n // nothing follows it, but the note closing a phrase inside the line has the\n // next phrase's first note after it and is heard as an ornament of it — a\n // close on the tonic between two supertonics reads as a lower neighbour. A\n // phrase's last note is structural for the same reason the melody's last note\n // is, so the closes the caller named are put back.\n const phraseClosingNotes = closingNoteIndices(spans, innerEnds);\n for (const segment of segments) {\n const structural = segment.noteIndices.filter(\n (idx) => tones[idx]?.ornamental !== true || phraseClosingNotes.has(idx),\n );\n const costIndices = structural.length > 0 ? structural : segment.noteIndices;\n let closingStart = Number.NEGATIVE_INFINITY;\n for (const index of costIndices) {\n const span = spans[index];\n if (!span) {\n continue;\n }\n const overlapStart = Math.max(span.startBeat, segment.startBeat);\n const overlap = Math.min(span.endBeat, segment.endBeat) - overlapStart;\n if (overlap <= 0) {\n continue;\n }\n const weight = Math.max(0.25, overlap);\n segment.costNotes.push({ index, weight, strong: isStrongBeat(overlapStart, ts) });\n segment.weight += weight;\n // The tone a phrase closes on is the last one to start, which is not the\n // last one listed when a note held from earlier is still sounding under it.\n if (span.startBeat >= closingStart) {\n closingStart = span.startBeat;\n segment.closingIndex = index;\n }\n }\n }\n return { segments, innerEnds, bounds, segmentStart };\n}\n","import {\n isDiatonic,\n parallelKey,\n type TonicizableDegree,\n tonicizableDegrees,\n} from '../../analyze/functional/index.js';\nimport { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport type { Chord, ChordQuality } from '../../theory/chord/index.js';\nimport { chordPitchClasses, diatonicTriad, makeChord } from '../../theory/chord/index.js';\nimport { scaleTonesInDegreeOrder } from '../../theory/scale/index.js';\nimport { heptatonicFrameOf } from '../../theory/tendency/index.js';\nimport type { Candidate, HarmonizeOptions } from './internal.js';\nimport { cadencesThroughRaisedSeventh } from './internal.js';\n\n/**\n * What a root costs to build a chord on, by the semitones it sits above the\n * tonic: the tonic is free, the other primary triads are nearly so, and the\n * further a root sits from them the more evidence the melody has to supply.\n * Without it every triad sharing a melody note is equally good and the search\n * wanders through the ones that chain by fifths.\n *\n * It is read by interval rather than by the ordinal a degree occupies in the\n * scale, so a root carries the tonal weight of the function it actually has:\n * the subdominant costs what a subdominant costs whether the key spells it IV\n * or iv, and the subtonic a minor key closes plagally through is not charged\n * what a major key's leading-tone triad is charged for standing seventh in the\n * list. Roots no diatonic degree of a common mode reaches — the flat\n * supertonic, the tritone, the raised leading tone — are remote, and cost what\n * a chord the melody has to spell outright costs.\n */\nconst ROOT_WEIGHT_BY_SEMITONE = [0, 0.9, 0.3, 0.45, 0.45, 0.15, 0.9, 0.1, 0.3, 0.3, 0.3, 0.9];\n/**\n * What a triad costs for its own instability, wherever in the key it sits. A\n * diminished or augmented triad in root position is unstable as a harmony, not\n * because of the degree it happens to fall on, so the melody has to sound the\n * note only that chord explains before the search will spend it — and the same\n * triad costs the same in every key that holds it.\n */\nconst UNSTABLE_TRIAD = 0.9;\n/** The triad qualities {@link UNSTABLE_TRIAD} is charged for. */\nconst UNSTABLE_QUALITIES: ReadonlySet<ChordQuality> = new Set<ChordQuality>([\n 'dim',\n 'aug',\n 'dim7',\n 'm7b5',\n 'aug7',\n]);\n/**\n * What using one of the key's own chords costs, standing for how much tonal\n * weight it carries.\n *\n * The price is a function of what the chord *is* in the key — where its root\n * sits above the tonic, and what the chord itself is — and never of the ordinal\n * position its degree occupies in the scale. A mode that spells its second\n * degree as a diminished triad therefore pays for a diminished triad rather\n * than inheriting what a minor supertonic costs, and one whose seventh degree\n * is a major triad pays for a subtonic rather than for a leading-tone triad.\n */\nfunction vocabularyBase(rootPc: number, quality: ChordQuality, tonicPc: number): number {\n const above = (pitchClass(rootPc) - tonicPc + 12) % 12;\n const root = ROOT_WEIGHT_BY_SEMITONE[above] ?? UNSTABLE_TRIAD;\n return UNSTABLE_QUALITIES.has(quality) ? Math.max(root, UNSTABLE_TRIAD) : root;\n}\n/**\n * What a chord from outside the key's own triads costs — more than any of them,\n * and more than the flow reward reaching it and leaving it can repay. A chromatic\n * chord is therefore never reached by chord flow alone: the melody has to sound\n * the note only that chord explains, which is worth several times as much.\n * Widening the vocabulary adds the chords the melody asks for and leaves a\n * melody with no accidental in it where it was.\n */\nconst SECONDARY_DOMINANT_BASE = 0.5;\nconst BORROWED_BASE = 0.5;\n/**\n * The degrees a secondary dominant conventionally tonicizes, in the order the\n * candidate list has always held them.\n */\nconst SECONDARY_DOMINANT_TARGETS = [2, 4, 5, 6];\n/**\n * Those degrees the key at hand can actually make a local tonic.\n *\n * Nothing tonicizes a diminished triad, so a degree whose own triad spans no\n * perfect fifth is no target: the supertonic of a minor key is diminished, and\n * a dominant seventh placed in front of it is a chord the analysis layer refuses\n * to read as applied. The degrees come from the same predicate that layer names\n * its targets with, so what the generator marks as a secondary dominant is what\n * the numeral can be written for.\n */\nfunction secondaryDominantTargets(key: KeyScale): TonicizableDegree[] {\n const tonicizable = new Map(\n tonicizableDegrees(key).map((degree) => [degree.degreeNumber, degree] as const),\n );\n return SECONDARY_DOMINANT_TARGETS.map((degree) => tonicizable.get(degree)).filter(\n (degree): degree is TonicizableDegree => degree !== undefined,\n );\n}\n/**\n * The order those dominants open up in as the harmonic dial rises, by the\n * degree each tonicizes: the dominant's own dominant is the one an arranger\n * reaches for first, then the relative minor's, and the subdominant's last.\n * Kept apart from {@link SECONDARY_DOMINANT_TARGETS} so that widening the\n * vocabulary adds to the candidate list without reordering it.\n */\nconst SECONDARY_DOMINANT_ENTRY_ORDER = [5, 6, 2, 4];\n/**\n * The order the parallel mode's chords open up in as the dial rises, keyed by\n * the semitone each root stands above the tonic: the minor subdominant first,\n * then the flat-side major triads a pop arranger reaches for, and the chords\n * that displace the tonic itself — the minor tonic and the diminished\n * supertonic — last. Kept apart from the vocabulary for the reason\n * {@link SECONDARY_DOMINANT_ENTRY_ORDER} is, and read as a preference rather\n * than a filter, so an offset it does not name still opens, behind the ones it\n * does.\n */\nconst BORROWED_ENTRY_ORDER = [5, 10, 8, 3, 7, 2, 0];\n/**\n * The dial position at which the last secondary dominant has entered. Above it\n * the parallel mode's chords start arriving, so the two families open one after\n * the other rather than at once.\n */\nconst SECONDARY_DOMINANT_BAND = 0.5;\n/** Every named reharmonization strength, in dial order. */\nexport const REHARMONIZE_STRENGTHS = ['diatonic', 'secondaryDominant', 'borrowed'] as const;\n/** Where each named reharmonization strength sits on the harmonic dial. */\nexport const REHARMONIZE_DIAL: Record<NonNullable<HarmonizeOptions['reharmonize']>, number> = {\n diatonic: 0,\n secondaryDominant: SECONDARY_DOMINANT_BAND,\n borrowed: 1,\n};\n/**\n * How many members of a vocabulary family a dial fraction opens: none at or\n * below 0, all at 1, and one more each time the fraction crosses another\n * `1 / size` of the way up. That is what makes the dial continuous — a small\n * move opens one more chord rather than a whole family — while leaving the\n * family complete at the top of its band, so the named strengths reach exactly\n * the vocabulary they always did.\n */\nfunction admittedCount(fraction: number, size: number): number {\n return Math.max(0, Math.min(size, Math.ceil(fraction * size)));\n}\n/**\n * Enumerate candidate chords for the key, gated by the harmonic dial.\n *\n * Exported for the tests that pin the vocabulary a dial position opens; it is\n * not part of the package surface.\n */\nexport function buildCandidates(key: KeyScale, harmonic: number): Candidate[] {\n const tonicPc = pitchClass(key.rootPc);\n // A degree is one of seven, so a key with some other number of tones — a\n // pentatonic or a blues scale, which pops writes tunes in — is read against\n // its parallel major, which is the frame the numerals and the tonicization\n // targets below are already measured in. Harmonizing a five-tone melody with\n // the triads of the major it lives in is what a player does; refusing it\n // because the scale has five tones is not.\n const frame = heptatonicFrameOf(key);\n const tones = scaleTonesInDegreeOrder(frame);\n const candidates: Candidate[] = tones.map((rootPc, index) => {\n // Scale degrees are 1-based across the library, while the array index is\n // not; the candidate records the degree, which is what reaches the caller.\n const degree = index + 1;\n const quality = diatonicTriad(degree, frame).quality;\n return {\n rootPc,\n quality,\n degree,\n secondaryDominant: false,\n base: vocabularyBase(rootPc, quality, tonicPc),\n pcs: chordPitchClasses(makeChord(rootPc, quality)),\n };\n });\n\n // A minor key cadences through the harmonic-minor dominant, so the major triad\n // a fifth above the tonic belongs to the key's own vocabulary rather than to\n // any widening of it. Without it the dominant-to-tonic and cadence terms are\n // unreachable in minor, and a minor melody can be harmonized but never closed.\n // The natural-minor `v` stays alongside it and carries the same degree, so the\n // search chooses between them on the melody. `generateProgression` has no\n // melody to choose on and settles the same question once, from the preset it\n // is writing: it raises the third of the fifth degree only under a preset\n // labelled `functional: 'cadenceStrong'`, and writes the natural-minor `v`\n // under every other one. A major key already holds this chord as its diatonic V, and\n // the duplicate filter below drops the repeat. A mode is left alone: the\n // diatonic tier promises the key's own triads, and the raised seventh this\n // adds is not one of dorian's, phrygian's or locrian's.\n if (cadencesThroughRaisedSeventh(key)) {\n const rootPc = (tonicPc + 7) % 12;\n candidates.push({\n rootPc,\n quality: 'maj',\n degree: 5,\n secondaryDominant: false,\n base: vocabularyBase(rootPc, 'maj', tonicPc),\n pcs: chordPitchClasses(makeChord(rootPc, 'maj')),\n });\n }\n\n // The degrees are kept in the same 1-based space as `Candidate.degree`, which\n // the voice-leading cost compares them against.\n const targets = secondaryDominantTargets(key);\n const entryOrder = SECONDARY_DOMINANT_ENTRY_ORDER.filter((degree) =>\n targets.some((target) => target.degreeNumber === degree),\n );\n const secondaryOpen = admittedCount(harmonic / SECONDARY_DOMINANT_BAND, targets.length);\n for (const target of targets) {\n if (entryOrder.indexOf(target.degreeNumber) >= secondaryOpen) {\n continue;\n }\n // The root comes from the target itself rather than from a second lookup by\n // degree number: the targets are numbered in the key's heptatonic frame, so\n // indexing any other list of tones by that number reads a different degree.\n const rootPc = (target.rootPc + 7) % 12;\n candidates.push({\n rootPc,\n quality: 'dom7',\n secondaryDominant: true,\n targetDegree: target.degreeNumber,\n base: SECONDARY_DOMINANT_BASE,\n pcs: chordPitchClasses(makeChord(rootPc, 'dom7')),\n });\n }\n\n const parallel = parallelKey(key);\n const parallelTones = scaleTonesInDegreeOrder(parallel);\n const borrowed: Chord[] = [];\n for (let degree = 1; degree <= parallelTones.length; degree += 1) {\n const chord = diatonicTriad(degree, parallel);\n if (!isDiatonic(chord, key)) {\n borrowed.push(chord);\n }\n }\n // Usefulness, not degree order, decides which borrowing the next notch of the\n // dial buys: bVII is the borrowed chord pop writing reaches for most often and\n // the minor tonic the least, and taking them in degree order opened them the\n // other way round.\n const borrowedRank = (chord: Chord): number => {\n const offset = BORROWED_ENTRY_ORDER.indexOf(pitchClass(chord.rootPc - tonicPc));\n return offset < 0 ? BORROWED_ENTRY_ORDER.length : offset;\n };\n borrowed.sort((a, b) => borrowedRank(a) - borrowedRank(b));\n const borrowedOpen = admittedCount(\n (harmonic - SECONDARY_DOMINANT_BAND) / (1 - SECONDARY_DOMINANT_BAND),\n borrowed.length,\n );\n for (const chord of borrowed.slice(0, borrowedOpen)) {\n candidates.push({\n rootPc: chord.rootPc,\n quality: chord.quality,\n secondaryDominant: false,\n // A diminished or augmented triad is charged for what it is wherever it\n // comes from: priced at the flat borrowing rate, the ii dim borrowed from\n // the parallel minor came out cheaper than the key's own vii dim, and\n // which of the two a phrase took was decided by the tie-break jitter\n // rather than by the melody.\n base: UNSTABLE_QUALITIES.has(chord.quality)\n ? Math.max(BORROWED_BASE, UNSTABLE_TRIAD)\n : BORROWED_BASE,\n pcs: chordPitchClasses(chord),\n });\n }\n\n const seen = new Set<string>();\n return candidates.filter((candidate) => {\n const id = `${candidate.rootPc}:${candidate.quality}`;\n if (seen.has(id)) {\n return false;\n }\n seen.add(id);\n return true;\n });\n}\n","import { meterAt, pulseBeats, toMeterData } from '../../core/meter/index.js';\nimport {\n assertFiniteNumber,\n assertGenerationBudget,\n assertNoteEvents,\n assertOneOf,\n assertRange,\n assertRecord,\n assertTimeSignature,\n soundingNotesOnly,\n} from '../../core/validation/index.js';\nimport type { Chord } from '../../theory/chord/index.js';\nimport { makeChord } from '../../theory/chord/index.js';\nimport { roleOf } from '../../theory/harmony/index.js';\nimport { resolveKey } from '../../theory/scale/index.js';\nimport { resolveContext } from '../context/index.js';\nimport type { ChordSpan } from '../progression/index.js';\nimport { classifyMelodyTones, snapToPulse } from './nct.js';\n\nexport type { ClassifiedMelodyTone, MelodyToneRole } from './nct.js';\nexport { classifyMelodyTones } from './nct.js';\n\nimport type { Candidate, HarmonizeOptions, HarmonizeResult } from './internal.js';\nimport {\n DEFAULT_METER,\n DEFAULT_PLACEMENT,\n defaultHarmonicRhythm,\n FALLBACK,\n inferKey,\n TIE_BREAK_JITTER,\n} from './internal.js';\nimport { keyFitCost, tessituraCost } from './key-fit.js';\nimport { harmonizeOnce } from './search.js';\nimport { buildSegments, mergeRepeats, phraseEndSegments } from './segments.js';\n\nexport type {\n HarmonizeOptions,\n HarmonizePlacement,\n HarmonizeResult,\n MelodyNote,\n} from './internal.js';\n\nimport { buildCandidates, REHARMONIZE_DIAL, REHARMONIZE_STRENGTHS } from './candidates.js';\nimport { segmentIndexAt } from './segments.js';\n\n/**\n * Harmonize a melody with a min-cost chord progression.\n *\n * The melody's ornaments are classified first, from the melody and the metre\n * alone (see {@link classifyMelodyTones}); the chords then have to explain the\n * structural tones only, so a passing tone no longer buys itself a chord. The\n * melody is segmented by `harmonicRhythm`; each segment is scored against the\n * candidate chords the harmonic dial opens — the key's own triads, then the\n * secondary dominants, then the parallel mode's chords, one at a time as\n * `ctx.complexity.harmonic` rises from 0 to 1 — by the fit of those structural\n * tones, and a Viterbi search picks the lowest-cost path using a\n * functional-flow transition cost and a cadence bonus at the end of the melody\n * it is given — one cadence, at the close, unless `phraseEnds` names the closes\n * inside a longer line, and then one at each of them as well. Runs of the same\n * chord are reported once, so the chord count follows the harmony rather than\n * the grid.\n *\n * With `placement.transposeSearch` the melody is moved into the key it is\n * harmonized in, and `transposeSemitones` reports how far; with\n * `placement.octaveSearch` it is moved by octaves into a comfortable register,\n * which leaves the key and the chords untouched. Both may be set, and the\n * reported shift is their sum.\n *\n * The `seed` drives a deterministic tie-break only: it perturbs candidates by a\n * magnitude far below any real cost difference (see `TIE_BREAK_JITTER`),\n * so it can decide between chords or paths of otherwise-equal cost but never\n * overrides melody fit or functional flow. For a well-determined melody the\n * result is identical across seeds; the same seed always yields the same result.\n *\n * @param opts The melody and, optionally, the generation context, key, harmonic\n * rhythm, reharmonization strength, placement search, meter, and seed. Only\n * `melody` is required.\n * @returns The chosen transpose, key, chord path, and per-note roles.\n * @example\n * ```ts\n * import { harmonizeMelody } from '@libraz/libcantus';\n * const result = harmonizeMelody({\n * melody: [\n * { pitch: 60, startBeat: 0, durationBeat: 1 },\n * { pitch: 64, startBeat: 1, durationBeat: 1 },\n * ],\n * });\n * result.chords; // one ChordSpan per chord change, on the harmonic-rhythm grid\n * ```\n * @category Reharmonization\n */\nexport function harmonizeMelody(opts: HarmonizeOptions): HarmonizeResult {\n assertRecord(opts, 'harmonize options');\n assertNoteEvents(opts.melody, 'harmonize melody', {\n allowNonPositiveDuration: true,\n budget: opts.budget,\n });\n const phraseEnds = opts.phraseEnds ?? [];\n for (const end of phraseEnds) {\n assertFiniteNumber(end, 'harmonize phrase end');\n }\n const soundingMelody = soundingNotesOnly(opts.melody);\n const ts = meterAt(0, toMeterData(opts.ts ?? DEFAULT_METER, 'ts'));\n assertTimeSignature(ts);\n const requestedKey = opts.key ?? 'infer';\n // The key is read once, here at the boundary, and kept whole: the search\n // below wants only the pitch classes, and the result reports the key the\n // caller named — spelling and all — rather than the pitch classes it worked\n // from. An inferred key is read the same way, so both arrive spelled.\n const resolvedKey = resolveKey(\n requestedKey === 'infer' ? inferKey(soundingMelody) : requestedKey,\n );\n const key = resolvedKey.scale;\n const placement = opts.placement ?? DEFAULT_PLACEMENT;\n const ctx = resolveContext(opts.ctx);\n // `reharmonize` names three points on the dial the context sets continuously,\n // so a caller who says `'secondaryDominant'` gets exactly the vocabulary that\n // name has always meant. The name is checked whether or not the context also\n // sets the dial: a value from a config file that reached the table unchecked\n // would read as `undefined`, and the NaN dial position that follows opens the\n // secondary-dominant family in full while suppressing the borrowed one.\n const reharmonize = assertOneOf(\n opts.reharmonize ?? 'diatonic',\n REHARMONIZE_STRENGTHS,\n 'reharmonize',\n );\n const harmonic = ctx.harmonic ?? REHARMONIZE_DIAL[reharmonize];\n // Nothing to harmonize: inventing a tonic bar here would silently insert a\n // ghost chord into a chart built by harmonizing sections and concatenating\n // them. The sibling generators return an empty result for empty input too.\n if (soundingMelody.length === 0) {\n return { transposeSemitones: 0, key: resolvedKey, chords: [], melodyRoles: [] };\n }\n const candidates = buildCandidates(key, harmonic);\n const candAt = (i: number): Candidate => candidates[i] ?? FALLBACK;\n const draw = ctx.part('harmony');\n const jitter = candidates.map((_, index) => draw.at('jitter', index) * TIE_BREAK_JITTER);\n\n // Read every onset and release as the metric position it is playing. A melody\n // arrives as note events from a DAW or MIDI track, so its beats carry the\n // timing of a performance; without this a few milliseconds of jitter would put\n // a note in the slot before the one it belongs to, weigh a sliver of it as if\n // it sounded there, and change the chords chosen for the whole phrase.\n const pulse = pulseBeats(ts);\n const spans = soundingMelody.map((note) => ({\n startBeat: snapToPulse(note.startBeat, pulse),\n endBeat: snapToPulse(note.startBeat + note.durationBeat, pulse),\n }));\n\n const melodyStart = spans.reduce(\n (start, span) => Math.min(start, span.startBeat),\n Number.POSITIVE_INFINITY,\n );\n // Accumulated from the melody alone rather than from beat 0: a pickup sounds\n // before the first downbeat, and a melody written entirely in one — an\n // upbeat lifted out of a chart — ended at beat 0 by the accumulator's own\n // starting value, which grew a grid past where the melody reaches and put the\n // cadence bonus on an empty segment after its last note.\n const melodyEnd = spans.reduce(\n (end, span) => Math.max(end, span.endBeat),\n Number.NEGATIVE_INFINITY,\n );\n // Any positive value is honoured: a silent clamp would make the option mean\n // something different here than it does on the analysis side. A value small\n // enough to make the search explode is caught by the budget assertions below,\n // not rounded away.\n const hr = assertRange(\n opts.harmonicRhythm ?? defaultHarmonicRhythm(ts),\n Number.MIN_VALUE,\n Number.MAX_SAFE_INTEGER,\n 'harmonic rhythm',\n );\n const { segments, innerEnds, bounds, segmentStart } = buildSegments(\n spans,\n soundingMelody,\n ts,\n { start: melodyStart, end: melodyEnd, harmonicRhythm: hr },\n phraseEnds,\n opts.budget,\n );\n\n const { closing: closingSegments, named: namedEndSegments } = phraseEndSegments(\n segments,\n innerEnds,\n );\n // A cadence needs two chords to be told apart, so the articulation rule can\n // only be applied where the vocabulary offers a second harmony to move to.\n const articulatedSegments =\n new Set(candidates.map((c) => `${c.rootPc}:${c.quality}`)).size > 1\n ? namedEndSegments\n : new Set<number>();\n\n // Two independent axes: the semitone shift that puts the melody in the key it\n // is harmonized in, and the octave that puts it in a comfortable register.\n // Octaves preserve pitch classes, so they can only move the register.\n const semitoneShifts: number[] = [0];\n if (placement.transposeSearch) {\n for (let s = -6; s <= 6; s += 1) {\n if (s !== 0) {\n semitoneShifts.push(s);\n }\n }\n }\n const octaveShifts: number[] = placement.octaveSearch ? [0, -12, 12] : [0];\n const transposes = semitoneShifts.flatMap((s) => octaveShifts.map((o) => s + o));\n // The estimate is the search itself: one pass per placement visits every pair\n // of candidates in every slot, and that product dominates everything else the\n // call does. A caller harmonizing a whole piece with the placement search on\n // raises `budget` to cover it, the way the sibling generators are given room\n // for the work they were asked for.\n assertGenerationBudget(\n transposes.length * segments.length * candidates.length * candidates.length,\n 'harmonization placement search',\n opts.budget,\n );\n\n let bestCost = Number.POSITIVE_INFINITY;\n let bestTs = 0;\n let bestPath: number[] = [];\n for (const shift of transposes) {\n const shifted = soundingMelody.map((n) => ({ ...n, pitch: n.pitch + shift }));\n const { cost, path } = harmonizeOnce(\n shifted,\n key,\n candidates,\n segments,\n jitter,\n closingSegments,\n articulatedSegments,\n );\n const total = cost + keyFitCost(shifted, key) + tessituraCost(shifted);\n if (total < bestCost) {\n bestCost = total;\n bestTs = shift;\n bestPath = path;\n }\n }\n\n const chords = mergeRepeats(\n bestPath.map((ci, s) => {\n const cand = candAt(ci);\n const chord: ChordSpan = {\n rootPc: cand.rootPc,\n quality: cand.quality,\n startBeat: segments[s]?.startBeat ?? segmentStart + s * hr,\n };\n if (cand.degree !== undefined) {\n chord.degree = cand.degree;\n }\n if (cand.secondaryDominant) {\n chord.secondaryDominant = true;\n }\n return chord;\n }),\n );\n\n // A note's role is its role in the chord the search chose for the slot it was\n // charged against, so the slot is read from the same snapped onset the cost\n // model read: taking the raw onset would report a note that begins a few\n // milliseconds before a boundary against the chord on the other side of it,\n // which is the performance jitter the snap exists to absorb.\n const melodyRoles = opts.melody.map((note, noteIndex) => {\n const segIdx = segmentIndexAt(bounds, snapToPulse(note.startBeat, pulse));\n const cand = candAt(bestPath[segIdx] ?? 0);\n const chord: Chord = makeChord(cand.rootPc, cand.quality);\n return { noteIndex, role: roleOf(note.pitch + bestTs, chord).role };\n });\n\n return { transposeSemitones: bestTs, key: resolvedKey, chords, melodyRoles };\n}\n","import { BEAT_EPS } from '../../analyze/adjacency.js';\nimport { assertChordTimeline, type ChordTimeline } from '../../analyze/timeline/index.js';\nimport { InvalidInputError } from '../../core/errors/index.js';\nimport type { MeterLike, TimeSignature } from '../../core/meter/index.js';\nimport { beatsPerBar, meterAt, toMeterData } from '../../core/meter/index.js';\nimport { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\nimport type { KeyScale, NoteEvent } from '../../core/types.js';\nimport {\n assertFiniteNumber,\n assertGenerationBudget,\n assertInteger,\n assertNoteEvents,\n assertOneOf,\n assertPositiveInt,\n assertRecord,\n clampToMidi,\n} from '../../core/validation/index.js';\nimport type { Chord } from '../../theory/chord/index.js';\nimport { chordPitchClasses } from '../../theory/chord/index.js';\nimport {\n isScaleTone,\n type KeyLike,\n shiftByScaleDegrees,\n toKeyScale,\n} from '../../theory/scale/index.js';\nimport { type ChordLike, toChordData } from '../../theory/symbol/index.js';\nimport { type GenerationContextInput, resolveContextWith } from '../context/index.js';\n\n/**\n * A transformation applicable to a motif cell.\n *\n * @category Composition\n */\nconst MOTIF_TRANSFORMS = Object.freeze([\n 'transposeDiatonic',\n 'transposeChromatic',\n 'invert',\n 'retrograde',\n 'augment',\n 'diminish',\n 'sequence',\n] as const);\n\n/**\n * The name of a transformation {@link transformMotif} can apply to a cell.\n *\n * Derived from the table the transforms are dispatched from, so a transform\n * added to one is named by the other on the same commit.\n *\n * @category Composition\n */\nexport type MotifTransform = (typeof MOTIF_TRANSFORMS)[number];\n\n/**\n * A single note within a motif cell.\n *\n * @category Composition\n */\nexport type MotifNote = {\n pitch: number;\n startBeat: number;\n durationBeat: number;\n};\n\n/**\n * A short melodic cell.\n *\n * @category Composition\n */\nexport type MotifCell = {\n notes: MotifNote[];\n};\n\n/**\n * Melodic contour shape for {@link generateMotif}.\n *\n * @category Composition\n */\nexport type MotifContour = 'arch' | 'ascending' | 'descending' | 'wave';\n\n/**\n * Options controlling {@link generateMotif}.\n *\n * @category Composition\n */\nexport type MotifOptions = {\n /** The key the line is written in; a key name such as `'C major'` is read as that key. */\n key: KeyLike;\n /**\n * Chord the downbeats are pulled to, if any; it may be written as a chord\n * symbol as well as given as chord data.\n */\n chord?: ChordLike | null;\n /** Length of the motif in bars of `ts`. */\n bars: number;\n /**\n * Meter the bars are counted in. It sets the bar length, so a motif shares a\n * bar grid with the other generators instead of assuming four beats.\n *\n * @defaultValue 4/4\n */\n ts?: MeterLike;\n /**\n * Melodic contour shape the line follows.\n *\n * @defaultValue `'arch'`\n */\n contour?: MotifContour;\n /**\n * Probability in [0, 1] that a note is nudged by a single diatonic step. The\n * nudge direction is balanced (up or down with equal odds), so it adds\n * variety without biasing the line off its contour. Default 0, which\n * reproduces the requested contour exactly (no drift, tails return to tonic).\n *\n * Sugar for `ctx: { complexity: { ornament } }` — a nudge off the contour is\n * decoration rather than rhythm; the context wins where both are given. Being\n * the same dial, it takes the same values: one outside [0, 1] is rejected here\n * exactly as it is there, rather than being folded to the nearest end.\n *\n * @defaultValue 0\n * @throws If it is not a finite number in [0, 1].\n */\n jitter?: number;\n /**\n * The generation context. Its `complexity.ornament` is this generator's\n * jitter, and its `seed` fixes which notes are nudged.\n *\n * @defaultValue `{ seed: 0 }`\n */\n ctx?: GenerationContextInput;\n};\n\n/**\n * Smallest span, in beats, treated as a distinct tile step. A positive but\n * near-zero cell span is clamped to this so tiling cannot stall.\n */\nconst MIN_TILE_SPAN = 1 / 256;\n\n/** Meter assumed when none is supplied. */\nconst DEFAULT_TS: TimeSignature = { numerator: 4, denominator: 4 };\n\n/** Whether a beat position falls on a bar line of a `barBeats`-long bar. */\nfunction isDownbeat(startBeat: number, barBeats: number): boolean {\n const bar = startBeat / barBeats;\n return Math.abs(bar - Math.round(bar)) < BEAT_EPS;\n}\n\n/** Nearest chord tone to `pitch`, preferring the lower pitch on a tie. */\nfunction nearestChordTone(pitch: number, chord: Chord): number {\n const pcs = chordPitchClasses(chord);\n for (let d = 0; d <= 12; d += 1) {\n if (pcs.includes(pitchClass(pitch - d))) {\n return pitch - d;\n }\n if (pcs.includes(pitchClass(pitch + d))) {\n return pitch + d;\n }\n }\n return pitch;\n}\n\n/** Widest move, in semitones, a developed note is displaced by. */\nconst MAX_DEVELOP_SHIFT = 12;\n\n/**\n * Pick the pitch a developed note takes.\n *\n * The note moves to the nearest pitch its position allows — a chord tone where\n * the position carries structural weight, any tone of the key elsewhere — and\n * skips a pitch a different note of the same tile already took, so two pitches\n * that differ in the cell still differ in the development. A tie is broken in\n * the direction the melody was already moving, which keeps a step a step\n * instead of folding it back onto the note before it.\n *\n * @param pitch The note's pitch in the cell.\n * @param allows Whether a candidate pitch is one this position may take.\n * @param direction Sign of the melodic motion into the note.\n * @param taken Pitches already placed in this tile, by the cell pitch that took\n * them.\n * @returns The developed pitch, or `pitch` when the position allows nothing\n * within reach.\n */\nfunction developedPitch(\n pitch: number,\n allows: (candidate: number) => boolean,\n direction: number,\n taken: Map<number, number>,\n): number {\n const ahead = direction >= 0 ? 1 : -1;\n for (let d = 0; d <= MAX_DEVELOP_SHIFT; d += 1) {\n for (const candidate of d === 0 ? [pitch] : [pitch + ahead * d, pitch - ahead * d]) {\n if (candidate < 0 || candidate > 127 || !allows(candidate)) {\n continue;\n }\n const owner = taken.get(candidate);\n if (owner === undefined || owner === pitch) {\n return candidate;\n }\n }\n }\n return pitch;\n}\n\n/** Earliest onset in a cell; the time origin for timing transforms. */\nfunction cellOrigin(notes: MotifNote[]): number {\n let origin = Number.POSITIVE_INFINITY;\n for (const note of notes) {\n origin = Math.min(origin, note.startBeat);\n }\n return Number.isFinite(origin) ? origin : 0;\n}\n\n/** Latest offset in a cell, without spreading an unbounded caller array. */\nfunction cellEnd(notes: MotifNote[]): number {\n let end = Number.NEGATIVE_INFINITY;\n for (const note of notes) {\n end = Math.max(end, note.startBeat + note.durationBeat);\n }\n return Number.isFinite(end) ? end : 0;\n}\n\n/**\n * Total beat span covered by a cell (from first onset to last offset).\n *\n * Shared with the class layer rather than restated there: a cell is accepted up\n * to the generation budget, which is far past the argument count a spread can\n * carry, so the loop above is the only way to answer this for every cell the\n * constructor takes.\n */\nexport function cellSpan(cell: MotifCell): number {\n if (cell.notes.length === 0) {\n return 0;\n }\n const start = cellOrigin(cell.notes);\n const end = cellEnd(cell.notes);\n return end - start;\n}\n\nfunction clone(cell: MotifCell): MotifCell {\n return { notes: cell.notes.map((n) => ({ ...n })) };\n}\n\n/**\n * The cell with its notes in time order.\n *\n * A cell is read by index in several places — the pivot of an inversion is its\n * first note, and an analysis of the line reads note `i + 1` as the one that\n * follows note `i` — so every cell that leaves this module runs forwards. The\n * sort is stable, which leaves the notes of a chord in the order they were\n * written.\n */\nfunction inTimeOrder(cell: MotifCell): MotifCell {\n return { notes: [...cell.notes].sort((a, b) => a.startBeat - b.startBeat) };\n}\n\n/** The pitch an inversion reflects about: the cell's earliest note. */\nfunction pivotPitch(notes: readonly MotifNote[]): number {\n let pivot: MotifNote | undefined;\n for (const note of notes) {\n if (pivot === undefined || note.startBeat < pivot.startBeat) {\n pivot = note;\n }\n }\n return pivot?.pitch ?? 0;\n}\n\n/** Diatonic scale-degree offsets shaping a contour of `count` notes. */\nfunction contourOffsets(contour: MotifContour, count: number): number[] {\n const offsets: number[] = [];\n for (let i = 0; i < count; i += 1) {\n switch (contour) {\n case 'ascending':\n offsets.push(i);\n break;\n case 'descending':\n offsets.push(-i);\n break;\n case 'arch':\n // Symmetric rise-and-fall: a palindrome that starts and ends on the\n // tonic (offset 0) for both odd and even lengths.\n offsets.push(Math.min(i, count - 1 - i));\n break;\n case 'wave':\n offsets.push([0, 1, 0, -1][i % 4] ?? 0);\n break;\n }\n }\n return offsets;\n}\n\n/**\n * Generate a short seed motif over `bars`.\n *\n * The melody follows the requested contour in diatonic steps from the tonic,\n * snapped to the key. When a chord is supplied, notes landing on bar downbeats\n * are pulled to the nearest chord tone. An optional `jitter` adds balanced\n * per-note variation without biasing the contour; it is off by default, so the\n * contour is reproduced exactly. Output is deterministic for a given seed.\n *\n * @param opts Key, optional chord, length, contour, jitter, and context.\n * @returns The generated motif cell.\n *\n * @example\n * ```ts\n * import { generateMotif, majorKey } from '@libraz/libcantus';\n * const cell = generateMotif({ key: majorKey(0), bars: 2, contour: 'arch' });\n * // cell.notes is a deterministic MotifCell (seed defaults to 0)\n * ```\n *\n * @category Composition\n */\nexport function generateMotif(opts: MotifOptions): MotifCell {\n assertRecord(opts, 'motif options');\n const contour = opts.contour ?? 'arch';\n const bars = assertPositiveInt(opts.bars, 'motif bars');\n // The meter is read once, here: a motif is one repeated bar grid, so a\n // caller who names a whole map gets the signature the map opens in.\n const barBeats = beatsPerBar(meterAt(0, toMeterData(opts.ts ?? DEFAULT_TS, 'ts')));\n const totalBeats = bars * barBeats;\n const noteCount = Math.max(3, bars * 2);\n assertGenerationBudget(noteCount, 'motif notes');\n const beatsPerNote = totalBeats / noteCount;\n // The dial is handed over as it was given: `jitter` is the sugar for the\n // context's ornament dial, so it has to accept and reject exactly what the\n // context does. Clamping here would make the sugar take a value its own\n // desugared form refuses, which is a different option wearing the same name.\n const ctx = resolveContextWith(opts.ctx, { ornament: opts.jitter });\n const jitterProb = ctx.ornament ?? 0;\n const draw = ctx.part('motif');\n // Key and chord are read into their plain form once, here at the boundary;\n // the loop below works on the plain forms alone.\n const key = toKeyScale(opts.key);\n const chord = opts.chord ? toChordData(opts.chord) : null;\n const tonic = pitchClass(key.rootPc) + 60;\n const offsets = contourOffsets(contour, noteCount);\n\n const notes: MotifNote[] = [];\n for (let i = 0; i < noteCount; i += 1) {\n // Opt-in, direction-balanced jitter: disabled by default so the contour is\n // preserved exactly. When enabled it nudges up or down with equal odds,\n // avoiding the upward bias that used to drift arch/wave tails off the\n // tonic. Each note's draw belongs to its own index, so raising the dial\n // nudges further notes while the ones already nudged keep their nudge.\n let jitter = 0;\n if (draw.prob(jitterProb, 'jitter', i)) {\n jitter = draw.prob(0.5, 'direction', i) ? 1 : -1;\n }\n let pitch = shiftByScaleDegrees(tonic, (offsets[i] ?? 0) + jitter, key);\n const startBeat = i * beatsPerNote;\n if (chord && isDownbeat(startBeat, barBeats)) {\n pitch = nearestChordTone(pitch, chord);\n }\n notes.push({\n pitch: clampToMidi(pitch, 'generated motif pitch'),\n startBeat,\n durationBeat: beatsPerNote,\n });\n }\n return { notes };\n}\n\n/**\n * Turn a motif cell into note events.\n *\n * A {@link MotifNote} already carries the {@link NoteEvent} fields, but a cell\n * wraps them in an object; this unwraps it, so a motif can be humanized,\n * grooved, or written out like any other line.\n *\n * @param cell The motif to convert.\n * @returns The cell's notes, copied, in the order they are stored.\n * @example\n * ```ts\n * import { generateMotif, humanize, majorKey, motifToNoteEvents } from '@libraz/libcantus';\n * humanize(motifToNoteEvents(generateMotif({ key: majorKey(0), bars: 2 })));\n * ```\n * @category Composition\n */\nexport function motifToNoteEvents(cell: MotifCell): NoteEvent[] {\n return assertCellNotes(cell, 'motif cell notes').map((note) => ({ ...note }));\n}\n\n/**\n * Read a cell's statement, checking the cell before the events inside it.\n *\n * Every entrance that takes a cell reads the same two things — that it is an\n * object, and that what it holds is a sounding event array — so both checks\n * live here. Reading the array off an unchecked cell reports this library's own\n * `TypeError` rather than the malformed cell, which is what a caller and a host\n * branch on.\n */\nfunction assertCellNotes(cell: MotifCell, name: string): MotifNote[] {\n const read = assertRecord<MotifCell>(cell, 'motif cell');\n assertNoteEvents(read.notes, name);\n return read.notes;\n}\n\n/**\n * Apply a transformation to a motif cell.\n *\n * Every transform hands the cell back in onset order, so the note a later\n * transform reads as the first one is the note that sounds first.\n *\n * `invert` reflects pitches about the earliest note (chromatic, self-inverse);\n * `retrograde` mirrors onsets about the cell span, preserving rests (self-inverse);\n * `augment`/`diminish` scale time by `amount ?? 2` and its reciprocal;\n * `transposeChromatic` adds `amount` semitones; `transposeDiatonic` shifts by\n * `amount` scale degrees when a `key` is given, or — with no `key` — falls back\n * to a chromatic shift of `amount` semitones; `sequence` appends a shifted copy\n * (by `amount` diatonic degrees with a `key`, or by `amount` semitones without\n * one, since it delegates to `transposeDiatonic`).\n *\n * @param cell The cell to transform.\n * @param t The transformation.\n * @param amount Optional parameter (semitones, degrees, or time factor).\n * @param key Key context for the diatonic transforms, as a key name such as\n * `'C major'` or as a key/scale; without it, `transposeDiatonic` and\n * `sequence` shift chromatically by semitones.\n * @returns The transformed cell, its notes in time order whatever order the\n * source cell was written in, so it can be read as a line.\n * @throws If a transformed pitch would fall outside the MIDI range 0..127.\n *\n * @example\n * ```ts\n * import { generateMotif, transformMotif, majorKey } from '@libraz/libcantus';\n * const cell = generateMotif({ key: majorKey(0), bars: 1 });\n * const inverted = transformMotif(cell, 'invert');\n * ```\n *\n * @category Composition\n */\nexport function transformMotif(\n cell: MotifCell,\n t: MotifTransform,\n amount?: number,\n key?: KeyLike,\n): MotifCell {\n // The key is read into its plain form once, here at the boundary; `sequence`\n // re-enters through `transformCell` so it is never read a second time.\n return transformCell(cell, t, amount, key === undefined ? undefined : toKeyScale(key));\n}\n\n/** The transform on an already-resolved key, checked. */\nfunction transformCell(\n cell: MotifCell,\n t: MotifTransform,\n amount?: number,\n key?: KeyScale,\n): MotifCell {\n // Every transform leaves through here, so this is where the result is put\n // back into time order — a retrograde reverses when the notes sound, not the\n // order they are written in.\n return assertInRange(inTimeOrder(transformUnchecked(cell, t, amount, key)));\n}\n\n/**\n * Reject a cell whose pitches have left the MIDI range.\n *\n * Transposing far enough, or inverting about a low pivot, walks a motif off\n * either end of the range. Returning such a note would carry an unplayable\n * pitch through `developMotif` and into whatever consumes the result, so it is\n * a caller error rather than something to clamp — clamping would distort the\n * very intervals the transform exists to preserve.\n */\nfunction assertInRange(cell: MotifCell): MotifCell {\n for (const note of cell.notes) {\n if (note.pitch < 0 || note.pitch > 127) {\n throw new InvalidInputError(\n `transformed motif pitch ${note.pitch} is outside the MIDI range 0..127`,\n );\n }\n }\n return cell;\n}\n\n/** The transform itself, before the range check. */\nfunction transformUnchecked(\n cell: MotifCell,\n t: MotifTransform,\n amount?: number,\n key?: KeyScale,\n): MotifCell {\n const notes = assertCellNotes(cell, 'motif notes');\n // The switch below has a case per transform and no fallthrough, so a name\n // that is in no case returns nothing at all and fails as a missing cell one\n // frame later. A union is checked when the caller compiles, and this is where\n // one from a config file or a JavaScript caller arrives.\n assertOneOf(t, MOTIF_TRANSFORMS, 'motif transform');\n if (amount !== undefined) {\n assertFiniteNumber(amount, 'motif transform amount');\n }\n if (\n (t === 'transposeChromatic' || t === 'transposeDiatonic' || t === 'sequence') &&\n amount !== undefined\n ) {\n assertInteger(\n amount,\n `${t === 'transposeChromatic' ? 'chromatic' : 'diatonic'} transform amount`,\n );\n }\n if ((t === 'augment' || t === 'diminish') && amount !== undefined && amount <= 0) {\n throw new InvalidInputError('time transform amount must be positive');\n }\n switch (t) {\n case 'transposeChromatic': {\n const semis = amount ?? 0;\n return { notes: notes.map((n) => ({ ...n, pitch: n.pitch + semis })) };\n }\n case 'transposeDiatonic': {\n const degrees = amount ?? 1;\n return {\n notes: notes.map((n) => ({\n ...n,\n // With a key, shift by scale degrees; without one, fall back to a\n // chromatic shift of `degrees` semitones. A pitch between two scale\n // tones keeps its distance above the one below it, so the chromatic\n // notes a motif is written with — a blues lick, a semitone neighbour —\n // come through as themselves, and shifting by no degrees returns the\n // cell it was given.\n pitch: key ? shiftByScaleDegrees(n.pitch, degrees, key) : n.pitch + degrees,\n })),\n };\n }\n case 'invert': {\n const pivot = pivotPitch(notes);\n return { notes: notes.map((n) => ({ ...n, pitch: 2 * pivot - n.pitch })) };\n }\n case 'retrograde': {\n if (notes.length === 0) {\n return { notes: [] };\n }\n const start = cellOrigin(notes);\n const end = cellEnd(notes);\n return {\n notes: notes.map((n) => ({\n pitch: n.pitch,\n startBeat: start + (end - (n.startBeat + n.durationBeat)),\n durationBeat: n.durationBeat,\n })),\n };\n }\n case 'augment':\n return scaleTime(cell, amount ?? 2);\n case 'diminish':\n return scaleTime(cell, 1 / (amount ?? 2));\n case 'sequence': {\n const degrees = amount ?? 2;\n const span = cellSpan(cell);\n // Delegates to transposeDiatonic: a diatonic shift when `key` is given,\n // otherwise a chromatic shift of `degrees` semitones.\n const copy = transformCell(cell, 'transposeDiatonic', degrees, key);\n const shifted = copy.notes.map((n) => ({ ...n, startBeat: n.startBeat + span }));\n return { notes: [...clone(cell).notes, ...shifted] };\n }\n }\n}\n\n/** Scale a cell's timing about its origin by `factor`. */\nfunction scaleTime(cell: MotifCell, factor: number): MotifCell {\n const origin = cellOrigin(cell.notes);\n return {\n notes: cell.notes.map((n) => ({\n pitch: n.pitch,\n startBeat: origin + (n.startBeat - origin) * factor,\n durationBeat: n.durationBeat * factor,\n })),\n };\n}\n\n/**\n * Lay a motif across `bars` and snap it to a chord timeline.\n *\n * The cell is tiled back-to-back to fill the requested span. Notes that carry\n * structural weight — the head of each tile and every bar line — are pulled to\n * the nearest chord tone of the segment sounding at that onset, so the\n * developed line spells the underlying harmony. The notes between them are kept\n * in the key as passing and neighbour tones, and two pitches that differ in the\n * cell still differ in the development, so the development is still recognizably\n * the motif rather than a chord arpeggiated over the cell's rhythm.\n *\n * @param cell The source motif.\n * @param timeline Chord segments to snap against.\n * @param key Key context, as a key name such as `'C major'` or as a key/scale;\n * the notes off the structural positions are kept in it.\n * @param bars Number of bars to fill.\n * @param ts Meter the bars are counted in. Asked for rather than defaulted: a\n * development laid on the wrong bar length drifts off the part it plays\n * under, and a caller who leaves it out has no way to hear that.\n * @returns The developed, harmony-aware cell, its notes in time order.\n *\n * @example\n * ```ts\n * import { chordTimelineFromChords, developMotif, generateMotif, majorKey } from '@libraz/libcantus';\n * const key = majorKey(0);\n * const timeline = chordTimelineFromChords([{ rootPc: 0, quality: 'maj', startBeat: 0 }], 8);\n * const developed = developMotif(generateMotif({ key, bars: 1 }), timeline, key, 2, '4/4');\n * developed.notes.map((n) => n.pitch); // [60, 62, 60, 60, 62, 60] — the C-D-C cell, twice\n * ```\n *\n * @category Composition\n */\nexport function developMotif(\n cell: MotifCell,\n timeline: ChordTimeline,\n key: KeyLike,\n bars: number,\n ts: MeterLike,\n): MotifCell {\n const meter = meterAt(0, toMeterData(ts, 'ts'));\n assertChordTimeline(timeline);\n assertPositiveInt(bars, 'development bars');\n const cellNotes = assertCellNotes(cell, 'motif notes');\n // The key is read into its plain form once, here at the boundary; the tiling\n // below is given the scale it resolved to.\n const scale = toKeyScale(key);\n const span = cellSpan(cell);\n const barBeats = beatsPerBar(meter);\n const totalBeats = bars * barBeats;\n const origin = cellOrigin(cellNotes);\n const out: MotifNote[] = [];\n\n if (span <= 0) {\n return inTimeOrder(cell);\n }\n\n // Clamp a degenerate near-zero span so the tile count stays finite, and step\n // the offset multiplicatively so rounding error cannot accumulate over tiles.\n const tileSpan = Math.max(span, MIN_TILE_SPAN);\n const tileCount = Math.ceil(totalBeats / tileSpan);\n assertGenerationBudget(tileCount * cellNotes.length, 'developed motif notes');\n for (let k = 0; k < tileCount; k += 1) {\n const offset = k * tileSpan;\n // Placed pitch by cell pitch, so a tile cannot fold two of the cell's\n // pitches onto one. It is per tile: each tile is the same cell again, and\n // the same cell pitch under the same harmony wants the same answer.\n const taken = new Map<number, number>();\n let previous: number | undefined;\n for (const n of cellNotes) {\n const startBeat = offset + (n.startBeat - origin);\n if (startBeat >= totalBeats) {\n continue;\n }\n const chord = timeline.at(startBeat);\n // The head of the cell and the bar lines are where the ear reads the\n // harmony; everything else is free to pass between them.\n const structural = n.startBeat === origin || isDownbeat(startBeat, barBeats);\n const chordTones = chord && structural ? chordPitchClasses(chord) : null;\n const allows = chordTones\n ? (candidate: number) => chordTones.includes(pitchClass(candidate))\n : (candidate: number) => isScaleTone(candidate, scale);\n const direction = previous === undefined ? 0 : n.pitch - previous;\n const pitch = developedPitch(n.pitch, allows, direction, taken);\n taken.set(pitch, n.pitch);\n previous = n.pitch;\n out.push({ pitch, startBeat, durationBeat: n.durationBeat });\n }\n }\n return inTimeOrder({ notes: out });\n}\n","/**\n * Ornaments applied to material that already exists.\n *\n * Decoration is not a property of generation: this layer takes notes and gives\n * notes back, the way {@link humanize} does, so a line from any source — a\n * generator, a MIDI import, a caller's own array — can be decorated without\n * being generated again by whichever generator happens to write ghost notes.\n *\n * The decoration is carried as an {@link Articulation}, not baked into extra\n * onsets: a flam is one note marked `'flam'`, so the grace stroke stays the\n * reader's to voice and nothing downstream has to guess which of two onsets was\n * the ornament.\n */\n\nimport type { Articulation } from '../../core/instrument/index.js';\nimport {\n isStrongBeat,\n type MeterLike,\n meterAt,\n type TimeSignature,\n toMeterData,\n} from '../../core/meter/index.js';\nimport type { NoteEvent } from '../../core/types.js';\nimport {\n assertNoteEvents,\n assertOneOf,\n assertOptions,\n assertRange,\n assertTimeSignature,\n dropSilentNotes,\n} from '../../core/validation/index.js';\nimport {\n type GenerationContextInput,\n resolveContextWith,\n sustainsStrokes,\n} from '../context/index.js';\n\n/**\n * Every ornament idiom, in declaration order.\n *\n * @category Composition\n */\nexport const ORNAMENT_STYLES = Object.freeze(['ghost', 'flam', 'drag', 'slide', 'accent'] as const);\n\n/**\n * The kind of ornament to apply.\n *\n * - `'ghost'`: soften weak-position notes into ghosted strokes.\n * - `'flam'`: mark accented notes as flammed — the strong positions `'accent'`\n * also takes, voiced as a grace stroke instead of a velocity lift.\n * - `'drag'`: mark notes leading into a strong position as dragged. A weak-position\n * note qualifies only when the next onset in the material falls on a strong one.\n * - `'slide'`: mark a note reached by a step or a leap as slid into. A repeated\n * pitch is not: a slide is the move between two pitches, and there is none.\n * - `'accent'`: mark strong-position notes as accented, and lift their velocity.\n *\n * @category Composition\n */\nexport type OrnamentStyle = (typeof ORNAMENT_STYLES)[number];\n\n/**\n * Options controlling {@link ornament}.\n *\n * @category Composition\n */\nexport type OrnamentOptions = {\n /**\n * The ornament to apply.\n *\n * @defaultValue `'ghost'`\n */\n style?: OrnamentStyle;\n /**\n * How much of the eligible material is decorated, in [0, 1]. Raising it only\n * ornaments more notes; the notes already ornamented keep the ornament they\n * had. Defaults to `complexity.ornament` from the context, or 0.5.\n */\n amount?: number;\n /**\n * Time signature, in any form that names one, used to tell strong positions\n * from weak ones.\n *\n * @defaultValue 4/4\n */\n ts?: MeterLike;\n /**\n * The generation context. Its `complexity.ornament` sets the amount, its\n * `complexity.difficulty` — with a tempo — keeps ornaments off passages too\n * fast to play them in, and its `seed` fixes which notes are decorated.\n *\n * @defaultValue `{ seed: 0 }`\n */\n ctx?: GenerationContextInput;\n};\n\nconst DEFAULT_TS: TimeSignature = { numerator: 4, denominator: 4 };\nconst DEFAULT_STYLE: OrnamentStyle = 'ghost';\nconst DEFAULT_AMOUNT = 0.5;\n/** Velocity a ghosted stroke keeps, as a fraction of what it had. */\nconst GHOST_VELOCITY_SCALE = 0.45;\n/** Velocity an accent adds, in MIDI units. */\nconst ACCENT_VELOCITY_BOOST = 12;\n/** Velocity assumed for a note that carries none. */\nconst DEFAULT_VELOCITY = 80;\nconst MIN_VELOCITY = 1;\nconst MAX_VELOCITY = 127;\n/**\n * Interval, in semitones, from which a move reads as slid into rather than\n * restruck. A semitone is the most idiomatic slide there is on a fretted or a\n * bowed instrument, so every move of the line qualifies and only a repeated\n * pitch, which is no move at all, does not.\n */\nconst SLIDE_MIN_SEMITONES = 1;\n\n/** Whether a note is a candidate for an ornament of this style. */\nfunction eligible(\n style: OrnamentStyle,\n note: NoteEvent,\n previous: NoteEvent | undefined,\n next: NoteEvent | undefined,\n ts: TimeSignature,\n): boolean {\n const strong = isStrongBeat(note.startBeat, ts);\n switch (style) {\n case 'ghost':\n return !strong;\n case 'flam':\n case 'accent':\n return strong;\n case 'drag':\n // A drag leads somewhere: it is the weak note before a strong one, so the\n // next onset in the material has to be the strong position it runs into.\n return !strong && next !== undefined && isStrongBeat(next.startBeat, ts);\n case 'slide':\n return previous !== undefined && Math.abs(note.pitch - previous.pitch) >= SLIDE_MIN_SEMITONES;\n }\n}\n\n/** The articulation a style writes. */\nfunction articulationOf(style: OrnamentStyle): Articulation {\n return style;\n}\n\n/** Velocity after an ornament, or the note's own where the ornament changes none. */\nfunction ornamentedVelocity(style: OrnamentStyle, note: NoteEvent): number | undefined {\n if (note.velocity === undefined && style !== 'ghost' && style !== 'accent') {\n return undefined;\n }\n const velocity = note.velocity ?? DEFAULT_VELOCITY;\n if (style === 'ghost') {\n return clampVelocity(velocity * GHOST_VELOCITY_SCALE);\n }\n if (style === 'accent') {\n return clampVelocity(velocity + ACCENT_VELOCITY_BOOST);\n }\n return velocity;\n}\n\nfunction clampVelocity(velocity: number): number {\n return Math.min(MAX_VELOCITY, Math.max(MIN_VELOCITY, Math.round(velocity)));\n}\n\n/**\n * Decorate existing notes with an ornament.\n *\n * Which notes are eligible follows from the style and the meter — ghosts fall on\n * weak positions, drags on the weak position before a strong one, flams and\n * accents on strong ones, slides on notes the line moves to, by a step or by a\n * leap — and `amount`\n * decides how many of those eligible notes are taken. The choice is addressed by\n * each note's own position, so raising `amount` adds ornaments without moving the\n * ones already there, and no onset is ever added or shifted: a decorated note is\n * the same note carrying an {@link Articulation}. A note that already carries one\n * is left alone, so ornament passes can be layered.\n *\n * With a difficulty ceiling and a tempo in the context, a passage whose strokes\n * come too fast for that ceiling is left plain — the ornament is exactly the\n * kind of candidate a ceiling exists to reject.\n *\n * @param notes The material to decorate.\n * @param opts Style, amount, meter, and context.\n * @returns Copies of the sounding notes, in input order, with ornaments applied.\n * @example\n * ```ts\n * import { generateBassLine, majorKey, makeChord, ornament } from '@libraz/libcantus';\n * const line = generateBassLine({\n * segments: [{ startBeat: 0, endBeat: 4, chord: makeChord(0, 'maj') }],\n * key: majorKey(0),\n * style: 'pop',\n * });\n * ornament(line, { style: 'ghost', amount: 0.4, ctx: 3 });\n * ```\n * Notes with a zero or negative duration never sound and are dropped, so the\n * result can be shorter than the input.\n *\n * @category Composition\n */\nexport function ornament(notes: readonly NoteEvent[], opts: OrnamentOptions = {}): NoteEvent[] {\n const asked = assertOptions(opts, 'opts');\n assertNoteEvents(notes, 'ornament notes', { allowNonPositiveDuration: true });\n const style = assertOneOf(asked.style ?? DEFAULT_STYLE, ORNAMENT_STYLES, 'ornament style');\n const ts = meterAt(0, toMeterData(asked.ts ?? DEFAULT_TS, 'ts'));\n assertTimeSignature(ts);\n if (asked.amount !== undefined) {\n assertRange(asked.amount, 0, 1, 'ornament amount');\n }\n const ctx = resolveContextWith(asked.ctx, { ornament: asked.amount });\n const amount = ctx.ornament ?? DEFAULT_AMOUNT;\n const draw = ctx.part('ornament');\n\n // Zero-length artefacts are routine in MIDI imports and never sound, so they\n // are accepted and dropped here, exactly as humanize and the analysis layer do.\n const sounding = dropSilentNotes(notes);\n const ordered = [...sounding].sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);\n const previousOf = previousOnsets(ordered);\n const nextOf = nextOnsets(ordered);\n const gaps = strokeGaps(ordered);\n\n return sounding.map((note) => {\n const decorated: NoteEvent = { ...note };\n if (\n note.articulation !== undefined ||\n !eligible(style, note, previousOf.get(note), nextOf.get(note), ts)\n ) {\n return decorated;\n }\n // The ceiling only ever rejects: an ornament this fast is a candidate the\n // player at that level would not take.\n if (!sustainsStrokes(gaps.get(note) ?? Number.POSITIVE_INFINITY, ctx.bpm, ctx.difficulty)) {\n return decorated;\n }\n if (!draw.prob(amount, style, note.startBeat, note.pitch)) {\n return decorated;\n }\n decorated.articulation = articulationOf(style);\n const velocity = ornamentedVelocity(style, note);\n if (velocity !== undefined) {\n decorated.velocity = velocity;\n }\n return decorated;\n });\n}\n\n/**\n * The last note of the previous onset before each note.\n *\n * The previous onset rather than the previous note, for the reason\n * {@link nextOnsets} looks ahead by onset: the notes of a chord share a\n * position, so the note beside one in a chord is not a step the line took and\n * cannot be the leap a slide is played into.\n */\nfunction previousOnsets(ordered: readonly NoteEvent[]): Map<NoteEvent, NoteEvent | undefined> {\n const previousOf = new Map<NoteEvent, NoteEvent | undefined>();\n let earlier: NoteEvent | undefined;\n for (const note of ordered) {\n if (earlier !== undefined && earlier.startBeat === note.startBeat) {\n previousOf.set(note, previousOf.get(earlier));\n } else {\n previousOf.set(note, earlier);\n }\n earlier = note;\n }\n return previousOf;\n}\n\n/**\n * The first note of the next onset after each note.\n *\n * The next onset rather than the next note: the notes of a chord share a\n * position and say nothing about where the line is heading.\n */\nfunction nextOnsets(ordered: readonly NoteEvent[]): Map<NoteEvent, NoteEvent | undefined> {\n const nextOf = new Map<NoteEvent, NoteEvent | undefined>();\n let later: NoteEvent | undefined;\n for (let index = ordered.length - 1; index >= 0; index -= 1) {\n const note = ordered[index];\n if (!note) continue;\n if (later !== undefined && later.startBeat === note.startBeat) {\n nextOf.set(note, nextOf.get(later));\n } else {\n nextOf.set(note, later);\n }\n later = note;\n }\n return nextOf;\n}\n\n/**\n * The tightest gap around each onset, in beats.\n *\n * Both neighbours count: a stroke is hard to place because of how close it sits\n * to the one before it as much as the one after, and reading only the following\n * onset would let the last note of a fast passage pass as unhurried.\n */\nfunction strokeGaps(ordered: readonly NoteEvent[]): Map<NoteEvent, number> {\n const distinct = [...new Set(ordered.map((note) => note.startBeat))].sort((a, b) => a - b);\n const gapAt = new Map<number, number>();\n for (let index = 0; index < distinct.length; index += 1) {\n const beat = distinct[index] ?? 0;\n const before = index === 0 ? Number.POSITIVE_INFINITY : beat - (distinct[index - 1] ?? 0);\n const after =\n index === distinct.length - 1 ? Number.POSITIVE_INFINITY : (distinct[index + 1] ?? 0) - beat;\n gapAt.set(beat, Math.min(before, after));\n }\n const gaps = new Map<NoteEvent, number>();\n for (const note of ordered) {\n gaps.set(note, gapAt.get(note.startBeat) ?? Number.POSITIVE_INFINITY);\n }\n return gaps;\n}\n","import { isMinorKey, tonicizableDegrees } from '../../analyze/functional/index.js';\nimport { InvalidInputError } from '../../core/errors/index.js';\nimport { beatsPerBar, type MeterLike } from '../../core/meter/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport {\n assertDegree,\n assertGenerationBudget,\n assertOneOf,\n assertPositiveInt,\n assertRecord,\n} from '../../core/validation/index.js';\nimport type { ChordQuality, ChordSpan } from '../../theory/chord/index.js';\nimport { chordQualities, diatonicTriad, makeChord } from '../../theory/chord/index.js';\nimport { type KeyLike, scaleTonesInDegreeOrder, toKeyScale } from '../../theory/scale/index.js';\nimport { fifthPcOf, heptatonicFrameOf } from '../../theory/tendency/index.js';\nimport {\n type GenerationContextInput,\n type ResolvedContext,\n resolveContext,\n} from '../context/index.js';\n\nexport type { ChordSpan } from '../../theory/chord/index.js';\n\n/** The meter a progression is laid out in when the caller names none. */\nconst DEFAULT_TS = { numerator: 4, denominator: 4 };\n\n/**\n * Every production style a preset may suit, in declaration order.\n *\n * Written as a table rather than only as a union so the names exist at run time:\n * a union is checked when the caller compiles, and a style arriving from a\n * config file or a JavaScript caller is checked against this.\n */\nconst PROG_STYLES = Object.freeze(['minimal', 'dance', 'idol', 'rock'] as const);\n\n/**\n * Broad production style a progression preset suits.\n *\n * @category Composition\n */\nexport type ProgStyle = (typeof PROG_STYLES)[number];\n\n/**\n * Harmonic-function role of a progression as a whole.\n *\n * @category Composition\n */\nexport type ProgFunction = 'loop' | 'tensionBuild' | 'cadenceStrong' | 'stable';\n\n/**\n * A named chord-progression preset expressed in scale degrees.\n *\n * @category Composition\n */\nexport type ProgressionPreset = {\n id: string;\n name: string;\n /**\n * The chord roots, in order, as {@link ProgressionDegree} codes: 1..7 for the\n * key's own scale degrees, counted as a musician counts them, and\n * {@link BORROWED_DEGREES} for the borrowed ones.\n */\n degrees: ProgressionDegree[];\n functional: ProgFunction;\n styles: ProgStyle[];\n};\n\n/**\n * The borrowed (non-diatonic) chord roots a preset can name, as their semitone\n * code for a borrowed degree. These are stable identifiers, not semitone\n * offsets: their pitch-class offsets are resolved internally.\n *\n * Scale degrees 1..7 address the key's own chords; these codes continue the\n * numbering for the chromatic chords a pop progression borrows, so a preset is\n * one flat list of degree codes.\n *\n * @example\n * ```ts\n * import { BORROWED_DEGREES, generateProgression, majorKey } from '@libraz/libcantus';\n * generateProgression({\n * key: majorKey(0),\n * style: 'rock',\n * bars: 4,\n * preset: { degrees: [1, BORROWED_DEGREES.bVII, 4, 1] },\n * });\n * ```\n * @category Composition\n */\nexport const BORROWED_DEGREES = Object.freeze({\n /** Flat submediant: bVI. */\n bVI: 8,\n /** Flat subtonic: bVII. */\n bVII: 10,\n /** Flat mediant: bIII. */\n bIII: 11,\n /** Minor subdominant borrowed from the parallel minor: iv. */\n iv: 12,\n /** Neapolitan: bII. */\n bII: 13,\n /** Sharp subdominant, the diminished #IV. */\n sharpIV: 14,\n} as const);\n\n/**\n * A chord root in a preset: a scale degree 1..7, or one of\n * {@link BORROWED_DEGREES}.\n *\n * @category Composition\n */\nexport type ProgressionDegree = number;\n\n/**\n * Options controlling {@link generateProgression}.\n *\n * @category Composition\n */\nexport type ProgressionOptions = {\n /**\n * The key the degrees are resolved against; a key name such as `'C major'` is\n * read as that key.\n */\n key: KeyLike;\n /**\n * Which pool of built-in presets to choose from. Ignored when `preset` names\n * the progression outright.\n */\n style: ProgStyle;\n bars: number;\n /**\n * The meter the bars are counted in, in any form that names one; a meter map\n * is read as the signature it opens in. One chord is laid out per bar, so this\n * is what the chords are spaced by — a chord every three beats in 3/4, every\n * three in 6/8 — and a progression written against another bar length drifts\n * off the bar lines of the part it is played under.\n *\n * @defaultValue `4/4`\n */\n ts?: MeterLike;\n /** Pick a specific built-in preset by id instead of choosing one by style. */\n presetId?: string;\n /**\n * Use this progression rather than a built-in one. Only `degrees` is\n * required; see {@link BORROWED_DEGREES} for the non-diatonic codes.\n */\n preset?: Partial<ProgressionPreset> & { degrees: ProgressionDegree[] };\n ext?: ChordQuality | 'auto';\n /**\n * The generation context.\n *\n * Its `complexity.harmonic` sets how much of the progression is replaced by\n * the secondary dominant of what follows: 0 — the default — leaves it alone,\n * 0.5 takes about half of what the voice-leading rules allow, and 1 takes\n * every one of them. Its `seed` fixes both the preset choice and which chords\n * are replaced, so the same seed always yields the same progression.\n *\n * @defaultValue `{ seed: 0 }`\n */\n ctx?: GenerationContextInput;\n};\n\nconst PRESETS: ProgressionPreset[] = [\n {\n id: 'fourChordPop',\n name: 'Four Chord Pop',\n degrees: [1, 5, 6, 4],\n functional: 'loop',\n styles: ['minimal', 'dance', 'idol'],\n },\n {\n id: 'pop1',\n name: 'Pop 1',\n degrees: [1, 6, 4, 5],\n functional: 'loop',\n styles: ['minimal', 'dance', 'idol'],\n },\n {\n id: 'axis',\n name: 'Axis',\n degrees: [6, 4, 1, 5],\n functional: 'loop',\n styles: ['minimal', 'dance', 'idol', 'rock'],\n },\n {\n id: 'pop2',\n name: 'Pop 2',\n degrees: [4, 1, 5, 6],\n functional: 'loop',\n styles: ['minimal', 'dance', 'idol'],\n },\n {\n id: 'classic',\n name: 'Classic',\n degrees: [1, 4, 5, 1],\n functional: 'cadenceStrong',\n styles: ['dance', 'idol', 'rock'],\n },\n {\n id: 'pop3',\n name: 'Pop 3',\n degrees: [1, 4, 6, 5],\n functional: 'loop',\n styles: ['minimal', 'dance', 'idol'],\n },\n {\n id: 'royalRoad',\n name: 'Royal Road',\n degrees: [4, 5, 3, 6],\n functional: 'tensionBuild',\n styles: ['dance', 'idol'],\n },\n {\n id: 'minor1',\n name: 'Minor 1',\n degrees: [6, 5, 4, 5],\n functional: 'tensionBuild',\n styles: ['idol', 'rock'],\n },\n {\n id: 'minor2',\n name: 'Minor 2',\n degrees: [6, 4, 5, 1],\n functional: 'tensionBuild',\n styles: ['idol', 'rock'],\n },\n {\n id: 'pop4',\n name: 'Pop 4',\n degrees: [1, 5, 3, 4],\n functional: 'loop',\n styles: ['minimal', 'dance', 'idol'],\n },\n {\n id: 'pop5',\n name: 'Pop 5',\n degrees: [1, 3, 4, 5],\n functional: 'stable',\n styles: ['minimal', 'dance', 'idol'],\n },\n {\n id: 'rock1',\n name: 'Rock 1',\n degrees: [1, 10, 4, 1],\n functional: 'tensionBuild',\n styles: ['rock'],\n },\n {\n id: 'rock2',\n name: 'Rock 2',\n degrees: [1, 4, 10, 1],\n functional: 'tensionBuild',\n styles: ['rock'],\n },\n {\n id: 'extended4',\n name: 'Extended 4',\n degrees: [1, 5, 6, 3],\n functional: 'stable',\n styles: ['minimal', 'dance'],\n },\n {\n id: 'minor3',\n name: 'Minor 3',\n degrees: [6, 1, 5, 4],\n functional: 'loop',\n styles: ['dance', 'idol'],\n },\n {\n id: 'aeolianPop',\n name: 'Aeolian Pop',\n degrees: [6, 8, 10, 1],\n functional: 'tensionBuild',\n styles: ['minimal', 'dance', 'idol', 'rock'],\n },\n {\n id: 'animeHighEnergy1',\n name: 'Anime High Energy 1',\n degrees: [6, 3, 4, 1],\n functional: 'loop',\n styles: ['dance', 'idol'],\n },\n {\n id: 'jazzPop',\n name: 'Jazz Pop',\n degrees: [2, 5, 1, 6],\n functional: 'cadenceStrong',\n styles: ['minimal', 'dance'],\n },\n {\n id: 'animeHighEnergy2',\n name: 'Anime High Energy 2',\n degrees: [6, 2, 5, 1],\n functional: 'cadenceStrong',\n styles: ['dance', 'idol'],\n },\n {\n id: 'cityPop',\n name: 'City Pop',\n degrees: [1, 6, 2, 5],\n functional: 'stable',\n styles: ['minimal', 'dance'],\n },\n {\n id: 'extended5',\n name: 'Extended 5',\n degrees: [1, 5, 6, 3, 4],\n functional: 'loop',\n styles: ['minimal', 'dance', 'idol'],\n },\n {\n id: 'neapolitanPop',\n name: 'Neapolitan Pop',\n degrees: [6, 12, 13, 5, 1],\n functional: 'cadenceStrong',\n styles: ['minimal', 'dance', 'idol'],\n },\n];\n\n/** Share of the eligible chords replaced when the caller names no dial. */\nconst DEFAULT_HARMONIC = 0;\n\n/** Chromatic semitone offset from the tonic for borrowed (non-diatonic) degrees. */\nconst BORROWED_OFFSET: Record<number, number> = {\n [BORROWED_DEGREES.bVI]: 8,\n [BORROWED_DEGREES.bVII]: 10,\n [BORROWED_DEGREES.bIII]: 3,\n [BORROWED_DEGREES.iv]: 5,\n [BORROWED_DEGREES.bII]: 1,\n [BORROWED_DEGREES.sharpIV]: 6,\n};\n\n/**\n * All built-in progression presets.\n *\n * @category Composition\n */\nexport function progressions(): ProgressionPreset[] {\n return PRESETS.map((p) => ({ ...p, degrees: [...p.degrees], styles: [...p.styles] }));\n}\n\n/**\n * Presets whose style list includes the given style.\n *\n * @category Composition\n */\nexport function progressionsByStyle(style: ProgStyle): ProgressionPreset[] {\n // A style union is checked at compile time only, so a name from a config file\n // or a JavaScript caller would otherwise select nothing — which reads exactly\n // like a style the library stocks no preset for.\n const named = assertOneOf(style, PROG_STYLES, 'style');\n return progressions().filter((p) => p.styles.includes(named));\n}\n\n/**\n * Root pitch class of a scale degree in the given key, including borrowed\n * degrees.\n *\n * Degrees are read in the key's heptatonic frame, as the numerals and the\n * tonicization targets are: a preset is written in degrees one to seven, and a\n * key with some other number of tones has no degree-for-degree frame of its own\n * to read them in. Wrapping them into a five-tone scale instead answered the\n * sixth degree with the first.\n */\nfunction degreeToRootPc(degree: number, key: KeyScale): number {\n if (degree >= 1 && degree <= 7) {\n const tones = scaleTonesInDegreeOrder(heptatonicFrameOf(key));\n return tones[degree - 1] ?? key.rootPc % 12;\n }\n const offset = BORROWED_OFFSET[degree] ?? 0;\n return ((((key.rootPc % 12) + offset) % 12) + 12) % 12;\n}\n\n/**\n * Diatonic (or borrowed) triad quality of a degree in the given key.\n *\n * Diatonic degrees (1-7) take their scale-correct triad quality, so non-major\n * keys yield diatonic chords. Borrowed degrees keep their fixed chromatic\n * qualities (`#IV` diminished, `iv` minor, the rest major).\n */\nfunction autoQuality(degree: number, key: KeyScale, harmonicDominant: boolean): ChordQuality {\n if (degree >= 1 && degree <= 7) {\n // A cadence-oriented progression needs a leading tone in minor too: use\n // the conventional harmonic-minor V rather than the natural-minor v.\n if (harmonicDominant && isMinorKey(key) && degree === 5) {\n return 'maj';\n }\n // The same frame the root was read in, so the two halves of one chord\n // cannot come from different degree spaces.\n return diatonicTriad(degree, heptatonicFrameOf(key)).quality;\n }\n if (degree === 14) {\n return 'dim';\n }\n if (degree === 12) {\n return 'min';\n }\n return 'maj';\n}\n\n/** One resolved step of a preset cycle. */\ntype CycleStep = { degree?: number; rootPc: number; quality: ChordQuality };\n\n/**\n * Resolve a preset's degrees against a key, one step per degree.\n *\n * Presets are written in scale degrees plus a handful of chromatic borrowings\n * measured from the tonic, and the two can name the same chord: `bVI` is the\n * sixth degree of a minor key, so `vi bVI bVII I` — a major-key device — turns\n * into `bVI bVI bVII i` there. The repeat is kept, which is one chord held over\n * two bars rather than a chord change: dropping it shortened the loop to three\n * bars in that key and four in every other, and a harmony that turns over every\n * three bars crosses every phrase boundary the bass, the drums and the\n * countermelody are written against.\n */\nfunction resolveCycle(\n degrees: readonly number[],\n key: KeyScale,\n ext: ProgressionOptions['ext'],\n harmonicDominant: boolean,\n): CycleStep[] {\n const steps: CycleStep[] = degrees.map((degree) => {\n const step: CycleStep = {\n rootPc: degreeToRootPc(degree, key),\n quality:\n ext !== undefined && ext !== 'auto' ? ext : autoQuality(degree, key, harmonicDominant),\n };\n if (degree >= 1 && degree <= 7) {\n step.degree = degree;\n }\n return step;\n });\n // The fallback carries no degree at all: `ChordSpan.degree` is a scale degree,\n // one to seven, and a chord that stands for a cycle with nothing in it is on\n // none of them. Numbering it zero put a value outside the field's own domain\n // into a span a caller reads.\n return steps.length > 0 ? steps : [{ rootPc: degreeToRootPc(1, key), quality: 'maj' }];\n}\n\n/**\n * The preset {@link generateProgression} would choose for a style and seed.\n *\n * The choice is otherwise invisible: the generator returns chords, not the\n * preset it drew them from, so a caller who wants to show or reproduce it has\n * no way to name it.\n *\n * @param style The style pool to choose from.\n * @param ctx The same context the generator would be given, or the seed alone.\n * @returns The preset that context selects.\n * @throws If no preset claims the style.\n * @example\n * ```ts\n * import { pickProgressionPreset } from '@libraz/libcantus';\n * pickProgressionPreset('dance', 3).name;\n * ```\n * @category Composition\n */\nexport function pickProgressionPreset(\n style: ProgStyle,\n ctx: GenerationContextInput = 0,\n): ProgressionPreset {\n return presetFor(style, resolveContext(ctx));\n}\n\n/**\n * The preset a style resolves to under a context that is already resolved.\n *\n * One top-level call resolves its context once and every draw of that call\n * comes from it, the preset choice included. Re-deriving a context from the\n * bare seed here took the choice from a source the caller never named — a\n * caller supplying its own positional source to reroll got the same loop every\n * time — and at whatever version the build defaults to, so a project\n * pinned to one version would get another version's loop under its own\n * substitutions.\n */\nfunction presetFor(style: ProgStyle, ctx: ResolvedContext): ProgressionPreset {\n // A style no preset claims is a caller error, exactly as an unknown\n // presetId is: falling back to the whole pool would answer a typo with a\n // plausible but stylistically unrelated progression.\n const pool = PRESETS.filter((preset) => preset.styles.includes(style));\n if (pool.length === 0) {\n throw new InvalidInputError(`Unknown progression style: ${style}`);\n }\n const index = ctx.part('progression').range(0, pool.length - 1, 'preset');\n return pool[index] ?? (PRESETS[0] as ProgressionPreset);\n}\n\n/**\n * The pitch classes of the degrees this key can make a local tonic.\n *\n * The degrees come from the same predicate the analysis layer names an applied\n * numeral's target with, so a chord flagged as a secondary dominant here is one\n * that layer can write `V7/x` for.\n */\nfunction tonicizableRootPcs(key: KeyScale): Set<number> {\n return new Set(tonicizableDegrees(key).map((degree) => degree.rootPc));\n}\n\n/**\n * Whether the chord a secondary dominant would resolve to can stand as a local\n * tonic.\n *\n * Nothing tonicizes a diminished triad: the supertonic of a minor key is one,\n * and a dominant seventh placed in front of it is a chord read against the home\n * key rather than as an applied dominant. Both the degree and the chord\n * actually sounding on it have to hold a perfect fifth, since a caller's `ext`\n * can force a quality the degree does not carry.\n *\n * @param span The chord the dominant would point at.\n * @param tonicizable The key's tonicizable degree roots, as pitch classes.\n */\nfunction canBeTonicized(span: ChordSpan, tonicizable: ReadonlySet<number>): boolean {\n if (!tonicizable.has(span.rootPc)) {\n return false;\n }\n return fifthPcOf(makeChord(span.rootPc, span.quality)) === (span.rootPc + 7) % 12;\n}\n\n/**\n * Generate a chord progression laid out one chord per bar.\n *\n * A preset is chosen by `presetId` when given, otherwise deterministically from\n * the presets matching `style`, seeded by the context's `seed`. An unknown\n * `presetId`, or a `style` no preset claims, is a caller error and throws rather\n * than silently falling back to a random preset. The preset's degrees cycle to\n * fill `bars`; a bar is one bar of `ts`, so `startBeat` is `barIndex` bars of\n * it — four beats apart in 4/4, three in 3/4 and in 6/8 alike. Chord\n * roots come from the key's diatonic scale-degree mapping. When `ext` is\n * omitted or `'auto'`, each chord takes its diatonic triad quality; otherwise\n * `ext` is forced on every chord — except a reharmonized chord, which is a\n * secondary dominant and is therefore always a `dom7`. With\n * `complexity.harmonic` above 0, some chords are deterministically replaced with\n * the secondary dominant (V7) of the following chord, flagged with\n * `secondaryDominant`. Only a chord the key can make a local tonic is given one:\n * nothing tonicizes a diminished triad, so the supertonic of a minor key takes\n * no dominant of its own.\n *\n * @param opts Generation options.\n * @returns One chord per bar in timeline order.\n * @throws If `presetId` matches no built-in preset, or `style` matches none.\n *\n * @example\n * ```ts\n * import { generateProgression, majorKey } from '@libraz/libcantus';\n * const chords = generateProgression({ key: majorKey(0), style: 'dance', bars: 4 });\n * // Deterministic for a given seed (defaults to 0); one ChordSpan per bar.\n * const richer = generateProgression({\n * key: majorKey(0),\n * style: 'dance',\n * bars: 4,\n * ctx: { seed: 3, complexity: { harmonic: 0.5 } },\n * });\n * // Some chords are now the secondary dominant of the chord that follows.\n * ```\n *\n * @category Composition\n */\nexport function generateProgression(opts: ProgressionOptions): ChordSpan[] {\n assertRecord(opts, 'progression options');\n assertPositiveInt(opts.bars, 'progression bars');\n assertGenerationBudget(opts.bars, 'progression chords');\n // The key is read into its plain form once, here at the boundary; the degree\n // resolution below is given the scale it resolved to.\n const key = toKeyScale(opts.key);\n const ctx = resolveContext(opts.ctx);\n let preset: ProgressionPreset | undefined;\n if (opts.preset !== undefined) {\n if (opts.preset.degrees.length === 0) {\n throw new InvalidInputError('progression preset must name at least one degree');\n }\n preset = {\n id: opts.preset.id ?? 'custom',\n name: opts.preset.name ?? 'Custom',\n degrees: opts.preset.degrees.map((degree, index) => {\n assertDegree(degree, `progression preset degrees[${index}]`);\n const supported =\n (degree >= 1 && degree <= 7) ||\n (Object.values(BORROWED_DEGREES) as number[]).includes(degree);\n if (!supported) {\n throw new InvalidInputError(\n `progression preset degrees[${index}] is not a supported progression degree; received ${degree}`,\n );\n }\n return degree;\n }),\n functional: opts.preset.functional ?? 'loop',\n styles: opts.preset.styles ?? [opts.style],\n };\n }\n if (preset === undefined && opts.presetId !== undefined) {\n preset = PRESETS.find((p) => p.id === opts.presetId);\n if (preset === undefined) {\n throw new InvalidInputError(`Unknown progression preset: ${opts.presetId}`);\n }\n }\n preset ??= presetFor(opts.style, ctx);\n const ext =\n opts.ext === undefined\n ? 'auto'\n : assertOneOf(opts.ext, ['auto', ...chordQualities()], 'progression extension');\n const cycle = resolveCycle(\n preset?.degrees ?? [0],\n key,\n ext,\n preset?.functional === 'cadenceStrong',\n );\n // One chord per bar of the meter the caller counts in, so the chord changes\n // fall on the bar lines every other part is written against.\n const barBeats = beatsPerBar(opts.ts ?? DEFAULT_TS);\n const chords: ChordSpan[] = [];\n for (let bar = 0; bar < opts.bars; bar += 1) {\n const step = cycle[bar % cycle.length];\n const chord: ChordSpan = {\n rootPc: step?.rootPc ?? 0,\n quality: step?.quality ?? 'maj',\n startBeat: bar * barBeats,\n };\n if (step?.degree !== undefined) {\n chord.degree = step.degree;\n }\n chords.push(chord);\n }\n\n const harmonic = ctx.harmonic ?? DEFAULT_HARMONIC;\n if (harmonic > 0) {\n const tonicPc = (((key.rootPc % 12) + 12) % 12) as number;\n const tonicizable = tonicizableRootPcs(key);\n const draw = ctx.part('progression');\n let tonicStatements = chords.filter((chord) => chord.rootPc === tonicPc).length;\n for (let i = 0; i < chords.length - 1; i += 1) {\n const cur = chords[i];\n const next = chords[i + 1];\n if (!cur || !next) {\n continue;\n }\n const domRoot = (next.rootPc + 7) % 12;\n const targetsTonic = (next.rootPc - tonicPc + 12) % 12 === 0;\n const isLastTonicStatement = cur.rootPc === tonicPc && tonicStatements <= 1;\n const alreadySecondary = cur.rootPc === domRoot && cur.quality === 'dom7';\n // A secondary dominant inserted at i-1 resolves onto this chord; replacing\n // it here would orphan that dominant.\n const isResolutionTarget = chords[i - 1]?.secondaryDominant === true;\n const takesNoDominant = !canBeTonicized(next, tonicizable);\n if (\n targetsTonic ||\n isLastTonicStatement ||\n alreadySecondary ||\n isResolutionTarget ||\n takesNoDominant\n ) {\n continue;\n }\n if (draw.prob(harmonic, 'reharmonize', i)) {\n if (cur.rootPc === tonicPc) {\n tonicStatements -= 1;\n }\n chords[i] = {\n rootPc: domRoot,\n quality: 'dom7',\n startBeat: cur.startBeat,\n secondaryDominant: true,\n };\n }\n }\n }\n\n return chords;\n}\n","/**\n * Reharmonization: chord substitution, modal-interchange palettes, and negative\n * harmony. These transforms take an existing chord (or a key) and propose\n * alternative harmonies that preserve a chosen relationship — a shared function,\n * a common tone, or a reflection across the key axis.\n *\n * Roots are pitch classes (0..11); no spelled key signature is required. Every\n * proposed chord nevertheless carries the spelling hints of the key it was asked\n * for, so a flat-side key names its chromatic chords with flats (the tritone\n * substitute of G7 in C major is Db7, not C#7).\n */\n\nimport { detectChordBest } from '../../analyze/detect/index.js';\nimport {\n type BorrowedSource,\n borrowedSource,\n chordToRoman,\n functionOf,\n type HarmonicFunction,\n isDiatonic,\n parallelKey,\n} from '../../analyze/functional/index.js';\nimport {\n pitchClassOf as mod12,\n type Note,\n noteToPitchClass,\n transposeNote,\n} from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport { assertOptions } from '../../core/validation/index.js';\nimport {\n type Chord,\n type ChordQuality,\n chordPitchClasses,\n diatonicTriad,\n makeChord,\n type PitchSpelling,\n} from '../../theory/chord/index.js';\nimport {\n isScaleTone,\n resolveKey,\n type SpelledKeyLike,\n scaleTonesInDegreeOrder,\n} from '../../theory/scale/index.js';\nimport { spellPitchClass } from '../../theory/spelling/index.js';\nimport { type ChordLike, toChordData } from '../../theory/symbol/index.js';\nimport { soundsDominantSeventh } from '../../theory/tendency/index.js';\n\n/**\n * The kind of substitution relationship a candidate realizes.\n *\n * An applied dominant is not among them: which dominant applies is decided by\n * the harmony that follows, and {@link substituteChord} is given one chord and a\n * key. Build one with `secondaryDominantOf` where the target is known, or let\n * `harmonizeMelody` open its vocabulary with `reharmonize: 'secondaryDominant'`\n * where the progression is being chosen as a whole.\n *\n * @category Reharmonization\n */\nexport type SubstitutionType = 'tritone' | 'relative' | 'borrowed' | 'chromaticMediant';\n\n/**\n * A proposed chord substitution with its relationship, numeral, and function.\n *\n * @category Reharmonization\n */\nexport type Substitution = {\n chord: Chord;\n type: SubstitutionType;\n roman: string;\n function: HarmonicFunction;\n};\n\n/**\n * Options controlling which substitutions {@link substituteChord} returns.\n *\n * @category Reharmonization\n */\nexport type SubstituteOptions = {\n /**\n * Melody pitch classes that must remain chord tones. When given, only\n * substitutions whose pitch classes contain every listed pitch class are\n * kept, so the melody stays consonant against the new harmony.\n */\n melodyPcs?: number[];\n};\n\n/** Count the pitch classes shared between two pitch-class sets. */\nfunction commonToneCount(a: number[], b: number[]): number {\n const set = new Set(b);\n return a.filter((pc) => set.has(pc)).length;\n}\n\n/** The plain triad quality underlying a chord's own quality. */\nfunction triadQualityOf(chord: Chord): ChordQuality {\n const semis = new Set(chord.intervals.map((interval) => mod12(interval)));\n const hasThird = semis.has(4) ? 'maj' : semis.has(3) ? 'min' : undefined;\n if (hasThird === undefined) {\n return chord.quality; // sus, power, and other non-tertian chords stand alone\n }\n if (semis.has(6) && !semis.has(7)) {\n return hasThird === 'min' ? 'dim' : 'majb5';\n }\n if (semis.has(8) && !semis.has(7)) {\n return 'aug';\n }\n return hasThird;\n}\n\n/** The diatonic triads of a key, one per scale degree. */\nfunction diatonicTriadsOf(key: KeyScale): Chord[] {\n const degrees = scaleTonesInDegreeOrder(key).length;\n const triads: Chord[] = [];\n for (let degree = 1; degree <= degrees; degree += 1) {\n triads.push(diatonicTriad(degree, key));\n }\n return triads;\n}\n\n/** Whether two chords share the same root pitch class and quality. */\nfunction sameChord(a: Chord, b: Chord): boolean {\n return mod12(a.rootPc) === mod12(b.rootPc) && a.quality === b.quality;\n}\n\n/** Root offsets (semitones) that form a third above or below a root. */\nconst THIRD_OFFSETS = [3, 4, 8, 9] as const;\n\n/** Widest alteration a {@link PitchSpelling} hint may carry: a double accidental. */\nconst MAX_HINT_ALTER = 2;\n\n/** Reduce a spelled note to the bare letter/alter a chord records as a hint. */\nfunction bareSpelling(note: Note): PitchSpelling {\n return { letter: note.letter, alter: note.alter };\n}\n\n/**\n * Copy a chord with `root` recorded as its root spelling, plus the key's own\n * spelling of the bass when one is set.\n *\n * A hint is only worth carrying while it still names the pitch class it belongs\n * to, so a root spelling that does not — or that would need more than a double\n * accidental — gives way to the key's spelling of that pitch class.\n */\nfunction spelledOnRoot(chord: Chord, root: Note, tonic: Note, key: KeyScale): Chord {\n const usable =\n noteToPitchClass(root) === mod12(chord.rootPc) && Math.abs(root.alter) <= MAX_HINT_ALTER;\n const spelled: Chord = {\n ...chord,\n rootSpelling: bareSpelling(usable ? root : spellPitchClass(chord.rootPc, tonic, key)),\n };\n if (chord.bassPc !== undefined) {\n spelled.bassSpelling = bareSpelling(spellPitchClass(chord.bassPc, tonic, key));\n }\n return spelled;\n}\n\n/** Copy a chord with the key's own spelling of its root and bass attached. */\nfunction spelledInKey(chord: Chord, tonic: Note, key: KeyScale): Chord {\n return spelledOnRoot(chord, spellPitchClass(chord.rootPc, tonic, key), tonic, key);\n}\n\n/**\n * The root spelling of a tritone substitute: the flattened supertonic of the\n * chord the substituted dominant resolves to, since the substitute is that\n * chord's bII7. G7 resolves to C, so its substitute is written Db7 — never C#7,\n * whichever letter the pitch class alone would suggest.\n *\n * A pitch class the key already spells as a scale degree keeps that letter\n * instead: the same substitution read in B major is the II7 written C#7.\n */\nfunction tritoneSubstituteRoot(dominant: Chord, tonic: Note, key: KeyScale): Note {\n const substituteRootPc = mod12(dominant.rootPc + 6);\n if (isScaleTone(substituteRootPc, key)) {\n return spellPitchClass(substituteRootPc, tonic, key);\n }\n const resolution = spellPitchClass(mod12(dominant.rootPc + 5), tonic, key);\n return transposeNote(resolution, 1);\n}\n\n/**\n * Propose substitutions for a chord within a key.\n *\n * Candidates are generated by several relationships and deduplicated against the\n * original chord and each other:\n *\n * - `tritone`: for a dominant-type chord, the dominant seventh a tritone away.\n * - `relative`: a diatonic triad a third away that shares two common tones.\n * - `borrowed`: a triad from the parallel mode with the same harmonic function.\n * - `chromaticMediant`: a major/minor triad a third away sharing one common\n * tone.\n *\n * These four are the whole vocabulary. An applied dominant is not proposed\n * because the chord it applies to is the one that follows, which a call about a\n * single chord cannot see; `secondaryDominantOf` builds one from a named target.\n *\n * Each result carries its Roman numeral and harmonic function in `key`. When\n * `opts.melodyPcs` is given, only substitutions whose pitch classes contain all\n * of those pitch classes are returned.\n *\n * Every substitution is spelled the way `key` writes it, as `rootSpelling` and\n * `bassSpelling` hints, so a flat-side key keeps flat names: in C major the\n * tritone substitute of G7 formats as `Db7` and its chromatic mediants as `Bb`\n * and `Eb`. Where the substitution's function and the bare pitch class disagree,\n * the function decides — a tritone substitute is a bII7 and so is written on the\n * flattened supertonic of the chord it resolves to, and a borrowed chord takes\n * the spelling of the mode it is borrowed from.\n *\n * @param chord The chord to reharmonize, as a chord symbol or as chord data.\n * @param key The prevailing key, as a key name or as a key/scale.\n * @param opts Optional melody-preservation constraint.\n * @returns The deduplicated substitution candidates.\n * @example\n * ```ts\n * import { substituteChord, makeChord, majorKey, formatChordSymbol } from '@libraz/libcantus';\n * const subs = substituteChord(makeChord(7, 'dom7'), majorKey(0));\n * subs\n * .filter((sub) => sub.type === 'tritone')\n * .map((sub) => formatChordSymbol(sub.chord)); // ['Db7'] — the bII7 of C major, not 'C#7'\n * ```\n * @category Reharmonization\n */\nexport function substituteChord(\n chord: ChordLike,\n key: SpelledKeyLike,\n opts?: SubstituteOptions,\n): Substitution[] {\n assertOptions(opts, 'opts');\n // Chord and key are read once, here at the boundary. The key keeps the tonic\n // it was named with: everything below spells from that tonic, so the letters\n // are the caller's key's rather than the ones its pitch classes read best as.\n const source = toChordData(chord);\n const { scale, tonic } = resolveKey(key);\n // The relative test counts common tones against the chord's triad, not its\n // full pitch-class set: an exact count of two would otherwise be decided by\n // how many tensions the input carries, so C proposes both Em and Am while\n // Cmaj7 proposes only Am — penalising the candidate that overlaps *more*.\n const originalTriad = chordPitchClasses(makeChord(source.rootPc, triadQualityOf(source)));\n const candidates: { chord: Chord; type: SubstitutionType }[] = [];\n\n // Tritone substitution: only for dominant-type chords.\n if (soundsDominantSeventh(source)) {\n const substitute = makeChord(mod12(source.rootPc + 6), 'dom7');\n candidates.push({\n chord: spelledOnRoot(substitute, tritoneSubstituteRoot(source, tonic, scale), tonic, scale),\n type: 'tritone',\n });\n }\n\n // Relative: a diatonic triad a third away sharing two common tones.\n for (const triad of diatonicTriadsOf(scale)) {\n const offset = mod12(triad.rootPc - source.rootPc);\n if (\n (THIRD_OFFSETS as readonly number[]).includes(offset) &&\n commonToneCount(originalTriad, chordPitchClasses(triad)) === 2\n ) {\n candidates.push({ chord: spelledInKey(triad, tonic, scale), type: 'relative' });\n }\n }\n\n // Borrowed: a parallel-mode triad sharing the original's harmonic function.\n // It is written the way the mode it comes from writes it, which is also how\n // the key names the borrowing: C major borrows Eb and Ab from C minor.\n const targetFunction = functionOf(source, scale);\n const parallel = parallelKey(scale);\n for (const triad of diatonicTriadsOf(parallel)) {\n if (functionOf(triad, scale) === targetFunction && !isDiatonic(triad, scale)) {\n candidates.push({ chord: spelledInKey(triad, tonic, parallel), type: 'borrowed' });\n }\n }\n\n // Chromatic mediants: major/minor triads a third away sharing one common tone.\n for (const offset of THIRD_OFFSETS) {\n for (const quality of ['maj', 'min'] as const) {\n const mediant = makeChord(mod12(source.rootPc + offset), quality);\n if (\n commonToneCount(originalTriad, chordPitchClasses(mediant)) === 1 &&\n !isDiatonic(mediant, scale)\n ) {\n candidates.push({ chord: spelledInKey(mediant, tonic, scale), type: 'chromaticMediant' });\n }\n }\n }\n\n const melodyPcs = opts?.melodyPcs?.map(mod12);\n const seen = new Set<string>();\n const results: Substitution[] = [];\n for (const candidate of candidates) {\n if (sameChord(candidate.chord, source)) {\n continue;\n }\n const dedupeKey = `${mod12(candidate.chord.rootPc)}:${candidate.chord.quality}`;\n if (seen.has(dedupeKey)) {\n continue;\n }\n seen.add(dedupeKey);\n if (melodyPcs !== undefined) {\n const pcs = new Set(chordPitchClasses(candidate.chord));\n if (!melodyPcs.every((pc) => pcs.has(pc))) {\n continue;\n }\n }\n results.push({\n chord: candidate.chord,\n type: candidate.type,\n roman: chordToRoman(candidate.chord, scale),\n function: functionOf(candidate.chord, scale),\n });\n }\n return results;\n}\n\n/**\n * A borrowed chord in the modal-interchange palette of a key.\n *\n * @category Reharmonization\n */\nexport type BorrowedChord = {\n chord: Chord;\n roman: string;\n source: BorrowedSource;\n};\n\n/**\n * The modal-interchange palette of a key.\n *\n * Lists the diatonic triads of the parallel key that are not diatonic to\n * `key` (the classic borrowed chords such as `iv`, `bVI`, and `bVII` in a major\n * key), plus the Neapolitan (a major triad on the flat second degree). Each\n * carries its Roman numeral and {@link borrowedSource} relative to `key`.\n *\n * Every chord is spelled as the mode it is borrowed from writes it, recorded as\n * a `rootSpelling` hint: C major borrows `Fm`, `Ab` and `Bb` from C minor and\n * takes `Db` as its Neapolitan, while a minor key borrows in the other direction\n * (A minor borrows `A`, `D` and `F#m` from A major, with `Bb` as its\n * Neapolitan). The major dominant of a minor key — `E` in A minor — is not among\n * them: raising the seventh degree is an alteration inside the key rather than a\n * chord taken from the parallel mode.\n *\n * @param key The prevailing key, as a key name or as a key/scale.\n * @returns The borrowed-chord palette.\n * @example\n * ```ts\n * import { modalInterchangePalette, majorKey, formatChordSymbol } from '@libraz/libcantus';\n * const palette = modalInterchangePalette(majorKey(0));\n * palette.map((borrowed) => formatChordSymbol(borrowed.chord));\n * // ['Cm', 'Ddim', 'Eb', 'Fm', 'Gm', 'Ab', 'Bb', 'Db'] — flat-side, not 'D#'/'G#'/'A#'\n * ```\n * @category Reharmonization\n */\nexport function modalInterchangePalette(key: SpelledKeyLike): BorrowedChord[] {\n // The key is read once, here at the boundary, and keeps the tonic it was\n // named with: every chord below is spelled from that tonic.\n const { scale, tonic } = resolveKey(key);\n const palette: BorrowedChord[] = [];\n const seen = new Set<string>();\n const add = (chord: Chord) => {\n const dedupeKey = `${mod12(chord.rootPc)}:${chord.quality}`;\n if (seen.has(dedupeKey)) {\n return;\n }\n seen.add(dedupeKey);\n palette.push({\n chord,\n roman: chordToRoman(chord, scale),\n source: borrowedSource(chord, scale),\n });\n };\n\n const parallel = parallelKey(scale);\n for (const triad of diatonicTriadsOf(parallel)) {\n if (!isDiatonic(triad, scale) && borrowedSource(triad, scale) !== null) {\n add(spelledInKey(triad, tonic, parallel));\n }\n }\n // The Neapolitan (major triad on b2) sits outside both parallel modes, so it\n // is added explicitly — unless b2 is already diatonic (e.g. in a Phrygian key,\n // where it is a native chord rather than a borrowing). Being a bII, it is\n // written a minor second above the tonic: Db in C major, C in B major.\n const neapolitan = makeChord(mod12(scale.rootPc + 1), 'maj');\n if (!isDiatonic(neapolitan, scale)) {\n add(spelledOnRoot(neapolitan, transposeNote(tonic, 1), tonic, scale));\n }\n return palette;\n}\n\n/**\n * Reflect a chord across the tonic–dominant axis of a key (negative harmony).\n *\n * Every pitch class `p` maps to `(2*T + 7 - p) mod 12`, where `T` is the key\n * tonic — the reflection that swaps the tonic with the dominant. The mirrored\n * pitch classes (including the bass, if set) are reassembled into a chord by\n * {@link detectChordBest}; when no chord is recognized, the mirrored root and\n * the original quality are used as a fallback.\n *\n * The result is spelled the way `key` writes it, as `rootSpelling` and\n * `bassSpelling` hints, so the reflection of a chord in a flat-side key keeps\n * flat names: in Eb major the mirror of Bb formats as `Abm`, not `G#m`.\n *\n * @param chord The chord to reflect, as a chord symbol or as chord data.\n * @param key The prevailing key, as a key name or as a key/scale.\n * @returns The negative-harmony counterpart of the chord.\n * @example\n * ```ts\n * import { negativeHarmonyMirror, makeChord, majorKey } from '@libraz/libcantus';\n * negativeHarmonyMirror(makeChord(7, 'maj'), majorKey(0));\n * // G major reflected across C's axis => F minor (rootPc 5, quality 'min')\n * ```\n * @category Reharmonization\n */\nexport function negativeHarmonyMirror(chord: ChordLike, key: SpelledKeyLike): Chord {\n // Chord and key are read once, here at the boundary. The key keeps the tonic\n // it was named with, which is what the mirrored chord is spelled from.\n const source = toChordData(chord);\n const { scale, tonic } = resolveKey(key);\n const tonicPc = mod12(scale.rootPc);\n const mirror = (p: number) => mod12(2 * tonicPc + 7 - p);\n const pcs = chordPitchClasses(source).map(mirror);\n const bassPc = source.bassPc !== undefined ? mirror(source.bassPc) : undefined;\n // Place the mirrored bass below the rest so it is recognized as the lowest\n // note and preserved through detection. The others move up rather than the\n // bass moving down: a pitch class is already at the bottom of the MIDI range,\n // so dropping one an octave leaves it, and every chord carrying a bass was\n // refused by the pitch check rather than mirrored.\n const pitches = bassPc !== undefined ? [bassPc, ...pcs.map((pc) => pc + 12)] : pcs;\n const detected = detectChordBest(pitches);\n if (detected) {\n // Without an input bass the mirrored tones are octave-less, so detection may\n // read the lowest pitch class as a slash bass and report a spurious\n // inversion. Restore the recognized chord to root position in that case.\n if (bassPc === undefined && detected.bassPc !== undefined) {\n return spelledInKey(makeChord(detected.rootPc, detected.quality), tonic, scale);\n }\n return spelledInKey(detected, tonic, scale);\n }\n const quality: ChordQuality = source.quality;\n return spelledInKey(makeChord(mirror(source.rootPc), quality, bassPc), tonic, scale);\n}\n","/**\n * Deterministic rhythm generation over a metric grid.\n *\n * Builds a fixed-resolution grid across one or more bars of a time signature\n * and decides, per grid slot, whether it becomes a note onset. Onset\n * probability follows the metric-accent hierarchy from the `meter` module, so\n * strong positions (downbeats, secondary pulses) are favored over weak\n * off-pulse subdivisions. Given a seed the output is fully reproducible.\n *\n * Positions and durations are measured in quarter-note beats, matching the\n * library-wide convention.\n */\n\nimport {\n beatsPerBar,\n type MeterLike,\n meterAt,\n metricWeight,\n toMeterData,\n} from '../../core/meter/index.js';\nimport type { NoteEvent } from '../../core/types.js';\nimport {\n assertArray,\n assertGenerationBudget,\n assertInteger,\n assertMidiPitch,\n assertOptions,\n assertPositiveInt,\n} from '../../core/validation/index.js';\nimport { type GenerationContextInput, resolveContext } from '../context/index.js';\n\n/**\n * A single rhythmic note: an onset position and how long it sounds.\n *\n * @category Rhythm & Meter\n */\nexport type RhythmEvent = {\n /** Onset position in quarter-note beats, absolute from the start of the span. */\n position: number;\n /** Duration in quarter-note beats, extending to the next onset. */\n duration: number;\n};\n\n/** Velocity given to converted rhythm events when the caller names none. */\nconst DEFAULT_RHYTHM_VELOCITY = 96;\n\n/**\n * Turn rhythm events into note events at a fixed pitch.\n *\n * {@link RhythmEvent} names its fields `position`/`duration` because a rhythm\n * has no pitch; every other generator in the library speaks {@link NoteEvent}.\n * This is the bridge, so a generated rhythm can be humanized, grooved, or\n * written out like any other line.\n *\n * @param events The rhythm events to convert.\n * @param pitch The MIDI pitch to give every note.\n * @param velocity Velocity for every note; defaults to a mezzo-forte 96.\n * @returns The note events, in the order given.\n * @throws If the pitch or the velocity is not a whole MIDI value in [0, 127].\n * @example\n * ```ts\n * import { generateRhythm, humanize, rhythmToNoteEvents } from '@libraz/libcantus';\n * const rhythm = generateRhythm({ numerator: 4, denominator: 4 }, { bars: 2 });\n * humanize(rhythmToNoteEvents(rhythm, 38));\n * ```\n * @category Rhythm & Meter\n */\nexport function rhythmToNoteEvents(\n events: RhythmEvent[],\n pitch: number,\n velocity: number = DEFAULT_RHYTHM_VELOCITY,\n): NoteEvent[] {\n // Both fields are checked here rather than left to whatever reads the result:\n // this builds a note event without going through the note-event guard, so a\n // pitch or a velocity the guard would refuse would otherwise reach a public\n // note by the one path that does not ask. The domains are the guard's — whole\n // MIDI quantities, not merely finite numbers.\n assertMidiPitch(pitch, 'pitch');\n assertInteger(velocity, 'velocity', 0, 127);\n return assertArray<RhythmEvent>(events, 'events').map((event) => ({\n pitch,\n startBeat: event.position,\n durationBeat: event.duration,\n velocity,\n }));\n}\n\n/**\n * Options controlling {@link generateRhythm}.\n *\n * @category Rhythm & Meter\n */\nexport type RhythmOptions = {\n /**\n * Number of bars to generate.\n *\n * @defaultValue 1\n */\n bars?: number;\n /**\n * Grid resolution as the number of equal grid steps per quarter-note beat.\n * The default of 2 yields an eighth-note grid; 4 yields a sixteenth-note\n * grid, 3 an eighth-note triplet grid, and so on.\n *\n * @defaultValue 2\n */\n subdivision?: number;\n /**\n * The generation context.\n *\n * Its `complexity.rhythmic` is this generator's overall onset density: it\n * scales the per-slot onset probability, so higher values fill more grid\n * slots. A value outside [0, 1] is rejected. Its `seed` fixes which slots are\n * taken, so the same seed always yields the same pattern.\n *\n * @defaultValue `{ seed: 0, complexity: { rhythmic: 0.5 } }`\n */\n ctx?: GenerationContextInput;\n};\n\nconst DEFAULT_SUBDIVISION = 2;\nconst DEFAULT_DENSITY = 0.5;\nconst DEFAULT_BARS = 1;\n\n/**\n * Map a metric weight (0–3 from {@link metricWeight}) to a base onset\n * probability before density scaling. Strong positions are much more likely to\n * carry an onset than weak ones; the curve is intentionally small and explicit\n * so its behavior is easy to inspect and test.\n *\n * @param weight The metric weight, 0 (off-pulse) to 3 (downbeat).\n * @returns A base onset probability in [0, 1].\n * @category Rhythm & Meter\n */\nexport function onsetWeightCurve(weight: number): number {\n // The rank is a position on a stated scale, so one off it is refused rather\n // than folded into the lowest: sharing an answer with the off-pulse rank would\n // make a weight nobody computed read as a weak position of a real bar.\n assertInteger(weight, 'weight', 0, 3);\n switch (weight) {\n case 3:\n return 1;\n case 2:\n return 0.8;\n case 1:\n return 0.55;\n default:\n return 0.25;\n }\n}\n\n/**\n * Generate a deterministic rhythmic pattern over `bars` bars of a time\n * signature. A grid is built at the chosen subdivision; each slot becomes an\n * onset when the draw belonging to that slot falls under its metric weight\n * scaled by the density dial. Because the draw belongs to the slot rather than\n * to the order the slots were visited, raising the dial only adds onsets:\n * everything already sounding stays where it was. Every bar's downbeat is always\n * an onset, so a pattern of any length has a reliable pulse independent of the\n * seed. Each event's duration extends to the next onset, and the last event\n * extends to the end of the span.\n *\n * @param ts The time signature, in any form that names one.\n * @param opts Generation options.\n * @returns Onset events sorted by position, non-overlapping, covering the span.\n * @example\n * ```ts\n * import { generateRhythm } from '@libraz/libcantus';\n * generateRhythm('4/4', { ctx: { seed: 42, complexity: { rhythmic: 0.6 } } });\n * // onset events over one bar\n * ```\n * @category Rhythm & Meter\n */\nexport function generateRhythm(ts: MeterLike, opts: RhythmOptions = {}): RhythmEvent[] {\n // The meter is read once, here: the grid below is one repeated bar, so a\n // caller who names a whole map gets the signature the map opens in.\n const asked = assertOptions(opts, 'opts');\n const meter = meterAt(0, toMeterData(ts, 'ts'));\n const bars = asked.bars ?? DEFAULT_BARS;\n const subdivision = asked.subdivision ?? DEFAULT_SUBDIVISION;\n const ctx = resolveContext(asked.ctx);\n const density = ctx.rhythmic ?? DEFAULT_DENSITY;\n\n assertPositiveInt(bars, 'rhythm bars');\n assertPositiveInt(subdivision, 'rhythm subdivision');\n\n const barBeats = beatsPerBar(meter);\n const spanBeats = barBeats * bars;\n const step = 1 / subdivision;\n const slotsPerBar = Math.ceil(barBeats * subdivision);\n const slotCount = slotsPerBar * bars;\n assertGenerationBudget(slotCount, 'rhythm grid slots');\n const draw = ctx.part('rhythm');\n\n const positions: number[] = [];\n for (let bar = 0; bar < bars; bar += 1) {\n const barStart = bar * barBeats;\n const barEnd = barStart + barBeats;\n for (let slot = 0; slot < slotsPerBar; slot += 1) {\n const position = barStart + slot * step;\n if (position >= barEnd - Number.EPSILON) {\n break;\n }\n // Force every bar's downbeat so the pulse is reliable regardless of seed.\n if (slot === 0) {\n positions.push(position);\n continue;\n }\n const weight = metricWeight(position, meter);\n const probability = onsetWeightCurve(weight) * density;\n if (draw.prob(probability, 'onset', bar, slot)) {\n positions.push(position);\n }\n }\n }\n\n const events: RhythmEvent[] = [];\n for (let i = 0; i < positions.length; i += 1) {\n const position = positions[i] ?? 0;\n const nextPosition = positions[i + 1] ?? spanBeats;\n events.push({ position, duration: nextPosition - position });\n }\n return events;\n}\n\n/**\n * Onset density of a generated pattern: the mean number of onsets per bar.\n *\n * @param events Events from {@link generateRhythm}.\n * @param ts The time signature, in any form that names one.\n * @returns The onset count divided by the number of bars the events span.\n * @category Rhythm & Meter\n */\nexport function rhythmDensity(events: RhythmEvent[], ts: MeterLike): number {\n assertArray<RhythmEvent>(events, 'events');\n // Read before the empty case, so a malformed meter is rejected whichever way\n // the call leaves rather than only when there is something to count.\n const barBeats = beatsPerBar(ts);\n if (events.length === 0) {\n return 0;\n }\n const last = events[events.length - 1];\n const spanBeats = (last?.position ?? 0) + (last?.duration ?? 0);\n const bars = spanBeats / barBeats;\n return bars > 0 ? events.length / bars : 0;\n}\n"]}