@libraz/libcantus 0.9.3 → 0.9.4

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 (198) hide show
  1. package/README.md +2 -2
  2. package/dist/analyze/index.cjs +39 -1947
  3. package/dist/analyze/index.cjs.map +1 -1
  4. package/dist/analyze/index.d.cts +197 -37
  5. package/dist/analyze/index.d.ts +197 -37
  6. package/dist/analyze/index.js +19 -8
  7. package/dist/chunk-36N73KZ3.cjs +246 -0
  8. package/dist/chunk-36N73KZ3.cjs.map +1 -0
  9. package/dist/chunk-4HLLZPXT.cjs +3077 -0
  10. package/dist/chunk-4HLLZPXT.cjs.map +1 -0
  11. package/dist/chunk-7MGZ7D62.cjs +498 -0
  12. package/dist/chunk-7MGZ7D62.cjs.map +1 -0
  13. package/dist/{chunk-4MHA432G.js → chunk-7NKDPF74.js} +403 -180
  14. package/dist/chunk-7NKDPF74.js.map +1 -0
  15. package/dist/chunk-CAN76NCU.cjs +679 -0
  16. package/dist/chunk-CAN76NCU.cjs.map +1 -0
  17. package/dist/{chunk-MIWO425M.js → chunk-D2FAJBYL.js} +101 -34
  18. package/dist/chunk-D2FAJBYL.js.map +1 -0
  19. package/dist/{chunk-SGU7SPLK.js → chunk-EHVEBWM5.js} +1056 -531
  20. package/dist/chunk-EHVEBWM5.js.map +1 -0
  21. package/dist/chunk-FCANXBAT.cjs +30 -0
  22. package/dist/chunk-FCANXBAT.cjs.map +1 -0
  23. package/dist/chunk-FNDWXRRI.cjs +479 -0
  24. package/dist/chunk-FNDWXRRI.cjs.map +1 -0
  25. package/dist/{chunk-IT2O4XFE.js → chunk-GQSGS2N4.js} +43 -24
  26. package/dist/chunk-GQSGS2N4.js.map +1 -0
  27. package/dist/{chunk-62QZKCJQ.js → chunk-HKGYCQ34.js} +10 -6
  28. package/dist/chunk-HKGYCQ34.js.map +1 -0
  29. package/dist/{chunk-AKLYPWIN.js → chunk-I24MV65U.js} +29 -6
  30. package/dist/chunk-I24MV65U.js.map +1 -0
  31. package/dist/{chunk-CHMXELDS.js → chunk-KMHOICIY.js} +311 -181
  32. package/dist/chunk-KMHOICIY.js.map +1 -0
  33. package/dist/chunk-LW53AHQU.cjs +1117 -0
  34. package/dist/chunk-LW53AHQU.cjs.map +1 -0
  35. package/dist/{chunk-2ZWMSJH6.js → chunk-NKIBQ6PT.js} +2 -2
  36. package/dist/chunk-NKIBQ6PT.js.map +1 -0
  37. package/dist/chunk-NP4NA2FB.cjs +106 -0
  38. package/dist/chunk-NP4NA2FB.cjs.map +1 -0
  39. package/dist/chunk-NVE34YGZ.cjs +1449 -0
  40. package/dist/chunk-NVE34YGZ.cjs.map +1 -0
  41. package/dist/{chunk-EH6EM37U.js → chunk-NYYWMV2C.js} +61 -49
  42. package/dist/chunk-NYYWMV2C.js.map +1 -0
  43. package/dist/{chunk-GOAW4VYL.js → chunk-Q2XUP227.js} +243 -117
  44. package/dist/chunk-Q2XUP227.js.map +1 -0
  45. package/dist/chunk-Q6OP4U43.cjs +327 -0
  46. package/dist/chunk-Q6OP4U43.cjs.map +1 -0
  47. package/dist/chunk-RC6GBMCD.cjs +845 -0
  48. package/dist/chunk-RC6GBMCD.cjs.map +1 -0
  49. package/dist/{chunk-C5XYKUVG.js → chunk-RK35RIN7.js} +370 -157
  50. package/dist/chunk-RK35RIN7.js.map +1 -0
  51. package/dist/chunk-RUMU3UA6.cjs +53 -0
  52. package/dist/chunk-RUMU3UA6.cjs.map +1 -0
  53. package/dist/chunk-T64C7ZD7.cjs +1 -0
  54. package/dist/chunk-T64C7ZD7.cjs.map +1 -0
  55. package/dist/chunk-XEBG3G53.js +498 -0
  56. package/dist/chunk-XEBG3G53.js.map +1 -0
  57. package/dist/{chunk-I5R7ZBJS.js → chunk-YLEMGEFH.js} +103 -59
  58. package/dist/chunk-YLEMGEFH.js.map +1 -0
  59. package/dist/chunk-ZEJR4TKL.cjs +173 -0
  60. package/dist/chunk-ZEJR4TKL.cjs.map +1 -0
  61. package/dist/core/index.cjs +130 -647
  62. package/dist/core/index.cjs.map +1 -1
  63. package/dist/core/index.d.cts +301 -39
  64. package/dist/core/index.d.ts +301 -39
  65. package/dist/core/index.js +54 -16
  66. package/dist/{function-DYt4iKsn.d.cts → function-BtiZb9i9.d.ts} +28 -7
  67. package/dist/{function-DVH4Eusr.d.ts → function-DNugAOrt.d.cts} +28 -7
  68. package/dist/generate/index.cjs +28 -4085
  69. package/dist/generate/index.cjs.map +1 -1
  70. package/dist/generate/index.d.cts +370 -62
  71. package/dist/generate/index.d.ts +370 -62
  72. package/dist/generate/index.js +23 -9
  73. package/dist/{index-CxqBQtH1.d.cts → index-6VR8-kit.d.ts} +19 -4
  74. package/dist/index-70p168VM.d.ts +192 -0
  75. package/dist/{index-CpOsP1y9.d.cts → index-Be74mcon.d.cts} +18 -17
  76. package/dist/index-BtXT6UQ0.d.cts +192 -0
  77. package/dist/{index-DxvARIP2.d.ts → index-C5UBbQ4G.d.ts} +18 -17
  78. package/dist/{index-Bt4tOUD9.d.ts → index-C83mZI1P.d.ts} +93 -8
  79. package/dist/{index-Bg5dYMmZ.d.ts → index-D6b6na69.d.cts} +14 -11
  80. package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
  81. package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
  82. package/dist/index-OG65ocOr.d.cts +272 -0
  83. package/dist/index-OG65ocOr.d.ts +272 -0
  84. package/dist/{index-Jc0Ryi79.d.ts → index-tJhkmQgE.d.cts} +19 -4
  85. package/dist/{index-Dikst44v.d.cts → index-tSQh-xwY.d.ts} +14 -11
  86. package/dist/{index-BNlGP86A.d.cts → index-y_ak9_Hi.d.cts} +93 -8
  87. package/dist/index.cjs +419 -7383
  88. package/dist/index.cjs.map +1 -1
  89. package/dist/index.d.cts +14 -14
  90. package/dist/index.d.ts +14 -14
  91. package/dist/index.js +111 -27
  92. package/dist/model/index.cjs +8 -2562
  93. package/dist/model/index.cjs.map +1 -1
  94. package/dist/model/index.d.cts +433 -75
  95. package/dist/model/index.d.ts +433 -75
  96. package/dist/model/index.js +6 -7
  97. package/dist/styled-DIRkk9Uz.d.ts +506 -0
  98. package/dist/styled-DJbkJQfj.d.cts +506 -0
  99. package/dist/theory/index.cjs +172 -1822
  100. package/dist/theory/index.cjs.map +1 -1
  101. package/dist/theory/index.d.cts +151 -233
  102. package/dist/theory/index.d.ts +151 -233
  103. package/dist/theory/index.js +30 -7
  104. package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
  105. package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
  106. package/package.json +24 -6
  107. package/dist/chunk-2ZWMSJH6.js.map +0 -1
  108. package/dist/chunk-4MHA432G.js.map +0 -1
  109. package/dist/chunk-62QZKCJQ.js.map +0 -1
  110. package/dist/chunk-AKLYPWIN.js.map +0 -1
  111. package/dist/chunk-C5XYKUVG.js.map +0 -1
  112. package/dist/chunk-CHMXELDS.js.map +0 -1
  113. package/dist/chunk-DYWFNC4A.js +0 -152
  114. package/dist/chunk-DYWFNC4A.js.map +0 -1
  115. package/dist/chunk-EH6EM37U.js.map +0 -1
  116. package/dist/chunk-FBQK3ZQN.js +0 -131
  117. package/dist/chunk-FBQK3ZQN.js.map +0 -1
  118. package/dist/chunk-GOAW4VYL.js.map +0 -1
  119. package/dist/chunk-I5R7ZBJS.js.map +0 -1
  120. package/dist/chunk-IT2O4XFE.js.map +0 -1
  121. package/dist/chunk-MIWO425M.js.map +0 -1
  122. package/dist/chunk-SGU7SPLK.js.map +0 -1
  123. package/dist/index-CGMZNCx6.d.cts +0 -140
  124. package/dist/index-CGMZNCx6.d.ts +0 -140
  125. package/dist/index-CnZCo29u.d.cts +0 -108
  126. package/dist/index-JnF4v9bx.d.ts +0 -108
  127. package/dist/styled-DZ7GjShb.d.ts +0 -282
  128. package/dist/styled-ZTbdaVFM.d.cts +0 -282
  129. package/src/analyze/arrange/index.ts +0 -21
  130. package/src/analyze/arrange/internal.ts +0 -127
  131. package/src/analyze/arrange/tension.ts +0 -199
  132. package/src/analyze/arrange/tracks.ts +0 -312
  133. package/src/analyze/detect/index.ts +0 -272
  134. package/src/analyze/functional/borrowed.ts +0 -84
  135. package/src/analyze/functional/cadence.ts +0 -66
  136. package/src/analyze/functional/function.ts +0 -184
  137. package/src/analyze/functional/index.ts +0 -23
  138. package/src/analyze/functional/internal.ts +0 -29
  139. package/src/analyze/functional/roman.ts +0 -402
  140. package/src/analyze/index.ts +0 -49
  141. package/src/analyze/timeline/index.ts +0 -451
  142. package/src/analyze/voice/index.ts +0 -339
  143. package/src/core/event-index/index.ts +0 -104
  144. package/src/core/index.ts +0 -61
  145. package/src/core/interval/index.ts +0 -81
  146. package/src/core/meter/index.ts +0 -302
  147. package/src/core/pitch/index.ts +0 -311
  148. package/src/core/random/index.ts +0 -76
  149. package/src/core/tuning/index.ts +0 -186
  150. package/src/core/types.ts +0 -29
  151. package/src/core/validation/index.ts +0 -119
  152. package/src/generate/bass/index.ts +0 -373
  153. package/src/generate/countermelody/index.ts +0 -470
  154. package/src/generate/drums/beat.ts +0 -266
  155. package/src/generate/drums/euclid.ts +0 -65
  156. package/src/generate/drums/fills.ts +0 -356
  157. package/src/generate/drums/ghost.ts +0 -119
  158. package/src/generate/drums/hihat.ts +0 -285
  159. package/src/generate/drums/hit.ts +0 -31
  160. package/src/generate/drums/index.ts +0 -361
  161. package/src/generate/drums/internal.ts +0 -257
  162. package/src/generate/drums/kick.ts +0 -160
  163. package/src/generate/drums/percussion.ts +0 -121
  164. package/src/generate/drums/rng.ts +0 -7
  165. package/src/generate/drums/swing.ts +0 -71
  166. package/src/generate/groove/index.ts +0 -351
  167. package/src/generate/harmonize/index.ts +0 -451
  168. package/src/generate/index.ts +0 -58
  169. package/src/generate/motif/index.ts +0 -422
  170. package/src/generate/progression/index.ts +0 -362
  171. package/src/generate/reharmony/index.ts +0 -323
  172. package/src/generate/rhythm/index.ts +0 -175
  173. package/src/index.ts +0 -13
  174. package/src/model/chord.ts +0 -442
  175. package/src/model/index.ts +0 -15
  176. package/src/model/interval.ts +0 -82
  177. package/src/model/key.ts +0 -266
  178. package/src/model/note.ts +0 -176
  179. package/src/model/progression.ts +0 -161
  180. package/src/model/shared.ts +0 -12
  181. package/src/theory/chord/index.ts +0 -375
  182. package/src/theory/chordscale/index.ts +0 -374
  183. package/src/theory/counterpoint/index.ts +0 -230
  184. package/src/theory/harmony/index.ts +0 -77
  185. package/src/theory/index.ts +0 -112
  186. package/src/theory/safety/index.ts +0 -488
  187. package/src/theory/scale/degrees.ts +0 -123
  188. package/src/theory/scale/index.ts +0 -28
  189. package/src/theory/scale/key.ts +0 -56
  190. package/src/theory/scale/masks.ts +0 -153
  191. package/src/theory/spelling/index.ts +0 -249
  192. package/src/theory/symbol/index.ts +0 -268
  193. package/src/theory/voicing/index.ts +0 -5
  194. package/src/theory/voicing/internal.ts +0 -168
  195. package/src/theory/voicing/leading.ts +0 -112
  196. package/src/theory/voicing/satb.ts +0 -216
  197. package/src/theory/voicing/styled.ts +0 -199
  198. package/tsconfig.json +0 -23
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/libcantus/libcantus/dist/chunk-NVE34YGZ.cjs","../src/model/interval.ts","../src/model/note.ts","../src/model/shared.ts","../src/model/key.ts","../src/model/progression.ts","../src/model/chord.ts"],"names":[],"mappings":"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","file":"/home/runner/work/libcantus/libcantus/dist/chunk-NVE34YGZ.cjs","sourcesContent":[null,"import { InvalidInputError } from '../core/errors/index.js';\nimport { isConsonantInterval } from '../core/interval/index.js';\nimport type { IntervalQualityLabel, SpelledInterval } from '../core/pitch/index.js';\nimport { intervalSemitones, parseInterval } from '../core/pitch/index.js';\nimport type { Note } from './note.js';\n\n/** Number of diatonic degrees an interval and its inversion span together. */\nconst INVERSION_SUM = 9;\n\n/** The quality an interval's inversion carries. */\nfunction invertQuality(quality: IntervalQualityLabel): IntervalQualityLabel {\n if (quality === 'P') {\n return 'P';\n }\n if (quality === 'M') {\n return 'm';\n }\n if (quality === 'm') {\n return 'M';\n }\n return quality.startsWith('A')\n ? (`d${'d'.repeat(quality.length - 1)}` as IntervalQualityLabel)\n : (`A${'A'.repeat(quality.length - 1)}` as IntervalQualityLabel);\n}\n\n/**\n * An immutable spelled interval value: a diatonic number, a quality label, and\n * a signed semitone span. A thin convenience wrapper over the pitch module's\n * plain interval result.\n *\n * @category Class API\n * @example\n * ```ts\n * import { Interval, Note } from '@libraz/libcantus';\n * Interval.between(Note.of('C4'), Note.of('G4')).name; // 'P5'\n * ```\n */\nexport class Interval {\n readonly #number: number;\n readonly #quality: IntervalQualityLabel;\n readonly #semitones: number;\n\n private constructor(numberValue: number, quality: IntervalQualityLabel, semitones: number) {\n this.#number = numberValue;\n this.#quality = quality;\n this.#semitones = semitones;\n }\n\n /**\n * The spelled interval between two notes.\n *\n * @param a The first note.\n * @param b The second note.\n * @returns The interval from `a` to `b`.\n */\n static between(a: Note, b: Note): Interval {\n return a.intervalTo(b);\n }\n\n /**\n * Build an interval from explicit components.\n *\n * @param numberValue Diatonic size: 1 = unison, 2 = second, ... 8 = octave.\n * @param quality Quality label: `'P'`, `'M'`, `'m'`, or repeated `'A'`/`'d'`.\n * @param semitones Signed semitone span; its magnitude must be the span the\n * number and quality describe.\n * @returns The interval.\n * @throws If the three components do not describe the same interval — a\n * `P5` spanning 8 semitones is not a value any other method can produce.\n */\n static of(numberValue: number, quality: IntervalQualityLabel, semitones: number): Interval {\n const expected = intervalSemitones(numberValue, quality);\n if (Math.abs(semitones) !== expected) {\n throw new InvalidInputError(\n `${quality}${numberValue} spans ${expected} semitones; received ${semitones}`,\n );\n }\n return new Interval(numberValue, quality, semitones);\n }\n\n /**\n * Wrap a plain spelled interval, as returned by the pitch module.\n *\n * @param data The plain interval.\n * @returns The wrapped interval.\n */\n static fromData(data: SpelledInterval): Interval {\n return new Interval(data.number, data.quality, data.semitones);\n }\n\n /**\n * Rebuild an interval from its {@link Interval.toJSON} output.\n *\n * @param data The serialized interval.\n * @returns The wrapped interval.\n */\n static fromJSON(data: SpelledInterval): Interval {\n return Interval.fromData(data);\n }\n\n /**\n * Parse an interval name such as `'P5'`, `'m3'`, or `'AA4'`.\n *\n * @param name The interval name.\n * @returns The ascending interval of that name.\n * @throws If the name is not a quality label followed by a diatonic number.\n */\n static parse(name: string): Interval {\n return Interval.fromData(parseInterval(name));\n }\n\n /** Diatonic size: 1 = unison, 2 = second, ... 8 = octave, and beyond. */\n get number(): number {\n return this.#number;\n }\n\n /** Quality label: `'P'`, `'M'`, `'m'`, or repeated `'A'`/`'d'`. */\n get quality(): IntervalQualityLabel {\n return this.#quality;\n }\n\n /** Signed semitone span. */\n get semitones(): number {\n return this.#semitones;\n }\n\n /** A readable label composed of quality and number, e.g. `'P5'` or `'M3'`. */\n get name(): string {\n return `${this.#quality}${this.#number}`;\n }\n\n /**\n * The interval's inversion: the complement that completes the octave.\n *\n * A compound interval is reduced to its simple form first, and the result is\n * always ascending — an inversion answers \"what is left of the octave\",\n * which has no direction of its own.\n *\n * @returns The inverted interval, e.g. `M3` becomes `m6`.\n */\n invert(): Interval {\n const simple = ((this.#number - 1) % 7) + 1;\n const numberValue = INVERSION_SUM - simple;\n const quality = invertQuality(this.#quality);\n return Interval.of(numberValue, quality, intervalSemitones(numberValue, quality));\n }\n\n /**\n * Whether the interval is consonant.\n *\n * @param twoVoice When true, the perfect fourth counts as a dissonance,\n * matching two-voice counterpoint.\n * @returns True if the interval is consonant in that context.\n */\n isConsonant(twoVoice = true): boolean {\n return isConsonantInterval(this.#semitones, twoVoice);\n }\n\n /**\n * Whether another interval is spelled identically.\n *\n * Enharmonic equivalents are not equal: an augmented second and a minor\n * third span the same distance but are different intervals.\n *\n * @param other The interval to compare with.\n * @returns True when number, quality, and span all match.\n */\n equals(other: Interval): boolean {\n return (\n this.#number === other.number &&\n this.#quality === other.quality &&\n this.#semitones === other.semitones\n );\n }\n\n /**\n * The plain interval data, for JSON serialization.\n *\n * Private class fields do not serialize, so an explicit `toJSON` keeps\n * `JSON.stringify(interval)` from collapsing to `{}`.\n *\n * @returns The diatonic number, quality, and semitone span.\n */\n toJSON(): SpelledInterval {\n return { number: this.#number, quality: this.#quality, semitones: this.#semitones };\n }\n\n /**\n * The interval's name, so a template literal or a log line reads as the\n * interval.\n *\n * @returns The name, e.g. `'M3'`.\n */\n toString(): string {\n return this.name;\n }\n}\n","import { InvalidInputError } from '../core/errors/index.js';\nimport {\n formatNote,\n midiToNote,\n type Note as NoteData,\n noteToMidi,\n noteToPitchClass,\n parseNote,\n spelledInterval,\n transposeByInterval,\n transposeNote,\n} from '../core/pitch/index.js';\nimport { Interval } from './interval.js';\n\n/**\n * Defensive copy of a plain note, with the letter checked.\n *\n * Formatting and pitch-class arithmetic reduce the letter modulo 7, but\n * `equals` compares it directly: an unreduced letter produces two notes that\n * print the same name and report the same pitch class yet compare unequal.\n */\nfunction copyNote(data: NoteData): NoteData {\n if (!Number.isInteger(data.letter) || data.letter < 0 || data.letter > 6) {\n throw new InvalidInputError(\n `note letter must be an integer in [0, 6]; received ${data.letter}`,\n );\n }\n const copy: NoteData = { letter: data.letter, alter: data.alter };\n if (data.octave !== undefined) {\n copy.octave = data.octave;\n }\n return copy;\n}\n\n/**\n * An immutable spelled note: a diatonic letter plus a chromatic alteration and\n * an optional octave. Wraps the plain note object and delegates to the pitch\n * module; every transformation returns a new instance.\n *\n * @category Class API\n * @example\n * ```ts\n * import { Note } from '@libraz/libcantus';\n * Note.of('C4').transpose(7).name; // 'G4'\n * ```\n */\nexport class Note {\n readonly #data: NoteData;\n\n /**\n * Wrap a plain note object.\n *\n * @param data The spelled note; it is copied, never retained or mutated.\n * @throws If `letter` is not an integer in 0..6.\n */\n constructor(data: NoteData) {\n this.#data = copyNote(data);\n }\n\n /**\n * Parse scientific pitch notation (e.g. `'C#4'`, `'Bb'`, `'F##3'`).\n *\n * @param name The note text.\n * @returns The parsed note.\n * @throws If the text is not a valid note.\n */\n static of(name: string): Note {\n return new Note(parseNote(name));\n }\n\n /**\n * Name a MIDI number (middle C = C4 = 60) as a note.\n *\n * @param midi The MIDI number.\n * @param spelling Whether to prefer sharps or flats for black keys.\n * @returns The spelled note, with octave.\n */\n static fromMidi(midi: number, spelling: 'sharp' | 'flat' = 'sharp'): Note {\n return new Note(midiToNote(midi, spelling));\n }\n\n /**\n * Wrap an existing plain note object.\n *\n * @param data The plain note.\n * @returns The wrapped note.\n * @throws If `letter` is not an integer in 0..6.\n */\n static fromData(data: NoteData): Note {\n return new Note(data);\n }\n\n /** The note rendered as scientific pitch notation, e.g. `'G4'` or `'Bb'`. */\n get name(): string {\n return formatNote(this.#data);\n }\n\n /** The pitch class (0..11), ignoring octave. */\n get pitchClass(): number {\n return noteToPitchClass(this.#data);\n }\n\n /**\n * The MIDI number (middle C = C4 = 60).\n *\n * @throws If the note has no octave and therefore no fixed pitch.\n */\n get midi(): number {\n if (this.#data.octave === undefined) {\n throw new InvalidInputError(`note ${this.name} has no octave, so it has no MIDI number`);\n }\n return noteToMidi(this.#data);\n }\n\n /** The diatonic letter number: 0..6 for C..B. */\n get letter(): number {\n return this.#data.letter;\n }\n\n /** The chromatic alteration in semitones: -1 flat, 0 natural, +1 sharp, ... */\n get alter(): number {\n return this.#data.alter;\n }\n\n /** The octave (scientific pitch notation), or undefined for a bare pitch class. */\n get octave(): number | undefined {\n return this.#data.octave;\n }\n\n /** A copy of the underlying plain note object. */\n get data(): NoteData {\n return copyNote(this.#data);\n }\n\n /**\n * Transpose by a signed number of semitones, keeping the spelling.\n *\n * The letter moves by the diatonic distance of the conventional interval for\n * that many semitones, so `Ab4` up a major second is `Bb4` rather than `A#4`\n * and a flat key stays on the flat side. An octave-less note stays\n * octave-less: only its pitch class is moved. Transposing by zero is the\n * identity: the original spelling is preserved (no enharmonic respelling).\n *\n * @param semitones The signed semitone offset.\n * @param opts `spelling` forces the result onto the sharp or flat side\n * instead of following this note's letter.\n * @returns The transposed note.\n * @example\n * ```ts\n * import { Note } from '@libraz/libcantus';\n * Note.of('Ab4').transpose(2).name; // 'Bb4'\n * Note.of('Ab4').transpose(2, { spelling: 'sharp' }).name; // 'A#4'\n * ```\n */\n transpose(semitones: number, opts?: { spelling?: 'sharp' | 'flat' }): Note {\n if (semitones === 0 && opts?.spelling === undefined) {\n return new Note(this.#data);\n }\n return new Note(transposeNote(this.#data, semitones, opts));\n }\n\n /**\n * The spelled interval from this note to another.\n *\n * @param other The second note.\n * @returns The interval, which {@link Interval.toJSON} unwraps to plain data.\n */\n intervalTo(other: Note): Interval {\n // Read the other note through its public accessor rather than its private\n // field: a bundler that emits two copies of this class — as a CommonJS\n // build without shared chunks does for the root and /model entries — would\n // otherwise throw on the brand check.\n return Interval.fromData(spelledInterval(this.#data, other.data));\n }\n\n /**\n * Transpose by a spelled interval, keeping the spelling the interval names.\n *\n * Unlike {@link Note.transpose}, which picks a letter from the semitone\n * count, the interval's diatonic number decides the letter: C up an\n * augmented second is D#, not Eb.\n *\n * @param interval The interval to apply; a descending interval moves down.\n * @returns The transposed note.\n * @example\n * ```ts\n * import { Interval, Note } from '@libraz/libcantus';\n * Note.of('C4').transposeBy(Interval.parse('A2')).name; // 'D#4'\n * ```\n */\n transposeBy(interval: Interval): Note {\n return new Note(transposeByInterval(this.#data, interval.toJSON()));\n }\n\n /**\n * Rebuild a note from its {@link Note.toJSON} output.\n *\n * @param data The serialized note.\n * @returns The wrapped note.\n * @throws If `letter` is not an integer in 0..6.\n */\n static fromJSON(data: NoteData): Note {\n return new Note(data);\n }\n\n /**\n * Whether another note has the same letter, alteration, and octave.\n *\n * @param other The note to compare.\n * @returns True if the spellings are identical.\n */\n equals(other: Note): boolean {\n const b = other.data;\n return (\n this.#data.letter === b.letter &&\n this.#data.alter === b.alter &&\n this.#data.octave === b.octave\n );\n }\n\n /**\n * The plain note data, for JSON serialization.\n *\n * Private class fields do not serialize, so an explicit `toJSON` keeps\n * `JSON.stringify(note)` from collapsing to `{}`.\n *\n * @returns A copy of the underlying plain note object.\n */\n toJSON(): NoteData {\n return this.data;\n }\n\n /**\n * The note's name, so a template literal or a log line reads as the note.\n *\n * @returns The spelled name, e.g. `'Bb3'`.\n */\n toString(): string {\n return this.name;\n }\n}\n","import { midiToNote, pitchClassOf as mod12, type Note as NoteData } from '../core/pitch/index.js';\n\nexport { pitchClassOf as mod12 } from '../core/pitch/index.js';\n\n/** Spell a bare pitch class as an octave-less note with a sharp/flat preference. */\nexport function spellPitchClassBare(pc: number, spelling: 'sharp' | 'flat'): NoteData {\n const spelled = midiToNote(60 + mod12(pc), spelling);\n return { letter: spelled.letter, alter: spelled.alter };\n}\n","import type { DetectKeyOptions } from '../analyze/detect/index.js';\nimport { detectKey, detectKeyBest } from '../analyze/detect/index.js';\nimport { isMinorKey, romanToChord } from '../analyze/functional/index.js';\nimport { InvalidInputError } from '../core/errors/index.js';\nimport type { Note as NoteData } from '../core/pitch/index.js';\nimport type { KeyScale } from '../core/types.js';\nimport { assertFiniteNumber } from '../core/validation/index.js';\nimport {\n type ChordQuality,\n chordFromDegree,\n diatonicSeventh,\n diatonicTriad,\n} from '../theory/chord/index.js';\nimport {\n isScaleTone,\n majorKey,\n minorKey,\n scaleByName,\n scaleTonesInDegreeOrder,\n} from '../theory/scale/index.js';\nimport { spellScale } from '../theory/spelling/index.js';\nimport { Chord } from './chord.js';\nimport { Note } from './note.js';\nimport { mod12, spellPitchClassBare } from './shared.js';\n\n/** Total accidentals a spelled tonic produces across a key's whole scale. */\nfunction accidentalLoad(tonic: NoteData, scale: KeyScale): number {\n return spellScale(tonic, scale).reduce((sum, note) => sum + Math.abs(note.alter), 0);\n}\n\n/**\n * Choose the tonic spelling (sharp- or flat-side) that spells `scale` with the\n * fewest accidentals, so a numeric root never yields a double-flat/double-sharp\n * scale (e.g. pitch class 6 minor spells as F# minor, not Gb minor with Bbb).\n */\nfunction bestTonicForScale(rootPc: number, scale: KeyScale): Note {\n const sharp = spellPitchClassBare(rootPc, 'sharp');\n const flat = spellPitchClassBare(rootPc, 'flat');\n if (sharp.letter === flat.letter && sharp.alter === flat.alter) {\n return new Note(sharp);\n }\n return accidentalLoad(flat, scale) <= accidentalLoad(sharp, scale)\n ? new Note(flat)\n : new Note(sharp);\n}\n\n/**\n * An immutable key/scale: a `KeyScale` (root pitch class plus mode mask) paired\n * with a spelled tonic that anchors letter-name spelling. Acts as the factory\n * for key-aware chords.\n *\n * @category Class API\n * @example\n * ```ts\n * import { Key } from '@libraz/libcantus';\n * Key.major('C').chord(4).symbol(); // 'G' (the diatonic triad on scale degree 4)\n * ```\n */\nexport class Key {\n readonly #scale: KeyScale;\n readonly #tonic: Note;\n\n /**\n * Wrap a key/scale and its spelled tonic.\n *\n * @param scale The key/scale; its root is normalized to a pitch class.\n * @param tonic The spelled tonic anchoring letter-name spelling.\n */\n constructor(scale: KeyScale, tonic: Note) {\n const rootPc = mod12(scale.rootPc);\n if (tonic.pitchClass !== rootPc) {\n throw new InvalidInputError(\n `tonic ${tonic.name} does not match the scale root pitch class ${rootPc}; ` +\n 'pass a tonic that spells the scale root, or omit it to have one chosen',\n );\n }\n this.#scale = { rootPc, modeMask12: scale.modeMask12 };\n this.#tonic = tonic;\n }\n\n /**\n * A major key.\n *\n * @param root Tonic as a note name (e.g. `'Eb'`) or a pitch class; a numeric\n * root is spelled with whichever accidental side yields the fewest\n * accidentals across the scale.\n * @returns The major key.\n */\n static major(root: string | number): Key {\n if (typeof root === 'string') {\n const tonic = Note.of(root);\n return new Key(majorKey(tonic.pitchClass), tonic);\n }\n const scale = majorKey(root);\n return new Key(scale, bestTonicForScale(root, scale));\n }\n\n /**\n * A natural-minor key.\n *\n * @param root Tonic as a note name or a pitch class; a numeric root is\n * spelled with whichever accidental side yields the fewest accidentals\n * across the scale.\n * @returns The minor key.\n */\n static minor(root: string | number): Key {\n if (typeof root === 'string') {\n const tonic = Note.of(root);\n return new Key(minorKey(tonic.pitchClass), tonic);\n }\n const scale = minorKey(root);\n return new Key(scale, bestTonicForScale(root, scale));\n }\n\n /**\n * A key on a named scale (e.g. `'dorian'`, `'harmonicMinor'`).\n *\n * @param name The scale name, a key of the scale module's named-scale table.\n * @param root Tonic as a note name or a pitch class; a numeric root is\n * spelled with whichever accidental side yields the fewest accidentals\n * across the scale, exactly as {@link Key.major} does.\n * @returns The key.\n * @throws If the name is not a known scale.\n */\n static named(name: string, root: string | number): Key {\n if (typeof root === 'string') {\n const tonic = Note.of(root);\n return new Key(scaleByName(name, tonic.pitchClass), tonic);\n }\n const scale = scaleByName(name, mod12(root));\n return new Key(scale, bestTonicForScale(mod12(root), scale));\n }\n\n /**\n * Wrap an existing `KeyScale`, synthesizing a spelled tonic when none is\n * given.\n *\n * The synthesized tonic is chosen the same way as for a numeric root\n * elsewhere: whichever accidental side spells the scale with the fewest\n * accidentals. This is what keeps `Key.of(detectKey(...).scale)` from handing\n * every downstream chord a double-sharp spelling.\n *\n * @param scale The key/scale to wrap.\n * @param tonic Optional spelled tonic; must spell the scale's root pitch class.\n * @returns The key.\n * @throws If the given tonic is not the scale's root pitch class.\n */\n static of(scale: KeyScale, tonic?: Note): Key {\n return new Key(scale, tonic ?? bestTonicForScale(mod12(scale.rootPc), scale));\n }\n\n /**\n * Rebuild a key from its {@link Key.toJSON} output.\n *\n * @param data The serialized key and tonic.\n * @returns The key.\n */\n static fromJSON(data: { scale: KeyScale; tonic: NoteData }): Key {\n return new Key(data.scale, new Note(data.tonic));\n }\n\n /**\n * Identify the keys a set of pitches fits, best interpretation first.\n *\n * The counterpart of {@link Chord.detect}.\n *\n * @param pitches MIDI pitches or bare pitch classes.\n * @param opts How to weigh the input; see {@link DetectKeyOptions}.\n * @returns Ranked keys (may be empty).\n */\n static detect(pitches: readonly number[], opts?: DetectKeyOptions): Key[] {\n return detectKey(pitches, opts).map((match) => Key.of(match.key));\n }\n\n /**\n * The single best key interpretation of a pitch set.\n *\n * @param pitches MIDI pitches or bare pitch classes.\n * @param opts How to weigh the input; see {@link DetectKeyOptions}.\n * @returns The top-ranked key, or null when nothing matches.\n * @example\n * ```ts\n * import { Key } from '@libraz/libcantus';\n * Key.detectBest([0, 2, 4, 5, 7, 9, 11])?.toString(); // 'C major'\n * ```\n */\n static detectBest(pitches: readonly number[], opts?: DetectKeyOptions): Key | null {\n const best = detectKeyBest(pitches, opts);\n return best === null ? null : Key.of(best.key);\n }\n\n /**\n * Whether another key has the same tonic pitch class and mode.\n *\n * The spelled tonic is not compared: C# major and Db major are the same key\n * written two ways.\n *\n * @param other The key to compare.\n * @returns True when tonic and mode mask both match.\n */\n equals(other: Key): boolean {\n const scale = other.scale;\n return this.#scale.rootPc === scale.rootPc && this.#scale.modeMask12 === scale.modeMask12;\n }\n\n /** A copy of the underlying plain `KeyScale`. */\n get scale(): KeyScale {\n return { rootPc: this.#scale.rootPc, modeMask12: this.#scale.modeMask12 };\n }\n\n /** The spelled tonic. */\n get tonic(): Note {\n return this.#tonic;\n }\n\n /** The tonic pitch class (0..11). */\n get rootPc(): number {\n return this.#scale.rootPc;\n }\n\n /** Whether the scale has a minor third and no major third. */\n get isMinor(): boolean {\n return isMinorKey(this.#scale);\n }\n\n /**\n * The scale's pitch classes in ascending scale-degree order (degree 0 first).\n *\n * @returns One pitch class per scale degree.\n */\n pitchClasses(): number[] {\n return scaleTonesInDegreeOrder(this.#scale);\n }\n\n /**\n * Transpose the key by a number of semitones.\n *\n * The mode mask is unchanged, so the scale keeps its shape; only the tonic\n * moves. The new tonic is spelled the way that key is conventionally written\n * — D major, not C## major.\n *\n * @param semitones The signed semitone offset.\n * @returns The transposed key.\n * @example\n * ```ts\n * import { Key } from '@libraz/libcantus';\n * Key.major('C').transpose(2).toString(); // 'D major'\n * ```\n */\n transpose(semitones: number): Key {\n assertFiniteNumber(semitones, 'semitones');\n const rootPc = mod12(this.#scale.rootPc + Math.round(semitones));\n return Key.of({ rootPc, modeMask12: this.#scale.modeMask12 });\n }\n\n /**\n * The spelled scale, one note per degree (e.g. C D E F G A B for C major).\n *\n * @returns Spelled octave-less notes in scale-degree order.\n */\n notes(): Note[] {\n return spellScale(this.#tonic.data, this.#scale).map((note) => new Note(note));\n }\n\n /**\n * Alias of {@link Key.notes}.\n *\n * @returns Spelled octave-less notes in scale-degree order.\n */\n spell(): Note[] {\n return this.notes();\n }\n\n /**\n * The spelled scale as letter-name strings.\n *\n * @returns One name per scale degree.\n */\n noteNames(): string[] {\n return this.notes().map((note) => note.name);\n }\n\n /**\n * Build a chord on a scale degree, carrying this key as context.\n *\n * With an explicit quality the quality's interval template is attached to the\n * degree's diatonic root; without one the scale-correct diatonic triad is\n * stacked (e.g. a diminished triad on the leading tone of a major key).\n *\n * @param degree 0-based scale degree of the chord root.\n * @param quality Optional chord quality.\n * @returns The chord, with this key attached.\n * @throws Without a `quality`, if this key's scale is not heptatonic —\n * stacking thirds needs seven degrees, so the pentatonic, blues,\n * whole-tone, octatonic and chromatic scales have no diatonic triad. Pass\n * an explicit `quality` for those.\n */\n chord(degree: number, quality?: ChordQuality): Chord {\n const data =\n quality === undefined\n ? diatonicTriad(degree, this.#scale)\n : chordFromDegree(degree, quality, this.#scale);\n return new Chord(data, this);\n }\n\n /**\n * The diatonic triad on a scale degree, carrying this key as context.\n *\n * @param degree 0-based scale degree of the chord root.\n * @returns The triad, with this key attached.\n * @throws If this key's scale is not heptatonic; stacking thirds needs seven\n * degrees. Use {@link Key.chord} with an explicit quality instead.\n */\n diatonicTriad(degree: number): Chord {\n return new Chord(diatonicTriad(degree, this.#scale), this);\n }\n\n /**\n * The diatonic seventh chord on a scale degree, carrying this key as context.\n *\n * @param degree 0-based scale degree of the chord root.\n * @returns The seventh chord, with this key attached.\n * @throws If this key's scale is not heptatonic; stacking thirds needs seven\n * degrees. Use {@link Key.chord} with an explicit quality instead.\n */\n diatonicSeventh(degree: number): Chord {\n return new Chord(diatonicSeventh(degree, this.#scale), this);\n }\n\n /**\n * Build the chord denoted by a Roman numeral in this key (including applied\n * chords such as `'V7/V'`), carrying this key as context.\n *\n * @param text The Roman numeral.\n * @returns The chord, with this key attached.\n * @throws If the numeral is not valid.\n */\n roman(text: string): Chord {\n return new Chord(romanToChord(text, this.#scale), this);\n }\n\n /**\n * Whether a pitch belongs to the scale.\n *\n * @param x A MIDI pitch, bare pitch class, or note.\n * @returns True if the pitch class is a scale tone.\n */\n contains(x: number | Note): boolean {\n return isScaleTone(typeof x === 'number' ? x : x.pitchClass, this.#scale);\n }\n\n /**\n * The plain key data, for JSON serialization.\n *\n * Private class fields do not serialize, so an explicit `toJSON` keeps\n * `JSON.stringify(key)` from collapsing to `{}`. The result pairs the\n * `KeyScale` with the spelled tonic, enough to reconstruct the key via\n * {@link Key.of}.\n *\n * @returns The key/scale and its spelled tonic.\n */\n toJSON(): { scale: KeyScale; tonic: NoteData } {\n return { scale: this.scale, tonic: this.#tonic.data };\n }\n\n /**\n * The key's tonic and mode, so a template literal or a log line reads as the\n * key. Only the major/minor distinction is named, since a mask does not carry\n * a scale name.\n *\n * @returns The name, e.g. `'C major'` or `'A minor'`.\n */\n toString(): string {\n return `${this.#tonic.name} ${this.isMinor ? 'minor' : 'major'}`;\n }\n}\n","import {\n type Cadence,\n type ChordAnalysis,\n detectCadence,\n type HarmonicFunction,\n} from '../analyze/functional/index.js';\nimport { InvalidInputError } from '../core/errors/index.js';\nimport type { Note as NoteData } from '../core/pitch/index.js';\nimport type { KeyScale } from '../core/types.js';\nimport type { Chord as ChordData, ChordSpan } from '../theory/chord/index.js';\nimport { makeChord } from '../theory/chord/index.js';\nimport { type VoicingOptions, voiceProgression } from '../theory/voicing/index.js';\nimport type { Chord } from './chord.js';\nimport { Chord as ChordClass } from './chord.js';\nimport type { Key } from './key.js';\nimport { Key as KeyClass } from './key.js';\n\n/**\n * An immutable ordered sequence of chords, optionally carrying a {@link Key}\n * context shared by its analysis methods.\n *\n * @category Class API\n * @example\n * ```ts\n * import { Key } from '@libraz/libcantus';\n * const key = Key.major('C');\n * key.chord(1).progressionTo(key.chord(4), key.chord(0)).roman();\n * // ['ii', 'V', 'I']\n * ```\n */\nexport class Progression {\n readonly #chords: readonly Chord[];\n readonly #key: Key | undefined;\n\n /**\n * Wrap a chord sequence.\n *\n * @param chords The chords in order; the array is copied.\n * @param key Optional key context for analysis methods.\n */\n constructor(chords: readonly Chord[], key?: Key) {\n this.#chords = Object.freeze([...chords]);\n this.#key = key;\n }\n\n /**\n * Build a progression from the {@link ChordSpan} records the generators\n * return, keeping their order.\n *\n * The spans' `startBeat`, `degree`, and `secondaryDominant` are analysis\n * annotations that a chord sequence does not carry; only the harmony crosses\n * over. Keep the spans themselves if the timing matters.\n *\n * @param spans The chord spans, in order.\n * @param key Optional key context for the analysis methods.\n * @returns The progression.\n * @example\n * ```ts\n * import { generateProgression, Key, Progression } from '@libraz/libcantus';\n * const key = Key.major('C');\n * const spans = generateProgression({ key: key.scale, style: 'dance', bars: 4 });\n * Progression.fromSpans(spans, key).roman();\n * ```\n */\n static fromSpans(spans: readonly ChordSpan[], key?: Key): Progression {\n const chords = spans.map((span) => {\n const chord = ChordClass.from(makeChord(span.rootPc, span.quality, span.bassPc));\n return key === undefined ? chord : chord.withKey(key);\n });\n return key === undefined ? new Progression(chords) : new Progression(chords, key);\n }\n\n /**\n * Rebuild a progression from its {@link Progression.toJSON} output.\n *\n * @param data The serialized chords and key.\n * @returns The progression.\n */\n static fromJSON(data: {\n chords: ChordData[];\n key?: { scale: KeyScale; tonic: NoteData } | undefined;\n }): Progression {\n const key = data.key === undefined ? undefined : KeyClass.fromJSON(data.key);\n const chords = data.chords.map((chord) => ChordClass.fromJSON(chord));\n return key === undefined ? new Progression(chords) : new Progression(chords, key);\n }\n\n /**\n * The chord sequence.\n *\n * The array is the progression's own and is frozen rather than copied, so\n * reading it in a loop stays linear.\n */\n get chords(): readonly Chord[] {\n return this.#chords;\n }\n\n /** The carried key context, if any. */\n get key(): Key | undefined {\n return this.#key;\n }\n\n /** The number of chords. */\n get length(): number {\n return this.#chords.length;\n }\n\n /**\n * The chord at an index.\n *\n * @param index 0-based position; a negative index counts from the end.\n * @returns The chord, or undefined when the index is out of range.\n */\n at(index: number): Chord | undefined {\n return this.#chords.at(index);\n }\n\n /** Iterate the chords in order, so a progression works with `for...of`. */\n [Symbol.iterator](): IterableIterator<Chord> {\n return this.#chords[Symbol.iterator]();\n }\n\n /**\n * Whether another progression holds the same chords in the same order.\n *\n * The key context is not compared: it is an analysis lens, not part of the\n * harmony. Chord equality follows {@link Chord.equals}.\n *\n * @param other The progression to compare.\n * @returns True when the chord sequences match.\n */\n equals(other: Progression): boolean {\n if (this.#chords.length !== other.length) {\n return false;\n }\n return this.#chords.every((chord, index) => {\n const theirs = other.at(index);\n return theirs !== undefined && chord.equals(theirs);\n });\n }\n\n /**\n * A copy of this progression with a chord appended.\n *\n * @param chord The chord to append.\n * @returns The new progression.\n */\n add(chord: Chord): Progression {\n return new Progression([...this.#chords, chord], this.#key);\n }\n\n /**\n * A copy of this progression carrying the given key context.\n *\n * @param key The key context to attach.\n * @returns The new progression.\n */\n withKey(key: Key): Progression {\n return new Progression(this.#chords, key);\n }\n\n /**\n * Voice the progression with smooth voice leading.\n *\n * @param opts Voicing options; defaults to four SATB voices.\n * @returns One ascending voicing (MIDI pitches) per chord.\n * @throws If any chord admits no voicing within the given ranges.\n */\n voice(opts?: VoicingOptions): number[][] {\n return voiceProgression(\n this.#chords.map((chord) => chord.data),\n opts,\n );\n }\n\n /**\n * The Roman numeral of each chord in a key.\n *\n * @param key Key to analyze in; falls back to the carried context.\n * @returns One numeral per chord.\n * @throws If no key is given and none is carried.\n */\n roman(key?: Key): string[] {\n const resolved = this.#resolveKey(key);\n return this.#chords.map((chord) => chord.roman(resolved));\n }\n\n /**\n * The harmonic function of each chord in a key.\n *\n * @param key Key to analyze in; falls back to the carried context.\n * @returns One function per chord.\n * @throws If no key is given and none is carried.\n */\n functions(key?: Key): HarmonicFunction[] {\n const resolved = this.#resolveKey(key);\n return this.#chords.map((chord) => chord.function(resolved));\n }\n\n /**\n * Analyze every chord and classify the closing cadence.\n *\n * The cadence is detected on the final chord pair and is null when the\n * progression has fewer than two chords.\n *\n * @param key Key to analyze in; falls back to the carried context.\n * @returns Per-chord analyses and the closing cadence.\n * @throws If no key is given and none is carried.\n */\n analyze(key?: Key): { chords: ChordAnalysis[]; cadence: Cadence } {\n const resolved = this.#resolveKey(key);\n const chords = this.#chords.map((chord) => chord.analyze(resolved));\n const from = this.#chords[this.#chords.length - 2];\n const to = this.#chords[this.#chords.length - 1];\n const cadence =\n from !== undefined && to !== undefined\n ? detectCadence(from.data, to.data, resolved.scale)\n : null;\n return { chords, cadence };\n }\n\n /**\n * The plain progression data, for JSON serialization.\n *\n * Private class fields do not serialize, so an explicit `toJSON` keeps\n * `JSON.stringify(progression)` from collapsing to `{}`. The chords are\n * emitted as plain data and the carried key, when present, as its own data.\n *\n * @returns The chord data sequence and the carried key, if any.\n */\n /**\n * Transpose every chord by a number of semitones.\n *\n * A carried key moves with the chords, so the progression keeps its degrees\n * and functions in the new key.\n *\n * @param semitones The signed semitone offset.\n * @returns The transposed progression.\n * @example\n * ```ts\n * import { Chord, Progression } from '@libraz/libcantus';\n * new Progression([Chord.parse('C'), Chord.parse('G')]).transpose(2).toString(); // 'D A'\n * ```\n */\n transpose(semitones: number): Progression {\n const key = this.#key?.transpose(semitones);\n const chords = this.#chords.map((chord) => {\n const moved = chord.transpose(semitones);\n // A chord that carried no key of its own is spelled by this progression's\n // key, so it has to receive the transposed one or it would fall back to\n // sharps in a flat key.\n return key !== undefined && moved.key === undefined ? moved.withKey(key) : moved;\n });\n return key === undefined ? new Progression(chords) : new Progression(chords, key);\n }\n\n /**\n * The chord symbols separated by spaces, so a template literal or a log line\n * reads as the progression.\n *\n * @returns The symbols in order, e.g. `'C Am F G'`.\n */\n toString(): string {\n return this.#chords.map((chord) => chord.symbol()).join(' ');\n }\n\n toJSON(): { chords: ChordData[]; key: { scale: KeyScale; tonic: NoteData } | undefined } {\n return {\n chords: this.#chords.map((chord) => chord.data),\n key: this.#key?.toJSON(),\n };\n }\n\n /** Resolve the key for an analysis method: explicit first, then carried. */\n #resolveKey(key?: Key): Key {\n const resolved = key ?? this.#key;\n if (resolved === undefined) {\n throw new InvalidInputError(\n 'progression has no key context; pass a Key or attach one with withKey()',\n );\n }\n return resolved;\n }\n}\n","import type { ChordMatch, DetectChordOptions } from '../analyze/detect/index.js';\nimport { detectChord, detectChordBest } from '../analyze/detect/index.js';\nimport {\n analyzeChord,\n type BorrowedSource,\n borrowedSource,\n type ChordAnalysis,\n chordToRoman,\n functionOf,\n type HarmonicFunction,\n isBorrowedChord,\n} from '../analyze/functional/index.js';\nimport { InvalidInputError } from '../core/errors/index.js';\nimport type { Note as NoteData } from '../core/pitch/index.js';\nimport { noteToPitchClass } from '../core/pitch/index.js';\nimport { negativeHarmonyMirror } from '../generate/reharmony/index.js';\nimport {\n type Chord as ChordData,\n type ChordQuality,\n chordPitchClasses,\n makeChord,\n type PitchSpelling,\n transposeChord,\n} from '../theory/chord/index.js';\nimport {\n availableTensions,\n avoidNotes,\n type ChordScaleMatch,\n chordScales,\n} from '../theory/chordscale/index.js';\nimport type { ScaleNameInput } from '../theory/scale/index.js';\nimport { spellChord, spellChordFromRoot, spellPitchClass } from '../theory/spelling/index.js';\nimport { formatChordSymbol, parseChordSymbol } from '../theory/symbol/index.js';\nimport {\n type StyledVoicingOptions,\n type VoicingOptions,\n voiceChord,\n voiceChordStyled,\n} from '../theory/voicing/index.js';\nimport type { Key } from './key.js';\nimport { Note } from './note.js';\nimport { Progression } from './progression.js';\nimport { mod12 } from './shared.js';\n\n/** Whether a spelling hint still names the pitch class it is attached to. */\nfunction hintMatches(hint: PitchSpelling | undefined, pc: number | undefined): boolean {\n return hint !== undefined && pc !== undefined && mod12(noteToPitchClass(hint)) === mod12(pc);\n}\n\n/**\n * Defensive copy of a plain chord.\n *\n * Enharmonic spelling hints (`rootSpelling`/`bassSpelling`, populated by\n * `parseChordSymbol`) are carried through so a flat-named chord round-trips\n * through the class API — but only while they still name their own pitch\n * class. A stale hint is dropped here rather than passed on, so that everything\n * downstream, including the derived bass spelling, reads a hint it can trust.\n */\nfunction copyChord(data: ChordData): ChordData {\n const copy: ChordData = {\n rootPc: data.rootPc,\n quality: data.quality,\n intervals: [...data.intervals],\n };\n if (data.bassPc !== undefined) {\n copy.bassPc = data.bassPc;\n }\n if (hintMatches(data.rootSpelling, data.rootPc) && data.rootSpelling !== undefined) {\n copy.rootSpelling = { letter: data.rootSpelling.letter, alter: data.rootSpelling.alter };\n }\n if (hintMatches(data.bassSpelling, data.bassPc) && data.bassSpelling !== undefined) {\n copy.bassSpelling = { letter: data.bassSpelling.letter, alter: data.bassSpelling.alter };\n }\n return copy;\n}\n\n/**\n * Spell the slash bass of a chord whose root spelling is already settled.\n *\n * A bass that is one of the chord's own tones takes that tone's spelling, so\n * `Eb/Bb` never renders as `Eb/A#`; a bass outside the chord is spelled by the\n * key when one is available.\n */\nfunction deriveBassSpelling(chord: ChordData, key: Key | undefined): PitchSpelling | undefined {\n const bassPc = chord.bassPc;\n if (bassPc === undefined) {\n return undefined;\n }\n const root = chord.rootSpelling;\n if (root !== undefined) {\n const index = chord.intervals.findIndex((i) => mod12(chord.rootPc + i) === bassPc);\n const tone = index >= 0 ? spellChordFromRoot(chord, root)[index] : undefined;\n if (tone !== undefined) {\n return { letter: tone.letter, alter: tone.alter };\n }\n }\n if (key === undefined) {\n return undefined;\n }\n const spelled = spellPitchClass(bassPc, key.tonic.data, key.scale);\n return { letter: spelled.letter, alter: spelled.alter };\n}\n\n/**\n * An immutable chord: a root pitch class, quality, interval template, and\n * optional slash bass, optionally carrying a {@link Key} context. Analysis\n * methods (`roman`, `function`, `analyze`, ...) use an explicitly passed key\n * first and fall back to the carried context.\n *\n * @category Class API\n * @example\n * ```ts\n * import { Chord } from '@libraz/libcantus';\n * Chord.parse('Cmaj7').invert(1).symbol(); // 'Cmaj7/E' (third in the bass)\n * ```\n */\nexport class Chord {\n /** Exactly what the caller supplied: any spelling here is the caller's own. */\n readonly #given: ChordData;\n readonly #key: Key | undefined;\n\n /**\n * Wrap a plain chord object.\n *\n * @param data The chord; it is copied, never retained or mutated.\n * @param key Optional key context for analysis methods.\n */\n constructor(data: ChordData, key?: Key) {\n this.#given = copyChord(data);\n this.#key = key;\n }\n\n /**\n * The chord data with any missing spelling hint filled in from the key.\n *\n * Derived spellings are computed on read rather than baked in at construction,\n * so re-attaching a different key re-spells the chord instead of carrying the\n * first key's letters forever. A spelling the caller supplied always wins.\n */\n get #data(): ChordData {\n const out = copyChord(this.#given);\n const key = this.#key;\n if (out.rootSpelling === undefined && key !== undefined) {\n out.rootSpelling = spellPitchClass(out.rootPc, key.tonic.data, key.scale);\n }\n if (out.bassSpelling === undefined) {\n const bass = deriveBassSpelling(out, key);\n if (bass !== undefined) {\n out.bassSpelling = bass;\n }\n }\n return out;\n }\n\n /**\n * Build a chord from a root and quality.\n *\n * @param root Root as a note name (e.g. `'Eb'`) or a pitch class.\n * @param quality The chord quality.\n * @param bass Optional slash-chord bass, as a note name (e.g. `'Bb'`) or a\n * pitch class. A named bass keeps its own spelling.\n * @returns The chord (without key context).\n */\n static of(root: string | number, quality: ChordQuality, bass?: string | number): Chord {\n const bassNote = typeof bass === 'string' ? Note.of(bass) : undefined;\n const bassPc = bassNote !== undefined ? bassNote.pitchClass : (bass as number | undefined);\n const rootNote = typeof root === 'string' ? Note.of(root) : undefined;\n const data = makeChord(\n rootNote !== undefined ? rootNote.pitchClass : (root as number),\n quality,\n bassPc,\n );\n if (rootNote !== undefined) {\n data.rootSpelling = { letter: rootNote.letter, alter: rootNote.alter };\n }\n if (bassNote !== undefined) {\n data.bassSpelling = { letter: bassNote.letter, alter: bassNote.alter };\n }\n return new Chord(data);\n }\n\n /**\n * Wrap an existing plain chord object.\n *\n * @param data The plain chord.\n * @returns The wrapped chord (without key context).\n */\n static from(data: ChordData): Chord {\n return new Chord(data);\n }\n\n /**\n * Alias of {@link Chord.from}, matching the `fromData` factory on the other\n * classes.\n *\n * @param data The plain chord.\n * @returns The wrapped chord (without key context).\n */\n static fromData(data: ChordData): Chord {\n return new Chord(data);\n }\n\n /**\n * Rebuild a chord from its {@link Chord.toJSON} output.\n *\n * The key context is not serialized, so the result carries none; re-attach\n * one with {@link Chord.withKey}.\n *\n * @param data The serialized chord.\n * @returns The wrapped chord.\n */\n static fromJSON(data: ChordData): Chord {\n return new Chord(data);\n }\n\n /**\n * Parse a lead-sheet chord symbol (e.g. `'Cmaj7'`, `'F#m7b5'`, `'C/G'`).\n *\n * @param symbol The chord symbol.\n * @returns The chord (without key context).\n * @throws If the root or quality is not recognized.\n */\n static parse(symbol: string): Chord {\n return new Chord(parseChordSymbol(symbol));\n }\n\n /**\n * Identify the chords matching a set of pitches, best interpretation first.\n *\n * @param pitches MIDI pitches or bare pitch classes.\n * @param opts How to interpret the input; see {@link DetectChordOptions}.\n * @returns Ranked chord interpretations (may be empty).\n */\n static detect(pitches: readonly number[], opts?: DetectChordOptions): Chord[] {\n return detectChord(pitches, opts).map(\n (match) => new Chord(makeChord(match.rootPc, match.quality, match.bassPc)),\n );\n }\n\n /**\n * Identify the chords matching a set of pitches, keeping each match's\n * recognition metadata beside the chord.\n *\n * {@link Chord.detect} discards the confidence signals a recognition UI needs\n * — whether the set matched exactly, which chord tones were missing, which\n * input notes were foreign, and which inversion the bass implies.\n *\n * @param pitches MIDI pitches or bare pitch classes.\n * @param opts How to interpret the input; see {@link DetectChordOptions}.\n * @returns Ranked interpretations, each with its chord and its match record.\n * @example\n * ```ts\n * import { Chord } from '@libraz/libcantus';\n * const [best] = Chord.detectMatches([60, 64, 67]);\n * best?.match.exact; // true\n * ```\n */\n static detectMatches(\n pitches: readonly number[],\n opts?: DetectChordOptions,\n ): { chord: Chord; match: ChordMatch }[] {\n return detectChord(pitches, opts).map((match) => ({\n chord: new Chord(makeChord(match.rootPc, match.quality, match.bassPc)),\n match,\n }));\n }\n\n /**\n * The single best chord interpretation of a pitch set.\n *\n * @param pitches MIDI pitches or bare pitch classes.\n * @param opts How to interpret the input; see {@link DetectChordOptions}.\n * @returns The top-ranked chord, or null when nothing matches.\n */\n static detectBest(pitches: readonly number[], opts?: DetectChordOptions): Chord | null {\n const best = detectChordBest(pitches, opts);\n return best === null ? null : new Chord(best);\n }\n\n /** The root pitch class (0..11). */\n get rootPc(): number {\n return this.#data.rootPc;\n }\n\n /** The chord quality. */\n get quality(): ChordQuality {\n return this.#data.quality;\n }\n\n /** A copy of the semitone offsets above the root. */\n get intervals(): number[] {\n return [...this.#data.intervals];\n }\n\n /** The slash-chord bass pitch class, or undefined in root position. */\n get bassPc(): number | undefined {\n return this.#data.bassPc;\n }\n\n /**\n * A copy of the underlying plain chord object, including the spellings\n * derived from any attached key.\n */\n get data(): ChordData {\n return this.#data;\n }\n\n /** The carried key context, if any. */\n get key(): Key | undefined {\n return this.#key;\n }\n\n /**\n * A copy of this chord carrying the given key context.\n *\n * Only a spelling the caller supplied (via `Chord.parse`, `Chord.of` with a\n * named root, or plain data carrying a hint) survives; a spelling that came\n * from a previously attached key is re-derived, so re-keying a progression\n * during a modulation does not keep the old key's letters and the order of\n * `withKey` calls does not affect the result.\n *\n * @param key The key context to attach.\n * @returns The new chord.\n */\n withKey(key: Key): Chord {\n return new Chord(this.#given, key);\n }\n\n /**\n * The chord's sorted, deduplicated pitch classes.\n *\n * @returns Pitch classes ascending in [0, 11].\n */\n pitchClasses(): number[] {\n return chordPitchClasses(this.#data);\n }\n\n /**\n * The chord's Roman numeral in a key.\n *\n * @param key Key to analyze in; falls back to the carried context.\n * @returns The Roman numeral string.\n * @throws If no key is given and none is carried.\n */\n roman(key?: Key): string {\n return chordToRoman(this.#data, this.#resolveKey(key).scale);\n }\n\n /**\n * The chord's harmonic function (tonic / subdominant / dominant) in a key.\n *\n * @param key Key to analyze in; falls back to the carried context.\n * @returns The harmonic function.\n * @throws If no key is given and none is carried.\n */\n function(key?: Key): HarmonicFunction {\n return functionOf(this.#data, this.#resolveKey(key).scale);\n }\n\n /**\n * Full functional analysis: function, borrowing, and Roman numeral.\n *\n * @param key Key to analyze in; falls back to the carried context.\n * @returns The chord analysis.\n * @throws If no key is given and none is carried.\n */\n analyze(key?: Key): ChordAnalysis {\n return analyzeChord(this.#data, this.#resolveKey(key).scale);\n }\n\n /**\n * Whether the chord is borrowed from the parallel mode (modal interchange).\n *\n * @param key Key to analyze in; falls back to the carried context.\n * @returns True if the chord is borrowed.\n * @throws If no key is given and none is carried.\n */\n isBorrowed(key?: Key): boolean {\n return isBorrowedChord(this.#data, this.#resolveKey(key).scale);\n }\n\n /**\n * The origin of a non-diatonic chord (parallel mode or Neapolitan), or null.\n *\n * @param key Key to analyze in; falls back to the carried context.\n * @returns The borrowing source, or null.\n * @throws If no key is given and none is carried.\n */\n borrowedSource(key?: Key): BorrowedSource {\n return borrowedSource(this.#data, this.#resolveKey(key).scale);\n }\n\n /**\n * The chord rendered as a lead-sheet symbol (e.g. `'Cmaj7'`, `'F#m7'`, `'C/G'`).\n *\n * @param opts Set `flats: true` to spell the root/bass with flats.\n * @returns The chord symbol.\n */\n symbol(opts?: { flats?: boolean }): string {\n return formatChordSymbol(this.#data, opts);\n }\n\n /**\n * Realize the chord as one MIDI pitch per voice, ascending.\n *\n * A chord that carries a key passes it to the voicer, so the leading tone is\n * not doubled; an explicit `opts.key` overrides it.\n *\n * @param opts Voicing options; defaults to four SATB voices.\n * @returns MIDI pitches, ascending, one per voice.\n * @throws If no voicing fits the given ranges.\n */\n voice(opts?: VoicingOptions): number[] {\n const key = opts?.key ?? this.#key?.scale;\n return voiceChord(this.#data, key === undefined ? opts : { ...opts, key });\n }\n\n /**\n * Realize the chord as a single styled voicing (`close`, `drop2`, `drop3`,\n * `shell`, or `rootless`), optionally constraining the top voice.\n *\n * @param opts Styled-voicing options; defaults to a close-position voicing.\n * @returns MIDI pitches, ascending.\n */\n styledVoicing(opts?: StyledVoicingOptions): number[] {\n return voiceChordStyled(this.#data, opts);\n }\n\n /**\n * The negative-harmony mirror of the chord about the key's tonic–dominant\n * axis (major becomes minor and vice versa).\n *\n * @param key Key providing the reflection axis; falls back to the carried\n * context.\n * @returns The mirrored chord, keeping any key context.\n * @throws If no key is given and none is carried.\n */\n negativeHarmony(key?: Key): Chord {\n const resolved = this.#resolveKey(key);\n // Retain the key that anchored the reflection (explicit first, then carried)\n // so a later no-arg analysis method still has a key context.\n return new Chord(negativeHarmonyMirror(this.#data, resolved.scale), key ?? this.#key);\n }\n\n /**\n * The n-th inversion: a copy whose bass is the chord tone `n` steps above the\n * root in the interval template (`invert(1)` puts the third in the bass).\n * `n` wraps around the template length; negative values count backwards.\n *\n * `invert(0)` (and any `n` that wraps to it) is root position, so it carries\n * no slash bass and equals the original chord.\n *\n * @param n The inversion number.\n * @returns The inverted chord, keeping any key context.\n * @throws If the chord has no intervals.\n */\n invert(n: number): Chord {\n const data = copyChord(this.#given);\n const intervals = data.intervals;\n const length = intervals.length;\n if (length === 0) {\n throw new InvalidInputError('cannot invert a chord with no intervals');\n }\n const index = ((n % length) + length) % length;\n // The bass spelling follows from the root spelling in force at read time, so\n // it is derived rather than frozen in here.\n delete data.bassSpelling;\n if (index === 0) {\n delete data.bassPc;\n } else {\n data.bassPc = mod12(data.rootPc + (intervals[index] ?? 0));\n }\n return new Chord(data, this.#key);\n }\n\n /**\n * Transpose the chord by a number of semitones.\n *\n * The quality and interval template are carried over, so a chord that a\n * symbol round-trip could not express — a custom interval set, an inversion —\n * survives. A carried key moves with the chord, so the transposed chord keeps\n * the same degree and function inside the transposed key.\n *\n * @param semitones The signed semitone offset.\n * @returns The transposed chord, in the transposed key when one is carried.\n * @example\n * ```ts\n * import { Chord } from '@libraz/libcantus';\n * Chord.parse('C/G').transpose(2).symbol(); // 'D/A'\n * ```\n */\n transpose(semitones: number): Chord {\n const moved = transposeChord(this.#given, semitones);\n return new Chord(moved, this.#key?.transpose(semitones));\n }\n\n /**\n * The named scales that fit over this chord, best fit first, rooted on the\n * chord root.\n *\n * @returns The matching scales.\n */\n scales(): ChordScaleMatch[] {\n return chordScales(this.#data);\n }\n\n /**\n * The available tensions (usable non-chord, non-avoid scale tones) of a\n * scale over this chord.\n *\n * @param scaleName A named scale, rooted on the chord root.\n * @returns Tension pitch classes, ascending in [0, 11].\n * @throws If `scaleName` is not a built-in scale.\n */\n tensions(scaleName: ScaleNameInput): number[] {\n return availableTensions(this.#data, scaleName);\n }\n\n /**\n * The avoid notes (scale tones a semitone above a chord tone) of a scale\n * over this chord.\n *\n * @param scaleName A named scale, rooted on the chord root.\n * @returns Avoid-note pitch classes, ascending in [0, 11].\n * @throws If `scaleName` is not a built-in scale.\n */\n avoidNotes(scaleName: ScaleNameInput): number[] {\n return avoidNotes(this.#data, scaleName);\n }\n\n /**\n * Spell the chord tones with letter names, root first, in the key's spelling.\n *\n * A chord that already knows how its root is spelled — one from\n * `Chord.parse`, or `Chord.of` with a named root — can spell itself with no\n * key at all; a key is only needed to choose a spelling for a bare pitch\n * class.\n *\n * A slash bass outside the chord tones is appended, so the spelled notes\n * are the notes the chord sounds — the same set {@link Chord.pitchClasses}\n * reports.\n *\n * @param key Key providing the spelled tonic; falls back to the carried\n * context, then to the chord's own root spelling.\n * @returns Spelled octave-less notes in the chord's own (tertian) order,\n * with any foreign slash bass last.\n * @throws If no key is given, none is carried, and the chord has no root\n * spelling of its own.\n */\n spell(key?: Key): Note[] {\n const data = this.#data;\n const resolved = key ?? this.#key;\n let tones: NoteData[];\n if (resolved === undefined) {\n const root = data.rootSpelling;\n if (root === undefined) {\n throw new InvalidInputError(\n 'chord has no key context and no root spelling; pass a Key, attach one with withKey(), or build the chord from a symbol',\n );\n }\n tones = spellChordFromRoot(data, root);\n } else {\n tones = spellChord(data, resolved.tonic.data, resolved.scale);\n }\n const bassPc = data.bassPc;\n const bass = data.bassSpelling;\n if (\n bassPc !== undefined &&\n bass !== undefined &&\n !tones.some((tone) => mod12(noteToPitchClass(tone)) === mod12(bassPc))\n ) {\n tones = [...tones, { letter: bass.letter, alter: bass.alter }];\n }\n return tones.map((note) => new Note(note));\n }\n\n /**\n * Start a progression with this chord followed by others, carrying this\n * chord's key context (if any).\n *\n * @param others The chords following this one.\n * @returns The progression.\n */\n progressionTo(...others: Chord[]): Progression {\n return new Progression([this, ...others], this.#key);\n }\n\n /**\n * Whether another chord has the same root, quality, intervals, and bass.\n * Key context is not compared.\n *\n * @param other The chord to compare.\n * @returns True if the chord data is identical.\n */\n equals(other: Chord): boolean {\n const a = this.#data;\n const b = other.data;\n return (\n a.rootPc === b.rootPc &&\n a.quality === b.quality &&\n a.bassPc === b.bassPc &&\n a.intervals.length === b.intervals.length &&\n a.intervals.every((interval, i) => interval === b.intervals[i])\n );\n }\n\n /**\n * The plain chord data, for JSON serialization.\n *\n * @returns A copy of the underlying plain chord object.\n */\n toJSON(): ChordData {\n return this.data;\n }\n\n /**\n * The chord symbol, so a template literal or a log line reads as the chord.\n *\n * @returns The symbol, e.g. `'Cmaj7'`.\n */\n toString(): string {\n return this.symbol();\n }\n\n /** Resolve the key for an analysis method: explicit first, then carried. */\n #resolveKey(key?: Key): Key {\n const resolved = key ?? this.#key;\n if (resolved === undefined) {\n throw new InvalidInputError(\n 'chord has no key context; pass a Key or attach one with withKey()',\n );\n }\n return resolved;\n }\n}\n"]}
@@ -1,16 +1,18 @@
1
1
  import {
2
- chordPitchClasses,
3
2
  createsHiddenParallelPerfect,
4
3
  createsParallelPerfect,
5
4
  createsVerticalDissonance,
5
+ intervalAboveRoot,
6
+ isChordMember,
6
7
  isForbiddenMelodicLeap,
7
8
  isScaleTone
8
- } from "./chunk-MIWO425M.js";
9
+ } from "./chunk-D2FAJBYL.js";
9
10
  import {
11
+ InvalidInputError,
10
12
  assertFiniteNumber,
11
13
  assertGenerationBudget,
12
14
  assertInteger
13
- } from "./chunk-FBQK3ZQN.js";
15
+ } from "./chunk-XEBG3G53.js";
14
16
 
