@libraz/libcantus 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +63 -0
- package/README.md +3 -1
- package/README_ja.md +13 -11
- package/dist/analyze/index.cjs +7 -7
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +14 -12
- package/dist/analyze/index.d.ts +14 -12
- package/dist/analyze/index.js +6 -6
- package/dist/{chunk-NB25P3EH.js → chunk-2G5MCGYU.js} +33 -11
- package/dist/chunk-2G5MCGYU.js.map +1 -0
- package/dist/{chunk-NM7UAMC3.cjs → chunk-3FDBMIPR.cjs} +64 -64
- package/dist/chunk-3FDBMIPR.cjs.map +1 -0
- package/dist/{chunk-YFA7X2F6.cjs → chunk-3MDOEFIQ.cjs} +411 -327
- package/dist/chunk-3MDOEFIQ.cjs.map +1 -0
- package/dist/{chunk-IPEF6NWB.js → chunk-5MWRBYAT.js} +278 -154
- package/dist/chunk-5MWRBYAT.js.map +1 -0
- package/dist/{chunk-B2PW6EHN.cjs → chunk-6TTBEON3.cjs} +1253 -889
- package/dist/chunk-6TTBEON3.cjs.map +1 -0
- package/dist/{chunk-UMWZ4ZET.js → chunk-7NQ6AL3H.js} +4 -4
- package/dist/chunk-7NQ6AL3H.js.map +1 -0
- package/dist/{chunk-PWC4CRWT.cjs → chunk-E3QO3UET.cjs} +31 -23
- package/dist/chunk-E3QO3UET.cjs.map +1 -0
- package/dist/{chunk-GNVN4PPJ.js → chunk-FJFULJZE.js} +531 -213
- package/dist/chunk-FJFULJZE.js.map +1 -0
- package/dist/{chunk-CPMB7P75.js → chunk-FK3DGAIZ.js} +608 -244
- package/dist/chunk-FK3DGAIZ.js.map +1 -0
- package/dist/chunk-GEDTJHAC.cjs.map +1 -1
- package/dist/{chunk-RFIBF2RR.js → chunk-H7FVMBUC.js} +138 -65
- package/dist/chunk-H7FVMBUC.js.map +1 -0
- package/dist/{chunk-4R2LWV7E.cjs → chunk-HYSVSFNI.cjs} +664 -540
- package/dist/chunk-HYSVSFNI.cjs.map +1 -0
- package/dist/{chunk-35NBV2FT.cjs → chunk-KMYGRFZ6.cjs} +47 -24
- package/dist/chunk-KMYGRFZ6.cjs.map +1 -0
- package/dist/{chunk-IFZDWIKM.cjs → chunk-LQZQDBCD.cjs} +1048 -730
- package/dist/chunk-LQZQDBCD.cjs.map +1 -0
- package/dist/{chunk-PM4XJE2Y.cjs → chunk-P3CPXAJ5.cjs} +76 -54
- package/dist/chunk-P3CPXAJ5.cjs.map +1 -0
- package/dist/{chunk-S5BKZZGM.cjs → chunk-S3QELJUE.cjs} +72 -56
- package/dist/chunk-S3QELJUE.cjs.map +1 -0
- package/dist/{chunk-PUKJP2Z5.js → chunk-TXFWSU5D.js} +31 -8
- package/dist/chunk-TXFWSU5D.js.map +1 -0
- package/dist/{chunk-76ZBTRG2.cjs → chunk-U4EQTDGN.cjs} +258 -218
- package/dist/chunk-U4EQTDGN.cjs.map +1 -0
- package/dist/chunk-UGLFDG7L.cjs.map +1 -1
- package/dist/{chunk-MLBPXA4Z.js → chunk-UTE5A36R.js} +198 -114
- package/dist/chunk-UTE5A36R.js.map +1 -0
- package/dist/{chunk-ZBUROTJQ.js → chunk-UZKQISRF.js} +17 -9
- package/dist/chunk-UZKQISRF.js.map +1 -0
- package/dist/{chunk-7KG3RTYS.js → chunk-VL5NXVCN.js} +281 -155
- package/dist/chunk-VL5NXVCN.js.map +1 -0
- package/dist/{chunk-CGBADIO5.cjs → chunk-WOMCXN2W.cjs} +328 -202
- package/dist/chunk-WOMCXN2W.cjs.map +1 -0
- package/dist/{chunk-3R3QVKY5.cjs → chunk-YDGMWH43.cjs} +397 -324
- package/dist/chunk-YDGMWH43.cjs.map +1 -0
- package/dist/{chunk-AGOVW7U6.js → chunk-YYVXRV3V.js} +72 -56
- package/dist/chunk-YYVXRV3V.js.map +1 -0
- package/dist/{chunk-M5RYMAFI.js → chunk-ZLNSH4UL.js} +71 -31
- package/dist/chunk-ZLNSH4UL.js.map +1 -0
- package/dist/{coerce-DRERzdve.d.cts → coerce-CUhHc3Tu.d.ts} +16 -8
- package/dist/{coerce-DP0vYu-x.d.ts → coerce-Ytt2Eiq9.d.cts} +16 -8
- package/dist/core/index.cjs +5 -5
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +9 -8
- package/dist/core/index.d.ts +9 -8
- package/dist/core/index.js +4 -4
- package/dist/generate/index.cjs +8 -8
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +28 -18
- package/dist/generate/index.d.ts +28 -18
- package/dist/generate/index.js +7 -7
- package/dist/{index-BMweUlm5.d.cts → index-5VJgKz_Q.d.cts} +6 -2
- package/dist/{index-BHIUyjhZ.d.ts → index-BLuDesVG.d.ts} +20 -13
- package/dist/{index-B9QLziDe.d.cts → index-BV6APVa6.d.cts} +20 -13
- package/dist/{index-CL4b4gUQ.d.ts → index-Bf0Sv8a9.d.ts} +6 -2
- package/dist/{index-Dw2DP7vm.d.ts → index-BkT21EMv.d.cts} +3 -2
- package/dist/{index-BL5N2Oyz.d.cts → index-C7KCaTRz.d.ts} +3 -2
- package/dist/{index-Cy15oTvh.d.ts → index-C9MYbuKS.d.cts} +7 -3
- package/dist/{index-Cy15oTvh.d.cts → index-C9MYbuKS.d.ts} +7 -3
- package/dist/{index-CY-3NVfy.d.cts → index-DVInzCDS.d.cts} +53 -29
- package/dist/{index-9eoQs2C1.d.ts → index-Dg-SAfhz.d.ts} +6 -4
- package/dist/{index-zaZdmnU4.d.ts → index-jgeug9EA.d.ts} +53 -29
- package/dist/{index-cHvbYgJI.d.cts → index-pQihL4z7.d.cts} +6 -4
- package/dist/index.cjs +13 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -11
- package/dist/index.d.ts +11 -11
- package/dist/index.js +12 -12
- package/dist/{masks-Cc7-MzzY.d.ts → masks-BnzXnBj0.d.cts} +3 -2
- package/dist/{masks-pADfTOiD.d.cts → masks-tycmkrgs.d.ts} +3 -2
- package/dist/model/index.cjs +12 -12
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +63 -48
- package/dist/model/index.d.ts +63 -48
- package/dist/model/index.js +11 -11
- package/dist/{positional-DJgs6ltD.d.ts → positional-CscpxtlE.d.cts} +3 -2
- package/dist/{positional-DMZer-FM.d.cts → positional-DLnfUgqG.d.ts} +3 -2
- package/dist/{profile-DLdlIpS9.d.ts → profile-B4WqcM4H.d.ts} +25 -17
- package/dist/{profile-CQMEqdH9.d.cts → profile-BJd1XkPB.d.cts} +25 -17
- package/dist/theory/index.cjs +5 -5
- package/dist/theory/index.cjs.map +1 -1
- package/dist/theory/index.d.cts +18 -14
- package/dist/theory/index.d.ts +18 -14
- package/dist/theory/index.js +4 -4
- package/dist/{types-D-XgxGvp.d.cts → types-7mGFwvMm.d.ts} +45 -22
- package/dist/{types-Dh7C064C.d.ts → types-_ofpb2be.d.cts} +45 -22
- package/package.json +13 -13
- package/dist/chunk-35NBV2FT.cjs.map +0 -1
- package/dist/chunk-3R3QVKY5.cjs.map +0 -1
- package/dist/chunk-4R2LWV7E.cjs.map +0 -1
- package/dist/chunk-76ZBTRG2.cjs.map +0 -1
- package/dist/chunk-7KG3RTYS.js.map +0 -1
- package/dist/chunk-AGOVW7U6.js.map +0 -1
- package/dist/chunk-B2PW6EHN.cjs.map +0 -1
- package/dist/chunk-CGBADIO5.cjs.map +0 -1
- package/dist/chunk-CPMB7P75.js.map +0 -1
- package/dist/chunk-GNVN4PPJ.js.map +0 -1
- package/dist/chunk-IFZDWIKM.cjs.map +0 -1
- package/dist/chunk-IPEF6NWB.js.map +0 -1
- package/dist/chunk-M5RYMAFI.js.map +0 -1
- package/dist/chunk-MLBPXA4Z.js.map +0 -1
- package/dist/chunk-NB25P3EH.js.map +0 -1
- package/dist/chunk-NM7UAMC3.cjs.map +0 -1
- package/dist/chunk-PM4XJE2Y.cjs.map +0 -1
- package/dist/chunk-PUKJP2Z5.js.map +0 -1
- package/dist/chunk-PWC4CRWT.cjs.map +0 -1
- package/dist/chunk-RFIBF2RR.js.map +0 -1
- package/dist/chunk-S5BKZZGM.cjs.map +0 -1
- package/dist/chunk-UMWZ4ZET.js.map +0 -1
- package/dist/chunk-YFA7X2F6.cjs.map +0 -1
- package/dist/chunk-ZBUROTJQ.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/libcantus/libcantus/ts/dist/chunk-3MDOEFIQ.cjs","../src/analyze/arrange/internal.ts","../src/analyze/arrange/tracks.ts","../src/analyze/arrange/session.ts","../src/analyze/arrange/tension.ts","../src/analyze/form/hypermeter.ts","../src/analyze/form/phrase.ts","../src/analyze/form/section.ts","../src/analyze/reference/compare.ts","../src/analyze/reference/profile.ts"],"names":[],"mappings":"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","file":"/home/runner/work/libcantus/libcantus/ts/dist/chunk-3MDOEFIQ.cjs","sourcesContent":[null,"/**\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 { InvalidInputError } from '../../core/errors/index.js';\nimport type { NoteEvent } from '../../core/types.js';\nimport type { NoteEventAssertOptions } from '../../core/validation/index.js';\nimport {\n assertArray,\n assertFiniteNumber,\n assertNoteEvents,\n assertOneOf,\n describeRejected,\n} from '../../core/validation/index.js';\nimport { PROFILE_WEIGHTS, type SafetyProfile } from '../../theory/safety/index.js';\nimport { resolveKey } from '../../theory/scale/index.js';\nimport { adjacent, BEAT_EPS, hasEnded, sameInstant } from '../adjacency.js';\nimport type { KeyRegion } from '../keys/index.js';\nimport { assertChordTimeline, assertSpansInOrder, type ChordTimeline } from '../timeline/index.js';\nimport type { IdentifiedVoiceNote } from '../voice/index.js';\nimport type { ArrangementTrack, TrackRole } from './tracks.js';\n\n/**\n * Largest interval, in semitones, a sub-voice will span between consecutive\n * notes. Beyond two octaves the connection is not a melodic step or leap but an\n * artefact of lane packing, so a new sub-voice is opened instead.\n */\nexport const MAX_LANE_LEAP = 24;\n\n/** A note with the pitch that immediately preceded it in the same sub-voice. */\nexport type PreparedNote = {\n /** The id the note carries in the analysis. */\n id: number;\n /** The note's index in its track's own note array. */\n originalIndex?: number;\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: IdentifiedVoiceNote[];\n /** The same notes as sounding spans, each carrying its predecessor's pitch. */\n sounding: PreparedNote[];\n /**\n * The span sounding at a beat, or undefined. A sub-voice is monophonic by\n * construction, so at most one span covers any beat and the lookup is a\n * single binary search rather than a scan.\n */\n at: (beat: number) => PreparedNote | undefined;\n};\n\n/** A track prepared for analysis: resolved metadata plus monophonic sub-voices. */\nexport type PreparedTrack = {\n name: string;\n role: TrackRole;\n /** The track's index in the caller's input array. */\n trackIndex: number;\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 - BEAT_EPS && beat < note.endBeat - BEAT_EPS;\n}\n\n/** Whether a track's role means it carries no harmonic content. */\nexport function isPercussion(role: TrackRole | undefined): boolean {\n return role === 'drums';\n}\n\n/**\n * The roles that exist, written once as a table the compiler checks against\n * {@link TrackRole}: a role added to the union without a column here fails to\n * compile rather than becoming a name the check below quietly rejects.\n */\nconst TRACK_ROLE_TABLE: Readonly<Record<TrackRole, true>> = {\n melody: true,\n harmony: true,\n bass: true,\n drums: true,\n other: true,\n};\n\n/** The roles that exist, read off the table rather than listed again here. */\nexport const TRACK_ROLES = Object.keys(TRACK_ROLE_TABLE) as TrackRole[];\n\n/**\n * The role a track plays, checked against the roles that exist.\n *\n * A name outside the table is an input error rather than a quiet fall back to\n * `'other'`: a drum track labelled `'percussion'` — the word a MIDI import\n * naturally writes — would be read as pitched material and pooled into the key\n * and the chords, and the role would be reported back as a value\n * {@link TrackRole} says cannot occur.\n *\n * @param role The track's role, if any.\n * @param name What the track is called in an error message.\n * @returns The role, or `'other'` when the track carries none.\n * @throws If the role names no known role.\n */\nexport function trackRoleOf(role: TrackRole | undefined, name: string): TrackRole {\n return role === undefined ? 'other' : assertOneOf(role, TRACK_ROLES, name);\n}\n\n/** The safety profile an arrangement is judged under when the caller names none. */\nconst DEFAULT_PROFILE: SafetyProfile = 'pop';\n\n/** {@link DEFAULT_PROFILE} as a one-entry list, the form the generated tables read. */\nexport const DEFAULT_PROFILE_NAMES: readonly SafetyProfile[] = [DEFAULT_PROFILE];\n\n/** The profiles that exist, read off the table rather than listed again here. */\nconst SAFETY_PROFILES = Object.keys(PROFILE_WEIGHTS) as SafetyProfile[];\n\n/**\n * The safety profile an arrangement is judged under, checked against the\n * profiles that exist.\n *\n * A name outside the table is an input error rather than a quiet fall back to\n * `pop`, which would read as a verdict about the music instead of as the\n * misspelling it is. Both entry points resolve it here so they cannot disagree\n * about which names are taken.\n *\n * @param profile The caller's profile, if any.\n * @returns The profile to judge under.\n * @throws If the profile names no known profile.\n */\nexport function arrangementProfile(profile: SafetyProfile | undefined): SafetyProfile {\n return assertOneOf(profile ?? DEFAULT_PROFILE, SAFETY_PROFILES, 'arrangement profile');\n}\n\n/**\n * Validate the note array of every track, naming the track that is wrong.\n *\n * A missing or non-array `notes` is rejected here rather than becoming a raw\n * `TypeError` further down, where the message no longer says which track. Every\n * entry point that takes arrangement tracks checks them this way, so the same\n * malformed input is refused with the same message whichever one is called.\n *\n * @param tracks The tracks as the caller passed them.\n * @param options The note-event assertion options this entry point works under.\n * @returns How many notes the tracks hold in total.\n * @throws If a track's `notes` is not an array, or holds a malformed event.\n */\nexport function assertTrackNotes(\n tracks: readonly ArrangementTrack[],\n options: NoteEventAssertOptions,\n): number {\n assertArray(tracks, 'arrangement tracks');\n let noteCount = 0;\n for (let index = 0; index < tracks.length; index += 1) {\n const notes = tracks[index]?.notes;\n if (!Array.isArray(notes)) {\n throw new InvalidInputError(\n `tracks[${index}].notes must be an array; received ${describeRejected(notes)}`,\n );\n }\n assertNoteEvents(notes, `tracks[${index}].notes`, options);\n noteCount += notes.length;\n }\n return noteCount;\n}\n\n/**\n * Reject a caller's chord timeline that is not one, when one was given at all.\n *\n * The option is what {@link assertChordTimeline} reads, with the difference\n * that leaving it out is how a caller asks for the timeline to be inferred.\n *\n * @param timeline The timeline as the caller passed it, or nothing.\n * @param name What the option is called in an error message.\n * @throws If the value is not a timeline of chord segments.\n */\nexport function assertGivenTimeline(timeline: ChordTimeline | undefined, name: string): void {\n if (timeline !== undefined) {\n assertChordTimeline(timeline, name);\n }\n}\n\n/**\n * Reject key regions that are not regions, or that name no key.\n *\n * The sibling of {@link assertGivenTimeline}, for the option that answers the\n * other half of the question: a region missing its `key` became a raw\n * `TypeError` inside the key reduction, several layers from the option it came\n * from. The key itself is resolved here rather than described, since resolving\n * it is what every reader of the region goes on to do.\n *\n * @param keys The regions as the caller passed them, or nothing.\n * @param name What the option is called in an error message.\n * @throws If the value is not an array of key regions, or the regions are not\n * disjoint, in beat order and of positive length.\n */\nexport function assertGivenKeys(keys: readonly KeyRegion[] | undefined, name: string): void {\n if (keys === undefined) {\n return;\n }\n if (!Array.isArray(keys)) {\n throw new InvalidInputError(\n `${name} must be an array of key regions; received ${describeRejected(keys)}`,\n );\n }\n for (let index = 0; index < keys.length; index += 1) {\n const region = keys[index];\n if (typeof region !== 'object' || region === null) {\n throw new InvalidInputError(\n `${name}[${index}] must be a key region; received ${describeRejected(region)}`,\n );\n }\n assertFiniteNumber(region.startBeat, `${name}[${index}].startBeat`);\n assertFiniteNumber(region.endBeat, `${name}[${index}].endBeat`);\n if (\n region.key === undefined ||\n region.key === null ||\n (typeof region.key !== 'string' && typeof region.key !== 'object')\n ) {\n throw new InvalidInputError(\n `${name}[${index}].key must name a key; received ${describeRejected(region.key)}`,\n );\n }\n resolveKey(region.key);\n }\n assertSpansInOrder(keys, name);\n}\n\n/**\n * The tracks the harmony is inferred from, as a set of indices checked against\n * the tracks that exist.\n *\n * An index naming no track cannot be honoured, and matching nothing quietly\n * turns a stale index — the ordinary result of deleting a track — into an\n * analysis with no harmony at all, reported as though the arrangement were at\n * fault. A percussion track is refused for the same reason: {@link poolNotes}\n * drops it, so naming one is naming no harmony, and the analysis that comes back\n * blames the music for clashing with a harmony nothing states. The session's\n * `trackIndex` is checked the same way.\n *\n * @param harmonyTracks The caller's indices, if any.\n * @param tracks The tracks the indices are read against.\n * @param name What the indices belong to, for the error message.\n * @returns The indices as a set, or undefined when the caller named none.\n * @throws If an index is not finite, names no track, or names a track whose\n * notes carry no harmony.\n */\nexport function harmonyTrackSet(\n harmonyTracks: readonly number[] | undefined,\n tracks: readonly ArrangementTrack[],\n name = 'harmonyTracks',\n): ReadonlySet<number> | undefined {\n if (harmonyTracks === undefined) {\n return undefined;\n }\n const only = new Set<number>();\n for (let index = 0; index < harmonyTracks.length; index += 1) {\n const value = harmonyTracks[index];\n if (value === undefined || !Number.isFinite(value)) {\n throw new InvalidInputError(`${name}[${index}] must be a track index; received ${value}`);\n }\n const track = Math.trunc(value);\n if (track < 0 || track >= tracks.length) {\n throw new InvalidInputError(\n `${name}[${index}] ${track} is outside the arrangement's ${tracks.length} tracks`,\n );\n }\n if (isPercussion(tracks[track]?.role)) {\n throw new InvalidInputError(\n `${name}[${index}] ${track} names a percussion track, whose pitches state no harmony`,\n );\n }\n only.add(track);\n }\n return only;\n}\n\n/**\n * Every sounding note of every pitched track pooled into one flat list. Zero-\n * and negative-length notes never sound, so they are dropped here (matching the\n * chord-inference ingest in the timeline module), and percussion tracks are\n * skipped entirely: their pitches are instrument selections, not harmony.\n */\nexport function poolNotes(tracks: ArrangementTrack[], only?: ReadonlySet<number>): NoteEvent[] {\n const notes: NoteEvent[] = [];\n for (let index = 0; index < tracks.length; index += 1) {\n const track = tracks[index];\n if (track === undefined || isPercussion(track.role)) {\n continue;\n }\n if (only !== undefined && !only.has(index)) {\n continue;\n }\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 * Each note joins the free sub-voice — one whose last note has ended — whose\n * last pitch is nearest its own, so a track whose voices cross keeps each voice\n * on its own lane instead of handing a low note to whichever lane happened to\n * be created first. Ties go to the lane that ended latest, which is the one the\n * note actually follows. A connection wider than {@link MAX_LANE_LEAP} is not a\n * melodic one, so it opens a new sub-voice instead.\n *\n * The predecessor pitch this produces feeds leap and parallel detection, so a\n * mis-assigned lane invents voice-leading faults that are not in the music.\n */\nfunction splitIntoSubVoices(ordered: IdentifiedVoiceNote[]): IdentifiedVoiceNote[][] {\n const lanes: IdentifiedVoiceNote[][] = [];\n const attachNearest = (note: IdentifiedVoiceNote, excluded = new Set<number>()): number => {\n let best = -1;\n let bestDistance = Number.POSITIVE_INFINITY;\n let bestEnd = Number.NEGATIVE_INFINITY;\n for (let lane = 0; lane < lanes.length; lane += 1) {\n if (excluded.has(lane)) {\n continue;\n }\n const last = lanes[lane]?.[(lanes[lane]?.length ?? 0) - 1];\n if (last === undefined) {\n continue;\n }\n const end = last.startBeat + last.durationBeat;\n if (!hasEnded(end, note.startBeat)) {\n continue; // still sounding, so the lane is not free\n }\n const distance = Math.abs(last.pitch - note.pitch);\n if (distance > MAX_LANE_LEAP) {\n continue;\n }\n if (distance < bestDistance || (distance === bestDistance && end > bestEnd)) {\n best = lane;\n bestDistance = distance;\n bestEnd = end;\n }\n }\n if (best >= 0) {\n lanes[best]?.push(note);\n }\n return best;\n };\n\n for (let start = 0; start < ordered.length; ) {\n const onset = ordered[start]?.startBeat ?? 0;\n let end = start + 1;\n while (end < ordered.length && sameInstant(ordered[end]?.startBeat ?? 0, onset)) {\n end += 1;\n }\n const block = ordered.slice(start, end);\n // Notes struck together form a vertical slice, not a sequence of greedy\n // nearest-neighbour decisions, so their lanes are chosen as a whole.\n if (block.length > 1) {\n const freeLanes = lanes\n .map((lane, index) => ({ index, last: lane[lane.length - 1] }))\n .filter(\n (entry): entry is { index: number; last: IdentifiedVoiceNote } =>\n entry.last !== undefined &&\n hasEnded(entry.last.startBeat + entry.last.durationBeat, onset),\n )\n .sort((a, b) => a.last.pitch - b.last.pitch);\n const assignment = pairWithFreeLanes(block, freeLanes);\n // Every lane the slice takes is settled before a note is placed: a note\n // left for {@link attachNearest} must not be handed a lane the slice\n // already spoke for, and neither may two of them share one.\n const taken = new Set<number>(\n assignment.filter((lane): lane is number => lane !== undefined),\n );\n for (let index = 0; index < block.length; index += 1) {\n const note = block[index];\n if (note === undefined) {\n continue;\n }\n const lane = assignment[index];\n if (lane !== undefined) {\n lanes[lane]?.push(note);\n continue;\n }\n const attached = attachNearest(note, taken);\n if (attached < 0) {\n lanes.push([note]);\n } else {\n taken.add(attached);\n }\n }\n } else {\n const note = block[0];\n if (note !== undefined && attachNearest(note) < 0) {\n lanes.push([note]);\n }\n }\n start = end;\n }\n return lanes;\n}\n\n/**\n * Choose a free lane for each note of one simultaneous block.\n *\n * The whole slice is assigned at once, by the rule {@link splitIntoSubVoices}\n * follows note by note: each note joins the free lane whose last pitch is\n * nearest its own, with the block's low-to-high order preserved so no two\n * assignments cross. Pairing by position instead — the lowest note to the lowest\n * free lane — hands a thinned chord's notes to lanes they never sounded on: a\n * C-E-G that becomes E-G gives the E the C's lane and invents the leap that\n * follows from it.\n *\n * A connection wider than {@link MAX_LANE_LEAP} is not a melodic one, so leaving\n * a note for a lane of its own costs exactly that: it is taken whenever no\n * remaining lane is closer than the widest connection a lane admits.\n *\n * @param block The notes struck together, low to high.\n * @param freeLanes The lanes free at that onset, by their last pitch, low to high.\n * @returns One entry per block note: the lane it joins, or undefined when it\n * joins none of them.\n */\nfunction pairWithFreeLanes(\n block: readonly IdentifiedVoiceNote[],\n freeLanes: readonly { index: number; last: IdentifiedVoiceNote }[],\n): (number | undefined)[] {\n const noteCount = block.length;\n const laneCount = freeLanes.length;\n const assignment = new Array<number | undefined>(noteCount).fill(undefined);\n if (laneCount === 0) {\n return assignment;\n }\n // Cost of the cheapest order-preserving assignment of the first i notes over\n // the first j lanes, and the step it ends with: 0 leaves lane j-1 unused, 1\n // pairs note i-1 with it, 2 leaves note i-1 without a lane.\n const width = laneCount + 1;\n const cost = new Float64Array((noteCount + 1) * width);\n const step = new Uint8Array((noteCount + 1) * width);\n for (let i = 1; i <= noteCount; i += 1) {\n cost[i * width] = i * MAX_LANE_LEAP;\n step[i * width] = 2;\n }\n for (let i = 1; i <= noteCount; i += 1) {\n for (let j = 1; j <= laneCount; j += 1) {\n let best = cost[i * width + j - 1] ?? 0;\n let choice = 0;\n const unpaired = (cost[(i - 1) * width + j] ?? 0) + MAX_LANE_LEAP;\n if (unpaired < best) {\n best = unpaired;\n choice = 2;\n }\n const note = block[i - 1];\n const lane = freeLanes[j - 1];\n if (note !== undefined && lane !== undefined) {\n const distance = Math.abs(lane.last.pitch - note.pitch);\n const paired = (cost[(i - 1) * width + j - 1] ?? 0) + distance;\n if (distance <= MAX_LANE_LEAP && paired < best) {\n best = paired;\n choice = 1;\n }\n }\n cost[i * width + j] = best;\n step[i * width + j] = choice;\n }\n }\n for (let i = noteCount, j = laneCount; i > 0; ) {\n if (j === 0 || step[i * width + j] === 2) {\n i -= 1;\n } else if (step[i * width + j] === 1) {\n assignment[i - 1] = freeLanes[j - 1]?.index;\n i -= 1;\n j -= 1;\n } else {\n j -= 1;\n }\n }\n return assignment;\n}\n\n/** Index of the last span starting at or before a beat, or -1. */\nfunction lastStartingAtOrBefore(sounding: PreparedNote[], beat: number): number {\n let low = 0;\n let high = sounding.length;\n while (low < high) {\n const middle = Math.floor((low + high) / 2);\n if ((sounding[middle]?.startBeat ?? Number.POSITIVE_INFINITY) <= beat + BEAT_EPS) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n return low - 1;\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; each note keeps the index it had in the caller's\n * array so annotations can be mapped back. Zero- and negative-length notes never\n * sound and are dropped at ingest. Each track is then partitioned into\n * monophonic sub-voices (see {@link splitIntoSubVoices}); the predecessor pitch\n * used for suspension and parallel detection is the previous note of the same\n * sub-voice, 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: IdentifiedVoiceNote[] = track.notes\n .map((note, originalIndex) => ({ note, originalIndex }))\n .filter(({ note }) => note.durationBeat > 0)\n .sort((a, b) => a.note.startBeat - b.note.startBeat || a.note.pitch - b.note.pitch)\n // A pitch of -0 is pitch 0, which is all an integer pitch can hold.\n .map(({ note, originalIndex }) => ({\n ...note,\n pitch: note.pitch === 0 ? 0 : note.pitch,\n id: nextId++,\n originalIndex,\n }));\n const voices: PreparedVoice[] = splitIntoSubVoices(ordered).map((voice) => {\n const sounding: PreparedNote[] = 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 && adjacent(prevEnd, note.startBeat);\n return {\n id: note.id,\n originalIndex: note.originalIndex,\n pitch: note.pitch,\n prevPitch: contiguous ? prev?.pitch : undefined,\n startBeat: note.startBeat,\n endBeat: note.startBeat + note.durationBeat,\n };\n });\n return {\n voice,\n sounding,\n at: (beat: number) => {\n const candidate = sounding[lastStartingAtOrBefore(sounding, beat)];\n return candidate !== undefined && covers(candidate, beat) ? candidate : undefined;\n },\n };\n });\n return {\n name: track.name ?? `track ${index + 1}`,\n role: trackRoleOf(track.role, `tracks[${index}].role`),\n trackIndex: index,\n voices,\n };\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 type { MeterLike, MeterMap } from '../../core/meter/index.js';\nimport { beatsPerBarAt, isStrongBeat, resolveMeters } from '../../core/meter/index.js';\nimport type { NoteEvent } from '../../core/types.js';\nimport type { NoteEventAssertOptions } from '../../core/validation/index.js';\nimport {\n assertArray,\n assertFunction,\n assertGenerationBudget,\n assertInteger,\n assertNoteEvents,\n assertRange,\n} from '../../core/validation/index.js';\nimport type { Chord } from '../../theory/chord/index.js';\nimport {\n evaluateSafety,\n NoteSafety,\n type SafetyProfile,\n type VoiceSnapshot,\n} from '../../theory/safety/index.js';\nimport type { KeyLike } from '../../theory/scale/index.js';\nimport { majorKey, type ResolvedKey, resolveKey, scaleOf } from '../../theory/scale/index.js';\nimport { BEAT_EPS, sameInstant } from '../adjacency.js';\nimport { gridForNotes, latestSoundingEnd } from '../grid.js';\nimport {\n attachPivots,\n type KeyRegion,\n keyLookup,\n keyTimelineFromNotes,\n prevailingKeyOf,\n} from '../keys/index.js';\nimport {\n analyzeTimeline,\n type CadenceHit,\n type ChordTimeline,\n type ChordTimelineResult,\n type DirtySlots,\n detectCadences,\n statedKeyRegions,\n type TimelineEvidence,\n} from '../timeline/index.js';\nimport {\n type AnalyzedNote,\n analyzeVoice,\n type KeyContext,\n type TheoryLabel,\n type VoiceNote,\n} from '../voice/index.js';\nimport {\n arrangementProfile,\n assertGivenKeys,\n assertGivenTimeline,\n assertTrackNotes,\n harmonyTrackSet,\n isPercussion,\n type PreparedNote,\n type PreparedTrack,\n poolNotes,\n prepareTracks,\n} from './internal.js';\n\n/**\n * The musical role a track plays in the arrangement.\n *\n * Only `drums` changes the analysis: its pitches select instruments rather than\n * naming harmony, so such a track is excluded from chord and key inference and\n * from every voice-leading comparison, and is reported back with no\n * annotations. `harmony` is meaningful through\n * {@link ArrangementOptions.harmonyTracks}, which names the tracks the chords\n * are inferred from. The rest are labels carried through to\n * {@link TrackAnalysis.role} for the caller's own use; nothing is inferred, so\n * a track left unlabelled reports `'other'` rather than a guess.\n *\n * @category Arrangement & Analysis\n */\nexport type TrackRole = 'melody' | 'harmony' | 'bass' | 'drums' | '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: readonly 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 sounding with a {@link Conflict} that raises a pairwise flag against it.\n *\n * @category Arrangement & Analysis\n */\nexport type ConflictPartner = {\n /** The partner's track index in the caller's input array. */\n trackIndex: number;\n /** The partner note's index in that track's own note array. */\n originalIndex?: number;\n /** The id the partner note carries in {@link TrackAnalysis.notes}. */\n noteId: number;\n /** The pairwise flags this note raises, as {@link SafetyPartner.reasons}. */\n reasons: number;\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 /** The clashing track's index in the caller's input array. */\n trackIndex: number;\n /** The note's index in that track's own note array. */\n originalIndex?: number;\n /** The id the note carries in {@link TrackAnalysis.notes}. */\n noteId: number;\n pitch: number;\n /**\n * The note's theory labels at its onset, so a caller can tell a genuine clash\n * from a passing tone, a neighbour, or a prepared suspension — all of which\n * are ordinary non-chord tones that still evaluate as unsafe.\n */\n labels: TheoryLabel[];\n /** The clash severity: {@link NoteSafety.Warning} or {@link NoteSafety.Dissonant}. */\n safety: NoteSafety;\n /** {@link 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 /**\n * The notes behind the pairwise flags in `reasons` — a parallel fifth, a\n * crossing, a vertical clash — when any are set.\n */\n partners?: ConflictPartner[];\n rationale?: string;\n};\n\n/**\n * Override an inferred result's key regions with ones the caller already had.\n *\n * The chords stay as inferred: they were found against the caller's regions\n * only if the caller also passed them down, and quietly re-deriving them here\n * would make the same options produce different chords depending on which pass\n * ran first.\n */\nfunction withGivenKeys(\n result: ChordTimelineResult,\n given: KeyRegion[] | undefined,\n): ChordTimelineResult {\n if (given === undefined) {\n return result;\n }\n return { ...result, keys: given, prevailingKey: prevailingKeyOf(given) ?? result.prevailingKey };\n}\n\n/**\n * Build the same result shape from a timeline the caller already had.\n *\n * The timeline was not inferred from these notes, so it carries no measured\n * confidence — reporting 1 would turn every confidence gate into an\n * unconditional pass. The key regions still come from the notes, because the\n * caller supplied chords, not an answer about the key; they are read over the\n * span the notes occupy, against the whole meter map, and annotated with their\n * pivots, exactly as the inferring path reads and annotates its own.\n */\nfunction callerTimeline(\n timeline: ChordTimeline,\n key: KeyLike | undefined,\n given: KeyRegion[] | undefined,\n pooled: NoteEvent[],\n meters: MeterMap,\n budget: number | undefined,\n): {\n timeline: ChordTimeline;\n keys: KeyRegion[];\n prevailingKey: ResolvedKey;\n segmentConfidence: number[];\n} {\n // The span is the one the notes sound over, not the one the chords cover: a\n // chart of the opening bars is a partial answer about the harmony and no\n // answer at all about where the piece ends, so stopping the key search at its\n // last chord would read every note after it against the opening key.\n const chartEnd = timeline.segments.reduce(\n (end, segment) => Math.max(end, segment.endBeat),\n Number.NEGATIVE_INFINITY,\n );\n const soundingEnd = latestSoundingEnd(pooled, Number.NEGATIVE_INFINITY);\n const spanEnd = Math.max(chartEnd, soundingEnd);\n const totalBeats = Number.isFinite(spanEnd) ? spanEnd : 0;\n // ...and it begins where the music begins, read from the one derivation of an\n // analysed span's origin that the inferring path and the tension curve read\n // too: an excerpt lifted from bar 9 holds no key over the eight bars of\n // silence a hardcoded 0 would report it as covering.\n const { startBeat: musicStart } = gridForNotes(pooled, beatsPerBarAt(0, meters));\n // Caller-supplied regions are the caller's answer and are passed through as\n // they are; the ones derived here are annotated like any other, since the\n // chords a pivot is read from are in hand either way.\n const keys =\n given ??\n attachPivots(\n key !== undefined\n ? statedKeyRegions(resolveKey(key), musicStart, totalBeats)\n : keyTimelineFromNotes(pooled, { meters, totalBeats, budget }),\n timeline.segments,\n );\n return {\n timeline,\n keys,\n prevailingKey: prevailingKeyOf(keys) ?? resolveKey(majorKey(0)),\n segmentConfidence: timeline.segments.map(() => 0),\n };\n}\n\n/**\n * The full result of {@link analyzeArrangement}.\n *\n * @category Arrangement & Analysis\n */\nexport type ArrangementAnalysis = {\n /**\n * The key regions the analysis ran against, in time order. Roman numerals,\n * harmonic function and note safety are all judged against the region\n * covering the beat, so a piece that modulates is not read against the key it\n * started in for every bar after it leaves.\n */\n keys: KeyRegion[];\n /**\n * The key held longest across {@link ArrangementAnalysis.keys}, whole: the\n * pitch classes with the spelling and the scale form the regions carry.\n */\n prevailingKey: ResolvedKey;\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 /**\n * A single key held across the whole arrangement. Omit it, and `keys` with\n * it, to have the key searched for over time so a modulating piece is read\n * against the key actually in force.\n *\n * Taken in whatever form the caller holds a key, and kept whole: the region\n * built from it carries the spelling and the scale form it was named with.\n */\n key?: KeyLike;\n /**\n * Key regions to judge against, when a previous pass already worked them out.\n * Takes precedence over `key`; supplying both is answering the same question\n * twice, and the more specific answer wins.\n */\n keys?: KeyRegion[];\n /**\n * A chord timeline to analyse against, instead of inferring one from the\n * notes. Supply it to analyse a hand-written progression, or to reuse the\n * timeline of an earlier call rather than re-running chord inference.\n */\n timeline?: ChordTimeline;\n /**\n * Indices of the tracks the harmony is inferred from. Defaults to every\n * pitched track. Percussion tracks are excluded either way. An index naming\n * no track is an input error rather than one that matches nothing.\n */\n harmonyTracks?: number[];\n /**\n * Lowest severity reported in `conflicts`.\n *\n * @defaultValue {@link NoteSafety.Warning}\n */\n minSeverity?: NoteSafety;\n /**\n * A single time signature held across the whole arrangement, as sugar for a\n * one-element `meters`; defaults to 4/4. Giving both is an input error.\n *\n * @defaultValue `4/4`\n */\n ts?: MeterLike;\n /**\n * The meter as it changes over the arrangement. Bar lines, downbeats and\n * metric weight all follow the signature in force at the beat in question.\n *\n * @defaultValue 4/4 throughout\n */\n meters?: MeterLike;\n /**\n * Length of the pickup in beats, when the piece starts with one.\n *\n * The first downbeat is beat 0, so an upbeat is written at negative beats.\n * Declaring its length rejects a note that starts before the pickup does;\n * leave it unset to accept any onset within ±`Number.MAX_SAFE_INTEGER`\n * (2^53 - 1).\n *\n * @defaultValue no declared pickup\n */\n pickupBeats?: number;\n /**\n * Expected chord length in beats; defaults to the length of the opening bar.\n *\n * A prior rather than a grid: the arrangement analysis infers its harmony\n * with dynamic segmentation, where the longer a chord is expected to last the\n * more evidence a change needs before the search will place one. A chord may\n * still begin at any position the notes argue for. Supply `timeline` to\n * analyse against boundaries you have fixed yourself.\n *\n * @defaultValue the length of the opening bar\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 * Upper bound on the work this call may do — note counts, windows, and\n * candidate counts are each checked against it before anything is allocated.\n *\n * Raise it to analyse a piece larger than the default allows; the default is\n * {@link DEFAULT_GENERATION_BUDGET}, chosen so a runaway input fails fast\n * rather than blocking the thread.\n *\n * @defaultValue {@link DEFAULT_GENERATION_BUDGET}\n */\n budget?: number;\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 */\ntype SoundingVoice = {\n track: number;\n voice: number;\n /** The track's index in the caller's input array. */\n trackIndex: number;\n note: PreparedNote;\n snapshot: VoiceSnapshot;\n};\n\n/** Build one complete arrangement snapshot for an evaluation beat. */\nfunction soundingVoicesAt(prepared: PreparedTrack[], beat: number): SoundingVoice[] {\n const out: SoundingVoice[] = [];\n for (let t = 0; t < prepared.length; t += 1) {\n const track = prepared[t];\n if (!track || isPercussion(track.role)) {\n continue;\n }\n for (let v = 0; v < track.voices.length; v += 1) {\n // Each sub-voice is monophonic, so this is one binary search per voice\n // rather than a scan of every note in the arrangement.\n const note = track.voices[v]?.at(beat);\n if (note === undefined) {\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 = sameInstant(note.startBeat, beat) ? note.prevPitch : note.pitch;\n if (previous !== undefined) {\n snap.prevPitch = previous;\n }\n out.push({ track: t, voice: v, trackIndex: track.trackIndex, note, snapshot: snap });\n }\n }\n return out;\n}\n\n/** Get every simultaneous voice except the one being evaluated. */\nfunction otherVoicesSounding(\n prepared: PreparedTrack[],\n excludeTrack: number,\n excludeVoice: number,\n beat: number,\n cache: Map<number, SoundingVoice[]>,\n): SoundingVoice[] {\n const sounding = cache.get(beat) ?? soundingVoicesAt(prepared, beat);\n cache.set(beat, sounding);\n return sounding.filter(({ track, voice }) => track !== excludeTrack || voice !== excludeVoice);\n}\n\n/** Name a sounding voice the way a {@link Conflict} names its own note. */\nfunction conflictPartner(sounding: SoundingVoice, reasons: number): ConflictPartner {\n const partner: ConflictPartner = {\n trackIndex: sounding.trackIndex,\n noteId: sounding.note.id,\n reasons,\n };\n if (sounding.note.originalIndex !== undefined) {\n partner.originalIndex = sounding.note.originalIndex;\n }\n return partner;\n}\n\n/** The snapshots {@link evaluateSafety} reads, in the order partners index them. */\nfunction snapshotsOf(voices: SoundingVoice[]): VoiceSnapshot[] {\n return voices.map(({ snapshot }) => snapshot);\n}\n\n/**\n * Label every note of one polyphonic passage against the harmony under it and\n * the voices around it.\n *\n * {@link analyzeVoice} reads a single line: it takes the note before and the note\n * after each note as that note's melodic neighbours, and it is given no other\n * voices, so over a flat array of everything that sounds it invents figures\n * across parts — the alto between two soprano notes becomes a passing tone —\n * and can never find a suspension, which is by definition a dissonance against\n * something else sounding. So the passage is partitioned into monophonic\n * sub-voices first and each is read with the others handed to it, which is what\n * {@link analyzeArrangement} does with the tracks it is given.\n *\n * @param notes The passage, in any order; the ids are indices into this array.\n * @param chordAtBeat Chord sounding at a given beat, or null.\n * @param key The key, or the key in force at a given beat.\n * @returns One annotation per note, in the order the notes were given, each\n * carrying its position in that array as its id.\n */\nexport function analyzePolyphony(\n notes: readonly NoteEvent[],\n chordAtBeat: (beat: number) => Chord | null,\n key: KeyContext,\n): AnalyzedNote[] {\n // The same reading of an event array the arrangement entrances apply, for the\n // same reason: the passage is walked note by note further down, where a null\n // in the array reports this library's own `TypeError` rather than the input.\n // Zero-length events are admitted here too — the split drops them.\n const passage = assertNoteEvents(notes, 'notes', { allowNonPositiveDuration: true });\n assertFunction<(beat: number) => Chord | null>(chordAtBeat, 'chordAtBeat');\n const prepared = prepareTracks([{ notes: [...passage] }]);\n const track = prepared[0];\n // Every note is read against the other sub-voices sounding under it, so the\n // work is the product of the two. This entry point takes no options, so it is\n // the default budget it is charged against; what matters is that it fails\n // before the cache is filled rather than after the comparisons are made.\n // Its siblings over the same machinery charge the same product.\n assertGenerationBudget(\n passage.length * (track?.voices.length ?? 0),\n 'polyphony note-voice comparisons',\n );\n const soundingCache = new Map<number, SoundingVoice[]>();\n const byIndex = new Map<number, AnalyzedNote>();\n for (let v = 0; v < (track?.voices.length ?? 0); v += 1) {\n const subVoice = track?.voices[v];\n if (subVoice === undefined) {\n continue;\n }\n const analyzed = analyzeVoice(subVoice.voice, chordAtBeat, key, (beat) =>\n snapshotsOf(otherVoicesSounding(prepared, 0, v, beat, soundingCache)),\n );\n for (const note of analyzed) {\n if (note.originalIndex !== undefined) {\n // The split's own bookkeeping stops here: the caller asked about its own\n // array, so the answer carries that position as the id and nothing that\n // says the notes were taken apart on the way.\n const { originalIndex, ...annotation } = note;\n byIndex.set(originalIndex, { ...annotation, noteId: originalIndex });\n }\n }\n }\n // A note of no length never sounds, so the split above drops it and no other\n // voice can be dissonant against it; it is still one of the caller's notes,\n // and reading it against the chord alone says what it is.\n return notes.map((note, index) => {\n const analyzed = byIndex.get(index);\n if (analyzed !== undefined) {\n return analyzed;\n }\n const [only] = analyzeVoice([{ ...note, id: index }], chordAtBeat, key);\n return (\n only ?? {\n noteId: index,\n pitch: note.pitch,\n startBeat: note.startBeat,\n durationBeat: note.durationBeat,\n labels: [],\n }\n );\n });\n}\n\n/** Index of the first segment starting strictly after a beat. */\nfunction firstSegmentAfter(timeline: ChordTimeline, beat: number): number {\n let low = 0;\n let high = timeline.segments.length;\n while (low < high) {\n const middle = Math.floor((low + high) / 2);\n if ((timeline.segments[middle]?.startBeat ?? Number.POSITIVE_INFINITY) <= beat + BEAT_EPS) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n return low;\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 *\n * Segments are in beat order, so the crossed ones are a contiguous run found by\n * binary search rather than by scanning the whole timeline for every note.\n */\nfunction evaluationBeats(note: VoiceNote, timeline: ChordTimeline): number[] {\n const noteEnd = note.startBeat + note.durationBeat;\n const beats = [note.startBeat];\n for (let i = firstSegmentAfter(timeline, note.startBeat); i < timeline.segments.length; i += 1) {\n const start = timeline.segments[i]?.startBeat;\n if (start === undefined || start >= noteEnd - BEAT_EPS) {\n break;\n }\n beats.push(start);\n }\n return beats;\n}\n\n/**\n * Analyse a whole arrangement against the harmony inferred from it.\n *\n * The chord timeline and the key regions it is read against are inferred from\n * the pooled notes of every pitched track (see\n * {@link chordTimelineFromNotes}); pooling all voices is\n * robust even when roles are absent or a track doubles the harmony, so it is\n * preferred over deriving the harmony from a subset. A track marked\n * `role: 'drums'` is excluded — its pitches name instruments, not harmony, and\n * pooling them would corrupt the key, the timeline and every reading built on\n * them. Pass `harmonyTracks` to restrict inference further, or `timeline` to\n * analyse against a progression you already have. Zero- and negative-length\n * notes never sound, so they are dropped at ingest and appear in neither the\n * annotations 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 * Every ordinary non-chord tone — a passing note, a neighbour, a prepared\n * suspension — is by definition unsafe against the chord under it, so the\n * conflict list is a report, not a fault list. Each conflict carries the note's\n * `labels` and its `trackIndex`/`originalIndex`, so a caller can tell those\n * apart and map a conflict back to the note it passed in; `minSeverity` narrows\n * the list to outright dissonance.\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 { prevailingKey, conflicts } = analyzeArrangement([{ role: 'melody', notes: melody }]);\n * prevailingKey.scale.rootPc; // the key held longest across the analysis\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 return analyzeArrangementWith(tracks, opts).analysis;\n}\n\n/**\n * Analyse an arrangement, carrying over the harmonic evidence an edit cannot\n * reach and handing back the evidence this pass leaves behind.\n *\n * Only the harmony is carried over. The per-note annotation pass is re-run in\n * full because neither half of it is local to the edit: sub-voice lane\n * assignment is a greedy left-to-right pass over the whole track, so an inserted\n * note can re-lane every note after it, and every note is evaluated against the\n * other voices sounding beneath it, so an edit in one track changes what every\n * other track hears at those beats.\n *\n * Not part of the public surface: the evidence is the private state of an\n * incremental re-analysis, and a caller holding it could pair it with notes it\n * was never derived from.\n *\n * @param tracks The tracks to analyse.\n * @param opts Analysis options.\n * @param previous Evidence from the analysis being updated, when there is one.\n * @param dirty The slots the edit may have changed.\n * @returns The analysis, and the evidence behind it when the harmony was\n * inferred rather than supplied.\n */\nexport function analyzeArrangementWith(\n tracks: ArrangementTrack[],\n opts: ArrangementOptions = {},\n previous?: TimelineEvidence,\n dirty?: DirtySlots,\n): { analysis: ArrangementAnalysis; evidence?: TimelineEvidence } {\n const meters = resolveMeters(opts, 'arrangement meters');\n const budget = opts.budget;\n const noteOptions: NoteEventAssertOptions = { allowNonPositiveDuration: true, budget };\n if (opts.pickupBeats !== undefined) {\n assertRange(opts.pickupBeats, 0, Number.MAX_SAFE_INTEGER, 'arrangement pickupBeats');\n noteOptions.minStartBeat = -opts.pickupBeats;\n }\n assertArray(tracks, 'arrangement tracks');\n assertGenerationBudget(tracks.length, 'arrangement tracks', budget);\n const noteCount = assertTrackNotes(tracks, noteOptions);\n // The pooled total is what the downstream timeline analysis actually sizes\n // its work by, so it is checked here, under this layer's own name.\n assertGenerationBudget(noteCount, 'arrangement notes', budget);\n const profile = arrangementProfile(opts.profile);\n // A severity outside the scale narrows the report to nothing, which reads\n // exactly like a clean arrangement; the class API refuses it, so the function\n // it delegates to has to refuse the same values.\n const minSeverity =\n opts.minSeverity === undefined\n ? NoteSafety.Warning\n : assertInteger(\n opts.minSeverity,\n 'arrangement minSeverity',\n NoteSafety.Safe,\n NoteSafety.Dissonant,\n );\n // The two options a caller can hand a previous pass's work through are read\n // for their shape here, with the rest of the options, rather than at the line\n // that first dereferences them several layers down.\n assertGivenTimeline(opts.timeline, 'arrangement timeline');\n assertGivenKeys(opts.keys, 'arrangement keys');\n const harmonyTracks = harmonyTrackSet(opts.harmonyTracks, tracks);\n const prepared = prepareTracks(tracks);\n // Every note is evaluated against the other sub-voices sounding beneath it, so\n // the work is the product of the two — not either dimension alone, which is\n // all the scalar checks above measure. The sub-voices have to be derived to be\n // counted, but that is the same O(n log n) pass the note checks already are,\n // and the check still lands before the harmony is pooled and inferred.\n const voiceCount = prepared.reduce((sum, track) => sum + track.voices.length, 0);\n assertGenerationBudget(noteCount * voiceCount, 'arrangement note-voice comparisons', budget);\n const pooled = poolNotes(tracks, harmonyTracks);\n\n let evidence: TimelineEvidence | undefined;\n let inferred: ChordTimelineResult;\n if (opts.timeline === undefined) {\n const run = analyzeTimeline(\n pooled,\n {\n key: opts.key,\n meters,\n pickupBeats: opts.pickupBeats,\n harmonicRhythm: opts.harmonicRhythm,\n budget,\n },\n previous,\n dirty,\n );\n evidence = run.evidence;\n inferred = withGivenKeys(run.result, opts.keys);\n } else {\n inferred = callerTimeline(opts.timeline, opts.key, opts.keys, pooled, meters, budget);\n }\n const { timeline, keys, prevailingKey, segmentConfidence } = inferred;\n const keyAt = keyLookup(keys, prevailingKey);\n // The pitch classes of the key in force, for the readings below that ask\n // which notes are in the key rather than how it is written. Named here so the\n // spelling is dropped once, where it can be seen, instead of at each call.\n const scaleAt = (beat: number) => scaleOf(keyAt(beat));\n // A cadence is heard in the key it arrives in, which after a modulation is\n // not the key the piece opened in.\n const cadences = detectCadences(timeline, scaleAt);\n const soundingCache = new Map<number, SoundingVoice[]>();\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 if (isPercussion(track.role)) {\n // A drum hit has no harmonic reading, so labelling it against the chord\n // would produce noise rather than analysis.\n trackAnalyses.push({ name: track.name, role: track.role, notes: [] });\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 const analyzed = analyzeVoice(subVoice.voice, timeline.at, scaleAt, (beat) =>\n snapshotsOf(otherVoicesSounding(prepared, t, v, beat, soundingCache)),\n );\n // Appended one at a time: a spread of a long sub-voice passes every note\n // as an argument, which overflows the call stack on a track the budget\n // above accepts.\n for (const note of analyzed) {\n notes.push({ ...note, trackIndex: track.trackIndex });\n }\n const labelsById = new Map(analyzed.map((note) => [note.noteId, note.labels]));\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 = sameInstant(beat, note.startBeat);\n const others = otherVoicesSounding(prepared, t, v, beat, soundingCache);\n const safetyQuery = {\n profile,\n candidatePitch: note.pitch,\n prevPitch: atOnset ? preparedNote.prevPitch : note.pitch,\n chord: timeline.at(beat),\n key: scaleAt(beat),\n otherVoices: snapshotsOf(others),\n strongBeat: isStrongBeat(beat, meters),\n };\n // Most evaluations become no reportable conflict. Avoid their\n // replacement-pitch scan (up to 24 recursive safety checks), then\n // collect suggestions only for the conflicts we will retain.\n let result = evaluateSafety(safetyQuery, { suggestions: false });\n if (result.safety !== NoteSafety.Safe && result.safety >= minSeverity) {\n result = evaluateSafety(safetyQuery, { partners: true });\n const conflict: Conflict = {\n beat,\n trackName: track.name,\n trackIndex: track.trackIndex,\n noteId: note.id,\n pitch: note.pitch,\n labels: labelsById.get(note.id) ?? [],\n safety: result.safety,\n reasons: result.reasons,\n };\n if (note.originalIndex !== undefined) {\n conflict.originalIndex = note.originalIndex;\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 if (result.partners !== undefined) {\n conflict.partners = result.partners.map(({ index, reasons }) =>\n conflictPartner(others[index] as SoundingVoice, reasons),\n );\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 const analysis: ArrangementAnalysis = {\n keys,\n prevailingKey,\n timeline,\n segmentConfidence,\n cadences,\n tracks: trackAnalyses,\n conflicts,\n };\n return evidence === undefined ? { analysis } : { analysis, evidence };\n}\n","/**\n * Incremental arrangement analysis: an analysis held open across edits, so a\n * host that re-analyses on every keystroke pays for the beats that changed\n * rather than for the whole piece.\n */\n\nimport { InvalidInputError } from '../../core/errors/index.js';\nimport type { NoteEvent } from '../../core/types.js';\nimport {\n assertGenerationBudget,\n assertNoteEvents,\n describeRejected,\n} from '../../core/validation/index.js';\nimport type { DirtySlots, TimelineEvidence } from '../timeline/index.js';\nimport { dirtySlotsFor } from '../timeline/index.js';\nimport type { ArrangementAnalysis, ArrangementOptions, ArrangementTrack } from './tracks.js';\nimport { analyzeArrangementWith } from './tracks.js';\n\n/**\n * One track's notes as they stand after an edit.\n *\n * The whole note array is given rather than a description of what moved: a host\n * always has the track's current contents, and working out what actually\n * changed is this module's job, not the caller's.\n *\n * @category Arrangement & Analysis\n */\nexport type TrackEdit = {\n /** Index of the track in the session's track array. */\n trackIndex: number;\n /** The track's notes after the edit. */\n notes: readonly NoteEvent[];\n};\n\n/**\n * An arrangement analysis held open for editing.\n *\n * @category Arrangement & Analysis\n */\nexport type ArrangementSession = {\n /** The tracks the current analysis was made from. */\n readonly tracks: readonly ArrangementTrack[];\n /** The analysis of those tracks. */\n readonly analysis: ArrangementAnalysis;\n /**\n * Re-analyse after replacing the notes of one or more tracks, reusing\n * everything the edit provably cannot reach.\n *\n * The result equals {@link analyzeArrangement} over the edited tracks — the\n * same segments, confidences, key regions, cadences, annotations and\n * conflicts — so a host may hold a session for as long as it likes without\n * the analysis drifting from what a fresh pass would say.\n */\n update: (edits: readonly TrackEdit[]) => ArrangementSession;\n};\n\n/** Identity of one note, for telling an edited note array from an unedited one. */\nfunction noteId(note: NoteEvent): string {\n return `${note.pitch}|${note.startBeat}|${note.durationBeat}|${note.velocity ?? ''}`;\n}\n\n/** The beats an edit touched, or null when the two note arrays agree. */\ntype EditedSpan = { startBeat: number; endBeat: number };\n\n/**\n * Whether two note arrays hold the same notes in the same positions.\n *\n * A re-ordering changes no beat of the music, but `originalIndex` reports a\n * note's position in the caller's array, so the annotations still have to be\n * remade against the new order.\n */\nfunction sameOrder(before: readonly NoteEvent[], after: readonly NoteEvent[]): boolean {\n if (before.length !== after.length) {\n return false;\n }\n for (let index = 0; index < before.length; index += 1) {\n const previous = before[index];\n const current = after[index];\n if (previous === undefined || current === undefined || noteId(previous) !== noteId(current)) {\n return false;\n }\n }\n return true;\n}\n\n/**\n * The beats covered by the notes that `before` and `after` disagree on.\n *\n * Notes are compared as values and by multiplicity, so re-ordering a track's\n * array — which no beat of the harmony is sensitive to — covers no beats at\n * all, while adding a second copy of a note covers the one it sounds on.\n */\nfunction editedSpan(before: readonly NoteEvent[], after: readonly NoteEvent[]): EditedSpan | null {\n const counts = new Map<string, { count: number; startBeat: number; endBeat: number }>();\n const tally = (notes: readonly NoteEvent[], sign: number): void => {\n for (const note of notes) {\n const id = noteId(note);\n const entry = counts.get(id);\n if (entry === undefined) {\n counts.set(id, {\n count: sign,\n startBeat: note.startBeat,\n endBeat: note.startBeat + note.durationBeat,\n });\n } else {\n entry.count += sign;\n }\n }\n };\n tally(before, 1);\n tally(after, -1);\n let startBeat = Number.POSITIVE_INFINITY;\n let endBeat = Number.NEGATIVE_INFINITY;\n for (const entry of counts.values()) {\n if (entry.count === 0) {\n continue;\n }\n startBeat = Math.min(startBeat, entry.startBeat);\n // A note of zero or negative length never sounds, but its onset still marks\n // where the caller edited, so the span is never empty for a real change.\n endBeat = Math.max(endBeat, entry.endBeat, entry.startBeat);\n }\n return startBeat <= endBeat ? { startBeat, endBeat } : null;\n}\n\n/** Merge two edited spans into the one covering both. */\nfunction unionSpan(a: EditedSpan | null, b: EditedSpan | null): EditedSpan | null {\n if (a === null) {\n return b;\n }\n if (b === null) {\n return a;\n }\n return {\n startBeat: Math.min(a.startBeat, b.startBeat),\n endBeat: Math.max(a.endBeat, b.endBeat),\n };\n}\n\n/** Build a session around one completed analysis. */\nfunction sessionOf(\n tracks: readonly ArrangementTrack[],\n opts: ArrangementOptions,\n analysis: ArrangementAnalysis,\n evidence: TimelineEvidence | undefined,\n): ArrangementSession {\n return {\n tracks,\n analysis,\n update(edits) {\n assertGenerationBudget(edits.length, 'arrangement edits', opts.budget);\n const next = [...tracks];\n let edited: EditedSpan | null = null;\n let reordered = false;\n for (let index = 0; index < edits.length; index += 1) {\n const edit = edits[index];\n if (edit === undefined) {\n continue;\n }\n const target = next[edit.trackIndex];\n if (target === undefined) {\n throw new InvalidInputError(\n `edits[${index}].trackIndex ${edit.trackIndex} is outside the session's ${tracks.length} tracks`,\n );\n }\n if (!Array.isArray(edit.notes)) {\n throw new InvalidInputError(\n `edits[${index}].notes must be an array; received ${describeRejected(edit.notes)}`,\n );\n }\n assertNoteEvents(edit.notes, `edits[${index}].notes`, {\n allowNonPositiveDuration: true,\n budget: opts.budget,\n });\n edited = unionSpan(edited, editedSpan(target.notes, edit.notes));\n reordered = reordered || !sameOrder(target.notes, edit.notes);\n next[edit.trackIndex] = { ...target, notes: edit.notes };\n }\n // Nothing the analysis reads actually changed, so the analysis has not.\n if (edited === null && !reordered) {\n return sessionOf(next, opts, analysis, evidence);\n }\n // A pure re-ordering moves no note, so every slot of the harmony is clean\n // and the whole timeline is carried over; what it does move is the array\n // position each annotation reports, so the annotation pass is re-run.\n const dirty: DirtySlots | undefined =\n evidence === undefined\n ? undefined\n : edited === null\n ? { from: 0, to: 0 }\n : dirtySlotsFor(evidence, edited.startBeat, edited.endBeat);\n const run = analyzeArrangementWith(next, opts, evidence, dirty);\n return sessionOf(next, opts, run.analysis, run.evidence);\n },\n };\n}\n\n/**\n * Open an arrangement analysis that can be updated a few beats at a time.\n *\n * The first pass is an ordinary {@link analyzeArrangement}; what the session\n * adds is the slot-level evidence behind it, so a later {@link\n * ArrangementSession.update} can work out which beats an edit could possibly\n * have changed and leave the rest of the harmony alone. An update returns a new\n * session, so the analysis before an edit stays valid and a host can keep it\n * for undo.\n *\n * What an update recomputes is derived rather than guessed at:\n *\n * - The edited beats are the beats covered by the notes the old and new track\n * arrays disagree on, which is narrower than the track and usually far\n * narrower than the piece.\n * - A slot's evidence sees exactly the notes overlapping it, so the slots to\n * re-derive are the slots overlapping those beats — plus one either side, so\n * that an onset sitting a floating-point hair off a slot boundary cannot\n * leave a stale slot behind.\n * - The boundary search itself has no such margin. It is a shortest-path search\n * over a whole run of slots, so a changed slot can move a boundary anywhere in\n * the run, and its change cost is calibrated on the mean weight of every\n * sounding slot in the piece, which couples the runs to one another. The\n * search is therefore re-run over the whole piece — arithmetic over the\n * carried-over evidence, with nothing allocated per slot — rather than being\n * spliced at a margin that does not exist.\n * - Each settled segment's chord is carried over when its bounds, its key and\n * its slots are all unchanged, since those are the only things chord\n * inference reads.\n *\n * Key regions and the per-note annotations are recomputed in full. Both are\n * whole-piece passes — the key search is a shortest-path search with no reset\n * points, sub-voice lane assignment is greedy from the first note, and every\n * note is judged against the other voices sounding beneath it — so neither has\n * a sound local answer, and reporting one would mean an update that quietly\n * disagreed with a fresh analysis.\n *\n * An update falls back to a full analysis whenever the edit moves the ground the\n * evidence stands on: a changed piece length, a changed first onset (both of\n * which move the slot grid), or an edit to a session whose harmony was supplied\n * through {@link ArrangementOptions.timeline}. The answer is the same either\n * way; only the work differs.\n *\n * @param tracks The tracks to analyse.\n * @param opts Analysis options; see {@link ArrangementOptions}.\n * @returns A session holding the analysis and the evidence behind it.\n * @example\n * ```ts\n * import { createArrangementSession } from '@libraz/libcantus';\n * const melody = [{ pitch: 60, startBeat: 0, durationBeat: 4 }];\n * const session = createArrangementSession([{ role: 'melody', notes: melody }]);\n * const moved = [{ pitch: 62, startBeat: 0, durationBeat: 4 }];\n * const next = session.update([{ trackIndex: 0, notes: moved }]);\n * next.analysis.timeline.segments; // as if analyzed from scratch\n * ```\n * @category Arrangement & Analysis\n */\nexport function createArrangementSession(\n tracks: ArrangementTrack[],\n opts: ArrangementOptions = {},\n): ArrangementSession {\n const run = analyzeArrangementWith(tracks, opts);\n return sessionOf([...tracks], opts, run.analysis, run.evidence);\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 type { MeterMap } from '../../core/meter/index.js';\nimport { isStrongBeat, resolveMeters } from '../../core/meter/index.js';\nimport { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\nimport type { NoteEventAssertOptions } from '../../core/validation/index.js';\nimport {\n assertArray,\n assertGenerationBudget,\n assertInteger,\n assertRange,\n assertRecord,\n} from '../../core/validation/index.js';\nimport { chordPitchClasses } from '../../theory/chord/index.js';\nimport { evaluateSafety, NoteSafety, type SafetyProfile } from '../../theory/safety/index.js';\nimport { majorKey, type ResolvedKey, resolveKey } from '../../theory/scale/index.js';\nimport { BEAT_EPS } from '../adjacency.js';\nimport { functionOf } from '../functional/index.js';\nimport { gridForNotes, latestSoundingEnd } from '../grid.js';\nimport { keyLookup, keyTimelineFromNotes, prevailingKeyOf } from '../keys/index.js';\nimport { type ChordTimeline, chordTimelineFromNotes, statedKeyRegions } from '../timeline/index.js';\nimport {\n arrangementProfile,\n assertGivenKeys,\n assertGivenTimeline,\n assertTrackNotes,\n harmonyTrackSet,\n isPercussion,\n type PreparedTrack,\n poolNotes,\n prepareTracks,\n} from './internal.js';\nimport type { ArrangementAnalysis, 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. */\nexport const 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. */\nexport const FUNCTION_WEIGHT = 0.5;\n/** Weight of the non-chord-tone / dissonance term. */\nexport const DISSONANCE_WEIGHT = 0.35;\n/** Weight of the registral-span term. */\nexport const SPAN_WEIGHT = 0.15;\n/** Pitch span, in semitones, that saturates the span term. */\nexport const SPAN_SATURATION = 24;\n/** Distinct MIDI pitches a sample can hold, which bounds the safety evaluations it makes. */\nexport const MAX_DISTINCT_PITCHES = 128;\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 `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 `DISSONANCE_WEIGHT`;\n * - the registral span of the sounding notes, saturating at\n * `SPAN_SATURATION` semitones, weighted `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 meters = resolveMeters(opts, 'arrangement meters');\n const budget = opts.budget;\n const noteOptions: NoteEventAssertOptions = { allowNonPositiveDuration: true, budget };\n if (opts.pickupBeats !== undefined) {\n assertRange(opts.pickupBeats, 0, Number.MAX_SAFE_INTEGER, 'arrangement pickupBeats');\n noteOptions.minStartBeat = -opts.pickupBeats;\n }\n assertArray(tracks, 'arrangement tracks');\n assertGenerationBudget(tracks.length, 'arrangement tracks', budget);\n const noteCount = assertTrackNotes(tracks, noteOptions);\n // The pooled total is what the sampling and the inferred timeline size their\n // work by, so it is checked here, under this layer's own name.\n assertGenerationBudget(noteCount, 'arrangement notes', budget);\n const profile = arrangementProfile(opts.profile);\n const step = opts.step ?? 1;\n assertRange(step, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, 'tension sampling step');\n // Read by no sampling this call makes, and checked all the same: the option is\n // one of the arrangement options this entry point accepts, and a value the\n // sibling entry point refuses cannot be one this one silently takes.\n if (opts.minSeverity !== undefined) {\n assertInteger(\n opts.minSeverity,\n 'arrangement minSeverity',\n NoteSafety.Safe,\n NoteSafety.Dissonant,\n );\n }\n\n // The two options a caller can hand a previous pass's work through are read\n // for their shape here, with the rest of the options, rather than at the line\n // that first dereferences them several layers down.\n assertGivenTimeline(opts.timeline, 'arrangement timeline');\n assertGivenKeys(opts.keys, 'arrangement keys');\n const harmonyTracks = harmonyTrackSet(opts.harmonyTracks, tracks);\n const pooled = poolNotes(tracks, harmonyTracks);\n const all = poolNotes(tracks);\n const totalBeats = latestSoundingEnd(all);\n // The grid the samples sit on is a whole number of steps from beat 0, and the\n // music itself starts at `musicStart`: a pickup sounds before beat 0 and an\n // excerpt lifted from bar 9 begins there, and neither is sampled across\n // silence it never had.\n const { origin: sampleStart, startBeat: musicStart } = gridForNotes(all, step);\n // A caller-supplied timeline still needs key regions to be read against: the\n // ones the caller gave, the single key they named, or the ones the notes\n // themselves give, since supplying chords answers nothing about the key.\n const suppliedKeys =\n opts.timeline === undefined\n ? undefined\n : (opts.keys ??\n (opts.key !== undefined\n ? // The one region a named key holds starts where the music does, as\n // the searched regions and the timeline's own key window do: a key\n // cannot be in force over beats nothing sounds in.\n statedKeyRegions(resolveKey(opts.key), musicStart, totalBeats)\n : keyTimelineFromNotes(pooled, { meters, totalBeats, budget })));\n const { timeline, keys, prevailingKey } =\n opts.timeline === undefined || suppliedKeys === undefined\n ? chordTimelineFromNotes(pooled, {\n key: opts.key,\n meters,\n pickupBeats: opts.pickupBeats,\n harmonicRhythm: opts.harmonicRhythm,\n budget,\n })\n : {\n timeline: opts.timeline,\n keys: suppliedKeys,\n prevailingKey: prevailingKeyOf(suppliedKeys) ?? resolveKey(majorKey(0)),\n };\n // Harmonic tension is judged against the key in force at the sample, not\n // against one key for the piece: a chord is only tense relative to a tonic.\n const regions = opts.keys ?? keys;\n const keyAt = keyLookup(regions, prevailingKeyOf(regions) ?? prevailingKey);\n\n const prepared = prepareTracks(tracks);\n const points: TensionPoint[] = [];\n const sampleCount = Math.max(0, Math.ceil((totalBeats - sampleStart) / step - BEAT_EPS));\n assertGenerationBudget(sampleCount, 'tension samples', budget);\n // The per-sample cost is one lookup per sub-voice, so it is their product —\n // not either dimension alone — that has to stay inside the budget.\n const voiceCount = prepared.reduce((sum, track) => sum + track.voices.length, 0);\n assertGenerationBudget(\n sampleCount * Math.min(MAX_DISTINCT_PITCHES, voiceCount) * voiceCount,\n 'tension sample-voice comparisons',\n budget,\n );\n for (let i = 0; i < sampleCount; i += 1) {\n // The first sample is taken where the music starts rather than at the slot\n // boundary before it, so a curve carries no phantom lead-in over silence the\n // piece never had; every later sample stays a whole number of steps from\n // beat 0, which is what keeps them on the bar lines.\n const beat = i === 0 ? Math.max(sampleStart, musicStart) : sampleStart + i * step;\n points.push({\n beat,\n // The key whole: an augmented sixth is told from a dominant by its letters.\n tension: sampleTension(prepared, timeline, keyAt(beat), meters, profile, beat),\n });\n }\n return points;\n}\n\n/**\n * Sample the tension of an arrangement already analysed by\n * {@link analyzeArrangement}, reusing its harmony instead of inferring it again.\n *\n * `analyzeArrangement` followed by `tensionCurve` runs chord and key inference\n * twice over the same notes. This takes the analysis it already produced, so\n * the curve and the annotations describe the same harmony by construction.\n *\n * The analysis is the harmony to fall back on rather than one laid over the\n * caller: `key`, `keys` and `harmonyTracks` each name a harmony the analysis\n * does not hold, so naming one is asking for a different reading, and the curve\n * is taken under it. Nothing else about the analysis is re-derived.\n *\n * @param tracks The same tracks that were analysed.\n * @param analysis The result of {@link analyzeArrangement} for those tracks.\n * @param opts Analysis options plus an optional `step` (default one beat).\n * @returns One {@link TensionPoint} per sampled beat, in beat order.\n * @example\n * ```ts\n * import { analyzeArrangement, tensionCurveFrom } from '@libraz/libcantus';\n * const tracks = [{ notes: [{ pitch: 60, startBeat: 0, durationBeat: 4 }] }];\n * const analysis = analyzeArrangement(tracks);\n * tensionCurveFrom(tracks, analysis, { step: 1 });\n * ```\n * @category Arrangement & Analysis\n */\nexport function tensionCurveFrom(\n tracks: ArrangementTrack[],\n analysis: ArrangementAnalysis,\n opts: ArrangementOptions & { step?: number } = {},\n): TensionPoint[] {\n const asked = assertRecord<ArrangementOptions & { step?: number }>(opts, 'opts');\n const carried = carriedHarmony(assertRecord<ArrangementAnalysis>(analysis, 'analysis'), asked);\n return tensionCurve(tracks, { ...carried, ...asked });\n}\n\n/**\n * How much of an analysis's harmony a set of options leaves in place.\n *\n * Restricting the harmony to a few tracks changes which notes state it, so\n * nothing of the analysis survives; naming a key or its regions changes only\n * what the chords are heard against, so the chords themselves are kept.\n */\nfunction carriedHarmony(\n analysis: ArrangementAnalysis,\n opts: ArrangementOptions,\n): ArrangementOptions {\n if (opts.harmonyTracks !== undefined) {\n return {};\n }\n if (opts.key !== undefined || opts.keys !== undefined) {\n return { timeline: analysis.timeline };\n }\n return { timeline: analysis.timeline, keys: analysis.keys };\n}\n\n/**\n * All sounding pitched notes across every track at a beat, tagged with their\n * track. Percussion carries no harmony, so it is skipped; each sub-voice is\n * monophonic, so its contribution is one binary search rather than a scan.\n */\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 || isPercussion(track.role)) {\n continue;\n }\n for (const subVoice of track.voices) {\n const note = subVoice.at(beat);\n if (note !== undefined) {\n out.push({ pitch: note.pitch, track: t });\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: ResolvedKey,\n meters: MeterMap,\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, meters);\n let nonChord = 0;\n let dissonant = 0;\n // A coarse tension sample supplies no individual voice history. Every voice\n // at the same MIDI pitch consequently receives the same safety verdict, so\n // a dense pad needs at most 128 evaluations instead of one per voice.\n const pitches = new Map<number, number>();\n for (const voice of sounding) {\n pitches.set(voice.pitch, (pitches.get(voice.pitch) ?? 0) + 1);\n }\n // The voices a candidate is weighed against are the texture with one of its\n // own occurrences taken out, so the texture is written down once for the\n // sample and that one occurrence is lifted out and put back around each\n // evaluation. Rebuilding it per distinct pitch instead allocated a record per\n // voice per pitch — a hundred-voice pad spending ten thousand objects on a\n // beat to say the same thing a hundred times.\n const others: { pitch: number }[] = [];\n const firstOf = new Map<number, number>();\n for (const [pitch, count] of pitches) {\n firstOf.set(pitch, others.length);\n for (let i = 0; i < count; i += 1) {\n others.push({ pitch });\n }\n }\n for (const [pitch, count] of pitches) {\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(pitch))) {\n nonChord += count;\n }\n // One occurrence of the candidate's own pitch is moved to the end and\n // dropped, which leaves exactly the multiset the caller would have built.\n const at = firstOf.get(pitch) ?? 0;\n const last = others.length - 1;\n const held = others[at];\n const tail = others[last];\n if (held !== undefined && tail !== undefined) {\n others[at] = tail;\n others.pop();\n }\n // Only the verdict is read, so the replacement-pitch search is skipped.\n const result = evaluateSafety(\n { profile, candidatePitch: pitch, chord, key: key.scale, otherVoices: others, strongBeat },\n { suggestions: false },\n );\n if (held !== undefined && tail !== undefined) {\n others.push(tail);\n others[at] = held;\n }\n if (result.safety === NoteSafety.Dissonant) {\n dissonant += count;\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","/**\n * Hypermeter: the grouping of bars into hyperbars.\n *\n * Bars are felt in groups the way beats are felt in bars — four bars at a time\n * in most tonal and popular music, but two, three, six or eight elsewhere. The\n * grouping is not written in the score, so it has to be inferred from where the\n * music actually restarts: harmony changes on a hypermetric downbeat, and\n * cadences arrive at the end of a hyperbar.\n */\n\nimport type { MeterLike } from '../../core/meter/index.js';\nimport { barIndexAt, resolveMeters } from '../../core/meter/index.js';\nimport type { NoteEvent } from '../../core/types.js';\nimport {\n assertGenerationBudget,\n assertNoteEvents,\n assertOptions,\n} from '../../core/validation/index.js';\nimport type { ChordSegment } from '../../theory/chord/index.js';\nimport { BEAT_EPS } from '../adjacency.js';\nimport { barGridStart, latestSoundingEnd } from '../grid.js';\nimport { segmentStartingAt } from '../timeline/index.js';\nimport type { BarSlice } from './internal.js';\nimport {\n clamp01,\n firstSoundingBeat,\n floorMod,\n harmonicNovelty,\n lastBarOf,\n resolveSpanEnd,\n sliceBars,\n} from './internal.js';\n\n/**\n * The bar groupings the search considers.\n *\n * Four is the norm, two and eight its halving and doubling, three and six the\n * triple-hypermetre alternatives. Anything else is better described as an\n * irregular succession of these than as a grouping of its own.\n */\nexport const GROUP_CANDIDATES: readonly number[] = [2, 3, 4, 6, 8];\n\n/**\n * How readily each grouping is believed before the music is consulted.\n *\n * The prior only settles ties: a piece that is plainly in three does not become\n * a piece in four because four is commoner. It is what decides a genuinely\n * ambiguous case, where two-bar and four-bar readings explain the accents\n * equally well.\n */\nexport const GROUP_PRIOR_LIST: readonly (readonly [number, number])[] = [\n [2, 0.9],\n [3, 0.75],\n [4, 1],\n [6, 0.7],\n [8, 0.85],\n];\n\n/** {@link GROUP_PRIOR_LIST} by grouping. */\nconst GROUP_PRIOR: ReadonlyMap<number, number> = new Map(GROUP_PRIOR_LIST);\n\n/** Share of the evidence carried by harmonic change landing on group heads. */\nexport const CONTRAST_SHARE = 0.65;\n\n/** Share of the evidence carried by cadences landing at group ends. */\nexport const CADENCE_SHARE = 0.35;\n\n/** How much of a group end's fit a cadence arriving in it earns. */\nexport const GROUP_FINAL_FIT = 1;\n\n/**\n * How much of a group end's fit an arrival on the next group's head earns.\n *\n * An elided cadence lands on the downbeat that starts the next hyperbar, so the\n * reading is real; it is weaker evidence than a group-final arrival only\n * because a chord on a hypermetric downbeat is unremarkable on its own.\n */\nexport const ELIDED_FIT = 0.5;\n\n/**\n * How many hyperbars the span must hold before a grouping is considered.\n *\n * One statement of a grouping is not evidence of a grouping: any span is\n * trivially one group of its own length.\n */\nexport const MIN_GROUPS = 2;\n\n/** Share of the confidence that survives a rival grouping scoring just as well. */\nexport const MARGIN_FLOOR = 0.7;\n\n/**\n * A bar grouping inferred from the music.\n *\n * @category Arrangement & Analysis\n */\nexport type Hypermeter = {\n /**\n * Bars per hyperbar. Falls back to the span's own bar count when the span is\n * too short to hold two of any candidate grouping.\n */\n groupBars: number;\n /**\n * Absolute beat of each hypermetric downbeat inside the analysed span, in\n * time order. A pickup bar is not a group head, so the first entry is the\n * first full bar that starts a group.\n */\n downbeats: number[];\n /**\n * How much the music supports the grouping, in [0, 1]: how strongly harmonic\n * change and cadences prefer these bars over the others, discounted when the\n * runner-up grouping explains the music nearly as well.\n */\n confidence: number;\n /** Why this grouping was chosen. */\n rationale: string;\n};\n\n/**\n * Options for {@link hypermeter}.\n *\n * @category Arrangement & Analysis\n */\nexport type HypermeterOptions = {\n /**\n * End of the analysed 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 * Beats at which cadences arrive, as {@link CadenceHit.atBeat} reports them.\n *\n * A cadence is the clearest hypermetric evidence there is — it arrives at the\n * end of a hyperbar — but finding one needs a chord timeline and a key, which\n * this function is not given. Callers that already hold cadences should pass\n * them; {@link phrasesFromTimeline} does.\n *\n * @defaultValue none\n */\n cadenceBeats?: readonly number[];\n /**\n * Upper bound on the work this call may do.\n *\n * @defaultValue {@link DEFAULT_GENERATION_BUDGET}\n */\n budget?: number;\n};\n\n/**\n * A cadence as the form analyses read it: the span its arrival chord sounds\n * through, and how strongly it marks the end of a group.\n *\n * Phrase boundaries and hyperbar ends are both read from this one record, so a\n * held arrival chord is credited the same way by both.\n */\nexport type CadenceSpan = {\n /** The beat the cadence arrives on. */\n atBeat: number;\n /** Where the arrival chord stops sounding; `atBeat` when only the arrival is known. */\n endBeat: number;\n /** How strongly the cadence marks a boundary, in [0, 1]. */\n strength: number;\n};\n\n/**\n * Each cadence as the span its arrival chord sounds through.\n *\n * @param arrivals The beat each cadence arrives on, with its strength.\n * @param segments The chord timeline's segments; with none, each span is the\n * arrival beat alone.\n * @returns One span per arrival, in the same order.\n */\nexport function cadenceSpans(\n arrivals: readonly { atBeat: number; strength: number }[],\n segments: readonly ChordSegment[],\n): CadenceSpan[] {\n return arrivals.map(({ atBeat, strength }) => ({\n atBeat,\n endBeat: segmentStartingAt(segments, atBeat)?.endBeat ?? atBeat,\n strength,\n }));\n}\n\n/** A cadence read in bars: where it arrives, and the last bar its chord sounds in. */\ntype CadenceBar = {\n arrival: number;\n last: number;\n strength: number;\n};\n\n/** Read cadence spans as bars. */\nfunction cadenceBarsOf(spans: readonly CadenceSpan[], meter: MeterLike): CadenceBar[] {\n return spans.map((span) => ({\n arrival: barIndexAt(span.atBeat, meter),\n last: lastBarOf(meter, span.atBeat, span.endBeat),\n strength: span.strength,\n }));\n}\n\n/** One grouping-and-phase reading, with the evidence behind it. */\ntype Reading = {\n groupBars: number;\n phase: number;\n contrast: number;\n cadenceFit: number;\n evidence: number;\n score: number;\n};\n\n/**\n * How much more the group heads are accented than the bars between them.\n *\n * The difference of the two means rather than the head mean alone: a piece\n * whose every bar changes harmony accents its group heads no more than anything\n * else, and a reading that cannot tell the two apart has found nothing.\n */\nfunction accentContrast(\n novelty: readonly number[],\n slices: readonly BarSlice[],\n groupBars: number,\n phase: number,\n): number {\n let headSum = 0;\n let headCount = 0;\n let otherSum = 0;\n let otherCount = 0;\n for (let i = 0; i < slices.length; i += 1) {\n const slice = slices[i];\n if (slice === undefined) {\n continue;\n }\n const value = novelty[i] ?? 0;\n if (floorMod(slice.index - phase, groupBars) === 0) {\n headSum += value;\n headCount += 1;\n } else {\n otherSum += value;\n otherCount += 1;\n }\n }\n if (headCount === 0 || otherCount === 0) {\n return 0;\n }\n return headSum / headCount - otherSum / otherCount;\n}\n\n/** How well a reading's group ends meet the cadences. */\ntype CadenceFit = {\n /** Share of the group ends a cadence closes, whatever its type. */\n closed: number;\n /** The same share with each end weighed by the strongest cadence closing it. */\n weighted: number;\n};\n\n/**\n * Share of the hyperbar ends the reading predicts that a cadence closes.\n *\n * Measured over the group ends rather than over the cadences, because the two\n * count different things: a cadence every four bars sits at a group end under a\n * two-bar reading as readily as under a four-bar one, so counting cadences\n * cannot tell the two apart. Counting group ends can — half the two-bar groups\n * end in nothing. The weighted share is what tells two phases apart: a passing\n * plagal close in mid-phrase must not tie the half and authentic cadences that\n * end a period's phrases.\n */\nfunction cadenceFitOf(\n cadences: readonly CadenceBar[],\n slices: readonly BarSlice[],\n groupBars: number,\n phase: number,\n): CadenceFit {\n const lastBar = slices[slices.length - 1]?.index ?? -1;\n // Each cadence counts once: it closes the first group end at or after its\n // arrival while its chord still sounds there, and one held further closes no\n // later group. Failing that, one arriving on a group head is elided into it.\n const closing = new Map<number, number>();\n const eliding = new Map<number, number>();\n const credit = (bars: Map<number, number>, bar: number, strength: number): void => {\n bars.set(bar, Math.max(bars.get(bar) ?? 0, strength));\n };\n for (const cadence of cadences) {\n const end = cadence.arrival + floorMod(groupBars - 1 + phase - cadence.arrival, groupBars);\n if (end <= cadence.last) {\n credit(closing, end, cadence.strength);\n } else if (\n floorMod(cadence.arrival - phase, groupBars) === 0 &&\n // Elision needs a next group to arrive into. Past the last bar the span\n // holds there is none, so a cadence beat beyond the music is not evidence\n // about how the music that was read is grouped.\n cadence.arrival <= lastBar\n ) {\n credit(eliding, cadence.arrival - 1, cadence.strength);\n }\n }\n let ends = 0;\n let closed = 0;\n let weighted = 0;\n for (const slice of slices) {\n // A pickup heads no group, so it ends none either.\n if (slice.index < 0 || floorMod(slice.index - phase, groupBars) !== groupBars - 1) {\n continue;\n }\n ends += 1;\n const final = closing.get(slice.index);\n const elided = eliding.get(slice.index);\n closed += Math.max(\n final === undefined ? 0 : GROUP_FINAL_FIT,\n elided === undefined ? 0 : ELIDED_FIT,\n );\n weighted += Math.max(GROUP_FINAL_FIT * (final ?? 0), ELIDED_FIT * (elided ?? 0));\n }\n return ends === 0\n ? { closed: 0, weighted: 0 }\n : { closed: clamp01(closed / ends), weighted: clamp01(weighted / ends) };\n}\n\n/**\n * The reading a span too short to hold two of any candidate grouping gets.\n *\n * The pickup is dropped before the group is read, exactly as it is on the\n * decided path: an upbeat leads into the first hyperbar rather than heading one,\n * so it neither lengthens the group nor supplies its downbeat. A span in which\n * only the pickup sounds has no hypermetric downbeat at all.\n */\nfunction undecidedReading(slices: readonly BarSlice[]): Hypermeter {\n const fullBars = slices.filter((slice) => slice.index >= 0);\n const groupBars = Math.max(1, fullBars.length);\n const first = fullBars[0];\n return {\n groupBars,\n downbeats: first === undefined ? [] : [first.startBeat],\n confidence: 0,\n rationale: `Span of ${fullBars.length} bar(s) is too short to establish a grouping; read as one group`,\n };\n}\n\n/**\n * Infer how a piece's bars group into hyperbars.\n *\n * Groupings of 2, 3, 4, 6, and 8 bars are tried at every phase, and each\n * reading is scored on two counts: how much more harmonic change lands on the\n * bars it calls group heads than on the bars between them, and how many of the\n * hyperbar ends it predicts are closed by one of the cadences it is given.\n * Where cadences are given they settle the phase, since a hyperbar is what a\n * cadence ends; the harmony then chooses among the phases they agree with. The\n * commonest groupings are believed a little more readily than the rare ones,\n * which settles ties without letting a prior overrule the music.\n *\n * The meter is read at the beat in question rather than at the start, so a\n * piece that changes metre keeps being cut at its own bar lines. Bar 0 is the\n * first full bar however much pickup precedes it, so an upbeat is bar -1 and is\n * never taken for a hypermetric downbeat. Whether there is an upbeat at all is\n * decided the way the rest of the analysis decides it: a note a millibeat early\n * is playing the downbeat, not anticipating it.\n *\n * @param notes The notes to read, in any order. Notes that never sound are\n * dropped.\n * @param meter A single time signature, or the piece's meter map; defaults to\n * 4/4 throughout.\n * @param opts Analysis options; see {@link HypermeterOptions}.\n * @returns The grouping, its downbeats, a confidence, and a rationale.\n * @example\n * ```ts\n * import { hypermeter } from '@libraz/libcantus';\n * const notes = [\n * { pitch: 60, startBeat: 0, durationBeat: 16 },\n * { pitch: 65, startBeat: 16, durationBeat: 16 },\n * ];\n * hypermeter(notes).groupBars; // 4\n * ```\n * @category Arrangement & Analysis\n */\nexport function hypermeter(\n notes: readonly NoteEvent[],\n meter?: MeterLike,\n opts: HypermeterOptions = {},\n): Hypermeter {\n return hypermeterOver(notes, meter, opts, (asked) =>\n cadenceSpans(\n (asked.cadenceBeats ?? []).map((atBeat) => ({ atBeat, strength: 1 })),\n [],\n ),\n );\n}\n\n/**\n * {@link hypermeter} with the cadences given as spans; `opts.cadenceBeats` is\n * not read. `spansOf` is asked only once the span is long enough to group.\n */\nexport function hypermeterOver(\n notes: readonly NoteEvent[],\n meter: MeterLike | undefined,\n opts: HypermeterOptions,\n spansOf: (asked: HypermeterOptions) => readonly CadenceSpan[],\n): Hypermeter {\n const asked = assertOptions(opts, 'opts');\n // Read once, here: every bar question below is asked of the resolved map\n // rather than of whichever form the caller happened to hold. Absent is the\n // library's default bar; a `null` is a caller that meant to name one.\n const meters = resolveMeters({ meters: meter }, 'meter');\n assertNoteEvents(notes, 'hypermeter notes', {\n allowNonPositiveDuration: true,\n budget: asked.budget,\n });\n const sounding = notes.filter((note) => note.durationBeat > 0);\n const firstOnset = firstSoundingBeat(sounding);\n const spanEnd = resolveSpanEnd(\n asked.totalBeats,\n firstOnset,\n latestSoundingEnd(sounding),\n 'hypermeter totalBeats',\n );\n // Whether the music starts before the downbeat is the analysis-wide pickup\n // question, and the slot grids of chord and key inference answer it here: a\n // note a millibeat early is playing beat 0, and only one a whole grid unit\n // early is an upbeat. Reading a hair-early onset as a pickup would hand the\n // phase search a silent bar of its own to group against. An excerpt that\n // begins later keeps the beat it begins on.\n const spanStart = barGridStart(firstOnset, meters);\n if (sounding.length === 0 || spanEnd <= spanStart + BEAT_EPS) {\n // Nothing sounds, so there is no bar to group, as sections report no\n // section for it.\n return undecidedReading([]);\n }\n const slices = sliceBars(sounding, meters, spanStart, spanEnd, asked.budget);\n\n // Counted over the bars a grouping can actually occupy, which is the set\n // `undecidedReading` and the downbeats are read from: a pickup leads into the\n // first hyperbar rather than filling one, so it cannot be what makes a span\n // long enough to hold two groups.\n const fullBars = slices.reduce((count, slice) => (slice.index >= 0 ? count + 1 : count), 0);\n const candidates = GROUP_CANDIDATES.filter((groupBars) => fullBars >= groupBars * MIN_GROUPS);\n if (candidates.length === 0) {\n return undecidedReading(slices);\n }\n assertGenerationBudget(\n candidates.reduce((sum, groupBars) => sum + groupBars * slices.length, 0),\n 'hypermeter readings',\n asked.budget,\n );\n\n const novelty = slices.map((_, index) => harmonicNovelty(slices, index));\n const cadenceBars = cadenceBarsOf(spansOf(asked), meters);\n const hasCadences = cadenceBars.length > 0;\n\n const readings: Reading[] = [];\n for (const groupBars of candidates) {\n const phases = Array.from({ length: groupBars }, (_, phase) => ({\n phase,\n contrast: accentContrast(novelty, slices, groupBars, phase),\n fit: cadenceFitOf(cadenceBars, slices, groupBars, phase),\n }));\n // Where the cadences are known, they decide the phase: a cadence is what a\n // hyperbar ends with, so a reading that leaves them mid-group has misread\n // the phase however the harmony moves. The phase is chosen on the weighted\n // fit; across the groupings it is the share of ends closed that counts.\n // Harmonic contrast then chooses among the phases the cadences agree with,\n // and across the groupings.\n const bestFit = phases.reduce((best, reading) => Math.max(best, reading.fit.weighted), 0);\n for (const { phase, contrast, fit } of phases) {\n if (hasCadences && fit.weighted < bestFit - BEAT_EPS) {\n continue;\n }\n const cadenceFit = fit.closed;\n const evidence = clamp01(\n hasCadences\n ? // Floored at zero rather than summed raw: group heads that restate\n // the harmony their group opened with — the commonest way a phrase\n // begins — change less than the bars between them, and a negative\n // contrast subtracted from the cadence term would count that\n // restatement as evidence against the very phase it confirms.\n CONTRAST_SHARE * clamp01(contrast) + CADENCE_SHARE * cadenceFit\n : contrast,\n );\n readings.push({\n groupBars,\n phase,\n contrast,\n cadenceFit,\n evidence,\n score: evidence * (GROUP_PRIOR.get(groupBars) ?? 0),\n });\n }\n }\n // Ties keep the commoner grouping, then the shorter one, then the earlier\n // phase, so the answer never depends on enumeration order.\n readings.sort(\n (a, b) =>\n b.score - a.score ||\n (GROUP_PRIOR.get(b.groupBars) ?? 0) - (GROUP_PRIOR.get(a.groupBars) ?? 0) ||\n a.groupBars - b.groupBars ||\n a.phase - b.phase,\n );\n const best = readings[0];\n if (best === undefined) {\n return undecidedReading(slices);\n }\n const runnerUp = readings.find((reading) => reading.groupBars !== best.groupBars);\n const margin =\n runnerUp === undefined || best.score <= BEAT_EPS\n ? 1\n : clamp01((best.score - runnerUp.score) / best.score);\n\n // A pickup bar is bar -1, and a phase can put it on a group head — but an\n // upbeat is what leads into the first hyperbar, not what starts one, so it\n // never becomes a hypermetric downbeat however the phase falls.\n const downbeats = slices\n .filter((slice) => slice.index >= 0 && floorMod(slice.index - best.phase, best.groupBars) === 0)\n .map((slice) => slice.startBeat);\n const cadencePart = hasCadences\n ? `${Math.round(best.cadenceFit * 100)}% of group ends closed by a cadence`\n : 'no cadences given';\n\n return {\n groupBars: best.groupBars,\n downbeats,\n // Mostly the evidence, discounted when a rival grouping explains the\n // accents nearly as well. The discount is the smaller term because the\n // runner-up is nearly always a multiple or a divisor of the winner — four\n // bars nest inside eight — and a reading of the same accents at another\n // level is weaker counter-evidence than an unrelated rival would be.\n confidence: clamp01(best.evidence * (MARGIN_FLOOR + (1 - MARGIN_FLOOR) * margin)),\n rationale:\n `${best.groupBars}-bar groups from bar ${best.phase}: harmonic change ` +\n `${Math.round(best.contrast * 100)}% stronger on group heads, ${cadencePart}`,\n };\n}\n","/**\n * Phrases: the unit of music one bar larger than the chord.\n *\n * Every other analysis in this library is local — a chord against the next\n * chord, a note against the chord under it — and that is the ceiling on what it\n * can say. A cadence is a cadence because it falls at the end of a phrase, so\n * two chords read in isolation cannot settle how much weight one carries. This\n * module draws the boundaries, then ranks the cadences that sit on them.\n *\n * No single signal draws a phrase boundary reliably, so three are combined: the\n * cadences the harmony forms, the rests and long notes the melody breathes at,\n * and the points where the music starts saying something it has said before. A\n * boundary is reported with the evidence behind it and a confidence, because a\n * phrase reading is an interpretation and pretending otherwise would be worse\n * than useless to a caller deciding whether to trust it.\n */\n\nimport type { MeterLike, MeterMap } from '../../core/meter/index.js';\nimport {\n barIndexAt,\n barPositionToBeat,\n beatsPerBarAt,\n resolveMeters,\n} from '../../core/meter/index.js';\nimport type { NoteEvent } from '../../core/types.js';\nimport {\n assertArray,\n assertGenerationBudget,\n assertNoteEvents,\n assertPositiveInt,\n assertRange,\n assertRecord,\n} from '../../core/validation/index.js';\nimport { majorKey, resolveKey, scaleOf } from '../../theory/scale/index.js';\nimport { BEAT_EPS } from '../adjacency.js';\nimport type { CadenceResult } from '../functional/index.js';\nimport { barGridStart, latestSoundingEnd } from '../grid.js';\nimport { detectModulations, keyLookup, prevailingKeyOf } from '../keys/index.js';\nimport { melodicSimilarity } from '../melody/index.js';\nimport type { CadenceHit, ChordTimeline } from '../timeline/index.js';\nimport { assertChordTimeline, detectCadences } from '../timeline/index.js';\nimport type { KeyContext } from '../voice/index.js';\nimport type { CadenceSpan, Hypermeter } from './hypermeter.js';\nimport { cadenceSpans, hypermeterOver } from './hypermeter.js';\nimport {\n barSpanOf,\n clamp01,\n combineEvidence,\n firstSoundingBeat,\n resolveSpanEnd,\n sliceBars,\n} from './internal.js';\n\n/**\n * A reason a phrase boundary was placed where it was.\n *\n * @category Arrangement & Analysis\n */\nexport type PhraseSignal =\n /** A cadence closed on the beat the boundary falls at. */\n | 'cadence'\n /** The melody stopped sounding long enough to have taken a breath. */\n | 'rest'\n /** A note was held long enough to read as an arrival. */\n | 'longNote'\n /** The music began repeating material heard immediately before. */\n | 'repetition'\n /** The boundary falls on a hypermetric downbeat. */\n | 'hypermeter'\n /** The music ends here, which closes whatever phrase was open. */\n | 'pieceEnd';\n\n/**\n * One phrase: a span of music closed by a boundary, and the evidence for it.\n *\n * @category Arrangement & Analysis\n */\nexport type Phrase = {\n /** First beat of the phrase; negative when the phrase begins in a pickup. */\n startBeat: number;\n /** End of the phrase, exclusive. */\n endBeat: number;\n /**\n * The cadence closing the phrase, or null when the boundary rests on the\n * melody and the metre alone.\n */\n cadence: CadenceHit | null;\n /** Why the closing boundary was placed here, strongest evidence first. */\n signals: PhraseSignal[];\n /**\n * How well evidenced the phrase is, in [0, 1]: how strong its closing\n * boundary is, and how close its length is to the length expected of a\n * phrase in this piece.\n */\n confidence: number;\n /**\n * How much structural weight the closing cadence carries, in [0, 1]; 0 for a\n * phrase that no cadence closes.\n *\n * This is the number that answers \"which cadence matters most\": it grades the\n * cadence by how conclusive its type is, then by where the phrase it closes\n * sits — a cadence ending the piece or landing on a hypermetric boundary\n * closes more than one in the middle of a hyperbar.\n * {@link structuralCadences} ranks by it.\n */\n structuralWeight: number;\n /** A short reading of the phrase in words. */\n rationale: string;\n};\n\n/**\n * Options for {@link phrasesFromTimeline}.\n *\n * @category Arrangement & Analysis\n */\nexport type PhraseOptions = {\n /**\n * A single time signature held across the whole span, as sugar for a\n * one-element `meters`; defaults to 4/4. Giving both is an input error.\n *\n * @defaultValue `4/4`\n */\n ts?: MeterLike;\n /**\n * The meter as it changes over the span. Bar lines and hypermetric downbeats\n * follow the signature in force at the beat in question, so a piece that\n * changes metre is cut at its own bar lines; the length defaults below are\n * lengths rather than positions, and each is taken once from the meter the\n * span opens in.\n *\n * @defaultValue 4/4 throughout\n */\n meters?: MeterLike;\n /**\n * Key context for cadence detection: one key for the whole span, or the key\n * in force at a given beat. Omit it to have the key searched for over time,\n * so a piece that modulates has each cadence read in the key it arrives in.\n *\n * @defaultValue the key inferred from the notes over time\n */\n key?: KeyContext;\n /**\n * End of the analysed span in beats; defaults to the end of the last note or\n * timeline segment.\n *\n * @defaultValue the end of the input\n */\n totalBeats?: number;\n /**\n * Length a phrase is expected to run, in beats; defaults to one hyperbar.\n *\n * This is a prior, not a rule: a boundary is placed where the evidence puts\n * it, and this decides how much evidence an irregular length has to carry\n * before it is preferred to a regular one. Set it to two hyperbars for music\n * whose phrases are periods.\n *\n * @defaultValue one hyperbar of the opening meter\n */\n expectedPhraseBeats?: number;\n /**\n * Shortest phrase that may be reported, in beats; defaults to one bar of the\n * opening meter.\n *\n * @defaultValue one bar of the opening meter\n */\n minPhraseBeats?: number;\n /**\n * Silence at least this long divides phrases; defaults to half a bar of the\n * opening meter.\n *\n * @defaultValue half a bar of the opening meter\n */\n restBeats?: number;\n /**\n * A note held at least this long reads as an arrival and closes a phrase;\n * defaults to one bar of the opening meter.\n *\n * @defaultValue one bar of the opening meter\n */\n longNoteBeats?: number;\n /**\n * A hypermeter already computed for this piece, to be used instead of\n * inferring one. Pass it when the caller has already asked for the grouping,\n * or to impose a grouping the search would not have chosen.\n *\n * @defaultValue inferred from the notes and the cadences found\n */\n hypermeter?: Hypermeter;\n /**\n * Upper bound on the work this call may do.\n *\n * @defaultValue {@link DEFAULT_GENERATION_BUDGET}\n */\n budget?: number;\n};\n\n/**\n * A cadence ranked by how much of the structure it closes.\n *\n * @category Arrangement & Analysis\n */\nexport type StructuralCadence = {\n /** Index of the phrase this cadence closes, in the array it came from. */\n phraseIndex: number;\n /** The beat the cadence arrives on. */\n atBeat: number;\n /** The beat the phrase it closes ends at. */\n endBeat: number;\n /** The cadence itself. */\n cadence: CadenceResult;\n /** Structural weight in [0, 1]; {@link Phrase.structuralWeight}. */\n weight: number;\n /** Why it weighs what it does. */\n rationale: string;\n};\n\n/**\n * How strongly each cadence type marks a boundary.\n *\n * This grades the cadence as a punctuation mark, not as a close: a half cadence\n * ends an antecedent as audibly as an authentic one ends a consequent, which is\n * why it scores near it here and far below it in {@link CADENCE_CLOSURE}. A\n * deceptive cadence scores lowest of all because its whole effect is to\n * withhold the ending it prepared, so the phrase usually runs on past it.\n */\nexport const CADENCE_BOUNDARY_STRENGTH: Record<NonNullable<CadenceResult['type']>, number> = {\n authentic: 0.9,\n modal: 0.75,\n plagal: 0.7,\n phrygian: 0.65,\n half: 0.6,\n deceptive: 0.45,\n};\n\n/**\n * How much of the structure each cadence type closes.\n *\n * The perfect authentic cadence is the only one that ends anything outright;\n * everything else leaves the music open to some degree, and the half cadence\n * most of all — it ends a phrase by pointing at the next one.\n */\nexport const CADENCE_CLOSURE: Record<NonNullable<CadenceResult['type']>, number> = {\n authentic: 0.85,\n modal: 0.6,\n plagal: 0.55,\n phrygian: 0.4,\n half: 0.35,\n deceptive: 0.3,\n};\n\n/** Weight of a full-bar rest as a boundary signal. */\nexport const REST_STRENGTH = 0.6;\n\n/** Weight of a held note as a boundary signal. */\nexport const LONG_NOTE_STRENGTH = 0.4;\n\n/** Weight of immediate repetition as a boundary signal. */\nexport const REPETITION_STRENGTH = 0.5;\n\n/** How alike two adjacent spans must be before the second is heard as a restart. */\nexport const REPETITION_THRESHOLD = 0.75;\n\n/** Weight of a hypermetric downbeat as a boundary signal. */\nexport const HYPERMETER_STRENGTH = 0.25;\n\n/** Weight of the end of the music as a boundary signal. */\nexport const PIECE_END_STRENGTH = 0.5;\n\n/**\n * What a phrase boundary has to be worth before one is placed.\n *\n * The search adds up boundary evidence and length fit across a whole reading,\n * so without a cost per cut it would cut wherever cutting was not actively\n * wrong. Set just above the best length fit alone, so that a boundary needs\n * some evidence of its own: metre may confirm a phrase ending, but it may not\n * declare one. A boundary worth exactly this much is break-even, and a\n * break-even boundary is not taken — see {@link choosePhrasePath}.\n *\n * Not part of the public surface; exported so the search's break-even point can\n * be stated exactly rather than approximated.\n */\nexport const PHRASE_CUT_COST = 1.3;\n\n/** Share of a phrase's confidence carried by its closing boundary. */\nexport const CLOSING_SHARE = 0.65;\n\n/** Bonus for a cadence that closes the last phrase of the piece. */\nexport const FINAL_PHRASE_BONUS = 0.15;\n\n/** Bonus for a cadence whose phrase ends on a hypermetric boundary. */\nexport const HYPERMETRIC_CLOSE_BONUS = 0.1;\n\n/** How much of an authentic cadence's closure survives an imperfect grading. */\nexport const IMPERFECT_CLOSURE_FACTOR = 0.8;\n\n/** How much of a cadence's weight survives resolving onto an inverted tonic. */\nexport const EVADED_FACTOR = 0.6;\n\n/** Order the boundary signals are reported in when they are equally strong. */\nexport const SIGNAL_ORDER: readonly PhraseSignal[] = [\n 'cadence',\n 'rest',\n 'repetition',\n 'longNote',\n 'pieceEnd',\n 'hypermeter',\n];\n\n/** A candidate boundary and everything that argues for it. */\ntype Boundary = {\n beat: number;\n strengths: Map<PhraseSignal, number>;\n cadence: CadenceHit | null;\n};\n\n/** Record a signal at a beat, keeping the strongest claim made for it. */\nfunction addSignal(\n boundaries: Map<number, Boundary>,\n beat: number,\n signal: PhraseSignal,\n strength: number,\n cadence: CadenceHit | null = null,\n): void {\n // Beats arrive from bar arithmetic and from note ends, which land an epsilon\n // apart on the same boundary; keying on a rounded beat merges them.\n const key = Math.round(beat / BEAT_EPS);\n const existing = boundaries.get(key);\n const boundary = existing ?? { beat, strengths: new Map(), cadence: null };\n boundary.strengths.set(signal, Math.max(boundary.strengths.get(signal) ?? 0, strength));\n if (cadence !== null && boundary.cadence === null) {\n boundary.cadence = cadence;\n }\n boundaries.set(key, boundary);\n}\n\n/** What a boundary is worth, with and without the cadence it carries. */\ntype BoundaryReading = {\n /** Total strength of every signal. */\n strength: number;\n /** Total strength of the signals other than the cadence. */\n uncredited: number;\n /** The signals, strongest first. */\n signals: PhraseSignal[];\n};\n\n/** Total strength of a boundary, and its signals strongest first. */\nfunction readBoundary(boundary: Boundary): BoundaryReading {\n const signals = [...boundary.strengths.entries()].sort(\n (a, b) => b[1] - a[1] || SIGNAL_ORDER.indexOf(a[0]) - SIGNAL_ORDER.indexOf(b[0]),\n );\n return {\n strength: combineEvidence(signals.map(([, strength]) => strength)),\n uncredited: combineEvidence(\n signals.filter(([signal]) => signal !== 'cadence').map(([, strength]) => strength),\n ),\n signals: signals.map(([signal]) => signal),\n };\n}\n\n/**\n * Whether a phrase from `startBeat` to `endBeat` is closed by a cadence\n * arriving at `atBeat`: only one that arrived inside it is.\n */\nfunction closesPhrase(atBeat: number, startBeat: number, endBeat: number): boolean {\n return atBeat >= startBeat - BEAT_EPS && atBeat < endBeat - BEAT_EPS;\n}\n\n/**\n * How well a length matches what a phrase is expected to run.\n *\n * 1 at the expected length, 0 at twice or none of it, and negative beyond —\n * a phrase three times the expected length is not merely unsurprising, it is\n * evidence that a boundary was missed.\n */\nfunction lengthFit(length: number, expected: number): number {\n if (!(expected > 0)) {\n return 0;\n }\n return Math.max(-1, 1 - Math.abs(length - expected) / expected);\n}\n\n/**\n * Choose which of the candidate stops become phrase boundaries.\n *\n * A reading is worth the evidence at every boundary it takes, plus how well\n * each phrase's length matches what a phrase is expected to run, less a cost\n * per cut. The best reading is found by dynamic programming over the stops,\n * since a boundary's worth depends on where the previous one fell.\n *\n * Predecessors are considered in time order, and only a strictly better reading\n * displaces the one already found. Two readings that score the same therefore\n * settle on the earlier predecessor — the longer phrase, with one boundary\n * fewer — because a search may not invent a boundary that the evidence does not\n * prefer to having none. The rule is a property of the comparison, not of the\n * order the loop happens to run in.\n *\n * Not part of the public surface: it takes the stops and strengths some other\n * pass already derived, so exposing it would invite callers to pair the two by\n * hand.\n *\n * A stop whose cadence arrived before the phrase that would end there began is\n * worth only its other evidence: the cadence closes the phrase it arrived in.\n *\n * @param beats Beat of each stop, ascending; index 0 is the span's start and\n * the last is its end.\n * @param strengths Boundary evidence at each stop, in the same order.\n * @param minPhraseBeats Shortest phrase the search may report.\n * @param expected Length a phrase is expected to run, in beats.\n * @param held Per stop, the beat its cadence arrived on and the evidence left\n * without that cadence; omitted, every stop is worth its full strength.\n * @returns The chosen stop indices after the start, in time order.\n */\nexport function choosePhrasePath(\n beats: readonly number[],\n strengths: readonly number[],\n minPhraseBeats: number,\n expected: number,\n held?: { arrivals: readonly number[]; uncredited: readonly number[] },\n): number[] {\n const value = new Array<number>(beats.length).fill(Number.NEGATIVE_INFINITY);\n const cameFrom = new Array<number>(beats.length).fill(-1);\n value[0] = 0;\n for (let j = 1; j < beats.length; j += 1) {\n const endBeat = beats[j] ?? 0;\n for (let i = 0; i < j; i += 1) {\n if (!Number.isFinite(value[i] ?? Number.NEGATIVE_INFINITY)) {\n continue;\n }\n const length = endBeat - (beats[i] ?? 0);\n if (length < minPhraseBeats - BEAT_EPS) {\n continue;\n }\n const credited =\n held === undefined || closesPhrase(held.arrivals[j] ?? endBeat, beats[i] ?? 0, endBeat);\n const strength = credited ? (strengths[j] ?? 0) : (held.uncredited[j] ?? 0);\n const gain = (value[i] ?? 0) + strength + lengthFit(length, expected) - PHRASE_CUT_COST;\n if (gain > (value[j] ?? Number.NEGATIVE_INFINITY)) {\n value[j] = gain;\n cameFrom[j] = i;\n }\n }\n }\n\n const last = beats.length - 1;\n const path: number[] = [];\n if (last > 0 && Number.isFinite(value[last] ?? Number.NEGATIVE_INFINITY)) {\n for (let i = last; i > 0; i = cameFrom[i] ?? 0) {\n path.push(i);\n }\n path.reverse();\n } else if (last > 0) {\n // Nothing reached the end — the span is shorter than one phrase — so the\n // whole of it is the phrase.\n path.push(last);\n }\n return path;\n}\n\n/**\n * The cadences of a timeline that mark a boundary, each once, with the span its\n * arrival chord sounds through and the strength its type marks a boundary with.\n *\n * Phrase boundaries and the hyperbar ends the phrases are fitted against are\n * both read from these spans, so a held arrival is credited alike by both.\n *\n * Not part of the public surface.\n *\n * @param timeline The chord timeline the cadences are read from.\n * @param key The key context to read them against.\n * @returns The cadences and their spans, index for index, in time order.\n */\nexport function boundaryCadences(\n timeline: ChordTimeline,\n key: KeyContext,\n): { hits: CadenceHit[]; spans: CadenceSpan[] } {\n const hits = detectCadences(timeline, key).filter((hit) => hit.cadence.type !== null);\n const spans = cadenceSpans(\n hits.map((hit) => ({\n atBeat: hit.atBeat,\n strength: CADENCE_BOUNDARY_STRENGTH[hit.cadence.type as NonNullable<CadenceResult['type']>],\n })),\n timeline.segments,\n );\n return { hits, spans };\n}\n\n/**\n * The key context cadences are read against: `key` when one is given,\n * otherwise the keys the timeline's chords modulate through.\n *\n * Not part of the public surface.\n *\n * @param key The key the caller gave, if any.\n * @param timeline The chord timeline the cadences are read from.\n * @param meters The meter map the span is read in.\n * @param totalBeats End of the analysed span; omitted, the end of the last chord.\n * @param budget Upper bound on the work this may do.\n * @returns The key context.\n */\nexport function keyContextFor(\n key: KeyContext | undefined,\n timeline: ChordTimeline,\n meters: MeterLike,\n totalBeats: number | undefined,\n budget: number | undefined,\n): KeyContext {\n if (key !== undefined) {\n return key;\n }\n // Read from the chords the cadences are formed of, as the timeline's own\n // modulations are: a dominant passed through on the way to a cadence is not a\n // key of its own, and reading it as one would turn its V-I into no cadence.\n const regions = detectModulations(timeline.segments, { meters, totalBeats, budget });\n // The pitch classes alone: a phrase boundary is argued from cadences and\n // scale membership, neither of which reads how the key is written.\n const keyAt = keyLookup(regions, prevailingKeyOf(regions) ?? resolveKey(majorKey(0)));\n return (beat) => scaleOf(keyAt(beat));\n}\n\n/**\n * Boundaries argued for by a cadence's arrival chord held across bar lines.\n *\n * A chord held past a bar line closes its phrase at that line as well as where\n * it stops sounding: the tonic a period's consequent opens on is the same tonic\n * the antecedent cadenced to, and reading the arrival only at the end of the\n * held chord would leave the seam between the two with no cadence on it. Every\n * bar line the chord sounds across is such a seam, however many bars separate\n * it from the arrival.\n */\nfunction heldCadenceBoundaries(\n boundaries: Map<number, Boundary>,\n hit: CadenceHit,\n span: CadenceSpan,\n meters: MeterMap,\n spanEnd: number,\n): void {\n // Bar lines past the end of the span bound no phrase.\n const firstLine = barIndexAt(span.atBeat, meters) + 1;\n const lastLine = barIndexAt(Math.min(span.endBeat, spanEnd), meters);\n // The lines lie inside the span, whose bar count the form bars check has bounded.\n for (let bar = firstLine; bar <= lastLine; bar += 1) {\n const line = barPositionToBeat({ bar, beat: 0 }, meters);\n if (line < span.endBeat - BEAT_EPS) {\n addSignal(boundaries, line, 'cadence', span.strength, hit);\n }\n }\n}\n\n/** Boundaries argued for by silence in the melody. */\nfunction restBoundaries(\n boundaries: Map<number, Boundary>,\n notes: readonly NoteEvent[],\n meters: MeterMap,\n restBeats: number,\n): void {\n const ordered = [...notes].sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);\n let covered = Number.NEGATIVE_INFINITY;\n for (const note of ordered) {\n const gap = note.startBeat - covered;\n if (Number.isFinite(covered) && gap >= restBeats - BEAT_EPS) {\n const bar = beatsPerBarAt(note.startBeat, meters);\n addSignal(\n boundaries,\n note.startBeat,\n 'rest',\n REST_STRENGTH * clamp01(bar > 0 ? gap / bar : 1),\n );\n }\n covered = Math.max(covered, note.startBeat + note.durationBeat);\n }\n}\n\n/** Boundaries argued for by immediate repetition of the preceding span. */\nfunction repetitionBoundaries(\n boundaries: Map<number, Boundary>,\n notes: readonly NoteEvent[],\n meters: MeterMap,\n spanStart: number,\n spanEnd: number,\n windowBeats: number,\n budget: number | undefined,\n): void {\n if (!(windowBeats > 0)) {\n return;\n }\n const slices = sliceBars(notes, meters, spanStart, spanEnd, budget);\n assertGenerationBudget(slices.length * notes.length, 'form repetition comparisons', budget);\n const onsetsIn = (from: number, to: number): NoteEvent[] =>\n notes.filter((note) => note.startBeat >= from - BEAT_EPS && note.startBeat < to - BEAT_EPS);\n // Comparing the two windows runs an edit distance over their notes, so the\n // work is the product of what each side holds rather than one unit per slice.\n // It is charged as it is incurred, so a passage dense enough to matter is\n // refused before its distance is run rather than after.\n let comparisons = 0;\n for (const slice of slices) {\n const beat = slice.startBeat;\n if (beat - windowBeats < spanStart - BEAT_EPS || beat + windowBeats > spanEnd + BEAT_EPS) {\n continue;\n }\n const before = onsetsIn(beat - windowBeats, beat);\n const after = onsetsIn(beat, beat + windowBeats);\n // One note is a pitch, not a figure; two identical single notes would score\n // a perfect likeness and mark a boundary at every held tone.\n if (before.length < 2 || after.length < 2) {\n continue;\n }\n comparisons += before.length * after.length;\n assertGenerationBudget(comparisons, 'form repetition melody comparisons', budget);\n const similarity = melodicSimilarity(before, after);\n if (similarity >= REPETITION_THRESHOLD) {\n addSignal(boundaries, beat, 'repetition', REPETITION_STRENGTH * similarity);\n }\n }\n}\n\n/** Grade a cadence by how much of the structure it closes. */\nfunction structuralWeightOf(\n cadence: CadenceHit | null,\n confidence: number,\n aligned: boolean,\n isLast: boolean,\n): number {\n if (cadence === null || cadence.cadence.type === null) {\n return 0;\n }\n const result = cadence.cadence;\n let base = CADENCE_CLOSURE[result.type as NonNullable<CadenceResult['type']>];\n if (result.type === 'authentic') {\n if (result.strength === 'perfect') {\n base = 1;\n } else if (result.strength === 'imperfect') {\n base *= IMPERFECT_CLOSURE_FACTOR;\n }\n }\n if (result.evaded) {\n base *= EVADED_FACTOR;\n }\n let weight = base * confidence;\n if (isLast) {\n weight += FINAL_PHRASE_BONUS;\n }\n if (aligned) {\n weight += HYPERMETRIC_CLOSE_BONUS;\n }\n return clamp01(weight);\n}\n\n/** Name a cadence the way a rationale reads it. */\nfunction describeCadence(cadence: CadenceHit | null): string {\n if (cadence === null || cadence.cadence.type === null) {\n return 'no cadence';\n }\n const { type, strength } = cadence.cadence;\n const graded = strength === null ? type : `${strength} ${type}`;\n return `a ${graded} cadence at beat ${cadence.atBeat}`;\n}\n\n/**\n * Split a piece into phrases.\n *\n * Three kinds of evidence are gathered independently and then reconciled. The\n * harmony contributes its cadences, each closing at the end of the chord it\n * arrives on, and at every bar line that chord is held across — a tonic still\n * sounding into the next phrase closed this one at the bar line. The\n * cadence reported for a phrase is always one that arrived inside it. The\n * melody contributes the rests it breathes at and the notes it\n * holds long enough to read as arrivals. Repetition contributes the beats where\n * the music restarts material it has just played. The metre contributes its\n * hypermetric downbeats, which confirm a boundary but never declare one on\n * their own.\n *\n * The boundaries are then chosen together rather than one at a time: the\n * reading that best trades each boundary's evidence against how regular the\n * phrase lengths come out is selected by dynamic programming, at a fixed cost\n * per cut. That is what stops a phrase being declared at every plausible beat,\n * and what lets a well-evidenced irregular phrase survive next to regular ones.\n *\n * The result is contiguous: phrases abut, and together they cover the span from\n * the first note to the end of the music. A cadence found in the middle of a\n * phrase is not lost — it is still reachable through {@link detectCadences} —\n * but only the cadences the reading puts at phrase ends are ranked by\n * {@link structuralCadences}.\n *\n * @param timeline The chord timeline the cadences are read from.\n * @param notes The melody, for its rests, held notes and repetitions. Notes\n * that never sound are dropped.\n * @param opts Analysis options; see {@link PhraseOptions}.\n * @returns The phrases, in time order; one phrase covering the whole span when\n * the span is too short to divide, and none at all when nothing sounds —\n * silence is no material to hear a phrase in.\n * @example\n * ```ts\n * import { chordTimelineFromNotes, phrasesFromTimeline } from '@libraz/libcantus';\n * // Four bars of C - F - G - C. Triads rather than single notes: one line at a\n * // time names no harmony, so nothing would cadence.\n * const triad = (pitches: number[], startBeat: number) =>\n * pitches.map((pitch) => ({ pitch, startBeat, durationBeat: 4 }));\n * const notes = [\n * ...triad([60, 64, 67], 0),\n * ...triad([65, 69, 72], 4),\n * ...triad([67, 71, 74], 8),\n * ...triad([60, 64, 67], 12),\n * ];\n * const { timeline } = chordTimelineFromNotes(notes);\n * const phrases = phrasesFromTimeline(timeline, notes);\n * phrases[phrases.length - 1]?.cadence?.cadence.type; // 'authentic'\n * phrases[phrases.length - 1]?.cadence?.atBeat; // 12, inside the phrase it closes\n * ```\n * @category Arrangement & Analysis\n */\nexport function phrasesFromTimeline(\n timeline: ChordTimeline,\n notes: readonly NoteEvent[],\n opts: PhraseOptions = {},\n): Phrase[] {\n assertChordTimeline(timeline);\n const meters = resolveMeters(opts, 'phrase meters');\n assertNoteEvents(notes, 'phrase notes', {\n allowNonPositiveDuration: true,\n budget: opts.budget,\n });\n if (opts.hypermeter !== undefined) {\n // The one structured option, held to the contract its own type states: a\n // confidence outside [0, 1] scales the metrical signal past the cost of a\n // cut, which would let the metre declare a boundary rather than confirm\n // one, and a fractional group is not a number of bars.\n assertRange(opts.hypermeter.confidence, 0, 1, 'phrase hypermeter confidence');\n assertPositiveInt(opts.hypermeter.groupBars, 'phrase hypermeter groupBars');\n }\n const sounding = notes.filter((note) => note.durationBeat > 0);\n const segments = timeline.segments;\n const firstSegment = segments[0];\n const lastSegment = segments[segments.length - 1];\n // Whichever starts first, the notes or the chords; with neither, beat 0.\n const earliest = Math.min(\n firstSoundingBeat(sounding, Number.POSITIVE_INFINITY),\n firstSegment?.startBeat ?? Number.POSITIVE_INFINITY,\n );\n const firstOnset = Number.isFinite(earliest) ? earliest : 0;\n // The pickup rule the whole analysis shares, so the first beat this reports\n // is the one sections and hypermeter report for the same notes.\n const spanStart = barGridStart(firstOnset, meters);\n const latestEnd = Math.max(\n lastSegment?.endBeat ?? Number.NEGATIVE_INFINITY,\n latestSoundingEnd(sounding, Number.NEGATIVE_INFINITY),\n );\n const notesEnd = Number.isFinite(latestEnd) ? latestEnd : 0;\n const spanEnd = resolveSpanEnd(opts.totalBeats, spanStart, notesEnd, 'phrase totalBeats');\n if (spanEnd <= spanStart + BEAT_EPS) {\n // Nothing sounds, so there is no material to hear a phrase in — the answer\n // `sectionsFromNotes` gives an empty span. A phrase of no length, closed by\n // nothing, would be a marker rather than a phrase.\n return [];\n }\n\n const barBeats = beatsPerBarAt(Math.max(0, spanStart), meters);\n const restBeats = opts.restBeats ?? barBeats / 2;\n assertRange(restBeats, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, 'phrase restBeats');\n const longNoteBeats = opts.longNoteBeats ?? barBeats;\n assertRange(longNoteBeats, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, 'phrase longNoteBeats');\n const minPhraseBeats = opts.minPhraseBeats ?? barBeats;\n assertRange(minPhraseBeats, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, 'phrase minPhraseBeats');\n\n const key = keyContextFor(opts.key, timeline, meters, spanEnd, opts.budget);\n const { hits: cadences, spans } = boundaryCadences(timeline, key);\n const grouping =\n opts.hypermeter ??\n hypermeterOver(sounding, meters, { totalBeats: spanEnd, budget: opts.budget }, () => spans);\n // The hyperbar the span opens in, not the one the piece opens in. Bar 0 is\n // always the bar beginning at beat 0 of the meter map, so measuring the group\n // from there hands an excerpt starting after a change of meter the bar length\n // of a meter it never sounds in — and that wrong length is the prior every\n // phrase in it is fitted against. The bar the span itself opens in is the\n // anchor the line above already uses.\n const firstBar = barIndexAt(Math.max(0, spanStart), meters);\n const hyperBeats = Math.max(\n barBeats,\n barPositionToBeat({ bar: firstBar + grouping.groupBars, beat: 0 }, meters) -\n barPositionToBeat({ bar: firstBar, beat: 0 }, meters),\n );\n const expected = opts.expectedPhraseBeats ?? hyperBeats;\n assertRange(expected, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, 'phrase expectedPhraseBeats');\n\n const boundaries = new Map<number, Boundary>();\n cadences.forEach((hit, index) => {\n const span = spans[index] as CadenceSpan;\n addSignal(boundaries, span.endBeat, 'cadence', span.strength, hit);\n heldCadenceBoundaries(boundaries, hit, span, meters, spanEnd);\n });\n restBoundaries(boundaries, sounding, meters, restBeats);\n for (const note of sounding) {\n if (note.durationBeat >= longNoteBeats - BEAT_EPS) {\n addSignal(boundaries, note.startBeat + note.durationBeat, 'longNote', LONG_NOTE_STRENGTH);\n }\n }\n repetitionBoundaries(boundaries, sounding, meters, spanStart, spanEnd, hyperBeats, opts.budget);\n for (const downbeat of grouping.downbeats) {\n addSignal(boundaries, downbeat, 'hypermeter', HYPERMETER_STRENGTH * grouping.confidence);\n }\n addSignal(boundaries, spanEnd, 'pieceEnd', PIECE_END_STRENGTH);\n\n // The span's own ends are boundaries whether or not anything argues for them,\n // so the phrases cover the music rather than only its well-marked middle.\n const candidates = [...boundaries.values()]\n .filter(\n (boundary) => boundary.beat > spanStart + BEAT_EPS && boundary.beat < spanEnd - BEAT_EPS,\n )\n // A stop nothing argues for is not a boundary that a length can buy. A\n // hypermeter position read with no confidence registers at strength zero,\n // and the path search weighs a cut by the two lengths it makes as well as\n // by the evidence: over a pedal point or a drone, where no cadence and no\n // harmonic novelty argue anywhere, the length term alone put boundaries in\n // the reading that a caller cannot tell from evidenced ones.\n .filter((boundary) => readBoundary(boundary).strength > 0)\n .sort((a, b) => a.beat - b.beat);\n const endBoundary = boundaries.get(Math.round(spanEnd / BEAT_EPS)) ?? {\n beat: spanEnd,\n strengths: new Map<PhraseSignal, number>(),\n cadence: null,\n };\n const stops: Boundary[] = [\n { beat: spanStart, strengths: new Map(), cadence: null },\n ...candidates,\n endBoundary,\n ];\n assertGenerationBudget(stops.length * stops.length, 'form phrase boundary pairs', opts.budget);\n\n const readings = stops.map(readBoundary);\n const path = choosePhrasePath(\n stops.map((stop) => stop.beat),\n readings.map((reading) => reading.strength),\n minPhraseBeats,\n expected,\n {\n arrivals: stops.map((stop) => stop.cadence?.atBeat ?? stop.beat),\n uncredited: readings.map((reading) => reading.uncredited),\n },\n );\n\n const downbeatKeys = new Set(\n grouping.downbeats.map((downbeat) => Math.round(downbeat / BEAT_EPS)),\n );\n const phrases: Phrase[] = [];\n let from = 0;\n for (let n = 0; n < path.length; n += 1) {\n const to = path[n] ?? stops.length - 1;\n const startBeat = stops[from]?.beat ?? spanStart;\n const stop = stops[to];\n const endBeat = stop?.beat ?? spanEnd;\n const reading = readings[to] ?? { strength: 0, uncredited: 0, signals: [] as PhraseSignal[] };\n const length = endBeat - startBeat;\n const isLast = n === path.length - 1;\n // A cadence that arrived before this phrase began cannot be the one closing\n // it: its chord was still sounding at the boundary, which is a held arrival\n // and not a second one. The cadence belongs to the phrase it arrived in, so\n // a reported `cadence.atBeat` always lies inside `[startBeat, endBeat)`.\n const closing = stop?.cadence ?? null;\n const cadence =\n closing !== null && closesPhrase(closing.atBeat, startBeat, endBeat) ? closing : null;\n // Read from the signals the phrase reports, so a dropped cadence adds nothing.\n const closingStrength = cadence === null ? reading.uncredited : reading.strength;\n const confidence = clamp01(\n CLOSING_SHARE * closingStrength +\n (1 - CLOSING_SHARE) * Math.max(0, lengthFit(length, expected)),\n );\n const structuralWeight = structuralWeightOf(\n cadence,\n confidence,\n downbeatKeys.has(Math.round(endBeat / BEAT_EPS)),\n isLast,\n );\n // The signals say what the boundary was read from, so a cadence dropped as\n // belonging to an earlier phrase is dropped from them too: reporting a\n // phrase closing on no cadence whose boundary came `from cadence` states\n // both halves of one fact and contradicts itself.\n const signals =\n cadence === null ? reading.signals.filter((signal) => signal !== 'cadence') : reading.signals;\n const bars = Math.round(barSpanOf(startBeat, endBeat, meters) * 100) / 100;\n phrases.push({\n startBeat,\n endBeat,\n cadence,\n signals,\n confidence,\n structuralWeight,\n rationale:\n `Phrase of ${bars} bar(s) closing on ${describeCadence(cadence)}; ` +\n `boundary from ${signals.length === 0 ? 'the end of the span' : signals.join(', ')}`,\n });\n from = to;\n }\n return phrases;\n}\n\n/**\n * Rank the cadences that close phrases by how much structure they close.\n *\n * This is the question the phrase layer exists to answer. Two chords alone\n * cannot say whether a V-to-I is the end of the piece or a passing confirmation\n * halfway through a hyperbar; a phrase reading can, because it knows what the\n * cadence closes and where that sits. Phrases no cadence closes are left out.\n *\n * @param phrases The phrases, as {@link phrasesFromTimeline} returns them.\n * @returns The cadences, heaviest first; ties keep the earlier one.\n * @example\n * ```ts\n * import { chordTimelineFromNotes, phrasesFromTimeline, structuralCadences } from '@libraz/libcantus';\n * const triad = (pitches: number[], startBeat: number) =>\n * pitches.map((pitch) => ({ pitch, startBeat, durationBeat: 4 }));\n * const notes = [\n * ...triad([60, 64, 67], 0),\n * ...triad([65, 69, 72], 4),\n * ...triad([67, 71, 74], 8),\n * ...triad([60, 64, 67], 12),\n * ];\n * const { timeline } = chordTimelineFromNotes(notes);\n * const ranked = structuralCadences(phrasesFromTimeline(timeline, notes));\n * ranked[0]?.cadence.type; // 'authentic' — the cadence carrying the most weight\n * ranked[0]?.atBeat; // 12\n * ```\n * @category Arrangement & Analysis\n */\nexport function structuralCadences(phrases: readonly Phrase[]): StructuralCadence[] {\n assertArray(phrases, 'phrases');\n const ranked: StructuralCadence[] = [];\n for (let index = 0; index < phrases.length; index += 1) {\n const phrase = assertRecord<Phrase>(phrases[index], `phrases[${index}]`);\n if (\n phrase.cadence === null ||\n phrase.cadence === undefined ||\n phrase.cadence.cadence.type === null\n ) {\n continue;\n }\n const hit = phrase.cadence;\n ranked.push({\n phraseIndex: index,\n atBeat: hit.atBeat,\n endBeat: phrase.endBeat,\n cadence: hit.cadence,\n weight: phrase.structuralWeight,\n rationale: `Closes ${describeCadence(hit)} on a phrase of confidence ${\n Math.round(phrase.confidence * 100) / 100\n }`,\n });\n }\n return ranked.sort((a, b) => b.weight - a.weight || a.atBeat - b.atBeat);\n}\n","/**\n * Sections: the largest unit this library reads, recovered from repetition.\n *\n * A section is found, not named. What the analysis can see is that a span of\n * music is a restatement of an earlier one, so the spans are labelled by order\n * of first appearance — A, B, C — and nothing more is claimed. Calling a\n * repeated span a chorus would be a guess dressed as a finding: repetition\n * alone cannot tell a chorus from a second verse, and the label a listener\n * would give depends on lyrics, arrangement and loudness that the notes do not\n * carry.\n */\n\nimport type { MeterLike } from '../../core/meter/index.js';\nimport { barIndexAt, barPositionToBeat, resolveMeters } from '../../core/meter/index.js';\nimport type { NoteEvent } from '../../core/types.js';\nimport {\n assertGenerationBudget,\n assertNoteEvents,\n assertPositiveInt,\n assertRange,\n} from '../../core/validation/index.js';\nimport { BEAT_EPS } from '../adjacency.js';\nimport { barGridStart, latestSoundingEnd } from '../grid.js';\nimport { windowWeights } from '../histogram.js';\nimport { melodicSimilarity } from '../melody/index.js';\nimport { hypermeter } from './hypermeter.js';\nimport {\n clamp01,\n firstSoundingBeat,\n lastBarOf,\n resolveSpanEnd,\n weightSimilarity,\n} from './internal.js';\n\n/**\n * A span of music labelled by which earlier span it restates.\n *\n * @category Arrangement & Analysis\n */\nexport type FormSection = {\n /**\n * The section's letter, assigned in order of first appearance: the opening\n * material is A, the first material unlike it is B, and a later restatement\n * of A is A again. Past Z the letters double (AA, AB), the way spreadsheet\n * columns do. No functional name — verse, chorus, bridge — is ever inferred.\n */\n label: string;\n /** First beat of the section; negative when it opens with a pickup. */\n startBeat: number;\n /** End of the section, exclusive. */\n endBeat: number;\n /** The section's length in bars. */\n bars: number;\n /**\n * Index, in the returned array, of the first section carrying this label; the\n * section's own index when it is that first appearance.\n */\n firstOccurrence: number;\n /**\n * How alike this section and its first occurrence are, in [0, 1]; 1 on the\n * first occurrence itself. The mean over the units the section was merged\n * from, each scored against the unit it matched.\n */\n similarity: number;\n /** How the section came to carry its label. */\n rationale: string;\n};\n\n/**\n * Options for {@link sectionsFromNotes}.\n *\n * @category Arrangement & Analysis\n */\nexport type FormSectionOptions = {\n /**\n * A single time signature held across the whole span, as sugar for a\n * one-element `meters`; defaults to 4/4. Giving both is an input error.\n *\n * @defaultValue `4/4`\n */\n ts?: MeterLike;\n /**\n * The meter as it changes over the span, so a piece that changes metre is cut\n * at its own bar lines.\n *\n * @defaultValue 4/4 throughout\n */\n meters?: MeterLike;\n /**\n * Length of the unit spans are compared in, in bars; defaults to one hyperbar.\n *\n * Sections are found by comparing equal units and merging the neighbours that\n * match, so this is the resolution of the answer rather than the length of a\n * section: a section is any whole number of units long.\n *\n * @defaultValue the inferred hyperbar, in bars\n */\n unitBars?: number;\n /**\n * How alike two units must be before the later one is called a restatement of\n * the earlier, in [0, 1].\n *\n * @defaultValue `0.8`\n */\n threshold?: number;\n /**\n * End of the analysed 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 * Upper bound on the work this call may do.\n *\n * @defaultValue {@link DEFAULT_GENERATION_BUDGET}\n */\n budget?: number;\n};\n\n/** Share of a unit's likeness carried by its melodic line. */\nexport const MELODY_SHARE = 0.6;\n\n/** Share of a unit's likeness carried by its harmonic content. */\nexport const HARMONY_SHARE = 0.4;\n\n/** How alike two units must be before one is called a restatement of the other. */\nexport const DEFAULT_THRESHOLD = 0.8;\n\n/** One unit of comparison: a fixed run of bars and what sounds in it. */\ntype Unit = {\n startBeat: number;\n endBeat: number;\n /** The notes whose onsets fall inside the unit, in time order. */\n notes: NoteEvent[];\n /** Pitch-class weights over the unit. */\n weights: number[];\n};\n\n/**\n * The nth label in the sequence A, B, ... Z, AA, AB, ...\n *\n * Doubling the letters past Z rather than numbering them (A1, A2) keeps the\n * label free of digits, so nothing reads as \"the second A\" when it is in fact\n * the twenty-seventh distinct span.\n */\nfunction labelAt(index: number): string {\n let n = index;\n let label = '';\n do {\n label = String.fromCharCode(65 + (n % 26)) + label;\n n = Math.floor(n / 26) - 1;\n } while (n >= 0);\n return label;\n}\n\n/**\n * Whether nothing sounds in a unit.\n *\n * A unit holds the notes that begin in it, and its weights are drawn from those\n * notes, so a unit with no onsets carries no melodic content and an all-zero\n * histogram: a tacet, a general pause, a drum break, a track that enters late.\n * Silence states nothing, restates nothing and is like nothing, so it takes no\n * label of its own — the fixed-length grid put a boundary there, the music did\n * not.\n */\nfunction isSilent(unit: Unit): boolean {\n return unit.notes.length === 0;\n}\n\n/**\n * How alike two units are, in [0, 1].\n *\n * Melody and harmony are both asked, because either alone is fooled: the same\n * tune over new chords is a variation and not a restatement, and the same\n * chords under a new tune is a different section of the same song.\n *\n * A figure needs two notes, so a unit holding fewer has no melody to compare.\n * Two such units are compared on their harmony alone; one of them against a\n * unit that does have a melody is not a restatement at all, and scores zero\n * however alike the two harmonies are — a bar reduced to a single held tone is\n * not a statement of the passage it was reduced from. The harmony is compared\n * as pitch-class weight over the unit, which is a shape rather than a series,\n * so it says nothing about the two units being the same length.\n */\nfunction unitSimilarity(a: Unit, b: Unit): number {\n const harmony = weightSimilarity(a.weights, b.weights);\n if (a.notes.length < 2 || b.notes.length < 2) {\n return a.notes.length === b.notes.length ? harmony : 0;\n }\n const melody = melodicSimilarity(a.notes, b.notes);\n return clamp01(MELODY_SHARE * melody + HARMONY_SHARE * harmony);\n}\n\n/**\n * Recover a piece's sections from what it repeats.\n *\n * The span is cut into equal units of `unitBars` bars, each unit is compared\n * with every unit before it, and a unit alike enough to an earlier one takes\n * that unit's label. Neighbouring units carrying the same label are then merged,\n * so an eight-bar section built of two like four-bar units is reported as one\n * section rather than two.\n *\n * Likeness is measured on the melodic line and on the harmonic content\n * together — the same tune under different harmony is a variation, not a\n * restatement — and the labels are letters in order of first appearance.\n * Nothing beyond \"this is a restatement of that\" is asserted: see\n * {@link FormSection.label}.\n *\n * A pickup belongs to the section it leads into, so material before the first\n * bar is folded into the opening unit rather than made a section of its own.\n *\n * @param notes The piece's notes, in any order. Notes that never sound are\n * dropped.\n * @param opts Analysis options; see {@link FormSectionOptions}.\n * @returns The sections, in time order.\n * @example\n * ```ts\n * import { sectionsFromNotes } from '@libraz/libcantus';\n * const bar = (pitch: number, at: number) => ({ pitch, startBeat: at, durationBeat: 4 });\n * const notes = [bar(60, 0), bar(64, 4), bar(67, 8), bar(60, 12)];\n * sectionsFromNotes(notes, { unitBars: 2 }).map((section) => section.label);\n * ```\n * @category Arrangement & Analysis\n */\nexport function sectionsFromNotes(\n notes: readonly NoteEvent[],\n opts: FormSectionOptions = {},\n): FormSection[] {\n const meters = resolveMeters(opts, 'section meters');\n assertNoteEvents(notes, 'section notes', {\n allowNonPositiveDuration: true,\n budget: opts.budget,\n });\n const sounding = notes.filter((note) => note.durationBeat > 0);\n // The pickup rule the whole analysis shares: a note a millibeat before the\n // downbeat is playing it, and only one a whole bar early opens a pickup.\n const firstSounding = firstSoundingBeat(sounding);\n const firstOnset = barGridStart(firstSounding, meters);\n const spanEnd = resolveSpanEnd(\n opts.totalBeats,\n firstOnset,\n latestSoundingEnd(sounding),\n 'section totalBeats',\n );\n if (sounding.length === 0) {\n // Nothing sounds, so there is no material to hear a form in. A section over\n // an empty span would name a stretch of silence as a statement.\n return [];\n }\n const threshold = opts.threshold ?? DEFAULT_THRESHOLD;\n assertRange(threshold, 0, 1, 'section threshold');\n const unitBars =\n opts.unitBars ??\n hypermeter(sounding, meters, { totalBeats: spanEnd, budget: opts.budget }).groupBars;\n assertPositiveInt(unitBars, 'section unitBars');\n\n const firstBar = barIndexAt(firstOnset, meters);\n const lastBar = lastBarOf(meters, firstOnset, spanEnd);\n // The units are counted from the bar the music starts in, not from bar 0: an\n // excerpt lifted from bar 9 has no bars 0 to 8 to divide. A pickup leads into\n // the first full bar rather than starting a unit of its own, so it is folded\n // into the opening one.\n const startBar = Math.max(0, firstBar);\n const unitCount = Math.max(1, Math.ceil((lastBar - startBar + 1) / unitBars));\n assertGenerationBudget(unitCount * unitCount, 'form section comparisons', opts.budget);\n assertGenerationBudget(unitCount * sounding.length, 'form section unit notes', opts.budget);\n\n const units: Unit[] = [];\n for (let index = 0; index < unitCount; index += 1) {\n const barStart = startBar + index * unitBars;\n const startBeat =\n index === 0 ? firstOnset : barPositionToBeat({ bar: barStart, beat: 0 }, meters);\n const endBeat =\n index === unitCount - 1\n ? spanEnd\n : barPositionToBeat({ bar: barStart + unitBars, beat: 0 }, meters);\n // The opening unit holds everything before its end: a note a millibeat\n // early sounds in it even though the grid starts on the downbeat.\n const fromBeat = index === 0 ? Math.min(firstSounding, startBeat) : startBeat;\n const unitNotes = sounding\n .filter(\n (note) => note.startBeat >= fromBeat - BEAT_EPS && note.startBeat < endBeat - BEAT_EPS,\n )\n .sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);\n units.push({\n startBeat,\n endBeat,\n notes: unitNotes,\n weights: windowWeights(unitNotes, startBeat, endBeat, meters).weights,\n });\n }\n\n // Comparing two units runs an edit distance over their notes, so what the\n // comparisons cost is the sum over pairs of the product of what each holds —\n // not the pair count, and not the pair count times the piece. A piece whose\n // notes gather in a few units keeps both of those inside the budget while the\n // distances themselves run for seconds, so the sum is charged before the\n // first of them is run.\n let comparisons = 0;\n let earlierNotes = 0;\n for (const unit of units) {\n comparisons += unit.notes.length * earlierNotes;\n earlierNotes += unit.notes.length;\n }\n assertGenerationBudget(comparisons, 'form section melody comparisons', opts.budget);\n\n // Label every unit, then merge the neighbours that agree. A silent unit takes\n // no label: it is carried by the section it follows, so the letters count\n // statements rather than stretches of the grid.\n const labels: (string | null)[] = [];\n const scores: number[] = [];\n let distinct = 0;\n for (let index = 0; index < units.length; index += 1) {\n const unit = units[index];\n if (unit === undefined) {\n continue;\n }\n if (isSilent(unit)) {\n labels.push(null);\n scores.push(scores[index - 1] ?? 1);\n continue;\n }\n let bestScore = -1;\n let bestIndex = -1;\n for (let earlier = 0; earlier < index; earlier += 1) {\n const other = units[earlier];\n if (other === undefined || isSilent(other)) {\n continue;\n }\n const score = unitSimilarity(unit, other);\n // Ties keep the earlier match, which is the statement being restated.\n if (score > bestScore) {\n bestScore = score;\n bestIndex = earlier;\n }\n }\n if (bestIndex >= 0 && bestScore >= threshold) {\n labels.push(labels[bestIndex] ?? labelAt(distinct));\n scores.push(bestScore);\n } else {\n labels.push(labelAt(distinct));\n scores.push(1);\n distinct += 1;\n }\n }\n\n const sections: FormSection[] = [];\n const firstByLabel = new Map<string, number>();\n for (let index = 0; index < units.length; ) {\n const label = labels[index] ?? null;\n if (label === null) {\n // Only reachable before anything sounds, which the unit grid rules out:\n // it starts at the first onset. Nothing to report over silence either way.\n index += 1;\n continue;\n }\n let end = index;\n let scoreSum = 0;\n let sounded = 0;\n while (end < units.length && labels[end] === label) {\n scoreSum += scores[end] ?? 1;\n sounded += 1;\n end += 1;\n }\n // Silence following the statement belongs to it — a section runs on until\n // the next one begins — but it also closes it: a restatement after a general\n // pause is heard as a second statement, not as more of the first.\n while (end < units.length && labels[end] === null) {\n end += 1;\n }\n const startBeat = units[index]?.startBeat ?? 0;\n const endBeat = units[end - 1]?.endBeat ?? spanEnd;\n // Counted on the unit grid the sections were merged on, not from the raw\n // start beat: a pickup was folded into the opening unit, so re-deriving the\n // length from its beat would hand the first section a bar the grid does not\n // hold and leave it off the multiple of `unitBars` a section is built from.\n const sectionFirstBar = startBar + index * unitBars;\n const sectionLastBar = Math.max(\n sectionFirstBar,\n Math.min(lastBar, startBar + end * unitBars - 1),\n );\n const sectionIndex = sections.length;\n const first = firstByLabel.get(label);\n if (first === undefined) {\n firstByLabel.set(label, sectionIndex);\n }\n const similarity = first === undefined ? 1 : clamp01(scoreSum / Math.max(1, sounded));\n sections.push({\n label,\n startBeat,\n endBeat,\n bars: sectionLastBar - sectionFirstBar + 1,\n firstOccurrence: first ?? sectionIndex,\n similarity,\n rationale:\n first === undefined\n ? `Section ${label}: material first heard at beat ${startBeat}`\n : `Section ${label}: ${Math.round(similarity * 100)}% alike the ${label} at beat ${\n sections[first]?.startBeat ?? 0\n }`,\n });\n index = end;\n }\n return sections;\n}\n","/**\n * Comparing two {@link ReferenceProfile}s dimension by dimension.\n *\n * The answer is never folded into one number: each field names the aspect it\n * measures, so \"a different song built the same way\" reads as high structural\n * fields beside a low surface one. Only `melody.surfaceSimilarity` reads the\n * motifs' intervals; every other field is invariant under transposition, tempo\n * and a change of motif intervals.\n */\n\nimport { assertOptions } from '../../core/validation/index.js';\nimport {\n cadenceSimilarity,\n contourSimilarity,\n densitySimilarity,\n durationSimilarity,\n functionSimilarity,\n keyPlanSimilarity,\n motifStructureSimilarity,\n onsetSimilarity,\n phraseLengthSimilarity,\n progressionSimilarity,\n registerSimilarity,\n sectionSequenceSimilarity,\n surfaceSimilarity,\n syncopationSimilarity,\n} from './measures.js';\nimport type { ReferenceProfile } from './types.js';\nimport { assertReferenceProfile } from './validate.js';\n\n/**\n * Options for {@link compareReferences}.\n *\n * @category Arrangement & Analysis\n */\nexport type ReferenceComparisonOptions = {\n /**\n * Upper bound on the cells of the progression alignment.\n *\n * @defaultValue {@link DEFAULT_GENERATION_BUDGET}\n */\n budget?: number;\n};\n\n/**\n * How alike two profiled pieces are, one field per aspect.\n *\n * Every value is in [0, 1] and symmetric in the two profiles. A value is\n * `null` when neither profile has anything to measure it on, and 0 when only\n * one does.\n *\n * @category Arrangement & Analysis\n */\nexport type ReferenceComparison = {\n /** Formal layout. */\n form: {\n /** Likeness of the section-label sequences. */\n sectionSequenceSimilarity: number | null;\n /** Likeness of the phrase-length sequences. */\n phraseLengthSimilarity: number | null;\n };\n /** Harmony. */\n harmony: {\n /** Likeness of the structural Roman-numeral progressions. */\n progressionSimilarity: number | null;\n /** Likeness of how long each harmonic function is held. */\n functionSimilarity: number | null;\n /** Likeness of the phrase-by-phrase cadence sequences. */\n cadenceSimilarity: number | null;\n /** Likeness of the key plans, read relative to each piece's first key. */\n keyPlanSimilarity: number | null;\n /** Likeness of where in the bar the chords change. */\n harmonicRhythmOnsetSimilarity: number | null;\n /** Likeness of how often the chords change. */\n harmonicRhythmDensitySimilarity: number | null;\n };\n /** Melody. */\n melody: {\n /** Likeness of the motifs' interval content: the one field that reads the surface. */\n surfaceSimilarity: number | null;\n /** Likeness of how the motifs derive from one another. */\n motifStructureSimilarity: number | null;\n /** Likeness of the phrases' melodic outlines, each scaled by its own range. */\n contourSimilarity: number | null;\n /** Likeness of the phrases' registers, each relative to its piece's melodic mean. */\n registerSimilarity: number | null;\n };\n /** Melodic rhythm. */\n rhythm: {\n /** Likeness of where in the bar the melody's onsets fall. */\n onsetSimilarity: number | null;\n /** Likeness of the melodies' onsets per bar. */\n densitySimilarity: number | null;\n /** Likeness of the melodies' inter-onset-interval distributions. */\n durationSimilarity: number | null;\n /** Likeness of the melodies' syncopation. */\n syncopationSimilarity: number | null;\n };\n /** The two most and the two least alike fields, by name. */\n rationale: string;\n};\n\n/** How many fields the rationale names at each end. */\nexport const RATIONALE_FIELDS = 2;\n\n/** A sentence naming the most and least alike of the measured fields. */\nfunction rationaleOf(comparison: Omit<ReferenceComparison, 'rationale'>): string {\n const measured: { name: string; value: number; order: number }[] = [];\n for (const group of [comparison.form, comparison.harmony, comparison.melody, comparison.rhythm]) {\n for (const [name, value] of Object.entries(group)) {\n if (value !== null) {\n measured.push({ name, value, order: measured.length });\n }\n }\n }\n if (measured.length === 0) {\n return 'Nothing to compare: neither profile has material for any field';\n }\n const describe = (entries: typeof measured): string =>\n entries.map((entry) => `${entry.name} ${Math.round(entry.value * 100)}%`).join(', ');\n const highest = [...measured].sort((x, y) => y.value - x.value || x.order - y.order);\n const lowest = [...measured].sort((x, y) => x.value - y.value || x.order - y.order);\n return (\n `Most alike: ${describe(highest.slice(0, RATIONALE_FIELDS))}; ` +\n `least alike: ${describe(lowest.slice(0, RATIONALE_FIELDS))}`\n );\n}\n\n/**\n * Compare two profiled pieces aspect by aspect: form, harmony, melody and\n * melodic rhythm.\n *\n * Sequences — sections, phrase lengths, cadences, key plans, structural\n * chords, phrase contours and registers — are aligned by a graded edit\n * distance; distributions — harmonic function, onset placement, inter-onset\n * intervals, motif derivations — by histogram intersection. The surface field\n * compares the motifs' interval cells, so a transposition of the same piece\n * reads 1 on every field. No aggregate is returned: which aspects agree is the\n * answer.\n *\n * @param a The first profile.\n * @param b The second profile.\n * @param opts The work budget.\n * @returns One likeness per aspect, and a rationale naming the extremes.\n * @throws {InvalidInputError} If either profile or the options cannot be read.\n * @throws {BudgetExceededError} If the progression alignment would exceed the budget.\n * @category Arrangement & Analysis\n */\nexport function compareReferences(\n a: ReferenceProfile,\n b: ReferenceProfile,\n opts?: ReferenceComparisonOptions,\n): ReferenceComparison {\n assertReferenceProfile(a, 'reference profile a');\n assertReferenceProfile(b, 'reference profile b');\n const { budget } = assertOptions(opts, 'compare options');\n const structural = (profile: ReferenceProfile) =>\n profile.harmony.chords.filter((chord) => chord.level === 'structural');\n const cadences = (profile: ReferenceProfile) =>\n profile.form.phrases.map((phrase) => phrase.cadence?.type ?? null);\n const phraseMelodies = (profile: ReferenceProfile) =>\n profile.form.phrases.map((phrase) => phrase.melody);\n const comparison: Omit<ReferenceComparison, 'rationale'> = {\n form: {\n sectionSequenceSimilarity: sectionSequenceSimilarity(\n a.form.sections.map((section) => section.label),\n b.form.sections.map((section) => section.label),\n ),\n phraseLengthSimilarity: phraseLengthSimilarity(\n a.form.phrases.map((phrase) => phrase.bars),\n b.form.phrases.map((phrase) => phrase.bars),\n ),\n },\n harmony: {\n progressionSimilarity: progressionSimilarity(structural(a), structural(b), budget),\n functionSimilarity: functionSimilarity(a.harmony.chords, b.harmony.chords),\n cadenceSimilarity: cadenceSimilarity(cadences(a), cadences(b)),\n keyPlanSimilarity: keyPlanSimilarity(\n a.harmony.keys.map((region) => region.key),\n b.harmony.keys.map((region) => region.key),\n ),\n harmonicRhythmOnsetSimilarity: onsetSimilarity(a.harmony.rhythm, b.harmony.rhythm),\n harmonicRhythmDensitySimilarity: densitySimilarity(a.harmony.rhythm, b.harmony.rhythm),\n },\n melody: {\n surfaceSimilarity: surfaceSimilarity(a.melody.motifs, b.melody.motifs),\n motifStructureSimilarity: motifStructureSimilarity(\n { graph: a.melody.graph, spanBeats: a.span.endBeat - a.span.startBeat },\n { graph: b.melody.graph, spanBeats: b.span.endBeat - b.span.startBeat },\n ),\n contourSimilarity: contourSimilarity(phraseMelodies(a), phraseMelodies(b)),\n registerSimilarity: registerSimilarity(\n { phrases: phraseMelodies(a), mean: a.melody.register?.mean ?? null },\n { phrases: phraseMelodies(b), mean: b.melody.register?.mean ?? null },\n ),\n },\n rhythm: {\n onsetSimilarity: onsetSimilarity(a.melody.rhythm, b.melody.rhythm),\n densitySimilarity: densitySimilarity(a.melody.rhythm, b.melody.rhythm),\n durationSimilarity: durationSimilarity(a.melody.rhythm, b.melody.rhythm),\n syncopationSimilarity: syncopationSimilarity(a.melody.rhythm, b.melody.rhythm),\n },\n };\n return { ...comparison, rationale: rationaleOf(comparison) };\n}\n","/**\n * Reading a piece into a {@link ReferenceProfile}.\n *\n * The profile is assembled from analyses the library already runs — chord and\n * key inference, hypermeter, phrases, sections, reduction, motifs and rhythm —\n * each run exactly once and all read against one span: every analysis is handed\n * the same `totalBeats`, so the phrases, sections and both rhythm readings\n * describe the same stretch of music. The harmonic reading enters as a bundle\n * of its own, so a caller already holding one (a score's kept chord analysis,\n * an arrangement's session) builds the profile from it without inferring the\n * harmony a second time.\n */\n\nimport type { MeterLike, MeterMap } from '../../core/meter/index.js';\nimport { resolveMeters } from '../../core/meter/index.js';\nimport { pitchClassOf } from '../../core/pitch/index.js';\nimport type { NoteEvent } from '../../core/types.js';\nimport { assertNoteEvents, assertOptions, assertRange } from '../../core/validation/index.js';\nimport type { KeyLike, ResolvedKey } from '../../theory/scale/index.js';\nimport { majorKey, resolveKey, scaleOf } from '../../theory/scale/index.js';\nimport { BEAT_EPS } from '../adjacency.js';\nimport type { FormSection } from '../form/index.js';\nimport {\n hypermeter,\n phrasesFromTimeline,\n sectionsFromNotes,\n structuralCadences,\n} from '../form/index.js';\nimport { barSpanOf, firstSoundingBeat } from '../form/internal.js';\nimport { functionOf } from '../functional/function.js';\nimport { chordToRoman } from '../functional/roman.js';\nimport { barGridStart, latestSoundingEnd } from '../grid.js';\nimport type { KeyRegion } from '../keys/index.js';\nimport { keyLookup, keyTimelineFromNotes, prevailingKeyOf } from '../keys/index.js';\nimport { melodicContour } from '../melody/contour.js';\nimport type { MotifGraph } from '../melody/graph.js';\nimport { motifGraph } from '../melody/graph.js';\nimport { orderedNotes } from '../melody/internal.js';\nimport { extractMotifs } from '../melody/motifs.js';\nimport { reduceProgression } from '../reduction/index.js';\nimport { analyzeRhythm, rhythmOver } from '../rhythm/index.js';\nimport type { ChordTimeline, ChordTimelineResult } from '../timeline/index.js';\nimport {\n assertChordTimeline,\n chordTimelineFromNotes,\n detectCadences,\n statedKeyRegions,\n} from '../timeline/index.js';\nimport { phraseMelody, weightedMean } from './internal.js';\nimport type {\n ReferenceCadence,\n ReferenceChord,\n ReferenceKeyRegion,\n ReferenceMelody,\n ReferenceMotif,\n ReferencePhrase,\n ReferenceProfile,\n} from './types.js';\nimport { REFERENCE_PROFILE_VERSION } from './types.js';\n\n/**\n * Options for {@link analyzeReference}.\n *\n * @category Arrangement & Analysis\n */\nexport type ReferenceProfileOptions = {\n /**\n * A single time signature held across the whole span, as sugar for a\n * one-element `meters`; defaults to 4/4. Giving both is an input error.\n *\n * @defaultValue `4/4`\n */\n ts?: MeterLike;\n /**\n * The meter as it changes over the span.\n *\n * @defaultValue 4/4 throughout\n */\n meters?: MeterLike;\n /**\n * The key, when it is already known: read as one key across the whole span,\n * as {@link chordTimelineFromNotes} reads it. Omit it to have the keys\n * searched for over time.\n *\n * @defaultValue the keys inferred from the notes over time\n */\n key?: KeyLike;\n /**\n * The harmony, given rather than inferred — the shape an arrangement's\n * `timeline` option takes. The key regions are still read from `key` or,\n * without one, from the notes.\n *\n * @defaultValue inferred from the notes\n */\n timeline?: ChordTimeline;\n /**\n * The melodic line. Omit it to read the top line of `notes`, where notes\n * struck together are one event and the highest of them stands for it. Only\n * notes struck together are collapsed: a lower note struck while a higher\n * one still sounds — a broken-chord or Alberti accompaniment under a held\n * melody note — joins the line, so pass the melody of such a texture.\n *\n * @defaultValue the top line of the notes\n */\n melody?: readonly NoteEvent[];\n /**\n * End of the analysed span in beats.\n *\n * @defaultValue the end of the last note or chord segment\n */\n totalBeats?: number;\n /**\n * Upper bound on the work each analysis may do.\n *\n * @defaultValue {@link DEFAULT_GENERATION_BUDGET}\n */\n budget?: number;\n};\n\n/**\n * The harmonic reading a profile is built on: the chord timeline, the key\n * regions it was read against, and the key held longest across them.\n *\n * {@link ChordTimelineResult} carries all three, and so does anything that\n * contains one, so a caller already holding a chord analysis hands it over as\n * it is.\n */\nexport type ReferenceReadings = Pick<ChordTimelineResult, 'timeline' | 'keys' | 'prevailingKey'>;\n\n/**\n * The span every analysis is read over: from the bar-grid start of the\n * earliest onset or chord segment, to `totalBeats` or else the later of the\n * last note's end and the last segment's end — the span\n * {@link phrasesFromTimeline} reads, so the phrases tile it exactly.\n */\nfunction spanOf(\n sounding: readonly NoteEvent[],\n timeline: ChordTimeline,\n meters: MeterMap,\n totalBeats: number | undefined,\n): { startBeat: number; endBeat: number } {\n const segments = timeline.segments;\n const firstSegment = segments[0];\n const lastSegment = segments[segments.length - 1];\n const firstOnset = firstSoundingBeat(sounding, firstSegment?.startBeat ?? 0);\n const startBeat = barGridStart(\n Math.min(firstOnset, firstSegment?.startBeat ?? firstOnset),\n meters,\n );\n const latestEnd = Math.max(\n lastSegment?.endBeat ?? Number.NEGATIVE_INFINITY,\n latestSoundingEnd(sounding, Number.NEGATIVE_INFINITY),\n );\n const notesEnd = Number.isFinite(latestEnd) ? latestEnd : 0;\n return { startBeat, endBeat: Math.max(startBeat, totalBeats ?? notesEnd) };\n}\n\n/** Whether two keys hold the same pitch classes, the identity `prevailingKeyOf` weighs by. */\nfunction sameKey(a: ResolvedKey, b: ResolvedKey): boolean {\n return (\n pitchClassOf(a.scale.rootPc) === pitchClassOf(b.scale.rootPc) &&\n a.scale.modeMask12 === b.scale.modeMask12\n );\n}\n\n/**\n * Index of the key region a chord is read against: the region holding its\n * midpoint, or else the first region in the prevailing key.\n */\nfunction keyIndexAt(\n regions: readonly ReferenceKeyRegion[],\n beat: number,\n prevailing: ResolvedKey,\n): number {\n const holding = regions.findIndex((region) => region.startBeat <= beat && beat < region.endBeat);\n if (holding >= 0) {\n return holding;\n }\n return Math.max(\n 0,\n regions.findIndex((region) => sameKey(region.key, prevailing)),\n );\n}\n\n/** Index of the section a beat falls in, or null when none holds it. */\nfunction sectionAt(sections: readonly FormSection[], beat: number): number | null {\n const index = sections.findIndex(\n (section) => beat >= section.startBeat - BEAT_EPS && beat < section.endBeat - BEAT_EPS,\n );\n return index < 0 ? null : index;\n}\n\n/**\n * The harmonic reading for {@link analyzeReference}: inferred from the notes,\n * or, when a timeline is given, that timeline read against the stated key or\n * the keys searched for in the notes.\n *\n * @param notes The notes the harmony is read from.\n * @param opts The options {@link analyzeReference} was given.\n * @returns The timeline, its key regions, and the prevailing key.\n */\nexport function readingsFor(\n notes: readonly NoteEvent[],\n opts: ReferenceProfileOptions = {},\n): ReferenceReadings {\n const asked = assertOptions(opts, 'reference options');\n const meters = resolveMeters(asked, 'reference meters');\n const budget = asked.budget;\n if (asked.timeline === undefined) {\n return chordTimelineFromNotes(notes, {\n meters,\n key: asked.key,\n totalBeats: asked.totalBeats,\n budget,\n });\n }\n const timeline = assertChordTimeline(asked.timeline, 'reference timeline');\n let keys: KeyRegion[];\n if (asked.key === undefined) {\n keys = keyTimelineFromNotes(notes, { meters, totalBeats: asked.totalBeats, budget });\n } else {\n // A stated key is taken as read across the span, as chord inference takes it.\n const sounding = assertNoteEvents(notes, 'reference notes', {\n allowNonPositiveDuration: true,\n budget,\n }).filter((note) => note.durationBeat > 0);\n const span = spanOf(sounding, timeline, meters, asked.totalBeats);\n keys = statedKeyRegions(resolveKey(asked.key), span.startBeat, span.endBeat);\n }\n return { timeline, keys, prevailingKey: prevailingKeyOf(keys) ?? resolveKey(majorKey(0)) };\n}\n\n/**\n * Build a profile from a harmonic reading already in hand.\n *\n * `readings` must have been read from the same notes, meter, key and span the\n * options name; nothing here re-derives the harmony to check.\n *\n * @param notes The notes the form, harmony and (without `opts.melody`) melody are read from.\n * @param readings The chord timeline and key regions to read the harmony from.\n * @param opts The meter, the melody, the span's end and the budget.\n * @returns The profile.\n */\nexport function referenceFromReadings(\n notes: readonly NoteEvent[],\n readings: ReferenceReadings,\n opts: ReferenceProfileOptions = {},\n): ReferenceProfile {\n const asked = assertOptions(opts, 'reference options');\n const meters = resolveMeters(asked, 'reference meters');\n const budget = asked.budget;\n const sounding = assertNoteEvents(notes, 'reference notes', {\n allowNonPositiveDuration: true,\n budget,\n }).filter((note) => note.durationBeat > 0);\n if (asked.totalBeats !== undefined) {\n assertRange(asked.totalBeats, 0, Number.MAX_SAFE_INTEGER, 'reference totalBeats');\n }\n // A pitch of -0 is pitch 0, which is all an integer pitch can hold.\n const line = orderedNotes(asked.melody ?? notes, 'reference melody', budget).map((note) =>\n Object.is(note.pitch, -0) ? { ...note, pitch: 0 } : note,\n );\n const { timeline, prevailingKey } = readings;\n\n const span = spanOf(sounding, timeline, meters, asked.totalBeats);\n const spanEnd = span.endBeat;\n // Named keys for numerals, functions and motif relations; pitch classes for\n // the analyses that take a key context.\n const keyAt = keyLookup(readings.keys, prevailingKey);\n const scaleAt = (beat: number) => scaleOf(keyAt(beat));\n\n const cadenceBeats = detectCadences(timeline, scaleAt).map((hit) => hit.atBeat);\n const grouping = hypermeter(sounding, meters, { cadenceBeats, totalBeats: spanEnd, budget });\n const phrases = phrasesFromTimeline(timeline, notes, {\n meters,\n key: scaleAt,\n hypermeter: grouping,\n totalBeats: spanEnd,\n budget,\n });\n const cadences = new Map<number, ReferenceCadence>();\n for (const closing of structuralCadences(phrases)) {\n const type = closing.cadence.type;\n if (type !== null) {\n cadences.set(closing.phraseIndex, {\n atBeat: closing.atBeat,\n type,\n weight: closing.weight,\n });\n }\n }\n const sections = sectionsFromNotes(notes, {\n meters,\n unitBars: grouping.groupBars,\n totalBeats: spanEnd,\n budget,\n });\n\n // A timeline with no key region under it — one given with no notes to key\n // it — is read in the prevailing key throughout, and the region records that.\n const keys: ReferenceKeyRegion[] =\n readings.keys.length === 0 && timeline.segments.length > 0\n ? [\n {\n startBeat: span.startBeat,\n endBeat: spanEnd,\n key: resolveKey(prevailingKey),\n confidence: 0,\n },\n ]\n : readings.keys.map((region) => ({\n startBeat: region.startBeat,\n endBeat: region.endBeat,\n key: resolveKey(region.key),\n confidence: region.confidence,\n }));\n const chords: ReferenceChord[] = reduceProgression(timeline, scaleAt).map((reduced) => {\n const index = keyIndexAt(keys, (reduced.startBeat + reduced.endBeat) / 2, prevailingKey);\n const key = (keys[index] as ReferenceKeyRegion).key;\n return {\n startBeat: reduced.startBeat,\n endBeat: reduced.endBeat,\n key: index,\n roman: chordToRoman(reduced.chord, key),\n function: functionOf(reduced.chord, key),\n level: reduced.level,\n };\n });\n const chordOnsets: NoteEvent[] = timeline.segments.map((segment) => ({\n pitch: 60,\n startBeat: segment.startBeat,\n durationBeat: segment.endBeat - segment.startBeat,\n }));\n const harmonyRhythm = analyzeRhythm(chordOnsets, { meters, totalBeats: spanEnd, budget });\n\n const motifs = extractMotifs(line, { budget });\n const graph: MotifGraph = motifGraph(line, motifs, { key: prevailingKey, budget });\n let register: ReferenceMelody['register'] = null;\n if (line.length > 0) {\n const pitches = line.map((note) => note.pitch);\n register = {\n low: Math.min(...pitches),\n high: Math.max(...pitches),\n mean: weightedMean(line),\n };\n }\n const lineMean = register?.mean ?? 0;\n const referenceMotifs: ReferenceMotif[] = motifs.map((motif) => {\n const first = motif.occurrences[0];\n return {\n intervals: motif.intervals,\n rhythm: motif.rhythm,\n unitBeats:\n motif.notes.length < 2\n ? 0\n : (motif.notes[1] as NoteEvent).startBeat - (motif.notes[0] as NoteEvent).startBeat,\n spanBeats: first === undefined ? 0 : first.endBeat - first.startBeat,\n occurrences: motif.occurrences.length,\n };\n });\n\n const referencePhrases: ReferencePhrase[] = phrases.map((phrase, index) => ({\n startBeat: phrase.startBeat,\n endBeat: phrase.endBeat,\n bars: barSpanOf(phrase.startBeat, phrase.endBeat, meters),\n section: sectionAt(sections, phrase.startBeat),\n confidence: phrase.confidence,\n cadence: cadences.get(index) ?? null,\n melody: phraseMelody(line, phrase.startBeat, phrase.endBeat, lineMean, meters, budget),\n motifNodes: graph.nodes.flatMap((node, nodeIndex) =>\n node.startBeat >= phrase.startBeat - BEAT_EPS && node.startBeat < phrase.endBeat - BEAT_EPS\n ? [nodeIndex]\n : [],\n ),\n }));\n\n return {\n profileVersion: REFERENCE_PROFILE_VERSION,\n span: {\n startBeat: span.startBeat,\n endBeat: spanEnd,\n bars: barSpanOf(span.startBeat, spanEnd, meters),\n },\n meters,\n form: {\n hypermeter: { groupBars: grouping.groupBars, confidence: grouping.confidence },\n sections,\n phrases: referencePhrases,\n },\n harmony: { keys, chords, rhythm: harmonyRhythm },\n melody: {\n register,\n shape: line.length < 2 ? null : melodicContour(line).shape,\n motifs: referenceMotifs,\n graph,\n rhythm: rhythmOver(line, { meters, totalBeats: spanEnd, budget }, span.startBeat),\n },\n };\n}\n\n/**\n * Read a piece's structure into a {@link ReferenceProfile}: its form, phrase\n * shapes, harmonic function and rhythm, motif derivations and melodic rhythm,\n * without the notes themselves.\n *\n * The melody is the `melody` option when given, and otherwise the top line of\n * the notes — notes struck together read as one event, the highest standing\n * for it. A lower note struck while a higher one still sounds stays in that\n * line, so a part whose accompaniment moves under a held melody note is read\n * as its melody only when the melody is passed. The harmony\n * is the `timeline` option when given, and otherwise inferred from all the\n * notes. The motif search expects quantized onsets.\n *\n * The sections are cut on the hyperbar grouping the phrase reading settles\n * on, which weighs where the harmony cadences; `sectionsFromNotes` alone\n * groups without the cadences, so the two can differ.\n *\n * @param notes The piece's notes, in any order. Notes that never sound are dropped.\n * @param opts The meter, key, harmony, melody, span end and budget; see\n * {@link ReferenceProfileOptions}.\n * @returns The profile.\n * @throws {InvalidInputError} If the notes or an option cannot be read.\n * @throws {BudgetExceededError} If an analysis would exceed the budget.\n * @example\n * ```ts\n * import { analyzeReference } from '@libraz/libcantus';\n * const triad = (pitches: number[], startBeat: number) =>\n * pitches.map((pitch) => ({ pitch, startBeat, durationBeat: 4 }));\n * const notes = [\n * ...triad([60, 64, 67], 0),\n * ...triad([65, 69, 72], 4),\n * ...triad([67, 71, 74], 8),\n * ...triad([60, 64, 67], 12),\n * ];\n * const profile = analyzeReference(notes, { key: 'C major' });\n * profile.harmony.chords.map((chord) => chord.roman); // ['I', 'IV', 'V', 'I']\n * profile.span.bars; // 4\n * ```\n * @category Arrangement & Analysis\n */\nexport function analyzeReference(\n notes: readonly NoteEvent[],\n opts?: ReferenceProfileOptions,\n): ReferenceProfile {\n return referenceFromReadings(notes, readingsFor(notes, opts), opts);\n}\n"]}
|