@libraz/libcantus 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +4 -0
  2. package/README_ja.md +4 -0
  3. package/dist/analyze/index.cjs +31 -7
  4. package/dist/analyze/index.cjs.map +1 -1
  5. package/dist/analyze/index.d.cts +155 -9
  6. package/dist/analyze/index.d.ts +155 -9
  7. package/dist/analyze/index.js +37 -13
  8. package/dist/{chunk-TT6PKMPR.cjs → chunk-35NBV2FT.cjs} +16 -16
  9. package/dist/{chunk-TT6PKMPR.cjs.map → chunk-35NBV2FT.cjs.map} +1 -1
  10. package/dist/{chunk-YPB3A3EY.cjs → chunk-3R3QVKY5.cjs} +314 -285
  11. package/dist/chunk-3R3QVKY5.cjs.map +1 -0
  12. package/dist/{chunk-GDKE6RQW.cjs → chunk-4R2LWV7E.cjs} +691 -413
  13. package/dist/chunk-4R2LWV7E.cjs.map +1 -0
  14. package/dist/{chunk-RVS4G4MR.cjs → chunk-76ZBTRG2.cjs} +198 -198
  15. package/dist/{chunk-RVS4G4MR.cjs.map → chunk-76ZBTRG2.cjs.map} +1 -1
  16. package/dist/{chunk-X6RJAHII.js → chunk-7KG3RTYS.js} +2 -2
  17. package/dist/{chunk-PQL7E5F6.js → chunk-AGOVW7U6.js} +1 -1
  18. package/dist/{chunk-PQL7E5F6.js.map → chunk-AGOVW7U6.js.map} +1 -1
  19. package/dist/{chunk-5KW7DNCJ.cjs → chunk-B2PW6EHN.cjs} +2423 -453
  20. package/dist/chunk-B2PW6EHN.cjs.map +1 -0
  21. package/dist/{chunk-J772DL3B.cjs → chunk-CGBADIO5.cjs} +53 -53
  22. package/dist/{chunk-J772DL3B.cjs.map → chunk-CGBADIO5.cjs.map} +1 -1
  23. package/dist/{chunk-R5VUXQMP.js → chunk-CPMB7P75.js} +1978 -8
  24. package/dist/chunk-CPMB7P75.js.map +1 -0
  25. package/dist/{chunk-VLE5R7PF.js → chunk-GNVN4PPJ.js} +1874 -8
  26. package/dist/chunk-GNVN4PPJ.js.map +1 -0
  27. package/dist/{chunk-Q6R3QAZD.cjs → chunk-IFZDWIKM.cjs} +2199 -333
  28. package/dist/chunk-IFZDWIKM.cjs.map +1 -0
  29. package/dist/{chunk-DKICR7AN.js → chunk-IPEF6NWB.js} +296 -18
  30. package/dist/chunk-IPEF6NWB.js.map +1 -0
  31. package/dist/{chunk-AWEMD747.js → chunk-M5RYMAFI.js} +3 -3
  32. package/dist/{chunk-HSY2VQXZ.js → chunk-MLBPXA4Z.js} +330 -728
  33. package/dist/chunk-MLBPXA4Z.js.map +1 -0
  34. package/dist/{chunk-H66ARFU5.js → chunk-NB25P3EH.js} +3 -3
  35. package/dist/{chunk-5JJ6EHEZ.cjs → chunk-NM7UAMC3.cjs} +62 -62
  36. package/dist/{chunk-5JJ6EHEZ.cjs.map → chunk-NM7UAMC3.cjs.map} +1 -1
  37. package/dist/{chunk-R7B5N2JK.cjs → chunk-PM4XJE2Y.cjs} +47 -47
  38. package/dist/{chunk-R7B5N2JK.cjs.map → chunk-PM4XJE2Y.cjs.map} +1 -1
  39. package/dist/{chunk-7GOKQJTQ.js → chunk-PUKJP2Z5.js} +2 -2
  40. package/dist/{chunk-QCXYKSZS.cjs → chunk-PWC4CRWT.cjs} +19 -19
  41. package/dist/{chunk-QCXYKSZS.cjs.map → chunk-PWC4CRWT.cjs.map} +1 -1
  42. package/dist/{chunk-FPBZFNCD.js → chunk-RFIBF2RR.js} +47 -18
  43. package/dist/{chunk-FPBZFNCD.js.map → chunk-RFIBF2RR.js.map} +1 -1
  44. package/dist/{chunk-XKSO2V5I.cjs → chunk-S5BKZZGM.cjs} +1 -1
  45. package/dist/{chunk-XKSO2V5I.cjs.map → chunk-S5BKZZGM.cjs.map} +1 -1
  46. package/dist/{chunk-HOHFYGKW.js → chunk-UMWZ4ZET.js} +2 -2
  47. package/dist/{chunk-XVMJO3QF.cjs → chunk-YFA7X2F6.cjs} +489 -887
  48. package/dist/chunk-YFA7X2F6.cjs.map +1 -0
  49. package/dist/{chunk-57WB2EDF.js → chunk-ZBUROTJQ.js} +3 -3
  50. package/dist/{coerce-wwdELNYS.d.ts → coerce-DP0vYu-x.d.ts} +77 -1
  51. package/dist/{coerce-CJlgULrQ.d.cts → coerce-DRERzdve.d.cts} +77 -1
  52. package/dist/core/index.cjs +5 -5
  53. package/dist/core/index.d.cts +2 -2
  54. package/dist/core/index.d.ts +2 -2
  55. package/dist/core/index.js +4 -4
  56. package/dist/generate/index.cjs +20 -8
  57. package/dist/generate/index.cjs.map +1 -1
  58. package/dist/generate/index.d.cts +87 -9
  59. package/dist/generate/index.d.ts +87 -9
  60. package/dist/generate/index.js +19 -7
  61. package/dist/{index-BaoKVVhE.d.cts → index-B9QLziDe.d.cts} +2 -2
  62. package/dist/{index-CnHQgqNh.d.ts → index-BHIUyjhZ.d.ts} +2 -2
  63. package/dist/{index-CBCP84rv.d.cts → index-BL5N2Oyz.d.cts} +1 -1
  64. package/dist/{index-D_hzxD8w.d.cts → index-BMweUlm5.d.cts} +3 -0
  65. package/dist/{index-Ci3xyows.d.ts → index-CL4b4gUQ.d.ts} +3 -0
  66. package/dist/{index-BzLNkmA7.d.cts → index-CY-3NVfy.d.cts} +486 -5
  67. package/dist/{index-CnIdP4EZ.d.ts → index-Dw2DP7vm.d.ts} +1 -1
  68. package/dist/{index-DpC-mDlV.d.ts → index-zaZdmnU4.d.ts} +486 -5
  69. package/dist/index.cjs +49 -13
  70. package/dist/index.cjs.map +1 -1
  71. package/dist/index.d.cts +10 -10
  72. package/dist/index.d.ts +10 -10
  73. package/dist/index.js +55 -19
  74. package/dist/{masks-O73Rhj5d.d.ts → masks-Cc7-MzzY.d.ts} +1 -1
  75. package/dist/{masks-C8TWLFIi.d.cts → masks-pADfTOiD.d.cts} +1 -1
  76. package/dist/model/index.cjs +12 -12
  77. package/dist/model/index.d.cts +213 -16
  78. package/dist/model/index.d.ts +213 -16
  79. package/dist/model/index.js +11 -11
  80. package/dist/{index-DTsKaPaC.d.cts → profile-CQMEqdH9.d.cts} +88 -601
  81. package/dist/{index-BUtIf2go.d.ts → profile-DLdlIpS9.d.ts} +88 -601
  82. package/dist/theory/index.cjs +5 -5
  83. package/dist/theory/index.d.cts +7 -7
  84. package/dist/theory/index.d.ts +7 -7
  85. package/dist/theory/index.js +4 -4
  86. package/dist/{index-BpWOhkkE.d.ts → types-D-XgxGvp.d.cts} +1170 -14
  87. package/dist/{index-B3WCj_Hm.d.cts → types-Dh7C064C.d.ts} +1170 -14
  88. package/package.json +6 -5
  89. package/dist/chunk-5KW7DNCJ.cjs.map +0 -1
  90. package/dist/chunk-DKICR7AN.js.map +0 -1
  91. package/dist/chunk-GDKE6RQW.cjs.map +0 -1
  92. package/dist/chunk-HSY2VQXZ.js.map +0 -1
  93. package/dist/chunk-Q6R3QAZD.cjs.map +0 -1
  94. package/dist/chunk-R5VUXQMP.js.map +0 -1
  95. package/dist/chunk-VLE5R7PF.js.map +0 -1
  96. package/dist/chunk-XVMJO3QF.cjs.map +0 -1
  97. package/dist/chunk-YPB3A3EY.cjs.map +0 -1
  98. /package/dist/{chunk-X6RJAHII.js.map → chunk-7KG3RTYS.js.map} +0 -0
  99. /package/dist/{chunk-AWEMD747.js.map → chunk-M5RYMAFI.js.map} +0 -0
  100. /package/dist/{chunk-H66ARFU5.js.map → chunk-NB25P3EH.js.map} +0 -0
  101. /package/dist/{chunk-7GOKQJTQ.js.map → chunk-PUKJP2Z5.js.map} +0 -0
  102. /package/dist/{chunk-HOHFYGKW.js.map → chunk-UMWZ4ZET.js.map} +0 -0
  103. /package/dist/{chunk-57WB2EDF.js.map → chunk-ZBUROTJQ.js.map} +0 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/runner/work/libcantus/libcantus/dist/chunk-RVS4G4MR.cjs","../src/theory/transposition/index.ts","../src/theory/figured-bass/index.ts","../src/theory/partwriting/cross-relation.ts","../src/theory/voicing/tendency.ts","../src/theory/voicing/internal.ts","../src/theory/voicing/satb.ts","../src/theory/voicing/leading.ts","../src/theory/voicing/styled.ts","../src/theory/partwriting/internal.ts","../src/theory/partwriting/species.ts","../src/theory/partwriting/index.ts"],"names":[],"mappings":"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","file":"/home/runner/work/libcantus/libcantus/dist/chunk-RVS4G4MR.cjs","sourcesContent":[null,"/**\n * Transposing instruments: the fixed interval between the pitch a player reads\n * and the pitch the instrument sounds.\n *\n * The distance is a spelled interval, never a semitone count. A clarinet in A\n * sounds a minor third below what it reads, so a written C sounds A, a written\n * D sounds B, and a written D# sounds B#: the letter always moves by the\n * interval's diatonic number, where a bare count of three semitones would take\n * the last of those to C natural and lose the letter the part is written on.\n *\n * The octave is part of that interval rather than folded away into a pitch\n * class, so instruments pitched in the same key stay apart: the alto saxophone\n * sounds a major sixth lower, the baritone saxophone a major thirteenth (the\n * same key an octave further down), and the E flat clarinet a minor third\n * higher. The pure octave transposers — piccolo, double bass, contrabassoon,\n * guitar — are a perfect octave, a unison-class interval displaced by one\n * octave.\n */\n\nimport { InvalidInputError } from '../../core/errors/index.js';\nimport type { IntervalLike, Note, NoteLike, SpelledInterval } from '../../core/pitch/index.js';\nimport {\n parseInterval,\n toNoteData,\n toSpelledInterval,\n transposeByInterval,\n} from '../../core/pitch/index.js';\n\n/**\n * The interval from a written pitch to the pitch the instrument sounds, one\n * entry per built-in instrument.\n *\n * Each value is an interval name as {@link parseInterval} reads it, in the\n * written-to-sounding direction: a leading `'-'` marks an instrument that\n * sounds below what it reads, which is most of them. A transposition wider than\n * an octave is named by its compound interval rather than reduced, because the\n * octave is exactly what separates the tenor saxophone from the soprano and the\n * baritone from the alto.\n *\n * @example\n * ```ts\n * import { TRANSPOSING_INSTRUMENTS } from '@libraz/libcantus';\n * TRANSPOSING_INSTRUMENTS.clarinetA; // '-m3' — sounds a minor third lower\n * TRANSPOSING_INSTRUMENTS.piccolo; // 'P8' — sounds an octave higher\n * ```\n * @category Pitch & Intervals\n */\nexport const TRANSPOSING_INSTRUMENTS = Object.freeze({\n // In B flat: a written C sounds B flat. The lower members of the family read\n // in the same key an octave higher, so their interval is the compound one.\n clarinetBb: '-M2',\n trumpetBb: '-M2',\n sopranoSax: '-M2',\n tenorSax: '-M9',\n bassClarinet: '-M9',\n // In A: a written C sounds A.\n clarinetA: '-m3',\n // In F: a written C sounds F.\n hornF: '-P5',\n englishHorn: '-P5',\n // In E flat: one key, three registers. The sopranino clarinet sounds above\n // what it reads, the alto saxophone a major sixth below, and the baritone\n // saxophone an octave below that.\n clarinetEb: 'm3',\n altoSax: '-M6',\n baritoneSax: '-M13',\n // In D: a written C sounds D.\n trumpetD: 'M2',\n // Octave transposers: the instrument is in C, only the register moves.\n piccolo: 'P8',\n doubleBass: '-P8',\n contrabassoon: '-P8',\n guitar: '-P8',\n});\n\n/**\n * Built-in profile names whose transposition is another entry's.\n *\n * An instrument profile names its tuning where the table names the instrument:\n * `'guitar (drop D)'` is a guitar with one string lowered, and retuning a\n * string does not move the interval between the part and the pitch it sounds.\n * The alias resolves to the instrument so that every built-in profile answers\n * from its own name, rather than one of them alone needing a transposition\n * spelled out.\n */\nconst PROFILE_NAME_ALIASES: Readonly<Record<string, TransposingInstrumentName>> = Object.freeze({\n 'guitar (drop D)': 'guitar',\n});\n\n/**\n * The name of a built-in transposing instrument, as\n * {@link TRANSPOSING_INSTRUMENTS} keys them.\n *\n * @example\n * ```ts\n * import type { TransposingInstrumentName } from '@libraz/libcantus';\n * const part: TransposingInstrumentName = 'clarinetA';\n * ```\n * @category Pitch & Intervals\n */\nexport type TransposingInstrumentName = keyof typeof TRANSPOSING_INSTRUMENTS;\n\n/**\n * A transposing instrument: one of the built-in names, or a spelled interval\n * for an instrument the table does not carry.\n *\n * An interval is read in the same written-to-sounding direction as the table,\n * so `'-M2'` is any instrument in B flat and `'P8'` any that sounds an octave\n * up. The names complete in an editor while any other string is still accepted,\n * which is what lets an instrument read from configuration go straight in.\n *\n * @example\n * ```ts\n * import type { TransposingInstrument } from '@libraz/libcantus';\n * const clarinet: TransposingInstrument = 'clarinetA';\n * const altoFlute: TransposingInstrument = '-P4';\n * ```\n * @category Pitch & Intervals\n */\nexport type TransposingInstrument =\n | TransposingInstrumentName\n | (string & {})\n | Exclude<IntervalLike, string>;\n\n/**\n * The interval a transposing instrument sounds at, measured from what it reads.\n *\n * This is the table's own direction: the answer descends for an instrument that\n * sounds below the printed part, which is most of them. Applying it to a\n * written pitch is {@link toSoundingPitch}, and applying it to a concert key is\n * how a written key is read back as the key it sounds in.\n *\n * @param instrument A built-in instrument name, or an interval naming a\n * transposition the table does not carry.\n * @returns The written-to-sounding interval.\n * @throws If the value is neither a known instrument nor a spelled interval.\n * @example\n * ```ts\n * import { instrumentTransposition } from '@libraz/libcantus';\n * instrumentTransposition('clarinetA');\n * // { number: 3, quality: 'm', semitones: -3, descending: true }\n * instrumentTransposition('piccolo');\n * // { number: 8, quality: 'P', semitones: 12, descending: false }\n * ```\n * @category Pitch & Intervals\n */\nexport function instrumentTransposition(instrument: TransposingInstrument): SpelledInterval {\n if (typeof instrument !== 'string') {\n return toSpelledInterval(instrument);\n }\n // Looked up through `hasOwn` so a name such as `'constructor'` cannot resolve\n // to something inherited from `Object.prototype`.\n const name = Object.hasOwn(PROFILE_NAME_ALIASES, instrument)\n ? (PROFILE_NAME_ALIASES[instrument] as TransposingInstrumentName)\n : instrument;\n if (Object.hasOwn(TRANSPOSING_INSTRUMENTS, name)) {\n return parseInterval(TRANSPOSING_INSTRUMENTS[name as TransposingInstrumentName]);\n }\n try {\n return parseInterval(instrument);\n } catch (error) {\n if (error instanceof InvalidInputError) {\n // A mistyped instrument name is the likely mistake, so the message names\n // that possibility rather than only the interval grammar it also failed.\n throw new InvalidInputError(\n `unknown transposing instrument ${JSON.stringify(instrument)}; ` +\n \"pass a name from TRANSPOSING_INSTRUMENTS or an interval such as '-M2'\",\n );\n }\n throw error;\n }\n}\n\n/**\n * The same interval taken the other way, which is what makes the two\n * conversions exact inverses.\n *\n * The direction is flipped rather than read back off the sign, because a unison\n * spans zero semitones in both directions and the sign alone cannot say which\n * way a custom `'-P1'` was meant to go.\n */\nfunction reversedInterval(step: SpelledInterval): SpelledInterval {\n return {\n number: step.number,\n quality: step.quality,\n // Negating a zero span yields -0, which compares unequal to 0 under Object.is.\n semitones: step.semitones === 0 ? 0 : -step.semitones,\n descending: !step.descending,\n };\n}\n\n/**\n * The pitch an instrument sounds for a written note.\n *\n * The interval decides the letter, so the spelling of the part survives the\n * conversion: on a clarinet in A a written C sounds A, a written D sounds B,\n * and a written D# sounds B# — a semitone count alone could answer C natural\n * for that last one and lose the letter the part is written on. A note carrying\n * an octave moves register with it — a written C4 sounds A3 — while an\n * octave-less note stays octave-less, so a pure octave transposer leaves it\n * alone.\n *\n * @param note The written note, as the player reads it: a note name, a MIDI\n * number, note data, or a `Note`.\n * @param instrument A built-in instrument name, or an interval naming a\n * transposition the table does not carry.\n * @returns The sounding note, at concert pitch.\n * @throws If the note is malformed, or the instrument is neither a known name\n * nor a spelled interval.\n * @example\n * ```ts\n * import { formatNote, parseNote, toSoundingPitch } from '@libraz/libcantus';\n * formatNote(toSoundingPitch(parseNote('C4'), 'clarinetA')); // 'A3'\n * formatNote(toSoundingPitch('D#4', 'clarinetA')); // 'B#3'\n * formatNote(toSoundingPitch(parseNote('C4'), 'piccolo')); // 'C5'\n * ```\n * @category Pitch & Intervals\n */\nexport function toSoundingPitch(note: NoteLike, instrument: TransposingInstrument): Note {\n return transposeByInterval(toNoteData(note), instrumentTransposition(instrument));\n}\n\n/**\n * The note an instrument must read to sound a given concert pitch.\n *\n * The exact inverse of {@link toSoundingPitch} for every instrument: the same\n * interval applied the other way, so the letter and the octave both come back.\n * A concert A3 is written C4 for a clarinet in A, and a concert E flat 3 is\n * written C4 for an alto saxophone but C5 for a baritone saxophone, which reads\n * the same key an octave lower.\n *\n * @param note The sounding note, at concert pitch: a note name, a MIDI number,\n * note data, or a `Note`.\n * @param instrument A built-in instrument name, or an interval naming a\n * transposition the table does not carry.\n * @returns The written note, as the player reads it.\n * @throws If the note is malformed, or the instrument is neither a known name\n * nor a spelled interval.\n * @example\n * ```ts\n * import { formatNote, parseNote, toWrittenPitch } from '@libraz/libcantus';\n * formatNote(toWrittenPitch(parseNote('A3'), 'clarinetA')); // 'C4'\n * formatNote(toWrittenPitch('Eb3', 'altoSax')); // 'C4'\n * formatNote(toWrittenPitch(parseNote('Eb3'), 'baritoneSax')); // 'C5'\n * ```\n * @category Pitch & Intervals\n */\nexport function toWrittenPitch(note: NoteLike, instrument: TransposingInstrument): Note {\n return transposeByInterval(\n toNoteData(note),\n reversedInterval(instrumentTransposition(instrument)),\n );\n}\n","/**\n * Figured bass: read a bass note and its figures as the chord they name.\n *\n * The figures are intervals above the bass, and every interval a figure does\n * not alter is the one the key's own scale gives. That is the whole rule of the\n * notation, and the reason a realization needs the key: the same `6` names a\n * major triad over one degree of C major and a diminished one over another.\n *\n * The bass arrives spelled, so the intervals above it are spelled too. The\n * third above a G in C minor is the Bb the signature gives, and the accidental\n * that changes it is the one a score prints in front of that note: the leading\n * tone there is a B natural, so the figure is `n`. That is the whole convention\n * for accidentals here — a sign on a figure names its note exactly as it does on\n * the staff — and it is what makes the figures of a real exercise readable as\n * written. The crossed or slashed figure, typed `+`, is the one sign that raises\n * instead of naming.\n */\n\nimport { InvalidInputError, NoSolutionError } from '../../core/errors/index.js';\nimport type { Note, NoteLike } from '../../core/pitch/index.js';\nimport {\n diatonicLetterOf as mod7,\n pitchClassOf as mod12,\n noteToPitchClass,\n toNoteData,\n} from '../../core/pitch/index.js';\nimport { describeRejected } from '../../core/validation/index.js';\nimport type { Chord, ChordQuality } from '../chord/index.js';\nimport { chordQualities, makeChord } from '../chord/index.js';\nimport {\n type KeyLike,\n type ResolvedKey,\n resolveKey,\n type SpelledKeyLike,\n scaleTonesInDegreeOrder,\n} from '../scale/index.js';\nimport { noteNames, spellChord, spellScale } from '../spelling/index.js';\nimport { type ChordLike, toChordData } from '../symbol/index.js';\n\n/** Smallest interval a figure may name above the bass: a second. */\nconst MIN_FIGURE_NUMBER = 2;\n\n/** Largest interval a figure may name above the bass: a ninth. */\nconst MAX_FIGURE_NUMBER = 9;\n\n/** The octave figure, which names the bass itself and so adds no chord tone. */\nconst OCTAVE_FIGURE = 8;\n\n/** Number of tones in the largest stack a figure can name: a seventh chord. */\nconst MAX_STACK_SIZE = 4;\n\n/** The accidental written on a figure, or the absence of one. */\ntype FigureAccidental =\n | 'none'\n | 'sharp'\n | 'doubleSharp'\n | 'flat'\n | 'doubleFlat'\n | 'natural'\n | 'raised';\n\n/** One written figure: an interval above the bass and the accidental on it. */\ntype Figure = { number: number; accidental: FigureAccidental };\n\n/** A figure that moves to another over the same bass, such as `4-3`. */\ntype FigureMotion = { from: Figure; to: Figure };\n\n/** The figures a written string names: the static ones and the moving ones. */\ntype ParsedFigures = { figures: Figure[]; motions: FigureMotion[] };\n\n/** Two-character accidental prefixes, matched before the single-character ones. */\nconst DOUBLE_ACCIDENTALS: Record<string, FigureAccidental> = {\n '##': 'doubleSharp',\n '♯♯': 'doubleSharp',\n bb: 'doubleFlat',\n '♭♭': 'doubleFlat',\n};\n\n/**\n * Single-character accidental prefixes. `+` is the typed form of the slashed or\n * crossed figure, which is the one sign that raises rather than names.\n */\nconst SINGLE_ACCIDENTALS: Record<string, FigureAccidental> = {\n '#': 'sharp',\n '♯': 'sharp',\n '+': 'raised',\n b: 'flat',\n '♭': 'flat',\n n: 'natural',\n '♮': 'natural',\n};\n\n/** Characters that merely separate stacked figures, as the slash in `6/4` does. */\nconst FIGURE_SEPARATORS = new Set([' ', '\\t', ',', '/']);\n\n/** Characters that link a figure to the figure it moves to, as in `4-3`. */\nconst MOTION_MARKS = new Set(['-', '–']);\n\n/**\n * The one convention the accidentals of this notation are read and written\n * under: an accidental on a figure is the accidental a score prints in front of\n * that note, exactly as it is on the staff.\n *\n * Each entry pairs the sign with the alteration it names, and both directions\n * are built from it, so the reader and the writer cannot come to mean different\n * things. Under any other reading the natural sign has nothing to say — `♮`\n * restores a note to natural, which is a statement about the note and not about\n * a distance from the key — and the slashed or crossed figure, whose whole\n * function is to raise, would be a second spelling of `♯`.\n */\nconst PRINTED_ACCIDENTALS: readonly (readonly [FigureAccidental, string, number])[] = [\n ['natural', 'n', 0],\n ['sharp', '#', 1],\n ['doubleSharp', '##', 2],\n ['flat', 'b', -1],\n ['doubleFlat', 'bb', -2],\n];\n\n/** The alteration each printed accidental names. */\nconst PRINTED_ALTERS = new Map<FigureAccidental, number>(\n PRINTED_ACCIDENTALS.map(([accidental, , alter]) => [accidental, alter]),\n);\n\n/** The sign a score prints for each alteration: the inverse of the above. */\nconst ACCIDENTAL_GLYPHS = new Map<number, string>(\n PRINTED_ACCIDENTALS.map(([, glyph, alter]) => [alter, glyph]),\n);\n\n/** An error naming the figure string that was rejected. */\nfunction invalidFigures(figures: string, detail: string): InvalidInputError {\n return new InvalidInputError(`figured bass ${describeRejected(figures)} ${detail}`);\n}\n\n/**\n * The alteration a figure's accidental gives the note it stands on, against the\n * alteration the key's signature gives that letter.\n *\n * A printed accidental names the note outright: `#` over a bass in a key whose\n * signature flattens that letter names the sharp note, not the natural one the\n * signature would be raised to. Two signs read the key instead — the absence of\n * an accidental, which takes what the key gives, and the crossed figure, which\n * raises it a semitone and is the only sign that means \"raise\".\n */\nfunction alteredBy(diatonicAlter: number, accidental: FigureAccidental): number {\n const printed = PRINTED_ALTERS.get(accidental);\n if (printed !== undefined) {\n return printed;\n }\n return accidental === 'raised' ? diatonicAlter + 1 : diatonicAlter;\n}\n\n/**\n * Read one figure — an optional accidental and an optional digit — starting at\n * `start`, and report where the figure ends.\n *\n * An accidental with no digit alters the third above the bass, which is what a\n * lone `#` under a bass note means.\n */\nfunction readFigure(text: string, start: number): { figure: Figure; index: number } {\n let index = start;\n let accidental: FigureAccidental = 'none';\n const pair = DOUBLE_ACCIDENTALS[text.slice(index, index + 2)];\n const single = SINGLE_ACCIDENTALS[text[index] ?? ''];\n if (pair !== undefined) {\n accidental = pair;\n index += 2;\n } else if (single !== undefined) {\n accidental = single;\n index += 1;\n }\n const digit = text[index] ?? '';\n if (digit >= '0' && digit <= '9') {\n const number = Number(digit);\n if (number < MIN_FIGURE_NUMBER || number > MAX_FIGURE_NUMBER) {\n throw invalidFigures(\n text,\n `names the interval ${number}, which is outside ${MIN_FIGURE_NUMBER}..${MAX_FIGURE_NUMBER}`,\n );\n }\n return { figure: { number, accidental }, index: index + 1 };\n }\n if (accidental === 'none') {\n // An empty slice means the string ended where a figure was expected, as in\n // a trailing `4-`; naming the position is more use than naming nothing.\n const found = text.slice(start, start + 1);\n throw invalidFigures(\n text,\n found === ''\n ? `ends where a figure was expected, at position ${start}`\n : `has an unexpected character ${JSON.stringify(found)}`,\n );\n }\n return { figure: { number: 3, accidental }, index };\n}\n\n/** Split a figure string into its static figures and its moving ones. */\nfunction parseFigures(text: string): ParsedFigures {\n const figures: Figure[] = [];\n const motions: FigureMotion[] = [];\n let index = 0;\n while (index < text.length) {\n if (FIGURE_SEPARATORS.has(text[index] ?? '')) {\n index += 1;\n continue;\n }\n const first = readFigure(text, index);\n index = first.index;\n if (!MOTION_MARKS.has(text[index] ?? '')) {\n figures.push(first.figure);\n continue;\n }\n const second = readFigure(text, index + 1);\n index = second.index;\n if (MOTION_MARKS.has(text[index] ?? '')) {\n throw invalidFigures(text, 'chains more than one motion onto a single figure');\n }\n if (first.figure.number === second.figure.number) {\n throw invalidFigures(text, `moves the interval ${first.figure.number} onto itself`);\n }\n motions.push({ from: first.figure, to: second.figure });\n }\n return { figures, motions };\n}\n\n/** What a figure set sounds, and where the chord root sits above the bass. */\ntype FiguredSonority = { intervals: readonly number[]; rootInterval: number };\n\n/** The root-position triad, which every unfigured bass note carries. */\nconst TRIAD: FiguredSonority = { intervals: [3, 5], rootInterval: 1 };\n\n/** The first-inversion triad: the bass is the third of the chord. */\nconst TRIAD_FIRST: FiguredSonority = { intervals: [3, 6], rootInterval: 6 };\n\n/** The second-inversion triad: the bass is the fifth of the chord. */\nconst TRIAD_SECOND: FiguredSonority = { intervals: [4, 6], rootInterval: 4 };\n\n/** The root-position seventh chord. */\nconst SEVENTH: FiguredSonority = { intervals: [3, 5, 7], rootInterval: 1 };\n\n/** The first-inversion seventh chord, written `6/5`. */\nconst SEVENTH_FIRST: FiguredSonority = { intervals: [3, 5, 6], rootInterval: 6 };\n\n/** The second-inversion seventh chord, written `4/3`. */\nconst SEVENTH_SECOND: FiguredSonority = { intervals: [3, 4, 6], rootInterval: 4 };\n\n/** The third-inversion seventh chord, written `4/2` or `2`. */\nconst SEVENTH_THIRD: FiguredSonority = { intervals: [2, 4, 6], rootInterval: 2 };\n\n/**\n * Every figure set the grammar recognizes, keyed by the intervals actually\n * written in ascending order. Both the abbreviation a musician writes and the\n * full stack it stands for are listed, so `6` and `63` name one sonority and\n * `2`, `42` and `642` another.\n */\nconst FIGURE_SETS: ReadonlyMap<string, FiguredSonority> = new Map([\n ['', TRIAD],\n ['3', TRIAD],\n ['5', TRIAD],\n ['3,5', TRIAD],\n ['6', TRIAD_FIRST],\n ['3,6', TRIAD_FIRST],\n ['4,6', TRIAD_SECOND],\n ['7', SEVENTH],\n ['3,7', SEVENTH],\n ['5,7', SEVENTH],\n ['3,5,7', SEVENTH],\n ['5,6', SEVENTH_FIRST],\n ['3,5,6', SEVENTH_FIRST],\n ['3,4', SEVENTH_SECOND],\n ['3,4,6', SEVENTH_SECOND],\n ['2', SEVENTH_THIRD],\n ['2,4', SEVENTH_THIRD],\n ['2,4,6', SEVENTH_THIRD],\n]);\n\n/**\n * The digits a realized sonority is conventionally written with, keyed by its\n * full interval stack. The inverse of the abbreviations in {@link FIGURE_SETS},\n * and the reason {@link figuredBassOf} emits `6` rather than `63`.\n */\nconst FIGURE_ABBREVIATIONS: ReadonlyMap<string, readonly number[]> = new Map([\n [TRIAD.intervals.join(','), []],\n [TRIAD_FIRST.intervals.join(','), [6]],\n [TRIAD_SECOND.intervals.join(','), [6, 4]],\n [SEVENTH.intervals.join(','), [7]],\n [SEVENTH_FIRST.intervals.join(','), [6, 5]],\n [SEVENTH_SECOND.intervals.join(','), [4, 3]],\n [SEVENTH_THIRD.intervals.join(','), [4, 2]],\n]);\n\n/**\n * Index the chord vocabulary by interval template, for the triads and seventh\n * chords a figure can stack. Reading the templates back from the chord builder\n * keeps the two in step: a quality added to the vocabulary becomes reachable\n * from a figure without a second table to maintain.\n */\nfunction tertianQualityIndex(): ReadonlyMap<string, ChordQuality> {\n const index = new Map<string, ChordQuality>();\n for (const quality of chordQualities()) {\n const { intervals } = makeChord(0, quality);\n const template = intervals.join(',');\n // Declaration order decides a tie, exactly as it does for chord detection.\n if (intervals.length >= 3 && intervals.length <= MAX_STACK_SIZE && !index.has(template)) {\n index.set(template, quality);\n }\n }\n return index;\n}\n\nconst TERTIAN_QUALITIES = tertianQualityIndex();\n\n/**\n * The alteration each diatonic letter carries in a key, indexed by letter.\n *\n * This one lookup is what makes an unfigured interval diatonic: the letter of\n * an interval above the bass comes from counting letters, and its accidental\n * comes from the key's own spelling of that letter, so a flat-side key hands\n * back its flats and a harmonic minor its raised leading tone.\n */\nfunction keyLetterAlters(key: ResolvedKey): number[] {\n const size = scaleTonesInDegreeOrder(key.scale).length;\n if (size !== 7) {\n throw new InvalidInputError(`figured bass requires a heptatonic key (received ${size} tones)`);\n }\n const alters: number[] = [];\n // The key's own tonic, so an Ab minor hands back its flats: reading the letters\n // off a scale respelled from the pitch classes would put the exercise in G#\n // minor and give every unfigured third a sharp the score never printed.\n for (const note of spellScale(key.tonic, key.scale)) {\n alters[mod7(note.letter)] = note.alter;\n }\n return alters;\n}\n\n/** The note a figure names: its letter from the bass, its accidental from the key. */\nfunction noteAbove(bass: Note, figure: Figure, alters: readonly number[]): Note {\n const letter = mod7(bass.letter + figure.number - 1);\n return { letter, alter: alteredBy(alters[letter] ?? 0, figure.accidental) };\n}\n\n/** A bass note reduced to the letter and accidental the figures are read from. */\nfunction bareNote(note: Note): Note {\n return { letter: mod7(note.letter), alter: note.alter };\n}\n\n/**\n * Build the chord a resolved figure set names over a bass.\n *\n * The figure set fixes which tone is the root, so the stack is read from that\n * root upward in diatonic thirds and matched against the chord vocabulary. A\n * stack no quality names — the Italian and German sixths, above all — is\n * reported as having no solution rather than being rounded to a chord it is\n * not. The French sixth is not among them: it stacks from its own lowered\n * supertonic, and the figures reach it.\n */\nfunction realizeSonority(\n bass: Note,\n resolved: Figure[],\n text: string,\n alters: readonly number[],\n): { chord: Chord; notes: Note[] } {\n const numbers = [...new Set(resolved.map((figure) => figure.number))].sort((a, b) => a - b);\n const sonority = FIGURE_SETS.get(numbers.join(','));\n if (sonority === undefined) {\n throw invalidFigures(text, 'names no chord over the bass');\n }\n const written = new Map(resolved.map((figure) => [figure.number, figure.accidental]));\n const notes = sonority.intervals.map((number) =>\n noteAbove(bass, { number, accidental: written.get(number) ?? 'none' }, alters),\n );\n const tones = [bass, ...notes];\n const root = tones[[1, ...sonority.intervals].indexOf(sonority.rootInterval)] ?? bass;\n const rootPc = noteToPitchClass(root);\n const stack: number[] = [];\n for (let step = 0; step < tones.length; step += 1) {\n // The chord tones sit two letters apart: root, third, fifth, seventh.\n const tone = tones.find((candidate) => mod7(candidate.letter - root.letter) === step * 2);\n if (tone === undefined) {\n throw new NoSolutionError(\n `figured bass ${describeRejected(text)} sounds ${noteNames(tones).join(' ')}, which does not stack in thirds`,\n );\n }\n stack.push(mod12(noteToPitchClass(tone) - rootPc));\n }\n const quality = TERTIAN_QUALITIES.get(stack.join(','));\n if (quality === undefined) {\n throw new NoSolutionError(\n `figured bass ${describeRejected(text)} sounds ${noteNames(tones).join(' ')}, which no chord quality names`,\n );\n }\n const chord = makeChord(rootPc, quality, noteToPitchClass(bass));\n chord.rootSpelling = { letter: root.letter, alter: root.alter };\n chord.bassSpelling = { letter: bass.letter, alter: bass.alter };\n return { chord, notes };\n}\n\n/**\n * A figure that moves over a held bass, such as the `4-3` suspension.\n *\n * @category Functional Harmony\n */\nexport type FiguredBassSuspension = {\n /** The interval above the bass that sounds first: the suspended note. */\n from: number;\n /** The interval it moves to, which is the one the chord contains. */\n to: number;\n /** The suspended note, spelled. */\n note: Note;\n /** The note it moves to, spelled. */\n resolution: Note;\n};\n\n/**\n * Everything a figured bass states: the chord, its spelled notes, and any\n * figure that moves.\n *\n * @category Functional Harmony\n */\nexport type FiguredBassRealization = {\n /**\n * The chord the figures name, carrying the given bass as its `bassPc` and\n * the letter spellings of its root and bass as enharmonic hints.\n */\n chord: Chord;\n /**\n * The spelled notes, bass first and then each figured interval in ascending\n * order. They carry no octave: figures fix the intervals above the bass, not\n * the register the upper voices take them in.\n */\n notes: Note[];\n /**\n * Every moving figure, in written order, or an empty array for a static one.\n * The chord above is the harmony these resolve into.\n */\n suspensions: FiguredBassSuspension[];\n};\n\n/**\n * Read a figured bass in full: the chord, its spelled notes, and its\n * suspensions.\n *\n * The figure grammar, in the forms this accepts:\n *\n * - Nothing at all, `5`, `3` or `53` — the root-position triad.\n * - `6` or `63` — the first-inversion triad; `64` — the second-inversion triad.\n * - `7` — the root-position seventh chord; `65`, `43`, and `42` (also written\n * `2`) — its first, second, and third inversions. The full stacks `753`,\n * `653`, `643` and `642` name the same four chords.\n * - Stacked figures may be written run together (`64`) or separated by a slash,\n * a comma or a space (`6/4`), which is the same figure either way.\n * - An accidental in front of a figure names that interval's note, exactly as\n * the same sign does on the staff: `#6`, `b7`, `#43`, and `n3` for a note the\n * signature would otherwise flatten. `##` and `bb` name the double\n * accidentals. An accidental with no figure applies to the third above the\n * bass, so a lone `n` over the dominant of C minor names its leading tone. The\n * Unicode accidentals are accepted alongside the typed ones.\n * - A slashed or crossed figure raises the interval the key gives by a semitone,\n * whatever accidental that lands on, and is typed `+`. It is the one sign here\n * that moves an interval rather than naming it, which is why the notation\n * carries it beside the printed accidentals: `+3` over the dominant of C minor\n * and `+3` over the dominant of A minor are each that key's leading tone,\n * written `n3` and `#3` respectively.\n * - A figure that moves is written with a hyphen: `4-3`, `9-8`, `7-6`, and any\n * other pair. Each side may carry its own accidental, as `4-#3` does in a\n * minor key. Moving figures may stand beside static ones (`5 4-3`).\n *\n * Every interval no accidental touches is the one the key's scale gives, which\n * is why the key is required and why the same figure names different qualities\n * over different degrees.\n *\n * A moving figure resolves into the chord: `4-3` over a G in C major returns\n * the G major triad, and the suspended C is reported as a suspension rather\n * than as a chord tone. A suspension is a melodic event over a single harmony —\n * the sounding chord does not change when it resolves — so naming the\n * resolution is the reading that answers \"which chord is this\", and the record\n * of what was suspended is kept beside it rather than thrown away.\n *\n * @param bass The bass note, spelled: a note name, a MIDI number, note data, or\n * a `Note`; its octave, if any, is not used.\n * @param figures The figures written under the bass; empty for an unfigured\n * note.\n * @param key The prevailing key, which supplies every unaltered interval, as a\n * key name, a key/scale, or a `Key`.\n * @returns The chord, its spelled notes, and any suspensions.\n * @throws If the figures are malformed or name no chord\n * ({@link InvalidInputError}), if the key is not heptatonic\n * ({@link InvalidInputError}), or if the figures sound a chord no\n * {@link ChordQuality} names, such as an augmented sixth\n * ({@link NoSolutionError}).\n * @example\n * ```ts\n * import { figuredBassRealization, majorKey, noteNames, parseNote } from '@libraz/libcantus';\n * const realized = figuredBassRealization(parseNote('G'), '4-3', majorKey(0));\n * noteNames(realized.notes); // ['G', 'B', 'D']\n * realized.suspensions[0]?.from; // 4 — the suspended C above the bass\n * ```\n * @category Functional Harmony\n */\nexport function figuredBassRealization(\n bass: NoteLike,\n figures: string,\n key: KeyLike,\n): FiguredBassRealization {\n if (typeof figures !== 'string') {\n throw new InvalidInputError(`figured bass must be a string; received ${typeof figures}`);\n }\n const bassData = toNoteData(bass);\n const alters = keyLetterAlters(resolveKey(key));\n // Reading the pitch class validates the note's fields before its letter is\n // used for the interval arithmetic above it.\n noteToPitchClass(bassData);\n const bassNote = bareNote(bassData);\n const text = figures.trim();\n const { figures: written, motions } = parseFigures(text);\n const suspensions = motions.map((motion) => ({\n from: motion.from.number,\n to: motion.to.number,\n note: noteAbove(bassNote, motion.from, alters),\n resolution: noteAbove(bassNote, motion.to, alters),\n }));\n // A figure resolving onto the octave lands on the bass itself, which is\n // already sounding and so adds nothing to the stack: `9-8` resolves to a\n // plain root-position triad.\n const resolved = [\n ...written,\n ...motions.map((motion) => motion.to).filter((figure) => figure.number !== OCTAVE_FIGURE),\n ];\n const { chord, notes } = realizeSonority(bassNote, resolved, text, alters);\n return { chord, notes: [bassNote, ...notes], suspensions };\n}\n\n/**\n * Realize a figured bass: the chord a bass note and its figures name in a key.\n *\n * The chord carries the given bass as its `bassPc`, so an inversion reads as\n * one everywhere the library measures a bass against a root, and carries the\n * letter spellings of its root and bass so the realization can be respelled\n * without losing the accidentals the figures asked for.\n *\n * See {@link figuredBassRealization} for the figure grammar this accepts and\n * for the spelled notes and suspensions it drops. A moving figure such as\n * `4-3` returns the chord it resolves into.\n *\n * @param bass The bass note, spelled; its octave, if any, is not used.\n * @param figures The figures written under the bass; empty for an unfigured\n * note.\n * @param key The prevailing key, which supplies every unaltered interval.\n * @param bass The bass note, spelled: a note name, a MIDI number, note data, or\n * a `Note`.\n * @param figures The figures written under the bass.\n * @param key The prevailing key, as a key name, a key/scale, or a `Key`.\n * @returns The chord the figures name.\n * @throws If the figures are malformed or name no chord\n * ({@link InvalidInputError}), if the key is not heptatonic\n * ({@link InvalidInputError}), or if the figures sound a chord no\n * {@link ChordQuality} names ({@link NoSolutionError}).\n * @example\n * ```ts\n * import { majorKey, minorKey, parseNote, realizeFiguredBass } from '@libraz/libcantus';\n * realizeFiguredBass(parseNote('B'), '6', majorKey(0));\n * // G major with B in the bass: { rootPc: 7, quality: 'maj', bassPc: 11, ... }\n * realizeFiguredBass(parseNote('G'), 'n', minorKey(0));\n * // G major in C minor: the leading tone is the B natural the score prints\n * ```\n * @category Functional Harmony\n */\nexport function realizeFiguredBass(bass: NoteLike, figures: string, key: KeyLike): Chord {\n return figuredBassRealization(bass, figures, key).chord;\n}\n\n/**\n * Write a chord as the figures a bass would carry under it, in a key.\n *\n * The inverse of {@link realizeFiguredBass} for every chord the figure grammar\n * reaches: the conventional abbreviation for the inversion (`6`, `64`, `7`,\n * `65`, `43`, `42`, or nothing at all for a root-position triad), with an\n * accidental on any interval the key does not already give. The accidental is\n * the sign the score prints in front of that note, so the third of the dominant\n * in C minor — a B natural under a signature carrying B flat — is written `n3`\n * rather than as a raise from the flat the key gives. An altered interval the\n * abbreviation would leave implied is written out, so the figure names the\n * interval it lands on rather than the bare accidental a score puts under the\n * bass — the figures this returns read back as the same chord.\n *\n * What this returns is read back before it is returned, so the two directions\n * cannot disagree: a chord the digits would name only approximately has no\n * figures rather than figures that fail to realize. What that excludes is a\n * stack, not a family of chords — the Italian and German sixths stack in thirds\n * over no root of their own and have no figures, while the French sixth stacks\n * from its lowered supertonic and is written `#643`, which realizes back to the\n * chord it was written from.\n *\n * @param chord The chord to figure, as a chord symbol, chord data, or a\n * `Chord`; its `bassPc`, when present, is the bass the intervals are measured\n * above.\n * @param key The prevailing key, which decides which intervals need no\n * accidental, as a key name, a key/scale, or a `Key`.\n * @returns The figures, as {@link realizeFiguredBass} accepts them.\n * @throws If the key is not heptatonic ({@link InvalidInputError}), or if no\n * figure names the chord — its bass is not one of its tones, its tones do not\n * stack in diatonic thirds above that bass, or it is an added-tone or\n * extended chord the notation has no abbreviation for\n * ({@link NoSolutionError}).\n * @example\n * ```ts\n * import { figuredBassOf, majorKey, makeChord } from '@libraz/libcantus';\n * figuredBassOf(makeChord(7, 'maj', 11), majorKey(0)); // '6' — G major over B\n * figuredBassOf(makeChord(7, 'dom7', 2), majorKey(0)); // '43' — G7 over D\n * ```\n * @category Functional Harmony\n */\nexport function figuredBassOf(chord: ChordLike, key: SpelledKeyLike): string {\n const data = toChordData(chord);\n const resolved = resolveKey(key);\n const alters = keyLetterAlters(resolved);\n const tones = spellChord(data, resolved.tonic, resolved.scale);\n const bassPc = mod12(data.bassPc ?? data.rootPc);\n const bass = tones.find((tone) => noteToPitchClass(tone) === bassPc);\n if (bass === undefined) {\n throw new NoSolutionError(\n \"figured bass measures intervals above a chord tone, and this chord's bass is not one of its tones\",\n );\n }\n const written = new Map<number, Note>();\n for (const tone of tones) {\n if (tone !== bass) {\n written.set(mod7(tone.letter - bass.letter) + 1, tone);\n }\n }\n const digits = FIGURE_ABBREVIATIONS.get([...written.keys()].sort((a, b) => a - b).join(','));\n if (digits === undefined) {\n throw new NoSolutionError(\n `no figured bass names ${noteNames(tones).join(' ')} over ${noteNames([bass])[0]}`,\n );\n }\n const accidentals = new Map<number, string>();\n for (const [number, tone] of written) {\n if (tone.alter === (alters[mod7(tone.letter)] ?? 0)) {\n // The signature already writes this note, so the score prints no sign.\n continue;\n }\n const glyph = ACCIDENTAL_GLYPHS.get(tone.alter);\n if (glyph === undefined) {\n throw new NoSolutionError(`no accidental writes a note altered by ${tone.alter} semitones`);\n }\n accidentals.set(number, glyph);\n }\n // Figures are written from the top down, and an altered interval the\n // abbreviation leaves implied has to be named for its accidental to land on\n // anything: a raised third under a `6` is written `6#3`, not `#6`.\n const figures = [...new Set([...digits, ...accidentals.keys()])]\n .sort((a, b) => b - a)\n .map((number) => `${accidentals.get(number) ?? ''}${number}`)\n .join('');\n assertRealizesAs(bass, figures, resolved, tones);\n return figures;\n}\n\n/** The pitch classes a set of spelled notes sounds, ascending. */\nfunction pitchClassesOf(notes: readonly Note[]): number[] {\n return [...new Set(notes.map(noteToPitchClass))].sort((a, b) => a - b);\n}\n\n/**\n * Require the figures just written to read back as the chord they were written\n * from, and report the ones that do not as having no figured bass.\n *\n * The digits are chosen by which intervals are written above the bass, which is\n * a weaker test than the one a realization applies: a chord may occupy the\n * interval positions of an inversion without stacking in thirds over any root,\n * and the Italian and German sixths do exactly that. Reading the figures back\n * is what keeps the two directions from disagreeing about which chords the\n * notation reaches, rather than a second table that would drift from the first.\n */\nfunction assertRealizesAs(bass: Note, figures: string, key: ResolvedKey, tones: Note[]): void {\n const noFigures = (detail: string): NoSolutionError =>\n new NoSolutionError(\n `no figured bass names ${noteNames(tones).join(' ')} over ${noteNames([bass])[0]}: the figures ${describeRejected(figures)} ${detail}`,\n );\n let realized: FiguredBassRealization;\n try {\n realized = figuredBassRealization(bass, figures, key);\n } catch (error) {\n throw error instanceof NoSolutionError ? noFigures('read back as no chord at all') : error;\n }\n const written = pitchClassesOf(realized.notes);\n const expected = pitchClassesOf(tones);\n if (written.length !== expected.length || written.some((pc, index) => pc !== expected[index])) {\n throw noFigures(`sound ${noteNames(realized.notes).join(' ')} instead`);\n }\n}\n","/**\n * The cross relation, written once.\n *\n * One letter carrying two different accidentals in two different voices across\n * a chord change is a rule the library states twice: the voicing search has to\n * refuse the contradiction, and the checker has to report it. Both read it from\n * here, so a voicing the search calls clean is one the checker calls clean.\n */\n\nimport type { Note } from '../../core/pitch/index.js';\nimport {\n diatonicLetterOf,\n noteToPitchClass,\n pitchClassOf,\n spelledInterval,\n} from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport type { Chord } from '../chord/index.js';\nimport {\n appliedDominantTarget,\n hasDominantSonority,\n isNeapolitanChordOf,\n} from '../tendency/index.js';\n\n/** One chord of an exercise as this rule reads it: what sounds, and what it is. */\nexport type ChromaticMoment = {\n /** The written notes, ascending (index 0 = lowest). */\n notes: readonly Note[];\n /** The chord they realize. */\n chord: Chord;\n};\n\n/**\n * What stands on either side of the pair being judged. Each is what tells a\n * chromatic tone that was led into by step from one that was sprung, and an\n * applied dominant from the diatonic triad that sounds the same; a caller that\n * cannot see that far leaves it out, and the rule reads the pair as exposed.\n */\nexport type ChromaticContext = {\n /** The voicing before the pair, naming how the earlier notes were reached. */\n before?: readonly Note[];\n /** The chord after the pair, naming what the later chord resolves to. */\n after?: Chord;\n};\n\n/** One exposed contradiction, by the voices holding the two notes. */\nexport type CrossRelation = {\n /** The voice holding the earlier note. */\n earlierVoice: number;\n /** The voice holding the later one. */\n laterVoice: number;\n /** The earlier note itself. */\n earlier: Note;\n /** The later note itself. */\n later: Note;\n};\n\n/**\n * Whether a chord resolves as an applied dominant: the dominant's sonority,\n * taken down a fifth onto a degree the key can make a local tonic, with the\n * chord it names actually following.\n *\n * Named for the resolution because the resolution is the only thing this adds:\n * which degrees may be tonicized is asked of the theory layer, so a chord\n * exempted here is one the analysis layer can write an applied numeral for.\n * What the two do differ in is what they are for — there a chord is an applied\n * dominant by what it is, here only by what comes next. The same major triad on\n * the third degree of a major key is V/vi where vi follows and a plain III\n * where nothing does, and only the first of the two licenses the chromatic tone\n * it introduces.\n */\nfunction resolvesAsAppliedDominant(chord: Chord, next: Chord | undefined, key: KeyScale): boolean {\n if (next === undefined || !hasDominantSonority(chord)) {\n return false;\n }\n const target = appliedDominantTarget(chord.rootPc + 5, key);\n return target !== null && pitchClassOf(next.rootPc) === target.rootPc;\n}\n\n/**\n * Whether a voicing sounds an augmented sixth above the lowered submediant.\n *\n * That interval is what the Italian, French and German chords are all named\n * for, and it is the only thing separating them from the bVI7 they sound like,\n * so it is read off the written letters rather than the pitch classes.\n */\nfunction soundsAugmentedSixth(moment: ChromaticMoment, key: KeyScale): boolean {\n const bass = moment.notes[0];\n if (bass === undefined || noteToPitchClass(bass) !== pitchClassOf(key.rootPc + 8)) {\n return false;\n }\n return moment.notes.some((note, voice) => {\n if (voice === 0) {\n return false;\n }\n const interval = spelledInterval(bass, note);\n return ((interval.number - 1) % 7) + 1 === 6 && pitchClassOf(interval.semitones) === 10;\n });\n}\n\n/**\n * Whether a chord is one of the chromatic harmonies whose own definition\n * contains the contradiction: an applied dominant, the Neapolitan, or an\n * augmented sixth. Approaching or leaving one of these is where the textbooks\n * license the cross relation, since the chord cannot be written without it.\n *\n * The exemption is for chromatic harmony, so a chord the key already contains\n * cannot claim it: a mode with a lowered second of its own — phrygian, locrian —\n * sounds the major triad on that degree as its native II, and reading it as the\n * Neapolitan would exempt the key's own chord from the rule every other diatonic\n * chord answers to.\n */\nfunction isChromaticHarmony(\n moment: ChromaticMoment,\n next: Chord | undefined,\n key: KeyScale,\n): boolean {\n return (\n resolvesAsAppliedDominant(moment.chord, next, key) ||\n isNeapolitanChordOf(moment.chord, key) ||\n soundsAugmentedSixth(moment, key)\n );\n}\n\n/** Whether a voice reaches its note by a diatonic step from the voicing before. */\nfunction approachedByStep(\n before: readonly Note[] | undefined,\n moment: readonly Note[],\n voice: number,\n): boolean {\n const earlier = before?.[voice];\n const later = moment[voice];\n if (earlier === undefined || later === undefined) {\n return false;\n }\n return spelledInterval(earlier, later).number === 2;\n}\n\n/** Whether a voice is the bass or the top voice of its chord. */\nexport function isOuterVoice(voice: number, notes: readonly Note[]): boolean {\n return voice === 0 || voice === notes.length - 1;\n}\n\n/**\n * Every cross relation between two consecutive chords: the same letter carrying\n * different accidentals in two different voices. Within one voice the same\n * motion is an ordinary chromatic inflection, so a voice is never compared with\n * itself.\n *\n * What the rule forbids is a semitone exposed across the texture, and three\n * things take that exposure away, each of which is reason enough on its own:\n *\n * - either of the two notes is led into by step, which is how a chromatic tone\n * is introduced rather than sprung;\n * - both notes lie in inner voices, where the classical norm is markedly\n * milder than it is between the outer ones;\n * - the chord being left or reached is an applied dominant, the Neapolitan, or\n * an augmented sixth, whose definition contains the altered degree.\n *\n * Each pair of voices is reported once. The two voices are walked in both\n * directions, so the same clash is met twice and the second reading is dropped.\n *\n * @param from The chord being left.\n * @param to The chord being reached.\n * @param key The key the exercise is written in.\n * @param context What stands on either side of the pair, where the caller knows\n * it. A missing neighbour is read as no exemption rather than as one.\n * @returns One entry per contradicting pair of voices, in voice order.\n */\nexport function crossRelations(\n from: ChromaticMoment,\n to: ChromaticMoment,\n key: KeyScale,\n context: ChromaticContext = {},\n): CrossRelation[] {\n const found: CrossRelation[] = [];\n const chromatic =\n isChromaticHarmony(from, to.chord, key) || isChromaticHarmony(to, context.after, key);\n if (chromatic) {\n return found;\n }\n const reported = new Set<string>();\n for (let first = 0; first < from.notes.length; first += 1) {\n for (let second = 0; second < to.notes.length; second += 1) {\n if (first === second) {\n continue;\n }\n const earlier = from.notes[first];\n const later = to.notes[second];\n if (earlier === undefined || later === undefined) {\n continue;\n }\n const letter = diatonicLetterOf(earlier.letter);\n if (letter !== diatonicLetterOf(later.letter) || earlier.alter === later.alter) {\n continue;\n }\n if (\n approachedByStep(context.before, from.notes, first) ||\n approachedByStep(from.notes, to.notes, second) ||\n (!isOuterVoice(first, from.notes) && !isOuterVoice(second, to.notes))\n ) {\n continue;\n }\n const pair = `${letter}:${Math.min(first, second)}:${Math.max(first, second)}`;\n if (reported.has(pair)) {\n continue;\n }\n reported.add(pair);\n found.push({ earlierVoice: first, laterVoice: second, earlier, later });\n }\n }\n return found;\n}\n","/**\n * Tendency tones: which pitch class of a chord owes its next move to a rule.\n *\n * The voicing search and the part-writing checker both need this, and they have\n * to agree on it. A generator resolving a tone the checker does not recognize —\n * or holding one the checker thinks must move — would report its own output as\n * faulty, so both read the same predicates from here.\n */\n\nimport type { Note } from '../../core/pitch/index.js';\nimport { noteToMidi, pitchClassOf, spelledInterval } from '../../core/pitch/index.js';\nimport type { Chord } from '../chord/index.js';\nimport { isAugmentedMelodicInterval } from '../counterpoint/index.js';\nimport { type KeyLike, resolveKey } from '../scale/index.js';\nimport { spellPitchClass } from '../spelling/index.js';\nimport { isDescendingStep } from '../tendency/index.js';\n\n/**\n * How one chord's tones are written: one spelling per pitch class, and the MIDI\n * pitch each of those spellings sounds at octave 0.\n *\n * The search reads a candidate's letters thousands of times per chord, and a\n * spelling depends on the pitch class alone once the chord and the key are\n * fixed, so the twelve of them are worked out once and the octave is arithmetic.\n */\nexport type SpellingTable = {\n /** The octave-less spelling of each pitch class 0-11. */\n notes: readonly Note[];\n /** The MIDI pitch each of those spellings sounds at octave 0. */\n bases: readonly number[];\n};\n\n/**\n * Name every pitch class the way {@link spellVoicing} names it under one chord.\n *\n * The generator and the checker have to read the same letters — an augmented\n * second is a minor third by ear, so a generator spelling its own output by\n * another rule would write intervals its checker forbids — and both go through\n * {@link spellPitchClass} with the chord as context.\n *\n * @param keyLike The prevailing key, in any form a key is held in.\n * @param chord The chord sounding, or undefined to let the key alone decide.\n * @returns The table, indexed by pitch class.\n */\nexport function spellingTable(keyLike: KeyLike, chord?: Chord): SpellingTable {\n // Read whole rather than reduced: an exercise written in Ab minor is judged on\n // the letters it is written with, and a key that arrived carrying its tonic\n // would otherwise have that tonic derived back from the pitch classes.\n const { tonic, scale: key } = resolveKey(keyLike);\n const notes: Note[] = [];\n const bases: number[] = [];\n for (let pc = 0; pc < 12; pc += 1) {\n const note =\n chord === undefined\n ? spellPitchClass(pc, tonic, key)\n : spellPitchClass(pc, tonic, key, { chord });\n notes.push(note);\n bases.push(noteToMidi({ letter: note.letter, alter: note.alter, octave: 0 }));\n }\n return { notes, bases };\n}\n\n/** The spelled note a table gives a sounding pitch, carrying its octave. */\nexport function spelledAt(table: SpellingTable, pitch: number): Note | undefined {\n const pc = pitchClassOf(pitch);\n const note = table.notes[pc];\n const base = table.bases[pc];\n if (note === undefined || base === undefined) {\n return undefined;\n }\n return { letter: note.letter, alter: note.alter, octave: (pitch - base) / 12 };\n}\n\n/**\n * Whether a voice moving between two sounding pitches writes an augmented\n * interval, read through the same spelling the checker applies to the result.\n *\n * @param from The table of the chord left.\n * @param fromPitch The pitch the voice sang.\n * @param to The table of the chord arrived on.\n * @param toPitch The pitch it moved to.\n * @returns True when the written interval is augmented.\n */\nexport function movesByAugmentedInterval(\n from: SpellingTable,\n fromPitch: number,\n to: SpellingTable,\n toPitch: number,\n): boolean {\n const earlier = spelledAt(from, fromPitch);\n const later = spelledAt(to, toPitch);\n if (earlier === undefined || later === undefined) {\n return false;\n }\n return isAugmentedMelodicInterval(earlier, later);\n}\n\n/**\n * Whether a voice moving between two sounding pitches falls by a diatonic step,\n * which is how a chordal seventh resolves.\n *\n * Read through the same spelling the checker applies to the result: two\n * semitones down is a diminished third as often as a major second once the\n * letters are settled, and only one of the two is a resolution.\n *\n * @param from The table of the chord left.\n * @param fromPitch The pitch the voice sang.\n * @param to The table of the chord arrived on.\n * @param toPitch The pitch it moved to.\n * @returns True when the written interval is a descending second.\n */\nexport function movesByDescendingStep(\n from: SpellingTable,\n fromPitch: number,\n to: SpellingTable,\n toPitch: number,\n): boolean {\n const earlier = spelledAt(from, fromPitch);\n const later = spelledAt(to, toPitch);\n if (earlier === undefined || later === undefined) {\n return false;\n }\n return isDescendingStep(spelledInterval(earlier, later));\n}\n","import { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\n\nexport { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\n\nimport { NoSolutionError } from '../../core/errors/index.js';\nimport type { Note } from '../../core/pitch/index.js';\nimport { diatonicLetterOf } from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport type { Chord } from '../chord/index.js';\nimport { chordPitchClasses, chordToneRole } from '../chord/index.js';\nimport {\n createsHiddenParallelPerfect,\n createsParallelPerfect,\n createsVoiceOverlap,\n} from '../counterpoint/index.js';\nimport { crossRelations } from '../partwriting/cross-relation.js';\nimport type { ResolvedKey } from '../scale/index.js';\nimport {\n isFrustratedLeadingTone,\n isFunctioningLeadingTone,\n leadingTonePcOf,\n seventhPcOf,\n} from '../tendency/index.js';\nimport type { VoiceRange } from './satb.js';\nimport type { SpellingTable } from './tendency.js';\nimport {\n movesByAugmentedInterval,\n movesByDescendingStep,\n spelledAt,\n spellingTable,\n} from './tendency.js';\n\n/** Default maximum spacing between adjacent upper voices (one octave). */\nexport const DEFAULT_MAX_SPACING = 12;\n\n/** Score penalty per counterpoint violation between consecutive voicings. */\nexport const VIOLATION_PENALTY = 1000;\n/** Score penalty per chord tone absent from a voicing. */\nconst MISSING_TONE_PENALTY = 500;\n/**\n * Score penalty for omitting the chord's fifth. Far milder than any other\n * missing tone: the fifth carries no identity, and the textbook resolution of a\n * complete dominant seventh onto a tonic triad — every tendency tone resolving\n * inward — leaves the tonic without its fifth. Weighted below\n * {@link RESOLUTION_PENALTY} so a resolution can buy the omission, and above\n * {@link POOR_DOUBLING_PENALTY} so nothing else can.\n */\nconst MISSING_FIFTH_PENALTY = 20;\n/** Score penalty per doubled tone that is neither the root nor the fifth. */\nconst POOR_DOUBLING_PENALTY = 4;\n/** Penalty for adjacent voices sharing exactly the same MIDI pitch. */\nconst UNISON_PENALTY = 100;\n/**\n * Score penalty for doubling the key's leading tone. Doubling it guarantees\n * parallel octaves on its resolution, so it is a rule violation rather than a\n * matter of taste — weighted far above {@link POOR_DOUBLING_PENALTY}, but below\n * a missing chord tone, which damages the chord itself.\n */\nconst LEADING_TONE_DOUBLING_PENALTY = 200;\n/**\n * Score penalty per tendency tone that fails to resolve. Well above the few\n * semitones of extra motion a correct resolution usually costs, so smoothness\n * can never buy an unresolved seventh, and well below\n * {@link VIOLATION_PENALTY}, so avoiding parallels still comes first.\n */\nexport const RESOLUTION_PENALTY = 200;\n/** Hard cap on candidate voicings evaluated per chord, keeping the search bounded. */\nconst DEFAULT_MAX_CANDIDATES = 4000;\n/**\n * Hard cap on the candidate pairs one chord's lookahead may weigh. A lookahead\n * reads every candidate of the chord against every candidate of the chord after\n * it, so the pairs grow as the square of a per-chord search that is itself only\n * bounded at a few thousand. Past this many the lookahead is dropped for that\n * chord — for all of its candidates alike, so the choice is never biased by\n * where the cap happened to fall.\n */\nexport const MAX_LOOKAHEAD_PAIRS = 1_000_000;\n/**\n * Hard cap on search-tree nodes visited per chord. The candidate cap alone only\n * counts completed voicings, so a search whose upper voices admit no chord tone\n * would expand the whole cartesian product without ever reaching a leaf.\n */\nconst SEARCH_NODES_PER_CANDIDATE = 16;\n\n/**\n * Score penalty for a hidden/direct perfect fifth or octave reached on the\n * outer-voice (bass–soprano) pair. The checker reports one as a broken rule, so\n * the search cannot treat it as a nudge: the weight is heavy enough that the\n * textbook escape — tripling the root and omitting the fifth, which costs\n * {@link MISSING_FIFTH_PENALTY} — is the cheaper answer. It stays below\n * {@link VIOLATION_PENALTY} because a hidden perfect is discouraged where a\n * true parallel is forbidden, which is the order the rules are ranked in.\n *\n * It is scored beside the other penalties rather than inside\n * {@link leadingCost}, whose published sibling `voiceLeadingCost` is defined as\n * the summed semitone motion and nothing else.\n */\nexport const HIDDEN_PERFECT_PENALTY = 100;\n\n/**\n * Score penalty per cross relation written between two consecutive voicings.\n *\n * The checker reports the exposed contradiction as a broken rule, so the search\n * has to be able to see it: without a term of its own the rule was stated on\n * one side only, and a placement that spells the clash away could never be\n * preferred. Weighted with {@link HIDDEN_PERFECT_PENALTY}, the other rule about\n * an interval being exposed rather than forbidden outright, so a cross relation\n * is worth a little rougher voice leading but never a parallel or a missing\n * chord tone — some of them cannot be voiced away at all.\n */\nexport const CROSS_RELATION_PENALTY = HIDDEN_PERFECT_PENALTY;\n\n/**\n * Score penalty per voice moving by an augmented interval. The augmented second\n * between the lowered sixth and the raised seventh is what a minor-key search\n * writes if nothing stops it, and the checker reports it as a broken rule\n * rather than as rough writing — so it is weighted with the counterpoint\n * violations, where no amount of smoother motion can buy it back, rather than\n * alongside them. A chord whose every candidate writes one is still voiced: the\n * penalty then falls on all of them alike and the rest of the scoring decides.\n */\nexport const AUGMENTED_MELODIC_PENALTY = VIOLATION_PENALTY;\n\n/**\n * Candidate voicings of one chord, held flat: voice `v` of candidate `i` sits at\n * `pitches[i * voices + v]`.\n *\n * A four-voice search reaches four thousand candidates, so a voicing per\n * candidate would be four thousand short arrays per chord — the bulk of what\n * voicing a lead sheet allocates. One buffer holds them all instead, and\n * {@link voiceProgression} reuses the same buffer for every chord, so the whole\n * progression allocates it once.\n */\nexport type VoicingCandidates = {\n /** The candidate pitches, valid up to `count * voices`. */\n pitches: Int32Array;\n /** How many candidates the last enumeration wrote. */\n count: number;\n /** Voices per candidate, i.e. the stride of `pitches`. */\n voices: number;\n};\n\n/** Candidates a fresh buffer holds before it has to grow. */\nconst INITIAL_CANDIDATE_CAPACITY = 512;\n\n/** An empty candidate buffer, ready to be filled by {@link enumerateVoicings}. */\nexport function createCandidateBuffer(): VoicingCandidates {\n return { pitches: new Int32Array(0), count: 0, voices: 0 };\n}\n\n/**\n * All MIDI pitches of a pitch class inside an inclusive range, ordered from the\n * centre of the range outward (ties break toward the lower pitch).\n *\n * The ordering is within one pitch class, not across them: the enumeration\n * above visits the chord's pitch classes in their own order and walks each\n * one's octaves centre-outward. So a candidate set truncated at\n * {@link DEFAULT_MAX_CANDIDATES} holds the register-central octaves of the\n * pitch classes it reached, rather than the low octaves a plain ascending scan\n * would visit first — which is what this ordering buys, and no more.\n */\nfunction pitchesForPc(pc: number, range: VoiceRange): number[] {\n const result: number[] = [];\n for (let midi = Math.ceil(range.min); midi <= range.max; midi += 1) {\n if (pitchClass(midi) === pc) {\n result.push(midi);\n }\n }\n const center = (range.min + range.max) / 2;\n return result.sort((a, b) => {\n const da = Math.abs(a - center);\n const db = Math.abs(b - center);\n return da === db ? a - b : da - db;\n });\n}\n\n/**\n * Enumerate candidate voicings for a chord: the bass takes the chord's bass\n * (or root) pitch class at each available octave, and each upper voice takes\n * any chord pitch class within its range. Candidates are built in ascending\n * voice order and pruned to exclude voice crossings and over-wide adjacent\n * spacing (the bass–tenor pair is allowed an extra octave, per convention).\n * Enumeration is deterministic and capped at `maxCandidates` completed\n * voicings and a proportional number of visited nodes.\n *\n * Each voice's admissible pitches are resolved once up front rather than\n * recomputed at every node, and a voice that admits no chord tone at all fails\n * immediately: continuing would explore every combination of the voices below\n * it only to reach no leaf.\n *\n * @param into A buffer to fill, reused and grown in place. Pass the same one\n * for every chord of a progression so the search allocates once rather than\n * once per chord.\n * @throws If any voice's range contains no pitch of any chord tone.\n */\nexport function enumerateVoicings(\n chord: Chord,\n ranges: VoiceRange[],\n maxSpacing: number,\n maxCandidates = DEFAULT_MAX_CANDIDATES,\n into: VoicingCandidates = createCandidateBuffer(),\n): VoicingCandidates {\n const chordPcs = chordPitchClasses(chord);\n const bassPc = pitchClass(chord.bassPc ?? chord.rootPc);\n // Pitch-class-major order, matching the enumeration order of the search.\n const byVoice = ranges.map((range, voice) =>\n (voice === 0 ? [bassPc] : chordPcs).flatMap((pc) => pitchesForPc(pc, range)),\n );\n for (let voice = 0; voice < byVoice.length; voice += 1) {\n if ((byVoice[voice] ?? []).length === 0) {\n throw new NoSolutionError('no voicing satisfies the given ranges');\n }\n }\n const voices = ranges.length;\n into.voices = voices;\n into.count = 0;\n let capacity = Math.floor(into.pitches.length / Math.max(1, voices));\n if (capacity === 0) {\n capacity = Math.min(maxCandidates, INITIAL_CANDIDATE_CAPACITY);\n into.pitches = new Int32Array(capacity * voices);\n }\n const current = new Int32Array(voices);\n let nodes = 0;\n const maxSearchNodes = maxCandidates * SEARCH_NODES_PER_CANDIDATE;\n const build = (voice: number): void => {\n nodes += 1;\n if (into.count >= maxCandidates || nodes > maxSearchNodes) {\n return;\n }\n if (voice === voices) {\n if (into.count === capacity) {\n capacity = Math.min(maxCandidates, capacity * 2);\n const grown = new Int32Array(capacity * voices);\n grown.set(into.pitches.subarray(0, into.count * voices));\n into.pitches = grown;\n }\n into.pitches.set(current, into.count * voices);\n into.count += 1;\n return;\n }\n const prev = voice === 0 ? undefined : current[voice - 1];\n for (const pitch of byVoice[voice] ?? []) {\n if (prev !== undefined) {\n if (pitch < prev) {\n continue; // would cross below the next lower voice\n }\n const spacingLimit = voice === 1 ? maxSpacing + 12 : maxSpacing;\n if (pitch - prev > spacingLimit) {\n continue;\n }\n }\n current[voice] = pitch;\n build(voice + 1);\n }\n };\n build(0);\n return into;\n}\n\n/**\n * The per-chord tables {@link structuralPenalty} reads.\n *\n * Every entry is a function of the chord and key alone, so it is worked out\n * once per chord instead of once per candidate — which is what keeps a\n * four-thousand-candidate search from rebuilding the chord's pitch-class set,\n * and re-deriving every tone's role, four thousand times.\n */\nexport type StructuralTables = {\n /** The chord's pitch classes, ascending. */\n tones: readonly number[];\n /** Penalty for the tone at the same index of `tones` being absent. */\n missing: readonly number[];\n /** Penalty per extra copy of each pitch class 0-11. */\n doubling: Float64Array;\n};\n\n/**\n * Build the structural tables for one chord: heavily penalize missing chord\n * tones (mildly for the fifth, which carries no identity), mildly penalize\n * doubling anything other than the root or fifth, and — when a key is known and\n * the chord is one where the leading tone functions as one — heavily penalize\n * doubling that leading tone.\n *\n * The exempt fifth is the chord's own fifth, whatever its size: a diminished,\n * augmented, or absent fifth would leave the root as the only freely doubled\n * tone, which is what forces a doubled leading tone in a `viio` chord.\n */\nexport function structuralTables(chord: Chord, key?: KeyScale): StructuralTables {\n const tones = chordPitchClasses(chord);\n const missing = tones.map((pc) =>\n chordToneRole(pc, chord) === 'fifth' ? MISSING_FIFTH_PENALTY : MISSING_TONE_PENALTY,\n );\n const rootPc = pitchClass(chord.rootPc);\n const leadingTonePc =\n key !== undefined && isFunctioningLeadingTone(chord, key) ? leadingTonePcOf(key) : -1;\n const doubling = new Float64Array(12);\n for (let pc = 0; pc < 12; pc += 1) {\n if (pc === leadingTonePc) {\n doubling[pc] = LEADING_TONE_DOUBLING_PENALTY;\n } else if (pc !== rootPc && chordToneRole(pc, chord) !== 'fifth') {\n doubling[pc] = POOR_DOUBLING_PENALTY;\n }\n }\n return { tones, missing, doubling };\n}\n\n/**\n * Pitch-class tally shared by every structural evaluation. The scoring loop\n * calls nothing that could re-enter it, so one buffer serves the whole search.\n */\nconst PITCH_CLASS_COUNTS = new Int32Array(12);\n\n/**\n * Structural quality penalty of the candidate at `offset` in `pitches`, read\n * off the chord's {@link StructuralTables}.\n */\nexport function structuralPenalty(\n tables: StructuralTables,\n pitches: ArrayLike<number>,\n offset: number,\n voices: number,\n): number {\n PITCH_CLASS_COUNTS.fill(0);\n let penalty = 0;\n for (let index = 0; index < voices; index += 1) {\n const pitch = pitches[offset + index];\n if (pitch === undefined) {\n continue;\n }\n const pc = pitchClass(pitch);\n PITCH_CLASS_COUNTS[pc] = (PITCH_CLASS_COUNTS[pc] ?? 0) + 1;\n if (index > 0 && pitch === pitches[offset + index - 1]) {\n penalty += UNISON_PENALTY;\n }\n }\n for (let index = 0; index < tables.tones.length; index += 1) {\n const pc = tables.tones[index];\n if (pc !== undefined && PITCH_CLASS_COUNTS[pc] === 0) {\n penalty += tables.missing[index] ?? 0;\n }\n }\n for (let pc = 0; pc < 12; pc += 1) {\n const count = PITCH_CLASS_COUNTS[pc] ?? 0;\n if (count > 1) {\n penalty += (count - 1) * (tables.doubling[pc] ?? 0);\n }\n }\n return penalty;\n}\n\n/**\n * The per-chord-pair tables {@link resolutionViolations} reads, worked out once\n * per chord rather than once per candidate.\n */\nexport type ResolutionTables = {\n /** The leaving chord's seventh, or -1 when it has none. */\n seventhPc: number;\n /**\n * The key's leading tone, or -1 without a key and wherever the leaving chord\n * is not one the leading tone functions in.\n */\n leadingTonePc: number;\n /** The key's tonic, or -1 without a key. */\n tonicPc: number;\n /** Whether the arriving chord contains each pitch class 0-11. */\n nextHas: Uint8Array;\n /** The arriving chord itself, which names the fifth a leading tone may fall to. */\n nextChord: Chord;\n};\n\n/** Build the resolution tables for one chord-to-chord move. */\nexport function resolutionTables(\n prevChord: Chord,\n nextChord: Chord,\n key?: KeyScale,\n): ResolutionTables {\n const nextHas = new Uint8Array(12);\n for (const pc of chordPitchClasses(nextChord)) {\n nextHas[pc] = 1;\n }\n return {\n seventhPc: seventhPcOf(prevChord) ?? -1,\n leadingTonePc:\n key !== undefined && isFunctioningLeadingTone(prevChord, key) ? leadingTonePcOf(key) : -1,\n tonicPc: key === undefined ? -1 : pitchClass(key.rootPc),\n nextHas,\n nextChord,\n };\n}\n\n/**\n * Count the tendency tones that fail to resolve between two consecutive\n * voicings of the same progression.\n *\n * A chordal seventh is a dissonance: unless the next chord holds it as a common\n * tone, the voice carrying it must fall by step. That step is the written one,\n * read through the same spelling {@link checkPartWriting} will mark the result\n * by — a fall of two semitones that writes a diminished third resolves nothing,\n * and a search counting semitones alone would accept what its own checker\n * rejects. Where no key names the letters there is no spelling to read, and the\n * sounding step is all either layer can go by.\n *\n * A leading tone must rise to the tonic whenever the next chord contains one,\n * and only where it is functioning as a leading tone; without a key there is no\n * leading tone to speak of, so that half of the rule is skipped. An inner voice\n * may frustrate its leading tone instead, exactly as {@link checkPartWriting}\n * allows, so the search is not scored against a resolution its own checker\n * accepts.\n */\nexport function resolutionViolations(\n tables: ResolutionTables,\n spelling: MelodicSpelling | undefined,\n prev: ArrayLike<number>,\n prevOffset: number,\n cur: ArrayLike<number>,\n curOffset: number,\n voices: number,\n): number {\n const { seventhPc, leadingTonePc, tonicPc, nextHas, nextChord } = tables;\n let count = 0;\n for (let voice = 0; voice < voices; voice += 1) {\n const from = prev[prevOffset + voice];\n const to = cur[curOffset + voice];\n if (from === undefined || to === undefined) {\n continue;\n }\n const fromPc = pitchClass(from);\n const motion = to - from;\n const resolved =\n spelling === undefined\n ? motion === -1 || motion === -2\n : movesByDescendingStep(spelling.from, from, spelling.to, to);\n if (fromPc === seventhPc && nextHas[fromPc] === 0 && !resolved) {\n count += 1;\n }\n const inner = voice > 0 && voice < voices - 1;\n if (\n fromPc === leadingTonePc &&\n tonicPc >= 0 &&\n nextHas[tonicPc] === 1 &&\n nextHas[fromPc] === 0 &&\n motion !== 1 &&\n !(inner && isFrustratedLeadingTone(from, to, nextChord))\n ) {\n count += 1;\n }\n }\n return count;\n}\n\n/**\n * Total voice-leading cost from the voicing at `prevOffset` to the one at\n * `curOffset`: the summed absolute semitone motion, and nothing else.\n */\nexport function leadingCost(\n prev: ArrayLike<number>,\n prevOffset: number,\n cur: ArrayLike<number>,\n curOffset: number,\n voices: number,\n): number {\n let total = 0;\n for (let voice = 0; voice < voices; voice += 1) {\n const a = prev[prevOffset + voice];\n const b = cur[curOffset + voice];\n if (a === undefined || b === undefined) {\n continue;\n }\n total += Math.abs(b - a);\n }\n return total;\n}\n\n/**\n * Whether the outermost voices reach a hidden/direct perfect fifth or octave by\n * similar motion, the pair {@link checkPartWriting} judges the rule on. Between\n * inner voices the interval is covered by the others, and no rule reports it.\n */\nexport function createsOuterHiddenPerfect(\n prev: ArrayLike<number>,\n prevOffset: number,\n cur: ArrayLike<number>,\n curOffset: number,\n voices: number,\n): boolean {\n if (voices < 2) {\n return false;\n }\n const bassPrev = prev[prevOffset];\n const bassCur = cur[curOffset];\n const sopPrev = prev[prevOffset + voices - 1];\n const sopCur = cur[curOffset + voices - 1];\n if (\n bassPrev === undefined ||\n bassCur === undefined ||\n sopPrev === undefined ||\n sopCur === undefined\n ) {\n return false;\n }\n return createsHiddenParallelPerfect(sopPrev, sopCur, bassPrev, bassCur);\n}\n\n/**\n * The spelling of the chord left and of the chord reached, which is what lets\n * the search read the letters its own output will be checked by.\n */\nexport type MelodicSpelling = {\n /** How the chord being left is written. */\n from: SpellingTable;\n /** How the chord being reached is written. */\n to: SpellingTable;\n};\n\n/**\n * Count the voices that move by an augmented interval between two consecutive\n * voicings, reading both through the spelling\n * {@link spellVoicing} gives them.\n */\nexport function augmentedMelodicCount(\n spelling: MelodicSpelling,\n prev: ArrayLike<number>,\n prevOffset: number,\n cur: ArrayLike<number>,\n curOffset: number,\n voices: number,\n): number {\n let count = 0;\n for (let voice = 0; voice < voices; voice += 1) {\n const from = prev[prevOffset + voice];\n const to = cur[curOffset + voice];\n if (from === undefined || to === undefined) {\n continue;\n }\n if (movesByAugmentedInterval(spelling.from, from, spelling.to, to)) {\n count += 1;\n }\n }\n return count;\n}\n\n/**\n * What the cross-relation rule reads besides the voicings themselves, worked\n * out once per chord-to-chord move.\n */\nexport type CrossRelationTables = {\n /** The chord being left. */\n fromChord: Chord;\n /** The chord being reached. */\n toChord: Chord;\n /**\n * The chord after that one, where the caller knows it. It is what tells an\n * applied dominant — which is written with the contradiction and licenses it —\n * from the diatonic triad that sounds the same; a search that cannot see that\n * far reads the pair as exposed rather than exempt.\n */\n afterChord: Chord | undefined;\n /** The prevailing key. */\n key: KeyScale;\n};\n\n/** Spell one candidate voicing, so the letters can be put to the written rules. */\nfunction spellRow(\n table: SpellingTable,\n pitches: ArrayLike<number>,\n offset: number,\n voices: number,\n): Note[] {\n const notes: Note[] = [];\n for (let voice = 0; voice < voices; voice += 1) {\n const pitch = pitches[offset + voice];\n const note = pitch === undefined ? undefined : spelledAt(table, pitch);\n if (note !== undefined) {\n notes.push(note);\n }\n }\n return notes;\n}\n\n/**\n * Whether any letter is written two ways across the move: the cheap half of the\n * cross-relation rule, which the spelling tables answer without octaves and\n * without allocating a note.\n */\nfunction contradictsALetter(\n spelling: MelodicSpelling,\n prev: ArrayLike<number>,\n prevOffset: number,\n cur: ArrayLike<number>,\n curOffset: number,\n voices: number,\n): boolean {\n for (let voice = 0; voice < voices; voice += 1) {\n const from = prev[prevOffset + voice];\n const earlier = from === undefined ? undefined : spelling.from.notes[pitchClass(from)];\n if (earlier === undefined) {\n continue;\n }\n for (let other = 0; other < voices; other += 1) {\n if (other === voice) {\n continue;\n }\n const to = cur[curOffset + other];\n const later = to === undefined ? undefined : spelling.to.notes[pitchClass(to)];\n if (\n later !== undefined &&\n earlier.alter !== later.alter &&\n diatonicLetterOf(earlier.letter) === diatonicLetterOf(later.letter)\n ) {\n return true;\n }\n }\n }\n return false;\n}\n\n/**\n * Count the cross relations two consecutive voicings write, by the same rule\n * {@link checkPartWriting} reports them under.\n *\n * The letters are scanned first and the full rule — which asks what chord is\n * being written and how each note was reached — is put only to a move that\n * actually contradicts one, since almost none do.\n */\nexport function crossRelationCount(\n spelling: MelodicSpelling,\n tables: CrossRelationTables,\n prev: ArrayLike<number>,\n prevOffset: number,\n cur: ArrayLike<number>,\n curOffset: number,\n voices: number,\n): number {\n if (!contradictsALetter(spelling, prev, prevOffset, cur, curOffset, voices)) {\n return 0;\n }\n return crossRelations(\n { notes: spellRow(spelling.from, prev, prevOffset, voices), chord: tables.fromChord },\n { notes: spellRow(spelling.to, cur, curOffset, voices), chord: tables.toChord },\n tables.key,\n { after: tables.afterChord },\n ).length;\n}\n\n/**\n * Count counterpoint violations between two consecutive voicings of equal\n * length: parallel perfects (fifths and octaves alike, since octaves are the\n * perfect-class-zero case of {@link createsParallelPerfect}) on every voice\n * pair, and voice overlaps on adjacent pairs.\n *\n * Voice crossing and over-wide spacing are not counted: {@link\n * enumerateVoicings} rejects both while building a candidate, so a candidate\n * that reaches here cannot exhibit either. Overlap involves the previous\n * voicing and so is still live.\n */\nexport function violationCount(\n prev: ArrayLike<number>,\n prevOffset: number,\n cur: ArrayLike<number>,\n curOffset: number,\n voices: number,\n): number {\n let count = 0;\n for (let lower = 0; lower < voices; lower += 1) {\n for (let upper = lower + 1; upper < voices; upper += 1) {\n const prevLower = prev[prevOffset + lower];\n const prevUpper = prev[prevOffset + upper];\n const curLower = cur[curOffset + lower];\n const curUpper = cur[curOffset + upper];\n if (\n prevLower === undefined ||\n prevUpper === undefined ||\n curLower === undefined ||\n curUpper === undefined\n ) {\n continue;\n }\n if (createsParallelPerfect(prevUpper, curUpper, prevLower, curLower)) {\n count += 1;\n }\n if (upper === lower + 1) {\n if (createsVoiceOverlap(prevUpper, curUpper, prevLower, curLower)) {\n count += 1;\n }\n }\n }\n }\n return count;\n}\n\n/**\n * Everything a move onto one chord is scored against, worked out once per chord\n * and then read for each of the candidates weighed against it.\n */\nexport type MoveScoring = {\n /** The arriving chord's structural tables. */\n structure: StructuralTables;\n /** The tendency-tone tables, absent when no chord is being left. */\n resolution: ResolutionTables | undefined;\n /** How both chords are written, absent when no key names the letters. */\n spelling: MelodicSpelling | undefined;\n /**\n * What the cross-relation rule reads, absent when no chord is being left or\n * no key names the letters the rule is about.\n */\n crossRelation: CrossRelationTables | undefined;\n};\n\n/**\n * Build the scoring context of a move onto `chord`.\n *\n * The letters are only decidable against a key, so the augmented-interval and\n * cross-relation rules join the search wherever one was given — the same\n * condition the leading-tone rules are under.\n *\n * @param prevChord The chord being left, or undefined when nothing precedes it.\n * @param chord The chord being reached.\n * @param key The prevailing key, or undefined when none was given.\n * @param nextChord The chord after the one being reached, where the caller\n * knows it. It licenses the chromatic tone an applied dominant is written\n * with, and nothing else reads it.\n */\nexport function moveScoring(\n prevChord: Chord | undefined,\n chord: Chord,\n key?: ResolvedKey,\n nextChord?: Chord,\n): MoveScoring {\n // The pitch-class tables want the scale alone and say so here; the spelling\n // table is handed the key whole, since the letters it writes are the key's.\n return {\n structure: structuralTables(chord, key?.scale),\n resolution:\n prevChord === undefined ? undefined : resolutionTables(prevChord, chord, key?.scale),\n spelling:\n key === undefined\n ? undefined\n : { from: spellingTable(key, prevChord), to: spellingTable(key, chord) },\n crossRelation:\n prevChord === undefined || key === undefined\n ? undefined\n : { fromChord: prevChord, toChord: chord, afterChord: nextChord, key: key.scale },\n };\n}\n\n/**\n * The rule-violation weight of one move: the counterpoint violations, the\n * tendency tones left unresolved, the augmented intervals written, the cross\n * relations exposed, and a direct perfect reached by the outer voices.\n *\n * This is the part of a move's score that a shorter line cannot buy back, which\n * is also what makes it the part worth looking a chord ahead for: a candidate\n * that spares its successor a broken rule is worth reaching for, while one that\n * merely spares it a few semitones of motion is not.\n */\nexport function violationWeight(\n scoring: MoveScoring,\n prev: ArrayLike<number>,\n prevOffset: number,\n cur: ArrayLike<number>,\n curOffset: number,\n voices: number,\n): number {\n const { resolution, spelling, crossRelation } = scoring;\n const unresolved =\n resolution === undefined\n ? 0\n : resolutionViolations(resolution, spelling, prev, prevOffset, cur, curOffset, voices);\n const augmented =\n spelling === undefined\n ? 0\n : augmentedMelodicCount(spelling, prev, prevOffset, cur, curOffset, voices);\n const contradictions =\n spelling === undefined || crossRelation === undefined\n ? 0\n : crossRelationCount(spelling, crossRelation, prev, prevOffset, cur, curOffset, voices);\n return (\n VIOLATION_PENALTY * violationCount(prev, prevOffset, cur, curOffset, voices) +\n RESOLUTION_PENALTY * unresolved +\n AUGMENTED_MELODIC_PENALTY * augmented +\n CROSS_RELATION_PENALTY * contradictions +\n (createsOuterHiddenPerfect(prev, prevOffset, cur, curOffset, voices)\n ? HIDDEN_PERFECT_PENALTY\n : 0)\n );\n}\n\n/**\n * The whole score of one move: its rule-violation weight, the structural\n * quality of the chord arrived on, and the distance the voices travelled.\n */\nexport function moveScore(\n scoring: MoveScoring,\n prev: ArrayLike<number>,\n prevOffset: number,\n cur: ArrayLike<number>,\n curOffset: number,\n voices: number,\n): number {\n return (\n structuralPenalty(scoring.structure, cur, curOffset, voices) +\n leadingCost(prev, prevOffset, cur, curOffset, voices) +\n violationWeight(scoring, prev, prevOffset, cur, curOffset, voices)\n );\n}\n","import { InvalidInputError, NoSolutionError } from '../../core/errors/index.js';\nimport {\n assertArray,\n assertFiniteNumber,\n assertGenerationBudget,\n assertOptions,\n assertPositiveInt,\n} from '../../core/validation/index.js';\nimport type { Chord } from '../chord/index.js';\nimport { type KeyLike, type ResolvedKey, resolveKey } from '../scale/index.js';\nimport { type ChordLike, formatChordSymbol, toChordData } from '../symbol/index.js';\nimport {\n createCandidateBuffer,\n DEFAULT_MAX_SPACING,\n enumerateVoicings,\n leadingCost,\n MAX_LOOKAHEAD_PAIRS,\n type MoveScoring,\n moveScore,\n moveScoring,\n structuralPenalty,\n type VoicingCandidates,\n violationWeight,\n} from './internal.js';\n\n/**\n * An inclusive MIDI pitch range for a single voice.\n *\n * @category Voicing & Counterpoint\n */\nexport type VoiceRange = {\n readonly min: number;\n readonly max: number;\n};\n\n/**\n * Default four-voice SATB ranges, ascending (index 0 = lowest):\n * bass E2–C4 (40–60), tenor C3–G4 (48–67), alto G3–D5 (55–74),\n * soprano C4–G5 (60–79).\n *\n * @category Voicing & Counterpoint\n */\nexport const SATB_RANGES: readonly Readonly<VoiceRange>[] = Object.freeze([\n Object.freeze({ min: 40, max: 60 }),\n Object.freeze({ min: 48, max: 67 }),\n Object.freeze({ min: 55, max: 74 }),\n Object.freeze({ min: 60, max: 79 }),\n]);\n\n/**\n * Options controlling {@link voiceChord} and {@link voiceProgression}.\n *\n * @category Voicing & Counterpoint\n */\nexport type VoicingOptions = {\n /**\n * Number of voices to realize. Ignored when `ranges` is given.\n *\n * @defaultValue 4\n */\n voices?: number;\n /** Explicit per-voice ranges, ascending (index 0 = lowest). Takes precedence over `voices`. */\n ranges?: VoiceRange[];\n /**\n * Maximum spacing in semitones between adjacent upper voices.\n *\n * @defaultValue 12\n */\n maxSpacing?: number;\n /**\n * Maximum candidate voicings evaluated for one chord.\n *\n * Raise this for large voicings when exact optimum matters more than bounded\n * search time.\n *\n * @defaultValue 4000\n */\n maxCandidates?: number;\n /**\n * Maximum number of chords {@link voiceProgression} will voice. The search\n * per chord is bounded internally, so the cost of a progression is linear in\n * its length; this is the guard against an unbounded caller, not a limit on\n * the search.\n *\n * @defaultValue 1000000\n */\n budget?: number;\n /**\n * The prevailing key, as a key name, a key/scale, or a `Key`. Supplying it\n * enables the rules that only make sense relative to a tonic: the leading\n * tone is not doubled, and — where the chord being left is known as well —\n * it is not left unresolved. Without it, voicings are chosen from chord\n * structure and voice-leading distance alone.\n */\n key?: KeyLike;\n /**\n * The chord the current voicing was written on, as a chord symbol, chord\n * data, or a `Chord`.\n *\n * It enables every rule that is read from the chord being left: the\n * resolution of a chordal seventh, the resolution of a leading tone, and the\n * cross relation between the two chords. All three are scored exactly as\n * {@link voiceProgression} scores them, and none of them is scored without\n * it — a leading tone cannot be told from the third of a chord that merely\n * contains it until the chord it belongs to is known. Supply it whenever the\n * caller has the chord, not only where that chord carried a seventh.\n */\n previousChord?: ChordLike;\n};\n\n/** Overall pitch floor/ceiling used when deriving ranges for arbitrary voice counts. */\nconst DERIVED_LOW = 40;\nconst DERIVED_HIGH = 79;\n/** Span of each derived per-voice range in semitones. */\nconst DERIVED_SPAN = 19;\n\n/**\n * Resolve the per-voice ranges implied by the options: explicit `ranges` win,\n * four voices use {@link SATB_RANGES}, and other counts get evenly spaced\n * ranges spanning roughly the bass-to-soprano compass.\n */\nexport function resolveRanges(opts?: VoicingOptions): VoiceRange[] {\n // Every voicing entrance resolves its ranges, so the options bag they share is\n // read as one here rather than at four entrances separately.\n assertOptions(opts, 'opts');\n if (opts?.ranges !== undefined) {\n if (opts.ranges.length === 0) {\n throw new InvalidInputError('ranges must contain at least one voice range');\n }\n assertGenerationBudget(opts.ranges.length, 'voice ranges', 128);\n return opts.ranges.map((range, index) => {\n assertFiniteNumber(range.min, `ranges[${index}].min`);\n assertFiniteNumber(range.max, `ranges[${index}].max`);\n if (range.min > range.max) {\n throw new InvalidInputError(`ranges[${index}].min must not exceed max`);\n }\n assertGenerationBudget(\n Math.floor(range.max) - Math.ceil(range.min) + 1,\n `ranges[${index}] span`,\n 4096,\n );\n return { ...range };\n });\n }\n const voices = opts?.voices ?? 4;\n assertPositiveInt(voices, 'voices', 128);\n if (voices === 4) {\n return SATB_RANGES.map((range) => ({ ...range }));\n }\n if (voices === 1) {\n return [{ min: DERIVED_LOW, max: DERIVED_HIGH }];\n }\n const ranges: VoiceRange[] = [];\n for (let i = 0; i < voices; i += 1) {\n const min = Math.round(\n DERIVED_LOW + (i * (DERIVED_HIGH - DERIVED_SPAN - DERIVED_LOW)) / (voices - 1),\n );\n ranges.push({ min, max: min + DERIVED_SPAN });\n }\n return ranges;\n}\n\n/**\n * Resolve and validate the adjacent-voice spacing limit. Shared by every entry\n * point so `nextVoicing` cannot accept a NaN that silently disables the spacing\n * constraint, nor a negative value that reports itself as an impossible range.\n */\nexport function resolveMaxSpacing(opts?: VoicingOptions): number {\n const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;\n assertFiniteNumber(maxSpacing, 'maxSpacing');\n if (maxSpacing < 0) {\n throw new InvalidInputError('maxSpacing must be non-negative');\n }\n return maxSpacing;\n}\n\n/**\n * Resolve the prevailing key once per entry point, so the scoring tables below\n * are handed one key rather than re-reading whatever form the caller wrote.\n *\n * The whole key, spelling and all: the search grades the letters it writes, and\n * a key handed in as an Ab minor would otherwise be scored on the sharps a G#\n * minor spells.\n */\nexport function resolvedKeyOf(opts?: VoicingOptions): ResolvedKey | undefined {\n return opts?.key === undefined ? undefined : resolveKey(opts.key);\n}\n\n/** Resolve the chord a `nextVoicing` caller says the current voicing came from. */\nexport function resolvePreviousChord(opts?: VoicingOptions): Chord | undefined {\n return opts?.previousChord === undefined ? undefined : toChordData(opts.previousChord);\n}\n\n/** Resolve and validate the per-chord candidate-search cap. */\nexport function resolveMaxCandidates(opts?: VoicingOptions): number | undefined {\n if (opts?.maxCandidates === undefined) return undefined;\n assertPositiveInt(opts.maxCandidates, 'maxCandidates', 1_000_000);\n return opts.maxCandidates;\n}\n\n/**\n * How far a candidate sits from the middle of each voice's range, summed.\n *\n * A chord with nothing before it is placed by this alone once its structure is\n * settled: preferring pitches near the middle of every range is what makes the\n * default voicing centered and compact rather than pushed to one end of the\n * compass.\n */\nfunction centeringPenalty(\n ranges: readonly VoiceRange[],\n pitches: ArrayLike<number>,\n offset: number,\n voices: number,\n): number {\n let penalty = 0;\n for (let voice = 0; voice < voices; voice += 1) {\n const pitch = pitches[offset + voice];\n const range = ranges[voice];\n if (pitch === undefined || range === undefined) {\n continue;\n }\n penalty += Math.abs(pitch - (range.min + range.max) / 2);\n }\n return penalty;\n}\n\n/**\n * Realize a single chord as one MIDI pitch per voice, ascending (index 0 =\n * lowest). The bass voice takes the chord's `bassPc` when set, otherwise the\n * root; upper voices take chord pitch classes, doubling the root or fifth as\n * needed to fill all voices. The result stays inside each voice's range, keeps\n * adjacent upper voices within `maxSpacing`, avoids voice crossing, and is\n * deterministic: a compact close-position voicing centered in the ranges.\n *\n * @param chord The chord to voice.\n * @param opts Voicing options; defaults to four voices in {@link SATB_RANGES}.\n * @returns MIDI pitches, ascending, one per voice.\n * @throws If no voicing fits the given ranges.\n * @example\n * ```ts\n * import { parseChordSymbol, voiceChord } from '@libraz/libcantus';\n * const chord = parseChordSymbol('Cmaj7');\n * voiceChord(chord); // four ascending MIDI pitches within the SATB ranges\n * ```\n * @category Voicing & Counterpoint\n */\nexport function voiceChord(chord: ChordLike, opts?: VoicingOptions): number[] {\n const data = toChordData(chord);\n const ranges = resolveRanges(opts);\n const maxSpacing = resolveMaxSpacing(opts);\n const candidates = enumerateVoicings(data, ranges, maxSpacing, resolveMaxCandidates(opts));\n const { structure } = moveScoring(undefined, data, resolvedKeyOf(opts));\n const { pitches, voices } = candidates;\n let bestOffset = -1;\n let bestScore = Number.POSITIVE_INFINITY;\n for (let index = 0; index < candidates.count; index += 1) {\n const offset = index * voices;\n const score =\n structuralPenalty(structure, pitches, offset, voices) +\n centeringPenalty(ranges, pitches, offset, voices);\n if (score < bestScore) {\n bestScore = score;\n bestOffset = offset;\n }\n }\n if (bestOffset < 0) {\n throw new NoSolutionError(\n `no voicing satisfies the given ranges for ${formatChordSymbol(chord)}`,\n );\n }\n return [...pitches.subarray(bestOffset, bestOffset + voices)];\n}\n\n/**\n * Voice a chord progression with smooth voice leading. Each chord picks, from a\n * bounded deterministic candidate set, the voicing minimizing the voice-leading\n * cost from the previous voicing plus a large penalty per counterpoint\n * violation (parallel perfects/octaves, voice crossing, voice overlap, and\n * over-wide upper-voice spacing).\n *\n * The choice also weighs the chord that follows. A voicing costs whatever the\n * move onto the next chord will cost in broken rules, so a placement that is\n * marginally smoother now but strands a voice on a leap it cannot write cleanly\n * loses to the one that leaves the line somewhere the next chord can be reached\n * from. The lookahead is one chord deep and reads only the violation weight of\n * the connection, so a chord is otherwise voiced exactly as it would be on its\n * own merits.\n *\n * Tendency tones are resolved rather than merely moved economically: the voice\n * holding a chordal seventh falls by step unless the next chord keeps that tone,\n * and — when `opts.key` is given — the leading tone rises to the tonic and is\n * never doubled, wherever it is functioning as a leading tone rather than\n * sounding as an ordinary tone of some other chord. A key also names the letters\n * each voice is written with, which is what lets the search refuse the augmented\n * second between the lowered sixth and the raised seventh that a minor-key line\n * otherwise falls into. Without a key neither rule can apply, since nothing\n * identifies which pitch class is the leading tone or how a tone is spelled.\n *\n * @param chords The chords to voice in order.\n * @param opts Voicing options; defaults to four voices in {@link SATB_RANGES}.\n * @returns One voicing per chord, each ascending with one MIDI pitch per voice.\n * @throws If any chord admits no voicing within the given ranges, or if the\n * progression is longer than `opts.budget` chords.\n * @example\n * ```ts\n * import { parseChordSymbol, voiceProgression } from '@libraz/libcantus';\n * const chords = ['C', 'Am', 'F', 'G'].map((s) => parseChordSymbol(s));\n * voiceProgression(chords); // one four-voice voicing per chord, smoothly led\n * ```\n * @category Voicing & Counterpoint\n */\n/** The unsatisfiable-constraints error for one chord of a progression. */\nfunction noSolutionAt(index: number, chord: Chord): NoSolutionError {\n return new NoSolutionError(\n `no voicing satisfies the given ranges for ${formatChordSymbol(chord)} at index ${index}`,\n { at: index },\n );\n}\n\n/**\n * Run one chord's work, re-raising an unsatisfiable-constraints failure with\n * the chord it happened on. Voicing a whole lead sheet is otherwise told only\n * that some chord did not fit.\n */\nfunction locate<T>(index: number, chord: Chord, work: () => T): T {\n try {\n return work();\n } catch (error) {\n if (error instanceof NoSolutionError) {\n throw noSolutionAt(index, chord);\n }\n throw error;\n }\n}\n\n/** The chord after the one being voiced: its candidates, and how a move onto it scores. */\ntype Lookahead = {\n /** The next chord's candidate voicings. */\n candidates: VoicingCandidates;\n /** What a move from the chord being voiced onto that chord is scored against. */\n scoring: MoveScoring;\n};\n\n/**\n * The violation weight a candidate commits the next chord to: the weight of the\n * connection the following step would write, arriving from this candidate.\n *\n * The following step chooses by the whole score, so the connection weighed here\n * is the one it will actually pick rather than the cleanest one it could reach —\n * a lookahead that reported an unreachable best would recommend placements the\n * search then declines to use.\n */\nfunction successorWeight(\n next: Lookahead,\n cur: ArrayLike<number>,\n curOffset: number,\n voices: number,\n): number {\n const { candidates, scoring } = next;\n const { pitches } = candidates;\n let bestScore = Number.POSITIVE_INFINITY;\n let bestWeight = 0;\n for (let candidate = 0; candidate < candidates.count; candidate += 1) {\n const offset = candidate * voices;\n const weight = violationWeight(scoring, cur, curOffset, pitches, offset, voices);\n const score =\n weight +\n structuralPenalty(scoring.structure, pitches, offset, voices) +\n leadingCost(cur, curOffset, pitches, offset, voices);\n if (score < bestScore) {\n bestScore = score;\n bestWeight = weight;\n }\n }\n return bestWeight;\n}\n\n/**\n * Voice a chord progression with smooth, bounded SATB-style leading.\n *\n * Each chord is chosen from a deterministic candidate set, minimizing motion\n * while penalizing counterpoint violations, and weighing the violations the\n * move onto the following chord would then be forced into. A supplied key\n * additionally resolves leading tones and chordal sevenths.\n *\n * @param chords The chords to voice in order.\n * @param opts Voicing options; defaults to four voices in {@link SATB_RANGES}.\n * @returns One ascending MIDI voicing per chord.\n * @throws {@link NoSolutionError} with the failing chord index when a chord\n * cannot fit the requested ranges.\n * @category Voicing & Counterpoint\n */\nexport function voiceProgression(chords: readonly ChordLike[], opts?: VoicingOptions): number[][] {\n const progression = assertArray<ChordLike>(chords, 'chords');\n assertGenerationBudget(progression.length, 'voiced progression chords', opts?.budget);\n const data = progression.map((chord) => toChordData(chord));\n const key = resolvedKeyOf(opts);\n const ranges = resolveRanges(opts);\n const maxSpacing = resolveMaxSpacing(opts);\n const maxCandidates = resolveMaxCandidates(opts);\n const result: number[][] = [];\n const first = data[0];\n if (first === undefined) {\n return result;\n }\n // Two candidate buffers serve the whole progression: the chord being voiced,\n // and the one looked ahead to. Each step hands its lookahead buffer on as the\n // next step's own, so every chord is enumerated once and a lead sheet\n // allocates two buffers rather than two per chord.\n let current = locate(0, first, () =>\n enumerateVoicings(first, ranges, maxSpacing, maxCandidates, createCandidateBuffer()),\n );\n let spare = createCandidateBuffer();\n // The scoring of a move onto the chord being voiced is the very context its\n // predecessor built to look ahead with, so it is carried forward rather than\n // rebuilt.\n let scoring = moveScoring(undefined, first, key);\n let prev: number[] | undefined;\n for (const [index, chord] of data.entries()) {\n const nextChord = data[index + 1];\n const lookahead: Lookahead | undefined =\n nextChord === undefined\n ? undefined\n : {\n candidates: locate(index + 1, nextChord, () =>\n enumerateVoicings(nextChord, ranges, maxSpacing, maxCandidates, spare),\n ),\n // The chord after the one being reached goes in too: it is what\n // says whether the chromatic tone the next chord introduces is the\n // one an applied dominant is written with.\n scoring: moveScoring(chord, nextChord, key, data[index + 2]),\n };\n const { pitches, voices, count } = current;\n // A lookahead weighs every pair of candidates, so it is dropped wherever the\n // pairs would outgrow the bounded per-chord search — for the whole chord, so\n // no candidate is judged on terms the others were not.\n const weighed =\n lookahead !== undefined && count * lookahead.candidates.count <= MAX_LOOKAHEAD_PAIRS\n ? lookahead\n : undefined;\n let bestOffset = -1;\n let bestScore = Number.POSITIVE_INFINITY;\n for (let candidate = 0; candidate < count; candidate += 1) {\n const offset = candidate * voices;\n let score =\n prev === undefined\n ? structuralPenalty(scoring.structure, pitches, offset, voices) +\n centeringPenalty(ranges, pitches, offset, voices)\n : moveScore(scoring, prev, 0, pitches, offset, voices);\n if (weighed !== undefined) {\n score += successorWeight(weighed, pitches, offset, voices);\n }\n if (score < bestScore) {\n bestScore = score;\n bestOffset = offset;\n }\n }\n if (bestOffset < 0) {\n throw noSolutionAt(index, chord);\n }\n const best = [...pitches.subarray(bestOffset, bestOffset + voices)];\n result.push(best);\n prev = best;\n if (lookahead !== undefined) {\n spare = current;\n current = lookahead.candidates;\n scoring = lookahead.scoring;\n }\n }\n return result;\n}\n","import { InvalidInputError, NoSolutionError } from '../../core/errors/index.js';\nimport { assertArray, assertFiniteNumber, assertOptions } from '../../core/validation/index.js';\nimport { type ChordLike, toChordData } from '../symbol/index.js';\nimport { enumerateVoicings, leadingCost, moveScore, moveScoring } from './internal.js';\nimport type { VoicingOptions } from './satb.js';\nimport {\n resolvedKeyOf,\n resolveMaxCandidates,\n resolveMaxSpacing,\n resolvePreviousChord,\n resolveRanges,\n} from './satb.js';\n\n/**\n * Read a voicing as one: an array, holding a pitch in every voice.\n *\n * Both entrances here walk a voicing pitch by pitch and do arithmetic on what\n * they find, and arithmetic coerces — a voice holding a `null` measures as\n * having moved nowhere, and one holding a string as `NaN` that then loses every\n * comparison it takes part in.\n */\nfunction assertVoicing(pitches: readonly number[], name: string): readonly number[] {\n const voices = assertArray<number>(pitches, name);\n for (let index = 0; index < voices.length; index += 1) {\n assertFiniteNumber(voices[index] ?? Number.NaN, `${name}[${index}]`);\n }\n return voices;\n}\n\n/**\n * Total voice-leading cost between two voicings: the sum of absolute semitone\n * motion across voices, and nothing besides. The rules the search also weighs —\n * parallels, hidden perfects, unresolved tendency tones — are judged and\n * reported by {@link checkPartWriting}, so measuring distance stays a\n * measurement. The arrays must be the same length; when they differ the\n * voicings are not comparable and the cost is `Infinity`.\n *\n * @param from The previous voicing, one MIDI pitch per voice.\n * @param to The next voicing, one MIDI pitch per voice.\n * @returns The summed absolute motion, or `Infinity` when lengths differ.\n * @category Voicing & Counterpoint\n */\nexport function voiceLeadingCost(from: number[], to: number[]): number {\n // The voices are subtracted from one another, and subtraction coerces: a\n // voicing holding a null would be measured as having moved nowhere.\n const previous = assertVoicing(from, 'from');\n const next = assertVoicing(to, 'to');\n if (previous.length !== next.length) {\n return Number.POSITIVE_INFINITY;\n }\n return leadingCost(previous, 0, next, 0, previous.length);\n}\n\n/**\n * Voice a single chord to follow smoothly from an arbitrary current voicing.\n *\n * Candidate voicings of `chord` are enumerated within ranges taken from `opts`,\n * or — when neither `voices` nor `ranges` is given — from a one-octave window\n * around each pitch of `current` (so the result matches `current`'s voice\n * count). Each candidate is scored by structural quality, voice-leading motion\n * from `current`, and a large penalty per counterpoint violation; the lowest\n * scoring candidate is returned, ascending.\n *\n * The rules read from the chord being left — the resolution of a chordal\n * seventh, the resolution of a leading tone, and the cross relation between the\n * two chords — are scored only when `opts.previousChord` names it: what a\n * voicing is written on cannot be read back from the pitches it holds. An\n * editor moving from one chord to the next has that chord, and passing it is\n * what keeps this generator and {@link checkPartWriting} agreeing about the\n * pair.\n *\n * @param current The current voicing to lead from, ascending (index 0 = lowest).\n * @param chord The next chord to voice, as a chord symbol, chord data, or a\n * `Chord`.\n * @param opts Voicing options; when omitted, ranges follow `current`'s span.\n * @returns The chosen voicing, ascending with one MIDI pitch per voice.\n * @throws If `current` is empty or holds a non-finite pitch while the ranges\n * are derived from it, if it asks for more voices than the search will take,\n * or if no voicing fits the ranges.\n * @category Voicing & Counterpoint\n */\nexport function nextVoicing(current: number[], chord: ChordLike, opts?: VoicingOptions): number[] {\n assertOptions(opts, 'opts');\n assertVoicing(current, 'current');\n const data = toChordData(chord);\n const derived = opts?.ranges === undefined && opts?.voices === undefined;\n if (derived) {\n // The ranges are read off `current`, so an empty voicing would describe a\n // zero-voice chord and return an empty result — the same input that throws\n // when written as an explicit `{ ranges: [] }`.\n if (current.length === 0) {\n throw new InvalidInputError('current must contain at least one pitch');\n }\n assertVoicing(current, 'current');\n }\n // Both branches end in the same resolver, so the voice-count budget and the\n // range checks apply wherever the ranges came from: a texture read off\n // `current` is no smaller a search than the same one written out as `ranges`.\n const ranges = derived\n ? resolveRanges({\n ranges: current.map((pitch) => {\n // Window one octave around each current pitch, clamped so extreme-low\n // or extreme-high input can never yield MIDI outside [0, 127].\n const centre = Math.min(127, Math.max(0, pitch));\n return { min: Math.max(0, centre - 12), max: Math.min(127, centre + 12) };\n }),\n })\n : resolveRanges(opts);\n assertVoicing(current, 'current');\n if (current.length < ranges.length) {\n throw new InvalidInputError(\n `current has ${current.length} voices but the requested ranges require ${ranges.length}`,\n );\n }\n // Thinning an existing texture is a normal use case (SATB to three voices,\n // for example). Keep the outer voices and select evenly spaced inner voices\n // so motion is still measured between like voice roles.\n const source =\n current.length === ranges.length\n ? current\n : ranges.length === 1\n ? [current[0] ?? Number.NaN]\n : ranges.map((_, index) => {\n const sourceIndex = Math.round((index * (current.length - 1)) / (ranges.length - 1));\n return current[sourceIndex] ?? Number.NaN;\n });\n const maxSpacing = resolveMaxSpacing(opts);\n const candidates = enumerateVoicings(data, ranges, maxSpacing, resolveMaxCandidates(opts));\n const key = resolvedKeyOf(opts);\n // The chord the current voicing came from is optional, so the line it is\n // leaving is spelled against that chord when the caller named one and by the\n // key alone when it did not. Only one chord is being voiced, so there is\n // nothing after it to look ahead to; the move is otherwise scored exactly as\n // one step of a progression is.\n const scoring = moveScoring(resolvePreviousChord(opts), data, key);\n const { pitches, voices } = candidates;\n let bestOffset = -1;\n let bestScore = Number.POSITIVE_INFINITY;\n for (let candidate = 0; candidate < candidates.count; candidate += 1) {\n const offset = candidate * voices;\n const score = moveScore(scoring, source, 0, pitches, offset, voices);\n if (score < bestScore) {\n bestScore = score;\n bestOffset = offset;\n }\n }\n if (bestOffset < 0) {\n throw new NoSolutionError('no voicing satisfies the given ranges');\n }\n return [...pitches.subarray(bestOffset, bestOffset + voices)];\n}\n","import { InvalidInputError } from '../../core/errors/index.js';\nimport { assertFiniteNumber, assertInteger, assertOptions } from '../../core/validation/index.js';\nimport { chordToneRole } from '../chord/index.js';\nimport { type ChordLike, toChordData } from '../symbol/index.js';\nimport { pitchClass } from './internal.js';\n\n/**\n * A tertian voicing style for {@link voiceChordStyled}.\n *\n * - `close`: the plain close-position tertian stack.\n * - `drop2`: the second voice from the top dropped an octave (drop-2 voicing).\n * It takes at least three voices, since the voice it names has to have\n * another below it for the drop to open the stack at all.\n * - `drop3`: the third voice from the top dropped an octave (drop-3 voicing).\n * It takes at least four voices, for the same reason.\n * - `shell`: root plus guide tones (third and seventh) for seventh chords, or\n * root/third/fifth for triads; the fifth and tensions are omitted.\n * - `rootless`: the root omitted, keeping third/fifth/seventh and tensions\n * (a typical left-hand jazz voicing).\n *\n * A chord with too few voices for the drop it asks for is voiced in close\n * position instead: a three-note `drop3` is the close stack, not a stack with\n * its bottom voice pushed an octave down. A slash bass the chord does not\n * contain sounds under the stack rather than in it, and is not one of the\n * voices counted here.\n *\n * @category Voicing & Counterpoint\n */\nexport type VoicingStyle = 'close' | 'drop2' | 'drop3' | 'shell' | 'rootless';\n\n/**\n * Options controlling {@link voiceChordStyled}.\n *\n * @category Voicing & Counterpoint\n */\nexport type StyledVoicingOptions = {\n /**\n * Voicing style to build.\n *\n * @defaultValue 'close'\n */\n style?: VoicingStyle;\n /** Constrain the highest voice to this pitch class (0..11) when given. */\n topNote?: number;\n /**\n * Base octave in scientific pitch notation: octave 4 starts the stack near\n * middle C (MIDI 60), i.e. near `12 * (octave + 1)`.\n *\n * @defaultValue 4\n */\n octave?: number;\n /**\n * Omit the root regardless of style (relevant for jazz voicings).\n *\n * @defaultValue false\n */\n rootless?: boolean;\n};\n\n/** Default base octave for a styled close-position stack. */\nconst DEFAULT_STYLE_OCTAVE = 4;\n\n/**\n * Which voice of the close stack, counted from the top, a style lowers an\n * octave; 0 for a style that lowers none.\n */\nfunction dropPositionFromTop(style: VoicingStyle): number {\n if (style === 'drop2') {\n return 2;\n }\n return style === 'drop3' ? 3 : 0;\n}\n\n/** Return the pitch classes of a list in order, without duplicates. */\nfunction dedupePcs(pcs: number[]): number[] {\n const seen = new Set<number>();\n const result: number[] = [];\n for (const pc of pcs) {\n if (!seen.has(pc)) {\n seen.add(pc);\n result.push(pc);\n }\n }\n return result;\n}\n\n/** The lowest MIDI pitch of a pitch class at or above a floor. */\nfunction lowestPitchAtOrAbove(pc: number, floor: number): number {\n return floor + ((((pc - pitchClass(floor)) % 12) + 12) % 12);\n}\n\n/** The pitch class in `pcs` whose circular distance to `target` is smallest. */\nfunction nearestPc(target: number, pcs: number[]): number {\n let best = pcs[0] ?? target;\n let bestDist = 12;\n for (const pc of pcs) {\n const dist = Math.min((((pc - target) % 12) + 12) % 12, (((target - pc) % 12) + 12) % 12);\n if (dist < bestDist) {\n bestDist = dist;\n best = pc;\n }\n }\n return best;\n}\n\n/**\n * Realize a single chord as an explicit tertian voicing in a chosen style,\n * independent of the SATB range search used by {@link voiceChord}.\n *\n * The chord tones are stacked in close position from the bass (`bassPc`, else\n * `rootPc`) in the given scientific-pitch `octave` upward (octave 4 starts the\n * bass near C4 = MIDI 60, matching {@link midiToNote} and the bass module).\n * When `topNote` is given the stack is rotated so the highest voice's pitch\n * class equals that note (or the nearest chord tone). The chosen\n * {@link VoicingStyle} then transforms the stack, and the result is returned as\n * ascending MIDI pitches (index 0 = lowest).\n *\n * A drop voicing lowers one voice below the rest, so that voice is its bass —\n * which is what tells the drop-2 voicings of one chord apart. When the chord\n * names a bass tone of its own, the close stack is therefore rotated so the\n * style drops that tone, and the result is the drop voicing whose bass is the\n * tone asked for; the named bass is not lowered under an already dropped voice\n * afterwards, which would leave a hole wider than an octave in the middle. A\n * bass the chord does not contain is no voice of the stack and cannot be the\n * dropped one, so it keeps the bottom and the drop happens above it.\n *\n * Every returned pitch is a valid MIDI number: an octave whose stack would run\n * off either end of the 0..127 range is rejected rather than voiced out of\n * range, matching {@link nextVoicing}, which clamps its derived ranges.\n *\n * @param given The chord to voice, in whatever form it is held: a chord symbol,\n * plain chord data, or a `Chord`.\n * @param opts Styled voicing options; defaults to a close voicing at octave 4.\n * @returns MIDI pitches, ascending, one per retained voice.\n * @throws If the voicing would not fit inside MIDI 0..127 at the given octave.\n * @example\n * ```ts\n * import { voiceChordStyled } from '@libraz/libcantus';\n * voiceChordStyled('Dm7', { style: 'drop2' }); // ascending MIDI pitches, drop-2 voicing\n * ```\n * @category Voicing & Counterpoint\n */\nexport function voiceChordStyled(given: ChordLike, opts?: StyledVoicingOptions): number[] {\n assertOptions(opts, 'opts');\n const chord = toChordData(given);\n const style = opts?.style ?? 'close';\n const octave = opts?.octave ?? DEFAULT_STYLE_OCTAVE;\n assertInteger(octave, 'styled voicing octave', -10, 10);\n if (opts?.topNote !== undefined) {\n assertFiniteNumber(opts.topNote, 'styled voicing topNote');\n }\n const base = 12 * (octave + 1);\n const rootPc = pitchClass(chord.rootPc);\n const bassPc = pitchClass(chord.bassPc ?? chord.rootPc);\n const omitRoot = style === 'rootless' || opts?.rootless === true;\n\n // Tertian chord tones in order, with the bass tone rotated to the bottom.\n let order = dedupePcs(chord.intervals.map((interval) => pitchClass(chord.rootPc + interval)));\n const bassIndex = order.indexOf(bassPc);\n const bassIsChordTone = bassIndex >= 0;\n if (bassIndex > 0) {\n order = [...order.slice(bassIndex), ...order.slice(0, bassIndex)];\n } else if (bassIndex < 0) {\n order = [bassPc, ...order];\n }\n\n if (style === 'shell') {\n const roles = order.map((pc) => chordToneRole(pc, chord));\n // The guide tone is the seventh when present; on a sixth chord the sixth\n // stands in for it, and otherwise the voicing falls back to the fifth.\n const guide = roles.includes('seventh')\n ? 'seventh'\n : roles.includes('sixth')\n ? 'sixth'\n : 'fifth';\n order = order.filter((_pc, i) => {\n const role = roles[i];\n return _pc === bassPc || role === 'root' || role === 'third' || role === guide;\n });\n }\n\n if (omitRoot) {\n order = order.filter((pc) => pc !== rootPc || (chord.bassPc !== undefined && pc === bassPc));\n }\n\n if (order.length === 0) {\n return [];\n }\n\n if (opts?.topNote !== undefined) {\n const target = order.includes(pitchClass(opts.topNote))\n ? pitchClass(opts.topNote)\n : nearestPc(pitchClass(opts.topNote), order);\n const targetIndex = order.indexOf(target);\n order = [...order.slice(targetIndex + 1), ...order.slice(0, targetIndex + 1)];\n }\n\n // A drop puts the voice it lowers under the whole stack, so on a drop voicing\n // the dropped voice is the bass. When the chord names a bass tone of its own,\n // rotate the close stack so the named tone is the one the style drops: the\n // requested bass then reaches the bottom through the drop itself, which is\n // also what tells the four drop-2 voicings of a seventh chord apart. Lowering\n // it afterwards instead, under a voice already dropped below it, would open a\n // hole wider than an octave inside a voicing whose point is compactness.\n // A rotation is a single choice, so this overrides any `topNote` rotation.\n const dropFromTop = dropPositionFromTop(style);\n const dropsToNamedBass =\n dropFromTop > 0 && bassIsChordTone && chord.bassPc !== undefined && order.length > dropFromTop;\n if (dropsToNamedBass) {\n const bassAt = order.indexOf(bassPc);\n if (bassAt >= 0) {\n const shift =\n (((bassAt - (order.length - dropFromTop)) % order.length) + order.length) % order.length;\n order = [...order.slice(shift), ...order.slice(0, shift)];\n }\n }\n\n // Stack the ordered pitch classes upward in close position.\n const stack: number[] = [];\n let prev: number | undefined;\n for (const pc of order) {\n if (prev === undefined) {\n prev = lowestPitchAtOrAbove(pc, base);\n } else {\n let delta = (((pc - pitchClass(prev)) % 12) + 12) % 12;\n if (delta === 0) {\n delta = 12;\n }\n prev += delta;\n }\n stack.push(prev);\n }\n\n // The dropped voice needs another voice below it, or the drop only moves the\n // bottom of the stack down an octave and leaves the rest where it was, which\n // opens the stack nowhere. A chord that short is voiced in close position.\n // A bass the chord does not contain is no part of the stacked chord tones —\n // it sounds under them — so it is neither dropped nor counted as that voice.\n const stackedFloor = bassIsChordTone ? 0 : 1;\n if (dropFromTop > 0 && stack.length - dropFromTop > stackedFloor) {\n const dropIndex = stack.length - dropFromTop;\n stack[dropIndex] = (stack[dropIndex] ?? 0) - 12;\n }\n\n // An explicit slash bass is a structural requirement, including when it is\n // not a chord member — and a bass that is no chord tone cannot be the voice a\n // drop lowers, so it stays where the stack put it. Top-note rotation, or a\n // stack wide enough that the dropped voice lands above the bass, may leave a\n // different tone below it, so lower the retained bass by octaves before the\n // final sort. This gives the bass priority over a conflicting top-note hint.\n if (chord.bassPc !== undefined) {\n const bassIndexInStack = stack.findIndex((pitch) => pitchClass(pitch) === bassPc);\n if (bassIndexInStack >= 0) {\n const otherLowest = Math.min(\n ...stack.filter((_pitch, index) => index !== bassIndexInStack),\n Number.POSITIVE_INFINITY,\n );\n while ((stack[bassIndexInStack] ?? 0) >= otherLowest) {\n stack[bassIndexInStack] = (stack[bassIndexInStack] ?? 0) - 12;\n }\n }\n }\n\n const voicing = stack.sort((a, b) => a - b);\n const lowest = voicing[0] ?? 0;\n const highest = voicing[voicing.length - 1] ?? 0;\n if (lowest < 0 || highest > 127) {\n throw new InvalidInputError(\n `styled voicing at octave ${octave} spans MIDI ${lowest}..${highest}, outside 0..127`,\n );\n }\n return voicing;\n}\n","/**\n * Shared construction of {@link PartWritingViolation} records.\n *\n * The four-part checker and the species checker report the same violation type,\n * so they build it and name intervals the same way.\n */\n\nimport type { PartWritingViolation, PartWritingViolationKind } from './index.js';\n\n/**\n * Diatonic names of the interval numbers a line can span.\n *\n * Compound numbers are named too, because a leap of a ninth or a tenth is a\n * leap a rule fires on and the number is the whole of what the rationale is\n * saying: the fallback was the bare noun `interval`, which named nothing and\n * read as `The leap of a interval` where the sentence supplies the article.\n */\nconst INTERVAL_WORDS = [\n 'unison',\n 'second',\n 'third',\n 'fourth',\n 'fifth',\n 'sixth',\n 'seventh',\n 'octave',\n 'ninth',\n 'tenth',\n 'eleventh',\n 'twelfth',\n 'thirteenth',\n 'fourteenth',\n 'fifteenth',\n] as const;\n\n/**\n * Name an interval number.\n *\n * Past the range the words cover the number itself is written, as the sibling\n * rules that report a leap already do: `a 17th` names the interval, where a\n * noun that stands for every interval names none of them.\n */\nexport function intervalWord(numberValue: number): string {\n return INTERVAL_WORDS[numberValue - 1] ?? `${numberValue}th`;\n}\n\n/**\n * The article an interval name takes.\n *\n * Read from the name rather than from the number, so a name added to the list\n * carries its own article: `an octave`, `an eleventh`, `a ninth`.\n */\nexport function intervalArticle(word: string): string {\n return /^[aeiou]/.test(word) ? 'an' : 'a';\n}\n\n/** Build one violation record, so the field order is written once. */\nexport function violation(\n kind: PartWritingViolationKind,\n voices: number[],\n fromIndex: number,\n toIndex: number,\n rationale: string,\n): PartWritingViolation {\n return { kind, voices, fromIndex, toIndex, rationale };\n}\n","/**\n * Species counterpoint: mark a two-voice exercise against the sixteenth-century\n * rules.\n *\n * This is the conservatory exercise, not a general style checker. It grades a\n * counterpoint written above or below a given cantus firmus in one of the five\n * species, and reports what it finds as the same {@link PartWritingViolation}\n * the four-part checker uses.\n *\n * Both voices arrive as spelled notes, because half of what the rules forbid is\n * invisible in a pitch: the augmented second of the harmonic minor is a minor\n * third by ear, and the diminished fourth that may not stand as a consonance is\n * a major third by ear.\n */\n\nimport { InvalidInputError } from '../../core/errors/index.js';\nimport { ConsonanceClass } from '../../core/interval/index.js';\nimport { BEAT_EPS } from '../../core/meter/index.js';\nimport type { Note, SpelledInterval } from '../../core/pitch/index.js';\nimport {\n noteToMidi,\n noteToPitchClass,\n pitchClassOf,\n spelledInterval,\n} from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport { assertArray, assertOptions, describeRejected } from '../../core/validation/index.js';\nimport {\n classifySpelledInterval,\n createsBattuta,\n createsHiddenParallelPerfect,\n createsParallelPerfect,\n createsVoiceCrossing,\n isAugmentedMelodicInterval,\n isForbiddenMelodicLeap,\n} from '../counterpoint/index.js';\nimport { simpleIntervalNumber } from '../counterpoint/internal.js';\nimport { type KeyLike, toKeyScale } from '../scale/index.js';\nimport type { PartWritingViolation } from './index.js';\nimport { intervalArticle, intervalWord, violation } from './internal.js';\n\n/**\n * Which species an exercise is written in: note against note, two, three or\n * four notes against one, suspensions, or the florid mixture of them all.\n *\n * @category Voicing & Counterpoint\n */\nexport type Species = 1 | 2 | 3 | 4 | 5;\n\n/**\n * Options controlling {@link checkSpecies}.\n *\n * @category Voicing & Counterpoint\n */\nexport type SpeciesOptions = {\n /**\n * Length of each counterpoint note, measured in cantus-firmus notes: 1 is a\n * whole note filling a measure, 0.5 a half, 0.25 a quarter.\n *\n * The first four species have a fixed ratio, so the durations are implied by\n * the note count and this is only needed to describe something unusual. The\n * fifth species mixes note values by definition and cannot be read without it.\n */\n durations?: readonly number[];\n /**\n * Which side of the cantus firmus the counterpoint is written on. Defaults to\n * whichever side it actually lies on, by mean pitch.\n */\n counterpointAbove?: boolean;\n};\n\n/** Notes of counterpoint per cantus-firmus note, by species. */\nconst SPECIES_RATIO: Readonly<Record<Species, number>> = { 1: 1, 2: 2, 3: 4, 4: 2, 5: 0 };\n\n/** The species there are, read from the ratio table rather than listed twice. */\nconst SPECIES_NUMBERS: readonly number[] = Object.keys(SPECIES_RATIO).map(Number);\n\n/** The smallest note value the fifth species may write, in cantus-firmus notes. */\nconst MIN_FLORID_VALUE = 0.125;\n\n/**\n * One counterpoint note against one cantus-firmus note. A note that is still\n * sounding when the next cantus-firmus note is struck meets both of them, and\n * so has one entry per measure it sounds in: the first struck, the rest held.\n */\ntype Entry = {\n /** The counterpoint note itself. */\n note: Note;\n /** Its index in the counterpoint array. */\n index: number;\n /** Where this stretch of it begins, in cantus-firmus notes from the start. */\n onset: number;\n /** How long it goes on sounding from there, in cantus-firmus notes. */\n duration: number;\n /**\n * Whether the note is sounding on from an earlier attack rather than being\n * struck here — a ligature carried across the bar line. What it forms against\n * the new measure is judged as any other interval is; the melodic rules read\n * the attacks alone, since nothing was sung twice.\n */\n held: boolean;\n /** The cantus-firmus note sounding under it. */\n against: Note;\n /** Index of that cantus-firmus note. */\n againstIndex: number;\n /** Whether the note falls on the downbeat of its measure. */\n downbeat: boolean;\n /** The interval between the two voices, read from the lower note upward. */\n interval: SpelledInterval;\n /** How that interval sounds in two-voice counterpoint. */\n consonance: ConsonanceClass;\n};\n\n/** Mean sounding pitch of a line, for deciding which voice lies above. */\nfunction meanPitch(notes: readonly Note[]): number {\n if (notes.length === 0) {\n return 0;\n }\n return notes.reduce((sum, note) => sum + noteToMidi(note), 0) / notes.length;\n}\n\n/**\n * Note lengths implied by the species when the caller gives none.\n *\n * Every measure but the last carries the species' full ratio; whatever notes\n * are left share the closing measure, which lets an exercise end on the single\n * whole note convention asks for without being described note by note.\n */\nfunction impliedDurations(count: number, measures: number, ratio: number): number[] {\n const full = ratio * Math.max(measures - 1, 0);\n const closing = Math.max(count - full, 1);\n return Array.from({ length: count }, (_, index) => (index < full ? 1 / ratio : 1 / closing));\n}\n\n/** Whether a melodic interval is a diatonic step. */\nfunction isStep(interval: SpelledInterval): boolean {\n return interval.number === 2;\n}\n\n/** The direction a melodic interval moves: 1 up, -1 down, 0 for a repetition. */\nfunction direction(interval: SpelledInterval): number {\n return Math.sign(interval.semitones);\n}\n\n/** Whether two spelled notes are the same note, so a tie joins them. */\nfunction sameNote(a: Note, b: Note): boolean {\n return a.letter === b.letter && a.alter === b.alter && a.octave === b.octave;\n}\n\n/**\n * Whether a dissonance is passed through: approached and left by step in one\n * direction (a passing note) or by step and back (a neighbour note).\n *\n * The two are not licensed by the same species. A passing dissonance fills the\n * step between two consonances and every moving species writes one; a neighbour\n * turns back on the note it left, which belongs to the quarter-note line of the\n * third and fifth species. The second species moves a note against each half of\n * the measure and the only dissonance it may write is the passing one, so the\n * neighbour is read there as the dissonance it is.\n */\nfunction isPassedThrough(entries: readonly Entry[], position: number, species: Species): boolean {\n const previous = entries[position - 1];\n const current = entries[position];\n const next = entries[position + 1];\n if (previous === undefined || current === undefined || next === undefined) {\n return false;\n }\n const incoming = spelledInterval(previous.note, current.note);\n const outgoing = spelledInterval(current.note, next.note);\n if (!isStep(incoming) || !isStep(outgoing)) {\n return false;\n }\n if (direction(incoming) === direction(outgoing)) {\n return true;\n }\n return sameNote(previous.note, next.note) && (species === 3 || species === 5);\n}\n\n/** Whether a melodic interval leaps a third, the only leap the figures write. */\nfunction isThird(interval: SpelledInterval): boolean {\n return interval.number === 3;\n}\n\n/** The four notes of a figure, starting at one position, if the line has them. */\nfunction figureAt(\n entries: readonly Entry[],\n start: number,\n): [Entry, Entry, Entry, Entry] | undefined {\n const first = entries[start];\n const second = entries[start + 1];\n const third = entries[start + 2];\n const fourth = entries[start + 3];\n if (first === undefined || second === undefined || third === undefined || fourth === undefined) {\n return undefined;\n }\n return [first, second, third, fourth];\n}\n\n/**\n * Whether four notes spell the *nota cambiata*: a consonance, a dissonance\n * stepped down onto, a leap of a third down onto a consonance, and a step back\n * up.\n *\n * Fux grants the figure by name. The dissonance is quitted by leap, which every\n * other licence forbids, and the ear hears the note leapt to as the resolution\n * the step would have reached.\n */\nfunction isCambiata(entries: readonly Entry[], start: number): boolean {\n const figure = figureAt(entries, start);\n if (figure === undefined) {\n return false;\n }\n const [first, second, third, fourth] = figure;\n if (\n first.consonance === ConsonanceClass.Dissonance ||\n third.consonance === ConsonanceClass.Dissonance\n ) {\n return false;\n }\n const approach = spelledInterval(first.note, second.note);\n const leap = spelledInterval(second.note, third.note);\n const answer = spelledInterval(third.note, fourth.note);\n return (\n isStep(approach) &&\n direction(approach) < 0 &&\n isThird(leap) &&\n direction(leap) < 0 &&\n isStep(answer) &&\n direction(answer) > 0\n );\n}\n\n/**\n * Whether four notes spell the double neighbour: a step to one side of a note,\n * a leap of a third across to the other side, and a step back to the note the\n * figure left.\n *\n * Both notes in the middle decorate the same consonance, so either of them may\n * be the dissonance even though the figure moves between them by leap.\n */\nfunction isDoubleNeighbour(entries: readonly Entry[], start: number): boolean {\n const figure = figureAt(entries, start);\n if (figure === undefined) {\n return false;\n }\n const [first, second, third, fourth] = figure;\n if (first.consonance === ConsonanceClass.Dissonance || !sameNote(first.note, fourth.note)) {\n return false;\n }\n const away = spelledInterval(first.note, second.note);\n const across = spelledInterval(second.note, third.note);\n const back = spelledInterval(third.note, fourth.note);\n return (\n isStep(away) &&\n isThird(across) &&\n isStep(back) &&\n direction(away) !== 0 &&\n direction(across) === -direction(away)\n );\n}\n\n/**\n * Whether a dissonance stands inside one of the figures that quit a dissonance\n * by leap: the *nota cambiata* or the double neighbour.\n *\n * Both belong to the quarter-note line, so they are read in the third and fifth\n * species only. The second species moves a note against each half of the\n * measure and writes neither.\n */\nfunction isFiguredDissonance(\n entries: readonly Entry[],\n position: number,\n species: Species,\n): boolean {\n if (species !== 3 && species !== 5) {\n return false;\n }\n return (\n isCambiata(entries, position - 1) ||\n isDoubleNeighbour(entries, position - 1) ||\n isDoubleNeighbour(entries, position - 2)\n );\n}\n\n/** Where a species allows a dissonance to fall. */\ntype DissonanceLicence = 'none' | 'passing' | 'suspension';\n\n/** The licence a species grants at one metric position. */\nfunction licenceFor(species: Species, downbeat: boolean): DissonanceLicence {\n if (species === 1) {\n return 'none';\n }\n if (species === 4) {\n return downbeat ? 'suspension' : 'none';\n }\n if (species === 5) {\n return downbeat ? 'suspension' : 'passing';\n }\n return downbeat ? 'none' : 'passing';\n}\n\n/**\n * The rules judged on one counterpoint note: the vertical interval it forms,\n * and whether the voices have crossed.\n */\nfunction verticalViolations(\n entries: readonly Entry[],\n position: number,\n species: Species,\n counterpointAbove: boolean,\n crossed: Set<number>,\n): PartWritingViolation[] {\n const entry = entries[position];\n if (entry === undefined) {\n return [];\n }\n const found: PartWritingViolation[] = [];\n const upper = counterpointAbove ? entry.note : entry.against;\n const lower = counterpointAbove ? entry.against : entry.note;\n // Once per written note, not once per measure it sounds in: a note carried\n // across the bar line has an entry in each measure, and every field of a\n // crossing record comes from the note's own index, so a second one is a\n // duplicate a caller cannot tell from the first — two marks on one note in a\n // scoring UI, and the same error subtracted twice. The vertical interval\n // below is judged per measure, since a held note forms a new one there.\n if (!crossed.has(entry.index) && createsVoiceCrossing(upper, lower)) {\n crossed.add(entry.index);\n found.push(\n violation(\n 'voiceCrossing',\n [0, 1],\n entry.index,\n entry.index,\n 'The counterpoint crosses to the wrong side of the cantus firmus',\n ),\n );\n }\n if (entry.consonance !== ConsonanceClass.Dissonance) {\n return found;\n }\n const licence = licenceFor(species, entry.downbeat);\n if (\n licence === 'passing' &&\n (isPassedThrough(entries, position, species) || isFiguredDissonance(entries, position, species))\n ) {\n return found;\n }\n if (licence === 'suspension') {\n found.push(...suspensionViolations(entries, position));\n return found;\n }\n found.push(\n violation(\n 'unpreparedDissonance',\n [0, 1],\n entry.index,\n entry.index,\n licence === 'none'\n ? `A ${entry.interval.quality}${entry.interval.number} falls where this species allows only a consonance`\n : 'The dissonance is not passed through by step',\n ),\n );\n return found;\n}\n\n/**\n * The rules judged on a dissonant downbeat where the species writes\n * suspensions: it must be tied over from a consonance and fall by step onto the\n * next note.\n */\nfunction suspensionViolations(entries: readonly Entry[], position: number): PartWritingViolation[] {\n const entry = entries[position];\n if (entry === undefined) {\n return [];\n }\n const previous = entries[position - 1];\n const next = entries[position + 1];\n const prepared =\n previous !== undefined &&\n sameNote(previous.note, entry.note) &&\n previous.consonance !== ConsonanceClass.Dissonance;\n if (!prepared) {\n return [\n violation(\n 'unpreparedDissonance',\n [0, 1],\n entry.index,\n entry.index,\n 'The suspended dissonance is not tied over from a consonance',\n ),\n ];\n }\n const resolution = next === undefined ? undefined : spelledInterval(entry.note, next.note);\n if (\n next === undefined ||\n resolution === undefined ||\n !isStep(resolution) ||\n direction(resolution) >= 0\n ) {\n return [\n violation(\n 'unresolvedSuspension',\n [0, 1],\n entry.index,\n next === undefined ? entry.index : next.index,\n 'The suspension does not fall by step onto its resolution',\n ),\n ];\n }\n // The step down is only half of the resolution: what it falls onto has to be\n // the consonance the ear was waiting for. A suspension resolving to another\n // dissonance is reported here, on the suspension itself, rather than left to\n // whatever the note it lands on is charged with — in the fifth species that\n // note may be excused as a passing dissonance and nothing would be said at all.\n if (next.consonance === ConsonanceClass.Dissonance) {\n return [\n violation(\n 'unresolvedSuspension',\n [0, 1],\n entry.index,\n next.index,\n `The suspension falls onto a ${next.interval.quality}${next.interval.number}, which is no resolution`,\n ),\n ];\n }\n return [];\n}\n\n/** The rules judged along the counterpoint line itself. */\nfunction melodicViolations(from: Entry, to: Entry): PartWritingViolation[] {\n const interval = spelledInterval(from.note, to.note);\n if (isAugmentedMelodicInterval(from.note, to.note)) {\n return [\n violation(\n 'augmentedMelodicInterval',\n [1],\n from.index,\n to.index,\n `The counterpoint moves by an augmented ${intervalWord(interval.number)}`,\n ),\n ];\n }\n if (isForbiddenMelodicLeap(from.note, to.note)) {\n return [\n violation(\n 'illegalLeap',\n [1],\n from.index,\n to.index,\n `The counterpoint leaps by a ${interval.quality}${interval.number}`,\n ),\n ];\n }\n return [];\n}\n\n/** How many leaps in a row may carry the line the same way. */\nconst MAX_LEAPS_IN_ONE_DIRECTION = 2;\n\n/** How far leaps in one direction may carry the line in total, in semitones. */\nconst MAX_LEAP_CHAIN_SPAN = 12;\n\n/** The smallest leap that must be answered by a step the other way. */\nconst LEAP_NEEDING_ANSWER = 6;\n\n/** The tritone, in semitones, whichever way it is spelled. */\nconst TRITONE_SEMITONES = 6;\n\n/** One melodic move of the counterpoint, as the shape rules read it. */\ntype Move = {\n /** The note moved from. */\n from: Entry;\n /** The note moved to. */\n to: Entry;\n /** The interval between them. */\n interval: SpelledInterval;\n};\n\n/** Whether a melodic interval leaps rather than steps or repeats the note. */\nfunction isLeap(interval: SpelledInterval): boolean {\n return interval.number > 2;\n}\n\n/** Every move of the counterpoint, in order. */\nfunction melodicMoves(entries: readonly Entry[]): Move[] {\n const moves: Move[] = [];\n for (let position = 1; position < entries.length; position += 1) {\n const from = entries[position - 1];\n const to = entries[position];\n if (from === undefined || to === undefined) {\n continue;\n }\n moves.push({ from, to, interval: spelledInterval(from.note, to.note) });\n }\n return moves;\n}\n\n/**\n * The one high point: the line reaches its highest note once and does not\n * return to it.\n *\n * A note repeated in place is one arrival, and the closing note is left out of\n * the comparison — where the counterpoint ends is fixed by the cadence rather\n * than by the shape of the line, so a line that climbs to its final is not\n * charged with reaching its peak twice.\n */\nfunction climaxViolations(entries: readonly Entry[]): PartWritingViolation[] {\n const body = entries.slice(0, -1);\n const peak = body.reduce(\n (high, entry) => Math.max(high, noteToMidi(entry.note)),\n Number.NEGATIVE_INFINITY,\n );\n const arrivals = body.filter((entry, position) => {\n const previous = body[position - 1];\n return (\n noteToMidi(entry.note) === peak &&\n (previous === undefined || noteToMidi(previous.note) !== peak)\n );\n });\n const first = arrivals[0];\n const second = arrivals[1];\n if (first === undefined || second === undefined) {\n return [];\n }\n return [\n violation(\n 'melodicShape',\n [1],\n first.index,\n second.index,\n 'The line reaches its highest note more than once, so it has no one climax',\n ),\n ];\n}\n\n/**\n * Leaps taken one after another the same way: two at most, spanning an octave\n * at most, so that a chain of leaps still outlines a chord the ear can follow.\n */\nfunction leapChainViolations(moves: readonly Move[]): PartWritingViolation[] {\n const found: PartWritingViolation[] = [];\n let run: Move[] = [];\n const close = (): void => {\n const first = run[0];\n const last = run[run.length - 1];\n const span = run.reduce((total, move) => total + Math.abs(move.interval.semitones), 0);\n if (first !== undefined && last !== undefined) {\n if (run.length > MAX_LEAPS_IN_ONE_DIRECTION) {\n found.push(\n violation(\n 'melodicShape',\n [1],\n first.from.index,\n last.to.index,\n `The counterpoint leaps ${run.length} times in a row in the same direction`,\n ),\n );\n } else if (run.length > 1 && span > MAX_LEAP_CHAIN_SPAN) {\n found.push(\n violation(\n 'melodicShape',\n [1],\n first.from.index,\n last.to.index,\n 'Two leaps in the same direction carry the line further than an octave',\n ),\n );\n }\n }\n run = [];\n };\n for (const move of moves) {\n if (!isLeap(move.interval)) {\n close();\n continue;\n }\n const previous = run[run.length - 1];\n if (previous !== undefined && direction(previous.interval) !== direction(move.interval)) {\n close();\n }\n run.push(move);\n }\n close();\n return found;\n}\n\n/**\n * A wide leap is answered by a step the other way, which fills the gap it tore\n * open. Thirds, fourths and fifths are free to carry on: the wider the leap,\n * the stronger the style asks for the turn, and it asks outright from the sixth\n * upward.\n */\nfunction leapAnswerViolations(moves: readonly Move[]): PartWritingViolation[] {\n const found: PartWritingViolation[] = [];\n for (let position = 0; position < moves.length; position += 1) {\n const leap = moves[position];\n const answer = moves[position + 1];\n if (leap === undefined || answer === undefined || leap.interval.number < LEAP_NEEDING_ANSWER) {\n continue;\n }\n if (isStep(answer.interval) && direction(answer.interval) === -direction(leap.interval)) {\n continue;\n }\n const word = intervalWord(leap.interval.number);\n found.push(\n violation(\n 'melodicShape',\n [1],\n leap.from.index,\n answer.to.index,\n `The leap of ${intervalArticle(word)} ${word} is not answered by a step the other way`,\n ),\n );\n }\n return found;\n}\n\n/**\n * The tritone the line may not leap is also the tritone it may not draw: a run\n * of notes moving one way, however it is filled in, may not turn around on the\n * interval between its own extremes.\n *\n * A repeated note does not end the run, and a run of a single interval is left\n * to the leap rule, which already forbids it.\n */\nfunction tritoneOutlineViolations(entries: readonly Entry[]): PartWritingViolation[] {\n const found: PartWritingViolation[] = [];\n let run: Move[] = [];\n const close = (): void => {\n const first = run[0];\n const last = run[run.length - 1];\n if (first !== undefined && last !== undefined && run.length > 1) {\n const outline = spelledInterval(first.from.note, last.to.note);\n if (Math.abs(outline.semitones) === TRITONE_SEMITONES) {\n found.push(\n violation(\n 'melodicShape',\n [1],\n first.from.index,\n last.to.index,\n 'The line turns around on a tritone, outlining the interval it may not leap',\n ),\n );\n }\n }\n run = [];\n };\n for (const move of melodicMoves(entries)) {\n if (direction(move.interval) === 0) {\n continue;\n }\n const previous = run[run.length - 1];\n if (previous !== undefined && direction(previous.interval) !== direction(move.interval)) {\n close();\n }\n run.push(move);\n }\n close();\n return found;\n}\n\n/**\n * The moving species do not repeat a note: the second and third species are\n * written to keep the line going, and the repetition that carries a fourth or\n * fifth species phrase is a tie rather than a second attack.\n *\n * The closing measure is exempt. An exercise that writes its final measure in\n * the species' own note values rather than as the single whole note convention\n * asks for is holding the final, not attacking it again.\n */\nfunction repeatedNoteViolations(\n moves: readonly Move[],\n closingMeasure: number,\n): PartWritingViolation[] {\n return moves\n .filter(\n (move) => sameNote(move.from.note, move.to.note) && move.from.againstIndex < closingMeasure,\n )\n .map((move) =>\n violation(\n 'melodicShape',\n [1],\n move.from.index,\n move.to.index,\n 'The counterpoint repeats a note where this species keeps the line moving',\n ),\n );\n}\n\n/**\n * The rules judged on the shape of the counterpoint rather than on any one of\n * its intervals: the single climax, the chain of leaps and the step that\n * answers a wide one, the tritone outlined between turning points, and the\n * repeated note the moving species do not write.\n *\n * These belong to the species exercise, whose whole subject is the line. The\n * four-part exercise is judged chord by chord and lets a voice leap where the\n * harmony asks, so {@link checkPartWriting} does not apply them.\n */\nfunction melodicShapeViolations(\n entries: readonly Entry[],\n species: Species,\n): PartWritingViolation[] {\n const moves = melodicMoves(entries);\n const closingMeasure = entries[entries.length - 1]?.againstIndex ?? 0;\n const found = [\n ...climaxViolations(entries),\n ...leapChainViolations(moves),\n ...leapAnswerViolations(moves),\n ...tritoneOutlineViolations(entries),\n ...(species === 2 || species === 3 ? repeatedNoteViolations(moves, closingMeasure) : []),\n ];\n return found.sort((left, right) => left.fromIndex - right.fromIndex);\n}\n\n/**\n * The violations in the order the exercise commits them: by the note the fault\n * starts on, then by the note it ends on.\n *\n * The rules are accumulated rule by rule, since each reads the line its own way,\n * but a marked exercise is read from the top — so they are put back into time\n * order before they are handed out. The sort is stable, so two faults on the\n * same note keep the order the rules found them in and the same exercise always\n * marks the same way.\n */\nfunction inTimeOrder(violations: readonly PartWritingViolation[]): PartWritingViolation[] {\n return [...violations].sort(\n (left, right) => left.fromIndex - right.fromIndex || left.toIndex - right.toIndex,\n );\n}\n\n/** The rules judged between two voices moving together. */\nfunction motionViolations(\n from: Entry,\n to: Entry,\n counterpointAbove: boolean,\n): PartWritingViolation[] {\n const found: PartWritingViolation[] = [];\n const upperPrev = counterpointAbove ? from.note : from.against;\n const upperCur = counterpointAbove ? to.note : to.against;\n const lowerPrev = counterpointAbove ? from.against : from.note;\n const lowerCur = counterpointAbove ? to.against : to.note;\n if (createsParallelPerfect(upperPrev, upperCur, lowerPrev, lowerCur)) {\n const fifth = simpleIntervalNumber(to.interval.number) === 5;\n found.push(\n violation(\n fifth ? 'parallelFifth' : 'parallelOctave',\n [0, 1],\n from.index,\n to.index,\n fifth\n ? 'The two voices move into consecutive perfect fifths'\n : 'The two voices move into consecutive perfect octaves',\n ),\n );\n }\n // Two voices and nothing between them: the sixteenth-century rule forbids the\n // arrival however the upper voice reached it, so the step exception the\n // four-part chorale grants its outer voices is not applied here.\n if (createsHiddenParallelPerfect(upperPrev, upperCur, lowerPrev, lowerCur, 'twoVoice')) {\n found.push(\n violation(\n 'hiddenPerfect',\n [0, 1],\n from.index,\n to.index,\n 'The voices move into a perfect interval by similar motion',\n ),\n );\n }\n if (createsBattuta(upperPrev, upperCur, lowerPrev, lowerCur)) {\n found.push(\n violation(\n 'battuta',\n [0, 1],\n from.index,\n to.index,\n 'The upper voice leaps down into an octave against the lower voice',\n ),\n );\n }\n return found;\n}\n\n/**\n * The last counterpoint note sounding against an earlier cantus-firmus note\n * than the given one.\n *\n * The closing formula is a motion from one measure into the next, so it is read\n * by measure rather than by position in the array. A closing measure written in\n * the species' own note values holds more than one counterpoint note, and the\n * note before the last one then lies inside the final measure, which would\n * judge the cadence against itself.\n */\nfunction lastEntryBefore(entries: readonly Entry[], againstIndex: number): Entry | undefined {\n for (let position = entries.length - 1; position >= 0; position -= 1) {\n const entry = entries[position];\n if (entry !== undefined && entry.againstIndex < againstIndex) {\n return entry;\n }\n }\n return undefined;\n}\n\n/**\n * The opening and closing formulas.\n *\n * An exercise begins on a perfect consonance and ends on the octave or unison,\n * approached by step in the counterpoint from the major sixth above the cantus\n * firmus, or the minor third below it — the sixth or third that carries the\n * leading tone.\n */\nfunction cadenceViolations(\n entries: readonly Entry[],\n mode: KeyScale,\n counterpointAbove: boolean,\n): PartWritingViolation[] {\n const found: PartWritingViolation[] = [];\n const first = entries[0];\n const last = entries[entries.length - 1];\n if (first === undefined || last === undefined) {\n return found;\n }\n if (classifySpelledInterval(first.interval) !== ConsonanceClass.PerfectConsonance) {\n found.push(\n violation(\n 'missingCadence',\n [0, 1],\n first.index,\n first.index,\n 'The exercise does not begin on a perfect consonance',\n ),\n );\n }\n if (noteToPitchClass(last.note) !== pitchClassOf(mode.rootPc)) {\n found.push(\n violation(\n 'missingCadence',\n [0, 1],\n last.index,\n last.index,\n 'The counterpoint does not close on the final of the mode',\n ),\n );\n }\n const closing = last.interval;\n if (closing.quality !== 'P' || simpleIntervalNumber(closing.number) !== 1) {\n found.push(\n violation(\n 'missingCadence',\n [0, 1],\n last.index,\n last.index,\n 'The exercise does not close on an octave or unison',\n ),\n );\n return found;\n }\n const penultimate = lastEntryBefore(entries, last.againstIndex);\n if (penultimate === undefined) {\n return found;\n }\n const arrival = entries.find((entry) => entry.againstIndex === last.againstIndex) ?? last;\n const approach = spelledInterval(penultimate.note, arrival.note);\n const expected = counterpointAbove ? { quality: 'M', number: 6 } : { quality: 'm', number: 3 };\n if (\n penultimate.interval.quality !== expected.quality ||\n simpleIntervalNumber(penultimate.interval.number) !== expected.number ||\n !isStep(approach)\n ) {\n found.push(\n violation(\n 'missingCadence',\n [0, 1],\n penultimate.index,\n arrival.index,\n counterpointAbove\n ? 'The close is not the major sixth stepping out to the octave'\n : 'The close is not the minor third stepping in to the unison',\n ),\n );\n }\n return found;\n}\n\n/** The note lengths to read the exercise by, or the reason they cannot be read. */\nfunction resolveDurations(\n counterpoint: readonly Note[],\n measures: number,\n species: Species,\n opts: SpeciesOptions | undefined,\n): { durations: number[] } | { problem: string } {\n const given = opts?.durations;\n if (given !== undefined) {\n if (given.length !== counterpoint.length) {\n return {\n problem: `The exercise gives ${given.length} durations for ${counterpoint.length} notes`,\n };\n }\n for (const duration of given) {\n const units = duration / MIN_FLORID_VALUE;\n if (!(duration > 0) || duration > 1 || Math.abs(units - Math.round(units)) > BEAT_EPS) {\n return {\n problem: `A note lasts ${duration} of a measure, which this style does not write`,\n };\n }\n }\n const total = given.reduce((sum, duration) => sum + duration, 0);\n if (Math.abs(total - measures) > BEAT_EPS) {\n return { problem: `The counterpoint fills ${total} of the ${measures} measures` };\n }\n return { durations: [...given] };\n }\n if (species === 5) {\n throw new InvalidInputError(\n 'checkSpecies needs durations for the fifth species, whose note values are not fixed',\n );\n }\n const ratio = SPECIES_RATIO[species];\n const full = ratio * measures;\n // A closing measure written as one whole note is the convention rather than\n // an exception, so both note counts read as the correct ratio.\n if (counterpoint.length !== full && counterpoint.length !== full - ratio + 1) {\n return {\n problem: `Species ${species} sets ${ratio} notes against each of the ${measures} cantus firmus notes, and the counterpoint has ${counterpoint.length}`,\n };\n }\n return { durations: impliedDurations(counterpoint.length, measures, ratio) };\n}\n\n/**\n * Place every counterpoint note against the cantus-firmus notes it sounds with.\n *\n * A note is measured against each cantus-firmus note struck while it is still\n * sounding, not only the one it was struck over: the syncopation that defines\n * the fourth and fifth species holds a note across the bar line, and the\n * interval it forms in the measure it arrives in is the whole point of the\n * figure. Leaving it unread would let a bare dissonance on a downbeat pass for\n * a clean exercise.\n */\nfunction buildEntries(\n cantusFirmus: readonly Note[],\n counterpoint: readonly Note[],\n durations: readonly number[],\n counterpointAbove: boolean,\n): Entry[] {\n const entries: Entry[] = [];\n let onset = 0;\n for (let index = 0; index < counterpoint.length; index += 1) {\n const note = counterpoint[index];\n const duration = durations[index] ?? 0;\n if (note === undefined) {\n continue;\n }\n const struckOver = Math.min(Math.floor(onset + BEAT_EPS), cantusFirmus.length - 1);\n const lastOver = Math.min(\n Math.max(Math.ceil(onset + duration - BEAT_EPS) - 1, struckOver),\n cantusFirmus.length - 1,\n );\n for (let againstIndex = struckOver; againstIndex <= lastOver; againstIndex += 1) {\n const against = cantusFirmus[againstIndex];\n if (against === undefined) {\n continue;\n }\n const held = againstIndex > struckOver;\n // A held stretch begins where the measure does; the first begins wherever\n // the note was struck, which is what makes it a weak-beat note or not.\n const begins = held ? againstIndex : onset;\n const interval = counterpointAbove\n ? spelledInterval(against, note)\n : spelledInterval(note, against);\n entries.push({\n note,\n index,\n onset: begins,\n duration: onset + duration - begins,\n held,\n against,\n againstIndex,\n downbeat: Math.abs(begins - Math.round(begins)) < BEAT_EPS,\n interval,\n consonance: classifySpelledInterval(interval),\n });\n }\n onset += duration;\n }\n return entries;\n}\n\n/**\n * The pairs of counterpoint notes the motion rules are judged over: the\n * successive attacks that cross a bar line. It is given the notes actually\n * struck, since a note held on across the bar line is not a motion at all.\n *\n * Two counterpoint notes over one unmoving cantus-firmus note are oblique\n * motion, which no parallel rule can touch, so the pairs that can break a rule\n * are the ones where the lower voice has moved too. Perfect intervals on two\n * successive downbeats with other notes in between are deliberately not judged\n * here: whether the intervening note excuses them is exactly what the sources\n * disagree about, and the fourth species is built on the ligature that does.\n */\nfunction motionPairs(entries: readonly Entry[]): [number, number][] {\n const pairs: [number, number][] = [];\n for (let position = 1; position < entries.length; position += 1) {\n const previous = entries[position - 1];\n const current = entries[position];\n if (previous === undefined || current === undefined) {\n continue;\n }\n if (previous.againstIndex !== current.againstIndex) {\n pairs.push([position - 1, position]);\n }\n }\n return pairs;\n}\n\n/**\n * Check a species counterpoint exercise and report every rule it breaks.\n *\n * The cantus firmus is one note per measure. The counterpoint is aligned to it\n * by position rather than by written rhythm: the first four species have a fixed\n * number of notes per measure, so the note count alone says where each note\n * falls, and a closing measure written as a single whole note is accepted as the\n * convention it is. The fifth species mixes note values and therefore needs\n * `opts.durations`, given in cantus-firmus notes.\n *\n * Judged are the vertical intervals under each species' dissonance licence\n * (none in the first, a passing dissonance on the weak half in the second and\n * third, a prepared suspension on the downbeat in the fourth, both in the\n * fifth; the neighbour that turns back on its own note belongs to the\n * quarter-note line of the third and fifth species, which also write the *nota\n * cambiata* and the double neighbour, quitting a dissonance by leap). A note\n * still sounding when the next cantus-firmus note is struck is judged against\n * that note too, so a ligature carried across the bar line is read where it\n * lands as well as where it began. Judged besides are parallel and hidden\n * perfects and the *battuta* between the voices, augmented and otherwise\n * forbidden leaps along the counterpoint, the shape of the counterpoint as a\n * line, and the opening and closing formulas. Violations name the cantus firmus\n * as voice 0 and the counterpoint as voice 1, and index into the counterpoint.\n *\n * @param cantusFirmus The given voice, one spelled note per measure.\n * @param counterpoint The written voice, in spelled notes.\n * @param species Which species the exercise is written in.\n * @param modeLike The mode the exercise is in.\n * @param opts Note lengths, and which side the counterpoint is written on.\n * @returns Every violation found, in the order the exercise commits them; an\n * empty array for a clean exercise.\n * @throws If the species is not one of the five, either voice is empty, a note\n * carries no octave, or the fifth species is given without durations.\n * @example\n * ```ts\n * import { checkSpecies, majorKey, parseNote } from '@libraz/libcantus';\n * const cf = ['C4', 'D4', 'E4', 'D4', 'C4'].map((n) => parseNote(n));\n * const cp = ['C5', 'A4', 'G4', 'B4', 'C5'].map((n) => parseNote(n));\n * checkSpecies(cf, cp, 1, majorKey(0)); // [] — a clean first-species exercise\n * ```\n * @category Voicing & Counterpoint\n */\nexport function checkSpecies(\n cantusFirmus: readonly Note[],\n counterpoint: readonly Note[],\n species: Species,\n modeLike: KeyLike,\n opts?: SpeciesOptions,\n): PartWritingViolation[] {\n assertArray<Note>(cantusFirmus, 'cantus firmus');\n assertArray<Note>(counterpoint, 'counterpoint');\n const asked = assertOptions(opts, 'opts');\n const mode = toKeyScale(modeLike);\n if (!SPECIES_NUMBERS.includes(species)) {\n throw new InvalidInputError(\n `species must be one of ${SPECIES_NUMBERS.join(', ')}; received ${describeRejected(species)}`,\n );\n }\n if (cantusFirmus.length === 0 || counterpoint.length === 0) {\n throw new InvalidInputError('checkSpecies needs both a cantus firmus and a counterpoint');\n }\n const resolved = resolveDurations(counterpoint, cantusFirmus.length, species, asked);\n if ('problem' in resolved) {\n return inTimeOrder([\n violation('wrongRhythmicRatio', [1], 0, counterpoint.length - 1, resolved.problem),\n ]);\n }\n const counterpointAbove =\n asked.counterpointAbove ?? meanPitch(counterpoint) >= meanPitch(cantusFirmus);\n const entries = buildEntries(cantusFirmus, counterpoint, resolved.durations, counterpointAbove);\n // Every interval the exercise sounds is judged, including the ones a held\n // note forms in the measure it is carried into. The line itself is read from\n // the attacks alone: a note going on sounding was not sung a second time, and\n // the motion the voices make across a bar line is the one from the note\n // struck before it to the note struck after.\n const struck = entries.filter((entry) => !entry.held);\n\n const violations: PartWritingViolation[] = [];\n const crossed = new Set<number>();\n for (let position = 0; position < entries.length; position += 1) {\n violations.push(...verticalViolations(entries, position, species, counterpointAbove, crossed));\n }\n for (let position = 1; position < struck.length; position += 1) {\n const previous = struck[position - 1];\n const current = struck[position];\n if (previous !== undefined && current !== undefined) {\n violations.push(...melodicViolations(previous, current));\n }\n }\n violations.push(...melodicShapeViolations(struck, species));\n for (const [from, to] of motionPairs(struck)) {\n const previous = struck[from];\n const current = struck[to];\n if (previous !== undefined && current !== undefined) {\n violations.push(...motionViolations(previous, current, counterpointAbove));\n }\n }\n violations.push(...cadenceViolations(struck, mode, counterpointAbove));\n return inTimeOrder(violations);\n}\n","/**\n * Part writing: grade a finished exercise against the classical four-part rules.\n *\n * The counterpoint predicates each judge one pair of voices at one moment; this\n * module walks a whole progression and reports every violation it finds, so a\n * completed exercise can be marked in a single call.\n *\n * The input is spelled notes rather than MIDI pitches because two of the rules\n * cannot be decided without them: a cross relation is one letter carrying two\n * different accidentals, and an augmented second is a minor third under another\n * name. A caller holding only pitches spells them with {@link spellVoicing}.\n */\n\nimport { InvalidInputError } from '../../core/errors/index.js';\nimport type { Note } from '../../core/pitch/index.js';\nimport {\n formatNote,\n noteToMidi,\n noteToPitchClass,\n pitchClassOf,\n spelledInterval,\n} from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport { assertArray, assertOptions } from '../../core/validation/index.js';\nimport type { Chord } from '../chord/index.js';\nimport { assertChord, chordPitchClasses } from '../chord/index.js';\nimport {\n createsHiddenParallelPerfect,\n createsParallelPerfect,\n createsVoiceCrossing,\n createsVoiceOverlap,\n exceedsSpacing,\n isAugmentedMelodicInterval,\n isLeadingToneResolution,\n} from '../counterpoint/index.js';\nimport { type KeyLike, resolveKey, type SpelledKeyLike, toKeyScale } from '../scale/index.js';\nimport { spellPitch } from '../spelling/index.js';\nimport {\n isDescendingStep,\n isFrustratedLeadingTone,\n isFunctioningLeadingTone,\n leadingTonePcOf,\n seventhPcOf,\n} from '../tendency/index.js';\nimport type { VoiceRange } from '../voicing/index.js';\nimport { SATB_RANGES } from '../voicing/index.js';\nimport { resolveMaxSpacing, resolveRanges } from '../voicing/satb.js';\nimport { crossRelations, isOuterVoice } from './cross-relation.js';\nimport { intervalWord, violation } from './internal.js';\n\n/**\n * The rule a {@link PartWritingViolation} breaks.\n *\n * - `parallelFifth`, `parallelOctave`: consecutive perfect intervals of the same\n * kind between one pair of voices. A parallel unison is reported as a\n * `parallelOctave`, since it is the same perfect class.\n * - `hiddenPerfect`: the outer voices reach a perfect fifth or octave by similar\n * motion with a leap in the upper voice. The leap is what exposes it in a\n * four-part texture; in a two-voice species exercise nothing covers the\n * arrival, so similar motion into a perfect interval is reported however the\n * upper voice got there.\n * - `crossRelation`: one letter carries two different accidentals in two\n * different voices across a chord change, exposed — neither note led into by\n * step, at least one of them in an outer voice, and neither chord one of the\n * chromatic harmonies that are written with the contradiction.\n * - `voiceCrossing`, `overlap`, `spacing`, `range`: the texture rules.\n * - `unresolvedLeadingTone`, `unresolvedSeventh`: a tendency tone that does not\n * go where it must.\n * - `augmentedMelodicInterval`: a single voice moves by an augmented interval.\n *\n * The rest are raised by {@link checkSpecies} alone, since they describe faults\n * only a species exercise can commit:\n *\n * - `wrongRhythmicRatio`: the counterpoint's rhythm does not read against the\n * cantus firmus as its species requires — the wrong number of notes per\n * cantus-firmus note, and, in the fifth species, note values the style does\n * not write or a `durations` array that does not describe the counterpoint it\n * was given.\n * - `unpreparedDissonance`: a dissonance falls where the species allows none, or\n * is reached or left by leap where it must be passed through by step.\n * - `unresolvedSuspension`: a fourth-species suspension that does not fall by\n * step onto the following consonance.\n * - `illegalLeap`: a melodic leap the style forbids — either seventh, the\n * tritone, or anything wider than an octave.\n * - `battuta`: an octave or unison reached by contrary motion with the upper\n * voice leaping down.\n * - `missingCadence`: the exercise does not open or close on the formula its\n * species requires.\n * - `melodicShape`: the counterpoint is a poor line — it has no single climax,\n * leaps too often or too far one way, leaves a wide leap unanswered, outlines\n * a tritone, or repeats a note where the species keeps moving. The rationale\n * says which.\n *\n * @category Voicing & Counterpoint\n */\nexport type PartWritingViolationKind =\n | 'parallelFifth'\n | 'parallelOctave'\n | 'hiddenPerfect'\n | 'crossRelation'\n | 'voiceCrossing'\n | 'overlap'\n | 'spacing'\n | 'range'\n | 'unresolvedLeadingTone'\n | 'unresolvedSeventh'\n | 'augmentedMelodicInterval'\n | 'wrongRhythmicRatio'\n | 'unpreparedDissonance'\n | 'unresolvedSuspension'\n | 'illegalLeap'\n | 'battuta'\n | 'missingCadence'\n | 'melodicShape';\n\n/**\n * One broken rule, located in the progression and in the texture.\n *\n * @category Voicing & Counterpoint\n */\nexport type PartWritingViolation = {\n /** The rule that was broken. */\n kind: PartWritingViolationKind;\n /**\n * The voices involved, by index into a voicing (0 = lowest). A rule about a\n * pair names both, ascending; a rule about a single line names one. A\n * `crossRelation` is the exception: it names the voice holding the earlier\n * note first, whichever is higher. In a species exercise there are only two\n * voices and they are named by role instead: 0 is the cantus firmus and 1 the\n * counterpoint, whichever lies above.\n */\n voices: number[];\n /**\n * Index of the chord the motion starts on; in a species exercise, the index\n * into the counterpoint.\n */\n fromIndex: number;\n /** Index of the chord it ends on; equal to `fromIndex` for a vertical rule. */\n toIndex: number;\n /** A short sentence naming the musical reason. */\n rationale: string;\n};\n\n/**\n * A chord realized as one spelled note per voice, in voice order (index 0 =\n * bass), matching the order {@link voiceChord} and {@link SATB_RANGES} use.\n * Pitches need not ascend; a crossing is a reportable violation.\n *\n * Every note must carry an octave: the rules measure real distances between\n * sounding pitches, which a bare pitch class does not have.\n *\n * @category Voicing & Counterpoint\n */\nexport type SpelledVoicing = Note[];\n\n/**\n * Options controlling {@link checkPartWriting}.\n *\n * @category Voicing & Counterpoint\n */\nexport type PartWritingOptions = {\n /**\n * Per-voice ranges, ascending (index 0 = lowest), against which the `range`\n * rule is judged. Defaults to {@link SATB_RANGES} for a four-voice exercise;\n * for any other voice count there is no conventional compass to assume, so\n * the rule is skipped unless ranges are given. Given ranges must cover every\n * voice of the widest chord, and each must be finite with `min` no greater\n * than `max`.\n */\n ranges?: VoiceRange[];\n /**\n * Maximum spacing in semitones between adjacent upper voices. The bass-tenor\n * pair is exempt, as convention has it. Must be finite and non-negative.\n *\n * @defaultValue 12\n */\n maxSpacing?: number;\n};\n\n/** A note without its octave, for naming it in a rationale. */\nfunction bare(note: Note): Note {\n return { letter: note.letter, alter: note.alter };\n}\n\n/** The sounding pitch of a spelled note, located for the error message. */\nfunction midiOf(note: Note, index: number, voice: number): number {\n if (note.octave === undefined) {\n throw new InvalidInputError(`voicings[${index}][${voice}] must carry an octave`);\n }\n return noteToMidi(note);\n}\n\n/** One chord of the exercise, and everything the rules read off it. */\ntype Moment = {\n notes: SpelledVoicing;\n pitches: number[];\n chord: Chord;\n index: number;\n};\n\n/** The rules judged inside one chord: crossing, spacing, and range. */\nfunction verticalViolations(\n moment: Moment,\n ranges: readonly Readonly<VoiceRange>[] | undefined,\n maxSpacing: number,\n): PartWritingViolation[] {\n const found: PartWritingViolation[] = [];\n const { pitches, index } = moment;\n for (let voice = 1; voice < pitches.length; voice += 1) {\n const upper = pitches[voice];\n const lower = pitches[voice - 1];\n if (upper === undefined || lower === undefined) {\n continue;\n }\n if (createsVoiceCrossing(upper, lower)) {\n found.push(\n violation(\n 'voiceCrossing',\n [voice - 1, voice],\n index,\n index,\n 'The upper voice lies below the voice beneath it',\n ),\n );\n }\n // The bass-tenor pair is conventionally allowed to open up further than the\n // upper voices, so the spacing rule starts one pair above it.\n if (voice >= 2 && exceedsSpacing(upper, lower, maxSpacing)) {\n found.push(\n violation(\n 'spacing',\n [voice - 1, voice],\n index,\n index,\n `Adjacent upper voices lie more than ${maxSpacing} semitones apart`,\n ),\n );\n }\n }\n if (ranges === undefined) {\n return found;\n }\n for (let voice = 0; voice < pitches.length; voice += 1) {\n const pitch = pitches[voice];\n const range = ranges[voice];\n if (pitch === undefined || range === undefined) {\n continue;\n }\n if (pitch < range.min || pitch > range.max) {\n found.push(violation('range', [voice], index, index, 'The voice sings outside its range'));\n }\n }\n return found;\n}\n\n/**\n * Two consecutive chords of the exercise, the key they sound in, and the chords\n * on either side of the pair. The neighbours are what tells a chromatic tone\n * that was led into by step from one that was leapt at, and an applied dominant\n * from the diatonic triad that sounds the same.\n */\ntype Transition = {\n from: Moment;\n to: Moment;\n key: KeyScale;\n before?: Moment;\n after?: Moment;\n};\n\n/** The rules judged between two voices moving together: parallels and overlap. */\nfunction pairViolations(transition: Transition): PartWritingViolation[] {\n const found: PartWritingViolation[] = [];\n const { from, to } = transition;\n const voices = Math.min(from.pitches.length, to.pitches.length);\n for (let lower = 0; lower < voices; lower += 1) {\n for (let upper = lower + 1; upper < voices; upper += 1) {\n const upperPrev = from.pitches[upper];\n const upperCur = to.pitches[upper];\n const lowerPrev = from.pitches[lower];\n const lowerCur = to.pitches[lower];\n if (\n upperPrev === undefined ||\n upperCur === undefined ||\n lowerPrev === undefined ||\n lowerCur === undefined\n ) {\n continue;\n }\n if (createsParallelPerfect(upperPrev, upperCur, lowerPrev, lowerCur)) {\n const fifth = pitchClassOf(upperCur - lowerCur) === 7;\n found.push(\n violation(\n fifth ? 'parallelFifth' : 'parallelOctave',\n [lower, upper],\n from.index,\n to.index,\n fifth\n ? 'The two voices move into consecutive perfect fifths'\n : 'The two voices move into consecutive perfect octaves',\n ),\n );\n }\n if (upper === lower + 1 && createsVoiceOverlap(upperPrev, upperCur, lowerPrev, lowerCur)) {\n found.push(\n violation(\n 'overlap',\n [lower, upper],\n from.index,\n to.index,\n 'The voice moves past where its neighbour just was',\n ),\n );\n }\n }\n }\n // The hidden perfect is judged on the outer voices alone, where the bare fifth\n // or octave is exposed; between inner voices it is covered by the others.\n const bassPrev = from.pitches[0];\n const bassCur = to.pitches[0];\n const topPrev = from.pitches[voices - 1];\n const topCur = to.pitches[voices - 1];\n if (\n voices >= 2 &&\n bassPrev !== undefined &&\n bassCur !== undefined &&\n topPrev !== undefined &&\n topCur !== undefined &&\n createsHiddenParallelPerfect(topPrev, topCur, bassPrev, bassCur)\n ) {\n found.push(\n violation(\n 'hiddenPerfect',\n [0, voices - 1],\n from.index,\n to.index,\n 'The outer voices reach a perfect interval by similar motion, the upper one by leap',\n ),\n );\n }\n return found;\n}\n\n/**\n * Cross relations: the same letter carrying different accidentals in two\n * different voices across the chord change, reported as broken rules.\n *\n * The rule itself lives beside the voicing search that has to obey it, so what\n * is left here is naming the clash the search refused to write.\n */\nfunction crossRelationViolations(transition: Transition): PartWritingViolation[] {\n const { from, to, key, before, after } = transition;\n return crossRelations(from, to, key, { before: before?.notes, after: after?.chord }).map(\n (clash) =>\n violation(\n 'crossRelation',\n [clash.earlierVoice, clash.laterVoice],\n from.index,\n to.index,\n `${formatNote(bare(clash.earlier))} is contradicted by ${formatNote(bare(clash.later))} in another voice`,\n ),\n );\n}\n\n/**\n * The rules judged along one line: augmented melodic intervals, and the tendency\n * tones that must go somewhere in particular.\n *\n * A chordal seventh is a dissonance, so the voice carrying it falls by step\n * unless the next chord holds it as a common tone. A leading tone rises to the\n * tonic when it is functioning as one — the third of a dominant chord or the\n * root of a leading-tone chord — and the next chord contains the tonic and has\n * dropped the leading tone itself. Elsewhere the same pitch class is an\n * ordinary chord tone, so `iii` moving to `IV` is not asked to resolve its\n * fifth.\n *\n * The leading-tone rule is judged in every voice, but not by the same measure in\n * all of them: an inner voice may frustrate its leading tone, falling a third\n * onto the fifth of the arriving chord to complete a triad the rising resolution\n * would leave without one. That is the other standard answer to a complete\n * dominant seventh, so it is not reported. In the bass or the top voice the\n * leading tone is exposed and is still required to rise; any other way of\n * leaving it, in an inner voice or an outer one, is reported as before.\n */\nfunction melodicViolations(transition: Transition): PartWritingViolation[] {\n const found: PartWritingViolation[] = [];\n const { from, to, key } = transition;\n const seventhPc = seventhPcOf(from.chord);\n const nextPcs = new Set(chordPitchClasses(to.chord));\n const leadingTonePc = leadingTonePcOf(key);\n const tonicPc = pitchClassOf(key.rootPc);\n const voices = Math.min(from.notes.length, to.notes.length);\n for (let voice = 0; voice < voices; voice += 1) {\n const earlier = from.notes[voice];\n const later = to.notes[voice];\n const earlierPitch = from.pitches[voice];\n const laterPitch = to.pitches[voice];\n if (\n earlier === undefined ||\n later === undefined ||\n earlierPitch === undefined ||\n laterPitch === undefined\n ) {\n continue;\n }\n const interval = spelledInterval(earlier, later);\n if (isAugmentedMelodicInterval(earlier, later)) {\n found.push(\n violation(\n 'augmentedMelodicInterval',\n [voice],\n from.index,\n to.index,\n `The voice moves by an augmented ${intervalWord(interval.number)}`,\n ),\n );\n }\n const earlierPc = noteToPitchClass(earlier);\n if (earlierPc === seventhPc && !nextPcs.has(earlierPc) && !isDescendingStep(interval)) {\n found.push(\n violation(\n 'unresolvedSeventh',\n [voice],\n from.index,\n to.index,\n 'The chordal seventh does not fall by step',\n ),\n );\n }\n const inner = !isOuterVoice(voice, from.notes) && !isOuterVoice(voice, to.notes);\n if (\n earlierPc === leadingTonePc &&\n isFunctioningLeadingTone(from.chord, key) &&\n nextPcs.has(tonicPc) &&\n !nextPcs.has(earlierPc) &&\n !isLeadingToneResolution(earlierPitch, laterPitch, key) &&\n !(inner && isFrustratedLeadingTone(earlierPitch, laterPitch, to.chord))\n ) {\n found.push(\n violation(\n 'unresolvedLeadingTone',\n [voice],\n from.index,\n to.index,\n 'The leading tone does not rise to the tonic',\n ),\n );\n }\n }\n return found;\n}\n\n/**\n * Spell a voicing given as MIDI pitches, so it can be checked by\n * {@link checkPartWriting}.\n *\n * Each pitch is named against the chord sounding under it and then the key, so a\n * chord tone takes the letter the degree it plays in that chord implies: the\n * third of a D major chord in C major spells F#, not Gb. The chord is passed\n * whole rather than by its root, so a chord carrying its own spelling names its\n * tones here exactly as {@link spellChord} names them — the German sixth keeps\n * the F# that resolves outward to the dominant. The octave is kept, since the\n * rules measure real distances.\n *\n * @param voicing MIDI pitches in voice order (index 0 = bass).\n * @param chord The chord sounding, supplying the enharmonic evidence.\n * @param key The key the exercise is written in.\n * @returns One spelled note per pitch, in the same order.\n * @example\n * ```ts\n * import { majorKey, makeChord, noteNames, spellVoicing } from '@libraz/libcantus';\n * noteNames(spellVoicing([50, 57, 66, 69], makeChord(2, 'maj'), majorKey(0)));\n * // ['D3', 'A3', 'F#4', 'A4'] — the third of D spells F# in C major\n * ```\n * @category Voicing & Counterpoint\n */\nexport function spellVoicing(\n voicing: readonly number[],\n chord: Chord,\n key: SpelledKeyLike,\n): SpelledVoicing {\n assertArray<number>(voicing, 'voicing');\n assertChord(chord);\n // Read whole rather than reduced: a voicing in Ab minor is written on flats,\n // and a key handed in spelled that way was losing its spelling right here.\n const { tonic, scale } = resolveKey(key);\n return voicing.map((pitch) => spellPitch(pitch, tonic, scale, { chord }));\n}\n\n/**\n * Check a finished part-writing exercise and report every rule it breaks.\n *\n * The voicings are given one per chord, each with one spelled note per voice in\n * voice order (index 0 = bass), the order {@link voiceChord} and\n * {@link SATB_RANGES} already use. Notes must carry octaves. A crossing is\n * reported rather than rejected.\n *\n * Each chord is checked for voice crossing, over-wide spacing between adjacent\n * upper voices, and voices outside their range; each pair of consecutive chords\n * for parallel and hidden perfects, voice overlap, cross relations, augmented\n * melodic intervals, and unresolved tendency tones. Violations come back in\n * musical order: the rules inside a chord, then the rules taking it to the next.\n *\n * @param voicings One voicing per chord, in voice order, in spelled notes.\n * @param chords The chords those voicings realize, in the same order.\n * @param keyLike The key the exercise is written in; it names the leading tone.\n * @param opts Ranges and the upper-voice spacing limit. Both are validated\n * before any rule runs: an empty result has to mean that nothing was broken,\n * never that a rule could not be evaluated.\n * @returns Every violation found, in musical order; an empty array for a clean\n * exercise.\n * @throws If the two arrays differ in length, a note carries no octave, the\n * spacing limit is not a non-negative finite number, or the ranges are empty,\n * malformed, or too few for the voices being checked.\n * @example\n * ```ts\n * import { checkPartWriting, majorKey, makeChord, spellVoicing } from '@libraz/libcantus';\n * const key = majorKey(0);\n * const chords = [makeChord(0, 'maj'), makeChord(2, 'min')];\n * const voicings = [[48, 55, 64, 72], [50, 57, 65, 69]].map((pitches, i) =>\n * spellVoicing(pitches, chords[i] ?? chords[0], key),\n * );\n * const violations = checkPartWriting(voicings, chords, key);\n * violations.map((violation) => violation.kind); // ['parallelFifth']\n * violations[0]?.voices; // [0, 1]\n * ```\n * @category Voicing & Counterpoint\n */\nexport function checkPartWriting(\n voicings: readonly SpelledVoicing[],\n chords: readonly Chord[],\n keyLike: KeyLike,\n opts?: PartWritingOptions,\n): PartWritingViolation[] {\n assertArray<SpelledVoicing>(voicings, 'voicings');\n assertArray<Chord>(chords, 'chords');\n const asked = assertOptions(opts, 'opts');\n const key = toKeyScale(keyLike);\n if (voicings.length !== chords.length) {\n throw new InvalidInputError(\n `checkPartWriting needs one chord per voicing; received ${voicings.length} voicings and ${chords.length} chords`,\n );\n }\n // The limits are resolved through the same validators the voicing search\n // uses, and before any rule runs: a NaN limit would silently switch the\n // spacing rule off, and ranges too short for the texture would leave the top\n // voices unjudged, both of which report as a clean exercise.\n const maxSpacing = resolveMaxSpacing(asked);\n const voiceCount = voicings.reduce((widest, voicing) => Math.max(widest, voicing.length), 0);\n let ranges: readonly Readonly<VoiceRange>[] | undefined;\n if (asked.ranges === undefined) {\n // Four voices are the SATB exercise these ranges were written for; any other\n // count has no conventional compass, so an unasked-for range check would\n // invent one.\n ranges = voiceCount === 4 ? SATB_RANGES : undefined;\n } else {\n ranges = resolveRanges(asked);\n if (ranges.length < voiceCount) {\n throw new InvalidInputError(\n `checkPartWriting needs one range per voice; received ${ranges.length} ranges for ${voiceCount} voices`,\n );\n }\n }\n\n const moments: Moment[] = [];\n for (let index = 0; index < voicings.length; index += 1) {\n const notes = voicings[index];\n const chord = chords[index];\n if (notes === undefined || chord === undefined) {\n continue;\n }\n moments.push({\n notes,\n pitches: notes.map((note, voice) => midiOf(note, index, voice)),\n chord,\n index,\n });\n }\n\n const violations: PartWritingViolation[] = [];\n for (let position = 0; position < moments.length; position += 1) {\n const moment = moments[position];\n const previous = position > 0 ? moments[position - 1] : undefined;\n if (moment === undefined) {\n continue;\n }\n if (previous !== undefined) {\n const transition: Transition = {\n from: previous,\n to: moment,\n key,\n before: position > 1 ? moments[position - 2] : undefined,\n after: moments[position + 1],\n };\n violations.push(...pairViolations(transition));\n violations.push(...crossRelationViolations(transition));\n violations.push(...melodicViolations(transition));\n }\n violations.push(...verticalViolations(moment, ranges, maxSpacing));\n }\n return violations;\n}\n\nexport type { Species, SpeciesOptions } from './species.js';\nexport { checkSpecies } from './species.js';\n"]}
1
+ {"version":3,"sources":["/home/runner/work/libcantus/libcantus/dist/chunk-76ZBTRG2.cjs","../src/theory/transposition/index.ts","../src/theory/figured-bass/index.ts","../src/theory/partwriting/cross-relation.ts","../src/theory/voicing/tendency.ts","../src/theory/voicing/internal.ts","../src/theory/voicing/satb.ts","../src/theory/voicing/leading.ts","../src/theory/voicing/styled.ts","../src/theory/partwriting/internal.ts","../src/theory/partwriting/species.ts","../src/theory/partwriting/index.ts"],"names":[],"mappings":"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","file":"/home/runner/work/libcantus/libcantus/dist/chunk-76ZBTRG2.cjs","sourcesContent":[null,"/**\n * Transposing instruments: the fixed interval between the pitch a player reads\n * and the pitch the instrument sounds.\n *\n * The distance is a spelled interval, never a semitone count. A clarinet in A\n * sounds a minor third below what it reads, so a written C sounds A, a written\n * D sounds B, and a written D# sounds B#: the letter always moves by the\n * interval's diatonic number, where a bare count of three semitones would take\n * the last of those to C natural and lose the letter the part is written on.\n *\n * The octave is part of that interval rather than folded away into a pitch\n * class, so instruments pitched in the same key stay apart: the alto saxophone\n * sounds a major sixth lower, the baritone saxophone a major thirteenth (the\n * same key an octave further down), and the E flat clarinet a minor third\n * higher. The pure octave transposers — piccolo, double bass, contrabassoon,\n * guitar — are a perfect octave, a unison-class interval displaced by one\n * octave.\n */\n\nimport { InvalidInputError } from '../../core/errors/index.js';\nimport type { IntervalLike, Note, NoteLike, SpelledInterval } from '../../core/pitch/index.js';\nimport {\n parseInterval,\n toNoteData,\n toSpelledInterval,\n transposeByInterval,\n} from '../../core/pitch/index.js';\n\n/**\n * The interval from a written pitch to the pitch the instrument sounds, one\n * entry per built-in instrument.\n *\n * Each value is an interval name as {@link parseInterval} reads it, in the\n * written-to-sounding direction: a leading `'-'` marks an instrument that\n * sounds below what it reads, which is most of them. A transposition wider than\n * an octave is named by its compound interval rather than reduced, because the\n * octave is exactly what separates the tenor saxophone from the soprano and the\n * baritone from the alto.\n *\n * @example\n * ```ts\n * import { TRANSPOSING_INSTRUMENTS } from '@libraz/libcantus';\n * TRANSPOSING_INSTRUMENTS.clarinetA; // '-m3' — sounds a minor third lower\n * TRANSPOSING_INSTRUMENTS.piccolo; // 'P8' — sounds an octave higher\n * ```\n * @category Pitch & Intervals\n */\nexport const TRANSPOSING_INSTRUMENTS = Object.freeze({\n // In B flat: a written C sounds B flat. The lower members of the family read\n // in the same key an octave higher, so their interval is the compound one.\n clarinetBb: '-M2',\n trumpetBb: '-M2',\n sopranoSax: '-M2',\n tenorSax: '-M9',\n bassClarinet: '-M9',\n // In A: a written C sounds A.\n clarinetA: '-m3',\n // In F: a written C sounds F.\n hornF: '-P5',\n englishHorn: '-P5',\n // In E flat: one key, three registers. The sopranino clarinet sounds above\n // what it reads, the alto saxophone a major sixth below, and the baritone\n // saxophone an octave below that.\n clarinetEb: 'm3',\n altoSax: '-M6',\n baritoneSax: '-M13',\n // In D: a written C sounds D.\n trumpetD: 'M2',\n // Octave transposers: the instrument is in C, only the register moves.\n piccolo: 'P8',\n doubleBass: '-P8',\n contrabassoon: '-P8',\n guitar: '-P8',\n});\n\n/**\n * Built-in profile names whose transposition is another entry's.\n *\n * An instrument profile names its tuning where the table names the instrument:\n * `'guitar (drop D)'` is a guitar with one string lowered, and retuning a\n * string does not move the interval between the part and the pitch it sounds.\n * The alias resolves to the instrument so that every built-in profile answers\n * from its own name, rather than one of them alone needing a transposition\n * spelled out.\n */\nconst PROFILE_NAME_ALIASES: Readonly<Record<string, TransposingInstrumentName>> = Object.freeze({\n 'guitar (drop D)': 'guitar',\n});\n\n/**\n * The name of a built-in transposing instrument, as\n * {@link TRANSPOSING_INSTRUMENTS} keys them.\n *\n * @example\n * ```ts\n * import type { TransposingInstrumentName } from '@libraz/libcantus';\n * const part: TransposingInstrumentName = 'clarinetA';\n * ```\n * @category Pitch & Intervals\n */\nexport type TransposingInstrumentName = keyof typeof TRANSPOSING_INSTRUMENTS;\n\n/**\n * A transposing instrument: one of the built-in names, or a spelled interval\n * for an instrument the table does not carry.\n *\n * An interval is read in the same written-to-sounding direction as the table,\n * so `'-M2'` is any instrument in B flat and `'P8'` any that sounds an octave\n * up. The names complete in an editor while any other string is still accepted,\n * which is what lets an instrument read from configuration go straight in.\n *\n * @example\n * ```ts\n * import type { TransposingInstrument } from '@libraz/libcantus';\n * const clarinet: TransposingInstrument = 'clarinetA';\n * const altoFlute: TransposingInstrument = '-P4';\n * ```\n * @category Pitch & Intervals\n */\nexport type TransposingInstrument =\n | TransposingInstrumentName\n | (string & {})\n | Exclude<IntervalLike, string>;\n\n/**\n * The interval a transposing instrument sounds at, measured from what it reads.\n *\n * This is the table's own direction: the answer descends for an instrument that\n * sounds below the printed part, which is most of them. Applying it to a\n * written pitch is {@link toSoundingPitch}, and applying it to a concert key is\n * how a written key is read back as the key it sounds in.\n *\n * @param instrument A built-in instrument name, or an interval naming a\n * transposition the table does not carry.\n * @returns The written-to-sounding interval.\n * @throws If the value is neither a known instrument nor a spelled interval.\n * @example\n * ```ts\n * import { instrumentTransposition } from '@libraz/libcantus';\n * instrumentTransposition('clarinetA');\n * // { number: 3, quality: 'm', semitones: -3, descending: true }\n * instrumentTransposition('piccolo');\n * // { number: 8, quality: 'P', semitones: 12, descending: false }\n * ```\n * @category Pitch & Intervals\n */\nexport function instrumentTransposition(instrument: TransposingInstrument): SpelledInterval {\n if (typeof instrument !== 'string') {\n return toSpelledInterval(instrument);\n }\n // Looked up through `hasOwn` so a name such as `'constructor'` cannot resolve\n // to something inherited from `Object.prototype`.\n const name = Object.hasOwn(PROFILE_NAME_ALIASES, instrument)\n ? (PROFILE_NAME_ALIASES[instrument] as TransposingInstrumentName)\n : instrument;\n if (Object.hasOwn(TRANSPOSING_INSTRUMENTS, name)) {\n return parseInterval(TRANSPOSING_INSTRUMENTS[name as TransposingInstrumentName]);\n }\n try {\n return parseInterval(instrument);\n } catch (error) {\n if (error instanceof InvalidInputError) {\n // A mistyped instrument name is the likely mistake, so the message names\n // that possibility rather than only the interval grammar it also failed.\n throw new InvalidInputError(\n `unknown transposing instrument ${JSON.stringify(instrument)}; ` +\n \"pass a name from TRANSPOSING_INSTRUMENTS or an interval such as '-M2'\",\n );\n }\n throw error;\n }\n}\n\n/**\n * The same interval taken the other way, which is what makes the two\n * conversions exact inverses.\n *\n * The direction is flipped rather than read back off the sign, because a unison\n * spans zero semitones in both directions and the sign alone cannot say which\n * way a custom `'-P1'` was meant to go.\n */\nfunction reversedInterval(step: SpelledInterval): SpelledInterval {\n return {\n number: step.number,\n quality: step.quality,\n // Negating a zero span yields -0, which compares unequal to 0 under Object.is.\n semitones: step.semitones === 0 ? 0 : -step.semitones,\n descending: !step.descending,\n };\n}\n\n/**\n * The pitch an instrument sounds for a written note.\n *\n * The interval decides the letter, so the spelling of the part survives the\n * conversion: on a clarinet in A a written C sounds A, a written D sounds B,\n * and a written D# sounds B# — a semitone count alone could answer C natural\n * for that last one and lose the letter the part is written on. A note carrying\n * an octave moves register with it — a written C4 sounds A3 — while an\n * octave-less note stays octave-less, so a pure octave transposer leaves it\n * alone.\n *\n * @param note The written note, as the player reads it: a note name, a MIDI\n * number, note data, or a `Note`.\n * @param instrument A built-in instrument name, or an interval naming a\n * transposition the table does not carry.\n * @returns The sounding note, at concert pitch.\n * @throws If the note is malformed, or the instrument is neither a known name\n * nor a spelled interval.\n * @example\n * ```ts\n * import { formatNote, parseNote, toSoundingPitch } from '@libraz/libcantus';\n * formatNote(toSoundingPitch(parseNote('C4'), 'clarinetA')); // 'A3'\n * formatNote(toSoundingPitch('D#4', 'clarinetA')); // 'B#3'\n * formatNote(toSoundingPitch(parseNote('C4'), 'piccolo')); // 'C5'\n * ```\n * @category Pitch & Intervals\n */\nexport function toSoundingPitch(note: NoteLike, instrument: TransposingInstrument): Note {\n return transposeByInterval(toNoteData(note), instrumentTransposition(instrument));\n}\n\n/**\n * The note an instrument must read to sound a given concert pitch.\n *\n * The exact inverse of {@link toSoundingPitch} for every instrument: the same\n * interval applied the other way, so the letter and the octave both come back.\n * A concert A3 is written C4 for a clarinet in A, and a concert E flat 3 is\n * written C4 for an alto saxophone but C5 for a baritone saxophone, which reads\n * the same key an octave lower.\n *\n * @param note The sounding note, at concert pitch: a note name, a MIDI number,\n * note data, or a `Note`.\n * @param instrument A built-in instrument name, or an interval naming a\n * transposition the table does not carry.\n * @returns The written note, as the player reads it.\n * @throws If the note is malformed, or the instrument is neither a known name\n * nor a spelled interval.\n * @example\n * ```ts\n * import { formatNote, parseNote, toWrittenPitch } from '@libraz/libcantus';\n * formatNote(toWrittenPitch(parseNote('A3'), 'clarinetA')); // 'C4'\n * formatNote(toWrittenPitch('Eb3', 'altoSax')); // 'C4'\n * formatNote(toWrittenPitch(parseNote('Eb3'), 'baritoneSax')); // 'C5'\n * ```\n * @category Pitch & Intervals\n */\nexport function toWrittenPitch(note: NoteLike, instrument: TransposingInstrument): Note {\n return transposeByInterval(\n toNoteData(note),\n reversedInterval(instrumentTransposition(instrument)),\n );\n}\n","/**\n * Figured bass: read a bass note and its figures as the chord they name.\n *\n * The figures are intervals above the bass, and every interval a figure does\n * not alter is the one the key's own scale gives. That is the whole rule of the\n * notation, and the reason a realization needs the key: the same `6` names a\n * major triad over one degree of C major and a diminished one over another.\n *\n * The bass arrives spelled, so the intervals above it are spelled too. The\n * third above a G in C minor is the Bb the signature gives, and the accidental\n * that changes it is the one a score prints in front of that note: the leading\n * tone there is a B natural, so the figure is `n`. That is the whole convention\n * for accidentals here — a sign on a figure names its note exactly as it does on\n * the staff — and it is what makes the figures of a real exercise readable as\n * written. The crossed or slashed figure, typed `+`, is the one sign that raises\n * instead of naming.\n */\n\nimport { InvalidInputError, NoSolutionError } from '../../core/errors/index.js';\nimport type { Note, NoteLike } from '../../core/pitch/index.js';\nimport {\n diatonicLetterOf as mod7,\n pitchClassOf as mod12,\n noteToPitchClass,\n toNoteData,\n} from '../../core/pitch/index.js';\nimport { describeRejected } from '../../core/validation/index.js';\nimport type { Chord, ChordQuality } from '../chord/index.js';\nimport { chordQualities, makeChord } from '../chord/index.js';\nimport {\n type KeyLike,\n type ResolvedKey,\n resolveKey,\n type SpelledKeyLike,\n scaleTonesInDegreeOrder,\n} from '../scale/index.js';\nimport { noteNames, spellChord, spellScale } from '../spelling/index.js';\nimport { type ChordLike, toChordData } from '../symbol/index.js';\n\n/** Smallest interval a figure may name above the bass: a second. */\nconst MIN_FIGURE_NUMBER = 2;\n\n/** Largest interval a figure may name above the bass: a ninth. */\nconst MAX_FIGURE_NUMBER = 9;\n\n/** The octave figure, which names the bass itself and so adds no chord tone. */\nconst OCTAVE_FIGURE = 8;\n\n/** Number of tones in the largest stack a figure can name: a seventh chord. */\nconst MAX_STACK_SIZE = 4;\n\n/** The accidental written on a figure, or the absence of one. */\ntype FigureAccidental =\n | 'none'\n | 'sharp'\n | 'doubleSharp'\n | 'flat'\n | 'doubleFlat'\n | 'natural'\n | 'raised';\n\n/** One written figure: an interval above the bass and the accidental on it. */\ntype Figure = { number: number; accidental: FigureAccidental };\n\n/** A figure that moves to another over the same bass, such as `4-3`. */\ntype FigureMotion = { from: Figure; to: Figure };\n\n/** The figures a written string names: the static ones and the moving ones. */\ntype ParsedFigures = { figures: Figure[]; motions: FigureMotion[] };\n\n/** Two-character accidental prefixes, matched before the single-character ones. */\nconst DOUBLE_ACCIDENTALS: Record<string, FigureAccidental> = {\n '##': 'doubleSharp',\n '♯♯': 'doubleSharp',\n bb: 'doubleFlat',\n '♭♭': 'doubleFlat',\n};\n\n/**\n * Single-character accidental prefixes. `+` is the typed form of the slashed or\n * crossed figure, which is the one sign that raises rather than names.\n */\nconst SINGLE_ACCIDENTALS: Record<string, FigureAccidental> = {\n '#': 'sharp',\n '♯': 'sharp',\n '+': 'raised',\n b: 'flat',\n '♭': 'flat',\n n: 'natural',\n '♮': 'natural',\n};\n\n/** Characters that merely separate stacked figures, as the slash in `6/4` does. */\nconst FIGURE_SEPARATORS = new Set([' ', '\\t', ',', '/']);\n\n/** Characters that link a figure to the figure it moves to, as in `4-3`. */\nconst MOTION_MARKS = new Set(['-', '–']);\n\n/**\n * The one convention the accidentals of this notation are read and written\n * under: an accidental on a figure is the accidental a score prints in front of\n * that note, exactly as it is on the staff.\n *\n * Each entry pairs the sign with the alteration it names, and both directions\n * are built from it, so the reader and the writer cannot come to mean different\n * things. Under any other reading the natural sign has nothing to say — `♮`\n * restores a note to natural, which is a statement about the note and not about\n * a distance from the key — and the slashed or crossed figure, whose whole\n * function is to raise, would be a second spelling of `♯`.\n */\nconst PRINTED_ACCIDENTALS: readonly (readonly [FigureAccidental, string, number])[] = [\n ['natural', 'n', 0],\n ['sharp', '#', 1],\n ['doubleSharp', '##', 2],\n ['flat', 'b', -1],\n ['doubleFlat', 'bb', -2],\n];\n\n/** The alteration each printed accidental names. */\nconst PRINTED_ALTERS = new Map<FigureAccidental, number>(\n PRINTED_ACCIDENTALS.map(([accidental, , alter]) => [accidental, alter]),\n);\n\n/** The sign a score prints for each alteration: the inverse of the above. */\nconst ACCIDENTAL_GLYPHS = new Map<number, string>(\n PRINTED_ACCIDENTALS.map(([, glyph, alter]) => [alter, glyph]),\n);\n\n/** An error naming the figure string that was rejected. */\nfunction invalidFigures(figures: string, detail: string): InvalidInputError {\n return new InvalidInputError(`figured bass ${describeRejected(figures)} ${detail}`);\n}\n\n/**\n * The alteration a figure's accidental gives the note it stands on, against the\n * alteration the key's signature gives that letter.\n *\n * A printed accidental names the note outright: `#` over a bass in a key whose\n * signature flattens that letter names the sharp note, not the natural one the\n * signature would be raised to. Two signs read the key instead — the absence of\n * an accidental, which takes what the key gives, and the crossed figure, which\n * raises it a semitone and is the only sign that means \"raise\".\n */\nfunction alteredBy(diatonicAlter: number, accidental: FigureAccidental): number {\n const printed = PRINTED_ALTERS.get(accidental);\n if (printed !== undefined) {\n return printed;\n }\n return accidental === 'raised' ? diatonicAlter + 1 : diatonicAlter;\n}\n\n/**\n * Read one figure — an optional accidental and an optional digit — starting at\n * `start`, and report where the figure ends.\n *\n * An accidental with no digit alters the third above the bass, which is what a\n * lone `#` under a bass note means.\n */\nfunction readFigure(text: string, start: number): { figure: Figure; index: number } {\n let index = start;\n let accidental: FigureAccidental = 'none';\n const pair = DOUBLE_ACCIDENTALS[text.slice(index, index + 2)];\n const single = SINGLE_ACCIDENTALS[text[index] ?? ''];\n if (pair !== undefined) {\n accidental = pair;\n index += 2;\n } else if (single !== undefined) {\n accidental = single;\n index += 1;\n }\n const digit = text[index] ?? '';\n if (digit >= '0' && digit <= '9') {\n const number = Number(digit);\n if (number < MIN_FIGURE_NUMBER || number > MAX_FIGURE_NUMBER) {\n throw invalidFigures(\n text,\n `names the interval ${number}, which is outside ${MIN_FIGURE_NUMBER}..${MAX_FIGURE_NUMBER}`,\n );\n }\n return { figure: { number, accidental }, index: index + 1 };\n }\n if (accidental === 'none') {\n // An empty slice means the string ended where a figure was expected, as in\n // a trailing `4-`; naming the position is more use than naming nothing.\n const found = text.slice(start, start + 1);\n throw invalidFigures(\n text,\n found === ''\n ? `ends where a figure was expected, at position ${start}`\n : `has an unexpected character ${JSON.stringify(found)}`,\n );\n }\n return { figure: { number: 3, accidental }, index };\n}\n\n/** Split a figure string into its static figures and its moving ones. */\nfunction parseFigures(text: string): ParsedFigures {\n const figures: Figure[] = [];\n const motions: FigureMotion[] = [];\n let index = 0;\n while (index < text.length) {\n if (FIGURE_SEPARATORS.has(text[index] ?? '')) {\n index += 1;\n continue;\n }\n const first = readFigure(text, index);\n index = first.index;\n if (!MOTION_MARKS.has(text[index] ?? '')) {\n figures.push(first.figure);\n continue;\n }\n const second = readFigure(text, index + 1);\n index = second.index;\n if (MOTION_MARKS.has(text[index] ?? '')) {\n throw invalidFigures(text, 'chains more than one motion onto a single figure');\n }\n if (first.figure.number === second.figure.number) {\n throw invalidFigures(text, `moves the interval ${first.figure.number} onto itself`);\n }\n motions.push({ from: first.figure, to: second.figure });\n }\n return { figures, motions };\n}\n\n/** What a figure set sounds, and where the chord root sits above the bass. */\ntype FiguredSonority = { intervals: readonly number[]; rootInterval: number };\n\n/** The root-position triad, which every unfigured bass note carries. */\nconst TRIAD: FiguredSonority = { intervals: [3, 5], rootInterval: 1 };\n\n/** The first-inversion triad: the bass is the third of the chord. */\nconst TRIAD_FIRST: FiguredSonority = { intervals: [3, 6], rootInterval: 6 };\n\n/** The second-inversion triad: the bass is the fifth of the chord. */\nconst TRIAD_SECOND: FiguredSonority = { intervals: [4, 6], rootInterval: 4 };\n\n/** The root-position seventh chord. */\nconst SEVENTH: FiguredSonority = { intervals: [3, 5, 7], rootInterval: 1 };\n\n/** The first-inversion seventh chord, written `6/5`. */\nconst SEVENTH_FIRST: FiguredSonority = { intervals: [3, 5, 6], rootInterval: 6 };\n\n/** The second-inversion seventh chord, written `4/3`. */\nconst SEVENTH_SECOND: FiguredSonority = { intervals: [3, 4, 6], rootInterval: 4 };\n\n/** The third-inversion seventh chord, written `4/2` or `2`. */\nconst SEVENTH_THIRD: FiguredSonority = { intervals: [2, 4, 6], rootInterval: 2 };\n\n/**\n * Every figure set the grammar recognizes, keyed by the intervals actually\n * written in ascending order. Both the abbreviation a musician writes and the\n * full stack it stands for are listed, so `6` and `63` name one sonority and\n * `2`, `42` and `642` another.\n */\nconst FIGURE_SETS: ReadonlyMap<string, FiguredSonority> = new Map([\n ['', TRIAD],\n ['3', TRIAD],\n ['5', TRIAD],\n ['3,5', TRIAD],\n ['6', TRIAD_FIRST],\n ['3,6', TRIAD_FIRST],\n ['4,6', TRIAD_SECOND],\n ['7', SEVENTH],\n ['3,7', SEVENTH],\n ['5,7', SEVENTH],\n ['3,5,7', SEVENTH],\n ['5,6', SEVENTH_FIRST],\n ['3,5,6', SEVENTH_FIRST],\n ['3,4', SEVENTH_SECOND],\n ['3,4,6', SEVENTH_SECOND],\n ['2', SEVENTH_THIRD],\n ['2,4', SEVENTH_THIRD],\n ['2,4,6', SEVENTH_THIRD],\n]);\n\n/**\n * The digits a realized sonority is conventionally written with, keyed by its\n * full interval stack. The inverse of the abbreviations in {@link FIGURE_SETS},\n * and the reason {@link figuredBassOf} emits `6` rather than `63`.\n */\nconst FIGURE_ABBREVIATIONS: ReadonlyMap<string, readonly number[]> = new Map([\n [TRIAD.intervals.join(','), []],\n [TRIAD_FIRST.intervals.join(','), [6]],\n [TRIAD_SECOND.intervals.join(','), [6, 4]],\n [SEVENTH.intervals.join(','), [7]],\n [SEVENTH_FIRST.intervals.join(','), [6, 5]],\n [SEVENTH_SECOND.intervals.join(','), [4, 3]],\n [SEVENTH_THIRD.intervals.join(','), [4, 2]],\n]);\n\n/**\n * Index the chord vocabulary by interval template, for the triads and seventh\n * chords a figure can stack. Reading the templates back from the chord builder\n * keeps the two in step: a quality added to the vocabulary becomes reachable\n * from a figure without a second table to maintain.\n */\nfunction tertianQualityIndex(): ReadonlyMap<string, ChordQuality> {\n const index = new Map<string, ChordQuality>();\n for (const quality of chordQualities()) {\n const { intervals } = makeChord(0, quality);\n const template = intervals.join(',');\n // Declaration order decides a tie, exactly as it does for chord detection.\n if (intervals.length >= 3 && intervals.length <= MAX_STACK_SIZE && !index.has(template)) {\n index.set(template, quality);\n }\n }\n return index;\n}\n\nconst TERTIAN_QUALITIES = tertianQualityIndex();\n\n/**\n * The alteration each diatonic letter carries in a key, indexed by letter.\n *\n * This one lookup is what makes an unfigured interval diatonic: the letter of\n * an interval above the bass comes from counting letters, and its accidental\n * comes from the key's own spelling of that letter, so a flat-side key hands\n * back its flats and a harmonic minor its raised leading tone.\n */\nfunction keyLetterAlters(key: ResolvedKey): number[] {\n const size = scaleTonesInDegreeOrder(key.scale).length;\n if (size !== 7) {\n throw new InvalidInputError(`figured bass requires a heptatonic key (received ${size} tones)`);\n }\n const alters: number[] = [];\n // The key's own tonic, so an Ab minor hands back its flats: reading the letters\n // off a scale respelled from the pitch classes would put the exercise in G#\n // minor and give every unfigured third a sharp the score never printed.\n for (const note of spellScale(key.tonic, key.scale)) {\n alters[mod7(note.letter)] = note.alter;\n }\n return alters;\n}\n\n/** The note a figure names: its letter from the bass, its accidental from the key. */\nfunction noteAbove(bass: Note, figure: Figure, alters: readonly number[]): Note {\n const letter = mod7(bass.letter + figure.number - 1);\n return { letter, alter: alteredBy(alters[letter] ?? 0, figure.accidental) };\n}\n\n/** A bass note reduced to the letter and accidental the figures are read from. */\nfunction bareNote(note: Note): Note {\n return { letter: mod7(note.letter), alter: note.alter };\n}\n\n/**\n * Build the chord a resolved figure set names over a bass.\n *\n * The figure set fixes which tone is the root, so the stack is read from that\n * root upward in diatonic thirds and matched against the chord vocabulary. A\n * stack no quality names — the Italian and German sixths, above all — is\n * reported as having no solution rather than being rounded to a chord it is\n * not. The French sixth is not among them: it stacks from its own lowered\n * supertonic, and the figures reach it.\n */\nfunction realizeSonority(\n bass: Note,\n resolved: Figure[],\n text: string,\n alters: readonly number[],\n): { chord: Chord; notes: Note[] } {\n const numbers = [...new Set(resolved.map((figure) => figure.number))].sort((a, b) => a - b);\n const sonority = FIGURE_SETS.get(numbers.join(','));\n if (sonority === undefined) {\n throw invalidFigures(text, 'names no chord over the bass');\n }\n const written = new Map(resolved.map((figure) => [figure.number, figure.accidental]));\n const notes = sonority.intervals.map((number) =>\n noteAbove(bass, { number, accidental: written.get(number) ?? 'none' }, alters),\n );\n const tones = [bass, ...notes];\n const root = tones[[1, ...sonority.intervals].indexOf(sonority.rootInterval)] ?? bass;\n const rootPc = noteToPitchClass(root);\n const stack: number[] = [];\n for (let step = 0; step < tones.length; step += 1) {\n // The chord tones sit two letters apart: root, third, fifth, seventh.\n const tone = tones.find((candidate) => mod7(candidate.letter - root.letter) === step * 2);\n if (tone === undefined) {\n throw new NoSolutionError(\n `figured bass ${describeRejected(text)} sounds ${noteNames(tones).join(' ')}, which does not stack in thirds`,\n );\n }\n stack.push(mod12(noteToPitchClass(tone) - rootPc));\n }\n const quality = TERTIAN_QUALITIES.get(stack.join(','));\n if (quality === undefined) {\n throw new NoSolutionError(\n `figured bass ${describeRejected(text)} sounds ${noteNames(tones).join(' ')}, which no chord quality names`,\n );\n }\n const chord = makeChord(rootPc, quality, noteToPitchClass(bass));\n chord.rootSpelling = { letter: root.letter, alter: root.alter };\n chord.bassSpelling = { letter: bass.letter, alter: bass.alter };\n return { chord, notes };\n}\n\n/**\n * A figure that moves over a held bass, such as the `4-3` suspension.\n *\n * @category Functional Harmony\n */\nexport type FiguredBassSuspension = {\n /** The interval above the bass that sounds first: the suspended note. */\n from: number;\n /** The interval it moves to, which is the one the chord contains. */\n to: number;\n /** The suspended note, spelled. */\n note: Note;\n /** The note it moves to, spelled. */\n resolution: Note;\n};\n\n/**\n * Everything a figured bass states: the chord, its spelled notes, and any\n * figure that moves.\n *\n * @category Functional Harmony\n */\nexport type FiguredBassRealization = {\n /**\n * The chord the figures name, carrying the given bass as its `bassPc` and\n * the letter spellings of its root and bass as enharmonic hints.\n */\n chord: Chord;\n /**\n * The spelled notes, bass first and then each figured interval in ascending\n * order. They carry no octave: figures fix the intervals above the bass, not\n * the register the upper voices take them in.\n */\n notes: Note[];\n /**\n * Every moving figure, in written order, or an empty array for a static one.\n * The chord above is the harmony these resolve into.\n */\n suspensions: FiguredBassSuspension[];\n};\n\n/**\n * Read a figured bass in full: the chord, its spelled notes, and its\n * suspensions.\n *\n * The figure grammar, in the forms this accepts:\n *\n * - Nothing at all, `5`, `3` or `53` — the root-position triad.\n * - `6` or `63` — the first-inversion triad; `64` — the second-inversion triad.\n * - `7` — the root-position seventh chord; `65`, `43`, and `42` (also written\n * `2`) — its first, second, and third inversions. The full stacks `753`,\n * `653`, `643` and `642` name the same four chords.\n * - Stacked figures may be written run together (`64`) or separated by a slash,\n * a comma or a space (`6/4`), which is the same figure either way.\n * - An accidental in front of a figure names that interval's note, exactly as\n * the same sign does on the staff: `#6`, `b7`, `#43`, and `n3` for a note the\n * signature would otherwise flatten. `##` and `bb` name the double\n * accidentals. An accidental with no figure applies to the third above the\n * bass, so a lone `n` over the dominant of C minor names its leading tone. The\n * Unicode accidentals are accepted alongside the typed ones.\n * - A slashed or crossed figure raises the interval the key gives by a semitone,\n * whatever accidental that lands on, and is typed `+`. It is the one sign here\n * that moves an interval rather than naming it, which is why the notation\n * carries it beside the printed accidentals: `+3` over the dominant of C minor\n * and `+3` over the dominant of A minor are each that key's leading tone,\n * written `n3` and `#3` respectively.\n * - A figure that moves is written with a hyphen: `4-3`, `9-8`, `7-6`, and any\n * other pair. Each side may carry its own accidental, as `4-#3` does in a\n * minor key. Moving figures may stand beside static ones (`5 4-3`).\n *\n * Every interval no accidental touches is the one the key's scale gives, which\n * is why the key is required and why the same figure names different qualities\n * over different degrees.\n *\n * A moving figure resolves into the chord: `4-3` over a G in C major returns\n * the G major triad, and the suspended C is reported as a suspension rather\n * than as a chord tone. A suspension is a melodic event over a single harmony —\n * the sounding chord does not change when it resolves — so naming the\n * resolution is the reading that answers \"which chord is this\", and the record\n * of what was suspended is kept beside it rather than thrown away.\n *\n * @param bass The bass note, spelled: a note name, a MIDI number, note data, or\n * a `Note`; its octave, if any, is not used.\n * @param figures The figures written under the bass; empty for an unfigured\n * note.\n * @param key The prevailing key, which supplies every unaltered interval, as a\n * key name, a key/scale, or a `Key`.\n * @returns The chord, its spelled notes, and any suspensions.\n * @throws If the figures are malformed or name no chord\n * ({@link InvalidInputError}), if the key is not heptatonic\n * ({@link InvalidInputError}), or if the figures sound a chord no\n * {@link ChordQuality} names, such as an augmented sixth\n * ({@link NoSolutionError}).\n * @example\n * ```ts\n * import { figuredBassRealization, majorKey, noteNames, parseNote } from '@libraz/libcantus';\n * const realized = figuredBassRealization(parseNote('G'), '4-3', majorKey(0));\n * noteNames(realized.notes); // ['G', 'B', 'D']\n * realized.suspensions[0]?.from; // 4 — the suspended C above the bass\n * ```\n * @category Functional Harmony\n */\nexport function figuredBassRealization(\n bass: NoteLike,\n figures: string,\n key: KeyLike,\n): FiguredBassRealization {\n if (typeof figures !== 'string') {\n throw new InvalidInputError(`figured bass must be a string; received ${typeof figures}`);\n }\n const bassData = toNoteData(bass);\n const alters = keyLetterAlters(resolveKey(key));\n // Reading the pitch class validates the note's fields before its letter is\n // used for the interval arithmetic above it.\n noteToPitchClass(bassData);\n const bassNote = bareNote(bassData);\n const text = figures.trim();\n const { figures: written, motions } = parseFigures(text);\n const suspensions = motions.map((motion) => ({\n from: motion.from.number,\n to: motion.to.number,\n note: noteAbove(bassNote, motion.from, alters),\n resolution: noteAbove(bassNote, motion.to, alters),\n }));\n // A figure resolving onto the octave lands on the bass itself, which is\n // already sounding and so adds nothing to the stack: `9-8` resolves to a\n // plain root-position triad.\n const resolved = [\n ...written,\n ...motions.map((motion) => motion.to).filter((figure) => figure.number !== OCTAVE_FIGURE),\n ];\n const { chord, notes } = realizeSonority(bassNote, resolved, text, alters);\n return { chord, notes: [bassNote, ...notes], suspensions };\n}\n\n/**\n * Realize a figured bass: the chord a bass note and its figures name in a key.\n *\n * The chord carries the given bass as its `bassPc`, so an inversion reads as\n * one everywhere the library measures a bass against a root, and carries the\n * letter spellings of its root and bass so the realization can be respelled\n * without losing the accidentals the figures asked for.\n *\n * See {@link figuredBassRealization} for the figure grammar this accepts and\n * for the spelled notes and suspensions it drops. A moving figure such as\n * `4-3` returns the chord it resolves into.\n *\n * @param bass The bass note, spelled; its octave, if any, is not used.\n * @param figures The figures written under the bass; empty for an unfigured\n * note.\n * @param key The prevailing key, which supplies every unaltered interval.\n * @param bass The bass note, spelled: a note name, a MIDI number, note data, or\n * a `Note`.\n * @param figures The figures written under the bass.\n * @param key The prevailing key, as a key name, a key/scale, or a `Key`.\n * @returns The chord the figures name.\n * @throws If the figures are malformed or name no chord\n * ({@link InvalidInputError}), if the key is not heptatonic\n * ({@link InvalidInputError}), or if the figures sound a chord no\n * {@link ChordQuality} names ({@link NoSolutionError}).\n * @example\n * ```ts\n * import { majorKey, minorKey, parseNote, realizeFiguredBass } from '@libraz/libcantus';\n * realizeFiguredBass(parseNote('B'), '6', majorKey(0));\n * // G major with B in the bass: { rootPc: 7, quality: 'maj', bassPc: 11, ... }\n * realizeFiguredBass(parseNote('G'), 'n', minorKey(0));\n * // G major in C minor: the leading tone is the B natural the score prints\n * ```\n * @category Functional Harmony\n */\nexport function realizeFiguredBass(bass: NoteLike, figures: string, key: KeyLike): Chord {\n return figuredBassRealization(bass, figures, key).chord;\n}\n\n/**\n * Write a chord as the figures a bass would carry under it, in a key.\n *\n * The inverse of {@link realizeFiguredBass} for every chord the figure grammar\n * reaches: the conventional abbreviation for the inversion (`6`, `64`, `7`,\n * `65`, `43`, `42`, or nothing at all for a root-position triad), with an\n * accidental on any interval the key does not already give. The accidental is\n * the sign the score prints in front of that note, so the third of the dominant\n * in C minor — a B natural under a signature carrying B flat — is written `n3`\n * rather than as a raise from the flat the key gives. An altered interval the\n * abbreviation would leave implied is written out, so the figure names the\n * interval it lands on rather than the bare accidental a score puts under the\n * bass — the figures this returns read back as the same chord.\n *\n * What this returns is read back before it is returned, so the two directions\n * cannot disagree: a chord the digits would name only approximately has no\n * figures rather than figures that fail to realize. What that excludes is a\n * stack, not a family of chords — the Italian and German sixths stack in thirds\n * over no root of their own and have no figures, while the French sixth stacks\n * from its lowered supertonic and is written `#643`, which realizes back to the\n * chord it was written from.\n *\n * @param chord The chord to figure, as a chord symbol, chord data, or a\n * `Chord`; its `bassPc`, when present, is the bass the intervals are measured\n * above.\n * @param key The prevailing key, which decides which intervals need no\n * accidental, as a key name, a key/scale, or a `Key`.\n * @returns The figures, as {@link realizeFiguredBass} accepts them.\n * @throws If the key is not heptatonic ({@link InvalidInputError}), or if no\n * figure names the chord — its bass is not one of its tones, its tones do not\n * stack in diatonic thirds above that bass, or it is an added-tone or\n * extended chord the notation has no abbreviation for\n * ({@link NoSolutionError}).\n * @example\n * ```ts\n * import { figuredBassOf, majorKey, makeChord } from '@libraz/libcantus';\n * figuredBassOf(makeChord(7, 'maj', 11), majorKey(0)); // '6' — G major over B\n * figuredBassOf(makeChord(7, 'dom7', 2), majorKey(0)); // '43' — G7 over D\n * ```\n * @category Functional Harmony\n */\nexport function figuredBassOf(chord: ChordLike, key: SpelledKeyLike): string {\n const data = toChordData(chord);\n const resolved = resolveKey(key);\n const alters = keyLetterAlters(resolved);\n const tones = spellChord(data, resolved.tonic, resolved.scale);\n const bassPc = mod12(data.bassPc ?? data.rootPc);\n const bass = tones.find((tone) => noteToPitchClass(tone) === bassPc);\n if (bass === undefined) {\n throw new NoSolutionError(\n \"figured bass measures intervals above a chord tone, and this chord's bass is not one of its tones\",\n );\n }\n const written = new Map<number, Note>();\n for (const tone of tones) {\n if (tone !== bass) {\n written.set(mod7(tone.letter - bass.letter) + 1, tone);\n }\n }\n const digits = FIGURE_ABBREVIATIONS.get([...written.keys()].sort((a, b) => a - b).join(','));\n if (digits === undefined) {\n throw new NoSolutionError(\n `no figured bass names ${noteNames(tones).join(' ')} over ${noteNames([bass])[0]}`,\n );\n }\n const accidentals = new Map<number, string>();\n for (const [number, tone] of written) {\n if (tone.alter === (alters[mod7(tone.letter)] ?? 0)) {\n // The signature already writes this note, so the score prints no sign.\n continue;\n }\n const glyph = ACCIDENTAL_GLYPHS.get(tone.alter);\n if (glyph === undefined) {\n throw new NoSolutionError(`no accidental writes a note altered by ${tone.alter} semitones`);\n }\n accidentals.set(number, glyph);\n }\n // Figures are written from the top down, and an altered interval the\n // abbreviation leaves implied has to be named for its accidental to land on\n // anything: a raised third under a `6` is written `6#3`, not `#6`.\n const figures = [...new Set([...digits, ...accidentals.keys()])]\n .sort((a, b) => b - a)\n .map((number) => `${accidentals.get(number) ?? ''}${number}`)\n .join('');\n assertRealizesAs(bass, figures, resolved, tones);\n return figures;\n}\n\n/** The pitch classes a set of spelled notes sounds, ascending. */\nfunction pitchClassesOf(notes: readonly Note[]): number[] {\n return [...new Set(notes.map(noteToPitchClass))].sort((a, b) => a - b);\n}\n\n/**\n * Require the figures just written to read back as the chord they were written\n * from, and report the ones that do not as having no figured bass.\n *\n * The digits are chosen by which intervals are written above the bass, which is\n * a weaker test than the one a realization applies: a chord may occupy the\n * interval positions of an inversion without stacking in thirds over any root,\n * and the Italian and German sixths do exactly that. Reading the figures back\n * is what keeps the two directions from disagreeing about which chords the\n * notation reaches, rather than a second table that would drift from the first.\n */\nfunction assertRealizesAs(bass: Note, figures: string, key: ResolvedKey, tones: Note[]): void {\n const noFigures = (detail: string): NoSolutionError =>\n new NoSolutionError(\n `no figured bass names ${noteNames(tones).join(' ')} over ${noteNames([bass])[0]}: the figures ${describeRejected(figures)} ${detail}`,\n );\n let realized: FiguredBassRealization;\n try {\n realized = figuredBassRealization(bass, figures, key);\n } catch (error) {\n throw error instanceof NoSolutionError ? noFigures('read back as no chord at all') : error;\n }\n const written = pitchClassesOf(realized.notes);\n const expected = pitchClassesOf(tones);\n if (written.length !== expected.length || written.some((pc, index) => pc !== expected[index])) {\n throw noFigures(`sound ${noteNames(realized.notes).join(' ')} instead`);\n }\n}\n","/**\n * The cross relation, written once.\n *\n * One letter carrying two different accidentals in two different voices across\n * a chord change is a rule the library states twice: the voicing search has to\n * refuse the contradiction, and the checker has to report it. Both read it from\n * here, so a voicing the search calls clean is one the checker calls clean.\n */\n\nimport type { Note } from '../../core/pitch/index.js';\nimport {\n diatonicLetterOf,\n noteToPitchClass,\n pitchClassOf,\n spelledInterval,\n} from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport type { Chord } from '../chord/index.js';\nimport {\n appliedDominantTarget,\n hasDominantSonority,\n isNeapolitanChordOf,\n} from '../tendency/index.js';\n\n/** One chord of an exercise as this rule reads it: what sounds, and what it is. */\nexport type ChromaticMoment = {\n /** The written notes, ascending (index 0 = lowest). */\n notes: readonly Note[];\n /** The chord they realize. */\n chord: Chord;\n};\n\n/**\n * What stands on either side of the pair being judged. Each is what tells a\n * chromatic tone that was led into by step from one that was sprung, and an\n * applied dominant from the diatonic triad that sounds the same; a caller that\n * cannot see that far leaves it out, and the rule reads the pair as exposed.\n */\nexport type ChromaticContext = {\n /** The voicing before the pair, naming how the earlier notes were reached. */\n before?: readonly Note[];\n /** The chord after the pair, naming what the later chord resolves to. */\n after?: Chord;\n};\n\n/** One exposed contradiction, by the voices holding the two notes. */\nexport type CrossRelation = {\n /** The voice holding the earlier note. */\n earlierVoice: number;\n /** The voice holding the later one. */\n laterVoice: number;\n /** The earlier note itself. */\n earlier: Note;\n /** The later note itself. */\n later: Note;\n};\n\n/**\n * Whether a chord resolves as an applied dominant: the dominant's sonority,\n * taken down a fifth onto a degree the key can make a local tonic, with the\n * chord it names actually following.\n *\n * Named for the resolution because the resolution is the only thing this adds:\n * which degrees may be tonicized is asked of the theory layer, so a chord\n * exempted here is one the analysis layer can write an applied numeral for.\n * What the two do differ in is what they are for — there a chord is an applied\n * dominant by what it is, here only by what comes next. The same major triad on\n * the third degree of a major key is V/vi where vi follows and a plain III\n * where nothing does, and only the first of the two licenses the chromatic tone\n * it introduces.\n */\nfunction resolvesAsAppliedDominant(chord: Chord, next: Chord | undefined, key: KeyScale): boolean {\n if (next === undefined || !hasDominantSonority(chord)) {\n return false;\n }\n const target = appliedDominantTarget(chord.rootPc + 5, key);\n return target !== null && pitchClassOf(next.rootPc) === target.rootPc;\n}\n\n/**\n * Whether a voicing sounds an augmented sixth above the lowered submediant.\n *\n * That interval is what the Italian, French and German chords are all named\n * for, and it is the only thing separating them from the bVI7 they sound like,\n * so it is read off the written letters rather than the pitch classes.\n */\nfunction soundsAugmentedSixth(moment: ChromaticMoment, key: KeyScale): boolean {\n const bass = moment.notes[0];\n if (bass === undefined || noteToPitchClass(bass) !== pitchClassOf(key.rootPc + 8)) {\n return false;\n }\n return moment.notes.some((note, voice) => {\n if (voice === 0) {\n return false;\n }\n const interval = spelledInterval(bass, note);\n return ((interval.number - 1) % 7) + 1 === 6 && pitchClassOf(interval.semitones) === 10;\n });\n}\n\n/**\n * Whether a chord is one of the chromatic harmonies whose own definition\n * contains the contradiction: an applied dominant, the Neapolitan, or an\n * augmented sixth. Approaching or leaving one of these is where the textbooks\n * license the cross relation, since the chord cannot be written without it.\n *\n * The exemption is for chromatic harmony, so a chord the key already contains\n * cannot claim it: a mode with a lowered second of its own — phrygian, locrian —\n * sounds the major triad on that degree as its native II, and reading it as the\n * Neapolitan would exempt the key's own chord from the rule every other diatonic\n * chord answers to.\n */\nfunction isChromaticHarmony(\n moment: ChromaticMoment,\n next: Chord | undefined,\n key: KeyScale,\n): boolean {\n return (\n resolvesAsAppliedDominant(moment.chord, next, key) ||\n isNeapolitanChordOf(moment.chord, key) ||\n soundsAugmentedSixth(moment, key)\n );\n}\n\n/** Whether a voice reaches its note by a diatonic step from the voicing before. */\nfunction approachedByStep(\n before: readonly Note[] | undefined,\n moment: readonly Note[],\n voice: number,\n): boolean {\n const earlier = before?.[voice];\n const later = moment[voice];\n if (earlier === undefined || later === undefined) {\n return false;\n }\n return spelledInterval(earlier, later).number === 2;\n}\n\n/** Whether a voice is the bass or the top voice of its chord. */\nexport function isOuterVoice(voice: number, notes: readonly Note[]): boolean {\n return voice === 0 || voice === notes.length - 1;\n}\n\n/**\n * Every cross relation between two consecutive chords: the same letter carrying\n * different accidentals in two different voices. Within one voice the same\n * motion is an ordinary chromatic inflection, so a voice is never compared with\n * itself.\n *\n * What the rule forbids is a semitone exposed across the texture, and three\n * things take that exposure away, each of which is reason enough on its own:\n *\n * - either of the two notes is led into by step, which is how a chromatic tone\n * is introduced rather than sprung;\n * - both notes lie in inner voices, where the classical norm is markedly\n * milder than it is between the outer ones;\n * - the chord being left or reached is an applied dominant, the Neapolitan, or\n * an augmented sixth, whose definition contains the altered degree.\n *\n * Each pair of voices is reported once. The two voices are walked in both\n * directions, so the same clash is met twice and the second reading is dropped.\n *\n * @param from The chord being left.\n * @param to The chord being reached.\n * @param key The key the exercise is written in.\n * @param context What stands on either side of the pair, where the caller knows\n * it. A missing neighbour is read as no exemption rather than as one.\n * @returns One entry per contradicting pair of voices, in voice order.\n */\nexport function crossRelations(\n from: ChromaticMoment,\n to: ChromaticMoment,\n key: KeyScale,\n context: ChromaticContext = {},\n): CrossRelation[] {\n const found: CrossRelation[] = [];\n const chromatic =\n isChromaticHarmony(from, to.chord, key) || isChromaticHarmony(to, context.after, key);\n if (chromatic) {\n return found;\n }\n const reported = new Set<string>();\n for (let first = 0; first < from.notes.length; first += 1) {\n for (let second = 0; second < to.notes.length; second += 1) {\n if (first === second) {\n continue;\n }\n const earlier = from.notes[first];\n const later = to.notes[second];\n if (earlier === undefined || later === undefined) {\n continue;\n }\n const letter = diatonicLetterOf(earlier.letter);\n if (letter !== diatonicLetterOf(later.letter) || earlier.alter === later.alter) {\n continue;\n }\n if (\n approachedByStep(context.before, from.notes, first) ||\n approachedByStep(from.notes, to.notes, second) ||\n (!isOuterVoice(first, from.notes) && !isOuterVoice(second, to.notes))\n ) {\n continue;\n }\n const pair = `${letter}:${Math.min(first, second)}:${Math.max(first, second)}`;\n if (reported.has(pair)) {\n continue;\n }\n reported.add(pair);\n found.push({ earlierVoice: first, laterVoice: second, earlier, later });\n }\n }\n return found;\n}\n","/**\n * Tendency tones: which pitch class of a chord owes its next move to a rule.\n *\n * The voicing search and the part-writing checker both need this, and they have\n * to agree on it. A generator resolving a tone the checker does not recognize —\n * or holding one the checker thinks must move — would report its own output as\n * faulty, so both read the same predicates from here.\n */\n\nimport type { Note } from '../../core/pitch/index.js';\nimport { noteToMidi, pitchClassOf, spelledInterval } from '../../core/pitch/index.js';\nimport type { Chord } from '../chord/index.js';\nimport { isAugmentedMelodicInterval } from '../counterpoint/index.js';\nimport { type KeyLike, resolveKey } from '../scale/index.js';\nimport { spellPitchClass } from '../spelling/index.js';\nimport { isDescendingStep } from '../tendency/index.js';\n\n/**\n * How one chord's tones are written: one spelling per pitch class, and the MIDI\n * pitch each of those spellings sounds at octave 0.\n *\n * The search reads a candidate's letters thousands of times per chord, and a\n * spelling depends on the pitch class alone once the chord and the key are\n * fixed, so the twelve of them are worked out once and the octave is arithmetic.\n */\nexport type SpellingTable = {\n /** The octave-less spelling of each pitch class 0-11. */\n notes: readonly Note[];\n /** The MIDI pitch each of those spellings sounds at octave 0. */\n bases: readonly number[];\n};\n\n/**\n * Name every pitch class the way {@link spellVoicing} names it under one chord.\n *\n * The generator and the checker have to read the same letters — an augmented\n * second is a minor third by ear, so a generator spelling its own output by\n * another rule would write intervals its checker forbids — and both go through\n * {@link spellPitchClass} with the chord as context.\n *\n * @param keyLike The prevailing key, in any form a key is held in.\n * @param chord The chord sounding, or undefined to let the key alone decide.\n * @returns The table, indexed by pitch class.\n */\nexport function spellingTable(keyLike: KeyLike, chord?: Chord): SpellingTable {\n // Read whole rather than reduced: an exercise written in Ab minor is judged on\n // the letters it is written with, and a key that arrived carrying its tonic\n // would otherwise have that tonic derived back from the pitch classes.\n const { tonic, scale: key } = resolveKey(keyLike);\n const notes: Note[] = [];\n const bases: number[] = [];\n for (let pc = 0; pc < 12; pc += 1) {\n const note =\n chord === undefined\n ? spellPitchClass(pc, tonic, key)\n : spellPitchClass(pc, tonic, key, { chord });\n notes.push(note);\n bases.push(noteToMidi({ letter: note.letter, alter: note.alter, octave: 0 }));\n }\n return { notes, bases };\n}\n\n/** The spelled note a table gives a sounding pitch, carrying its octave. */\nexport function spelledAt(table: SpellingTable, pitch: number): Note | undefined {\n const pc = pitchClassOf(pitch);\n const note = table.notes[pc];\n const base = table.bases[pc];\n if (note === undefined || base === undefined) {\n return undefined;\n }\n return { letter: note.letter, alter: note.alter, octave: (pitch - base) / 12 };\n}\n\n/**\n * Whether a voice moving between two sounding pitches writes an augmented\n * interval, read through the same spelling the checker applies to the result.\n *\n * @param from The table of the chord left.\n * @param fromPitch The pitch the voice sang.\n * @param to The table of the chord arrived on.\n * @param toPitch The pitch it moved to.\n * @returns True when the written interval is augmented.\n */\nexport function movesByAugmentedInterval(\n from: SpellingTable,\n fromPitch: number,\n to: SpellingTable,\n toPitch: number,\n): boolean {\n const earlier = spelledAt(from, fromPitch);\n const later = spelledAt(to, toPitch);\n if (earlier === undefined || later === undefined) {\n return false;\n }\n return isAugmentedMelodicInterval(earlier, later);\n}\n\n/**\n * Whether a voice moving between two sounding pitches falls by a diatonic step,\n * which is how a chordal seventh resolves.\n *\n * Read through the same spelling the checker applies to the result: two\n * semitones down is a diminished third as often as a major second once the\n * letters are settled, and only one of the two is a resolution.\n *\n * @param from The table of the chord left.\n * @param fromPitch The pitch the voice sang.\n * @param to The table of the chord arrived on.\n * @param toPitch The pitch it moved to.\n * @returns True when the written interval is a descending second.\n */\nexport function movesByDescendingStep(\n from: SpellingTable,\n fromPitch: number,\n to: SpellingTable,\n toPitch: number,\n): boolean {\n const earlier = spelledAt(from, fromPitch);\n const later = spelledAt(to, toPitch);\n if (earlier === undefined || later === undefined) {\n return false;\n }\n return isDescendingStep(spelledInterval(earlier, later));\n}\n","import { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\n\nexport { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\n\nimport { NoSolutionError } from '../../core/errors/index.js';\nimport type { Note } from '../../core/pitch/index.js';\nimport { diatonicLetterOf } from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport type { Chord } from '../chord/index.js';\nimport { chordPitchClasses, chordToneRole } from '../chord/index.js';\nimport {\n createsHiddenParallelPerfect,\n createsParallelPerfect,\n createsVoiceOverlap,\n} from '../counterpoint/index.js';\nimport { crossRelations } from '../partwriting/cross-relation.js';\nimport type { ResolvedKey } from '../scale/index.js';\nimport {\n isFrustratedLeadingTone,\n isFunctioningLeadingTone,\n leadingTonePcOf,\n seventhPcOf,\n} from '../tendency/index.js';\nimport type { VoiceRange } from './satb.js';\nimport type { SpellingTable } from './tendency.js';\nimport {\n movesByAugmentedInterval,\n movesByDescendingStep,\n spelledAt,\n spellingTable,\n} from './tendency.js';\n\n/** Default maximum spacing between adjacent upper voices (one octave). */\nexport const DEFAULT_MAX_SPACING = 12;\n\n/** Score penalty per counterpoint violation between consecutive voicings. */\nexport const VIOLATION_PENALTY = 1000;\n/** Score penalty per chord tone absent from a voicing. */\nconst MISSING_TONE_PENALTY = 500;\n/**\n * Score penalty for omitting the chord's fifth. Far milder than any other\n * missing tone: the fifth carries no identity, and the textbook resolution of a\n * complete dominant seventh onto a tonic triad — every tendency tone resolving\n * inward — leaves the tonic without its fifth. Weighted below\n * {@link RESOLUTION_PENALTY} so a resolution can buy the omission, and above\n * {@link POOR_DOUBLING_PENALTY} so nothing else can.\n */\nconst MISSING_FIFTH_PENALTY = 20;\n/** Score penalty per doubled tone that is neither the root nor the fifth. */\nconst POOR_DOUBLING_PENALTY = 4;\n/** Penalty for adjacent voices sharing exactly the same MIDI pitch. */\nconst UNISON_PENALTY = 100;\n/**\n * Score penalty for doubling the key's leading tone. Doubling it guarantees\n * parallel octaves on its resolution, so it is a rule violation rather than a\n * matter of taste — weighted far above {@link POOR_DOUBLING_PENALTY}, but below\n * a missing chord tone, which damages the chord itself.\n */\nconst LEADING_TONE_DOUBLING_PENALTY = 200;\n/**\n * Score penalty per tendency tone that fails to resolve. Well above the few\n * semitones of extra motion a correct resolution usually costs, so smoothness\n * can never buy an unresolved seventh, and well below\n * {@link VIOLATION_PENALTY}, so avoiding parallels still comes first.\n */\nexport const RESOLUTION_PENALTY = 200;\n/** Hard cap on candidate voicings evaluated per chord, keeping the search bounded. */\nconst DEFAULT_MAX_CANDIDATES = 4000;\n/**\n * Hard cap on the candidate pairs one chord's lookahead may weigh. A lookahead\n * reads every candidate of the chord against every candidate of the chord after\n * it, so the pairs grow as the square of a per-chord search that is itself only\n * bounded at a few thousand. Past this many the lookahead is dropped for that\n * chord — for all of its candidates alike, so the choice is never biased by\n * where the cap happened to fall.\n */\nexport const MAX_LOOKAHEAD_PAIRS = 1_000_000;\n/**\n * Hard cap on search-tree nodes visited per chord. The candidate cap alone only\n * counts completed voicings, so a search whose upper voices admit no chord tone\n * would expand the whole cartesian product without ever reaching a leaf.\n */\nconst SEARCH_NODES_PER_CANDIDATE = 16;\n\n/**\n * Score penalty for a hidden/direct perfect fifth or octave reached on the\n * outer-voice (bass–soprano) pair. The checker reports one as a broken rule, so\n * the search cannot treat it as a nudge: the weight is heavy enough that the\n * textbook escape — tripling the root and omitting the fifth, which costs\n * {@link MISSING_FIFTH_PENALTY} — is the cheaper answer. It stays below\n * {@link VIOLATION_PENALTY} because a hidden perfect is discouraged where a\n * true parallel is forbidden, which is the order the rules are ranked in.\n *\n * It is scored beside the other penalties rather than inside\n * {@link leadingCost}, whose published sibling `voiceLeadingCost` is defined as\n * the summed semitone motion and nothing else.\n */\nexport const HIDDEN_PERFECT_PENALTY = 100;\n\n/**\n * Score penalty per cross relation written between two consecutive voicings.\n *\n * The checker reports the exposed contradiction as a broken rule, so the search\n * has to be able to see it: without a term of its own the rule was stated on\n * one side only, and a placement that spells the clash away could never be\n * preferred. Weighted with {@link HIDDEN_PERFECT_PENALTY}, the other rule about\n * an interval being exposed rather than forbidden outright, so a cross relation\n * is worth a little rougher voice leading but never a parallel or a missing\n * chord tone — some of them cannot be voiced away at all.\n */\nexport const CROSS_RELATION_PENALTY = HIDDEN_PERFECT_PENALTY;\n\n/**\n * Score penalty per voice moving by an augmented interval. The augmented second\n * between the lowered sixth and the raised seventh is what a minor-key search\n * writes if nothing stops it, and the checker reports it as a broken rule\n * rather than as rough writing — so it is weighted with the counterpoint\n * violations, where no amount of smoother motion can buy it back, rather than\n * alongside them. A chord whose every candidate writes one is still voiced: the\n * penalty then falls on all of them alike and the rest of the scoring decides.\n */\nexport const AUGMENTED_MELODIC_PENALTY = VIOLATION_PENALTY;\n\n/**\n * Candidate voicings of one chord, held flat: voice `v` of candidate `i` sits at\n * `pitches[i * voices + v]`.\n *\n * A four-voice search reaches four thousand candidates, so a voicing per\n * candidate would be four thousand short arrays per chord — the bulk of what\n * voicing a lead sheet allocates. One buffer holds them all instead, and\n * {@link voiceProgression} reuses the same buffer for every chord, so the whole\n * progression allocates it once.\n */\nexport type VoicingCandidates = {\n /** The candidate pitches, valid up to `count * voices`. */\n pitches: Int32Array;\n /** How many candidates the last enumeration wrote. */\n count: number;\n /** Voices per candidate, i.e. the stride of `pitches`. */\n voices: number;\n};\n\n/** Candidates a fresh buffer holds before it has to grow. */\nconst INITIAL_CANDIDATE_CAPACITY = 512;\n\n/** An empty candidate buffer, ready to be filled by {@link enumerateVoicings}. */\nexport function createCandidateBuffer(): VoicingCandidates {\n return { pitches: new Int32Array(0), count: 0, voices: 0 };\n}\n\n/**\n * All MIDI pitches of a pitch class inside an inclusive range, ordered from the\n * centre of the range outward (ties break toward the lower pitch).\n *\n * The ordering is within one pitch class, not across them: the enumeration\n * above visits the chord's pitch classes in their own order and walks each\n * one's octaves centre-outward. So a candidate set truncated at\n * {@link DEFAULT_MAX_CANDIDATES} holds the register-central octaves of the\n * pitch classes it reached, rather than the low octaves a plain ascending scan\n * would visit first — which is what this ordering buys, and no more.\n */\nfunction pitchesForPc(pc: number, range: VoiceRange): number[] {\n const result: number[] = [];\n for (let midi = Math.ceil(range.min); midi <= range.max; midi += 1) {\n if (pitchClass(midi) === pc) {\n result.push(midi);\n }\n }\n const center = (range.min + range.max) / 2;\n return result.sort((a, b) => {\n const da = Math.abs(a - center);\n const db = Math.abs(b - center);\n return da === db ? a - b : da - db;\n });\n}\n\n/**\n * Enumerate candidate voicings for a chord: the bass takes the chord's bass\n * (or root) pitch class at each available octave, and each upper voice takes\n * any chord pitch class within its range. Candidates are built in ascending\n * voice order and pruned to exclude voice crossings and over-wide adjacent\n * spacing (the bass–tenor pair is allowed an extra octave, per convention).\n * Enumeration is deterministic and capped at `maxCandidates` completed\n * voicings and a proportional number of visited nodes.\n *\n * Each voice's admissible pitches are resolved once up front rather than\n * recomputed at every node, and a voice that admits no chord tone at all fails\n * immediately: continuing would explore every combination of the voices below\n * it only to reach no leaf.\n *\n * @param into A buffer to fill, reused and grown in place. Pass the same one\n * for every chord of a progression so the search allocates once rather than\n * once per chord.\n * @throws If any voice's range contains no pitch of any chord tone.\n */\nexport function enumerateVoicings(\n chord: Chord,\n ranges: VoiceRange[],\n maxSpacing: number,\n maxCandidates = DEFAULT_MAX_CANDIDATES,\n into: VoicingCandidates = createCandidateBuffer(),\n): VoicingCandidates {\n const chordPcs = chordPitchClasses(chord);\n const bassPc = pitchClass(chord.bassPc ?? chord.rootPc);\n // Pitch-class-major order, matching the enumeration order of the search.\n const byVoice = ranges.map((range, voice) =>\n (voice === 0 ? [bassPc] : chordPcs).flatMap((pc) => pitchesForPc(pc, range)),\n );\n for (let voice = 0; voice < byVoice.length; voice += 1) {\n if ((byVoice[voice] ?? []).length === 0) {\n throw new NoSolutionError('no voicing satisfies the given ranges');\n }\n }\n const voices = ranges.length;\n into.voices = voices;\n into.count = 0;\n let capacity = Math.floor(into.pitches.length / Math.max(1, voices));\n if (capacity === 0) {\n capacity = Math.min(maxCandidates, INITIAL_CANDIDATE_CAPACITY);\n into.pitches = new Int32Array(capacity * voices);\n }\n const current = new Int32Array(voices);\n let nodes = 0;\n const maxSearchNodes = maxCandidates * SEARCH_NODES_PER_CANDIDATE;\n const build = (voice: number): void => {\n nodes += 1;\n if (into.count >= maxCandidates || nodes > maxSearchNodes) {\n return;\n }\n if (voice === voices) {\n if (into.count === capacity) {\n capacity = Math.min(maxCandidates, capacity * 2);\n const grown = new Int32Array(capacity * voices);\n grown.set(into.pitches.subarray(0, into.count * voices));\n into.pitches = grown;\n }\n into.pitches.set(current, into.count * voices);\n into.count += 1;\n return;\n }\n const prev = voice === 0 ? undefined : current[voice - 1];\n for (const pitch of byVoice[voice] ?? []) {\n if (prev !== undefined) {\n if (pitch < prev) {\n continue; // would cross below the next lower voice\n }\n const spacingLimit = voice === 1 ? maxSpacing + 12 : maxSpacing;\n if (pitch - prev > spacingLimit) {\n continue;\n }\n }\n current[voice] = pitch;\n build(voice + 1);\n }\n };\n build(0);\n return into;\n}\n\n/**\n * The per-chord tables {@link structuralPenalty} reads.\n *\n * Every entry is a function of the chord and key alone, so it is worked out\n * once per chord instead of once per candidate — which is what keeps a\n * four-thousand-candidate search from rebuilding the chord's pitch-class set,\n * and re-deriving every tone's role, four thousand times.\n */\nexport type StructuralTables = {\n /** The chord's pitch classes, ascending. */\n tones: readonly number[];\n /** Penalty for the tone at the same index of `tones` being absent. */\n missing: readonly number[];\n /** Penalty per extra copy of each pitch class 0-11. */\n doubling: Float64Array;\n};\n\n/**\n * Build the structural tables for one chord: heavily penalize missing chord\n * tones (mildly for the fifth, which carries no identity), mildly penalize\n * doubling anything other than the root or fifth, and — when a key is known and\n * the chord is one where the leading tone functions as one — heavily penalize\n * doubling that leading tone.\n *\n * The exempt fifth is the chord's own fifth, whatever its size: a diminished,\n * augmented, or absent fifth would leave the root as the only freely doubled\n * tone, which is what forces a doubled leading tone in a `viio` chord.\n */\nexport function structuralTables(chord: Chord, key?: KeyScale): StructuralTables {\n const tones = chordPitchClasses(chord);\n const missing = tones.map((pc) =>\n chordToneRole(pc, chord) === 'fifth' ? MISSING_FIFTH_PENALTY : MISSING_TONE_PENALTY,\n );\n const rootPc = pitchClass(chord.rootPc);\n const leadingTonePc =\n key !== undefined && isFunctioningLeadingTone(chord, key) ? leadingTonePcOf(key) : -1;\n const doubling = new Float64Array(12);\n for (let pc = 0; pc < 12; pc += 1) {\n if (pc === leadingTonePc) {\n doubling[pc] = LEADING_TONE_DOUBLING_PENALTY;\n } else if (pc !== rootPc && chordToneRole(pc, chord) !== 'fifth') {\n doubling[pc] = POOR_DOUBLING_PENALTY;\n }\n }\n return { tones, missing, doubling };\n}\n\n/**\n * Pitch-class tally shared by every structural evaluation. The scoring loop\n * calls nothing that could re-enter it, so one buffer serves the whole search.\n */\nconst PITCH_CLASS_COUNTS = new Int32Array(12);\n\n/**\n * Structural quality penalty of the candidate at `offset` in `pitches`, read\n * off the chord's {@link StructuralTables}.\n */\nexport function structuralPenalty(\n tables: StructuralTables,\n pitches: ArrayLike<number>,\n offset: number,\n voices: number,\n): number {\n PITCH_CLASS_COUNTS.fill(0);\n let penalty = 0;\n for (let index = 0; index < voices; index += 1) {\n const pitch = pitches[offset + index];\n if (pitch === undefined) {\n continue;\n }\n const pc = pitchClass(pitch);\n PITCH_CLASS_COUNTS[pc] = (PITCH_CLASS_COUNTS[pc] ?? 0) + 1;\n if (index > 0 && pitch === pitches[offset + index - 1]) {\n penalty += UNISON_PENALTY;\n }\n }\n for (let index = 0; index < tables.tones.length; index += 1) {\n const pc = tables.tones[index];\n if (pc !== undefined && PITCH_CLASS_COUNTS[pc] === 0) {\n penalty += tables.missing[index] ?? 0;\n }\n }\n for (let pc = 0; pc < 12; pc += 1) {\n const count = PITCH_CLASS_COUNTS[pc] ?? 0;\n if (count > 1) {\n penalty += (count - 1) * (tables.doubling[pc] ?? 0);\n }\n }\n return penalty;\n}\n\n/**\n * The per-chord-pair tables {@link resolutionViolations} reads, worked out once\n * per chord rather than once per candidate.\n */\nexport type ResolutionTables = {\n /** The leaving chord's seventh, or -1 when it has none. */\n seventhPc: number;\n /**\n * The key's leading tone, or -1 without a key and wherever the leaving chord\n * is not one the leading tone functions in.\n */\n leadingTonePc: number;\n /** The key's tonic, or -1 without a key. */\n tonicPc: number;\n /** Whether the arriving chord contains each pitch class 0-11. */\n nextHas: Uint8Array;\n /** The arriving chord itself, which names the fifth a leading tone may fall to. */\n nextChord: Chord;\n};\n\n/** Build the resolution tables for one chord-to-chord move. */\nexport function resolutionTables(\n prevChord: Chord,\n nextChord: Chord,\n key?: KeyScale,\n): ResolutionTables {\n const nextHas = new Uint8Array(12);\n for (const pc of chordPitchClasses(nextChord)) {\n nextHas[pc] = 1;\n }\n return {\n seventhPc: seventhPcOf(prevChord) ?? -1,\n leadingTonePc:\n key !== undefined && isFunctioningLeadingTone(prevChord, key) ? leadingTonePcOf(key) : -1,\n tonicPc: key === undefined ? -1 : pitchClass(key.rootPc),\n nextHas,\n nextChord,\n };\n}\n\n/**\n * Count the tendency tones that fail to resolve between two consecutive\n * voicings of the same progression.\n *\n * A chordal seventh is a dissonance: unless the next chord holds it as a common\n * tone, the voice carrying it must fall by step. That step is the written one,\n * read through the same spelling {@link checkPartWriting} will mark the result\n * by — a fall of two semitones that writes a diminished third resolves nothing,\n * and a search counting semitones alone would accept what its own checker\n * rejects. Where no key names the letters there is no spelling to read, and the\n * sounding step is all either layer can go by.\n *\n * A leading tone must rise to the tonic whenever the next chord contains one,\n * and only where it is functioning as a leading tone; without a key there is no\n * leading tone to speak of, so that half of the rule is skipped. An inner voice\n * may frustrate its leading tone instead, exactly as {@link checkPartWriting}\n * allows, so the search is not scored against a resolution its own checker\n * accepts.\n */\nexport function resolutionViolations(\n tables: ResolutionTables,\n spelling: MelodicSpelling | undefined,\n prev: ArrayLike<number>,\n prevOffset: number,\n cur: ArrayLike<number>,\n curOffset: number,\n voices: number,\n): number {\n const { seventhPc, leadingTonePc, tonicPc, nextHas, nextChord } = tables;\n let count = 0;\n for (let voice = 0; voice < voices; voice += 1) {\n const from = prev[prevOffset + voice];\n const to = cur[curOffset + voice];\n if (from === undefined || to === undefined) {\n continue;\n }\n const fromPc = pitchClass(from);\n const motion = to - from;\n const resolved =\n spelling === undefined\n ? motion === -1 || motion === -2\n : movesByDescendingStep(spelling.from, from, spelling.to, to);\n if (fromPc === seventhPc && nextHas[fromPc] === 0 && !resolved) {\n count += 1;\n }\n const inner = voice > 0 && voice < voices - 1;\n if (\n fromPc === leadingTonePc &&\n tonicPc >= 0 &&\n nextHas[tonicPc] === 1 &&\n nextHas[fromPc] === 0 &&\n motion !== 1 &&\n !(inner && isFrustratedLeadingTone(from, to, nextChord))\n ) {\n count += 1;\n }\n }\n return count;\n}\n\n/**\n * Total voice-leading cost from the voicing at `prevOffset` to the one at\n * `curOffset`: the summed absolute semitone motion, and nothing else.\n */\nexport function leadingCost(\n prev: ArrayLike<number>,\n prevOffset: number,\n cur: ArrayLike<number>,\n curOffset: number,\n voices: number,\n): number {\n let total = 0;\n for (let voice = 0; voice < voices; voice += 1) {\n const a = prev[prevOffset + voice];\n const b = cur[curOffset + voice];\n if (a === undefined || b === undefined) {\n continue;\n }\n total += Math.abs(b - a);\n }\n return total;\n}\n\n/**\n * Whether the outermost voices reach a hidden/direct perfect fifth or octave by\n * similar motion, the pair {@link checkPartWriting} judges the rule on. Between\n * inner voices the interval is covered by the others, and no rule reports it.\n */\nexport function createsOuterHiddenPerfect(\n prev: ArrayLike<number>,\n prevOffset: number,\n cur: ArrayLike<number>,\n curOffset: number,\n voices: number,\n): boolean {\n if (voices < 2) {\n return false;\n }\n const bassPrev = prev[prevOffset];\n const bassCur = cur[curOffset];\n const sopPrev = prev[prevOffset + voices - 1];\n const sopCur = cur[curOffset + voices - 1];\n if (\n bassPrev === undefined ||\n bassCur === undefined ||\n sopPrev === undefined ||\n sopCur === undefined\n ) {\n return false;\n }\n return createsHiddenParallelPerfect(sopPrev, sopCur, bassPrev, bassCur);\n}\n\n/**\n * The spelling of the chord left and of the chord reached, which is what lets\n * the search read the letters its own output will be checked by.\n */\nexport type MelodicSpelling = {\n /** How the chord being left is written. */\n from: SpellingTable;\n /** How the chord being reached is written. */\n to: SpellingTable;\n};\n\n/**\n * Count the voices that move by an augmented interval between two consecutive\n * voicings, reading both through the spelling\n * {@link spellVoicing} gives them.\n */\nexport function augmentedMelodicCount(\n spelling: MelodicSpelling,\n prev: ArrayLike<number>,\n prevOffset: number,\n cur: ArrayLike<number>,\n curOffset: number,\n voices: number,\n): number {\n let count = 0;\n for (let voice = 0; voice < voices; voice += 1) {\n const from = prev[prevOffset + voice];\n const to = cur[curOffset + voice];\n if (from === undefined || to === undefined) {\n continue;\n }\n if (movesByAugmentedInterval(spelling.from, from, spelling.to, to)) {\n count += 1;\n }\n }\n return count;\n}\n\n/**\n * What the cross-relation rule reads besides the voicings themselves, worked\n * out once per chord-to-chord move.\n */\nexport type CrossRelationTables = {\n /** The chord being left. */\n fromChord: Chord;\n /** The chord being reached. */\n toChord: Chord;\n /**\n * The chord after that one, where the caller knows it. It is what tells an\n * applied dominant — which is written with the contradiction and licenses it —\n * from the diatonic triad that sounds the same; a search that cannot see that\n * far reads the pair as exposed rather than exempt.\n */\n afterChord: Chord | undefined;\n /** The prevailing key. */\n key: KeyScale;\n};\n\n/** Spell one candidate voicing, so the letters can be put to the written rules. */\nfunction spellRow(\n table: SpellingTable,\n pitches: ArrayLike<number>,\n offset: number,\n voices: number,\n): Note[] {\n const notes: Note[] = [];\n for (let voice = 0; voice < voices; voice += 1) {\n const pitch = pitches[offset + voice];\n const note = pitch === undefined ? undefined : spelledAt(table, pitch);\n if (note !== undefined) {\n notes.push(note);\n }\n }\n return notes;\n}\n\n/**\n * Whether any letter is written two ways across the move: the cheap half of the\n * cross-relation rule, which the spelling tables answer without octaves and\n * without allocating a note.\n */\nfunction contradictsALetter(\n spelling: MelodicSpelling,\n prev: ArrayLike<number>,\n prevOffset: number,\n cur: ArrayLike<number>,\n curOffset: number,\n voices: number,\n): boolean {\n for (let voice = 0; voice < voices; voice += 1) {\n const from = prev[prevOffset + voice];\n const earlier = from === undefined ? undefined : spelling.from.notes[pitchClass(from)];\n if (earlier === undefined) {\n continue;\n }\n for (let other = 0; other < voices; other += 1) {\n if (other === voice) {\n continue;\n }\n const to = cur[curOffset + other];\n const later = to === undefined ? undefined : spelling.to.notes[pitchClass(to)];\n if (\n later !== undefined &&\n earlier.alter !== later.alter &&\n diatonicLetterOf(earlier.letter) === diatonicLetterOf(later.letter)\n ) {\n return true;\n }\n }\n }\n return false;\n}\n\n/**\n * Count the cross relations two consecutive voicings write, by the same rule\n * {@link checkPartWriting} reports them under.\n *\n * The letters are scanned first and the full rule — which asks what chord is\n * being written and how each note was reached — is put only to a move that\n * actually contradicts one, since almost none do.\n */\nexport function crossRelationCount(\n spelling: MelodicSpelling,\n tables: CrossRelationTables,\n prev: ArrayLike<number>,\n prevOffset: number,\n cur: ArrayLike<number>,\n curOffset: number,\n voices: number,\n): number {\n if (!contradictsALetter(spelling, prev, prevOffset, cur, curOffset, voices)) {\n return 0;\n }\n return crossRelations(\n { notes: spellRow(spelling.from, prev, prevOffset, voices), chord: tables.fromChord },\n { notes: spellRow(spelling.to, cur, curOffset, voices), chord: tables.toChord },\n tables.key,\n { after: tables.afterChord },\n ).length;\n}\n\n/**\n * Count counterpoint violations between two consecutive voicings of equal\n * length: parallel perfects (fifths and octaves alike, since octaves are the\n * perfect-class-zero case of {@link createsParallelPerfect}) on every voice\n * pair, and voice overlaps on adjacent pairs.\n *\n * Voice crossing and over-wide spacing are not counted: {@link\n * enumerateVoicings} rejects both while building a candidate, so a candidate\n * that reaches here cannot exhibit either. Overlap involves the previous\n * voicing and so is still live.\n */\nexport function violationCount(\n prev: ArrayLike<number>,\n prevOffset: number,\n cur: ArrayLike<number>,\n curOffset: number,\n voices: number,\n): number {\n let count = 0;\n for (let lower = 0; lower < voices; lower += 1) {\n for (let upper = lower + 1; upper < voices; upper += 1) {\n const prevLower = prev[prevOffset + lower];\n const prevUpper = prev[prevOffset + upper];\n const curLower = cur[curOffset + lower];\n const curUpper = cur[curOffset + upper];\n if (\n prevLower === undefined ||\n prevUpper === undefined ||\n curLower === undefined ||\n curUpper === undefined\n ) {\n continue;\n }\n if (createsParallelPerfect(prevUpper, curUpper, prevLower, curLower)) {\n count += 1;\n }\n if (upper === lower + 1) {\n if (createsVoiceOverlap(prevUpper, curUpper, prevLower, curLower)) {\n count += 1;\n }\n }\n }\n }\n return count;\n}\n\n/**\n * Everything a move onto one chord is scored against, worked out once per chord\n * and then read for each of the candidates weighed against it.\n */\nexport type MoveScoring = {\n /** The arriving chord's structural tables. */\n structure: StructuralTables;\n /** The tendency-tone tables, absent when no chord is being left. */\n resolution: ResolutionTables | undefined;\n /** How both chords are written, absent when no key names the letters. */\n spelling: MelodicSpelling | undefined;\n /**\n * What the cross-relation rule reads, absent when no chord is being left or\n * no key names the letters the rule is about.\n */\n crossRelation: CrossRelationTables | undefined;\n};\n\n/**\n * Build the scoring context of a move onto `chord`.\n *\n * The letters are only decidable against a key, so the augmented-interval and\n * cross-relation rules join the search wherever one was given — the same\n * condition the leading-tone rules are under.\n *\n * @param prevChord The chord being left, or undefined when nothing precedes it.\n * @param chord The chord being reached.\n * @param key The prevailing key, or undefined when none was given.\n * @param nextChord The chord after the one being reached, where the caller\n * knows it. It licenses the chromatic tone an applied dominant is written\n * with, and nothing else reads it.\n */\nexport function moveScoring(\n prevChord: Chord | undefined,\n chord: Chord,\n key?: ResolvedKey,\n nextChord?: Chord,\n): MoveScoring {\n // The pitch-class tables want the scale alone and say so here; the spelling\n // table is handed the key whole, since the letters it writes are the key's.\n return {\n structure: structuralTables(chord, key?.scale),\n resolution:\n prevChord === undefined ? undefined : resolutionTables(prevChord, chord, key?.scale),\n spelling:\n key === undefined\n ? undefined\n : { from: spellingTable(key, prevChord), to: spellingTable(key, chord) },\n crossRelation:\n prevChord === undefined || key === undefined\n ? undefined\n : { fromChord: prevChord, toChord: chord, afterChord: nextChord, key: key.scale },\n };\n}\n\n/**\n * The rule-violation weight of one move: the counterpoint violations, the\n * tendency tones left unresolved, the augmented intervals written, the cross\n * relations exposed, and a direct perfect reached by the outer voices.\n *\n * This is the part of a move's score that a shorter line cannot buy back, which\n * is also what makes it the part worth looking a chord ahead for: a candidate\n * that spares its successor a broken rule is worth reaching for, while one that\n * merely spares it a few semitones of motion is not.\n */\nexport function violationWeight(\n scoring: MoveScoring,\n prev: ArrayLike<number>,\n prevOffset: number,\n cur: ArrayLike<number>,\n curOffset: number,\n voices: number,\n): number {\n const { resolution, spelling, crossRelation } = scoring;\n const unresolved =\n resolution === undefined\n ? 0\n : resolutionViolations(resolution, spelling, prev, prevOffset, cur, curOffset, voices);\n const augmented =\n spelling === undefined\n ? 0\n : augmentedMelodicCount(spelling, prev, prevOffset, cur, curOffset, voices);\n const contradictions =\n spelling === undefined || crossRelation === undefined\n ? 0\n : crossRelationCount(spelling, crossRelation, prev, prevOffset, cur, curOffset, voices);\n return (\n VIOLATION_PENALTY * violationCount(prev, prevOffset, cur, curOffset, voices) +\n RESOLUTION_PENALTY * unresolved +\n AUGMENTED_MELODIC_PENALTY * augmented +\n CROSS_RELATION_PENALTY * contradictions +\n (createsOuterHiddenPerfect(prev, prevOffset, cur, curOffset, voices)\n ? HIDDEN_PERFECT_PENALTY\n : 0)\n );\n}\n\n/**\n * The whole score of one move: its rule-violation weight, the structural\n * quality of the chord arrived on, and the distance the voices travelled.\n */\nexport function moveScore(\n scoring: MoveScoring,\n prev: ArrayLike<number>,\n prevOffset: number,\n cur: ArrayLike<number>,\n curOffset: number,\n voices: number,\n): number {\n return (\n structuralPenalty(scoring.structure, cur, curOffset, voices) +\n leadingCost(prev, prevOffset, cur, curOffset, voices) +\n violationWeight(scoring, prev, prevOffset, cur, curOffset, voices)\n );\n}\n","import { InvalidInputError, NoSolutionError } from '../../core/errors/index.js';\nimport {\n assertArray,\n assertFiniteNumber,\n assertGenerationBudget,\n assertOptions,\n assertPositiveInt,\n} from '../../core/validation/index.js';\nimport type { Chord } from '../chord/index.js';\nimport { type KeyLike, type ResolvedKey, resolveKey } from '../scale/index.js';\nimport { type ChordLike, formatChordSymbol, toChordData } from '../symbol/index.js';\nimport {\n createCandidateBuffer,\n DEFAULT_MAX_SPACING,\n enumerateVoicings,\n leadingCost,\n MAX_LOOKAHEAD_PAIRS,\n type MoveScoring,\n moveScore,\n moveScoring,\n structuralPenalty,\n type VoicingCandidates,\n violationWeight,\n} from './internal.js';\n\n/**\n * An inclusive MIDI pitch range for a single voice.\n *\n * @category Voicing & Counterpoint\n */\nexport type VoiceRange = {\n readonly min: number;\n readonly max: number;\n};\n\n/**\n * Default four-voice SATB ranges, ascending (index 0 = lowest):\n * bass E2–C4 (40–60), tenor C3–G4 (48–67), alto G3–D5 (55–74),\n * soprano C4–G5 (60–79).\n *\n * @category Voicing & Counterpoint\n */\nexport const SATB_RANGES: readonly Readonly<VoiceRange>[] = Object.freeze([\n Object.freeze({ min: 40, max: 60 }),\n Object.freeze({ min: 48, max: 67 }),\n Object.freeze({ min: 55, max: 74 }),\n Object.freeze({ min: 60, max: 79 }),\n]);\n\n/**\n * Options controlling {@link voiceChord} and {@link voiceProgression}.\n *\n * @category Voicing & Counterpoint\n */\nexport type VoicingOptions = {\n /**\n * Number of voices to realize. Ignored when `ranges` is given.\n *\n * @defaultValue 4\n */\n voices?: number;\n /** Explicit per-voice ranges, ascending (index 0 = lowest). Takes precedence over `voices`. */\n ranges?: VoiceRange[];\n /**\n * Maximum spacing in semitones between adjacent upper voices.\n *\n * @defaultValue 12\n */\n maxSpacing?: number;\n /**\n * Maximum candidate voicings evaluated for one chord.\n *\n * Raise this for large voicings when exact optimum matters more than bounded\n * search time.\n *\n * @defaultValue 4000\n */\n maxCandidates?: number;\n /**\n * Maximum number of chords {@link voiceProgression} will voice. The search\n * per chord is bounded internally, so the cost of a progression is linear in\n * its length; this is the guard against an unbounded caller, not a limit on\n * the search.\n *\n * @defaultValue 1000000\n */\n budget?: number;\n /**\n * The prevailing key, as a key name, a key/scale, or a `Key`. Supplying it\n * enables the rules that only make sense relative to a tonic: the leading\n * tone is not doubled, and — where the chord being left is known as well —\n * it is not left unresolved. Without it, voicings are chosen from chord\n * structure and voice-leading distance alone.\n */\n key?: KeyLike;\n /**\n * The chord the current voicing was written on, as a chord symbol, chord\n * data, or a `Chord`.\n *\n * It enables every rule that is read from the chord being left: the\n * resolution of a chordal seventh, the resolution of a leading tone, and the\n * cross relation between the two chords. All three are scored exactly as\n * {@link voiceProgression} scores them, and none of them is scored without\n * it — a leading tone cannot be told from the third of a chord that merely\n * contains it until the chord it belongs to is known. Supply it whenever the\n * caller has the chord, not only where that chord carried a seventh.\n */\n previousChord?: ChordLike;\n};\n\n/** Overall pitch floor/ceiling used when deriving ranges for arbitrary voice counts. */\nconst DERIVED_LOW = 40;\nconst DERIVED_HIGH = 79;\n/** Span of each derived per-voice range in semitones. */\nconst DERIVED_SPAN = 19;\n\n/**\n * Resolve the per-voice ranges implied by the options: explicit `ranges` win,\n * four voices use {@link SATB_RANGES}, and other counts get evenly spaced\n * ranges spanning roughly the bass-to-soprano compass.\n */\nexport function resolveRanges(opts?: VoicingOptions): VoiceRange[] {\n // Every voicing entrance resolves its ranges, so the options bag they share is\n // read as one here rather than at four entrances separately.\n assertOptions(opts, 'opts');\n if (opts?.ranges !== undefined) {\n if (opts.ranges.length === 0) {\n throw new InvalidInputError('ranges must contain at least one voice range');\n }\n assertGenerationBudget(opts.ranges.length, 'voice ranges', 128);\n return opts.ranges.map((range, index) => {\n assertFiniteNumber(range.min, `ranges[${index}].min`);\n assertFiniteNumber(range.max, `ranges[${index}].max`);\n if (range.min > range.max) {\n throw new InvalidInputError(`ranges[${index}].min must not exceed max`);\n }\n assertGenerationBudget(\n Math.floor(range.max) - Math.ceil(range.min) + 1,\n `ranges[${index}] span`,\n 4096,\n );\n return { ...range };\n });\n }\n const voices = opts?.voices ?? 4;\n assertPositiveInt(voices, 'voices', 128);\n if (voices === 4) {\n return SATB_RANGES.map((range) => ({ ...range }));\n }\n if (voices === 1) {\n return [{ min: DERIVED_LOW, max: DERIVED_HIGH }];\n }\n const ranges: VoiceRange[] = [];\n for (let i = 0; i < voices; i += 1) {\n const min = Math.round(\n DERIVED_LOW + (i * (DERIVED_HIGH - DERIVED_SPAN - DERIVED_LOW)) / (voices - 1),\n );\n ranges.push({ min, max: min + DERIVED_SPAN });\n }\n return ranges;\n}\n\n/**\n * Resolve and validate the adjacent-voice spacing limit. Shared by every entry\n * point so `nextVoicing` cannot accept a NaN that silently disables the spacing\n * constraint, nor a negative value that reports itself as an impossible range.\n */\nexport function resolveMaxSpacing(opts?: VoicingOptions): number {\n const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;\n assertFiniteNumber(maxSpacing, 'maxSpacing');\n if (maxSpacing < 0) {\n throw new InvalidInputError('maxSpacing must be non-negative');\n }\n return maxSpacing;\n}\n\n/**\n * Resolve the prevailing key once per entry point, so the scoring tables below\n * are handed one key rather than re-reading whatever form the caller wrote.\n *\n * The whole key, spelling and all: the search grades the letters it writes, and\n * a key handed in as an Ab minor would otherwise be scored on the sharps a G#\n * minor spells.\n */\nexport function resolvedKeyOf(opts?: VoicingOptions): ResolvedKey | undefined {\n return opts?.key === undefined ? undefined : resolveKey(opts.key);\n}\n\n/** Resolve the chord a `nextVoicing` caller says the current voicing came from. */\nexport function resolvePreviousChord(opts?: VoicingOptions): Chord | undefined {\n return opts?.previousChord === undefined ? undefined : toChordData(opts.previousChord);\n}\n\n/** Resolve and validate the per-chord candidate-search cap. */\nexport function resolveMaxCandidates(opts?: VoicingOptions): number | undefined {\n if (opts?.maxCandidates === undefined) return undefined;\n assertPositiveInt(opts.maxCandidates, 'maxCandidates', 1_000_000);\n return opts.maxCandidates;\n}\n\n/**\n * How far a candidate sits from the middle of each voice's range, summed.\n *\n * A chord with nothing before it is placed by this alone once its structure is\n * settled: preferring pitches near the middle of every range is what makes the\n * default voicing centered and compact rather than pushed to one end of the\n * compass.\n */\nfunction centeringPenalty(\n ranges: readonly VoiceRange[],\n pitches: ArrayLike<number>,\n offset: number,\n voices: number,\n): number {\n let penalty = 0;\n for (let voice = 0; voice < voices; voice += 1) {\n const pitch = pitches[offset + voice];\n const range = ranges[voice];\n if (pitch === undefined || range === undefined) {\n continue;\n }\n penalty += Math.abs(pitch - (range.min + range.max) / 2);\n }\n return penalty;\n}\n\n/**\n * Realize a single chord as one MIDI pitch per voice, ascending (index 0 =\n * lowest). The bass voice takes the chord's `bassPc` when set, otherwise the\n * root; upper voices take chord pitch classes, doubling the root or fifth as\n * needed to fill all voices. The result stays inside each voice's range, keeps\n * adjacent upper voices within `maxSpacing`, avoids voice crossing, and is\n * deterministic: a compact close-position voicing centered in the ranges.\n *\n * @param chord The chord to voice.\n * @param opts Voicing options; defaults to four voices in {@link SATB_RANGES}.\n * @returns MIDI pitches, ascending, one per voice.\n * @throws If no voicing fits the given ranges.\n * @example\n * ```ts\n * import { parseChordSymbol, voiceChord } from '@libraz/libcantus';\n * const chord = parseChordSymbol('Cmaj7');\n * voiceChord(chord); // four ascending MIDI pitches within the SATB ranges\n * ```\n * @category Voicing & Counterpoint\n */\nexport function voiceChord(chord: ChordLike, opts?: VoicingOptions): number[] {\n const data = toChordData(chord);\n const ranges = resolveRanges(opts);\n const maxSpacing = resolveMaxSpacing(opts);\n const candidates = enumerateVoicings(data, ranges, maxSpacing, resolveMaxCandidates(opts));\n const { structure } = moveScoring(undefined, data, resolvedKeyOf(opts));\n const { pitches, voices } = candidates;\n let bestOffset = -1;\n let bestScore = Number.POSITIVE_INFINITY;\n for (let index = 0; index < candidates.count; index += 1) {\n const offset = index * voices;\n const score =\n structuralPenalty(structure, pitches, offset, voices) +\n centeringPenalty(ranges, pitches, offset, voices);\n if (score < bestScore) {\n bestScore = score;\n bestOffset = offset;\n }\n }\n if (bestOffset < 0) {\n throw new NoSolutionError(\n `no voicing satisfies the given ranges for ${formatChordSymbol(chord)}`,\n );\n }\n return [...pitches.subarray(bestOffset, bestOffset + voices)];\n}\n\n/**\n * Voice a chord progression with smooth voice leading. Each chord picks, from a\n * bounded deterministic candidate set, the voicing minimizing the voice-leading\n * cost from the previous voicing plus a large penalty per counterpoint\n * violation (parallel perfects/octaves, voice crossing, voice overlap, and\n * over-wide upper-voice spacing).\n *\n * The choice also weighs the chord that follows. A voicing costs whatever the\n * move onto the next chord will cost in broken rules, so a placement that is\n * marginally smoother now but strands a voice on a leap it cannot write cleanly\n * loses to the one that leaves the line somewhere the next chord can be reached\n * from. The lookahead is one chord deep and reads only the violation weight of\n * the connection, so a chord is otherwise voiced exactly as it would be on its\n * own merits.\n *\n * Tendency tones are resolved rather than merely moved economically: the voice\n * holding a chordal seventh falls by step unless the next chord keeps that tone,\n * and — when `opts.key` is given — the leading tone rises to the tonic and is\n * never doubled, wherever it is functioning as a leading tone rather than\n * sounding as an ordinary tone of some other chord. A key also names the letters\n * each voice is written with, which is what lets the search refuse the augmented\n * second between the lowered sixth and the raised seventh that a minor-key line\n * otherwise falls into. Without a key neither rule can apply, since nothing\n * identifies which pitch class is the leading tone or how a tone is spelled.\n *\n * @param chords The chords to voice in order.\n * @param opts Voicing options; defaults to four voices in {@link SATB_RANGES}.\n * @returns One voicing per chord, each ascending with one MIDI pitch per voice.\n * @throws If any chord admits no voicing within the given ranges, or if the\n * progression is longer than `opts.budget` chords.\n * @example\n * ```ts\n * import { parseChordSymbol, voiceProgression } from '@libraz/libcantus';\n * const chords = ['C', 'Am', 'F', 'G'].map((s) => parseChordSymbol(s));\n * voiceProgression(chords); // one four-voice voicing per chord, smoothly led\n * ```\n * @category Voicing & Counterpoint\n */\n/** The unsatisfiable-constraints error for one chord of a progression. */\nfunction noSolutionAt(index: number, chord: Chord): NoSolutionError {\n return new NoSolutionError(\n `no voicing satisfies the given ranges for ${formatChordSymbol(chord)} at index ${index}`,\n { at: index },\n );\n}\n\n/**\n * Run one chord's work, re-raising an unsatisfiable-constraints failure with\n * the chord it happened on. Voicing a whole lead sheet is otherwise told only\n * that some chord did not fit.\n */\nfunction locate<T>(index: number, chord: Chord, work: () => T): T {\n try {\n return work();\n } catch (error) {\n if (error instanceof NoSolutionError) {\n throw noSolutionAt(index, chord);\n }\n throw error;\n }\n}\n\n/** The chord after the one being voiced: its candidates, and how a move onto it scores. */\ntype Lookahead = {\n /** The next chord's candidate voicings. */\n candidates: VoicingCandidates;\n /** What a move from the chord being voiced onto that chord is scored against. */\n scoring: MoveScoring;\n};\n\n/**\n * The violation weight a candidate commits the next chord to: the weight of the\n * connection the following step would write, arriving from this candidate.\n *\n * The following step chooses by the whole score, so the connection weighed here\n * is the one it will actually pick rather than the cleanest one it could reach —\n * a lookahead that reported an unreachable best would recommend placements the\n * search then declines to use.\n */\nfunction successorWeight(\n next: Lookahead,\n cur: ArrayLike<number>,\n curOffset: number,\n voices: number,\n): number {\n const { candidates, scoring } = next;\n const { pitches } = candidates;\n let bestScore = Number.POSITIVE_INFINITY;\n let bestWeight = 0;\n for (let candidate = 0; candidate < candidates.count; candidate += 1) {\n const offset = candidate * voices;\n const weight = violationWeight(scoring, cur, curOffset, pitches, offset, voices);\n const score =\n weight +\n structuralPenalty(scoring.structure, pitches, offset, voices) +\n leadingCost(cur, curOffset, pitches, offset, voices);\n if (score < bestScore) {\n bestScore = score;\n bestWeight = weight;\n }\n }\n return bestWeight;\n}\n\n/**\n * Voice a chord progression with smooth, bounded SATB-style leading.\n *\n * Each chord is chosen from a deterministic candidate set, minimizing motion\n * while penalizing counterpoint violations, and weighing the violations the\n * move onto the following chord would then be forced into. A supplied key\n * additionally resolves leading tones and chordal sevenths.\n *\n * @param chords The chords to voice in order.\n * @param opts Voicing options; defaults to four voices in {@link SATB_RANGES}.\n * @returns One ascending MIDI voicing per chord.\n * @throws {@link NoSolutionError} with the failing chord index when a chord\n * cannot fit the requested ranges.\n * @category Voicing & Counterpoint\n */\nexport function voiceProgression(chords: readonly ChordLike[], opts?: VoicingOptions): number[][] {\n const progression = assertArray<ChordLike>(chords, 'chords');\n assertGenerationBudget(progression.length, 'voiced progression chords', opts?.budget);\n const data = progression.map((chord) => toChordData(chord));\n const key = resolvedKeyOf(opts);\n const ranges = resolveRanges(opts);\n const maxSpacing = resolveMaxSpacing(opts);\n const maxCandidates = resolveMaxCandidates(opts);\n const result: number[][] = [];\n const first = data[0];\n if (first === undefined) {\n return result;\n }\n // Two candidate buffers serve the whole progression: the chord being voiced,\n // and the one looked ahead to. Each step hands its lookahead buffer on as the\n // next step's own, so every chord is enumerated once and a lead sheet\n // allocates two buffers rather than two per chord.\n let current = locate(0, first, () =>\n enumerateVoicings(first, ranges, maxSpacing, maxCandidates, createCandidateBuffer()),\n );\n let spare = createCandidateBuffer();\n // The scoring of a move onto the chord being voiced is the very context its\n // predecessor built to look ahead with, so it is carried forward rather than\n // rebuilt.\n let scoring = moveScoring(undefined, first, key);\n let prev: number[] | undefined;\n for (const [index, chord] of data.entries()) {\n const nextChord = data[index + 1];\n const lookahead: Lookahead | undefined =\n nextChord === undefined\n ? undefined\n : {\n candidates: locate(index + 1, nextChord, () =>\n enumerateVoicings(nextChord, ranges, maxSpacing, maxCandidates, spare),\n ),\n // The chord after the one being reached goes in too: it is what\n // says whether the chromatic tone the next chord introduces is the\n // one an applied dominant is written with.\n scoring: moveScoring(chord, nextChord, key, data[index + 2]),\n };\n const { pitches, voices, count } = current;\n // A lookahead weighs every pair of candidates, so it is dropped wherever the\n // pairs would outgrow the bounded per-chord search — for the whole chord, so\n // no candidate is judged on terms the others were not.\n const weighed =\n lookahead !== undefined && count * lookahead.candidates.count <= MAX_LOOKAHEAD_PAIRS\n ? lookahead\n : undefined;\n let bestOffset = -1;\n let bestScore = Number.POSITIVE_INFINITY;\n for (let candidate = 0; candidate < count; candidate += 1) {\n const offset = candidate * voices;\n let score =\n prev === undefined\n ? structuralPenalty(scoring.structure, pitches, offset, voices) +\n centeringPenalty(ranges, pitches, offset, voices)\n : moveScore(scoring, prev, 0, pitches, offset, voices);\n if (weighed !== undefined) {\n score += successorWeight(weighed, pitches, offset, voices);\n }\n if (score < bestScore) {\n bestScore = score;\n bestOffset = offset;\n }\n }\n if (bestOffset < 0) {\n throw noSolutionAt(index, chord);\n }\n const best = [...pitches.subarray(bestOffset, bestOffset + voices)];\n result.push(best);\n prev = best;\n if (lookahead !== undefined) {\n spare = current;\n current = lookahead.candidates;\n scoring = lookahead.scoring;\n }\n }\n return result;\n}\n","import { InvalidInputError, NoSolutionError } from '../../core/errors/index.js';\nimport { assertArray, assertFiniteNumber, assertOptions } from '../../core/validation/index.js';\nimport { type ChordLike, toChordData } from '../symbol/index.js';\nimport { enumerateVoicings, leadingCost, moveScore, moveScoring } from './internal.js';\nimport type { VoicingOptions } from './satb.js';\nimport {\n resolvedKeyOf,\n resolveMaxCandidates,\n resolveMaxSpacing,\n resolvePreviousChord,\n resolveRanges,\n} from './satb.js';\n\n/**\n * Read a voicing as one: an array, holding a pitch in every voice.\n *\n * Both entrances here walk a voicing pitch by pitch and do arithmetic on what\n * they find, and arithmetic coerces — a voice holding a `null` measures as\n * having moved nowhere, and one holding a string as `NaN` that then loses every\n * comparison it takes part in.\n */\nfunction assertVoicing(pitches: readonly number[], name: string): readonly number[] {\n const voices = assertArray<number>(pitches, name);\n for (let index = 0; index < voices.length; index += 1) {\n assertFiniteNumber(voices[index] ?? Number.NaN, `${name}[${index}]`);\n }\n return voices;\n}\n\n/**\n * Total voice-leading cost between two voicings: the sum of absolute semitone\n * motion across voices, and nothing besides. The rules the search also weighs —\n * parallels, hidden perfects, unresolved tendency tones — are judged and\n * reported by {@link checkPartWriting}, so measuring distance stays a\n * measurement. The arrays must be the same length; when they differ the\n * voicings are not comparable and the cost is `Infinity`.\n *\n * @param from The previous voicing, one MIDI pitch per voice.\n * @param to The next voicing, one MIDI pitch per voice.\n * @returns The summed absolute motion, or `Infinity` when lengths differ.\n * @category Voicing & Counterpoint\n */\nexport function voiceLeadingCost(from: number[], to: number[]): number {\n // The voices are subtracted from one another, and subtraction coerces: a\n // voicing holding a null would be measured as having moved nowhere.\n const previous = assertVoicing(from, 'from');\n const next = assertVoicing(to, 'to');\n if (previous.length !== next.length) {\n return Number.POSITIVE_INFINITY;\n }\n return leadingCost(previous, 0, next, 0, previous.length);\n}\n\n/**\n * Voice a single chord to follow smoothly from an arbitrary current voicing.\n *\n * Candidate voicings of `chord` are enumerated within ranges taken from `opts`,\n * or — when neither `voices` nor `ranges` is given — from a one-octave window\n * around each pitch of `current` (so the result matches `current`'s voice\n * count). Each candidate is scored by structural quality, voice-leading motion\n * from `current`, and a large penalty per counterpoint violation; the lowest\n * scoring candidate is returned, ascending.\n *\n * The rules read from the chord being left — the resolution of a chordal\n * seventh, the resolution of a leading tone, and the cross relation between the\n * two chords — are scored only when `opts.previousChord` names it: what a\n * voicing is written on cannot be read back from the pitches it holds. An\n * editor moving from one chord to the next has that chord, and passing it is\n * what keeps this generator and {@link checkPartWriting} agreeing about the\n * pair.\n *\n * @param current The current voicing to lead from, ascending (index 0 = lowest).\n * @param chord The next chord to voice, as a chord symbol, chord data, or a\n * `Chord`.\n * @param opts Voicing options; when omitted, ranges follow `current`'s span.\n * @returns The chosen voicing, ascending with one MIDI pitch per voice.\n * @throws If `current` is empty or holds a non-finite pitch while the ranges\n * are derived from it, if it asks for more voices than the search will take,\n * or if no voicing fits the ranges.\n * @category Voicing & Counterpoint\n */\nexport function nextVoicing(current: number[], chord: ChordLike, opts?: VoicingOptions): number[] {\n assertOptions(opts, 'opts');\n assertVoicing(current, 'current');\n const data = toChordData(chord);\n const derived = opts?.ranges === undefined && opts?.voices === undefined;\n if (derived) {\n // The ranges are read off `current`, so an empty voicing would describe a\n // zero-voice chord and return an empty result — the same input that throws\n // when written as an explicit `{ ranges: [] }`.\n if (current.length === 0) {\n throw new InvalidInputError('current must contain at least one pitch');\n }\n assertVoicing(current, 'current');\n }\n // Both branches end in the same resolver, so the voice-count budget and the\n // range checks apply wherever the ranges came from: a texture read off\n // `current` is no smaller a search than the same one written out as `ranges`.\n const ranges = derived\n ? resolveRanges({\n ranges: current.map((pitch) => {\n // Window one octave around each current pitch, clamped so extreme-low\n // or extreme-high input can never yield MIDI outside [0, 127].\n const centre = Math.min(127, Math.max(0, pitch));\n return { min: Math.max(0, centre - 12), max: Math.min(127, centre + 12) };\n }),\n })\n : resolveRanges(opts);\n assertVoicing(current, 'current');\n if (current.length < ranges.length) {\n throw new InvalidInputError(\n `current has ${current.length} voices but the requested ranges require ${ranges.length}`,\n );\n }\n // Thinning an existing texture is a normal use case (SATB to three voices,\n // for example). Keep the outer voices and select evenly spaced inner voices\n // so motion is still measured between like voice roles.\n const source =\n current.length === ranges.length\n ? current\n : ranges.length === 1\n ? [current[0] ?? Number.NaN]\n : ranges.map((_, index) => {\n const sourceIndex = Math.round((index * (current.length - 1)) / (ranges.length - 1));\n return current[sourceIndex] ?? Number.NaN;\n });\n const maxSpacing = resolveMaxSpacing(opts);\n const candidates = enumerateVoicings(data, ranges, maxSpacing, resolveMaxCandidates(opts));\n const key = resolvedKeyOf(opts);\n // The chord the current voicing came from is optional, so the line it is\n // leaving is spelled against that chord when the caller named one and by the\n // key alone when it did not. Only one chord is being voiced, so there is\n // nothing after it to look ahead to; the move is otherwise scored exactly as\n // one step of a progression is.\n const scoring = moveScoring(resolvePreviousChord(opts), data, key);\n const { pitches, voices } = candidates;\n let bestOffset = -1;\n let bestScore = Number.POSITIVE_INFINITY;\n for (let candidate = 0; candidate < candidates.count; candidate += 1) {\n const offset = candidate * voices;\n const score = moveScore(scoring, source, 0, pitches, offset, voices);\n if (score < bestScore) {\n bestScore = score;\n bestOffset = offset;\n }\n }\n if (bestOffset < 0) {\n throw new NoSolutionError('no voicing satisfies the given ranges');\n }\n return [...pitches.subarray(bestOffset, bestOffset + voices)];\n}\n","import { InvalidInputError } from '../../core/errors/index.js';\nimport { assertFiniteNumber, assertInteger, assertOptions } from '../../core/validation/index.js';\nimport { chordToneRole } from '../chord/index.js';\nimport { type ChordLike, toChordData } from '../symbol/index.js';\nimport { pitchClass } from './internal.js';\n\n/**\n * A tertian voicing style for {@link voiceChordStyled}.\n *\n * - `close`: the plain close-position tertian stack.\n * - `drop2`: the second voice from the top dropped an octave (drop-2 voicing).\n * It takes at least three voices, since the voice it names has to have\n * another below it for the drop to open the stack at all.\n * - `drop3`: the third voice from the top dropped an octave (drop-3 voicing).\n * It takes at least four voices, for the same reason.\n * - `shell`: root plus guide tones (third and seventh) for seventh chords, or\n * root/third/fifth for triads; the fifth and tensions are omitted.\n * - `rootless`: the root omitted, keeping third/fifth/seventh and tensions\n * (a typical left-hand jazz voicing).\n *\n * A chord with too few voices for the drop it asks for is voiced in close\n * position instead: a three-note `drop3` is the close stack, not a stack with\n * its bottom voice pushed an octave down. A slash bass the chord does not\n * contain sounds under the stack rather than in it, and is not one of the\n * voices counted here.\n *\n * @category Voicing & Counterpoint\n */\nexport type VoicingStyle = 'close' | 'drop2' | 'drop3' | 'shell' | 'rootless';\n\n/**\n * Options controlling {@link voiceChordStyled}.\n *\n * @category Voicing & Counterpoint\n */\nexport type StyledVoicingOptions = {\n /**\n * Voicing style to build.\n *\n * @defaultValue 'close'\n */\n style?: VoicingStyle;\n /** Constrain the highest voice to this pitch class (0..11) when given. */\n topNote?: number;\n /**\n * Base octave in scientific pitch notation: octave 4 starts the stack near\n * middle C (MIDI 60), i.e. near `12 * (octave + 1)`.\n *\n * @defaultValue 4\n */\n octave?: number;\n /**\n * Omit the root regardless of style (relevant for jazz voicings).\n *\n * @defaultValue false\n */\n rootless?: boolean;\n};\n\n/** Default base octave for a styled close-position stack. */\nconst DEFAULT_STYLE_OCTAVE = 4;\n\n/**\n * Which voice of the close stack, counted from the top, a style lowers an\n * octave; 0 for a style that lowers none.\n */\nfunction dropPositionFromTop(style: VoicingStyle): number {\n if (style === 'drop2') {\n return 2;\n }\n return style === 'drop3' ? 3 : 0;\n}\n\n/** Return the pitch classes of a list in order, without duplicates. */\nfunction dedupePcs(pcs: number[]): number[] {\n const seen = new Set<number>();\n const result: number[] = [];\n for (const pc of pcs) {\n if (!seen.has(pc)) {\n seen.add(pc);\n result.push(pc);\n }\n }\n return result;\n}\n\n/** The lowest MIDI pitch of a pitch class at or above a floor. */\nfunction lowestPitchAtOrAbove(pc: number, floor: number): number {\n return floor + ((((pc - pitchClass(floor)) % 12) + 12) % 12);\n}\n\n/** The pitch class in `pcs` whose circular distance to `target` is smallest. */\nfunction nearestPc(target: number, pcs: number[]): number {\n let best = pcs[0] ?? target;\n let bestDist = 12;\n for (const pc of pcs) {\n const dist = Math.min((((pc - target) % 12) + 12) % 12, (((target - pc) % 12) + 12) % 12);\n if (dist < bestDist) {\n bestDist = dist;\n best = pc;\n }\n }\n return best;\n}\n\n/**\n * Realize a single chord as an explicit tertian voicing in a chosen style,\n * independent of the SATB range search used by {@link voiceChord}.\n *\n * The chord tones are stacked in close position from the bass (`bassPc`, else\n * `rootPc`) in the given scientific-pitch `octave` upward (octave 4 starts the\n * bass near C4 = MIDI 60, matching {@link midiToNote} and the bass module).\n * When `topNote` is given the stack is rotated so the highest voice's pitch\n * class equals that note (or the nearest chord tone). The chosen\n * {@link VoicingStyle} then transforms the stack, and the result is returned as\n * ascending MIDI pitches (index 0 = lowest).\n *\n * A drop voicing lowers one voice below the rest, so that voice is its bass —\n * which is what tells the drop-2 voicings of one chord apart. When the chord\n * names a bass tone of its own, the close stack is therefore rotated so the\n * style drops that tone, and the result is the drop voicing whose bass is the\n * tone asked for; the named bass is not lowered under an already dropped voice\n * afterwards, which would leave a hole wider than an octave in the middle. A\n * bass the chord does not contain is no voice of the stack and cannot be the\n * dropped one, so it keeps the bottom and the drop happens above it.\n *\n * Every returned pitch is a valid MIDI number: an octave whose stack would run\n * off either end of the 0..127 range is rejected rather than voiced out of\n * range, matching {@link nextVoicing}, which clamps its derived ranges.\n *\n * @param given The chord to voice, in whatever form it is held: a chord symbol,\n * plain chord data, or a `Chord`.\n * @param opts Styled voicing options; defaults to a close voicing at octave 4.\n * @returns MIDI pitches, ascending, one per retained voice.\n * @throws If the voicing would not fit inside MIDI 0..127 at the given octave.\n * @example\n * ```ts\n * import { voiceChordStyled } from '@libraz/libcantus';\n * voiceChordStyled('Dm7', { style: 'drop2' }); // ascending MIDI pitches, drop-2 voicing\n * ```\n * @category Voicing & Counterpoint\n */\nexport function voiceChordStyled(given: ChordLike, opts?: StyledVoicingOptions): number[] {\n assertOptions(opts, 'opts');\n const chord = toChordData(given);\n const style = opts?.style ?? 'close';\n const octave = opts?.octave ?? DEFAULT_STYLE_OCTAVE;\n assertInteger(octave, 'styled voicing octave', -10, 10);\n if (opts?.topNote !== undefined) {\n assertFiniteNumber(opts.topNote, 'styled voicing topNote');\n }\n const base = 12 * (octave + 1);\n const rootPc = pitchClass(chord.rootPc);\n const bassPc = pitchClass(chord.bassPc ?? chord.rootPc);\n const omitRoot = style === 'rootless' || opts?.rootless === true;\n\n // Tertian chord tones in order, with the bass tone rotated to the bottom.\n let order = dedupePcs(chord.intervals.map((interval) => pitchClass(chord.rootPc + interval)));\n const bassIndex = order.indexOf(bassPc);\n const bassIsChordTone = bassIndex >= 0;\n if (bassIndex > 0) {\n order = [...order.slice(bassIndex), ...order.slice(0, bassIndex)];\n } else if (bassIndex < 0) {\n order = [bassPc, ...order];\n }\n\n if (style === 'shell') {\n const roles = order.map((pc) => chordToneRole(pc, chord));\n // The guide tone is the seventh when present; on a sixth chord the sixth\n // stands in for it, and otherwise the voicing falls back to the fifth.\n const guide = roles.includes('seventh')\n ? 'seventh'\n : roles.includes('sixth')\n ? 'sixth'\n : 'fifth';\n order = order.filter((_pc, i) => {\n const role = roles[i];\n return _pc === bassPc || role === 'root' || role === 'third' || role === guide;\n });\n }\n\n if (omitRoot) {\n order = order.filter((pc) => pc !== rootPc || (chord.bassPc !== undefined && pc === bassPc));\n }\n\n if (order.length === 0) {\n return [];\n }\n\n if (opts?.topNote !== undefined) {\n const target = order.includes(pitchClass(opts.topNote))\n ? pitchClass(opts.topNote)\n : nearestPc(pitchClass(opts.topNote), order);\n const targetIndex = order.indexOf(target);\n order = [...order.slice(targetIndex + 1), ...order.slice(0, targetIndex + 1)];\n }\n\n // A drop puts the voice it lowers under the whole stack, so on a drop voicing\n // the dropped voice is the bass. When the chord names a bass tone of its own,\n // rotate the close stack so the named tone is the one the style drops: the\n // requested bass then reaches the bottom through the drop itself, which is\n // also what tells the four drop-2 voicings of a seventh chord apart. Lowering\n // it afterwards instead, under a voice already dropped below it, would open a\n // hole wider than an octave inside a voicing whose point is compactness.\n // A rotation is a single choice, so this overrides any `topNote` rotation.\n const dropFromTop = dropPositionFromTop(style);\n const dropsToNamedBass =\n dropFromTop > 0 && bassIsChordTone && chord.bassPc !== undefined && order.length > dropFromTop;\n if (dropsToNamedBass) {\n const bassAt = order.indexOf(bassPc);\n if (bassAt >= 0) {\n const shift =\n (((bassAt - (order.length - dropFromTop)) % order.length) + order.length) % order.length;\n order = [...order.slice(shift), ...order.slice(0, shift)];\n }\n }\n\n // Stack the ordered pitch classes upward in close position.\n const stack: number[] = [];\n let prev: number | undefined;\n for (const pc of order) {\n if (prev === undefined) {\n prev = lowestPitchAtOrAbove(pc, base);\n } else {\n let delta = (((pc - pitchClass(prev)) % 12) + 12) % 12;\n if (delta === 0) {\n delta = 12;\n }\n prev += delta;\n }\n stack.push(prev);\n }\n\n // The dropped voice needs another voice below it, or the drop only moves the\n // bottom of the stack down an octave and leaves the rest where it was, which\n // opens the stack nowhere. A chord that short is voiced in close position.\n // A bass the chord does not contain is no part of the stacked chord tones —\n // it sounds under them — so it is neither dropped nor counted as that voice.\n const stackedFloor = bassIsChordTone ? 0 : 1;\n if (dropFromTop > 0 && stack.length - dropFromTop > stackedFloor) {\n const dropIndex = stack.length - dropFromTop;\n stack[dropIndex] = (stack[dropIndex] ?? 0) - 12;\n }\n\n // An explicit slash bass is a structural requirement, including when it is\n // not a chord member — and a bass that is no chord tone cannot be the voice a\n // drop lowers, so it stays where the stack put it. Top-note rotation, or a\n // stack wide enough that the dropped voice lands above the bass, may leave a\n // different tone below it, so lower the retained bass by octaves before the\n // final sort. This gives the bass priority over a conflicting top-note hint.\n if (chord.bassPc !== undefined) {\n const bassIndexInStack = stack.findIndex((pitch) => pitchClass(pitch) === bassPc);\n if (bassIndexInStack >= 0) {\n const otherLowest = Math.min(\n ...stack.filter((_pitch, index) => index !== bassIndexInStack),\n Number.POSITIVE_INFINITY,\n );\n while ((stack[bassIndexInStack] ?? 0) >= otherLowest) {\n stack[bassIndexInStack] = (stack[bassIndexInStack] ?? 0) - 12;\n }\n }\n }\n\n const voicing = stack.sort((a, b) => a - b);\n const lowest = voicing[0] ?? 0;\n const highest = voicing[voicing.length - 1] ?? 0;\n if (lowest < 0 || highest > 127) {\n throw new InvalidInputError(\n `styled voicing at octave ${octave} spans MIDI ${lowest}..${highest}, outside 0..127`,\n );\n }\n return voicing;\n}\n","/**\n * Shared construction of {@link PartWritingViolation} records.\n *\n * The four-part checker and the species checker report the same violation type,\n * so they build it and name intervals the same way.\n */\n\nimport type { PartWritingViolation, PartWritingViolationKind } from './index.js';\n\n/**\n * Diatonic names of the interval numbers a line can span.\n *\n * Compound numbers are named too, because a leap of a ninth or a tenth is a\n * leap a rule fires on and the number is the whole of what the rationale is\n * saying: the fallback was the bare noun `interval`, which named nothing and\n * read as `The leap of a interval` where the sentence supplies the article.\n */\nconst INTERVAL_WORDS = [\n 'unison',\n 'second',\n 'third',\n 'fourth',\n 'fifth',\n 'sixth',\n 'seventh',\n 'octave',\n 'ninth',\n 'tenth',\n 'eleventh',\n 'twelfth',\n 'thirteenth',\n 'fourteenth',\n 'fifteenth',\n] as const;\n\n/**\n * Name an interval number.\n *\n * Past the range the words cover the number itself is written, as the sibling\n * rules that report a leap already do: `a 17th` names the interval, where a\n * noun that stands for every interval names none of them.\n */\nexport function intervalWord(numberValue: number): string {\n return INTERVAL_WORDS[numberValue - 1] ?? `${numberValue}th`;\n}\n\n/**\n * The article an interval name takes.\n *\n * Read from the name rather than from the number, so a name added to the list\n * carries its own article: `an octave`, `an eleventh`, `a ninth`.\n */\nexport function intervalArticle(word: string): string {\n return /^[aeiou]/.test(word) ? 'an' : 'a';\n}\n\n/** Build one violation record, so the field order is written once. */\nexport function violation(\n kind: PartWritingViolationKind,\n voices: number[],\n fromIndex: number,\n toIndex: number,\n rationale: string,\n): PartWritingViolation {\n return { kind, voices, fromIndex, toIndex, rationale };\n}\n","/**\n * Species counterpoint: mark a two-voice exercise against the sixteenth-century\n * rules.\n *\n * This is the conservatory exercise, not a general style checker. It grades a\n * counterpoint written above or below a given cantus firmus in one of the five\n * species, and reports what it finds as the same {@link PartWritingViolation}\n * the four-part checker uses.\n *\n * Both voices arrive as spelled notes, because half of what the rules forbid is\n * invisible in a pitch: the augmented second of the harmonic minor is a minor\n * third by ear, and the diminished fourth that may not stand as a consonance is\n * a major third by ear.\n */\n\nimport { InvalidInputError } from '../../core/errors/index.js';\nimport { ConsonanceClass } from '../../core/interval/index.js';\nimport { BEAT_EPS } from '../../core/meter/index.js';\nimport type { Note, SpelledInterval } from '../../core/pitch/index.js';\nimport {\n noteToMidi,\n noteToPitchClass,\n pitchClassOf,\n spelledInterval,\n} from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport { assertArray, assertOptions, describeRejected } from '../../core/validation/index.js';\nimport {\n classifySpelledInterval,\n createsBattuta,\n createsHiddenParallelPerfect,\n createsParallelPerfect,\n createsVoiceCrossing,\n isAugmentedMelodicInterval,\n isForbiddenMelodicLeap,\n} from '../counterpoint/index.js';\nimport { simpleIntervalNumber } from '../counterpoint/internal.js';\nimport { type KeyLike, toKeyScale } from '../scale/index.js';\nimport type { PartWritingViolation } from './index.js';\nimport { intervalArticle, intervalWord, violation } from './internal.js';\n\n/**\n * Which species an exercise is written in: note against note, two, three or\n * four notes against one, suspensions, or the florid mixture of them all.\n *\n * @category Voicing & Counterpoint\n */\nexport type Species = 1 | 2 | 3 | 4 | 5;\n\n/**\n * Options controlling {@link checkSpecies}.\n *\n * @category Voicing & Counterpoint\n */\nexport type SpeciesOptions = {\n /**\n * Length of each counterpoint note, measured in cantus-firmus notes: 1 is a\n * whole note filling a measure, 0.5 a half, 0.25 a quarter.\n *\n * The first four species have a fixed ratio, so the durations are implied by\n * the note count and this is only needed to describe something unusual. The\n * fifth species mixes note values by definition and cannot be read without it.\n */\n durations?: readonly number[];\n /**\n * Which side of the cantus firmus the counterpoint is written on. Defaults to\n * whichever side it actually lies on, by mean pitch.\n */\n counterpointAbove?: boolean;\n};\n\n/** Notes of counterpoint per cantus-firmus note, by species. */\nconst SPECIES_RATIO: Readonly<Record<Species, number>> = { 1: 1, 2: 2, 3: 4, 4: 2, 5: 0 };\n\n/** The species there are, read from the ratio table rather than listed twice. */\nconst SPECIES_NUMBERS: readonly number[] = Object.keys(SPECIES_RATIO).map(Number);\n\n/** The smallest note value the fifth species may write, in cantus-firmus notes. */\nconst MIN_FLORID_VALUE = 0.125;\n\n/**\n * One counterpoint note against one cantus-firmus note. A note that is still\n * sounding when the next cantus-firmus note is struck meets both of them, and\n * so has one entry per measure it sounds in: the first struck, the rest held.\n */\ntype Entry = {\n /** The counterpoint note itself. */\n note: Note;\n /** Its index in the counterpoint array. */\n index: number;\n /** Where this stretch of it begins, in cantus-firmus notes from the start. */\n onset: number;\n /** How long it goes on sounding from there, in cantus-firmus notes. */\n duration: number;\n /**\n * Whether the note is sounding on from an earlier attack rather than being\n * struck here — a ligature carried across the bar line. What it forms against\n * the new measure is judged as any other interval is; the melodic rules read\n * the attacks alone, since nothing was sung twice.\n */\n held: boolean;\n /** The cantus-firmus note sounding under it. */\n against: Note;\n /** Index of that cantus-firmus note. */\n againstIndex: number;\n /** Whether the note falls on the downbeat of its measure. */\n downbeat: boolean;\n /** The interval between the two voices, read from the lower note upward. */\n interval: SpelledInterval;\n /** How that interval sounds in two-voice counterpoint. */\n consonance: ConsonanceClass;\n};\n\n/** Mean sounding pitch of a line, for deciding which voice lies above. */\nfunction meanPitch(notes: readonly Note[]): number {\n if (notes.length === 0) {\n return 0;\n }\n return notes.reduce((sum, note) => sum + noteToMidi(note), 0) / notes.length;\n}\n\n/**\n * Note lengths implied by the species when the caller gives none.\n *\n * Every measure but the last carries the species' full ratio; whatever notes\n * are left share the closing measure, which lets an exercise end on the single\n * whole note convention asks for without being described note by note.\n */\nfunction impliedDurations(count: number, measures: number, ratio: number): number[] {\n const full = ratio * Math.max(measures - 1, 0);\n const closing = Math.max(count - full, 1);\n return Array.from({ length: count }, (_, index) => (index < full ? 1 / ratio : 1 / closing));\n}\n\n/** Whether a melodic interval is a diatonic step. */\nfunction isStep(interval: SpelledInterval): boolean {\n return interval.number === 2;\n}\n\n/** The direction a melodic interval moves: 1 up, -1 down, 0 for a repetition. */\nfunction direction(interval: SpelledInterval): number {\n return Math.sign(interval.semitones);\n}\n\n/** Whether two spelled notes are the same note, so a tie joins them. */\nfunction sameNote(a: Note, b: Note): boolean {\n return a.letter === b.letter && a.alter === b.alter && a.octave === b.octave;\n}\n\n/**\n * Whether a dissonance is passed through: approached and left by step in one\n * direction (a passing note) or by step and back (a neighbour note).\n *\n * The two are not licensed by the same species. A passing dissonance fills the\n * step between two consonances and every moving species writes one; a neighbour\n * turns back on the note it left, which belongs to the quarter-note line of the\n * third and fifth species. The second species moves a note against each half of\n * the measure and the only dissonance it may write is the passing one, so the\n * neighbour is read there as the dissonance it is.\n */\nfunction isPassedThrough(entries: readonly Entry[], position: number, species: Species): boolean {\n const previous = entries[position - 1];\n const current = entries[position];\n const next = entries[position + 1];\n if (previous === undefined || current === undefined || next === undefined) {\n return false;\n }\n const incoming = spelledInterval(previous.note, current.note);\n const outgoing = spelledInterval(current.note, next.note);\n if (!isStep(incoming) || !isStep(outgoing)) {\n return false;\n }\n if (direction(incoming) === direction(outgoing)) {\n return true;\n }\n return sameNote(previous.note, next.note) && (species === 3 || species === 5);\n}\n\n/** Whether a melodic interval leaps a third, the only leap the figures write. */\nfunction isThird(interval: SpelledInterval): boolean {\n return interval.number === 3;\n}\n\n/** The four notes of a figure, starting at one position, if the line has them. */\nfunction figureAt(\n entries: readonly Entry[],\n start: number,\n): [Entry, Entry, Entry, Entry] | undefined {\n const first = entries[start];\n const second = entries[start + 1];\n const third = entries[start + 2];\n const fourth = entries[start + 3];\n if (first === undefined || second === undefined || third === undefined || fourth === undefined) {\n return undefined;\n }\n return [first, second, third, fourth];\n}\n\n/**\n * Whether four notes spell the *nota cambiata*: a consonance, a dissonance\n * stepped down onto, a leap of a third down onto a consonance, and a step back\n * up.\n *\n * Fux grants the figure by name. The dissonance is quitted by leap, which every\n * other licence forbids, and the ear hears the note leapt to as the resolution\n * the step would have reached.\n */\nfunction isCambiata(entries: readonly Entry[], start: number): boolean {\n const figure = figureAt(entries, start);\n if (figure === undefined) {\n return false;\n }\n const [first, second, third, fourth] = figure;\n if (\n first.consonance === ConsonanceClass.Dissonance ||\n third.consonance === ConsonanceClass.Dissonance\n ) {\n return false;\n }\n const approach = spelledInterval(first.note, second.note);\n const leap = spelledInterval(second.note, third.note);\n const answer = spelledInterval(third.note, fourth.note);\n return (\n isStep(approach) &&\n direction(approach) < 0 &&\n isThird(leap) &&\n direction(leap) < 0 &&\n isStep(answer) &&\n direction(answer) > 0\n );\n}\n\n/**\n * Whether four notes spell the double neighbour: a step to one side of a note,\n * a leap of a third across to the other side, and a step back to the note the\n * figure left.\n *\n * Both notes in the middle decorate the same consonance, so either of them may\n * be the dissonance even though the figure moves between them by leap.\n */\nfunction isDoubleNeighbour(entries: readonly Entry[], start: number): boolean {\n const figure = figureAt(entries, start);\n if (figure === undefined) {\n return false;\n }\n const [first, second, third, fourth] = figure;\n if (first.consonance === ConsonanceClass.Dissonance || !sameNote(first.note, fourth.note)) {\n return false;\n }\n const away = spelledInterval(first.note, second.note);\n const across = spelledInterval(second.note, third.note);\n const back = spelledInterval(third.note, fourth.note);\n return (\n isStep(away) &&\n isThird(across) &&\n isStep(back) &&\n direction(away) !== 0 &&\n direction(across) === -direction(away)\n );\n}\n\n/**\n * Whether a dissonance stands inside one of the figures that quit a dissonance\n * by leap: the *nota cambiata* or the double neighbour.\n *\n * Both belong to the quarter-note line, so they are read in the third and fifth\n * species only. The second species moves a note against each half of the\n * measure and writes neither.\n */\nfunction isFiguredDissonance(\n entries: readonly Entry[],\n position: number,\n species: Species,\n): boolean {\n if (species !== 3 && species !== 5) {\n return false;\n }\n return (\n isCambiata(entries, position - 1) ||\n isDoubleNeighbour(entries, position - 1) ||\n isDoubleNeighbour(entries, position - 2)\n );\n}\n\n/** Where a species allows a dissonance to fall. */\ntype DissonanceLicence = 'none' | 'passing' | 'suspension';\n\n/** The licence a species grants at one metric position. */\nfunction licenceFor(species: Species, downbeat: boolean): DissonanceLicence {\n if (species === 1) {\n return 'none';\n }\n if (species === 4) {\n return downbeat ? 'suspension' : 'none';\n }\n if (species === 5) {\n return downbeat ? 'suspension' : 'passing';\n }\n return downbeat ? 'none' : 'passing';\n}\n\n/**\n * The rules judged on one counterpoint note: the vertical interval it forms,\n * and whether the voices have crossed.\n */\nfunction verticalViolations(\n entries: readonly Entry[],\n position: number,\n species: Species,\n counterpointAbove: boolean,\n crossed: Set<number>,\n): PartWritingViolation[] {\n const entry = entries[position];\n if (entry === undefined) {\n return [];\n }\n const found: PartWritingViolation[] = [];\n const upper = counterpointAbove ? entry.note : entry.against;\n const lower = counterpointAbove ? entry.against : entry.note;\n // Once per written note, not once per measure it sounds in: a note carried\n // across the bar line has an entry in each measure, and every field of a\n // crossing record comes from the note's own index, so a second one is a\n // duplicate a caller cannot tell from the first — two marks on one note in a\n // scoring UI, and the same error subtracted twice. The vertical interval\n // below is judged per measure, since a held note forms a new one there.\n if (!crossed.has(entry.index) && createsVoiceCrossing(upper, lower)) {\n crossed.add(entry.index);\n found.push(\n violation(\n 'voiceCrossing',\n [0, 1],\n entry.index,\n entry.index,\n 'The counterpoint crosses to the wrong side of the cantus firmus',\n ),\n );\n }\n if (entry.consonance !== ConsonanceClass.Dissonance) {\n return found;\n }\n const licence = licenceFor(species, entry.downbeat);\n if (\n licence === 'passing' &&\n (isPassedThrough(entries, position, species) || isFiguredDissonance(entries, position, species))\n ) {\n return found;\n }\n if (licence === 'suspension') {\n found.push(...suspensionViolations(entries, position));\n return found;\n }\n found.push(\n violation(\n 'unpreparedDissonance',\n [0, 1],\n entry.index,\n entry.index,\n licence === 'none'\n ? `A ${entry.interval.quality}${entry.interval.number} falls where this species allows only a consonance`\n : 'The dissonance is not passed through by step',\n ),\n );\n return found;\n}\n\n/**\n * The rules judged on a dissonant downbeat where the species writes\n * suspensions: it must be tied over from a consonance and fall by step onto the\n * next note.\n */\nfunction suspensionViolations(entries: readonly Entry[], position: number): PartWritingViolation[] {\n const entry = entries[position];\n if (entry === undefined) {\n return [];\n }\n const previous = entries[position - 1];\n const next = entries[position + 1];\n const prepared =\n previous !== undefined &&\n sameNote(previous.note, entry.note) &&\n previous.consonance !== ConsonanceClass.Dissonance;\n if (!prepared) {\n return [\n violation(\n 'unpreparedDissonance',\n [0, 1],\n entry.index,\n entry.index,\n 'The suspended dissonance is not tied over from a consonance',\n ),\n ];\n }\n const resolution = next === undefined ? undefined : spelledInterval(entry.note, next.note);\n if (\n next === undefined ||\n resolution === undefined ||\n !isStep(resolution) ||\n direction(resolution) >= 0\n ) {\n return [\n violation(\n 'unresolvedSuspension',\n [0, 1],\n entry.index,\n next === undefined ? entry.index : next.index,\n 'The suspension does not fall by step onto its resolution',\n ),\n ];\n }\n // The step down is only half of the resolution: what it falls onto has to be\n // the consonance the ear was waiting for. A suspension resolving to another\n // dissonance is reported here, on the suspension itself, rather than left to\n // whatever the note it lands on is charged with — in the fifth species that\n // note may be excused as a passing dissonance and nothing would be said at all.\n if (next.consonance === ConsonanceClass.Dissonance) {\n return [\n violation(\n 'unresolvedSuspension',\n [0, 1],\n entry.index,\n next.index,\n `The suspension falls onto a ${next.interval.quality}${next.interval.number}, which is no resolution`,\n ),\n ];\n }\n return [];\n}\n\n/** The rules judged along the counterpoint line itself. */\nfunction melodicViolations(from: Entry, to: Entry): PartWritingViolation[] {\n const interval = spelledInterval(from.note, to.note);\n if (isAugmentedMelodicInterval(from.note, to.note)) {\n return [\n violation(\n 'augmentedMelodicInterval',\n [1],\n from.index,\n to.index,\n `The counterpoint moves by an augmented ${intervalWord(interval.number)}`,\n ),\n ];\n }\n if (isForbiddenMelodicLeap(from.note, to.note)) {\n return [\n violation(\n 'illegalLeap',\n [1],\n from.index,\n to.index,\n `The counterpoint leaps by a ${interval.quality}${interval.number}`,\n ),\n ];\n }\n return [];\n}\n\n/** How many leaps in a row may carry the line the same way. */\nconst MAX_LEAPS_IN_ONE_DIRECTION = 2;\n\n/** How far leaps in one direction may carry the line in total, in semitones. */\nconst MAX_LEAP_CHAIN_SPAN = 12;\n\n/** The smallest leap that must be answered by a step the other way. */\nconst LEAP_NEEDING_ANSWER = 6;\n\n/** The tritone, in semitones, whichever way it is spelled. */\nconst TRITONE_SEMITONES = 6;\n\n/** One melodic move of the counterpoint, as the shape rules read it. */\ntype Move = {\n /** The note moved from. */\n from: Entry;\n /** The note moved to. */\n to: Entry;\n /** The interval between them. */\n interval: SpelledInterval;\n};\n\n/** Whether a melodic interval leaps rather than steps or repeats the note. */\nfunction isLeap(interval: SpelledInterval): boolean {\n return interval.number > 2;\n}\n\n/** Every move of the counterpoint, in order. */\nfunction melodicMoves(entries: readonly Entry[]): Move[] {\n const moves: Move[] = [];\n for (let position = 1; position < entries.length; position += 1) {\n const from = entries[position - 1];\n const to = entries[position];\n if (from === undefined || to === undefined) {\n continue;\n }\n moves.push({ from, to, interval: spelledInterval(from.note, to.note) });\n }\n return moves;\n}\n\n/**\n * The one high point: the line reaches its highest note once and does not\n * return to it.\n *\n * A note repeated in place is one arrival, and the closing note is left out of\n * the comparison — where the counterpoint ends is fixed by the cadence rather\n * than by the shape of the line, so a line that climbs to its final is not\n * charged with reaching its peak twice.\n */\nfunction climaxViolations(entries: readonly Entry[]): PartWritingViolation[] {\n const body = entries.slice(0, -1);\n const peak = body.reduce(\n (high, entry) => Math.max(high, noteToMidi(entry.note)),\n Number.NEGATIVE_INFINITY,\n );\n const arrivals = body.filter((entry, position) => {\n const previous = body[position - 1];\n return (\n noteToMidi(entry.note) === peak &&\n (previous === undefined || noteToMidi(previous.note) !== peak)\n );\n });\n const first = arrivals[0];\n const second = arrivals[1];\n if (first === undefined || second === undefined) {\n return [];\n }\n return [\n violation(\n 'melodicShape',\n [1],\n first.index,\n second.index,\n 'The line reaches its highest note more than once, so it has no one climax',\n ),\n ];\n}\n\n/**\n * Leaps taken one after another the same way: two at most, spanning an octave\n * at most, so that a chain of leaps still outlines a chord the ear can follow.\n */\nfunction leapChainViolations(moves: readonly Move[]): PartWritingViolation[] {\n const found: PartWritingViolation[] = [];\n let run: Move[] = [];\n const close = (): void => {\n const first = run[0];\n const last = run[run.length - 1];\n const span = run.reduce((total, move) => total + Math.abs(move.interval.semitones), 0);\n if (first !== undefined && last !== undefined) {\n if (run.length > MAX_LEAPS_IN_ONE_DIRECTION) {\n found.push(\n violation(\n 'melodicShape',\n [1],\n first.from.index,\n last.to.index,\n `The counterpoint leaps ${run.length} times in a row in the same direction`,\n ),\n );\n } else if (run.length > 1 && span > MAX_LEAP_CHAIN_SPAN) {\n found.push(\n violation(\n 'melodicShape',\n [1],\n first.from.index,\n last.to.index,\n 'Two leaps in the same direction carry the line further than an octave',\n ),\n );\n }\n }\n run = [];\n };\n for (const move of moves) {\n if (!isLeap(move.interval)) {\n close();\n continue;\n }\n const previous = run[run.length - 1];\n if (previous !== undefined && direction(previous.interval) !== direction(move.interval)) {\n close();\n }\n run.push(move);\n }\n close();\n return found;\n}\n\n/**\n * A wide leap is answered by a step the other way, which fills the gap it tore\n * open. Thirds, fourths and fifths are free to carry on: the wider the leap,\n * the stronger the style asks for the turn, and it asks outright from the sixth\n * upward.\n */\nfunction leapAnswerViolations(moves: readonly Move[]): PartWritingViolation[] {\n const found: PartWritingViolation[] = [];\n for (let position = 0; position < moves.length; position += 1) {\n const leap = moves[position];\n const answer = moves[position + 1];\n if (leap === undefined || answer === undefined || leap.interval.number < LEAP_NEEDING_ANSWER) {\n continue;\n }\n if (isStep(answer.interval) && direction(answer.interval) === -direction(leap.interval)) {\n continue;\n }\n const word = intervalWord(leap.interval.number);\n found.push(\n violation(\n 'melodicShape',\n [1],\n leap.from.index,\n answer.to.index,\n `The leap of ${intervalArticle(word)} ${word} is not answered by a step the other way`,\n ),\n );\n }\n return found;\n}\n\n/**\n * The tritone the line may not leap is also the tritone it may not draw: a run\n * of notes moving one way, however it is filled in, may not turn around on the\n * interval between its own extremes.\n *\n * A repeated note does not end the run, and a run of a single interval is left\n * to the leap rule, which already forbids it.\n */\nfunction tritoneOutlineViolations(entries: readonly Entry[]): PartWritingViolation[] {\n const found: PartWritingViolation[] = [];\n let run: Move[] = [];\n const close = (): void => {\n const first = run[0];\n const last = run[run.length - 1];\n if (first !== undefined && last !== undefined && run.length > 1) {\n const outline = spelledInterval(first.from.note, last.to.note);\n if (Math.abs(outline.semitones) === TRITONE_SEMITONES) {\n found.push(\n violation(\n 'melodicShape',\n [1],\n first.from.index,\n last.to.index,\n 'The line turns around on a tritone, outlining the interval it may not leap',\n ),\n );\n }\n }\n run = [];\n };\n for (const move of melodicMoves(entries)) {\n if (direction(move.interval) === 0) {\n continue;\n }\n const previous = run[run.length - 1];\n if (previous !== undefined && direction(previous.interval) !== direction(move.interval)) {\n close();\n }\n run.push(move);\n }\n close();\n return found;\n}\n\n/**\n * The moving species do not repeat a note: the second and third species are\n * written to keep the line going, and the repetition that carries a fourth or\n * fifth species phrase is a tie rather than a second attack.\n *\n * The closing measure is exempt. An exercise that writes its final measure in\n * the species' own note values rather than as the single whole note convention\n * asks for is holding the final, not attacking it again.\n */\nfunction repeatedNoteViolations(\n moves: readonly Move[],\n closingMeasure: number,\n): PartWritingViolation[] {\n return moves\n .filter(\n (move) => sameNote(move.from.note, move.to.note) && move.from.againstIndex < closingMeasure,\n )\n .map((move) =>\n violation(\n 'melodicShape',\n [1],\n move.from.index,\n move.to.index,\n 'The counterpoint repeats a note where this species keeps the line moving',\n ),\n );\n}\n\n/**\n * The rules judged on the shape of the counterpoint rather than on any one of\n * its intervals: the single climax, the chain of leaps and the step that\n * answers a wide one, the tritone outlined between turning points, and the\n * repeated note the moving species do not write.\n *\n * These belong to the species exercise, whose whole subject is the line. The\n * four-part exercise is judged chord by chord and lets a voice leap where the\n * harmony asks, so {@link checkPartWriting} does not apply them.\n */\nfunction melodicShapeViolations(\n entries: readonly Entry[],\n species: Species,\n): PartWritingViolation[] {\n const moves = melodicMoves(entries);\n const closingMeasure = entries[entries.length - 1]?.againstIndex ?? 0;\n const found = [\n ...climaxViolations(entries),\n ...leapChainViolations(moves),\n ...leapAnswerViolations(moves),\n ...tritoneOutlineViolations(entries),\n ...(species === 2 || species === 3 ? repeatedNoteViolations(moves, closingMeasure) : []),\n ];\n return found.sort((left, right) => left.fromIndex - right.fromIndex);\n}\n\n/**\n * The violations in the order the exercise commits them: by the note the fault\n * starts on, then by the note it ends on.\n *\n * The rules are accumulated rule by rule, since each reads the line its own way,\n * but a marked exercise is read from the top — so they are put back into time\n * order before they are handed out. The sort is stable, so two faults on the\n * same note keep the order the rules found them in and the same exercise always\n * marks the same way.\n */\nfunction inTimeOrder(violations: readonly PartWritingViolation[]): PartWritingViolation[] {\n return [...violations].sort(\n (left, right) => left.fromIndex - right.fromIndex || left.toIndex - right.toIndex,\n );\n}\n\n/** The rules judged between two voices moving together. */\nfunction motionViolations(\n from: Entry,\n to: Entry,\n counterpointAbove: boolean,\n): PartWritingViolation[] {\n const found: PartWritingViolation[] = [];\n const upperPrev = counterpointAbove ? from.note : from.against;\n const upperCur = counterpointAbove ? to.note : to.against;\n const lowerPrev = counterpointAbove ? from.against : from.note;\n const lowerCur = counterpointAbove ? to.against : to.note;\n if (createsParallelPerfect(upperPrev, upperCur, lowerPrev, lowerCur)) {\n const fifth = simpleIntervalNumber(to.interval.number) === 5;\n found.push(\n violation(\n fifth ? 'parallelFifth' : 'parallelOctave',\n [0, 1],\n from.index,\n to.index,\n fifth\n ? 'The two voices move into consecutive perfect fifths'\n : 'The two voices move into consecutive perfect octaves',\n ),\n );\n }\n // Two voices and nothing between them: the sixteenth-century rule forbids the\n // arrival however the upper voice reached it, so the step exception the\n // four-part chorale grants its outer voices is not applied here.\n if (createsHiddenParallelPerfect(upperPrev, upperCur, lowerPrev, lowerCur, 'twoVoice')) {\n found.push(\n violation(\n 'hiddenPerfect',\n [0, 1],\n from.index,\n to.index,\n 'The voices move into a perfect interval by similar motion',\n ),\n );\n }\n if (createsBattuta(upperPrev, upperCur, lowerPrev, lowerCur)) {\n found.push(\n violation(\n 'battuta',\n [0, 1],\n from.index,\n to.index,\n 'The upper voice leaps down into an octave against the lower voice',\n ),\n );\n }\n return found;\n}\n\n/**\n * The last counterpoint note sounding against an earlier cantus-firmus note\n * than the given one.\n *\n * The closing formula is a motion from one measure into the next, so it is read\n * by measure rather than by position in the array. A closing measure written in\n * the species' own note values holds more than one counterpoint note, and the\n * note before the last one then lies inside the final measure, which would\n * judge the cadence against itself.\n */\nfunction lastEntryBefore(entries: readonly Entry[], againstIndex: number): Entry | undefined {\n for (let position = entries.length - 1; position >= 0; position -= 1) {\n const entry = entries[position];\n if (entry !== undefined && entry.againstIndex < againstIndex) {\n return entry;\n }\n }\n return undefined;\n}\n\n/**\n * The opening and closing formulas.\n *\n * An exercise begins on a perfect consonance and ends on the octave or unison,\n * approached by step in the counterpoint from the major sixth above the cantus\n * firmus, or the minor third below it — the sixth or third that carries the\n * leading tone.\n */\nfunction cadenceViolations(\n entries: readonly Entry[],\n mode: KeyScale,\n counterpointAbove: boolean,\n): PartWritingViolation[] {\n const found: PartWritingViolation[] = [];\n const first = entries[0];\n const last = entries[entries.length - 1];\n if (first === undefined || last === undefined) {\n return found;\n }\n if (classifySpelledInterval(first.interval) !== ConsonanceClass.PerfectConsonance) {\n found.push(\n violation(\n 'missingCadence',\n [0, 1],\n first.index,\n first.index,\n 'The exercise does not begin on a perfect consonance',\n ),\n );\n }\n if (noteToPitchClass(last.note) !== pitchClassOf(mode.rootPc)) {\n found.push(\n violation(\n 'missingCadence',\n [0, 1],\n last.index,\n last.index,\n 'The counterpoint does not close on the final of the mode',\n ),\n );\n }\n const closing = last.interval;\n if (closing.quality !== 'P' || simpleIntervalNumber(closing.number) !== 1) {\n found.push(\n violation(\n 'missingCadence',\n [0, 1],\n last.index,\n last.index,\n 'The exercise does not close on an octave or unison',\n ),\n );\n return found;\n }\n const penultimate = lastEntryBefore(entries, last.againstIndex);\n if (penultimate === undefined) {\n return found;\n }\n const arrival = entries.find((entry) => entry.againstIndex === last.againstIndex) ?? last;\n const approach = spelledInterval(penultimate.note, arrival.note);\n const expected = counterpointAbove ? { quality: 'M', number: 6 } : { quality: 'm', number: 3 };\n if (\n penultimate.interval.quality !== expected.quality ||\n simpleIntervalNumber(penultimate.interval.number) !== expected.number ||\n !isStep(approach)\n ) {\n found.push(\n violation(\n 'missingCadence',\n [0, 1],\n penultimate.index,\n arrival.index,\n counterpointAbove\n ? 'The close is not the major sixth stepping out to the octave'\n : 'The close is not the minor third stepping in to the unison',\n ),\n );\n }\n return found;\n}\n\n/** The note lengths to read the exercise by, or the reason they cannot be read. */\nfunction resolveDurations(\n counterpoint: readonly Note[],\n measures: number,\n species: Species,\n opts: SpeciesOptions | undefined,\n): { durations: number[] } | { problem: string } {\n const given = opts?.durations;\n if (given !== undefined) {\n if (given.length !== counterpoint.length) {\n return {\n problem: `The exercise gives ${given.length} durations for ${counterpoint.length} notes`,\n };\n }\n for (const duration of given) {\n const units = duration / MIN_FLORID_VALUE;\n if (!(duration > 0) || duration > 1 || Math.abs(units - Math.round(units)) > BEAT_EPS) {\n return {\n problem: `A note lasts ${duration} of a measure, which this style does not write`,\n };\n }\n }\n const total = given.reduce((sum, duration) => sum + duration, 0);\n if (Math.abs(total - measures) > BEAT_EPS) {\n return { problem: `The counterpoint fills ${total} of the ${measures} measures` };\n }\n return { durations: [...given] };\n }\n if (species === 5) {\n throw new InvalidInputError(\n 'checkSpecies needs durations for the fifth species, whose note values are not fixed',\n );\n }\n const ratio = SPECIES_RATIO[species];\n const full = ratio * measures;\n // A closing measure written as one whole note is the convention rather than\n // an exception, so both note counts read as the correct ratio.\n if (counterpoint.length !== full && counterpoint.length !== full - ratio + 1) {\n return {\n problem: `Species ${species} sets ${ratio} notes against each of the ${measures} cantus firmus notes, and the counterpoint has ${counterpoint.length}`,\n };\n }\n return { durations: impliedDurations(counterpoint.length, measures, ratio) };\n}\n\n/**\n * Place every counterpoint note against the cantus-firmus notes it sounds with.\n *\n * A note is measured against each cantus-firmus note struck while it is still\n * sounding, not only the one it was struck over: the syncopation that defines\n * the fourth and fifth species holds a note across the bar line, and the\n * interval it forms in the measure it arrives in is the whole point of the\n * figure. Leaving it unread would let a bare dissonance on a downbeat pass for\n * a clean exercise.\n */\nfunction buildEntries(\n cantusFirmus: readonly Note[],\n counterpoint: readonly Note[],\n durations: readonly number[],\n counterpointAbove: boolean,\n): Entry[] {\n const entries: Entry[] = [];\n let onset = 0;\n for (let index = 0; index < counterpoint.length; index += 1) {\n const note = counterpoint[index];\n const duration = durations[index] ?? 0;\n if (note === undefined) {\n continue;\n }\n const struckOver = Math.min(Math.floor(onset + BEAT_EPS), cantusFirmus.length - 1);\n const lastOver = Math.min(\n Math.max(Math.ceil(onset + duration - BEAT_EPS) - 1, struckOver),\n cantusFirmus.length - 1,\n );\n for (let againstIndex = struckOver; againstIndex <= lastOver; againstIndex += 1) {\n const against = cantusFirmus[againstIndex];\n if (against === undefined) {\n continue;\n }\n const held = againstIndex > struckOver;\n // A held stretch begins where the measure does; the first begins wherever\n // the note was struck, which is what makes it a weak-beat note or not.\n const begins = held ? againstIndex : onset;\n const interval = counterpointAbove\n ? spelledInterval(against, note)\n : spelledInterval(note, against);\n entries.push({\n note,\n index,\n onset: begins,\n duration: onset + duration - begins,\n held,\n against,\n againstIndex,\n downbeat: Math.abs(begins - Math.round(begins)) < BEAT_EPS,\n interval,\n consonance: classifySpelledInterval(interval),\n });\n }\n onset += duration;\n }\n return entries;\n}\n\n/**\n * The pairs of counterpoint notes the motion rules are judged over: the\n * successive attacks that cross a bar line. It is given the notes actually\n * struck, since a note held on across the bar line is not a motion at all.\n *\n * Two counterpoint notes over one unmoving cantus-firmus note are oblique\n * motion, which no parallel rule can touch, so the pairs that can break a rule\n * are the ones where the lower voice has moved too. Perfect intervals on two\n * successive downbeats with other notes in between are deliberately not judged\n * here: whether the intervening note excuses them is exactly what the sources\n * disagree about, and the fourth species is built on the ligature that does.\n */\nfunction motionPairs(entries: readonly Entry[]): [number, number][] {\n const pairs: [number, number][] = [];\n for (let position = 1; position < entries.length; position += 1) {\n const previous = entries[position - 1];\n const current = entries[position];\n if (previous === undefined || current === undefined) {\n continue;\n }\n if (previous.againstIndex !== current.againstIndex) {\n pairs.push([position - 1, position]);\n }\n }\n return pairs;\n}\n\n/**\n * Check a species counterpoint exercise and report every rule it breaks.\n *\n * The cantus firmus is one note per measure. The counterpoint is aligned to it\n * by position rather than by written rhythm: the first four species have a fixed\n * number of notes per measure, so the note count alone says where each note\n * falls, and a closing measure written as a single whole note is accepted as the\n * convention it is. The fifth species mixes note values and therefore needs\n * `opts.durations`, given in cantus-firmus notes.\n *\n * Judged are the vertical intervals under each species' dissonance licence\n * (none in the first, a passing dissonance on the weak half in the second and\n * third, a prepared suspension on the downbeat in the fourth, both in the\n * fifth; the neighbour that turns back on its own note belongs to the\n * quarter-note line of the third and fifth species, which also write the *nota\n * cambiata* and the double neighbour, quitting a dissonance by leap). A note\n * still sounding when the next cantus-firmus note is struck is judged against\n * that note too, so a ligature carried across the bar line is read where it\n * lands as well as where it began. Judged besides are parallel and hidden\n * perfects and the *battuta* between the voices, augmented and otherwise\n * forbidden leaps along the counterpoint, the shape of the counterpoint as a\n * line, and the opening and closing formulas. Violations name the cantus firmus\n * as voice 0 and the counterpoint as voice 1, and index into the counterpoint.\n *\n * @param cantusFirmus The given voice, one spelled note per measure.\n * @param counterpoint The written voice, in spelled notes.\n * @param species Which species the exercise is written in.\n * @param modeLike The mode the exercise is in.\n * @param opts Note lengths, and which side the counterpoint is written on.\n * @returns Every violation found, in the order the exercise commits them; an\n * empty array for a clean exercise.\n * @throws If the species is not one of the five, either voice is empty, a note\n * carries no octave, or the fifth species is given without durations.\n * @example\n * ```ts\n * import { checkSpecies, majorKey, parseNote } from '@libraz/libcantus';\n * const cf = ['C4', 'D4', 'E4', 'D4', 'C4'].map((n) => parseNote(n));\n * const cp = ['C5', 'A4', 'G4', 'B4', 'C5'].map((n) => parseNote(n));\n * checkSpecies(cf, cp, 1, majorKey(0)); // [] — a clean first-species exercise\n * ```\n * @category Voicing & Counterpoint\n */\nexport function checkSpecies(\n cantusFirmus: readonly Note[],\n counterpoint: readonly Note[],\n species: Species,\n modeLike: KeyLike,\n opts?: SpeciesOptions,\n): PartWritingViolation[] {\n assertArray<Note>(cantusFirmus, 'cantus firmus');\n assertArray<Note>(counterpoint, 'counterpoint');\n const asked = assertOptions(opts, 'opts');\n const mode = toKeyScale(modeLike);\n if (!SPECIES_NUMBERS.includes(species)) {\n throw new InvalidInputError(\n `species must be one of ${SPECIES_NUMBERS.join(', ')}; received ${describeRejected(species)}`,\n );\n }\n if (cantusFirmus.length === 0 || counterpoint.length === 0) {\n throw new InvalidInputError('checkSpecies needs both a cantus firmus and a counterpoint');\n }\n const resolved = resolveDurations(counterpoint, cantusFirmus.length, species, asked);\n if ('problem' in resolved) {\n return inTimeOrder([\n violation('wrongRhythmicRatio', [1], 0, counterpoint.length - 1, resolved.problem),\n ]);\n }\n const counterpointAbove =\n asked.counterpointAbove ?? meanPitch(counterpoint) >= meanPitch(cantusFirmus);\n const entries = buildEntries(cantusFirmus, counterpoint, resolved.durations, counterpointAbove);\n // Every interval the exercise sounds is judged, including the ones a held\n // note forms in the measure it is carried into. The line itself is read from\n // the attacks alone: a note going on sounding was not sung a second time, and\n // the motion the voices make across a bar line is the one from the note\n // struck before it to the note struck after.\n const struck = entries.filter((entry) => !entry.held);\n\n const violations: PartWritingViolation[] = [];\n const crossed = new Set<number>();\n for (let position = 0; position < entries.length; position += 1) {\n violations.push(...verticalViolations(entries, position, species, counterpointAbove, crossed));\n }\n for (let position = 1; position < struck.length; position += 1) {\n const previous = struck[position - 1];\n const current = struck[position];\n if (previous !== undefined && current !== undefined) {\n violations.push(...melodicViolations(previous, current));\n }\n }\n violations.push(...melodicShapeViolations(struck, species));\n for (const [from, to] of motionPairs(struck)) {\n const previous = struck[from];\n const current = struck[to];\n if (previous !== undefined && current !== undefined) {\n violations.push(...motionViolations(previous, current, counterpointAbove));\n }\n }\n violations.push(...cadenceViolations(struck, mode, counterpointAbove));\n return inTimeOrder(violations);\n}\n","/**\n * Part writing: grade a finished exercise against the classical four-part rules.\n *\n * The counterpoint predicates each judge one pair of voices at one moment; this\n * module walks a whole progression and reports every violation it finds, so a\n * completed exercise can be marked in a single call.\n *\n * The input is spelled notes rather than MIDI pitches because two of the rules\n * cannot be decided without them: a cross relation is one letter carrying two\n * different accidentals, and an augmented second is a minor third under another\n * name. A caller holding only pitches spells them with {@link spellVoicing}.\n */\n\nimport { InvalidInputError } from '../../core/errors/index.js';\nimport type { Note } from '../../core/pitch/index.js';\nimport {\n formatNote,\n noteToMidi,\n noteToPitchClass,\n pitchClassOf,\n spelledInterval,\n} from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport { assertArray, assertOptions } from '../../core/validation/index.js';\nimport type { Chord } from '../chord/index.js';\nimport { assertChord, chordPitchClasses } from '../chord/index.js';\nimport {\n createsHiddenParallelPerfect,\n createsParallelPerfect,\n createsVoiceCrossing,\n createsVoiceOverlap,\n exceedsSpacing,\n isAugmentedMelodicInterval,\n isLeadingToneResolution,\n} from '../counterpoint/index.js';\nimport { type KeyLike, resolveKey, type SpelledKeyLike, toKeyScale } from '../scale/index.js';\nimport { spellPitch } from '../spelling/index.js';\nimport {\n isDescendingStep,\n isFrustratedLeadingTone,\n isFunctioningLeadingTone,\n leadingTonePcOf,\n seventhPcOf,\n} from '../tendency/index.js';\nimport type { VoiceRange } from '../voicing/index.js';\nimport { SATB_RANGES } from '../voicing/index.js';\nimport { resolveMaxSpacing, resolveRanges } from '../voicing/satb.js';\nimport { crossRelations, isOuterVoice } from './cross-relation.js';\nimport { intervalWord, violation } from './internal.js';\n\n/**\n * The rule a {@link PartWritingViolation} breaks.\n *\n * - `parallelFifth`, `parallelOctave`: consecutive perfect intervals of the same\n * kind between one pair of voices. A parallel unison is reported as a\n * `parallelOctave`, since it is the same perfect class.\n * - `hiddenPerfect`: the outer voices reach a perfect fifth or octave by similar\n * motion with a leap in the upper voice. The leap is what exposes it in a\n * four-part texture; in a two-voice species exercise nothing covers the\n * arrival, so similar motion into a perfect interval is reported however the\n * upper voice got there.\n * - `crossRelation`: one letter carries two different accidentals in two\n * different voices across a chord change, exposed — neither note led into by\n * step, at least one of them in an outer voice, and neither chord one of the\n * chromatic harmonies that are written with the contradiction.\n * - `voiceCrossing`, `overlap`, `spacing`, `range`: the texture rules.\n * - `unresolvedLeadingTone`, `unresolvedSeventh`: a tendency tone that does not\n * go where it must.\n * - `augmentedMelodicInterval`: a single voice moves by an augmented interval.\n *\n * The rest are raised by {@link checkSpecies} alone, since they describe faults\n * only a species exercise can commit:\n *\n * - `wrongRhythmicRatio`: the counterpoint's rhythm does not read against the\n * cantus firmus as its species requires — the wrong number of notes per\n * cantus-firmus note, and, in the fifth species, note values the style does\n * not write or a `durations` array that does not describe the counterpoint it\n * was given.\n * - `unpreparedDissonance`: a dissonance falls where the species allows none, or\n * is reached or left by leap where it must be passed through by step.\n * - `unresolvedSuspension`: a fourth-species suspension that does not fall by\n * step onto the following consonance.\n * - `illegalLeap`: a melodic leap the style forbids — either seventh, the\n * tritone, or anything wider than an octave.\n * - `battuta`: an octave or unison reached by contrary motion with the upper\n * voice leaping down.\n * - `missingCadence`: the exercise does not open or close on the formula its\n * species requires.\n * - `melodicShape`: the counterpoint is a poor line — it has no single climax,\n * leaps too often or too far one way, leaves a wide leap unanswered, outlines\n * a tritone, or repeats a note where the species keeps moving. The rationale\n * says which.\n *\n * @category Voicing & Counterpoint\n */\nexport type PartWritingViolationKind =\n | 'parallelFifth'\n | 'parallelOctave'\n | 'hiddenPerfect'\n | 'crossRelation'\n | 'voiceCrossing'\n | 'overlap'\n | 'spacing'\n | 'range'\n | 'unresolvedLeadingTone'\n | 'unresolvedSeventh'\n | 'augmentedMelodicInterval'\n | 'wrongRhythmicRatio'\n | 'unpreparedDissonance'\n | 'unresolvedSuspension'\n | 'illegalLeap'\n | 'battuta'\n | 'missingCadence'\n | 'melodicShape';\n\n/**\n * One broken rule, located in the progression and in the texture.\n *\n * @category Voicing & Counterpoint\n */\nexport type PartWritingViolation = {\n /** The rule that was broken. */\n kind: PartWritingViolationKind;\n /**\n * The voices involved, by index into a voicing (0 = lowest). A rule about a\n * pair names both, ascending; a rule about a single line names one. A\n * `crossRelation` is the exception: it names the voice holding the earlier\n * note first, whichever is higher. In a species exercise there are only two\n * voices and they are named by role instead: 0 is the cantus firmus and 1 the\n * counterpoint, whichever lies above.\n */\n voices: number[];\n /**\n * Index of the chord the motion starts on; in a species exercise, the index\n * into the counterpoint.\n */\n fromIndex: number;\n /** Index of the chord it ends on; equal to `fromIndex` for a vertical rule. */\n toIndex: number;\n /** A short sentence naming the musical reason. */\n rationale: string;\n};\n\n/**\n * A chord realized as one spelled note per voice, in voice order (index 0 =\n * bass), matching the order {@link voiceChord} and {@link SATB_RANGES} use.\n * Pitches need not ascend; a crossing is a reportable violation.\n *\n * Every note must carry an octave: the rules measure real distances between\n * sounding pitches, which a bare pitch class does not have.\n *\n * @category Voicing & Counterpoint\n */\nexport type SpelledVoicing = Note[];\n\n/**\n * Options controlling {@link checkPartWriting}.\n *\n * @category Voicing & Counterpoint\n */\nexport type PartWritingOptions = {\n /**\n * Per-voice ranges, ascending (index 0 = lowest), against which the `range`\n * rule is judged. Defaults to {@link SATB_RANGES} for a four-voice exercise;\n * for any other voice count there is no conventional compass to assume, so\n * the rule is skipped unless ranges are given. Given ranges must cover every\n * voice of the widest chord, and each must be finite with `min` no greater\n * than `max`.\n */\n ranges?: VoiceRange[];\n /**\n * Maximum spacing in semitones between adjacent upper voices. The bass-tenor\n * pair is exempt, as convention has it. Must be finite and non-negative.\n *\n * @defaultValue 12\n */\n maxSpacing?: number;\n};\n\n/** A note without its octave, for naming it in a rationale. */\nfunction bare(note: Note): Note {\n return { letter: note.letter, alter: note.alter };\n}\n\n/** The sounding pitch of a spelled note, located for the error message. */\nfunction midiOf(note: Note, index: number, voice: number): number {\n if (note.octave === undefined) {\n throw new InvalidInputError(`voicings[${index}][${voice}] must carry an octave`);\n }\n return noteToMidi(note);\n}\n\n/** One chord of the exercise, and everything the rules read off it. */\ntype Moment = {\n notes: SpelledVoicing;\n pitches: number[];\n chord: Chord;\n index: number;\n};\n\n/** The rules judged inside one chord: crossing, spacing, and range. */\nfunction verticalViolations(\n moment: Moment,\n ranges: readonly Readonly<VoiceRange>[] | undefined,\n maxSpacing: number,\n): PartWritingViolation[] {\n const found: PartWritingViolation[] = [];\n const { pitches, index } = moment;\n for (let voice = 1; voice < pitches.length; voice += 1) {\n const upper = pitches[voice];\n const lower = pitches[voice - 1];\n if (upper === undefined || lower === undefined) {\n continue;\n }\n if (createsVoiceCrossing(upper, lower)) {\n found.push(\n violation(\n 'voiceCrossing',\n [voice - 1, voice],\n index,\n index,\n 'The upper voice lies below the voice beneath it',\n ),\n );\n }\n // The bass-tenor pair is conventionally allowed to open up further than the\n // upper voices, so the spacing rule starts one pair above it.\n if (voice >= 2 && exceedsSpacing(upper, lower, maxSpacing)) {\n found.push(\n violation(\n 'spacing',\n [voice - 1, voice],\n index,\n index,\n `Adjacent upper voices lie more than ${maxSpacing} semitones apart`,\n ),\n );\n }\n }\n if (ranges === undefined) {\n return found;\n }\n for (let voice = 0; voice < pitches.length; voice += 1) {\n const pitch = pitches[voice];\n const range = ranges[voice];\n if (pitch === undefined || range === undefined) {\n continue;\n }\n if (pitch < range.min || pitch > range.max) {\n found.push(violation('range', [voice], index, index, 'The voice sings outside its range'));\n }\n }\n return found;\n}\n\n/**\n * Two consecutive chords of the exercise, the key they sound in, and the chords\n * on either side of the pair. The neighbours are what tells a chromatic tone\n * that was led into by step from one that was leapt at, and an applied dominant\n * from the diatonic triad that sounds the same.\n */\ntype Transition = {\n from: Moment;\n to: Moment;\n key: KeyScale;\n before?: Moment;\n after?: Moment;\n};\n\n/** The rules judged between two voices moving together: parallels and overlap. */\nfunction pairViolations(transition: Transition): PartWritingViolation[] {\n const found: PartWritingViolation[] = [];\n const { from, to } = transition;\n const voices = Math.min(from.pitches.length, to.pitches.length);\n for (let lower = 0; lower < voices; lower += 1) {\n for (let upper = lower + 1; upper < voices; upper += 1) {\n const upperPrev = from.pitches[upper];\n const upperCur = to.pitches[upper];\n const lowerPrev = from.pitches[lower];\n const lowerCur = to.pitches[lower];\n if (\n upperPrev === undefined ||\n upperCur === undefined ||\n lowerPrev === undefined ||\n lowerCur === undefined\n ) {\n continue;\n }\n if (createsParallelPerfect(upperPrev, upperCur, lowerPrev, lowerCur)) {\n const fifth = pitchClassOf(upperCur - lowerCur) === 7;\n found.push(\n violation(\n fifth ? 'parallelFifth' : 'parallelOctave',\n [lower, upper],\n from.index,\n to.index,\n fifth\n ? 'The two voices move into consecutive perfect fifths'\n : 'The two voices move into consecutive perfect octaves',\n ),\n );\n }\n if (upper === lower + 1 && createsVoiceOverlap(upperPrev, upperCur, lowerPrev, lowerCur)) {\n found.push(\n violation(\n 'overlap',\n [lower, upper],\n from.index,\n to.index,\n 'The voice moves past where its neighbour just was',\n ),\n );\n }\n }\n }\n // The hidden perfect is judged on the outer voices alone, where the bare fifth\n // or octave is exposed; between inner voices it is covered by the others.\n const bassPrev = from.pitches[0];\n const bassCur = to.pitches[0];\n const topPrev = from.pitches[voices - 1];\n const topCur = to.pitches[voices - 1];\n if (\n voices >= 2 &&\n bassPrev !== undefined &&\n bassCur !== undefined &&\n topPrev !== undefined &&\n topCur !== undefined &&\n createsHiddenParallelPerfect(topPrev, topCur, bassPrev, bassCur)\n ) {\n found.push(\n violation(\n 'hiddenPerfect',\n [0, voices - 1],\n from.index,\n to.index,\n 'The outer voices reach a perfect interval by similar motion, the upper one by leap',\n ),\n );\n }\n return found;\n}\n\n/**\n * Cross relations: the same letter carrying different accidentals in two\n * different voices across the chord change, reported as broken rules.\n *\n * The rule itself lives beside the voicing search that has to obey it, so what\n * is left here is naming the clash the search refused to write.\n */\nfunction crossRelationViolations(transition: Transition): PartWritingViolation[] {\n const { from, to, key, before, after } = transition;\n return crossRelations(from, to, key, { before: before?.notes, after: after?.chord }).map(\n (clash) =>\n violation(\n 'crossRelation',\n [clash.earlierVoice, clash.laterVoice],\n from.index,\n to.index,\n `${formatNote(bare(clash.earlier))} is contradicted by ${formatNote(bare(clash.later))} in another voice`,\n ),\n );\n}\n\n/**\n * The rules judged along one line: augmented melodic intervals, and the tendency\n * tones that must go somewhere in particular.\n *\n * A chordal seventh is a dissonance, so the voice carrying it falls by step\n * unless the next chord holds it as a common tone. A leading tone rises to the\n * tonic when it is functioning as one — the third of a dominant chord or the\n * root of a leading-tone chord — and the next chord contains the tonic and has\n * dropped the leading tone itself. Elsewhere the same pitch class is an\n * ordinary chord tone, so `iii` moving to `IV` is not asked to resolve its\n * fifth.\n *\n * The leading-tone rule is judged in every voice, but not by the same measure in\n * all of them: an inner voice may frustrate its leading tone, falling a third\n * onto the fifth of the arriving chord to complete a triad the rising resolution\n * would leave without one. That is the other standard answer to a complete\n * dominant seventh, so it is not reported. In the bass or the top voice the\n * leading tone is exposed and is still required to rise; any other way of\n * leaving it, in an inner voice or an outer one, is reported as before.\n */\nfunction melodicViolations(transition: Transition): PartWritingViolation[] {\n const found: PartWritingViolation[] = [];\n const { from, to, key } = transition;\n const seventhPc = seventhPcOf(from.chord);\n const nextPcs = new Set(chordPitchClasses(to.chord));\n const leadingTonePc = leadingTonePcOf(key);\n const tonicPc = pitchClassOf(key.rootPc);\n const voices = Math.min(from.notes.length, to.notes.length);\n for (let voice = 0; voice < voices; voice += 1) {\n const earlier = from.notes[voice];\n const later = to.notes[voice];\n const earlierPitch = from.pitches[voice];\n const laterPitch = to.pitches[voice];\n if (\n earlier === undefined ||\n later === undefined ||\n earlierPitch === undefined ||\n laterPitch === undefined\n ) {\n continue;\n }\n const interval = spelledInterval(earlier, later);\n if (isAugmentedMelodicInterval(earlier, later)) {\n found.push(\n violation(\n 'augmentedMelodicInterval',\n [voice],\n from.index,\n to.index,\n `The voice moves by an augmented ${intervalWord(interval.number)}`,\n ),\n );\n }\n const earlierPc = noteToPitchClass(earlier);\n if (earlierPc === seventhPc && !nextPcs.has(earlierPc) && !isDescendingStep(interval)) {\n found.push(\n violation(\n 'unresolvedSeventh',\n [voice],\n from.index,\n to.index,\n 'The chordal seventh does not fall by step',\n ),\n );\n }\n const inner = !isOuterVoice(voice, from.notes) && !isOuterVoice(voice, to.notes);\n if (\n earlierPc === leadingTonePc &&\n isFunctioningLeadingTone(from.chord, key) &&\n nextPcs.has(tonicPc) &&\n !nextPcs.has(earlierPc) &&\n !isLeadingToneResolution(earlierPitch, laterPitch, key) &&\n !(inner && isFrustratedLeadingTone(earlierPitch, laterPitch, to.chord))\n ) {\n found.push(\n violation(\n 'unresolvedLeadingTone',\n [voice],\n from.index,\n to.index,\n 'The leading tone does not rise to the tonic',\n ),\n );\n }\n }\n return found;\n}\n\n/**\n * Spell a voicing given as MIDI pitches, so it can be checked by\n * {@link checkPartWriting}.\n *\n * Each pitch is named against the chord sounding under it and then the key, so a\n * chord tone takes the letter the degree it plays in that chord implies: the\n * third of a D major chord in C major spells F#, not Gb. The chord is passed\n * whole rather than by its root, so a chord carrying its own spelling names its\n * tones here exactly as {@link spellChord} names them — the German sixth keeps\n * the F# that resolves outward to the dominant. The octave is kept, since the\n * rules measure real distances.\n *\n * @param voicing MIDI pitches in voice order (index 0 = bass).\n * @param chord The chord sounding, supplying the enharmonic evidence.\n * @param key The key the exercise is written in.\n * @returns One spelled note per pitch, in the same order.\n * @example\n * ```ts\n * import { majorKey, makeChord, noteNames, spellVoicing } from '@libraz/libcantus';\n * noteNames(spellVoicing([50, 57, 66, 69], makeChord(2, 'maj'), majorKey(0)));\n * // ['D3', 'A3', 'F#4', 'A4'] — the third of D spells F# in C major\n * ```\n * @category Voicing & Counterpoint\n */\nexport function spellVoicing(\n voicing: readonly number[],\n chord: Chord,\n key: SpelledKeyLike,\n): SpelledVoicing {\n assertArray<number>(voicing, 'voicing');\n assertChord(chord);\n // Read whole rather than reduced: a voicing in Ab minor is written on flats,\n // and a key handed in spelled that way was losing its spelling right here.\n const { tonic, scale } = resolveKey(key);\n return voicing.map((pitch) => spellPitch(pitch, tonic, scale, { chord }));\n}\n\n/**\n * Check a finished part-writing exercise and report every rule it breaks.\n *\n * The voicings are given one per chord, each with one spelled note per voice in\n * voice order (index 0 = bass), the order {@link voiceChord} and\n * {@link SATB_RANGES} already use. Notes must carry octaves. A crossing is\n * reported rather than rejected.\n *\n * Each chord is checked for voice crossing, over-wide spacing between adjacent\n * upper voices, and voices outside their range; each pair of consecutive chords\n * for parallel and hidden perfects, voice overlap, cross relations, augmented\n * melodic intervals, and unresolved tendency tones. Violations come back in\n * musical order: the rules inside a chord, then the rules taking it to the next.\n *\n * @param voicings One voicing per chord, in voice order, in spelled notes.\n * @param chords The chords those voicings realize, in the same order.\n * @param keyLike The key the exercise is written in; it names the leading tone.\n * @param opts Ranges and the upper-voice spacing limit. Both are validated\n * before any rule runs: an empty result has to mean that nothing was broken,\n * never that a rule could not be evaluated.\n * @returns Every violation found, in musical order; an empty array for a clean\n * exercise.\n * @throws If the two arrays differ in length, a note carries no octave, the\n * spacing limit is not a non-negative finite number, or the ranges are empty,\n * malformed, or too few for the voices being checked.\n * @example\n * ```ts\n * import { checkPartWriting, majorKey, makeChord, spellVoicing } from '@libraz/libcantus';\n * const key = majorKey(0);\n * const chords = [makeChord(0, 'maj'), makeChord(2, 'min')];\n * const voicings = [[48, 55, 64, 72], [50, 57, 65, 69]].map((pitches, i) =>\n * spellVoicing(pitches, chords[i] ?? chords[0], key),\n * );\n * const violations = checkPartWriting(voicings, chords, key);\n * violations.map((violation) => violation.kind); // ['parallelFifth']\n * violations[0]?.voices; // [0, 1]\n * ```\n * @category Voicing & Counterpoint\n */\nexport function checkPartWriting(\n voicings: readonly SpelledVoicing[],\n chords: readonly Chord[],\n keyLike: KeyLike,\n opts?: PartWritingOptions,\n): PartWritingViolation[] {\n assertArray<SpelledVoicing>(voicings, 'voicings');\n assertArray<Chord>(chords, 'chords');\n const asked = assertOptions(opts, 'opts');\n const key = toKeyScale(keyLike);\n if (voicings.length !== chords.length) {\n throw new InvalidInputError(\n `checkPartWriting needs one chord per voicing; received ${voicings.length} voicings and ${chords.length} chords`,\n );\n }\n // The limits are resolved through the same validators the voicing search\n // uses, and before any rule runs: a NaN limit would silently switch the\n // spacing rule off, and ranges too short for the texture would leave the top\n // voices unjudged, both of which report as a clean exercise.\n const maxSpacing = resolveMaxSpacing(asked);\n const voiceCount = voicings.reduce((widest, voicing) => Math.max(widest, voicing.length), 0);\n let ranges: readonly Readonly<VoiceRange>[] | undefined;\n if (asked.ranges === undefined) {\n // Four voices are the SATB exercise these ranges were written for; any other\n // count has no conventional compass, so an unasked-for range check would\n // invent one.\n ranges = voiceCount === 4 ? SATB_RANGES : undefined;\n } else {\n ranges = resolveRanges(asked);\n if (ranges.length < voiceCount) {\n throw new InvalidInputError(\n `checkPartWriting needs one range per voice; received ${ranges.length} ranges for ${voiceCount} voices`,\n );\n }\n }\n\n const moments: Moment[] = [];\n for (let index = 0; index < voicings.length; index += 1) {\n const notes = voicings[index];\n const chord = chords[index];\n if (notes === undefined || chord === undefined) {\n continue;\n }\n moments.push({\n notes,\n pitches: notes.map((note, voice) => midiOf(note, index, voice)),\n chord,\n index,\n });\n }\n\n const violations: PartWritingViolation[] = [];\n for (let position = 0; position < moments.length; position += 1) {\n const moment = moments[position];\n const previous = position > 0 ? moments[position - 1] : undefined;\n if (moment === undefined) {\n continue;\n }\n if (previous !== undefined) {\n const transition: Transition = {\n from: previous,\n to: moment,\n key,\n before: position > 1 ? moments[position - 2] : undefined,\n after: moments[position + 1],\n };\n violations.push(...pairViolations(transition));\n violations.push(...crossRelationViolations(transition));\n violations.push(...melodicViolations(transition));\n }\n violations.push(...verticalViolations(moment, ranges, maxSpacing));\n }\n return violations;\n}\n\nexport type { Species, SpeciesOptions } from './species.js';\nexport { checkSpecies } from './species.js';\n"]}