15
17
  // src/theory/safety/index.ts
16
18
  var NoteSafety = /* @__PURE__ */ ((NoteSafety2) => {
@@ -65,21 +67,19 @@ function minimumSafetyForReasons(reasons, profile) {
65
67
  }
66
68
  return minimum;
67
69
  }
68
- function pitchClass(pitch) {
69
- return (Math.trunc(pitch) % 12 + 12) % 12;
70
- }
71
- function intervalAboveRoot(pitch, chord) {
72
- return ((pitchClass(pitch) - pitchClass(chord.rootPc)) % 12 + 12) % 12;
73
- }
74
- function isChordMember(pitch, chord) {
75
- return chordPitchClasses(chord).includes(pitchClass(pitch));
76
- }
77
70
  function avoidsFourth(chord) {
78
71
  return chord.intervals.includes(4) && !chord.intervals.includes(5);
79
72
  }
80
73
  function avoidsMajorSeventh(chord) {
81
74
  return chord.intervals.includes(4) && chord.intervals.includes(10);
82
75
  }
76
+ function isStructuralChordDissonance(a, b, chord) {
77
+ const cls = Math.abs(Math.trunc(a) - Math.trunc(b)) % 12;
78
+ if (cls !== 6 && cls !== 10 && cls !== 11) {
79
+ return false;
80
+ }
81
+ return isChordMember(a, chord) && isChordMember(b, chord);
82
+ }
83
83
  function stepResolution(pitch, chord) {
84
84
  for (let delta = 1; delta <= 2; delta += 1) {
85
85
  if (isChordMember(pitch - delta, chord)) {
@@ -88,17 +88,17 @@ function stepResolution(pitch, chord) {
88
88
  }
89
89
  return void 0;
90
90
  }
91
- function evaluateSafety(q) {
91
+ function evaluateSafety(q, opts = {}) {
92
92
  assertSafetyContext(q);
93
93
  assertFiniteNumber(q.candidatePitch, "candidatePitch");
94
- return evaluateInternal(q, true);
94
+ return evaluateInternal(q, opts.suggestions ?? true);
95
95
  }
96
96
  function assertSafetyContext(q) {
97
97
  if (q.prevPitch !== void 0) assertFiniteNumber(q.prevPitch, "prevPitch");
98
98
  if (q.vocalLow !== void 0) assertFiniteNumber(q.vocalLow, "vocalLow");
99
99
  if (q.vocalHigh !== void 0) assertFiniteNumber(q.vocalHigh, "vocalHigh");
100
100
  if (q.vocalLow !== void 0 && q.vocalHigh !== void 0 && q.vocalLow > q.vocalHigh) {
101
- throw new RangeError("vocalLow must not exceed vocalHigh");
101
+ throw new InvalidInputError("vocalLow must not exceed vocalHigh");
102
102
  }
103
103
  assertGenerationBudget(q.otherVoices.length, "other voices");
104
104
  for (let index = 0; index < q.otherVoices.length; index += 1) {
@@ -159,7 +159,7 @@ function evaluateInternal(q, collectSuggestions) {
159
159
  reasons |= 16 /* NonScale */;
160
160
  raise(2 /* Dissonant */);
161
161
  }
162
- if (chord && !(chord.intervals.includes(4) && chord.intervals.includes(10))) {
162
+ if (chord && !isChordMember(pitch, chord)) {
163
163
  const ic = intervalAboveRoot(pitch, chord);
164
164
  const toneOffsets = chord.intervals.map((i) => (i % 12 + 12) % 12);
165
165
  if (toneOffsets.some((t) => Math.abs(ic - t) === 6)) {
@@ -190,6 +190,9 @@ function evaluateInternal(q, collectSuggestions) {
190
190
  if (!createsVerticalDissonance(pitch, ov.pitch, twoVoice)) {
191
191
  continue;
192
192
  }
193
+ if (chord && isStructuralChordDissonance(pitch, ov.pitch, chord)) {
194
+ continue;
195
+ }
193
196
  reasons |= 1024 /* VerticalDissonance */;
194
197
  raise(2 /* Dissonant */);
195
198
  if (prev !== void 0 && pitch === prev && ov.prevPitch !== void 0 && !createsVerticalDissonance(prev, ov.prevPitch, twoVoice)) {
@@ -246,39 +249,46 @@ function findSafeNearby(q, candidate) {
246
249
  }
247
250
  return out;
248
251
  }
252
+ var REASON_TEXT = [
253
+ [32 /* OutOfRange */, "Outside the target vocal range"],
254
+ [
255
+ 16384 /* Suspension */,
256
+ "Prepared suspension \u2014 a held consonance now dissonant, awaiting resolution"
257
+ ],
258
+ [1024 /* VerticalDissonance */, "Dissonant against a sounding voice on a strong beat"],
259
+ [64 /* Tritone */, "Tritone against the sounding harmony"],
260
+ [4 /* AvoidNote */, "Avoid note \u2014 clashes with a chord tone a semitone away"],
261
+ [16 /* NonScale */, "Outside the key"],
262
+ [2048 /* ParallelPerfect */, "Parallel perfect interval with another voice"],
263
+ [4096 /* HiddenParallel */, "Hidden perfect interval by similar motion"],
264
+ [8192 /* VoiceCrossing */, "Crosses another voice"],
265
+ [128 /* LargeLeap */, "Leap wider than the profile allows"],
266
+ [32768 /* NeedsResolution */, "Dissonance awaiting a stepwise resolution"],
267
+ [2 /* Tension */, "Chord tension"],
268
+ [8 /* ScaleTone */, "Scale tone, not in the chord"],
269
+ [256 /* MinorSecond */, "A semitone from a sounding voice"],
270
+ [512 /* MajorSeventh */, "A major seventh from a sounding voice"],
271
+ [1 /* ChordTone */, "Chord tone"]
272
+ ];
273
+ function severityOfReason(flag, profile) {
274
+ const entry = REASON_SEVERITY.find(([candidate]) => candidate === flag);
275
+ return entry === void 0 ? 0 /* Safe */ : entry[profile === "strict" ? 2 : 1];
276
+ }
249
277
  function describe(reasons, q) {
250
- if (reasons & 32 /* OutOfRange */) {
251
- return "Outside the target vocal range";
252
- }
253
- if (reasons & 16384 /* Suspension */) {
254
- return "Prepared suspension \u2014 a held consonance now dissonant, awaiting resolution";
255
- }
256
- if (reasons & 1024 /* VerticalDissonance */) {
257
- return "Dissonant against a sounding voice on a strong beat";
258
- }
259
- if (reasons & 4 /* AvoidNote */) {
260
- return "Avoid note \u2014 clashes with a chord tone a semitone away";
261
- }
262
- if (reasons & 2048 /* ParallelPerfect */) {
263
- return "Parallel perfect interval with another voice";
264
- }
265
- if (reasons & 4096 /* HiddenParallel */) {
266
- return "Hidden perfect interval by similar motion";
267
- }
268
- if (reasons & 8192 /* VoiceCrossing */) {
269
- return "Crosses another voice";
270
- }
271
- if (reasons & 1 /* ChordTone */) {
272
- return "Chord tone";
273
- }
274
- if (reasons & 2 /* Tension */) {
275
- return "Chord tension";
276
- }
277
- if (reasons & 16 /* NonScale */) {
278
- return "Outside the key";
278
+ let best;
279
+ let bestSeverity = -1;
280
+ for (const [flag, text] of REASON_TEXT) {
281
+ if (!(reasons & flag)) {
282
+ continue;
283
+ }
284
+ const severity = severityOfReason(flag, q.profile);
285
+ if (severity > bestSeverity) {
286
+ bestSeverity = severity;
287
+ best = text;
288
+ }
279
289
  }
280
- if (reasons & 8 /* ScaleTone */) {
281
- return "Scale tone, not in the chord";
290
+ if (best !== void 0) {
291
+ return best;
282
292
  }
283
293
  return q.chord ? "Placeable" : "No chord context";
284
294
  }
@@ -289,7 +299,9 @@ function enumerateSafePitches(q, pitchLow, pitchHigh) {
289
299
  assertInteger(pitchLow, "pitchLow");
290
300
  assertInteger(pitchHigh, "pitchHigh");
291
301
  if (pitchLow > pitchHigh) {
292
- throw new RangeError(`pitchLow must not exceed pitchHigh; received ${pitchLow} > ${pitchHigh}`);
302
+ throw new InvalidInputError(
303
+ `pitchLow must not exceed pitchHigh; received ${pitchLow} > ${pitchHigh}`
304
+ );
293
305
  }
294
306
  assertGenerationBudget(pitchHigh - pitchLow + 1, "safe pitch candidates");
295
307
  for (let pitch = pitchHigh; pitch >= pitchLow; pitch -= 1) {
@@ -312,4 +324,4 @@ export {
312
324
  evaluateSafety,
313
325
  enumerateSafePitches
314
326
  };
315
- //# sourceMappingURL=chunk-EH6EM37U.js.map
327
+ //# sourceMappingURL=chunk-NYYWMV2C.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/theory/safety/index.ts"],"sourcesContent":["import { InvalidInputError } from '../../core/errors/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport {\n assertFiniteNumber,\n assertGenerationBudget,\n assertInteger,\n} from '../../core/validation/index.js';\nimport type { Chord } from '../chord/index.js';\nimport { intervalAboveRoot, isChordMember } from '../chord/index.js';\nimport {\n createsHiddenParallelPerfect,\n createsParallelPerfect,\n createsVerticalDissonance,\n isForbiddenMelodicLeap,\n} from '../counterpoint/index.js';\nimport { isScaleTone } from '../scale/index.js';\n\n/**\n * Severity policy: `strict` counterpoint vs. lenient `pop` voice-leading.\n *\n * @category Arrangement & Analysis\n */\nexport type SafetyProfile = 'strict' | 'pop';\n\n/**\n * Overall placeability verdict for a candidate pitch.\n *\n * @category Arrangement & Analysis\n */\nexport enum NoteSafety {\n Safe = 0,\n Warning = 1,\n Dissonant = 2,\n}\n\n/**\n * Bit flags describing why a pitch received its verdict.\n *\n * @category Arrangement & Analysis\n */\nexport enum ReasonFlag {\n ChordTone = 1 << 0,\n Tension = 1 << 1,\n AvoidNote = 1 << 2,\n ScaleTone = 1 << 3,\n NonScale = 1 << 4,\n OutOfRange = 1 << 5,\n Tritone = 1 << 6,\n LargeLeap = 1 << 7,\n MinorSecond = 1 << 8,\n MajorSeventh = 1 << 9,\n VerticalDissonance = 1 << 10,\n ParallelPerfect = 1 << 11,\n HiddenParallel = 1 << 12,\n VoiceCrossing = 1 << 13,\n Suspension = 1 << 14,\n NeedsResolution = 1 << 15,\n}\n\n/**\n * A neighbouring voice's current and previous pitch.\n *\n * @category Arrangement & Analysis\n */\nexport type VoiceSnapshot = {\n pitch: number;\n prevPitch?: number;\n};\n\n/**\n * Everything needed to evaluate one candidate pitch.\n *\n * @category Arrangement & Analysis\n */\nexport type SafetyQuery = {\n profile: SafetyProfile;\n candidatePitch: number;\n prevPitch?: number;\n chord: Chord | null;\n key: KeyScale;\n otherVoices: VoiceSnapshot[];\n strongBeat: boolean;\n vocalLow?: number;\n vocalHigh?: number;\n};\n\n/**\n * The verdict, reason flags, and optional resolution guidance.\n *\n * @category Arrangement & Analysis\n */\nexport type SafetyResult = {\n safety: NoteSafety;\n reasons: number;\n resolveTo?: number;\n suggestions?: number[];\n rationale?: string;\n};\n\n/** Semitone radius searched around a rejected candidate for safe alternatives. */\nconst SUGGESTION_WINDOW = 12;\n/** Maximum number of alternative pitches returned in `SafetyResult.suggestions`. */\nconst MAX_SUGGESTIONS = 3;\n\n/** Canonical minimum verdict contributed by each non-informational reason. */\nconst REASON_SEVERITY: ReadonlyArray<\n readonly [flag: ReasonFlag, pop: NoteSafety, strict: NoteSafety]\n> = [\n [ReasonFlag.Tension, NoteSafety.Warning, NoteSafety.Warning],\n [ReasonFlag.AvoidNote, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.ScaleTone, NoteSafety.Warning, NoteSafety.Warning],\n [ReasonFlag.NonScale, NoteSafety.Dissonant, NoteSafety.Dissonant],\n [ReasonFlag.OutOfRange, NoteSafety.Warning, NoteSafety.Warning],\n [ReasonFlag.Tritone, NoteSafety.Dissonant, NoteSafety.Dissonant],\n [ReasonFlag.LargeLeap, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.VerticalDissonance, NoteSafety.Dissonant, NoteSafety.Dissonant],\n [ReasonFlag.ParallelPerfect, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.HiddenParallel, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.VoiceCrossing, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.Suspension, NoteSafety.Dissonant, NoteSafety.Dissonant],\n [ReasonFlag.NeedsResolution, NoteSafety.Warning, NoteSafety.Dissonant],\n];\n\nfunction minimumSafetyForReasons(reasons: number, profile: SafetyProfile): NoteSafety {\n let minimum = NoteSafety.Safe;\n const severityIndex = profile === 'strict' ? 2 : 1;\n for (const entry of REASON_SEVERITY) {\n if (reasons & entry[0]) {\n minimum = Math.max(minimum, entry[severityIndex]) as NoteSafety;\n }\n }\n return minimum;\n}\n\n/** Whether the perfect fourth is an avoid note over this chord. */\nfunction avoidsFourth(chord: Chord): boolean {\n return chord.intervals.includes(4) && !chord.intervals.includes(5);\n}\n\n/** Whether the major seventh is an avoid note over this chord (dominant). */\nfunction avoidsMajorSeventh(chord: Chord): boolean {\n return chord.intervals.includes(4) && chord.intervals.includes(10);\n}\n\n/**\n * Whether a dissonance between two chord tones is one the chord is built from.\n *\n * A seventh chord states a seventh against its root and a dominant states the\n * tritone between its third and seventh; those intervals are the chord's\n * identity, not a clash to be prepared and resolved. A second between two chord\n * tones (a ninth voiced against the root, say) is a spacing problem rather than\n * a property of the harmony, so it stays reportable.\n */\nfunction isStructuralChordDissonance(a: number, b: number, chord: Chord): boolean {\n const cls = Math.abs(Math.trunc(a) - Math.trunc(b)) % 12;\n if (cls !== 6 && cls !== 10 && cls !== 11) {\n return false;\n }\n return isChordMember(a, chord) && isChordMember(b, chord);\n}\n\n/** Nearest chord tone at or below the candidate, within two semitones. */\nfunction stepResolution(pitch: number, chord: Chord): number | undefined {\n for (let delta = 1; delta <= 2; delta += 1) {\n if (isChordMember(pitch - delta, chord)) {\n return pitch - delta;\n }\n }\n return undefined;\n}\n\n/**\n * Evaluate the safety of placing a candidate pitch in a voice.\n *\n * Reasons are merged from harmonic context (chord tone / tension / avoid / scale)\n * and counterpoint (vertical dissonance, parallels, voice crossing). The `strict`\n * profile treats avoid notes and parallels as dissonant; `pop` treats them as\n * warnings. The final verdict is the worst severity among the reasons.\n *\n * @param q The candidate and its harmonic/voice-leading context.\n * @returns The verdict, reason bitmask, and optional resolution guidance.\n * @example\n * ```ts\n * import { evaluateSafety, makeChord, majorKey, NoteSafety } from '@libraz/libcantus';\n * const result = evaluateSafety({\n * profile: 'pop',\n * candidatePitch: 60, // C over a C major chord\n * chord: makeChord(0, 'maj'),\n * key: majorKey(0),\n * otherVoices: [],\n * strongBeat: true,\n * });\n * result.safety === NoteSafety.Safe; // true — C is a chord tone\n * ```\n * @category Arrangement & Analysis\n */\nexport function evaluateSafety(q: SafetyQuery, opts: EvaluateSafetyOptions = {}): SafetyResult {\n assertSafetyContext(q);\n assertFiniteNumber(q.candidatePitch, 'candidatePitch');\n return evaluateInternal(q, opts.suggestions ?? true);\n}\n\n/**\n * Options for {@link evaluateSafety}.\n *\n * @category Arrangement & Analysis\n */\nexport type EvaluateSafetyOptions = {\n /**\n * Whether to search for safe replacement pitches when the verdict is not\n * safe. Turning it off skips up to two dozen extra evaluations per rejected\n * pitch, which is worth doing when only the verdict is wanted.\n *\n * @defaultValue true\n */\n suggestions?: boolean;\n};\n\n/** Validate all numeric context fields once at a public safety entry point. */\nfunction assertSafetyContext(q: Omit<SafetyQuery, 'candidatePitch'>): void {\n if (q.prevPitch !== undefined) assertFiniteNumber(q.prevPitch, 'prevPitch');\n if (q.vocalLow !== undefined) assertFiniteNumber(q.vocalLow, 'vocalLow');\n if (q.vocalHigh !== undefined) assertFiniteNumber(q.vocalHigh, 'vocalHigh');\n if (q.vocalLow !== undefined && q.vocalHigh !== undefined && q.vocalLow > q.vocalHigh) {\n throw new InvalidInputError('vocalLow must not exceed vocalHigh');\n }\n assertGenerationBudget(q.otherVoices.length, 'other voices');\n for (let index = 0; index < q.otherVoices.length; index += 1) {\n const voice = q.otherVoices[index];\n if (!voice) continue;\n assertFiniteNumber(voice.pitch, `otherVoices[${index}].pitch`);\n if (voice.prevPitch !== undefined) {\n assertFiniteNumber(voice.prevPitch, `otherVoices[${index}].prevPitch`);\n }\n }\n}\n\n/**\n * Core safety evaluation.\n *\n * @param q The candidate and its harmonic/voice-leading context.\n * @param collectSuggestions Whether to search for safe alternatives on a\n * non-Safe verdict. Set false during that search itself to bound recursion.\n * @returns The verdict, reason bitmask, and optional guidance.\n */\nfunction evaluateInternal(q: SafetyQuery, collectSuggestions: boolean): SafetyResult {\n let reasons = 0;\n let level = NoteSafety.Safe;\n let resolveTo: number | undefined;\n const raise = (l: NoteSafety) => {\n if (l > level) {\n level = l;\n }\n };\n const avoidSeverity = q.profile === 'strict' ? NoteSafety.Dissonant : NoteSafety.Warning;\n const parallelSeverity = avoidSeverity;\n const pitch = q.candidatePitch;\n\n if (\n (q.vocalLow !== undefined && pitch < q.vocalLow) ||\n (q.vocalHigh !== undefined && pitch > q.vocalHigh)\n ) {\n reasons |= ReasonFlag.OutOfRange;\n raise(NoteSafety.Warning);\n }\n\n const inScale = isScaleTone(pitch, q.key);\n const chord = q.chord;\n\n if (chord && isChordMember(pitch, chord)) {\n reasons |= ReasonFlag.ChordTone;\n } else if (chord) {\n const ic = intervalAboveRoot(pitch, chord);\n const isTension = ic === 2 || ic === 5 || ic === 9;\n const avoid = (ic === 5 && avoidsFourth(chord)) || (ic === 11 && avoidsMajorSeventh(chord));\n if (avoid) {\n reasons |= ReasonFlag.AvoidNote;\n if (isTension) {\n reasons |= ReasonFlag.Tension;\n }\n reasons |= ReasonFlag.NeedsResolution;\n resolveTo = stepResolution(pitch, chord);\n raise(avoidSeverity);\n } else if (isTension && inScale) {\n reasons |= ReasonFlag.Tension;\n raise(NoteSafety.Warning);\n } else if (inScale) {\n reasons |= ReasonFlag.ScaleTone;\n raise(NoteSafety.Warning);\n } else {\n reasons |= ReasonFlag.NonScale;\n raise(NoteSafety.Dissonant);\n }\n } else if (inScale) {\n reasons |= ReasonFlag.ScaleTone;\n raise(NoteSafety.Warning);\n } else {\n reasons |= ReasonFlag.NonScale;\n raise(NoteSafety.Dissonant);\n }\n\n // Harmonic tritone against a chord tone. A chord tone is never flagged for a\n // tritone its own chord spells: the interval is the chord's identity, not a\n // clash with it. This covers dominant sevenths (third against seventh) and\n // equally the diminished, half-diminished, diminished-seventh and minor-sixth\n // families, where the tritone falls between two chord tones as well.\n if (chord && !isChordMember(pitch, chord)) {\n const ic = intervalAboveRoot(pitch, chord);\n const toneOffsets = chord.intervals.map((i) => ((i % 12) + 12) % 12);\n if (toneOffsets.some((t) => Math.abs(ic - t) === 6)) {\n reasons |= ReasonFlag.Tritone;\n }\n }\n\n if (q.prevPitch !== undefined) {\n // Quality flags reduce the melodic interval to its pitch-class interval so\n // compound intervals are treated uniformly with their simple form. The\n // forbidden-leap flag defers to the counterpoint rule, which preserves the\n // octave boundary (an octave leap is allowed, wider leaps are not).\n const pc = Math.abs(pitch - q.prevPitch) % 12;\n if (pc === 6) {\n reasons |= ReasonFlag.Tritone;\n raise(NoteSafety.Dissonant);\n }\n if (isForbiddenMelodicLeap(q.prevPitch, pitch)) {\n reasons |= ReasonFlag.LargeLeap;\n raise(parallelSeverity);\n }\n if (pc === 1) {\n reasons |= ReasonFlag.MinorSecond;\n }\n if (pc === 11) {\n reasons |= ReasonFlag.MajorSeventh;\n }\n }\n\n if (q.strongBeat) {\n const twoVoice = q.otherVoices.length === 1;\n const prev = q.prevPitch;\n for (const ov of q.otherVoices) {\n if (!createsVerticalDissonance(pitch, ov.pitch, twoVoice)) {\n continue;\n }\n if (chord && isStructuralChordDissonance(pitch, ov.pitch, chord)) {\n continue;\n }\n reasons |= ReasonFlag.VerticalDissonance;\n raise(NoteSafety.Dissonant);\n // A held pitch that was consonant on the previous step and is now\n // dissonant against the same voice is a prepared suspension.\n if (\n prev !== undefined &&\n pitch === prev &&\n ov.prevPitch !== undefined &&\n !createsVerticalDissonance(prev, ov.prevPitch, twoVoice)\n ) {\n reasons |= ReasonFlag.Suspension;\n }\n }\n }\n\n if (q.prevPitch !== undefined) {\n const prev = q.prevPitch;\n for (const ov of q.otherVoices) {\n if (ov.prevPitch === undefined) {\n continue;\n }\n // createsParallelPerfect covers every perfect class (unison/octave and\n // fifth), so no separate octave check is needed.\n if (createsParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {\n reasons |= ReasonFlag.ParallelPerfect;\n raise(parallelSeverity);\n }\n if (createsHiddenParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {\n reasons |= ReasonFlag.HiddenParallel;\n raise(parallelSeverity);\n }\n const crossedNow = pitch - ov.pitch;\n const crossedPrev = prev - ov.prevPitch;\n if (crossedNow !== 0 && crossedPrev !== 0 && crossedNow > 0 !== crossedPrev > 0) {\n reasons |= ReasonFlag.VoiceCrossing;\n raise(parallelSeverity);\n }\n }\n }\n\n // Keep reason production and verdict policy as one invariant even when a new\n // call path skips an earlier local `raise`: informational flags (ChordTone,\n // MinorSecond, MajorSeventh) may coexist with Safe; every policy flag above\n // contributes at least its table severity.\n raise(minimumSafetyForReasons(reasons, q.profile));\n\n const result: SafetyResult = { safety: level, reasons, rationale: describe(reasons, q) };\n if (resolveTo !== undefined) {\n result.resolveTo = resolveTo;\n }\n if (collectSuggestions && level !== NoteSafety.Safe) {\n const suggestions = findSafeNearby(q, pitch);\n if (suggestions.length > 0) {\n result.suggestions = suggestions;\n }\n }\n return result;\n}\n\n/**\n * Find safe alternative pitches near a rejected candidate.\n *\n * Searches outward from the candidate within `SUGGESTION_WINDOW` semitones,\n * probing the pitch below before the one above at each distance so the result\n * is deterministic and ordered by nearness.\n *\n * @param q The safety context of the rejected candidate.\n * @param candidate The candidate pitch that was not Safe.\n * @returns Up to `MAX_SUGGESTIONS` safe pitches, nearest first.\n */\nfunction findSafeNearby(q: SafetyQuery, candidate: number): number[] {\n const out: number[] = [];\n for (let d = 1; d <= SUGGESTION_WINDOW; d += 1) {\n for (const p of [candidate - d, candidate + d]) {\n if (evaluateInternal({ ...q, candidatePitch: p }, false).safety === NoteSafety.Safe) {\n out.push(p);\n if (out.length >= MAX_SUGGESTIONS) {\n return out;\n }\n }\n }\n }\n return out;\n}\n\n/**\n * Wording for every reason flag, most to least specific.\n *\n * The list is exhaustive by construction: {@link describe} picks from it by\n * severity, so a flag with no wording here would let a rejected pitch be\n * explained by a milder reason — the seventh of a dominant reading as a plain\n * \"Chord tone\" while its verdict says dissonant.\n */\nconst REASON_TEXT: ReadonlyArray<readonly [ReasonFlag, string]> = [\n [ReasonFlag.OutOfRange, 'Outside the target vocal range'],\n [\n ReasonFlag.Suspension,\n 'Prepared suspension — a held consonance now dissonant, awaiting resolution',\n ],\n [ReasonFlag.VerticalDissonance, 'Dissonant against a sounding voice on a strong beat'],\n [ReasonFlag.Tritone, 'Tritone against the sounding harmony'],\n [ReasonFlag.AvoidNote, 'Avoid note — clashes with a chord tone a semitone away'],\n [ReasonFlag.NonScale, 'Outside the key'],\n [ReasonFlag.ParallelPerfect, 'Parallel perfect interval with another voice'],\n [ReasonFlag.HiddenParallel, 'Hidden perfect interval by similar motion'],\n [ReasonFlag.VoiceCrossing, 'Crosses another voice'],\n [ReasonFlag.LargeLeap, 'Leap wider than the profile allows'],\n [ReasonFlag.NeedsResolution, 'Dissonance awaiting a stepwise resolution'],\n [ReasonFlag.Tension, 'Chord tension'],\n [ReasonFlag.ScaleTone, 'Scale tone, not in the chord'],\n [ReasonFlag.MinorSecond, 'A semitone from a sounding voice'],\n [ReasonFlag.MajorSeventh, 'A major seventh from a sounding voice'],\n [ReasonFlag.ChordTone, 'Chord tone'],\n];\n\n/** The severity a single flag carries under a profile; Safe when purely informational. */\nfunction severityOfReason(flag: ReasonFlag, profile: SafetyProfile): NoteSafety {\n const entry = REASON_SEVERITY.find(([candidate]) => candidate === flag);\n return entry === undefined ? NoteSafety.Safe : entry[profile === 'strict' ? 2 : 1];\n}\n\n/**\n * Build a short human explanation from the reason flags.\n *\n * The flag driving the verdict wins: the one whose severity under this profile\n * is highest, ties going to the more specific wording. Explaining a rejected\n * pitch by an informational flag it also carries would tell the caller nothing\n * about what to fix.\n */\nfunction describe(reasons: number, q: SafetyQuery): string {\n let best: string | undefined;\n let bestSeverity = -1;\n for (const [flag, text] of REASON_TEXT) {\n if (!(reasons & flag)) {\n continue;\n }\n const severity = severityOfReason(flag, q.profile);\n if (severity > bestSeverity) {\n bestSeverity = severity;\n best = text;\n }\n }\n if (best !== undefined) {\n return best;\n }\n return q.chord ? 'Placeable' : 'No chord context';\n}\n\n/**\n * Enumerate placeable pitches in a range, chord tones first, descending.\n *\n * @param q The safety context minus the candidate pitch.\n * @param pitchLow Lowest MIDI pitch to consider (inclusive).\n * @param pitchHigh Highest MIDI pitch to consider (inclusive).\n * @returns Placeable pitches (non-dissonant), chord tones before others, each group descending.\n * @throws If either bound is non-finite/non-integral, reversed, or exceeds the generation budget.\n * @example\n * ```ts\n * import { enumerateSafePitches, makeChord, majorKey } from '@libraz/libcantus';\n * const pitches = enumerateSafePitches(\n * { profile: 'pop', chord: makeChord(0, 'maj'), key: majorKey(0), otherVoices: [], strongBeat: true },\n * 60,\n * 72,\n * );\n * pitches; // placeable pitches in [60, 72], chord tones first, each group descending\n * ```\n * @category Arrangement & Analysis\n */\nexport function enumerateSafePitches(\n q: Omit<SafetyQuery, 'candidatePitch'>,\n pitchLow: number,\n pitchHigh: number,\n): number[] {\n assertSafetyContext(q);\n const chordTones: number[] = [];\n const others: number[] = [];\n assertInteger(pitchLow, 'pitchLow');\n assertInteger(pitchHigh, 'pitchHigh');\n if (pitchLow > pitchHigh) {\n throw new InvalidInputError(\n `pitchLow must not exceed pitchHigh; received ${pitchLow} > ${pitchHigh}`,\n );\n }\n assertGenerationBudget(pitchHigh - pitchLow + 1, 'safe pitch candidates');\n for (let pitch = pitchHigh; pitch >= pitchLow; pitch -= 1) {\n const result = evaluateInternal({ ...q, candidatePitch: pitch }, false);\n if (result.safety === NoteSafety.Dissonant) {\n continue;\n }\n if (result.reasons & ReasonFlag.ChordTone) {\n chordTones.push(pitch);\n } else {\n others.push(pitch);\n }\n }\n return [...chordTones, ...others];\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA6BO,IAAK,aAAL,kBAAKA,gBAAL;AACL,EAAAA,wBAAA,UAAO,KAAP;AACA,EAAAA,wBAAA,aAAU,KAAV;AACA,EAAAA,wBAAA,eAAY,KAAZ;AAHU,SAAAA;AAAA,GAAA;AAWL,IAAK,aAAL,kBAAKC,gBAAL;AACL,EAAAA,wBAAA,eAAY,KAAZ;AACA,EAAAA,wBAAA,aAAU,KAAV;AACA,EAAAA,wBAAA,eAAY,KAAZ;AACA,EAAAA,wBAAA,eAAY,KAAZ;AACA,EAAAA,wBAAA,cAAW,MAAX;AACA,EAAAA,wBAAA,gBAAa,MAAb;AACA,EAAAA,wBAAA,aAAU,MAAV;AACA,EAAAA,wBAAA,eAAY,OAAZ;AACA,EAAAA,wBAAA,iBAAc,OAAd;AACA,EAAAA,wBAAA,kBAAe,OAAf;AACA,EAAAA,wBAAA,wBAAqB,QAArB;AACA,EAAAA,wBAAA,qBAAkB,QAAlB;AACA,EAAAA,wBAAA,oBAAiB,QAAjB;AACA,EAAAA,wBAAA,mBAAgB,QAAhB;AACA,EAAAA,wBAAA,gBAAa,SAAb;AACA,EAAAA,wBAAA,qBAAkB,SAAlB;AAhBU,SAAAA;AAAA,GAAA;AA4DZ,IAAM,oBAAoB;AAE1B,IAAM,kBAAkB;AAGxB,IAAM,kBAEF;AAAA,EACF,CAAC,iBAAoB,iBAAoB,eAAkB;AAAA,EAC3D,CAAC,mBAAsB,iBAAoB,iBAAoB;AAAA,EAC/D,CAAC,mBAAsB,iBAAoB,eAAkB;AAAA,EAC7D,CAAC,mBAAqB,mBAAsB,iBAAoB;AAAA,EAChE,CAAC,qBAAuB,iBAAoB,eAAkB;AAAA,EAC9D,CAAC,kBAAoB,mBAAsB,iBAAoB;AAAA,EAC/D,CAAC,qBAAsB,iBAAoB,iBAAoB;AAAA,EAC/D,CAAC,+BAA+B,mBAAsB,iBAAoB;AAAA,EAC1E,CAAC,4BAA4B,iBAAoB,iBAAoB;AAAA,EACrE,CAAC,2BAA2B,iBAAoB,iBAAoB;AAAA,EACpE,CAAC,0BAA0B,iBAAoB,iBAAoB;AAAA,EACnE,CAAC,wBAAuB,mBAAsB,iBAAoB;AAAA,EAClE,CAAC,6BAA4B,iBAAoB,iBAAoB;AACvE;AAEA,SAAS,wBAAwB,SAAiB,SAAoC;AACpF,MAAI,UAAU;AACd,QAAM,gBAAgB,YAAY,WAAW,IAAI;AACjD,aAAW,SAAS,iBAAiB;AACnC,QAAI,UAAU,MAAM,CAAC,GAAG;AACtB,gBAAU,KAAK,IAAI,SAAS,MAAM,aAAa,CAAC;AAAA,IAClD;AAAA,EACF;AACA,SAAO;AACT;AAGA,SAAS,aAAa,OAAuB;AAC3C,SAAO,MAAM,UAAU,SAAS,CAAC,KAAK,CAAC,MAAM,UAAU,SAAS,CAAC;AACnE;AAGA,SAAS,mBAAmB,OAAuB;AACjD,SAAO,MAAM,UAAU,SAAS,CAAC,KAAK,MAAM,UAAU,SAAS,EAAE;AACnE;AAWA,SAAS,4BAA4B,GAAW,GAAW,OAAuB;AAChF,QAAM,MAAM,KAAK,IAAI,KAAK,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,IAAI;AACtD,MAAI,QAAQ,KAAK,QAAQ,MAAM,QAAQ,IAAI;AACzC,WAAO;AAAA,EACT;AACA,SAAO,cAAc,GAAG,KAAK,KAAK,cAAc,GAAG,KAAK;AAC1D;AAGA,SAAS,eAAe,OAAe,OAAkC;AACvE,WAAS,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG;AAC1C,QAAI,cAAc,QAAQ,OAAO,KAAK,GAAG;AACvC,aAAO,QAAQ;AAAA,IACjB;AAAA,EACF;AACA,SAAO;AACT;AA2BO,SAAS,eAAe,GAAgB,OAA8B,CAAC,GAAiB;AAC7F,sBAAoB,CAAC;AACrB,qBAAmB,EAAE,gBAAgB,gBAAgB;AACrD,SAAO,iBAAiB,GAAG,KAAK,eAAe,IAAI;AACrD;AAmBA,SAAS,oBAAoB,GAA8C;AACzE,MAAI,EAAE,cAAc,OAAW,oBAAmB,EAAE,WAAW,WAAW;AAC1E,MAAI,EAAE,aAAa,OAAW,oBAAmB,EAAE,UAAU,UAAU;AACvE,MAAI,EAAE,cAAc,OAAW,oBAAmB,EAAE,WAAW,WAAW;AAC1E,MAAI,EAAE,aAAa,UAAa,EAAE,cAAc,UAAa,EAAE,WAAW,EAAE,WAAW;AACrF,UAAM,IAAI,kBAAkB,oCAAoC;AAAA,EAClE;AACA,yBAAuB,EAAE,YAAY,QAAQ,cAAc;AAC3D,WAAS,QAAQ,GAAG,QAAQ,EAAE,YAAY,QAAQ,SAAS,GAAG;AAC5D,UAAM,QAAQ,EAAE,YAAY,KAAK;AACjC,QAAI,CAAC,MAAO;AACZ,uBAAmB,MAAM,OAAO,eAAe,KAAK,SAAS;AAC7D,QAAI,MAAM,cAAc,QAAW;AACjC,yBAAmB,MAAM,WAAW,eAAe,KAAK,aAAa;AAAA,IACvE;AAAA,EACF;AACF;AAUA,SAAS,iBAAiB,GAAgB,oBAA2C;AACnF,MAAI,UAAU;AACd,MAAI,QAAQ;AACZ,MAAI;AACJ,QAAM,QAAQ,CAAC,MAAkB;AAC/B,QAAI,IAAI,OAAO;AACb,cAAQ;AAAA,IACV;AAAA,EACF;AACA,QAAM,gBAAgB,EAAE,YAAY,WAAW,oBAAuB;AACtE,QAAM,mBAAmB;AACzB,QAAM,QAAQ,EAAE;AAEhB,MACG,EAAE,aAAa,UAAa,QAAQ,EAAE,YACtC,EAAE,cAAc,UAAa,QAAQ,EAAE,WACxC;AACA,eAAW;AACX,UAAM,eAAkB;AAAA,EAC1B;AAEA,QAAM,UAAU,YAAY,OAAO,EAAE,GAAG;AACxC,QAAM,QAAQ,EAAE;AAEhB,MAAI,SAAS,cAAc,OAAO,KAAK,GAAG;AACxC,eAAW;AAAA,EACb,WAAW,OAAO;AAChB,UAAM,KAAK,kBAAkB,OAAO,KAAK;AACzC,UAAM,YAAY,OAAO,KAAK,OAAO,KAAK,OAAO;AACjD,UAAM,QAAS,OAAO,KAAK,aAAa,KAAK,KAAO,OAAO,MAAM,mBAAmB,KAAK;AACzF,QAAI,OAAO;AACT,iBAAW;AACX,UAAI,WAAW;AACb,mBAAW;AAAA,MACb;AACA,iBAAW;AACX,kBAAY,eAAe,OAAO,KAAK;AACvC,YAAM,aAAa;AAAA,IACrB,WAAW,aAAa,SAAS;AAC/B,iBAAW;AACX,YAAM,eAAkB;AAAA,IAC1B,WAAW,SAAS;AAClB,iBAAW;AACX,YAAM,eAAkB;AAAA,IAC1B,OAAO;AACL,iBAAW;AACX,YAAM,iBAAoB;AAAA,IAC5B;AAAA,EACF,WAAW,SAAS;AAClB,eAAW;AACX,UAAM,eAAkB;AAAA,EAC1B,OAAO;AACL,eAAW;AACX,UAAM,iBAAoB;AAAA,EAC5B;AAOA,MAAI,SAAS,CAAC,cAAc,OAAO,KAAK,GAAG;AACzC,UAAM,KAAK,kBAAkB,OAAO,KAAK;AACzC,UAAM,cAAc,MAAM,UAAU,IAAI,CAAC,OAAQ,IAAI,KAAM,MAAM,EAAE;AACnE,QAAI,YAAY,KAAK,CAAC,MAAM,KAAK,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG;AACnD,iBAAW;AAAA,IACb;AAAA,EACF;AAEA,MAAI,EAAE,cAAc,QAAW;AAK7B,UAAM,KAAK,KAAK,IAAI,QAAQ,EAAE,SAAS,IAAI;AAC3C,QAAI,OAAO,GAAG;AACZ,iBAAW;AACX,YAAM,iBAAoB;AAAA,IAC5B;AACA,QAAI,uBAAuB,EAAE,WAAW,KAAK,GAAG;AAC9C,iBAAW;AACX,YAAM,gBAAgB;AAAA,IACxB;AACA,QAAI,OAAO,GAAG;AACZ,iBAAW;AAAA,IACb;AACA,QAAI,OAAO,IAAI;AACb,iBAAW;AAAA,IACb;AAAA,EACF;AAEA,MAAI,EAAE,YAAY;AAChB,UAAM,WAAW,EAAE,YAAY,WAAW;AAC1C,UAAM,OAAO,EAAE;AACf,eAAW,MAAM,EAAE,aAAa;AAC9B,UAAI,CAAC,0BAA0B,OAAO,GAAG,OAAO,QAAQ,GAAG;AACzD;AAAA,MACF;AACA,UAAI,SAAS,4BAA4B,OAAO,GAAG,OAAO,KAAK,GAAG;AAChE;AAAA,MACF;AACA,iBAAW;AACX,YAAM,iBAAoB;AAG1B,UACE,SAAS,UACT,UAAU,QACV,GAAG,cAAc,UACjB,CAAC,0BAA0B,MAAM,GAAG,WAAW,QAAQ,GACvD;AACA,mBAAW;AAAA,MACb;AAAA,IACF;AAAA,EACF;AAEA,MAAI,EAAE,cAAc,QAAW;AAC7B,UAAM,OAAO,EAAE;AACf,eAAW,MAAM,EAAE,aAAa;AAC9B,UAAI,GAAG,cAAc,QAAW;AAC9B;AAAA,MACF;AAGA,UAAI,uBAAuB,MAAM,OAAO,GAAG,WAAW,GAAG,KAAK,GAAG;AAC/D,mBAAW;AACX,cAAM,gBAAgB;AAAA,MACxB;AACA,UAAI,6BAA6B,MAAM,OAAO,GAAG,WAAW,GAAG,KAAK,GAAG;AACrE,mBAAW;AACX,cAAM,gBAAgB;AAAA,MACxB;AACA,YAAM,aAAa,QAAQ,GAAG;AAC9B,YAAM,cAAc,OAAO,GAAG;AAC9B,UAAI,eAAe,KAAK,gBAAgB,KAAK,aAAa,MAAM,cAAc,GAAG;AAC/E,mBAAW;AACX,cAAM,gBAAgB;AAAA,MACxB;AAAA,IACF;AAAA,EACF;AAMA,QAAM,wBAAwB,SAAS,EAAE,OAAO,CAAC;AAEjD,QAAM,SAAuB,EAAE,QAAQ,OAAO,SAAS,WAAW,SAAS,SAAS,CAAC,EAAE;AACvF,MAAI,cAAc,QAAW;AAC3B,WAAO,YAAY;AAAA,EACrB;AACA,MAAI,sBAAsB,UAAU,cAAiB;AACnD,UAAM,cAAc,eAAe,GAAG,KAAK;AAC3C,QAAI,YAAY,SAAS,GAAG;AAC1B,aAAO,cAAc;AAAA,IACvB;AAAA,EACF;AACA,SAAO;AACT;AAaA,SAAS,eAAe,GAAgB,WAA6B;AACnE,QAAM,MAAgB,CAAC;AACvB,WAAS,IAAI,GAAG,KAAK,mBAAmB,KAAK,GAAG;AAC9C,eAAW,KAAK,CAAC,YAAY,GAAG,YAAY,CAAC,GAAG;AAC9C,UAAI,iBAAiB,EAAE,GAAG,GAAG,gBAAgB,EAAE,GAAG,KAAK,EAAE,WAAW,cAAiB;AACnF,YAAI,KAAK,CAAC;AACV,YAAI,IAAI,UAAU,iBAAiB;AACjC,iBAAO;AAAA,QACT;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAUA,IAAM,cAA4D;AAAA,EAChE,CAAC,qBAAuB,gCAAgC;AAAA,EACxD;AAAA,IACE;AAAA,IACA;AAAA,EACF;AAAA,EACA,CAAC,+BAA+B,qDAAqD;AAAA,EACrF,CAAC,kBAAoB,sCAAsC;AAAA,EAC3D,CAAC,mBAAsB,6DAAwD;AAAA,EAC/E,CAAC,mBAAqB,iBAAiB;AAAA,EACvC,CAAC,4BAA4B,8CAA8C;AAAA,EAC3E,CAAC,2BAA2B,2CAA2C;AAAA,EACvE,CAAC,0BAA0B,uBAAuB;AAAA,EAClD,CAAC,qBAAsB,oCAAoC;AAAA,EAC3D,CAAC,6BAA4B,2CAA2C;AAAA,EACxE,CAAC,iBAAoB,eAAe;AAAA,EACpC,CAAC,mBAAsB,8BAA8B;AAAA,EACrD,CAAC,uBAAwB,kCAAkC;AAAA,EAC3D,CAAC,wBAAyB,uCAAuC;AAAA,EACjE,CAAC,mBAAsB,YAAY;AACrC;AAGA,SAAS,iBAAiB,MAAkB,SAAoC;AAC9E,QAAM,QAAQ,gBAAgB,KAAK,CAAC,CAAC,SAAS,MAAM,cAAc,IAAI;AACtE,SAAO,UAAU,SAAY,eAAkB,MAAM,YAAY,WAAW,IAAI,CAAC;AACnF;AAUA,SAAS,SAAS,SAAiB,GAAwB;AACzD,MAAI;AACJ,MAAI,eAAe;AACnB,aAAW,CAAC,MAAM,IAAI,KAAK,aAAa;AACtC,QAAI,EAAE,UAAU,OAAO;AACrB;AAAA,IACF;AACA,UAAM,WAAW,iBAAiB,MAAM,EAAE,OAAO;AACjD,QAAI,WAAW,cAAc;AAC3B,qBAAe;AACf,aAAO;AAAA,IACT;AAAA,EACF;AACA,MAAI,SAAS,QAAW;AACtB,WAAO;AAAA,EACT;AACA,SAAO,EAAE,QAAQ,cAAc;AACjC;AAsBO,SAAS,qBACd,GACA,UACA,WACU;AACV,sBAAoB,CAAC;AACrB,QAAM,aAAuB,CAAC;AAC9B,QAAM,SAAmB,CAAC;AAC1B,gBAAc,UAAU,UAAU;AAClC,gBAAc,WAAW,WAAW;AACpC,MAAI,WAAW,WAAW;AACxB,UAAM,IAAI;AAAA,MACR,gDAAgD,QAAQ,MAAM,SAAS;AAAA,IACzE;AAAA,EACF;AACA,yBAAuB,YAAY,WAAW,GAAG,uBAAuB;AACxE,WAAS,QAAQ,WAAW,SAAS,UAAU,SAAS,GAAG;AACzD,UAAM,SAAS,iBAAiB,EAAE,GAAG,GAAG,gBAAgB,MAAM,GAAG,KAAK;AACtE,QAAI,OAAO,WAAW,mBAAsB;AAC1C;AAAA,IACF;AACA,QAAI,OAAO,UAAU,mBAAsB;AACzC,iBAAW,KAAK,KAAK;AAAA,IACvB,OAAO;AACL,aAAO,KAAK,KAAK;AAAA,IACnB;AAAA,EACF;AACA,SAAO,CAAC,GAAG,YAAY,GAAG,MAAM;AAClC;","names":["NoteSafety","ReasonFlag"]}