@libraz/libcantus 0.9.1 → 0.9.3

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 (160) hide show
  1. package/README.md +19 -3
  2. package/dist/analyze/index.cjs +1970 -0
  3. package/dist/analyze/index.cjs.map +1 -0
  4. package/dist/analyze/index.d.cts +341 -0
  5. package/dist/analyze/index.d.ts +341 -0
  6. package/dist/analyze/index.js +51 -0
  7. package/dist/analyze/index.js.map +1 -0
  8. package/dist/chunk-2ZWMSJH6.js +30 -0
  9. package/dist/chunk-2ZWMSJH6.js.map +1 -0
  10. package/dist/chunk-4MHA432G.js +2854 -0
  11. package/dist/chunk-4MHA432G.js.map +1 -0
  12. package/dist/chunk-62QZKCJQ.js +49 -0
  13. package/dist/chunk-62QZKCJQ.js.map +1 -0
  14. package/dist/chunk-AKLYPWIN.js +83 -0
  15. package/dist/chunk-AKLYPWIN.js.map +1 -0
  16. package/dist/chunk-C5XYKUVG.js +904 -0
  17. package/dist/chunk-C5XYKUVG.js.map +1 -0
  18. package/dist/chunk-CHMXELDS.js +549 -0
  19. package/dist/chunk-CHMXELDS.js.map +1 -0
  20. package/dist/chunk-DYWFNC4A.js +152 -0
  21. package/dist/chunk-DYWFNC4A.js.map +1 -0
  22. package/dist/chunk-EH6EM37U.js +315 -0
  23. package/dist/chunk-EH6EM37U.js.map +1 -0
  24. package/dist/chunk-FBQK3ZQN.js +131 -0
  25. package/dist/chunk-FBQK3ZQN.js.map +1 -0
  26. package/dist/chunk-GOAW4VYL.js +719 -0
  27. package/dist/chunk-GOAW4VYL.js.map +1 -0
  28. package/dist/chunk-I5R7ZBJS.js +202 -0
  29. package/dist/chunk-I5R7ZBJS.js.map +1 -0
  30. package/dist/chunk-IT2O4XFE.js +154 -0
  31. package/dist/chunk-IT2O4XFE.js.map +1 -0
  32. package/dist/chunk-MIWO425M.js +412 -0
  33. package/dist/chunk-MIWO425M.js.map +1 -0
  34. package/dist/chunk-N6IHQUOX.js +1 -0
  35. package/dist/chunk-N6IHQUOX.js.map +1 -0
  36. package/dist/chunk-SGU7SPLK.js +924 -0
  37. package/dist/chunk-SGU7SPLK.js.map +1 -0
  38. package/dist/core/index.cjs +648 -0
  39. package/dist/core/index.cjs.map +1 -0
  40. package/dist/core/index.d.cts +262 -0
  41. package/dist/core/index.d.ts +262 -0
  42. package/dist/core/index.js +93 -0
  43. package/dist/core/index.js.map +1 -0
  44. package/dist/function-DVH4Eusr.d.ts +187 -0
  45. package/dist/function-DYt4iKsn.d.cts +187 -0
  46. package/dist/generate/index.cjs +4123 -0
  47. package/dist/generate/index.cjs.map +1 -0
  48. package/dist/generate/index.d.cts +986 -0
  49. package/dist/generate/index.d.ts +986 -0
  50. package/dist/generate/index.js +52 -0
  51. package/dist/generate/index.js.map +1 -0
  52. package/dist/index-BNlGP86A.d.cts +157 -0
  53. package/dist/index-Bg5dYMmZ.d.ts +56 -0
  54. package/dist/index-Bt4tOUD9.d.ts +157 -0
  55. package/dist/index-CGMZNCx6.d.cts +140 -0
  56. package/dist/index-CGMZNCx6.d.ts +140 -0
  57. package/dist/index-Cl7mhOFM.d.cts +164 -0
  58. package/dist/index-Cl7mhOFM.d.ts +164 -0
  59. package/dist/index-CnZCo29u.d.cts +108 -0
  60. package/dist/index-CpOsP1y9.d.cts +151 -0
  61. package/dist/index-CxqBQtH1.d.cts +128 -0
  62. package/dist/index-Dikst44v.d.cts +56 -0
  63. package/dist/index-DxvARIP2.d.ts +151 -0
  64. package/dist/index-Jc0Ryi79.d.ts +128 -0
  65. package/dist/index-JnF4v9bx.d.ts +108 -0
  66. package/dist/index.cjs +3710 -3143
  67. package/dist/index.cjs.map +1 -1
  68. package/dist/index.d.cts +15 -3982
  69. package/dist/index.d.ts +15 -3982
  70. package/dist/index.js +191 -6504
  71. package/dist/index.js.map +1 -1
  72. package/dist/model/index.cjs +2574 -0
  73. package/dist/model/index.cjs.map +1 -0
  74. package/dist/model/index.d.cts +631 -0
  75. package/dist/model/index.d.ts +631 -0
  76. package/dist/model/index.js +21 -0
  77. package/dist/model/index.js.map +1 -0
  78. package/dist/styled-DZ7GjShb.d.ts +282 -0
  79. package/dist/styled-ZTbdaVFM.d.cts +282 -0
  80. package/dist/theory/index.cjs +1823 -0
  81. package/dist/theory/index.cjs.map +1 -0
  82. package/dist/theory/index.d.cts +535 -0
  83. package/dist/theory/index.d.ts +535 -0
  84. package/dist/theory/index.js +150 -0
  85. package/dist/theory/index.js.map +1 -0
  86. package/dist/types-MGP29r3t.d.cts +30 -0
  87. package/dist/types-MGP29r3t.d.ts +30 -0
  88. package/package.json +61 -5
  89. package/src/analyze/arrange/index.ts +21 -0
  90. package/src/analyze/arrange/internal.ts +127 -0
  91. package/src/analyze/arrange/tension.ts +199 -0
  92. package/src/analyze/arrange/tracks.ts +312 -0
  93. package/src/{detect → analyze/detect}/index.ts +50 -13
  94. package/src/analyze/functional/borrowed.ts +84 -0
  95. package/src/analyze/functional/cadence.ts +66 -0
  96. package/src/analyze/functional/function.ts +184 -0
  97. package/src/analyze/functional/index.ts +23 -0
  98. package/src/analyze/functional/internal.ts +29 -0
  99. package/src/{functional/index.ts → analyze/functional/roman.ts} +23 -332
  100. package/src/analyze/index.ts +49 -0
  101. package/src/{timeline → analyze/timeline}/index.ts +50 -18
  102. package/src/{analysis → analyze/voice}/index.ts +19 -7
  103. package/src/core/event-index/index.ts +104 -0
  104. package/src/core/index.ts +61 -0
  105. package/src/{meter → core/meter}/index.ts +24 -4
  106. package/src/{random → core/random}/index.ts +26 -3
  107. package/src/{tuning → core/tuning}/index.ts +36 -6
  108. package/src/core/validation/index.ts +119 -0
  109. package/src/{bass → generate/bass}/index.ts +28 -5
  110. package/src/{countermelody → generate/countermelody}/index.ts +176 -85
  111. package/src/{drums → generate/drums}/euclid.ts +10 -3
  112. package/src/{drums → generate/drums}/index.ts +58 -26
  113. package/src/{drums → generate/drums}/percussion.ts +1 -1
  114. package/src/{drums → generate/drums}/rng.ts +2 -2
  115. package/src/{groove → generate/groove}/index.ts +28 -3
  116. package/src/{harmonize → generate/harmonize}/index.ts +68 -57
  117. package/src/generate/index.ts +58 -0
  118. package/src/{motif → generate/motif}/index.ts +52 -28
  119. package/src/{progression → generate/progression}/index.ts +19 -41
  120. package/src/{reharmony → generate/reharmony}/index.ts +12 -19
  121. package/src/{rhythm → generate/rhythm}/index.ts +13 -3
  122. package/src/index.ts +13 -234
  123. package/src/model/chord.ts +442 -0
  124. package/src/model/index.ts +5 -1096
  125. package/src/model/interval.ts +82 -0
  126. package/src/model/key.ts +266 -0
  127. package/src/model/note.ts +176 -0
  128. package/src/model/progression.ts +161 -0
  129. package/src/model/shared.ts +12 -0
  130. package/src/{chord → theory/chord}/index.ts +23 -3
  131. package/src/{counterpoint → theory/counterpoint}/index.ts +2 -2
  132. package/src/{harmony → theory/harmony}/index.ts +6 -21
  133. package/src/theory/index.ts +112 -0
  134. package/src/{safety → theory/safety}/index.ts +72 -3
  135. package/src/theory/scale/degrees.ts +123 -0
  136. package/src/theory/scale/index.ts +28 -0
  137. package/src/theory/scale/key.ts +56 -0
  138. package/src/theory/scale/masks.ts +153 -0
  139. package/src/{spelling → theory/spelling}/index.ts +73 -6
  140. package/src/{symbol → theory/symbol}/index.ts +8 -1
  141. package/src/theory/voicing/index.ts +5 -0
  142. package/src/theory/voicing/internal.ts +168 -0
  143. package/src/theory/voicing/leading.ts +112 -0
  144. package/src/theory/voicing/satb.ts +216 -0
  145. package/src/theory/voicing/styled.ts +199 -0
  146. package/src/arrange/index.ts +0 -571
  147. package/src/scale/index.ts +0 -331
  148. package/src/voicing/index.ts +0 -623
  149. /package/src/{interval → core/interval}/index.ts +0 -0
  150. /package/src/{pitch → core/pitch}/index.ts +0 -0
  151. /package/src/{types.ts → core/types.ts} +0 -0
  152. /package/src/{drums → generate/drums}/beat.ts +0 -0
  153. /package/src/{drums → generate/drums}/fills.ts +0 -0
  154. /package/src/{drums → generate/drums}/ghost.ts +0 -0
  155. /package/src/{drums → generate/drums}/hihat.ts +0 -0
  156. /package/src/{drums → generate/drums}/hit.ts +0 -0
  157. /package/src/{drums → generate/drums}/internal.ts +0 -0
  158. /package/src/{drums → generate/drums}/kick.ts +0 -0
  159. /package/src/{drums → generate/drums}/swing.ts +0 -0
  160. /package/src/{chordscale → theory/chordscale}/index.ts +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/analyze/timeline/index.ts","../src/analyze/arrange/internal.ts","../src/analyze/arrange/tension.ts","../src/analyze/voice/index.ts","../src/analyze/arrange/tracks.ts"],"sourcesContent":["import { createNoteEventIndex } from '../../core/event-index/index.js';\nimport type { TimeSignature } from '../../core/meter/index.js';\nimport { beatsPerBar, metricWeight, parseTimeSignature } from '../../core/meter/index.js';\nimport type { KeyScale, NoteEvent } from '../../core/types.js';\nimport {\n assertFiniteNumber,\n assertGenerationBudget,\n assertNoteEvents,\n assertRange,\n assertTimeSignature,\n} from '../../core/validation/index.js';\nimport type { Chord, ChordSpan } from '../../theory/chord/index.js';\nimport { chordPitchClasses, makeChord } from '../../theory/chord/index.js';\nimport { isScaleTone, majorKey } from '../../theory/scale/index.js';\nimport type { ChordMatch } from '../detect/index.js';\nimport { detectChord, detectChordBest, detectKey } from '../detect/index.js';\nimport type { Cadence } from '../functional/index.js';\nimport { detectCadence } from '../functional/index.js';\n\n/**\n * A chord occupying a half-open beat span.\n *\n * @category Arrangement & Analysis\n */\nexport type ChordSegment = {\n startBeat: number;\n endBeat: number;\n chord: Chord;\n};\n\n/**\n * A beat-indexed sequence of chord segments.\n *\n * @category Arrangement & Analysis\n */\nexport type ChordTimeline = {\n /** The chord sounding at a beat, or null when no segment covers it. */\n at: (beat: number) => Chord | null;\n segments: ChordSegment[];\n};\n\n/**\n * Build a chord timeline from placed chords.\n *\n * Each chord spans from its `startBeat` to the next chord's `startBeat`; the last\n * chord runs to `totalBeats`. Segments with no positive length (from duplicate\n * onsets or a last chord at or past `totalBeats`) are dropped. `at(beat)` returns\n * the covering segment's chord, or null when the beat lies outside every segment.\n *\n * @param chords Placed chords in time order.\n * @param totalBeats End of the timeline in beats.\n * @returns A queryable chord timeline.\n * @category Arrangement & Analysis\n */\nexport function chordTimelineFromChords(chords: ChordSpan[], totalBeats: number): ChordTimeline {\n assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, 'timeline totalBeats');\n assertGenerationBudget(chords.length, 'timeline chords');\n for (let index = 0; index < chords.length; index += 1) {\n assertRange(\n chords[index]?.startBeat ?? Number.NaN,\n 0,\n Number.MAX_SAFE_INTEGER,\n `chords[${index}].startBeat`,\n );\n }\n const sorted = [...chords]\n .filter((chord) => chord.startBeat < totalBeats)\n .sort((a, b) => a.startBeat - b.startBeat);\n const segments: ChordSegment[] = sorted\n .map((gc, i) => {\n const next = sorted[i + 1];\n const nextBeat = next ? next.startBeat : totalBeats;\n const endBeat = Math.min(totalBeats, Math.max(gc.startBeat, nextBeat));\n return {\n startBeat: gc.startBeat,\n endBeat,\n chord: makeChord(gc.rootPc, gc.quality, gc.bassPc),\n };\n })\n .filter((seg) => seg.endBeat > seg.startBeat);\n\n return { at: segmentLookup(segments), segments };\n}\n\n/** Build the `at(beat)` lookup over a segment list. */\nfunction segmentLookup(segments: ChordSegment[]): (beat: number) => Chord | null {\n return (beat) => {\n assertFiniteNumber(beat, 'timeline query beat');\n for (const seg of segments) {\n if (beat >= seg.startBeat && beat < seg.endBeat) {\n return seg.chord;\n }\n }\n return null;\n };\n}\n\nconst EPS = 1e-9;\n\n/** Fraction of the strongest pitch-class weight below which a pc is noise. */\nconst NOISE_THRESHOLD_RATIO = 0.2;\n\n/** Maximum number of pitch classes fed to chord detection per window. */\nconst MAX_DETECTION_PCS = 6;\n\n/** Number of top-weighted pitch classes tried in the detection fallback. */\nconst FALLBACK_PCS = 3;\n\n/** Confidence multiplier applied when the chosen chord match is inexact. */\nconst INEXACT_CONFIDENCE_FACTOR = 0.85;\n\n/** Score bonus for a match whose tones are all in the key. */\nconst DIATONIC_BONUS = 0.5;\n\n/** Score bonus for an exact match (no extra and no missing tones). */\nconst EXACT_BONUS = 0.5;\n\n/** Score penalty per extra or missing tone in a match. */\nconst MISMATCH_PENALTY = 0.3;\n\n/** Reduce a value to a pitch class in [0, 11]. */\nfunction pitchClass(value: number): number {\n return ((Math.trunc(value) % 12) + 12) % 12;\n}\n\n/**\n * Options controlling {@link chordTimelineFromNotes}.\n *\n * @category Arrangement & Analysis\n */\nexport type ChordTimelineOptions = {\n /** Key context; inferred from the notes with {@link detectKey} when omitted. */\n key?: KeyScale;\n /**\n * Time signature used for metric accents; defaults to 4/4.\n *\n * @defaultValue `4/4`\n */\n ts?: TimeSignature;\n /**\n * Window length in beats per chord slot; defaults to one bar of `ts`.\n *\n * @defaultValue one bar of `ts`\n */\n harmonicRhythm?: number;\n /**\n * End of the analyzed span in beats; defaults to the end of the last note.\n *\n * @defaultValue the end of the last note\n */\n totalBeats?: number;\n};\n\n/**\n * The result of {@link chordTimelineFromNotes}.\n *\n * @category Arrangement & Analysis\n */\nexport type ChordTimelineResult = {\n /** The inferred timeline, with adjacent identical chords merged. */\n timeline: ChordTimeline;\n /** The key used for the analysis (given or inferred). */\n key: KeyScale;\n /** One confidence value in [0, 1] per segment, in segment order. */\n segmentConfidence: number[];\n};\n\n/** A window's inferred chord and its confidence, or null for an empty window. */\ntype WindowChord = {\n chord: Chord;\n confidence: number;\n};\n\n/**\n * Score a chord match against a window's pitch-class weights: a heavily\n * weighted root, in-key tones, and exactness all raise the score; extra or\n * missing tones lower it.\n */\nfunction scoreMatch(\n match: ChordMatch,\n weights: number[],\n maxWeight: number,\n key: KeyScale,\n): number {\n const rootWeight = maxWeight > 0 ? (weights[match.rootPc] ?? 0) / maxWeight : 0;\n const tones = chordPitchClasses(makeChord(match.rootPc, match.quality));\n let score = rootWeight;\n if (tones.every((pc) => isScaleTone(pc, key))) {\n score += DIATONIC_BONUS;\n }\n if (match.exact) {\n score += EXACT_BONUS;\n }\n score -= MISMATCH_PENALTY * (match.extraPcs.length + match.missingPcs.length);\n return score;\n}\n\n/** Confidence of a chord for a window: chord-tone weight over total weight. */\nfunction chordConfidence(\n chord: Chord,\n weights: number[],\n totalWeight: number,\n exact: boolean,\n): number {\n const tones = new Set(chordPitchClasses(chord));\n let chordWeight = 0;\n for (let pc = 0; pc < 12; pc += 1) {\n if (tones.has(pc)) {\n chordWeight += weights[pc] ?? 0;\n }\n }\n const raw = totalWeight > 0 ? chordWeight / totalWeight : 0;\n const confidence = Math.min(1, Math.max(0, raw));\n return exact ? confidence : confidence * INEXACT_CONFIDENCE_FACTOR;\n}\n\n/**\n * Infer the chord sounding in one window from the notes overlapping it.\n *\n * Builds a pitch-class weight histogram (overlap duration x velocity x\n * metric-accent bonus for onsets inside the window), keeps the significantly\n * weighted pitch classes, and picks the best {@link detectChord} match by\n * root weight, key membership, and exactness.\n */\nfunction analyzeWindow(\n notes: NoteEvent[],\n windowStart: number,\n windowEnd: number,\n ts: TimeSignature,\n key: KeyScale,\n): WindowChord | null {\n const weights = new Array<number>(12).fill(0);\n let lowestPitch = Number.POSITIVE_INFINITY;\n for (const note of notes) {\n const noteEnd = note.startBeat + note.durationBeat;\n const overlap = Math.min(noteEnd, windowEnd) - Math.max(note.startBeat, windowStart);\n if (overlap <= EPS) {\n continue;\n }\n const velocityFactor = note.velocity !== undefined ? note.velocity / 127 : 1;\n const onsetInWindow = note.startBeat >= windowStart - EPS && note.startBeat < windowEnd - EPS;\n const accent = onsetInWindow ? 1 + metricWeight(note.startBeat, ts) / 3 : 1;\n const pc = pitchClass(note.pitch);\n weights[pc] = (weights[pc] ?? 0) + overlap * velocityFactor * accent;\n if (note.pitch < lowestPitch) {\n lowestPitch = note.pitch;\n }\n }\n let totalWeight = 0;\n let maxWeight = 0;\n for (const w of weights) {\n totalWeight += w;\n maxWeight = Math.max(maxWeight, w);\n }\n if (totalWeight <= EPS) {\n return null;\n }\n\n // Keep pitch classes with meaningful weight, strongest first.\n const ranked = weights\n .map((weight, pc) => ({ pc, weight }))\n .filter(({ weight }) => weight > 0)\n .sort((a, b) => b.weight - a.weight || a.pc - b.pc);\n const selected = ranked\n .filter(({ weight }) => weight >= maxWeight * NOISE_THRESHOLD_RATIO)\n .slice(0, MAX_DETECTION_PCS)\n .map(({ pc }) => pc);\n\n // Feed detection with the window's true bass as the lowest pitch so inversion\n // detection works; when the bass pc was filtered out as noise, anchor on the\n // heaviest pc instead so no spurious inversion is reported.\n const bassPc = pitchClass(lowestPitch);\n const anchorPc = selected.includes(bassPc) ? bassPc : (selected[0] ?? bassPc);\n const detectionPitches = selected.map((pc) => (pc === anchorPc ? pc : pc + 12));\n\n const matches = detectChord(detectionPitches);\n let bestMatch: ChordMatch | null = null;\n let bestScore = Number.NEGATIVE_INFINITY;\n for (const match of matches) {\n const score = scoreMatch(match, weights, maxWeight, key);\n if (score > bestScore) {\n bestScore = score;\n bestMatch = match;\n }\n }\n if (bestMatch) {\n const chord = makeChord(bestMatch.rootPc, bestMatch.quality, bestMatch.bassPc);\n return { chord, confidence: chordConfidence(chord, weights, totalWeight, bestMatch.exact) };\n }\n\n // Fallback: retry on only the top-weighted pitch classes.\n const fallback = detectChordBest(selected.slice(0, FALLBACK_PCS));\n if (fallback) {\n return { chord: fallback, confidence: chordConfidence(fallback, weights, totalWeight, false) };\n }\n return null;\n}\n\n/** Whether two chords are the same root, quality, and bass. */\nfunction sameChord(a: Chord, b: Chord): boolean {\n return a.rootPc === b.rootPc && a.quality === b.quality && a.bassPc === b.bassPc;\n}\n\n/**\n * Infer a chord timeline from raw multi-track notes.\n *\n * The span `[0, totalBeats)` is sliced into `harmonicRhythm`-beat windows; each\n * window's chord is inferred from a pitch-class weight histogram of the notes\n * overlapping it (weight = overlap duration x velocity x metric-accent bonus\n * for onsets in the window). Adjacent windows carrying the identical chord are\n * merged into one segment; windows with no notes produce no segment, so\n * `at(beat)` returns null there. Each segment carries a confidence in [0, 1]:\n * the fraction of the window weight explained by chord tones, reduced when the\n * match is inexact, and duration-weighted across merged windows.\n *\n * Notes with a zero or negative duration never sound, so they are dropped at\n * ingest: they contribute to neither the key inference, the span, nor any\n * window's histogram.\n *\n * @param notes The notes to analyze (any number of tracks, flattened).\n * @param opts Analysis options; see {@link ChordTimelineOptions}.\n * @returns The inferred timeline, the key used, and per-segment confidences.\n * @throws If `harmonicRhythm` is not positive.\n * @example\n * ```ts\n * import { chordTimelineFromNotes } from '@libraz/libcantus';\n * const notes = [\n * { pitch: 60, startBeat: 0, durationBeat: 2 }, // C\n * { pitch: 64, startBeat: 0, durationBeat: 2 }, // E\n * { pitch: 67, startBeat: 0, durationBeat: 2 }, // G\n * ];\n * const { timeline, key } = chordTimelineFromNotes(notes);\n * timeline.at(0); // the chord inferred over beat 0, or null\n * ```\n * @category Arrangement & Analysis\n */\nexport function chordTimelineFromNotes(\n notes: NoteEvent[],\n opts: ChordTimelineOptions = {},\n): ChordTimelineResult {\n const ts = opts.ts ?? parseTimeSignature('4/4');\n assertTimeSignature(ts);\n const harmonicRhythm = opts.harmonicRhythm ?? beatsPerBar(ts);\n assertRange(harmonicRhythm, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, 'harmonic rhythm');\n assertNoteEvents(notes, 'timeline notes', { allowNonPositiveDuration: true });\n // Zero/negative-length notes never sound; drop them before any inference.\n const soundingIndex = createNoteEventIndex(notes.filter((note) => note.durationBeat > 0));\n const sounding = soundingIndex.notes.map(({ note }) => note);\n const lastNoteEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);\n const totalBeats = opts.totalBeats ?? lastNoteEnd;\n assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, 'timeline totalBeats');\n const key = opts.key ?? detectKey(sounding.map((n) => n.pitch))[0]?.key ?? majorKey(0);\n\n const segments: ChordSegment[] = [];\n const segmentConfidence: number[] = [];\n const windowCount = Math.max(0, Math.ceil(totalBeats / harmonicRhythm - EPS));\n assertGenerationBudget(windowCount, 'timeline windows');\n const windowNotes: NoteEvent[][] = Array.from({ length: windowCount }, () => []);\n let memberships = 0;\n for (const indexed of soundingIndex.notes) {\n const first = Math.max(0, Math.floor(indexed.note.startBeat / harmonicRhythm));\n const lastExclusive = Math.min(windowCount, Math.ceil(indexed.endBeat / harmonicRhythm));\n memberships += Math.max(0, lastExclusive - first);\n assertGenerationBudget(memberships, 'timeline note-to-window memberships');\n for (let window = first; window < lastExclusive; window += 1) {\n windowNotes[window]?.push(indexed.note);\n }\n }\n for (let i = 0; i < windowCount; i += 1) {\n const start = i * harmonicRhythm;\n const end = Math.min(start + harmonicRhythm, totalBeats);\n const inferred = analyzeWindow(windowNotes[i] ?? [], start, end, ts, key);\n if (!inferred) {\n continue;\n }\n const last = segments[segments.length - 1];\n if (last && sameChord(last.chord, inferred.chord) && Math.abs(last.endBeat - start) < EPS) {\n // Merge into the previous segment, blending confidence by duration.\n const lastLength = last.endBeat - last.startBeat;\n const length = end - start;\n const lastConfidence = segmentConfidence[segmentConfidence.length - 1] ?? 0;\n segmentConfidence[segmentConfidence.length - 1] =\n (lastConfidence * lastLength + inferred.confidence * length) / (lastLength + length);\n last.endBeat = end;\n } else {\n segments.push({ startBeat: start, endBeat: end, chord: inferred.chord });\n segmentConfidence.push(inferred.confidence);\n }\n }\n\n return {\n timeline: { at: segmentLookup(segments), segments },\n key,\n segmentConfidence,\n };\n}\n\n/**\n * A cadence found between two consecutive timeline segments.\n *\n * @category Arrangement & Analysis\n */\nexport type CadenceHit = {\n /** The beat where the cadence arrives (the second chord's onset). */\n atBeat: number;\n type: Exclude<Cadence, null>;\n from: Chord;\n to: Chord;\n};\n\n/**\n * Find cadences between consecutive segments of a chord timeline.\n *\n * Each temporally adjacent segment pair is classified with\n * {@link detectCadence}; pairs forming no cadence are skipped. Segments\n * separated by a gap (a rest in the timeline) are not a chord-to-chord\n * progression, so they are never paired.\n *\n * @param timeline The chord timeline to scan.\n * @param key The prevailing key.\n * @returns The cadences found, in time order.\n * @example\n * ```ts\n * import { chordTimelineFromNotes, detectCadences } from '@libraz/libcantus';\n * const notes = [\n * { pitch: 67, startBeat: 0, durationBeat: 4 }, // G, a dominant\n * { pitch: 60, startBeat: 4, durationBeat: 4 }, // C, the tonic\n * ];\n * const { timeline, key } = chordTimelineFromNotes(notes);\n * const cadences = detectCadences(timeline, key);\n * ```\n * @category Arrangement & Analysis\n */\nexport function detectCadences(timeline: ChordTimeline, key: KeyScale): CadenceHit[] {\n const hits: CadenceHit[] = [];\n for (let i = 1; i < timeline.segments.length; i += 1) {\n const prev = timeline.segments[i - 1];\n const cur = timeline.segments[i];\n if (!prev || !cur) {\n continue;\n }\n if (Math.abs(cur.startBeat - prev.endBeat) > EPS) {\n continue; // A rest separates the chords; no cadential motion across it.\n }\n const type = detectCadence(prev.chord, cur.chord, key);\n if (type !== null) {\n hits.push({ atBeat: cur.startBeat, type, from: prev.chord, to: cur.chord });\n }\n }\n return hits;\n}\n","/**\n * Shared helpers for the arrangement-analysis modules: pooling every track's\n * sounding notes and partitioning a track into monophonic sub-voices, plus the\n * small geometric primitives both the track-role and tension passes rely on.\n */\n\nimport type { NoteEvent } from '../../core/types.js';\nimport type { VoiceNote } from '../voice/index.js';\nimport type { ArrangementTrack, TrackRole } from './tracks.js';\n\n/** Float tolerance for beat boundary comparisons. */\nexport const EPS = 1e-9;\n\n/** A note with the pitch that immediately preceded it in the same sub-voice. */\nexport type PreparedNote = {\n pitch: number;\n prevPitch?: number;\n startBeat: number;\n endBeat: number;\n};\n\n/** One monophonic sub-voice of a track: ordered notes plus sounding spans. */\nexport type PreparedVoice = {\n /** The sub-voice's notes in time order, each with a stable id. */\n voice: VoiceNote[];\n /** The same notes as sounding spans, each carrying its predecessor's pitch. */\n sounding: PreparedNote[];\n};\n\n/** A track prepared for analysis: resolved metadata plus monophonic sub-voices. */\nexport type PreparedTrack = {\n name: string;\n role: TrackRole;\n /**\n * The track's notes partitioned into monophonic sub-voices. Voice analysis\n * assumes one note at a time, so a polyphonic track (e.g. a 'harmony' track\n * playing block chords) is split here; a monophonic track yields exactly one\n * sub-voice.\n */\n voices: PreparedVoice[];\n};\n\n/** Whether a sounding span `[start, end)` covers a beat. */\nexport function covers(note: PreparedNote, beat: number): boolean {\n return beat >= note.startBeat - EPS && beat < note.endBeat - EPS;\n}\n\n/**\n * Every sounding note of every track pooled into one flat list. Zero- and\n * negative-length notes never sound, so they are dropped here (matching the\n * chord-inference ingest in the timeline module).\n */\nexport function poolNotes(tracks: ArrangementTrack[]): NoteEvent[] {\n const notes: NoteEvent[] = [];\n for (const track of tracks) {\n for (const note of track.notes) {\n if (note.durationBeat > 0) {\n notes.push(note);\n }\n }\n }\n return notes;\n}\n\n/**\n * Split a track's ordered notes into monophonic sub-voices.\n *\n * Greedy lane assignment: each note joins the first sub-voice that is free at\n * its onset (its last note has ended), otherwise it opens a new sub-voice. A\n * monophonic track therefore stays a single sub-voice, while block chords fan\n * out into one sub-voice per simultaneous note, keeping every sub-voice's\n * note-to-note adjacency genuinely melodic.\n */\nfunction splitIntoSubVoices(ordered: VoiceNote[]): VoiceNote[][] {\n const lanes: VoiceNote[][] = [];\n for (const note of ordered) {\n const lane = lanes.find((candidate) => {\n const last = candidate[candidate.length - 1];\n return last !== undefined && last.startBeat + last.durationBeat <= note.startBeat + EPS;\n });\n if (lane) {\n lane.push(note);\n } else {\n lanes.push([note]);\n }\n }\n return lanes;\n}\n\n/**\n * Resolve each track's metadata and derive its monophonic sub-voices.\n *\n * Notes are sorted by onset, then pitch, so ordering is deterministic, and ids\n * are assigned in that order. Zero- and negative-length notes never sound and\n * are dropped at ingest. Each track is then partitioned into monophonic\n * sub-voices (see {@link splitIntoSubVoices}); the predecessor pitch used for\n * suspension and parallel detection is the previous note of the same sub-voice,\n * never a simultaneous chord member.\n */\nexport function prepareTracks(tracks: ArrangementTrack[]): PreparedTrack[] {\n let nextId = 0;\n return tracks.map((track, index) => {\n const ordered: VoiceNote[] = track.notes\n .filter((note) => note.durationBeat > 0)\n .sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch)\n .map((note) => ({ ...note, id: nextId++ }));\n const voices: PreparedVoice[] = splitIntoSubVoices(ordered).map((voice) => ({\n voice,\n sounding: voice.map((note, i) => {\n const prev = i > 0 ? voice[i - 1] : undefined;\n const prevEnd = prev === undefined ? undefined : prev.startBeat + prev.durationBeat;\n const contiguous = prevEnd !== undefined && Math.abs(prevEnd - note.startBeat) <= EPS;\n return {\n pitch: note.pitch,\n prevPitch: contiguous ? prev?.pitch : undefined,\n startBeat: note.startBeat,\n endBeat: note.startBeat + note.durationBeat,\n };\n }),\n }));\n return {\n name: track.name ?? `track ${index + 1}`,\n role: track.role ?? 'other',\n voices,\n };\n });\n}\n","/**\n * Arrangement tension analysis: a coarse harmonic-tension curve sampled at\n * regular beats across a whole multi-track piece, combining the sounding\n * chord's harmonic function, the dissonance of the sounding notes, and their\n * registral span into a single normalized reading.\n */\n\nimport { isStrongBeat, parseTimeSignature, type TimeSignature } from '../../core/meter/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport {\n assertGenerationBudget,\n assertNoteEvents,\n assertRange,\n assertTimeSignature,\n} from '../../core/validation/index.js';\nimport { chordPitchClasses } from '../../theory/chord/index.js';\nimport { evaluateSafety, NoteSafety, type SafetyProfile } from '../../theory/safety/index.js';\nimport { functionOf } from '../functional/index.js';\nimport { type ChordTimeline, chordTimelineFromNotes } from '../timeline/index.js';\nimport { covers, EPS, type PreparedTrack, poolNotes, prepareTracks } from './internal.js';\nimport type { ArrangementOptions, ArrangementTrack } from './tracks.js';\n\n/**\n * A tension reading sampled at a beat.\n *\n * @category Arrangement & Analysis\n */\nexport type TensionPoint = {\n beat: number;\n /** Combined tension in [0, 1]. */\n tension: number;\n};\n\n/** Tension contributed by the sounding chord's harmonic function. */\nconst FUNCTION_TENSION: Record<'tonic' | 'subdominant' | 'dominant', number> = {\n tonic: 0,\n subdominant: 0.5,\n dominant: 1,\n};\n\n/** Weight of the harmonic-function term in the combined tension score. */\nconst FUNCTION_WEIGHT = 0.5;\n/** Weight of the non-chord-tone / dissonance term. */\nconst DISSONANCE_WEIGHT = 0.35;\n/** Weight of the registral-span term. */\nconst SPAN_WEIGHT = 0.15;\n/** Pitch span, in semitones, that saturates the span term. */\nconst SPAN_SATURATION = 24;\n\n/** Reduce a pitch to a pitch class in [0, 11]. */\nfunction pitchClass(pitch: number): number {\n return ((Math.trunc(pitch) % 12) + 12) % 12;\n}\n\n/** Clamp a value into [0, 1]. */\nfunction clamp01(value: number): number {\n return Math.min(1, Math.max(0, value));\n}\n\n/**\n * Sample the harmonic tension of an arrangement at regular beats.\n *\n * At each sample beat the tension combines three normalized terms:\n * - the sounding chord's harmonic function (dominant `1`, subdominant `0.5`,\n * tonic or no chord `0`), weighted {@link FUNCTION_WEIGHT};\n * - the dissonance of the sounding notes — the larger of the share of sounding\n * pitches that are not chord tones and the share that {@link evaluateSafety}\n * rates {@link NoteSafety.Dissonant} — weighted {@link DISSONANCE_WEIGHT};\n * - the registral span of the sounding notes, saturating at\n * {@link SPAN_SATURATION} semitones, weighted {@link SPAN_WEIGHT}.\n *\n * The weighted sum is clamped to [0, 1]. The harmony is inferred from all tracks\n * pooled together, so the result is deterministic and self-contained.\n *\n * @param tracks The tracks to sample.\n * @param opts Analysis options plus an optional `step` (default one beat).\n * @returns One {@link TensionPoint} per sampled beat, in beat order.\n * @throws If `step` is not positive.\n * @example\n * ```ts\n * import { tensionCurve } from '@libraz/libcantus';\n * const notes = [{ pitch: 60, startBeat: 0, durationBeat: 4 }];\n * const curve = tensionCurve([{ notes }], { step: 1 });\n * curve; // one { beat, tension } sample per beat, in beat order\n * ```\n * @category Arrangement & Analysis\n */\nexport function tensionCurve(\n tracks: ArrangementTrack[],\n opts: ArrangementOptions & { step?: number } = {},\n): TensionPoint[] {\n const ts = opts.ts ?? parseTimeSignature('4/4');\n assertTimeSignature(ts);\n assertGenerationBudget(tracks.length, 'arrangement tracks');\n for (let index = 0; index < tracks.length; index += 1) {\n assertNoteEvents(tracks[index]?.notes ?? [], `tracks[${index}].notes`, {\n allowNonPositiveDuration: true,\n });\n }\n const profile: SafetyProfile = opts.profile ?? 'pop';\n const step = opts.step ?? 1;\n assertRange(step, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, 'tension sampling step');\n\n const pooled = poolNotes(tracks);\n const totalBeats = pooled.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);\n const { timeline, key } = chordTimelineFromNotes(pooled, {\n key: opts.key,\n ts,\n harmonicRhythm: opts.harmonicRhythm,\n });\n\n const prepared = prepareTracks(tracks);\n const points: TensionPoint[] = [];\n const sampleCount = Math.max(0, Math.ceil(totalBeats / step - EPS));\n assertGenerationBudget(sampleCount, 'tension samples');\n for (let i = 0; i < sampleCount; i += 1) {\n const beat = i * step;\n points.push({ beat, tension: sampleTension(prepared, timeline, key, ts, profile, beat) });\n }\n return points;\n}\n\n/** All sounding notes across every track at a beat, tagged with their track. */\nfunction allSounding(prepared: PreparedTrack[], beat: number): { pitch: number; track: number }[] {\n const out: { pitch: number; track: number }[] = [];\n for (let t = 0; t < prepared.length; t += 1) {\n const track = prepared[t];\n if (!track) {\n continue;\n }\n for (const subVoice of track.voices) {\n for (const note of subVoice.sounding) {\n if (covers(note, beat)) {\n out.push({ pitch: note.pitch, track: t });\n }\n }\n }\n }\n return out;\n}\n\n/** Combine the harmonic, dissonance, and span terms at one sample beat. */\nfunction sampleTension(\n prepared: PreparedTrack[],\n timeline: ChordTimeline,\n key: KeyScale,\n ts: TimeSignature,\n profile: SafetyProfile,\n beat: number,\n): number {\n const sounding = allSounding(prepared, beat);\n if (sounding.length === 0) {\n return 0;\n }\n const chord = timeline.at(beat);\n const functionScore = chord ? FUNCTION_TENSION[functionOf(chord, key)] : 0;\n\n const chordPcs = chord ? new Set(chordPitchClasses(chord)) : null;\n const strongBeat = isStrongBeat(beat, ts);\n let nonChord = 0;\n let dissonant = 0;\n for (let i = 0; i < sounding.length; i += 1) {\n const voice = sounding[i];\n if (!voice) {\n continue;\n }\n // Non-chord-tone share only applies when a chord is sounding; at a timeline\n // gap there is no reference harmony, so vertical dissonance alone drives the\n // dissonance term (the chord function term is already 0 there).\n if (chordPcs && !chordPcs.has(pitchClass(voice.pitch))) {\n nonChord += 1;\n }\n const others = sounding.filter((_, j) => j !== i).map((s) => ({ pitch: s.pitch }));\n const result = evaluateSafety({\n profile,\n candidatePitch: voice.pitch,\n chord,\n key,\n otherVoices: others,\n strongBeat,\n });\n if (result.safety === NoteSafety.Dissonant) {\n dissonant += 1;\n }\n }\n const dissonanceScore = Math.max(nonChord, dissonant) / sounding.length;\n\n let low = Number.POSITIVE_INFINITY;\n let high = Number.NEGATIVE_INFINITY;\n for (const voice of sounding) {\n low = Math.min(low, voice.pitch);\n high = Math.max(high, voice.pitch);\n }\n const spanScore = Math.min(1, (high - low) / SPAN_SATURATION);\n\n return clamp01(\n FUNCTION_WEIGHT * functionScore + DISSONANCE_WEIGHT * dissonanceScore + SPAN_WEIGHT * spanScore,\n );\n}\n","import type { KeyScale, NoteEvent } from '../../core/types.js';\nimport { assertNoteEvents } from '../../core/validation/index.js';\nimport type { Chord } from '../../theory/chord/index.js';\nimport { chordPitchClasses, chordToneRole } from '../../theory/chord/index.js';\nimport {\n createsVerticalDissonance,\n isLeadingToneResolution,\n} from '../../theory/counterpoint/index.js';\nimport type { VoiceSnapshot } from '../../theory/safety/index.js';\n\n/**\n * A theory annotation attached to a note.\n *\n * @category Arrangement & Analysis\n */\nexport type TheoryLabel =\n | { kind: 'chordTone'; role: 'root' | 'third' | 'fifth' | 'sixth' | 'seventh' }\n | { kind: 'tension'; degree: 9 | 11 | 13 }\n | { kind: 'avoid' }\n | { kind: 'passing' }\n | { kind: 'neighbor' }\n | { kind: 'suspension'; type: 'sus4-3' | 'sus7-6' | 'sus9-8' | 'sus2-3'; resolveTo: number }\n | { kind: 'anticipation' }\n | { kind: 'escape' }\n | { kind: 'needsResolution'; resolveTo: number }\n | { kind: 'leadingTone'; resolveTo: number };\n\n/**\n * A note with its theory labels and a short rationale.\n *\n * @category Arrangement & Analysis\n */\nexport type AnalyzedNote = {\n noteId: number;\n labels: TheoryLabel[];\n rationale?: string;\n};\n\n/**\n * A single note in a monophonic voice: a {@link NoteEvent} with a stable id.\n *\n * @category Arrangement & Analysis\n */\nexport type VoiceNote = NoteEvent & { id: number };\n\n/** Float tolerance for beat comparisons. */\nconst EPS = 1e-9;\n\nfunction pitchClass(pitch: number): number {\n return ((Math.trunc(pitch) % 12) + 12) % 12;\n}\n\nfunction intervalAboveRoot(pitch: number, chord: Chord): number {\n return (((pitchClass(pitch) - pitchClass(chord.rootPc)) % 12) + 12) % 12;\n}\n\n/**\n * Interval class of a pitch above the actual sounding bass.\n *\n * Suspension figures (4-3, 7-6, 9-8) are named for the interval above the bass,\n * not the chord root, so this prefers the lowest pitch among the other sounding\n * voices when one lies below the note. Without a lower sounding voice it falls\n * back to the chord's bass pitch class (or root in root position).\n */\nfunction intervalAboveBass(pitch: number, others: VoiceSnapshot[], chord: Chord): number {\n let bass = Number.POSITIVE_INFINITY;\n for (const ov of others) {\n bass = Math.min(bass, ov.pitch);\n }\n if (Number.isFinite(bass) && bass < pitch) {\n return (((pitch - bass) % 12) + 12) % 12;\n }\n const ref = chord.bassPc ?? chord.rootPc;\n return (((pitchClass(pitch) - pitchClass(ref)) % 12) + 12) % 12;\n}\n\n/**\n * Map an interval class above the root to its extended-tension scale degree:\n * a flat/natural ninth (1 or 2) to 9, an eleventh (5 or 6) to 11, and a\n * thirteenth (8 or 9) to 13.\n */\nfunction tensionDegree(ic: number): 9 | 11 | 13 {\n if (ic === 1 || ic === 2) {\n return 9;\n }\n if (ic === 5 || ic === 6) {\n return 11;\n }\n return 13;\n}\n\nfunction isChordMember(pitch: number, chord: Chord | null): boolean {\n return chord ? chordPitchClasses(chord).includes(pitchClass(pitch)) : false;\n}\n\nfunction isStep(a: number, b: number): boolean {\n const d = Math.abs(a - b);\n return d === 1 || d === 2;\n}\n\n/**\n * Classify a suspension figure from the interval class above the sounding bass\n * (`ic`) and the resolution direction (`delta`, positive when resolving upward).\n */\nfunction suspensionType(ic: number, delta: number): TheoryLabel & { kind: 'suspension' } {\n let type: 'sus4-3' | 'sus7-6' | 'sus9-8' | 'sus2-3';\n if (delta > 0) {\n type = 'sus2-3';\n } else if (ic === 2) {\n type = 'sus9-8';\n } else if (ic === 10 || ic === 11) {\n type = 'sus7-6';\n } else {\n type = 'sus4-3';\n }\n return { kind: 'suspension', type, resolveTo: 0 };\n}\n\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 * Label every note of a voice with its theory roles.\n *\n * Each note is classified against the chord sounding at its beat. Chord tones\n * get a role label; non-chord tones are matched, in order, as suspensions\n * (prepared by an identical consonant pitch and resolving by step), passing\n * tones, neighbors, anticipations, and escape tones, then fall back to tension,\n * avoid, or an unresolved-dissonance label. Leading-tone resolutions are noted\n * additionally.\n *\n * The voice is expected to be monophonic (one note at a time). Notes sharing an\n * onset are treated as simultaneous cluster members, not melodic neighbors, so\n * they receive no melodic labels (suspension, passing, neighbor, anticipation,\n * escape) — only harmonic ones. Callers with truly polyphonic material should\n * split it into monophonic sub-voices first, as `analyzeArrangement` does.\n *\n * @param voice The monophonic voice, in time order.\n * @param chordAtBeat Chord sounding at a given beat, or null.\n * @param key Key context for leading-tone detection.\n * @param otherVoicesAtBeat Other sounding voices at a given beat.\n * @returns One annotation per input note.\n * @example\n * ```ts\n * import { analyzeVoice, makeChord, majorKey } from '@libraz/libcantus';\n * const voice = [\n * { id: 0, pitch: 60, startBeat: 0, durationBeat: 1 },\n * { id: 1, pitch: 62, startBeat: 1, durationBeat: 1 },\n * { id: 2, pitch: 64, startBeat: 2, durationBeat: 1 },\n * ];\n * const cMajor = makeChord(0, 'maj');\n * const labels = analyzeVoice(voice, () => cMajor, majorKey(0), () => []);\n * labels; // one AnalyzedNote per input note, in the same order\n * ```\n * @category Arrangement & Analysis\n */\nexport function analyzeVoice(\n voice: VoiceNote[],\n chordAtBeat: (beat: number) => Chord | null,\n key: KeyScale,\n otherVoicesAtBeat: (beat: number) => VoiceSnapshot[],\n): AnalyzedNote[] {\n assertNoteEvents(voice, 'voice notes', { allowNonPositiveDuration: true });\n const result: AnalyzedNote[] = [];\n\n for (let i = 0; i < voice.length; i += 1) {\n const note = voice[i];\n if (!note) {\n continue;\n }\n const prevNote = i > 0 ? voice[i - 1] : undefined;\n const nextNote = i + 1 < voice.length ? voice[i + 1] : undefined;\n // A neighboring note sharing this note's onset is a simultaneous cluster\n // member, not a melodic predecessor/successor; treating it as one would\n // fabricate passing/suspension figures, so it is dropped here.\n const prevEnd =\n prevNote === undefined\n ? Number.NEGATIVE_INFINITY\n : prevNote.startBeat + prevNote.durationBeat;\n const noteEnd = note.startBeat + note.durationBeat;\n const prev =\n prevNote !== undefined && Math.abs(prevEnd - note.startBeat) <= EPS ? prevNote : undefined;\n const next =\n nextNote !== undefined && Math.abs(noteEnd - nextNote.startBeat) <= EPS\n ? nextNote\n : undefined;\n const chord = chordAtBeat(note.startBeat);\n const labels: TheoryLabel[] = [];\n const member = isChordMember(note.pitch, chord);\n let handled = false;\n\n if (chord && member) {\n const role = chordToneRole(note.pitch, chord);\n if (role) {\n labels.push({ kind: 'chordTone', role });\n } else {\n const ic = intervalAboveRoot(note.pitch, chord);\n labels.push({ kind: 'tension', degree: tensionDegree(ic) });\n }\n handled = true;\n }\n\n if (!handled && chord && prev && next) {\n const prevChord = chordAtBeat(prev.startBeat);\n const prepared = prev.pitch === note.pitch && isChordMember(prev.pitch, prevChord);\n const resolves = isStep(next.pitch, note.pitch);\n const others = otherVoicesAtBeat(note.startBeat);\n const verticallyDissonant = others.some((ov) =>\n createsVerticalDissonance(note.pitch, ov.pitch, true),\n );\n if (prepared && verticallyDissonant && resolves) {\n const ic = intervalAboveBass(note.pitch, others, chord);\n const sus = suspensionType(ic, next.pitch - note.pitch);\n sus.resolveTo = next.pitch;\n labels.push(sus);\n handled = true;\n }\n }\n\n if (!handled && chord && prev && next) {\n const prevMember = isChordMember(prev.pitch, chordAtBeat(prev.startBeat));\n const nextMember = isChordMember(next.pitch, chordAtBeat(next.startBeat));\n const up = note.pitch - prev.pitch > 0;\n const contInto = next.pitch - note.pitch > 0;\n const sameDir = note.pitch !== prev.pitch && next.pitch !== note.pitch && up === contInto;\n if (\n prevMember &&\n nextMember &&\n !member &&\n isStep(note.pitch, prev.pitch) &&\n isStep(next.pitch, note.pitch) &&\n sameDir\n ) {\n labels.push({ kind: 'passing' });\n handled = true;\n }\n }\n\n if (\n !handled &&\n chord &&\n prev &&\n next &&\n !member &&\n prev.pitch === next.pitch &&\n isChordMember(prev.pitch, chordAtBeat(prev.startBeat)) &&\n isStep(note.pitch, prev.pitch)\n ) {\n labels.push({ kind: 'neighbor' });\n handled = true;\n }\n\n if (!handled && chord && next && !member && note.pitch === next.pitch) {\n const nextChord = chordAtBeat(next.startBeat);\n if (isChordMember(next.pitch, nextChord)) {\n labels.push({ kind: 'anticipation' });\n handled = true;\n }\n }\n\n if (!handled && chord && prev && next && !member) {\n const prevMember = isChordMember(prev.pitch, chordAtBeat(prev.startBeat));\n const nextMember = isChordMember(next.pitch, chordAtBeat(next.startBeat));\n const stepFromPrev = isStep(note.pitch, prev.pitch);\n const leapToNext = Math.abs(next.pitch - note.pitch) >= 3;\n const opposite = note.pitch - prev.pitch > 0 !== next.pitch - note.pitch > 0;\n if (prevMember && nextMember && stepFromPrev && leapToNext && opposite) {\n labels.push({ kind: 'escape' });\n handled = true;\n }\n }\n\n if (!handled && chord && !member) {\n const ic = intervalAboveRoot(note.pitch, chord);\n const isTension = ic === 2 || ic === 5 || ic === 9;\n const avoid =\n (ic === 5 && chord.intervals.includes(4) && !chord.intervals.includes(5)) ||\n (ic === 11 && chord.intervals.includes(4) && chord.intervals.includes(10));\n if (avoid) {\n labels.push({ kind: 'avoid' });\n const resolveTo = stepResolution(note.pitch, chord);\n if (resolveTo !== undefined) {\n labels.push({ kind: 'needsResolution', resolveTo });\n }\n } else if (isTension) {\n labels.push({ kind: 'tension', degree: tensionDegree(ic) });\n } else {\n const resolveTo = stepResolution(note.pitch, chord);\n if (resolveTo !== undefined) {\n labels.push({ kind: 'needsResolution', resolveTo });\n }\n }\n }\n\n if (next && isLeadingToneResolution(note.pitch, next.pitch, key)) {\n labels.push({ kind: 'leadingTone', resolveTo: next.pitch });\n }\n\n result.push({ noteId: note.id, labels, rationale: describe(labels) });\n }\n\n return result;\n}\n\n/** Build a short rationale from a note's primary label. */\nfunction describe(labels: TheoryLabel[]): string {\n const primary = labels[0];\n if (!primary) {\n return 'Unclassified note';\n }\n switch (primary.kind) {\n case 'chordTone':\n return `Chord ${primary.role}`;\n case 'tension':\n return `${primary.degree}th tension`;\n case 'avoid':\n return 'Avoid note';\n case 'passing':\n return 'Passing tone between chord tones';\n case 'neighbor':\n return 'Neighbor tone';\n case 'suspension':\n return `Suspension (${primary.type})`;\n case 'anticipation':\n return 'Anticipation of the next chord';\n case 'escape':\n return 'Escape tone';\n case 'needsResolution':\n return 'Unresolved dissonance';\n case 'leadingTone':\n return 'Leading tone resolving to the tonic';\n }\n}\n","/**\n * Arrangement track-role analysis: a single harmony is inferred from every\n * track's notes pooled together, each track is then analysed against that shared\n * harmony, and notes that clash with the sounding chord are collected as\n * conflicts.\n */\n\nimport { isStrongBeat, parseTimeSignature, type TimeSignature } from '../../core/meter/index.js';\nimport type { KeyScale, NoteEvent } from '../../core/types.js';\nimport {\n assertGenerationBudget,\n assertNoteEvents,\n assertTimeSignature,\n} from '../../core/validation/index.js';\nimport {\n evaluateSafety,\n NoteSafety,\n type SafetyProfile,\n type VoiceSnapshot,\n} from '../../theory/safety/index.js';\nimport {\n type CadenceHit,\n type ChordTimeline,\n chordTimelineFromNotes,\n detectCadences,\n} from '../timeline/index.js';\nimport { type AnalyzedNote, analyzeVoice, type VoiceNote } from '../voice/index.js';\nimport { covers, EPS, type PreparedTrack, poolNotes, prepareTracks } from './internal.js';\n\n/**\n * The musical role a track plays in the arrangement.\n *\n * @category Arrangement & Analysis\n */\nexport type TrackRole = 'melody' | 'harmony' | 'bass' | 'other';\n\n/**\n * One input track: its notes plus optional name and role.\n *\n * @category Arrangement & Analysis\n */\nexport type ArrangementTrack = {\n name?: string;\n role?: TrackRole;\n notes: NoteEvent[];\n};\n\n/**\n * A track after analysis: its resolved name, role, and per-note annotations.\n *\n * @category Arrangement & Analysis\n */\nexport type TrackAnalysis = {\n name: string;\n role: TrackRole;\n /** One {@link AnalyzedNote} per note, labelled against the inferred chords. */\n notes: AnalyzedNote[];\n};\n\n/**\n * A note that clashes with the harmony sounding beneath it.\n *\n * @category Arrangement & Analysis\n */\nexport type Conflict = {\n beat: number;\n trackName: string;\n pitch: number;\n /** The clash severity: {@link NoteSafety.Warning} or {@link NoteSafety.Dissonant}. */\n safety: NoteSafety;\n /** {@link import('../safety/index.js').SafetyResult.reasons} bitmask. */\n reasons: number;\n /** Preferred stepwise resolution, when the safety evaluator provides one. */\n resolveTo?: number;\n /** Nearby fully safe replacement pitches, nearest first. */\n suggestions?: number[];\n rationale?: string;\n};\n\n/**\n * The full result of {@link analyzeArrangement}.\n *\n * @category Arrangement & Analysis\n */\nexport type ArrangementAnalysis = {\n key: KeyScale;\n timeline: ChordTimeline;\n segmentConfidence: number[];\n cadences: CadenceHit[];\n tracks: TrackAnalysis[];\n /** Notes clashing with the sounding harmony, worst severity first. */\n conflicts: Conflict[];\n};\n\n/**\n * Options controlling {@link analyzeArrangement} and {@link tensionCurve}.\n *\n * @category Arrangement & Analysis\n */\nexport type ArrangementOptions = {\n /** Key context; inferred from the pooled notes when omitted. */\n key?: KeyScale;\n /**\n * Time signature; defaults to 4/4.\n *\n * @defaultValue `4/4`\n */\n ts?: TimeSignature;\n /**\n * Chord-slot length in beats; defaults to one bar of `ts`.\n *\n * @defaultValue one bar of `ts`\n */\n harmonicRhythm?: number;\n /**\n * Safety profile used for conflict detection; defaults to `pop`.\n *\n * @defaultValue `'pop'`\n */\n profile?: SafetyProfile;\n};\n\n/**\n * Sounding pitches of every sub-voice at a beat, excluding one sub-voice.\n *\n * Only the sub-voice under analysis is excluded — sibling sub-voices of the\n * same track are included, so dissonant clusters inside a single polyphonic\n * track are still detected.\n */\nfunction otherVoicesSounding(\n prepared: PreparedTrack[],\n excludeTrack: number,\n excludeVoice: number,\n beat: number,\n): VoiceSnapshot[] {\n const out: VoiceSnapshot[] = [];\n for (let t = 0; t < prepared.length; t += 1) {\n const track = prepared[t];\n if (!track) {\n continue;\n }\n for (let v = 0; v < track.voices.length; v += 1) {\n if (t === excludeTrack && v === excludeVoice) {\n continue;\n }\n const subVoice = track.voices[v];\n if (!subVoice) {\n continue;\n }\n for (const note of subVoice.sounding) {\n if (!covers(note, beat)) {\n continue;\n }\n const snap: VoiceSnapshot = { pitch: note.pitch };\n // Motion reasons compare one real transition shared by both voices.\n // A voice attacking exactly here contributes its adjacent predecessor;\n // a sustained voice contributes the same pitch (oblique motion).\n const previous = Math.abs(note.startBeat - beat) <= EPS ? note.prevPitch : note.pitch;\n if (previous !== undefined) {\n snap.prevPitch = previous;\n }\n out.push(snap);\n }\n }\n }\n return out;\n}\n\n/**\n * The beats at which a note must be checked against the harmony: its onset,\n * plus the onset of every later chord segment the note sustains across. Each\n * crossed segment is visited exactly once (at its own start), so a boundary\n * beat is never evaluated twice for the same note.\n */\nfunction evaluationBeats(note: VoiceNote, timeline: ChordTimeline): number[] {\n const noteEnd = note.startBeat + note.durationBeat;\n const beats = [note.startBeat];\n for (const segment of timeline.segments) {\n if (segment.startBeat > note.startBeat + EPS && segment.startBeat < noteEnd - EPS) {\n beats.push(segment.startBeat);\n }\n }\n return beats;\n}\n\n/**\n * Analyse a whole arrangement against a single inferred harmony.\n *\n * The chord timeline and key are inferred from the pooled notes of every track\n * (see {@link chordTimelineFromNotes}); pooling all voices is robust even when\n * roles are absent or a track doubles the harmony, so it is preferred over\n * deriving the harmony from a subset. Zero- and negative-length notes never\n * sound, so they are dropped at ingest and appear in neither the annotations\n * nor the conflicts.\n *\n * Voice analysis assumes one monophonic voice at a time, so each track is\n * first partitioned into monophonic sub-voices (a polyphonic block-chord track\n * fans out into one sub-voice per simultaneous note; a monophonic track is\n * unaffected). Each sub-voice is labelled note-by-note against the harmony\n * with {@link analyzeVoice}, seeing every other sub-voice — including siblings\n * within the same track — as its accompaniment. Every note is then re-checked\n * with {@link evaluateSafety} at its onset and again at each chord change it\n * sustains across, so a held note that clashes with a later chord is caught:\n * evaluations that are not {@link NoteSafety.Safe} become conflicts, one per\n * clashing beat, sorted worst severity first and then by beat.\n *\n * @param tracks The tracks to analyse.\n * @param opts Analysis options; see {@link ArrangementOptions}.\n * @returns The inferred harmony, per-track annotations, cadences, and conflicts.\n * @example\n * ```ts\n * import { analyzeArrangement } from '@libraz/libcantus';\n * const melody = [\n * { pitch: 60, startBeat: 0, durationBeat: 2 },\n * { pitch: 67, startBeat: 2, durationBeat: 2 },\n * ];\n * const { key, conflicts } = analyzeArrangement([{ role: 'melody', notes: melody }]);\n * conflicts; // notes clashing with the inferred harmony, worst severity first\n * ```\n * @category Arrangement & Analysis\n */\nexport function analyzeArrangement(\n tracks: ArrangementTrack[],\n opts: ArrangementOptions = {},\n): ArrangementAnalysis {\n const ts = opts.ts ?? parseTimeSignature('4/4');\n assertTimeSignature(ts);\n assertGenerationBudget(tracks.length, 'arrangement tracks');\n for (let index = 0; index < tracks.length; index += 1) {\n assertNoteEvents(tracks[index]?.notes ?? [], `tracks[${index}].notes`, {\n allowNonPositiveDuration: true,\n });\n }\n const profile: SafetyProfile = opts.profile ?? 'pop';\n const pooled = poolNotes(tracks);\n\n const { timeline, key, segmentConfidence } = chordTimelineFromNotes(pooled, {\n key: opts.key,\n ts,\n harmonicRhythm: opts.harmonicRhythm,\n });\n const cadences = detectCadences(timeline, key);\n const prepared = prepareTracks(tracks);\n\n const trackAnalyses: TrackAnalysis[] = [];\n const conflicts: Conflict[] = [];\n\n for (let t = 0; t < prepared.length; t += 1) {\n const track = prepared[t];\n if (!track) {\n continue;\n }\n const notes: AnalyzedNote[] = [];\n for (let v = 0; v < track.voices.length; v += 1) {\n const subVoice = track.voices[v];\n if (!subVoice) {\n continue;\n }\n notes.push(\n ...analyzeVoice(subVoice.voice, timeline.at, key, (beat) =>\n otherVoicesSounding(prepared, t, v, beat),\n ),\n );\n\n for (let noteIndex = 0; noteIndex < subVoice.voice.length; noteIndex += 1) {\n const note = subVoice.voice[noteIndex];\n const preparedNote = subVoice.sounding[noteIndex];\n if (note === undefined || preparedNote === undefined) {\n continue;\n }\n for (const beat of evaluationBeats(note, timeline)) {\n const atOnset = Math.abs(beat - note.startBeat) <= EPS;\n const result = evaluateSafety({\n profile,\n candidatePitch: note.pitch,\n prevPitch: atOnset ? preparedNote.prevPitch : note.pitch,\n chord: timeline.at(beat),\n key,\n otherVoices: otherVoicesSounding(prepared, t, v, beat),\n strongBeat: isStrongBeat(beat, ts),\n });\n if (result.safety !== NoteSafety.Safe) {\n const conflict: Conflict = {\n beat,\n trackName: track.name,\n pitch: note.pitch,\n safety: result.safety,\n reasons: result.reasons,\n };\n if (result.rationale !== undefined) {\n conflict.rationale = result.rationale;\n }\n if (result.resolveTo !== undefined) {\n conflict.resolveTo = result.resolveTo;\n }\n if (result.suggestions !== undefined) {\n conflict.suggestions = [...result.suggestions];\n }\n conflicts.push(conflict);\n }\n }\n }\n }\n // Sub-voice results interleave; restore the track's onset-then-pitch order.\n notes.sort((a, b) => a.noteId - b.noteId);\n trackAnalyses.push({ name: track.name, role: track.role, notes });\n }\n\n conflicts.sort((a, b) => b.safety - a.safety || a.beat - b.beat);\n\n return { key, timeline, segmentConfidence, cadences, tracks: trackAnalyses, conflicts };\n}\n"],"mappings":"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","names":["EPS","pitchClass","EPS","EPS","pitchClass","EPS"]}
@@ -0,0 +1,202 @@
1
+ import {
2
+ assertFiniteNumber,
3
+ assertInteger,
4
+ assertNoteEvents,
5
+ assertPositiveInt,
6
+ assertRange,
7
+ assertTimeSignature
8
+ } from "./chunk-FBQK3ZQN.js";
9
+
10
+ // src/core/meter/index.ts
11
+ var EPS = 1e-9;
12
+ function isMultiple(value, unit) {
13
+ if (unit === 0) {
14
+ return false;
15
+ }
16
+ const ratio = value / unit;
17
+ return Math.abs(ratio - Math.round(ratio)) < EPS;
18
+ }
19
+ function parseTimeSignature(text) {
20
+ const match = /^\s*(\d+)\s*\/\s*(\d+)\s*$/.exec(text);
21
+ if (!match) {
22
+ throw new Error(`Invalid time signature: ${text}`);
23
+ }
24
+ const numerator = Number.parseInt(match[1] ?? "", 10);
25
+ const denominator = Number.parseInt(match[2] ?? "", 10);
26
+ if (!Number.isSafeInteger(numerator) || !Number.isSafeInteger(denominator) || numerator <= 0 || denominator <= 0) {
27
+ throw new Error(`Invalid time signature: ${text}`);
28
+ }
29
+ return { numerator, denominator };
30
+ }
31
+ function formatTimeSignature(ts) {
32
+ assertTimeSignature(ts);
33
+ return `${ts.numerator}/${ts.denominator}`;
34
+ }
35
+ function isCompound(ts) {
36
+ assertTimeSignature(ts);
37
+ return ts.numerator % 3 === 0 && ts.numerator > 3;
38
+ }
39
+ function unitBeats(ts) {
40
+ return 4 / ts.denominator;
41
+ }
42
+ function beatsPerBar(ts) {
43
+ assertTimeSignature(ts);
44
+ return ts.numerator * unitBeats(ts);
45
+ }
46
+ function pulseBeats(ts) {
47
+ return isCompound(ts) ? 3 * unitBeats(ts) : unitBeats(ts);
48
+ }
49
+ function pulsesPerBar(ts) {
50
+ return beatsPerBar(ts) / pulseBeats(ts);
51
+ }
52
+ function isValidGrouping(grouping, pulses) {
53
+ if (grouping.length === 0) {
54
+ return false;
55
+ }
56
+ let sum = 0;
57
+ for (const g of grouping) {
58
+ if (!Number.isInteger(g) || g <= 0) {
59
+ return false;
60
+ }
61
+ sum += g;
62
+ }
63
+ return sum === pulses;
64
+ }
65
+ function isGroupHead(grouping, pulseIndex) {
66
+ let acc = 0;
67
+ for (const g of grouping) {
68
+ if (acc === pulseIndex) {
69
+ return true;
70
+ }
71
+ acc += g;
72
+ }
73
+ return false;
74
+ }
75
+ function beatToBarPosition(beatInQuarters, ts) {
76
+ assertFiniteNumber(beatInQuarters, "beat");
77
+ const barLen = beatsPerBar(ts);
78
+ const bar = Math.floor(beatInQuarters / barLen);
79
+ return { bar, beat: beatInQuarters - bar * barLen };
80
+ }
81
+ function barPositionToBeat(pos, ts) {
82
+ assertInteger(pos.bar, "bar position bar");
83
+ assertFiniteNumber(pos.beat, "bar position beat");
84
+ return pos.bar * beatsPerBar(ts) + pos.beat;
85
+ }
86
+ function metricWeight(beatInQuarters, ts) {
87
+ assertFiniteNumber(beatInQuarters, "beat");
88
+ assertTimeSignature(ts);
89
+ const { beat } = beatToBarPosition(beatInQuarters, ts);
90
+ const pulse = pulseBeats(ts);
91
+ if (!isMultiple(beat, pulse)) {
92
+ return 0;
93
+ }
94
+ const pulses = pulsesPerBar(ts);
95
+ const pulseIndex = Math.round(beat / pulse) % pulses;
96
+ if (pulseIndex === 0) {
97
+ return 3;
98
+ }
99
+ const grouping = ts.grouping;
100
+ if (grouping !== void 0) {
101
+ if (!isValidGrouping(grouping, pulses)) {
102
+ throw new Error(
103
+ `Invalid grouping [${grouping.join(", ")}] for ${formatTimeSignature(ts)}: entries must be positive integers summing to ${pulses}`
104
+ );
105
+ }
106
+ return isGroupHead(grouping, pulseIndex) ? 2 : 1;
107
+ }
108
+ if (pulses % 2 === 0 && pulseIndex === pulses / 2) {
109
+ return 2;
110
+ }
111
+ return 1;
112
+ }
113
+ function isStrongBeat(beatInQuarters, ts) {
114
+ return metricWeight(beatInQuarters, ts) >= 2;
115
+ }
116
+ function tuplet(totalBeats, count) {
117
+ assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, "tuplet total beats");
118
+ assertPositiveInt(count, "tuplet count");
119
+ return new Array(count).fill(totalBeats / count);
120
+ }
121
+
122
+ // src/core/event-index/index.ts
123
+ var EPS2 = 1e-9;
124
+ function upperBound(values, beat) {
125
+ let low = 0;
126
+ let high = values.length;
127
+ while (low < high) {
128
+ const middle = Math.floor((low + high) / 2);
129
+ if ((values[middle]?.note.startBeat ?? Number.POSITIVE_INFINITY) <= beat) {
130
+ low = middle + 1;
131
+ } else {
132
+ high = middle;
133
+ }
134
+ }
135
+ return low;
136
+ }
137
+ function createNoteEventIndex(events, options = {}) {
138
+ assertNoteEvents(events, "note events", options);
139
+ const notes = events.map((note, originalIndex) => ({
140
+ note,
141
+ originalIndex,
142
+ endBeat: note.startBeat + note.durationBeat
143
+ })).sort((a, b) => a.note.startBeat - b.note.startBeat || a.originalIndex - b.originalIndex);
144
+ const prefixMaxEnd = [];
145
+ let maxEnd = Number.NEGATIVE_INFINITY;
146
+ for (const indexed of notes) {
147
+ maxEnd = Math.max(maxEnd, indexed.endBeat);
148
+ prefixMaxEnd.push(maxEnd);
149
+ }
150
+ return {
151
+ notes,
152
+ at(beat) {
153
+ let index = upperBound(notes, beat + EPS2) - 1;
154
+ while (index >= 0) {
155
+ if ((prefixMaxEnd[index] ?? Number.NEGATIVE_INFINITY) <= beat + EPS2) {
156
+ return void 0;
157
+ }
158
+ const indexed = notes[index];
159
+ if (indexed && indexed.note.durationBeat > 0 && indexed.note.startBeat - EPS2 <= beat && beat < indexed.endBeat - EPS2) {
160
+ return indexed;
161
+ }
162
+ index -= 1;
163
+ }
164
+ return void 0;
165
+ },
166
+ attacksAt(beat) {
167
+ const firstAfter = upperBound(notes, beat + EPS2);
168
+ const candidate = notes[firstAfter - 1];
169
+ return candidate !== void 0 && Math.abs(candidate.note.startBeat - beat) < EPS2;
170
+ },
171
+ onsetsBetween(startBeat, endBeat) {
172
+ const result = [];
173
+ let index = upperBound(notes, startBeat + EPS2);
174
+ while (index < notes.length) {
175
+ const onset = notes[index]?.note.startBeat;
176
+ if (onset === void 0 || onset >= endBeat - EPS2) {
177
+ break;
178
+ }
179
+ if (result[result.length - 1] !== onset) {
180
+ result.push(onset);
181
+ }
182
+ index += 1;
183
+ }
184
+ return result;
185
+ }
186
+ };
187
+ }
188
+
189
+ export {
190
+ parseTimeSignature,
191
+ formatTimeSignature,
192
+ isCompound,
193
+ beatsPerBar,
194
+ pulsesPerBar,
195
+ beatToBarPosition,
196
+ barPositionToBeat,
197
+ metricWeight,
198
+ isStrongBeat,
199
+ tuplet,
200
+ createNoteEventIndex
201
+ };
202
+ //# sourceMappingURL=chunk-I5R7ZBJS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/meter/index.ts","../src/core/event-index/index.ts"],"sourcesContent":["/**\n * Meter and time signatures: bar/beat arithmetic, simple vs. compound meters,\n * metric-accent hierarchy, and tuplet subdivision.\n *\n * Positions and durations are measured in quarter-note beats, matching the rest\n * of the library's beat convention (four quarter-note beats per bar in 4/4).\n */\n\nimport {\n assertFiniteNumber,\n assertInteger,\n assertPositiveInt,\n assertRange,\n assertTimeSignature,\n} from '../validation/index.js';\n\n/**\n * A time signature as a numerator over a note-value denominator.\n *\n * @category Rhythm & Meter\n */\nexport type TimeSignature = {\n numerator: number;\n denominator: number;\n /**\n * Optional additive grouping of the bar's main pulses, as the felt-beat\n * lengths in pulses — e.g. `[2, 2, 3]` for a 2+2+3 reading of 7/8, or\n * `[3, 2]` for 5/8. The entries must be positive integers summing to\n * {@link pulsesPerBar}. When present, the head pulse of each group (other\n * than the downbeat) is treated as a secondary strong pulse by\n * {@link metricWeight}/{@link isStrongBeat}; when absent, all main pulses\n * are weighted equally as flat, evenly divided pulses.\n */\n grouping?: number[];\n};\n\n/**\n * A position expressed as a bar index and a quarter-note offset within the bar.\n *\n * @category Rhythm & Meter\n */\nexport type BarPosition = {\n bar: number;\n /** Quarter-note offset from the start of the bar. */\n beat: number;\n};\n\nconst EPS = 1e-9;\n\n/** Whether `value` is an integer multiple of `unit` (within a float tolerance). */\nfunction isMultiple(value: number, unit: number): boolean {\n if (unit === 0) {\n return false;\n }\n const ratio = value / unit;\n return Math.abs(ratio - Math.round(ratio)) < EPS;\n}\n\n/**\n * Parse a time signature such as `\"4/4\"` or `\"6/8\"`.\n *\n * @param text The signature text.\n * @returns The parsed time signature.\n * @throws If the text is not `n/d` with positive integers.\n * @example\n * ```ts\n * import { parseTimeSignature } from '@libraz/libcantus';\n * parseTimeSignature('6/8'); // { numerator: 6, denominator: 8 }\n * ```\n * @category Rhythm & Meter\n */\nexport function parseTimeSignature(text: string): TimeSignature {\n const match = /^\\s*(\\d+)\\s*\\/\\s*(\\d+)\\s*$/.exec(text);\n if (!match) {\n throw new Error(`Invalid time signature: ${text}`);\n }\n const numerator = Number.parseInt(match[1] ?? '', 10);\n const denominator = Number.parseInt(match[2] ?? '', 10);\n if (\n !Number.isSafeInteger(numerator) ||\n !Number.isSafeInteger(denominator) ||\n numerator <= 0 ||\n denominator <= 0\n ) {\n throw new Error(`Invalid time signature: ${text}`);\n }\n return { numerator, denominator };\n}\n\n/**\n * Render a time signature as `\"n/d\"`.\n *\n * @category Rhythm & Meter\n */\nexport function formatTimeSignature(ts: TimeSignature): string {\n assertTimeSignature(ts);\n return `${ts.numerator}/${ts.denominator}`;\n}\n\n/**\n * Whether a meter is compound: its main pulses each divide into three, as in\n * 6/8, 9/8, 12/8, or 6/4. Compound meters are those whose numerator is a\n * multiple of three greater than three, independent of the denominator. Meters\n * like 3/8 and 3/4 (simple triples, numerator 3) are not compound.\n *\n * @param ts The time signature.\n * @returns True for compound meters.\n * @category Rhythm & Meter\n */\nexport function isCompound(ts: TimeSignature): boolean {\n assertTimeSignature(ts);\n return ts.numerator % 3 === 0 && ts.numerator > 3;\n}\n\n/** Length of one denominator unit in quarter-note beats. */\nfunction unitBeats(ts: TimeSignature): number {\n return 4 / ts.denominator;\n}\n\n/**\n * Length of a bar in quarter-note beats.\n *\n * @param ts The time signature.\n * @returns The bar length in quarter notes.\n * @category Rhythm & Meter\n */\nexport function beatsPerBar(ts: TimeSignature): number {\n assertTimeSignature(ts);\n return ts.numerator * unitBeats(ts);\n}\n\n/** Length of one main pulse (a dotted value in compound meters) in quarter notes. */\nfunction pulseBeats(ts: TimeSignature): number {\n return isCompound(ts) ? 3 * unitBeats(ts) : unitBeats(ts);\n}\n\n/**\n * Number of main pulses (felt beats) per bar: the numerator for simple meters,\n * a third of it for compound meters.\n *\n * For additive/irregular meters such as 7/8 or 5/8 this counts the raw pulses\n * (7 and 5 respectively), which are otherwise felt as flat, evenly divided and\n * equally accented pulses. To recover a 2+2+3 or 3+2 felt-beat grouping, set\n * {@link TimeSignature.grouping}; {@link metricWeight} then accents each\n * group's head pulse.\n *\n * @param ts The time signature.\n * @returns The pulse count per bar.\n * @category Rhythm & Meter\n */\nexport function pulsesPerBar(ts: TimeSignature): number {\n return beatsPerBar(ts) / pulseBeats(ts);\n}\n\n/**\n * Whether `grouping` is a valid additive grouping for a bar of `pulses` main\n * pulses: a non-empty list of positive integers summing to `pulses`.\n */\nfunction isValidGrouping(grouping: number[], pulses: number): boolean {\n if (grouping.length === 0) {\n return false;\n }\n let sum = 0;\n for (const g of grouping) {\n if (!Number.isInteger(g) || g <= 0) {\n return false;\n }\n sum += g;\n }\n return sum === pulses;\n}\n\n/** Whether `pulseIndex` is the head pulse of one of the additive groups. */\nfunction isGroupHead(grouping: number[], pulseIndex: number): boolean {\n let acc = 0;\n for (const g of grouping) {\n if (acc === pulseIndex) {\n return true;\n }\n acc += g;\n }\n return false;\n}\n\n/**\n * Convert an absolute quarter-note position to a bar index and in-bar offset.\n *\n * @param beatInQuarters Absolute position in quarter-note beats.\n * @param ts The time signature.\n * @returns The bar and in-bar quarter-note offset.\n * @example\n * ```ts\n * import { parseTimeSignature, beatToBarPosition } from '@libraz/libcantus';\n * const ts = parseTimeSignature('4/4');\n * beatToBarPosition(5, ts); // { bar: 1, beat: 1 }\n * ```\n * @category Rhythm & Meter\n */\nexport function beatToBarPosition(beatInQuarters: number, ts: TimeSignature): BarPosition {\n assertFiniteNumber(beatInQuarters, 'beat');\n const barLen = beatsPerBar(ts);\n const bar = Math.floor(beatInQuarters / barLen);\n return { bar, beat: beatInQuarters - bar * barLen };\n}\n\n/**\n * Convert a bar index and in-bar offset back to an absolute quarter-note\n * position.\n *\n * @param pos The bar position.\n * @param ts The time signature.\n * @returns The absolute position in quarter-note beats.\n * @category Rhythm & Meter\n */\nexport function barPositionToBeat(pos: BarPosition, ts: TimeSignature): number {\n assertInteger(pos.bar, 'bar position bar');\n assertFiniteNumber(pos.beat, 'bar position beat');\n return pos.bar * beatsPerBar(ts) + pos.beat;\n}\n\n/**\n * Metric weight of a position within its bar, on a 0–3 scale:\n * 3 the downbeat, 2 a secondary strong pulse, 1 any other main pulse, and 0 an\n * off-pulse subdivision.\n *\n * The secondary strong pulse is the bar's midpoint in even simple/compound\n * meters. For an additive/irregular meter with {@link TimeSignature.grouping}\n * set (e.g. `[2, 2, 3]` for 7/8), each group's head pulse other than the\n * downbeat is the secondary strong pulse instead; without a grouping every\n * non-downbeat main pulse of such a meter weighs 1 (flat, evenly divided\n * pulses).\n *\n * @param beatInQuarters Absolute or in-bar quarter-note position.\n * @param ts The time signature.\n * @returns The metric weight (0–3).\n * @throws If `ts.grouping` is present but is not a positive-integer list\n * summing to {@link pulsesPerBar}.\n * @example\n * ```ts\n * import { parseTimeSignature, metricWeight } from '@libraz/libcantus';\n * const ts = parseTimeSignature('4/4');\n * metricWeight(0, ts); // 3 (the downbeat)\n * ```\n * @category Rhythm & Meter\n */\nexport function metricWeight(beatInQuarters: number, ts: TimeSignature): number {\n assertFiniteNumber(beatInQuarters, 'beat');\n assertTimeSignature(ts);\n const { beat } = beatToBarPosition(beatInQuarters, ts);\n const pulse = pulseBeats(ts);\n if (!isMultiple(beat, pulse)) {\n return 0;\n }\n const pulses = pulsesPerBar(ts);\n const pulseIndex = Math.round(beat / pulse) % pulses;\n if (pulseIndex === 0) {\n return 3;\n }\n const grouping = ts.grouping;\n if (grouping !== undefined) {\n if (!isValidGrouping(grouping, pulses)) {\n throw new Error(\n `Invalid grouping [${grouping.join(', ')}] for ${formatTimeSignature(ts)}: ` +\n `entries must be positive integers summing to ${pulses}`,\n );\n }\n return isGroupHead(grouping, pulseIndex) ? 2 : 1;\n }\n if (pulses % 2 === 0 && pulseIndex === pulses / 2) {\n return 2;\n }\n return 1;\n}\n\n/**\n * Whether a position is metrically accented (weight 2 or more — a downbeat or a\n * secondary strong pulse).\n *\n * @param beatInQuarters Absolute or in-bar quarter-note position.\n * @param ts The time signature.\n * @returns True on strong beats.\n * @category Rhythm & Meter\n */\nexport function isStrongBeat(beatInQuarters: number, ts: TimeSignature): boolean {\n return metricWeight(beatInQuarters, ts) >= 2;\n}\n\n/**\n * Subdivide a span into `count` equal tuplet durations (e.g. an eighth-note\n * triplet is `tuplet(1, 3)`).\n *\n * @param totalBeats Total span in quarter-note beats.\n * @param count Number of equal parts.\n * @returns `count` equal durations summing to `totalBeats`.\n * @throws If `count` is not a positive integer.\n * @category Rhythm & Meter\n */\nexport function tuplet(totalBeats: number, count: number): number[] {\n assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, 'tuplet total beats');\n assertPositiveInt(count, 'tuplet count');\n return new Array<number>(count).fill(totalBeats / count);\n}\n","import type { NoteEvent } from '../types.js';\nimport { assertNoteEvents } from '../validation/index.js';\n\n/** A validated note retaining its position in the caller's original array. */\nexport type IndexedNoteEvent = {\n note: NoteEvent;\n originalIndex: number;\n endBeat: number;\n};\n\n/** Binary-searchable, stable-sorted index over note onsets and active spans. */\nexport type NoteEventIndex = {\n notes: IndexedNoteEvent[];\n /** Latest-onset note sounding at `beat`, with later input order winning ties. */\n at: (beat: number) => IndexedNoteEvent | undefined;\n /** Whether one or more notes attack at `beat`. */\n attacksAt: (beat: number) => boolean;\n /** Unique attack beats strictly inside `(startBeat, endBeat)`. */\n onsetsBetween: (startBeat: number, endBeat: number) => number[];\n};\n\nconst EPS = 1e-9;\n\nfunction upperBound(values: IndexedNoteEvent[], beat: number): number {\n let low = 0;\n let high = values.length;\n while (low < high) {\n const middle = Math.floor((low + high) / 2);\n if ((values[middle]?.note.startBeat ?? Number.POSITIVE_INFINITY) <= beat) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n return low;\n}\n\n/**\n * Validate and stable-sort note events once, then expose logarithmic onset and\n * active-note lookups. Non-positive-duration notes may be retained for callers\n * that intentionally filter them later, but never count as sounding.\n */\nexport function createNoteEventIndex(\n events: NoteEvent[],\n options: { allowNonPositiveDuration?: boolean } = {},\n): NoteEventIndex {\n assertNoteEvents(events, 'note events', options);\n const notes = events\n .map((note, originalIndex) => ({\n note,\n originalIndex,\n endBeat: note.startBeat + note.durationBeat,\n }))\n .sort((a, b) => a.note.startBeat - b.note.startBeat || a.originalIndex - b.originalIndex);\n const prefixMaxEnd: number[] = [];\n let maxEnd = Number.NEGATIVE_INFINITY;\n for (const indexed of notes) {\n maxEnd = Math.max(maxEnd, indexed.endBeat);\n prefixMaxEnd.push(maxEnd);\n }\n\n return {\n notes,\n at(beat) {\n let index = upperBound(notes, beat + EPS) - 1;\n while (index >= 0) {\n if ((prefixMaxEnd[index] ?? Number.NEGATIVE_INFINITY) <= beat + EPS) {\n return undefined;\n }\n const indexed = notes[index];\n if (\n indexed &&\n indexed.note.durationBeat > 0 &&\n indexed.note.startBeat - EPS <= beat &&\n beat < indexed.endBeat - EPS\n ) {\n return indexed;\n }\n index -= 1;\n }\n return undefined;\n },\n attacksAt(beat) {\n const firstAfter = upperBound(notes, beat + EPS);\n const candidate = notes[firstAfter - 1];\n return candidate !== undefined && Math.abs(candidate.note.startBeat - beat) < EPS;\n },\n onsetsBetween(startBeat, endBeat) {\n const result: number[] = [];\n let index = upperBound(notes, startBeat + EPS);\n while (index < notes.length) {\n const onset = notes[index]?.note.startBeat;\n if (onset === undefined || onset >= endBeat - EPS) {\n break;\n }\n if (result[result.length - 1] !== onset) {\n result.push(onset);\n }\n index += 1;\n }\n return result;\n },\n };\n}\n"],"mappings":"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","names":["EPS"]}
@@ -0,0 +1,154 @@
1
+ import {
2
+ borrowedSource,
3
+ chordToRoman,
4
+ detectChordBest,
5
+ functionOf,
6
+ isDiatonic,
7
+ parallelKey
8
+ } from "./chunk-CHMXELDS.js";
9
+ import {
10
+ chordPitchClasses,
11
+ diatonicTriad,
12
+ makeChord,
13
+ scaleTonesInDegreeOrder
14
+ } from "./chunk-MIWO425M.js";
15
+
16
+ // src/generate/reharmony/index.ts
17
+ function mod12(n) {
18
+ return (n % 12 + 12) % 12;
19
+ }
20
+ function hasInterval(chord, semitone) {
21
+ return chord.intervals.some((iv) => mod12(iv) === semitone);
22
+ }
23
+ function isDominantType(chord) {
24
+ return hasInterval(chord, 4) && hasInterval(chord, 10);
25
+ }
26
+ function isStableTarget(chord) {
27
+ const hasFifth = hasInterval(chord, 7);
28
+ const hasThird = hasInterval(chord, 3) || hasInterval(chord, 4);
29
+ return hasFifth && hasThird && !isDominantType(chord);
30
+ }
31
+ function commonToneCount(a, b) {
32
+ const set = new Set(b);
33
+ return a.filter((pc) => set.has(pc)).length;
34
+ }
35
+ function diatonicTriadsOf(key) {
36
+ const degrees = scaleTonesInDegreeOrder(key).length;
37
+ const triads = [];
38
+ for (let degree = 0; degree < degrees; degree += 1) {
39
+ triads.push(diatonicTriad(degree, key));
40
+ }
41
+ return triads;
42
+ }
43
+ function sameChord(a, b) {
44
+ return mod12(a.rootPc) === mod12(b.rootPc) && a.quality === b.quality;
45
+ }
46
+ var THIRD_OFFSETS = [3, 4, 8, 9];
47
+ function substituteChord(chord, key, opts) {
48
+ const original = chordPitchClasses(chord);
49
+ const candidates = [];
50
+ if (isDominantType(chord)) {
51
+ candidates.push({ chord: makeChord(mod12(chord.rootPc + 6), "dom7"), type: "tritone" });
52
+ }
53
+ if (isStableTarget(chord)) {
54
+ candidates.push({
55
+ chord: makeChord(mod12(chord.rootPc + 7), "dom7"),
56
+ type: "secondary-dominant"
57
+ });
58
+ }
59
+ for (const triad of diatonicTriadsOf(key)) {
60
+ const offset = mod12(triad.rootPc - chord.rootPc);
61
+ if (THIRD_OFFSETS.includes(offset) && commonToneCount(original, chordPitchClasses(triad)) === 2) {
62
+ candidates.push({ chord: triad, type: "relative" });
63
+ }
64
+ }
65
+ const targetFunction = functionOf(chord, key);
66
+ for (const triad of diatonicTriadsOf(parallelKey(key))) {
67
+ if (functionOf(triad, key) === targetFunction && !isDiatonic(triad, key)) {
68
+ candidates.push({ chord: triad, type: "borrowed" });
69
+ }
70
+ }
71
+ for (const offset of THIRD_OFFSETS) {
72
+ for (const quality of ["maj", "min"]) {
73
+ const mediant = makeChord(mod12(chord.rootPc + offset), quality);
74
+ if (commonToneCount(original, chordPitchClasses(mediant)) === 1 && !isDiatonic(mediant, key)) {
75
+ candidates.push({ chord: mediant, type: "chromatic-mediant" });
76
+ }
77
+ }
78
+ }
79
+ const melodyPcs = opts?.melodyPcs?.map(mod12);
80
+ const seen = /* @__PURE__ */ new Set();
81
+ const results = [];
82
+ for (const candidate of candidates) {
83
+ if (sameChord(candidate.chord, chord)) {
84
+ continue;
85
+ }
86
+ const dedupeKey = `${mod12(candidate.chord.rootPc)}:${candidate.chord.quality}`;
87
+ if (seen.has(dedupeKey)) {
88
+ continue;
89
+ }
90
+ seen.add(dedupeKey);
91
+ if (melodyPcs !== void 0) {
92
+ const pcs = new Set(chordPitchClasses(candidate.chord));
93
+ if (!melodyPcs.every((pc) => pcs.has(pc))) {
94
+ continue;
95
+ }
96
+ }
97
+ results.push({
98
+ chord: candidate.chord,
99
+ type: candidate.type,
100
+ roman: chordToRoman(candidate.chord, key),
101
+ function: functionOf(candidate.chord, key)
102
+ });
103
+ }
104
+ return results;
105
+ }
106
+ function modalInterchangePalette(key) {
107
+ const palette = [];
108
+ const seen = /* @__PURE__ */ new Set();
109
+ const add = (chord) => {
110
+ const dedupeKey = `${mod12(chord.rootPc)}:${chord.quality}`;
111
+ if (seen.has(dedupeKey)) {
112
+ return;
113
+ }
114
+ seen.add(dedupeKey);
115
+ palette.push({
116
+ chord,
117
+ roman: chordToRoman(chord, key),
118
+ source: borrowedSource(chord, key)
119
+ });
120
+ };
121
+ for (const triad of diatonicTriadsOf(parallelKey(key))) {
122
+ if (!isDiatonic(triad, key)) {
123
+ add(triad);
124
+ }
125
+ }
126
+ const neapolitan = makeChord(mod12(key.rootPc + 1), "maj");
127
+ if (!isDiatonic(neapolitan, key)) {
128
+ add(neapolitan);
129
+ }
130
+ return palette;
131
+ }
132
+ function negativeHarmonyMirror(chord, key) {
133
+ const tonic = mod12(key.rootPc);
134
+ const mirror = (p) => mod12(2 * tonic + 7 - p);
135
+ const pcs = chordPitchClasses(chord).map(mirror);
136
+ const bassPc = chord.bassPc !== void 0 ? mirror(chord.bassPc) : void 0;
137
+ const pitches = bassPc !== void 0 ? [bassPc - 12, ...pcs] : pcs;
138
+ const detected = detectChordBest(pitches);
139
+ if (detected) {
140
+ if (bassPc === void 0 && detected.bassPc !== void 0) {
141
+ return makeChord(detected.rootPc, detected.quality);
142
+ }
143
+ return detected;
144
+ }
145
+ const quality = chord.quality;
146
+ return makeChord(mirror(chord.rootPc), quality, bassPc);
147
+ }
148
+
149
+ export {
150
+ substituteChord,
151
+ modalInterchangePalette,
152
+ negativeHarmonyMirror
153
+ };
154
+ //# sourceMappingURL=chunk-IT2O4XFE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/generate/reharmony/index.ts"],"sourcesContent":["/**\n * Reharmonization: chord substitution, modal-interchange palettes, and negative\n * harmony. These transforms take an existing chord (or a key) and propose\n * alternative harmonies that preserve a chosen relationship — a shared function,\n * a common tone, a tonicizing dominant, or a reflection across the key axis.\n *\n * Roots are pitch classes (0..11); no spelled key signature is required.\n */\n\nimport { detectChordBest } from '../../analyze/detect/index.js';\nimport {\n type BorrowedSource,\n borrowedSource,\n chordToRoman,\n functionOf,\n type HarmonicFunction,\n isDiatonic,\n parallelKey,\n} from '../../analyze/functional/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport {\n type Chord,\n type ChordQuality,\n chordPitchClasses,\n diatonicTriad,\n makeChord,\n} from '../../theory/chord/index.js';\nimport { scaleTonesInDegreeOrder } from '../../theory/scale/index.js';\n\n/** Reduce a value to a pitch class in [0, 11]. */\nfunction mod12(n: number): number {\n return ((n % 12) + 12) % 12;\n}\n\n/**\n * The kind of substitution relationship a candidate realizes.\n *\n * @category Reharmonization\n */\nexport type SubstitutionType =\n | 'tritone'\n | 'relative'\n | 'borrowed'\n | 'chromatic-mediant'\n | 'secondary-dominant';\n\n/**\n * A proposed chord substitution with its relationship, numeral, and function.\n *\n * @category Reharmonization\n */\nexport type Substitution = {\n chord: Chord;\n type: SubstitutionType;\n roman: string;\n function: HarmonicFunction;\n};\n\n/**\n * Options controlling which substitutions {@link substituteChord} returns.\n *\n * @category Reharmonization\n */\nexport type SubstituteOptions = {\n /**\n * Melody pitch classes that must remain chord tones. When given, only\n * substitutions whose pitch classes contain every listed pitch class are\n * kept, so the melody stays consonant against the new harmony.\n */\n melodyPcs?: number[];\n};\n\n/** Whether the chord's template carries a given interval (mod 12). */\nfunction hasInterval(chord: Chord, semitone: number): boolean {\n return chord.intervals.some((iv) => mod12(iv) === semitone);\n}\n\n/** Whether a chord is a dominant type: a major third with a minor seventh. */\nfunction isDominantType(chord: Chord): boolean {\n return hasInterval(chord, 4) && hasInterval(chord, 10);\n}\n\n/**\n * A chord that can be tonicized: it has a perfect fifth and a third but is not\n * itself a dominant seventh (which is an unstable, tonicizing sonority rather\n * than a stable target).\n */\nfunction isStableTarget(chord: Chord): boolean {\n const hasFifth = hasInterval(chord, 7);\n const hasThird = hasInterval(chord, 3) || hasInterval(chord, 4);\n return hasFifth && hasThird && !isDominantType(chord);\n}\n\n/** Count the pitch classes shared between two pitch-class sets. */\nfunction commonToneCount(a: number[], b: number[]): number {\n const set = new Set(b);\n return a.filter((pc) => set.has(pc)).length;\n}\n\n/** The diatonic triads of a key, one per scale degree. */\nfunction diatonicTriadsOf(key: KeyScale): Chord[] {\n const degrees = scaleTonesInDegreeOrder(key).length;\n const triads: Chord[] = [];\n for (let degree = 0; degree < degrees; degree += 1) {\n triads.push(diatonicTriad(degree, key));\n }\n return triads;\n}\n\n/** Whether two chords share the same root pitch class and quality. */\nfunction sameChord(a: Chord, b: Chord): boolean {\n return mod12(a.rootPc) === mod12(b.rootPc) && a.quality === b.quality;\n}\n\n/** Root offsets (semitones) that form a third above or below a root. */\nconst THIRD_OFFSETS = [3, 4, 8, 9] as const;\n\n/**\n * Propose substitutions for a chord within a key.\n *\n * Candidates are generated by several relationships and deduplicated against the\n * original chord and each other:\n *\n * - `tritone`: for a dominant-type chord, the dominant seventh a tritone away.\n * - `secondary-dominant`: for a stable target, the dominant seventh a fifth\n * above its root (tonicizing it).\n * - `relative`: a diatonic triad a third away that shares two common tones.\n * - `borrowed`: a triad from the parallel mode with the same harmonic function.\n * - `chromatic-mediant`: a major/minor triad a third away sharing one common\n * tone.\n *\n * Each result carries its Roman numeral and harmonic function in `key`. When\n * `opts.melodyPcs` is given, only substitutions whose pitch classes contain all\n * of those pitch classes are returned.\n *\n * @param chord The chord to reharmonize.\n * @param key The prevailing key.\n * @param opts Optional melody-preservation constraint.\n * @returns The deduplicated substitution candidates.\n * @example\n * ```ts\n * import { substituteChord, makeChord, majorKey } from '@libraz/libcantus';\n * const subs = substituteChord(makeChord(7, 'dom7'), majorKey(0));\n * // includes the tritone sub Db7 (rootPc 1, quality 'dom7')\n * ```\n * @category Reharmonization\n */\nexport function substituteChord(\n chord: Chord,\n key: KeyScale,\n opts?: SubstituteOptions,\n): Substitution[] {\n const original = chordPitchClasses(chord);\n const candidates: { chord: Chord; type: SubstitutionType }[] = [];\n\n // Tritone substitution: only for dominant-type chords.\n if (isDominantType(chord)) {\n candidates.push({ chord: makeChord(mod12(chord.rootPc + 6), 'dom7'), type: 'tritone' });\n }\n\n // Secondary dominant: the V7 tonicizing a stable target.\n if (isStableTarget(chord)) {\n candidates.push({\n chord: makeChord(mod12(chord.rootPc + 7), 'dom7'),\n type: 'secondary-dominant',\n });\n }\n\n // Relative: a diatonic triad a third away sharing two common tones.\n for (const triad of diatonicTriadsOf(key)) {\n const offset = mod12(triad.rootPc - chord.rootPc);\n if (\n (THIRD_OFFSETS as readonly number[]).includes(offset) &&\n commonToneCount(original, chordPitchClasses(triad)) === 2\n ) {\n candidates.push({ chord: triad, type: 'relative' });\n }\n }\n\n // Borrowed: a parallel-mode triad sharing the original's harmonic function.\n const targetFunction = functionOf(chord, key);\n for (const triad of diatonicTriadsOf(parallelKey(key))) {\n if (functionOf(triad, key) === targetFunction && !isDiatonic(triad, key)) {\n candidates.push({ chord: triad, type: 'borrowed' });\n }\n }\n\n // Chromatic mediants: major/minor triads a third away sharing one common tone.\n for (const offset of THIRD_OFFSETS) {\n for (const quality of ['maj', 'min'] as const) {\n const mediant = makeChord(mod12(chord.rootPc + offset), quality);\n if (\n commonToneCount(original, chordPitchClasses(mediant)) === 1 &&\n !isDiatonic(mediant, key)\n ) {\n candidates.push({ chord: mediant, type: 'chromatic-mediant' });\n }\n }\n }\n\n const melodyPcs = opts?.melodyPcs?.map(mod12);\n const seen = new Set<string>();\n const results: Substitution[] = [];\n for (const candidate of candidates) {\n if (sameChord(candidate.chord, chord)) {\n continue;\n }\n const dedupeKey = `${mod12(candidate.chord.rootPc)}:${candidate.chord.quality}`;\n if (seen.has(dedupeKey)) {\n continue;\n }\n seen.add(dedupeKey);\n if (melodyPcs !== undefined) {\n const pcs = new Set(chordPitchClasses(candidate.chord));\n if (!melodyPcs.every((pc) => pcs.has(pc))) {\n continue;\n }\n }\n results.push({\n chord: candidate.chord,\n type: candidate.type,\n roman: chordToRoman(candidate.chord, key),\n function: functionOf(candidate.chord, key),\n });\n }\n return results;\n}\n\n/**\n * A borrowed chord in the modal-interchange palette of a key.\n *\n * @category Reharmonization\n */\nexport type BorrowedChord = {\n chord: Chord;\n roman: string;\n source: BorrowedSource;\n};\n\n/**\n * The modal-interchange palette of a key.\n *\n * Lists the diatonic triads of the {@link parallelKey} that are not diatonic to\n * `key` (the classic borrowed chords such as `iv`, `bVI`, and `bVII` in a major\n * key), plus the Neapolitan (a major triad on the flat second degree). Each\n * carries its Roman numeral and {@link borrowedSource} relative to `key`.\n *\n * @param key The prevailing key.\n * @returns The borrowed-chord palette.\n * @category Reharmonization\n */\nexport function modalInterchangePalette(key: KeyScale): BorrowedChord[] {\n const palette: BorrowedChord[] = [];\n const seen = new Set<string>();\n const add = (chord: Chord) => {\n const dedupeKey = `${mod12(chord.rootPc)}:${chord.quality}`;\n if (seen.has(dedupeKey)) {\n return;\n }\n seen.add(dedupeKey);\n palette.push({\n chord,\n roman: chordToRoman(chord, key),\n source: borrowedSource(chord, key),\n });\n };\n\n for (const triad of diatonicTriadsOf(parallelKey(key))) {\n if (!isDiatonic(triad, key)) {\n add(triad);\n }\n }\n // The Neapolitan (major triad on b2) sits outside both parallel modes, so it\n // is added explicitly — unless b2 is already diatonic (e.g. in a Phrygian key,\n // where it is a native chord rather than a borrowing).\n const neapolitan = makeChord(mod12(key.rootPc + 1), 'maj');\n if (!isDiatonic(neapolitan, key)) {\n add(neapolitan);\n }\n return palette;\n}\n\n/**\n * Reflect a chord across the tonic–dominant axis of a key (negative harmony).\n *\n * Every pitch class `p` maps to `(2*T + 7 - p) mod 12`, where `T` is the key\n * tonic — the reflection that swaps the tonic with the dominant. The mirrored\n * pitch classes (including the bass, if set) are reassembled into a chord by\n * {@link detectChordBest}; when no chord is recognized, the mirrored root and\n * the original quality are used as a fallback.\n *\n * @param chord The chord to reflect.\n * @param key The prevailing key.\n * @returns The negative-harmony counterpart of the chord.\n * @example\n * ```ts\n * import { negativeHarmonyMirror, makeChord, majorKey } from '@libraz/libcantus';\n * negativeHarmonyMirror(makeChord(7, 'maj'), majorKey(0));\n * // G major reflected across C's axis => F minor (rootPc 5, quality 'min')\n * ```\n * @category Reharmonization\n */\nexport function negativeHarmonyMirror(chord: Chord, key: KeyScale): Chord {\n const tonic = mod12(key.rootPc);\n const mirror = (p: number) => mod12(2 * tonic + 7 - p);\n const pcs = chordPitchClasses(chord).map(mirror);\n const bassPc = chord.bassPc !== undefined ? mirror(chord.bassPc) : undefined;\n // Place the mirrored bass an octave below so it is recognized as the lowest\n // note and preserved through detection.\n const pitches = bassPc !== undefined ? [bassPc - 12, ...pcs] : pcs;\n const detected = detectChordBest(pitches);\n if (detected) {\n // Without an input bass the mirrored tones are octave-less, so detection may\n // read the lowest pitch class as a slash bass and report a spurious\n // inversion. Restore the recognized chord to root position in that case.\n if (bassPc === undefined && detected.bassPc !== undefined) {\n return makeChord(detected.rootPc, detected.quality);\n }\n return detected;\n }\n const quality: ChordQuality = chord.quality;\n return makeChord(mirror(chord.rootPc), quality, bassPc);\n}\n"],"mappings":"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","names":[]}