@libraz/libcantus 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/NOTICE +63 -0
  2. package/README.md +3 -1
  3. package/README_ja.md +13 -11
  4. package/dist/analyze/index.cjs +7 -7
  5. package/dist/analyze/index.cjs.map +1 -1
  6. package/dist/analyze/index.d.cts +14 -12
  7. package/dist/analyze/index.d.ts +14 -12
  8. package/dist/analyze/index.js +6 -6
  9. package/dist/{chunk-NB25P3EH.js → chunk-2G5MCGYU.js} +33 -11
  10. package/dist/chunk-2G5MCGYU.js.map +1 -0
  11. package/dist/{chunk-NM7UAMC3.cjs → chunk-3FDBMIPR.cjs} +64 -64
  12. package/dist/chunk-3FDBMIPR.cjs.map +1 -0
  13. package/dist/{chunk-YFA7X2F6.cjs → chunk-3MDOEFIQ.cjs} +411 -327
  14. package/dist/chunk-3MDOEFIQ.cjs.map +1 -0
  15. package/dist/{chunk-IPEF6NWB.js → chunk-5MWRBYAT.js} +278 -154
  16. package/dist/chunk-5MWRBYAT.js.map +1 -0
  17. package/dist/{chunk-B2PW6EHN.cjs → chunk-6TTBEON3.cjs} +1253 -889
  18. package/dist/chunk-6TTBEON3.cjs.map +1 -0
  19. package/dist/{chunk-UMWZ4ZET.js → chunk-7NQ6AL3H.js} +4 -4
  20. package/dist/chunk-7NQ6AL3H.js.map +1 -0
  21. package/dist/{chunk-PWC4CRWT.cjs → chunk-E3QO3UET.cjs} +31 -23
  22. package/dist/chunk-E3QO3UET.cjs.map +1 -0
  23. package/dist/{chunk-GNVN4PPJ.js → chunk-FJFULJZE.js} +531 -213
  24. package/dist/chunk-FJFULJZE.js.map +1 -0
  25. package/dist/{chunk-CPMB7P75.js → chunk-FK3DGAIZ.js} +608 -244
  26. package/dist/chunk-FK3DGAIZ.js.map +1 -0
  27. package/dist/chunk-GEDTJHAC.cjs.map +1 -1
  28. package/dist/{chunk-RFIBF2RR.js → chunk-H7FVMBUC.js} +138 -65
  29. package/dist/chunk-H7FVMBUC.js.map +1 -0
  30. package/dist/{chunk-4R2LWV7E.cjs → chunk-HYSVSFNI.cjs} +664 -540
  31. package/dist/chunk-HYSVSFNI.cjs.map +1 -0
  32. package/dist/{chunk-35NBV2FT.cjs → chunk-KMYGRFZ6.cjs} +47 -24
  33. package/dist/chunk-KMYGRFZ6.cjs.map +1 -0
  34. package/dist/{chunk-IFZDWIKM.cjs → chunk-LQZQDBCD.cjs} +1048 -730
  35. package/dist/chunk-LQZQDBCD.cjs.map +1 -0
  36. package/dist/{chunk-PM4XJE2Y.cjs → chunk-P3CPXAJ5.cjs} +76 -54
  37. package/dist/chunk-P3CPXAJ5.cjs.map +1 -0
  38. package/dist/{chunk-S5BKZZGM.cjs → chunk-S3QELJUE.cjs} +72 -56
  39. package/dist/chunk-S3QELJUE.cjs.map +1 -0
  40. package/dist/{chunk-PUKJP2Z5.js → chunk-TXFWSU5D.js} +31 -8
  41. package/dist/chunk-TXFWSU5D.js.map +1 -0
  42. package/dist/{chunk-76ZBTRG2.cjs → chunk-U4EQTDGN.cjs} +258 -218
  43. package/dist/chunk-U4EQTDGN.cjs.map +1 -0
  44. package/dist/chunk-UGLFDG7L.cjs.map +1 -1
  45. package/dist/{chunk-MLBPXA4Z.js → chunk-UTE5A36R.js} +198 -114
  46. package/dist/chunk-UTE5A36R.js.map +1 -0
  47. package/dist/{chunk-ZBUROTJQ.js → chunk-UZKQISRF.js} +17 -9
  48. package/dist/chunk-UZKQISRF.js.map +1 -0
  49. package/dist/{chunk-7KG3RTYS.js → chunk-VL5NXVCN.js} +281 -155
  50. package/dist/chunk-VL5NXVCN.js.map +1 -0
  51. package/dist/{chunk-CGBADIO5.cjs → chunk-WOMCXN2W.cjs} +328 -202
  52. package/dist/chunk-WOMCXN2W.cjs.map +1 -0
  53. package/dist/{chunk-3R3QVKY5.cjs → chunk-YDGMWH43.cjs} +397 -324
  54. package/dist/chunk-YDGMWH43.cjs.map +1 -0
  55. package/dist/{chunk-AGOVW7U6.js → chunk-YYVXRV3V.js} +72 -56
  56. package/dist/chunk-YYVXRV3V.js.map +1 -0
  57. package/dist/{chunk-M5RYMAFI.js → chunk-ZLNSH4UL.js} +71 -31
  58. package/dist/chunk-ZLNSH4UL.js.map +1 -0
  59. package/dist/{coerce-DRERzdve.d.cts → coerce-CUhHc3Tu.d.ts} +16 -8
  60. package/dist/{coerce-DP0vYu-x.d.ts → coerce-Ytt2Eiq9.d.cts} +16 -8
  61. package/dist/core/index.cjs +5 -5
  62. package/dist/core/index.cjs.map +1 -1
  63. package/dist/core/index.d.cts +9 -8
  64. package/dist/core/index.d.ts +9 -8
  65. package/dist/core/index.js +4 -4
  66. package/dist/generate/index.cjs +8 -8
  67. package/dist/generate/index.cjs.map +1 -1
  68. package/dist/generate/index.d.cts +28 -18
  69. package/dist/generate/index.d.ts +28 -18
  70. package/dist/generate/index.js +7 -7
  71. package/dist/{index-BMweUlm5.d.cts → index-5VJgKz_Q.d.cts} +6 -2
  72. package/dist/{index-BHIUyjhZ.d.ts → index-BLuDesVG.d.ts} +20 -13
  73. package/dist/{index-B9QLziDe.d.cts → index-BV6APVa6.d.cts} +20 -13
  74. package/dist/{index-CL4b4gUQ.d.ts → index-Bf0Sv8a9.d.ts} +6 -2
  75. package/dist/{index-Dw2DP7vm.d.ts → index-BkT21EMv.d.cts} +3 -2
  76. package/dist/{index-BL5N2Oyz.d.cts → index-C7KCaTRz.d.ts} +3 -2
  77. package/dist/{index-Cy15oTvh.d.ts → index-C9MYbuKS.d.cts} +7 -3
  78. package/dist/{index-Cy15oTvh.d.cts → index-C9MYbuKS.d.ts} +7 -3
  79. package/dist/{index-CY-3NVfy.d.cts → index-DVInzCDS.d.cts} +53 -29
  80. package/dist/{index-9eoQs2C1.d.ts → index-Dg-SAfhz.d.ts} +6 -4
  81. package/dist/{index-zaZdmnU4.d.ts → index-jgeug9EA.d.ts} +53 -29
  82. package/dist/{index-cHvbYgJI.d.cts → index-pQihL4z7.d.cts} +6 -4
  83. package/dist/index.cjs +13 -13
  84. package/dist/index.cjs.map +1 -1
  85. package/dist/index.d.cts +11 -11
  86. package/dist/index.d.ts +11 -11
  87. package/dist/index.js +12 -12
  88. package/dist/{masks-Cc7-MzzY.d.ts → masks-BnzXnBj0.d.cts} +3 -2
  89. package/dist/{masks-pADfTOiD.d.cts → masks-tycmkrgs.d.ts} +3 -2
  90. package/dist/model/index.cjs +12 -12
  91. package/dist/model/index.cjs.map +1 -1
  92. package/dist/model/index.d.cts +63 -48
  93. package/dist/model/index.d.ts +63 -48
  94. package/dist/model/index.js +11 -11
  95. package/dist/{positional-DJgs6ltD.d.ts → positional-CscpxtlE.d.cts} +3 -2
  96. package/dist/{positional-DMZer-FM.d.cts → positional-DLnfUgqG.d.ts} +3 -2
  97. package/dist/{profile-DLdlIpS9.d.ts → profile-B4WqcM4H.d.ts} +25 -17
  98. package/dist/{profile-CQMEqdH9.d.cts → profile-BJd1XkPB.d.cts} +25 -17
  99. package/dist/theory/index.cjs +5 -5
  100. package/dist/theory/index.cjs.map +1 -1
  101. package/dist/theory/index.d.cts +18 -14
  102. package/dist/theory/index.d.ts +18 -14
  103. package/dist/theory/index.js +4 -4
  104. package/dist/{types-D-XgxGvp.d.cts → types-7mGFwvMm.d.ts} +45 -22
  105. package/dist/{types-Dh7C064C.d.ts → types-_ofpb2be.d.cts} +45 -22
  106. package/package.json +13 -13
  107. package/dist/chunk-35NBV2FT.cjs.map +0 -1
  108. package/dist/chunk-3R3QVKY5.cjs.map +0 -1
  109. package/dist/chunk-4R2LWV7E.cjs.map +0 -1
  110. package/dist/chunk-76ZBTRG2.cjs.map +0 -1
  111. package/dist/chunk-7KG3RTYS.js.map +0 -1
  112. package/dist/chunk-AGOVW7U6.js.map +0 -1
  113. package/dist/chunk-B2PW6EHN.cjs.map +0 -1
  114. package/dist/chunk-CGBADIO5.cjs.map +0 -1
  115. package/dist/chunk-CPMB7P75.js.map +0 -1
  116. package/dist/chunk-GNVN4PPJ.js.map +0 -1
  117. package/dist/chunk-IFZDWIKM.cjs.map +0 -1
  118. package/dist/chunk-IPEF6NWB.js.map +0 -1
  119. package/dist/chunk-M5RYMAFI.js.map +0 -1
  120. package/dist/chunk-MLBPXA4Z.js.map +0 -1
  121. package/dist/chunk-NB25P3EH.js.map +0 -1
  122. package/dist/chunk-NM7UAMC3.cjs.map +0 -1
  123. package/dist/chunk-PM4XJE2Y.cjs.map +0 -1
  124. package/dist/chunk-PUKJP2Z5.js.map +0 -1
  125. package/dist/chunk-PWC4CRWT.cjs.map +0 -1
  126. package/dist/chunk-RFIBF2RR.js.map +0 -1
  127. package/dist/chunk-S5BKZZGM.cjs.map +0 -1
  128. package/dist/chunk-UMWZ4ZET.js.map +0 -1
  129. package/dist/chunk-YFA7X2F6.cjs.map +0 -1
  130. package/dist/chunk-ZBUROTJQ.js.map +0 -1
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/theory/chordscale/index.ts","../src/theory/harmony/index.ts"],"sourcesContent":["import { InvalidInputError } from '../../core/errors/index.js';\nimport { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\nimport {\n assertArray,\n assertGenerationBudget,\n assertOptions,\n assertPitchClass,\n assertPositiveInt,\n} from '../../core/validation/index.js';\nimport type { Chord } from '../chord/index.js';\nimport { chordPitchClasses, displacedThirds } from '../chord/index.js';\nimport type { ScaleNameInput } from '../scale/index.js';\nimport {\n assertModeMask,\n NAMED_SCALES,\n namedScaleMask,\n requireScaleMask,\n resolveScaleName,\n} from '../scale/index.js';\nimport { type ChordLike, toChordData } from '../symbol/index.js';\n\n/**\n * The mask of a scale a chord-scale question may be asked about.\n *\n * Chord-scale theory is a Western practice, so the register of world scales is\n * not a vocabulary these functions answer from: a raga offered as the scale\n * over an altered dominant would read as an answer while being a category\n * error. {@link chordScales} proposes {@link NAMED_SCALES} and nothing else,\n * and the questions asked about one chosen scale accept exactly what it can\n * propose — an alias of a Western scale included, a world scale not.\n *\n * @param name The scale name or alias.\n * @param label What the name is, for the error message.\n * @returns The 12-bit mask.\n * @throws If the name is not a built-in scale, or names a world scale.\n */\nfunction requireChordScaleMask(name: ScaleNameInput, label = 'scale'): number {\n const canonical = resolveScaleName(name);\n if (canonical !== undefined && !Object.hasOwn(NAMED_SCALES, canonical)) {\n throw new InvalidInputError(\n `${label} must name a scale chord-scale theory ranks; ${String(name)} is a world scale`,\n );\n }\n return requireScaleMask(name, label);\n}\n\n/** Count the set bits (scale tones) in a 12-bit mode mask. */\nfunction popcount12(mask: number): number {\n let count = 0;\n for (let n = 0; n < 12; n += 1) {\n count += (mask >> n) & 1;\n }\n return count;\n}\n\n/** Test whether a pitch class belongs to a mode mask rooted on `scaleRootPc`. */\nfunction maskHasPitchClass(mask: number, scaleRootPc: number, pc: number): boolean {\n const offset = pitchClass(pc - scaleRootPc);\n return ((mask >> offset) & 1) === 1;\n}\n\n/**\n * Test whether every chord pitch class is contained in a scale.\n *\n * @param chordPcs The chord's pitch classes (0..11).\n * @param scaleMask The scale's 12-bit mode mask.\n * @param scaleRootPc The pitch class the mask is rooted on.\n * @returns True if the scale is a superset of the chord.\n *\n * @category Scales\n */\nexport function scaleMatchesChord(\n chordPcs: number[],\n scaleMask: number,\n scaleRootPc: number,\n): boolean {\n // The mask is read with bit operations and a pitch class shifts it, so both\n // coerce: an entry that is not a pitch class would silently be read as one.\n assertModeMask(scaleMask, 'scaleMask');\n assertPitchClass(scaleRootPc, 'scaleRootPc');\n const pcs = assertArray<number>(chordPcs, 'chord pitch classes');\n for (let index = 0; index < pcs.length; index += 1) {\n const pc = assertPitchClass(pcs[index] ?? Number.NaN, `chord pitch classes[${index}]`);\n if (!maskHasPitchClass(scaleMask, scaleRootPc, pc)) {\n return false;\n }\n }\n return true;\n}\n\n/**\n * A named scale rooted on a pitch class that fits over a chord.\n *\n * @category Scales\n */\nexport type ChordScaleMatch = {\n name: string;\n rootPc: number;\n};\n\n/**\n * Scale names whose mask duplicates another {@link NAMED_SCALES} entry\n * (major/ionian, naturalMinor/aeolian). Chord-scale analysis conventionally\n * speaks in mode names, so these aliases are skipped and only the modal name\n * is reported.\n */\nconst ALIASED_SCALE_NAMES = new Set(['major', 'naturalMinor']);\n\n/** Number of tones in a heptatonic (seven-note) scale. */\nconst HEPTATONIC_SIZE = 7;\n\n/**\n * Conventional preference among equally-fitting scales, brightest first.\n *\n * Several modes fit a chord that states no third or sixth — a power chord, a\n * suspension — equally well. Alphabetical order made `aeolian` the answer for\n * those, naming a minor mode as the best fit for a mode-neutral chord.\n */\nconst SCALE_PREFERENCE = [\n 'ionian',\n 'major',\n 'mixolydian',\n 'dorian',\n 'lydian',\n 'aeolian',\n 'naturalMinor',\n 'phrygian',\n 'locrian',\n] as const;\n\n/** Conventional first choices that pure set-fit ranking cannot express. */\nconst IDIOMATIC_SCALES: Partial<Record<Chord['quality'], readonly string[]>> = {\n aug: ['wholeTone'],\n m7b5: ['locrian'],\n // The diminished scale played over a diminished seventh is the whole-half\n // octatonic. Its half-whole rotation belongs to the dominant it colours: over\n // a diminished seventh every one of its non-chord tones lands a semitone\n // above a chord tone, so it offers no tension at all.\n dim7: ['octatonicWholeHalf'],\n minMaj7: ['melodicMinor'],\n};\n\n/** Position of a scale in {@link SCALE_PREFERENCE}, or last when unlisted. */\nfunction conventionalRank(name: string): number {\n const index = (SCALE_PREFERENCE as readonly string[]).indexOf(name);\n return index < 0 ? SCALE_PREFERENCE.length : index;\n}\n\n/** Whether this scale is an idiomatic color choice for the chord quality. */\nfunction idiomaticRank(chord: Chord, name: string): number {\n const choices = IDIOMATIC_SCALES[chord.quality];\n if (choices === undefined) {\n return 0;\n }\n const index = choices.indexOf(name);\n return index < 0 ? choices.length + 1 : index;\n}\n\n/**\n * An ordinary dominant seventh may use altered colors while retaining its\n * perfect fifth in the voicing; the `7alt` quality makes that alteration\n * explicit, but lead sheets commonly write the shorter `7` here.\n */\nfunction isAlteredDominant(chord: Chord, scaleName: string): boolean {\n return chord.quality === 'dom7' && scaleName === 'altered';\n}\n\n/**\n * List the scales that fit over a chord, best fit first.\n *\n * Only the chord root is considered as the scale root, matching the\n * conventional chord-scale relationship. Every entry of {@link NAMED_SCALES}\n * whose pitch-class set is a superset of the chord's is returned once per\n * distinct pitch-class set (aliased masks such as major/ionian report only the\n * modal name). The chromatic scale is only returned as a fallback when no other\n * scale contains the chord.\n *\n * The ranking applies these keys in order:\n *\n * 1. The idiomatic scales for the chord quality, in the order they are named\n * there, ahead of every scale that is not one of them.\n * 2. For bare triads and smaller chords, heptatonic scales ahead of pentatonics\n * and other sizes: a pentatonic adds no modal color over a triad, so the\n * seven-note modes are the more useful answer.\n * 3. Fewest extra scale tones beyond the chord.\n * 4. Conventional modal preference.\n * 5. Fewest avoid notes, which separates the symmetric and melodic-minor\n * scales the modal preference does not name.\n * 6. Alphabetical order, so the list is stable.\n *\n * The idiomatic key comes first, so the head of the list is the scale a player\n * would name rather than the tightest fit: over `Cm7b5` the locrian mode leads\n * even though the blues scale adds fewer tones, because locrian is what a\n * half-diminished chord is read as. Read further down the list, or pick by name,\n * when the closest fit is what is wanted; {@link scalesForChanges} makes the\n * choice for a whole progression.\n *\n * @param chord The chord to fit scales over, as a chord symbol, chord data, or\n * a `Chord`.\n * @returns The matching scales rooted on the chord root, best fit first.\n *\n * @example\n * ```ts\n * import { makeChord, chordScales } from '@libraz/libcantus';\n * const scales = chordScales(makeChord(0, 'maj7'));\n * // scales for Cmaj7, best fit first; each is { name, rootPc: 0 }\n * ```\n *\n * @category Scales\n */\nexport function chordScales(chord: ChordLike): ChordScaleMatch[] {\n const data = toChordData(chord);\n const chordPcs = chordPitchClasses(data);\n const rootPc = pitchClass(data.rootPc);\n const ranked: { name: string; extra: number; size: number; avoid: number }[] = [];\n const seenMasks = new Set<number>();\n for (const name of Object.keys(NAMED_SCALES)) {\n if (name === 'chromatic' || ALIASED_SCALE_NAMES.has(name)) {\n continue;\n }\n const mask = namedScaleMask(name);\n if (mask === undefined || seenMasks.has(mask)) {\n continue;\n }\n seenMasks.add(mask);\n if (scaleMatchesChord(chordPcs, mask, rootPc) || isAlteredDominant(data, name)) {\n const size = popcount12(mask);\n ranked.push({\n name,\n extra: size - chordPcs.length,\n size,\n avoid: avoidNotes(data, name).length,\n });\n }\n }\n if (ranked.length === 0) {\n // No diatonic-style scale contains the chord; fall back to the chromatic\n // scale, which trivially contains every pitch class.\n if (NAMED_SCALES.chromatic !== undefined) {\n return [{ name: 'chromatic', rootPc }];\n }\n return [];\n }\n const preferHeptatonic = chordPcs.length <= 3;\n ranked.sort((a, b) => {\n const idiomaticA = idiomaticRank(data, a.name);\n const idiomaticB = idiomaticRank(data, b.name);\n if (idiomaticA !== idiomaticB) {\n return idiomaticA - idiomaticB;\n }\n if (preferHeptatonic && (a.size === HEPTATONIC_SIZE) !== (b.size === HEPTATONIC_SIZE)) {\n return a.size === HEPTATONIC_SIZE ? -1 : 1;\n }\n if (a.extra !== b.extra) {\n return a.extra - b.extra;\n }\n const rankA = conventionalRank(a.name);\n const rankB = conventionalRank(b.name);\n if (rankA !== rankB) {\n return rankA - rankB;\n }\n // Among scales the modal preference does not name — the symmetric and\n // melodic-minor scales — the one that clashes with the chord least is the\n // one a player reaches for. Alphabetical order answered with the rotation\n // that leaves nothing playable.\n if (a.avoid !== b.avoid) {\n return a.avoid - b.avoid;\n }\n return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;\n });\n return ranked.map((entry) => ({ name: entry.name, rootPc }));\n}\n\n/**\n * Which use an avoid note is judged for: sounding it, or passing through it.\n *\n * `'harmonic'` is the Berklee-style rule in full — a tone that must not be\n * sounded against the chord. `'melodic'` keeps only what a line cannot pass\n * through either, which is the semitone above the root; every other clash\n * resolves as the line moves on.\n *\n * @category Scales\n */\nexport type AvoidNoteUse = 'harmonic' | 'melodic';\n\n/**\n * Options for {@link avoidNotes}.\n *\n * @category Scales\n */\nexport type AvoidNotesOptions = {\n use?: AvoidNoteUse;\n};\n\n/**\n * List a scale's avoid notes over a chord.\n *\n * An avoid note is a non-chord scale tone that lies a semitone directly above a\n * chord tone, or the third a suspension displaced; sounding it against the\n * chord clashes. The scale is rooted on the chord root. If the scale does not\n * contain the chord, no avoid notes exist.\n *\n * The rule states which tones must not be *sounded* against the chord, which is\n * not the same question as which tones a line may not touch. Pass\n * `{ use: 'melodic' }` for the melodic reading: only the semitone above the\n * root survives it, so the result is always a subset of the harmonic one.\n *\n * @param chord The chord providing the chord tones, as a chord symbol, chord\n * data, or a `Chord`.\n * @param scaleName A key of {@link NAMED_SCALES}, or an alias of one. A world\n * scale is not one: chord-scale theory is a Western practice, so a raga named\n * here is rejected rather than answered with a number.\n * @param opts Set `use: 'melodic'` to judge a line rather than a voicing.\n * @returns The avoid-note pitch classes, sorted ascending in [0, 11].\n * @throws If `scaleName` is not a built-in scale. An empty result already\n * means \"this scale has no avoid notes over this chord\"; answering a typo\n * the same way would make the two indistinguishable.\n *\n * @example\n * ```ts\n * import { makeChord, avoidNotes } from '@libraz/libcantus';\n * avoidNotes(makeChord(0, 'maj7'), 'ionian'); // [5] — F clashes with the third\n * avoidNotes(makeChord(0, 'maj7'), 'ionian', { use: 'melodic' }); // [] — a line may pass through it\n * ```\n *\n * @category Scales\n */\nexport function avoidNotes(\n chord: ChordLike,\n scaleName: ScaleNameInput,\n opts: AvoidNotesOptions = {},\n): number[] {\n const asked = assertOptions(opts, 'opts');\n const mask = requireChordScaleMask(scaleName);\n const data = toChordData(chord);\n const rootPc = pitchClass(data.rootPc);\n const chordPcs = chordPitchClasses(data);\n const alteredDominant = isAlteredDominant(data, scaleName);\n if (!scaleMatchesChord(chordPcs, mask, rootPc) && !alteredDominant) {\n return [];\n }\n const chordSet = new Set(chordPcs);\n // Sounding the third a suspension stands in for is what undoes the\n // suspension, so it is an avoid note however consonant it is against the\n // scale. Both thirds are named: a suspension does not say which one it\n // displaced — that is what suspending is — so meeting either one ends it.\n const displaced = new Set(displacedThirds(data));\n const melodic = asked.use === 'melodic';\n const semitoneAboveRoot = pitchClass(rootPc + 1);\n const avoid: number[] = [];\n for (let pc = 0; pc < 12; pc += 1) {\n if (!maskHasPitchClass(mask, rootPc, pc) || chordSet.has(pc)) {\n continue;\n }\n if (melodic && pc !== semitoneAboveRoot) {\n continue;\n }\n if (!alteredDominant && (chordSet.has(pitchClass(pc - 1)) || displaced.has(pc))) {\n avoid.push(pc);\n }\n }\n return avoid.sort((a, b) => a - b);\n}\n\n/**\n * Semitones above the root of the colors a dominant takes when it resolves to a\n * minor tonic: the flat ninth, the sharp ninth, and the flat thirteenth.\n *\n * Over a dominant read on its own, each of them sits a semitone above a chord\n * tone and is an avoid note. Heard as the dominant of a minor key they are the\n * key's own tones, and standard practice, so the function the chord serves is\n * what decides.\n */\nconst MINOR_RESOLUTION_TENSIONS = [1, 3, 8] as const;\n\n/** Whether this chord is a dominant seventh resolving down a fifth to minor. */\nfunction resolvesToMinorTonic(\n chord: Chord,\n chordPcs: number[],\n target: Chord | undefined,\n): boolean {\n if (target === undefined) {\n return false;\n }\n const rootPc = pitchClass(chord.rootPc);\n const sounds = (semitones: number): boolean => chordPcs.includes(pitchClass(rootPc + semitones));\n if (!sounds(4) || !sounds(10)) {\n return false;\n }\n const targetRootPc = pitchClass(target.rootPc);\n if (targetRootPc !== pitchClass(rootPc + 5)) {\n return false;\n }\n return chordPitchClasses(target).includes(pitchClass(targetRootPc + 3));\n}\n\n/**\n * Options for {@link availableTensions}.\n *\n * @category Scales\n */\nexport type AvailableTensionsOptions = {\n /**\n * The chord this one resolves to, as a chord symbol, chord data, or a\n * `Chord`.\n */\n resolvesTo?: ChordLike;\n};\n\n/**\n * List a scale's available tensions over a chord.\n *\n * Available tensions are scale tones that are neither chord tones nor avoid\n * notes, i.e. the usable color tones (typically the 9/11/13 region). The scale\n * is rooted on the chord root. If the scale does not contain the chord, there\n * are no available tensions.\n *\n * A tension's availability also depends on what the chord is doing. Pass\n * `resolvesTo` to say which chord this one resolves to: a dominant seventh\n * resolving down a fifth to a minor tonic takes its flat ninth, sharp ninth and\n * flat thirteenth, which read on their own would each be an avoid note. Only\n * scale tones are ever added, so the result stays within the scale.\n *\n * @param chord The chord providing the chord tones, as a chord symbol, chord\n * data, or a `Chord`.\n * @param scaleName A key of {@link NAMED_SCALES}, or an alias of one; a world\n * scale is outside chord-scale theory, as in {@link avoidNotes}.\n * @param opts Set `resolvesTo` to the chord this one resolves to.\n * @returns The available-tension pitch classes, sorted ascending in [0, 11].\n * @throws If `scaleName` is not a built-in scale, as {@link avoidNotes} does.\n *\n * @example\n * ```ts\n * import { makeChord, availableTensions } from '@libraz/libcantus';\n * availableTensions(makeChord(0, 'maj7'), 'ionian'); // [2, 9] — color tones over Cmaj7\n * availableTensions(makeChord(7, 'dom7'), 'phrygianDominant', { resolvesTo: makeChord(0, 'min') });\n * // [3, 8] — the b13 and b9 the resolution to C minor makes available\n * ```\n *\n * @category Scales\n */\nexport function availableTensions(\n chord: ChordLike,\n scaleName: ScaleNameInput,\n opts: AvailableTensionsOptions = {},\n): number[] {\n const asked = assertOptions(opts, 'opts');\n const mask = requireChordScaleMask(scaleName);\n const data = toChordData(chord);\n const rootPc = pitchClass(data.rootPc);\n const chordPcs = chordPitchClasses(data);\n const alteredDominant = isAlteredDominant(data, scaleName);\n if (!scaleMatchesChord(chordPcs, mask, rootPc) && !alteredDominant) {\n return [];\n }\n const chordSet = new Set(chordPcs);\n const avoidSet = alteredDominant ? new Set<number>() : new Set(avoidNotes(data, scaleName));\n const functional = resolvesToMinorTonic(\n data,\n chordPcs,\n asked.resolvesTo === undefined ? undefined : toChordData(asked.resolvesTo),\n )\n ? new Set(MINOR_RESOLUTION_TENSIONS.map((semitones) => pitchClass(rootPc + semitones)))\n : undefined;\n const tensions: number[] = [];\n for (let pc = 0; pc < 12; pc += 1) {\n if (!maskHasPitchClass(mask, rootPc, pc) || chordSet.has(pc)) {\n continue;\n }\n if (avoidSet.has(pc) && functional?.has(pc) !== true) {\n continue;\n }\n tensions.push(pc);\n }\n return tensions.sort((a, b) => a - b);\n}\n\n/**\n * A scale fit over a chord, with its non-chord tones sorted by what may be done\n * with them.\n *\n * `avoid`, `passing` and `tensions` partition the scale tones the chord does\n * not state: `avoid` may not be played at all, `passing` may be passed through\n * melodically but not sounded against the chord, and `tensions` may be added\n * freely as color. `avoid` is {@link avoidNotes} under `use: 'melodic'`, and\n * `avoid` together with `passing` is the harmonic reading.\n *\n * @category Scales\n */\nexport type ChordScaleReportEntry = {\n name: string;\n rootPc: number;\n avoid: number[];\n passing: number[];\n tensions: number[];\n};\n\n/**\n * Report the best-fitting scales for a chord with their avoid notes and tensions.\n *\n * Combines {@link chordScales}, {@link avoidNotes}, and {@link availableTensions}\n * into a single ergonomic result, ordered best fit first. The report is read as\n * playing advice, so the tones a line may pass through are reported apart from\n * the ones it may not touch; see {@link ChordScaleReportEntry}.\n *\n * @param chord The chord to analyze, as a chord symbol, chord data, or a\n * `Chord`.\n * @param limit Optional maximum number of scales to report; all by default.\n * @returns One entry per reported scale, best fit first.\n *\n * @category Scales\n */\nexport function chordScaleReport(chord: ChordLike, limit?: number): ChordScaleReportEntry[] {\n if (limit !== undefined) {\n assertPositiveInt(limit, 'chord-scale report limit');\n }\n const data = toChordData(chord);\n const matches = chordScales(data);\n const chosen = limit === undefined ? matches : matches.slice(0, limit);\n return chosen.map((match) => {\n const harmonic = avoidNotes(data, match.name);\n const melodic = new Set(avoidNotes(data, match.name, { use: 'melodic' }));\n return {\n name: match.name,\n rootPc: match.rootPc,\n avoid: harmonic.filter((pc) => melodic.has(pc)),\n passing: harmonic.filter((pc) => !melodic.has(pc)),\n tensions: availableTensions(data, match.name),\n };\n });\n}\n\n/**\n * A chord paired with the scale chosen for it by {@link scalesForChanges}.\n *\n * @category Scales\n */\nexport type ScaleChoice = {\n chord: Chord;\n scale: ChordScaleMatch;\n};\n\n/**\n * Small per-candidate penalty added in {@link scalesForChanges} so that, among\n * choices of otherwise-equal transition cost, the tighter best-fit scale wins.\n */\nconst RANK_PENALTY = 0.01;\n\n/** Build the pitch-class set of a named scale rooted on `rootPc`. */\nfunction scalePitchClassSet(name: string, rootPc: number): Set<number> {\n const mask = namedScaleMask(name);\n const pcs = new Set<number>();\n if (mask === undefined) {\n return pcs;\n }\n for (let n = 0; n < 12; n += 1) {\n if (((mask >> n) & 1) === 1) {\n pcs.add(pitchClass(rootPc + n));\n }\n }\n return pcs;\n}\n\n/** Count pitch classes that belong to exactly one of two sets. */\nfunction symmetricDifferenceSize(a: Set<number>, b: Set<number>): number {\n let count = 0;\n for (const pc of a) {\n if (!b.has(pc)) {\n count += 1;\n }\n }\n for (const pc of b) {\n if (!a.has(pc)) {\n count += 1;\n }\n }\n return count;\n}\n\n/**\n * Choose one scale per chord across a progression, favoring continuity.\n *\n * Each chord's {@link chordScales} candidates form a stage in a Viterbi-style\n * dynamic program. The transition cost between adjacent choices is the number\n * of pitch classes that differ between their pitch-class sets (the symmetric\n * difference), plus a small penalty for straying from a chord's best-fit scale\n * so that ties break toward the tighter fit. The minimum-total-cost path is\n * returned, one {@link ScaleChoice} per input chord in the original order.\n *\n * Each choice pairs its scale with the chord exactly as it was handed in, so\n * this takes plain chord data rather than the wider forms the entry points\n * above accept.\n *\n * @param chords The chord sequence to choose scales for.\n * @returns One scale choice per chord, in input order.\n *\n * @category Scales\n */\nexport function scalesForChanges(chords: readonly Chord[]): ScaleChoice[] {\n assertArray(chords, 'chord-scale changes');\n assertGenerationBudget(chords.length, 'chord-scale changes');\n if (chords.length === 0) {\n return [];\n }\n const candidateLists = chords.map((chord) => chordScales(chord));\n const pcSets = candidateLists.map((candidates) =>\n candidates.map((match) => scalePitchClassSet(match.name, match.rootPc)),\n );\n\n const first = candidateLists[0] ?? [];\n const dp: number[][] = [first.map((_, j) => j * RANK_PENALTY)];\n const back: number[][] = [first.map(() => -1)];\n\n for (let i = 1; i < candidateLists.length; i += 1) {\n const candidates = candidateLists[i] ?? [];\n const prevCosts = dp[i - 1] ?? [];\n const prevSets = pcSets[i - 1] ?? [];\n const curSets = pcSets[i] ?? [];\n const stageCosts: number[] = [];\n const stageBack: number[] = [];\n for (let j = 0; j < candidates.length; j += 1) {\n let bestCost = Number.POSITIVE_INFINITY;\n let bestPrev = -1;\n for (let k = 0; k < prevCosts.length; k += 1) {\n const transition = symmetricDifferenceSize(\n prevSets[k] ?? new Set(),\n curSets[j] ?? new Set(),\n );\n const cost = (prevCosts[k] ?? 0) + transition;\n if (cost < bestCost) {\n bestCost = cost;\n bestPrev = k;\n }\n }\n stageCosts.push(bestCost + j * RANK_PENALTY);\n stageBack.push(bestPrev);\n }\n dp.push(stageCosts);\n back.push(stageBack);\n }\n\n const lastCosts = dp[dp.length - 1] ?? [];\n let bestFinal = 0;\n for (let j = 1; j < lastCosts.length; j += 1) {\n if ((lastCosts[j] ?? Number.POSITIVE_INFINITY) < (lastCosts[bestFinal] ?? 0)) {\n bestFinal = j;\n }\n }\n\n const chosenIndices: number[] = new Array(candidateLists.length);\n let current = bestFinal;\n for (let i = candidateLists.length - 1; i >= 0; i -= 1) {\n chosenIndices[i] = current;\n current = back[i]?.[current] ?? -1;\n }\n\n return chords.map((chord, i) => {\n const candidates = candidateLists[i] ?? [];\n const scale = candidates[chosenIndices[i] ?? 0] ?? { name: 'chromatic', rootPc: chord.rootPc };\n return { chord, scale };\n });\n}\n","import { pitchClassOf } from '../../core/pitch/index.js';\nimport { assertFiniteNumber } from '../../core/validation/index.js';\nimport { type ChordToneRole, chordToneRole, thirdSlotOf } from '../chord/index.js';\nimport { type ChordLike, toChordData } from '../symbol/index.js';\n\n/**\n * Harmonic role a pitch plays within a chord.\n *\n * @category Functional Harmony\n */\nexport type HarmonyRole = ChordToneRole | 'tension' | 'doubling';\n\n/**\n * How firmly a pitch is locked to the chord's identity.\n *\n * - `identity`: moving it produces a different chord (the root).\n * - `quality`: moving it flips the chord quality (the third).\n * - `voicing`: it can move freely without changing chord identity or quality.\n *\n * @category Functional Harmony\n */\nexport type LockLevel = 'identity' | 'quality' | 'voicing';\n\n/**\n * A pitch's role, lock level, and owning chord.\n *\n * @category Functional Harmony\n */\nexport type VoicedRole = {\n role: HarmonyRole;\n lock: LockLevel;\n belongsToChordId: number;\n};\n\n/**\n * Classify a pitch's harmonic role and lock level within a chord.\n *\n * The role comes from the pitch's interval class above the chord root, read\n * against the chord's own template: root (0), third (3/4), fifth (6/7/8),\n * sixth or seventh (9/10/11). Every pitch the template does not claim is\n * `'tension'` — including one that is simply foreign to the chord, which this\n * single-pitch query cannot tell from a colour tone. The root locks the chord\n * identity, the third locks its quality, and everything else is free voicing.\n * Where a chord has no third, the tone standing in its place — the fourth of a\n * `sus4`, the second of a `sus2`, the eleventh of an eleventh chord — takes the\n * third's slot and is locked to `quality`, since moving it changes the chord.\n * At most one interval class of a chord is ever reported as its third, so a\n * consumer reading `lock: 'quality'` as \"this tone identifies the chord\" gets\n * one answer rather than two. Detecting an octave doubling requires the\n * surrounding voicing, which this query does not carry, so `'doubling'` is part\n * of the type — the shared vocabulary of harmonic roles — but is never returned\n * here.\n *\n * @param pitch MIDI pitch or bare pitch class.\n * @param chord The chord providing the root reference, as a chord symbol, chord\n * data, or a `Chord`.\n * @param chordId Identifier stored on the result (defaults to 0).\n * @returns The pitch's role, lock level, and owning chord id.\n * @example\n * ```ts\n * import { roleOf, makeChord } from '@libraz/libcantus';\n * roleOf(64, makeChord(0, 'maj')); // E over C major\n * // { role: 'third', lock: 'quality', belongsToChordId: 0 }\n * ```\n * @category Functional Harmony\n */\nexport function roleOf(pitch: number, chord: ChordLike, chordId = 0): VoicedRole {\n // The id is carried into the result, so a value that is not one would be\n // reported back as the chord this role belongs to.\n assertFiniteNumber(chordId, 'chordId');\n const data = toChordData(chord);\n const interval = (pitchClassOf(pitch) - pitchClassOf(data.rootPc) + 12) % 12;\n // What stands in the third's slot is read where the chord is read, not here:\n // the tone that identifies a chord and the tone the avoid-note rule refuses\n // against it are two consequences of one fact about the chord.\n const slot = thirdSlotOf(data);\n const isSuspendedTone = slot.kind === 'suspension' && interval === slot.interval;\n const chordRole = chordToneRole(pitch, data);\n let role: HarmonyRole;\n let lock: LockLevel;\n if (isSuspendedTone) {\n role = 'third'; // suspended tone occupies the third's quality-defining slot\n lock = 'quality';\n } else if (chordRole !== null) {\n role = chordRole;\n lock = chordRole === 'root' ? 'identity' : chordRole === 'third' ? 'quality' : 'voicing';\n } else {\n role = 'tension';\n lock = 'voicing';\n }\n return { role, lock, belongsToChordId: chordId };\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["/home/runner/work/libcantus/libcantus/dist/chunk-NM7UAMC3.cjs","../src/core/duration/index.ts","../src/core/tempo/internal.ts","../src/core/tempo/index.ts","../src/core/tuning/index.ts"],"names":["SECONDS_PER_MINUTE"],"mappings":"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AwBO;AACL,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAmBO;AACL,EAAA;AACA,EAAA;AACA,EAAA;AACF;AA2BO;AACL,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACF,EAAA;AACF;AAyBO;AACL,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAgBO;AACL,EAAA;AACA,EAAA;AACA,EAAA;AACF;AHpCO;AACA;AIzSM;AAEb;AAIM,EAAA;AACF,IAAA;AACE,MAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAiBO;AACL,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAgBO;AACL,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAWO;AACL,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAmBO;AACL,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAiBO;AACL,EAAA;AACA,EAAA;AACF;AAWO;AACL,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAkBO;AACL,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAUO;AACL,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAUO;AACL,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAWO;AACL,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAGA;AACE,EAAA;AACF;AAQa;AAGR,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACC,EAAA;AACA,EAAA;AACA,EAAA;AACL;AAGD;AACE,EAAA;AACF;AAWO;AACL,EAAA;AACK,EAAA;AACH,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACF;AJ8IO;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-NM7UAMC3.cjs","sourcesContent":[null,"/**\n * Note values: the written durations a score shows — a base value, its\n * augmentation dots, and an optional tuplet ratio — and the conversion between\n * them and the quarter-note beats the rest of the library measures in.\n *\n * The base values are named the American way (whole, half, quarter, eighth),\n * matching the library's \"quarter-note beat\" convention and the vocabulary of\n * the MIDI and DAW world this engine reads events from.\n */\n\nimport { InvalidInputError, NoSolutionError } from '../errors/index.js';\nimport {\n assertFiniteNumber,\n assertGenerationBudget,\n assertInteger,\n assertOneOf,\n assertOptions,\n assertPositiveInt,\n describeRejected,\n} from '../validation/index.js';\n\n/**\n * A written base note value, from the whole note down to the sixty-fourth.\n *\n * Longer values (the breve and beyond) are written as tied notes; see\n * {@link beatsToTiedDurations}.\n *\n * @category Rhythm & Meter\n */\nexport type NoteValue =\n | 'whole'\n | 'half'\n | 'quarter'\n | 'eighth'\n | 'sixteenth'\n | 'thirtySecond'\n | 'sixtyFourth';\n\n/**\n * Every base note value, longest first.\n *\n * @category Rhythm & Meter\n */\nexport const NOTE_VALUES: readonly NoteValue[] = Object.freeze([\n 'whole',\n 'half',\n 'quarter',\n 'eighth',\n 'sixteenth',\n 'thirtySecond',\n 'sixtyFourth',\n] as const);\n\n/**\n * A tuplet ratio: `actual` written notes played in the time of `normal` ones —\n * `{ actual: 3, normal: 2 }` for a triplet, `{ actual: 5, normal: 4 }` for a\n * quintuplet.\n *\n * @category Rhythm & Meter\n */\nexport type Tuplet = {\n /** How many notes are written. */\n actual: number;\n /** How many of the same base value they occupy. */\n normal: number;\n};\n\n/**\n * A written duration: a base value, optional augmentation dots, and an optional\n * tuplet ratio. Its length is `base * (2 - 2^-dots) * normal / actual`.\n *\n * @category Rhythm & Meter\n */\nexport type DurationData = {\n base: NoteValue;\n /** Augmentation dots, 0 when absent; up to four are accepted. */\n dots?: number;\n tuplet?: Tuplet;\n};\n\n/**\n * A duration as {@link beatsToDuration} spells it, with the dot count always\n * stated.\n *\n * @category Rhythm & Meter\n */\nexport type SpelledDuration = {\n base: NoteValue;\n dots: number;\n tuplet?: Tuplet;\n};\n\n/** Length of each base value in quarter notes. */\nconst NOTE_VALUE_QUARTERS: Readonly<Record<NoteValue, number>> = Object.freeze({\n whole: 4,\n half: 2,\n quarter: 1,\n eighth: 0.5,\n sixteenth: 0.25,\n thirtySecond: 0.125,\n sixtyFourth: 0.0625,\n});\n\n/**\n * Match tolerance in quarter-note beats. Wide enough to read a seven-digit\n * decimal such as 0.3333333 as a triplet, and still far below one tick at\n * PPQ 960 (≈ 0.00104 beats), so no two spellings a sequencer can distinguish\n * collapse into one.\n */\nconst EPS = 1e-6;\n\n/** Most dots {@link beatsToDuration} will spell; more is never the reading. */\nconst MAX_SPELLED_DOTS = 2;\n\n/** Most dots {@link durationToBeats} will read; triple dots occur, four is the ceiling. */\nconst MAX_DOTS = 4;\n\n/** Upper bound on either side of a tuplet ratio. */\nconst MAX_TUPLET = 64;\n\n/**\n * The tuplet families {@link beatsToDuration} spells, in preference order, with\n * the plain (non-tuplet) reading first. Quintuplets and septuplets are taken in\n * the time of four, the reading that fills one beat of their base value.\n */\nconst SPELLED_TUPLETS: readonly (Tuplet | undefined)[] = Object.freeze([\n undefined,\n { actual: 3, normal: 2 },\n { actual: 5, normal: 4 },\n { actual: 7, normal: 4 },\n]);\n\n/** Length in quarter notes of a base value with dots and an optional tuplet. */\nfunction quartersOfParts(base: NoteValue, dots: number, tuplet: Tuplet | undefined): number {\n const dotted = NOTE_VALUE_QUARTERS[base] * (2 - 2 ** -dots);\n return tuplet === undefined ? dotted : (dotted * tuplet.normal) / tuplet.actual;\n}\n\n/** Validate a duration, accepting the bare base value as shorthand. */\nfunction assertDuration(value: NoteValue | DurationData, name: string): DurationData {\n if (typeof value === 'string') {\n return { base: assertOneOf(value, NOTE_VALUES, name), dots: 0 };\n }\n if (typeof value !== 'object' || value === null) {\n throw new InvalidInputError(\n `${name} must be a note value or a duration; received ${describeRejected(value)}`,\n );\n }\n const base = assertOneOf(value.base, NOTE_VALUES, `${name}.base`);\n const dots =\n value.dots === undefined ? 0 : assertInteger(value.dots, `${name}.dots`, 0, MAX_DOTS);\n if (value.tuplet === undefined) {\n return { base, dots };\n }\n // A duration restored from a file can carry a null where the tuplet is, and\n // reading a field off it would report the library's own TypeError rather than\n // the malformed input it is.\n if (typeof value.tuplet !== 'object' || value.tuplet === null) {\n throw new InvalidInputError(\n `${name}.tuplet must be a tuplet; received ${describeRejected(value.tuplet)}`,\n );\n }\n const tuplet = {\n actual: assertPositiveInt(value.tuplet.actual, `${name}.tuplet.actual`, MAX_TUPLET),\n normal: assertPositiveInt(value.tuplet.normal, `${name}.tuplet.normal`, MAX_TUPLET),\n };\n return { base, dots, tuplet };\n}\n\n/** Length of a validated duration in quarter notes. */\nfunction quartersOf(duration: DurationData): number {\n return quartersOfParts(duration.base, duration.dots ?? 0, duration.tuplet);\n}\n\n/** Length of the beat unit in quarter notes; a quarter note unless overridden. */\nfunction beatQuarters(options: { beatUnit?: NoteValue | DurationData }): number {\n if (options.beatUnit === undefined) {\n return NOTE_VALUE_QUARTERS.quarter;\n }\n return quartersOf(assertDuration(options.beatUnit, 'beatUnit'));\n}\n\n/** The conventional spelling of a length in quarter notes, if it has one. */\nfunction spellQuarters(quarters: number): SpelledDuration | undefined {\n for (const tuplet of SPELLED_TUPLETS) {\n for (let dots = 0; dots <= MAX_SPELLED_DOTS; dots += 1) {\n for (const base of NOTE_VALUES) {\n if (Math.abs(quartersOfParts(base, dots, tuplet) - quarters) <= EPS) {\n return tuplet === undefined\n ? { base, dots }\n : { base, dots, tuplet: { actual: tuplet.actual, normal: tuplet.normal } };\n }\n }\n }\n }\n return undefined;\n}\n\n/** The undotted values a tie chain is built from, longest first. */\nconst PLAIN_VALUES: readonly { base: NoteValue; quarters: number }[] = Object.freeze(\n NOTE_VALUES.map((base) => ({ base, quarters: NOTE_VALUE_QUARTERS[base] })),\n);\n\n/** Require a positive, finite beat count. */\nfunction assertPositiveBeats(beats: number, name: string): number {\n assertFiniteNumber(beats, name);\n if (beats <= 0) {\n throw new InvalidInputError(`${name} must be positive; received ${beats}`);\n }\n return beats;\n}\n\n/**\n * Length of a written duration in beats.\n *\n * @param duration The duration, or a bare base value as shorthand for an\n * undotted one.\n * @param options `beatUnit` sets what one beat is; a quarter note by default,\n * so a dotted quarter reads as 1.5. Pass `{ base: 'quarter', dots: 1 }` to\n * count in the felt beats of a compound meter.\n * @returns The length in beats.\n * @throws If the base value is unknown, the dot count is not an integer in\n * 0..4, or a tuplet side is not a positive integer.\n * @example\n * ```ts\n * import { durationToBeats } from '@libraz/libcantus';\n * durationToBeats('quarter'); // 1\n * durationToBeats({ base: 'quarter', dots: 1 }); // 1.5\n * durationToBeats({ base: 'eighth', tuplet: { actual: 3, normal: 2 } }) * 3; // 1 — three\n * // eighth-note triplets fill the beat two eighths would\n * ```\n * @category Rhythm & Meter\n */\nexport function durationToBeats(\n duration: NoteValue | DurationData,\n options: { beatUnit?: NoteValue | DurationData } = {},\n): number {\n const quarters = quartersOf(assertDuration(duration, 'duration'));\n return quarters / beatQuarters(assertOptions(options, 'options'));\n}\n\n/**\n * Spell a length in beats as the note value a score would show.\n *\n * The spelling is the conventional one, not merely an arithmetically valid one:\n * 1.5 beats is a dotted quarter rather than a quarter tied to an eighth, and a\n * third of a beat is an eighth triplet rather than a twenty-fourth note. Plain\n * values are preferred to dotted ones and both to tuplets, and within the\n * tuplets triplets come before quintuplets and septuplets.\n *\n * Lengths are matched within 1e-6 quarter notes, so a rounded decimal such as\n * 0.3333333 still reads as a triplet. The tolerance is measured in quarter\n * notes whatever `beatUnit` names, so a longer beat unit narrows it in\n * proportion: under `{ beatUnit: 'whole' }` it is 2.5e-7 of a beat.\n *\n * @param beats The length in beats.\n * @param options `beatUnit` sets what one beat is; a quarter note by default.\n * @returns The base value, its dot count, and its tuplet when it has one.\n * @throws If `beats` is not positive, or — as a `NoSolutionError` — if no single\n * note value spells it, as for 5 beats in 4/4. Use\n * {@link beatsToTiedDurations} for those.\n * @example\n * ```ts\n * import { beatsToDuration } from '@libraz/libcantus';\n * beatsToDuration(1.5); // { base: 'quarter', dots: 1 }\n * beatsToDuration(1 / 3); // { base: 'eighth', dots: 0, tuplet: { actual: 3, normal: 2 } }\n * ```\n * @category Rhythm & Meter\n */\nexport function beatsToDuration(\n beats: number,\n options: { beatUnit?: NoteValue | DurationData } = {},\n): SpelledDuration {\n assertPositiveBeats(beats, 'beats');\n const spelled = spellQuarters(beats * beatQuarters(assertOptions(options, 'options')));\n if (spelled === undefined) {\n throw new NoSolutionError(`no single note value spells ${beats} beats`);\n }\n return spelled;\n}\n\n/**\n * Spell a length in beats as a chain of tied note values.\n *\n * A length with a single spelling comes back as a one-element chain. Anything\n * else is broken into undotted values, longest first, and a value immediately\n * followed by its own half is then written as a dot instead: 5 beats reads as a\n * whole note tied to a quarter, and 1.9375 beats as a double-dotted quarter tied\n * to a dotted thirty-second. The dot is only written where it leaves the chain\n * in longest-first order, so 10 beats reads as two whole notes tied to a half\n * rather than as a whole note tied to a longer dotted whole. The chain knows\n * nothing of barlines or beat grouping — an engraver splits further at those —\n * and it uses no tuplets, so a length off the plain grid and with no single\n * spelling has no chain either.\n *\n * @param beats The length in beats.\n * @param options `beatUnit` sets what one beat is; a quarter note by default.\n * @returns The tied durations, longest first, summing to `beats`.\n * @throws If `beats` is not positive, if the chain would exceed the generation\n * budget, or — as a `NoSolutionError` — if no chain of plain and dotted values\n * sums to it.\n * @example\n * ```ts\n * import { beatsToTiedDurations } from '@libraz/libcantus';\n * beatsToTiedDurations(5); // [{ base: 'whole', dots: 0 }, { base: 'quarter', dots: 0 }]\n * ```\n * @category Rhythm & Meter\n */\nexport function beatsToTiedDurations(\n beats: number,\n options: { beatUnit?: NoteValue | DurationData } = {},\n): SpelledDuration[] {\n assertPositiveBeats(beats, 'beats');\n const quarters = beats * beatQuarters(assertOptions(options, 'options'));\n const single = spellQuarters(quarters);\n if (single !== undefined) {\n return [single];\n }\n assertGenerationBudget(\n Math.ceil(quarters / NOTE_VALUE_QUARTERS.whole) + PLAIN_VALUES.length,\n 'tie chain length',\n );\n const chain: SpelledDuration[] = [];\n let remaining = quarters;\n let previousQuarters = 0;\n while (remaining > EPS) {\n const next = PLAIN_VALUES.find((candidate) => candidate.quarters <= remaining + EPS);\n if (next === undefined) {\n throw new NoSolutionError(`no chain of tied note values sums to ${beats} beats`);\n }\n const last = chain[chain.length - 1];\n const before = chain[chain.length - 2];\n // A value followed by exactly its own half is one dotted note, not a tie —\n // but the dot lengthens a value already written, so it is only taken where\n // the result still fits under the value it is tied after. Dotting the\n // second whole note of 10 beats would make it outlast the first.\n const dotted =\n last === undefined || last.dots >= MAX_SPELLED_DOTS\n ? undefined\n : quartersOfParts(last.base, last.dots + 1, undefined);\n const staysInOrder =\n dotted !== undefined && (before === undefined || dotted <= quartersOf(before) + EPS);\n if (\n last !== undefined &&\n staysInOrder &&\n Math.abs(next.quarters * 2 - previousQuarters) <= EPS\n ) {\n last.dots += 1;\n } else {\n chain.push({ base: next.base, dots: 0 });\n }\n previousQuarters = next.quarters;\n remaining -= next.quarters;\n }\n return chain;\n}\n","/**\n * The index a tempo map is read through, and the mark that says it is valid.\n *\n * A map is handed to a conversion once per note, so a pass over a piece asks\n * the same array the same questions tens of thousands of times: is it valid,\n * and which segment holds this beat. Answered from the array each time, both\n * cost the whole map per note — an imported accelerando marks a tempo every\n * tick, and the two counts multiply. They are answered from a snapshot instead,\n * which is the shape the meter map already reads through.\n */\n\nimport type { TempoMap } from './index.js';\n\n/** One tempo map as it read when the index was built. */\nexport type TempoIndex = {\n /** Onsets in map order. */\n startBeats: number[];\n /** Tempo of each entry. */\n bpms: number[];\n /**\n * Seconds elapsed from the map's origin to each entry's own onset — the\n * prefix sum of the segments before it, summed in map order.\n *\n * The same additions the origin-to-beat integral makes, made once for the\n * map rather than once per note: asking when the last note of an imported\n * accelerando sounds otherwise re-adds every tempo change in the piece.\n */\n secondsBefore: number[];\n /** Whether the map had passed validation when the index was built. */\n validated: boolean;\n};\n\nconst SECONDS_PER_MINUTE = 60;\n\n/**\n * Indexes held against the identity of the map they describe.\n *\n * A caller keeps its own array and may write to it after the library has read\n * it, so an index is only trusted while every entry still reads as it did when\n * the index was built. Anything else rebuilds, and a map that had passed\n * validation is validated again.\n */\nconst INDEX_CACHE = new WeakMap<TempoMap, TempoIndex>();\n\n/** Whether the index still describes the map it was built from. */\nfunction describesMap(index: TempoIndex, map: TempoMap): boolean {\n if (index.startBeats.length !== map.length) {\n return false;\n }\n for (let i = 0; i < map.length; i += 1) {\n const event = map[i];\n if (\n event === undefined ||\n event === null ||\n event.startBeat !== index.startBeats[i] ||\n event.bpm !== index.bpms[i]\n ) {\n return false;\n }\n }\n return true;\n}\n\n/** Build and cache the index of a map, recording whether it is validated. */\nfunction buildTempoIndex(map: TempoMap, validated: boolean): TempoIndex {\n const startBeats: number[] = [];\n const bpms: number[] = [];\n const secondsBefore: number[] = [];\n let elapsed = 0;\n for (let i = 0; i < map.length; i += 1) {\n const event = map[i];\n if (event === undefined) {\n continue;\n }\n const previousBeat = startBeats[i - 1];\n const previousBpm = bpms[i - 1];\n if (previousBeat !== undefined && previousBpm !== undefined) {\n elapsed += ((event.startBeat - previousBeat) * SECONDS_PER_MINUTE) / previousBpm;\n }\n startBeats.push(event.startBeat);\n bpms.push(event.bpm);\n secondsBefore.push(elapsed);\n }\n const index: TempoIndex = { startBeats, bpms, secondsBefore, validated };\n INDEX_CACHE.set(map, index);\n return index;\n}\n\n/** The index of `map`, rebuilt when the caller has written to the array since. */\nexport function tempoIndexOf(map: TempoMap): TempoIndex {\n const cached = INDEX_CACHE.get(map);\n return cached !== undefined && describesMap(cached, map) ? cached : buildTempoIndex(map, false);\n}\n\n/**\n * Whether the map has already passed validation and is unchanged since.\n *\n * What this still pays on every call is a comparison per entry: an array the\n * caller keeps says nothing about whether it has been written to since, so the\n * only way to know a validated map is still the one that was validated is to\n * read it. What it saves is the validation itself — the range checks, the\n * ordering, and the error messages built to describe them.\n */\nexport function isValidatedTempoMap(map: TempoMap): boolean {\n const cached = INDEX_CACHE.get(map);\n return cached?.validated === true && describesMap(cached, map);\n}\n\n/** Record that a map passed validation, and snapshot it. */\nexport function rememberValidatedTempoMap(map: TempoMap): void {\n buildTempoIndex(map, true);\n}\n\n/**\n * Index of the segment in force at `beat`, by binary search.\n *\n * The opening tempo reaches back before the beat it is marked on, so a beat\n * before the first entry answers with entry 0: a pickup is played at the tempo\n * of the piece it leads into, and the map names no other one to play it at.\n *\n * @param map A validated tempo map.\n * @param beat The beat to place.\n * @returns The index of the entry whose tempo holds at `beat`.\n */\nexport function tempoSegmentAt(map: TempoMap, beat: number): number {\n return segmentBy(tempoIndexOf(map).startBeats, beat);\n}\n\n/**\n * Seconds from the map's origin to `beat`, read off the prefix sums.\n *\n * The same additions the segment-by-segment integral makes, in the same order:\n * every whole segment before `beat` was summed into the index in map order, and\n * the part of the segment holding `beat` is added last. A beat before the origin\n * comes back negative, at the opening tempo.\n *\n * @param map A validated tempo map.\n * @param beat The beat to place in time.\n * @returns Elapsed seconds, 0 at the map's first event.\n */\nexport function elapsedSecondsAt(map: TempoMap, beat: number): number {\n return elapsedSecondsIn(tempoIndexOf(map), beat);\n}\n\n/** Seconds from the origin to `beat`, against an index already in hand. */\nexport function elapsedSecondsIn(index: TempoIndex, beat: number): number {\n const at = segmentBy(index.startBeats, beat);\n const start = index.startBeats[at] ?? 0;\n const bpm = index.bpms[at] ?? 1;\n return (index.secondsBefore[at] ?? 0) + ((beat - start) * SECONDS_PER_MINUTE) / bpm;\n}\n\n/**\n * Beat reached after `seconds` from the map's origin — the inverse of\n * {@link elapsedSecondsAt}, read off the same prefix sums.\n *\n * @param map A validated tempo map.\n * @param seconds Elapsed seconds; negative for a position before the origin.\n * @returns The position in quarter-note beats.\n */\nexport function beatAtElapsedSeconds(map: TempoMap, seconds: number): number {\n return beatAtElapsedIn(tempoIndexOf(map), seconds);\n}\n\n/** The beat reached after `seconds`, against an index already in hand. */\nexport function beatAtElapsedIn(index: TempoIndex, seconds: number): number {\n const at = segmentBy(index.secondsBefore, seconds);\n const start = index.startBeats[at] ?? 0;\n const bpm = index.bpms[at] ?? 1;\n return start + ((seconds - (index.secondsBefore[at] ?? 0)) * bpm) / SECONDS_PER_MINUTE;\n}\n\n/**\n * The last index of an ascending run whose value is at or below `value`, and 0\n * for a value below the run's first: the opening segment governs what precedes\n * it, so there is no index before it to answer with.\n */\nfunction segmentBy(ascending: readonly number[], value: number): number {\n let low = 0;\n let high = ascending.length - 1;\n while (low < high) {\n const mid = (low + high + 1) >> 1;\n const at = ascending[mid];\n if (at !== undefined && at <= value) {\n low = mid;\n } else {\n high = mid - 1;\n }\n }\n return Math.max(0, low);\n}\n","/**\n * Tempo and time conversion: beats to wall-clock seconds across tempo changes,\n * and beats to MIDI ticks.\n *\n * Positions are measured in quarter-note beats, matching the rest of the\n * library's beat convention. A {@link TempoMap} is piecewise constant: each\n * entry sets the tempo from its beat onwards, so the elapsed time over a span is\n * the sum of the segments it crosses rather than the tempo at either end.\n */\n\nimport { InvalidInputError } from '../errors/index.js';\nimport {\n assertFiniteNumber,\n assertGenerationBudget,\n assertInteger,\n assertPositiveInt,\n assertRange,\n describeRejected,\n} from '../validation/index.js';\nimport {\n beatAtElapsedIn,\n beatAtElapsedSeconds,\n elapsedSecondsAt,\n elapsedSecondsIn,\n isValidatedTempoMap,\n rememberValidatedTempoMap,\n tempoIndexOf,\n tempoSegmentAt,\n} from './internal.js';\n\n/**\n * A tempo change: the beat it takes effect on, and the tempo it sets.\n *\n * The tempo holds until the next entry, so a map of one entry is a constant\n * tempo for the whole timeline.\n *\n * @category Rhythm & Meter\n */\nexport type TempoEvent = {\n /** Onset in quarter-note beats, absolute from the start of the timeline. */\n readonly startBeat: number;\n /** Quarter-note beats per minute from `startBeat` onwards. */\n readonly bpm: number;\n};\n\n/**\n * A tempo track: {@link TempoEvent}s sorted by `startBeat`, strictly ascending.\n *\n * The first entry is the time origin — {@link beatsToSeconds} returns 0 for its\n * `startBeat` — and its tempo also governs the beats before it, which is where\n * a pickup sounds. A map cannot say anything else about that stretch, and a\n * piece is played into its first downbeat at the tempo it starts in, so elapsed\n * time there is negative rather than an error.\n *\n * @category Rhythm & Meter\n */\nexport type TempoMap = readonly TempoEvent[];\n\nconst SECONDS_PER_MINUTE = 60;\n\n/**\n * Upper bound on a tempo, matching the bound the generators accept. A faster\n * marking is a notation of a slower pulse, not a distinct tempo.\n */\nconst MAX_BPM = 1000;\n\n/**\n * Lower bound on a tempo. One beat lasts ten minutes here; anything slower is a\n * corrupt or mis-scaled value rather than a marking, and letting it through\n * would make the conversions overflow to infinity while still reporting a\n * validated tempo.\n */\nconst MIN_BPM = 0.1;\n\n/**\n * Upper bound on the number of changes one tempo map may declare.\n *\n * A map is read on every conversion asked of it, so an unbounded map is an\n * unbounded per-note cost the way an unbounded event array is an unbounded\n * pass. The bound is the one the meter map takes, and for the same reason: far\n * above any notated piece — a change on every bar of a hundred thousand bars —\n * and it rejects the map rather than the work built on it. A recorded\n * accelerando reaches it, which is exactly the import the bound is for.\n */\nconst MAX_TEMPO_CHANGES = 100_000;\n\n/**\n * Validate a tempo map: non-empty, strictly ascending, finite onsets, and\n * tempos inside the range the conversions stay finite over.\n *\n * An onset is not bounded below, for the reason the note events are not: beat 0\n * is the first downbeat, so a piece that opens with a pickup marks its tempo\n * before it.\n *\n * An unsorted map is rejected rather than sorted: reordering it would silently\n * accept a data error, and two entries on the same beat name no segment at all.\n */\nfunction assertTempoMap(map: TempoMap, name = 'tempo map'): TempoMap {\n if (!Array.isArray(map) || map.length === 0) {\n throw new InvalidInputError(`${name} must hold at least one tempo event`);\n }\n assertGenerationBudget(map.length, `${name} count`, MAX_TEMPO_CHANGES);\n // A map is re-validated on every conversion asked of it, so a pass over a\n // piece pays for the whole map per note unless a map it has already read is\n // recognized as one. The meter map is read the same way.\n if (isValidatedTempoMap(map)) {\n return map;\n }\n for (let index = 0; index < map.length; index += 1) {\n const event = map[index];\n // A hole in a sparse array, an explicit null, and a number are all rejected\n // here rather than at the first field read, which would surface as the\n // library's own TypeError instead of as the malformed input it is. The\n // meter map is read the same way.\n if (typeof event !== 'object' || event === null) {\n throw new InvalidInputError(\n `${name}[${index}] must be a tempo event; received ${describeRejected(event)}`,\n );\n }\n assertFiniteNumber(event.startBeat, `${name}[${index}].startBeat`);\n assertRange(event.bpm, MIN_BPM, MAX_BPM, `${name}[${index}].bpm`);\n const previous = map[index - 1];\n if (previous !== undefined && event.startBeat <= previous.startBeat) {\n throw new InvalidInputError(\n `${name} must be sorted by startBeat with no repeats; ` +\n `[${index}].startBeat ${event.startBeat} does not follow ${previous.startBeat}`,\n );\n }\n }\n rememberValidatedTempoMap(map);\n return map;\n}\n\n/** The map's first event, which is its time origin. */\nfunction firstEvent(map: TempoMap): TempoEvent {\n const first = map[0];\n if (first === undefined) {\n throw new InvalidInputError('tempo map must hold at least one tempo event');\n }\n return first;\n}\n\n/**\n * Elapsed seconds over `[fromBeat, toBeat]`, summed segment by segment. Both\n * bounds are assumed validated; a span running backwards reports negative time,\n * which is what keeps the integral additive across the origin.\n *\n * The sum starts at the segment the span starts in rather than at the map's\n * first, so a note late in a piece is not charged for every tempo change before\n * it. What it costs is the segments the span actually crosses, which is what\n * the span is made of.\n */\nfunction integrateSeconds(map: TempoMap, fromBeat: number, toBeat: number): number {\n if (toBeat < fromBeat) {\n return -integrateSeconds(map, toBeat, fromBeat);\n }\n let seconds = 0;\n for (let index = tempoSegmentAt(map, fromBeat); index < map.length; index += 1) {\n const event = map[index];\n if (event === undefined) {\n continue;\n }\n // The opening tempo reaches back before the beat it is marked on: a pickup\n // is played at the tempo of the piece it leads into, and the map names no\n // other one to play it at.\n const segmentStart = index === 0 ? Number.NEGATIVE_INFINITY : event.startBeat;\n const segmentEnd = map[index + 1]?.startBeat ?? Number.POSITIVE_INFINITY;\n const low = Math.max(fromBeat, segmentStart);\n const high = Math.min(toBeat, segmentEnd);\n if (high > low) {\n seconds += ((high - low) * SECONDS_PER_MINUTE) / event.bpm;\n }\n if (toBeat <= segmentEnd) {\n break;\n }\n }\n return seconds;\n}\n\n/**\n * Wall-clock seconds from the start of a tempo map to a beat.\n *\n * The result integrates every tempo segment the span crosses; it is not the\n * tempo in effect at `beat` applied to the whole span.\n *\n * A beat before the map's first event reports negative elapsed time, at the\n * opening tempo: a pickup sounds before the downbeat, and asking when it sounds\n * is a fair question.\n *\n * @param beat Position in quarter-note beats.\n * @param map The tempo map; its first event is the time origin.\n * @returns Elapsed seconds, 0 at the map's first event and negative before it.\n * @throws If the map is empty, unsorted, or carries a tempo outside 0.1..1000\n * quarter-note beats per minute.\n * @example\n * ```ts\n * import { beatsToSeconds } from '@libraz/libcantus';\n * const map = [{ startBeat: 0, bpm: 120 }, { startBeat: 4, bpm: 60 }];\n * beatsToSeconds(8, map); // 6 = 4 beats at 120 + 4 beats at 60\n * ```\n * @category Rhythm & Meter\n */\nexport function beatsToSeconds(beat: number, map: TempoMap): number {\n assertFiniteNumber(beat, 'beat');\n assertTempoMap(map);\n // The origin-to-beat integral crosses every segment before `beat`, so a pass\n // that asks it of every note re-adds the whole map per note. The sum of the\n // whole segments is the map's own, made once and read here.\n return elapsedSecondsAt(map, beat);\n}\n\n/**\n * The beat reached after a number of seconds — the inverse of\n * {@link beatsToSeconds}.\n *\n * @param seconds Elapsed seconds from the map's first event; negative for a\n * position before it, such as a pickup.\n * @param map The tempo map.\n * @returns The position in quarter-note beats.\n * @throws If the map is malformed or `seconds` is not finite.\n * @example\n * ```ts\n * import { secondsToBeats } from '@libraz/libcantus';\n * const map = [{ startBeat: 0, bpm: 120 }, { startBeat: 4, bpm: 60 }];\n * secondsToBeats(6, map); // 8\n * ```\n * @category Rhythm & Meter\n */\nexport function secondsToBeats(seconds: number, map: TempoMap): number {\n assertFiniteNumber(seconds, 'seconds');\n assertTempoMap(map);\n // Before the origin only the opening tempo is in force, which is the segment\n // {@link beatsToSeconds} integrates backwards over; the prefix sums place a\n // negative elapsed time there without a case of its own.\n return beatAtElapsedSeconds(map, seconds);\n}\n\n/**\n * Duration in seconds of a span of beats.\n *\n * Equal to the difference of two {@link beatsToSeconds} calls — the integral is\n * additive, so that reading is exact — but integrated over the span directly, so\n * a short note late in a long piece does not lose precision to the subtraction\n * of two large elapsed times.\n *\n * @param startBeat Where the span begins, in quarter-note beats; a pickup\n * starts before the map's first event and is measured like any other span.\n * @param lengthBeats How long the span lasts, in quarter-note beats.\n * @param map The tempo map.\n * @returns The span's duration in seconds, never negative.\n * @throws If the map is malformed or the length is negative.\n * @example\n * ```ts\n * import { durationToSeconds } from '@libraz/libcantus';\n * const map = [{ startBeat: 0, bpm: 120 }, { startBeat: 4, bpm: 60 }];\n * durationToSeconds(2, 4, map); // 3 = 2 beats at 120 + 2 beats at 60\n * ```\n * @category Rhythm & Meter\n */\nexport function durationToSeconds(startBeat: number, lengthBeats: number, map: TempoMap): number {\n assertFiniteNumber(startBeat, 'startBeat');\n assertRange(lengthBeats, 0, Number.MAX_SAFE_INTEGER, 'lengthBeats');\n assertTempoMap(map);\n return integrateSeconds(map, startBeat, startBeat + lengthBeats);\n}\n\n/**\n * The tempo in effect at a beat.\n *\n * A beat before the first event reads the opening tempo, which is the tempo a\n * pickup is played at.\n *\n * @param beat Position in quarter-note beats.\n * @param map The tempo map.\n * @returns Quarter-note beats per minute.\n * @throws If the map is malformed.\n * @example\n * ```ts\n * import { tempoAt } from '@libraz/libcantus';\n * tempoAt(5, [{ startBeat: 0, bpm: 120 }, { startBeat: 4, bpm: 60 }]); // 60\n * ```\n * @category Rhythm & Meter\n */\nexport function tempoAt(beat: number, map: TempoMap): number {\n assertFiniteNumber(beat, 'beat');\n assertTempoMap(map);\n return map[tempoSegmentAt(map, beat)]?.bpm ?? firstEvent(map).bpm;\n}\n\n/** A reader that converts many positions against one tempo map. */\nexport type TempoReader = {\n /** Elapsed seconds from the map's origin to a beat. */\n secondsAt(beat: number): number;\n /** The beat reached after a number of seconds from the origin. */\n beatAt(seconds: number): number;\n};\n\n/**\n * Hold a tempo map for the length of one pass, and convert against it.\n *\n * A conversion validates the map it is handed and re-reads it to know the\n * caller has not written to it since, so a pass that converts every note of a\n * piece pays for the whole map per note — and an imported accelerando marks a\n * tempo every tick. A pass that reads the map once instead pays for it once.\n *\n * The map is read when the reader is made, so the reader answers for the map as\n * it was then: a pass hands out no reader it does not own the map of.\n *\n * Not part of the public surface: it hands out a view of a validated map, which\n * only the pass that validated it can say is still current.\n *\n * @param map The tempo map, validated here.\n * @returns The conversions, reading one snapshot of the map.\n */\nexport function tempoReader(map: TempoMap): TempoReader {\n assertTempoMap(map);\n const index = tempoIndexOf(map);\n return {\n secondsAt: (beat) => elapsedSecondsIn(index, assertFiniteNumber(beat, 'beat')),\n beatAt: (seconds) => beatAtElapsedIn(index, assertFiniteNumber(seconds, 'seconds')),\n };\n}\n\n/**\n * Convert quarter-note beats to MIDI ticks.\n *\n * Ticks are whole units of a sequencer's grid, so the result is rounded to the\n * nearest tick; a beat that is not on the grid is not representable and comes\n * back quantized.\n *\n * A position before the first downbeat is a negative tick count, which is how a\n * pickup crosses the MIDI boundary and comes back. A position that rounds to\n * the downbeat from below comes back as positive zero: `-0` and `0` are the\n * same tick, and only one of them keys as it under `Object.is`.\n *\n * @param beat Position or length in quarter-note beats.\n * @param ppq Pulses (ticks) per quarter note, e.g. 96, 480, or 960.\n * @returns The whole tick count.\n * @throws If `beat` is not finite or `ppq` is not a positive integer.\n * @example\n * ```ts\n * import { beatsToTicks } from '@libraz/libcantus';\n * beatsToTicks(1.5, 480); // 720\n * ```\n * @category Rhythm & Meter\n */\nexport function beatsToTicks(beat: number, ppq: number): number {\n assertFiniteNumber(beat, 'beat');\n assertPositiveInt(ppq, 'ppq');\n const ticks = Math.round(beat * ppq);\n return ticks === 0 ? 0 : ticks;\n}\n\n/**\n * Convert MIDI ticks to quarter-note beats.\n *\n * @param ticks Whole tick count, negative before the first downbeat.\n * @param ppq Pulses (ticks) per quarter note, e.g. 96, 480, or 960.\n * @returns The position or length in quarter-note beats.\n * @throws If `ticks` is not an integer or `ppq` is not a positive integer.\n * @example\n * ```ts\n * import { ticksToBeats } from '@libraz/libcantus';\n * ticksToBeats(720, 480); // 1.5\n * ```\n * @category Rhythm & Meter\n */\nexport function ticksToBeats(ticks: number, ppq: number): number {\n assertInteger(ticks, 'ticks');\n assertPositiveInt(ppq, 'ppq');\n return ticks / ppq;\n}\n","/**\n * Tuning and microtonality: frequency conversion, cents, arbitrary equal\n * temperaments (EDO), and just-intonation ratios.\n *\n * The rest of the library reasons in twelve-tone pitch classes, which is a\n * deliberate scope choice for tonal theory. This module is the escape hatch for\n * anyone who needs actual frequencies, cents, non-12 equal temperaments, or the\n * acoustic (just) tuning behind the tempered intervals.\n */\n\nimport { InvalidInputError } from '../errors/index.js';\nimport {\n assertFiniteNumber,\n assertInteger,\n assertPositiveInt,\n assertRange,\n describeRejected,\n} from '../validation/index.js';\n\n/**\n * An equal-tempered tuning: a reference pitch and a number of equal divisions of\n * the octave. With `divisions === 12` a step index is an ordinary MIDI number.\n *\n * @category Pitch & Intervals\n */\nexport type TuningTable = {\n /** Step index (MIDI number when `divisions` is 12) whose frequency is `refFreq`. */\n readonly refStep: number;\n /** Frequency in Hz of `refStep`. */\n readonly refFreq: number;\n /** Equal divisions of the octave (12 standard; e.g. 19 or 31 for microtonal). */\n readonly divisions: number;\n};\n\n/**\n * Standard twelve-tone equal temperament, A4 (MIDI 69) = 440 Hz.\n *\n * @category Pitch & Intervals\n */\nexport const TWELVE_TET: TuningTable = Object.freeze({ refStep: 69, refFreq: 440, divisions: 12 });\n\nfunction assertTuning(tuning: TuningTable): TuningTable {\n // The shape is checked before any field is read: a value from JSON or a\n // plugin host may be null or a number, and reading a reference step off it\n // would report the library's own TypeError instead of the caller's bad input.\n if (typeof tuning !== 'object' || tuning === null) {\n throw new InvalidInputError(\n `tuning must be a tuning table; received ${describeRejected(tuning)}`,\n );\n }\n assertFiniteNumber(tuning.refStep, 'tuning.refStep');\n assertRange(tuning.refFreq, Number.MIN_VALUE, Number.MAX_VALUE, 'tuning.refFreq');\n assertPositiveInt(tuning.divisions, 'tuning.divisions');\n return tuning;\n}\n\n/**\n * Build an equal temperament with `n` divisions of the octave.\n *\n * @param n Divisions of the octave (e.g. 19, 24, 31).\n * @param refFreq Reference frequency in Hz (default 440).\n * @param refStep Step index of the reference (default 69).\n * @returns The tuning.\n * @example\n * ```ts\n * import { edo, frequencyOf } from '@libraz/libcantus';\n * const et19 = edo(19); // { refStep: 69, refFreq: 440, divisions: 19 }\n * frequencyOf(70, et19); // one 19-EDO step above A4\n * ```\n * @category Pitch & Intervals\n */\nexport function edo(n: number, refFreq = 440, refStep = 69): TuningTable {\n assertPositiveInt(n, 'EDO divisions');\n assertRange(refFreq, Number.MIN_VALUE, Number.MAX_VALUE, 'reference frequency');\n assertFiniteNumber(refStep, 'reference step');\n return { refStep, refFreq, divisions: n };\n}\n\n/**\n * Frequency in Hz of a step index under a tuning.\n *\n * @param step Step index (a MIDI number under 12-EDO).\n * @param tuning The tuning (default 12-TET).\n * @returns The frequency in Hz.\n * @example\n * ```ts\n * import { frequencyOf } from '@libraz/libcantus';\n * frequencyOf(69); // 440 (A4 in 12-TET)\n * frequencyOf(60); // middle C in Hz\n * ```\n * @category Pitch & Intervals\n */\nexport function frequencyOf(step: number, tuning: TuningTable = TWELVE_TET): number {\n assertFiniteNumber(step, 'step');\n assertTuning(tuning);\n const result = tuning.refFreq * 2 ** ((step - tuning.refStep) / tuning.divisions);\n return assertRange(result, Number.MIN_VALUE, Number.MAX_VALUE, 'frequency result');\n}\n\n/**\n * Nearest step index to a frequency under a tuning (the inverse of\n * {@link frequencyOf}, rounded).\n *\n * @param freq Frequency in Hz.\n * @param tuning The tuning (default 12-TET).\n * @returns The nearest step index.\n * @category Pitch & Intervals\n */\nexport function nearestStep(freq: number, tuning: TuningTable = TWELVE_TET): number {\n assertRange(freq, Number.MIN_VALUE, Number.MAX_VALUE, 'frequency');\n assertTuning(tuning);\n return Math.round(tuning.refStep + tuning.divisions * Math.log2(freq / tuning.refFreq));\n}\n\n/**\n * Step index of a frequency under a tuning, unrounded.\n *\n * The exact inverse of {@link frequencyOf}. {@link nearestStep} rounds it,\n * which discards how far off the pitch actually is — the figure a tuner\n * display, a pitch-bend amount, or an intonation check is built on.\n *\n * @param freq Frequency in Hz.\n * @param tuning The tuning (default 12-TET).\n * @returns The fractional step index.\n * @example\n * ```ts\n * import { stepOf } from '@libraz/libcantus';\n * stepOf(442); // slightly above 69, the step for A440\n * ```\n * @category Pitch & Intervals\n */\nexport function stepOf(freq: number, tuning: TuningTable = TWELVE_TET): number {\n assertRange(freq, Number.MIN_VALUE, Number.MAX_VALUE, 'frequency');\n assertTuning(tuning);\n return tuning.refStep + tuning.divisions * Math.log2(freq / tuning.refFreq);\n}\n\n/**\n * How far a frequency sits from its nearest step, in cents.\n *\n * Positive means sharp of the step, negative flat.\n *\n * @param freq Frequency in Hz.\n * @param tuning The tuning (default 12-TET).\n * @returns The signed deviation in cents.\n * @example\n * ```ts\n * import { centsFromNearestStep } from '@libraz/libcantus';\n * Math.round(centsFromNearestStep(442)); // 8\n * ```\n * @category Pitch & Intervals\n */\nexport function centsFromNearestStep(freq: number, tuning: TuningTable = TWELVE_TET): number {\n const exact = stepOf(freq, tuning);\n return centsOfSteps(exact - Math.round(exact), tuning);\n}\n\n/**\n * Steps spanned by an interval in cents, unrounded — the inverse of\n * {@link centsOfSteps}.\n *\n * @param cents The interval in cents.\n * @param tuning The tuning (default 12-TET).\n * @returns The fractional step count.\n * @category Pitch & Intervals\n */\nexport function stepsOfCents(cents: number, tuning: TuningTable = TWELVE_TET): number {\n assertFiniteNumber(cents, 'cents');\n assertTuning(tuning);\n return assertFiniteNumber((cents * tuning.divisions) / 1200, 'steps result');\n}\n\n/**\n * The frequency ratio an interval in cents spans — the inverse of\n * {@link ratioToCents}.\n *\n * Multiply a frequency by this to move it by that many cents, which is how a\n * cents offset (a pitch-bend spec, a tuning table entry) is applied.\n *\n * @param cents The interval in cents.\n * @returns The frequency ratio.\n * @example\n * ```ts\n * import { centsToRatio } from '@libraz/libcantus';\n * 440 * centsToRatio(14); // A440 raised by 14 cents\n * ```\n * @category Pitch & Intervals\n */\nexport function centsToRatio(cents: number): number {\n assertFiniteNumber(cents, 'cents');\n const result = 2 ** (cents / 1200);\n return assertRange(result, Number.MIN_VALUE, Number.MAX_VALUE, 'ratio result');\n}\n\n/**\n * Interval in cents between two frequencies.\n *\n * @param a Lower/first frequency in Hz.\n * @param b Upper/second frequency in Hz.\n * @returns Cents from `a` to `b` (negative if `b` is lower).\n * @category Pitch & Intervals\n */\nexport function centsBetweenFreq(a: number, b: number): number {\n assertRange(a, Number.MIN_VALUE, Number.MAX_VALUE, 'first frequency');\n assertRange(b, Number.MIN_VALUE, Number.MAX_VALUE, 'second frequency');\n return 1200 * (Math.log2(b) - Math.log2(a));\n}\n\n/**\n * Cents spanned by a number of equal-temperament steps.\n *\n * @param steps Number of steps.\n * @param tuning The tuning (default 12-TET).\n * @returns The cents.\n * @category Pitch & Intervals\n */\nexport function centsOfSteps(steps: number, tuning: TuningTable = TWELVE_TET): number {\n assertFiniteNumber(steps, 'steps');\n assertTuning(tuning);\n return assertFiniteNumber((steps * 1200) / tuning.divisions, 'cents result');\n}\n\n/**\n * Cents of a frequency ratio, e.g. `ratioToCents(3, 2)` ≈ 701.955 for the just\n * perfect fifth.\n *\n * @param numerator Ratio numerator.\n * @param denominator Ratio denominator.\n * @returns The interval in cents.\n * @category Pitch & Intervals\n */\nexport function ratioToCents(numerator: number, denominator: number): number {\n assertRange(numerator, Number.MIN_VALUE, Number.MAX_VALUE, 'ratio numerator');\n assertRange(denominator, Number.MIN_VALUE, Number.MAX_VALUE, 'ratio denominator');\n return 1200 * (Math.log2(numerator) - Math.log2(denominator));\n}\n\n/** One entry of {@link JUST_RATIOS}: a numerator and a denominator, frozen. */\nfunction justRatio(numerator: number, denominator: number): readonly [number, number] {\n return Object.freeze([numerator, denominator] as [number, number]);\n}\n\n/**\n * Five-limit just-intonation ratios for the twelve interval classes above a\n * unison, indexed by semitone class (0..12).\n *\n * @category Pitch & Intervals\n */\nexport const JUST_RATIOS: Readonly<\n Record<0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12, readonly [number, number]>\n> = Object.freeze({\n 0: justRatio(1, 1),\n 1: justRatio(16, 15),\n 2: justRatio(9, 8),\n 3: justRatio(6, 5),\n 4: justRatio(5, 4),\n 5: justRatio(4, 3),\n 6: justRatio(45, 32),\n 7: justRatio(3, 2),\n 8: justRatio(8, 5),\n 9: justRatio(5, 3),\n 10: justRatio(9, 5),\n 11: justRatio(15, 8),\n 12: justRatio(2, 1),\n});\n\n/** Whether a semitone class is one of the classes {@link JUST_RATIOS} lists. */\nfunction isJustSemitoneClass(value: number): value is keyof typeof JUST_RATIOS {\n return value in JUST_RATIOS;\n}\n\n/**\n * Cents by which a five-limit just interval departs from its 12-TET tempering\n * (positive means the just interval is wider).\n *\n * @param semitoneClass Semitone class in [0, 12].\n * @returns The deviation in cents.\n * @throws If `semitoneClass` is not an integer in [0, 12].\n * @category Pitch & Intervals\n */\nexport function justDeviationCents(semitoneClass: number): number {\n assertInteger(semitoneClass, 'semitone class', 0, 12);\n if (!isJustSemitoneClass(semitoneClass)) {\n throw new InvalidInputError(`semitone class has no just ratio: ${semitoneClass}`);\n }\n const ratio = JUST_RATIOS[semitoneClass];\n return ratioToCents(ratio[0], ratio[1]) - semitoneClass * 100;\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["/home/runner/work/libcantus/libcantus/dist/chunk-PM4XJE2Y.cjs","../src/theory/chordscale/index.ts","../src/theory/harmony/index.ts"],"names":[],"mappings":"AAAA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,wDAA6B;AAC7B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,wDAA6B;AAC7B;AACA;ACaA,SAAS,qBAAA,CAAsB,IAAA,EAAsB,MAAA,EAAQ,OAAA,EAAiB;AAC5E,EAAA,MAAM,UAAA,EAAY,gDAAA,IAAqB,CAAA;AACvC,EAAA,GAAA,CAAI,UAAA,IAAc,KAAA,EAAA,GAAa,CAAC,MAAA,CAAO,MAAA,CAAO,8BAAA,EAAc,SAAS,CAAA,EAAG;AACtE,IAAA,MAAM,IAAI,wCAAA;AAAA,MACR,CAAA,EAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAGA;AACM,EAAA;AACJ,EAAA;AACE,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAGA;AACE,EAAA;AACA,EAAA;AACF;AAYO;AAOL,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAkBM;AAGA;AASA;AACJ,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAGM;AACC,EAAA;AACL,EAAA;AAAgB;AAAA;AAAA;AAAA;AAKhB,EAAA;AACA,EAAA;AACF;AAGA;AACE,EAAA;AACA,EAAA;AACF;AAGA;AACE,EAAA;AACI,EAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAOA;AACE,EAAA;AACF;AA6CO;AACL,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AAAY,QAAA;AACV,QAAA;AACuB,QAAA;AACvB,QAAA;AAED,MAAA;AACH,IAAA;AACF,EAAA;AACI,EAAA;AAGF,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AAKA,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACD,EAAA;AACD,EAAA;AACF;AAwDO;AAKL,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACK,EAAA;AACH,IAAA;AACF,EAAA;AACA,EAAA;AAKA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAWM;AAGN;AAKM,EAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACK,EAAA;AACH,IAAA;AACF,EAAA;AACA,EAAA;AACI,EAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACF;AA+CO;AAKL,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACK,EAAA;AACH,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AAEE,EAAA;AAEJ,EAAA;AACA,EAAA;AACE,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAqCO;AACD,EAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACD,EAAA;AACH;AAgBM;AAGN;AACE,EAAA;AACA,EAAA;AACI,EAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACE,IAAA;AACE,MAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAGA;AACM,EAAA;AACJ,EAAA;AACE,IAAA;AACE,MAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACE,IAAA;AACE,MAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAqBO;AACL,EAAA;AACA,EAAA;AACI,EAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AAAmC,IAAA;AAEnC,EAAA;AAEA,EAAA;AACA,EAAA;AACA,EAAA;AAEA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACE,QAAA;AAAmB,2BAAA;AACM,2BAAA;AACD,QAAA;AAExB,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AAAW,QAAA;AAEf,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACG,IAAA;AACH,IAAA;AACF,EAAA;AAEA,EAAA;AACI,EAAA;AACJ,EAAA;AACE,IAAA;AACE,MAAA;AACF,IAAA;AACF,EAAA;AAEA,EAAA;AACI,EAAA;AACJ,EAAA;AACE,IAAA;AACA,IAAA;AACF,EAAA;AAEA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACD,EAAA;AACH;AD5UO;AACA;AExQA;AAGL,EAAA;AACA,EAAA;AACA,EAAA;AAIA,EAAA;AACA,EAAA;AACA,EAAA;AACI,EAAA;AACA,EAAA;AACA,EAAA;AACF,IAAA;AACA,IAAA;AACF,EAAA;AACE,IAAA;AACA,IAAA;AACF,EAAA;AACE,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACF;AFqQO;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/libcantus/libcantus/dist/chunk-PM4XJE2Y.cjs","sourcesContent":[null,"import { InvalidInputError } from '../../core/errors/index.js';\nimport { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\nimport {\n assertArray,\n assertGenerationBudget,\n assertOptions,\n assertPitchClass,\n assertPositiveInt,\n} from '../../core/validation/index.js';\nimport type { Chord } from '../chord/index.js';\nimport { chordPitchClasses, displacedThirds } from '../chord/index.js';\nimport type { ScaleNameInput } from '../scale/index.js';\nimport {\n assertModeMask,\n NAMED_SCALES,\n namedScaleMask,\n requireScaleMask,\n resolveScaleName,\n} from '../scale/index.js';\nimport { type ChordLike, toChordData } from '../symbol/index.js';\n\n/**\n * The mask of a scale a chord-scale question may be asked about.\n *\n * Chord-scale theory is a Western practice, so the register of world scales is\n * not a vocabulary these functions answer from: a raga offered as the scale\n * over an altered dominant would read as an answer while being a category\n * error. {@link chordScales} proposes {@link NAMED_SCALES} and nothing else,\n * and the questions asked about one chosen scale accept exactly what it can\n * propose — an alias of a Western scale included, a world scale not.\n *\n * @param name The scale name or alias.\n * @param label What the name is, for the error message.\n * @returns The 12-bit mask.\n * @throws If the name is not a built-in scale, or names a world scale.\n */\nfunction requireChordScaleMask(name: ScaleNameInput, label = 'scale'): number {\n const canonical = resolveScaleName(name);\n if (canonical !== undefined && !Object.hasOwn(NAMED_SCALES, canonical)) {\n throw new InvalidInputError(\n `${label} must name a scale chord-scale theory ranks; ${String(name)} is a world scale`,\n );\n }\n return requireScaleMask(name, label);\n}\n\n/** Count the set bits (scale tones) in a 12-bit mode mask. */\nfunction popcount12(mask: number): number {\n let count = 0;\n for (let n = 0; n < 12; n += 1) {\n count += (mask >> n) & 1;\n }\n return count;\n}\n\n/** Test whether a pitch class belongs to a mode mask rooted on `scaleRootPc`. */\nfunction maskHasPitchClass(mask: number, scaleRootPc: number, pc: number): boolean {\n const offset = pitchClass(pc - scaleRootPc);\n return ((mask >> offset) & 1) === 1;\n}\n\n/**\n * Test whether every chord pitch class is contained in a scale.\n *\n * @param chordPcs The chord's pitch classes (0..11).\n * @param scaleMask The scale's 12-bit mode mask.\n * @param scaleRootPc The pitch class the mask is rooted on.\n * @returns True if the scale is a superset of the chord.\n *\n * @category Scales\n */\nexport function scaleMatchesChord(\n chordPcs: number[],\n scaleMask: number,\n scaleRootPc: number,\n): boolean {\n // The mask is read with bit operations and a pitch class shifts it, so both\n // coerce: an entry that is not a pitch class would silently be read as one.\n assertModeMask(scaleMask, 'scaleMask');\n assertPitchClass(scaleRootPc, 'scaleRootPc');\n const pcs = assertArray<number>(chordPcs, 'chord pitch classes');\n for (let index = 0; index < pcs.length; index += 1) {\n const pc = assertPitchClass(pcs[index] ?? Number.NaN, `chord pitch classes[${index}]`);\n if (!maskHasPitchClass(scaleMask, scaleRootPc, pc)) {\n return false;\n }\n }\n return true;\n}\n\n/**\n * A named scale rooted on a pitch class that fits over a chord.\n *\n * @category Scales\n */\nexport type ChordScaleMatch = {\n name: string;\n rootPc: number;\n};\n\n/**\n * Scale names whose mask duplicates another {@link NAMED_SCALES} entry\n * (major/ionian, naturalMinor/aeolian). Chord-scale analysis conventionally\n * speaks in mode names, so these aliases are skipped and only the modal name\n * is reported.\n */\nconst ALIASED_SCALE_NAMES = new Set(['major', 'naturalMinor']);\n\n/** Number of tones in a heptatonic (seven-note) scale. */\nconst HEPTATONIC_SIZE = 7;\n\n/**\n * Conventional preference among equally-fitting scales, brightest first.\n *\n * Several modes fit a chord that states no third or sixth — a power chord, a\n * suspension — equally well. Alphabetical order made `aeolian` the answer for\n * those, naming a minor mode as the best fit for a mode-neutral chord.\n */\nconst SCALE_PREFERENCE = [\n 'ionian',\n 'major',\n 'mixolydian',\n 'dorian',\n 'lydian',\n 'aeolian',\n 'naturalMinor',\n 'phrygian',\n 'locrian',\n] as const;\n\n/** Conventional first choices that pure set-fit ranking cannot express. */\nconst IDIOMATIC_SCALES: Partial<Record<Chord['quality'], readonly string[]>> = {\n aug: ['wholeTone'],\n m7b5: ['locrian'],\n // The diminished scale played over a diminished seventh is the whole-half\n // octatonic. Its half-whole rotation belongs to the dominant it colours: over\n // a diminished seventh every one of its non-chord tones lands a semitone\n // above a chord tone, so it offers no tension at all.\n dim7: ['octatonicWholeHalf'],\n minMaj7: ['melodicMinor'],\n};\n\n/** Position of a scale in {@link SCALE_PREFERENCE}, or last when unlisted. */\nfunction conventionalRank(name: string): number {\n const index = (SCALE_PREFERENCE as readonly string[]).indexOf(name);\n return index < 0 ? SCALE_PREFERENCE.length : index;\n}\n\n/** Whether this scale is an idiomatic color choice for the chord quality. */\nfunction idiomaticRank(chord: Chord, name: string): number {\n const choices = IDIOMATIC_SCALES[chord.quality];\n if (choices === undefined) {\n return 0;\n }\n const index = choices.indexOf(name);\n return index < 0 ? choices.length + 1 : index;\n}\n\n/**\n * An ordinary dominant seventh may use altered colors while retaining its\n * perfect fifth in the voicing; the `7alt` quality makes that alteration\n * explicit, but lead sheets commonly write the shorter `7` here.\n */\nfunction isAlteredDominant(chord: Chord, scaleName: string): boolean {\n return chord.quality === 'dom7' && scaleName === 'altered';\n}\n\n/**\n * List the scales that fit over a chord, best fit first.\n *\n * Only the chord root is considered as the scale root, matching the\n * conventional chord-scale relationship. Every entry of {@link NAMED_SCALES}\n * whose pitch-class set is a superset of the chord's is returned once per\n * distinct pitch-class set (aliased masks such as major/ionian report only the\n * modal name). The chromatic scale is only returned as a fallback when no other\n * scale contains the chord.\n *\n * The ranking applies these keys in order:\n *\n * 1. The idiomatic scales for the chord quality, in the order they are named\n * there, ahead of every scale that is not one of them.\n * 2. For bare triads and smaller chords, heptatonic scales ahead of pentatonics\n * and other sizes: a pentatonic adds no modal color over a triad, so the\n * seven-note modes are the more useful answer.\n * 3. Fewest extra scale tones beyond the chord.\n * 4. Conventional modal preference.\n * 5. Fewest avoid notes, which separates the symmetric and melodic-minor\n * scales the modal preference does not name.\n * 6. Alphabetical order, so the list is stable.\n *\n * The idiomatic key comes first, so the head of the list is the scale a player\n * would name rather than the tightest fit: over `Cm7b5` the locrian mode leads\n * even though the blues scale adds fewer tones, because locrian is what a\n * half-diminished chord is read as. Read further down the list, or pick by name,\n * when the closest fit is what is wanted; {@link scalesForChanges} makes the\n * choice for a whole progression.\n *\n * @param chord The chord to fit scales over, as a chord symbol, chord data, or\n * a `Chord`.\n * @returns The matching scales rooted on the chord root, best fit first.\n *\n * @example\n * ```ts\n * import { makeChord, chordScales } from '@libraz/libcantus';\n * const scales = chordScales(makeChord(0, 'maj7'));\n * // scales for Cmaj7, best fit first; each is { name, rootPc: 0 }\n * ```\n *\n * @category Scales\n */\nexport function chordScales(chord: ChordLike): ChordScaleMatch[] {\n const data = toChordData(chord);\n const chordPcs = chordPitchClasses(data);\n const rootPc = pitchClass(data.rootPc);\n const ranked: { name: string; extra: number; size: number; avoid: number }[] = [];\n const seenMasks = new Set<number>();\n for (const name of Object.keys(NAMED_SCALES)) {\n if (name === 'chromatic' || ALIASED_SCALE_NAMES.has(name)) {\n continue;\n }\n const mask = namedScaleMask(name);\n if (mask === undefined || seenMasks.has(mask)) {\n continue;\n }\n seenMasks.add(mask);\n if (scaleMatchesChord(chordPcs, mask, rootPc) || isAlteredDominant(data, name)) {\n const size = popcount12(mask);\n ranked.push({\n name,\n extra: size - chordPcs.length,\n size,\n avoid: avoidNotes(data, name).length,\n });\n }\n }\n if (ranked.length === 0) {\n // No diatonic-style scale contains the chord; fall back to the chromatic\n // scale, which trivially contains every pitch class.\n if (NAMED_SCALES.chromatic !== undefined) {\n return [{ name: 'chromatic', rootPc }];\n }\n return [];\n }\n const preferHeptatonic = chordPcs.length <= 3;\n ranked.sort((a, b) => {\n const idiomaticA = idiomaticRank(data, a.name);\n const idiomaticB = idiomaticRank(data, b.name);\n if (idiomaticA !== idiomaticB) {\n return idiomaticA - idiomaticB;\n }\n if (preferHeptatonic && (a.size === HEPTATONIC_SIZE) !== (b.size === HEPTATONIC_SIZE)) {\n return a.size === HEPTATONIC_SIZE ? -1 : 1;\n }\n if (a.extra !== b.extra) {\n return a.extra - b.extra;\n }\n const rankA = conventionalRank(a.name);\n const rankB = conventionalRank(b.name);\n if (rankA !== rankB) {\n return rankA - rankB;\n }\n // Among scales the modal preference does not name — the symmetric and\n // melodic-minor scales — the one that clashes with the chord least is the\n // one a player reaches for. Alphabetical order answered with the rotation\n // that leaves nothing playable.\n if (a.avoid !== b.avoid) {\n return a.avoid - b.avoid;\n }\n return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;\n });\n return ranked.map((entry) => ({ name: entry.name, rootPc }));\n}\n\n/**\n * Which use an avoid note is judged for: sounding it, or passing through it.\n *\n * `'harmonic'` is the Berklee-style rule in full — a tone that must not be\n * sounded against the chord. `'melodic'` keeps only what a line cannot pass\n * through either, which is the semitone above the root; every other clash\n * resolves as the line moves on.\n *\n * @category Scales\n */\nexport type AvoidNoteUse = 'harmonic' | 'melodic';\n\n/**\n * Options for {@link avoidNotes}.\n *\n * @category Scales\n */\nexport type AvoidNotesOptions = {\n use?: AvoidNoteUse;\n};\n\n/**\n * List a scale's avoid notes over a chord.\n *\n * An avoid note is a non-chord scale tone that lies a semitone directly above a\n * chord tone, or the third a suspension displaced; sounding it against the\n * chord clashes. The scale is rooted on the chord root. If the scale does not\n * contain the chord, no avoid notes exist.\n *\n * The rule states which tones must not be *sounded* against the chord, which is\n * not the same question as which tones a line may not touch. Pass\n * `{ use: 'melodic' }` for the melodic reading: only the semitone above the\n * root survives it, so the result is always a subset of the harmonic one.\n *\n * @param chord The chord providing the chord tones, as a chord symbol, chord\n * data, or a `Chord`.\n * @param scaleName A key of {@link NAMED_SCALES}, or an alias of one. A world\n * scale is not one: chord-scale theory is a Western practice, so a raga named\n * here is rejected rather than answered with a number.\n * @param opts Set `use: 'melodic'` to judge a line rather than a voicing.\n * @returns The avoid-note pitch classes, sorted ascending in [0, 11].\n * @throws If `scaleName` is not a built-in scale. An empty result already\n * means \"this scale has no avoid notes over this chord\"; answering a typo\n * the same way would make the two indistinguishable.\n *\n * @example\n * ```ts\n * import { makeChord, avoidNotes } from '@libraz/libcantus';\n * avoidNotes(makeChord(0, 'maj7'), 'ionian'); // [5] — F clashes with the third\n * avoidNotes(makeChord(0, 'maj7'), 'ionian', { use: 'melodic' }); // [] — a line may pass through it\n * ```\n *\n * @category Scales\n */\nexport function avoidNotes(\n chord: ChordLike,\n scaleName: ScaleNameInput,\n opts: AvoidNotesOptions = {},\n): number[] {\n const asked = assertOptions(opts, 'opts');\n const mask = requireChordScaleMask(scaleName);\n const data = toChordData(chord);\n const rootPc = pitchClass(data.rootPc);\n const chordPcs = chordPitchClasses(data);\n const alteredDominant = isAlteredDominant(data, scaleName);\n if (!scaleMatchesChord(chordPcs, mask, rootPc) && !alteredDominant) {\n return [];\n }\n const chordSet = new Set(chordPcs);\n // Sounding the third a suspension stands in for is what undoes the\n // suspension, so it is an avoid note however consonant it is against the\n // scale. Both thirds are named: a suspension does not say which one it\n // displaced — that is what suspending is — so meeting either one ends it.\n const displaced = new Set(displacedThirds(data));\n const melodic = asked.use === 'melodic';\n const semitoneAboveRoot = pitchClass(rootPc + 1);\n const avoid: number[] = [];\n for (let pc = 0; pc < 12; pc += 1) {\n if (!maskHasPitchClass(mask, rootPc, pc) || chordSet.has(pc)) {\n continue;\n }\n if (melodic && pc !== semitoneAboveRoot) {\n continue;\n }\n if (!alteredDominant && (chordSet.has(pitchClass(pc - 1)) || displaced.has(pc))) {\n avoid.push(pc);\n }\n }\n return avoid.sort((a, b) => a - b);\n}\n\n/**\n * Semitones above the root of the colors a dominant takes when it resolves to a\n * minor tonic: the flat ninth, the sharp ninth, and the flat thirteenth.\n *\n * Over a dominant read on its own, each of them sits a semitone above a chord\n * tone and is an avoid note. Heard as the dominant of a minor key they are the\n * key's own tones, and standard practice, so the function the chord serves is\n * what decides.\n */\nconst MINOR_RESOLUTION_TENSIONS = [1, 3, 8] as const;\n\n/** Whether this chord is a dominant seventh resolving down a fifth to minor. */\nfunction resolvesToMinorTonic(\n chord: Chord,\n chordPcs: number[],\n target: Chord | undefined,\n): boolean {\n if (target === undefined) {\n return false;\n }\n const rootPc = pitchClass(chord.rootPc);\n const sounds = (semitones: number): boolean => chordPcs.includes(pitchClass(rootPc + semitones));\n if (!sounds(4) || !sounds(10)) {\n return false;\n }\n const targetRootPc = pitchClass(target.rootPc);\n if (targetRootPc !== pitchClass(rootPc + 5)) {\n return false;\n }\n return chordPitchClasses(target).includes(pitchClass(targetRootPc + 3));\n}\n\n/**\n * Options for {@link availableTensions}.\n *\n * @category Scales\n */\nexport type AvailableTensionsOptions = {\n /**\n * The chord this one resolves to, as a chord symbol, chord data, or a\n * `Chord`.\n */\n resolvesTo?: ChordLike;\n};\n\n/**\n * List a scale's available tensions over a chord.\n *\n * Available tensions are scale tones that are neither chord tones nor avoid\n * notes, i.e. the usable color tones (typically the 9/11/13 region). The scale\n * is rooted on the chord root. If the scale does not contain the chord, there\n * are no available tensions.\n *\n * A tension's availability also depends on what the chord is doing. Pass\n * `resolvesTo` to say which chord this one resolves to: a dominant seventh\n * resolving down a fifth to a minor tonic takes its flat ninth, sharp ninth and\n * flat thirteenth, which read on their own would each be an avoid note. Only\n * scale tones are ever added, so the result stays within the scale.\n *\n * @param chord The chord providing the chord tones, as a chord symbol, chord\n * data, or a `Chord`.\n * @param scaleName A key of {@link NAMED_SCALES}, or an alias of one; a world\n * scale is outside chord-scale theory, as in {@link avoidNotes}.\n * @param opts Set `resolvesTo` to the chord this one resolves to.\n * @returns The available-tension pitch classes, sorted ascending in [0, 11].\n * @throws If `scaleName` is not a built-in scale, as {@link avoidNotes} does.\n *\n * @example\n * ```ts\n * import { makeChord, availableTensions } from '@libraz/libcantus';\n * availableTensions(makeChord(0, 'maj7'), 'ionian'); // [2, 9] — color tones over Cmaj7\n * availableTensions(makeChord(7, 'dom7'), 'phrygianDominant', { resolvesTo: makeChord(0, 'min') });\n * // [3, 8] — the b13 and b9 the resolution to C minor makes available\n * ```\n *\n * @category Scales\n */\nexport function availableTensions(\n chord: ChordLike,\n scaleName: ScaleNameInput,\n opts: AvailableTensionsOptions = {},\n): number[] {\n const asked = assertOptions(opts, 'opts');\n const mask = requireChordScaleMask(scaleName);\n const data = toChordData(chord);\n const rootPc = pitchClass(data.rootPc);\n const chordPcs = chordPitchClasses(data);\n const alteredDominant = isAlteredDominant(data, scaleName);\n if (!scaleMatchesChord(chordPcs, mask, rootPc) && !alteredDominant) {\n return [];\n }\n const chordSet = new Set(chordPcs);\n const avoidSet = alteredDominant ? new Set<number>() : new Set(avoidNotes(data, scaleName));\n const functional = resolvesToMinorTonic(\n data,\n chordPcs,\n asked.resolvesTo === undefined ? undefined : toChordData(asked.resolvesTo),\n )\n ? new Set(MINOR_RESOLUTION_TENSIONS.map((semitones) => pitchClass(rootPc + semitones)))\n : undefined;\n const tensions: number[] = [];\n for (let pc = 0; pc < 12; pc += 1) {\n if (!maskHasPitchClass(mask, rootPc, pc) || chordSet.has(pc)) {\n continue;\n }\n if (avoidSet.has(pc) && functional?.has(pc) !== true) {\n continue;\n }\n tensions.push(pc);\n }\n return tensions.sort((a, b) => a - b);\n}\n\n/**\n * A scale fit over a chord, with its non-chord tones sorted by what may be done\n * with them.\n *\n * `avoid`, `passing` and `tensions` partition the scale tones the chord does\n * not state: `avoid` may not be played at all, `passing` may be passed through\n * melodically but not sounded against the chord, and `tensions` may be added\n * freely as color. `avoid` is {@link avoidNotes} under `use: 'melodic'`, and\n * `avoid` together with `passing` is the harmonic reading.\n *\n * @category Scales\n */\nexport type ChordScaleReportEntry = {\n name: string;\n rootPc: number;\n avoid: number[];\n passing: number[];\n tensions: number[];\n};\n\n/**\n * Report the best-fitting scales for a chord with their avoid notes and tensions.\n *\n * Combines {@link chordScales}, {@link avoidNotes}, and {@link availableTensions}\n * into a single ergonomic result, ordered best fit first. The report is read as\n * playing advice, so the tones a line may pass through are reported apart from\n * the ones it may not touch; see {@link ChordScaleReportEntry}.\n *\n * @param chord The chord to analyze, as a chord symbol, chord data, or a\n * `Chord`.\n * @param limit Optional maximum number of scales to report; all by default.\n * @returns One entry per reported scale, best fit first.\n *\n * @category Scales\n */\nexport function chordScaleReport(chord: ChordLike, limit?: number): ChordScaleReportEntry[] {\n if (limit !== undefined) {\n assertPositiveInt(limit, 'chord-scale report limit');\n }\n const data = toChordData(chord);\n const matches = chordScales(data);\n const chosen = limit === undefined ? matches : matches.slice(0, limit);\n return chosen.map((match) => {\n const harmonic = avoidNotes(data, match.name);\n const melodic = new Set(avoidNotes(data, match.name, { use: 'melodic' }));\n return {\n name: match.name,\n rootPc: match.rootPc,\n avoid: harmonic.filter((pc) => melodic.has(pc)),\n passing: harmonic.filter((pc) => !melodic.has(pc)),\n tensions: availableTensions(data, match.name),\n };\n });\n}\n\n/**\n * A chord paired with the scale chosen for it by {@link scalesForChanges}.\n *\n * @category Scales\n */\nexport type ScaleChoice = {\n chord: Chord;\n scale: ChordScaleMatch;\n};\n\n/**\n * Small per-candidate penalty added in {@link scalesForChanges} so that, among\n * choices of otherwise-equal transition cost, the tighter best-fit scale wins.\n */\nconst RANK_PENALTY = 0.01;\n\n/** Build the pitch-class set of a named scale rooted on `rootPc`. */\nfunction scalePitchClassSet(name: string, rootPc: number): Set<number> {\n const mask = namedScaleMask(name);\n const pcs = new Set<number>();\n if (mask === undefined) {\n return pcs;\n }\n for (let n = 0; n < 12; n += 1) {\n if (((mask >> n) & 1) === 1) {\n pcs.add(pitchClass(rootPc + n));\n }\n }\n return pcs;\n}\n\n/** Count pitch classes that belong to exactly one of two sets. */\nfunction symmetricDifferenceSize(a: Set<number>, b: Set<number>): number {\n let count = 0;\n for (const pc of a) {\n if (!b.has(pc)) {\n count += 1;\n }\n }\n for (const pc of b) {\n if (!a.has(pc)) {\n count += 1;\n }\n }\n return count;\n}\n\n/**\n * Choose one scale per chord across a progression, favoring continuity.\n *\n * Each chord's {@link chordScales} candidates form a stage in a Viterbi-style\n * dynamic program. The transition cost between adjacent choices is the number\n * of pitch classes that differ between their pitch-class sets (the symmetric\n * difference), plus a small penalty for straying from a chord's best-fit scale\n * so that ties break toward the tighter fit. The minimum-total-cost path is\n * returned, one {@link ScaleChoice} per input chord in the original order.\n *\n * Each choice pairs its scale with the chord exactly as it was handed in, so\n * this takes plain chord data rather than the wider forms the entry points\n * above accept.\n *\n * @param chords The chord sequence to choose scales for.\n * @returns One scale choice per chord, in input order.\n *\n * @category Scales\n */\nexport function scalesForChanges(chords: readonly Chord[]): ScaleChoice[] {\n assertArray(chords, 'chord-scale changes');\n assertGenerationBudget(chords.length, 'chord-scale changes');\n if (chords.length === 0) {\n return [];\n }\n const candidateLists = chords.map((chord) => chordScales(chord));\n const pcSets = candidateLists.map((candidates) =>\n candidates.map((match) => scalePitchClassSet(match.name, match.rootPc)),\n );\n\n const first = candidateLists[0] ?? [];\n const dp: number[][] = [first.map((_, j) => j * RANK_PENALTY)];\n const back: number[][] = [first.map(() => -1)];\n\n for (let i = 1; i < candidateLists.length; i += 1) {\n const candidates = candidateLists[i] ?? [];\n const prevCosts = dp[i - 1] ?? [];\n const prevSets = pcSets[i - 1] ?? [];\n const curSets = pcSets[i] ?? [];\n const stageCosts: number[] = [];\n const stageBack: number[] = [];\n for (let j = 0; j < candidates.length; j += 1) {\n let bestCost = Number.POSITIVE_INFINITY;\n let bestPrev = -1;\n for (let k = 0; k < prevCosts.length; k += 1) {\n const transition = symmetricDifferenceSize(\n prevSets[k] ?? new Set(),\n curSets[j] ?? new Set(),\n );\n const cost = (prevCosts[k] ?? 0) + transition;\n if (cost < bestCost) {\n bestCost = cost;\n bestPrev = k;\n }\n }\n stageCosts.push(bestCost + j * RANK_PENALTY);\n stageBack.push(bestPrev);\n }\n dp.push(stageCosts);\n back.push(stageBack);\n }\n\n const lastCosts = dp[dp.length - 1] ?? [];\n let bestFinal = 0;\n for (let j = 1; j < lastCosts.length; j += 1) {\n if ((lastCosts[j] ?? Number.POSITIVE_INFINITY) < (lastCosts[bestFinal] ?? 0)) {\n bestFinal = j;\n }\n }\n\n const chosenIndices: number[] = new Array(candidateLists.length);\n let current = bestFinal;\n for (let i = candidateLists.length - 1; i >= 0; i -= 1) {\n chosenIndices[i] = current;\n current = back[i]?.[current] ?? -1;\n }\n\n return chords.map((chord, i) => {\n const candidates = candidateLists[i] ?? [];\n const scale = candidates[chosenIndices[i] ?? 0] ?? { name: 'chromatic', rootPc: chord.rootPc };\n return { chord, scale };\n });\n}\n","import { pitchClassOf } from '../../core/pitch/index.js';\nimport { assertFiniteNumber } from '../../core/validation/index.js';\nimport { type ChordToneRole, chordToneRole, thirdSlotOf } from '../chord/index.js';\nimport { type ChordLike, toChordData } from '../symbol/index.js';\n\n/**\n * Harmonic role a pitch plays within a chord.\n *\n * @category Functional Harmony\n */\nexport type HarmonyRole = ChordToneRole | 'tension' | 'doubling';\n\n/**\n * How firmly a pitch is locked to the chord's identity.\n *\n * - `identity`: moving it produces a different chord (the root).\n * - `quality`: moving it flips the chord quality (the third).\n * - `voicing`: it can move freely without changing chord identity or quality.\n *\n * @category Functional Harmony\n */\nexport type LockLevel = 'identity' | 'quality' | 'voicing';\n\n/**\n * A pitch's role, lock level, and owning chord.\n *\n * @category Functional Harmony\n */\nexport type VoicedRole = {\n role: HarmonyRole;\n lock: LockLevel;\n belongsToChordId: number;\n};\n\n/**\n * Classify a pitch's harmonic role and lock level within a chord.\n *\n * The role comes from the pitch's interval class above the chord root, read\n * against the chord's own template: root (0), third (3/4), fifth (6/7/8),\n * sixth or seventh (9/10/11). Every pitch the template does not claim is\n * `'tension'` — including one that is simply foreign to the chord, which this\n * single-pitch query cannot tell from a colour tone. The root locks the chord\n * identity, the third locks its quality, and everything else is free voicing.\n * Where a chord has no third, the tone standing in its place — the fourth of a\n * `sus4`, the second of a `sus2`, the eleventh of an eleventh chord — takes the\n * third's slot and is locked to `quality`, since moving it changes the chord.\n * At most one interval class of a chord is ever reported as its third, so a\n * consumer reading `lock: 'quality'` as \"this tone identifies the chord\" gets\n * one answer rather than two. Detecting an octave doubling requires the\n * surrounding voicing, which this query does not carry, so `'doubling'` is part\n * of the type — the shared vocabulary of harmonic roles — but is never returned\n * here.\n *\n * @param pitch MIDI pitch or bare pitch class.\n * @param chord The chord providing the root reference, as a chord symbol, chord\n * data, or a `Chord`.\n * @param chordId Identifier stored on the result (defaults to 0).\n * @returns The pitch's role, lock level, and owning chord id.\n * @example\n * ```ts\n * import { roleOf, makeChord } from '@libraz/libcantus';\n * roleOf(64, makeChord(0, 'maj')); // E over C major\n * // { role: 'third', lock: 'quality', belongsToChordId: 0 }\n * ```\n * @category Functional Harmony\n */\nexport function roleOf(pitch: number, chord: ChordLike, chordId = 0): VoicedRole {\n // The id is carried into the result, so a value that is not one would be\n // reported back as the chord this role belongs to.\n assertFiniteNumber(chordId, 'chordId');\n const data = toChordData(chord);\n const interval = (pitchClassOf(pitch) - pitchClassOf(data.rootPc) + 12) % 12;\n // What stands in the third's slot is read where the chord is read, not here:\n // the tone that identifies a chord and the tone the avoid-note rule refuses\n // against it are two consequences of one fact about the chord.\n const slot = thirdSlotOf(data);\n const isSuspendedTone = slot.kind === 'suspension' && interval === slot.interval;\n const chordRole = chordToneRole(pitch, data);\n let role: HarmonyRole;\n let lock: LockLevel;\n if (isSuspendedTone) {\n role = 'third'; // suspended tone occupies the third's quality-defining slot\n lock = 'quality';\n } else if (chordRole !== null) {\n role = chordRole;\n lock = chordRole === 'root' ? 'identity' : chordRole === 'third' ? 'quality' : 'voicing';\n } else {\n role = 'tension';\n lock = 'voicing';\n }\n return { role, lock, belongsToChordId: chordId };\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/core/event-index/index.ts"],"sourcesContent":["import { BEAT_EPS } from '../meter/internal.js';\nimport type { NoteEvent } from '../types.js';\nimport { assertFiniteNumber, assertNoteEvents, assertOptions } from '../validation/index.js';\n\n/**\n * A validated note retaining its position in the caller's original array.\n *\n * @category Core\n */\nexport type IndexedNoteEvent = {\n readonly note: Readonly<NoteEvent>;\n readonly originalIndex: number;\n readonly endBeat: number;\n};\n\n/**\n * Which note wins when several sound from the same onset.\n *\n * `'highest'` and `'lowest'` name a voice, so the answer does not depend on the\n * order the caller happened to store the chord in; `'last'` takes the one\n * latest in the input array. Two notes of the same pitch name no voice apart,\n * so both voice tie-breaks settle a unison on the lower `originalIndex` and\n * name the same note.\n *\n * @category Core\n */\nexport type OnsetTieBreak = 'highest' | 'lowest' | 'last';\n\n/**\n * Binary-searchable, stable-sorted index over note onsets and active spans.\n *\n * @category Core\n */\nexport type NoteEventIndex = {\n notes: readonly IndexedNoteEvent[];\n /**\n * Latest-onset note sounding at `beat`. Simultaneous onsets are resolved by\n * the index's {@link OnsetTieBreak}.\n *\n * @throws If `beat` is not finite.\n */\n at: (beat: number) => IndexedNoteEvent | undefined;\n /**\n * Whether one or more sounding notes attack at `beat`.\n *\n * @throws If `beat` is not finite.\n */\n attacksAt: (beat: number) => boolean;\n /**\n * Unique sounding-note attack beats strictly inside `(startBeat, endBeat)`.\n *\n * @throws If either bound is not finite.\n */\n onsetsBetween: (startBeat: number, endBeat: number) => number[];\n};\n\nfunction upperBound(values: readonly IndexedNoteEvent[], beat: number): number {\n let low = 0;\n let high = values.length;\n while (low < high) {\n const middle = Math.floor((low + high) / 2);\n if ((values[middle]?.note.startBeat ?? Number.POSITIVE_INFINITY) <= beat) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n return low;\n}\n\n/**\n * Copy note events and put them in stable onset order.\n *\n * The order is the one {@link createNoteEventIndex} numbers its notes in, so a\n * pass may read the sequence here and the index there and see the same notes in\n * the same places. What it does not build is the index itself: a caller that\n * only walks the notes in time order would otherwise pay for a segment tree and\n * a wrapper per note that it never queries.\n *\n * The events are copied, so the sequence is the caller's own and two entries\n * that arrived as one shared object stay two.\n *\n * @param events The events to order; already validated by the caller.\n * @returns The copies, earliest onset first, ties in input order.\n */\nexport function sortedNoteEvents(events: readonly NoteEvent[]): NoteEvent[] {\n const sorted = events.map((note) => ({ ...note }));\n // Array sort is stable, so events sharing an onset keep the order they were\n // given in — the tie-break the index spells out as `originalIndex`.\n sorted.sort((a, b) => a.startBeat - b.startBeat);\n return sorted;\n}\n\n/**\n * How an index reads the events it is built from.\n *\n * @category Core\n */\nexport type NoteEventIndexOptions = {\n allowNonPositiveDuration?: boolean;\n tieBreak?: OnsetTieBreak;\n budget?: number;\n /** What the events are, for the validation error message. */\n name?: string;\n};\n\n/**\n * Validate and stable-sort note events once, then expose onset and active-note\n * lookups that cost `O(log n + k)`, where `k` is how many notes share the onset\n * the answer comes from. Non-positive-duration notes may be retained for\n * callers that intentionally filter them later, but never count as sounding.\n *\n * @category Core\n */\nexport function createNoteEventIndex(\n events: readonly NoteEvent[],\n options: NoteEventIndexOptions = {},\n): NoteEventIndex {\n const asked = assertOptions(options, 'options');\n assertNoteEvents(events, asked.name ?? 'note events', asked);\n const tieBreak = asked.tieBreak ?? 'highest';\n // Each wrapper is frozen, not only the array and the note inside it: `endBeat`\n // is what the active-note search is built from, so a host that writes to it\n // would leave the index answering from a span nothing else knows about.\n const notes = Object.freeze(\n events\n .map((note, originalIndex) =>\n Object.freeze({\n note: Object.freeze({ ...note }),\n originalIndex,\n endBeat: note.startBeat + note.durationBeat,\n }),\n )\n .sort((a, b) => a.note.startBeat - b.note.startBeat || a.originalIndex - b.originalIndex),\n );\n // Segment-tree maxima find the latest active onset in logarithmic time even\n // when an early, very long note would defeat a prefix-max backwards scan.\n //\n // It is built on the first `at` query rather than on construction: the onset\n // queries answer from the sorted notes alone, so a caller that never asks\n // which note is sounding would otherwise pay for a structure it never reads.\n let maxEndTree: number[] | undefined;\n let treeSize = 0;\n const endTree = (): number[] => {\n if (maxEndTree !== undefined) {\n return maxEndTree;\n }\n treeSize = 1;\n while (treeSize < notes.length) treeSize *= 2;\n const tree = new Array<number>(treeSize * 2).fill(Number.NEGATIVE_INFINITY);\n for (let index = 0; index < notes.length; index += 1) {\n tree[treeSize + index] = notes[index]?.endBeat ?? Number.NEGATIVE_INFINITY;\n }\n for (let index = treeSize - 1; index > 0; index -= 1) {\n tree[index] = Math.max(\n tree[index * 2] ?? Number.NEGATIVE_INFINITY,\n tree[index * 2 + 1] ?? Number.NEGATIVE_INFINITY,\n );\n }\n maxEndTree = tree;\n return tree;\n };\n const latestActiveIndex = (exclusive: number, beat: number): number => {\n const tree = endTree();\n const find = (node: number, start: number, end: number): number => {\n if (start >= exclusive || (tree[node] ?? Number.NEGATIVE_INFINITY) <= beat + BEAT_EPS) {\n return -1;\n }\n if (end - start === 1) return start < notes.length ? start : -1;\n const mid = Math.floor((start + end) / 2);\n const right = find(node * 2 + 1, mid, end);\n return right >= 0 ? right : find(node * 2, start, mid);\n };\n return find(1, 0, treeSize);\n };\n\n return {\n notes,\n at(beat) {\n // A query beat comes from wherever the host got it — a scrub position, a\n // parsed field — so it is checked like any other entry argument. `NaN`\n // compares false against every bound, which would read as an open window\n // rather than as the missing value it is.\n assertFiniteNumber(beat, 'beat');\n const index = latestActiveIndex(upperBound(notes, beat + BEAT_EPS), beat);\n if (index < 0) return undefined;\n const onset = notes[index]?.note.startBeat;\n let best: IndexedNoteEvent | undefined;\n for (let candidateIndex = index; candidateIndex >= 0; candidateIndex -= 1) {\n const indexed = notes[candidateIndex];\n if (indexed !== undefined && indexed.note.startBeat !== onset) break;\n if (\n indexed === undefined ||\n indexed.note.durationBeat <= 0 ||\n indexed.note.startBeat - BEAT_EPS > beat ||\n beat >= indexed.endBeat - BEAT_EPS\n ) {\n continue;\n }\n if (best === undefined) {\n best = indexed;\n continue;\n }\n if (tieBreak === 'last') continue;\n const difference = indexed.note.pitch - best.note.pitch;\n // Equal pitches name no voice apart, so they are settled on the lower\n // original index rather than on whichever end of the array the scan\n // happens to reach last — otherwise `'highest'` and `'lowest'` would\n // answer a unison differently.\n const wins =\n difference === 0\n ? indexed.originalIndex < best.originalIndex\n : difference > 0 === (tieBreak === 'highest');\n if (wins) {\n best = indexed;\n }\n }\n return best;\n },\n attacksAt(beat) {\n assertFiniteNumber(beat, 'beat');\n // Only a sounding note attacks: a zero-length artefact must not make\n // `attacksAt(b)` true on a beat where `at(b)` finds nothing.\n let index = upperBound(notes, beat + BEAT_EPS) - 1;\n while (index >= 0) {\n const candidate = notes[index];\n if (candidate === undefined || Math.abs(candidate.note.startBeat - beat) >= BEAT_EPS) {\n return false;\n }\n if (candidate.note.durationBeat > 0) {\n return true;\n }\n index -= 1;\n }\n return false;\n },\n onsetsBetween(startBeat, endBeat) {\n assertFiniteNumber(startBeat, 'startBeat');\n assertFiniteNumber(endBeat, 'endBeat');\n const result: number[] = [];\n let index = upperBound(notes, startBeat + BEAT_EPS);\n while (index < notes.length) {\n const indexed = notes[index];\n const onset = indexed?.note.startBeat;\n if (indexed === undefined || onset === undefined || onset >= endBeat - BEAT_EPS) {\n break;\n }\n index += 1;\n if (indexed.note.durationBeat <= 0) {\n continue;\n }\n const previous = result[result.length - 1];\n if (previous === undefined || Math.abs(previous - onset) >= BEAT_EPS) {\n result.push(onset);\n }\n }\n return result;\n },\n };\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["/home/runner/work/libcantus/libcantus/dist/chunk-PWC4CRWT.cjs","../src/analyze/spelling/index.ts"],"names":[],"mappings":"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","file":"/home/runner/work/libcantus/libcantus/dist/chunk-PWC4CRWT.cjs","sourcesContent":[null,"/**\n * Line spelling: name every note of a melodic line in one pass.\n *\n * {@link spellPitch} decides one note from the key, the root of the chord under\n * it and its two immediate neighbours. That is enough for a single note, but a\n * line spelled note by note can zig-zag between the two readings of one idea —\n * a rising whole-tone figure comes out F# Ab Bb, with a diminished third in the\n * middle of it.\n *\n * This module solves the whole line at once. The theory layer's own judgment\n * becomes the cost of emitting a spelling, the melodic rules become the cost of\n * moving from one spelling to the next, and a dynamic program takes the\n * cheapest path across the line. Nothing here re-decides what\n * {@link spellPitchClass} and {@link spellChord} already decide: it only weighs\n * their answers against the shape of the line they sit in.\n */\n\nimport type { Note, NoteLike } from '../../core/pitch/index.js';\nimport {\n diatonicLetterOf as mod7,\n pitchClassOf as mod12,\n naturalPitchClassOf as naturalPc,\n toNoteData,\n} from '../../core/pitch/index.js';\nimport type { KeyScale, NoteEvent } from '../../core/types.js';\nimport {\n assertGenerationBudget,\n assertNoteEvents,\n assertOptions,\n} from '../../core/validation/index.js';\nimport type { Chord } from '../../theory/chord/index.js';\nimport { isScaleTone, resolveKey, type SpelledKeyLike } from '../../theory/scale/index.js';\nimport { assertTonicOf, spellChord, spellPitchClass } from '../../theory/spelling/index.js';\nimport type { ChordTimeline } from '../timeline/index.js';\n\n/**\n * The four rules, and what happens where they pull against each other.\n *\n * The costs below are spread far enough apart that a rule can never be\n * outvoted by any number of the rules under it, which fixes the precedence:\n *\n * 1. **The sounding chord.** A note the chord names takes the chord's own\n * spelling of that tone. This is the one place the line departs from\n * {@link spellPitchClass}, which never respells a tone of the key: over a\n * Db7 the seventh has to read Cb even in C major, where B is a scale tone,\n * because a dominant seventh spelled as a sixth is not the chord that was\n * played.\n * 2. **The key's scale.** Anything else the key itself spells — a scale tone,\n * and the tonic — keeps that spelling, exactly as the note-by-note path\n * keeps it. No amount of melodic pressure moves it.\n * 3. **The shape of the line, then its direction.** A chromatic note is spelled\n * so the letters of the intervals around it match the intervals actually\n * sounding, and a stepwise line then prefers sharps rising and flats\n * falling. Shape outranks direction because a rising line spelled with\n * sharps is still wrong if it spells a whole tone as a diminished third.\n * 4. **The key's chromatic convention, then the fewest accidentals.** What the\n * key alone would have said, as the tiebreak it is: the direction rule beats\n * it, which is why a chromatic ascent in C major spells C# D# G# A# rather\n * than the flats the key prefers, but one-sided evidence — a chromatic note\n * with only one stepwise neighbour — does not.\n */\n\n/** Emission cost for a spelling the chord sounding under the note does not name. */\nconst CHORD_MISMATCH = 64;\n\n/** Emission cost for respelling a tone the key's own scale already names. */\nconst SCALE_TONE_MISMATCH = 32;\n\n/** Transition cost per diatonic step an interval's letters miss its written size by. */\nconst LETTER_STEP_DEVIATION = 4;\n\n/**\n * How many steps of deviation are counted before the cost stops growing.\n *\n * Two letters out is already an interval no reading supports, and capping there\n * keeps the whole melodic term below {@link SCALE_TONE_MISMATCH}, so a badly\n * shaped line can never argue a scale tone out of its key spelling.\n */\nconst MAX_COUNTED_DEVIATION = 2;\n\n/** Transition cost for an accidental leaning against the direction of a stepwise line. */\nconst AGAINST_DIRECTION = 3;\n\n/** Emission cost for a chromatic spelling the key alone would not choose. */\nconst KEY_MISMATCH = 4;\n\n/** Emission cost per semitone of alteration: what minimises accidentals overall. */\nconst ACCIDENTAL = 1;\n\n/** Widest melodic interval still counted as a step, where the direction rule applies. */\nconst MAX_STEP = 2;\n\n/** Widest alteration a spelling may carry: a double accidental. */\nconst MAX_ALTER = 2;\n\n/**\n * Spellings each pitch class has within a double accidental.\n *\n * Always exactly three — a natural or its accidental, one letter above, one\n * letter below (C is C, B# and Dbb) — which is what bounds the work the dynamic\n * program is allowed to do.\n */\nconst SPELLINGS_PER_PITCH = 3;\n\n/**\n * The diatonic letter distances a melodic interval of a given size is\n * conventionally written with, indexed by its semitone class.\n *\n * Most sizes read only one way: a whole tone is a second, a minor third is a\n * third, so spelling one as anything else costs. Two sizes read two ways and\n * both are free — a semitone is either a chromatic inflection of one letter\n * (F to F#) or a diatonic step (E to F), and the tritone is either an augmented\n * fourth or a diminished fifth. Direction chooses between those.\n */\nconst CONVENTIONAL_LETTER_STEPS: readonly (readonly number[])[] = [\n [0], // unison\n [0, 1], // chromatic semitone or diatonic semitone\n [1], // second\n [2], // minor third\n [2], // major third\n [3], // fourth\n [3, 4], // augmented fourth or diminished fifth\n [4], // fifth\n [5], // minor sixth\n [5], // major sixth\n [6], // minor seventh\n [6], // major seventh\n];\n\n/**\n * Options for {@link spellLine}.\n *\n * @category Arrangement & Analysis\n */\nexport type SpellLineOptions = {\n /**\n * Tonic spelling to anchor the key, for a caller that already knows how the\n * piece is written. It must sound the key's own root pitch class; anything\n * else is rejected rather than spelled. Defaults to the conventional\n * spelling the key itself is written with. Accepted as a note name, a MIDI\n * number, note data, or a `Note`.\n */\n tonic?: NoteLike;\n /** Upper bound on the work the line may cost; defaults to the generation budget. */\n budget?: number;\n};\n\n/** Shortest signed alteration (in [-6, 6]) taking a letter's natural pc to `pc`. */\nfunction alterationFor(letter: number, pc: number): number {\n const distance = mod12(pc - naturalPc(letter));\n return distance > 6 ? distance - 12 : distance;\n}\n\n/** The octave that reproduces `pitch` from a letter and its alteration. */\nfunction octaveFor(pitch: number, letter: number, alter: number): number {\n return (pitch - naturalPc(letter) - alter) / 12 - 1;\n}\n\n/** Whether two spellings name the same letter with the same alteration. */\nfunction sameSpelling(a: Note, b: Note): boolean {\n return a.letter === b.letter && a.alter === b.alter;\n}\n\n/**\n * Every spelling of `pitch` within a double accidental, `preferred` first.\n *\n * The order is what settles a tie: two readings that cost the same are equally\n * defensible, and the one the theory layer would have chosen alone is then the\n * one that stands.\n */\nfunction candidatesFor(pitch: number, preferred: Note): Note[] {\n const pc = mod12(pitch);\n const rest: Note[] = [];\n for (let letter = 0; letter < 7; letter += 1) {\n const alter = alterationFor(letter, pc);\n if (Math.abs(alter) > MAX_ALTER || letter === preferred.letter) {\n continue;\n }\n rest.push({ letter, alter, octave: octaveFor(pitch, letter, alter) });\n }\n rest.sort((a, b) => Math.abs(a.alter) - Math.abs(b.alter) || a.letter - b.letter);\n return [\n {\n letter: preferred.letter,\n alter: preferred.alter,\n octave: octaveFor(pitch, preferred.letter, preferred.alter),\n },\n ...rest,\n ];\n}\n\n/**\n * The chord's own spelling of a pitch class, or undefined when the chord does\n * not sound it.\n *\n * The tones come from {@link spellChord}, so a chord carrying its own spellings\n * — a parsed symbol, an augmented sixth — is read by them; a slash bass outside\n * the chord's tones is spelled from its hint when it has one.\n */\nfunction chordSpellingOf(pc: number, chord: Chord, tonic: Note, key: KeyScale): Note | undefined {\n for (const tone of spellChord(chord, tonic, key)) {\n if (mod12(naturalPc(tone.letter) + tone.alter) === pc) {\n return tone;\n }\n }\n const bass = chord.bassPc;\n if (bass === undefined || mod12(bass) !== pc) {\n return undefined;\n }\n const hint = chord.bassSpelling;\n return hint !== undefined && mod12(naturalPc(hint.letter) + hint.alter) === pc\n ? { letter: mod7(hint.letter), alter: hint.alter }\n : spellPitchClass(pc, tonic, key);\n}\n\n/** One note of the line, with the spellings open to it and what each costs. */\ntype LineState = {\n pitch: number;\n /** True when the key does not already spell this pitch, so direction may decide. */\n chromatic: boolean;\n candidates: Note[];\n emissions: number[];\n /** The spelling the note-by-note path would have chosen, used as the fallback. */\n reference: Note;\n};\n\n/**\n * Weigh every spelling open to one note against what the chord and the key say\n * about it.\n *\n * The reference spelling is the theory layer's own answer — the chord's, when\n * the chord names the note, and the key's otherwise — and the cost of departing\n * from it is what ranks the three rules that do not depend on the neighbours.\n */\nfunction stateFor(pitch: number, chord: Chord | null, tonic: Note, key: KeyScale): LineState {\n const pc = mod12(pitch);\n const fromChord = chord === null ? undefined : chordSpellingOf(pc, chord, tonic, key);\n const scaleTone = isScaleTone(pc, key);\n const reference = fromChord ?? spellPitchClass(pc, tonic, key);\n const mismatch =\n fromChord !== undefined ? CHORD_MISMATCH : scaleTone ? SCALE_TONE_MISMATCH : KEY_MISMATCH;\n const candidates = candidatesFor(pitch, reference);\n return {\n pitch,\n chromatic: !scaleTone,\n candidates,\n emissions: candidates.map(\n (candidate) =>\n (sameSpelling(candidate, reference) ? 0 : mismatch) +\n ACCIDENTAL * Math.abs(candidate.alter),\n ),\n reference,\n };\n}\n\n/** A spelling's position on the diatonic ladder, octave included. */\nfunction diatonicIndex(note: Note): number {\n return note.letter + 7 * (note.octave ?? 0);\n}\n\n/**\n * What the letters of a melodic interval cost when they do not match the\n * interval that actually sounds.\n *\n * Both notes are already spelled, so the sounding interval and the written one\n * are both known: a whole tone written F# to Ab is two letters apart where a\n * second is one, and pays for the difference.\n */\nfunction letterStepCost(from: Note, to: Note, semitones: number): number {\n const written = diatonicIndex(to) - diatonicIndex(from);\n const size = Math.abs(semitones);\n const octaves = Math.floor(size / 12);\n const direction = semitones < 0 ? -1 : 1;\n let deviation = Number.POSITIVE_INFINITY;\n for (const steps of CONVENTIONAL_LETTER_STEPS[size % 12] ?? []) {\n deviation = Math.min(deviation, Math.abs(written - direction * (octaves * 7 + steps)));\n }\n if (!Number.isFinite(deviation)) {\n return 0;\n }\n return LETTER_STEP_DEVIATION * Math.min(deviation, MAX_COUNTED_DEVIATION);\n}\n\n/**\n * What an accidental costs for leaning the wrong way in a stepwise line: a\n * rising line is written with sharps and a falling one with flats.\n *\n * Only steps are judged. The rule describes chromatic motion, and a leap onto a\n * flat says nothing about the flat — the key and the chord have the whole story\n * there.\n */\nfunction directionCost(note: Note, chromatic: boolean, semitones: number): number {\n if (!chromatic || semitones === 0 || Math.abs(semitones) > MAX_STEP) {\n return 0;\n }\n const rising = semitones > 0;\n return (rising && note.alter < 0) || (!rising && note.alter > 0) ? AGAINST_DIRECTION : 0;\n}\n\n/**\n * The cost of following one spelling with another.\n *\n * Direction is charged to both notes: a chromatic note is judged by the step it\n * arrives on and by the step it leaves on, so a note inside a run collects the\n * rule twice and a note at the end of one collects it once.\n */\nfunction transitionCost(from: LineState, source: Note, to: LineState, target: Note): number {\n const semitones = to.pitch - from.pitch;\n return (\n letterStepCost(source, target, semitones) +\n directionCost(target, to.chromatic, semitones) +\n directionCost(source, from.chromatic, semitones)\n );\n}\n\n/**\n * Spell a whole melodic line at once, so its notes agree with each other.\n *\n * Every note is spelled together with the rest of the line rather than one at a\n * time: each pitch offers the three spellings a double accidental allows, the\n * chord and the key price them, the intervals between neighbours price the\n * pairs, and the cheapest reading of the whole line wins. Ties fall to the\n * spelling {@link spellPitchClass} would have chosen alone, so the answer never\n * drifts from the theory layer without a reason.\n *\n * Four rules decide, in this order:\n *\n * 1. A note the sounding chord names takes that chord's spelling of it, even\n * when the key spells the pitch otherwise — the seventh of a Db7 reads Cb in\n * C major, not B.\n * 2. Anything else the key's scale spells keeps its key spelling, exactly as\n * the note-by-note path keeps it.\n * 3. A chromatic note is spelled so the written intervals around it match the\n * sounding ones, and then so a rising step carries a sharp and a falling\n * step a flat.\n * 4. Failing all of that, the key's own chromatic spelling stands, and the\n * fewest accidentals win.\n *\n * Without a timeline — pass `null` — rules 2 to 4 still apply in full, so a\n * diatonic line and a chromatic run come out the same. What is lost is rule 1:\n * a chromatic chord can no longer name its own tones, so the tones of a\n * borrowed or altered chord fall back to the key's convention (that Cb becomes\n * B), and an enharmonically ambiguous tone under a chord that would have fixed\n * it is decided by the line's direction instead.\n *\n * @param notes The line, monophonic and in time order. Notes sharing an onset\n * are read as consecutive, so split polyphonic material into voices first.\n * @param timeline The chords sounding under the line, or null when only the key\n * is known. Each note is judged against the chord at its own onset.\n * @param key The key the line is written in, as a key name, a key/scale, or a\n * `Key`.\n * @param opts Optional tonic spelling and work budget.\n * @returns One spelled note per input note, in input order, each carrying the\n * octave that reproduces its pitch.\n * @throws If `opts.tonic` does not sound the key's root pitch class, if a note\n * event is malformed, or if the line is longer than the budget allows.\n * @example\n * ```ts\n * import { chordTimelineFromChords, majorKey, noteNames, spellLine } from '@libraz/libcantus';\n * // A chromatic ascent spells with sharps, whatever the key would say alone.\n * const rising = [60, 61, 62, 63, 64].map((pitch, index) => ({\n * pitch,\n * startBeat: index,\n * durationBeat: 1,\n * }));\n * noteNames(spellLine(rising, null, majorKey(0)));\n * // ['C4', 'C#4', 'D4', 'D#4', 'E4']\n * // Under a Db7 the same B sounds as the chord's seventh.\n * const timeline = chordTimelineFromChords([{ rootPc: 1, quality: 'dom7', startBeat: 0 }], 4);\n * noteNames(spellLine([{ pitch: 71, startBeat: 0, durationBeat: 1 }], timeline, majorKey(0)));\n * // ['Cb5']\n * ```\n * @category Arrangement & Analysis\n */\nexport function spellLine(\n notes: readonly NoteEvent[],\n timeline: ChordTimeline | null,\n key: SpelledKeyLike,\n opts: SpellLineOptions = {},\n): Note[] {\n assertOptions(opts, 'opts');\n // Read whole rather than reduced: a line in Ab minor is written on flats, and\n // a key handed in spelled that way would otherwise have its spelling derived\n // back from the pitch classes and come out in G# minor.\n const resolved = resolveKey(key);\n const scale = resolved.scale;\n const given = opts.tonic === undefined ? undefined : toNoteData(opts.tonic);\n assertNoteEvents(notes, 'line notes', {\n allowNonPositiveDuration: true,\n budget: opts.budget,\n });\n // The dynamic program compares every spelling of a note with every spelling\n // of the one before it, so the work is bounded before any of it is done.\n assertGenerationBudget(\n notes.length * SPELLINGS_PER_PITCH * SPELLINGS_PER_PITCH,\n 'line spelling states',\n opts.budget,\n );\n if (given !== undefined) {\n assertTonicOf(given, scale, 'spellLine');\n }\n if (notes.length === 0) {\n return [];\n }\n const tonic = given ?? resolved.tonic;\n const states = notes.map((note) =>\n stateFor(note.pitch, timeline?.at(note.startBeat) ?? null, tonic, scale),\n );\n\n // Viterbi across the line: `costs` holds the cheapest path reaching each\n // spelling of the current note, and `back` remembers which spelling of the\n // previous note that path came through.\n let costs: number[] = [];\n let previous: LineState | undefined;\n const back: number[][] = [];\n for (const current of states) {\n if (previous === undefined) {\n costs = [...current.emissions];\n back.push(current.candidates.map(() => 0));\n previous = current;\n continue;\n }\n const reached: number[] = [];\n const chosen: number[] = [];\n for (const [target, candidate] of current.candidates.entries()) {\n let best = Number.POSITIVE_INFINITY;\n let bestSource = 0;\n for (const [source, sourceNote] of previous.candidates.entries()) {\n const total =\n (costs[source] ?? Number.POSITIVE_INFINITY) +\n transitionCost(previous, sourceNote, current, candidate);\n if (total < best) {\n best = total;\n bestSource = source;\n }\n }\n reached.push(best + (current.emissions[target] ?? 0));\n chosen.push(bestSource);\n }\n costs = reached;\n back.push(chosen);\n previous = current;\n }\n\n let cursor = 0;\n for (const [index, cost] of costs.entries()) {\n if (cost < (costs[cursor] ?? Number.POSITIVE_INFINITY)) {\n cursor = index;\n }\n }\n const picked: number[] = new Array(states.length).fill(0);\n for (let index = states.length - 1; index >= 0; index -= 1) {\n picked[index] = cursor;\n cursor = back[index]?.[cursor] ?? 0;\n }\n return states.map((state, index) => state.candidates[picked[index] ?? 0] ?? state.reference);\n}\n"]}